dsh-paoding 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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/README_en.md +95 -0
  4. package/bin/dsh-paoding.mjs +12 -0
  5. package/cordis.patch.yml +6 -0
  6. package/docs/architecture.md +292 -0
  7. package/docs/architecture_en.md +363 -0
  8. package/docs/configuration.md +551 -0
  9. package/docs/configuration_en.md +560 -0
  10. package/docs/images/1.png +0 -0
  11. package/docs/images/2.png +0 -0
  12. package/docs/images/3.png +0 -0
  13. package/docs/images/4.png +0 -0
  14. package/docs/images/5.png +0 -0
  15. package/docs/images/6.png +0 -0
  16. package/docs/installation.md +364 -0
  17. package/docs/installation_en.md +367 -0
  18. package/docs/orchestration.md +254 -0
  19. package/docs/orchestration_en.md +255 -0
  20. package/install.sh +31 -0
  21. package/package.json +29 -0
  22. package/plugins/paoding-config-ui/api-core.mjs +100 -0
  23. package/plugins/paoding-config-ui/index.mjs +327 -0
  24. package/plugins/paoding-config-ui/lib/base.css +615 -0
  25. package/plugins/paoding-config-ui/lib/client.js +1154 -0
  26. package/plugins/paoding-config-ui/package.json +23 -0
  27. package/plugins/paoding-config-ui/runtime-inventory.mjs +212 -0
  28. package/presets/orchestrator/agent.cordis.yml +323 -0
  29. package/presets/orchestrator/preset.yml +6 -0
  30. package/presets/orchestrator/restrict.mjs +191 -0
  31. package/tools/install.mjs +24 -0
  32. package/tools/lib/alloc.mjs +138 -0
  33. package/tools/lib/cli.mjs +522 -0
  34. package/tools/lib/compose.mjs +647 -0
  35. package/tools/lib/config.mjs +297 -0
  36. package/tools/lib/host.mjs +288 -0
  37. package/tools/lib/skills.mjs +146 -0
  38. package/tools/lib/spans.mjs +188 -0
  39. package/tools/lib/state.mjs +328 -0
  40. package/tools/lib/util.mjs +26 -0
  41. package/tools/lib/wizard.mjs +516 -0
  42. package/tools/lib/yaml.mjs +266 -0
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 底层常量与最小工具:仓库路径(REPO_ROOT / SRC_DIR —— lib 模块位于
3
+ * <root>/tools/lib/,上跳两层即仓库根;旧单文件 install.mjs 位于 <root>/tools/
4
+ * 上跳一层,二者推导结果相同)、内置角色名(ROLES)、主 persona 尾部追加默认值
5
+ * (DEFAULT_MAIN_AGENT_PERSONA_EXTRA)与 warn()。
6
+ * 本模块不依赖任何 lib 兄弟模块,供全部其它模块引用。
7
+ */
8
+ import path from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ export const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
12
+ export const SRC_DIR = path.join(REPO_ROOT, 'presets', 'orchestrator')
13
+
14
+ export const ROLES = ['search_external', 'design', 'implement']
15
+
16
+ /**
17
+ * 主 agent persona 尾部追加规则(默认值)。
18
+ * 用户可在配置 UI 的「人设追加」里编辑/清空;配置键 main_agent_persona_extra:
19
+ * null/缺省 = 用本默认;'' = 明确清空(不追加);其他字符串 = 覆盖。
20
+ * {{cwd}}/{{model}} 是 persona 模板占位符,DSH 运行时替换,必须原样保留。
21
+ */
22
+ export const DEFAULT_MAIN_AGENT_PERSONA_EXTRA = `Codegraph MCP default project may be a DIFFERENT repository than {{cwd}}. Never call codegraph_* tools without passing projectPath = {{cwd}} (the absolute path of your working directory). If {{cwd}} has no .codegraph index, fall back to glob/grep/read directly and do not loop or comment on project mismatches.`
23
+
24
+ export function warn(message) {
25
+ console.warn(`warning: ${message}`)
26
+ }
@@ -0,0 +1,516 @@
1
+ /**
2
+ * 交互式安装向导:runWizard(readline 驱动全流程;node:readline 保持原文件的
3
+ * 动态 import('node:readline'))与 createCustomRole(新角色工具池选择)。
4
+ * 依赖 util(ROLES)、config(normalizeMainAgentName / normalizeModelRef)、
5
+ * host(isHostDependent)与 compose(firstLine);被 cli 引用。
6
+ */
7
+ import { ROLES } from './util.mjs'
8
+ import { normalizeMainAgentName, normalizeModelRef } from './config.mjs'
9
+ import { isHostDependent } from './host.mjs'
10
+ import { firstLine } from './compose.mjs'
11
+
12
+ // ── interactive wizard ──────────────────────────────────────────────────────
13
+
14
+ /**
15
+ * Interactive install wizard (readline, zero dependencies). Walks the user
16
+ * through: detection report → MCP server assignment (default roles, main
17
+ * agent, none, or a freshly created custom role) → custom role creation →
18
+ * optional tweaks of the default roles → builtin role deletion
19
+ * (roles_remove) → main-agent rename (main_agent_name) → builtin-role model
20
+ * config (roles.<toolName>.model / .provider,4d:映射到 dsh-tool-subagent 的
21
+ * agentOptions,回车 = 跟随主 agent 不注入) → main-agent base-tool removal →
22
+ * skill assignment → main-agent soft skill rows (optional hard inline subset)
23
+ * → confirm summary. Returns the assignments object { roles, roles_remove,
24
+ * main_agent_extra, main_agent_remove, main_agent_skills,
25
+ * main_agent_skills_inline, main_agent_persona_extra, main_agent_name,
26
+ * skills },roles 条目形状 { name, model, provider, persona, tools }(内置角色
27
+ * model/provider 取向导 4d 结果;自定义角色从 existingConfig 透传保留)。
28
+ * main_agent_persona_extra 无独立问题步骤:从 existingConfig 原样继承(缺省 null),
29
+ * null 时 compose 回落默认常量 DEFAULT_MAIN_AGENT_PERSONA_EXTRA;编辑/清空走配置 UI
30
+ * 或手编配置。roles_remove / main_agent_name 同样从 existingConfig 继承为初值。
31
+ */
32
+ export async function runWizard({ inventory, mcpReports, pluginReports, skills, staticBase, suggested, existingConfig, personaBlocks, srcText, restrictBase }) {
33
+ const { createInterface } = await import('node:readline')
34
+ // Node's rl.question() hangs on the second call when stdin is not a TTY, so
35
+ // drive prompts from a line queue + one pending resolver instead.
36
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false })
37
+ const inputQueue = []
38
+ let pending = null
39
+ rl.on('line', (line) => {
40
+ if (pending !== null) {
41
+ const resolve = pending
42
+ pending = null
43
+ resolve(line)
44
+ } else {
45
+ inputQueue.push(line)
46
+ }
47
+ })
48
+ rl.on('close', () => {
49
+ // Resolve any pending prompt with an empty line so the wizard can end
50
+ // cleanly when stdin is exhausted early.
51
+ if (pending !== null) {
52
+ const resolve = pending
53
+ pending = null
54
+ resolve('')
55
+ }
56
+ })
57
+ process.on('SIGINT', () => {
58
+ console.log('\naborted')
59
+ process.exit(130)
60
+ })
61
+ const question = (prompt) =>
62
+ new Promise((resolve) => {
63
+ if (inputQueue.length > 0) {
64
+ resolve(inputQueue.shift())
65
+ return
66
+ }
67
+ process.stdout.write(prompt)
68
+ pending = resolve
69
+ })
70
+ const ask = async (prompt, fallback = '') => {
71
+ const raw = (await question(prompt)).trim()
72
+ return raw === '' ? fallback : raw
73
+ }
74
+
75
+ // Working state. Seeded from the existing config when present (explicit
76
+ // role tools/personas win), otherwise from smart defaults; custom roles are
77
+ // appended during the wizard.
78
+ const roleOrder = [...ROLES]
79
+ const roleTools = new Map()
80
+ const rolePersonas = new Map()
81
+ // 角色专用模型工作态(roles.<toolName>.model / .provider,向导 4d):内置角色
82
+ // 从 existingConfig 播种(?? null = 跟随主 agent),suggested / 静态播种的内置
83
+ // 角色一律 null(smartDefaults 不设模型);自定义角色在结尾 rolesOut 处从
84
+ // existingConfig 透传,不经这两个 Map。
85
+ const roleModels = new Map()
86
+ const roleProviders = new Map()
87
+ const mainAgentExtra = new Set(existingConfig?.main_agent_extra ?? suggested.main_agent_extra)
88
+ const mainAgentRemove = new Set(existingConfig?.main_agent_remove ?? [])
89
+ const mainAgentSkills = new Set(existingConfig?.main_agent_skills ?? [])
90
+ const mainAgentSkillsInline = new Set(existingConfig?.main_agent_skills_inline ?? [])
91
+ const skillsMap = new Map(Object.entries(existingConfig?.skills ?? {}))
92
+ for (const [toolName, role] of Object.entries(suggested.roles)) {
93
+ roleTools.set(toolName, new Set(role.tools))
94
+ rolePersonas.set(toolName, role.persona)
95
+ roleModels.set(toolName, role.model ?? null)
96
+ roleProviders.set(toolName, role.provider ?? null)
97
+ }
98
+ // Ensure every default role is present (seeded from the static base) so the
99
+ // summary and the tweak step always see them, even when a role received no
100
+ // host tools.
101
+ for (const role of ROLES) {
102
+ if (!roleTools.has(role)) {
103
+ roleTools.set(role, new Set(staticBase[role] ?? []))
104
+ rolePersonas.set(role, null)
105
+ roleModels.set(role, null)
106
+ roleProviders.set(role, null)
107
+ }
108
+ }
109
+ for (const [toolName, role] of Object.entries(existingConfig?.roles ?? {})) {
110
+ if (!roleTools.has(toolName)) roleOrder.push(toolName)
111
+ roleTools.set(toolName, new Set(role.tools ?? []))
112
+ rolePersonas.set(toolName, role.persona ?? null)
113
+ roleModels.set(toolName, role.model ?? null)
114
+ roleProviders.set(toolName, role.provider ?? null)
115
+ }
116
+ // 4b/4c 的持久状态:roles_remove / main_agent_name 从既有配置继承为初值。
117
+ const rolesRemove = new Set(existingConfig?.roles_remove ?? [])
118
+ let mainAgentName = existingConfig?.main_agent_name ?? null
119
+ // 既有配置里已删除的内置角色不进角色列表/摘要/后续分配(同步工作集)。
120
+ for (const role of [...rolesRemove]) {
121
+ const i = roleOrder.indexOf(role)
122
+ if (i !== -1) roleOrder.splice(i, 1)
123
+ roleTools.delete(role)
124
+ rolePersonas.delete(role)
125
+ roleModels.delete(role)
126
+ roleProviders.delete(role)
127
+ }
128
+ const mergeRole = (toolName, tools) => {
129
+ if (!roleTools.has(toolName)) {
130
+ roleTools.set(toolName, new Set())
131
+ rolePersonas.set(toolName, null)
132
+ roleOrder.push(toolName)
133
+ }
134
+ for (const t of tools) roleTools.get(toolName).add(t)
135
+ }
136
+
137
+ // Tool pool for custom-role selection: union of the static role allows plus
138
+ // every detected host tool.
139
+ const toolPool = [...new Set([...ROLES.flatMap((r) => staticBase[r] ?? []), ...inventory])].sort()
140
+
141
+ // 1. welcome + detection report
142
+ console.log('\ndsh-paoding(庖丁)交互式安装向导')
143
+ console.log('-------------------------')
144
+ console.log('检测到的工具:')
145
+ for (const r of mcpReports) {
146
+ const tools = [...inventory].filter((n) => n.startsWith(`mcp__${r.serverName}__`)).join(', ')
147
+ console.log(` MCP ${r.serverName} (${r.transport ?? '-'}): ${tools || '(no tools resolved)'}`)
148
+ }
149
+ for (const p of pluginReports) console.log(` 插件 ${p.label}: ${p.tools.join(', ')}`)
150
+ if (skills.length > 0) console.log(` 技能: ${skills.join(', ')}`)
151
+ console.log()
152
+
153
+ // 2. per-server assignment
154
+ for (const report of mcpReports) {
155
+ const serverTools = [...inventory].filter((n) => n.startsWith(`mcp__${report.serverName}__`))
156
+ if (serverTools.length === 0) continue
157
+ const current = (() => {
158
+ for (const [toolName, tools] of roleTools) {
159
+ if (serverTools.every((t) => tools.has(t))) return toolName
160
+ }
161
+ if (serverTools.every((t) => mainAgentExtra.has(t))) return '主 agent'
162
+ return '未分配'
163
+ })()
164
+ console.log(`服务器 ${report.serverName} 的工具: ${serverTools.join(', ')}`)
165
+ console.log(` 当前分配: ${current}`)
166
+ const choices = ['search_external', 'design', 'implement', '主 agent', '不分配', ...roleOrder.filter((r) => !ROLES.includes(r))]
167
+ const menu = choices.map((c, i) => `[${i + 1}] ${c}`).join(' ')
168
+ console.log(` 分配到哪里? ${menu} [0] 新建角色 [回车] 保持`)
169
+ const pick = (await ask(' > ')).trim()
170
+ if (pick === '') continue
171
+ if (pick === '0') {
172
+ const toolName = await createCustomRole({ question, ask, toolPool, roleTools, rolePersonas, roleOrder })
173
+ if (toolName) {
174
+ mergeRole(toolName, serverTools)
175
+ console.log(` → ${serverTools.length} 个工具已加入角色 ${toolName}`)
176
+ }
177
+ continue
178
+ }
179
+ const idx = Number(pick)
180
+ if (!Number.isInteger(idx) || idx < 1 || idx > choices.length) {
181
+ console.log(' 无效输入,保持当前分配')
182
+ continue
183
+ }
184
+ const target = choices[idx - 1]
185
+ if (target === '主 agent') {
186
+ for (const t of serverTools) {
187
+ for (const set of roleTools.values()) set.delete(t)
188
+ mainAgentExtra.add(t)
189
+ }
190
+ } else if (target === '不分配') {
191
+ for (const t of serverTools) {
192
+ for (const set of roleTools.values()) set.delete(t)
193
+ mainAgentExtra.delete(t)
194
+ }
195
+ } else {
196
+ mergeRole(target, serverTools)
197
+ for (const t of serverTools) mainAgentExtra.delete(t)
198
+ }
199
+ console.log(` → ${report.serverName} → ${target}`)
200
+ }
201
+
202
+ // 3. offer creating more custom roles
203
+ for (;;) {
204
+ const more = (await ask('创建自定义角色?[y/N] ', 'N')).toLowerCase()
205
+ if (more !== 'y') break
206
+ const toolName = await createCustomRole({ question, ask, toolPool, roleTools, rolePersonas, roleOrder })
207
+ if (!toolName) break
208
+ console.log(` 角色 ${toolName} 已创建`)
209
+ }
210
+
211
+ // 4. optional tweaks of the default roles (已删除/将删除的角色跳过)
212
+ const tweak = (await ask('调整默认角色的 persona 或工具?[y/N] ', 'N')).toLowerCase()
213
+ if (tweak === 'y') {
214
+ for (const role of ROLES) {
215
+ if (rolesRemove.has(role)) continue // roles_remove 命中的角色不再可调(整条委派行删除)
216
+ console.log(`\n角色 ${role} — 当前工具 (${roleTools.get(role)?.size ?? 0}): ${[...(roleTools.get(role) ?? [])].join(', ')}`)
217
+ if ((await ask(' 修改 persona?[y/N] ', 'N')).toLowerCase() === 'y') {
218
+ console.log(' 输入新 persona(单独一行 `.` 结束,空行保持默认):')
219
+ const lines = []
220
+ for (;;) {
221
+ const line = await question(' > ')
222
+ if (line.trim() === '.') break
223
+ lines.push(line)
224
+ }
225
+ if (lines.length > 0) rolePersonas.set(role, lines.join('\n').trim())
226
+ }
227
+ const add = (await ask(' 添加工具(工具池编号,逗号分隔,回车跳过)?', '')).trim()
228
+ if (add !== '') {
229
+ for (const n of add.split(',')) {
230
+ const i = Number(n.trim())
231
+ if (Number.isInteger(i) && i >= 1 && i <= toolPool.length) roleTools.get(role).add(toolPool[i - 1])
232
+ }
233
+ }
234
+ const del = (await ask(' 移除工具(当前列表编号,逗号分隔,回车跳过)?', '')).trim()
235
+ if (del !== '') {
236
+ const list = [...roleTools.get(role)]
237
+ for (const n of del.split(',')) {
238
+ const i = Number(n.trim())
239
+ if (Number.isInteger(i) && i >= 1 && i <= list.length) roleTools.get(role).delete(list[i - 1])
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ // 4b. 删除内置角色(roles_remove):整条委派行 + 主 persona 引用 + restrict allow
246
+ // 三处一并移除;非法名提示重问,回车 = 不新增删除(既有配置里已删的保持删除)。
247
+ if (rolesRemove.size > 0) console.log(` 已配置删除内置角色: ${[...rolesRemove].join(', ')}`)
248
+ for (;;) {
249
+ const delBuiltin = (await ask('删除内置角色?(空格分隔名字, 回车=不删):')).trim()
250
+ if (delBuiltin === '') break
251
+ const tokens = delBuiltin.split(/\s+/)
252
+ const unknown = tokens.filter((t) => !ROLES.includes(t))
253
+ if (unknown.length > 0) {
254
+ console.log(` '${unknown.join("', '")}' 不是内置角色(可选: ${ROLES.join(', ')}),请重输`)
255
+ continue
256
+ }
257
+ for (const t of tokens) {
258
+ if (rolesRemove.has(t)) continue
259
+ rolesRemove.add(t)
260
+ const i = roleOrder.indexOf(t)
261
+ if (i !== -1) roleOrder.splice(i, 1)
262
+ roleTools.delete(t)
263
+ rolePersonas.delete(t)
264
+ roleModels.delete(t)
265
+ roleProviders.delete(t)
266
+ console.log(` → 已标记删除内置角色: ${t}`)
267
+ }
268
+ break
269
+ }
270
+
271
+ // 4c. 主 agent 名称(main_agent_name):preset 显示名 + persona 身份行两处生效;
272
+ // 回车 = 维持默认(既有配置已设名则保持该名,否则默认「编排模式 (Orchestrator)」)。
273
+ const nameHint = existingConfig?.main_agent_name ?? '默认编排模式 (Orchestrator)'
274
+ for (;;) {
275
+ const raw = (await ask(`主 agent 名称?(显示名 + persona 身份行,回车 = ${nameHint}):`)).trim()
276
+ if (raw === '') break
277
+ try {
278
+ mainAgentName = normalizeMainAgentName(raw)
279
+ break
280
+ } catch (err) {
281
+ console.log(` ${err.message},请重输(回车 = 保持默认「${nameHint}」)`)
282
+ }
283
+ }
284
+
285
+ // 4d. 内置角色专用模型(roles.<toolName>.model / .provider):生成层映射到
286
+ // dsh-tool-subagent 的 agentOptions(provider 在前、model 在后)。回车 = 跟随
287
+ // 主 agent(null,不注入);provider 仅在模型非空时才问/生效。roles_remove
288
+ // 命中的角色跳过(委派行已整条删除)。非 TTY / 回车跳过时保持播种值不变。
289
+ if ((await ask('为内置角色配置专用模型?[y/N] ', 'N')).toLowerCase() === 'y') {
290
+ for (const role of ROLES) {
291
+ if (rolesRemove.has(role)) continue
292
+ const currentModel = roleModels.get(role) ?? null
293
+ const currentProvider = roleProviders.get(role) ?? null
294
+ console.log(` 角色 ${role} — 当前模型: ${currentModel ? `${currentProvider ? currentProvider + '/' : ''}${currentModel}` : '(跟随主 agent)'}`)
295
+ for (;;) {
296
+ const raw = (await ask(' 专用模型 id(回车=跟随主 agent):')).trim()
297
+ if (raw === '') {
298
+ roleModels.set(role, null)
299
+ break
300
+ }
301
+ try {
302
+ roleModels.set(role, normalizeModelRef(raw, `roles.${role}.model`))
303
+ break
304
+ } catch (err) {
305
+ console.log(` ${err.message},请重输(回车 = 跟随主 agent)`)
306
+ }
307
+ }
308
+ // provider-alone 不生效:模型为空时 provider 强制置 null,不再提问。
309
+ if ((roleModels.get(role) ?? null) !== null) {
310
+ for (;;) {
311
+ const raw = (await ask(' provider 路由(回车=跟随主 agent 的 provider,如 deepseek-official):')).trim()
312
+ if (raw === '') {
313
+ roleProviders.set(role, null)
314
+ break
315
+ }
316
+ try {
317
+ roleProviders.set(role, normalizeModelRef(raw, `roles.${role}.provider`))
318
+ break
319
+ } catch (err) {
320
+ console.log(` ${err.message},请重输(回车 = 跟随主 agent 的 provider)`)
321
+ }
322
+ }
323
+ } else {
324
+ roleProviders.set(role, null)
325
+ }
326
+ }
327
+ }
328
+
329
+ // 5. main-agent base-tool removal (main_agent_remove)
330
+ const mainBase = [...new Set([...(restrictBase ?? []), ...mainAgentExtra])]
331
+ .filter((n) => !isHostDependent(n) || inventory.has(n))
332
+ console.log(`\n主 agent 工具面 (${mainBase.length}): ${mainBase.join(', ')}`)
333
+ if (mainAgentRemove.size > 0) console.log(` 已配置移除: ${[...mainAgentRemove].join(', ')}`)
334
+ const delMain = (await ask(' 移除基础工具(编号,逗号分隔,回车跳过)?', '')).trim()
335
+ if (delMain !== '') {
336
+ const picks = delMain.split(',').map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n >= 1 && n <= mainBase.length)
337
+ const removing = picks.map((n) => mainBase[n - 1])
338
+ const remaining = mainBase.filter((n) => !mainAgentRemove.has(n) && !removing.includes(n))
339
+ if (remaining.length === 0) {
340
+ console.log(' 拒绝:这会清空主 agent 的全部工具')
341
+ } else {
342
+ for (const n of removing) mainAgentRemove.add(n)
343
+ console.log(` → 已标记移除: ${removing.join(', ')}`)
344
+ }
345
+ }
346
+ if (mainAgentRemove.size > 0) {
347
+ const removedList = [...mainAgentRemove]
348
+ console.log(` 可恢复项 (${removedList.length}): ${removedList.map((n, i) => `[${i + 1}] ${n}`).join(' ')}`)
349
+ const restore = (await ask(' 恢复已移除工具(编号,逗号分隔,回车跳过)?', '')).trim()
350
+ if (restore !== '') {
351
+ for (const s of restore.split(',')) {
352
+ const i = Number(s.trim())
353
+ if (Number.isInteger(i) && i >= 1 && i <= removedList.length) mainAgentRemove.delete(removedList[i - 1])
354
+ }
355
+ }
356
+ }
357
+
358
+ // 6. skill assignment (only roles whose allow includes `skill` can use them)
359
+ const skillCapable = [...roleTools.entries()].filter(([, tools]) => tools.has('skill')).map(([toolName]) => toolName)
360
+ if (skills.length > 0 && skillCapable.length > 0) {
361
+ console.log('\n技能分配(引导写入角色 persona;skill 目录对角色全量可见,这是 DSH 的软约束):')
362
+ for (const skill of skills) {
363
+ const currentRoles = skillsMap.get(skill) ?? []
364
+ const menu = skillCapable.map((r, i) => `[${i + 1}] ${r}`).join(' ')
365
+ const pick = await ask(` ${skill} 分配给哪些角色? ${menu} [0] 不分配 [回车] 保持(${currentRoles.join(', ') || '不分配'}) `)
366
+ if (pick.trim() === '') continue
367
+ const chosen = pick.split(',').map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n >= 1 && n <= skillCapable.length)
368
+ if (chosen.length > 0) skillsMap.set(skill, chosen.map((n) => skillCapable[n - 1]))
369
+ else skillsMap.delete(skill)
370
+ }
371
+ }
372
+
373
+ // 6b. main-agent soft skill rows (main_agent_skills): the selected skills'
374
+ // compact rows (name + description + SKILL.md path) are written into the
375
+ // main-agent persona — the main agent has no skill tool / directory
376
+ // injection, so it loads a skill's full rules on demand with its own read
377
+ // tool when a task matches (same soft-guidance semantics as roles).
378
+ if (skills.length > 0) {
379
+ console.log('\n主 agent 技能(软引导:写入 persona 技能行,主 agent 用 read 按需加载正文):')
380
+ console.log(` ${skills.map((s, i) => `[${i + 1}] ${s}`).join(' ')}`)
381
+ const current = [...mainAgentSkills].filter((s) => skills.includes(s)).join(', ') || '(无)'
382
+ const pick = (await ask(` 选择(编号逗号分隔,0 清空,回车保持${current}):`)).trim()
383
+ if (pick === '0') {
384
+ mainAgentSkills.clear()
385
+ } else if (pick !== '') {
386
+ const chosen = pick.split(',').map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n >= 1 && n <= skills.length)
387
+ if (chosen.length > 0) {
388
+ mainAgentSkills.clear()
389
+ for (const n of chosen) mainAgentSkills.add(skills[n - 1])
390
+ }
391
+ }
392
+ }
393
+
394
+ // 6c. optional hard inline subset (main_agent_skills_inline): the skills
395
+ // picked here are inlined in full into the main-agent persona instead of a
396
+ // soft row (fixed per-response persona cost) — for style skills like caveman
397
+ // that must apply unconditionally.
398
+ if (skills.length > 0) {
399
+ console.log('\n其中哪些需全文内联硬生效(风格技能,如 caveman;编号逗号分隔,0 清空,回车保持)?')
400
+ console.log(` ${skills.map((s, i) => `[${i + 1}] ${s}`).join(' ')}`)
401
+ const current = [...mainAgentSkillsInline].filter((s) => skills.includes(s)).join(', ') || '(无)'
402
+ const pick = (await ask(` 选择(编号逗号分隔,0 清空,回车保持${current}):`)).trim()
403
+ if (pick === '0') {
404
+ mainAgentSkillsInline.clear()
405
+ } else if (pick !== '') {
406
+ const chosen = pick.split(',').map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n >= 1 && n <= skills.length)
407
+ if (chosen.length > 0) {
408
+ mainAgentSkillsInline.clear()
409
+ for (const n of chosen) mainAgentSkillsInline.add(skills[n - 1])
410
+ }
411
+ }
412
+ }
413
+
414
+ // 7. confirmation summary
415
+ console.log('\n确认摘要:')
416
+ for (const toolName of roleOrder) {
417
+ const persona = rolePersonas.get(toolName)
418
+ // 模型信息:内置角色取 4d 工作态;自定义角色播种自 existingConfig(新 建 角色
419
+ // 无 Map 项 → ?? null = 跟随主 agent)。
420
+ const model = roleModels.get(toolName) ?? null
421
+ const provider = roleProviders.get(toolName) ?? null
422
+ console.log(
423
+ ` ${toolName}: ${roleTools.get(toolName)?.size ?? 0} 工具${persona ? ` | persona: ${firstLine(persona)}` : ''}` +
424
+ `${model ? ` | 模型: ${provider ? provider + '/' : ''}${model}` : ''}`,
425
+ )
426
+ }
427
+ console.log(` 主 agent 追加: ${[...mainAgentExtra].join(', ') || '(无)'}`)
428
+ console.log(` 主 agent 移除: ${[...mainAgentRemove].join(', ') || '(无)'}`)
429
+ console.log(` 主 agent 技能: ${[...mainAgentSkills].join(', ') || '(无)'}`)
430
+ if (mainAgentSkillsInline.size > 0) console.log(` 主 agent 技能(内联): ${[...mainAgentSkillsInline].join(', ')}`)
431
+ if (rolesRemove.size > 0) console.log(` 内置角色已删除: ${[...rolesRemove].join(', ')}(委派行/主 persona 引用/restrict allow 一并移除)`)
432
+ if (mainAgentName !== null) console.log(` 主 agent 名称: ${mainAgentName}`)
433
+ if (skillsMap.size > 0) {
434
+ for (const [skill, roleList] of skillsMap) console.log(` 技能 ${skill} → ${roleList.join(', ')}`)
435
+ }
436
+ const confirmed = (await ask('\n确认安装并写入配置?[Y/n] ', 'Y')).toLowerCase()
437
+ rl.close()
438
+ if (confirmed === 'n') {
439
+ console.log('已取消,未写入任何文件')
440
+ return null
441
+ }
442
+
443
+ const roles = {}
444
+ for (const toolName of roleOrder) {
445
+ const tools = [...(roleTools.get(toolName) ?? [])]
446
+ if (tools.length === 0 && ROLES.includes(toolName)) continue
447
+ // 内置角色显示名(roles.<toolName>.name)从 existingConfig 继承:向导不提供改名步骤,
448
+ // 但不许在保存时静默丢键(用户手编或 UI 面板配过的 name 要在向导重跑后保留)。
449
+ // 专用模型同理:内置角色取 4d 工作态;自定义角色向导不提供模型步骤,model /
450
+ // provider 从 existingConfig 透传保留(同款保留理由)。
451
+ roles[toolName] = {
452
+ name: ROLES.includes(toolName) ? (existingConfig?.roles?.[toolName]?.name ?? null) : null,
453
+ model: ROLES.includes(toolName) ? (roleModels.get(toolName) ?? null) : (existingConfig?.roles?.[toolName]?.model ?? null),
454
+ provider: ROLES.includes(toolName) ? (roleProviders.get(toolName) ?? null) : (existingConfig?.roles?.[toolName]?.provider ?? null),
455
+ persona: rolePersonas.get(toolName) ?? null,
456
+ tools,
457
+ }
458
+ }
459
+ const skillsOut = {}
460
+ for (const [skill, roleList] of skillsMap) if (roleList.length > 0) skillsOut[skill] = roleList
461
+ return {
462
+ roles,
463
+ roles_remove: [...rolesRemove],
464
+ main_agent_extra: [...mainAgentExtra],
465
+ main_agent_remove: [...mainAgentRemove],
466
+ main_agent_skills: [...mainAgentSkills],
467
+ main_agent_skills_inline: [...mainAgentSkillsInline],
468
+ main_agent_persona_extra: existingConfig?.main_agent_persona_extra ?? null,
469
+ main_agent_name: mainAgentName,
470
+ skills: skillsOut,
471
+ }
472
+ }
473
+
474
+ /** Prompt-driven creation of one custom role; returns its toolName or null. */
475
+ export async function createCustomRole({ ask, toolPool, roleTools, rolePersonas, roleOrder }) {
476
+ const toolName = (await ask(' 新角色 toolName(小写字母/数字/下划线,done 取消): ')).trim()
477
+ if (toolName === '' || toolName === 'done') return null
478
+ if (!/^[a-z][a-z0-9_]*$/.test(toolName)) {
479
+ console.log(' toolName 需匹配 /^[a-z][a-z0-9_]*$/')
480
+ return null
481
+ }
482
+ if (roleTools.has(toolName)) {
483
+ console.log(` 角色 ${toolName} 已存在`)
484
+ return null
485
+ }
486
+ console.log(' 工具池(输入逗号分隔编号,all 全选,none 清空):')
487
+ // 零工具重问循环:tools 为空的自定义角色会在应用时被生成器拒绝(空
488
+ // toolFilter.allow → 子 agent 零工具),所以这里选不出工具就重显工具池重问,
489
+ // 直到至少选了一个;回车 / 'none' 视为清空重问。刻意不设跳出口——想放弃就回
490
+ // toolName 步骤输 done(本函数开头已处理)或直接 Ctrl-C;非 TTY 下向导根本
491
+ // 不会进入(cli 已挡),不存在 stdin 耗尽死循环的路径。
492
+ const picked = new Set()
493
+ while (picked.size === 0) {
494
+ for (let i = 0; i < toolPool.length; i++) {
495
+ if (i % 4 === 0) console.log(' ')
496
+ process.stdout.write(`${i + 1}:${toolPool[i]} `)
497
+ }
498
+ console.log()
499
+ const raw = (await ask(' > ')).trim()
500
+ if (raw === 'all') toolPool.forEach((t) => picked.add(t))
501
+ else if (raw !== 'none' && raw !== '') {
502
+ for (const n of raw.split(',')) {
503
+ const i = Number(n.trim())
504
+ if (Number.isInteger(i) && i >= 1 && i <= toolPool.length) picked.add(toolPool[i - 1])
505
+ }
506
+ }
507
+ if (picked.size === 0) console.log(' 至少选择一个工具(零工具自定义角色会在应用时被拒绝)')
508
+ }
509
+ const personaInput = (await ask(' persona 一句话(回车默认): ')).trim()
510
+ const persona = personaInput === '' ? null : `You are the ${toolName} agent. ${personaInput}`
511
+ roleTools.set(toolName, picked)
512
+ rolePersonas.set(toolName, persona)
513
+ roleOrder.push(toolName)
514
+ console.log(` ✓ 角色 ${toolName} 已创建(${picked.size} 工具)`)
515
+ return toolName
516
+ }