kandev 0.49.0 → 0.51.0

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.
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MANAGED_MARKER_TEXT = void 0;
7
+ exports.looksLikeManagedUnit = looksLikeManagedUnit;
8
+ exports.renderSystemdUnit = renderSystemdUnit;
9
+ exports.renderLaunchdPlist = renderLaunchdPlist;
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ // User-mode PATH includes ~/.local/bin so user-installed agent CLIs (npm user
13
+ // prefix, pipx, fnm, etc.) are discoverable.
14
+ const SYSTEMD_SYSTEM_PATH = "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin";
15
+ const SYSTEMD_USER_PATH = `%h/.local/bin:${SYSTEMD_SYSTEM_PATH}`;
16
+ const LAUNCHD_SYSTEM_PATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
17
+ const launchdUserPath = () => `${node_os_1.default.homedir()}/.local/bin:${LAUNCHD_SYSTEM_PATH}`;
18
+ // Prepend the launcher node's bin dir so `npm`/`npx` resolve under per-user
19
+ // node managers (fnm, nvm, asdf, volta, mise), where node lives in a versioned
20
+ // subdirectory not covered by the system PATH. ExecStart already points at this
21
+ // node, so its parent dir is exactly where the matching npm/npx live — without
22
+ // this, npx-based ACP agents (claude, codex, opencode) fail to spawn.
23
+ //
24
+ // The isAbsolute guard exists because `path.dirname` on POSIX returns `'.'`
25
+ // for a path with no `/` separator (e.g. a Windows-style `C:\…\node.exe`
26
+ // passed through). Prepending `.` would put CWD ahead of system dirs in the
27
+ // daemon's PATH — a privilege-escalation footgun. Production callers always
28
+ // pass `process.execPath` (absolute on Linux/macOS), so the guard only kicks
29
+ // in for non-POSIX or relative inputs from future callers / tests.
30
+ function pathWithNodeBinDir(basePath, nodePath) {
31
+ const nodeBinDir = node_path_1.default.dirname(nodePath);
32
+ if (!node_path_1.default.isAbsolute(nodeBinDir))
33
+ return basePath;
34
+ const parts = basePath.split(":");
35
+ if (parts.includes(nodeBinDir))
36
+ return basePath;
37
+ return `${nodeBinDir}:${basePath}`;
38
+ }
39
+ /**
40
+ * Marker substring baked into every unit/plist kandev writes. Used to safely
41
+ * detect "this file is ours, overwriting is fine" vs "user has put something
42
+ * else here, warn loudly before replacing".
43
+ */
44
+ exports.MANAGED_MARKER_TEXT = "managed by kandev";
45
+ const SYSTEMD_MARKER = `# ${exports.MANAGED_MARKER_TEXT} — regenerated by \`kandev service install\``;
46
+ const PLIST_MARKER = `<!-- ${exports.MANAGED_MARKER_TEXT} — regenerated by \`kandev service install\` -->`;
47
+ function looksLikeManagedUnit(content) {
48
+ return content.includes(exports.MANAGED_MARKER_TEXT);
49
+ }
50
+ /**
51
+ * Render a systemd unit file for kandev.
52
+ *
53
+ * The unit hard-codes absolute paths so it works without a user PATH. We pass
54
+ * `--headless` so the daemon doesn't try to open a browser. KANDEV_BUNDLE_DIR /
55
+ * KANDEV_VERSION are surfaced only when present (Homebrew installs only) so
56
+ * `npm i -g` installs don't get spurious env vars.
57
+ */
58
+ function renderSystemdUnit(input) {
59
+ const basePath = input.mode === "system" ? SYSTEMD_SYSTEM_PATH : SYSTEMD_USER_PATH;
60
+ const env = [
61
+ envLine("KANDEV_HOME_DIR", input.homeDir),
62
+ envLine("KANDEV_LOG_LEVEL", "info"),
63
+ envLine("PATH", pathWithNodeBinDir(basePath, input.launcher.nodePath)),
64
+ ];
65
+ if (input.port !== undefined) {
66
+ env.push(envLine("KANDEV_SERVER_PORT", String(input.port)));
67
+ }
68
+ if (input.launcher.bundleDir) {
69
+ env.push(envLine("KANDEV_BUNDLE_DIR", input.launcher.bundleDir));
70
+ }
71
+ if (input.launcher.version) {
72
+ env.push(envLine("KANDEV_VERSION", input.launcher.version));
73
+ }
74
+ const wantedBy = input.mode === "system" ? "multi-user.target" : "default.target";
75
+ const userLine = input.mode === "system" && input.systemUser ? `User=${input.systemUser}\n` : "";
76
+ const exec = `${quoteForUnit(input.launcher.nodePath)} ${quoteForUnit(input.launcher.cliEntry)} --headless`;
77
+ return `${SYSTEMD_MARKER}
78
+ [Unit]
79
+ Description=Kandev autonomous agent platform
80
+ Documentation=https://github.com/kdlbs/kandev
81
+ After=network-online.target
82
+ Wants=network-online.target
83
+
84
+ [Service]
85
+ Type=simple
86
+ ExecStart=${exec}
87
+ ${userLine}${env.join("\n")}
88
+ Restart=on-failure
89
+ RestartSec=5s
90
+ KillMode=mixed
91
+ TimeoutStopSec=30s
92
+
93
+ [Install]
94
+ WantedBy=${wantedBy}
95
+ `;
96
+ }
97
+ /**
98
+ * Render a launchd plist for kandev.
99
+ *
100
+ * launchd has no equivalent to systemd's enable-linger: a user agent only runs
101
+ * while the user is logged in. For a Mac-as-server scenario, use --system to
102
+ * get a LaunchDaemon that runs at boot regardless of login.
103
+ */
104
+ function renderLaunchdPlist(input) {
105
+ const basePath = input.mode === "system" ? LAUNCHD_SYSTEM_PATH : launchdUserPath();
106
+ const envEntries = [
107
+ ["KANDEV_HOME_DIR", input.homeDir],
108
+ ["KANDEV_LOG_LEVEL", "info"],
109
+ ["PATH", pathWithNodeBinDir(basePath, input.launcher.nodePath)],
110
+ ];
111
+ if (input.port !== undefined) {
112
+ envEntries.push(["KANDEV_SERVER_PORT", String(input.port)]);
113
+ }
114
+ if (input.launcher.bundleDir) {
115
+ envEntries.push(["KANDEV_BUNDLE_DIR", input.launcher.bundleDir]);
116
+ }
117
+ if (input.launcher.version) {
118
+ envEntries.push(["KANDEV_VERSION", input.launcher.version]);
119
+ }
120
+ const envXml = envEntries
121
+ .map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
122
+ .join("\n");
123
+ const args = [input.launcher.nodePath, input.launcher.cliEntry, "--headless"];
124
+ const argsXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
125
+ // For system-mode LaunchDaemons, run as a specific user instead of root.
126
+ // For user agents this directive is omitted — the agent already runs as
127
+ // the loading user.
128
+ const userBlock = input.mode === "system" && input.systemUser
129
+ ? ` <key>UserName</key>\n <string>${escapeXml(input.systemUser)}</string>\n`
130
+ : "";
131
+ return `<?xml version="1.0" encoding="UTF-8"?>
132
+ ${PLIST_MARKER}
133
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
134
+ <plist version="1.0">
135
+ <dict>
136
+ <key>Label</key>
137
+ <string>com.kdlbs.kandev</string>
138
+ <key>ProgramArguments</key>
139
+ <array>
140
+ ${argsXml}
141
+ </array>
142
+ <key>EnvironmentVariables</key>
143
+ <dict>
144
+ ${envXml}
145
+ </dict>
146
+ ${userBlock} <key>RunAtLoad</key>
147
+ <true/>
148
+ <key>KeepAlive</key>
149
+ <true/>
150
+ <key>StandardOutPath</key>
151
+ <string>${escapeXml(`${input.logDir}/service.out`)}</string>
152
+ <key>StandardErrorPath</key>
153
+ <string>${escapeXml(`${input.logDir}/service.err`)}</string>
154
+ <key>WorkingDirectory</key>
155
+ <string>${escapeXml(input.homeDir)}</string>
156
+ </dict>
157
+ </plist>
158
+ `;
159
+ }
160
+ function escapeXml(value) {
161
+ return value
162
+ .replace(/&/g, "&amp;")
163
+ .replace(/</g, "&lt;")
164
+ .replace(/>/g, "&gt;")
165
+ .replace(/"/g, "&quot;")
166
+ .replace(/'/g, "&apos;");
167
+ }
168
+ // systemd unit "Environment=" lines can contain spaces by wrapping in double quotes.
169
+ // ExecStart needs special handling for spaces too — paths with spaces must be
170
+ // double-quoted per systemd.unit(5). Escape backslashes before double-quotes so
171
+ // a literal `\` in a path is preserved instead of merging with the following
172
+ // character into an escape sequence.
173
+ function quoteForUnit(value) {
174
+ if (!/[\s"\\]/.test(value))
175
+ return value;
176
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
177
+ return `"${escaped}"`;
178
+ }
179
+ // Render a systemd `Environment=KEY=value` line, quoting the whole assignment
180
+ // when the value contains whitespace, double-quote, or backslash. Without
181
+ // quoting, systemd splits the line on the first space and treats subsequent
182
+ // tokens as separate KEY=value pairs — which silently corrupts paths like
183
+ // `/home/john doe/.kandev`.
184
+ function envLine(key, value) {
185
+ if (!/[\s"\\]/.test(value))
186
+ return `Environment=${key}=${value}`;
187
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
188
+ return `Environment="${key}=${escaped}"`;
189
+ }
package/dist/shared.js CHANGED
@@ -5,12 +5,18 @@
5
5
  * This module extracts common patterns used across different launch modes
6
6
  * to reduce duplication and ensure consistent behavior.
7
7
  */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
8
11
  Object.defineProperty(exports, "__esModule", { value: true });
9
12
  exports.pickPorts = pickPorts;
10
13
  exports.buildBackendEnv = buildBackendEnv;
11
14
  exports.buildWebEnv = buildWebEnv;
15
+ exports.listHostNetworkAddresses = listHostNetworkAddresses;
12
16
  exports.logStartupInfo = logStartupInfo;
17
+ exports.networkUrlsForPort = networkUrlsForPort;
13
18
  exports.attachBackendExitHandler = attachBackendExitHandler;
19
+ const node_os_1 = __importDefault(require("node:os"));
14
20
  const constants_1 = require("./constants");
15
21
  const ports_1 = require("./ports");
16
22
  /**
@@ -80,31 +86,110 @@ function buildWebEnv(options) {
80
86
  // URLs like `https://host:38429/...` that aren't reachable behind a
81
87
  // reverse proxy / ingress / Cloudflare tunnel.
82
88
  env.NEXT_PUBLIC_KANDEV_API_PORT = String(ports.backendPort);
89
+ // Auto-allow the host's own LAN / VPN addresses so a dev hitting the dev
90
+ // server from another device on the same network (Tailscale, LAN IP, WSL
91
+ // mirrored mode, etc.) passes Next.js's allowedDevOrigins check and HMR
92
+ // works. The user can still extend the list via NEXT_ALLOWED_DEV_ORIGINS.
93
+ // Skip the assignment when there's nothing to add — keeps the env clean
94
+ // for the loopback-only case.
95
+ const merged = mergeAllowedDevOrigins(process.env.NEXT_ALLOWED_DEV_ORIGINS, listHostNetworkAddresses());
96
+ if (merged)
97
+ env.NEXT_ALLOWED_DEV_ORIGINS = merged;
83
98
  }
84
99
  if (debug) {
85
100
  env.NEXT_PUBLIC_KANDEV_DEBUG = "true";
86
101
  }
87
102
  return env;
88
103
  }
104
+ /**
105
+ * Returns the host's non-loopback, non-internal IPv4/IPv6 addresses. Used to
106
+ * auto-populate Next.js `allowedDevOrigins` so the dev server accepts
107
+ * connections from LAN / Tailscale / SSH-forwarded clients.
108
+ */
109
+ function listHostNetworkAddresses() {
110
+ const v4 = [];
111
+ const v6 = [];
112
+ const seen = new Set();
113
+ const interfaces = node_os_1.default.networkInterfaces();
114
+ for (const addrs of Object.values(interfaces)) {
115
+ if (!addrs)
116
+ continue;
117
+ for (const addr of addrs) {
118
+ if (addr.internal)
119
+ continue;
120
+ // Skip link-local IPv6 (fe80::/10) and link-local IPv4 (169.254.0.0/16,
121
+ // RFC 3927) — neither is reachable from a remote machine, and the
122
+ // 169.254 range in particular is what Hyper-V assigns to its phantom
123
+ // WSL adapter, which clutters the startup output. The regex covers the
124
+ // full /10 (fe80::–febf::); OS stacks only assign fe80::/64 in practice
125
+ // but a stricter check is the same effort and removes the surprise.
126
+ if (addr.family === "IPv6" && /^fe[89ab]/i.test(addr.address))
127
+ continue;
128
+ if (addr.family === "IPv4" && addr.address.startsWith("169.254."))
129
+ continue;
130
+ if (seen.has(addr.address))
131
+ continue;
132
+ seen.add(addr.address);
133
+ if (addr.family === "IPv4")
134
+ v4.push(addr.address);
135
+ else
136
+ v6.push(addr.address);
137
+ }
138
+ }
139
+ // IPv4 first — LAN + Tailscale IPv4 are what people usually want.
140
+ return [...v4, ...v6];
141
+ }
142
+ function mergeAllowedDevOrigins(existing, extra) {
143
+ const set = new Set();
144
+ if (existing) {
145
+ for (const s of existing.split(",")) {
146
+ const trimmed = s.trim();
147
+ if (trimmed)
148
+ set.add(trimmed);
149
+ }
150
+ }
151
+ for (const s of extra)
152
+ set.add(s);
153
+ return [...set].join(",");
154
+ }
89
155
  /**
90
156
  * Logs a unified startup info block to the console.
157
+ *
158
+ * Shows only the URL the user actually opens — start/run modes have the Go
159
+ * backend reverse-proxy Next.js on a single port, dev mode hits Next.js
160
+ * directly. The other port and the agentctl port are internal plumbing and
161
+ * would only mislead. Below the URL, lists the same port on each non-loopback
162
+ * interface (LAN, Tailscale) so a user opening the app remotely sees the
163
+ * right address.
91
164
  */
92
165
  function logStartupInfo(options) {
93
- const { header, ports, dbPath, logLevel } = options;
94
- const backendUrl = ports.backendUrl;
95
- const webUrl = `http://localhost:${ports.webPort}`;
166
+ const { header, ports, primary = "backend", dbPath, logLevel } = options;
167
+ const primaryPort = primary === "web" ? ports.webPort : ports.backendPort;
168
+ const primaryUrl = `http://localhost:${primaryPort}`;
169
+ const networkHosts = listHostNetworkAddresses();
96
170
  console.log(`[kandev] ${header}`);
97
- console.log("[kandev] backend:", backendUrl);
98
- console.log("[kandev] web:", webUrl);
99
- console.log("[kandev] agentctl port:", ports.agentctlPort);
100
- console.log("[kandev] mcp url:", `${backendUrl}/mcp`);
171
+ console.log("[kandev] url:", primaryUrl);
172
+ for (const url of networkUrlsForPort(primaryPort, networkHosts)) {
173
+ console.log("[kandev] network:", url);
174
+ }
175
+ console.log("[kandev] mcp:", `${ports.backendUrl}/mcp`);
101
176
  if (dbPath) {
102
- console.log("[kandev] db path:", dbPath);
177
+ console.log("[kandev] db:", dbPath);
103
178
  }
104
179
  if (logLevel) {
105
180
  console.log("[kandev] log level:", logLevel);
106
181
  }
107
182
  }
183
+ /**
184
+ * Builds `http://<host>:<port>` URLs from a list of host addresses, wrapping
185
+ * IPv6 addresses in brackets per RFC 3986.
186
+ */
187
+ function networkUrlsForPort(port, hosts) {
188
+ return hosts.map((host) => {
189
+ const formatted = host.includes(":") ? `[${host}]` : host;
190
+ return `http://${formatted}:${port}`;
191
+ });
192
+ }
108
193
  /**
109
194
  * Attaches a standardized exit handler to a backend process.
110
195
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kandev",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
4
4
  "private": false,
5
5
  "description": "Launcher for Kandev — manage tasks, orchestrate agents, review changes, and ship value",
6
6
  "license": "AGPL-3.0-only",
@@ -22,11 +22,11 @@
22
22
  "npm": ">=7"
23
23
  },
24
24
  "optionalDependencies": {
25
- "@kdlbs/runtime-linux-x64": "0.49.0",
26
- "@kdlbs/runtime-linux-arm64": "0.49.0",
27
- "@kdlbs/runtime-darwin-x64": "0.49.0",
28
- "@kdlbs/runtime-darwin-arm64": "0.49.0",
29
- "@kdlbs/runtime-win32-x64": "0.49.0"
25
+ "@kdlbs/runtime-linux-x64": "0.51.0",
26
+ "@kdlbs/runtime-linux-arm64": "0.51.0",
27
+ "@kdlbs/runtime-darwin-x64": "0.51.0",
28
+ "@kdlbs/runtime-darwin-arm64": "0.51.0",
29
+ "@kdlbs/runtime-win32-x64": "0.51.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "tar": "^7.5.11",