@agentuity/cli 0.0.66 → 0.0.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,178 @@
1
+ /**
2
+ * macOS Keychain integration for secure auth token storage
3
+ *
4
+ * Stores auth tokens encrypted in the macOS Keychain using a per-device AES-256 key.
5
+ * No user prompts required - fully automatic and secure.
6
+ */
7
+
8
+ const SERVICE_PREFIX = "com.agentuity.cli";
9
+ const KEY_ACCOUNT = "aes-encryption-key";
10
+
11
+ /**
12
+ * Check if we're running on macOS
13
+ */
14
+ export function isMacOS(): boolean {
15
+ return process.platform === 'darwin';
16
+ }
17
+
18
+ /**
19
+ * Get or create an AES encryption key stored in the macOS Keychain
20
+ */
21
+ async function ensureEncryptionKey(service: string): Promise<Uint8Array> {
22
+ // Try to read existing key
23
+ const find = Bun.spawn(
24
+ ['security', 'find-generic-password', '-s', service, '-a', KEY_ACCOUNT, '-w'],
25
+ { stderr: 'ignore' }
26
+ );
27
+
28
+ const stdout = await new Response(find.stdout).text();
29
+
30
+ if (stdout.length > 0) {
31
+ const b64 = stdout.trim();
32
+ return Uint8Array.from(Buffer.from(b64, 'base64'));
33
+ }
34
+
35
+ // Create a new 32-byte (256-bit) AES key
36
+ const key = crypto.getRandomValues(new Uint8Array(32));
37
+ const b64 = Buffer.from(key).toString('base64');
38
+
39
+ // Store in macOS Keychain (no user prompts with -U flag)
40
+ const add = Bun.spawn([
41
+ 'security',
42
+ 'add-generic-password',
43
+ '-s',
44
+ service,
45
+ '-a',
46
+ KEY_ACCOUNT,
47
+ '-w',
48
+ b64,
49
+ '-U', // Update without user confirmation
50
+ ]);
51
+ await add.exited;
52
+
53
+ return key;
54
+ }
55
+
56
+ /**
57
+ * Encrypt data using AES-256-GCM
58
+ */
59
+ async function encrypt(data: string, keyBytes: Uint8Array): Promise<Uint8Array> {
60
+ const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', false, ['encrypt']);
61
+
62
+ const iv = crypto.getRandomValues(new Uint8Array(12));
63
+ const plaintext = new TextEncoder().encode(data);
64
+
65
+ const ciphertext = new Uint8Array(
66
+ await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext)
67
+ );
68
+
69
+ // Combine IV + ciphertext
70
+ const combined = new Uint8Array(iv.length + ciphertext.length);
71
+ combined.set(iv, 0);
72
+ combined.set(ciphertext, iv.length);
73
+
74
+ return combined;
75
+ }
76
+
77
+ /**
78
+ * Decrypt data using AES-256-GCM
79
+ */
80
+ async function decrypt(combined: Uint8Array, keyBytes: Uint8Array): Promise<string> {
81
+ const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', false, ['decrypt']);
82
+
83
+ const iv = combined.slice(0, 12);
84
+ const ciphertext = combined.slice(12);
85
+
86
+ const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
87
+
88
+ return new TextDecoder().decode(plaintext);
89
+ }
90
+
91
+ /**
92
+ * Store auth data in macOS Keychain
93
+ */
94
+ export async function saveAuthToKeychain(
95
+ profileName: string,
96
+ authData: { api_key: string; user_id: string; expires: number }
97
+ ): Promise<void> {
98
+ const service = `${SERVICE_PREFIX}.${profileName}`;
99
+ const account = 'auth-token';
100
+
101
+ // Get or create encryption key
102
+ const key = await ensureEncryptionKey(service);
103
+
104
+ // Encrypt the auth data
105
+ const json = JSON.stringify(authData);
106
+ const encrypted = await encrypt(json, key);
107
+ const b64 = Buffer.from(encrypted).toString('base64');
108
+
109
+ // Store encrypted auth in keychain
110
+ // First try to delete if exists, then add
111
+ const del = Bun.spawn(
112
+ ['security', 'delete-generic-password', '-s', service, '-a', account],
113
+ { stderr: 'ignore' }
114
+ );
115
+ await del.exited;
116
+
117
+ const add = Bun.spawn([
118
+ 'security',
119
+ 'add-generic-password',
120
+ '-s',
121
+ service,
122
+ '-a',
123
+ account,
124
+ '-w',
125
+ b64,
126
+ '-U',
127
+ ]);
128
+ await add.exited;
129
+ }
130
+
131
+ /**
132
+ * Retrieve auth data from macOS Keychain
133
+ */
134
+ export async function getAuthFromKeychain(
135
+ profileName: string
136
+ ): Promise<{ api_key: string; user_id: string; expires: number } | null> {
137
+ const service = `${SERVICE_PREFIX}.${profileName}`;
138
+ const account = 'auth-token';
139
+
140
+ try {
141
+ // Get the encrypted auth data
142
+ const find = Bun.spawn(
143
+ ['security', 'find-generic-password', '-s', service, '-a', account, '-w'],
144
+ { stderr: 'ignore' }
145
+ );
146
+
147
+ const stdout = await new Response(find.stdout).text();
148
+ if (stdout.length === 0) {
149
+ return null;
150
+ }
151
+
152
+ const b64 = stdout.trim();
153
+ const encrypted = Uint8Array.from(Buffer.from(b64, 'base64'));
154
+
155
+ // Get the encryption key
156
+ const key = await ensureEncryptionKey(service);
157
+
158
+ // Decrypt the auth data
159
+ const json = await decrypt(encrypted, key);
160
+ return JSON.parse(json);
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Delete auth data from macOS Keychain
168
+ */
169
+ export async function deleteAuthFromKeychain(profileName: string): Promise<void> {
170
+ const service = `${SERVICE_PREFIX}.${profileName}`;
171
+ const account = 'auth-token';
172
+
173
+ const del = Bun.spawn(
174
+ ['security', 'delete-generic-password', '-s', service, '-a', account],
175
+ { stderr: 'ignore' }
176
+ );
177
+ await del.exited;
178
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Detects if a file path represents a subagent based on path structure.
3
3
  *
4
- * Subagents follow the pattern: agents/parent/child/agent.ts or agents/parent/child/route.ts
4
+ * Subagents follow the pattern: agent/parent/child/agent.ts or agent/parent/child/route.ts
5
5
  * The path structure is currently hardcoded to 4 segments but could be made configurable later.
6
6
  *
7
7
  * @param filePath - The file path to analyze (can include leading './')
@@ -22,9 +22,9 @@ export function detectSubagent(
22
22
  // Strip leading './' and split into parts, filtering out empty segments
23
23
  const pathParts = normalizedPath.replace(/^\.\//, '').split('/').filter(Boolean);
24
24
 
25
- // Path structure assumption: ['agents', 'parent', 'child', 'agent.ts' | 'route.ts' | 'route']
25
+ // Path structure assumption: ['agent', 'parent', 'child', 'agent.ts' | 'route.ts' | 'route']
26
26
  // Currently hardcoded to 4 segments - consider making configurable in the future
27
- const isSubagent = pathParts.length === 4 && pathParts[0] === 'agents';
27
+ const isSubagent = pathParts.length === 4 && pathParts[0] === 'agent';
28
28
  const parentName = isSubagent ? pathParts[1] : null;
29
29
 
30
30
  return { isSubagent, parentName };