agent-protocol-cli 1.0.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.
@@ -0,0 +1,67 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { buildCorpus, search as semanticSearch } from '../../vectorizer.js';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const INDEX_PATH = join(__dirname, '..', '..', 'data', 'index.json');
8
+
9
+ export async function searchCommand(query, options) {
10
+ const limit = parseInt(options.limit || '10');
11
+ const asJson = options.json || false;
12
+
13
+ if (!query || !query.trim()) {
14
+ const index = loadIndex();
15
+ printResults(index.slice(0, limit), query, asJson);
16
+ return;
17
+ }
18
+
19
+ const index = loadIndex();
20
+ try {
21
+ const corpus = buildCorpus(index);
22
+ const results = semanticSearch(query, corpus, index, limit);
23
+ printResults(results, query, asJson);
24
+ } catch (err) {
25
+ // Fallback: keyword match
26
+ const kw = query.toLowerCase();
27
+ const results = index.filter(m => {
28
+ const text = [m.name, m.description, m.id, ...(m.tags || [])].join(' ').toLowerCase();
29
+ return text.includes(kw);
30
+ }).slice(0, limit);
31
+ printResults(results, query, asJson);
32
+ }
33
+ }
34
+
35
+ function loadIndex() {
36
+ if (!existsSync(INDEX_PATH)) {
37
+ console.error('❌ 索引文件未找到。请重新安装 agent-protocol-cli');
38
+ process.exit(1);
39
+ }
40
+ return JSON.parse(readFileSync(INDEX_PATH, 'utf-8'));
41
+ }
42
+
43
+ function printResults(results, query, asJson) {
44
+ if (asJson) {
45
+ console.log(JSON.stringify({ query, total: results.length, results }, null, 2));
46
+ return;
47
+ }
48
+
49
+ if (results.length === 0) {
50
+ console.log(`\n 🔍 未找到匹配 "${query}" 的 Skill\n`);
51
+ return;
52
+ }
53
+
54
+ console.log(`\n 📦 找到 ${results.length} 个匹配的 Skill(查询: "${query}")\n`);
55
+ console.log(' ' + '─'.repeat(64));
56
+
57
+ results.forEach((m, i) => {
58
+ const capCount = m.capabilities?.length || 0;
59
+ const repo = m._repo || m.id?.replace(/\./g, '/') || 'unknown';
60
+ const score = m.score ? (m.score * 100).toFixed(1) + '% 匹配' : '';
61
+ console.log(` ${(i + 1).toString().padStart(2)}. ${m.name} v${m.version || '0.1'} ${score}`);
62
+ console.log(` 描述: ${(m.description || '暂无').slice(0, 80)}`);
63
+ console.log(` 能力: ${capCount} 个 | 引擎: ${m.runtime?.engine || '未指定'}`);
64
+ console.log(` 安装: agent install ${repo}`);
65
+ console.log('');
66
+ });
67
+ }
@@ -0,0 +1,339 @@
1
+ import { spawnSync } from 'child_process';
2
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ import chalk from 'chalk';
6
+ import { createInterface } from 'readline';
7
+
8
+ const AGENTS_DIR = join(homedir(), '.agents');
9
+ const CONFIG_PATH = join(AGENTS_DIR, '.agent-config.json');
10
+
11
+ export async function updateCommand(skillName, options) {
12
+ const all = options.all || !skillName;
13
+ const isRollback = options.rollback || false;
14
+ const force = options.force || false;
15
+ const checkOnly = options.check || options.dryRun || false;
16
+
17
+ // Load config
18
+ const config = loadConfig();
19
+ const installed = config.installed || {};
20
+ const names = Object.keys(installed);
21
+
22
+ // If a specific skill name is given, update only that one
23
+ const targets = skillName
24
+ ? (installed[skillName] ? [skillName] : [])
25
+ : names;
26
+
27
+ if (names.length === 0) {
28
+ console.log(chalk.yellow(' 尚未安装任何 Skill。'));
29
+ console.log(chalk.gray(' 使用: agent install user/repo'));
30
+ return;
31
+ }
32
+
33
+ if (skillName && !installed[skillName]) {
34
+ console.error(chalk.red(`❌ 未找到已安装的 Skill: ${skillName}`));
35
+ console.log(chalk.gray(' 已安装的: ' + names.join(', ')));
36
+ process.exit(1);
37
+ }
38
+
39
+ if (targets.length === 0) {
40
+ console.log(chalk.yellow(' 没有需要更新的 Skill。'));
41
+ return;
42
+ }
43
+
44
+ // ── Rollback mode ──
45
+ if (isRollback) {
46
+ await rollbackSkill(skillName, options, installed);
47
+ return;
48
+ }
49
+
50
+ // ── Check-only mode ──
51
+ if (checkOnly) {
52
+ await checkUpdates(targets, installed);
53
+ return;
54
+ }
55
+
56
+ // ── Update mode ──
57
+ console.log(chalk.blue(all
58
+ ? `🔄 更新所有已安装的 Skill (${targets.length} 个)...`
59
+ : `🔄 更新 ${chalk.bold(skillName)}...`));
60
+ console.log('');
61
+
62
+ let successCount = 0;
63
+ let failCount = 0;
64
+ let skippedCount = 0;
65
+
66
+ for (const name of targets) {
67
+ const skill = installed[name];
68
+
69
+ if (!existsSync(skill.dir)) {
70
+ console.log(chalk.yellow(` ⚠ ${chalk.bold(name)}: 目录不存在,跳过`));
71
+ const answer = await ask(chalk.yellow(` 是否从配置中移除?[y/N] `));
72
+ if (answer.toLowerCase() === 'y') {
73
+ delete installed[name];
74
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
75
+ console.log(chalk.gray(` ✓ ${name} 已从注册表中移除`));
76
+ }
77
+ skippedCount++;
78
+ continue;
79
+ }
80
+
81
+ // Check if update is available
82
+ const updateAvailable = await checkRemoteUpdate(name, skill);
83
+ if (!updateAvailable && !force) {
84
+ console.log(chalk.gray(` - ${chalk.bold(name)}: 已是最新版本 (v${skill.version})`));
85
+ skippedCount++;
86
+ continue;
87
+ }
88
+
89
+ // Stash local changes before pull
90
+ try {
91
+ const stashResult = spawnSync('git', ['stash'], {
92
+ cwd: skill.dir, stdio: 'pipe', encoding: 'utf-8', timeout: 10000,
93
+ });
94
+ const hasStash = stashResult.stdout?.includes('Saved') || false;
95
+
96
+ // Pull latest
97
+ console.log(chalk.gray(` ${chalk.bold(name)}: git pull...`));
98
+ const pullResult = spawnSync('git', ['pull', '--rebase', '--autostash'], {
99
+ cwd: skill.dir, stdio: 'pipe', encoding: 'utf-8', timeout: 30000,
100
+ });
101
+
102
+ if (pullResult.status !== 0) {
103
+ // Fallback to simple pull
104
+ const fallback = spawnSync('git', ['pull'], {
105
+ cwd: skill.dir, stdio: 'pipe', encoding: 'utf-8', timeout: 30000,
106
+ });
107
+ if (fallback.status !== 0) {
108
+ throw new Error(fallback.stderr?.split('\n')[0]?.slice(0, 100) || 'git pull 失败');
109
+ }
110
+ }
111
+
112
+ // Get new commit hash
113
+ const newCommit = getCommitHash(skill.dir);
114
+ const newManifestVersion = getUpdatedManifestVersion(skill.dir);
115
+
116
+ // Update config
117
+ installed[name].updated_at = new Date().toISOString();
118
+ installed[name].last_commit = newCommit;
119
+ if (newManifestVersion) {
120
+ installed[name].previous_version = skill.version;
121
+ installed[name].version = newManifestVersion;
122
+ }
123
+
124
+ // Run on_update lifecycle hook if manifest has one
125
+ const manifest = tryLoadManifest(skill.dir);
126
+ if (manifest?.lifecycle?.on_update) {
127
+ console.log(chalk.blue(` 🎣 执行更新钩子...`));
128
+ await runHook(skill.dir, manifest.lifecycle.on_update, 'on_update');
129
+ }
130
+
131
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
132
+ console.log(chalk.green(` ✓ ${chalk.bold(name)}: ${newManifestVersion ? `v${skill.version} → v${newManifestVersion}` : '已更新'}`));
133
+ successCount++;
134
+
135
+ } catch (err) {
136
+ // Attempt to restore stash
137
+ try {
138
+ spawnSync('git', ['stash', 'pop'], { cwd: skill.dir, stdio: 'ignore', timeout: 5000 });
139
+ } catch {}
140
+
141
+ console.log(chalk.red(` ✗ ${chalk.bold(name)}: ${err.message}`));
142
+ failCount++;
143
+ }
144
+ }
145
+
146
+ // ── Summary ──
147
+ console.log('');
148
+ if (checkOnly) return;
149
+
150
+ const total = successCount + failCount + skippedCount;
151
+ console.log(all
152
+ ? chalk.green(`✅ 更新完成: ${successCount} 成功, ${failCount} 失败, ${skippedCount} 跳过`)
153
+ : (failCount === 0
154
+ ? chalk.green(`✅ ${skillName} 已更新`)
155
+ : chalk.red(`❌ ${skillName} 更新失败`)));
156
+ }
157
+
158
+ // ─── Check-only mode ───
159
+ async function checkUpdates(targets, installed) {
160
+ console.log(chalk.blue('🔍 检查更新...'));
161
+ console.log('');
162
+
163
+ let hasUpdates = false;
164
+
165
+ for (const name of targets) {
166
+ const skill = installed[name];
167
+ if (!existsSync(skill.dir)) {
168
+ console.log(chalk.yellow(` ⚠ ${name}: 目录不存在`));
169
+ continue;
170
+ }
171
+
172
+ const localVersion = skill.version || '?';
173
+ const remoteVersion = await getRemoteVersion(name, skill);
174
+
175
+ if (remoteVersion && remoteVersion !== localVersion) {
176
+ console.log(chalk.green(` 📦 ${chalk.bold(name)}: v${localVersion} → v${remoteVersion}`));
177
+ hasUpdates = true;
178
+ } else {
179
+ console.log(chalk.gray(` ✓ ${chalk.bold(name)}: v${localVersion}(最新)`));
180
+ }
181
+ }
182
+
183
+ console.log('');
184
+ if (!hasUpdates) {
185
+ console.log(chalk.green('✅ 所有 Skill 已是最新版本'));
186
+ } else {
187
+ console.log(chalk.green(' 使用 agent update --all 更新全部'));
188
+ }
189
+ }
190
+
191
+ async function checkRemoteUpdate(name, skill) {
192
+ try {
193
+ const result = spawnSync('git', ['fetch', '--dry-run'], {
194
+ cwd: skill.dir, stdio: 'pipe', encoding: 'utf-8', timeout: 15000,
195
+ });
196
+ return result.stderr?.includes('->') || false;
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ async function getRemoteVersion(name, skill) {
203
+ try {
204
+ // Try to get version from remote agent.json
205
+ const rawUrl = `https://raw.githubusercontent.com/${skill.repo}/main/agent.json`;
206
+ // We'll use a simpler approach: fetch via git
207
+ const fetchResult = spawnSync('git', ['ls-remote', '--tags', `https://github.com/${skill.repo}.git`], {
208
+ stdio: 'pipe', encoding: 'utf-8', timeout: 15000,
209
+ });
210
+ const tags = fetchResult.stdout?.match(/v?\d+\.\d+\.\d+/g);
211
+ if (tags?.length) {
212
+ return tags.sort(semverSort).pop();
213
+ }
214
+ return null;
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+
220
+ // ─── Rollback ───
221
+ async function rollbackSkill(name, options, installed) {
222
+ if (!name) {
223
+ console.error(chalk.red('❌ 请指定要回滚的 Skill: agent update <name> --rollback'));
224
+ process.exit(1);
225
+ }
226
+
227
+ const skill = installed[name];
228
+ if (!skill) {
229
+ console.error(chalk.red(`❌ 未找到已安装的 Skill: ${name}`));
230
+ process.exit(1);
231
+ }
232
+
233
+ if (!existsSync(skill.dir)) {
234
+ console.error(chalk.red(`❌ 目录不存在: ${skill.dir}`));
235
+ process.exit(1);
236
+ }
237
+
238
+ // List recent git refs
239
+ console.log(chalk.blue(`🕒 查看 ${name} 的版本历史...`));
240
+ const logResult = spawnSync('git', ['log', '--oneline', '-10'], {
241
+ cwd: skill.dir, stdio: 'pipe', encoding: 'utf-8', timeout: 10000,
242
+ });
243
+ console.log(chalk.gray(logResult.stdout || '无历史记录'));
244
+
245
+ const targetRef = options.ref || options.to;
246
+ if (!targetRef) {
247
+ console.log(chalk.yellow(' 使用 --to <commit> 指定要回滚到的版本'));
248
+ process.exit(1);
249
+ }
250
+
251
+ console.log(chalk.blue(` git checkout ${targetRef}...`));
252
+ const result = spawnSync('git', ['checkout', targetRef], {
253
+ cwd: skill.dir, stdio: 'pipe', encoding: 'utf-8', timeout: 15000,
254
+ });
255
+
256
+ if (result.status === 0) {
257
+ console.log(chalk.green(`✅ ${name} 已回滚到 ${targetRef}`));
258
+ installed[name].rollback_ref = targetRef;
259
+ installed[name].updated_at = new Date().toISOString();
260
+ writeFileSync(CONFIG_PATH, JSON.stringify({ installed }, null, 2));
261
+ } else {
262
+ console.error(chalk.red(`❌ 回滚失败: ${result.stderr?.slice(0, 200)}`));
263
+ }
264
+ }
265
+
266
+ // ─── Helpers ───
267
+ function loadConfig() {
268
+ if (!existsSync(CONFIG_PATH)) return {};
269
+ try {
270
+ return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
271
+ } catch {
272
+ return {};
273
+ }
274
+ }
275
+
276
+ function getCommitHash(dir) {
277
+ try {
278
+ const result = spawnSync('git', ['rev-parse', '--short', 'HEAD'], {
279
+ cwd: dir, stdio: 'pipe', encoding: 'utf-8', timeout: 5000,
280
+ });
281
+ return result.stdout?.trim() || 'unknown';
282
+ } catch {
283
+ return 'unknown';
284
+ }
285
+ }
286
+
287
+ function getUpdatedManifestVersion(dir) {
288
+ try {
289
+ const path = join(dir, 'agent.json');
290
+ if (existsSync(path)) {
291
+ const manifest = JSON.parse(readFileSync(path, 'utf-8'));
292
+ return manifest.version || null;
293
+ }
294
+ } catch {}
295
+ return null;
296
+ }
297
+
298
+ function tryLoadManifest(dir) {
299
+ try {
300
+ const path = join(dir, 'agent.json');
301
+ if (existsSync(path)) {
302
+ return JSON.parse(readFileSync(path, 'utf-8'));
303
+ }
304
+ } catch {}
305
+ return null;
306
+ }
307
+
308
+ async function runHook(dir, hook, hookName) {
309
+ if (!hook) return;
310
+
311
+ if (hook.requires_approval) {
312
+ const answer = await ask(chalk.yellow(` 更新钩子: ${hook.description || hookName}\n 确认?[y/N] `));
313
+ if (answer.toLowerCase() !== 'y') {
314
+ console.log(chalk.gray(` 跳过 ${hookName}`));
315
+ return;
316
+ }
317
+ }
318
+
319
+ try {
320
+ if (hook.command) {
321
+ spawnSync(hook.command, { cwd: dir, shell: true, timeout: hook.timeout_ms || 30000, stdio: 'pipe', encoding: 'utf-8' });
322
+ }
323
+ } catch {}
324
+ }
325
+
326
+ function semverSort(a, b) {
327
+ const clean = v => v.replace(/^v/, '').split('.').map(Number);
328
+ const [a1, a2, a3] = clean(a);
329
+ const [b1, b2, b3] = clean(b);
330
+ return a1 - b1 || a2 - b2 || a3 - b3;
331
+ }
332
+
333
+ function ask(query) {
334
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
335
+ return new Promise(resolve => rl.question(query, answer => {
336
+ rl.close();
337
+ resolve(answer);
338
+ }));
339
+ }
@@ -0,0 +1,102 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import chalk from 'chalk';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const SCHEMA_PATH = join(__dirname, '..', 'data', 'manifest.schema.json');
8
+
9
+ export async function validateCommand(path) {
10
+ if (!existsSync(path)) {
11
+ console.error(chalk.red(`❌ 文件不存在: ${path}`));
12
+ process.exit(1);
13
+ }
14
+
15
+ console.log(chalk.blue(`🔍 校验: ${path}`));
16
+ console.log();
17
+
18
+ let manifest;
19
+ try {
20
+ const content = readFileSync(path, 'utf-8');
21
+ // Try JSON first, could also support YAML later
22
+ manifest = JSON.parse(content);
23
+ } catch (err) {
24
+ console.error(chalk.red(`❌ 解析失败: ${err.message}`));
25
+ process.exit(1);
26
+ }
27
+
28
+ // Load schema
29
+ let schema;
30
+ try {
31
+ schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf-8'));
32
+ } catch {
33
+ console.log(chalk.yellow(' ⚠ 无法加载 Schema 文件,执行基础校验...'));
34
+ basicValidate(manifest);
35
+ return;
36
+ }
37
+
38
+ // Dynamic import of ajv for JSON Schema validation
39
+ try {
40
+ const Ajv = (await import('ajv')).default;
41
+ const addFormats = (await import('ajv-formats')).default;
42
+ const ajv = new Ajv({ allErrors: true });
43
+ addFormats(ajv);
44
+ const validate = ajv.compile(schema);
45
+ const valid = validate(manifest);
46
+
47
+ if (valid) {
48
+ console.log(chalk.green('✅ Manifest 验证通过!'));
49
+ console.log();
50
+ printSummary(manifest);
51
+ } else {
52
+ console.log(chalk.red(`❌ 发现 ${validate.errors.length} 个问题:`));
53
+ for (const err of validate.errors) {
54
+ const path = err.instancePath || '/';
55
+ console.log(` ${chalk.red('✗')} ${path} ${err.message}`);
56
+ }
57
+ process.exit(1);
58
+ }
59
+ } catch (err) {
60
+ console.log(chalk.yellow(` ⚠ 高级校验不可用: ${err.message}`));
61
+ basicValidate(manifest);
62
+ }
63
+ }
64
+
65
+ function basicValidate(manifest) {
66
+ const errors = [];
67
+
68
+ if (!manifest.id) errors.push('缺少必需字段: id');
69
+ if (!manifest.name) errors.push('缺少必需字段: name');
70
+ if (!manifest.version) errors.push('缺少必需字段: version');
71
+ if (!manifest.capabilities || !Array.isArray(manifest.capabilities) || manifest.capabilities.length === 0) {
72
+ errors.push('必须至少声明一个 capability');
73
+ }
74
+
75
+ if (errors.length > 0) {
76
+ console.log(chalk.red(`❌ 发现 ${errors.length} 个问题:`));
77
+ for (const err of errors) {
78
+ console.log(` ${chalk.red('✗')} ${err}`);
79
+ }
80
+ process.exit(1);
81
+ }
82
+
83
+ console.log(chalk.green('✅ 基础验证通过!'));
84
+ printSummary(manifest);
85
+ }
86
+
87
+ function printSummary(manifest) {
88
+ console.log(` ${chalk.bold(manifest.name)} v${manifest.version}`);
89
+ console.log(` ID: ${chalk.cyan(manifest.id)}`);
90
+ console.log(` 能力数: ${chalk.green(manifest.capabilities?.length || 0)}`);
91
+
92
+ const caps = manifest.capabilities || [];
93
+ for (const cap of caps) {
94
+ const intentCount = cap.intents?.length || 0;
95
+ const hasInput = cap.input ? '✓' : '✗';
96
+ console.log(` ${chalk.cyan('·')} ${cap.name} (intents: ${intentCount}, input: ${hasInput})`);
97
+ }
98
+
99
+ if (manifest.trust?.permissions?.length) {
100
+ console.log(` 声明的权限: ${chalk.yellow(manifest.trust.permissions.join(', '))}`);
101
+ }
102
+ }