dsh-plugin-prompt-tool 0.3.1 → 0.4.1

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,15 +1,239 @@
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 { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
6
- import { join } from "node:path";
6
+ import { basename, join } from "node:path";
7
7
  import { homedir } from "node:os";
8
+ //#region src/web-surface.ts
9
+ /**
10
+ * prompt-tool 自愈层:补 profile manifest 与本插件相关的装配项。
11
+ *
12
+ * 官方 profile 组合规则:`dsh plugin add` 只把声明了 `dsh.bundle` 的
13
+ * **直接依赖**追加进 `dsh.profile.bundles`,而 `@deepseek-ai/dsh-web-app`
14
+ * 是随 dsh 安装自带的 in-box bundle。自定义 profile(如 prompt-tool)
15
+ * 首次初始化只含 `@deepseek-ai/dsh-base`,单独执行
16
+ * `dsh plugin --profile prompt-tool add link:<本仓库>` 后,本插件等待的
17
+ * `webServer` 服务不存在。
18
+ *
19
+ * 因此:
20
+ * 1. 当前 profile 缺 webServer 时,把 web-app 补进当前 profile 的
21
+ * bundles(base 之后、本插件之前),重启一次后生效;
22
+ * 2. 同级 `web` / `dsh-tui` profile 存在时,只把
23
+ * `dsh-plugin-prompt-tool` 写进它们的 dependencies + bundles。
24
+ * node_modules 的物化交给官方 `dsh plugin --profile <name> install`
25
+ * (本质是 pnpm)完成,本插件不手工创建链接。
26
+ * `dsh-tui` 只有在确实装有 `@deepseek-harness-tui/dsh-tui` 时才处理,
27
+ * 否则整体跳过——不写入本插件。dsh-tui **不需要** web-app。
28
+ *
29
+ * 官方 Loader 只会在进程启动时读取 profile 的 bundles 列表,因此本层只
30
+ * 持久化 manifest 并提示重启,不做运行中热挂载。所有写入都是幂等的。
31
+ * 当前 profile 首次补上 web-app 后会自动退出(exit 0),完成初始化。
32
+ *
33
+ * 失败只降级为日志告警,绝不阻断启动。
34
+ */
35
+ const FALLBACK_WEB_APP_BUNDLE = "@deepseek-ai/dsh-web-app";
36
+ const BASE_BUNDLE = "@deepseek-ai/dsh-base";
37
+ const PLUGIN_BUNDLE = "dsh-plugin-prompt-tool";
38
+ const TUI_BUNDLE = "@deepseek-harness-tui/dsh-tui";
39
+ const SIBLING_PROFILES = ["web", "dsh-tui"];
40
+ /** 首次初始化完成后的自动退出延迟,给日志落盘与终端刷新留时间。 */
41
+ const AUTO_EXIT_DELAY_MS = 500;
42
+ /**
43
+ * 从本插件自己的 package.json 读取 `dsh.bundle.requires` 中声明的
44
+ * Web surface bundle(缺失时回退到 @deepseek-ai/dsh-web-app)。
45
+ * 这样“把 web-app 补进本插件 package.json”就是装配事实,而不是散落的魔法字符串。
46
+ */
47
+ function readWebAppBundle() {
48
+ try {
49
+ const manifestPath = new URL("../package.json", import.meta.url);
50
+ const requires = JSON.parse(readFileSync(manifestPath, "utf8")).dsh?.bundle?.requires;
51
+ if (Array.isArray(requires)) {
52
+ const first = requires.find((entry) => typeof entry === "string");
53
+ if (first !== void 0 && first.length > 0) return first;
54
+ }
55
+ } catch {}
56
+ return FALLBACK_WEB_APP_BUNDLE;
57
+ }
58
+ const WEB_APP_BUNDLE = readWebAppBundle();
59
+ /** 从 loader 根 Include 的 baseUrl 推导 profile 目录。 */
60
+ function resolveProfileDir(ctx) {
61
+ const baseUrl = ctx.baseUrl;
62
+ if (typeof baseUrl !== "string" || baseUrl.length === 0) return void 0;
63
+ try {
64
+ return fileURLToPath(new URL(".", baseUrl));
65
+ } catch {
66
+ return;
67
+ }
68
+ }
69
+ function readManifest(packagePath, warn) {
70
+ try {
71
+ return JSON.parse(readFileSync(packagePath, "utf8"));
72
+ } catch (error) {
73
+ warn(`prompt-tool: cannot read profile manifest ${packagePath}: ${error instanceof Error ? error.message : String(error)}`);
74
+ return;
75
+ }
76
+ }
77
+ function writeManifest(packagePath, manifest, warn) {
78
+ try {
79
+ writeFileSync(packagePath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
80
+ return true;
81
+ } catch (error) {
82
+ warn(`prompt-tool: cannot update profile manifest ${packagePath}: ${error instanceof Error ? error.message : String(error)}`);
83
+ return false;
84
+ }
85
+ }
86
+ /** dsh-tui profile 是否真的装着 dsh-tui(依赖声明或 bundle 层都算)。 */
87
+ function hasDshTui(manifest) {
88
+ return manifest.dependencies?.[TUI_BUNDLE] !== void 0 || manifest.dsh?.profile?.bundles?.includes(TUI_BUNDLE) === true;
89
+ }
90
+ /**
91
+ * 把 web-app 补进当前 profile 的 bundles(base 之后、本插件之前)。
92
+ * 只用于当前 prompt-tool profile;dsh-tui 不需要 web-app。
93
+ */
94
+ function ensureCurrentWebAppBundle(manifest) {
95
+ const bundles = manifest.dsh?.profile?.bundles;
96
+ if (!Array.isArray(bundles)) return "invalid";
97
+ if (bundles.includes(WEB_APP_BUNDLE)) return "already";
98
+ const pluginIndex = bundles.indexOf(PLUGIN_BUNDLE);
99
+ const baseIndex = bundles.indexOf(BASE_BUNDLE);
100
+ const insertAt = pluginIndex >= 0 ? pluginIndex : baseIndex >= 0 ? baseIndex + 1 : bundles.length;
101
+ bundles.splice(insertAt, 0, WEB_APP_BUNDLE);
102
+ return "added";
103
+ }
104
+ /**
105
+ * 把本插件补进同级 profile 的 dependencies + bundles。
106
+ * @returns 实际写入的条目描述;返回 undefined 表示跳过/失败。
107
+ */
108
+ function ensurePluginInSiblingProfile(packagePath, manifest, pluginSpec, warn) {
109
+ const bundles = manifest.dsh?.profile?.bundles;
110
+ if (!Array.isArray(bundles)) {
111
+ warn(`prompt-tool: sibling profile manifest has no dsh.profile.bundles: ${packagePath}`);
112
+ return;
113
+ }
114
+ const hasDependency = manifest.dependencies?.[PLUGIN_BUNDLE] !== void 0;
115
+ const added = [];
116
+ if (!hasDependency) {
117
+ if (typeof pluginSpec !== "string" || pluginSpec.length === 0) {
118
+ warn(`prompt-tool: skip sibling profile ${packagePath} — current profile declares no install spec for ${PLUGIN_BUNDLE}`);
119
+ return;
120
+ }
121
+ manifest.dependencies = {
122
+ ...manifest.dependencies,
123
+ [PLUGIN_BUNDLE]: pluginSpec
124
+ };
125
+ added.push(`dependency ${PLUGIN_BUNDLE}=${pluginSpec}`);
126
+ }
127
+ if (!bundles.includes(PLUGIN_BUNDLE)) {
128
+ bundles.push(PLUGIN_BUNDLE);
129
+ added.push(`bundle ${PLUGIN_BUNDLE}`);
130
+ }
131
+ if (added.length === 0) return void 0;
132
+ if (!writeManifest(packagePath, manifest, warn)) return void 0;
133
+ return added.join(", ");
134
+ }
135
+ /** 自愈同级 web / dsh-tui profile;只写 package.json,不创建 node_modules 链接。 */
136
+ function healSiblingProfiles(profileDir, pluginSpec, warn) {
137
+ const profilesDir = join(profileDir, "..");
138
+ for (const profileName of SIBLING_PROFILES) {
139
+ const packagePath = join(profilesDir, profileName, "package.json");
140
+ if (!existsSync(packagePath)) {
141
+ warn(`prompt-tool: skip sibling heal for "${profileName}" — profile package.json not found`);
142
+ continue;
143
+ }
144
+ const manifest = readManifest(packagePath, warn);
145
+ if (manifest === void 0) continue;
146
+ if (profileName === "dsh-tui" && !hasDshTui(manifest)) {
147
+ warn(`prompt-tool: skip sibling heal for "dsh-tui" — ${TUI_BUNDLE} is not installed; dsh-plugin-prompt-tool will not be written into it`);
148
+ continue;
149
+ }
150
+ const changes = ensurePluginInSiblingProfile(packagePath, manifest, pluginSpec, warn);
151
+ if (changes !== void 0) warn(`prompt-tool: healed sibling profile "${profileName}": added ${changes}`);
152
+ }
153
+ }
154
+ /**
155
+ * 每次启动:
156
+ * 1. 同级 web / dsh-tui 补本插件(dsh-tui 未安装则跳过);
157
+ * 2. 当前 profile 缺 webServer 时补 web-app 并提示重启。
158
+ * webServer 已存在时当前 profile 无需处理,但同级自愈仍会执行(幂等)。
159
+ */
160
+ function ensureWebSurface(ctx, warn) {
161
+ const notify = (message) => {
162
+ process.stderr.write(message + "\n");
163
+ warn(message);
164
+ };
165
+ const profileDir = resolveProfileDir(ctx);
166
+ if (profileDir === void 0) {
167
+ notify("prompt-tool: cannot locate profile directory to repair bundles; add @deepseek-ai/dsh-web-app to dsh.profile.bundles manually");
168
+ return;
169
+ }
170
+ const packagePath = join(profileDir, "package.json");
171
+ const manifest = readManifest(packagePath, notify);
172
+ if (manifest === void 0) return;
173
+ healSiblingProfiles(profileDir, typeof manifest.dependencies?.[PLUGIN_BUNDLE] === "string" ? manifest.dependencies[PLUGIN_BUNDLE] : void 0, notify);
174
+ const profileName = basename(profileDir);
175
+ if (profileName === "dsh-tui" || hasDshTui(manifest)) return;
176
+ if (ctx.get("webServer") !== void 0) return;
177
+ const result = ensureCurrentWebAppBundle(manifest);
178
+ if (result === "invalid") {
179
+ notify(`prompt-tool: profile manifest has no dsh.profile.bundles: ${packagePath}`);
180
+ return;
181
+ }
182
+ if (result === "added" && !writeManifest(packagePath, manifest, notify)) return;
183
+ if (result === "added") {
184
+ notify(`prompt-tool: auto-added ${WEB_APP_BUNDLE} to dsh.profile.bundles for profile "${profileName}"; next launch will mount the Web surface (dsh web / dsh-tui / dsh --profile ${profileName})`);
185
+ notify("prompt-tool: initialization complete — exiting so the repaired profile can be launched");
186
+ setTimeout(() => process.exit(0), AUTO_EXIT_DELAY_MS);
187
+ }
188
+ }
189
+ //#endregion
190
+ //#region src/profile-skills.ts
191
+ /**
192
+ * prompt-tool skills 的 profile 副本。
193
+ *
194
+ * 安装命令 `dsh plugin add` 是官方的 pnpm 转发器,插件代码不会在该命令中
195
+ * 执行(本项目也不修改官方源码)。因此复制动作放在插件首次 apply 时:
196
+ * 把包内 `skills/` 增量复制到当前 profile 目录下的 `skills/`
197
+ * (`$DSH_HOME/profiles/<profile>/skills`),并且后续启动优先使用这份副本。
198
+ *
199
+ * 合并规则:
200
+ * - 只补缺失项,绝不覆盖已存在文件——用户在 profile 副本里的修改会保留;
201
+ * - 包内新增的技能目录/文件会在下次启动补进副本;
202
+ * - 副本目录不可写或 profile 定位失败时回退到包内目录,只告警不阻断。
203
+ */
204
+ /** 递归把 src 中缺失的目录/文件补到 dst,已有项保持不动。 */
205
+ function mergeMissing(src, dst) {
206
+ mkdirSync(dst, { recursive: true });
207
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
208
+ const srcEntry = join(src, entry.name);
209
+ const dstEntry = join(dst, entry.name);
210
+ if (!existsSync(dstEntry)) cpSync(srcEntry, dstEntry, { recursive: true });
211
+ else if (entry.isDirectory()) mergeMissing(srcEntry, dstEntry);
212
+ }
213
+ }
214
+ /**
215
+ * 返回本插件应使用的 skills 目录:
216
+ * 优先当前 profile 下的 `skills/` 副本;无法确定 profile 或复制失败时
217
+ * 回退到 sourceDir(默认包内 `skills/`)。
218
+ */
219
+ function resolveProfileSkillsDir(ctx, sourceDir, warn) {
220
+ const profileDir = resolveProfileDir(ctx);
221
+ if (profileDir === void 0) return sourceDir;
222
+ const targetDir = join(profileDir, "skills");
223
+ try {
224
+ if (!existsSync(sourceDir)) return targetDir;
225
+ mergeMissing(sourceDir, targetDir);
226
+ return targetDir;
227
+ } catch (error) {
228
+ warn(`prompt-tool: failed to copy skills into profile directory ${targetDir}: ${error instanceof Error ? error.message : String(error)}`);
229
+ return sourceDir;
230
+ }
231
+ }
232
+ //#endregion
8
233
  //#region src/index.ts
9
234
  const name = "prompt-tool";
10
235
  const inject = [
11
236
  "skills",
12
- "webServer",
13
237
  "commands",
14
238
  "llm",
15
239
  "subagents"
@@ -33,7 +257,16 @@ const PRESET_TEMPLATE_INJECTOR = fileURLToPath(new URL("../preset/prompt-injecto
33
257
  const PRESET_TEMPLATE_ANCHOR = fileURLToPath(new URL("../preset/near-anchor.mjs", import.meta.url));
34
258
  const PRESET_TEMPLATE_ROUTER = fileURLToPath(new URL("../preset/router-first-turn.mjs", import.meta.url));
35
259
  const PRESET_TEMPLATE_GUIDE = fileURLToPath(new URL("../preset/router-guide.mjs", import.meta.url));
36
- const VENDOR_PRESET_DIR = fileURLToPath(new URL("../upstream/dsh-anchored-standard/preset", import.meta.url));
260
+ const LOCAL_PRESET_DIR = fileURLToPath(new URL("../preset", import.meta.url));
261
+ const LOCAL_PRESET_SCRIPTS = [
262
+ "shared.mjs",
263
+ "context-gate.mjs",
264
+ "compaction-epoch.mjs",
265
+ "custom-bash.mjs",
266
+ "instruction-hint.mjs",
267
+ "skill-search.mjs",
268
+ "tool-bootstrap.mjs"
269
+ ];
37
270
  const Config = z.object({
38
271
  text: z.string().default(""),
39
272
  agentsText: z.string().default(""),
@@ -51,6 +284,7 @@ const Config = z.object({
51
284
  subagentFlashProvider: z.string().default("deepseek-official"),
52
285
  subagentFlashModel: z.string().default("deepseek-v4-flash"),
53
286
  bootstrapMaxTokens: z.natural().default(0),
287
+ usePtcMode: z.boolean().default(true),
54
288
  skillsDir: z.string().default(DEFAULT_SKILLS_DIR),
55
289
  skillRankBase: z.natural().default(DEFAULT_SKILL_RANK_BASE),
56
290
  residentAgentsPath: z.string().default(DEFAULT_RESIDENT_AGENTS_PATH),
@@ -71,6 +305,7 @@ const PromptSettingsSchema = z.object({
71
305
  guideCustom: z.boolean().default(false),
72
306
  subagentFlash: z.boolean().default(false),
73
307
  bootstrapMaxTokens: z.natural().default(0),
308
+ usePtcMode: z.boolean().default(true),
74
309
  deepseekAvailable: z.boolean().default(true),
75
310
  injectPrompt: z.boolean().default(true),
76
311
  skillSwitches: z.dict(z.boolean()).default({}),
@@ -79,6 +314,8 @@ const PromptSettingsSchema = z.object({
79
314
  name: z.string(),
80
315
  description: z.string().default("")
81
316
  })).default([]),
317
+ skillsDir: z.string().default(""),
318
+ activeSkillsDir: z.string().default(""),
82
319
  writeAgents: z.boolean().default(true),
83
320
  writePreset: z.boolean().default(true)
84
321
  });
@@ -228,28 +465,6 @@ function warn(ctx, message) {
228
465
  ctx.logger?.warn(message);
229
466
  } catch {}
230
467
  }
231
- /** 把上游 instruction-hint.mjs 的提示文本替换为读同目录 agents-instruction.txt。 */
232
- function patchInstructionHint(source) {
233
- source = source.replace(/\r\n/g, "\n");
234
- const original = [
235
- " const text = [",
236
- " ...sections,",
237
- " 'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',",
238
- " ].join(' ')"
239
- ].join("\n");
240
- const replacement = [
241
- " const agentsInstructionText = readFileSync(new URL('./agents-instruction.txt', import.meta.url), 'utf8').trim()",
242
- " const text = agentsInstructionText.length > 0",
243
- " ? agentsInstructionText",
244
- " : [",
245
- " ...sections,",
246
- " 'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',",
247
- " ].join(' ')"
248
- ].join("\n");
249
- if (!source.includes("import { createEpochPromotion }")) throw new Error("instruction-hint.mjs import marker missing");
250
- if (!source.includes(original)) throw new Error("instruction-hint.mjs text marker missing");
251
- return source.replace("import { createEpochPromotion } from './compaction-epoch.mjs'", "import { readFileSync } from 'node:fs'\nimport { createEpochPromotion } from './compaction-epoch.mjs'").replace(original, replacement);
252
- }
253
468
  const MAX_SETTINGS_BRIDGE_BODY = 65536;
254
469
  /** 仅允许本机回环请求,镜像官方 settings bridge 的边界。 */
255
470
  function isLoopbackRequest(req) {
@@ -296,7 +511,8 @@ const TUI_BOOLEAN_SWITCHES = [
296
511
  ["anchorFirstTurn", "追加任务引导"],
297
512
  ["anchorCustom", "使用自定义引导(首句)"],
298
513
  ["guideCustom", "使用自定义引导(每轮)"],
299
- ["subagentFlash", "子代理固定 Flash 模型"]
514
+ ["subagentFlash", "子代理固定 Flash 模型"],
515
+ ["usePtcMode", "使用 PTC 模式"]
300
516
  ];
301
517
  /** 把布尔开关渲染成 dsh-tui 命令输出。 */
302
518
  function renderTuiStatus(source) {
@@ -311,6 +527,7 @@ function renderTuiStatus(source) {
311
527
  ` anchorText ${source.anchorText.length > 0 ? source.anchorText : "(空 = 按任务自动选择)"}`,
312
528
  ` deepseekAvailable ${source.deepseekAvailable ? "是" : "否(未检测到 DeepSeek 模型,subagentFlash 不可用)"}`,
313
529
  ` bootstrapMaxTokens ${source.bootstrapMaxTokens > 0 ? source.bootstrapMaxTokens : "0(关闭,不设封顶)"}`,
530
+ ` activeSkillsDir ${source.activeSkillsDir || "(未解析到技能目录)"}`,
314
531
  "技能开关:"
315
532
  ];
316
533
  for (const skill of source.skillCatalog) {
@@ -335,7 +552,7 @@ function registerTuiCommand(ctx, getSource, getDeepseekAvailable, getDeepseekSta
335
552
  handler: async (invocation) => {
336
553
  const usage = () => ({
337
554
  kind: "error",
338
- text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn|anchorCustom|guideCustom|subagentFlash>\n /prompt-tool skill <技能目录名> on|off|toggle\n /prompt-tool bootstrapMaxTokens <正整数|0(关闭)>"
555
+ text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn|anchorCustom|guideCustom|subagentFlash|usePtcMode>\n /prompt-tool skill <技能目录名> on|off|toggle\n /prompt-tool bootstrapMaxTokens <正整数|0(关闭)>"
339
556
  });
340
557
  const tokens = invocation.rawInput.trim().split(/\s+/).filter((token) => token.length > 0);
341
558
  const source = getSource();
@@ -418,8 +635,8 @@ ${renderTuiStatus(getSource())}`
418
635
  });
419
636
  }
420
637
  /** 自建 loopback settings bridge:替代 registerConfigurableProviders,避免模型设置区出现插件条目。 */
421
- function registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState) {
422
- ctx.inject(["settings"], (sctx) => {
638
+ function registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState, getSkillsState) {
639
+ ctx.inject(["settings", "webServer"], (sctx) => {
423
640
  sctx.effect(() => {
424
641
  const findDescriptor = () => sctx.settings.describe({ redactSecrets: true }).find((entry) => String(entry.ns) === String(NS));
425
642
  const guard = (req, res) => {
@@ -457,12 +674,15 @@ function registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState) {
457
674
  return;
458
675
  }
459
676
  const detection = getDeepseekState();
677
+ const skillsState = getSkillsState();
460
678
  writeBridgeJson(res, 200, {
461
679
  ok: true,
462
680
  value: descriptor,
463
681
  deepseekAvailable: detection.available,
464
682
  deepseekProviders: detection.providers,
465
- deepseekError: detection.error
683
+ deepseekError: detection.error,
684
+ activeSkillsDir: skillsState.activeSkillsDir,
685
+ skillCatalog: skillsState.skillCatalog
466
686
  });
467
687
  }
468
688
  }),
@@ -656,26 +876,6 @@ function installSubagentFlashRoute(ctx, isEnabled, provider, model) {
656
876
  };
657
877
  });
658
878
  }
659
- /** prompt-tool 补丁:子代理直接全量放行(assembled.tools 本身已是动态白名单)。 */
660
- function patchToolBootstrap(source) {
661
- source = source.replace(/\r\n/g, "\n");
662
- const original = [
663
- " const assembled = await next()",
664
- " try {",
665
- " const status = promotion.status(context.agent)"
666
- ].join("\n");
667
- const replacement = [
668
- " const assembled = await next()",
669
- " try {",
670
- " // prompt-tool 补丁:子代理跳过目录裁剪,直接使用组装结果;",
671
- " // 调用方(如 dsh-mnemon)的工具白名单已先行过滤 assembled.tools,",
672
- " // 因此任意前缀的新插件工具都会自动出现在子代理第一次会话。",
673
- " if ((context.agent?.session?.header?.delegationDepth ?? 0) > 0) return assembled",
674
- " const status = promotion.status(context.agent)"
675
- ].join("\n");
676
- if (!source.includes(original)) throw new Error("tool-bootstrap.mjs assembled marker missing");
677
- return source.replace(original, replacement);
678
- }
679
879
  function writePreset(prompt, options) {
680
880
  const presetDir = options.presetDir;
681
881
  mkdirSync(presetDir, { recursive: true });
@@ -689,21 +889,16 @@ function writePreset(prompt, options) {
689
889
  subagentFlash: options.subagentFlash,
690
890
  subagentFlashProvider: options.subagentFlashProvider,
691
891
  subagentFlashModel: options.subagentFlashModel,
692
- bootstrapMaxTokens: options.bootstrapMaxTokens
892
+ bootstrapMaxTokens: options.bootstrapMaxTokens,
893
+ usePtcMode: options.usePtcMode
693
894
  }), "utf8");
694
895
  const meta = readFileSync(PRESET_TEMPLATE_META, "utf8").replace(/^order:.*$/m, `order: ${options.presetOrder}`);
695
896
  writeFileSync(join(presetDir, "preset.yml"), meta, "utf8");
696
- for (const file of readdirSync(VENDOR_PRESET_DIR)) {
697
- if (!file.endsWith(".mjs")) continue;
698
- const vendorSource = readFileSync(join(VENDOR_PRESET_DIR, file), "utf8");
699
- writeFileSync(join(presetDir, file), file === "tool-bootstrap.mjs" ? patchToolBootstrap(vendorSource) : vendorSource, "utf8");
700
- }
897
+ for (const file of LOCAL_PRESET_SCRIPTS) writeFileSync(join(presetDir, file), readFileSync(join(LOCAL_PRESET_DIR, file), "utf8"), "utf8");
898
+ rmSync(join(presetDir, "dev-tool-search.mjs"), { force: true });
701
899
  const agentsInstructionPath = join(presetDir, "agents-instruction.txt");
702
- if (options.agentsInstructionText !== void 0) {
703
- writeFileSync(agentsInstructionPath, options.agentsInstructionText, "utf8");
704
- const hintPath = join(presetDir, "instruction-hint.mjs");
705
- writeFileSync(hintPath, patchInstructionHint(readFileSync(hintPath, "utf8")), "utf8");
706
- } else rmSync(agentsInstructionPath, { force: true });
900
+ if (options.agentsInstructionText !== void 0) writeFileSync(agentsInstructionPath, options.agentsInstructionText, "utf8");
901
+ else rmSync(agentsInstructionPath, { force: true });
707
902
  const injectorPath = join(presetDir, "prompt-injector.mjs");
708
903
  if (options.injectPrompt) writeFileSync(injectorPath, readFileSync(PRESET_TEMPLATE_INJECTOR, "utf8"), "utf8");
709
904
  else rmSync(injectorPath, { force: true });
@@ -720,11 +915,24 @@ function apply(ctx, config) {
720
915
  const getDeepseekState = () => deepseekState();
721
916
  let current = config.text || readPromptFile(config.fallbackText);
722
917
  let currentAgents = config.agentsText || readAgents();
723
- const skillCatalog = readSkills(config.skillsDir).map((skill) => ({
918
+ const configuredSkillsDir = config.skillsDir !== DEFAULT_SKILLS_DIR && config.skillsDir.length > 0 ? config.skillsDir : "";
919
+ let activeSkillsDir = configuredSkillsDir.length > 0 ? configuredSkillsDir : resolveProfileSkillsDir(ctx, DEFAULT_SKILLS_DIR, (message) => warn(ctx, message));
920
+ let skillCatalog = readSkills(activeSkillsDir).map((skill) => ({
724
921
  folder: skill.folder,
725
922
  name: skill.name,
726
923
  description: skill.description
727
924
  }));
925
+ /** 切换生效技能目录并刷新目录快照(供 describe / TUI 显示)。 */
926
+ const applyActiveSkillsDir = (dir) => {
927
+ activeSkillsDir = dir;
928
+ skillCatalog = readSkills(dir).map((skill) => ({
929
+ folder: skill.folder,
930
+ name: skill.name,
931
+ description: skill.description
932
+ }));
933
+ };
934
+ /** 用户 skillsDir 设置 → 实际生效目录。 */
935
+ const resolveActiveSkillsDir = (userDir) => userDir.length > 0 ? userDir : resolveProfileSkillsDir(ctx, DEFAULT_SKILLS_DIR, (message) => warn(ctx, message));
728
936
  let skillSwitches = { ...config.skillSwitches };
729
937
  let invalidateSkills;
730
938
  ctx.skills.registerProvider((control) => {
@@ -733,7 +941,7 @@ function apply(ctx, config) {
733
941
  name: "prompt-tool",
734
942
  list: async (options) => {
735
943
  if (options.signal?.aborted) return [];
736
- return readSkills(config.skillsDir).filter((skill) => skillSwitches[skill.folder] !== false).map((skill, index) => ({
944
+ return readSkills(activeSkillsDir).filter((skill) => skillSwitches[skill.folder] !== false).map((skill, index) => ({
737
945
  name: skill.name,
738
946
  description: skill.description || skill.folder,
739
947
  ...skill.whenToUse !== void 0 ? { whenToUse: skill.whenToUse } : {},
@@ -745,7 +953,7 @@ function apply(ctx, config) {
745
953
  provider: "prompt-tool",
746
954
  resourceBase: {
747
955
  kind: "directory",
748
- path: join(config.skillsDir, skill.folder)
956
+ path: join(activeSkillsDir, skill.folder)
749
957
  },
750
958
  rank: config.skillRankBase + index,
751
959
  locator: skill.folder,
@@ -755,7 +963,7 @@ function apply(ctx, config) {
755
963
  },
756
964
  get: async (candidate, options) => {
757
965
  if (options.signal?.aborted) return void 0;
758
- const skill = readSkills(config.skillsDir).find((entry) => entry.folder === candidate.locator || entry.name === candidate.name);
966
+ const skill = readSkills(activeSkillsDir).find((entry) => entry.folder === candidate.locator || entry.name === candidate.name);
759
967
  if (skill === void 0 || skillSwitches[skill.folder] === false) return void 0;
760
968
  return {
761
969
  name: candidate.name,
@@ -772,20 +980,28 @@ function apply(ctx, config) {
772
980
  }
773
981
  };
774
982
  });
775
- registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState);
983
+ registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState, () => ({
984
+ activeSkillsDir,
985
+ skillCatalog
986
+ }));
987
+ setImmediate(() => {
988
+ ensureWebSurface(ctx, (message) => warn(ctx, message));
989
+ });
776
990
  const runtime = {
777
991
  writeAgents: config.writeAgents,
778
992
  writePreset: config.writePreset,
779
993
  injectAgentsPrompt: config.injectAgentsPrompt,
780
994
  injectPrompt: config.injectPrompt,
781
995
  skillSwitches: { ...config.skillSwitches },
996
+ skillsDir: configuredSkillsDir,
782
997
  anchorFirstTurn: config.anchorFirstTurn,
783
998
  anchorText: config.anchorText,
784
999
  anchorCustom: config.anchorCustom,
785
1000
  guideText: config.guideText,
786
1001
  guideCustom: config.guideCustom,
787
1002
  subagentFlash: config.subagentFlash && getDeepseekAvailable(),
788
- bootstrapMaxTokens: config.bootstrapMaxTokens
1003
+ bootstrapMaxTokens: config.bootstrapMaxTokens,
1004
+ usePtcMode: config.usePtcMode
789
1005
  };
790
1006
  installSubagentFlashRoute(ctx, () => runtime.subagentFlash, config.subagentFlashProvider, config.subagentFlashModel);
791
1007
  let currentSource = () => ({
@@ -801,10 +1017,13 @@ function apply(ctx, config) {
801
1017
  guideCustom: runtime.guideCustom,
802
1018
  subagentFlash: runtime.subagentFlash,
803
1019
  bootstrapMaxTokens: runtime.bootstrapMaxTokens,
1020
+ usePtcMode: runtime.usePtcMode,
804
1021
  deepseekAvailable: getDeepseekAvailable(),
805
1022
  injectPrompt: runtime.injectPrompt,
806
1023
  skillSwitches: runtime.skillSwitches,
807
1024
  skillCatalog,
1025
+ skillsDir: runtime.skillsDir,
1026
+ activeSkillsDir,
808
1027
  writeAgents: runtime.writeAgents,
809
1028
  writePreset: runtime.writePreset
810
1029
  });
@@ -818,18 +1037,21 @@ function apply(ctx, config) {
818
1037
  injectAgentsPrompt: typeof next.injectAgentsPrompt === "boolean" ? next.injectAgentsPrompt : config.injectAgentsPrompt,
819
1038
  injectPrompt: typeof next.injectPrompt === "boolean" ? next.injectPrompt : config.injectPrompt,
820
1039
  skillSwitches: next.skillSwitches !== void 0 ? next.skillSwitches : config.skillSwitches,
1040
+ skillsDir: typeof next.skillsDir === "string" ? next.skillsDir : configuredSkillsDir,
821
1041
  anchorFirstTurn: typeof next.anchorFirstTurn === "boolean" ? next.anchorFirstTurn : config.anchorFirstTurn,
822
1042
  anchorText: normalizeAnchorText(typeof next.anchorText === "string" ? next.anchorText : config.anchorText),
823
1043
  anchorCustom: typeof next.anchorCustom === "boolean" ? next.anchorCustom : config.anchorCustom,
824
1044
  guideText: typeof next.guideText === "string" ? next.guideText : config.guideText,
825
1045
  guideCustom: typeof next.guideCustom === "boolean" ? next.guideCustom : config.guideCustom,
826
1046
  subagentFlash: (typeof next.subagentFlash === "boolean" ? next.subagentFlash : config.subagentFlash) && getDeepseekAvailable(),
827
- bootstrapMaxTokens: Number.isSafeInteger(next.bootstrapMaxTokens) && next.bootstrapMaxTokens >= 0 ? next.bootstrapMaxTokens : config.bootstrapMaxTokens
1047
+ bootstrapMaxTokens: Number.isSafeInteger(next.bootstrapMaxTokens) && next.bootstrapMaxTokens >= 0 ? next.bootstrapMaxTokens : config.bootstrapMaxTokens,
1048
+ usePtcMode: typeof next.usePtcMode === "boolean" ? next.usePtcMode : config.usePtcMode
828
1049
  };
829
1050
  const promptChanged = next.promptText !== current;
830
1051
  const agentsChanged = next.agentsText !== currentAgents;
831
1052
  const skillSwitchesChanged = JSON.stringify(runtime.skillSwitches) !== JSON.stringify(nextRuntime.skillSwitches);
832
- 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.guideText !== nextRuntime.guideText || runtime.guideCustom !== nextRuntime.guideCustom || runtime.subagentFlash !== nextRuntime.subagentFlash || runtime.bootstrapMaxTokens !== nextRuntime.bootstrapMaxTokens;
1053
+ const skillsDirChanged = runtime.skillsDir !== nextRuntime.skillsDir;
1054
+ const settingsChanged = runtime.writeAgents !== nextRuntime.writeAgents || runtime.writePreset !== nextRuntime.writePreset || runtime.injectAgentsPrompt !== nextRuntime.injectAgentsPrompt || runtime.injectPrompt !== nextRuntime.injectPrompt || skillSwitchesChanged || skillsDirChanged || runtime.anchorFirstTurn !== nextRuntime.anchorFirstTurn || runtime.anchorText !== nextRuntime.anchorText || runtime.anchorCustom !== nextRuntime.anchorCustom || runtime.guideText !== nextRuntime.guideText || runtime.guideCustom !== nextRuntime.guideCustom || runtime.subagentFlash !== nextRuntime.subagentFlash || runtime.bootstrapMaxTokens !== nextRuntime.bootstrapMaxTokens || runtime.usePtcMode !== nextRuntime.usePtcMode;
833
1055
  if (!needsInitialApply && !promptChanged && !agentsChanged && !settingsChanged) return;
834
1056
  needsInitialApply = false;
835
1057
  if (promptChanged) current = next.promptText;
@@ -839,6 +1061,7 @@ function apply(ctx, config) {
839
1061
  runtime.injectAgentsPrompt = nextRuntime.injectAgentsPrompt;
840
1062
  runtime.injectPrompt = nextRuntime.injectPrompt;
841
1063
  runtime.skillSwitches = nextRuntime.skillSwitches;
1064
+ runtime.skillsDir = nextRuntime.skillsDir;
842
1065
  runtime.anchorFirstTurn = nextRuntime.anchorFirstTurn;
843
1066
  runtime.anchorText = nextRuntime.anchorText;
844
1067
  runtime.anchorCustom = nextRuntime.anchorCustom;
@@ -846,8 +1069,12 @@ function apply(ctx, config) {
846
1069
  runtime.guideCustom = nextRuntime.guideCustom;
847
1070
  runtime.subagentFlash = nextRuntime.subagentFlash;
848
1071
  runtime.bootstrapMaxTokens = nextRuntime.bootstrapMaxTokens;
1072
+ runtime.usePtcMode = nextRuntime.usePtcMode;
849
1073
  skillSwitches = runtime.skillSwitches;
850
- if (skillSwitchesChanged) invalidateSkills?.();
1074
+ if (skillsDirChanged) {
1075
+ applyActiveSkillsDir(resolveActiveSkillsDir(runtime.skillsDir));
1076
+ invalidateSkills?.();
1077
+ } else if (skillSwitchesChanged) invalidateSkills?.();
851
1078
  let residentAgentsWritten = false;
852
1079
  if (runtime.writeAgents) {
853
1080
  residentAgentsWritten = writeAgents(currentAgents, config.residentAgentsPath);
@@ -867,6 +1094,7 @@ function apply(ctx, config) {
867
1094
  subagentFlashProvider: config.subagentFlashProvider,
868
1095
  subagentFlashModel: config.subagentFlashModel,
869
1096
  bootstrapMaxTokens: runtime.bootstrapMaxTokens,
1097
+ usePtcMode: runtime.usePtcMode,
870
1098
  agentsInstructionText: runtime.injectAgentsPrompt && currentAgents.length > 0 ? currentAgents : void 0,
871
1099
  presetDir: config.presetDir,
872
1100
  presetOrder: config.presetOrder
@@ -893,10 +1121,13 @@ function apply(ctx, config) {
893
1121
  guideCustom: config.guideCustom,
894
1122
  subagentFlash: config.subagentFlash && getDeepseekAvailable(),
895
1123
  bootstrapMaxTokens: config.bootstrapMaxTokens,
1124
+ usePtcMode: config.usePtcMode,
896
1125
  deepseekAvailable: getDeepseekAvailable(),
897
1126
  injectPrompt: config.injectPrompt,
898
1127
  skillSwitches: { ...config.skillSwitches },
899
1128
  skillCatalog,
1129
+ skillsDir: configuredSkillsDir,
1130
+ activeSkillsDir,
900
1131
  writeAgents: config.writeAgents,
901
1132
  writePreset: config.writePreset
902
1133
  };
@@ -30,7 +30,11 @@ interface BuildCordisOptions {
30
30
  subagentFlashModel?: string;
31
31
  /** 首轮输出封顶(bootstrapMaxTokens);0 或未设置 = 本项目默认无封顶。 */
32
32
  bootstrapMaxTokens?: number;
33
+ /** 使用 PTC 模式:默认 true——晋升后把 wire 切换为 Code Mode(单一 run_code,完整插件工具经生成 SDK 调用);显式 false 时晋升后恢复原生完整工具目录。 */
34
+ usePtcMode?: boolean;
33
35
  }
36
+ /** 给生成的 tool-bootstrap.mjs 打补丁:子代理全量放行 + 可选的 PTC 呈现切换。 */
37
+ declare function patchToolBootstrap(source: string): string;
34
38
  declare function buildCordis(prompt: string, options?: BuildCordisOptions): string;
35
39
  //#endregion
36
- export { BuildCordisOptions, SkillFrontmatter, buildCordis, parseFrontmatter };
40
+ export { BuildCordisOptions, SkillFrontmatter, buildCordis, parseFrontmatter, patchToolBootstrap };