@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.
- package/dist/agent-detection.d.ts.map +1 -1
- package/dist/agent-detection.js +4 -6
- package/dist/agent-detection.js.map +1 -1
- package/dist/ai-help.js +10 -10
- package/dist/ai-help.js.map +1 -1
- package/dist/cmd/ai/capabilities/show.d.ts.map +1 -1
- package/dist/cmd/ai/capabilities/show.js +6 -0
- package/dist/cmd/ai/capabilities/show.js.map +1 -1
- package/dist/cmd/ai/intro.d.ts.map +1 -1
- package/dist/cmd/ai/intro.js +1 -0
- package/dist/cmd/ai/intro.js.map +1 -1
- package/dist/cmd/cloud/aigateway/complete.d.ts +7 -0
- package/dist/cmd/cloud/aigateway/complete.d.ts.map +1 -0
- package/dist/cmd/cloud/aigateway/complete.js +386 -0
- package/dist/cmd/cloud/aigateway/complete.js.map +1 -0
- package/dist/cmd/cloud/aigateway/index.d.ts +3 -0
- package/dist/cmd/cloud/aigateway/index.d.ts.map +1 -0
- package/dist/cmd/cloud/aigateway/index.js +20 -0
- package/dist/cmd/cloud/aigateway/index.js.map +1 -0
- package/dist/cmd/cloud/aigateway/model-cache.d.ts +4 -0
- package/dist/cmd/cloud/aigateway/model-cache.d.ts.map +1 -0
- package/dist/cmd/cloud/aigateway/model-cache.js +72 -0
- package/dist/cmd/cloud/aigateway/model-cache.js.map +1 -0
- package/dist/cmd/cloud/aigateway/models.d.ts +2 -0
- package/dist/cmd/cloud/aigateway/models.d.ts.map +1 -0
- package/dist/cmd/cloud/aigateway/models.js +193 -0
- package/dist/cmd/cloud/aigateway/models.js.map +1 -0
- package/dist/cmd/cloud/aigateway/util.d.ts +20 -0
- package/dist/cmd/cloud/aigateway/util.d.ts.map +1 -0
- package/dist/cmd/cloud/aigateway/util.js +58 -0
- package/dist/cmd/cloud/aigateway/util.js.map +1 -0
- package/dist/cmd/cloud/index.d.ts.map +1 -1
- package/dist/cmd/cloud/index.js +2 -0
- package/dist/cmd/cloud/index.js.map +1 -1
- package/dist/cmd/cloud/sandbox/create.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/create.js +46 -4
- package/dist/cmd/cloud/sandbox/create.js.map +1 -1
- package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/exec.js +4 -3
- package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
- package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/run.js +9 -5
- package/dist/cmd/cloud/sandbox/run.js.map +1 -1
- package/dist/cmd/coder/skill/create.d.ts +2 -0
- package/dist/cmd/coder/skill/create.d.ts.map +1 -0
- package/dist/cmd/coder/skill/create.js +104 -0
- package/dist/cmd/coder/skill/create.js.map +1 -0
- package/dist/cmd/coder/skill/index.d.ts.map +1 -1
- package/dist/cmd/coder/skill/index.js +12 -1
- package/dist/cmd/coder/skill/index.js.map +1 -1
- package/dist/cmd/coder/start.d.ts.map +1 -1
- package/dist/cmd/coder/start.js +1 -0
- package/dist/cmd/coder/start.js.map +1 -1
- package/dist/cmd/coder/workspace/common.d.ts +22 -2
- package/dist/cmd/coder/workspace/common.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/common.js +38 -2
- package/dist/cmd/coder/workspace/common.js.map +1 -1
- package/dist/cmd/coder/workspace/create.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/create.js +34 -2
- package/dist/cmd/coder/workspace/create.js.map +1 -1
- package/dist/cmd/coder/workspace/update.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/update.js +33 -1
- package/dist/cmd/coder/workspace/update.js.map +1 -1
- package/dist/cmd/dev/download.d.ts +8 -0
- package/dist/cmd/dev/download.d.ts.map +1 -1
- package/dist/cmd/dev/download.js +27 -1
- package/dist/cmd/dev/download.js.map +1 -1
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/index.js +18 -7
- package/dist/cmd/dev/index.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/types.d.ts +3 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +7 -7
- package/src/agent-detection.ts +4 -6
- package/src/ai-help.ts +10 -10
- package/src/cmd/ai/capabilities/show.ts +6 -0
- package/src/cmd/ai/intro.ts +1 -0
- package/src/cmd/cloud/aigateway/complete.ts +461 -0
- package/src/cmd/cloud/aigateway/index.ts +21 -0
- package/src/cmd/cloud/aigateway/model-cache.ts +89 -0
- package/src/cmd/cloud/aigateway/models.ts +219 -0
- package/src/cmd/cloud/aigateway/util.ts +86 -0
- package/src/cmd/cloud/index.ts +2 -0
- package/src/cmd/cloud/sandbox/create.ts +57 -4
- package/src/cmd/cloud/sandbox/exec.ts +4 -3
- package/src/cmd/cloud/sandbox/run.ts +9 -5
- package/src/cmd/coder/skill/create.ts +122 -0
- package/src/cmd/coder/skill/index.ts +14 -1
- package/src/cmd/coder/start.ts +1 -0
- package/src/cmd/coder/workspace/common.ts +46 -2
- package/src/cmd/coder/workspace/create.ts +34 -1
- package/src/cmd/coder/workspace/update.ts +33 -0
- package/src/cmd/dev/download.ts +32 -1
- package/src/cmd/dev/index.ts +24 -8
- package/src/config.ts +3 -0
- 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
|
+
}
|
package/src/cmd/cloud/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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 (
|
|
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 (
|
|
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 ||
|
|
191
|
+
options.json || quiet || stdoutIsNull
|
|
191
192
|
? createCaptureStream((chunk) => outputChunks.push(chunk))
|
|
192
193
|
: process.stdout;
|
|
193
194
|
const stderr =
|
|
194
|
-
options.json ||
|
|
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
|
-
//
|
|
237
|
-
|
|
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: [
|
|
41
|
+
subcommands: [
|
|
42
|
+
listSubcommand,
|
|
43
|
+
createCustomSkillSubcommand,
|
|
44
|
+
saveSkillSubcommand,
|
|
45
|
+
deleteSkillSubcommand,
|
|
46
|
+
bucketsSubcommand,
|
|
47
|
+
],
|
|
35
48
|
});
|
package/src/cmd/coder/start.ts
CHANGED