@ishlabs/cli 0.8.5 → 0.10.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 +55 -6
- package/dist/auth.d.ts +23 -4
- package/dist/auth.js +165 -39
- package/dist/commands/ask.d.ts +12 -0
- package/dist/commands/ask.js +127 -2
- package/dist/commands/chat.d.ts +17 -0
- package/dist/commands/chat.js +589 -0
- package/dist/commands/iteration.js +232 -13
- package/dist/commands/secret.d.ts +20 -0
- package/dist/commands/secret.js +246 -0
- package/dist/commands/source.js +24 -2
- package/dist/commands/study-run.d.ts +38 -0
- package/dist/commands/study-run.js +199 -80
- package/dist/commands/study-tester.js +17 -2
- package/dist/commands/study.js +311 -39
- package/dist/commands/workspace.js +81 -0
- package/dist/config.d.ts +7 -0
- package/dist/connect.d.ts +3 -0
- package/dist/connect.js +359 -24
- package/dist/index.js +67 -9
- package/dist/lib/alias-hydrate.d.ts +42 -0
- package/dist/lib/alias-hydrate.js +175 -0
- package/dist/lib/alias-store.d.ts +1 -0
- package/dist/lib/alias-store.js +28 -1
- package/dist/lib/auth.js +11 -3
- package/dist/lib/chat-endpoint-formatters.d.ts +39 -0
- package/dist/lib/chat-endpoint-formatters.js +104 -0
- package/dist/lib/command-helpers.d.ts +18 -0
- package/dist/lib/command-helpers.js +188 -53
- package/dist/lib/docs.js +662 -34
- package/dist/lib/modality.d.ts +42 -0
- package/dist/lib/modality.js +192 -0
- package/dist/lib/output.d.ts +41 -0
- package/dist/lib/output.js +453 -19
- package/dist/lib/paths.d.ts +1 -0
- package/dist/lib/paths.js +3 -0
- package/dist/lib/skill-content.js +183 -13
- package/dist/lib/types.d.ts +15 -0
- package/package.json +3 -3
|
@@ -45,6 +45,77 @@ function describeFilters(flags) {
|
|
|
45
45
|
parts.push(`--visibility ${flags.visibility}`);
|
|
46
46
|
return parts.join(" ");
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Best-effort: surface the top-3 populated countries so an empty-audience
|
|
50
|
+
* error doesn't strand the agent without a pivot. Two tiers — tier 1 keeps
|
|
51
|
+
* non-country filters and drops `--country` (used when `--country` was the
|
|
52
|
+
* binding constraint); tier 2 fires when tier 1 returns empty (or when
|
|
53
|
+
* `--country` wasn't set at all) and drops every filter so the workspace's
|
|
54
|
+
* overall country distribution is the fallback. Returns the empty string on
|
|
55
|
+
* any failure — never replaces the primary error with a secondary one.
|
|
56
|
+
*/
|
|
57
|
+
async function suggestCountries(client, workspace, flags, opts) {
|
|
58
|
+
const countOf = (items) => {
|
|
59
|
+
const counts = new Map();
|
|
60
|
+
for (const p of items) {
|
|
61
|
+
const c = typeof p.country === "string" ? p.country : null;
|
|
62
|
+
if (c)
|
|
63
|
+
counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
64
|
+
}
|
|
65
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
66
|
+
};
|
|
67
|
+
const fetchFacet = async (keepOtherFilters) => {
|
|
68
|
+
try {
|
|
69
|
+
const broader = {
|
|
70
|
+
product_id: workspace,
|
|
71
|
+
type: "ai",
|
|
72
|
+
limit: "500",
|
|
73
|
+
offset: "0",
|
|
74
|
+
};
|
|
75
|
+
if (keepOtherFilters) {
|
|
76
|
+
if (flags.search)
|
|
77
|
+
broader.search = flags.search;
|
|
78
|
+
if (flags.gender && flags.gender.length > 0)
|
|
79
|
+
broader.gender = flags.gender;
|
|
80
|
+
if (flags.minAge)
|
|
81
|
+
broader.min_age = flags.minAge;
|
|
82
|
+
if (flags.maxAge)
|
|
83
|
+
broader.max_age = flags.maxAge;
|
|
84
|
+
if (flags.visibility)
|
|
85
|
+
broader.visibility = flags.visibility;
|
|
86
|
+
}
|
|
87
|
+
const data = await client.get("/tester-profiles", broader);
|
|
88
|
+
const items = Array.isArray(data)
|
|
89
|
+
? data
|
|
90
|
+
: Array.isArray(data?.items)
|
|
91
|
+
? data.items
|
|
92
|
+
: [];
|
|
93
|
+
const pool = opts.requireSimulatable ? items.filter(isSimulatable) : items;
|
|
94
|
+
return countOf(pool);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const countrySet = !!(flags.country && flags.country.length > 0);
|
|
101
|
+
// Tier 1: when --country is set, drop just country and keep other filters.
|
|
102
|
+
// When --country isn't set, skip straight to the unfiltered fallback —
|
|
103
|
+
// tier 1 with `keepOtherFilters=false` would re-issue the same query that
|
|
104
|
+
// already returned 0.
|
|
105
|
+
let top = [];
|
|
106
|
+
let label = "Populated countries";
|
|
107
|
+
if (countrySet) {
|
|
108
|
+
top = await fetchFacet(true);
|
|
109
|
+
label = "Populated countries with these other filters";
|
|
110
|
+
}
|
|
111
|
+
if (top.length === 0) {
|
|
112
|
+
top = await fetchFacet(false);
|
|
113
|
+
label = "Populated countries";
|
|
114
|
+
}
|
|
115
|
+
if (top.length === 0)
|
|
116
|
+
return "";
|
|
117
|
+
return ` ${label}: ${top.map(([c, n]) => `${c} (${n})`).join(", ")}.`;
|
|
118
|
+
}
|
|
48
119
|
function hasFilterFlag(flags) {
|
|
49
120
|
return Boolean(flags.search
|
|
50
121
|
|| (flags.gender && flags.gender.length > 0)
|
|
@@ -129,56 +200,18 @@ export async function resolveAudienceProfileIds(client, workspace, flags, opts =
|
|
|
129
200
|
if (opts.excludeProfileIds && opts.excludeProfileIds.size > 0 && !filterDesc) {
|
|
130
201
|
throw new Error("All matching profiles are already in this audience.");
|
|
131
202
|
}
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
}
|
|
203
|
+
// Country suggestion: surface the top populated countries so the agent
|
|
204
|
+
// doesn't have to round-trip through `ish profile list` to find one that
|
|
205
|
+
// matches. Two tiers — tier 1 drops `--country` only and retains other
|
|
206
|
+
// filters when `--country` was set (so the hint reflects "of countries
|
|
207
|
+
// that match your other filters, here are the populated ones"). Tier 2
|
|
208
|
+
// fires when tier 1 returns nothing *and* tier 1 was scoped, or when
|
|
209
|
+
// `--country` wasn't the constraint to begin with: drop every filter and
|
|
210
|
+
// surface the workspace's overall country distribution. Pure best-effort
|
|
211
|
+
// — any failure falls back to the original error.
|
|
212
|
+
const suggestion = await suggestCountries(client, workspace, flags, {
|
|
213
|
+
requireSimulatable: !!opts.requireSimulatable,
|
|
214
|
+
});
|
|
182
215
|
if (filterDesc) {
|
|
183
216
|
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.`);
|
|
184
217
|
}
|
|
@@ -315,6 +348,20 @@ export function exitCodeFromError(err) {
|
|
|
315
348
|
// Client-side validation failures
|
|
316
349
|
if (err.name === "ValidationError" || /^invalid |^cannot read |is empty:|--\w[\w-]* must be|pick an audience|use either /i.test(err.message))
|
|
317
350
|
return 2;
|
|
351
|
+
// Errors that pre-declare their own retryability / error_code take
|
|
352
|
+
// precedence — WaitTimeoutError sets `error_code: "wait_timeout"`
|
|
353
|
+
// and `retryable: true`, so callers can branch on exit 5 (transient)
|
|
354
|
+
// distinct from the api-client's generic 408/timeout family.
|
|
355
|
+
const tagged = err;
|
|
356
|
+
if (tagged.error_code === "wait_timeout")
|
|
357
|
+
return 5;
|
|
358
|
+
if (typeof tagged.retryable === "boolean" && tagged.retryable)
|
|
359
|
+
return 5;
|
|
360
|
+
// Structured error_kind on the Error object (set by chat endpoint test/init,
|
|
361
|
+
// simulation routes, etc.). TunnelInactive is the canonical transient one.
|
|
362
|
+
const kind = err.error_kind;
|
|
363
|
+
if (typeof kind === "string" && kind === "TunnelInactive")
|
|
364
|
+
return 5;
|
|
318
365
|
}
|
|
319
366
|
return 1;
|
|
320
367
|
}
|
|
@@ -406,6 +453,18 @@ export function readJsonFileOrStdin(filePath) {
|
|
|
406
453
|
process.stdin.on("error", reject);
|
|
407
454
|
});
|
|
408
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Build the suggested re-invocation example by taking the live argv and
|
|
458
|
+
* appending `--yes` if it isn't already there. Strips the `node` /
|
|
459
|
+
* `dist/index.js` prefix so the example reads as a normal `ish` command.
|
|
460
|
+
*/
|
|
461
|
+
function buildConfirmationExample() {
|
|
462
|
+
const argv = process.argv.slice(2);
|
|
463
|
+
const args = argv.includes("--yes") || argv.includes("-y")
|
|
464
|
+
? argv
|
|
465
|
+
: [...argv, "--yes"];
|
|
466
|
+
return ["ish", ...args].join(" ");
|
|
467
|
+
}
|
|
409
468
|
/**
|
|
410
469
|
* Prompt for confirmation of a destructive action, or short-circuit when
|
|
411
470
|
* `--yes` is set. In `--json` mode without `--yes` we refuse with a usage
|
|
@@ -418,11 +477,15 @@ export async function confirmDestructive(prompt, opts) {
|
|
|
418
477
|
if (opts.json) {
|
|
419
478
|
const err = new Error(`--yes is required for destructive actions in --json mode. Refusing to proceed without explicit confirmation.`);
|
|
420
479
|
err.name = "ValidationError";
|
|
480
|
+
err.error_kind = "ConfirmationRequired";
|
|
481
|
+
err.example = buildConfirmationExample();
|
|
421
482
|
throw err;
|
|
422
483
|
}
|
|
423
484
|
if (!process.stdin.isTTY) {
|
|
424
485
|
const err = new Error(`--yes is required for destructive actions when stdin is not a TTY. Refusing to proceed without explicit confirmation.`);
|
|
425
486
|
err.name = "ValidationError";
|
|
487
|
+
err.error_kind = "ConfirmationRequired";
|
|
488
|
+
err.example = buildConfirmationExample();
|
|
426
489
|
throw err;
|
|
427
490
|
}
|
|
428
491
|
process.stderr.write(`${prompt} [y/N] `);
|
|
@@ -453,6 +516,23 @@ export async function confirmDestructive(prompt, opts) {
|
|
|
453
516
|
throw err;
|
|
454
517
|
}
|
|
455
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* Construct a structured "no active <thing>" Error so the JSON envelope from
|
|
521
|
+
* `outputError` carries `error_code` + `suggestions` rather than a generic
|
|
522
|
+
* `client_error`. Pattern A (Sprint 2): when the active study/workspace
|
|
523
|
+
* evaporates between commands, agents need to branch on the error code, not
|
|
524
|
+
* scrape prose. Without this, downstream modality validation in
|
|
525
|
+
* `iteration create` would surface a misleading "Image iterations require
|
|
526
|
+
* --image-urls" message instead of the real "no active study" cause.
|
|
527
|
+
*/
|
|
528
|
+
function noActiveContextError(message, errorCode, suggestions) {
|
|
529
|
+
const err = new Error(message);
|
|
530
|
+
err.name = "NoActiveContextError";
|
|
531
|
+
err.error_code = errorCode;
|
|
532
|
+
err.retryable = false;
|
|
533
|
+
err.suggestions = suggestions;
|
|
534
|
+
return err;
|
|
535
|
+
}
|
|
456
536
|
export function resolveWorkspace(explicit) {
|
|
457
537
|
if (explicit)
|
|
458
538
|
return resolveId(explicit);
|
|
@@ -467,7 +547,10 @@ export function resolveWorkspace(explicit) {
|
|
|
467
547
|
const config = loadConfig();
|
|
468
548
|
if (config.workspace)
|
|
469
549
|
return config.workspace;
|
|
470
|
-
throw
|
|
550
|
+
throw noActiveContextError('No active workspace. Run `ish workspace use <workspace-id>` or pass --workspace <id>.', "no_active_workspace", [
|
|
551
|
+
"ish workspace list",
|
|
552
|
+
"ish workspace use <workspace-id>",
|
|
553
|
+
]);
|
|
471
554
|
}
|
|
472
555
|
export function resolveStudy(explicit) {
|
|
473
556
|
if (explicit)
|
|
@@ -478,7 +561,10 @@ export function resolveStudy(explicit) {
|
|
|
478
561
|
const config = loadConfig();
|
|
479
562
|
if (config.study)
|
|
480
563
|
return config.study;
|
|
481
|
-
throw
|
|
564
|
+
throw noActiveContextError('No active study. Run `ish study use <study-id>` or pass --study <id>.', "no_active_study", [
|
|
565
|
+
"ish study use <study-id>",
|
|
566
|
+
"ish iteration create --study <study-id> ...",
|
|
567
|
+
]);
|
|
482
568
|
}
|
|
483
569
|
export function resolveAsk(explicit) {
|
|
484
570
|
if (explicit)
|
|
@@ -489,7 +575,28 @@ export function resolveAsk(explicit) {
|
|
|
489
575
|
const config = loadConfig();
|
|
490
576
|
if (config.ask)
|
|
491
577
|
return config.ask;
|
|
492
|
-
throw
|
|
578
|
+
throw noActiveContextError('No active ask. Run `ish ask use <ask-id>` or pass the ask ID as an argument.', "no_active_ask", [
|
|
579
|
+
"ish ask list",
|
|
580
|
+
"ish ask use <ask-id>",
|
|
581
|
+
]);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Resolve a chat endpoint id from (in order): the positional argument, the
|
|
585
|
+
* `--endpoint <id>` flag, the `ISH_CHAT_ENDPOINT` env var, or the active
|
|
586
|
+
* endpoint persisted by `ish chat endpoint use`. Throws when none are set.
|
|
587
|
+
*/
|
|
588
|
+
export function resolveChatEndpoint(positional, flag) {
|
|
589
|
+
if (positional)
|
|
590
|
+
return resolveId(positional);
|
|
591
|
+
if (flag)
|
|
592
|
+
return resolveId(flag);
|
|
593
|
+
const env = process.env.ISH_CHAT_ENDPOINT;
|
|
594
|
+
if (env)
|
|
595
|
+
return resolveId(env);
|
|
596
|
+
const config = loadConfig();
|
|
597
|
+
if (config.chat_endpoint)
|
|
598
|
+
return config.chat_endpoint;
|
|
599
|
+
throw new Error('No chat endpoint set. Use `ish chat endpoint use <id>`, pass the endpoint id, or set --endpoint.');
|
|
493
600
|
}
|
|
494
601
|
/** Commander option-collector for repeatable flags (e.g. `--variant text:"..."` repeated). */
|
|
495
602
|
export function collectRepeatable(value, prev = []) {
|
|
@@ -538,6 +645,34 @@ const WORKSPACE_SCOPED_GROUPS = new Set([
|
|
|
538
645
|
* body. Resolvers (`resolveWorkspace`, `resolveAudienceProfileIds`) ignore
|
|
539
646
|
* unused values.
|
|
540
647
|
*/
|
|
648
|
+
/**
|
|
649
|
+
* Read a `--*-config <file>` style flag value, treating "-" as "read from
|
|
650
|
+
* stdin" and any other value as a file path on disk. Trailing newlines on
|
|
651
|
+
* stdin input are stripped so the resulting string parses cleanly as JSON.
|
|
652
|
+
*
|
|
653
|
+
* Throws when "-" is passed but stdin is a TTY (no upstream pipe).
|
|
654
|
+
*
|
|
655
|
+
* Mirrors the readSecretFlag pattern in src/commands/workspace.ts; extracted
|
|
656
|
+
* so every `--<x>-config <file>` flag across commands shares one
|
|
657
|
+
* implementation.
|
|
658
|
+
*/
|
|
659
|
+
export async function readFileOrStdin(path) {
|
|
660
|
+
if (path === "-") {
|
|
661
|
+
if (process.stdin.isTTY) {
|
|
662
|
+
throw new Error('Use "-" only when piping the value on stdin.');
|
|
663
|
+
}
|
|
664
|
+
return await new Promise((resolve, reject) => {
|
|
665
|
+
let data = "";
|
|
666
|
+
process.stdin.setEncoding("utf-8");
|
|
667
|
+
process.stdin.on("data", (chunk) => {
|
|
668
|
+
data += chunk;
|
|
669
|
+
});
|
|
670
|
+
process.stdin.on("end", () => resolve(data.replace(/\r?\n$/, "")));
|
|
671
|
+
process.stdin.on("error", reject);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return fs.readFileSync(path, "utf-8");
|
|
675
|
+
}
|
|
541
676
|
export function injectGlobalWorkspaceOption(program) {
|
|
542
677
|
const walk = (cmd) => {
|
|
543
678
|
if (cmd.commands.length === 0) {
|