pikiloom 0.4.26 → 0.4.27

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 (155) hide show
  1. package/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DIP-y4dl.js} +1 -1
  2. package/dashboard/dist/assets/ConnectionModal-BCfvhkiI.js +1 -0
  3. package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-Dot7ua9-.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-GioMx4S6.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C9_r8jJP.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-M1AP1sgR.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-CCm3UUfJ.js} +1 -1
  8. package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-YubDM16P.js} +1 -1
  9. package/dashboard/dist/assets/{SessionPanel-_0dLWvEb.js → SessionPanel-Cnv9o96n.js} +1 -1
  10. package/dashboard/dist/assets/SystemTab-VgNivIe3.js +1 -0
  11. package/dashboard/dist/assets/index-BY7l20bi.js +23 -0
  12. package/dashboard/dist/assets/index-BlqvzDUL.js +3 -0
  13. package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
  14. package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C0SAInnT.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/acp-client.js +0 -59
  17. package/dist/agent/artifacts.js +1 -38
  18. package/dist/agent/auto-update.js +2 -78
  19. package/dist/agent/await-resume.js +0 -20
  20. package/dist/agent/cli/auth.js +3 -51
  21. package/dist/agent/cli/catalog.js +0 -3
  22. package/dist/agent/cli/detector.js +1 -19
  23. package/dist/agent/cli/index.js +0 -3
  24. package/dist/agent/cli/registry.js +0 -25
  25. package/dist/agent/driver.js +0 -14
  26. package/dist/agent/drivers/claude-tui.js +2 -769
  27. package/dist/agent/drivers/claude.js +12 -631
  28. package/dist/agent/drivers/codex.js +11 -162
  29. package/dist/agent/drivers/gemini.js +6 -114
  30. package/dist/agent/drivers/hermes.js +0 -128
  31. package/dist/agent/goal.js +0 -45
  32. package/dist/agent/handover.js +1 -46
  33. package/dist/agent/images.js +7 -133
  34. package/dist/agent/index.js +1 -29
  35. package/dist/agent/mcp/bridge.js +1 -197
  36. package/dist/agent/mcp/extensions.js +5 -84
  37. package/dist/agent/mcp/oauth.js +2 -69
  38. package/dist/agent/mcp/registry.js +2 -33
  39. package/dist/agent/mcp/session-server.js +2 -45
  40. package/dist/agent/mcp/tools/ask-user.js +0 -6
  41. package/dist/agent/mcp/tools/await-resume.js +0 -15
  42. package/dist/agent/mcp/tools/goal.js +0 -25
  43. package/dist/agent/mcp/tools/types.js +0 -5
  44. package/dist/agent/mcp/tools/workspace.js +0 -23
  45. package/dist/agent/npm.js +0 -14
  46. package/dist/agent/session.js +86 -196
  47. package/dist/agent/skill-installer.js +3 -70
  48. package/dist/agent/skills.js +0 -43
  49. package/dist/agent/stream.js +5 -148
  50. package/dist/agent/types.js +0 -12
  51. package/dist/agent/utils.js +2 -120
  52. package/dist/bot/bot.js +3 -468
  53. package/dist/bot/command-ui.js +0 -45
  54. package/dist/bot/commands.js +0 -51
  55. package/dist/bot/headless-bot.js +0 -21
  56. package/dist/bot/host.js +1 -5
  57. package/dist/bot/human-loop.js +0 -8
  58. package/dist/bot/menu.js +0 -3
  59. package/dist/bot/orchestration.js +0 -3
  60. package/dist/bot/render-shared.js +0 -63
  61. package/dist/bot/session-hub.js +14 -92
  62. package/dist/bot/session-status.js +1 -5
  63. package/dist/bot/streaming.js +0 -53
  64. package/dist/browser-profile.js +0 -51
  65. package/dist/browser-supervisor.js +0 -75
  66. package/dist/catalog/cli-tools.js +0 -53
  67. package/dist/catalog/index.js +0 -18
  68. package/dist/catalog/local-models.js +0 -18
  69. package/dist/catalog/mcp-servers.js +0 -35
  70. package/dist/catalog/skill-repos.js +0 -32
  71. package/dist/channels/base.js +0 -19
  72. package/dist/channels/dingtalk/bot.js +0 -3
  73. package/dist/channels/dingtalk/channel.js +0 -33
  74. package/dist/channels/discord/bot.js +0 -7
  75. package/dist/channels/discord/channel.js +0 -22
  76. package/dist/channels/feishu/bot.js +0 -71
  77. package/dist/channels/feishu/channel.js +0 -78
  78. package/dist/channels/feishu/markdown.js +0 -5
  79. package/dist/channels/feishu/render.js +0 -45
  80. package/dist/channels/health.js +0 -46
  81. package/dist/channels/slack/bot.js +0 -7
  82. package/dist/channels/slack/channel.js +1 -27
  83. package/dist/channels/states.js +0 -3
  84. package/dist/channels/telegram/bot.js +0 -60
  85. package/dist/channels/telegram/channel.js +2 -99
  86. package/dist/channels/telegram/directory.js +0 -3
  87. package/dist/channels/telegram/live-preview.js +0 -8
  88. package/dist/channels/telegram/render.js +0 -30
  89. package/dist/channels/wecom/bot.js +0 -3
  90. package/dist/channels/wecom/channel.js +0 -58
  91. package/dist/channels/weixin/api.js +0 -9
  92. package/dist/channels/weixin/bot.js +0 -88
  93. package/dist/channels/weixin/channel.js +0 -6
  94. package/dist/cli/autostart.js +1 -44
  95. package/dist/cli/channel-supervisor.js +1 -59
  96. package/dist/cli/channels.js +0 -6
  97. package/dist/cli/main.js +1 -145
  98. package/dist/cli/onboarding.js +0 -6
  99. package/dist/cli/run.js +0 -11
  100. package/dist/cli/setup-wizard.js +0 -3
  101. package/dist/core/config/runtime-config.js +0 -38
  102. package/dist/core/config/user-config.js +0 -63
  103. package/dist/core/config/validation.js +0 -9
  104. package/dist/core/constants.js +0 -268
  105. package/dist/core/git.js +0 -31
  106. package/dist/core/legacy-compat.js +1 -28
  107. package/dist/core/logging.js +0 -3
  108. package/dist/core/platform.js +0 -28
  109. package/dist/core/process-control.js +0 -8
  110. package/dist/core/secrets/index.js +0 -17
  111. package/dist/core/secrets/inline-seal.js +0 -7
  112. package/dist/core/secrets/ref.js +0 -19
  113. package/dist/core/secrets/resolver.js +0 -8
  114. package/dist/core/secrets/store.js +1 -12
  115. package/dist/core/utils.js +1 -37
  116. package/dist/core/version.js +0 -3
  117. package/dist/dashboard/platform.js +1 -35
  118. package/dist/dashboard/routes/agents.js +2 -71
  119. package/dist/dashboard/routes/cli.js +0 -27
  120. package/dist/dashboard/routes/config.js +0 -54
  121. package/dist/dashboard/routes/extensions.js +0 -132
  122. package/dist/dashboard/routes/local-models.js +0 -68
  123. package/dist/dashboard/routes/models.js +0 -46
  124. package/dist/dashboard/routes/sessions.js +0 -100
  125. package/dist/dashboard/runtime.js +1 -58
  126. package/dist/dashboard/server.js +0 -49
  127. package/dist/dashboard/session-control.js +2 -69
  128. package/dist/model/catalog.js +1 -17
  129. package/dist/model/index.js +0 -15
  130. package/dist/model/injector.js +1 -181
  131. package/dist/model/provider-models.js +1 -48
  132. package/dist/model/responses-bridge.js +3 -45
  133. package/dist/model/store.js +0 -17
  134. package/dist/model/types.js +0 -12
  135. package/dist/model/validation.js +1 -18
  136. package/dist/pikichannel/adapter-pikiloom.js +0 -31
  137. package/dist/pikichannel/code.js +0 -12
  138. package/dist/pikichannel/codec.js +0 -14
  139. package/dist/pikichannel/host.js +4 -33
  140. package/dist/pikichannel/protocol.js +0 -52
  141. package/dist/pikichannel/rendezvous-broker.js +5 -26
  142. package/dist/pikichannel/rendezvous-host.js +3 -16
  143. package/dist/pikichannel/server.js +6 -55
  144. package/dist/pikichannel/transport.js +0 -25
  145. package/dist/pikichannel/transports/webrtc-host.js +0 -18
  146. package/dist/pikichannel/transports/webrtc-shared.js +4 -34
  147. package/dist/pikichannel/transports/websocket-host.js +1 -14
  148. package/dist/pikichannel/turn.js +2 -69
  149. package/dist/pikichannel/web/sdk.js +14 -71
  150. package/package.json +1 -1
  151. package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
  152. package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
  153. package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
  154. package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
  155. package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
@@ -1,6 +1,3 @@
1
- /**
2
- * WeChat bot orchestration.
3
- */
4
1
  import fs from 'node:fs';
5
2
  import os from 'node:os';
6
3
  import path from 'node:path';
@@ -21,11 +18,6 @@ const SHUTDOWN_EXIT_CODE = {
21
18
  function describeError(error) {
22
19
  return error instanceof Error ? error.message : String(error ?? 'unknown error');
23
20
  }
24
- /**
25
- * Plain-text echo of a resolved human-loop prompt — WeChat doesn't render
26
- * markdown or cards, so we keep the format minimal and obviously distinct
27
- * from a regular agent reply.
28
- */
29
21
  function buildInteractionEchoPlain(summary) {
30
22
  if (summary.status === 'cancelled')
31
23
  return '⊘ Prompt cancelled.';
@@ -143,9 +135,6 @@ export class WeixinBot extends Bot {
143
135
  const [rawCommand, ...rest] = text.trim().slice(1).split(/\s+/);
144
136
  const command = rawCommand?.toLowerCase() || '';
145
137
  const args = rest.join(' ').trim();
146
- // `/cancel` aborts any pending command-UI / im_ask_user prompt for this
147
- // chat. Special-cased before the auto-cancel below so a bare `/cancel`
148
- // doesn't get treated as the start of a new interactive command.
149
138
  if (command === 'cancel' || command === 'quit') {
150
139
  const pending = this.pendingHumanLoopPrompt(ctx.chatId);
151
140
  if (pending) {
@@ -156,8 +145,6 @@ export class WeixinBot extends Bot {
156
145
  }
157
146
  return true;
158
147
  }
159
- // Auto-clear any stale picker before starting a new interactive command
160
- // so the user isn't trapped behind a forgotten `/agent` / `/models` etc.
161
148
  const pendingPrompt = this.pendingHumanLoopPrompt(ctx.chatId);
162
149
  if (pendingPrompt) {
163
150
  this.humanLoopCancel(pendingPrompt.promptId, 'Cancelled — new command issued.');
@@ -292,9 +279,6 @@ export class WeixinBot extends Bot {
292
279
  }
293
280
  async cmdAgent(ctx, args) {
294
281
  if (args) {
295
- // Power-user shortcut: `/agent claude` switches directly without the
296
- // interactive picker — same behaviour as before, preserved for muscle
297
- // memory and scripted flows.
298
282
  try {
299
283
  const agent = normalizeAgent(args);
300
284
  this.switchAgentForChat(ctx.chatId, agent);
@@ -309,9 +293,6 @@ export class WeixinBot extends Bot {
309
293
  }
310
294
  async cmdModels(ctx, args) {
311
295
  if (args) {
312
- // Power-user shortcut: `/models <number|name>` switches directly. Skips
313
- // the multi-step picker (model + effort + Apply) used by the interactive
314
- // path; effort stays unchanged.
315
296
  const d = await getModelsListData(this, ctx.chatId);
316
297
  const idx = parseInt(args, 10);
317
298
  let modelId = null;
@@ -378,7 +359,6 @@ export class WeixinBot extends Bot {
378
359
  }
379
360
  const trimmed = args.trim();
380
361
  if (trimmed) {
381
- // Power-user shortcut: `/workspaces <#>` switches directly.
382
362
  const idx = parseInt(trimmed, 10);
383
363
  if (Number.isNaN(idx) || idx < 1 || idx > data.workspaces.length) {
384
364
  await ctx.reply(`Workspace #${trimmed} not found. Use /workspaces to list.`);
@@ -393,10 +373,6 @@ export class WeixinBot extends Bot {
393
373
  await ctx.reply(`Workdir switched:\n${oldPath}\n→ ${ws.path}`);
394
374
  return;
395
375
  }
396
- // Interactive picker — there is no `command-ui` builder for workspaces
397
- // (Telegram/Feishu render this via channel-local helpers), so we open a
398
- // tailored human-loop prompt here. Disabled rows for missing paths are
399
- // marked so users see them but can't pick.
400
376
  const taskId = `wxcmd-ws-${Date.now().toString(36)}`;
401
377
  const promptLines = [
402
378
  '【Workspaces】',
@@ -480,8 +456,6 @@ export class WeixinBot extends Bot {
480
456
  }
481
457
  const idx = parseInt(arg, 10);
482
458
  if (!isNaN(idx) && idx >= 1) {
483
- // Power-user shortcut: `/sessions <#>` jumps directly using the same
484
- // first-100 lookup the old text flow used.
485
459
  const d = await getSessionsPageData(this, ctx.chatId, 0, 100);
486
460
  const target = d.sessions[idx - 1];
487
461
  if (target) {
@@ -499,8 +473,6 @@ export class WeixinBot extends Bot {
499
473
  await ctx.reply(`Session #${idx} not found.`);
500
474
  return;
501
475
  }
502
- // Interactive list with pagination — `executeCommandAction` returns the
503
- // next page as a fresh view, so the loop re-renders without leaving.
504
476
  await this.runCommandUiLoop(ctx, () => buildSessionsCommandView(this, ctx.chatId, 0));
505
477
  }
506
478
  async cmdSkills(ctx) {
@@ -553,12 +525,6 @@ export class WeixinBot extends Bot {
553
525
  : ['Task failed.', result.error || result.message || 'Unknown error.'].filter(Boolean).join('\n');
554
526
  await this.channel.send(chatId, text);
555
527
  }
556
- /**
557
- * Format a human-in-the-loop prompt as plain text for WeChat (no card / button
558
- * support on personal accounts). Options are numbered so the user can either
559
- * reply with the number or freeform text — both are routed back through the
560
- * same handler in `handleMessage`.
561
- */
562
528
  formatHumanLoopPromptText(prompt) {
563
529
  const question = currentHumanLoopQuestion(prompt);
564
530
  const lines = [];
@@ -586,17 +552,10 @@ export class WeixinBot extends Bot {
586
552
  }
587
553
  return lines.join('\n');
588
554
  }
589
- /**
590
- * IM presenter for programmatic submissions (e.g. /goal-driven turns).
591
- * WeChat has no edit / card primitive, so streaming preview isn't possible
592
- * — the result is just posted once when the turn completes, mirroring
593
- * handleMessage's plain-text path.
594
- */
595
555
  async createImTaskPresenter(opts) {
596
556
  const chatId = String(opts.chatId);
597
557
  return {
598
558
  onText: () => {
599
- // No streaming render in WeChat — text accumulates and is sent once.
600
559
  },
601
560
  onSuccess: async (result) => {
602
561
  await this.sendResult(chatId, result);
@@ -621,11 +580,6 @@ export class WeixinBot extends Bot {
621
580
  this.log(`weixin renderInteractionPrompt failed: ${describeError(error)}`);
622
581
  }
623
582
  }
624
- /**
625
- * WeChat has no message-edit primitive, so the original prompt text stays
626
- * frozen in chat history. The echo message is the only way to record the
627
- * decision — keep it short and prefix-marked so it's easy to skim.
628
- */
629
583
  async onInteractionAnswered(prompt, summary) {
630
584
  const text = buildInteractionEchoPlain(summary);
631
585
  if (!text)
@@ -637,11 +591,6 @@ export class WeixinBot extends Bot {
637
591
  this.log(`weixin onInteractionAnswered echo failed: ${describeError(error)}`);
638
592
  }
639
593
  }
640
- /**
641
- * If the incoming text looks like an option number (e.g. "1", "2.", "③"),
642
- * resolve it against the current question's options. Otherwise return null
643
- * and let the freeform path take over.
644
- */
645
594
  parseHumanLoopOptionPick(text, prompt) {
646
595
  const question = currentHumanLoopQuestion(prompt);
647
596
  if (!question?.options?.length)
@@ -656,14 +605,6 @@ export class WeixinBot extends Bot {
656
605
  const opt = question.options[idx - 1];
657
606
  return opt?.value || opt?.label || null;
658
607
  }
659
- // ---- Command-UI adapter (numbered-text fallback for non-card IMs) -------
660
- //
661
- // Telegram / Feishu render `CommandSelectionView` as tap-able button cards.
662
- // WeChat (personal accounts) can't render cards, so we flatten each view's
663
- // button rows into a numbered text prompt and let the user reply with a
664
- // digit. Re-uses the SAME `executeCommandAction` data layer — no parallel
665
- // logic — so `/agents` / `/models` / `/sessions` / `/skills` / `/mode`
666
- // behave identically across channels, just rendered differently.
667
608
  decorateCommandButtonLabel(button) {
668
609
  let label = button.label.trim();
669
610
  if (button.state === 'current' || button.primary)
@@ -674,11 +615,6 @@ export class WeixinBot extends Bot {
674
615
  label += ' [n/a]';
675
616
  return label;
676
617
  }
677
- /**
678
- * Render a `CommandSelectionView` as plain WeChat text: title + meta lines +
679
- * detailed item list (when present) + numbered button list. Always ends with
680
- * "回复编号选择" so the user knows what to do next.
681
- */
682
618
  formatCommandViewText(view, buttons) {
683
619
  const lines = [];
684
620
  lines.push(`【${view.title}】`);
@@ -717,12 +653,6 @@ export class WeixinBot extends Bot {
717
653
  parts.push(notice.detail);
718
654
  return parts.join('\n');
719
655
  }
720
- /**
721
- * Open one prompt for a `CommandSelectionView` and resolve once the user
722
- * picks an option (or cancels). The picked CommandAction is executed via the
723
- * shared `executeCommandAction` so the side effects (state mutations,
724
- * notices) match every other channel exactly.
725
- */
726
656
  async promptCommandView(ctx, view) {
727
657
  const buttons = view.rows.flat();
728
658
  if (!buttons.length) {
@@ -779,13 +709,6 @@ export class WeixinBot extends Bot {
779
709
  .catch(() => resolve(null));
780
710
  });
781
711
  }
782
- /**
783
- * Multi-step command flow driver. Some views (notably `/models`'s "select
784
- * model → set effort → Apply") return another `CommandSelectionView` from
785
- * `executeCommandAction` rather than a terminal notice; this loop keeps
786
- * re-prompting until we hit a notice / skill / noop / cancellation. Capped
787
- * at 12 iterations to guard against pathological loops.
788
- */
789
712
  async runCommandUiLoop(ctx, viewBuilder) {
790
713
  let view = await Promise.resolve(viewBuilder());
791
714
  let safety = 12;
@@ -801,9 +724,6 @@ export class WeixinBot extends Bot {
801
724
  await ctx.reply(this.formatCommandNotice(result.notice));
802
725
  return;
803
726
  case 'skill':
804
- // Dispatch the resolved skill prompt back through the regular task
805
- // pipeline (same machinery as a typed message), so the agent picks
806
- // it up with the right session + workspace.
807
727
  await ctx.reply(`Running /${result.skillName}…`);
808
728
  await this.dispatchUserPrompt(ctx, result.prompt, []);
809
729
  return;
@@ -815,11 +735,6 @@ export class WeixinBot extends Bot {
815
735
  if (safety <= 0)
816
736
  await ctx.reply('Command UI loop terminated (too many steps).');
817
737
  }
818
- /**
819
- * Submit a user-authored prompt through the same path `handleMessage` uses
820
- * for natural-language messages. Lets the command-UI loop forward a resolved
821
- * `/skill` expansion as if the user had typed it.
822
- */
823
738
  async dispatchUserPrompt(ctx, text, files) {
824
739
  const session = this.resolveSession(ctx.chatId, text, files);
825
740
  const prompt = buildPrompt(text, files);
@@ -879,8 +794,6 @@ export class WeixinBot extends Bot {
879
794
  await ctx.reply('This Weixin channel currently supports text input only.');
880
795
  return;
881
796
  }
882
- // Active human-in-the-loop prompt takes priority — text-only replies are
883
- // routed back to the pending question (option-number pick OR freeform).
884
797
  const pendingPrompt = this.pendingHumanLoopPrompt(ctx.chatId);
885
798
  if (pendingPrompt && text && !msg.files.length) {
886
799
  const optionValue = this.parseHumanLoopOptionPick(text, pendingPrompt);
@@ -892,7 +805,6 @@ export class WeixinBot extends Bot {
892
805
  return;
893
806
  }
894
807
  if (result.completed) {
895
- // Closing message comes from onInteractionAnswered — no extra reply.
896
808
  }
897
809
  else if (result.advanced) {
898
810
  const next = this.humanLoopPrompt(pendingPrompt.promptId);
@@ -1,6 +1,3 @@
1
- /**
2
- * WeChat channel transport.
3
- */
4
1
  import { Channel, DEFAULT_CHANNEL_CAPABILITIES, splitText, sleep, } from '../base.js';
5
2
  import { WEIXIN_LIMITS } from '../../core/constants.js';
6
3
  import { ChannelHealth } from '../health.js';
@@ -82,9 +79,6 @@ export class WeixinChannel extends Channel {
82
79
  });
83
80
  if (response.get_updates_buf !== undefined)
84
81
  this.updateBuf = response.get_updates_buf || '';
85
- // Errcode check BEFORE recordSuccess(): an HTTP 200 carrying a
86
- // business error code (e.g. "session timeout") must not silently
87
- // reset the exponential backoff.
88
82
  if ((response.ret ?? 0) !== 0 || (response.errcode ?? 0) !== 0) {
89
83
  throw new Error(`Weixin getupdates failed: ${response.errmsg || response.errcode || response.ret}`);
90
84
  }
@@ -1,26 +1,3 @@
1
- /**
2
- * macOS LaunchAgent integration for `pikiloom --daemon`.
3
- *
4
- * Every time the user runs pikiloom with an explicit `--daemon` flag on
5
- * macOS *without* a LaunchAgent already installed, an osascript dialog asks
6
- * whether to enable login auto-start. Choosing Enable writes
7
- * `~/Library/LaunchAgents/ai.pikiloom.gateway.plist` and loads it via
8
- * `launchctl bootstrap`. There is intentionally no CLI to disable: the user
9
- * toggles it off under System Settings → General → Login Items, which is
10
- * where macOS surfaces every LaunchAgent installed in
11
- * `~/Library/LaunchAgents`.
12
- *
13
- * Decision flow (`maybePromptAutostart`):
14
- * 1. Non-darwin → no-op.
15
- * 2. Already running under launchd (PIKILOOM_FROM_LAUNCHD set) → no-op.
16
- * 3. plist exists but its ProgramArguments no longer point to a valid
17
- * binary (e.g. Homebrew migration moved node) → silently rewrite.
18
- * 4. plist already valid → no-op.
19
- * 5. Non-interactive (no TTY, CI=1) → no-op.
20
- * 6. Otherwise → show dialog after a short delay so the bot is already
21
- * live when the user sees it. "Not now" silently dismisses; the next
22
- * `--daemon` run will ask again.
23
- */
24
1
  import { exec, execFile } from 'node:child_process';
25
2
  import fs from 'node:fs';
26
3
  import os from 'node:os';
@@ -33,16 +10,9 @@ const PLIST_LABEL = 'ai.pikiloom.gateway';
33
10
  const PLIST_DIR = path.join(os.homedir(), 'Library', 'LaunchAgents');
34
11
  const PLIST_PATH = path.join(PLIST_DIR, `${PLIST_LABEL}.plist`);
35
12
  const PIKILOOM_HOME = path.join(os.homedir(), STATE_DIR_NAME);
36
- // Pre-rename LaunchAgent (pikiclaw) — removed on (re)install so an upgraded
37
- // machine never runs two daemons. Delete a couple releases post-rename.
38
13
  const LEGACY_PLIST_LABELS = ['ai.pikiclaw.gateway'];
39
14
  const PROMPT_DELAY_MS = 3000;
40
15
  export const FROM_LAUNCHD_ENV = 'PIKILOOM_FROM_LAUNCHD';
41
- /**
42
- * Resolve the command used to launch pikiloom, so the plist can re-launch it
43
- * the same way. We distinguish npx (`.../_npx/<hash>/.../main.js`) from a
44
- * globally installed binary (`pikiloom` on PATH).
45
- */
46
16
  function detectInvocation() {
47
17
  const entry = process.argv[1] || '';
48
18
  const userArgs = process.argv.slice(2);
@@ -146,15 +116,12 @@ async function showEnableDialog() {
146
116
  return 'closed';
147
117
  }
148
118
  catch {
149
- // User pressed Cmd-. or closed the dialog — osascript exits non-zero.
150
119
  return 'closed';
151
120
  }
152
121
  }
153
122
  async function bootstrapLaunchAgent(log) {
154
123
  const uid = process.getuid?.() ?? 0;
155
124
  const domain = `gui/${uid}`;
156
- // bootout first so a reinstall replaces any previously-loaded copy. We
157
- // swallow errors here since bootout fails harmlessly when nothing is loaded.
158
125
  await new Promise(resolve => {
159
126
  exec(`launchctl bootout ${domain}/${PLIST_LABEL}`, () => resolve());
160
127
  });
@@ -167,11 +134,6 @@ async function bootstrapLaunchAgent(log) {
167
134
  return false;
168
135
  }
169
136
  }
170
- /**
171
- * Remove the pre-rename LaunchAgent (`ai.pikiclaw.gateway`) if present, so an
172
- * upgraded install never runs two daemons. Best-effort; safe when nothing is
173
- * loaded. Delete a couple releases post-rename.
174
- */
175
137
  async function cleanupLegacyAutostart(log) {
176
138
  const uid = process.getuid?.() ?? 0;
177
139
  for (const label of LEGACY_PLIST_LABELS) {
@@ -193,7 +155,7 @@ async function cleanupLegacyAutostart(log) {
193
155
  log(`autostart: removed legacy LaunchAgent ${label}`);
194
156
  }
195
157
  }
196
- catch { /* best-effort */ }
158
+ catch { }
197
159
  }
198
160
  }
199
161
  async function installAutostart(log, invocation) {
@@ -213,10 +175,6 @@ async function installAutostart(log, invocation) {
213
175
  }
214
176
  return loaded;
215
177
  }
216
- /**
217
- * Top-level entry called once from the watchdog process when the user passed
218
- * an explicit `--daemon` flag. Fires and forgets; never throws.
219
- */
220
178
  export function maybePromptAutostart(log) {
221
179
  if (process.platform !== 'darwin')
222
180
  return;
@@ -250,7 +208,6 @@ export function maybePromptAutostart(log) {
250
208
  })();
251
209
  }, PROMPT_DELAY_MS).unref?.();
252
210
  }
253
- // ─── exports for tests ─────────────────────────────────────────────────
254
211
  export const __test = {
255
212
  PLIST_LABEL,
256
213
  PLIST_PATH,
@@ -1,20 +1,3 @@
1
- /**
2
- * channel-supervisor.ts — Owns the set of running IM channel bots and
3
- * reconciles it against the current user config without restarting the
4
- * pikiloom process.
5
- *
6
- * Subscribes to onUserConfigChange. Whenever the channels array or any
7
- * channel's credentials change, the supervisor stops the affected bot,
8
- * disposes its transport, and (if still configured) launches a fresh
9
- * instance with the new config — all in-process, leaving the dashboard,
10
- * other channels, and active coding sessions on unaffected bots running.
11
- *
12
- * Dashboard-as-terminal: the Web Dashboard is a first-class terminal, equal to
13
- * the IM channels. When no IM channel is configured, the supervisor keeps a
14
- * headless bot attached to the dashboard so it's a fully usable terminal on its
15
- * own; when an IM channel is added, that channel's bot takes over the dashboard
16
- * attachment and the headless bot is torn down.
17
- */
18
1
  import { loadUserConfig, onUserConfigChange } from '../core/config/user-config.js';
19
2
  import { resolveConfiguredChannels } from './channels.js';
20
3
  const CHANNEL_AFFECTING_KEYS = new Set([
@@ -35,26 +18,10 @@ const CHANNEL_AFFECTING_KEYS = new Set([
35
18
  'wecomBotSecret',
36
19
  'wecomEndpoint',
37
20
  ]);
38
- /**
39
- * How long to wait between stopping a channel and starting a replacement
40
- * with new credentials. Some IM providers hold the old connection slot
41
- * server-side for a moment after we close the socket; if a new client
42
- * with the same credentials connects too quickly the server rejects with
43
- * "system busy" and the SDK ends up in a broken state.
44
- *
45
- * Feishu's lark SDK is the known offender — its WSClient handshake races
46
- * with a "PingInterval" setup that crashes when the server rejects the
47
- * second connect. Telegram (long-poll over HTTP) and Weixin (HTTP long-
48
- * poll) tolerate immediate replacement.
49
- */
50
21
  const CHANNEL_REPLACE_SETTLE_MS = {
51
22
  telegram: 0,
52
23
  weixin: 0,
53
24
  feishu: 5_000,
54
- // The Slack / Discord / DingTalk / WeChat-Work providers all hold the
55
- // single-bot socket slot for a moment after disconnect; give them a
56
- // small grace window so token rotation doesn't trip "already connected"
57
- // style errors on the immediate replacement connection.
58
25
  slack: 3_000,
59
26
  discord: 3_000,
60
27
  dingtalk: 3_000,
@@ -140,7 +107,6 @@ function describeError(error) {
140
107
  }
141
108
  export class ChannelSupervisor {
142
109
  running = new Map();
143
- /** Headless dashboard-terminal bot, present only while no IM channel runs. */
144
110
  headless = null;
145
111
  dashboard;
146
112
  log;
@@ -152,10 +118,6 @@ export class ChannelSupervisor {
152
118
  this.dashboard = opts.dashboard;
153
119
  this.log = opts.log;
154
120
  }
155
- /**
156
- * Initial launch + subscribe to config changes. Idempotent: calling
157
- * twice is a no-op for the second call.
158
- */
159
121
  async start() {
160
122
  if (this.started)
161
123
  return;
@@ -167,7 +129,6 @@ export class ChannelSupervisor {
167
129
  void this.reconcile(config);
168
130
  });
169
131
  }
170
- /** Stop every running channel and unsubscribe from config changes. */
171
132
  async stop() {
172
133
  this.unsubscribe?.();
173
134
  this.unsubscribe = null;
@@ -175,7 +136,6 @@ export class ChannelSupervisor {
175
136
  await Promise.all(channels.map(channel => this.stopChannel(channel)));
176
137
  await this.stopHeadless();
177
138
  }
178
- /** Reconcile running bots against the desired channel set + credentials. */
179
139
  async reconcile(config) {
180
140
  if (this.reconcileInFlight) {
181
141
  this.reconcilePending = true;
@@ -189,8 +149,6 @@ export class ChannelSupervisor {
189
149
  await this.doReconcile(current);
190
150
  if (!this.reconcilePending)
191
151
  break;
192
- // Newer config arrived during reconcile — re-read from disk so the
193
- // next pass acts on the latest state.
194
152
  current = loadUserConfig();
195
153
  }
196
154
  }
@@ -201,7 +159,6 @@ export class ChannelSupervisor {
201
159
  async doReconcile(config) {
202
160
  const desired = resolveConfiguredChannels({ config });
203
161
  const desiredSet = new Set(desired);
204
- // Stop channels that are no longer desired OR whose credentials changed.
205
162
  const toStop = [];
206
163
  for (const [channel, entry] of this.running) {
207
164
  if (!desiredSet.has(channel)) {
@@ -215,18 +172,12 @@ export class ChannelSupervisor {
215
172
  if (toStop.length) {
216
173
  await Promise.all(toStop.map(channel => this.stopChannel(channel)));
217
174
  }
218
- // For channels being replaced (still desired, just stopped above for
219
- // credential rotation), wait the per-channel settle delay so the
220
- // remote server has time to release the old connection slot before
221
- // we open a new one.
222
175
  const replacing = toStop.filter(channel => desiredSet.has(channel));
223
176
  const settleMs = replacing.reduce((max, channel) => Math.max(max, CHANNEL_REPLACE_SETTLE_MS[channel] ?? 0), 0);
224
177
  if (settleMs > 0) {
225
178
  this.log(`waiting ${settleMs}ms for ${replacing.join(', ')} to settle before relaunch`);
226
179
  await sleep(settleMs);
227
180
  }
228
- // Start channels that should be running but aren't (either added or
229
- // just stopped above due to credential rotation).
230
181
  for (const channel of desired) {
231
182
  if (this.running.has(channel))
232
183
  continue;
@@ -237,11 +188,6 @@ export class ChannelSupervisor {
237
188
  this.log(`channel ${channel}: failed to start — ${describeError(err)}`);
238
189
  }
239
190
  }
240
- // Dashboard-as-terminal: keep a headless bot attached whenever no channel
241
- // bot is actually running (none configured, or all failed to start), so the
242
- // dashboard stays a usable terminal on its own. Gate on `running.size`
243
- // rather than `desired.length` so a channel that fails to come up still
244
- // leaves the dashboard with a working bot.
245
191
  if (this.running.size > 0) {
246
192
  await this.stopHeadless();
247
193
  }
@@ -249,7 +195,6 @@ export class ChannelSupervisor {
249
195
  await this.startHeadless();
250
196
  }
251
197
  }
252
- /** Launch the headless dashboard-terminal bot and attach it to the dashboard. */
253
198
  async startHeadless() {
254
199
  if (this.headless)
255
200
  return;
@@ -263,7 +208,6 @@ export class ChannelSupervisor {
263
208
  });
264
209
  this.headless = { bot, runPromise };
265
210
  }
266
- /** Tear down the headless bot once a real channel bot owns the dashboard. */
267
211
  async stopHeadless() {
268
212
  if (!this.headless)
269
213
  return;
@@ -274,7 +218,7 @@ export class ChannelSupervisor {
274
218
  try {
275
219
  await runPromise;
276
220
  }
277
- catch { /* already logged */ }
221
+ catch { }
278
222
  }
279
223
  async startChannel(channel, config) {
280
224
  this.log(`channel ${channel}: starting`);
@@ -295,8 +239,6 @@ export class ChannelSupervisor {
295
239
  if (!entry)
296
240
  return;
297
241
  this.log(`channel ${channel}: stopping`);
298
- // Remove from the map up-front so a concurrent reconcile pass doesn't
299
- // try to stop the same instance twice.
300
242
  this.running.delete(channel);
301
243
  try {
302
244
  entry.bot.requestStop();
@@ -1,11 +1,5 @@
1
- /**
2
- * Channel resolution helpers for CLI startup.
3
- */
4
1
  import { applyChannelEnvFallback } from '../core/config/user-config.js';
5
2
  export function hasConfiguredChannelToken(rawConfig, channel, tokenOverride) {
6
- // Channel transports already fall back to env vars at runtime
7
- // (e.g. TELEGRAM_BOT_TOKEN). Mirror that here so a docker-only env setup
8
- // doesn't make startup think there's nothing configured.
9
3
  const config = applyChannelEnvFallback(rawConfig);
10
4
  switch (channel) {
11
5
  case 'telegram':