@pushpalsdev/cli 1.0.9 → 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.
@@ -6,12 +6,12 @@ import {
6
6
  appendFileSync,
7
7
  chmodSync,
8
8
  cpSync,
9
- existsSync as existsSync3,
9
+ existsSync as existsSync4,
10
10
  mkdirSync,
11
- readFileSync as readFileSync3,
11
+ readFileSync as readFileSync4,
12
12
  writeFileSync
13
13
  } from "fs";
14
- import { dirname, extname, join as join2, resolve as resolve3 } from "path";
14
+ import { dirname, extname, join as join2, resolve as resolve4 } from "path";
15
15
  import { createInterface } from "readline";
16
16
 
17
17
  // ../shared/src/client_preflight.ts
@@ -21,12 +21,53 @@ import { relative, resolve as resolve2 } from "path";
21
21
  // ../shared/src/config.ts
22
22
  import { existsSync, readFileSync } from "fs";
23
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
24
64
  var PROJECT_ROOT = resolve(import.meta.dir, "..", "..", "..");
25
65
  var DEFAULT_CONFIG_DIR = "configs";
26
66
  var LEGACY_CONFIG_DIR = "config";
27
67
  var TRUTHY = new Set(["1", "true", "yes", "on"]);
28
68
  var FALSY = new Set(["0", "false", "no", "off"]);
29
69
  var DEFAULT_WORKERPALS_QUALITY_CRITIC_MIN_SCORE = 8;
70
+ var DEFAULT_WORKERPALS_QUALITY_MAX_AUTO_REVISIONS = 1;
30
71
  var DEFAULT_WORKERPALS_FILE_MODIFYING_JOBS = ["task.execute"];
31
72
  var DEFAULT_WORKERPALS_OUTPUT_MAX_CHARS = 192 * 1024;
32
73
  var DEFAULT_WORKERPALS_OUTPUT_MAX_LINES = 600;
@@ -294,13 +335,14 @@ function loadPushPalsConfig(options = {}) {
294
335
  const remotebuddyDbPath = resolvePathFromRoot(projectRoot, firstNonEmpty(process.env.REMOTEBUDDY_DB_PATH, asString(pathsNode.remotebuddy_db_path, join(dataDir, "remotebuddy-state.db"))));
295
336
  const serverNode = getObject(merged, "server");
296
337
  const serverPort = Math.max(1, asInt(parseIntEnv("PUSHPALS_PORT") ?? serverNode.port, 3001));
297
- const serverUrl = firstNonEmpty(process.env.PUSHPALS_SERVER_URL, asString(serverNode.url, `http://localhost:${serverPort}`), `http://localhost:${serverPort}`);
298
- 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")));
299
340
  const debugHttp = parseBoolEnv("PUSHPALS_DEBUG_HTTP") ?? asBoolean(serverNode.debug_http, false);
300
341
  const staleClaimTtlMs = Math.max(5000, asInt(parseIntEnv("PUSHPALS_STALE_CLAIM_TTL_MS") ?? serverNode.stale_claim_ttl_ms, 120000));
301
342
  const staleClaimSweepIntervalMs = Math.max(1000, asInt(parseIntEnv("PUSHPALS_STALE_CLAIM_SWEEP_INTERVAL_MS") ?? serverNode.stale_claim_sweep_interval_ms, 5000));
302
343
  const globalStatusHeartbeatMs = parseIntEnv("PUSHPALS_STATUS_HEARTBEAT_MS");
303
344
  const localNode = getObject(merged, "localbuddy");
345
+ const localEnabled = parseBoolEnv("LOCALBUDDY_ENABLED") ?? asBoolean(localNode.enabled, false);
304
346
  const localPort = Math.max(1, asInt(parseIntEnv("LOCAL_AGENT_PORT") ?? localNode.port, 3003));
305
347
  const localStatusHeartbeatMs = Math.max(0, asInt(parseIntEnv("LOCALBUDDY_STATUS_HEARTBEAT_MS") ?? globalStatusHeartbeatMs ?? localNode.status_heartbeat_ms, 120000));
306
348
  const localLlm = resolveLlmConfig(localNode, "LOCALBUDDY", {
@@ -377,7 +419,7 @@ function loadPushPalsConfig(options = {}) {
377
419
  const workerMinisweTimeoutMs = Math.max(1e4, asInt(parseIntEnv("WORKERPALS_MINISWE_TIMEOUT_MS") ?? workerNode.miniswe_timeout_ms, 1800000));
378
420
  const workerOpenAICodexPython = firstNonEmpty(process.env.PUSHPALS_OPENAI_CODEX_PYTHON, asString(workerNode.openai_codex_python, "python"), "python");
379
421
  const workerOpenAICodexTimeoutMs = Math.max(1e4, asInt(workerNode.openai_codex_timeout_ms, 7200000));
380
- 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)));
381
423
  const workerFileModifyingJobs = (() => {
382
424
  const envRaw = firstNonEmpty(process.env.WORKERPALS_FILE_MODIFYING_JOBS);
383
425
  const parsed = envRaw ? envRaw.split(",").map((entry) => entry.trim()).filter(Boolean) : asStringArray(workerNode.file_modifying_jobs);
@@ -539,6 +581,7 @@ function loadPushPalsConfig(options = {}) {
539
581
  staleClaimSweepIntervalMs
540
582
  },
541
583
  localbuddy: {
584
+ enabled: localEnabled,
542
585
  port: localPort,
543
586
  statusHeartbeatMs: localStatusHeartbeatMs,
544
587
  llm: localLlm
@@ -742,7 +785,7 @@ function loadPushPalsConfig(options = {}) {
742
785
  portConflictPolicy: startupPortConflictPolicy
743
786
  },
744
787
  client: {
745
- 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),
746
789
  traceTailLines: Math.max(10, asInt(parseIntEnv("EXPO_PUBLIC_PUSHPALS_TRACE_TAIL_LINES") ?? clientNode.trace_tail_lines, 100))
747
790
  }
748
791
  };
@@ -1061,6 +1104,46 @@ function formatClientRuntimePreflightLines(result, prefix) {
1061
1104
  return lines;
1062
1105
  }
1063
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
+
1064
1147
  // ../../scripts/pushpals-cli.ts
1065
1148
  var DEFAULT_MONITOR_PORT = 8081;
1066
1149
  var MONITOR_SCAN_PORTS = 32;
@@ -1081,6 +1164,29 @@ var GITHUB_HEADERS = {
1081
1164
  "User-Agent": "pushpals-cli"
1082
1165
  };
1083
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();
1084
1190
  function logCliInvocation(argv) {
1085
1191
  const startedAt = new Date().toISOString();
1086
1192
  const cliVersion = String(process.env.PUSHPALS_CLI_PACKAGE_VERSION ?? "").trim() || "unknown";
@@ -1106,6 +1212,7 @@ function printUsage() {
1106
1212
  console.log(" --runtime-tag <tag> Override runtime release tag (e.g. v1.0.2)");
1107
1213
  console.log(" --no-auto-start Disable runtime auto-start when LocalBuddy is down");
1108
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");
1109
1216
  console.log(" -h, --help Show this help");
1110
1217
  console.log("");
1111
1218
  console.log("Chat commands:");
@@ -1120,7 +1227,7 @@ function printUsage() {
1120
1227
  console.log(" - LocalBuddy must be attached to the same repo root.");
1121
1228
  }
1122
1229
  function parseArgs(argv) {
1123
- const options = { noAutoStart: false, noStream: false };
1230
+ const options = { noAutoStart: false, noStream: false, runtimeOnly: false };
1124
1231
  for (let i = 0;i < argv.length; i++) {
1125
1232
  const arg = argv[i];
1126
1233
  if (arg === "-h" || arg === "--help") {
@@ -1135,6 +1242,10 @@ function parseArgs(argv) {
1135
1242
  options.noAutoStart = true;
1136
1243
  continue;
1137
1244
  }
1245
+ if (arg === "--runtime-only") {
1246
+ options.runtimeOnly = true;
1247
+ continue;
1248
+ }
1138
1249
  if (arg === "--server-url") {
1139
1250
  options.serverUrl = argv[++i];
1140
1251
  continue;
@@ -1170,6 +1281,30 @@ function normalizeUrl(value, fallback = "") {
1170
1281
  const selected = text || fallback;
1171
1282
  return selected.replace(/\/+$/, "");
1172
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
+ }
1173
1308
  function parsePositiveInt(value, fallback) {
1174
1309
  const parsed = Number.parseInt(String(value ?? "").trim(), 10);
1175
1310
  if (!Number.isFinite(parsed) || parsed <= 0)
@@ -1177,7 +1312,7 @@ function parsePositiveInt(value, fallback) {
1177
1312
  return parsed;
1178
1313
  }
1179
1314
  function normalizePath(value) {
1180
- const normalized = resolve3(value).replace(/\\/g, "/").replace(/\/+$/, "");
1315
+ const normalized = resolve4(value).replace(/\\/g, "/").replace(/\/+$/, "");
1181
1316
  if (process.platform === "win32")
1182
1317
  return normalized.toLowerCase();
1183
1318
  return normalized;
@@ -1205,11 +1340,11 @@ async function resolveCurrentGitRepoRoot(cwd) {
1205
1340
  const root = await runGit(["rev-parse", "--show-toplevel"], cwd);
1206
1341
  if (!root.ok || !root.stdout)
1207
1342
  return null;
1208
- return resolve3(root.stdout);
1343
+ return resolve4(root.stdout);
1209
1344
  }
1210
1345
  function resolveDefaultRuntimeRoot() {
1211
1346
  const home = process.env.USERPROFILE || process.env.HOME || process.cwd();
1212
- return resolve3(home, ".pushpals", "runtime");
1347
+ return resolve4(home, ".pushpals", "runtime");
1213
1348
  }
1214
1349
  function buildRuntimeAssetSource(root, protocolSchemasDir) {
1215
1350
  return {
@@ -1222,13 +1357,13 @@ function buildRuntimeAssetSource(root, protocolSchemasDir) {
1222
1357
  };
1223
1358
  }
1224
1359
  function isCompleteRuntimeAssetSource(source) {
1225
- 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"));
1226
1361
  }
1227
1362
  function resolveBundledRuntimeAssetSource() {
1228
1363
  const candidates = [
1229
- buildRuntimeAssetSource(resolve3(import.meta.dir, "..", "runtime"), resolve3(import.meta.dir, "..", "runtime", "protocol", "schemas")),
1230
- buildRuntimeAssetSource(resolve3(import.meta.dir, ".."), resolve3(import.meta.dir, "..", "packages", "protocol", "src", "schemas")),
1231
- 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"))
1232
1367
  ];
1233
1368
  for (const candidate of candidates) {
1234
1369
  if (isCompleteRuntimeAssetSource(candidate))
@@ -1237,12 +1372,12 @@ function resolveBundledRuntimeAssetSource() {
1237
1372
  return null;
1238
1373
  }
1239
1374
  function looksLikeMonitoringHubBuild(root) {
1240
- return existsSync3(join2(root, "index.html")) && existsSync3(join2(root, "_expo"));
1375
+ return existsSync4(join2(root, "index.html")) && existsSync4(join2(root, "_expo"));
1241
1376
  }
1242
1377
  function resolveBundledMonitoringHubRoot() {
1243
1378
  const candidates = [
1244
- resolve3(import.meta.dir, "..", "monitor-ui"),
1245
- resolve3(import.meta.dir, "..", "packages", "cli", "monitor-ui")
1379
+ resolve4(import.meta.dir, "..", "monitor-ui"),
1380
+ resolve4(import.meta.dir, "..", "packages", "cli", "monitor-ui")
1246
1381
  ];
1247
1382
  for (const candidate of candidates) {
1248
1383
  if (looksLikeMonitoringHubBuild(candidate))
@@ -1252,12 +1387,12 @@ function resolveBundledMonitoringHubRoot() {
1252
1387
  }
1253
1388
  function resolveCliSourceCheckoutRoot() {
1254
1389
  const candidates = [
1255
- resolve3(import.meta.dir, ".."),
1256
- resolve3(import.meta.dir, "..", ".."),
1257
- resolve3(import.meta.dir, "..", "..", "..")
1390
+ resolve4(import.meta.dir, ".."),
1391
+ resolve4(import.meta.dir, "..", ".."),
1392
+ resolve4(import.meta.dir, "..", "..", "..")
1258
1393
  ];
1259
1394
  for (const candidate of candidates) {
1260
- if (existsSync3(join2(candidate, "package.json")) && existsSync3(join2(candidate, "apps", "client", "app.json")) && existsSync3(join2(candidate, "scripts", "sync-cli-monitor-ui.ts"))) {
1395
+ if (existsSync4(join2(candidate, "package.json")) && existsSync4(join2(candidate, "apps", "client", "app.json")) && existsSync4(join2(candidate, "scripts", "sync-cli-monitor-ui.ts"))) {
1261
1396
  return candidate;
1262
1397
  }
1263
1398
  }
@@ -1287,7 +1422,7 @@ async function ensureBundledMonitoringHubRoot() {
1287
1422
  return resolveBundledMonitoringHubRoot();
1288
1423
  }
1289
1424
  function repoLooksLikePushPalsSourceCheckout(repoRoot) {
1290
- 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"));
1291
1426
  }
1292
1427
  function parseSemverFromPackageVersion(value) {
1293
1428
  const raw = String(value ?? "").trim();
@@ -1325,6 +1460,7 @@ async function resolveRuntimeReleaseTag(explicitTag) {
1325
1460
  if (fromEnv)
1326
1461
  return fromEnv;
1327
1462
  const packageVersion = parseSemverFromPackageVersion(process.env.PUSHPALS_CLI_PACKAGE_VERSION);
1463
+ console.log("[pushpals] Resolving embedded runtime release tag from GitHub...");
1328
1464
  try {
1329
1465
  return await fetchLatestReleaseTag();
1330
1466
  } catch (err) {
@@ -1337,7 +1473,7 @@ async function resolveRuntimeReleaseTag(explicitTag) {
1337
1473
  }
1338
1474
  }
1339
1475
  function writeTextFileIfMissing(pathValue, text) {
1340
- if (existsSync3(pathValue))
1476
+ if (existsSync4(pathValue))
1341
1477
  return;
1342
1478
  mkdirSync(dirname(pathValue), { recursive: true });
1343
1479
  writeFileSync(pathValue, text, "utf8");
@@ -1380,8 +1516,8 @@ function seedRuntimePreflightAssets(runtimeRoot) {
1380
1516
  writeTextFileIfMissing(join2(runtimeRoot, ".env"), `# Local PushPals runtime environment
1381
1517
  `);
1382
1518
  const localExamplePath = join2(runtimeRoot, "configs", "local.example.toml");
1383
- if (existsSync3(localExamplePath)) {
1384
- writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync3(localExamplePath, "utf8"));
1519
+ if (existsSync4(localExamplePath)) {
1520
+ writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync4(localExamplePath, "utf8"));
1385
1521
  } else {
1386
1522
  writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), `# Local PushPals runtime overrides
1387
1523
  `);
@@ -1395,6 +1531,7 @@ async function fetchTextFromUrl(url, timeoutMs = 20000) {
1395
1531
  return await response.text();
1396
1532
  }
1397
1533
  async function downloadRuntimeAssetsFromSourceTag(runtimeRoot, tag) {
1534
+ console.log(`[pushpals] Downloading embedded runtime assets from source tag ${tag}...`);
1398
1535
  const treeUrl = `${GITHUB_API_URL}/git/trees/${encodeURIComponent(tag)}?recursive=1`;
1399
1536
  const treeResponse = await fetchWithTimeout(treeUrl, { headers: GITHUB_HEADERS }, 30000);
1400
1537
  if (!treeResponse.ok) {
@@ -1415,16 +1552,19 @@ async function downloadRuntimeAssetsFromSourceTag(runtimeRoot, tag) {
1415
1552
  }
1416
1553
  }
1417
1554
  async function ensureRuntimeAssets(runtimeRoot, runtimeTag) {
1555
+ console.log(`[pushpals] Preparing embedded runtime assets for ${runtimeTag}...`);
1418
1556
  const markerPath = join2(runtimeRoot, ".runtime-assets-tag");
1419
- const currentTag = existsSync3(markerPath) ? readFileSync3(markerPath, "utf8").trim() : "";
1557
+ const currentTag = existsSync4(markerPath) ? readFileSync4(markerPath, "utf8").trim() : "";
1420
1558
  const protocolSchemasDir = join2(runtimeRoot, "protocol", "schemas");
1421
- const hasProtocolSchemas = existsSync3(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync3(join2(protocolSchemasDir, "events.schema.json"));
1422
- 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;
1423
1561
  if (!hasAssets || currentTag !== runtimeTag) {
1562
+ console.log(`[pushpals] Embedded runtime assets ${hasAssets ? "are stale" : "are missing"}; refreshing bundle...`);
1424
1563
  copyBundledRuntimeAssets(runtimeRoot);
1425
- const hasProtocolSchemasAfterCopy = existsSync3(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync3(join2(protocolSchemasDir, "events.schema.json"));
1426
- 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;
1427
1566
  if (!hasAssetsAfterCopy) {
1567
+ console.log("[pushpals] Bundled runtime assets are incomplete; falling back to release source downloads...");
1428
1568
  await downloadRuntimeAssetsFromSourceTag(runtimeRoot, runtimeTag);
1429
1569
  }
1430
1570
  writeFileSync(markerPath, `${runtimeTag}
@@ -1433,18 +1573,19 @@ async function ensureRuntimeAssets(runtimeRoot, runtimeTag) {
1433
1573
  writeTextFileIfMissing(join2(runtimeRoot, ".env"), `# Local PushPals runtime environment
1434
1574
  `);
1435
1575
  const localExamplePath = join2(runtimeRoot, "configs", "local.example.toml");
1436
- if (existsSync3(localExamplePath)) {
1437
- writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync3(localExamplePath, "utf8"));
1576
+ if (existsSync4(localExamplePath)) {
1577
+ writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync4(localExamplePath, "utf8"));
1438
1578
  } else {
1439
1579
  writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), `# Local PushPals runtime overrides
1440
1580
  `);
1441
1581
  }
1582
+ console.log("[pushpals] Embedded runtime assets are ready.");
1442
1583
  }
1443
1584
  function resolveDeferredRuntimeTagHint(explicitTag) {
1444
1585
  return String(explicitTag || process.env.PUSHPALS_RUNTIME_TAG || "").trim();
1445
1586
  }
1446
1587
  async function prepareCliRuntime(opts) {
1447
- const runtimeRoot = resolve3(opts.runtimeRoot || process.env.PUSHPALS_RUNTIME_ROOT || resolveDefaultRuntimeRoot());
1588
+ const runtimeRoot = resolve4(opts.runtimeRoot || process.env.PUSHPALS_RUNTIME_ROOT || resolveDefaultRuntimeRoot());
1448
1589
  if (repoLooksLikePushPalsSourceCheckout(opts.repoRoot)) {
1449
1590
  return {
1450
1591
  runtimeRoot,
@@ -1551,9 +1692,9 @@ function timestampFileToken() {
1551
1692
  return new Date().toISOString().replace(/[:.]/g, "-");
1552
1693
  }
1553
1694
  function readLogTail(logPath, maxLines = 40) {
1554
- if (!existsSync3(logPath))
1695
+ if (!existsSync4(logPath))
1555
1696
  return "";
1556
- const raw = readFileSync3(logPath, "utf8");
1697
+ const raw = readFileSync4(logPath, "utf8");
1557
1698
  const lines = raw.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.length > 0);
1558
1699
  if (lines.length === 0)
1559
1700
  return "";
@@ -1561,6 +1702,7 @@ function readLogTail(logPath, maxLines = 40) {
1561
1702
  `);
1562
1703
  }
1563
1704
  async function downloadBinaryAsset(tag, assetName, outPath) {
1705
+ console.log(`[pushpals] Downloading embedded runtime binary ${assetName} from ${tag}...`);
1564
1706
  const url = `${GITHUB_RELEASE_URL}/${encodeURIComponent(tag)}/${assetName}`;
1565
1707
  const response = await fetchWithTimeout(url, { headers: GITHUB_HEADERS }, 60000);
1566
1708
  if (!response.ok) {
@@ -1572,6 +1714,7 @@ async function downloadBinaryAsset(tag, assetName, outPath) {
1572
1714
  }
1573
1715
  async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
1574
1716
  const platformKey = resolveRuntimePlatformKey();
1717
+ console.log(`[pushpals] Preparing embedded runtime binaries for ${runtimeTag} (${platformKey})...`);
1575
1718
  const binDir = join2(runtimeRoot, "bin", `${runtimeTag}-${platformKey}`);
1576
1719
  mkdirSync(binDir, { recursive: true });
1577
1720
  const runtimeBinaries = {
@@ -1586,11 +1729,13 @@ async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
1586
1729
  runtimeBinaries.remotebuddy,
1587
1730
  runtimeBinaries.sourceControlManager
1588
1731
  ];
1732
+ let downloadedCount = 0;
1589
1733
  for (const binaryPath of requiredAssets) {
1590
- if (existsSync3(binaryPath))
1734
+ if (existsSync4(binaryPath))
1591
1735
  continue;
1592
1736
  const assetName = binaryPath.split(/[\\/]/).pop() || "";
1593
1737
  await downloadBinaryAsset(runtimeTag, assetName, binaryPath);
1738
+ downloadedCount++;
1594
1739
  }
1595
1740
  if (process.platform !== "win32") {
1596
1741
  for (const binaryPath of requiredAssets) {
@@ -1599,6 +1744,12 @@ async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
1599
1744
  } catch {}
1600
1745
  }
1601
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.");
1602
1753
  return runtimeBinaries;
1603
1754
  }
1604
1755
  function spawnRuntimeService(name, command, cwd, env, logPath) {
@@ -1738,18 +1889,34 @@ async function fetchJsonWithTimeout(url, init = {}, timeoutMs = HTTP_TIMEOUT_MS)
1738
1889
  return null;
1739
1890
  }
1740
1891
  }
1741
- function authHeaders(authToken) {
1742
- if (!authToken)
1743
- return {};
1744
- 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)}`;
1745
1910
  }
1746
- async function probeLocalBuddy(localAgentUrl, authToken) {
1747
- 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);
1748
1913
  }
1749
1914
  async function autoStartRuntimeServices(opts) {
1750
1915
  const { runtimePreflight } = opts.preparedRuntime;
1751
1916
  const runtimeRoot = opts.preparedRuntime.runtimeRoot;
1752
1917
  const runtimeTag = opts.preparedRuntime.runtimeTag || await resolveRuntimeReleaseTag(opts.requestedRuntimeTag);
1918
+ const localBuddyEnabled = Boolean(runtimePreflight.config.localbuddy.enabled);
1919
+ const requireLocalBuddy = opts.requireLocalBuddy ?? true;
1753
1920
  console.log(`[pushpals] LocalBuddy unavailable. Auto-starting runtime for repo: ${opts.repoRoot}`);
1754
1921
  console.log(`[pushpals] runtimeRoot=${runtimeRoot}`);
1755
1922
  console.log(`[pushpals] runtimeTag=${runtimeTag}`);
@@ -1805,10 +1972,14 @@ ${tail}` : ""}`);
1805
1972
  } else {
1806
1973
  console.log("[pushpals] Server already healthy; skipping embedded server start.");
1807
1974
  }
1808
- console.log("[pushpals] Starting embedded LocalBuddy...");
1809
- const localbuddyService = spawnRuntimeService("localbuddy", [runtimeBinaries.localbuddy], opts.repoRoot, runtimeEnv, logPathFor("localbuddy"));
1810
- services.push(localbuddyService);
1811
- 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
+ }
1812
1983
  console.log("[pushpals] Starting embedded RemoteBuddy...");
1813
1984
  const remotebuddyService = spawnRuntimeService("remotebuddy", [runtimeBinaries.remotebuddy], opts.repoRoot, runtimeEnv, logPathFor("remotebuddy"));
1814
1985
  services.push(remotebuddyService);
@@ -1858,8 +2029,8 @@ ${tail2}`);
1858
2029
  ${tail}` : ""}`);
1859
2030
  }
1860
2031
  }
1861
- const health = await probeLocalBuddy(opts.localAgentUrl, opts.authToken);
1862
- if (health?.ok) {
2032
+ const health = localBuddyEnabled ? await probeLocalBuddy(opts.localAgentUrl) : null;
2033
+ if (!requireLocalBuddy || localBuddyEnabled && health?.ok) {
1863
2034
  const stabilityDeadline = Date.now() + DEFAULT_SERVICE_STABILITY_GRACE_MS;
1864
2035
  while (Date.now() < stabilityDeadline) {
1865
2036
  for (let i = services.length - 1;i >= 0; i--) {
@@ -1890,13 +2061,16 @@ ${tail}` : ""}`);
1890
2061
  await Bun.sleep(DEFAULT_RUNTIME_BOOT_POLL_MS);
1891
2062
  }
1892
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
+ }
1893
2067
  throw new Error(`Timed out waiting for LocalBuddy at ${opts.localAgentUrl} after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
1894
2068
  }
1895
2069
  function readCliState(pathValue) {
1896
- if (!existsSync3(pathValue))
2070
+ if (!existsSync4(pathValue))
1897
2071
  return {};
1898
2072
  try {
1899
- const raw = readFileSync3(pathValue, "utf8");
2073
+ const raw = readFileSync4(pathValue, "utf8");
1900
2074
  const parsed = JSON.parse(raw);
1901
2075
  if (!parsed || typeof parsed !== "object")
1902
2076
  return {};
@@ -1922,6 +2096,9 @@ function writeCliState(pathValue, state) {
1922
2096
  writeFileSync(pathValue, `${JSON.stringify(payload, null, 2)}
1923
2097
  `, "utf8");
1924
2098
  }
2099
+ function resolveCliStatePath(repoRoot) {
2100
+ return resolveGitStateFilePath(repoRoot, "pushpals-cli-state.json");
2101
+ }
1925
2102
  async function looksLikeMonitoringHub(url) {
1926
2103
  try {
1927
2104
  const response = await fetchWithTimeout(url, {}, 700);
@@ -1942,7 +2119,9 @@ function buildMonitoringHubRuntimeBootstrap(opts) {
1942
2119
  serverUrl: opts.serverUrl,
1943
2120
  localAgentUrl: opts.localAgentUrl,
1944
2121
  sessionId: opts.sessionId,
1945
- authToken: opts.authToken
2122
+ clientId: `cli-monitor-${opts.sessionId}`,
2123
+ clientKind: "cli_monitor",
2124
+ clientLabel: "CLI Monitor"
1946
2125
  };
1947
2126
  }
1948
2127
  function injectMonitoringHubBootstrap(html, bootstrap) {
@@ -1983,19 +2162,19 @@ function monitoringHubContentType(pathValue) {
1983
2162
  }
1984
2163
  }
1985
2164
  function resolveMonitoringHubAssetPath(assetRoot, pathname) {
1986
- const root = resolve3(assetRoot);
2165
+ const root = resolve4(assetRoot);
1987
2166
  const rootPrefix = `${root}${root.endsWith("\\") || root.endsWith("/") ? "" : process.platform === "win32" ? "\\" : "/"}`;
1988
2167
  const decodedPath = decodeURIComponent(pathname);
1989
2168
  const trimmedPath = decodedPath === "/" ? "/index.html" : decodedPath;
1990
2169
  const relativePath = trimmedPath.replace(/^\/+/, "");
1991
- const candidatePath = resolve3(root, relativePath);
2170
+ const candidatePath = resolve4(root, relativePath);
1992
2171
  if (candidatePath !== root && !candidatePath.startsWith(rootPrefix))
1993
2172
  return null;
1994
- if (existsSync3(candidatePath))
2173
+ if (existsSync4(candidatePath))
1995
2174
  return candidatePath;
1996
2175
  if (!extname(relativePath)) {
1997
- const nestedIndexPath = resolve3(root, relativePath, "index.html");
1998
- if ((nestedIndexPath === root || nestedIndexPath.startsWith(rootPrefix)) && existsSync3(nestedIndexPath)) {
2176
+ const nestedIndexPath = resolve4(root, relativePath, "index.html");
2177
+ if ((nestedIndexPath === root || nestedIndexPath.startsWith(rootPrefix)) && existsSync4(nestedIndexPath)) {
1999
2178
  return nestedIndexPath;
2000
2179
  }
2001
2180
  return join2(root, "index.html");
@@ -2004,10 +2183,10 @@ function resolveMonitoringHubAssetPath(assetRoot, pathname) {
2004
2183
  }
2005
2184
  async function serveBundledMonitoringHub(assetRoot, pathname, bootstrap) {
2006
2185
  const assetPath = resolveMonitoringHubAssetPath(assetRoot, pathname);
2007
- if (!assetPath || !existsSync3(assetPath))
2186
+ if (!assetPath || !existsSync4(assetPath))
2008
2187
  return null;
2009
2188
  if (assetPath.endsWith("index.html")) {
2010
- const html = injectMonitoringHubBootstrap(readFileSync3(assetPath, "utf8"), bootstrap);
2189
+ const html = injectMonitoringHubBootstrap(readFileSync4(assetPath, "utf8"), bootstrap);
2011
2190
  return new Response(html, {
2012
2191
  headers: {
2013
2192
  "content-type": "text/html; charset=utf-8",
@@ -2156,9 +2335,9 @@ function buildEmbeddedMonitoringHubHtml(opts) {
2156
2335
  </body>
2157
2336
  </html>`;
2158
2337
  }
2159
- async function proxyMonitoringHubRequest(serverUrl, authToken, pathValue) {
2338
+ async function proxyMonitoringHubRequest(serverUrl, pathValue) {
2160
2339
  const target = `${serverUrl}${pathValue}`;
2161
- const upstream = await fetchWithTimeout(target, { headers: authHeaders(authToken) }, 1e4);
2340
+ const upstream = await fetchWithTimeout(target, {}, 1e4);
2162
2341
  const body = await upstream.text();
2163
2342
  return new Response(body, {
2164
2343
  status: upstream.status,
@@ -2177,14 +2356,14 @@ async function startEmbeddedMonitoringHub(opts) {
2177
2356
  const bootstrap = buildMonitoringHubRuntimeBootstrap({
2178
2357
  serverUrl: opts.serverUrl,
2179
2358
  localAgentUrl: opts.localAgentUrl,
2180
- sessionId: opts.sessionId,
2181
- authToken: opts.authToken
2359
+ sessionId: opts.sessionId
2182
2360
  });
2183
2361
  const candidatePorts = Array.from({ length: MONITOR_SCAN_PORTS }, (_, index) => opts.preferredPort + index).concat(0);
2184
2362
  for (const port of candidatePorts) {
2185
2363
  try {
2186
2364
  const server = Bun.serve({
2187
2365
  port,
2366
+ hostname: "127.0.0.1",
2188
2367
  idleTimeout: 30,
2189
2368
  fetch: async (req) => {
2190
2369
  const url = new URL(req.url);
@@ -2197,16 +2376,16 @@ async function startEmbeddedMonitoringHub(opts) {
2197
2376
  });
2198
2377
  }
2199
2378
  if (url.pathname === "/api/status") {
2200
- return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/system/status");
2379
+ return await proxyMonitoringHubRequest(opts.serverUrl, "/system/status");
2201
2380
  }
2202
2381
  if (url.pathname === "/api/requests") {
2203
- return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/requests?status=all&limit=20");
2382
+ return await proxyMonitoringHubRequest(opts.serverUrl, "/requests?status=all&limit=20");
2204
2383
  }
2205
2384
  if (url.pathname === "/api/jobs") {
2206
- return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/jobs?status=all&limit=20");
2385
+ return await proxyMonitoringHubRequest(opts.serverUrl, "/jobs?status=all&limit=20");
2207
2386
  }
2208
2387
  if (url.pathname === "/api/completions") {
2209
- return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/completions?status=all&limit=20");
2388
+ return await proxyMonitoringHubRequest(opts.serverUrl, "/completions?status=all&limit=20");
2210
2389
  }
2211
2390
  const bundledResponse = await serveBundledMonitoringHub(monitoringHubAssetRoot, url.pathname, bootstrap);
2212
2391
  if (bundledResponse)
@@ -2227,10 +2406,13 @@ async function startEmbeddedMonitoringHub(opts) {
2227
2406
  async function resolveMonitoringHub(opts) {
2228
2407
  const explicit = normalizeUrl(opts.preferredUrl);
2229
2408
  if (explicit) {
2230
- 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)) {
2231
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.`);
2232
2415
  }
2233
- console.warn(`[pushpals] Preferred monitoring hub ${explicit} is unavailable; starting embedded monitor instead.`);
2234
2416
  }
2235
2417
  for (let port = opts.fallbackPort;port < opts.fallbackPort + MONITOR_SCAN_PORTS; port++) {
2236
2418
  const candidate = `http://127.0.0.1:${port}`;
@@ -2350,12 +2532,11 @@ function formatSessionEventLine(event) {
2350
2532
  }
2351
2533
  return null;
2352
2534
  }
2353
- async function runSessionStream(serverUrl, sessionId, authToken, print, signal) {
2535
+ async function runSessionStream(serverUrl, sessionId, client, print, signal) {
2354
2536
  let cursor = 0;
2355
2537
  while (!signal.aborted) {
2356
- const headers = authHeaders(authToken);
2357
2538
  try {
2358
- 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);
2359
2540
  if (!response.ok || !response.body) {
2360
2541
  print(`[pushpals] Session stream unavailable: HTTP ${response.status}`);
2361
2542
  await Bun.sleep(SSE_RECONNECT_MS);
@@ -2469,10 +2650,9 @@ async function main() {
2469
2650
  process.exit(1);
2470
2651
  }
2471
2652
  const config = preparedRuntime.runtimePreflight.config;
2472
- const serverUrl = normalizeUrl(parsed.serverUrl ?? process.env.PUSHPALS_SERVER_URL, config.server.url);
2473
- 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);
2474
2655
  const sessionId = String(parsed.sessionId ?? process.env.PUSHPALS_SESSION_ID ?? config.sessionId).trim();
2475
- const authToken = config.authToken;
2476
2656
  let autoStartedServices = [];
2477
2657
  const stopAutoStartedServices = () => {
2478
2658
  if (autoStartedServices.length === 0)
@@ -2480,8 +2660,10 @@ async function main() {
2480
2660
  stopRuntimeServices(autoStartedServices);
2481
2661
  autoStartedServices = [];
2482
2662
  };
2483
- let health = await probeLocalBuddy(localAgentUrl, authToken);
2484
- 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) {
2485
2667
  try {
2486
2668
  autoStartedServices = await autoStartRuntimeServices({
2487
2669
  repoRoot,
@@ -2489,45 +2671,58 @@ async function main() {
2489
2671
  localAgentUrl,
2490
2672
  sourceControlManagerPort: config.sourceControlManager.port,
2491
2673
  sourceControlManagerRemote: config.sourceControlManager.remote,
2492
- authToken,
2493
2674
  preparedRuntime,
2494
- requestedRuntimeTag: parsed.runtimeTag
2675
+ requestedRuntimeTag: parsed.runtimeTag,
2676
+ requireLocalBuddy: !parsed.runtimeOnly
2495
2677
  });
2496
- health = await probeLocalBuddy(localAgentUrl, authToken);
2678
+ serverHealthy = await probeServer(serverUrl);
2679
+ health = await probeLocalBuddy(localAgentUrl);
2497
2680
  } catch (err) {
2498
2681
  console.error(`[pushpals] Auto-start failed: ${String(err)}`);
2499
2682
  stopAutoStartedServices();
2500
2683
  }
2501
2684
  }
2502
- if (!health?.ok) {
2503
- console.error(`[pushpals] LocalBuddy is unavailable at ${localAgentUrl}.`);
2685
+ if (parsed.runtimeOnly && !serverHealthy) {
2686
+ console.error(`[pushpals] Server is unavailable at ${serverUrl}.`);
2504
2687
  if (parsed.noAutoStart) {
2505
2688
  console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
2506
2689
  } else {
2507
- console.error("[pushpals] Auto-start could not bring LocalBuddy online.");
2690
+ console.error("[pushpals] Auto-start could not bring the embedded runtime online.");
2508
2691
  }
2509
2692
  process.exit(1);
2510
2693
  }
2511
- const localBuddyRepo = health.repo ? resolve3(health.repo) : "";
2512
- if (!localBuddyRepo) {
2513
- stopAutoStartedServices();
2514
- console.error("[pushpals] LocalBuddy health response did not include repo path.");
2515
- process.exit(1);
2516
- }
2517
- if (normalizePath(localBuddyRepo) !== normalizePath(repoRoot)) {
2518
- stopAutoStartedServices();
2519
- console.error("[pushpals] Repo mismatch detected.");
2520
- console.error(`[pushpals] currentRepo=${repoRoot}`);
2521
- console.error(`[pushpals] localBuddyRepo=${localBuddyRepo}`);
2522
- 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
+ }
2523
2701
  process.exit(1);
2524
2702
  }
2525
- const localBuddySessionId = health.sessionId && String(health.sessionId).trim() ? String(health.sessionId).trim() : sessionId;
2526
- if (sessionId && sessionId !== localBuddySessionId) {
2527
- 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
+ }
2528
2723
  }
2529
- const statePath = resolve3(repoRoot, ".git", "pushpals-cli-state.json");
2530
- const saved = readCliState(statePath);
2724
+ const statePath = resolveCliStatePath(repoRoot);
2725
+ const saved = statePath ? readCliState(statePath) : {};
2531
2726
  const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
2532
2727
  const monitorPort = parsePositiveInt(process.env.PUSHPALS_CLIENT_PORT, DEFAULT_MONITOR_PORT);
2533
2728
  const monitoringHub = await resolveMonitoringHub({
@@ -2535,32 +2730,48 @@ async function main() {
2535
2730
  fallbackPort: monitorPort,
2536
2731
  serverUrl,
2537
2732
  localAgentUrl,
2538
- sessionId: localBuddySessionId,
2539
- authToken
2733
+ sessionId: localBuddySessionId
2540
2734
  });
2541
2735
  const monitoringHubUrl = monitoringHub?.url ?? "";
2542
- writeCliState(statePath, {
2543
- monitoringHubUrl: monitoringHubUrl || undefined,
2544
- serverUrl,
2545
- localAgentUrl,
2546
- sessionId: localBuddySessionId,
2547
- repoRoot
2548
- });
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
+ }
2549
2747
  console.log("[pushpals] Connected.");
2550
2748
  if (monitoringHubUrl) {
2551
- console.log(`monitoringHubUrl=${monitoringHubUrl}`);
2749
+ console.log(`[pushpals] monitoringHubUrl=${monitoringHubUrl}`);
2552
2750
  if (monitoringHub?.embedded) {
2553
2751
  console.log("[pushpals] Embedded monitoring hub is running.");
2554
2752
  }
2555
2753
  } else {
2556
- console.log("monitoringHubUrl=unavailable");
2557
- }
2558
- console.log(`serverUrl=${serverUrl}`);
2559
- console.log(`localAgentUrl=${localAgentUrl}`);
2560
- console.log(`sessionId=${localBuddySessionId}`);
2561
- console.log(`repoRoot=${repoRoot}`);
2562
- console.log(`cliStateFile=${statePath}`);
2563
- console.log("[pushpals] Type a message and press Enter. Use /exit or 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
+ };
2564
2775
  const streamAbort = new AbortController;
2565
2776
  let rl = null;
2566
2777
  const printIncoming = (line) => {
@@ -2575,7 +2786,7 @@ ${line}
2575
2786
  }
2576
2787
  console.log(line);
2577
2788
  };
2578
- 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);
2579
2790
  let shuttingDown = false;
2580
2791
  const requestStop = () => {
2581
2792
  if (shuttingDown)
@@ -2596,6 +2807,39 @@ ${line}
2596
2807
  process.once("SIGINT", requestStop);
2597
2808
  process.once("SIGTERM", requestStop);
2598
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
+ }
2599
2843
  rl = createInterface({
2600
2844
  input: process.stdin,
2601
2845
  output: process.stdout,
@@ -2614,16 +2858,16 @@ ${line}
2614
2858
  break;
2615
2859
  }
2616
2860
  if (text === "/hub") {
2617
- console.log(monitoringHubUrl ? `monitoringHubUrl=${monitoringHubUrl}` : "monitoringHubUrl=unavailable");
2861
+ console.log(monitoringHubUrl ? `[pushpals] monitoringHubUrl=${monitoringHubUrl}` : "[pushpals] monitoringHubUrl=unavailable");
2618
2862
  rl.prompt();
2619
2863
  continue;
2620
2864
  }
2621
2865
  if (text === "/status") {
2622
- console.log(`serverUrl=${serverUrl}`);
2623
- console.log(`localAgentUrl=${localAgentUrl}`);
2624
- console.log(`sessionId=${localBuddySessionId}`);
2625
- console.log(`repoRoot=${repoRoot}`);
2626
- 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");
2627
2871
  rl.prompt();
2628
2872
  continue;
2629
2873
  }
@@ -2656,12 +2900,14 @@ if (import.meta.main) {
2656
2900
  export {
2657
2901
  startEmbeddedMonitoringHub,
2658
2902
  resolveCommandPath,
2903
+ resolveCliStatePath,
2659
2904
  resolveBundledRuntimeAssetSource,
2660
2905
  resolveBundledMonitoringHubRoot,
2661
2906
  prepareCliRuntime,
2662
2907
  normalizeChildProcessEnv,
2663
2908
  isCliExitCommand,
2664
2909
  injectMonitoringHubBootstrap,
2910
+ formatTimestampedCliLine,
2665
2911
  buildServiceStopCommand,
2666
2912
  buildOpenMonitoringHubCommand,
2667
2913
  buildEmbeddedRuntimeEnv,