@openacp/cli 0.3.1 → 0.4.1

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 (48) hide show
  1. package/dist/{autostart-N4HIL6C3.js → autostart-DZ3MHHMM.js} +3 -3
  2. package/dist/{chunk-LVSQQRCF.js → chunk-2SY7Y2VB.js} +3 -3
  3. package/dist/{chunk-7W5SOJPD.js → chunk-3QACY5E3.js} +2 -2
  4. package/dist/{chunk-CA6FXPLH.js → chunk-BLVZFCKN.js} +4 -4
  5. package/dist/chunk-KSIQZC3J.js +98 -0
  6. package/dist/chunk-KSIQZC3J.js.map +1 -0
  7. package/dist/{chunk-JOSJGZGF.js → chunk-LYKCQTH5.js} +1 -9
  8. package/dist/{chunk-JOSJGZGF.js.map → chunk-LYKCQTH5.js.map} +1 -1
  9. package/dist/{chunk-5E6ZXCNN.js → chunk-MRKYJ422.js} +2 -2
  10. package/dist/{chunk-RBDPCHGD.js → chunk-V3BA2MJ6.js} +2 -2
  11. package/dist/{chunk-YXMRR2E3.js → chunk-WF5XDN4D.js} +65 -40
  12. package/dist/chunk-WF5XDN4D.js.map +1 -0
  13. package/dist/{chunk-NS2L445T.js → chunk-WHKLPZGK.js} +4 -4
  14. package/dist/{chunk-SWQRUVBW.js → chunk-YD7ILGA6.js} +624 -322
  15. package/dist/chunk-YD7ILGA6.js.map +1 -0
  16. package/dist/cli.js +258 -316
  17. package/dist/cli.js.map +1 -1
  18. package/dist/{config-2CBRLF3R.js → config-J5YQOMDU.js} +3 -3
  19. package/dist/config-editor-IXL4BFG3.js +11 -0
  20. package/dist/{daemon-UXC7PB4P.js → daemon-SLGQGRKO.js} +4 -4
  21. package/dist/index.d.ts +184 -71
  22. package/dist/index.js +18 -10
  23. package/dist/install-cloudflared-ILUXKLAC.js +8 -0
  24. package/dist/{main-PLTMIVYL.js → main-OUVYNLZN.js} +53 -17
  25. package/dist/main-OUVYNLZN.js.map +1 -0
  26. package/dist/{setup-UKWBLJIT.js → setup-JQZBPXWS.js} +4 -4
  27. package/dist/{tunnel-service-4GISQZNP.js → tunnel-service-DASSH7OA.js} +3 -3
  28. package/dist/version-VC5CPXBX.js +15 -0
  29. package/dist/version-VC5CPXBX.js.map +1 -0
  30. package/package.json +1 -1
  31. package/dist/chunk-SWQRUVBW.js.map +0 -1
  32. package/dist/chunk-YXMRR2E3.js.map +0 -1
  33. package/dist/config-editor-UN56HQCW.js +0 -11
  34. package/dist/install-cloudflared-LMM7MFQX.js +0 -8
  35. package/dist/main-PLTMIVYL.js.map +0 -1
  36. /package/dist/{autostart-N4HIL6C3.js.map → autostart-DZ3MHHMM.js.map} +0 -0
  37. /package/dist/{chunk-LVSQQRCF.js.map → chunk-2SY7Y2VB.js.map} +0 -0
  38. /package/dist/{chunk-7W5SOJPD.js.map → chunk-3QACY5E3.js.map} +0 -0
  39. /package/dist/{chunk-CA6FXPLH.js.map → chunk-BLVZFCKN.js.map} +0 -0
  40. /package/dist/{chunk-5E6ZXCNN.js.map → chunk-MRKYJ422.js.map} +0 -0
  41. /package/dist/{chunk-RBDPCHGD.js.map → chunk-V3BA2MJ6.js.map} +0 -0
  42. /package/dist/{chunk-NS2L445T.js.map → chunk-WHKLPZGK.js.map} +0 -0
  43. /package/dist/{config-2CBRLF3R.js.map → config-J5YQOMDU.js.map} +0 -0
  44. /package/dist/{config-editor-UN56HQCW.js.map → config-editor-IXL4BFG3.js.map} +0 -0
  45. /package/dist/{daemon-UXC7PB4P.js.map → daemon-SLGQGRKO.js.map} +0 -0
  46. /package/dist/{install-cloudflared-LMM7MFQX.js.map → install-cloudflared-ILUXKLAC.js.map} +0 -0
  47. /package/dist/{setup-UKWBLJIT.js.map → setup-JQZBPXWS.js.map} +0 -0
  48. /package/dist/{tunnel-service-4GISQZNP.js.map → tunnel-service-DASSH7OA.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createChildLogger,
3
3
  createSessionLogger
4
- } from "./chunk-JOSJGZGF.js";
4
+ } from "./chunk-LYKCQTH5.js";
5
5
 
6
6
  // src/core/streams.ts
7
7
  function nodeToWebWritable(nodeStream) {
@@ -504,10 +504,174 @@ var AgentManager = class {
504
504
  }
505
505
  };
506
506
 
507
+ // src/core/typed-emitter.ts
508
+ var TypedEmitter = class {
509
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
510
+ listeners = /* @__PURE__ */ new Map();
511
+ paused = false;
512
+ buffer = [];
513
+ on(event, listener) {
514
+ let set = this.listeners.get(event);
515
+ if (!set) {
516
+ set = /* @__PURE__ */ new Set();
517
+ this.listeners.set(event, set);
518
+ }
519
+ set.add(listener);
520
+ return this;
521
+ }
522
+ off(event, listener) {
523
+ this.listeners.get(event)?.delete(listener);
524
+ return this;
525
+ }
526
+ emit(event, ...args) {
527
+ if (this.paused) {
528
+ if (this.passthroughFn?.(event, args)) {
529
+ this.deliver(event, args);
530
+ } else {
531
+ this.buffer.push({ event, args });
532
+ }
533
+ return;
534
+ }
535
+ this.deliver(event, args);
536
+ }
537
+ /**
538
+ * Pause event delivery. Events emitted while paused are buffered.
539
+ * Optionally pass a filter to allow specific events through even while paused.
540
+ */
541
+ pause(passthrough) {
542
+ this.paused = true;
543
+ this.passthroughFn = passthrough;
544
+ }
545
+ passthroughFn;
546
+ /** Resume event delivery and replay buffered events in order. */
547
+ resume() {
548
+ this.paused = false;
549
+ this.passthroughFn = void 0;
550
+ const buffered = this.buffer.splice(0);
551
+ for (const { event, args } of buffered) {
552
+ this.deliver(event, args);
553
+ }
554
+ }
555
+ /** Discard all buffered events without delivering them. */
556
+ clearBuffer() {
557
+ this.buffer.length = 0;
558
+ }
559
+ get isPaused() {
560
+ return this.paused;
561
+ }
562
+ get bufferSize() {
563
+ return this.buffer.length;
564
+ }
565
+ removeAllListeners(event) {
566
+ if (event) {
567
+ this.listeners.delete(event);
568
+ } else {
569
+ this.listeners.clear();
570
+ }
571
+ }
572
+ deliver(event, args) {
573
+ const set = this.listeners.get(event);
574
+ if (!set) return;
575
+ for (const listener of set) {
576
+ listener(...args);
577
+ }
578
+ }
579
+ };
580
+
581
+ // src/core/prompt-queue.ts
582
+ var PromptQueue = class {
583
+ constructor(processor, onError) {
584
+ this.processor = processor;
585
+ this.onError = onError;
586
+ }
587
+ queue = [];
588
+ processing = false;
589
+ async enqueue(text) {
590
+ if (this.processing) {
591
+ return new Promise((resolve) => {
592
+ this.queue.push({ text, resolve });
593
+ });
594
+ }
595
+ await this.process(text);
596
+ }
597
+ async process(text) {
598
+ this.processing = true;
599
+ try {
600
+ await this.processor(text);
601
+ } catch (err) {
602
+ this.onError?.(err);
603
+ } finally {
604
+ this.processing = false;
605
+ this.drainNext();
606
+ }
607
+ }
608
+ drainNext() {
609
+ const next = this.queue.shift();
610
+ if (next) {
611
+ this.process(next.text).then(next.resolve);
612
+ }
613
+ }
614
+ clear() {
615
+ for (const item of this.queue) {
616
+ item.resolve();
617
+ }
618
+ this.queue = [];
619
+ }
620
+ get pending() {
621
+ return this.queue.length;
622
+ }
623
+ get isProcessing() {
624
+ return this.processing;
625
+ }
626
+ };
627
+
628
+ // src/core/permission-gate.ts
629
+ var PermissionGate = class {
630
+ request;
631
+ resolveFn;
632
+ rejectFn;
633
+ settled = false;
634
+ setPending(request) {
635
+ this.request = request;
636
+ this.settled = false;
637
+ return new Promise((resolve, reject) => {
638
+ this.resolveFn = resolve;
639
+ this.rejectFn = reject;
640
+ });
641
+ }
642
+ resolve(optionId) {
643
+ if (this.settled || !this.resolveFn) return;
644
+ this.settled = true;
645
+ this.resolveFn(optionId);
646
+ this.cleanup();
647
+ }
648
+ reject(reason) {
649
+ if (this.settled || !this.rejectFn) return;
650
+ this.settled = true;
651
+ this.rejectFn(new Error(reason ?? "Permission rejected"));
652
+ this.cleanup();
653
+ }
654
+ get isPending() {
655
+ return !!this.request && !this.settled;
656
+ }
657
+ get currentRequest() {
658
+ return this.isPending ? this.request : void 0;
659
+ }
660
+ /** The request ID of the current pending request, undefined after settlement */
661
+ get requestId() {
662
+ return this.request?.id;
663
+ }
664
+ cleanup() {
665
+ this.request = void 0;
666
+ this.resolveFn = void 0;
667
+ this.rejectFn = void 0;
668
+ }
669
+ };
670
+
507
671
  // src/core/session.ts
508
672
  import { nanoid } from "nanoid";
509
673
  var moduleLog = createChildLogger({ module: "session" });
510
- var Session = class {
674
+ var Session = class extends TypedEmitter {
511
675
  id;
512
676
  channelId;
513
677
  threadId = "";
@@ -517,15 +681,15 @@ var Session = class {
517
681
  agentSessionId = "";
518
682
  status = "initializing";
519
683
  name;
520
- promptQueue = [];
521
- promptRunning = false;
522
684
  createdAt = /* @__PURE__ */ new Date();
523
685
  adapter;
524
686
  // Set by wireSessionEvents for renaming
525
- pendingPermission;
526
687
  dangerousMode = false;
527
688
  log;
689
+ permissionGate = new PermissionGate();
690
+ queue;
528
691
  constructor(opts) {
692
+ super();
529
693
  this.id = opts.id || nanoid(12);
530
694
  this.channelId = opts.channelId;
531
695
  this.agentName = opts.agentName;
@@ -533,45 +697,57 @@ var Session = class {
533
697
  this.agentInstance = opts.agentInstance;
534
698
  this.log = createSessionLogger(this.id, moduleLog);
535
699
  this.log.info({ agentName: this.agentName }, "Session created");
700
+ this.queue = new PromptQueue(
701
+ (text) => this.processPrompt(text),
702
+ (err) => {
703
+ this.status = "error";
704
+ this.log.error({ err }, "Prompt execution failed");
705
+ }
706
+ );
707
+ }
708
+ // --- Backward-compatible properties ---
709
+ /** @deprecated Use permissionGate directly */
710
+ get pendingPermission() {
711
+ if (!this.permissionGate.isPending) return void 0;
712
+ return {
713
+ requestId: this.permissionGate.requestId,
714
+ resolve: (optionId) => this.permissionGate.resolve(optionId)
715
+ };
716
+ }
717
+ set pendingPermission(val) {
718
+ }
719
+ /** Number of prompts waiting in queue */
720
+ get queueDepth() {
721
+ return this.queue.pending;
722
+ }
723
+ get promptRunning() {
724
+ return this.queue.isProcessing;
536
725
  }
726
+ // --- Public API ---
537
727
  async enqueuePrompt(text) {
538
- if (this.promptRunning) {
539
- this.promptQueue.push(text);
540
- this.log.debug({ queueDepth: this.promptQueue.length }, "Prompt queued");
728
+ await this.queue.enqueue(text);
729
+ }
730
+ async processPrompt(text) {
731
+ if (text === "\0__warmup__") {
732
+ await this.runWarmup();
541
733
  return;
542
734
  }
543
- await this.runPrompt(text);
544
- }
545
- async runPrompt(text) {
546
- this.promptRunning = true;
547
735
  this.status = "active";
548
736
  const promptStart = Date.now();
549
737
  this.log.debug("Prompt execution started");
550
- try {
551
- await this.agentInstance.prompt(text);
552
- this.log.info(
553
- { durationMs: Date.now() - promptStart },
554
- "Prompt execution completed"
555
- );
556
- if (!this.name) {
557
- await this.autoName();
558
- }
559
- } catch (err) {
560
- this.status = "error";
561
- this.log.error({ err }, "Prompt execution failed");
562
- } finally {
563
- this.promptRunning = false;
564
- if (this.promptQueue.length > 0) {
565
- const next = this.promptQueue.shift();
566
- await this.runPrompt(next);
567
- }
738
+ await this.agentInstance.prompt(text);
739
+ this.log.info(
740
+ { durationMs: Date.now() - promptStart },
741
+ "Prompt execution completed"
742
+ );
743
+ if (!this.name) {
744
+ await this.autoName();
568
745
  }
569
746
  }
570
747
  // NOTE: This injects a summary prompt into the agent's conversation history.
571
- // Known Phase 1 limitation — the agent sees this prompt in its context.
572
748
  async autoName() {
573
749
  let title = "";
574
- const prevHandler = this.agentInstance.onSessionUpdate;
750
+ const originalHandler = this.agentInstance.onSessionUpdate;
575
751
  this.agentInstance.onSessionUpdate = (event) => {
576
752
  if (event.type === "text") title += event.content;
577
753
  };
@@ -579,7 +755,7 @@ var Session = class {
579
755
  await this.agentInstance.prompt(
580
756
  "Summarize this conversation in max 5 words for a topic title. Reply ONLY with the title, nothing else."
581
757
  );
582
- this.name = title.trim().slice(0, 50);
758
+ this.name = title.trim().slice(0, 50) || `Session ${this.id.slice(0, 6)}`;
583
759
  this.log.info({ name: this.name }, "Session auto-named");
584
760
  if (this.adapter && this.name) {
585
761
  await this.adapter.renameSessionThread(this.id, this.name);
@@ -587,16 +763,18 @@ var Session = class {
587
763
  } catch {
588
764
  this.name = `Session ${this.id.slice(0, 6)}`;
589
765
  } finally {
590
- this.agentInstance.onSessionUpdate = prevHandler;
766
+ this.agentInstance.onSessionUpdate = originalHandler;
591
767
  }
592
768
  }
593
769
  /** Fire-and-forget warm-up: primes model cache while user types their first message */
594
770
  async warmup() {
595
- this.promptRunning = true;
596
- const prevHandler = this.agentInstance.onSessionUpdate;
597
- this.agentInstance.onSessionUpdate = (event) => {
598
- if (event.type === "commands_update") prevHandler(event);
599
- };
771
+ await this.queue.enqueue("\0__warmup__");
772
+ }
773
+ async runWarmup() {
774
+ this.pause((_event, args) => {
775
+ const agentEvent = args[0];
776
+ return agentEvent?.type === "commands_update";
777
+ });
600
778
  try {
601
779
  const start = Date.now();
602
780
  await this.agentInstance.prompt('Reply with only "ready".');
@@ -605,19 +783,15 @@ var Session = class {
605
783
  } catch (err) {
606
784
  this.log.error({ err }, "Warm-up failed");
607
785
  } finally {
608
- this.agentInstance.onSessionUpdate = prevHandler;
609
- this.promptRunning = false;
610
- if (this.promptQueue.length > 0) {
611
- const next = this.promptQueue.shift();
612
- await this.runPrompt(next);
613
- }
786
+ this.clearBuffer();
787
+ this.resume();
614
788
  }
615
789
  }
616
790
  async cancel() {
617
- this.promptQueue = [];
618
- this.status = "cancelled";
791
+ this.queue.clear();
619
792
  this.log.info("Session cancelled");
620
793
  await this.agentInstance.cancel();
794
+ this.status = "active";
621
795
  }
622
796
  async destroy() {
623
797
  this.log.info("Session destroyed");
@@ -763,6 +937,206 @@ var NotificationManager = class {
763
937
  }
764
938
  };
765
939
 
940
+ // src/tunnel/extract-file-info.ts
941
+ function extractFileInfo(name, kind, content, rawInput, meta) {
942
+ if (kind && !["read", "edit", "write"].includes(kind)) return null;
943
+ let info = null;
944
+ if (meta) {
945
+ const m = meta;
946
+ const tr = m?.claudeCode?.toolResponse;
947
+ const file = tr?.file;
948
+ if (file?.filePath && file?.content) {
949
+ info = { filePath: file.filePath, content: file.content };
950
+ }
951
+ if (!info && tr?.filePath && tr?.content) {
952
+ info = { filePath: tr.filePath, content: tr.content };
953
+ }
954
+ }
955
+ if (!info && rawInput) {
956
+ const ri = rawInput;
957
+ const filePath = ri?.file_path || ri?.filePath || ri?.path;
958
+ if (typeof filePath === "string") {
959
+ const parsed = content ? parseContent(content) : null;
960
+ info = { filePath, content: parsed?.content || ri?.content, oldContent: parsed?.oldContent };
961
+ }
962
+ }
963
+ if (!info && content) {
964
+ info = parseContent(content);
965
+ }
966
+ if (!info) return null;
967
+ if (!info.filePath) {
968
+ const pathMatch = name.match(/(?:Read|Edit|Write|View)\s+(.+)/i);
969
+ if (pathMatch) info.filePath = pathMatch[1].trim();
970
+ }
971
+ if (!info.filePath || !info.content) return null;
972
+ return info;
973
+ }
974
+ function parseContent(content) {
975
+ if (typeof content === "string") {
976
+ return { content };
977
+ }
978
+ if (Array.isArray(content)) {
979
+ for (const block of content) {
980
+ const result = parseContent(block);
981
+ if (result?.content || result?.filePath) return result;
982
+ }
983
+ return null;
984
+ }
985
+ if (typeof content === "object" && content !== null) {
986
+ const c = content;
987
+ if (c.type === "diff" && typeof c.path === "string") {
988
+ const newText = c.newText;
989
+ const oldText = c.oldText;
990
+ if (newText) {
991
+ return {
992
+ filePath: c.path,
993
+ content: newText,
994
+ oldContent: oldText ?? void 0
995
+ };
996
+ }
997
+ }
998
+ if (c.type === "content" && c.content) {
999
+ return parseContent(c.content);
1000
+ }
1001
+ if (c.type === "text" && typeof c.text === "string") {
1002
+ return { content: c.text, filePath: c.filePath };
1003
+ }
1004
+ if (typeof c.text === "string") {
1005
+ return { content: c.text, filePath: c.filePath };
1006
+ }
1007
+ if (typeof c.file_path === "string" || typeof c.filePath === "string" || typeof c.path === "string") {
1008
+ const filePath = c.file_path || c.filePath || c.path;
1009
+ const fileContent = c.content || c.text || c.output || c.newText;
1010
+ if (typeof fileContent === "string") {
1011
+ return {
1012
+ filePath,
1013
+ content: fileContent,
1014
+ oldContent: c.old_content || c.oldText
1015
+ };
1016
+ }
1017
+ }
1018
+ if (c.input) {
1019
+ const result = parseContent(c.input);
1020
+ if (result) return result;
1021
+ }
1022
+ if (c.output) {
1023
+ const result = parseContent(c.output);
1024
+ if (result) return result;
1025
+ }
1026
+ }
1027
+ return null;
1028
+ }
1029
+
1030
+ // src/core/message-transformer.ts
1031
+ var log2 = createChildLogger({ module: "message-transformer" });
1032
+ var MessageTransformer = class {
1033
+ constructor(tunnelService) {
1034
+ this.tunnelService = tunnelService;
1035
+ }
1036
+ transform(event, sessionContext) {
1037
+ switch (event.type) {
1038
+ case "text":
1039
+ return { type: "text", text: event.content };
1040
+ case "thought":
1041
+ return { type: "thought", text: event.content };
1042
+ case "tool_call": {
1043
+ const metadata = {
1044
+ id: event.id,
1045
+ name: event.name,
1046
+ kind: event.kind,
1047
+ status: event.status,
1048
+ content: event.content,
1049
+ locations: event.locations
1050
+ };
1051
+ this.enrichWithViewerLinks(event, metadata, sessionContext);
1052
+ return { type: "tool_call", text: event.name, metadata };
1053
+ }
1054
+ case "tool_update": {
1055
+ const metadata = {
1056
+ id: event.id,
1057
+ name: event.name,
1058
+ kind: event.kind,
1059
+ status: event.status,
1060
+ content: event.content
1061
+ };
1062
+ this.enrichWithViewerLinks(event, metadata, sessionContext);
1063
+ return { type: "tool_update", text: "", metadata };
1064
+ }
1065
+ case "plan":
1066
+ return {
1067
+ type: "plan",
1068
+ text: "",
1069
+ metadata: { entries: event.entries }
1070
+ };
1071
+ case "usage":
1072
+ return {
1073
+ type: "usage",
1074
+ text: "",
1075
+ metadata: {
1076
+ tokensUsed: event.tokensUsed,
1077
+ contextSize: event.contextSize,
1078
+ cost: event.cost
1079
+ }
1080
+ };
1081
+ case "session_end":
1082
+ return { type: "session_end", text: `Done (${event.reason})` };
1083
+ case "error":
1084
+ return { type: "error", text: event.message };
1085
+ default:
1086
+ return { type: "text", text: "" };
1087
+ }
1088
+ }
1089
+ enrichWithViewerLinks(event, metadata, sessionContext) {
1090
+ if (!this.tunnelService || !sessionContext) return;
1091
+ const name = "name" in event ? event.name || "" : "";
1092
+ const kind = "kind" in event ? event.kind : void 0;
1093
+ log2.debug(
1094
+ { name, kind, status: event.status, hasContent: !!event.content },
1095
+ "enrichWithViewerLinks: inspecting event"
1096
+ );
1097
+ const fileInfo = extractFileInfo(
1098
+ name,
1099
+ kind,
1100
+ event.content,
1101
+ event.rawInput,
1102
+ event.meta
1103
+ );
1104
+ if (!fileInfo) return;
1105
+ log2.info(
1106
+ {
1107
+ name,
1108
+ kind,
1109
+ filePath: fileInfo.filePath,
1110
+ hasOldContent: !!fileInfo.oldContent
1111
+ },
1112
+ "enrichWithViewerLinks: extracted file info"
1113
+ );
1114
+ const store = this.tunnelService.getStore();
1115
+ const viewerLinks = {};
1116
+ if (fileInfo.oldContent) {
1117
+ const id2 = store.storeDiff(
1118
+ sessionContext.id,
1119
+ fileInfo.filePath,
1120
+ fileInfo.oldContent,
1121
+ fileInfo.content,
1122
+ sessionContext.workingDirectory
1123
+ );
1124
+ if (id2) viewerLinks.diff = this.tunnelService.diffUrl(id2);
1125
+ }
1126
+ const id = store.storeFile(
1127
+ sessionContext.id,
1128
+ fileInfo.filePath,
1129
+ fileInfo.content,
1130
+ sessionContext.workingDirectory
1131
+ );
1132
+ if (id) viewerLinks.file = this.tunnelService.fileUrl(id);
1133
+ if (Object.keys(viewerLinks).length > 0) {
1134
+ metadata.viewerLinks = viewerLinks;
1135
+ metadata.viewerFilePath = fileInfo.filePath;
1136
+ }
1137
+ }
1138
+ };
1139
+
766
1140
  // src/core/core.ts
767
1141
  import path3 from "path";
768
1142
  import os from "os";
@@ -770,7 +1144,7 @@ import os from "os";
770
1144
  // src/core/session-store.ts
771
1145
  import fs2 from "fs";
772
1146
  import path2 from "path";
773
- var log2 = createChildLogger({ module: "session-store" });
1147
+ var log3 = createChildLogger({ module: "session-store" });
774
1148
  var DEBOUNCE_MS = 2e3;
775
1149
  var JsonFileSessionStore = class {
776
1150
  records = /* @__PURE__ */ new Map();
@@ -847,7 +1221,7 @@ var JsonFileSessionStore = class {
847
1221
  fs2.readFileSync(this.filePath, "utf-8")
848
1222
  );
849
1223
  if (raw.version !== 1) {
850
- log2.warn(
1224
+ log3.warn(
851
1225
  { version: raw.version },
852
1226
  "Unknown session store version, skipping load"
853
1227
  );
@@ -856,9 +1230,9 @@ var JsonFileSessionStore = class {
856
1230
  for (const [id, record] of Object.entries(raw.sessions)) {
857
1231
  this.records.set(id, record);
858
1232
  }
859
- log2.info({ count: this.records.size }, "Loaded session records");
1233
+ log3.info({ count: this.records.size }, "Loaded session records");
860
1234
  } catch (err) {
861
- log2.error({ err }, "Failed to load session store");
1235
+ log3.error({ err }, "Failed to load session store");
862
1236
  }
863
1237
  }
864
1238
  cleanup() {
@@ -874,7 +1248,7 @@ var JsonFileSessionStore = class {
874
1248
  }
875
1249
  }
876
1250
  if (removed > 0) {
877
- log2.info({ removed }, "Cleaned up expired session records");
1251
+ log3.info({ removed }, "Cleaned up expired session records");
878
1252
  this.scheduleDiskWrite();
879
1253
  }
880
1254
  }
@@ -886,105 +1260,18 @@ var JsonFileSessionStore = class {
886
1260
  }
887
1261
  };
888
1262
 
889
- // src/tunnel/extract-file-info.ts
890
- function extractFileInfo(name, kind, content, rawInput, meta) {
891
- if (kind && !["read", "edit", "write"].includes(kind)) return null;
892
- let info = null;
893
- if (meta) {
894
- const m = meta;
895
- const tr = m?.claudeCode?.toolResponse;
896
- const file = tr?.file;
897
- if (file?.filePath && file?.content) {
898
- info = { filePath: file.filePath, content: file.content };
899
- }
900
- if (!info && tr?.filePath && tr?.content) {
901
- info = { filePath: tr.filePath, content: tr.content };
902
- }
903
- }
904
- if (!info && rawInput) {
905
- const ri = rawInput;
906
- const filePath = ri?.file_path || ri?.filePath || ri?.path;
907
- if (typeof filePath === "string") {
908
- const parsed = content ? parseContent(content) : null;
909
- info = { filePath, content: parsed?.content || ri?.content, oldContent: parsed?.oldContent };
910
- }
911
- }
912
- if (!info && content) {
913
- info = parseContent(content);
914
- }
915
- if (!info) return null;
916
- if (!info.filePath) {
917
- const pathMatch = name.match(/(?:Read|Edit|Write|View)\s+(.+)/i);
918
- if (pathMatch) info.filePath = pathMatch[1].trim();
919
- }
920
- if (!info.filePath || !info.content) return null;
921
- return info;
922
- }
923
- function parseContent(content) {
924
- if (typeof content === "string") {
925
- return { content };
926
- }
927
- if (Array.isArray(content)) {
928
- for (const block of content) {
929
- const result = parseContent(block);
930
- if (result?.content || result?.filePath) return result;
931
- }
932
- return null;
933
- }
934
- if (typeof content === "object" && content !== null) {
935
- const c = content;
936
- if (c.type === "diff" && typeof c.path === "string") {
937
- const newText = c.newText;
938
- const oldText = c.oldText;
939
- if (newText) {
940
- return {
941
- filePath: c.path,
942
- content: newText,
943
- oldContent: oldText ?? void 0
944
- };
945
- }
946
- }
947
- if (c.type === "content" && c.content) {
948
- return parseContent(c.content);
949
- }
950
- if (c.type === "text" && typeof c.text === "string") {
951
- return { content: c.text, filePath: c.filePath };
952
- }
953
- if (typeof c.text === "string") {
954
- return { content: c.text, filePath: c.filePath };
955
- }
956
- if (typeof c.file_path === "string" || typeof c.filePath === "string" || typeof c.path === "string") {
957
- const filePath = c.file_path || c.filePath || c.path;
958
- const fileContent = c.content || c.text || c.output || c.newText;
959
- if (typeof fileContent === "string") {
960
- return {
961
- filePath,
962
- content: fileContent,
963
- oldContent: c.old_content || c.oldText
964
- };
965
- }
966
- }
967
- if (c.input) {
968
- const result = parseContent(c.input);
969
- if (result) return result;
970
- }
971
- if (c.output) {
972
- const result = parseContent(c.output);
973
- if (result) return result;
974
- }
975
- }
976
- return null;
977
- }
978
-
979
1263
  // src/core/core.ts
980
- var log3 = createChildLogger({ module: "core" });
1264
+ var log4 = createChildLogger({ module: "core" });
981
1265
  var OpenACPCore = class {
982
1266
  configManager;
983
1267
  agentManager;
984
1268
  sessionManager;
985
1269
  notificationManager;
1270
+ messageTransformer;
986
1271
  adapters = /* @__PURE__ */ new Map();
987
- tunnelService;
1272
+ /** Set by main.ts — triggers graceful shutdown with restart exit code */
1273
+ requestRestart = null;
1274
+ _tunnelService;
988
1275
  sessionStore = null;
989
1276
  resumeLocks = /* @__PURE__ */ new Map();
990
1277
  constructor(configManager) {
@@ -998,6 +1285,14 @@ var OpenACPCore = class {
998
1285
  );
999
1286
  this.sessionManager = new SessionManager(this.sessionStore);
1000
1287
  this.notificationManager = new NotificationManager(this.adapters);
1288
+ this.messageTransformer = new MessageTransformer();
1289
+ }
1290
+ get tunnelService() {
1291
+ return this._tunnelService;
1292
+ }
1293
+ set tunnelService(service) {
1294
+ this._tunnelService = service;
1295
+ this.messageTransformer = new MessageTransformer(service);
1001
1296
  }
1002
1297
  registerAdapter(name, adapter) {
1003
1298
  this.adapters.set(name, adapter);
@@ -1024,7 +1319,7 @@ var OpenACPCore = class {
1024
1319
  // --- Message Routing ---
1025
1320
  async handleMessage(message) {
1026
1321
  const config = this.configManager.get();
1027
- log3.debug(
1322
+ log4.debug(
1028
1323
  {
1029
1324
  channelId: message.channelId,
1030
1325
  threadId: message.threadId,
@@ -1034,7 +1329,7 @@ var OpenACPCore = class {
1034
1329
  );
1035
1330
  if (config.security.allowedUserIds.length > 0) {
1036
1331
  if (!config.security.allowedUserIds.includes(message.userId)) {
1037
- log3.warn(
1332
+ log4.warn(
1038
1333
  { userId: message.userId },
1039
1334
  "Rejected message from unauthorized user"
1040
1335
  );
@@ -1043,7 +1338,7 @@ var OpenACPCore = class {
1043
1338
  }
1044
1339
  const activeSessions = this.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
1045
1340
  if (activeSessions.length >= config.security.maxConcurrentSessions) {
1046
- log3.warn(
1341
+ log4.warn(
1047
1342
  {
1048
1343
  userId: message.userId,
1049
1344
  currentCount: activeSessions.length,
@@ -1074,7 +1369,7 @@ var OpenACPCore = class {
1074
1369
  async handleNewSession(channelId, agentName, workspacePath) {
1075
1370
  const config = this.configManager.get();
1076
1371
  const resolvedAgent = agentName || config.defaultAgent;
1077
- log3.info({ channelId, agentName: resolvedAgent }, "New session request");
1372
+ log4.info({ channelId, agentName: resolvedAgent }, "New session request");
1078
1373
  const resolvedWorkspace = this.configManager.resolveWorkspace(
1079
1374
  workspacePath || config.agents[resolvedAgent]?.workingDirectory
1080
1375
  );
@@ -1152,13 +1447,13 @@ var OpenACPCore = class {
1152
1447
  status: "active",
1153
1448
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1154
1449
  });
1155
- log3.info(
1450
+ log4.info(
1156
1451
  { sessionId: session.id, threadId: message.threadId },
1157
1452
  "Lazy resume successful"
1158
1453
  );
1159
1454
  return session;
1160
1455
  } catch (err) {
1161
- log3.error({ err, record }, "Lazy resume failed");
1456
+ log4.error({ err, record }, "Lazy resume failed");
1162
1457
  return null;
1163
1458
  } finally {
1164
1459
  this.resumeLocks.delete(lockKey);
@@ -1168,98 +1463,27 @@ var OpenACPCore = class {
1168
1463
  return resumePromise;
1169
1464
  }
1170
1465
  // --- Event Wiring ---
1171
- toOutgoingMessage(event, session) {
1172
- switch (event.type) {
1173
- case "text":
1174
- return { type: "text", text: event.content };
1175
- case "thought":
1176
- return { type: "thought", text: event.content };
1177
- case "tool_call": {
1178
- const metadata = {
1179
- id: event.id,
1180
- name: event.name,
1181
- kind: event.kind,
1182
- status: event.status,
1183
- content: event.content,
1184
- locations: event.locations
1185
- };
1186
- this.enrichWithViewerLinks(event, metadata, session);
1187
- return { type: "tool_call", text: event.name, metadata };
1188
- }
1189
- case "tool_update": {
1190
- const metadata = {
1191
- id: event.id,
1192
- name: event.name,
1193
- kind: event.kind,
1194
- status: event.status,
1195
- content: event.content
1196
- };
1197
- this.enrichWithViewerLinks(event, metadata, session);
1198
- return { type: "tool_update", text: "", metadata };
1199
- }
1200
- case "plan":
1201
- return { type: "plan", text: "", metadata: { entries: event.entries } };
1202
- case "usage":
1203
- return {
1204
- type: "usage",
1205
- text: "",
1206
- metadata: {
1207
- tokensUsed: event.tokensUsed,
1208
- contextSize: event.contextSize,
1209
- cost: event.cost
1210
- }
1211
- };
1212
- default:
1213
- return { type: "text", text: "" };
1214
- }
1215
- }
1216
- enrichWithViewerLinks(event, metadata, session) {
1217
- if (!this.tunnelService || !session) return;
1218
- const name = "name" in event ? event.name || "" : "";
1219
- const kind = "kind" in event ? event.kind : void 0;
1220
- log3.debug(
1221
- { name, kind, status: event.status, hasContent: !!event.content },
1222
- "enrichWithViewerLinks: inspecting event"
1223
- );
1224
- const fileInfo = extractFileInfo(name, kind, event.content, event.rawInput, event.meta);
1225
- if (!fileInfo) return;
1226
- log3.info(
1227
- {
1228
- name,
1229
- kind,
1230
- filePath: fileInfo.filePath,
1231
- hasOldContent: !!fileInfo.oldContent
1232
- },
1233
- "enrichWithViewerLinks: extracted file info"
1234
- );
1235
- const store = this.tunnelService.getStore();
1236
- const viewerLinks = {};
1237
- if (fileInfo.oldContent) {
1238
- const id2 = store.storeDiff(
1239
- session.id,
1240
- fileInfo.filePath,
1241
- fileInfo.oldContent,
1242
- fileInfo.content,
1243
- session.workingDirectory
1244
- );
1245
- if (id2) viewerLinks.diff = this.tunnelService.diffUrl(id2);
1246
- }
1247
- const id = store.storeFile(
1248
- session.id,
1249
- fileInfo.filePath,
1250
- fileInfo.content,
1251
- session.workingDirectory
1252
- );
1253
- if (id) viewerLinks.file = this.tunnelService.fileUrl(id);
1254
- if (Object.keys(viewerLinks).length > 0) {
1255
- metadata.viewerLinks = viewerLinks;
1256
- metadata.viewerFilePath = fileInfo.filePath;
1257
- }
1258
- }
1259
1466
  // Public — adapters call this for assistant session wiring
1260
1467
  wireSessionEvents(session, adapter) {
1261
1468
  session.adapter = adapter;
1262
1469
  session.agentInstance.onSessionUpdate = (event) => {
1470
+ session.emit("agent_event", event);
1471
+ };
1472
+ session.agentInstance.onPermissionRequest = async (request) => {
1473
+ session.emit("permission_request", request);
1474
+ const promise = session.permissionGate.setPending(request);
1475
+ await adapter.sendPermissionRequest(session.id, request);
1476
+ return promise;
1477
+ };
1478
+ const sessionContext = {
1479
+ get id() {
1480
+ return session.id;
1481
+ },
1482
+ get workingDirectory() {
1483
+ return session.workingDirectory;
1484
+ }
1485
+ };
1486
+ session.on("agent_event", (event) => {
1263
1487
  switch (event.type) {
1264
1488
  case "text":
1265
1489
  case "thought":
@@ -1269,17 +1493,17 @@ var OpenACPCore = class {
1269
1493
  case "usage":
1270
1494
  adapter.sendMessage(
1271
1495
  session.id,
1272
- this.toOutgoingMessage(event, session)
1496
+ this.messageTransformer.transform(event, sessionContext)
1273
1497
  );
1274
1498
  break;
1275
1499
  case "session_end":
1276
1500
  session.status = "finished";
1277
1501
  this.sessionManager.updateSessionStatus(session.id, "finished");
1278
1502
  adapter.cleanupSkillCommands(session.id);
1279
- adapter.sendMessage(session.id, {
1280
- type: "session_end",
1281
- text: `Done (${event.reason})`
1282
- });
1503
+ adapter.sendMessage(
1504
+ session.id,
1505
+ this.messageTransformer.transform(event)
1506
+ );
1283
1507
  this.notificationManager.notify(session.channelId, {
1284
1508
  sessionId: session.id,
1285
1509
  sessionName: session.name,
@@ -1290,10 +1514,10 @@ var OpenACPCore = class {
1290
1514
  case "error":
1291
1515
  this.sessionManager.updateSessionStatus(session.id, "error");
1292
1516
  adapter.cleanupSkillCommands(session.id);
1293
- adapter.sendMessage(session.id, {
1294
- type: "error",
1295
- text: event.message
1296
- });
1517
+ adapter.sendMessage(
1518
+ session.id,
1519
+ this.messageTransformer.transform(event)
1520
+ );
1297
1521
  this.notificationManager.notify(session.channelId, {
1298
1522
  sessionId: session.id,
1299
1523
  sessionName: session.name,
@@ -1302,18 +1526,11 @@ var OpenACPCore = class {
1302
1526
  });
1303
1527
  break;
1304
1528
  case "commands_update":
1305
- log3.debug({ commands: event.commands }, "Commands available");
1529
+ log4.debug({ commands: event.commands }, "Commands available");
1306
1530
  adapter.sendSkillCommands(session.id, event.commands);
1307
1531
  break;
1308
1532
  }
1309
- };
1310
- session.agentInstance.onPermissionRequest = async (request) => {
1311
- const promise = new Promise((resolve) => {
1312
- session.pendingPermission = { requestId: request.id, resolve };
1313
- });
1314
- await adapter.sendPermissionRequest(session.id, request);
1315
- return promise;
1316
- };
1533
+ });
1317
1534
  }
1318
1535
  };
1319
1536
 
@@ -1335,7 +1552,7 @@ import * as http from "http";
1335
1552
  import * as fs3 from "fs";
1336
1553
  import * as path4 from "path";
1337
1554
  import * as os2 from "os";
1338
- var log4 = createChildLogger({ module: "api-server" });
1555
+ var log5 = createChildLogger({ module: "api-server" });
1339
1556
  var DEFAULT_PORT_FILE = path4.join(os2.homedir(), ".openacp", "api.port");
1340
1557
  var ApiServer = class {
1341
1558
  constructor(core, config, portFilePath) {
@@ -1351,7 +1568,7 @@ var ApiServer = class {
1351
1568
  await new Promise((resolve, reject) => {
1352
1569
  this.server.on("error", (err) => {
1353
1570
  if (err.code === "EADDRINUSE") {
1354
- log4.warn({ port: this.config.port }, "API port in use, continuing without API server");
1571
+ log5.warn({ port: this.config.port }, "API port in use, continuing without API server");
1355
1572
  this.server = null;
1356
1573
  resolve();
1357
1574
  } else {
@@ -1364,7 +1581,7 @@ var ApiServer = class {
1364
1581
  this.actualPort = addr.port;
1365
1582
  }
1366
1583
  this.writePortFile();
1367
- log4.info({ host: this.config.host, port: this.actualPort }, "API server listening");
1584
+ log5.info({ host: this.config.host, port: this.actualPort }, "API server listening");
1368
1585
  resolve();
1369
1586
  });
1370
1587
  });
@@ -1409,7 +1626,7 @@ var ApiServer = class {
1409
1626
  this.sendJson(res, 404, { error: "Not found" });
1410
1627
  }
1411
1628
  } catch (err) {
1412
- log4.error({ err }, "API request error");
1629
+ log5.error({ err }, "API request error");
1413
1630
  this.sendJson(res, 500, { error: "Internal server error" });
1414
1631
  }
1415
1632
  }
@@ -1444,17 +1661,17 @@ var ApiServer = class {
1444
1661
  session.threadId = threadId;
1445
1662
  this.core.wireSessionEvents(session, adapter);
1446
1663
  } catch (err) {
1447
- log4.warn({ err, sessionId: session.id }, "Failed to create session thread on adapter, running headless");
1664
+ log5.warn({ err, sessionId: session.id }, "Failed to create session thread on adapter, running headless");
1448
1665
  }
1449
1666
  }
1450
1667
  if (!adapter) {
1451
1668
  session.agentInstance.onPermissionRequest = async (request) => {
1452
1669
  const allowOption = request.options.find((o) => o.isAllow);
1453
- log4.debug({ sessionId: session.id, permissionId: request.id, option: allowOption?.id }, "Auto-approving permission for API session");
1670
+ log5.debug({ sessionId: session.id, permissionId: request.id, option: allowOption?.id }, "Auto-approving permission for API session");
1454
1671
  return allowOption?.id ?? request.options[0]?.id ?? "";
1455
1672
  };
1456
1673
  }
1457
- session.warmup().catch((err) => log4.warn({ err, sessionId: session.id }, "API session warmup failed"));
1674
+ session.warmup().catch((err) => log5.warn({ err, sessionId: session.id }, "API session warmup failed"));
1458
1675
  this.sendJson(res, 200, {
1459
1676
  sessionId: session.id,
1460
1677
  agent: session.agentName,
@@ -1824,7 +2041,7 @@ function buildDeepLink(chatId, messageId) {
1824
2041
  // src/adapters/telegram/commands.ts
1825
2042
  import { InlineKeyboard } from "grammy";
1826
2043
  import { nanoid as nanoid2 } from "nanoid";
1827
- var log6 = createChildLogger({ module: "telegram-commands" });
2044
+ var log7 = createChildLogger({ module: "telegram-commands" });
1828
2045
  function setupCommands(bot, core, chatId, assistant) {
1829
2046
  bot.command("new", (ctx) => handleNew(ctx, core, chatId, assistant));
1830
2047
  bot.command("new_chat", (ctx) => handleNewChat(ctx, core, chatId));
@@ -1835,9 +2052,11 @@ function setupCommands(bot, core, chatId, assistant) {
1835
2052
  bot.command("menu", (ctx) => handleMenu(ctx));
1836
2053
  bot.command("enable_dangerous", (ctx) => handleEnableDangerous(ctx, core));
1837
2054
  bot.command("disable_dangerous", (ctx) => handleDisableDangerous(ctx, core));
2055
+ bot.command("restart", (ctx) => handleRestart(ctx, core));
2056
+ bot.command("update", (ctx) => handleUpdate(ctx, core));
1838
2057
  }
1839
2058
  function buildMenuKeyboard() {
1840
- return new InlineKeyboard().text("\u{1F195} New Session", "m:new").text("\u{1F4AC} New Chat", "m:new_chat").row().text("\u26D4 Cancel", "m:cancel").text("\u{1F4CA} Status", "m:status").row().text("\u{1F916} Agents", "m:agents").text("\u2753 Help", "m:help");
2059
+ return new InlineKeyboard().text("\u{1F195} New Session", "m:new").text("\u{1F4AC} New Chat", "m:new_chat").row().text("\u26D4 Cancel", "m:cancel").text("\u{1F4CA} Status", "m:status").row().text("\u{1F916} Agents", "m:agents").text("\u2753 Help", "m:help").row().text("\u{1F504} Restart", "m:restart").text("\u2B06\uFE0F Update", "m:update");
1841
2060
  }
1842
2061
  function setupMenuCallbacks(bot, core, chatId) {
1843
2062
  bot.callbackQuery(/^m:/, async (ctx) => {
@@ -1865,6 +2084,12 @@ function setupMenuCallbacks(bot, core, chatId) {
1865
2084
  case "m:help":
1866
2085
  await handleHelp(ctx);
1867
2086
  break;
2087
+ case "m:restart":
2088
+ await handleRestart(ctx, core);
2089
+ break;
2090
+ case "m:update":
2091
+ await handleUpdate(ctx, core);
2092
+ break;
1868
2093
  }
1869
2094
  });
1870
2095
  }
@@ -1890,7 +2115,7 @@ async function handleNew(ctx, core, chatId, assistant) {
1890
2115
  return;
1891
2116
  }
1892
2117
  }
1893
- log6.info({ userId: ctx.from?.id, agentName }, "New session command");
2118
+ log7.info({ userId: ctx.from?.id, agentName }, "New session command");
1894
2119
  let threadId;
1895
2120
  try {
1896
2121
  const topicName = `\u{1F504} New Session`;
@@ -1924,9 +2149,9 @@ async function handleNew(ctx, core, chatId, assistant) {
1924
2149
  reply_markup: buildDangerousModeKeyboard(session.id, false)
1925
2150
  }
1926
2151
  );
1927
- session.warmup().catch((err) => log6.error({ err }, "Warm-up error"));
2152
+ session.warmup().catch((err) => log7.error({ err }, "Warm-up error"));
1928
2153
  } catch (err) {
1929
- log6.error({ err }, "Session creation failed");
2154
+ log7.error({ err }, "Session creation failed");
1930
2155
  if (threadId) {
1931
2156
  try {
1932
2157
  await ctx.api.deleteForumTopic(chatId, threadId);
@@ -1946,16 +2171,30 @@ async function handleNewChat(ctx, core, chatId) {
1946
2171
  );
1947
2172
  return;
1948
2173
  }
1949
- try {
1950
- const session = await core.handleNewChat("telegram", String(threadId));
1951
- if (!session) {
2174
+ const currentSession = core.sessionManager.getSessionByThread(
2175
+ "telegram",
2176
+ String(threadId)
2177
+ );
2178
+ let agentName;
2179
+ let workspace;
2180
+ if (currentSession) {
2181
+ agentName = currentSession.agentName;
2182
+ workspace = currentSession.workingDirectory;
2183
+ } else {
2184
+ const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
2185
+ if (!record || record.status === "cancelled" || record.status === "error") {
1952
2186
  await ctx.reply("No active session in this topic.", {
1953
2187
  parse_mode: "HTML"
1954
2188
  });
1955
2189
  return;
1956
2190
  }
1957
- const topicName = `\u{1F504} ${session.agentName} \u2014 New Chat`;
1958
- const newThreadId = await createSessionTopic(
2191
+ agentName = record.agentName;
2192
+ workspace = record.workingDir;
2193
+ }
2194
+ let newThreadId;
2195
+ try {
2196
+ const topicName = `\u{1F504} ${agentName} \u2014 New Chat`;
2197
+ newThreadId = await createSessionTopic(
1959
2198
  botFromCtx(ctx),
1960
2199
  chatId,
1961
2200
  topicName
@@ -1964,6 +2203,11 @@ async function handleNewChat(ctx, core, chatId) {
1964
2203
  message_thread_id: newThreadId,
1965
2204
  parse_mode: "HTML"
1966
2205
  });
2206
+ const session = await core.handleNewSession(
2207
+ "telegram",
2208
+ agentName,
2209
+ workspace
2210
+ );
1967
2211
  session.threadId = String(newThreadId);
1968
2212
  await core.sessionManager.updateSessionPlatform(session.id, {
1969
2213
  topicId: newThreadId
@@ -1979,8 +2223,14 @@ async function handleNewChat(ctx, core, chatId) {
1979
2223
  reply_markup: buildDangerousModeKeyboard(session.id, false)
1980
2224
  }
1981
2225
  );
1982
- session.warmup().catch((err) => log6.error({ err }, "Warm-up error"));
2226
+ session.warmup().catch((err) => log7.error({ err }, "Warm-up error"));
1983
2227
  } catch (err) {
2228
+ if (newThreadId) {
2229
+ try {
2230
+ await ctx.api.deleteForumTopic(chatId, newThreadId);
2231
+ } catch {
2232
+ }
2233
+ }
1984
2234
  const message = err instanceof Error ? err.message : String(err);
1985
2235
  await ctx.reply(`\u274C ${escapeHtml(message)}`, { parse_mode: "HTML" });
1986
2236
  }
@@ -2002,14 +2252,14 @@ async function handleCancel(ctx, core, assistant) {
2002
2252
  String(threadId)
2003
2253
  );
2004
2254
  if (session) {
2005
- log6.info({ sessionId: session.id }, "Cancel session command");
2255
+ log7.info({ sessionId: session.id }, "Cancel session command");
2006
2256
  await session.cancel();
2007
2257
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
2008
2258
  return;
2009
2259
  }
2010
2260
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
2011
2261
  if (record && record.status !== "cancelled" && record.status !== "error") {
2012
- log6.info({ sessionId: record.sessionId }, "Cancel session command (from store)");
2262
+ log7.info({ sessionId: record.sessionId }, "Cancel session command (from store)");
2013
2263
  await core.sessionManager.cancelSession(record.sessionId);
2014
2264
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
2015
2265
  }
@@ -2027,7 +2277,7 @@ async function handleStatus(ctx, core) {
2027
2277
  <b>Agent:</b> ${escapeHtml(session.agentName)}
2028
2278
  <b>Status:</b> ${escapeHtml(session.status)}
2029
2279
  <b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>
2030
- <b>Queue:</b> ${session.promptQueue.length} pending`,
2280
+ <b>Queue:</b> ${session.queueDepth} pending`,
2031
2281
  { parse_mode: "HTML" }
2032
2282
  );
2033
2283
  } else {
@@ -2083,6 +2333,8 @@ async function handleHelp(ctx) {
2083
2333
  /status \u2014 Show session/system status
2084
2334
  /agents \u2014 List available agents
2085
2335
  /menu \u2014 Show interactive menu
2336
+ /restart \u2014 Restart OpenACP
2337
+ /update \u2014 Update to latest version and restart
2086
2338
  /help \u2014 Show this help
2087
2339
 
2088
2340
  Or just chat in the \u{1F916} Assistant topic for help!`,
@@ -2107,7 +2359,7 @@ function setupDangerousModeCallbacks(bot, core) {
2107
2359
  return;
2108
2360
  }
2109
2361
  session.dangerousMode = !session.dangerousMode;
2110
- log6.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
2362
+ log7.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
2111
2363
  const toastText = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
2112
2364
  try {
2113
2365
  await ctx.answerCallbackQuery({ text: toastText });
@@ -2146,6 +2398,52 @@ Use /disable_dangerous to restore normal behaviour.`,
2146
2398
  { parse_mode: "HTML" }
2147
2399
  );
2148
2400
  }
2401
+ async function handleUpdate(ctx, core) {
2402
+ if (!core.requestRestart) {
2403
+ await ctx.reply("\u26A0\uFE0F Update is not available (no restart handler registered).", { parse_mode: "HTML" });
2404
+ return;
2405
+ }
2406
+ const { getCurrentVersion, getLatestVersion, compareVersions, runUpdate } = await import("./version-VC5CPXBX.js");
2407
+ const current = getCurrentVersion();
2408
+ const statusMsg = await ctx.reply(`\u{1F50D} Checking for updates... (current: v${escapeHtml(current)})`, { parse_mode: "HTML" });
2409
+ const latest = await getLatestVersion();
2410
+ if (!latest) {
2411
+ await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, "\u274C Could not check for updates.", { parse_mode: "HTML" });
2412
+ return;
2413
+ }
2414
+ if (compareVersions(current, latest) >= 0) {
2415
+ await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, `\u2705 Already up to date (v${escapeHtml(current)}).`, { parse_mode: "HTML" });
2416
+ return;
2417
+ }
2418
+ await ctx.api.editMessageText(
2419
+ ctx.chat.id,
2420
+ statusMsg.message_id,
2421
+ `\u2B07\uFE0F Updating v${escapeHtml(current)} \u2192 v${escapeHtml(latest)}...`,
2422
+ { parse_mode: "HTML" }
2423
+ );
2424
+ const ok = await runUpdate();
2425
+ if (!ok) {
2426
+ await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, "\u274C Update failed. Try manually: <code>npm install -g @openacp/cli@latest</code>", { parse_mode: "HTML" });
2427
+ return;
2428
+ }
2429
+ await ctx.api.editMessageText(
2430
+ ctx.chat.id,
2431
+ statusMsg.message_id,
2432
+ `\u2705 Updated to v${escapeHtml(latest)}. Restarting...`,
2433
+ { parse_mode: "HTML" }
2434
+ );
2435
+ await new Promise((r) => setTimeout(r, 500));
2436
+ await core.requestRestart();
2437
+ }
2438
+ async function handleRestart(ctx, core) {
2439
+ if (!core.requestRestart) {
2440
+ await ctx.reply("\u26A0\uFE0F Restart is not available (no restart handler registered).", { parse_mode: "HTML" });
2441
+ return;
2442
+ }
2443
+ await ctx.reply("\u{1F504} <b>Restarting OpenACP...</b>\nRebuilding and restarting. Be back shortly.", { parse_mode: "HTML" });
2444
+ await new Promise((r) => setTimeout(r, 500));
2445
+ await core.requestRestart();
2446
+ }
2149
2447
  async function handleDisableDangerous(ctx, core) {
2150
2448
  const threadId = ctx.message?.message_thread_id;
2151
2449
  if (!threadId) {
@@ -2222,7 +2520,7 @@ async function executeNewSession(bot, core, chatId, agentName, workspace) {
2222
2520
  });
2223
2521
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
2224
2522
  await renameSessionTopic(bot, chatId, threadId, finalName);
2225
- session.warmup().catch((err) => log6.error({ err }, "Warm-up error"));
2523
+ session.warmup().catch((err) => log7.error({ err }, "Warm-up error"));
2226
2524
  return { session, threadId, firstMsgId };
2227
2525
  } catch (err) {
2228
2526
  try {
@@ -2248,13 +2546,15 @@ var STATIC_COMMANDS = [
2248
2546
  { command: "help", description: "Help" },
2249
2547
  { command: "menu", description: "Show menu" },
2250
2548
  { command: "enable_dangerous", description: "Auto-approve all permission requests (session only)" },
2251
- { command: "disable_dangerous", description: "Restore normal permission prompts (session only)" }
2549
+ { command: "disable_dangerous", description: "Restore normal permission prompts (session only)" },
2550
+ { command: "restart", description: "Restart OpenACP" },
2551
+ { command: "update", description: "Update to latest version and restart" }
2252
2552
  ];
2253
2553
 
2254
2554
  // src/adapters/telegram/permissions.ts
2255
2555
  import { InlineKeyboard as InlineKeyboard2 } from "grammy";
2256
2556
  import { nanoid as nanoid3 } from "nanoid";
2257
- var log7 = createChildLogger({ module: "telegram-permissions" });
2557
+ var log8 = createChildLogger({ module: "telegram-permissions" });
2258
2558
  var PermissionHandler = class {
2259
2559
  constructor(bot, chatId, getSession, sendNotification) {
2260
2560
  this.bot = bot;
@@ -2314,10 +2614,9 @@ ${escapeHtml(request.description)}`,
2314
2614
  }
2315
2615
  const session = this.getSession(pending.sessionId);
2316
2616
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
2317
- log7.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
2318
- if (session?.pendingPermission?.requestId === pending.requestId) {
2319
- session.pendingPermission.resolve(optionId);
2320
- session.pendingPermission = void 0;
2617
+ log8.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
2618
+ if (session?.permissionGate.requestId === pending.requestId) {
2619
+ session.permissionGate.resolve(optionId);
2321
2620
  }
2322
2621
  this.pending.delete(callbackKey);
2323
2622
  try {
@@ -2333,10 +2632,10 @@ ${escapeHtml(request.description)}`,
2333
2632
  };
2334
2633
 
2335
2634
  // src/adapters/telegram/assistant.ts
2336
- var log8 = createChildLogger({ module: "telegram-assistant" });
2635
+ var log9 = createChildLogger({ module: "telegram-assistant" });
2337
2636
  async function spawnAssistant(core, adapter, assistantTopicId) {
2338
2637
  const config = core.configManager.get();
2339
- log8.info({ agent: config.defaultAgent }, "Creating assistant session...");
2638
+ log9.info({ agent: config.defaultAgent }, "Creating assistant session...");
2340
2639
  const session = await core.sessionManager.createSession(
2341
2640
  "telegram",
2342
2641
  config.defaultAgent,
@@ -2345,13 +2644,13 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
2345
2644
  );
2346
2645
  session.threadId = String(assistantTopicId);
2347
2646
  session.name = "Assistant";
2348
- log8.info({ sessionId: session.id }, "Assistant agent spawned");
2647
+ log9.info({ sessionId: session.id }, "Assistant agent spawned");
2349
2648
  core.wireSessionEvents(session, adapter);
2350
2649
  const systemPrompt = buildAssistantSystemPrompt(config);
2351
2650
  const ready = session.enqueuePrompt(systemPrompt).then(() => {
2352
- log8.info({ sessionId: session.id }, "Assistant system prompt completed");
2651
+ log9.info({ sessionId: session.id }, "Assistant system prompt completed");
2353
2652
  }).catch((err) => {
2354
- log8.warn({ err }, "Assistant system prompt failed");
2653
+ log9.warn({ err }, "Assistant system prompt failed");
2355
2654
  });
2356
2655
  return { session, ready };
2357
2656
  }
@@ -2389,7 +2688,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
2389
2688
  }
2390
2689
 
2391
2690
  // src/adapters/telegram/activity.ts
2392
- var log9 = createChildLogger({ module: "telegram:activity" });
2691
+ var log10 = createChildLogger({ module: "telegram:activity" });
2393
2692
  var THINKING_REFRESH_MS = 15e3;
2394
2693
  var THINKING_MAX_MS = 3 * 60 * 1e3;
2395
2694
  var ThinkingIndicator = class {
@@ -2421,7 +2720,7 @@ var ThinkingIndicator = class {
2421
2720
  this.startRefreshTimer();
2422
2721
  }
2423
2722
  } catch (err) {
2424
- log9.warn({ err }, "ThinkingIndicator.show() failed");
2723
+ log10.warn({ err }, "ThinkingIndicator.show() failed");
2425
2724
  } finally {
2426
2725
  this.sending = false;
2427
2726
  }
@@ -2494,7 +2793,7 @@ var UsageMessage = class {
2494
2793
  if (result) this.msgId = result.message_id;
2495
2794
  }
2496
2795
  } catch (err) {
2497
- log9.warn({ err }, "UsageMessage.send() failed");
2796
+ log10.warn({ err }, "UsageMessage.send() failed");
2498
2797
  }
2499
2798
  }
2500
2799
  getMsgId() {
@@ -2507,7 +2806,7 @@ var UsageMessage = class {
2507
2806
  try {
2508
2807
  await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
2509
2808
  } catch (err) {
2510
- log9.warn({ err }, "UsageMessage.delete() failed");
2809
+ log10.warn({ err }, "UsageMessage.delete() failed");
2511
2810
  }
2512
2811
  }
2513
2812
  };
@@ -2590,7 +2889,7 @@ var PlanCard = class {
2590
2889
  if (result) this.msgId = result.message_id;
2591
2890
  }
2592
2891
  } catch (err) {
2593
- log9.warn({ err }, "PlanCard flush failed");
2892
+ log10.warn({ err }, "PlanCard flush failed");
2594
2893
  }
2595
2894
  }
2596
2895
  };
@@ -2653,7 +2952,7 @@ var ActivityTracker = class {
2653
2952
  })
2654
2953
  );
2655
2954
  } catch (err) {
2656
- log9.warn({ err }, "ActivityTracker.onComplete() Done send failed");
2955
+ log10.warn({ err }, "ActivityTracker.onComplete() Done send failed");
2657
2956
  }
2658
2957
  }
2659
2958
  }
@@ -2880,7 +3179,7 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
2880
3179
  }
2881
3180
 
2882
3181
  // src/adapters/telegram/adapter.ts
2883
- var log10 = createChildLogger({ module: "telegram" });
3182
+ var log11 = createChildLogger({ module: "telegram" });
2884
3183
  function patchedFetch(input, init) {
2885
3184
  if (init?.signal && !(init.signal instanceof AbortSignal)) {
2886
3185
  const nativeController = new AbortController();
@@ -2931,7 +3230,7 @@ var TelegramAdapter = class extends ChannelAdapter {
2931
3230
  this.bot = new Bot(this.telegramConfig.botToken, { client: { fetch: patchedFetch } });
2932
3231
  this.bot.catch((err) => {
2933
3232
  const rootCause = err.error instanceof Error ? err.error : err;
2934
- log10.error({ err: rootCause }, "Telegram bot error");
3233
+ log11.error({ err: rootCause }, "Telegram bot error");
2935
3234
  });
2936
3235
  this.bot.api.config.use(async (prev, method, payload, signal) => {
2937
3236
  const maxRetries = 3;
@@ -2945,7 +3244,7 @@ var TelegramAdapter = class extends ChannelAdapter {
2945
3244
  if (rateLimitedMethods.includes(method)) {
2946
3245
  this.sendQueue.onRateLimited();
2947
3246
  }
2948
- log10.warn(
3247
+ log11.warn(
2949
3248
  { method, retryAfter, attempt: attempt + 1 },
2950
3249
  "Rate limited by Telegram, retrying"
2951
3250
  );
@@ -3015,7 +3314,7 @@ var TelegramAdapter = class extends ChannelAdapter {
3015
3314
  this.setupRoutes();
3016
3315
  this.bot.start({
3017
3316
  allowed_updates: ["message", "callback_query"],
3018
- onStart: () => log10.info(
3317
+ onStart: () => log11.info(
3019
3318
  { chatId: this.telegramConfig.chatId },
3020
3319
  "Telegram bot started"
3021
3320
  )
@@ -3037,10 +3336,10 @@ Workspace: <code>${workspace}</code>
3037
3336
  reply_markup: buildMenuKeyboard()
3038
3337
  });
3039
3338
  } catch (err) {
3040
- log10.warn({ err }, "Failed to send welcome message");
3339
+ log11.warn({ err }, "Failed to send welcome message");
3041
3340
  }
3042
3341
  try {
3043
- log10.info("Spawning assistant session...");
3342
+ log11.info("Spawning assistant session...");
3044
3343
  const { session, ready } = await spawnAssistant(
3045
3344
  this.core,
3046
3345
  this,
@@ -3048,13 +3347,13 @@ Workspace: <code>${workspace}</code>
3048
3347
  );
3049
3348
  this.assistantSession = session;
3050
3349
  this.assistantInitializing = true;
3051
- log10.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
3350
+ log11.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
3052
3351
  ready.then(() => {
3053
3352
  this.assistantInitializing = false;
3054
- log10.info({ sessionId: session.id }, "Assistant ready for user messages");
3353
+ log11.info({ sessionId: session.id }, "Assistant ready for user messages");
3055
3354
  });
3056
3355
  } catch (err) {
3057
- log10.error({ err }, "Failed to spawn assistant");
3356
+ log11.error({ err }, "Failed to spawn assistant");
3058
3357
  this.bot.api.sendMessage(
3059
3358
  this.telegramConfig.chatId,
3060
3359
  `\u26A0\uFE0F <b>Failed to start assistant session.</b>
@@ -3070,7 +3369,7 @@ Workspace: <code>${workspace}</code>
3070
3369
  await this.assistantSession.destroy();
3071
3370
  }
3072
3371
  await this.bot.stop();
3073
- log10.info("Telegram bot stopped");
3372
+ log11.info("Telegram bot stopped");
3074
3373
  }
3075
3374
  setupRoutes() {
3076
3375
  this.bot.on("message:text", async (ctx) => {
@@ -3093,7 +3392,7 @@ Workspace: <code>${workspace}</code>
3093
3392
  ctx.replyWithChatAction("typing").catch(() => {
3094
3393
  });
3095
3394
  handleAssistantMessage(this.assistantSession, ctx.message.text).catch(
3096
- (err) => log10.error({ err }, "Assistant error")
3395
+ (err) => log11.error({ err }, "Assistant error")
3097
3396
  );
3098
3397
  return;
3099
3398
  }
@@ -3110,7 +3409,7 @@ Workspace: <code>${workspace}</code>
3110
3409
  threadId: String(threadId),
3111
3410
  userId: String(ctx.from.id),
3112
3411
  text: ctx.message.text
3113
- }).catch((err) => log10.error({ err }, "handleMessage error"));
3412
+ }).catch((err) => log11.error({ err }, "handleMessage error"));
3114
3413
  });
3115
3414
  }
3116
3415
  // --- ChannelAdapter implementations ---
@@ -3190,7 +3489,7 @@ Workspace: <code>${workspace}</code>
3190
3489
  if (toolState) {
3191
3490
  if (meta.viewerLinks) {
3192
3491
  toolState.viewerLinks = meta.viewerLinks;
3193
- log10.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
3492
+ log11.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
3194
3493
  }
3195
3494
  const viewerFilePath = content.metadata?.viewerFilePath;
3196
3495
  if (viewerFilePath) toolState.viewerFilePath = viewerFilePath;
@@ -3199,7 +3498,7 @@ Workspace: <code>${workspace}</code>
3199
3498
  const isTerminal = meta.status === "completed" || meta.status === "failed";
3200
3499
  if (!isTerminal && !meta.viewerLinks) break;
3201
3500
  await toolState.ready;
3202
- log10.debug(
3501
+ log11.debug(
3203
3502
  { toolId: meta.id, status: meta.status, hasViewerLinks: !!toolState.viewerLinks, viewerLinks: toolState.viewerLinks, name: toolState.name, msgId: toolState.msgId },
3204
3503
  "Tool completed, preparing edit"
3205
3504
  );
@@ -3221,7 +3520,7 @@ Workspace: <code>${workspace}</code>
3221
3520
  )
3222
3521
  );
3223
3522
  } catch (err) {
3224
- log10.warn(
3523
+ log11.warn(
3225
3524
  { err, msgId: toolState.msgId, textLen: formattedText.length, hasViewerLinks: !!merged.viewerLinks },
3226
3525
  "Tool update edit failed"
3227
3526
  );
@@ -3316,17 +3615,16 @@ Task completed.
3316
3615
  }
3317
3616
  }
3318
3617
  async sendPermissionRequest(sessionId, request) {
3319
- log10.info({ sessionId, requestId: request.id }, "Permission request sent");
3618
+ log11.info({ sessionId, requestId: request.id }, "Permission request sent");
3320
3619
  const session = this.core.sessionManager.getSession(
3321
3620
  sessionId
3322
3621
  );
3323
3622
  if (!session) return;
3324
3623
  if (session.dangerousMode) {
3325
3624
  const allowOption = request.options.find((o) => o.isAllow);
3326
- if (allowOption && session.pendingPermission?.requestId === request.id) {
3327
- log10.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
3328
- session.pendingPermission.resolve(allowOption.id);
3329
- session.pendingPermission = void 0;
3625
+ if (allowOption && session.permissionGate.requestId === request.id) {
3626
+ log11.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
3627
+ session.permissionGate.resolve(allowOption.id);
3330
3628
  }
3331
3629
  return;
3332
3630
  }
@@ -3336,7 +3634,7 @@ Task completed.
3336
3634
  }
3337
3635
  async sendNotification(notification) {
3338
3636
  if (notification.sessionId === this.assistantSession?.id) return;
3339
- log10.info(
3637
+ log11.info(
3340
3638
  { sessionId: notification.sessionId, type: notification.type },
3341
3639
  "Notification sent"
3342
3640
  );
@@ -3372,7 +3670,7 @@ Task completed.
3372
3670
  );
3373
3671
  }
3374
3672
  async createSessionThread(sessionId, name) {
3375
- log10.info({ sessionId, name }, "Session topic created");
3673
+ log11.info({ sessionId, name }, "Session topic created");
3376
3674
  return String(
3377
3675
  await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
3378
3676
  );
@@ -3457,7 +3755,7 @@ Task completed.
3457
3755
  }
3458
3756
  );
3459
3757
  } catch (err) {
3460
- log10.error({ err, sessionId }, "Failed to send skill commands");
3758
+ log11.error({ err, sessionId }, "Failed to send skill commands");
3461
3759
  }
3462
3760
  await this.updateCommandAutocomplete(session.agentName, commands);
3463
3761
  }
@@ -3493,12 +3791,12 @@ Task completed.
3493
3791
  await this.bot.api.setMyCommands(all, {
3494
3792
  scope: { type: "chat", chat_id: this.telegramConfig.chatId }
3495
3793
  });
3496
- log10.info(
3794
+ log11.info(
3497
3795
  { count: all.length, skills: validSkills.length },
3498
3796
  "Updated command autocomplete"
3499
3797
  );
3500
3798
  } catch (err) {
3501
- log10.error(
3799
+ log11.error(
3502
3800
  { err, commands: all },
3503
3801
  "Failed to update command autocomplete"
3504
3802
  );
@@ -3539,12 +3837,16 @@ export {
3539
3837
  StderrCapture,
3540
3838
  AgentInstance,
3541
3839
  AgentManager,
3840
+ TypedEmitter,
3841
+ PromptQueue,
3842
+ PermissionGate,
3542
3843
  Session,
3543
3844
  SessionManager,
3544
3845
  NotificationManager,
3846
+ MessageTransformer,
3545
3847
  OpenACPCore,
3546
3848
  ChannelAdapter,
3547
3849
  ApiServer,
3548
3850
  TelegramAdapter
3549
3851
  };
3550
- //# sourceMappingURL=chunk-SWQRUVBW.js.map
3852
+ //# sourceMappingURL=chunk-YD7ILGA6.js.map