@snowyroad/arp 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +94 -14
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1867,7 +1867,7 @@ var ActivityBeacon = class {
1867
1867
  // src/adapter.ts
1868
1868
  import { query } from "@anthropic-ai/claude-agent-sdk";
1869
1869
  import { accessSync, constants, existsSync as existsSync2, statSync } from "fs";
1870
- import { delimiter, dirname as dirname2, join as join3, resolve as resolve2 } from "path";
1870
+ import { delimiter, dirname as dirname3, join as join4, resolve as resolve2 } from "path";
1871
1871
 
1872
1872
  // src/acp/client.ts
1873
1873
  import { spawn } from "child_process";
@@ -1967,6 +1967,7 @@ var AcpClient = class {
1967
1967
  child = null;
1968
1968
  conn = null;
1969
1969
  _sessionId = null;
1970
+ loadSupported = false;
1970
1971
  /**
1971
1972
  * The currently-running turn's reply accumulator. Set for the duration of one
1972
1973
  * turn so agent_message_chunk text lands in THIS turn's buffer only. Because
@@ -2075,7 +2076,7 @@ var AcpClient = class {
2075
2076
  }
2076
2077
  };
2077
2078
  this.conn = new ClientSideConnection(() => client, stream);
2078
- await this.guard(
2079
+ const init = await this.guard(
2079
2080
  this.conn.initialize({
2080
2081
  protocolVersion: 1,
2081
2082
  clientCapabilities: {
@@ -2085,10 +2086,29 @@ var AcpClient = class {
2085
2086
  clientInfo: { name: "arp-bridge", version: "0.1.0" }
2086
2087
  })
2087
2088
  );
2088
- const session = await this.guard(
2089
- this.conn.newSession({ cwd: this.launch.cwd, mcpServers: [] })
2090
- );
2091
- this._sessionId = session.sessionId;
2089
+ this.loadSupported = init.agentCapabilities?.loadSession === true;
2090
+ const candidateId = this._sessionId ?? this.launch.session?.persistedId ?? null;
2091
+ let liveId = null;
2092
+ if (candidateId && this.loadSupported) {
2093
+ try {
2094
+ await this.guard(
2095
+ this.conn.loadSession({ sessionId: candidateId, cwd: this.launch.cwd, mcpServers: [] })
2096
+ );
2097
+ liveId = candidateId;
2098
+ } catch (err) {
2099
+ console.warn(
2100
+ `[arp-bridge] session/load failed; starting a fresh session: ${sanitizeForTty(String(err?.message ?? err))}`
2101
+ );
2102
+ }
2103
+ }
2104
+ if (!liveId) {
2105
+ const session = await this.guard(
2106
+ this.conn.newSession({ cwd: this.launch.cwd, mcpServers: [] })
2107
+ );
2108
+ liveId = session.sessionId;
2109
+ }
2110
+ this._sessionId = liveId;
2111
+ this.launch.session?.save(liveId);
2092
2112
  }
2093
2113
  /**
2094
2114
  * Send one user turn. Resolves with the full assembled reply text once the
@@ -2189,6 +2209,45 @@ var AcpClient = class {
2189
2209
  }
2190
2210
  };
2191
2211
 
2212
+ // src/sessionStore.ts
2213
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, chmodSync as chmodSync2 } from "fs";
2214
+ import { join as join3, dirname as dirname2 } from "path";
2215
+ function safe(s) {
2216
+ return s.replace(/[^a-zA-Z0-9._-]/g, "_");
2217
+ }
2218
+ function sessionFilePath(dir, relayUrl, agentName, channelId) {
2219
+ return join3(dir, "sessions", relayHostSegment(relayUrl), `${safe(agentName)}__${safe(channelId)}.json`);
2220
+ }
2221
+ function loadStoredSession(dir, relayUrl, agentName, channelId) {
2222
+ const file = sessionFilePath(dir, relayUrl, agentName, channelId);
2223
+ let raw;
2224
+ try {
2225
+ raw = readFileSync2(file, "utf8");
2226
+ } catch {
2227
+ return null;
2228
+ }
2229
+ try {
2230
+ const p = JSON.parse(raw);
2231
+ if (typeof p.sessionId === "string" && p.sessionId.trim() !== "" && typeof p.cwd === "string" && p.cwd.trim() !== "") {
2232
+ return { sessionId: p.sessionId, cwd: p.cwd };
2233
+ }
2234
+ return null;
2235
+ } catch {
2236
+ return null;
2237
+ }
2238
+ }
2239
+ function saveStoredSession(dir, relayUrl, agentName, channelId, rec) {
2240
+ const file = sessionFilePath(dir, relayUrl, agentName, channelId);
2241
+ mkdirSync2(dirname2(file), { recursive: true, mode: 448 });
2242
+ const tmp = `${file}.tmp-${process.pid}`;
2243
+ writeFileSync2(tmp, JSON.stringify(rec, null, 2) + "\n", { mode: 384 });
2244
+ renameSync2(tmp, file);
2245
+ try {
2246
+ chmodSync2(file, 384);
2247
+ } catch {
2248
+ }
2249
+ }
2250
+
2192
2251
  // src/adapter.ts
2193
2252
  function defaultToolPolicy() {
2194
2253
  return { mode: "readonly", configDirAbs: resolve2(configDir(process.env)) };
@@ -2205,9 +2264,9 @@ function pinned(pkg) {
2205
2264
  var npxBinaryAbs = null;
2206
2265
  function resolveNpxBinary() {
2207
2266
  if (npxBinaryAbs) return npxBinaryAbs;
2208
- const nodeDir = dirname2(process.execPath);
2267
+ const nodeDir = dirname3(process.execPath);
2209
2268
  for (const name of ["npx", "npx.cmd"]) {
2210
- const candidate = join3(nodeDir, name);
2269
+ const candidate = join4(nodeDir, name);
2211
2270
  if (existsSync2(candidate)) {
2212
2271
  npxBinaryAbs = candidate;
2213
2272
  return candidate;
@@ -2220,7 +2279,7 @@ function resolveNpxBinary() {
2220
2279
  function which(cmd, pathEnv = process.env.PATH ?? "") {
2221
2280
  for (const dir of pathEnv.split(delimiter)) {
2222
2281
  if (!dir) continue;
2223
- const candidate = join3(dir, cmd);
2282
+ const candidate = join4(dir, cmd);
2224
2283
  try {
2225
2284
  accessSync(candidate, constants.X_OK);
2226
2285
  if (statSync(candidate).isFile()) return resolve2(candidate);
@@ -2274,13 +2333,15 @@ var defaultAcpClientFactory = (launch) => new AcpClient(launch);
2274
2333
  var MAX_CONSECUTIVE_RESTARTS = 3;
2275
2334
  var RESTART_BACKOFF_MS = 250;
2276
2335
  var AcpAdapter = class {
2277
- constructor(agent, makeClient = defaultAcpClientFactory, backoffMs = RESTART_BACKOFF_MS, policy = defaultToolPolicy()) {
2336
+ constructor(agent, makeClient = defaultAcpClientFactory, backoffMs = RESTART_BACKOFF_MS, policy = defaultToolPolicy(), session) {
2278
2337
  this.makeClient = makeClient;
2279
2338
  this.backoffMs = backoffMs;
2339
+ this.session = session;
2280
2340
  this.launch = { ...launchSpecFor(agent), policy };
2281
2341
  }
2282
2342
  makeClient;
2283
2343
  backoffMs;
2344
+ session;
2284
2345
  launch;
2285
2346
  // --- supervised live state (set in start()) ---
2286
2347
  client = null;
@@ -2294,7 +2355,17 @@ var AcpAdapter = class {
2294
2355
  /** Latched once the loop guard trips, so we stop trying (and stop retrying turns). */
2295
2356
  gaveUp = false;
2296
2357
  async start(_opts) {
2297
- this.client = this.makeClient(this.launch);
2358
+ const rec = this.session?.load() ?? null;
2359
+ const cwd = rec?.cwd ?? this.launch.cwd;
2360
+ const launch = {
2361
+ ...this.launch,
2362
+ cwd,
2363
+ session: this.session ? {
2364
+ persistedId: rec?.sessionId ?? null,
2365
+ save: (id) => this.session.save({ sessionId: id, cwd })
2366
+ } : void 0
2367
+ };
2368
+ this.client = this.makeClient(launch);
2298
2369
  await this.client.start();
2299
2370
  return {
2300
2371
  submit: (text) => {
@@ -2520,14 +2591,23 @@ var ClaudeAdapter = class {
2520
2591
  };
2521
2592
  }
2522
2593
  };
2523
- function createAdapter(cfg) {
2594
+ function createAdapter(cfg, channelId) {
2524
2595
  const policy = {
2525
2596
  mode: cfg.toolMode,
2526
2597
  configDirAbs: resolve2(configDir(process.env)),
2527
2598
  agentName: cfg.agentName
2528
2599
  // for the once-per-process "arp tools full <name>" denial hint
2529
2600
  };
2530
- return cfg.agentMode === "acp" ? new AcpAdapter(cfg.agent, void 0, void 0, policy) : new ClaudeAdapter(policy);
2601
+ if (cfg.agentMode !== "acp") return new ClaudeAdapter(policy);
2602
+ const session = channelId ? makeSessionPersistence(cfg, channelId) : void 0;
2603
+ return new AcpAdapter(cfg.agent, void 0, void 0, policy, session);
2604
+ }
2605
+ function makeSessionPersistence(cfg, channelId) {
2606
+ const dir = configDir(process.env);
2607
+ return {
2608
+ load: () => loadStoredSession(dir, cfg.relayWsUrl, cfg.agentName, channelId),
2609
+ save: (rec) => saveStoredSession(dir, cfg.relayWsUrl, cfg.agentName, channelId, rec)
2610
+ };
2531
2611
  }
2532
2612
 
2533
2613
  // src/elicit.ts
@@ -2612,7 +2692,7 @@ async function startBridge(cfg, relay, deps) {
2612
2692
  const inFlight = pending.get(channelId);
2613
2693
  if (inFlight) return inFlight;
2614
2694
  const p = (async () => {
2615
- const adapter = deps.makeAdapter(cfg);
2695
+ const adapter = deps.makeAdapter(cfg, channelId);
2616
2696
  const beacon = new ActivityBeacon((state) => relay.sendActivity(channelId, state));
2617
2697
  const session = new ChannelSession(
2618
2698
  adapter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowyroad/arp",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Connect your own coding agent (Claude Code, Codex, Gemini, Grok) to an Agent Relay Protocol channel and collaborate with other agents and humans.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "SnowyRoad",