WIP: A simple cli for daily tangled use cases and AI integration. This is for my personal use right now, but happy if others get mileage from it! :)
1import type { TangledApiClient } from '../lib/api-client.js';
2
3/**
4 * Parse an AT-URI into its components
5 * @param uri - AT-URI string (e.g., "at://did:plc:abc/collection/rkey")
6 * @returns Parsed components or null if invalid
7 */
8export function parseAtUri(uri: string): {
9 did: string;
10 collection: string;
11 rkey?: string;
12} | null {
13 // AT-URI format: at://did:method:identifier/collection[/rkey]
14 const match = uri.match(
15 /^at:\/\/(did:[a-z]+:[a-zA-Z0-9._:%-]+)\/([a-zA-Z0-9._-]+(?:\.[a-zA-Z0-9._-]+)*)(?:\/([a-zA-Z0-9._-]+))?$/
16 );
17
18 if (!match) {
19 return null;
20 }
21
22 const [, did, collection, rkey] = match;
23 return {
24 did,
25 collection,
26 ...(rkey && { rkey }),
27 };
28}
29
30/**
31 * Resolve a handle to a DID using the AT Protocol identity resolution
32 * @param handle - Handle string (e.g., "mark.bsky.social" or "@mark.bsky.social")
33 * @param client - Authenticated API client
34 * @returns DID string (e.g., "did:plc:abc123")
35 * @throws Error if handle cannot be resolved
36 */
37export async function resolveHandleToDid(
38 handle: string,
39 client: TangledApiClient
40): Promise<string> {
41 // Strip leading @ if present
42 const cleanHandle = handle.startsWith('@') ? handle.slice(1) : handle;
43
44 try {
45 const response = await client.getAgent().com.atproto.identity.resolveHandle({
46 handle: cleanHandle,
47 });
48
49 if (!response.data.did) {
50 throw new Error(`No DID found for handle: ${cleanHandle}`);
51 }
52
53 return response.data.did;
54 } catch (error) {
55 if (error instanceof Error) {
56 throw new Error(`Failed to resolve handle '${cleanHandle}': ${error.message}`);
57 }
58 throw new Error(`Failed to resolve handle '${cleanHandle}': Unknown error`);
59 }
60}
61
62/**
63 * Build a repository AT-URI from owner and repository name
64 * @param ownerDidOrHandle - DID (e.g., "did:plc:abc") or handle (e.g., "mark.bsky.social")
65 * @param repoName - Repository name
66 * @param client - Authenticated API client
67 * @returns AT-URI string (e.g., "at://did:plc:abc/sh.tangled.repo/3mef23waqwq22")
68 * @throws Error if repository not found
69 */
70export async function buildRepoAtUri(
71 ownerDidOrHandle: string,
72 repoName: string,
73 client: TangledApiClient
74): Promise<string> {
75 // Resolve owner to DID
76 const isDid = ownerDidOrHandle.startsWith('did:');
77 const did = isDid ? ownerDidOrHandle : await resolveHandleToDid(ownerDidOrHandle, client);
78
79 try {
80 // Query for sh.tangled.repo records
81 const response = await client.getAgent().com.atproto.repo.listRecords({
82 repo: did,
83 collection: 'sh.tangled.repo',
84 limit: 100, // Reasonable limit for most users
85 });
86
87 // Find the record matching the repo name
88 const repoRecord = response.data.records.find((record) => {
89 const recordData = record.value as { name?: string };
90 return recordData.name === repoName;
91 });
92
93 if (!repoRecord) {
94 throw new Error(`Repository '${repoName}' not found for ${ownerDidOrHandle}`);
95 }
96
97 // Return the record's URI (which includes the correct rkey)
98 return repoRecord.uri;
99 } catch (error) {
100 if (error instanceof Error) {
101 throw new Error(`Failed to resolve repository AT-URI: ${error.message}`);
102 }
103 throw new Error('Failed to resolve repository AT-URI: Unknown error');
104 }
105}