@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.
- package/README.md +54 -54
- package/dist/commands/ask.d.ts +4 -4
- package/dist/commands/ask.js +66 -66
- package/dist/commands/chat.js +10 -10
- package/dist/commands/config.js +1 -1
- package/dist/commands/docs.js +1 -1
- package/dist/commands/iteration.js +57 -57
- package/dist/commands/mcp.d.ts +23 -0
- package/dist/commands/mcp.js +676 -0
- package/dist/commands/person.d.ts +5 -0
- package/dist/commands/{profile.js → person.js} +197 -162
- package/dist/commands/source.d.ts +6 -2
- package/dist/commands/source.js +35 -30
- package/dist/commands/study-analyze.d.ts +1 -1
- package/dist/commands/study-analyze.js +3 -3
- package/dist/commands/study-participant.d.ts +8 -0
- package/dist/commands/{study-tester.js → study-participant.js} +50 -50
- package/dist/commands/study-run.d.ts +6 -6
- package/dist/commands/study-run.js +295 -271
- package/dist/commands/study.js +89 -66
- package/dist/commands/workspace.js +13 -13
- package/dist/connect.js +5 -5
- package/dist/index.js +6 -4
- package/dist/lib/accessibility-profile.d.ts +1 -1
- package/dist/lib/accessibility-profile.js +1 -1
- package/dist/lib/alias-hydrate.js +4 -4
- package/dist/lib/alias-store.d.ts +5 -5
- package/dist/lib/alias-store.js +8 -8
- package/dist/lib/api-client.d.ts +1 -1
- package/dist/lib/api-client.js +1 -1
- package/dist/lib/billing.d.ts +11 -11
- package/dist/lib/billing.js +16 -16
- package/dist/lib/chat-endpoint-templates.js +1 -1
- package/dist/lib/command-helpers.d.ts +18 -18
- package/dist/lib/command-helpers.js +83 -53
- package/dist/lib/docs.js +560 -386
- package/dist/lib/enums.d.ts +2 -2
- package/dist/lib/enums.js +2 -2
- package/dist/lib/local-sim/browser.d.ts +1 -1
- package/dist/lib/local-sim/browser.js +1 -1
- package/dist/lib/local-sim/debug-report.d.ts +2 -2
- package/dist/lib/local-sim/debug-report.js +3 -3
- package/dist/lib/local-sim/loop.d.ts +5 -5
- package/dist/lib/local-sim/loop.js +38 -38
- package/dist/lib/local-sim/types.d.ts +12 -12
- package/dist/lib/mcp-clients.d.ts +51 -0
- package/dist/lib/mcp-clients.js +175 -0
- package/dist/lib/modality.d.ts +10 -10
- package/dist/lib/modality.js +46 -46
- package/dist/lib/observability.d.ts +11 -0
- package/dist/lib/observability.js +16 -3
- package/dist/lib/output.d.ts +13 -12
- package/dist/lib/output.js +244 -184
- package/dist/lib/profile-sources.d.ts +64 -16
- package/dist/lib/profile-sources.js +91 -30
- package/dist/lib/skill-content.js +215 -168
- package/dist/lib/study-events.d.ts +3 -3
- package/dist/lib/study-events.js +1 -1
- package/dist/lib/study-inputs.d.ts +11 -1
- package/dist/lib/study-inputs.js +68 -17
- package/dist/lib/types.d.ts +105 -34
- package/package.json +1 -1
- package/dist/commands/profile.d.ts +0 -5
- package/dist/commands/study-tester.d.ts +0 -8
|
@@ -1,33 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Participant attachment upload + processing lifecycle.
|
|
3
3
|
*
|
|
4
|
-
* Mirrors the frontend flow in
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 {
|
|
18
|
+
import type { Attachment, AttachmentKind, GenerationJob } from "./types.js";
|
|
17
19
|
/**
|
|
18
|
-
* Map a local file to the backend's
|
|
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
|
|
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
|
|
25
|
-
export declare function
|
|
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?:
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
2
|
+
* Participant attachment upload + processing lifecycle.
|
|
3
3
|
*
|
|
4
|
-
* Mirrors the frontend flow in
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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(`
|
|
87
|
+
throw new Error(`Attachment upload failed (HTTP ${res.status} ${res.statusText})${body ? ` — ${body.slice(0, 500)}` : ""}`);
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
|
-
/** Poll
|
|
87
|
-
export async function
|
|
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
|
|
91
|
-
if (TERMINAL_STATUSES.has(
|
|
92
|
-
return
|
|
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
|
|
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
|
|
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 ??
|
|
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("/
|
|
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(`/
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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 (
|
|
154
|
-
throw new 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
|
}
|