@openacp/cli 0.4.6 → 0.4.9

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 (35) hide show
  1. package/README.md +8 -8
  2. package/dist/{chunk-2M4O7AFI.js → chunk-45DFYWJT.js} +1172 -119
  3. package/dist/chunk-45DFYWJT.js.map +1 -0
  4. package/dist/{chunk-IUPHAXGA.js → chunk-6MJLVZXV.js} +3 -3
  5. package/dist/{chunk-3QACY5E3.js → chunk-C6YIUTGR.js} +2 -2
  6. package/dist/{chunk-2SY7Y2VB.js → chunk-HZD3CGPK.js} +2 -2
  7. package/dist/{chunk-ARWC4S35.js → chunk-UAUTLC4E.js} +13 -7
  8. package/dist/{chunk-ARWC4S35.js.map → chunk-UAUTLC4E.js.map} +1 -1
  9. package/dist/{chunk-WF5XDN4D.js → chunk-ZRFBLD3W.js} +6 -2
  10. package/dist/chunk-ZRFBLD3W.js.map +1 -0
  11. package/dist/cli.js +37 -31
  12. package/dist/cli.js.map +1 -1
  13. package/dist/{config-J5YQOMDU.js → config-H2DSEHNW.js} +2 -2
  14. package/dist/config-editor-SKS4LJLT.js +11 -0
  15. package/dist/{daemon-SLGQGRKO.js → daemon-VF6HJQXD.js} +3 -3
  16. package/dist/index.d.ts +18 -0
  17. package/dist/index.js +6 -6
  18. package/dist/integrate-WUPLRJD3.js +145 -0
  19. package/dist/integrate-WUPLRJD3.js.map +1 -0
  20. package/dist/{main-3POGUQPY.js → main-7T5YHFHO.js} +11 -11
  21. package/dist/{setup-CEDO6VWV.js → setup-FCVL75K6.js} +3 -3
  22. package/package.json +1 -1
  23. package/dist/chunk-2M4O7AFI.js.map +0 -1
  24. package/dist/chunk-WF5XDN4D.js.map +0 -1
  25. package/dist/config-editor-EPOKAEP6.js +0 -11
  26. package/dist/integrate-HYDSHAF3.js +0 -123
  27. package/dist/integrate-HYDSHAF3.js.map +0 -1
  28. /package/dist/{chunk-IUPHAXGA.js.map → chunk-6MJLVZXV.js.map} +0 -0
  29. /package/dist/{chunk-3QACY5E3.js.map → chunk-C6YIUTGR.js.map} +0 -0
  30. /package/dist/{chunk-2SY7Y2VB.js.map → chunk-HZD3CGPK.js.map} +0 -0
  31. /package/dist/{config-J5YQOMDU.js.map → config-H2DSEHNW.js.map} +0 -0
  32. /package/dist/{config-editor-EPOKAEP6.js.map → config-editor-SKS4LJLT.js.map} +0 -0
  33. /package/dist/{daemon-SLGQGRKO.js.map → daemon-VF6HJQXD.js.map} +0 -0
  34. /package/dist/{main-3POGUQPY.js.map → main-7T5YHFHO.js.map} +0 -0
  35. /package/dist/{setup-CEDO6VWV.js.map → setup-FCVL75K6.js.map} +0 -0
@@ -830,6 +830,7 @@ var SessionManager = class {
830
830
  createdAt: session.createdAt.toISOString(),
831
831
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
832
832
  name: session.name,
833
+ dangerousMode: false,
833
834
  platform: {}
834
835
  });
835
836
  }
@@ -2016,7 +2017,7 @@ var ApiServer = class {
2016
2017
  return;
2017
2018
  }
2018
2019
  target[lastKey] = value;
2019
- const { ConfigSchema } = await import("./config-J5YQOMDU.js");
2020
+ const { ConfigSchema } = await import("./config-H2DSEHNW.js");
2020
2021
  const result = ConfigSchema.safeParse(cloned);
2021
2022
  if (!result.success) {
2022
2023
  this.sendJson(res, 400, {
@@ -2418,7 +2419,7 @@ function formatUsage(usage) {
2418
2419
  return `${emoji} ${formatTokens(tokensUsed)} / ${formatTokens(contextSize)} tokens
2419
2420
  ${bar} ${pct}%`;
2420
2421
  }
2421
- function splitMessage(text, maxLength = 4096) {
2422
+ function splitMessage(text, maxLength = 3800) {
2422
2423
  if (text.length <= maxLength) return [text];
2423
2424
  const chunks = [];
2424
2425
  let remaining = text;
@@ -2428,14 +2429,23 @@ function splitMessage(text, maxLength = 4096) {
2428
2429
  break;
2429
2430
  }
2430
2431
  let splitAt = remaining.lastIndexOf("\n\n", maxLength);
2431
- if (splitAt === -1 || splitAt < maxLength * 0.5) {
2432
+ if (splitAt === -1 || splitAt < maxLength * 0.2) {
2432
2433
  splitAt = remaining.lastIndexOf("\n", maxLength);
2433
2434
  }
2434
- if (splitAt === -1 || splitAt < maxLength * 0.5) {
2435
+ if (splitAt === -1 || splitAt < maxLength * 0.2) {
2435
2436
  splitAt = maxLength;
2436
2437
  }
2438
+ const candidate = remaining.slice(0, splitAt);
2439
+ const fences = candidate.match(/```/g);
2440
+ if (fences && fences.length % 2 !== 0) {
2441
+ const closingFence = remaining.indexOf("```", splitAt);
2442
+ if (closingFence !== -1) {
2443
+ const afterFence = remaining.indexOf("\n", closingFence + 3);
2444
+ splitAt = afterFence !== -1 ? afterFence + 1 : closingFence + 3;
2445
+ }
2446
+ }
2437
2447
  chunks.push(remaining.slice(0, splitAt));
2438
- remaining = remaining.slice(splitAt).trimStart();
2448
+ remaining = remaining.slice(splitAt).replace(/^\n+/, "");
2439
2449
  }
2440
2450
  return chunks;
2441
2451
  }
@@ -2472,14 +2482,22 @@ var MessageDraft = class {
2472
2482
  async flush() {
2473
2483
  if (!this.buffer) return;
2474
2484
  if (this.firstFlushPending) return;
2475
- const html = markdownToTelegramHtml(this.buffer);
2476
- const truncated = html.length > 4096 ? html.slice(0, 4090) + "\n..." : html;
2477
- if (!truncated) return;
2485
+ let displayBuffer = this.buffer;
2486
+ if (displayBuffer.length > 3800) {
2487
+ let cutAt = displayBuffer.lastIndexOf("\n", 3800);
2488
+ if (cutAt < 800) cutAt = 3800;
2489
+ displayBuffer = displayBuffer.slice(0, cutAt) + "\n\u2026";
2490
+ }
2491
+ let html = markdownToTelegramHtml(displayBuffer);
2492
+ if (!html) return;
2493
+ if (html.length > 4096) {
2494
+ html = html.slice(0, 4090) + "\n\u2026";
2495
+ }
2478
2496
  if (!this.messageId) {
2479
2497
  this.firstFlushPending = true;
2480
2498
  try {
2481
2499
  const result = await this.sendQueue.enqueue(
2482
- () => this.bot.api.sendMessage(this.chatId, truncated, {
2500
+ () => this.bot.api.sendMessage(this.chatId, html, {
2483
2501
  message_thread_id: this.threadId,
2484
2502
  parse_mode: "HTML",
2485
2503
  disable_notification: true
@@ -2497,7 +2515,7 @@ var MessageDraft = class {
2497
2515
  } else {
2498
2516
  try {
2499
2517
  await this.sendQueue.enqueue(
2500
- () => this.bot.api.editMessageText(this.chatId, this.messageId, truncated, {
2518
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
2501
2519
  parse_mode: "HTML"
2502
2520
  }),
2503
2521
  { type: "text", key: this.sessionId }
@@ -2517,21 +2535,20 @@ var MessageDraft = class {
2517
2535
  if (this.messageId && this.buffer === this.lastSentBuffer) {
2518
2536
  return this.messageId;
2519
2537
  }
2520
- const html = markdownToTelegramHtml(this.buffer);
2521
- const chunks = splitMessage(html);
2522
- try {
2523
- for (let i = 0; i < chunks.length; i++) {
2524
- const chunk = chunks[i];
2538
+ const mdChunks = splitMessage(this.buffer);
2539
+ for (let i = 0; i < mdChunks.length; i++) {
2540
+ const html = markdownToTelegramHtml(mdChunks[i]);
2541
+ try {
2525
2542
  if (i === 0 && this.messageId) {
2526
2543
  await this.sendQueue.enqueue(
2527
- () => this.bot.api.editMessageText(this.chatId, this.messageId, chunk, {
2544
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
2528
2545
  parse_mode: "HTML"
2529
2546
  }),
2530
2547
  { type: "other" }
2531
2548
  );
2532
2549
  } else {
2533
2550
  const msg = await this.sendQueue.enqueue(
2534
- () => this.bot.api.sendMessage(this.chatId, chunk, {
2551
+ () => this.bot.api.sendMessage(this.chatId, html, {
2535
2552
  message_thread_id: this.threadId,
2536
2553
  parse_mode: "HTML",
2537
2554
  disable_notification: true
@@ -2542,17 +2559,25 @@ var MessageDraft = class {
2542
2559
  this.messageId = msg.message_id;
2543
2560
  }
2544
2561
  }
2545
- }
2546
- } catch {
2547
- if (this.buffer !== this.lastSentBuffer) {
2562
+ } catch {
2548
2563
  try {
2549
- await this.sendQueue.enqueue(
2550
- () => this.bot.api.sendMessage(this.chatId, this.buffer.slice(0, 4096), {
2551
- message_thread_id: this.threadId,
2552
- disable_notification: true
2553
- }),
2554
- { type: "other" }
2555
- );
2564
+ if (i === 0 && this.messageId) {
2565
+ await this.sendQueue.enqueue(
2566
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, mdChunks[i].slice(0, 4096)),
2567
+ { type: "other" }
2568
+ );
2569
+ } else {
2570
+ const msg = await this.sendQueue.enqueue(
2571
+ () => this.bot.api.sendMessage(this.chatId, mdChunks[i].slice(0, 4096), {
2572
+ message_thread_id: this.threadId,
2573
+ disable_notification: true
2574
+ }),
2575
+ { type: "other" }
2576
+ );
2577
+ if (msg) {
2578
+ this.messageId = msg.message_id;
2579
+ }
2580
+ }
2556
2581
  } catch {
2557
2582
  }
2558
2583
  }
@@ -2598,11 +2623,21 @@ function buildDeepLink(chatId, messageId) {
2598
2623
  // src/adapters/telegram/commands.ts
2599
2624
  import { InlineKeyboard } from "grammy";
2600
2625
  var log8 = createChildLogger({ module: "telegram-commands" });
2626
+ var pendingNewSessions = /* @__PURE__ */ new Map();
2627
+ var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
2628
+ function cleanupPending(userId) {
2629
+ const pending = pendingNewSessions.get(userId);
2630
+ if (pending) {
2631
+ clearTimeout(pending.timer);
2632
+ pendingNewSessions.delete(userId);
2633
+ }
2634
+ }
2601
2635
  function setupCommands(bot, core, chatId, assistant) {
2602
2636
  bot.command("new", (ctx) => handleNew(ctx, core, chatId, assistant));
2603
2637
  bot.command("newchat", (ctx) => handleNewChat(ctx, core, chatId));
2604
2638
  bot.command("cancel", (ctx) => handleCancel(ctx, core, assistant));
2605
2639
  bot.command("status", (ctx) => handleStatus(ctx, core));
2640
+ bot.command("sessions", (ctx) => handleTopics(ctx, core));
2606
2641
  bot.command("agents", (ctx) => handleAgents(ctx, core));
2607
2642
  bot.command("help", (ctx) => handleHelp(ctx));
2608
2643
  bot.command("menu", (ctx) => handleMenu(ctx));
@@ -2610,27 +2645,96 @@ function setupCommands(bot, core, chatId, assistant) {
2610
2645
  bot.command("disable_dangerous", (ctx) => handleDisableDangerous(ctx, core));
2611
2646
  bot.command("restart", (ctx) => handleRestart(ctx, core));
2612
2647
  bot.command("update", (ctx) => handleUpdate(ctx, core));
2648
+ bot.command("integrate", (ctx) => handleIntegrate(ctx, core));
2649
+ bot.command("clear", (ctx) => handleClear(ctx, assistant));
2613
2650
  }
2614
2651
  function buildMenuKeyboard() {
2615
- return new InlineKeyboard().text("\u{1F195} New Session", "m:new").text("\u{1F4AC} New Chat", "m:newchat").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");
2652
+ return new InlineKeyboard().text("\u{1F195} New Session", "m:new").text("\u{1F4CB} Sessions", "m:topics").row().text("\u{1F4CA} Status", "m:status").text("\u{1F916} Agents", "m:agents").row().text("\u{1F517} Integrate", "m:integrate").text("\u2753 Help", "m:help").row().text("\u{1F504} Restart", "m:restart").text("\u2B06\uFE0F Update", "m:update");
2616
2653
  }
2617
- function setupMenuCallbacks(bot, core, chatId) {
2654
+ function setupMenuCallbacks(bot, core, chatId, systemTopicIds) {
2618
2655
  bot.callbackQuery(/^m:/, async (ctx) => {
2619
2656
  const data = ctx.callbackQuery.data;
2620
2657
  try {
2621
2658
  await ctx.answerCallbackQuery();
2622
2659
  } catch {
2623
2660
  }
2661
+ if (data.startsWith("m:new:agent:")) {
2662
+ const agentName = data.replace("m:new:agent:", "");
2663
+ const userId = ctx.from?.id;
2664
+ if (userId) await startWorkspaceStep(ctx, core, chatId, userId, agentName);
2665
+ return;
2666
+ }
2667
+ if (data === "m:new:ws:default") {
2668
+ const userId = ctx.from?.id;
2669
+ if (!userId) return;
2670
+ const pending = pendingNewSessions.get(userId);
2671
+ if (!pending?.agentName) return;
2672
+ const workspace = core.configManager.get().workspace.baseDir;
2673
+ await startConfirmStep(ctx, chatId, userId, pending.agentName, workspace);
2674
+ return;
2675
+ }
2676
+ if (data === "m:new:ws:custom") {
2677
+ const userId = ctx.from?.id;
2678
+ if (!userId) return;
2679
+ const pending = pendingNewSessions.get(userId);
2680
+ if (!pending?.agentName) return;
2681
+ try {
2682
+ await ctx.api.editMessageText(
2683
+ chatId,
2684
+ pending.messageId,
2685
+ `\u270F\uFE0F <b>Enter your project path:</b>
2686
+
2687
+ Full path like <code>~/code/my-project</code>
2688
+ Or just the folder name like <code>my-project</code> (will use ${core.configManager.get().workspace.baseDir}/)`,
2689
+ { parse_mode: "HTML" }
2690
+ );
2691
+ } catch {
2692
+ await ctx.reply(
2693
+ `\u270F\uFE0F <b>Enter your project path:</b>`,
2694
+ { parse_mode: "HTML" }
2695
+ );
2696
+ }
2697
+ clearTimeout(pending.timer);
2698
+ pending.step = "workspace_input";
2699
+ pending.timer = setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS);
2700
+ return;
2701
+ }
2702
+ if (data === "m:new:confirm") {
2703
+ const userId = ctx.from?.id;
2704
+ if (!userId) return;
2705
+ const pending = pendingNewSessions.get(userId);
2706
+ if (!pending?.agentName || !pending?.workspace) return;
2707
+ cleanupPending(userId);
2708
+ const confirmMsgId = pending.messageId;
2709
+ try {
2710
+ await ctx.api.editMessageText(chatId, confirmMsgId, `\u23F3 Creating session...`, { parse_mode: "HTML" });
2711
+ } catch {
2712
+ }
2713
+ const resultThreadId = await createSessionDirect(ctx, core, chatId, pending.agentName, pending.workspace);
2714
+ try {
2715
+ if (resultThreadId) {
2716
+ const link = buildDeepLink(chatId, resultThreadId);
2717
+ await ctx.api.editMessageText(chatId, confirmMsgId, `\u2705 Session created \u2192 <a href="${link}">Open topic</a>`, { parse_mode: "HTML" });
2718
+ } else {
2719
+ await ctx.api.editMessageText(chatId, confirmMsgId, `\u274C Session creation failed.`, { parse_mode: "HTML" });
2720
+ }
2721
+ } catch {
2722
+ }
2723
+ return;
2724
+ }
2725
+ if (data === "m:new:cancel") {
2726
+ const userId = ctx.from?.id;
2727
+ if (userId) cleanupPending(userId);
2728
+ try {
2729
+ await ctx.editMessageText("\u274C Session creation cancelled.", { parse_mode: "HTML" });
2730
+ } catch {
2731
+ }
2732
+ return;
2733
+ }
2624
2734
  switch (data) {
2625
2735
  case "m:new":
2626
2736
  await handleNew(ctx, core, chatId);
2627
2737
  break;
2628
- case "m:newchat":
2629
- await handleNewChat(ctx, core, chatId);
2630
- break;
2631
- case "m:cancel":
2632
- await handleCancel(ctx, core);
2633
- break;
2634
2738
  case "m:status":
2635
2739
  await handleStatus(ctx, core);
2636
2740
  break;
@@ -2646,6 +2750,27 @@ function setupMenuCallbacks(bot, core, chatId) {
2646
2750
  case "m:update":
2647
2751
  await handleUpdate(ctx, core);
2648
2752
  break;
2753
+ case "m:integrate":
2754
+ await handleIntegrate(ctx, core);
2755
+ break;
2756
+ case "m:topics":
2757
+ await handleTopics(ctx, core);
2758
+ break;
2759
+ case "m:cleanup:finished":
2760
+ await handleCleanup(ctx, core, chatId, ["finished"]);
2761
+ break;
2762
+ case "m:cleanup:errors":
2763
+ await handleCleanup(ctx, core, chatId, ["error", "cancelled"]);
2764
+ break;
2765
+ case "m:cleanup:all":
2766
+ await handleCleanup(ctx, core, chatId, ["finished", "error", "cancelled"]);
2767
+ break;
2768
+ case "m:cleanup:everything":
2769
+ await handleCleanupEverything(ctx, core, chatId, systemTopicIds);
2770
+ break;
2771
+ case "m:cleanup:everything:confirm":
2772
+ await handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicIds);
2773
+ break;
2649
2774
  }
2650
2775
  });
2651
2776
  }
@@ -2662,16 +2787,111 @@ async function handleNew(ctx, core, chatId, assistant) {
2662
2787
  const args = matchStr.split(" ").filter(Boolean);
2663
2788
  const agentName = args[0];
2664
2789
  const workspace = args[1];
2790
+ if (agentName && workspace) {
2791
+ await createSessionDirect(ctx, core, chatId, agentName, workspace);
2792
+ return;
2793
+ }
2665
2794
  const currentThreadId = ctx.message?.message_thread_id;
2666
- if (assistant && currentThreadId === assistant.topicId && (!agentName || !workspace)) {
2795
+ if (assistant && currentThreadId === assistant.topicId) {
2667
2796
  const assistantSession = assistant.getSession();
2668
2797
  if (assistantSession) {
2669
- const prompt = agentName ? `User wants to create a new session with agent "${agentName}" but didn't specify a workspace. Ask them which workspace to use.` : `User wants to create a new session. Ask them which agent and workspace to use.`;
2798
+ const prompt = agentName ? `User wants to create a new session with agent "${agentName}" but didn't specify a workspace. Ask them which project directory to use as workspace.` : `User wants to create a new session. Guide them through choosing an agent and workspace (project directory).`;
2670
2799
  await assistantSession.enqueuePrompt(prompt);
2671
2800
  return;
2672
2801
  }
2673
2802
  }
2674
- log8.info({ userId: ctx.from?.id, agentName }, "New session command");
2803
+ const userId = ctx.from?.id;
2804
+ if (!userId) return;
2805
+ const agents = core.agentManager.getAvailableAgents();
2806
+ const config = core.configManager.get();
2807
+ if (agentName || agents.length === 1) {
2808
+ const selectedAgent = agentName || config.defaultAgent;
2809
+ await startWorkspaceStep(ctx, core, chatId, userId, selectedAgent);
2810
+ return;
2811
+ }
2812
+ const keyboard = new InlineKeyboard();
2813
+ for (const agent of agents) {
2814
+ const label = agent.name === config.defaultAgent ? `${agent.name} (default)` : agent.name;
2815
+ keyboard.text(label, `m:new:agent:${agent.name}`).row();
2816
+ }
2817
+ const msg = await ctx.reply(
2818
+ `\u{1F916} <b>Choose an agent:</b>`,
2819
+ { parse_mode: "HTML", reply_markup: keyboard }
2820
+ );
2821
+ cleanupPending(userId);
2822
+ pendingNewSessions.set(userId, {
2823
+ step: "agent",
2824
+ messageId: msg.message_id,
2825
+ threadId: currentThreadId,
2826
+ timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
2827
+ });
2828
+ }
2829
+ async function startWorkspaceStep(ctx, core, chatId, userId, agentName) {
2830
+ const config = core.configManager.get();
2831
+ const baseDir = config.workspace.baseDir;
2832
+ const keyboard = new InlineKeyboard().text(`\u{1F4C1} Use ${baseDir}`, "m:new:ws:default").row().text("\u270F\uFE0F Enter project path", "m:new:ws:custom");
2833
+ const text = `\u{1F4C1} <b>Where should ${escapeHtml(agentName)} work?</b>
2834
+
2835
+ Enter the path to your project folder \u2014 the agent will read, write, and run code there.
2836
+
2837
+ Or use the default directory below:`;
2838
+ let msg;
2839
+ try {
2840
+ const pending = pendingNewSessions.get(userId);
2841
+ if (pending?.messageId) {
2842
+ await ctx.api.editMessageText(chatId, pending.messageId, text, {
2843
+ parse_mode: "HTML",
2844
+ reply_markup: keyboard
2845
+ });
2846
+ msg = { message_id: pending.messageId };
2847
+ } else {
2848
+ msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
2849
+ }
2850
+ } catch {
2851
+ msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
2852
+ }
2853
+ cleanupPending(userId);
2854
+ pendingNewSessions.set(userId, {
2855
+ agentName,
2856
+ step: "workspace",
2857
+ messageId: msg.message_id,
2858
+ threadId: ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id,
2859
+ timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
2860
+ });
2861
+ }
2862
+ async function startConfirmStep(ctx, chatId, userId, agentName, workspace) {
2863
+ const keyboard = new InlineKeyboard().text("\u2705 Create", "m:new:confirm").text("\u274C Cancel", "m:new:cancel");
2864
+ const text = `\u2705 <b>Ready to create session?</b>
2865
+
2866
+ <b>Agent:</b> ${escapeHtml(agentName)}
2867
+ <b>Project:</b> <code>${escapeHtml(workspace)}</code>`;
2868
+ let msg;
2869
+ try {
2870
+ const pending = pendingNewSessions.get(userId);
2871
+ if (pending?.messageId) {
2872
+ await ctx.api.editMessageText(chatId, pending.messageId, text, {
2873
+ parse_mode: "HTML",
2874
+ reply_markup: keyboard
2875
+ });
2876
+ msg = { message_id: pending.messageId };
2877
+ } else {
2878
+ msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
2879
+ }
2880
+ } catch {
2881
+ msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
2882
+ }
2883
+ cleanupPending(userId);
2884
+ pendingNewSessions.set(userId, {
2885
+ agentName,
2886
+ workspace,
2887
+ step: "confirm",
2888
+ messageId: msg.message_id,
2889
+ threadId: ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id,
2890
+ timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
2891
+ });
2892
+ }
2893
+ async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
2894
+ log8.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
2675
2895
  let threadId;
2676
2896
  try {
2677
2897
  const topicName = `\u{1F504} New Session`;
@@ -2680,15 +2900,9 @@ async function handleNew(ctx, core, chatId, assistant) {
2680
2900
  message_thread_id: threadId,
2681
2901
  parse_mode: "HTML"
2682
2902
  });
2683
- const session = await core.handleNewSession(
2684
- "telegram",
2685
- agentName,
2686
- workspace
2687
- );
2903
+ const session = await core.handleNewSession("telegram", agentName, workspace);
2688
2904
  session.threadId = String(threadId);
2689
- await core.sessionManager.updateSessionPlatform(session.id, {
2690
- topicId: threadId
2691
- });
2905
+ await core.sessionManager.updateSessionPlatform(session.id, { topicId: threadId });
2692
2906
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
2693
2907
  try {
2694
2908
  await ctx.api.editForumTopic(chatId, threadId, { name: finalName });
@@ -2696,9 +2910,11 @@ async function handleNew(ctx, core, chatId, assistant) {
2696
2910
  }
2697
2911
  await ctx.api.sendMessage(
2698
2912
  chatId,
2699
- `\u2705 Session started
2913
+ `\u2705 <b>Session started</b>
2700
2914
  <b>Agent:</b> ${escapeHtml(session.agentName)}
2701
- <b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>`,
2915
+ <b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>
2916
+
2917
+ This is your coding session \u2014 chat here to work with the agent.`,
2702
2918
  {
2703
2919
  message_thread_id: threadId,
2704
2920
  parse_mode: "HTML",
@@ -2706,6 +2922,7 @@ async function handleNew(ctx, core, chatId, assistant) {
2706
2922
  }
2707
2923
  );
2708
2924
  session.warmup().catch((err) => log8.error({ err }, "Warm-up error"));
2925
+ return threadId ?? null;
2709
2926
  } catch (err) {
2710
2927
  log8.error({ err }, "Session creation failed");
2711
2928
  if (threadId) {
@@ -2716,6 +2933,7 @@ async function handleNew(ctx, core, chatId, assistant) {
2716
2933
  }
2717
2934
  const message = err instanceof Error ? err.message : typeof err === "object" ? JSON.stringify(err) : String(err);
2718
2935
  await ctx.reply(`\u274C ${escapeHtml(message)}`, { parse_mode: "HTML" });
2936
+ return null;
2719
2937
  }
2720
2938
  }
2721
2939
  async function handleNewChat(ctx, core, chatId) {
@@ -2870,6 +3088,188 @@ Total sessions: ${sessions.length}`,
2870
3088
  );
2871
3089
  }
2872
3090
  }
3091
+ async function handleTopics(ctx, core) {
3092
+ try {
3093
+ const allRecords = core.sessionManager.listRecords();
3094
+ const records = allRecords.filter((r) => {
3095
+ const platform = r.platform;
3096
+ return !!platform?.topicId;
3097
+ });
3098
+ const headlessCount = allRecords.length - records.length;
3099
+ if (records.length === 0) {
3100
+ const extra = headlessCount > 0 ? ` (${headlessCount} headless hidden)` : "";
3101
+ await ctx.reply(`No sessions with topics found.${extra}`, { parse_mode: "HTML" });
3102
+ return;
3103
+ }
3104
+ const statusEmoji = {
3105
+ active: "\u{1F7E2}",
3106
+ initializing: "\u{1F7E1}",
3107
+ finished: "\u2705",
3108
+ error: "\u274C",
3109
+ cancelled: "\u26D4"
3110
+ };
3111
+ const statusOrder = { active: 0, initializing: 1, error: 2, finished: 3, cancelled: 4 };
3112
+ records.sort((a, b) => (statusOrder[a.status] ?? 5) - (statusOrder[b.status] ?? 5));
3113
+ const MAX_DISPLAY = 30;
3114
+ const displayed = records.slice(0, MAX_DISPLAY);
3115
+ const lines = displayed.map((r) => {
3116
+ const emoji = statusEmoji[r.status] || "\u26AA";
3117
+ const name = r.name?.trim();
3118
+ const label = name ? escapeHtml(name) : `<i>${escapeHtml(r.agentName)} session</i>`;
3119
+ return `${emoji} ${label} <code>[${r.status}]</code>`;
3120
+ });
3121
+ const header = `<b>Sessions: ${records.length}</b>` + (headlessCount > 0 ? ` (${headlessCount} headless hidden)` : "");
3122
+ const truncated = records.length > MAX_DISPLAY ? `
3123
+
3124
+ <i>...and ${records.length - MAX_DISPLAY} more</i>` : "";
3125
+ const finishedCount = records.filter((r) => r.status === "finished").length;
3126
+ const errorCount = records.filter((r) => r.status === "error" || r.status === "cancelled").length;
3127
+ const activeCount = records.filter((r) => r.status === "active" || r.status === "initializing").length;
3128
+ const keyboard = new InlineKeyboard();
3129
+ if (finishedCount > 0) {
3130
+ keyboard.text(`Cleanup finished (${finishedCount})`, "m:cleanup:finished").row();
3131
+ }
3132
+ if (errorCount > 0) {
3133
+ keyboard.text(`Cleanup errors (${errorCount})`, "m:cleanup:errors").row();
3134
+ }
3135
+ if (finishedCount + errorCount > 0) {
3136
+ keyboard.text(`Cleanup all non-active (${finishedCount + errorCount})`, "m:cleanup:all").row();
3137
+ }
3138
+ keyboard.text(`\u26A0\uFE0F Cleanup ALL (${records.length})`, "m:cleanup:everything").row();
3139
+ keyboard.text("Refresh", "m:topics");
3140
+ await ctx.reply(
3141
+ `${header}
3142
+
3143
+ ${lines.join("\n")}${truncated}`,
3144
+ { parse_mode: "HTML", reply_markup: keyboard }
3145
+ );
3146
+ } catch (err) {
3147
+ log8.error({ err }, "handleTopics error");
3148
+ await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
3149
+ });
3150
+ }
3151
+ }
3152
+ async function handleCleanup(ctx, core, chatId, statuses) {
3153
+ const allRecords = core.sessionManager.listRecords();
3154
+ const cleanable = allRecords.filter((r) => {
3155
+ const platform = r.platform;
3156
+ return !!platform?.topicId && statuses.includes(r.status);
3157
+ });
3158
+ if (cleanable.length === 0) {
3159
+ await ctx.reply("Nothing to clean up.", { parse_mode: "HTML" });
3160
+ return;
3161
+ }
3162
+ let deleted = 0;
3163
+ let failed = 0;
3164
+ for (const record of cleanable) {
3165
+ try {
3166
+ const topicId = record.platform?.topicId;
3167
+ if (topicId) {
3168
+ try {
3169
+ await ctx.api.deleteForumTopic(chatId, topicId);
3170
+ } catch (err) {
3171
+ log8.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3172
+ }
3173
+ }
3174
+ await core.sessionManager.removeRecord(record.sessionId);
3175
+ deleted++;
3176
+ } catch (err) {
3177
+ log8.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3178
+ failed++;
3179
+ }
3180
+ }
3181
+ await ctx.reply(
3182
+ `\u{1F5D1} Cleaned up <b>${deleted}</b> sessions${failed > 0 ? ` (${failed} failed)` : ""}.`,
3183
+ { parse_mode: "HTML" }
3184
+ );
3185
+ }
3186
+ async function handleCleanupEverything(ctx, core, chatId, systemTopicIds) {
3187
+ const allRecords = core.sessionManager.listRecords();
3188
+ const cleanable = allRecords.filter((r) => {
3189
+ const platform = r.platform;
3190
+ if (!platform?.topicId) return false;
3191
+ if (systemTopicIds && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
3192
+ return true;
3193
+ });
3194
+ if (cleanable.length === 0) {
3195
+ await ctx.reply("Nothing to clean up.", { parse_mode: "HTML" });
3196
+ return;
3197
+ }
3198
+ const statusCounts = /* @__PURE__ */ new Map();
3199
+ for (const r of cleanable) {
3200
+ statusCounts.set(r.status, (statusCounts.get(r.status) ?? 0) + 1);
3201
+ }
3202
+ const statusEmoji = {
3203
+ active: "\u{1F7E2}",
3204
+ initializing: "\u{1F7E1}",
3205
+ finished: "\u2705",
3206
+ error: "\u274C",
3207
+ cancelled: "\u26D4"
3208
+ };
3209
+ const breakdown = Array.from(statusCounts.entries()).map(([status, count]) => `${statusEmoji[status] ?? "\u26AA"} ${status}: ${count}`).join("\n");
3210
+ const activeCount = (statusCounts.get("active") ?? 0) + (statusCounts.get("initializing") ?? 0);
3211
+ const activeWarning = activeCount > 0 ? `
3212
+
3213
+ \u26A0\uFE0F <b>${activeCount} active session(s) will be cancelled and their agents stopped!</b>` : "";
3214
+ const keyboard = new InlineKeyboard().text("Yes, delete all", "m:cleanup:everything:confirm").text("Cancel", "m:topics");
3215
+ await ctx.reply(
3216
+ `<b>Delete ${cleanable.length} topics?</b>
3217
+
3218
+ This will:
3219
+ \u2022 Delete all session topics from this group
3220
+ \u2022 Cancel any running agent sessions
3221
+ \u2022 Remove all session records
3222
+
3223
+ <b>Breakdown:</b>
3224
+ ${breakdown}${activeWarning}
3225
+
3226
+ <i>Notifications and Assistant topics will NOT be deleted.</i>`,
3227
+ { parse_mode: "HTML", reply_markup: keyboard }
3228
+ );
3229
+ }
3230
+ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicIds) {
3231
+ const allRecords = core.sessionManager.listRecords();
3232
+ const cleanable = allRecords.filter((r) => {
3233
+ const platform = r.platform;
3234
+ if (!platform?.topicId) return false;
3235
+ if (systemTopicIds && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
3236
+ return true;
3237
+ });
3238
+ if (cleanable.length === 0) {
3239
+ await ctx.reply("Nothing to clean up.", { parse_mode: "HTML" });
3240
+ return;
3241
+ }
3242
+ let deleted = 0;
3243
+ let failed = 0;
3244
+ for (const record of cleanable) {
3245
+ try {
3246
+ if (record.status === "active" || record.status === "initializing") {
3247
+ try {
3248
+ await core.sessionManager.cancelSession(record.sessionId);
3249
+ } catch (err) {
3250
+ log8.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
3251
+ }
3252
+ }
3253
+ const topicId = record.platform?.topicId;
3254
+ if (topicId) {
3255
+ try {
3256
+ await ctx.api.deleteForumTopic(chatId, topicId);
3257
+ } catch (err) {
3258
+ log8.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3259
+ }
3260
+ }
3261
+ await core.sessionManager.removeRecord(record.sessionId);
3262
+ deleted++;
3263
+ } catch (err) {
3264
+ log8.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3265
+ failed++;
3266
+ }
3267
+ }
3268
+ await ctx.reply(
3269
+ `\u{1F5D1} Cleaned up <b>${deleted}</b> sessions${failed > 0 ? ` (${failed} failed)` : ""}.`,
3270
+ { parse_mode: "HTML" }
3271
+ );
3272
+ }
2873
3273
  async function handleAgents(ctx, core) {
2874
3274
  const agents = core.agentManager.getAvailableAgents();
2875
3275
  const defaultAgent = core.configManager.get().defaultAgent;
@@ -2886,22 +3286,54 @@ No agents configured.`;
2886
3286
  }
2887
3287
  async function handleHelp(ctx) {
2888
3288
  await ctx.reply(
2889
- `<b>OpenACP Commands:</b>
3289
+ `\u{1F4D6} <b>OpenACP Help</b>
2890
3290
 
3291
+ \u{1F680} <b>Getting Started</b>
3292
+ Tap \u{1F195} New Session to start coding with AI.
3293
+ Each session gets its own topic \u2014 chat there to work with the agent.
3294
+
3295
+ \u{1F4A1} <b>Common Tasks</b>
2891
3296
  /new [agent] [workspace] \u2014 Create new session
2892
- /newchat \u2014 New chat, same agent &amp; workspace
2893
- /cancel \u2014 Cancel current session
2894
- /status \u2014 Show session/system status
3297
+ /cancel \u2014 Cancel session (in session topic)
3298
+ /status \u2014 Show session or system status
3299
+ /sessions \u2014 List all sessions
2895
3300
  /agents \u2014 List available agents
2896
- /menu \u2014 Show interactive menu
3301
+
3302
+ \u2699\uFE0F <b>System</b>
2897
3303
  /restart \u2014 Restart OpenACP
2898
- /update \u2014 Update to latest version and restart
2899
- /help \u2014 Show this help
3304
+ /update \u2014 Update to latest version
3305
+ /integrate \u2014 Manage agent integrations
3306
+ /menu \u2014 Show action menu
3307
+
3308
+ \u{1F512} <b>Session Options</b>
3309
+ /enable_dangerous \u2014 Auto-approve permissions
3310
+ /disable_dangerous \u2014 Restore permission prompts
3311
+ /handoff \u2014 Continue session in terminal
3312
+ /clear \u2014 Clear assistant history
2900
3313
 
2901
- Or just chat in the \u{1F916} Assistant topic for help!`,
3314
+ \u{1F4AC} Need help? Just ask me in this topic!`,
2902
3315
  { parse_mode: "HTML" }
2903
3316
  );
2904
3317
  }
3318
+ async function handleClear(ctx, assistant) {
3319
+ if (!assistant) {
3320
+ await ctx.reply("\u26A0\uFE0F Assistant is not available.", { parse_mode: "HTML" });
3321
+ return;
3322
+ }
3323
+ const threadId = ctx.message?.message_thread_id;
3324
+ if (threadId !== assistant.topicId) {
3325
+ await ctx.reply("\u2139\uFE0F /clear only works in the Assistant topic.", { parse_mode: "HTML" });
3326
+ return;
3327
+ }
3328
+ await ctx.reply("\u{1F504} Clearing assistant history...", { parse_mode: "HTML" });
3329
+ try {
3330
+ await assistant.respawn();
3331
+ await ctx.reply("\u2705 Assistant history cleared.", { parse_mode: "HTML" });
3332
+ } catch (err) {
3333
+ const message = err instanceof Error ? err.message : String(err);
3334
+ await ctx.reply(`\u274C Failed to clear: <code>${message}</code>`, { parse_mode: "HTML" });
3335
+ }
3336
+ }
2905
3337
  function buildDangerousModeKeyboard(sessionId, enabled) {
2906
3338
  return new InlineKeyboard().text(
2907
3339
  enabled ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
@@ -3126,16 +3558,182 @@ async function executeCancelSession(core, excludeSessionId) {
3126
3558
  await session.cancel();
3127
3559
  return session;
3128
3560
  }
3561
+ async function handleIntegrate(ctx, _core) {
3562
+ const { listIntegrations } = await import("./integrate-WUPLRJD3.js");
3563
+ const agents = listIntegrations();
3564
+ const keyboard = new InlineKeyboard();
3565
+ for (const agent of agents) {
3566
+ keyboard.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
3567
+ }
3568
+ await ctx.reply(
3569
+ `<b>\u{1F517} Integrations</b>
3570
+
3571
+ Select an agent to manage its integrations.`,
3572
+ { parse_mode: "HTML", reply_markup: keyboard }
3573
+ );
3574
+ }
3575
+ function buildAgentItemsKeyboard(agentName, items) {
3576
+ const keyboard = new InlineKeyboard();
3577
+ for (const item of items) {
3578
+ const installed = item.isInstalled();
3579
+ keyboard.text(
3580
+ installed ? `\u2705 ${item.name} \u2014 Uninstall` : `\u{1F4E6} ${item.name} \u2014 Install`,
3581
+ installed ? `i:uninstall:${agentName}:${item.id}` : `i:install:${agentName}:${item.id}`
3582
+ ).row();
3583
+ }
3584
+ keyboard.text("\u2190 Back", "i:back").row();
3585
+ return keyboard;
3586
+ }
3587
+ function setupIntegrateCallbacks(bot, core) {
3588
+ bot.callbackQuery(/^i:/, async (ctx) => {
3589
+ const data = ctx.callbackQuery.data;
3590
+ try {
3591
+ await ctx.answerCallbackQuery();
3592
+ } catch {
3593
+ }
3594
+ if (data === "i:back") {
3595
+ const { listIntegrations } = await import("./integrate-WUPLRJD3.js");
3596
+ const agents = listIntegrations();
3597
+ const keyboard2 = new InlineKeyboard();
3598
+ for (const agent of agents) {
3599
+ keyboard2.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
3600
+ }
3601
+ try {
3602
+ await ctx.editMessageText(
3603
+ `<b>\u{1F517} Integrations</b>
3604
+
3605
+ Select an agent to manage its integrations.`,
3606
+ { parse_mode: "HTML", reply_markup: keyboard2 }
3607
+ );
3608
+ } catch {
3609
+ }
3610
+ return;
3611
+ }
3612
+ const agentMatch = data.match(/^i:agent:(.+)$/);
3613
+ if (agentMatch) {
3614
+ const agentName2 = agentMatch[1];
3615
+ const { getIntegration: getIntegration2 } = await import("./integrate-WUPLRJD3.js");
3616
+ const integration2 = getIntegration2(agentName2);
3617
+ if (!integration2) {
3618
+ await ctx.reply(`\u274C No integration available for '${escapeHtml(agentName2)}'.`, { parse_mode: "HTML" });
3619
+ return;
3620
+ }
3621
+ const keyboard2 = buildAgentItemsKeyboard(agentName2, integration2.items);
3622
+ try {
3623
+ await ctx.editMessageText(
3624
+ `<b>\u{1F517} ${escapeHtml(agentName2)} Integrations</b>
3625
+
3626
+ ${integration2.items.map((i) => `\u2022 <b>${escapeHtml(i.name)}</b> \u2014 ${escapeHtml(i.description)}`).join("\n")}`,
3627
+ { parse_mode: "HTML", reply_markup: keyboard2 }
3628
+ );
3629
+ } catch {
3630
+ await ctx.reply(
3631
+ `<b>\u{1F517} ${escapeHtml(agentName2)} Integrations</b>`,
3632
+ { parse_mode: "HTML", reply_markup: keyboard2 }
3633
+ );
3634
+ }
3635
+ return;
3636
+ }
3637
+ const actionMatch = data.match(/^i:(install|uninstall):([^:]+):(.+)$/);
3638
+ if (!actionMatch) return;
3639
+ const action = actionMatch[1];
3640
+ const agentName = actionMatch[2];
3641
+ const itemId = actionMatch[3];
3642
+ const { getIntegration } = await import("./integrate-WUPLRJD3.js");
3643
+ const integration = getIntegration(agentName);
3644
+ if (!integration) return;
3645
+ const item = integration.items.find((i) => i.id === itemId);
3646
+ if (!item) return;
3647
+ const result = action === "install" ? await item.install() : await item.uninstall();
3648
+ const installed = action === "install" && result.success;
3649
+ await core.configManager.save({
3650
+ integrations: {
3651
+ [agentName]: {
3652
+ installed,
3653
+ installedAt: installed ? (/* @__PURE__ */ new Date()).toISOString() : void 0
3654
+ }
3655
+ }
3656
+ });
3657
+ const statusEmoji = result.success ? "\u2705" : "\u274C";
3658
+ const actionLabel = action === "install" ? "installed" : "uninstalled";
3659
+ const logsText = result.logs.map((l) => `<code>${escapeHtml(l)}</code>`).join("\n");
3660
+ const resultText = `${statusEmoji} <b>${escapeHtml(item.name)}</b> ${actionLabel}.
3661
+
3662
+ ${logsText}`;
3663
+ const keyboard = buildAgentItemsKeyboard(agentName, integration.items);
3664
+ try {
3665
+ await ctx.editMessageText(
3666
+ `<b>\u{1F517} ${escapeHtml(agentName)} Integrations</b>
3667
+
3668
+ ${resultText}`,
3669
+ { parse_mode: "HTML", reply_markup: keyboard }
3670
+ );
3671
+ } catch {
3672
+ await ctx.reply(resultText, { parse_mode: "HTML" });
3673
+ }
3674
+ });
3675
+ }
3676
+ async function handlePendingWorkspaceInput(ctx, core, chatId, assistantTopicId) {
3677
+ const userId = ctx.from?.id;
3678
+ if (!userId) return false;
3679
+ const pending = pendingNewSessions.get(userId);
3680
+ if (!pending || !ctx.message?.text) return false;
3681
+ if (pending.step !== "workspace_input" && pending.step !== "workspace") return false;
3682
+ const threadId = ctx.message.message_thread_id;
3683
+ if (threadId && threadId !== assistantTopicId) return false;
3684
+ let workspace = ctx.message.text.trim();
3685
+ if (!workspace || !pending.agentName) {
3686
+ await ctx.reply("\u26A0\uFE0F Please enter a valid directory path.", { parse_mode: "HTML" });
3687
+ return true;
3688
+ }
3689
+ if (!workspace.startsWith("/") && !workspace.startsWith("~")) {
3690
+ const baseDir = core.configManager.get().workspace.baseDir;
3691
+ workspace = `${baseDir.replace(/\/$/, "")}/${workspace}`;
3692
+ }
3693
+ await startConfirmStep(ctx, chatId, userId, pending.agentName, workspace);
3694
+ return true;
3695
+ }
3696
+ async function startInteractiveNewSession(ctx, core, chatId, agentName) {
3697
+ const userId = ctx.from?.id;
3698
+ if (!userId) return;
3699
+ const agents = core.agentManager.getAvailableAgents();
3700
+ const config = core.configManager.get();
3701
+ if (agentName || agents.length === 1) {
3702
+ const selectedAgent = agentName || config.defaultAgent;
3703
+ await startWorkspaceStep(ctx, core, chatId, userId, selectedAgent);
3704
+ return;
3705
+ }
3706
+ const keyboard = new InlineKeyboard();
3707
+ for (const agent of agents) {
3708
+ const label = agent.name === config.defaultAgent ? `${agent.name} (default)` : agent.name;
3709
+ keyboard.text(label, `m:new:agent:${agent.name}`).row();
3710
+ }
3711
+ const msg = await ctx.reply(
3712
+ `\u{1F916} <b>Choose an agent:</b>`,
3713
+ { parse_mode: "HTML", reply_markup: keyboard }
3714
+ );
3715
+ cleanupPending(userId);
3716
+ pendingNewSessions.set(userId, {
3717
+ step: "agent",
3718
+ messageId: msg.message_id,
3719
+ threadId: ctx.callbackQuery?.message?.message_thread_id,
3720
+ timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
3721
+ });
3722
+ }
3129
3723
  var STATIC_COMMANDS = [
3130
3724
  { command: "new", description: "Create new session" },
3131
3725
  { command: "newchat", description: "New chat, same agent & workspace" },
3132
3726
  { command: "cancel", description: "Cancel current session" },
3133
3727
  { command: "status", description: "Show status" },
3728
+ { command: "sessions", description: "List all sessions" },
3134
3729
  { command: "agents", description: "List available agents" },
3135
3730
  { command: "help", description: "Help" },
3136
3731
  { command: "menu", description: "Show menu" },
3137
3732
  { command: "enable_dangerous", description: "Auto-approve all permission requests (session only)" },
3138
3733
  { command: "disable_dangerous", description: "Restore normal permission prompts (session only)" },
3734
+ { command: "integrate", description: "Manage agent integrations" },
3735
+ { command: "handoff", description: "Continue this session in your terminal" },
3736
+ { command: "clear", description: "Clear assistant history" },
3139
3737
  { command: "restart", description: "Restart OpenACP" },
3140
3738
  { command: "update", description: "Update to latest version and restart" }
3141
3739
  ];
@@ -3220,6 +3818,370 @@ ${escapeHtml(request.description)}`,
3220
3818
  }
3221
3819
  };
3222
3820
 
3821
+ // src/product-guide.ts
3822
+ var PRODUCT_GUIDE = `
3823
+ # OpenACP \u2014 Product Guide
3824
+
3825
+ OpenACP lets you chat with AI coding agents (like Claude Code) through Telegram.
3826
+ You type messages in Telegram, the agent reads/writes/runs code in your project folder, and results stream back in real time.
3827
+
3828
+ ---
3829
+
3830
+ ## Quick Start
3831
+
3832
+ 1. Start OpenACP: \`openacp\` (or \`openacp start\` for background daemon)
3833
+ 2. Open your Telegram group \u2014 you'll see the Assistant topic
3834
+ 3. Tap \u{1F195} New Session or type /new
3835
+ 4. Pick an agent and a project folder
3836
+ 5. Chat in the session topic \u2014 the agent works on your code
3837
+
3838
+ ---
3839
+
3840
+ ## Core Concepts
3841
+
3842
+ ### Sessions
3843
+ A session = one conversation with one AI agent working in one project folder.
3844
+ Each session gets its own Telegram topic. Chat there to give instructions to the agent.
3845
+
3846
+ ### Agents
3847
+ An agent is an AI coding tool (e.g., Claude Code). You can configure multiple agents.
3848
+ The default agent is used when you don't specify one.
3849
+
3850
+ ### Project Folder (Workspace)
3851
+ The directory where the agent reads, writes, and runs code.
3852
+ When creating a session, you choose which folder the agent works in.
3853
+ You can type a full path like \`~/code/my-project\` or just a name like \`my-project\` (it becomes \`<base-dir>/my-project\`).
3854
+
3855
+ ### System Topics
3856
+ - **Assistant** \u2014 Always-on helper that can answer questions, create sessions, check status, troubleshoot
3857
+ - **Notifications** \u2014 System alerts (permission requests, session errors, completions)
3858
+
3859
+ ---
3860
+
3861
+ ## Creating Sessions
3862
+
3863
+ ### From menu
3864
+ Tap \u{1F195} New Session \u2192 choose agent (if multiple) \u2192 choose project folder \u2192 confirm
3865
+
3866
+ ### From command
3867
+ - \`/new\` \u2014 Interactive flow (asks agent + folder)
3868
+ - \`/new claude ~/code/my-project\` \u2014 Create directly with specific agent and folder
3869
+
3870
+ ### From Assistant topic
3871
+ Just ask: "Create a session for my-project with claude" \u2014 the assistant handles it
3872
+
3873
+ ### Quick new chat
3874
+ \`/newchat\` in a session topic \u2014 creates new session with same agent and folder as current one
3875
+
3876
+ ---
3877
+
3878
+ ## Working with Sessions
3879
+
3880
+ ### Chat
3881
+ Type messages in the session topic. The agent responds with code changes, explanations, tool outputs.
3882
+
3883
+ ### What you see while the agent works
3884
+ - **\u{1F4AD} Thinking indicator** \u2014 Shows when the agent is reasoning, with elapsed time
3885
+ - **Text responses** \u2014 Streamed in real time, updated every few seconds
3886
+ - **Tool calls** \u2014 When the agent runs commands or edits files, you see tool name, input, status, and output
3887
+ - **\u{1F4CB} Plan card** \u2014 Visual task progress with completed/in-progress/pending items and progress bar
3888
+ - **"View File" / "View Diff" buttons** \u2014 Opens in browser with Monaco editor (requires tunnel)
3889
+
3890
+ ### Session lifecycle
3891
+ 1. **Creating** \u2014 Topic created, agent spawning
3892
+ 2. **Warming up** \u2014 Agent primes its cache (happens automatically, invisible to you)
3893
+ 3. **Active** \u2014 Ready for your messages
3894
+ 4. **Auto-naming** \u2014 After your first message, the session gets a descriptive name (agent summarizes in ~5 words). The topic title updates automatically.
3895
+ 5. **Finished/Error** \u2014 Session completed or hit an error
3896
+
3897
+ ### Agent skills
3898
+ Some agents provide slash commands (e.g., /compact, /review). Available skills are pinned in the session topic.
3899
+
3900
+ ### Permission requests
3901
+ When the agent wants to run a command, it asks for permission.
3902
+ You see buttons: \u2705 Allow, \u274C Reject (and sometimes "Always Allow").
3903
+ A notification also appears in the Notifications topic with a link to the request.
3904
+
3905
+ ### Dangerous mode
3906
+ Auto-approves ALL permission requests \u2014 the agent runs any command without asking.
3907
+ - Enable: \`/enable_dangerous\` or tap the \u2620\uFE0F button in the session
3908
+ - Disable: \`/disable_dangerous\` or tap the \u{1F510} button
3909
+ - \u26A0\uFE0F Use with caution \u2014 the agent can execute anything
3910
+
3911
+ ### Session timeout
3912
+ Idle sessions are automatically cancelled after a configurable timeout (default: 60 minutes).
3913
+ Configure via \`security.sessionTimeoutMinutes\` in config.
3914
+
3915
+ ---
3916
+
3917
+ ## Session Transfer (Handoff)
3918
+
3919
+ ### Telegram \u2192 Terminal
3920
+ 1. Type \`/handoff\` in a session topic
3921
+ 2. You get a command like \`claude --resume <SESSION_ID>\`
3922
+ 3. Copy and run it in your terminal \u2014 the session continues there with full conversation history
3923
+
3924
+ ### Terminal \u2192 Telegram
3925
+ 1. First time: run \`openacp integrate claude\` to install the handoff skill (one-time setup)
3926
+ 2. In Claude Code, use the /openacp:handoff slash command
3927
+ 3. The session appears as a new topic in Telegram and you can continue chatting there
3928
+
3929
+ ### How it works
3930
+ - The agent session ID is shared between platforms
3931
+ - Conversation history is preserved \u2014 pick up where you left off
3932
+ - The agent that supports resume (e.g., Claude with \`--resume\`) handles the actual transfer
3933
+
3934
+ ---
3935
+
3936
+ ## Managing Sessions
3937
+
3938
+ ### Status
3939
+ - \`/status\` \u2014 Shows active sessions count and details
3940
+ - Ask the Assistant: "What sessions are running?"
3941
+
3942
+ ### List all sessions
3943
+ - \`/sessions\` \u2014 Shows all sessions with status (active, finished, error)
3944
+
3945
+ ### Cancel
3946
+ - \`/cancel\` in a session topic \u2014 cancels that session
3947
+ - Ask the Assistant: "Cancel the stuck session"
3948
+
3949
+ ### Cleanup
3950
+ - From \`/sessions\` \u2192 tap cleanup buttons (finished, errors, all)
3951
+ - Ask the Assistant: "Clean up old sessions"
3952
+
3953
+ ---
3954
+
3955
+ ## Assistant Topic
3956
+
3957
+ The Assistant is an always-on AI helper in its own topic. It can:
3958
+ - Answer questions about OpenACP
3959
+ - Create sessions for you
3960
+ - Check status and health
3961
+ - Cancel sessions
3962
+ - Clean up old sessions
3963
+ - Troubleshoot issues
3964
+ - Manage configuration
3965
+
3966
+ Just chat naturally: "How do I create a session?", "What's the status?", "Something is stuck"
3967
+
3968
+ ### Clear history
3969
+ \`/clear\` in the Assistant topic \u2014 resets the conversation
3970
+
3971
+ ---
3972
+
3973
+ ## System Commands
3974
+
3975
+ | Command | Where | What it does |
3976
+ |---------|-------|-------------|
3977
+ | \`/new [agent] [path]\` | Anywhere | Create new session |
3978
+ | \`/newchat\` | Session topic | New session, same agent + folder |
3979
+ | \`/cancel\` | Session topic | Cancel current session |
3980
+ | \`/status\` | Anywhere | Show status |
3981
+ | \`/sessions\` | Anywhere | List all sessions |
3982
+ | \`/agents\` | Anywhere | List available agents |
3983
+ | \`/enable_dangerous\` | Session topic | Auto-approve all permissions |
3984
+ | \`/disable_dangerous\` | Session topic | Restore permission prompts |
3985
+ | \`/handoff\` | Session topic | Transfer session to terminal |
3986
+ | \`/clear\` | Assistant topic | Clear assistant history |
3987
+ | \`/menu\` | Anywhere | Show action menu |
3988
+ | \`/help\` | Anywhere | Show help |
3989
+ | \`/restart\` | Anywhere | Restart OpenACP |
3990
+ | \`/update\` | Anywhere | Update to latest version |
3991
+ | \`/integrate\` | Anywhere | Manage agent integrations |
3992
+
3993
+ ---
3994
+
3995
+ ## Menu Buttons
3996
+
3997
+ | Button | Action |
3998
+ |--------|--------|
3999
+ | \u{1F195} New Session | Create new session (interactive) |
4000
+ | \u{1F4CB} Sessions | List all sessions with cleanup options |
4001
+ | \u{1F4CA} Status | Show active/total session count |
4002
+ | \u{1F916} Agents | List available agents |
4003
+ | \u{1F517} Integrate | Manage agent integrations |
4004
+ | \u2753 Help | Show help text |
4005
+ | \u{1F504} Restart | Restart OpenACP |
4006
+ | \u2B06\uFE0F Update | Check and install updates |
4007
+
4008
+ ---
4009
+
4010
+ ## CLI Commands
4011
+
4012
+ ### Server
4013
+ - \`openacp\` \u2014 Start (uses configured mode: foreground or daemon)
4014
+ - \`openacp start\` \u2014 Start as background daemon
4015
+ - \`openacp stop\` \u2014 Stop daemon
4016
+ - \`openacp status\` \u2014 Show daemon status
4017
+ - \`openacp logs\` \u2014 Tail daemon logs
4018
+ - \`openacp --foreground\` \u2014 Force foreground mode (useful for debugging or containers)
4019
+
4020
+ ### Auto-start (run on boot)
4021
+ - macOS: installs a LaunchAgent in \`~/Library/LaunchAgents/\`
4022
+ - Linux: installs a systemd user service in \`~/.config/systemd/user/\`
4023
+ - Enabled automatically when you start the daemon. Remove with \`openacp stop\`.
4024
+
4025
+ ### Configuration
4026
+ - \`openacp config\` \u2014 Interactive config editor
4027
+ - \`openacp reset\` \u2014 Delete all data and start fresh
4028
+
4029
+ ### Plugins
4030
+ - \`openacp install <package>\` \u2014 Install adapter plugin (e.g., \`@openacp/adapter-discord\`)
4031
+ - \`openacp uninstall <package>\` \u2014 Remove adapter plugin
4032
+ - \`openacp plugins\` \u2014 List installed plugins
4033
+
4034
+ ### Integration
4035
+ - \`openacp integrate <agent>\` \u2014 Install agent integration (e.g., Claude handoff skill)
4036
+ - \`openacp integrate <agent> --uninstall\` \u2014 Remove integration
4037
+
4038
+ ### API (requires running daemon)
4039
+ \`openacp api <command>\` \u2014 Interact with running daemon:
4040
+
4041
+ | Command | Description |
4042
+ |---------|-------------|
4043
+ | \`status\` | List active sessions |
4044
+ | \`session <id>\` | Session details |
4045
+ | \`new <agent> <path>\` | Create session |
4046
+ | \`send <id> "text"\` | Send prompt |
4047
+ | \`cancel <id>\` | Cancel session |
4048
+ | \`dangerous <id> on/off\` | Toggle dangerous mode |
4049
+ | \`topics [--status x,y]\` | List topics |
4050
+ | \`delete-topic <id> [--force]\` | Delete topic |
4051
+ | \`cleanup [--status x,y]\` | Cleanup old topics |
4052
+ | \`agents\` | List agents |
4053
+ | \`health\` | System health |
4054
+ | \`config\` | Show config |
4055
+ | \`config set <key> <value>\` | Update config |
4056
+ | \`adapters\` | List adapters |
4057
+ | \`tunnel\` | Tunnel status |
4058
+ | \`notify "message"\` | Send notification |
4059
+ | \`version\` | Daemon version |
4060
+ | \`restart\` | Restart daemon |
4061
+
4062
+ ---
4063
+
4064
+ ## File Viewer (Tunnel)
4065
+
4066
+ When tunnel is enabled, file edits and diffs get "View" buttons that open in your browser:
4067
+ - **Monaco Editor** \u2014 Full VS Code editor with syntax highlighting
4068
+ - **Diff viewer** \u2014 Side-by-side or inline comparison
4069
+ - **Line highlighting** \u2014 Click lines to highlight
4070
+ - Dark/light theme toggle
4071
+
4072
+ ### Setup
4073
+ Enable in config: set \`tunnel.enabled\` to \`true\`.
4074
+ Providers: Cloudflare (default, free), ngrok, bore, Tailscale Funnel.
4075
+
4076
+ ---
4077
+
4078
+ ## Configuration
4079
+
4080
+ Config file: \`~/.openacp/config.json\`
4081
+
4082
+ ### Telegram
4083
+ - **telegram.botToken** \u2014 Your Telegram bot token
4084
+ - **telegram.chatId** \u2014 Your Telegram supergroup ID
4085
+
4086
+ ### Agents
4087
+ - **agents.<name>.command** \u2014 Agent executable path (e.g., \`claude\`, \`codex\`)
4088
+ - **agents.<name>.args** \u2014 Arguments to pass to the agent command
4089
+ - **agents.<name>.env** \u2014 Custom environment variables for the agent subprocess
4090
+ - **defaultAgent** \u2014 Which agent to use by default
4091
+
4092
+ ### Workspace
4093
+ - **workspace.baseDir** \u2014 Base directory for project folders (default: \`~/openacp-workspace\`)
4094
+
4095
+ ### Security
4096
+ - **security.allowedUserIds** \u2014 Restrict who can use the bot (empty = everyone)
4097
+ - **security.maxConcurrentSessions** \u2014 Max parallel sessions (default: 5)
4098
+ - **security.sessionTimeoutMinutes** \u2014 Auto-cancel idle sessions (default: 60)
4099
+
4100
+ ### Tunnel / File Viewer
4101
+ - **tunnel.enabled** \u2014 Enable file viewer tunnel
4102
+ - **tunnel.provider** \u2014 Tunnel provider: cloudflare (default, free), ngrok, bore, tailscale
4103
+ - **tunnel.port** \u2014 Local port for tunnel server (default: 3100)
4104
+ - **tunnel.auth.enabled** \u2014 Enable authentication for tunnel URLs
4105
+ - **tunnel.auth.token** \u2014 Auth token for tunnel access
4106
+ - **tunnel.storeTtlMinutes** \u2014 How long viewer links stay cached (default: 60)
4107
+
4108
+ ### Logging
4109
+ - **logging.level** \u2014 Log level: silent, debug, info, warn, error, fatal (default: info)
4110
+ - **logging.logDir** \u2014 Log directory (default: \`~/.openacp/logs\`)
4111
+ - **logging.maxFileSize** \u2014 Max log file size before rotation
4112
+ - **logging.maxFiles** \u2014 Max number of rotated log files
4113
+ - **logging.sessionLogRetentionDays** \u2014 Auto-delete old session logs (default: 30)
4114
+
4115
+ ### Data Retention
4116
+ - **sessionStore.ttlDays** \u2014 How long session records persist (default: 30). Old records are cleaned up automatically.
4117
+
4118
+ ### Environment variables
4119
+ Override config with env vars:
4120
+ - \`OPENACP_TELEGRAM_BOT_TOKEN\`
4121
+ - \`OPENACP_TELEGRAM_CHAT_ID\`
4122
+ - \`OPENACP_DEFAULT_AGENT\`
4123
+ - \`OPENACP_RUN_MODE\` \u2014 foreground or daemon
4124
+ - \`OPENACP_API_PORT\` \u2014 API server port (default: 21420)
4125
+ - \`OPENACP_TUNNEL_ENABLED\`
4126
+ - \`OPENACP_TUNNEL_PORT\`
4127
+ - \`OPENACP_TUNNEL_PROVIDER\`
4128
+ - \`OPENACP_LOG_LEVEL\`
4129
+ - \`OPENACP_LOG_DIR\`
4130
+ - \`OPENACP_DEBUG\` \u2014 Sets log level to debug
4131
+
4132
+ ---
4133
+
4134
+ ## Troubleshooting
4135
+
4136
+ ### Session stuck / not responding
4137
+ - Check status: ask Assistant "Is anything stuck?"
4138
+ - Cancel and create new: \`/cancel\` then \`/new\`
4139
+ - Check system health: Assistant can run health check
4140
+
4141
+ ### Agent not found
4142
+ - Check available agents: \`/agents\`
4143
+ - Verify agent command is installed and in PATH
4144
+ - Check config: agent command + args must be correct
4145
+
4146
+ ### Permission request not showing
4147
+ - Check Notifications topic for the alert
4148
+ - Try \`/enable_dangerous\` to auto-approve (if you trust the agent)
4149
+
4150
+ ### Session disappeared after restart
4151
+ - Sessions persist across restarts
4152
+ - Send a message in the old topic \u2014 it auto-resumes
4153
+ - If topic was deleted, the session record may still exist in status
4154
+
4155
+ ### Bot not responding at all
4156
+ - Check daemon: \`openacp status\`
4157
+ - Check logs: \`openacp logs\`
4158
+ - Restart: \`openacp start\` or \`/restart\`
4159
+
4160
+ ### Messages going to wrong topic
4161
+ - Each session is bound to a specific Telegram topic
4162
+ - If you see messages appearing in the Assistant topic instead of the session topic, try creating a new session
4163
+
4164
+ ### Viewing logs
4165
+ - Session-specific logs: \`~/.openacp/logs/sessions/\`
4166
+ - System logs: \`openacp logs\` to tail live
4167
+ - Set \`OPENACP_DEBUG=true\` for verbose output
4168
+
4169
+ ---
4170
+
4171
+ ## Data & Storage
4172
+
4173
+ All data is stored in \`~/.openacp/\`:
4174
+ - \`config.json\` \u2014 Configuration
4175
+ - \`sessions/\` \u2014 Session records and state
4176
+ - \`topics/\` \u2014 Topic-to-session mappings
4177
+ - \`logs/\` \u2014 System and session logs
4178
+ - \`plugins/\` \u2014 Installed adapter plugins
4179
+ - \`openacp.pid\` \u2014 Daemon PID file
4180
+
4181
+ Session records auto-cleanup: 30 days (configurable via \`sessionStore.ttlDays\`).
4182
+ Session logs auto-cleanup: 30 days (configurable via \`logging.sessionLogRetentionDays\`).
4183
+ `;
4184
+
3223
4185
  // src/adapters/telegram/assistant.ts
3224
4186
  var log10 = createChildLogger({ module: "telegram-assistant" });
3225
4187
  async function spawnAssistant(core, adapter, assistantTopicId) {
@@ -3256,52 +4218,100 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
3256
4218
  });
3257
4219
  return { session, ready };
3258
4220
  }
4221
+ function buildWelcomeMessage(ctx) {
4222
+ const { activeCount, errorCount, totalCount, agents, defaultAgent } = ctx;
4223
+ const agentList = agents.map((a) => `${a}${a === defaultAgent ? " (default)" : ""}`).join(", ");
4224
+ if (totalCount === 0) {
4225
+ return `\u{1F44B} <b>OpenACP is ready!</b>
4226
+
4227
+ No sessions yet. Tap \u{1F195} New Session to start, or ask me anything!`;
4228
+ }
4229
+ if (errorCount > 0) {
4230
+ return `\u{1F44B} <b>OpenACP is ready!</b>
4231
+
4232
+ \u{1F4CA} ${activeCount} active, ${errorCount} errors / ${totalCount} total
4233
+ \u26A0\uFE0F ${errorCount} session${errorCount > 1 ? "s have" : " has"} errors \u2014 ask me to check if you'd like.
4234
+
4235
+ Agents: ${agentList}`;
4236
+ }
4237
+ return `\u{1F44B} <b>OpenACP is ready!</b>
4238
+
4239
+ \u{1F4CA} ${activeCount} active / ${totalCount} total
4240
+ Agents: ${agentList}`;
4241
+ }
3259
4242
  function buildAssistantSystemPrompt(ctx) {
3260
4243
  const { config, activeSessionCount, totalSessionCount, topicSummary } = ctx;
3261
4244
  const agentNames = Object.keys(config.agents).join(", ");
3262
4245
  const topicBreakdown = topicSummary.map((s) => `${s.status}: ${s.count}`).join(", ") || "none";
3263
- return `You are the OpenACP Assistant. Help users manage their AI coding sessions and topics.
4246
+ return `You are the OpenACP Assistant \u2014 a helpful guide for managing AI coding sessions.
3264
4247
 
3265
4248
  ## Current State
3266
4249
  - Active sessions: ${activeSessionCount} / ${totalSessionCount} total
3267
4250
  - Topics by status: ${topicBreakdown}
3268
4251
  - Available agents: ${agentNames}
3269
4252
  - Default agent: ${config.defaultAgent}
3270
- - Workspace base: ${config.workspace.baseDir}
4253
+ - Workspace base directory: ${config.workspace.baseDir}
4254
+
4255
+ ## Action Playbook
4256
+
4257
+ ### Create Session
4258
+ - The workspace is the project directory where the agent will work (read, write, execute code). It is NOT the base directory \u2014 it should be a specific project folder like \`~/code/my-project\` or \`${config.workspace.baseDir}/my-app\`.
4259
+ - Ask which agent to use (if multiple are configured). Show available: ${agentNames}
4260
+ - Ask which project directory to use as workspace. Suggest \`${config.workspace.baseDir}\` as the base, but explain the user can provide any path.
4261
+ - Confirm before creating: show agent name + full workspace path.
4262
+ - Create via: \`openacp api new <agent> <workspace>\`
4263
+
4264
+ ### Check Status / List Sessions
4265
+ - Run \`openacp api status\` for active sessions overview
4266
+ - Run \`openacp api topics\` for full list with statuses
4267
+ - Format the output nicely for the user
4268
+
4269
+ ### Cancel Session
4270
+ - Run \`openacp api status\` to see what's active
4271
+ - If 1 active session \u2192 ask user to confirm \u2192 \`openacp api cancel <id>\`
4272
+ - If multiple \u2192 list them, ask user which one to cancel
3271
4273
 
3272
- ## Session Management Commands
3273
- These are Telegram bot commands (type directly in chat):
3274
- - /new [agent] [workspace] \u2014 Create new session
3275
- - /newchat \u2014 New chat with same agent & workspace
3276
- - /cancel \u2014 Cancel current session
3277
- - /status \u2014 Show status
3278
- - /agents \u2014 List agents
3279
- - /help \u2014 Show help
4274
+ ### Troubleshoot (Session Stuck, Errors)
4275
+ - Run \`openacp api health\` + \`openacp api status\` to diagnose
4276
+ - Small issue (stuck session) \u2192 suggest cancel + create new
4277
+ - Big issue (system-level) \u2192 suggest restart, ask for confirmation first
3280
4278
 
3281
- ## Management Commands (via CLI)
3282
- You have access to bash. Use these commands to manage OpenACP:
4279
+ ### Cleanup Old Sessions
4280
+ - Run \`openacp api topics --status finished,error\` to see what can be cleaned
4281
+ - Report the count, ask user to confirm
4282
+ - Execute: \`openacp api cleanup --status <statuses>\`
3283
4283
 
3284
- ### Session management
4284
+ ### Configuration
4285
+ - View: \`openacp api config\`
4286
+ - Update: \`openacp api config set <key> <value>\`
4287
+
4288
+ ### Restart / Update
4289
+ - Always ask for confirmation \u2014 these are disruptive actions
4290
+ - Guide user: "Tap \u{1F504} Restart button or type /restart"
4291
+
4292
+ ### Toggle Dangerous Mode
4293
+ - Run \`openacp api dangerous <id> on|off\`
4294
+ - Explain: dangerous mode auto-approves all permission requests \u2014 the agent can run any command without asking
4295
+
4296
+ ## CLI Commands Reference
3285
4297
  \`\`\`bash
4298
+ # Session management
3286
4299
  openacp api status # List active sessions
3287
4300
  openacp api session <id> # Session detail
4301
+ openacp api new <agent> <workspace> # Create new session
3288
4302
  openacp api send <id> "prompt text" # Send prompt to session
3289
4303
  openacp api cancel <id> # Cancel session
3290
4304
  openacp api dangerous <id> on|off # Toggle dangerous mode
3291
- \`\`\`
3292
4305
 
3293
- ### Topic management
3294
- \`\`\`bash
3295
- openacp api topics # List topics
4306
+ # Topic management
4307
+ openacp api topics # List all topics
3296
4308
  openacp api topics --status finished,error
3297
4309
  openacp api delete-topic <id> # Delete topic
3298
4310
  openacp api delete-topic <id> --force # Force delete active
3299
4311
  openacp api cleanup # Cleanup finished topics
3300
4312
  openacp api cleanup --status finished,error
3301
- \`\`\`
3302
4313
 
3303
- ### System
3304
- \`\`\`bash
4314
+ # System
3305
4315
  openacp api health # System health
3306
4316
  openacp api config # Show config
3307
4317
  openacp api config set <key> <value> # Update config
@@ -3313,13 +4323,18 @@ openacp api restart # Restart daemon
3313
4323
  \`\`\`
3314
4324
 
3315
4325
  ## Guidelines
3316
- - When a user asks about sessions or topics, run \`openacp api topics\` or \`openacp api status\` to get current data.
3317
- - When deleting: if the session is active/initializing, warn the user first. Only use --force if they confirm.
3318
- - Use \`openacp api health\` to check system status.
3319
- - Use \`openacp api config\` to check configuration, \`openacp api config set\` to update values.
3320
- - Format responses nicely for Telegram (use bold, code blocks).
3321
- - Be concise and helpful. Respond in the same language the user uses.
3322
- - When creating sessions, guide through: agent selection \u2192 workspace \u2192 confirm.`;
4326
+ - NEVER show \`openacp api ...\` commands to users. These are internal tools for YOU to run silently. Users should only see natural language responses and results.
4327
+ - Run \`openacp api ...\` commands yourself for everything you can. Only guide users to Telegram buttons/menu when needed (e.g., "Tap \u{1F195} New Session" or "Go to the session topic to chat with the agent").
4328
+ - When creating sessions: guide user through agent + workspace choice conversationally, then run the command yourself.
4329
+ - Destructive actions (cancel active session, restart, cleanup) \u2192 always ask user to confirm first in natural language.
4330
+ - Small/obvious issues (clearly stuck session with no activity) \u2192 fix it and report back.
4331
+ - Respond in the same language the user uses.
4332
+ - Format responses for Telegram: use <b>bold</b>, <code>code</code>, keep it concise.
4333
+ - When you don't know something, check with the relevant \`openacp api\` command first before answering.
4334
+ - Talk to users like a helpful assistant, not a CLI manual. Example: "B\u1EA1n c\xF3 2 session \u0111ang ch\u1EA1y. Mu\u1ED1n xem chi ti\u1EBFt kh\xF4ng?" instead of listing commands.
4335
+
4336
+ ## Product Reference
4337
+ ${PRODUCT_GUIDE}`;
3323
4338
  }
3324
4339
  async function handleAssistantMessage(session, text) {
3325
4340
  if (!session) return;
@@ -3761,27 +4776,38 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
3761
4776
  removeAction(actionId);
3762
4777
  try {
3763
4778
  if (action.action === "new_session") {
3764
- await ctx.answerCallbackQuery({ text: "\u23F3 Creating session..." });
3765
- const { threadId, firstMsgId } = await executeNewSession(
3766
- bot,
3767
- core,
3768
- chatId,
3769
- action.agent,
3770
- action.workspace
3771
- );
3772
- const topicLink = `https://t.me/c/${String(chatId).replace("-100", "")}/${firstMsgId ?? threadId}`;
3773
- const originalText = ctx.callbackQuery.message?.text ?? "";
3774
- try {
3775
- await ctx.editMessageText(
3776
- originalText + `
4779
+ if (action.agent && action.workspace) {
4780
+ await ctx.answerCallbackQuery({ text: "\u23F3 Creating session..." });
4781
+ const { threadId, firstMsgId } = await executeNewSession(
4782
+ bot,
4783
+ core,
4784
+ chatId,
4785
+ action.agent,
4786
+ action.workspace
4787
+ );
4788
+ const topicLink = `https://t.me/c/${String(chatId).replace("-100", "")}/${firstMsgId ?? threadId}`;
4789
+ const originalText = ctx.callbackQuery.message?.text ?? "";
4790
+ try {
4791
+ await ctx.editMessageText(
4792
+ originalText + `
3777
4793
 
3778
4794
  \u2705 Session created \u2192 <a href="${topicLink}">Go to topic</a>`,
3779
- { parse_mode: "HTML" }
3780
- );
3781
- } catch {
3782
- await ctx.editMessageReplyMarkup({
3783
- reply_markup: { inline_keyboard: [] }
3784
- });
4795
+ { parse_mode: "HTML" }
4796
+ );
4797
+ } catch {
4798
+ await ctx.editMessageReplyMarkup({
4799
+ reply_markup: { inline_keyboard: [] }
4800
+ });
4801
+ }
4802
+ } else {
4803
+ await ctx.answerCallbackQuery();
4804
+ try {
4805
+ await ctx.editMessageReplyMarkup({
4806
+ reply_markup: { inline_keyboard: [] }
4807
+ });
4808
+ } catch {
4809
+ }
4810
+ await startInteractiveNewSession(ctx, core, chatId, action.agent);
3785
4811
  }
3786
4812
  } else if (action.action === "cancel_session") {
3787
4813
  const assistantId = getAssistantSessionId();
@@ -3942,10 +4968,12 @@ var TelegramAdapter = class extends ChannelAdapter {
3942
4968
  this.telegramConfig.chatId,
3943
4969
  () => this.assistantSession?.id
3944
4970
  );
4971
+ setupIntegrateCallbacks(this.bot, this.core);
3945
4972
  setupMenuCallbacks(
3946
4973
  this.bot,
3947
4974
  this.core,
3948
- this.telegramConfig.chatId
4975
+ this.telegramConfig.chatId,
4976
+ { notificationTopicId: this.notificationTopicId, assistantTopicId: this.assistantTopicId }
3949
4977
  );
3950
4978
  setupCommands(
3951
4979
  this.bot,
@@ -3953,7 +4981,23 @@ var TelegramAdapter = class extends ChannelAdapter {
3953
4981
  this.telegramConfig.chatId,
3954
4982
  {
3955
4983
  topicId: this.assistantTopicId,
3956
- getSession: () => this.assistantSession
4984
+ getSession: () => this.assistantSession,
4985
+ respawn: async () => {
4986
+ if (this.assistantSession) {
4987
+ await this.assistantSession.destroy();
4988
+ this.assistantSession = null;
4989
+ }
4990
+ const { session, ready } = await spawnAssistant(
4991
+ this.core,
4992
+ this,
4993
+ this.assistantTopicId
4994
+ );
4995
+ this.assistantSession = session;
4996
+ this.assistantInitializing = true;
4997
+ ready.then(() => {
4998
+ this.assistantInitializing = false;
4999
+ });
5000
+ }
3957
5001
  }
3958
5002
  );
3959
5003
  this.permissionHandler.setupCallbackHandler();
@@ -3967,24 +5011,26 @@ var TelegramAdapter = class extends ChannelAdapter {
3967
5011
  return;
3968
5012
  }
3969
5013
  const session = this.core.sessionManager.getSessionByThread("telegram", String(threadId));
3970
- if (!session) {
3971
- await ctx.reply("No active session in this topic.", {
5014
+ const record = session ? void 0 : this.core.sessionManager.getRecordByThread("telegram", String(threadId));
5015
+ const agentName = session?.agentName ?? record?.agentName;
5016
+ const agentSessionId = session?.agentSessionId ?? record?.agentSessionId;
5017
+ if (!agentName || !agentSessionId) {
5018
+ await ctx.reply("No session found for this topic.", {
3972
5019
  message_thread_id: threadId
3973
5020
  });
3974
5021
  return;
3975
5022
  }
3976
5023
  const { getAgentCapabilities: getAgentCapabilities2 } = await import("./agent-registry-7HC6D4CH.js");
3977
- const caps = getAgentCapabilities2(session.agentName);
5024
+ const caps = getAgentCapabilities2(agentName);
3978
5025
  if (!caps.supportsResume || !caps.resumeCommand) {
3979
- await ctx.reply("This agent does not support CLI handoff.", {
5026
+ await ctx.reply("This agent does not support session transfer.", {
3980
5027
  message_thread_id: threadId
3981
5028
  });
3982
5029
  return;
3983
5030
  }
3984
- const agentSessionId = session.agentSessionId;
3985
5031
  const command = caps.resumeCommand(agentSessionId);
3986
5032
  await ctx.reply(
3987
- `Resume this session on CLI:
5033
+ `Run this in your terminal to continue the session:
3988
5034
 
3989
5035
  <code>${command}</code>`,
3990
5036
  {
@@ -4004,14 +5050,14 @@ var TelegramAdapter = class extends ChannelAdapter {
4004
5050
  try {
4005
5051
  const config = this.core.configManager.get();
4006
5052
  const agents = this.core.agentManager.getAvailableAgents();
4007
- const agentList = agents.map((a) => `${escapeHtml(a.name)}${a.name === config.defaultAgent ? " (default)" : ""}`).join(", ");
4008
- const workspace = escapeHtml(config.workspace.baseDir);
4009
- const welcomeText = `\u{1F44B} <b>OpenACP Assistant</b> is online.
4010
-
4011
- Available agents: ${agentList}
4012
- Workspace: <code>${workspace}</code>
4013
-
4014
- <b>Select an action:</b>`;
5053
+ const allRecords = this.core.sessionManager.listRecords();
5054
+ const welcomeText = buildWelcomeMessage({
5055
+ activeCount: allRecords.filter((r) => r.status === "active" || r.status === "initializing").length,
5056
+ errorCount: allRecords.filter((r) => r.status === "error").length,
5057
+ totalCount: allRecords.length,
5058
+ agents: agents.map((a) => a.name),
5059
+ defaultAgent: config.defaultAgent
5060
+ });
4015
5061
  await this.bot.api.sendMessage(this.telegramConfig.chatId, welcomeText, {
4016
5062
  message_thread_id: this.assistantTopicId,
4017
5063
  parse_mode: "HTML",
@@ -4056,6 +5102,9 @@ Workspace: <code>${workspace}</code>
4056
5102
  setupRoutes() {
4057
5103
  this.bot.on("message:text", async (ctx) => {
4058
5104
  const threadId = ctx.message.message_thread_id;
5105
+ if (await handlePendingWorkspaceInput(ctx, this.core, this.telegramConfig.chatId, this.assistantTopicId)) {
5106
+ return;
5107
+ }
4059
5108
  if (!threadId) {
4060
5109
  const html = redirectToAssistant(
4061
5110
  this.telegramConfig.chatId,
@@ -4102,6 +5151,10 @@ Workspace: <code>${workspace}</code>
4102
5151
  );
4103
5152
  if (!session) return;
4104
5153
  const threadId = Number(session.threadId);
5154
+ if (!threadId || isNaN(threadId)) {
5155
+ log12.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
5156
+ return;
5157
+ }
4105
5158
  switch (content.type) {
4106
5159
  case "thought": {
4107
5160
  const tracker = this.getOrCreateTracker(sessionId, threadId);
@@ -4533,4 +5586,4 @@ export {
4533
5586
  TopicManager,
4534
5587
  TelegramAdapter
4535
5588
  };
4536
- //# sourceMappingURL=chunk-2M4O7AFI.js.map
5589
+ //# sourceMappingURL=chunk-45DFYWJT.js.map