@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.
- 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 +49 -37
- 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/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,39 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ish
|
|
2
|
+
* ish person — Manage people, generation, and source uploads.
|
|
3
3
|
*/
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import { withClient, readJsonFileOrStdin, resolveWorkspace } from "../lib/command-helpers.js";
|
|
6
6
|
import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
7
|
-
import {
|
|
7
|
+
import { formatPersonList, output, } from "../lib/output.js";
|
|
8
8
|
import { resolveTextContent } from "../lib/upload.js";
|
|
9
|
-
import { isUuid, resolveSourceRef } from "../lib/profile-sources.js";
|
|
9
|
+
import { isUuid, pollGenerationJobUntilDone, resolveSourceRef, } from "../lib/profile-sources.js";
|
|
10
10
|
import { assertEnumValue, EDUCATION_LEVELS, EVIDENCE_SOURCES, HOUSEHOLDS, LOCALE_TYPES, INCOME_LEVELS, EMPLOYMENT_STATUSES, } from "../lib/enums.js";
|
|
11
11
|
import { validateAccessibilityProfile } from "../lib/accessibility-profile.js";
|
|
12
12
|
function collect(value, prev) {
|
|
13
13
|
return prev.concat(value);
|
|
14
14
|
}
|
|
15
|
-
export function
|
|
16
|
-
const
|
|
17
|
-
.command("
|
|
18
|
-
.
|
|
19
|
-
.description("Manage profiles, audience generation, and source uploads")
|
|
15
|
+
export function registerPersonCommands(program) {
|
|
16
|
+
const person = program
|
|
17
|
+
.command("person")
|
|
18
|
+
.description("Manage people, generation, and source uploads")
|
|
20
19
|
.addHelpText("after", `
|
|
21
|
-
A
|
|
22
|
-
\`ish
|
|
23
|
-
(transcripts, audio, images, PDFs). Distinct from a "
|
|
24
|
-
instance of a
|
|
20
|
+
A person is a reusable persona scoped to a workspace.
|
|
21
|
+
\`ish person generate\` produces people from a written brief and/or sources
|
|
22
|
+
(transcripts, audio, images, PDFs). Distinct from a "participant" (\`pt-\`), which is one
|
|
23
|
+
instance of a person inside one iteration.
|
|
25
24
|
|
|
26
|
-
Concept pages: ish docs get-page concepts/
|
|
25
|
+
Concept pages: ish docs get-page concepts/person
|
|
27
26
|
ish docs get-page concepts/source
|
|
28
|
-
ish docs get-page concepts/
|
|
29
|
-
|
|
27
|
+
ish docs get-page concepts/people`);
|
|
28
|
+
person
|
|
30
29
|
.command("list")
|
|
31
|
-
.description("List
|
|
30
|
+
.description("List people (defaults to simulatable AI people)")
|
|
32
31
|
.option("--workspace <id>", "Filter by workspace ID")
|
|
33
|
-
.option("--search <query>", "Substring match against
|
|
34
|
-
.option("--bio <text>", "Substring match against
|
|
35
|
-
.option("--occupation <text>", "Substring match against
|
|
36
|
-
.option("--type <type>", "
|
|
32
|
+
.option("--search <query>", "Substring match against person name")
|
|
33
|
+
.option("--bio <text>", "Substring match against person bio")
|
|
34
|
+
.option("--occupation <text>", "Substring match against person occupation (repeatable)", collect, [])
|
|
35
|
+
.option("--type <type>", "Person type: ai, human, all (default: ai)", "ai")
|
|
37
36
|
.option("--gender <gender>", "Filter by gender (repeatable)", collect, [])
|
|
38
37
|
.option("--country <country>", "Filter by country code, e.g. US (repeatable)", collect, [])
|
|
39
38
|
.option("--min-age <n>", "Minimum age")
|
|
@@ -42,16 +41,16 @@ Concept pages: ish docs get-page concepts/profile
|
|
|
42
41
|
.option("--offset <n>", "Offset for pagination", "0")
|
|
43
42
|
.addHelpText("after", `
|
|
44
43
|
Examples:
|
|
45
|
-
$ ish
|
|
46
|
-
$ ish
|
|
47
|
-
$ ish
|
|
48
|
-
$ ish
|
|
49
|
-
$ ish
|
|
50
|
-
$ ish
|
|
44
|
+
$ ish person list
|
|
45
|
+
$ ish person list --search "engineer" --country US
|
|
46
|
+
$ ish person list --bio "voice-first user"
|
|
47
|
+
$ ish person list --occupation founder --occupation designer
|
|
48
|
+
$ ish person list --gender female --gender male --country US --country GB
|
|
49
|
+
$ ish person list --type all --json
|
|
51
50
|
|
|
52
51
|
# Pagination: default --limit is 50, iterate with --offset.
|
|
53
|
-
$ ish
|
|
54
|
-
$ ish
|
|
52
|
+
$ ish person list --limit 100
|
|
53
|
+
$ ish person list --limit 100 --offset 100 # next page
|
|
55
54
|
# When more results exist, a stderr hint surfaces the next --offset / --limit.`)
|
|
56
55
|
.action(async (opts, cmd) => {
|
|
57
56
|
await withClient(cmd, async (client, globals) => {
|
|
@@ -76,8 +75,8 @@ Examples:
|
|
|
76
75
|
params.min_age = opts.minAge;
|
|
77
76
|
if (opts.maxAge)
|
|
78
77
|
params.max_age = opts.maxAge;
|
|
79
|
-
const data = await client.get("/
|
|
80
|
-
|
|
78
|
+
const data = await client.get("/people", params);
|
|
79
|
+
formatPersonList(data, globals.json, parseInt(opts.limit, 10));
|
|
81
80
|
// Pattern H1: when there's more data, surface a stderr hint so agents
|
|
82
81
|
// know `--limit / --offset` exist without re-reading help. Stderr only,
|
|
83
82
|
// so it doesn't pollute machine-readable stdout — that means we DON'T
|
|
@@ -85,7 +84,7 @@ Examples:
|
|
|
85
84
|
// exactly when the agent needs it most). --quiet is the explicit
|
|
86
85
|
// opt-out for progress chatter.
|
|
87
86
|
//
|
|
88
|
-
// The raw `/
|
|
87
|
+
// The raw `/people` envelope is `{items, total, limit, offset}`
|
|
89
88
|
// — `has_more` and `returned` are synthesized client-side by `wrapList`
|
|
90
89
|
// only when the response is rendered. Compute them locally here so the
|
|
91
90
|
// hint actually fires; reading `data.has_more` directly would always
|
|
@@ -103,55 +102,61 @@ Examples:
|
|
|
103
102
|
}
|
|
104
103
|
});
|
|
105
104
|
});
|
|
106
|
-
|
|
105
|
+
person
|
|
107
106
|
.command("create")
|
|
108
|
-
.description("Create a
|
|
109
|
-
.requiredOption("--file <path>", "JSON file with
|
|
107
|
+
.description("Create a person from an exact JSON spec (no LLM)")
|
|
108
|
+
.requiredOption("--file <path>", "JSON file with person data")
|
|
110
109
|
.option("--workspace <id>", "Workspace (product) ID; falls back to active workspace")
|
|
111
|
-
.addHelpText("after", "\nExamples:\n $ ish
|
|
110
|
+
.addHelpText("after", "\nExamples:\n $ ish person create --file profile.json\n\n Expected JSON: { \"name\": \"...\", \"type\": \"ai\", \"gender\": \"female\", \"country\": \"US\", \"occupation\": \"...\", \"bio\": \"...\" }")
|
|
112
111
|
.action(async (opts, cmd) => {
|
|
113
112
|
await withClient(cmd, async (client, globals) => {
|
|
114
113
|
const body = await readJsonFileOrStdin(opts.file);
|
|
115
114
|
if (opts.workspace || body.product_id == null) {
|
|
116
115
|
body.product_id = resolveWorkspace(opts.workspace);
|
|
117
116
|
}
|
|
118
|
-
const data = await client.post("/
|
|
117
|
+
const data = await client.post("/people", body);
|
|
119
118
|
const result = data;
|
|
120
119
|
if (result.id)
|
|
121
|
-
result.alias = tagAlias(ALIAS_PREFIX.
|
|
120
|
+
result.alias = tagAlias(ALIAS_PREFIX.person, String(result.id));
|
|
122
121
|
output(result, globals.json, { writePath: true });
|
|
123
122
|
});
|
|
124
123
|
});
|
|
125
|
-
|
|
124
|
+
person
|
|
126
125
|
.command("generate")
|
|
127
|
-
.description("Generate
|
|
128
|
-
.option("--description <text>", "
|
|
126
|
+
.description("Generate people plus evidence-grounded scenarios from a brief and/or sources")
|
|
127
|
+
.option("--description <text>", "Description / researcher brief (use @path to read from file)")
|
|
129
128
|
.option("--description-file <path>", "Read description from a file")
|
|
130
129
|
.option("--source <id-or-path>", "Source UUID or local file path; auto-uploads paths (repeatable)", collect, [])
|
|
131
|
-
.option("--source-description <text>", "Per-source
|
|
130
|
+
.option("--source-description <text>", "Per-source researcher note — how the person reacted to THAT file. Paired with --source by index (repeatable). Only applies to local-path sources; for already-uploaded aliases the note is whatever was set at upload time.", collect, [])
|
|
132
131
|
.option("--diarize", "Apply speaker diarization to audio sources (silently ignored for text/image)")
|
|
133
|
-
.option("--count <n>", "Number of
|
|
134
|
-
.option("--propose-count", "Print the LLM's suggested
|
|
132
|
+
.option("--count <n>", "Number of people to generate (1-10). Omit to let the model propose a count")
|
|
133
|
+
.option("--propose-count", "Print the LLM's suggested count for a single processed source and exit (no generation)")
|
|
135
134
|
.option("--workspace <id>", "Workspace (product) ID; falls back to active workspace")
|
|
135
|
+
.option("--no-scenarios", "Skip fetching the evidence-grounded scenarios for each generated person")
|
|
136
136
|
.option("--no-wait", "Don't poll source-processing status. Only relevant when --source is a local path (paths get auto-uploaded and processed).")
|
|
137
|
-
.option("--timeout <seconds>", "Source-processing poll timeout in seconds. Only relevant when --source is a local path.", "300")
|
|
138
|
-
.option("--
|
|
137
|
+
.option("--source-timeout <seconds>", "Source-processing poll timeout in seconds. Only relevant when --source is a local path.", "300")
|
|
138
|
+
.option("--timeout <seconds>", "Generation-job poll timeout in seconds (default 600). The job keeps running server-side past this; re-poll, don't re-enqueue.", "600")
|
|
139
139
|
.addHelpText("after", `
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
Generation is an async job: ish enqueues it, polls until the people plus
|
|
141
|
+
their evidence-grounded scenarios are ready (~30-60s), then prints them. The
|
|
142
|
+
job reads your brief and any uploaded sources (transcripts, emails, PDFs,
|
|
143
|
+
audio, images) describing how real people reacted, and grounds each generated
|
|
144
|
+
scenario in those reactions. Provide --description (>=10 chars) and/or --source.
|
|
143
145
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
Examples:
|
|
147
|
+
# Generate 3 people from a description
|
|
148
|
+
$ ish person generate --description "Tech-savvy millennials in the US who use mobile banking" --count 3
|
|
146
149
|
|
|
147
|
-
#
|
|
148
|
-
$ ish
|
|
150
|
+
# Ground a person in how someone reacted to a real artifact
|
|
151
|
+
$ ish source upload ./proposal.eml --description "called this proposal lazy and vague"
|
|
152
|
+
# → ps-3a4
|
|
153
|
+
$ ish person generate --description "Skeptical enterprise buyer" --source ps-3a4 --count 1 --json
|
|
149
154
|
|
|
150
|
-
#
|
|
151
|
-
$ ish
|
|
155
|
+
# Inline source upload with a per-file researcher note
|
|
156
|
+
$ ish person generate --description "Voices behind support tickets" --source ./call.mp3 --source-description "frustrated about repeated transfers" --diarize --count 5
|
|
152
157
|
|
|
153
|
-
# Ask the LLM how many
|
|
154
|
-
$ ish
|
|
158
|
+
# Ask the LLM how many people a processed source warrants (no generation)
|
|
159
|
+
$ ish person generate --source ps-3a4 --propose-count`)
|
|
155
160
|
.action(async (opts, cmd) => {
|
|
156
161
|
await withClient(cmd, async (client, globals) => {
|
|
157
162
|
const productId = resolveWorkspace(opts.workspace);
|
|
@@ -167,9 +172,9 @@ Examples:
|
|
|
167
172
|
const raw = opts.source[0];
|
|
168
173
|
const id = isUuid(raw) ? raw : tryResolveSourceAlias(raw);
|
|
169
174
|
if (!id) {
|
|
170
|
-
throw new Error("--propose-count expects an uploaded source ID or alias (e.g.
|
|
175
|
+
throw new Error("--propose-count expects an uploaded source ID or alias (e.g. ps-3a4). Upload first with `ish source upload`.");
|
|
171
176
|
}
|
|
172
|
-
const data = await client.post(`/
|
|
177
|
+
const data = await client.post(`/people/attachments/${id}/propose-count`, undefined, { timeout: 60_000 });
|
|
173
178
|
output(data, globals.json);
|
|
174
179
|
return;
|
|
175
180
|
}
|
|
@@ -183,7 +188,24 @@ Examples:
|
|
|
183
188
|
if (!description && opts.source.length === 0) {
|
|
184
189
|
throw new Error("Provide --description, --description-file, or at least one --source.");
|
|
185
190
|
}
|
|
186
|
-
|
|
191
|
+
// The backend requires a description of >=10 chars when one is given.
|
|
192
|
+
// Catch it before uploading sources / enqueueing so the agent gets a
|
|
193
|
+
// fast, local validation error rather than a 422 mid-flight.
|
|
194
|
+
if (description !== undefined && description.trim().length > 0 && description.trim().length < 10) {
|
|
195
|
+
throw new Error("--description must be at least 10 characters when provided.");
|
|
196
|
+
}
|
|
197
|
+
let count;
|
|
198
|
+
if (opts.count) {
|
|
199
|
+
const n = parseInt(opts.count, 10);
|
|
200
|
+
if (Number.isNaN(n) || n < 1 || n > 10) {
|
|
201
|
+
throw new Error("--count must be an integer between 1 and 10.");
|
|
202
|
+
}
|
|
203
|
+
count = n;
|
|
204
|
+
}
|
|
205
|
+
// --timeout bounds the generation-job poll; --source-timeout bounds the
|
|
206
|
+
// (separate) source-processing poll that only runs for local-path sources.
|
|
207
|
+
const jobTimeoutMs = Math.max(1, parseInt(opts.timeout, 10)) * 1000;
|
|
208
|
+
const sourceTimeoutMs = Math.max(1, parseInt(opts.sourceTimeout, 10)) * 1000;
|
|
187
209
|
const wait = opts.wait !== false;
|
|
188
210
|
if (opts.sourceDescription.length > opts.source.length) {
|
|
189
211
|
throw new Error(`Got ${opts.sourceDescription.length} --source-description value(s) but only ${opts.source.length} --source value(s). Each --source-description is paired with --source by position; provide one --source per --source-description.`);
|
|
@@ -196,13 +218,13 @@ Examples:
|
|
|
196
218
|
// Treat as alias if it matches our prefix pattern.
|
|
197
219
|
const candidate = isUuid(raw) ? raw : tryResolveSourceAlias(raw);
|
|
198
220
|
if (candidate) {
|
|
199
|
-
// Already-uploaded source: the
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
// is the M3 bug — error out instead so the user knows.
|
|
221
|
+
// Already-uploaded source: the job request carries only
|
|
222
|
+
// source_upload_ids, no per-source description override. The
|
|
223
|
+
// researcher note set at upload time (the confirm `description`) is
|
|
224
|
+
// what the pipeline reads. Silently dropping a CLI-supplied note
|
|
225
|
+
// here is the M3 bug — error out instead so the user knows.
|
|
204
226
|
if (desc !== undefined) {
|
|
205
|
-
throw new Error(`--source-description for "${raw}" is ignored because that source is already uploaded — the
|
|
227
|
+
throw new Error(`--source-description for "${raw}" is ignored because that source is already uploaded — the note set at upload time is what generate uses.\n` +
|
|
206
228
|
"Either:\n" +
|
|
207
229
|
" - Drop --source-description for this source, OR\n" +
|
|
208
230
|
" - Re-upload via path: --source <local-path> --source-description \"...\"");
|
|
@@ -215,7 +237,7 @@ Examples:
|
|
|
215
237
|
description: desc,
|
|
216
238
|
diarize: opts.diarize,
|
|
217
239
|
wait,
|
|
218
|
-
timeoutMs,
|
|
240
|
+
timeoutMs: sourceTimeoutMs,
|
|
219
241
|
quiet: globals.quiet,
|
|
220
242
|
});
|
|
221
243
|
sourceIds.push(id);
|
|
@@ -227,48 +249,61 @@ Examples:
|
|
|
227
249
|
body.description = description;
|
|
228
250
|
if (sourceIds.length > 0)
|
|
229
251
|
body.source_upload_ids = sourceIds;
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
body.count = n;
|
|
236
|
-
}
|
|
237
|
-
// Pattern D1: emit stderr progress before the LLM call so agents (and
|
|
238
|
-
// humans) see something is happening during the ~10–20s wait. Mirrors
|
|
239
|
-
// the `--wait` ergonomics on `ask create` / `study run`.
|
|
252
|
+
if (count !== undefined)
|
|
253
|
+
body.count = count;
|
|
254
|
+
// Enqueue the agentic generation job. It turns the evidence bundle
|
|
255
|
+
// (brief + sources describing real reactions) into people plus
|
|
256
|
+
// scenarios grounded in those reactions — runs ~30-60s.
|
|
240
257
|
if (!globals.quiet) {
|
|
241
|
-
const target =
|
|
242
|
-
console.error(`
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const
|
|
258
|
+
const target = count ? `${count} person${count === 1 ? "" : "s"}` : "people";
|
|
259
|
+
console.error(` enqueuing generation job for ${target}...`);
|
|
260
|
+
}
|
|
261
|
+
const job = await client.post("/people/generation-jobs", body, { timeout: 60_000 });
|
|
262
|
+
const final = await pollGenerationJobUntilDone(client, job.id, {
|
|
263
|
+
timeoutMs: jobTimeoutMs,
|
|
264
|
+
quiet: globals.quiet,
|
|
265
|
+
});
|
|
266
|
+
if (final.status === "failed") {
|
|
267
|
+
throw new Error(`Generation job failed: ${final.error ?? "unknown error"}`);
|
|
268
|
+
}
|
|
269
|
+
const personIds = final.person_ids ?? [];
|
|
246
270
|
if (!globals.quiet) {
|
|
247
|
-
console.error(` generated ${
|
|
248
|
-
}
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
271
|
+
console.error(` generated ${personIds.length} person${personIds.length === 1 ? "" : "s"}`);
|
|
272
|
+
}
|
|
273
|
+
// Fetch each generated person (and, unless --no-scenarios, its
|
|
274
|
+
// evidence-grounded scenarios) so the output is the people
|
|
275
|
+
// themselves rather than bare ids.
|
|
276
|
+
const fetchScenarios = opts.scenarios !== false;
|
|
277
|
+
const people = await Promise.all(personIds.map(async (pid) => {
|
|
278
|
+
const person = await client.get(`/people/${pid}`);
|
|
279
|
+
const record = person;
|
|
280
|
+
record.alias = tagAlias(ALIAS_PREFIX.person, pid);
|
|
281
|
+
if (fetchScenarios) {
|
|
282
|
+
const scenarios = await client.get(`/people/${pid}/scenarios`);
|
|
283
|
+
record.scenarios = scenarios;
|
|
284
|
+
}
|
|
285
|
+
return record;
|
|
286
|
+
}));
|
|
287
|
+
if (globals.json) {
|
|
288
|
+
output({
|
|
289
|
+
job: { id: job.id, status: final.status, person_ids: personIds },
|
|
290
|
+
people,
|
|
291
|
+
}, true);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
formatPersonList(people, false);
|
|
260
295
|
});
|
|
261
296
|
});
|
|
262
|
-
|
|
297
|
+
person
|
|
263
298
|
.command("get")
|
|
264
|
-
.description("Get
|
|
265
|
-
.argument("<ids...>", "
|
|
299
|
+
.description("Get person details (accepts multiple IDs for batched lookup)")
|
|
300
|
+
.argument("<ids...>", "Person ID(s) — one or more aliases/UUIDs (space- or comma-separated)")
|
|
266
301
|
.addHelpText("after", `
|
|
267
302
|
Examples:
|
|
268
|
-
$ ish
|
|
269
|
-
$ ish
|
|
270
|
-
$ ish
|
|
271
|
-
$ ish
|
|
303
|
+
$ ish person get p-1b9
|
|
304
|
+
$ ish person get p-1b9 --json
|
|
305
|
+
$ ish person get p-1b9 p-fc1 p-2fc
|
|
306
|
+
$ ish person get p-1b9,p-fc1,p-2fc --fields alias,name,country,occupation
|
|
272
307
|
|
|
273
308
|
With multiple IDs, returns a {items:[...], total:N} envelope and uses the
|
|
274
309
|
list table layout in human mode. Use --fields to project per-item.`)
|
|
@@ -276,38 +311,38 @@ list table layout in human mode. Use --fields to project per-item.`)
|
|
|
276
311
|
await withClient(cmd, async (client, globals) => {
|
|
277
312
|
const flat = ids.flatMap((s) => s.split(",").map((x) => x.trim()).filter(Boolean));
|
|
278
313
|
if (flat.length === 0)
|
|
279
|
-
throw new Error("Provide at least one
|
|
314
|
+
throw new Error("Provide at least one person id.");
|
|
280
315
|
if (flat.length === 1) {
|
|
281
|
-
const data = await client.get(`/
|
|
316
|
+
const data = await client.get(`/people/${resolveId(flat[0])}`);
|
|
282
317
|
const result = data;
|
|
283
318
|
if (result.id)
|
|
284
|
-
result.alias = tagAlias(ALIAS_PREFIX.
|
|
319
|
+
result.alias = tagAlias(ALIAS_PREFIX.person, String(result.id));
|
|
285
320
|
output(result, globals.json);
|
|
286
321
|
return;
|
|
287
322
|
}
|
|
288
323
|
const results = await Promise.all(flat.map(async (raw) => {
|
|
289
|
-
const data = await client.get(`/
|
|
324
|
+
const data = await client.get(`/people/${resolveId(raw)}`);
|
|
290
325
|
const r = data;
|
|
291
326
|
if (r.id)
|
|
292
|
-
r.alias = tagAlias(ALIAS_PREFIX.
|
|
327
|
+
r.alias = tagAlias(ALIAS_PREFIX.person, String(r.id));
|
|
293
328
|
return r;
|
|
294
329
|
}));
|
|
295
330
|
if (globals.json) {
|
|
296
331
|
output({ items: results, total: results.length }, true);
|
|
297
332
|
}
|
|
298
333
|
else {
|
|
299
|
-
|
|
334
|
+
formatPersonList(results, false);
|
|
300
335
|
}
|
|
301
336
|
});
|
|
302
337
|
});
|
|
303
|
-
|
|
338
|
+
person
|
|
304
339
|
.command("update")
|
|
305
|
-
.description("Update a
|
|
306
|
-
.argument("<id>", "
|
|
340
|
+
.description("Update a person")
|
|
341
|
+
.argument("<id>", "Person ID")
|
|
307
342
|
.option("--file <path>", "JSON file with update data (escape hatch for fields not covered by inline flags)")
|
|
308
|
-
.option("--name <text>", "
|
|
309
|
-
.option("--description <text>", "
|
|
310
|
-
.option("--bio <text>", "
|
|
343
|
+
.option("--name <text>", "Person name")
|
|
344
|
+
.option("--description <text>", "Person description")
|
|
345
|
+
.option("--bio <text>", "Person bio")
|
|
311
346
|
.option("--occupation <text>", "Occupation")
|
|
312
347
|
.option("--country <code>", "Country code, e.g. US")
|
|
313
348
|
.option("--gender <g>", "Gender, e.g. female")
|
|
@@ -320,13 +355,13 @@ list table layout in human mode. Use --fields to project per-item.`)
|
|
|
320
355
|
.option("--accessibility-profile <json-or-path>", "AccessibilityProfile v1.0 as an inline JSON string OR a path to a JSON file. Empty object {} is the canonical default. Validated client-side against the spec before submit.")
|
|
321
356
|
.addHelpText("after", `
|
|
322
357
|
Examples:
|
|
323
|
-
$ ish
|
|
324
|
-
$ ish
|
|
325
|
-
$ ish
|
|
326
|
-
$ ish
|
|
327
|
-
$ ish
|
|
328
|
-
$ ish
|
|
329
|
-
$ ish
|
|
358
|
+
$ ish person update <id> --bio "Edited bio"
|
|
359
|
+
$ ish person update <id> --name "Alice" --country US --education-level bachelor
|
|
360
|
+
$ ish person update <id> --household couple_with_kids --locale-type suburban
|
|
361
|
+
$ ish person update <id> --income-level middle --employment-status employed_full_time
|
|
362
|
+
$ ish person update <id> --accessibility-profile '{"version":"1.0","visual":{"uses_screen_reader":true,"text_size":"large"},"cognitive":{"reduce_motion":true},"assistive_tech":["VoiceOver"]}'
|
|
363
|
+
$ ish person update <id> --accessibility-profile ./a11y.json
|
|
364
|
+
$ ish person update <id> --file updates.json
|
|
330
365
|
|
|
331
366
|
Inline flags compose into the patch body. --file is an escape hatch when you
|
|
332
367
|
need fields not covered by the inline flags. When both are provided, inline
|
|
@@ -381,31 +416,31 @@ Schema: https://ishlabs.io/spec/accessibility-profile-schema.v1.json`)
|
|
|
381
416
|
if (Object.keys(body).length === 0) {
|
|
382
417
|
throw new Error("Nothing to update. Provide --file or at least one inline flag (e.g. --bio).");
|
|
383
418
|
}
|
|
384
|
-
const data = await client.put(`/
|
|
419
|
+
const data = await client.put(`/people/${resolveId(id)}`, body);
|
|
385
420
|
const result = data;
|
|
386
421
|
if (result.id)
|
|
387
|
-
result.alias = tagAlias(ALIAS_PREFIX.
|
|
422
|
+
result.alias = tagAlias(ALIAS_PREFIX.person, String(result.id));
|
|
388
423
|
output(result, globals.json, { writePath: true });
|
|
389
424
|
});
|
|
390
425
|
});
|
|
391
|
-
|
|
426
|
+
person
|
|
392
427
|
.command("delete")
|
|
393
|
-
.description("Delete a
|
|
394
|
-
.argument("<id>", "
|
|
395
|
-
.option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from the
|
|
396
|
-
.addHelpText("after", "\nExamples:\n $ ish
|
|
428
|
+
.description("Delete a person")
|
|
429
|
+
.argument("<id>", "Person ID")
|
|
430
|
+
.option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from the person)")
|
|
431
|
+
.addHelpText("after", "\nExamples:\n $ ish person delete <id>")
|
|
397
432
|
.action(async (id, _opts, cmd) => {
|
|
398
433
|
await withClient(cmd, async (client, globals) => {
|
|
399
434
|
const rid = resolveId(id);
|
|
400
|
-
await client.del(`/
|
|
401
|
-
output({ id: rid, alias: tagAlias(ALIAS_PREFIX.
|
|
435
|
+
await client.del(`/people/${rid}`);
|
|
436
|
+
output({ id: rid, alias: tagAlias(ALIAS_PREFIX.person, rid), message: "Profile deleted" }, globals.json, { writePath: true });
|
|
402
437
|
});
|
|
403
438
|
});
|
|
404
|
-
|
|
439
|
+
person
|
|
405
440
|
.command("suggest-scenarios")
|
|
406
|
-
.description("Ask the LLM for scenario probes to craft a specific simulated
|
|
441
|
+
.description("Ask the LLM for scenario probes to craft a specific simulated person")
|
|
407
442
|
.option("--workspace <id>", "Workspace (product) ID; falls back to active workspace")
|
|
408
|
-
.option("--context <text>", "What you already know about this
|
|
443
|
+
.option("--context <text>", "What you already know about this participant. Use @path to read from file.")
|
|
409
444
|
.option("--context-file <path>", "Read --context from a file")
|
|
410
445
|
.option("--count <n>", "Number of scenarios to return (1-10, default 5)")
|
|
411
446
|
.option("--previous-answers <json-or-@path>", "Answers already collected this session. Inline JSON, @/path/to.json, or - for stdin. Array of {type, prompt, answer}; max 40.")
|
|
@@ -413,23 +448,23 @@ Schema: https://ishlabs.io/spec/accessibility-profile-schema.v1.json`)
|
|
|
413
448
|
.addHelpText("after", `
|
|
414
449
|
Examples:
|
|
415
450
|
# Bare invocation: 5 scenarios from a free-form context blob
|
|
416
|
-
$ ish
|
|
451
|
+
$ ish person suggest-scenarios --context "Mid-career engineer who handles oncall for a Stripe-using fintech"
|
|
417
452
|
|
|
418
453
|
# Load context from a file, ask for 3 scenarios
|
|
419
|
-
$ ish
|
|
454
|
+
$ ish person suggest-scenarios --context-file ./persona-notes.md --count 3
|
|
420
455
|
|
|
421
456
|
# Follow-up probe: skip prompts already shown, build on prior answers
|
|
422
|
-
$ ish
|
|
457
|
+
$ ish person suggest-scenarios \\
|
|
423
458
|
--context "$(cat notes.md)" \\
|
|
424
459
|
--count 3 \\
|
|
425
460
|
--already-surfaced '["How do you triage 02:00 pages?"]' \\
|
|
426
461
|
--previous-answers @./answers.json
|
|
427
462
|
|
|
428
463
|
# Capture just the first scenario's type
|
|
429
|
-
$ ish
|
|
464
|
+
$ ish person suggest-scenarios --context "..." --count 1 --get scenarios[0].type
|
|
430
465
|
|
|
431
|
-
The loop: suggest → answer locally → persist via \`ish
|
|
432
|
-
See \`ish docs get-page guides/build-specific-
|
|
466
|
+
The loop: suggest → answer locally → persist via \`ish person evidence add <id>\` (read back with \`evidence list\`).
|
|
467
|
+
See \`ish docs get-page guides/build-specific-participant\` for the full workflow.`)
|
|
433
468
|
.action(async (opts, cmd) => {
|
|
434
469
|
await withClient(cmd, async (client, globals) => {
|
|
435
470
|
const productId = resolveWorkspace(opts.workspace);
|
|
@@ -493,44 +528,44 @@ See \`ish docs get-page guides/build-specific-tester\` for the full workflow.`)
|
|
|
493
528
|
const target = body.count ?? 5;
|
|
494
529
|
console.error(` suggesting ${target} scenario${target === 1 ? "" : "s"}...`);
|
|
495
530
|
}
|
|
496
|
-
const data = await client.post("/
|
|
531
|
+
const data = await client.post("/people/suggest-scenarios", body, { timeout: 180_000 });
|
|
497
532
|
output(data, globals.json);
|
|
498
533
|
});
|
|
499
534
|
});
|
|
500
|
-
const evidence =
|
|
535
|
+
const evidence = person
|
|
501
536
|
.command("evidence")
|
|
502
|
-
.description("Manage scenario-answer evidence on a
|
|
537
|
+
.description("Manage scenario-answer evidence on a person")
|
|
503
538
|
.addHelpText("after", `
|
|
504
539
|
Evidence rows persist answers to \`suggest-scenarios\` probes onto a
|
|
505
|
-
specific
|
|
540
|
+
specific person. The \`source\` field on every trace is the same enum
|
|
506
541
|
as the \`type\` field on a suggested scenario — copy verbatim when
|
|
507
542
|
building a traces.json.
|
|
508
543
|
|
|
509
|
-
Guide: ish docs get-page guides/build-specific-
|
|
544
|
+
Guide: ish docs get-page guides/build-specific-participant`);
|
|
510
545
|
evidence
|
|
511
546
|
.command("add")
|
|
512
|
-
.description("Persist scenario answers as structured evidence on a
|
|
513
|
-
.argument("<id>", "
|
|
547
|
+
.description("Persist scenario answers as structured evidence on a person")
|
|
548
|
+
.argument("<id>", "Person ID (alias or UUID)")
|
|
514
549
|
.option("--traces <json-or-@path>", `Array of {text, source, scenario_prompt?, raw_response?} where source ∈ ${EVIDENCE_SOURCES.join("|")}. Inline JSON, @/path/to.json, or - for stdin.`)
|
|
515
550
|
.option("--traces-file <path>", "Read --traces from a JSON file")
|
|
516
551
|
.addHelpText("after", `
|
|
517
552
|
Examples:
|
|
518
553
|
# Inline JSON for a single trace
|
|
519
|
-
$ ish
|
|
554
|
+
$ ish person evidence add p-d4e --traces '[{"text":"I would page my staff engineer first.","source":"situation","scenario_prompt":"PagerDuty fires at 02:00."}]'
|
|
520
555
|
|
|
521
556
|
# From a file
|
|
522
|
-
$ ish
|
|
557
|
+
$ ish person evidence add p-d4e --traces-file ./answers.json
|
|
523
558
|
|
|
524
559
|
# From stdin (pipe-friendly)
|
|
525
|
-
$ jq -c '.traces' session.json | ish
|
|
560
|
+
$ jq -c '.traces' session.json | ish person evidence add p-d4e --traces -
|
|
526
561
|
|
|
527
562
|
# Project the response
|
|
528
|
-
$ ish
|
|
563
|
+
$ ish person evidence add p-d4e --traces-file ./answers.json --fields id,source,created_at
|
|
529
564
|
|
|
530
565
|
Valid source values: ${EVIDENCE_SOURCES.join(", ")}.
|
|
531
566
|
\`source\` on a trace = \`type\` on a suggested scenario — same enum.
|
|
532
|
-
Pair with \`ish
|
|
533
|
-
Verify with \`ish
|
|
567
|
+
Pair with \`ish person suggest-scenarios\` to drive the iterative probe → answer loop.
|
|
568
|
+
Verify with \`ish person evidence list <id>\`.`)
|
|
534
569
|
.action(async (id, opts, cmd) => {
|
|
535
570
|
await withClient(cmd, async (client, globals) => {
|
|
536
571
|
if (opts.traces && opts.tracesFile) {
|
|
@@ -571,9 +606,9 @@ Verify with \`ish profile evidence list <id>\`.`)
|
|
|
571
606
|
const rid = resolveId(id);
|
|
572
607
|
const body = { traces };
|
|
573
608
|
if (!globals.quiet) {
|
|
574
|
-
console.error(` persisting ${traces.length} evidence trace${traces.length === 1 ? "" : "s"} on ${tagAlias(ALIAS_PREFIX.
|
|
609
|
+
console.error(` persisting ${traces.length} evidence trace${traces.length === 1 ? "" : "s"} on ${tagAlias(ALIAS_PREFIX.person, rid)}...`);
|
|
575
610
|
}
|
|
576
|
-
const data = await client.post(`/
|
|
611
|
+
const data = await client.post(`/people/${rid}/scenarios`, body);
|
|
577
612
|
if (globals.json) {
|
|
578
613
|
output({ items: data, total: data.length }, true);
|
|
579
614
|
}
|
|
@@ -584,24 +619,24 @@ Verify with \`ish profile evidence list <id>\`.`)
|
|
|
584
619
|
});
|
|
585
620
|
evidence
|
|
586
621
|
.command("list")
|
|
587
|
-
.description("List evidence traces persisted on a
|
|
588
|
-
.argument("<id>", "
|
|
622
|
+
.description("List evidence traces persisted on a person (newest first)")
|
|
623
|
+
.argument("<id>", "Person ID (alias or UUID)")
|
|
589
624
|
.addHelpText("after", `
|
|
590
625
|
Examples:
|
|
591
|
-
# Read back every trace on a
|
|
592
|
-
$ ish
|
|
626
|
+
# Read back every trace on a person
|
|
627
|
+
$ ish person evidence list p-d4e
|
|
593
628
|
|
|
594
629
|
# Project per-row fields
|
|
595
|
-
$ ish
|
|
630
|
+
$ ish person evidence list p-d4e --fields id,source,scenario_prompt
|
|
596
631
|
|
|
597
632
|
# Capture just the sources, one per line (auto-descends into items)
|
|
598
|
-
$ ish
|
|
633
|
+
$ ish person evidence list p-d4e --get source
|
|
599
634
|
|
|
600
635
|
Returns a {items, total} envelope. Use the same id you passed to \`evidence add\`.`)
|
|
601
636
|
.action(async (id, _opts, cmd) => {
|
|
602
637
|
await withClient(cmd, async (client, globals) => {
|
|
603
638
|
const rid = resolveId(id);
|
|
604
|
-
const data = await client.get(`/
|
|
639
|
+
const data = await client.get(`/people/${rid}/scenarios`);
|
|
605
640
|
if (globals.json) {
|
|
606
641
|
output({ items: data, total: data.length }, true);
|
|
607
642
|
}
|
|
@@ -639,12 +674,12 @@ async function parseJsonFlag(value, flagName) {
|
|
|
639
674
|
throw new Error(`${flagName} expects inline JSON (starting with { or [), an @path reference (@/path/to/file.json), or '-' for stdin.`);
|
|
640
675
|
}
|
|
641
676
|
/**
|
|
642
|
-
* If the value matches the
|
|
677
|
+
* If the value matches the personSource alias pattern (e.g. "ps-3a4"),
|
|
643
678
|
* resolve it to the underlying UUID. Otherwise return undefined so the caller
|
|
644
679
|
* falls back to treating the value as a path.
|
|
645
680
|
*/
|
|
646
681
|
function tryResolveSourceAlias(value) {
|
|
647
|
-
if (!/^
|
|
682
|
+
if (!/^ps-[0-9a-f]{3,}$/.test(value))
|
|
648
683
|
return undefined;
|
|
649
684
|
try {
|
|
650
685
|
return resolveId(value);
|