kandev 0.48.0 → 0.50.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,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MANAGED_MARKER_TEXT = void 0;
4
+ exports.looksLikeManagedUnit = looksLikeManagedUnit;
5
+ exports.renderSystemdUnit = renderSystemdUnit;
6
+ exports.renderLaunchdPlist = renderLaunchdPlist;
7
+ const SYSTEMD_PATH = "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin";
8
+ const LAUNCHD_PATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
9
+ /**
10
+ * Marker substring baked into every unit/plist kandev writes. Used to safely
11
+ * detect "this file is ours, overwriting is fine" vs "user has put something
12
+ * else here, warn loudly before replacing".
13
+ */
14
+ exports.MANAGED_MARKER_TEXT = "managed by kandev";
15
+ const SYSTEMD_MARKER = `# ${exports.MANAGED_MARKER_TEXT} — regenerated by \`kandev service install\``;
16
+ const PLIST_MARKER = `<!-- ${exports.MANAGED_MARKER_TEXT} — regenerated by \`kandev service install\` -->`;
17
+ function looksLikeManagedUnit(content) {
18
+ return content.includes(exports.MANAGED_MARKER_TEXT);
19
+ }
20
+ /**
21
+ * Render a systemd unit file for kandev.
22
+ *
23
+ * The unit hard-codes absolute paths so it works without a user PATH. We pass
24
+ * `--headless` so the daemon doesn't try to open a browser. KANDEV_BUNDLE_DIR /
25
+ * KANDEV_VERSION are surfaced only when present (Homebrew installs only) so
26
+ * `npm i -g` installs don't get spurious env vars.
27
+ */
28
+ function renderSystemdUnit(input) {
29
+ const env = [
30
+ envLine("KANDEV_HOME_DIR", input.homeDir),
31
+ envLine("KANDEV_LOG_LEVEL", "info"),
32
+ envLine("PATH", SYSTEMD_PATH),
33
+ ];
34
+ if (input.port !== undefined) {
35
+ env.push(envLine("KANDEV_SERVER_PORT", String(input.port)));
36
+ }
37
+ if (input.launcher.bundleDir) {
38
+ env.push(envLine("KANDEV_BUNDLE_DIR", input.launcher.bundleDir));
39
+ }
40
+ if (input.launcher.version) {
41
+ env.push(envLine("KANDEV_VERSION", input.launcher.version));
42
+ }
43
+ const wantedBy = input.mode === "system" ? "multi-user.target" : "default.target";
44
+ const userLine = input.mode === "system" && input.systemUser ? `User=${input.systemUser}\n` : "";
45
+ const exec = `${quoteForUnit(input.launcher.nodePath)} ${quoteForUnit(input.launcher.cliEntry)} --headless`;
46
+ return `${SYSTEMD_MARKER}
47
+ [Unit]
48
+ Description=Kandev autonomous agent platform
49
+ Documentation=https://github.com/kdlbs/kandev
50
+ After=network-online.target
51
+ Wants=network-online.target
52
+
53
+ [Service]
54
+ Type=simple
55
+ ExecStart=${exec}
56
+ ${userLine}${env.join("\n")}
57
+ Restart=on-failure
58
+ RestartSec=5s
59
+ KillMode=mixed
60
+ TimeoutStopSec=30s
61
+
62
+ [Install]
63
+ WantedBy=${wantedBy}
64
+ `;
65
+ }
66
+ /**
67
+ * Render a launchd plist for kandev.
68
+ *
69
+ * launchd has no equivalent to systemd's enable-linger: a user agent only runs
70
+ * while the user is logged in. For a Mac-as-server scenario, use --system to
71
+ * get a LaunchDaemon that runs at boot regardless of login.
72
+ */
73
+ function renderLaunchdPlist(input) {
74
+ const envEntries = [
75
+ ["KANDEV_HOME_DIR", input.homeDir],
76
+ ["KANDEV_LOG_LEVEL", "info"],
77
+ ["PATH", LAUNCHD_PATH],
78
+ ];
79
+ if (input.port !== undefined) {
80
+ envEntries.push(["KANDEV_SERVER_PORT", String(input.port)]);
81
+ }
82
+ if (input.launcher.bundleDir) {
83
+ envEntries.push(["KANDEV_BUNDLE_DIR", input.launcher.bundleDir]);
84
+ }
85
+ if (input.launcher.version) {
86
+ envEntries.push(["KANDEV_VERSION", input.launcher.version]);
87
+ }
88
+ const envXml = envEntries
89
+ .map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
90
+ .join("\n");
91
+ const args = [input.launcher.nodePath, input.launcher.cliEntry, "--headless"];
92
+ const argsXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
93
+ // For system-mode LaunchDaemons, run as a specific user instead of root.
94
+ // For user agents this directive is omitted — the agent already runs as
95
+ // the loading user.
96
+ const userBlock = input.mode === "system" && input.systemUser
97
+ ? ` <key>UserName</key>\n <string>${escapeXml(input.systemUser)}</string>\n`
98
+ : "";
99
+ return `<?xml version="1.0" encoding="UTF-8"?>
100
+ ${PLIST_MARKER}
101
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
102
+ <plist version="1.0">
103
+ <dict>
104
+ <key>Label</key>
105
+ <string>com.kdlbs.kandev</string>
106
+ <key>ProgramArguments</key>
107
+ <array>
108
+ ${argsXml}
109
+ </array>
110
+ <key>EnvironmentVariables</key>
111
+ <dict>
112
+ ${envXml}
113
+ </dict>
114
+ ${userBlock} <key>RunAtLoad</key>
115
+ <true/>
116
+ <key>KeepAlive</key>
117
+ <true/>
118
+ <key>StandardOutPath</key>
119
+ <string>${escapeXml(`${input.logDir}/service.out`)}</string>
120
+ <key>StandardErrorPath</key>
121
+ <string>${escapeXml(`${input.logDir}/service.err`)}</string>
122
+ <key>WorkingDirectory</key>
123
+ <string>${escapeXml(input.homeDir)}</string>
124
+ </dict>
125
+ </plist>
126
+ `;
127
+ }
128
+ function escapeXml(value) {
129
+ return value
130
+ .replace(/&/g, "&amp;")
131
+ .replace(/</g, "&lt;")
132
+ .replace(/>/g, "&gt;")
133
+ .replace(/"/g, "&quot;")
134
+ .replace(/'/g, "&apos;");
135
+ }
136
+ // systemd unit "Environment=" lines can contain spaces by wrapping in double quotes.
137
+ // ExecStart needs special handling for spaces too — paths with spaces must be
138
+ // double-quoted per systemd.unit(5). Escape backslashes before double-quotes so
139
+ // a literal `\` in a path is preserved instead of merging with the following
140
+ // character into an escape sequence.
141
+ function quoteForUnit(value) {
142
+ if (!/[\s"\\]/.test(value))
143
+ return value;
144
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
145
+ return `"${escaped}"`;
146
+ }
147
+ // Render a systemd `Environment=KEY=value` line, quoting the whole assignment
148
+ // when the value contains whitespace, double-quote, or backslash. Without
149
+ // quoting, systemd splits the line on the first space and treats subsequent
150
+ // tokens as separate KEY=value pairs — which silently corrupts paths like
151
+ // `/home/john doe/.kandev`.
152
+ function envLine(key, value) {
153
+ if (!/[\s"\\]/.test(value))
154
+ return `Environment=${key}=${value}`;
155
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
156
+ return `Environment="${key}=${escaped}"`;
157
+ }
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.48.0",
3
+ "version": "0.50.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.48.0",
26
- "@kdlbs/runtime-linux-arm64": "0.48.0",
27
- "@kdlbs/runtime-darwin-x64": "0.48.0",
28
- "@kdlbs/runtime-darwin-arm64": "0.48.0",
29
- "@kdlbs/runtime-win32-x64": "0.48.0"
25
+ "@kdlbs/runtime-linux-x64": "0.50.0",
26
+ "@kdlbs/runtime-linux-arm64": "0.50.0",
27
+ "@kdlbs/runtime-darwin-x64": "0.50.0",
28
+ "@kdlbs/runtime-darwin-arm64": "0.50.0",
29
+ "@kdlbs/runtime-win32-x64": "0.50.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "tar": "^7.5.11",