@validflow/ccneed-cli 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +12 -0
- package/.claude-plugin/marketplace.json +13 -0
- package/.claude-plugin/plugin.json +11 -0
- package/.codebuddy-plugin/marketplace.json +13 -0
- package/.codebuddy-plugin/plugin.json +11 -0
- package/.codex-plugin/plugin.json +11 -0
- package/INSTALL.md +46 -30
- package/README.md +24 -10
- package/checksums.json +8 -8
- package/hooks/acquisition.mjs +9 -0
- package/hooks/claude.json +27 -0
- package/hooks/codex.json +27 -0
- package/hooks/workbuddy.json +27 -0
- package/package.json +10 -1
- package/scripts/npm/install.mjs +10 -2
- package/scripts/npm/plugin.mjs +441 -0
- package/scripts/npm/setup.mjs +61 -11
- package/scripts/npm/skills.mjs +47 -1
- package/skills/ccneed/SKILL.md +6 -3
- package/skills/ccneed/references/acquisition.md +71 -0
- package/skills/ccneed/references/resources.md +11 -2
- package/skills/ccneed/scripts/acquisition.mjs +351 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, mkdirSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
|
|
7
|
+
const hostDirs = { codex: '.codex', claude: '.claude', workbuddy: '.workbuddy' };
|
|
8
|
+
const commands = ['ccneed', 'ccneed.cmd', 'ccneed.exe'];
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
/** fileText 只读取普通配置文件,拒绝链接和过大的配置。
|
|
12
|
+
* Args:
|
|
13
|
+
* path: 当前选中宿主的配置路径。
|
|
14
|
+
* Returns:
|
|
15
|
+
* 文件原文或 null。
|
|
16
|
+
* Errors:
|
|
17
|
+
* 特殊文件、链接或读取失败返回异常。
|
|
18
|
+
*/
|
|
19
|
+
function fileText(path) {
|
|
20
|
+
let stat;
|
|
21
|
+
try { stat = lstatSync(path); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
22
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024) throw new Error('Plugin config must be a bounded regular file: ' + path);
|
|
23
|
+
return readFileSync(path, 'utf8');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** checkParents 禁止宿主目录通过链接重定向安装写入。
|
|
27
|
+
* Args:
|
|
28
|
+
* home: 安装目标的明确用户目录。
|
|
29
|
+
* directory: 当前文件的父目录。
|
|
30
|
+
* Errors:
|
|
31
|
+
* 越界路径、父目录链接或特殊文件返回异常。
|
|
32
|
+
*/
|
|
33
|
+
function checkParents(home, directory) {
|
|
34
|
+
const root = resolve(home);
|
|
35
|
+
const target = resolve(directory);
|
|
36
|
+
if (target !== root && !target.startsWith(root + (process.platform === 'win32' ? '\\' : '/'))) throw new Error('Plugin target is outside the selected home');
|
|
37
|
+
const paths = [];
|
|
38
|
+
for (let path = target; path !== root; path = dirname(path)) paths.push(path);
|
|
39
|
+
for (const path of [root, ...paths.reverse()]) {
|
|
40
|
+
let stat;
|
|
41
|
+
try { stat = lstatSync(path); } catch (error) { if (error.code === 'ENOENT') continue; throw error; }
|
|
42
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('Plugin parent must be a plain directory: ' + path);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** hookHandler 重建旧安装器生成的 handler,仅用于识别可迁移配置。
|
|
47
|
+
* Args:
|
|
48
|
+
* root: 固定全局 npm 包目录。
|
|
49
|
+
* host: 明确选择的宿主。
|
|
50
|
+
* node: 旧 handler 中的 Node 路径,不会执行。
|
|
51
|
+
* Returns:
|
|
52
|
+
* 不经动态 shell 插值的命令配置。
|
|
53
|
+
* Errors:
|
|
54
|
+
* 路径包含换行或 NUL 时拒绝生成配置。
|
|
55
|
+
*/
|
|
56
|
+
function hookHandler(root, host, node = process.execPath) {
|
|
57
|
+
const args = [node, join(root, 'skills', 'ccneed', 'scripts', 'acquisition.mjs'), '--host', host];
|
|
58
|
+
if (args.some(value => /[\r\n\0]/.test(value))) throw new Error('Invalid hook path');
|
|
59
|
+
const posix = args.map(value => "'" + value.replaceAll("'", "'\\''") + "'").join(' ');
|
|
60
|
+
const powershell = '& ' + args.map(value => "'" + value.replaceAll("'", "''") + "'").join(' ');
|
|
61
|
+
if (host === 'codex') return { type: 'command', command: posix, commandWindows: powershell, timeout: 10 };
|
|
62
|
+
return { type: 'command', command: process.platform === 'win32' ? powershell : posix, ...(process.platform === 'win32' ? { shell: 'powershell' } : {}), timeout: 10 };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** sameConfig 比较配置语义,避免 JSON 键序或缩进影响自有配置识别。
|
|
66
|
+
* Args:
|
|
67
|
+
* left: 待检查配置。
|
|
68
|
+
* right: 已知安装器配置。
|
|
69
|
+
* Returns:
|
|
70
|
+
* 两份配置是否相同。
|
|
71
|
+
*/
|
|
72
|
+
function sameConfig(left, right) {
|
|
73
|
+
const canonical = value => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) : value;
|
|
74
|
+
return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** ownedLegacyHandler 识别完整旧模板,不把路径包含关键字当作所有权证明。
|
|
78
|
+
* Args:
|
|
79
|
+
* handler: 用户配置中的一条 handler。
|
|
80
|
+
* root: 稳定全局包目录。
|
|
81
|
+
* host: 当前宿主。
|
|
82
|
+
* Returns:
|
|
83
|
+
* handler 是否为未定制的旧安装器输出。
|
|
84
|
+
*/
|
|
85
|
+
function ownedLegacyHandler(handler, root, host) {
|
|
86
|
+
if (!handler || typeof handler.command !== 'string') return false;
|
|
87
|
+
const match = /^(?:& )?'((?:[^']|'')*)'\s/.exec(handler.command);
|
|
88
|
+
const node = match?.[1]?.replaceAll("''", "'");
|
|
89
|
+
if (!node || !/(?:^|[\\/])node(?:\.exe)?$/i.test(node)) return false;
|
|
90
|
+
return sameConfig(handler, hookHandler(root, host, node));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** removeOwnedHooks 只清理旧安装器原样生成的全局 hooks,保留定制 handler。
|
|
94
|
+
* Args:
|
|
95
|
+
* settings: 当前用户配置对象。
|
|
96
|
+
* root: 稳定全局包目录。
|
|
97
|
+
* host: 当前宿主。
|
|
98
|
+
* Returns:
|
|
99
|
+
* 是否移除了旧安装器 handler。
|
|
100
|
+
* Errors:
|
|
101
|
+
* hooks 配置格式无效时停止。
|
|
102
|
+
*/
|
|
103
|
+
function removeOwnedHooks(settings, root, host) {
|
|
104
|
+
if (settings.hooks === undefined) return false;
|
|
105
|
+
if (!settings.hooks || Array.isArray(settings.hooks) || typeof settings.hooks !== 'object') throw new Error('Plugin hooks must be a JSON object');
|
|
106
|
+
let removed = false;
|
|
107
|
+
for (const event of ['SessionStart', 'PreToolUse', 'PostToolUse']) {
|
|
108
|
+
const entries = settings.hooks[event];
|
|
109
|
+
if (entries === undefined) continue;
|
|
110
|
+
if (!Array.isArray(entries)) throw new Error('Plugin event hooks must be an array');
|
|
111
|
+
settings.hooks[event] = entries.flatMap(entry => {
|
|
112
|
+
const matcher = event === 'SessionStart' ? undefined : 'Bash|PowerShell|exec_command';
|
|
113
|
+
if (!entry || entry.matcher !== matcher || !Array.isArray(entry.hooks) || Object.keys(entry).some(key => !['matcher', 'hooks'].includes(key))) return [entry];
|
|
114
|
+
const hooks = entry.hooks.filter(handler => !ownedLegacyHandler(handler, root, host));
|
|
115
|
+
if (hooks.length === entry.hooks.length) return [entry];
|
|
116
|
+
removed = true;
|
|
117
|
+
return hooks.length ? [{ ...entry, hooks }] : [];
|
|
118
|
+
});
|
|
119
|
+
if (!settings.hooks[event].length) delete settings.hooks[event];
|
|
120
|
+
}
|
|
121
|
+
if (!Object.keys(settings.hooks).length) delete settings.hooks;
|
|
122
|
+
return removed;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** removeOwnedAskRules 在存在旧安装器 hook 证据时清理其通用 ask 规则。
|
|
126
|
+
* Args:
|
|
127
|
+
* settings: 当前用户配置。
|
|
128
|
+
* Errors:
|
|
129
|
+
* permissions 配置格式无效时停止。
|
|
130
|
+
*/
|
|
131
|
+
function removeOwnedAskRules(settings) {
|
|
132
|
+
if (settings.permissions === undefined) return;
|
|
133
|
+
const permissions = settings.permissions;
|
|
134
|
+
if (!permissions || Array.isArray(permissions) || typeof permissions !== 'object') throw new Error('Plugin permissions must be a JSON object');
|
|
135
|
+
if (permissions.ask === undefined) return;
|
|
136
|
+
if (!Array.isArray(permissions.ask) || permissions.ask.some(value => typeof value !== 'string')) throw new Error('Plugin ask rules must be strings');
|
|
137
|
+
const owned = new Set(commands.flatMap(binary => ['Bash', 'PowerShell'].flatMap(tool => [`${tool}(${binary} kb start *)`, `${tool}(${binary} tasks reply *)`])));
|
|
138
|
+
permissions.ask = permissions.ask.filter(value => !owned.has(value));
|
|
139
|
+
if (!permissions.ask.length) delete permissions.ask;
|
|
140
|
+
if (!Object.keys(permissions).length) delete settings.permissions;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** mergeSettings 清理自有旧 hooks,仅为 WorkBuddy 保留独立原生执行规则。
|
|
144
|
+
* Args:
|
|
145
|
+
* before: 已有配置原文或 null。
|
|
146
|
+
* root: 固定全局 npm 包目录。
|
|
147
|
+
* host: 明确选择的宿主。
|
|
148
|
+
* Returns:
|
|
149
|
+
* 合并后的 JSON 配置原文;无变化时保留原文。
|
|
150
|
+
* Errors:
|
|
151
|
+
* 配置格式无效时停止;自定义 handler 原样保留。
|
|
152
|
+
*/
|
|
153
|
+
function mergeSettings(before, root, host) {
|
|
154
|
+
const settings = before === null ? {} : JSON.parse(before.replace(/^\uFEFF/, ''));
|
|
155
|
+
if (!settings || Array.isArray(settings) || typeof settings !== 'object') throw new Error('Plugin settings must be a JSON object');
|
|
156
|
+
const previous = JSON.stringify(settings);
|
|
157
|
+
if (removeOwnedHooks(settings, root, host) && host !== 'codex') removeOwnedAskRules(settings);
|
|
158
|
+
if (host === 'workbuddy') {
|
|
159
|
+
settings.permissions ??= {};
|
|
160
|
+
if (!settings.permissions || Array.isArray(settings.permissions) || typeof settings.permissions !== 'object') throw new Error('WorkBuddy permissions must be a JSON object');
|
|
161
|
+
const permissionAsk = settings.permissions.ask ?? [];
|
|
162
|
+
if (!Array.isArray(permissionAsk) || permissionAsk.some(value => typeof value !== 'string')) throw new Error('WorkBuddy ask rules must be strings');
|
|
163
|
+
const compatibleAsk = commands.flatMap(binary => ['Bash', 'PowerShell'].flatMap(tool => [`${tool}(${binary} kb start *)`, `${tool}(${binary} tasks reply *)`]));
|
|
164
|
+
settings.permissions.ask = [...new Set([...permissionAsk, ...compatibleAsk])];
|
|
165
|
+
settings.sandbox ??= {};
|
|
166
|
+
if (!settings.sandbox || Array.isArray(settings.sandbox) || typeof settings.sandbox !== 'object') throw new Error('WorkBuddy sandbox must be a JSON object');
|
|
167
|
+
settings.sandbox.commandSafety ??= {};
|
|
168
|
+
const safety = settings.sandbox.commandSafety;
|
|
169
|
+
if (!safety || Array.isArray(safety) || typeof safety !== 'object') throw new Error('WorkBuddy command safety must be a JSON object');
|
|
170
|
+
const ask = safety.ask ?? [];
|
|
171
|
+
if (!Array.isArray(ask) || ask.some(rule => !Array.isArray(rule?.prefix) || !rule.prefix.length || rule.prefix.some(value => typeof value !== 'string'))) throw new Error('WorkBuddy command safety ask rules must contain command prefixes');
|
|
172
|
+
const added = commands.flatMap(binary => [['kb', 'start'], ['tasks', 'reply']].map(words => ({ prefix: [binary, ...words] })));
|
|
173
|
+
safety.ask = [...ask, ...added.filter(rule => !ask.some(existing => JSON.stringify(existing.prefix) === JSON.stringify(rule.prefix)))];
|
|
174
|
+
}
|
|
175
|
+
return previous === JSON.stringify(settings) && before !== null ? before : JSON.stringify(settings, null, 2) + '\n';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** codexRules 生成原生逐次确认策略,保留只读命令自动执行。
|
|
179
|
+
* Returns:
|
|
180
|
+
* Codex Starlark 规则原文。
|
|
181
|
+
*/
|
|
182
|
+
export function codexRules() {
|
|
183
|
+
return '# Managed by the CCNeed installer; keep manual approvals enabled.\n' + [['kb', 'start'], ['tasks', 'reply']].map(words =>
|
|
184
|
+
'prefix_rule(pattern = ' + JSON.stringify([commands, ...words]) + ', decision = "prompt", justification = "Confirm this CCNeed operation after reviewing the final collection plan")\n').join('');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** preparePluginIntegration 在 npm 替换包之前检查宿主配置冲突,不写文件。
|
|
188
|
+
* Args:
|
|
189
|
+
* home: 当前用户的明确目录,测试可注入隔离目录。
|
|
190
|
+
* agents: 用户明确选择的宿主名称。
|
|
191
|
+
* root: 固定全局 npm 包目录。
|
|
192
|
+
* Returns:
|
|
193
|
+
* 自有迁移和权限配置目标;不会注册全局 hooks。
|
|
194
|
+
* Errors:
|
|
195
|
+
* 无效路径、已有配置冲突或非法 JSON 返回异常。
|
|
196
|
+
*/
|
|
197
|
+
export function preparePluginIntegration(home, agents, root) {
|
|
198
|
+
if (!isAbsolute(home) || !isAbsolute(root)) throw new Error('Plugin directories must be absolute');
|
|
199
|
+
return agents.flatMap(host => {
|
|
200
|
+
if (!Object.hasOwn(hostDirs, host)) throw new Error('Unsupported plugin host');
|
|
201
|
+
const directory = join(home, hostDirs[host]);
|
|
202
|
+
const target = join(directory, host === 'codex' ? 'hooks.json' : 'settings.json');
|
|
203
|
+
checkParents(home, dirname(target));
|
|
204
|
+
const before = fileText(target);
|
|
205
|
+
const files = before === null && host !== 'workbuddy' ? [] : [{ home, host, target, before, after: mergeSettings(before, root, host), root }];
|
|
206
|
+
if (host === 'codex') {
|
|
207
|
+
const target = join(directory, 'rules', 'ccneed.rules');
|
|
208
|
+
checkParents(home, dirname(target));
|
|
209
|
+
const before = fileText(target);
|
|
210
|
+
const after = codexRules();
|
|
211
|
+
if (before !== null && before !== after) throw new Error('Existing custom CCNeed rules will not be overwritten');
|
|
212
|
+
files.unshift({ home, host, target, before, after, root });
|
|
213
|
+
}
|
|
214
|
+
return files;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** replaceText 原子替换已核对的配置,避免保留半份 JSON。
|
|
219
|
+
* Args:
|
|
220
|
+
* path: 明确的宿主配置目标。
|
|
221
|
+
* text: 完整新配置。
|
|
222
|
+
* Errors:
|
|
223
|
+
* 写入或发布失败时返回异常。
|
|
224
|
+
*/
|
|
225
|
+
function replaceText(path, text) {
|
|
226
|
+
const temporary = path + '.' + randomUUID() + '.tmp';
|
|
227
|
+
try {
|
|
228
|
+
writeFileSync(temporary, text, { flag: 'wx', mode: 0o600 });
|
|
229
|
+
renameSync(temporary, path);
|
|
230
|
+
} finally {
|
|
231
|
+
try { unlinkSync(temporary); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** attachPluginIntegration 发布明确选择的宿主配置,不修改安全模式或其他设置。
|
|
236
|
+
* Args:
|
|
237
|
+
* plans: 无副作用预检得到的配置计划。
|
|
238
|
+
* Returns:
|
|
239
|
+
* 实际配置文件路径;仍需宿主信任和重新加载。
|
|
240
|
+
* Errors:
|
|
241
|
+
* 缺失 handler、并发配置修改或写入失败时撤回本次配置写入。
|
|
242
|
+
*/
|
|
243
|
+
export function attachPluginIntegration(plans) {
|
|
244
|
+
for (const plan of plans) {
|
|
245
|
+
if (!fileText(join(plan.root, 'skills', 'ccneed', 'scripts', 'acquisition.mjs'))?.trim()) throw new Error('Incomplete bundled CCNeed plugin');
|
|
246
|
+
checkParents(plan.home, dirname(plan.target));
|
|
247
|
+
if (fileText(plan.target) !== plan.before) throw new Error('Host settings changed during installation');
|
|
248
|
+
}
|
|
249
|
+
const written = [];
|
|
250
|
+
try {
|
|
251
|
+
for (const plan of plans) {
|
|
252
|
+
checkParents(plan.home, dirname(plan.target));
|
|
253
|
+
mkdirSync(dirname(plan.target), { recursive: true });
|
|
254
|
+
checkParents(plan.home, dirname(plan.target));
|
|
255
|
+
if (fileText(plan.target) !== plan.before) throw new Error('Host settings changed during installation');
|
|
256
|
+
if (plan.before === plan.after) continue;
|
|
257
|
+
replaceText(plan.target, plan.after);
|
|
258
|
+
written.push(plan);
|
|
259
|
+
}
|
|
260
|
+
return plans.map(plan => plan.target);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
for (const plan of written.reverse()) {
|
|
263
|
+
if (fileText(plan.target) !== plan.after) continue;
|
|
264
|
+
if (plan.before === null) unlinkSync(plan.target); else replaceText(plan.target, plan.before);
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** nativeLauncher 解析 Windows 的官方 npm launcher,不把参数拼成 shell 命令。
|
|
271
|
+
* Args:
|
|
272
|
+
* host: claude 或 codex。
|
|
273
|
+
* Returns:
|
|
274
|
+
* 当前 Node 可执行的官方宿主脚本。
|
|
275
|
+
* Errors:
|
|
276
|
+
* 缺少官方 npm 布局时失败,不执行未知 cmd 文件。
|
|
277
|
+
*/
|
|
278
|
+
async function nativeLauncher(host) {
|
|
279
|
+
// Resolve PATH with Unicode-aware filesystem calls. Windows where.exe can
|
|
280
|
+
// fail on non-ASCII profile paths despite the shim being directly readable.
|
|
281
|
+
const paths = (process.env.PATH ?? process.env.Path ?? '').split(delimiter).filter(Boolean);
|
|
282
|
+
const shim = paths.map(value => join(value.replace(/^"(.*)"$/, '$1'), host + '.cmd')).find(path => {
|
|
283
|
+
try { return isAbsolute(path) && lstatSync(path).isFile(); } catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return false; throw error; }
|
|
284
|
+
});
|
|
285
|
+
if (!shim) throw new Error('Native plugin manager launcher unavailable');
|
|
286
|
+
const identity = host === 'codex' ? '@openai/codex' : '@anthropic-ai/claude-code';
|
|
287
|
+
const root = join(dirname(shim), 'node_modules', identity);
|
|
288
|
+
const manifest = JSON.parse(fileText(join(root, 'package.json')) ?? '{}');
|
|
289
|
+
const script = join(root, host === 'codex' ? 'bin/codex.js' : 'cli.js');
|
|
290
|
+
const stat = lstatSync(script);
|
|
291
|
+
if (manifest.name !== identity || !stat.isFile() || stat.isSymbolicLink()) throw new Error('Native plugin manager launcher unavailable');
|
|
292
|
+
return script;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** runNative 调用宿主原生插件生命周期,不启动模型或自行写插件缓存。
|
|
296
|
+
* Args:
|
|
297
|
+
* host: claude 或 codex;WorkBuddy 没有已验证的注册 CLI。
|
|
298
|
+
* args: 固定的原生插件命令参数数组。
|
|
299
|
+
* Returns:
|
|
300
|
+
* 原生命令的 stdout 文本。
|
|
301
|
+
* Errors:
|
|
302
|
+
* 宿主缺失、超时或失败时返回不含 stderr 和凭证的异常。
|
|
303
|
+
*/
|
|
304
|
+
export async function runNative(host, args) {
|
|
305
|
+
if (!['claude', 'codex'].includes(host)) throw new Error('Native plugin manager unavailable');
|
|
306
|
+
const options = { timeout: 180_000, maxBuffer: 1024 * 1024, windowsHide: true, shell: false };
|
|
307
|
+
try {
|
|
308
|
+
if (process.platform !== 'win32') return (await exec(host, args, options)).stdout;
|
|
309
|
+
try { return (await exec(host + '.exe', args, options)).stdout; }
|
|
310
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
311
|
+
const launcher = await nativeLauncher(host);
|
|
312
|
+
return (await exec(process.execPath, [launcher, ...args], options)).stdout;
|
|
313
|
+
} catch { throw new Error('Native plugin manager failed; check the selected host installation and retry'); }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** samePath 规范本地来源路径,包括 Windows 原生工具返回的扩展路径前缀。
|
|
317
|
+
* Args:
|
|
318
|
+
* actual: 原生工具报告的本地路径。
|
|
319
|
+
* expected: 固定的全局包路径。
|
|
320
|
+
* Returns:
|
|
321
|
+
* 两个绝对路径是否指向同一词法位置。
|
|
322
|
+
*/
|
|
323
|
+
function samePath(actual, expected) {
|
|
324
|
+
if (typeof actual !== 'string') return false;
|
|
325
|
+
const clean = value => value.replace(/^\\\\\?\\UNC\\/i, '\\\\').replace(/^\\\\\?\\/, '');
|
|
326
|
+
if (!isAbsolute(clean(actual))) return false;
|
|
327
|
+
const left = resolve(clean(actual)), right = resolve(clean(expected));
|
|
328
|
+
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** validateNativePayload 确保原生管理器读取同版本包内的受限载荷路径。
|
|
332
|
+
* Args:
|
|
333
|
+
* root: 稳定的全局 npm 包目录。
|
|
334
|
+
* host: 所选宿主。
|
|
335
|
+
* version: 本次已核验的 CLI 版本。
|
|
336
|
+
* Errors:
|
|
337
|
+
* manifest、catalog 或 hooks 缺失、错版或越界时失败。
|
|
338
|
+
*/
|
|
339
|
+
function validateNativePayload(root, host, version) {
|
|
340
|
+
const directories = { claude: '.claude-plugin', codex: '.codex-plugin', workbuddy: '.codebuddy-plugin' };
|
|
341
|
+
const manifest = JSON.parse(fileText(join(root, directories[host], 'plugin.json')) ?? '{}');
|
|
342
|
+
if (manifest.name !== 'ccneed' || manifest.version !== version || manifest.skills !== './skills' || manifest.hooks !== './hooks/' + host + '.json') throw new Error('Native plugin payload does not match the CLI release');
|
|
343
|
+
const hooks = JSON.parse(fileText(join(root, 'hooks', host + '.json')) ?? '{}');
|
|
344
|
+
if (!hooks.hooks || typeof hooks.hooks !== 'object' || Array.isArray(hooks.hooks)) throw new Error('Native plugin hooks are missing');
|
|
345
|
+
const catalogDirectory = host === 'codex' ? '.agents/plugins' : directories[host];
|
|
346
|
+
const catalog = JSON.parse(fileText(join(root, catalogDirectory, 'marketplace.json')) ?? '{}');
|
|
347
|
+
const entry = Array.isArray(catalog.plugins) ? catalog.plugins.find(value => value.name === 'ccneed') : null;
|
|
348
|
+
const source = host === 'codex' ? { source: 'local', path: './' } : './';
|
|
349
|
+
if (catalog.name !== 'ccneed' || !entry || !sameConfig(entry.source, source) || (entry.version !== undefined && entry.version !== version)) throw new Error('Native marketplace source is invalid');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** nativeJSON 读取有界原生 JSON 输出,不把人类提示误认为安装成功。
|
|
353
|
+
* Args:
|
|
354
|
+
* native: 可注入的原生进程边界。
|
|
355
|
+
* host: 当前宿主。
|
|
356
|
+
* args: 原生命令参数。
|
|
357
|
+
* Returns:
|
|
358
|
+
* 解析后的原生 JSON 数据。
|
|
359
|
+
* Errors:
|
|
360
|
+
* 输出不是有界 JSON 时失败。
|
|
361
|
+
*/
|
|
362
|
+
async function nativeJSON(native, host, args) {
|
|
363
|
+
const stdout = await native(host, args);
|
|
364
|
+
if (typeof stdout !== 'string' || stdout.length > 1024 * 1024) throw new Error('Invalid native plugin response');
|
|
365
|
+
return JSON.parse(stdout.replace(/^\uFEFF/, ''));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** ensureMarketplace 仅注册稳定包目录,拒绝接管同名的其他来源。
|
|
369
|
+
* Args:
|
|
370
|
+
* native: 原生进程边界。
|
|
371
|
+
* host: claude 或 codex。
|
|
372
|
+
* root: 稳定全局包目录。
|
|
373
|
+
* Returns:
|
|
374
|
+
* 已核验的原生 marketplace 记录。
|
|
375
|
+
* Errors:
|
|
376
|
+
* 同名异源、输出无效或原生注册失败时停止。
|
|
377
|
+
*/
|
|
378
|
+
async function ensureMarketplace(native, host, root) {
|
|
379
|
+
const read = async () => {
|
|
380
|
+
const value = await nativeJSON(native, host, ['plugin', 'marketplace', 'list', '--json']);
|
|
381
|
+
const rows = host === 'claude' ? value : value.marketplaces;
|
|
382
|
+
if (!Array.isArray(rows)) throw new Error('Invalid native marketplace response');
|
|
383
|
+
const matches = rows.filter(value => value.name === 'ccneed');
|
|
384
|
+
if (matches.length > 1) throw new Error('Ambiguous native marketplace');
|
|
385
|
+
return matches[0];
|
|
386
|
+
};
|
|
387
|
+
let marketplace = await read();
|
|
388
|
+
if (!marketplace) {
|
|
389
|
+
await native(host, ['plugin', 'marketplace', 'add', root, ...(host === 'claude' ? ['--scope', 'user'] : ['--json'])]);
|
|
390
|
+
marketplace = await read();
|
|
391
|
+
}
|
|
392
|
+
const path = host === 'claude' ? marketplace?.path ?? marketplace?.installLocation : marketplace?.root;
|
|
393
|
+
if (!samePath(path, root) || (host === 'claude' && !['directory', 'local'].includes(marketplace?.source))) throw new Error('Existing CCNeed marketplace has another source');
|
|
394
|
+
return marketplace;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** installedPlugin 读取原生用户级安装状态,不用缓存目录存在代替实际状态。
|
|
398
|
+
* Args:
|
|
399
|
+
* native: 原生进程边界。
|
|
400
|
+
* host: claude 或 codex。
|
|
401
|
+
* Returns:
|
|
402
|
+
* 当前插件记录,未安装时返回 null。
|
|
403
|
+
* Errors:
|
|
404
|
+
* 无效或重复的原生安装记录返回异常。
|
|
405
|
+
*/
|
|
406
|
+
async function installedPlugin(native, host) {
|
|
407
|
+
const value = await nativeJSON(native, host, ['plugin', 'list', '--json']);
|
|
408
|
+
const rows = host === 'claude' ? value : value.installed;
|
|
409
|
+
if (!Array.isArray(rows)) throw new Error('Invalid native plugin response');
|
|
410
|
+
const matches = rows.filter(row => host === 'claude' ? row.id === 'ccneed@ccneed' && row.scope === 'user' : row.pluginId === 'ccneed@ccneed');
|
|
411
|
+
if (matches.length > 1) throw new Error('Ambiguous native plugin installation');
|
|
412
|
+
return matches[0] ?? null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** installNativePlugin 委托宿主注册与升级,逐宿主报告实际版本和待完成操作。
|
|
416
|
+
* Args:
|
|
417
|
+
* native: 原生进程边界,接收 host 和参数数组。
|
|
418
|
+
* host: 当前选择的宿主。
|
|
419
|
+
* root: 已验证的全局包目录。
|
|
420
|
+
* version: 期望的同包版本。
|
|
421
|
+
* Returns:
|
|
422
|
+
* 原生安装结果;错误和 WorkBuddy 手动步骤不会伪装为 installed。
|
|
423
|
+
*/
|
|
424
|
+
export async function installNativePlugin(native, host, root, version) {
|
|
425
|
+
const base = { host, plugin_id: 'ccneed@ccneed', requested_version: version, version: null, marketplace_root: root };
|
|
426
|
+
try {
|
|
427
|
+
validateNativePayload(root, host, version);
|
|
428
|
+
if (host === 'workbuddy') return { ...base, status: 'action_required', next_steps: ['Open WorkBuddy desktop’s native plugin manager and register the local CCNeed marketplace at ' + root, 'Install and enable ccneed@ccneed version ' + version + '; verify that version in the desktop plugin manager', 'Trust the bundled hooks and restart WorkBuddy with default permissions; no supported desktop CLI registration API was used'] };
|
|
429
|
+
await ensureMarketplace(native, host, root);
|
|
430
|
+
let installed = await installedPlugin(native, host);
|
|
431
|
+
if (installed?.enabled === false) return { ...base, status: 'action_required', version: installed.version ?? null, next_steps: ['The native plugin is disabled; review and enable it in the host plugin manager before retrying'] };
|
|
432
|
+
if (!installed || installed.version !== version) {
|
|
433
|
+
if (host === 'claude' && installed) await native(host, ['plugin', 'marketplace', 'update', 'ccneed']);
|
|
434
|
+
await native(host, ['plugin', host === 'claude' ? (installed ? 'update' : 'install') : 'add', 'ccneed@ccneed', ...(host === 'claude' ? ['--scope', 'user'] : ['--json'])]);
|
|
435
|
+
installed = await installedPlugin(native, host);
|
|
436
|
+
}
|
|
437
|
+
const sourceMatches = host === 'claude' || (installed?.installed === true && installed?.source?.source === 'local' && samePath(installed.source.path, root));
|
|
438
|
+
if (!installed || installed.version !== version || installed.enabled !== true || !sourceMatches) return { ...base, status: 'action_required', version: installed?.version ?? null, next_steps: ['The native manager did not verify the requested enabled plugin version; complete installation in the host plugin manager and retry'] };
|
|
439
|
+
return { ...base, status: 'installed', version: installed.version, restart_required: true, hook_trust_required: true };
|
|
440
|
+
} catch { return { ...base, status: 'failed', error: 'native_plugin_registration_failed', next_steps: ['Check the selected host supports native plugin marketplaces and that ccneed is sourced from this exact package directory, then retry; existing custom configuration was preserved'] }; }
|
|
441
|
+
}
|
package/scripts/npm/setup.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { readFileSync, lstatSync, existsSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { dirname, isAbsolute, join, parse } from 'node:path';
|
|
3
|
+
import { dirname, isAbsolute, join, parse, resolve } from 'node:path';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { installBinary, packageRoot } from './install.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { prepareLegacySkillCleanup, removeLegacySkillLinks, parseAgents, validateBundledSkill } from './skills.mjs';
|
|
8
|
+
import { preparePluginIntegration, attachPluginIntegration, installNativePlugin, runNative } from './plugin.mjs';
|
|
8
9
|
|
|
9
10
|
const exec = promisify(execFile);
|
|
10
11
|
const officialPackage = '@validflow/ccneed-cli';
|
|
@@ -97,22 +98,50 @@ function newerVersion(current, desired) {
|
|
|
97
98
|
return BigInt(left[4]) > BigInt(right[4]);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
/**
|
|
101
|
+
/** customNativeProfile 识别无法与默认权限配置一起自动安装的宿主 profile。
|
|
101
102
|
* Args:
|
|
102
|
-
*
|
|
103
|
+
* host: 本次选择的宿主。
|
|
104
|
+
* home: 安装器使用的用户目录。
|
|
105
|
+
* env: 原生进程实际继承的环境,测试可随 native 边界注入。
|
|
103
106
|
* Returns:
|
|
104
|
-
*
|
|
107
|
+
* 非默认 profile 的环境变量名,默认 profile 返回 null。
|
|
108
|
+
*/
|
|
109
|
+
function customNativeProfile(host, home, env) {
|
|
110
|
+
const variable = { codex: 'CODEX_HOME', claude: 'CLAUDE_CONFIG_DIR' }[host];
|
|
111
|
+
const value = variable && env?.[variable];
|
|
112
|
+
if (!value) return null;
|
|
113
|
+
if (typeof value !== 'string') return variable;
|
|
114
|
+
const clean = path => path.replace(/^\\\\\?\\UNC\\/i, '\\\\').replace(/^\\\\\?\\/, '');
|
|
115
|
+
if (!isAbsolute(clean(value))) return variable;
|
|
116
|
+
const actual = resolve(clean(value));
|
|
117
|
+
const expected = resolve(join(home, host === 'codex' ? '.codex' : '.claude'));
|
|
118
|
+
const same = process.platform === 'win32' ? actual.toLowerCase() === expected.toLowerCase() : actual === expected;
|
|
119
|
+
return same ? null : variable;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** installIntegration 全局安装精确版本 CLI,再逐宿主验证原生插件生命周期。
|
|
123
|
+
* Args:
|
|
124
|
+
* options: 包来源、宿主、用户目录和可替换的 npm/程序/native(host,args)/env 边界。
|
|
125
|
+
* Returns:
|
|
126
|
+
* CLI 版本、各宿主实际版本及信任、重启或自定义 profile 的手动设置要求。
|
|
105
127
|
* Errors:
|
|
106
|
-
*
|
|
128
|
+
* npm/CLI 完整性错误直接失败;原生宿主失败返回真实的逐宿主部分结果。
|
|
107
129
|
*/
|
|
108
|
-
export async function installIntegration({ root = packageRoot, agents, home = homedir(), npm = runNpm, verify = verifyInstalledBinary }) {
|
|
130
|
+
export async function installIntegration({ root = packageRoot, agents, home = homedir(), npm = runNpm, verify = verifyInstalledBinary, native = runNative, env = process.env }) {
|
|
109
131
|
const desired = releaseManifest(root);
|
|
110
132
|
const selected = parseAgents(agents);
|
|
133
|
+
// The real runner inherits process.env; injected runners may supply their
|
|
134
|
+
// own environment snapshot without changing the installer's process.
|
|
135
|
+
const nativeEnv = native === runNative ? process.env : env;
|
|
136
|
+
const customProfiles = new Map(selected.map(host => [host, customNativeProfile(host, home, nativeEnv)]));
|
|
137
|
+
const automatic = selected.filter(host => !customProfiles.get(host));
|
|
138
|
+
if (native === runNative && resolve(home) !== resolve(homedir()) && automatic.some(host => host !== 'workbuddy')) throw new Error('A custom home requires an injected native process boundary');
|
|
111
139
|
validateBundledSkill(root);
|
|
112
140
|
const globalRoot = (await npm(['root', '--global'])).trim();
|
|
113
141
|
if (!isAbsolute(globalRoot) || globalRoot === parse(globalRoot).root || /[\r\n]/.test(globalRoot)) throw new Error('Invalid global npm directory');
|
|
114
142
|
const globalPackage = join(globalRoot, '@validflow', 'ccneed-cli');
|
|
115
|
-
|
|
143
|
+
prepareLegacySkillCleanup(home, automatic, globalPackage);
|
|
144
|
+
preparePluginIntegration(home, automatic, globalPackage);
|
|
116
145
|
const current = existingManifest(globalPackage);
|
|
117
146
|
if (current) validateBundledSkill(globalPackage);
|
|
118
147
|
if (current && newerVersion(current.version, desired.version)) throw new Error('Refusing to downgrade; use the current official installer');
|
|
@@ -123,8 +152,29 @@ export async function installIntegration({ root = packageRoot, agents, home = ho
|
|
|
123
152
|
if (releaseManifest(globalPackage).version !== desired.version) throw new Error('Global package version mismatch');
|
|
124
153
|
validateBundledSkill(globalPackage);
|
|
125
154
|
await verify(globalPackage, desired.version);
|
|
126
|
-
const
|
|
127
|
-
|
|
155
|
+
const plugins = [];
|
|
156
|
+
for (const host of selected) {
|
|
157
|
+
const variable = customProfiles.get(host);
|
|
158
|
+
if (variable) {
|
|
159
|
+
plugins.push({ host, plugin_id: 'ccneed@ccneed', requested_version: desired.version, version: null, status: 'action_required', error: 'custom_profile', next_steps: [variable + ' selects a nondefault native profile; automatic registration, default-profile rules and legacy cleanup were not performed for this host', 'Use that profile’s native plugin manager to register ' + globalPackage + ' and install/enable ccneed@ccneed version ' + desired.version, 'Set up the required native permission rules in the same active profile, review and trust the hooks, and restart the host before collection'] });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const result = await installNativePlugin(native, host, globalPackage, desired.version);
|
|
163
|
+
if (result.status === 'installed' || (host === 'workbuddy' && result.status === 'action_required')) {
|
|
164
|
+
try {
|
|
165
|
+
// Native managers can update settings during registration. Re-read
|
|
166
|
+
// rather than overwriting their enabledPlugins or marketplace state.
|
|
167
|
+
result.configuration_targets = attachPluginIntegration(preparePluginIntegration(home, [host], globalPackage));
|
|
168
|
+
result.removed_legacy_skill_links = removeLegacySkillLinks(prepareLegacySkillCleanup(home, [host], globalPackage));
|
|
169
|
+
} catch {
|
|
170
|
+
result.status = 'action_required';
|
|
171
|
+
result.error = 'legacy_integration_cleanup_required';
|
|
172
|
+
result.next_steps = ['Native registration or manual import is not sufficient yet: review the retained custom or concurrently changed legacy configuration, then retry the installer'];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
plugins.push(result);
|
|
176
|
+
}
|
|
177
|
+
return { status: plugins.every(plugin => plugin.status === 'installed') ? 'installed' : 'action_required', version: desired.version, agents: selected, plugins, restart_required: true, hook_trust_required: true, manual_approval_required: true, auth: 'not_started' };
|
|
128
178
|
}
|
|
129
179
|
|
|
130
180
|
/** installerMain 提供安装与更新共用的明确入口,帮助和参数错误不触发网络。
|
|
@@ -137,7 +187,7 @@ export async function installIntegration({ root = packageRoot, agents, home = ho
|
|
|
137
187
|
*/
|
|
138
188
|
export async function installerMain(args) {
|
|
139
189
|
if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
|
|
140
|
-
return 'Install or update CLI
|
|
190
|
+
return 'Install or update the global CLI and register same-version native plugins.\nUsage: npx @validflow/ccneed-cli@latest install --agent codex,workbuddy\nRC: npx @validflow/ccneed-cli@next install --agent codex,workbuddy\nHosts: codex, claude; WorkBuddy requires native desktop import. Check each host result, restart, trust hooks, and keep manual approvals enabled.\n';
|
|
141
191
|
}
|
|
142
192
|
if (args.length !== 2 || args[0] !== '--agent') throw new Error('Use install --agent codex,workbuddy,claude; see install --help');
|
|
143
193
|
return JSON.stringify(await installIntegration({ agents: args[1] })) + '\n';
|
package/scripts/npm/skills.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { lstatSync, mkdirSync, readFileSync, readlinkSync, readdirSync, symlinkS
|
|
|
2
2
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
|
|
5
|
-
const hosts = { codex: '.agents', workbuddy: '.
|
|
5
|
+
const hosts = { codex: '.agents', workbuddy: '.workbuddy', claude: '.claude' };
|
|
6
6
|
|
|
7
7
|
/** fingerprintSkill 为完整技能的文件名和字节生成稳定摘要,不跟随内部链接。
|
|
8
8
|
* Args:
|
|
@@ -193,3 +193,49 @@ export function attachSkillLinks(plans, createLink = symlinkSync) {
|
|
|
193
193
|
throw error;
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
/** prepareLegacySkillCleanup 只识别旧安装器指向同一全局包的技能入口。
|
|
198
|
+
* Args:
|
|
199
|
+
* home: 明确的用户目录,测试使用隔离目录。
|
|
200
|
+
* agents: 本次明确选择的宿主。
|
|
201
|
+
* root: 稳定的全局 npm 包目录。
|
|
202
|
+
* Returns:
|
|
203
|
+
* 可以删除的自有链接;不注册第二份独立技能。
|
|
204
|
+
* Errors:
|
|
205
|
+
* 自定义同名技能、非法宿主或父目录链接使预检失败,不更改文件。
|
|
206
|
+
*/
|
|
207
|
+
export function prepareLegacySkillCleanup(home, agents, root) {
|
|
208
|
+
if (!isAbsolute(home) || !isAbsolute(root)) throw new Error('Installation directories must be absolute');
|
|
209
|
+
const source = join(resolve(root), 'skills', 'ccneed');
|
|
210
|
+
return agents.flatMap(agent => {
|
|
211
|
+
if (!Object.hasOwn(hosts, agent)) throw new Error('Unsupported agent');
|
|
212
|
+
const directories = agent === 'workbuddy' ? [hosts[agent], '.codebuddy'] : [hosts[agent]];
|
|
213
|
+
return directories.flatMap(directory => {
|
|
214
|
+
const parents = [resolve(home), join(home, directory), join(home, directory, 'skills')];
|
|
215
|
+
checkParents(parents);
|
|
216
|
+
const target = join(parents[2], 'ccneed');
|
|
217
|
+
if (statOrMissing(target) && !sameLink(target, source)) throw new Error('Existing custom Skill must be resolved before native installation: ' + target);
|
|
218
|
+
return sameLink(target, source) ? [{ agent, parents, target, source }] : [];
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** removeLegacySkillLinks 移除仍然属于安装器的旧链接,不递归删除技能内容。
|
|
224
|
+
* Args:
|
|
225
|
+
* plans: 已核对来源的旧技能链接。
|
|
226
|
+
* Returns:
|
|
227
|
+
* 实际移除的链接路径。
|
|
228
|
+
* Errors:
|
|
229
|
+
* 父目录或链接在预检后改变时停止,保留未知文件。
|
|
230
|
+
*/
|
|
231
|
+
export function removeLegacySkillLinks(plans) {
|
|
232
|
+
const removed = [];
|
|
233
|
+
for (const plan of plans) {
|
|
234
|
+
checkParents(plan.parents);
|
|
235
|
+
if (!statOrMissing(plan.target)) continue;
|
|
236
|
+
if (!sameLink(plan.target, plan.source)) throw new Error('Legacy Skill changed during installation');
|
|
237
|
+
unlinkSync(plan.target);
|
|
238
|
+
removed.push(plan.target);
|
|
239
|
+
}
|
|
240
|
+
return removed;
|
|
241
|
+
}
|
package/skills/ccneed/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ccneed
|
|
3
|
-
description: 使用 ccneed CLI 连接 CCNeed
|
|
3
|
+
description: 使用 ccneed CLI 连接 CCNeed,准备和确认采集方案,创建或补充知识库、导出 CSV/JSON 数据供本地分析,或把资料交给云端 Agent 分析并下载报告;也可管理登录、会话、文件与账号积分。适用于 Codex、Claude Code、WorkBuddy 等可执行命令的宿主 Agent。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# CCNeed
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
需要数据时,由宿主准备方案,CCNeed 校验、计费和采集,导出后交给用户自己的 Agent 分析。需要平台分析时,继续把知识库交给 CCNeed 云端 Agent。两条路径都在正式采集前展示最终方案并请求用户确认。
|
|
9
9
|
|
|
10
10
|
CLI 与本 Skill 已正式发行;**部分云端命令仍取决于 CCNeed 正式服务接口是否上线**。不能把本地契约测试当成线上能力已验收。平台自定义 Skill/MCP 的 CLI 管理暂不开放,云端仍按账号已有配置运行。
|
|
11
11
|
|
|
@@ -15,16 +15,19 @@ CLI 与本 Skill 已正式发行;**部分云端命令仍取决于 CCNeed 正
|
|
|
15
15
|
| --- | --- |
|
|
16
16
|
| 登录、验证或退出登录、处理凭证错误、切换开发环境 | [认证与环境](references/auth.md) |
|
|
17
17
|
| 查看积分、订阅、近期使用记录 | [账号查询](references/account.md) |
|
|
18
|
+
| 创建/补充知识库、调整采集方案与配额、处理采集 HITL | [采集方案与确认](references/acquisition.md) |
|
|
18
19
|
| 开启或选择会话,提交、查询或观察回合,回答 HITL | [会话与任务](references/tasks.md) |
|
|
19
20
|
| 选择知识库、上传/绑定文件,查看 findings/reports、下载报告 | [资源与产出](references/resources.md) |
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
先确认用户要原始数据还是平台分析。已有数据可直接导出;需要新数据时先准备方案。平台分析才使用 `run` 开启或继续会话。已有会话用真实 `thread_id`,只执行用户此次委托,不自动创建付费任务。
|
|
22
23
|
|
|
23
24
|
## 调用约定
|
|
24
25
|
|
|
25
26
|
- 不熟悉安装版本时先运行 `ccneed --help`、`ccneed version --no-update-check`,再看目标命令的 `--help`。缺少命令说明本版本不支持,不猜 flag 或绕过为未经确认的 HTTP 请求。
|
|
26
27
|
- 产品命令默认 JSON;结果和错误均在 stdout,诊断及更新提醒在 stderr。先检查退出码,再解释业务状态:`pending`、`accepted`、`queued` 都不代表工作已完成。无需 Lark 的 `ok` 信封或 `--json` 参数。
|
|
27
28
|
- 官网授权链接必须先展示并交还用户;用户确认后才运行完成登录步骤。任务 `waiting_user` 时原样说明问题和候选项,停下来等用户选择;不要为了验证流程而替用户选择、批准或补全答案。
|
|
29
|
+
- 本 Skill 随原生插件加载,统一安装器逐宿主报告结果。安装 CLI 不等于插件已启用;`action_required`、`failed` 或待信任 hooks 时先完成宿主操作,不另外复制一份全局 Skill。
|
|
30
|
+
- 采集方案的展示和执行授权由已加载的 plugin hooks 与宿主原生权限共同承担。不能用一句“用户已同意”、`--yes` 或 AskUserQuestion 代替工具执行前的原生权限确认;缺少 hooks、权限策略或可交互确认能力时停止正式采集,按[采集说明](references/acquisition.md)处理。
|
|
28
31
|
- CLI 默认连接正式线上服务 `https://ccneed.ai`;普通用户不需要 `--api-url`。只有获授权的开发者才能用它连接 staging,并使用测试账号;它不是接口路径或环境访问凭证。同一流程保持同一 origin,认证错误不能通过切回生产绕过。
|
|
29
32
|
- `--no-update-check` 关闭本次更新检查。更新提醒不取代当前任务;安装与升级以仓库 README 为准,不把源码构建或手工复制的 Skill 当成可自动联动更新的 npm 安装。
|
|
30
33
|
- 凭证由 CLI 交给系统凭证库;不读取、输出或转交 token、网站 Cookie、设备秘密。只展示公开回答与 findings/reports,不要求原始工具日志、隐藏推理或任意沙箱路径。
|