termdock 1.4.70 → 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/dist/client/assets/{ModelPreview-Cs14IQuQ.js → ModelPreview-CG6vLwyw.js} +1 -1
- package/dist/client/assets/{abnfDiagram-VRR7QNED-DuDfdv-6.js → abnfDiagram-VRR7QNED-CrXEZLWX.js} +1 -1
- package/dist/client/assets/{arc-CSYzGV_2.js → arc-DbKGR7Ex.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-YiZ0HXNi.js → architectureDiagram-ZJ3FMSHR-PF5BoUbI.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-IaQMzmYv.js → blockDiagram-677ZJIJ3-D1gRzVWB.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-BwyuIPRL.js → c4Diagram-LMCZKHZV-CwvOLync.js} +1 -1
- package/dist/client/assets/channel-JTEuKVy6.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-BtHQYLBT.js → chunk-2Q5K7J3B-BeRdP-xP.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-CXrK07sW.js → chunk-32BRIVSS-CrqR5rvl.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-BfBshn11.js → chunk-5VM5RSS4-CjbUr-ko.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-BJbilNaf.js → chunk-EX3LRPZG-BVEk3Wma.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-wgcmSRlL.js → chunk-JWPE2WC7-WitJe8kR.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-8VZVUeDI.js → chunk-MOJQB5TN-BCewlLYk.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-B900YBFa.js → chunk-RYQCIY6F-CoP0AA4G.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-BK4_RLz-.js → chunk-V7JOEXUC-CSZrnmp_.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-Kj5_ycjB.js → chunk-VR4S4FIN-BB-7zz-9.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-BolvPLRz.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-9_9_SLqv.js → cose-bilkent-JH36ORCC-DcmzUDwh.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-yEpoi3vG.js → cynefin-VYW2F7L2-C1XlDVoJ.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-Dy-gvup_.js → cynefinDiagram-TSTJHNR4-zY6Aet2S.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-CNjJ5pRK.js → dagre-VKFMJZFB-DqGTi6zR.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-uZ73Rj3T.js → diagram-FQU43EPY-DStcsU7S.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-Cxt7TYZT.js → diagram-G47NLZAW-Cs6IMNip.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-B4TKinuZ.js → diagram-NH7WQ7WH-yA2bpWN7.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-CGSuenHU.js → diagram-OA4YK3LP-Dzf9M0PI.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-DqqzDaUZ.js → diagram-WEI45ONY-D75lpQKA.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-B2SgXL5w.js → ebnfDiagram-CCIWWBDH-CkQvIpWy.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-D-ibkr8y.js → erDiagram-Q63AITRT-Ca00Vjxz.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-BPeyMqr-.js → flowDiagram-23GEKE2U-CGrnSTEp.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-DcEqEzzu.js → ganttDiagram-NO4QXBWP-0SHfOIP4.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-DTZ6UCQY.js → gitGraphDiagram-IHSO6WYX-BeigYWvs.js} +1 -1
- package/dist/client/assets/{index-ivnGnbVg.js → index-D5PKdbhW.js} +58 -58
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-BHrzVXeX.js → infoDiagram-FWYZ7A6U-CiY1-xjN.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-CwuoQJku.js → ishikawaDiagram-FXEZZL3T-CmtDjUm_.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-ZEcy0rDZ.js → journeyDiagram-5HDEW3XC-B7qOr9LY.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-BDBawNbr.js → kanban-definition-HUTT4EX6-DO2P2Um3.js} +1 -1
- package/dist/client/assets/{linear-smYQWzDV.js → linear-B832hJt0.js} +1 -1
- package/dist/client/assets/{mermaid.core-eJs7Ouvw.js → mermaid.core-CsAzdiCU.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-BK1IZ9zg.js → mindmap-definition-LN4V7U3C-HI__vXhQ.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-B3-mVNgX.js → pegDiagram-2B236MQR--rZIwjXq.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-Bkeeyx5l.js → pieDiagram-ENE6RG2P-D9jPf5x9.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DouIklEO.js → quadrantDiagram-ABIIQ3AL-DTCP5ji2.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-BB_jXQd2.js → railroadDiagram-RFXS5EU6-D2Gspvrl.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-u56IHfiR.js → requirementDiagram-TGXJPOKE-DEJC02IW.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-eDi2cs5E.js → sankeyDiagram-HTMAVEWB-opOhKPap.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-DnAA2xYa.js → sequenceDiagram-DBY2YBRQ-_sz2CRAG.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-BogPui79.js → sizeCapture-X5ZJPWSS-D0Q2QkYF.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC--EeXDy2M.js → stateDiagram-2N3HPSRC-VgeSQTtW.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BVh2FRO-.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-DZAw7-kY.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-DkcnpB-Y.js → timeline-definition-FHXFAJF6-BXNsrECV.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-BStcDGEf.js → vennDiagram-L72KCM5P-BgwiqZd-.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-Cj4CQn1M.js → wardleyDiagram-EHGQE667-DiqN5ERx.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-_uBCjai8.js → xychartDiagram-FW5EYKEG-DQjFVqC6.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/client/sw.js +1 -1
- package/dist/server/agent/autoTitle.js +14 -37
- package/dist/server/agent/installers.js +98 -15
- package/dist/server/agent/plugins.js +45 -0
- package/dist/server/agent/registry.js +1 -1
- package/dist/server/agent/session.js +10 -1
- package/dist/server/agent/titleNamerCatalog.js +100 -2
- package/dist/server/agentHook.js +47 -0
- package/dist/server/cli.js +20 -0
- package/dist/server/routes/terminal.js +37 -10
- package/dist/server/utils/settings.js +4 -5
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
- package/dist/client/assets/channel-BotxtiNE.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-CLWlbTSC.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-CLWlbTSC.js +0 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Dc-3HKTs.js +0 -1
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-CI1Wlr0V.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') {
|
|
@@ -700,7 +771,7 @@ function hookCommandForPlugin(slug, event, status) {
|
|
|
700
771
|
* List all agents with hook support — both built-in and plugin-defined.
|
|
701
772
|
* Plugin agents appear after built-in agents.
|
|
702
773
|
*/
|
|
703
|
-
export function listAllHookAgents() {
|
|
774
|
+
export async function listAllHookAgents() {
|
|
704
775
|
const builtIn = HOOK_AGENTS.map((slug) => {
|
|
705
776
|
const agent = agentBySlug(slug);
|
|
706
777
|
return {
|
|
@@ -727,6 +798,9 @@ export function listAllHookAgents() {
|
|
|
727
798
|
iconVersion: agent?.iconVersion ?? null,
|
|
728
799
|
};
|
|
729
800
|
});
|
|
801
|
+
const codex = builtIn.find((entry) => entry.slug === 'codex');
|
|
802
|
+
if (codex)
|
|
803
|
+
codex.state = await codexRuntimeHooksState();
|
|
730
804
|
return [...builtIn, ...pluginAgents];
|
|
731
805
|
}
|
|
732
806
|
export function hooksStateForSlug(slug) {
|
|
@@ -763,7 +837,7 @@ export async function uninstallHooksForSlug(slug) {
|
|
|
763
837
|
// Generic hookmap logic (shared with built-in Claude/Codex + all plugins)
|
|
764
838
|
// ---------------------------------------------------------------------------
|
|
765
839
|
/** Generic state check for a hookmap-style hooks file. */
|
|
766
|
-
function pluginHooksState(slug, file, events) {
|
|
840
|
+
export function pluginHooksState(slug, file, events) {
|
|
767
841
|
const root = readJsonObject(file);
|
|
768
842
|
if (!root)
|
|
769
843
|
return 'not-installed';
|
|
@@ -771,16 +845,22 @@ function pluginHooksState(slug, file, events) {
|
|
|
771
845
|
const hooks = root.hooks;
|
|
772
846
|
let any = false;
|
|
773
847
|
let complete = true;
|
|
774
|
-
for (const [hookEvent, sentinel, , , status] of events) {
|
|
848
|
+
for (const [hookEvent, sentinel, matcher, timeout, status] of events) {
|
|
775
849
|
const list = hooks && typeof hooks === 'object'
|
|
776
850
|
? hooks[hookEvent]
|
|
777
851
|
: null;
|
|
778
852
|
const ours = Array.isArray(list)
|
|
779
|
-
? list.
|
|
780
|
-
:
|
|
781
|
-
if (ours !==
|
|
853
|
+
? list.find((entry) => markerCommand(entry, mark) !== null)
|
|
854
|
+
: undefined;
|
|
855
|
+
if (ours !== undefined) {
|
|
782
856
|
any = true;
|
|
783
|
-
|
|
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)
|
|
784
864
|
complete = false;
|
|
785
865
|
}
|
|
786
866
|
else {
|
|
@@ -792,7 +872,7 @@ function pluginHooksState(slug, file, events) {
|
|
|
792
872
|
return complete ? 'installed' : 'outdated';
|
|
793
873
|
}
|
|
794
874
|
/** Generic install for a hookmap-style hooks file. */
|
|
795
|
-
function installPluginHooks(slug, file, events) {
|
|
875
|
+
export function installPluginHooks(slug, file, events) {
|
|
796
876
|
const { devMode } = resolveHookScript();
|
|
797
877
|
let root = {};
|
|
798
878
|
try {
|
|
@@ -815,7 +895,7 @@ function installPluginHooks(slug, file, events) {
|
|
|
815
895
|
}
|
|
816
896
|
const hooks = root.hooks;
|
|
817
897
|
const mark = marker(slug);
|
|
818
|
-
for (const [hookEvent, sentinel, matcher, , status] of events) {
|
|
898
|
+
for (const [hookEvent, sentinel, matcher, timeout, status] of events) {
|
|
819
899
|
const command = hookCommandForPlugin(slug, sentinel, status);
|
|
820
900
|
let list = hooks[hookEvent];
|
|
821
901
|
if (list === undefined) {
|
|
@@ -825,7 +905,10 @@ function installPluginHooks(slug, file, events) {
|
|
|
825
905
|
if (!Array.isArray(list))
|
|
826
906
|
continue;
|
|
827
907
|
const kept = list.filter((entry) => markerCommand(entry, mark) === null);
|
|
828
|
-
const
|
|
908
|
+
const handler = { type: 'command', command };
|
|
909
|
+
if (timeout !== undefined)
|
|
910
|
+
handler.timeout = timeout;
|
|
911
|
+
const entry = { hooks: [handler] };
|
|
829
912
|
if (matcher)
|
|
830
913
|
entry.matcher = matcher;
|
|
831
914
|
kept.push(entry);
|
|
@@ -236,6 +236,50 @@ export function validateManifest(raw, dir) {
|
|
|
236
236
|
};
|
|
237
237
|
}
|
|
238
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
|
+
}
|
|
239
283
|
if (errors.length > 0) {
|
|
240
284
|
return {
|
|
241
285
|
error: {
|
|
@@ -259,6 +303,7 @@ export function validateManifest(raw, dir) {
|
|
|
259
303
|
statuses,
|
|
260
304
|
hooks,
|
|
261
305
|
resume: resumeConfig,
|
|
306
|
+
titleNamer,
|
|
262
307
|
},
|
|
263
308
|
};
|
|
264
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;
|
|
@@ -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',
|
|
@@ -140,13 +147,15 @@ export function parseAgentEvent(payload) {
|
|
|
140
147
|
const eventName = nonEmpty(w.event);
|
|
141
148
|
if (!eventName || !EVENT_KINDS.has(eventName))
|
|
142
149
|
return null;
|
|
143
|
-
const
|
|
150
|
+
const agentSlug = nonEmpty(w.agent);
|
|
151
|
+
const agent = agentBySlug(agentSlug);
|
|
144
152
|
const statusId = nonEmpty(w.status);
|
|
145
153
|
const status = statusId
|
|
146
154
|
? agent?.statuses?.find((definition) => definition.id === statusId) ?? null
|
|
147
155
|
: null;
|
|
148
156
|
return {
|
|
149
157
|
agent,
|
|
158
|
+
agentSlug,
|
|
150
159
|
kind: eventName,
|
|
151
160
|
sessionId: nonEmpty(w.session_id),
|
|
152
161
|
message: nonEmpty(w.message),
|
|
@@ -3,12 +3,16 @@ import fs from 'fs';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { promisify } from 'util';
|
|
6
|
+
import { loadPlugins } from './plugins.js';
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
7
8
|
const CACHE_FRESH_MS = 24 * 60 * 60_000;
|
|
8
9
|
const PROBE_TIMEOUT_MS = 10_000;
|
|
9
10
|
const CACHE_FILE = path.join(os.homedir(), '.termdock', 'title-namer-catalog.json');
|
|
10
11
|
let cached = null;
|
|
11
12
|
let refreshPromise = null;
|
|
13
|
+
export function invalidateTitleNamerCatalog() {
|
|
14
|
+
cached = null;
|
|
15
|
+
}
|
|
12
16
|
function normalizeCachedCatalog(input) {
|
|
13
17
|
if (!input || typeof input !== 'object')
|
|
14
18
|
return null;
|
|
@@ -19,7 +23,7 @@ function normalizeCachedCatalog(input) {
|
|
|
19
23
|
if (!entry || typeof entry !== 'object')
|
|
20
24
|
return [];
|
|
21
25
|
const candidate = entry;
|
|
22
|
-
if (candidate.slug !== '
|
|
26
|
+
if (typeof candidate.slug !== 'string' || !/^[a-z][a-z0-9-]{0,39}$/.test(candidate.slug))
|
|
23
27
|
return [];
|
|
24
28
|
if (!Array.isArray(candidate.models))
|
|
25
29
|
return [];
|
|
@@ -155,11 +159,89 @@ async function listClaudeModels() {
|
|
|
155
159
|
return parseClaudeSupportedModels(`${candidate.stdout ?? ''}\n${candidate.stderr ?? ''}`);
|
|
156
160
|
}
|
|
157
161
|
}
|
|
162
|
+
function normalizeDiscoveredModels(input) {
|
|
163
|
+
if (!Array.isArray(input))
|
|
164
|
+
return [];
|
|
165
|
+
return input.flatMap((entry) => {
|
|
166
|
+
if (typeof entry === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(entry)) {
|
|
167
|
+
return [{ id: entry, displayName: entry, description: '', isDefault: false }];
|
|
168
|
+
}
|
|
169
|
+
if (!entry || typeof entry !== 'object')
|
|
170
|
+
return [];
|
|
171
|
+
const model = entry;
|
|
172
|
+
if (typeof model.id !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(model.id))
|
|
173
|
+
return [];
|
|
174
|
+
return [{
|
|
175
|
+
id: model.id,
|
|
176
|
+
displayName: typeof model.displayName === 'string' ? model.displayName : model.id,
|
|
177
|
+
description: typeof model.description === 'string' ? model.description : '',
|
|
178
|
+
isDefault: model.isDefault === true,
|
|
179
|
+
}];
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async function listPluginModels(config) {
|
|
183
|
+
if (!config.models)
|
|
184
|
+
return [];
|
|
185
|
+
const { stdout } = await execFileAsync(config.models.command, config.models.args ?? [], {
|
|
186
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
187
|
+
maxBuffer: 256 * 1024,
|
|
188
|
+
env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
|
|
189
|
+
});
|
|
190
|
+
return normalizeDiscoveredModels(JSON.parse(stdout));
|
|
191
|
+
}
|
|
192
|
+
function pluginTitleNamers() {
|
|
193
|
+
return loadPlugins().plugins.flatMap((plugin) => plugin.manifest.titleNamer
|
|
194
|
+
? [{
|
|
195
|
+
slug: plugin.manifest.slug,
|
|
196
|
+
displayName: plugin.manifest.displayName,
|
|
197
|
+
config: plugin.manifest.titleNamer,
|
|
198
|
+
}]
|
|
199
|
+
: []);
|
|
200
|
+
}
|
|
201
|
+
export async function runTitleNamer(slug, prompt, model) {
|
|
202
|
+
let command;
|
|
203
|
+
let args;
|
|
204
|
+
if (slug === 'codex') {
|
|
205
|
+
command = 'codex';
|
|
206
|
+
args = ['exec', '--ephemeral', '--skip-git-repo-check', '--color', 'never', ...(model ? ['--model', model] : []), prompt];
|
|
207
|
+
}
|
|
208
|
+
else if (slug === 'claude') {
|
|
209
|
+
command = 'claude';
|
|
210
|
+
args = [...(model ? ['--model', model] : []), '--no-session-persistence', '-p', prompt];
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const provider = pluginTitleNamers().find((entry) => entry.slug === slug);
|
|
214
|
+
if (!provider)
|
|
215
|
+
return null;
|
|
216
|
+
command = provider.config.command;
|
|
217
|
+
args = provider.config.args.flatMap((arg) => {
|
|
218
|
+
if (arg.includes('{model}') && !model)
|
|
219
|
+
return [];
|
|
220
|
+
return [arg.replaceAll('{prompt}', prompt).replaceAll('{model}', model ?? '')];
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const { stdout } = await execFileAsync(command, args, {
|
|
225
|
+
timeout: 45_000,
|
|
226
|
+
maxBuffer: 256 * 1024,
|
|
227
|
+
env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
|
|
228
|
+
});
|
|
229
|
+
return stdout;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
158
235
|
async function refreshTitleNamerCatalog() {
|
|
159
236
|
if (refreshPromise)
|
|
160
237
|
return refreshPromise;
|
|
161
238
|
refreshPromise = (async () => {
|
|
162
|
-
const
|
|
239
|
+
const plugins = pluginTitleNamers();
|
|
240
|
+
const [codex, claude, ...pluginResults] = await Promise.allSettled([
|
|
241
|
+
listCodexModels(),
|
|
242
|
+
listClaudeModels(),
|
|
243
|
+
...plugins.map((provider) => listPluginModels(provider.config)),
|
|
244
|
+
]);
|
|
163
245
|
const previousCodex = cached?.value.find((namer) => namer.slug === 'codex');
|
|
164
246
|
const previousClaude = cached?.value.find((namer) => namer.slug === 'claude');
|
|
165
247
|
const codexModels = codex.status === 'fulfilled' ? codex.value : previousCodex?.models ?? [];
|
|
@@ -179,6 +261,18 @@ async function refreshTitleNamerCatalog() {
|
|
|
179
261
|
models: claudeModels,
|
|
180
262
|
recommendedModel: recommendTitleModel(claudeModels),
|
|
181
263
|
},
|
|
264
|
+
...plugins.map((provider, index) => {
|
|
265
|
+
const result = pluginResults[index];
|
|
266
|
+
const previous = cached?.value.find((namer) => namer.slug === provider.slug);
|
|
267
|
+
const models = result?.status === 'fulfilled' ? result.value : previous?.models ?? [];
|
|
268
|
+
return {
|
|
269
|
+
slug: provider.slug,
|
|
270
|
+
displayName: provider.displayName,
|
|
271
|
+
available: provider.config.models ? result?.status === 'fulfilled' || previous?.available === true : true,
|
|
272
|
+
models,
|
|
273
|
+
recommendedModel: recommendTitleModel(models),
|
|
274
|
+
};
|
|
275
|
+
}),
|
|
182
276
|
];
|
|
183
277
|
cached = { version: 1, updatedAt: Date.now(), value };
|
|
184
278
|
await savePersistentCache(cached).catch(() => undefined);
|
|
@@ -195,6 +289,10 @@ export async function getTitleNamerCatalog(force = false) {
|
|
|
195
289
|
cached = await loadPersistentCache();
|
|
196
290
|
if (!cached)
|
|
197
291
|
return refreshTitleNamerCatalog();
|
|
292
|
+
const expectedSlugs = new Set(['codex', 'claude', ...pluginTitleNamers().map((provider) => provider.slug)]);
|
|
293
|
+
if (cached.value.length !== expectedSlugs.size || cached.value.some((entry) => !expectedSlugs.has(entry.slug))) {
|
|
294
|
+
return refreshTitleNamerCatalog();
|
|
295
|
+
}
|
|
198
296
|
if (Date.now() - cached.updatedAt > CACHE_FRESH_MS) {
|
|
199
297
|
// Stale-while-revalidate: callers get the last known catalog immediately.
|
|
200
298
|
void refreshTitleNamerCatalog();
|
package/dist/server/agentHook.js
CHANGED
|
@@ -103,6 +103,47 @@ function ancestorTtyDevice() {
|
|
|
103
103
|
}
|
|
104
104
|
return null;
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* tmux passthrough is version/configuration sensitive and, more importantly,
|
|
108
|
+
* only works while tmux considers the escape sequence pane output. Termdock
|
|
109
|
+
* itself is an attached tmux client, so write the sentinel to that client's
|
|
110
|
+
* tty directly when possible. This also works for detached hook subprocesses
|
|
111
|
+
* whose own `/dev/tty` is unavailable.
|
|
112
|
+
*/
|
|
113
|
+
function tmuxClientTtyDevices() {
|
|
114
|
+
const pane = process.env.TMUX_PANE;
|
|
115
|
+
if (!process.env.TMUX || !pane)
|
|
116
|
+
return [];
|
|
117
|
+
try {
|
|
118
|
+
const output = execFileSync('tmux', ['list-clients', '-t', pane, '-F', '#{client_tty}\t#{client_pid}'], {
|
|
119
|
+
encoding: 'utf8',
|
|
120
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
121
|
+
timeout: 2000,
|
|
122
|
+
});
|
|
123
|
+
return [...new Set(output.split(/\r?\n/).flatMap((line) => {
|
|
124
|
+
const [device, pid] = line.trim().split('\t');
|
|
125
|
+
if (!/^\/dev\/(?:pts\/\d+|tty[^/]*)$/.test(device ?? ''))
|
|
126
|
+
return [];
|
|
127
|
+
// On Linux the environment gives us an exact ownership marker. An
|
|
128
|
+
// ordinary `tmux attach` must not steal a Termdock sentinel merely
|
|
129
|
+
// because tmux listed that client first.
|
|
130
|
+
const environ = pid && /^\d+$/.test(pid) ? `/proc/${pid}/environ` : null;
|
|
131
|
+
if (environ && fs.existsSync(environ)) {
|
|
132
|
+
try {
|
|
133
|
+
if (!fs.readFileSync(environ).toString().split('\0').includes('TERMDOCK=1'))
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return [device];
|
|
141
|
+
}))];
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
106
147
|
/**
|
|
107
148
|
* Write the sequence to the pane's PTY so the server sniffs it as pane output.
|
|
108
149
|
* Two routes: /dev/tty (hook attached to its tty), or an ancestor's tty
|
|
@@ -116,6 +157,12 @@ function ancestorTtyDevice() {
|
|
|
116
157
|
* the termdock server — as the plain inner OSC.
|
|
117
158
|
*/
|
|
118
159
|
function writeToControllingTty(sequence, inTmuxPane) {
|
|
160
|
+
if (inTmuxPane) {
|
|
161
|
+
const clientTtys = tmuxClientTtyDevices();
|
|
162
|
+
if (clientTtys.length > 0) {
|
|
163
|
+
return clientTtys.some((device) => writeDev(device, sequence));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
119
166
|
// Only genuine tmux panes (TMUX_PANE is set by tmux itself) get the DCS
|
|
120
167
|
// passthrough wrap; a bare inherited TMUX just means an ancestor ran tmux.
|
|
121
168
|
const wire = inTmuxPane
|
package/dist/server/cli.js
CHANGED
|
@@ -1725,6 +1725,14 @@ const AGENT_PLUGIN_EXAMPLE = {
|
|
|
1725
1725
|
command: 'my-agent --resume {sessionId}',
|
|
1726
1726
|
staleFlags: ['--resume'],
|
|
1727
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
|
+
},
|
|
1728
1736
|
};
|
|
1729
1737
|
function runPluginInitJson() {
|
|
1730
1738
|
console.log(JSON.stringify({
|
|
@@ -1770,6 +1778,8 @@ function runPluginInitJson() {
|
|
|
1770
1778
|
'Run td plugin-create <manifest-file>; the Agent appears in Termdock immediately.',
|
|
1771
1779
|
'Ask the user to open Settings > Detection Rules and click Install for this Agent. They can uninstall it there later.',
|
|
1772
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.',
|
|
1773
1783
|
'Run td plugin-list to verify registration.',
|
|
1774
1784
|
],
|
|
1775
1785
|
manifest: AGENT_PLUGIN_EXAMPLE,
|
|
@@ -1874,6 +1884,15 @@ ${c.dim('───────────────────')}
|
|
|
1874
1884
|
"command": "trae --resume {sessionId}",
|
|
1875
1885
|
${c.dim('// Flags to strip from the original launch argv when replaying the resume.')}
|
|
1876
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"] }
|
|
1877
1896
|
}
|
|
1878
1897
|
}
|
|
1879
1898
|
|
|
@@ -1887,6 +1906,7 @@ ${c.dim('─────────────────────')}
|
|
|
1887
1906
|
tool-complete A tool execution completed (unblocks waiting state)
|
|
1888
1907
|
notification Generic notification from the agent (heuristic gated)
|
|
1889
1908
|
stop Agent turn finished / went idle
|
|
1909
|
+
Also triggers automatic title generation when enabled
|
|
1890
1910
|
session-end Agent session closed / binary exited
|
|
1891
1911
|
|
|
1892
1912
|
${c.bold('Creating a plugin')}
|