@migor/cli 0.10.3 → 0.10.4

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.
@@ -110,6 +110,13 @@ const STATIC = new Map([
110
110
  ['fromTrace.js', join(ASSETS, 'fromTrace.js')],
111
111
  ['summary.js', join(ASSETS, 'summary.js')],
112
112
  ['trace-view.css', join(ASSETS, 'trace-view.css')],
113
+ /* 单文件 bundle(`agentia view` 内联进自包含 HTML 用)。⚠️ **页面不 import 它** ——
114
+ * 面板仍走上面的分文件;它是 CLI 自己 readFile 后内联的(`src/view.ts`)。
115
+ * 之所以还列进白名单:copy-assets 是**整树**拷 trace-view 的 dist ⇒ 它必然出现在
116
+ * dist/inspector/,而 `structure.test.mjs` 的 W5 要求「dist/inspector 的每个文件都在
117
+ * 白名单里」(防 trace-view 拆模块后浏览器 404 白屏)。与其放宽那条守卫的安全语义,
118
+ * 不如显式批一个本来就公开的前端渲染器文件。 */
119
+ ['trace-view.bundle.js', join(ASSETS, 'trace-view.bundle.js')],
113
120
  ['panel-logic.js', join(HERE, 'panel-logic.js')],
114
121
  // 面板的 Markdown 解析器(零依赖、按构造不产生 HTML —— 见 src/markdown.ts 的头注)。
115
122
  // 与 panel-logic.js 同处 dist 根:两个都是 CLI 自己编译出来的模块,单测直接 import 它们。
package/dist/redact.js ADDED
@@ -0,0 +1,82 @@
1
+ /** `keys` 命中的字段整体抹成的文案(不看值,最严的一档) */
2
+ export const REDACTED = '[REDACTED]';
3
+ /**
4
+ * 内置正则预设(脱敏面 = agent 服务最常见的四类泄密:LLM key / 令牌 / PII)。
5
+ * 每条预设的替换文案带**类别标签**(`[REDACTED:email]`)—— 让「这里被改过、改的是哪类」
6
+ * 在 trace 里可见:静默替换会让下游排查误以为数据本来如此。
7
+ * 预设按数组序依次应用(bearer 先于 jwt —— `Bearer eyJ…` 整体归 bearer,不被 jwt 截走)。
8
+ *
9
+ * ⚠️ 与 `examples/observability/src/index.ts` 的 `REDACT_PRESETS` **逐字同形**,
10
+ * 由 `packages/cli/test/view.test.mjs` 的对拍守着(改任一处必须两边同步)。
11
+ */
12
+ export const REDACT_PRESETS = [
13
+ { name: 'bearer', re: /\bBearer\s+[A-Za-z0-9\-._~+/]+=*/g },
14
+ { name: 'jwt', re: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g },
15
+ { name: 'aws-access-key', re: /\bAKIA[0-9A-Z]{16}\b/g },
16
+ { name: 'llm-api-key', re: /\bsk-[A-Za-z0-9]{16,}\b/g },
17
+ { name: 'email', re: /[\w.+-]+@[\w-]+\.[\w.]+/g },
18
+ { name: 'phone-cn', re: /(?<!\d)1[3-9]\d{9}(?!\d)/g },
19
+ ];
20
+ function hitKey(key, keys) {
21
+ const k = key.toLowerCase();
22
+ return keys.some((needle) => k.includes(needle.toLowerCase()));
23
+ }
24
+ /** 应用一组「正则 → 替换文案」规则;规则 regex 带 /g,跨字符串复用前必须重置 lastIndex */
25
+ function applyRules(s, rules) {
26
+ let out = s;
27
+ for (const { re, label } of rules) {
28
+ re.lastIndex = 0;
29
+ out = out.replace(re, label);
30
+ }
31
+ return out;
32
+ }
33
+ /** 递归深拷贝 + 抹值:原 trace 不被改动(调用方可能还要拿原文) */
34
+ function redactValue(value, keys, rules) {
35
+ if (typeof value === 'string')
36
+ return applyRules(value, rules);
37
+ if (value === null || typeof value !== 'object')
38
+ return value;
39
+ if (Array.isArray(value))
40
+ return value.map((v) => redactValue(v, keys, rules));
41
+ const out = {};
42
+ for (const [k, v] of Object.entries(value)) {
43
+ out[k] = hitKey(k, keys) ? REDACTED : redactValue(v, keys, rules);
44
+ }
45
+ return out;
46
+ }
47
+ function buildRules(presets) {
48
+ if (presets === false)
49
+ return [];
50
+ const selected = presets ?? REDACT_PRESETS.map((p) => p.name);
51
+ return REDACT_PRESETS.filter((p) => selected.includes(p.name)).map((p) => ({ re: p.re, label: `[REDACTED:${p.name}]` }));
52
+ }
53
+ /** 预设名(供产物里「已脱敏 · 覆盖 X 类」那句话用) */
54
+ export function activePresetNames(presets) {
55
+ return presets === false ? [] : (presets ?? REDACT_PRESETS.map((p) => p.name));
56
+ }
57
+ /**
58
+ * 脱敏一条 trace(**返回副本**,不改入参)。
59
+ *
60
+ * 规则顺序:预设(默认全开)→ 字段级 `keys`(后者整字段抹平,最严)。
61
+ */
62
+ export function redactTrace(trace, opts = {}) {
63
+ const keys = opts.keys ?? [];
64
+ const rules = buildRules(opts.presets);
65
+ const spans = (trace.spans ?? []).map((span) => ({
66
+ ...span,
67
+ attributes: redactValue(span.attributes, keys, rules),
68
+ events: (span.events ?? []).map((e) => ({ ...e, body: redactValue(e.body, keys, rules) })),
69
+ ...(span.error
70
+ ? { error: { ...span.error, message: applyRules(span.error.message ?? '', rules) } }
71
+ : {}),
72
+ }));
73
+ return {
74
+ ...trace,
75
+ totalUsage: trace.totalUsage ? { ...trace.totalUsage } : trace.totalUsage,
76
+ spans,
77
+ // 见文件头注:比 examples 多覆盖的一层(手写 trace 可能有顶层 events)
78
+ ...(Array.isArray(trace.events)
79
+ ? { events: redactValue(trace.events, keys, rules) }
80
+ : {}),
81
+ };
82
+ }
@@ -1,6 +1,6 @@
1
1
  # __PROJECT_NAME__
2
2
 
3
- 基于 [Agentia](https://github.com/retrychx/agentia) 框架的 agent 应用。
3
+ 基于 [Agentia](https://github.com/migoric/agentia) 框架的 agent 应用。
4
4
 
5
5
  ## 目录约定
6
6
 
@@ -93,6 +93,59 @@ npm start # 起 HTTP 服务(= node dist/main.js)
93
93
  (`@migor/agentia` 同样导出、同一套 API,代价是单写者前提)。`npm run dev` 与
94
94
  `npm run start:batch` 不碰它,它们的 Node 下限仍是 18。
95
95
 
96
+ ## 本地怎么调
97
+
98
+ ⚠️ 先认下这条,否则会照着别的框架的经验找错东西:**这里没有「一个业务一个 URL」的接口层**。
99
+ HTTP 面就上面那几条**固定**端点 —— 加一个 `@Tool` 不会多出一条路,**调哪个能力由模型决定,
100
+ 不在 URL 里**。⇒ 想要「一个能力一个接口」的形态,走下面的 ③。
101
+
102
+ **① 不起服务(最快)**:
103
+
104
+ ```bash
105
+ npm run build && npm run start:batch -- "你的问题" # 只跑一轮、打印结果、退出
106
+ ```
107
+
108
+ **② 起了服务之后**(`npm start`,端口 `PORT` 缺省 3000):
109
+
110
+ ```bash
111
+ curl -s localhost:3000/healthz # 先确认活着(不鉴权)
112
+ curl -s localhost:3000/run \
113
+ -H 'content-type: application/json' \
114
+ -d '{"prompt":"介绍一下你自己"}' # body 也收裸字符串 "..." 与 messages 数组
115
+ curl -sN localhost:3000/run \
116
+ -H 'content-type: application/json' \
117
+ -H 'Accept: text/event-stream' -d '"你的问题"' # -N 关掉 curl 缓冲 ⇒ 逐帧滚动
118
+ ```
119
+
120
+ ⚠️ 两个别只看 curl 退出码的判据:
121
+ ① `POST /run` **失败也回 200** —— 硬失败在响应体的 `status: "failed"` + `error` 字段里
122
+ (框架用 `rethrow:false` 语义:失败不用 HTTP 错误码表达)⇒ 脚本里判 `status`,别判 `$?`;
123
+ ② SSE 那条必须加 `-N`,否则是 curl 自己在缓冲,看起来像「服务没吐东西」。
124
+
125
+ **③ 想单挑某一个工具**(不想让模型决定):用 `createMcpServer(app, { transport: 'stdio' })`,
126
+ 每个 `@Tool` 变成一个标准 MCP tool,`tools/call` 直接调 —— 这是「一个能力一个接口」的形态,
127
+ 只是它长在 MCP 层、不是 HTTP 路由层。范围**只到 tools**(不含 skill / subagent / prompt)。
128
+
129
+ **④ 带调用树可视化地调**:`npm run dev`(= `agentia dev`)—— 终端打出
130
+ `Inspector: http://127.0.0.1:<随机端口>/?t=<token>`,浏览器里选能力 / 工作目录、输 prompt,
131
+ 右栏实时调用树;改代码自动重启。
132
+
133
+ ## trace 出口(默认开着)
134
+
135
+ 每次 run 的调用树默认落盘到 `.agentia/traces.jsonl`(已进 `.gitignore`)—— 接的是
136
+ `src/app.ts` 里那一行 `sinks: [jsonlTraceSink({ path: TRACE_FILE })]`。dev 面板、同步
137
+ `/run`、异步 `/tasks` 三条路都经 `app.run` ⇒ 一处接入、三条全覆盖:
138
+
139
+ ```bash
140
+ agentia report .agentia/traces.jsonl # 哪个能力慢 / 贵 / 爱失败
141
+ agentia diff a.jsonl b.jsonl # 两次 run 的调用树 A/B
142
+ ```
143
+
144
+ ⚠️ 落的是**完整 trace**(含 prompt 与工具出入参)⇒ 上线前按你的合规要求处置。删掉 `sinks`
145
+ 那一行即关闭 —— **关掉之后异步 `/tasks` 的 trace 会落进任务库而没有任何人导出**(同步
146
+ `/run` 的 trace 仍随响应回去,所以「好像也没什么区别」正是这个坑的形状)。
147
+ 不确定自己关没关:`agentia doctor` 会查 `src/app.ts` 里这一行。
148
+
96
149
  ## 一次性任务(Job 形态)
97
150
 
98
151
  cron / CI / 容器里「跑一次就退出」的那种,用 `batch.ts`:
@@ -106,7 +159,7 @@ npm run build && npm run start:batch -- "你的问题" # = node dist/batch.js
106
159
  ## 更完整的生产配方
107
160
 
108
161
  要 metrics(`/metrics`)/ OTLP 导出 / Dockerfile / compose 编排,直接抄框架仓库的
109
- [`examples/deploy/`](https://github.com/retrychx/agentia/tree/main/examples/deploy)
162
+ [`examples/deploy/`](https://github.com/migoric/agentia/tree/main/examples/deploy)
110
163
  与 `docs/deployment.md` —— `src/main.ts` 是它的精简版,两者同一套 API;上线前的决定清单
111
164
  也在那份 `docs/deployment.md` 里。API 细节见本项目 `AGENTS.md` 的「触发与宿主」节。
112
165
 
@@ -5,7 +5,8 @@ dist
5
5
  .env
6
6
  .env.local
7
7
 
8
- # dev 环的本地状态:对话历史(.agentia/session.json)等 —— 是本机调试产物,不进版本库
8
+ # dev 环与运行期的本地状态:对话历史(.agentia/session.json)、trace 落盘(.agentia/traces.jsonl)
9
+ # —— 都是本机产物,且 trace 含 prompt 与工具出入参,绝不进版本库
9
10
  .agentia/
10
11
 
11
12
  # 默认服务入口(src/main.ts)的 SQLite 任务库(wal/shm 是它的伴生文件)
@@ -0,0 +1,36 @@
1
+ # __PROJECT_NAME__
2
+
3
+ 一个最小 Agentia 应用:一个入口文件、一个裸工具,没有装饰器、DI 或服务端样板。
4
+
5
+ ## 运行
6
+
7
+ ```bash
8
+ npm install
9
+ $EDITOR .env # 填 ANTHROPIC_API_KEY
10
+ npm run dev -- "你的问题" # tsx src/main.ts,跑一次并退出
11
+ ```
12
+
13
+ 构建后运行:
14
+
15
+ ```bash
16
+ npm run typecheck
17
+ npm run build
18
+ npm start -- "你的问题" # node dist/main.js
19
+ ```
20
+
21
+ `src/main.ts` 里的 `result` 主要包含:
22
+
23
+ - `finalText`:最终文本;先检查 `result.error`,不要只看它是否为空。
24
+ - `stopReason` / `error`:结束原因与结构化错误,失败时进程退出码非零。
25
+ - `trace`:完整调用树、token 与成本;需要文件、指标或 OTLP 出口时接 `TraceSink`。
26
+
27
+ ## 需要更多能力时
28
+
29
+ 当前 minimal 模板**不包含** inspector 面板、能力目录/registry、异步任务或 HTTP 宿主。
30
+ 需要这些时,用默认完整模板另建项目:
31
+
32
+ ```bash
33
+ npx @migor/cli create my-service
34
+ ```
35
+
36
+ 也可以直接在当前 `tools` 数组里继续添加裸 `AgentTool`,不必先引入装饰器。
@@ -0,0 +1,35 @@
1
+ import { createApp, loadEnvFile } from '@migor/agentia';
2
+ import type { AgentTool } from '@migor/agentia';
3
+
4
+ loadEnvFile();
5
+
6
+ const echo = {
7
+ name: 'echo',
8
+ description: '原样返回输入文本,用来验证模型与工具链路',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: { text: { type: 'string' } },
12
+ required: ['text'],
13
+ additionalProperties: false,
14
+ },
15
+ run: (input) => (input as { text: string }).text,
16
+ } satisfies AgentTool;
17
+
18
+ const app = createApp({
19
+ name: '__PROJECT_NAME__',
20
+ system: '你是简洁的助手,需要原样返回文本时调用 echo 工具。',
21
+ tools: [echo],
22
+ });
23
+
24
+ const { result } = await app.run([
25
+ { role: 'user', content: process.argv[2] ?? '用 echo 返回:hello agentia' },
26
+ ]);
27
+
28
+ if (result.error) {
29
+ console.error(`run 失败(stopReason=${result.stopReason}):${result.error.message}`);
30
+ console.error('请把 ANTHROPIC_API_KEY 填进 .env,或设置对应的环境变量。');
31
+ process.exitCode = 1;
32
+ } else {
33
+ console.log(result.finalText);
34
+ // result.trace 已包含本次调用的完整调用树与 token/成本;需要落盘时再接 TraceSink。
35
+ }
@@ -11,10 +11,10 @@
11
11
  "typecheck": "tsc --noEmit -p tsconfig.json"
12
12
  },
13
13
  "dependencies": {
14
- "@migor/agentia": "^0.10.3"
14
+ "@migor/agentia": "^0.10.4"
15
15
  },
16
16
  "devDependencies": {
17
- "@migor/cli": "^0.10.3",
17
+ "@migor/cli": "^0.10.4",
18
18
  "tsx": "^4.19.0",
19
19
  "typescript": "^7.0.2",
20
20
  "@types/node": "^22.0.0"
@@ -1,10 +1,9 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { createApp, loadEnvFile, SystemPrompt } from '@migor/agentia';
4
+ import { createApp, jsonlTraceSink, loadEnvFile, SystemPrompt } from '@migor/agentia';
5
5
  import type { AgentApp, SessionStore } from '@migor/agentia';
6
6
  import { FileSessionStore } from './session-store.js';
7
- import ReadFileTool from './tools/read-file/index.js';
8
7
 
9
8
  /**
10
9
  * 应用装配 —— **与启动分离**(app.ts 只负责「造一个 app」,不负责「跑一次」)。
@@ -61,6 +60,20 @@ export const PROJECT_ROOT = resolve(fileURLToPath(import.meta.url), '../..');
61
60
  /** 会话文件:整段对话历史(跨进程重启接得上)。已进 .gitignore */
62
61
  export const SESSION_FILE = resolve(PROJECT_ROOT, '.agentia', 'session.json');
63
62
 
63
+ /**
64
+ * trace 落盘文件(一行一条裸 Trace)。已进 .gitignore。
65
+ *
66
+ * 为什么**默认就接上**:框架的差异化是「每次 run 都产出可观测调用树」,但框架刻意
67
+ * **不内建**任何出口(spec §9.3 —— 出口只有 `sinks` 这条缝),于是「缝在那儿、没人接」
68
+ * 时表现是:同步 `/run` 的 trace 随响应回去了,而**异步 `/tasks` 的 trace 落进任务库里
69
+ * 没有任何人导出**,「这个任务为什么慢」要排查时它正好不在。接上这一条之后
70
+ * `agentia report` / `diff` / `harvest` / `export` 开箱可读。
71
+ *
72
+ * ⚠️ 落的是**完整 trace**(含 prompt 与工具出入参)⇒ 上线前按你自己的合规要求处置
73
+ * (删这一行即关掉;换 OTLP / 指标见 docs/observability.md 的配方)。
74
+ */
75
+ export const TRACE_FILE = resolve(PROJECT_ROOT, '.agentia', 'traces.jsonl');
76
+
64
77
  export interface CreateAgentAppOptions {
65
78
  /** 只收集这些 token(能力文件夹名)上的能力;不传 = 全量。dev 环的「能力选择」喂这里 */
66
79
  toolSources?: string[];
@@ -74,21 +87,23 @@ export async function createAgentApp(opts: CreateAgentAppOptions = {}): Promise<
74
87
  discover: capabilityPaths(),
75
88
  providers: [
76
89
  // 工作目录以**值**注入给需要它的能力(D3-D:现成 DI,零框架改动)。
77
- // 消费方式:能力类写 `constructor(private readonly root: string)`,并在 providers
78
- // 里声明 `{ provide: '<能力 token>', useClass: Xxx, deps: ['WORKDIR'] }`
79
- // (discover 自动注册的 provider **没有 deps**,所以要注入的能力得从 discover 挪到显式
80
- // providers;同一个 token 混用不会重复收集菜单)。
81
- // ⚠️ 若同时用 src/registry.ts(那里也有一份 `WORKDIR = process.cwd()`):
82
- // 同 token **后注册覆盖先注册**,registry 的 providers 必须排在**这份之前**,
83
- // 否则面板喂进来的 workdir 会被 process.cwd() 静默顶掉。
90
+ // 消费侧两选一,推荐第一种(依赖声明跟着能力走,装配文件不必知道):
91
+ // ① 能力类自带 `static readonly deps = ['WORKDIR']` —— discover 扫到就带上,
92
+ // 脚手架的 read-file 就是这么接的;
93
+ // ② 在这里显式声明 `{ provide: '<token>', useClass: Xxx, deps: ['WORKDIR'] }`
94
+ // (能力不走 discover、或依赖是装配期才知道的值时用这个)。
95
+ // ⚠️ **别两条同时用**:同一 token 出现两份时框架会告警(后注册覆盖先注册,
96
+ // 谁生效取决于顺序)。`src/registry.ts` 里也有一份 `WORKDIR`(显式装配路线),
97
+ // 值与这里相同 ⇒ 混用不会改变语义,但真要二选一时留 `app.ts` 这份。
84
98
  { provide: 'WORKDIR', useValue: opts.workdir ?? PROJECT_ROOT },
85
- // 例子:把工作目录注入给 read-file 工具(脚手架自带的第二个能力,
86
- // 也是「面板上的文件夹选择器真的生效」的那个接线)
87
- { provide: 'read-file', useClass: ReadFileTool, deps: ['WORKDIR'] },
88
99
  ],
89
100
  // 收窄发生在 createApp 期:toolSources 是 token 级白名单,一次收窄顺带把装配期
90
101
  // 静态校验与 prompts.versions 那本账一起收窄。不传 = 全量(缺省路径)。
91
102
  ...(opts.toolSources && opts.toolSources.length > 0 ? { toolSources: opts.toolSources } : {}),
103
+ // trace 出口(观测)。框架刻意不内建 ⇒ 不接就是「跑得挺好、什么都看不见」;
104
+ // 接入后 dev 面板、同步 /run、异步 /tasks 三条路都经 `app.run` ⇒ 这一处全覆盖。
105
+ // 删掉即关闭;换 OTLP / 指标配方见框架 docs/observability.md。
106
+ sinks: [jsonlTraceSink({ path: TRACE_FILE })],
92
107
  system: new SystemPrompt().add(
93
108
  'role',
94
109
  '你是 __PROJECT_NAME__ 的主 agent,按任务自主调度菜单里的能力。',
@@ -1,18 +1,20 @@
1
1
  // Agentia 能力注册表 —— 由 agentia CLI 维护(agentia g 自动更新,也可手工编辑)
2
2
  // @agentia:imports
3
3
  // @agentia:imports-end
4
+ import { resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
4
6
  import type { Provider } from '@migor/agentia';
5
7
 
6
8
  /** 显式装配路线:createApp({ providers, system: ... })(与 discover 目录扫描二选一或混用) */
7
9
  export const providers: Provider[] = [
8
10
  // 「工作目录」以**值**注入给需要它的能力(消费方式见 src/tools/read-file/index.ts)。
9
- // 想改成固定路径 / 从环境变量取,改这一行即可 —— 这是显式装配路线的意义。
10
- //
11
- // ⚠️ 与 src/app.ts 里那份 `WORKDIR` 是**二选一**的关系,混用时**顺序承重**:
12
- // createApp 对同 token 是「后注册覆盖先注册」—— 把这份 providers 拼在 app.ts 那份
13
- // **之后**,dev 面板喂进来的 workdir 会被这里的 `process.cwd()` 静默顶掉。
14
- // 要混用就让本注册表排在前(让 app.ts 的值赢),否则只留一边。
15
- { provide: 'WORKDIR', useValue: process.cwd() },
11
+ // 项目根按**本文件位置**解析(src/registry.ts → 上一层),与 src/app.ts 那份同值 ——
12
+ // ⚠️ 此前这里是 `process.cwd()`:换目录启动(systemd / 容器 `-w`)时两份 `WORKDIR`
13
+ // 值不同,混用两条装配路线时谁生效取决于拼接顺序(「后注册覆盖先注册」),
14
+ // 而框架现在会对同 token 重复注册出声告警 ⇒ 两份同值之后,混用不再改变语义。
15
+ // resolve 顺手去掉尾斜杠:与 app.ts 的 PROJECT_ROOT **逐字同值**(两份若只差一个尾斜杠,
16
+ // 能力侧拿到的字符串就不一样,而「两份 WORKDIR 值不同」正是最难查的那类差异)
17
+ { provide: 'WORKDIR', useValue: resolve(fileURLToPath(new URL('..', import.meta.url))) },
16
18
  // @agentia:entries
17
19
  // @agentia:entries-end
18
20
  ];
@@ -10,8 +10,13 @@ const LIST_LIMIT = 200;
10
10
  *
11
11
  * 它是脚手架自带的第二个能力,作用是把「工作目录」这条线**接通**:
12
12
  * `agentia dev` 的面板上有「工作目录」控件,而只有当**某个能力真的消费它**时,
13
- * 那个控件才有可观测的效果。做法就是这里 —— 根由 DI 注入(`WORKDIR`),
14
- * 不在代码里写死。见 app.ts 的 providers。
13
+ * 那个控件才有可观测的效果。做法就是这里 —— 根由 DI 注入(`WORKDIR`),不在代码里写死。
14
+ *
15
+ * ⚠️ **依赖声明就在本类上**(`static readonly deps = ['WORKDIR']`,见下):`discover`
16
+ * 读到它就会把 deps 带上,所以**装配文件不需要知道这个能力要什么**。
17
+ * 此前(2026-10-16 前)discover 对「类」只产 `useClass`,需要注入的能力只能挪到
18
+ * `app.ts` 的显式 providers 里**再注册一遍** ⇒ 同一 token 出现两份(一份无 deps、
19
+ * 一份有),靠「后注册覆盖先注册」碰巧成立 —— 顺序一变就静默失效。现在没这个必要了。
15
20
  *
16
21
  * 为什么是一对而不是只有 read_file:换了目录之后,模型既不知道自己此刻在哪个目录、
17
22
  * 也不知道里面有什么(run 的 prompt 不会变)⇒ 只能瞎猜文件名,行为看起来「没变」。
@@ -22,14 +27,20 @@ const LIST_LIMIT = 200;
22
27
  * 值从构造器进来,方法里用 `this.root`。
23
28
  */
24
29
  export default class ReadFileTool {
30
+ /**
31
+ * 依赖声明(discover 会读它并带上 deps ⇒ `app.ts` 不必为这个能力再注册一遍)。
32
+ * ⚠️ 必须是**字符串数组**:写成 `deps = 'WORKDIR'` 时 discover 会响亮报错
33
+ * (不当成「没有依赖」去无参构造 —— 那正是要避免的静默失效)。
34
+ */
35
+ static readonly deps = ['WORKDIR'];
36
+
25
37
  constructor(private readonly root: string) {
26
- // discover 自动注册的 provider **没有 deps** —— 如果哪天 app.ts 里那行显式 provider
27
- // 被删掉,容器会无参构造这个类,root 变成 undefined,工具就会静默地按 cwd 解析。
28
- // 与其静默,不如在这里响亮报错。
38
+ // 兜底:若哪天 deps 被摘掉(或能力被以无 deps 的方式注册),容器会无参构造这个类,
39
+ // root 变成 undefined,工具就会静默地按 cwd 解析。与其静默,不如在这里响亮报错。
29
40
  if (typeof root !== 'string' || root.length === 0) {
30
41
  throw new Error(
31
- 'ReadFileTool 需要注入工作目录:请在 src/app.ts 的 providers 里保留 ' +
32
- "{ provide: 'read-file', useClass: ReadFileTool, deps: ['WORKDIR'] }",
42
+ "ReadFileTool 需要注入工作目录:请在能力类上保留 `static readonly deps = ['WORKDIR']`" +
43
+ "(discover 会带上它),或在显式 providers 里声明 { provide: 'read-file', deps: ['WORKDIR'] }",
33
44
  );
34
45
  }
35
46
  // ⚠️ **归一化**:调用方给的工作目录可能带尾斜杠(面板输入、dev.config 手写、
package/dist/templates.js CHANGED
@@ -103,6 +103,12 @@ function capabilityVars(name) {
103
103
  export function projectPackageJson(name) {
104
104
  return renderTemplate('package.json', { __PROJECT_NAME__: name });
105
105
  }
106
+ export function minimalMainTs(name) {
107
+ return renderTemplate('minimal/src/main.ts', { __PROJECT_NAME__: name });
108
+ }
109
+ export function minimalReadme(name) {
110
+ return renderTemplate('minimal/README.md', { __PROJECT_NAME__: name });
111
+ }
106
112
  /**
107
113
  * 脚手架 tsconfig(templates/tsconfig.json)。两处口径:
108
114
  * - `rootDir: src` + `outDir: dist`:编译产物落 dist/(npm run build → npm start 跑
@@ -0,0 +1,31 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ /**
4
+ * CLI 版本 = 本包 package.json 的 `version`(**单源**:`scripts/check-release.mjs` 已保证它
5
+ * 与根包一致,所以这里不另存一份常量 —— 常量会漂,读到的文件不会)。
6
+ *
7
+ * 源码直跑(`src/*.ts`)与发布物(`dist/*.js`)的 `../package.json` 都是包根那一份。
8
+ *
9
+ * 抽成模块是因为**三个**命令都要它:`--version`(cli)、写进项目 `AGENTS.md` 的版本标记
10
+ * (create)、以及 `doctor` 拿它比对「说明文档是不是这一版写的」—— 三处各读一次就会漂。
11
+ */
12
+ export function readCliVersion() {
13
+ try {
14
+ const pkg = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8'));
15
+ return pkg.version ?? 'unknown';
16
+ }
17
+ catch {
18
+ return 'unknown';
19
+ }
20
+ }
21
+ /**
22
+ * 项目内 `AGENTS.md` 的版本标记(`create` 写、`doctor` 读)。
23
+ *
24
+ * 为什么需要它:`AGENTS.md` 是 `create` 时从 `docs/usage-guide.md` **拷**进工程的一份
25
+ * **快照**,之后**没有任何命令会更新它**,而它恰恰是「AI 辅助编码的权威入口」——
26
+ * 框架升级后它会静默过期,AI 则照着过期的 API 写代码。有了这个标记,`doctor` 才能把
27
+ * 「说明是哪一版写的」说出口(此前只能靠人记得去对拍)。
28
+ */
29
+ export const GUIDE_VERSION_MARKER = (version) => `<!-- agentia-guide-version: ${version} -->`;
30
+ /** 匹配上面那个标记:允许标记内多余空白,**区分大小写**(标记本身是我们写死的,不必放宽) */
31
+ export const GUIDE_VERSION_RE = /<!--\s*agentia-guide-version:\s*([^\s-]+)\s*-->/;