gitwarren 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/lib/gitwarren.cjs +183 -108
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -61,6 +61,18 @@ The MCP server reads the same SQLite file the browser view does, so an agent
61
61
  can open and comment on reviews whether or not GitWarren is being served. What
62
62
  serving adds is that the links an agent hands you have something to open.
63
63
 
64
+ On a machine where nothing has been installed, the same server starts by name:
65
+ `npx gitwarren mcp`. That is the command a Claude Code plugin or an MCP registry
66
+ entry names, and it is what makes either work before you have decided to keep
67
+ GitWarren. It writes no launcher and asks for no login item; it reads the same
68
+ database, so the reviews are there when you do.
69
+
70
+ `npx gitwarren mcp --serve` also serves the review page, on loopback and for as
71
+ long as the agent keeps the server running, so the links the agent hands out
72
+ open even on a machine with nothing else installed. If GitWarren is already
73
+ running, as the app or as `gitwarren serve`, it serves nothing and the links
74
+ open there instead.
75
+
64
76
  ## Where your data is
65
77
 
66
78
  One SQLite database in the usual place for your platform —
package/lib/gitwarren.cjs CHANGED
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
- const Client = require("better-sqlite3");
3
- const crypto$1 = require("node:crypto");
2
+ const node_module = require("node:module");
4
3
  const fs = require("node:fs");
5
- const node_os = require("node:os");
6
4
  const node_path = require("node:path");
5
+ const node_os = require("node:os");
6
+ const Client = require("better-sqlite3");
7
+ const crypto$1 = require("node:crypto");
7
8
  const node_child_process = require("node:child_process");
8
9
  const node_stream = require("node:stream");
9
10
  const require$$0$3 = require("events");
@@ -23,6 +24,99 @@ const node_util = require("node:util");
23
24
  const node_url = require("node:url");
24
25
  const node_http = require("node:http");
25
26
  var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
27
+ const INSTANCE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
28
+ function isInstanceId(value) {
29
+ return INSTANCE_ID_PATTERN.test(value);
30
+ }
31
+ const APP_DIR_NAME = "GitWarren";
32
+ const DATABASE_FILE_NAME = "gitwarren.db";
33
+ const INSTANCE_FILE_NAME = "instance-id";
34
+ const DATA_DIR_ENV_VAR = "GITWARREN_DATA_DIR";
35
+ function getDataDirectory() {
36
+ const override = process.env[DATA_DIR_ENV_VAR]?.trim();
37
+ if (override) return override;
38
+ switch (process.platform) {
39
+ case "win32":
40
+ return node_path.join(process.env.APPDATA ?? node_path.join(node_os.homedir(), "AppData", "Roaming"), APP_DIR_NAME);
41
+ case "darwin":
42
+ return node_path.join(node_os.homedir(), "Library", "Application Support", APP_DIR_NAME);
43
+ default:
44
+ return node_path.join(process.env.XDG_CONFIG_HOME ?? node_path.join(node_os.homedir(), ".config"), APP_DIR_NAME);
45
+ }
46
+ }
47
+ function getDatabasePath() {
48
+ return node_path.join(getDataDirectory(), DATABASE_FILE_NAME);
49
+ }
50
+ function getInstanceIdPath() {
51
+ return node_path.join(getDataDirectory(), INSTANCE_FILE_NAME);
52
+ }
53
+ function ensureDataDirectory() {
54
+ const dir = getDataDirectory();
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ return dir;
57
+ }
58
+ const DAEMON_CACHE_DIR_NAME = "daemon-cache";
59
+ function getDaemonCacheDirectory() {
60
+ return node_path.join(getDataDirectory(), DAEMON_CACHE_DIR_NAME);
61
+ }
62
+ const RUNTIME_FILE_NAME = "daemon-runtime.json";
63
+ const LEGACY_FILE_NAME = "gui-runtime.json";
64
+ function getDaemonRuntimePath() {
65
+ return node_path.join(getDataDirectory(), RUNTIME_FILE_NAME);
66
+ }
67
+ function writeDaemonRuntime(runtime) {
68
+ try {
69
+ ensureDataDirectory();
70
+ const merged = { ...runtime, webRoot: runtime.webRoot ?? exposure };
71
+ published = merged;
72
+ fs.writeFileSync(getDaemonRuntimePath(), JSON.stringify(merged), "utf8");
73
+ fs.rmSync(node_path.join(getDataDirectory(), LEGACY_FILE_NAME), { force: true });
74
+ } catch (error2) {
75
+ console.error("[runtime] could not publish the runtime file", error2);
76
+ }
77
+ }
78
+ function clearDaemonRuntime() {
79
+ published = null;
80
+ exposure = null;
81
+ try {
82
+ fs.rmSync(getDaemonRuntimePath(), { force: true });
83
+ } catch (error2) {
84
+ console.error("[runtime] could not remove the runtime file", error2);
85
+ }
86
+ }
87
+ let published = null;
88
+ let exposure = null;
89
+ function writeDaemonExposure(webRoot) {
90
+ exposure = webRoot;
91
+ if (!published) return;
92
+ if ((published.webRoot ?? null) === webRoot) return;
93
+ writeDaemonRuntime({ ...published, webRoot });
94
+ }
95
+ function isAlive(pid) {
96
+ try {
97
+ process.kill(pid, 0);
98
+ return true;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+ function readLiveDaemonRuntime() {
104
+ let parsed;
105
+ try {
106
+ parsed = JSON.parse(fs.readFileSync(getDaemonRuntimePath(), "utf8"));
107
+ } catch {
108
+ return null;
109
+ }
110
+ if (typeof parsed !== "object" || parsed === null) return null;
111
+ const { instanceId, pid, linkPort, owner, webRoot } = parsed;
112
+ if (typeof instanceId !== "string" || !isInstanceId(instanceId)) return null;
113
+ if (!Number.isInteger(pid) || pid === void 0 || pid <= 0) return null;
114
+ if (owner !== "gui" && owner !== "daemon") return null;
115
+ const port = linkPort === null ? null : Number.isInteger(linkPort) && linkPort !== void 0 && linkPort > 0 && linkPort <= 65535 ? linkPort : void 0;
116
+ if (port === void 0) return null;
117
+ const root = typeof webRoot === "string" ? webRoot : null;
118
+ return isAlive(pid) ? { instanceId, pid, linkPort: port, owner, webRoot: root } : null;
119
+ }
26
120
  const entityKind = /* @__PURE__ */ Symbol.for("drizzle:entityKind");
27
121
  function is(value, type) {
28
122
  if (!value || typeof value !== "object") {
@@ -4871,37 +4965,6 @@ function migrate(db, config2) {
4871
4965
  const migrations = readMigrationFiles(config2);
4872
4966
  db.dialect.migrate(migrations, db.session, config2);
4873
4967
  }
4874
- const APP_DIR_NAME = "GitWarren";
4875
- const DATABASE_FILE_NAME = "gitwarren.db";
4876
- const INSTANCE_FILE_NAME = "instance-id";
4877
- const DATA_DIR_ENV_VAR = "GITWARREN_DATA_DIR";
4878
- function getDataDirectory() {
4879
- const override = process.env[DATA_DIR_ENV_VAR]?.trim();
4880
- if (override) return override;
4881
- switch (process.platform) {
4882
- case "win32":
4883
- return node_path.join(process.env.APPDATA ?? node_path.join(node_os.homedir(), "AppData", "Roaming"), APP_DIR_NAME);
4884
- case "darwin":
4885
- return node_path.join(node_os.homedir(), "Library", "Application Support", APP_DIR_NAME);
4886
- default:
4887
- return node_path.join(process.env.XDG_CONFIG_HOME ?? node_path.join(node_os.homedir(), ".config"), APP_DIR_NAME);
4888
- }
4889
- }
4890
- function getDatabasePath() {
4891
- return node_path.join(getDataDirectory(), DATABASE_FILE_NAME);
4892
- }
4893
- function getInstanceIdPath() {
4894
- return node_path.join(getDataDirectory(), INSTANCE_FILE_NAME);
4895
- }
4896
- function ensureDataDirectory() {
4897
- const dir = getDataDirectory();
4898
- fs.mkdirSync(dir, { recursive: true });
4899
- return dir;
4900
- }
4901
- const DAEMON_CACHE_DIR_NAME = "daemon-cache";
4902
- function getDaemonCacheDirectory() {
4903
- return node_path.join(getDataDirectory(), DAEMON_CACHE_DIR_NAME);
4904
- }
4905
4968
  const MIGRATIONS_DIR_ENV_VAR = "GITWARREN_MIGRATIONS_DIR";
4906
4969
  function isMigrationsFolder(candidate) {
4907
4970
  return fs.existsSync(node_path.join(candidate, "meta", "_journal.json"));
@@ -5295,10 +5358,6 @@ const schema = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
5295
5358
  reviewedFiles,
5296
5359
  reviews
5297
5360
  }, Symbol.toStringTag, { value: "Module" }));
5298
- const INSTANCE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
5299
- function isInstanceId(value) {
5300
- return INSTANCE_ID_PATTERN.test(value);
5301
- }
5302
5361
  let cached$2 = null;
5303
5362
  function readExisting() {
5304
5363
  try {
@@ -10242,7 +10301,7 @@ function cmdQuote(value) {
10242
10301
  if (value.includes('"')) throw new Error(`a Windows path cannot contain a quote: ${value}`);
10243
10302
  return `"${value}"`;
10244
10303
  }
10245
- const APP_VERSION = "0.1.10";
10304
+ const APP_VERSION = "0.1.11";
10246
10305
  const REMOTE_ROOT = "$HOME/.gitwarren";
10247
10306
  const runOnHost = (route2, options) => {
10248
10307
  switch (route2.kind) {
@@ -10498,64 +10557,6 @@ async function unserveTailnet(port) {
10498
10557
  await tailscale(["serve", "--http", String(port), "off"], SERVE_TIMEOUT_MS);
10499
10558
  return await tailnetServeOrigin(port) !== null;
10500
10559
  }
10501
- const RUNTIME_FILE_NAME = "daemon-runtime.json";
10502
- const LEGACY_FILE_NAME = "gui-runtime.json";
10503
- function getDaemonRuntimePath() {
10504
- return node_path.join(getDataDirectory(), RUNTIME_FILE_NAME);
10505
- }
10506
- function writeDaemonRuntime(runtime) {
10507
- try {
10508
- ensureDataDirectory();
10509
- const merged = { ...runtime, webRoot: runtime.webRoot ?? exposure };
10510
- published = merged;
10511
- fs.writeFileSync(getDaemonRuntimePath(), JSON.stringify(merged), "utf8");
10512
- fs.rmSync(node_path.join(getDataDirectory(), LEGACY_FILE_NAME), { force: true });
10513
- } catch (error2) {
10514
- console.error("[runtime] could not publish the runtime file", error2);
10515
- }
10516
- }
10517
- function clearDaemonRuntime() {
10518
- published = null;
10519
- exposure = null;
10520
- try {
10521
- fs.rmSync(getDaemonRuntimePath(), { force: true });
10522
- } catch (error2) {
10523
- console.error("[runtime] could not remove the runtime file", error2);
10524
- }
10525
- }
10526
- let published = null;
10527
- let exposure = null;
10528
- function writeDaemonExposure(webRoot) {
10529
- exposure = webRoot;
10530
- if (!published) return;
10531
- if ((published.webRoot ?? null) === webRoot) return;
10532
- writeDaemonRuntime({ ...published, webRoot });
10533
- }
10534
- function isAlive(pid) {
10535
- try {
10536
- process.kill(pid, 0);
10537
- return true;
10538
- } catch {
10539
- return false;
10540
- }
10541
- }
10542
- function readLiveDaemonRuntime() {
10543
- let parsed;
10544
- try {
10545
- parsed = JSON.parse(fs.readFileSync(getDaemonRuntimePath(), "utf8"));
10546
- } catch {
10547
- return null;
10548
- }
10549
- if (typeof parsed !== "object" || parsed === null) return null;
10550
- const { instanceId, pid, linkPort, owner, webRoot } = parsed;
10551
- if (typeof instanceId !== "string" || !isInstanceId(instanceId)) return null;
10552
- if (!Number.isInteger(pid) || pid === void 0 || pid <= 0) return null;
10553
- if (owner !== "gui" && owner !== "daemon") return null;
10554
- const port = linkPort === null ? null : Number.isInteger(linkPort) && linkPort !== void 0 && linkPort > 0 && linkPort <= 65535 ? linkPort : void 0;
10555
- if (port === void 0) return null;
10556
- const root = typeof webRoot === "string" ? webRoot : null;
10557
- return isAlive(pid) ? { instanceId, pid, linkPort: port, owner, webRoot: root } : null;
10558
- }
10559
10560
  const UNAVAILABLE = {
10560
10561
  available: false,
10561
10562
  dnsName: null,
@@ -26868,7 +26869,7 @@ Connection: close\r
26868
26869
  }
26869
26870
  };
26870
26871
  }
26871
- const VERSION$1 = "0.1.10";
26872
+ const VERSION$1 = "0.1.11";
26872
26873
  function isWebBuild(directory) {
26873
26874
  return fs.existsSync(node_path.join(directory, "index.html")) && !fs.existsSync(node_path.join(directory, "main.ts"));
26874
26875
  }
@@ -26971,6 +26972,11 @@ function runListen(hooks = {}) {
26971
26972
  owner: "daemon"
26972
26973
  });
26973
26974
  const url = `http://${LINK_SERVER_HOST}:${LINK_SERVER_PORT}/?${TOKEN_PARAM}=${token}`;
26975
+ if (hooks.brief) {
26976
+ console.error(`[gitwarren] serving the review page at ${url}`);
26977
+ hooks.onListening?.(url);
26978
+ return;
26979
+ }
26974
26980
  console.error(
26975
26981
  `[gitwarren] GitWarren is running at
26976
26982
 
@@ -27087,17 +27093,21 @@ function findMcpServer(script) {
27087
27093
  }
27088
27094
  return null;
27089
27095
  }
27090
- function describeSelf() {
27096
+ function resolveScript() {
27091
27097
  const argv1 = process.argv[1];
27092
- let script = null;
27093
- if (argv1 !== void 0) {
27094
- try {
27095
- const resolved2 = fs.realpathSync(argv1);
27096
- script = /\.[cm]?ts$/i.test(resolved2) ? null : resolved2;
27097
- } catch {
27098
- script = null;
27099
- }
27098
+ if (argv1 === void 0) return null;
27099
+ try {
27100
+ const resolved2 = fs.realpathSync(argv1);
27101
+ return /\.[cm]?ts$/i.test(resolved2) ? null : resolved2;
27102
+ } catch {
27103
+ return null;
27100
27104
  }
27105
+ }
27106
+ function locateMcpServer() {
27107
+ return findMcpServer(resolveScript());
27108
+ }
27109
+ function describeSelf() {
27110
+ const script = resolveScript();
27101
27111
  return { node: fs.realpathSync(process.execPath), script, mcpServer: findMcpServer(script), env: relaunchEnv() };
27102
27112
  }
27103
27113
  const BANNER = (what) => [
@@ -27609,7 +27619,7 @@ function runService(argv) {
27609
27619
  return true;
27610
27620
  }
27611
27621
  }
27612
- const VERSION = "0.1.10";
27622
+ const VERSION = "0.1.11";
27613
27623
  const USAGE = `gitwarren - code review for your own git repositories, in a browser tab
27614
27624
 
27615
27625
  Run it now
@@ -27626,6 +27636,8 @@ Keep it running
27626
27636
  Let a coding agent in
27627
27637
  gitwarren agent-setup print the one sentence to give an agent so it can
27628
27638
  reach this GitWarren over MCP
27639
+ gitwarren mcp [--serve] run the MCP server itself, on stdin/stdout - what
27640
+ \`npx gitwarren mcp\` in an agent's config starts
27629
27641
 
27630
27642
  gitwarren serve --stdio answer GitWarren's protocol on stdin/stdout (this
27631
27643
  is what another machine's GitWarren spawns)
@@ -27652,6 +27664,22 @@ a URL that carries this launch's token. Ctrl-C stops it.
27652
27664
  Serving also writes ~/.gitwarren/bin/gitwarren-mcp, the command a coding agent
27653
27665
  starts the MCP server with - see \`gitwarren agent-setup\`.
27654
27666
  `;
27667
+ const MCP_USAGE = `gitwarren mcp [--serve]
27668
+
27669
+ Runs GitWarren's MCP server, speaking MCP over stdin and stdout. This is for an
27670
+ agent to run, not a person: it is the same server ~/.gitwarren/bin/gitwarren-mcp
27671
+ starts, reachable by name so that an agent's config can say \`npx gitwarren mcp\`
27672
+ on a machine where GitWarren was never installed.
27673
+
27674
+ It reads the same SQLite file the app and \`gitwarren serve\` do, so reviews an
27675
+ agent makes this way are there the moment a person opens GitWarren to look.
27676
+
27677
+ --serve also serve the review page on 127.0.0.1 for as long as the agent
27678
+ keeps this running, unless a GitWarren is already running on this
27679
+ machine - then links open there and this serves nothing. This is
27680
+ what a plugin asks for, so the links its agent hands out open on a
27681
+ machine with nothing else installed.
27682
+ `;
27655
27683
  function afterListening(url, open2) {
27656
27684
  const launchers = ensureLaunchers();
27657
27685
  if (launchers.refused) {
@@ -27667,6 +27695,51 @@ function afterListening(url, open2) {
27667
27695
  openInBrowser(url, (message) => console.error(`[gitwarren] ${message}. The URL is above.`));
27668
27696
  }
27669
27697
  }
27698
+ function runMcp(argv) {
27699
+ if (argv.includes("--help") || argv.includes("-h")) {
27700
+ console.log(MCP_USAGE);
27701
+ return true;
27702
+ }
27703
+ if (argv.some((argument) => argument !== "--serve")) {
27704
+ console.error(MCP_USAGE);
27705
+ return false;
27706
+ }
27707
+ const server = locateMcpServer();
27708
+ if (!server) {
27709
+ console.error(
27710
+ "[gitwarren] this install has no MCP server bundle next to it. From a checkout, run `npm run build:mcp` first, or use `npm run mcp:dev`."
27711
+ );
27712
+ return false;
27713
+ }
27714
+ process.removeAllListeners("SIGINT");
27715
+ process.removeAllListeners("SIGTERM");
27716
+ if (argv.includes("--serve")) serveBesideMcp();
27717
+ node_module.createRequire(server)(server);
27718
+ return true;
27719
+ }
27720
+ function serveBesideMcp() {
27721
+ const owner = readLiveDaemonRuntime();
27722
+ if (owner) {
27723
+ console.error(
27724
+ `[gitwarren] GitWarren is already running on this machine as ${owner.owner === "gui" ? "the desktop app" : "a server"}; links will open there.`
27725
+ );
27726
+ return;
27727
+ }
27728
+ if (!runDaemon(["--listen"], { brief: true })) {
27729
+ process.exitCode = void 0;
27730
+ console.error(
27731
+ "[gitwarren] the review page could not be served, so links will not open until a GitWarren is started on this machine. The MCP server is running regardless."
27732
+ );
27733
+ return;
27734
+ }
27735
+ const release = () => shutdownListen();
27736
+ process.on("SIGINT", release);
27737
+ process.on("SIGTERM", release);
27738
+ process.stdin.once("end", () => {
27739
+ shutdownListen();
27740
+ process.exit(0);
27741
+ });
27742
+ }
27670
27743
  function runCli(argv) {
27671
27744
  const [command, ...rest] = argv;
27672
27745
  switch (command) {
@@ -27684,6 +27757,8 @@ function runCli(argv) {
27684
27757
  return runService(rest);
27685
27758
  case "agent-setup":
27686
27759
  return runAgentSetup(rest);
27760
+ case "mcp":
27761
+ return runMcp(rest);
27687
27762
  case "--version":
27688
27763
  case "-v":
27689
27764
  console.log(VERSION);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitwarren",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Code review for your own machines and your own agents, served on loopback. No Electron, no account.",
5
5
  "keywords": ["code-review", "git", "diff", "mcp", "local-first", "ssh", "wsl", "tailscale", "self-hosted"],
6
6
  "homepage": "https://github.com/klarluft/gitwarren-app",