@agentuity/cli 0.0.67 → 0.0.69

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.
Files changed (63) hide show
  1. package/bin/cli.ts +20 -8
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +16 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cmd/ai/prompt/agent.d.ts.map +1 -1
  6. package/dist/cmd/ai/prompt/agent.js +24 -25
  7. package/dist/cmd/ai/prompt/agent.js.map +1 -1
  8. package/dist/cmd/ai/prompt/api.d.ts.map +1 -1
  9. package/dist/cmd/ai/prompt/api.js +12 -9
  10. package/dist/cmd/ai/prompt/api.js.map +1 -1
  11. package/dist/cmd/build/ast.js +4 -4
  12. package/dist/cmd/build/ast.js.map +1 -1
  13. package/dist/cmd/build/bundler.js +2 -2
  14. package/dist/cmd/build/bundler.js.map +1 -1
  15. package/dist/cmd/build/plugin.d.ts.map +1 -1
  16. package/dist/cmd/build/plugin.js +9 -14
  17. package/dist/cmd/build/plugin.js.map +1 -1
  18. package/dist/cmd/dev/index.js +2 -2
  19. package/dist/cmd/dev/index.js.map +1 -1
  20. package/dist/cmd/dev/templates.d.ts.map +1 -1
  21. package/dist/cmd/dev/templates.js +10 -3
  22. package/dist/cmd/dev/templates.js.map +1 -1
  23. package/dist/cmd/project/download.js +10 -10
  24. package/dist/cmd/project/download.js.map +1 -1
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +48 -87
  27. package/dist/config.js.map +1 -1
  28. package/dist/keychain.d.ts +31 -0
  29. package/dist/keychain.d.ts.map +1 -0
  30. package/dist/keychain.js +135 -0
  31. package/dist/keychain.js.map +1 -0
  32. package/dist/utils/detectSubagent.d.ts +1 -1
  33. package/dist/utils/detectSubagent.js +3 -3
  34. package/dist/utils/detectSubagent.js.map +1 -1
  35. package/package.json +3 -3
  36. package/src/cli.ts +19 -0
  37. package/src/cmd/ai/prompt/agent.ts +24 -25
  38. package/src/cmd/ai/prompt/api.ts +12 -9
  39. package/src/cmd/build/ast.ts +4 -4
  40. package/src/cmd/build/bundler.ts +2 -2
  41. package/src/cmd/build/plugin.ts +9 -14
  42. package/src/cmd/dev/index.ts +2 -2
  43. package/src/cmd/dev/templates.ts +10 -3
  44. package/src/cmd/project/download.ts +10 -10
  45. package/src/config.ts +54 -97
  46. package/src/keychain.ts +176 -0
  47. package/src/utils/detectSubagent.ts +3 -3
  48. package/dist/cmd/build/ast.test.d.ts +0 -2
  49. package/dist/cmd/build/ast.test.d.ts.map +0 -1
  50. package/dist/cmd/build/ast.test.js +0 -339
  51. package/dist/cmd/build/ast.test.js.map +0 -1
  52. package/dist/cmd/build/fix-duplicate-exports.test.d.ts +0 -2
  53. package/dist/cmd/build/fix-duplicate-exports.test.d.ts.map +0 -1
  54. package/dist/cmd/build/fix-duplicate-exports.test.js +0 -300
  55. package/dist/cmd/build/fix-duplicate-exports.test.js.map +0 -1
  56. package/dist/crypto/box.test.d.ts +0 -2
  57. package/dist/crypto/box.test.d.ts.map +0 -1
  58. package/dist/crypto/box.test.js +0 -317
  59. package/dist/crypto/box.test.js.map +0 -1
  60. package/dist/env-util.test.d.ts +0 -2
  61. package/dist/env-util.test.d.ts.map +0 -1
  62. package/dist/env-util.test.js +0 -146
  63. package/dist/env-util.test.js.map +0 -1
package/src/config.ts CHANGED
@@ -15,6 +15,12 @@ import JSON5 from 'json5';
15
15
  import type { Config, Profile, AuthData } from './types';
16
16
  import { ConfigSchema, ProjectSchema } from './types';
17
17
  import * as tui from './tui';
18
+ import {
19
+ isMacOS,
20
+ saveAuthToKeychain,
21
+ getAuthFromKeychain,
22
+ deleteAuthFromKeychain,
23
+ } from './keychain';
18
24
 
19
25
  export const defaultProfileName = 'production';
20
26
 
@@ -233,11 +239,7 @@ export async function saveConfig(config: Config, customPath?: string): Promise<v
233
239
  const configPath = customPath || (await getProfile());
234
240
  await ensureConfigDir();
235
241
 
236
- // Strip auth from config before saving (never store auth in config file)
237
- const configToSave = { ...config };
238
- delete configToSave.auth;
239
-
240
- const content = formatYAML(configToSave);
242
+ const content = formatYAML(config);
241
243
  await writeFile(configPath, content + '\n', { mode: 0o600 });
242
244
  // Ensure existing files get correct permissions on upgrade
243
245
  await chmod(configPath, 0o600);
@@ -264,47 +266,45 @@ export async function saveAuth(auth: AuthData): Promise<void> {
264
266
  expires: auth.expires.getTime(),
265
267
  };
266
268
 
267
- // Try to store in Bun secrets API
268
- try {
269
- await Bun.secrets.set({
270
- service: `agentuity.cli.${profileName}`,
271
- name: 'auth',
272
- value: JSON.stringify(authData),
273
- });
274
-
275
- // Successfully stored in secrets, ensure auth is removed from config file
276
- if (config.auth) {
277
- delete config.auth;
278
- await saveConfig(config);
279
- }
280
- } catch {
281
- // Bun.secrets API not available or failed, fallback to config file
282
- config.auth = authData;
283
- config.preferences = config.preferences || {};
284
- (config.preferences as Record<string, unknown>).orgId = '';
269
+ // On macOS, store in Keychain for better security
270
+ if (isMacOS()) {
271
+ try {
272
+ await saveAuthToKeychain(profileName, authData);
285
273
 
286
- // Save with auth in config as fallback (saveConfig will try to strip it but we're setting it here)
287
- const configPath = await getProfile();
288
- await ensureConfigDir();
289
- const content = formatYAML(config);
290
- await writeFile(configPath, content + '\n', { mode: 0o600 });
291
- await chmod(configPath, 0o600);
292
- cachedConfig = config;
274
+ // Successfully stored in keychain, remove from config if present
275
+ if (config.auth) {
276
+ delete config.auth;
277
+ await saveConfig(config);
278
+ }
279
+ return;
280
+ } catch (error) {
281
+ // Keychain failed, fall back to config file
282
+ console.warn('Failed to store auth in keychain, falling back to config file:', error);
283
+ }
293
284
  }
285
+
286
+ // Store in config file (non-macOS or keychain failed)
287
+ config.auth = authData;
288
+ config.preferences = config.preferences || {};
289
+ (config.preferences as Record<string, unknown>).orgId = '';
290
+
291
+ await saveConfig(config);
294
292
  }
295
293
 
296
294
  export async function clearAuth(): Promise<void> {
297
295
  const config = await getOrInitConfig();
298
296
  const profileName = config.name || defaultProfileName;
299
297
 
300
- // Try to delete from Bun secrets API
301
- try {
302
- await Bun.secrets.delete({ service: `agentuity.cli.${profileName}`, name: 'auth' });
303
- } catch {
304
- // Bun.secrets API not available or failed
298
+ // On macOS, clear from Keychain
299
+ if (isMacOS()) {
300
+ try {
301
+ await deleteAuthFromKeychain(profileName);
302
+ } catch {
303
+ // Ignore errors - keychain entry may not exist
304
+ }
305
305
  }
306
306
 
307
- // Clear auth from config file (for backwards compatibility)
307
+ // Also clear from config file (for backwards compatibility)
308
308
  if (config.auth) {
309
309
  delete config.auth;
310
310
  config.preferences = config.preferences || {};
@@ -328,59 +328,11 @@ export async function saveOrgId(orgId: string): Promise<void> {
328
328
  await saveConfig(config);
329
329
  }
330
330
 
331
- async function migrateAuthToSecrets(
332
- config: Config,
333
- profileName: string,
334
- auth: { api_key: string; user_id: string; expires: number }
335
- ): Promise<boolean> {
336
- try {
337
- const authData = {
338
- api_key: auth.api_key,
339
- user_id: auth.user_id,
340
- expires: auth.expires,
341
- };
342
-
343
- await Bun.secrets.set({
344
- service: `agentuity.cli.${profileName}`,
345
- name: 'auth',
346
- value: JSON.stringify(authData),
347
- });
348
-
349
- // Successfully migrated, remove from config file
350
- delete config.auth;
351
- await saveConfig(config);
352
-
353
- return true;
354
- } catch {
355
- // Migration failed, leave in config file
356
- return false;
357
- }
358
- }
359
-
360
331
  export async function getAuth(): Promise<AuthData | null> {
361
332
  const config = await loadConfig();
362
333
  const profileName = config?.name || defaultProfileName;
363
334
 
364
- // Priority 1: Try Bun secrets API first (most secure)
365
- try {
366
- const authJson = await Bun.secrets.get({
367
- service: `agentuity.cli.${profileName}`,
368
- name: 'auth',
369
- });
370
-
371
- if (authJson) {
372
- const auth = JSON.parse(authJson) as { api_key: string; user_id: string; expires: number };
373
- return {
374
- apiKey: auth.api_key,
375
- userId: auth.user_id,
376
- expires: new Date(auth.expires),
377
- };
378
- }
379
- } catch {
380
- // Bun.secrets API not available or failed, fallback to other methods
381
- }
382
-
383
- // Priority 2: Allow automated login from environment variables
335
+ // Priority 1: Allow automated login from environment variables
384
336
  if (process.env.AGENTUITY_CLI_API_KEY && process.env.AGENTUITY_USER_ID) {
385
337
  return {
386
338
  apiKey: process.env.AGENTUITY_CLI_API_KEY,
@@ -389,7 +341,23 @@ export async function getAuth(): Promise<AuthData | null> {
389
341
  };
390
342
  }
391
343
 
392
- // Priority 3: Fallback to config file (backwards compatibility + migration)
344
+ // Priority 2: On macOS, try to read from Keychain
345
+ if (isMacOS()) {
346
+ try {
347
+ const keychainAuth = await getAuthFromKeychain(profileName);
348
+ if (keychainAuth) {
349
+ return {
350
+ apiKey: keychainAuth.api_key,
351
+ userId: keychainAuth.user_id,
352
+ expires: new Date(keychainAuth.expires),
353
+ };
354
+ }
355
+ } catch {
356
+ // Keychain read failed, fall through to config file
357
+ }
358
+ }
359
+
360
+ // Priority 3: Read from config file (non-macOS or keychain failed)
393
361
  if (!config) return null;
394
362
  const auth = config.auth as { api_key?: string; user_id?: string; expires?: number } | undefined;
395
363
 
@@ -397,18 +365,7 @@ export async function getAuth(): Promise<AuthData | null> {
397
365
  return null;
398
366
  }
399
367
 
400
- // Check if token is unexpired
401
368
  const expiresDate = new Date(auth.expires || 0);
402
- const isExpired = expiresDate.getTime() <= Date.now();
403
-
404
- // If unexpired, attempt to migrate to Bun.secrets
405
- if (!isExpired) {
406
- await migrateAuthToSecrets(config, profileName, {
407
- api_key: auth.api_key,
408
- user_id: auth.user_id,
409
- expires: auth.expires || 0,
410
- });
411
- }
412
369
 
413
370
  return {
414
371
  apiKey: auth.api_key,
@@ -0,0 +1,176 @@
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(['security', 'delete-generic-password', '-s', service, '-a', account], {
112
+ stderr: 'ignore',
113
+ });
114
+ await del.exited;
115
+
116
+ const add = Bun.spawn([
117
+ 'security',
118
+ 'add-generic-password',
119
+ '-s',
120
+ service,
121
+ '-a',
122
+ account,
123
+ '-w',
124
+ b64,
125
+ '-U',
126
+ ]);
127
+ await add.exited;
128
+ }
129
+
130
+ /**
131
+ * Retrieve auth data from macOS Keychain
132
+ */
133
+ export async function getAuthFromKeychain(
134
+ profileName: string
135
+ ): Promise<{ api_key: string; user_id: string; expires: number } | null> {
136
+ const service = `${SERVICE_PREFIX}.${profileName}`;
137
+ const account = 'auth-token';
138
+
139
+ try {
140
+ // Get the encrypted auth data
141
+ const find = Bun.spawn(
142
+ ['security', 'find-generic-password', '-s', service, '-a', account, '-w'],
143
+ { stderr: 'ignore' }
144
+ );
145
+
146
+ const stdout = await new Response(find.stdout).text();
147
+ if (stdout.length === 0) {
148
+ return null;
149
+ }
150
+
151
+ const b64 = stdout.trim();
152
+ const encrypted = Uint8Array.from(Buffer.from(b64, 'base64'));
153
+
154
+ // Get the encryption key
155
+ const key = await ensureEncryptionKey(service);
156
+
157
+ // Decrypt the auth data
158
+ const json = await decrypt(encrypted, key);
159
+ return JSON.parse(json);
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Delete auth data from macOS Keychain
167
+ */
168
+ export async function deleteAuthFromKeychain(profileName: string): Promise<void> {
169
+ const service = `${SERVICE_PREFIX}.${profileName}`;
170
+ const account = 'auth-token';
171
+
172
+ const del = Bun.spawn(['security', 'delete-generic-password', '-s', service, '-a', account], {
173
+ stderr: 'ignore',
174
+ });
175
+ await del.exited;
176
+ }
@@ -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 };
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=ast.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ast.test.d.ts","sourceRoot":"","sources":["../../../src/cmd/build/ast.test.ts"],"names":[],"mappings":""}