@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
package/dist/commands/study.js
CHANGED
|
@@ -8,13 +8,13 @@ import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
|
8
8
|
import { loadConfig, saveConfig } from "../config.js";
|
|
9
9
|
import { formatStudyList, formatStudyDetail, formatStudyResults, buildStudyResultsSummary, buildChatTranscript, output, ValidationError, } from "../lib/output.js";
|
|
10
10
|
import { VALID_CONTENT_TYPES } from "../lib/types.js";
|
|
11
|
-
import { parseAssignment, loadAssignmentsFile, parseQuestion } from "../lib/study-inputs.js";
|
|
11
|
+
import { parseAssignment, loadAssignmentsFile, validateAssignmentsArray, parseQuestion } from "../lib/study-inputs.js";
|
|
12
12
|
import { loadQuestionsManifest } from "../lib/ask-questions.js";
|
|
13
13
|
import { isLocalPath } from "../lib/upload.js";
|
|
14
14
|
import { normalizeChatMode, validateRoleCriteria } from "../lib/modality.js";
|
|
15
15
|
import { normalizeEnumValue, SCREEN_FORMATS } from "../lib/enums.js";
|
|
16
16
|
import { attachStudyRunCommands } from "./study-run.js";
|
|
17
|
-
import {
|
|
17
|
+
import { attachStudyParticipantCommands } from "./study-participant.js";
|
|
18
18
|
import { attachStudyAnalyzeCommands } from "./study-analyze.js";
|
|
19
19
|
import { attachStudyScreenshotsCommands } from "./study-screenshots.js";
|
|
20
20
|
function collectRepeatable(value, prev = []) {
|
|
@@ -51,12 +51,14 @@ function resolveAssignments(opts) {
|
|
|
51
51
|
return loadAssignmentsFile(opts.assignmentsFile);
|
|
52
52
|
}
|
|
53
53
|
if (opts.assignments) {
|
|
54
|
+
let parsed;
|
|
54
55
|
try {
|
|
55
|
-
|
|
56
|
+
parsed = JSON.parse(opts.assignments);
|
|
56
57
|
}
|
|
57
58
|
catch {
|
|
58
59
|
throw new Error("Invalid --assignments JSON");
|
|
59
60
|
}
|
|
61
|
+
return validateAssignmentsArray(parsed, "--assignments");
|
|
60
62
|
}
|
|
61
63
|
return undefined;
|
|
62
64
|
}
|
|
@@ -120,19 +122,19 @@ Concept pages: ish docs get-page concepts/study
|
|
|
120
122
|
.option("--title <title>", "Content title (text + media modalities — image, video, audio, document; optional). Not used for interactive / chat.")
|
|
121
123
|
.option("--endpoint <id>", "Saved chatbot endpoint id or alias. Creates iteration A inline (chat modality, external_chatbot mode)")
|
|
122
124
|
.option("--endpoint-config <file>", "ChatbotEndpointConfig JSON file (or `-` for stdin); embedded directly. Mutually exclusive with --endpoint (chat modality, external_chatbot mode)")
|
|
123
|
-
.option("--max-turns <n>", "Maximum conversation turns per
|
|
124
|
-
.option("--chat-mode <mode>", "Chat mode: external_chatbot (default) or
|
|
125
|
-
.option("--
|
|
126
|
-
.option("--
|
|
127
|
-
.option("--scenario-a <text-or-@file>", "Side-A scenario + goal — chat
|
|
128
|
-
.option("--scenario-b <text-or-@file>", "Side-B scenario + goal — chat
|
|
129
|
-
.option("--initiator-side <a|b>", "Which side speaks first (default: a) — chat
|
|
130
|
-
.option("--role-criteria-a <json-or-@file>", 'RoleCriteria filter for side A (JSON object or @filepath). Keys: occupation[], min_age, max_age, gender[], country[], education_level_in[], household_in[], locale_type_in[], income_level_in[], employment_status_in[], requires_captions, uses_screen_reader, prefers_reduced_motion, prefers_high_contrast, has_any_accessibility_need. The five *_in arrays accept snake_case spec values; the five accessibility filters are booleans. Use INSTEAD of --
|
|
131
|
-
.option("--role-criteria-b <json-or-@file>", "RoleCriteria filter for side B — same shape as --role-criteria-a. chat
|
|
125
|
+
.option("--max-turns <n>", "Maximum conversation turns per participant (chat modality only; default 12)", (v) => Number(v))
|
|
126
|
+
.option("--chat-mode <mode>", "Chat mode: external_chatbot (default) or participant_pair (two AI groups talk to each other) — chat modality only")
|
|
127
|
+
.option("--group-a <ids>", "Person IDs/aliases for group A (comma-separated or repeatable). Pass a single profile and N on --group-b to broadcast (1×N rehearsal: fix side A, vary side B) — chat participant_pair mode", (value, prev = []) => prev.concat(value.split(",").map((s) => s.trim()).filter(Boolean)), [])
|
|
128
|
+
.option("--group-b <ids>", "Person IDs/aliases for group B. When both sides are explicit they must be equal length, BUT if either side is a singleton it's auto-broadcast to match the other (1×N rehearsal) — chat participant_pair mode", (value, prev = []) => prev.concat(value.split(",").map((s) => s.trim()).filter(Boolean)), [])
|
|
129
|
+
.option("--scenario-a <text-or-@file>", "Side-A scenario + goal — chat participant_pair mode")
|
|
130
|
+
.option("--scenario-b <text-or-@file>", "Side-B scenario + goal — chat participant_pair mode")
|
|
131
|
+
.option("--initiator-side <a|b>", "Which side speaks first (default: a) — chat participant_pair mode")
|
|
132
|
+
.option("--role-criteria-a <json-or-@file>", 'RoleCriteria filter for side A (JSON object or @filepath). Keys: occupation[], min_age, max_age, gender[], country[], education_level_in[], household_in[], locale_type_in[], income_level_in[], employment_status_in[], requires_captions, uses_screen_reader, prefers_reduced_motion, prefers_high_contrast, has_any_accessibility_need. The five *_in arrays accept snake_case spec values; the five accessibility filters are booleans. Use INSTEAD of --group-a or alongside it. chat participant_pair mode.')
|
|
133
|
+
.option("--role-criteria-b <json-or-@file>", "RoleCriteria filter for side B — same shape as --role-criteria-a. chat participant_pair mode.")
|
|
132
134
|
.addHelpText("after", `
|
|
133
135
|
Note: --workspace is optional if set via \`ish workspace use <alias>\`.
|
|
134
136
|
|
|
135
|
-
The questionnaire is the set of questions
|
|
137
|
+
The questionnaire is the set of questions participants answer. Use \`--question\` to
|
|
136
138
|
quickly add simple text questions, or \`--questionnaire <file.json>\` for richer
|
|
137
139
|
types (slider, likert, single-choice, multiple-choice, number) and custom
|
|
138
140
|
timing. The two forms are mutually exclusive — pick one.
|
|
@@ -187,6 +189,23 @@ Examples:
|
|
|
187
189
|
$ ish study create --name "Newsletter" --modality text --content-type email \\
|
|
188
190
|
--assignments-file ./assignments.json
|
|
189
191
|
|
|
192
|
+
# Interactive study whose assignment carries a step-by-step checklist
|
|
193
|
+
# (JSON only — the inline --assignment shorthand can't express steps).
|
|
194
|
+
# Steps are honored for interactive + external_chatbot chat modalities only;
|
|
195
|
+
# the backend rejects them for media (text/video/audio/image/document) and
|
|
196
|
+
# for chat participant_pair. After a run, study get reports per-step completion.
|
|
197
|
+
$ ish study create --name "Checkout" --modality interactive \\
|
|
198
|
+
--assignments-file ./assignments.json
|
|
199
|
+
# assignments.json:
|
|
200
|
+
# [
|
|
201
|
+
# { "name": "Buy", "instructions": "Add to cart and check out",
|
|
202
|
+
# "steps": [
|
|
203
|
+
# { "name": "Find a product", "description": "Browse to any item" },
|
|
204
|
+
# { "name": "Add to cart" },
|
|
205
|
+
# { "name": "Complete checkout" }
|
|
206
|
+
# ] }
|
|
207
|
+
# ]
|
|
208
|
+
|
|
190
209
|
# Chat study targeting a saved chatbot endpoint:
|
|
191
210
|
$ ish study create --name "Onboarding bot" --modality chat \\
|
|
192
211
|
--endpoint ep-abc \\
|
|
@@ -213,7 +232,7 @@ Tips:
|
|
|
213
232
|
\`--fields a,b,c\` to project the JSON output to listed fields.
|
|
214
233
|
|
|
215
234
|
Next: configure a run with \`ish iteration create --study <id>\`,
|
|
216
|
-
then dispatch with \`ish study run\` (
|
|
235
|
+
then dispatch with \`ish study run\` (people are selected on run, not create).`)
|
|
217
236
|
.action(async (opts, cmd) => {
|
|
218
237
|
await withClient(cmd, async (client, globals) => {
|
|
219
238
|
const assignments = resolveAssignments(opts);
|
|
@@ -246,23 +265,23 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
246
265
|
// against the freshly-created study.
|
|
247
266
|
const normalizedChatMode = normalizeChatMode(opts.chatMode);
|
|
248
267
|
if (opts.chatMode !== undefined && normalizedChatMode === null) {
|
|
249
|
-
throw new ValidationError(`Invalid --chat-mode "${opts.chatMode}". Expected "external_chatbot" or "
|
|
268
|
+
throw new ValidationError(`Invalid --chat-mode "${opts.chatMode}". Expected "external_chatbot" or "participant_pair" (hyphenated variants accepted).`, ["external_chatbot", "participant_pair"]);
|
|
250
269
|
}
|
|
251
|
-
const pairFlagsSet = (opts.
|
|
252
|
-
|| (opts.
|
|
270
|
+
const pairFlagsSet = (opts.groupA && opts.groupA.length > 0)
|
|
271
|
+
|| (opts.groupB && opts.groupB.length > 0)
|
|
253
272
|
|| opts.scenarioA !== undefined
|
|
254
273
|
|| opts.scenarioB !== undefined
|
|
255
274
|
|| opts.initiatorSide !== undefined
|
|
256
275
|
|| opts.roleCriteriaA !== undefined
|
|
257
276
|
|| opts.roleCriteriaB !== undefined
|
|
258
|
-
|| normalizedChatMode === "
|
|
277
|
+
|| normalizedChatMode === "participant_pair";
|
|
259
278
|
const inlineMediaFlagsSet = [
|
|
260
279
|
opts.contentText !== undefined ? "--content-text" : null,
|
|
261
280
|
opts.url !== undefined ? "--url" : null,
|
|
262
281
|
opts.contentUrl !== undefined ? "--content-url" : null,
|
|
263
282
|
opts.imageUrls !== undefined ? "--image-urls" : null,
|
|
264
283
|
(opts.endpoint !== undefined || opts.endpointConfig !== undefined) ? "--endpoint/--endpoint-config" : null,
|
|
265
|
-
pairFlagsSet ? "--chat-mode
|
|
284
|
+
pairFlagsSet ? "--chat-mode participant_pair (with --group-a/-b or --role-criteria-a/-b plus --scenario-a/-b)" : null,
|
|
266
285
|
].filter((f) => f !== null);
|
|
267
286
|
if (inlineMediaFlagsSet.length > 1) {
|
|
268
287
|
throw new ValidationError(`Pass only one inline-iteration flag: ${inlineMediaFlagsSet.join(", ")}.`, inlineMediaFlagsSet);
|
|
@@ -367,7 +386,7 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
367
386
|
throw new ValidationError(`--endpoint / --endpoint-config require --modality chat (got "${opts.modality}").`, ["chat"]);
|
|
368
387
|
}
|
|
369
388
|
if (normalizedChatMode && normalizedChatMode !== "external_chatbot") {
|
|
370
|
-
throw new ValidationError(`--endpoint / --endpoint-config are only valid with --chat-mode external_chatbot (got "${opts.chatMode}"). For
|
|
389
|
+
throw new ValidationError(`--endpoint / --endpoint-config are only valid with --chat-mode external_chatbot (got "${opts.chatMode}"). For participant_pair use --group-a/-b and --scenario-a/-b.`, ["external_chatbot"]);
|
|
371
390
|
}
|
|
372
391
|
let endpointConfig;
|
|
373
392
|
if (opts.endpoint !== undefined) {
|
|
@@ -389,7 +408,7 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
389
408
|
throw new Error("Invalid --endpoint-config JSON.");
|
|
390
409
|
}
|
|
391
410
|
}
|
|
392
|
-
const maxTurns = opts.maxTurns ??
|
|
411
|
+
const maxTurns = opts.maxTurns ?? 14;
|
|
393
412
|
inlineIteration = {
|
|
394
413
|
name: "A",
|
|
395
414
|
details: {
|
|
@@ -406,13 +425,13 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
406
425
|
}
|
|
407
426
|
else if (pairFlagsSet) {
|
|
408
427
|
if (opts.modality && opts.modality !== "chat") {
|
|
409
|
-
throw new ValidationError(`--chat-mode
|
|
428
|
+
throw new ValidationError(`--chat-mode participant_pair (with --group-a/-b or --role-criteria-a/-b plus --scenario-a/-b) requires --modality chat (got "${opts.modality}").`, ["chat"]);
|
|
410
429
|
}
|
|
411
|
-
if (normalizedChatMode && normalizedChatMode !== "
|
|
412
|
-
throw new ValidationError(`--
|
|
430
|
+
if (normalizedChatMode && normalizedChatMode !== "participant_pair") {
|
|
431
|
+
throw new ValidationError(`--group-a/-b or --role-criteria-a/-b imply --chat-mode participant_pair (got "${opts.chatMode}").`, ["participant_pair"]);
|
|
413
432
|
}
|
|
414
|
-
const audA = (opts.
|
|
415
|
-
const audB = (opts.
|
|
433
|
+
const audA = (opts.groupA ?? []).map(resolveId);
|
|
434
|
+
const audB = (opts.groupB ?? []).map(resolveId);
|
|
416
435
|
// Parse + validate role criteria if supplied (JSON or @filepath).
|
|
417
436
|
const parseCriteria = (raw, flag) => {
|
|
418
437
|
if (raw === undefined)
|
|
@@ -445,21 +464,21 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
445
464
|
const sideAHasInput = audA.length > 0 || !!critA;
|
|
446
465
|
const sideBHasInput = audB.length > 0 || !!critB;
|
|
447
466
|
if (!sideAHasInput || !sideBHasInput) {
|
|
448
|
-
throw new Error("
|
|
467
|
+
throw new Error("participant_pair chat iterations require, for each side, either explicit people (--group-a / --group-b) or a role-criteria filter (--role-criteria-a / --role-criteria-b).");
|
|
449
468
|
}
|
|
450
469
|
// 1×N broadcast: canonical "rehearse one side against N
|
|
451
470
|
// variations" shape. See iteration.ts buildIterationDetails
|
|
452
|
-
//
|
|
471
|
+
// participant_pair arm for the rationale.
|
|
453
472
|
let audA_final = audA;
|
|
454
473
|
let audB_final = audB;
|
|
455
474
|
let broadcastMsg;
|
|
456
475
|
if (audA.length === 1 && audB.length > 1 && !critA && !critB) {
|
|
457
476
|
audA_final = Array(audB.length).fill(audA[0]);
|
|
458
|
-
broadcastMsg = `Broadcasting --
|
|
477
|
+
broadcastMsg = `Broadcasting --group-a (1 profile) to length ${audB.length} to match --group-b — same side-A profile across all ${audB.length} conversations.`;
|
|
459
478
|
}
|
|
460
479
|
else if (audB.length === 1 && audA.length > 1 && !critA && !critB) {
|
|
461
480
|
audB_final = Array(audA.length).fill(audB[0]);
|
|
462
|
-
broadcastMsg = `Broadcasting --
|
|
481
|
+
broadcastMsg = `Broadcasting --group-b (1 profile) to length ${audA.length} to match --group-a — same side-B profile across all ${audA.length} conversations.`;
|
|
463
482
|
}
|
|
464
483
|
if (broadcastMsg) {
|
|
465
484
|
console.error(broadcastMsg);
|
|
@@ -469,13 +488,13 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
469
488
|
// CLI's 1×N broadcast (above) already cloned the singleton side,
|
|
470
489
|
// so this branch only fires when both sides ship >1 with
|
|
471
490
|
// mismatched counts. Server rejects the same way.
|
|
472
|
-
throw new ValidationError(`--
|
|
491
|
+
throw new ValidationError(`--group-a (${audA_final.length}) and --group-b (${audB_final.length}) cannot be paired. ` +
|
|
473
492
|
`Pick the same number on each side (1:1 by index), or pass exactly one profile on one side to broadcast ` +
|
|
474
|
-
`(e.g. --
|
|
475
|
-
`or use --role-criteria-a/-b on either side to let the backend resolve the pool.`, ["--
|
|
493
|
+
`(e.g. --group-a p-rep --group-b p-cto1,p-cto2,p-cto3), ` +
|
|
494
|
+
`or use --role-criteria-a/-b on either side to let the backend resolve the pool.`, ["--group-a", "--group-b"]);
|
|
476
495
|
}
|
|
477
496
|
if (!opts.scenarioA || !opts.scenarioB) {
|
|
478
|
-
throw new Error("
|
|
497
|
+
throw new Error("participant_pair chat iterations require --scenario-a <text-or-@file> and --scenario-b <text-or-@file>.");
|
|
479
498
|
}
|
|
480
499
|
const scenarioA = opts.scenarioA.startsWith("@")
|
|
481
500
|
? readFileSync(opts.scenarioA.slice(1), "utf8")
|
|
@@ -490,15 +509,15 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
490
509
|
if (initiator !== "a" && initiator !== "b") {
|
|
491
510
|
throw new ValidationError(`Invalid --initiator-side "${opts.initiatorSide}". Expected "a" or "b".`, ["a", "b"]);
|
|
492
511
|
}
|
|
493
|
-
const maxTurns = opts.maxTurns ??
|
|
512
|
+
const maxTurns = opts.maxTurns ?? 14;
|
|
494
513
|
inlineIteration = {
|
|
495
514
|
name: "A",
|
|
496
515
|
details: {
|
|
497
516
|
type: "chat",
|
|
498
517
|
mode_details: {
|
|
499
|
-
mode: "
|
|
500
|
-
|
|
501
|
-
|
|
518
|
+
mode: "participant_pair",
|
|
519
|
+
group_a: audA_final,
|
|
520
|
+
group_b: audB_final,
|
|
502
521
|
scenario_a: scenarioA,
|
|
503
522
|
scenario_b: scenarioB,
|
|
504
523
|
initiator_side: initiator,
|
|
@@ -632,84 +651,84 @@ list table layout in human mode.`)
|
|
|
632
651
|
});
|
|
633
652
|
study
|
|
634
653
|
.command("results")
|
|
635
|
-
.description("View aggregated results:
|
|
654
|
+
.description("View aggregated results: participant counts, sentiment, interview answers. Returns a stable envelope with empty fields when no runs have completed.")
|
|
636
655
|
.argument("<id>", "Study ID")
|
|
637
656
|
.option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from the study)")
|
|
638
|
-
.option("--summary", "Lean summary projection: counts + sentiment + per-
|
|
657
|
+
.option("--summary", "Lean summary projection: counts + sentiment + per-participant {alias, status, sentiment, comment}. Drops interview_answers + per-interaction breakdowns.")
|
|
639
658
|
// PC-N4: agents reach for `--summarize` (verb) by analogy with the MCP
|
|
640
659
|
// `summarize` action; accept it as a hidden alias of --summary so the
|
|
641
660
|
// canonical flag stays the documented one but the muscle-memory variant
|
|
642
661
|
// works without a round-trip.
|
|
643
662
|
.addOption(new Option("--summarize", "Hidden alias for --summary").hideHelp())
|
|
644
|
-
.option("--transcript <
|
|
663
|
+
.option("--transcript <participant_id>", "Chat transcript projection for one participant: flat role/text/turn-index array (chat-modality only). Mirrors the MCP `get_chat_transcript` shape.")
|
|
645
664
|
.addHelpText("after", `
|
|
646
665
|
Examples:
|
|
647
666
|
$ ish study results <id>
|
|
648
667
|
$ ish study results <id> --json
|
|
649
668
|
$ ish study results <id> --summary --json
|
|
650
|
-
$ ish study results <id> --transcript
|
|
669
|
+
$ ish study results <id> --transcript pt-d4e --json
|
|
651
670
|
|
|
652
671
|
Default --json envelope (M10: per-answer sentiment now included):
|
|
653
672
|
{
|
|
654
673
|
"study": { "alias": "s-...", "name": "...", "modality": "..." },
|
|
655
|
-
"
|
|
674
|
+
"participant_count": 12,
|
|
656
675
|
"completed_count": 8,
|
|
657
676
|
"failed_count": 0,
|
|
658
677
|
"sentiment": { "counts": { "Satisfied": 5, "Frustrated": 2 }, "total": 7 },
|
|
659
678
|
"interview_answers": [
|
|
660
679
|
{ "question": "...", "type": "text",
|
|
661
680
|
"answers": [
|
|
662
|
-
{ "
|
|
681
|
+
{ "participant_alias": "pt-...", "participant_name": "...", "iteration": "A",
|
|
663
682
|
"answer": "...", "sentiment": "Satisfied" }
|
|
664
683
|
] }
|
|
665
684
|
],
|
|
666
|
-
"
|
|
667
|
-
{ "alias": "
|
|
685
|
+
"participants": [
|
|
686
|
+
{ "alias": "pt-...", "name": "...", "iteration": "A", "status": "completed",
|
|
668
687
|
"interaction_count": 12, "sentiment": "Satisfied", "comment": "...",
|
|
669
688
|
"error_message": "..." }
|
|
670
689
|
]
|
|
671
690
|
}
|
|
672
691
|
|
|
673
692
|
--summary projection (M2-friction-7: drops the interview_answers payload):
|
|
674
|
-
{ study,
|
|
693
|
+
{ study, participant_count, completed_count, failed_count, sentiment, participants: [...] }
|
|
675
694
|
|
|
676
|
-
--transcript <
|
|
695
|
+
--transcript <participant_id> projection (M2-friction-12, chat modality):
|
|
677
696
|
{
|
|
678
|
-
"
|
|
697
|
+
"participant_id": "...", "participant_alias": "pt-...",
|
|
679
698
|
"instance_name": "...", "modality": "chat",
|
|
680
699
|
"transcript": [
|
|
681
700
|
{ "role": "bot", "text": "Hi…", "turn_index": 0, "failure": null },
|
|
682
|
-
{ "role": "
|
|
701
|
+
{ "role": "participant", "text": "Pricing?", "turn_index": 0,
|
|
683
702
|
"action_type": "send_text", "option_label": null, "sentiment": null }
|
|
684
703
|
],
|
|
685
704
|
"unique_bot_replies": 2,
|
|
686
|
-
"
|
|
705
|
+
"participant_summary": { "comment": "...", "sentiment": {...} }
|
|
687
706
|
}
|
|
688
707
|
|
|
689
708
|
Tips:
|
|
690
|
-
Use \`--get <path>\` for a single value (e.g. \`--get
|
|
709
|
+
Use \`--get <path>\` for a single value (e.g. \`--get participant_count\`),
|
|
691
710
|
\`--fields a,b,c\` to project the JSON output further.
|
|
692
711
|
|
|
693
712
|
Common --get paths (default envelope):
|
|
694
|
-
--get
|
|
713
|
+
--get participant_count # how many participants ran
|
|
695
714
|
--get completed_count # how many finished
|
|
696
715
|
--get failed_count # how many errored
|
|
697
716
|
--get sentiment # {counts, total} histogram
|
|
698
717
|
--get sentiment.counts # bare label→count map
|
|
699
718
|
--get sentiment.total # total sentiment-tagged answers
|
|
700
719
|
--get study.modality # interactive | text | image | …
|
|
701
|
-
--get
|
|
702
|
-
--get
|
|
703
|
-
--get
|
|
720
|
+
--get participants.alias # one alias per line
|
|
721
|
+
--get participants.0.comment # first participant's narrative comment
|
|
722
|
+
--get participants.0.sentiment # first participant's aggregate sentiment
|
|
704
723
|
--get interview_answers # full per-question payload
|
|
705
724
|
--get interview_answers.0.question # text of the first question
|
|
706
725
|
--get interview_answers.0.answers.0.answer # first answer to the first question
|
|
707
726
|
|
|
708
|
-
Common --get paths (--transcript <
|
|
727
|
+
Common --get paths (--transcript <participant_id> envelope):
|
|
709
728
|
--get transcript # full role/text/turn array
|
|
710
729
|
--get transcript.text # one text per turn
|
|
711
|
-
--get
|
|
712
|
-
--get
|
|
730
|
+
--get participant_summary.comment # narrative comment
|
|
731
|
+
--get participant_summary.sentiment # aggregate sentiment map
|
|
713
732
|
--get unique_bot_replies # bot-side message count
|
|
714
733
|
|
|
715
734
|
When no runs have completed, the default envelope is returned with zero counts and empty arrays.`)
|
|
@@ -724,12 +743,12 @@ When no runs have completed, the default envelope is returned with zero counts a
|
|
|
724
743
|
}
|
|
725
744
|
const rid = resolveId(id);
|
|
726
745
|
if (opts.transcript) {
|
|
727
|
-
// --transcript <
|
|
728
|
-
// the named
|
|
746
|
+
// --transcript <participant_id>: bypass the study aggregator; fetch
|
|
747
|
+
// the named participant directly. Cheaper (one GET, no nested
|
|
729
748
|
// iterations payload) and shapes 1:1 with the MCP transcript.
|
|
730
|
-
const
|
|
731
|
-
const
|
|
732
|
-
output(buildChatTranscript(
|
|
749
|
+
const participantId = resolveId(opts.transcript);
|
|
750
|
+
const participant = await client.get(`/participants/${participantId}`);
|
|
751
|
+
output(buildChatTranscript(participant), globals.json, { preProjected: true });
|
|
733
752
|
return;
|
|
734
753
|
}
|
|
735
754
|
const data = await client.get(`/studies/${rid}`);
|
|
@@ -770,7 +789,11 @@ Examples:
|
|
|
770
789
|
$ ish study update <id> --assignment "Sign up:Complete the signup flow" \\
|
|
771
790
|
--question "How easy was it?"
|
|
772
791
|
$ ish study update <id> --questionnaire ./questionnaire.json
|
|
773
|
-
$ ish study update <id> --assignments-file ./assignments.json
|
|
792
|
+
$ ish study update <id> --assignments-file ./assignments.json
|
|
793
|
+
|
|
794
|
+
Replacing assignments replaces the full list (no additive edit). Assignment
|
|
795
|
+
checklists ("steps") ride along when present in the JSON forms
|
|
796
|
+
(--assignments-file / --assignments) — see docs get-page concepts/assignment.`)
|
|
774
797
|
.action(async (id, opts, cmd) => {
|
|
775
798
|
await withClient(cmd, async (client, globals) => {
|
|
776
799
|
const assignments = resolveAssignments(opts);
|
|
@@ -852,7 +875,7 @@ Examples:
|
|
|
852
875
|
});
|
|
853
876
|
});
|
|
854
877
|
attachStudyRunCommands(study);
|
|
855
|
-
|
|
878
|
+
attachStudyParticipantCommands(study);
|
|
856
879
|
attachStudyAnalyzeCommands(study);
|
|
857
880
|
attachStudyScreenshotsCommands(study);
|
|
858
881
|
}
|
|
@@ -152,7 +152,7 @@ existing workspace was returned. On creation, \`reused: false\`.`)
|
|
|
152
152
|
.addHelpText("after", `
|
|
153
153
|
Usage counters:
|
|
154
154
|
studies_used / studies_max — current study count vs the user's plan cap
|
|
155
|
-
|
|
155
|
+
people_used / people_max — workspace-private participant profile count vs cap
|
|
156
156
|
|
|
157
157
|
Caps fall back to null when the user's plan grants unlimited (math.inf). The
|
|
158
158
|
account tier is read from /account/me; limit tables come from /billing/limits.
|
|
@@ -174,7 +174,7 @@ Examples:
|
|
|
174
174
|
if (usage.tier)
|
|
175
175
|
console.log(`Plan: ${usage.tier}`);
|
|
176
176
|
console.log(`Studies: ${usage.studies_used} / ${renderCap(usage.studies_max)}`);
|
|
177
|
-
console.log(`Custom
|
|
177
|
+
console.log(`Custom people: ${usage.people_used} / ${renderCap(usage.people_max)}`);
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
180
|
workspace
|
|
@@ -213,11 +213,11 @@ async function collectWorkspaceUsage(client, workspaceId) {
|
|
|
213
213
|
const studiesPromise = client
|
|
214
214
|
.get(`/products/${workspaceId}/studies`)
|
|
215
215
|
.catch(() => []);
|
|
216
|
-
//
|
|
217
|
-
// gates `
|
|
216
|
+
// people_used: paginated list returns { total, items, ... }. Backend
|
|
217
|
+
// gates `maxCustomPersons` on visibility=workspace (rows owned by
|
|
218
218
|
// this workspace; was `private` before the visibility rename).
|
|
219
|
-
const
|
|
220
|
-
.get("/
|
|
219
|
+
const peoplePromise = client
|
|
220
|
+
.get("/people", {
|
|
221
221
|
product_id: workspaceId,
|
|
222
222
|
visibility: "workspace",
|
|
223
223
|
type: "ai",
|
|
@@ -232,18 +232,18 @@ async function collectWorkspaceUsage(client, workspaceId) {
|
|
|
232
232
|
const limitsPromise = client
|
|
233
233
|
.get("/billing/limits")
|
|
234
234
|
.catch(() => ({ tiers: {} }));
|
|
235
|
-
const [product, studies,
|
|
235
|
+
const [product, studies, participants, account, limits] = await Promise.all([
|
|
236
236
|
productPromise,
|
|
237
237
|
studiesPromise,
|
|
238
|
-
|
|
238
|
+
peoplePromise,
|
|
239
239
|
acctPromise,
|
|
240
240
|
limitsPromise,
|
|
241
241
|
]);
|
|
242
242
|
const tier = typeof account.credits?.tier === "string" ? account.credits.tier : null;
|
|
243
243
|
const tierTable = tier ? limits.tiers?.[tier] ?? null : null;
|
|
244
244
|
const studiesMax = tierTable && "maxStudiesPerProduct" in tierTable ? tierTable.maxStudiesPerProduct : null;
|
|
245
|
-
const
|
|
246
|
-
? tierTable.
|
|
245
|
+
const peopleMax = tierTable && "maxCustomPersons" in tierTable
|
|
246
|
+
? tierTable.maxCustomPersons
|
|
247
247
|
: null;
|
|
248
248
|
return {
|
|
249
249
|
id: workspaceId,
|
|
@@ -252,8 +252,8 @@ async function collectWorkspaceUsage(client, workspaceId) {
|
|
|
252
252
|
tier,
|
|
253
253
|
studies_used: Array.isArray(studies) ? studies.length : 0,
|
|
254
254
|
studies_max: studiesMax,
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
people_used: typeof participants.total === "number" ? participants.total : 0,
|
|
256
|
+
people_max: peopleMax,
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
259
|
// ---------------------------------------------------------------------------
|
|
@@ -364,7 +364,7 @@ function registerSiteAccessCommands(workspace) {
|
|
|
364
364
|
});
|
|
365
365
|
sa
|
|
366
366
|
.command("login")
|
|
367
|
-
.description("Set login form credentials a
|
|
367
|
+
.description("Set login form credentials a participant will type into the site")
|
|
368
368
|
.requiredOption("--username <u>", 'Username (or "-" to read from stdin)')
|
|
369
369
|
.requiredOption("--password <p>", 'Password (or "-" to read from stdin)')
|
|
370
370
|
.option("--workspace <id>", "Workspace ID; defaults to active workspace")
|
package/dist/connect.js
CHANGED
|
@@ -46,7 +46,7 @@ function row(content, inner) {
|
|
|
46
46
|
}
|
|
47
47
|
function renderCard(sim) {
|
|
48
48
|
const inner = CARD_WIDTH - 4;
|
|
49
|
-
const name = truncate(sim.
|
|
49
|
+
const name = truncate(sim.participant_name ?? sim.instance_name ?? sim.participant_id.slice(0, 8), inner - 2);
|
|
50
50
|
// Top border with name
|
|
51
51
|
const nameSegment = `─ ${name} `;
|
|
52
52
|
const topPad = CARD_WIDTH - 2 - nameSegment.length;
|
|
@@ -160,16 +160,16 @@ function renderSimulationCards(simulations) {
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
// --- Local storage for completed simulations ---
|
|
163
|
-
const
|
|
163
|
+
const storedParticipantIds = new Set();
|
|
164
164
|
function storeCompletedSimulation(sim) {
|
|
165
|
-
if (
|
|
165
|
+
if (storedParticipantIds.has(sim.participant_id))
|
|
166
166
|
return;
|
|
167
|
-
|
|
167
|
+
storedParticipantIds.add(sim.participant_id);
|
|
168
168
|
const dir = simulationsDir();
|
|
169
169
|
mkdirSync(dir, { recursive: true });
|
|
170
170
|
const logFile = join(dir, "history.jsonl");
|
|
171
171
|
const entry = {
|
|
172
|
-
|
|
172
|
+
participant_id: sim.participant_id,
|
|
173
173
|
instance_name: sim.instance_name,
|
|
174
174
|
status: sim.status,
|
|
175
175
|
study_name: sim.study_name,
|
package/dist/index.js
CHANGED
|
@@ -10,13 +10,14 @@ import { upgrade } from "./upgrade.js";
|
|
|
10
10
|
import { registerWorkspaceCommands } from "./commands/workspace.js";
|
|
11
11
|
import { registerStudyCommands } from "./commands/study.js";
|
|
12
12
|
import { registerIterationCommands } from "./commands/iteration.js";
|
|
13
|
-
import {
|
|
13
|
+
import { registerPersonCommands } from "./commands/person.js";
|
|
14
14
|
import { registerSourceCommands } from "./commands/source.js";
|
|
15
15
|
import { registerConfigCommands } from "./commands/config.js";
|
|
16
16
|
import { registerAskCommands } from "./commands/ask.js";
|
|
17
17
|
import { registerChatCommand } from "./commands/chat.js";
|
|
18
18
|
import { registerDocsCommands } from "./commands/docs.js";
|
|
19
19
|
import { registerInitCommands } from "./commands/init.js";
|
|
20
|
+
import { registerMcpCommands } from "./commands/mcp.js";
|
|
20
21
|
import { registerSecretCommands } from "./commands/secret.js";
|
|
21
22
|
import { AGENT_HELP_FOOTER } from "./lib/docs.js";
|
|
22
23
|
import { runInline, EXIT_USAGE, injectGlobalWorkspaceOption } from "./lib/command-helpers.js";
|
|
@@ -43,7 +44,7 @@ catch {
|
|
|
43
44
|
}
|
|
44
45
|
program
|
|
45
46
|
.name("ish")
|
|
46
|
-
.description("ish CLI — run studies and asks
|
|
47
|
+
.description("ish CLI — run studies and asks with AI people")
|
|
47
48
|
.version(version)
|
|
48
49
|
.addHelpText("after", AGENT_HELP_FOOTER);
|
|
49
50
|
// Unified error envelope for Commander-level failures (unknown command,
|
|
@@ -91,7 +92,7 @@ program
|
|
|
91
92
|
.addOption(new Option("--dev", "Use local dev API (http://localhost:8000)").hideHelp())
|
|
92
93
|
.option("--workspace <id>", "Default workspace ID; per-subcommand --workspace overrides")
|
|
93
94
|
.option("--json", "Output as JSON (auto-enabled when piped)")
|
|
94
|
-
.option("--get <field>", "Extract a single field from the JSON response and print only its value (implies --json internally; supports dotted paths e.g.
|
|
95
|
+
.option("--get <field>", "Extract a single field from the JSON response and print only its value (implies --json internally; supports dotted paths e.g. person.name)")
|
|
95
96
|
.option("--human", "Force human-readable output even when stdout is piped (overrides JSON-when-piped auto-detection)")
|
|
96
97
|
.option("--fields <fields>", "Comma-separated fields to include in JSON output (e.g. alias,name,status)")
|
|
97
98
|
.option("--verbose", "Include full UUIDs and timestamps in JSON output")
|
|
@@ -305,13 +306,14 @@ program
|
|
|
305
306
|
registerWorkspaceCommands(program);
|
|
306
307
|
registerStudyCommands(program);
|
|
307
308
|
registerIterationCommands(program);
|
|
308
|
-
|
|
309
|
+
registerPersonCommands(program);
|
|
309
310
|
registerSourceCommands(program);
|
|
310
311
|
registerConfigCommands(program);
|
|
311
312
|
registerAskCommands(program);
|
|
312
313
|
registerChatCommand(program);
|
|
313
314
|
registerDocsCommands(program);
|
|
314
315
|
registerInitCommands(program);
|
|
316
|
+
registerMcpCommands(program);
|
|
315
317
|
registerSecretCommands(program);
|
|
316
318
|
program
|
|
317
319
|
.command("upgrade")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-side validator for the AccessibilityProfile v1.0 JSONB shape on
|
|
3
|
-
*
|
|
3
|
+
* Person.accessibility_profile. Mirrors
|
|
4
4
|
* `ish-mcp/spec/accessibility-profile-schema.v1.json` (additionalProperties:
|
|
5
5
|
* false at every level except `extensions`). An empty object `{}` is the
|
|
6
6
|
* canonical default. When non-empty, `version` is required and must be
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-side validator for the AccessibilityProfile v1.0 JSONB shape on
|
|
3
|
-
*
|
|
3
|
+
* Person.accessibility_profile. Mirrors
|
|
4
4
|
* `ish-mcp/spec/accessibility-profile-schema.v1.json` (additionalProperties:
|
|
5
5
|
* false at every level except `extensions`). An empty object `{}` is the
|
|
6
6
|
* canonical default. When non-empty, `version` is required and must be
|
|
@@ -116,10 +116,10 @@ export async function hydrateForAlias(client, alias, hints = {}) {
|
|
|
116
116
|
await hydrateList(client, ALIAS_PREFIX.iteration, `/studies/${study}/iterations`);
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
|
-
case ALIAS_PREFIX.
|
|
119
|
+
case ALIAS_PREFIX.person: {
|
|
120
120
|
if (!ws)
|
|
121
121
|
return;
|
|
122
|
-
await hydrateList(client, ALIAS_PREFIX.
|
|
122
|
+
await hydrateList(client, ALIAS_PREFIX.person, "/people", { workspace_id: ws, type: "all", limit: "200" });
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
case ALIAS_PREFIX.ask: {
|
|
@@ -135,9 +135,9 @@ export async function hydrateForAlias(client, alias, hints = {}) {
|
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
// No cheap single-call hydrate for the rest:
|
|
138
|
-
//
|
|
138
|
+
// participant (`pt-`) — scoped to iteration
|
|
139
139
|
// ask round (`r-`) — nested on the ask
|
|
140
|
-
//
|
|
140
|
+
// person source (`ps-`) — fetched per-id
|
|
141
141
|
// simulation config (`c-`) — no list endpoint yet
|
|
142
142
|
default:
|
|
143
143
|
return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Short alias system for entity IDs.
|
|
3
3
|
*
|
|
4
|
-
* Generates deterministic aliases from UUID prefixes (e.g. w-6ec, s-b2c,
|
|
4
|
+
* Generates deterministic aliases from UUID prefixes (e.g. w-6ec, s-b2c, p-d4e, pt-a3f).
|
|
5
5
|
* Same UUID always produces the same alias — stable across commands and terminals.
|
|
6
6
|
* Aliases are persisted to ~/.ish/aliases.json for resolution.
|
|
7
7
|
*/
|
|
@@ -10,9 +10,9 @@ export declare const ALIAS_PREFIX: {
|
|
|
10
10
|
readonly workspace: "w";
|
|
11
11
|
readonly study: "s";
|
|
12
12
|
readonly iteration: "i";
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
15
|
-
readonly
|
|
13
|
+
readonly person: "p";
|
|
14
|
+
readonly personSource: "ps";
|
|
15
|
+
readonly participant: "pt";
|
|
16
16
|
readonly config: "c";
|
|
17
17
|
readonly job: "j";
|
|
18
18
|
readonly ask: "a";
|
|
@@ -46,7 +46,7 @@ export declare function deterministicAlias(prefix: string, uuid: string): string
|
|
|
46
46
|
* Resolve a short alias to a full UUID, or validate and pass through a full UUID.
|
|
47
47
|
*
|
|
48
48
|
* Accepted formats:
|
|
49
|
-
* - Short alias: w-6ec, s-b2c,
|
|
49
|
+
* - Short alias: w-6ec, s-b2c, p-d4e (resolved from ~/.ish/aliases.json)
|
|
50
50
|
* - Full UUID: 6ecf2857-1d7a-4f9c-85da-c2ac6c5c5346
|
|
51
51
|
*
|
|
52
52
|
* Everything else throws with guidance toward the correct usage.
|