@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,9 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { createCommand } from '../../../types';
3
3
  import * as tui from '../../../tui';
4
- import { createStorageAdapter } from './util';
4
+ import { createStorageAdapter, resolveMeId } from './util';
5
5
  import { getCommand } from '../../../command-prefix';
6
- import type { TaskPriority, TaskStatus, TaskType, Task } from '@agentuity/core';
6
+ import type { TaskPriority, TaskStatus, TaskType, Task, TaskIncludeField } from '@agentuity/core';
7
7
 
8
8
  const TaskListResponseSchema = z.object({
9
9
  success: z.boolean().describe('Whether the operation succeeded'),
@@ -14,6 +14,12 @@ const TaskListResponseSchema = z.object({
14
14
  type: z.string(),
15
15
  status: z.string(),
16
16
  priority: z.string(),
17
+ description: z.string().optional(),
18
+ metadata: z.record(z.string(), z.unknown()).optional(),
19
+ tags: z.array(z.object({ id: z.string(), name: z.string() })).optional(),
20
+ subtask_count: z.number().optional(),
21
+ created_id: z.string().optional(),
22
+ deleted: z.boolean().optional(),
17
23
  creator: z
18
24
  .object({
19
25
  id: z.string(),
@@ -73,6 +79,38 @@ function truncate(s: string, max: number): string {
73
79
  return `${s.slice(0, max - 1)}…`;
74
80
  }
75
81
 
82
+ const VALID_INCLUDE_FIELDS = new Set<TaskIncludeField>([
83
+ 'description',
84
+ 'metadata',
85
+ 'tags',
86
+ 'subtask_count',
87
+ 'created_id',
88
+ 'deleted',
89
+ ]);
90
+
91
+ function parseIncludeParam(include: string | undefined): TaskIncludeField[] | undefined {
92
+ if (!include) return undefined;
93
+ const fields: TaskIncludeField[] = [];
94
+ for (const f of include.split(',')) {
95
+ const trimmed = f.trim() as TaskIncludeField;
96
+ if (VALID_INCLUDE_FIELDS.has(trimmed)) {
97
+ fields.push(trimmed);
98
+ } else {
99
+ tui.fatal(
100
+ `Invalid include field: "${trimmed}". Valid fields are: ${[...VALID_INCLUDE_FIELDS].join(', ')}`
101
+ );
102
+ }
103
+ }
104
+ return fields.length > 0 ? fields : undefined;
105
+ }
106
+
107
+ function hasIncludeField(
108
+ include: TaskIncludeField[] | undefined,
109
+ field: TaskIncludeField
110
+ ): boolean {
111
+ return include?.includes(field) ?? false;
112
+ }
113
+
76
114
  export const listSubcommand = createCommand({
77
115
  name: 'list',
78
116
  aliases: ['ls'],
@@ -103,6 +141,14 @@ export const listSubcommand = createCommand({
103
141
  command: getCommand('cloud task list --assigned-id agent_001 --limit 10'),
104
142
  description: 'List first 10 tasks assigned to an agent',
105
143
  },
144
+ {
145
+ command: getCommand('cloud task list --created-id me --include description,metadata,tags'),
146
+ description: 'List tasks created by me with full details',
147
+ },
148
+ {
149
+ command: getCommand('cloud task list --project-id proj_abc123'),
150
+ description: 'List tasks for a specific project',
151
+ },
106
152
  ],
107
153
  schema: {
108
154
  options: z.object({
@@ -118,8 +164,24 @@ export const listSubcommand = createCommand({
118
164
  .enum(['high', 'medium', 'low', 'none'])
119
165
  .optional()
120
166
  .describe('filter by priority'),
121
- assignedId: z.string().optional().describe('filter by assigned agent or user ID'),
167
+ assignedId: z
168
+ .string()
169
+ .optional()
170
+ .describe('filter by assigned agent or user ID (use "me" for current user)'),
171
+ createdId: z
172
+ .string()
173
+ .optional()
174
+ .describe('filter by creator ID (use "me" for current user)'),
122
175
  parentId: z.string().optional().describe('filter by parent task ID'),
176
+ projectId: z.string().optional().describe('filter by project ID'),
177
+ tagId: z.string().optional().describe('filter by tag ID'),
178
+ deleted: z.boolean().optional().describe('include soft-deleted tasks'),
179
+ include: z
180
+ .string()
181
+ .optional()
182
+ .describe(
183
+ 'comma-separated fields to include: description,metadata,tags,subtask_count,created_id,deleted'
184
+ ),
123
185
  sort: z
124
186
  .enum(['created_at', 'updated_at', 'priority'])
125
187
  .optional()
@@ -127,6 +189,7 @@ export const listSubcommand = createCommand({
127
189
  order: z.enum(['asc', 'desc']).optional().describe('sort order (default: desc)'),
128
190
  limit: z.coerce.number().optional().describe('max results to return (default: 50)'),
129
191
  offset: z.coerce.number().optional().describe('offset for pagination'),
192
+ orgId: z.string().optional().describe('organization ID (uses default if not specified)'),
130
193
  }),
131
194
  response: TaskListResponseSchema,
132
195
  },
@@ -136,12 +199,22 @@ export const listSubcommand = createCommand({
136
199
  const started = Date.now();
137
200
  const storage = await createStorageAdapter(ctx);
138
201
 
202
+ const createdId = resolveMeId(opts.createdId, ctx);
203
+ const assignedId = resolveMeId(opts.assignedId, ctx);
204
+
205
+ const includeFields = parseIncludeParam(opts.include);
206
+
139
207
  const result = await storage.list({
140
208
  status: opts.status as TaskStatus | undefined,
141
209
  type: opts.type as TaskType | undefined,
142
210
  priority: opts.priority as TaskPriority | undefined,
143
- assigned_id: opts.assignedId,
211
+ assigned_id: assignedId,
212
+ created_id: createdId,
144
213
  parent_id: opts.parentId,
214
+ project_id: opts.projectId,
215
+ tag_id: opts.tagId,
216
+ deleted: opts.deleted,
217
+ include: includeFields,
145
218
  sort: opts.sort,
146
219
  order: opts.order,
147
220
  limit: opts.limit,
@@ -154,6 +227,10 @@ export const listSubcommand = createCommand({
154
227
  if (result.tasks.length === 0) {
155
228
  tui.info('No tasks found');
156
229
  } else {
230
+ const showDescription = hasIncludeField(includeFields, 'description');
231
+ const showTags = hasIncludeField(includeFields, 'tags');
232
+ const showMetadata = hasIncludeField(includeFields, 'metadata');
233
+
157
234
  const tableData = result.tasks.map((task: Task) => ({
158
235
  ID: tui.muted(truncate(task.id, 28)),
159
236
  Title: truncate(task.title, 40),
@@ -176,6 +253,30 @@ export const listSubcommand = createCommand({
176
253
  { name: 'Updated', alignment: 'left' },
177
254
  ]);
178
255
 
256
+ // Show extra details for each task if included
257
+ if (showDescription || showTags || showMetadata) {
258
+ for (const task of result.tasks) {
259
+ const extras: string[] = [];
260
+ if (showDescription && task.description) {
261
+ extras.push(`${tui.muted('Desc:')} ${truncate(task.description, 80)}`);
262
+ }
263
+ if (showTags && task.tags && task.tags.length > 0) {
264
+ const tagList = task.tags.map((t) => t.name).join(', ');
265
+ extras.push(`${tui.muted('Tags:')} ${tagList}`);
266
+ }
267
+ if (showMetadata && task.metadata) {
268
+ const metaStr =
269
+ typeof task.metadata === 'object'
270
+ ? JSON.stringify(task.metadata)
271
+ : String(task.metadata);
272
+ extras.push(`${tui.muted('Meta:')} ${truncate(metaStr, 80)}`);
273
+ }
274
+ if (extras.length > 0) {
275
+ tui.output(` ${tui.muted(truncate(task.id, 28))} → ${extras.join(' | ')}`);
276
+ }
277
+ }
278
+ }
279
+
179
280
  tui.info(
180
281
  `Showing ${result.tasks.length} of ${result.total} ${tui.plural(result.total, 'task', 'tasks')} (${durationMs.toFixed(1)}ms)`
181
282
  );
@@ -190,6 +291,12 @@ export const listSubcommand = createCommand({
190
291
  type: task.type,
191
292
  status: task.status,
192
293
  priority: task.priority,
294
+ description: task.description,
295
+ metadata: task.metadata,
296
+ tags: task.tags,
297
+ subtask_count: task.subtask_count,
298
+ created_id: task.created_id,
299
+ deleted: task.deleted,
193
300
  creator: task.creator,
194
301
  assignee: task.assignee,
195
302
  project: task.project,
@@ -11,11 +11,14 @@ export interface TaskContext {
11
11
  auth: AuthData;
12
12
  config: Config | null;
13
13
  options: GlobalOptions;
14
+ project?: {
15
+ projectId: string;
16
+ orgId: string;
17
+ };
14
18
  }
15
19
 
16
20
  export async function createStorageAdapter(ctx: TaskContext) {
17
- const orgId =
18
- ctx.options.orgId ?? (process.env.AGENTUITY_CLOUD_ORG_ID || ctx.config?.preferences?.orgId);
21
+ const orgId = resolveOrgId(ctx);
19
22
  if (!orgId) {
20
23
  tui.fatal('Organization ID is required. Use --org-id flag or set AGENTUITY_CLOUD_ORG_ID.');
21
24
  }
@@ -37,8 +40,7 @@ export async function createStorageAdapter(ctx: TaskContext) {
37
40
  }
38
41
 
39
42
  export async function createStorageAdapterOptionalOrg(ctx: TaskContext) {
40
- const orgId =
41
- ctx.options.orgId ?? (process.env.AGENTUITY_CLOUD_ORG_ID || ctx.config?.preferences?.orgId);
43
+ const orgId = resolveOrgId(ctx);
42
44
 
43
45
  const headers: Record<string, string> = {
44
46
  Authorization: `Bearer ${ctx.auth.apiKey}`,
@@ -54,17 +56,30 @@ export async function createStorageAdapterOptionalOrg(ctx: TaskContext) {
54
56
  return new TaskStorageService(baseUrl, adapter);
55
57
  }
56
58
 
59
+ function resolveOrgId(ctx: TaskContext): string | undefined {
60
+ return (
61
+ ctx.options.orgId ??
62
+ process.env.AGENTUITY_CLOUD_ORG_ID ??
63
+ ctx.project?.orgId ??
64
+ ctx.config?.preferences?.orgId
65
+ );
66
+ }
67
+
57
68
  export async function cacheTaskId(
58
69
  ctx: {
59
70
  config: Config | null;
60
71
  options: GlobalOptions;
72
+ project?: { orgId: string };
61
73
  },
62
74
  taskId: string
63
75
  ) {
64
76
  const profileName = ctx.config?.name ?? defaultProfileName;
65
77
  const region = await getDefaultRegion(profileName, ctx.config);
66
78
  const orgId =
67
- ctx.options.orgId ?? (process.env.AGENTUITY_CLOUD_ORG_ID || ctx.config?.preferences?.orgId);
79
+ ctx.options.orgId ??
80
+ process.env.AGENTUITY_CLOUD_ORG_ID ??
81
+ ctx.project?.orgId ??
82
+ ctx.config?.preferences?.orgId;
68
83
  await setResourceInfo('task', profileName, taskId, region, orgId);
69
84
  }
70
85
 
@@ -76,3 +91,42 @@ export function parseMetadataFlag(raw: string | undefined): Record<string, unkno
76
91
  tui.fatal('Invalid JSON for --metadata flag');
77
92
  }
78
93
  }
94
+
95
+ const DURATION_UNITS: Record<string, number> = {
96
+ s: 1000,
97
+ m: 60 * 1000,
98
+ h: 60 * 60 * 1000,
99
+ d: 24 * 60 * 60 * 1000,
100
+ w: 7 * 24 * 60 * 60 * 1000,
101
+ };
102
+
103
+ export function parseDuration(duration: string): number {
104
+ const match = duration.match(/^(\d+)([smhdw])$/);
105
+ if (!match) {
106
+ tui.fatal(
107
+ `Invalid duration format: "${duration}". Use a number followed by s (seconds), m (minutes), h (hours), d (days), or w (weeks). Examples: 30s, 30m, 24h, 7d, 2w`
108
+ );
109
+ throw new Error('unreachable');
110
+ }
111
+ const value = parseInt(match[1]!, 10);
112
+ const unit = match[2]!;
113
+ const ms = DURATION_UNITS[unit];
114
+ if (!ms) {
115
+ tui.fatal(`Unknown duration unit: "${unit}"`);
116
+ throw new Error('unreachable');
117
+ }
118
+ return value * ms;
119
+ }
120
+
121
+ export function truncate(s: string, max: number): string {
122
+ if (s.length <= max) return s;
123
+ return `${s.slice(0, max - 1)}…`;
124
+ }
125
+
126
+ export function resolveMeId(id: string | undefined, ctx: TaskContext): string | undefined {
127
+ if (!id) return undefined;
128
+ if (id === 'me') {
129
+ return ctx.auth.userId;
130
+ }
131
+ return id;
132
+ }
@@ -0,0 +1,20 @@
1
+ import { createCommand } from '../../../types';
2
+ import { getCommand } from '../../../command-prefix';
3
+ import { setSubcommand } from './set';
4
+
5
+ export const configSubcommand = createCommand({
6
+ name: 'config',
7
+ description: 'Manage stored Coder Hub configuration',
8
+ tags: ['fast'],
9
+ examples: [
10
+ {
11
+ command: getCommand('coder config set url https://hub.example.com'),
12
+ description: 'Set the default Coder Hub URL for the active profile',
13
+ },
14
+ {
15
+ command: getCommand('coder config set apikey agc_...'),
16
+ description: 'Set the default Coder Hub API key for the active profile',
17
+ },
18
+ ],
19
+ subcommands: [setSubcommand],
20
+ });
@@ -0,0 +1,112 @@
1
+ import { z } from 'zod';
2
+ import { saveCoderApiKey, saveCoderHubUrl } from '../../../coder-config';
3
+ import { normalizeCoderHubHttpUrl } from '../../../coder-hub-url';
4
+ import { getCommand } from '../../../command-prefix';
5
+ import * as tui from '../../../tui';
6
+ import { createCommand, createSubcommand } from '../../../types';
7
+
8
+ const setUrlSubcommand = createSubcommand({
9
+ name: 'url',
10
+ description: 'Set the default Coder Hub URL for the active profile',
11
+ tags: ['mutating', 'fast'],
12
+ examples: [
13
+ {
14
+ command: getCommand('coder config set url https://hub.example.com'),
15
+ description: 'Set the default Coder Hub URL',
16
+ },
17
+ {
18
+ command: getCommand('coder config set url ws://127.0.0.1:3650/api/ws'),
19
+ description: 'Store a local dev Hub URL using a WebSocket input',
20
+ },
21
+ ],
22
+ schema: {
23
+ args: z.object({
24
+ url: z.string().min(1).describe('Hub URL to store for the active profile'),
25
+ }),
26
+ response: z.object({
27
+ profile: z.string().describe('Active CLI profile name'),
28
+ hubUrl: z.string().describe('Normalized stored Hub HTTP URL'),
29
+ }),
30
+ },
31
+ async handler(ctx) {
32
+ const { args, options } = ctx;
33
+ const normalized = normalizeCoderHubHttpUrl(args.url);
34
+
35
+ try {
36
+ new URL(normalized);
37
+ } catch {
38
+ tui.fatal(
39
+ `Invalid Hub URL: ${args.url}\n\nExpected a full URL such as https://hub.example.com or ws://127.0.0.1:3650/api/ws`
40
+ );
41
+ }
42
+
43
+ const result = await saveCoderHubUrl(normalized);
44
+
45
+ if (!options.json) {
46
+ tui.success(
47
+ `Default Coder Hub URL set to ${tui.bold(result.hubUrl)} for profile ${tui.bold(result.profileName)}`
48
+ );
49
+ }
50
+
51
+ return {
52
+ profile: result.profileName,
53
+ hubUrl: result.hubUrl,
54
+ };
55
+ },
56
+ });
57
+
58
+ const setApiKeySubcommand = createSubcommand({
59
+ name: 'apikey',
60
+ description: 'Set the default Coder Hub API key for the active profile',
61
+ tags: ['mutating', 'fast'],
62
+ examples: [
63
+ {
64
+ command: getCommand('coder config set apikey agc_...'),
65
+ description: 'Set the default Coder Hub API key',
66
+ },
67
+ ],
68
+ schema: {
69
+ args: z.object({
70
+ apikey: z.string().min(1).describe('Hub API key to store for the active profile'),
71
+ }),
72
+ response: z.object({
73
+ profile: z.string().describe('Active CLI profile name'),
74
+ stored: z.boolean().describe('Whether the API key was stored successfully'),
75
+ }),
76
+ },
77
+ async handler(ctx) {
78
+ const { args, options } = ctx;
79
+ const trimmed = args.apikey.trim();
80
+ if (!trimmed) {
81
+ tui.fatal('Hub API key cannot be empty');
82
+ }
83
+
84
+ const result = await saveCoderApiKey(trimmed);
85
+
86
+ if (!options.json) {
87
+ tui.success(`Coder Hub API key stored for profile ${tui.bold(result.profileName)}`);
88
+ }
89
+
90
+ return {
91
+ profile: result.profileName,
92
+ stored: true,
93
+ };
94
+ },
95
+ });
96
+
97
+ export const setSubcommand = createCommand({
98
+ name: 'set',
99
+ description: 'Set stored Coder Hub configuration values',
100
+ tags: ['mutating', 'fast'],
101
+ examples: [
102
+ {
103
+ command: getCommand('coder config set url https://hub.example.com'),
104
+ description: 'Store the default Hub URL',
105
+ },
106
+ {
107
+ command: getCommand('coder config set apikey agc_...'),
108
+ description: 'Store the default Hub API key',
109
+ },
110
+ ],
111
+ subcommands: [setUrlSubcommand, setApiKeySubcommand],
112
+ });
@@ -4,11 +4,27 @@
4
4
  * Resolution priority:
5
5
  * 1. --hub-url flag (explicit per-command override)
6
6
  * 2. AGENTUITY_CODER_HUB_URL env var
7
- * 3. AGENTUITY_DEVMODE_URL env var (dev tunnel URL)
7
+ * 3. Stored per-profile Hub URL
8
+ * 4. AGENTUITY_DEVMODE_URL env var (dev tunnel URL)
8
9
  */
9
10
 
11
+ import {
12
+ clearStoredCoderApiKey,
13
+ getStoredCoderApiKey,
14
+ getStoredCoderHubUrl,
15
+ } from '../../coder-config';
16
+ import { normalizeCoderHubHttpUrl, toCoderHubWsUrl } from '../../coder-hub-url';
17
+ import { getCommand } from '../../command-prefix';
18
+ import type { Config } from '../../types';
10
19
  import { getVersion } from '../../version';
11
20
 
21
+ export type HubApiKeySource = 'env' | 'stored' | 'none';
22
+
23
+ export interface ResolvedHubApiKey {
24
+ apiKey: string | null;
25
+ source: HubApiKeySource;
26
+ }
27
+
12
28
  /**
13
29
  * Resolve the Hub HTTP base URL for REST API calls.
14
30
  * Converts ws:// URLs to http:// automatically.
@@ -16,17 +32,24 @@ import { getVersion } from '../../version';
16
32
  * @param flagUrl Optional --hub-url flag value
17
33
  * @returns HTTP base URL (e.g. "http://localhost:3500") or null if Hub is unreachable
18
34
  */
19
- export async function resolveHubUrl(flagUrl?: string): Promise<string | null> {
35
+ export async function resolveHubUrl(
36
+ flagUrl?: string,
37
+ config?: Config | null
38
+ ): Promise<string | null> {
20
39
  // 1. Explicit flag
21
- if (flagUrl) return normalizeToHttp(flagUrl);
40
+ if (flagUrl) return normalizeCoderHubHttpUrl(flagUrl);
22
41
 
23
42
  // 2. Env var (explicit)
24
43
  const envUrl = process.env.AGENTUITY_CODER_HUB_URL;
25
- if (envUrl) return normalizeToHttp(envUrl);
44
+ if (envUrl) return normalizeCoderHubHttpUrl(envUrl);
45
+
46
+ // 3. Stored profile config
47
+ const storedUrl = await getStoredCoderHubUrl(config);
48
+ if (storedUrl) return storedUrl;
26
49
 
27
- // 3. Dev mode URL (tunnel)
50
+ // 4. Dev mode URL (tunnel)
28
51
  const devUrl = process.env.AGENTUITY_DEVMODE_URL;
29
- if (devUrl) return normalizeToHttp(devUrl);
52
+ if (devUrl) return normalizeCoderHubHttpUrl(devUrl);
30
53
 
31
54
  return null;
32
55
  }
@@ -38,74 +61,145 @@ export async function resolveHubUrl(flagUrl?: string): Promise<string | null> {
38
61
  * @param flagUrl Optional --hub-url flag value
39
62
  * @returns WebSocket URL (e.g. "ws://127.0.0.1:3500/api/ws") or null
40
63
  */
41
- export async function resolveHubWsUrl(flagUrl?: string): Promise<string | null> {
42
- const httpUrl = await resolveHubUrl(flagUrl);
64
+ export async function resolveHubWsUrl(
65
+ flagUrl?: string,
66
+ config?: Config | null
67
+ ): Promise<string | null> {
68
+ const httpUrl = await resolveHubUrl(flagUrl, config);
43
69
  if (!httpUrl) return null;
44
70
  return toHubWsUrl(httpUrl);
45
71
  }
46
72
 
47
73
  export function toHubWsUrl(hubHttpUrl: string): string {
48
- return normalizeToWs(hubHttpUrl);
74
+ return toCoderHubWsUrl(hubHttpUrl);
49
75
  }
50
76
 
51
77
  /**
52
- * Convert any URL form to an HTTP base URL (strip paths, convert ws->http).
78
+ * Resolve the API key for Hub authentication.
53
79
  */
54
- function normalizeToHttp(url: string): string {
55
- let normalized = url.trim().replace(/\/+$/, '');
80
+ function resolveEnvApiKey(): string | null {
81
+ return process.env.AGENTUITY_CODER_API_KEY || null;
82
+ }
56
83
 
57
- // ws:// -> http://
58
- if (normalized.startsWith('ws://')) normalized = 'http://' + normalized.slice(5);
59
- else if (normalized.startsWith('wss://')) normalized = 'https://' + normalized.slice(6);
84
+ export async function resolveHubApiKey(config?: Config | null): Promise<ResolvedHubApiKey> {
85
+ const envApiKey = resolveEnvApiKey();
86
+ if (envApiKey) {
87
+ return {
88
+ apiKey: envApiKey,
89
+ source: 'env',
90
+ };
91
+ }
60
92
 
61
- // Strip known Hub transport/helper paths to get the HTTP base URL.
62
- // Accept `/ws` as a convenience alias because users often copy the raw route name.
63
- normalized = normalized.replace(/\/api\/ws\b.*$/, '');
64
- normalized = normalized.replace(/\/ws\b.*$/, '');
65
- normalized = normalized.replace(/\/api\/hub\b.*$/, '');
93
+ const storedApiKey = await getStoredCoderApiKey(config);
94
+ if (storedApiKey) {
95
+ return {
96
+ apiKey: storedApiKey,
97
+ source: 'stored',
98
+ };
99
+ }
66
100
 
67
- return normalized.replace(/\/+$/, '');
101
+ return {
102
+ apiKey: null,
103
+ source: 'none',
104
+ };
68
105
  }
69
106
 
70
107
  /**
71
- * Convert an HTTP base URL to a WebSocket URL with /api/ws path.
108
+ * Build headers object with API key if available.
72
109
  */
73
- function normalizeToWs(httpUrl: string): string {
74
- let wsUrl = httpUrl;
75
- if (wsUrl.startsWith('http://')) wsUrl = 'ws://' + wsUrl.slice(7);
76
- else if (wsUrl.startsWith('https://')) wsUrl = 'wss://' + wsUrl.slice(8);
110
+ export function hubFetchHeaders(
111
+ extra?: Record<string, string>,
112
+ apiKey?: string | null
113
+ ): Record<string, string> {
114
+ const headers: Record<string, string> = { ...extra };
115
+ headers['User-Agent'] = `Agentuity Coder/${getVersion()}`;
116
+ const resolvedApiKey = apiKey === undefined ? resolveEnvApiKey() : apiKey;
117
+ if (resolvedApiKey) headers['x-agentuity-auth-api-key'] = resolvedApiKey;
118
+ return headers;
119
+ }
77
120
 
78
- try {
79
- const parsed = new URL(wsUrl);
80
- if (parsed.pathname !== '/api/ws') {
81
- parsed.pathname = '/api/ws';
82
- wsUrl = parsed.toString().replace(/\/$/, '');
83
- }
84
- } catch {
85
- if (!wsUrl.endsWith('/api/ws')) {
86
- wsUrl = wsUrl.replace(/\/?$/, '/api/ws');
87
- }
121
+ export function isHubUnauthorizedStatus(status: number): boolean {
122
+ return status === 401 || status === 403;
123
+ }
124
+
125
+ export async function clearStoredHubApiKeyOnUnauthorized(
126
+ status: number,
127
+ resolvedApiKey: ResolvedHubApiKey,
128
+ config?: Config | null,
129
+ clearStoredApiKey: (config?: Config | null) => Promise<unknown> = clearStoredCoderApiKey
130
+ ): Promise<boolean> {
131
+ if (!isHubUnauthorizedStatus(status) || resolvedApiKey.source !== 'stored') {
132
+ return false;
88
133
  }
89
134
 
90
- return wsUrl;
135
+ await clearStoredApiKey(config);
136
+ return true;
91
137
  }
92
138
 
93
- /**
94
- * Resolve the API key for Hub authentication.
95
- * TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
96
- */
97
- export function resolveApiKey(): string | null {
98
- return process.env.AGENTUITY_CODER_API_KEY || null;
139
+ export function getHubUrlSetupGuidance(): string {
140
+ return (
141
+ `Set a default Hub URL with:\n` +
142
+ ` ${getCommand('coder config set url <url>')}\n\n` +
143
+ `Or pass --hub-url for a one-off override, or use AGENTUITY_CODER_HUB_URL.`
144
+ );
99
145
  }
100
146
 
101
- /**
102
- * Build headers object with API key if available.
103
- * TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
104
- */
105
- export function hubFetchHeaders(extra?: Record<string, string>): Record<string, string> {
106
- const headers: Record<string, string> = { ...extra };
107
- headers['User-Agent'] = `Agentuity Coder/${getVersion()}`;
108
- const apiKey = resolveApiKey();
109
- if (apiKey) headers['x-agentuity-auth-api-key'] = apiKey;
110
- return headers;
147
+ export function getHubApiKeySetupGuidance(): string {
148
+ return (
149
+ `Set a Hub API key with:\n` +
150
+ ` ${getCommand('coder config set apikey <apikey>')}\n\n` +
151
+ `Or use AGENTUITY_CODER_API_KEY as an override.`
152
+ );
153
+ }
154
+
155
+ export function formatMissingHubUrlMessage(): string {
156
+ return `Could not find a configured Coder Hub URL.\n\n${getHubUrlSetupGuidance()}`;
157
+ }
158
+
159
+ export function formatHubUnauthorizedMessage(
160
+ hubUrl: string,
161
+ serverMessage: string,
162
+ options?: {
163
+ clearedStoredKey?: boolean;
164
+ }
165
+ ): string {
166
+ const clearedStoredKey = options?.clearedStoredKey ? 'Stored Hub API key cleared.\n\n' : '';
167
+
168
+ return (
169
+ `Coder Hub at ${hubUrl} requires a valid API key.\n\n` +
170
+ `${clearedStoredKey}${getHubApiKeySetupGuidance()}\n\n` +
171
+ `Server said: ${serverMessage}`
172
+ );
173
+ }
174
+
175
+ export async function getHubResponseErrorMessage(response: Response): Promise<string> {
176
+ const fallback = `${response.status} ${response.statusText}`;
177
+
178
+ try {
179
+ const payload = (await response.clone().json()) as {
180
+ error?: unknown;
181
+ message?: unknown;
182
+ details?: unknown;
183
+ };
184
+ if (typeof payload.error === 'string' && payload.error.trim()) {
185
+ return payload.error.trim();
186
+ }
187
+ if (typeof payload.message === 'string' && payload.message.trim()) {
188
+ return payload.message.trim();
189
+ }
190
+ if (typeof payload.details === 'string' && payload.details.trim()) {
191
+ return payload.details.trim();
192
+ }
193
+ } catch {
194
+ // Fall back to the response text below.
195
+ }
196
+
197
+ try {
198
+ const text = (await response.text()).trim();
199
+ if (text) return text;
200
+ } catch {
201
+ // Fall back to the status text below.
202
+ }
203
+
204
+ return fallback;
111
205
  }