termdock 1.4.71 → 1.4.72
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/README.md +36 -0
- package/dist/client/assets/{ModelPreview-CG6vLwyw.js → ModelPreview-i9dS1A3I.js} +1 -1
- package/dist/client/assets/{abnfDiagram-VRR7QNED-CrXEZLWX.js → abnfDiagram-VRR7QNED-Cs-ty-HA.js} +1 -1
- package/dist/client/assets/{arc-DbKGR7Ex.js → arc-CIX5k_JD.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-PF5BoUbI.js → architectureDiagram-ZJ3FMSHR-DnPnQoJy.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-D1gRzVWB.js → blockDiagram-677ZJIJ3-Dhew-L5v.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-CwvOLync.js → c4Diagram-LMCZKHZV-BSsEMeWn.js} +1 -1
- package/dist/client/assets/channel-CASnt1pP.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-BeRdP-xP.js → chunk-2Q5K7J3B-BWm--1Mo.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-CrqR5rvl.js → chunk-32BRIVSS-D_WFmuvb.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-CjbUr-ko.js → chunk-5VM5RSS4-CQdzqlQg.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-BVEk3Wma.js → chunk-EX3LRPZG-Cm3zLQpt.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-WitJe8kR.js → chunk-JWPE2WC7-Cmcv5sEN.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-BCewlLYk.js → chunk-MOJQB5TN-Bam44EwJ.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-CoP0AA4G.js → chunk-RYQCIY6F-DIVOuE8y.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-CSZrnmp_.js → chunk-V7JOEXUC-BfOTf7yg.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-BB-7zz-9.js → chunk-VR4S4FIN-DbiXn0dd.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-CcZZlCik.js → chunk-XXDRQBXY-C4pOe4OC.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-Da_SQWEq.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-Da_SQWEq.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-DcmzUDwh.js → cose-bilkent-JH36ORCC-CCoSPmoc.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-C1XlDVoJ.js → cynefin-VYW2F7L2-B2lYz7sl.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-zY6Aet2S.js → cynefinDiagram-TSTJHNR4-Cd7crN9s.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-DqGTi6zR.js → dagre-VKFMJZFB-q9Fj9HuQ.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-DStcsU7S.js → diagram-FQU43EPY-iqHy5YHW.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-Cs6IMNip.js → diagram-G47NLZAW-BE-jlh1B.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-yA2bpWN7.js → diagram-NH7WQ7WH-DmP7_WOC.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-Dzf9M0PI.js → diagram-OA4YK3LP-CHx_k8Ar.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-D75lpQKA.js → diagram-WEI45ONY-D0wImpAF.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-CkQvIpWy.js → ebnfDiagram-CCIWWBDH-DyCB0rF8.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-Ca00Vjxz.js → erDiagram-Q63AITRT-SRcd1B2Z.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-CGrnSTEp.js → flowDiagram-23GEKE2U-D0i9mFwt.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-0SHfOIP4.js → ganttDiagram-NO4QXBWP-BfwGIYDn.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-BeigYWvs.js → gitGraphDiagram-IHSO6WYX-BXkJQP7O.js} +1 -1
- package/dist/client/assets/index-BA-DqJM2.js +451 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-CiY1-xjN.js → infoDiagram-FWYZ7A6U-CM-Ebv9R.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-CmtDjUm_.js → ishikawaDiagram-FXEZZL3T-U-qnP7xi.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-B7qOr9LY.js → journeyDiagram-5HDEW3XC-D4RxvsuL.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-DO2P2Um3.js → kanban-definition-HUTT4EX6-Dm_asCYK.js} +1 -1
- package/dist/client/assets/{linear-B832hJt0.js → linear-Zhe8d635.js} +1 -1
- package/dist/client/assets/{mermaid.core-CsAzdiCU.js → mermaid.core-D_NWIuMY.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-HI__vXhQ.js → mindmap-definition-LN4V7U3C-DXpNidd5.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR--rZIwjXq.js → pegDiagram-2B236MQR-DdOwOEgu.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-D9jPf5x9.js → pieDiagram-ENE6RG2P-D_A9HoY_.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DTCP5ji2.js → quadrantDiagram-ABIIQ3AL-D-Y3cLEF.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-D2Gspvrl.js → railroadDiagram-RFXS5EU6-DD6eFV82.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-DEJC02IW.js → requirementDiagram-TGXJPOKE-B59HJgL2.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-opOhKPap.js → sankeyDiagram-HTMAVEWB-C2BvHMH8.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-_sz2CRAG.js → sequenceDiagram-DBY2YBRQ-3004tCRm.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-D0Q2QkYF.js → sizeCapture-X5ZJPWSS-DCBhg8QW.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-VgeSQTtW.js → stateDiagram-2N3HPSRC-BVqiKVe0.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Z0GHWyoF.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-ClvBvSl0.js → swimlanes-5IMT3BWC-DS6-sdEW.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-el4HDX0G.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-BXNsrECV.js → timeline-definition-FHXFAJF6-wVOEmQwG.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-BgwiqZd-.js → vennDiagram-L72KCM5P-DMZiSJDk.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-DiqN5ERx.js → wardleyDiagram-EHGQE667-Dx5Aob4Y.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-DQjFVqC6.js → xychartDiagram-FW5EYKEG-BqMhkhmW.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/client/sw.js +1 -1
- package/dist/server/agent/autoTitle.js +3 -0
- package/dist/server/agent/pluginPackages.js +289 -0
- package/dist/server/agent/plugins.js +123 -19
- package/dist/server/agent/titleNamerCatalog.js +39 -8
- package/dist/server/cli.js +205 -14
- package/dist/server/entry.js +11 -2
- package/dist/server/routes/terminal.js +232 -29
- package/dist/server/utils/csrfProtection.js +4 -1
- package/dist/server/utils/settings.js +14 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
- package/dist/client/assets/channel-JTEuKVy6.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-BiFJB_5s.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-BiFJB_5s.js +0 -1
- package/dist/client/assets/index-D5PKdbhW.js +0 -451
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BVh2FRO-.js +0 -1
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-C17V-kgm.js +0 -8
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { ICON_FILE, MANIFEST_FILE, PLUGINS_DIR, SOURCE_METADATA_FILE, validateManifest, writePluginSourceMetadata, } from './plugins.js';
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const MAX_PACKAGE_FILES = 512;
|
|
10
|
+
const MAX_PACKAGE_BYTES = 16 * 1024 * 1024;
|
|
11
|
+
const MAX_SINGLE_FILE_BYTES = 8 * 1024 * 1024;
|
|
12
|
+
const GIT_TIMEOUT_MS = 45_000;
|
|
13
|
+
const IGNORED_NAMES = new Set(['.git', '.github', 'node_modules', SOURCE_METADATA_FILE]);
|
|
14
|
+
const DEFAULT_GIT_HOSTS = new Set(['github.com', 'gitlab.com', 'codeberg.org', 'bitbucket.org']);
|
|
15
|
+
function hardenedGitEnv() {
|
|
16
|
+
return {
|
|
17
|
+
...process.env,
|
|
18
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
19
|
+
GIT_CONFIG_COUNT: '0',
|
|
20
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
21
|
+
GIT_CONFIG_SYSTEM: '/dev/null',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function allowedGitHosts() {
|
|
25
|
+
const configured = (process.env.TERMDOCK_PLUGIN_GIT_HOSTS ?? '')
|
|
26
|
+
.split(',')
|
|
27
|
+
.map((host) => host.trim().toLowerCase())
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
return new Set([...DEFAULT_GIT_HOSTS, ...configured]);
|
|
30
|
+
}
|
|
31
|
+
export function normalizeGitPluginSource(input) {
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(input.trim());
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash)
|
|
40
|
+
return null;
|
|
41
|
+
if (!allowedGitHosts().has(url.hostname.toLowerCase()))
|
|
42
|
+
return null;
|
|
43
|
+
if (!/^\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?\/?$/.test(url.pathname))
|
|
44
|
+
return null;
|
|
45
|
+
url.pathname = url.pathname.replace(/\/$/, '');
|
|
46
|
+
return url.toString();
|
|
47
|
+
}
|
|
48
|
+
async function packageTreeDigest(root) {
|
|
49
|
+
const hash = createHash('sha256');
|
|
50
|
+
let files = 0;
|
|
51
|
+
let bytes = 0;
|
|
52
|
+
async function visit(directory, relative = '') {
|
|
53
|
+
const entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
54
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (IGNORED_NAMES.has(entry.name))
|
|
57
|
+
continue;
|
|
58
|
+
if (entry.isSymbolicLink())
|
|
59
|
+
throw new Error(`Plugin packages may not contain symbolic links: ${path.join(relative, entry.name)}`);
|
|
60
|
+
const absolute = path.join(directory, entry.name);
|
|
61
|
+
const nextRelative = path.join(relative, entry.name);
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
await visit(absolute, nextRelative);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!entry.isFile())
|
|
67
|
+
throw new Error(`Unsupported plugin package entry: ${nextRelative}`);
|
|
68
|
+
const stat = await fs.promises.stat(absolute);
|
|
69
|
+
files += 1;
|
|
70
|
+
bytes += stat.size;
|
|
71
|
+
if (files > MAX_PACKAGE_FILES)
|
|
72
|
+
throw new Error(`Plugin package exceeds ${MAX_PACKAGE_FILES} files`);
|
|
73
|
+
if (stat.size > MAX_SINGLE_FILE_BYTES)
|
|
74
|
+
throw new Error(`Plugin file is too large: ${nextRelative}`);
|
|
75
|
+
if (bytes > MAX_PACKAGE_BYTES)
|
|
76
|
+
throw new Error('Plugin package exceeds the 16 MB unpacked limit');
|
|
77
|
+
hash.update(nextRelative.replaceAll(path.sep, '/'));
|
|
78
|
+
hash.update('\0');
|
|
79
|
+
hash.update(await fs.promises.readFile(absolute));
|
|
80
|
+
hash.update('\0');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
await visit(root);
|
|
84
|
+
return hash.digest('hex');
|
|
85
|
+
}
|
|
86
|
+
async function copyPackageTree(source, destination) {
|
|
87
|
+
await packageTreeDigest(source); // validates limits and symlinks before any destination write
|
|
88
|
+
await fs.promises.mkdir(destination, { recursive: true });
|
|
89
|
+
async function copy(directory, output) {
|
|
90
|
+
const entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (IGNORED_NAMES.has(entry.name))
|
|
93
|
+
continue;
|
|
94
|
+
const from = path.join(directory, entry.name);
|
|
95
|
+
const to = path.join(output, entry.name);
|
|
96
|
+
if (entry.isDirectory()) {
|
|
97
|
+
await fs.promises.mkdir(to, { recursive: true });
|
|
98
|
+
await copy(from, to);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
await fs.promises.copyFile(from, to);
|
|
102
|
+
const stat = await fs.promises.stat(from);
|
|
103
|
+
await fs.promises.chmod(to, stat.mode & 0o777);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
await copy(source, destination);
|
|
108
|
+
}
|
|
109
|
+
function validateIcon(packageDir) {
|
|
110
|
+
const iconPath = path.join(packageDir, ICON_FILE);
|
|
111
|
+
if (!fs.existsSync(iconPath))
|
|
112
|
+
return;
|
|
113
|
+
const svg = fs.readFileSync(iconPath, 'utf8');
|
|
114
|
+
if (!/^\s*<svg\b/i.test(svg))
|
|
115
|
+
throw new Error('icon.svg must contain an SVG document');
|
|
116
|
+
if (/<(?:script|style|foreignObject|iframe|object|embed)\b/i.test(svg)
|
|
117
|
+
|| /<\?(?:xml|xml-stylesheet)\b|<!DOCTYPE\b/i.test(svg)
|
|
118
|
+
|| /\bon[a-z]+\s*=/i.test(svg)
|
|
119
|
+
|| /(?:href|src)\s*=\s*["']?\s*(?!#)[^"'\s>]+/i.test(svg)
|
|
120
|
+
|| /url\(\s*(?!["']?#)[^)]+\)/i.test(svg)) {
|
|
121
|
+
throw new Error('icon.svg contains unsafe active content');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function readAndValidateManifest(packageDir, expectedSlug) {
|
|
125
|
+
const manifestPath = path.join(packageDir, MANIFEST_FILE);
|
|
126
|
+
let raw;
|
|
127
|
+
try {
|
|
128
|
+
raw = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'));
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
throw new Error(`Plugin package must contain a valid root manifest.json: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
const validation = validateManifest(raw, packageDir);
|
|
134
|
+
if ('error' in validation) {
|
|
135
|
+
const error = new Error(validation.error.errors.join('\n'));
|
|
136
|
+
error.code = validation.error.code ?? 'AGENT_PLUGIN_MANIFEST_INVALID';
|
|
137
|
+
error.migration = validation.error.migration;
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
if (expectedSlug && validation.manifest.slug !== expectedSlug) {
|
|
141
|
+
throw new Error(`Update source declares slug "${validation.manifest.slug}"; expected "${expectedSlug}"`);
|
|
142
|
+
}
|
|
143
|
+
validateIcon(packageDir);
|
|
144
|
+
return validation.manifest;
|
|
145
|
+
}
|
|
146
|
+
export async function preparePluginPackage(sourceInput, expectedSlug) {
|
|
147
|
+
const source = sourceInput.trim();
|
|
148
|
+
if (!source)
|
|
149
|
+
throw new Error('Plugin source is required');
|
|
150
|
+
const remote = normalizeGitPluginSource(source);
|
|
151
|
+
const temporaryRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'termdock-plugin-'));
|
|
152
|
+
let packageDir;
|
|
153
|
+
let type;
|
|
154
|
+
let normalizedSource;
|
|
155
|
+
let revision;
|
|
156
|
+
try {
|
|
157
|
+
if (remote) {
|
|
158
|
+
packageDir = path.join(temporaryRoot, 'repo');
|
|
159
|
+
await execFileAsync('git', [
|
|
160
|
+
'-c', 'credential.helper=',
|
|
161
|
+
'-c', 'core.hooksPath=/dev/null',
|
|
162
|
+
'-c', 'filter.lfs.smudge=',
|
|
163
|
+
'-c', 'filter.lfs.required=false',
|
|
164
|
+
'clone', '--depth', '1', '--single-branch', '--no-recurse-submodules', '--', remote, packageDir,
|
|
165
|
+
], {
|
|
166
|
+
timeout: GIT_TIMEOUT_MS,
|
|
167
|
+
maxBuffer: 1024 * 1024,
|
|
168
|
+
env: hardenedGitEnv(),
|
|
169
|
+
});
|
|
170
|
+
const result = await execFileAsync('git', ['-C', packageDir, 'rev-parse', 'HEAD'], {
|
|
171
|
+
timeout: 5000,
|
|
172
|
+
maxBuffer: 128 * 1024,
|
|
173
|
+
});
|
|
174
|
+
revision = result.stdout.trim();
|
|
175
|
+
type = 'git';
|
|
176
|
+
normalizedSource = remote;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const resolved = path.resolve(source);
|
|
180
|
+
const stat = await fs.promises.stat(resolved).catch(() => null);
|
|
181
|
+
if (!stat)
|
|
182
|
+
throw new Error('Plugin source must be an existing local path or an allowed HTTPS Git repository');
|
|
183
|
+
packageDir = stat.isDirectory() ? resolved : path.dirname(resolved);
|
|
184
|
+
if (!stat.isDirectory() && path.basename(resolved) !== MANIFEST_FILE) {
|
|
185
|
+
throw new Error('A local plugin file must be named manifest.json');
|
|
186
|
+
}
|
|
187
|
+
type = 'local';
|
|
188
|
+
normalizedSource = packageDir;
|
|
189
|
+
revision = await packageTreeDigest(packageDir);
|
|
190
|
+
}
|
|
191
|
+
const manifest = await readAndValidateManifest(packageDir, expectedSlug);
|
|
192
|
+
const previous = expectedSlug
|
|
193
|
+
? (() => {
|
|
194
|
+
try {
|
|
195
|
+
return JSON.parse(fs.readFileSync(path.join(PLUGINS_DIR, expectedSlug, SOURCE_METADATA_FILE), 'utf8'));
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
})()
|
|
201
|
+
: null;
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
return {
|
|
204
|
+
manifest,
|
|
205
|
+
packageDir,
|
|
206
|
+
metadata: {
|
|
207
|
+
version: 1,
|
|
208
|
+
type,
|
|
209
|
+
source: normalizedSource,
|
|
210
|
+
revision,
|
|
211
|
+
latestRevision: revision,
|
|
212
|
+
installedAt: typeof previous?.installedAt === 'number' ? previous.installedAt : now,
|
|
213
|
+
updatedAt: now,
|
|
214
|
+
checkedAt: now,
|
|
215
|
+
},
|
|
216
|
+
cleanup: () => fs.promises.rm(temporaryRoot, { recursive: true, force: true }),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
await fs.promises.rm(temporaryRoot, { recursive: true, force: true });
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export async function commitPreparedPlugin(prepared, replace = false) {
|
|
225
|
+
await fs.promises.mkdir(PLUGINS_DIR, { recursive: true });
|
|
226
|
+
const target = path.join(PLUGINS_DIR, prepared.manifest.slug);
|
|
227
|
+
if (!replace && fs.existsSync(target))
|
|
228
|
+
throw new Error(`Plugin "${prepared.manifest.slug}" already exists`);
|
|
229
|
+
const stage = path.join(PLUGINS_DIR, `.stage-${prepared.manifest.slug}-${randomUUID()}`);
|
|
230
|
+
const backup = path.join(PLUGINS_DIR, `.backup-${prepared.manifest.slug}-${randomUUID()}`);
|
|
231
|
+
try {
|
|
232
|
+
await copyPackageTree(prepared.packageDir, stage);
|
|
233
|
+
await fs.promises.writeFile(path.join(stage, MANIFEST_FILE), JSON.stringify(prepared.manifest, null, 2), 'utf8');
|
|
234
|
+
writePluginSourceMetadata(stage, prepared.metadata);
|
|
235
|
+
if (fs.existsSync(target))
|
|
236
|
+
await fs.promises.rename(target, backup);
|
|
237
|
+
try {
|
|
238
|
+
await fs.promises.rename(stage, target);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (fs.existsSync(backup))
|
|
242
|
+
await fs.promises.rename(backup, target);
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
await fs.promises.rm(backup, { recursive: true, force: true });
|
|
246
|
+
return target;
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
await fs.promises.rm(stage, { recursive: true, force: true });
|
|
250
|
+
await prepared.cleanup();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
export async function checkPluginPackageUpdate(plugin) {
|
|
254
|
+
const checkedAt = Date.now();
|
|
255
|
+
const metadata = plugin.source;
|
|
256
|
+
if (!metadata?.source || metadata.type === 'manifest') {
|
|
257
|
+
return { supported: false, updateAvailable: false, revision: metadata?.revision ?? null, latestRevision: null, checkedAt };
|
|
258
|
+
}
|
|
259
|
+
let latestRevision;
|
|
260
|
+
if (metadata.type === 'git') {
|
|
261
|
+
const remote = normalizeGitPluginSource(metadata.source);
|
|
262
|
+
if (!remote)
|
|
263
|
+
throw new Error('The stored Git source is no longer allowed');
|
|
264
|
+
const { stdout } = await execFileAsync('git', [
|
|
265
|
+
'-c', 'credential.helper=',
|
|
266
|
+
'-c', 'core.hooksPath=/dev/null',
|
|
267
|
+
'ls-remote', remote, 'HEAD',
|
|
268
|
+
], {
|
|
269
|
+
timeout: 15_000,
|
|
270
|
+
maxBuffer: 128 * 1024,
|
|
271
|
+
env: hardenedGitEnv(),
|
|
272
|
+
});
|
|
273
|
+
latestRevision = stdout.trim().split(/\s+/)[0] ?? '';
|
|
274
|
+
if (!/^[0-9a-f]{40,64}$/i.test(latestRevision))
|
|
275
|
+
throw new Error('Unable to resolve the plugin repository HEAD');
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
latestRevision = await packageTreeDigest(metadata.source);
|
|
279
|
+
}
|
|
280
|
+
const next = { ...metadata, latestRevision, checkedAt };
|
|
281
|
+
writePluginSourceMetadata(plugin.dir, next);
|
|
282
|
+
return {
|
|
283
|
+
supported: true,
|
|
284
|
+
updateAvailable: Boolean(metadata.revision && latestRevision !== metadata.revision),
|
|
285
|
+
revision: metadata.revision,
|
|
286
|
+
latestRevision,
|
|
287
|
+
checkedAt,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -13,13 +13,42 @@
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import os from 'node:os';
|
|
16
|
-
const PLUGINS_DIR = path.join(os.homedir(), '.termdock', 'agent-plugins');
|
|
17
|
-
const MANIFEST_FILE = 'manifest.json';
|
|
18
|
-
const ICON_FILE = 'icon.svg';
|
|
16
|
+
export const PLUGINS_DIR = path.join(os.homedir(), '.termdock', 'agent-plugins');
|
|
17
|
+
export const MANIFEST_FILE = 'manifest.json';
|
|
18
|
+
export const ICON_FILE = 'icon.svg';
|
|
19
|
+
export const SOURCE_METADATA_FILE = '.termdock-source.json';
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Manifest schema
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
export const PLUGIN_MANIFEST_VERSION = 2;
|
|
24
|
+
function parseSourceMetadata(raw) {
|
|
25
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
26
|
+
return null;
|
|
27
|
+
const value = raw;
|
|
28
|
+
if (value.version !== 1 || !['git', 'local', 'manifest'].includes(String(value.type)))
|
|
29
|
+
return null;
|
|
30
|
+
return {
|
|
31
|
+
version: 1,
|
|
32
|
+
type: value.type,
|
|
33
|
+
source: typeof value.source === 'string' ? value.source : null,
|
|
34
|
+
revision: typeof value.revision === 'string' ? value.revision : null,
|
|
35
|
+
latestRevision: typeof value.latestRevision === 'string' ? value.latestRevision : null,
|
|
36
|
+
installedAt: typeof value.installedAt === 'number' ? value.installedAt : 0,
|
|
37
|
+
updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : 0,
|
|
38
|
+
checkedAt: typeof value.checkedAt === 'number' ? value.checkedAt : null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function readPluginSourceMetadata(dir) {
|
|
42
|
+
try {
|
|
43
|
+
return parseSourceMetadata(JSON.parse(fs.readFileSync(path.join(dir, SOURCE_METADATA_FILE), 'utf8')));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function writePluginSourceMetadata(dir, metadata) {
|
|
50
|
+
fs.writeFileSync(path.join(dir, SOURCE_METADATA_FILE), JSON.stringify(metadata, null, 2), 'utf8');
|
|
51
|
+
}
|
|
23
52
|
// ---------------------------------------------------------------------------
|
|
24
53
|
// Known sentinel event kinds — the set of events our state machine accepts
|
|
25
54
|
// ---------------------------------------------------------------------------
|
|
@@ -48,6 +77,14 @@ function isValidSlug(s) {
|
|
|
48
77
|
// Hex color validation
|
|
49
78
|
// ---------------------------------------------------------------------------
|
|
50
79
|
const HEX_COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
|
|
80
|
+
function isSafeSvgIcon(svg) {
|
|
81
|
+
return /^\s*<svg\b/i.test(svg)
|
|
82
|
+
&& !/<(?:script|style|foreignObject|iframe|object|embed)\b/i.test(svg)
|
|
83
|
+
&& !/<\?(?:xml|xml-stylesheet)\b|<!DOCTYPE\b/i.test(svg)
|
|
84
|
+
&& !/\bon[a-z]+\s*=/i.test(svg)
|
|
85
|
+
&& !/(?:href|src)\s*=\s*["']?\s*(?!#)[^"'\s>]+/i.test(svg)
|
|
86
|
+
&& !/url\(\s*(?!["']?#)[^)]+\)/i.test(svg);
|
|
87
|
+
}
|
|
51
88
|
// ---------------------------------------------------------------------------
|
|
52
89
|
// Path resolution: ~ → home directory
|
|
53
90
|
// ---------------------------------------------------------------------------
|
|
@@ -57,6 +94,33 @@ function resolveHome(p) {
|
|
|
57
94
|
}
|
|
58
95
|
return p;
|
|
59
96
|
}
|
|
97
|
+
function validateHookTarget(target) {
|
|
98
|
+
if (!target.startsWith('~/') || !target.toLowerCase().endsWith('.json')) {
|
|
99
|
+
return 'hooks.target must be a ~/ path to a JSON file';
|
|
100
|
+
}
|
|
101
|
+
const home = path.resolve(os.homedir());
|
|
102
|
+
const resolved = path.resolve(home, target.slice(2));
|
|
103
|
+
if (resolved === home || !resolved.startsWith(`${home}${path.sep}`)) {
|
|
104
|
+
return 'hooks.target must stay inside the user home directory';
|
|
105
|
+
}
|
|
106
|
+
// Existing symlinks in any target component could redirect a later write
|
|
107
|
+
// outside $HOME. Refuse them instead of relying on a racy realpath check.
|
|
108
|
+
let current = home;
|
|
109
|
+
for (const part of path.relative(home, resolved).split(path.sep)) {
|
|
110
|
+
current = path.join(current, part);
|
|
111
|
+
try {
|
|
112
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
113
|
+
return 'hooks.target may not traverse symbolic links';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error.code !== 'ENOENT') {
|
|
118
|
+
return 'hooks.target could not be safely inspected';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
60
124
|
export const V1_MIGRATION_GUIDE_COMMAND = 'td agent-plugin --json';
|
|
61
125
|
export const V1_MIGRATION_AI_PROMPT = [
|
|
62
126
|
'Please migrate the attached Termdock Agent plugin manifest from v1 to v2.',
|
|
@@ -86,12 +150,13 @@ export function validateManifest(raw, dir) {
|
|
|
86
150
|
errors.push('slug must be a lowercase alphanumeric string with hyphens (max 40 chars)');
|
|
87
151
|
}
|
|
88
152
|
const displayName = m.displayName;
|
|
89
|
-
if (typeof displayName !== 'string' || displayName.trim().length === 0) {
|
|
90
|
-
errors.push('displayName is required and must be a non-empty string');
|
|
153
|
+
if (typeof displayName !== 'string' || displayName.trim().length === 0 || displayName.length > 80) {
|
|
154
|
+
errors.push('displayName is required and must be a non-empty string (max 80 chars)');
|
|
91
155
|
}
|
|
92
156
|
const aliases = m.aliases;
|
|
93
|
-
if (!Array.isArray(aliases) || aliases.length === 0 ||
|
|
94
|
-
|
|
157
|
+
if (!Array.isArray(aliases) || aliases.length === 0 || aliases.length > 16
|
|
158
|
+
|| !aliases.every((a) => typeof a === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(a))) {
|
|
159
|
+
errors.push('aliases must contain 1-16 safe command names');
|
|
95
160
|
}
|
|
96
161
|
const accentColor = m.accentColor;
|
|
97
162
|
if (typeof accentColor !== 'string' || !HEX_COLOR_RE.test(accentColor)) {
|
|
@@ -167,6 +232,11 @@ export function validateManifest(raw, dir) {
|
|
|
167
232
|
if (typeof target !== 'string' || target.trim().length === 0) {
|
|
168
233
|
errors.push('hooks.target is required and must be a file path (~-abbreviated ok)');
|
|
169
234
|
}
|
|
235
|
+
else {
|
|
236
|
+
const targetError = validateHookTarget(target.trim());
|
|
237
|
+
if (targetError)
|
|
238
|
+
errors.push(targetError);
|
|
239
|
+
}
|
|
170
240
|
const events = h.events;
|
|
171
241
|
if (!Array.isArray(events) || events.length === 0) {
|
|
172
242
|
errors.push('hooks.events must be a non-empty array');
|
|
@@ -182,15 +252,15 @@ export function validateManifest(raw, dir) {
|
|
|
182
252
|
const evt = e;
|
|
183
253
|
const hook = evt.hook;
|
|
184
254
|
const event = evt.event;
|
|
185
|
-
if (typeof hook !== 'string' || hook.trim().length === 0) {
|
|
255
|
+
if (typeof hook !== 'string' || hook.trim().length === 0 || hook.length > 120) {
|
|
186
256
|
errors.push(`hooks.events[${i}].hook is required`);
|
|
187
257
|
}
|
|
188
258
|
if (typeof event !== 'string' || !VALID_EVENT_KINDS.has(event)) {
|
|
189
259
|
errors.push(`hooks.events[${i}].event must be one of: ${[...VALID_EVENT_KINDS].join(', ')}`);
|
|
190
260
|
}
|
|
191
261
|
const matcher = evt.matcher;
|
|
192
|
-
if (matcher !== undefined && typeof matcher !== 'string') {
|
|
193
|
-
errors.push(`hooks.events[${i}].matcher must be a string if provided`);
|
|
262
|
+
if (matcher !== undefined && (typeof matcher !== 'string' || matcher.length > 512)) {
|
|
263
|
+
errors.push(`hooks.events[${i}].matcher must be a string (max 512 chars) if provided`);
|
|
194
264
|
}
|
|
195
265
|
const timeout = evt.timeout;
|
|
196
266
|
if (timeout !== undefined && (typeof timeout !== 'number' || timeout <= 0)) {
|
|
@@ -226,9 +296,20 @@ export function validateManifest(raw, dir) {
|
|
|
226
296
|
if (typeof command !== 'string' || !command.includes('{sessionId}')) {
|
|
227
297
|
errors.push('resume.command must contain the {sessionId} placeholder');
|
|
228
298
|
}
|
|
299
|
+
else {
|
|
300
|
+
const tokens = command.trim().split(/\s+/);
|
|
301
|
+
const executable = path.basename(tokens[0] ?? '');
|
|
302
|
+
if (command.length > 1024
|
|
303
|
+
|| !tokens.every((token) => /^[A-Za-z0-9_./:@%+,={}-]+$/.test(token))
|
|
304
|
+
|| !Array.isArray(aliases)
|
|
305
|
+
|| !aliases.includes(executable)) {
|
|
306
|
+
errors.push('resume.command must be a simple argv-like command whose executable is one of aliases; shell syntax is not allowed');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
229
309
|
const staleFlags = r.staleFlags;
|
|
230
|
-
if (staleFlags !== undefined && (!Array.isArray(staleFlags) ||
|
|
231
|
-
|
|
310
|
+
if (staleFlags !== undefined && (!Array.isArray(staleFlags) || staleFlags.length > 32
|
|
311
|
+
|| !staleFlags.every((f) => typeof f === 'string' && /^--?[A-Za-z0-9][A-Za-z0-9-]{0,79}$/.test(f)))) {
|
|
312
|
+
errors.push('resume.staleFlags must be an array of safe CLI flags if provided');
|
|
232
313
|
}
|
|
233
314
|
resumeConfig = {
|
|
234
315
|
command: command,
|
|
@@ -248,10 +329,12 @@ export function validateManifest(raw, dir) {
|
|
|
248
329
|
const n = m.titleNamer;
|
|
249
330
|
const command = n.command;
|
|
250
331
|
const args = n.args;
|
|
251
|
-
if (typeof command !== 'string' || command.trim().length === 0) {
|
|
332
|
+
if (typeof command !== 'string' || command.trim().length === 0 || command.length > 1024) {
|
|
252
333
|
errors.push('titleNamer.command is required');
|
|
253
334
|
}
|
|
254
|
-
if (!Array.isArray(args) ||
|
|
335
|
+
if (!Array.isArray(args) || args.length > 64
|
|
336
|
+
|| !args.every((arg) => typeof arg === 'string' && arg.length <= 16_384)
|
|
337
|
+
|| !args.some((arg) => arg.includes('{prompt}'))) {
|
|
255
338
|
errors.push('titleNamer.args must be a string array containing {prompt}');
|
|
256
339
|
}
|
|
257
340
|
let models;
|
|
@@ -261,10 +344,11 @@ export function validateManifest(raw, dir) {
|
|
|
261
344
|
}
|
|
262
345
|
else {
|
|
263
346
|
const rawModels = n.models;
|
|
264
|
-
if (typeof rawModels.command !== 'string' || rawModels.command.trim().length === 0) {
|
|
347
|
+
if (typeof rawModels.command !== 'string' || rawModels.command.trim().length === 0 || rawModels.command.length > 1024) {
|
|
265
348
|
errors.push('titleNamer.models.command is required');
|
|
266
349
|
}
|
|
267
|
-
if (rawModels.args !== undefined && (!Array.isArray(rawModels.args) ||
|
|
350
|
+
if (rawModels.args !== undefined && (!Array.isArray(rawModels.args) || rawModels.args.length > 64
|
|
351
|
+
|| !rawModels.args.every((arg) => typeof arg === 'string' && arg.length <= 16_384))) {
|
|
268
352
|
errors.push('titleNamer.models.args must be a string array if present');
|
|
269
353
|
}
|
|
270
354
|
if (typeof rawModels.command === 'string') {
|
|
@@ -348,18 +432,27 @@ export function loadPlugins() {
|
|
|
348
432
|
}
|
|
349
433
|
const iconPath = path.join(dir, ICON_FILE);
|
|
350
434
|
let iconMtime = 0;
|
|
351
|
-
|
|
435
|
+
let iconExists = fs.existsSync(iconPath);
|
|
352
436
|
if (iconExists) {
|
|
353
437
|
try {
|
|
354
|
-
|
|
438
|
+
if (!isSafeSvgIcon(fs.readFileSync(iconPath, 'utf8'))) {
|
|
439
|
+
errors.push({ slug: result.manifest.slug, errors: ['icon.svg contains unsafe or invalid SVG content'] });
|
|
440
|
+
iconExists = false;
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
iconMtime = fs.statSync(iconPath).mtimeMs;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
iconExists = false;
|
|
355
448
|
}
|
|
356
|
-
catch { /* keep 0 */ }
|
|
357
449
|
}
|
|
358
450
|
plugins.push({
|
|
359
451
|
manifest: result.manifest,
|
|
360
452
|
dir,
|
|
361
453
|
iconPath: iconExists ? iconPath : null,
|
|
362
454
|
iconMtime,
|
|
455
|
+
source: readPluginSourceMetadata(dir),
|
|
363
456
|
});
|
|
364
457
|
}
|
|
365
458
|
return { plugins, errors };
|
|
@@ -372,6 +465,17 @@ export function savePlugin(manifest) {
|
|
|
372
465
|
fs.mkdirSync(dir, { recursive: true });
|
|
373
466
|
const manifestPath = path.join(dir, MANIFEST_FILE);
|
|
374
467
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
|
|
468
|
+
const now = Date.now();
|
|
469
|
+
writePluginSourceMetadata(dir, {
|
|
470
|
+
version: 1,
|
|
471
|
+
type: 'manifest',
|
|
472
|
+
source: null,
|
|
473
|
+
revision: null,
|
|
474
|
+
latestRevision: null,
|
|
475
|
+
installedAt: now,
|
|
476
|
+
updatedAt: now,
|
|
477
|
+
checkedAt: null,
|
|
478
|
+
});
|
|
375
479
|
return dir;
|
|
376
480
|
}
|
|
377
481
|
/**
|
|
@@ -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() {
|
|
@@ -179,13 +193,17 @@ function normalizeDiscoveredModels(input) {
|
|
|
179
193
|
}];
|
|
180
194
|
});
|
|
181
195
|
}
|
|
182
|
-
|
|
196
|
+
function expandPluginPath(value, pluginDir) {
|
|
197
|
+
return value.replaceAll('{pluginDir}', pluginDir);
|
|
198
|
+
}
|
|
199
|
+
async function listPluginModels(config, pluginDir) {
|
|
183
200
|
if (!config.models)
|
|
184
201
|
return [];
|
|
185
|
-
const { stdout } = await execFileAsync(config.models.command, config.models.args ?? [], {
|
|
202
|
+
const { stdout } = await execFileAsync(expandPluginPath(config.models.command, pluginDir), (config.models.args ?? []).map((arg) => expandPluginPath(arg, pluginDir)), {
|
|
186
203
|
timeout: PROBE_TIMEOUT_MS,
|
|
187
204
|
maxBuffer: 256 * 1024,
|
|
188
|
-
|
|
205
|
+
cwd: pluginDir,
|
|
206
|
+
env: pluginCommandEnv(),
|
|
189
207
|
});
|
|
190
208
|
return normalizeDiscoveredModels(JSON.parse(stdout));
|
|
191
209
|
}
|
|
@@ -195,12 +213,21 @@ function pluginTitleNamers() {
|
|
|
195
213
|
slug: plugin.manifest.slug,
|
|
196
214
|
displayName: plugin.manifest.displayName,
|
|
197
215
|
config: plugin.manifest.titleNamer,
|
|
216
|
+
dir: plugin.dir,
|
|
198
217
|
}]
|
|
199
218
|
: []);
|
|
200
219
|
}
|
|
220
|
+
export function shouldRunPluginTitleCommands(slug, selected, enabledAgents) {
|
|
221
|
+
return selected === slug
|
|
222
|
+
|| (selected === 'auto' && enabledAgents.includes(slug));
|
|
223
|
+
}
|
|
224
|
+
function isPluginTitleExecutionEnabled(slug) {
|
|
225
|
+
return shouldRunPluginTitleCommands(slug, getAutoRenameNamerSetting(), getAutoRenameAgentsSetting());
|
|
226
|
+
}
|
|
201
227
|
export async function runTitleNamer(slug, prompt, model) {
|
|
202
228
|
let command;
|
|
203
229
|
let args;
|
|
230
|
+
let cwd;
|
|
204
231
|
if (slug === 'codex') {
|
|
205
232
|
command = 'codex';
|
|
206
233
|
args = ['exec', '--ephemeral', '--skip-git-repo-check', '--color', 'never', ...(model ? ['--model', model] : []), prompt];
|
|
@@ -211,20 +238,22 @@ export async function runTitleNamer(slug, prompt, model) {
|
|
|
211
238
|
}
|
|
212
239
|
else {
|
|
213
240
|
const provider = pluginTitleNamers().find((entry) => entry.slug === slug);
|
|
214
|
-
if (!provider)
|
|
241
|
+
if (!provider || !isPluginTitleExecutionEnabled(slug))
|
|
215
242
|
return null;
|
|
216
|
-
|
|
243
|
+
cwd = provider.dir;
|
|
244
|
+
command = expandPluginPath(provider.config.command, provider.dir);
|
|
217
245
|
args = provider.config.args.flatMap((arg) => {
|
|
218
246
|
if (arg.includes('{model}') && !model)
|
|
219
247
|
return [];
|
|
220
|
-
return [arg.replaceAll('{prompt}', prompt).replaceAll('{model}', model ?? '')];
|
|
248
|
+
return [expandPluginPath(arg, provider.dir).replaceAll('{prompt}', prompt).replaceAll('{model}', model ?? '')];
|
|
221
249
|
});
|
|
222
250
|
}
|
|
223
251
|
try {
|
|
224
252
|
const { stdout } = await execFileAsync(command, args, {
|
|
225
253
|
timeout: 45_000,
|
|
226
254
|
maxBuffer: 256 * 1024,
|
|
227
|
-
|
|
255
|
+
cwd,
|
|
256
|
+
env: cwd ? pluginCommandEnv() : { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
|
|
228
257
|
});
|
|
229
258
|
return stdout;
|
|
230
259
|
}
|
|
@@ -240,7 +269,9 @@ async function refreshTitleNamerCatalog() {
|
|
|
240
269
|
const [codex, claude, ...pluginResults] = await Promise.allSettled([
|
|
241
270
|
listCodexModels(),
|
|
242
271
|
listClaudeModels(),
|
|
243
|
-
...plugins.map((provider) =>
|
|
272
|
+
...plugins.map((provider) => provider.config.models && isPluginTitleExecutionEnabled(provider.slug)
|
|
273
|
+
? listPluginModels(provider.config, provider.dir)
|
|
274
|
+
: Promise.resolve([])),
|
|
244
275
|
]);
|
|
245
276
|
const previousCodex = cached?.value.find((namer) => namer.slug === 'codex');
|
|
246
277
|
const previousClaude = cached?.value.find((namer) => namer.slug === 'claude');
|