@agentuity/cli 2.0.5 → 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.
Files changed (128) hide show
  1. package/README.md +11 -0
  2. package/dist/cmd/build/patch/otel-llm.js +2 -2
  3. package/dist/cmd/build/patch/otel-llm.js.map +1 -1
  4. package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
  5. package/dist/cmd/build/vite/bun-dev-server.js +1 -0
  6. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  7. package/dist/cmd/build/vite/index.d.ts +0 -28
  8. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  9. package/dist/cmd/build/vite/index.js +1 -104
  10. package/dist/cmd/build/vite/index.js.map +1 -1
  11. package/dist/cmd/build/vite/metadata-generator.d.ts.map +1 -1
  12. package/dist/cmd/build/vite/metadata-generator.js +8 -2
  13. package/dist/cmd/build/vite/metadata-generator.js.map +1 -1
  14. package/dist/cmd/build/vite/vite-asset-server-config.d.ts +2 -0
  15. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  16. package/dist/cmd/build/vite/vite-asset-server-config.js +5 -1
  17. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  18. package/dist/cmd/build/vite/vite-asset-server.d.ts +2 -0
  19. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  20. package/dist/cmd/build/vite/vite-asset-server.js +2 -1
  21. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  22. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  23. package/dist/cmd/build/vite/vite-builder.js +143 -2
  24. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  25. package/dist/cmd/cloud/task/close.d.ts +3 -0
  26. package/dist/cmd/cloud/task/close.d.ts.map +1 -0
  27. package/dist/cmd/cloud/task/close.js +286 -0
  28. package/dist/cmd/cloud/task/close.js.map +1 -0
  29. package/dist/cmd/cloud/task/delete.d.ts +1 -5
  30. package/dist/cmd/cloud/task/delete.d.ts.map +1 -1
  31. package/dist/cmd/cloud/task/delete.js +15 -38
  32. package/dist/cmd/cloud/task/delete.js.map +1 -1
  33. package/dist/cmd/cloud/task/index.d.ts.map +1 -1
  34. package/dist/cmd/cloud/task/index.js +10 -0
  35. package/dist/cmd/cloud/task/index.js.map +1 -1
  36. package/dist/cmd/cloud/task/list.d.ts.map +1 -1
  37. package/dist/cmd/cloud/task/list.js +97 -3
  38. package/dist/cmd/cloud/task/list.js.map +1 -1
  39. package/dist/cmd/cloud/task/util.d.ts +10 -0
  40. package/dist/cmd/cloud/task/util.d.ts.map +1 -1
  41. package/dist/cmd/cloud/task/util.js +47 -3
  42. package/dist/cmd/cloud/task/util.js.map +1 -1
  43. package/dist/cmd/coder/config/index.d.ts +2 -0
  44. package/dist/cmd/coder/config/index.d.ts.map +1 -0
  45. package/dist/cmd/coder/config/index.js +20 -0
  46. package/dist/cmd/coder/config/index.js.map +1 -0
  47. package/dist/cmd/coder/config/set.d.ts +2 -0
  48. package/dist/cmd/coder/config/set.d.ts.map +1 -0
  49. package/dist/cmd/coder/config/set.js +100 -0
  50. package/dist/cmd/coder/config/set.js.map +1 -0
  51. package/dist/cmd/coder/hub-url.d.ts +21 -10
  52. package/dist/cmd/coder/hub-url.d.ts.map +1 -1
  53. package/dist/cmd/coder/hub-url.js +97 -55
  54. package/dist/cmd/coder/hub-url.js.map +1 -1
  55. package/dist/cmd/coder/index.d.ts.map +1 -1
  56. package/dist/cmd/coder/index.js +6 -1
  57. package/dist/cmd/coder/index.js.map +1 -1
  58. package/dist/cmd/coder/inspect.d.ts.map +1 -1
  59. package/dist/cmd/coder/inspect.js +15 -7
  60. package/dist/cmd/coder/inspect.js.map +1 -1
  61. package/dist/cmd/coder/list.d.ts.map +1 -1
  62. package/dist/cmd/coder/list.js +14 -7
  63. package/dist/cmd/coder/list.js.map +1 -1
  64. package/dist/cmd/coder/start.d.ts.map +1 -1
  65. package/dist/cmd/coder/start.js +38 -23
  66. package/dist/cmd/coder/start.js.map +1 -1
  67. package/dist/cmd/coder/tui-init.d.ts +4 -1
  68. package/dist/cmd/coder/tui-init.d.ts.map +1 -1
  69. package/dist/cmd/coder/tui-init.js +3 -2
  70. package/dist/cmd/coder/tui-init.js.map +1 -1
  71. package/dist/cmd/dev/index.d.ts.map +1 -1
  72. package/dist/cmd/dev/index.js +1 -0
  73. package/dist/cmd/dev/index.js.map +1 -1
  74. package/dist/cmd/dev/sync.js +5 -5
  75. package/dist/cmd/dev/sync.js.map +1 -1
  76. package/dist/coder-config.d.ts +14 -0
  77. package/dist/coder-config.d.ts.map +1 -0
  78. package/dist/coder-config.js +119 -0
  79. package/dist/coder-config.js.map +1 -0
  80. package/dist/coder-hub-url.d.ts +3 -0
  81. package/dist/coder-hub-url.d.ts.map +1 -0
  82. package/dist/coder-hub-url.js +32 -0
  83. package/dist/coder-hub-url.js.map +1 -0
  84. package/dist/config.d.ts +1 -0
  85. package/dist/config.d.ts.map +1 -1
  86. package/dist/config.js +11 -0
  87. package/dist/config.js.map +1 -1
  88. package/dist/internal-logger.d.ts +4 -0
  89. package/dist/internal-logger.d.ts.map +1 -1
  90. package/dist/internal-logger.js +64 -2
  91. package/dist/internal-logger.js.map +1 -1
  92. package/dist/keychain.d.ts +3 -0
  93. package/dist/keychain.d.ts.map +1 -1
  94. package/dist/keychain.js +47 -28
  95. package/dist/keychain.js.map +1 -1
  96. package/dist/types.d.ts +4 -0
  97. package/dist/types.d.ts.map +1 -1
  98. package/dist/types.js +10 -0
  99. package/dist/types.js.map +1 -1
  100. package/package.json +6 -6
  101. package/src/cmd/build/patch/otel-llm.ts +2 -2
  102. package/src/cmd/build/vite/bun-dev-server.ts +1 -0
  103. package/src/cmd/build/vite/index.ts +1 -148
  104. package/src/cmd/build/vite/metadata-generator.ts +8 -2
  105. package/src/cmd/build/vite/vite-asset-server-config.ts +16 -1
  106. package/src/cmd/build/vite/vite-asset-server.ts +4 -0
  107. package/src/cmd/build/vite/vite-builder.ts +171 -9
  108. package/src/cmd/cloud/task/close.ts +319 -0
  109. package/src/cmd/cloud/task/delete.ts +15 -43
  110. package/src/cmd/cloud/task/index.ts +10 -0
  111. package/src/cmd/cloud/task/list.ts +111 -4
  112. package/src/cmd/cloud/task/util.ts +59 -5
  113. package/src/cmd/coder/config/index.ts +20 -0
  114. package/src/cmd/coder/config/set.ts +112 -0
  115. package/src/cmd/coder/hub-url.ts +147 -53
  116. package/src/cmd/coder/index.ts +6 -1
  117. package/src/cmd/coder/inspect.ts +33 -10
  118. package/src/cmd/coder/list.ts +33 -10
  119. package/src/cmd/coder/start.ts +62 -26
  120. package/src/cmd/coder/tui-init.ts +7 -2
  121. package/src/cmd/dev/index.ts +1 -0
  122. package/src/cmd/dev/sync.ts +5 -5
  123. package/src/coder-config.ts +141 -0
  124. package/src/coder-hub-url.ts +32 -0
  125. package/src/config.ts +13 -0
  126. package/src/internal-logger.ts +83 -2
  127. package/src/keychain.ts +68 -39
  128. package/src/types.ts +10 -0
@@ -1,4 +1,5 @@
1
1
  import { createCommand } from '../../types';
2
+ import { configSubcommand } from './config';
2
3
  import { listSubcommand } from './list';
3
4
  import { inspectSubcommand } from './inspect';
4
5
  import { startSubcommand } from './start';
@@ -21,7 +22,11 @@ export const command = createCommand({
21
22
  command: getCommand('coder inspect <session-id>'),
22
23
  description: 'Show detailed session information',
23
24
  },
25
+ {
26
+ command: getCommand('coder config set url https://hub.example.com'),
27
+ description: 'Set the default Coder Hub URL for this profile',
28
+ },
24
29
  ],
25
- subcommands: [startSubcommand, listSubcommand, inspectSubcommand],
30
+ subcommands: [startSubcommand, listSubcommand, inspectSubcommand, configSubcommand],
26
31
  optional: { auth: true },
27
32
  });
@@ -3,7 +3,17 @@ import { createSubcommand } from '../../types';
3
3
  import * as tui from '../../tui';
4
4
  import { getCommand } from '../../command-prefix';
5
5
  import { ErrorCode } from '../../errors';
6
- import { resolveHubUrl, hubFetchHeaders } from './hub-url';
6
+ import {
7
+ clearStoredHubApiKeyOnUnauthorized,
8
+ formatHubUnauthorizedMessage,
9
+ formatMissingHubUrlMessage,
10
+ getHubResponseErrorMessage,
11
+ getHubUrlSetupGuidance,
12
+ hubFetchHeaders,
13
+ isHubUnauthorizedStatus,
14
+ resolveHubApiKey,
15
+ resolveHubUrl,
16
+ } from './hub-url';
7
17
 
8
18
  function formatRelativeTime(isoDate: string): string {
9
19
  const diffMs = Date.now() - new Date(isoDate).getTime();
@@ -52,18 +62,17 @@ export const inspectSubcommand = createSubcommand({
52
62
  }),
53
63
  },
54
64
  async handler(ctx) {
55
- const { args, options, opts } = ctx;
65
+ const { args, options, opts, config } = ctx;
56
66
  const sessionId = args.session_id;
57
- const hubUrl = await resolveHubUrl(opts?.hubUrl);
67
+ const hubUrl = await resolveHubUrl(opts?.hubUrl, config);
58
68
 
59
69
  if (!hubUrl) {
60
- tui.fatal(
61
- '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',
62
- ErrorCode.NETWORK_ERROR
63
- );
70
+ tui.fatal(formatMissingHubUrlMessage(), ErrorCode.NETWORK_ERROR);
64
71
  return;
65
72
  }
66
73
 
74
+ const resolvedHubApiKey = await resolveHubApiKey(config);
75
+
67
76
  let data: {
68
77
  sessionId: string;
69
78
  label: string;
@@ -104,9 +113,22 @@ export const inspectSubcommand = createSubcommand({
104
113
 
105
114
  try {
106
115
  const resp = await fetch(`${hubUrl}/api/hub/session/${encodeURIComponent(sessionId)}`, {
107
- headers: hubFetchHeaders(),
116
+ headers: hubFetchHeaders(undefined, resolvedHubApiKey.apiKey),
108
117
  signal: AbortSignal.timeout(10_000),
109
118
  });
119
+ if (isHubUnauthorizedStatus(resp.status)) {
120
+ const message = await getHubResponseErrorMessage(resp);
121
+ const clearedStoredKey = await clearStoredHubApiKeyOnUnauthorized(
122
+ resp.status,
123
+ resolvedHubApiKey,
124
+ config
125
+ );
126
+ tui.fatal(
127
+ formatHubUnauthorizedMessage(hubUrl, message, { clearedStoredKey }),
128
+ ErrorCode.API_ERROR
129
+ );
130
+ return;
131
+ }
110
132
  if (resp.status === 404) {
111
133
  tui.fatal(`Session not found: ${sessionId}`, ErrorCode.RESOURCE_NOT_FOUND);
112
134
  return;
@@ -116,8 +138,9 @@ export const inspectSubcommand = createSubcommand({
116
138
  return;
117
139
  }
118
140
  if (!resp.ok) {
141
+ const message = await getHubResponseErrorMessage(resp);
119
142
  tui.fatal(
120
- `Hub returned ${resp.status}: ${resp.statusText}. Is the Coder Hub running at ${hubUrl}?`,
143
+ `Hub returned ${resp.status}: ${message}. Is the Coder Hub running at ${hubUrl}?`,
121
144
  ErrorCode.API_ERROR
122
145
  );
123
146
  return;
@@ -126,7 +149,7 @@ export const inspectSubcommand = createSubcommand({
126
149
  } catch (err) {
127
150
  const msg = err instanceof Error ? err.message : String(err);
128
151
  tui.fatal(
129
- `Could not connect to Coder Hub at ${hubUrl}: ${msg}\n\nSet AGENTUITY_CODER_HUB_URL or start the Hub with: bun run dev`,
152
+ `Could not connect to Coder Hub at ${hubUrl}: ${msg}\n\n${getHubUrlSetupGuidance()}`,
130
153
  ErrorCode.NETWORK_ERROR
131
154
  );
132
155
  return;
@@ -3,7 +3,17 @@ import { createSubcommand } from '../../types';
3
3
  import * as tui from '../../tui';
4
4
  import { getCommand } from '../../command-prefix';
5
5
  import { ErrorCode } from '../../errors';
6
- import { resolveHubUrl, hubFetchHeaders } from './hub-url';
6
+ import {
7
+ clearStoredHubApiKeyOnUnauthorized,
8
+ formatHubUnauthorizedMessage,
9
+ formatMissingHubUrlMessage,
10
+ getHubResponseErrorMessage,
11
+ getHubUrlSetupGuidance,
12
+ hubFetchHeaders,
13
+ isHubUnauthorizedStatus,
14
+ resolveHubApiKey,
15
+ resolveHubUrl,
16
+ } from './hub-url';
7
17
 
8
18
  function formatRelativeTime(isoDate: string): string {
9
19
  const diffMs = Date.now() - new Date(isoDate).getTime();
@@ -54,17 +64,16 @@ export const listSubcommand = createSubcommand({
54
64
  response: SessionListResponseSchema,
55
65
  },
56
66
  async handler(ctx) {
57
- const { options, opts } = ctx;
58
- const hubUrl = await resolveHubUrl(opts?.hubUrl);
67
+ const { options, opts, config } = ctx;
68
+ const hubUrl = await resolveHubUrl(opts?.hubUrl, config);
59
69
 
60
70
  if (!hubUrl) {
61
- tui.fatal(
62
- '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',
63
- ErrorCode.NETWORK_ERROR
64
- );
71
+ tui.fatal(formatMissingHubUrlMessage(), ErrorCode.NETWORK_ERROR);
65
72
  return [];
66
73
  }
67
74
 
75
+ const resolvedHubApiKey = await resolveHubApiKey(config);
76
+
68
77
  let data: {
69
78
  sessions: {
70
79
  websocket: Array<{
@@ -85,12 +94,26 @@ export const listSubcommand = createSubcommand({
85
94
 
86
95
  try {
87
96
  const resp = await fetch(`${hubUrl}/api/hub/sessions`, {
88
- headers: hubFetchHeaders(),
97
+ headers: hubFetchHeaders(undefined, resolvedHubApiKey.apiKey),
89
98
  signal: AbortSignal.timeout(10_000),
90
99
  });
91
100
  if (!resp.ok) {
101
+ const message = await getHubResponseErrorMessage(resp);
102
+ if (isHubUnauthorizedStatus(resp.status)) {
103
+ const clearedStoredKey = await clearStoredHubApiKeyOnUnauthorized(
104
+ resp.status,
105
+ resolvedHubApiKey,
106
+ config
107
+ );
108
+ tui.fatal(
109
+ formatHubUnauthorizedMessage(hubUrl, message, { clearedStoredKey }),
110
+ ErrorCode.API_ERROR
111
+ );
112
+ return [];
113
+ }
114
+
92
115
  tui.fatal(
93
- `Hub returned ${resp.status}: ${resp.statusText}. Is the Coder Hub running at ${hubUrl}?`,
116
+ `Hub returned ${resp.status}: ${message}. Is the Coder Hub running at ${hubUrl}?`,
94
117
  ErrorCode.API_ERROR
95
118
  );
96
119
  return [];
@@ -99,7 +122,7 @@ export const listSubcommand = createSubcommand({
99
122
  } catch (err) {
100
123
  const msg = err instanceof Error ? err.message : String(err);
101
124
  tui.fatal(
102
- `Could not connect to Coder Hub at ${hubUrl}: ${msg}\n\nSet AGENTUITY_CODER_HUB_URL or start the Hub with: bun run dev`,
125
+ `Could not connect to Coder Hub at ${hubUrl}: ${msg}\n\n${getHubUrlSetupGuidance()}`,
103
126
  ErrorCode.NETWORK_ERROR
104
127
  );
105
128
  return [];
@@ -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 { toHubWsUrl, resolveHubUrl, hubFetchHeaders } from './hub-url';
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
- `Coder Hub at ${hubHttpUrl} requires authentication.\n\nSet AGENTUITY_CODER_API_KEY in your shell and retry.\n\nServer said: ${initProbe.message}`,
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.statusText}`);
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
- 'No connectable sandbox sessions found.\n\nCreate one with: ag-dev coder session create --task "your task"',
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({ 'Content-Type': 'application/json' }),
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} ${errText}`,
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
- // TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
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
- fetchImpl: typeof fetch = fetch
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
 
@@ -552,6 +552,7 @@ export const command = createCommand({
552
552
  port: viteInternalPort,
553
553
  backendPort: bunBackendPort,
554
554
  routePaths,
555
+ liveHostname: devmode?.hostname,
555
556
  });
556
557
  viteServer = viteResult.server;
557
558
  vitePort = viteResult.port;
@@ -169,7 +169,7 @@ class DevmodeSyncService implements IDevmodeSyncService {
169
169
  }
170
170
  }
171
171
  }
172
- this.logger.debug('Previous metadata found with %d eval(s)', prevEvalCount);
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 eval(s)',
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 eval(s): %d', currentEvalCount);
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 eval(s) to create, %d eval(s) to delete',
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 eval(s) to create, %d eval(s) to delete',
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,