@agentuity/cli 2.0.7 → 2.0.9

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 (176) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +4 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
  5. package/dist/cmd/build/vite/route-discovery.js +6 -0
  6. package/dist/cmd/build/vite/route-discovery.js.map +1 -1
  7. package/dist/cmd/cloud/sandbox/fs/rm.d.ts.map +1 -1
  8. package/dist/cmd/cloud/sandbox/fs/rm.js +9 -3
  9. package/dist/cmd/cloud/sandbox/fs/rm.js.map +1 -1
  10. package/dist/cmd/cloud/sandbox/fs/rmdir.d.ts.map +1 -1
  11. package/dist/cmd/cloud/sandbox/fs/rmdir.js +9 -3
  12. package/dist/cmd/cloud/sandbox/fs/rmdir.js.map +1 -1
  13. package/dist/cmd/coder/archive.d.ts +2 -0
  14. package/dist/cmd/coder/archive.d.ts.map +1 -0
  15. package/dist/cmd/coder/archive.js +57 -0
  16. package/dist/cmd/coder/archive.js.map +1 -0
  17. package/dist/cmd/coder/create.d.ts +2 -0
  18. package/dist/cmd/coder/create.d.ts.map +1 -0
  19. package/dist/cmd/coder/create.js +245 -0
  20. package/dist/cmd/coder/create.js.map +1 -0
  21. package/dist/cmd/coder/delete.d.ts +2 -0
  22. package/dist/cmd/coder/delete.d.ts.map +1 -0
  23. package/dist/cmd/coder/delete.js +64 -0
  24. package/dist/cmd/coder/delete.js.map +1 -0
  25. package/dist/cmd/coder/events.d.ts +2 -0
  26. package/dist/cmd/coder/events.d.ts.map +1 -0
  27. package/dist/cmd/coder/events.js +99 -0
  28. package/dist/cmd/coder/events.js.map +1 -0
  29. package/dist/cmd/coder/extension-path.d.ts +8 -0
  30. package/dist/cmd/coder/extension-path.d.ts.map +1 -0
  31. package/dist/cmd/coder/extension-path.js +59 -0
  32. package/dist/cmd/coder/extension-path.js.map +1 -0
  33. package/dist/cmd/coder/get.d.ts +2 -0
  34. package/dist/cmd/coder/get.d.ts.map +1 -0
  35. package/dist/cmd/coder/{inspect.js → get.js} +38 -42
  36. package/dist/cmd/coder/get.js.map +1 -0
  37. package/dist/cmd/coder/index.d.ts.map +1 -1
  38. package/dist/cmd/coder/index.js +52 -7
  39. package/dist/cmd/coder/index.js.map +1 -1
  40. package/dist/cmd/coder/list.d.ts.map +1 -1
  41. package/dist/cmd/coder/list.js +26 -42
  42. package/dist/cmd/coder/list.js.map +1 -1
  43. package/dist/cmd/coder/loop.d.ts +2 -0
  44. package/dist/cmd/coder/loop.d.ts.map +1 -0
  45. package/dist/cmd/coder/loop.js +78 -0
  46. package/dist/cmd/coder/loop.js.map +1 -0
  47. package/dist/cmd/coder/participants.d.ts +2 -0
  48. package/dist/cmd/coder/participants.d.ts.map +1 -0
  49. package/dist/cmd/coder/participants.js +93 -0
  50. package/dist/cmd/coder/participants.js.map +1 -0
  51. package/dist/cmd/coder/replay.d.ts +2 -0
  52. package/dist/cmd/coder/replay.d.ts.map +1 -0
  53. package/dist/cmd/coder/replay.js +53 -0
  54. package/dist/cmd/coder/replay.js.map +1 -0
  55. package/dist/cmd/coder/resolve-repo.d.ts +27 -0
  56. package/dist/cmd/coder/resolve-repo.d.ts.map +1 -0
  57. package/dist/cmd/coder/resolve-repo.js +97 -0
  58. package/dist/cmd/coder/resolve-repo.js.map +1 -0
  59. package/dist/cmd/coder/skill/buckets.d.ts +2 -0
  60. package/dist/cmd/coder/skill/buckets.d.ts.map +1 -0
  61. package/dist/cmd/coder/skill/buckets.js +174 -0
  62. package/dist/cmd/coder/skill/buckets.js.map +1 -0
  63. package/dist/cmd/coder/skill/delete.d.ts +2 -0
  64. package/dist/cmd/coder/skill/delete.d.ts.map +1 -0
  65. package/dist/cmd/coder/skill/delete.js +64 -0
  66. package/dist/cmd/coder/skill/delete.js.map +1 -0
  67. package/dist/cmd/coder/skill/index.d.ts +2 -0
  68. package/dist/cmd/coder/skill/index.d.ts.map +1 -0
  69. package/dist/cmd/coder/skill/index.js +33 -0
  70. package/dist/cmd/coder/skill/index.js.map +1 -0
  71. package/dist/cmd/coder/skill/list.d.ts +2 -0
  72. package/dist/cmd/coder/skill/list.d.ts.map +1 -0
  73. package/dist/cmd/coder/skill/list.js +93 -0
  74. package/dist/cmd/coder/skill/list.js.map +1 -0
  75. package/dist/cmd/coder/skill/save.d.ts +2 -0
  76. package/dist/cmd/coder/skill/save.d.ts.map +1 -0
  77. package/dist/cmd/coder/skill/save.js +77 -0
  78. package/dist/cmd/coder/skill/save.js.map +1 -0
  79. package/dist/cmd/coder/start.d.ts.map +1 -1
  80. package/dist/cmd/coder/start.js +87 -131
  81. package/dist/cmd/coder/start.js.map +1 -1
  82. package/dist/cmd/coder/tui-init.d.ts.map +1 -1
  83. package/dist/cmd/coder/tui-init.js +7 -2
  84. package/dist/cmd/coder/tui-init.js.map +1 -1
  85. package/dist/cmd/coder/update.d.ts +2 -0
  86. package/dist/cmd/coder/update.d.ts.map +1 -0
  87. package/dist/cmd/coder/update.js +126 -0
  88. package/dist/cmd/coder/update.js.map +1 -0
  89. package/dist/cmd/coder/users.d.ts +2 -0
  90. package/dist/cmd/coder/users.d.ts.map +1 -0
  91. package/dist/cmd/coder/users.js +97 -0
  92. package/dist/cmd/coder/users.js.map +1 -0
  93. package/dist/cmd/coder/workspace/create.d.ts +2 -0
  94. package/dist/cmd/coder/workspace/create.d.ts.map +1 -0
  95. package/dist/cmd/coder/workspace/create.js +97 -0
  96. package/dist/cmd/coder/workspace/create.js.map +1 -0
  97. package/dist/cmd/coder/workspace/delete.d.ts +2 -0
  98. package/dist/cmd/coder/workspace/delete.d.ts.map +1 -0
  99. package/dist/cmd/coder/workspace/delete.js +64 -0
  100. package/dist/cmd/coder/workspace/delete.js.map +1 -0
  101. package/dist/cmd/coder/workspace/get.d.ts +2 -0
  102. package/dist/cmd/coder/workspace/get.d.ts.map +1 -0
  103. package/dist/cmd/coder/workspace/get.js +109 -0
  104. package/dist/cmd/coder/workspace/get.js.map +1 -0
  105. package/dist/cmd/coder/workspace/index.d.ts +2 -0
  106. package/dist/cmd/coder/workspace/index.d.ts.map +1 -0
  107. package/dist/cmd/coder/workspace/index.js +38 -0
  108. package/dist/cmd/coder/workspace/index.js.map +1 -0
  109. package/dist/cmd/coder/workspace/list.d.ts +2 -0
  110. package/dist/cmd/coder/workspace/list.d.ts.map +1 -0
  111. package/dist/cmd/coder/workspace/list.js +93 -0
  112. package/dist/cmd/coder/workspace/list.js.map +1 -0
  113. package/dist/config.d.ts.map +1 -1
  114. package/dist/config.js +3 -3
  115. package/dist/config.js.map +1 -1
  116. package/dist/types.d.ts +1 -5
  117. package/dist/types.d.ts.map +1 -1
  118. package/dist/types.js +0 -10
  119. package/dist/types.js.map +1 -1
  120. package/package.json +7 -6
  121. package/src/cli.ts +4 -2
  122. package/src/cmd/ai/prompt/agent.md +6 -6
  123. package/src/cmd/build/vite/route-discovery.ts +8 -0
  124. package/src/cmd/cloud/sandbox/fs/rm.ts +8 -3
  125. package/src/cmd/cloud/sandbox/fs/rmdir.ts +8 -3
  126. package/src/cmd/coder/archive.ts +59 -0
  127. package/src/cmd/coder/create.ts +268 -0
  128. package/src/cmd/coder/delete.ts +67 -0
  129. package/src/cmd/coder/events.ts +106 -0
  130. package/src/cmd/coder/extension-path.ts +71 -0
  131. package/src/cmd/coder/{inspect.ts → get.ts} +45 -69
  132. package/src/cmd/coder/index.ts +52 -7
  133. package/src/cmd/coder/list.ts +29 -89
  134. package/src/cmd/coder/loop.ts +85 -0
  135. package/src/cmd/coder/participants.ts +100 -0
  136. package/src/cmd/coder/replay.ts +58 -0
  137. package/src/cmd/coder/resolve-repo.ts +119 -0
  138. package/src/cmd/coder/skill/buckets.ts +191 -0
  139. package/src/cmd/coder/skill/delete.ts +67 -0
  140. package/src/cmd/coder/skill/index.ts +35 -0
  141. package/src/cmd/coder/skill/list.ts +97 -0
  142. package/src/cmd/coder/skill/save.ts +84 -0
  143. package/src/cmd/coder/start.ts +101 -174
  144. package/src/cmd/coder/tui-init.ts +7 -3
  145. package/src/cmd/coder/update.ts +128 -0
  146. package/src/cmd/coder/users.ts +101 -0
  147. package/src/cmd/coder/workspace/create.ts +104 -0
  148. package/src/cmd/coder/workspace/delete.ts +70 -0
  149. package/src/cmd/coder/workspace/get.ts +112 -0
  150. package/src/cmd/coder/workspace/index.ts +38 -0
  151. package/src/cmd/coder/workspace/list.ts +101 -0
  152. package/src/config.ts +4 -3
  153. package/src/types.ts +0 -10
  154. package/dist/cmd/coder/config/index.d.ts +0 -2
  155. package/dist/cmd/coder/config/index.d.ts.map +0 -1
  156. package/dist/cmd/coder/config/index.js +0 -20
  157. package/dist/cmd/coder/config/index.js.map +0 -1
  158. package/dist/cmd/coder/config/set.d.ts +0 -2
  159. package/dist/cmd/coder/config/set.d.ts.map +0 -1
  160. package/dist/cmd/coder/config/set.js +0 -100
  161. package/dist/cmd/coder/config/set.js.map +0 -1
  162. package/dist/cmd/coder/hub-url.d.ts +0 -47
  163. package/dist/cmd/coder/hub-url.d.ts.map +0 -1
  164. package/dist/cmd/coder/hub-url.js +0 -148
  165. package/dist/cmd/coder/hub-url.js.map +0 -1
  166. package/dist/cmd/coder/inspect.d.ts +0 -2
  167. package/dist/cmd/coder/inspect.d.ts.map +0 -1
  168. package/dist/cmd/coder/inspect.js.map +0 -1
  169. package/dist/coder-config.d.ts +0 -14
  170. package/dist/coder-config.d.ts.map +0 -1
  171. package/dist/coder-config.js +0 -119
  172. package/dist/coder-config.js.map +0 -1
  173. package/src/cmd/coder/config/index.ts +0 -20
  174. package/src/cmd/coder/config/set.ts +0 -112
  175. package/src/cmd/coder/hub-url.ts +0 -205
  176. package/src/coder-config.ts +0 -141
@@ -1,95 +1,75 @@
1
+ import { dirname, resolve } from 'node:path';
1
2
  import { z } from 'zod';
2
- import { existsSync } from 'node:fs';
3
- import { resolve, dirname, join } from 'node:path';
3
+ import { CoderClient, type CoderSessionListItem } from '@agentuity/core/coder';
4
+ import { ValidationOutputError } from '@agentuity/core';
5
+ import { toCoderHubWsUrl } from '../../coder-hub-url';
4
6
  import { createSubcommand } from '../../types';
5
7
  import * as tui from '../../tui';
6
8
  import { getCommand } from '../../command-prefix';
7
9
  import { ErrorCode } from '../../errors';
8
- import {
9
- clearStoredHubApiKeyOnUnauthorized,
10
- formatHubUnauthorizedMessage,
11
- formatMissingHubUrlMessage,
12
- getHubResponseErrorMessage,
13
- hubFetchHeaders,
14
- isHubUnauthorizedStatus,
15
- resolveHubApiKey,
16
- resolveHubUrl,
17
- toHubWsUrl,
18
- } from './hub-url';
10
+ import { resolveExtensionPath, resolveExtensionRuntimeModulePath } from './extension-path';
19
11
  import { probeHubInitAccess } from './tui-init';
20
12
 
21
- /**
22
- * Resolve the Coder extension path.
23
- *
24
- * Priority:
25
- * 1. --extension flag (explicit override)
26
- * 2. AGENTUITY_CODER_EXTENSION env var
27
- * 3. Installed @agentuity/coder package (node_modules)
28
- * 4. Local dev path relative to CLI package (SDK monorepo)
29
- */
30
- function resolveExtensionPath(flagPath?: string): string | null {
31
- // 1. Explicit flag
32
- if (flagPath) {
33
- const resolved = resolve(flagPath);
34
- if (existsSync(resolved)) return resolved;
35
- return null;
36
- }
37
-
38
- // 2. Env var
39
- const envPath = process.env.AGENTUITY_CODER_EXTENSION;
40
- if (envPath) {
41
- const resolved = resolve(envPath);
42
- if (existsSync(resolved)) return resolved;
43
- }
44
-
45
- // 3. Installed npm package in cwd
46
- const cwdNodeModules = resolve(process.cwd(), 'node_modules', '@agentuity', 'coder');
47
- if (existsSync(cwdNodeModules)) return cwdNodeModules;
48
-
49
- // 4. SDK monorepo sibling (for development)
50
- // This file is at packages/cli/src/cmd/coder/start.ts — 5 levels up to SDK root
51
- try {
52
- const cliDir = dirname(new URL(import.meta.url).pathname);
53
- const sdkRoot = resolve(cliDir, '..', '..', '..', '..', '..');
54
- const coderPath = join(sdkRoot, 'packages', 'coder');
55
- if (existsSync(join(coderPath, 'src', 'index.ts'))) return coderPath;
56
- } catch {
57
- // Not in SDK monorepo
58
- }
59
-
60
- return null;
61
- }
62
-
63
13
  /**
64
14
  * Find the `pi` binary.
65
15
  *
66
16
  * Priority:
67
17
  * 1. --pi flag (explicit override)
68
18
  * 2. AGENTUITY_CODER_PI_PATH env var
69
- * 3. `pi` on PATH (default)
19
+ * 3. Bundled pi from coder-tui's node_modules/.bin/pi
20
+ * 4. `pi` on PATH (fallback)
70
21
  */
71
- function resolvePiBinary(flagPath?: string): string {
22
+ async function resolvePiBinary(flagPath?: string, extensionDir?: string): Promise<string> {
72
23
  if (flagPath) return flagPath;
73
24
  const envPath = process.env.AGENTUITY_CODER_PI_PATH;
74
25
  if (envPath) return envPath;
26
+
27
+ // Look for pi bundled with the coder-tui extension
28
+ if (extensionDir) {
29
+ // Prefer require.resolve via package.json — handles hoisted deps, Bun's .bun cache, etc.
30
+ try {
31
+ const pkgJson = require.resolve('@mariozechner/pi-coding-agent/package.json', {
32
+ paths: [extensionDir],
33
+ });
34
+ const piCli = resolve(dirname(pkgJson), 'dist', 'cli.js');
35
+ if (await Bun.file(piCli).exists()) return piCli;
36
+ } catch {
37
+ // Fallback: direct .bin symlink check
38
+ const bundledPi = resolve(extensionDir, 'node_modules', '.bin', 'pi');
39
+ if (await Bun.file(bundledPi).exists()) return bundledPi;
40
+ }
41
+ }
42
+
75
43
  return 'pi';
76
44
  }
77
45
 
46
+ function logValidationIssues(
47
+ ctx: { logger: { trace: (...args: unknown[]) => void } },
48
+ err: unknown
49
+ ): void {
50
+ if (err instanceof ValidationOutputError) {
51
+ ctx.logger.trace('Validation response URL: %s', err.url ?? 'unknown');
52
+ ctx.logger.trace('Validation issues: %s', JSON.stringify(err.issues, null, 2));
53
+ }
54
+ }
55
+
78
56
  export const startSubcommand = createSubcommand({
79
57
  name: 'start',
58
+ aliases: ['run'],
80
59
  description: 'Start a Pi coding session connected to the Coder Hub',
81
60
  tags: ['fast', 'requires-auth'],
61
+ requires: { auth: true, org: true },
82
62
  examples: [
83
63
  {
84
64
  command: getCommand('coder start'),
85
65
  description: 'Start Pi with auto-detected Hub and extension',
86
66
  },
87
67
  {
88
- command: getCommand('coder start --hub-url ws://127.0.0.1:3500/api/ws'),
68
+ command: getCommand('coder start --url ws://127.0.0.1:3500/api/ws'),
89
69
  description: 'Start with explicit Hub URL',
90
70
  },
91
71
  {
92
- command: getCommand('coder start --extension ~/repos/agentuity/sdk/packages/coder'),
72
+ command: getCommand('coder start --extension ~/repos/agentuity/sdk/packages/coder-tui'),
93
73
  description: 'Start with explicit extension path',
94
74
  },
95
75
  {
@@ -97,7 +77,7 @@ export const startSubcommand = createSubcommand({
97
77
  description: 'Start as a specific agent role',
98
78
  },
99
79
  {
100
- command: getCommand('coder start --remote codesess_abc123'),
80
+ command: getCommand('coder start --remote codesess_abc123456789'),
101
81
  description: 'Connect to an existing sandbox session remotely',
102
82
  },
103
83
  {
@@ -117,7 +97,7 @@ export const startSubcommand = createSubcommand({
117
97
  ],
118
98
  schema: {
119
99
  options: z.object({
120
- hubUrl: z.string().optional().describe('Hub WebSocket URL override'),
100
+ url: z.string().optional().describe('Coder API URL override'),
121
101
  extension: z.string().optional().describe('Coder extension path override'),
122
102
  pi: z.string().optional().describe('Path to pi binary'),
123
103
  agent: z.string().optional().describe('Agent role (e.g. scout, builder)'),
@@ -135,51 +115,25 @@ export const startSubcommand = createSubcommand({
135
115
  .optional()
136
116
  .describe('Git repo URL to clone in the sandbox (used with --sandbox)'),
137
117
  }),
118
+ aliases: {
119
+ remote: ['session'],
120
+ },
138
121
  },
139
122
  async handler(ctx) {
140
- const { opts, options, config } = ctx;
123
+ const { opts, options } = ctx;
124
+ const client = new CoderClient({
125
+ apiKey: ctx.auth.apiKey,
126
+ url: opts?.url,
127
+ orgId: ctx.orgId,
128
+ });
141
129
 
142
- // Resolve Hub URL
143
- const hubHttpUrl = await resolveHubUrl(opts?.hubUrl, config);
144
- if (!hubHttpUrl) {
145
- tui.fatal(formatMissingHubUrlMessage(), ErrorCode.NETWORK_ERROR);
146
- return;
147
- }
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
- };
130
+ const hubHttpUrl = await client.getUrl();
131
+ const hubWsUrl = toCoderHubWsUrl(hubHttpUrl);
166
132
 
167
133
  const initProbe = await probeHubInitAccess(hubHttpUrl, {
168
- apiKey: resolvedHubApiKey.apiKey,
134
+ apiKey: ctx.auth.apiKey,
169
135
  });
170
136
  if (!initProbe.ok) {
171
- if (initProbe.code === 'unauthorized') {
172
- const clearedStoredKey =
173
- resolvedHubApiKey.source === 'stored'
174
- ? await clearStoredHubApiKeyOnUnauthorized(401, resolvedHubApiKey, config)
175
- : false;
176
- tui.fatal(
177
- formatHubUnauthorizedMessage(hubHttpUrl, initProbe.message, { clearedStoredKey }),
178
- ErrorCode.NETWORK_ERROR
179
- );
180
- return;
181
- }
182
-
183
137
  tui.fatal(
184
138
  `Could not bootstrap the Coder Hub at ${hubHttpUrl}: ${initProbe.message}`,
185
139
  ErrorCode.NETWORK_ERROR
@@ -188,17 +142,27 @@ export const startSubcommand = createSubcommand({
188
142
  }
189
143
 
190
144
  // Resolve extension path
191
- const extensionPath = resolveExtensionPath(opts?.extension);
145
+ const extensionPath = await resolveExtensionPath(opts?.extension);
192
146
  if (!extensionPath) {
193
147
  tui.fatal(
194
- 'Could not find the Agentuity Coder extension.\n\nEither:\n - Install it: npm install @agentuity/coder\n - Set AGENTUITY_CODER_EXTENSION environment variable\n - Pass --extension flag',
148
+ 'Could not find the Agentuity Coder extension.\n\nThis CLI install should include it automatically. Try:\n - Reinstall or update @agentuity/cli\n - Install it locally: npm install @agentuity/coder-tui\n - Set AGENTUITY_CODER_EXTENSION environment variable\n - Pass --extension flag',
195
149
  ErrorCode.CONFIG_INVALID
196
150
  );
197
151
  return;
198
152
  }
199
153
 
154
+ const loadRemoteTui = async () => {
155
+ const modulePath = await resolveExtensionRuntimeModulePath(extensionPath);
156
+ if (!modulePath) {
157
+ throw new Error(
158
+ `Coder extension at ${extensionPath} is missing the remote TUI entrypoint`
159
+ );
160
+ }
161
+ return import(modulePath);
162
+ };
163
+
200
164
  // Resolve pi binary
201
- const piBinary = resolvePiBinary(opts?.pi);
165
+ const piBinary = await resolvePiBinary(opts?.pi, extensionPath);
202
166
 
203
167
  // ── Remote mode: resolve session ID ──
204
168
  let remoteSessionId: string | undefined;
@@ -210,31 +174,9 @@ export const startSubcommand = createSubcommand({
210
174
  } else {
211
175
  // No session ID — fetch connectable sessions and show picker
212
176
  try {
213
- type SessionInfo = {
214
- id: string;
215
- label: string;
216
- status: string;
217
- task: string | null;
218
- createdAt: string;
219
- };
220
-
221
177
  const sessions = await tui.spinner({
222
178
  message: 'Fetching connectable sessions…',
223
- callback: async () => {
224
- const resp = await fetch(`${hubHttpUrl}/api/hub/sessions/connectable`, {
225
- headers: hubFetchHeaders(undefined, resolvedHubApiKey.apiKey),
226
- signal: AbortSignal.timeout(10_000),
227
- });
228
- if (isHubUnauthorizedStatus(resp.status)) {
229
- await handleUnauthorizedResponse(resp);
230
- throw new Error('Hub authentication failed');
231
- }
232
- if (!resp.ok) {
233
- throw new Error(`${resp.status} ${await getHubResponseErrorMessage(resp)}`);
234
- }
235
- const data = (await resp.json()) as { sessions: SessionInfo[] };
236
- return data.sessions;
237
- },
179
+ callback: async () => (await client.listConnectableSessions()).sessions,
238
180
  });
239
181
 
240
182
  if (sessions.length === 0) {
@@ -248,22 +190,20 @@ export const startSubcommand = createSubcommand({
248
190
  const prompt = tui.createPrompt();
249
191
  remoteSessionId = await prompt.select<string>({
250
192
  message: 'Select a sandbox session to connect to',
251
- options: sessions.map((s) => {
193
+ options: sessions.map((s: CoderSessionListItem) => {
252
194
  const age = timeSince(new Date(s.createdAt));
253
- const taskPreview = s.task ? s.task.slice(0, 55) : null;
254
- const label = taskPreview
255
- ? `${s.label} ${tui.muted(`(${s.status}, ${age})`)} — ${taskPreview}`
256
- : `${s.label} ${tui.muted(`(${s.status}, ${age})`)}`;
195
+ const label = `${s.label} ${tui.muted(`(${s.status}, ${age})`)}`;
257
196
  return {
258
- value: s.id,
197
+ value: s.sessionId,
259
198
  label,
260
- hint: s.id,
199
+ hint: s.sessionId,
261
200
  };
262
201
  }),
263
202
  });
264
203
  } catch (err) {
204
+ logValidationIssues(ctx, err);
265
205
  const msg = err instanceof Error ? err.message : String(err);
266
- if (msg === 'User cancelled' || msg === 'Hub authentication failed') return;
206
+ if (msg === 'User cancelled') return;
267
207
  tui.fatal(`Failed to fetch connectable sessions: ${msg}`, ErrorCode.NETWORK_ERROR);
268
208
  return;
269
209
  }
@@ -284,10 +224,12 @@ export const startSubcommand = createSubcommand({
284
224
  }
285
225
 
286
226
  try {
287
- const { runRemoteTui } = await import(join(extensionPath, 'src', 'remote-tui.ts'));
227
+ const { runRemoteTui } = await loadRemoteTui();
288
228
  await runRemoteTui({
289
229
  hubWsUrl,
290
230
  sessionId: remoteSessionId,
231
+ apiKey: ctx.auth.apiKey,
232
+ orgId: ctx.orgId,
291
233
  });
292
234
  } catch (err) {
293
235
  const msg = err instanceof Error ? err.message : String(err);
@@ -308,7 +250,10 @@ export const startSubcommand = createSubcommand({
308
250
  }
309
251
 
310
252
  // Build request body
311
- const body: Record<string, unknown> = { task };
253
+ const body: {
254
+ task: string;
255
+ repo?: { url: string };
256
+ } = { task };
312
257
  if (opts?.repo) {
313
258
  body.repo = { url: opts.repo };
314
259
  }
@@ -319,29 +264,10 @@ export const startSubcommand = createSubcommand({
319
264
 
320
265
  let sessionId: string;
321
266
  try {
322
- const resp = await fetch(`${hubHttpUrl}/api/hub/session`, {
323
- method: 'POST',
324
- headers: hubFetchHeaders(
325
- { 'Content-Type': 'application/json' },
326
- resolvedHubApiKey.apiKey
327
- ),
328
- body: JSON.stringify(body),
329
- signal: AbortSignal.timeout(10_000),
330
- });
331
- if (isHubUnauthorizedStatus(resp.status)) {
332
- await handleUnauthorizedResponse(resp);
333
- return;
334
- }
335
- if (!resp.ok) {
336
- tui.fatal(
337
- `Failed to create sandbox session: ${resp.status} ${await getHubResponseErrorMessage(resp)}`,
338
- ErrorCode.NETWORK_ERROR
339
- );
340
- return;
341
- }
342
- const sessionInfo = (await resp.json()) as { sessionId: string };
267
+ const sessionInfo = await client.createSession(body);
343
268
  sessionId = sessionInfo.sessionId;
344
269
  } catch (err) {
270
+ logValidationIssues(ctx, err);
345
271
  const msg = err instanceof Error ? err.message : String(err);
346
272
  tui.fatal(`Failed to create sandbox session: ${msg}`, ErrorCode.NETWORK_ERROR);
347
273
  return;
@@ -361,22 +287,10 @@ export const startSubcommand = createSubcommand({
361
287
  while (Date.now() - pollStart < POLL_TIMEOUT) {
362
288
  await new Promise((r) => setTimeout(r, POLL_INTERVAL));
363
289
  try {
364
- const pollResp = await fetch(`${hubHttpUrl}/api/hub/session/${sessionId}`, {
365
- headers: hubFetchHeaders(undefined, resolvedHubApiKey.apiKey),
366
- signal: AbortSignal.timeout(5_000),
367
- });
368
- if (isHubUnauthorizedStatus(pollResp.status)) {
369
- await handleUnauthorizedResponse(pollResp);
370
- return;
371
- }
372
- if (pollResp.ok) {
373
- const data = (await pollResp.json()) as {
374
- participants?: Array<{ role: string }>;
375
- };
376
- if (data.participants?.some((p) => p.role === 'lead')) {
377
- driverConnected = true;
378
- break;
379
- }
290
+ const data = await client.listParticipants(sessionId);
291
+ if (data.participants?.some((p) => p.role === 'lead')) {
292
+ driverConnected = true;
293
+ break;
380
294
  }
381
295
  } catch {
382
296
  // Network blip — keep polling
@@ -395,10 +309,12 @@ export const startSubcommand = createSubcommand({
395
309
  tui.newline();
396
310
 
397
311
  try {
398
- const { runRemoteTui } = await import(join(extensionPath, 'src', 'remote-tui.ts'));
312
+ const { runRemoteTui } = await loadRemoteTui();
399
313
  await runRemoteTui({
400
314
  hubWsUrl,
401
315
  sessionId,
316
+ apiKey: ctx.auth.apiKey,
317
+ orgId: ctx.orgId,
402
318
  });
403
319
  } catch (err) {
404
320
  const msg = err instanceof Error ? err.message : String(err);
@@ -412,7 +328,7 @@ export const startSubcommand = createSubcommand({
412
328
  ...(process.env as Record<string, string>),
413
329
  AGENTUITY_CODER_HUB_URL: hubWsUrl,
414
330
  };
415
- if (resolvedHubApiKey.apiKey) env.AGENTUITY_CODER_API_KEY = resolvedHubApiKey.apiKey;
331
+ env.AGENTUITY_CODER_API_KEY = ctx.auth.apiKey;
416
332
 
417
333
  if (opts?.agent) {
418
334
  env.AGENTUITY_CODER_AGENT = opts.agent;
@@ -440,7 +356,18 @@ export const startSubcommand = createSubcommand({
440
356
  stderr: 'inherit',
441
357
  });
442
358
 
359
+ // Forward signals to the child process so Ctrl+C exits cleanly
360
+ const onSigInt = () => proc.kill(2);
361
+ const onSigTerm = () => proc.kill(15);
362
+ process.on('SIGINT', onSigInt);
363
+ process.on('SIGTERM', onSigTerm);
364
+
443
365
  const exitCode = await proc.exited;
366
+
367
+ // Clean up only our signal handlers (preserve other modules' listeners)
368
+ process.removeListener('SIGINT', onSigInt);
369
+ process.removeListener('SIGTERM', onSigTerm);
370
+
444
371
  process.exit(exitCode);
445
372
  } catch (err) {
446
373
  const msg = err instanceof Error ? err.message : String(err);
@@ -1,5 +1,3 @@
1
- import { hubFetchHeaders } from './hub-url';
2
-
3
1
  export type HubInitProbeResult =
4
2
  | { ok: true }
5
3
  | {
@@ -27,10 +25,16 @@ export async function probeHubInitAccess(
27
25
  }
28
26
  ): Promise<HubInitProbeResult> {
29
27
  const fetchImpl = options?.fetchImpl ?? fetch;
28
+ const headers: Record<string, string> = {
29
+ accept: 'application/json',
30
+ };
31
+ if (options?.apiKey) {
32
+ headers['x-agentuity-auth-api-key'] = options.apiKey;
33
+ }
30
34
 
31
35
  try {
32
36
  const response = await fetchImpl(`${hubHttpUrl}/api/hub/init`, {
33
- headers: hubFetchHeaders({ accept: 'application/json' }, options?.apiKey),
37
+ headers,
34
38
  signal: AbortSignal.timeout(5_000),
35
39
  });
36
40
 
@@ -0,0 +1,128 @@
1
+ import { z } from 'zod';
2
+ import { CoderClient, normalizeVisibility } from '@agentuity/core/coder';
3
+ import { ValidationOutputError } from '@agentuity/core';
4
+ import { createSubcommand } from '../../types';
5
+ import * as tui from '../../tui';
6
+ import { getCommand } from '../../command-prefix';
7
+ import { ErrorCode } from '../../errors';
8
+
9
+ export const updateSubcommand = createSubcommand({
10
+ name: 'update',
11
+ description: 'Update a Coder session',
12
+ tags: ['mutating', 'requires-auth'],
13
+ requires: { auth: true, org: true },
14
+ aliases: ['set', 'edit'],
15
+ examples: [
16
+ {
17
+ command: getCommand('coder update codesess_abc123 --label "New Label"'),
18
+ description: 'Update the session label',
19
+ },
20
+ {
21
+ command: getCommand('coder update codesess_abc123 --visibility org'),
22
+ description: 'Make a session visible to the org',
23
+ },
24
+ {
25
+ command: getCommand('coder update codesess_abc123 --tags "urgent,frontend"'),
26
+ description: 'Update tags on a session',
27
+ },
28
+ {
29
+ command: getCommand(
30
+ 'coder update codesess_abc123 --workflow-mode loop --loop-goal "Build it" --loop-max-iterations 20'
31
+ ),
32
+ description: 'Switch a session to loop mode',
33
+ },
34
+ ],
35
+ schema: {
36
+ args: z.object({
37
+ sessionId: z.string().describe('Session ID to update'),
38
+ }),
39
+ options: z.object({
40
+ url: z.string().optional().describe('Coder API URL override'),
41
+ label: z.string().optional().describe('Updated session label'),
42
+ agent: z.string().optional().describe('Updated default agent role'),
43
+ visibility: z
44
+ .string()
45
+ .optional()
46
+ .describe('Updated visibility: private, org, or collaborate'),
47
+ workflowMode: z.string().optional().describe('Updated workflow mode: standard or loop'),
48
+ loopGoal: z.string().optional().describe('Goal for loop mode'),
49
+ loopMaxIterations: z.number().optional().describe('Maximum loop iterations'),
50
+ loopAutoContinue: z.boolean().optional().describe('Auto-continue loop'),
51
+ loopAllowDetached: z.boolean().optional().describe('Allow detached loop execution'),
52
+ tags: z.string().optional().describe('Comma-separated tags (replaces existing)'),
53
+ }),
54
+ },
55
+ async handler(ctx) {
56
+ const { args, opts, options } = ctx;
57
+ const client = new CoderClient({
58
+ apiKey: ctx.auth.apiKey,
59
+ url: opts?.url,
60
+ orgId: ctx.orgId,
61
+ });
62
+
63
+ const body: Record<string, unknown> = {};
64
+
65
+ if (opts?.label) body.label = opts.label;
66
+ if (opts?.agent) body.agent = opts.agent;
67
+ if (opts?.visibility) body.visibility = normalizeVisibility(opts.visibility);
68
+ if (opts?.workflowMode) body.workflowMode = opts.workflowMode;
69
+ if (opts?.tags) {
70
+ body.tags = opts.tags
71
+ .split(',')
72
+ .map((t) => t.trim())
73
+ .filter(Boolean);
74
+ }
75
+
76
+ if (
77
+ opts?.loopGoal ||
78
+ opts?.loopMaxIterations ||
79
+ opts?.loopAutoContinue !== undefined ||
80
+ opts?.loopAllowDetached !== undefined
81
+ ) {
82
+ const loop: Record<string, unknown> = {};
83
+ if (opts.loopGoal) loop.goal = opts.loopGoal;
84
+ if (opts.loopMaxIterations) loop.maxIterations = opts.loopMaxIterations;
85
+ if (opts.loopAutoContinue !== undefined) loop.autoContinue = opts.loopAutoContinue;
86
+ if (opts.loopAllowDetached !== undefined) loop.allowDetached = opts.loopAllowDetached;
87
+ body.loop = loop;
88
+ if (!body.workflowMode) body.workflowMode = 'loop';
89
+ }
90
+
91
+ if (Object.keys(body).length === 0) {
92
+ tui.fatal(
93
+ 'No update fields provided. Use --label, --visibility, --tags, --agent, --workflow-mode, or loop options.',
94
+ ErrorCode.VALIDATION_FAILED
95
+ );
96
+ }
97
+
98
+ try {
99
+ const updated = await client.updateSession(args.sessionId, body as any);
100
+
101
+ if (options.json) {
102
+ return updated;
103
+ }
104
+
105
+ tui.success(`Session ${args.sessionId} updated.`);
106
+
107
+ const fields: string[] = [];
108
+ if (opts?.label) fields.push(`Label: ${opts.label}`);
109
+ if (opts?.visibility) fields.push(`Visibility: ${body.visibility}`);
110
+ if (opts?.tags) fields.push(`Tags: ${(body.tags as string[]).join(', ')}`);
111
+ if (opts?.agent) fields.push(`Agent: ${opts.agent}`);
112
+ if (opts?.workflowMode || body.loop) fields.push(`Workflow: ${body.workflowMode}`);
113
+
114
+ for (const f of fields) {
115
+ tui.output(` ${f}`);
116
+ }
117
+
118
+ return updated;
119
+ } catch (err) {
120
+ if (err instanceof ValidationOutputError) {
121
+ ctx.logger.trace('Validation response URL: %s', err.url ?? 'unknown');
122
+ ctx.logger.trace('Validation issues: %s', JSON.stringify(err.issues, null, 2));
123
+ }
124
+ const msg = err instanceof Error ? err.message : String(err);
125
+ tui.fatal(`Failed to update session ${args.sessionId}: ${msg}`, ErrorCode.NETWORK_ERROR);
126
+ }
127
+ },
128
+ });
@@ -0,0 +1,101 @@
1
+ import { z } from 'zod';
2
+ import { CoderClient, type CoderUser } from '@agentuity/core/coder';
3
+ import { ValidationOutputError } from '@agentuity/core';
4
+ import { createSubcommand } from '../../types';
5
+ import * as tui from '../../tui';
6
+ import { getCommand } from '../../command-prefix';
7
+ import { ErrorCode } from '../../errors';
8
+
9
+ function formatRelativeTime(isoDate: string): string {
10
+ const parsed = new Date(isoDate).getTime();
11
+ if (Number.isNaN(parsed)) return 'unknown';
12
+ const diffMs = Math.max(0, Date.now() - parsed);
13
+ const seconds = Math.floor(diffMs / 1000);
14
+ if (seconds < 60) return `${seconds}s ago`;
15
+ const minutes = Math.floor(seconds / 60);
16
+ if (minutes < 60) return `${minutes}m ago`;
17
+ const hours = Math.floor(minutes / 60);
18
+ if (hours < 24) return `${hours}h ago`;
19
+ const days = Math.floor(hours / 24);
20
+ return `${days}d ago`;
21
+ }
22
+
23
+ export const usersSubcommand = createSubcommand({
24
+ name: 'users',
25
+ aliases: ['user'],
26
+ description: 'List known Coder Hub users',
27
+ tags: ['read-only', 'fast', 'requires-auth'],
28
+ idempotent: true,
29
+ requires: { auth: true, org: true },
30
+ examples: [
31
+ {
32
+ command: getCommand('coder users'),
33
+ description: 'List known users',
34
+ },
35
+ {
36
+ command: getCommand('coder users --search jenny'),
37
+ description: 'Filter users by display name',
38
+ },
39
+ {
40
+ command: getCommand('coder users --json'),
41
+ description: 'Return users as JSON',
42
+ },
43
+ ],
44
+ schema: {
45
+ options: z.object({
46
+ search: z.string().optional().describe('Filter users by display name'),
47
+ url: z.string().optional().describe('Coder API URL override'),
48
+ }),
49
+ },
50
+ async handler(ctx) {
51
+ const { options, opts } = ctx;
52
+ const client = new CoderClient({
53
+ apiKey: ctx.auth.apiKey,
54
+ url: opts?.url,
55
+ orgId: ctx.orgId,
56
+ });
57
+
58
+ let users: CoderUser[] = [];
59
+ try {
60
+ const response = await client.listUsers({ search: opts?.search });
61
+ users = response.users;
62
+ } catch (err) {
63
+ if (err instanceof ValidationOutputError) {
64
+ ctx.logger.trace('Validation response URL: %s', err.url ?? 'unknown');
65
+ ctx.logger.trace('Validation issues: %s', JSON.stringify(err.issues, null, 2));
66
+ }
67
+ const msg = err instanceof Error ? err.message : String(err);
68
+ tui.fatal(`Failed to list Coder users: ${msg}`, ErrorCode.NETWORK_ERROR);
69
+ }
70
+
71
+ if (options.json) {
72
+ return users;
73
+ }
74
+
75
+ if (users.length === 0) {
76
+ tui.info('No Coder users found.');
77
+ return [];
78
+ }
79
+
80
+ tui.table(
81
+ users.map((u) => ({
82
+ User: u.displayName,
83
+ Email: u.email,
84
+ Provider: u.provider,
85
+ 'Last Login': formatRelativeTime(u.lastLoginAt),
86
+ 'Last Seen': formatRelativeTime(u.lastSeenAt),
87
+ Joined: formatRelativeTime(u.createdAt),
88
+ })),
89
+ [
90
+ { name: 'User', alignment: 'left' },
91
+ { name: 'Email', alignment: 'left' },
92
+ { name: 'Provider', alignment: 'left' },
93
+ { name: 'Last Login', alignment: 'right' },
94
+ { name: 'Last Seen', alignment: 'right' },
95
+ { name: 'Joined', alignment: 'right' },
96
+ ]
97
+ );
98
+
99
+ return users;
100
+ },
101
+ });