@agentuity/cli 2.0.14 → 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 (85) hide show
  1. package/dist/agent-detection.d.ts.map +1 -1
  2. package/dist/agent-detection.js +3 -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/coder/skill/create.d.ts +2 -0
  36. package/dist/cmd/coder/skill/create.d.ts.map +1 -0
  37. package/dist/cmd/coder/skill/create.js +104 -0
  38. package/dist/cmd/coder/skill/create.js.map +1 -0
  39. package/dist/cmd/coder/skill/index.d.ts.map +1 -1
  40. package/dist/cmd/coder/skill/index.js +12 -1
  41. package/dist/cmd/coder/skill/index.js.map +1 -1
  42. package/dist/cmd/coder/workspace/common.d.ts +22 -2
  43. package/dist/cmd/coder/workspace/common.d.ts.map +1 -1
  44. package/dist/cmd/coder/workspace/common.js +38 -2
  45. package/dist/cmd/coder/workspace/common.js.map +1 -1
  46. package/dist/cmd/coder/workspace/create.d.ts.map +1 -1
  47. package/dist/cmd/coder/workspace/create.js +34 -2
  48. package/dist/cmd/coder/workspace/create.js.map +1 -1
  49. package/dist/cmd/coder/workspace/update.d.ts.map +1 -1
  50. package/dist/cmd/coder/workspace/update.js +33 -1
  51. package/dist/cmd/coder/workspace/update.js.map +1 -1
  52. package/dist/cmd/dev/download.d.ts +8 -0
  53. package/dist/cmd/dev/download.d.ts.map +1 -1
  54. package/dist/cmd/dev/download.js +27 -1
  55. package/dist/cmd/dev/download.js.map +1 -1
  56. package/dist/cmd/dev/index.d.ts.map +1 -1
  57. package/dist/cmd/dev/index.js +18 -7
  58. package/dist/cmd/dev/index.js.map +1 -1
  59. package/dist/config.d.ts.map +1 -1
  60. package/dist/config.js +3 -0
  61. package/dist/config.js.map +1 -1
  62. package/dist/types.d.ts +3 -2
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js +1 -0
  65. package/dist/types.js.map +1 -1
  66. package/package.json +7 -7
  67. package/src/agent-detection.ts +3 -6
  68. package/src/ai-help.ts +10 -10
  69. package/src/cmd/ai/capabilities/show.ts +6 -0
  70. package/src/cmd/ai/intro.ts +1 -0
  71. package/src/cmd/cloud/aigateway/complete.ts +461 -0
  72. package/src/cmd/cloud/aigateway/index.ts +21 -0
  73. package/src/cmd/cloud/aigateway/model-cache.ts +89 -0
  74. package/src/cmd/cloud/aigateway/models.ts +219 -0
  75. package/src/cmd/cloud/aigateway/util.ts +86 -0
  76. package/src/cmd/cloud/index.ts +2 -0
  77. package/src/cmd/coder/skill/create.ts +122 -0
  78. package/src/cmd/coder/skill/index.ts +14 -1
  79. package/src/cmd/coder/workspace/common.ts +46 -2
  80. package/src/cmd/coder/workspace/create.ts +34 -1
  81. package/src/cmd/coder/workspace/update.ts +33 -0
  82. package/src/cmd/dev/download.ts +32 -1
  83. package/src/cmd/dev/index.ts +24 -8
  84. package/src/config.ts +3 -0
  85. 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,
@@ -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
  });
@@ -2,16 +2,32 @@ import {
2
2
  type CoderCreateWorkspaceRequest,
3
3
  type CoderUpdateWorkspaceRequest,
4
4
  type CoderWorkspaceDetail,
5
+ type CoderWorkspaceSystemPromptMode,
5
6
  } from '@agentuity/core/coder';
6
7
  import { StructuredError } from '@agentuity/core';
7
8
  import * as tui from '../../../tui';
8
9
 
9
10
  export const EMPTY_WORKSPACE_ERROR =
10
- 'A workspace needs at least one repo, dependency, setup script, saved skill, skill bucket, or agent';
11
+ 'A workspace needs at least one repo, dependency, setup script, system prompt, saved skill, skill bucket, or agent';
11
12
  export const SetupScriptValidationError = StructuredError('SetupScriptValidationError')<{
12
13
  message: string;
13
14
  path?: string;
14
15
  }>();
16
+ export const SystemPromptValidationError = StructuredError('SystemPromptValidationError')<{
17
+ message: string;
18
+ path?: string;
19
+ }>();
20
+
21
+ export function normalizeSystemPromptMode(
22
+ value?: string
23
+ ): CoderWorkspaceSystemPromptMode | undefined {
24
+ if (value === undefined) return undefined;
25
+ const normalized = value.trim().toLowerCase();
26
+ if (normalized === 'append' || normalized === 'overwrite') return normalized;
27
+ throw new SystemPromptValidationError({
28
+ message: 'Use --system-prompt-mode append or --system-prompt-mode overwrite.',
29
+ });
30
+ }
15
31
 
16
32
  export function parseCommaList(value?: string): string[] {
17
33
  return value
@@ -46,11 +62,36 @@ export async function readSetupScript(input: {
46
62
  }
47
63
  }
48
64
 
65
+ export async function readSystemPrompt(input: {
66
+ systemPrompt?: string;
67
+ systemPromptFile?: string;
68
+ }): Promise<string | undefined> {
69
+ if (input.systemPrompt !== undefined && input.systemPromptFile) {
70
+ throw new SystemPromptValidationError({
71
+ message: 'Use either --system-prompt or --system-prompt-file, not both.',
72
+ });
73
+ }
74
+ if (input.systemPrompt !== undefined) return input.systemPrompt;
75
+ if (!input.systemPromptFile) return undefined;
76
+ try {
77
+ return await Bun.file(input.systemPromptFile).text();
78
+ } catch (error) {
79
+ throw new SystemPromptValidationError({
80
+ message: `Failed to read system prompt file "${input.systemPromptFile}": ${
81
+ error instanceof Error ? error.message : String(error)
82
+ }`,
83
+ path: input.systemPromptFile,
84
+ cause: error,
85
+ });
86
+ }
87
+ }
88
+
49
89
  export function hasWorkspaceSelections(input: CoderCreateWorkspaceRequest): boolean {
50
90
  return (
51
91
  (input.repos?.length ?? 0) > 0 ||
52
92
  (input.dependencies?.length ?? 0) > 0 ||
53
93
  Boolean(input.setupScript?.trim()) ||
94
+ Boolean(input.systemPrompt?.trim()) ||
54
95
  (input.savedSkillIds?.length ?? 0) > 0 ||
55
96
  (input.skillBucketIds?.length ?? 0) > 0 ||
56
97
  (input.enabledAgents?.length ?? 0) > 0
@@ -67,7 +108,7 @@ export function formatWorkspaceValidationMessage(issues: Array<{ message: string
67
108
  return 'Invalid workspace configuration';
68
109
  }
69
110
  if (messages.includes(EMPTY_WORKSPACE_ERROR)) {
70
- return `${EMPTY_WORKSPACE_ERROR}. Use --repo, --dependency, --setup-script, or --enabled-agents.`;
111
+ return `${EMPTY_WORKSPACE_ERROR}. Use --repo, --dependency, --setup-script, --system-prompt, or --enabled-agents.`;
71
112
  }
72
113
  return messages.join('; ');
73
114
  }
@@ -94,6 +135,9 @@ export function printWorkspaceSummary(workspace: CoderWorkspaceDetail): void {
94
135
  if (workspace.setupScript) {
95
136
  tui.output(' Setup: configured');
96
137
  }
138
+ if (workspace.systemPrompt) {
139
+ tui.output(` Prompt: configured (${workspace.systemPromptMode})`);
140
+ }
97
141
  if (workspace.snapshot?.status) {
98
142
  tui.output(` Snapshot: ${workspace.snapshot.status}`);
99
143
  }
@@ -14,8 +14,10 @@ import {
14
14
  EMPTY_WORKSPACE_ERROR,
15
15
  formatWorkspaceValidationMessage,
16
16
  hasWorkspaceSelections,
17
+ normalizeSystemPromptMode,
17
18
  parseCommaList,
18
19
  printWorkspaceSummary,
20
+ readSystemPrompt,
19
21
  readSetupScript,
20
22
  } from './common';
21
23
 
@@ -38,6 +40,12 @@ export const createWorkspaceSubcommand = createSubcommand({
38
40
  ),
39
41
  description: 'Create an org-scoped workspace with dependencies and a setup script',
40
42
  },
43
+ {
44
+ command: getCommand(
45
+ 'coder workspace create "My Workspace" --system-prompt-file ./WORKSPACE_PROMPT.md --system-prompt-mode overwrite'
46
+ ),
47
+ description: 'Create a workspace with Lead system prompt instructions',
48
+ },
41
49
  {
42
50
  command: getCommand('coder workspace create "My Workspace" --enabled-agents code-review'),
43
51
  description: 'Create a workspace with an agent roster',
@@ -71,6 +79,18 @@ export const createWorkspaceSubcommand = createSubcommand({
71
79
  .string()
72
80
  .optional()
73
81
  .describe('Path to a shell script to run while preparing workspace snapshots'),
82
+ systemPrompt: z
83
+ .string()
84
+ .optional()
85
+ .describe('Inline Lead system prompt to apply to sessions created from this workspace'),
86
+ systemPromptFile: z
87
+ .string()
88
+ .optional()
89
+ .describe('Path to a file containing the workspace Lead system prompt'),
90
+ systemPromptMode: z
91
+ .string()
92
+ .optional()
93
+ .describe('How to apply the system prompt: append or overwrite'),
74
94
  enabledAgents: z
75
95
  .string()
76
96
  .optional()
@@ -116,12 +136,25 @@ export const createWorkspaceSubcommand = createSubcommand({
116
136
  tui.fatal(`Failed to read setup script: ${msg}`, ErrorCode.VALIDATION_FAILED);
117
137
  return;
118
138
  }
139
+ try {
140
+ const systemPrompt = await readSystemPrompt({
141
+ systemPrompt: opts?.systemPrompt,
142
+ systemPromptFile: opts?.systemPromptFile,
143
+ });
144
+ if (systemPrompt !== undefined) body.systemPrompt = systemPrompt;
145
+ const systemPromptMode = normalizeSystemPromptMode(opts?.systemPromptMode);
146
+ if (systemPromptMode !== undefined) body.systemPromptMode = systemPromptMode;
147
+ } catch (err) {
148
+ const msg = err instanceof Error ? err.message : String(err);
149
+ tui.fatal(`Failed to read system prompt: ${msg}`, ErrorCode.VALIDATION_FAILED);
150
+ return;
151
+ }
119
152
  if (opts?.enabledAgents) {
120
153
  body.enabledAgents = parseCommaList(opts.enabledAgents);
121
154
  }
122
155
  if (!hasWorkspaceSelections(body)) {
123
156
  tui.fatal(
124
- `Failed to create workspace: ${EMPTY_WORKSPACE_ERROR}. Use --repo, --dependency, --setup-script, or --enabled-agents.`,
157
+ `Failed to create workspace: ${EMPTY_WORKSPACE_ERROR}. Use --repo, --dependency, --setup-script, --system-prompt, or --enabled-agents.`,
125
158
  ErrorCode.VALIDATION_FAILED
126
159
  );
127
160
  }
@@ -13,8 +13,10 @@ import { resolveGitHubRepo } from '../resolve-repo';
13
13
  import {
14
14
  formatWorkspaceValidationMessage,
15
15
  hasWorkspaceUpdate,
16
+ normalizeSystemPromptMode,
16
17
  parseCommaList,
17
18
  printWorkspaceSummary,
19
+ readSystemPrompt,
18
20
  readSetupScript,
19
21
  } from './common';
20
22
 
@@ -33,6 +35,12 @@ export const updateWorkspaceSubcommand = createSubcommand({
33
35
  command: getCommand('coder workspace update ws_abc123 --setup-script-file ./setup.sh'),
34
36
  description: 'Update the workspace setup script',
35
37
  },
38
+ {
39
+ command: getCommand(
40
+ 'coder workspace update ws_abc123 --system-prompt-file ./WORKSPACE_PROMPT.md --system-prompt-mode append'
41
+ ),
42
+ description: 'Update the workspace Lead system prompt',
43
+ },
36
44
  ],
37
45
  schema: {
38
46
  args: z.object({
@@ -57,6 +65,18 @@ export const updateWorkspaceSubcommand = createSubcommand({
57
65
  .string()
58
66
  .optional()
59
67
  .describe('Path to a shell script to run while preparing workspace snapshots'),
68
+ systemPrompt: z
69
+ .string()
70
+ .optional()
71
+ .describe('Inline Lead system prompt to apply to sessions created from this workspace'),
72
+ systemPromptFile: z
73
+ .string()
74
+ .optional()
75
+ .describe('Path to a file containing the workspace Lead system prompt'),
76
+ systemPromptMode: z
77
+ .string()
78
+ .optional()
79
+ .describe('How to apply the system prompt: append or overwrite'),
60
80
  enabledAgents: z
61
81
  .string()
62
82
  .optional()
@@ -102,6 +122,19 @@ export const updateWorkspaceSubcommand = createSubcommand({
102
122
  tui.fatal(`Failed to read setup script: ${msg}`, ErrorCode.VALIDATION_FAILED);
103
123
  return;
104
124
  }
125
+ try {
126
+ const systemPrompt = await readSystemPrompt({
127
+ systemPrompt: opts?.systemPrompt,
128
+ systemPromptFile: opts?.systemPromptFile,
129
+ });
130
+ if (systemPrompt !== undefined) body.systemPrompt = systemPrompt;
131
+ const systemPromptMode = normalizeSystemPromptMode(opts?.systemPromptMode);
132
+ if (systemPromptMode !== undefined) body.systemPromptMode = systemPromptMode;
133
+ } catch (err) {
134
+ const msg = err instanceof Error ? err.message : String(err);
135
+ tui.fatal(`Failed to read system prompt: ${msg}`, ErrorCode.VALIDATION_FAILED);
136
+ return;
137
+ }
105
138
  if (opts?.enabledAgents) {
106
139
  body.enabledAgents = parseCommaList(opts.enabledAgents);
107
140
  }