dsh-plugin-prompt-tool 0.1.2 → 0.1.3
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/AGENTS.md +0 -2
- package/README.md +211 -204
- package/lib/client.js +126 -51
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +46 -23
- package/lib/index.mjs +375 -88
- package/package.json +96 -88
- package/preset/prompt-injector.mjs +6 -1
- package/preset.md +1 -1
- package/vendor/dsh-anchored-standard/README.md +205 -48
- package/vendor/dsh-anchored-standard/preset/agent.cordis.yml +2 -2
- package/vendor/dsh-anchored-standard/preset/compaction-epoch.mjs +10 -3
package/lib/index.mjs
CHANGED
|
@@ -7,7 +7,11 @@ import { join } from "node:path";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
//#region src/index.ts
|
|
9
9
|
const name = "prompt-tool";
|
|
10
|
-
const inject = [
|
|
10
|
+
const inject = [
|
|
11
|
+
"skills",
|
|
12
|
+
"webServer",
|
|
13
|
+
"commands"
|
|
14
|
+
];
|
|
11
15
|
const PRESET_FILE_URL = new URL("../preset.md", import.meta.url);
|
|
12
16
|
const PRESET_FILE_PATH = fileURLToPath(PRESET_FILE_URL);
|
|
13
17
|
const SKILLS_DIR = fileURLToPath(new URL("../skills", import.meta.url));
|
|
@@ -15,40 +19,67 @@ const AGENTS_URL = new URL("../AGENTS.md", import.meta.url);
|
|
|
15
19
|
const AGENTS_FILE_PATH = fileURLToPath(AGENTS_URL);
|
|
16
20
|
const NS = settingsNamespace("prompt-tool");
|
|
17
21
|
const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const PRESET_ANCHOR = join(PRESET_DIR, "turn-anchor.mjs");
|
|
22
|
+
const DEFAULT_RESIDENT_AGENTS_PATH = join(DSH_HOME, "AGENTS.md");
|
|
23
|
+
const DEFAULT_PRESET_DIR = join(DSH_HOME, ".agent-presets", "prompt-tool");
|
|
24
|
+
const DEFAULT_SKILLS_DIR = SKILLS_DIR;
|
|
25
|
+
const DEFAULT_PRESET_ORDER = 5;
|
|
26
|
+
const DEFAULT_SKILL_RANK_BASE = 250;
|
|
24
27
|
const PRESET_TEMPLATE_META = fileURLToPath(new URL("../preset/preset.yml", import.meta.url));
|
|
25
28
|
const PRESET_TEMPLATE_INJECTOR = fileURLToPath(new URL("../preset/prompt-injector.mjs", import.meta.url));
|
|
26
29
|
const PRESET_TEMPLATE_ANCHOR = fileURLToPath(new URL("../preset/turn-anchor.mjs", import.meta.url));
|
|
27
30
|
const VENDOR_PRESET_DIR = fileURLToPath(new URL("../vendor/dsh-anchored-standard/preset", import.meta.url));
|
|
28
|
-
const
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const Config = z.object({
|
|
32
|
+
text: z.string().default(""),
|
|
33
|
+
agentsText: z.string().default(""),
|
|
34
|
+
injectAgentsPrompt: z.boolean().default(false),
|
|
35
|
+
writeAgents: z.boolean().default(true),
|
|
36
|
+
writePreset: z.boolean().default(true),
|
|
37
|
+
injectPrompt: z.boolean().default(true),
|
|
38
|
+
skillSwitches: z.dict(z.boolean()).default({}),
|
|
39
|
+
anchorFirstTurn: z.boolean().default(false),
|
|
40
|
+
anchorText: z.string().default("You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'."),
|
|
41
|
+
skillsDir: z.string().default(DEFAULT_SKILLS_DIR),
|
|
42
|
+
skillRankBase: z.natural().default(DEFAULT_SKILL_RANK_BASE),
|
|
43
|
+
residentAgentsPath: z.string().default(DEFAULT_RESIDENT_AGENTS_PATH),
|
|
44
|
+
presetDir: z.string().default(DEFAULT_PRESET_DIR),
|
|
45
|
+
presetOrder: z.natural().default(DEFAULT_PRESET_ORDER),
|
|
46
|
+
fallbackText: z.string().default("")
|
|
47
|
+
});
|
|
48
|
+
const PromptSettingsSchema = z.object({
|
|
49
|
+
promptText: z.string().default(""),
|
|
50
|
+
promptPath: z.string().default(""),
|
|
51
|
+
agentsText: z.string().default(""),
|
|
52
|
+
agentsPath: z.string().default(""),
|
|
53
|
+
injectAgentsPrompt: z.boolean().default(false),
|
|
54
|
+
anchorFirstTurn: z.boolean().default(false),
|
|
55
|
+
anchorText: z.string().default("You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'."),
|
|
56
|
+
injectPrompt: z.boolean().default(true),
|
|
57
|
+
skillSwitches: z.dict(z.boolean()).default({}),
|
|
58
|
+
skillCatalog: z.array(z.object({
|
|
59
|
+
folder: z.string(),
|
|
60
|
+
name: z.string(),
|
|
61
|
+
description: z.string().default("")
|
|
62
|
+
})).default([]),
|
|
63
|
+
writeAgents: z.boolean().default(true),
|
|
64
|
+
writePreset: z.boolean().default(true)
|
|
65
|
+
});
|
|
66
|
+
function readPromptFile(fallbackText) {
|
|
36
67
|
try {
|
|
37
68
|
return readFileSync(PRESET_FILE_URL, "utf8");
|
|
38
69
|
} catch {
|
|
39
|
-
return
|
|
70
|
+
return fallbackText;
|
|
40
71
|
}
|
|
41
72
|
}
|
|
42
|
-
function listSkillFolders() {
|
|
73
|
+
function listSkillFolders(skillsDir) {
|
|
43
74
|
try {
|
|
44
|
-
return readdirSync(
|
|
75
|
+
return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
|
|
45
76
|
} catch {
|
|
46
77
|
return [];
|
|
47
78
|
}
|
|
48
79
|
}
|
|
49
|
-
function readSkills() {
|
|
50
|
-
return listSkillFolders().flatMap((folder) => {
|
|
51
|
-
const file = join(
|
|
80
|
+
function readSkills(skillsDir) {
|
|
81
|
+
return listSkillFolders(skillsDir).flatMap((folder) => {
|
|
82
|
+
const file = join(skillsDir, folder, "SKILL.md");
|
|
52
83
|
try {
|
|
53
84
|
const raw = readFileSync(file, "utf8");
|
|
54
85
|
const { data, body } = parseFrontmatter(raw);
|
|
@@ -73,10 +104,10 @@ function readAgents() {
|
|
|
73
104
|
return "";
|
|
74
105
|
}
|
|
75
106
|
}
|
|
76
|
-
function writeAgents(text) {
|
|
107
|
+
function writeAgents(text, targetPath) {
|
|
77
108
|
try {
|
|
78
|
-
mkdirSync(
|
|
79
|
-
writeFileSync(
|
|
109
|
+
mkdirSync(join(targetPath, ".."), { recursive: true });
|
|
110
|
+
writeFileSync(targetPath, text, "utf8");
|
|
80
111
|
return true;
|
|
81
112
|
} catch {
|
|
82
113
|
return false;
|
|
@@ -87,50 +118,287 @@ function warn(ctx, message) {
|
|
|
87
118
|
ctx.logger?.warn(message);
|
|
88
119
|
} catch {}
|
|
89
120
|
}
|
|
121
|
+
/** 把上游 instruction-hint.mjs 的提示文本替换为读同目录 agents-instruction.txt。 */
|
|
122
|
+
function patchInstructionHint(source) {
|
|
123
|
+
source = source.replace(/\r\n/g, "\n");
|
|
124
|
+
const original = [
|
|
125
|
+
" const text = [",
|
|
126
|
+
" ...sections,",
|
|
127
|
+
" 'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',",
|
|
128
|
+
" ].join(' ')"
|
|
129
|
+
].join("\n");
|
|
130
|
+
const replacement = [
|
|
131
|
+
" const agentsInstructionText = readFileSync(new URL('./agents-instruction.txt', import.meta.url), 'utf8').trim()",
|
|
132
|
+
" const text = agentsInstructionText.length > 0",
|
|
133
|
+
" ? agentsInstructionText",
|
|
134
|
+
" : [",
|
|
135
|
+
" ...sections,",
|
|
136
|
+
" 'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',",
|
|
137
|
+
" ].join(' ')"
|
|
138
|
+
].join("\n");
|
|
139
|
+
if (!source.includes("import { createEpochPromotion }")) throw new Error("instruction-hint.mjs import marker missing");
|
|
140
|
+
if (!source.includes(original)) throw new Error("instruction-hint.mjs text marker missing");
|
|
141
|
+
return source.replace("import { createEpochPromotion } from './compaction-epoch.mjs'", "import { readFileSync } from 'node:fs'\nimport { createEpochPromotion } from './compaction-epoch.mjs'").replace(original, replacement);
|
|
142
|
+
}
|
|
143
|
+
const MAX_SETTINGS_BRIDGE_BODY = 65536;
|
|
144
|
+
/** 仅允许本机回环请求,镜像官方 settings bridge 的边界。 */
|
|
145
|
+
function isLoopbackRequest(req) {
|
|
146
|
+
const address = req.socket.remoteAddress;
|
|
147
|
+
if (address !== "127.0.0.1" && address !== "::1" && address !== "::ffff:127.0.0.1") return false;
|
|
148
|
+
const host = req.headers.host;
|
|
149
|
+
if (typeof host !== "string") return false;
|
|
150
|
+
try {
|
|
151
|
+
const hostname = new URL("http://" + host).hostname;
|
|
152
|
+
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]";
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function writeBridgeJson(res, status, body) {
|
|
158
|
+
const payload = JSON.stringify(body);
|
|
159
|
+
res.writeHead(status, {
|
|
160
|
+
"content-type": "application/json; charset=utf-8",
|
|
161
|
+
"referrer-policy": "no-referrer"
|
|
162
|
+
});
|
|
163
|
+
res.end(payload);
|
|
164
|
+
}
|
|
165
|
+
async function readBridgeBody(req) {
|
|
166
|
+
const chunks = [];
|
|
167
|
+
let size = 0;
|
|
168
|
+
for await (const chunk of req) {
|
|
169
|
+
const buffer = chunk;
|
|
170
|
+
size += buffer.length;
|
|
171
|
+
if (size > MAX_SETTINGS_BRIDGE_BODY) return void 0;
|
|
172
|
+
chunks.push(buffer);
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
176
|
+
} catch {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** 自建 loopback settings bridge:替代 registerConfigurableProviders,避免模型设置区出现插件条目。 */
|
|
181
|
+
/** dsh-tui 暴露的布尔开关:键名与 settings 路径一致。 */
|
|
182
|
+
const TUI_BOOLEAN_SWITCHES = [
|
|
183
|
+
["writeAgents", "写入常驻规则 AGENTS.md"],
|
|
184
|
+
["writePreset", "生成 prompt-tool 独立 preset"],
|
|
185
|
+
["injectPrompt", "锚定确认后注入 preset.md"],
|
|
186
|
+
["injectAgentsPrompt", "用 AGENTS.md 替换 instruction-hint 提示"],
|
|
187
|
+
["anchorFirstTurn", "开启首轮独立锚定轮"]
|
|
188
|
+
];
|
|
189
|
+
/** 把布尔开关渲染成 dsh-tui 命令输出。 */
|
|
190
|
+
function renderTuiStatus(source) {
|
|
191
|
+
const onOff = (value) => value ? "开" : "关";
|
|
192
|
+
const lines = [
|
|
193
|
+
"提示词工具开关",
|
|
194
|
+
...TUI_BOOLEAN_SWITCHES.map(([key, label]) => {
|
|
195
|
+
const value = source[key];
|
|
196
|
+
return `${key.padEnd(22)}${onOff(typeof value === "boolean" ? value : false)} ${label}`;
|
|
197
|
+
}),
|
|
198
|
+
"技能开关:"
|
|
199
|
+
];
|
|
200
|
+
for (const skill of source.skillCatalog) {
|
|
201
|
+
const value = source.skillSwitches[skill.folder] !== false;
|
|
202
|
+
lines.push(`${("skill " + skill.folder).padEnd(22)}${onOff(value)} ${skill.name || skill.folder}`);
|
|
203
|
+
}
|
|
204
|
+
return lines.join("\n");
|
|
205
|
+
}
|
|
206
|
+
/** 解析 on/off/toggle 三种输入。 */
|
|
207
|
+
function parseTuiBoolean(token, current) {
|
|
208
|
+
if (token === "on") return true;
|
|
209
|
+
if (token === "off") return false;
|
|
210
|
+
if (token === "toggle") return !current;
|
|
211
|
+
}
|
|
212
|
+
/** 通过 DSH 命令注册表暴露 /prompt-tool,Web 与 dsh-tui 都能执行。 */
|
|
213
|
+
function registerTuiCommand(ctx, getSource) {
|
|
214
|
+
ctx.inject(["settings"], (sctx) => {
|
|
215
|
+
sctx.commands.register({
|
|
216
|
+
name: "prompt-tool",
|
|
217
|
+
description: "提示词工具:查看或切换本插件开关",
|
|
218
|
+
input: { hint: "status | on/off/toggle <开关> | skill <目录名> on/off" },
|
|
219
|
+
handler: async (invocation) => {
|
|
220
|
+
const usage = () => ({
|
|
221
|
+
kind: "error",
|
|
222
|
+
text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn>\n /prompt-tool skill <技能目录名> on|off|toggle"
|
|
223
|
+
});
|
|
224
|
+
const tokens = invocation.rawInput.trim().split(/\s+/).filter((token) => token.length > 0);
|
|
225
|
+
const source = getSource();
|
|
226
|
+
if (tokens.length === 0 || tokens[0] === "status") return {
|
|
227
|
+
kind: "success",
|
|
228
|
+
text: renderTuiStatus(source)
|
|
229
|
+
};
|
|
230
|
+
if (tokens[0] === "skill") {
|
|
231
|
+
const folder = tokens[1];
|
|
232
|
+
if (folder === void 0) return usage();
|
|
233
|
+
const current = source.skillSwitches[folder] !== false;
|
|
234
|
+
const next = parseTuiBoolean(tokens[2], current);
|
|
235
|
+
if (next === void 0) return usage();
|
|
236
|
+
await sctx.settings.mutate(NS, [{
|
|
237
|
+
op: "set",
|
|
238
|
+
path: ["skillSwitches", folder],
|
|
239
|
+
value: next
|
|
240
|
+
}]);
|
|
241
|
+
return {
|
|
242
|
+
kind: "success",
|
|
243
|
+
text: `已把技能 ${folder} 设为 ${next ? "开" : "关"}
|
|
244
|
+
|
|
245
|
+
${renderTuiStatus(getSource())}`
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const action = tokens[0];
|
|
249
|
+
const key = tokens[1];
|
|
250
|
+
if (action !== "on" && action !== "off" && action !== "toggle") return usage();
|
|
251
|
+
if (key === void 0 || !TUI_BOOLEAN_SWITCHES.some(([candidate]) => candidate === key)) return usage();
|
|
252
|
+
const currentValue = source[key];
|
|
253
|
+
if (typeof currentValue !== "boolean") return {
|
|
254
|
+
kind: "error",
|
|
255
|
+
text: `${key} 不是布尔开关,不能这样切换`
|
|
256
|
+
};
|
|
257
|
+
const next = parseTuiBoolean(action, currentValue);
|
|
258
|
+
if (next === void 0) return usage();
|
|
259
|
+
await sctx.settings.mutate(NS, [{
|
|
260
|
+
op: "set",
|
|
261
|
+
path: [key],
|
|
262
|
+
value: next
|
|
263
|
+
}]);
|
|
264
|
+
return {
|
|
265
|
+
kind: "success",
|
|
266
|
+
text: `已把 ${key} 设为 ${next ? "开" : "关"}
|
|
267
|
+
|
|
268
|
+
${renderTuiStatus(getSource())}`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function registerSettingsBridge(ctx) {
|
|
275
|
+
ctx.inject(["settings"], (sctx) => {
|
|
276
|
+
sctx.effect(() => {
|
|
277
|
+
const findDescriptor = () => sctx.settings.describe({ redactSecrets: true }).find((entry) => String(entry.ns) === String(NS));
|
|
278
|
+
const guard = (req, res) => {
|
|
279
|
+
if (!isLoopbackRequest(req)) {
|
|
280
|
+
writeBridgeJson(res, 403, {
|
|
281
|
+
ok: false,
|
|
282
|
+
code: "settings-not-exposed",
|
|
283
|
+
message: "loopback requests only"
|
|
284
|
+
});
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
if (req.method !== "POST") {
|
|
288
|
+
writeBridgeJson(res, 405, {
|
|
289
|
+
ok: false,
|
|
290
|
+
code: "settings-not-exposed",
|
|
291
|
+
message: "method not allowed: " + (req.method ?? "")
|
|
292
|
+
});
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
};
|
|
297
|
+
const disposers = [sctx.webServer.register({
|
|
298
|
+
kind: "exact",
|
|
299
|
+
path: "/api/prompt-tool/settings/describe",
|
|
300
|
+
handler: async (req, res) => {
|
|
301
|
+
if (!guard(req, res)) return;
|
|
302
|
+
const descriptor = findDescriptor();
|
|
303
|
+
if (descriptor === void 0) {
|
|
304
|
+
writeBridgeJson(res, 404, {
|
|
305
|
+
ok: false,
|
|
306
|
+
code: "settings-not-exposed",
|
|
307
|
+
message: "prompt-tool settings namespace is not registered"
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
writeBridgeJson(res, 200, {
|
|
312
|
+
ok: true,
|
|
313
|
+
value: descriptor
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}), sctx.webServer.register({
|
|
317
|
+
kind: "exact",
|
|
318
|
+
path: "/api/prompt-tool/settings/mutate",
|
|
319
|
+
handler: async (req, res) => {
|
|
320
|
+
if (!guard(req, res)) return;
|
|
321
|
+
const body = await readBridgeBody(req);
|
|
322
|
+
if (body === null || body === void 0 || typeof body !== "object") {
|
|
323
|
+
writeBridgeJson(res, 400, {
|
|
324
|
+
ok: false,
|
|
325
|
+
code: "settings-rejected",
|
|
326
|
+
message: "unreadable JSON body"
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const record = body;
|
|
331
|
+
if (!Array.isArray(record.ops)) {
|
|
332
|
+
writeBridgeJson(res, 400, {
|
|
333
|
+
ok: false,
|
|
334
|
+
code: "settings-rejected",
|
|
335
|
+
message: "malformed bridge settings request"
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const expectedRevision = typeof record.expectedRevision === "number" ? record.expectedRevision : void 0;
|
|
340
|
+
try {
|
|
341
|
+
await sctx.settings.mutate(NS, record.ops, expectedRevision);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
writeBridgeJson(res, 409, {
|
|
344
|
+
ok: false,
|
|
345
|
+
code: "settings-rejected",
|
|
346
|
+
message: error instanceof Error ? error.message : String(error)
|
|
347
|
+
});
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const descriptor = findDescriptor();
|
|
351
|
+
if (descriptor === void 0) {
|
|
352
|
+
writeBridgeJson(res, 500, {
|
|
353
|
+
ok: false,
|
|
354
|
+
code: "settings-rejected",
|
|
355
|
+
message: "prompt-tool settings namespace was disposed after mutate"
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
writeBridgeJson(res, 200, {
|
|
360
|
+
ok: true,
|
|
361
|
+
value: descriptor
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
})];
|
|
365
|
+
return () => {
|
|
366
|
+
for (const dispose of disposers) dispose();
|
|
367
|
+
};
|
|
368
|
+
}, "prompt-tool: settings bridge");
|
|
369
|
+
});
|
|
370
|
+
}
|
|
90
371
|
function writePreset(prompt, options) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
writeFileSync(
|
|
372
|
+
const presetDir = options.presetDir;
|
|
373
|
+
mkdirSync(presetDir, { recursive: true });
|
|
374
|
+
writeFileSync(join(presetDir, "agent.cordis.yml"), buildCordis(prompt, {
|
|
375
|
+
anchorFirstTurn: options.anchorFirstTurn,
|
|
376
|
+
anchorText: options.anchorText,
|
|
377
|
+
injectPrompt: options.injectPrompt
|
|
378
|
+
}), "utf8");
|
|
379
|
+
const meta = readFileSync(PRESET_TEMPLATE_META, "utf8").replace(/^order:.*$/m, `order: ${options.presetOrder}`);
|
|
380
|
+
writeFileSync(join(presetDir, "preset.yml"), meta, "utf8");
|
|
94
381
|
for (const file of readdirSync(VENDOR_PRESET_DIR)) {
|
|
95
382
|
if (!file.endsWith(".mjs")) continue;
|
|
96
|
-
writeFileSync(join(
|
|
383
|
+
writeFileSync(join(presetDir, file), readFileSync(join(VENDOR_PRESET_DIR, file), "utf8"), "utf8");
|
|
97
384
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
385
|
+
const agentsInstructionPath = join(presetDir, "agents-instruction.txt");
|
|
386
|
+
if (options.agentsInstructionText !== void 0) {
|
|
387
|
+
writeFileSync(agentsInstructionPath, options.agentsInstructionText, "utf8");
|
|
388
|
+
const hintPath = join(presetDir, "instruction-hint.mjs");
|
|
389
|
+
writeFileSync(hintPath, patchInstructionHint(readFileSync(hintPath, "utf8")), "utf8");
|
|
390
|
+
} else rmSync(agentsInstructionPath, { force: true });
|
|
391
|
+
const injectorPath = join(presetDir, "prompt-injector.mjs");
|
|
392
|
+
if (options.injectPrompt) writeFileSync(injectorPath, readFileSync(PRESET_TEMPLATE_INJECTOR, "utf8"), "utf8");
|
|
393
|
+
else rmSync(injectorPath, { force: true });
|
|
394
|
+
const anchorPath = join(presetDir, "turn-anchor.mjs");
|
|
395
|
+
if (options.anchorFirstTurn) writeFileSync(anchorPath, readFileSync(PRESET_TEMPLATE_ANCHOR, "utf8"), "utf8");
|
|
396
|
+
else rmSync(anchorPath, { force: true });
|
|
102
397
|
}
|
|
103
|
-
const Config = z.object({
|
|
104
|
-
text: z.string().default(""),
|
|
105
|
-
agentsText: z.string().default(""),
|
|
106
|
-
writeAgents: z.boolean().default(true),
|
|
107
|
-
writePreset: z.boolean().default(true),
|
|
108
|
-
injectPrompt: z.boolean().default(true),
|
|
109
|
-
skillSwitches: z.dict(z.boolean()).default({}),
|
|
110
|
-
anchorFirstTurn: z.boolean().default(false),
|
|
111
|
-
anchorText: z.string().default("You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'.")
|
|
112
|
-
});
|
|
113
|
-
const PromptSettingsSchema = z.object({
|
|
114
|
-
promptText: z.string().default(""),
|
|
115
|
-
promptPath: z.string().default(""),
|
|
116
|
-
agentsText: z.string().default(""),
|
|
117
|
-
agentsPath: z.string().default(""),
|
|
118
|
-
anchorFirstTurn: z.boolean().default(false),
|
|
119
|
-
anchorText: z.string().default("You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'."),
|
|
120
|
-
injectPrompt: z.boolean().default(true),
|
|
121
|
-
skillSwitches: z.dict(z.boolean()).default({}),
|
|
122
|
-
skillCatalog: z.array(z.object({
|
|
123
|
-
folder: z.string(),
|
|
124
|
-
name: z.string(),
|
|
125
|
-
description: z.string().default("")
|
|
126
|
-
})).default([]),
|
|
127
|
-
writeAgents: z.boolean().default(true),
|
|
128
|
-
writePreset: z.boolean().default(true)
|
|
129
|
-
});
|
|
130
398
|
function apply(ctx, config) {
|
|
131
|
-
let current = config.text || readPromptFile();
|
|
399
|
+
let current = config.text || readPromptFile(config.fallbackText);
|
|
132
400
|
let currentAgents = config.agentsText || readAgents();
|
|
133
|
-
const skillCatalog = readSkills().map((skill) => ({
|
|
401
|
+
const skillCatalog = readSkills(config.skillsDir).map((skill) => ({
|
|
134
402
|
folder: skill.folder,
|
|
135
403
|
name: skill.name,
|
|
136
404
|
description: skill.description
|
|
@@ -138,11 +406,12 @@ function apply(ctx, config) {
|
|
|
138
406
|
let skillSwitches = { ...config.skillSwitches };
|
|
139
407
|
let invalidateSkills;
|
|
140
408
|
ctx.skills.registerProvider((control) => {
|
|
141
|
-
|
|
409
|
+
invalidateSkills = control.invalidate;
|
|
142
410
|
return {
|
|
143
411
|
name: "prompt-tool",
|
|
144
|
-
list: async () => {
|
|
145
|
-
|
|
412
|
+
list: async (options) => {
|
|
413
|
+
if (options.signal?.aborted) return [];
|
|
414
|
+
return readSkills(config.skillsDir).filter((skill) => skillSwitches[skill.folder] !== false).map((skill, index) => ({
|
|
146
415
|
name: skill.name,
|
|
147
416
|
description: skill.description || skill.folder,
|
|
148
417
|
...skill.whenToUse !== void 0 ? { whenToUse: skill.whenToUse } : {},
|
|
@@ -154,15 +423,17 @@ function apply(ctx, config) {
|
|
|
154
423
|
provider: "prompt-tool",
|
|
155
424
|
resourceBase: {
|
|
156
425
|
kind: "directory",
|
|
157
|
-
path: join(
|
|
426
|
+
path: join(config.skillsDir, skill.folder)
|
|
158
427
|
},
|
|
159
|
-
rank:
|
|
428
|
+
rank: config.skillRankBase + index,
|
|
160
429
|
locator: skill.folder,
|
|
430
|
+
path: skill.file,
|
|
161
431
|
...skill.metadata !== void 0 ? { metadata: skill.metadata } : {}
|
|
162
432
|
}));
|
|
163
433
|
},
|
|
164
|
-
get: async (candidate) => {
|
|
165
|
-
|
|
434
|
+
get: async (candidate, options) => {
|
|
435
|
+
if (options.signal?.aborted) return void 0;
|
|
436
|
+
const skill = readSkills(config.skillsDir).find((entry) => entry.folder === candidate.locator || entry.name === candidate.name);
|
|
166
437
|
if (skill === void 0 || skillSwitches[skill.folder] === false) return void 0;
|
|
167
438
|
return {
|
|
168
439
|
name: candidate.name,
|
|
@@ -172,23 +443,18 @@ function apply(ctx, config) {
|
|
|
172
443
|
source: candidate.source,
|
|
173
444
|
provider: candidate.provider,
|
|
174
445
|
resourceBase: candidate.resourceBase,
|
|
175
|
-
|
|
176
|
-
locator: candidate.locator,
|
|
446
|
+
...candidate.path !== void 0 ? { path: candidate.path } : { path: skill.file },
|
|
177
447
|
...candidate.metadata !== void 0 ? { metadata: candidate.metadata } : {},
|
|
178
|
-
content: skill.body
|
|
448
|
+
content: skill.body
|
|
179
449
|
};
|
|
180
450
|
}
|
|
181
451
|
};
|
|
182
452
|
});
|
|
183
|
-
ctx
|
|
184
|
-
provider: "prompt-tool",
|
|
185
|
-
displayName: "提示词工具",
|
|
186
|
-
settingsNs: NS,
|
|
187
|
-
settingsPath: []
|
|
188
|
-
}]);
|
|
453
|
+
registerSettingsBridge(ctx);
|
|
189
454
|
const runtime = {
|
|
190
455
|
writeAgents: config.writeAgents,
|
|
191
456
|
writePreset: config.writePreset,
|
|
457
|
+
injectAgentsPrompt: config.injectAgentsPrompt,
|
|
192
458
|
injectPrompt: config.injectPrompt,
|
|
193
459
|
skillSwitches: { ...config.skillSwitches },
|
|
194
460
|
anchorFirstTurn: config.anchorFirstTurn,
|
|
@@ -199,6 +465,7 @@ function apply(ctx, config) {
|
|
|
199
465
|
promptPath: PRESET_FILE_PATH,
|
|
200
466
|
agentsText: currentAgents,
|
|
201
467
|
agentsPath: AGENTS_FILE_PATH,
|
|
468
|
+
injectAgentsPrompt: runtime.injectAgentsPrompt,
|
|
202
469
|
anchorFirstTurn: runtime.anchorFirstTurn,
|
|
203
470
|
anchorText: runtime.anchorText,
|
|
204
471
|
injectPrompt: runtime.injectPrompt,
|
|
@@ -207,21 +474,23 @@ function apply(ctx, config) {
|
|
|
207
474
|
writeAgents: runtime.writeAgents,
|
|
208
475
|
writePreset: runtime.writePreset
|
|
209
476
|
});
|
|
477
|
+
registerTuiCommand(ctx, () => currentSource());
|
|
210
478
|
let needsInitialApply = true;
|
|
211
479
|
const applyState = () => {
|
|
212
480
|
const next = currentSource();
|
|
213
481
|
const nextRuntime = {
|
|
214
482
|
writeAgents: typeof next.writeAgents === "boolean" ? next.writeAgents : config.writeAgents,
|
|
215
483
|
writePreset: typeof next.writePreset === "boolean" ? next.writePreset : config.writePreset,
|
|
484
|
+
injectAgentsPrompt: typeof next.injectAgentsPrompt === "boolean" ? next.injectAgentsPrompt : config.injectAgentsPrompt,
|
|
216
485
|
injectPrompt: typeof next.injectPrompt === "boolean" ? next.injectPrompt : config.injectPrompt,
|
|
217
486
|
skillSwitches: next.skillSwitches !== void 0 ? next.skillSwitches : config.skillSwitches,
|
|
218
487
|
anchorFirstTurn: typeof next.anchorFirstTurn === "boolean" ? next.anchorFirstTurn : config.anchorFirstTurn,
|
|
219
488
|
anchorText: typeof next.anchorText === "string" && next.anchorText.length > 0 ? next.anchorText : config.anchorText
|
|
220
489
|
};
|
|
221
|
-
const promptChanged =
|
|
222
|
-
const agentsChanged =
|
|
490
|
+
const promptChanged = next.promptText !== current;
|
|
491
|
+
const agentsChanged = next.agentsText !== currentAgents;
|
|
223
492
|
const skillSwitchesChanged = JSON.stringify(runtime.skillSwitches) !== JSON.stringify(nextRuntime.skillSwitches);
|
|
224
|
-
const settingsChanged = runtime.writeAgents !== nextRuntime.writeAgents || runtime.writePreset !== nextRuntime.writePreset || runtime.injectPrompt !== nextRuntime.injectPrompt || skillSwitchesChanged || runtime.anchorFirstTurn !== nextRuntime.anchorFirstTurn || runtime.anchorText !== nextRuntime.anchorText;
|
|
493
|
+
const settingsChanged = runtime.writeAgents !== nextRuntime.writeAgents || runtime.writePreset !== nextRuntime.writePreset || runtime.injectAgentsPrompt !== nextRuntime.injectAgentsPrompt || runtime.injectPrompt !== nextRuntime.injectPrompt || skillSwitchesChanged || runtime.anchorFirstTurn !== nextRuntime.anchorFirstTurn || runtime.anchorText !== nextRuntime.anchorText;
|
|
225
494
|
if (!needsInitialApply && !promptChanged && !agentsChanged && !settingsChanged) return;
|
|
226
495
|
needsInitialApply = false;
|
|
227
496
|
if (promptChanged) {
|
|
@@ -229,7 +498,7 @@ function apply(ctx, config) {
|
|
|
229
498
|
try {
|
|
230
499
|
writeFileSync(PRESET_FILE_URL, next.promptText, "utf8");
|
|
231
500
|
} catch (error) {
|
|
232
|
-
warn(ctx, `prompt-tool: failed to write ${PRESET_FILE_PATH}: ${String(error
|
|
501
|
+
warn(ctx, `prompt-tool: failed to write ${PRESET_FILE_PATH}: ${String(error)}`);
|
|
233
502
|
}
|
|
234
503
|
}
|
|
235
504
|
if (agentsChanged) {
|
|
@@ -237,29 +506,46 @@ function apply(ctx, config) {
|
|
|
237
506
|
try {
|
|
238
507
|
writeFileSync(AGENTS_URL, next.agentsText, "utf8");
|
|
239
508
|
} catch (error) {
|
|
240
|
-
warn(ctx, `prompt-tool: failed to write ${AGENTS_FILE_PATH}: ${String(error
|
|
509
|
+
warn(ctx, `prompt-tool: failed to write ${AGENTS_FILE_PATH}: ${String(error)}`);
|
|
241
510
|
}
|
|
242
511
|
}
|
|
243
512
|
runtime.writeAgents = nextRuntime.writeAgents;
|
|
244
513
|
runtime.writePreset = nextRuntime.writePreset;
|
|
514
|
+
runtime.injectAgentsPrompt = nextRuntime.injectAgentsPrompt;
|
|
245
515
|
runtime.injectPrompt = nextRuntime.injectPrompt;
|
|
246
516
|
runtime.skillSwitches = nextRuntime.skillSwitches;
|
|
247
517
|
runtime.anchorFirstTurn = nextRuntime.anchorFirstTurn;
|
|
248
518
|
runtime.anchorText = nextRuntime.anchorText;
|
|
249
519
|
skillSwitches = runtime.skillSwitches;
|
|
250
520
|
if (skillSwitchesChanged) invalidateSkills?.();
|
|
251
|
-
|
|
252
|
-
if (runtime.
|
|
521
|
+
let residentAgentsWritten = false;
|
|
522
|
+
if (runtime.writeAgents) {
|
|
523
|
+
residentAgentsWritten = writeAgents(currentAgents, config.residentAgentsPath);
|
|
524
|
+
if (!residentAgentsWritten) warn(ctx, `prompt-tool: failed to write resident rules to ${config.residentAgentsPath}`);
|
|
525
|
+
}
|
|
526
|
+
if (runtime.writePreset) writePreset(runtime.injectPrompt && current.length > 0 ? current : "", {
|
|
253
527
|
anchorFirstTurn: runtime.anchorFirstTurn,
|
|
254
528
|
anchorText: runtime.anchorText,
|
|
255
|
-
injectPrompt: runtime.injectPrompt
|
|
529
|
+
injectPrompt: runtime.injectPrompt,
|
|
530
|
+
agentsInstructionText: runtime.injectAgentsPrompt && currentAgents.length > 0 ? currentAgents : void 0,
|
|
531
|
+
presetDir: config.presetDir,
|
|
532
|
+
presetOrder: config.presetOrder
|
|
256
533
|
});
|
|
534
|
+
else try {
|
|
535
|
+
rmSync(config.presetDir, {
|
|
536
|
+
recursive: true,
|
|
537
|
+
force: true
|
|
538
|
+
});
|
|
539
|
+
} catch (error) {
|
|
540
|
+
warn(ctx, "prompt-tool: failed to remove " + config.presetDir + ": " + String(error));
|
|
541
|
+
}
|
|
257
542
|
};
|
|
258
|
-
|
|
543
|
+
const settingsEntry = {
|
|
259
544
|
promptText: current,
|
|
260
545
|
promptPath: PRESET_FILE_PATH,
|
|
261
546
|
agentsText: currentAgents,
|
|
262
547
|
agentsPath: AGENTS_FILE_PATH,
|
|
548
|
+
injectAgentsPrompt: config.injectAgentsPrompt,
|
|
263
549
|
anchorFirstTurn: config.anchorFirstTurn,
|
|
264
550
|
anchorText: config.anchorText,
|
|
265
551
|
injectPrompt: config.injectPrompt,
|
|
@@ -267,7 +553,8 @@ function apply(ctx, config) {
|
|
|
267
553
|
skillCatalog,
|
|
268
554
|
writeAgents: config.writeAgents,
|
|
269
555
|
writePreset: config.writePreset
|
|
270
|
-
}
|
|
556
|
+
};
|
|
557
|
+
installSettingsSection(ctx, NS, PromptSettingsSchema, settingsEntry, {
|
|
271
558
|
setSource: (source) => {
|
|
272
559
|
currentSource = source;
|
|
273
560
|
},
|
|
@@ -275,4 +562,4 @@ function apply(ctx, config) {
|
|
|
275
562
|
});
|
|
276
563
|
}
|
|
277
564
|
//#endregion
|
|
278
|
-
export { Config,
|
|
565
|
+
export { Config, apply, inject, name };
|