@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,7 +7,14 @@ import { output, formatConfigList } from "../lib/output.js";
7
7
  export function registerConfigCommands(program) {
8
8
  const config = program
9
9
  .command("config")
10
- .description("Manage simulation configs");
10
+ .description("Manage simulation configs")
11
+ .addHelpText("after", `
12
+ A simulation config tunes how testers behave during a run (model, timing, retries, etc.).
13
+ Pass \`--config <id>\` to \`ish study run\` to override the default for one dispatch.
14
+
15
+ Configs are global — not scoped to a workspace. The --workspace flag is rejected. Use aliases (c-...) or UUIDs to identify configs.
16
+
17
+ Run \`ish docs overview\` for the full mental model.`);
11
18
  config
12
19
  .command("list")
13
20
  .description("List all simulation configs")
@@ -22,9 +29,25 @@ export function registerConfigCommands(program) {
22
29
  .command("create")
23
30
  .description("Create a simulation config")
24
31
  .requiredOption("--file <path>", "JSON file with config data")
32
+ .addHelpText("after", "\nMinimal body: {\"name\": \"...\", \"model_settings\": {...}}. See `ish config schema` for the full JSON Schema.")
25
33
  .action(async (opts, cmd) => {
26
34
  await withClient(cmd, async (client, globals) => {
27
35
  const body = await readJsonFileOrStdin(opts.file);
36
+ const proposedName = typeof body.name === "string" ? body.name : "";
37
+ // Names are informational; aliases (c-...) are canonical. Warn if a
38
+ // config with the same name already exists so the user isn't surprised
39
+ // when a list shows duplicates.
40
+ if (proposedName) {
41
+ try {
42
+ const existing = await client.get("/dev/simulation-configs");
43
+ if (existing.some((c) => c.name === proposedName)) {
44
+ process.stderr.write(`Note: a config named "${proposedName}" already exists. Aliases (c-...) are the canonical handle.\n`);
45
+ }
46
+ }
47
+ catch {
48
+ // Best-effort: skip the warning if the pre-check fails.
49
+ }
50
+ }
28
51
  const data = await client.post("/dev/simulation-configs", body);
29
52
  const result = data;
30
53
  if (result.id)
@@ -59,6 +82,7 @@ export function registerConfigCommands(program) {
59
82
  .description("Update a simulation config")
60
83
  .argument("<id>", "Config ID")
61
84
  .requiredOption("--file <path>", "JSON file with update data")
85
+ .addHelpText("after", "\nMinimal body: {\"name\": \"...\", \"model_settings\": {...}}. See `ish config schema` for the full JSON Schema.")
62
86
  .action(async (id, opts, cmd) => {
63
87
  await withClient(cmd, async (client, globals) => {
64
88
  const body = await readJsonFileOrStdin(opts.file);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * ish docs — Offline, in-binary documentation aimed at AI coding agents.
3
+ *
4
+ * Modeled on LiveKit's `lk docs` and Speakeasy's `speakeasy agent`. The
5
+ * goal is that an agent that only ever sees what `ish --help` and
6
+ * subcommands print can still reach a complete domain primer without
7
+ * needing the README or web docs.
8
+ *
9
+ * Surface:
10
+ * ish docs # alias for `ish docs overview`
11
+ * ish docs overview # mental model + traversal hints
12
+ * ish docs list # every page with one-line description
13
+ * ish docs get-page <slug> # full markdown for one page
14
+ * ish docs search <query> # keyword search across all pages
15
+ */
16
+ import type { Command } from "commander";
17
+ export declare function registerDocsCommands(program: Command): void;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * ish docs — Offline, in-binary documentation aimed at AI coding agents.
3
+ *
4
+ * Modeled on LiveKit's `lk docs` and Speakeasy's `speakeasy agent`. The
5
+ * goal is that an agent that only ever sees what `ish --help` and
6
+ * subcommands print can still reach a complete domain primer without
7
+ * needing the README or web docs.
8
+ *
9
+ * Surface:
10
+ * ish docs # alias for `ish docs overview`
11
+ * ish docs overview # mental model + traversal hints
12
+ * ish docs list # every page with one-line description
13
+ * ish docs get-page <slug> # full markdown for one page
14
+ * ish docs search <query> # keyword search across all pages
15
+ */
16
+ import { getGlobals } from "../lib/command-helpers.js";
17
+ import { listPages, getPage, searchPages, } from "../lib/docs.js";
18
+ import { output, outputList } from "../lib/output.js";
19
+ function printOverview(json) {
20
+ const page = getPage("overview");
21
+ if (!page)
22
+ throw new Error("Overview page is missing.");
23
+ if (json) {
24
+ output(page, json);
25
+ return;
26
+ }
27
+ process.stdout.write(page.body);
28
+ if (!page.body.endsWith("\n"))
29
+ process.stdout.write("\n");
30
+ }
31
+ function printList(json) {
32
+ const pages = listPages();
33
+ if (json) {
34
+ outputList(pages.map((p) => ({
35
+ slug: p.slug,
36
+ title: p.title,
37
+ description: p.description,
38
+ })), json);
39
+ return;
40
+ }
41
+ const widest = pages.reduce((n, p) => Math.max(n, p.slug.length), 0);
42
+ for (const p of pages) {
43
+ const pad = " ".repeat(widest - p.slug.length + 2);
44
+ process.stdout.write(` ${p.slug}${pad}${p.description}\n`);
45
+ }
46
+ process.stdout.write("\nRead a page: ish docs get-page <slug>\n" +
47
+ "Search: ish docs search <query>\n");
48
+ }
49
+ function printPage(slug, json) {
50
+ const page = getPage(slug);
51
+ if (!page) {
52
+ const available = listPages().map((p) => p.slug);
53
+ const err = new Error(`Unknown page "${slug}". Available pages:\n ${available.join("\n ")}`);
54
+ if (json) {
55
+ output({ error: "page_not_found", slug, available_slugs: available }, json);
56
+ process.exit(4);
57
+ }
58
+ throw err;
59
+ }
60
+ if (json) {
61
+ output(page, json);
62
+ return;
63
+ }
64
+ process.stdout.write(page.body);
65
+ if (!page.body.endsWith("\n"))
66
+ process.stdout.write("\n");
67
+ }
68
+ function printSearch(query, json) {
69
+ const hits = searchPages(query, 20);
70
+ if (json) {
71
+ outputList(hits, json);
72
+ return;
73
+ }
74
+ if (hits.length === 0) {
75
+ process.stdout.write(`No matches for "${query}".\n`);
76
+ return;
77
+ }
78
+ for (const hit of hits) {
79
+ process.stdout.write(`${hit.slug} — ${hit.title}\n`);
80
+ process.stdout.write(` ${hit.snippet}\n\n`);
81
+ }
82
+ process.stdout.write(`Read a result: ish docs get-page <slug>\n`);
83
+ }
84
+ export function registerDocsCommands(program) {
85
+ const docs = program
86
+ .command("docs")
87
+ .description("Offline docs for agents — mental model, concept pages, search")
88
+ .addHelpText("after", `\nSubcommands:
89
+ overview Mental model + how to navigate (default)
90
+ list All pages with one-line descriptions
91
+ get-page <slug> Full markdown for one page
92
+ search <query> Keyword search across all pages
93
+
94
+ Examples:
95
+ $ ish docs # overview
96
+ $ ish docs list
97
+ $ ish docs get-page concepts/study
98
+ $ ish docs search "assignment"
99
+ $ ish docs get-page reference/json-mode --json | jq -r .body
100
+ `)
101
+ .action((_opts, cmd) => {
102
+ // Bare `ish docs` → overview.
103
+ const globals = getGlobals(cmd);
104
+ printOverview(globals.json);
105
+ });
106
+ docs
107
+ .command("overview")
108
+ .description("Print the agent-facing mental model and traversal hints")
109
+ .addHelpText("after", "\nExamples:\n $ ish docs overview\n $ ish docs overview --json")
110
+ .action((_opts, cmd) => {
111
+ const globals = getGlobals(cmd);
112
+ printOverview(globals.json);
113
+ });
114
+ docs
115
+ .command("list")
116
+ .description("List every doc page with its one-line description")
117
+ .addHelpText("after", "\nExamples:\n $ ish docs list\n $ ish docs list --json | jq '.[].slug'")
118
+ .action((_opts, cmd) => {
119
+ const globals = getGlobals(cmd);
120
+ printList(globals.json);
121
+ });
122
+ docs
123
+ .command("get-page")
124
+ .description("Print the full markdown for one page")
125
+ .argument("<slug>", "Page slug, e.g. concepts/study (see `ish docs list`)")
126
+ .addHelpText("after", `\nExamples:
127
+ $ ish docs get-page overview
128
+ $ ish docs get-page concepts/study
129
+ $ ish docs get-page reference/aliases
130
+ $ ish docs get-page concepts/run-verbs --json`)
131
+ .action((slug, _opts, cmd) => {
132
+ const globals = getGlobals(cmd);
133
+ printPage(slug, globals.json);
134
+ });
135
+ docs
136
+ .command("search")
137
+ .description("Keyword search across every doc page")
138
+ .argument("<query>", "Search query (case-insensitive substring)")
139
+ .addHelpText("after", `\nExamples:
140
+ $ ish docs search assignment
141
+ $ ish docs search "site access"
142
+ $ ish docs search audience --json`)
143
+ .action((query, _opts, cmd) => {
144
+ const globals = getGlobals(cmd);
145
+ printSearch(query, globals.json);
146
+ });
147
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * ish init — Install the ish Agent Skill into a project.
3
+ *
4
+ * Spec the skill conforms to: https://agentskills.io/specification
5
+ *
6
+ * Default target is `.claude/skills/ish/` because Claude Code is the
7
+ * largest install base and originator of the SKILL.md format. Other
8
+ * agents (Codex, Cursor, Cline, Roo Code) read `.agents/skills/<name>/`
9
+ * — pass `--target agents` or `--target both`.
10
+ *
11
+ * The skill is materialised from in-binary string constants
12
+ * (src/lib/skill-content.ts) so the same content ships in both `tsc`
13
+ * builds and `bun build --compile` single-binary builds.
14
+ */
15
+ import type { Command } from "commander";
16
+ export declare function registerInitCommands(program: Command): void;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * ish init — Install the ish Agent Skill into a project.
3
+ *
4
+ * Spec the skill conforms to: https://agentskills.io/specification
5
+ *
6
+ * Default target is `.claude/skills/ish/` because Claude Code is the
7
+ * largest install base and originator of the SKILL.md format. Other
8
+ * agents (Codex, Cursor, Cline, Roo Code) read `.agents/skills/<name>/`
9
+ * — pass `--target agents` or `--target both`.
10
+ *
11
+ * The skill is materialised from in-binary string constants
12
+ * (src/lib/skill-content.ts) so the same content ships in both `tsc`
13
+ * builds and `bun build --compile` single-binary builds.
14
+ */
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ import { getGlobals } from "../lib/command-helpers.js";
18
+ import { output } from "../lib/output.js";
19
+ import { buildSkillFiles, getSkillMd, SKILL_TARGETS, } from "../lib/skill-content.js";
20
+ function resolveTargets(opts) {
21
+ if (opts.dir) {
22
+ return [{ key: "claude", path: opts.dir, consumers: ["custom"] }];
23
+ }
24
+ if (opts.target === "both")
25
+ return SKILL_TARGETS;
26
+ return SKILL_TARGETS.filter((t) => t.key === opts.target);
27
+ }
28
+ /**
29
+ * Per-file idempotence: matching contents are skipped, non-matching
30
+ * existing contents trigger a refusal unless --force is set. Files in
31
+ * the directory that aren't part of the bundle are never touched.
32
+ */
33
+ function writeBundle(root, files, force) {
34
+ const conflicts = [];
35
+ const plan = [];
36
+ for (const file of files) {
37
+ const abs = path.join(root, file.relativePath);
38
+ let prior;
39
+ try {
40
+ prior = fs.readFileSync(abs, "utf-8");
41
+ }
42
+ catch {
43
+ // not present
44
+ }
45
+ if (prior === file.contents) {
46
+ plan.push({ file, action: "skip" });
47
+ }
48
+ else if (prior !== undefined && !force) {
49
+ conflicts.push(file.relativePath);
50
+ }
51
+ else {
52
+ plan.push({ file, action: "write" });
53
+ }
54
+ }
55
+ if (conflicts.length > 0) {
56
+ throw new Error(`Refusing to overwrite existing files in ${root}:\n` +
57
+ conflicts.map((f) => ` - ${f}`).join("\n") +
58
+ `\n\nRe-run with --force to overwrite, or pass --dir <path> to write elsewhere.`);
59
+ }
60
+ fs.mkdirSync(root, { recursive: true });
61
+ const written = [];
62
+ const skipped = [];
63
+ for (const item of plan) {
64
+ const abs = path.join(root, item.file.relativePath);
65
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
66
+ if (item.action === "skip") {
67
+ skipped.push(item.file.relativePath);
68
+ continue;
69
+ }
70
+ fs.writeFileSync(abs, item.file.contents, "utf-8");
71
+ written.push(item.file.relativePath);
72
+ }
73
+ return { root, written, skipped };
74
+ }
75
+ function buildHints(targets, opts) {
76
+ if (opts.dir) {
77
+ return [
78
+ "Custom directory: tell your agent where to find this skill (most agents " +
79
+ "look at .claude/skills/<name>/ or .agents/skills/<name>/).",
80
+ ];
81
+ }
82
+ const installed = new Set(targets.map((t) => t.key));
83
+ const hints = [];
84
+ for (const target of SKILL_TARGETS) {
85
+ if (installed.has(target.key))
86
+ continue;
87
+ const consumers = target.consumers.join(", ");
88
+ hints.push(`${consumers}: install to ${target.path} via ` +
89
+ `\`ish init --target ${target.key}${opts.force ? " --force" : ""}\` ` +
90
+ `(or copy: \`cp -r ${targets[0]?.path ?? ".claude/skills/ish"} ${target.path}\`).`);
91
+ }
92
+ return hints;
93
+ }
94
+ export function registerInitCommands(program) {
95
+ program
96
+ .command("init")
97
+ .description("Install the ish Agent Skill into a project (Claude Code, Codex, Cursor, Cline, Roo Code)")
98
+ .option("--target <which>", "Where to install: claude (.claude/skills/ish), agents (.agents/skills/ish), or both", "claude")
99
+ .option("--dir <path>", "Write to a custom directory instead of a recognised target path")
100
+ .option("--stdout", "Print the SKILL.md to stdout instead of writing files (skips references/)")
101
+ .option("--force", "Overwrite existing files in the target directory")
102
+ .addHelpText("after", `\nExamples:
103
+ $ ish init # → .claude/skills/ish/ (Claude Code)
104
+ $ ish init --target agents # → .agents/skills/ish/ (Codex / Cursor / Cline / Roo)
105
+ $ ish init --target both # both of the above
106
+ $ ish init --dir vendor/ish-skill # custom path
107
+ $ ish init --stdout > /tmp/SKILL.md # just the SKILL.md
108
+ $ ish init --force # overwrite existing skill files
109
+
110
+ Where do skills live?
111
+ Claude Code .claude/skills/<name>/ (also read by Cursor for back-compat)
112
+ Codex / Cursor / .agents/skills/<name>/ (cross-vendor convention)
113
+ Cline / Roo Code
114
+
115
+ Spec: https://agentskills.io/specification`)
116
+ .action(async (rawOpts, cmd) => {
117
+ const globals = getGlobals(cmd);
118
+ try {
119
+ const target = String(rawOpts.target ?? "claude");
120
+ if (!["claude", "agents", "both"].includes(target)) {
121
+ throw new Error(`Invalid --target "${target}". Valid: claude | agents | both.`);
122
+ }
123
+ const opts = {
124
+ target,
125
+ dir: typeof rawOpts.dir === "string" ? rawOpts.dir : undefined,
126
+ stdout: rawOpts.stdout === true,
127
+ force: rawOpts.force === true,
128
+ };
129
+ if (opts.stdout) {
130
+ process.stdout.write(getSkillMd());
131
+ return;
132
+ }
133
+ const targets = resolveTargets(opts);
134
+ const files = buildSkillFiles();
135
+ const results = [];
136
+ for (const t of targets) {
137
+ results.push(writeBundle(t.path, files, !!opts.force));
138
+ }
139
+ const hints = buildHints(targets, opts);
140
+ if (globals.json) {
141
+ output({
142
+ ok: true,
143
+ installed: results.map((r) => ({
144
+ root: r.root,
145
+ written: r.written,
146
+ skipped: r.skipped,
147
+ })),
148
+ other_paths: hints,
149
+ }, true);
150
+ return;
151
+ }
152
+ for (const r of results) {
153
+ if (r.written.length === 0 && r.skipped.length > 0) {
154
+ process.stdout.write(`Skill already up to date at ${r.root} (${r.skipped.length} files unchanged).\n`);
155
+ }
156
+ else {
157
+ process.stdout.write(`Installed skill at ${r.root}\n`);
158
+ for (const f of r.written)
159
+ process.stdout.write(` + ${f}\n`);
160
+ for (const f of r.skipped)
161
+ process.stdout.write(` = ${f} (unchanged)\n`);
162
+ }
163
+ }
164
+ if (hints.length > 0) {
165
+ process.stdout.write("\nOther agent paths you may want to populate:\n");
166
+ for (const h of hints)
167
+ process.stdout.write(` - ${h}\n`);
168
+ }
169
+ process.stdout.write("\nTip: run `ish docs overview` next — it's the entry point your agent should hit first.\n");
170
+ }
171
+ catch (err) {
172
+ const message = err instanceof Error ? err.message : String(err);
173
+ if (globals.json) {
174
+ output({ ok: false, error: message }, true);
175
+ }
176
+ else {
177
+ process.stderr.write(`${message}\n`);
178
+ }
179
+ process.exit(2);
180
+ }
181
+ });
182
+ }
@@ -1,5 +1,9 @@
1
1
  /**
2
- * ish iteration — Manage iterations (usually created via `simulation run`).
2
+ * ish iteration — Manage iterations of a study.
3
+ *
4
+ * An iteration carries the run-time details (URL for interactive,
5
+ * content/file for media). Create one before `ish study run` dispatches
6
+ * simulations against it.
3
7
  */
4
8
  import type { Command } from "commander";
5
9
  export declare function registerIterationCommands(program: Command): void;