@ishlabs/cli 0.17.7 → 0.19.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 +341 -290
  20. package/dist/commands/study.js +106 -72
  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 +49 -37
  36. package/dist/lib/docs.js +570 -387
  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/output.d.ts +16 -15
  51. package/dist/lib/output.js +291 -226
  52. package/dist/lib/profile-sources.d.ts +64 -16
  53. package/dist/lib/profile-sources.js +91 -30
  54. package/dist/lib/skill-content.js +216 -168
  55. package/dist/lib/study-events.d.ts +3 -3
  56. package/dist/lib/study-events.js +1 -1
  57. package/dist/lib/study-inputs.d.ts +11 -1
  58. package/dist/lib/study-inputs.js +68 -17
  59. package/dist/lib/study-participants.d.ts +32 -0
  60. package/dist/lib/study-participants.js +12 -0
  61. package/dist/lib/types.d.ts +104 -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
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Wrapper for the post-split `GET /studies/{id}/participants` endpoint.
3
+ *
4
+ * Returns a flat list of participants for one study with an `iteration_id`
5
+ * discriminator on each row. Each row carries the rich per-participant graph
6
+ * (person, interactions[], participant_summary, interview_answers, …) that
7
+ * used to be embedded under `study.iterations[*].participants[*]` on the
8
+ * legacy `GET /studies/{id}` response.
9
+ */
10
+ import type { ApiClient } from "./api-client.js";
11
+ import type { Participant } from "./types.js";
12
+ export interface StudyParticipant extends Participant {
13
+ person?: {
14
+ id?: string;
15
+ name?: string;
16
+ };
17
+ interactions?: unknown[];
18
+ participant_summary?: Record<string, unknown> | null;
19
+ interview_answers?: Array<{
20
+ question_id?: string;
21
+ answer?: unknown;
22
+ }>;
23
+ participant_files?: unknown[];
24
+ participant_assignments?: unknown[];
25
+ conversation_id?: string | null;
26
+ error_message?: string | null;
27
+ error_kind?: string | null;
28
+ [k: string]: unknown;
29
+ }
30
+ export declare function fetchStudyParticipants(client: ApiClient, studyId: string, opts?: {
31
+ timeout?: number;
32
+ }): Promise<StudyParticipant[]>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Wrapper for the post-split `GET /studies/{id}/participants` endpoint.
3
+ *
4
+ * Returns a flat list of participants for one study with an `iteration_id`
5
+ * discriminator on each row. Each row carries the rich per-participant graph
6
+ * (person, interactions[], participant_summary, interview_answers, …) that
7
+ * used to be embedded under `study.iterations[*].participants[*]` on the
8
+ * legacy `GET /studies/{id}` response.
9
+ */
10
+ export async function fetchStudyParticipants(client, studyId, opts) {
11
+ return await client.get(`/studies/${studyId}/participants`, undefined, opts);
12
+ }
@@ -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,6 @@ export interface Iteration {
119
155
  description?: string;
120
156
  label?: string;
121
157
  details?: Record<string, unknown>;
122
- testers?: Tester[];
123
158
  created_at: string;
124
159
  updated_at: string;
125
160
  }
@@ -134,39 +169,75 @@ export interface IterationUpdateInput {
134
169
  details?: Record<string, unknown>;
135
170
  label?: string;
136
171
  }
137
- export interface TesterProfile {
172
+ export interface Person {
138
173
  id: string;
139
174
  name: string;
140
175
  [key: string]: unknown;
141
176
  }
142
- export type SourceKind = "text_file" | "audio" | "image";
143
- export type SourceStatus = "pending_upload" | "uploaded" | "transcribing" | "processed" | "failed";
144
- export interface AudienceSource {
177
+ export type AttachmentKind = "text_file" | "audio" | "image";
178
+ export type AttachmentStatus = "pending_upload" | "uploaded" | "transcribing" | "processing" | "processed" | "failed";
179
+ /** How a file relates to a participant profile (`identity` is reserved). */
180
+ export type AttachmentRelation = "seed" | "attached";
181
+ /** Single attachment row returned by GET /people/attachments/{id}. */
182
+ export interface Attachment {
145
183
  id: string;
146
184
  product_id: string;
147
- kind: SourceKind;
148
- status: SourceStatus;
149
- original_filename: string;
185
+ kind: AttachmentKind;
186
+ status: AttachmentStatus;
187
+ file_name: string;
150
188
  content_type: string;
189
+ file_size_bytes?: number | null;
151
190
  extracted_text_length?: number | null;
191
+ description?: string | null;
152
192
  error?: string | null;
193
+ relations: AttachmentRelation[];
153
194
  created_at: string;
154
195
  }
155
- export interface InitiateSourceUploadResponse {
156
- source_id: string;
196
+ export interface InitiateAttachmentUploadResponse {
197
+ attachment_id: string;
157
198
  signed_url: string;
158
199
  upload_token: string;
159
200
  }
201
+ export type SourceKind = AttachmentKind;
202
+ export type SourceStatus = AttachmentStatus;
203
+ export type PersonSource = Attachment;
204
+ export type InitiateSourceUploadResponse = InitiateAttachmentUploadResponse;
160
205
  export interface ProposeCountResponse {
161
206
  proposed_count: number;
162
207
  rationale: string;
163
208
  }
164
- export interface GenerateAudienceRequest {
209
+ export interface GeneratePeopleRequest {
165
210
  product_id: string;
166
211
  description?: string;
167
212
  source_upload_ids?: string[];
168
213
  count?: number;
169
214
  }
215
+ export type GenerationJobStatus = "queued" | "processing" | "completed" | "failed";
216
+ export interface CreateGenerationJobRequest {
217
+ product_id: string;
218
+ description?: string;
219
+ source_upload_ids?: string[];
220
+ count?: number;
221
+ }
222
+ export interface GenerationJob {
223
+ id: string;
224
+ status: GenerationJobStatus;
225
+ progress_message: string | null;
226
+ person_ids: string[];
227
+ error: string | null;
228
+ created_at: string;
229
+ updated_at: string;
230
+ }
231
+ /** One scenario the job grounded in a real reaction (GET /people/{id}/scenarios). */
232
+ export interface ProfileScenario {
233
+ source: string;
234
+ scenario_prompt: string;
235
+ text: string;
236
+ raw_response?: {
237
+ evidence_quote?: string;
238
+ } & Record<string, unknown>;
239
+ [key: string]: unknown;
240
+ }
170
241
  export interface GeneratedProfile {
171
242
  id: string;
172
243
  name: string;
@@ -243,30 +314,30 @@ export interface EvidenceTraceResponse {
243
314
  raw_response: Record<string, unknown> | null;
244
315
  created_at: string;
245
316
  }
246
- export interface Tester {
317
+ export interface Participant {
247
318
  id: string;
248
319
  iteration_id: string;
249
- tester_profile_id: string;
320
+ person_id: string;
250
321
  instance_name?: string;
251
322
  instance_number?: number;
252
323
  status: string;
253
324
  language?: string;
254
325
  platform?: string;
255
- tester_type?: string;
326
+ participant_type?: string;
256
327
  viewport_width?: number;
257
328
  viewport_height?: number;
258
329
  created_at: string;
259
330
  }
260
- export interface TesterCreateInput {
261
- tester_profile_id: string;
331
+ export interface ParticipantCreateInput {
332
+ person_id: string;
262
333
  instance_name?: string;
263
334
  status?: string;
264
335
  language?: string;
265
336
  platform?: string;
266
- tester_type?: string;
337
+ participant_type?: string;
267
338
  }
268
339
  export interface SimulationStartResponse {
269
- tester_id: string;
340
+ participant_id: string;
270
341
  study_id: string;
271
342
  job_id: string | null;
272
343
  message: string;
@@ -296,7 +367,6 @@ export interface SimulationConfig {
296
367
  id: string;
297
368
  name: string;
298
369
  model_settings?: Record<string, unknown>;
299
- simulation_settings?: Record<string, unknown>;
300
370
  prompts?: Record<string, unknown>;
301
371
  outputs?: Record<string, unknown>;
302
372
  source_type?: string;
@@ -329,14 +399,14 @@ export interface InterviewAnswer {
329
399
  /**
330
400
  * Pattern B — drill-in subset for a follow-up ask round.
331
401
  *
332
- * Filters the new round's audience to the testers who picked
402
+ * Filters the new round's participants to those who picked
333
403
  * `picked_variant_id` on the 1-indexed prior `round`. Mirrors the
334
- * backend's `AudienceSubset` model. Only valid on follow-up rounds —
404
+ * backend's `ParticipantSubset` model. Only valid on follow-up rounds —
335
405
  * round 1 has no prior round to filter against. The backend rejects
336
406
  * unresolvable subsets with a 422 carrying
337
- * `error_kind: "audience_subset_invalid"`.
407
+ * `error_kind: "participant_subset_invalid"`.
338
408
  */
339
- export interface AudienceSubset {
409
+ export interface ParticipantSubset {
340
410
  round: number;
341
411
  picked_variant_id: string;
342
412
  }
@@ -346,13 +416,13 @@ export interface AskRoundInput {
346
416
  wants_pick?: boolean;
347
417
  wants_ratings?: boolean;
348
418
  questions?: InterviewQuestion[];
349
- audience_subset?: AudienceSubset;
419
+ participant_subset?: ParticipantSubset;
350
420
  }
351
421
  export interface AskCreateInput {
352
422
  name: string;
353
423
  description?: string;
354
424
  language?: string;
355
- tester_profile_ids: string[];
425
+ person_ids: string[];
356
426
  first_round: AskRoundInput;
357
427
  dispatch?: boolean;
358
428
  }
@@ -361,20 +431,20 @@ export interface AskUpdateInput {
361
431
  description?: string;
362
432
  is_archived?: boolean;
363
433
  }
364
- export interface AddTestersInput {
365
- tester_profile_ids: string[];
366
- round_id: string;
434
+ export interface AddPeopleInput {
435
+ person_ids: string[];
436
+ dispatch_into_round_id: string;
367
437
  backfill_prior_rounds?: boolean;
368
438
  }
369
439
  export interface AddRoundQuestionsInput {
370
440
  questions: InterviewQuestion[];
371
441
  redispatch_all?: boolean;
372
442
  }
373
- export interface AskAudienceTester {
443
+ export interface AskParticipant {
374
444
  id: string;
375
445
  ask_id?: string;
376
- tester_profile_id?: string;
377
- tester_profile?: Record<string, unknown> | null;
446
+ person_id?: string;
447
+ person?: Record<string, unknown> | null;
378
448
  instance_name?: string;
379
449
  status?: string;
380
450
  [key: string]: unknown;
@@ -382,7 +452,7 @@ export interface AskAudienceTester {
382
452
  export interface AskResponseModel {
383
453
  id: string;
384
454
  ask_round_id: string;
385
- tester_id: string;
455
+ participant_id: string;
386
456
  comment?: string | null;
387
457
  variant_pick_id?: string | null;
388
458
  pick_confidence?: number | null;
@@ -418,7 +488,7 @@ export interface Ask {
418
488
  description?: string | null;
419
489
  is_archived: boolean;
420
490
  status?: AskStatus;
421
- testers: AskAudienceTester[];
491
+ participants: AskParticipant[];
422
492
  rounds: AskRound[];
423
493
  created_at: string;
424
494
  updated_at: string;
@@ -431,7 +501,7 @@ export interface AskListItem {
431
501
  description?: string | null;
432
502
  is_archived: boolean;
433
503
  status?: AskStatus;
434
- audience_count: number;
504
+ participant_count: number;
435
505
  round_count: number;
436
506
  last_round_at?: string | null;
437
507
  created_at: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ishlabs/cli",
3
- "version": "0.17.7",
3
+ "version": "0.19.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;