@ishlabs/cli 0.9.0 → 0.11.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 (39) hide show
  1. package/README.md +54 -5
  2. package/dist/commands/ask.d.ts +12 -0
  3. package/dist/commands/ask.js +127 -2
  4. package/dist/commands/chat.d.ts +17 -0
  5. package/dist/commands/chat.js +655 -0
  6. package/dist/commands/iteration.js +134 -14
  7. package/dist/commands/secret.d.ts +20 -0
  8. package/dist/commands/secret.js +246 -0
  9. package/dist/commands/study-run.d.ts +38 -0
  10. package/dist/commands/study-run.js +199 -80
  11. package/dist/commands/study-tester.js +17 -2
  12. package/dist/commands/study.js +309 -37
  13. package/dist/commands/workspace.js +81 -0
  14. package/dist/config.d.ts +3 -0
  15. package/dist/connect.d.ts +3 -0
  16. package/dist/connect.js +346 -22
  17. package/dist/index.js +64 -6
  18. package/dist/lib/alias-hydrate.d.ts +42 -0
  19. package/dist/lib/alias-hydrate.js +175 -0
  20. package/dist/lib/alias-store.d.ts +1 -0
  21. package/dist/lib/alias-store.js +28 -1
  22. package/dist/lib/auth.js +4 -2
  23. package/dist/lib/chat-endpoint-formatters.d.ts +74 -0
  24. package/dist/lib/chat-endpoint-formatters.js +154 -0
  25. package/dist/lib/chat-endpoint-templates.d.ts +35 -0
  26. package/dist/lib/chat-endpoint-templates.js +210 -0
  27. package/dist/lib/command-helpers.d.ts +18 -0
  28. package/dist/lib/command-helpers.js +105 -3
  29. package/dist/lib/docs.js +641 -17
  30. package/dist/lib/modality.d.ts +42 -0
  31. package/dist/lib/modality.js +192 -0
  32. package/dist/lib/output.d.ts +41 -0
  33. package/dist/lib/output.js +453 -19
  34. package/dist/lib/paths.d.ts +1 -0
  35. package/dist/lib/paths.js +3 -0
  36. package/dist/lib/skill-content.d.ts +18 -0
  37. package/dist/lib/skill-content.js +223 -12
  38. package/dist/lib/types.d.ts +15 -0
  39. package/package.json +2 -2
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Hand-curated `ChatbotEndpointConfig` templates for `ish chat endpoint init
3
+ * --template <name>`.
4
+ *
5
+ * Each template is a known-good wire shape an agent can drop straight into
6
+ * `create_chatbot_endpoint` — derived from public docs for the named
7
+ * provider. Auth / API key values are placeholder secret refs
8
+ * (`{{secret:NAME}}`); the agent stores the real value via
9
+ * `ish secret set` before testing.
10
+ *
11
+ * Templates are intentionally minimal:
12
+ * - `transport`, `outgoing`, `incoming` (slots-only).
13
+ * - No `retry` block — defaults are fine.
14
+ * - No `asyncPoll` — none of the listed providers use it.
15
+ * - `streaming` only when the provider's default flow is SSE-shaped
16
+ * (we currently keep all templates in `sync` mode; agents who want
17
+ * streaming flip the transport themselves after init).
18
+ *
19
+ * Slots / references are populated when the provider documents a stable
20
+ * structured-output container (e.g. Bot Framework's `suggestedActions`,
21
+ * Watson Assistant's `output.generic[]`). Where the provider is pure text
22
+ * (vanilla OpenAI / Anthropic chat-completions), the lists stay empty —
23
+ * the runtime auto-classifier handles any structured content the bot
24
+ * decides to emit per turn.
25
+ */
26
+ const OPENAI = {
27
+ name: "openai",
28
+ description: "OpenAI chat-completions wire shape. Stateless by default; ish ships the rolled-up history per turn. Drop in any OpenAI-compatible bot (Groq / Together / vLLM / OpenRouter / LiteLLM).",
29
+ config: {
30
+ transport: "sync",
31
+ outgoing: {
32
+ url: "https://api.openai.com/v1/chat/completions",
33
+ method: "POST",
34
+ headers: {
35
+ "content-type": "application/json",
36
+ Authorization: "Bearer {{secret:OPENAI_API_KEY}}",
37
+ },
38
+ bodyTemplate: {
39
+ model: "gpt-4o-mini",
40
+ messages: "{{history_with_current}}",
41
+ },
42
+ mode: "stateless",
43
+ roleAliases: {},
44
+ },
45
+ incoming: {
46
+ messagePath: "choices[0].message.content",
47
+ toolCallsPath: "choices[0].message.tool_calls",
48
+ tokenUsagePath: "usage",
49
+ slots: [],
50
+ references: [],
51
+ },
52
+ },
53
+ };
54
+ const ANTHROPIC = {
55
+ name: "anthropic",
56
+ description: "Anthropic Messages API. Stateless: each request carries the full history and a fresh system prompt. Default model is the latest Sonnet.",
57
+ config: {
58
+ transport: "sync",
59
+ outgoing: {
60
+ url: "https://api.anthropic.com/v1/messages",
61
+ method: "POST",
62
+ headers: {
63
+ "content-type": "application/json",
64
+ "anthropic-version": "2023-06-01",
65
+ "x-api-key": "{{secret:ANTHROPIC_API_KEY}}",
66
+ },
67
+ bodyTemplate: {
68
+ model: "claude-sonnet-4-20250514",
69
+ max_tokens: 1024,
70
+ messages: "{{history_with_current}}",
71
+ },
72
+ mode: "stateless",
73
+ roleAliases: {},
74
+ },
75
+ incoming: {
76
+ messagePath: "content[0].text",
77
+ tokenUsagePath: "usage",
78
+ slots: [],
79
+ references: [],
80
+ },
81
+ },
82
+ };
83
+ const VOICEFLOW = {
84
+ name: "voiceflow",
85
+ description: "Voiceflow Dialog Manager API. Stateful — the URL embeds the user/session id, and Voiceflow returns a list of trace objects. Body is `{action: {type: 'text', payload: <text>}}` per turn.",
86
+ config: {
87
+ transport: "sync",
88
+ outgoing: {
89
+ url: "https://general-runtime.voiceflow.com/state/user/{{conversation_id}}/interact",
90
+ method: "POST",
91
+ headers: {
92
+ "content-type": "application/json",
93
+ Authorization: "{{secret:VOICEFLOW_API_KEY}}",
94
+ versionID: "production",
95
+ },
96
+ bodyTemplate: {
97
+ action: {
98
+ type: "text",
99
+ payload: "{{action.text}}",
100
+ },
101
+ },
102
+ mode: "stateful",
103
+ roleAliases: {},
104
+ },
105
+ incoming: {
106
+ // Voiceflow's runtime returns an array of trace objects; the bot's
107
+ // textual reply lives in the first `speak` / `text` trace's payload.
108
+ // Agents typically tighten this path after a probe turn.
109
+ messagePath: "[0].payload.message",
110
+ slots: [
111
+ {
112
+ containerPath: "[0].payload.choices",
113
+ kind: "alternatives",
114
+ labelPath: "name",
115
+ idPath: "intent",
116
+ },
117
+ ],
118
+ references: [],
119
+ },
120
+ },
121
+ };
122
+ const DIALOGFLOW_CX = {
123
+ name: "dialogflow-cx",
124
+ description: "Google Dialogflow CX `detectIntent`. Stateful: the URL embeds the agent + session id and the response carries the next `currentPage` plus a list of fulfillment messages.",
125
+ config: {
126
+ transport: "sync",
127
+ outgoing: {
128
+ url: "https://dialogflow.googleapis.com/v3/projects/{{secret:GCP_PROJECT}}/locations/global/agents/{{secret:DIALOGFLOW_AGENT_ID}}/sessions/{{conversation_id}}:detectIntent",
129
+ method: "POST",
130
+ headers: {
131
+ "content-type": "application/json",
132
+ Authorization: "Bearer {{secret:GOOGLE_ACCESS_TOKEN}}",
133
+ },
134
+ bodyTemplate: {
135
+ queryInput: {
136
+ text: { text: "{{action.text}}" },
137
+ languageCode: "en",
138
+ },
139
+ },
140
+ mode: "stateful",
141
+ roleAliases: {},
142
+ },
143
+ incoming: {
144
+ messagePath: "queryResult.responseMessages[0].text.text[0]",
145
+ conversationIdPath: "responseId",
146
+ slots: [
147
+ {
148
+ containerPath: "queryResult.responseMessages",
149
+ kind: null,
150
+ },
151
+ ],
152
+ references: [],
153
+ },
154
+ },
155
+ };
156
+ const BOTFRAMEWORK = {
157
+ name: "botframework",
158
+ description: "Microsoft Bot Framework Direct Line `conversations/{id}/activities`. Stateful via the conversation id; activities[] carry the bot's reply plus suggestedActions for choice slots.",
159
+ config: {
160
+ transport: "sync",
161
+ outgoing: {
162
+ url: "https://directline.botframework.com/v3/directline/conversations/{{conversation_id}}/activities",
163
+ method: "POST",
164
+ headers: {
165
+ "content-type": "application/json",
166
+ Authorization: "Bearer {{secret:DIRECTLINE_SECRET}}",
167
+ },
168
+ bodyTemplate: {
169
+ type: "message",
170
+ from: { id: "{{tester.name}}" },
171
+ text: "{{action.text}}",
172
+ },
173
+ mode: "stateful",
174
+ roleAliases: {},
175
+ },
176
+ incoming: {
177
+ messagePath: "activities[0].text",
178
+ conversationIdPath: "activities[0].conversation.id",
179
+ slots: [
180
+ {
181
+ containerPath: "activities[0].suggestedActions.actions",
182
+ kind: "alternatives",
183
+ labelPath: "title",
184
+ idPath: "value",
185
+ },
186
+ ],
187
+ references: [
188
+ {
189
+ containerPath: "activities[0].attachments",
190
+ labelPath: "name",
191
+ urlPath: "contentUrl",
192
+ },
193
+ ],
194
+ },
195
+ },
196
+ };
197
+ const TEMPLATES = {
198
+ openai: OPENAI,
199
+ anthropic: ANTHROPIC,
200
+ voiceflow: VOICEFLOW,
201
+ "dialogflow-cx": DIALOGFLOW_CX,
202
+ botframework: BOTFRAMEWORK,
203
+ };
204
+ export const TEMPLATE_NAMES = Object.keys(TEMPLATES);
205
+ export function getChatEndpointTemplate(name) {
206
+ return TEMPLATES[name];
207
+ }
208
+ export function listChatEndpointTemplates() {
209
+ return TEMPLATE_NAMES.map((n) => TEMPLATES[n]);
210
+ }
@@ -123,6 +123,12 @@ export declare function confirmDestructive(prompt: string, opts: {
123
123
  export declare function resolveWorkspace(explicit?: string): string;
124
124
  export declare function resolveStudy(explicit?: string): string;
125
125
  export declare function resolveAsk(explicit?: string): string;
126
+ /**
127
+ * Resolve a chat endpoint id from (in order): the positional argument, the
128
+ * `--endpoint <id>` flag, the `ISH_CHAT_ENDPOINT` env var, or the active
129
+ * endpoint persisted by `ish chat endpoint use`. Throws when none are set.
130
+ */
131
+ export declare function resolveChatEndpoint(positional?: string, flag?: string): string;
126
132
  /** Commander option-collector for repeatable flags (e.g. `--variant text:"..."` repeated). */
127
133
  export declare function collectRepeatable(value: string, prev?: string[]): string[];
128
134
  /**
@@ -150,4 +156,16 @@ export declare function parseWaitTimeout(raw: string | undefined, defaultMs?: nu
150
156
  * body. Resolvers (`resolveWorkspace`, `resolveAudienceProfileIds`) ignore
151
157
  * unused values.
152
158
  */
159
+ /**
160
+ * Read a `--*-config <file>` style flag value, treating "-" as "read from
161
+ * stdin" and any other value as a file path on disk. Trailing newlines on
162
+ * stdin input are stripped so the resulting string parses cleanly as JSON.
163
+ *
164
+ * Throws when "-" is passed but stdin is a TTY (no upstream pipe).
165
+ *
166
+ * Mirrors the readSecretFlag pattern in src/commands/workspace.ts; extracted
167
+ * so every `--<x>-config <file>` flag across commands shares one
168
+ * implementation.
169
+ */
170
+ export declare function readFileOrStdin(path: string): Promise<string>;
153
171
  export declare function injectGlobalWorkspaceOption(program: Command): void;
@@ -348,6 +348,20 @@ export function exitCodeFromError(err) {
348
348
  // Client-side validation failures
349
349
  if (err.name === "ValidationError" || /^invalid |^cannot read |is empty:|--\w[\w-]* must be|pick an audience|use either /i.test(err.message))
350
350
  return 2;
351
+ // Errors that pre-declare their own retryability / error_code take
352
+ // precedence — WaitTimeoutError sets `error_code: "wait_timeout"`
353
+ // and `retryable: true`, so callers can branch on exit 5 (transient)
354
+ // distinct from the api-client's generic 408/timeout family.
355
+ const tagged = err;
356
+ if (tagged.error_code === "wait_timeout")
357
+ return 5;
358
+ if (typeof tagged.retryable === "boolean" && tagged.retryable)
359
+ return 5;
360
+ // Structured error_kind on the Error object (set by chat endpoint test/init,
361
+ // simulation routes, etc.). TunnelInactive is the canonical transient one.
362
+ const kind = err.error_kind;
363
+ if (typeof kind === "string" && kind === "TunnelInactive")
364
+ return 5;
351
365
  }
352
366
  return 1;
353
367
  }
@@ -439,6 +453,18 @@ export function readJsonFileOrStdin(filePath) {
439
453
  process.stdin.on("error", reject);
440
454
  });
441
455
  }
456
+ /**
457
+ * Build the suggested re-invocation example by taking the live argv and
458
+ * appending `--yes` if it isn't already there. Strips the `node` /
459
+ * `dist/index.js` prefix so the example reads as a normal `ish` command.
460
+ */
461
+ function buildConfirmationExample() {
462
+ const argv = process.argv.slice(2);
463
+ const args = argv.includes("--yes") || argv.includes("-y")
464
+ ? argv
465
+ : [...argv, "--yes"];
466
+ return ["ish", ...args].join(" ");
467
+ }
442
468
  /**
443
469
  * Prompt for confirmation of a destructive action, or short-circuit when
444
470
  * `--yes` is set. In `--json` mode without `--yes` we refuse with a usage
@@ -451,11 +477,15 @@ export async function confirmDestructive(prompt, opts) {
451
477
  if (opts.json) {
452
478
  const err = new Error(`--yes is required for destructive actions in --json mode. Refusing to proceed without explicit confirmation.`);
453
479
  err.name = "ValidationError";
480
+ err.error_kind = "ConfirmationRequired";
481
+ err.example = buildConfirmationExample();
454
482
  throw err;
455
483
  }
456
484
  if (!process.stdin.isTTY) {
457
485
  const err = new Error(`--yes is required for destructive actions when stdin is not a TTY. Refusing to proceed without explicit confirmation.`);
458
486
  err.name = "ValidationError";
487
+ err.error_kind = "ConfirmationRequired";
488
+ err.example = buildConfirmationExample();
459
489
  throw err;
460
490
  }
461
491
  process.stderr.write(`${prompt} [y/N] `);
@@ -486,6 +516,23 @@ export async function confirmDestructive(prompt, opts) {
486
516
  throw err;
487
517
  }
488
518
  }
519
+ /**
520
+ * Construct a structured "no active <thing>" Error so the JSON envelope from
521
+ * `outputError` carries `error_code` + `suggestions` rather than a generic
522
+ * `client_error`. Pattern A (Sprint 2): when the active study/workspace
523
+ * evaporates between commands, agents need to branch on the error code, not
524
+ * scrape prose. Without this, downstream modality validation in
525
+ * `iteration create` would surface a misleading "Image iterations require
526
+ * --image-urls" message instead of the real "no active study" cause.
527
+ */
528
+ function noActiveContextError(message, errorCode, suggestions) {
529
+ const err = new Error(message);
530
+ err.name = "NoActiveContextError";
531
+ err.error_code = errorCode;
532
+ err.retryable = false;
533
+ err.suggestions = suggestions;
534
+ return err;
535
+ }
489
536
  export function resolveWorkspace(explicit) {
490
537
  if (explicit)
491
538
  return resolveId(explicit);
@@ -500,7 +547,10 @@ export function resolveWorkspace(explicit) {
500
547
  const config = loadConfig();
501
548
  if (config.workspace)
502
549
  return config.workspace;
503
- throw new Error('No workspace set. Use `ish workspace use <alias>` or pass --workspace.');
550
+ throw noActiveContextError('No active workspace. Run `ish workspace use <workspace-id>` or pass --workspace <id>.', "no_active_workspace", [
551
+ "ish workspace list",
552
+ "ish workspace use <workspace-id>",
553
+ ]);
504
554
  }
505
555
  export function resolveStudy(explicit) {
506
556
  if (explicit)
@@ -511,7 +561,10 @@ export function resolveStudy(explicit) {
511
561
  const config = loadConfig();
512
562
  if (config.study)
513
563
  return config.study;
514
- throw new Error('No study set. Use `ish study use <alias>` or pass --study.');
564
+ throw noActiveContextError('No active study. Run `ish study use <study-id>` or pass --study <id>.', "no_active_study", [
565
+ "ish study use <study-id>",
566
+ "ish iteration create --study <study-id> ...",
567
+ ]);
515
568
  }
516
569
  export function resolveAsk(explicit) {
517
570
  if (explicit)
@@ -522,7 +575,28 @@ export function resolveAsk(explicit) {
522
575
  const config = loadConfig();
523
576
  if (config.ask)
524
577
  return config.ask;
525
- throw new Error('No ask set. Use `ish ask use <alias>` or pass the ask ID as an argument.');
578
+ throw noActiveContextError('No active ask. Run `ish ask use <ask-id>` or pass the ask ID as an argument.', "no_active_ask", [
579
+ "ish ask list",
580
+ "ish ask use <ask-id>",
581
+ ]);
582
+ }
583
+ /**
584
+ * Resolve a chat endpoint id from (in order): the positional argument, the
585
+ * `--endpoint <id>` flag, the `ISH_CHAT_ENDPOINT` env var, or the active
586
+ * endpoint persisted by `ish chat endpoint use`. Throws when none are set.
587
+ */
588
+ export function resolveChatEndpoint(positional, flag) {
589
+ if (positional)
590
+ return resolveId(positional);
591
+ if (flag)
592
+ return resolveId(flag);
593
+ const env = process.env.ISH_CHAT_ENDPOINT;
594
+ if (env)
595
+ return resolveId(env);
596
+ const config = loadConfig();
597
+ if (config.chat_endpoint)
598
+ return config.chat_endpoint;
599
+ throw new Error('No chat endpoint set. Use `ish chat endpoint use <id>`, pass the endpoint id, or set --endpoint.');
526
600
  }
527
601
  /** Commander option-collector for repeatable flags (e.g. `--variant text:"..."` repeated). */
528
602
  export function collectRepeatable(value, prev = []) {
@@ -571,6 +645,34 @@ const WORKSPACE_SCOPED_GROUPS = new Set([
571
645
  * body. Resolvers (`resolveWorkspace`, `resolveAudienceProfileIds`) ignore
572
646
  * unused values.
573
647
  */
648
+ /**
649
+ * Read a `--*-config <file>` style flag value, treating "-" as "read from
650
+ * stdin" and any other value as a file path on disk. Trailing newlines on
651
+ * stdin input are stripped so the resulting string parses cleanly as JSON.
652
+ *
653
+ * Throws when "-" is passed but stdin is a TTY (no upstream pipe).
654
+ *
655
+ * Mirrors the readSecretFlag pattern in src/commands/workspace.ts; extracted
656
+ * so every `--<x>-config <file>` flag across commands shares one
657
+ * implementation.
658
+ */
659
+ export async function readFileOrStdin(path) {
660
+ if (path === "-") {
661
+ if (process.stdin.isTTY) {
662
+ throw new Error('Use "-" only when piping the value on stdin.');
663
+ }
664
+ return await new Promise((resolve, reject) => {
665
+ let data = "";
666
+ process.stdin.setEncoding("utf-8");
667
+ process.stdin.on("data", (chunk) => {
668
+ data += chunk;
669
+ });
670
+ process.stdin.on("end", () => resolve(data.replace(/\r?\n$/, "")));
671
+ process.stdin.on("error", reject);
672
+ });
673
+ }
674
+ return fs.readFileSync(path, "utf-8");
675
+ }
574
676
  export function injectGlobalWorkspaceOption(program) {
575
677
  const walk = (cmd) => {
576
678
  if (cmd.commands.length === 0) {