@ishlabs/cli 0.19.0 → 0.20.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/dist/commands/ask.js +26 -2
- package/dist/commands/config.js +9 -1
- package/dist/commands/docs.js +6 -7
- package/dist/commands/person.js +123 -9
- package/dist/commands/secret.js +25 -2
- package/dist/commands/source.d.ts +1 -1
- package/dist/commands/source.js +10 -6
- package/dist/commands/study.js +19 -0
- package/dist/commands/workspace.js +41 -6
- package/dist/index.js +227 -44
- package/dist/lib/alias-store.js +23 -4
- package/dist/lib/auth.js +22 -4
- package/dist/lib/baggage.d.ts +15 -6
- package/dist/lib/baggage.js +14 -8
- package/dist/lib/command-helpers.d.ts +1 -0
- package/dist/lib/command-helpers.js +79 -7
- package/dist/lib/docs.js +211 -21
- package/dist/lib/output.js +61 -17
- package/dist/lib/profile-sources.js +18 -0
- package/dist/lib/skill-content.js +10 -2
- package/dist/upgrade.js +9 -2
- package/package.json +1 -1
|
@@ -28,7 +28,7 @@ const VISIBILITY_ALIASES = {
|
|
|
28
28
|
private: "workspace",
|
|
29
29
|
public: "platform",
|
|
30
30
|
};
|
|
31
|
-
function normalizeVisibility(raw) {
|
|
31
|
+
export function normalizeVisibility(raw) {
|
|
32
32
|
if (raw === undefined)
|
|
33
33
|
return undefined;
|
|
34
34
|
return VISIBILITY_ALIASES[raw] ?? raw;
|
|
@@ -186,6 +186,12 @@ export async function resolvePersonIds(client, workspace, flags, opts = {}) {
|
|
|
186
186
|
if (sampleN === undefined && !flags.all && !filtersUsed) {
|
|
187
187
|
throw new Error(`Select people: pass --person <id> (repeatable), --sample <N>, ${allFlagName}, or filter flags (--bio, --country, --gender, --min-age, --max-age, --occupation, --search, --visibility).`);
|
|
188
188
|
}
|
|
189
|
+
// NEW-CP-2 / Pattern H: --sample N > backend cap is detectable without
|
|
190
|
+
// any API call — fail fast so a bad value doesn't burn a /people round-trip
|
|
191
|
+
// before the cap message surfaces.
|
|
192
|
+
if (sampleN !== undefined && sampleN > PARTICIPANT_BATCH_CAP) {
|
|
193
|
+
throw new Error(`--sample ${sampleN} exceeds the per-dispatch participant cap of ${PARTICIPANT_BATCH_CAP}. Pass --sample ${PARTICIPANT_BATCH_CAP} or fewer, or split the run into multiple dispatches.`);
|
|
194
|
+
}
|
|
189
195
|
const params = {
|
|
190
196
|
product_id: workspace,
|
|
191
197
|
type: "ai",
|
|
@@ -245,15 +251,41 @@ export async function resolvePersonIds(client, workspace, flags, opts = {}) {
|
|
|
245
251
|
throw new Error(`No ${sim}people found in workspace ${workspace}.${opts.requireSimulatable ? " Create people with simulation configs first." : ""}`);
|
|
246
252
|
}
|
|
247
253
|
if (flags.all)
|
|
248
|
-
return pool.map((p) => p.id);
|
|
254
|
+
return enforceParticipantCap(pool.map((p) => p.id), flags, opts);
|
|
249
255
|
if (sampleN !== undefined) {
|
|
250
256
|
if (sampleN > pool.length) {
|
|
251
257
|
throw new Error(`--sample ${sampleN} requested but only ${pool.length} matching person${pool.length === 1 ? "" : "s"} available.`);
|
|
252
258
|
}
|
|
259
|
+
// sampleN > PARTICIPANT_BATCH_CAP is caught earlier (before the
|
|
260
|
+
// /people fetch) so we don't reach here with an over-cap sample.
|
|
253
261
|
return shuffleInPlace([...pool]).slice(0, sampleN).map((p) => p.id);
|
|
254
262
|
}
|
|
255
|
-
// Filters only, no --sample/--all → return every match.
|
|
256
|
-
return pool.map((p) => p.id);
|
|
263
|
+
// Filters only, no --sample/--all → return every match (subject to cap).
|
|
264
|
+
return enforceParticipantCap(pool.map((p) => p.id), flags, opts);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Backend caps each dispatch batch at 20 simulations (Pydantic
|
|
268
|
+
* `max_length=20` on the `simulations` array in
|
|
269
|
+
* app/api/models/simulation.py, media.py, and chat.py). Without a
|
|
270
|
+
* client-side guard, `--all` on a workspace with platform-visible
|
|
271
|
+
* people resolves to ~200 (the `/people` pagination limit) and the
|
|
272
|
+
* backend returns a `validation_error` ("List should have at most 20
|
|
273
|
+
* items after validation, not 200") that's confusing without context.
|
|
274
|
+
* Throw a helpful client-side error before the dispatch so the user
|
|
275
|
+
* knows to sample explicitly.
|
|
276
|
+
*
|
|
277
|
+
* Discovered during the 2026-05-26 post-remediation checkpoint sweep
|
|
278
|
+
* (NEW-CP-2). If the backend cap changes, update PARTICIPANT_BATCH_CAP.
|
|
279
|
+
*/
|
|
280
|
+
const PARTICIPANT_BATCH_CAP = 20;
|
|
281
|
+
function enforceParticipantCap(ids, flags, opts) {
|
|
282
|
+
if (ids.length <= PARTICIPANT_BATCH_CAP)
|
|
283
|
+
return ids;
|
|
284
|
+
const allFlagName = opts.allFlagName ?? "--all";
|
|
285
|
+
const filterDesc = describeFilters(flags) || "no filter";
|
|
286
|
+
throw new Error(`Resolved ${ids.length} participants (${filterDesc}) but the backend caps each dispatch at ${PARTICIPANT_BATCH_CAP}. ` +
|
|
287
|
+
`Pass \`--sample ${PARTICIPANT_BATCH_CAP}\` to randomly subsample the pool, narrow your filters, or run the dispatch ` +
|
|
288
|
+
`multiple times against different slices. ${allFlagName} without --sample is only safe when the matching pool is ≤${PARTICIPANT_BATCH_CAP}.`);
|
|
257
289
|
}
|
|
258
290
|
/**
|
|
259
291
|
* Attach the person-selection flag set to a Commander command.
|
|
@@ -375,7 +407,25 @@ export function exitCodeFromError(err) {
|
|
|
375
407
|
return 5;
|
|
376
408
|
}
|
|
377
409
|
if (err instanceof Error) {
|
|
378
|
-
//
|
|
410
|
+
// Pattern D: structured `error_code` on the Error object takes
|
|
411
|
+
// precedence over message-regex sniffing. Sites that self-tag
|
|
412
|
+
// (alias-store, auth, sub-command envelopes) get a deterministic
|
|
413
|
+
// exit code; sites that don't fall back to the legacy regex below.
|
|
414
|
+
// Order matters: this block must run BEFORE the /^invalid /i and
|
|
415
|
+
// /no auth token found/ regexes (which would otherwise misclassify
|
|
416
|
+
// the new `not_found` tag as `2` because the message starts with
|
|
417
|
+
// "Invalid ID").
|
|
418
|
+
const code = err.error_code;
|
|
419
|
+
if (typeof code === "string") {
|
|
420
|
+
if (code === "usage_error")
|
|
421
|
+
return 2;
|
|
422
|
+
if (code === "auth_failed")
|
|
423
|
+
return 3;
|
|
424
|
+
if (code === "not_found")
|
|
425
|
+
return 4;
|
|
426
|
+
}
|
|
427
|
+
// Auth-related client errors (e.g. missing token) — legacy regex
|
|
428
|
+
// path; the new error_code tagging above is the preferred route.
|
|
379
429
|
if (/no auth token found|run "ish login"|saved token is invalid|session expired/i.test(err.message))
|
|
380
430
|
return 3;
|
|
381
431
|
// Client-side validation failures
|
|
@@ -393,8 +443,30 @@ export function exitCodeFromError(err) {
|
|
|
393
443
|
// Structured error_kind on the Error object (set by chat endpoint test/init,
|
|
394
444
|
// simulation routes, etc.). TunnelInactive is the canonical transient one.
|
|
395
445
|
const kind = err.error_kind;
|
|
396
|
-
if (typeof kind === "string"
|
|
397
|
-
|
|
446
|
+
if (typeof kind === "string") {
|
|
447
|
+
// Transient: user can fix the cause and retry (tunnel down, bot
|
|
448
|
+
// auth credentials missing/wrong, upstream rate-limited).
|
|
449
|
+
if (kind === "TunnelInactive" || kind === "BotAuthError")
|
|
450
|
+
return 5;
|
|
451
|
+
// Validation-shaped: user passed something bad to the CLI/endpoint.
|
|
452
|
+
if (kind === "ConfirmationRequired" || kind === "BotShapeError")
|
|
453
|
+
return 2;
|
|
454
|
+
}
|
|
455
|
+
// (error_code mapping moved earlier in the function — see top of the
|
|
456
|
+
// `err instanceof Error` block. Order matters: it must run BEFORE
|
|
457
|
+
// the legacy regex sniffers to honor self-tagged errors.)
|
|
458
|
+
// Pattern E (ISSUE-021): DNS / connection failures are transient.
|
|
459
|
+
// Node's fetch surfaces them as TypeError with .cause = { code: "..." }.
|
|
460
|
+
const cause = err.cause;
|
|
461
|
+
if (cause && typeof cause === "object") {
|
|
462
|
+
const causeCode = cause.code;
|
|
463
|
+
if (typeof causeCode === "string" && (causeCode === "ENOTFOUND"
|
|
464
|
+
|| causeCode === "ECONNREFUSED"
|
|
465
|
+
|| causeCode === "ECONNRESET"
|
|
466
|
+
|| causeCode === "ETIMEDOUT"
|
|
467
|
+
|| causeCode === "EAI_AGAIN"))
|
|
468
|
+
return 5;
|
|
469
|
+
}
|
|
398
470
|
}
|
|
399
471
|
return 1;
|
|
400
472
|
}
|
package/dist/lib/docs.js
CHANGED
|
@@ -156,6 +156,25 @@ first scraping the list.
|
|
|
156
156
|
The full saturated-account walkthrough (with branch logic + a worked
|
|
157
157
|
transcript) lives at \`guides/cold-start\`.
|
|
158
158
|
|
|
159
|
+
## Deleting a workspace
|
|
160
|
+
|
|
161
|
+
\`ish workspace delete <id>\` is the **highest-blast-radius destructive op
|
|
162
|
+
in the CLI** — it removes ALL nested studies, asks, people, secrets,
|
|
163
|
+
configs, sources, and chat endpoints. The confirmation guard is
|
|
164
|
+
mandatory:
|
|
165
|
+
|
|
166
|
+
- **Interactive (TTY)**: prompts on stderr naming the workspace; type
|
|
167
|
+
\`y\` to proceed.
|
|
168
|
+
- **Non-interactive** (\`--json\`, piped, or non-TTY stdin): pass
|
|
169
|
+
\`-y\` / \`--yes\` to confirm. Without it, the CLI exits with usage
|
|
170
|
+
code 2 rather than deleting silently.
|
|
171
|
+
|
|
172
|
+
\`\`\`
|
|
173
|
+
ish workspace delete w-6ec # interactive prompt
|
|
174
|
+
ish workspace delete w-6ec --yes # skip prompt
|
|
175
|
+
ish workspace delete w-6ec --json --yes # JSON/agent consumers must be explicit
|
|
176
|
+
\`\`\`
|
|
177
|
+
|
|
159
178
|
## Related
|
|
160
179
|
|
|
161
180
|
- \`guides/cold-start\` — saturated-account first-step playbook
|
|
@@ -1083,12 +1102,32 @@ round-trips when you know them up front:
|
|
|
1083
1102
|
- \`image:./hero-a.png\` — local image (auto-uploaded)
|
|
1084
1103
|
- \`image:./a.png::label=A\` — with explicit label
|
|
1085
1104
|
|
|
1105
|
+
## Deleting an ask
|
|
1106
|
+
|
|
1107
|
+
\`ish ask delete <id>\` requires explicit confirmation (parallels
|
|
1108
|
+
\`workspace delete\`, \`study delete\`, \`person delete\`, \`source
|
|
1109
|
+
delete\`, \`chat endpoint delete\`):
|
|
1110
|
+
|
|
1111
|
+
- **Interactive (TTY)**: prompts on stderr; type \`y\` to proceed.
|
|
1112
|
+
- **Non-interactive** (\`--json\`, piped, or non-TTY stdin): pass
|
|
1113
|
+
\`-y\` / \`--yes\` to confirm. Without it, the CLI exits with usage
|
|
1114
|
+
code 2 rather than deleting silently.
|
|
1115
|
+
|
|
1116
|
+
\`\`\`
|
|
1117
|
+
ish ask delete a-6ec # interactive prompt
|
|
1118
|
+
ish ask delete a-6ec --yes # skip prompt
|
|
1119
|
+
ish ask delete a-6ec --json --yes # JSON consumers must be explicit
|
|
1120
|
+
\`\`\`
|
|
1121
|
+
|
|
1122
|
+
The active ask is auto-cleared from \`~/.ish/config.json\` if the
|
|
1123
|
+
deleted ask was the active one.
|
|
1124
|
+
|
|
1086
1125
|
## Related
|
|
1087
1126
|
|
|
1088
1127
|
- \`concepts/round\` — what a round is and how it executes.
|
|
1089
1128
|
- \`concepts/people\` — how participants are chosen at ask creation.
|
|
1090
1129
|
- \`concepts/run-verbs\` — \`ish ask run\` vs \`ish study run\`.
|
|
1091
|
-
- \`reference/credits\` — ask rounds bill 1
|
|
1130
|
+
- \`reference/credits\` — ask rounds bill \`n_participants * (1 + len(questions))\` credits per round; \`questions\` follow-ups bill *per participant* on top of the base response, so a 3-person panel with 2 follow-up questions costs \`3 * (1 + 2) = 9\` credits when all complete (not 3).
|
|
1092
1131
|
`;
|
|
1093
1132
|
const CONCEPT_ROUND = `# concept: round
|
|
1094
1133
|
|
|
@@ -1261,6 +1300,21 @@ The legacy \`--tech-savviness\` flag was removed in
|
|
|
1261
1300
|
\`person-schema-v2\`; passing it now produces commander's standard
|
|
1262
1301
|
"unknown option" error.
|
|
1263
1302
|
|
|
1303
|
+
## Deleting a person
|
|
1304
|
+
|
|
1305
|
+
\`ish person delete <id>\` requires explicit confirmation:
|
|
1306
|
+
|
|
1307
|
+
- **Interactive (TTY)**: prompts on stderr; type \`y\` to proceed.
|
|
1308
|
+
- **Non-interactive** (\`--json\`, piped, or non-TTY stdin): pass
|
|
1309
|
+
\`-y\` / \`--yes\` to confirm. Without it, the CLI exits with usage
|
|
1310
|
+
code 2 rather than deleting silently.
|
|
1311
|
+
|
|
1312
|
+
\`\`\`
|
|
1313
|
+
ish person delete p-d4e # interactive prompt
|
|
1314
|
+
ish person delete p-d4e --yes # skip prompt
|
|
1315
|
+
ish person delete p-d4e --json --yes # JSON consumers must be explicit
|
|
1316
|
+
\`\`\`
|
|
1317
|
+
|
|
1264
1318
|
## Related
|
|
1265
1319
|
|
|
1266
1320
|
- \`concepts/source\` — the inputs to \`person generate\`.
|
|
@@ -1303,6 +1357,24 @@ in real customer evidence.
|
|
|
1303
1357
|
ish source get ps-3a4
|
|
1304
1358
|
\`\`\`
|
|
1305
1359
|
|
|
1360
|
+
## Deleting a source
|
|
1361
|
+
|
|
1362
|
+
\`ish source delete <id>\` requires explicit confirmation:
|
|
1363
|
+
|
|
1364
|
+
- **Interactive (TTY)**: prompts on stderr; type \`y\` to proceed.
|
|
1365
|
+
- **Non-interactive** (\`--json\`, piped, or non-TTY stdin): pass
|
|
1366
|
+
\`-y\` / \`--yes\` to confirm. Without it, the CLI exits with usage
|
|
1367
|
+
code 2 rather than deleting silently.
|
|
1368
|
+
|
|
1369
|
+
\`\`\`
|
|
1370
|
+
ish source delete ps-3a4 # interactive prompt
|
|
1371
|
+
ish source delete ps-3a4 --yes # skip prompt
|
|
1372
|
+
ish source delete ps-3a4 --json --yes # JSON consumers must be explicit
|
|
1373
|
+
\`\`\`
|
|
1374
|
+
|
|
1375
|
+
The backend ref-counts the underlying file: the storage object is
|
|
1376
|
+
removed only when no profile mappings remain.
|
|
1377
|
+
|
|
1306
1378
|
## Related
|
|
1307
1379
|
|
|
1308
1380
|
- \`concepts/person\` — sources feed profile generation.
|
|
@@ -1405,6 +1477,30 @@ manager"\` or \`"retail associate"\` return many. Two adaptations:
|
|
|
1405
1477
|
- \`ish ask run\` (without \`--new\`) → cannot change participants; the ask
|
|
1406
1478
|
fixes it at creation. Audience flags only apply with \`--new\`.
|
|
1407
1479
|
|
|
1480
|
+
## Per-dispatch cap (20)
|
|
1481
|
+
|
|
1482
|
+
Each \`study run\` / \`ask run\` / \`chat\` dispatch is capped at **20
|
|
1483
|
+
participants** by the backend (\`max_length=20\` on the \`simulations\`
|
|
1484
|
+
list). The CLI enforces this client-side BEFORE the network round-trip
|
|
1485
|
+
so a too-large \`--sample\` or an unbounded \`--all\` returns a clear
|
|
1486
|
+
error instead of a confusing server-side \`validation_error\`:
|
|
1487
|
+
|
|
1488
|
+
\`\`\`
|
|
1489
|
+
$ ish study run --all # on a workspace with platform pool
|
|
1490
|
+
Error: Resolved 200 participants (no filter) but the backend caps each dispatch at 20.
|
|
1491
|
+
Pass \`--sample 20\` to randomly subsample the pool, narrow your filters, or run
|
|
1492
|
+
the dispatch multiple times against different slices. --all without --sample is
|
|
1493
|
+
only safe when the matching pool is ≤20.
|
|
1494
|
+
|
|
1495
|
+
$ ish study run --sample 25 # bad value caught before /people fetch
|
|
1496
|
+
Error: --sample 25 exceeds the per-dispatch participant cap of 20.
|
|
1497
|
+
Pass --sample 20 or fewer, or split the run into multiple dispatches.
|
|
1498
|
+
\`\`\`
|
|
1499
|
+
|
|
1500
|
+
For larger panels: dispatch multiple times against different demographic
|
|
1501
|
+
slices (\`--country SE\`, then \`--country GB\`, etc.) or use the web UI
|
|
1502
|
+
which batches behind the scenes.
|
|
1503
|
+
|
|
1408
1504
|
## Examples
|
|
1409
1505
|
|
|
1410
1506
|
\`\`\`
|
|
@@ -2254,17 +2350,33 @@ The CLI guarantees these contracts so agents can chain safely:
|
|
|
2254
2350
|
|
|
2255
2351
|
## Exit codes
|
|
2256
2352
|
|
|
2257
|
-
| Code | Meaning |
|
|
2258
|
-
|
|
2259
|
-
| 0 | Success |
|
|
2260
|
-
| 1 | General error |
|
|
2261
|
-
| 2 | Usage / validation |
|
|
2262
|
-
| 3 | Auth (re-run \`ish login\`) |
|
|
2263
|
-
| 4 | Not found |
|
|
2264
|
-
| 5 | Transient — retryable
|
|
2353
|
+
| Code | Meaning | Common \`error_code\` values |
|
|
2354
|
+
|------|----------------------|------------------------------|
|
|
2355
|
+
| 0 | Success | — |
|
|
2356
|
+
| 1 | General error | \`server\`, \`client_error\` (uncategorized) |
|
|
2357
|
+
| 2 | Usage / validation | \`usage_error\` (Commander), \`validation_error\` (server), \`ConfirmationRequired\` |
|
|
2358
|
+
| 3 | Auth (re-run \`ish login\`) | \`auth_failed\`, missing-token errors |
|
|
2359
|
+
| 4 | Not found | \`not_found\` |
|
|
2360
|
+
| 5 | Transient — retryable | \`timeout\`, \`TunnelInactive\`, \`BotAuthError\`, DNS / network (\`ENOTFOUND\`, \`ECONNREFUSED\`) |
|
|
2265
2361
|
|
|
2266
2362
|
Use these to branch in scripts; do not parse the human stderr message.
|
|
2267
2363
|
|
|
2364
|
+
**Commander-level errors** (unknown command, missing required argument,
|
|
2365
|
+
missing required option) all exit **2** with \`error_code: "usage_error"\`.
|
|
2366
|
+
The suggestion field points at the right help target:
|
|
2367
|
+
|
|
2368
|
+
- Unknown command → \`Run \`ish --help\` for usage\` (the typo IS the
|
|
2369
|
+
command name — don't point at it; Commander also appends
|
|
2370
|
+
\`(Did you mean workspace?)\` for near-matches).
|
|
2371
|
+
- Missing argument / option → \`Run \`ish <command> --help\` for usage\`
|
|
2372
|
+
(substituted with the actual command, e.g. \`ish workspace --help\`).
|
|
2373
|
+
|
|
2374
|
+
**DNS / connection failures** (a wrong \`--api-url\`, a backend that's
|
|
2375
|
+
down, a captive portal) exit **5** so scripts retry rather than abort
|
|
2376
|
+
permanently. The underlying \`fetch\` \`TypeError\` is detected via its
|
|
2377
|
+
\`cause.code\` (\`ENOTFOUND\`, \`ECONNREFUSED\`, \`ECONNRESET\`,
|
|
2378
|
+
\`ETIMEDOUT\`, \`EAI_AGAIN\`).
|
|
2379
|
+
|
|
2268
2380
|
## Error envelope
|
|
2269
2381
|
|
|
2270
2382
|
When a command fails with \`--json\` (or piped stdout), the CLI prints
|
|
@@ -2294,6 +2406,12 @@ a structured error object on **stdout** and a human message on
|
|
|
2294
2406
|
(\`validation\`, \`auth\`, \`not_found\`, \`timeout\`, \`server\`,
|
|
2295
2407
|
\`network\`, …). \`retryable: true\` matches exit code 5.
|
|
2296
2408
|
|
|
2409
|
+
The \`status\` field carries the upstream **HTTP status code** when one
|
|
2410
|
+
is available (e.g. \`401\`, \`404\`, \`422\`). It is **omitted entirely**
|
|
2411
|
+
from envelopes that don't originate from an HTTP response (Commander
|
|
2412
|
+
parse errors, local validation failures, alias-resolution errors). Do
|
|
2413
|
+
not branch on \`status: 0\` — that value is never emitted as of 0.20.
|
|
2414
|
+
|
|
2297
2415
|
## Conventions
|
|
2298
2416
|
|
|
2299
2417
|
- Successful commands exit 0 and print one JSON object/array on stdout.
|
|
@@ -2411,13 +2529,19 @@ The CLI keeps a small amount of session state in \`~/.ish/config.json\`
|
|
|
2411
2529
|
(or wherever \`ISH_HOME\` points) so commands don't need to repeat IDs:
|
|
2412
2530
|
|
|
2413
2531
|
- \`access_token\` / \`refresh_token\` — the OAuth pair from \`ish login\`.
|
|
2414
|
-
- \`workspace\`
|
|
2415
|
-
- \`study\`
|
|
2416
|
-
- \`ask\`
|
|
2532
|
+
- \`workspace\` — set by \`ish workspace use <id>\`.
|
|
2533
|
+
- \`study\` — set by \`ish study use <id>\` (or implicitly by \`ish study create\`).
|
|
2534
|
+
- \`ask\` — set by \`ish ask use <id>\` (or implicitly by \`ish ask create\`).
|
|
2535
|
+
- \`chat_endpoint\` — set by \`ish chat endpoint use <id>\`.
|
|
2417
2536
|
|
|
2418
2537
|
Most commands fall back to these when their corresponding flag is
|
|
2419
2538
|
omitted (\`--workspace\`, \`--study\`, \`--ask\`).
|
|
2420
2539
|
|
|
2540
|
+
**\`workspace\` is the parent** of \`study\`, \`ask\`, and \`chat_endpoint\` —
|
|
2541
|
+
all three are scoped to a single workspace. Switching workspaces
|
|
2542
|
+
(\`ish workspace use <other>\`) clears all three to avoid cross-workspace
|
|
2543
|
+
footguns, and the CLI prints a one-line stderr note when it does so.
|
|
2544
|
+
|
|
2421
2545
|
## Inspecting active context
|
|
2422
2546
|
|
|
2423
2547
|
\`ish status\` (alias: \`ish whoami\`) is the canonical way to see what's
|
|
@@ -2430,7 +2554,8 @@ ish status
|
|
|
2430
2554
|
# User: you@example.com (token valid, expires in 47m)
|
|
2431
2555
|
# Workspace: Onboarding revamp (w-6ec)
|
|
2432
2556
|
# Study: —
|
|
2433
|
-
# Ask: a-6ec
|
|
2557
|
+
# Ask: tagline AB (a-6ec)
|
|
2558
|
+
# Chat ep: —
|
|
2434
2559
|
# Home: /home/you/.ish
|
|
2435
2560
|
# API: https://api.ishlabs.io
|
|
2436
2561
|
\`\`\`
|
|
@@ -2439,12 +2564,13 @@ JSON shape (\`ish status --json\` or piped):
|
|
|
2439
2564
|
|
|
2440
2565
|
\`\`\`json
|
|
2441
2566
|
{
|
|
2442
|
-
"user":
|
|
2443
|
-
"workspace":
|
|
2444
|
-
"study":
|
|
2445
|
-
"ask":
|
|
2446
|
-
"
|
|
2447
|
-
"
|
|
2567
|
+
"user": { "email": "...", "token_valid": true, "expires_in_seconds": 2820 },
|
|
2568
|
+
"workspace": { "id": "...", "alias": "w-6ec", "name": "Onboarding revamp" },
|
|
2569
|
+
"study": null,
|
|
2570
|
+
"ask": { "id": "...", "alias": "a-6ec", "name": "tagline AB" },
|
|
2571
|
+
"chat_endpoint": null,
|
|
2572
|
+
"api_url": "https://api.ishlabs.io",
|
|
2573
|
+
"home": "/home/you/.ish"
|
|
2448
2574
|
}
|
|
2449
2575
|
\`\`\`
|
|
2450
2576
|
|
|
@@ -2453,19 +2579,83 @@ JSON shape (\`ish status --json\` or piped):
|
|
|
2453
2579
|
\`ish login\`. Safe to run unconditionally at the start of any
|
|
2454
2580
|
script or agent session.
|
|
2455
2581
|
|
|
2582
|
+
### \`ish login\` is idempotent
|
|
2583
|
+
|
|
2584
|
+
When you already have a valid saved token, \`ish login\` short-circuits
|
|
2585
|
+
with a friendly "Already logged in" message and **does not** open a new
|
|
2586
|
+
browser tab or register a fresh OAuth client. Use \`--force\` (or \`-f\`)
|
|
2587
|
+
to bypass the guard — typical reason is switching accounts.
|
|
2588
|
+
|
|
2589
|
+
\`\`\`bash
|
|
2590
|
+
ish login # no-op when already authenticated
|
|
2591
|
+
ish login --force # always re-run the browser flow
|
|
2592
|
+
\`\`\`
|
|
2593
|
+
|
|
2594
|
+
The short-circuit returns a structured envelope under \`--json\`:
|
|
2595
|
+
|
|
2596
|
+
\`\`\`json
|
|
2597
|
+
{
|
|
2598
|
+
"message": "Already logged in",
|
|
2599
|
+
"email": "you@example.com",
|
|
2600
|
+
"token_valid": true,
|
|
2601
|
+
"expires_in_seconds": 2820,
|
|
2602
|
+
"hint": "Pass --force to re-run the browser flow (e.g. to switch accounts)."
|
|
2603
|
+
}
|
|
2604
|
+
\`\`\`
|
|
2605
|
+
|
|
2606
|
+
### Orphan / stale active refs
|
|
2607
|
+
|
|
2608
|
+
If an active ref points at an entity that no longer exists or moved
|
|
2609
|
+
workspace, \`status\` surfaces a \`warning\` field on that ref (instead
|
|
2610
|
+
of silently dropping the \`name\`). Each warned ref also gets a \`hint\`
|
|
2611
|
+
field with the exact command to clear or replace it:
|
|
2612
|
+
|
|
2613
|
+
\`\`\`json
|
|
2614
|
+
{
|
|
2615
|
+
"study": {
|
|
2616
|
+
"id": "...",
|
|
2617
|
+
"alias": "s-74d",
|
|
2618
|
+
"warning": "orphan — entity no longer exists in this workspace",
|
|
2619
|
+
"hint": "Active study is no longer accessible (deleted, moved workspace, or auth issue). Use \`ish study use <id>\` to switch, or \`ish study use --clear\` to drop."
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
\`\`\`
|
|
2623
|
+
|
|
2624
|
+
In human output the warning prints as \`⚠ ...\` under the row and a
|
|
2625
|
+
follow-up line shows the hint.
|
|
2626
|
+
|
|
2456
2627
|
## Setting / clearing active context
|
|
2457
2628
|
|
|
2458
2629
|
\`\`\`bash
|
|
2459
|
-
ish workspace use w-6ec # set
|
|
2460
|
-
ish workspace use --clear # clear
|
|
2630
|
+
ish workspace use w-6ec # set (also clears active study/ask/chat_endpoint if workspace changed)
|
|
2631
|
+
ish workspace use --clear # clear workspace + all workspace-scoped children
|
|
2461
2632
|
|
|
2462
2633
|
ish study use s-b2c
|
|
2463
2634
|
ish study use --clear
|
|
2464
2635
|
|
|
2465
2636
|
ish ask use a-6ec
|
|
2466
2637
|
ish ask use --clear
|
|
2638
|
+
|
|
2639
|
+
ish chat endpoint use ep-abc
|
|
2640
|
+
ish chat endpoint use --clear
|
|
2467
2641
|
\`\`\`
|
|
2468
2642
|
|
|
2643
|
+
### Auto-activation on create
|
|
2644
|
+
|
|
2645
|
+
\`ish study create\`, \`ish ask create\`, and \`ish workspace use\` all
|
|
2646
|
+
update the active context as a side-effect (so the natural next command
|
|
2647
|
+
— \`ish iteration create --study <new>\`, \`ish ask add-round\`, etc. —
|
|
2648
|
+
"just works" without re-typing the ID). The CLI **emits a one-line
|
|
2649
|
+
stderr notice** when this happens; consumers piping stdout get the new
|
|
2650
|
+
record while the auto-activate is visible to operators.
|
|
2651
|
+
|
|
2652
|
+
### Cleanup on delete
|
|
2653
|
+
|
|
2654
|
+
\`ish workspace delete\`, \`ish study delete\`, \`ish ask delete\`, and
|
|
2655
|
+
\`ish chat endpoint delete\` automatically clear matching active refs
|
|
2656
|
+
from \`~/.ish/config.json\` so subsequent commands don't render orphans.
|
|
2657
|
+
\`workspace delete\` also clears all workspace-scoped children.
|
|
2658
|
+
|
|
2469
2659
|
## Overriding without persisting
|
|
2470
2660
|
|
|
2471
2661
|
Every read command accepts \`--workspace <id>\`, \`--study <id>\`, or
|
package/dist/lib/output.js
CHANGED
|
@@ -224,7 +224,11 @@ function wrapList(items, existing, opts = {}) {
|
|
|
224
224
|
const limit = typeof existing?.limit === "number" ? existing.limit : returned;
|
|
225
225
|
const offset = typeof existing?.offset === "number" ? existing.offset : 0;
|
|
226
226
|
const has_more = total > offset + returned;
|
|
227
|
-
|
|
227
|
+
// ISSUE-031: if the user explicitly named fields via --fields or --get,
|
|
228
|
+
// skip the per-item lean-strip so the requested fields actually survive
|
|
229
|
+
// to the output. Otherwise `--fields id,alias` silently drops `id`.
|
|
230
|
+
const userSpecifiedFields = (_fields && _fields.length > 0) || (typeof _getField === "string" && _getField.length > 0);
|
|
231
|
+
const leanItems = _verbose || opts.preProjectedItems || userSpecifiedFields
|
|
228
232
|
? items
|
|
229
233
|
: leanJson(items) ?? [];
|
|
230
234
|
return { items: leanItems, total, returned, limit, offset, has_more };
|
|
@@ -277,9 +281,16 @@ function pickFields(data, fields) {
|
|
|
277
281
|
/** Serialize data as JSON, applying lean transform and field selection. */
|
|
278
282
|
function jsonOutput(data, options = {}) {
|
|
279
283
|
let out;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
284
|
+
// ISSUE-031: when the user explicitly names fields via --fields or
|
|
285
|
+
// --get, their request takes precedence over the default lean-strip.
|
|
286
|
+
// Previously --fields id,alias would silently drop `id` because
|
|
287
|
+
// leanJson ran first and stripped UUID-valued fields. The user
|
|
288
|
+
// asking for a field is unambiguous intent — bypass the strip.
|
|
289
|
+
const userSpecifiedFields = (_fields && _fields.length > 0) || (typeof _getField === "string" && _getField.length > 0);
|
|
290
|
+
if (_verbose || options.preProjected || userSpecifiedFields) {
|
|
291
|
+
// Verbose: full payload. preProjected: caller already chose the fields.
|
|
292
|
+
// userSpecifiedFields: caller said exactly what they want — don't
|
|
293
|
+
// second-guess by stripping UUIDs they asked for.
|
|
283
294
|
out = data;
|
|
284
295
|
}
|
|
285
296
|
else {
|
|
@@ -461,6 +472,29 @@ function suggestionsForError(err) {
|
|
|
461
472
|
}
|
|
462
473
|
return [];
|
|
463
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* Pattern F (ISSUE-023): rewrite server-side internal entity names to the
|
|
477
|
+
* user-facing CLI surface names so messages like "Product not found" don't
|
|
478
|
+
* leak the backend's internal vocabulary to a first-time CLI user.
|
|
479
|
+
*
|
|
480
|
+
* Mapping reflects the rename audit: server still calls workspaces
|
|
481
|
+
* "products" and people "profiles" / "tester profiles" in some response
|
|
482
|
+
* messages; the CLI surface is `workspace` / `person`. Source is
|
|
483
|
+
* `attachment` server-side.
|
|
484
|
+
*
|
|
485
|
+
* Word-boundary anchored to avoid touching e.g. "productivity" or
|
|
486
|
+
* "Attachments must be ..." (the latter is genuinely about attachments).
|
|
487
|
+
*/
|
|
488
|
+
function remapEntityName(message) {
|
|
489
|
+
return message
|
|
490
|
+
.replace(/\bProduct not found\b/g, "Workspace not found")
|
|
491
|
+
.replace(/\bproduct not found\b/g, "workspace not found")
|
|
492
|
+
.replace(/\bTester [Pp]rofile not found\b/g, (m) => m.includes("P") ? "Person not found" : "person not found")
|
|
493
|
+
.replace(/\bProfile not found\b/g, "Person not found")
|
|
494
|
+
.replace(/\bprofile not found\b/g, "person not found")
|
|
495
|
+
.replace(/\bAttachment not found\b/g, "Source not found")
|
|
496
|
+
.replace(/\battachment not found\b/g, "source not found");
|
|
497
|
+
}
|
|
464
498
|
export function outputError(err, json) {
|
|
465
499
|
const suggestions = suggestionsForError(err);
|
|
466
500
|
if (err instanceof ApiError) {
|
|
@@ -505,7 +539,7 @@ export function outputError(err, json) {
|
|
|
505
539
|
: undefined;
|
|
506
540
|
if (json) {
|
|
507
541
|
console.error(JSON.stringify({
|
|
508
|
-
error: err.message,
|
|
542
|
+
error: remapEntityName(err.message),
|
|
509
543
|
error_code: err.error_code,
|
|
510
544
|
status: err.status,
|
|
511
545
|
retryable: err.retryable,
|
|
@@ -527,7 +561,7 @@ export function outputError(err, json) {
|
|
|
527
561
|
console.error("Error: Insufficient credits. Purchase more at https://app.ishlabs.io");
|
|
528
562
|
}
|
|
529
563
|
else {
|
|
530
|
-
console.error(`Error: ${err.message}`);
|
|
564
|
+
console.error(`Error: ${remapEntityName(err.message)}`);
|
|
531
565
|
}
|
|
532
566
|
if (Array.isArray(bodyErrors)) {
|
|
533
567
|
for (const entry of bodyErrors) {
|
|
@@ -549,6 +583,12 @@ export function outputError(err, json) {
|
|
|
549
583
|
else if (err instanceof ValidationError) {
|
|
550
584
|
if (json) {
|
|
551
585
|
console.error(JSON.stringify({
|
|
586
|
+
// ValidationError is CLI-thrown (we control its message), so we
|
|
587
|
+
// don't apply remapEntityName — that helper exists to translate
|
|
588
|
+
// server-side internal vocabulary ("Product"/"Profile"/"Attachment")
|
|
589
|
+
// back to user-facing names. Applying it here risks mangling
|
|
590
|
+
// user-supplied content (e.g. a workspace name containing "Profile
|
|
591
|
+
// not found"). Restrict the remap to the ApiError branch.
|
|
552
592
|
error: err.message,
|
|
553
593
|
error_code: "validation_error",
|
|
554
594
|
retryable: false,
|
|
@@ -597,6 +637,10 @@ export function outputError(err, json) {
|
|
|
597
637
|
const mergedSuggestions = [...new Set([...suggestions, ...taggedSuggestions])];
|
|
598
638
|
if (json) {
|
|
599
639
|
console.error(JSON.stringify({
|
|
640
|
+
// Generic Error: CLI-thrown (we control the message), so we don't
|
|
641
|
+
// apply remapEntityName — see ValidationError branch above for the
|
|
642
|
+
// same reasoning. Server-side names only leak through the ApiError
|
|
643
|
+
// branch where remapEntityName IS applied.
|
|
600
644
|
error: err.message,
|
|
601
645
|
error_code: errorCode,
|
|
602
646
|
retryable,
|
|
@@ -702,7 +746,7 @@ export function formatWorkspaceList(workspaces, json) {
|
|
|
702
746
|
return;
|
|
703
747
|
}
|
|
704
748
|
const aliasMap = getAliasMap(ALIAS_PREFIX.workspace);
|
|
705
|
-
printTable(["
|
|
749
|
+
printTable(["ALIAS", "NAME", "ROOM", "STUDIES", "ASKS", "PARTICIPANTS", "LAST ACTIVITY"], workspaces.map((w) => [
|
|
706
750
|
aliasMap.get(String(w.id)) || String(w.id || ""),
|
|
707
751
|
String(w.name || ""),
|
|
708
752
|
formatHeadroom(w.has_headroom),
|
|
@@ -784,7 +828,7 @@ export function formatStudyList(studies, json) {
|
|
|
784
828
|
return;
|
|
785
829
|
}
|
|
786
830
|
const aliasMap = getAliasMap(ALIAS_PREFIX.study);
|
|
787
|
-
printTable(["
|
|
831
|
+
printTable(["ALIAS", "NAME", "MODALITY", "TYPE", "STATUS", "PARTICIPANTS"], studies.map((s) => [
|
|
788
832
|
aliasMap.get(String(s.id)) || String(s.id || ""),
|
|
789
833
|
String(s.name || ""),
|
|
790
834
|
String(s.modality || "-"),
|
|
@@ -934,7 +978,7 @@ export function formatStudyDetail(study, json, options = {}, participants) {
|
|
|
934
978
|
const allParticipants = collectParticipants(participants, Array.isArray(study.iterations) ? study.iterations : []);
|
|
935
979
|
if (allParticipants.length > 0) {
|
|
936
980
|
console.log(`\nParticipants (${allParticipants.length}):`);
|
|
937
|
-
printTable(["
|
|
981
|
+
printTable(["ALIAS", "NAME", "ITERATION", "STATUS", "INTERACTIONS"], allParticipants.map((t) => [
|
|
938
982
|
t.id ? deterministicAlias(ALIAS_PREFIX.participant, t.id) : t.id,
|
|
939
983
|
t.name,
|
|
940
984
|
t.iterationLabel,
|
|
@@ -1074,7 +1118,7 @@ export function formatStudyResults(study, participants, json) {
|
|
|
1074
1118
|
// Participants table
|
|
1075
1119
|
if (allParticipants.length > 0) {
|
|
1076
1120
|
console.log("\nParticipants:");
|
|
1077
|
-
printTable(["
|
|
1121
|
+
printTable(["ALIAS", "NAME", "ITERATION", "STATUS", "INTERACTIONS", "SENTIMENT"], allParticipants.map((t) => {
|
|
1078
1122
|
const parts = Object.entries(t.sentimentCounts).map(([label, count]) => `${count} ${label.toLowerCase()}`);
|
|
1079
1123
|
return [
|
|
1080
1124
|
t.id ? deterministicAlias(ALIAS_PREFIX.participant, t.id) : t.id,
|
|
@@ -1375,7 +1419,7 @@ export function formatIterationList(iterations, json) {
|
|
|
1375
1419
|
return;
|
|
1376
1420
|
}
|
|
1377
1421
|
const aliasMap = getAliasMap(ALIAS_PREFIX.iteration);
|
|
1378
|
-
printTable(["
|
|
1422
|
+
printTable(["ALIAS", "LABEL", "NAME", "PARTICIPANTS", "CREATED"], iterations.map((it) => {
|
|
1379
1423
|
const participants = Array.isArray(it.participants) ? it.participants.length : 0;
|
|
1380
1424
|
return [
|
|
1381
1425
|
aliasMap.get(String(it.id)) || String(it.id || ""),
|
|
@@ -1449,7 +1493,7 @@ export function formatPersonList(profiles, json, limit) {
|
|
|
1449
1493
|
console.log("No participant profiles.");
|
|
1450
1494
|
return;
|
|
1451
1495
|
}
|
|
1452
|
-
printTable(["
|
|
1496
|
+
printTable(["ALIAS", "NAME", "OCCUPATION", "COUNTRY", "GENDER", "AGE"], list.map((p) => [
|
|
1453
1497
|
String(p.alias || p.id || ""),
|
|
1454
1498
|
String(p.name || ""),
|
|
1455
1499
|
String(p.occupation || "-"),
|
|
@@ -1504,7 +1548,7 @@ export function formatGeneratedProfileList(profiles, json) {
|
|
|
1504
1548
|
console.log(jsonOutput(list));
|
|
1505
1549
|
return;
|
|
1506
1550
|
}
|
|
1507
|
-
printTable(["
|
|
1551
|
+
printTable(["ALIAS", "NAME", "OCCUPATION", "COUNTRY", "GENDER", "AGE"], list.map((p) => [
|
|
1508
1552
|
String(p.alias || p.id || ""),
|
|
1509
1553
|
String(p.name || ""),
|
|
1510
1554
|
String(p.occupation || "-"),
|
|
@@ -1529,7 +1573,7 @@ export function formatSimulationPoll(results, json, isMedia = false) {
|
|
|
1529
1573
|
}
|
|
1530
1574
|
const aliasMap = getAliasMap(ALIAS_PREFIX.participant);
|
|
1531
1575
|
const countHeader = isMedia ? "SEGMENTS" : "INTERACTIONS";
|
|
1532
|
-
printTable(["
|
|
1576
|
+
printTable(["ALIAS", "PARTICIPANT", "STATUS", countHeader], results.map((r) => {
|
|
1533
1577
|
const id = String(r.id || r.participant_id || "");
|
|
1534
1578
|
return [
|
|
1535
1579
|
aliasMap.get(id) || id,
|
|
@@ -1574,7 +1618,7 @@ export function formatAskList(asks, json) {
|
|
|
1574
1618
|
return;
|
|
1575
1619
|
}
|
|
1576
1620
|
const aliasMap = getAliasMap(ALIAS_PREFIX.ask);
|
|
1577
|
-
printTable(["
|
|
1621
|
+
printTable(["ALIAS", "NAME", "STATUS", "PARTICIPANTS", "ROUNDS", "LAST ROUND", "ARCHIVED"], asks.map((a) => [
|
|
1578
1622
|
aliasMap.get(String(a.id)) || String(a.id || ""),
|
|
1579
1623
|
String(a.name || ""),
|
|
1580
1624
|
String(a.status || "-"),
|
|
@@ -1675,7 +1719,7 @@ export function formatAskDetail(ask, json) {
|
|
|
1675
1719
|
String(obj.status || "-"),
|
|
1676
1720
|
];
|
|
1677
1721
|
});
|
|
1678
|
-
printTable(["
|
|
1722
|
+
printTable(["ALIAS", "NAME", "STATUS"], rows);
|
|
1679
1723
|
if (participants.length > 20)
|
|
1680
1724
|
console.log(` … and ${participants.length - 20} more`);
|
|
1681
1725
|
}
|
|
@@ -2145,7 +2189,7 @@ export function formatConfigList(configs, json) {
|
|
|
2145
2189
|
return;
|
|
2146
2190
|
}
|
|
2147
2191
|
const aliasMap = getAliasMap(ALIAS_PREFIX.config);
|
|
2148
|
-
printTable(["
|
|
2192
|
+
printTable(["ALIAS", "NAME", "SOURCE", "CREATED"], configs.map((c) => [
|
|
2149
2193
|
aliasMap.get(String(c.id)) || String(c.id || ""),
|
|
2150
2194
|
String(c.name || ""),
|
|
2151
2195
|
String(c.source_type || "manual"),
|