@appchy/jarvis 0.1.43 → 0.1.45

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/bin/jarvis.mjs CHANGED
@@ -8,25 +8,91 @@
8
8
  * pairs, so one binary serves our cloud, a self-hosted instance and a laptop
9
9
  * alike. For source-tree iteration with hot reload, run
10
10
  * `pnpm -C apps/cli dev` — that runs `tsx src/bin.ts` directly.
11
+ *
12
+ * It does decide WHICH BUILD, for the two commands a repo reaches jarvis with.
13
+ * A repo names jarvis by `command: "jarvis"` in a checked-in `.mcp.json`, so the
14
+ * only place a machine-specific answer can live is here.
11
15
  */
12
16
 
13
17
  import { fileURLToPath } from "url";
14
- import { dirname, join } from "path";
15
- import { existsSync } from "fs";
18
+ import { dirname, isAbsolute, join, relative } from "path";
19
+ import { existsSync, readFileSync, realpathSync } from "fs";
20
+ import { homedir } from "os";
16
21
 
17
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
23
  const distEntry = join(__dirname, "..", "dist", "bin.js");
19
24
 
20
- if (!existsSync(distEntry)) {
21
- const isSourceCheckout = existsSync(join(__dirname, "..", "src", "bin.ts"));
22
- console.error("Error: dist/bin.js not found.");
23
- if (isSourceCheckout) {
24
- console.error(" This is a source checkout. Run `pnpm -C apps/cli dev` for dev work,");
25
- console.error(" or `pnpm -C apps/cli build` to produce a bundle.");
26
- } else {
27
- console.error(" The package may be corrupted. Reinstall with `pnpm i -g @appchy/jarvis`.");
25
+ /** The commands a repo reaches jarvis with. Machine lifecycle — `start`, `init`, `pair` —
26
+ * is deliberately absent: a redirect meant for an MCP server must never move the daemon,
27
+ * which owns this machine's Claude Code and is identified by its config dir, not its build. */
28
+ const REDIRECTED = new Set(["serve", "work"]);
29
+
30
+ /** The development build to run here, or null for the one this launcher ships with.
31
+ * `src/dev.ts` answers the same question for `jarvis dev status`; it cannot be imported,
32
+ * because this has to decide before any bundle is loaded. Keep the two answers identical. */
33
+ function devBuild() {
34
+ const command = process.argv[2];
35
+ if (!REDIRECTED.has(command)) return null;
36
+
37
+ const configDir = process.env.JARVIS_CONFIG_DIR ?? join(homedir(), ".jarvis");
38
+ let redirect;
39
+ try {
40
+ redirect = JSON.parse(readFileSync(join(configDir, "dev.json"), "utf-8"));
41
+ } catch {
42
+ return null;
28
43
  }
29
- process.exit(1);
44
+ if (typeof redirect?.root !== "string") return null;
45
+
46
+ // Every symlink resolved on both sides: an unresolved comparison fails wherever a home
47
+ // or temp directory is a link, and that failure is indistinguishable from being switched off.
48
+ const real = (target) => {
49
+ try {
50
+ return realpathSync(target);
51
+ } catch {
52
+ return target;
53
+ }
54
+ };
55
+ const cwd = real(process.cwd());
56
+ const here =
57
+ redirect.all === true ||
58
+ (Array.isArray(redirect.repos) &&
59
+ redirect.repos.some((repo) => {
60
+ if (typeof repo !== "string") return false;
61
+ const rel = relative(real(repo), cwd);
62
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
63
+ }));
64
+ if (!here) return null;
65
+
66
+ const entry = join(redirect.root, "apps", "cli", "dist", "bin.js");
67
+ if (!existsSync(entry)) {
68
+ // Falling back silently would serve the installed build under the name of the one
69
+ // being tested, which is the shape of this bug nobody catches: the half you look at
70
+ // is right. stderr, because stdout is the MCP transport.
71
+ process.stderr.write(
72
+ `jarvis: ${redirect.root} has no build to run — using the installed one.\n` +
73
+ ` Build it with \`pnpm --filter @appchy/jarvis build\`.\n`,
74
+ );
75
+ return null;
76
+ }
77
+ return entry;
30
78
  }
31
79
 
32
- await import(distEntry);
80
+ const dev = devBuild();
81
+ if (dev) {
82
+ process.stderr.write(`jarvis: development build, from ${dev}\n`);
83
+ await import(dev);
84
+ } else {
85
+ if (!existsSync(distEntry)) {
86
+ const isSourceCheckout = existsSync(join(__dirname, "..", "src", "bin.ts"));
87
+ console.error("Error: dist/bin.js not found.");
88
+ if (isSourceCheckout) {
89
+ console.error(" This is a source checkout. Run `pnpm -C apps/cli dev` for dev work,");
90
+ console.error(" or `pnpm -C apps/cli build` to produce a bundle.");
91
+ } else {
92
+ console.error(" The package may be corrupted. Reinstall with `pnpm i -g @appchy/jarvis`.");
93
+ }
94
+ process.exit(1);
95
+ }
96
+
97
+ await import(distEntry);
98
+ }
package/dist/bin.js CHANGED
@@ -4433,8 +4433,8 @@ async function scanTree(root) {
4433
4433
  for (const file of await walk(join3(work, base))) {
4434
4434
  const text = await readFile(file, "utf-8");
4435
4435
  const changedAt = (await stat(file)).mtime.toISOString().slice(0, 10);
4436
- const path20 = relative(work, file).split(sep).join("/");
4437
- const item = toWorkItem({ path: path20, text, changedAt });
4436
+ const path22 = relative(work, file).split(sep).join("/");
4437
+ const item = toWorkItem({ path: path22, text, changedAt });
4438
4438
  if (item) items.push(item);
4439
4439
  }
4440
4440
  }
@@ -5400,8 +5400,8 @@ function reason(err) {
5400
5400
  }
5401
5401
  function issueText(issues) {
5402
5402
  return issues.slice(0, 3).map((issue) => {
5403
- const path20 = (issue.path ?? []).map((step) => String(typeof step === "object" ? step.key : step)).join(".");
5404
- return path20 ? `${path20}: ${issue.message}` : issue.message;
5403
+ const path22 = (issue.path ?? []).map((step) => String(typeof step === "object" ? step.key : step)).join(".");
5404
+ return path22 ? `${path22}: ${issue.message}` : issue.message;
5405
5405
  }).join("; ");
5406
5406
  }
5407
5407
  function keyText(data) {
@@ -5689,9 +5689,9 @@ function localFiles(options = {}) {
5689
5689
  }
5690
5690
  },
5691
5691
  save: async (ctx, key, bytes) => {
5692
- const path20 = pathOf(ctx, key);
5693
- await mkdir(dirname3(path20), { recursive: true });
5694
- await writeFile(path20, bytes, "utf8");
5692
+ const path22 = pathOf(ctx, key);
5693
+ await mkdir(dirname3(path22), { recursive: true });
5694
+ await writeFile(path22, bytes, "utf8");
5695
5695
  },
5696
5696
  locate: (ctx, key) => pathOf(ctx, key)
5697
5697
  });
@@ -5709,10 +5709,10 @@ async function loadConfig2(cwd) {
5709
5709
  let dir = resolve3(cwd);
5710
5710
  for (; ; ) {
5711
5711
  for (const name of CONFIG_NAMES) {
5712
- const path20 = resolve3(dir, name);
5713
- if (existsSync5(path20)) {
5714
- const config2 = await importConfig(path20);
5715
- validateConfig(config2, path20);
5712
+ const path22 = resolve3(dir, name);
5713
+ if (existsSync5(path22)) {
5714
+ const config2 = await importConfig(path22);
5715
+ validateConfig(config2, path22);
5716
5716
  return {
5717
5717
  ...config2,
5718
5718
  persistence: config2.persistence ?? localFiles(),
@@ -5728,9 +5728,9 @@ async function loadConfig2(cwd) {
5728
5728
  `No jarvis.config.{ts,js,mjs} found from ${cwd}. A repo maps its world in one, at its root.`
5729
5729
  );
5730
5730
  }
5731
- async function importConfig(path20) {
5731
+ async function importConfig(path22) {
5732
5732
  const jiti = createJiti(import.meta.url, { moduleCache: false, alias: selfAlias() });
5733
- const mod = await jiti.import(path20);
5733
+ const mod = await jiti.import(path22);
5734
5734
  const config2 = mod.default ?? mod;
5735
5735
  return config2;
5736
5736
  }
@@ -5770,9 +5770,9 @@ var VENDOR_SUBPATHS = [
5770
5770
  "backends",
5771
5771
  "embedders",
5772
5772
  "finders",
5773
+ "labelers",
5773
5774
  "linkers",
5774
5775
  "persistences",
5775
- "rerankers",
5776
5776
  "stores"
5777
5777
  ];
5778
5778
  function toVendorAlias(dir) {
@@ -5783,9 +5783,9 @@ function toVendorAlias(dir) {
5783
5783
  function toSpecifier(subpath) {
5784
5784
  return subpath === "." ? PACKAGE_NAME : `${PACKAGE_NAME}/${subpath.replace(/^\.\//, "")}`;
5785
5785
  }
5786
- function validateConfig(value, path20) {
5786
+ function validateConfig(value, path22) {
5787
5787
  const fail = (msg) => {
5788
- throw new Error(`${path20}: ${msg}`);
5788
+ throw new Error(`${path22}: ${msg}`);
5789
5789
  };
5790
5790
  if (typeof value !== "object" || value === null) fail("config must export an object");
5791
5791
  const c = value;
@@ -5809,11 +5809,11 @@ function validateConfig(value, path20) {
5809
5809
  const search3 = c.search;
5810
5810
  if (search3 !== void 0) {
5811
5811
  if (typeof search3.embedder?.embed !== "function")
5812
- fail("`search.embedder` must be an embedder (e.g. localEmbeddings())");
5812
+ fail("`search.embedder` must be an embedder (e.g. openaiEmbeddings())");
5813
5813
  if (search3.store !== void 0 && typeof search3.store.query !== "function")
5814
5814
  fail("`search.store` must be a vector store");
5815
5815
  if (search3.reranker !== void 0 && typeof search3.reranker.rerank !== "function")
5816
- fail("`search.reranker` must be a reranker (e.g. localReranker())");
5816
+ fail("`search.reranker` must be a reranker");
5817
5817
  }
5818
5818
  }
5819
5819
 
@@ -5822,6 +5822,7 @@ async function buildEmbeddings(config2, graph2) {
5822
5822
  const cfg = config2.search;
5823
5823
  if (!cfg) return void 0;
5824
5824
  const embedder2 = cfg.embedder.resolve ? await cfg.embedder.resolve() : cfg.embedder;
5825
+ if (!embedder2) return declined(cfg.embedder, "no semantic index was built");
5825
5826
  const storeImpl = cfg.store;
5826
5827
  const nodes = cfg.types ? cfg.types.flatMap((t) => graph2.nodes(t)) : graph2.nodes();
5827
5828
  const ctx = { repoRoot: config2.repoRoot };
@@ -5880,6 +5881,7 @@ async function loadEmbeddings(config2, graph2) {
5880
5881
  const cfg = config2.search;
5881
5882
  if (!cfg) return void 0;
5882
5883
  const embedder2 = cfg.embedder.resolve ? await cfg.embedder.resolve() : cfg.embedder;
5884
+ if (!embedder2) return declined(cfg.embedder, "search is lexical");
5883
5885
  await probeEmbedder(embedder2);
5884
5886
  const storeImpl = cfg.store;
5885
5887
  const nodes = cfg.types ? cfg.types.flatMap((t) => graph2.nodes(t)) : graph2.nodes();
@@ -5911,6 +5913,13 @@ function toRuntime(cfg, embedder2, storeImpl, items) {
5911
5913
  ...cfg.confidence ? { confidence: cfg.confidence } : {}
5912
5914
  };
5913
5915
  }
5916
+ function declined(embedder2, consequence) {
5917
+ process.stderr.write(
5918
+ `[data:warn] embedder ${embedder2.name} is not available on this machine \u2014 ${consequence}. Token-overlap (BM25) search still applies.
5919
+ `
5920
+ );
5921
+ return void 0;
5922
+ }
5914
5923
  async function probeEmbedder(embedder2) {
5915
5924
  try {
5916
5925
  await embedder2.embed(["probe"]);
@@ -6491,10 +6500,10 @@ async function git3(root, argv, timeout = 5e3) {
6491
6500
  }
6492
6501
  }
6493
6502
  async function fetchHeadIso(root) {
6494
- const path20 = await git3(root, ["rev-parse", "--git-path", "FETCH_HEAD"]);
6495
- if (!path20) return void 0;
6503
+ const path22 = await git3(root, ["rev-parse", "--git-path", "FETCH_HEAD"]);
6504
+ if (!path22) return void 0;
6496
6505
  try {
6497
- return (await stat2(resolve4(root, path20))).mtime.toISOString();
6506
+ return (await stat2(resolve4(root, path22))).mtime.toISOString();
6498
6507
  } catch {
6499
6508
  return void 0;
6500
6509
  }
@@ -6571,10 +6580,10 @@ async function freshness(ctx) {
6571
6580
  };
6572
6581
  }
6573
6582
  async function snapshotMtimeMs(ctx) {
6574
- const path20 = ctx.config.persistence?.locate?.({ repoRoot: ctx.config.repoRoot }, "snapshot");
6575
- if (!path20) return void 0;
6583
+ const path22 = ctx.config.persistence?.locate?.({ repoRoot: ctx.config.repoRoot }, "snapshot");
6584
+ if (!path22) return void 0;
6576
6585
  try {
6577
- return (await stat2(path20)).mtimeMs;
6586
+ return (await stat2(path22)).mtimeMs;
6578
6587
  } catch {
6579
6588
  return void 0;
6580
6589
  }
@@ -6996,10 +7005,10 @@ async function cachedEnforcerDiags(ctx, freshPaths) {
6996
7005
  }
6997
7006
  function isFresh(snapshot, repoRoot, freshPaths) {
6998
7007
  if (!freshPaths || freshPaths.length === 0) return true;
6999
- for (const path20 of freshPaths) {
7008
+ for (const path22 of freshPaths) {
7000
7009
  let mtimeMs;
7001
7010
  try {
7002
- mtimeMs = statSync2(resolve5(repoRoot, path20)).mtimeMs;
7011
+ mtimeMs = statSync2(resolve5(repoRoot, path22)).mtimeMs;
7003
7012
  } catch {
7004
7013
  return false;
7005
7014
  }
@@ -7652,15 +7661,15 @@ import { readFile as readFile5 } from "fs/promises";
7652
7661
  import { resolve as resolve6 } from "path";
7653
7662
 
7654
7663
  // ../../packages/data/src/meta.ts
7655
- function stringMeta(metadata, path20) {
7656
- const value = metaValue(metadata, path20);
7664
+ function stringMeta(metadata, path22) {
7665
+ const value = metaValue(metadata, path22);
7657
7666
  return typeof value === "string" ? value : void 0;
7658
7667
  }
7659
- function metaValue(metadata, path20) {
7668
+ function metaValue(metadata, path22) {
7660
7669
  if (!metadata) return void 0;
7661
- if (Object.prototype.hasOwnProperty.call(metadata, path20)) return metadata[path20];
7670
+ if (Object.prototype.hasOwnProperty.call(metadata, path22)) return metadata[path22];
7662
7671
  let current = metadata;
7663
- for (const part of path20.split(".")) {
7672
+ for (const part of path22.split(".")) {
7664
7673
  if (!current || typeof current !== "object" || !Object.prototype.hasOwnProperty.call(current, part))
7665
7674
  return void 0;
7666
7675
  current = current[part];
@@ -7771,8 +7780,8 @@ function resolveScopeGovernance(ctx, members, codeNodes) {
7771
7780
  }
7772
7781
  return [...out.values()];
7773
7782
  }
7774
- function resolveDirectoryGovernance(ctx, path20) {
7775
- let dir = path20.includes("/") ? path20.slice(0, path20.lastIndexOf("/")) : "";
7783
+ function resolveDirectoryGovernance(ctx, path22) {
7784
+ let dir = path22.includes("/") ? path22.slice(0, path22.lastIndexOf("/")) : "";
7776
7785
  for (; ; ) {
7777
7786
  const siblings = codeNodesInDir(ctx, dir);
7778
7787
  if (siblings.length > 0) return resolveScopeGovernance(ctx, [], siblings);
@@ -8333,15 +8342,15 @@ async function review(ctx, req) {
8333
8342
  cites: []
8334
8343
  });
8335
8344
  }
8336
- for (const path20 of unresolved) {
8337
- const dirGov = resolveDirectoryGovernance(ctx, path20).filter((n) => !isWorkItem(n)).map((g) => g.id).filter((id) => !cites.has(id)).slice(0, 3);
8345
+ for (const path22 of unresolved) {
8346
+ const dirGov = resolveDirectoryGovernance(ctx, path22).filter((n) => !isWorkItem(n)).map((g) => g.id).filter((id) => !cites.has(id)).slice(0, 3);
8338
8347
  const hint = dirGov.length ? ` Files in its directory are governed by ${dirGov.join(", ")} \u2014 cite if it applies.` : "";
8339
8348
  findings.push({
8340
8349
  code: "unresolved-edit",
8341
8350
  severity: "info",
8342
8351
  confidence: "precise",
8343
- subject: path20,
8344
- message: `${path20} isn't in the graph yet (a new file, or a path that doesn't match).${hint || " No governed siblings to assess it by."}`,
8352
+ subject: path22,
8353
+ message: `${path22} isn't in the graph yet (a new file, or a path that doesn't match).${hint || " No governed siblings to assess it by."}`,
8345
8354
  cites: dirGov
8346
8355
  });
8347
8356
  }
@@ -9192,8 +9201,8 @@ async function readFileContent(workdir, relPath) {
9192
9201
 
9193
9202
  // src/workspace.ts
9194
9203
  var workspacePath = null;
9195
- function setWorkspacePath(path20) {
9196
- workspacePath = path20;
9204
+ function setWorkspacePath(path22) {
9205
+ workspacePath = path22;
9197
9206
  }
9198
9207
  function getWorkspacePath() {
9199
9208
  if (!workspacePath) {
@@ -9612,10 +9621,11 @@ import WebSocket from "ws";
9612
9621
  import { createRequire as createRequire2 } from "module";
9613
9622
  var _require = createRequire2(import.meta.url);
9614
9623
  var VERSION2 = _require("../package.json").version ?? "0.0.0";
9615
- var SHA = "c87ba61";
9624
+ var IS_DEV = String(_require("../package.json").name ?? "").endsWith("-dev");
9625
+ var SHA = "86c8e54";
9616
9626
  var BUILT = "2026-09-07";
9617
9627
  var BUILD = SHA ?? "source";
9618
- var BUILD_LABEL = SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`;
9628
+ var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
9619
9629
 
9620
9630
  // src/upstream.ts
9621
9631
  function createUpstreamClient(config2) {
@@ -10542,12 +10552,12 @@ function register14(program) {
10542
10552
  }
10543
10553
 
10544
10554
  // src/commands/mcp.ts
10545
- async function call(path20, method, body) {
10555
+ async function call(path22, method, body) {
10546
10556
  const config2 = loadConfig();
10547
10557
  if (!config2?.token) {
10548
10558
  throw new Error("not connected \u2014 run `jarvis connect` first");
10549
10559
  }
10550
- const resp = await fetch(new URL(path20, config2.url ?? getJarvisUrl()), {
10560
+ const resp = await fetch(new URL(path22, config2.url ?? getJarvisUrl()), {
10551
10561
  method,
10552
10562
  headers: {
10553
10563
  authorization: `Bearer ${config2.token}`,
@@ -10556,7 +10566,7 @@ async function call(path20, method, body) {
10556
10566
  ...body === void 0 ? {} : { body: JSON.stringify(body) }
10557
10567
  });
10558
10568
  if (!resp.ok) {
10559
- throw new Error(`${method} ${path20} failed: ${resp.status} ${await resp.text()}`);
10569
+ throw new Error(`${method} ${path22} failed: ${resp.status} ${await resp.text()}`);
10560
10570
  }
10561
10571
  return await resp.json();
10562
10572
  }
@@ -10613,6 +10623,33 @@ function register15(program) {
10613
10623
  });
10614
10624
  }
10615
10625
 
10626
+ // src/commands/model.ts
10627
+ function register16(program) {
10628
+ const model = program.command("model").description("Model access for the map's semantic search, on this machine");
10629
+ model.command("key").argument("[key]", "The OpenAI key to store. Omit it to see what is set.").option("--show", "Say whether a key is set, without printing it").option("--clear", "Forget the stored key").description("Set the OpenAI key the map embeds with").action((key, opts) => {
10630
+ const config2 = loadConfig();
10631
+ if (!config2) {
10632
+ console.error("This machine has no jarvis config yet \u2014 run `jarvis init` first.");
10633
+ process.exitCode = 1;
10634
+ return;
10635
+ }
10636
+ if (opts.clear) {
10637
+ saveConfig({ ...config2, openaiApiKey: void 0 });
10638
+ console.log("Forgotten. The map will build with no semantic index.");
10639
+ return;
10640
+ }
10641
+ if (!key || opts.show) {
10642
+ console.log(
10643
+ config2.openaiApiKey ? `A key is set (\u2026${config2.openaiApiKey.slice(-4)}). The map embeds with it.` : "No key set. The map builds with no semantic index and search stays lexical."
10644
+ );
10645
+ if (!key) console.log("Set one with `jarvis model key <key>`.");
10646
+ return;
10647
+ }
10648
+ saveConfig({ ...config2, openaiApiKey: key });
10649
+ console.log("Stored. Run `jarvis build graph` to build the semantic index.");
10650
+ });
10651
+ }
10652
+
10616
10653
  // src/graph/build.ts
10617
10654
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, rmSync, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
10618
10655
  import { dirname as dirname5, resolve as resolve7 } from "path";
@@ -10786,12 +10823,6 @@ async function runBuild(cwd, argv = []) {
10786
10823
  if (argv.includes("--rebuild")) process.env.JARVIS_DATA_REBUILD = "1";
10787
10824
  else process.env.JARVIS_DATA_REFRESH = "1";
10788
10825
  if (argv.includes("--label")) process.env.JARVIS_DATA_LABEL = "1";
10789
- const labelBackend = readFlag(argv, "--backend");
10790
- if (labelBackend) process.env.JARVIS_DATA_BACKEND = labelBackend;
10791
- const labelModel = readFlag(argv, "--model");
10792
- if (labelModel) process.env.JARVIS_DATA_MODEL = labelModel;
10793
- const labelTier = readFlag(argv, "--tier");
10794
- if (labelTier) process.env.JARVIS_DATA_TIER = labelTier;
10795
10826
  const lock = acquireBuildLock(cwd);
10796
10827
  if (!lock) {
10797
10828
  process.stderr.write(
@@ -10845,13 +10876,6 @@ ${formatLedger(ledger)}
10845
10876
  }
10846
10877
  return errors > 0 ? 1 : 0;
10847
10878
  }
10848
- function readFlag(argv, flag2) {
10849
- const exact = argv.indexOf(flag2);
10850
- if (exact >= 0) return argv[exact + 1];
10851
- const prefix = `${flag2}=`;
10852
- const inline = argv.find((arg) => arg.startsWith(prefix));
10853
- return inline ? inline.slice(prefix.length) : void 0;
10854
- }
10855
10879
  var DEFAULT_STALE_MS = 10 * 60 * 1e3;
10856
10880
  function acquireBuildLock(repoRoot, options = {}) {
10857
10881
  const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
@@ -10904,29 +10928,122 @@ function resolveRepo(repo) {
10904
10928
  loadEnvFile(resolve8(homedir4(), ".jarvis", "data.env"));
10905
10929
  return cwd;
10906
10930
  }
10907
- function loadEnvFile(path20) {
10931
+ function loadEnvFile(path22) {
10908
10932
  try {
10909
- process.loadEnvFile(path20);
10933
+ process.loadEnvFile(path22);
10910
10934
  } catch {
10911
10935
  }
10912
10936
  }
10913
10937
 
10914
10938
  // src/commands/build.ts
10915
- function register16(program) {
10939
+ function register17(program) {
10916
10940
  const build2 = program.command("build").description("Build an artifact from this repo");
10917
- build2.command("graph").description("Read the repo's jarvis.config.ts, build the graph, and say what is wrong with it").option("--repo <path>", "The repo to map. Default: the working directory").option("--rebuild", "LAST RESORT: throw the graph away and re-extract from scratch").option("--label", "Re-name the detected communities through the configured labeler").option("--gate", "Fail when a governance class has more findings than this repo carries").option("--backend <name>", "Label the communities as this backend").option("--model <name>", "Label the communities with this model").option("--tier <tier>", "Label the communities at this tier").action(async (opts) => {
10941
+ build2.command("graph").description("Read the repo's jarvis.config.ts, build the graph, and say what is wrong with it").option("--repo <path>", "The repo to map. Default: the working directory").option("--rebuild", "LAST RESORT: throw the graph away and re-extract from scratch").option("--label", "Re-name the detected communities through the configured labeler").option("--gate", "Fail when a governance class has more findings than this repo carries").action(async (opts) => {
10918
10942
  const argv = [
10919
10943
  ...opts.rebuild ? ["--rebuild"] : [],
10920
10944
  ...opts.label ? ["--label"] : [],
10921
- ...opts.gate ? ["--gate"] : [],
10922
- ...opts.backend ? ["--backend", opts.backend] : [],
10923
- ...opts.model ? ["--model", opts.model] : [],
10924
- ...opts.tier ? ["--tier", opts.tier] : []
10945
+ ...opts.gate ? ["--gate"] : []
10925
10946
  ];
10926
10947
  process.exitCode = await runBuild(resolveRepo(opts.repo), argv);
10927
10948
  });
10928
10949
  }
10929
10950
 
10951
+ // src/commands/dev.ts
10952
+ import path20 from "path";
10953
+
10954
+ // src/dev.ts
10955
+ import fs18 from "fs";
10956
+ import path19 from "path";
10957
+ function redirectFile() {
10958
+ return path19.join(getConfigDir(), "dev.json");
10959
+ }
10960
+ function readDevRedirect() {
10961
+ try {
10962
+ const raw = JSON.parse(fs18.readFileSync(redirectFile(), "utf-8"));
10963
+ if (typeof raw.root !== "string" || raw.root.length === 0) return null;
10964
+ return {
10965
+ root: raw.root,
10966
+ repos: Array.isArray(raw.repos) ? raw.repos.filter((r) => typeof r === "string") : [],
10967
+ all: raw.all === true
10968
+ };
10969
+ } catch {
10970
+ return null;
10971
+ }
10972
+ }
10973
+ function saveDevRedirect(req) {
10974
+ const dir = getConfigDir();
10975
+ fs18.mkdirSync(dir, { recursive: true });
10976
+ fs18.writeFileSync(redirectFile(), `${JSON.stringify(req, null, 2)}
10977
+ `);
10978
+ }
10979
+ function clearDevRedirect() {
10980
+ fs18.rmSync(redirectFile(), { force: true });
10981
+ }
10982
+ function realPath(target) {
10983
+ try {
10984
+ return fs18.realpathSync(target);
10985
+ } catch {
10986
+ return target;
10987
+ }
10988
+ }
10989
+ function isRedirected(req) {
10990
+ if (req.redirect.all) return true;
10991
+ const cwd = realPath(req.cwd);
10992
+ return req.redirect.repos.some((repo) => {
10993
+ const rel = path19.relative(realPath(repo), cwd);
10994
+ return rel === "" || !rel.startsWith("..") && !path19.isAbsolute(rel);
10995
+ });
10996
+ }
10997
+
10998
+ // src/commands/dev.ts
10999
+ function register18(program) {
11000
+ const dev = program.command("dev").description("Run a development build in a repo, without replacing the installed jarvis");
11001
+ dev.command("use").argument("[where]", "'here' for the working directory, 'all' for every repo", "here").option("--root <path>", "The checkout to run. Default: the one already recorded").description("Point a repo at the development build").action((where, opts) => {
11002
+ const current = readDevRedirect();
11003
+ const root = path20.resolve(opts.root ?? current?.root ?? process.cwd());
11004
+ if (where !== "here" && where !== "all") {
11005
+ console.error(`Unknown target '${where}' \u2014 say 'here' or 'all'.`);
11006
+ process.exitCode = 1;
11007
+ return;
11008
+ }
11009
+ const repos = new Set(current?.repos ?? []);
11010
+ if (where === "here") repos.add(realPath(process.cwd()));
11011
+ saveDevRedirect({ root, repos: [...repos], all: where === "all" || (current?.all ?? false) });
11012
+ console.log(
11013
+ where === "all" ? `Every repo on this machine now serves the development build in ${root}.` : `${process.cwd()} now serves the development build in ${root}.`
11014
+ );
11015
+ console.log("The installed jarvis is untouched. Undo with `jarvis dev off`.");
11016
+ });
11017
+ dev.command("off").argument("[where]", "'here' for the working directory, 'all' to stop everywhere", "all").description("Stop serving the development build").action((where) => {
11018
+ const current = readDevRedirect();
11019
+ if (!current) {
11020
+ console.log("No repo is on a development build.");
11021
+ return;
11022
+ }
11023
+ if (where === "here") {
11024
+ const repos = current.repos.filter((repo) => realPath(repo) !== realPath(process.cwd()));
11025
+ saveDevRedirect({ ...current, repos, all: false });
11026
+ console.log(`${process.cwd()} is back on the installed jarvis.`);
11027
+ return;
11028
+ }
11029
+ clearDevRedirect();
11030
+ console.log("Every repo is back on the installed jarvis.");
11031
+ });
11032
+ dev.command("status").description("Say which repos are on a development build").action(() => {
11033
+ const current = readDevRedirect();
11034
+ if (!current) {
11035
+ console.log("No repo is on a development build.");
11036
+ return;
11037
+ }
11038
+ console.log(`development build ${current.root}`);
11039
+ console.log(current.all ? " every repo on this machine" : "");
11040
+ for (const repo of current.repos) console.log(` ${repo}`);
11041
+ console.log(
11042
+ isRedirected({ cwd: process.cwd(), redirect: current }) ? "\nHere: the development build." : "\nHere: the installed jarvis."
11043
+ );
11044
+ });
11045
+ }
11046
+
10930
11047
  // src/graph/mcp.ts
10931
11048
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10932
11049
  async function runMcp(cwd, hosted = [], prompts = []) {
@@ -11464,7 +11581,7 @@ function describeStart(response) {
11464
11581
  // src/serve/approvals.ts
11465
11582
  import { readFileSync as readFileSync7 } from "fs";
11466
11583
  import os10 from "os";
11467
- import path19 from "path";
11584
+ import path21 from "path";
11468
11585
  function pendingApproval(cwd) {
11469
11586
  const unapproved = unapprovedServers(cwd);
11470
11587
  const reasons = [
@@ -11475,15 +11592,15 @@ function pendingApproval(cwd) {
11475
11592
  return `it will HOLD at a prompt and take no turn, because ${reasons.join(" and ")}. Nothing will appear in Claude Code's history or in the app until somebody opens ${cwd} in Claude Code once and answers. This is a one-time gate per folder.`;
11476
11593
  }
11477
11594
  function untrustedFolder(cwd) {
11478
- const state2 = readJson2(path19.join(os10.homedir(), ".claude.json"));
11595
+ const state2 = readJson2(path21.join(os10.homedir(), ".claude.json"));
11479
11596
  const project = state2?.projects?.[cwd];
11480
11597
  if (!state2) return false;
11481
11598
  return project?.hasTrustDialogAccepted !== true;
11482
11599
  }
11483
11600
  function unapprovedServers(cwd) {
11484
- const declared = readJson2(path19.join(cwd, ".mcp.json"));
11601
+ const declared = readJson2(path21.join(cwd, ".mcp.json"));
11485
11602
  if (!declared?.mcpServers) return [];
11486
- const settings = readJson2(path19.join(cwd, ".claude", "settings.local.json"));
11603
+ const settings = readJson2(path21.join(cwd, ".claude", "settings.local.json"));
11487
11604
  if (settings?.enableAllProjectMcpServers === true) return [];
11488
11605
  const answered = /* @__PURE__ */ new Set([
11489
11606
  ...settings?.enabledMcpjsonServers ?? [],
@@ -11593,7 +11710,7 @@ async function brief2(repo) {
11593
11710
  }
11594
11711
 
11595
11712
  // src/commands/serve.ts
11596
- function register17(program) {
11713
+ function register19(program) {
11597
11714
  program.command("serve").description("Serve this repo to an agent over stdio \u2014 what an .mcp.json launches").option("--repo <path>", "The repo to serve. Default: the working directory").action(async (opts) => {
11598
11715
  const repo = resolveRepo(opts.repo);
11599
11716
  const problem = harnessProblem();
@@ -11655,7 +11772,7 @@ function refuseNamedConfigDir() {
11655
11772
  );
11656
11773
  return true;
11657
11774
  }
11658
- function register18(program) {
11775
+ function register20(program) {
11659
11776
  const hooks = program.command("hooks").description("Wire this machine's Claude Code to jarvis \u2014 no pairing, no daemon");
11660
11777
  hooks.command("install").description("Install the Claude Code hooks (the session block and the rules on edit)").action(() => {
11661
11778
  if (refuseNamedConfigDir()) process.exit(1);
@@ -11710,7 +11827,7 @@ Open Claude Code in a repo that has a work/ tree to see them.`);
11710
11827
 
11711
11828
  // src/commands/work.ts
11712
11829
  import { spawnSync as spawnSync3 } from "child_process";
11713
- function register19(program) {
11830
+ function register21(program) {
11714
11831
  program.command("work").description("The board \u2014 take work, prove it, finish it").argument("[args...]", "Passed to the harness untouched").allowUnknownOption().passThroughOptions().helpOption(false).action((passed) => {
11715
11832
  const problem = harnessProblem();
11716
11833
  if (problem) {
@@ -11752,22 +11869,24 @@ function createCli() {
11752
11869
  register17(program);
11753
11870
  register18(program);
11754
11871
  register19(program);
11872
+ register20(program);
11873
+ register21(program);
11755
11874
  return program;
11756
11875
  }
11757
11876
 
11758
11877
  // src/bin.ts
11759
11878
  if (isRunningFromSource()) {
11760
11879
  const dotenv = await import("dotenv");
11761
- const fs18 = await import("fs");
11762
- const path20 = await import("path");
11880
+ const fs19 = await import("fs");
11881
+ const path22 = await import("path");
11763
11882
  let dir = process.cwd();
11764
- while (dir !== path20.dirname(dir)) {
11765
- const envPath = path20.join(dir, ".env");
11766
- if (fs18.existsSync(envPath)) {
11883
+ while (dir !== path22.dirname(dir)) {
11884
+ const envPath = path22.join(dir, ".env");
11885
+ if (fs19.existsSync(envPath)) {
11767
11886
  dotenv.config({ path: envPath });
11768
11887
  break;
11769
11888
  }
11770
- dir = path20.dirname(dir);
11889
+ dir = path22.dirname(dir);
11771
11890
  }
11772
11891
  }
11773
11892
  createCli().parse();