@migor/cli 0.2.2 → 0.3.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/dist/AGENTS.md CHANGED
@@ -70,9 +70,9 @@ console.log(result.finalText, result.stopReason, result.trace.totalUsage);
70
70
  ## 2. 项目结构(CLI 约定)
71
71
 
72
72
  ```bash
73
- npx @migor/cli create my-app # 脚手架
73
+ npx @migor/cli create my-app # 脚手架(含 .env / .env.example)
74
74
  cd my-app && npm install
75
- export ANTHROPIC_API_KEY=sk-ant-...
75
+ $EDITOR .env # 填 ANTHROPIC_API_KEY(脚手架已生成,且已被 .gitignore 挡住)
76
76
  npx @migor/cli dev # tsx watch + 本地 inspector 面板
77
77
  npx @migor/cli g tool fetch-weather # 生成到 src/tools/fetch-weather/(skill/prompt/subagent 同理)
78
78
  npx @migor/cli doctor # 静态体检(未登记/悬空/命名/重复)
@@ -283,6 +283,31 @@ result.typed; // { answer: string } | undefined
283
283
  | `isSuccessStopReason` | `end_turn` / `stop_sequence` 都算正常收尾 |
284
284
  | `resolveDefaultModel` | 显式 > `AGENTIA_MODEL` > `claude-opus-5` |
285
285
 
286
+ ### 环境变量与 `.env`(`loadEnvFile`)
287
+
288
+ | API | 说明 |
289
+ |---|---|
290
+ | `loadEnvFile` | 读一份 `.env` 进 `process.env`,返回**本次真正生效**的键;选项 `LoadEnvOptions`:`{ path?, override? }` |
291
+
292
+ ```ts
293
+ import { createApp, loadEnvFile } from '@migor/agentia';
294
+
295
+ loadEnvFile(); // 缺省 cwd/.env;文件不存在 = 静默返回 {}(首次 clone、CI 的正常路径)
296
+ const app = await createApp({ ... });
297
+ ```
298
+
299
+ 三条语义(都刻意,别当成实现细节):
300
+
301
+ - **框架不自动读 `.env`** —— 读哪个文件、什么时候读是宿主的启动决策。塞进 `createApp` 里自动做,会让「同一份代码换个目录跑结果不同」变成要花时间排查的悬案;而放 CLI 里只有 `agentia dev` 生效。写在**你的** `main.ts` 里,`node dist/main.js`、docker、别的宿主都一样读得到。
302
+ - **真实环境变量优先**(缺省不覆盖):`process.env` 里已定义(哪怕空串)的键保持不动 —— CI / docker / `FOO=bar npm start` 永远赢过文件。要让文件里的值压过环境变量就 `loadEnvFile({ override: true })`。
303
+ - **想知道「生效没」看返回值**,别去看文件:被挡下的键不在返回对象里。
304
+
305
+ `agentia create` 生成的脚手架把 `.env`、`.env.example` 与 `main.ts` 首行的 `loadEnvFile();` 都备好了,并在 `.gitignore` 里挡住 `.env` —— 生成 `.env` 却不 ignore,等于把 key 送进用户的第一个 commit。
306
+
307
+ > ⚠️ **本机 export 过 `ANTHROPIC_API_KEY` 的人**(比如同时用 Claude Code):按上面的优先级,脚手架 `.env` 里的 key 会被**静默压住**。改了 `.env` 却「没生效」时,先 `echo $ANTHROPIC_API_KEY` 看看环境里是不是已经有一份。
308
+
309
+ 解析规则(刻意窄,够用就好):`KEY=VALUE`,允许 `export ` 前缀与 `=` 两侧空白;`#` 整行注释;单引号内原样、双引号内认 `\n \r \t \" \\`;未加引号的值里 ` #` 起为行内注释。**不做变量展开、不合并续行**(需要就上专门的库);既不像 `KEY=VALUE` 又不是注释的行**直接报错并指出行号** —— 静默跳过等于让你以为「配上了其实没配上」。
310
+
286
311
  ### 宿主(换宿主不换语义)
287
312
 
288
313
  | API | 说明 |
@@ -320,14 +345,16 @@ const callable = { name: app.name, run: (msgs, opts) => app.run(msgs, { ...opts,
320
345
  const handler = createHttpHandler(callable, { runner });
321
346
  ```
322
347
 
323
- 异步侧更直接:`AsyncRunner` 的构造选项就有 `client`。完整可跑写法见仓库 `examples/complete/`。
348
+ 异步侧更直接:`AsyncRunner` 的构造选项就有 `client`。完整可跑写法见仓库 `examples/complete/`
349
+ (**不随 npm 包发布** —— 包里只有 `dist/`、README、LICENSE 与本说明):
350
+ <https://github.com/retrychx/agentia/tree/main/examples/complete>
324
351
 
325
352
  ### `createHttpHandler(app, opts?: HttpHandlerOptions)`
326
353
 
327
354
  | 选项 | 说明 |
328
355
  |---|---|
329
356
  | `authenticate` | 入口鉴权钩子:**除 `/healthz` 与 `/metrics` 外所有路径**都过它,且在**读 body 之前**(未通过就不收 body)。正常返回即通过;抛 `HttpException` 按其 `status`/`body` 回;抛别的错误回 401,原文只进服务端日志。框架**不实现策略**(不读 env、不碰凭据) |
330
- | `metrics` | 指标出口:给 `metricsSink()`(或任意 `{ render() }` / 返回字符串的闭包)后,`GET /metrics` 回它的 Prometheus 文本。**不鉴权**(拉取端在集群内网);要保护请放反代后面。不给则该路径 404 |
357
+ | `metrics` | 指标出口:给 `metricsSink()`(或任意 `{ render() }` / 返回字符串的闭包)后,`GET /metrics` 回它的 Prometheus 文本。**不鉴权**(拉取端在集群内网);要保护请放反代后面。不给则该路径 404。⚠️ 这只管**渲染** —— 数字要真的累计,必须把**同一个** sink 注册进 `createApp({ sinks: [metrics] })`(它靠 run 收尾投递,不自己埋点),否则 `/metrics` 恒为 0 **且不报错** |
331
358
  | `maxBodyBytes` | 请求 body 上限(字节),超限回 413;缺省 1 MiB |
332
359
  | `maxConcurrentRuns` | 同时在跑的 `POST /run` 上限,超限回 503 + `Retry-After`;缺省 32(传 `Infinity` 恢复无上限) |
333
360
  | `exposeErrors` | 是否把内部异常原文回给调用方;缺省 `false`(细节只进服务端日志) |
@@ -613,6 +640,22 @@ CLI 侧有薄壳:`agentia report <trace.jsonl>` —— 每行一个 JSON(裸
613
640
  > 要看分位请用 `mergeRunReports` 汇总多条,或用 `metricsSink` 的直方图。
614
641
  > CLI 报告的聚合口径与 `agentia dev` 面板的能力排行同源(同一份 `@migor/trace-view` 实现)。
615
642
 
643
+ 那个 jsonl 从哪来 —— 框架不替你落盘(观测出口是缝),自己接一个 sink 就行,零依赖:
644
+
645
+ ```ts
646
+ import { appendFileSync } from 'node:fs';
647
+ import { createApp, type TraceSink } from '@migor/agentia';
648
+
649
+ const jsonl: TraceSink = {
650
+ export: (trace) => appendFileSync('trace.jsonl', `${JSON.stringify(trace)}\n`, 'utf8'),
651
+ };
652
+ const app = await createApp({ /* … */ sinks: [jsonl] });
653
+ // 之后:agentia report trace.jsonl
654
+ ```
655
+
656
+ 异步宿主更省事:把 `FileTaskStore` 的落盘文件直接喂给它 —— `TaskRecord` 里带 `result.trace`,
657
+ `report` 认这种形态,不用另写 sink。
658
+
616
659
  ### 生效配置快照(「这条 run 用了哪套旋钮」)
617
660
 
618
661
  每个 run 的**根 span** 都带一组 `config.*` attributes(`config.maxTokens` / `config.maxCostUsd` /
@@ -739,10 +782,10 @@ const callable = {
739
782
  | 能力引用是 provider 粒度 | 子 agent / skill 的 `tools` 写的是 **provider token**,不是单个工具名 |
740
783
  | 取消要传进客户端才有效 | 传 `signal` 后框架会 abort 在飞请求(内置 Anthropic / OpenAI 适配器都转发);不转发 `signal` 的自定义 `ModelClient` 只能「放弃等待」(请求在后台跑完、产物丢弃) |
741
784
  | 观测失败被吞 | sink 抛错不影响 run(观测是辅助动作);同理记忆水合/回写失败也不击穿 run |
742
- | 框架不读 env | 除 `AGENTIA_MODEL`(缺省模型覆盖)与 `OPENAI_API_KEY`(OpenAI 适配器)外不读环境变量;不含 dev 逻辑 |
785
+ | 框架不自动读 .env | 除 `AGENTIA_MODEL`(缺省模型覆盖)与 `OPENAI_API_KEY`(OpenAI 适配器)外,框架自己不去翻环境变量,也不读 `.env`;要读就在启动代码里调 `loadEnvFile()`(脚手架已内置那行),**真实环境变量优先**于文件 |
743
786
  | 鉴权只是缝 | 框架**不实现** token / JWT / 签名策略,也不碰凭据 env —— `authenticate` 只承诺「拦在入口、读 body 之前」;策略是宿主或反代的事 |
744
787
  | 运行时是 Node | 按 Node ≥ 18 设计与测试(`engines` 写明,CI 在 18/20/22 上守);**未对 Deno / edge 做验证**。`SqliteTaskStore` 需 Node ≥ 22.5(`node:sqlite`),未提供时构造期抛可读报错 |
745
- | 停机不由框架触发 | 框架给 `drain()` 但**不订阅** `SIGTERM`/`SIGINT`(不读 env、不做进程级决策);信号处理是宿主的 |
788
+ | 停机不由框架触发 | 框架给 `drain()` 但**不订阅** `SIGTERM`/`SIGINT`(不做进程级决策);信号处理是宿主的 |
746
789
  | 停机可能切断 SSE | `drain()` 超时后会强制关闭仍开着的 SSE 流,其 run 以 `stopReason='aborted'` 收尾 —— 客户端应把断流当作可重试 |
747
790
  | 鉴权失败即断连 | 未通过鉴权时在读到 body 之前就回响应,连接**不可复用**(显式 `connection: close`);这是「不收body省资源」的代价 |
748
791
  | 预算护栏不是硬实时 | 一回合记账完才判,实际用量可能超上限一个回合的量;模型自然收尾的那回合超限**不算失败**(只留 `budget.exceeded` 事件) |
package/dist/cli.js CHANGED
File without changes
package/dist/create.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, 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, projectGitignore, projectPackageJson, projectReadme, projectTsconfig, REGISTRY_PATH, toolIndexTs, } from './templates.js';
5
+ import { CAPABILITY_DIR_LIST, 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);
@@ -51,11 +51,16 @@ export function createProject(name, parent) {
51
51
  write(dir, REGISTRY_PATH, emptyRegistryTemplate());
52
52
  write(dir, 'README.md', projectReadme(name));
53
53
  write(dir, '.gitignore', projectGitignore());
54
- // 把四个分类目录都建出来:目录名自解释,用户一眼知道「新能力往哪放」
54
+ // 四个分类目录都建出来:目录名自解释,用户一看就知道新能力往哪放
55
55
  // (.gitkeep 让空目录能进版本库;discover 只认目录,会忽略它)
56
56
  for (const relDir of CAPABILITY_DIR_LIST) {
57
57
  write(dir, `${relDir}/.gitkeep`, '');
58
58
  }
59
+ // .env 是「填上就能跑」的入口(main.ts 首行 loadEnvFile() 读它);
60
+ // .env.example 进版本库当变量清单。**两者必须与 gitignore 的 .env 同时存在** ——
61
+ // 生成 .env 却不忽略它,等于把 key 直接送进用户的第一个 commit。
62
+ write(dir, '.env', projectDotEnv());
63
+ write(dir, '.env.example', projectDotEnvExample());
59
64
  // AI 使用说明:让 Claude Code / Cursor / Copilot 等一进项目就拿到权威 API 速查
60
65
  write(dir, 'AGENTS.md', guide);
61
66
  registerCapability(dir, 'hello', 'tool');
@@ -64,7 +69,7 @@ export function createProject(name, parent) {
64
69
  后续步骤:
65
70
  cd ${dir}
66
71
  npm install
67
- export ANTHROPIC_API_KEY=sk-ant-...
72
+ 把 API key 填进 .env(已生成,且已被 .gitignore 忽略)
68
73
  npm run dev
69
74
 
70
75
  目录约定:src/tools/ · src/skills/ · src/prompts/ · src/subagents/(一能力一文件夹)
package/dist/templates.js CHANGED
@@ -60,7 +60,7 @@ export function projectPackageJson(name) {
60
60
  dev: 'tsx src/main.ts',
61
61
  typecheck: 'tsc --noEmit -p tsconfig.json',
62
62
  },
63
- dependencies: { '@migor/agentia': '^0.2.2' },
63
+ dependencies: { '@migor/agentia': '^0.3.0' },
64
64
  devDependencies: {
65
65
  tsx: '^4.19.0',
66
66
  typescript: '^7.0.2',
@@ -83,7 +83,12 @@ export function projectTsconfig() {
83
83
  }, null, 2)}\n`;
84
84
  }
85
85
  export function mainTs(name) {
86
- return `import { createApp, SystemPrompt } from '@migor/agentia';
86
+ return `import { createApp, loadEnvFile, SystemPrompt } from '@migor/agentia';
87
+
88
+ // 读同目录的 .env(key 写文件里即可,不必每次 export)。框架**不自动**读 .env ——
89
+ // 读哪个文件、什么时候读由这里决定;已存在的真实环境变量优先,不会被文件覆盖。
90
+ // 想换路径/顺序:loadEnvFile({ path: '.env.local' }) 或直接删掉这一行改用自己的加载器。
91
+ loadEnvFile();
87
92
 
88
93
  const app = await createApp({
89
94
  name: '${name}',
@@ -99,7 +104,8 @@ const { result } = await app.run(
99
104
  // 「打印一行空白 + 退出 0」,让首次运行(比如忘了配 ANTHROPIC_API_KEY)看起来像成功。
100
105
  if (result.error) {
101
106
  console.error(\`run 失败(stopReason=\${result.stopReason}):\${result.error.message}\`);
102
- console.error('提示:模型调用默认读 ANTHROPIC_API_KEY;换端点或注入 client 见项目内 AGENTS.md。');
107
+ console.error('提示:模型调用读 ANTHROPIC_API_KEY —— 填进 .env(首行 loadEnvFile() 会读)或 export 均可;');
108
+ console.error(' 换端点 / 注入自定义 client 见项目内 AGENTS.md。');
103
109
  process.exitCode = 1;
104
110
  }
105
111
  if (result.finalText) console.log(result.finalText);
@@ -141,17 +147,52 @@ agentia g subagent my-agent # → src/subagents/my-agent/(含 system.md)
141
147
 
142
148
  ## 运行
143
149
 
144
- 需要 Anthropic API key:
150
+ 需要 Anthropic API key —— 填进脚手架已生成的 \`.env\` 即可(本文件在 \`.gitignore\` 里):
151
+
152
+ \`\`\`bash
153
+ # .env
154
+ ANTHROPIC_API_KEY=sk-ant-...
155
+ \`\`\`
156
+
157
+ \`\`\`bash
158
+ npm run dev -- "你的问题"
159
+ \`\`\`
160
+
161
+ 也可以用环境变量(适合 CI / 容器)——**真实环境变量优先,不会被 \`.env\` 覆盖**:
145
162
 
146
163
  \`\`\`bash
147
164
  export ANTHROPIC_API_KEY=sk-ant-...
148
165
  npm run dev -- "你的问题"
149
166
  \`\`\`
167
+
168
+ \`.env\` 由 \`src/main.ts\` 首行的 \`loadEnvFile()\` 读取。框架**不会自动读**它 ——
169
+ 读哪个文件、什么时候读由你的启动代码决定(这样「换目录跑」不会悄悄改变行为)。
150
170
  `;
151
171
  }
152
172
  export function projectGitignore() {
153
173
  return `node_modules
154
174
  dist
175
+
176
+ # 本地环境变量(可能含 key)—— 绝不提交
177
+ .env
178
+ .env.local
179
+ `;
180
+ }
181
+ /** 脚手架生成的 .env:填上就能跑,已进 .gitignore */
182
+ export function projectDotEnv() {
183
+ return `# 模型 API key —— 填上后 npm run dev / npm start 直接可用
184
+ # 本文件已被 .gitignore 忽略:不要提交,也不要把 key 写进 README / AGENTS.md
185
+ ANTHROPIC_API_KEY=
186
+ `;
187
+ }
188
+ /** 随脚手架提交的 .env.example:进版本库的变量清单,值一律留空 */
189
+ export function projectDotEnvExample() {
190
+ return `# 复制成 .env 再填值(.env 已 gitignore,不要提交)
191
+ ANTHROPIC_API_KEY=
192
+
193
+ # 可选
194
+ # ANTHROPIC_BASE_URL=https://api.anthropic.com
195
+ # AGENTIA_MODEL=claude-opus-5
155
196
  `;
156
197
  }
157
198
  // ---------- g 命令能力模板 ----------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@migor/cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Agentia 框架命令行工具:create 脚手架 + g 能力生成 + dev 本地可观测面板(trace inspector)",
5
5
  "type": "module",
6
6
  "bin": {