@ishlabs/cli 0.17.6 → 0.18.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 (64) hide show
  1. package/README.md +54 -54
  2. package/dist/commands/ask.d.ts +4 -4
  3. package/dist/commands/ask.js +66 -66
  4. package/dist/commands/chat.js +10 -10
  5. package/dist/commands/config.js +1 -1
  6. package/dist/commands/docs.js +1 -1
  7. package/dist/commands/iteration.js +57 -57
  8. package/dist/commands/mcp.d.ts +23 -0
  9. package/dist/commands/mcp.js +676 -0
  10. package/dist/commands/person.d.ts +5 -0
  11. package/dist/commands/{profile.js → person.js} +197 -162
  12. package/dist/commands/source.d.ts +6 -2
  13. package/dist/commands/source.js +35 -30
  14. package/dist/commands/study-analyze.d.ts +1 -1
  15. package/dist/commands/study-analyze.js +3 -3
  16. package/dist/commands/study-participant.d.ts +8 -0
  17. package/dist/commands/{study-tester.js → study-participant.js} +50 -50
  18. package/dist/commands/study-run.d.ts +6 -6
  19. package/dist/commands/study-run.js +295 -271
  20. package/dist/commands/study.js +89 -66
  21. package/dist/commands/workspace.js +13 -13
  22. package/dist/connect.js +5 -5
  23. package/dist/index.js +6 -4
  24. package/dist/lib/accessibility-profile.d.ts +1 -1
  25. package/dist/lib/accessibility-profile.js +1 -1
  26. package/dist/lib/alias-hydrate.js +4 -4
  27. package/dist/lib/alias-store.d.ts +5 -5
  28. package/dist/lib/alias-store.js +8 -8
  29. package/dist/lib/api-client.d.ts +1 -1
  30. package/dist/lib/api-client.js +1 -1
  31. package/dist/lib/billing.d.ts +11 -11
  32. package/dist/lib/billing.js +16 -16
  33. package/dist/lib/chat-endpoint-templates.js +1 -1
  34. package/dist/lib/command-helpers.d.ts +18 -18
  35. package/dist/lib/command-helpers.js +83 -53
  36. package/dist/lib/docs.js +560 -386
  37. package/dist/lib/enums.d.ts +2 -2
  38. package/dist/lib/enums.js +2 -2
  39. package/dist/lib/local-sim/browser.d.ts +1 -1
  40. package/dist/lib/local-sim/browser.js +1 -1
  41. package/dist/lib/local-sim/debug-report.d.ts +2 -2
  42. package/dist/lib/local-sim/debug-report.js +3 -3
  43. package/dist/lib/local-sim/loop.d.ts +5 -5
  44. package/dist/lib/local-sim/loop.js +38 -38
  45. package/dist/lib/local-sim/types.d.ts +12 -12
  46. package/dist/lib/mcp-clients.d.ts +51 -0
  47. package/dist/lib/mcp-clients.js +175 -0
  48. package/dist/lib/modality.d.ts +10 -10
  49. package/dist/lib/modality.js +46 -46
  50. package/dist/lib/observability.d.ts +11 -0
  51. package/dist/lib/observability.js +16 -3
  52. package/dist/lib/output.d.ts +13 -12
  53. package/dist/lib/output.js +244 -184
  54. package/dist/lib/profile-sources.d.ts +64 -16
  55. package/dist/lib/profile-sources.js +91 -30
  56. package/dist/lib/skill-content.js +215 -168
  57. package/dist/lib/study-events.d.ts +3 -3
  58. package/dist/lib/study-events.js +1 -1
  59. package/dist/lib/study-inputs.d.ts +11 -1
  60. package/dist/lib/study-inputs.js +68 -17
  61. package/dist/lib/types.d.ts +105 -34
  62. package/package.json +1 -1
  63. package/dist/commands/profile.d.ts +0 -5
  64. package/dist/commands/study-tester.d.ts +0 -8
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * SSE consumer for the backend's per-study event stream.
3
3
  *
4
- * Used by `study run --wait` to wake up the poll loop as soon as a tester
4
+ * Used by `study run --wait` to wake up the poll loop as soon as a participant
5
5
  * status / interaction event arrives, instead of waiting for the next poll
6
6
  * tick. The canonical truth source remains `GET /studies/{id}` — SSE here
7
7
  * only shortens the latency between a backend event and the next status
@@ -26,11 +26,11 @@ export interface StudyEvent {
26
26
  type: string;
27
27
  study_id: string;
28
28
  iteration_id?: string | null;
29
- tester_id?: string | null;
29
+ participant_id?: string | null;
30
30
  interaction_id?: string | null;
31
31
  frame_id?: string | null;
32
32
  frame_version_id?: string | null;
33
- tester_status?: string | null;
33
+ participant_status?: string | null;
34
34
  ts: string;
35
35
  seq: number;
36
36
  payload?: unknown;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * SSE consumer for the backend's per-study event stream.
3
3
  *
4
- * Used by `study run --wait` to wake up the poll loop as soon as a tester
4
+ * Used by `study run --wait` to wake up the poll loop as soon as a participant
5
5
  * status / interaction event arrives, instead of waiting for the next poll
6
6
  * tick. The canonical truth source remains `GET /studies/{id}` — SSE here
7
7
  * only shortens the latency between a backend event and the next status
@@ -3,13 +3,23 @@
3
3
  * flags. Mirrors the loose validation style of `src/lib/ask-questions.ts`.
4
4
  */
5
5
  import type { Assignment, InterviewQuestion } from "./types.js";
6
+ /**
7
+ * Validate a parsed array of assignment objects. Shared by the
8
+ * `--assignments-file` and inline `--assignments` paths so both enforce the
9
+ * same shape: each entry needs a non-empty `name` + `instructions`, and an
10
+ * optional `steps` checklist (validated against backend bounds). Response-only
11
+ * keys like `id` / `step_completion` are tolerated and passed through so a
12
+ * `study get --json` payload round-trips back into a create.
13
+ */
14
+ export declare function validateAssignmentsArray(parsed: unknown, label: string): Assignment[];
6
15
  /**
7
16
  * Parse `"Name:Instructions"`. Splits on the first `:`, so colons inside
8
17
  * the instructions text are preserved.
9
18
  */
10
19
  export declare function parseAssignment(value: string): Assignment;
11
20
  /**
12
- * Read a JSON file containing an array of `{name, instructions}` entries.
21
+ * Read a JSON file containing an array of `{name, instructions, steps?}`
22
+ * entries. `steps` is an optional checklist of `{name, description?}` actions.
13
23
  */
14
24
  export declare function loadAssignmentsFile(filePath: string): Assignment[];
15
25
  /**
@@ -4,6 +4,71 @@
4
4
  */
5
5
  import { readFileSync } from "node:fs";
6
6
  import { resolve as resolvePath } from "node:path";
7
+ const STEP_NAME_MAX = 80;
8
+ const STEP_DESC_MAX = 500;
9
+ /**
10
+ * Validate an optional `steps` checklist on one assignment. Mirrors the
11
+ * backend bounds (`AssignmentStep`: name 1–80, description ≤500) so authors get
12
+ * a local error instead of a 422. Returns the cleaned step list.
13
+ */
14
+ function validateSteps(raw, label) {
15
+ if (!Array.isArray(raw)) {
16
+ throw new Error(`${label}.steps must be an array of {name, description?} objects.`);
17
+ }
18
+ return raw.map((entry, j) => {
19
+ const s = entry;
20
+ if (!s || typeof s !== "object") {
21
+ throw new Error(`${label}.steps[${j}] must be an object with a name.`);
22
+ }
23
+ if (typeof s.name !== "string" || !s.name.trim()) {
24
+ throw new Error(`${label}.steps[${j}].name must be a non-empty string.`);
25
+ }
26
+ if (s.name.length > STEP_NAME_MAX) {
27
+ throw new Error(`${label}.steps[${j}].name must be ≤${STEP_NAME_MAX} characters.`);
28
+ }
29
+ if (s.description !== undefined && s.description !== null) {
30
+ if (typeof s.description !== "string") {
31
+ throw new Error(`${label}.steps[${j}].description must be a string.`);
32
+ }
33
+ if (s.description.length > STEP_DESC_MAX) {
34
+ throw new Error(`${label}.steps[${j}].description must be ≤${STEP_DESC_MAX} characters.`);
35
+ }
36
+ }
37
+ const step = { name: s.name };
38
+ if (typeof s.description === "string")
39
+ step.description = s.description;
40
+ return step;
41
+ });
42
+ }
43
+ /**
44
+ * Validate a parsed array of assignment objects. Shared by the
45
+ * `--assignments-file` and inline `--assignments` paths so both enforce the
46
+ * same shape: each entry needs a non-empty `name` + `instructions`, and an
47
+ * optional `steps` checklist (validated against backend bounds). Response-only
48
+ * keys like `id` / `step_completion` are tolerated and passed through so a
49
+ * `study get --json` payload round-trips back into a create.
50
+ */
51
+ export function validateAssignmentsArray(parsed, label) {
52
+ if (!Array.isArray(parsed) || parsed.length === 0) {
53
+ throw new Error(`${label} must be a non-empty JSON array.`);
54
+ }
55
+ for (let i = 0; i < parsed.length; i++) {
56
+ const a = parsed[i];
57
+ if (!a || typeof a !== "object") {
58
+ throw new Error(`assignments[${i}] must be an object with name + instructions.`);
59
+ }
60
+ if (typeof a.name !== "string" || !a.name.trim()) {
61
+ throw new Error(`assignments[${i}].name must be a non-empty string.`);
62
+ }
63
+ if (typeof a.instructions !== "string" || !a.instructions.trim()) {
64
+ throw new Error(`assignments[${i}].instructions must be a non-empty string.`);
65
+ }
66
+ if (a.steps !== undefined && a.steps !== null) {
67
+ a.steps = validateSteps(a.steps, `assignments[${i}]`);
68
+ }
69
+ }
70
+ return parsed;
71
+ }
7
72
  /**
8
73
  * Parse `"Name:Instructions"`. Splits on the first `:`, so colons inside
9
74
  * the instructions text are preserved.
@@ -24,7 +89,8 @@ export function parseAssignment(value) {
24
89
  return { name, instructions };
25
90
  }
26
91
  /**
27
- * Read a JSON file containing an array of `{name, instructions}` entries.
92
+ * Read a JSON file containing an array of `{name, instructions, steps?}`
93
+ * entries. `steps` is an optional checklist of `{name, description?}` actions.
28
94
  */
29
95
  export function loadAssignmentsFile(filePath) {
30
96
  let raw;
@@ -41,22 +107,7 @@ export function loadAssignmentsFile(filePath) {
41
107
  catch {
42
108
  throw new Error(`Invalid JSON in assignments file: ${filePath}`);
43
109
  }
44
- if (!Array.isArray(parsed) || parsed.length === 0) {
45
- throw new Error(`Assignments file must be a non-empty JSON array: ${filePath}`);
46
- }
47
- for (let i = 0; i < parsed.length; i++) {
48
- const a = parsed[i];
49
- if (!a || typeof a !== "object") {
50
- throw new Error(`assignments[${i}] must be an object with name + instructions.`);
51
- }
52
- if (typeof a.name !== "string" || !a.name.trim()) {
53
- throw new Error(`assignments[${i}].name must be a non-empty string.`);
54
- }
55
- if (typeof a.instructions !== "string" || !a.instructions.trim()) {
56
- throw new Error(`assignments[${i}].instructions must be a non-empty string.`);
57
- }
58
- }
59
- return parsed;
110
+ return validateAssignmentsArray(parsed, `Assignments file ${filePath}`);
60
111
  }
61
112
  /**
62
113
  * Parse a plain question text into a default text-typed, after-timed
@@ -54,10 +54,46 @@ export interface SecretUpdateInput {
54
54
  export interface SecretBatchCreateInput {
55
55
  secrets: SecretCreateInput[];
56
56
  }
57
+ /**
58
+ * One atomic, author-supplied action inside an assignment's checklist
59
+ * (e.g. "Add to cart"). Authored via the JSON forms of `study create/update`
60
+ * (`--assignments-file` / `--assignments`) — only for `interactive` and
61
+ * `external_chatbot chat` modalities; the backend rejects steps elsewhere.
62
+ */
63
+ export interface AssignmentStep {
64
+ /** Response-only server slug (e.g. "add-to-cart"); omitted on write. */
65
+ id?: string;
66
+ /** 1–80 chars. */
67
+ name: string;
68
+ /** ≤500 chars. */
69
+ description?: string;
70
+ }
71
+ /** One sampled participant who failed a step, with the verifier's reason. */
72
+ export interface SampleFailure {
73
+ participant_id: string;
74
+ reason: string;
75
+ }
76
+ /**
77
+ * Response-only per-step completion rollup, populated by the backend after a
78
+ * run grades each step per participant. Ignored on write.
79
+ */
80
+ export interface StepCompletion {
81
+ step_id: string;
82
+ name: string;
83
+ description?: string;
84
+ total: number;
85
+ passed: number;
86
+ rate?: number | null;
87
+ sample_failures?: SampleFailure[];
88
+ }
57
89
  export interface Assignment {
58
90
  id?: string;
59
91
  name: string;
60
92
  instructions: string;
93
+ /** Optional checklist authored on write; resolved with `id` on read. */
94
+ steps?: AssignmentStep[];
95
+ /** Response-only completion rollup; ignored on write. */
96
+ step_completion?: StepCompletion[];
61
97
  }
62
98
  export interface InterviewQuestion {
63
99
  id?: string;
@@ -119,7 +155,7 @@ export interface Iteration {
119
155
  description?: string;
120
156
  label?: string;
121
157
  details?: Record<string, unknown>;
122
- testers?: Tester[];
158
+ participants?: Participant[];
123
159
  created_at: string;
124
160
  updated_at: string;
125
161
  }
@@ -134,39 +170,75 @@ export interface IterationUpdateInput {
134
170
  details?: Record<string, unknown>;
135
171
  label?: string;
136
172
  }
137
- export interface TesterProfile {
173
+ export interface Person {
138
174
  id: string;
139
175
  name: string;
140
176
  [key: string]: unknown;
141
177
  }
142
- export type SourceKind = "text_file" | "audio" | "image";
143
- export type SourceStatus = "pending_upload" | "uploaded" | "transcribing" | "processed" | "failed";
144
- export interface AudienceSource {
178
+ export type AttachmentKind = "text_file" | "audio" | "image";
179
+ export type AttachmentStatus = "pending_upload" | "uploaded" | "transcribing" | "processing" | "processed" | "failed";
180
+ /** How a file relates to a participant profile (`identity` is reserved). */
181
+ export type AttachmentRelation = "seed" | "attached";
182
+ /** Single attachment row returned by GET /people/attachments/{id}. */
183
+ export interface Attachment {
145
184
  id: string;
146
185
  product_id: string;
147
- kind: SourceKind;
148
- status: SourceStatus;
149
- original_filename: string;
186
+ kind: AttachmentKind;
187
+ status: AttachmentStatus;
188
+ file_name: string;
150
189
  content_type: string;
190
+ file_size_bytes?: number | null;
151
191
  extracted_text_length?: number | null;
192
+ description?: string | null;
152
193
  error?: string | null;
194
+ relations: AttachmentRelation[];
153
195
  created_at: string;
154
196
  }
155
- export interface InitiateSourceUploadResponse {
156
- source_id: string;
197
+ export interface InitiateAttachmentUploadResponse {
198
+ attachment_id: string;
157
199
  signed_url: string;
158
200
  upload_token: string;
159
201
  }
202
+ export type SourceKind = AttachmentKind;
203
+ export type SourceStatus = AttachmentStatus;
204
+ export type PersonSource = Attachment;
205
+ export type InitiateSourceUploadResponse = InitiateAttachmentUploadResponse;
160
206
  export interface ProposeCountResponse {
161
207
  proposed_count: number;
162
208
  rationale: string;
163
209
  }
164
- export interface GenerateAudienceRequest {
210
+ export interface GeneratePeopleRequest {
165
211
  product_id: string;
166
212
  description?: string;
167
213
  source_upload_ids?: string[];
168
214
  count?: number;
169
215
  }
216
+ export type GenerationJobStatus = "queued" | "processing" | "completed" | "failed";
217
+ export interface CreateGenerationJobRequest {
218
+ product_id: string;
219
+ description?: string;
220
+ source_upload_ids?: string[];
221
+ count?: number;
222
+ }
223
+ export interface GenerationJob {
224
+ id: string;
225
+ status: GenerationJobStatus;
226
+ progress_message: string | null;
227
+ person_ids: string[];
228
+ error: string | null;
229
+ created_at: string;
230
+ updated_at: string;
231
+ }
232
+ /** One scenario the job grounded in a real reaction (GET /people/{id}/scenarios). */
233
+ export interface ProfileScenario {
234
+ source: string;
235
+ scenario_prompt: string;
236
+ text: string;
237
+ raw_response?: {
238
+ evidence_quote?: string;
239
+ } & Record<string, unknown>;
240
+ [key: string]: unknown;
241
+ }
170
242
  export interface GeneratedProfile {
171
243
  id: string;
172
244
  name: string;
@@ -243,30 +315,30 @@ export interface EvidenceTraceResponse {
243
315
  raw_response: Record<string, unknown> | null;
244
316
  created_at: string;
245
317
  }
246
- export interface Tester {
318
+ export interface Participant {
247
319
  id: string;
248
320
  iteration_id: string;
249
- tester_profile_id: string;
321
+ person_id: string;
250
322
  instance_name?: string;
251
323
  instance_number?: number;
252
324
  status: string;
253
325
  language?: string;
254
326
  platform?: string;
255
- tester_type?: string;
327
+ participant_type?: string;
256
328
  viewport_width?: number;
257
329
  viewport_height?: number;
258
330
  created_at: string;
259
331
  }
260
- export interface TesterCreateInput {
261
- tester_profile_id: string;
332
+ export interface ParticipantCreateInput {
333
+ person_id: string;
262
334
  instance_name?: string;
263
335
  status?: string;
264
336
  language?: string;
265
337
  platform?: string;
266
- tester_type?: string;
338
+ participant_type?: string;
267
339
  }
268
340
  export interface SimulationStartResponse {
269
- tester_id: string;
341
+ participant_id: string;
270
342
  study_id: string;
271
343
  job_id: string | null;
272
344
  message: string;
@@ -296,7 +368,6 @@ export interface SimulationConfig {
296
368
  id: string;
297
369
  name: string;
298
370
  model_settings?: Record<string, unknown>;
299
- simulation_settings?: Record<string, unknown>;
300
371
  prompts?: Record<string, unknown>;
301
372
  outputs?: Record<string, unknown>;
302
373
  source_type?: string;
@@ -329,14 +400,14 @@ export interface InterviewAnswer {
329
400
  /**
330
401
  * Pattern B — drill-in subset for a follow-up ask round.
331
402
  *
332
- * Filters the new round's audience to the testers who picked
403
+ * Filters the new round's participants to those who picked
333
404
  * `picked_variant_id` on the 1-indexed prior `round`. Mirrors the
334
- * backend's `AudienceSubset` model. Only valid on follow-up rounds —
405
+ * backend's `ParticipantSubset` model. Only valid on follow-up rounds —
335
406
  * round 1 has no prior round to filter against. The backend rejects
336
407
  * unresolvable subsets with a 422 carrying
337
- * `error_kind: "audience_subset_invalid"`.
408
+ * `error_kind: "participant_subset_invalid"`.
338
409
  */
339
- export interface AudienceSubset {
410
+ export interface ParticipantSubset {
340
411
  round: number;
341
412
  picked_variant_id: string;
342
413
  }
@@ -346,13 +417,13 @@ export interface AskRoundInput {
346
417
  wants_pick?: boolean;
347
418
  wants_ratings?: boolean;
348
419
  questions?: InterviewQuestion[];
349
- audience_subset?: AudienceSubset;
420
+ participant_subset?: ParticipantSubset;
350
421
  }
351
422
  export interface AskCreateInput {
352
423
  name: string;
353
424
  description?: string;
354
425
  language?: string;
355
- tester_profile_ids: string[];
426
+ person_ids: string[];
356
427
  first_round: AskRoundInput;
357
428
  dispatch?: boolean;
358
429
  }
@@ -361,20 +432,20 @@ export interface AskUpdateInput {
361
432
  description?: string;
362
433
  is_archived?: boolean;
363
434
  }
364
- export interface AddTestersInput {
365
- tester_profile_ids: string[];
366
- round_id: string;
435
+ export interface AddPeopleInput {
436
+ person_ids: string[];
437
+ dispatch_into_round_id: string;
367
438
  backfill_prior_rounds?: boolean;
368
439
  }
369
440
  export interface AddRoundQuestionsInput {
370
441
  questions: InterviewQuestion[];
371
442
  redispatch_all?: boolean;
372
443
  }
373
- export interface AskAudienceTester {
444
+ export interface AskParticipant {
374
445
  id: string;
375
446
  ask_id?: string;
376
- tester_profile_id?: string;
377
- tester_profile?: Record<string, unknown> | null;
447
+ person_id?: string;
448
+ person?: Record<string, unknown> | null;
378
449
  instance_name?: string;
379
450
  status?: string;
380
451
  [key: string]: unknown;
@@ -382,7 +453,7 @@ export interface AskAudienceTester {
382
453
  export interface AskResponseModel {
383
454
  id: string;
384
455
  ask_round_id: string;
385
- tester_id: string;
456
+ participant_id: string;
386
457
  comment?: string | null;
387
458
  variant_pick_id?: string | null;
388
459
  pick_confidence?: number | null;
@@ -418,7 +489,7 @@ export interface Ask {
418
489
  description?: string | null;
419
490
  is_archived: boolean;
420
491
  status?: AskStatus;
421
- testers: AskAudienceTester[];
492
+ participants: AskParticipant[];
422
493
  rounds: AskRound[];
423
494
  created_at: string;
424
495
  updated_at: string;
@@ -431,7 +502,7 @@ export interface AskListItem {
431
502
  description?: string | null;
432
503
  is_archived: boolean;
433
504
  status?: AskStatus;
434
- audience_count: number;
505
+ participant_count: number;
435
506
  round_count: number;
436
507
  last_round_at?: string | null;
437
508
  created_at: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ishlabs/cli",
3
- "version": "0.17.6",
3
+ "version": "0.18.0",
4
4
  "description": "The command-line interface for ish",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +0,0 @@
1
- /**
2
- * ish profile — Manage profiles, audience generation, and source uploads.
3
- */
4
- import type { Command } from "commander";
5
- export declare function registerProfileCommands(program: Command): void;
@@ -1,8 +0,0 @@
1
- /**
2
- * ish study tester — Inspect and manage testers (low-level; usually
3
- * created via `ish study run`).
4
- *
5
- * Default action: `ish study tester <id>` shows tester details and results.
6
- */
7
- import type { Command } from "commander";
8
- export declare function attachStudyTesterCommands(study: Command): void;