@ishlabs/cli 0.8.1 → 0.8.3

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.
Files changed (70) hide show
  1. package/README.md +323 -21
  2. package/dist/auth.d.ts +17 -1
  3. package/dist/auth.js +62 -9
  4. package/dist/commands/ask.d.ts +5 -0
  5. package/dist/commands/ask.js +722 -0
  6. package/dist/commands/config.js +25 -1
  7. package/dist/commands/docs.d.ts +17 -0
  8. package/dist/commands/docs.js +147 -0
  9. package/dist/commands/init.d.ts +16 -0
  10. package/dist/commands/init.js +182 -0
  11. package/dist/commands/iteration.d.ts +5 -1
  12. package/dist/commands/iteration.js +243 -31
  13. package/dist/commands/profile.d.ts +5 -0
  14. package/dist/commands/profile.js +313 -0
  15. package/dist/commands/source.d.ts +10 -0
  16. package/dist/commands/source.js +78 -0
  17. package/dist/commands/study-run.d.ts +11 -0
  18. package/dist/commands/study-run.js +552 -0
  19. package/dist/commands/study-tester.d.ts +8 -0
  20. package/dist/commands/study-tester.js +149 -0
  21. package/dist/commands/study.js +145 -70
  22. package/dist/commands/workspace.js +193 -7
  23. package/dist/config.d.ts +3 -1
  24. package/dist/config.js +10 -10
  25. package/dist/connect.d.ts +4 -1
  26. package/dist/connect.js +127 -94
  27. package/dist/index.js +82 -34
  28. package/dist/lib/alias-store.d.ts +3 -0
  29. package/dist/lib/alias-store.js +9 -7
  30. package/dist/lib/api-client.d.ts +9 -6
  31. package/dist/lib/api-client.js +87 -26
  32. package/dist/lib/ask-questions.d.ts +9 -0
  33. package/dist/lib/ask-questions.js +35 -0
  34. package/dist/lib/ask-variants.d.ts +48 -0
  35. package/dist/lib/ask-variants.js +236 -0
  36. package/dist/lib/auth.d.ts +1 -1
  37. package/dist/lib/auth.js +24 -8
  38. package/dist/lib/colors.d.ts +30 -0
  39. package/dist/lib/colors.js +48 -0
  40. package/dist/lib/command-helpers.d.ts +74 -0
  41. package/dist/lib/command-helpers.js +232 -6
  42. package/dist/lib/docs.d.ts +32 -0
  43. package/dist/lib/docs.js +930 -0
  44. package/dist/lib/local-sim/browser.d.ts +0 -1
  45. package/dist/lib/local-sim/browser.js +0 -2
  46. package/dist/lib/local-sim/install.d.ts +2 -12
  47. package/dist/lib/local-sim/install.js +22 -30
  48. package/dist/lib/output.d.ts +25 -3
  49. package/dist/lib/output.js +465 -20
  50. package/dist/lib/paths.d.ts +14 -0
  51. package/dist/lib/paths.js +36 -0
  52. package/dist/lib/profile-sources.d.ts +55 -0
  53. package/dist/lib/profile-sources.js +157 -0
  54. package/dist/lib/site-access.d.ts +80 -0
  55. package/dist/lib/site-access.js +188 -0
  56. package/dist/lib/skill-content.d.ts +31 -0
  57. package/dist/lib/skill-content.js +462 -0
  58. package/dist/lib/study-inputs.d.ts +20 -0
  59. package/dist/lib/study-inputs.js +72 -0
  60. package/dist/lib/types.d.ts +207 -9
  61. package/dist/lib/types.js +7 -0
  62. package/dist/lib/upload.js +2 -2
  63. package/dist/upgrade.js +11 -1
  64. package/package.json +3 -2
  65. package/dist/commands/simulation.d.ts +0 -10
  66. package/dist/commands/simulation.js +0 -647
  67. package/dist/commands/tester-profile.d.ts +0 -5
  68. package/dist/commands/tester-profile.js +0 -109
  69. package/dist/commands/tester.d.ts +0 -5
  70. package/dist/commands/tester.js +0 -73
@@ -7,8 +7,160 @@ import { resolveApiUrl, resolveToken } from "./auth.js";
7
7
  import { getAppUrl } from "../auth.js";
8
8
  import { ApiClient, ApiError } from "./api-client.js";
9
9
  import { outputError, setVerbose, setFields } from "./output.js";
10
+ import { setColorsEnabled, colorsEnabled } from "./colors.js";
10
11
  import { loadConfig } from "../config.js";
11
12
  import { resolveId } from "./alias-store.js";
13
+ function isSimulatable(p) {
14
+ return Boolean(p.simulation_config_id) || Boolean(p.simulation_config);
15
+ }
16
+ function shuffleInPlace(arr) {
17
+ for (let i = arr.length - 1; i > 0; i--) {
18
+ const j = Math.floor(Math.random() * (i + 1));
19
+ [arr[i], arr[j]] = [arr[j], arr[i]];
20
+ }
21
+ return arr;
22
+ }
23
+ function parseSampleSize(raw) {
24
+ if (raw === undefined)
25
+ return undefined;
26
+ const n = parseInt(raw, 10);
27
+ if (Number.isNaN(n) || n <= 0) {
28
+ throw new Error(`--sample must be a positive integer, got "${raw}".`);
29
+ }
30
+ return n;
31
+ }
32
+ function describeFilters(flags) {
33
+ const parts = [];
34
+ if (flags.search)
35
+ parts.push(`--search "${flags.search}"`);
36
+ if (flags.gender?.length)
37
+ parts.push(...flags.gender.map((g) => `--gender ${g}`));
38
+ if (flags.country?.length)
39
+ parts.push(...flags.country.map((c) => `--country ${c}`));
40
+ if (flags.minAge)
41
+ parts.push(`--min-age ${flags.minAge}`);
42
+ if (flags.maxAge)
43
+ parts.push(`--max-age ${flags.maxAge}`);
44
+ if (flags.visibility)
45
+ parts.push(`--visibility ${flags.visibility}`);
46
+ return parts.join(" ");
47
+ }
48
+ function hasFilterFlag(flags) {
49
+ return Boolean(flags.search
50
+ || (flags.gender && flags.gender.length > 0)
51
+ || (flags.country && flags.country.length > 0)
52
+ || flags.minAge
53
+ || flags.maxAge
54
+ || flags.visibility);
55
+ }
56
+ /** True when any audience-selection flag is set on the command. */
57
+ export function hasAudienceFlags(flags) {
58
+ return Boolean((flags.profile && flags.profile.length > 0)
59
+ || flags.sample !== undefined
60
+ || flags.all
61
+ || hasFilterFlag(flags));
62
+ }
63
+ /**
64
+ * Resolve a tester-profile audience from CLI flags.
65
+ *
66
+ * Modes (mutually exclusive):
67
+ * - explicit: `--profile <id>` (repeatable) — returns those IDs verbatim.
68
+ * - sample: `--sample N` + optional filter flags — fetches the matching
69
+ * pool, randomly samples N.
70
+ * - all: caller's "all" flag + optional filter flags — returns every
71
+ * matching profile.
72
+ * - filtered: filter flags only — treated as "all matching".
73
+ *
74
+ * Always sends `type=ai` to the backend (simulations are AI-driven).
75
+ * If `requireSimulatable`, applies a client-side filter for profiles that
76
+ * have a simulation config attached.
77
+ */
78
+ export async function resolveAudienceProfileIds(client, workspace, flags, opts = {}) {
79
+ const allFlagName = opts.allFlagName ?? "--all";
80
+ const explicit = (flags.profile ?? []).map(resolveId);
81
+ const sampleN = parseSampleSize(flags.sample);
82
+ const filtersUsed = hasFilterFlag(flags);
83
+ if (explicit.length > 0) {
84
+ if (sampleN !== undefined || flags.all || filtersUsed) {
85
+ throw new Error(`Use either explicit --profile flags or --sample/${allFlagName}/filter flags (--country, --gender, --min-age, --max-age, --search, --visibility), not both.`);
86
+ }
87
+ return explicit;
88
+ }
89
+ if (sampleN === undefined && !flags.all && !filtersUsed) {
90
+ throw new Error(`Pick an audience: pass --profile <id> (repeatable), --sample <N>, ${allFlagName}, or filter flags (--country, --gender, --min-age, --max-age, --search, --visibility).`);
91
+ }
92
+ const params = {
93
+ product_id: workspace,
94
+ type: "ai",
95
+ limit: "200",
96
+ offset: "0",
97
+ };
98
+ if (flags.search)
99
+ params.search = flags.search;
100
+ if (flags.gender && flags.gender.length > 0)
101
+ params.gender = flags.gender;
102
+ if (flags.country && flags.country.length > 0)
103
+ params.country = flags.country;
104
+ if (flags.minAge)
105
+ params.min_age = flags.minAge;
106
+ if (flags.maxAge)
107
+ params.max_age = flags.maxAge;
108
+ if (flags.visibility)
109
+ params.visibility = flags.visibility;
110
+ const data = await client.get("/tester-profiles", params);
111
+ const items = Array.isArray(data)
112
+ ? data
113
+ : Array.isArray(data?.items)
114
+ ? data.items
115
+ : [];
116
+ let pool = items;
117
+ if (opts.requireSimulatable) {
118
+ pool = pool.filter(isSimulatable);
119
+ }
120
+ if (opts.excludeProfileIds && opts.excludeProfileIds.size > 0) {
121
+ pool = pool.filter((p) => !opts.excludeProfileIds.has(p.id));
122
+ }
123
+ if (pool.length === 0) {
124
+ const filterDesc = describeFilters(flags);
125
+ const sim = opts.requireSimulatable ? "simulatable AI " : "AI ";
126
+ if (opts.excludeProfileIds && opts.excludeProfileIds.size > 0 && !filterDesc) {
127
+ throw new Error("All matching profiles are already in this audience.");
128
+ }
129
+ if (filterDesc) {
130
+ throw new Error(`No ${sim}tester profiles in workspace ${workspace} match: ${filterDesc}. Broaden your filters or run \`ish profile list\` to inspect the pool.`);
131
+ }
132
+ throw new Error(`No ${sim}tester profiles found in workspace ${workspace}.${opts.requireSimulatable ? " Create profiles with simulation configs first." : ""}`);
133
+ }
134
+ if (flags.all)
135
+ return pool.map((p) => p.id);
136
+ if (sampleN !== undefined) {
137
+ if (sampleN > pool.length) {
138
+ throw new Error(`--sample ${sampleN} requested but only ${pool.length} matching profile${pool.length === 1 ? "" : "s"} available.`);
139
+ }
140
+ return shuffleInPlace([...pool]).slice(0, sampleN).map((p) => p.id);
141
+ }
142
+ // Filters only, no --sample/--all → return every match.
143
+ return pool.map((p) => p.id);
144
+ }
145
+ /**
146
+ * Attach the audience-selection flag set to a Commander command.
147
+ * Pass `allFlagName: "--all-simulatable"` for ask commands to keep the
148
+ * established flag name; defaults to `--all` for study run.
149
+ */
150
+ export function addAudienceFilterFlags(cmd, opts = {}) {
151
+ const allFlag = opts.allFlagName ?? "--all";
152
+ const allDesc = opts.allFlagDescription ?? "Use every profile matching the filters";
153
+ return cmd
154
+ .option("--profile <ids>", "Tester profile IDs/aliases (comma-separated or repeatable)", collectIds, [])
155
+ .option("--sample <N>", "Randomly sample N profiles from the matching pool")
156
+ .option(allFlag, allDesc)
157
+ .option("--search <text>", "Free-text search (matches profile name and bio)")
158
+ .option("--gender <gender>", "Filter by gender (repeatable)", collectRepeatable, [])
159
+ .option("--country <code>", "Filter by 2-letter country code (repeatable)", collectRepeatable, [])
160
+ .option("--min-age <n>", "Minimum age (inclusive)")
161
+ .option("--max-age <n>", "Maximum age (inclusive)")
162
+ .option("--visibility <v>", "Filter by visibility (private|public)");
163
+ }
12
164
  export function getGlobals(cmd) {
13
165
  const opts = cmd.optsWithGlobals();
14
166
  // Auto-switch to JSON when stdout is piped (non-TTY)
@@ -17,16 +169,28 @@ export function getGlobals(cmd) {
17
169
  const fields = opts.fields
18
170
  ? String(opts.fields).split(",").map((f) => f.trim()).filter(Boolean)
19
171
  : undefined;
172
+ // Commander's negatable boolean: --no-color sets color=false, default is undefined.
173
+ // Fall back to the env-aware default from colors.ts.
174
+ const color = opts.color === false ? false : (json ? false : colorsEnabled());
20
175
  return {
21
176
  token: opts.token,
177
+ tokenFile: opts.tokenFile,
22
178
  apiUrl: opts.apiUrl,
23
179
  dev: opts.dev,
24
180
  json,
25
181
  verbose: opts.verbose ?? false,
26
182
  quiet: opts.quiet ?? (json && !opts.json), // auto-quiet when auto-json via pipe
183
+ color,
27
184
  fields,
28
185
  };
29
186
  }
187
+ /**
188
+ * Semantic exit codes used across the CLI.
189
+ * 0=success, 1=general error, 2=usage/validation, 3=auth, 4=not found, 5=transient.
190
+ * `EXIT_USAGE` matches the POSIX convention for argv/usage errors and is
191
+ * what `program.exitOverride()` returns from Commander-level failures.
192
+ */
193
+ export const EXIT_USAGE = 2;
30
194
  /**
31
195
  * Map errors to semantic exit codes.
32
196
  * 0=success, 1=general, 2=usage/validation, 3=auth, 4=not found, 5=transient
@@ -42,20 +206,29 @@ export function exitCodeFromError(err) {
42
206
  if (err.retryable)
43
207
  return 5;
44
208
  }
45
- // Auth-related client errors (e.g. missing token)
46
- if (err instanceof Error && /no auth token found|run "ish login"|saved token is invalid|session expired/i.test(err.message))
47
- return 3;
209
+ if (err instanceof Error) {
210
+ // Auth-related client errors (e.g. missing token)
211
+ if (/no auth token found|run "ish login"|saved token is invalid|session expired/i.test(err.message))
212
+ return 3;
213
+ // Client-side validation failures
214
+ if (err.name === "ValidationError" || /^invalid |^cannot read |is empty:|--\w[\w-]* must be|pick an audience|use either /i.test(err.message))
215
+ return 2;
216
+ }
48
217
  return 1;
49
218
  }
50
219
  export async function createClient(globals) {
51
220
  const apiUrl = resolveApiUrl(globals.apiUrl, globals.dev);
52
- const token = await resolveToken(globals.token, apiUrl);
221
+ const token = await resolveToken(globals.token, apiUrl, globals.tokenFile);
53
222
  return new ApiClient({ apiUrl, token });
54
223
  }
55
- export async function withClient(cmd, fn) {
56
- const globals = getGlobals(cmd);
224
+ function applyGlobals(globals) {
57
225
  setVerbose(globals.verbose);
58
226
  setFields(globals.fields);
227
+ setColorsEnabled(globals.color);
228
+ }
229
+ export async function withClient(cmd, fn) {
230
+ const globals = getGlobals(cmd);
231
+ applyGlobals(globals);
59
232
  try {
60
233
  const client = await createClient(globals);
61
234
  await fn(client, globals);
@@ -65,6 +238,22 @@ export async function withClient(cmd, fn) {
65
238
  process.exit(exitCodeFromError(err));
66
239
  }
67
240
  }
241
+ /**
242
+ * Wrap an inline (non-API) command action with the same uniform error
243
+ * formatting + exit codes used by withClient. Use this for actions that
244
+ * don't need an ApiClient — login, logout, connect, etc.
245
+ */
246
+ export async function runInline(cmd, fn) {
247
+ const globals = getGlobals(cmd);
248
+ applyGlobals(globals);
249
+ try {
250
+ await fn(globals);
251
+ }
252
+ catch (err) {
253
+ outputError(err, globals.json);
254
+ process.exit(exitCodeFromError(err));
255
+ }
256
+ }
68
257
  export function getWebUrl(globals, path) {
69
258
  const base = globals.dev ? "http://localhost:3000" : getAppUrl();
70
259
  return `${base}${path}`;
@@ -129,3 +318,40 @@ export function resolveStudy(explicit) {
129
318
  return config.study;
130
319
  throw new Error('No study set. Use `ish study use <alias>` or pass --study.');
131
320
  }
321
+ export function resolveAsk(explicit) {
322
+ if (explicit)
323
+ return resolveId(explicit);
324
+ const env = process.env.ISH_ASK;
325
+ if (env)
326
+ return resolveId(env);
327
+ const config = loadConfig();
328
+ if (config.ask)
329
+ return config.ask;
330
+ throw new Error('No ask set. Use `ish ask use <alias>` or pass the ask ID as an argument.');
331
+ }
332
+ /** Commander option-collector for repeatable flags (e.g. `--variant text:"..."` repeated). */
333
+ export function collectRepeatable(value, prev = []) {
334
+ return prev.concat([value]);
335
+ }
336
+ /**
337
+ * Commander collector for ID-list flags. Accepts a comma-separated value
338
+ * (`--profile a,b,c`) and also concatenates across repeated flags
339
+ * (`--profile a --profile b`). Trims whitespace and drops empty entries.
340
+ *
341
+ * Use this for flags whose values are IDs/aliases — never for free-form
342
+ * strings that may legitimately contain commas (those should use
343
+ * `collectRepeatable`).
344
+ */
345
+ export function collectIds(value, prev = []) {
346
+ return prev.concat(value.split(",").map((s) => s.trim()).filter(Boolean));
347
+ }
348
+ /** Parse a `--timeout <seconds>` flag into milliseconds, with validation. */
349
+ export function parseWaitTimeout(raw, defaultMs = 5 * 60 * 1000) {
350
+ if (raw === undefined)
351
+ return defaultMs;
352
+ const n = parseInt(raw, 10);
353
+ if (Number.isNaN(n) || n <= 0) {
354
+ throw new Error(`--timeout must be a positive integer (seconds), got "${raw}".`);
355
+ }
356
+ return n * 1000;
357
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ish docs — In-binary documentation aimed at AI coding agents.
3
+ *
4
+ * Pages ship as TypeScript string constants so they survive both the `tsc`
5
+ * build and the `bun build --compile` single-binary build. Slugs use
6
+ * forward-slashes so the surface mirrors a doc tree (`concepts/study`).
7
+ */
8
+ export interface DocPage {
9
+ slug: string;
10
+ title: string;
11
+ /** One-line description shown in `ish docs list` and search. */
12
+ description: string;
13
+ body: string;
14
+ }
15
+ export declare function listPages(): DocPage[];
16
+ export declare function getPage(slug: string): DocPage | undefined;
17
+ /** Concise pointer printed at the bottom of `--help` outputs. */
18
+ export declare const AGENT_HELP_FOOTER: string;
19
+ export interface SearchHit {
20
+ slug: string;
21
+ title: string;
22
+ description: string;
23
+ /** Highest-scoring match snippet, max ~200 chars. */
24
+ snippet: string;
25
+ score: number;
26
+ }
27
+ /**
28
+ * Substring search over title + description + body. Title hits score
29
+ * highest, then description, then body. Returns at most `limit` hits
30
+ * sorted by score desc.
31
+ */
32
+ export declare function searchPages(query: string, limit?: number): SearchHit[];