@agentuity/cli 2.0.6 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +11 -0
  2. package/dist/cmd/cloud/task/close.d.ts +3 -0
  3. package/dist/cmd/cloud/task/close.d.ts.map +1 -0
  4. package/dist/cmd/cloud/task/close.js +286 -0
  5. package/dist/cmd/cloud/task/close.js.map +1 -0
  6. package/dist/cmd/cloud/task/delete.d.ts +1 -5
  7. package/dist/cmd/cloud/task/delete.d.ts.map +1 -1
  8. package/dist/cmd/cloud/task/delete.js +15 -38
  9. package/dist/cmd/cloud/task/delete.js.map +1 -1
  10. package/dist/cmd/cloud/task/index.d.ts.map +1 -1
  11. package/dist/cmd/cloud/task/index.js +10 -0
  12. package/dist/cmd/cloud/task/index.js.map +1 -1
  13. package/dist/cmd/cloud/task/list.d.ts.map +1 -1
  14. package/dist/cmd/cloud/task/list.js +97 -3
  15. package/dist/cmd/cloud/task/list.js.map +1 -1
  16. package/dist/cmd/cloud/task/util.d.ts +10 -0
  17. package/dist/cmd/cloud/task/util.d.ts.map +1 -1
  18. package/dist/cmd/cloud/task/util.js +47 -3
  19. package/dist/cmd/cloud/task/util.js.map +1 -1
  20. package/dist/cmd/coder/config/index.d.ts +2 -0
  21. package/dist/cmd/coder/config/index.d.ts.map +1 -0
  22. package/dist/cmd/coder/config/index.js +20 -0
  23. package/dist/cmd/coder/config/index.js.map +1 -0
  24. package/dist/cmd/coder/config/set.d.ts +2 -0
  25. package/dist/cmd/coder/config/set.d.ts.map +1 -0
  26. package/dist/cmd/coder/config/set.js +100 -0
  27. package/dist/cmd/coder/config/set.js.map +1 -0
  28. package/dist/cmd/coder/hub-url.d.ts +21 -10
  29. package/dist/cmd/coder/hub-url.d.ts.map +1 -1
  30. package/dist/cmd/coder/hub-url.js +97 -55
  31. package/dist/cmd/coder/hub-url.js.map +1 -1
  32. package/dist/cmd/coder/index.d.ts.map +1 -1
  33. package/dist/cmd/coder/index.js +6 -1
  34. package/dist/cmd/coder/index.js.map +1 -1
  35. package/dist/cmd/coder/inspect.d.ts.map +1 -1
  36. package/dist/cmd/coder/inspect.js +15 -7
  37. package/dist/cmd/coder/inspect.js.map +1 -1
  38. package/dist/cmd/coder/list.d.ts.map +1 -1
  39. package/dist/cmd/coder/list.js +14 -7
  40. package/dist/cmd/coder/list.js.map +1 -1
  41. package/dist/cmd/coder/start.d.ts.map +1 -1
  42. package/dist/cmd/coder/start.js +38 -23
  43. package/dist/cmd/coder/start.js.map +1 -1
  44. package/dist/cmd/coder/tui-init.d.ts +4 -1
  45. package/dist/cmd/coder/tui-init.d.ts.map +1 -1
  46. package/dist/cmd/coder/tui-init.js +3 -2
  47. package/dist/cmd/coder/tui-init.js.map +1 -1
  48. package/dist/cmd/dev/sync.js +5 -5
  49. package/dist/cmd/dev/sync.js.map +1 -1
  50. package/dist/coder-config.d.ts +14 -0
  51. package/dist/coder-config.d.ts.map +1 -0
  52. package/dist/coder-config.js +119 -0
  53. package/dist/coder-config.js.map +1 -0
  54. package/dist/coder-hub-url.d.ts +3 -0
  55. package/dist/coder-hub-url.d.ts.map +1 -0
  56. package/dist/coder-hub-url.js +32 -0
  57. package/dist/coder-hub-url.js.map +1 -0
  58. package/dist/config.d.ts +1 -0
  59. package/dist/config.d.ts.map +1 -1
  60. package/dist/config.js +11 -0
  61. package/dist/config.js.map +1 -1
  62. package/dist/internal-logger.d.ts +4 -0
  63. package/dist/internal-logger.d.ts.map +1 -1
  64. package/dist/internal-logger.js +64 -2
  65. package/dist/internal-logger.js.map +1 -1
  66. package/dist/keychain.d.ts +3 -0
  67. package/dist/keychain.d.ts.map +1 -1
  68. package/dist/keychain.js +47 -28
  69. package/dist/keychain.js.map +1 -1
  70. package/dist/types.d.ts +4 -0
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js +10 -0
  73. package/dist/types.js.map +1 -1
  74. package/package.json +6 -6
  75. package/src/cmd/cloud/task/close.ts +319 -0
  76. package/src/cmd/cloud/task/delete.ts +15 -43
  77. package/src/cmd/cloud/task/index.ts +10 -0
  78. package/src/cmd/cloud/task/list.ts +111 -4
  79. package/src/cmd/cloud/task/util.ts +59 -5
  80. package/src/cmd/coder/config/index.ts +20 -0
  81. package/src/cmd/coder/config/set.ts +112 -0
  82. package/src/cmd/coder/hub-url.ts +147 -53
  83. package/src/cmd/coder/index.ts +6 -1
  84. package/src/cmd/coder/inspect.ts +33 -10
  85. package/src/cmd/coder/list.ts +33 -10
  86. package/src/cmd/coder/start.ts +62 -26
  87. package/src/cmd/coder/tui-init.ts +7 -2
  88. package/src/cmd/dev/sync.ts +5 -5
  89. package/src/coder-config.ts +141 -0
  90. package/src/coder-hub-url.ts +32 -0
  91. package/src/config.ts +13 -0
  92. package/src/internal-logger.ts +83 -2
  93. package/src/keychain.ts +68 -39
  94. package/src/types.ts +10 -0
@@ -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
  }
@@ -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 [];