@ishlabs/cli 0.12.2 → 0.14.0

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 (38) hide show
  1. package/dist/commands/chat-config.d.ts +23 -0
  2. package/dist/commands/chat-config.js +289 -0
  3. package/dist/commands/chat.js +26 -37
  4. package/dist/commands/iteration.js +219 -22
  5. package/dist/commands/profile.js +75 -9
  6. package/dist/commands/source.js +6 -4
  7. package/dist/commands/study-analyze.d.ts +41 -0
  8. package/dist/commands/study-analyze.js +187 -0
  9. package/dist/commands/study-run.js +359 -30
  10. package/dist/commands/study-screenshots.d.ts +20 -0
  11. package/dist/commands/study-screenshots.js +216 -0
  12. package/dist/commands/study.js +174 -9
  13. package/dist/commands/workspace.js +35 -2
  14. package/dist/lib/accessibility-profile.d.ts +12 -0
  15. package/dist/lib/accessibility-profile.js +136 -0
  16. package/dist/lib/alias-store.d.ts +1 -0
  17. package/dist/lib/alias-store.js +1 -0
  18. package/dist/lib/ask-questions.js +9 -0
  19. package/dist/lib/billing.d.ts +55 -0
  20. package/dist/lib/billing.js +77 -0
  21. package/dist/lib/command-helpers.d.ts +6 -0
  22. package/dist/lib/command-helpers.js +12 -0
  23. package/dist/lib/docs.js +1181 -38
  24. package/dist/lib/enums.d.ts +54 -0
  25. package/dist/lib/enums.js +100 -0
  26. package/dist/lib/local-sim/actions.d.ts +2 -1
  27. package/dist/lib/local-sim/actions.js +88 -13
  28. package/dist/lib/local-sim/loop.js +49 -19
  29. package/dist/lib/local-sim/tabs.d.ts +27 -0
  30. package/dist/lib/local-sim/tabs.js +157 -0
  31. package/dist/lib/local-sim/types.d.ts +15 -0
  32. package/dist/lib/modality.d.ts +70 -1
  33. package/dist/lib/modality.js +323 -17
  34. package/dist/lib/output.js +61 -4
  35. package/dist/lib/skill-content.js +397 -19
  36. package/dist/lib/types.d.ts +6 -1
  37. package/dist/lib/types.js +1 -1
  38. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ish chat config — Manage chatbot configurations (agent shape).
3
+ *
4
+ * A configuration captures *what the chatbot is* (model, system prompt,
5
+ * tools, sub-agents, custom {key, value} fields) — distinct from the
6
+ * *wire envelope* on a chatbot endpoint. Configurations live N-per-
7
+ * endpoint; chat iterations reference one and freeze a snapshot at
8
+ * creation time so historical comparisons stay reproducible.
9
+ *
10
+ * Four-verb surface mirroring `ish secret`: `list / set / get /
11
+ * delete`. `set` is the create-or-update upsert (composite write —
12
+ * mirrors MCP `chatbot_config_set` and the `chatbot_setup` precedent).
13
+ * `get --view iterations` surfaces the cross-study aggregation.
14
+ */
15
+ import type { Command } from "commander";
16
+ interface CustomField {
17
+ key: string;
18
+ value: string;
19
+ }
20
+ export declare function parseCustomField(raw: string): CustomField;
21
+ export declare function parseCustomFieldsFlag(values: string[] | undefined): CustomField[] | undefined;
22
+ export declare function attachChatConfigCommands(parent: Command): void;
23
+ export {};
@@ -0,0 +1,289 @@
1
+ /**
2
+ * ish chat config — Manage chatbot configurations (agent shape).
3
+ *
4
+ * A configuration captures *what the chatbot is* (model, system prompt,
5
+ * tools, sub-agents, custom {key, value} fields) — distinct from the
6
+ * *wire envelope* on a chatbot endpoint. Configurations live N-per-
7
+ * endpoint; chat iterations reference one and freeze a snapshot at
8
+ * creation time so historical comparisons stay reproducible.
9
+ *
10
+ * Four-verb surface mirroring `ish secret`: `list / set / get /
11
+ * delete`. `set` is the create-or-update upsert (composite write —
12
+ * mirrors MCP `chatbot_config_set` and the `chatbot_setup` precedent).
13
+ * `get --view iterations` surfaces the cross-study aggregation.
14
+ */
15
+ import * as fs from "node:fs";
16
+ import { ApiError } from "../lib/api-client.js";
17
+ import { ALIAS_PREFIX, tagAlias } from "../lib/alias-store.js";
18
+ import { collectRepeatable, confirmDestructive, resolveChatConfig, resolveChatEndpoint, withClient, } from "../lib/command-helpers.js";
19
+ import { output } from "../lib/output.js";
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+ export function parseCustomField(raw) {
24
+ const idx = raw.indexOf("=");
25
+ if (idx <= 0) {
26
+ throw new Error(`--custom-field must be \`key=value\` (got "${raw}").`);
27
+ }
28
+ return { key: raw.slice(0, idx).trim(), value: raw.slice(idx + 1) };
29
+ }
30
+ export function parseCustomFieldsFlag(values) {
31
+ if (values === undefined)
32
+ return undefined;
33
+ const seen = new Set();
34
+ const out = [];
35
+ for (const raw of values) {
36
+ const f = parseCustomField(raw);
37
+ if (!f.key)
38
+ throw new Error("--custom-field key cannot be empty.");
39
+ if (seen.has(f.key)) {
40
+ throw new Error(`--custom-field key "${f.key}" is repeated; keys must be unique.`);
41
+ }
42
+ seen.add(f.key);
43
+ out.push(f);
44
+ }
45
+ return out;
46
+ }
47
+ function parseJsonListFlag(file, label) {
48
+ if (file === undefined)
49
+ return undefined;
50
+ const content = file === "-" ? fs.readFileSync(0, "utf-8") : fs.readFileSync(file, "utf-8");
51
+ let parsed;
52
+ try {
53
+ parsed = JSON.parse(content);
54
+ }
55
+ catch (err) {
56
+ const m = err instanceof Error ? err.message : String(err);
57
+ throw new Error(`${label} is not valid JSON: ${m}`);
58
+ }
59
+ if (!Array.isArray(parsed))
60
+ throw new Error(`${label} must be a JSON array.`);
61
+ return parsed;
62
+ }
63
+ function readSystemPrompt(systemPrompt, systemPromptFile) {
64
+ if (systemPrompt !== undefined && systemPromptFile !== undefined) {
65
+ throw new Error("Pass at most one of --system-prompt or --system-prompt-file.");
66
+ }
67
+ if (systemPrompt !== undefined)
68
+ return systemPrompt;
69
+ if (systemPromptFile !== undefined) {
70
+ return systemPromptFile === "-"
71
+ ? fs.readFileSync(0, "utf-8")
72
+ : fs.readFileSync(systemPromptFile, "utf-8");
73
+ }
74
+ return undefined;
75
+ }
76
+ function pruneUndefined(obj) {
77
+ const out = {};
78
+ for (const [k, v] of Object.entries(obj)) {
79
+ if (v !== undefined)
80
+ out[k] = v;
81
+ }
82
+ return out;
83
+ }
84
+ // ---------------------------------------------------------------------------
85
+ // Commands
86
+ // ---------------------------------------------------------------------------
87
+ function attachList(parent) {
88
+ parent
89
+ .command("list")
90
+ .description("List configurations under a chatbot endpoint")
91
+ .option("--endpoint <id>", "Endpoint alias or UUID (defaults to active endpoint)")
92
+ .addHelpText("after", "\nExamples:\n $ ish chat config list\n $ ish chat config list --json | jq '.[] | {alias, name, isDefault, usageCount}'")
93
+ .action(async (opts, cmd) => {
94
+ await withClient(cmd, async (client, globals) => {
95
+ const epId = resolveChatEndpoint(undefined, opts.endpoint);
96
+ const rows = await client.get(`/chatbot-endpoints/${epId}/configurations`);
97
+ for (const row of rows) {
98
+ if (row.id)
99
+ tagAlias(ALIAS_PREFIX.chatConfig, row.id);
100
+ }
101
+ if (globals.json) {
102
+ output(rows, true);
103
+ return;
104
+ }
105
+ if (rows.length === 0) {
106
+ console.log("No configurations yet. Run `ish chat config set --name <name>` to author one.");
107
+ return;
108
+ }
109
+ for (const row of rows) {
110
+ const alias = row.id ? tagAlias(ALIAS_PREFIX.chatConfig, row.id) : "?";
111
+ const def = row.isDefault ? " [default]" : "";
112
+ const ver = row.versionLabel ? ` (${row.versionLabel})` : "";
113
+ const model = row.model ? ` model=${row.model}` : "";
114
+ const usage = ` used=${row.usageCount ?? 0}`;
115
+ console.log(`${alias} ${row.name}${ver}${def}${model}${usage}`);
116
+ }
117
+ });
118
+ });
119
+ }
120
+ function attachSet(parent) {
121
+ parent
122
+ .command("set")
123
+ .description("Create or update a chatbot configuration (composite upsert)")
124
+ .requiredOption("--name <name>", "Configuration name (unique per endpoint)")
125
+ .option("--endpoint <id>", "Endpoint alias or UUID (defaults to active endpoint; create only)")
126
+ .option("--config <id>", "Configuration alias or UUID — switches to update mode")
127
+ .option("--version-label <label>", "Optional version tag (e.g. v3, 1.2.0, git sha)")
128
+ .option("--description <text>", "Optional description")
129
+ .option("--model <id>", "Primary model identifier (e.g. claude-sonnet-4-6)")
130
+ .option("--system-prompt <text>", "System prompt text")
131
+ .option("--system-prompt-file <file>", 'Read system prompt from file (or "-" for stdin)')
132
+ .option("--tools-file <file>", 'JSON array of tools (or "-" for stdin)')
133
+ .option("--sub-agents-file <file>", 'JSON array of sub-agents (or "-" for stdin)')
134
+ .option("--custom-field <key=value>", "Custom field; repeat. Replaces all custom fields on update.", collectRepeatable)
135
+ .option("--default", "Mark as the endpoint's default configuration")
136
+ .addHelpText("after", `
137
+ Without --config, POSTs a new configuration under the endpoint. With
138
+ --config, PUTs an update. With --default, fires the set-default call
139
+ after the write so a single command leaves the endpoint with the
140
+ right default. Editing does NOT retroactively change historical
141
+ iterations — each iteration froze a snapshot at creation.
142
+
143
+ Examples:
144
+ $ ish chat config set --name v1-sonnet --model claude-sonnet-4-6 \\
145
+ --system-prompt-file ./prompt.txt --custom-field git_sha=abc --default
146
+ $ ish chat config set --config cc-abc --name v1.1-sonnet
147
+ $ cat tools.json | ish chat config set --name with-search --tools-file -`)
148
+ .action(async (opts, cmd) => {
149
+ await withClient(cmd, async (client, globals) => {
150
+ const cid = opts.config ? resolveChatConfig(undefined, opts.config) : null;
151
+ const body = pruneUndefined({
152
+ name: opts.name,
153
+ versionLabel: opts.versionLabel,
154
+ description: opts.description,
155
+ model: opts.model,
156
+ systemPrompt: readSystemPrompt(opts.systemPrompt, opts.systemPromptFile),
157
+ tools: parseJsonListFlag(opts.toolsFile, "--tools-file"),
158
+ subAgents: parseJsonListFlag(opts.subAgentsFile, "--sub-agents-file"),
159
+ customFields: parseCustomFieldsFlag(opts.customField),
160
+ });
161
+ let saved;
162
+ if (cid === null) {
163
+ const epId = resolveChatEndpoint(undefined, opts.endpoint);
164
+ if (opts.default)
165
+ body.isDefault = true;
166
+ saved = await client.post(`/chatbot-endpoints/${epId}/configurations`, body);
167
+ }
168
+ else {
169
+ saved = await client.put(`/chatbot-configurations/${cid}`, body);
170
+ if (opts.default) {
171
+ saved = await client.post(`/chatbot-configurations/${cid}/set-default`);
172
+ }
173
+ }
174
+ if (saved.id)
175
+ tagAlias(ALIAS_PREFIX.chatConfig, saved.id);
176
+ if (!globals.quiet) {
177
+ const action = cid === null ? "Created" : "Updated";
178
+ const tag = saved.isDefault ? " (default)" : "";
179
+ const alias = saved.id ? tagAlias(ALIAS_PREFIX.chatConfig, saved.id) : "?";
180
+ console.error(`${action} configuration ${alias}${tag}`);
181
+ }
182
+ output(saved, globals.json);
183
+ });
184
+ });
185
+ }
186
+ function attachGet(parent) {
187
+ parent
188
+ .command("get")
189
+ .description("Get a chatbot configuration (or its referencing iterations)")
190
+ .argument("[id]", "Configuration alias or UUID")
191
+ .option("--config <id>", "Configuration alias or UUID (alternative to positional)")
192
+ .option("--view <view>", "default | iterations. `iterations` returns the cross-study aggregation list.", "default")
193
+ .addHelpText("after", `
194
+ \`--view iterations\` returns every iteration referencing this
195
+ configuration across studies, joined to study metadata. Mirrors
196
+ \`study_get(view=...)\`.
197
+
198
+ Examples:
199
+ $ ish chat config get cc-abc
200
+ $ ish chat config get cc-abc --view iterations --json | jq 'group_by(.studyName) | length'`)
201
+ .action(async (id, opts, cmd) => {
202
+ await withClient(cmd, async (client, globals) => {
203
+ const cid = resolveChatConfig(id, opts.config);
204
+ if (opts.view === "iterations") {
205
+ const rows = await client.get(`/chatbot-configurations/${cid}/iterations`);
206
+ if (globals.json) {
207
+ output(rows, true);
208
+ return;
209
+ }
210
+ if (rows.length === 0) {
211
+ console.log("No iterations reference this configuration yet.");
212
+ return;
213
+ }
214
+ for (const r of rows) {
215
+ console.log(`${r.iterationLabel} ${r.studyName} — ${r.iterationName} (${r.createdAt})`);
216
+ }
217
+ return;
218
+ }
219
+ if (opts.view !== "default") {
220
+ throw new Error(`--view must be 'default' or 'iterations' (got "${opts.view}").`);
221
+ }
222
+ const row = await client.get(`/chatbot-configurations/${cid}`);
223
+ if (row.id)
224
+ tagAlias(ALIAS_PREFIX.chatConfig, row.id);
225
+ output(row, globals.json);
226
+ });
227
+ });
228
+ }
229
+ function attachDelete(parent) {
230
+ parent
231
+ .command("delete")
232
+ .description("Delete a chatbot configuration (rejected when iterations reference it)")
233
+ .argument("[id]", "Configuration alias or UUID")
234
+ .option("--config <id>", "Configuration alias or UUID (alternative to positional)")
235
+ .option("-y, --yes", "Skip confirmation prompt")
236
+ .addHelpText("after", `
237
+ Returns HTTP 409 (configuration_in_use) when iterations reference the
238
+ configuration. Snapshots remain readable on those iterations but the
239
+ live cross-study aggregation link breaks. Inspect \`usageCount\`
240
+ via \`ish chat config get <id>\` first.
241
+
242
+ Examples:
243
+ $ ish chat config delete cc-abc --yes`)
244
+ .action(async (id, opts, cmd) => {
245
+ await withClient(cmd, async (client, globals) => {
246
+ const cid = resolveChatConfig(id, opts.config);
247
+ await confirmDestructive(`Delete chat configuration ${tagAlias(ALIAS_PREFIX.chatConfig, cid)}? This cannot be undone.`, { yes: opts.yes, json: globals.json });
248
+ try {
249
+ await client.del(`/chatbot-configurations/${cid}`);
250
+ }
251
+ catch (err) {
252
+ if (err instanceof ApiError && err.status === 409) {
253
+ const detail = err.body;
254
+ const ids = detail?.detail?.iteration_ids ?? [];
255
+ const e = new Error(`Cannot delete: configuration is referenced by ${ids.length} iteration(s).`);
256
+ e.error_kind =
257
+ "configuration_in_use";
258
+ e.iteration_ids = ids;
259
+ throw e;
260
+ }
261
+ throw err;
262
+ }
263
+ output({
264
+ success: true,
265
+ deleted: true,
266
+ id: cid,
267
+ alias: tagAlias(ALIAS_PREFIX.chatConfig, cid),
268
+ }, globals.json, { writePath: true });
269
+ });
270
+ });
271
+ }
272
+ // ---------------------------------------------------------------------------
273
+ // Registration
274
+ // ---------------------------------------------------------------------------
275
+ export function attachChatConfigCommands(parent) {
276
+ const config = parent
277
+ .command("config")
278
+ .description("Manage chatbot configurations (agent shape: model, prompt, tools, sub-agents)")
279
+ .addHelpText("after", `
280
+ Configurations live under a chatbot endpoint and are referenced by chat
281
+ iterations. Each iteration freezes a snapshot at creation so editing a
282
+ configuration does NOT retroactively change historical iterations. Use
283
+ \`set --config <id>\` to update in place, or omit \`--config\` to create
284
+ a new configuration. Aliases use the \`cc-\` prefix.`);
285
+ attachList(config);
286
+ attachSet(config);
287
+ attachGet(config);
288
+ attachDelete(config);
289
+ }
@@ -18,6 +18,7 @@ import { ApiError } from "../lib/api-client.js";
18
18
  import { output } from "../lib/output.js";
19
19
  import { formatChatEndpointList, formatChatEndpointDetail, envelopeFromRow, } from "../lib/chat-endpoint-formatters.js";
20
20
  import { getChatEndpointTemplate, TEMPLATE_NAMES, } from "../lib/chat-endpoint-templates.js";
21
+ import { attachChatConfigCommands } from "./chat-config.js";
21
22
  // ---------------------------------------------------------------------------
22
23
  // Helpers
23
24
  // ---------------------------------------------------------------------------
@@ -57,25 +58,6 @@ function urlLooksLocal(url) {
57
58
  return false;
58
59
  }
59
60
  }
60
- function inferredToConfig(inferred) {
61
- const cfg = {
62
- transport: inferred.transport,
63
- outgoing: {
64
- url: inferred.outgoing.url ?? undefined,
65
- method: inferred.outgoing.method,
66
- headers: inferred.outgoing.headers ?? {},
67
- bodyTemplate: inferred.outgoing.bodyTemplate ?? {},
68
- mode: inferred.outgoing.mode,
69
- roleAliases: inferred.outgoing.roleAliases ?? {},
70
- },
71
- incoming: inferred.incoming,
72
- asyncPoll: inferred.asyncPoll ?? null,
73
- };
74
- if (inferred.streaming) {
75
- cfg.streaming = inferred.streaming;
76
- }
77
- return cfg;
78
- }
79
61
  async function tunnelGuard(client) {
80
62
  try {
81
63
  await client.get("/connect/active");
@@ -355,12 +337,12 @@ endpoint, apply the override, and PUT the merged result. Field flags win over
355
337
  });
356
338
  }
357
339
  // ---------------------------------------------------------------------------
358
- // init — auto-detect-shape onboarding
340
+ // init — test-and-map onboarding (auto-detect via /chat/test-and-map)
359
341
  // ---------------------------------------------------------------------------
360
342
  function attachChatEndpointInit(parent) {
361
343
  parent
362
344
  .command("init")
363
- .description("Author an endpoint from a curl/JSON sample via auto-detect-shape, or from a known-good template")
345
+ .description("Author an endpoint from a curl/JSON sample via test-and-map, or from a known-good template")
364
346
  .option("--from-curl <file>", 'Path to a curl example file (or "-" for stdin)')
365
347
  .option("--from-json <file>", 'Path to a JSON request/response sample (or "-" for stdin)')
366
348
  .option("--template <name>", `Start from a known-good template (one of: ${TEMPLATE_NAMES.join(", ")})`)
@@ -446,22 +428,27 @@ Examples:
446
428
  output(result, globals.json, { writePath: true });
447
429
  return;
448
430
  }
449
- // Auto-detect path (curl or JSON paste).
431
+ // Auto-detect path (curl or JSON paste). Drives the same
432
+ // /test-and-map endpoint the editor uses, so the CLI and editor
433
+ // stay aligned on inference behaviour and error vocabulary.
450
434
  const path = (opts.fromCurl ?? opts.fromJson);
451
435
  const paste = await readFileOrStdin(path);
452
- const inferRes = await client.post(`/products/${ws}/chat/auto-detect-shape`, { paste }, { timeout: 120_000 });
453
- if (!inferRes.ok) {
436
+ const mapRes = await client.post(`/products/${ws}/chat/test-and-map`, { documentation: paste }, { timeout: 120_000 });
437
+ if (mapRes.kind === "failure") {
454
438
  // Surface as a structured failure envelope on stdout AND throw so
455
439
  // the wrapper sets a non-zero exit. The thrown Error carries the
456
- // shape's error_kind for the agent to branch on.
457
- const err = new Error(inferRes.errorMessage ?? "auto-detect-shape failed.");
458
- err.error_kind = inferRes.errorKind;
440
+ // backend's error_kind for the agent to branch on.
441
+ const err = new Error(mapRes.errorMessage ?? "test-and-map failed.");
442
+ err.error_kind = mapRes.errorKind;
459
443
  throw err;
460
444
  }
461
- const inferred = inferRes.inferred;
462
- const config = inferredToConfig(inferred);
463
- const inferredUrl = inferred.outgoing.url ?? null;
464
- const detectedTunnel = urlLooksLocal(inferredUrl);
445
+ const config = mapRes.inferredConfig;
446
+ const inferredUrl = (typeof config.outgoing?.url === "string" && config.outgoing.url
447
+ ? config.outgoing.url
448
+ : null);
449
+ // Trust the backend's tunnel detection when present; fall back to
450
+ // the local heuristic only if the envelope didn't populate it.
451
+ const detectedTunnel = mapRes.tunnelBackedDetected ?? urlLooksLocal(inferredUrl);
465
452
  let tunnelBacked;
466
453
  if (opts.tunnelBacked === true)
467
454
  tunnelBacked = true;
@@ -474,8 +461,9 @@ Examples:
474
461
  if (!inferredUrl) {
475
462
  warnings.push("Inferred shape has no URL; set --url before testing.");
476
463
  }
477
- if (inferred.confidence !== "high") {
478
- warnings.push(`Auto-detect confidence: ${inferred.confidence} verify the shape before running.`);
464
+ const confidence = mapRes.confidence ?? null;
465
+ if (confidence && confidence !== "high") {
466
+ warnings.push(`Auto-detect confidence: ${confidence} — verify the shape before running.`);
479
467
  }
480
468
  // Decide whether to save. --no-save short-circuits; otherwise save when
481
469
  // a name is available (--name wins; else fall back to the inferred
@@ -508,8 +496,8 @@ Examples:
508
496
  }
509
497
  }
510
498
  }
511
- const missingSignals = Array.isArray(inferred.missingSignals)
512
- ? inferred.missingSignals
499
+ const missingSignals = Array.isArray(mapRes.missingSignals)
500
+ ? mapRes.missingSignals
513
501
  : [];
514
502
  const result = {
515
503
  success: true,
@@ -519,8 +507,8 @@ Examples:
519
507
  config,
520
508
  tunnel_backed: tunnelBacked,
521
509
  tunnel_backed_detected: detectedTunnel,
522
- confidence: inferred.confidence,
523
- explanation: inferred.explanation,
510
+ confidence,
511
+ explanation: mapRes.explanation ?? "",
524
512
  missingSignals,
525
513
  warnings,
526
514
  };
@@ -714,6 +702,7 @@ mirrors that editing model.`);
714
702
  attachChatEndpointInit(endpoint);
715
703
  attachChatEndpointTest(endpoint);
716
704
  attachChatEndpointMap(endpoint);
705
+ attachChatConfigCommands(chat);
717
706
  }
718
707
  // Re-exported for tests / external integration if needed.
719
708
  export { envelopeFromRow };