@ishlabs/cli 0.8.4 → 0.8.5
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 +1 -1
- package/dist/auth.d.ts +15 -0
- package/dist/auth.js +40 -0
- package/dist/commands/ask.js +28 -2
- package/dist/commands/profile.js +25 -12
- package/dist/commands/study.js +12 -4
- package/dist/connect.js +87 -12
- package/dist/index.js +3 -0
- package/dist/lib/command-helpers.d.ts +37 -0
- package/dist/lib/command-helpers.js +166 -7
- package/dist/lib/docs.js +196 -22
- package/dist/lib/output.d.ts +6 -0
- package/dist/lib/output.js +133 -2
- package/dist/lib/skill-content.js +119 -5
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import { resolveApiUrl, resolveToken } from "./auth.js";
|
|
7
7
|
import { getAppUrl } from "../auth.js";
|
|
8
8
|
import { ApiClient, ApiError } from "./api-client.js";
|
|
9
|
-
import { outputError, setVerbose, setFields } from "./output.js";
|
|
9
|
+
import { outputError, setVerbose, setFields, setGetField } from "./output.js";
|
|
10
10
|
import { setColorsEnabled, colorsEnabled } from "./colors.js";
|
|
11
11
|
import { loadConfig } from "../config.js";
|
|
12
12
|
import { resolveId } from "./alias-store.js";
|
|
@@ -129,8 +129,58 @@ export async function resolveAudienceProfileIds(client, workspace, flags, opts =
|
|
|
129
129
|
if (opts.excludeProfileIds && opts.excludeProfileIds.size > 0 && !filterDesc) {
|
|
130
130
|
throw new Error("All matching profiles are already in this audience.");
|
|
131
131
|
}
|
|
132
|
+
// When --country was the binding constraint, query the broader pool (drop
|
|
133
|
+
// country filter, keep the rest) and surface the top populated countries
|
|
134
|
+
// so the agent doesn't have to round-trip through `ish profile list` to
|
|
135
|
+
// find one that matches. Pure best-effort — any failure falls back to the
|
|
136
|
+
// original error.
|
|
137
|
+
let suggestion = "";
|
|
138
|
+
if (flags.country && flags.country.length > 0) {
|
|
139
|
+
try {
|
|
140
|
+
const broader = {
|
|
141
|
+
product_id: workspace,
|
|
142
|
+
type: "ai",
|
|
143
|
+
limit: "500",
|
|
144
|
+
offset: "0",
|
|
145
|
+
};
|
|
146
|
+
if (flags.search)
|
|
147
|
+
broader.search = flags.search;
|
|
148
|
+
if (flags.gender && flags.gender.length > 0)
|
|
149
|
+
broader.gender = flags.gender;
|
|
150
|
+
if (flags.minAge)
|
|
151
|
+
broader.min_age = flags.minAge;
|
|
152
|
+
if (flags.maxAge)
|
|
153
|
+
broader.max_age = flags.maxAge;
|
|
154
|
+
if (flags.visibility)
|
|
155
|
+
broader.visibility = flags.visibility;
|
|
156
|
+
const broaderData = await client.get("/tester-profiles", broader);
|
|
157
|
+
const broaderItems = Array.isArray(broaderData)
|
|
158
|
+
? broaderData
|
|
159
|
+
: Array.isArray(broaderData?.items)
|
|
160
|
+
? broaderData.items
|
|
161
|
+
: [];
|
|
162
|
+
const broaderPool = opts.requireSimulatable
|
|
163
|
+
? broaderItems.filter(isSimulatable)
|
|
164
|
+
: broaderItems;
|
|
165
|
+
const counts = new Map();
|
|
166
|
+
for (const p of broaderPool) {
|
|
167
|
+
const c = typeof p.country === "string" ? p.country : null;
|
|
168
|
+
if (c)
|
|
169
|
+
counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
170
|
+
}
|
|
171
|
+
const top = [...counts.entries()]
|
|
172
|
+
.sort((a, b) => b[1] - a[1])
|
|
173
|
+
.slice(0, 3);
|
|
174
|
+
if (top.length > 0) {
|
|
175
|
+
suggestion = ` Populated countries with these other filters: ${top.map(([c, n]) => `${c} (${n})`).join(", ")}.`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Swallow — never replace the user's error with a secondary failure.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
132
182
|
if (filterDesc) {
|
|
133
|
-
throw new Error(`No ${sim}tester profiles in workspace ${workspace} match: ${filterDesc}
|
|
183
|
+
throw new Error(`No ${sim}tester profiles in workspace ${workspace} match: ${filterDesc}.${suggestion} Broaden your filters or run \`ish profile list\` to inspect the pool.`);
|
|
134
184
|
}
|
|
135
185
|
throw new Error(`No ${sim}tester profiles found in workspace ${workspace}.${opts.requireSimulatable ? " Create profiles with simulation configs first." : ""}`);
|
|
136
186
|
}
|
|
@@ -165,9 +215,52 @@ export function addAudienceFilterFlags(cmd, opts = {}) {
|
|
|
165
215
|
.option("--visibility <v>", "Filter by visibility (private|public)");
|
|
166
216
|
}
|
|
167
217
|
export function getGlobals(cmd) {
|
|
218
|
+
let globals;
|
|
219
|
+
try {
|
|
220
|
+
globals = computeGlobals(cmd);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
// Validation errors (e.g. --get with --human) need to surface as a
|
|
224
|
+
// clean usage error regardless of which entry point invoked us. Many
|
|
225
|
+
// commands resolve globals without a withClient/runInline wrapper
|
|
226
|
+
// (e.g. `ish docs *`) so we cannot rely on those try/catches.
|
|
227
|
+
const useJson = process.argv.includes("--json")
|
|
228
|
+
|| process.argv.includes("--get")
|
|
229
|
+
|| !process.stdout.isTTY;
|
|
230
|
+
outputError(err, useJson);
|
|
231
|
+
process.exit(exitCodeFromError(err));
|
|
232
|
+
}
|
|
233
|
+
// Apply side effects (verbose, fields, colors, get-field, active workspace)
|
|
234
|
+
// here so commands that resolve globals without going through withClient /
|
|
235
|
+
// runInline still get --get / --human / --fields honored.
|
|
236
|
+
applyGlobals(globals);
|
|
237
|
+
return globals;
|
|
238
|
+
}
|
|
239
|
+
function computeGlobals(cmd) {
|
|
168
240
|
const opts = cmd.optsWithGlobals();
|
|
169
|
-
//
|
|
170
|
-
|
|
241
|
+
// Pattern Ω: display-vs-capture controls.
|
|
242
|
+
// --get <field> implies --json (need structured data to extract from).
|
|
243
|
+
// --human forces the human renderer regardless of TTY/pipe state.
|
|
244
|
+
// Both passed together is a usage error — capture and display are
|
|
245
|
+
// different modes; pick one.
|
|
246
|
+
const getField = typeof opts.get === "string" && opts.get.length > 0
|
|
247
|
+
? opts.get
|
|
248
|
+
: undefined;
|
|
249
|
+
const human = opts.human === true;
|
|
250
|
+
if (getField && human) {
|
|
251
|
+
const err = new Error("--get and --human are mutually exclusive: --get captures a value (JSON-derived), --human forces human display.");
|
|
252
|
+
err.name = "ValidationError";
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
// Auto-switch to JSON when stdout is piped (non-TTY).
|
|
256
|
+
// --human overrides the auto-flip; --get implies --json.
|
|
257
|
+
let json;
|
|
258
|
+
if (human)
|
|
259
|
+
json = false;
|
|
260
|
+
else if (getField)
|
|
261
|
+
json = true;
|
|
262
|
+
else
|
|
263
|
+
json = opts.json ?? !process.stdout.isTTY;
|
|
171
264
|
// Parse --fields into an array
|
|
172
265
|
const fields = opts.fields
|
|
173
266
|
? String(opts.fields).split(",").map((f) => f.trim()).filter(Boolean)
|
|
@@ -182,9 +275,15 @@ export function getGlobals(cmd) {
|
|
|
182
275
|
dev: opts.dev,
|
|
183
276
|
json,
|
|
184
277
|
verbose: opts.verbose ?? false,
|
|
185
|
-
|
|
278
|
+
// --get is silent capture: suppress stderr progress so the bare value is
|
|
279
|
+
// the only thing the agent has to parse. --human keeps progress on.
|
|
280
|
+
quiet: opts.quiet ?? (getField ? true : (json && !opts.json)),
|
|
281
|
+
quietExplicit: opts.quiet === true || getField !== undefined,
|
|
186
282
|
color,
|
|
187
283
|
fields,
|
|
284
|
+
get: getField,
|
|
285
|
+
human,
|
|
286
|
+
workspace: typeof opts.workspace === "string" ? opts.workspace : undefined,
|
|
188
287
|
};
|
|
189
288
|
}
|
|
190
289
|
/**
|
|
@@ -224,14 +323,23 @@ export async function createClient(globals) {
|
|
|
224
323
|
const token = await resolveToken(globals.token, apiUrl, globals.tokenFile);
|
|
225
324
|
return new ApiClient({ apiUrl, token });
|
|
226
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Module-level fallback for `resolveWorkspace`. Set by `applyGlobals` from the
|
|
328
|
+
* merged `optsWithGlobals().workspace`, so the program-root --workspace flag
|
|
329
|
+
* propagates to subcommand resolvers without each call site having to thread
|
|
330
|
+
* `globals` through. Subcommand-level --workspace still wins via Commander's
|
|
331
|
+
* own merge (it's already reflected in `globals.workspace`).
|
|
332
|
+
*/
|
|
333
|
+
let _activeWorkspace;
|
|
227
334
|
function applyGlobals(globals) {
|
|
228
335
|
setVerbose(globals.verbose);
|
|
229
336
|
setFields(globals.fields);
|
|
337
|
+
setGetField(globals.get);
|
|
230
338
|
setColorsEnabled(globals.color);
|
|
339
|
+
_activeWorkspace = globals.workspace;
|
|
231
340
|
}
|
|
232
341
|
export async function withClient(cmd, fn) {
|
|
233
342
|
const globals = getGlobals(cmd);
|
|
234
|
-
applyGlobals(globals);
|
|
235
343
|
try {
|
|
236
344
|
const client = await createClient(globals);
|
|
237
345
|
await fn(client, globals);
|
|
@@ -248,7 +356,6 @@ export async function withClient(cmd, fn) {
|
|
|
248
356
|
*/
|
|
249
357
|
export async function runInline(cmd, fn) {
|
|
250
358
|
const globals = getGlobals(cmd);
|
|
251
|
-
applyGlobals(globals);
|
|
252
359
|
try {
|
|
253
360
|
await fn(globals);
|
|
254
361
|
}
|
|
@@ -299,9 +406,61 @@ export function readJsonFileOrStdin(filePath) {
|
|
|
299
406
|
process.stdin.on("error", reject);
|
|
300
407
|
});
|
|
301
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Prompt for confirmation of a destructive action, or short-circuit when
|
|
411
|
+
* `--yes` is set. In `--json` mode without `--yes` we refuse with a usage
|
|
412
|
+
* error rather than silently proceeding or hanging on a prompt — agents
|
|
413
|
+
* piping through CLI must be explicit about destructive intent.
|
|
414
|
+
*/
|
|
415
|
+
export async function confirmDestructive(prompt, opts) {
|
|
416
|
+
if (opts.yes)
|
|
417
|
+
return;
|
|
418
|
+
if (opts.json) {
|
|
419
|
+
const err = new Error(`--yes is required for destructive actions in --json mode. Refusing to proceed without explicit confirmation.`);
|
|
420
|
+
err.name = "ValidationError";
|
|
421
|
+
throw err;
|
|
422
|
+
}
|
|
423
|
+
if (!process.stdin.isTTY) {
|
|
424
|
+
const err = new Error(`--yes is required for destructive actions when stdin is not a TTY. Refusing to proceed without explicit confirmation.`);
|
|
425
|
+
err.name = "ValidationError";
|
|
426
|
+
throw err;
|
|
427
|
+
}
|
|
428
|
+
process.stderr.write(`${prompt} [y/N] `);
|
|
429
|
+
const answer = await new Promise((resolve, reject) => {
|
|
430
|
+
let data = "";
|
|
431
|
+
const onData = (chunk) => {
|
|
432
|
+
data += chunk.toString();
|
|
433
|
+
if (data.includes("\n")) {
|
|
434
|
+
process.stdin.off("data", onData);
|
|
435
|
+
process.stdin.off("error", onError);
|
|
436
|
+
process.stdin.pause();
|
|
437
|
+
resolve(data.trim().toLowerCase());
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const onError = (err) => {
|
|
441
|
+
process.stdin.off("data", onData);
|
|
442
|
+
process.stdin.off("error", onError);
|
|
443
|
+
reject(err);
|
|
444
|
+
};
|
|
445
|
+
process.stdin.setEncoding("utf-8");
|
|
446
|
+
process.stdin.on("data", onData);
|
|
447
|
+
process.stdin.on("error", onError);
|
|
448
|
+
process.stdin.resume();
|
|
449
|
+
});
|
|
450
|
+
if (answer !== "y" && answer !== "yes") {
|
|
451
|
+
const err = new Error("Aborted.");
|
|
452
|
+
err.name = "ValidationError";
|
|
453
|
+
throw err;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
302
456
|
export function resolveWorkspace(explicit) {
|
|
303
457
|
if (explicit)
|
|
304
458
|
return resolveId(explicit);
|
|
459
|
+
// Fall back to the program-root --workspace cached by applyGlobals — covers
|
|
460
|
+
// `ish --workspace W study list` where the subcommand action doesn't see the
|
|
461
|
+
// flag in its local opts.
|
|
462
|
+
if (_activeWorkspace)
|
|
463
|
+
return resolveId(_activeWorkspace);
|
|
305
464
|
const env = process.env.ISH_WORKSPACE;
|
|
306
465
|
if (env)
|
|
307
466
|
return resolveId(env);
|
package/dist/lib/docs.js
CHANGED
|
@@ -47,7 +47,8 @@ Two top-level run verbs:
|
|
|
47
47
|
and prints active workspace/study/ask. See \`concepts/active-context\`.
|
|
48
48
|
- Running your first study? \`ish docs get-page guides/first-study\`.
|
|
49
49
|
- Comparing study vs ask? \`ish docs get-page concepts/run-verbs\`.
|
|
50
|
-
-
|
|
50
|
+
- **Output modes** (display vs capture vs chain — \`--human\`, \`--get\`,
|
|
51
|
+
\`--json\`)? \`ish docs get-page reference/json-mode\`.
|
|
51
52
|
- Auth gated URL? \`ish docs get-page concepts/site-access\`.
|
|
52
53
|
|
|
53
54
|
## Install the skill into this project
|
|
@@ -74,6 +75,22 @@ A workspace carries:
|
|
|
74
75
|
- Site-access credentials (encrypted at rest) — see \`concepts/site-access\`.
|
|
75
76
|
- Tester profiles + sources visible to every study/ask in the workspace.
|
|
76
77
|
|
|
78
|
+
## Selecting a workspace per command
|
|
79
|
+
|
|
80
|
+
\`--workspace <id>\` works at the **program root** as well as on each
|
|
81
|
+
subcommand — both forms are equivalent, and the subcommand-level flag
|
|
82
|
+
wins on conflict:
|
|
83
|
+
|
|
84
|
+
\`\`\`
|
|
85
|
+
ish --workspace w-6ec study list # program root
|
|
86
|
+
ish study list --workspace w-6ec # subcommand (same effect)
|
|
87
|
+
ish --workspace w-6ec study list --workspace w-other # w-other wins
|
|
88
|
+
\`\`\`
|
|
89
|
+
|
|
90
|
+
Use whichever is most natural for your scripting. Without either, the
|
|
91
|
+
CLI falls back to \`ISH_WORKSPACE\` (env var) and then the
|
|
92
|
+
\`workspace\` saved in \`~/.ish/config.json\`.
|
|
93
|
+
|
|
77
94
|
## Common commands
|
|
78
95
|
|
|
79
96
|
\`\`\`
|
|
@@ -148,6 +165,21 @@ Every study response carries two status-shaped fields:
|
|
|
148
165
|
The CLI also surfaces a \`status_inferred\` field + stderr warning when
|
|
149
166
|
it detects raw-vs-derived inconsistencies. See \`reference/json-mode\`.
|
|
150
167
|
|
|
168
|
+
## Deleting a study
|
|
169
|
+
|
|
170
|
+
\`ish study delete <id>\` requires explicit confirmation:
|
|
171
|
+
|
|
172
|
+
- **Interactive (TTY)**: prompts on stderr; type \`y\` to proceed.
|
|
173
|
+
- **Non-interactive** (\`--json\`, piped, or non-TTY stdin): pass
|
|
174
|
+
\`-y\` / \`--yes\` to confirm. Without it, the CLI exits with usage
|
|
175
|
+
code 2 rather than deleting silently.
|
|
176
|
+
|
|
177
|
+
\`\`\`
|
|
178
|
+
ish study delete s-b2c # interactive prompt
|
|
179
|
+
ish study delete s-b2c --yes # skip prompt
|
|
180
|
+
ish study delete s-b2c --json --yes # JSON consumers must be explicit
|
|
181
|
+
\`\`\`
|
|
182
|
+
|
|
151
183
|
## Generate vs create
|
|
152
184
|
|
|
153
185
|
\`ish study generate --problem "..."\` runs an LLM-backed flow that
|
|
@@ -379,7 +411,12 @@ ish ask results a-6ec --json | jq '.rounds[0].aggregates'
|
|
|
379
411
|
|
|
380
412
|
For \`--wants-pick\` / \`--wants-ratings\` rounds, \`ask results --json\`
|
|
381
413
|
includes an \`aggregates\` field per round so you don't have to parse
|
|
382
|
-
prose
|
|
414
|
+
prose. Each individual pick also carries a **\`pick_confidence\`** score
|
|
415
|
+
(0..1) — the model's self-reported confidence in its variant choice.
|
|
416
|
+
Use it to break ties: when two variants are nominally close on count,
|
|
417
|
+
the variant with higher mean \`pick_confidence\` is the more decisive
|
|
418
|
+
choice. \`pick_confidence\` is only present on rounds run with
|
|
419
|
+
\`--wants-pick\`.
|
|
383
420
|
|
|
384
421
|
\`\`\`json
|
|
385
422
|
{
|
|
@@ -517,6 +554,24 @@ ish profile create --file profile.json
|
|
|
517
554
|
Expected JSON: \`{ "name": "...", "type": "ai", "gender": "female",
|
|
518
555
|
"country": "US", "occupation": "...", "bio": "..." }\`
|
|
519
556
|
|
|
557
|
+
## Generation behavior to expect
|
|
558
|
+
|
|
559
|
+
- **Latency**: \`profile generate\` is LLM-backed and typically takes
|
|
560
|
+
10–20s for 1–5 profiles. The CLI emits stderr progress lines
|
|
561
|
+
(\`generating N profiles…\` then \`generated N profiles\`) so you
|
|
562
|
+
know it's not stuck. Suppress with \`--quiet\`.
|
|
563
|
+
- **Brief fidelity**: bios reference domain-specific terms from your
|
|
564
|
+
description verbatim or as close paraphrase. If you mention
|
|
565
|
+
\`F-skatt\`, "manual Excel invoicing", "Stripe payouts", or similar
|
|
566
|
+
tools/jargon, expect those terms (or paraphrases) to appear in
|
|
567
|
+
each generated bio's daily-routine framing — not sanded down to
|
|
568
|
+
generic prose.
|
|
569
|
+
- **DOB diversity**: month-and-day are derived from a deterministic
|
|
570
|
+
per-profile hash so birthdays spread across the year (no more
|
|
571
|
+
every-profile-on-\`06-15\`). Year follows the requested age.
|
|
572
|
+
Re-generating the same name/country/occupation/age yields the
|
|
573
|
+
same DOB.
|
|
574
|
+
|
|
520
575
|
## Related
|
|
521
576
|
|
|
522
577
|
- \`concepts/source\` — the inputs to \`profile generate\`.
|
|
@@ -577,6 +632,24 @@ flags. Two ways to select:
|
|
|
577
632
|
The two modes are **mutually exclusive** — pass either \`--profile\` or
|
|
578
633
|
the filter set, not both.
|
|
579
634
|
|
|
635
|
+
## Empty-pool suggestions
|
|
636
|
+
|
|
637
|
+
When a filter combination matches zero profiles, the error message
|
|
638
|
+
includes the top three populated countries that satisfy your *other*
|
|
639
|
+
filters — so you can pivot to a country with actual coverage without a
|
|
640
|
+
second \`profile list\` round-trip:
|
|
641
|
+
|
|
642
|
+
\`\`\`
|
|
643
|
+
$ ish study run --country XX --min-age 35 --sample 5
|
|
644
|
+
Error: No simulatable AI tester profiles in workspace w-b32 match:
|
|
645
|
+
--country XX --min-age 35.
|
|
646
|
+
Populated countries with these other filters: SE (12), DE (8), NL (3).
|
|
647
|
+
Broaden your filters or run \`ish profile list\` to inspect the pool.
|
|
648
|
+
\`\`\`
|
|
649
|
+
|
|
650
|
+
The suggestion is best-effort — it never replaces the original error,
|
|
651
|
+
just augments it.
|
|
652
|
+
|
|
580
653
|
## Defaults
|
|
581
654
|
|
|
582
655
|
- \`ish study run\` with no audience flags → reuses the iteration's
|
|
@@ -709,6 +782,13 @@ ish study cancel <tester_id> # cancel a running simulation
|
|
|
709
782
|
\`<tester_id>\` accepts a tester alias (\`t-…\`) or a full UUID. The
|
|
710
783
|
study-level \`poll\`/\`wait\` forms also exist (\`--study <id>\` /
|
|
711
784
|
\`--iteration <id>\`) for whole-batch progress.
|
|
785
|
+
|
|
786
|
+
## Related
|
|
787
|
+
|
|
788
|
+
- \`reference/json-mode\` — output modes (display vs capture vs chain).
|
|
789
|
+
Use \`--get tester_aliases\` to capture the run's testers without
|
|
790
|
+
piping through \`jq\`. \`--human\` forces table output even through
|
|
791
|
+
\`tee\`/redirection.
|
|
712
792
|
`;
|
|
713
793
|
const REFERENCE_ALIASES = `# reference: aliases
|
|
714
794
|
|
|
@@ -740,15 +820,84 @@ ish profile generate --source tps-3a4 --count 4
|
|
|
740
820
|
The full UUID is also always accepted. Add \`--verbose\` to JSON output
|
|
741
821
|
to see UUIDs alongside aliases.
|
|
742
822
|
`;
|
|
743
|
-
const REFERENCE_JSON_MODE = `# reference:
|
|
823
|
+
const REFERENCE_JSON_MODE = `# reference: output modes for agents
|
|
824
|
+
|
|
825
|
+
\`ish\` distinguishes **three output modes** so agents don't have to
|
|
826
|
+
post-process CLI output with \`jq\` or \`python\` for routine tasks:
|
|
827
|
+
|
|
828
|
+
1. **Display mode (human)** — readable tables and key/value blocks.
|
|
829
|
+
Default on a TTY. Force it anywhere with \`--human\` (e.g. \`ish
|
|
830
|
+
workspace list --human | tee /tmp/x.txt\` keeps the table layout
|
|
831
|
+
even though stdout is redirected).
|
|
832
|
+
2. **Capture mode (single value)** — \`--get <field>\` extracts the
|
|
833
|
+
value at a dotted path and prints it bare (no JSON quotes, no
|
|
834
|
+
indentation). Use this to feed one CLI's output into another:
|
|
835
|
+
\`ASK=$(ish ask create … --get alias)\` instead of
|
|
836
|
+
\`ASK=$(ish ask create … --json | jq -r .alias)\`.
|
|
837
|
+
3. **Chain mode (full JSON)** — \`--json\` (or auto-enabled when stdout
|
|
838
|
+
is piped). Returns structured payloads for downstream parsing.
|
|
839
|
+
Reach for this only when you actually need multiple fields or a
|
|
840
|
+
nested shape; for one value, \`--get\` is shorter.
|
|
841
|
+
|
|
842
|
+
## Picking the right mode
|
|
843
|
+
|
|
844
|
+
| You want to… | Mode |
|
|
845
|
+
|-------------------------------------------|--------------------------------------------------|
|
|
846
|
+
| Show the user a list of workspaces | bare command (TTY) or \`--human\` if redirecting |
|
|
847
|
+
| Capture an alias for a follow-up command | \`--get alias\` |
|
|
848
|
+
| Inspect a specific nested field | \`--get tester_profile.name\` |
|
|
849
|
+
| Compare 2+ fields, or pipe into jq | \`--json\` (or auto-on when piped) |
|
|
850
|
+
| Force human output through \`tee\` | \`--human\` |
|
|
851
|
+
| Force JSON on a TTY | \`--json\` |
|
|
852
|
+
|
|
853
|
+
\`--get\` and \`--human\` are mutually exclusive — capture and display are
|
|
854
|
+
different intents; pick one. \`--get\` implies \`--json\` internally so the
|
|
855
|
+
renderer always has structured data to extract from; you don't need to
|
|
856
|
+
add \`--json\` yourself.
|
|
857
|
+
|
|
858
|
+
### Worked example: display vs. capture
|
|
859
|
+
|
|
860
|
+
\`\`\`bash
|
|
861
|
+
# Display: bare command on a TTY → human table.
|
|
862
|
+
ish workspace list
|
|
863
|
+
|
|
864
|
+
# Capture: feed one alias into the next command, no jq required.
|
|
865
|
+
ASK=$(ish ask create --new --name demo \\
|
|
866
|
+
--prompt "Which?" --variant text:A --variant text:B \\
|
|
867
|
+
--sample 30 --get alias)
|
|
868
|
+
ish ask wait "$ASK" --timeout 600
|
|
869
|
+
|
|
870
|
+
# Capture across an entire list: one value per line.
|
|
871
|
+
ish workspace list --get alias
|
|
872
|
+
# w-6ec
|
|
873
|
+
# w-d02
|
|
874
|
+
# …
|
|
744
875
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
876
|
+
# Display preserved through tee:
|
|
877
|
+
ish ask results "$ASK" --human | tee /tmp/transcript.txt
|
|
878
|
+
\`\`\`
|
|
748
879
|
|
|
749
880
|
## Flags
|
|
750
881
|
|
|
751
|
-
- \`--
|
|
882
|
+
- \`--human\` — force human-readable output regardless of TTY
|
|
883
|
+
state (overrides the auto-flip-to-JSON when
|
|
884
|
+
stdout is piped). Mutually exclusive with
|
|
885
|
+
\`--get\`.
|
|
886
|
+
- \`--get <field>\` — extract a single field from the JSON response
|
|
887
|
+
and print only its bare value. Supports dotted
|
|
888
|
+
paths (\`tester_profile.name\`). On a paginated
|
|
889
|
+
\`{items: [...]}\` response, the path
|
|
890
|
+
auto-descends into \`items\` so \`--get alias\`
|
|
891
|
+
on a list yields one value per line. Implies
|
|
892
|
+
\`--json\` internally; mutually exclusive with
|
|
893
|
+
\`--human\`. Strings/numbers/bools are printed
|
|
894
|
+
unquoted; \`null\` prints as an empty line;
|
|
895
|
+
arrays print one element per line; objects
|
|
896
|
+
print as compact one-line JSON. Missing field
|
|
897
|
+
→ exit 2 with a usage error.
|
|
898
|
+
- \`--json\` — force JSON output even on a TTY. Auto-enabled
|
|
899
|
+
when stdout is piped (unless \`--human\` is
|
|
900
|
+
set).
|
|
752
901
|
- \`--fields a,b,c\` — keep only these fields in JSON output (e.g.
|
|
753
902
|
\`alias,name,status\`). Filters per item only;
|
|
754
903
|
list wrappers (\`{items, total, returned,
|
|
@@ -757,7 +906,9 @@ mode is **auto-enabled when stdout is piped**, so an agent rarely needs
|
|
|
757
906
|
write paths) the full server payload instead
|
|
758
907
|
of the compact response.
|
|
759
908
|
- \`-q, --quiet\` — suppress progress messages on stderr (errors
|
|
760
|
-
still go to stderr).
|
|
909
|
+
still go to stderr). \`--get\` implies
|
|
910
|
+
\`--quiet\` so the bare value is the only
|
|
911
|
+
thing on stdout.
|
|
761
912
|
|
|
762
913
|
## Stable shape rules
|
|
763
914
|
|
|
@@ -854,9 +1005,12 @@ The CLI guarantees these contracts so agents can chain safely:
|
|
|
854
1005
|
in the \`testers[]\` array, and a "Failed testers" subsection in
|
|
855
1006
|
human output. Empty when the tester succeeded.
|
|
856
1007
|
- **\`profile list\` emits a stderr pagination hint** when
|
|
857
|
-
\`has_more=true\` and
|
|
858
|
-
|
|
859
|
-
|
|
1008
|
+
\`has_more=true\` and \`--quiet\` is not set. The hint goes to **stderr
|
|
1009
|
+
in every mode** including \`--json\` and piped stdout — it never
|
|
1010
|
+
pollutes machine-readable stdout but is visible to any agent that
|
|
1011
|
+
reads stderr (which they should, for warnings and progress). Format:
|
|
1012
|
+
"showing N–M of TOTAL; pass --offset M --limit N for more."
|
|
1013
|
+
JSON consumers can also read \`has_more\` directly off the envelope.
|
|
860
1014
|
- **\`ask results --json\` adds an \`aggregates\` field per round.** For
|
|
861
1015
|
rounds with \`wants_pick\`/\`wants_ratings\`, the CLI computes the
|
|
862
1016
|
verdict locally so agents don't have to parse comment prose:
|
|
@@ -938,25 +1092,43 @@ a structured error object on **stdout** and a human message on
|
|
|
938
1092
|
## Examples
|
|
939
1093
|
|
|
940
1094
|
\`\`\`
|
|
941
|
-
|
|
942
|
-
ish
|
|
1095
|
+
# Display (table on TTY, JSON when piped):
|
|
1096
|
+
ish workspace list
|
|
1097
|
+
|
|
1098
|
+
# Display preserved through tee/pipe (force human):
|
|
1099
|
+
ish ask results a-6ec --human | tee /tmp/results.txt
|
|
1100
|
+
|
|
1101
|
+
# Capture a single alias to feed into the next command:
|
|
1102
|
+
WS=$(ish workspace list --get alias | head -1)
|
|
1103
|
+
|
|
1104
|
+
# Inspect a nested field:
|
|
1105
|
+
ish study tester t-a17 --get tester_profile.name
|
|
1106
|
+
|
|
1107
|
+
# Chain (full JSON for jq when you need multiple fields):
|
|
1108
|
+
ish study get s-b2c --fields alias,name,status,iterations --json
|
|
943
1109
|
ish ask results a-6ec --round 1 --json
|
|
944
|
-
ish profile generate --description "..." --count 3 --json | jq '.[].alias'
|
|
945
1110
|
\`\`\`
|
|
946
1111
|
|
|
947
1112
|
## Composing commands
|
|
948
1113
|
|
|
949
|
-
|
|
1114
|
+
\`--get\` removes most of the \`jq\` shims agents reach for. Capture in
|
|
1115
|
+
a script, then display the final result back to the user:
|
|
950
1116
|
|
|
951
1117
|
\`\`\`
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1118
|
+
# Capture — bare values, no jq needed:
|
|
1119
|
+
ITER=$(ish iteration create --url https://example.com --get alias)
|
|
1120
|
+
TESTERS=$(ish study run --iteration "$ITER" --sample 5 --country SE --get tester_aliases)
|
|
955
1121
|
for t in $TESTERS; do
|
|
956
1122
|
ish study wait "$t" --timeout 600
|
|
957
1123
|
done
|
|
958
|
-
|
|
1124
|
+
|
|
1125
|
+
# Display the final results to the user, even though we're in a script:
|
|
1126
|
+
ish study results --human
|
|
959
1127
|
\`\`\`
|
|
1128
|
+
|
|
1129
|
+
When you genuinely need multiple fields in one parse pass, \`--json\` is
|
|
1130
|
+
still the right tool — \`--get\` is for single-value capture, not for
|
|
1131
|
+
reshaping output.
|
|
960
1132
|
`;
|
|
961
1133
|
const GUIDE_FIRST_STUDY = `# guide: your first study, end to end
|
|
962
1134
|
|
|
@@ -1098,7 +1270,9 @@ of scope: \`workspace\`, \`config\`, \`docs\`, \`init\`, \`login\`,
|
|
|
1098
1270
|
## Related
|
|
1099
1271
|
|
|
1100
1272
|
- \`reference/aliases\` — the prefix scheme used by every entity.
|
|
1101
|
-
- \`reference/json-mode\` — output
|
|
1273
|
+
- \`reference/json-mode\` — output modes (display vs capture vs chain),
|
|
1274
|
+
including \`--get workspace.alias\` to capture the active workspace
|
|
1275
|
+
without piping \`ish status --json\` through \`jq\`.
|
|
1102
1276
|
`;
|
|
1103
1277
|
const REFERENCE_BILLING_LIMITS = `# reference: billing tier limits
|
|
1104
1278
|
|
|
@@ -1272,8 +1446,8 @@ const PAGES = [
|
|
|
1272
1446
|
},
|
|
1273
1447
|
{
|
|
1274
1448
|
slug: "reference/json-mode",
|
|
1275
|
-
title: "reference:
|
|
1276
|
-
description: "
|
|
1449
|
+
title: "reference: output modes for agents (display, capture, chain)",
|
|
1450
|
+
description: "Display vs capture vs chain: --human, --get, --json, --fields, exit codes, pipe behavior.",
|
|
1277
1451
|
body: REFERENCE_JSON_MODE,
|
|
1278
1452
|
},
|
|
1279
1453
|
{
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
/** Set by withClient() based on global flags. */
|
|
10
10
|
export declare function setVerbose(v: boolean): void;
|
|
11
11
|
export declare function setFields(fields?: string[]): void;
|
|
12
|
+
/**
|
|
13
|
+
* Pattern Ω capture mode: when set, jsonOutput() returns the bare value at
|
|
14
|
+
* the dotted path instead of the full JSON. Cleared between command runs by
|
|
15
|
+
* each invocation of `applyGlobals()`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function setGetField(field?: string): void;
|
|
12
18
|
/** Per-call output options for stable JSON contracts. */
|
|
13
19
|
export interface OutputOptions {
|
|
14
20
|
/**
|