termdock 1.4.68 → 1.4.70

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 +13 -0
  3. package/dist/client/assets/{ModelPreview-BgnWlhMf.js → ModelPreview-Cs14IQuQ.js} +1 -1
  4. package/dist/client/assets/{abnfDiagram-VRR7QNED-C3RDwPoE.js → abnfDiagram-VRR7QNED-DuDfdv-6.js} +1 -1
  5. package/dist/client/assets/{arc-n-PrqSp_.js → arc-CSYzGV_2.js} +1 -1
  6. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-D-l8Q2rF.js → architectureDiagram-ZJ3FMSHR-YiZ0HXNi.js} +1 -1
  7. package/dist/client/assets/{blockDiagram-677ZJIJ3-D5b5kvhB.js → blockDiagram-677ZJIJ3-IaQMzmYv.js} +1 -1
  8. package/dist/client/assets/{c4Diagram-LMCZKHZV-CsvG6hAc.js → c4Diagram-LMCZKHZV-BwyuIPRL.js} +1 -1
  9. package/dist/client/assets/channel-BotxtiNE.js +1 -0
  10. package/dist/client/assets/{chunk-2Q5K7J3B-CcTcAD2X.js → chunk-2Q5K7J3B-BtHQYLBT.js} +1 -1
  11. package/dist/client/assets/{chunk-32BRIVSS-c--MtlS9.js → chunk-32BRIVSS-CXrK07sW.js} +1 -1
  12. package/dist/client/assets/{chunk-5VM5RSS4-BCdLlLPD.js → chunk-5VM5RSS4-BfBshn11.js} +1 -1
  13. package/dist/client/assets/{chunk-EX3LRPZG-BXYeFL7Q.js → chunk-EX3LRPZG-BJbilNaf.js} +1 -1
  14. package/dist/client/assets/{chunk-JWPE2WC7-Cakio9E1.js → chunk-JWPE2WC7-wgcmSRlL.js} +1 -1
  15. package/dist/client/assets/{chunk-MOJQB5TN-CR9m_lTv.js → chunk-MOJQB5TN-8VZVUeDI.js} +1 -1
  16. package/dist/client/assets/{chunk-RYQCIY6F-DvPE2m9c.js → chunk-RYQCIY6F-B900YBFa.js} +1 -1
  17. package/dist/client/assets/{chunk-V7JOEXUC-BdHIv-k_.js → chunk-V7JOEXUC-BK4_RLz-.js} +1 -1
  18. package/dist/client/assets/{chunk-VR4S4FIN-CNYv3_OD.js → chunk-VR4S4FIN-Kj5_ycjB.js} +1 -1
  19. package/dist/client/assets/{chunk-XXDRQBXY-Ddhpk5bE.js → chunk-XXDRQBXY-BolvPLRz.js} +1 -1
  20. package/dist/client/assets/classDiagram-OUVF2IWQ-CLWlbTSC.js +1 -0
  21. package/dist/client/assets/classDiagram-v2-EOCWNBFH-CLWlbTSC.js +1 -0
  22. package/dist/client/assets/{cose-bilkent-JH36ORCC-vc2IL5Qo.js → cose-bilkent-JH36ORCC-9_9_SLqv.js} +1 -1
  23. package/dist/client/assets/{cynefin-VYW2F7L2-Cr5U2Llm.js → cynefin-VYW2F7L2-yEpoi3vG.js} +1 -1
  24. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-DaSVhxBB.js → cynefinDiagram-TSTJHNR4-Dy-gvup_.js} +1 -1
  25. package/dist/client/assets/{dagre-VKFMJZFB-B8Ao-OKf.js → dagre-VKFMJZFB-CNjJ5pRK.js} +1 -1
  26. package/dist/client/assets/{diagram-FQU43EPY-a-sl4C9b.js → diagram-FQU43EPY-uZ73Rj3T.js} +1 -1
  27. package/dist/client/assets/{diagram-G47NLZAW-CmezqIfj.js → diagram-G47NLZAW-Cxt7TYZT.js} +1 -1
  28. package/dist/client/assets/{diagram-NH7WQ7WH-rM4l3wBG.js → diagram-NH7WQ7WH-B4TKinuZ.js} +1 -1
  29. package/dist/client/assets/{diagram-OA4YK3LP-DS3rHIQ7.js → diagram-OA4YK3LP-CGSuenHU.js} +1 -1
  30. package/dist/client/assets/{diagram-WEI45ONY-BUyvO5qF.js → diagram-WEI45ONY-DqqzDaUZ.js} +1 -1
  31. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-C6KPNqQ0.js → ebnfDiagram-CCIWWBDH-B2SgXL5w.js} +1 -1
  32. package/dist/client/assets/{erDiagram-Q63AITRT-u9pQXlID.js → erDiagram-Q63AITRT-D-ibkr8y.js} +1 -1
  33. package/dist/client/assets/{flowDiagram-23GEKE2U-h9QkExto.js → flowDiagram-23GEKE2U-BPeyMqr-.js} +1 -1
  34. package/dist/client/assets/{ganttDiagram-NO4QXBWP-C9AXkH3Y.js → ganttDiagram-NO4QXBWP-DcEqEzzu.js} +1 -1
  35. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CXYfDg8b.js → gitGraphDiagram-IHSO6WYX-DTZ6UCQY.js} +1 -1
  36. package/dist/client/assets/index-DhfIiBLW.css +1 -0
  37. package/dist/client/assets/index-ivnGnbVg.js +451 -0
  38. package/dist/client/assets/{infoDiagram-FWYZ7A6U-B_Kb0Chj.js → infoDiagram-FWYZ7A6U-BHrzVXeX.js} +1 -1
  39. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-B2kT_9-T.js → ishikawaDiagram-FXEZZL3T-CwuoQJku.js} +1 -1
  40. package/dist/client/assets/{journeyDiagram-5HDEW3XC-DdOISfLz.js → journeyDiagram-5HDEW3XC-ZEcy0rDZ.js} +1 -1
  41. package/dist/client/assets/{kanban-definition-HUTT4EX6-CdIIjjXO.js → kanban-definition-HUTT4EX6-BDBawNbr.js} +1 -1
  42. package/dist/client/assets/{linear-CWpzI-ns.js → linear-smYQWzDV.js} +1 -1
  43. package/dist/client/assets/{mermaid.core-BLHPinUj.js → mermaid.core-eJs7Ouvw.js} +4 -4
  44. package/dist/client/assets/{mindmap-definition-LN4V7U3C-qFnTyqQA.js → mindmap-definition-LN4V7U3C-BK1IZ9zg.js} +1 -1
  45. package/dist/client/assets/{pegDiagram-2B236MQR-Bu7ijeZf.js → pegDiagram-2B236MQR-B3-mVNgX.js} +1 -1
  46. package/dist/client/assets/{pieDiagram-ENE6RG2P-BygAm24V.js → pieDiagram-ENE6RG2P-Bkeeyx5l.js} +1 -1
  47. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-3-9rkPqr.js → quadrantDiagram-ABIIQ3AL-DouIklEO.js} +1 -1
  48. package/dist/client/assets/{railroadDiagram-RFXS5EU6-BAsvFjIm.js → railroadDiagram-RFXS5EU6-BB_jXQd2.js} +1 -1
  49. package/dist/client/assets/{requirementDiagram-TGXJPOKE-H-BySumI.js → requirementDiagram-TGXJPOKE-u56IHfiR.js} +1 -1
  50. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-D2PDv2gA.js → sankeyDiagram-HTMAVEWB-eDi2cs5E.js} +1 -1
  51. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-C9pvFRUG.js → sequenceDiagram-DBY2YBRQ-DnAA2xYa.js} +1 -1
  52. package/dist/client/assets/{sizeCapture-X5ZJPWSS-Cx0TrKvi.js → sizeCapture-X5ZJPWSS-BogPui79.js} +1 -1
  53. package/dist/client/assets/{stateDiagram-2N3HPSRC-Ciytmucj.js → stateDiagram-2N3HPSRC--EeXDy2M.js} +1 -1
  54. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Dc-3HKTs.js +1 -0
  55. package/dist/client/assets/{swimlanes-5IMT3BWC-Dwk151xG.js → swimlanes-5IMT3BWC-DZAw7-kY.js} +2 -2
  56. package/dist/client/assets/swimlanesDiagram-G3AALYLV-CI1Wlr0V.js +8 -0
  57. package/dist/client/assets/{timeline-definition-FHXFAJF6-Bym0jj2o.js → timeline-definition-FHXFAJF6-DkcnpB-Y.js} +1 -1
  58. package/dist/client/assets/{vennDiagram-L72KCM5P-CToPJelW.js → vennDiagram-L72KCM5P-BStcDGEf.js} +1 -1
  59. package/dist/client/assets/{wardleyDiagram-EHGQE667-BZ6V3CA9.js → wardleyDiagram-EHGQE667-Cj4CQn1M.js} +1 -1
  60. package/dist/client/assets/{xychartDiagram-FW5EYKEG-D5NzFpxi.js → xychartDiagram-FW5EYKEG-_uBCjai8.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 +118 -0
  64. package/dist/server/agent/installers.js +11 -10
  65. package/dist/server/agent/plugins.js +90 -4
  66. package/dist/server/agent/registry.js +1 -0
  67. package/dist/server/agent/session.js +18 -2
  68. package/dist/server/agent/titleNamerCatalog.js +203 -0
  69. package/dist/server/agentHook.js +2 -1
  70. package/dist/server/cli.js +255 -30
  71. package/dist/server/routes/filesystem.js +11 -8
  72. package/dist/server/routes/terminal.js +351 -10
  73. package/dist/server/utils/autoUpdate.js +175 -0
  74. package/dist/server/utils/npmUpdate.js +139 -0
  75. package/dist/server/utils/settings.js +44 -0
  76. package/package.json +1 -1
  77. package/runtime-manifest.json +2 -2
  78. package/dist/client/assets/channel-DqH6WWOA.js +0 -1
  79. package/dist/client/assets/classDiagram-OUVF2IWQ-DbsEzwLQ.js +0 -1
  80. package/dist/client/assets/classDiagram-v2-EOCWNBFH-DbsEzwLQ.js +0 -1
  81. package/dist/client/assets/index-ChhyERKX.css +0 -1
  82. package/dist/client/assets/index-DVo7u2vf.js +0 -442
  83. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-C9GGeb7o.js +0 -1
  84. package/dist/client/assets/swimlanesDiagram-G3AALYLV-BRRLZYN8.js +0 -8
@@ -16,14 +16,16 @@ 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';
26
27
  import { localAccessManager, getLanIPv4Addresses } from './utils/localAccess.js';
28
+ import { OFFICIAL_NPM_REGISTRY, updateTermdockFromOfficialRegistry } from './utils/npmUpdate.js';
27
29
  import { clearAuthFile, destroyAllSessions, hashPassword, isAuthEnabled, isEnvPasswordSet, writeAuthFile, } from './utils/authProtection.js';
28
30
  const execFileAsync = promisify(execFile);
29
31
  const TERMDOCK_VERSION = (() => {
@@ -118,13 +120,15 @@ Options:
118
120
  --foreground Run in the foreground
119
121
  --status Show background server status
120
122
  --stop Stop the background server
123
+ --update Update the global CLI from the official npm registry
121
124
  -v, --version Print version and exit
122
125
  --set-password Set or update the access password (interactive prompt)
123
126
  --clear-password Remove the access password and disable authentication
124
127
  --tls List termdock-managed tmux sessions (reads tmux directly,
125
128
  no server connection required)
126
129
  -a, --all With --tls/--attach-tmux: include tmux sessions not stamped by termdock
127
- --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
128
132
  --attach-tmux [n] Attach to a termdock-managed tmux session.
129
133
  With no name: interactive picker.
130
134
  With name: attach directly (e.g. --attach-tmux wt-foo).
@@ -152,7 +156,7 @@ Options:
152
156
  --change-audit-explain <id> [cwd]
153
157
  Read a natural-language explanation from stdin and inject
154
158
  it for the specified Termdock hunk ID.
155
- --plugin-init Print the agent plugin manifest schema reference
159
+ --plugin-init Print the Agent integration protocol reference
156
160
  --plugin-create <file>
157
161
  Create a plugin from a manifest JSON file
158
162
  --plugin-list List installed agent plugins
@@ -161,7 +165,12 @@ Options:
161
165
  -h, --help Show this help message
162
166
 
163
167
  Short commands:
164
- 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
165
174
  plugin-create <file>
166
175
  Same as --plugin-create <file>
167
176
  plugin-list Same as --plugin-list
@@ -171,6 +180,8 @@ Short commands:
171
180
  st Same as --status
172
181
  x Same as --stop
173
182
  stop Same as --stop
183
+ update Update the global CLI from the official npm registry
184
+ upgrade Same as update
174
185
  l Same as --tls
175
186
  ls Same as --tls
176
187
  la Same as --tls --all
@@ -481,6 +492,7 @@ function parseArgs(argv) {
481
492
  let foreground = false;
482
493
  let status = false;
483
494
  let stop = false;
495
+ let update = false;
484
496
  let setPassword = false;
485
497
  let clearPassword = false;
486
498
  let tls = false;
@@ -499,9 +511,11 @@ function parseArgs(argv) {
499
511
  let changeAuditShow;
500
512
  let injectChangeAuditHunk;
501
513
  let pluginInit = false;
514
+ let pluginInitJson = false;
502
515
  let pluginCreate;
503
516
  let pluginList = false;
504
517
  let pluginRemove;
518
+ let agentEvent;
505
519
  // Short command aliases for the common path. Keep these positional-only so
506
520
  // long-form flags remain the single source of truth for option semantics.
507
521
  if (argv[0] && !argv[0].startsWith('-')) {
@@ -524,6 +538,10 @@ function parseArgs(argv) {
524
538
  stop = true;
525
539
  argv = argv.slice(1);
526
540
  }
541
+ else if (command === 'update' || command === 'upgrade') {
542
+ update = true;
543
+ argv = argv.slice(1);
544
+ }
527
545
  else if (command === 'p' || command === 'pw') {
528
546
  setPassword = true;
529
547
  argv = argv.slice(1);
@@ -597,9 +615,22 @@ function parseArgs(argv) {
597
615
  injectChangeAuditHunk = { id: next, cwd };
598
616
  argv = argv.slice(cwd ? 3 : 2);
599
617
  }
600
- else if (command === 'pi' || command === 'plugin-init') {
618
+ else if (command === 'pi' || command === 'plugin-init' || command === 'agent-plugin') {
601
619
  pluginInit = true;
602
- 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
+ }
603
634
  }
604
635
  else if (command === 'pcreate' || command === 'plugin-create') {
605
636
  if (next && !next.startsWith('-')) {
@@ -683,6 +714,10 @@ function parseArgs(argv) {
683
714
  stop = true;
684
715
  continue;
685
716
  }
717
+ if (arg === '--update') {
718
+ update = true;
719
+ continue;
720
+ }
686
721
  if (arg === '--set-password') {
687
722
  setPassword = true;
688
723
  continue;
@@ -700,7 +735,10 @@ function parseArgs(argv) {
700
735
  continue;
701
736
  }
702
737
  if (arg === '--json') {
703
- tlsJson = true;
738
+ if (pluginInit)
739
+ pluginInitJson = true;
740
+ else
741
+ tlsJson = true;
704
742
  continue;
705
743
  }
706
744
  if (arg === '--attach-tmux') {
@@ -855,6 +893,7 @@ function parseArgs(argv) {
855
893
  foreground,
856
894
  status,
857
895
  stop,
896
+ update,
858
897
  setPassword,
859
898
  clearPassword,
860
899
  tls,
@@ -873,9 +912,11 @@ function parseArgs(argv) {
873
912
  changeAuditShow,
874
913
  injectChangeAuditHunk,
875
914
  pluginInit,
915
+ pluginInitJson,
876
916
  pluginCreate,
877
917
  pluginList,
878
918
  pluginRemove,
919
+ agentEvent,
879
920
  };
880
921
  }
881
922
  // Reads a single line from stdin without echoing keystrokes. Used for password
@@ -1650,10 +1691,124 @@ function requireRunningServer() {
1650
1691
  }
1651
1692
  return { baseUrl: resolveServerBaseUrl(state), token };
1652
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
+ };
1729
+ function runPluginInitJson() {
1730
+ console.log(JSON.stringify({
1731
+ protocol: 'termdock-agent-plugin',
1732
+ version: 2,
1733
+ purpose: 'Register this Agent with Termdock and publish lifecycle/status updates.',
1734
+ commands: {
1735
+ guide: 'td agent-plugin --json',
1736
+ register: 'td plugin-create ./manifest.json',
1737
+ emit: 'td agent-event <slug> <event> [status]',
1738
+ inspect: 'td plugin-list',
1739
+ remove: 'td plugin-remove <slug>',
1740
+ },
1741
+ ui: {
1742
+ location: 'Settings > Detection Rules',
1743
+ registration: 'A successfully created plugin appears immediately without restarting Termdock.',
1744
+ install: 'The user clicks Install to write the declared hook mappings into hooks.target.',
1745
+ uninstall: 'The user clicks Uninstall to remove only Termdock-owned hook entries.',
1746
+ },
1747
+ lifecycleEvents: AGENT_EVENT_KINDS,
1748
+ phases: ['idle', 'working', 'waiting', 'done'],
1749
+ indicators: ['spinner', 'pulse', 'dot', 'ring', 'badge', 'terminal', 'question'],
1750
+ tones: ['neutral', 'info', 'success', 'warning', 'danger', 'accent'],
1751
+ statusModel: {
1752
+ phase: 'Stable Termdock behavior used for notifications and attention.',
1753
+ id: 'Agent-defined fine-grained status referenced by hook mappings or agent-event.',
1754
+ label: 'Human-readable status text.',
1755
+ indicator: 'Theme-safe visual indicator.',
1756
+ tone: 'Theme token; arbitrary status colors are not accepted.',
1757
+ },
1758
+ migrationV1: {
1759
+ errorCode: 'AGENT_PLUGIN_MANIFEST_V1_UNSUPPORTED',
1760
+ guideCommand: 'td agent-plugin --json',
1761
+ 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.',
1762
+ },
1763
+ hookPayload: {
1764
+ stdin: 'Optional JSON object.',
1765
+ recognizedFields: ['session_id', 'sessionId', 'message', 'cwd'],
1766
+ example: 'printf \'%s\' \'{"session_id":"abc","message":"Indexing"}\' | td agent-event my-agent prompt-submit thinking',
1767
+ },
1768
+ integrationSteps: [
1769
+ 'Generate a version 2 manifest using this schema and save it as JSON.',
1770
+ 'Run td plugin-create <manifest-file>; the Agent appears in Termdock immediately.',
1771
+ 'Ask the user to open Settings > Detection Rules and click Install for this Agent. They can uninstall it there later.',
1772
+ 'If the Agent hook format is not a Claude-style hooks map, configure native hooks to call td agent-event directly.',
1773
+ 'Run td plugin-list to verify registration.',
1774
+ ],
1775
+ manifest: AGENT_PLUGIN_EXAMPLE,
1776
+ }, null, 2));
1777
+ }
1778
+ function runAgentEvent(input) {
1779
+ const slugPattern = /^[a-z][a-z0-9-]{0,39}$/;
1780
+ if (!slugPattern.test(input.slug)) {
1781
+ console.error(`${ICON.err} ${c.red('agent-event slug must be lowercase alphanumeric with hyphens')}`);
1782
+ process.exit(1);
1783
+ }
1784
+ if (!AGENT_EVENT_KINDS.includes(input.event)) {
1785
+ console.error(`${ICON.err} ${c.red(`Unknown agent event: ${input.event}`)}`);
1786
+ console.error(` ${c.dim(`Expected one of: ${AGENT_EVENT_KINDS.join(', ')}`)}`);
1787
+ process.exit(1);
1788
+ }
1789
+ if (input.status && !slugPattern.test(input.status)) {
1790
+ console.error(`${ICON.err} ${c.red('agent-event status must be lowercase alphanumeric with hyphens')}`);
1791
+ process.exit(1);
1792
+ }
1793
+ const script = fileURLToPath(new URL('./agentHook.js', import.meta.url));
1794
+ const result = spawnSync(process.execPath, [script, 'agent-hook', input.slug, input.event, ...(input.status ? [input.status] : [])], {
1795
+ stdio: 'inherit',
1796
+ env: process.env,
1797
+ });
1798
+ if (result.error) {
1799
+ console.error(`${ICON.err} ${c.red(`Failed to emit Agent event: ${result.error.message}`)}`);
1800
+ process.exit(1);
1801
+ }
1802
+ process.exit(result.status ?? 0);
1803
+ }
1653
1804
  function runPluginInit() {
1654
1805
  console.log(`\
1655
- ${c.bold('Agent Plugin Reference')}
1656
- ${c.dim('─────────────────────')}
1806
+ ${c.bold('Termdock Agent Integration Protocol v2')}
1807
+ ${c.dim('──────────────────────────────────────')}
1808
+
1809
+ Machine-readable form for an Agent to inspect itself:
1810
+
1811
+ ${c.cyan('td agent-plugin --json')}
1657
1812
 
1658
1813
  A plugin is a directory under ${c.cyan('~/.termdock/agent-plugins/<slug>/')} containing:
1659
1814
 
@@ -1664,8 +1819,8 @@ ${c.bold('manifest.json schema')}
1664
1819
  ${c.dim('───────────────────')}
1665
1820
 
1666
1821
  {
1667
- ${c.dim('// Manifest version. Must be')} ${c.green('1')}${c.dim('.')}
1668
- "version": 1,
1822
+ ${c.dim('// Manifest version. Must be')} ${c.green('2')}${c.dim('.')}
1823
+ "version": 2,
1669
1824
 
1670
1825
  ${c.dim('// Unique slug (lowercase alphanum + hyphens, max 40 chars).')}
1671
1826
  ${c.dim('// Must not collide with built-in agent slugs.')}
@@ -1688,6 +1843,13 @@ ${c.dim('───────────────────')}
1688
1843
  ${c.dim('// "native" - SVG rendered directly, preserving original fills/gradients.')}
1689
1844
  "iconMode": "native",
1690
1845
 
1846
+ ${c.dim('// Optional fine-grained states. phase drives behavior; the rest drives UI.')}
1847
+ "statuses": [
1848
+ { "id": "thinking", "phase": "working", "label": "Thinking", "indicator": "spinner", "tone": "info" },
1849
+ { "id": "approval", "phase": "waiting", "label": "Needs approval", "indicator": "question", "tone": "warning" },
1850
+ { "id": "complete", "phase": "done", "label": "Complete", "indicator": "badge", "tone": "success" }
1851
+ ],
1852
+
1691
1853
  ${c.dim('// ── optional: hook installation (bring your own hooks.json surface) ──')}
1692
1854
  "hooks": {
1693
1855
  ${c.dim("// Path to the agent's hooks config file (~-abbreviated).")}
@@ -1698,10 +1860,10 @@ ${c.dim('───────────────────')}
1698
1860
  ${c.dim('// Optional matcher narrows the subscription (e.g. permission type).')}
1699
1861
  "events": [
1700
1862
  { "hook": "SessionStart", "event": "session-start" },
1701
- { "hook": "UserPromptSubmit", "event": "prompt-submit" },
1702
- { "hook": "Notification", "event": "notification", "matcher": "elicitation_dialog" },
1863
+ { "hook": "UserPromptSubmit", "event": "prompt-submit", "status": "thinking" },
1864
+ { "hook": "Notification", "event": "notification", "status": "approval", "matcher": "elicitation_dialog" },
1703
1865
  { "hook": "PostToolUse", "event": "tool-complete" },
1704
- { "hook": "Stop", "event": "stop" },
1866
+ { "hook": "Stop", "event": "stop", "status": "complete" },
1705
1867
  { "hook": "SessionEnd", "event": "session-end" }
1706
1868
  ]
1707
1869
  },
@@ -1730,28 +1892,48 @@ ${c.dim('─────────────────────')}
1730
1892
  ${c.bold('Creating a plugin')}
1731
1893
  ${c.dim('──────────────────')}
1732
1894
 
1733
- # 1. Create the plugin directory with manifest + icon
1734
- mkdir -p ~/.termdock/agent-plugins/trae
1735
- cp manifest.json icon.svg ~/.termdock/agent-plugins/trae/
1736
-
1737
- ${c.dim('# 2. Install the plugin via CLI (needs running server)')}
1738
- td plugin-create ~/.termdock/agent-plugins/trae/manifest.json
1739
-
1740
- ${c.dim('# 3. Or create directly from the manifest JSON')}
1895
+ ${c.dim('# Inject the Agent definition; it appears in the UI immediately')}
1741
1896
  td plugin-create ./trae-manifest.json
1742
1897
 
1743
- ${c.dim('# 4. List installed plugins')}
1898
+ ${c.dim('# The user then opens Settings > Detection Rules and clicks Install.')}
1899
+ ${c.dim('# The same UI button can uninstall the hooks later.')}
1900
+
1901
+ ${c.dim('# Verify registration')}
1744
1902
  td plugin-list
1745
1903
 
1746
- ${c.dim('# 5. Remove a plugin')}
1904
+ ${c.dim('# Remove')}
1747
1905
  td plugin-remove trae
1748
1906
 
1907
+ ${c.bold('Direct event bridge for any hook system')}
1908
+ ${c.dim('───────────────────────────────────────')}
1909
+
1910
+ If the Agent does not use a Claude-style hooks JSON file, configure its
1911
+ native hooks to invoke this stable public CLI command instead:
1912
+
1913
+ td agent-event trae prompt-submit thinking
1914
+ td agent-event trae permission-request approval
1915
+ td agent-event trae stop complete
1916
+
1917
+ Hook JSON may be piped on stdin. Termdock recognizes session_id/sessionId,
1918
+ message, and cwd:
1919
+
1920
+ printf '%s' '{"session_id":"abc","message":"Indexing"}' | td agent-event trae prompt-submit thinking
1921
+
1749
1922
  ${c.bold('Manifest validation at startup')}
1750
1923
  ${c.dim('───────────────────────────────')}
1751
1924
 
1752
- Termdock validates every plugin manifest on server start. Errors are logged
1925
+ Termdock validates every plugin manifest before registration. Errors are logged
1753
1926
  to ${c.cyan('~/.termdock/server.log')} and visible via ${c.cyan('td plugin-list')}${c.dim('.')}
1754
1927
  Slugs/aliases that collide with built-in agents are skipped.
1928
+
1929
+ ${c.bold('Migrating a v1 plugin')}
1930
+ ${c.dim('─────────────────────')}
1931
+
1932
+ v1 manifests fail with ${c.cyan('AGENT_PLUGIN_MANIFEST_V1_UNSUPPORTED')} and an
1933
+ AI-ready repair prompt. Copy that prompt together with the old manifest to
1934
+ an AI, or let the AI inspect the current schema directly:
1935
+
1936
+ ${c.cyan('td agent-plugin --json')}
1755
1937
  `);
1756
1938
  }
1757
1939
  async function runPluginCreate(manifestPath) {
@@ -1776,12 +1958,20 @@ async function runPluginCreate(manifestPath) {
1776
1958
  const response = await postLocalJson(baseUrl, token, '/api/terminal/agent-plugins', manifest);
1777
1959
  const body = JSON.parse(response.body || '{}');
1778
1960
  if (response.statusCode < 200 || response.statusCode >= 300) {
1779
- console.error(`${ICON.err} ${c.red(body.error || 'Failed to create plugin')}`);
1961
+ console.error(`${ICON.err} ${c.red(body.code ? `${body.code}:` : 'Plugin validation failed:')}`);
1962
+ console.error(body.error || 'Failed to create plugin');
1963
+ if (body.migration?.guideCommand) {
1964
+ console.error(`\n${c.bold('Current schema:')} ${c.cyan(body.migration.guideCommand)}`);
1965
+ }
1966
+ if (body.migration?.aiPrompt) {
1967
+ console.error(`\n${c.bold('Copy this to your AI:')}\n${body.migration.aiPrompt}`);
1968
+ }
1780
1969
  process.exit(1);
1781
1970
  }
1782
1971
  console.log(`${ICON.ok} ${c.green(`Plugin "${body.slug}" created.`)}`);
1783
1972
  console.log(` ${c.dim('Directory:')} ${c.cyan(body.dir ?? resolved)}`);
1784
- console.log(` ${c.dim('Restart Termdock to activate:')} ${c.cyan('td --stop && td')}`);
1973
+ console.log(` ${c.dim('Visible immediately in Settings > Detection Rules; no restart required.')}`);
1974
+ console.log(` ${c.dim('The user can click Install or Uninstall there.')}`);
1785
1975
  }
1786
1976
  async function runPluginList() {
1787
1977
  const { baseUrl, token } = requireRunningServer();
@@ -1794,7 +1984,7 @@ async function runPluginList() {
1794
1984
  const plugins = body.plugins ?? [];
1795
1985
  if (plugins.length === 0) {
1796
1986
  console.log(`${c.dim('No plugins installed.')}`);
1797
- console.log(` ${c.dim('See:')} ${c.cyan('td plugin-init')}`);
1987
+ console.log(` ${c.dim('See:')} ${c.cyan('td agent-plugin')}`);
1798
1988
  }
1799
1989
  else {
1800
1990
  console.log(`${c.bold('Installed plugins')} ${c.dim(`(${plugins.length})`)}`);
@@ -1817,7 +2007,13 @@ async function runPluginList() {
1817
2007
  console.log('');
1818
2008
  console.log(`${c.yellow('Validation errors')}`);
1819
2009
  for (const e of errors) {
1820
- console.log(` ${c.red('✗')} ${c.cyan(e.slug)}${c.dim(':')} ${e.errors.join('; ')}`);
2010
+ console.log(` ${c.red('✗')} ${c.cyan(e.slug)}${c.dim(':')} ${e.code ? c.yellow(e.code) : e.errors.join('; ')}`);
2011
+ for (const message of e.errors)
2012
+ console.log(` ${message}`);
2013
+ if (e.migration?.guideCommand)
2014
+ console.log(` ${c.dim('Current schema:')} ${c.cyan(e.migration.guideCommand)}`);
2015
+ if (e.migration?.aiPrompt)
2016
+ console.log(` ${c.dim('Copy this to your AI:')} ${e.migration.aiPrompt}`);
1821
2017
  }
1822
2018
  }
1823
2019
  }
@@ -2714,6 +2910,29 @@ async function refreshDefaultHttpsCertificateSafely() {
2714
2910
  }
2715
2911
  }
2716
2912
  async function main() {
2913
+ if (options.update) {
2914
+ console.log(`${ICON.info} ${c.dim(`Checking ${OFFICIAL_NPM_REGISTRY} for updates...`)}`);
2915
+ const result = await updateTermdockFromOfficialRegistry(TERMDOCK_VERSION, (stage, error) => {
2916
+ const action = stage === 'query' ? 'query' : 'installation';
2917
+ console.warn(`${ICON.warn} ${c.yellow(`Official npm registry ${action} failed; retrying with your configured npm registry.`)}`);
2918
+ console.warn(` ${c.dim(error.message)}`);
2919
+ });
2920
+ if (result.status === 'current') {
2921
+ console.log(`${ICON.ok} ${c.green(`Termdock ${result.currentVersion} is already up to date.`)}`);
2922
+ process.exit(0);
2923
+ }
2924
+ if (result.status === 'newer-than-registry') {
2925
+ console.log(`${ICON.info} ${c.dim(`Installed Termdock ${result.currentVersion} is newer than npm latest ${result.latestVersion}; nothing changed.`)}`);
2926
+ process.exit(0);
2927
+ }
2928
+ const sourceLabel = result.source === 'official' ? 'official npm registry' : 'configured npm registry';
2929
+ console.log(`${ICON.ok} ${c.green(`Updated Termdock ${result.currentVersion} → ${result.latestVersion} via ${sourceLabel}.`)}`);
2930
+ if (getRunningState()) {
2931
+ console.log(`${ICON.info} ${c.yellow('The running server is still using the previous version.')}`);
2932
+ console.log(` ${c.dim('Restart when convenient with:')} ${c.cyan('td --stop && td')}`);
2933
+ }
2934
+ process.exit(0);
2935
+ }
2717
2936
  if (options.setupLocalHttps) {
2718
2937
  try {
2719
2938
  await runSetupLocalHttps();
@@ -2772,8 +2991,14 @@ async function main() {
2772
2991
  runClearPassword();
2773
2992
  process.exit(0);
2774
2993
  }
2994
+ if (options.agentEvent) {
2995
+ runAgentEvent(options.agentEvent);
2996
+ }
2775
2997
  if (options.pluginInit) {
2776
- runPluginInit();
2998
+ if (options.pluginInitJson)
2999
+ runPluginInitJson();
3000
+ else
3001
+ runPluginInit();
2777
3002
  process.exit(0);
2778
3003
  }
2779
3004
  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) {