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,10 +16,11 @@ import https from 'https';
16
16
  import path from 'path';
17
17
  import readline from 'readline';
18
18
  import { createHash, randomUUID } from 'crypto';
19
- import { spawn, execFile } from 'child_process';
19
+ import { spawn, spawnSync, execFile } from 'child_process';
20
20
  import { promisify } from 'util';
21
21
  import { Writable } from 'stream';
22
22
  import { createRequire } from 'module';
23
+ import { fileURLToPath } from 'url';
23
24
  import { PORT, DEFAULT_HOST, TMUX } from './config.js';
24
25
  import { isFirstRunCompleted, markFirstRunCompleted, normalizeLocalAccessName, setLocalAccessSetting, getLocalAccessSetting, getPreventSleepSetting } from './utils/settings.js';
25
26
  import { runBootChecks, formatBootCheckReport } from './utils/bootCheck.js';
@@ -126,7 +127,8 @@ Options:
126
127
  --tls List termdock-managed tmux sessions (reads tmux directly,
127
128
  no server connection required)
128
129
  -a, --all With --tls/--attach-tmux: include tmux sessions not stamped by termdock
129
- --json With --tls: emit JSON instead of a table
130
+ --json With --tls: emit session JSON; with --plugin-init:
131
+ emit the machine-readable Agent integration protocol
130
132
  --attach-tmux [n] Attach to a termdock-managed tmux session.
131
133
  With no name: interactive picker.
132
134
  With name: attach directly (e.g. --attach-tmux wt-foo).
@@ -154,7 +156,7 @@ Options:
154
156
  --change-audit-explain <id> [cwd]
155
157
  Read a natural-language explanation from stdin and inject
156
158
  it for the specified Termdock hunk ID.
157
- --plugin-init Print the agent plugin manifest schema reference
159
+ --plugin-init Print the Agent integration protocol reference
158
160
  --plugin-create <file>
159
161
  Create a plugin from a manifest JSON file
160
162
  --plugin-list List installed agent plugins
@@ -163,7 +165,12 @@ Options:
163
165
  -h, --help Show this help message
164
166
 
165
167
  Short commands:
166
- pi Same as --plugin-init
168
+ agent-plugin [--json]
169
+ Explain how an Agent integrates itself. JSON mode is
170
+ machine-readable and intended for Agent consumption.
171
+ agent-event <slug> <event> [status]
172
+ Emit one lifecycle/status event from an Agent hook.
173
+ pi Same as agent-plugin
167
174
  plugin-create <file>
168
175
  Same as --plugin-create <file>
169
176
  plugin-list Same as --plugin-list
@@ -504,9 +511,11 @@ function parseArgs(argv) {
504
511
  let changeAuditShow;
505
512
  let injectChangeAuditHunk;
506
513
  let pluginInit = false;
514
+ let pluginInitJson = false;
507
515
  let pluginCreate;
508
516
  let pluginList = false;
509
517
  let pluginRemove;
518
+ let agentEvent;
510
519
  // Short command aliases for the common path. Keep these positional-only so
511
520
  // long-form flags remain the single source of truth for option semantics.
512
521
  if (argv[0] && !argv[0].startsWith('-')) {
@@ -606,9 +615,22 @@ function parseArgs(argv) {
606
615
  injectChangeAuditHunk = { id: next, cwd };
607
616
  argv = argv.slice(cwd ? 3 : 2);
608
617
  }
609
- else if (command === 'pi' || command === 'plugin-init') {
618
+ else if (command === 'pi' || command === 'plugin-init' || command === 'agent-plugin') {
610
619
  pluginInit = true;
611
- argv = argv.slice(1);
620
+ pluginInitJson = next === '--json';
621
+ argv = argv.slice(pluginInitJson ? 2 : 1);
622
+ }
623
+ else if (command === 'agent-event') {
624
+ const event = argv[2];
625
+ const customStatus = argv[3];
626
+ if (next && event && !next.startsWith('-') && !event.startsWith('-')) {
627
+ agentEvent = { slug: next, event, status: customStatus && !customStatus.startsWith('-') ? customStatus : undefined };
628
+ argv = argv.slice(agentEvent.status ? 4 : 3);
629
+ }
630
+ else {
631
+ console.error(`${ICON.err} ${c.red('Usage: td agent-event <slug> <event> [status]')}`);
632
+ process.exit(1);
633
+ }
612
634
  }
613
635
  else if (command === 'pcreate' || command === 'plugin-create') {
614
636
  if (next && !next.startsWith('-')) {
@@ -713,7 +735,10 @@ function parseArgs(argv) {
713
735
  continue;
714
736
  }
715
737
  if (arg === '--json') {
716
- tlsJson = true;
738
+ if (pluginInit)
739
+ pluginInitJson = true;
740
+ else
741
+ tlsJson = true;
717
742
  continue;
718
743
  }
719
744
  if (arg === '--attach-tmux') {
@@ -887,9 +912,11 @@ function parseArgs(argv) {
887
912
  changeAuditShow,
888
913
  injectChangeAuditHunk,
889
914
  pluginInit,
915
+ pluginInitJson,
890
916
  pluginCreate,
891
917
  pluginList,
892
918
  pluginRemove,
919
+ agentEvent,
893
920
  };
894
921
  }
895
922
  // Reads a single line from stdin without echoing keystrokes. Used for password
@@ -1664,10 +1691,134 @@ function requireRunningServer() {
1664
1691
  }
1665
1692
  return { baseUrl: resolveServerBaseUrl(state), token };
1666
1693
  }
1694
+ const AGENT_EVENT_KINDS = [
1695
+ 'session-start',
1696
+ 'prompt-submit',
1697
+ 'permission-request',
1698
+ 'question-asked',
1699
+ 'tool-complete',
1700
+ 'notification',
1701
+ 'stop',
1702
+ 'session-end',
1703
+ ];
1704
+ const AGENT_PLUGIN_EXAMPLE = {
1705
+ version: 2,
1706
+ slug: 'my-agent',
1707
+ displayName: 'My Agent',
1708
+ aliases: ['my-agent'],
1709
+ accentColor: '#4385BE',
1710
+ iconMode: 'mask',
1711
+ statuses: [
1712
+ { id: 'thinking', phase: 'working', label: 'Thinking', indicator: 'spinner', tone: 'info' },
1713
+ { id: 'approval', phase: 'waiting', label: 'Needs approval', indicator: 'question', tone: 'warning' },
1714
+ { id: 'complete', phase: 'done', label: 'Complete', indicator: 'badge', tone: 'success' },
1715
+ ],
1716
+ hooks: {
1717
+ target: '~/.my-agent/settings.json',
1718
+ events: [
1719
+ { hook: 'UserPromptSubmit', event: 'prompt-submit', status: 'thinking' },
1720
+ { hook: 'PermissionRequest', event: 'permission-request', status: 'approval' },
1721
+ { hook: 'Stop', event: 'stop', status: 'complete' },
1722
+ ],
1723
+ },
1724
+ resume: {
1725
+ command: 'my-agent --resume {sessionId}',
1726
+ staleFlags: ['--resume'],
1727
+ },
1728
+ titleNamer: {
1729
+ command: 'my-agent',
1730
+ args: ['title', '--json', '--prompt', '{prompt}', '--model={model}'],
1731
+ models: {
1732
+ command: 'my-agent',
1733
+ args: ['models', '--json'],
1734
+ },
1735
+ },
1736
+ };
1737
+ function runPluginInitJson() {
1738
+ console.log(JSON.stringify({
1739
+ protocol: 'termdock-agent-plugin',
1740
+ version: 2,
1741
+ purpose: 'Register this Agent with Termdock and publish lifecycle/status updates.',
1742
+ commands: {
1743
+ guide: 'td agent-plugin --json',
1744
+ register: 'td plugin-create ./manifest.json',
1745
+ emit: 'td agent-event <slug> <event> [status]',
1746
+ inspect: 'td plugin-list',
1747
+ remove: 'td plugin-remove <slug>',
1748
+ },
1749
+ ui: {
1750
+ location: 'Settings > Detection Rules',
1751
+ registration: 'A successfully created plugin appears immediately without restarting Termdock.',
1752
+ install: 'The user clicks Install to write the declared hook mappings into hooks.target.',
1753
+ uninstall: 'The user clicks Uninstall to remove only Termdock-owned hook entries.',
1754
+ },
1755
+ lifecycleEvents: AGENT_EVENT_KINDS,
1756
+ phases: ['idle', 'working', 'waiting', 'done'],
1757
+ indicators: ['spinner', 'pulse', 'dot', 'ring', 'badge', 'terminal', 'question'],
1758
+ tones: ['neutral', 'info', 'success', 'warning', 'danger', 'accent'],
1759
+ statusModel: {
1760
+ phase: 'Stable Termdock behavior used for notifications and attention.',
1761
+ id: 'Agent-defined fine-grained status referenced by hook mappings or agent-event.',
1762
+ label: 'Human-readable status text.',
1763
+ indicator: 'Theme-safe visual indicator.',
1764
+ tone: 'Theme token; arbitrary status colors are not accepted.',
1765
+ },
1766
+ migrationV1: {
1767
+ errorCode: 'AGENT_PLUGIN_MANIFEST_V1_UNSUPPORTED',
1768
+ guideCommand: 'td agent-plugin --json',
1769
+ aiPrompt: 'Please migrate the attached Termdock Agent plugin manifest from v1 to v2. Preserve slug, displayName, aliases, accentColor, iconMode, hooks.target, and resume. Add statuses[] entries with id, phase (idle|working|waiting|done), label, indicator, and tone. Add a status reference to each relevant hooks.events[] mapping while keeping its core event value. Use the output of `td agent-plugin --json` as the authoritative schema. Return only the corrected manifest JSON.',
1770
+ },
1771
+ hookPayload: {
1772
+ stdin: 'Optional JSON object.',
1773
+ recognizedFields: ['session_id', 'sessionId', 'message', 'cwd'],
1774
+ example: 'printf \'%s\' \'{"session_id":"abc","message":"Indexing"}\' | td agent-event my-agent prompt-submit thinking',
1775
+ },
1776
+ integrationSteps: [
1777
+ 'Generate a version 2 manifest using this schema and save it as JSON.',
1778
+ 'Run td plugin-create <manifest-file>; the Agent appears in Termdock immediately.',
1779
+ 'Ask the user to open Settings > Detection Rules and click Install for this Agent. They can uninstall it there later.',
1780
+ 'If the Agent hook format is not a Claude-style hooks map, configure native hooks to call td agent-event directly.',
1781
+ 'Map the completed-turn hook to event "stop"; this is also the automatic-title trigger when the user enables titles for the Agent.',
1782
+ 'Optionally declare titleNamer so this Agent can generate titles and dynamically publish its own model catalog.',
1783
+ 'Run td plugin-list to verify registration.',
1784
+ ],
1785
+ manifest: AGENT_PLUGIN_EXAMPLE,
1786
+ }, null, 2));
1787
+ }
1788
+ function runAgentEvent(input) {
1789
+ const slugPattern = /^[a-z][a-z0-9-]{0,39}$/;
1790
+ if (!slugPattern.test(input.slug)) {
1791
+ console.error(`${ICON.err} ${c.red('agent-event slug must be lowercase alphanumeric with hyphens')}`);
1792
+ process.exit(1);
1793
+ }
1794
+ if (!AGENT_EVENT_KINDS.includes(input.event)) {
1795
+ console.error(`${ICON.err} ${c.red(`Unknown agent event: ${input.event}`)}`);
1796
+ console.error(` ${c.dim(`Expected one of: ${AGENT_EVENT_KINDS.join(', ')}`)}`);
1797
+ process.exit(1);
1798
+ }
1799
+ if (input.status && !slugPattern.test(input.status)) {
1800
+ console.error(`${ICON.err} ${c.red('agent-event status must be lowercase alphanumeric with hyphens')}`);
1801
+ process.exit(1);
1802
+ }
1803
+ const script = fileURLToPath(new URL('./agentHook.js', import.meta.url));
1804
+ const result = spawnSync(process.execPath, [script, 'agent-hook', input.slug, input.event, ...(input.status ? [input.status] : [])], {
1805
+ stdio: 'inherit',
1806
+ env: process.env,
1807
+ });
1808
+ if (result.error) {
1809
+ console.error(`${ICON.err} ${c.red(`Failed to emit Agent event: ${result.error.message}`)}`);
1810
+ process.exit(1);
1811
+ }
1812
+ process.exit(result.status ?? 0);
1813
+ }
1667
1814
  function runPluginInit() {
1668
1815
  console.log(`\
1669
- ${c.bold('Agent Plugin Reference')}
1670
- ${c.dim('─────────────────────')}
1816
+ ${c.bold('Termdock Agent Integration Protocol v2')}
1817
+ ${c.dim('──────────────────────────────────────')}
1818
+
1819
+ Machine-readable form for an Agent to inspect itself:
1820
+
1821
+ ${c.cyan('td agent-plugin --json')}
1671
1822
 
1672
1823
  A plugin is a directory under ${c.cyan('~/.termdock/agent-plugins/<slug>/')} containing:
1673
1824
 
@@ -1678,8 +1829,8 @@ ${c.bold('manifest.json schema')}
1678
1829
  ${c.dim('───────────────────')}
1679
1830
 
1680
1831
  {
1681
- ${c.dim('// Manifest version. Must be')} ${c.green('1')}${c.dim('.')}
1682
- "version": 1,
1832
+ ${c.dim('// Manifest version. Must be')} ${c.green('2')}${c.dim('.')}
1833
+ "version": 2,
1683
1834
 
1684
1835
  ${c.dim('// Unique slug (lowercase alphanum + hyphens, max 40 chars).')}
1685
1836
  ${c.dim('// Must not collide with built-in agent slugs.')}
@@ -1702,6 +1853,13 @@ ${c.dim('───────────────────')}
1702
1853
  ${c.dim('// "native" - SVG rendered directly, preserving original fills/gradients.')}
1703
1854
  "iconMode": "native",
1704
1855
 
1856
+ ${c.dim('// Optional fine-grained states. phase drives behavior; the rest drives UI.')}
1857
+ "statuses": [
1858
+ { "id": "thinking", "phase": "working", "label": "Thinking", "indicator": "spinner", "tone": "info" },
1859
+ { "id": "approval", "phase": "waiting", "label": "Needs approval", "indicator": "question", "tone": "warning" },
1860
+ { "id": "complete", "phase": "done", "label": "Complete", "indicator": "badge", "tone": "success" }
1861
+ ],
1862
+
1705
1863
  ${c.dim('// ── optional: hook installation (bring your own hooks.json surface) ──')}
1706
1864
  "hooks": {
1707
1865
  ${c.dim("// Path to the agent's hooks config file (~-abbreviated).")}
@@ -1712,10 +1870,10 @@ ${c.dim('───────────────────')}
1712
1870
  ${c.dim('// Optional matcher narrows the subscription (e.g. permission type).')}
1713
1871
  "events": [
1714
1872
  { "hook": "SessionStart", "event": "session-start" },
1715
- { "hook": "UserPromptSubmit", "event": "prompt-submit" },
1716
- { "hook": "Notification", "event": "notification", "matcher": "elicitation_dialog" },
1873
+ { "hook": "UserPromptSubmit", "event": "prompt-submit", "status": "thinking" },
1874
+ { "hook": "Notification", "event": "notification", "status": "approval", "matcher": "elicitation_dialog" },
1717
1875
  { "hook": "PostToolUse", "event": "tool-complete" },
1718
- { "hook": "Stop", "event": "stop" },
1876
+ { "hook": "Stop", "event": "stop", "status": "complete" },
1719
1877
  { "hook": "SessionEnd", "event": "session-end" }
1720
1878
  ]
1721
1879
  },
@@ -1726,6 +1884,15 @@ ${c.dim('───────────────────')}
1726
1884
  "command": "trae --resume {sessionId}",
1727
1885
  ${c.dim('// Flags to strip from the original launch argv when replaying the resume.')}
1728
1886
  "staleFlags": ["--resume", "-r", "--session-id", "--continue"]
1887
+ },
1888
+
1889
+ ${c.dim('// ── optional: injectable automatic-title provider ──')}
1890
+ "titleNamer": {
1891
+ ${c.dim('// No shell expansion. {prompt} is required; a bare {model} arg is omitted when automatic.')}
1892
+ "command": "trae",
1893
+ "args": ["title", "--prompt", "{prompt}", "--model={model}"],
1894
+ ${c.dim('// Must print JSON: strings or {id, displayName, description, isDefault} objects.')}
1895
+ "models": { "command": "trae", "args": ["models", "--json"] }
1729
1896
  }
1730
1897
  }
1731
1898
 
@@ -1739,33 +1906,54 @@ ${c.dim('─────────────────────')}
1739
1906
  tool-complete A tool execution completed (unblocks waiting state)
1740
1907
  notification Generic notification from the agent (heuristic gated)
1741
1908
  stop Agent turn finished / went idle
1909
+ Also triggers automatic title generation when enabled
1742
1910
  session-end Agent session closed / binary exited
1743
1911
 
1744
1912
  ${c.bold('Creating a plugin')}
1745
1913
  ${c.dim('──────────────────')}
1746
1914
 
1747
- # 1. Create the plugin directory with manifest + icon
1748
- mkdir -p ~/.termdock/agent-plugins/trae
1749
- cp manifest.json icon.svg ~/.termdock/agent-plugins/trae/
1750
-
1751
- ${c.dim('# 2. Install the plugin via CLI (needs running server)')}
1752
- td plugin-create ~/.termdock/agent-plugins/trae/manifest.json
1753
-
1754
- ${c.dim('# 3. Or create directly from the manifest JSON')}
1915
+ ${c.dim('# Inject the Agent definition; it appears in the UI immediately')}
1755
1916
  td plugin-create ./trae-manifest.json
1756
1917
 
1757
- ${c.dim('# 4. List installed plugins')}
1918
+ ${c.dim('# The user then opens Settings > Detection Rules and clicks Install.')}
1919
+ ${c.dim('# The same UI button can uninstall the hooks later.')}
1920
+
1921
+ ${c.dim('# Verify registration')}
1758
1922
  td plugin-list
1759
1923
 
1760
- ${c.dim('# 5. Remove a plugin')}
1924
+ ${c.dim('# Remove')}
1761
1925
  td plugin-remove trae
1762
1926
 
1927
+ ${c.bold('Direct event bridge for any hook system')}
1928
+ ${c.dim('───────────────────────────────────────')}
1929
+
1930
+ If the Agent does not use a Claude-style hooks JSON file, configure its
1931
+ native hooks to invoke this stable public CLI command instead:
1932
+
1933
+ td agent-event trae prompt-submit thinking
1934
+ td agent-event trae permission-request approval
1935
+ td agent-event trae stop complete
1936
+
1937
+ Hook JSON may be piped on stdin. Termdock recognizes session_id/sessionId,
1938
+ message, and cwd:
1939
+
1940
+ printf '%s' '{"session_id":"abc","message":"Indexing"}' | td agent-event trae prompt-submit thinking
1941
+
1763
1942
  ${c.bold('Manifest validation at startup')}
1764
1943
  ${c.dim('───────────────────────────────')}
1765
1944
 
1766
- Termdock validates every plugin manifest on server start. Errors are logged
1945
+ Termdock validates every plugin manifest before registration. Errors are logged
1767
1946
  to ${c.cyan('~/.termdock/server.log')} and visible via ${c.cyan('td plugin-list')}${c.dim('.')}
1768
1947
  Slugs/aliases that collide with built-in agents are skipped.
1948
+
1949
+ ${c.bold('Migrating a v1 plugin')}
1950
+ ${c.dim('─────────────────────')}
1951
+
1952
+ v1 manifests fail with ${c.cyan('AGENT_PLUGIN_MANIFEST_V1_UNSUPPORTED')} and an
1953
+ AI-ready repair prompt. Copy that prompt together with the old manifest to
1954
+ an AI, or let the AI inspect the current schema directly:
1955
+
1956
+ ${c.cyan('td agent-plugin --json')}
1769
1957
  `);
1770
1958
  }
1771
1959
  async function runPluginCreate(manifestPath) {
@@ -1790,12 +1978,20 @@ async function runPluginCreate(manifestPath) {
1790
1978
  const response = await postLocalJson(baseUrl, token, '/api/terminal/agent-plugins', manifest);
1791
1979
  const body = JSON.parse(response.body || '{}');
1792
1980
  if (response.statusCode < 200 || response.statusCode >= 300) {
1793
- console.error(`${ICON.err} ${c.red(body.error || 'Failed to create plugin')}`);
1981
+ console.error(`${ICON.err} ${c.red(body.code ? `${body.code}:` : 'Plugin validation failed:')}`);
1982
+ console.error(body.error || 'Failed to create plugin');
1983
+ if (body.migration?.guideCommand) {
1984
+ console.error(`\n${c.bold('Current schema:')} ${c.cyan(body.migration.guideCommand)}`);
1985
+ }
1986
+ if (body.migration?.aiPrompt) {
1987
+ console.error(`\n${c.bold('Copy this to your AI:')}\n${body.migration.aiPrompt}`);
1988
+ }
1794
1989
  process.exit(1);
1795
1990
  }
1796
1991
  console.log(`${ICON.ok} ${c.green(`Plugin "${body.slug}" created.`)}`);
1797
1992
  console.log(` ${c.dim('Directory:')} ${c.cyan(body.dir ?? resolved)}`);
1798
- console.log(` ${c.dim('Restart Termdock to activate:')} ${c.cyan('td --stop && td')}`);
1993
+ console.log(` ${c.dim('Visible immediately in Settings > Detection Rules; no restart required.')}`);
1994
+ console.log(` ${c.dim('The user can click Install or Uninstall there.')}`);
1799
1995
  }
1800
1996
  async function runPluginList() {
1801
1997
  const { baseUrl, token } = requireRunningServer();
@@ -1808,7 +2004,7 @@ async function runPluginList() {
1808
2004
  const plugins = body.plugins ?? [];
1809
2005
  if (plugins.length === 0) {
1810
2006
  console.log(`${c.dim('No plugins installed.')}`);
1811
- console.log(` ${c.dim('See:')} ${c.cyan('td plugin-init')}`);
2007
+ console.log(` ${c.dim('See:')} ${c.cyan('td agent-plugin')}`);
1812
2008
  }
1813
2009
  else {
1814
2010
  console.log(`${c.bold('Installed plugins')} ${c.dim(`(${plugins.length})`)}`);
@@ -1831,7 +2027,13 @@ async function runPluginList() {
1831
2027
  console.log('');
1832
2028
  console.log(`${c.yellow('Validation errors')}`);
1833
2029
  for (const e of errors) {
1834
- console.log(` ${c.red('✗')} ${c.cyan(e.slug)}${c.dim(':')} ${e.errors.join('; ')}`);
2030
+ console.log(` ${c.red('✗')} ${c.cyan(e.slug)}${c.dim(':')} ${e.code ? c.yellow(e.code) : e.errors.join('; ')}`);
2031
+ for (const message of e.errors)
2032
+ console.log(` ${message}`);
2033
+ if (e.migration?.guideCommand)
2034
+ console.log(` ${c.dim('Current schema:')} ${c.cyan(e.migration.guideCommand)}`);
2035
+ if (e.migration?.aiPrompt)
2036
+ console.log(` ${c.dim('Copy this to your AI:')} ${e.migration.aiPrompt}`);
1835
2037
  }
1836
2038
  }
1837
2039
  }
@@ -2809,8 +3011,14 @@ async function main() {
2809
3011
  runClearPassword();
2810
3012
  process.exit(0);
2811
3013
  }
3014
+ if (options.agentEvent) {
3015
+ runAgentEvent(options.agentEvent);
3016
+ }
2812
3017
  if (options.pluginInit) {
2813
- runPluginInit();
3018
+ if (options.pluginInitJson)
3019
+ runPluginInitJson();
3020
+ else
3021
+ runPluginInit();
2814
3022
  process.exit(0);
2815
3023
  }
2816
3024
  if (options.pluginCreate !== undefined) {
@@ -905,7 +905,7 @@ function buildNestedRepoDisplayRootSet(workspaceRoot, repositories) {
905
905
  async function buildGitBundle(resolvedCwd, gitRoot, signal, options = {}) {
906
906
  const [branchOutput, changedResult] = await Promise.all([
907
907
  execGit(['branch', '--show-current'], gitRoot, signal, options.gitTimeoutMs).catch(emptyOnNonAbortGitError),
908
- getChangedFiles(gitRoot, signal, { includeUntracked: false, gitTimeoutMs: options.gitTimeoutMs }),
908
+ getChangedFiles(gitRoot, signal, { includeUntracked: true, gitTimeoutMs: options.gitTimeoutMs }),
909
909
  ]);
910
910
  const files = changedResult.files;
911
911
  const annotatedFiles = annotateRepoFiles(files, gitRoot, gitRoot);
@@ -1012,7 +1012,6 @@ function mergeChangedFilesByPath(current, incoming) {
1012
1012
  function updateGitBundleCachesWithUntracked(repoRoot, files) {
1013
1013
  if (files.length === 0)
1014
1014
  return;
1015
- const now = Date.now();
1016
1015
  for (const [cacheKey, cached] of gitBundleCache.entries()) {
1017
1016
  const bundle = cached.bundle;
1018
1017
  const repositories = bundle.repositories;
@@ -1046,7 +1045,7 @@ function updateGitBundleCachesWithUntracked(repoRoot, files) {
1046
1045
  repoFilters: buildGitRepositoryFilters(primary?.root ?? repoRoot, nextRepositories),
1047
1046
  untrackedDeferred: nextRepositories.some((repo) => repo.untrackedDeferred),
1048
1047
  };
1049
- gitBundleCache.set(cacheKey, { bundle: nextBundle, expiresAt: now + GIT_BUNDLE_CACHE_TTL_MS });
1048
+ gitBundleCache.set(cacheKey, { bundle: nextBundle, expiresAt: cached.expiresAt, updatedAt: cached.updatedAt });
1050
1049
  continue;
1051
1050
  }
1052
1051
  if (bundle.context?.root !== repoRoot)
@@ -1063,7 +1062,7 @@ function updateGitBundleCachesWithUntracked(repoRoot, files) {
1063
1062
  context: nextContext,
1064
1063
  untrackedDeferred: false,
1065
1064
  };
1066
- gitBundleCache.set(cacheKey, { bundle: nextBundle, expiresAt: now + GIT_BUNDLE_CACHE_TTL_MS });
1065
+ gitBundleCache.set(cacheKey, { bundle: nextBundle, expiresAt: cached.expiresAt, updatedAt: cached.updatedAt });
1067
1066
  }
1068
1067
  }
1069
1068
  async function buildWorkspaceGitBundle(resolvedCwd, gitRoot, includeNested, signal, options = {}) {
@@ -1138,6 +1137,7 @@ async function getCachedGitBundle(resolvedCwd, gitRoot, includeNested, refresh,
1138
1137
  cached: true,
1139
1138
  stale: cached.expiresAt <= now,
1140
1139
  cacheAgeMs,
1140
+ cacheUpdatedAt: cached.updatedAt,
1141
1141
  };
1142
1142
  }
1143
1143
  const canReusePending = !refresh && !signal;
@@ -1148,8 +1148,9 @@ async function getCachedGitBundle(resolvedCwd, gitRoot, includeNested, refresh,
1148
1148
  }
1149
1149
  const promise = buildWorkspaceGitBundle(resolvedCwd, gitRoot, includeNested, signal)
1150
1150
  .then((bundle) => {
1151
- gitBundleCache.set(cacheKey, { bundle, expiresAt: Date.now() + GIT_BUNDLE_CACHE_TTL_MS });
1152
- return bundle;
1151
+ const updatedAt = Date.now();
1152
+ gitBundleCache.set(cacheKey, { bundle, expiresAt: updatedAt + GIT_BUNDLE_CACHE_TTL_MS, updatedAt });
1153
+ return { ...bundle, cacheUpdatedAt: updatedAt };
1153
1154
  })
1154
1155
  .finally(() => {
1155
1156
  if (gitBundleBuildPromises.get(cacheKey) === promise)
@@ -1166,8 +1167,9 @@ async function refreshGitBundleCacheDetached(resolvedCwd, gitRoot, includeNested
1166
1167
  return pending;
1167
1168
  const promise = buildWorkspaceGitBundle(resolvedCwd, gitRoot, includeNested, undefined, options)
1168
1169
  .then((bundle) => {
1169
- gitBundleCache.set(cacheKey, { bundle, expiresAt: Date.now() + GIT_BUNDLE_CACHE_TTL_MS });
1170
- return bundle;
1170
+ const updatedAt = Date.now();
1171
+ gitBundleCache.set(cacheKey, { bundle, expiresAt: updatedAt + GIT_BUNDLE_CACHE_TTL_MS, updatedAt });
1172
+ return { ...bundle, cacheUpdatedAt: updatedAt };
1171
1173
  })
1172
1174
  .finally(() => {
1173
1175
  if (gitBundleBuildPromises.get(cacheKey) === promise)
@@ -1190,6 +1192,7 @@ function getGitBundleCache(gitRoot, includeNested, allowStale = false) {
1190
1192
  cached: true,
1191
1193
  stale: cached.expiresAt <= now,
1192
1194
  cacheAgeMs,
1195
+ cacheUpdatedAt: cached.updatedAt,
1193
1196
  };
1194
1197
  }
1195
1198
  function getSinglePath(paths) {