termdock 1.4.69 → 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.
- package/.desktop-dist/main.js +254 -49
- package/README.md +6 -0
- package/dist/client/assets/{ModelPreview-Bk8GM7G-.js → ModelPreview-Cs14IQuQ.js} +1 -1
- package/dist/client/assets/{abnfDiagram-VRR7QNED-BYiQXGYk.js → abnfDiagram-VRR7QNED-DuDfdv-6.js} +1 -1
- package/dist/client/assets/{arc-jlT_jmoZ.js → arc-CSYzGV_2.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-D1qZ875Q.js → architectureDiagram-ZJ3FMSHR-YiZ0HXNi.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-qkgn2Ysc.js → blockDiagram-677ZJIJ3-IaQMzmYv.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-JgcH49Pf.js → c4Diagram-LMCZKHZV-BwyuIPRL.js} +1 -1
- package/dist/client/assets/channel-BotxtiNE.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-E-3iflEy.js → chunk-2Q5K7J3B-BtHQYLBT.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-mM01y3kF.js → chunk-32BRIVSS-CXrK07sW.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-GO8Muo5l.js → chunk-5VM5RSS4-BfBshn11.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-BUSDluH0.js → chunk-EX3LRPZG-BJbilNaf.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-obSL6EzJ.js → chunk-JWPE2WC7-wgcmSRlL.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-zEktbtAG.js → chunk-MOJQB5TN-8VZVUeDI.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-kH7H8XgQ.js → chunk-RYQCIY6F-B900YBFa.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-BqDFpHKN.js → chunk-V7JOEXUC-BK4_RLz-.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-BGZ7kCIG.js → chunk-VR4S4FIN-Kj5_ycjB.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-D-TatWXe.js → chunk-XXDRQBXY-BolvPLRz.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-CLWlbTSC.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-CLWlbTSC.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-CfWyrViz.js → cose-bilkent-JH36ORCC-9_9_SLqv.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-BFdZ3FcE.js → cynefin-VYW2F7L2-yEpoi3vG.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-iAfCVoFU.js → cynefinDiagram-TSTJHNR4-Dy-gvup_.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-CZhh0hrr.js → dagre-VKFMJZFB-CNjJ5pRK.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-BqUK2OmV.js → diagram-FQU43EPY-uZ73Rj3T.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-vC0pSQYC.js → diagram-G47NLZAW-Cxt7TYZT.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-Dqc5oxed.js → diagram-NH7WQ7WH-B4TKinuZ.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-DIIXckFE.js → diagram-OA4YK3LP-CGSuenHU.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-DR9_cFeh.js → diagram-WEI45ONY-DqqzDaUZ.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-21OO0OXC.js → ebnfDiagram-CCIWWBDH-B2SgXL5w.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-YKnVG3az.js → erDiagram-Q63AITRT-D-ibkr8y.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-Cm7TGErF.js → flowDiagram-23GEKE2U-BPeyMqr-.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-D_76Effz.js → ganttDiagram-NO4QXBWP-DcEqEzzu.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CC1ysmVD.js → gitGraphDiagram-IHSO6WYX-DTZ6UCQY.js} +1 -1
- package/dist/client/assets/index-DhfIiBLW.css +1 -0
- package/dist/client/assets/index-ivnGnbVg.js +451 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-BbgpmhBz.js → infoDiagram-FWYZ7A6U-BHrzVXeX.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-C94bY_je.js → ishikawaDiagram-FXEZZL3T-CwuoQJku.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-BQ0iFRxs.js → journeyDiagram-5HDEW3XC-ZEcy0rDZ.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-BtAexXPo.js → kanban-definition-HUTT4EX6-BDBawNbr.js} +1 -1
- package/dist/client/assets/{linear-C2HnIdOK.js → linear-smYQWzDV.js} +1 -1
- package/dist/client/assets/{mermaid.core-BbG4sJBF.js → mermaid.core-eJs7Ouvw.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-B-uvViws.js → mindmap-definition-LN4V7U3C-BK1IZ9zg.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-DHYcR6tE.js → pegDiagram-2B236MQR-B3-mVNgX.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-DEE9cD6M.js → pieDiagram-ENE6RG2P-Bkeeyx5l.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DMQP5jG3.js → quadrantDiagram-ABIIQ3AL-DouIklEO.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-B-4WDTwc.js → railroadDiagram-RFXS5EU6-BB_jXQd2.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-C2xw_q-R.js → requirementDiagram-TGXJPOKE-u56IHfiR.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-wq0BPqhc.js → sankeyDiagram-HTMAVEWB-eDi2cs5E.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-BqpM0A9d.js → sequenceDiagram-DBY2YBRQ-DnAA2xYa.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-Ba2xxyp_.js → sizeCapture-X5ZJPWSS-BogPui79.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-agPx5qAN.js → stateDiagram-2N3HPSRC--EeXDy2M.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Dc-3HKTs.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-Cy6Lzj_L.js → swimlanes-5IMT3BWC-DZAw7-kY.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-CI1Wlr0V.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-LBrVfbdp.js → timeline-definition-FHXFAJF6-DkcnpB-Y.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-BiYIV7mX.js → vennDiagram-L72KCM5P-BStcDGEf.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-Q1QU9gG3.js → wardleyDiagram-EHGQE667-Cj4CQn1M.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-CzYpxk4l.js → xychartDiagram-FW5EYKEG-_uBCjai8.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/agent/autoTitle.js +118 -0
- package/dist/server/agent/installers.js +11 -10
- package/dist/server/agent/plugins.js +90 -4
- package/dist/server/agent/registry.js +1 -0
- package/dist/server/agent/session.js +18 -2
- package/dist/server/agent/titleNamerCatalog.js +203 -0
- package/dist/server/agentHook.js +2 -1
- package/dist/server/cli.js +218 -30
- package/dist/server/routes/filesystem.js +11 -8
- package/dist/server/routes/terminal.js +351 -10
- package/dist/server/utils/autoUpdate.js +175 -0
- package/dist/server/utils/npmUpdate.js +2 -1
- package/dist/server/utils/settings.js +44 -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
|
@@ -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) {
|
|
@@ -161,7 +237,16 @@ function validateManifest(raw, dir) {
|
|
|
161
237
|
}
|
|
162
238
|
}
|
|
163
239
|
if (errors.length > 0) {
|
|
164
|
-
return {
|
|
240
|
+
return {
|
|
241
|
+
error: {
|
|
242
|
+
slug: typeof slug === 'string' ? slug : path.basename(dir),
|
|
243
|
+
errors,
|
|
244
|
+
code: isV1 ? 'AGENT_PLUGIN_MANIFEST_V1_UNSUPPORTED' : undefined,
|
|
245
|
+
migration: isV1
|
|
246
|
+
? { guideCommand: V1_MIGRATION_GUIDE_COMMAND, aiPrompt: V1_MIGRATION_AI_PROMPT }
|
|
247
|
+
: undefined,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
165
250
|
}
|
|
166
251
|
return {
|
|
167
252
|
manifest: {
|
|
@@ -171,6 +256,7 @@ function validateManifest(raw, dir) {
|
|
|
171
256
|
aliases: aliases.map((a) => a.trim().toLowerCase()),
|
|
172
257
|
accentColor: accentColor,
|
|
173
258
|
iconMode: iconMode,
|
|
259
|
+
statuses,
|
|
174
260
|
hooks,
|
|
175
261
|
resume: resumeConfig,
|
|
176
262
|
},
|
|
@@ -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)
|
|
@@ -27,6 +27,7 @@ export function defaultAgentSessionState() {
|
|
|
27
27
|
agentCwd: null,
|
|
28
28
|
activity: 0,
|
|
29
29
|
reviewed: true,
|
|
30
|
+
presentation: null,
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
@@ -39,6 +40,9 @@ export function applyAgentEvent(state, ev) {
|
|
|
39
40
|
state.sessionId = ev.sessionId;
|
|
40
41
|
if (ev.cwd)
|
|
41
42
|
state.agentCwd = ev.cwd;
|
|
43
|
+
// A custom state is scoped to one hook edge; never let it leak into the
|
|
44
|
+
// next core lifecycle event that did not explicitly declare it.
|
|
45
|
+
state.presentation = null;
|
|
42
46
|
switch (ev.kind) {
|
|
43
47
|
case 'session-start':
|
|
44
48
|
state.status = 'idle';
|
|
@@ -100,6 +104,10 @@ export function applyAgentEvent(state, ev) {
|
|
|
100
104
|
state.agentCwd = null;
|
|
101
105
|
break;
|
|
102
106
|
}
|
|
107
|
+
if (ev.status) {
|
|
108
|
+
state.status = ev.status.phase;
|
|
109
|
+
state.presentation = ev.status;
|
|
110
|
+
}
|
|
103
111
|
}
|
|
104
112
|
function nonEmpty(s) {
|
|
105
113
|
return typeof s === 'string' && s.trim().length > 0 ? s : null;
|
|
@@ -132,12 +140,18 @@ export function parseAgentEvent(payload) {
|
|
|
132
140
|
const eventName = nonEmpty(w.event);
|
|
133
141
|
if (!eventName || !EVENT_KINDS.has(eventName))
|
|
134
142
|
return null;
|
|
143
|
+
const agent = agentBySlug(nonEmpty(w.agent));
|
|
144
|
+
const statusId = nonEmpty(w.status);
|
|
145
|
+
const status = statusId
|
|
146
|
+
? agent?.statuses?.find((definition) => definition.id === statusId) ?? null
|
|
147
|
+
: null;
|
|
135
148
|
return {
|
|
136
|
-
agent
|
|
149
|
+
agent,
|
|
137
150
|
kind: eventName,
|
|
138
151
|
sessionId: nonEmpty(w.session_id),
|
|
139
152
|
message: nonEmpty(w.message),
|
|
140
153
|
cwd: nonEmpty(w.cwd),
|
|
154
|
+
status,
|
|
141
155
|
};
|
|
142
156
|
}
|
|
143
157
|
/**
|
|
@@ -146,7 +160,7 @@ export function parseAgentEvent(payload) {
|
|
|
146
160
|
* `payloadJson` is the hook's raw stdin JSON from the agent; fields are lifted
|
|
147
161
|
* into the sentinel body (snake_case, with camelCase aliases for Grok).
|
|
148
162
|
*/
|
|
149
|
-
export function buildHookSequence(agent, event, stdinJson) {
|
|
163
|
+
export function buildHookSequence(agent, event, stdinJson, statusId) {
|
|
150
164
|
let payload = {};
|
|
151
165
|
try {
|
|
152
166
|
const parsed = JSON.parse(stdinJson);
|
|
@@ -159,6 +173,8 @@ export function buildHookSequence(agent, event, stdinJson) {
|
|
|
159
173
|
agent,
|
|
160
174
|
event,
|
|
161
175
|
};
|
|
176
|
+
if (statusId)
|
|
177
|
+
body.status = statusId;
|
|
162
178
|
for (const [key, alias] of [['session_id', 'sessionId'], ['message', 'message'], ['cwd', 'cwd']]) {
|
|
163
179
|
const v = nonEmpty(payload[key]) ?? nonEmpty(payload[alias]);
|
|
164
180
|
if (v)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { execFile, spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const CACHE_FRESH_MS = 24 * 60 * 60_000;
|
|
8
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
9
|
+
const CACHE_FILE = path.join(os.homedir(), '.termdock', 'title-namer-catalog.json');
|
|
10
|
+
let cached = null;
|
|
11
|
+
let refreshPromise = null;
|
|
12
|
+
function normalizeCachedCatalog(input) {
|
|
13
|
+
if (!input || typeof input !== 'object')
|
|
14
|
+
return null;
|
|
15
|
+
const doc = input;
|
|
16
|
+
if (doc.version !== 1 || typeof doc.updatedAt !== 'number' || !Array.isArray(doc.value))
|
|
17
|
+
return null;
|
|
18
|
+
const value = doc.value.flatMap((entry) => {
|
|
19
|
+
if (!entry || typeof entry !== 'object')
|
|
20
|
+
return [];
|
|
21
|
+
const candidate = entry;
|
|
22
|
+
if (candidate.slug !== 'codex' && candidate.slug !== 'claude')
|
|
23
|
+
return [];
|
|
24
|
+
if (!Array.isArray(candidate.models))
|
|
25
|
+
return [];
|
|
26
|
+
const models = candidate.models.filter((model) => Boolean(model
|
|
27
|
+
&& typeof model.id === 'string'
|
|
28
|
+
&& typeof model.displayName === 'string'
|
|
29
|
+
&& typeof model.description === 'string'
|
|
30
|
+
&& typeof model.isDefault === 'boolean'));
|
|
31
|
+
return [{
|
|
32
|
+
slug: candidate.slug,
|
|
33
|
+
displayName: typeof candidate.displayName === 'string' ? candidate.displayName : candidate.slug,
|
|
34
|
+
available: candidate.available === true,
|
|
35
|
+
models,
|
|
36
|
+
recommendedModel: typeof candidate.recommendedModel === 'string'
|
|
37
|
+
? candidate.recommendedModel
|
|
38
|
+
: recommendTitleModel(models),
|
|
39
|
+
}];
|
|
40
|
+
});
|
|
41
|
+
return value.length > 0 ? { version: 1, updatedAt: doc.updatedAt, value } : null;
|
|
42
|
+
}
|
|
43
|
+
async function loadPersistentCache() {
|
|
44
|
+
try {
|
|
45
|
+
return normalizeCachedCatalog(JSON.parse(await fs.promises.readFile(CACHE_FILE, 'utf8')));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function savePersistentCache(doc) {
|
|
52
|
+
const directory = path.dirname(CACHE_FILE);
|
|
53
|
+
const temporary = `${CACHE_FILE}.${process.pid}.tmp`;
|
|
54
|
+
await fs.promises.mkdir(directory, { recursive: true });
|
|
55
|
+
await fs.promises.writeFile(temporary, JSON.stringify(doc, null, 2), 'utf8');
|
|
56
|
+
await fs.promises.rename(temporary, CACHE_FILE);
|
|
57
|
+
}
|
|
58
|
+
export function recommendTitleModel(models) {
|
|
59
|
+
return models.find((model) => /affordable|cost[- ]efficient|cost[- ]sensitive/i.test(model.description))?.id
|
|
60
|
+
?? models.find((model) => model.isDefault)?.id
|
|
61
|
+
?? null;
|
|
62
|
+
}
|
|
63
|
+
async function listCodexModels() {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const child = spawn('codex', ['app-server', '--stdio'], {
|
|
66
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
67
|
+
env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
|
|
68
|
+
});
|
|
69
|
+
let buffer = '';
|
|
70
|
+
let settled = false;
|
|
71
|
+
const finish = (models, error) => {
|
|
72
|
+
if (settled)
|
|
73
|
+
return;
|
|
74
|
+
settled = true;
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
child.kill();
|
|
77
|
+
if (error)
|
|
78
|
+
reject(error);
|
|
79
|
+
else
|
|
80
|
+
resolve(models ?? []);
|
|
81
|
+
};
|
|
82
|
+
const timer = setTimeout(() => finish(undefined, new Error('Codex model discovery timed out')), PROBE_TIMEOUT_MS);
|
|
83
|
+
timer.unref?.();
|
|
84
|
+
child.on('error', (error) => finish(undefined, error));
|
|
85
|
+
child.stdout.on('data', (chunk) => {
|
|
86
|
+
buffer += chunk.toString();
|
|
87
|
+
for (;;) {
|
|
88
|
+
const newline = buffer.indexOf('\n');
|
|
89
|
+
if (newline < 0)
|
|
90
|
+
break;
|
|
91
|
+
const line = buffer.slice(0, newline).trim();
|
|
92
|
+
buffer = buffer.slice(newline + 1);
|
|
93
|
+
if (!line)
|
|
94
|
+
continue;
|
|
95
|
+
let message;
|
|
96
|
+
try {
|
|
97
|
+
message = JSON.parse(line);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (message.id === 1) {
|
|
103
|
+
child.stdin.write(`${JSON.stringify({ method: 'initialized', params: {} })}\n`);
|
|
104
|
+
child.stdin.write(`${JSON.stringify({ id: 2, method: 'model/list', params: { limit: 100 } })}\n`);
|
|
105
|
+
}
|
|
106
|
+
else if (message.id === 2) {
|
|
107
|
+
const data = message.result?.data;
|
|
108
|
+
const models = Array.isArray(data) ? data.flatMap((entry) => {
|
|
109
|
+
if (!entry || typeof entry !== 'object')
|
|
110
|
+
return [];
|
|
111
|
+
const model = entry;
|
|
112
|
+
if (typeof model.model !== 'string' || typeof model.displayName !== 'string')
|
|
113
|
+
return [];
|
|
114
|
+
return [{
|
|
115
|
+
id: model.model,
|
|
116
|
+
displayName: model.displayName,
|
|
117
|
+
description: typeof model.description === 'string' ? model.description : '',
|
|
118
|
+
isDefault: model.isDefault === true,
|
|
119
|
+
}];
|
|
120
|
+
}) : [];
|
|
121
|
+
finish(models);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
child.stdin.write(`${JSON.stringify({
|
|
126
|
+
id: 1,
|
|
127
|
+
method: 'initialize',
|
|
128
|
+
params: { clientInfo: { name: 'termdock', version: '1' } },
|
|
129
|
+
})}\n`);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
export function parseClaudeSupportedModels(output) {
|
|
133
|
+
const match = output.match(/supported (?:API )?model names are ([^\n]+?)(?:,?\s+but you passed|$)/i);
|
|
134
|
+
if (!match)
|
|
135
|
+
return [];
|
|
136
|
+
return match[1]
|
|
137
|
+
.split(/,|\band\b/i)
|
|
138
|
+
.map((model) => model.trim().replace(/^["'`]+|["'`]+$/g, ''))
|
|
139
|
+
.filter((model) => /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(model))
|
|
140
|
+
.map((model) => ({ id: model, displayName: model, description: '', isDefault: false }));
|
|
141
|
+
}
|
|
142
|
+
async function listClaudeModels() {
|
|
143
|
+
try {
|
|
144
|
+
await execFileAsync('claude', ['--model', '__termdock_model_probe__', '--no-session-persistence', '-p', 'x'], {
|
|
145
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
146
|
+
maxBuffer: 128 * 1024,
|
|
147
|
+
env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
|
|
148
|
+
});
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const candidate = error;
|
|
153
|
+
if (candidate.code === 'ENOENT')
|
|
154
|
+
throw error;
|
|
155
|
+
return parseClaudeSupportedModels(`${candidate.stdout ?? ''}\n${candidate.stderr ?? ''}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function refreshTitleNamerCatalog() {
|
|
159
|
+
if (refreshPromise)
|
|
160
|
+
return refreshPromise;
|
|
161
|
+
refreshPromise = (async () => {
|
|
162
|
+
const [codex, claude] = await Promise.allSettled([listCodexModels(), listClaudeModels()]);
|
|
163
|
+
const previousCodex = cached?.value.find((namer) => namer.slug === 'codex');
|
|
164
|
+
const previousClaude = cached?.value.find((namer) => namer.slug === 'claude');
|
|
165
|
+
const codexModels = codex.status === 'fulfilled' ? codex.value : previousCodex?.models ?? [];
|
|
166
|
+
const claudeModels = claude.status === 'fulfilled' ? claude.value : previousClaude?.models ?? [];
|
|
167
|
+
const value = [
|
|
168
|
+
{
|
|
169
|
+
slug: 'codex',
|
|
170
|
+
displayName: 'Codex',
|
|
171
|
+
available: codex.status === 'fulfilled' || previousCodex?.available === true,
|
|
172
|
+
models: codexModels,
|
|
173
|
+
recommendedModel: recommendTitleModel(codexModels),
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
slug: 'claude',
|
|
177
|
+
displayName: 'Claude Code',
|
|
178
|
+
available: claude.status === 'fulfilled' || previousClaude?.available === true,
|
|
179
|
+
models: claudeModels,
|
|
180
|
+
recommendedModel: recommendTitleModel(claudeModels),
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
cached = { version: 1, updatedAt: Date.now(), value };
|
|
184
|
+
await savePersistentCache(cached).catch(() => undefined);
|
|
185
|
+
return value;
|
|
186
|
+
})().finally(() => {
|
|
187
|
+
refreshPromise = null;
|
|
188
|
+
});
|
|
189
|
+
return refreshPromise;
|
|
190
|
+
}
|
|
191
|
+
export async function getTitleNamerCatalog(force = false) {
|
|
192
|
+
if (force)
|
|
193
|
+
return refreshTitleNamerCatalog();
|
|
194
|
+
if (!cached)
|
|
195
|
+
cached = await loadPersistentCache();
|
|
196
|
+
if (!cached)
|
|
197
|
+
return refreshTitleNamerCatalog();
|
|
198
|
+
if (Date.now() - cached.updatedAt > CACHE_FRESH_MS) {
|
|
199
|
+
// Stale-while-revalidate: callers get the last known catalog immediately.
|
|
200
|
+
void refreshTitleNamerCatalog();
|
|
201
|
+
}
|
|
202
|
+
return cached.value;
|
|
203
|
+
}
|
package/dist/server/agentHook.js
CHANGED
|
@@ -145,6 +145,7 @@ function main() {
|
|
|
145
145
|
args.shift();
|
|
146
146
|
let agent = args[0] ?? '';
|
|
147
147
|
const eventArg = args[1] ?? '';
|
|
148
|
+
const statusArg = args[2] ?? '';
|
|
148
149
|
if (!agent || !eventArg)
|
|
149
150
|
return;
|
|
150
151
|
if (process.env[GROK_HOOK_ENV])
|
|
@@ -164,7 +165,7 @@ function main() {
|
|
|
164
165
|
const event = effectiveEvent(agent, eventArg, input);
|
|
165
166
|
if (!event)
|
|
166
167
|
return;
|
|
167
|
-
writeToControllingTty(buildHookSequence(agent, event, input), inTmuxPane);
|
|
168
|
+
writeToControllingTty(buildHookSequence(agent, event, input, statusArg || undefined), inTmuxPane);
|
|
168
169
|
}
|
|
169
170
|
catch {
|
|
170
171
|
// Never break the agent's flow.
|