termdock 1.4.71 → 1.4.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +59 -0
  2. package/dist/client/assets/{ModelPreview-CG6vLwyw.js → ModelPreview-BYS46cJp.js} +1 -1
  3. package/dist/client/assets/{abnfDiagram-VRR7QNED-CrXEZLWX.js → abnfDiagram-VRR7QNED-Df5iizBM.js} +1 -1
  4. package/dist/client/assets/{arc-DbKGR7Ex.js → arc-BRLIrHaj.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-PF5BoUbI.js → architectureDiagram-ZJ3FMSHR-BmIKhrrq.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-677ZJIJ3-D1gRzVWB.js → blockDiagram-677ZJIJ3-PBtq2qLG.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-LMCZKHZV-CwvOLync.js → c4Diagram-LMCZKHZV-6pevynP4.js} +1 -1
  8. package/dist/client/assets/channel-CfecnPVa.js +1 -0
  9. package/dist/client/assets/{chunk-2Q5K7J3B-BeRdP-xP.js → chunk-2Q5K7J3B-78EZViva.js} +1 -1
  10. package/dist/client/assets/{chunk-32BRIVSS-CrqR5rvl.js → chunk-32BRIVSS-CekczK5l.js} +1 -1
  11. package/dist/client/assets/{chunk-5VM5RSS4-CjbUr-ko.js → chunk-5VM5RSS4-fMhlGbVA.js} +1 -1
  12. package/dist/client/assets/{chunk-EX3LRPZG-BVEk3Wma.js → chunk-EX3LRPZG-DOBewV4Z.js} +1 -1
  13. package/dist/client/assets/{chunk-JWPE2WC7-WitJe8kR.js → chunk-JWPE2WC7-rtk-mGC7.js} +1 -1
  14. package/dist/client/assets/{chunk-MOJQB5TN-BCewlLYk.js → chunk-MOJQB5TN-CWUZocq_.js} +1 -1
  15. package/dist/client/assets/{chunk-RYQCIY6F-CoP0AA4G.js → chunk-RYQCIY6F-DcHAjeyV.js} +1 -1
  16. package/dist/client/assets/{chunk-V7JOEXUC-CSZrnmp_.js → chunk-V7JOEXUC-C2paDSKz.js} +1 -1
  17. package/dist/client/assets/{chunk-VR4S4FIN-BB-7zz-9.js → chunk-VR4S4FIN-Cq0ZrFUq.js} +1 -1
  18. package/dist/client/assets/{chunk-XXDRQBXY-CcZZlCik.js → chunk-XXDRQBXY-BboySYmP.js} +1 -1
  19. package/dist/client/assets/classDiagram-OUVF2IWQ-CFpMarVm.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-EOCWNBFH-CFpMarVm.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-JH36ORCC-DcmzUDwh.js → cose-bilkent-JH36ORCC-CMnHIr6y.js} +1 -1
  22. package/dist/client/assets/{cynefin-VYW2F7L2-C1XlDVoJ.js → cynefin-VYW2F7L2-BpOtLuYh.js} +1 -1
  23. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-zY6Aet2S.js → cynefinDiagram-TSTJHNR4-CmrMqA7z.js} +1 -1
  24. package/dist/client/assets/{dagre-VKFMJZFB-DqGTi6zR.js → dagre-VKFMJZFB-iXOV-rm7.js} +1 -1
  25. package/dist/client/assets/{diagram-FQU43EPY-DStcsU7S.js → diagram-FQU43EPY-D5KagnmX.js} +1 -1
  26. package/dist/client/assets/{diagram-G47NLZAW-Cs6IMNip.js → diagram-G47NLZAW-CglgkfyY.js} +1 -1
  27. package/dist/client/assets/{diagram-NH7WQ7WH-yA2bpWN7.js → diagram-NH7WQ7WH-Dos-y1pl.js} +1 -1
  28. package/dist/client/assets/{diagram-OA4YK3LP-Dzf9M0PI.js → diagram-OA4YK3LP-Cw6yJqFX.js} +1 -1
  29. package/dist/client/assets/{diagram-WEI45ONY-D75lpQKA.js → diagram-WEI45ONY-C_MHHYRA.js} +1 -1
  30. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-CkQvIpWy.js → ebnfDiagram-CCIWWBDH-BB7x8X2l.js} +1 -1
  31. package/dist/client/assets/{erDiagram-Q63AITRT-Ca00Vjxz.js → erDiagram-Q63AITRT-BznrrU3P.js} +1 -1
  32. package/dist/client/assets/{flowDiagram-23GEKE2U-CGrnSTEp.js → flowDiagram-23GEKE2U-CMzPvwGa.js} +1 -1
  33. package/dist/client/assets/{ganttDiagram-NO4QXBWP-0SHfOIP4.js → ganttDiagram-NO4QXBWP-Dc9JCHv3.js} +1 -1
  34. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-BeigYWvs.js → gitGraphDiagram-IHSO6WYX-WlPpmZQm.js} +1 -1
  35. package/dist/client/assets/index-wX3Y6Ks7.js +451 -0
  36. package/dist/client/assets/{infoDiagram-FWYZ7A6U-CiY1-xjN.js → infoDiagram-FWYZ7A6U-DC6PBJiN.js} +1 -1
  37. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-CmtDjUm_.js → ishikawaDiagram-FXEZZL3T-DPfxPu0W.js} +1 -1
  38. package/dist/client/assets/{journeyDiagram-5HDEW3XC-B7qOr9LY.js → journeyDiagram-5HDEW3XC-aA2ovr_f.js} +1 -1
  39. package/dist/client/assets/{kanban-definition-HUTT4EX6-DO2P2Um3.js → kanban-definition-HUTT4EX6-_T30f7Or.js} +1 -1
  40. package/dist/client/assets/{linear-B832hJt0.js → linear-DRZcO7_m.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-CsAzdiCU.js → mermaid.core-BGXjYfdb.js} +4 -4
  42. package/dist/client/assets/{mindmap-definition-LN4V7U3C-HI__vXhQ.js → mindmap-definition-LN4V7U3C-Bv0lhpqs.js} +1 -1
  43. package/dist/client/assets/{pegDiagram-2B236MQR--rZIwjXq.js → pegDiagram-2B236MQR-D-EIIOZf.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-ENE6RG2P-D9jPf5x9.js → pieDiagram-ENE6RG2P-CATAcPgl.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DTCP5ji2.js → quadrantDiagram-ABIIQ3AL-2tglHVSk.js} +1 -1
  46. package/dist/client/assets/{railroadDiagram-RFXS5EU6-D2Gspvrl.js → railroadDiagram-RFXS5EU6-BMKC-e8R.js} +1 -1
  47. package/dist/client/assets/{requirementDiagram-TGXJPOKE-DEJC02IW.js → requirementDiagram-TGXJPOKE-B5J5_wFW.js} +1 -1
  48. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-opOhKPap.js → sankeyDiagram-HTMAVEWB-Djlm71ae.js} +1 -1
  49. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-_sz2CRAG.js → sequenceDiagram-DBY2YBRQ-BtR8OPSJ.js} +1 -1
  50. package/dist/client/assets/{sizeCapture-X5ZJPWSS-D0Q2QkYF.js → sizeCapture-X5ZJPWSS-CDelD9h_.js} +1 -1
  51. package/dist/client/assets/{stateDiagram-2N3HPSRC-VgeSQTtW.js → stateDiagram-2N3HPSRC-C50kGru1.js} +1 -1
  52. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-gIr1L4WZ.js +1 -0
  53. package/dist/client/assets/{swimlanes-5IMT3BWC-ClvBvSl0.js → swimlanes-5IMT3BWC-COHxl86-.js} +2 -2
  54. package/dist/client/assets/swimlanesDiagram-G3AALYLV-COE4p0kX.js +8 -0
  55. package/dist/client/assets/{timeline-definition-FHXFAJF6-BXNsrECV.js → timeline-definition-FHXFAJF6-br-VI8Fd.js} +1 -1
  56. package/dist/client/assets/{vennDiagram-L72KCM5P-BgwiqZd-.js → vennDiagram-L72KCM5P-zwMtEv81.js} +1 -1
  57. package/dist/client/assets/{wardleyDiagram-EHGQE667-DiqN5ERx.js → wardleyDiagram-EHGQE667-_C8v4nnr.js} +1 -1
  58. package/dist/client/assets/{xychartDiagram-FW5EYKEG-DQjFVqC6.js → xychartDiagram-FW5EYKEG-CSU2R6pt.js} +1 -1
  59. package/dist/client/index.html +1 -1
  60. package/dist/client/sw.js +1 -1
  61. package/dist/server/agent/autoTitle.js +3 -0
  62. package/dist/server/agent/pluginPackages.js +289 -0
  63. package/dist/server/agent/plugins.js +139 -20
  64. package/dist/server/agent/titleNamerCatalog.js +235 -40
  65. package/dist/server/cli.js +277 -17
  66. package/dist/server/entry.js +11 -2
  67. package/dist/server/routes/terminal.js +241 -30
  68. package/dist/server/utils/csrfProtection.js +4 -1
  69. package/dist/server/utils/settings.js +14 -0
  70. package/package.json +1 -1
  71. package/runtime-manifest.json +2 -2
  72. package/dist/client/assets/channel-JTEuKVy6.js +0 -1
  73. package/dist/client/assets/classDiagram-OUVF2IWQ-BiFJB_5s.js +0 -1
  74. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BiFJB_5s.js +0 -1
  75. package/dist/client/assets/index-D5PKdbhW.js +0 -451
  76. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BVh2FRO-.js +0 -1
  77. package/dist/client/assets/swimlanesDiagram-G3AALYLV-C17V-kgm.js +0 -8
@@ -4,10 +4,24 @@ import os from 'os';
4
4
  import path from 'path';
5
5
  import { promisify } from 'util';
6
6
  import { loadPlugins } from './plugins.js';
7
+ import { getAutoRenameAgentsSetting, getAutoRenameNamerSetting } from '../utils/settings.js';
7
8
  const execFileAsync = promisify(execFile);
8
9
  const CACHE_FRESH_MS = 24 * 60 * 60_000;
9
10
  const PROBE_TIMEOUT_MS = 10_000;
10
11
  const CACHE_FILE = path.join(os.homedir(), '.termdock', 'title-namer-catalog.json');
12
+ function pluginCommandEnv() {
13
+ const allowed = ['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'TERM', 'TMPDIR', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME'];
14
+ const env = { NO_COLOR: '1', TERM: 'dumb' };
15
+ for (const key of allowed) {
16
+ if (process.env[key] !== undefined)
17
+ env[key] = process.env[key];
18
+ }
19
+ for (const [key, value] of Object.entries(process.env)) {
20
+ if (key.startsWith('LC_') && value !== undefined)
21
+ env[key] = value;
22
+ }
23
+ return env;
24
+ }
11
25
  let cached = null;
12
26
  let refreshPromise = null;
13
27
  export function invalidateTitleNamerCatalog() {
@@ -27,19 +41,16 @@ function normalizeCachedCatalog(input) {
27
41
  return [];
28
42
  if (!Array.isArray(candidate.models))
29
43
  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'));
44
+ const catalog = normalizeDiscoveredModelCatalog({
45
+ models: candidate.models,
46
+ recommendedModel: candidate.recommendedModel,
47
+ });
35
48
  return [{
36
49
  slug: candidate.slug,
37
50
  displayName: typeof candidate.displayName === 'string' ? candidate.displayName : candidate.slug,
38
51
  available: candidate.available === true,
39
- models,
40
- recommendedModel: typeof candidate.recommendedModel === 'string'
41
- ? candidate.recommendedModel
42
- : recommendTitleModel(models),
52
+ models: catalog.models,
53
+ recommendedModel: catalog.recommendedModel,
43
54
  }];
44
55
  });
45
56
  return value.length > 0 ? { version: 1, updatedAt: doc.updatedAt, value } : null;
@@ -60,7 +71,7 @@ async function savePersistentCache(doc) {
60
71
  await fs.promises.rename(temporary, CACHE_FILE);
61
72
  }
62
73
  export function recommendTitleModel(models) {
63
- return models.find((model) => /affordable|cost[- ]efficient|cost[- ]sensitive/i.test(model.description))?.id
74
+ return models.find((model) => model.isEconomical)?.id
64
75
  ?? models.find((model) => model.isDefault)?.id
65
76
  ?? null;
66
77
  }
@@ -120,6 +131,7 @@ async function listCodexModels() {
120
131
  displayName: model.displayName,
121
132
  description: typeof model.description === 'string' ? model.description : '',
122
133
  isDefault: model.isDefault === true,
134
+ isEconomical: false,
123
135
  }];
124
136
  }) : [];
125
137
  finish(models);
@@ -141,7 +153,7 @@ export function parseClaudeSupportedModels(output) {
141
153
  .split(/,|\band\b/i)
142
154
  .map((model) => model.trim().replace(/^["'`]+|["'`]+$/g, ''))
143
155
  .filter((model) => /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(model))
144
- .map((model) => ({ id: model, displayName: model, description: '', isDefault: false }));
156
+ .map((model) => ({ id: model, displayName: model, description: '', isDefault: false, isEconomical: false }));
145
157
  }
146
158
  async function listClaudeModels() {
147
159
  try {
@@ -159,35 +171,85 @@ async function listClaudeModels() {
159
171
  return parseClaudeSupportedModels(`${candidate.stdout ?? ''}\n${candidate.stderr ?? ''}`);
160
172
  }
161
173
  }
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 }];
174
+ export function normalizeDiscoveredModelCatalog(input) {
175
+ const root = input && typeof input === 'object' && !Array.isArray(input)
176
+ ? input
177
+ : null;
178
+ const entries = Array.isArray(input)
179
+ ? input
180
+ : Array.isArray(root?.models)
181
+ ? root.models
182
+ : Array.isArray(root?.data)
183
+ ? root.data
184
+ : [];
185
+ let ignoredEntries = 0;
186
+ const seenIds = new Set();
187
+ const models = entries.flatMap((entry) => {
188
+ const rawId = typeof entry === 'string'
189
+ ? entry
190
+ : entry && typeof entry === 'object'
191
+ ? (() => {
192
+ const model = entry;
193
+ return typeof model.id === 'string'
194
+ ? model.id
195
+ : typeof model.name === 'string'
196
+ ? model.name
197
+ : typeof model.model === 'string'
198
+ ? model.model
199
+ : '';
200
+ })()
201
+ : '';
202
+ const id = rawId.trim();
203
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(id) || seenIds.has(id)) {
204
+ ignoredEntries += 1;
205
+ return [];
168
206
  }
169
- if (!entry || typeof entry !== 'object')
207
+ seenIds.add(id);
208
+ if (typeof entry === 'string') {
209
+ return [{ id, displayName: id, description: '', isDefault: false, isEconomical: false }];
210
+ }
211
+ if (!entry || typeof entry !== 'object') {
212
+ ignoredEntries += 1;
170
213
  return [];
214
+ }
171
215
  const model = entry;
172
- if (typeof model.id !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(model.id))
173
- return [];
216
+ const rawDisplayName = typeof model.displayName === 'string'
217
+ ? model.displayName
218
+ : typeof model.display_name === 'string'
219
+ ? model.display_name
220
+ : typeof model.label === 'string'
221
+ ? model.label
222
+ : id;
223
+ const displayName = rawDisplayName.trim().slice(0, 160) || id;
174
224
  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,
225
+ id,
226
+ displayName,
227
+ description: typeof model.description === 'string' ? model.description.trim().slice(0, 1000) : '',
228
+ isDefault: model.isDefault === true || model.is_default === true || model.default === true,
229
+ isEconomical: model.isEconomical === true || model.is_economical === true || model.economical === true,
179
230
  }];
180
231
  });
232
+ const declared = typeof (root?.recommendedModel ?? root?.recommended_model) === 'string'
233
+ ? String(root?.recommendedModel ?? root?.recommended_model).trim()
234
+ : '';
235
+ const recommendedModel = declared && models.some((model) => model.id === declared)
236
+ ? declared
237
+ : recommendTitleModel(models);
238
+ return { models, recommendedModel, ignoredEntries };
181
239
  }
182
- async function listPluginModels(config) {
240
+ function expandPluginPath(value, pluginDir) {
241
+ return value.replaceAll('{pluginDir}', pluginDir);
242
+ }
243
+ async function listPluginModels(config, pluginDir) {
183
244
  if (!config.models)
184
- return [];
185
- const { stdout } = await execFileAsync(config.models.command, config.models.args ?? [], {
245
+ return { models: [], recommendedModel: null, ignoredEntries: 0 };
246
+ const { stdout } = await execFileAsync(expandPluginPath(config.models.command, pluginDir), (config.models.args ?? []).map((arg) => expandPluginPath(arg, pluginDir)), {
186
247
  timeout: PROBE_TIMEOUT_MS,
187
248
  maxBuffer: 256 * 1024,
188
- env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
249
+ cwd: pluginDir,
250
+ env: pluginCommandEnv(),
189
251
  });
190
- return normalizeDiscoveredModels(JSON.parse(stdout));
252
+ return normalizeDiscoveredModelCatalog(JSON.parse(stdout));
191
253
  }
192
254
  function pluginTitleNamers() {
193
255
  return loadPlugins().plugins.flatMap((plugin) => plugin.manifest.titleNamer
@@ -195,12 +257,144 @@ function pluginTitleNamers() {
195
257
  slug: plugin.manifest.slug,
196
258
  displayName: plugin.manifest.displayName,
197
259
  config: plugin.manifest.titleNamer,
260
+ dir: plugin.dir,
198
261
  }]
199
262
  : []);
200
263
  }
264
+ export function shouldRunPluginTitleCommands(slug, selected, enabledAgents) {
265
+ return selected === slug
266
+ || (selected === 'auto' && enabledAgents.includes(slug));
267
+ }
268
+ function isPluginTitleExecutionEnabled(slug) {
269
+ return shouldRunPluginTitleCommands(slug, getAutoRenameNamerSetting(), getAutoRenameAgentsSetting());
270
+ }
271
+ async function cacheExplicitPluginProbe(slug, displayName, catalog) {
272
+ if (!cached)
273
+ cached = await loadPersistentCache();
274
+ const current = cached?.value ?? await getTitleNamerCatalog();
275
+ const value = current.map((entry) => entry.slug === slug
276
+ ? {
277
+ slug,
278
+ displayName,
279
+ available: true,
280
+ models: catalog.models,
281
+ recommendedModel: catalog.recommendedModel,
282
+ }
283
+ : entry);
284
+ cached = { version: 1, updatedAt: Date.now(), value };
285
+ await savePersistentCache(cached).catch(() => undefined);
286
+ }
287
+ function expandTitleArg(value, pluginDir, prompt, model) {
288
+ return expandPluginPath(value, pluginDir).replaceAll('{prompt}', prompt).replaceAll('{model}', model);
289
+ }
290
+ export function buildPluginTitleArgs(config, pluginDir, prompt, model) {
291
+ const baseArgs = [];
292
+ for (const arg of config.args) {
293
+ if (arg.includes('{model}') && !model) {
294
+ // Legacy v2 manifests put paired flags in args. Remove an immediately
295
+ // preceding standalone flag too, so `-c model={model}` cannot degrade
296
+ // into the invalid `-c -p ...`. New plugins should use modelArgs.
297
+ if (baseArgs.at(-1)?.startsWith('-') && !baseArgs.at(-1)?.includes('='))
298
+ baseArgs.pop();
299
+ continue;
300
+ }
301
+ baseArgs.push(expandTitleArg(arg, pluginDir, prompt, model ?? ''));
302
+ }
303
+ const selectedModelArgs = model && config.modelArgs
304
+ ? config.modelArgs.map((arg) => expandTitleArg(arg, pluginDir, prompt, model))
305
+ : [];
306
+ return [...selectedModelArgs, ...baseArgs];
307
+ }
308
+ export async function probePluginTitleNamer(slug) {
309
+ const plugin = loadPlugins().plugins.find((entry) => entry.manifest.slug === slug);
310
+ if (!plugin)
311
+ return null;
312
+ const config = plugin.manifest.titleNamer;
313
+ if (!config) {
314
+ return {
315
+ slug,
316
+ displayName: plugin.manifest.displayName,
317
+ hasTitleNamer: false,
318
+ hasModelCommand: false,
319
+ status: 'missing-title-namer',
320
+ models: [],
321
+ recommendedModel: null,
322
+ selectionBehavior: 'This plugin cannot generate titles until manifest.titleNamer is added.',
323
+ warnings: ['manifest.json has no titleNamer declaration.'],
324
+ error: null,
325
+ nextSteps: ['Run `td agent-plugin --json`, add manifest.titleNamer, then update/reinstall the plugin.'],
326
+ };
327
+ }
328
+ const warnings = [];
329
+ if (!config.modelArgs && config.args.some((arg) => arg.includes('{model}'))) {
330
+ warnings.push('Legacy {model} placement detected in args; use modelArgs for optional paired flags.');
331
+ }
332
+ if (!config.models) {
333
+ return {
334
+ slug,
335
+ displayName: plugin.manifest.displayName,
336
+ hasTitleNamer: true,
337
+ hasModelCommand: false,
338
+ status: 'cli-default',
339
+ models: [],
340
+ recommendedModel: null,
341
+ selectionBehavior: 'Termdock will omit all model arguments and use the Agent CLI default model.',
342
+ warnings,
343
+ error: null,
344
+ nextSteps: ['Optional: add titleNamer.models to expose selectable models; otherwise the CLI default is used.'],
345
+ };
346
+ }
347
+ try {
348
+ const catalog = await listPluginModels(config, plugin.dir);
349
+ await cacheExplicitPluginProbe(slug, plugin.manifest.displayName, catalog);
350
+ if (catalog.ignoredEntries > 0) {
351
+ warnings.push(`${catalog.ignoredEntries} model entr${catalog.ignoredEntries === 1 ? 'y was' : 'ies were'} ignored because its identifier was invalid or duplicated.`);
352
+ }
353
+ if (catalog.models.length > 0 && !catalog.recommendedModel) {
354
+ warnings.push('No recommendedModel, isEconomical, or isDefault marker was found; automatic mode will use the Agent CLI default model.');
355
+ }
356
+ return {
357
+ slug,
358
+ displayName: plugin.manifest.displayName,
359
+ hasTitleNamer: true,
360
+ hasModelCommand: true,
361
+ status: catalog.models.length > 0 ? 'ok' : 'no-models',
362
+ models: catalog.models,
363
+ recommendedModel: catalog.recommendedModel,
364
+ selectionBehavior: catalog.recommendedModel
365
+ ? `Automatic selection will pass model ${catalog.recommendedModel}.`
366
+ : 'No recommended model was declared; Termdock will omit model arguments and use the Agent CLI default model.',
367
+ warnings: catalog.models.length === 0
368
+ ? [...warnings, 'The model command succeeded but returned no usable models.']
369
+ : warnings,
370
+ error: null,
371
+ nextSteps: catalog.models.length > 0
372
+ ? catalog.recommendedModel
373
+ ? ['Select this provider/model in Settings, then enable automatic titles for the Agent.']
374
+ : ['Declare recommendedModel, mark one model isEconomical/isDefault, or let users select a model manually.']
375
+ : ['Return a JSON array (or {models}) using id/name/model plus optional displayName, description, isDefault, and isEconomical fields.'],
376
+ };
377
+ }
378
+ catch (error) {
379
+ return {
380
+ slug,
381
+ displayName: plugin.manifest.displayName,
382
+ hasTitleNamer: true,
383
+ hasModelCommand: true,
384
+ status: 'probe-failed',
385
+ models: [],
386
+ recommendedModel: null,
387
+ selectionBehavior: 'Model selection is unavailable until the probe succeeds; title generation falls back to the Agent CLI default model.',
388
+ warnings,
389
+ error: error.message,
390
+ nextSteps: ['Run the declared models command directly, ensure it prints JSON only, or use a {pluginDir} adapter script to normalize its output.'],
391
+ };
392
+ }
393
+ }
201
394
  export async function runTitleNamer(slug, prompt, model) {
202
395
  let command;
203
396
  let args;
397
+ let cwd;
204
398
  if (slug === 'codex') {
205
399
  command = 'codex';
206
400
  args = ['exec', '--ephemeral', '--skip-git-repo-check', '--color', 'never', ...(model ? ['--model', model] : []), prompt];
@@ -211,20 +405,18 @@ export async function runTitleNamer(slug, prompt, model) {
211
405
  }
212
406
  else {
213
407
  const provider = pluginTitleNamers().find((entry) => entry.slug === slug);
214
- if (!provider)
408
+ if (!provider || !isPluginTitleExecutionEnabled(slug))
215
409
  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
- });
410
+ cwd = provider.dir;
411
+ command = expandPluginPath(provider.config.command, provider.dir);
412
+ args = buildPluginTitleArgs(provider.config, provider.dir, prompt, model);
222
413
  }
223
414
  try {
224
415
  const { stdout } = await execFileAsync(command, args, {
225
416
  timeout: 45_000,
226
417
  maxBuffer: 256 * 1024,
227
- env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
418
+ cwd,
419
+ env: cwd ? pluginCommandEnv() : { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
228
420
  });
229
421
  return stdout;
230
422
  }
@@ -240,7 +432,9 @@ async function refreshTitleNamerCatalog() {
240
432
  const [codex, claude, ...pluginResults] = await Promise.allSettled([
241
433
  listCodexModels(),
242
434
  listClaudeModels(),
243
- ...plugins.map((provider) => listPluginModels(provider.config)),
435
+ ...plugins.map((provider) => provider.config.models && isPluginTitleExecutionEnabled(provider.slug)
436
+ ? listPluginModels(provider.config, provider.dir)
437
+ : Promise.resolve({ models: [], recommendedModel: null, ignoredEntries: 0 })),
244
438
  ]);
245
439
  const previousCodex = cached?.value.find((namer) => namer.slug === 'codex');
246
440
  const previousClaude = cached?.value.find((namer) => namer.slug === 'claude');
@@ -264,13 +458,14 @@ async function refreshTitleNamerCatalog() {
264
458
  ...plugins.map((provider, index) => {
265
459
  const result = pluginResults[index];
266
460
  const previous = cached?.value.find((namer) => namer.slug === provider.slug);
267
- const models = result?.status === 'fulfilled' ? result.value : previous?.models ?? [];
461
+ const discovered = result?.status === 'fulfilled' ? result.value : null;
462
+ const models = discovered?.models ?? previous?.models ?? [];
268
463
  return {
269
464
  slug: provider.slug,
270
465
  displayName: provider.displayName,
271
466
  available: provider.config.models ? result?.status === 'fulfilled' || previous?.available === true : true,
272
467
  models,
273
- recommendedModel: recommendTitleModel(models),
468
+ recommendedModel: discovered?.recommendedModel ?? recommendTitleModel(models),
274
469
  };
275
470
  }),
276
471
  ];