@pushpalsdev/cli 1.0.11 → 1.0.12

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.
@@ -7,7 +7,9 @@ import {
7
7
  chmodSync,
8
8
  cpSync,
9
9
  existsSync as existsSync4,
10
+ lstatSync,
10
11
  mkdirSync,
12
+ readdirSync,
11
13
  readFileSync as readFileSync4,
12
14
  writeFileSync
13
15
  } from "fs";
@@ -1104,6 +1106,17 @@ function formatClientRuntimePreflightLines(result, prefix) {
1104
1106
  return lines;
1105
1107
  }
1106
1108
 
1109
+ // ../shared/src/communication.ts
1110
+ function stripPresenceSourcePrefix(value) {
1111
+ return value.replace(/^(agent|client)(?:[\s:./_-]+)+/i, "");
1112
+ }
1113
+ function normalizePresenceClientLabel(value) {
1114
+ return stripPresenceSourcePrefix(String(value ?? "")).replace(/\s+/g, " ").trim();
1115
+ }
1116
+ function normalizePresenceLookupToken(value) {
1117
+ return normalizePresenceClientLabel(value).toLowerCase().replace(/[^a-z0-9]+/g, "");
1118
+ }
1119
+
1107
1120
  // ../shared/src/repo.ts
1108
1121
  import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
1109
1122
  import { resolve as resolve3 } from "path";
@@ -1144,6 +1157,21 @@ function resolveGitStateFilePath(repoRoot, fileName) {
1144
1157
  return resolve3(gitMetadataDir, normalizedFileName);
1145
1158
  }
1146
1159
 
1160
+ // ../shared/src/session_event_visibility.ts
1161
+ var HEARTBEAT_STATUS_RE = /\bheartbeat\b/i;
1162
+ function isHeartbeatStatusSessionEvent(event) {
1163
+ const type = String(event?.type ?? "").trim().toLowerCase();
1164
+ if (type !== "status")
1165
+ return false;
1166
+ const payload = event?.payload ?? {};
1167
+ const detail = typeof payload.detail === "string" ? payload.detail.trim() : "";
1168
+ const message = typeof payload.message === "string" ? payload.message.trim() : "";
1169
+ return HEARTBEAT_STATUS_RE.test(detail) || HEARTBEAT_STATUS_RE.test(message);
1170
+ }
1171
+ function shouldDisplayInteractiveSessionEvent(event) {
1172
+ return !isHeartbeatStatusSessionEvent(event);
1173
+ }
1174
+
1147
1175
  // ../../scripts/pushpals-cli.ts
1148
1176
  var DEFAULT_MONITOR_PORT = 8081;
1149
1177
  var MONITOR_SCAN_PORTS = 32;
@@ -1163,6 +1191,7 @@ var GITHUB_HEADERS = {
1163
1191
  Accept: "application/vnd.github+json",
1164
1192
  "User-Agent": "pushpals-cli"
1165
1193
  };
1194
+ var ASK_REMOTE_BUDDY_COMMAND = "/ask_remote_buddy";
1166
1195
  var stateVersion = 1;
1167
1196
  var cliTimestampedConsoleInstalled = false;
1168
1197
  function formatTimestampedCliLine(line, at = new Date) {
@@ -1172,6 +1201,21 @@ function formatTimestampedCliLine(line, at = new Date) {
1172
1201
  }
1173
1202
  return `[${at.toISOString()}]${text}`;
1174
1203
  }
1204
+ function normalizeCliInteractiveMessage(input) {
1205
+ const trimmed = String(input ?? "").trim();
1206
+ const command = ASK_REMOTE_BUDDY_COMMAND.toLowerCase();
1207
+ if (!trimmed.toLowerCase().startsWith(command)) {
1208
+ return { text: trimmed };
1209
+ }
1210
+ const rest = trimmed.slice(command.length).replace(/^[:\-]\s*/, "").trim();
1211
+ if (!rest) {
1212
+ return {
1213
+ text: "",
1214
+ usageMessage: "Usage: /ask_remote_buddy <request>. Example: /ask_remote_buddy fix the failing job status in the dashboard."
1215
+ };
1216
+ }
1217
+ return { text: rest };
1218
+ }
1175
1219
  function installTimestampedCliConsole() {
1176
1220
  if (cliTimestampedConsoleInstalled)
1177
1221
  return;
@@ -1205,12 +1249,12 @@ function printUsage() {
1205
1249
  console.log("");
1206
1250
  console.log("Options:");
1207
1251
  console.log(" --server-url <url> Override PushPals server URL");
1208
- console.log(" --local-agent-url <url> Override LocalBuddy URL");
1252
+ console.log(" --local-agent-url <url> Override LocalBuddy URL for monitoring/runtime state");
1209
1253
  console.log(" --session-id <id> Override session ID");
1210
1254
  console.log(" --hub-url <url> Override monitoring hub URL");
1211
1255
  console.log(" --runtime-root <path> Override embedded runtime directory for auto-start");
1212
1256
  console.log(" --runtime-tag <tag> Override runtime release tag (e.g. v1.0.2)");
1213
- console.log(" --no-auto-start Disable runtime auto-start when LocalBuddy is down");
1257
+ console.log(" --no-auto-start Disable runtime auto-start when the server is down");
1214
1258
  console.log(" --no-stream Disable live session event stream");
1215
1259
  console.log(" --runtime-only Start the local runtime and wait for shutdown without opening the interactive chat");
1216
1260
  console.log(" -h, --help Show this help");
@@ -1223,8 +1267,8 @@ function printUsage() {
1223
1267
  console.log("");
1224
1268
  console.log("Notes:");
1225
1269
  console.log(" - Must be run from inside a git repository.");
1226
- console.log(" - Auto-start can bootstrap server/localbuddy/remotebuddy/source_control_manager.");
1227
- console.log(" - LocalBuddy must be attached to the same repo root.");
1270
+ console.log(" - Auto-start can bootstrap server/remotebuddy/source_control_manager and LocalBuddy when runtime config enables it.");
1271
+ console.log(" - Interactive CLI talks directly to server sessions; LocalBuddy is optional.");
1228
1272
  }
1229
1273
  function parseArgs(argv) {
1230
1274
  const options = { noAutoStart: false, noStream: false, runtimeOnly: false };
@@ -1311,12 +1355,6 @@ function parsePositiveInt(value, fallback) {
1311
1355
  return fallback;
1312
1356
  return parsed;
1313
1357
  }
1314
- function normalizePath(value) {
1315
- const normalized = resolve4(value).replace(/\\/g, "/").replace(/\/+$/, "");
1316
- if (process.platform === "win32")
1317
- return normalized.toLowerCase();
1318
- return normalized;
1319
- }
1320
1358
  function jsonHtmlBootstrap(value) {
1321
1359
  return JSON.stringify(value).replace(/</g, "\\u003c");
1322
1360
  }
@@ -1374,6 +1412,42 @@ function resolveBundledRuntimeAssetSource() {
1374
1412
  function looksLikeMonitoringHubBuild(root) {
1375
1413
  return existsSync4(join2(root, "index.html")) && existsSync4(join2(root, "_expo"));
1376
1414
  }
1415
+ function latestPathMtimeMs(pathValue) {
1416
+ if (!existsSync4(pathValue))
1417
+ return 0;
1418
+ const stat = lstatSync(pathValue);
1419
+ let latest = stat.mtimeMs;
1420
+ if (!stat.isDirectory())
1421
+ return latest;
1422
+ for (const entry of readdirSync(pathValue)) {
1423
+ latest = Math.max(latest, latestPathMtimeMs(join2(pathValue, entry)));
1424
+ }
1425
+ return latest;
1426
+ }
1427
+ function bundledMonitoringHubSourceWatchPaths(sourceRoot) {
1428
+ return [
1429
+ join2(sourceRoot, "apps", "client", "app"),
1430
+ join2(sourceRoot, "apps", "client", "assets"),
1431
+ join2(sourceRoot, "apps", "client", "components"),
1432
+ join2(sourceRoot, "apps", "client", "constants"),
1433
+ join2(sourceRoot, "apps", "client", "hooks"),
1434
+ join2(sourceRoot, "apps", "client", "scripts"),
1435
+ join2(sourceRoot, "apps", "client", "src"),
1436
+ join2(sourceRoot, "apps", "client", "app.json"),
1437
+ join2(sourceRoot, "apps", "client", "package.json"),
1438
+ join2(sourceRoot, "packages", "shared", "src"),
1439
+ join2(sourceRoot, "scripts", "sync-cli-monitor-ui.ts")
1440
+ ];
1441
+ }
1442
+ function bundledMonitoringHubNeedsRefresh(existingRoot, sourceRoot) {
1443
+ if (!looksLikeMonitoringHubBuild(existingRoot))
1444
+ return true;
1445
+ const bundleMtimeMs = latestPathMtimeMs(existingRoot);
1446
+ if (bundleMtimeMs <= 0)
1447
+ return true;
1448
+ const sourceMtimeMs = bundledMonitoringHubSourceWatchPaths(sourceRoot).reduce((latest, pathValue) => Math.max(latest, latestPathMtimeMs(pathValue)), 0);
1449
+ return sourceMtimeMs > bundleMtimeMs;
1450
+ }
1377
1451
  function resolveBundledMonitoringHubRoot() {
1378
1452
  const candidates = [
1379
1453
  resolve4(import.meta.dir, "..", "monitor-ui"),
@@ -1413,11 +1487,15 @@ function exportBundledMonitoringHubFromSourceCheckout(sourceRoot) {
1413
1487
  }
1414
1488
  async function ensureBundledMonitoringHubRoot() {
1415
1489
  const existingRoot = resolveBundledMonitoringHubRoot();
1416
- if (existingRoot)
1417
- return existingRoot;
1418
1490
  const sourceRoot = resolveCliSourceCheckoutRoot();
1419
1491
  if (!sourceRoot)
1420
- return null;
1492
+ return existingRoot;
1493
+ if (existingRoot && !bundledMonitoringHubNeedsRefresh(existingRoot, sourceRoot)) {
1494
+ return existingRoot;
1495
+ }
1496
+ if (existingRoot) {
1497
+ console.log("[pushpals] Packaged monitor UI is stale; refreshing the exported client monitor...");
1498
+ }
1421
1499
  exportBundledMonitoringHubFromSourceCheckout(sourceRoot);
1422
1500
  return resolveBundledMonitoringHubRoot();
1423
1501
  }
@@ -1637,7 +1715,7 @@ function buildEmbeddedRuntimeEnv(baseEnv, opts) {
1637
1715
  PUSHPALS_PROMPTS_ROOT_OVERRIDE: opts.repoRoot
1638
1716
  },
1639
1717
  PUSHPALS_PROTOCOL_SCHEMAS_DIR: join2(opts.runtimeRoot, "protocol", "schemas"),
1640
- ...opts.forceLocalBuddyEnabled ? { LOCALBUDDY_ENABLED: "1" } : {},
1718
+ ...typeof opts.sessionId === "string" && opts.sessionId.trim() ? { PUSHPALS_SESSION_ID: opts.sessionId.trim() } : {},
1641
1719
  ...typeof env.PUSHPALS_GIT_BIN === "string" && env.PUSHPALS_GIT_BIN.trim() ? { PUSHPALS_GIT_BIN: env.PUSHPALS_GIT_BIN.trim() } : {}
1642
1720
  };
1643
1721
  }
@@ -1889,6 +1967,111 @@ async function probeServer(serverUrl) {
1889
1967
  return false;
1890
1968
  }
1891
1969
  }
1970
+ function normalizeRepoPathForComparison(repoPath) {
1971
+ const normalized = resolve4(String(repoPath ?? "")).replace(/\\/g, "/").replace(/\/+$/, "");
1972
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
1973
+ }
1974
+ async function fetchServerRepoRoot(serverUrl) {
1975
+ const response = await fetchWithTimeout(`${serverUrl}/system/status`, {}, 1e4);
1976
+ if (!response.ok) {
1977
+ throw new Error(`status probe failed with HTTP ${response.status}`);
1978
+ }
1979
+ const payload = await response.json().catch(() => ({}));
1980
+ const repoRoot = payload?.repo && typeof payload.repo.root === "string" ? payload.repo.root.trim() : "";
1981
+ if (!repoRoot) {
1982
+ throw new Error("server did not report repo.root in /system/status");
1983
+ }
1984
+ return repoRoot;
1985
+ }
1986
+ async function ensureServerRepoAffinity(serverUrl, currentRepoRoot) {
1987
+ const serverRepoRoot = await fetchServerRepoRoot(serverUrl);
1988
+ if (normalizeRepoPathForComparison(serverRepoRoot) === normalizeRepoPathForComparison(currentRepoRoot)) {
1989
+ return;
1990
+ }
1991
+ throw new Error(`repo mismatch: currentRepo=${currentRepoRoot} serverRepo=${serverRepoRoot}. Stop the existing runtime or switch to the matching repo.`);
1992
+ }
1993
+ function isRemoteBuddyClientRow(row) {
1994
+ const clientId = normalizePresenceLookupToken(row.clientId);
1995
+ const label = normalizePresenceLookupToken(row.label);
1996
+ return clientId.includes("remotebuddy") || label.includes("remotebuddy");
1997
+ }
1998
+ function extractRemoteBuddySessionConsumerHealth(statusPayload, sessionId) {
1999
+ const rows = Array.isArray(statusPayload?.clients?.items) ? statusPayload.clients?.items ?? [] : [];
2000
+ const sessionRows = rows.filter((row) => {
2001
+ if (!row || typeof row !== "object" || Array.isArray(row))
2002
+ return false;
2003
+ return String(row.sessionId ?? "").trim() === sessionId;
2004
+ });
2005
+ const remotebuddyRows = sessionRows.filter(isRemoteBuddyClientRow);
2006
+ const connectedRow = remotebuddyRows.find((row) => String(row.status ?? "").trim().toLowerCase() === "connected");
2007
+ if (connectedRow) {
2008
+ return {
2009
+ ok: true,
2010
+ detail: `RemoteBuddy session consumer connected (${String(connectedRow.clientId ?? "").trim()})`,
2011
+ clientId: String(connectedRow.clientId ?? "").trim() || undefined,
2012
+ sessionId
2013
+ };
2014
+ }
2015
+ const anyRemoteBuddyRows = rows.filter((row) => {
2016
+ if (!row || typeof row !== "object" || Array.isArray(row))
2017
+ return false;
2018
+ return isRemoteBuddyClientRow(row);
2019
+ });
2020
+ const connectedOtherSession = anyRemoteBuddyRows.find((row) => {
2021
+ const rowSessionId = String(row.sessionId ?? "").trim();
2022
+ if (!rowSessionId || rowSessionId === sessionId)
2023
+ return false;
2024
+ return String(row.status ?? "").trim().toLowerCase() === "connected";
2025
+ });
2026
+ if (connectedOtherSession) {
2027
+ const otherSessionId = String(connectedOtherSession.sessionId ?? "").trim();
2028
+ const otherClientId = String(connectedOtherSession.clientId ?? "").trim();
2029
+ return {
2030
+ ok: false,
2031
+ detail: `RemoteBuddy is connected to session ${otherSessionId || "unknown"} ` + `(${otherClientId || "unknown client"}), not ${sessionId}`,
2032
+ clientId: otherClientId || undefined,
2033
+ sessionId: otherSessionId || undefined
2034
+ };
2035
+ }
2036
+ if (remotebuddyRows.length > 0) {
2037
+ return {
2038
+ ok: false,
2039
+ detail: `RemoteBuddy session consumer exists for ${sessionId} but is not connected`,
2040
+ clientId: String(remotebuddyRows[0]?.clientId ?? "").trim() || undefined,
2041
+ sessionId
2042
+ };
2043
+ }
2044
+ if (anyRemoteBuddyRows.length > 0) {
2045
+ const knownSessions = [...new Set(anyRemoteBuddyRows.map((row) => String(row.sessionId ?? "").trim()))].filter(Boolean).sort();
2046
+ const suffix = knownSessions.length > 0 ? ` Known RemoteBuddy sessions: ${knownSessions.join(", ")}.` : "";
2047
+ return {
2048
+ ok: false,
2049
+ detail: `No connected RemoteBuddy session consumer found for session ${sessionId}.${suffix}`.trim()
2050
+ };
2051
+ }
2052
+ return {
2053
+ ok: false,
2054
+ detail: `No connected RemoteBuddy session consumer found for session ${sessionId}`
2055
+ };
2056
+ }
2057
+ async function probeRemoteBuddySessionConsumer(serverUrl, sessionId) {
2058
+ try {
2059
+ const response = await fetchWithTimeout(`${serverUrl}/system/status`, {}, 1e4);
2060
+ if (!response.ok) {
2061
+ return {
2062
+ ok: false,
2063
+ detail: `system status probe failed with HTTP ${response.status}`
2064
+ };
2065
+ }
2066
+ const payload = await response.json().catch(() => ({}));
2067
+ return extractRemoteBuddySessionConsumerHealth(payload, sessionId);
2068
+ } catch (err) {
2069
+ return {
2070
+ ok: false,
2071
+ detail: `system status probe failed: ${String(err)}`
2072
+ };
2073
+ }
2074
+ }
1892
2075
  async function probeSourceControlManager(port) {
1893
2076
  if (!Number.isFinite(port) || port <= 0)
1894
2077
  return false;
@@ -1940,13 +2123,43 @@ function createRuntimeClientId(prefix) {
1940
2123
  async function probeLocalBuddy(localAgentUrl) {
1941
2124
  return await fetchJsonWithTimeout(`${localAgentUrl}/healthz`, {}, LOCALBUDDY_TIMEOUT_MS);
1942
2125
  }
2126
+ function resolveCliLocalBuddyAutostart(runtimeOnly, runtimeConfigEnabled) {
2127
+ return runtimeOnly ? runtimeConfigEnabled : false;
2128
+ }
2129
+ async function ensureServerSession(serverUrl, requestedSessionId, client) {
2130
+ const response = await fetchWithTimeout(`${serverUrl}/sessions`, {
2131
+ method: "POST",
2132
+ headers: { "Content-Type": "application/json" },
2133
+ body: JSON.stringify({
2134
+ sessionId: requestedSessionId,
2135
+ client: {
2136
+ clientId: client.clientId,
2137
+ kind: client.kind,
2138
+ label: client.label,
2139
+ version: client.version,
2140
+ platform: client.platform,
2141
+ repoRoot: client.repoRoot
2142
+ }
2143
+ })
2144
+ }, 15000);
2145
+ if (!response.ok) {
2146
+ const detail = await response.text().catch(() => "");
2147
+ throw new Error(`Failed to create or join session ${requestedSessionId}: HTTP ${response.status}${detail ? ` ${detail}` : ""}`);
2148
+ }
2149
+ const payload = await response.json().catch(() => ({}));
2150
+ const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : "";
2151
+ if (!sessionId) {
2152
+ throw new Error("Server session bootstrap returned no sessionId.");
2153
+ }
2154
+ return sessionId;
2155
+ }
1943
2156
  async function autoStartRuntimeServices(opts) {
1944
2157
  const { runtimePreflight } = opts.preparedRuntime;
1945
2158
  const runtimeRoot = opts.preparedRuntime.runtimeRoot;
1946
2159
  const runtimeTag = opts.preparedRuntime.runtimeTag || await resolveRuntimeReleaseTag(opts.requestedRuntimeTag);
1947
- const requireLocalBuddy = opts.requireLocalBuddy ?? true;
1948
- const localBuddyEnabled = requireLocalBuddy || Boolean(runtimePreflight.config.localbuddy.enabled);
1949
- console.log(`[pushpals] LocalBuddy unavailable. Auto-starting runtime for repo: ${opts.repoRoot}`);
2160
+ const startLocalBuddy = opts.startLocalBuddy ?? Boolean(runtimePreflight.config.localbuddy.enabled);
2161
+ const localBuddyEnabled = startLocalBuddy;
2162
+ console.log(`[pushpals] Runtime unavailable. Auto-starting runtime for repo: ${opts.repoRoot}`);
1950
2163
  console.log(`[pushpals] runtimeRoot=${runtimeRoot}`);
1951
2164
  console.log(`[pushpals] runtimeTag=${runtimeTag}`);
1952
2165
  if (!runtimePreflight.ok) {
@@ -1958,7 +2171,7 @@ async function autoStartRuntimeServices(opts) {
1958
2171
  repoRoot: opts.repoRoot,
1959
2172
  runtimeRoot,
1960
2173
  useRuntimeConfig: opts.preparedRuntime.preflightUsesEmbeddedRuntime,
1961
- forceLocalBuddyEnabled: requireLocalBuddy
2174
+ sessionId: opts.sessionId
1962
2175
  });
1963
2176
  if (runtimeEnv.PUSHPALS_GIT_BIN) {
1964
2177
  applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, runtimeEnv.PUSHPALS_GIT_BIN);
@@ -2006,15 +2219,12 @@ ${tail}` : ""}`);
2006
2219
  console.log("[pushpals] Server already healthy; skipping embedded server start.");
2007
2220
  }
2008
2221
  if (localBuddyEnabled) {
2009
- if (requireLocalBuddy && !runtimePreflight.config.localbuddy.enabled) {
2010
- console.log("[pushpals] LocalBuddy is disabled in config; forcing it on for this CLI session.");
2011
- }
2012
2222
  console.log("[pushpals] Starting embedded LocalBuddy...");
2013
2223
  const localbuddyService = spawnRuntimeService("localbuddy", [runtimeBinaries.localbuddy], opts.repoRoot, runtimeEnv, logPathFor("localbuddy"));
2014
2224
  services.push(localbuddyService);
2015
2225
  console.log(`[pushpals] localbuddy log: ${localbuddyService.logPath}`);
2016
2226
  } else {
2017
- console.log("[pushpals] Embedded LocalBuddy disabled by runtime config; skipping start.");
2227
+ console.log("[pushpals] Embedded LocalBuddy disabled for this CLI session; skipping start.");
2018
2228
  }
2019
2229
  console.log("[pushpals] Starting embedded RemoteBuddy...");
2020
2230
  const remotebuddyService = spawnRuntimeService("remotebuddy", [runtimeBinaries.remotebuddy], opts.repoRoot, runtimeEnv, logPathFor("remotebuddy"));
@@ -2066,7 +2276,8 @@ ${tail}` : ""}`);
2066
2276
  }
2067
2277
  }
2068
2278
  const health = localBuddyEnabled ? await probeLocalBuddy(opts.localAgentUrl) : null;
2069
- if (!requireLocalBuddy || localBuddyEnabled && health?.ok) {
2279
+ const remoteBuddyHealth2 = await probeRemoteBuddySessionConsumer(opts.serverUrl, opts.sessionId);
2280
+ if ((!localBuddyEnabled || health?.ok) && remoteBuddyHealth2.ok) {
2070
2281
  const stabilityDeadline = Date.now() + DEFAULT_SERVICE_STABILITY_GRACE_MS;
2071
2282
  while (Date.now() < stabilityDeadline) {
2072
2283
  for (let i = services.length - 1;i >= 0; i--) {
@@ -2097,10 +2308,14 @@ ${tail}` : ""}`);
2097
2308
  await Bun.sleep(DEFAULT_RUNTIME_BOOT_POLL_MS);
2098
2309
  }
2099
2310
  stopRuntimeServices(services);
2311
+ const remoteBuddyHealth = await probeRemoteBuddySessionConsumer(opts.serverUrl, opts.sessionId);
2312
+ if (!localBuddyEnabled && !remoteBuddyHealth.ok) {
2313
+ throw new Error(`Timed out waiting for RemoteBuddy session consumer readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms (${remoteBuddyHealth.detail})`);
2314
+ }
2100
2315
  if (!localBuddyEnabled) {
2101
2316
  throw new Error(`Timed out waiting for embedded runtime readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
2102
2317
  }
2103
- throw new Error(`Timed out waiting for LocalBuddy at ${opts.localAgentUrl} after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
2318
+ throw new Error(`Timed out waiting for LocalBuddy at ${opts.localAgentUrl} and RemoteBuddy session consumer after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
2104
2319
  }
2105
2320
  function readCliState(pathValue) {
2106
2321
  if (!existsSync4(pathValue))
@@ -2153,7 +2368,6 @@ async function looksLikeMonitoringHub(url) {
2153
2368
  function buildMonitoringHubRuntimeBootstrap(opts) {
2154
2369
  return {
2155
2370
  serverUrl: opts.serverUrl,
2156
- localAgentUrl: opts.localAgentUrl,
2157
2371
  sessionId: opts.sessionId,
2158
2372
  clientId: `cli-monitor-${opts.sessionId}`,
2159
2373
  clientKind: "cli_monitor",
@@ -2238,7 +2452,10 @@ async function serveBundledMonitoringHub(assetRoot, pathname, bootstrap) {
2238
2452
  });
2239
2453
  }
2240
2454
  function buildEmbeddedMonitoringHubHtml(opts) {
2241
- const bootstrap = jsonHtmlBootstrap(opts);
2455
+ const bootstrap = jsonHtmlBootstrap({
2456
+ serverUrl: opts.serverUrl,
2457
+ sessionId: opts.sessionId
2458
+ });
2242
2459
  return `<!doctype html>
2243
2460
  <html lang="en">
2244
2461
  <head>
@@ -2334,7 +2551,6 @@ function buildEmbeddedMonitoringHubHtml(opts) {
2334
2551
  cardsEl.innerHTML = cards.map((card) => '<div class="card"><div class="label">' + esc(card.label) + '</div><div class="value">' + esc(card.value) + '</div><div class="sub">' + esc(card.sub) + '</div></div>').join("");
2335
2552
  metaEl.innerHTML = [
2336
2553
  '<span class="pill">server ' + esc(boot.serverUrl) + '</span>',
2337
- '<span class="pill">localbuddy ' + esc(boot.localAgentUrl) + '</span>',
2338
2554
  '<span class="pill">session ' + esc(boot.sessionId) + '</span>',
2339
2555
  '<span class="pill">repo ' + esc(repo?.root ?? repo?.remoteUrl ?? "current repo") + '</span>'
2340
2556
  ].join("");
@@ -2391,7 +2607,6 @@ async function startEmbeddedMonitoringHub(opts) {
2391
2607
  }
2392
2608
  const bootstrap = buildMonitoringHubRuntimeBootstrap({
2393
2609
  serverUrl: opts.serverUrl,
2394
- localAgentUrl: opts.localAgentUrl,
2395
2610
  sessionId: opts.sessionId
2396
2611
  });
2397
2612
  const candidatePorts = Array.from({ length: MONITOR_SCAN_PORTS }, (_, index) => opts.preferredPort + index).concat(0);
@@ -2462,72 +2677,30 @@ async function resolveMonitoringHub(opts) {
2462
2677
  }
2463
2678
  return embedded;
2464
2679
  }
2465
- async function sendMessageToLocalBuddy(localAgentUrl, text) {
2466
- let response;
2680
+ async function sendMessageToServerSession(serverUrl, sessionId, text) {
2467
2681
  try {
2468
- response = await fetchWithTimeout(`${localAgentUrl}/message`, {
2682
+ const response = await fetchWithTimeout(`${serverUrl}/sessions/${encodeURIComponent(sessionId)}/message`, {
2469
2683
  method: "POST",
2470
2684
  headers: { "Content-Type": "application/json" },
2471
2685
  body: JSON.stringify({ text })
2472
- }, 30000);
2686
+ }, 15000);
2687
+ if (!response.ok) {
2688
+ const detail = await response.text().catch(() => "");
2689
+ console.error(`[pushpals] Session message rejected: HTTP ${response.status}${detail ? ` ${detail}` : ""}`);
2690
+ return false;
2691
+ }
2692
+ return true;
2473
2693
  } catch (err) {
2474
- console.error(`[pushpals] Failed to reach LocalBuddy: ${String(err)}`);
2475
- return false;
2476
- }
2477
- if (!response.ok) {
2478
- const detail = await response.text().catch(() => "");
2479
- console.error(`[pushpals] LocalBuddy rejected message: HTTP ${response.status} ${detail}`);
2480
- return false;
2481
- }
2482
- const reader = response.body?.getReader();
2483
- if (!reader) {
2484
- console.error("[pushpals] LocalBuddy response stream missing.");
2694
+ console.error(`[pushpals] Failed to reach server session endpoint: ${String(err)}`);
2485
2695
  return false;
2486
2696
  }
2487
- let buffer = "";
2488
- const decoder = new TextDecoder;
2489
- let complete = false;
2490
- let ok = true;
2491
- while (true) {
2492
- const { done, value } = await reader.read();
2493
- if (done)
2494
- break;
2495
- buffer += decoder.decode(value, { stream: true });
2496
- const chunks = buffer.split(`
2497
-
2498
- `);
2499
- buffer = chunks.pop() ?? "";
2500
- for (const chunk of chunks) {
2501
- const dataLine = chunk.split(/\r?\n/).map((line) => line.trim()).find((line) => line.startsWith("data: "));
2502
- if (!dataLine)
2503
- continue;
2504
- try {
2505
- const payload = JSON.parse(dataLine.slice(6));
2506
- const type = String(payload.type ?? "").trim().toLowerCase();
2507
- const message = String(payload.message ?? "").trim();
2508
- if (type === "status" && message) {
2509
- console.log(`[localbuddy] ${message}`);
2510
- } else if (type === "error") {
2511
- ok = false;
2512
- console.log(`[localbuddy] ERROR: ${message || "Unknown failure"}`);
2513
- } else if (type === "complete") {
2514
- complete = true;
2515
- const requestId = payload.data && typeof payload.data.requestId === "string" ? payload.data.requestId : "";
2516
- if (requestId) {
2517
- console.log(`[localbuddy] requestId=${requestId}`);
2518
- } else if (message) {
2519
- console.log(`[localbuddy] ${message}`);
2520
- }
2521
- }
2522
- } catch {}
2523
- }
2524
- }
2525
- return ok && complete;
2526
2697
  }
2527
2698
  function formatSessionEventLine(event) {
2528
2699
  const type = String(event.type ?? "").toLowerCase();
2529
2700
  const from = String(event.from ?? "");
2530
2701
  const payload = event.payload ?? {};
2702
+ if (!shouldDisplayInteractiveSessionEvent(event))
2703
+ return null;
2531
2704
  if (type === "message")
2532
2705
  return null;
2533
2706
  if (type === "assistant_message") {
@@ -2689,6 +2862,15 @@ async function main() {
2689
2862
  const serverUrl = normalizeLoopbackUrl(parsed.serverUrl ?? process.env.PUSHPALS_SERVER_URL, config.server.url);
2690
2863
  const localAgentUrl = normalizeLoopbackUrl(parsed.localAgentUrl ?? process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, config.client.localAgentUrl);
2691
2864
  const sessionId = String(parsed.sessionId ?? process.env.PUSHPALS_SESSION_ID ?? config.sessionId).trim();
2865
+ const cliVersion = String(process.env.PUSHPALS_CLI_PACKAGE_VERSION ?? "").trim() || "unknown";
2866
+ const cliClient = {
2867
+ clientId: createRuntimeClientId("cli"),
2868
+ kind: "cli",
2869
+ label: "CLI",
2870
+ version: cliVersion,
2871
+ platform: `${process.platform}/${process.arch}`,
2872
+ repoRoot
2873
+ };
2692
2874
  let autoStartedServices = [];
2693
2875
  const stopAutoStartedServices = () => {
2694
2876
  if (autoStartedServices.length === 0)
@@ -2697,66 +2879,76 @@ async function main() {
2697
2879
  autoStartedServices = [];
2698
2880
  };
2699
2881
  let serverHealthy = await probeServer(serverUrl);
2700
- let health = await probeLocalBuddy(localAgentUrl);
2701
- const runtimeNeedsAutoStart = parsed.runtimeOnly ? !serverHealthy : !health?.ok;
2702
- if (runtimeNeedsAutoStart && !parsed.noAutoStart) {
2882
+ const serverWasAlreadyHealthy = serverHealthy;
2883
+ let remoteBuddyConsumerHealth = {
2884
+ ok: false,
2885
+ detail: `No connected RemoteBuddy session consumer found for session ${sessionId}`
2886
+ };
2887
+ if (!serverHealthy) {
2888
+ if (!parsed.noAutoStart) {
2889
+ try {
2890
+ autoStartedServices = await autoStartRuntimeServices({
2891
+ repoRoot,
2892
+ serverUrl,
2893
+ localAgentUrl,
2894
+ sessionId,
2895
+ sourceControlManagerPort: config.sourceControlManager.port,
2896
+ sourceControlManagerRemote: config.sourceControlManager.remote,
2897
+ preparedRuntime,
2898
+ requestedRuntimeTag: parsed.runtimeTag,
2899
+ startLocalBuddy: resolveCliLocalBuddyAutostart(parsed.runtimeOnly, Boolean(config.localbuddy.enabled))
2900
+ });
2901
+ serverHealthy = await probeServer(serverUrl);
2902
+ } catch (err) {
2903
+ console.error(`[pushpals] Auto-start failed: ${String(err)}`);
2904
+ stopAutoStartedServices();
2905
+ }
2906
+ }
2907
+ if (!serverHealthy) {
2908
+ console.error(`[pushpals] Server is unavailable at ${serverUrl}.`);
2909
+ if (parsed.noAutoStart) {
2910
+ console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
2911
+ } else {
2912
+ console.error("[pushpals] Auto-start could not bring the embedded runtime online.");
2913
+ }
2914
+ process.exit(1);
2915
+ }
2916
+ }
2917
+ try {
2918
+ await ensureServerRepoAffinity(serverUrl, repoRoot);
2919
+ } catch (err) {
2920
+ stopAutoStartedServices();
2921
+ console.error(`[pushpals] Repo affinity check failed: ${String(err)}`);
2922
+ process.exit(1);
2923
+ }
2924
+ let activeSessionId = sessionId;
2925
+ if (!parsed.runtimeOnly) {
2703
2926
  try {
2704
- autoStartedServices = await autoStartRuntimeServices({
2705
- repoRoot,
2706
- serverUrl,
2707
- localAgentUrl,
2708
- sourceControlManagerPort: config.sourceControlManager.port,
2709
- sourceControlManagerRemote: config.sourceControlManager.remote,
2710
- preparedRuntime,
2711
- requestedRuntimeTag: parsed.runtimeTag,
2712
- requireLocalBuddy: !parsed.runtimeOnly
2713
- });
2714
- serverHealthy = await probeServer(serverUrl);
2715
- health = await probeLocalBuddy(localAgentUrl);
2927
+ activeSessionId = await ensureServerSession(serverUrl, sessionId, cliClient);
2716
2928
  } catch (err) {
2717
- console.error(`[pushpals] Auto-start failed: ${String(err)}`);
2718
2929
  stopAutoStartedServices();
2930
+ console.error(`[pushpals] Session bootstrap failed: ${String(err)}`);
2931
+ process.exit(1);
2719
2932
  }
2720
2933
  }
2721
- if (parsed.runtimeOnly && !serverHealthy) {
2934
+ remoteBuddyConsumerHealth = await probeRemoteBuddySessionConsumer(serverUrl, activeSessionId);
2935
+ if (!serverHealthy) {
2722
2936
  console.error(`[pushpals] Server is unavailable at ${serverUrl}.`);
2723
- if (parsed.noAutoStart) {
2724
- console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
2725
- } else {
2726
- console.error("[pushpals] Auto-start could not bring the embedded runtime online.");
2727
- }
2728
2937
  process.exit(1);
2729
2938
  }
2730
- if (!parsed.runtimeOnly && !health?.ok) {
2731
- console.error(`[pushpals] LocalBuddy is unavailable at ${localAgentUrl}.`);
2732
- if (parsed.noAutoStart) {
2939
+ if (!remoteBuddyConsumerHealth.ok) {
2940
+ stopAutoStartedServices();
2941
+ console.error(`[pushpals] RemoteBuddy is not ready for session ${activeSessionId}: ${remoteBuddyConsumerHealth.detail}`);
2942
+ if (serverWasAlreadyHealthy) {
2943
+ console.error("[pushpals] A PushPals runtime is already serving this repo, but it does not have a connected RemoteBuddy consumer for this session.");
2944
+ console.error("[pushpals] Refusing to start another embedded RemoteBuddy against the same runtime. Restart or stop the existing runtime before retrying.");
2945
+ } else if (parsed.noAutoStart) {
2733
2946
  console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
2734
2947
  } else {
2735
- console.error("[pushpals] Auto-start could not bring LocalBuddy online.");
2948
+ console.error("[pushpals] Auto-start could not bring the embedded runtime into a usable state.");
2736
2949
  }
2737
2950
  process.exit(1);
2738
2951
  }
2739
- let localBuddySessionId = sessionId;
2740
- if (!parsed.runtimeOnly) {
2741
- const localBuddyRepo = health?.repo ? resolve4(health.repo) : "";
2742
- if (!localBuddyRepo) {
2743
- stopAutoStartedServices();
2744
- console.error("[pushpals] LocalBuddy health response did not include repo path.");
2745
- process.exit(1);
2746
- }
2747
- if (normalizePath(localBuddyRepo) !== normalizePath(repoRoot)) {
2748
- stopAutoStartedServices();
2749
- console.error("[pushpals] Repo mismatch detected.");
2750
- console.error(`[pushpals] currentRepo=${repoRoot}`);
2751
- console.error(`[pushpals] localBuddyRepo=${localBuddyRepo}`);
2752
- console.error("[pushpals] LocalBuddy must run against the same repo. Start PushPals from this repo and retry.");
2753
- process.exit(1);
2754
- }
2755
- localBuddySessionId = health?.sessionId && String(health.sessionId).trim() ? String(health.sessionId).trim() : sessionId;
2756
- if (sessionId && sessionId !== localBuddySessionId) {
2757
- console.warn(`[pushpals] Requested sessionId=${sessionId}, but LocalBuddy is currently attached to sessionId=${localBuddySessionId}.`);
2758
- }
2759
- }
2760
2952
  const statePath = resolveCliStatePath(repoRoot);
2761
2953
  const saved = statePath ? readCliState(statePath) : {};
2762
2954
  const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
@@ -2765,8 +2957,7 @@ async function main() {
2765
2957
  preferredUrl: preferredHubUrl,
2766
2958
  fallbackPort: monitorPort,
2767
2959
  serverUrl,
2768
- localAgentUrl,
2769
- sessionId: localBuddySessionId
2960
+ sessionId: activeSessionId
2770
2961
  });
2771
2962
  const monitoringHubUrl = monitoringHub?.url ?? "";
2772
2963
  if (statePath) {
@@ -2774,7 +2965,7 @@ async function main() {
2774
2965
  monitoringHubUrl: monitoringHubUrl || undefined,
2775
2966
  serverUrl,
2776
2967
  localAgentUrl,
2777
- sessionId: localBuddySessionId,
2968
+ sessionId: activeSessionId,
2778
2969
  repoRoot
2779
2970
  });
2780
2971
  } else {
@@ -2790,8 +2981,7 @@ async function main() {
2790
2981
  console.log("[pushpals] monitoringHubUrl=unavailable");
2791
2982
  }
2792
2983
  console.log(`[pushpals] serverUrl=${serverUrl}`);
2793
- console.log(`[pushpals] localAgentUrl=${localAgentUrl}`);
2794
- console.log(`[pushpals] sessionId=${localBuddySessionId}`);
2984
+ console.log(`[pushpals] sessionId=${activeSessionId}`);
2795
2985
  console.log(`[pushpals] repoRoot=${repoRoot}`);
2796
2986
  console.log(`[pushpals] cliStateFile=${statePath ?? "unavailable"}`);
2797
2987
  if (parsed.runtimeOnly) {
@@ -2799,15 +2989,6 @@ async function main() {
2799
2989
  } else {
2800
2990
  console.log("[pushpals] Type a message and press Enter. Use /exit or exit to quit.");
2801
2991
  }
2802
- const cliVersion = String(process.env.PUSHPALS_CLI_PACKAGE_VERSION ?? "").trim() || "unknown";
2803
- const cliClient = {
2804
- clientId: createRuntimeClientId("cli"),
2805
- kind: "cli",
2806
- label: "CLI",
2807
- version: cliVersion,
2808
- platform: `${process.platform}/${process.arch}`,
2809
- repoRoot
2810
- };
2811
2992
  const streamAbort = new AbortController;
2812
2993
  let rl = null;
2813
2994
  const printIncoming = (line) => {
@@ -2822,7 +3003,7 @@ ${line}
2822
3003
  }
2823
3004
  console.log(line);
2824
3005
  };
2825
- const streamTask = parsed.noStream ? Promise.resolve() : parsed.runtimeOnly ? Promise.resolve() : runSessionStream(serverUrl, localBuddySessionId, cliClient, printIncoming, streamAbort.signal);
3006
+ const streamTask = parsed.noStream ? Promise.resolve() : parsed.runtimeOnly ? Promise.resolve() : runSessionStream(serverUrl, activeSessionId, cliClient, printIncoming, streamAbort.signal);
2826
3007
  let shuttingDown = false;
2827
3008
  const requestStop = () => {
2828
3009
  if (shuttingDown)
@@ -2900,8 +3081,7 @@ ${line}
2900
3081
  }
2901
3082
  if (text === "/status") {
2902
3083
  console.log(`[pushpals] serverUrl=${serverUrl}`);
2903
- console.log(`[pushpals] localAgentUrl=${localAgentUrl}`);
2904
- console.log(`[pushpals] sessionId=${localBuddySessionId}`);
3084
+ console.log(`[pushpals] sessionId=${activeSessionId}`);
2905
3085
  console.log(`[pushpals] repoRoot=${repoRoot}`);
2906
3086
  console.log(monitoringHubUrl ? `[pushpals] monitoringHubUrl=${monitoringHubUrl}` : "[pushpals] monitoringHubUrl=unavailable");
2907
3087
  rl.prompt();
@@ -2918,7 +3098,13 @@ ${line}
2918
3098
  rl.prompt();
2919
3099
  continue;
2920
3100
  }
2921
- const ok = await sendMessageToLocalBuddy(localAgentUrl, text);
3101
+ const normalized = normalizeCliInteractiveMessage(text);
3102
+ if (normalized.usageMessage) {
3103
+ console.log(`[pushpals] ${normalized.usageMessage}`);
3104
+ rl.prompt();
3105
+ continue;
3106
+ }
3107
+ const ok = await sendMessageToServerSession(serverUrl, activeSessionId, normalized.text);
2922
3108
  if (!ok) {
2923
3109
  console.log("[pushpals] Message failed.");
2924
3110
  }
@@ -2937,13 +3123,19 @@ export {
2937
3123
  startEmbeddedMonitoringHub,
2938
3124
  resolveCommandPath,
2939
3125
  resolveCliStatePath,
3126
+ resolveCliLocalBuddyAutostart,
2940
3127
  resolveBundledRuntimeAssetSource,
2941
3128
  resolveBundledMonitoringHubRoot,
2942
3129
  prepareCliRuntime,
3130
+ normalizeRepoPathForComparison,
3131
+ normalizeCliInteractiveMessage,
2943
3132
  normalizeChildProcessEnv,
2944
3133
  isCliExitCommand,
2945
3134
  injectMonitoringHubBootstrap,
2946
3135
  formatTimestampedCliLine,
3136
+ formatSessionEventLine,
3137
+ extractRemoteBuddySessionConsumerHealth,
3138
+ bundledMonitoringHubNeedsRefresh,
2947
3139
  buildServiceStopCommand,
2948
3140
  buildOpenMonitoringHubCommand,
2949
3141
  buildEmbeddedRuntimeEnv,