@migor/cli 0.8.2 → 0.9.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/CHANGELOG.md +283 -2
- package/README.md +5 -5
- package/dist/AGENTS.md +132 -8
- package/dist/cli.js +4 -2
- package/dist/create.js +18 -3
- package/dist/dev-protocol.js +33 -0
- package/dist/dev-runner.js +445 -0
- package/dist/dev.js +947 -63
- package/dist/inspector-page.html +687 -34
- package/dist/inspector-sink.js +4 -5
- package/dist/inspector.js +326 -19
- package/dist/panel-logic.js +248 -0
- package/dist/registry.js +10 -3
- package/dist/templates/README.md +18 -2
- package/dist/templates/capabilities/subagent/index.ts +5 -1
- package/dist/templates/capabilities/subagent/system.md +15 -0
- package/dist/templates/gitignore +3 -0
- package/dist/templates/package.json +2 -2
- package/dist/templates/src/app.ts +106 -0
- package/dist/templates/src/dev.config.ts +40 -0
- package/dist/templates/src/main.ts +8 -26
- package/dist/templates/src/registry.ts +3 -0
- package/dist/templates/src/session-store.ts +68 -0
- package/dist/templates/src/tools/read-file/index.ts +70 -0
- package/dist/templates.js +51 -4
- package/package.json +1 -1
- package/dist/inspector-preload.js +0 -31
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve, sep } from 'node:path';
|
|
3
|
+
import { Tool } from '@migor/agentia';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 读取**工作目录**下的一个文本文件(相对路径)。
|
|
7
|
+
*
|
|
8
|
+
* 它是脚手架自带的第二个能力,作用是把「工作目录」这条线**接通**:
|
|
9
|
+
* `agentia dev` 的面板上有「工作目录」控件,而只有当**某个能力真的消费它**时,
|
|
10
|
+
* 那个控件才有可观测的效果。做法就是这里 —— 根由 DI 注入(`WORKDIR`),
|
|
11
|
+
* 不在代码里写死。见 app.ts 的 providers。
|
|
12
|
+
*
|
|
13
|
+
* 换成别的形态(把 `workdir` 换成某个 API 的 baseUrl、某个数据目录)完全一样:
|
|
14
|
+
* 值从构造器进来,方法里用 `this.root`。
|
|
15
|
+
*/
|
|
16
|
+
export default class ReadFileTool {
|
|
17
|
+
constructor(private readonly root: string) {
|
|
18
|
+
// discover 自动注册的 provider **没有 deps** —— 如果哪天 app.ts 里那行显式 provider
|
|
19
|
+
// 被删掉,容器会无参构造这个类,root 变成 undefined,工具就会静默地按 cwd 解析。
|
|
20
|
+
// 与其静默,不如在这里响亮报错。
|
|
21
|
+
if (typeof root !== 'string' || root.length === 0) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'ReadFileTool 需要注入工作目录:请在 src/app.ts 的 providers 里保留 ' +
|
|
24
|
+
"{ provide: 'read-file', useClass: ReadFileTool, deps: ['WORKDIR'] }",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
// ⚠️ **归一化**:调用方给的工作目录可能带尾斜杠(面板输入、dev.config 手写、
|
|
28
|
+
// `.../repo/` 粘进来都很常见),而 `safeResolve` 的越界判定是
|
|
29
|
+
// `abs.startsWith(root + sep)` 这类**字符串前缀**比较 —— root 不归一化时
|
|
30
|
+
// 拼接出来的前缀是 `/a/b//`,判定恒为假 ⇒ **每一次** read_file 都报
|
|
31
|
+
// 「路径越出工作目录」。那不是「误报」,是把工具整个变成坏的(且错误归因误导模型)。
|
|
32
|
+
this.root = resolve(root);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Tool({
|
|
36
|
+
description: '读取工作目录下的一个文本文件(给相对路径,如 README.md)',
|
|
37
|
+
schema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: { rel: { type: 'string', description: '相对工作目录的文件路径' } },
|
|
40
|
+
required: ['rel'],
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
},
|
|
43
|
+
strict: true,
|
|
44
|
+
})
|
|
45
|
+
// 方法名 = 模型看到的工具名 ⇒ 与其余模板一律 snake_case(doc_reviewer / note_writer …)。
|
|
46
|
+
// 顺带解掉一个坑:这里若叫 readFile,就与上面 import 进来的 readFile 同名 ——
|
|
47
|
+
// 方法体内 `this.readFile(...)` 会变成自我递归,而裸写 `readFile(...)` 读的是 import 的那个。
|
|
48
|
+
async read_file(input: { rel: string }): Promise<string> {
|
|
49
|
+
return readFile(safeResolve(this.root, input.rel), 'utf8');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 把模型给的路径约束在工作目录内(**越界响亮报错**,错误会作为 is_error 回给模型)。
|
|
55
|
+
*
|
|
56
|
+
* 这条纪律是「可指向任意目录的 agent」活得下去的前提:切目录本身不危险,
|
|
57
|
+
* 危险的是它悄悄跑到工作目录外面去读写。别改成静默截断 / 静默回退。
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ 两端都归一化再比:`root` 由调用方给(可能带尾斜杠),而下面判越界用的是
|
|
60
|
+
* **字符串前缀** —— `resolve(root) + sep` 不归一化时会是 `/a/b//`,于是每一次调用
|
|
61
|
+
* 都误报越界。归一化放在函数内部(不只依赖构造器):本函数是导出的,调用方可能不止一个。
|
|
62
|
+
*/
|
|
63
|
+
export function safeResolve(root: string, rel: string): string {
|
|
64
|
+
const base = resolve(root);
|
|
65
|
+
const abs = resolve(base, rel);
|
|
66
|
+
if (abs !== base && !abs.startsWith(base + sep)) {
|
|
67
|
+
throw new Error(`路径越出工作目录: "${rel}"(工作目录:${base})`);
|
|
68
|
+
}
|
|
69
|
+
return abs;
|
|
70
|
+
}
|
package/dist/templates.js
CHANGED
|
@@ -90,7 +90,7 @@ function capabilityVars(name) {
|
|
|
90
90
|
// ---------- create 命令模板 ----------
|
|
91
91
|
/**
|
|
92
92
|
* 脚手架 package.json(templates/package.json)。两处口径钉在这里,改动前先读:
|
|
93
|
-
* - `scripts.dev` 走 CLI 的 dev
|
|
93
|
+
* - `scripts.dev` 走 CLI 的 dev(本地 inspector 面板 + CLI 自己管的 runner):与文档/其它命令同一条路,
|
|
94
94
|
* 而不是「npm run dev 少一个面板、CLI dev 多一个面板」两种 dev;`npm run dev -- "你的问题"`
|
|
95
95
|
* 的参数由 CLI 原样透传给脚本。
|
|
96
96
|
* - `@migor/cli` 装进 devDependencies(而不是让用户每次 npx 去 registry 拉):工程内
|
|
@@ -112,9 +112,56 @@ export function projectTsconfig() {
|
|
|
112
112
|
return renderTemplate('tsconfig.json');
|
|
113
113
|
}
|
|
114
114
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
115
|
+
* 装配入口 app.ts(templates/src/app.ts)—— **与启动分离**的那个工厂。
|
|
116
|
+
*
|
|
117
|
+
* 为什么单独一个文件(D9(d)):dev 环要驱动用户的 app,就得有个「接受选项的工厂」
|
|
118
|
+
* 能喂进能力选择与工作目录。`CAPABILITY_DIRS` 也在它里面,且**同时是面板能力选择器的
|
|
119
|
+
* 菜单来源** —— 两侧同一个常量,不会漂移。
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ 它是**破坏性模板变更**的一部分:老工程只有 main.ts,`agentia dev` 会明确报错
|
|
122
|
+
* 并给出迁移指引(不静默降级)。迁移是机械的:把 main.ts 里 createApp(...) 那一段
|
|
123
|
+
* 整体搬进 app.ts 并包成工厂。
|
|
124
|
+
*/
|
|
125
|
+
export function appTs(name) {
|
|
126
|
+
return renderTemplate('src/app.ts', { __PROJECT_NAME__: name });
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* dev 环的数据声明(templates/src/dev.config.ts)。
|
|
130
|
+
*
|
|
131
|
+
* 只放**数据**,不放逻辑 —— 逻辑副本会漂移(CLI 修了 bug,工程里那份不会变),
|
|
132
|
+
* 数据不会。所以 `dev.ts` 被否掉,`dev.config.ts` 留下(D8 ②)。
|
|
133
|
+
*/
|
|
134
|
+
export function devConfigTs() {
|
|
135
|
+
return renderTemplate('src/dev.config.ts');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 文件后端 SessionStore(templates/src/session-store.ts)。
|
|
139
|
+
*
|
|
140
|
+
* 放在**模板**而不是框架里:它是对话型能力的**可选件**(任务型能力根本不需要对话历史,
|
|
141
|
+
* 对它们是负担 —— 跨仓库串味)。用户能直接改,换 sqlite / Redis 只改这一个文件。
|
|
142
|
+
*/
|
|
143
|
+
export function sessionStoreTs() {
|
|
144
|
+
return renderTemplate('src/session-store.ts');
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 脚手架自带的第二个能力:read-file 工具(templates/src/tools/read-file/index.ts)。
|
|
148
|
+
*
|
|
149
|
+
* 它存在的理由是**把「工作目录」这条线接通**:面板上那个文件夹控件,只有某个能力真的
|
|
150
|
+
* 消费了注入进来的根,才有可观测的效果。顺带它也是能力多选的第一个演示对象
|
|
151
|
+
* (一个工程里 ≥2 个能力,选择器才有东西可选)。
|
|
152
|
+
*/
|
|
153
|
+
export function readFileToolIndexTs() {
|
|
154
|
+
return renderTemplate('src/tools/read-file/index.ts');
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 入口 main.ts(templates/src/main.ts)。**薄入口**:调工厂 → 处理 result。
|
|
158
|
+
*
|
|
159
|
+
* 装配搬去了 app.ts(见上),这里只留「跑一次」。两件事的顺序与语义都不能变 ——
|
|
160
|
+
* 尤其 `result.error` 那段:run 失败**不抛**,不检查就是「打印空行 + 退出 0」,
|
|
161
|
+
* 让首次运行(比如忘了配 ANTHROPIC_API_KEY)看起来像成功。
|
|
162
|
+
*
|
|
163
|
+
* ⚠️ `loadEnvFile()` **不在**这里,在 app.ts —— dev 环只 import app.ts、从不执行本文件,
|
|
164
|
+
* 把读 .env 留在这边会让 `npm run dev` 与 `npm start` 对同一份 `.env` 有两个行为。
|
|
118
165
|
*/
|
|
119
166
|
export function mainTs(name) {
|
|
120
167
|
return renderTemplate('src/main.ts', { __PROJECT_NAME__: name });
|
package/package.json
CHANGED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* inspector preload —— 由 dev.ts 用 `NODE_OPTIONS=--import <本文件>` 注入到【子进程】。
|
|
3
|
-
*
|
|
4
|
-
* 为什么需要它:`agentia dev` 用 tsx 起的是子进程,而 trace sink 必须在子进程里注册
|
|
5
|
-
* (框架的 registerDefaultTraceSink 是进程内全局注册表)。CLI 无法在子进程外挂上,
|
|
6
|
-
* 于是用 preload 在应用代码 import 之前抢先注册。
|
|
7
|
-
*
|
|
8
|
-
* 关键:必须从【用户项目】(process.cwd())解析 @migor/agentia,而不是从 CLI 的
|
|
9
|
-
* 安装位置 —— 否则拿到的是另一个模块实例,注册表不共享,sink 静默失效。
|
|
10
|
-
*
|
|
11
|
-
* 失败一律降级:打印一行告警即可,绝不阻断 dev。
|
|
12
|
-
*/
|
|
13
|
-
import { createRequire } from 'node:module';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
import { pathToFileURL } from 'node:url';
|
|
16
|
-
import { createInspectSink } from './inspector-sink.js';
|
|
17
|
-
const port = Number(process.env.AGENTIA_INSPECT_PORT || 0);
|
|
18
|
-
if (port > 0) {
|
|
19
|
-
try {
|
|
20
|
-
const req = createRequire(join(process.cwd(), 'package.json'));
|
|
21
|
-
const entry = req.resolve('@migor/agentia');
|
|
22
|
-
const mod = (await import(pathToFileURL(entry).href));
|
|
23
|
-
if (typeof mod.registerDefaultTraceSink !== 'function') {
|
|
24
|
-
throw new Error('@migor/agentia 未导出 registerDefaultTraceSink(版本过旧?)');
|
|
25
|
-
}
|
|
26
|
-
mod.registerDefaultTraceSink(createInspectSink({ port }));
|
|
27
|
-
}
|
|
28
|
-
catch (e) {
|
|
29
|
-
console.warn(`[agentia] inspector 未挂载:${e.message}`);
|
|
30
|
-
}
|
|
31
|
-
}
|