@agentuity/cli 2.0.6 → 2.0.7
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/README.md +11 -0
- package/dist/cmd/cloud/task/close.d.ts +3 -0
- package/dist/cmd/cloud/task/close.d.ts.map +1 -0
- package/dist/cmd/cloud/task/close.js +286 -0
- package/dist/cmd/cloud/task/close.js.map +1 -0
- package/dist/cmd/cloud/task/delete.d.ts +1 -5
- package/dist/cmd/cloud/task/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/task/delete.js +15 -38
- package/dist/cmd/cloud/task/delete.js.map +1 -1
- package/dist/cmd/cloud/task/index.d.ts.map +1 -1
- package/dist/cmd/cloud/task/index.js +10 -0
- package/dist/cmd/cloud/task/index.js.map +1 -1
- package/dist/cmd/cloud/task/list.d.ts.map +1 -1
- package/dist/cmd/cloud/task/list.js +97 -3
- package/dist/cmd/cloud/task/list.js.map +1 -1
- package/dist/cmd/cloud/task/util.d.ts +10 -0
- package/dist/cmd/cloud/task/util.d.ts.map +1 -1
- package/dist/cmd/cloud/task/util.js +47 -3
- package/dist/cmd/cloud/task/util.js.map +1 -1
- package/dist/cmd/coder/config/index.d.ts +2 -0
- package/dist/cmd/coder/config/index.d.ts.map +1 -0
- package/dist/cmd/coder/config/index.js +20 -0
- package/dist/cmd/coder/config/index.js.map +1 -0
- package/dist/cmd/coder/config/set.d.ts +2 -0
- package/dist/cmd/coder/config/set.d.ts.map +1 -0
- package/dist/cmd/coder/config/set.js +100 -0
- package/dist/cmd/coder/config/set.js.map +1 -0
- package/dist/cmd/coder/hub-url.d.ts +21 -10
- package/dist/cmd/coder/hub-url.d.ts.map +1 -1
- package/dist/cmd/coder/hub-url.js +97 -55
- package/dist/cmd/coder/hub-url.js.map +1 -1
- package/dist/cmd/coder/index.d.ts.map +1 -1
- package/dist/cmd/coder/index.js +6 -1
- package/dist/cmd/coder/index.js.map +1 -1
- package/dist/cmd/coder/inspect.d.ts.map +1 -1
- package/dist/cmd/coder/inspect.js +15 -7
- package/dist/cmd/coder/inspect.js.map +1 -1
- package/dist/cmd/coder/list.d.ts.map +1 -1
- package/dist/cmd/coder/list.js +14 -7
- package/dist/cmd/coder/list.js.map +1 -1
- package/dist/cmd/coder/start.d.ts.map +1 -1
- package/dist/cmd/coder/start.js +38 -23
- package/dist/cmd/coder/start.js.map +1 -1
- package/dist/cmd/coder/tui-init.d.ts +4 -1
- package/dist/cmd/coder/tui-init.d.ts.map +1 -1
- package/dist/cmd/coder/tui-init.js +3 -2
- package/dist/cmd/coder/tui-init.js.map +1 -1
- package/dist/cmd/dev/sync.js +5 -5
- package/dist/cmd/dev/sync.js.map +1 -1
- package/dist/coder-config.d.ts +14 -0
- package/dist/coder-config.d.ts.map +1 -0
- package/dist/coder-config.js +119 -0
- package/dist/coder-config.js.map +1 -0
- package/dist/coder-hub-url.d.ts +3 -0
- package/dist/coder-hub-url.d.ts.map +1 -0
- package/dist/coder-hub-url.js +32 -0
- package/dist/coder-hub-url.js.map +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/internal-logger.d.ts +4 -0
- package/dist/internal-logger.d.ts.map +1 -1
- package/dist/internal-logger.js +64 -2
- package/dist/internal-logger.js.map +1 -1
- package/dist/keychain.d.ts +3 -0
- package/dist/keychain.d.ts.map +1 -1
- package/dist/keychain.js +47 -28
- package/dist/keychain.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +6 -6
- package/src/cmd/cloud/task/close.ts +319 -0
- package/src/cmd/cloud/task/delete.ts +15 -43
- package/src/cmd/cloud/task/index.ts +10 -0
- package/src/cmd/cloud/task/list.ts +111 -4
- package/src/cmd/cloud/task/util.ts +59 -5
- package/src/cmd/coder/config/index.ts +20 -0
- package/src/cmd/coder/config/set.ts +112 -0
- package/src/cmd/coder/hub-url.ts +147 -53
- package/src/cmd/coder/index.ts +6 -1
- package/src/cmd/coder/inspect.ts +33 -10
- package/src/cmd/coder/list.ts +33 -10
- package/src/cmd/coder/start.ts +62 -26
- package/src/cmd/coder/tui-init.ts +7 -2
- package/src/cmd/dev/sync.ts +5 -5
- package/src/coder-config.ts +141 -0
- package/src/coder-hub-url.ts +32 -0
- package/src/config.ts +13 -0
- package/src/internal-logger.ts +83 -2
- package/src/keychain.ts +68 -39
- package/src/types.ts +10 -0
package/src/cmd/coder/start.ts
CHANGED
|
@@ -5,7 +5,17 @@ import { createSubcommand } from '../../types';
|
|
|
5
5
|
import * as tui from '../../tui';
|
|
6
6
|
import { getCommand } from '../../command-prefix';
|
|
7
7
|
import { ErrorCode } from '../../errors';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
clearStoredHubApiKeyOnUnauthorized,
|
|
10
|
+
formatHubUnauthorizedMessage,
|
|
11
|
+
formatMissingHubUrlMessage,
|
|
12
|
+
getHubResponseErrorMessage,
|
|
13
|
+
hubFetchHeaders,
|
|
14
|
+
isHubUnauthorizedStatus,
|
|
15
|
+
resolveHubApiKey,
|
|
16
|
+
resolveHubUrl,
|
|
17
|
+
toHubWsUrl,
|
|
18
|
+
} from './hub-url';
|
|
9
19
|
import { probeHubInitAccess } from './tui-init';
|
|
10
20
|
|
|
11
21
|
/**
|
|
@@ -127,24 +137,44 @@ export const startSubcommand = createSubcommand({
|
|
|
127
137
|
}),
|
|
128
138
|
},
|
|
129
139
|
async handler(ctx) {
|
|
130
|
-
const { opts, options } = ctx;
|
|
140
|
+
const { opts, options, config } = ctx;
|
|
131
141
|
|
|
132
142
|
// Resolve Hub URL
|
|
133
|
-
const hubHttpUrl = await resolveHubUrl(opts?.hubUrl);
|
|
143
|
+
const hubHttpUrl = await resolveHubUrl(opts?.hubUrl, config);
|
|
134
144
|
if (!hubHttpUrl) {
|
|
135
|
-
tui.fatal(
|
|
136
|
-
'Could not find a running Coder Hub.\n\nEither:\n - Start the Hub with: bun run dev\n - Set AGENTUITY_CODER_HUB_URL environment variable\n - Pass --hub-url flag',
|
|
137
|
-
ErrorCode.NETWORK_ERROR
|
|
138
|
-
);
|
|
145
|
+
tui.fatal(formatMissingHubUrlMessage(), ErrorCode.NETWORK_ERROR);
|
|
139
146
|
return;
|
|
140
147
|
}
|
|
141
148
|
const hubWsUrl = toHubWsUrl(hubHttpUrl);
|
|
149
|
+
const resolvedHubApiKey = await resolveHubApiKey(config);
|
|
150
|
+
|
|
151
|
+
const handleUnauthorizedResponse = async (
|
|
152
|
+
response: Response,
|
|
153
|
+
errorCode: ErrorCode = ErrorCode.NETWORK_ERROR
|
|
154
|
+
): Promise<void> => {
|
|
155
|
+
const message = await getHubResponseErrorMessage(response);
|
|
156
|
+
const clearedStoredKey = await clearStoredHubApiKeyOnUnauthorized(
|
|
157
|
+
response.status,
|
|
158
|
+
resolvedHubApiKey,
|
|
159
|
+
config
|
|
160
|
+
);
|
|
161
|
+
tui.fatal(
|
|
162
|
+
formatHubUnauthorizedMessage(hubHttpUrl, message, { clearedStoredKey }),
|
|
163
|
+
errorCode
|
|
164
|
+
);
|
|
165
|
+
};
|
|
142
166
|
|
|
143
|
-
const initProbe = await probeHubInitAccess(hubHttpUrl
|
|
167
|
+
const initProbe = await probeHubInitAccess(hubHttpUrl, {
|
|
168
|
+
apiKey: resolvedHubApiKey.apiKey,
|
|
169
|
+
});
|
|
144
170
|
if (!initProbe.ok) {
|
|
145
171
|
if (initProbe.code === 'unauthorized') {
|
|
172
|
+
const clearedStoredKey =
|
|
173
|
+
resolvedHubApiKey.source === 'stored'
|
|
174
|
+
? await clearStoredHubApiKeyOnUnauthorized(401, resolvedHubApiKey, config)
|
|
175
|
+
: false;
|
|
146
176
|
tui.fatal(
|
|
147
|
-
|
|
177
|
+
formatHubUnauthorizedMessage(hubHttpUrl, initProbe.message, { clearedStoredKey }),
|
|
148
178
|
ErrorCode.NETWORK_ERROR
|
|
149
179
|
);
|
|
150
180
|
return;
|
|
@@ -192,11 +222,15 @@ export const startSubcommand = createSubcommand({
|
|
|
192
222
|
message: 'Fetching connectable sessions…',
|
|
193
223
|
callback: async () => {
|
|
194
224
|
const resp = await fetch(`${hubHttpUrl}/api/hub/sessions/connectable`, {
|
|
195
|
-
headers: hubFetchHeaders(),
|
|
225
|
+
headers: hubFetchHeaders(undefined, resolvedHubApiKey.apiKey),
|
|
196
226
|
signal: AbortSignal.timeout(10_000),
|
|
197
227
|
});
|
|
228
|
+
if (isHubUnauthorizedStatus(resp.status)) {
|
|
229
|
+
await handleUnauthorizedResponse(resp);
|
|
230
|
+
throw new Error('Hub authentication failed');
|
|
231
|
+
}
|
|
198
232
|
if (!resp.ok) {
|
|
199
|
-
throw new Error(`${resp.status} ${resp
|
|
233
|
+
throw new Error(`${resp.status} ${await getHubResponseErrorMessage(resp)}`);
|
|
200
234
|
}
|
|
201
235
|
const data = (await resp.json()) as { sessions: SessionInfo[] };
|
|
202
236
|
return data.sessions;
|
|
@@ -205,7 +239,7 @@ export const startSubcommand = createSubcommand({
|
|
|
205
239
|
|
|
206
240
|
if (sessions.length === 0) {
|
|
207
241
|
tui.fatal(
|
|
208
|
-
|
|
242
|
+
`No connectable sandbox sessions found.\n\nCreate one with:\n ${getCommand('coder start --sandbox "your task"')}`,
|
|
209
243
|
ErrorCode.CONFIG_INVALID
|
|
210
244
|
);
|
|
211
245
|
return;
|
|
@@ -229,7 +263,7 @@ export const startSubcommand = createSubcommand({
|
|
|
229
263
|
});
|
|
230
264
|
} catch (err) {
|
|
231
265
|
const msg = err instanceof Error ? err.message : String(err);
|
|
232
|
-
if (msg === 'User cancelled') return;
|
|
266
|
+
if (msg === 'User cancelled' || msg === 'Hub authentication failed') return;
|
|
233
267
|
tui.fatal(`Failed to fetch connectable sessions: ${msg}`, ErrorCode.NETWORK_ERROR);
|
|
234
268
|
return;
|
|
235
269
|
}
|
|
@@ -273,12 +307,6 @@ export const startSubcommand = createSubcommand({
|
|
|
273
307
|
return;
|
|
274
308
|
}
|
|
275
309
|
|
|
276
|
-
const hubHttpUrl = await resolveHubUrl(opts?.hubUrl);
|
|
277
|
-
if (!hubHttpUrl) {
|
|
278
|
-
tui.fatal('Could not find Hub URL for sandbox creation.', ErrorCode.NETWORK_ERROR);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
310
|
// Build request body
|
|
283
311
|
const body: Record<string, unknown> = { task };
|
|
284
312
|
if (opts?.repo) {
|
|
@@ -293,14 +321,20 @@ export const startSubcommand = createSubcommand({
|
|
|
293
321
|
try {
|
|
294
322
|
const resp = await fetch(`${hubHttpUrl}/api/hub/session`, {
|
|
295
323
|
method: 'POST',
|
|
296
|
-
headers: hubFetchHeaders(
|
|
324
|
+
headers: hubFetchHeaders(
|
|
325
|
+
{ 'Content-Type': 'application/json' },
|
|
326
|
+
resolvedHubApiKey.apiKey
|
|
327
|
+
),
|
|
297
328
|
body: JSON.stringify(body),
|
|
298
329
|
signal: AbortSignal.timeout(10_000),
|
|
299
330
|
});
|
|
331
|
+
if (isHubUnauthorizedStatus(resp.status)) {
|
|
332
|
+
await handleUnauthorizedResponse(resp);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
300
335
|
if (!resp.ok) {
|
|
301
|
-
const errText = await resp.text();
|
|
302
336
|
tui.fatal(
|
|
303
|
-
`Failed to create sandbox session: ${resp.status} ${
|
|
337
|
+
`Failed to create sandbox session: ${resp.status} ${await getHubResponseErrorMessage(resp)}`,
|
|
304
338
|
ErrorCode.NETWORK_ERROR
|
|
305
339
|
);
|
|
306
340
|
return;
|
|
@@ -328,9 +362,13 @@ export const startSubcommand = createSubcommand({
|
|
|
328
362
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
329
363
|
try {
|
|
330
364
|
const pollResp = await fetch(`${hubHttpUrl}/api/hub/session/${sessionId}`, {
|
|
331
|
-
headers: hubFetchHeaders(),
|
|
365
|
+
headers: hubFetchHeaders(undefined, resolvedHubApiKey.apiKey),
|
|
332
366
|
signal: AbortSignal.timeout(5_000),
|
|
333
367
|
});
|
|
368
|
+
if (isHubUnauthorizedStatus(pollResp.status)) {
|
|
369
|
+
await handleUnauthorizedResponse(pollResp);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
334
372
|
if (pollResp.ok) {
|
|
335
373
|
const data = (await pollResp.json()) as {
|
|
336
374
|
participants?: Array<{ role: string }>;
|
|
@@ -374,9 +412,7 @@ export const startSubcommand = createSubcommand({
|
|
|
374
412
|
...(process.env as Record<string, string>),
|
|
375
413
|
AGENTUITY_CODER_HUB_URL: hubWsUrl,
|
|
376
414
|
};
|
|
377
|
-
|
|
378
|
-
const cliApiKey = process.env.AGENTUITY_CODER_API_KEY;
|
|
379
|
-
if (cliApiKey) env.AGENTUITY_CODER_API_KEY = cliApiKey;
|
|
415
|
+
if (resolvedHubApiKey.apiKey) env.AGENTUITY_CODER_API_KEY = resolvedHubApiKey.apiKey;
|
|
380
416
|
|
|
381
417
|
if (opts?.agent) {
|
|
382
418
|
env.AGENTUITY_CODER_AGENT = opts.agent;
|
|
@@ -21,11 +21,16 @@ function normalizeErrorMessage(payload: unknown, fallback: string): string {
|
|
|
21
21
|
|
|
22
22
|
export async function probeHubInitAccess(
|
|
23
23
|
hubHttpUrl: string,
|
|
24
|
-
|
|
24
|
+
options?: {
|
|
25
|
+
apiKey?: string | null;
|
|
26
|
+
fetchImpl?: typeof fetch;
|
|
27
|
+
}
|
|
25
28
|
): Promise<HubInitProbeResult> {
|
|
29
|
+
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
30
|
+
|
|
26
31
|
try {
|
|
27
32
|
const response = await fetchImpl(`${hubHttpUrl}/api/hub/init`, {
|
|
28
|
-
headers: hubFetchHeaders({ accept: 'application/json' }),
|
|
33
|
+
headers: hubFetchHeaders({ accept: 'application/json' }, options?.apiKey),
|
|
29
34
|
signal: AbortSignal.timeout(5_000),
|
|
30
35
|
});
|
|
31
36
|
|
package/src/cmd/dev/sync.ts
CHANGED
|
@@ -169,7 +169,7 @@ class DevmodeSyncService implements IDevmodeSyncService {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
this.logger.debug('Previous metadata found with %d
|
|
172
|
+
this.logger.debug('Previous metadata found with %d evaluations', prevEvalCount);
|
|
173
173
|
} else {
|
|
174
174
|
this.logger.debug('No previous metadata, all evals will be treated as new');
|
|
175
175
|
}
|
|
@@ -182,7 +182,7 @@ class DevmodeSyncService implements IDevmodeSyncService {
|
|
|
182
182
|
if (agent.evals) {
|
|
183
183
|
currentEvalCount += agent.evals.length;
|
|
184
184
|
this.logger.debug(
|
|
185
|
-
'[CLI EVAL SYNC] Agent "%s" has %d
|
|
185
|
+
'[CLI EVAL SYNC] Agent "%s" has %d evaluations',
|
|
186
186
|
agent.name,
|
|
187
187
|
agent.evals.length
|
|
188
188
|
);
|
|
@@ -196,7 +196,7 @@ class DevmodeSyncService implements IDevmodeSyncService {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
|
-
this.logger.debug('[CLI EVAL SYNC] Total current
|
|
199
|
+
this.logger.debug('[CLI EVAL SYNC] Total current evaluations: %d', currentEvalCount);
|
|
200
200
|
|
|
201
201
|
// Get agents and evals to sync using shared diff logic
|
|
202
202
|
const { create: agentsToCreate, delete: agentsToDelete } = getAgentsToSync(
|
|
@@ -241,7 +241,7 @@ class DevmodeSyncService implements IDevmodeSyncService {
|
|
|
241
241
|
}
|
|
242
242
|
if (evalsToCreate.length > 0 || evalsToDelete.length > 0) {
|
|
243
243
|
this.logger.debug(
|
|
244
|
-
'Successfully bulk synced %d
|
|
244
|
+
'Successfully bulk synced %d evaluations to create, %d evaluations to delete',
|
|
245
245
|
evalsToCreate.length,
|
|
246
246
|
evalsToDelete.length
|
|
247
247
|
);
|
|
@@ -369,7 +369,7 @@ class MockDevmodeSyncService implements IDevmodeSyncService {
|
|
|
369
369
|
|
|
370
370
|
if (evalsToCreate.length > 0 || evalsToDelete.length > 0) {
|
|
371
371
|
this.logger.debug(
|
|
372
|
-
'[MOCK] Would make request: POST /cli/devmode/eval with %d
|
|
372
|
+
'[MOCK] Would make request: POST /cli/devmode/eval with %d evaluations to create, %d evaluations to delete',
|
|
373
373
|
evalsToCreate.length,
|
|
374
374
|
evalsToDelete.length
|
|
375
375
|
);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { defaultProfileName, getOrInitConfig, loadConfig, saveConfig } from './config';
|
|
2
|
+
import { normalizeCoderHubHttpUrl } from './coder-hub-url';
|
|
3
|
+
import {
|
|
4
|
+
deleteCoderApiKeyFromKeychain,
|
|
5
|
+
getCoderApiKeyFromKeychain,
|
|
6
|
+
isMacOS,
|
|
7
|
+
saveCoderApiKeyToKeychain,
|
|
8
|
+
} from './keychain';
|
|
9
|
+
import type { Config } from './types';
|
|
10
|
+
|
|
11
|
+
function getProfileName(config?: Config | null): string {
|
|
12
|
+
return config?.name || defaultProfileName;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function pruneCoderConfig(config: Config): void {
|
|
16
|
+
if (!config.coder) return;
|
|
17
|
+
|
|
18
|
+
const nextCoder = { ...config.coder };
|
|
19
|
+
if (!nextCoder.hubUrl) delete nextCoder.hubUrl;
|
|
20
|
+
if (!nextCoder.apiKey) delete nextCoder.apiKey;
|
|
21
|
+
|
|
22
|
+
if (Object.keys(nextCoder).length === 0) {
|
|
23
|
+
delete config.coder;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
config.coder = nextCoder;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function saveCoderHubUrl(
|
|
31
|
+
hubUrl: string
|
|
32
|
+
): Promise<{ profileName: string; hubUrl: string }> {
|
|
33
|
+
const normalized = normalizeCoderHubHttpUrl(hubUrl);
|
|
34
|
+
const config = await getOrInitConfig();
|
|
35
|
+
const profileName = getProfileName(config);
|
|
36
|
+
|
|
37
|
+
config.coder = {
|
|
38
|
+
...config.coder,
|
|
39
|
+
hubUrl: normalized,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
await saveConfig(config);
|
|
43
|
+
return { profileName, hubUrl: normalized };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function getStoredCoderHubUrl(config?: Config | null): Promise<string | null> {
|
|
47
|
+
const loadedConfig = config ?? (await loadConfig());
|
|
48
|
+
const hubUrl = loadedConfig?.coder?.hubUrl?.trim();
|
|
49
|
+
if (!hubUrl) return null;
|
|
50
|
+
return normalizeCoderHubHttpUrl(hubUrl);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function saveCoderApiKey(apiKey: string): Promise<{ profileName: string }> {
|
|
54
|
+
const trimmed = apiKey.trim();
|
|
55
|
+
const config = await getOrInitConfig();
|
|
56
|
+
const profileName = getProfileName(config);
|
|
57
|
+
|
|
58
|
+
if (isMacOS()) {
|
|
59
|
+
try {
|
|
60
|
+
await saveCoderApiKeyToKeychain(profileName, trimmed);
|
|
61
|
+
if (config.coder?.apiKey) {
|
|
62
|
+
config.coder = {
|
|
63
|
+
...config.coder,
|
|
64
|
+
};
|
|
65
|
+
delete config.coder.apiKey;
|
|
66
|
+
}
|
|
67
|
+
pruneCoderConfig(config);
|
|
68
|
+
await saveConfig(config);
|
|
69
|
+
return { profileName };
|
|
70
|
+
} catch {
|
|
71
|
+
// Fall back to config-file storage below.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
config.coder = {
|
|
76
|
+
...config.coder,
|
|
77
|
+
apiKey: trimmed,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
await saveConfig(config);
|
|
81
|
+
return { profileName };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function getStoredCoderApiKey(config?: Config | null): Promise<string | null> {
|
|
85
|
+
const loadedConfig = config ?? (await loadConfig());
|
|
86
|
+
const profileName = getProfileName(loadedConfig);
|
|
87
|
+
|
|
88
|
+
if (isMacOS()) {
|
|
89
|
+
try {
|
|
90
|
+
const keychainValue = await getCoderApiKeyFromKeychain(profileName);
|
|
91
|
+
if (keychainValue) {
|
|
92
|
+
if (loadedConfig?.coder?.apiKey) {
|
|
93
|
+
const configCopy = {
|
|
94
|
+
...loadedConfig,
|
|
95
|
+
coder: {
|
|
96
|
+
...loadedConfig.coder,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
delete configCopy.coder.apiKey;
|
|
100
|
+
pruneCoderConfig(configCopy);
|
|
101
|
+
await saveConfig(configCopy);
|
|
102
|
+
}
|
|
103
|
+
return keychainValue.trim() || null;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Fall back to config-file storage below.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const storedValue = loadedConfig?.coder?.apiKey?.trim();
|
|
111
|
+
return storedValue || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function clearStoredCoderApiKey(
|
|
115
|
+
config?: Config | null
|
|
116
|
+
): Promise<{ profileName: string }> {
|
|
117
|
+
const loadedConfig = config ?? (await getOrInitConfig());
|
|
118
|
+
const profileName = getProfileName(loadedConfig);
|
|
119
|
+
|
|
120
|
+
if (isMacOS()) {
|
|
121
|
+
try {
|
|
122
|
+
await deleteCoderApiKeyFromKeychain(profileName);
|
|
123
|
+
} catch {
|
|
124
|
+
// Ignore keychain cleanup errors.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (loadedConfig.coder?.apiKey) {
|
|
129
|
+
const configToSave = {
|
|
130
|
+
...loadedConfig,
|
|
131
|
+
coder: {
|
|
132
|
+
...loadedConfig.coder,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
delete configToSave.coder.apiKey;
|
|
136
|
+
pruneCoderConfig(configToSave);
|
|
137
|
+
await saveConfig(configToSave);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { profileName };
|
|
141
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function normalizeCoderHubHttpUrl(url: string): string {
|
|
2
|
+
let normalized = url.trim().replace(/\/+$/, '');
|
|
3
|
+
|
|
4
|
+
if (normalized.startsWith('ws://')) normalized = 'http://' + normalized.slice(5);
|
|
5
|
+
else if (normalized.startsWith('wss://')) normalized = 'https://' + normalized.slice(6);
|
|
6
|
+
|
|
7
|
+
normalized = normalized.replace(/\/api\/ws\b.*$/, '');
|
|
8
|
+
normalized = normalized.replace(/\/ws\b.*$/, '');
|
|
9
|
+
normalized = normalized.replace(/\/api\/hub\b.*$/, '');
|
|
10
|
+
|
|
11
|
+
return normalized.replace(/\/+$/, '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function toCoderHubWsUrl(hubHttpUrl: string): string {
|
|
15
|
+
let wsUrl = hubHttpUrl;
|
|
16
|
+
if (wsUrl.startsWith('http://')) wsUrl = 'ws://' + wsUrl.slice(7);
|
|
17
|
+
else if (wsUrl.startsWith('https://')) wsUrl = 'wss://' + wsUrl.slice(8);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const parsed = new URL(wsUrl);
|
|
21
|
+
if (parsed.pathname !== '/api/ws') {
|
|
22
|
+
parsed.pathname = '/api/ws';
|
|
23
|
+
wsUrl = parsed.toString().replace(/\/$/, '');
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
if (!wsUrl.endsWith('/api/ws')) {
|
|
27
|
+
wsUrl = wsUrl.replace(/\/?$/, '/api/ws');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return wsUrl;
|
|
32
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -28,6 +28,14 @@ import { ConfigSchema, ProjectSchema } from './types';
|
|
|
28
28
|
export const defaultProfileName = 'production';
|
|
29
29
|
|
|
30
30
|
export function getDefaultConfigDir(): string {
|
|
31
|
+
const configDirOverride = process.env.AGENTUITY_CONFIG_DIR?.trim();
|
|
32
|
+
if (configDirOverride) {
|
|
33
|
+
if (configDirOverride.startsWith('~/')) {
|
|
34
|
+
return resolve(join(homedir(), configDirOverride.slice(2)));
|
|
35
|
+
}
|
|
36
|
+
return resolve(configDirOverride);
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
return join(homedir(), '.config', 'agentuity');
|
|
32
40
|
}
|
|
33
41
|
|
|
@@ -144,6 +152,11 @@ let cachedConfig: Config | null | undefined;
|
|
|
144
152
|
// Track the resolved config path so saveConfig writes back to the same file
|
|
145
153
|
let cachedConfigPath: string | undefined;
|
|
146
154
|
|
|
155
|
+
export function resetConfigCache(): void {
|
|
156
|
+
cachedConfig = undefined;
|
|
157
|
+
cachedConfigPath = undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
147
160
|
export async function loadConfig(
|
|
148
161
|
customPath?: string,
|
|
149
162
|
skipCache = false,
|
package/src/internal-logger.ts
CHANGED
|
@@ -43,6 +43,16 @@ const SENSITIVE_ENV_PATTERNS = [
|
|
|
43
43
|
/AUTH/i,
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
+
const MASKED_ARG_VALUE = '***MASKED***';
|
|
47
|
+
const SENSITIVE_ARG_PATTERNS = [
|
|
48
|
+
/^--?api[-_]?key$/i,
|
|
49
|
+
/^--?token$/i,
|
|
50
|
+
/^--?secret$/i,
|
|
51
|
+
/^--?password$/i,
|
|
52
|
+
/^--?client[-_]?secret$/i,
|
|
53
|
+
/^--?auth[-_]?value$/i,
|
|
54
|
+
];
|
|
55
|
+
|
|
46
56
|
interface SessionMetadata {
|
|
47
57
|
sessionId: string;
|
|
48
58
|
bucket: number;
|
|
@@ -102,6 +112,75 @@ function maskEnvironment(): Record<string, string> {
|
|
|
102
112
|
return masked;
|
|
103
113
|
}
|
|
104
114
|
|
|
115
|
+
function isSensitiveArgFlag(arg: string): boolean {
|
|
116
|
+
return SENSITIVE_ARG_PATTERNS.some((pattern) => pattern.test(arg));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sanitizeArgsForLogging(args: string[]): string[] {
|
|
120
|
+
const sanitized: string[] = [];
|
|
121
|
+
let maskNextValue = false;
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
124
|
+
const arg = args[i]!;
|
|
125
|
+
|
|
126
|
+
if (maskNextValue) {
|
|
127
|
+
if (!arg.startsWith('-')) {
|
|
128
|
+
sanitized.push(MASKED_ARG_VALUE);
|
|
129
|
+
maskNextValue = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
sanitized.push(MASKED_ARG_VALUE);
|
|
134
|
+
maskNextValue = false;
|
|
135
|
+
i -= 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!arg.startsWith('-')) {
|
|
140
|
+
sanitized.push(arg);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const equalsIndex = arg.indexOf('=');
|
|
145
|
+
if (equalsIndex > 0) {
|
|
146
|
+
const flag = arg.slice(0, equalsIndex);
|
|
147
|
+
if (isSensitiveArgFlag(flag)) {
|
|
148
|
+
sanitized.push(`${flag}=${MASKED_ARG_VALUE}`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
sanitized.push(arg);
|
|
154
|
+
if (isSensitiveArgFlag(arg)) {
|
|
155
|
+
maskNextValue = true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return sanitized;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function sanitizeCliCommandForLogging(
|
|
163
|
+
command: string,
|
|
164
|
+
args: string[]
|
|
165
|
+
): { command: string; args: string[] } {
|
|
166
|
+
const commandTokens = command ? command.split(' ') : [];
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
commandTokens.length >= 5 &&
|
|
170
|
+
commandTokens[0] === 'coder' &&
|
|
171
|
+
commandTokens[1] === 'config' &&
|
|
172
|
+
commandTokens[2] === 'set' &&
|
|
173
|
+
commandTokens[3] === 'apikey'
|
|
174
|
+
) {
|
|
175
|
+
commandTokens[4] = MASKED_ARG_VALUE;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
command: commandTokens.join(' '),
|
|
180
|
+
args: sanitizeArgsForLogging(args),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
105
184
|
/**
|
|
106
185
|
* Get the logs directory path
|
|
107
186
|
*/
|
|
@@ -266,14 +345,16 @@ export class InternalLogger implements Logger {
|
|
|
266
345
|
// Use workingDir as cwd in session metadata
|
|
267
346
|
const cwd = workingDir;
|
|
268
347
|
|
|
348
|
+
const sanitizedInvocation = sanitizeCliCommandForLogging(command, args);
|
|
349
|
+
|
|
269
350
|
// Gather session metadata
|
|
270
351
|
const sessionMetadata: SessionMetadata = {
|
|
271
352
|
sessionId: this.sessionId,
|
|
272
353
|
bucket: this.bucket,
|
|
273
354
|
pid: process.pid,
|
|
274
355
|
ppid: process.ppid,
|
|
275
|
-
command,
|
|
276
|
-
args,
|
|
356
|
+
command: sanitizedInvocation.command,
|
|
357
|
+
args: sanitizedInvocation.args,
|
|
277
358
|
timestamp: new Date().toISOString(),
|
|
278
359
|
cli: {
|
|
279
360
|
version: this.cliVersion,
|
package/src/keychain.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
const SERVICE_PREFIX = 'com.agentuity.cli';
|
|
9
9
|
const KEY_ACCOUNT = 'aes-encryption-key';
|
|
10
|
+
const AUTH_ACCOUNT = 'auth-token';
|
|
11
|
+
const CODER_API_KEY_ACCOUNT = 'coder-hub-api-key';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Check if we're running on macOS
|
|
@@ -88,26 +90,15 @@ async function decrypt(combined: Uint8Array, keyBytes: Uint8Array): Promise<stri
|
|
|
88
90
|
return new TextDecoder().decode(plaintext);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
profileName: string,
|
|
96
|
-
authData: { api_key: string; user_id: string; expires: number }
|
|
93
|
+
async function saveEncryptedValueToKeychain(
|
|
94
|
+
service: string,
|
|
95
|
+
account: string,
|
|
96
|
+
value: string
|
|
97
97
|
): Promise<void> {
|
|
98
|
-
const service = `${SERVICE_PREFIX}.${profileName}`;
|
|
99
|
-
const account = 'auth-token';
|
|
100
|
-
|
|
101
|
-
// Get or create encryption key
|
|
102
98
|
const key = await ensureEncryptionKey(service);
|
|
103
|
-
|
|
104
|
-
// Encrypt the auth data
|
|
105
|
-
const json = JSON.stringify(authData);
|
|
106
|
-
const encrypted = await encrypt(json, key);
|
|
99
|
+
const encrypted = await encrypt(value, key);
|
|
107
100
|
const b64 = Buffer.from(encrypted).toString('base64');
|
|
108
101
|
|
|
109
|
-
// Store encrypted auth in keychain
|
|
110
|
-
// First try to delete if exists, then add
|
|
111
102
|
const del = Bun.spawn(['security', 'delete-generic-password', '-s', service, '-a', account], {
|
|
112
103
|
stderr: 'ignore',
|
|
113
104
|
});
|
|
@@ -127,6 +118,43 @@ export async function saveAuthToKeychain(
|
|
|
127
118
|
await add.exited;
|
|
128
119
|
}
|
|
129
120
|
|
|
121
|
+
async function getEncryptedValueFromKeychain(
|
|
122
|
+
service: string,
|
|
123
|
+
account: string
|
|
124
|
+
): Promise<string | null> {
|
|
125
|
+
const find = Bun.spawn(
|
|
126
|
+
['security', 'find-generic-password', '-s', service, '-a', account, '-w'],
|
|
127
|
+
{ stderr: 'ignore' }
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const stdout = await new Response(find.stdout).text();
|
|
131
|
+
if (stdout.length === 0) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const encrypted = Uint8Array.from(Buffer.from(stdout.trim(), 'base64'));
|
|
136
|
+
const key = await ensureEncryptionKey(service);
|
|
137
|
+
return decrypt(encrypted, key);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function deleteValueFromKeychain(service: string, account: string): Promise<void> {
|
|
141
|
+
const del = Bun.spawn(['security', 'delete-generic-password', '-s', service, '-a', account], {
|
|
142
|
+
stderr: 'ignore',
|
|
143
|
+
});
|
|
144
|
+
await del.exited;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Store auth data in macOS Keychain
|
|
149
|
+
*/
|
|
150
|
+
export async function saveAuthToKeychain(
|
|
151
|
+
profileName: string,
|
|
152
|
+
authData: { api_key: string; user_id: string; expires: number }
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
const service = `${SERVICE_PREFIX}.${profileName}`;
|
|
155
|
+
await saveEncryptedValueToKeychain(service, AUTH_ACCOUNT, JSON.stringify(authData));
|
|
156
|
+
}
|
|
157
|
+
|
|
130
158
|
/**
|
|
131
159
|
* Retrieve auth data from macOS Keychain
|
|
132
160
|
*/
|
|
@@ -134,28 +162,12 @@ export async function getAuthFromKeychain(
|
|
|
134
162
|
profileName: string
|
|
135
163
|
): Promise<{ api_key: string; user_id: string; expires: number } | null> {
|
|
136
164
|
const service = `${SERVICE_PREFIX}.${profileName}`;
|
|
137
|
-
const account = 'auth-token';
|
|
138
165
|
|
|
139
166
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
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) {
|
|
167
|
+
const json = await getEncryptedValueFromKeychain(service, AUTH_ACCOUNT);
|
|
168
|
+
if (!json) {
|
|
148
169
|
return null;
|
|
149
170
|
}
|
|
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
171
|
return JSON.parse(json);
|
|
160
172
|
} catch {
|
|
161
173
|
return null;
|
|
@@ -167,10 +179,27 @@ export async function getAuthFromKeychain(
|
|
|
167
179
|
*/
|
|
168
180
|
export async function deleteAuthFromKeychain(profileName: string): Promise<void> {
|
|
169
181
|
const service = `${SERVICE_PREFIX}.${profileName}`;
|
|
170
|
-
|
|
182
|
+
await deleteValueFromKeychain(service, AUTH_ACCOUNT);
|
|
183
|
+
}
|
|
171
184
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
185
|
+
export async function saveCoderApiKeyToKeychain(
|
|
186
|
+
profileName: string,
|
|
187
|
+
apiKey: string
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
const service = `${SERVICE_PREFIX}.${profileName}`;
|
|
190
|
+
await saveEncryptedValueToKeychain(service, CODER_API_KEY_ACCOUNT, apiKey);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function getCoderApiKeyFromKeychain(profileName: string): Promise<string | null> {
|
|
194
|
+
const service = `${SERVICE_PREFIX}.${profileName}`;
|
|
195
|
+
try {
|
|
196
|
+
return await getEncryptedValueFromKeychain(service, CODER_API_KEY_ACCOUNT);
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function deleteCoderApiKeyFromKeychain(profileName: string): Promise<void> {
|
|
203
|
+
const service = `${SERVICE_PREFIX}.${profileName}`;
|
|
204
|
+
await deleteValueFromKeychain(service, CODER_API_KEY_ACCOUNT);
|
|
176
205
|
}
|