@pushpalsdev/cli 1.0.8 → 1.0.10

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 (31) hide show
  1. package/dist/pushpals-cli.js +525 -122
  2. package/monitor-ui/+not-found.html +440 -0
  3. package/monitor-ui/_expo/.routes.json +3 -0
  4. package/monitor-ui/_expo/static/js/web/entry-1d5a07c47473852fe9754bfe3e6da301.js +1219 -0
  5. package/monitor-ui/_expo/static/js/web/index-5650b6cee60075cb63c1efc40dcf7683.js +6 -0
  6. package/monitor-ui/_sitemap.html +440 -0
  7. package/monitor-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/back-icon-mask.0a328cd9c1afd0afe8e3b1ec5165b1b4.png +0 -0
  8. package/monitor-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/back-icon.35ba0eaec5a4f5ed12ca16fabeae451d.png +0 -0
  9. package/monitor-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55.png +0 -0
  10. package/monitor-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@2x.png +0 -0
  11. package/monitor-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@3x.png +0 -0
  12. package/monitor-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@4x.png +0 -0
  13. package/monitor-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7.png +0 -0
  14. package/monitor-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@2x.png +0 -0
  15. package/monitor-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@3x.png +0 -0
  16. package/monitor-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@4x.png +0 -0
  17. package/monitor-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/search-icon.286d67d3f74808a60a78d3ebf1a5fb57.png +0 -0
  18. package/monitor-ui/assets/__node_modules/expo-router/assets/arrow_down.017bc6ba3fc25503e5eb5e53826d48a8.png +0 -0
  19. package/monitor-ui/assets/__node_modules/expo-router/assets/error.d1ea1496f9057eb392d5bbf3732a61b7.png +0 -0
  20. package/monitor-ui/assets/__node_modules/expo-router/assets/file.19eeb73b9593a38f8e9f418337fc7d10.png +0 -0
  21. package/monitor-ui/assets/__node_modules/expo-router/assets/forward.d8b800c443b8972542883e0b9de2bdc6.png +0 -0
  22. package/monitor-ui/assets/__node_modules/expo-router/assets/pkg.ab19f4cbc543357183a20571f68380a3.png +0 -0
  23. package/monitor-ui/assets/__node_modules/expo-router/assets/sitemap.412dd9275b6b48ad28f5e3d81bb1f626.png +0 -0
  24. package/monitor-ui/assets/__node_modules/expo-router/assets/unmatched.20e71bdf79e3a97bf55fd9e164041578.png +0 -0
  25. package/monitor-ui/favicon.ico +0 -0
  26. package/monitor-ui/index.html +440 -0
  27. package/monitor-ui/modal.html +440 -0
  28. package/package.json +3 -2
  29. package/runtime/.env.example +4 -4
  30. package/runtime/configs/default.toml +5 -4
  31. package/runtime/configs/local.example.toml +6 -1
@@ -2,8 +2,16 @@
2
2
  // @bun
3
3
 
4
4
  // ../../scripts/pushpals-cli.ts
5
- import { appendFileSync, chmodSync, cpSync, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
6
- import { dirname, join as join2, resolve as resolve3 } from "path";
5
+ import {
6
+ appendFileSync,
7
+ chmodSync,
8
+ cpSync,
9
+ existsSync as existsSync4,
10
+ mkdirSync,
11
+ readFileSync as readFileSync4,
12
+ writeFileSync
13
+ } from "fs";
14
+ import { dirname, extname, join as join2, resolve as resolve4 } from "path";
7
15
  import { createInterface } from "readline";
8
16
 
9
17
  // ../shared/src/client_preflight.ts
@@ -13,12 +21,53 @@ import { relative, resolve as resolve2 } from "path";
13
21
  // ../shared/src/config.ts
14
22
  import { existsSync, readFileSync } from "fs";
15
23
  import { join, resolve, isAbsolute } from "path";
24
+
25
+ // ../shared/src/local_network.ts
26
+ var DEFAULT_LOCAL_LOOPBACK_HOST = "127.0.0.1";
27
+ function isLoopbackHost(hostname) {
28
+ const normalized = String(hostname ?? "").trim().toLowerCase().replace(/^\[(.*)\]$/, "$1");
29
+ return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
30
+ }
31
+ function normalizeLoopbackHost(hostname) {
32
+ const normalized = String(hostname ?? "").trim().toLowerCase().replace(/^\[(.*)\]$/, "$1");
33
+ if (isLoopbackHost(normalized))
34
+ return DEFAULT_LOCAL_LOOPBACK_HOST;
35
+ return DEFAULT_LOCAL_LOOPBACK_HOST;
36
+ }
37
+ function normalizeLoopbackHttpUrl(value, fallbackPort) {
38
+ const fallback = `http://${DEFAULT_LOCAL_LOOPBACK_HOST}:${Math.max(1, fallbackPort)}`;
39
+ const text = String(value ?? "").trim();
40
+ if (!text)
41
+ return fallback;
42
+ try {
43
+ const parsed = new URL(text);
44
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
45
+ return fallback;
46
+ }
47
+ parsed.protocol = "http:";
48
+ parsed.username = "";
49
+ parsed.password = "";
50
+ parsed.hostname = normalizeLoopbackHost(parsed.hostname);
51
+ parsed.pathname = "/";
52
+ parsed.search = "";
53
+ parsed.hash = "";
54
+ if (!parsed.port) {
55
+ parsed.port = String(Math.max(1, fallbackPort));
56
+ }
57
+ return parsed.toString().replace(/\/+$/, "");
58
+ } catch {
59
+ return fallback;
60
+ }
61
+ }
62
+
63
+ // ../shared/src/config.ts
16
64
  var PROJECT_ROOT = resolve(import.meta.dir, "..", "..", "..");
17
65
  var DEFAULT_CONFIG_DIR = "configs";
18
66
  var LEGACY_CONFIG_DIR = "config";
19
67
  var TRUTHY = new Set(["1", "true", "yes", "on"]);
20
68
  var FALSY = new Set(["0", "false", "no", "off"]);
21
69
  var DEFAULT_WORKERPALS_QUALITY_CRITIC_MIN_SCORE = 8;
70
+ var DEFAULT_WORKERPALS_QUALITY_MAX_AUTO_REVISIONS = 1;
22
71
  var DEFAULT_WORKERPALS_FILE_MODIFYING_JOBS = ["task.execute"];
23
72
  var DEFAULT_WORKERPALS_OUTPUT_MAX_CHARS = 192 * 1024;
24
73
  var DEFAULT_WORKERPALS_OUTPUT_MAX_LINES = 600;
@@ -286,13 +335,14 @@ function loadPushPalsConfig(options = {}) {
286
335
  const remotebuddyDbPath = resolvePathFromRoot(projectRoot, firstNonEmpty(process.env.REMOTEBUDDY_DB_PATH, asString(pathsNode.remotebuddy_db_path, join(dataDir, "remotebuddy-state.db"))));
287
336
  const serverNode = getObject(merged, "server");
288
337
  const serverPort = Math.max(1, asInt(parseIntEnv("PUSHPALS_PORT") ?? serverNode.port, 3001));
289
- const serverUrl = firstNonEmpty(process.env.PUSHPALS_SERVER_URL, asString(serverNode.url, `http://localhost:${serverPort}`), `http://localhost:${serverPort}`);
290
- const serverHost = asString(serverNode.host, "0.0.0.0");
338
+ const serverUrl = normalizeLoopbackHttpUrl(firstNonEmpty(process.env.PUSHPALS_SERVER_URL, asString(serverNode.url, `http://127.0.0.1:${serverPort}`), `http://127.0.0.1:${serverPort}`), serverPort);
339
+ const serverHost = normalizeLoopbackHost(firstNonEmpty(process.env.PUSHPALS_HOST, asString(serverNode.host, "127.0.0.1")));
291
340
  const debugHttp = parseBoolEnv("PUSHPALS_DEBUG_HTTP") ?? asBoolean(serverNode.debug_http, false);
292
341
  const staleClaimTtlMs = Math.max(5000, asInt(parseIntEnv("PUSHPALS_STALE_CLAIM_TTL_MS") ?? serverNode.stale_claim_ttl_ms, 120000));
293
342
  const staleClaimSweepIntervalMs = Math.max(1000, asInt(parseIntEnv("PUSHPALS_STALE_CLAIM_SWEEP_INTERVAL_MS") ?? serverNode.stale_claim_sweep_interval_ms, 5000));
294
343
  const globalStatusHeartbeatMs = parseIntEnv("PUSHPALS_STATUS_HEARTBEAT_MS");
295
344
  const localNode = getObject(merged, "localbuddy");
345
+ const localEnabled = parseBoolEnv("LOCALBUDDY_ENABLED") ?? asBoolean(localNode.enabled, false);
296
346
  const localPort = Math.max(1, asInt(parseIntEnv("LOCAL_AGENT_PORT") ?? localNode.port, 3003));
297
347
  const localStatusHeartbeatMs = Math.max(0, asInt(parseIntEnv("LOCALBUDDY_STATUS_HEARTBEAT_MS") ?? globalStatusHeartbeatMs ?? localNode.status_heartbeat_ms, 120000));
298
348
  const localLlm = resolveLlmConfig(localNode, "LOCALBUDDY", {
@@ -369,7 +419,7 @@ function loadPushPalsConfig(options = {}) {
369
419
  const workerMinisweTimeoutMs = Math.max(1e4, asInt(parseIntEnv("WORKERPALS_MINISWE_TIMEOUT_MS") ?? workerNode.miniswe_timeout_ms, 1800000));
370
420
  const workerOpenAICodexPython = firstNonEmpty(process.env.PUSHPALS_OPENAI_CODEX_PYTHON, asString(workerNode.openai_codex_python, "python"), "python");
371
421
  const workerOpenAICodexTimeoutMs = Math.max(1e4, asInt(workerNode.openai_codex_timeout_ms, 7200000));
372
- const workerQualityMaxAutoRevisions = Math.max(0, Math.min(10, asInt(parseIntEnv("WORKERPALS_QUALITY_MAX_AUTO_REVISIONS") ?? workerNode.quality_max_auto_revisions, 4)));
422
+ const workerQualityMaxAutoRevisions = Math.max(0, Math.min(10, asInt(parseIntEnv("WORKERPALS_QUALITY_MAX_AUTO_REVISIONS") ?? workerNode.quality_max_auto_revisions, DEFAULT_WORKERPALS_QUALITY_MAX_AUTO_REVISIONS)));
373
423
  const workerFileModifyingJobs = (() => {
374
424
  const envRaw = firstNonEmpty(process.env.WORKERPALS_FILE_MODIFYING_JOBS);
375
425
  const parsed = envRaw ? envRaw.split(",").map((entry) => entry.trim()).filter(Boolean) : asStringArray(workerNode.file_modifying_jobs);
@@ -531,6 +581,7 @@ function loadPushPalsConfig(options = {}) {
531
581
  staleClaimSweepIntervalMs
532
582
  },
533
583
  localbuddy: {
584
+ enabled: localEnabled,
534
585
  port: localPort,
535
586
  statusHeartbeatMs: localStatusHeartbeatMs,
536
587
  llm: localLlm
@@ -734,7 +785,7 @@ function loadPushPalsConfig(options = {}) {
734
785
  portConflictPolicy: startupPortConflictPolicy
735
786
  },
736
787
  client: {
737
- localAgentUrl: firstNonEmpty(process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, asString(clientNode.local_agent_url, `http://localhost:${localPort}`), `http://localhost:${localPort}`),
788
+ localAgentUrl: normalizeLoopbackHttpUrl(firstNonEmpty(process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, asString(clientNode.local_agent_url, `http://127.0.0.1:${localPort}`), `http://127.0.0.1:${localPort}`), localPort),
738
789
  traceTailLines: Math.max(10, asInt(parseIntEnv("EXPO_PUBLIC_PUSHPALS_TRACE_TAIL_LINES") ?? clientNode.trace_tail_lines, 100))
739
790
  }
740
791
  };
@@ -1053,6 +1104,46 @@ function formatClientRuntimePreflightLines(result, prefix) {
1053
1104
  return lines;
1054
1105
  }
1055
1106
 
1107
+ // ../shared/src/repo.ts
1108
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
1109
+ import { resolve as resolve3 } from "path";
1110
+ function resolveDotGitEntry(repoRoot) {
1111
+ return resolve3(repoRoot, ".git");
1112
+ }
1113
+ function resolveGitMetadataDir(repoRoot) {
1114
+ const dotGitPath = resolveDotGitEntry(repoRoot);
1115
+ if (!existsSync3(dotGitPath))
1116
+ return null;
1117
+ try {
1118
+ const stat = statSync(dotGitPath);
1119
+ if (stat.isDirectory()) {
1120
+ return dotGitPath;
1121
+ }
1122
+ if (!stat.isFile()) {
1123
+ return null;
1124
+ }
1125
+ } catch {
1126
+ return null;
1127
+ }
1128
+ try {
1129
+ const firstLine = readFileSync3(dotGitPath, "utf8").split(/\r?\n/, 1)[0] ?? "";
1130
+ const match = firstLine.match(/^gitdir:\s*(.+)\s*$/i);
1131
+ if (!match)
1132
+ return null;
1133
+ const gitDir = resolve3(repoRoot, match[1].trim());
1134
+ return existsSync3(gitDir) ? gitDir : null;
1135
+ } catch {
1136
+ return null;
1137
+ }
1138
+ }
1139
+ function resolveGitStateFilePath(repoRoot, fileName) {
1140
+ const gitMetadataDir = resolveGitMetadataDir(repoRoot);
1141
+ const normalizedFileName = String(fileName ?? "").trim();
1142
+ if (!gitMetadataDir || !normalizedFileName)
1143
+ return null;
1144
+ return resolve3(gitMetadataDir, normalizedFileName);
1145
+ }
1146
+
1056
1147
  // ../../scripts/pushpals-cli.ts
1057
1148
  var DEFAULT_MONITOR_PORT = 8081;
1058
1149
  var MONITOR_SCAN_PORTS = 32;
@@ -1073,6 +1164,29 @@ var GITHUB_HEADERS = {
1073
1164
  "User-Agent": "pushpals-cli"
1074
1165
  };
1075
1166
  var stateVersion = 1;
1167
+ var cliTimestampedConsoleInstalled = false;
1168
+ function formatTimestampedCliLine(line, at = new Date) {
1169
+ const text = String(line ?? "");
1170
+ if (!text.startsWith("[pushpals]") && !text.startsWith("[localbuddy]")) {
1171
+ return text;
1172
+ }
1173
+ return `[${at.toISOString()}]${text}`;
1174
+ }
1175
+ function installTimestampedCliConsole() {
1176
+ if (cliTimestampedConsoleInstalled)
1177
+ return;
1178
+ cliTimestampedConsoleInstalled = true;
1179
+ const patch = (original) => (...args) => {
1180
+ if (args.length > 0 && typeof args[0] === "string") {
1181
+ args[0] = formatTimestampedCliLine(args[0]);
1182
+ }
1183
+ return original(...args);
1184
+ };
1185
+ console.log = patch(console.log.bind(console));
1186
+ console.warn = patch(console.warn.bind(console));
1187
+ console.error = patch(console.error.bind(console));
1188
+ }
1189
+ installTimestampedCliConsole();
1076
1190
  function logCliInvocation(argv) {
1077
1191
  const startedAt = new Date().toISOString();
1078
1192
  const cliVersion = String(process.env.PUSHPALS_CLI_PACKAGE_VERSION ?? "").trim() || "unknown";
@@ -1098,6 +1212,7 @@ function printUsage() {
1098
1212
  console.log(" --runtime-tag <tag> Override runtime release tag (e.g. v1.0.2)");
1099
1213
  console.log(" --no-auto-start Disable runtime auto-start when LocalBuddy is down");
1100
1214
  console.log(" --no-stream Disable live session event stream");
1215
+ console.log(" --runtime-only Start the local runtime and wait for shutdown without opening the interactive chat");
1101
1216
  console.log(" -h, --help Show this help");
1102
1217
  console.log("");
1103
1218
  console.log("Chat commands:");
@@ -1112,7 +1227,7 @@ function printUsage() {
1112
1227
  console.log(" - LocalBuddy must be attached to the same repo root.");
1113
1228
  }
1114
1229
  function parseArgs(argv) {
1115
- const options = { noAutoStart: false, noStream: false };
1230
+ const options = { noAutoStart: false, noStream: false, runtimeOnly: false };
1116
1231
  for (let i = 0;i < argv.length; i++) {
1117
1232
  const arg = argv[i];
1118
1233
  if (arg === "-h" || arg === "--help") {
@@ -1127,6 +1242,10 @@ function parseArgs(argv) {
1127
1242
  options.noAutoStart = true;
1128
1243
  continue;
1129
1244
  }
1245
+ if (arg === "--runtime-only") {
1246
+ options.runtimeOnly = true;
1247
+ continue;
1248
+ }
1130
1249
  if (arg === "--server-url") {
1131
1250
  options.serverUrl = argv[++i];
1132
1251
  continue;
@@ -1162,6 +1281,30 @@ function normalizeUrl(value, fallback = "") {
1162
1281
  const selected = text || fallback;
1163
1282
  return selected.replace(/\/+$/, "");
1164
1283
  }
1284
+ function normalizeLoopbackUrl(value, fallback) {
1285
+ const selected = normalizeUrl(value, fallback);
1286
+ if (!selected)
1287
+ return "";
1288
+ try {
1289
+ const parsed = new URL(selected);
1290
+ parsed.protocol = "http:";
1291
+ parsed.username = "";
1292
+ parsed.password = "";
1293
+ parsed.hostname = "127.0.0.1";
1294
+ return parsed.toString().replace(/\/+$/, "");
1295
+ } catch {
1296
+ return normalizeUrl(fallback);
1297
+ }
1298
+ }
1299
+ function isLoopbackUrl(value) {
1300
+ try {
1301
+ const parsed = new URL(normalizeUrl(value));
1302
+ const hostname = String(parsed.hostname ?? "").trim().toLowerCase();
1303
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
1304
+ } catch {
1305
+ return false;
1306
+ }
1307
+ }
1165
1308
  function parsePositiveInt(value, fallback) {
1166
1309
  const parsed = Number.parseInt(String(value ?? "").trim(), 10);
1167
1310
  if (!Number.isFinite(parsed) || parsed <= 0)
@@ -1169,7 +1312,7 @@ function parsePositiveInt(value, fallback) {
1169
1312
  return parsed;
1170
1313
  }
1171
1314
  function normalizePath(value) {
1172
- const normalized = resolve3(value).replace(/\\/g, "/").replace(/\/+$/, "");
1315
+ const normalized = resolve4(value).replace(/\\/g, "/").replace(/\/+$/, "");
1173
1316
  if (process.platform === "win32")
1174
1317
  return normalized.toLowerCase();
1175
1318
  return normalized;
@@ -1197,11 +1340,11 @@ async function resolveCurrentGitRepoRoot(cwd) {
1197
1340
  const root = await runGit(["rev-parse", "--show-toplevel"], cwd);
1198
1341
  if (!root.ok || !root.stdout)
1199
1342
  return null;
1200
- return resolve3(root.stdout);
1343
+ return resolve4(root.stdout);
1201
1344
  }
1202
1345
  function resolveDefaultRuntimeRoot() {
1203
1346
  const home = process.env.USERPROFILE || process.env.HOME || process.cwd();
1204
- return resolve3(home, ".pushpals", "runtime");
1347
+ return resolve4(home, ".pushpals", "runtime");
1205
1348
  }
1206
1349
  function buildRuntimeAssetSource(root, protocolSchemasDir) {
1207
1350
  return {
@@ -1214,13 +1357,13 @@ function buildRuntimeAssetSource(root, protocolSchemasDir) {
1214
1357
  };
1215
1358
  }
1216
1359
  function isCompleteRuntimeAssetSource(source) {
1217
- return existsSync3(source.envExamplePath) && existsSync3(source.visionExamplePath) && existsSync3(join2(source.configsDir, "default.toml")) && existsSync3(source.promptsDir) && existsSync3(join2(source.protocolSchemasDir, "envelope.schema.json")) && existsSync3(join2(source.protocolSchemasDir, "events.schema.json"));
1360
+ return existsSync4(source.envExamplePath) && existsSync4(source.visionExamplePath) && existsSync4(join2(source.configsDir, "default.toml")) && existsSync4(source.promptsDir) && existsSync4(join2(source.protocolSchemasDir, "envelope.schema.json")) && existsSync4(join2(source.protocolSchemasDir, "events.schema.json"));
1218
1361
  }
1219
1362
  function resolveBundledRuntimeAssetSource() {
1220
1363
  const candidates = [
1221
- buildRuntimeAssetSource(resolve3(import.meta.dir, "..", "runtime"), resolve3(import.meta.dir, "..", "runtime", "protocol", "schemas")),
1222
- buildRuntimeAssetSource(resolve3(import.meta.dir, ".."), resolve3(import.meta.dir, "..", "packages", "protocol", "src", "schemas")),
1223
- buildRuntimeAssetSource(resolve3(import.meta.dir, "..", "packages", "cli", "runtime"), resolve3(import.meta.dir, "..", "packages", "cli", "runtime", "protocol", "schemas"))
1364
+ buildRuntimeAssetSource(resolve4(import.meta.dir, "..", "runtime"), resolve4(import.meta.dir, "..", "runtime", "protocol", "schemas")),
1365
+ buildRuntimeAssetSource(resolve4(import.meta.dir, ".."), resolve4(import.meta.dir, "..", "packages", "protocol", "src", "schemas")),
1366
+ buildRuntimeAssetSource(resolve4(import.meta.dir, "..", "packages", "cli", "runtime"), resolve4(import.meta.dir, "..", "packages", "cli", "runtime", "protocol", "schemas"))
1224
1367
  ];
1225
1368
  for (const candidate of candidates) {
1226
1369
  if (isCompleteRuntimeAssetSource(candidate))
@@ -1228,8 +1371,58 @@ function resolveBundledRuntimeAssetSource() {
1228
1371
  }
1229
1372
  return null;
1230
1373
  }
1374
+ function looksLikeMonitoringHubBuild(root) {
1375
+ return existsSync4(join2(root, "index.html")) && existsSync4(join2(root, "_expo"));
1376
+ }
1377
+ function resolveBundledMonitoringHubRoot() {
1378
+ const candidates = [
1379
+ resolve4(import.meta.dir, "..", "monitor-ui"),
1380
+ resolve4(import.meta.dir, "..", "packages", "cli", "monitor-ui")
1381
+ ];
1382
+ for (const candidate of candidates) {
1383
+ if (looksLikeMonitoringHubBuild(candidate))
1384
+ return candidate;
1385
+ }
1386
+ return null;
1387
+ }
1388
+ function resolveCliSourceCheckoutRoot() {
1389
+ const candidates = [
1390
+ resolve4(import.meta.dir, ".."),
1391
+ resolve4(import.meta.dir, "..", ".."),
1392
+ resolve4(import.meta.dir, "..", "..", "..")
1393
+ ];
1394
+ for (const candidate of candidates) {
1395
+ if (existsSync4(join2(candidate, "package.json")) && existsSync4(join2(candidate, "apps", "client", "app.json")) && existsSync4(join2(candidate, "scripts", "sync-cli-monitor-ui.ts"))) {
1396
+ return candidate;
1397
+ }
1398
+ }
1399
+ return null;
1400
+ }
1401
+ function exportBundledMonitoringHubFromSourceCheckout(sourceRoot) {
1402
+ const exportScriptPath = join2(sourceRoot, "scripts", "sync-cli-monitor-ui.ts");
1403
+ console.log("[pushpals] Packaged monitor UI missing; exporting the shared client monitor...");
1404
+ const proc = Bun.spawnSync([process.execPath, exportScriptPath], {
1405
+ cwd: sourceRoot,
1406
+ stdout: "inherit",
1407
+ stderr: "inherit",
1408
+ env: process.env
1409
+ });
1410
+ if (proc.exitCode !== 0) {
1411
+ throw new Error(`Failed to export packaged monitor UI from source checkout (exit ${proc.exitCode || 1})`);
1412
+ }
1413
+ }
1414
+ async function ensureBundledMonitoringHubRoot() {
1415
+ const existingRoot = resolveBundledMonitoringHubRoot();
1416
+ if (existingRoot)
1417
+ return existingRoot;
1418
+ const sourceRoot = resolveCliSourceCheckoutRoot();
1419
+ if (!sourceRoot)
1420
+ return null;
1421
+ exportBundledMonitoringHubFromSourceCheckout(sourceRoot);
1422
+ return resolveBundledMonitoringHubRoot();
1423
+ }
1231
1424
  function repoLooksLikePushPalsSourceCheckout(repoRoot) {
1232
- return existsSync3(join2(repoRoot, "configs", "default.toml")) || existsSync3(join2(repoRoot, "config", "default.toml"));
1425
+ return existsSync4(join2(repoRoot, "configs", "default.toml")) || existsSync4(join2(repoRoot, "config", "default.toml"));
1233
1426
  }
1234
1427
  function parseSemverFromPackageVersion(value) {
1235
1428
  const raw = String(value ?? "").trim();
@@ -1267,6 +1460,7 @@ async function resolveRuntimeReleaseTag(explicitTag) {
1267
1460
  if (fromEnv)
1268
1461
  return fromEnv;
1269
1462
  const packageVersion = parseSemverFromPackageVersion(process.env.PUSHPALS_CLI_PACKAGE_VERSION);
1463
+ console.log("[pushpals] Resolving embedded runtime release tag from GitHub...");
1270
1464
  try {
1271
1465
  return await fetchLatestReleaseTag();
1272
1466
  } catch (err) {
@@ -1279,7 +1473,7 @@ async function resolveRuntimeReleaseTag(explicitTag) {
1279
1473
  }
1280
1474
  }
1281
1475
  function writeTextFileIfMissing(pathValue, text) {
1282
- if (existsSync3(pathValue))
1476
+ if (existsSync4(pathValue))
1283
1477
  return;
1284
1478
  mkdirSync(dirname(pathValue), { recursive: true });
1285
1479
  writeFileSync(pathValue, text, "utf8");
@@ -1322,8 +1516,8 @@ function seedRuntimePreflightAssets(runtimeRoot) {
1322
1516
  writeTextFileIfMissing(join2(runtimeRoot, ".env"), `# Local PushPals runtime environment
1323
1517
  `);
1324
1518
  const localExamplePath = join2(runtimeRoot, "configs", "local.example.toml");
1325
- if (existsSync3(localExamplePath)) {
1326
- writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync3(localExamplePath, "utf8"));
1519
+ if (existsSync4(localExamplePath)) {
1520
+ writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync4(localExamplePath, "utf8"));
1327
1521
  } else {
1328
1522
  writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), `# Local PushPals runtime overrides
1329
1523
  `);
@@ -1337,6 +1531,7 @@ async function fetchTextFromUrl(url, timeoutMs = 20000) {
1337
1531
  return await response.text();
1338
1532
  }
1339
1533
  async function downloadRuntimeAssetsFromSourceTag(runtimeRoot, tag) {
1534
+ console.log(`[pushpals] Downloading embedded runtime assets from source tag ${tag}...`);
1340
1535
  const treeUrl = `${GITHUB_API_URL}/git/trees/${encodeURIComponent(tag)}?recursive=1`;
1341
1536
  const treeResponse = await fetchWithTimeout(treeUrl, { headers: GITHUB_HEADERS }, 30000);
1342
1537
  if (!treeResponse.ok) {
@@ -1357,16 +1552,19 @@ async function downloadRuntimeAssetsFromSourceTag(runtimeRoot, tag) {
1357
1552
  }
1358
1553
  }
1359
1554
  async function ensureRuntimeAssets(runtimeRoot, runtimeTag) {
1555
+ console.log(`[pushpals] Preparing embedded runtime assets for ${runtimeTag}...`);
1360
1556
  const markerPath = join2(runtimeRoot, ".runtime-assets-tag");
1361
- const currentTag = existsSync3(markerPath) ? readFileSync3(markerPath, "utf8").trim() : "";
1557
+ const currentTag = existsSync4(markerPath) ? readFileSync4(markerPath, "utf8").trim() : "";
1362
1558
  const protocolSchemasDir = join2(runtimeRoot, "protocol", "schemas");
1363
- const hasProtocolSchemas = existsSync3(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync3(join2(protocolSchemasDir, "events.schema.json"));
1364
- const hasAssets = existsSync3(join2(runtimeRoot, ".env.example")) && existsSync3(join2(runtimeRoot, "vision.example.md")) && existsSync3(join2(runtimeRoot, "configs", "default.toml")) && existsSync3(join2(runtimeRoot, "prompts")) && hasProtocolSchemas;
1559
+ const hasProtocolSchemas = existsSync4(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync4(join2(protocolSchemasDir, "events.schema.json"));
1560
+ const hasAssets = existsSync4(join2(runtimeRoot, ".env.example")) && existsSync4(join2(runtimeRoot, "vision.example.md")) && existsSync4(join2(runtimeRoot, "configs", "default.toml")) && existsSync4(join2(runtimeRoot, "prompts")) && hasProtocolSchemas;
1365
1561
  if (!hasAssets || currentTag !== runtimeTag) {
1562
+ console.log(`[pushpals] Embedded runtime assets ${hasAssets ? "are stale" : "are missing"}; refreshing bundle...`);
1366
1563
  copyBundledRuntimeAssets(runtimeRoot);
1367
- const hasProtocolSchemasAfterCopy = existsSync3(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync3(join2(protocolSchemasDir, "events.schema.json"));
1368
- const hasAssetsAfterCopy = existsSync3(join2(runtimeRoot, ".env.example")) && existsSync3(join2(runtimeRoot, "vision.example.md")) && existsSync3(join2(runtimeRoot, "configs", "default.toml")) && existsSync3(join2(runtimeRoot, "prompts")) && hasProtocolSchemasAfterCopy;
1564
+ const hasProtocolSchemasAfterCopy = existsSync4(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync4(join2(protocolSchemasDir, "events.schema.json"));
1565
+ const hasAssetsAfterCopy = existsSync4(join2(runtimeRoot, ".env.example")) && existsSync4(join2(runtimeRoot, "vision.example.md")) && existsSync4(join2(runtimeRoot, "configs", "default.toml")) && existsSync4(join2(runtimeRoot, "prompts")) && hasProtocolSchemasAfterCopy;
1369
1566
  if (!hasAssetsAfterCopy) {
1567
+ console.log("[pushpals] Bundled runtime assets are incomplete; falling back to release source downloads...");
1370
1568
  await downloadRuntimeAssetsFromSourceTag(runtimeRoot, runtimeTag);
1371
1569
  }
1372
1570
  writeFileSync(markerPath, `${runtimeTag}
@@ -1375,18 +1573,19 @@ async function ensureRuntimeAssets(runtimeRoot, runtimeTag) {
1375
1573
  writeTextFileIfMissing(join2(runtimeRoot, ".env"), `# Local PushPals runtime environment
1376
1574
  `);
1377
1575
  const localExamplePath = join2(runtimeRoot, "configs", "local.example.toml");
1378
- if (existsSync3(localExamplePath)) {
1379
- writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync3(localExamplePath, "utf8"));
1576
+ if (existsSync4(localExamplePath)) {
1577
+ writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync4(localExamplePath, "utf8"));
1380
1578
  } else {
1381
1579
  writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), `# Local PushPals runtime overrides
1382
1580
  `);
1383
1581
  }
1582
+ console.log("[pushpals] Embedded runtime assets are ready.");
1384
1583
  }
1385
1584
  function resolveDeferredRuntimeTagHint(explicitTag) {
1386
1585
  return String(explicitTag || process.env.PUSHPALS_RUNTIME_TAG || "").trim();
1387
1586
  }
1388
1587
  async function prepareCliRuntime(opts) {
1389
- const runtimeRoot = resolve3(opts.runtimeRoot || process.env.PUSHPALS_RUNTIME_ROOT || resolveDefaultRuntimeRoot());
1588
+ const runtimeRoot = resolve4(opts.runtimeRoot || process.env.PUSHPALS_RUNTIME_ROOT || resolveDefaultRuntimeRoot());
1390
1589
  if (repoLooksLikePushPalsSourceCheckout(opts.repoRoot)) {
1391
1590
  return {
1392
1591
  runtimeRoot,
@@ -1479,10 +1678,7 @@ async function resolveCommandPath(command, cwd, env) {
1479
1678
  stdout: "pipe",
1480
1679
  stderr: "ignore"
1481
1680
  });
1482
- const [stdout, exitCode] = await Promise.all([
1483
- new Response(proc.stdout).text(),
1484
- proc.exited
1485
- ]);
1681
+ const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
1486
1682
  if (exitCode !== 0)
1487
1683
  continue;
1488
1684
  const resolved = stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
@@ -1496,9 +1692,9 @@ function timestampFileToken() {
1496
1692
  return new Date().toISOString().replace(/[:.]/g, "-");
1497
1693
  }
1498
1694
  function readLogTail(logPath, maxLines = 40) {
1499
- if (!existsSync3(logPath))
1695
+ if (!existsSync4(logPath))
1500
1696
  return "";
1501
- const raw = readFileSync3(logPath, "utf8");
1697
+ const raw = readFileSync4(logPath, "utf8");
1502
1698
  const lines = raw.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.length > 0);
1503
1699
  if (lines.length === 0)
1504
1700
  return "";
@@ -1506,6 +1702,7 @@ function readLogTail(logPath, maxLines = 40) {
1506
1702
  `);
1507
1703
  }
1508
1704
  async function downloadBinaryAsset(tag, assetName, outPath) {
1705
+ console.log(`[pushpals] Downloading embedded runtime binary ${assetName} from ${tag}...`);
1509
1706
  const url = `${GITHUB_RELEASE_URL}/${encodeURIComponent(tag)}/${assetName}`;
1510
1707
  const response = await fetchWithTimeout(url, { headers: GITHUB_HEADERS }, 60000);
1511
1708
  if (!response.ok) {
@@ -1517,6 +1714,7 @@ async function downloadBinaryAsset(tag, assetName, outPath) {
1517
1714
  }
1518
1715
  async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
1519
1716
  const platformKey = resolveRuntimePlatformKey();
1717
+ console.log(`[pushpals] Preparing embedded runtime binaries for ${runtimeTag} (${platformKey})...`);
1520
1718
  const binDir = join2(runtimeRoot, "bin", `${runtimeTag}-${platformKey}`);
1521
1719
  mkdirSync(binDir, { recursive: true });
1522
1720
  const runtimeBinaries = {
@@ -1531,11 +1729,13 @@ async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
1531
1729
  runtimeBinaries.remotebuddy,
1532
1730
  runtimeBinaries.sourceControlManager
1533
1731
  ];
1732
+ let downloadedCount = 0;
1534
1733
  for (const binaryPath of requiredAssets) {
1535
- if (existsSync3(binaryPath))
1734
+ if (existsSync4(binaryPath))
1536
1735
  continue;
1537
1736
  const assetName = binaryPath.split(/[\\/]/).pop() || "";
1538
1737
  await downloadBinaryAsset(runtimeTag, assetName, binaryPath);
1738
+ downloadedCount++;
1539
1739
  }
1540
1740
  if (process.platform !== "win32") {
1541
1741
  for (const binaryPath of requiredAssets) {
@@ -1544,6 +1744,12 @@ async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
1544
1744
  } catch {}
1545
1745
  }
1546
1746
  }
1747
+ if (downloadedCount === 0) {
1748
+ console.log("[pushpals] Embedded runtime binaries are already present.");
1749
+ } else {
1750
+ console.log(`[pushpals] Embedded runtime binaries downloaded: ${downloadedCount}.`);
1751
+ }
1752
+ console.log("[pushpals] Embedded runtime binaries are ready.");
1547
1753
  return runtimeBinaries;
1548
1754
  }
1549
1755
  function spawnRuntimeService(name, command, cwd, env, logPath) {
@@ -1683,18 +1889,34 @@ async function fetchJsonWithTimeout(url, init = {}, timeoutMs = HTTP_TIMEOUT_MS)
1683
1889
  return null;
1684
1890
  }
1685
1891
  }
1686
- function authHeaders(authToken) {
1687
- if (!authToken)
1688
- return {};
1689
- return { Authorization: `Bearer ${authToken}` };
1892
+ function buildClientTransportQuery(cursor, client) {
1893
+ const params = new URLSearchParams;
1894
+ if (cursor > 0)
1895
+ params.set("after", String(cursor));
1896
+ params.set("clientId", client.clientId);
1897
+ params.set("clientKind", client.kind);
1898
+ params.set("clientLabel", client.label);
1899
+ params.set("clientVersion", client.version);
1900
+ params.set("clientPlatform", client.platform);
1901
+ params.set("clientRepoRoot", client.repoRoot);
1902
+ const query = params.toString();
1903
+ return query ? `?${query}` : "";
1904
+ }
1905
+ function createRuntimeClientId(prefix) {
1906
+ if (typeof crypto?.randomUUID === "function") {
1907
+ return `${prefix}-${crypto.randomUUID()}`;
1908
+ }
1909
+ return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1690
1910
  }
1691
- async function probeLocalBuddy(localAgentUrl, authToken) {
1692
- return await fetchJsonWithTimeout(`${localAgentUrl}/healthz`, { headers: authHeaders(authToken) }, LOCALBUDDY_TIMEOUT_MS);
1911
+ async function probeLocalBuddy(localAgentUrl) {
1912
+ return await fetchJsonWithTimeout(`${localAgentUrl}/healthz`, {}, LOCALBUDDY_TIMEOUT_MS);
1693
1913
  }
1694
1914
  async function autoStartRuntimeServices(opts) {
1695
1915
  const { runtimePreflight } = opts.preparedRuntime;
1696
1916
  const runtimeRoot = opts.preparedRuntime.runtimeRoot;
1697
1917
  const runtimeTag = opts.preparedRuntime.runtimeTag || await resolveRuntimeReleaseTag(opts.requestedRuntimeTag);
1918
+ const localBuddyEnabled = Boolean(runtimePreflight.config.localbuddy.enabled);
1919
+ const requireLocalBuddy = opts.requireLocalBuddy ?? true;
1698
1920
  console.log(`[pushpals] LocalBuddy unavailable. Auto-starting runtime for repo: ${opts.repoRoot}`);
1699
1921
  console.log(`[pushpals] runtimeRoot=${runtimeRoot}`);
1700
1922
  console.log(`[pushpals] runtimeTag=${runtimeTag}`);
@@ -1750,10 +1972,14 @@ ${tail}` : ""}`);
1750
1972
  } else {
1751
1973
  console.log("[pushpals] Server already healthy; skipping embedded server start.");
1752
1974
  }
1753
- console.log("[pushpals] Starting embedded LocalBuddy...");
1754
- const localbuddyService = spawnRuntimeService("localbuddy", [runtimeBinaries.localbuddy], opts.repoRoot, runtimeEnv, logPathFor("localbuddy"));
1755
- services.push(localbuddyService);
1756
- console.log(`[pushpals] localbuddy log: ${localbuddyService.logPath}`);
1975
+ if (localBuddyEnabled) {
1976
+ console.log("[pushpals] Starting embedded LocalBuddy...");
1977
+ const localbuddyService = spawnRuntimeService("localbuddy", [runtimeBinaries.localbuddy], opts.repoRoot, runtimeEnv, logPathFor("localbuddy"));
1978
+ services.push(localbuddyService);
1979
+ console.log(`[pushpals] localbuddy log: ${localbuddyService.logPath}`);
1980
+ } else {
1981
+ console.log("[pushpals] Embedded LocalBuddy disabled by runtime config; skipping start.");
1982
+ }
1757
1983
  console.log("[pushpals] Starting embedded RemoteBuddy...");
1758
1984
  const remotebuddyService = spawnRuntimeService("remotebuddy", [runtimeBinaries.remotebuddy], opts.repoRoot, runtimeEnv, logPathFor("remotebuddy"));
1759
1985
  services.push(remotebuddyService);
@@ -1803,8 +2029,8 @@ ${tail2}`);
1803
2029
  ${tail}` : ""}`);
1804
2030
  }
1805
2031
  }
1806
- const health = await probeLocalBuddy(opts.localAgentUrl, opts.authToken);
1807
- if (health?.ok) {
2032
+ const health = localBuddyEnabled ? await probeLocalBuddy(opts.localAgentUrl) : null;
2033
+ if (!requireLocalBuddy || localBuddyEnabled && health?.ok) {
1808
2034
  const stabilityDeadline = Date.now() + DEFAULT_SERVICE_STABILITY_GRACE_MS;
1809
2035
  while (Date.now() < stabilityDeadline) {
1810
2036
  for (let i = services.length - 1;i >= 0; i--) {
@@ -1835,13 +2061,16 @@ ${tail}` : ""}`);
1835
2061
  await Bun.sleep(DEFAULT_RUNTIME_BOOT_POLL_MS);
1836
2062
  }
1837
2063
  stopRuntimeServices(services);
2064
+ if (!localBuddyEnabled) {
2065
+ throw new Error(`Timed out waiting for embedded runtime readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
2066
+ }
1838
2067
  throw new Error(`Timed out waiting for LocalBuddy at ${opts.localAgentUrl} after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
1839
2068
  }
1840
2069
  function readCliState(pathValue) {
1841
- if (!existsSync3(pathValue))
2070
+ if (!existsSync4(pathValue))
1842
2071
  return {};
1843
2072
  try {
1844
- const raw = readFileSync3(pathValue, "utf8");
2073
+ const raw = readFileSync4(pathValue, "utf8");
1845
2074
  const parsed = JSON.parse(raw);
1846
2075
  if (!parsed || typeof parsed !== "object")
1847
2076
  return {};
@@ -1867,6 +2096,9 @@ function writeCliState(pathValue, state) {
1867
2096
  writeFileSync(pathValue, `${JSON.stringify(payload, null, 2)}
1868
2097
  `, "utf8");
1869
2098
  }
2099
+ function resolveCliStatePath(repoRoot) {
2100
+ return resolveGitStateFilePath(repoRoot, "pushpals-cli-state.json");
2101
+ }
1870
2102
  async function looksLikeMonitoringHub(url) {
1871
2103
  try {
1872
2104
  const response = await fetchWithTimeout(url, {}, 700);
@@ -1882,6 +2114,93 @@ async function looksLikeMonitoringHub(url) {
1882
2114
  return false;
1883
2115
  }
1884
2116
  }
2117
+ function buildMonitoringHubRuntimeBootstrap(opts) {
2118
+ return {
2119
+ serverUrl: opts.serverUrl,
2120
+ localAgentUrl: opts.localAgentUrl,
2121
+ sessionId: opts.sessionId,
2122
+ clientId: `cli-monitor-${opts.sessionId}`,
2123
+ clientKind: "cli_monitor",
2124
+ clientLabel: "CLI Monitor"
2125
+ };
2126
+ }
2127
+ function injectMonitoringHubBootstrap(html, bootstrap) {
2128
+ const payload = jsonHtmlBootstrap(bootstrap);
2129
+ const script = `<script>globalThis.__PUSHPALS_WEB_BOOTSTRAP__=${payload};</script>`;
2130
+ if (html.includes("</head>")) {
2131
+ return html.replace("</head>", `${script}</head>`);
2132
+ }
2133
+ return `${script}${html}`;
2134
+ }
2135
+ function monitoringHubContentType(pathValue) {
2136
+ switch (extname(pathValue).toLowerCase()) {
2137
+ case ".html":
2138
+ return "text/html; charset=utf-8";
2139
+ case ".js":
2140
+ return "application/javascript; charset=utf-8";
2141
+ case ".css":
2142
+ return "text/css; charset=utf-8";
2143
+ case ".json":
2144
+ return "application/json; charset=utf-8";
2145
+ case ".svg":
2146
+ return "image/svg+xml";
2147
+ case ".png":
2148
+ return "image/png";
2149
+ case ".jpg":
2150
+ case ".jpeg":
2151
+ return "image/jpeg";
2152
+ case ".ico":
2153
+ return "image/x-icon";
2154
+ case ".woff2":
2155
+ return "font/woff2";
2156
+ case ".ttf":
2157
+ return "font/ttf";
2158
+ case ".map":
2159
+ return "application/json; charset=utf-8";
2160
+ default:
2161
+ return "application/octet-stream";
2162
+ }
2163
+ }
2164
+ function resolveMonitoringHubAssetPath(assetRoot, pathname) {
2165
+ const root = resolve4(assetRoot);
2166
+ const rootPrefix = `${root}${root.endsWith("\\") || root.endsWith("/") ? "" : process.platform === "win32" ? "\\" : "/"}`;
2167
+ const decodedPath = decodeURIComponent(pathname);
2168
+ const trimmedPath = decodedPath === "/" ? "/index.html" : decodedPath;
2169
+ const relativePath = trimmedPath.replace(/^\/+/, "");
2170
+ const candidatePath = resolve4(root, relativePath);
2171
+ if (candidatePath !== root && !candidatePath.startsWith(rootPrefix))
2172
+ return null;
2173
+ if (existsSync4(candidatePath))
2174
+ return candidatePath;
2175
+ if (!extname(relativePath)) {
2176
+ const nestedIndexPath = resolve4(root, relativePath, "index.html");
2177
+ if ((nestedIndexPath === root || nestedIndexPath.startsWith(rootPrefix)) && existsSync4(nestedIndexPath)) {
2178
+ return nestedIndexPath;
2179
+ }
2180
+ return join2(root, "index.html");
2181
+ }
2182
+ return null;
2183
+ }
2184
+ async function serveBundledMonitoringHub(assetRoot, pathname, bootstrap) {
2185
+ const assetPath = resolveMonitoringHubAssetPath(assetRoot, pathname);
2186
+ if (!assetPath || !existsSync4(assetPath))
2187
+ return null;
2188
+ if (assetPath.endsWith("index.html")) {
2189
+ const html = injectMonitoringHubBootstrap(readFileSync4(assetPath, "utf8"), bootstrap);
2190
+ return new Response(html, {
2191
+ headers: {
2192
+ "content-type": "text/html; charset=utf-8",
2193
+ "cache-control": "no-store"
2194
+ }
2195
+ });
2196
+ }
2197
+ return new Response(Bun.file(assetPath), {
2198
+ headers: {
2199
+ "content-type": monitoringHubContentType(assetPath),
2200
+ "cache-control": "no-store"
2201
+ }
2202
+ });
2203
+ }
1885
2204
  function buildEmbeddedMonitoringHubHtml(opts) {
1886
2205
  const bootstrap = jsonHtmlBootstrap(opts);
1887
2206
  return `<!doctype html>
@@ -2016,9 +2335,9 @@ function buildEmbeddedMonitoringHubHtml(opts) {
2016
2335
  </body>
2017
2336
  </html>`;
2018
2337
  }
2019
- async function proxyMonitoringHubRequest(serverUrl, authToken, pathValue) {
2338
+ async function proxyMonitoringHubRequest(serverUrl, pathValue) {
2020
2339
  const target = `${serverUrl}${pathValue}`;
2021
- const upstream = await fetchWithTimeout(target, { headers: authHeaders(authToken) }, 1e4);
2340
+ const upstream = await fetchWithTimeout(target, {}, 1e4);
2022
2341
  const body = await upstream.text();
2023
2342
  return new Response(body, {
2024
2343
  status: upstream.status,
@@ -2029,7 +2348,12 @@ async function proxyMonitoringHubRequest(serverUrl, authToken, pathValue) {
2029
2348
  });
2030
2349
  }
2031
2350
  async function startEmbeddedMonitoringHub(opts) {
2032
- const html = buildEmbeddedMonitoringHubHtml({
2351
+ const monitoringHubAssetRoot = opts.assetRoot === undefined ? await ensureBundledMonitoringHubRoot() : opts.assetRoot;
2352
+ if (!monitoringHubAssetRoot || !looksLikeMonitoringHubBuild(monitoringHubAssetRoot)) {
2353
+ console.error("[pushpals] Unified monitoring hub assets are unavailable; build or export the packaged client monitor first.");
2354
+ return null;
2355
+ }
2356
+ const bootstrap = buildMonitoringHubRuntimeBootstrap({
2033
2357
  serverUrl: opts.serverUrl,
2034
2358
  localAgentUrl: opts.localAgentUrl,
2035
2359
  sessionId: opts.sessionId
@@ -2039,32 +2363,33 @@ async function startEmbeddedMonitoringHub(opts) {
2039
2363
  try {
2040
2364
  const server = Bun.serve({
2041
2365
  port,
2366
+ hostname: "127.0.0.1",
2042
2367
  idleTimeout: 30,
2043
2368
  fetch: async (req) => {
2044
2369
  const url = new URL(req.url);
2045
- if (url.pathname === "/") {
2046
- return new Response(html, {
2047
- headers: {
2048
- "content-type": "text/html; charset=utf-8",
2049
- "cache-control": "no-store"
2050
- }
2051
- });
2052
- }
2053
2370
  if (url.pathname === "/healthz") {
2054
- return Response.json({ ok: true, port, serverUrl: opts.serverUrl, sessionId: opts.sessionId });
2371
+ return Response.json({
2372
+ ok: true,
2373
+ port,
2374
+ serverUrl: opts.serverUrl,
2375
+ sessionId: opts.sessionId
2376
+ });
2055
2377
  }
2056
2378
  if (url.pathname === "/api/status") {
2057
- return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/system/status");
2379
+ return await proxyMonitoringHubRequest(opts.serverUrl, "/system/status");
2058
2380
  }
2059
2381
  if (url.pathname === "/api/requests") {
2060
- return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/requests?status=all&limit=20");
2382
+ return await proxyMonitoringHubRequest(opts.serverUrl, "/requests?status=all&limit=20");
2061
2383
  }
2062
2384
  if (url.pathname === "/api/jobs") {
2063
- return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/jobs?status=all&limit=20");
2385
+ return await proxyMonitoringHubRequest(opts.serverUrl, "/jobs?status=all&limit=20");
2064
2386
  }
2065
2387
  if (url.pathname === "/api/completions") {
2066
- return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/completions?status=all&limit=20");
2388
+ return await proxyMonitoringHubRequest(opts.serverUrl, "/completions?status=all&limit=20");
2067
2389
  }
2390
+ const bundledResponse = await serveBundledMonitoringHub(monitoringHubAssetRoot, url.pathname, bootstrap);
2391
+ if (bundledResponse)
2392
+ return bundledResponse;
2068
2393
  return new Response("Not found", { status: 404 });
2069
2394
  }
2070
2395
  });
@@ -2081,10 +2406,13 @@ async function startEmbeddedMonitoringHub(opts) {
2081
2406
  async function resolveMonitoringHub(opts) {
2082
2407
  const explicit = normalizeUrl(opts.preferredUrl);
2083
2408
  if (explicit) {
2084
- if (await looksLikeMonitoringHub(explicit)) {
2409
+ if (!isLoopbackUrl(explicit)) {
2410
+ console.warn(`[pushpals] Preferred monitoring hub ${explicit} is not local; ignoring it and starting a local monitor instead.`);
2411
+ } else if (await looksLikeMonitoringHub(explicit)) {
2085
2412
  return { url: explicit, port: 0, stop: () => {}, embedded: false };
2413
+ } else {
2414
+ console.warn(`[pushpals] Preferred monitoring hub ${explicit} is unavailable; starting embedded monitor instead.`);
2086
2415
  }
2087
- console.warn(`[pushpals] Preferred monitoring hub ${explicit} is unavailable; starting embedded monitor instead.`);
2088
2416
  }
2089
2417
  for (let port = opts.fallbackPort;port < opts.fallbackPort + MONITOR_SCAN_PORTS; port++) {
2090
2418
  const candidate = `http://127.0.0.1:${port}`;
@@ -2204,12 +2532,11 @@ function formatSessionEventLine(event) {
2204
2532
  }
2205
2533
  return null;
2206
2534
  }
2207
- async function runSessionStream(serverUrl, sessionId, authToken, print, signal) {
2535
+ async function runSessionStream(serverUrl, sessionId, client, print, signal) {
2208
2536
  let cursor = 0;
2209
2537
  while (!signal.aborted) {
2210
- const headers = authHeaders(authToken);
2211
2538
  try {
2212
- const response = await fetchWithTimeout(`${serverUrl}/sessions/${encodeURIComponent(sessionId)}/events${cursor > 0 ? `?after=${cursor}` : ""}`, { headers }, 15000);
2539
+ const response = await fetchWithTimeout(`${serverUrl}/sessions/${encodeURIComponent(sessionId)}/events${buildClientTransportQuery(cursor, client)}`, {}, 15000);
2213
2540
  if (!response.ok || !response.body) {
2214
2541
  print(`[pushpals] Session stream unavailable: HTTP ${response.status}`);
2215
2542
  await Bun.sleep(SSE_RECONNECT_MS);
@@ -2286,6 +2613,10 @@ async function openMonitoringHub(url) {
2286
2613
  const code = await proc.exited;
2287
2614
  return code === 0;
2288
2615
  }
2616
+ function isCliExitCommand(text) {
2617
+ const normalized = String(text ?? "").trim().toLowerCase();
2618
+ return normalized === "/exit" || normalized === "/quit" || normalized === "exit" || normalized === "quit";
2619
+ }
2289
2620
  async function main() {
2290
2621
  const argv = process.argv.slice(2);
2291
2622
  logCliInvocation(argv);
@@ -2319,10 +2650,9 @@ async function main() {
2319
2650
  process.exit(1);
2320
2651
  }
2321
2652
  const config = preparedRuntime.runtimePreflight.config;
2322
- const serverUrl = normalizeUrl(parsed.serverUrl ?? process.env.PUSHPALS_SERVER_URL, config.server.url);
2323
- const localAgentUrl = normalizeUrl(parsed.localAgentUrl ?? process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, config.client.localAgentUrl);
2653
+ const serverUrl = normalizeLoopbackUrl(parsed.serverUrl ?? process.env.PUSHPALS_SERVER_URL, config.server.url);
2654
+ const localAgentUrl = normalizeLoopbackUrl(parsed.localAgentUrl ?? process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, config.client.localAgentUrl);
2324
2655
  const sessionId = String(parsed.sessionId ?? process.env.PUSHPALS_SESSION_ID ?? config.sessionId).trim();
2325
- const authToken = config.authToken;
2326
2656
  let autoStartedServices = [];
2327
2657
  const stopAutoStartedServices = () => {
2328
2658
  if (autoStartedServices.length === 0)
@@ -2330,8 +2660,10 @@ async function main() {
2330
2660
  stopRuntimeServices(autoStartedServices);
2331
2661
  autoStartedServices = [];
2332
2662
  };
2333
- let health = await probeLocalBuddy(localAgentUrl, authToken);
2334
- if (!health?.ok && !parsed.noAutoStart) {
2663
+ let serverHealthy = await probeServer(serverUrl);
2664
+ let health = await probeLocalBuddy(localAgentUrl);
2665
+ const runtimeNeedsAutoStart = parsed.runtimeOnly ? !serverHealthy : !health?.ok;
2666
+ if (runtimeNeedsAutoStart && !parsed.noAutoStart) {
2335
2667
  try {
2336
2668
  autoStartedServices = await autoStartRuntimeServices({
2337
2669
  repoRoot,
@@ -2339,45 +2671,58 @@ async function main() {
2339
2671
  localAgentUrl,
2340
2672
  sourceControlManagerPort: config.sourceControlManager.port,
2341
2673
  sourceControlManagerRemote: config.sourceControlManager.remote,
2342
- authToken,
2343
2674
  preparedRuntime,
2344
- requestedRuntimeTag: parsed.runtimeTag
2675
+ requestedRuntimeTag: parsed.runtimeTag,
2676
+ requireLocalBuddy: !parsed.runtimeOnly
2345
2677
  });
2346
- health = await probeLocalBuddy(localAgentUrl, authToken);
2678
+ serverHealthy = await probeServer(serverUrl);
2679
+ health = await probeLocalBuddy(localAgentUrl);
2347
2680
  } catch (err) {
2348
2681
  console.error(`[pushpals] Auto-start failed: ${String(err)}`);
2349
2682
  stopAutoStartedServices();
2350
2683
  }
2351
2684
  }
2352
- if (!health?.ok) {
2353
- console.error(`[pushpals] LocalBuddy is unavailable at ${localAgentUrl}.`);
2685
+ if (parsed.runtimeOnly && !serverHealthy) {
2686
+ console.error(`[pushpals] Server is unavailable at ${serverUrl}.`);
2354
2687
  if (parsed.noAutoStart) {
2355
2688
  console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
2356
2689
  } else {
2357
- console.error("[pushpals] Auto-start could not bring LocalBuddy online.");
2690
+ console.error("[pushpals] Auto-start could not bring the embedded runtime online.");
2358
2691
  }
2359
2692
  process.exit(1);
2360
2693
  }
2361
- const localBuddyRepo = health.repo ? resolve3(health.repo) : "";
2362
- if (!localBuddyRepo) {
2363
- stopAutoStartedServices();
2364
- console.error("[pushpals] LocalBuddy health response did not include repo path.");
2365
- process.exit(1);
2366
- }
2367
- if (normalizePath(localBuddyRepo) !== normalizePath(repoRoot)) {
2368
- stopAutoStartedServices();
2369
- console.error("[pushpals] Repo mismatch detected.");
2370
- console.error(`[pushpals] currentRepo=${repoRoot}`);
2371
- console.error(`[pushpals] localBuddyRepo=${localBuddyRepo}`);
2372
- console.error("[pushpals] LocalBuddy must run against the same repo. Start PushPals from this repo and retry.");
2694
+ if (!parsed.runtimeOnly && !health?.ok) {
2695
+ console.error(`[pushpals] LocalBuddy is unavailable at ${localAgentUrl}.`);
2696
+ if (parsed.noAutoStart) {
2697
+ console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
2698
+ } else {
2699
+ console.error("[pushpals] Auto-start could not bring LocalBuddy online.");
2700
+ }
2373
2701
  process.exit(1);
2374
2702
  }
2375
- const localBuddySessionId = health.sessionId && String(health.sessionId).trim() ? String(health.sessionId).trim() : sessionId;
2376
- if (sessionId && sessionId !== localBuddySessionId) {
2377
- console.warn(`[pushpals] Requested sessionId=${sessionId}, but LocalBuddy is currently attached to sessionId=${localBuddySessionId}.`);
2703
+ let localBuddySessionId = sessionId;
2704
+ if (!parsed.runtimeOnly) {
2705
+ const localBuddyRepo = health?.repo ? resolve4(health.repo) : "";
2706
+ if (!localBuddyRepo) {
2707
+ stopAutoStartedServices();
2708
+ console.error("[pushpals] LocalBuddy health response did not include repo path.");
2709
+ process.exit(1);
2710
+ }
2711
+ if (normalizePath(localBuddyRepo) !== normalizePath(repoRoot)) {
2712
+ stopAutoStartedServices();
2713
+ console.error("[pushpals] Repo mismatch detected.");
2714
+ console.error(`[pushpals] currentRepo=${repoRoot}`);
2715
+ console.error(`[pushpals] localBuddyRepo=${localBuddyRepo}`);
2716
+ console.error("[pushpals] LocalBuddy must run against the same repo. Start PushPals from this repo and retry.");
2717
+ process.exit(1);
2718
+ }
2719
+ localBuddySessionId = health?.sessionId && String(health.sessionId).trim() ? String(health.sessionId).trim() : sessionId;
2720
+ if (sessionId && sessionId !== localBuddySessionId) {
2721
+ console.warn(`[pushpals] Requested sessionId=${sessionId}, but LocalBuddy is currently attached to sessionId=${localBuddySessionId}.`);
2722
+ }
2378
2723
  }
2379
- const statePath = resolve3(repoRoot, ".git", "pushpals-cli-state.json");
2380
- const saved = readCliState(statePath);
2724
+ const statePath = resolveCliStatePath(repoRoot);
2725
+ const saved = statePath ? readCliState(statePath) : {};
2381
2726
  const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
2382
2727
  const monitorPort = parsePositiveInt(process.env.PUSHPALS_CLIENT_PORT, DEFAULT_MONITOR_PORT);
2383
2728
  const monitoringHub = await resolveMonitoringHub({
@@ -2385,32 +2730,48 @@ async function main() {
2385
2730
  fallbackPort: monitorPort,
2386
2731
  serverUrl,
2387
2732
  localAgentUrl,
2388
- sessionId: localBuddySessionId,
2389
- authToken
2733
+ sessionId: localBuddySessionId
2390
2734
  });
2391
2735
  const monitoringHubUrl = monitoringHub?.url ?? "";
2392
- writeCliState(statePath, {
2393
- monitoringHubUrl: monitoringHubUrl || undefined,
2394
- serverUrl,
2395
- localAgentUrl,
2396
- sessionId: localBuddySessionId,
2397
- repoRoot
2398
- });
2736
+ if (statePath) {
2737
+ writeCliState(statePath, {
2738
+ monitoringHubUrl: monitoringHubUrl || undefined,
2739
+ serverUrl,
2740
+ localAgentUrl,
2741
+ sessionId: localBuddySessionId,
2742
+ repoRoot
2743
+ });
2744
+ } else {
2745
+ console.warn("[pushpals] Could not resolve git metadata dir; skipping CLI state persistence.");
2746
+ }
2399
2747
  console.log("[pushpals] Connected.");
2400
2748
  if (monitoringHubUrl) {
2401
- console.log(`monitoringHubUrl=${monitoringHubUrl}`);
2749
+ console.log(`[pushpals] monitoringHubUrl=${monitoringHubUrl}`);
2402
2750
  if (monitoringHub?.embedded) {
2403
2751
  console.log("[pushpals] Embedded monitoring hub is running.");
2404
2752
  }
2405
2753
  } else {
2406
- console.log("monitoringHubUrl=unavailable");
2407
- }
2408
- console.log(`serverUrl=${serverUrl}`);
2409
- console.log(`localAgentUrl=${localAgentUrl}`);
2410
- console.log(`sessionId=${localBuddySessionId}`);
2411
- console.log(`repoRoot=${repoRoot}`);
2412
- console.log(`cliStateFile=${statePath}`);
2413
- console.log("[pushpals] Type a message and press Enter. Use /exit to quit.");
2754
+ console.log("[pushpals] monitoringHubUrl=unavailable");
2755
+ }
2756
+ console.log(`[pushpals] serverUrl=${serverUrl}`);
2757
+ console.log(`[pushpals] localAgentUrl=${localAgentUrl}`);
2758
+ console.log(`[pushpals] sessionId=${localBuddySessionId}`);
2759
+ console.log(`[pushpals] repoRoot=${repoRoot}`);
2760
+ console.log(`[pushpals] cliStateFile=${statePath ?? "unavailable"}`);
2761
+ if (parsed.runtimeOnly) {
2762
+ console.log("[pushpals] runtimeOnly=true");
2763
+ } else {
2764
+ console.log("[pushpals] Type a message and press Enter. Use /exit or exit to quit.");
2765
+ }
2766
+ const cliVersion = String(process.env.PUSHPALS_CLI_PACKAGE_VERSION ?? "").trim() || "unknown";
2767
+ const cliClient = {
2768
+ clientId: createRuntimeClientId("cli"),
2769
+ kind: "cli",
2770
+ label: "CLI",
2771
+ version: cliVersion,
2772
+ platform: `${process.platform}/${process.arch}`,
2773
+ repoRoot
2774
+ };
2414
2775
  const streamAbort = new AbortController;
2415
2776
  let rl = null;
2416
2777
  const printIncoming = (line) => {
@@ -2425,23 +2786,60 @@ ${line}
2425
2786
  }
2426
2787
  console.log(line);
2427
2788
  };
2428
- const streamTask = parsed.noStream ? Promise.resolve() : runSessionStream(serverUrl, localBuddySessionId, authToken, printIncoming, streamAbort.signal);
2789
+ const streamTask = parsed.noStream ? Promise.resolve() : parsed.runtimeOnly ? Promise.resolve() : runSessionStream(serverUrl, localBuddySessionId, cliClient, printIncoming, streamAbort.signal);
2429
2790
  let shuttingDown = false;
2430
2791
  const requestStop = () => {
2431
2792
  if (shuttingDown)
2432
2793
  return;
2433
2794
  shuttingDown = true;
2795
+ console.log("[pushpals] Shutting down CLI session...");
2434
2796
  streamAbort.abort();
2435
2797
  if (rl)
2436
2798
  rl.close();
2437
2799
  try {
2438
2800
  monitoringHub?.stop();
2439
2801
  } catch {}
2802
+ if (autoStartedServices.length > 0) {
2803
+ console.log("[pushpals] Stopping embedded runtime services...");
2804
+ }
2440
2805
  stopAutoStartedServices();
2441
2806
  };
2442
2807
  process.once("SIGINT", requestStop);
2443
2808
  process.once("SIGTERM", requestStop);
2444
2809
  process.once("exit", requestStop);
2810
+ if (parsed.runtimeOnly) {
2811
+ console.log("[pushpals] Runtime-only mode is active. Send `exit` on stdin or terminate the process to stop.");
2812
+ await new Promise((resolveStop) => {
2813
+ let resolved = false;
2814
+ const finish = () => {
2815
+ if (resolved)
2816
+ return;
2817
+ resolved = true;
2818
+ resolveStop();
2819
+ };
2820
+ process.once("SIGINT", finish);
2821
+ process.once("SIGTERM", finish);
2822
+ const runtimeOnlyInput = createInterface({
2823
+ input: process.stdin,
2824
+ output: process.stdout,
2825
+ terminal: false
2826
+ });
2827
+ runtimeOnlyInput.on("line", (line) => {
2828
+ if (!isCliExitCommand(line))
2829
+ return;
2830
+ requestStop();
2831
+ runtimeOnlyInput.close();
2832
+ finish();
2833
+ });
2834
+ runtimeOnlyInput.on("close", () => {
2835
+ requestStop();
2836
+ finish();
2837
+ });
2838
+ });
2839
+ requestStop();
2840
+ await Promise.race([streamTask, Bun.sleep(2000)]);
2841
+ return;
2842
+ }
2445
2843
  rl = createInterface({
2446
2844
  input: process.stdin,
2447
2845
  output: process.stdout,
@@ -2455,21 +2853,21 @@ ${line}
2455
2853
  rl.prompt();
2456
2854
  continue;
2457
2855
  }
2458
- if (text === "/exit" || text === "/quit") {
2856
+ if (isCliExitCommand(text)) {
2459
2857
  requestStop();
2460
2858
  break;
2461
2859
  }
2462
2860
  if (text === "/hub") {
2463
- console.log(monitoringHubUrl ? `monitoringHubUrl=${monitoringHubUrl}` : "monitoringHubUrl=unavailable");
2861
+ console.log(monitoringHubUrl ? `[pushpals] monitoringHubUrl=${monitoringHubUrl}` : "[pushpals] monitoringHubUrl=unavailable");
2464
2862
  rl.prompt();
2465
2863
  continue;
2466
2864
  }
2467
2865
  if (text === "/status") {
2468
- console.log(`serverUrl=${serverUrl}`);
2469
- console.log(`localAgentUrl=${localAgentUrl}`);
2470
- console.log(`sessionId=${localBuddySessionId}`);
2471
- console.log(`repoRoot=${repoRoot}`);
2472
- console.log(monitoringHubUrl ? `monitoringHubUrl=${monitoringHubUrl}` : "monitoringHubUrl=unavailable");
2866
+ console.log(`[pushpals] serverUrl=${serverUrl}`);
2867
+ console.log(`[pushpals] localAgentUrl=${localAgentUrl}`);
2868
+ console.log(`[pushpals] sessionId=${localBuddySessionId}`);
2869
+ console.log(`[pushpals] repoRoot=${repoRoot}`);
2870
+ console.log(monitoringHubUrl ? `[pushpals] monitoringHubUrl=${monitoringHubUrl}` : "[pushpals] monitoringHubUrl=unavailable");
2473
2871
  rl.prompt();
2474
2872
  continue;
2475
2873
  }
@@ -2502,9 +2900,14 @@ if (import.meta.main) {
2502
2900
  export {
2503
2901
  startEmbeddedMonitoringHub,
2504
2902
  resolveCommandPath,
2903
+ resolveCliStatePath,
2505
2904
  resolveBundledRuntimeAssetSource,
2905
+ resolveBundledMonitoringHubRoot,
2506
2906
  prepareCliRuntime,
2507
2907
  normalizeChildProcessEnv,
2908
+ isCliExitCommand,
2909
+ injectMonitoringHubBootstrap,
2910
+ formatTimestampedCliLine,
2508
2911
  buildServiceStopCommand,
2509
2912
  buildOpenMonitoringHubCommand,
2510
2913
  buildEmbeddedRuntimeEnv,