@ishlabs/cli 0.17.7 → 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 (62) 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 +49 -37
  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/output.d.ts +13 -12
  51. package/dist/lib/output.js +244 -184
  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 +215 -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/types.d.ts +105 -34
  60. package/package.json +1 -1
  61. package/dist/commands/profile.d.ts +0 -5
  62. package/dist/commands/study-tester.d.ts +0 -8
@@ -1,33 +1,45 @@
1
1
  /**
2
- * Audience source upload + processing lifecycle.
2
+ * Participant attachment upload + processing lifecycle.
3
3
  *
4
- * Mirrors the frontend flow in ensure-sources-processed.ts and
5
- * UploadSection.tsx:
6
- * 1. POST /tester-profiles/sources/initiate signed URL + source_id
7
- * 2. PUT signed_url upload bytes (x-upsert: true)
8
- * 3. POST /tester-profiles/sources/{id}/confirm kick off parse / transcribe
9
- * 4. GET /tester-profiles/sources/{id} → poll until terminal status
4
+ * Mirrors the frontend flow in the creation flow's upload hook:
5
+ * 1. POST /people/attachments/initiate → signed URL + attachment_id
6
+ * 2. PUT signed_url upload bytes (x-upsert: true)
7
+ * 3. POST /people/attachments/{id}/confirm kick off parse / transcribe
8
+ * 4. GET /people/attachments/{id} poll until terminal status
10
9
  *
11
10
  * Distinct from the study upload in upload.ts: different endpoints and a
12
11
  * fourth polling step because text/image are parsed inline but audio is
13
12
  * transcribed by an async worker.
13
+ *
14
+ * The public CLI command name `source` is preserved for compatibility; the
15
+ * internal vocabulary tracks the unified `attachments` model (ADR-0034).
14
16
  */
15
17
  import type { ApiClient } from "./api-client.js";
16
- import type { AudienceSource, SourceKind } from "./types.js";
18
+ import type { Attachment, AttachmentKind, GenerationJob } from "./types.js";
17
19
  /**
18
- * Map a local file to the backend's SourceKind enum.
20
+ * Map a local file to the backend's AttachmentKind enum.
19
21
  * Throws if the extension/MIME isn't recognized — caller can pass an
20
22
  * explicit `kind` to override.
21
23
  */
22
- export declare function inferSourceKind(filePath: string): SourceKind;
24
+ export declare function inferAttachmentKind(filePath: string): AttachmentKind;
25
+ /** Back-compat re-export: the public CLI keeps the `source` vocabulary. */
26
+ export declare const inferSourceKind: typeof inferAttachmentKind;
23
27
  export declare function isUuid(value: string): boolean;
24
- /** Poll a source until it reaches a terminal status or the timeout fires. */
25
- export declare function pollSourceUntilTerminal(client: ApiClient, sourceId: string, timeoutMs?: number): Promise<AudienceSource>;
28
+ /** Poll an attachment until it reaches a terminal status or the timeout fires. */
29
+ export declare function pollAttachmentUntilTerminal(client: ApiClient, attachmentId: string, timeoutMs?: number): Promise<Attachment>;
30
+ /** Back-compat alias. */
31
+ export declare const pollSourceUntilTerminal: typeof pollAttachmentUntilTerminal;
26
32
  export interface UploadAndProcessOpts {
27
33
  productId: string;
28
34
  filePath: string;
29
- kind?: SourceKind;
35
+ kind?: AttachmentKind;
30
36
  description?: string;
37
+ /**
38
+ * Retained for CLI compat — the new /attachments/confirm endpoint does NOT
39
+ * accept a diarize flag (audio confirm always runs the diarization-capable
40
+ * worker). The value is ignored; callers can keep passing --diarize until
41
+ * the flag is dropped in a follow-up.
42
+ */
31
43
  diarize?: boolean;
32
44
  /** When false, return after confirm without polling. Default true. */
33
45
  wait?: boolean;
@@ -36,7 +48,9 @@ export interface UploadAndProcessOpts {
36
48
  quiet?: boolean;
37
49
  }
38
50
  /** Run the full initiate → PUT → confirm → poll cycle for a single file. */
39
- export declare function uploadAndProcessSource(client: ApiClient, opts: UploadAndProcessOpts): Promise<AudienceSource>;
51
+ export declare function uploadAndProcessAttachment(client: ApiClient, opts: UploadAndProcessOpts): Promise<Attachment>;
52
+ /** Back-compat alias for callers still importing the `Source` name. */
53
+ export declare const uploadAndProcessSource: typeof uploadAndProcessAttachment;
40
54
  export interface ResolveSourceRefOpts {
41
55
  productId: string;
42
56
  description?: string;
@@ -47,9 +61,43 @@ export interface ResolveSourceRefOpts {
47
61
  }
48
62
  /**
49
63
  * Resolve a `--source` value: a UUID is returned as-is; a local path is
50
- * uploaded and the resulting source ID is returned.
64
+ * uploaded and the resulting attachment ID is returned.
51
65
  *
52
66
  * If a UUID-shaped string is provided but turns out not to be a real
53
- * source, the backend rejects it on /generate — no need to pre-check.
67
+ * attachment, the backend rejects it on /generate — no need to pre-check.
54
68
  */
55
69
  export declare function resolveSourceRef(client: ApiClient, value: string, opts: ResolveSourceRefOpts): Promise<string>;
70
+ /**
71
+ * Thrown when a generation job doesn't reach a terminal status before the
72
+ * --timeout fires. Carries `error_code`/`retryable`/`progress` so the JSON
73
+ * error envelope (output.ts) surfaces them — the job keeps running
74
+ * server-side, so the agent can re-poll the same job id rather than re-enqueue.
75
+ */
76
+ export declare class GenerationTimeoutError extends Error {
77
+ readonly progress: {
78
+ job_id: string;
79
+ timeout_seconds: number;
80
+ status: string;
81
+ progress_message: string | null;
82
+ };
83
+ readonly error_code = "wait_timeout";
84
+ readonly retryable = true;
85
+ constructor(message: string, progress: {
86
+ job_id: string;
87
+ timeout_seconds: number;
88
+ status: string;
89
+ progress_message: string | null;
90
+ });
91
+ }
92
+ export interface PollGenerationJobOpts {
93
+ timeoutMs?: number;
94
+ /** Print status progress lines to stderr. Default true unless quiet. */
95
+ quiet?: boolean;
96
+ }
97
+ /**
98
+ * Poll a generation job until it reaches `completed` or `failed`, or the
99
+ * timeout fires. Emits a stderr line each time `progress_message`/`status`
100
+ * changes so agents see the ~30-60s run isn't stuck (mirrors the attachment-poll
101
+ * UX). Returns the terminal job; callers branch on `status` themselves.
102
+ */
103
+ export declare function pollGenerationJobUntilDone(client: ApiClient, jobId: string, opts?: PollGenerationJobOpts): Promise<GenerationJob>;
@@ -1,16 +1,18 @@
1
1
  /**
2
- * Audience source upload + processing lifecycle.
2
+ * Participant attachment upload + processing lifecycle.
3
3
  *
4
- * Mirrors the frontend flow in ensure-sources-processed.ts and
5
- * UploadSection.tsx:
6
- * 1. POST /tester-profiles/sources/initiate signed URL + source_id
7
- * 2. PUT signed_url upload bytes (x-upsert: true)
8
- * 3. POST /tester-profiles/sources/{id}/confirm kick off parse / transcribe
9
- * 4. GET /tester-profiles/sources/{id} → poll until terminal status
4
+ * Mirrors the frontend flow in the creation flow's upload hook:
5
+ * 1. POST /people/attachments/initiate → signed URL + attachment_id
6
+ * 2. PUT signed_url upload bytes (x-upsert: true)
7
+ * 3. POST /people/attachments/{id}/confirm kick off parse / transcribe
8
+ * 4. GET /people/attachments/{id} poll until terminal status
10
9
  *
11
10
  * Distinct from the study upload in upload.ts: different endpoints and a
12
11
  * fourth polling step because text/image are parsed inline but audio is
13
12
  * transcribed by an async worker.
13
+ *
14
+ * The public CLI command name `source` is preserved for compatibility; the
15
+ * internal vocabulary tracks the unified `attachments` model (ADR-0034).
14
16
  */
15
17
  import { readFile } from "node:fs/promises";
16
18
  import { basename, extname, resolve as resolvePath } from "node:path";
@@ -33,11 +35,11 @@ const TEXT_EXTS = new Set([
33
35
  ".docx",
34
36
  ]);
35
37
  /**
36
- * Map a local file to the backend's SourceKind enum.
38
+ * Map a local file to the backend's AttachmentKind enum.
37
39
  * Throws if the extension/MIME isn't recognized — caller can pass an
38
40
  * explicit `kind` to override.
39
41
  */
40
- export function inferSourceKind(filePath) {
42
+ export function inferAttachmentKind(filePath) {
41
43
  const ext = extname(filePath).toLowerCase();
42
44
  if (AUDIO_EXTS.has(ext))
43
45
  return "audio";
@@ -57,8 +59,10 @@ export function inferSourceKind(filePath) {
57
59
  mime.includes("officedocument")) {
58
60
  return "text_file";
59
61
  }
60
- throw new Error(`Cannot infer source kind for ${filePath}. Pass --kind text_file|audio|image to override.`);
62
+ throw new Error(`Cannot infer attachment kind for ${filePath}. Pass --kind text_file|audio|image to override.`);
61
63
  }
64
+ /** Back-compat re-export: the public CLI keeps the `source` vocabulary. */
65
+ export const inferSourceKind = inferAttachmentKind;
62
66
  export function isUuid(value) {
63
67
  return UUID_RE.test(value);
64
68
  }
@@ -68,7 +72,7 @@ async function putFileToSignedUrl(signedUrl, filePath, mime) {
68
72
  // Supabase signed-upload endpoint authenticates via the ?token=... query
69
73
  // string already in the URL. Adding Authorization: Bearer here makes
70
74
  // Supabase fall through to JWT validation and 400s on PDFs (see frontend
71
- // putAudienceSourceFile comment).
75
+ // putAttachmentFile comment).
72
76
  const res = await fetch(signedUrl, {
73
77
  method: "PUT",
74
78
  headers: {
@@ -80,42 +84,43 @@ async function putFileToSignedUrl(signedUrl, filePath, mime) {
80
84
  });
81
85
  if (!res.ok) {
82
86
  const body = await res.text().catch(() => "");
83
- throw new Error(`Source upload failed (HTTP ${res.status} ${res.statusText})${body ? ` — ${body.slice(0, 500)}` : ""}`);
87
+ throw new Error(`Attachment upload failed (HTTP ${res.status} ${res.statusText})${body ? ` — ${body.slice(0, 500)}` : ""}`);
84
88
  }
85
89
  }
86
- /** Poll a source until it reaches a terminal status or the timeout fires. */
87
- export async function pollSourceUntilTerminal(client, sourceId, timeoutMs = DEFAULT_POLL_TIMEOUT_MS) {
90
+ /** Poll an attachment until it reaches a terminal status or the timeout fires. */
91
+ export async function pollAttachmentUntilTerminal(client, attachmentId, timeoutMs = DEFAULT_POLL_TIMEOUT_MS) {
88
92
  const deadline = Date.now() + timeoutMs;
89
93
  while (true) {
90
- const source = await client.get(`/tester-profiles/sources/${sourceId}`);
91
- if (TERMINAL_STATUSES.has(source.status))
92
- return source;
94
+ const attachment = await client.get(`/people/attachments/${attachmentId}`);
95
+ if (TERMINAL_STATUSES.has(attachment.status))
96
+ return attachment;
93
97
  if (Date.now() >= deadline) {
94
- throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for source ${sourceId} (status: ${source.status})`);
98
+ throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for attachment ${attachmentId} (status: ${attachment.status})`);
95
99
  }
96
100
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
97
101
  }
98
102
  }
103
+ /** Back-compat alias. */
104
+ export const pollSourceUntilTerminal = pollAttachmentUntilTerminal;
99
105
  /** Run the full initiate → PUT → confirm → poll cycle for a single file. */
100
- export async function uploadAndProcessSource(client, opts) {
106
+ export async function uploadAndProcessAttachment(client, opts) {
101
107
  const resolved = resolvePath(opts.filePath);
102
108
  const { mime } = await validateFile(opts.filePath);
103
109
  const fileName = basename(resolved);
104
- const kind = opts.kind ?? inferSourceKind(opts.filePath);
110
+ const kind = opts.kind ?? inferAttachmentKind(opts.filePath);
105
111
  const log = (msg) => {
106
112
  if (!opts.quiet)
107
113
  process.stderr.write(msg);
108
114
  };
109
115
  log(`Uploading ${fileName}…`);
110
- const init = await client.post("/tester-profiles/sources/initiate", {
116
+ const init = await client.post("/people/attachments/initiate", {
111
117
  product_id: opts.productId,
112
118
  file_name: fileName,
113
119
  content_type: mime,
114
120
  kind,
115
121
  }, { timeout: 60_000 });
116
122
  await putFileToSignedUrl(init.signed_url, resolved, mime);
117
- const confirmed = await client.post(`/tester-profiles/sources/${init.source_id}/confirm`, {
118
- diarize: opts.diarize,
123
+ const confirmed = await client.post(`/people/attachments/${init.attachment_id}/confirm`, {
119
124
  description: opts.description,
120
125
  }, { timeout: 60_000 });
121
126
  if (opts.wait === false) {
@@ -127,21 +132,23 @@ export async function uploadAndProcessSource(client, opts) {
127
132
  return confirmed;
128
133
  }
129
134
  log(" processing…");
130
- const final = await pollSourceUntilTerminal(client, init.source_id, opts.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS);
135
+ const final = await pollAttachmentUntilTerminal(client, init.attachment_id, opts.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS);
131
136
  log(` ${final.status}.\n`);
132
137
  return final;
133
138
  }
139
+ /** Back-compat alias for callers still importing the `Source` name. */
140
+ export const uploadAndProcessSource = uploadAndProcessAttachment;
134
141
  /**
135
142
  * Resolve a `--source` value: a UUID is returned as-is; a local path is
136
- * uploaded and the resulting source ID is returned.
143
+ * uploaded and the resulting attachment ID is returned.
137
144
  *
138
145
  * If a UUID-shaped string is provided but turns out not to be a real
139
- * source, the backend rejects it on /generate — no need to pre-check.
146
+ * attachment, the backend rejects it on /generate — no need to pre-check.
140
147
  */
141
148
  export async function resolveSourceRef(client, value, opts) {
142
149
  if (isUuid(value))
143
150
  return value;
144
- const source = await uploadAndProcessSource(client, {
151
+ const attachment = await uploadAndProcessAttachment(client, {
145
152
  productId: opts.productId,
146
153
  filePath: value,
147
154
  description: opts.description,
@@ -150,8 +157,62 @@ export async function resolveSourceRef(client, value, opts) {
150
157
  timeoutMs: opts.timeoutMs,
151
158
  quiet: opts.quiet,
152
159
  });
153
- if (source.status === "failed") {
154
- throw new Error(`Source ${source.original_filename} failed to process: ${source.error ?? "unknown error"}`);
160
+ if (attachment.status === "failed") {
161
+ throw new Error(`Attachment ${attachment.file_name} failed to process: ${attachment.error ?? "unknown error"}`);
162
+ }
163
+ return attachment.id;
164
+ }
165
+ // --- Agentic generation jobs ---
166
+ const GENERATION_POLL_INTERVAL_MS = 2_500;
167
+ const GENERATION_TERMINAL_STATUSES = new Set(["completed", "failed"]);
168
+ /**
169
+ * Thrown when a generation job doesn't reach a terminal status before the
170
+ * --timeout fires. Carries `error_code`/`retryable`/`progress` so the JSON
171
+ * error envelope (output.ts) surfaces them — the job keeps running
172
+ * server-side, so the agent can re-poll the same job id rather than re-enqueue.
173
+ */
174
+ export class GenerationTimeoutError extends Error {
175
+ progress;
176
+ error_code = "wait_timeout";
177
+ retryable = true;
178
+ constructor(message, progress) {
179
+ super(message);
180
+ this.progress = progress;
181
+ this.name = "GenerationTimeoutError";
182
+ }
183
+ }
184
+ /**
185
+ * Poll a generation job until it reaches `completed` or `failed`, or the
186
+ * timeout fires. Emits a stderr line each time `progress_message`/`status`
187
+ * changes so agents see the ~30-60s run isn't stuck (mirrors the attachment-poll
188
+ * UX). Returns the terminal job; callers branch on `status` themselves.
189
+ */
190
+ export async function pollGenerationJobUntilDone(client, jobId, opts = {}) {
191
+ const timeoutMs = opts.timeoutMs ?? 600_000;
192
+ const deadline = Date.now() + timeoutMs;
193
+ let lastReported = "";
194
+ while (true) {
195
+ const job = await client.get(`/people/generation-jobs/${jobId}`, undefined, { timeout: 60_000 });
196
+ if (!opts.quiet) {
197
+ const line = job.progress_message
198
+ ? `${job.status}: ${job.progress_message}`
199
+ : job.status;
200
+ if (line !== lastReported) {
201
+ process.stderr.write(` ${line}\n`);
202
+ lastReported = line;
203
+ }
204
+ }
205
+ if (GENERATION_TERMINAL_STATUSES.has(job.status))
206
+ return job;
207
+ if (Date.now() >= deadline) {
208
+ throw new GenerationTimeoutError(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for generation job ${jobId} ` +
209
+ `(status: ${job.status}). The job is still running — re-poll later, don't re-enqueue.`, {
210
+ job_id: jobId,
211
+ timeout_seconds: Math.round(timeoutMs / 1000),
212
+ status: job.status,
213
+ progress_message: job.progress_message,
214
+ });
215
+ }
216
+ await new Promise((r) => setTimeout(r, GENERATION_POLL_INTERVAL_MS));
155
217
  }
156
- return source.id;
157
218
  }