@migor/cli 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,9 +7,9 @@ import { RegistryError } from './registry.js';
7
7
  import { devServer } from './dev.js';
8
8
  import { doctor } from './doctor.js';
9
9
  import { addPackage } from './add.js';
10
- import { reportCommand } from './report.js';
11
- import { harvestCommand } from './harvest.js';
12
- import { diffCommand } from './diff.js';
10
+ import { reportCommand, USAGE as REPORT_USAGE } from './report.js';
11
+ import { harvestCommand, USAGE as HARVEST_USAGE } from './harvest.js';
12
+ import { diffCommand, USAGE as DIFF_USAGE } from './diff.js';
13
13
  const USAGE = `agentia —— Agentia 框架命令行工具
14
14
 
15
15
  用法:
@@ -27,6 +27,23 @@ const USAGE = `agentia —— Agentia 框架命令行工具
27
27
 
28
28
  name 规则:小写字母开头的小写 kebab-case(如 hello、doc-reviewer)
29
29
  `;
30
+ // 各子命令自己的用法串 —— 同时供 fail() 与「子命令级 --help」使用(单源,不写两遍)。
31
+ // report / harvest / diff 的用法串在各自模块里(那里本来就有),此处 import 复用。
32
+ const CREATE_USAGE = '用法:agentia create <name> [--dir <parent>]';
33
+ const G_USAGE = `用法:agentia g <type> <name>(type: ${CAPABILITY_TYPES.join(' | ')})`;
34
+ const ADD_USAGE = '用法:agentia add <pkg>(npm 包名或本地包路径)';
35
+ /** 子命令 → 自己的用法串。`--help` 落在子命令上时打它。 */
36
+ const SUB_USAGE = {
37
+ create: CREATE_USAGE,
38
+ g: G_USAGE,
39
+ generate: G_USAGE,
40
+ dev: '用法:agentia dev',
41
+ doctor: '用法:agentia doctor',
42
+ report: REPORT_USAGE,
43
+ harvest: HARVEST_USAGE,
44
+ diff: DIFF_USAGE,
45
+ add: ADD_USAGE,
46
+ };
30
47
  function fail(message) {
31
48
  console.error(`错误:${message}`);
32
49
  process.exitCode = 1;
@@ -43,6 +60,13 @@ function main(argv) {
43
60
  console.log(USAGE);
44
61
  return 0;
45
62
  }
63
+ // 子命令级 --help:此前 `agentia report --help` 会把 --help 当成「要读的文件名」,
64
+ // 用户拿到的是 ENOENT 而不是该命令的用法(report / harvest / diff 尤其需要)。
65
+ const subUsage = SUB_USAGE[command];
66
+ if (subUsage !== undefined && (rest[0] === '--help' || rest[0] === '-h')) {
67
+ console.log(subUsage);
68
+ return 0;
69
+ }
46
70
  if (command === 'create') {
47
71
  const name = rest[0];
48
72
  let parent;
@@ -58,7 +82,7 @@ function main(argv) {
58
82
  }
59
83
  }
60
84
  if (name === undefined)
61
- return fail('缺少项目名,用法:agentia create <name> [--dir <parent>]');
85
+ return fail(`缺少项目名,${CREATE_USAGE}`);
62
86
  if (!checkName(name)) {
63
87
  return fail(`非法项目名「${name}」:需匹配小写 kebab-case(如 my-app)`);
64
88
  }
@@ -67,7 +91,7 @@ function main(argv) {
67
91
  if (command === 'g' || command === 'generate') {
68
92
  const [type, name, ...extra] = rest;
69
93
  if (type === undefined || name === undefined || extra.length > 0) {
70
- return fail(`用法:agentia g <type> <name>(type: ${CAPABILITY_TYPES.join(' | ')})`);
94
+ return fail(G_USAGE);
71
95
  }
72
96
  if (!isCapabilityType(type)) {
73
97
  return fail(`未知能力类型「${type}」,可选:${CAPABILITY_TYPES.join(' | ')}`);
@@ -122,7 +146,7 @@ function main(argv) {
122
146
  if (command === 'add') {
123
147
  const [pkg, ...extra] = rest;
124
148
  if (pkg === undefined || extra.length > 0) {
125
- return fail('用法:agentia add <pkg>(npm 包名或本地包路径)');
149
+ return fail(ADD_USAGE);
126
150
  }
127
151
  try {
128
152
  return addPackage(pkg);
package/dist/create.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /** create 命令:生成 Agentia 项目脚手架 */
2
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { CAPABILITY_DIR_LIST, emptyRegistryTemplate, mainTs, projectDotEnv, projectDotEnvExample, projectGitignore, projectPackageJson, projectReadme, projectTsconfig, REGISTRY_PATH, toolIndexTs, } from './templates.js';
5
+ import { CAPABILITY_DIR_LIST, copyAssetsMjs, emptyRegistryTemplate, mainTs, projectDotEnv, projectDotEnvExample, projectGitignore, projectPackageJson, projectReadme, projectTsconfig, REGISTRY_PATH, toolIndexTs, } from './templates.js';
6
6
  import { registerCapability } from './registry.js';
7
7
  function write(dir, rel, content) {
8
8
  const file = join(dir, rel);
@@ -38,6 +38,14 @@ export function createProject(name, parent) {
38
38
  process.exitCode = 1;
39
39
  return 1;
40
40
  }
41
+ // 路径存在但不是目录时 readdirSync 会抛原始 ENOTDIR(栈里全是 node:fs 内部帧),
42
+ // 下面那句友好文案根本轮不到 —— 先判类型(`agentia create my-app` 而 my-app 是
43
+ // 个文件是常见手误)。
44
+ if (existsSync(dir) && !statSync(dir).isDirectory()) {
45
+ console.error(`错误:${dir} 已存在且不是目录`);
46
+ process.exitCode = 1;
47
+ return 1;
48
+ }
41
49
  if (existsSync(dir) && readdirSync(dir).length > 0) {
42
50
  console.error(`错误:目录 ${dir} 已存在且非空`);
43
51
  process.exitCode = 1;
@@ -48,6 +56,7 @@ export function createProject(name, parent) {
48
56
  write(dir, 'tsconfig.json', projectTsconfig());
49
57
  write(dir, 'src/main.ts', mainTs(name));
50
58
  write(dir, 'src/tools/hello/index.ts', toolIndexTs('hello'));
59
+ write(dir, 'scripts/copy-assets.mjs', copyAssetsMjs());
51
60
  write(dir, REGISTRY_PATH, emptyRegistryTemplate());
52
61
  write(dir, 'README.md', projectReadme(name));
53
62
  write(dir, '.gitignore', projectGitignore());
@@ -72,6 +81,8 @@ export function createProject(name, parent) {
72
81
  把 API key 填进 .env(已生成,且已被 .gitignore 忽略)
73
82
  npm run dev
74
83
 
84
+ 生产构建:npm run build && npm start(tsc → dist/,.md 资产由 scripts/copy-assets.mjs 跟随拷贝)
85
+
75
86
  目录约定:src/tools/ · src/skills/ · src/prompts/ · src/subagents/(一能力一文件夹)
76
87
 
77
88
  提示:项目内 AGENTS.md 是本框架的使用说明(API 速查 + 已知边界),
package/dist/diff.js CHANGED
@@ -216,7 +216,8 @@ function pushEventDiffs(out, x, y) {
216
216
  }
217
217
  }
218
218
  /* ── 命令本体:两个 JSONL 各取第一条 trace → diff → 人读输出 ──────────────────── */
219
- const USAGE = '用法:agentia diff <a.jsonl> <b.jsonl>';
219
+ /** 用法串(cli.ts 的子命令 `--help` 也从这里取,避免两处各写一份) */
220
+ export const USAGE = '用法:agentia diff <a.jsonl> <b.jsonl>';
220
221
  /** 逐行 JSON.parse + extractTrace(同 harvest),取第一条可提取的 trace;坏行跳过计数 */
221
222
  async function readFirstTrace(file) {
222
223
  let raw;
package/dist/harvest.js CHANGED
@@ -6,9 +6,11 @@
6
6
  * trace 的 llm.turn 重建),汇总成一个 eval TS 脚手架写到 --out(缺省 stdout)。
7
7
  *
8
8
  * ⚠️ 用例骨架生成器与框架侧 `src/eval/harvest.ts` 的 `harvestEvalCase` **同源同形** ——
9
- * CLI 零运行时依赖、不能 import 框架,此处是逐行移植(packages/cli/test/harvest.test.mjs
10
- * 里有一条「与框架产物逐字一致」的对拍,改生成格式必须两边同步)。
9
+ * CLI 零运行时依赖、不能 import 框架,此处是去类型移植(正文逐行相同,另有 3 处空值兜底:
10
+ * `trace.spans ?? []`、`startedAt ?? 0` ×2)。**产物**逐字一致由
11
+ * packages/cli/test/harvest.test.mjs 的对拍守着(改生成格式必须两边同步)。
11
12
  */
13
+ import { existsSync } from 'node:fs';
12
14
  import { readFile, writeFile } from 'node:fs/promises';
13
15
  import { extractTrace } from './report.js';
14
16
  /** trace 不记 assistant 文本 —— 脚本 text 块的统一占位(与框架侧同一个词) */
@@ -25,16 +27,26 @@ function stringifySafe(x) {
25
27
  }
26
28
  }
27
29
  function toolInputsOf(turn) {
28
- return (turn.events ?? [])
29
- .filter((e) => e.name === 'tool.input')
30
- .map((e) => {
30
+ const out = [];
31
+ for (const e of turn.events ?? []) {
32
+ if (e.name !== 'tool.input')
33
+ continue;
31
34
  const b = (e.body ?? {});
32
- return {
33
- tool: typeof b.tool === 'string' ? b.tool : 'unknown',
35
+ // 缺 `tool` 的事件**跳过**,不回填占位名(与框架侧 src/eval/harvest.ts 逐字同步):
36
+ // 伪造的 'unknown' 会在生成的脚本里变成一个真的(且断言必然通过的)工具调用。
37
+ if (typeof b.tool !== 'string')
38
+ continue;
39
+ out.push({
40
+ tool: b.tool,
34
41
  toolUseId: typeof b.tool_use_id === 'string' ? b.tool_use_id : undefined,
35
42
  input: b.input === undefined ? undefined : stringifySafe(b.input),
36
- };
37
- });
43
+ });
44
+ }
45
+ return out;
46
+ }
47
+ /** 注释里只放单行:外来 trace 的 name/source/traceId 含换行会击穿生成物的注释语法 */
48
+ function oneLine(s) {
49
+ return s.replace(/[\r\n]+/g, ' ');
38
50
  }
39
51
  function parseToolInput(raw) {
40
52
  const s = raw ?? '';
@@ -75,7 +87,7 @@ function lastUserText(messages) {
75
87
  /** 与框架侧 harvestEvalCase 逐字同形的生成器(见文件头注释的对拍测试) */
76
88
  export function harvestEvalCase(input) {
77
89
  const { trace } = input;
78
- const name = input.name ?? `harvest-${trace.traceId}`;
90
+ const name = oneLine(input.name ?? `harvest-${trace.traceId}`);
79
91
  const spans = trace.spans ?? [];
80
92
  const mainTurns = spans
81
93
  .filter((s) => s.kind === 'llm.turn' && s.parentSpanId === trace.rootSpanId)
@@ -106,7 +118,7 @@ export function harvestEvalCase(input) {
106
118
  const toolSeq = mainTurns.flatMap((t) => toolInputsOf(t).map((e) => e.tool));
107
119
  const userText = lastUserText(input.messages);
108
120
  const head = [
109
- `// ┄┄ harvest 用例骨架:${name}(trace ${trace.traceId}${input.source ? `,来源 ${input.source}` : ''})┄┄`,
121
+ `// ┄┄ harvest 用例骨架:${name}(trace ${oneLine(trace.traceId ?? '')}${input.source ? `,来源 ${oneLine(input.source)}` : ''})┄┄`,
110
122
  '// ⚠️ 脚手架,不是成品 —— 人工核对后再进 CI:',
111
123
  '// · trace 不记 assistant 文本(llm.turn 只记 usage/事件),脚本里的 text 块是占位;',
112
124
  ];
@@ -156,7 +168,8 @@ function toRow(v, index) {
156
168
  label: t.traceId ?? `row-${index + 1}`,
157
169
  };
158
170
  }
159
- const USAGE = '用法:agentia harvest <trace.jsonl> [--out <file.ts>] [--failed] [--limit N]';
171
+ /** 用法串(cli.ts 的子命令 `--help` 也从这里取,避免两处各写一份) */
172
+ export const USAGE = '用法:agentia harvest <trace.jsonl> [--out <file.ts>] [--force] [--failed] [--limit N]';
160
173
  function renderEvalFile(opts) {
161
174
  const note = `来源:${opts.file},${opts.cases.length} 条记录${opts.onlyFailed ? '(仅失败)' : ''}` +
162
175
  (opts.badLines > 0 ? `,跳过无法解析 ${opts.badLines} 行` : '');
@@ -196,12 +209,16 @@ export async function harvestCommand(args) {
196
209
  let file;
197
210
  let out;
198
211
  let onlyFailed = false;
212
+ let force = false;
199
213
  let limit;
200
214
  for (let i = 0; i < args.length; i += 1) {
201
215
  const a = args[i];
202
216
  if (a === '--failed') {
203
217
  onlyFailed = true;
204
218
  }
219
+ else if (a === '--force') {
220
+ force = true;
221
+ }
205
222
  else if (a === '--out') {
206
223
  out = args[i + 1];
207
224
  if (out === undefined)
@@ -270,6 +287,11 @@ export async function harvestCommand(args) {
270
287
  process.stdout.write(content);
271
288
  }
272
289
  else {
290
+ // 产物是「人工核对后再进 CI」的脚手架,它的价值恰恰在用户手改过的断言与 input 上:
291
+ // 重跑一次就静默抹掉等于毁掉那份人工成果,所以默认拒绝覆盖(要覆盖显式 --force)。
292
+ if (!force && existsSync(out)) {
293
+ throw new Error(`${out} 已存在(产物需人工核对,默认不覆盖);要覆盖请加 --force\n${USAGE}`);
294
+ }
273
295
  await writeFile(out, content, 'utf8');
274
296
  console.log(`已写出 ${out}:${picked.length} 个用例骨架` +
275
297
  (onlyFailed ? '(仅失败记录)' : '') +
package/dist/report.js CHANGED
@@ -30,12 +30,14 @@ export function extractTrace(v) {
30
30
  return null;
31
31
  }
32
32
  /** 打印一张按总耗时降序的能力排行(跨多行记录时按能力合并) */
33
+ /** 用法串(cli.ts 的子命令 `--help` 也从这里取,避免两处各写一份) */
34
+ export const USAGE = '用法:agentia report <trace.jsonl>';
33
35
  export async function reportCommand(args) {
34
36
  const file = args[0];
35
37
  // 失败一律**抛错**(而不是就地设 process.exitCode)—— 本命令是异步的,
36
38
  // 就地设的 exitCode 会被 cli.ts 末尾那句 `process.exitCode = main(...)` 覆盖成 0。
37
39
  if (file === undefined || args.length > 1) {
38
- throw new Error('用法:agentia report <trace.jsonl>');
40
+ throw new Error(USAGE);
39
41
  }
40
42
  let raw;
41
43
  try {
@@ -66,7 +68,17 @@ export async function reportCommand(args) {
66
68
  (badLines > 0 ? `;另有 ${badLines} 行无法解析` : ''));
67
69
  }
68
70
  // 聚合口径与 inspector 面板 / 官网 playground 同源(构建期拷进来的 trace-view 产物)
69
- const { summarizeTrace } = (await import(new URL('./inspector/summary.js', import.meta.url).href));
71
+ // 这份聚合实现由 packages/cli/scripts/copy-assets.mjs 在构建期从 trace-view 产物拷进来。
72
+ // 缺它只可能是「装坏了」或「直接跑未构建的源码」—— 原始 ERR_MODULE_NOT_FOUND 里全是
73
+ // dist 内部路径,用户读不出该做什么,故换成带补救动作的报错。
74
+ let summarizeTrace;
75
+ try {
76
+ ({ summarizeTrace } = (await import(new URL('./inspector/summary.js', import.meta.url).href)));
77
+ }
78
+ catch (e) {
79
+ throw new Error(`缺少 CLI 资源(inspector/summary.js):${e.message};` +
80
+ '请重装 @migor/cli 或重新构建(npm run build:cli)');
81
+ }
70
82
  const merged = new Map();
71
83
  let okRuns = 0;
72
84
  for (const t of traces) {
package/dist/templates.js CHANGED
@@ -58,9 +58,11 @@ export function projectPackageJson(name) {
58
58
  type: 'module',
59
59
  scripts: {
60
60
  dev: 'tsx src/main.ts',
61
+ build: 'tsc -p tsconfig.json && node scripts/copy-assets.mjs',
62
+ start: 'node dist/main.js',
61
63
  typecheck: 'tsc --noEmit -p tsconfig.json',
62
64
  },
63
- dependencies: { '@migor/agentia': '^0.6.0' },
65
+ dependencies: { '@migor/agentia': '^0.6.1' },
64
66
  devDependencies: {
65
67
  tsx: '^4.19.0',
66
68
  typescript: '^7.0.2',
@@ -76,6 +78,11 @@ export function projectTsconfig() {
76
78
  moduleResolution: 'NodeNext',
77
79
  lib: ['ES2022'],
78
80
  strict: true,
81
+ // 编译产物落 dist/(npm run build → npm start 跑 dist/main.js);
82
+ // .md 文本资产由 scripts/copy-assets.mjs 跟随同相对路径拷过去
83
+ // (asset(import.meta.url, './x.md') 按**文件位置**解析,.md 必须跟着 .js 走)
84
+ rootDir: 'src',
85
+ outDir: 'dist',
79
86
  // 框架零运行时依赖(不再有厂商 SDK 经传递链把 @types/node 带进编译程序),
80
87
  // node 全局类型必须显式声明
81
88
  types: ['node'],
@@ -161,6 +168,13 @@ ANTHROPIC_API_KEY=sk-ant-...
161
168
  npm run dev -- "你的问题"
162
169
  \`\`\`
163
170
 
171
+ ## 构建与生产运行
172
+
173
+ \`\`\`bash
174
+ npm run build # tsc → dist/ + .md 文本资产跟随拷贝(asset() 按文件位置解析,必须跟着 .js 走)
175
+ npm start -- "你的问题" # 跑编译产物 dist/main.js(部署/Docker 用这条)
176
+ \`\`\`
177
+
164
178
  也可以用环境变量(适合 CI / 容器)——**真实环境变量优先,不会被 \`.env\` 覆盖**:
165
179
 
166
180
  \`\`\`bash
@@ -198,6 +212,36 @@ ANTHROPIC_API_KEY=
198
212
  # AGENTIA_MODEL=claude-opus-5
199
213
  `;
200
214
  }
215
+ /** 脚手架的 scripts/copy-assets.mjs:把 src 下的 .md 文本资产拷进 dist/(同相对路径) */
216
+ export function copyAssetsMjs() {
217
+ return `// 把能力文件夹里的 .md 文本资产拷进 dist/(与编译产物同相对路径)。
218
+ // asset(import.meta.url, './x.md') 按**文件位置**解析,所以 .md 必须跟着 .js 走。
219
+ import { cpSync, readdirSync } from 'node:fs';
220
+ import { join } from 'node:path';
221
+ import { fileURLToPath } from 'node:url';
222
+
223
+ const here = fileURLToPath(new URL('.', import.meta.url));
224
+ const srcDir = join(here, '..', 'src');
225
+ const outDir = join(here, '..', 'dist');
226
+
227
+ function walk(dir) {
228
+ const out = [];
229
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
230
+ const p = join(dir, e.name);
231
+ if (e.isDirectory()) out.push(...walk(p));
232
+ else if (e.name.endsWith('.md')) out.push(p);
233
+ }
234
+ return out;
235
+ }
236
+
237
+ let n = 0;
238
+ for (const f of walk(srcDir)) {
239
+ cpSync(f, join(outDir, f.slice(srcDir.length + 1)));
240
+ n++;
241
+ }
242
+ console.log(\`[copy-assets] \${n} 个 .md 资产 → dist/\`);
243
+ `;
244
+ }
201
245
  // ---------- g 命令能力模板 ----------
202
246
  export function toolIndexTs(name) {
203
247
  const cls = kebabToPascal(name);
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@migor/cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Agentia 框架命令行工具:create 脚手架 + g 能力生成 + dev 本地可观测面板(trace inspector)",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "agentia": "./dist/cli.js"
8
8
  },
9
9
  "files": [
10
- "dist"
10
+ "dist",
11
+ "CHANGELOG.md"
11
12
  ],
12
13
  "repository": "retrychx/agentia",
13
14
  "homepage": "https://agentia-web.pages.dev",