termdock 1.4.69 → 1.4.71

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 (84) hide show
  1. package/.desktop-dist/main.js +254 -49
  2. package/README.md +6 -0
  3. package/dist/client/assets/{ModelPreview-Bk8GM7G-.js → ModelPreview-CG6vLwyw.js} +1 -1
  4. package/dist/client/assets/{abnfDiagram-VRR7QNED-BYiQXGYk.js → abnfDiagram-VRR7QNED-CrXEZLWX.js} +1 -1
  5. package/dist/client/assets/{arc-jlT_jmoZ.js → arc-DbKGR7Ex.js} +1 -1
  6. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-D1qZ875Q.js → architectureDiagram-ZJ3FMSHR-PF5BoUbI.js} +1 -1
  7. package/dist/client/assets/{blockDiagram-677ZJIJ3-qkgn2Ysc.js → blockDiagram-677ZJIJ3-D1gRzVWB.js} +1 -1
  8. package/dist/client/assets/{c4Diagram-LMCZKHZV-JgcH49Pf.js → c4Diagram-LMCZKHZV-CwvOLync.js} +1 -1
  9. package/dist/client/assets/channel-JTEuKVy6.js +1 -0
  10. package/dist/client/assets/{chunk-2Q5K7J3B-E-3iflEy.js → chunk-2Q5K7J3B-BeRdP-xP.js} +1 -1
  11. package/dist/client/assets/{chunk-32BRIVSS-mM01y3kF.js → chunk-32BRIVSS-CrqR5rvl.js} +1 -1
  12. package/dist/client/assets/{chunk-5VM5RSS4-GO8Muo5l.js → chunk-5VM5RSS4-CjbUr-ko.js} +1 -1
  13. package/dist/client/assets/{chunk-EX3LRPZG-BUSDluH0.js → chunk-EX3LRPZG-BVEk3Wma.js} +1 -1
  14. package/dist/client/assets/{chunk-JWPE2WC7-obSL6EzJ.js → chunk-JWPE2WC7-WitJe8kR.js} +1 -1
  15. package/dist/client/assets/{chunk-MOJQB5TN-zEktbtAG.js → chunk-MOJQB5TN-BCewlLYk.js} +1 -1
  16. package/dist/client/assets/{chunk-RYQCIY6F-kH7H8XgQ.js → chunk-RYQCIY6F-CoP0AA4G.js} +1 -1
  17. package/dist/client/assets/{chunk-V7JOEXUC-BqDFpHKN.js → chunk-V7JOEXUC-CSZrnmp_.js} +1 -1
  18. package/dist/client/assets/{chunk-VR4S4FIN-BGZ7kCIG.js → chunk-VR4S4FIN-BB-7zz-9.js} +1 -1
  19. package/dist/client/assets/{chunk-XXDRQBXY-D-TatWXe.js → chunk-XXDRQBXY-CcZZlCik.js} +1 -1
  20. package/dist/client/assets/classDiagram-OUVF2IWQ-BiFJB_5s.js +1 -0
  21. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BiFJB_5s.js +1 -0
  22. package/dist/client/assets/{cose-bilkent-JH36ORCC-CfWyrViz.js → cose-bilkent-JH36ORCC-DcmzUDwh.js} +1 -1
  23. package/dist/client/assets/{cynefin-VYW2F7L2-BFdZ3FcE.js → cynefin-VYW2F7L2-C1XlDVoJ.js} +1 -1
  24. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-iAfCVoFU.js → cynefinDiagram-TSTJHNR4-zY6Aet2S.js} +1 -1
  25. package/dist/client/assets/{dagre-VKFMJZFB-CZhh0hrr.js → dagre-VKFMJZFB-DqGTi6zR.js} +1 -1
  26. package/dist/client/assets/{diagram-FQU43EPY-BqUK2OmV.js → diagram-FQU43EPY-DStcsU7S.js} +1 -1
  27. package/dist/client/assets/{diagram-G47NLZAW-vC0pSQYC.js → diagram-G47NLZAW-Cs6IMNip.js} +1 -1
  28. package/dist/client/assets/{diagram-NH7WQ7WH-Dqc5oxed.js → diagram-NH7WQ7WH-yA2bpWN7.js} +1 -1
  29. package/dist/client/assets/{diagram-OA4YK3LP-DIIXckFE.js → diagram-OA4YK3LP-Dzf9M0PI.js} +1 -1
  30. package/dist/client/assets/{diagram-WEI45ONY-DR9_cFeh.js → diagram-WEI45ONY-D75lpQKA.js} +1 -1
  31. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-21OO0OXC.js → ebnfDiagram-CCIWWBDH-CkQvIpWy.js} +1 -1
  32. package/dist/client/assets/{erDiagram-Q63AITRT-YKnVG3az.js → erDiagram-Q63AITRT-Ca00Vjxz.js} +1 -1
  33. package/dist/client/assets/{flowDiagram-23GEKE2U-Cm7TGErF.js → flowDiagram-23GEKE2U-CGrnSTEp.js} +1 -1
  34. package/dist/client/assets/{ganttDiagram-NO4QXBWP-D_76Effz.js → ganttDiagram-NO4QXBWP-0SHfOIP4.js} +1 -1
  35. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CC1ysmVD.js → gitGraphDiagram-IHSO6WYX-BeigYWvs.js} +1 -1
  36. package/dist/client/assets/index-D5PKdbhW.js +451 -0
  37. package/dist/client/assets/index-DhfIiBLW.css +1 -0
  38. package/dist/client/assets/{infoDiagram-FWYZ7A6U-BbgpmhBz.js → infoDiagram-FWYZ7A6U-CiY1-xjN.js} +1 -1
  39. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-C94bY_je.js → ishikawaDiagram-FXEZZL3T-CmtDjUm_.js} +1 -1
  40. package/dist/client/assets/{journeyDiagram-5HDEW3XC-BQ0iFRxs.js → journeyDiagram-5HDEW3XC-B7qOr9LY.js} +1 -1
  41. package/dist/client/assets/{kanban-definition-HUTT4EX6-BtAexXPo.js → kanban-definition-HUTT4EX6-DO2P2Um3.js} +1 -1
  42. package/dist/client/assets/{linear-C2HnIdOK.js → linear-B832hJt0.js} +1 -1
  43. package/dist/client/assets/{mermaid.core-BbG4sJBF.js → mermaid.core-CsAzdiCU.js} +4 -4
  44. package/dist/client/assets/{mindmap-definition-LN4V7U3C-B-uvViws.js → mindmap-definition-LN4V7U3C-HI__vXhQ.js} +1 -1
  45. package/dist/client/assets/{pegDiagram-2B236MQR-DHYcR6tE.js → pegDiagram-2B236MQR--rZIwjXq.js} +1 -1
  46. package/dist/client/assets/{pieDiagram-ENE6RG2P-DEE9cD6M.js → pieDiagram-ENE6RG2P-D9jPf5x9.js} +1 -1
  47. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DMQP5jG3.js → quadrantDiagram-ABIIQ3AL-DTCP5ji2.js} +1 -1
  48. package/dist/client/assets/{railroadDiagram-RFXS5EU6-B-4WDTwc.js → railroadDiagram-RFXS5EU6-D2Gspvrl.js} +1 -1
  49. package/dist/client/assets/{requirementDiagram-TGXJPOKE-C2xw_q-R.js → requirementDiagram-TGXJPOKE-DEJC02IW.js} +1 -1
  50. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-wq0BPqhc.js → sankeyDiagram-HTMAVEWB-opOhKPap.js} +1 -1
  51. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-BqpM0A9d.js → sequenceDiagram-DBY2YBRQ-_sz2CRAG.js} +1 -1
  52. package/dist/client/assets/{sizeCapture-X5ZJPWSS-Ba2xxyp_.js → sizeCapture-X5ZJPWSS-D0Q2QkYF.js} +1 -1
  53. package/dist/client/assets/{stateDiagram-2N3HPSRC-agPx5qAN.js → stateDiagram-2N3HPSRC-VgeSQTtW.js} +1 -1
  54. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BVh2FRO-.js +1 -0
  55. package/dist/client/assets/{swimlanes-5IMT3BWC-Cy6Lzj_L.js → swimlanes-5IMT3BWC-ClvBvSl0.js} +2 -2
  56. package/dist/client/assets/swimlanesDiagram-G3AALYLV-C17V-kgm.js +8 -0
  57. package/dist/client/assets/{timeline-definition-FHXFAJF6-LBrVfbdp.js → timeline-definition-FHXFAJF6-BXNsrECV.js} +1 -1
  58. package/dist/client/assets/{vennDiagram-L72KCM5P-BiYIV7mX.js → vennDiagram-L72KCM5P-BgwiqZd-.js} +1 -1
  59. package/dist/client/assets/{wardleyDiagram-EHGQE667-Q1QU9gG3.js → wardleyDiagram-EHGQE667-DiqN5ERx.js} +1 -1
  60. package/dist/client/assets/{xychartDiagram-FW5EYKEG-CzYpxk4l.js → xychartDiagram-FW5EYKEG-DQjFVqC6.js} +1 -1
  61. package/dist/client/index.html +2 -2
  62. package/dist/client/sw.js +1 -1
  63. package/dist/server/agent/autoTitle.js +95 -0
  64. package/dist/server/agent/installers.js +106 -22
  65. package/dist/server/agent/plugins.js +135 -4
  66. package/dist/server/agent/registry.js +2 -1
  67. package/dist/server/agent/session.js +27 -2
  68. package/dist/server/agent/titleNamerCatalog.js +301 -0
  69. package/dist/server/agentHook.js +49 -1
  70. package/dist/server/cli.js +238 -30
  71. package/dist/server/routes/filesystem.js +11 -8
  72. package/dist/server/routes/terminal.js +382 -14
  73. package/dist/server/utils/autoUpdate.js +175 -0
  74. package/dist/server/utils/npmUpdate.js +2 -1
  75. package/dist/server/utils/settings.js +43 -0
  76. package/package.json +1 -1
  77. package/runtime-manifest.json +2 -2
  78. package/dist/client/assets/channel-C2i7H7bH.js +0 -1
  79. package/dist/client/assets/classDiagram-OUVF2IWQ-BlGG6XFj.js +0 -1
  80. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BlGG6XFj.js +0 -1
  81. package/dist/client/assets/index-ChhyERKX.css +0 -1
  82. package/dist/client/assets/index-r-PkH7SB.js +0 -442
  83. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-beoQXpWJ.js +0 -1
  84. package/dist/client/assets/swimlanesDiagram-G3AALYLV-BxL1xNhC.js +0 -8
@@ -16,7 +16,7 @@
16
16
  import fs from 'node:fs';
17
17
  import os from 'node:os';
18
18
  import path from 'node:path';
19
- import { execFile } from 'node:child_process';
19
+ import { execFile, spawn } from 'node:child_process';
20
20
  import { agentBySlug, isPluginAgent } from './registry.js';
21
21
  import { loadPlugins, resolveHookTarget } from './plugins.js';
22
22
  export const HOOK_AGENTS = ['claude', 'codex', 'copilot', 'opencode', 'pi', 'grok', 'kimi'];
@@ -121,13 +121,14 @@ const CLAUDE_HOOK_EVENTS = [
121
121
  ['Stop', 'stop'],
122
122
  ['SessionEnd', 'session-end'],
123
123
  ];
124
- /** Codex's hook events (~/.codex/hooks.json, Claude-shaped). Codex is
125
- * turn-level only: no Notification hook, and no SessionEnd — the pane's
126
- * foreground detection clears the badge when Codex exits. */
124
+ /** Codex's native hooks.json event vocabulary. */
127
125
  const CODEX_HOOK_EVENTS = [
128
126
  ['SessionStart', 'session-start'],
129
127
  ['UserPromptSubmit', 'prompt-submit'],
128
+ ['PermissionRequest', 'permission-request'],
129
+ ['PostToolUse', 'tool-complete'],
130
130
  ['Stop', 'stop'],
131
+ ['SessionEnd', 'session-end'],
131
132
  ];
132
133
  /**
133
134
  * Kimi Code's hook events (~/.kimi-code/config.toml `[[hooks]]` entries, TOML).
@@ -620,6 +621,73 @@ export function hooksState(agent) {
620
621
  return 'not-installed';
621
622
  return ownedFileState(file, expected, marker(agent));
622
623
  }
624
+ let codexRuntimeStateCache = null;
625
+ /** Query Codex's own hook registry: a structurally present hook can still be
626
+ * quarantined until the user approves its current hash in `/hooks`. */
627
+ async function codexRuntimeHooksState() {
628
+ const structural = hooksState('codex');
629
+ if (structural !== 'installed')
630
+ return structural;
631
+ if (codexRuntimeStateCache && Date.now() - codexRuntimeStateCache.at < 30_000) {
632
+ return codexRuntimeStateCache.state;
633
+ }
634
+ const state = await new Promise((resolve) => {
635
+ const child = spawn('codex', ['app-server', '--stdio'], {
636
+ stdio: ['pipe', 'pipe', 'ignore'],
637
+ env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
638
+ });
639
+ let buffer = '';
640
+ let settled = false;
641
+ const finish = (value) => {
642
+ if (settled)
643
+ return;
644
+ settled = true;
645
+ clearTimeout(timer);
646
+ child.kill();
647
+ resolve(value);
648
+ };
649
+ const timer = setTimeout(() => finish(structural), 5_000);
650
+ timer.unref?.();
651
+ child.on('error', () => finish(structural));
652
+ child.stdout.on('data', (chunk) => {
653
+ buffer += chunk.toString();
654
+ for (;;) {
655
+ const newline = buffer.indexOf('\n');
656
+ if (newline < 0)
657
+ break;
658
+ const line = buffer.slice(0, newline).trim();
659
+ buffer = buffer.slice(newline + 1);
660
+ if (!line)
661
+ continue;
662
+ let message;
663
+ try {
664
+ message = JSON.parse(line);
665
+ }
666
+ catch {
667
+ continue;
668
+ }
669
+ if (message.id === 1) {
670
+ child.stdin.write(`${JSON.stringify({ method: 'initialized', params: {} })}\n`);
671
+ child.stdin.write(`${JSON.stringify({ id: 2, method: 'hooks/list', params: { cwds: [process.cwd()] } })}\n`);
672
+ }
673
+ else if (message.id === 2) {
674
+ const ours = (message.result?.data ?? [])
675
+ .flatMap((entry) => entry.hooks ?? [])
676
+ .filter((hook) => typeof hook.command === 'string' && hook.command.includes(marker('codex')));
677
+ if (ours.length < CODEX_HOOK_EVENTS.length)
678
+ finish('outdated');
679
+ else if (ours.some((hook) => hook.enabled === false || !['trusted', 'managed'].includes(hook.trustStatus ?? '')))
680
+ finish('needs-approval');
681
+ else
682
+ finish('installed');
683
+ }
684
+ }
685
+ });
686
+ child.stdin.write(`${JSON.stringify({ id: 1, method: 'initialize', params: { clientInfo: { name: 'termdock', version: '1' } } })}\n`);
687
+ });
688
+ codexRuntimeStateCache = { at: Date.now(), state };
689
+ return state;
690
+ }
623
691
  export function listHookAgents() {
624
692
  return HOOK_AGENTS.map((slug) => ({
625
693
  slug,
@@ -641,9 +709,10 @@ export async function installHooks(agent) {
641
709
  }
642
710
  if (agent === 'codex') {
643
711
  hookMapInstall(file, agent, CODEX_HOOK_EVENTS);
712
+ codexRuntimeStateCache = null;
644
713
  try {
645
714
  await enableCodexHooksFeature();
646
- return { summary: 'Installed', devMode };
715
+ return { summary: 'Installed — open /hooks in Codex and approve any new Termdock hooks', devMode };
647
716
  }
648
717
  catch (error) {
649
718
  return {
@@ -668,6 +737,8 @@ export async function installHooks(agent) {
668
737
  export async function uninstallHooks(agent) {
669
738
  const file = targetPath(agent);
670
739
  if (agent === 'claude' || agent === 'codex') {
740
+ if (agent === 'codex')
741
+ codexRuntimeStateCache = null;
671
742
  return hookMapUninstall(file, agent);
672
743
  }
673
744
  if (agent === 'kimi') {
@@ -681,7 +752,7 @@ function pluginHookEntries() {
681
752
  return plugins
682
753
  .filter((p) => p.manifest.hooks && p.manifest.hooks.events.length > 0)
683
754
  .map((p) => {
684
- const events = p.manifest.hooks.events.map((e) => [e.hook, e.event, e.matcher ?? null, e.timeout]);
755
+ const events = p.manifest.hooks.events.map((e) => [e.hook, e.event, e.matcher ?? null, e.timeout, e.status]);
685
756
  return {
686
757
  slug: p.manifest.slug,
687
758
  displayName: p.manifest.displayName,
@@ -691,15 +762,16 @@ function pluginHookEntries() {
691
762
  });
692
763
  }
693
764
  /** Hook command for a plugin agent. Uses the same emitter script but with the plugin slug. */
694
- function hookCommandForPlugin(slug, event) {
765
+ function hookCommandForPlugin(slug, event, status) {
695
766
  const { script } = resolveHookScript();
696
- return `"${process.execPath}" "${script}" agent-hook ${slug} ${event}`;
767
+ const statusArg = status ? ` ${status}` : '';
768
+ return `"${process.execPath}" "${script}" agent-hook ${slug} ${event}${statusArg}`;
697
769
  }
698
770
  /**
699
771
  * List all agents with hook support — both built-in and plugin-defined.
700
772
  * Plugin agents appear after built-in agents.
701
773
  */
702
- export function listAllHookAgents() {
774
+ export async function listAllHookAgents() {
703
775
  const builtIn = HOOK_AGENTS.map((slug) => {
704
776
  const agent = agentBySlug(slug);
705
777
  return {
@@ -719,13 +791,16 @@ export function listAllHookAgents() {
719
791
  slug: entry.slug,
720
792
  displayName: entry.displayName,
721
793
  targetDisplay: abbreviateHome(entry.targetPath),
722
- state: pluginHooksState(entry.slug, entry.targetPath, entry.events.map(([h, s]) => [h, s])),
794
+ state: pluginHooksState(entry.slug, entry.targetPath, entry.events),
723
795
  accentColor: agent?.accentColor ?? '#878580',
724
796
  icon: agent?.icon ?? null,
725
797
  iconMode: agent?.iconMode ?? null,
726
798
  iconVersion: agent?.iconVersion ?? null,
727
799
  };
728
800
  });
801
+ const codex = builtIn.find((entry) => entry.slug === 'codex');
802
+ if (codex)
803
+ codex.state = await codexRuntimeHooksState();
729
804
  return [...builtIn, ...pluginAgents];
730
805
  }
731
806
  export function hooksStateForSlug(slug) {
@@ -734,7 +809,7 @@ export function hooksStateForSlug(slug) {
734
809
  const entry = entries.find((e) => e.slug === slug);
735
810
  if (!entry)
736
811
  return 'not-installed';
737
- return pluginHooksState(slug, entry.targetPath, entry.events.map(([h, s]) => [h, s]));
812
+ return pluginHooksState(slug, entry.targetPath, entry.events);
738
813
  }
739
814
  return hooksState(slug);
740
815
  }
@@ -762,7 +837,7 @@ export async function uninstallHooksForSlug(slug) {
762
837
  // Generic hookmap logic (shared with built-in Claude/Codex + all plugins)
763
838
  // ---------------------------------------------------------------------------
764
839
  /** Generic state check for a hookmap-style hooks file. */
765
- function pluginHooksState(slug, file, events) {
840
+ export function pluginHooksState(slug, file, events) {
766
841
  const root = readJsonObject(file);
767
842
  if (!root)
768
843
  return 'not-installed';
@@ -770,16 +845,22 @@ function pluginHooksState(slug, file, events) {
770
845
  const hooks = root.hooks;
771
846
  let any = false;
772
847
  let complete = true;
773
- for (const [hookEvent, sentinel] of events) {
848
+ for (const [hookEvent, sentinel, matcher, timeout, status] of events) {
774
849
  const list = hooks && typeof hooks === 'object'
775
850
  ? hooks[hookEvent]
776
851
  : null;
777
852
  const ours = Array.isArray(list)
778
- ? list.map((entry) => markerCommand(entry, mark)).find((c) => c !== null) ?? null
779
- : null;
780
- if (ours !== null) {
853
+ ? list.find((entry) => markerCommand(entry, mark) !== null)
854
+ : undefined;
855
+ if (ours !== undefined) {
781
856
  any = true;
782
- if (ours !== hookCommandForPlugin(slug, sentinel))
857
+ const group = ours && typeof ours === 'object' ? ours : {};
858
+ const handler = Array.isArray(group.hooks) && group.hooks[0] && typeof group.hooks[0] === 'object'
859
+ ? group.hooks[0]
860
+ : {};
861
+ if (handler.command !== hookCommandForPlugin(slug, sentinel, status)
862
+ || (matcher ?? undefined) !== group.matcher
863
+ || (timeout ?? undefined) !== handler.timeout)
783
864
  complete = false;
784
865
  }
785
866
  else {
@@ -791,7 +872,7 @@ function pluginHooksState(slug, file, events) {
791
872
  return complete ? 'installed' : 'outdated';
792
873
  }
793
874
  /** Generic install for a hookmap-style hooks file. */
794
- function installPluginHooks(slug, file, events) {
875
+ export function installPluginHooks(slug, file, events) {
795
876
  const { devMode } = resolveHookScript();
796
877
  let root = {};
797
878
  try {
@@ -814,8 +895,8 @@ function installPluginHooks(slug, file, events) {
814
895
  }
815
896
  const hooks = root.hooks;
816
897
  const mark = marker(slug);
817
- for (const [hookEvent, sentinel, matcher] of events) {
818
- const command = hookCommandForPlugin(slug, sentinel);
898
+ for (const [hookEvent, sentinel, matcher, timeout, status] of events) {
899
+ const command = hookCommandForPlugin(slug, sentinel, status);
819
900
  let list = hooks[hookEvent];
820
901
  if (list === undefined) {
821
902
  list = [];
@@ -824,7 +905,10 @@ function installPluginHooks(slug, file, events) {
824
905
  if (!Array.isArray(list))
825
906
  continue;
826
907
  const kept = list.filter((entry) => markerCommand(entry, mark) === null);
827
- const entry = { hooks: [{ type: 'command', command }] };
908
+ const handler = { type: 'command', command };
909
+ if (timeout !== undefined)
910
+ handler.timeout = timeout;
911
+ const entry = { hooks: [handler] };
828
912
  if (matcher)
829
913
  entry.matcher = matcher;
830
914
  kept.push(entry);
@@ -882,7 +966,7 @@ export function refreshStaleHooksAtLaunch() {
882
966
  }
883
967
  // Plugin agents
884
968
  for (const entry of pluginHookEntries()) {
885
- const state = pluginHooksState(entry.slug, entry.targetPath, entry.events.map(([h, s]) => [h, s]));
969
+ const state = pluginHooksState(entry.slug, entry.targetPath, entry.events);
886
970
  if (state !== 'outdated')
887
971
  continue;
888
972
  try {
@@ -19,7 +19,7 @@ const ICON_FILE = 'icon.svg';
19
19
  // ---------------------------------------------------------------------------
20
20
  // Manifest schema
21
21
  // ---------------------------------------------------------------------------
22
- export const PLUGIN_MANIFEST_VERSION = 1;
22
+ export const PLUGIN_MANIFEST_VERSION = 2;
23
23
  // ---------------------------------------------------------------------------
24
24
  // Known sentinel event kinds — the set of events our state machine accepts
25
25
  // ---------------------------------------------------------------------------
@@ -33,6 +33,10 @@ const VALID_EVENT_KINDS = new Set([
33
33
  'stop',
34
34
  'session-end',
35
35
  ]);
36
+ const VALID_STATUS_PHASES = new Set(['idle', 'working', 'waiting', 'done']);
37
+ const VALID_STATUS_INDICATORS = new Set(['spinner', 'pulse', 'dot', 'ring', 'badge', 'terminal', 'question']);
38
+ const VALID_STATUS_TONES = new Set(['neutral', 'info', 'success', 'warning', 'danger', 'accent']);
39
+ const STATUS_ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
36
40
  // ---------------------------------------------------------------------------
37
41
  // Slug validation (alphanumeric + hyphens, non-empty)
38
42
  // ---------------------------------------------------------------------------
@@ -53,14 +57,28 @@ function resolveHome(p) {
53
57
  }
54
58
  return p;
55
59
  }
56
- function validateManifest(raw, dir) {
60
+ export const V1_MIGRATION_GUIDE_COMMAND = 'td agent-plugin --json';
61
+ export const V1_MIGRATION_AI_PROMPT = [
62
+ 'Please migrate the attached Termdock Agent plugin manifest from v1 to v2.',
63
+ 'Preserve slug, displayName, aliases, accentColor, iconMode, hooks.target, and resume.',
64
+ 'Add statuses[] entries with id, phase (idle|working|waiting|done), label, indicator, and tone.',
65
+ 'Add a status reference to each relevant hooks.events[] mapping while keeping its core event value.',
66
+ `Use the output of \`${V1_MIGRATION_GUIDE_COMMAND}\` as the authoritative schema.`,
67
+ 'Return only the corrected manifest JSON.',
68
+ ].join(' ');
69
+ export function validateManifest(raw, dir) {
57
70
  const errors = [];
58
71
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
59
72
  return { error: { slug: path.basename(dir), errors: ['manifest.json is not a JSON object'] } };
60
73
  }
61
74
  const m = raw;
62
75
  const version = m.version;
63
- if (typeof version !== 'number' || version !== PLUGIN_MANIFEST_VERSION) {
76
+ const isV1 = version === 1;
77
+ if (isV1) {
78
+ errors.push('manifest v1 is no longer supported; migrate this plugin to manifest v2');
79
+ errors.push(`read the current machine-readable schema with: ${V1_MIGRATION_GUIDE_COMMAND}`);
80
+ }
81
+ else if (typeof version !== 'number' || version !== PLUGIN_MANIFEST_VERSION) {
64
82
  errors.push(`version must be ${PLUGIN_MANIFEST_VERSION}`);
65
83
  }
66
84
  const slug = m.slug;
@@ -84,6 +102,59 @@ function validateManifest(raw, dir) {
84
102
  if (iconMode !== undefined && iconMode !== 'mask' && iconMode !== 'native') {
85
103
  errors.push('iconMode must be "mask" or "native" if provided');
86
104
  }
105
+ // Optional plugin-defined status vocabulary. These decorate the stable
106
+ // idle/working/waiting/done phases instead of replacing their semantics.
107
+ let statuses;
108
+ const statusIds = new Set();
109
+ if (m.statuses !== undefined) {
110
+ if (!Array.isArray(m.statuses) || m.statuses.length === 0) {
111
+ errors.push('statuses must be a non-empty array if present');
112
+ }
113
+ else {
114
+ const validStatuses = [];
115
+ for (let i = 0; i < m.statuses.length; i++) {
116
+ const rawStatus = m.statuses[i];
117
+ if (!rawStatus || typeof rawStatus !== 'object' || Array.isArray(rawStatus)) {
118
+ errors.push(`statuses[${i}] is not an object`);
119
+ continue;
120
+ }
121
+ const status = rawStatus;
122
+ const id = status.id;
123
+ const phase = status.phase;
124
+ const label = status.label;
125
+ const indicator = status.indicator;
126
+ const tone = status.tone;
127
+ if (typeof id !== 'string' || !STATUS_ID_RE.test(id)) {
128
+ errors.push(`statuses[${i}].id must be lowercase alphanumeric with hyphens (max 40 chars)`);
129
+ continue;
130
+ }
131
+ if (statusIds.has(id))
132
+ errors.push(`statuses[${i}].id duplicates "${id}"`);
133
+ statusIds.add(id);
134
+ if (typeof phase !== 'string' || !VALID_STATUS_PHASES.has(phase)) {
135
+ errors.push(`statuses[${i}].phase must be one of: ${[...VALID_STATUS_PHASES].join(', ')}`);
136
+ }
137
+ if (typeof label !== 'string' || label.trim().length === 0 || label.length > 80) {
138
+ errors.push(`statuses[${i}].label must be a non-empty string (max 80 chars)`);
139
+ }
140
+ if (indicator !== undefined && (typeof indicator !== 'string' || !VALID_STATUS_INDICATORS.has(indicator))) {
141
+ errors.push(`statuses[${i}].indicator must be one of: ${[...VALID_STATUS_INDICATORS].join(', ')}`);
142
+ }
143
+ if (tone !== undefined && (typeof tone !== 'string' || !VALID_STATUS_TONES.has(tone))) {
144
+ errors.push(`statuses[${i}].tone must be one of: ${[...VALID_STATUS_TONES].join(', ')}`);
145
+ }
146
+ validStatuses.push({
147
+ id,
148
+ phase: phase,
149
+ label: typeof label === 'string' ? label.trim() : '',
150
+ indicator: indicator,
151
+ tone: tone,
152
+ });
153
+ }
154
+ if (validStatuses.length > 0)
155
+ statuses = validStatuses;
156
+ }
157
+ }
87
158
  // Optional hooks
88
159
  let hooks;
89
160
  if (m.hooks !== undefined) {
@@ -125,11 +196,16 @@ function validateManifest(raw, dir) {
125
196
  if (timeout !== undefined && (typeof timeout !== 'number' || timeout <= 0)) {
126
197
  errors.push(`hooks.events[${i}].timeout must be a positive number if provided`);
127
198
  }
199
+ const status = evt.status;
200
+ if (status !== undefined && (typeof status !== 'string' || !statusIds.has(status))) {
201
+ errors.push(`hooks.events[${i}].status must reference a declared statuses[].id`);
202
+ }
128
203
  validEvents.push({
129
204
  hook: hook,
130
205
  event: event,
131
206
  matcher: typeof matcher === 'string' ? matcher : undefined,
132
207
  timeout: typeof timeout === 'number' ? timeout : undefined,
208
+ status: typeof status === 'string' ? status : undefined,
133
209
  });
134
210
  }
135
211
  if (validEvents.length > 0) {
@@ -160,8 +236,61 @@ function validateManifest(raw, dir) {
160
236
  };
161
237
  }
162
238
  }
239
+ // Optional automatic-title provider. Commands are argv arrays rather than
240
+ // shell snippets so a UI-installed manifest cannot gain accidental shell
241
+ // interpolation beyond the executable it explicitly declares.
242
+ let titleNamer;
243
+ if (m.titleNamer !== undefined) {
244
+ if (typeof m.titleNamer !== 'object' || Array.isArray(m.titleNamer) || m.titleNamer === null) {
245
+ errors.push('titleNamer must be an object if present');
246
+ }
247
+ else {
248
+ const n = m.titleNamer;
249
+ const command = n.command;
250
+ const args = n.args;
251
+ if (typeof command !== 'string' || command.trim().length === 0) {
252
+ errors.push('titleNamer.command is required');
253
+ }
254
+ if (!Array.isArray(args) || !args.every((arg) => typeof arg === 'string') || !args.some((arg) => arg.includes('{prompt}'))) {
255
+ errors.push('titleNamer.args must be a string array containing {prompt}');
256
+ }
257
+ let models;
258
+ if (n.models !== undefined) {
259
+ if (typeof n.models !== 'object' || Array.isArray(n.models) || n.models === null) {
260
+ errors.push('titleNamer.models must be an object if present');
261
+ }
262
+ else {
263
+ const rawModels = n.models;
264
+ if (typeof rawModels.command !== 'string' || rawModels.command.trim().length === 0) {
265
+ errors.push('titleNamer.models.command is required');
266
+ }
267
+ if (rawModels.args !== undefined && (!Array.isArray(rawModels.args) || !rawModels.args.every((arg) => typeof arg === 'string'))) {
268
+ errors.push('titleNamer.models.args must be a string array if present');
269
+ }
270
+ if (typeof rawModels.command === 'string') {
271
+ models = {
272
+ command: rawModels.command.trim(),
273
+ args: Array.isArray(rawModels.args) ? rawModels.args : undefined,
274
+ };
275
+ }
276
+ }
277
+ }
278
+ if (typeof command === 'string' && Array.isArray(args)) {
279
+ titleNamer = { command: command.trim(), args: args, models };
280
+ }
281
+ }
282
+ }
163
283
  if (errors.length > 0) {
164
- return { error: { slug: typeof slug === 'string' ? slug : path.basename(dir), errors } };
284
+ return {
285
+ error: {
286
+ slug: typeof slug === 'string' ? slug : path.basename(dir),
287
+ errors,
288
+ code: isV1 ? 'AGENT_PLUGIN_MANIFEST_V1_UNSUPPORTED' : undefined,
289
+ migration: isV1
290
+ ? { guideCommand: V1_MIGRATION_GUIDE_COMMAND, aiPrompt: V1_MIGRATION_AI_PROMPT }
291
+ : undefined,
292
+ },
293
+ };
165
294
  }
166
295
  return {
167
296
  manifest: {
@@ -171,8 +300,10 @@ function validateManifest(raw, dir) {
171
300
  aliases: aliases.map((a) => a.trim().toLowerCase()),
172
301
  accentColor: accentColor,
173
302
  iconMode: iconMode,
303
+ statuses,
174
304
  hooks,
175
305
  resume: resumeConfig,
306
+ titleNamer,
176
307
  },
177
308
  };
178
309
  }
@@ -33,7 +33,7 @@ export function agentBySlug(slug) {
33
33
  return BY_SLUG.get(slug.trim().toLowerCase()) ?? null;
34
34
  }
35
35
  export function listAgents() {
36
- return AGENTS.slice();
36
+ return [...BY_SLUG.values()];
37
37
  }
38
38
  function matchToken(token) {
39
39
  return BY_ALIAS.get(token) ?? null;
@@ -278,6 +278,7 @@ export function registerPluginAgents(plugins) {
278
278
  isPlugin: true,
279
279
  iconMode: manifest.iconMode,
280
280
  iconVersion: p.iconMtime || undefined,
281
+ statuses: manifest.statuses,
281
282
  };
282
283
  BY_SLUG.set(info.slug, info);
283
284
  for (const alias of info.aliases)
@@ -17,6 +17,13 @@ const EVENT_KINDS = new Set([
17
17
  'session-start', 'prompt-submit', 'permission-request', 'question-asked',
18
18
  'tool-complete', 'notification', 'stop', 'session-end',
19
19
  ]);
20
+ /** A hook may brand an unclaimed pane, but it may not overwrite or decorate
21
+ * a pane already owned by a different detected Agent. */
22
+ export function agentEventMatchesCurrentAgent(current, event) {
23
+ if (event.agentSlug && !event.agent)
24
+ return false;
25
+ return !current || !event.agent || current.slug === event.agent.slug;
26
+ }
20
27
  export function defaultAgentSessionState() {
21
28
  return {
22
29
  status: 'idle',
@@ -27,6 +34,7 @@ export function defaultAgentSessionState() {
27
34
  agentCwd: null,
28
35
  activity: 0,
29
36
  reviewed: true,
37
+ presentation: null,
30
38
  };
31
39
  }
32
40
  /**
@@ -39,6 +47,9 @@ export function applyAgentEvent(state, ev) {
39
47
  state.sessionId = ev.sessionId;
40
48
  if (ev.cwd)
41
49
  state.agentCwd = ev.cwd;
50
+ // A custom state is scoped to one hook edge; never let it leak into the
51
+ // next core lifecycle event that did not explicitly declare it.
52
+ state.presentation = null;
42
53
  switch (ev.kind) {
43
54
  case 'session-start':
44
55
  state.status = 'idle';
@@ -100,6 +111,10 @@ export function applyAgentEvent(state, ev) {
100
111
  state.agentCwd = null;
101
112
  break;
102
113
  }
114
+ if (ev.status) {
115
+ state.status = ev.status.phase;
116
+ state.presentation = ev.status;
117
+ }
103
118
  }
104
119
  function nonEmpty(s) {
105
120
  return typeof s === 'string' && s.trim().length > 0 ? s : null;
@@ -132,12 +147,20 @@ export function parseAgentEvent(payload) {
132
147
  const eventName = nonEmpty(w.event);
133
148
  if (!eventName || !EVENT_KINDS.has(eventName))
134
149
  return null;
150
+ const agentSlug = nonEmpty(w.agent);
151
+ const agent = agentBySlug(agentSlug);
152
+ const statusId = nonEmpty(w.status);
153
+ const status = statusId
154
+ ? agent?.statuses?.find((definition) => definition.id === statusId) ?? null
155
+ : null;
135
156
  return {
136
- agent: agentBySlug(nonEmpty(w.agent)),
157
+ agent,
158
+ agentSlug,
137
159
  kind: eventName,
138
160
  sessionId: nonEmpty(w.session_id),
139
161
  message: nonEmpty(w.message),
140
162
  cwd: nonEmpty(w.cwd),
163
+ status,
141
164
  };
142
165
  }
143
166
  /**
@@ -146,7 +169,7 @@ export function parseAgentEvent(payload) {
146
169
  * `payloadJson` is the hook's raw stdin JSON from the agent; fields are lifted
147
170
  * into the sentinel body (snake_case, with camelCase aliases for Grok).
148
171
  */
149
- export function buildHookSequence(agent, event, stdinJson) {
172
+ export function buildHookSequence(agent, event, stdinJson, statusId) {
150
173
  let payload = {};
151
174
  try {
152
175
  const parsed = JSON.parse(stdinJson);
@@ -159,6 +182,8 @@ export function buildHookSequence(agent, event, stdinJson) {
159
182
  agent,
160
183
  event,
161
184
  };
185
+ if (statusId)
186
+ body.status = statusId;
162
187
  for (const [key, alias] of [['session_id', 'sessionId'], ['message', 'message'], ['cwd', 'cwd']]) {
163
188
  const v = nonEmpty(payload[key]) ?? nonEmpty(payload[alias]);
164
189
  if (v)