@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,42 @@
1
+ /**
2
+ * Shared modality helpers.
3
+ *
4
+ * Centralises the modality-aware predicates and validators that used to
5
+ * live alongside individual command files. Keeping them here means a new
6
+ * modality (or a tweak to an existing one) only needs to land in one place
7
+ * and the call sites (`study run`, `iteration update`, ...) pick up the
8
+ * change for free.
9
+ */
10
+ export type Modality = "interactive" | "video" | "audio" | "text" | "image" | "document" | "chat";
11
+ /** True for the media-bucket modalities that share batch + content semantics. */
12
+ export declare function isMediaModality(modality: string | undefined): boolean;
13
+ /** True for chat modality. Wrapped as a helper so callers don't sprinkle string literals. */
14
+ export declare function isChatModality(modality: string | undefined): boolean;
15
+ /**
16
+ * Pattern F (Issue #9): an iteration without content is a footgun.
17
+ * `ish study generate` auto-creates an empty iteration "A" that becomes
18
+ * `study run`'s default target unless `--iteration` is explicit; agents
19
+ * silently dispatch against it. This predicate is the gate that lets
20
+ * study run refuse with a clear suggestion instead of silent failure.
21
+ */
22
+ export declare function iterationHasContent(details: unknown, modality: string | undefined): boolean;
23
+ /** The flag fragment a user should pass to populate content for a given modality. */
24
+ export declare function describeRequiredContentFlag(modality: string): string;
25
+ export type DetailsValidation = {
26
+ ok: true;
27
+ } | {
28
+ ok: false;
29
+ reason: string;
30
+ suggestion: string;
31
+ };
32
+ /**
33
+ * Detect modality/details mismatch on `iteration update --details-json`.
34
+ *
35
+ * Catches the silent-corruption footgun where an agent passes
36
+ * interactive-shape details (`{"url":"..."}`) to a chat iteration (or
37
+ * vice versa). Returns `{ ok: true }` for unknown modalities so we don't
38
+ * reject things we don't yet understand. Real shape validation (Zod,
39
+ * etc.) lives in the backend; this is a quick sanity check at the CLI
40
+ * boundary.
41
+ */
42
+ export declare function validateIterationDetails(modality: string | undefined, details: unknown): DetailsValidation;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Shared modality helpers.
3
+ *
4
+ * Centralises the modality-aware predicates and validators that used to
5
+ * live alongside individual command files. Keeping them here means a new
6
+ * modality (or a tweak to an existing one) only needs to land in one place
7
+ * and the call sites (`study run`, `iteration update`, ...) pick up the
8
+ * change for free.
9
+ */
10
+ import { MEDIA_MODALITIES } from "./types.js";
11
+ /** True for the media-bucket modalities that share batch + content semantics. */
12
+ export function isMediaModality(modality) {
13
+ return !!modality && MEDIA_MODALITIES.includes(modality);
14
+ }
15
+ /** True for chat modality. Wrapped as a helper so callers don't sprinkle string literals. */
16
+ export function isChatModality(modality) {
17
+ return modality === "chat";
18
+ }
19
+ /**
20
+ * Pattern F (Issue #9): an iteration without content is a footgun.
21
+ * `ish study generate` auto-creates an empty iteration "A" that becomes
22
+ * `study run`'s default target unless `--iteration` is explicit; agents
23
+ * silently dispatch against it. This predicate is the gate that lets
24
+ * study run refuse with a clear suggestion instead of silent failure.
25
+ */
26
+ export function iterationHasContent(details, modality) {
27
+ if (!details || typeof details !== "object")
28
+ return false;
29
+ const d = details;
30
+ if (modality === "text") {
31
+ return typeof d.content_text === "string" && d.content_text.length > 0;
32
+ }
33
+ if (modality === "video" || modality === "audio" || modality === "document") {
34
+ return typeof d.content_url === "string" && d.content_url.length > 0;
35
+ }
36
+ if (modality === "image") {
37
+ const urls = d.image_urls;
38
+ if (Array.isArray(urls))
39
+ return urls.length > 0;
40
+ if (typeof urls === "string")
41
+ return urls.length > 0;
42
+ return false;
43
+ }
44
+ if (modality === "chat") {
45
+ // A chat iteration carries either an inline endpoint config or a
46
+ // pointer to a saved chatbot_endpoints row.
47
+ const hasEndpoint = !!d.endpoint && typeof d.endpoint === "object";
48
+ const hasEndpointId = typeof d.chatbot_endpoint_id === "string"
49
+ && d.chatbot_endpoint_id.length > 0;
50
+ return hasEndpoint || hasEndpointId;
51
+ }
52
+ // interactive (default)
53
+ return typeof d.url === "string" && d.url.length > 0;
54
+ }
55
+ /** The flag fragment a user should pass to populate content for a given modality. */
56
+ export function describeRequiredContentFlag(modality) {
57
+ if (modality === "text")
58
+ return "--content-text <text-or-@file>";
59
+ if (modality === "video" || modality === "audio" || modality === "document") {
60
+ return "--content-url <url-or-file>";
61
+ }
62
+ if (modality === "image")
63
+ return "--image-urls <comma-separated>";
64
+ if (modality === "chat")
65
+ return "--endpoint <id> | --endpoint-config <file>";
66
+ return "--url <url>";
67
+ }
68
+ /**
69
+ * Detect modality/details mismatch on `iteration update --details-json`.
70
+ *
71
+ * Catches the silent-corruption footgun where an agent passes
72
+ * interactive-shape details (`{"url":"..."}`) to a chat iteration (or
73
+ * vice versa). Returns `{ ok: true }` for unknown modalities so we don't
74
+ * reject things we don't yet understand. Real shape validation (Zod,
75
+ * etc.) lives in the backend; this is a quick sanity check at the CLI
76
+ * boundary.
77
+ */
78
+ export function validateIterationDetails(modality, details) {
79
+ if (!details || typeof details !== "object" || Array.isArray(details)) {
80
+ return { ok: true };
81
+ }
82
+ const d = details;
83
+ const hasUrl = typeof d.url === "string" && d.url.length > 0;
84
+ const hasEndpointObj = !!d.endpoint && typeof d.endpoint === "object";
85
+ const hasEndpointId = typeof d.chatbot_endpoint_id === "string"
86
+ && d.chatbot_endpoint_id.length > 0;
87
+ const hasContentText = typeof d.content_text === "string" && d.content_text.length > 0;
88
+ const hasContentUrl = typeof d.content_url === "string" && d.content_url.length > 0;
89
+ const imageUrls = d.image_urls;
90
+ const hasImageUrls = Array.isArray(imageUrls)
91
+ ? imageUrls.length > 0
92
+ : typeof imageUrls === "string" && imageUrls.length > 0;
93
+ if (modality === "chat") {
94
+ if (hasEndpointObj || hasEndpointId)
95
+ return { ok: true };
96
+ if (hasUrl) {
97
+ return {
98
+ ok: false,
99
+ reason: "interactive-shape details on a chat iteration",
100
+ suggestion: "use --details-json with .endpoint or .chatbot_endpoint_id, or use `ish iteration update` with --endpoint <id>",
101
+ };
102
+ }
103
+ if (hasContentText || hasContentUrl || hasImageUrls) {
104
+ return {
105
+ ok: false,
106
+ reason: "media-shape details on a chat iteration",
107
+ suggestion: "chat iterations require .endpoint (object) or .chatbot_endpoint_id (string); pass --endpoint <id> or --endpoint-config <file>",
108
+ };
109
+ }
110
+ // No recognised content keys at all. Don't reject — agent might be
111
+ // updating ancillary fields we don't model here.
112
+ return { ok: true };
113
+ }
114
+ if (modality === "interactive") {
115
+ if (hasUrl)
116
+ return { ok: true };
117
+ if (hasEndpointObj || hasEndpointId) {
118
+ return {
119
+ ok: false,
120
+ reason: "chat-shape details on an interactive iteration",
121
+ suggestion: "use --details-json with .url for interactive iterations, or change the study modality to chat",
122
+ };
123
+ }
124
+ if (hasContentText || hasContentUrl || hasImageUrls) {
125
+ return {
126
+ ok: false,
127
+ reason: "media-shape details on an interactive iteration",
128
+ suggestion: "interactive iterations require .url; pass --url <url> or --details-json '{\"url\":\"...\"}'",
129
+ };
130
+ }
131
+ return { ok: true };
132
+ }
133
+ if (modality === "text") {
134
+ if (hasContentText)
135
+ return { ok: true };
136
+ if (hasUrl) {
137
+ return {
138
+ ok: false,
139
+ reason: "interactive-shape details on a text iteration",
140
+ suggestion: "text iterations require .content_text; pass --content-text <text-or-@file>",
141
+ };
142
+ }
143
+ if (hasEndpointObj || hasEndpointId) {
144
+ return {
145
+ ok: false,
146
+ reason: "chat-shape details on a text iteration",
147
+ suggestion: "text iterations require .content_text; pass --content-text <text-or-@file>",
148
+ };
149
+ }
150
+ return { ok: true };
151
+ }
152
+ if (modality === "video" || modality === "audio" || modality === "document") {
153
+ if (hasContentUrl)
154
+ return { ok: true };
155
+ if (hasUrl) {
156
+ return {
157
+ ok: false,
158
+ reason: `interactive-shape details on a ${modality} iteration`,
159
+ suggestion: `${modality} iterations require .content_url; pass --content-url <url-or-file>`,
160
+ };
161
+ }
162
+ if (hasEndpointObj || hasEndpointId) {
163
+ return {
164
+ ok: false,
165
+ reason: `chat-shape details on a ${modality} iteration`,
166
+ suggestion: `${modality} iterations require .content_url; pass --content-url <url-or-file>`,
167
+ };
168
+ }
169
+ return { ok: true };
170
+ }
171
+ if (modality === "image") {
172
+ if (hasImageUrls)
173
+ return { ok: true };
174
+ if (hasUrl) {
175
+ return {
176
+ ok: false,
177
+ reason: "interactive-shape details on an image iteration",
178
+ suggestion: "image iterations require .image_urls (array or comma-separated string); pass --image-urls <urls>",
179
+ };
180
+ }
181
+ if (hasEndpointObj || hasEndpointId) {
182
+ return {
183
+ ok: false,
184
+ reason: "chat-shape details on an image iteration",
185
+ suggestion: "image iterations require .image_urls; pass --image-urls <urls>",
186
+ };
187
+ }
188
+ return { ok: true };
189
+ }
190
+ // Unknown modality — don't reject, let the backend decide.
191
+ return { ok: true };
192
+ }
@@ -49,6 +49,30 @@ export declare function formatSiteAccessStatus(summary: import("./site-access.js
49
49
  export declare function formatStudyList(studies: Record<string, unknown>[], json: boolean): void;
50
50
  export declare function formatStudyDetail(study: Record<string, unknown>, json: boolean, options?: OutputOptions): void;
51
51
  export declare function formatStudyResults(study: Record<string, unknown>, json: boolean): void;
52
+ /**
53
+ * `study results --summary` projection. Drops interview_answers + per-tester
54
+ * interaction breakdowns; keeps headline counters, sentiment histogram, and a
55
+ * per-tester {alias, status, sentiment, comment} row. Useful for agents that
56
+ * need to branch on outcome without paying for the full envelope.
57
+ */
58
+ export declare function buildStudyResultsSummary(study: Record<string, unknown>): Record<string, unknown>;
59
+ /**
60
+ * `study results --transcript <tester_id>` projection. Mirrors the schema
61
+ * MCP's `get_chat_transcript` returns (`src/ish_mcp/projections.py:
62
+ * build_chat_transcript`) so callers see the same shape regardless of
63
+ * surface. Tester turns whose action carries no text (e.g. select_option)
64
+ * surface `text: null`; intent lives on `action_type` + `option_label`.
65
+ * Bot turns with a `bot_reply.failure` block surface `failure` and
66
+ * `text: null` and don't count toward `unique_bot_replies`.
67
+ */
68
+ export declare function buildChatTranscript(tester: Record<string, unknown>): Record<string, unknown>;
69
+ /**
70
+ * `study tester --summary` projection. Drops the action timeline; keeps the
71
+ * headline (alias, status, sentiment, comment, error_message). Useful for
72
+ * the common "did this tester finish, what did they say" check that's
73
+ * currently buried under the full interactions array.
74
+ */
75
+ export declare function buildTesterSummary(tester: Record<string, unknown>): Record<string, unknown>;
52
76
  export declare function formatIterationList(iterations: Record<string, unknown>[], json: boolean): void;
53
77
  export declare function formatTesterDetail(tester: Record<string, unknown>, json: boolean): void;
54
78
  export declare function formatTesterProfileList(profiles: unknown, json: boolean, limit?: number): void;
@@ -58,5 +82,22 @@ export declare function formatSimulationPoll(results: Record<string, unknown>[],
58
82
  export declare function formatAskList(asks: Record<string, unknown>[], json: boolean): void;
59
83
  export declare function formatAskDetail(ask: Record<string, unknown>, json: boolean): void;
60
84
  export declare function formatRoundDetail(round: Record<string, unknown>, json: boolean): void;
85
+ /**
86
+ * Derive a coarse confidence label from sample size + tied-ness + error mix.
87
+ *
88
+ * Rules (lowest wins):
89
+ * - low: n < 3 OR tied OR any errored response (we have visible failures)
90
+ * - medium: 3 <= n < 10 (small sample but clean)
91
+ * - high: n >= 10 AND no errored responses AND not tied
92
+ *
93
+ * Tuned for the typical 5-tester ask: a clean 5/5 lands at "medium" (you
94
+ * can probably trust the lean), 1/5 with no errors lands at "low" (you
95
+ * need more data), 5/5 with a tie lands at "low" (no winner to call).
96
+ */
97
+ export declare function deriveWinnerConfidence(args: {
98
+ n: number;
99
+ errored: number;
100
+ tied: boolean;
101
+ }): "low" | "medium" | "high";
61
102
  export declare function formatAskResults(ask: Record<string, unknown>, json: boolean, roundFilter?: number): void;
62
103
  export declare function formatConfigList(configs: Record<string, unknown>[], json: boolean): void;