agentic-flow-kit 2.0.0 → 2.1.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/LICENSE +21 -0
- package/README.md +92 -23
- package/bin/flow-kit.mjs +3 -1
- package/locales/en/installer.json +87 -2
- package/locales/en/overrides/.codebuddy/skills/tfs-pr/SKILL.md +1 -1
- package/locales/en/overrides/workflow/README.md +2 -0
- package/locales/zh-CN/installer.json +87 -2
- package/package.json +2 -2
- package/schemas/install-manifest.schema.json +3 -1
- package/schemas/project-profile.schema.json +1 -1
- package/src/cli/args.mjs +3 -1
- package/src/cli/main.mjs +9 -2
- package/src/commands/doctor.mjs +50 -4
- package/src/commands/install.mjs +160 -13
- package/src/hosts/registry.mjs +154 -10
- package/src/i18n.mjs +3 -0
- package/src/install/apply.mjs +17 -3
- package/src/install/frontmatter.mjs +84 -0
- package/src/install/plan.mjs +174 -16
- package/src/models/known.mjs +13 -0
- package/src/models/probe.mjs +163 -0
- package/src/models/resolve.mjs +312 -0
- package/src/render/tokens.mjs +56 -9
- package/src/tui/ansi.mjs +55 -0
- package/src/tui/flows.mjs +306 -0
- package/src/tui/prompts.mjs +139 -0
- package/src/tui/screen.mjs +217 -0
- package/src/tui/widgets.mjs +111 -0
- package/src/tui/wizard.mjs +68 -0
- package/templates/engine/.agents/commands/new-task.md +2 -2
- package/templates/engine/.agents/commands/pipeline-run.md +5 -1
- package/templates/engine/.agents/commands/stages.md +2 -1
- package/templates/engine/.agents/config/agent-eval.json +17 -0
- package/templates/engine/.agents/config/mechanism-registry.md +2 -1
- package/templates/engine/.agents/config/memory-tiers.json +13 -0
- package/templates/engine/.agents/config/pipeline.json +36 -0
- package/templates/engine/.agents/config/steering.json +28 -0
- package/templates/engine/.agents/evals/README.md +49 -0
- package/templates/engine/.agents/evals/tasks.tsv +6 -0
- package/templates/engine/.agents/hooks/pre-write-check.cjs +27 -0
- package/templates/engine/.agents/notes/injected-memory.md +14 -0
- package/templates/engine/.agents/notes/steering-tiers.md +28 -0
- package/templates/engine/.agents/scripts/agent-eval.mjs +268 -0
- package/templates/engine/.agents/scripts/check-pipeline-state.mjs +85 -0
- package/templates/engine/.agents/scripts/claims.mjs +166 -0
- package/templates/engine/.agents/scripts/dream-run.mjs +78 -3
- package/templates/engine/.agents/scripts/fix-task.mjs +77 -0
- package/templates/engine/.agents/scripts/memory-tier.mjs +196 -0
- package/templates/engine/.agents/scripts/memory-wiki.mjs +296 -0
- package/templates/engine/.agents/scripts/outcome-metrics.mjs +195 -0
- package/templates/engine/.agents/scripts/path-lease.mjs +177 -0
- package/templates/engine/.agents/scripts/session-log.mjs +157 -0
- package/templates/engine/.agents/scripts/steering.mjs +75 -0
- package/templates/engine/.agents/scripts/verify.mjs +16 -5
- package/templates/engine/.agents/skills/arena/SKILL.md +4 -0
- package/templates/engine/.agents/skills/dream/SKILL.md +21 -2
- package/templates/engine/.agents/skills/eval/SKILL.md +16 -0
- package/templates/engine/.agents/skills/model-upgrade/SKILL.md +41 -0
- package/templates/engine/.agents/skills/session-resume/SKILL.md +7 -0
- package/templates/engine/.agents/skills/show-me-your-work/SKILL.md +14 -0
- package/templates/engine/.agents/skills/wiki/SKILL.md +4 -1
- package/templates/git/.gitattributes +1 -0
- package/templates/git/.githooks/package.json +3 -0
- package/templates/git/.github/workflows/ci.yml +32 -0
- package/templates/git/azure-pipelines.yml +46 -0
- package/templates/host-meta/SKILLS.md +3 -0
- package/templates/host-meta/claude/skill-pointer.md +6 -0
- package/templates/host-meta/cline/skill-index.md +6 -0
- package/templates/host-meta/codebuddy/skill-pointer.md +6 -0
- package/templates/host-meta/commandcode/skill-index.md +5 -0
- package/templates/host-meta/opencode/skill-pointer.md +6 -0
- package/templates/host-meta/qoder/skill-index.md +5 -0
- package/templates/host-meta/qwen/skill-index.md +5 -0
- package/templates/host-meta/trae/skill-index.md +6 -0
- package/templates/hosts/claude/settings.hardened.json +94 -0
- package/templates/hosts/claude/settings.json +1 -3
- package/templates/hosts/commandcode/settings.json +1 -3
- package/templates/hosts/qoder/settings.json +1 -3
- package/templates/hosts/qwen/settings.json +1 -3
- package/templates/profiles/go/AGENTS.md +52 -0
- package/templates/profiles/java/AGENTS.md +52 -0
- package/templates/profiles/rust/AGENTS.md +52 -0
- package/templates/profiles/typescript/AGENTS.md +52 -0
- package/templates/scaffold/workflow/INDEX.md +16 -0
- package/templates/scaffold/workflow/README.md +2 -0
- package/templates/scaffold/workflow/artifact-sources.md +25 -0
- package/templates/scaffold/workflow/non-engineer-intent.md +51 -0
package/src/install/plan.mjs
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { HOSTS } from '../hosts/registry.mjs';
|
|
4
|
+
import { HOSTS, hostTargetRel, isAgentTemplate, modelRoleForAgent } from '../hosts/registry.mjs';
|
|
5
|
+
import { makeT } from '../i18n.mjs';
|
|
6
|
+
import { parseFrontmatter, yamlScalar } from './frontmatter.mjs';
|
|
5
7
|
|
|
6
8
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
7
9
|
export const KIT_ROOT = path.resolve(HERE, '..', '..');
|
|
8
10
|
export const TEMPLATES = path.join(KIT_ROOT, 'templates');
|
|
9
11
|
export const LOCALES = path.join(KIT_ROOT, 'locales');
|
|
12
|
+
// host-meta 存放生成技能指针/索引用的模板,不参与 templates/hosts/<host> 的静态枚举
|
|
13
|
+
export const HOST_META = path.join(TEMPLATES, 'host-meta');
|
|
14
|
+
|
|
15
|
+
// 模板目录可能被本机运行过脚本(评测凭证、缓存、本地模型角色)。这些是运行态,
|
|
16
|
+
// 既不该进包,也不该被安装到目标项目——尤其 evals/results.jsonl 是行为评测凭证,
|
|
17
|
+
// 若随模板出厂,会让每个新装项目“凭空”获得一张当前内容的合格凭证。
|
|
18
|
+
const RUNTIME_DIRS = new Set(['.agents/cache', '.agents/tmp', '.agents/tools']);
|
|
19
|
+
const RUNTIME_FILES = new Set([
|
|
20
|
+
'.agents/verifications.jsonl',
|
|
21
|
+
'.agents/judgments.jsonl',
|
|
22
|
+
'.agents/evals/results.jsonl',
|
|
23
|
+
'.agents/config/model-roles.local.json',
|
|
24
|
+
'.agents/notes/runtime-env.local.md',
|
|
25
|
+
]);
|
|
10
26
|
|
|
11
27
|
export function listFiles(dir) {
|
|
12
28
|
if (!fs.existsSync(dir)) return [];
|
|
@@ -15,8 +31,12 @@ export function listFiles(dir) {
|
|
|
15
31
|
for (const ent of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
16
32
|
const childAbs = path.join(abs, ent.name);
|
|
17
33
|
const childRel = rel ? `${rel}/${ent.name}` : ent.name;
|
|
18
|
-
if (ent.isDirectory())
|
|
19
|
-
|
|
34
|
+
if (ent.isDirectory()) {
|
|
35
|
+
if (RUNTIME_DIRS.has(childRel)) continue;
|
|
36
|
+
walk(childAbs, childRel);
|
|
37
|
+
} else if (ent.isFile() && !RUNTIME_FILES.has(childRel)) {
|
|
38
|
+
out.push(childRel);
|
|
39
|
+
}
|
|
20
40
|
}
|
|
21
41
|
};
|
|
22
42
|
walk(dir, '');
|
|
@@ -28,11 +48,135 @@ function localeOverride(lang, targetRel) {
|
|
|
28
48
|
return fs.existsSync(p) ? p : null;
|
|
29
49
|
}
|
|
30
50
|
|
|
51
|
+
function applyLocaleOverrides(lang, entries) {
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const override = localeOverride(lang, entry.targetRel);
|
|
54
|
+
if (!override) continue;
|
|
55
|
+
entry.srcAbs = override;
|
|
56
|
+
delete entry.getText;
|
|
57
|
+
}
|
|
58
|
+
return entries;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function dedupe(entries) {
|
|
62
|
+
const byTarget = new Map();
|
|
63
|
+
for (const entry of entries.sort((a, b) => a.priority - b.priority)) byTarget.set(entry.targetRel, entry);
|
|
64
|
+
return [...byTarget.values()];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function has(obj, key) {
|
|
68
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 单次替换:overrides 优先,其次全局 vars;不递归扫描已插入的内容。
|
|
72
|
+
function fillTemplate(template, vars, overrides = {}) {
|
|
73
|
+
return String(template).replace(/\{\{(\w+)\}\}/g, (m, key) => {
|
|
74
|
+
if (has(overrides, key)) return String(overrides[key]);
|
|
75
|
+
if (has(vars, key)) return String(vars[key]);
|
|
76
|
+
return m;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readTemplate(abs, fallback) {
|
|
81
|
+
return fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : fallback;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readSkillMeta(entry) {
|
|
85
|
+
let name = path.basename(path.dirname(entry.targetRel));
|
|
86
|
+
let description = name;
|
|
87
|
+
try {
|
|
88
|
+
const fm = parseFrontmatter(fs.readFileSync(entry.srcAbs, 'utf8'));
|
|
89
|
+
if (fm.name) name = fm.name;
|
|
90
|
+
if (fm.description) description = fm.description;
|
|
91
|
+
} catch {
|
|
92
|
+
/* 源缺失时保留目录名 */
|
|
93
|
+
}
|
|
94
|
+
return { name, description, targetRel: entry.targetRel };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildSkillsList(skills, t) {
|
|
98
|
+
if (!skills.length) return `- ${t('host.skillsIndexEmpty')}`;
|
|
99
|
+
return skills
|
|
100
|
+
.map((s) => t('host.skillsIndexLine', { name: s.name, description: s.description, path: s.targetRel }))
|
|
101
|
+
.join('\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function addGeneratedSkillSurfaces({ plan, hosts, skillEntries, t }) {
|
|
105
|
+
const skills = skillEntries.map(readSkillMeta).sort((a, b) => a.name.localeCompare(b.name));
|
|
106
|
+
|
|
107
|
+
for (const host of hosts) {
|
|
108
|
+
const h = HOSTS[host];
|
|
109
|
+
if (!h?.skills) continue;
|
|
110
|
+
|
|
111
|
+
if (h.skills.mode === 'native') {
|
|
112
|
+
const template = readTemplate(path.join(HOST_META, host, 'skill-pointer.md'), [
|
|
113
|
+
'---',
|
|
114
|
+
'name: {{skillName}}',
|
|
115
|
+
'description: {{skillDescription}}',
|
|
116
|
+
'---',
|
|
117
|
+
'{{pointer}}',
|
|
118
|
+
'',
|
|
119
|
+
].join('\n'));
|
|
120
|
+
for (const skill of skills) {
|
|
121
|
+
const overrides = {
|
|
122
|
+
skillName: skill.name,
|
|
123
|
+
skillDescription: yamlScalar(skill.description),
|
|
124
|
+
skillPath: skill.targetRel,
|
|
125
|
+
pointer: t('host.skillPointer', { path: skill.targetRel }),
|
|
126
|
+
};
|
|
127
|
+
plan.push({
|
|
128
|
+
srcAbs: path.join(HOST_META, host, 'skill-pointer.md'),
|
|
129
|
+
targetRel: `${h.dir}/${h.skills.dir}/${skill.name}/${h.skills.file}`,
|
|
130
|
+
category: 'host',
|
|
131
|
+
// 生成器读取语言覆盖后的技能 frontmatter,优先于静态宿主技能回退件。
|
|
132
|
+
priority: 35,
|
|
133
|
+
kind: 'generated',
|
|
134
|
+
getText: (vars) => fillTemplate(template, vars, overrides),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// shared / index:生成一个可发现入口,不复制技能正文
|
|
141
|
+
const hasCommands = Boolean(h.commands);
|
|
142
|
+
const targetRel = hasCommands
|
|
143
|
+
? `${h.dir}/${h.commands.dir}/wf-skills.md`
|
|
144
|
+
: `${h.dir}/SKILLS.md`;
|
|
145
|
+
const templateAbs = hasCommands
|
|
146
|
+
? path.join(HOST_META, host, 'skill-index.md')
|
|
147
|
+
: path.join(HOST_META, 'SKILLS.md');
|
|
148
|
+
const template = readTemplate(templateAbs, [
|
|
149
|
+
'---',
|
|
150
|
+
'description: {{indexDescription}}',
|
|
151
|
+
'---',
|
|
152
|
+
'{{skillsList}}',
|
|
153
|
+
'',
|
|
154
|
+
].join('\n'));
|
|
155
|
+
const overrides = {
|
|
156
|
+
indexDescription: t('host.skillsIndexDescription'),
|
|
157
|
+
title: t('host.skillsIndexTitle'),
|
|
158
|
+
skillsList: buildSkillsList(skills, t),
|
|
159
|
+
};
|
|
160
|
+
plan.push({
|
|
161
|
+
srcAbs: templateAbs,
|
|
162
|
+
targetRel,
|
|
163
|
+
category: 'host',
|
|
164
|
+
priority: 25,
|
|
165
|
+
kind: 'generated',
|
|
166
|
+
getText: (vars) => fillTemplate(template, vars, overrides),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
31
171
|
// 构建安装计划。targetRel 使用 / 分隔;profile 覆盖 engine,宿主只写自己的点目录。
|
|
32
|
-
|
|
172
|
+
// models: { [host]: { code?, judgment?, review? } },用于注入宿主子代理 model 字段。
|
|
173
|
+
export function buildPlan({ target, hosts, lang, stack, projectName, categories, models = null, tier = 'standard', hardened = false }) {
|
|
174
|
+
const t = makeT(lang);
|
|
33
175
|
const plan = [];
|
|
34
|
-
const push = (srcAbs, targetRel, category, priority = 10, kind = 'file') => {
|
|
35
|
-
|
|
176
|
+
const push = (srcAbs, targetRel, category, priority = 10, kind = 'file', extra = {}) => {
|
|
177
|
+
const entry = { srcAbs, targetRel: targetRel.replace(/\\/g, '/'), category, priority, kind, ...extra };
|
|
178
|
+
plan.push(entry);
|
|
179
|
+
return entry;
|
|
36
180
|
};
|
|
37
181
|
|
|
38
182
|
// 1) 引擎核心
|
|
@@ -50,8 +194,18 @@ export function buildPlan({ target, hosts, lang, stack, projectName, categories
|
|
|
50
194
|
const h = HOSTS[host];
|
|
51
195
|
if (!h) continue;
|
|
52
196
|
for (const rel of listFiles(path.join(TEMPLATES, 'hosts', host))) {
|
|
53
|
-
|
|
54
|
-
|
|
197
|
+
// 加固配置是 settings.json 的替换源,不作为独立文件安装。
|
|
198
|
+
if (rel === 'settings.hardened.json') continue;
|
|
199
|
+
const srcAbs = (hardened && host === 'claude' && rel === 'settings.json')
|
|
200
|
+
? path.join(TEMPLATES, 'hosts', host, 'settings.hardened.json')
|
|
201
|
+
: path.join(TEMPLATES, 'hosts', host, rel);
|
|
202
|
+
const entry = push(srcAbs, hostTargetRel(host, rel), 'host', 30);
|
|
203
|
+
if (h.agents?.modelField && isAgentTemplate(host, rel)) {
|
|
204
|
+
entry.modelRole = modelRoleForAgent(rel);
|
|
205
|
+
entry.modelField = h.agents.modelField;
|
|
206
|
+
entry.modelInherit = h.agents.inheritValue;
|
|
207
|
+
entry.modelValue = models?.[host]?.[entry.modelRole] ?? null;
|
|
208
|
+
}
|
|
55
209
|
}
|
|
56
210
|
}
|
|
57
211
|
|
|
@@ -62,21 +216,25 @@ export function buildPlan({ target, hosts, lang, stack, projectName, categories
|
|
|
62
216
|
|
|
63
217
|
// 5) git 门禁与合并策略
|
|
64
218
|
for (const rel of listFiles(path.join(TEMPLATES, 'git'))) {
|
|
219
|
+
// lean 档不生成 CI:小项目先跑本地钩子;standard/enterprise 才落 CI。
|
|
220
|
+
if (tier === 'lean' && (rel === 'azure-pipelines.yml' || rel.startsWith('.github/'))) continue;
|
|
65
221
|
if (rel === '.gitattributes') push(path.join(TEMPLATES, 'git', rel), '.gitattributes', 'git', 40, 'merge-gitattributes');
|
|
66
222
|
else if (rel === '.gitignore.engine') push(path.join(TEMPLATES, 'git', rel), '.gitignore', 'git', 40, 'merge-gitignore');
|
|
67
223
|
else push(path.join(TEMPLATES, 'git', rel), rel, 'git', 40);
|
|
68
224
|
}
|
|
69
225
|
|
|
70
226
|
// 6) 语言包覆盖(按目标路径镜像)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
227
|
+
applyLocaleOverrides(lang, plan);
|
|
228
|
+
let deduped = dedupe(plan);
|
|
229
|
+
|
|
230
|
+
// 7) 从覆盖后的 .agents/skills/<name>/SKILL.md 生成宿主技能面
|
|
231
|
+
const skillEntries = deduped.filter((e) => /^\.agents\/skills\/[^/]+\/SKILL\.md$/.test(e.targetRel));
|
|
232
|
+
addGeneratedSkillSurfaces({ plan, hosts, skillEntries, t });
|
|
233
|
+
|
|
234
|
+
// 生成项同样允许按目标路径覆盖,然后二次去重(静态宿主文件优先级更高)
|
|
235
|
+
applyLocaleOverrides(lang, plan);
|
|
236
|
+
deduped = dedupe(plan);
|
|
75
237
|
|
|
76
|
-
// 同目标去重:高优先级覆盖低优先级
|
|
77
|
-
const byTarget = new Map();
|
|
78
|
-
for (const entry of plan.sort((a, b) => a.priority - b.priority)) byTarget.set(entry.targetRel, entry);
|
|
79
|
-
let deduped = [...byTarget.values()];
|
|
80
238
|
if (categories && categories.length) deduped = deduped.filter((e) => categories.includes(e.category));
|
|
81
239
|
|
|
82
240
|
return { target, hosts, lang, stack, projectName, entries: deduped };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// 仅收录已核实的推荐别名。没有可靠来源的宿主不合成模型列表;探测不到就 inherit/手填。
|
|
2
|
+
export const KNOWN_MODELS = {
|
|
3
|
+
// Claude Code 子代理 model 接受 alias / full name;alias 随主模型档位走
|
|
4
|
+
claude: [
|
|
5
|
+
{ id: 'sonnet', roles: ['code'], note: 'balanced' },
|
|
6
|
+
{ id: 'haiku', roles: ['code'], note: 'fast/cheap' },
|
|
7
|
+
{ id: 'opus', roles: ['judgment', 'review'], note: 'strong reasoning' },
|
|
8
|
+
],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function knownFor(host, role) {
|
|
12
|
+
return (KNOWN_MODELS[host] || []).filter((m) => m.roles.includes(role)).map((m) => m.id);
|
|
13
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { HOSTS } from '../hosts/registry.mjs';
|
|
6
|
+
|
|
7
|
+
// 只读探测:宿主配置 / 环境变量为默认来源;CLI 列表命令必须显式允许。
|
|
8
|
+
// 任何失败都静默降级,安装不能被探测拖死或卡在认证提示上。
|
|
9
|
+
export function probeModels({
|
|
10
|
+
host,
|
|
11
|
+
target = process.cwd(),
|
|
12
|
+
home = os.homedir(),
|
|
13
|
+
env = process.env,
|
|
14
|
+
allowCli = false,
|
|
15
|
+
exec = spawnSync,
|
|
16
|
+
} = {}) {
|
|
17
|
+
const h = HOSTS[host];
|
|
18
|
+
if (!h) return [];
|
|
19
|
+
|
|
20
|
+
const found = [];
|
|
21
|
+
const add = (id, source) => {
|
|
22
|
+
const s = String(id ?? '').trim();
|
|
23
|
+
if (!s || s.length > 200) return;
|
|
24
|
+
// 模型 ID 常见字符:provider/model、:tag、#variant、@version、+build
|
|
25
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:/#@+~=-]*$/.test(s)) return;
|
|
26
|
+
found.push({ id: s, source });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
for (const raw of h.modelProbe?.configs || []) {
|
|
30
|
+
const json = readJsonLoose(resolveConfigPath(raw, target, home));
|
|
31
|
+
if (json) collectModels(json, add);
|
|
32
|
+
}
|
|
33
|
+
for (const key of h.modelProbe?.env || []) {
|
|
34
|
+
const value = env[key];
|
|
35
|
+
if (value) add(value, 'env');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (allowCli) {
|
|
39
|
+
for (const cli of h.modelProbe?.cli || []) {
|
|
40
|
+
if (!cli?.command) continue;
|
|
41
|
+
// Windows 的 `cmd` 往往是系统 cmd.exe,误执行会变成 shell 探测
|
|
42
|
+
if (process.platform === 'win32' && /^cmd(\.exe)?$/i.test(cli.command)) continue;
|
|
43
|
+
const r = exec(cli.command, cli.args || [], {
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
timeout: 1500,
|
|
46
|
+
windowsHide: true,
|
|
47
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
48
|
+
});
|
|
49
|
+
if (!r || r.error || r.status !== 0 || !r.stdout) continue;
|
|
50
|
+
for (const id of parseCliModels(r.stdout)) add(id, 'cli');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
return found.filter((m) => {
|
|
56
|
+
if (seen.has(m.id)) return false;
|
|
57
|
+
seen.add(m.id);
|
|
58
|
+
return true;
|
|
59
|
+
}).slice(0, 20);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveConfigPath(raw, target, home) {
|
|
63
|
+
const s = String(raw);
|
|
64
|
+
if (s === '~') return home;
|
|
65
|
+
if (s.startsWith('~/') || s.startsWith('~\\')) return path.join(home, s.slice(2));
|
|
66
|
+
return path.isAbsolute(s) ? s : path.resolve(target, s);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 容忍 JSONC(注释/尾逗号);解析失败返回 null,不抛。
|
|
70
|
+
export function readJsonLoose(file) {
|
|
71
|
+
let text;
|
|
72
|
+
try {
|
|
73
|
+
text = fs.readFileSync(file, 'utf8');
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(stripJsonComments(text));
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function stripJsonComments(text) {
|
|
85
|
+
let out = '';
|
|
86
|
+
let inString = false;
|
|
87
|
+
let quote = '';
|
|
88
|
+
let escape = false;
|
|
89
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
90
|
+
const ch = text[i];
|
|
91
|
+
const next = text[i + 1];
|
|
92
|
+
if (inString) {
|
|
93
|
+
out += ch;
|
|
94
|
+
if (escape) escape = false;
|
|
95
|
+
else if (ch === '\\') escape = true;
|
|
96
|
+
else if (ch === quote) inString = false;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ch === '"' || ch === "'") {
|
|
100
|
+
inString = true;
|
|
101
|
+
quote = ch;
|
|
102
|
+
out += ch;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (ch === '/' && next === '/') {
|
|
106
|
+
while (i < text.length && text[i] !== '\n') i += 1;
|
|
107
|
+
out += '\n';
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (ch === '/' && next === '*') {
|
|
111
|
+
i += 2;
|
|
112
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i += 1;
|
|
113
|
+
i += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
out += ch;
|
|
117
|
+
}
|
|
118
|
+
return out.replace(/,\s*([}\]])/g, '$1');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function collectModels(value, add, depth = 0) {
|
|
122
|
+
if (depth > 7 || value == null) return;
|
|
123
|
+
if (Array.isArray(value)) {
|
|
124
|
+
for (const item of value) {
|
|
125
|
+
if (typeof item === 'string') add(item, 'config');
|
|
126
|
+
else collectModels(item, add, depth + 1);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (typeof value !== 'object') return;
|
|
131
|
+
for (const [key, child] of Object.entries(value)) {
|
|
132
|
+
if (/^(model|modelId|defaultModel|fastModel|subagentModel|primaryModel)$/i.test(key)) {
|
|
133
|
+
if (typeof child === 'string') add(child, 'config');
|
|
134
|
+
else collectModels(child, add, depth + 1);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (/^(models|providers|provider)$/i.test(key)) {
|
|
138
|
+
collectModels(child, add, depth + 1);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// 只向下钻一层常见容器,避免把任意 settings 里的普通字符串当模型
|
|
142
|
+
if (child && typeof child === 'object' && /^(opencode|qwen|claude|pi|commandcode|qoder|zcode)$/i.test(key)) {
|
|
143
|
+
collectModels(child, add, depth + 1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function parseCliModels(stdout) {
|
|
149
|
+
const clean = String(stdout).replace(/\x1b\[[0-9;]*m/g, '');
|
|
150
|
+
const out = [];
|
|
151
|
+
for (const line of clean.split(/\r?\n/)) {
|
|
152
|
+
const trimmed = line.trim();
|
|
153
|
+
// 只过滤整行表头;`provider/model-a` 这类以 provider 开头的合法 ID 必须保留。
|
|
154
|
+
if (!trimmed
|
|
155
|
+
|| /^(?:available\s+models?|models?|usage|name|provider)\s*:?$/i.test(trimmed)
|
|
156
|
+
|| /^-+$/.test(trimmed)) continue;
|
|
157
|
+
// 取行内第一个像模型 ID 的 token;表格分隔线/标题会被过滤
|
|
158
|
+
const token = trimmed.split(/\s+/).find((t) => /^[A-Za-z0-9][A-Za-z0-9._:/#@+~=-]*$/.test(t)
|
|
159
|
+
&& (/[/:.]/.test(t) || t.length > 3));
|
|
160
|
+
if (token) out.push(token.replace(/[,;]+$/, ''));
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|