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 { AtpSessionData } from '@atproto/api';
2import { AsyncEntry } from '@napi-rs/keyring';
3
4const SERVICE_NAME = 'tangled-cli';
5
6export interface SessionMetadata {
7 handle: string;
8 did: string;
9 pds: string;
10 lastUsed: string; // ISO timestamp
11}
12
13/**
14 * Store session data in OS keychain
15 * @param sessionData - Session data from AtpAgent
16 */
17export async function saveSession(sessionData: AtpSessionData): Promise<void> {
18 try {
19 const accountId = sessionData.did || sessionData.handle;
20 if (!accountId) {
21 throw new Error('Session data must include DID or handle');
22 }
23
24 const serialized = JSON.stringify(sessionData);
25 const entry = new AsyncEntry(SERVICE_NAME, accountId);
26 await entry.setPassword(serialized);
27 } catch (error) {
28 throw new Error(
29 `Failed to save session to keychain: ${error instanceof Error ? error.message : 'Unknown error'}`
30 );
31 }
32}
33
34/**
35 * Retrieve session data from OS keychain
36 * @param accountId - User's DID or handle
37 */
38export async function loadSession(accountId: string): Promise<AtpSessionData | null> {
39 try {
40 const entry = new AsyncEntry(SERVICE_NAME, accountId);
41 const serialized = await entry.getPassword();
42 if (!serialized) {
43 return null;
44 }
45 return JSON.parse(serialized) as AtpSessionData;
46 } catch (error) {
47 throw new Error(
48 `Failed to load session from keychain: ${error instanceof Error ? error.message : 'Unknown error'}`
49 );
50 }
51}
52
53/**
54 * Delete session from OS keychain
55 * @param accountId - User's DID or handle
56 */
57export async function deleteSession(accountId: string): Promise<boolean> {
58 try {
59 const entry = new AsyncEntry(SERVICE_NAME, accountId);
60 return await entry.deleteCredential();
61 } catch (error) {
62 throw new Error(
63 `Failed to delete session from keychain: ${error instanceof Error ? error.message : 'Unknown error'}`
64 );
65 }
66}
67
68/**
69 * Store metadata about current session for CLI to track active user
70 * Uses a special "current" account in keychain
71 */
72export async function saveCurrentSessionMetadata(metadata: SessionMetadata): Promise<void> {
73 const serialized = JSON.stringify(metadata);
74 const entry = new AsyncEntry(SERVICE_NAME, 'current-session-metadata');
75 await entry.setPassword(serialized);
76}
77
78/**
79 * Get metadata about current active session
80 */
81export async function getCurrentSessionMetadata(): Promise<SessionMetadata | null> {
82 const entry = new AsyncEntry(SERVICE_NAME, 'current-session-metadata');
83 const serialized = await entry.getPassword();
84 if (!serialized) {
85 return null;
86 }
87 return JSON.parse(serialized) as SessionMetadata;
88}
89
90/**
91 * Clear current session metadata
92 */
93export async function clearCurrentSessionMetadata(): Promise<void> {
94 const entry = new AsyncEntry(SERVICE_NAME, 'current-session-metadata');
95 await entry.deleteCredential();
96}