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.
- package/.desktop-dist/main.js +254 -49
- package/README.md +6 -0
- package/dist/client/assets/{ModelPreview-Bk8GM7G-.js → ModelPreview-CG6vLwyw.js} +1 -1
- package/dist/client/assets/{abnfDiagram-VRR7QNED-BYiQXGYk.js → abnfDiagram-VRR7QNED-CrXEZLWX.js} +1 -1
- package/dist/client/assets/{arc-jlT_jmoZ.js → arc-DbKGR7Ex.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-D1qZ875Q.js → architectureDiagram-ZJ3FMSHR-PF5BoUbI.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-qkgn2Ysc.js → blockDiagram-677ZJIJ3-D1gRzVWB.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-JgcH49Pf.js → c4Diagram-LMCZKHZV-CwvOLync.js} +1 -1
- package/dist/client/assets/channel-JTEuKVy6.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-E-3iflEy.js → chunk-2Q5K7J3B-BeRdP-xP.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-mM01y3kF.js → chunk-32BRIVSS-CrqR5rvl.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-GO8Muo5l.js → chunk-5VM5RSS4-CjbUr-ko.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-BUSDluH0.js → chunk-EX3LRPZG-BVEk3Wma.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-obSL6EzJ.js → chunk-JWPE2WC7-WitJe8kR.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-zEktbtAG.js → chunk-MOJQB5TN-BCewlLYk.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-kH7H8XgQ.js → chunk-RYQCIY6F-CoP0AA4G.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-BqDFpHKN.js → chunk-V7JOEXUC-CSZrnmp_.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-BGZ7kCIG.js → chunk-VR4S4FIN-BB-7zz-9.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-D-TatWXe.js → chunk-XXDRQBXY-CcZZlCik.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-BiFJB_5s.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-BiFJB_5s.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-CfWyrViz.js → cose-bilkent-JH36ORCC-DcmzUDwh.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-BFdZ3FcE.js → cynefin-VYW2F7L2-C1XlDVoJ.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-iAfCVoFU.js → cynefinDiagram-TSTJHNR4-zY6Aet2S.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-CZhh0hrr.js → dagre-VKFMJZFB-DqGTi6zR.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-BqUK2OmV.js → diagram-FQU43EPY-DStcsU7S.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-vC0pSQYC.js → diagram-G47NLZAW-Cs6IMNip.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-Dqc5oxed.js → diagram-NH7WQ7WH-yA2bpWN7.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-DIIXckFE.js → diagram-OA4YK3LP-Dzf9M0PI.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-DR9_cFeh.js → diagram-WEI45ONY-D75lpQKA.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-21OO0OXC.js → ebnfDiagram-CCIWWBDH-CkQvIpWy.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-YKnVG3az.js → erDiagram-Q63AITRT-Ca00Vjxz.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-Cm7TGErF.js → flowDiagram-23GEKE2U-CGrnSTEp.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-D_76Effz.js → ganttDiagram-NO4QXBWP-0SHfOIP4.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CC1ysmVD.js → gitGraphDiagram-IHSO6WYX-BeigYWvs.js} +1 -1
- package/dist/client/assets/index-D5PKdbhW.js +451 -0
- package/dist/client/assets/index-DhfIiBLW.css +1 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-BbgpmhBz.js → infoDiagram-FWYZ7A6U-CiY1-xjN.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-C94bY_je.js → ishikawaDiagram-FXEZZL3T-CmtDjUm_.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-BQ0iFRxs.js → journeyDiagram-5HDEW3XC-B7qOr9LY.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-BtAexXPo.js → kanban-definition-HUTT4EX6-DO2P2Um3.js} +1 -1
- package/dist/client/assets/{linear-C2HnIdOK.js → linear-B832hJt0.js} +1 -1
- package/dist/client/assets/{mermaid.core-BbG4sJBF.js → mermaid.core-CsAzdiCU.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-B-uvViws.js → mindmap-definition-LN4V7U3C-HI__vXhQ.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-DHYcR6tE.js → pegDiagram-2B236MQR--rZIwjXq.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-DEE9cD6M.js → pieDiagram-ENE6RG2P-D9jPf5x9.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DMQP5jG3.js → quadrantDiagram-ABIIQ3AL-DTCP5ji2.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-B-4WDTwc.js → railroadDiagram-RFXS5EU6-D2Gspvrl.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-C2xw_q-R.js → requirementDiagram-TGXJPOKE-DEJC02IW.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-wq0BPqhc.js → sankeyDiagram-HTMAVEWB-opOhKPap.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-BqpM0A9d.js → sequenceDiagram-DBY2YBRQ-_sz2CRAG.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-Ba2xxyp_.js → sizeCapture-X5ZJPWSS-D0Q2QkYF.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-agPx5qAN.js → stateDiagram-2N3HPSRC-VgeSQTtW.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BVh2FRO-.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-Cy6Lzj_L.js → swimlanes-5IMT3BWC-ClvBvSl0.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-C17V-kgm.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-LBrVfbdp.js → timeline-definition-FHXFAJF6-BXNsrECV.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-BiYIV7mX.js → vennDiagram-L72KCM5P-BgwiqZd-.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-Q1QU9gG3.js → wardleyDiagram-EHGQE667-DiqN5ERx.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-CzYpxk4l.js → xychartDiagram-FW5EYKEG-DQjFVqC6.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/agent/autoTitle.js +95 -0
- package/dist/server/agent/installers.js +106 -22
- package/dist/server/agent/plugins.js +135 -4
- package/dist/server/agent/registry.js +2 -1
- package/dist/server/agent/session.js +27 -2
- package/dist/server/agent/titleNamerCatalog.js +301 -0
- package/dist/server/agentHook.js +49 -1
- package/dist/server/cli.js +238 -30
- package/dist/server/routes/filesystem.js +11 -8
- package/dist/server/routes/terminal.js +382 -14
- package/dist/server/utils/autoUpdate.js +175 -0
- package/dist/server/utils/npmUpdate.js +2 -1
- package/dist/server/utils/settings.js +43 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
- package/dist/client/assets/channel-C2i7H7bH.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-BlGG6XFj.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-BlGG6XFj.js +0 -1
- package/dist/client/assets/index-ChhyERKX.css +0 -1
- package/dist/client/assets/index-r-PkH7SB.js +0 -442
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-beoQXpWJ.js +0 -1
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
779
|
-
:
|
|
780
|
-
if (ours !==
|
|
853
|
+
? list.find((entry) => markerCommand(entry, mark) !== null)
|
|
854
|
+
: undefined;
|
|
855
|
+
if (ours !== undefined) {
|
|
781
856
|
any = true;
|
|
782
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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)
|