@agentuity/cli 2.0.13 → 2.0.15

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 (101) hide show
  1. package/dist/agent-detection.d.ts.map +1 -1
  2. package/dist/agent-detection.js +4 -6
  3. package/dist/agent-detection.js.map +1 -1
  4. package/dist/ai-help.js +10 -10
  5. package/dist/ai-help.js.map +1 -1
  6. package/dist/cmd/ai/capabilities/show.d.ts.map +1 -1
  7. package/dist/cmd/ai/capabilities/show.js +6 -0
  8. package/dist/cmd/ai/capabilities/show.js.map +1 -1
  9. package/dist/cmd/ai/intro.d.ts.map +1 -1
  10. package/dist/cmd/ai/intro.js +1 -0
  11. package/dist/cmd/ai/intro.js.map +1 -1
  12. package/dist/cmd/cloud/aigateway/complete.d.ts +7 -0
  13. package/dist/cmd/cloud/aigateway/complete.d.ts.map +1 -0
  14. package/dist/cmd/cloud/aigateway/complete.js +386 -0
  15. package/dist/cmd/cloud/aigateway/complete.js.map +1 -0
  16. package/dist/cmd/cloud/aigateway/index.d.ts +3 -0
  17. package/dist/cmd/cloud/aigateway/index.d.ts.map +1 -0
  18. package/dist/cmd/cloud/aigateway/index.js +20 -0
  19. package/dist/cmd/cloud/aigateway/index.js.map +1 -0
  20. package/dist/cmd/cloud/aigateway/model-cache.d.ts +4 -0
  21. package/dist/cmd/cloud/aigateway/model-cache.d.ts.map +1 -0
  22. package/dist/cmd/cloud/aigateway/model-cache.js +72 -0
  23. package/dist/cmd/cloud/aigateway/model-cache.js.map +1 -0
  24. package/dist/cmd/cloud/aigateway/models.d.ts +2 -0
  25. package/dist/cmd/cloud/aigateway/models.d.ts.map +1 -0
  26. package/dist/cmd/cloud/aigateway/models.js +193 -0
  27. package/dist/cmd/cloud/aigateway/models.js.map +1 -0
  28. package/dist/cmd/cloud/aigateway/util.d.ts +20 -0
  29. package/dist/cmd/cloud/aigateway/util.d.ts.map +1 -0
  30. package/dist/cmd/cloud/aigateway/util.js +58 -0
  31. package/dist/cmd/cloud/aigateway/util.js.map +1 -0
  32. package/dist/cmd/cloud/index.d.ts.map +1 -1
  33. package/dist/cmd/cloud/index.js +2 -0
  34. package/dist/cmd/cloud/index.js.map +1 -1
  35. package/dist/cmd/cloud/sandbox/create.d.ts.map +1 -1
  36. package/dist/cmd/cloud/sandbox/create.js +46 -4
  37. package/dist/cmd/cloud/sandbox/create.js.map +1 -1
  38. package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
  39. package/dist/cmd/cloud/sandbox/exec.js +4 -3
  40. package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
  41. package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
  42. package/dist/cmd/cloud/sandbox/run.js +9 -5
  43. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  44. package/dist/cmd/coder/skill/create.d.ts +2 -0
  45. package/dist/cmd/coder/skill/create.d.ts.map +1 -0
  46. package/dist/cmd/coder/skill/create.js +104 -0
  47. package/dist/cmd/coder/skill/create.js.map +1 -0
  48. package/dist/cmd/coder/skill/index.d.ts.map +1 -1
  49. package/dist/cmd/coder/skill/index.js +12 -1
  50. package/dist/cmd/coder/skill/index.js.map +1 -1
  51. package/dist/cmd/coder/start.d.ts.map +1 -1
  52. package/dist/cmd/coder/start.js +1 -0
  53. package/dist/cmd/coder/start.js.map +1 -1
  54. package/dist/cmd/coder/workspace/common.d.ts +22 -2
  55. package/dist/cmd/coder/workspace/common.d.ts.map +1 -1
  56. package/dist/cmd/coder/workspace/common.js +38 -2
  57. package/dist/cmd/coder/workspace/common.js.map +1 -1
  58. package/dist/cmd/coder/workspace/create.d.ts.map +1 -1
  59. package/dist/cmd/coder/workspace/create.js +34 -2
  60. package/dist/cmd/coder/workspace/create.js.map +1 -1
  61. package/dist/cmd/coder/workspace/update.d.ts.map +1 -1
  62. package/dist/cmd/coder/workspace/update.js +33 -1
  63. package/dist/cmd/coder/workspace/update.js.map +1 -1
  64. package/dist/cmd/dev/download.d.ts +8 -0
  65. package/dist/cmd/dev/download.d.ts.map +1 -1
  66. package/dist/cmd/dev/download.js +27 -1
  67. package/dist/cmd/dev/download.js.map +1 -1
  68. package/dist/cmd/dev/index.d.ts.map +1 -1
  69. package/dist/cmd/dev/index.js +18 -7
  70. package/dist/cmd/dev/index.js.map +1 -1
  71. package/dist/config.d.ts.map +1 -1
  72. package/dist/config.js +3 -0
  73. package/dist/config.js.map +1 -1
  74. package/dist/types.d.ts +3 -2
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +1 -0
  77. package/dist/types.js.map +1 -1
  78. package/package.json +7 -7
  79. package/src/agent-detection.ts +4 -6
  80. package/src/ai-help.ts +10 -10
  81. package/src/cmd/ai/capabilities/show.ts +6 -0
  82. package/src/cmd/ai/intro.ts +1 -0
  83. package/src/cmd/cloud/aigateway/complete.ts +461 -0
  84. package/src/cmd/cloud/aigateway/index.ts +21 -0
  85. package/src/cmd/cloud/aigateway/model-cache.ts +89 -0
  86. package/src/cmd/cloud/aigateway/models.ts +219 -0
  87. package/src/cmd/cloud/aigateway/util.ts +86 -0
  88. package/src/cmd/cloud/index.ts +2 -0
  89. package/src/cmd/cloud/sandbox/create.ts +57 -4
  90. package/src/cmd/cloud/sandbox/exec.ts +4 -3
  91. package/src/cmd/cloud/sandbox/run.ts +9 -5
  92. package/src/cmd/coder/skill/create.ts +122 -0
  93. package/src/cmd/coder/skill/index.ts +14 -1
  94. package/src/cmd/coder/start.ts +1 -0
  95. package/src/cmd/coder/workspace/common.ts +46 -2
  96. package/src/cmd/coder/workspace/create.ts +34 -1
  97. package/src/cmd/coder/workspace/update.ts +33 -0
  98. package/src/cmd/dev/download.ts +32 -1
  99. package/src/cmd/dev/index.ts +24 -8
  100. package/src/config.ts +3 -0
  101. package/src/types.ts +1 -0
@@ -0,0 +1,219 @@
1
+ import { z } from 'zod';
2
+ import { createCommand } from '../../../types';
3
+ import * as tui from '../../../tui';
4
+ import { getCommand } from '../../../command-prefix';
5
+ import { getExecutingAgent } from '../../../agent-detection';
6
+ import { createPublicAIGatewayService, getAIGatewayUrl } from './util';
7
+ import { getCachedAIGatewayModels, setCachedAIGatewayModels } from './model-cache';
8
+
9
+ const ModelRowSchema = z.object({
10
+ provider: z.string(),
11
+ id: z.string(),
12
+ name: z.string(),
13
+ api: z.string().optional(),
14
+ reasoning: z.boolean().optional(),
15
+ contextWindow: z.number().optional(),
16
+ maxOutputTokens: z.number().optional(),
17
+ });
18
+
19
+ const ModelsResponseSchema = z.object({
20
+ models: z.array(ModelRowSchema),
21
+ count: z.number(),
22
+ model: ModelRowSchema.nullable().optional(),
23
+ });
24
+
25
+ const recommendedModels = [
26
+ { use: 'fast', candidates: ['openai/gpt-4o-mini', 'openai/gpt-4.1-mini'] },
27
+ { use: 'reasoning', candidates: ['openai/gpt-5-mini', 'openai/o4-mini'] },
28
+ { use: 'coding', candidates: ['anthropic/claude-opus-4-7', 'openai/gpt-5-codex'] },
29
+ { use: 'cheap', candidates: ['openai/gpt-4.1-nano', 'openai/gpt-5-nano'] },
30
+ ];
31
+
32
+ function isAgentOutputMode(): boolean {
33
+ return Boolean(getExecutingAgent()) && process.env.AGENTUITY_AIGATEWAY_AGENT_OUTPUT !== 'false';
34
+ }
35
+
36
+ function getRecommendations(rows: z.infer<typeof ModelRowSchema>[]) {
37
+ const byId = new Map(rows.map((row) => [normalizeModelId(row.id), row]));
38
+ return recommendedModels
39
+ .map((rec) => {
40
+ const model = rec.candidates.map((id) => byId.get(normalizeModelId(id))).find(Boolean);
41
+ return model ? { use: rec.use, model: model.id, name: model.name } : undefined;
42
+ })
43
+ .filter((row): row is { use: string; model: string; name: string } => Boolean(row));
44
+ }
45
+
46
+ function normalizeModelId(id: string): string {
47
+ const normalized = id.toLowerCase();
48
+ const parts = normalized.split('/');
49
+ return parts.length > 1 ? (parts.at(-1) ?? normalized) : normalized;
50
+ }
51
+
52
+ function matchesProviderFilter(
53
+ provider: string,
54
+ modelId: string,
55
+ providerFilter?: string
56
+ ): boolean {
57
+ if (!providerFilter) {
58
+ return true;
59
+ }
60
+ return provider === providerFilter || modelId.startsWith(`${providerFilter}/`);
61
+ }
62
+
63
+ function matchesModelFilter(provider: string, modelId: string, modelFilter?: string): boolean {
64
+ if (!modelFilter) {
65
+ return true;
66
+ }
67
+ return modelId === modelFilter || `${provider}/${modelId}` === modelFilter;
68
+ }
69
+
70
+ function matchesNameFilter(modelId: string, modelName: string, nameFilter?: string): boolean {
71
+ if (!nameFilter) {
72
+ return true;
73
+ }
74
+ const normalized = nameFilter.toLowerCase();
75
+ return (
76
+ modelId.toLowerCase() === normalized ||
77
+ modelId.split('/').pop()?.toLowerCase() === normalized ||
78
+ modelName.toLowerCase() === normalized
79
+ );
80
+ }
81
+
82
+ export const modelsSubcommand = createCommand({
83
+ name: 'models',
84
+ aliases: ['list', 'ls'],
85
+ description: 'List AI Gateway models',
86
+ tags: ['read-only', 'fast'],
87
+ idempotent: true,
88
+ examples: [
89
+ { command: getCommand('cloud aigateway models'), description: 'List all models' },
90
+ {
91
+ command: getCommand('cloud aigateway models --provider openai'),
92
+ description: 'List OpenAI models',
93
+ },
94
+ {
95
+ command: getCommand('cloud aigateway models --model anthropic/claude-opus-4-7'),
96
+ description: 'Show one model by id',
97
+ },
98
+ ],
99
+ schema: {
100
+ options: z.object({
101
+ model: z.string().optional().describe('show one model by full provider/id'),
102
+ provider: z.string().optional().describe('filter by provider'),
103
+ name: z
104
+ .string()
105
+ .optional()
106
+ .describe('show one model by id or display name with --provider'),
107
+ reasoning: z.boolean().optional().describe('only show reasoning models'),
108
+ input: z.string().optional().describe('filter by input modality, such as text or image'),
109
+ output: z.string().optional().describe('filter by output modality, such as text or image'),
110
+ ids: z.boolean().optional().describe('only print model ids'),
111
+ simple: z.boolean().optional().describe('print a compact model list'),
112
+ recommended: z.boolean().optional().describe('show recommended models for common uses'),
113
+ refreshModels: z
114
+ .boolean()
115
+ .optional()
116
+ .describe('refresh the cached AI Gateway model catalog'),
117
+ }),
118
+ response: ModelsResponseSchema,
119
+ },
120
+ async handler(ctx) {
121
+ const service = createPublicAIGatewayService(ctx);
122
+ const profile = ctx.config?.name ?? 'default';
123
+ const cacheKey = getAIGatewayUrl(ctx.region, ctx.config?.overrides);
124
+ const cached = ctx.opts.refreshModels
125
+ ? null
126
+ : await getCachedAIGatewayModels(profile, cacheKey);
127
+ const catalog = cached ?? (await service.listModels());
128
+ if (!cached) {
129
+ await setCachedAIGatewayModels(profile, cacheKey, catalog);
130
+ }
131
+ const rows = Object.entries(catalog).flatMap(([provider, models]) =>
132
+ models
133
+ .filter((model) => matchesProviderFilter(provider, model.id, ctx.opts.provider))
134
+ .filter((model) => matchesModelFilter(provider, model.id, ctx.opts.model))
135
+ .filter((model) => matchesNameFilter(model.id, model.name, ctx.opts.name))
136
+ .filter((model) => !ctx.opts.reasoning || model.reasoning)
137
+ .filter((model) => !ctx.opts.input || model.input_modalities?.includes(ctx.opts.input))
138
+ .filter(
139
+ (model) => !ctx.opts.output || model.output_modalities?.includes(ctx.opts.output)
140
+ )
141
+ .map((model) => ({
142
+ provider,
143
+ id: model.id,
144
+ name: model.name,
145
+ api: model.api,
146
+ reasoning: model.reasoning,
147
+ contextWindow: model.context_window,
148
+ maxOutputTokens: model.max_output_tokens,
149
+ }))
150
+ );
151
+ const singleLookup = Boolean(ctx.opts.model || ctx.opts.name);
152
+ const selectedModel = singleLookup ? (rows[0] ?? null) : undefined;
153
+
154
+ const agentOutput = isAgentOutputMode();
155
+ if (ctx.options.json || agentOutput) {
156
+ if (agentOutput && !ctx.options.json) {
157
+ if (ctx.opts.ids) {
158
+ console.log(
159
+ JSON.stringify({ ids: rows.map((row) => row.id), count: rows.length }, null, 2)
160
+ );
161
+ } else if (ctx.opts.recommended) {
162
+ console.log(JSON.stringify({ recommendations: getRecommendations(rows) }, null, 2));
163
+ } else if (singleLookup) {
164
+ console.log(
165
+ JSON.stringify(
166
+ { model: selectedModel, models: rows, count: rows.length },
167
+ null,
168
+ 2
169
+ )
170
+ );
171
+ } else {
172
+ console.log(JSON.stringify({ models: rows, count: rows.length }, null, 2));
173
+ }
174
+ }
175
+ } else {
176
+ if (rows.length === 0) {
177
+ tui.info('No AI Gateway models found');
178
+ } else if (ctx.opts.ids) {
179
+ for (const row of rows) {
180
+ console.log(row.id);
181
+ }
182
+ } else if (ctx.opts.recommended) {
183
+ const recommendations = getRecommendations(rows).map((row) => ({
184
+ Use: row.use,
185
+ Model: row.model,
186
+ Name: row.name,
187
+ }));
188
+ if (recommendations.length === 0) {
189
+ tui.info('No recommended AI Gateway models found');
190
+ } else {
191
+ tui.table(recommendations, ['Use', 'Model', 'Name']);
192
+ }
193
+ } else if (ctx.opts.simple) {
194
+ tui.table(
195
+ rows.map((row) => ({
196
+ Model: row.id,
197
+ Name: row.name,
198
+ })),
199
+ ['Model', 'Name']
200
+ );
201
+ } else {
202
+ tui.info(`Found ${rows.length} AI Gateway model(s):`);
203
+ tui.table(
204
+ rows.map((row) => ({
205
+ Provider: row.provider,
206
+ Model: row.id,
207
+ Name: row.name,
208
+ API: row.api ?? '-',
209
+ Reasoning: row.reasoning ? 'yes' : 'no',
210
+ Context: row.contextWindow ?? '-',
211
+ })),
212
+ ['Provider', 'Model', 'Name', 'API', 'Reasoning', 'Context']
213
+ );
214
+ }
215
+ }
216
+
217
+ return { models: rows, count: rows.length, model: selectedModel };
218
+ },
219
+ });
@@ -0,0 +1,86 @@
1
+ import { AIGatewayService, type Logger } from '@agentuity/core';
2
+ import { createServerFetchAdapter, getServiceUrls } from '@agentuity/server';
3
+ import * as tui from '../../../tui';
4
+ import type { AuthData, Config, GlobalOptions, ProjectConfig } from '../../../types';
5
+
6
+ const defaultAIGatewayRegion = 'usc';
7
+
8
+ export function getAIGatewayUrl(
9
+ region?: string,
10
+ overrides?: { aigateway_url?: string } | null
11
+ ): string {
12
+ if (process.env.AGENTUITY_AIGATEWAY_URL) {
13
+ return process.env.AGENTUITY_AIGATEWAY_URL;
14
+ }
15
+ if (overrides?.aigateway_url) {
16
+ return overrides.aigateway_url;
17
+ }
18
+ return getServiceUrls(region || process.env.AGENTUITY_REGION || defaultAIGatewayRegion)
19
+ .aigateway;
20
+ }
21
+
22
+ export function createAIGatewayService(ctx: {
23
+ logger: Logger;
24
+ auth: AuthData;
25
+ region?: string;
26
+ project?: ProjectConfig;
27
+ config: Config | null;
28
+ options: GlobalOptions;
29
+ }) {
30
+ const orgId =
31
+ ctx.project?.orgId ??
32
+ ctx.options.orgId ??
33
+ (process.env.AGENTUITY_CLOUD_ORG_ID || ctx.config?.preferences?.orgId);
34
+ if (!orgId) {
35
+ tui.fatal(
36
+ 'Organization ID is required. Either run from a project directory or use --org-id flag.'
37
+ );
38
+ }
39
+
40
+ const adapter = createServerFetchAdapter(
41
+ {
42
+ headers: {
43
+ Authorization: `Bearer ${ctx.auth.apiKey}`,
44
+ 'x-agentuity-orgid': orgId,
45
+ },
46
+ },
47
+ ctx.logger
48
+ );
49
+
50
+ return new AIGatewayService(getAIGatewayUrl(ctx.region, ctx.config?.overrides), adapter);
51
+ }
52
+
53
+ export function createPublicAIGatewayService(ctx: {
54
+ logger: Logger;
55
+ region?: string;
56
+ config: Config | null;
57
+ }) {
58
+ const adapter = createServerFetchAdapter({ headers: {} }, ctx.logger);
59
+ return new AIGatewayService(getAIGatewayUrl(ctx.region, ctx.config?.overrides), adapter);
60
+ }
61
+
62
+ export function getCompletionText(response: unknown): string {
63
+ const choices = (response as { choices?: unknown }).choices;
64
+ const first =
65
+ Array.isArray(choices) && choices.length > 0
66
+ ? (choices[0] as { message?: { content?: unknown }; text?: unknown; delta?: unknown })
67
+ : undefined;
68
+ const content =
69
+ first?.message?.content ?? first?.text ?? (response as { content?: unknown }).content;
70
+ if (typeof content === 'string') {
71
+ return content;
72
+ }
73
+ if (Array.isArray(content)) {
74
+ return content
75
+ .map((part) => {
76
+ if (typeof part === 'string') return part;
77
+ if (part && typeof part === 'object' && 'text' in part) {
78
+ const text = (part as { text?: unknown }).text;
79
+ return typeof text === 'string' ? text : '';
80
+ }
81
+ return '';
82
+ })
83
+ .join('');
84
+ }
85
+ return '';
86
+ }
@@ -14,6 +14,7 @@ import webhookCommand from './webhook';
14
14
  import { agentCommand } from './agent';
15
15
  import envCommand from './env';
16
16
  import apikeyCommand from './apikey';
17
+ import { aigatewayCommand } from './aigateway';
17
18
  import oidcCommand from './oidc';
18
19
  import streamCommand from './stream';
19
20
  import vectorCommand from './vector';
@@ -41,6 +42,7 @@ export const command = createCommand({
41
42
  ],
42
43
  subcommands: [
43
44
  apikeyCommand,
45
+ aigatewayCommand,
44
46
  oidcCommand,
45
47
  keyvalueCommand,
46
48
  queueCommand,
@@ -3,7 +3,7 @@ import { createCommand } from '../../../types';
3
3
  import * as tui from '../../../tui';
4
4
  import { createSandboxClient, parseFileArgs, cacheSandboxTarget } from './util';
5
5
  import { getCommand } from '../../../command-prefix';
6
- import { sandboxCreate } from '@agentuity/server';
6
+ import { sandboxCreate, sandboxGet } from '@agentuity/server';
7
7
  import { StructuredError } from '@agentuity/core';
8
8
  import { validateAptDependencies } from '../../../utils/apt-validator';
9
9
  import { ErrorCode } from '../../../errors';
@@ -13,6 +13,11 @@ const InvalidMetadataError = StructuredError(
13
13
  'Metadata must be a valid JSON object'
14
14
  );
15
15
 
16
+ const CREATE_WAIT_POLL_MS = 1000;
17
+ const CREATE_WAIT_STATUSES = ['idle', 'running', 'failed', 'terminated', 'deleted'];
18
+ const CREATE_READY_STATUSES = new Set(['idle', 'running']);
19
+ const CREATE_TERMINAL_STATUSES = new Set(['failed', 'terminated', 'deleted']);
20
+
16
21
  const SandboxCreateResponseSchema = z.object({
17
22
  sandboxId: z.string().describe('Unique sandbox identifier'),
18
23
  status: z.string().describe('Current sandbox status'),
@@ -101,6 +106,14 @@ export const createSubcommand = createCommand({
101
106
  .optional()
102
107
  .describe('Port to expose from the sandbox to the outside Internet (1024-65535)'),
103
108
  projectId: z.string().optional().describe('Project ID to associate this sandbox with'),
109
+ wait: z.boolean().optional().describe('Wait until the sandbox is ready before returning'),
110
+ waitMs: z
111
+ .number()
112
+ .int()
113
+ .nonnegative()
114
+ .max(60000)
115
+ .optional()
116
+ .describe('Maximum time in milliseconds to wait when --wait is used'),
104
117
  }),
105
118
  response: SandboxCreateResponseSchema,
106
119
  },
@@ -180,7 +193,7 @@ export const createSubcommand = createCommand({
180
193
  metadata = parsed as Record<string, unknown>;
181
194
  }
182
195
 
183
- const result = await sandboxCreate(client, {
196
+ let result = await sandboxCreate(client, {
184
197
  options: {
185
198
  projectId,
186
199
  runtime: opts.runtime,
@@ -216,8 +229,48 @@ export const createSubcommand = createCommand({
216
229
  orgId,
217
230
  });
218
231
 
219
- // Cache routing context for future sandbox commands.
220
- await cacheSandboxTarget(config?.name, result.sandboxId, region, orgId);
232
+ if (opts.wait) {
233
+ const waitMs = opts.waitMs ?? 60000;
234
+ const deadline = Date.now() + waitMs;
235
+
236
+ while (
237
+ !CREATE_READY_STATUSES.has(result.status) &&
238
+ !CREATE_TERMINAL_STATUSES.has(result.status)
239
+ ) {
240
+ const remainingMs = Math.max(0, deadline - Date.now());
241
+ if (remainingMs === 0) {
242
+ break;
243
+ }
244
+ const pollWaitMs = Math.min(remainingMs, CREATE_WAIT_POLL_MS);
245
+ const current = await sandboxGet(client, {
246
+ sandboxId: result.sandboxId,
247
+ orgId,
248
+ includeDeleted: true,
249
+ waitForStatus: CREATE_WAIT_STATUSES,
250
+ waitMs: pollWaitMs,
251
+ });
252
+
253
+ result = {
254
+ ...result,
255
+ status: current.status === 'deleted' ? 'terminated' : current.status,
256
+ };
257
+
258
+ if (
259
+ CREATE_READY_STATUSES.has(current.status) ||
260
+ CREATE_TERMINAL_STATUSES.has(current.status)
261
+ ) {
262
+ break;
263
+ }
264
+ }
265
+ }
266
+
267
+ if (
268
+ CREATE_READY_STATUSES.has(result.status) &&
269
+ !CREATE_TERMINAL_STATUSES.has(result.status)
270
+ ) {
271
+ // Cache routing context for future sandbox commands.
272
+ await cacheSandboxTarget(config?.name, result.sandboxId, region, orgId);
273
+ }
221
274
 
222
275
  if (!options.json) {
223
276
  const duration = Date.now() - started;
@@ -101,6 +101,7 @@ export const execSubcommand = createCommand({
101
101
  // Detect if stdout/stderr are redirected to /dev/null
102
102
  const stdoutIsNull = detectNullStream(1);
103
103
  const stderrIsNull = detectNullStream(2);
104
+ const quiet = opts.quiet || options.quiet;
104
105
 
105
106
  // Build stream configuration
106
107
  const streamConfig: {
@@ -112,7 +113,7 @@ export const execSubcommand = createCommand({
112
113
  };
113
114
 
114
115
  // --quiet: suppress all output streams (no server streams, no local capture)
115
- if (opts.quiet) {
116
+ if (quiet) {
116
117
  streamConfig.stdout = 'ignore';
117
118
  streamConfig.stderr = 'ignore';
118
119
  } else if (!options.json) {
@@ -177,14 +178,14 @@ export const execSubcommand = createCommand({
177
178
  const stderrChunks: string[] = [];
178
179
 
179
180
  const stdoutWritable: NodeJS.WritableStream =
180
- options.json || stdoutIsNull
181
+ options.json || quiet || stdoutIsNull
181
182
  ? createCaptureStream((chunk) => {
182
183
  stdoutChunks.push(chunk);
183
184
  outputChunks.push(chunk);
184
185
  })
185
186
  : process.stdout;
186
187
  const stderrWritable: NodeJS.WritableStream =
187
- options.json || stderrIsNull
188
+ options.json || quiet || stderrIsNull
188
189
  ? createCaptureStream((chunk) => {
189
190
  stderrChunks.push(chunk);
190
191
  outputChunks.push(chunk);
@@ -157,6 +157,7 @@ export const runSubcommand = createCommand({
157
157
  // Detect if stdout/stderr are redirected to /dev/null
158
158
  const stdoutIsNull = detectNullStream(1);
159
159
  const stderrIsNull = detectNullStream(2);
160
+ const quiet = opts.quiet || options.quiet;
160
161
 
161
162
  // Detect stream configuration based on TTY status and flags
162
163
  const streamConfig: {
@@ -171,7 +172,7 @@ export const runSubcommand = createCommand({
171
172
  };
172
173
 
173
174
  // --quiet: suppress all output streams (no server streams, no local capture)
174
- if (opts.quiet) {
175
+ if (quiet) {
175
176
  streamConfig.stdout = 'ignore';
176
177
  streamConfig.stderr = 'ignore';
177
178
  } else if (!options.json) {
@@ -187,11 +188,11 @@ export const runSubcommand = createCommand({
187
188
 
188
189
  // For JSON output or quiet mode, we need to capture output instead of streaming to process
189
190
  const stdout =
190
- options.json || opts.quiet || stdoutIsNull
191
+ options.json || quiet || stdoutIsNull
191
192
  ? createCaptureStream((chunk) => outputChunks.push(chunk))
192
193
  : process.stdout;
193
194
  const stderr =
194
- options.json || opts.quiet || stderrIsNull
195
+ options.json || quiet || stderrIsNull
195
196
  ? createCaptureStream((chunk) => outputChunks.push(chunk))
196
197
  : process.stderr;
197
198
 
@@ -233,8 +234,11 @@ export const runSubcommand = createCommand({
233
234
  logger,
234
235
  });
235
236
 
236
- // Cache routing context for follow-up lookup/debug flows during execution.
237
- await cacheSandboxTarget(config?.name, result.sandboxId, region, orgId);
237
+ // Best-effort bookkeeping only. One-shot sandboxes are already done by
238
+ // this point, so don't keep the user waiting on local cache I/O.
239
+ void cacheSandboxTarget(config?.name, result.sandboxId, region, orgId).catch((err) => {
240
+ logger.debug('[run] failed to cache sandbox target: %s', err);
241
+ });
238
242
 
239
243
  const duration = Date.now() - started;
240
244
  const output = outputChunks.join('');
@@ -0,0 +1,122 @@
1
+ import { z } from 'zod';
2
+ import { CoderClient } 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
+ async function readSkillContent(input: {
10
+ content?: string;
11
+ contentFile?: string;
12
+ }): Promise<string> {
13
+ if (input.content !== undefined && input.contentFile) {
14
+ throw new Error('Use either --content or --content-file, not both.');
15
+ }
16
+ if (input.content !== undefined) return input.content;
17
+ if (!input.contentFile) {
18
+ throw new Error('Provide --content or --content-file.');
19
+ }
20
+ try {
21
+ return await Bun.file(input.contentFile).text();
22
+ } catch (error) {
23
+ throw new Error(
24
+ `Failed to read content file "${input.contentFile}": ${
25
+ error instanceof Error ? error.message : String(error)
26
+ }`,
27
+ { cause: error }
28
+ );
29
+ }
30
+ }
31
+
32
+ export const createCustomSkillSubcommand = createSubcommand({
33
+ name: 'create',
34
+ aliases: ['new'],
35
+ description: 'Create a custom SKILL.md-backed skill',
36
+ tags: ['mutating', 'requires-auth'],
37
+ requires: { auth: true, org: true },
38
+ examples: [
39
+ {
40
+ command: getCommand(
41
+ 'coder skill create --skill-id release-checklist --name "Release checklist" --content-file ./SKILL.md'
42
+ ),
43
+ description: 'Create a custom skill from a SKILL.md file',
44
+ },
45
+ {
46
+ command: getCommand(
47
+ 'coder skill create --skill-id release-checklist --name "Release checklist" --content "# Release checklist" --json'
48
+ ),
49
+ description: 'Create a custom skill from inline content and return JSON',
50
+ },
51
+ ],
52
+ schema: {
53
+ options: z.object({
54
+ url: z.string().optional().describe('Coder API URL override'),
55
+ skillId: z.string().describe('Skill identifier'),
56
+ name: z.string().describe('Skill name'),
57
+ description: z.string().optional().describe('Skill description'),
58
+ content: z.string().optional().describe('Inline SKILL.md content'),
59
+ contentFile: z.string().optional().describe('Path to a SKILL.md file'),
60
+ }),
61
+ },
62
+ async handler(ctx) {
63
+ const { opts, options } = ctx;
64
+ const client = new CoderClient({
65
+ apiKey: ctx.auth.apiKey,
66
+ url: opts?.url,
67
+ orgId: ctx.orgId,
68
+ });
69
+
70
+ let content: string;
71
+ try {
72
+ content = await readSkillContent({
73
+ content: opts?.content,
74
+ contentFile: opts?.contentFile,
75
+ });
76
+ } catch (err) {
77
+ const msg = err instanceof Error ? err.message : String(err);
78
+ tui.fatal(`Failed to create custom skill: ${msg}`, ErrorCode.VALIDATION_FAILED);
79
+ return;
80
+ }
81
+
82
+ if (!content.trim()) {
83
+ tui.fatal(
84
+ 'Failed to create custom skill: SKILL.md content cannot be empty.',
85
+ ErrorCode.VALIDATION_FAILED
86
+ );
87
+ return;
88
+ }
89
+
90
+ try {
91
+ const saved = await client.createCustomSkill({
92
+ skillId: opts.skillId,
93
+ name: opts.name,
94
+ ...(opts?.description !== undefined ? { description: opts.description } : {}),
95
+ content,
96
+ });
97
+
98
+ if (options.json) {
99
+ return saved;
100
+ }
101
+
102
+ tui.success(`Custom skill ${saved.id} created.`);
103
+ tui.newline();
104
+ tui.output(` Name: ${tui.bold(saved.name)}`);
105
+ tui.output(` Skill ID: ${saved.skillId}`);
106
+ tui.output(` Source: ${saved.source}`);
107
+ if (saved.description) {
108
+ tui.output(` Desc: ${saved.description}`);
109
+ }
110
+
111
+ return saved;
112
+ } catch (err) {
113
+ if (err instanceof ValidationOutputError) {
114
+ ctx.logger.trace('Validation response URL: %s', err.url ?? 'unknown');
115
+ ctx.logger.trace('Validation issues: %s', JSON.stringify(err.issues, null, 2));
116
+ tui.fatal(`Failed to create custom skill: ${err.message}`, ErrorCode.VALIDATION_FAILED);
117
+ }
118
+ const msg = err instanceof Error ? err.message : String(err);
119
+ tui.fatal(`Failed to create custom skill: ${msg}`, ErrorCode.NETWORK_ERROR);
120
+ }
121
+ },
122
+ });
@@ -1,5 +1,6 @@
1
1
  import { createCommand } from '../../../types';
2
2
  import { listSubcommand } from './list';
3
+ import { createCustomSkillSubcommand } from './create';
3
4
  import { saveSkillSubcommand } from './save';
4
5
  import { deleteSkillSubcommand } from './delete';
5
6
  import { bucketsSubcommand } from './buckets';
@@ -16,6 +17,12 @@ export const skillCommand = createCommand({
16
17
  command: getCommand('coder skill list'),
17
18
  description: 'List saved skills',
18
19
  },
20
+ {
21
+ command: getCommand(
22
+ 'coder skill create --skill-id release-checklist --name "Release checklist" --content-file ./SKILL.md'
23
+ ),
24
+ description: 'Create a custom skill',
25
+ },
19
26
  {
20
27
  command: getCommand(
21
28
  'coder skill save --repo org/repo --skill-id sk_abc --name "My Skill"'
@@ -31,5 +38,11 @@ export const skillCommand = createCommand({
31
38
  description: 'List skill buckets',
32
39
  },
33
40
  ],
34
- subcommands: [listSubcommand, saveSkillSubcommand, deleteSkillSubcommand, bucketsSubcommand],
41
+ subcommands: [
42
+ listSubcommand,
43
+ createCustomSkillSubcommand,
44
+ saveSkillSubcommand,
45
+ deleteSkillSubcommand,
46
+ bucketsSubcommand,
47
+ ],
35
48
  });
@@ -391,6 +391,7 @@ export const startSubcommand = createSubcommand({
391
391
  };
392
392
  env.AGENTUITY_CODER_API_KEY = ctx.auth.apiKey;
393
393
  env.AGENTUITY_ORGID = ctx.orgId;
394
+ env.PI_SKIP_VERSION_CHECK = '1';
394
395
 
395
396
  if (opts?.agent) {
396
397
  env.AGENTUITY_CODER_AGENT = opts.agent;