termdock 1.4.72 → 1.4.74

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 (72) hide show
  1. package/README.md +23 -0
  2. package/dist/client/assets/{ModelPreview-i9dS1A3I.js → ModelPreview-DENM9rCk.js} +1 -1
  3. package/dist/client/assets/{abnfDiagram-VRR7QNED-Cs-ty-HA.js → abnfDiagram-VRR7QNED-pkPIxvGG.js} +1 -1
  4. package/dist/client/assets/{arc-CIX5k_JD.js → arc-DU3Ux6au.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-DnPnQoJy.js → architectureDiagram-ZJ3FMSHR-CUFADBXQ.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-677ZJIJ3-Dhew-L5v.js → blockDiagram-677ZJIJ3-DF2WwD4e.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-LMCZKHZV-BSsEMeWn.js → c4Diagram-LMCZKHZV-B-kP9QsT.js} +1 -1
  8. package/dist/client/assets/channel-DQ60YB86.js +1 -0
  9. package/dist/client/assets/{chunk-2Q5K7J3B-BWm--1Mo.js → chunk-2Q5K7J3B-BZ0B5k0f.js} +1 -1
  10. package/dist/client/assets/{chunk-32BRIVSS-D_WFmuvb.js → chunk-32BRIVSS-CrCyd-CP.js} +1 -1
  11. package/dist/client/assets/{chunk-5VM5RSS4-CQdzqlQg.js → chunk-5VM5RSS4-C-5fXJD3.js} +1 -1
  12. package/dist/client/assets/{chunk-EX3LRPZG-Cm3zLQpt.js → chunk-EX3LRPZG-rtwRbQhD.js} +1 -1
  13. package/dist/client/assets/{chunk-JWPE2WC7-Cmcv5sEN.js → chunk-JWPE2WC7-CldgMQi9.js} +1 -1
  14. package/dist/client/assets/{chunk-MOJQB5TN-Bam44EwJ.js → chunk-MOJQB5TN-BQ9Y_aMM.js} +1 -1
  15. package/dist/client/assets/{chunk-RYQCIY6F-DIVOuE8y.js → chunk-RYQCIY6F-WndooeBw.js} +1 -1
  16. package/dist/client/assets/{chunk-V7JOEXUC-BfOTf7yg.js → chunk-V7JOEXUC-MKuAVmqI.js} +1 -1
  17. package/dist/client/assets/{chunk-VR4S4FIN-DbiXn0dd.js → chunk-VR4S4FIN-C78Ap4a7.js} +1 -1
  18. package/dist/client/assets/{chunk-XXDRQBXY-C4pOe4OC.js → chunk-XXDRQBXY-Cz9Vn8l7.js} +1 -1
  19. package/dist/client/assets/classDiagram-OUVF2IWQ-D6Ilyaxw.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-EOCWNBFH-D6Ilyaxw.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-JH36ORCC-CCoSPmoc.js → cose-bilkent-JH36ORCC-B7C5TIfj.js} +1 -1
  22. package/dist/client/assets/{cynefin-VYW2F7L2-B2lYz7sl.js → cynefin-VYW2F7L2-VDgIOFsH.js} +1 -1
  23. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-Cd7crN9s.js → cynefinDiagram-TSTJHNR4-77H8R2uk.js} +1 -1
  24. package/dist/client/assets/{dagre-VKFMJZFB-q9Fj9HuQ.js → dagre-VKFMJZFB-ZP-rOoSX.js} +1 -1
  25. package/dist/client/assets/{diagram-FQU43EPY-iqHy5YHW.js → diagram-FQU43EPY-C8A7-Ckt.js} +1 -1
  26. package/dist/client/assets/{diagram-G47NLZAW-BE-jlh1B.js → diagram-G47NLZAW-DvJCRYv0.js} +1 -1
  27. package/dist/client/assets/{diagram-NH7WQ7WH-DmP7_WOC.js → diagram-NH7WQ7WH-D6QOKKye.js} +1 -1
  28. package/dist/client/assets/{diagram-OA4YK3LP-CHx_k8Ar.js → diagram-OA4YK3LP-CxCaAx9f.js} +1 -1
  29. package/dist/client/assets/{diagram-WEI45ONY-D0wImpAF.js → diagram-WEI45ONY-BGWgfYUC.js} +1 -1
  30. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-DyCB0rF8.js → ebnfDiagram-CCIWWBDH-C2SwDpDQ.js} +1 -1
  31. package/dist/client/assets/{erDiagram-Q63AITRT-SRcd1B2Z.js → erDiagram-Q63AITRT-DuAEwpsT.js} +1 -1
  32. package/dist/client/assets/{flowDiagram-23GEKE2U-D0i9mFwt.js → flowDiagram-23GEKE2U-ZINMttS3.js} +1 -1
  33. package/dist/client/assets/{ganttDiagram-NO4QXBWP-BfwGIYDn.js → ganttDiagram-NO4QXBWP-WHmlGSqw.js} +1 -1
  34. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-BXkJQP7O.js → gitGraphDiagram-IHSO6WYX-C72t1eyA.js} +1 -1
  35. package/dist/client/assets/index-uT015e2L.js +451 -0
  36. package/dist/client/assets/{infoDiagram-FWYZ7A6U-CM-Ebv9R.js → infoDiagram-FWYZ7A6U-BjFL5b0C.js} +1 -1
  37. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-U-qnP7xi.js → ishikawaDiagram-FXEZZL3T-DJeIOIAV.js} +1 -1
  38. package/dist/client/assets/{journeyDiagram-5HDEW3XC-D4RxvsuL.js → journeyDiagram-5HDEW3XC-BUGggrpk.js} +1 -1
  39. package/dist/client/assets/{kanban-definition-HUTT4EX6-Dm_asCYK.js → kanban-definition-HUTT4EX6-BMUuu0OR.js} +1 -1
  40. package/dist/client/assets/{linear-Zhe8d635.js → linear-PKUAqFSJ.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-D_NWIuMY.js → mermaid.core-Bqj6Hft8.js} +4 -4
  42. package/dist/client/assets/{mindmap-definition-LN4V7U3C-DXpNidd5.js → mindmap-definition-LN4V7U3C-CRK1_AUB.js} +1 -1
  43. package/dist/client/assets/{pegDiagram-2B236MQR-DdOwOEgu.js → pegDiagram-2B236MQR-CNyaTowI.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-ENE6RG2P-D_A9HoY_.js → pieDiagram-ENE6RG2P-DzYXle3z.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-D-Y3cLEF.js → quadrantDiagram-ABIIQ3AL-CNx9eOaP.js} +1 -1
  46. package/dist/client/assets/{railroadDiagram-RFXS5EU6-DD6eFV82.js → railroadDiagram-RFXS5EU6-mG_PphXN.js} +1 -1
  47. package/dist/client/assets/{requirementDiagram-TGXJPOKE-B59HJgL2.js → requirementDiagram-TGXJPOKE-DCpYVlrJ.js} +1 -1
  48. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-C2BvHMH8.js → sankeyDiagram-HTMAVEWB-BzhsJ3A-.js} +1 -1
  49. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-3004tCRm.js → sequenceDiagram-DBY2YBRQ-Bzja_FXB.js} +1 -1
  50. package/dist/client/assets/{sizeCapture-X5ZJPWSS-DCBhg8QW.js → sizeCapture-X5ZJPWSS-I-AyY-Ov.js} +1 -1
  51. package/dist/client/assets/{stateDiagram-2N3HPSRC-BVqiKVe0.js → stateDiagram-2N3HPSRC-BmoVg6_v.js} +1 -1
  52. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-nL4W2TQF.js +1 -0
  53. package/dist/client/assets/{swimlanes-5IMT3BWC-DS6-sdEW.js → swimlanes-5IMT3BWC-DS1aJNte.js} +2 -2
  54. package/dist/client/assets/swimlanesDiagram-G3AALYLV-BXudfA57.js +8 -0
  55. package/dist/client/assets/{timeline-definition-FHXFAJF6-wVOEmQwG.js → timeline-definition-FHXFAJF6-DPOIaUOD.js} +1 -1
  56. package/dist/client/assets/{vennDiagram-L72KCM5P-DMZiSJDk.js → vennDiagram-L72KCM5P-stHONsut.js} +1 -1
  57. package/dist/client/assets/{wardleyDiagram-EHGQE667-Dx5Aob4Y.js → wardleyDiagram-EHGQE667-BRBiEAij.js} +1 -1
  58. package/dist/client/assets/{xychartDiagram-FW5EYKEG-BqMhkhmW.js → xychartDiagram-FW5EYKEG-pH_JBMfz.js} +1 -1
  59. package/dist/client/index.html +1 -1
  60. package/dist/client/sw.js +1 -1
  61. package/dist/server/agent/plugins.js +16 -1
  62. package/dist/server/agent/titleNamerCatalog.js +198 -34
  63. package/dist/server/cli.js +77 -8
  64. package/dist/server/routes/terminal.js +9 -1
  65. package/package.json +1 -1
  66. package/runtime-manifest.json +2 -2
  67. package/dist/client/assets/channel-CASnt1pP.js +0 -1
  68. package/dist/client/assets/classDiagram-OUVF2IWQ-Da_SQWEq.js +0 -1
  69. package/dist/client/assets/classDiagram-v2-EOCWNBFH-Da_SQWEq.js +0 -1
  70. package/dist/client/assets/index-BA-DqJM2.js +0 -451
  71. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Z0GHWyoF.js +0 -1
  72. package/dist/client/assets/swimlanesDiagram-G3AALYLV-el4HDX0G.js +0 -8
@@ -41,19 +41,16 @@ function normalizeCachedCatalog(input) {
41
41
  return [];
42
42
  if (!Array.isArray(candidate.models))
43
43
  return [];
44
- const models = candidate.models.filter((model) => Boolean(model
45
- && typeof model.id === 'string'
46
- && typeof model.displayName === 'string'
47
- && typeof model.description === 'string'
48
- && typeof model.isDefault === 'boolean'));
44
+ const catalog = normalizeDiscoveredModelCatalog({
45
+ models: candidate.models,
46
+ recommendedModel: candidate.recommendedModel,
47
+ });
49
48
  return [{
50
49
  slug: candidate.slug,
51
50
  displayName: typeof candidate.displayName === 'string' ? candidate.displayName : candidate.slug,
52
51
  available: candidate.available === true,
53
- models,
54
- recommendedModel: typeof candidate.recommendedModel === 'string'
55
- ? candidate.recommendedModel
56
- : recommendTitleModel(models),
52
+ models: catalog.models,
53
+ recommendedModel: catalog.recommendedModel,
57
54
  }];
58
55
  });
59
56
  return value.length > 0 ? { version: 1, updatedAt: doc.updatedAt, value } : null;
@@ -74,7 +71,7 @@ async function savePersistentCache(doc) {
74
71
  await fs.promises.rename(temporary, CACHE_FILE);
75
72
  }
76
73
  export function recommendTitleModel(models) {
77
- return models.find((model) => /affordable|cost[- ]efficient|cost[- ]sensitive/i.test(model.description))?.id
74
+ return models.find((model) => model.isEconomical)?.id
78
75
  ?? models.find((model) => model.isDefault)?.id
79
76
  ?? null;
80
77
  }
@@ -134,6 +131,7 @@ async function listCodexModels() {
134
131
  displayName: model.displayName,
135
132
  description: typeof model.description === 'string' ? model.description : '',
136
133
  isDefault: model.isDefault === true,
134
+ isEconomical: false,
137
135
  }];
138
136
  }) : [];
139
137
  finish(models);
@@ -155,7 +153,7 @@ export function parseClaudeSupportedModels(output) {
155
153
  .split(/,|\band\b/i)
156
154
  .map((model) => model.trim().replace(/^["'`]+|["'`]+$/g, ''))
157
155
  .filter((model) => /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(model))
158
- .map((model) => ({ id: model, displayName: model, description: '', isDefault: false }));
156
+ .map((model) => ({ id: model, displayName: model, description: '', isDefault: false, isEconomical: false }));
159
157
  }
160
158
  async function listClaudeModels() {
161
159
  try {
@@ -173,39 +171,85 @@ async function listClaudeModels() {
173
171
  return parseClaudeSupportedModels(`${candidate.stdout ?? ''}\n${candidate.stderr ?? ''}`);
174
172
  }
175
173
  }
176
- function normalizeDiscoveredModels(input) {
177
- if (!Array.isArray(input))
178
- return [];
179
- return input.flatMap((entry) => {
180
- if (typeof entry === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(entry)) {
181
- 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 [];
182
206
  }
183
- 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;
184
213
  return [];
214
+ }
185
215
  const model = entry;
186
- if (typeof model.id !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(model.id))
187
- 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;
188
224
  return [{
189
- id: model.id,
190
- displayName: typeof model.displayName === 'string' ? model.displayName : model.id,
191
- description: typeof model.description === 'string' ? model.description : '',
192
- 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,
193
230
  }];
194
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 };
195
239
  }
196
240
  function expandPluginPath(value, pluginDir) {
197
241
  return value.replaceAll('{pluginDir}', pluginDir);
198
242
  }
199
243
  async function listPluginModels(config, pluginDir) {
200
244
  if (!config.models)
201
- return [];
245
+ return { models: [], recommendedModel: null, ignoredEntries: 0 };
202
246
  const { stdout } = await execFileAsync(expandPluginPath(config.models.command, pluginDir), (config.models.args ?? []).map((arg) => expandPluginPath(arg, pluginDir)), {
203
247
  timeout: PROBE_TIMEOUT_MS,
204
248
  maxBuffer: 256 * 1024,
205
249
  cwd: pluginDir,
206
250
  env: pluginCommandEnv(),
207
251
  });
208
- return normalizeDiscoveredModels(JSON.parse(stdout));
252
+ return normalizeDiscoveredModelCatalog(JSON.parse(stdout));
209
253
  }
210
254
  function pluginTitleNamers() {
211
255
  return loadPlugins().plugins.flatMap((plugin) => plugin.manifest.titleNamer
@@ -224,6 +268,129 @@ export function shouldRunPluginTitleCommands(slug, selected, enabledAgents) {
224
268
  function isPluginTitleExecutionEnabled(slug) {
225
269
  return shouldRunPluginTitleCommands(slug, getAutoRenameNamerSetting(), getAutoRenameAgentsSetting());
226
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
+ }
227
394
  export async function runTitleNamer(slug, prompt, model) {
228
395
  let command;
229
396
  let args;
@@ -242,11 +409,7 @@ export async function runTitleNamer(slug, prompt, model) {
242
409
  return null;
243
410
  cwd = provider.dir;
244
411
  command = expandPluginPath(provider.config.command, provider.dir);
245
- args = provider.config.args.flatMap((arg) => {
246
- if (arg.includes('{model}') && !model)
247
- return [];
248
- return [expandPluginPath(arg, provider.dir).replaceAll('{prompt}', prompt).replaceAll('{model}', model ?? '')];
249
- });
412
+ args = buildPluginTitleArgs(provider.config, provider.dir, prompt, model);
250
413
  }
251
414
  try {
252
415
  const { stdout } = await execFileAsync(command, args, {
@@ -271,7 +434,7 @@ async function refreshTitleNamerCatalog() {
271
434
  listClaudeModels(),
272
435
  ...plugins.map((provider) => provider.config.models && isPluginTitleExecutionEnabled(provider.slug)
273
436
  ? listPluginModels(provider.config, provider.dir)
274
- : Promise.resolve([])),
437
+ : Promise.resolve({ models: [], recommendedModel: null, ignoredEntries: 0 })),
275
438
  ]);
276
439
  const previousCodex = cached?.value.find((namer) => namer.slug === 'codex');
277
440
  const previousClaude = cached?.value.find((namer) => namer.slug === 'claude');
@@ -295,13 +458,14 @@ async function refreshTitleNamerCatalog() {
295
458
  ...plugins.map((provider, index) => {
296
459
  const result = pluginResults[index];
297
460
  const previous = cached?.value.find((namer) => namer.slug === provider.slug);
298
- 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 ?? [];
299
463
  return {
300
464
  slug: provider.slug,
301
465
  displayName: provider.displayName,
302
466
  available: provider.config.models ? result?.status === 'fulfilled' || previous?.available === true : true,
303
467
  models,
304
- recommendedModel: recommendTitleModel(models),
468
+ recommendedModel: discovered?.recommendedModel ?? recommendTitleModel(models),
305
469
  };
306
470
  }),
307
471
  ];
@@ -166,6 +166,8 @@ Options:
166
166
  Check the installed plugin source for updates
167
167
  --plugin-update <slug>
168
168
  Update a plugin package from its recorded source
169
+ --plugin-doctor <slug>
170
+ Probe one plugin's title/model integration
169
171
  --plugin-hooks <slug> <install|uninstall>
170
172
  Manage the Agent-native hook entries for a plugin
171
173
  --plugin-remove <slug>
@@ -189,6 +191,8 @@ Short commands:
189
191
  Check one plugin for updates
190
192
  plugin-update <slug>
191
193
  Update one plugin from its recorded source
194
+ plugin-doctor <slug> [--json]
195
+ Validate title capability and dynamically probe models
192
196
  plugin-hooks <slug> <install|uninstall>
193
197
  Install or uninstall only that plugin's Agent hooks
194
198
  plugin-remove <slug>
@@ -535,6 +539,7 @@ function parseArgs(argv) {
535
539
  let pluginListJson = false;
536
540
  let pluginCheck;
537
541
  let pluginUpdate;
542
+ let pluginDoctor;
538
543
  let pluginHooks;
539
544
  let pluginRemove;
540
545
  let agentEvent;
@@ -681,6 +686,10 @@ function parseArgs(argv) {
681
686
  pluginUpdate = next && !next.startsWith('-') ? next : '';
682
687
  argv = argv.slice(pluginUpdate ? 2 : 1);
683
688
  }
689
+ else if (command === 'pdoctor' || command === 'plugin-doctor') {
690
+ pluginDoctor = { slug: next && !next.startsWith('-') ? next : '', json: argv[2] === '--json' };
691
+ argv = argv.slice(pluginDoctor.slug ? (pluginDoctor.json ? 3 : 2) : 1);
692
+ }
684
693
  else if (command === 'phooks' || command === 'plugin-hooks') {
685
694
  const action = argv[2];
686
695
  if (next && (action === 'install' || action === 'uninstall')) {
@@ -906,16 +915,18 @@ function parseArgs(argv) {
906
915
  pluginList = true;
907
916
  continue;
908
917
  }
909
- if (arg === '--plugin-install' || arg === '--plugin-check' || arg === '--plugin-update') {
918
+ if (arg === '--plugin-install' || arg === '--plugin-check' || arg === '--plugin-update' || arg === '--plugin-doctor') {
910
919
  const next = argv[index + 1];
911
920
  if (next && !next.startsWith('-')) {
912
921
  if (arg === '--plugin-install')
913
922
  pluginInstall = next;
914
923
  else if (arg === '--plugin-check')
915
924
  pluginCheck = next;
916
- else
925
+ else if (arg === '--plugin-update')
917
926
  pluginUpdate = next;
918
- index += 1;
927
+ else
928
+ pluginDoctor = { slug: next, json: argv[index + 2] === '--json' };
929
+ index += pluginDoctor?.json && arg === '--plugin-doctor' ? 2 : 1;
919
930
  }
920
931
  else if (arg === '--plugin-install') {
921
932
  pluginInstall = '';
@@ -923,9 +934,12 @@ function parseArgs(argv) {
923
934
  else if (arg === '--plugin-check') {
924
935
  pluginCheck = '';
925
936
  }
926
- else {
937
+ else if (arg === '--plugin-update') {
927
938
  pluginUpdate = '';
928
939
  }
940
+ else {
941
+ pluginDoctor = { slug: '', json: false };
942
+ }
929
943
  continue;
930
944
  }
931
945
  if (arg === '--plugin-hooks') {
@@ -1002,6 +1016,7 @@ function parseArgs(argv) {
1002
1016
  pluginListJson,
1003
1017
  pluginCheck,
1004
1018
  pluginUpdate,
1019
+ pluginDoctor,
1005
1020
  pluginHooks,
1006
1021
  pluginRemove,
1007
1022
  agentEvent,
@@ -1815,7 +1830,8 @@ const AGENT_PLUGIN_EXAMPLE = {
1815
1830
  },
1816
1831
  titleNamer: {
1817
1832
  command: 'my-agent',
1818
- args: ['title', '--json', '--prompt', '{prompt}', '--model={model}'],
1833
+ modelArgs: ['--model', '{model}'],
1834
+ args: ['title', '--json', '--prompt', '{prompt}'],
1819
1835
  models: {
1820
1836
  command: 'my-agent',
1821
1837
  args: ['models', '--json'],
@@ -1833,6 +1849,7 @@ function runPluginInitJson() {
1833
1849
  installPackage: 'td plugin-install https://github.com/owner/repository',
1834
1850
  checkUpdate: 'td plugin-check <slug>',
1835
1851
  updatePackage: 'td plugin-update <slug>',
1852
+ doctor: 'td plugin-doctor <slug> --json',
1836
1853
  emit: 'td agent-event <slug> <event> [status]',
1837
1854
  inspect: 'td plugin-list --json',
1838
1855
  hooks: 'td plugin-hooks <slug> <install|uninstall>',
@@ -1862,6 +1879,14 @@ function runPluginInitJson() {
1862
1879
  titleCommands: 'Installing or inspecting a plugin never executes its title/model commands. They run only after the user enables automatic titles for that plugin.',
1863
1880
  updates: 'Changes to hook or title command declarations revoke the corresponding prior opt-in.',
1864
1881
  },
1882
+ titleModelContract: {
1883
+ noCatalog: 'If titleNamer.models is absent or yields no recommendation, Termdock omits modelArgs and uses the Agent CLI default model.',
1884
+ optionalArguments: 'Put every model-only flag/value in titleNamer.modelArgs. The complete group is prepended only when a model is selected.',
1885
+ acceptedOutput: 'A JSON array or {models, recommendedModel}. Entries may use id, name, or model; optional fields: displayName/display_name, description, isDefault/is_default, isEconomical/is_economical.',
1886
+ automaticSelection: 'Top-level recommendedModel, then the first isEconomical model, then the first isDefault model; otherwise omit modelArgs and use the CLI default. Termdock never guesses price from names or descriptions.',
1887
+ adapters: 'Use command: node plus {pluginDir}/scripts/list-models.mjs when the native CLI output needs normalization. Never hardcode a model catalog.',
1888
+ verify: 'Run td plugin-doctor <slug> --json. This explicit command probes models but never spends a title-generation request.',
1889
+ },
1865
1890
  migrationV1: {
1866
1891
  errorCode: 'AGENT_PLUGIN_MANIFEST_V1_UNSUPPORTED',
1867
1892
  guideCommand: 'td agent-plugin --json',
@@ -1880,6 +1905,8 @@ function runPluginInitJson() {
1880
1905
  'If the Agent hook format is not a Claude-style hooks map, configure native hooks to call td agent-event directly.',
1881
1906
  'Map the completed-turn hook to event "stop"; this is also the automatic-title trigger when the user enables titles for the Agent.',
1882
1907
  'Optionally declare titleNamer so this Agent can generate titles and dynamically publish its own model catalog.',
1908
+ 'Put optional model flags in titleNamer.modelArgs; keep the always-present prompt invocation in titleNamer.args.',
1909
+ 'Run td plugin-doctor <slug> --json and fix every warning before sharing the repository.',
1883
1910
  'Tell the user that enabling a plugin title provider executes that plugin\'s declared CLI; installation alone never does.',
1884
1911
  'Run td plugin-list to verify registration.',
1885
1912
  ],
@@ -1990,10 +2017,12 @@ ${c.dim('───────────────────')}
1990
2017
  ${c.dim('// ── optional: injectable automatic-title provider ──')}
1991
2018
  "titleNamer": {
1992
2019
  ${c.dim('// No shell expansion. Commands run only after the user enables this title provider.')}
1993
- ${c.dim('// {prompt} is required; a bare {model} arg is omitted when automatic.')}
2020
+ ${c.dim('// modelArgs is an all-or-nothing group and is omitted for CLI-default mode.')}
1994
2021
  "command": "trae",
1995
- "args": ["title", "--prompt", "{prompt}", "--model={model}"],
1996
- ${c.dim('// Must print JSON: strings or {id, displayName, description, isDefault} objects.')}
2022
+ "modelArgs": ["--model", "{model}"],
2023
+ "args": ["title", "--prompt", "{prompt}"],
2024
+ ${c.dim('// JSON array or {models,recommendedModel}; id/name/model are accepted.')}
2025
+ ${c.dim('// Use node + {pluginDir}/scripts/... when native fields need conversion.')}
1997
2026
  "models": { "command": "trae", "args": ["models", "--json"] }
1998
2027
  }
1999
2028
  }
@@ -2029,6 +2058,7 @@ ${c.dim('──────────────────')}
2029
2058
  ${c.dim('# Manage the package and its hooks independently')}
2030
2059
  td plugin-check trae
2031
2060
  td plugin-update trae
2061
+ td plugin-doctor trae --json
2032
2062
  td plugin-hooks trae install
2033
2063
 
2034
2064
  ${c.dim('# Remove')}
@@ -2190,6 +2220,37 @@ async function runPluginSourceAction(slug, action) {
2190
2220
  console.log(`${ICON.warn} ${c.yellow(`Title commands need attention: ${body.titleWarning}`)}`);
2191
2221
  }
2192
2222
  }
2223
+ async function runPluginDoctor(slug, json = false) {
2224
+ const { baseUrl, token } = requireRunningServer();
2225
+ const response = await postLocalJson(baseUrl, token, `/api/terminal/agent-plugins/${encodeURIComponent(slug)}/doctor`, {});
2226
+ const body = JSON.parse(response.body || '{}');
2227
+ if (response.statusCode < 200 || response.statusCode >= 300) {
2228
+ console.error(`${ICON.err} ${c.red(body.error ?? 'Plugin diagnosis failed')}`);
2229
+ process.exit(1);
2230
+ }
2231
+ if (json) {
2232
+ console.log(JSON.stringify(body, null, 2));
2233
+ }
2234
+ else {
2235
+ const ok = body.status === 'ok' || body.status === 'cli-default';
2236
+ console.log(`${ok ? ICON.ok : ICON.warn} ${ok ? c.green(body.displayName ?? slug) : c.yellow(body.displayName ?? slug)} ${c.dim(`(${body.status ?? 'unknown'})`)}`);
2237
+ console.log(` ${c.dim('Title provider:')} ${body.hasTitleNamer ? 'yes' : 'no'}`);
2238
+ console.log(` ${c.dim('Model probe:')} ${body.hasModelCommand ? `${body.models?.length ?? 0} usable model(s)` : 'not declared; CLI default'}`);
2239
+ if (body.recommendedModel)
2240
+ console.log(` ${c.dim('Automatic model:')} ${c.cyan(body.recommendedModel)}`);
2241
+ if (body.selectionBehavior)
2242
+ console.log(` ${c.dim('Behavior:')} ${body.selectionBehavior}`);
2243
+ if (body.error)
2244
+ console.log(` ${c.red('Error:')} ${body.error}`);
2245
+ for (const warning of body.warnings ?? [])
2246
+ console.log(` ${ICON.warn} ${c.yellow(warning)}`);
2247
+ for (const step of body.nextSteps ?? [])
2248
+ console.log(` ${c.dim('Next:')} ${step}`);
2249
+ }
2250
+ if (body.status === 'missing-title-namer' || body.status === 'probe-failed' || body.status === 'no-models') {
2251
+ process.exitCode = 1;
2252
+ }
2253
+ }
2193
2254
  async function runPluginHooks(slug, action) {
2194
2255
  const { baseUrl, token } = requireRunningServer();
2195
2256
  const response = await postLocalJson(baseUrl, token, `/api/terminal/agent-hooks/${encodeURIComponent(slug)}/${action}`, {});
@@ -3220,6 +3281,14 @@ async function main() {
3220
3281
  await runPluginSourceAction(options.pluginUpdate, 'update');
3221
3282
  process.exit(0);
3222
3283
  }
3284
+ if (options.pluginDoctor) {
3285
+ if (!options.pluginDoctor.slug) {
3286
+ console.error(`${ICON.err} ${c.red('plugin-doctor requires a plugin slug')}`);
3287
+ process.exit(1);
3288
+ }
3289
+ await runPluginDoctor(options.pluginDoctor.slug, options.pluginDoctor.json);
3290
+ process.exit(process.exitCode ?? 0);
3291
+ }
3223
3292
  if (options.pluginHooks) {
3224
3293
  await runPluginHooks(options.pluginHooks.slug, options.pluginHooks.action);
3225
3294
  process.exit(0);
@@ -30,7 +30,7 @@ import { setClientViewingSession } from '../notifications/pushViewers.js';
30
30
  import { getQuotaStatus, refreshQuota, startQuotaManager, } from '../quota/QuotaManager.js';
31
31
  import { TermdockAutoUpdateManager } from '../utils/autoUpdate.js';
32
32
  import { AUTO_TITLE_MIN_CONTEXT_CHARS, cleanTerminalContext, generateAgentTitle, isNewAgentSessionId, isAutoTitleReevaluationDue, shouldReplaceAutoTitle, } from '../agent/autoTitle.js';
33
- import { getTitleNamerCatalog, invalidateTitleNamerCatalog } from '../agent/titleNamerCatalog.js';
33
+ import { getTitleNamerCatalog, invalidateTitleNamerCatalog, probePluginTitleNamer } from '../agent/titleNamerCatalog.js';
34
34
  const router = express.Router();
35
35
  const execFileAsync = promisify(execFile);
36
36
  const TERMDOCK_DIR = `${os.homedir()}/.termdock`;
@@ -4719,6 +4719,14 @@ router.post('/agent-plugins/:slug/check-update', async (req, res) => {
4719
4719
  res.status(400).json({ error: error.message, code: 'AGENT_PLUGIN_UPDATE_CHECK_FAILED' });
4720
4720
  }
4721
4721
  });
4722
+ router.post('/agent-plugins/:slug/doctor', async (req, res) => {
4723
+ const result = await probePluginTitleNamer(req.params.slug);
4724
+ if (!result) {
4725
+ res.status(404).json({ error: `Plugin "${req.params.slug}" not found`, code: 'AGENT_PLUGIN_NOT_FOUND' });
4726
+ return;
4727
+ }
4728
+ res.json(result);
4729
+ });
4722
4730
  router.post('/agent-plugins/:slug/update', async (req, res) => {
4723
4731
  const slug = req.params.slug;
4724
4732
  const plugin = loadPlugins().plugins.find((entry) => entry.manifest.slug === slug);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termdock",
3
- "version": "1.4.72",
3
+ "version": "1.4.74",
4
4
  "description": "A complete web-based terminal application with modern UI",
5
5
  "type": "module",
6
6
  "main": ".desktop-dist/main.js",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "packageName": "termdock",
4
- "version": "1.4.72",
4
+ "version": "1.4.74",
5
5
  "runtimeProtocolVersion": 1,
6
6
  "minimumDesktopVersion": "1.4.46",
7
7
  "nodeMajor": 22,
8
8
  "dependencyHash": "sha256-HxC6p3b4kFesefw+vmNYJtPtdIKJaZR1jFNb7DRJqjU=",
9
- "serverBundleHash": "sha256-4Ti+r7BiQtaMfPn2SORjiaLhcxgBT7xkGbas+Dsh6Zo=",
9
+ "serverBundleHash": "sha256-xzvKVLcvFdBQsk4jGML445ipTKOjQeaGWTmNMudp3p0=",
10
10
  "entrypoint": "dist/server/cli.js",
11
11
  "clientEntrypoint": "dist/client/index.html"
12
12
  }
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./mermaid.core-D_NWIuMY.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-BfOTf7yg.js";import{b as i}from"./mermaid.core-D_NWIuMY.js";import"./chunk-5VM5RSS4-CQdzqlQg.js";import"./chunk-XXDRQBXY-C4pOe4OC.js";import"./chunk-VR4S4FIN-DbiXn0dd.js";import"./chunk-32BRIVSS-D_WFmuvb.js";import"./purify.es-CC4Brkr_.js";import"./index-BA-DqJM2.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-BfOTf7yg.js";import{b as i}from"./mermaid.core-D_NWIuMY.js";import"./chunk-5VM5RSS4-CQdzqlQg.js";import"./chunk-XXDRQBXY-C4pOe4OC.js";import"./chunk-VR4S4FIN-DbiXn0dd.js";import"./chunk-32BRIVSS-D_WFmuvb.js";import"./purify.es-CC4Brkr_.js";import"./index-BA-DqJM2.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};