@appchy/jarvis 0.1.47 → 0.1.48

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/dist/bin.js CHANGED
@@ -30,7 +30,6 @@ import { Command } from "commander";
30
30
  import fs2 from "fs";
31
31
  import path2 from "path";
32
32
  import os from "os";
33
- var DEFAULT_URL = "https://jarvis.appchy.com";
34
33
  function configDir() {
35
34
  if (process.env.JARVIS_CONFIG_DIR) return process.env.JARVIS_CONFIG_DIR;
36
35
  return path2.join(os.homedir(), ".jarvis");
@@ -129,8 +128,8 @@ function register(program) {
129
128
  import { execSync as execSync4 } from "child_process";
130
129
  import { createInterface as createInterface2 } from "readline";
131
130
  import fs10 from "fs";
132
- import os5 from "os";
133
- import path9 from "path";
131
+ import os6 from "os";
132
+ import path10 from "path";
134
133
 
135
134
  // src/env.ts
136
135
  import fs7 from "fs";
@@ -1756,415 +1755,93 @@ var FallbackService = class {
1756
1755
  }
1757
1756
  };
1758
1757
 
1759
- // src/commands/init.ts
1760
- function register2(program) {
1761
- program.command("init").description("Pair this machine with Jarvis cloud").option("--url <url>", `Join a different jarvis (default: ${DEFAULT_URL})`).option("--skip-daemon", "Skip the background daemon install step").option("--force", "Re-ask all prompts even when config already has answers").action(async (opts) => {
1762
- const ok = await runInit({
1763
- url: opts.url,
1764
- skipDaemonInstall: opts.skipDaemon,
1765
- force: opts.force
1766
- });
1767
- if (!ok) process.exit(1);
1768
- });
1758
+ // src/hooks/install.ts
1759
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
1760
+ import os5 from "os";
1761
+ import path9 from "path";
1762
+ var SETTINGS_DIR = path9.join(os5.homedir(), ".claude");
1763
+ var SETTINGS_FILE = path9.join(SETTINGS_DIR, "settings.json");
1764
+ var HOOK_STEMS = [
1765
+ "pre-tool-use",
1766
+ "session-start",
1767
+ "stop",
1768
+ "user-prompt-submit",
1769
+ "config-change"
1770
+ ];
1771
+ function jarvisHooks() {
1772
+ return [
1773
+ { type: "PreToolUse", matcher: "*", binBasename: "pre-tool-use.mjs" },
1774
+ { type: "SessionStart", matcher: "*", binBasename: "session-start.mjs" },
1775
+ { type: "Stop", matcher: "*", binBasename: "stop.mjs" },
1776
+ { type: "UserPromptSubmit", matcher: "*", binBasename: "user-prompt-submit.mjs" },
1777
+ { type: "ConfigChange", matcher: "*", binBasename: "config-change.mjs" }
1778
+ ];
1769
1779
  }
1770
- async function runInit(options = {}) {
1771
- const url = options.url ?? loadConfig()?.url ?? DEFAULT_URL;
1772
- const prompts = options.prompts ?? defaultPrompts();
1773
- try {
1774
- console.log("\n Jarvis local agent setup");
1775
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
1776
- console.log(` \u25B8 Pairing this machine [${url}]`);
1777
- const saved = loadConfig();
1778
- let exchanged;
1779
- try {
1780
- exchanged = await exchangeCode({
1781
- url,
1782
- machineId: getMachine().id,
1783
- ...saved?.envId ? { envId: saved.envId } : {},
1784
- version: process.env.JARVIS_VERSION ?? "0.0.0"
1785
- });
1786
- } catch (err) {
1787
- const cause = err instanceof Error ? err.cause : void 0;
1788
- const errCode = cause?.code ? ` (${cause.code})` : "";
1789
- const hint = ` Check that ${url} is reachable.`;
1790
- throw new Error(`${err instanceof Error ? err.message : err}${errCode}
1791
- ${hint}`);
1792
- }
1793
- console.log(` paired \u2713 (you = ${exchanged.userId}, this env = "${os5.hostname()}")
1794
- `);
1795
- const workspacePath2 = saved?.workspacePath ?? process.cwd();
1796
- saveConfig({
1797
- ...saved ?? {},
1798
- hubUrl: exchanged.hubUrl,
1799
- url,
1800
- token: exchanged.token,
1801
- ...exchanged.refreshToken ? { refreshToken: exchanged.refreshToken } : {},
1802
- userId: exchanged.userId,
1803
- envId: exchanged.envId,
1804
- workspacePath: workspacePath2,
1805
- connectedAt: (/* @__PURE__ */ new Date()).toISOString()
1806
- });
1807
- const existingRoots = readLocalRoots();
1808
- const roots = await chooseRoots({
1809
- existingRoots,
1810
- force: options.force ?? false,
1811
- prompts
1812
- });
1813
- if (roots && roots.length > 0 && !rootsEqual(roots, existingRoots)) {
1814
- await applyRoots(roots);
1815
- console.log(` registered ${roots.length} root(s) \u2713
1816
- `);
1817
- } else if (roots && rootsEqual(roots, existingRoots)) {
1818
- console.log(` kept ${roots.length} saved root(s) \u2713
1819
- `);
1820
- }
1821
- if (!options.skipDaemonInstall) {
1822
- console.log(" \u25B8 Installing background daemon \u2026");
1823
- const installed = await tryInstallDaemon();
1824
- console.log(installed ? " \u2713\n" : " skipped (run 'jarvis start' manually)\n");
1825
- }
1826
- console.log(" \u2714 Done. Open " + url + " \u2014 your data is there.");
1827
- console.log(" Manage anytime: jarvis settings");
1828
- console.log(" " + url + "/settings/environments\n");
1829
- return true;
1830
- } catch (err) {
1831
- console.error(`
1832
- Error: ${err instanceof Error ? err.message : err}`);
1833
- return false;
1834
- }
1780
+ function isJarvisCommand(command) {
1781
+ const name = path9.basename(command);
1782
+ return HOOK_STEMS.some((stem) => name === `${stem}.mjs` || name === `${stem}.dev.mjs`);
1835
1783
  }
1836
- function discoverCandidateRoots() {
1837
- const home = os5.homedir();
1838
- const names = ["Code", "code", "work", "src", "Projects", "projects", "dev", "Developer"];
1839
- const out = [];
1840
- for (const name of names) {
1841
- const p = path9.join(home, name);
1842
- let exists = false;
1843
- try {
1844
- exists = fs10.statSync(p).isDirectory();
1845
- } catch {
1846
- continue;
1847
- }
1848
- if (!exists) continue;
1849
- out.push({
1850
- path: p,
1851
- exists: true,
1852
- repoCount: countRepos(p)
1853
- });
1784
+ function getCliRoot() {
1785
+ const binPath = process.argv[1];
1786
+ if (!binPath) {
1787
+ return process.cwd();
1854
1788
  }
1855
- return out;
1856
- }
1857
- function countRepos(root) {
1789
+ let entry = binPath;
1858
1790
  try {
1859
- const out = execSync4(`find "${root}" -maxdepth 3 -type d -name .git 2>/dev/null | wc -l`, {
1860
- encoding: "utf-8",
1861
- timeout: 5e3
1862
- }).trim();
1863
- const n = parseInt(out, 10);
1864
- return Number.isFinite(n) ? n : void 0;
1791
+ entry = realpathSync(binPath);
1865
1792
  } catch {
1866
- return void 0;
1867
1793
  }
1794
+ return path9.resolve(path9.dirname(entry), "..");
1868
1795
  }
1869
- async function tryInstallDaemon() {
1796
+ function resolveHooks() {
1797
+ const cliRoot = getCliRoot();
1798
+ return jarvisHooks().map((h) => ({
1799
+ ...h,
1800
+ binAbsPath: path9.join(cliRoot, "bin", h.binBasename)
1801
+ }));
1802
+ }
1803
+ function ownsClaudeCode() {
1804
+ return getConfigDir() === path9.join(os5.homedir(), ".jarvis");
1805
+ }
1806
+ function readSettings() {
1807
+ if (!existsSync(SETTINGS_FILE)) return {};
1870
1808
  try {
1871
- const service = createServiceManager();
1872
- if (service.isInstalled()) return true;
1873
- const { nodePath, entryPath } = resolveEntryPoint();
1874
- service.install({
1875
- workspacePath: process.cwd(),
1876
- nodePath,
1877
- entryPath,
1878
- upstream: true
1809
+ const raw = readFileSync(SETTINGS_FILE, "utf-8");
1810
+ return raw.trim() ? JSON.parse(raw) : {};
1811
+ } catch (err) {
1812
+ logger.sys.warn("[hooks.install] settings.json unreadable", {
1813
+ error: err instanceof Error ? err.message : String(err)
1879
1814
  });
1880
- return true;
1881
- } catch {
1882
- return false;
1815
+ return {};
1883
1816
  }
1884
1817
  }
1885
- async function chooseRoots(req) {
1886
- const { existingRoots, force, prompts } = req;
1887
- if (!force && existingRoots.length > 0) {
1888
- console.log(
1889
- ` \u25B8 Using existing roots: ${existingRoots.join(", ")} (pass --force to re-pick)
1890
- `
1891
- );
1892
- return null;
1893
- }
1894
- console.log(" \u25B8 Where does your code live?");
1895
- if (existingRoots.length > 0) {
1896
- console.log(" saved roots:\n");
1897
- for (const r of existingRoots) console.log(` [\u2713] ${r}`);
1898
- console.log();
1899
- }
1900
- const choices = [];
1901
- if (existingRoots.length > 0) {
1902
- choices.push({ key: "k", label: "keep saved (Enter)" });
1903
- }
1904
- choices.push({ key: "m", label: "manual entry" });
1905
- choices.push({ key: "s", label: "scan home dir" });
1906
- const prompt = " " + choices.map((c) => `[${c.key}] ${c.label}`).join(" \xB7 ") + "\n > ";
1907
- const answer = (await readlineQuestion2(prompt)).trim().toLowerCase();
1908
- const defaultKey = existingRoots.length > 0 ? "k" : "m";
1909
- const choice = answer || defaultKey;
1910
- if (choice === "k" || choice === "keep") return dedupePaths2(existingRoots);
1911
- if (choice === "m" || choice === "manual") {
1912
- const fallback = existingRoots.length > 0 ? existingRoots : [];
1913
- const hint = fallback.length > 0 ? ` enter comma-separated paths (or Enter to keep saved)
1914
- [${fallback.join(", ")}]
1915
- > ` : " enter comma-separated paths\n > ";
1916
- return promptList(hint, fallback);
1917
- }
1918
- console.log(" scanning home dir \u2026 (may take a few seconds)");
1919
- const discovered = discoverCandidateRoots();
1920
- const merged = mergeRootCandidates(existingRoots, discovered);
1921
- if (merged.length === 0) {
1922
- console.log(" no roots found under your home dir.");
1923
- return existingRoots.length > 0 ? dedupePaths2(existingRoots) : [];
1924
- }
1925
- if (existingRoots.length > 0) {
1926
- console.log(" saved [\u2713] / discovered [+] roots:\n");
1927
- } else {
1928
- console.log(" found these likely roots:\n");
1929
- }
1930
- for (const c of merged) {
1931
- const marker = c.saved ? "\u2713" : "+";
1932
- const meta = [
1933
- c.repoCount !== void 0 ? `${c.repoCount} repos` : null,
1934
- c.saved ? "saved" : null
1935
- ].filter(Boolean).join(", ");
1936
- const suffix = meta ? ` (${meta})` : "";
1937
- console.log(` [${marker}] ${c.path}${suffix}`);
1818
+ function writeSettings(s) {
1819
+ try {
1820
+ mkdirSync(SETTINGS_DIR, { recursive: true });
1821
+ writeFileSync(SETTINGS_FILE, JSON.stringify(s, null, 2) + "\n");
1822
+ return true;
1823
+ } catch (err) {
1824
+ logger.sys.warn("[hooks.install] settings.json unwritable", {
1825
+ error: err instanceof Error ? err.message : String(err)
1826
+ });
1827
+ return false;
1938
1828
  }
1939
- console.log();
1940
- return prompts.pickRoots(merged);
1941
1829
  }
1942
- function rootsEqual(a, b) {
1943
- if (a.length !== b.length) return false;
1944
- const setA = new Set(a.map(normalizePath));
1945
- for (const p of b) if (!setA.has(normalizePath(p))) return false;
1946
- return true;
1830
+ function ownsEntry(entry) {
1831
+ return (entry.hooks ?? []).some(
1832
+ (h) => typeof h.command === "string" && isJarvisCommand(h.command)
1833
+ );
1947
1834
  }
1948
- async function readlineQuestion2(question) {
1949
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
1950
- return new Promise((resolve9) => {
1951
- rl.question(question, (input) => {
1952
- rl.close();
1953
- resolve9(input);
1954
- });
1835
+ function upsertHook(settings, hook) {
1836
+ const hooks = { ...settings.hooks ?? {} };
1837
+ const prior = hooks[hook.type];
1838
+ const list4 = Array.isArray(prior) ? prior.filter((e) => !ownsEntry(e)) : [];
1839
+ list4.push({
1840
+ matcher: hook.matcher,
1841
+ hooks: [{ type: "command", command: hook.binAbsPath }]
1955
1842
  });
1956
- }
1957
- function normalizePath(p) {
1958
- const resolved = path9.resolve(p.trim());
1959
- try {
1960
- return fs10.realpathSync.native(resolved);
1961
- } catch {
1962
- return resolved;
1963
- }
1964
- }
1965
- function dedupePaths2(paths) {
1966
- const seen = /* @__PURE__ */ new Set();
1967
- const out = [];
1968
- for (const p of paths) {
1969
- if (!p) continue;
1970
- const norm = normalizePath(p);
1971
- if (seen.has(norm)) continue;
1972
- seen.add(norm);
1973
- out.push(norm);
1974
- }
1975
- return out;
1976
- }
1977
- function mergeRootCandidates(saved, discovered) {
1978
- const out = [];
1979
- const seen = /* @__PURE__ */ new Set();
1980
- const discoveredByPath = /* @__PURE__ */ new Map();
1981
- for (const c of discovered) discoveredByPath.set(normalizePath(c.path), c);
1982
- for (const raw of saved) {
1983
- const norm = normalizePath(raw);
1984
- if (seen.has(norm)) continue;
1985
- seen.add(norm);
1986
- const found = discoveredByPath.get(norm);
1987
- let exists = false;
1988
- try {
1989
- exists = fs10.statSync(norm).isDirectory();
1990
- } catch {
1991
- }
1992
- out.push({
1993
- path: norm,
1994
- exists,
1995
- saved: true,
1996
- ...found?.repoCount !== void 0 ? { repoCount: found.repoCount } : {}
1997
- });
1998
- }
1999
- for (const c of discovered) {
2000
- const norm = normalizePath(c.path);
2001
- if (seen.has(norm)) continue;
2002
- seen.add(norm);
2003
- out.push({ ...c, path: norm });
2004
- }
2005
- return out;
2006
- }
2007
- function readLocalRoots() {
2008
- try {
2009
- const raw = fs10.readFileSync(path9.join(getConfigDir(), "env.json"), "utf-8");
2010
- const parsed = JSON.parse(raw);
2011
- return Array.isArray(parsed.roots) ? parsed.roots : [];
2012
- } catch {
2013
- return [];
2014
- }
2015
- }
2016
- function defaultPrompts() {
2017
- return {
2018
- async pickRoots(candidates) {
2019
- const savedDefaults = candidates.filter((c) => c.saved).map((c) => c.path);
2020
- const defaults = savedDefaults.length > 0 ? savedDefaults : candidates.filter((c) => c.exists).map((c) => c.path);
2021
- const hint = defaults.length > 0 ? `keep saved (Enter), or enter comma-separated paths
2022
- [${defaults.join(", ")}]
2023
- > ` : " enter comma-separated paths\n > ";
2024
- return promptList(hint, defaults);
2025
- }
2026
- };
2027
- }
2028
- async function promptList(question, fallback) {
2029
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
2030
- const answer = await new Promise((resolve9) => {
2031
- rl.question(question, (input) => resolve9(input.trim()));
2032
- });
2033
- rl.close();
2034
- const raw = !answer ? fallback : answer.split(",").map((s) => s.trim()).filter(Boolean);
2035
- return dedupePaths2(raw);
2036
- }
2037
-
2038
- // src/commands/pair.ts
2039
- function register3(program) {
2040
- program.command("pair").description("Re-pair this machine (when token expired)").action(async () => {
2041
- try {
2042
- await runPair();
2043
- } catch (err) {
2044
- console.error(`
2045
- Error: ${err instanceof Error ? err.message : err}`);
2046
- process.exit(1);
2047
- }
2048
- });
2049
- }
2050
- async function runPair() {
2051
- const existing = loadConfig();
2052
- const url = existing?.url ?? DEFAULT_URL;
2053
- const result2 = await exchangeCode({
2054
- url,
2055
- machineId: getMachine().id,
2056
- ...existing?.envId ? { envId: existing.envId } : {}
2057
- });
2058
- saveConfig({
2059
- ...existing ?? {},
2060
- hubUrl: result2.hubUrl,
2061
- url,
2062
- token: result2.token,
2063
- ...result2.refreshToken ? { refreshToken: result2.refreshToken } : {},
2064
- userId: result2.userId,
2065
- envId: result2.envId,
2066
- connectedAt: (/* @__PURE__ */ new Date()).toISOString()
2067
- });
2068
- console.log(" paired \u2713");
2069
- }
2070
-
2071
- // src/commands/unpair.ts
2072
- import { createInterface as createInterface3 } from "readline";
2073
-
2074
- // src/mcp.ts
2075
- import { execFile } from "child_process";
2076
- import { existsSync as existsSync2, readdirSync } from "fs";
2077
- import os7 from "os";
2078
- import path11 from "path";
2079
- import { promisify } from "util";
2080
-
2081
- // src/hooks/install.ts
2082
- import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
2083
- import os6 from "os";
2084
- import path10 from "path";
2085
- var SETTINGS_DIR = path10.join(os6.homedir(), ".claude");
2086
- var SETTINGS_FILE = path10.join(SETTINGS_DIR, "settings.json");
2087
- var HOOK_STEMS = [
2088
- "pre-tool-use",
2089
- "session-start",
2090
- "stop",
2091
- "user-prompt-submit",
2092
- "config-change"
2093
- ];
2094
- function jarvisHooks() {
2095
- return [
2096
- { type: "PreToolUse", matcher: "*", binBasename: "pre-tool-use.mjs" },
2097
- { type: "SessionStart", matcher: "*", binBasename: "session-start.mjs" },
2098
- { type: "Stop", matcher: "*", binBasename: "stop.mjs" },
2099
- { type: "UserPromptSubmit", matcher: "*", binBasename: "user-prompt-submit.mjs" },
2100
- { type: "ConfigChange", matcher: "*", binBasename: "config-change.mjs" }
2101
- ];
2102
- }
2103
- function isJarvisCommand(command) {
2104
- const name = path10.basename(command);
2105
- return HOOK_STEMS.some((stem) => name === `${stem}.mjs` || name === `${stem}.dev.mjs`);
2106
- }
2107
- function getCliRoot() {
2108
- const binPath = process.argv[1];
2109
- if (!binPath) {
2110
- return process.cwd();
2111
- }
2112
- let entry = binPath;
2113
- try {
2114
- entry = realpathSync(binPath);
2115
- } catch {
2116
- }
2117
- return path10.resolve(path10.dirname(entry), "..");
2118
- }
2119
- function resolveHooks() {
2120
- const cliRoot = getCliRoot();
2121
- return jarvisHooks().map((h) => ({
2122
- ...h,
2123
- binAbsPath: path10.join(cliRoot, "bin", h.binBasename)
2124
- }));
2125
- }
2126
- function ownsClaudeCode() {
2127
- return getConfigDir() === path10.join(os6.homedir(), ".jarvis");
2128
- }
2129
- function readSettings() {
2130
- if (!existsSync(SETTINGS_FILE)) return {};
2131
- try {
2132
- const raw = readFileSync(SETTINGS_FILE, "utf-8");
2133
- return raw.trim() ? JSON.parse(raw) : {};
2134
- } catch (err) {
2135
- logger.sys.warn("[hooks.install] settings.json unreadable", {
2136
- error: err instanceof Error ? err.message : String(err)
2137
- });
2138
- return {};
2139
- }
2140
- }
2141
- function writeSettings(s) {
2142
- try {
2143
- mkdirSync(SETTINGS_DIR, { recursive: true });
2144
- writeFileSync(SETTINGS_FILE, JSON.stringify(s, null, 2) + "\n");
2145
- return true;
2146
- } catch (err) {
2147
- logger.sys.warn("[hooks.install] settings.json unwritable", {
2148
- error: err instanceof Error ? err.message : String(err)
2149
- });
2150
- return false;
2151
- }
2152
- }
2153
- function ownsEntry(entry) {
2154
- return (entry.hooks ?? []).some(
2155
- (h) => typeof h.command === "string" && isJarvisCommand(h.command)
2156
- );
2157
- }
2158
- function upsertHook(settings, hook) {
2159
- const hooks = { ...settings.hooks ?? {} };
2160
- const prior = hooks[hook.type];
2161
- const list4 = Array.isArray(prior) ? prior.filter((e) => !ownsEntry(e)) : [];
2162
- list4.push({
2163
- matcher: hook.matcher,
2164
- hooks: [{ type: "command", command: hook.binAbsPath }]
2165
- });
2166
- hooks[hook.type] = list4;
2167
- return { ...settings, hooks };
1843
+ hooks[hook.type] = list4;
1844
+ return { ...settings, hooks };
2168
1845
  }
2169
1846
  function removeHook(settings, hook) {
2170
1847
  const hooks = { ...settings.hooks ?? {} };
@@ -2243,48 +1920,507 @@ function installClaudeCodeHooks() {
2243
1920
  hooks: installed,
2244
1921
  settingsFile: SETTINGS_FILE
2245
1922
  });
2246
- } else {
2247
- logger.sys.warn("[hooks.install] settings.json write failed", { configDir: configDir2 });
1923
+ } else {
1924
+ logger.sys.warn("[hooks.install] settings.json write failed", { configDir: configDir2 });
1925
+ }
1926
+ }
1927
+ function uninstallClaudeCodeHooks() {
1928
+ const configDir2 = getConfigDir();
1929
+ if (!ownsClaudeCode()) {
1930
+ logger.sys.info("[hooks.install] not this daemon's Claude Code to unhook, skipping", {
1931
+ configDir: configDir2
1932
+ });
1933
+ return;
1934
+ }
1935
+ logger.sys.info("[hooks.install] uninstall starting", {
1936
+ configDir: configDir2,
1937
+ settingsFile: SETTINGS_FILE
1938
+ });
1939
+ let settings = readSettings();
1940
+ let changed = false;
1941
+ const removed = [];
1942
+ for (const hook of jarvisHooks()) {
1943
+ const r = removeHook(settings, hook);
1944
+ if (r.changed) {
1945
+ settings = r.next;
1946
+ changed = true;
1947
+ removed.push(`${hook.type}:${hook.binBasename}`);
1948
+ }
1949
+ }
1950
+ if (!changed) {
1951
+ logger.sys.info("[hooks.install] uninstall: nothing to remove", { configDir: configDir2 });
1952
+ return;
1953
+ }
1954
+ if (writeSettings(settings)) {
1955
+ logger.sys.info("[hooks.install] Claude Code hooks uninstalled", {
1956
+ configDir: configDir2,
1957
+ hooks: removed
1958
+ });
1959
+ } else {
1960
+ logger.sys.warn("[hooks.install] uninstall: settings.json write failed", { configDir: configDir2 });
1961
+ }
1962
+ }
1963
+
1964
+ // src/commands/hooks.ts
1965
+ var GREEN = (text) => `\x1B[32m${text}\x1B[0m`;
1966
+ var RED = (text) => `\x1B[31m${text}\x1B[0m`;
1967
+ var YELLOW = (text) => `\x1B[33m${text}\x1B[0m`;
1968
+ function quietInstallerNarration() {
1969
+ const drop = () => {
1970
+ };
1971
+ const provider2 = {
1972
+ debug: drop,
1973
+ info: drop,
1974
+ warn: (_ctx, message) => console.warn(` ${message}`),
1975
+ error: (_ctx, message) => console.error(` ${message}`),
1976
+ audit: drop
1977
+ };
1978
+ logger.init({ provider: provider2 });
1979
+ }
1980
+ function isOurs(hook) {
1981
+ return hook.command === hook.bin;
1982
+ }
1983
+ function isElsewhere(hook) {
1984
+ return hook.command !== void 0 && hook.command !== hook.bin;
1985
+ }
1986
+ function report(state2) {
1987
+ console.log(` Settings: ${claudeSettingsFile}`);
1988
+ for (const hook of state2) {
1989
+ const mark = isOurs(hook) ? GREEN("installed") : isElsewhere(hook) ? YELLOW("another install") : RED("missing");
1990
+ const gone = hook.exists ? "" : RED(" \u2014 the shim it names is not there");
1991
+ console.log(` ${hook.type.padEnd(17)} ${mark}${gone}`);
1992
+ console.log(` ${"".padEnd(17)} ${hook.command ?? hook.bin}`);
1993
+ }
1994
+ if (state2.some(isElsewhere)) {
1995
+ console.log(
1996
+ `
1997
+ Claude Code is running hooks from a different copy of the CLI than this one.
1998
+ Both work, and the one you are reading is not the one running. Install from
1999
+ whichever copy you mean to keep, or remove the other.`
2000
+ );
2001
+ }
2002
+ }
2003
+ function refuseNamedConfigDir() {
2004
+ if (ownsClaudeCode()) return false;
2005
+ console.error(
2006
+ `This config dir does not own this machine's Claude Code, so nothing was changed.
2007
+ Config dir: ${getConfigDir()}
2008
+ There is one Claude Code per machine and the default dir owns it. Run this
2009
+ without JARVIS_CONFIG_DIR set.`
2010
+ );
2011
+ return true;
2012
+ }
2013
+ function installHooks() {
2014
+ if (refuseNamedConfigDir()) return { outcome: "not-ours", installed: 0, total: 0 };
2015
+ quietInstallerNarration();
2016
+ installClaudeCodeHooks();
2017
+ const state2 = claudeCodeHooksState();
2018
+ const installed = state2.filter(isOurs).length;
2019
+ if (installed === 0) {
2020
+ console.error("No hooks were installed.");
2021
+ report(state2);
2022
+ console.error(
2023
+ `
2024
+ Every shim above is missing, which means this is not a complete install.
2025
+ Reinstall the CLI: npm i -g @appchy/jarvis`
2026
+ );
2027
+ return { outcome: "incomplete", installed, total: state2.length };
2028
+ }
2029
+ console.log(`Claude Code hooks installed (${installed} of ${state2.length}):`);
2030
+ report(state2);
2031
+ return { outcome: "installed", installed, total: state2.length };
2032
+ }
2033
+ function register2(program) {
2034
+ const hooks = program.command("hooks").description("Wire this machine's Claude Code to jarvis \u2014 no pairing, no daemon");
2035
+ hooks.command("install").description("Install the Claude Code hooks (the session block and the rules on edit)").action(() => {
2036
+ if (installHooks().outcome !== "installed") process.exit(1);
2037
+ console.log(`
2038
+ Open Claude Code in a repo that has a work/ tree to see them.`);
2039
+ });
2040
+ hooks.command("status").description("What Claude Code will actually run").action(() => {
2041
+ const state2 = claudeCodeHooksState();
2042
+ const installed = state2.filter(isOurs).length;
2043
+ console.log(
2044
+ installed === state2.length ? `All ${state2.length} Claude Code hooks are installed:` : `${installed} of ${state2.length} Claude Code hooks are installed:`
2045
+ );
2046
+ report(state2);
2047
+ if (!ownsClaudeCode()) {
2048
+ console.log(
2049
+ `
2050
+ Note: this config dir (${getConfigDir()}) does not own this machine's
2051
+ Claude Code, so it neither installs these nor removes them.`
2052
+ );
2053
+ }
2054
+ });
2055
+ hooks.command("uninstall").description("Remove the Claude Code hooks this machine installed").action(() => {
2056
+ if (refuseNamedConfigDir()) process.exit(1);
2057
+ quietInstallerNarration();
2058
+ uninstallClaudeCodeHooks();
2059
+ const left = claudeCodeHooksState().filter(isOurs);
2060
+ if (left.length === 0) {
2061
+ console.log("Claude Code hooks removed.");
2062
+ return;
2063
+ }
2064
+ console.error(`${left.length} hook(s) could not be removed:`);
2065
+ report(left);
2066
+ process.exit(1);
2067
+ });
2068
+ }
2069
+
2070
+ // src/commands/init.ts
2071
+ function register3(program) {
2072
+ program.command("init").description("Set this machine up to work in repos, and join an instance if you name one").option("--url <url>", "Also join the jarvis instance at this address").option("--skip-daemon", "Skip the background daemon install step").option("--force", "Re-ask all prompts even when config already has answers").action(async (opts) => {
2073
+ const ok = await runInit({
2074
+ url: opts.url,
2075
+ skipDaemonInstall: opts.skipDaemon,
2076
+ force: opts.force
2077
+ });
2078
+ if (!ok) process.exit(1);
2079
+ });
2080
+ }
2081
+ async function runInit(options = {}) {
2082
+ const url = options.url ?? loadConfig()?.url;
2083
+ const prompts = options.prompts ?? defaultPrompts();
2084
+ try {
2085
+ console.log("\n Jarvis setup");
2086
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
2087
+ setUpThisMachine();
2088
+ if (!url) {
2089
+ reportLocalOnly();
2090
+ return true;
2091
+ }
2092
+ console.log(` \u25B8 Pairing this machine [${url}]`);
2093
+ const saved = loadConfig();
2094
+ let exchanged;
2095
+ try {
2096
+ exchanged = await exchangeCode({
2097
+ url,
2098
+ machineId: getMachine().id,
2099
+ ...saved?.envId ? { envId: saved.envId } : {},
2100
+ version: process.env.JARVIS_VERSION ?? "0.0.0"
2101
+ });
2102
+ } catch (err) {
2103
+ const cause = err instanceof Error ? err.cause : void 0;
2104
+ const errCode = cause?.code ? ` (${cause.code})` : "";
2105
+ const hint = ` Check that ${url} is reachable.`;
2106
+ throw new Error(`${err instanceof Error ? err.message : err}${errCode}
2107
+ ${hint}`);
2108
+ }
2109
+ console.log(` paired \u2713 (you = ${exchanged.userId}, this env = "${os6.hostname()}")
2110
+ `);
2111
+ const workspacePath2 = saved?.workspacePath ?? process.cwd();
2112
+ saveConfig({
2113
+ ...saved ?? {},
2114
+ hubUrl: exchanged.hubUrl,
2115
+ url,
2116
+ token: exchanged.token,
2117
+ ...exchanged.refreshToken ? { refreshToken: exchanged.refreshToken } : {},
2118
+ userId: exchanged.userId,
2119
+ envId: exchanged.envId,
2120
+ workspacePath: workspacePath2,
2121
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
2122
+ });
2123
+ const existingRoots = readLocalRoots();
2124
+ const roots = await chooseRoots({
2125
+ existingRoots,
2126
+ force: options.force ?? false,
2127
+ prompts
2128
+ });
2129
+ if (roots && roots.length > 0 && !rootsEqual(roots, existingRoots)) {
2130
+ await applyRoots(roots);
2131
+ console.log(` registered ${roots.length} root(s) \u2713
2132
+ `);
2133
+ } else if (roots && rootsEqual(roots, existingRoots)) {
2134
+ console.log(` kept ${roots.length} saved root(s) \u2713
2135
+ `);
2136
+ }
2137
+ if (!options.skipDaemonInstall) {
2138
+ console.log(" \u25B8 Installing background daemon \u2026");
2139
+ const installed = await tryInstallDaemon();
2140
+ console.log(installed ? " \u2713\n" : " skipped (run 'jarvis start' manually)\n");
2141
+ }
2142
+ console.log(" \u2714 Done. This machine works in repos, and it has joined " + url + ".");
2143
+ console.log(" Manage anytime: jarvis settings");
2144
+ console.log(" " + url + "/settings/environments\n");
2145
+ return true;
2146
+ } catch (err) {
2147
+ console.error(`
2148
+ Error: ${err instanceof Error ? err.message : err}`);
2149
+ return false;
2150
+ }
2151
+ }
2152
+ function setUpThisMachine() {
2153
+ console.log(" \u25B8 Wiring this machine's Claude Code to jarvis");
2154
+ installHooks();
2155
+ console.log();
2156
+ console.log(" \u25B8 The model key the map embeds with");
2157
+ const key = loadConfig()?.openaiApiKey;
2158
+ console.log(
2159
+ key ? ` set (\u2026${key.slice(-4)}) \u2713 the map builds a semantic index
2160
+ ` : ` not set \u2014 the map still builds, and its search stays lexical.
2161
+ Set one with 'jarvis model key <key>'.
2162
+ `
2163
+ );
2164
+ }
2165
+ function reportLocalOnly() {
2166
+ console.log(" \u2714 Done. This machine works in any repo that has a work/ tree:");
2167
+ console.log(" the board, the map, the session block, the rules that arrive on edit,");
2168
+ console.log(" and 'jarvis serve' for any agent that speaks MCP.\n");
2169
+ console.log(" In a repo you have cloned: jarvis build graph");
2170
+ console.log(" jarvis work list\n");
2171
+ console.log(" It has joined no jarvis instance, so it has no web surface and cannot");
2172
+ console.log(" start sessions on other machines. Both need one:");
2173
+ console.log(" jarvis init --url <address>\n");
2174
+ }
2175
+ function discoverCandidateRoots() {
2176
+ const home = os6.homedir();
2177
+ const names = ["Code", "code", "work", "src", "Projects", "projects", "dev", "Developer"];
2178
+ const out = [];
2179
+ for (const name of names) {
2180
+ const p = path10.join(home, name);
2181
+ let exists = false;
2182
+ try {
2183
+ exists = fs10.statSync(p).isDirectory();
2184
+ } catch {
2185
+ continue;
2186
+ }
2187
+ if (!exists) continue;
2188
+ out.push({
2189
+ path: p,
2190
+ exists: true,
2191
+ repoCount: countRepos(p)
2192
+ });
2193
+ }
2194
+ return out;
2195
+ }
2196
+ function countRepos(root) {
2197
+ try {
2198
+ const out = execSync4(`find "${root}" -maxdepth 3 -type d -name .git 2>/dev/null | wc -l`, {
2199
+ encoding: "utf-8",
2200
+ timeout: 5e3
2201
+ }).trim();
2202
+ const n = parseInt(out, 10);
2203
+ return Number.isFinite(n) ? n : void 0;
2204
+ } catch {
2205
+ return void 0;
2206
+ }
2207
+ }
2208
+ async function tryInstallDaemon() {
2209
+ try {
2210
+ const service = createServiceManager();
2211
+ if (service.isInstalled()) return true;
2212
+ const { nodePath, entryPath } = resolveEntryPoint();
2213
+ service.install({
2214
+ workspacePath: process.cwd(),
2215
+ nodePath,
2216
+ entryPath,
2217
+ upstream: true
2218
+ });
2219
+ return true;
2220
+ } catch {
2221
+ return false;
2222
+ }
2223
+ }
2224
+ async function chooseRoots(req) {
2225
+ const { existingRoots, force, prompts } = req;
2226
+ if (!force && existingRoots.length > 0) {
2227
+ console.log(
2228
+ ` \u25B8 Using existing roots: ${existingRoots.join(", ")} (pass --force to re-pick)
2229
+ `
2230
+ );
2231
+ return null;
2232
+ }
2233
+ console.log(" \u25B8 Where does your code live?");
2234
+ if (existingRoots.length > 0) {
2235
+ console.log(" saved roots:\n");
2236
+ for (const r of existingRoots) console.log(` [\u2713] ${r}`);
2237
+ console.log();
2238
+ }
2239
+ const choices = [];
2240
+ if (existingRoots.length > 0) {
2241
+ choices.push({ key: "k", label: "keep saved (Enter)" });
2242
+ }
2243
+ choices.push({ key: "m", label: "manual entry" });
2244
+ choices.push({ key: "s", label: "scan home dir" });
2245
+ const prompt = " " + choices.map((c) => `[${c.key}] ${c.label}`).join(" \xB7 ") + "\n > ";
2246
+ const answer = (await readlineQuestion2(prompt)).trim().toLowerCase();
2247
+ const defaultKey = existingRoots.length > 0 ? "k" : "m";
2248
+ const choice = answer || defaultKey;
2249
+ if (choice === "k" || choice === "keep") return dedupePaths2(existingRoots);
2250
+ if (choice === "m" || choice === "manual") {
2251
+ const fallback = existingRoots.length > 0 ? existingRoots : [];
2252
+ const hint = fallback.length > 0 ? ` enter comma-separated paths (or Enter to keep saved)
2253
+ [${fallback.join(", ")}]
2254
+ > ` : " enter comma-separated paths\n > ";
2255
+ return promptList(hint, fallback);
2256
+ }
2257
+ console.log(" scanning home dir \u2026 (may take a few seconds)");
2258
+ const discovered = discoverCandidateRoots();
2259
+ const merged = mergeRootCandidates(existingRoots, discovered);
2260
+ if (merged.length === 0) {
2261
+ console.log(" no roots found under your home dir.");
2262
+ return existingRoots.length > 0 ? dedupePaths2(existingRoots) : [];
2263
+ }
2264
+ if (existingRoots.length > 0) {
2265
+ console.log(" saved [\u2713] / discovered [+] roots:\n");
2266
+ } else {
2267
+ console.log(" found these likely roots:\n");
2268
+ }
2269
+ for (const c of merged) {
2270
+ const marker = c.saved ? "\u2713" : "+";
2271
+ const meta = [
2272
+ c.repoCount !== void 0 ? `${c.repoCount} repos` : null,
2273
+ c.saved ? "saved" : null
2274
+ ].filter(Boolean).join(", ");
2275
+ const suffix = meta ? ` (${meta})` : "";
2276
+ console.log(` [${marker}] ${c.path}${suffix}`);
2277
+ }
2278
+ console.log();
2279
+ return prompts.pickRoots(merged);
2280
+ }
2281
+ function rootsEqual(a, b) {
2282
+ if (a.length !== b.length) return false;
2283
+ const setA = new Set(a.map(normalizePath));
2284
+ for (const p of b) if (!setA.has(normalizePath(p))) return false;
2285
+ return true;
2286
+ }
2287
+ async function readlineQuestion2(question) {
2288
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
2289
+ return new Promise((resolve9) => {
2290
+ rl.question(question, (input) => {
2291
+ rl.close();
2292
+ resolve9(input);
2293
+ });
2294
+ });
2295
+ }
2296
+ function normalizePath(p) {
2297
+ const resolved = path10.resolve(p.trim());
2298
+ try {
2299
+ return fs10.realpathSync.native(resolved);
2300
+ } catch {
2301
+ return resolved;
2302
+ }
2303
+ }
2304
+ function dedupePaths2(paths) {
2305
+ const seen = /* @__PURE__ */ new Set();
2306
+ const out = [];
2307
+ for (const p of paths) {
2308
+ if (!p) continue;
2309
+ const norm = normalizePath(p);
2310
+ if (seen.has(norm)) continue;
2311
+ seen.add(norm);
2312
+ out.push(norm);
2313
+ }
2314
+ return out;
2315
+ }
2316
+ function mergeRootCandidates(saved, discovered) {
2317
+ const out = [];
2318
+ const seen = /* @__PURE__ */ new Set();
2319
+ const discoveredByPath = /* @__PURE__ */ new Map();
2320
+ for (const c of discovered) discoveredByPath.set(normalizePath(c.path), c);
2321
+ for (const raw of saved) {
2322
+ const norm = normalizePath(raw);
2323
+ if (seen.has(norm)) continue;
2324
+ seen.add(norm);
2325
+ const found = discoveredByPath.get(norm);
2326
+ let exists = false;
2327
+ try {
2328
+ exists = fs10.statSync(norm).isDirectory();
2329
+ } catch {
2330
+ }
2331
+ out.push({
2332
+ path: norm,
2333
+ exists,
2334
+ saved: true,
2335
+ ...found?.repoCount !== void 0 ? { repoCount: found.repoCount } : {}
2336
+ });
2337
+ }
2338
+ for (const c of discovered) {
2339
+ const norm = normalizePath(c.path);
2340
+ if (seen.has(norm)) continue;
2341
+ seen.add(norm);
2342
+ out.push({ ...c, path: norm });
2248
2343
  }
2344
+ return out;
2249
2345
  }
2250
- function uninstallClaudeCodeHooks() {
2251
- const configDir2 = getConfigDir();
2252
- if (!ownsClaudeCode()) {
2253
- logger.sys.info("[hooks.install] not this daemon's Claude Code to unhook, skipping", {
2254
- configDir: configDir2
2255
- });
2256
- return;
2346
+ function readLocalRoots() {
2347
+ try {
2348
+ const raw = fs10.readFileSync(path10.join(getConfigDir(), "env.json"), "utf-8");
2349
+ const parsed = JSON.parse(raw);
2350
+ return Array.isArray(parsed.roots) ? parsed.roots : [];
2351
+ } catch {
2352
+ return [];
2257
2353
  }
2258
- logger.sys.info("[hooks.install] uninstall starting", {
2259
- configDir: configDir2,
2260
- settingsFile: SETTINGS_FILE
2354
+ }
2355
+ function defaultPrompts() {
2356
+ return {
2357
+ async pickRoots(candidates) {
2358
+ const savedDefaults = candidates.filter((c) => c.saved).map((c) => c.path);
2359
+ const defaults = savedDefaults.length > 0 ? savedDefaults : candidates.filter((c) => c.exists).map((c) => c.path);
2360
+ const hint = defaults.length > 0 ? `keep saved (Enter), or enter comma-separated paths
2361
+ [${defaults.join(", ")}]
2362
+ > ` : " enter comma-separated paths\n > ";
2363
+ return promptList(hint, defaults);
2364
+ }
2365
+ };
2366
+ }
2367
+ async function promptList(question, fallback) {
2368
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
2369
+ const answer = await new Promise((resolve9) => {
2370
+ rl.question(question, (input) => resolve9(input.trim()));
2261
2371
  });
2262
- let settings = readSettings();
2263
- let changed = false;
2264
- const removed = [];
2265
- for (const hook of jarvisHooks()) {
2266
- const r = removeHook(settings, hook);
2267
- if (r.changed) {
2268
- settings = r.next;
2269
- changed = true;
2270
- removed.push(`${hook.type}:${hook.binBasename}`);
2372
+ rl.close();
2373
+ const raw = !answer ? fallback : answer.split(",").map((s) => s.trim()).filter(Boolean);
2374
+ return dedupePaths2(raw);
2375
+ }
2376
+
2377
+ // src/commands/pair.ts
2378
+ function register4(program) {
2379
+ program.command("pair").description("Re-pair this machine (when token expired)").action(async () => {
2380
+ try {
2381
+ await runPair();
2382
+ } catch (err) {
2383
+ console.error(`
2384
+ Error: ${err instanceof Error ? err.message : err}`);
2385
+ process.exit(1);
2271
2386
  }
2387
+ });
2388
+ }
2389
+ async function runPair() {
2390
+ const existing = loadConfig();
2391
+ const url = existing?.url;
2392
+ if (!url) {
2393
+ throw new Error(
2394
+ "This machine has not joined a jarvis instance, so there is nothing to re-pair.\n Join one with: jarvis init --url <address>"
2395
+ );
2272
2396
  }
2273
- if (!changed) {
2274
- logger.sys.info("[hooks.install] uninstall: nothing to remove", { configDir: configDir2 });
2275
- return;
2276
- }
2277
- if (writeSettings(settings)) {
2278
- logger.sys.info("[hooks.install] Claude Code hooks uninstalled", {
2279
- configDir: configDir2,
2280
- hooks: removed
2281
- });
2282
- } else {
2283
- logger.sys.warn("[hooks.install] uninstall: settings.json write failed", { configDir: configDir2 });
2284
- }
2397
+ const result2 = await exchangeCode({
2398
+ url,
2399
+ machineId: getMachine().id,
2400
+ ...existing?.envId ? { envId: existing.envId } : {}
2401
+ });
2402
+ saveConfig({
2403
+ ...existing ?? {},
2404
+ hubUrl: result2.hubUrl,
2405
+ url,
2406
+ token: result2.token,
2407
+ ...result2.refreshToken ? { refreshToken: result2.refreshToken } : {},
2408
+ userId: result2.userId,
2409
+ envId: result2.envId,
2410
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
2411
+ });
2412
+ console.log(" paired \u2713");
2285
2413
  }
2286
2414
 
2415
+ // src/commands/unpair.ts
2416
+ import { createInterface as createInterface3 } from "readline";
2417
+
2287
2418
  // src/mcp.ts
2419
+ import { execFile } from "child_process";
2420
+ import { existsSync as existsSync2, readdirSync } from "fs";
2421
+ import os7 from "os";
2422
+ import path11 from "path";
2423
+ import { promisify } from "util";
2288
2424
  var run = promisify(execFile);
2289
2425
  var SERVER_NAME = "jarvis";
2290
2426
  function candidatePaths() {
@@ -2353,7 +2489,7 @@ async function unpair() {
2353
2489
  clearConfig();
2354
2490
  return { attempted: true, serverStatus };
2355
2491
  }
2356
- function register4(program) {
2492
+ function register5(program) {
2357
2493
  program.command("unpair").description("Unregister this env from cloud and wipe local config").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
2358
2494
  try {
2359
2495
  const config2 = loadConfig();
@@ -2408,7 +2544,7 @@ function getRoots() {
2408
2544
  const cfg = readConfig();
2409
2545
  return Array.isArray(cfg.roots) ? cfg.roots : [];
2410
2546
  }
2411
- function register5(program) {
2547
+ function register6(program) {
2412
2548
  const cmd = program.command("roots").description("Manage code-root directories");
2413
2549
  cmd.command("list", { isDefault: true }).description("List current roots").action(() => {
2414
2550
  const roots = getRoots();
@@ -2453,7 +2589,7 @@ function readLocalConfig() {
2453
2589
  return {};
2454
2590
  }
2455
2591
  }
2456
- function register6(program) {
2592
+ function register7(program) {
2457
2593
  program.command("settings").description("Show this env's settings (roots, repos map)").action(async () => {
2458
2594
  try {
2459
2595
  const config2 = loadConfig();
@@ -2496,14 +2632,21 @@ function register6(program) {
2496
2632
  import { execSync as execSync5 } from "child_process";
2497
2633
 
2498
2634
  // src/auth.ts
2499
- function getJarvisUrl() {
2500
- return loadConfig()?.url ?? DEFAULT_URL;
2501
- }
2502
- async function ensurePaired() {
2635
+ function refuseWithoutInstance() {
2503
2636
  const config2 = loadConfig();
2504
- if (config2?.anthropicApiKey || config2?.useSubscription) return true;
2505
- if (config2?.hubUrl && config2.envId && config2.token) return true;
2506
- return runInit({ url: getJarvisUrl() });
2637
+ if (config2?.hubUrl && config2.envId && config2.token) return false;
2638
+ console.error(
2639
+ `This machine has not joined a jarvis instance, so there is nothing for the daemon to
2640
+ connect to. The daemon is what carries sessions between machines and puts your
2641
+ boards on the web \u2014 it is not what makes jarvis work in a repo.
2642
+
2643
+ Working in a repo needs none of this. If you have not set this machine up yet:
2644
+ jarvis init
2645
+
2646
+ To join an instance, name it:
2647
+ jarvis init --url <address>`
2648
+ );
2649
+ return true;
2507
2650
  }
2508
2651
 
2509
2652
  // src/start.ts
@@ -9645,7 +9788,7 @@ import { createRequire as createRequire2 } from "module";
9645
9788
  var _require = createRequire2(import.meta.url);
9646
9789
  var VERSION2 = _require("../package.json").version ?? "0.0.0";
9647
9790
  var IS_DEV = String(_require("../package.json").name ?? "").endsWith("-dev");
9648
- var SHA = "ddc5877";
9791
+ var SHA = "5b34716";
9649
9792
  var BUILT = "2026-09-08";
9650
9793
  var BUILD = SHA ?? "source";
9651
9794
  var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
@@ -10163,15 +10306,9 @@ async function fetchRoots(hubUrl, envId, token) {
10163
10306
  }
10164
10307
 
10165
10308
  // src/commands/start.ts
10166
- function register7(program) {
10309
+ function register8(program) {
10167
10310
  program.command("start").description("Start the local agent (authenticates, cleans up stale processes, starts fresh)").option("-w, --workspace <path>", "Workspace root path").option("--api-key <key>", "Anthropic API key (for local-only use)").option("--no-upstream", "Don't connect to cloud (local-only mode)").option("--foreground", "Run in foreground (don't daemonize)").option("--force", "Re-ask all prompts even when config has them").action(async (opts) => {
10168
- if (!opts.foreground) {
10169
- const ok = await ensurePaired();
10170
- if (!ok) {
10171
- console.error("Setup failed. Cannot start agent.");
10172
- process.exit(1);
10173
- }
10174
- }
10311
+ if (refuseWithoutInstance()) process.exit(1);
10175
10312
  const config2 = loadConfig();
10176
10313
  const workspacePath2 = await resolveWorkspacePath(opts, config2);
10177
10314
  const userId = config2?.userId ?? process.env.JARVIS_USER_ID ?? "local";
@@ -10306,7 +10443,7 @@ function buildUpstreamConfig(req) {
10306
10443
  }
10307
10444
 
10308
10445
  // src/commands/stop.ts
10309
- function register8(program) {
10446
+ function register9(program) {
10310
10447
  program.command("stop").description("Stop the running agent").action(() => {
10311
10448
  uninstallClaudeCodeHooks();
10312
10449
  const service = createServiceManager();
@@ -10333,7 +10470,7 @@ function register8(program) {
10333
10470
  }
10334
10471
 
10335
10472
  // src/commands/restart.ts
10336
- function register9(program) {
10473
+ function register10(program) {
10337
10474
  program.command("restart").description("Restart the agent (alias for start \u2014 start always does a clean restart)").action(async () => {
10338
10475
  await program.parseAsync(["node", "jarvis", "start"]);
10339
10476
  });
@@ -10342,7 +10479,7 @@ function register9(program) {
10342
10479
  // src/commands/logs.ts
10343
10480
  import { spawn as spawn2, execSync as execSync6 } from "child_process";
10344
10481
  import fs16 from "fs";
10345
- function register10(program) {
10482
+ function register11(program) {
10346
10483
  program.command("logs").description("View agent logs").option("-f, --follow", "Follow log output (like tail -f)", true).option("-n, --lines <n>", "Number of lines to show", "50").action((opts) => {
10347
10484
  const logFile = getLogFile();
10348
10485
  if (!fs16.existsSync(logFile)) {
@@ -10415,16 +10552,16 @@ function portOf(url) {
10415
10552
  }
10416
10553
 
10417
10554
  // src/commands/status.ts
10418
- var GREEN = (text) => `\x1B[32m${text}\x1B[0m`;
10419
- var RED = (text) => `\x1B[31m${text}\x1B[0m`;
10420
- function register11(program) {
10555
+ var GREEN2 = (text) => `\x1B[32m${text}\x1B[0m`;
10556
+ var RED2 = (text) => `\x1B[31m${text}\x1B[0m`;
10557
+ function register12(program) {
10421
10558
  program.command("status").description("Show agent configuration and connection status").action(async () => {
10422
10559
  const config2 = loadConfig();
10423
10560
  const health = await getHealth();
10424
10561
  const { daemon } = health;
10425
10562
  console.log(`Jarvis Agent v${BUILD_LABEL}:`);
10426
10563
  console.log(
10427
- ` Status: ${daemon.running ? GREEN("running") : RED("stopped")}${daemon.pid ? ` (PID: ${daemon.pid})` : ""}`
10564
+ ` Status: ${daemon.running ? GREEN2("running") : RED2("stopped")}${daemon.pid ? ` (PID: ${daemon.pid})` : ""}`
10428
10565
  );
10429
10566
  console.log(` Config: ${getConfigPath()}`);
10430
10567
  if (config2) {
@@ -10442,7 +10579,7 @@ function register11(program) {
10442
10579
  const svcStatus = createServiceManager().status();
10443
10580
  if (svcStatus.installed) {
10444
10581
  const label = svcStatus.os ?? "unknown";
10445
- const state2 = svcStatus.running ? GREEN(`installed & running`) : `\x1B[33minstalled, stopped\x1B[0m`;
10582
+ const state2 = svcStatus.running ? GREEN2(`installed & running`) : `\x1B[33minstalled, stopped\x1B[0m`;
10446
10583
  console.log(` Service: ${state2} (${label})`);
10447
10584
  } else {
10448
10585
  console.log(` Service: not installed (run 'jarvis install' for always-on)`);
@@ -10452,13 +10589,13 @@ function register11(program) {
10452
10589
  }
10453
10590
  function endpoint(probed) {
10454
10591
  if (!probed) return null;
10455
- return `${probed.url} \u2014 ${probed.reachable ? GREEN("reachable") : RED("not answering")}`;
10592
+ return `${probed.url} \u2014 ${probed.reachable ? GREEN2("reachable") : RED2("not answering")}`;
10456
10593
  }
10457
10594
 
10458
10595
  // src/commands/install.ts
10459
- function register12(program) {
10596
+ function register13(program) {
10460
10597
  program.command("install").description("Install as OS service for auto-start on login and crash recovery").option("-w, --workspace <path>", "Workspace root path").option("--no-upstream", "Don't connect to cloud").action(async (opts) => {
10461
- await ensurePaired();
10598
+ if (refuseWithoutInstance()) process.exit(1);
10462
10599
  const config2 = loadConfig();
10463
10600
  const workspacePath2 = opts.workspace ?? config2?.workspacePath ?? process.cwd();
10464
10601
  const service = createServiceManager();
@@ -10505,7 +10642,7 @@ function register12(program) {
10505
10642
  import fs17 from "fs";
10506
10643
  import path18 from "path";
10507
10644
  import { createInterface as createInterface4 } from "readline";
10508
- function register13(program) {
10645
+ function register14(program) {
10509
10646
  program.command("uninstall").description("Stop the agent, remove the OS service, and wipe local config + identity").option("--keep-data", "Skip the data-wipe prompt; preserve skills/prompts/cache/settings").option("--purge", "Skip the prompt and wipe ALL local data (skills, prompts, cache, settings)").action(async (opts) => {
10510
10647
  try {
10511
10648
  await runUninstall(opts);
@@ -10567,7 +10704,7 @@ function confirm2(prompt) {
10567
10704
  }
10568
10705
 
10569
10706
  // src/commands/logout.ts
10570
- function register14(program) {
10707
+ function register15(program) {
10571
10708
  program.command("logout").description("Clear saved configuration").action(() => {
10572
10709
  clearConfig();
10573
10710
  console.log("Configuration cleared.");
@@ -10575,12 +10712,17 @@ function register14(program) {
10575
10712
  }
10576
10713
 
10577
10714
  // src/commands/mcp.ts
10715
+ function instanceUrl() {
10716
+ const url = loadConfig()?.url;
10717
+ if (!url) throw new Error("not connected \u2014 run `jarvis connect` first");
10718
+ return url;
10719
+ }
10578
10720
  async function call(path22, method, body) {
10579
10721
  const config2 = loadConfig();
10580
10722
  if (!config2?.token) {
10581
10723
  throw new Error("not connected \u2014 run `jarvis connect` first");
10582
10724
  }
10583
- const resp = await fetch(new URL(path22, config2.url ?? getJarvisUrl()), {
10725
+ const resp = await fetch(new URL(path22, instanceUrl()), {
10584
10726
  method,
10585
10727
  headers: {
10586
10728
  authorization: `Bearer ${config2.token}`,
@@ -10593,7 +10735,7 @@ async function call(path22, method, body) {
10593
10735
  }
10594
10736
  return await resp.json();
10595
10737
  }
10596
- function register15(program) {
10738
+ function register16(program) {
10597
10739
  const mcp = program.command("mcp").description("Connect an agent to this workspace's board");
10598
10740
  mcp.command("token").description("Mint a personal token for the Jarvis MCP server").option("--name <name>", "Which machine this token is for, e.g. 'laptop'").option("--scopes <list>", "read | write | read,write", "read,write").option("--show", "Show the current token's details instead of minting").option("--revoke", "Revoke the current token").action(async (opts) => {
10599
10741
  try {
@@ -10621,7 +10763,7 @@ function register15(program) {
10621
10763
  console.log(token);
10622
10764
  console.log("");
10623
10765
  console.log("Shown once. Paste it into your client's config now:");
10624
- console.log(` ${new URL("/api/mcp", loadConfig()?.url ?? getJarvisUrl())}`);
10766
+ console.log(` ${new URL("/api/mcp", instanceUrl())}`);
10625
10767
  console.log(" header: Authorization: Bearer <the token above>");
10626
10768
  if (!opts.name) console.log("");
10627
10769
  if (!opts.name) console.log("Tip: --name laptop makes it obvious later which is which.");
@@ -10647,7 +10789,7 @@ function register15(program) {
10647
10789
  }
10648
10790
 
10649
10791
  // src/commands/model.ts
10650
- function register16(program) {
10792
+ function register17(program) {
10651
10793
  const model = program.command("model").description("Model access for the map's semantic search, on this machine");
10652
10794
  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) => {
10653
10795
  const config2 = loadConfig();
@@ -10959,7 +11101,7 @@ function loadEnvFile(path22) {
10959
11101
  }
10960
11102
 
10961
11103
  // src/commands/build.ts
10962
- function register17(program) {
11104
+ function register18(program) {
10963
11105
  const build2 = program.command("build").description("Build an artifact from this repo");
10964
11106
  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) => {
10965
11107
  const argv = [
@@ -11019,7 +11161,7 @@ function isRedirected(req) {
11019
11161
  }
11020
11162
 
11021
11163
  // src/commands/dev.ts
11022
- function register18(program) {
11164
+ function register19(program) {
11023
11165
  const dev = program.command("dev").description("Run a development build in a repo, without replacing the installed jarvis");
11024
11166
  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) => {
11025
11167
  const current = readDevRedirect();
@@ -11733,7 +11875,7 @@ async function brief2(repo) {
11733
11875
  }
11734
11876
 
11735
11877
  // src/commands/serve.ts
11736
- function register19(program) {
11878
+ function register20(program) {
11737
11879
  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) => {
11738
11880
  const repo = resolveRepo(opts.repo);
11739
11881
  const problem = harnessProblem();
@@ -11746,108 +11888,6 @@ function register19(program) {
11746
11888
  });
11747
11889
  }
11748
11890
 
11749
- // src/commands/hooks.ts
11750
- var GREEN2 = (text) => `\x1B[32m${text}\x1B[0m`;
11751
- var RED2 = (text) => `\x1B[31m${text}\x1B[0m`;
11752
- var YELLOW = (text) => `\x1B[33m${text}\x1B[0m`;
11753
- function quietInstallerNarration() {
11754
- const drop = () => {
11755
- };
11756
- const provider2 = {
11757
- debug: drop,
11758
- info: drop,
11759
- warn: (_ctx, message) => console.warn(` ${message}`),
11760
- error: (_ctx, message) => console.error(` ${message}`),
11761
- audit: drop
11762
- };
11763
- logger.init({ provider: provider2 });
11764
- }
11765
- function isOurs(hook) {
11766
- return hook.command === hook.bin;
11767
- }
11768
- function isElsewhere(hook) {
11769
- return hook.command !== void 0 && hook.command !== hook.bin;
11770
- }
11771
- function report(state2) {
11772
- console.log(` Settings: ${claudeSettingsFile}`);
11773
- for (const hook of state2) {
11774
- const mark = isOurs(hook) ? GREEN2("installed") : isElsewhere(hook) ? YELLOW("another install") : RED2("missing");
11775
- const gone = hook.exists ? "" : RED2(" \u2014 the shim it names is not there");
11776
- console.log(` ${hook.type.padEnd(17)} ${mark}${gone}`);
11777
- console.log(` ${"".padEnd(17)} ${hook.command ?? hook.bin}`);
11778
- }
11779
- if (state2.some(isElsewhere)) {
11780
- console.log(
11781
- `
11782
- Claude Code is running hooks from a different copy of the CLI than this one.
11783
- Both work, and the one you are reading is not the one running. Install from
11784
- whichever copy you mean to keep, or remove the other.`
11785
- );
11786
- }
11787
- }
11788
- function refuseNamedConfigDir() {
11789
- if (ownsClaudeCode()) return false;
11790
- console.error(
11791
- `This config dir does not own this machine's Claude Code, so nothing was changed.
11792
- Config dir: ${getConfigDir()}
11793
- There is one Claude Code per machine and the default dir owns it. Run this
11794
- without JARVIS_CONFIG_DIR set.`
11795
- );
11796
- return true;
11797
- }
11798
- function register20(program) {
11799
- const hooks = program.command("hooks").description("Wire this machine's Claude Code to jarvis \u2014 no pairing, no daemon");
11800
- hooks.command("install").description("Install the Claude Code hooks (the session block and the rules on edit)").action(() => {
11801
- if (refuseNamedConfigDir()) process.exit(1);
11802
- quietInstallerNarration();
11803
- installClaudeCodeHooks();
11804
- const state2 = claudeCodeHooksState();
11805
- const installed = state2.filter(isOurs).length;
11806
- if (installed === 0) {
11807
- console.error("No hooks were installed.");
11808
- report(state2);
11809
- console.error(
11810
- `
11811
- Every shim above is missing, which means this is not a complete install.
11812
- Reinstall the CLI: npm i -g @appchy/jarvis`
11813
- );
11814
- process.exit(1);
11815
- }
11816
- console.log(`Claude Code hooks installed (${installed} of ${state2.length}):`);
11817
- report(state2);
11818
- console.log(`
11819
- Open Claude Code in a repo that has a work/ tree to see them.`);
11820
- });
11821
- hooks.command("status").description("What Claude Code will actually run").action(() => {
11822
- const state2 = claudeCodeHooksState();
11823
- const installed = state2.filter(isOurs).length;
11824
- console.log(
11825
- installed === state2.length ? `All ${state2.length} Claude Code hooks are installed:` : `${installed} of ${state2.length} Claude Code hooks are installed:`
11826
- );
11827
- report(state2);
11828
- if (!ownsClaudeCode()) {
11829
- console.log(
11830
- `
11831
- Note: this config dir (${getConfigDir()}) does not own this machine's
11832
- Claude Code, so it neither installs these nor removes them.`
11833
- );
11834
- }
11835
- });
11836
- hooks.command("uninstall").description("Remove the Claude Code hooks this machine installed").action(() => {
11837
- if (refuseNamedConfigDir()) process.exit(1);
11838
- quietInstallerNarration();
11839
- uninstallClaudeCodeHooks();
11840
- const left = claudeCodeHooksState().filter(isOurs);
11841
- if (left.length === 0) {
11842
- console.log("Claude Code hooks removed.");
11843
- return;
11844
- }
11845
- console.error(`${left.length} hook(s) could not be removed:`);
11846
- report(left);
11847
- process.exit(1);
11848
- });
11849
- }
11850
-
11851
11891
  // src/commands/work.ts
11852
11892
  import { spawnSync as spawnSync3 } from "child_process";
11853
11893
  function register21(program) {
@@ -11873,11 +11913,10 @@ function register21(program) {
11873
11913
  // src/cli.ts
11874
11914
  function createCli() {
11875
11915
  const program = new Command().enablePositionalOptions().name("jarvis").description("Jarvis local agent \u2014 watches Claude Code sessions on your machine").version(BUILD_LABEL);
11876
- register2(program);
11877
11916
  register3(program);
11878
11917
  register4(program);
11879
- register(program);
11880
11918
  register5(program);
11919
+ register(program);
11881
11920
  register6(program);
11882
11921
  register7(program);
11883
11922
  register8(program);
@@ -11893,6 +11932,7 @@ function createCli() {
11893
11932
  register18(program);
11894
11933
  register19(program);
11895
11934
  register20(program);
11935
+ register2(program);
11896
11936
  register21(program);
11897
11937
  return program;
11898
11938
  }