@ai-zen/air 0.2.0 → 0.2.2

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.
Files changed (130) hide show
  1. package/README.md +21 -6
  2. package/README.zh.md +31 -6
  3. package/dist/agent-factory.js +1 -1
  4. package/dist/agent-factory.js.map +1 -1
  5. package/dist/agent-runtime.d.ts.map +1 -1
  6. package/dist/agent-runtime.js +241 -228
  7. package/dist/agent-runtime.js.map +1 -1
  8. package/dist/chat/commands/back.d.ts +3 -0
  9. package/dist/chat/commands/back.d.ts.map +1 -0
  10. package/dist/chat/commands/back.js +95 -0
  11. package/dist/chat/commands/back.js.map +1 -0
  12. package/dist/chat/commands/editor.d.ts +3 -0
  13. package/dist/chat/commands/editor.d.ts.map +1 -0
  14. package/dist/chat/commands/editor.js +20 -0
  15. package/dist/chat/commands/editor.js.map +1 -0
  16. package/dist/chat/commands/exit.d.ts +3 -0
  17. package/dist/chat/commands/exit.d.ts.map +1 -0
  18. package/dist/chat/commands/exit.js +5 -0
  19. package/dist/chat/commands/exit.js.map +1 -0
  20. package/dist/chat/commands/help.d.ts +2 -0
  21. package/dist/chat/commands/help.d.ts.map +1 -0
  22. package/dist/chat/commands/help.js +4 -0
  23. package/dist/chat/commands/help.js.map +1 -0
  24. package/dist/chat/commands/index.d.ts +3 -0
  25. package/dist/chat/commands/index.d.ts.map +1 -0
  26. package/dist/chat/commands/index.js +37 -0
  27. package/dist/chat/commands/index.js.map +1 -0
  28. package/dist/chat/commands/load.d.ts +3 -0
  29. package/dist/chat/commands/load.d.ts.map +1 -0
  30. package/dist/chat/commands/load.js +37 -0
  31. package/dist/chat/commands/load.js.map +1 -0
  32. package/dist/chat/commands/message.d.ts +5 -0
  33. package/dist/chat/commands/message.d.ts.map +1 -0
  34. package/dist/chat/commands/message.js +53 -0
  35. package/dist/chat/commands/message.js.map +1 -0
  36. package/dist/chat/commands/new.d.ts +3 -0
  37. package/dist/chat/commands/new.d.ts.map +1 -0
  38. package/dist/chat/commands/new.js +11 -0
  39. package/dist/chat/commands/new.js.map +1 -0
  40. package/dist/chat/commands/save.d.ts +3 -0
  41. package/dist/chat/commands/save.d.ts.map +1 -0
  42. package/dist/chat/commands/save.js +5 -0
  43. package/dist/chat/commands/save.js.map +1 -0
  44. package/dist/chat/message.d.ts +3 -0
  45. package/dist/chat/message.d.ts.map +1 -0
  46. package/dist/chat/message.js +31 -0
  47. package/dist/chat/message.js.map +1 -0
  48. package/dist/chat/print.d.ts +3 -0
  49. package/dist/chat/print.d.ts.map +1 -0
  50. package/dist/chat/print.js +24 -0
  51. package/dist/chat/print.js.map +1 -0
  52. package/dist/chat/runtime.d.ts +2 -0
  53. package/dist/chat/runtime.d.ts.map +1 -0
  54. package/dist/chat/runtime.js +64 -0
  55. package/dist/chat/runtime.js.map +1 -0
  56. package/dist/chat/session.d.ts +8 -0
  57. package/dist/chat/session.d.ts.map +1 -0
  58. package/dist/chat/session.js +77 -0
  59. package/dist/chat/session.js.map +1 -0
  60. package/dist/chat/shared.d.ts +6 -0
  61. package/dist/chat/shared.d.ts.map +1 -0
  62. package/dist/chat/shared.js +16 -0
  63. package/dist/chat/shared.js.map +1 -0
  64. package/dist/cli.js +3 -3
  65. package/dist/cli.js.map +1 -1
  66. package/dist/config.js +1 -1
  67. package/dist/config.js.map +1 -1
  68. package/dist/session/commands/back.d.ts +3 -0
  69. package/dist/session/commands/back.d.ts.map +1 -0
  70. package/dist/session/commands/back.js +95 -0
  71. package/dist/session/commands/back.js.map +1 -0
  72. package/dist/session/commands/editor.d.ts +3 -0
  73. package/dist/session/commands/editor.d.ts.map +1 -0
  74. package/dist/session/commands/editor.js +20 -0
  75. package/dist/session/commands/editor.js.map +1 -0
  76. package/dist/session/commands/exit.d.ts +3 -0
  77. package/dist/session/commands/exit.d.ts.map +1 -0
  78. package/dist/session/commands/exit.js +5 -0
  79. package/dist/session/commands/exit.js.map +1 -0
  80. package/dist/session/commands/help.d.ts +2 -0
  81. package/dist/session/commands/help.d.ts.map +1 -0
  82. package/dist/session/commands/help.js +4 -0
  83. package/dist/session/commands/help.js.map +1 -0
  84. package/dist/session/commands/load.d.ts +3 -0
  85. package/dist/session/commands/load.d.ts.map +1 -0
  86. package/dist/session/commands/load.js +37 -0
  87. package/dist/session/commands/load.js.map +1 -0
  88. package/dist/session/commands/new.d.ts +3 -0
  89. package/dist/session/commands/new.d.ts.map +1 -0
  90. package/dist/session/commands/new.js +6 -0
  91. package/dist/session/commands/new.js.map +1 -0
  92. package/dist/session/commands/save.d.ts +3 -0
  93. package/dist/session/commands/save.d.ts.map +1 -0
  94. package/dist/session/commands/save.js +5 -0
  95. package/dist/session/commands/save.js.map +1 -0
  96. package/dist/session/message.d.ts +3 -0
  97. package/dist/session/message.d.ts.map +1 -0
  98. package/dist/session/message.js +31 -0
  99. package/dist/session/message.js.map +1 -0
  100. package/dist/session/print.d.ts +3 -0
  101. package/dist/session/print.d.ts.map +1 -0
  102. package/dist/session/print.js +24 -0
  103. package/dist/session/print.js.map +1 -0
  104. package/dist/session/runtime.d.ts +2 -0
  105. package/dist/session/runtime.d.ts.map +1 -0
  106. package/dist/session/runtime.js +46 -0
  107. package/dist/session/runtime.js.map +1 -0
  108. package/dist/session/shared.d.ts +8 -0
  109. package/dist/session/shared.d.ts.map +1 -0
  110. package/dist/session/shared.js +77 -0
  111. package/dist/session/shared.js.map +1 -0
  112. package/package.json +1 -1
  113. package/src/__tests__/chat.test.ts +147 -0
  114. package/src/__tests__/e2e.test.ts +187 -0
  115. package/src/agent-factory.ts +1 -1
  116. package/src/chat/commands/back.ts +90 -0
  117. package/src/chat/commands/editor.ts +18 -0
  118. package/src/chat/commands/exit.ts +6 -0
  119. package/src/chat/commands/help.ts +3 -0
  120. package/src/chat/commands/index.ts +38 -0
  121. package/src/chat/commands/load.ts +38 -0
  122. package/src/chat/commands/new.ts +12 -0
  123. package/src/chat/commands/save.ts +6 -0
  124. package/src/chat/message.ts +27 -0
  125. package/src/chat/print.ts +27 -0
  126. package/src/chat/runtime.ts +65 -0
  127. package/src/chat/shared.ts +21 -0
  128. package/src/cli.ts +4 -4
  129. package/src/config.ts +1 -1
  130. package/src/agent-runtime.ts +0 -265
package/README.md CHANGED
@@ -85,20 +85,35 @@ $ gred "hello" file.txt
85
85
  ```
86
86
  src/
87
87
  ├── cli.ts # CLI entry, commander
88
- ├── config.ts # Config read/write
89
- ├── delta-renderer.ts # Stream renderer (from agents project)
88
+ ├── config.ts # Config, context, snapshot read/write
89
+ ├── delta-renderer.ts # Stream renderer
90
+ ├── hook.ts # Fallback terminal hook (install/uninstall)
91
+ ├── migration.ts # Context counting & migration
90
92
  ├── tools.ts # Tool definitions — shell
91
93
  ├── 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)
94
+ ├── chat/
95
+ ├── shared.ts # ChatCtx type & SYSTEM_PROMPT
96
+ ├── runtime.ts # runChat() & chatLoop() — core runtime
97
+ │ ├── message.ts # handleMessage() — send & migrate
98
+ │ ├── print.ts # sendAndPrint() — stream output
99
+ │ └── commands/
100
+ │ ├── index.ts # dispatchCommand() — command router
101
+ │ ├── back.ts # /back — recall & resend
102
+ │ ├── editor.ts # /editor — multi-line input
103
+ │ ├── exit.ts # /exit — quit
104
+ │ ├── help.ts # /help
105
+ │ ├── load.ts # /load — load snapshot
106
+ │ ├── new.ts # /new — new session
107
+ │ └── save.ts # /save — save snapshot
95
108
  └── __tests__/
109
+ ├── chat.test.ts # Chat session tests
96
110
  ├── config.test.ts # Config/context/snapshot tests
97
111
  ├── main.test.ts # contextSize/shouldMigrate tests
112
+ ├── e2e.test.ts # End-to-end tests
98
113
  └── tools.test.ts # Shell tool structure tests
99
114
  ```
100
115
 
101
- ~46 KB, 870 lines.
116
+ ~49 KB, 881 lines.
102
117
 
103
118
  ## Tests
104
119
 
package/README.zh.md CHANGED
@@ -52,6 +52,16 @@ air hook uninstall
52
52
  | `/editor` | 打开系统编辑器输入多行文本 |
53
53
  | `/help` | 帮助 |
54
54
 
55
+ ### 兜底终端钩子
56
+
57
+ 安装后,任何 shell 中不存在的命令都会自动转发到 `air`,AI 会解读你的意图并帮助你。
58
+
59
+ ```bash
60
+ $ gred "hello" file.txt
61
+ # → command not found → 自动转发到 air
62
+ # → AI: "你是不是想用 grep?"
63
+ ```
64
+
55
65
  ## 设计
56
66
 
57
67
  ```
@@ -75,20 +85,35 @@ air hook uninstall
75
85
  ```
76
86
  src/
77
87
  ├── cli.ts # CLI 入口,commander
78
- ├── config.ts # 配置与文件读写
79
- ├── delta-renderer.ts # 流式渲染器(复用自 agents 项目)
88
+ ├── config.ts # 配置、上下文、快照读写
89
+ ├── delta-renderer.ts # 流式渲染器
90
+ ├── hook.ts # 兜底终端钩子(install/uninstall)
91
+ ├── migration.ts # 上下文计数与迁移
80
92
  ├── tools.ts # 工具定义——shell
81
93
  ├── agent-factory.ts # Agent 工厂——构建模型与 Agent
82
- ├── migration.ts # 上下文计数与任务迁移
83
- ├── agent-runtime.ts # 运行时——对话循环与命令处理
84
- ├── hook.ts # 兜底终端钩子(install/uninstall)
94
+ ├── chat/
95
+ ├── shared.ts # ChatCtx 类型 & SYSTEM_PROMPT
96
+ ├── runtime.ts # runChat() & chatLoop() — 核心运行时
97
+ │ ├── message.ts # handleMessage() — 发送与迁移
98
+ │ ├── print.ts # sendAndPrint() — 流式输出
99
+ │ └── commands/
100
+ │ ├── index.ts # dispatchCommand() — 命令分发入口
101
+ │ ├── back.ts # /back — 撤回消息
102
+ │ ├── editor.ts # /editor — 多行编辑器输入
103
+ │ ├── exit.ts # /exit — 退出
104
+ │ ├── help.ts # /help
105
+ │ ├── load.ts # /load — 加载快照
106
+ │ ├── new.ts # /new — 新会话
107
+ │ └── save.ts # /save — 保存快照
85
108
  └── __tests__/
109
+ ├── chat.test.ts # 聊天测试
86
110
  ├── config.test.ts # 配置/上下文/快照测试
87
111
  ├── main.test.ts # contextSize/shouldMigrate 测试
112
+ ├── e2e.test.ts # 端到端测试
88
113
  └── tools.test.ts # shell 工具结构测试
89
114
  ```
90
115
 
91
- 46 KB,870 行。
116
+ 49 KB,881 行。
92
117
 
93
118
  ## 测试
94
119
 
@@ -2,7 +2,7 @@ import { Agent, Message, OpenAI, ChatGPT } from "@ai-zen/agents-core";
2
2
  import { readConfig } from "./config.js";
3
3
  import { shellTool } from "./tools.js";
4
4
  const MODEL_NAME = "deepseek-v4-flash";
5
- const API_ENDPOINT = "https://api.deepseek.com/v1";
5
+ const API_ENDPOINT = process.env.AIR_API_ENDPOINT || "https://api.deepseek.com/v1";
6
6
  async function buildModel(apiKey) {
7
7
  const endpoint = new OpenAI({ openai_endpoint: API_ENDPOINT, api_key: apiKey });
8
8
  return new ChatGPT({
@@ -1 +1 @@
1
- {"version":3,"file":"agent-factory.js","sourceRoot":"","sources":["../src/agent-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEtE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,UAAU,GAAG,mBAAmB,CAAC;AACvC,MAAM,YAAY,GAAG,6BAA6B,CAAC;AAEnD,KAAK,UAAU,UAAU,CAAC,MAAc;IACtC,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,EAAE,eAAe,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAChF,OAAO,IAAI,OAAO,CAAC;QACjB,YAAY,EAAE,EAAE;QAChB,cAAc,EAAE,MAAM,QAAQ,CAAC,cAAc,CAAC,UAAU,CAAC;KAC1D,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,aAAoB;IACnD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAsB,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,IAAI,aAAa;QAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,OAAO,IAAI,KAAK,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;AAC5D,CAAC"}
1
+ {"version":3,"file":"agent-factory.js","sourceRoot":"","sources":["../src/agent-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEtE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,UAAU,GAAG,mBAAmB,CAAC;AACvC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,6BAA6B,CAAC;AAEnF,KAAK,UAAU,UAAU,CAAC,MAAc;IACtC,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,EAAE,eAAe,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAChF,OAAO,IAAI,OAAO,CAAC;QACjB,YAAY,EAAE,EAAE;QAChB,cAAc,EAAE,MAAM,QAAQ,CAAC,cAAc,CAAC,UAAU,CAAC;KAC1D,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,aAAoB;IACnD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAsB,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,IAAI,aAAa;QAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,OAAO,IAAI,KAAK,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;AAC5D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"agent-runtime.d.ts","sourceRoot":"","sources":["../src/agent-runtime.ts"],"names":[],"mappings":"AAwDA,wBAAsB,eAAe,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgN5E"}
1
+ {"version":3,"file":"agent-runtime.d.ts","sourceRoot":"","sources":["../src/agent-runtime.ts"],"names":[],"mappings":"AA0RA,wBAAsB,eAAe,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyC5E"}
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import { Message } from "@ai-zen/agents-core";
3
3
  import { DeltaRenderer } from "./delta-renderer.js";
4
4
  import inquirer from "inquirer";
5
- import { readConfig, readMessages, saveMessages, clearMessages, saveSnapshot, listSnapshots, loadSnapshot } from "./config.js";
5
+ import { readConfig, readMessages, saveMessages, saveSnapshot, listSnapshots, loadSnapshot } from "./config.js";
6
6
  import { buildAgent } from "./agent-factory.js";
7
7
  import { contextSize, shouldMigrate, generateMigrationDoc, MAX_CONTEXT_CHARS } from "./migration.js";
8
8
  const SYSTEM_PROMPT = [
@@ -20,8 +20,9 @@ const SYSTEM_PROMPT = [
20
20
  "- 项目记忆: $(cwd)/.ai-zen/air/temp/*.md",
21
21
  "下次启动时用 shell 读取即可。这是你唯一的记忆方式。",
22
22
  ].join("\n");
23
- // 流式发送
23
+ // ==================== 流式发送 ====================
24
24
  async function sendAndPrint(agent, text) {
25
+ console.log(chalk.green.bold("\n🤖 AI:"));
25
26
  const renderer = new DeltaRenderer({
26
27
  reasoningHeader: "\n\n💭 思考中...\n",
27
28
  contentHeader: "\n\n💭 回答中...\n",
@@ -41,261 +42,273 @@ async function sendAndPrint(agent, text) {
41
42
  process.stdout.write("\n\n");
42
43
  console.log();
43
44
  }
44
- async function sendAndSave(agent, text) {
45
- console.log(chalk.green.bold("\n🤖 AI:"));
46
- await sendAndPrint(agent, text);
45
+ // ==================== 公共操作 ====================
46
+ async function saveAndNew(ctx) {
47
+ const msgs = [Message.System(SYSTEM_PROMPT)];
48
+ saveMessages(msgs);
49
+ ctx.agent = await buildAgent(msgs);
47
50
  }
48
- // 对话循环
49
- export async function runConversation(initialMessage) {
50
- const config = readConfig();
51
- if (!config.apiKey) {
52
- console.error("❌ 请先设置 API Key: air key <your-key>");
53
- console.error(" 获取 Key: https://platform.deepseek.com/api_keys");
54
- process.exit(1);
55
- }
56
- let msgs = readMessages();
57
- if (msgs.length === 0) {
58
- msgs = [Message.System(SYSTEM_PROMPT)];
59
- saveMessages(msgs);
60
- }
61
- let agent = await buildAgent(msgs);
62
- if (initialMessage) {
63
- // 有旧对话则先存档,再开新对话
64
- const hasHistory = agent.messages.some(m => m.role === "user" || m.role === "assistant");
65
- if (hasHistory) {
66
- const snap = saveSnapshot(agent.messages);
67
- console.log(`💾 已存档旧对话: ${snap}\n`);
68
- }
69
- msgs = [Message.System(SYSTEM_PROMPT)];
70
- saveMessages(msgs);
71
- agent = await buildAgent(msgs);
72
- console.log(`💬 你: ${initialMessage}`);
73
- try {
74
- await sendAndSave(agent, initialMessage);
75
- saveMessages(agent.messages);
76
- console.log("💾 已保存\n");
77
- }
78
- catch (err) {
79
- console.error(`\n❌ ${err.message}`);
80
- }
81
- return;
82
- }
83
- const msgCount = agent.messages.filter((m) => m.role !== "system").length;
84
- console.log(msgCount > 0
85
- ? `\n💬 继续上次对话 (${msgCount} 条,/${contextSize(agent.messages)} 字符,输入 /new 重新开始)\n`
86
- : "\n💬 air — 极简 AI 助手 (输入 /help 查看命令)\n");
87
- async function ask() {
51
+ // ==================== 对话循环 ====================
52
+ async function ask(ctx) {
53
+ while (true) {
88
54
  const { input } = await inquirer.prompt([
89
55
  { type: "input", name: "input", message: "你:" },
90
56
  ]);
91
57
  const t = input.trim();
92
- if (!t) {
93
- await ask();
94
- return;
95
- }
58
+ if (!t)
59
+ continue;
96
60
  if (t.startsWith("/")) {
97
- const c = t.toLowerCase();
98
- if (c === "/exit" || c === "/quit") {
99
- console.log("\n👋 再见!");
100
- process.exit(0);
101
- }
102
- if (c === "/save") {
103
- console.log(`\n✅ 快照: ${saveSnapshot(agent.messages)}\n`);
104
- await ask();
105
- return;
106
- }
107
- if (c === "/new") {
108
- clearMessages();
109
- const msgs = [Message.System(SYSTEM_PROMPT)];
110
- saveMessages(msgs);
111
- agent = await buildAgent(msgs);
112
- console.log("\n🆕 重新开始\n");
113
- await ask();
114
- return;
115
- }
116
- if (c === "/load") {
117
- const snapshots = listSnapshots();
118
- if (snapshots.length === 0) {
119
- console.log("\n📭 没有可用的快照\n");
120
- await ask();
121
- return;
122
- }
123
- const { selectedName } = await inquirer.prompt([
124
- {
125
- type: "list",
126
- name: "selectedName",
127
- message: "选择要加载的快照:",
128
- pageSize: 15,
129
- choices: [
130
- { name: "↩️ 取消操作", value: "" },
131
- ...snapshots.map((s) => ({ name: s.date, value: s.name })),
132
- ],
133
- },
134
- ]);
135
- if (!selectedName) {
136
- console.log("\n已取消\n");
137
- await ask();
138
- return;
139
- }
140
- let msgs = loadSnapshot(selectedName);
141
- if (msgs.length === 0) {
142
- msgs = [Message.System(SYSTEM_PROMPT)];
143
- }
61
+ await dispatchCommand(ctx, t.toLowerCase());
62
+ continue;
63
+ }
64
+ await handleMessage(ctx, t);
65
+ }
66
+ }
67
+ async function dispatchCommand(ctx, cmd) {
68
+ switch (cmd) {
69
+ case "/exit":
70
+ case "/quit":
71
+ cmdExit(ctx);
72
+ break;
73
+ case "/save":
74
+ await cmdSave(ctx);
75
+ break;
76
+ case "/new":
77
+ await cmdNew(ctx);
78
+ break;
79
+ case "/load":
80
+ await cmdLoad(ctx);
81
+ break;
82
+ case "/back":
83
+ await handleBack(ctx);
84
+ break;
85
+ case "/editor":
86
+ await cmdEditor(ctx);
87
+ break;
88
+ case "/help":
89
+ cmdHelp();
90
+ break;
91
+ default:
92
+ console.log(`\n❌ 未知命令: ${cmd}\n`);
93
+ break;
94
+ }
95
+ }
96
+ async function cmdSave(ctx) {
97
+ console.log(`\n✅ 快照: ${saveSnapshot(ctx.agent.messages)}\n`);
98
+ }
99
+ async function cmdNew(ctx) {
100
+ await saveAndNew(ctx);
101
+ console.log("\n🆕 重新开始\n");
102
+ }
103
+ function cmdHelp() {
104
+ console.log("\n/exit /quit 退出\n/save 保存快照\n/load 加载快照\n/new 重新开始\n/back 撤回消息\n/help 帮助\n");
105
+ }
106
+ async function cmdExit(ctx) {
107
+ console.log("\n👋 再见!");
108
+ process.exit(0);
109
+ }
110
+ async function cmdLoad(ctx) {
111
+ const snapshots = listSnapshots();
112
+ if (snapshots.length === 0) {
113
+ console.log("\n📭 没有可用的快照\n");
114
+ ctx.agent = await buildAgent([]);
115
+ return;
116
+ }
117
+ const { selectedName } = await inquirer.prompt([
118
+ {
119
+ type: "list",
120
+ name: "selectedName",
121
+ message: "选择要加载的快照:",
122
+ pageSize: 15,
123
+ choices: [
124
+ { name: "\u21a9\ufe0f 取消操作", value: "" },
125
+ ...snapshots.map((s) => ({ name: s.date, value: s.name })),
126
+ ],
127
+ },
128
+ ]);
129
+ if (!selectedName) {
130
+ console.log("\n已取消\n");
131
+ ctx.agent = await buildAgent([]);
132
+ return;
133
+ }
134
+ let loaded = loadSnapshot(selectedName);
135
+ if (loaded.length === 0) {
136
+ loaded = [Message.System(SYSTEM_PROMPT)];
137
+ }
138
+ saveMessages(loaded);
139
+ const snap = snapshots.find((s) => s.name === selectedName);
140
+ console.log("\n✅ 已加载快照: " + (snap ? snap.date : selectedName) + "\n");
141
+ ctx.agent = await buildAgent(loaded);
142
+ }
143
+ async function cmdEditor(ctx) {
144
+ const { content } = await inquirer.prompt([
145
+ { type: "editor", name: "content", message: "编辑消息:" },
146
+ ]);
147
+ if (!content.trim()) {
148
+ console.log("\n已取消\n");
149
+ return;
150
+ }
151
+ try {
152
+ await sendAndPrint(ctx.agent, content.trim());
153
+ saveMessages(ctx.agent.messages);
154
+ }
155
+ catch (err) {
156
+ console.error(`\n❌ ${err.message}`);
157
+ }
158
+ }
159
+ async function handleMessage(ctx, text) {
160
+ try {
161
+ await sendAndPrint(ctx.agent, text);
162
+ saveMessages(ctx.agent.messages);
163
+ if (shouldMigrate(ctx.agent.messages)) {
164
+ console.log(`🔄 上下文 ${contextSize(ctx.agent.messages)}/${MAX_CONTEXT_CHARS},准备迁移...`);
165
+ try {
166
+ const snap = saveSnapshot(ctx.agent.messages);
167
+ console.log(` 💾 快照: ${snap}`);
168
+ const summary = await generateMigrationDoc(ctx.agent.messages);
169
+ const msgs = [Message.System(SYSTEM_PROMPT), Message.User(summary)];
144
170
  saveMessages(msgs);
145
- agent = await buildAgent(msgs);
146
- const snap = snapshots.find((s) => s.name === selectedName);
147
- console.log("\n✅ 已加载快照: " + (snap ? snap.date : selectedName) + "\n");
148
- await ask();
149
- return;
150
- }
151
- if (c === "/back") {
152
- await handleBack();
153
- await ask();
154
- return;
155
- }
156
- if (c === "/editor") {
157
- const { content } = await inquirer.prompt([
158
- { type: "editor", name: "content", message: "编辑消息:" },
159
- ]);
160
- if (!content.trim()) {
161
- console.log("\n已取消\n");
162
- await ask();
163
- return;
164
- }
165
- try {
166
- await sendAndSave(agent, content.trim());
167
- saveMessages(agent.messages);
168
- }
169
- catch (err) {
170
- console.error(`\n❌ ${err.message}`);
171
- }
172
- await ask();
173
- return;
171
+ console.log("✅ 迁移完成\n");
172
+ ctx.agent = await buildAgent(msgs);
174
173
  }
175
- if (c === "/help") {
176
- console.log("\n/exit /quit 退出\n/save 保存快照\n/load 加载快照\n/new 重新开始\n/back 撤回消息\n/help 帮助\n");
177
- await ask();
178
- return;
174
+ catch (err) {
175
+ console.error(`❌ 迁移失败: ${err.message}\n`);
179
176
  }
180
- console.log(`\n❌ 未知命令: ${t}\n`);
181
- await ask();
182
- return;
183
177
  }
184
- try {
185
- await sendAndSave(agent, t);
186
- saveMessages(agent.messages);
187
- if (shouldMigrate(agent.messages)) {
188
- console.log(`🔄 上下文 ${contextSize(agent.messages)}/${MAX_CONTEXT_CHARS},准备迁移...`);
189
- try {
190
- const snap = saveSnapshot(agent.messages);
191
- console.log(` 💾 快照: ${snap}`);
192
- const summary = await generateMigrationDoc(agent.messages);
193
- const msgs = [Message.System(SYSTEM_PROMPT), Message.User(summary)];
194
- saveMessages(msgs);
195
- agent = await buildAgent(msgs);
196
- console.log("✅ 迁移完成\n");
197
- }
198
- catch (err) {
199
- console.error(`❌ 迁移失败: ${err.message}\n`);
200
- }
201
- }
202
- }
203
- catch (err) {
204
- console.error(`\n❌ ${err.message}`);
205
- }
206
- await ask();
207
178
  }
208
- async function handleBack() {
209
- const targets = [];
210
- for (let i = 0; i < agent.messages.length; i++) {
211
- const msg = agent.messages[i];
212
- if (msg.role === "user") {
213
- const text = typeof msg.content === "string" ? msg.content : "";
214
- if (text) {
215
- targets.push({ index: i, role: "user", label: "👤 用户", preview: text.substring(0, 60) + (text.length > 60 ? "..." : "") });
216
- }
217
- }
218
- else if (msg.role === "tool" || msg.role === "function") {
219
- const text = typeof msg.content === "string" ? msg.content : "";
220
- if (text) {
221
- targets.push({ index: i, role: msg.role, label: "🔧 工具", preview: text.substring(0, 60) + (text.length > 60 ? "..." : "") });
222
- }
179
+ catch (err) {
180
+ console.error(`\n❌ ${err.message}`);
181
+ }
182
+ }
183
+ async function handleBack(ctx) {
184
+ const targets = [];
185
+ for (let i = 0; i < ctx.agent.messages.length; i++) {
186
+ const msg = ctx.agent.messages[i];
187
+ if (msg.role === "user") {
188
+ const text = typeof msg.content === "string" ? msg.content : "";
189
+ if (text) {
190
+ targets.push({ index: i, role: "user", label: "\u{1F464} 用户", preview: text.substring(0, 60) + (text.length > 60 ? "..." : "") });
223
191
  }
224
192
  }
225
- if (targets.length === 0) {
226
- console.log("\n❌ 还没有消息可以撤回\n");
227
- return;
193
+ else if (msg.role === "tool" || msg.role === "function") {
194
+ const text = typeof msg.content === "string" ? msg.content : "";
195
+ if (text) {
196
+ targets.push({ index: i, role: msg.role, label: "\u{1F527} 工具", preview: text.substring(0, 60) + (text.length > 60 ? "..." : "") });
197
+ }
228
198
  }
229
- console.log("\n📋 选择要撤回到哪条消息(将删除所选及其之后的所有内容):\n");
230
- const { selectedIndex } = await inquirer.prompt([
199
+ }
200
+ if (targets.length === 0) {
201
+ console.log("\n❌ 还没有消息可以撤回\n");
202
+ return;
203
+ }
204
+ console.log("\n📋 选择要撤回到哪条消息(将删除所选及其之后的所有内容):\n");
205
+ const { selectedIndex } = await inquirer.prompt([
206
+ {
207
+ type: "list",
208
+ name: "selectedIndex",
209
+ message: "撤回到:",
210
+ pageSize: 15,
211
+ choices: [
212
+ { name: "\u21a9\ufe0f 取消操作", value: -1 },
213
+ ...targets.map((t) => ({ name: t.label + " " + t.preview, value: t.index })),
214
+ ],
215
+ },
216
+ ]);
217
+ if (selectedIndex === -1) {
218
+ console.log("\n已取消\n");
219
+ return;
220
+ }
221
+ const selectedMsg = ctx.agent.messages[selectedIndex];
222
+ const isUserMsg = selectedMsg.role === "user";
223
+ const originalText = typeof selectedMsg.content === "string" ? selectedMsg.content : "";
224
+ const sliceEnd = isUserMsg ? selectedIndex : selectedIndex + 1;
225
+ ctx.agent.messages = ctx.agent.messages.slice(0, sliceEnd);
226
+ if (isUserMsg) {
227
+ console.log("\n原内容: " + originalText.substring(0, 200) + (originalText.length > 200 ? "..." : "") + "\n");
228
+ const { editChoice } = await inquirer.prompt([
231
229
  {
232
230
  type: "list",
233
- name: "selectedIndex",
234
- message: "撤回到:",
235
- pageSize: 15,
231
+ name: "editChoice",
232
+ message: "请选择:",
236
233
  choices: [
237
- { name: "↩️ 取消操作", value: -1 },
238
- ...targets.map((t) => ({ name: t.label + " " + t.preview, value: t.index })),
234
+ { name: "\u270f\ufe0f 修改后重新发送", value: "edit" },
235
+ { name: "\u{1F504} 直接重新发送", value: "resend" },
236
+ { name: "\u21a9\ufe0f 取消操作", value: "cancel" },
239
237
  ],
240
238
  },
241
239
  ]);
242
- if (selectedIndex === -1) {
240
+ if (editChoice === "cancel") {
243
241
  console.log("\n已取消\n");
244
242
  return;
245
243
  }
246
- const selectedMsg = agent.messages[selectedIndex];
247
- const isUserMsg = selectedMsg.role === "user";
248
- const originalText = typeof selectedMsg.content === "string" ? selectedMsg.content : "";
249
- const sliceEnd = isUserMsg ? selectedIndex : selectedIndex + 1;
250
- agent.messages = agent.messages.slice(0, sliceEnd);
251
- if (isUserMsg) {
252
- console.log("\n原内容: " + originalText.substring(0, 200) + (originalText.length > 200 ? "..." : "") + "\n");
253
- const { editChoice } = await inquirer.prompt([
254
- {
255
- type: "list",
256
- name: "editChoice",
257
- message: "请选择:",
258
- choices: [
259
- { name: "✏️ 修改后重新发送", value: "edit" },
260
- { name: "🔄 直接重新发送", value: "resend" },
261
- { name: "↩️ 取消操作", value: "cancel" },
262
- ],
263
- },
244
+ let textToSend = originalText;
245
+ if (editChoice === "edit") {
246
+ const { editedContent } = await inquirer.prompt([
247
+ { type: "input", name: "editedContent", message: "修改消息:", default: originalText },
264
248
  ]);
265
- if (editChoice === "cancel") {
266
- console.log("\n已取消\n");
249
+ if (!editedContent.trim()) {
250
+ console.log("\n❌ 消息不能为空\n");
267
251
  return;
268
252
  }
269
- let textToSend = originalText;
270
- if (editChoice === "edit") {
271
- const { editedContent } = await inquirer.prompt([
272
- { type: "input", name: "editedContent", message: "修改消息:", default: originalText },
273
- ]);
274
- if (!editedContent.trim()) {
275
- console.log("\n❌ 消息不能为空\n");
276
- return;
277
- }
278
- textToSend = editedContent.trim();
279
- }
280
- agent = await buildAgent(agent.messages);
281
- await sendAndSave(agent, textToSend);
282
- saveMessages(agent.messages);
253
+ textToSend = editedContent.trim();
283
254
  }
284
- else {
285
- console.log("\n💡 请输入一条新消息继续对话\n");
286
- const { newMessage } = await inquirer.prompt([
287
- { type: "input", name: "newMessage", message: "新消息:" },
288
- ]);
289
- if (!newMessage.trim()) {
290
- console.log("\n已取消\n");
291
- return;
292
- }
293
- agent = await buildAgent(agent.messages);
294
- await sendAndSave(agent, newMessage.trim());
295
- saveMessages(agent.messages);
255
+ ctx.agent = await buildAgent(ctx.agent.messages);
256
+ await sendAndPrint(ctx.agent, textToSend);
257
+ saveMessages(ctx.agent.messages);
258
+ }
259
+ else {
260
+ console.log("\n💡 请输入一条新消息继续对话\n");
261
+ const { newMessage } = await inquirer.prompt([
262
+ { type: "input", name: "newMessage", message: "新消息:" },
263
+ ]);
264
+ if (!newMessage.trim()) {
265
+ console.log("\n已取消\n");
266
+ return;
296
267
  }
268
+ ctx.agent = await buildAgent(ctx.agent.messages);
269
+ await sendAndPrint(ctx.agent, newMessage.trim());
270
+ saveMessages(ctx.agent.messages);
297
271
  }
298
- await ask();
272
+ }
273
+ export async function runConversation(initialMessage) {
274
+ const config = readConfig();
275
+ if (!config.apiKey) {
276
+ console.error("❌ 请先设置 API Key: air key <your-key>");
277
+ console.error(" 获取 Key: https://platform.deepseek.com/api_keys");
278
+ process.exit(1);
279
+ }
280
+ let msgs = readMessages();
281
+ if (msgs.length === 0) {
282
+ msgs = [Message.System(SYSTEM_PROMPT)];
283
+ saveMessages(msgs);
284
+ }
285
+ const agent = await buildAgent(msgs);
286
+ const ctx = { agent };
287
+ if (initialMessage) {
288
+ // 有旧对话则先存档,再开新对话
289
+ const hasHistory = ctx.agent.messages.some(m => m.role === "user" || m.role === "assistant");
290
+ if (hasHistory) {
291
+ const snap = saveSnapshot(ctx.agent.messages);
292
+ console.log(`💾 已存档旧对话: ${snap}\n`);
293
+ }
294
+ await saveAndNew(ctx);
295
+ console.log(`💬 你: ${initialMessage}`);
296
+ try {
297
+ await sendAndPrint(ctx.agent, initialMessage);
298
+ saveMessages(ctx.agent.messages);
299
+ }
300
+ catch (err) {
301
+ console.error(`\n❌ ${err.message}`);
302
+ }
303
+ console.log("\n💬 继续对话 (输入 /exit 退出)\n");
304
+ await ask(ctx);
305
+ return;
306
+ }
307
+ const msgCount = ctx.agent.messages.filter((m) => m.role !== "system").length;
308
+ console.log(msgCount > 0
309
+ ? `\n💬 继续上次对话 (${msgCount} 条,/${contextSize(ctx.agent.messages)} 字符,输入 /new 重新开始)\n`
310
+ : "\n💬 air — 极简 AI 助手 (输入 /help 查看命令)\n");
311
+ await ask(ctx);
299
312
  process.on("SIGINT", () => { console.log("\n\n👋 再见!"); process.exit(0); });
300
313
  }
301
314
  //# sourceMappingURL=agent-runtime.js.map