@itradingai/aiwiki 0.2.5
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/LICENSE +21 -0
- package/README.md +175 -0
- package/dist/src/app.js +417 -0
- package/dist/src/args.js +32 -0
- package/dist/src/cli.js +4 -0
- package/dist/src/ingest.js +428 -0
- package/dist/src/output.js +10 -0
- package/dist/src/paths.js +32 -0
- package/dist/src/payload.js +95 -0
- package/dist/src/workspace.js +495 -0
- package/docs/AGENT_HANDOFF.md +132 -0
- package/docs/OBSIDIAN_DATAVIEW_PLAN.md +194 -0
- package/docs/README.md +28 -0
- package/docs/USAGE.md +329 -0
- package/docs/architecture.svg +103 -0
- package/docs/assets/join-group.png +0 -0
- package/docs/assets/wechat-official-account.png +0 -0
- package/package.json +44 -0
- package/skill/SKILL.md +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 iTradingAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# AIWiki
|
|
2
|
+
|
|
3
|
+
AIWiki 是一个面向宿主 Agent 的本地知识库生产 CLI。用户把文章链接或正文发给 Codex、QClaw、OpenClaw、Claude Code 等 Agent,并使用“入库”触发词;宿主 Agent 负责读取网页或上下文,AIWiki 负责把内容写入本地知识库,并生成适合 Obsidian + Dataview 审阅的资料结构。
|
|
4
|
+
|
|
5
|
+
AIWiki 的重点不是替代 Agent 抓网页,而是把 Agent 已经读到的内容稳定沉淀为可追踪、可复盘、可继续写作的本地知识库。
|
|
6
|
+
|
|
7
|
+
## 安装与初始化
|
|
8
|
+
|
|
9
|
+
一次性运行交互式 setup:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @itradingai/aiwiki@latest setup
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
CLI 会询问知识库路径。直接回车会使用默认目录,确认后会创建或补齐 AIWiki 目录,并写入默认知识库配置。
|
|
16
|
+
|
|
17
|
+
自动化初始化可以使用:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @itradingai/aiwiki@latest setup --path "F:\knowledge_data\aiwiki" --yes
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
如果希望长期使用全局命令:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g @itradingai/aiwiki@latest
|
|
27
|
+
aiwiki setup
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
升级到最新版本:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g @itradingai/aiwiki@latest
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
要求 Node.js `>=20`。
|
|
37
|
+
|
|
38
|
+
## 让宿主 Agent 学会 AIWiki
|
|
39
|
+
|
|
40
|
+
初始化知识库后,先扫描本机支持的宿主 Agent:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
aiwiki agent list
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
再启动安装向导:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
aiwiki agent install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
也可以直接指定目标:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
aiwiki agent install --agent codex --yes
|
|
56
|
+
aiwiki agent install --agent qclaw --yes
|
|
57
|
+
aiwiki agent install --agent openclaw --yes
|
|
58
|
+
aiwiki agent install --agent claude --yes
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
如果当前宿主 Agent 暂不支持自动安装,可以输出通用对接协议:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
aiwiki prompt agent
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
把输出内容安装成宿主 Agent 的 skill,或粘贴到宿主 Agent 的项目/会话说明里。
|
|
68
|
+
|
|
69
|
+
## 入库流程
|
|
70
|
+
|
|
71
|
+
完成 setup 和 Agent 安装后,对宿主 Agent 发送:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
入库 https://example.com/article
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
宿主 Agent 读取网页后,通过 `aiwiki ingest-agent --stdin` 把结构化内容交给 AIWiki CLI。用户不需要手动保存 payload,也不需要每次输入 `--path`。
|
|
78
|
+
|
|
79
|
+
典型流程:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
用户发送链接 -> 宿主 Agent 读取内容 -> Agent 调用 AIWiki -> AIWiki 写入本地知识库 -> Obsidian 审阅和沉淀
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Obsidian 与 Dataview
|
|
86
|
+
|
|
87
|
+
AIWiki 初始化时会创建面向 Obsidian 的目录、Dashboard、Dataview 查询和 frontmatter 约定。核心入口包括:
|
|
88
|
+
|
|
89
|
+
- `dashboards/AIWiki Home.md`
|
|
90
|
+
- `dashboards/Review Queue.md`
|
|
91
|
+
- `dashboards/Claims Review.md`
|
|
92
|
+
- `_system/schemas/aiwiki-frontmatter.md`
|
|
93
|
+
|
|
94
|
+
完整方案见:[docs/OBSIDIAN_DATAVIEW_PLAN.md](docs/OBSIDIAN_DATAVIEW_PLAN.md)。
|
|
95
|
+
|
|
96
|
+
## 常用命令
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
aiwiki setup
|
|
100
|
+
aiwiki setup --path <path> --yes
|
|
101
|
+
aiwiki agent list
|
|
102
|
+
aiwiki agent install
|
|
103
|
+
aiwiki prompt agent
|
|
104
|
+
aiwiki doctor
|
|
105
|
+
aiwiki status
|
|
106
|
+
aiwiki ingest-agent --stdin
|
|
107
|
+
aiwiki ingest-file --file <file>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
高级/调试命令:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
aiwiki init --path <path> --yes --set-default
|
|
114
|
+
aiwiki config show
|
|
115
|
+
aiwiki ingest-agent --payload <file>
|
|
116
|
+
aiwiki ingest-url <url> --content-file <file>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 当前范围
|
|
120
|
+
|
|
121
|
+
- 单知识库
|
|
122
|
+
- 单次处理一条输入
|
|
123
|
+
- 宿主 Agent 读取网页、附件或正文
|
|
124
|
+
- CLI 写入本地文件和 Obsidian 友好的结构
|
|
125
|
+
- 生成资料卡、素材建议、主题候选、草稿大纲、处理摘要
|
|
126
|
+
|
|
127
|
+
## 当前不包含
|
|
128
|
+
|
|
129
|
+
- CLI 内置通用网页抓取
|
|
130
|
+
- 跨主题自动路由
|
|
131
|
+
- 批处理
|
|
132
|
+
- 定时或指定采集
|
|
133
|
+
- 长流程状态机
|
|
134
|
+
- 技术支持流程
|
|
135
|
+
|
|
136
|
+
## 文档
|
|
137
|
+
|
|
138
|
+
- 使用说明:[docs/USAGE.md](docs/USAGE.md)
|
|
139
|
+
- Agent 对接协议:[docs/AGENT_HANDOFF.md](docs/AGENT_HANDOFF.md)
|
|
140
|
+
- Obsidian + Dataview 方案:[docs/OBSIDIAN_DATAVIEW_PLAN.md](docs/OBSIDIAN_DATAVIEW_PLAN.md)
|
|
141
|
+
- 架构图:[docs/architecture.svg](docs/architecture.svg)
|
|
142
|
+
|
|
143
|
+
## 联系与交流
|
|
144
|
+
|
|
145
|
+
项目专题介绍:[maxking.cc](https://maxking.cc/aiwiki)
|
|
146
|
+
|
|
147
|
+
<table>
|
|
148
|
+
<tr>
|
|
149
|
+
<td align="center" width="50%">
|
|
150
|
+
<img src="docs/assets/join-group.png" alt="扫码进群交流" width="360">
|
|
151
|
+
<br>
|
|
152
|
+
<strong>扫码进群</strong>
|
|
153
|
+
</td>
|
|
154
|
+
<td align="center" width="50%">
|
|
155
|
+
<img src="docs/assets/wechat-official-account.png" alt="扫码关注公众号" width="360">
|
|
156
|
+
<br>
|
|
157
|
+
<strong>关注公众号</strong>
|
|
158
|
+
</td>
|
|
159
|
+
</tr>
|
|
160
|
+
</table>
|
|
161
|
+
|
|
162
|
+
## 本地开发
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npm install
|
|
166
|
+
npm run build
|
|
167
|
+
npm link
|
|
168
|
+
aiwiki setup --path "F:\knowledge_data\aiwiki-test" --yes
|
|
169
|
+
aiwiki prompt agent
|
|
170
|
+
aiwiki doctor
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT. See [LICENSE](LICENSE).
|
package/dist/src/app.js
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { flagBool, flagString, parseArgs } from "./args.js";
|
|
7
|
+
import { ingestFile, ingestPayload } from "./ingest.js";
|
|
8
|
+
import { CliError, writeLine } from "./output.js";
|
|
9
|
+
import { confirmInit, directorySummary, doctor, exists, initWorkspace, promptForSetup, promptForInitPath, readConfig, resolveWorkspace, setDefaultWorkspace, statusSummary } from "./workspace.js";
|
|
10
|
+
export const VERSION = "0.2.2";
|
|
11
|
+
export async function runCli(argv, streams = { stdout: process.stdout, stderr: process.stderr }) {
|
|
12
|
+
try {
|
|
13
|
+
const args = parseArgs(argv);
|
|
14
|
+
const [command, subcommand] = args.positional;
|
|
15
|
+
if (args.flags.has("version") || command === "version" || command === "-v") {
|
|
16
|
+
writeLine(streams.stdout, `aiwiki ${VERSION}`);
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
if (args.flags.has("help") || !command || command === "help" || command === "-h") {
|
|
20
|
+
printHelp(streams.stdout);
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
if (command === "setup") {
|
|
24
|
+
const setup = await promptForSetup({ rootPath: flagString(args, "path"), yes: flagBool(args, "yes") });
|
|
25
|
+
if (!setup.confirmed) {
|
|
26
|
+
writeLine(streams.stdout, "已取消。");
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
const result = await initWorkspace(setup.rootPath);
|
|
30
|
+
const defaultConfig = await setDefaultWorkspace(result.root);
|
|
31
|
+
writeLine(streams.stdout, `AIWiki 已初始化: ${result.root}`);
|
|
32
|
+
writeLine(streams.stdout, `配置: ${result.createdConfig ? "已创建" : "已保留"}`);
|
|
33
|
+
writeLine(streams.stdout, `新建目录数: ${result.createdDirs.length}`);
|
|
34
|
+
writeLine(streams.stdout, `新建数据库文件数: ${result.seededFiles.filter((file) => file.created).length}`);
|
|
35
|
+
writeLine(streams.stdout, `默认知识库: ${defaultConfig.defaultPath}`);
|
|
36
|
+
writeLine(streams.stdout, `用户配置: ${defaultConfig.configPath}`);
|
|
37
|
+
writeLine(streams.stdout, "Obsidian 入口: dashboards/AIWiki Home.md");
|
|
38
|
+
writeLine(streams.stdout, "下一步: 运行 `aiwiki agent install`,把 AIWiki 安装到宿主 Agent。");
|
|
39
|
+
writeLine(streams.stdout, "Agent 设置完成后: 向 Agent 发送 `入库 <url>`");
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
if (command === "agent" && subcommand === "install") {
|
|
43
|
+
const result = await installAgentSkill({
|
|
44
|
+
agentId: flagString(args, "agent"),
|
|
45
|
+
yes: flagBool(args, "yes"),
|
|
46
|
+
force: flagBool(args, "force"),
|
|
47
|
+
streams
|
|
48
|
+
});
|
|
49
|
+
if (result) {
|
|
50
|
+
writeLine(streams.stdout, `已安装: ${result.name}`);
|
|
51
|
+
writeLine(streams.stdout, `目标路径: ${result.target}`);
|
|
52
|
+
writeLine(streams.stdout, `下一步: 重启或重新加载 ${result.name},然后发送 \`入库 <url>\`。`);
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
if (command === "agent" && (subcommand === "list" || !subcommand)) {
|
|
57
|
+
printAgentList(streams.stdout, await discoverAgentTargets());
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
if (command === "prompt" && (subcommand === "agent" || !subcommand)) {
|
|
61
|
+
printAgentPrompt(streams.stdout);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
if (command === "init") {
|
|
65
|
+
let rootPath = flagString(args, "path");
|
|
66
|
+
if (!rootPath) {
|
|
67
|
+
rootPath = await promptForInitPath();
|
|
68
|
+
}
|
|
69
|
+
if (!flagBool(args, "yes")) {
|
|
70
|
+
const confirmed = await confirmInit(rootPath);
|
|
71
|
+
if (!confirmed) {
|
|
72
|
+
writeLine(streams.stdout, "已取消。");
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const result = await initWorkspace(rootPath);
|
|
77
|
+
writeLine(streams.stdout, `AIWiki 已初始化: ${result.root}`);
|
|
78
|
+
writeLine(streams.stdout, `配置: ${result.createdConfig ? "已创建" : "已保留"}`);
|
|
79
|
+
writeLine(streams.stdout, `新建目录数: ${result.createdDirs.length}`);
|
|
80
|
+
writeLine(streams.stdout, `新建数据库文件数: ${result.seededFiles.filter((file) => file.created).length}`);
|
|
81
|
+
writeLine(streams.stdout, "Obsidian 入口: dashboards/AIWiki Home.md");
|
|
82
|
+
if (flagBool(args, "set-default")) {
|
|
83
|
+
const defaultConfig = await setDefaultWorkspace(result.root);
|
|
84
|
+
writeLine(streams.stdout, `默认知识库: ${defaultConfig.defaultPath}`);
|
|
85
|
+
writeLine(streams.stdout, `用户配置: ${defaultConfig.configPath}`);
|
|
86
|
+
}
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
if (command === "config" && subcommand === "show") {
|
|
90
|
+
const root = await resolveWorkspace(flagString(args, "path"));
|
|
91
|
+
const config = await readConfig(root);
|
|
92
|
+
const summary = await directorySummary(root);
|
|
93
|
+
writeLine(streams.stdout, `知识库路径: ${root}`);
|
|
94
|
+
writeLine(streams.stdout, `产品: ${config.product}`);
|
|
95
|
+
writeLine(streams.stdout, `配置版本: ${config.schemaVersion}`);
|
|
96
|
+
writeLine(streams.stdout, `创建时间: ${config.createdAt}`);
|
|
97
|
+
writeLine(streams.stdout, `目录状态: ${summary.present} 个正常,${summary.missing.length} 个缺失`);
|
|
98
|
+
if (summary.missing.length) {
|
|
99
|
+
writeLine(streams.stdout, `缺失目录: ${summary.missing.join(", ")}`);
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
if (command === "doctor") {
|
|
104
|
+
const root = await resolveWorkspace(flagString(args, "path"));
|
|
105
|
+
const checks = await doctor(root);
|
|
106
|
+
let failed = false;
|
|
107
|
+
for (const check of checks) {
|
|
108
|
+
writeLine(streams.stdout, `${doctorStatusText(check.status)}: ${check.name}`);
|
|
109
|
+
if (check.status !== "ok") {
|
|
110
|
+
failed = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (failed) {
|
|
114
|
+
writeLine(streams.stdout, `修复命令: aiwiki setup --path "${root}" --yes`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
if (command === "status") {
|
|
120
|
+
const root = await resolveWorkspace(flagString(args, "path"));
|
|
121
|
+
const summary = await statusSummary(root);
|
|
122
|
+
writeLine(streams.stdout, `知识库路径: ${summary.root}`);
|
|
123
|
+
writeLine(streams.stdout, `处理次数: ${summary.runCount}`);
|
|
124
|
+
writeLine(streams.stdout, `失败次数: ${summary.failedCount}`);
|
|
125
|
+
writeLine(streams.stdout, `最近处理: ${summary.lastRunId ?? "无"}`);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
if (command === "ingest-agent") {
|
|
129
|
+
const root = await resolveWorkspace(flagString(args, "path"));
|
|
130
|
+
const payloadPath = flagString(args, "payload");
|
|
131
|
+
const useStdin = flagBool(args, "stdin");
|
|
132
|
+
if (!payloadPath && !useStdin) {
|
|
133
|
+
throw new CliError("请提供 --payload <file> 或 --stdin。");
|
|
134
|
+
}
|
|
135
|
+
const rawText = payloadPath ? await fs.readFile(payloadPath, "utf8") : await readStdin();
|
|
136
|
+
const payload = parseJson(rawText);
|
|
137
|
+
const result = await ingestPayload(root, payload);
|
|
138
|
+
printIngestResult(streams.stdout, result);
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
if (command === "ingest-file") {
|
|
142
|
+
const root = await resolveWorkspace(flagString(args, "path"));
|
|
143
|
+
const file = flagString(args, "file") ?? args.positional[1];
|
|
144
|
+
if (!file) {
|
|
145
|
+
throw new CliError("请提供 --file <file>。");
|
|
146
|
+
}
|
|
147
|
+
const result = await ingestFile(root, path.resolve(file));
|
|
148
|
+
printIngestResult(streams.stdout, result);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
if (command === "ingest-url") {
|
|
152
|
+
const contentFile = flagString(args, "content-file");
|
|
153
|
+
if (!contentFile) {
|
|
154
|
+
throw new CliError("AIWiki CLI 不抓取网页。请提供 --content-file <file>,让宿主 Agent 或用户先提供正文。");
|
|
155
|
+
}
|
|
156
|
+
const url = args.positional[1];
|
|
157
|
+
if (!url) {
|
|
158
|
+
throw new CliError("请提供 URL。");
|
|
159
|
+
}
|
|
160
|
+
const root = await resolveWorkspace(flagString(args, "path"));
|
|
161
|
+
const content = await fs.readFile(contentFile, "utf8");
|
|
162
|
+
const result = await ingestPayload(root, {
|
|
163
|
+
schema_version: "aiwiki.agent_payload.v1",
|
|
164
|
+
source: {
|
|
165
|
+
kind: "url",
|
|
166
|
+
url,
|
|
167
|
+
title: path.basename(contentFile),
|
|
168
|
+
content_format: "markdown",
|
|
169
|
+
content,
|
|
170
|
+
fetcher: "content-file",
|
|
171
|
+
fetch_status: "ok",
|
|
172
|
+
captured_at: new Date().toISOString()
|
|
173
|
+
},
|
|
174
|
+
request: {
|
|
175
|
+
mode: "ingest",
|
|
176
|
+
outputs: ["source_card", "creative_assets", "topics", "draft_outline", "processing_summary"],
|
|
177
|
+
language: "zh-CN"
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
printIngestResult(streams.stdout, result);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
throw new CliError(`未知命令: ${command}`);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (error instanceof CliError) {
|
|
187
|
+
writeLine(streams.stderr, `错误: ${error.message}`);
|
|
188
|
+
return error.exitCode;
|
|
189
|
+
}
|
|
190
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
191
|
+
writeLine(streams.stderr, `错误: ${message}`);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function printHelp(stream) {
|
|
196
|
+
writeLine(stream, "AIWiki");
|
|
197
|
+
writeLine(stream, "");
|
|
198
|
+
writeLine(stream, "用法:");
|
|
199
|
+
writeLine(stream, " aiwiki setup");
|
|
200
|
+
writeLine(stream, " aiwiki setup --path <path> --yes");
|
|
201
|
+
writeLine(stream, " aiwiki agent list");
|
|
202
|
+
writeLine(stream, " aiwiki agent install");
|
|
203
|
+
writeLine(stream, " aiwiki agent install --agent codex --yes");
|
|
204
|
+
writeLine(stream, " aiwiki prompt agent");
|
|
205
|
+
writeLine(stream, " aiwiki doctor");
|
|
206
|
+
writeLine(stream, " aiwiki status");
|
|
207
|
+
writeLine(stream, " aiwiki ingest-agent --stdin");
|
|
208
|
+
writeLine(stream, " aiwiki ingest-file --file <file>");
|
|
209
|
+
writeLine(stream, " aiwiki init --path <path> --yes --set-default");
|
|
210
|
+
writeLine(stream, " aiwiki config show");
|
|
211
|
+
writeLine(stream, " aiwiki ingest-agent --payload <file>");
|
|
212
|
+
writeLine(stream, " aiwiki ingest-url <url> --content-file <file>");
|
|
213
|
+
}
|
|
214
|
+
async function discoverAgentTargets() {
|
|
215
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
216
|
+
const skillSource = path.join(packageRoot, "skill", "SKILL.md");
|
|
217
|
+
const promptSource = path.join(packageRoot, "docs", "AGENT_HANDOFF.md");
|
|
218
|
+
const codexHome = process.env.CODEX_HOME ? path.resolve(process.env.CODEX_HOME) : path.join(os.homedir(), ".codex");
|
|
219
|
+
const qclawHome = process.env.QCLAW_HOME ? path.resolve(process.env.QCLAW_HOME) : path.join(os.homedir(), ".qclaw");
|
|
220
|
+
const openclawHome = process.env.OPENCLAW_HOME ? path.resolve(process.env.OPENCLAW_HOME) : path.join(os.homedir(), ".openclaw");
|
|
221
|
+
const claudeHome = process.env.CLAUDE_HOME ? path.resolve(process.env.CLAUDE_HOME) : path.join(os.homedir(), ".claude");
|
|
222
|
+
const opencodeHome = process.env.OPENCODE_HOME ? path.resolve(process.env.OPENCODE_HOME) : path.join(os.homedir(), ".opencode");
|
|
223
|
+
const hermesHome = process.env.HERMES_HOME ? path.resolve(process.env.HERMES_HOME) : path.join(process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local"), "hermes");
|
|
224
|
+
return [
|
|
225
|
+
{
|
|
226
|
+
id: "codex",
|
|
227
|
+
name: "Codex",
|
|
228
|
+
detected: await exists(codexHome),
|
|
229
|
+
installable: true,
|
|
230
|
+
kind: "skill",
|
|
231
|
+
source: skillSource,
|
|
232
|
+
target: path.join(codexHome, "skills", "aiwiki", "SKILL.md"),
|
|
233
|
+
note: "安装到 Codex 用户 skills 目录。"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "qclaw",
|
|
237
|
+
name: "QClaw",
|
|
238
|
+
detected: await exists(qclawHome),
|
|
239
|
+
installable: true,
|
|
240
|
+
kind: "skill",
|
|
241
|
+
source: skillSource,
|
|
242
|
+
target: path.join(qclawHome, "skills", "aiwiki", "SKILL.md"),
|
|
243
|
+
note: "安装到本机 QClaw skills 目录。"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "openclaw",
|
|
247
|
+
name: "OpenClaw",
|
|
248
|
+
detected: await exists(openclawHome),
|
|
249
|
+
installable: true,
|
|
250
|
+
kind: "skill",
|
|
251
|
+
source: skillSource,
|
|
252
|
+
target: path.join(openclawHome, "workspace", "skills", "aiwiki", "SKILL.md"),
|
|
253
|
+
note: "安装到 OpenClaw workspace skills 目录。"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "claude",
|
|
257
|
+
name: "Claude Code",
|
|
258
|
+
detected: await exists(claudeHome),
|
|
259
|
+
installable: true,
|
|
260
|
+
kind: "command",
|
|
261
|
+
source: promptSource,
|
|
262
|
+
target: path.join(claudeHome, "commands", "aiwiki.md"),
|
|
263
|
+
note: "安装为 Claude Code slash-command 提示文件。"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: "opencode",
|
|
267
|
+
name: "opencode",
|
|
268
|
+
detected: await exists(opencodeHome),
|
|
269
|
+
installable: false,
|
|
270
|
+
kind: "prompt",
|
|
271
|
+
note: "已检测到,但暂未确认稳定的用户提示目录。请先使用 aiwiki prompt agent。"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: "hermes",
|
|
275
|
+
name: "Hermes",
|
|
276
|
+
detected: await exists(hermesHome),
|
|
277
|
+
installable: false,
|
|
278
|
+
kind: "prompt",
|
|
279
|
+
note: "已检测到,但暂未确认稳定的 skill 目录。请先使用 aiwiki prompt agent。"
|
|
280
|
+
}
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
function printAgentList(stream, targets) {
|
|
284
|
+
writeLine(stream, "AIWiki 宿主 Agent 目标");
|
|
285
|
+
for (const target of targets) {
|
|
286
|
+
writeLine(stream, `${target.id}: ${target.name} | 已检测=${target.detected ? "是" : "否"} | 可安装=${target.installable ? "是" : "否"} | ${target.note}`);
|
|
287
|
+
if (target.target) {
|
|
288
|
+
writeLine(stream, ` 目标路径: ${target.target}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function installAgentSkill(options) {
|
|
293
|
+
const targets = await discoverAgentTargets();
|
|
294
|
+
const installable = targets.filter((target) => target.detected && target.installable);
|
|
295
|
+
let selected = options.agentId ? targets.find((target) => target.id === options.agentId) : undefined;
|
|
296
|
+
if (!selected && options.agentId) {
|
|
297
|
+
throw new CliError(`未知宿主 Agent: ${options.agentId}`);
|
|
298
|
+
}
|
|
299
|
+
if (!selected) {
|
|
300
|
+
if (installable.length === 0) {
|
|
301
|
+
printAgentList(options.streams.stdout, targets);
|
|
302
|
+
throw new CliError("未检测到可自动安装的宿主 Agent。请运行 aiwiki prompt agent,并手动粘贴对接协议。");
|
|
303
|
+
}
|
|
304
|
+
printAgentList(options.streams.stdout, targets);
|
|
305
|
+
const answer = await askQuestion(options.streams, "请输入要安装的目标 id 或序号: ");
|
|
306
|
+
const trimmed = answer.trim();
|
|
307
|
+
const byNumber = /^\d+$/.test(trimmed) ? installable[Number(trimmed) - 1] : undefined;
|
|
308
|
+
selected = byNumber ?? targets.find((target) => target.id === trimmed);
|
|
309
|
+
}
|
|
310
|
+
if (!selected) {
|
|
311
|
+
writeLine(options.streams.stdout, "已取消。");
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
if (!selected.installable) {
|
|
315
|
+
throw new CliError(`已检测到 ${selected.name},但暂未配置自动安装。请运行 aiwiki prompt agent。`);
|
|
316
|
+
}
|
|
317
|
+
if (!selected.source || !selected.target) {
|
|
318
|
+
throw new CliError(`${selected.name} 没有可用安装目标。`);
|
|
319
|
+
}
|
|
320
|
+
if (!options.yes) {
|
|
321
|
+
writeLine(options.streams.stdout, `将 AIWiki 安装到 ${selected.name}:`);
|
|
322
|
+
writeLine(options.streams.stdout, ` 来源: ${selected.source}`);
|
|
323
|
+
writeLine(options.streams.stdout, ` 目标路径: ${selected.target}`);
|
|
324
|
+
const answer = await askQuestion(options.streams, "确认安装?输入 y 继续: ");
|
|
325
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
326
|
+
writeLine(options.streams.stdout, "已取消。");
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
await copyInstallFile(selected.source, selected.target, options.force);
|
|
331
|
+
return selected;
|
|
332
|
+
}
|
|
333
|
+
async function askQuestion(streams, question) {
|
|
334
|
+
if (!process.stdin.isTTY) {
|
|
335
|
+
throw new CliError("交互式 Agent 安装需要终端环境。脚本中请使用 --agent <id> --yes。");
|
|
336
|
+
}
|
|
337
|
+
const rl = createInterface({ input: process.stdin, output: streams.stdout });
|
|
338
|
+
try {
|
|
339
|
+
return await rl.question(question);
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
rl.close();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async function copyInstallFile(source, target, force) {
|
|
346
|
+
await fs.access(source);
|
|
347
|
+
if (!force && (await exists(target))) {
|
|
348
|
+
throw new CliError(`目标文件已存在: ${target}。如需覆盖,请加 --force。`);
|
|
349
|
+
}
|
|
350
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
351
|
+
await fs.copyFile(source, target);
|
|
352
|
+
}
|
|
353
|
+
function printAgentPrompt(stream) {
|
|
354
|
+
writeLine(stream, "AIWiki Agent 中文提示");
|
|
355
|
+
writeLine(stream, "");
|
|
356
|
+
writeLine(stream, "当用户发送以下触发语时,请自动执行 AIWiki 入库流程:");
|
|
357
|
+
writeLine(stream, "- 入库 <url>");
|
|
358
|
+
writeLine(stream, "- 收录 <url>");
|
|
359
|
+
writeLine(stream, "- 存一下 <url>");
|
|
360
|
+
writeLine(stream, "- aiwiki <url>");
|
|
361
|
+
writeLine(stream, "");
|
|
362
|
+
writeLine(stream, "如果当前会话被用户明确设定为 AIWiki 入库助手,则用户只发送 URL 也默认触发入库。普通会话中不要把所有 URL 都自动入库。");
|
|
363
|
+
writeLine(stream, "");
|
|
364
|
+
writeLine(stream, "流程:读取网页正文;生成 aiwiki.agent_payload.v1;通过 stdin 调用 `aiwiki ingest-agent --stdin`;读取 CLI 输出;只向用户汇报 ingested、recorded、fit_score、fit_level、summary、run_dir、processing_summary、source_card、dashboard、review_queue。");
|
|
365
|
+
writeLine(stream, "回复措辞:成功时说“已加入 Obsidian 审阅队列”,并给出资料卡、处理记录、Obsidian 入口和待审队列。Dataview 是可选增强,不要替用户安装插件或修改 .obsidian。");
|
|
366
|
+
writeLine(stream, "");
|
|
367
|
+
writeLine(stream, "禁止:让用户保存 payload;让用户每次输入 --path;声称 AIWiki CLI 负责网页抓取。");
|
|
368
|
+
}
|
|
369
|
+
function doctorStatusText(status) {
|
|
370
|
+
if (status === "ok") {
|
|
371
|
+
return "正常";
|
|
372
|
+
}
|
|
373
|
+
if (status === "missing") {
|
|
374
|
+
return "缺失";
|
|
375
|
+
}
|
|
376
|
+
return "权限错误";
|
|
377
|
+
}
|
|
378
|
+
function printIngestResult(stream, result) {
|
|
379
|
+
writeLine(stream, `ingested: ${result.agentReport.ingested ? "yes" : "no"}`);
|
|
380
|
+
writeLine(stream, `recorded: ${result.agentReport.recorded ? "yes" : "no"}`);
|
|
381
|
+
writeLine(stream, `fetch_status: ${result.agentReport.fetchStatus}`);
|
|
382
|
+
writeLine(stream, `fit_score: ${result.agentReport.fitScore}`);
|
|
383
|
+
writeLine(stream, `fit_level: ${result.agentReport.fitLevel}`);
|
|
384
|
+
writeLine(stream, `source_title: ${result.agentReport.sourceTitle}`);
|
|
385
|
+
if (result.agentReport.sourceUrl) {
|
|
386
|
+
writeLine(stream, `source_url: ${result.agentReport.sourceUrl}`);
|
|
387
|
+
}
|
|
388
|
+
writeLine(stream, `summary: ${result.agentReport.summary}`);
|
|
389
|
+
writeLine(stream, `run_id: ${result.runId}`);
|
|
390
|
+
writeLine(stream, `run_dir: ${result.runDir}`);
|
|
391
|
+
writeLine(stream, `files: ${result.generatedFiles.length}`);
|
|
392
|
+
writeLine(stream, `processing_summary: ${result.agentReport.keyFiles.processingSummary}`);
|
|
393
|
+
if (result.agentReport.keyFiles.sourceCard) {
|
|
394
|
+
writeLine(stream, `source_card: ${result.agentReport.keyFiles.sourceCard}`);
|
|
395
|
+
}
|
|
396
|
+
if (result.agentReport.keyFiles.draftOutline) {
|
|
397
|
+
writeLine(stream, `draft_outline: ${result.agentReport.keyFiles.draftOutline}`);
|
|
398
|
+
}
|
|
399
|
+
writeLine(stream, `dashboard: ${result.agentReport.keyFiles.dashboard}`);
|
|
400
|
+
writeLine(stream, `review_queue: ${result.agentReport.keyFiles.reviewQueue}`);
|
|
401
|
+
writeLine(stream, `warnings: ${result.warnings.length}`);
|
|
402
|
+
}
|
|
403
|
+
async function readStdin() {
|
|
404
|
+
const chunks = [];
|
|
405
|
+
for await (const chunk of process.stdin) {
|
|
406
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
407
|
+
}
|
|
408
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
409
|
+
}
|
|
410
|
+
function parseJson(text) {
|
|
411
|
+
try {
|
|
412
|
+
return JSON.parse(text);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
throw new CliError("payload must be valid JSON");
|
|
416
|
+
}
|
|
417
|
+
}
|
package/dist/src/args.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function parseArgs(argv) {
|
|
2
|
+
const positional = [];
|
|
3
|
+
const flags = new Map();
|
|
4
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
5
|
+
const arg = argv[index];
|
|
6
|
+
if (!arg.startsWith("--") || arg === "--") {
|
|
7
|
+
positional.push(arg);
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const trimmed = arg.slice(2);
|
|
11
|
+
const eqIndex = trimmed.indexOf("=");
|
|
12
|
+
if (eqIndex >= 0) {
|
|
13
|
+
flags.set(trimmed.slice(0, eqIndex), trimmed.slice(eqIndex + 1));
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const next = argv[index + 1];
|
|
17
|
+
if (next && !next.startsWith("--")) {
|
|
18
|
+
flags.set(trimmed, next);
|
|
19
|
+
index += 1;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
flags.set(trimmed, true);
|
|
23
|
+
}
|
|
24
|
+
return { positional, flags };
|
|
25
|
+
}
|
|
26
|
+
export function flagString(args, name) {
|
|
27
|
+
const value = args.flags.get(name);
|
|
28
|
+
return typeof value === "string" ? value : undefined;
|
|
29
|
+
}
|
|
30
|
+
export function flagBool(args, name) {
|
|
31
|
+
return args.flags.get(name) === true;
|
|
32
|
+
}
|
package/dist/src/cli.js
ADDED