@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.
- package/bin/cli.ts +20 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -0
- package/dist/cli.js.map +1 -1
- package/dist/cmd/ai/prompt/agent.d.ts.map +1 -1
- package/dist/cmd/ai/prompt/agent.js +24 -25
- package/dist/cmd/ai/prompt/agent.js.map +1 -1
- package/dist/cmd/ai/prompt/api.d.ts.map +1 -1
- package/dist/cmd/ai/prompt/api.js +12 -9
- package/dist/cmd/ai/prompt/api.js.map +1 -1
- package/dist/cmd/build/ast.js +4 -4
- package/dist/cmd/build/ast.js.map +1 -1
- package/dist/cmd/build/bundler.js +2 -2
- package/dist/cmd/build/bundler.js.map +1 -1
- package/dist/cmd/build/plugin.d.ts.map +1 -1
- package/dist/cmd/build/plugin.js +9 -14
- package/dist/cmd/build/plugin.js.map +1 -1
- package/dist/cmd/dev/index.js +2 -2
- package/dist/cmd/dev/index.js.map +1 -1
- package/dist/cmd/dev/templates.d.ts.map +1 -1
- package/dist/cmd/dev/templates.js +10 -3
- package/dist/cmd/dev/templates.js.map +1 -1
- package/dist/cmd/project/download.js +10 -10
- package/dist/cmd/project/download.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +48 -87
- package/dist/config.js.map +1 -1
- package/dist/keychain.d.ts +31 -0
- package/dist/keychain.d.ts.map +1 -0
- package/dist/keychain.js +135 -0
- package/dist/keychain.js.map +1 -0
- package/dist/utils/detectSubagent.d.ts +1 -1
- package/dist/utils/detectSubagent.js +3 -3
- package/dist/utils/detectSubagent.js.map +1 -1
- package/package.json +3 -3
- package/src/cli.ts +19 -0
- package/src/cmd/ai/prompt/agent.ts +24 -25
- package/src/cmd/ai/prompt/api.ts +12 -9
- package/src/cmd/build/ast.ts +4 -4
- package/src/cmd/build/bundler.ts +2 -2
- package/src/cmd/build/plugin.ts +9 -14
- package/src/cmd/dev/index.ts +2 -2
- package/src/cmd/dev/templates.ts +10 -3
- package/src/cmd/project/download.ts +10 -10
- package/src/config.ts +54 -97
- package/src/keychain.ts +176 -0
- package/src/utils/detectSubagent.ts +3 -3
- package/dist/cmd/build/ast.test.d.ts +0 -2
- package/dist/cmd/build/ast.test.d.ts.map +0 -1
- package/dist/cmd/build/ast.test.js +0 -339
- package/dist/cmd/build/ast.test.js.map +0 -1
- package/dist/cmd/build/fix-duplicate-exports.test.d.ts +0 -2
- package/dist/cmd/build/fix-duplicate-exports.test.d.ts.map +0 -1
- package/dist/cmd/build/fix-duplicate-exports.test.js +0 -300
- package/dist/cmd/build/fix-duplicate-exports.test.js.map +0 -1
- package/dist/crypto/box.test.d.ts +0 -2
- package/dist/crypto/box.test.d.ts.map +0 -1
- package/dist/crypto/box.test.js +0 -317
- package/dist/crypto/box.test.js.map +0 -1
- package/dist/env-util.test.d.ts +0 -2
- package/dist/env-util.test.d.ts.map +0 -1
- package/dist/env-util.test.js +0 -146
- 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
|
-
|
|
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
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
//
|
|
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:
|
|
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
|
|
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,
|
package/src/keychain.ts
ADDED
|
@@ -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:
|
|
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: ['
|
|
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] === '
|
|
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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ast.test.d.ts","sourceRoot":"","sources":["../../../src/cmd/build/ast.test.ts"],"names":[],"mappings":""}
|