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
|
@@ -0,0 +1,301 @@
|
|
|
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
|
+
import { loadPlugins } from './plugins.js';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const CACHE_FRESH_MS = 24 * 60 * 60_000;
|
|
9
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
10
|
+
const CACHE_FILE = path.join(os.homedir(), '.termdock', 'title-namer-catalog.json');
|
|
11
|
+
let cached = null;
|
|
12
|
+
let refreshPromise = null;
|
|
13
|
+
export function invalidateTitleNamerCatalog() {
|
|
14
|
+
cached = null;
|
|
15
|
+
}
|
|
16
|
+
function normalizeCachedCatalog(input) {
|
|
17
|
+
if (!input || typeof input !== 'object')
|
|
18
|
+
return null;
|
|
19
|
+
const doc = input;
|
|
20
|
+
if (doc.version !== 1 || typeof doc.updatedAt !== 'number' || !Array.isArray(doc.value))
|
|
21
|
+
return null;
|
|
22
|
+
const value = doc.value.flatMap((entry) => {
|
|
23
|
+
if (!entry || typeof entry !== 'object')
|
|
24
|
+
return [];
|
|
25
|
+
const candidate = entry;
|
|
26
|
+
if (typeof candidate.slug !== 'string' || !/^[a-z][a-z0-9-]{0,39}$/.test(candidate.slug))
|
|
27
|
+
return [];
|
|
28
|
+
if (!Array.isArray(candidate.models))
|
|
29
|
+
return [];
|
|
30
|
+
const models = candidate.models.filter((model) => Boolean(model
|
|
31
|
+
&& typeof model.id === 'string'
|
|
32
|
+
&& typeof model.displayName === 'string'
|
|
33
|
+
&& typeof model.description === 'string'
|
|
34
|
+
&& typeof model.isDefault === 'boolean'));
|
|
35
|
+
return [{
|
|
36
|
+
slug: candidate.slug,
|
|
37
|
+
displayName: typeof candidate.displayName === 'string' ? candidate.displayName : candidate.slug,
|
|
38
|
+
available: candidate.available === true,
|
|
39
|
+
models,
|
|
40
|
+
recommendedModel: typeof candidate.recommendedModel === 'string'
|
|
41
|
+
? candidate.recommendedModel
|
|
42
|
+
: recommendTitleModel(models),
|
|
43
|
+
}];
|
|
44
|
+
});
|
|
45
|
+
return value.length > 0 ? { version: 1, updatedAt: doc.updatedAt, value } : null;
|
|
46
|
+
}
|
|
47
|
+
async function loadPersistentCache() {
|
|
48
|
+
try {
|
|
49
|
+
return normalizeCachedCatalog(JSON.parse(await fs.promises.readFile(CACHE_FILE, 'utf8')));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function savePersistentCache(doc) {
|
|
56
|
+
const directory = path.dirname(CACHE_FILE);
|
|
57
|
+
const temporary = `${CACHE_FILE}.${process.pid}.tmp`;
|
|
58
|
+
await fs.promises.mkdir(directory, { recursive: true });
|
|
59
|
+
await fs.promises.writeFile(temporary, JSON.stringify(doc, null, 2), 'utf8');
|
|
60
|
+
await fs.promises.rename(temporary, CACHE_FILE);
|
|
61
|
+
}
|
|
62
|
+
export function recommendTitleModel(models) {
|
|
63
|
+
return models.find((model) => /affordable|cost[- ]efficient|cost[- ]sensitive/i.test(model.description))?.id
|
|
64
|
+
?? models.find((model) => model.isDefault)?.id
|
|
65
|
+
?? null;
|
|
66
|
+
}
|
|
67
|
+
async function listCodexModels() {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const child = spawn('codex', ['app-server', '--stdio'], {
|
|
70
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
71
|
+
env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
|
|
72
|
+
});
|
|
73
|
+
let buffer = '';
|
|
74
|
+
let settled = false;
|
|
75
|
+
const finish = (models, error) => {
|
|
76
|
+
if (settled)
|
|
77
|
+
return;
|
|
78
|
+
settled = true;
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
child.kill();
|
|
81
|
+
if (error)
|
|
82
|
+
reject(error);
|
|
83
|
+
else
|
|
84
|
+
resolve(models ?? []);
|
|
85
|
+
};
|
|
86
|
+
const timer = setTimeout(() => finish(undefined, new Error('Codex model discovery timed out')), PROBE_TIMEOUT_MS);
|
|
87
|
+
timer.unref?.();
|
|
88
|
+
child.on('error', (error) => finish(undefined, error));
|
|
89
|
+
child.stdout.on('data', (chunk) => {
|
|
90
|
+
buffer += chunk.toString();
|
|
91
|
+
for (;;) {
|
|
92
|
+
const newline = buffer.indexOf('\n');
|
|
93
|
+
if (newline < 0)
|
|
94
|
+
break;
|
|
95
|
+
const line = buffer.slice(0, newline).trim();
|
|
96
|
+
buffer = buffer.slice(newline + 1);
|
|
97
|
+
if (!line)
|
|
98
|
+
continue;
|
|
99
|
+
let message;
|
|
100
|
+
try {
|
|
101
|
+
message = JSON.parse(line);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (message.id === 1) {
|
|
107
|
+
child.stdin.write(`${JSON.stringify({ method: 'initialized', params: {} })}\n`);
|
|
108
|
+
child.stdin.write(`${JSON.stringify({ id: 2, method: 'model/list', params: { limit: 100 } })}\n`);
|
|
109
|
+
}
|
|
110
|
+
else if (message.id === 2) {
|
|
111
|
+
const data = message.result?.data;
|
|
112
|
+
const models = Array.isArray(data) ? data.flatMap((entry) => {
|
|
113
|
+
if (!entry || typeof entry !== 'object')
|
|
114
|
+
return [];
|
|
115
|
+
const model = entry;
|
|
116
|
+
if (typeof model.model !== 'string' || typeof model.displayName !== 'string')
|
|
117
|
+
return [];
|
|
118
|
+
return [{
|
|
119
|
+
id: model.model,
|
|
120
|
+
displayName: model.displayName,
|
|
121
|
+
description: typeof model.description === 'string' ? model.description : '',
|
|
122
|
+
isDefault: model.isDefault === true,
|
|
123
|
+
}];
|
|
124
|
+
}) : [];
|
|
125
|
+
finish(models);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
child.stdin.write(`${JSON.stringify({
|
|
130
|
+
id: 1,
|
|
131
|
+
method: 'initialize',
|
|
132
|
+
params: { clientInfo: { name: 'termdock', version: '1' } },
|
|
133
|
+
})}\n`);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export function parseClaudeSupportedModels(output) {
|
|
137
|
+
const match = output.match(/supported (?:API )?model names are ([^\n]+?)(?:,?\s+but you passed|$)/i);
|
|
138
|
+
if (!match)
|
|
139
|
+
return [];
|
|
140
|
+
return match[1]
|
|
141
|
+
.split(/,|\band\b/i)
|
|
142
|
+
.map((model) => model.trim().replace(/^["'`]+|["'`]+$/g, ''))
|
|
143
|
+
.filter((model) => /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(model))
|
|
144
|
+
.map((model) => ({ id: model, displayName: model, description: '', isDefault: false }));
|
|
145
|
+
}
|
|
146
|
+
async function listClaudeModels() {
|
|
147
|
+
try {
|
|
148
|
+
await execFileAsync('claude', ['--model', '__termdock_model_probe__', '--no-session-persistence', '-p', 'x'], {
|
|
149
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
150
|
+
maxBuffer: 128 * 1024,
|
|
151
|
+
env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
|
|
152
|
+
});
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const candidate = error;
|
|
157
|
+
if (candidate.code === 'ENOENT')
|
|
158
|
+
throw error;
|
|
159
|
+
return parseClaudeSupportedModels(`${candidate.stdout ?? ''}\n${candidate.stderr ?? ''}`);
|
|
160
|
+
}
|
|
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
|
+
}
|
|
235
|
+
async function refreshTitleNamerCatalog() {
|
|
236
|
+
if (refreshPromise)
|
|
237
|
+
return refreshPromise;
|
|
238
|
+
refreshPromise = (async () => {
|
|
239
|
+
const plugins = pluginTitleNamers();
|
|
240
|
+
const [codex, claude, ...pluginResults] = await Promise.allSettled([
|
|
241
|
+
listCodexModels(),
|
|
242
|
+
listClaudeModels(),
|
|
243
|
+
...plugins.map((provider) => listPluginModels(provider.config)),
|
|
244
|
+
]);
|
|
245
|
+
const previousCodex = cached?.value.find((namer) => namer.slug === 'codex');
|
|
246
|
+
const previousClaude = cached?.value.find((namer) => namer.slug === 'claude');
|
|
247
|
+
const codexModels = codex.status === 'fulfilled' ? codex.value : previousCodex?.models ?? [];
|
|
248
|
+
const claudeModels = claude.status === 'fulfilled' ? claude.value : previousClaude?.models ?? [];
|
|
249
|
+
const value = [
|
|
250
|
+
{
|
|
251
|
+
slug: 'codex',
|
|
252
|
+
displayName: 'Codex',
|
|
253
|
+
available: codex.status === 'fulfilled' || previousCodex?.available === true,
|
|
254
|
+
models: codexModels,
|
|
255
|
+
recommendedModel: recommendTitleModel(codexModels),
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
slug: 'claude',
|
|
259
|
+
displayName: 'Claude Code',
|
|
260
|
+
available: claude.status === 'fulfilled' || previousClaude?.available === true,
|
|
261
|
+
models: claudeModels,
|
|
262
|
+
recommendedModel: recommendTitleModel(claudeModels),
|
|
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
|
+
}),
|
|
276
|
+
];
|
|
277
|
+
cached = { version: 1, updatedAt: Date.now(), value };
|
|
278
|
+
await savePersistentCache(cached).catch(() => undefined);
|
|
279
|
+
return value;
|
|
280
|
+
})().finally(() => {
|
|
281
|
+
refreshPromise = null;
|
|
282
|
+
});
|
|
283
|
+
return refreshPromise;
|
|
284
|
+
}
|
|
285
|
+
export async function getTitleNamerCatalog(force = false) {
|
|
286
|
+
if (force)
|
|
287
|
+
return refreshTitleNamerCatalog();
|
|
288
|
+
if (!cached)
|
|
289
|
+
cached = await loadPersistentCache();
|
|
290
|
+
if (!cached)
|
|
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
|
+
}
|
|
296
|
+
if (Date.now() - cached.updatedAt > CACHE_FRESH_MS) {
|
|
297
|
+
// Stale-while-revalidate: callers get the last known catalog immediately.
|
|
298
|
+
void refreshTitleNamerCatalog();
|
|
299
|
+
}
|
|
300
|
+
return cached.value;
|
|
301
|
+
}
|
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
|
|
@@ -145,6 +192,7 @@ function main() {
|
|
|
145
192
|
args.shift();
|
|
146
193
|
let agent = args[0] ?? '';
|
|
147
194
|
const eventArg = args[1] ?? '';
|
|
195
|
+
const statusArg = args[2] ?? '';
|
|
148
196
|
if (!agent || !eventArg)
|
|
149
197
|
return;
|
|
150
198
|
if (process.env[GROK_HOOK_ENV])
|
|
@@ -164,7 +212,7 @@ function main() {
|
|
|
164
212
|
const event = effectiveEvent(agent, eventArg, input);
|
|
165
213
|
if (!event)
|
|
166
214
|
return;
|
|
167
|
-
writeToControllingTty(buildHookSequence(agent, event, input), inTmuxPane);
|
|
215
|
+
writeToControllingTty(buildHookSequence(agent, event, input, statusArg || undefined), inTmuxPane);
|
|
168
216
|
}
|
|
169
217
|
catch {
|
|
170
218
|
// Never break the agent's flow.
|