@pushpalsdev/cli 1.0.10 → 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,11 +7,13 @@ 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";
14
- import { dirname, extname, join as join2, resolve as resolve4 } from "path";
16
+ import { basename, delimiter, dirname, extname, join as join2, resolve as resolve4 } from "path";
15
17
  import { createInterface } from "readline";
16
18
 
17
19
  // ../shared/src/client_preflight.ts
@@ -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,6 +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"),
1718
+ ...typeof opts.sessionId === "string" && opts.sessionId.trim() ? { PUSHPALS_SESSION_ID: opts.sessionId.trim() } : {},
1640
1719
  ...typeof env.PUSHPALS_GIT_BIN === "string" && env.PUSHPALS_GIT_BIN.trim() ? { PUSHPALS_GIT_BIN: env.PUSHPALS_GIT_BIN.trim() } : {}
1641
1720
  };
1642
1721
  }
@@ -1827,6 +1906,34 @@ function stopRuntimeServices(services) {
1827
1906
  } catch {}
1828
1907
  }
1829
1908
  }
1909
+ function prependExecutableDirToPath(env, executablePath, platform = process.platform) {
1910
+ const resolvedPath = String(executablePath ?? "").trim();
1911
+ if (!resolvedPath)
1912
+ return env;
1913
+ if (!resolvedPath.includes("/") && !resolvedPath.includes("\\")) {
1914
+ return env;
1915
+ }
1916
+ const executableDir = dirname(resolvedPath);
1917
+ const existingPath = platform === "win32" ? String(env.Path ?? env.PATH ?? "") : String(env.PATH ?? "");
1918
+ const pathEntries = existingPath.split(delimiter).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
1919
+ const hasDir = pathEntries.some((entry) => entry.toLowerCase() === executableDir.toLowerCase());
1920
+ const nextPath = hasDir ? existingPath : [executableDir, ...pathEntries].join(delimiter);
1921
+ if (platform === "win32") {
1922
+ env.Path = nextPath;
1923
+ env.PATH = nextPath;
1924
+ } else {
1925
+ env.PATH = nextPath;
1926
+ }
1927
+ return env;
1928
+ }
1929
+ function applyResolvedGitBinaryToRuntimeEnv(env, resolvedGitBinary, platform = process.platform) {
1930
+ const resolvedPath = String(resolvedGitBinary ?? "").trim();
1931
+ if (!resolvedPath)
1932
+ return env;
1933
+ prependExecutableDirToPath(env, resolvedPath, platform);
1934
+ env.PUSHPALS_GIT_BIN = basename(resolvedPath);
1935
+ return env;
1936
+ }
1830
1937
  function isOptionalEmbeddedService(name) {
1831
1938
  return name === "source_control_manager";
1832
1939
  }
@@ -1860,6 +1967,111 @@ async function probeServer(serverUrl) {
1860
1967
  return false;
1861
1968
  }
1862
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
+ }
1863
2075
  async function probeSourceControlManager(port) {
1864
2076
  if (!Number.isFinite(port) || port <= 0)
1865
2077
  return false;
@@ -1911,13 +2123,43 @@ function createRuntimeClientId(prefix) {
1911
2123
  async function probeLocalBuddy(localAgentUrl) {
1912
2124
  return await fetchJsonWithTimeout(`${localAgentUrl}/healthz`, {}, LOCALBUDDY_TIMEOUT_MS);
1913
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
+ }
1914
2156
  async function autoStartRuntimeServices(opts) {
1915
2157
  const { runtimePreflight } = opts.preparedRuntime;
1916
2158
  const runtimeRoot = opts.preparedRuntime.runtimeRoot;
1917
2159
  const runtimeTag = opts.preparedRuntime.runtimeTag || await resolveRuntimeReleaseTag(opts.requestedRuntimeTag);
1918
- const localBuddyEnabled = Boolean(runtimePreflight.config.localbuddy.enabled);
1919
- const requireLocalBuddy = opts.requireLocalBuddy ?? true;
1920
- 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}`);
1921
2163
  console.log(`[pushpals] runtimeRoot=${runtimeRoot}`);
1922
2164
  console.log(`[pushpals] runtimeTag=${runtimeTag}`);
1923
2165
  if (!runtimePreflight.ok) {
@@ -1928,11 +2170,15 @@ async function autoStartRuntimeServices(opts) {
1928
2170
  const runtimeEnv = buildEmbeddedRuntimeEnv(process.env, {
1929
2171
  repoRoot: opts.repoRoot,
1930
2172
  runtimeRoot,
1931
- useRuntimeConfig: opts.preparedRuntime.preflightUsesEmbeddedRuntime
2173
+ useRuntimeConfig: opts.preparedRuntime.preflightUsesEmbeddedRuntime,
2174
+ sessionId: opts.sessionId
1932
2175
  });
2176
+ if (runtimeEnv.PUSHPALS_GIT_BIN) {
2177
+ applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, runtimeEnv.PUSHPALS_GIT_BIN);
2178
+ }
1933
2179
  const resolvedGitBinary = await resolveCommandPath("git", opts.repoRoot, normalizeChildProcessEnv(process.env));
1934
2180
  if (resolvedGitBinary) {
1935
- runtimeEnv.PUSHPALS_GIT_BIN = resolvedGitBinary;
2181
+ applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, resolvedGitBinary);
1936
2182
  }
1937
2183
  const services = [];
1938
2184
  const runToken = timestampFileToken();
@@ -1978,7 +2224,7 @@ ${tail}` : ""}`);
1978
2224
  services.push(localbuddyService);
1979
2225
  console.log(`[pushpals] localbuddy log: ${localbuddyService.logPath}`);
1980
2226
  } else {
1981
- console.log("[pushpals] Embedded LocalBuddy disabled by runtime config; skipping start.");
2227
+ console.log("[pushpals] Embedded LocalBuddy disabled for this CLI session; skipping start.");
1982
2228
  }
1983
2229
  console.log("[pushpals] Starting embedded RemoteBuddy...");
1984
2230
  const remotebuddyService = spawnRuntimeService("remotebuddy", [runtimeBinaries.remotebuddy], opts.repoRoot, runtimeEnv, logPathFor("remotebuddy"));
@@ -2030,7 +2276,8 @@ ${tail}` : ""}`);
2030
2276
  }
2031
2277
  }
2032
2278
  const health = localBuddyEnabled ? await probeLocalBuddy(opts.localAgentUrl) : null;
2033
- if (!requireLocalBuddy || localBuddyEnabled && health?.ok) {
2279
+ const remoteBuddyHealth2 = await probeRemoteBuddySessionConsumer(opts.serverUrl, opts.sessionId);
2280
+ if ((!localBuddyEnabled || health?.ok) && remoteBuddyHealth2.ok) {
2034
2281
  const stabilityDeadline = Date.now() + DEFAULT_SERVICE_STABILITY_GRACE_MS;
2035
2282
  while (Date.now() < stabilityDeadline) {
2036
2283
  for (let i = services.length - 1;i >= 0; i--) {
@@ -2061,10 +2308,14 @@ ${tail}` : ""}`);
2061
2308
  await Bun.sleep(DEFAULT_RUNTIME_BOOT_POLL_MS);
2062
2309
  }
2063
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
+ }
2064
2315
  if (!localBuddyEnabled) {
2065
2316
  throw new Error(`Timed out waiting for embedded runtime readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
2066
2317
  }
2067
- 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`);
2068
2319
  }
2069
2320
  function readCliState(pathValue) {
2070
2321
  if (!existsSync4(pathValue))
@@ -2117,7 +2368,6 @@ async function looksLikeMonitoringHub(url) {
2117
2368
  function buildMonitoringHubRuntimeBootstrap(opts) {
2118
2369
  return {
2119
2370
  serverUrl: opts.serverUrl,
2120
- localAgentUrl: opts.localAgentUrl,
2121
2371
  sessionId: opts.sessionId,
2122
2372
  clientId: `cli-monitor-${opts.sessionId}`,
2123
2373
  clientKind: "cli_monitor",
@@ -2202,7 +2452,10 @@ async function serveBundledMonitoringHub(assetRoot, pathname, bootstrap) {
2202
2452
  });
2203
2453
  }
2204
2454
  function buildEmbeddedMonitoringHubHtml(opts) {
2205
- const bootstrap = jsonHtmlBootstrap(opts);
2455
+ const bootstrap = jsonHtmlBootstrap({
2456
+ serverUrl: opts.serverUrl,
2457
+ sessionId: opts.sessionId
2458
+ });
2206
2459
  return `<!doctype html>
2207
2460
  <html lang="en">
2208
2461
  <head>
@@ -2298,7 +2551,6 @@ function buildEmbeddedMonitoringHubHtml(opts) {
2298
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("");
2299
2552
  metaEl.innerHTML = [
2300
2553
  '<span class="pill">server ' + esc(boot.serverUrl) + '</span>',
2301
- '<span class="pill">localbuddy ' + esc(boot.localAgentUrl) + '</span>',
2302
2554
  '<span class="pill">session ' + esc(boot.sessionId) + '</span>',
2303
2555
  '<span class="pill">repo ' + esc(repo?.root ?? repo?.remoteUrl ?? "current repo") + '</span>'
2304
2556
  ].join("");
@@ -2355,7 +2607,6 @@ async function startEmbeddedMonitoringHub(opts) {
2355
2607
  }
2356
2608
  const bootstrap = buildMonitoringHubRuntimeBootstrap({
2357
2609
  serverUrl: opts.serverUrl,
2358
- localAgentUrl: opts.localAgentUrl,
2359
2610
  sessionId: opts.sessionId
2360
2611
  });
2361
2612
  const candidatePorts = Array.from({ length: MONITOR_SCAN_PORTS }, (_, index) => opts.preferredPort + index).concat(0);
@@ -2426,72 +2677,30 @@ async function resolveMonitoringHub(opts) {
2426
2677
  }
2427
2678
  return embedded;
2428
2679
  }
2429
- async function sendMessageToLocalBuddy(localAgentUrl, text) {
2430
- let response;
2680
+ async function sendMessageToServerSession(serverUrl, sessionId, text) {
2431
2681
  try {
2432
- response = await fetchWithTimeout(`${localAgentUrl}/message`, {
2682
+ const response = await fetchWithTimeout(`${serverUrl}/sessions/${encodeURIComponent(sessionId)}/message`, {
2433
2683
  method: "POST",
2434
2684
  headers: { "Content-Type": "application/json" },
2435
2685
  body: JSON.stringify({ text })
2436
- }, 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;
2437
2693
  } catch (err) {
2438
- console.error(`[pushpals] Failed to reach LocalBuddy: ${String(err)}`);
2694
+ console.error(`[pushpals] Failed to reach server session endpoint: ${String(err)}`);
2439
2695
  return false;
2440
2696
  }
2441
- if (!response.ok) {
2442
- const detail = await response.text().catch(() => "");
2443
- console.error(`[pushpals] LocalBuddy rejected message: HTTP ${response.status} ${detail}`);
2444
- return false;
2445
- }
2446
- const reader = response.body?.getReader();
2447
- if (!reader) {
2448
- console.error("[pushpals] LocalBuddy response stream missing.");
2449
- return false;
2450
- }
2451
- let buffer = "";
2452
- const decoder = new TextDecoder;
2453
- let complete = false;
2454
- let ok = true;
2455
- while (true) {
2456
- const { done, value } = await reader.read();
2457
- if (done)
2458
- break;
2459
- buffer += decoder.decode(value, { stream: true });
2460
- const chunks = buffer.split(`
2461
-
2462
- `);
2463
- buffer = chunks.pop() ?? "";
2464
- for (const chunk of chunks) {
2465
- const dataLine = chunk.split(/\r?\n/).map((line) => line.trim()).find((line) => line.startsWith("data: "));
2466
- if (!dataLine)
2467
- continue;
2468
- try {
2469
- const payload = JSON.parse(dataLine.slice(6));
2470
- const type = String(payload.type ?? "").trim().toLowerCase();
2471
- const message = String(payload.message ?? "").trim();
2472
- if (type === "status" && message) {
2473
- console.log(`[localbuddy] ${message}`);
2474
- } else if (type === "error") {
2475
- ok = false;
2476
- console.log(`[localbuddy] ERROR: ${message || "Unknown failure"}`);
2477
- } else if (type === "complete") {
2478
- complete = true;
2479
- const requestId = payload.data && typeof payload.data.requestId === "string" ? payload.data.requestId : "";
2480
- if (requestId) {
2481
- console.log(`[localbuddy] requestId=${requestId}`);
2482
- } else if (message) {
2483
- console.log(`[localbuddy] ${message}`);
2484
- }
2485
- }
2486
- } catch {}
2487
- }
2488
- }
2489
- return ok && complete;
2490
2697
  }
2491
2698
  function formatSessionEventLine(event) {
2492
2699
  const type = String(event.type ?? "").toLowerCase();
2493
2700
  const from = String(event.from ?? "");
2494
2701
  const payload = event.payload ?? {};
2702
+ if (!shouldDisplayInteractiveSessionEvent(event))
2703
+ return null;
2495
2704
  if (type === "message")
2496
2705
  return null;
2497
2706
  if (type === "assistant_message") {
@@ -2653,6 +2862,15 @@ async function main() {
2653
2862
  const serverUrl = normalizeLoopbackUrl(parsed.serverUrl ?? process.env.PUSHPALS_SERVER_URL, config.server.url);
2654
2863
  const localAgentUrl = normalizeLoopbackUrl(parsed.localAgentUrl ?? process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, config.client.localAgentUrl);
2655
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
+ };
2656
2874
  let autoStartedServices = [];
2657
2875
  const stopAutoStartedServices = () => {
2658
2876
  if (autoStartedServices.length === 0)
@@ -2661,66 +2879,76 @@ async function main() {
2661
2879
  autoStartedServices = [];
2662
2880
  };
2663
2881
  let serverHealthy = await probeServer(serverUrl);
2664
- let health = await probeLocalBuddy(localAgentUrl);
2665
- const runtimeNeedsAutoStart = parsed.runtimeOnly ? !serverHealthy : !health?.ok;
2666
- 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) {
2667
2926
  try {
2668
- autoStartedServices = await autoStartRuntimeServices({
2669
- repoRoot,
2670
- serverUrl,
2671
- localAgentUrl,
2672
- sourceControlManagerPort: config.sourceControlManager.port,
2673
- sourceControlManagerRemote: config.sourceControlManager.remote,
2674
- preparedRuntime,
2675
- requestedRuntimeTag: parsed.runtimeTag,
2676
- requireLocalBuddy: !parsed.runtimeOnly
2677
- });
2678
- serverHealthy = await probeServer(serverUrl);
2679
- health = await probeLocalBuddy(localAgentUrl);
2927
+ activeSessionId = await ensureServerSession(serverUrl, sessionId, cliClient);
2680
2928
  } catch (err) {
2681
- console.error(`[pushpals] Auto-start failed: ${String(err)}`);
2682
2929
  stopAutoStartedServices();
2930
+ console.error(`[pushpals] Session bootstrap failed: ${String(err)}`);
2931
+ process.exit(1);
2683
2932
  }
2684
2933
  }
2685
- if (parsed.runtimeOnly && !serverHealthy) {
2934
+ remoteBuddyConsumerHealth = await probeRemoteBuddySessionConsumer(serverUrl, activeSessionId);
2935
+ if (!serverHealthy) {
2686
2936
  console.error(`[pushpals] Server is unavailable at ${serverUrl}.`);
2687
- if (parsed.noAutoStart) {
2688
- console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
2689
- } else {
2690
- console.error("[pushpals] Auto-start could not bring the embedded runtime online.");
2691
- }
2692
2937
  process.exit(1);
2693
2938
  }
2694
- if (!parsed.runtimeOnly && !health?.ok) {
2695
- console.error(`[pushpals] LocalBuddy is unavailable at ${localAgentUrl}.`);
2696
- 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) {
2697
2946
  console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
2698
2947
  } else {
2699
- 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.");
2700
2949
  }
2701
2950
  process.exit(1);
2702
2951
  }
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
- }
2723
- }
2724
2952
  const statePath = resolveCliStatePath(repoRoot);
2725
2953
  const saved = statePath ? readCliState(statePath) : {};
2726
2954
  const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
@@ -2729,8 +2957,7 @@ async function main() {
2729
2957
  preferredUrl: preferredHubUrl,
2730
2958
  fallbackPort: monitorPort,
2731
2959
  serverUrl,
2732
- localAgentUrl,
2733
- sessionId: localBuddySessionId
2960
+ sessionId: activeSessionId
2734
2961
  });
2735
2962
  const monitoringHubUrl = monitoringHub?.url ?? "";
2736
2963
  if (statePath) {
@@ -2738,7 +2965,7 @@ async function main() {
2738
2965
  monitoringHubUrl: monitoringHubUrl || undefined,
2739
2966
  serverUrl,
2740
2967
  localAgentUrl,
2741
- sessionId: localBuddySessionId,
2968
+ sessionId: activeSessionId,
2742
2969
  repoRoot
2743
2970
  });
2744
2971
  } else {
@@ -2754,8 +2981,7 @@ async function main() {
2754
2981
  console.log("[pushpals] monitoringHubUrl=unavailable");
2755
2982
  }
2756
2983
  console.log(`[pushpals] serverUrl=${serverUrl}`);
2757
- console.log(`[pushpals] localAgentUrl=${localAgentUrl}`);
2758
- console.log(`[pushpals] sessionId=${localBuddySessionId}`);
2984
+ console.log(`[pushpals] sessionId=${activeSessionId}`);
2759
2985
  console.log(`[pushpals] repoRoot=${repoRoot}`);
2760
2986
  console.log(`[pushpals] cliStateFile=${statePath ?? "unavailable"}`);
2761
2987
  if (parsed.runtimeOnly) {
@@ -2763,15 +2989,6 @@ async function main() {
2763
2989
  } else {
2764
2990
  console.log("[pushpals] Type a message and press Enter. Use /exit or exit to quit.");
2765
2991
  }
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
- };
2775
2992
  const streamAbort = new AbortController;
2776
2993
  let rl = null;
2777
2994
  const printIncoming = (line) => {
@@ -2786,7 +3003,7 @@ ${line}
2786
3003
  }
2787
3004
  console.log(line);
2788
3005
  };
2789
- 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);
2790
3007
  let shuttingDown = false;
2791
3008
  const requestStop = () => {
2792
3009
  if (shuttingDown)
@@ -2864,8 +3081,7 @@ ${line}
2864
3081
  }
2865
3082
  if (text === "/status") {
2866
3083
  console.log(`[pushpals] serverUrl=${serverUrl}`);
2867
- console.log(`[pushpals] localAgentUrl=${localAgentUrl}`);
2868
- console.log(`[pushpals] sessionId=${localBuddySessionId}`);
3084
+ console.log(`[pushpals] sessionId=${activeSessionId}`);
2869
3085
  console.log(`[pushpals] repoRoot=${repoRoot}`);
2870
3086
  console.log(monitoringHubUrl ? `[pushpals] monitoringHubUrl=${monitoringHubUrl}` : "[pushpals] monitoringHubUrl=unavailable");
2871
3087
  rl.prompt();
@@ -2882,7 +3098,13 @@ ${line}
2882
3098
  rl.prompt();
2883
3099
  continue;
2884
3100
  }
2885
- 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);
2886
3108
  if (!ok) {
2887
3109
  console.log("[pushpals] Message failed.");
2888
3110
  }
@@ -2901,15 +3123,22 @@ export {
2901
3123
  startEmbeddedMonitoringHub,
2902
3124
  resolveCommandPath,
2903
3125
  resolveCliStatePath,
3126
+ resolveCliLocalBuddyAutostart,
2904
3127
  resolveBundledRuntimeAssetSource,
2905
3128
  resolveBundledMonitoringHubRoot,
2906
3129
  prepareCliRuntime,
3130
+ normalizeRepoPathForComparison,
3131
+ normalizeCliInteractiveMessage,
2907
3132
  normalizeChildProcessEnv,
2908
3133
  isCliExitCommand,
2909
3134
  injectMonitoringHubBootstrap,
2910
3135
  formatTimestampedCliLine,
3136
+ formatSessionEventLine,
3137
+ extractRemoteBuddySessionConsumerHealth,
3138
+ bundledMonitoringHubNeedsRefresh,
2911
3139
  buildServiceStopCommand,
2912
3140
  buildOpenMonitoringHubCommand,
2913
3141
  buildEmbeddedRuntimeEnv,
2914
- buildEmbeddedMonitoringHubHtml
3142
+ buildEmbeddedMonitoringHubHtml,
3143
+ applyResolvedGitBinaryToRuntimeEnv
2915
3144
  };