@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,131 @@
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
+ const SERVICE_PREFIX = "com.agentuity.cli";
8
+ const KEY_ACCOUNT = "aes-encryption-key";
9
+ /**
10
+ * Check if we're running on macOS
11
+ */
12
+ export function isMacOS() {
13
+ return process.platform === 'darwin';
14
+ }
15
+ /**
16
+ * Get or create an AES encryption key stored in the macOS Keychain
17
+ */
18
+ async function ensureEncryptionKey(service) {
19
+ // Try to read existing key
20
+ const find = Bun.spawn(['security', 'find-generic-password', '-s', service, '-a', KEY_ACCOUNT, '-w'], { stderr: 'ignore' });
21
+ const stdout = await new Response(find.stdout).text();
22
+ if (stdout.length > 0) {
23
+ const b64 = stdout.trim();
24
+ return Uint8Array.from(Buffer.from(b64, 'base64'));
25
+ }
26
+ // Create a new 32-byte (256-bit) AES key
27
+ const key = crypto.getRandomValues(new Uint8Array(32));
28
+ const b64 = Buffer.from(key).toString('base64');
29
+ // Store in macOS Keychain (no user prompts with -U flag)
30
+ const add = Bun.spawn([
31
+ 'security',
32
+ 'add-generic-password',
33
+ '-s',
34
+ service,
35
+ '-a',
36
+ KEY_ACCOUNT,
37
+ '-w',
38
+ b64,
39
+ '-U', // Update without user confirmation
40
+ ]);
41
+ await add.exited;
42
+ return key;
43
+ }
44
+ /**
45
+ * Encrypt data using AES-256-GCM
46
+ */
47
+ async function encrypt(data, keyBytes) {
48
+ const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', false, ['encrypt']);
49
+ const iv = crypto.getRandomValues(new Uint8Array(12));
50
+ const plaintext = new TextEncoder().encode(data);
51
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext));
52
+ // Combine IV + ciphertext
53
+ const combined = new Uint8Array(iv.length + ciphertext.length);
54
+ combined.set(iv, 0);
55
+ combined.set(ciphertext, iv.length);
56
+ return combined;
57
+ }
58
+ /**
59
+ * Decrypt data using AES-256-GCM
60
+ */
61
+ async function decrypt(combined, keyBytes) {
62
+ const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', false, ['decrypt']);
63
+ const iv = combined.slice(0, 12);
64
+ const ciphertext = combined.slice(12);
65
+ const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
66
+ return new TextDecoder().decode(plaintext);
67
+ }
68
+ /**
69
+ * Store auth data in macOS Keychain
70
+ */
71
+ export async function saveAuthToKeychain(profileName, authData) {
72
+ const service = `${SERVICE_PREFIX}.${profileName}`;
73
+ const account = 'auth-token';
74
+ // Get or create encryption key
75
+ const key = await ensureEncryptionKey(service);
76
+ // Encrypt the auth data
77
+ const json = JSON.stringify(authData);
78
+ const encrypted = await encrypt(json, key);
79
+ const b64 = Buffer.from(encrypted).toString('base64');
80
+ // Store encrypted auth in keychain
81
+ // First try to delete if exists, then add
82
+ const del = Bun.spawn(['security', 'delete-generic-password', '-s', service, '-a', account], { stderr: 'ignore' });
83
+ await del.exited;
84
+ const add = Bun.spawn([
85
+ 'security',
86
+ 'add-generic-password',
87
+ '-s',
88
+ service,
89
+ '-a',
90
+ account,
91
+ '-w',
92
+ b64,
93
+ '-U',
94
+ ]);
95
+ await add.exited;
96
+ }
97
+ /**
98
+ * Retrieve auth data from macOS Keychain
99
+ */
100
+ export async function getAuthFromKeychain(profileName) {
101
+ const service = `${SERVICE_PREFIX}.${profileName}`;
102
+ const account = 'auth-token';
103
+ try {
104
+ // Get the encrypted auth data
105
+ const find = Bun.spawn(['security', 'find-generic-password', '-s', service, '-a', account, '-w'], { stderr: 'ignore' });
106
+ const stdout = await new Response(find.stdout).text();
107
+ if (stdout.length === 0) {
108
+ return null;
109
+ }
110
+ const b64 = stdout.trim();
111
+ const encrypted = Uint8Array.from(Buffer.from(b64, 'base64'));
112
+ // Get the encryption key
113
+ const key = await ensureEncryptionKey(service);
114
+ // Decrypt the auth data
115
+ const json = await decrypt(encrypted, key);
116
+ return JSON.parse(json);
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ /**
123
+ * Delete auth data from macOS Keychain
124
+ */
125
+ export async function deleteAuthFromKeychain(profileName) {
126
+ const service = `${SERVICE_PREFIX}.${profileName}`;
127
+ const account = 'auth-token';
128
+ const del = Bun.spawn(['security', 'delete-generic-password', '-s', service, '-a', account], { stderr: 'ignore' });
129
+ await del.exited;
130
+ }
131
+ //# sourceMappingURL=keychain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain.js","sourceRoot":"","sources":["../src/keychain.ts"],"names":[],"mappings":"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"}
@@ -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 './')
@@ -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 './')
@@ -16,9 +16,9 @@ export function detectSubagent(filePath, srcDir) {
16
16
  }
17
17
  // Strip leading './' and split into parts, filtering out empty segments
18
18
  const pathParts = normalizedPath.replace(/^\.\//, '').split('/').filter(Boolean);
19
- // Path structure assumption: ['agents', 'parent', 'child', 'agent.ts' | 'route.ts' | 'route']
19
+ // Path structure assumption: ['agent', 'parent', 'child', 'agent.ts' | 'route.ts' | 'route']
20
20
  // Currently hardcoded to 4 segments - consider making configurable in the future
21
- const isSubagent = pathParts.length === 4 && pathParts[0] === 'agents';
21
+ const isSubagent = pathParts.length === 4 && pathParts[0] === 'agent';
22
22
  const parentName = isSubagent ? pathParts[1] : null;
23
23
  return { isSubagent, parentName };
24
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"detectSubagent.js","sourceRoot":"","sources":["../../src/utils/detectSubagent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAC7B,QAAgB,EAChB,MAAe;IAEf,IAAI,cAAc,GAAG,QAAQ,CAAC;IAE9B,2BAA2B;IAC3B,IAAI,MAAM,IAAI,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACjD,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,wEAAwE;IACxE,MAAM,SAAS,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAEjF,8FAA8F;IAC9F,iFAAiF;IACjF,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC;IACvE,MAAM,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEpD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AACnC,CAAC"}
1
+ {"version":3,"file":"detectSubagent.js","sourceRoot":"","sources":["../../src/utils/detectSubagent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAC7B,QAAgB,EAChB,MAAe;IAEf,IAAI,cAAc,GAAG,QAAQ,CAAC;IAE9B,2BAA2B;IAC3B,IAAI,MAAM,IAAI,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACjD,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,wEAAwE;IACxE,MAAM,SAAS,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAEjF,6FAA6F;IAC7F,iFAAiF;IACjF,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC;IACtE,MAAM,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEpD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AACnC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentuity/cli",
3
- "version": "0.0.66",
3
+ "version": "0.0.68",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Agentuity employees and contributors",
6
6
  "type": "module",
@@ -35,8 +35,8 @@
35
35
  "prepublishOnly": "bun run clean && bun run build"
36
36
  },
37
37
  "dependencies": {
38
- "@agentuity/core": "0.0.66",
39
- "@agentuity/server": "0.0.66",
38
+ "@agentuity/core": "0.0.68",
39
+ "@agentuity/server": "0.0.68",
40
40
  "@datasert/cronjs-parser": "^1.4.0",
41
41
  "@terascope/fetch-github-release": "^2.2.1",
42
42
  "acorn-loose": "^8.5.2",
@@ -27,7 +27,7 @@ Each agent folder must contain:
27
27
 
28
28
  Example structure:
29
29
  \`\`\`
30
- src/agents/
30
+ src/agent/
31
31
  ├── hello/
32
32
  │ ├── agent.ts
33
33
  │ └── route.ts
@@ -211,7 +211,7 @@ Agents can have subagents organized one level deep. This is useful for grouping
211
211
  ### Directory Structure for Subagents
212
212
 
213
213
  \`\`\`
214
- src/agents/
214
+ src/agent/
215
215
  └── team/ # Parent agent
216
216
  ├── agent.ts # Parent agent
217
217
  ├── route.ts # Parent routes
@@ -26,7 +26,7 @@ Each API folder must contain:
26
26
 
27
27
  Example structure:
28
28
  \`\`\`
29
- src/apis/
29
+ src/web/
30
30
  ├── status/
31
31
  │ └── route.ts
32
32
  ├── users/
@@ -928,14 +928,14 @@ export async function parseRoute(
928
928
 
929
929
  // Detect if this is a subagent route and build proper path
930
930
  const relativePath = relative(rootDir, dir)
931
- .replace(/^src\/agents\//, '')
932
- .replace(/^src\/apis\//, '');
931
+ .replace(/^src\/agent\//, '')
932
+ .replace(/^src\/web\//, '');
933
933
  const pathParts = relativePath.split('/').filter(Boolean);
934
- const isSubagent = pathParts.length === 2 && filename.includes('src/agents');
934
+ const isSubagent = pathParts.length === 2 && filename.includes('src/agent');
935
935
  const routeName = isSubagent ? pathParts.join('/') : name;
936
936
 
937
937
  const routes: RouteDefinition = [];
938
- const routePrefix = filename.includes('src/agents') ? '/agent' : '/api';
938
+ const routePrefix = filename.includes('src/agent') ? '/agent' : '/api';
939
939
 
940
940
  try {
941
941
  for (const body of ast.body) {
@@ -85,10 +85,10 @@ export async function bundle({
85
85
 
86
86
  const appEntrypoints: string[] = [];
87
87
 
88
- for (const folder of ['apis', 'agents']) {
88
+ for (const folder of ['web', 'agent']) {
89
89
  const dir = join(srcDir, folder);
90
90
  if (!existsSync(dir)) {
91
- if (folder === 'agents') {
91
+ if (folder === 'agent') {
92
92
  throw new AgentsDirNotFoundError({ message: `Expected directory not found: ${dir}` });
93
93
  }
94
94
  continue;
@@ -142,7 +142,7 @@ function generateAgentRegistry(srcDir: string, agentInfo: Array<Record<string, s
142
142
  .map(({ name, path, parent }) => {
143
143
  const fullName = parent ? `${parent}.${name}` : name;
144
144
  const camelName = toCamelCase(fullName.replace('.', '_'));
145
- const relativePath = path.replace(/^\.\/agents\//, './');
145
+ const relativePath = path.replace(/^\.\/agent\//, './');
146
146
  return `import ${camelName}Agent from '${relativePath}';`;
147
147
  })
148
148
  .join('\n');
@@ -340,11 +340,11 @@ ${reactAgentTypes}
340
340
  }
341
341
  `;
342
342
 
343
- const agentsDir = join(srcDir, 'agents');
343
+ const agentsDir = join(srcDir, 'agent');
344
344
  const registryPath = join(agentsDir, 'registry.generated.ts');
345
345
  const legacyTypesPath = join(agentsDir, 'types.generated.d.ts');
346
346
 
347
- // Ensure agents directory exists
347
+ // Ensure agent directory exists
348
348
  if (!existsSync(agentsDir)) {
349
349
  mkdirSync(agentsDir, { recursive: true });
350
350
  }
@@ -435,7 +435,7 @@ const AgentuityBundler: BunPlugin = {
435
435
  newsource = ns;
436
436
 
437
437
  // Detect if this is a subagent by checking path structure
438
- // Note: Path structure assumption - 4 segments: agents/parent/child/agent.ts
438
+ // Note: Path structure assumption - 4 segments: agent/parent/child/agent.ts
439
439
  const { isSubagent, parentName } = detectSubagent(args.path, srcDir);
440
440
  if (isSubagent && parentName) {
441
441
  md.set('parent', parentName);
@@ -521,14 +521,11 @@ const AgentuityBundler: BunPlugin = {
521
521
 
522
522
  const agentPath = route
523
523
  .replace(/\/route$/, '/*')
524
- .replace('/agents', '/agent')
525
524
  .replace('./', '/');
526
525
  const routePath = route
527
526
  .replace(/\/route$/, '')
528
- .replace('/apis/', '/api/')
529
- .replace('/apis', '/api')
530
- .replace('/agents', '/agent')
531
- .replace('/agents', '/agent')
527
+ .replace('/web/', '/api/')
528
+ .replace('/web', '/api')
532
529
  .replace('./', '/');
533
530
 
534
531
  const definitions = await parseRoute(
@@ -575,7 +572,7 @@ const AgentuityBundler: BunPlugin = {
575
572
  if (hasAgent) {
576
573
  const agentRegistrationName =
577
574
  isSubagent && parentName ? `${parentName}.${name}` : name;
578
- // Build evals path from agent path (e.g., 'agents/eval/agent' -> 'agents/eval/eval.ts')
575
+ // Build evals path from agent path (e.g., 'agent/eval/agent' -> 'agent/eval/eval.ts')
579
576
  const agentDirPath = agent.replace(/\/agent$/, '');
580
577
  const evalsPath = join(srcDir, agentDirPath, 'eval.ts');
581
578
  const evalsImport = existsSync(evalsPath)
@@ -694,7 +691,7 @@ import { readFileSync, existsSync } from 'node:fs';
694
691
  : join(rootDir, filename);
695
692
 
696
693
  // Convert to path relative to srcDir like route-based agents
697
- // e.g., /path/to/src/agents/lifecycle/agent.ts -> ./agents/lifecycle/agent
694
+ // e.g., /path/to/src/agent/lifecycle/agent.ts -> ./agent/lifecycle/agent
698
695
  const agentPath = absolutePath.replace(srcDir, '.').replace('.ts', '');
699
696
 
700
697
  // Extract folder name as agent name (same as route-based logic)
@@ -1002,10 +1002,10 @@ export const command = createCommand({
1002
1002
  statSync(absPath).isDirectory() &&
1003
1003
  readdirSync(absPath).length === 0
1004
1004
  ) {
1005
- if (changedFile?.startsWith('src/agents/')) {
1005
+ if (changedFile?.startsWith('src/agent/')) {
1006
1006
  logger.debug('agent directory created: %s', changedFile);
1007
1007
  createAgentTemplates(absPath);
1008
- } else if (changedFile?.startsWith('src/apis/')) {
1008
+ } else if (changedFile?.startsWith('src/web/')) {
1009
1009
  logger.debug('api directory created: %s', changedFile);
1010
1010
  createAPITemplates(absPath);
1011
1011
  }
@@ -53,7 +53,7 @@ async function cleanup(sourceDir: string, dest: string) {
53
53
  });
54
54
  }
55
55
 
56
- tui.spinner(`📦 Copying template files...`, async () => {
56
+ await tui.spinner(`📦 Copying template files...`, async () => {
57
57
  // Copy all files from source to dest
58
58
  const files = readdirSync(sourceDir);
59
59
  for (const file of files) {
@@ -281,22 +281,22 @@ export async function setupProject(options: SetupOptions): Promise<void> {
281
281
  }
282
282
 
283
283
  // generate and write AGENTS.md for each of the main folders
284
- const agentsDir = join(dest, 'src', 'agents');
285
- if (existsSync(agentsDir)) {
286
- const agentsAPIFile = join(agentsDir, 'AGENTS.md');
287
- await Bun.write(agentsAPIFile, generateAgentPrompt());
284
+ const agentDir = join(dest, 'src', 'agent');
285
+ if (existsSync(agentDir)) {
286
+ const agentAPIFile = join(agentDir, 'AGENTS.md');
287
+ await Bun.write(agentAPIFile, generateAgentPrompt());
288
288
  }
289
289
 
290
- const apisDir = join(dest, 'src', 'apis');
291
- if (existsSync(apisDir)) {
292
- const agentsAPIsFile = join(apisDir, 'AGENTS.md');
293
- await Bun.write(agentsAPIsFile, generateAPIPrompt());
290
+ const apiDir = join(dest, 'src', 'api');
291
+ if (existsSync(apiDir)) {
292
+ const agentAPIFile = join(apiDir, 'AGENTS.md');
293
+ await Bun.write(agentAPIFile, generateAPIPrompt());
294
294
  }
295
295
 
296
296
  const webDir = join(dest, 'src', 'web');
297
297
  if (existsSync(webDir)) {
298
- const agentsWebFile = join(webDir, 'AGENTS.md');
299
- await Bun.write(agentsWebFile, generateWebPrompt());
298
+ const webFile = join(webDir, 'AGENTS.md');
299
+ await Bun.write(webFile, generateWebPrompt());
300
300
  }
301
301
  }
302
302
 
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);
269
+ // On macOS, store in Keychain for better security
270
+ if (isMacOS()) {
271
+ try {
272
+ await saveAuthToKeychain(profileName, authData);
273
+
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);
279
283
  }
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 = '';
285
-
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;
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,12 @@ 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
331
 
360
332
  export async function getAuth(): Promise<AuthData | null> {
361
333
  const config = await loadConfig();
362
334
  const profileName = config?.name || defaultProfileName;
363
335
 
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
336
+ // Priority 1: Allow automated login from environment variables
384
337
  if (process.env.AGENTUITY_CLI_API_KEY && process.env.AGENTUITY_USER_ID) {
385
338
  return {
386
339
  apiKey: process.env.AGENTUITY_CLI_API_KEY,
@@ -389,7 +342,23 @@ export async function getAuth(): Promise<AuthData | null> {
389
342
  };
390
343
  }
391
344
 
392
- // Priority 3: Fallback to config file (backwards compatibility + migration)
345
+ // Priority 2: On macOS, try to read from Keychain
346
+ if (isMacOS()) {
347
+ try {
348
+ const keychainAuth = await getAuthFromKeychain(profileName);
349
+ if (keychainAuth) {
350
+ return {
351
+ apiKey: keychainAuth.api_key,
352
+ userId: keychainAuth.user_id,
353
+ expires: new Date(keychainAuth.expires),
354
+ };
355
+ }
356
+ } catch {
357
+ // Keychain read failed, fall through to config file
358
+ }
359
+ }
360
+
361
+ // Priority 3: Read from config file (non-macOS or keychain failed)
393
362
  if (!config) return null;
394
363
  const auth = config.auth as { api_key?: string; user_id?: string; expires?: number } | undefined;
395
364
 
@@ -397,18 +366,7 @@ export async function getAuth(): Promise<AuthData | null> {
397
366
  return null;
398
367
  }
399
368
 
400
- // Check if token is unexpired
401
369
  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
370
 
413
371
  return {
414
372
  apiKey: auth.api_key,