@ai-zen/air 0.1.9 → 0.2.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/README.md CHANGED
@@ -1,16 +1,16 @@
1
1
  # air
2
2
 
3
- 1% 的功能完成 99% 的事情。
3
+ > Do 99% of things with 1% of the features.
4
4
 
5
- 一个极简的 AI 命令行助手。只有一个 shell 工具,AI 自己用文件系统记东西,上下文满了自动迁移。
5
+ A minimalist AI CLI assistant. Just one shell tool — the AI remembers things by writing to the filesystem, and automatically migrates context when it gets too long.
6
6
 
7
- ## 安装
7
+ ## Installation
8
8
 
9
9
  ```bash
10
- # 全局安装(推荐)
10
+ # Global install (recommended)
11
11
  npm install -g @ai-zen/air
12
12
 
13
- # 或从源码构建
13
+ # Or build from source
14
14
  git clone git@github.com:ai-zen/air.git
15
15
  cd air
16
16
  npm install
@@ -18,72 +18,89 @@ npm run build
18
18
  npm install -g .
19
19
  ```
20
20
 
21
- ## 使用
21
+ ## Usage
22
22
 
23
23
  ```bash
24
- # 设置 API KeyDeepSeek
24
+ # Set API Key (DeepSeek)
25
25
  air key sk-xxxxxxxxxxxxxxxx
26
26
 
27
- # 交互模式(自动恢复上次对话)
27
+ # Interactive mode (auto-resumes last conversation)
28
28
  air
29
29
 
30
- # 直接发一条消息
31
- air shell 帮我看看当前目录有哪些文件
30
+ # One-shot message
31
+ air use shell to list files in current directory
32
32
 
33
- # 查看配置
33
+ # View config
34
34
  air config
35
+
36
+ # Install fallback hook (redirects unknown commands to air)
37
+ air hook install
38
+
39
+ # Uninstall fallback hook
40
+ air hook uninstall
35
41
  ```
36
42
 
37
- ### 交互命令
43
+ ### Interactive Commands
44
+
45
+ | Command | Description |
46
+ |---------|-------------|
47
+ | `/exit` `/quit` | Exit |
48
+ | `/save` | Save snapshot |
49
+ | `/load` | Load snapshot |
50
+ | `/new` | Clear context and start fresh |
51
+ | `/back` | Recall a message (optionally edit and resend) |
52
+ | `/editor` | Open system editor for multi-line input |
53
+ | `/help` | Help |
54
+
55
+ ### Fallback Terminal Hook
38
56
 
39
- | 命令 | 说明 |
40
- |------|------|
41
- | `/exit` `/quit` | 退出 |
42
- | `/save` | 保存快照 |
43
- | `/load` | 加载快照 |
44
- | `/new` | 清空上下文重新开始 |
45
- | `/back` | 撤回消息(可选修改后重发) |
46
- | `/editor` | 打开系统编辑器输入多行文本 |
47
- | `/help` | 帮助 |
57
+ When installed, any command that doesn't exist in your shell gets automatically forwarded to `air`. The AI will interpret what you meant and help you out.
58
+
59
+ ```bash
60
+ $ gred "hello" file.txt
61
+ # command not found → auto-redirects to air
62
+ # AI: "Did you mean grep?"
63
+ ```
48
64
 
49
- ## 设计
65
+ ## Design
50
66
 
51
67
  ```
52
68
  ~/.ai-zen/air/
53
69
  ├── config.json # { "apiKey": "sk-xxx" }
54
- ├── context.json # [ { role, content }, ... ] 当前对话
55
- ├── snapshots/ # /save 或迁移前自动快照
56
- └── temp/ # AI 自己写入的长期记忆 (*.md)
70
+ ├── context.json # [ { role, content }, ... ] Current conversation
71
+ ├── snapshots/ # Auto snapshots before migration or /save
72
+ └── temp/ # Long-term memory written by AI (*.md)
57
73
  ```
58
74
 
59
- ### 核心理念
75
+ ### Core Philosophy
60
76
 
61
- - **模型**: DeepSeek-V4-Flash(写死,只有一个)
62
- - **工具**: 只有一个 `shell`,AI 用它执行命令、读写文件
63
- - **记忆**: AI 自己决定记什么,用 shell 写入 `temp/*.md`,下次启动时读取。air 不做额外的持久化机制
64
- - **上下文**: JSON 序列化后超过 50 万字符自动迁移,迁移前拍快照
65
- - **行为准则**: 先商量再动手,危险操作必须获得用户书面确认。追责原则——每一步基于用户指令,用户承担责任
77
+ - **Model**: DeepSeek-V4-Flash (hardcoded, only one)
78
+ - **Tool**: Just one `shell` tool — the AI executes commands, reads and writes files through it
79
+ - **Memory**: The AI decides what to remember, writes to `temp/*.md` via shell, reads on next startup. No extra persistence mechanism
80
+ - **Context**: Auto-migrates when JSON serialization exceeds 500K chars, takes a snapshot before migration
81
+ - **Rules**: Consult the user before making changes. Dangerous operations require explicit written confirmation. The user takes responsibility for their own instructions
66
82
 
67
- ## 项目结构
83
+ ## Project Structure
68
84
 
69
85
  ```
70
86
  src/
71
- ├── cli.ts # CLI 入口,commander (48行)
72
- ├── config.ts # 配置与文件读写 (109行)
73
- ├── delta-renderer.ts # 流式渲染器 (126行,复用自 agents 项目)
74
- ├── tools.ts # 工具定义——shell (23行)
75
- ├── agent-factory.ts # Agent 工厂——构建模型与 Agent (23行)
76
- ├── migration.ts # 上下文计数与任务迁移 (72行)
77
- ├── agent-runtime.ts # 运行时——对话循环与命令处理 (256行)
87
+ ├── cli.ts # CLI entry, commander
88
+ ├── config.ts # Config read/write
89
+ ├── delta-renderer.ts # Stream renderer (from agents project)
90
+ ├── tools.ts # Tool definitions — shell
91
+ ├── agent-factory.ts # Agent factory build model & agent
92
+ ├── migration.ts # Context counting & migration
93
+ ├── agent-runtime.ts # Runtime — conversation loop & commands
94
+ ├── hook.ts # Fallback terminal hook (install/uninstall)
78
95
  └── __tests__/
79
- ├── config.test.ts # 配置/上下文/快照测试 (110行)
80
- ├── main.test.ts # contextSize/shouldMigrate 测试 (43行)
81
- └── tools.test.ts # shell 工具结构测试 (15行)
96
+ ├── config.test.ts # Config/context/snapshot tests
97
+ ├── main.test.ts # contextSize/shouldMigrate tests
98
+ └── tools.test.ts # Shell tool structure tests
82
99
  ```
83
100
 
84
- 40 KB,826 行。
101
+ ~46 KB, 870 lines.
85
102
 
86
- ## 测试
103
+ ## Tests
87
104
 
88
105
  ```bash
89
106
  npm test
package/README.zh.md ADDED
@@ -0,0 +1,97 @@
1
+ # air
2
+
3
+ 用 1% 的功能完成 99% 的事情。
4
+
5
+ 一个极简的 AI 命令行助手。只有一个 shell 工具,AI 自己用文件系统记东西,上下文满了自动迁移。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ # 全局安装(推荐)
11
+ npm install -g @ai-zen/air
12
+
13
+ # 或从源码构建
14
+ git clone git@github.com:ai-zen/air.git
15
+ cd air
16
+ npm install
17
+ npm run build
18
+ npm install -g .
19
+ ```
20
+
21
+ ## 使用
22
+
23
+ ```bash
24
+ # 设置 API Key(DeepSeek)
25
+ air key sk-xxxxxxxxxxxxxxxx
26
+
27
+ # 交互模式(自动恢复上次对话)
28
+ air
29
+
30
+ # 直接发一条消息
31
+ air 用 shell 帮我看看当前目录有哪些文件
32
+
33
+ # 查看配置
34
+ air config
35
+
36
+ # 安装兜底终端钩子(命令不存在时自动转发到 air)
37
+ air hook install
38
+
39
+ # 卸载兜底终端钩子
40
+ air hook uninstall
41
+ ```
42
+
43
+ ### 交互命令
44
+
45
+ | 命令 | 说明 |
46
+ |------|------|
47
+ | `/exit` `/quit` | 退出 |
48
+ | `/save` | 保存快照 |
49
+ | `/load` | 加载快照 |
50
+ | `/new` | 清空上下文重新开始 |
51
+ | `/back` | 撤回消息(可选修改后重发) |
52
+ | `/editor` | 打开系统编辑器输入多行文本 |
53
+ | `/help` | 帮助 |
54
+
55
+ ## 设计
56
+
57
+ ```
58
+ ~/.ai-zen/air/
59
+ ├── config.json # { "apiKey": "sk-xxx" }
60
+ ├── context.json # [ { role, content }, ... ] 当前对话
61
+ ├── snapshots/ # /save 或迁移前自动快照
62
+ └── temp/ # AI 自己写入的长期记忆 (*.md)
63
+ ```
64
+
65
+ ### 核心理念
66
+
67
+ - **模型**: DeepSeek-V4-Flash(写死,只有一个)
68
+ - **工具**: 只有一个 `shell`,AI 用它执行命令、读写文件
69
+ - **记忆**: AI 自己决定记什么,用 shell 写入 `temp/*.md`,下次启动时读取。air 不做额外的持久化机制
70
+ - **上下文**: JSON 序列化后超过 50 万字符自动迁移,迁移前拍快照
71
+ - **行为准则**: 先商量再动手,危险操作必须获得用户书面确认。追责原则——每一步基于用户指令,用户承担责任
72
+
73
+ ## 项目结构
74
+
75
+ ```
76
+ src/
77
+ ├── cli.ts # CLI 入口,commander
78
+ ├── config.ts # 配置与文件读写
79
+ ├── delta-renderer.ts # 流式渲染器(复用自 agents 项目)
80
+ ├── tools.ts # 工具定义——shell
81
+ ├── agent-factory.ts # Agent 工厂——构建模型与 Agent
82
+ ├── migration.ts # 上下文计数与任务迁移
83
+ ├── agent-runtime.ts # 运行时——对话循环与命令处理
84
+ ├── hook.ts # 兜底终端钩子(install/uninstall)
85
+ └── __tests__/
86
+ ├── config.test.ts # 配置/上下文/快照测试
87
+ ├── main.test.ts # contextSize/shouldMigrate 测试
88
+ └── tools.test.ts # shell 工具结构测试
89
+ ```
90
+
91
+ 共 46 KB,870 行。
92
+
93
+ ## 测试
94
+
95
+ ```bash
96
+ npm test
97
+ ```
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { readFileSync } from "node:fs";
3
3
  import { Command } from "commander";
4
4
  import { runConversation } from "./agent-runtime.js";
5
+ import { installHook, uninstallHook } from "./hook.js";
5
6
  import { readConfig, saveConfig } from "./config.js";
6
7
  const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
7
8
  const program = new Command();
@@ -25,6 +26,21 @@ program
25
26
  const key = c.apiKey ? "****" + c.apiKey.slice(-4) : "(未设置)";
26
27
  console.log(`API Key: ${key}`);
27
28
  });
29
+ program
30
+ .command("hook")
31
+ .description("安装/卸载兜底终端钩子(命令不存在时自动转发到 air)")
32
+ .argument("<action>", "install 或 uninstall")
33
+ .action((action) => {
34
+ if (action === "install") {
35
+ installHook();
36
+ }
37
+ else if (action === "uninstall") {
38
+ uninstallHook();
39
+ }
40
+ else {
41
+ console.error('❌ 未知操作,请使用 install 或 uninstall');
42
+ }
43
+ });
28
44
  program
29
45
  .argument("[message]", "要发送的消息(不传则进入交互模式)")
30
46
  .action(async (message) => {
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAErD,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,CAC5B,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CACnE,CAAC;AAEF,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,KAAK,CAAC;KACX,WAAW,CAAC,aAAa,CAAC;KAC1B,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,KAAK,CAAC;KACd,WAAW,CAAC,YAAY,CAAC;KACzB,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;KACxC,MAAM,CAAC,CAAC,MAAc,EAAE,EAAE;IACzB,UAAU,CAAC,MAAM,CAAC,CAAC;IACnB,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;AAC/B,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,QAAQ,CAAC;KACrB,MAAM,CAAC,GAAG,EAAE;IACX,MAAM,CAAC,GAAG,UAAU,EAAE,CAAC;IACvB,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAC7D,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,EAAE,CAAC,CAAC;AACjC,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC;KAC1C,MAAM,CAAC,KAAK,EAAE,OAAgB,EAAE,EAAE;IACjC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACnB,OAAO,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACpD,OAAO,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;QACnE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;AACjC,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAErD,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,CAC5B,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CACnE,CAAC;AAEF,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,KAAK,CAAC;KACX,WAAW,CAAC,aAAa,CAAC;KAC1B,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,KAAK,CAAC;KACd,WAAW,CAAC,YAAY,CAAC;KACzB,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;KACxC,MAAM,CAAC,CAAC,MAAc,EAAE,EAAE;IACzB,UAAU,CAAC,MAAM,CAAC,CAAC;IACnB,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;AAC/B,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,QAAQ,CAAC;KACrB,MAAM,CAAC,GAAG,EAAE;IACX,MAAM,CAAC,GAAG,UAAU,EAAE,CAAC;IACvB,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAC7D,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,EAAE,CAAC,CAAC;AACjC,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CAAC,8BAA8B,CAAC;KAC3C,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;KAC3C,MAAM,CAAC,CAAC,MAAc,EAAE,EAAE;IACzB,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,WAAW,EAAE,CAAC;IAChB,CAAC;SAAM,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,aAAa,EAAE,CAAC;IAClB,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;IAClD,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC;KAC1C,MAAM,CAAC,KAAK,EAAE,OAAgB,EAAE,EAAE;IACjC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACnB,OAAO,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACpD,OAAO,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;QACnE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;AACjC,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC"}
package/dist/hook.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function installHook(): void;
2
+ export declare function uninstallHook(): void;
3
+ //# sourceMappingURL=hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook.d.ts","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":"AAsCA,wBAAgB,WAAW,IAAI,IAAI,CAelC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAqCpC"}
package/dist/hook.js ADDED
@@ -0,0 +1,82 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { existsSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
4
+ const MARKER_START = "# === air hook (开始) ===";
5
+ const MARKER_END = "# === air hook (结束) ===";
6
+ function detectShell() {
7
+ const shell = process.env.SHELL || "";
8
+ const home = homedir();
9
+ if (shell.includes("zsh")) {
10
+ return { rcFile: join(home, ".zshrc"), hookFn: "command_not_found_handler" };
11
+ }
12
+ if (shell.includes("bash")) {
13
+ return { rcFile: join(home, ".bashrc"), hookFn: "command_not_found_handle" };
14
+ }
15
+ return null;
16
+ }
17
+ function hookCode(hookFn) {
18
+ return [
19
+ "",
20
+ MARKER_START,
21
+ `${hookFn}() {`,
22
+ ' air "$@"',
23
+ "}",
24
+ MARKER_END,
25
+ "",
26
+ ].join("\n");
27
+ }
28
+ function isInstalled(rcFile) {
29
+ if (!existsSync(rcFile))
30
+ return false;
31
+ const content = readFileSync(rcFile, "utf-8");
32
+ return content.includes(MARKER_START);
33
+ }
34
+ export function installHook() {
35
+ const shell = detectShell();
36
+ if (!shell) {
37
+ console.error("❌ 不支持的 shell(仅支持 bash / zsh)");
38
+ process.exit(1);
39
+ }
40
+ if (isInstalled(shell.rcFile)) {
41
+ console.log("ℹ️ air hook 已安装,无需重复操作");
42
+ return;
43
+ }
44
+ appendFileSync(shell.rcFile, hookCode(shell.hookFn), "utf-8");
45
+ console.log(`✅ air hook 已安装到 ${shell.rcFile}`);
46
+ console.log(` 重启终端或执行 source ${shell.rcFile} 即可生效`);
47
+ }
48
+ export function uninstallHook() {
49
+ const shell = detectShell();
50
+ if (!shell) {
51
+ console.error("❌ 不支持的 shell(仅支持 bash / zsh)");
52
+ process.exit(1);
53
+ }
54
+ if (!existsSync(shell.rcFile)) {
55
+ console.log("ℹ️ air hook 未安装(文件不存在)");
56
+ return;
57
+ }
58
+ const content = readFileSync(shell.rcFile, "utf-8");
59
+ if (!content.includes(MARKER_START)) {
60
+ console.log("ℹ️ air hook 未安装");
61
+ return;
62
+ }
63
+ const lines = content.split("\n");
64
+ const newLines = [];
65
+ let skipping = false;
66
+ for (const line of lines) {
67
+ if (line.trim() === MARKER_START) {
68
+ skipping = true;
69
+ continue;
70
+ }
71
+ if (line.trim() === MARKER_END) {
72
+ skipping = false;
73
+ continue;
74
+ }
75
+ if (!skipping) {
76
+ newLines.push(line);
77
+ }
78
+ }
79
+ writeFileSync(shell.rcFile, newLines.join("\n"), "utf-8");
80
+ console.log(`✅ air hook 已从 ${shell.rcFile} 卸载`);
81
+ }
82
+ //# sourceMappingURL=hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook.js","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ai-zen/air",
3
- "version": "0.1.9",
4
- "description": "极简 AI 命令行助手",
3
+ "version": "0.2.0",
4
+ "description": "Minimalist AI CLI assistant \u2014 one shell tool, filesystem memory, auto context migration",
5
5
  "type": "module",
6
6
  "main": "./dist/agent-runtime.js",
7
7
  "bin": {
@@ -25,5 +25,14 @@
25
25
  "@types/node": "^24.12.2",
26
26
  "typescript": "^5.3.3",
27
27
  "vitest": "^4.1.9"
28
- }
28
+ },
29
+ "keywords": [
30
+ "ai",
31
+ "cli",
32
+ "assistant",
33
+ "shell",
34
+ "deepseek",
35
+ "terminal",
36
+ "command-line"
37
+ ]
29
38
  }
package/src/cli.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { readFileSync } from "node:fs";
3
3
  import { Command } from "commander";
4
4
  import { runConversation } from "./agent-runtime.js";
5
+ import { installHook, uninstallHook } from "./hook.js";
5
6
  import { readConfig, saveConfig } from "./config.js";
6
7
 
7
8
  const { version } = JSON.parse(
@@ -33,6 +34,20 @@ program
33
34
  console.log(`API Key: ${key}`);
34
35
  });
35
36
 
37
+ program
38
+ .command("hook")
39
+ .description("安装/卸载兜底终端钩子(命令不存在时自动转发到 air)")
40
+ .argument("<action>", "install 或 uninstall")
41
+ .action((action: string) => {
42
+ if (action === "install") {
43
+ installHook();
44
+ } else if (action === "uninstall") {
45
+ uninstallHook();
46
+ } else {
47
+ console.error('❌ 未知操作,请使用 install 或 uninstall');
48
+ }
49
+ });
50
+
36
51
  program
37
52
  .argument("[message]", "要发送的消息(不传则进入交互模式)")
38
53
  .action(async (message?: string) => {
package/src/hook.ts ADDED
@@ -0,0 +1,93 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { existsSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
4
+
5
+ const MARKER_START = "# === air hook (开始) ===";
6
+ const MARKER_END = "# === air hook (结束) ===";
7
+
8
+ function detectShell(): { rcFile: string; hookFn: string } | null {
9
+ const shell = process.env.SHELL || "";
10
+ const home = homedir();
11
+
12
+ if (shell.includes("zsh")) {
13
+ return { rcFile: join(home, ".zshrc"), hookFn: "command_not_found_handler" };
14
+ }
15
+ if (shell.includes("bash")) {
16
+ return { rcFile: join(home, ".bashrc"), hookFn: "command_not_found_handle" };
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function hookCode(hookFn: string): string {
22
+ return [
23
+ "",
24
+ MARKER_START,
25
+ `${hookFn}() {`,
26
+ ' air "$@"',
27
+ "}",
28
+ MARKER_END,
29
+ "",
30
+ ].join("\n");
31
+ }
32
+
33
+ function isInstalled(rcFile: string): boolean {
34
+ if (!existsSync(rcFile)) return false;
35
+ const content = readFileSync(rcFile, "utf-8");
36
+ return content.includes(MARKER_START);
37
+ }
38
+
39
+ export function installHook(): void {
40
+ const shell = detectShell();
41
+ if (!shell) {
42
+ console.error("❌ 不支持的 shell(仅支持 bash / zsh)");
43
+ process.exit(1);
44
+ }
45
+
46
+ if (isInstalled(shell.rcFile)) {
47
+ console.log("ℹ️ air hook 已安装,无需重复操作");
48
+ return;
49
+ }
50
+
51
+ appendFileSync(shell.rcFile, hookCode(shell.hookFn), "utf-8");
52
+ console.log(`✅ air hook 已安装到 ${shell.rcFile}`);
53
+ console.log(` 重启终端或执行 source ${shell.rcFile} 即可生效`);
54
+ }
55
+
56
+ export function uninstallHook(): void {
57
+ const shell = detectShell();
58
+ if (!shell) {
59
+ console.error("❌ 不支持的 shell(仅支持 bash / zsh)");
60
+ process.exit(1);
61
+ }
62
+
63
+ if (!existsSync(shell.rcFile)) {
64
+ console.log("ℹ️ air hook 未安装(文件不存在)");
65
+ return;
66
+ }
67
+
68
+ const content = readFileSync(shell.rcFile, "utf-8");
69
+ if (!content.includes(MARKER_START)) {
70
+ console.log("ℹ️ air hook 未安装");
71
+ return;
72
+ }
73
+
74
+ const lines = content.split("\n");
75
+ const newLines: string[] = [];
76
+ let skipping = false;
77
+ for (const line of lines) {
78
+ if (line.trim() === MARKER_START) {
79
+ skipping = true;
80
+ continue;
81
+ }
82
+ if (line.trim() === MARKER_END) {
83
+ skipping = false;
84
+ continue;
85
+ }
86
+ if (!skipping) {
87
+ newLines.push(line);
88
+ }
89
+ }
90
+
91
+ writeFileSync(shell.rcFile, newLines.join("\n"), "utf-8");
92
+ console.log(`✅ air hook 已从 ${shell.rcFile} 卸载`);
93
+ }