dsh-plugin-prompt-tool 0.1.4 → 0.2.0

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/lib/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { buildCordis, parseFrontmatter } from "./preset-core.mjs";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
4
- import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { join } from "node:path";
7
7
  import { homedir } from "node:os";
@@ -10,7 +10,9 @@ const name = "prompt-tool";
10
10
  const inject = [
11
11
  "skills",
12
12
  "webServer",
13
- "commands"
13
+ "commands",
14
+ "llm",
15
+ "subagents"
14
16
  ];
15
17
  const PRESET_FILE_URL = new URL("../preset.md", import.meta.url);
16
18
  const PRESET_FILE_PATH = fileURLToPath(PRESET_FILE_URL);
@@ -26,8 +28,9 @@ const DEFAULT_PRESET_ORDER = 5;
26
28
  const DEFAULT_SKILL_RANK_BASE = 250;
27
29
  const PRESET_TEMPLATE_META = fileURLToPath(new URL("../preset/preset.yml", import.meta.url));
28
30
  const PRESET_TEMPLATE_INJECTOR = fileURLToPath(new URL("../preset/prompt-injector.mjs", import.meta.url));
29
- const PRESET_TEMPLATE_ANCHOR = fileURLToPath(new URL("../preset/turn-anchor.mjs", import.meta.url));
30
- const VENDOR_PRESET_DIR = fileURLToPath(new URL("../vendor/dsh-anchored-standard/preset", import.meta.url));
31
+ const PRESET_TEMPLATE_ANCHOR = fileURLToPath(new URL("../preset/near-anchor.mjs", import.meta.url));
32
+ const PRESET_TEMPLATE_ROUTER = fileURLToPath(new URL("../preset/router-first-turn.mjs", import.meta.url));
33
+ const VENDOR_PRESET_DIR = fileURLToPath(new URL("../upstream/dsh-anchored-standard/preset", import.meta.url));
31
34
  const Config = z.object({
32
35
  text: z.string().default(""),
33
36
  agentsText: z.string().default(""),
@@ -37,7 +40,12 @@ const Config = z.object({
37
40
  injectPrompt: z.boolean().default(true),
38
41
  skillSwitches: z.dict(z.boolean()).default({}),
39
42
  anchorFirstTurn: z.boolean().default(false),
40
- anchorText: z.string().default("You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'."),
43
+ anchorText: z.string().default(""),
44
+ anchorCustom: z.boolean().default(false),
45
+ subagentFlash: z.boolean().default(false),
46
+ subagentFlashProvider: z.string().default("deepseek-official"),
47
+ subagentFlashModel: z.string().default("deepseek-v4-flash"),
48
+ customBashPath: z.string().default("bash.exe"),
41
49
  skillsDir: z.string().default(DEFAULT_SKILLS_DIR),
42
50
  skillRankBase: z.natural().default(DEFAULT_SKILL_RANK_BASE),
43
51
  residentAgentsPath: z.string().default(DEFAULT_RESIDENT_AGENTS_PATH),
@@ -52,7 +60,10 @@ const PromptSettingsSchema = z.object({
52
60
  agentsPath: z.string().default(""),
53
61
  injectAgentsPrompt: z.boolean().default(false),
54
62
  anchorFirstTurn: z.boolean().default(false),
55
- anchorText: z.string().default("You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'."),
63
+ anchorText: z.string().default(""),
64
+ anchorCustom: z.boolean().default(false),
65
+ subagentFlash: z.boolean().default(false),
66
+ deepseekAvailable: z.boolean().default(true),
56
67
  injectPrompt: z.boolean().default(true),
57
68
  skillSwitches: z.dict(z.boolean()).default({}),
58
69
  skillCatalog: z.array(z.object({
@@ -104,10 +115,101 @@ function readAgents() {
104
115
  return "";
105
116
  }
106
117
  }
118
+ /** 直接读取项目根目录的 preset.md 与 AGENTS.md;宿主不再回写这两个文件。 */
119
+ function readProjectOriginals() {
120
+ return {
121
+ presetText: readPromptFile(""),
122
+ agentsText: readAgents()
123
+ };
124
+ }
125
+ /** 首次安装时,把项目文件内容作为 user 层种子写入 settings.yaml;已有字段不覆盖。 */
126
+ function seedSettingsOnce(ctx, originals) {
127
+ ctx.inject(["settings"], (sctx) => {
128
+ (async () => {
129
+ try {
130
+ const descriptor = sctx.settings.describe({ redactSecrets: true }).find((entry) => String(entry.ns) === String(NS));
131
+ if (descriptor === void 0) return;
132
+ const user = descriptor.user !== null && typeof descriptor.user === "object" ? descriptor.user : {};
133
+ const ops = [];
134
+ if (typeof user.promptText !== "string") ops.push({
135
+ op: "set",
136
+ path: ["promptText"],
137
+ value: originals.presetText
138
+ });
139
+ if (typeof user.agentsText !== "string") ops.push({
140
+ op: "set",
141
+ path: ["agentsText"],
142
+ value: originals.agentsText
143
+ });
144
+ if (ops.length === 0) return;
145
+ await sctx.settings.mutate(NS, ops);
146
+ } catch (error) {
147
+ warn(ctx, `prompt-tool: failed to seed settings from project files: ${error instanceof Error ? error.message : String(error)}`);
148
+ }
149
+ })();
150
+ });
151
+ }
152
+ const RESIDENT_AGENTS_BEGIN = "# === prompt-tool managed block begin ===";
153
+ const RESIDENT_AGENTS_END = "# === prompt-tool managed block end ===";
154
+ /** 从正文中删除成对的受管标记块;标记不成对时保持原样,避免误删。 */
155
+ function stripManagedBlock(source) {
156
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
157
+ const lines = source.replace(/\r\n/g, "\n").split("\n");
158
+ const start = lines.findIndex((line) => line.trim() === RESIDENT_AGENTS_BEGIN);
159
+ if (start < 0) return {
160
+ body: source,
161
+ found: false
162
+ };
163
+ const end = lines.findIndex((line, index) => index > start && line.trim() === RESIDENT_AGENTS_END);
164
+ if (end < 0) return {
165
+ body: source,
166
+ found: false
167
+ };
168
+ lines.splice(start, end - start + 1);
169
+ return {
170
+ body: lines.join(eol),
171
+ found: true
172
+ };
173
+ }
174
+ /** 生成要放到文件头部的受管块。 */
175
+ function buildManagedBlock(text, eol) {
176
+ const content = text.replace(/\r\n/g, "\n").trim();
177
+ return [
178
+ RESIDENT_AGENTS_BEGIN,
179
+ content,
180
+ RESIDENT_AGENTS_END
181
+ ].join(eol);
182
+ }
183
+ /** 把 AGENTS.md 内容作为受管块写到目标文件头部,保留文件其余内容。 */
107
184
  function writeAgents(text, targetPath) {
108
185
  try {
109
186
  mkdirSync(join(targetPath, ".."), { recursive: true });
110
- writeFileSync(targetPath, text, "utf8");
187
+ const existing = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : "";
188
+ const eol = existing.includes("\r\n") ? "\r\n" : "\n";
189
+ const stripped = stripManagedBlock(existing);
190
+ const content = text.trim();
191
+ if (content.length === 0) {
192
+ if (!stripped.found) return true;
193
+ writeFileSync(targetPath, stripped.body, "utf8");
194
+ return true;
195
+ }
196
+ const rest = stripped.body.replace(/^[\r\n]+/, "");
197
+ const managed = buildManagedBlock(content, eol);
198
+ const next = rest.length > 0 ? managed + eol + rest : managed + eol;
199
+ if (next === existing) return true;
200
+ writeFileSync(targetPath, next, "utf8");
201
+ return true;
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
206
+ /** 关闭写入开关后,从目标文件删除本插件的受管块。 */
207
+ function removeResidentAgentsBlock(targetPath) {
208
+ try {
209
+ if (!existsSync(targetPath)) return true;
210
+ const stripped = stripManagedBlock(readFileSync(targetPath, "utf8"));
211
+ if (!stripped.found) return true;
212
+ writeFileSync(targetPath, stripped.body, "utf8");
111
213
  return true;
112
214
  } catch {
113
215
  return false;
@@ -180,10 +282,12 @@ async function readBridgeBody(req) {
180
282
  /** dsh-tui 暴露的布尔开关:键名与 settings 路径一致。 */
181
283
  const TUI_BOOLEAN_SWITCHES = [
182
284
  ["writeAgents", "写入常驻规则 AGENTS.md"],
183
- ["writePreset", "生成 prompt-tool 独立 preset"],
285
+ ["writePreset", "启用锚定预设"],
184
286
  ["injectPrompt", "锚定确认后注入 preset.md"],
185
287
  ["injectAgentsPrompt", "用 AGENTS.md 替换 instruction-hint 提示"],
186
- ["anchorFirstTurn", "开启首轮独立锚定轮"]
288
+ ["anchorFirstTurn", "追加任务引导"],
289
+ ["anchorCustom", "使用自定义引导"],
290
+ ["subagentFlash", "子代理固定 Flash 模型"]
187
291
  ];
188
292
  /** 把布尔开关渲染成 dsh-tui 命令输出。 */
189
293
  function renderTuiStatus(source) {
@@ -194,6 +298,9 @@ function renderTuiStatus(source) {
194
298
  const value = source[key];
195
299
  return `${key.padEnd(22)}${onOff(typeof value === "boolean" ? value : false)} ${label}`;
196
300
  }),
301
+ "锚点文本:",
302
+ ` anchorText ${source.anchorText.length > 0 ? source.anchorText : "(空 = 按任务自动选择)"}`,
303
+ ` deepseekAvailable ${source.deepseekAvailable ? "是" : "否(未检测到 DeepSeek 模型,subagentFlash 不可用)"}`,
197
304
  "技能开关:"
198
305
  ];
199
306
  for (const skill of source.skillCatalog) {
@@ -209,7 +316,7 @@ function parseTuiBoolean(token, current) {
209
316
  if (token === "toggle") return !current;
210
317
  }
211
318
  /** 通过 DSH 命令注册表暴露 /prompt-tool,Web 与 dsh-tui 都能执行。 */
212
- function registerTuiCommand(ctx, getSource) {
319
+ function registerTuiCommand(ctx, getSource, getDeepseekAvailable, getDeepseekState) {
213
320
  ctx.inject(["settings"], (sctx) => {
214
321
  return sctx.commands.register({
215
322
  name: "prompt-tool",
@@ -218,14 +325,18 @@ function registerTuiCommand(ctx, getSource) {
218
325
  handler: async (invocation) => {
219
326
  const usage = () => ({
220
327
  kind: "error",
221
- text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn>\n /prompt-tool skill <技能目录名> on|off|toggle"
328
+ text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn|anchorCustom|subagentFlash>\n /prompt-tool skill <技能目录名> on|off|toggle"
222
329
  });
223
330
  const tokens = invocation.rawInput.trim().split(/\s+/).filter((token) => token.length > 0);
224
331
  const source = getSource();
225
- if (tokens.length === 0 || tokens[0] === "status") return {
226
- kind: "success",
227
- text: renderTuiStatus(source)
228
- };
332
+ if (tokens.length === 0 || tokens[0] === "status") {
333
+ const detection = getDeepseekState();
334
+ const deepseekLine = detection.available ? `检测到的 DeepSeek 模型路由: ${detection.providers.join(", ") || "(无)"}` : `未检测到 DeepSeek 模型路由。providers=[${detection.providers.join(", ") || "空"}] error=${detection.error ?? "无"}`;
335
+ return {
336
+ kind: "success",
337
+ text: renderTuiStatus(source) + "\n" + deepseekLine
338
+ };
339
+ }
229
340
  if (tokens[0] === "skill") {
230
341
  const folder = tokens[1];
231
342
  if (folder === void 0) return usage();
@@ -248,6 +359,13 @@ ${renderTuiStatus(getSource())}`
248
359
  const key = tokens[1];
249
360
  if (action !== "on" && action !== "off" && action !== "toggle") return usage();
250
361
  if (key === void 0 || !TUI_BOOLEAN_SWITCHES.some(([candidate]) => candidate === key)) return usage();
362
+ if (key === "subagentFlash" && !getDeepseekAvailable()) {
363
+ const detection = getDeepseekState();
364
+ return {
365
+ kind: "error",
366
+ text: `未检测到 DeepSeek 模型路由,subagentFlash 开关不可用。providers=[${detection.providers.join(", ") || "空"}] error=${detection.error ?? "无"}`
367
+ };
368
+ }
251
369
  const currentValue = source[key];
252
370
  if (typeof currentValue !== "boolean") return {
253
371
  kind: "error",
@@ -271,7 +389,7 @@ ${renderTuiStatus(getSource())}`
271
389
  });
272
390
  }
273
391
  /** 自建 loopback settings bridge:替代 registerConfigurableProviders,避免模型设置区出现插件条目。 */
274
- function registerSettingsBridge(ctx) {
392
+ function registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState) {
275
393
  ctx.inject(["settings"], (sctx) => {
276
394
  sctx.effect(() => {
277
395
  const findDescriptor = () => sctx.settings.describe({ redactSecrets: true }).find((entry) => String(entry.ns) === String(NS));
@@ -294,93 +412,260 @@ function registerSettingsBridge(ctx) {
294
412
  }
295
413
  return true;
296
414
  };
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"
415
+ const disposers = [
416
+ sctx.webServer.register({
417
+ kind: "exact",
418
+ path: "/api/prompt-tool/settings/describe",
419
+ handler: async (req, res) => {
420
+ if (!guard(req, res)) return;
421
+ const descriptor = findDescriptor();
422
+ if (descriptor === void 0) {
423
+ writeBridgeJson(res, 404, {
424
+ ok: false,
425
+ code: "settings-not-exposed",
426
+ message: "prompt-tool settings namespace is not registered"
427
+ });
428
+ return;
429
+ }
430
+ const detection = getDeepseekState();
431
+ writeBridgeJson(res, 200, {
432
+ ok: true,
433
+ value: descriptor,
434
+ deepseekAvailable: detection.available,
435
+ deepseekProviders: detection.providers,
436
+ deepseekError: detection.error
308
437
  });
309
- return;
310
438
  }
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"
439
+ }),
440
+ sctx.webServer.register({
441
+ kind: "exact",
442
+ path: "/api/prompt-tool/settings/mutate",
443
+ handler: async (req, res) => {
444
+ if (!guard(req, res)) return;
445
+ const body = await readBridgeBody(req);
446
+ if (body === null || body === void 0 || typeof body !== "object") {
447
+ writeBridgeJson(res, 400, {
448
+ ok: false,
449
+ code: "settings-rejected",
450
+ message: "unreadable JSON body"
451
+ });
452
+ return;
453
+ }
454
+ const record = body;
455
+ if (!Array.isArray(record.ops)) {
456
+ writeBridgeJson(res, 400, {
457
+ ok: false,
458
+ code: "settings-rejected",
459
+ message: "malformed bridge settings request"
460
+ });
461
+ return;
462
+ }
463
+ const expectedRevision = typeof record.expectedRevision === "number" ? record.expectedRevision : void 0;
464
+ try {
465
+ await sctx.settings.mutate(NS, record.ops, expectedRevision);
466
+ } catch (error) {
467
+ writeBridgeJson(res, 409, {
468
+ ok: false,
469
+ code: "settings-rejected",
470
+ message: error instanceof Error ? error.message : String(error)
471
+ });
472
+ return;
473
+ }
474
+ const descriptor = findDescriptor();
475
+ if (descriptor === void 0) {
476
+ writeBridgeJson(res, 500, {
477
+ ok: false,
478
+ code: "settings-rejected",
479
+ message: "prompt-tool settings namespace was disposed after mutate"
480
+ });
481
+ return;
482
+ }
483
+ writeBridgeJson(res, 200, {
484
+ ok: true,
485
+ value: descriptor
327
486
  });
328
- return;
329
487
  }
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"
488
+ }),
489
+ sctx.webServer.register({
490
+ kind: "exact",
491
+ path: "/api/prompt-tool/settings/restore-originals",
492
+ handler: async (req, res) => {
493
+ if (!guard(req, res)) return;
494
+ const body = await readBridgeBody(req);
495
+ if (body === null || body === void 0 || typeof body !== "object") {
496
+ writeBridgeJson(res, 400, {
497
+ ok: false,
498
+ code: "settings-rejected",
499
+ message: "unreadable JSON body"
500
+ });
501
+ return;
502
+ }
503
+ const record = body;
504
+ const scope = record.scope === "preset" || record.scope === "agents" || record.scope === "all" ? record.scope : "all";
505
+ const originals = readProjectOriginals();
506
+ const ops = [];
507
+ if (scope === "preset" || scope === "all") ops.push({
508
+ op: "set",
509
+ path: ["promptText"],
510
+ value: originals.presetText
336
511
  });
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)
512
+ if (scope === "agents" || scope === "all") ops.push({
513
+ op: "set",
514
+ path: ["agentsText"],
515
+ value: originals.agentsText
347
516
  });
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"
517
+ const expectedRevision = typeof record.expectedRevision === "number" ? record.expectedRevision : void 0;
518
+ try {
519
+ await sctx.settings.mutate(NS, ops, expectedRevision);
520
+ } catch (error) {
521
+ writeBridgeJson(res, 409, {
522
+ ok: false,
523
+ code: "settings-rejected",
524
+ message: error instanceof Error ? error.message : String(error)
525
+ });
526
+ return;
527
+ }
528
+ const descriptor = findDescriptor();
529
+ if (descriptor === void 0) {
530
+ writeBridgeJson(res, 500, {
531
+ ok: false,
532
+ code: "settings-rejected",
533
+ message: "prompt-tool settings namespace was disposed after restore"
534
+ });
535
+ return;
536
+ }
537
+ writeBridgeJson(res, 200, {
538
+ ok: true,
539
+ value: descriptor
356
540
  });
357
- return;
358
541
  }
359
- writeBridgeJson(res, 200, {
360
- ok: true,
361
- value: descriptor
362
- });
363
- }
364
- })];
542
+ })
543
+ ];
365
544
  return () => {
366
545
  for (const dispose of disposers) dispose();
367
546
  };
368
547
  }, "prompt-tool: settings bridge");
369
548
  });
370
549
  }
550
+ /** 旧版“每块强制 we need”默认锚句;已存 settings 时归一化为自动模式。 */
551
+ const LEGACY_ANCHOR_TEXT = [
552
+ "You are a helpful software assistant.",
553
+ "",
554
+ "Begin every reasoning block with 'We need'."
555
+ ].join("\n");
556
+ /** 旧默认值归一化为空(自动);用户自定义文本原样保留。 */
557
+ function normalizeAnchorText(text) {
558
+ const value = typeof text === "string" ? text : "";
559
+ return value.trim() === LEGACY_ANCHOR_TEXT.trim() ? "" : value;
560
+ }
561
+ /** 检测 DeepSeek 模型:live provider + 可配置 provider 目录双通道匹配。 */
562
+ function detectDeepseek(ctx) {
563
+ const empty = {
564
+ available: false,
565
+ providers: []
566
+ };
567
+ try {
568
+ const llm = ctx.get("llm");
569
+ if (llm === void 0) return {
570
+ ...empty,
571
+ error: "ctx.get(\"llm\") 返回 undefined"
572
+ };
573
+ const live = llm.listProviders?.() ?? [];
574
+ const configured = llm.listConfigurableProviders?.() ?? [];
575
+ const names = /* @__PURE__ */ new Set();
576
+ const matches = (id, name) => /deepseek/i.test(id ?? "") || /deepseek/i.test(name ?? "");
577
+ for (const provider of live) {
578
+ const id = typeof provider.id === "string" ? provider.id : String(provider.id ?? "");
579
+ names.add(id || provider.name || "(unnamed)");
580
+ if (matches(provider.id, provider.name)) return {
581
+ available: true,
582
+ providers: [...names]
583
+ };
584
+ }
585
+ for (const provider of configured) {
586
+ const id = typeof provider.provider === "string" ? provider.provider : "";
587
+ if (id.length > 0) names.add(id);
588
+ if (matches(provider.provider, provider.displayName)) return {
589
+ available: true,
590
+ providers: [...names]
591
+ };
592
+ }
593
+ return {
594
+ available: false,
595
+ providers: [...names],
596
+ ...live.length === 0 && configured.length === 0 ? { error: "llm 服务未返回任何 provider" } : {}
597
+ };
598
+ } catch (error) {
599
+ return {
600
+ ...empty,
601
+ error: error instanceof Error ? error.message : String(error)
602
+ };
603
+ }
604
+ }
605
+ /** 给宿主直派的子代理补 Flash 路由;调用方显式 provider/model 优先,不覆盖 persona 与工具白名单。 */
606
+ function installSubagentFlashRoute(ctx, isEnabled, provider, model) {
607
+ ctx.inject(["subagents"], (sctx) => {
608
+ const service = sctx.get("subagents");
609
+ if (service === void 0 || typeof service.start !== "function") return;
610
+ const original = service.start;
611
+ const wrapped = (name, request) => {
612
+ if (!isEnabled() || request === null || typeof request !== "object") return original.call(service, name, request);
613
+ const agentOptions = request.agentOptions !== null && typeof request.agentOptions === "object" ? request.agentOptions : {};
614
+ if (agentOptions.provider === void 0 && agentOptions.model === void 0) return original.call(service, name, {
615
+ ...request,
616
+ agentOptions: {
617
+ ...agentOptions,
618
+ provider,
619
+ model
620
+ }
621
+ });
622
+ return original.call(service, name, request);
623
+ };
624
+ service.start = wrapped;
625
+ return () => {
626
+ if (service.start === wrapped) service.start = original;
627
+ };
628
+ });
629
+ }
630
+ /** prompt-tool 补丁:子代理直接全量放行(assembled.tools 本身已是动态白名单)。 */
631
+ function patchToolBootstrap(source) {
632
+ source = source.replace(/\r\n/g, "\n");
633
+ const original = [
634
+ " const assembled = await next()",
635
+ " try {",
636
+ " const status = promotion.status(context.agent)"
637
+ ].join("\n");
638
+ const replacement = [
639
+ " const assembled = await next()",
640
+ " try {",
641
+ " // prompt-tool 补丁:子代理跳过目录裁剪,直接使用组装结果;",
642
+ " // 调用方(如 dsh-mnemon)的工具白名单已先行过滤 assembled.tools,",
643
+ " // 因此任意前缀的新插件工具都会自动出现在子代理第一次会话。",
644
+ " if ((context.agent?.session?.header?.delegationDepth ?? 0) > 0) return assembled",
645
+ " const status = promotion.status(context.agent)"
646
+ ].join("\n");
647
+ if (!source.includes(original)) throw new Error("tool-bootstrap.mjs assembled marker missing");
648
+ return source.replace(original, replacement);
649
+ }
371
650
  function writePreset(prompt, options) {
372
651
  const presetDir = options.presetDir;
373
652
  mkdirSync(presetDir, { recursive: true });
374
653
  writeFileSync(join(presetDir, "agent.cordis.yml"), buildCordis(prompt, {
375
654
  anchorFirstTurn: options.anchorFirstTurn,
376
655
  anchorText: options.anchorText,
377
- injectPrompt: options.injectPrompt
656
+ anchorCustom: options.anchorCustom,
657
+ injectPrompt: options.injectPrompt,
658
+ subagentFlash: options.subagentFlash,
659
+ subagentFlashProvider: options.subagentFlashProvider,
660
+ subagentFlashModel: options.subagentFlashModel,
661
+ bashPath: options.customBashPath
378
662
  }), "utf8");
379
663
  const meta = readFileSync(PRESET_TEMPLATE_META, "utf8").replace(/^order:.*$/m, `order: ${options.presetOrder}`);
380
664
  writeFileSync(join(presetDir, "preset.yml"), meta, "utf8");
381
665
  for (const file of readdirSync(VENDOR_PRESET_DIR)) {
382
666
  if (!file.endsWith(".mjs")) continue;
383
- writeFileSync(join(presetDir, file), readFileSync(join(VENDOR_PRESET_DIR, file), "utf8"), "utf8");
667
+ const vendorSource = readFileSync(join(VENDOR_PRESET_DIR, file), "utf8");
668
+ writeFileSync(join(presetDir, file), file === "tool-bootstrap.mjs" ? patchToolBootstrap(vendorSource) : vendorSource, "utf8");
384
669
  }
385
670
  const agentsInstructionPath = join(presetDir, "agents-instruction.txt");
386
671
  if (options.agentsInstructionText !== void 0) {
@@ -391,11 +676,16 @@ function writePreset(prompt, options) {
391
676
  const injectorPath = join(presetDir, "prompt-injector.mjs");
392
677
  if (options.injectPrompt) writeFileSync(injectorPath, readFileSync(PRESET_TEMPLATE_INJECTOR, "utf8"), "utf8");
393
678
  else rmSync(injectorPath, { force: true });
394
- const anchorPath = join(presetDir, "turn-anchor.mjs");
679
+ writeFileSync(join(presetDir, "router-first-turn.mjs"), readFileSync(PRESET_TEMPLATE_ROUTER, "utf8"), "utf8");
680
+ rmSync(join(presetDir, "turn-anchor.mjs"), { force: true });
681
+ const anchorPath = join(presetDir, "near-anchor.mjs");
395
682
  if (options.anchorFirstTurn) writeFileSync(anchorPath, readFileSync(PRESET_TEMPLATE_ANCHOR, "utf8"), "utf8");
396
683
  else rmSync(anchorPath, { force: true });
397
684
  }
398
685
  function apply(ctx, config) {
686
+ const deepseekState = () => detectDeepseek(ctx);
687
+ const getDeepseekAvailable = () => deepseekState().available;
688
+ const getDeepseekState = () => deepseekState();
399
689
  let current = config.text || readPromptFile(config.fallbackText);
400
690
  let currentAgents = config.agentsText || readAgents();
401
691
  const skillCatalog = readSkills(config.skillsDir).map((skill) => ({
@@ -450,7 +740,7 @@ function apply(ctx, config) {
450
740
  }
451
741
  };
452
742
  });
453
- registerSettingsBridge(ctx);
743
+ registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState);
454
744
  const runtime = {
455
745
  writeAgents: config.writeAgents,
456
746
  writePreset: config.writePreset,
@@ -458,8 +748,11 @@ function apply(ctx, config) {
458
748
  injectPrompt: config.injectPrompt,
459
749
  skillSwitches: { ...config.skillSwitches },
460
750
  anchorFirstTurn: config.anchorFirstTurn,
461
- anchorText: config.anchorText
751
+ anchorText: config.anchorText,
752
+ anchorCustom: config.anchorCustom,
753
+ subagentFlash: config.subagentFlash && getDeepseekAvailable()
462
754
  };
755
+ installSubagentFlashRoute(ctx, () => runtime.subagentFlash, config.subagentFlashProvider, config.subagentFlashModel);
463
756
  let currentSource = () => ({
464
757
  promptText: current,
465
758
  promptPath: PRESET_FILE_PATH,
@@ -467,14 +760,17 @@ function apply(ctx, config) {
467
760
  agentsPath: AGENTS_FILE_PATH,
468
761
  injectAgentsPrompt: runtime.injectAgentsPrompt,
469
762
  anchorFirstTurn: runtime.anchorFirstTurn,
470
- anchorText: runtime.anchorText,
763
+ anchorText: normalizeAnchorText(runtime.anchorText),
764
+ anchorCustom: runtime.anchorCustom,
765
+ subagentFlash: runtime.subagentFlash,
766
+ deepseekAvailable: getDeepseekAvailable(),
471
767
  injectPrompt: runtime.injectPrompt,
472
768
  skillSwitches: runtime.skillSwitches,
473
769
  skillCatalog,
474
770
  writeAgents: runtime.writeAgents,
475
771
  writePreset: runtime.writePreset
476
772
  });
477
- registerTuiCommand(ctx, () => currentSource());
773
+ registerTuiCommand(ctx, () => currentSource(), getDeepseekAvailable, getDeepseekState);
478
774
  let needsInitialApply = true;
479
775
  const applyState = () => {
480
776
  const next = currentSource();
@@ -485,30 +781,18 @@ function apply(ctx, config) {
485
781
  injectPrompt: typeof next.injectPrompt === "boolean" ? next.injectPrompt : config.injectPrompt,
486
782
  skillSwitches: next.skillSwitches !== void 0 ? next.skillSwitches : config.skillSwitches,
487
783
  anchorFirstTurn: typeof next.anchorFirstTurn === "boolean" ? next.anchorFirstTurn : config.anchorFirstTurn,
488
- anchorText: typeof next.anchorText === "string" && next.anchorText.length > 0 ? next.anchorText : config.anchorText
784
+ anchorText: normalizeAnchorText(typeof next.anchorText === "string" ? next.anchorText : config.anchorText),
785
+ anchorCustom: typeof next.anchorCustom === "boolean" ? next.anchorCustom : config.anchorCustom,
786
+ subagentFlash: (typeof next.subagentFlash === "boolean" ? next.subagentFlash : config.subagentFlash) && getDeepseekAvailable()
489
787
  };
490
788
  const promptChanged = next.promptText !== current;
491
789
  const agentsChanged = next.agentsText !== currentAgents;
492
790
  const skillSwitchesChanged = JSON.stringify(runtime.skillSwitches) !== JSON.stringify(nextRuntime.skillSwitches);
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;
791
+ 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 || runtime.anchorCustom !== nextRuntime.anchorCustom || runtime.subagentFlash !== nextRuntime.subagentFlash;
494
792
  if (!needsInitialApply && !promptChanged && !agentsChanged && !settingsChanged) return;
495
793
  needsInitialApply = false;
496
- if (promptChanged) {
497
- current = next.promptText;
498
- try {
499
- writeFileSync(PRESET_FILE_URL, next.promptText, "utf8");
500
- } catch (error) {
501
- warn(ctx, `prompt-tool: failed to write ${PRESET_FILE_PATH}: ${String(error)}`);
502
- }
503
- }
504
- if (agentsChanged) {
505
- currentAgents = next.agentsText;
506
- try {
507
- writeFileSync(AGENTS_URL, next.agentsText, "utf8");
508
- } catch (error) {
509
- warn(ctx, `prompt-tool: failed to write ${AGENTS_FILE_PATH}: ${String(error)}`);
510
- }
511
- }
794
+ if (promptChanged) current = next.promptText;
795
+ if (agentsChanged) currentAgents = next.agentsText;
512
796
  runtime.writeAgents = nextRuntime.writeAgents;
513
797
  runtime.writePreset = nextRuntime.writePreset;
514
798
  runtime.injectAgentsPrompt = nextRuntime.injectAgentsPrompt;
@@ -516,17 +800,27 @@ function apply(ctx, config) {
516
800
  runtime.skillSwitches = nextRuntime.skillSwitches;
517
801
  runtime.anchorFirstTurn = nextRuntime.anchorFirstTurn;
518
802
  runtime.anchorText = nextRuntime.anchorText;
803
+ runtime.anchorCustom = nextRuntime.anchorCustom;
804
+ runtime.subagentFlash = nextRuntime.subagentFlash;
519
805
  skillSwitches = runtime.skillSwitches;
520
806
  if (skillSwitchesChanged) invalidateSkills?.();
521
807
  let residentAgentsWritten = false;
522
808
  if (runtime.writeAgents) {
523
809
  residentAgentsWritten = writeAgents(currentAgents, config.residentAgentsPath);
524
810
  if (!residentAgentsWritten) warn(ctx, `prompt-tool: failed to write resident rules to ${config.residentAgentsPath}`);
811
+ } else {
812
+ residentAgentsWritten = removeResidentAgentsBlock(config.residentAgentsPath);
813
+ if (!residentAgentsWritten) warn(ctx, `prompt-tool: failed to remove resident rules block from ${config.residentAgentsPath}`);
525
814
  }
526
815
  if (runtime.writePreset) writePreset(runtime.injectPrompt && current.length > 0 ? current : "", {
527
816
  anchorFirstTurn: runtime.anchorFirstTurn,
528
817
  anchorText: runtime.anchorText,
818
+ anchorCustom: runtime.anchorCustom,
529
819
  injectPrompt: runtime.injectPrompt,
820
+ subagentFlash: runtime.subagentFlash,
821
+ subagentFlashProvider: config.subagentFlashProvider,
822
+ subagentFlashModel: config.subagentFlashModel,
823
+ customBashPath: config.customBashPath,
530
824
  agentsInstructionText: runtime.injectAgentsPrompt && currentAgents.length > 0 ? currentAgents : void 0,
531
825
  presetDir: config.presetDir,
532
826
  presetOrder: config.presetOrder
@@ -548,6 +842,9 @@ function apply(ctx, config) {
548
842
  injectAgentsPrompt: config.injectAgentsPrompt,
549
843
  anchorFirstTurn: config.anchorFirstTurn,
550
844
  anchorText: config.anchorText,
845
+ anchorCustom: config.anchorCustom,
846
+ subagentFlash: config.subagentFlash && getDeepseekAvailable(),
847
+ deepseekAvailable: getDeepseekAvailable(),
551
848
  injectPrompt: config.injectPrompt,
552
849
  skillSwitches: { ...config.skillSwitches },
553
850
  skillCatalog,
@@ -560,6 +857,7 @@ function apply(ctx, config) {
560
857
  },
561
858
  onChange: applyState
562
859
  });
860
+ seedSettingsOnce(ctx, readProjectOriginals());
563
861
  }
564
862
  //#endregion
565
863
  export { Config, apply, inject, name };