@wrongstack/acp 0.273.1 → 0.275.0

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.
package/dist/agent.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { writeErr, expectDefined } from '@wrongstack/core';
2
+ import * as fsp from 'fs/promises';
3
+ import * as path from 'path';
2
4
  import { fileURLToPath } from 'url';
5
+ import { createServer } from 'http';
3
6
 
4
7
  // src/agent/stdio-transport.ts
5
8
  var StdioTransport = class {
@@ -212,7 +215,16 @@ var ACP_PROTOCOL_VERSION = 1;
212
215
  function toWire(msg) {
213
216
  return msg;
214
217
  }
215
- var WRONGSTACK_VERSION = "0.263.0";
218
+ var WRONGSTACK_VERSION = "0.274.1";
219
+ var WRONGSTACK_AUTH_METHODS = [
220
+ {
221
+ id: "wrongstack-auth",
222
+ name: "Run wstack auth",
223
+ description: "Configure a WrongStack model provider in an interactive terminal.",
224
+ type: "terminal",
225
+ args: ["auth"]
226
+ }
227
+ ];
216
228
  var DEFAULT_MODE_ID = "code";
217
229
  var DEFAULT_MODES = [
218
230
  {
@@ -229,9 +241,17 @@ var ACPProtocolHandler = class {
229
241
  modes;
230
242
  configOptions;
231
243
  agentName;
244
+ replayFor;
245
+ seedFor;
246
+ store;
232
247
  initialized = false;
248
+ clientCapabilities = {};
233
249
  sessions = /* @__PURE__ */ new Map();
234
250
  nextId = 1;
251
+ // Outbound request correlation (server → client requests, e.g.
252
+ // session/request_permission). Keyed by our own `srv_N` ids.
253
+ pendingOut = /* @__PURE__ */ new Map();
254
+ nextOutId = 1;
235
255
  constructor(opts) {
236
256
  this.transport = opts.transport;
237
257
  this.defaultCwd = opts.defaultCwd;
@@ -241,6 +261,43 @@ var ACPProtocolHandler = class {
241
261
  this.modes = opts.modes ?? DEFAULT_MODES;
242
262
  this.configOptions = opts.configOptions ?? [];
243
263
  this.agentName = opts.agentName ?? "wrongstack";
264
+ this.replayFor = opts.replayFor;
265
+ this.seedFor = opts.seedFor;
266
+ this.store = opts.store;
267
+ if (typeof this.transport.onMessage === "function") {
268
+ this.transport.onMessage((m) => this.maybeResolvePending(m));
269
+ }
270
+ }
271
+ /**
272
+ * Send a request to the client and await its response. Used for
273
+ * server-initiated calls like `session/request_permission`. Rejects on
274
+ * timeout or transport error so the caller can pick a safe fallback.
275
+ */
276
+ request(method, params, timeoutMs = 6e4) {
277
+ const id = `srv_${this.nextOutId++}`;
278
+ return new Promise((resolve, reject) => {
279
+ const timer = setTimeout(() => {
280
+ this.pendingOut.delete(id);
281
+ reject(new Error(`${method} timed out after ${timeoutMs}ms`));
282
+ }, timeoutMs);
283
+ this.pendingOut.set(id, { resolve, reject, timer });
284
+ this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
285
+ clearTimeout(timer);
286
+ this.pendingOut.delete(id);
287
+ reject(e instanceof Error ? e : new Error(String(e)));
288
+ });
289
+ });
290
+ }
291
+ maybeResolvePending(m) {
292
+ const id = m.id;
293
+ if (typeof id !== "string") return;
294
+ const pending = this.pendingOut.get(id);
295
+ if (!pending) return;
296
+ this.pendingOut.delete(id);
297
+ clearTimeout(pending.timer);
298
+ const err = m.error;
299
+ if (err) pending.reject(new Error(err.message ?? "client request failed"));
300
+ else pending.resolve(m.result);
244
301
  }
245
302
  /**
246
303
  * Process one inbound message. Returns true if this was a terminal
@@ -267,6 +324,11 @@ var ACPProtocolHandler = class {
267
324
  session.abort.abort();
268
325
  }
269
326
  this.sessions.clear();
327
+ for (const [, p] of this.pendingOut) {
328
+ clearTimeout(p.timer);
329
+ p.reject(new Error("protocol handler closed"));
330
+ }
331
+ this.pendingOut.clear();
270
332
  }
271
333
  // ────────────────────────────────────────────────────────────────────
272
334
  // Requests
@@ -282,10 +344,18 @@ var ACPProtocolHandler = class {
282
344
  return await this.handleInitialize(id, params);
283
345
  case "authenticate":
284
346
  return await this.handleAuthenticate(id, params);
347
+ case "logout":
348
+ return await this.handleLogout(id, params);
285
349
  case "session/new":
286
350
  return await this.handleSessionNew(id, params);
287
351
  case "session/load":
288
352
  return await this.handleSessionLoad(id, params);
353
+ case "session/resume":
354
+ return await this.handleSessionResume(id, params);
355
+ case "session/close":
356
+ return await this.handleSessionClose(id, params);
357
+ case "session/delete":
358
+ return await this.handleSessionDelete(id, params);
289
359
  case "session/prompt":
290
360
  return await this.handleSessionPrompt(id, params);
291
361
  case "session/set_mode":
@@ -294,6 +364,16 @@ var ACPProtocolHandler = class {
294
364
  return await this.handleSetConfigOption(id, params);
295
365
  case "session/list":
296
366
  return await this.handleSessionList(id);
367
+ case "session/fork":
368
+ return await this.handleSessionFork(id, params);
369
+ case "providers/list":
370
+ return await this.handleProvidersList(id, params);
371
+ case "providers/set":
372
+ return await this.handleProvidersSet(id, params);
373
+ case "providers/disable":
374
+ return await this.handleProvidersDisable(id, params);
375
+ case "mcp/message":
376
+ return await this.handleMcpMessage(id, params);
297
377
  default:
298
378
  await this.sendError(id, -32601, `Unknown method: ${method}`);
299
379
  return false;
@@ -306,14 +386,8 @@ var ACPProtocolHandler = class {
306
386
  }
307
387
  async handleInitialize(id, params) {
308
388
  const p = params ?? {};
309
- const requested = typeof p.protocolVersion === "number" ? p.protocolVersion : 1;
310
- if (requested !== ACP_PROTOCOL_VERSION) {
311
- await this.sendError(
312
- id,
313
- -32e3,
314
- `server speaks protocolVersion=${ACP_PROTOCOL_VERSION}, client requested ${requested}`
315
- );
316
- return false;
389
+ if (p.clientCapabilities && typeof p.clientCapabilities === "object") {
390
+ this.clientCapabilities = p.clientCapabilities;
317
391
  }
318
392
  this.initialized = true;
319
393
  await this.transport.send(toWire({
@@ -324,9 +398,25 @@ var ACPProtocolHandler = class {
324
398
  agentCapabilities: {
325
399
  loadSession: true,
326
400
  promptCapabilities: {
327
- image: false,
401
+ // We route ACP image blocks into the core agent's multimodal
402
+ // input (server-agent-turn.promptToAgentInput); whether the
403
+ // model can see them is the configured provider's concern.
404
+ image: true,
328
405
  audio: false,
329
406
  embeddedContext: true
407
+ },
408
+ mcpCapabilities: {
409
+ http: false,
410
+ sse: false
411
+ },
412
+ sessionCapabilities: {
413
+ close: {},
414
+ list: {},
415
+ delete: {},
416
+ resume: {}
417
+ },
418
+ auth: {
419
+ logout: {}
330
420
  }
331
421
  },
332
422
  agentInfo: {
@@ -334,10 +424,7 @@ var ACPProtocolHandler = class {
334
424
  title: "WrongStack",
335
425
  version: WRONGSTACK_VERSION
336
426
  },
337
- // Static options advertised at handshake. They are also
338
- // re-sent on every `current_mode_update` / `config_option_update`
339
- // notification so late-joining clients see them.
340
- authMethods: [],
427
+ authMethods: WRONGSTACK_AUTH_METHODS,
341
428
  modes: this.modes,
342
429
  configOptions: this.configOptions
343
430
  }
@@ -352,6 +439,14 @@ var ACPProtocolHandler = class {
352
439
  }));
353
440
  return false;
354
441
  }
442
+ async handleLogout(id, _params) {
443
+ await this.transport.send(toWire({
444
+ jsonrpc: "2.0",
445
+ id,
446
+ result: {}
447
+ }));
448
+ return false;
449
+ }
355
450
  async handleSessionNew(id, params) {
356
451
  const p = params ?? {};
357
452
  const cwd = typeof p.cwd === "string" ? p.cwd : this.defaultCwd;
@@ -367,6 +462,7 @@ var ACPProtocolHandler = class {
367
462
  };
368
463
  this.sessions.set(sessionId, state);
369
464
  this.onSessionNew(state);
465
+ await this.persist(state);
370
466
  await this.sendNotification({
371
467
  sessionId,
372
468
  update: {
@@ -395,7 +491,173 @@ var ACPProtocolHandler = class {
395
491
  return false;
396
492
  }
397
493
  async handleSessionLoad(id, params) {
398
- return this.handleSessionNew(id, params);
494
+ const p = params ?? {};
495
+ const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
496
+ const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
497
+ let existing = sessionId ? this.sessions.get(sessionId) : void 0;
498
+ if (!existing && sessionId && this.store) {
499
+ const persisted = await this.store.load(sessionId);
500
+ if (persisted) {
501
+ const restored = {
502
+ id: sessionId,
503
+ cwd: persisted.cwd ?? loadCwd ?? this.defaultCwd,
504
+ abort: new AbortController(),
505
+ modeId: persisted.modeId ?? DEFAULT_MODE_ID,
506
+ createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
507
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
508
+ ...persisted.title !== void 0 ? { title: persisted.title } : {}
509
+ };
510
+ this.sessions.set(sessionId, restored);
511
+ this.seedFor?.(sessionId, persisted.history ?? []);
512
+ for (const update of persisted.history ?? []) {
513
+ await this.sendNotification({ sessionId, update });
514
+ }
515
+ await this.sendNotification({
516
+ sessionId,
517
+ update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
518
+ });
519
+ await this.transport.send(toWire({
520
+ jsonrpc: "2.0",
521
+ id,
522
+ result: {
523
+ initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
524
+ }
525
+ }));
526
+ return false;
527
+ }
528
+ }
529
+ if (existing) {
530
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
531
+ const replay = sessionId ? this.replayFor?.(sessionId) : void 0;
532
+ if (replay) {
533
+ for (const update of replay) {
534
+ await this.sendNotification({ sessionId, update });
535
+ }
536
+ }
537
+ await this.sendNotification({
538
+ sessionId,
539
+ update: {
540
+ sessionUpdate: "session_info_update",
541
+ updatedAt: existing.updatedAt
542
+ }
543
+ });
544
+ await this.sendNotification({
545
+ sessionId,
546
+ update: {
547
+ sessionUpdate: "current_mode_update",
548
+ modeId: existing.modeId
549
+ }
550
+ });
551
+ await this.transport.send(toWire({
552
+ jsonrpc: "2.0",
553
+ id,
554
+ result: {
555
+ initialMode: {
556
+ currentModeId: existing.modeId,
557
+ availableModes: this.modes
558
+ }
559
+ }
560
+ }));
561
+ return false;
562
+ }
563
+ await this.sendError(id, -32e3, `session not found: ${sessionId}`);
564
+ return false;
565
+ }
566
+ async handleSessionResume(id, params) {
567
+ const p = params ?? {};
568
+ const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
569
+ const existing = sessionId ? this.sessions.get(sessionId) : void 0;
570
+ if (existing) {
571
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
572
+ await this.transport.send(toWire({
573
+ jsonrpc: "2.0",
574
+ id,
575
+ result: {
576
+ initialMode: {
577
+ currentModeId: existing.modeId,
578
+ availableModes: this.modes
579
+ }
580
+ }
581
+ }));
582
+ return false;
583
+ }
584
+ await this.sendError(id, -32e3, `session not found: ${sessionId}`);
585
+ return false;
586
+ }
587
+ async handleSessionClose(id, params) {
588
+ const p = params ?? {};
589
+ const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
590
+ const session = sessionId ? this.sessions.get(sessionId) : void 0;
591
+ if (!session) {
592
+ await this.sendError(id, -32e3, `session not found: ${sessionId}`);
593
+ return false;
594
+ }
595
+ session.abort.abort();
596
+ if (sessionId) this.sessions.delete(sessionId);
597
+ await this.transport.send(toWire({
598
+ jsonrpc: "2.0",
599
+ id,
600
+ result: {}
601
+ }));
602
+ return false;
603
+ }
604
+ async handleSessionDelete(id, params) {
605
+ const p = params ?? {};
606
+ const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
607
+ if (!sessionId) {
608
+ await this.sendError(id, -32e3, `session not found: ${sessionId}`);
609
+ return false;
610
+ }
611
+ if (!this.sessions.has(sessionId)) {
612
+ await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
613
+ return false;
614
+ }
615
+ const session = this.sessions.get(sessionId);
616
+ session.abort.abort();
617
+ this.sessions.delete(sessionId);
618
+ await this.transport.send(toWire({
619
+ jsonrpc: "2.0",
620
+ id,
621
+ result: {}
622
+ }));
623
+ return false;
624
+ }
625
+ async handleSessionFork(id, params) {
626
+ const p = params ?? {};
627
+ const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
628
+ if (!sourceId || !this.sessions.has(sourceId)) {
629
+ await this.sendError(id, -32e3, `session not found: ${sourceId}`);
630
+ return false;
631
+ }
632
+ const forkParams = params;
633
+ return this.handleSessionNew(id, { ...forkParams, cwd: p.cwd ?? this.defaultCwd });
634
+ }
635
+ async handleProvidersList(id, _params) {
636
+ await this.transport.send(toWire({
637
+ jsonrpc: "2.0",
638
+ id,
639
+ result: {
640
+ providers: [],
641
+ currentProviderId: null
642
+ }
643
+ }));
644
+ return false;
645
+ }
646
+ async handleProvidersSet(id, _params) {
647
+ await this.sendError(id, -32e3, "provider configuration not available through ACP; use wstack auth");
648
+ return false;
649
+ }
650
+ async handleProvidersDisable(id, _params) {
651
+ await this.transport.send(toWire({
652
+ jsonrpc: "2.0",
653
+ id,
654
+ result: {}
655
+ }));
656
+ return false;
657
+ }
658
+ async handleMcpMessage(id, _params) {
659
+ await this.sendError(id, -32e3, "MCP message routing not available through ACP");
660
+ return false;
399
661
  }
400
662
  async handleSessionPrompt(id, params) {
401
663
  const p = params ?? {};
@@ -415,11 +677,54 @@ var ACPProtocolHandler = class {
415
677
  const turnSignal = new AbortController();
416
678
  const onCancel = () => turnSignal.abort();
417
679
  session.abort.signal.addEventListener("abort", onCancel, { once: true });
680
+ const api = {
681
+ clientCapabilities: this.clientCapabilities,
682
+ requestPermission: async (req) => {
683
+ const res = await this.request("session/request_permission", {
684
+ sessionId,
685
+ toolCall: req.toolCall,
686
+ options: req.options
687
+ });
688
+ const outcome = res?.outcome;
689
+ return outcome ?? { outcome: "cancelled" };
690
+ },
691
+ readTextFile: async (params2) => {
692
+ const res = await this.request("fs/read_text_file", { sessionId, ...params2 });
693
+ return String(res?.content ?? "");
694
+ },
695
+ writeTextFile: async (params2) => {
696
+ await this.request("fs/write_text_file", { sessionId, ...params2 });
697
+ },
698
+ runTerminal: async ({ command, args, cwd }) => {
699
+ const created = await this.request("terminal/create", {
700
+ sessionId,
701
+ command,
702
+ ...args ? { args } : {},
703
+ ...cwd ? { cwd } : {}
704
+ });
705
+ const terminalId = created?.terminalId;
706
+ if (!terminalId) return { output: "", exitCode: null };
707
+ try {
708
+ const exit = await this.request("terminal/wait_for_exit", { sessionId, terminalId });
709
+ const out = await this.request("terminal/output", { sessionId, terminalId });
710
+ return {
711
+ output: String(out?.output ?? ""),
712
+ exitCode: typeof exit?.exitCode === "number" ? exit.exitCode : null
713
+ };
714
+ } finally {
715
+ try {
716
+ await this.request("terminal/release", { sessionId, terminalId });
717
+ } catch {
718
+ }
719
+ }
720
+ }
721
+ };
418
722
  let result;
419
723
  try {
420
724
  result = await this.runTurn(
421
725
  { sessionId, prompt: p.prompt, signal: turnSignal.signal },
422
- (update) => this.sendNotification({ sessionId, update })
726
+ (update) => this.sendNotification({ sessionId, update }),
727
+ api
423
728
  );
424
729
  } catch (err) {
425
730
  session.abort.signal.removeEventListener("abort", onCancel);
@@ -429,6 +734,7 @@ var ACPProtocolHandler = class {
429
734
  }
430
735
  session.abort.signal.removeEventListener("abort", onCancel);
431
736
  session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
737
+ await this.persist(session);
432
738
  await this.transport.send(toWire({
433
739
  jsonrpc: "2.0",
434
740
  id,
@@ -457,12 +763,12 @@ var ACPProtocolHandler = class {
457
763
  async handleSetConfigOption(id, params) {
458
764
  const p = params ?? {};
459
765
  const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
460
- const optionId = typeof p.configOptionId === "string" ? p.configOptionId : null;
766
+ const optionId = typeof p.configId === "string" ? p.configId : null;
461
767
  const value = typeof p.value === "string" ? p.value : null;
462
768
  const session = sessionId ? this.sessions.get(sessionId) : void 0;
463
769
  const option = optionId ? this.configOptions.find((o) => o.id === optionId) : void 0;
464
770
  if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
465
- await this.sendError(id, -32602, "invalid sessionId, configOptionId, or value");
771
+ await this.sendError(id, -32602, "invalid sessionId, configId, or value");
466
772
  return false;
467
773
  }
468
774
  option.currentValue = value;
@@ -474,7 +780,7 @@ var ACPProtocolHandler = class {
474
780
  configOptions: [...this.configOptions]
475
781
  }
476
782
  });
477
- await this.transport.send(toWire({ jsonrpc: "2.0", id, result: {} }));
783
+ await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
478
784
  return false;
479
785
  }
480
786
  async handleSessionList(id) {
@@ -508,6 +814,9 @@ var ACPProtocolHandler = class {
508
814
  }
509
815
  return false;
510
816
  }
817
+ case "$/cancel_request": {
818
+ return false;
819
+ }
511
820
  case "exit":
512
821
  this.close();
513
822
  return true;
@@ -521,6 +830,14 @@ var ACPProtocolHandler = class {
521
830
  async sendNotification(params) {
522
831
  await this.transport.send(toWire({ jsonrpc: "2.0", method: "session/update", params }));
523
832
  }
833
+ /** Best-effort durable persistence of a session + its recorded history. */
834
+ async persist(state) {
835
+ if (!this.store) return;
836
+ try {
837
+ await this.store.save(state, this.replayFor?.(state.id));
838
+ } catch {
839
+ }
840
+ }
524
841
  async sendError(id, code, message, data) {
525
842
  const error = { code, message };
526
843
  if (data !== void 0) error.data = data;
@@ -545,29 +862,240 @@ function errorToJsonRpc(err) {
545
862
  const message = err instanceof Error ? err.message : String(err);
546
863
  return { code: -32603, message };
547
864
  }
865
+ var ACPSessionStore = class {
866
+ dir;
867
+ /**
868
+ * Memoized result of the first successful `init()`. Saved sessions
869
+ * are the hot path — calling `mkdir(..., {recursive:true})` on every
870
+ * turn adds an avoidable syscall to the per-prompt persistence flow.
871
+ * Cleared automatically if the directory disappears between calls.
872
+ */
873
+ initialized = false;
874
+ constructor(opts = {}) {
875
+ this.dir = opts.dir ?? path.join(process.cwd(), ".acp-sessions");
876
+ }
877
+ /** Ensure the store directory exists. Memoized — only mkdirs once. */
878
+ async init() {
879
+ if (this.initialized) return;
880
+ await fsp.mkdir(this.dir, { recursive: true });
881
+ this.initialized = true;
882
+ }
883
+ /**
884
+ * Persist a session state (and optionally its conversation history) to
885
+ * disk. Returns the session id. `history` enables cross-restart
886
+ * `session/load` replay.
887
+ */
888
+ async save(state, history) {
889
+ await this.init();
890
+ await fsp.writeFile(
891
+ path.join(this.dir, `${state.id}.json`),
892
+ JSON.stringify({
893
+ id: state.id,
894
+ cwd: state.cwd,
895
+ modeId: state.modeId,
896
+ createdAt: state.createdAt,
897
+ updatedAt: state.updatedAt,
898
+ title: state.title,
899
+ ...history && history.length > 0 ? { history } : {}
900
+ }),
901
+ "utf8"
902
+ );
903
+ await this.updateIndex(state.id, state.updatedAt);
904
+ return state.id;
905
+ }
906
+ /** Load a persisted session (metadata + history) from disk, or null. */
907
+ async load(sessionId) {
908
+ try {
909
+ const data = await fsp.readFile(path.join(this.dir, `${sessionId}.json`), "utf8");
910
+ return JSON.parse(data);
911
+ } catch {
912
+ return null;
913
+ }
914
+ }
915
+ /** List all persisted sessions. */
916
+ async list() {
917
+ const indexEntries = await this.readIndex();
918
+ if (indexEntries !== null) {
919
+ return indexEntries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
920
+ }
921
+ const files = [];
922
+ try {
923
+ const entries = await fsp.readdir(this.dir);
924
+ for (const entry of entries) {
925
+ if (entry.endsWith(".json") && entry !== "index.json") {
926
+ files.push(entry);
927
+ }
928
+ }
929
+ } catch {
930
+ return [];
931
+ }
932
+ const sessions = [];
933
+ for (const file of files) {
934
+ try {
935
+ const data = await fsp.readFile(path.join(this.dir, file), "utf8");
936
+ const parsed = JSON.parse(data);
937
+ if (parsed.id) {
938
+ sessions.push({ id: parsed.id, updatedAt: parsed.updatedAt ?? "" });
939
+ }
940
+ } catch {
941
+ }
942
+ }
943
+ sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
944
+ void this.writeIndex(sessions).catch(() => void 0);
945
+ return sessions;
946
+ }
947
+ /** Sidecar path that stores `{id, updatedAt}` for every saved session. */
948
+ indexPath() {
949
+ return path.join(this.dir, "index.json");
950
+ }
951
+ /** Read the sidecar index. Returns `null` when missing or unreadable. */
952
+ async readIndex() {
953
+ try {
954
+ const data = await fsp.readFile(this.indexPath(), "utf8");
955
+ const parsed = JSON.parse(data);
956
+ if (!Array.isArray(parsed)) return null;
957
+ const out = [];
958
+ for (const e of parsed) {
959
+ if (e && typeof e.id === "string" && typeof e.updatedAt === "string") {
960
+ out.push({
961
+ id: e.id,
962
+ updatedAt: e.updatedAt
963
+ });
964
+ }
965
+ }
966
+ return out;
967
+ } catch {
968
+ return null;
969
+ }
970
+ }
971
+ /** Atomically replace the sidecar index with the supplied entries. */
972
+ async writeIndex(entries) {
973
+ const target = this.indexPath();
974
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
975
+ await fsp.writeFile(tmp, JSON.stringify(entries), "utf8");
976
+ await fsp.rename(tmp, target);
977
+ }
978
+ /** Update one entry in the index, adding it if missing. Best-effort. */
979
+ async updateIndex(id, updatedAt) {
980
+ let entries = await this.readIndex();
981
+ if (entries === null) {
982
+ await this.list();
983
+ return;
984
+ }
985
+ const i = entries.findIndex((e) => e.id === id);
986
+ if (i >= 0) entries[i] = { id, updatedAt };
987
+ else entries.push({ id, updatedAt });
988
+ try {
989
+ await this.writeIndex(entries);
990
+ } catch {
991
+ }
992
+ }
993
+ /** Delete a session file. */
994
+ async delete(sessionId) {
995
+ try {
996
+ await fsp.unlink(path.join(this.dir, `${sessionId}.json`));
997
+ } catch {
998
+ }
999
+ const entries = await this.readIndex();
1000
+ if (entries === null) return;
1001
+ const next = entries.filter((e) => e.id !== sessionId);
1002
+ if (next.length !== entries.length) {
1003
+ try {
1004
+ await this.writeIndex(next);
1005
+ } catch {
1006
+ }
1007
+ }
1008
+ }
1009
+ /** Get the store directory path. */
1010
+ getDirectory() {
1011
+ return this.dir;
1012
+ }
1013
+ };
1014
+
1015
+ // src/agent/ws-bridge-transport.ts
1016
+ var WsBridgeTransport = class {
1017
+ /** @param sink Called with each outbound message to write to the socket. */
1018
+ constructor(sink) {
1019
+ this.sink = sink;
1020
+ }
1021
+ sink;
1022
+ handlers = /* @__PURE__ */ new Set();
1023
+ closed = false;
1024
+ send(msg) {
1025
+ if (this.closed) return Promise.resolve();
1026
+ try {
1027
+ this.sink(msg);
1028
+ } catch {
1029
+ }
1030
+ return Promise.resolve();
1031
+ }
1032
+ sendRaw() {
1033
+ }
1034
+ read() {
1035
+ return Promise.resolve(null);
1036
+ }
1037
+ onMessage(handler) {
1038
+ this.handlers.add(handler);
1039
+ return () => this.handlers.delete(handler);
1040
+ }
1041
+ close() {
1042
+ this.closed = true;
1043
+ this.handlers.clear();
1044
+ }
1045
+ /**
1046
+ * Feed one inbound message from the socket. Fires the registered
1047
+ * `onMessage` handlers (which route JSON-RPC responses to pending
1048
+ * outbound requests inside the handler). Inbound *requests* are processed
1049
+ * by the caller via `handler.handleMessage(msg)` — call both per message.
1050
+ */
1051
+ receive(msg) {
1052
+ if (this.closed) return;
1053
+ for (const handler of [...this.handlers]) {
1054
+ try {
1055
+ handler(msg);
1056
+ } catch {
1057
+ }
1058
+ }
1059
+ }
1060
+ };
548
1061
  var WrongStackACPServer = class {
549
1062
  transport;
550
1063
  handler;
1064
+ options;
1065
+ /** HTTP server when transport mode is HTTP. */
1066
+ httpServer = null;
551
1067
  running = false;
552
1068
  constructor(opts = {}) {
1069
+ this.options = opts;
553
1070
  this.transport = new StdioTransport();
554
1071
  const runTurn = opts.runTurn ?? defaultEchoRunTurn;
555
1072
  this.handler = new ACPProtocolHandler({
556
1073
  transport: this.transport,
557
1074
  defaultCwd: opts.defaultCwd ?? process.cwd(),
558
1075
  runTurn,
559
- agentName: opts.agentName
1076
+ agentName: opts.agentName,
1077
+ ...opts.replayFor ? { replayFor: opts.replayFor } : {},
1078
+ ...opts.seedFor ? { seedFor: opts.seedFor } : {},
1079
+ ...opts.store ? { store: opts.store } : {}
560
1080
  });
561
1081
  }
562
1082
  /**
563
- * Start the server. Blocks until the client disconnects.
564
- *
565
- * 1. Print the legacy `[wstack-acp]\n` marker so the client knows the
566
- * process is the ACP server (the old `StdioTransport` handshake).
567
- * 2. Loop: read messages, dispatch to the handler, until EOF / error.
1083
+ * Start the server. Mode depends on `options.transport`:
1084
+ * - 'stdio' (default): reads JSON-RPC from stdin, writes to stdout.
1085
+ * - number: listens as HTTP on the given port.
568
1086
  */
569
1087
  async start() {
570
- this.transport.sendStartupMarker();
1088
+ const transportMode = this.options.transport;
1089
+ if (typeof transportMode === "number") {
1090
+ await this.startHttp(transportMode);
1091
+ } else {
1092
+ await this.startStdio();
1093
+ }
1094
+ }
1095
+ async startStdio() {
1096
+ if (this.options.legacyStartupMarker) {
1097
+ this.transport.sendStartupMarker();
1098
+ }
571
1099
  this.running = true;
572
1100
  while (this.running) {
573
1101
  const msg = await this.transport.read();
@@ -577,10 +1105,80 @@ var WrongStackACPServer = class {
577
1105
  }
578
1106
  this.transport.close();
579
1107
  }
1108
+ async startHttp(port) {
1109
+ const host = this.options.host ?? "127.0.0.1";
1110
+ const handler = this.handler;
1111
+ this.httpServer = createServer(async (req, res) => {
1112
+ const selfOrigin = `http://${host}:${port}`;
1113
+ const reqOrigin = Array.isArray(req.headers.origin) ? req.headers.origin[0] : req.headers.origin;
1114
+ if (reqOrigin && reqOrigin !== selfOrigin) {
1115
+ res.writeHead(403);
1116
+ res.end(JSON.stringify({ error: "cross-origin request forbidden" }));
1117
+ return;
1118
+ }
1119
+ if (reqOrigin) res.setHeader("Access-Control-Allow-Origin", reqOrigin);
1120
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
1121
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
1122
+ if (req.method === "OPTIONS") {
1123
+ res.writeHead(204);
1124
+ res.end();
1125
+ return;
1126
+ }
1127
+ if (req.method !== "POST") {
1128
+ res.writeHead(405);
1129
+ res.end(JSON.stringify({ error: "method not allowed" }));
1130
+ return;
1131
+ }
1132
+ let body = "";
1133
+ for await (const chunk of req) {
1134
+ body += chunk;
1135
+ }
1136
+ let msg;
1137
+ try {
1138
+ msg = JSON.parse(body);
1139
+ } catch {
1140
+ res.writeHead(400);
1141
+ res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
1142
+ return;
1143
+ }
1144
+ const notifications = [];
1145
+ let response = null;
1146
+ const originalSend = this.transport.send.bind(this.transport);
1147
+ this.transport.send = async (m) => {
1148
+ if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
1149
+ response = m;
1150
+ } else if (m.method === "session/update") {
1151
+ notifications.push(m.params);
1152
+ } else {
1153
+ notifications.push(m);
1154
+ }
1155
+ };
1156
+ try {
1157
+ await handler.handleMessage(msg);
1158
+ } finally {
1159
+ this.transport.send = originalSend;
1160
+ }
1161
+ res.writeHead(200, { "Content-Type": "application/json" });
1162
+ const responseBody = response !== null ? { ...response, notifications } : { notifications };
1163
+ res.end(JSON.stringify(responseBody));
1164
+ });
1165
+ return new Promise((resolve) => {
1166
+ this.httpServer.listen(port, host, () => {
1167
+ writeErr(`[wstack-acp] HTTP server listening on http://${host}:${port}
1168
+ `);
1169
+ this.running = true;
1170
+ resolve();
1171
+ });
1172
+ });
1173
+ }
580
1174
  /** Stop the server. */
581
1175
  stop() {
582
1176
  this.running = false;
583
1177
  this.transport.close();
1178
+ if (this.httpServer) {
1179
+ this.httpServer.close();
1180
+ this.httpServer = null;
1181
+ }
584
1182
  }
585
1183
  };
586
1184
  var defaultEchoRunTurn = async (_input, _emit) => {
@@ -603,20 +1201,63 @@ if (isEntrypoint) {
603
1201
  function makeACPServerAgentTurn(opts) {
604
1202
  const agents = /* @__PURE__ */ new Map();
605
1203
  const timeouts = /* @__PURE__ */ new Map();
1204
+ const history = /* @__PURE__ */ new Map();
1205
+ const pendingSeed = /* @__PURE__ */ new Set();
606
1206
  const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
607
- return async (input, emit) => {
1207
+ const turn = async (input, emit, api) => {
608
1208
  let agent = agents.get(input.sessionId);
609
1209
  if (!agent) {
610
- agent = await opts.agentFor(input.sessionId, process.cwd());
1210
+ agent = await opts.agentFor(input.sessionId, process.cwd(), api);
611
1211
  agents.set(input.sessionId, agent);
1212
+ if (pendingSeed.has(input.sessionId)) {
1213
+ pendingSeed.delete(input.sessionId);
1214
+ seedAgentContext(agent, history.get(input.sessionId) ?? []);
1215
+ }
1216
+ }
1217
+ const turnAbort = new AbortController();
1218
+ const abortForTimeout = () => turnAbort.abort();
1219
+ const onParentAbort = () => turnAbort.abort();
1220
+ if (input.signal.aborted) {
1221
+ turnAbort.abort();
1222
+ } else {
1223
+ input.signal.addEventListener("abort", onParentAbort, { once: true });
612
1224
  }
613
1225
  const timer = setTimeout(() => {
614
1226
  timeouts.delete(input.sessionId);
1227
+ abortForTimeout();
615
1228
  }, timeoutMs);
616
1229
  timeouts.set(input.sessionId, timer);
1230
+ const unsub = [];
1231
+ const bus = agent.events;
1232
+ if (bus?.on) {
1233
+ unsub.push(
1234
+ bus.on("tool.started", (e) => {
1235
+ emit({
1236
+ sessionUpdate: "tool_call",
1237
+ toolCallId: e.id,
1238
+ title: toolTitle(e.name, e.input),
1239
+ kind: toolNameToKind(e.name),
1240
+ status: "in_progress",
1241
+ ...isRecord(e.input) ? { rawInput: e.input } : {}
1242
+ });
1243
+ }),
1244
+ bus.on("tool.executed", (e) => {
1245
+ emit({
1246
+ sessionUpdate: "tool_call_update",
1247
+ toolCallId: e.id ?? e.name,
1248
+ status: e.ok ? "completed" : "failed",
1249
+ ...e.output !== void 0 ? {
1250
+ content: [
1251
+ { type: "content", content: { type: "text", text: e.output } }
1252
+ ]
1253
+ } : {}
1254
+ });
1255
+ })
1256
+ );
1257
+ }
617
1258
  try {
618
- const userMessage = promptToText(input.prompt);
619
- const result = await agent.run(userMessage, { signal: input.signal });
1259
+ const userInput = promptToAgentInput(input.prompt);
1260
+ const result = await agent.run(userInput, { signal: turnAbort.signal });
620
1261
  const text = extractText(result);
621
1262
  if (text) {
622
1263
  emit({
@@ -624,16 +1265,114 @@ function makeACPServerAgentTurn(opts) {
624
1265
  content: { type: "text", text }
625
1266
  });
626
1267
  }
1268
+ const userText = promptToText(input.prompt);
1269
+ const hist = history.get(input.sessionId) ?? [];
1270
+ if (userText) {
1271
+ hist.push({ sessionUpdate: "user_message_chunk", content: { type: "text", text: userText } });
1272
+ }
1273
+ if (text) {
1274
+ hist.push({ sessionUpdate: "agent_message_chunk", content: { type: "text", text } });
1275
+ }
1276
+ if (hist.length > 0) history.set(input.sessionId, hist);
1277
+ const plan = extractPlan(result);
1278
+ if (plan.length > 0) {
1279
+ emit({
1280
+ sessionUpdate: "plan",
1281
+ entries: plan
1282
+ });
1283
+ }
1284
+ const usage = extractUsage(result);
1285
+ if (usage) {
1286
+ emit({
1287
+ sessionUpdate: "usage_update",
1288
+ used: usage.used,
1289
+ size: usage.size,
1290
+ ...usage.cost ? { cost: usage.cost } : {}
1291
+ });
1292
+ }
627
1293
  const result_out = {
628
- stopReason: pickStopReason(result, input.signal)
1294
+ // `turnAbort.signal` covers both client cancellation and the
1295
+ // wall-clock timeout, so either maps to stopReason 'cancelled'.
1296
+ stopReason: pickStopReason(result, turnAbort.signal)
629
1297
  };
630
1298
  if (text) result_out.text = text;
1299
+ const runTurnPlan = extractPlan(result);
1300
+ if (runTurnPlan.length > 0) result_out.plan = runTurnPlan;
1301
+ if (usage) result_out.usage = usage;
631
1302
  return result_out;
632
1303
  } finally {
633
1304
  clearTimeout(timer);
634
1305
  timeouts.delete(input.sessionId);
1306
+ input.signal.removeEventListener("abort", onParentAbort);
1307
+ for (const u of unsub) u();
635
1308
  }
636
1309
  };
1310
+ const replay = (sessionId) => history.get(sessionId) ?? [];
1311
+ const seed = (sessionId, incoming) => {
1312
+ if (incoming.length === 0) return;
1313
+ history.set(sessionId, [...incoming]);
1314
+ pendingSeed.add(sessionId);
1315
+ };
1316
+ return Object.assign(turn, { replay, seed });
1317
+ }
1318
+ function seedAgentContext(agent, history) {
1319
+ const state = agent.ctx?.state;
1320
+ if (!state?.appendMessage) return;
1321
+ for (const u of history) {
1322
+ const text = u.content?.text;
1323
+ if (typeof text !== "string" || text.length === 0) continue;
1324
+ const role = u.sessionUpdate === "user_message_chunk" ? "user" : "assistant";
1325
+ state.appendMessage({ role, content: text });
1326
+ }
1327
+ }
1328
+ function toolNameToKind(name) {
1329
+ const n = name.toLowerCase();
1330
+ if (n.includes("read") || n.includes("cat")) return "read";
1331
+ if (n.includes("write") || n.includes("edit") || n.includes("apply") || n.includes("patch")) return "edit";
1332
+ if (n.includes("delete") || n.includes("rm")) return "delete";
1333
+ if (n.includes("move") || n.includes("rename") || n.includes("mv")) return "move";
1334
+ if (n.includes("grep") || n.includes("glob") || n.includes("search") || n.includes("find")) return "search";
1335
+ if (n.includes("bash") || n.includes("shell") || n.includes("exec") || n.includes("run") || n.includes("terminal")) return "execute";
1336
+ if (n.includes("fetch") || n.includes("http") || n.includes("web") || n.includes("url")) return "fetch";
1337
+ if (n.includes("think") || n.includes("plan")) return "think";
1338
+ return "other";
1339
+ }
1340
+ function toolTitle(name, input) {
1341
+ if (isRecord(input)) {
1342
+ const path2 = input.path ?? input.file ?? input.filePath ?? input.pattern ?? input.command;
1343
+ if (typeof path2 === "string" && path2.length > 0) {
1344
+ return `${name}: ${path2.length > 80 ? `${path2.slice(0, 77)}\u2026` : path2}`;
1345
+ }
1346
+ }
1347
+ return name;
1348
+ }
1349
+ function isRecord(v) {
1350
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1351
+ }
1352
+ function promptToAgentInput(blocks) {
1353
+ const hasImage = blocks.some((b) => b.type === "image");
1354
+ if (!hasImage) {
1355
+ return promptToText(blocks);
1356
+ }
1357
+ const out = [];
1358
+ for (const b of blocks) {
1359
+ if (b.type === "text") {
1360
+ out.push({ type: "text", text: b.text });
1361
+ } else if (b.type === "image") {
1362
+ out.push({
1363
+ type: "image",
1364
+ source: { type: "base64", media_type: b.mimeType, data: b.data }
1365
+ });
1366
+ } else if (b.type === "audio") {
1367
+ out.push({ type: "text", text: `[audio: ${b.mimeType}]` });
1368
+ } else if (b.type === "resource") {
1369
+ const text = "text" in b.resource && typeof b.resource.text === "string" ? b.resource.text : `[embedded resource: ${b.resource.uri}]`;
1370
+ out.push({ type: "text", text });
1371
+ } else if (b.type === "resource_link") {
1372
+ out.push({ type: "text", text: `[resource link: ${b.uri}]` });
1373
+ }
1374
+ }
1375
+ return out;
637
1376
  }
638
1377
  function promptToText(blocks) {
639
1378
  const parts = [];
@@ -680,7 +1419,32 @@ function pickStopReason(result, signal) {
680
1419
  }
681
1420
  return "end_turn";
682
1421
  }
1422
+ function extractPlan(result) {
1423
+ if (typeof result !== "object" || result === null) return [];
1424
+ const r = result;
1425
+ if (Array.isArray(r.plan)) {
1426
+ return r.plan.filter(
1427
+ (e) => typeof e === "object" && e !== null && typeof e.content === "string"
1428
+ );
1429
+ }
1430
+ return [];
1431
+ }
1432
+ function extractUsage(result) {
1433
+ if (typeof result !== "object" || result === null) return null;
1434
+ const r = result;
1435
+ if (typeof r.usage === "object" && r.usage !== null) {
1436
+ const u = r.usage;
1437
+ if (typeof u.used === "number" && typeof u.size === "number") {
1438
+ return {
1439
+ used: u.used,
1440
+ size: u.size,
1441
+ ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
1442
+ };
1443
+ }
1444
+ }
1445
+ return null;
1446
+ }
683
1447
 
684
- export { ACPProtocolHandler, ACPToolsRegistry, StdioTransport, WrongStackACPServer, makeACPServerAgentTurn };
1448
+ export { ACPProtocolHandler, ACPSessionStore, ACPToolsRegistry, StdioTransport, WrongStackACPServer, WsBridgeTransport, makeACPServerAgentTurn };
685
1449
  //# sourceMappingURL=agent.js.map
686
1450
  //# sourceMappingURL=agent.js.map