nine-hundred 0.0.1

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 (107) hide show
  1. package/README.md +43 -0
  2. package/dist/agent/config/index.js +85 -0
  3. package/dist/agent/context/compress.js +145 -0
  4. package/dist/agent/context/index.js +68 -0
  5. package/dist/agent/context/token.js +30 -0
  6. package/dist/agent/hooks/config.js +41 -0
  7. package/dist/agent/hooks/core.js +177 -0
  8. package/dist/agent/hooks/events/index.js +9 -0
  9. package/dist/agent/hooks/events/post-tool-use-hook.js +7 -0
  10. package/dist/agent/hooks/events/pre-tool-use-hook.js +8 -0
  11. package/dist/agent/hooks/events/session-start-hook.js +8 -0
  12. package/dist/agent/hooks/events/stop-hook.js +10 -0
  13. package/dist/agent/hooks/events/user-prompt-submit-hook.js +7 -0
  14. package/dist/agent/hooks/index.js +7 -0
  15. package/dist/agent/hooks/types.js +32 -0
  16. package/dist/agent/index.js +465 -0
  17. package/dist/agent/llm/index.js +35 -0
  18. package/dist/agent/mcp/client.js +87 -0
  19. package/dist/agent/mcp/config.js +51 -0
  20. package/dist/agent/mcp/index.js +90 -0
  21. package/dist/agent/mcp/schema.js +56 -0
  22. package/dist/agent/permission/exec.js +71 -0
  23. package/dist/agent/permission/network.js +37 -0
  24. package/dist/agent/permission/read.js +27 -0
  25. package/dist/agent/permission/util/command-changes-directory.js +29 -0
  26. package/dist/agent/permission/util/dangerous-path.json +116 -0
  27. package/dist/agent/permission/util/detect-dangerous-operation.js +230 -0
  28. package/dist/agent/permission/util/detect-language-interpreter.js +66 -0
  29. package/dist/agent/permission/util/detect-safe-command.js +61 -0
  30. package/dist/agent/permission/util/index.js +16 -0
  31. package/dist/agent/permission/util/is-dangerous-path.js +98 -0
  32. package/dist/agent/permission/util/is-inside-cwd.js +83 -0
  33. package/dist/agent/permission/util/is-safe-domains.js +47 -0
  34. package/dist/agent/permission/write.js +31 -0
  35. package/dist/agent/prompt/index.js +42 -0
  36. package/dist/agent/skills/planner/SKILL.md +90 -0
  37. package/dist/agent/skills/programmer-resume/SKILL.md +113 -0
  38. package/dist/agent/skills/skill-creator/LICENSE.txt +202 -0
  39. package/dist/agent/skills/skill-creator/SKILL.md +495 -0
  40. package/dist/agent/skills/skill-creator/agents/analyzer.md +274 -0
  41. package/dist/agent/skills/skill-creator/agents/comparator.md +202 -0
  42. package/dist/agent/skills/skill-creator/agents/grader.md +223 -0
  43. package/dist/agent/skills/skill-creator/assets/eval_review.html +146 -0
  44. package/dist/agent/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  45. package/dist/agent/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  46. package/dist/agent/skills/skill-creator/references/schemas.md +430 -0
  47. package/dist/agent/skills/skill-creator/scripts/__init__.py +0 -0
  48. package/dist/agent/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  49. package/dist/agent/skills/skill-creator/scripts/generate_report.py +326 -0
  50. package/dist/agent/skills/skill-creator/scripts/improve_description.py +247 -0
  51. package/dist/agent/skills/skill-creator/scripts/package_skill.py +136 -0
  52. package/dist/agent/skills/skill-creator/scripts/quick_validate.py +103 -0
  53. package/dist/agent/skills/skill-creator/scripts/run_eval.py +310 -0
  54. package/dist/agent/skills/skill-creator/scripts/run_loop.py +328 -0
  55. package/dist/agent/skills/skill-creator/scripts/utils.py +47 -0
  56. package/dist/agent/skills.js +129 -0
  57. package/dist/agent/tools/agent_tool/agent_tool.test.js +64 -0
  58. package/dist/agent/tools/agent_tool/index.js +33 -0
  59. package/dist/agent/tools/exec_tool/exec_tool.test.js +48 -0
  60. package/dist/agent/tools/exec_tool/index.js +44 -0
  61. package/dist/agent/tools/load_skill_tool/index.js +5 -0
  62. package/dist/agent/tools/load_skill_tool/load_skill_tool.test.js +122 -0
  63. package/dist/agent/tools/memory_create_tool/index.js +8 -0
  64. package/dist/agent/tools/memory_create_tool/memory_create_tool.test.js +54 -0
  65. package/dist/agent/tools/memory_delete_tool/index.js +10 -0
  66. package/dist/agent/tools/memory_delete_tool/memory_delete_tool.test.js +39 -0
  67. package/dist/agent/tools/memory_retrieve_tool/index.js +61 -0
  68. package/dist/agent/tools/memory_retrieve_tool/memory_retrieve_tool.test.js +102 -0
  69. package/dist/agent/tools/profile_update_tool/index.js +30 -0
  70. package/dist/agent/tools/profile_update_tool/profile_update_tool.test.js +49 -0
  71. package/dist/agent/tools/read_file_tool/index.js +24 -0
  72. package/dist/agent/tools/read_file_tool/read_file_tool.test.js +43 -0
  73. package/dist/agent/tools/run_js_tool/index.js +48 -0
  74. package/dist/agent/tools/run_js_tool/run_js_tool.test.js +67 -0
  75. package/dist/agent/tools/run_py_tool/index.js +48 -0
  76. package/dist/agent/tools/run_py_tool/run_py_tool.test.js +67 -0
  77. package/dist/agent/tools/tool_logger.js +16 -0
  78. package/dist/agent/tools/tool_logger.test.js +22 -0
  79. package/dist/agent/tools/web_fetch_url/index.js +76 -0
  80. package/dist/agent/tools/web_fetch_url/web_fetch_url.test.js +102 -0
  81. package/dist/agent/tools/web_search_tool/index.js +26 -0
  82. package/dist/agent/tools/web_search_tool/web_search_tool.test.js +61 -0
  83. package/dist/agent/tools/write_file_tool/index.js +22 -0
  84. package/dist/agent/tools/write_file_tool/write_file_tool.test.js +46 -0
  85. package/dist/agent/tools.js +218 -0
  86. package/dist/cli/command/compact/index.js +14 -0
  87. package/dist/cli/command/index.js +62 -0
  88. package/dist/cli/command/invalid/index.js +4 -0
  89. package/dist/cli/command/new/chat-session.js +10 -0
  90. package/dist/cli/command/new/index.js +8 -0
  91. package/dist/cli/command/rewind/index.js +19 -0
  92. package/dist/cli/command/rewind/rewind-command.test.js +22 -0
  93. package/dist/cli/command/session/format.js +32 -0
  94. package/dist/cli/command/session/index.js +32 -0
  95. package/dist/cli/command/session/session-command.test.js +49 -0
  96. package/dist/cli/command/unknown/index.js +4 -0
  97. package/dist/cli/index.js +144 -0
  98. package/dist/db/checkpointer.js +15 -0
  99. package/dist/db/index.js +2 -0
  100. package/dist/db/path.js +8 -0
  101. package/dist/db/sessions.js +81 -0
  102. package/dist/db/tables/memory.js +12 -0
  103. package/dist/db/tables/memory_fts.js +29 -0
  104. package/dist/index.js +87 -0
  105. package/dist/install.js +154 -0
  106. package/package.json +51 -0
  107. package/pnpm-workspace.yaml +3 -0
@@ -0,0 +1,218 @@
1
+ import { tool } from '@langchain/core/tools';
2
+ import { z } from 'zod';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { mkdir, writeFile } from 'node:fs/promises';
6
+ import { agentTool as agentToolImpl, agentToolSchema } from './tools/agent_tool/index.js';
7
+ import { execTool as execToolImpl } from './tools/exec_tool/index.js';
8
+ import { loadSkillTool as loadSkillToolImpl } from './tools/load_skill_tool/index.js';
9
+ import { readFileTool as readFileToolImpl } from './tools/read_file_tool/index.js';
10
+ import { runJsTool as runJsToolImpl } from './tools/run_js_tool/index.js';
11
+ import { runPyTool as runPyToolImpl } from './tools/run_py_tool/index.js';
12
+ import { memoryCreateTool as memoryCreateToolImpl } from './tools/memory_create_tool/index.js';
13
+ import { memoryDeleteTool as memoryDeleteToolImpl } from './tools/memory_delete_tool/index.js';
14
+ import { memoryRetrieveTool as memoryRetrieveToolImpl } from './tools/memory_retrieve_tool/index.js';
15
+ import { profileUpdateTool as profileUpdateToolImpl } from './tools/profile_update_tool/index.js';
16
+ import { writeFileTool as writeFileToolImpl } from './tools/write_file_tool/index.js';
17
+ import { webFetchUrl as webFetchUrlImpl } from './tools/web_fetch_url/index.js';
18
+ import { webSearchTool as webSearchToolImpl } from './tools/web_search_tool/index.js';
19
+ import { getMcpToolWrappers } from './mcp/index.js';
20
+ export { registerSubagentRunner } from './tools/agent_tool/index.js';
21
+ function create900Tool(impl, options) {
22
+ const t = tool(impl, {
23
+ name: options.name,
24
+ description: options.description,
25
+ schema: options.schema,
26
+ });
27
+ const wrapped = t;
28
+ wrapped.permission_level = options.permissionLevel;
29
+ return wrapped;
30
+ }
31
+ const loadSkillTool = create900Tool(async ({ skillName }) => loadSkillToolImpl(skillName), {
32
+ name: 'load_skill_tool',
33
+ description: 'Load the full SKILL.md for exactly one available skill by name.',
34
+ schema: z.object({
35
+ skillName: z.string().describe('Name of exactly one available skill to load.'),
36
+ }),
37
+ permissionLevel: 'read',
38
+ });
39
+ const readFileTool = create900Tool(async ({ filePath }) => readFileToolImpl(filePath), {
40
+ name: 'read_file_tool',
41
+ description: 'Read the contents of a UTF-8 text file.',
42
+ schema: z.object({
43
+ filePath: z.string().describe('Absolute or relative path to the local file to read.'),
44
+ }),
45
+ permissionLevel: 'read',
46
+ });
47
+ const writeFileTool = create900Tool(async ({ filePath, content }) => writeFileToolImpl(filePath, content), {
48
+ name: 'write_file_tool',
49
+ description: 'Create or overwrite a file. Will create parent directories if needed.',
50
+ schema: z.object({
51
+ filePath: z.string().describe('Absolute or relative path to the local file to write.'),
52
+ content: z.string().describe('UTF-8 text content to write to the file.'),
53
+ }),
54
+ permissionLevel: 'write',
55
+ });
56
+ const execTool = create900Tool(async ({ command, cwd }) => execToolImpl(command, cwd), {
57
+ name: 'exec_tool',
58
+ description: 'Execute a safe shell command inside the current working directory. Dangerous file operations are not allowed.',
59
+ schema: z.object({
60
+ command: z.string().describe('Shell command to execute.'),
61
+ cwd: z
62
+ .string()
63
+ .optional()
64
+ .describe('Optional working directory relative to the current working directory.'),
65
+ }),
66
+ permissionLevel: 'exec',
67
+ });
68
+ const runJsTool = create900Tool(async ({ code, cwd }) => runJsToolImpl(code, cwd), {
69
+ name: 'run_js_tool',
70
+ description: 'Execute JavaScript code with local Node.js and return stdout, stderr, or errors.',
71
+ schema: z.object({
72
+ code: z.string().describe('JavaScript code to execute with local Node.js.'),
73
+ cwd: z
74
+ .string()
75
+ .optional()
76
+ .describe('Optional working directory relative to the current working directory.'),
77
+ }),
78
+ permissionLevel: 'exec',
79
+ });
80
+ const runPyTool = create900Tool(async ({ code, cwd }) => runPyToolImpl(code, cwd), {
81
+ name: 'run_py_tool',
82
+ description: 'Execute Python code with local Python 3 and return stdout, stderr, or errors.',
83
+ schema: z.object({
84
+ code: z.string().describe('Python code to execute with local Python 3.'),
85
+ cwd: z
86
+ .string()
87
+ .optional()
88
+ .describe('Optional working directory relative to the current working directory.'),
89
+ }),
90
+ permissionLevel: 'exec',
91
+ });
92
+ const webFetchUrl = create900Tool(async ({ url }) => webFetchUrlImpl(url), {
93
+ name: 'web_fetch_url',
94
+ description: 'Fetch a web page or HTTP API URL and return text content or a readable failure message.',
95
+ schema: z.object({
96
+ url: z.string().describe('The HTTP or HTTPS URL to fetch.'),
97
+ }),
98
+ permissionLevel: 'network',
99
+ });
100
+ const webSearchTool = create900Tool(async ({ query }) => webSearchToolImpl(query), {
101
+ name: 'web_search_tool',
102
+ description: 'Search the web using Tavily and return relevant results.',
103
+ schema: z.object({
104
+ query: z.string().describe('The web search query.'),
105
+ }),
106
+ permissionLevel: 'network',
107
+ });
108
+ const memoryCreateTool = create900Tool(async ({ type, content, keywords, importance }, config) => {
109
+ const threadId = config?.configurable?.thread_id;
110
+ return memoryCreateToolImpl(type, content, keywords, importance, threadId);
111
+ }, {
112
+ name: 'memory_create_tool',
113
+ description: '当用户要求记住、保存、记录某些信息,或对话中产生了值得长期保留的事实、事件、偏好或技能时,调用此工具将记忆写入数据库。',
114
+ schema: z.object({
115
+ type: z
116
+ .enum(['fact', 'event', 'preference', 'skill'])
117
+ .describe('记忆类型:fact(事实)、event(事件)、preference(偏好)、skill(技能)。'),
118
+ content: z.string().describe('需要保存的记忆内容,用自然语言描述。'),
119
+ keywords: z.array(z.string()).optional().describe('用于检索的关键词列表。'),
120
+ importance: z.number().min(1).max(5).optional().describe('重要程度,1~5,默认3。'),
121
+ }),
122
+ permissionLevel: 'db',
123
+ });
124
+ const memoryRetrieveTool = create900Tool(async ({ query, limit }) => memoryRetrieveToolImpl(query, limit), {
125
+ name: 'memory_retrieve_tool',
126
+ description: '当当前对话上下文中没有用户所需信息时,根据整理的关键词静默调用此工具从长期记忆数据库中检索相关内容。调用前不要向用户说明正在检索记忆,直接调用即可。',
127
+ schema: z.object({
128
+ query: z
129
+ .string()
130
+ .describe('搜索查询短语,由用户输入整理的关键词组成(空格分隔多个关键词)。'),
131
+ limit: z.number().min(1).max(50).optional().describe('返回结果数量上限,默认 10。'),
132
+ }),
133
+ permissionLevel: 'db',
134
+ });
135
+ const memoryDeleteTool = create900Tool(async ({ id }) => memoryDeleteToolImpl(id), {
136
+ name: 'memory_delete_tool',
137
+ description: '当用户要求删除或遗忘某条记忆时,调用此工具从数据库中删除指定 id 的记忆。',
138
+ schema: z.object({
139
+ id: z.number().int().positive().describe('要删除的记忆 id。'),
140
+ }),
141
+ permissionLevel: 'db',
142
+ });
143
+ const profileUpdateTool = create900Tool(async ({ content }) => profileUpdateToolImpl(content), {
144
+ name: 'profile_update_tool',
145
+ description: '当用户的基本身份、外貌、性格、兴趣爱好、技能、工作等 profile 信息发生变化时,调用此工具更新 profile 文件。更新时必须包含所有已有的 profile 信息,完整写入,不能只写变更的部分。',
146
+ schema: z.object({
147
+ content: z.string().describe('完整的 profile 内容,需包含所有已有的 profile 信息,不能遗漏。'),
148
+ }),
149
+ permissionLevel: 'write',
150
+ });
151
+ const agentTool = create900Tool(async ({ prompt }, config) => {
152
+ const parentThreadId = config?.configurable
153
+ ?.thread_id;
154
+ return agentToolImpl(prompt, parentThreadId);
155
+ }, {
156
+ name: 'agent_tool',
157
+ description: 'Start exactly one subagent with a plain text prompt. The subagent has its own isolated context and cannot start nested subagents.',
158
+ schema: agentToolSchema,
159
+ permissionLevel: 'exec',
160
+ });
161
+ export const baseTools = [
162
+ // search,
163
+ loadSkillTool,
164
+ readFileTool,
165
+ writeFileTool,
166
+ execTool,
167
+ runJsTool,
168
+ runPyTool,
169
+ webFetchUrl,
170
+ webSearchTool,
171
+ memoryCreateTool,
172
+ memoryRetrieveTool,
173
+ memoryDeleteTool,
174
+ profileUpdateTool,
175
+ ];
176
+ export const tools = [...baseTools, agentTool];
177
+ export const subagentTools = baseTools;
178
+ export function getToolsForRunMode(mode) {
179
+ const base = mode === 'subagent' ? subagentTools : tools;
180
+ const baseNames = new Set(base.map((t) => t.name));
181
+ const mcpTools = getMcpToolWrappers()
182
+ .map((w) => {
183
+ if (baseNames.has(w.name)) {
184
+ console.warn(`[MCP] Tool name conflict: "${w.name}" already exists, skipping.`);
185
+ return null;
186
+ }
187
+ return create900Tool(w.invoke, {
188
+ name: w.name,
189
+ description: w.description,
190
+ schema: w.schema,
191
+ permissionLevel: 'mcp',
192
+ });
193
+ })
194
+ .filter(Boolean);
195
+ return [...base, ...mcpTools];
196
+ }
197
+ const PERSIST_THRESHOLD = 50_000;
198
+ const PREVIEW_LENGTH = 2_000;
199
+ export async function maybePersistedOutput(content, toolCallId) {
200
+ if (content.length <= PERSIST_THRESHOLD) {
201
+ return content;
202
+ }
203
+ const dir = path.join(os.homedir(), '.900', '.tool_output');
204
+ await mkdir(dir, { recursive: true });
205
+ const filePath = `${dir}/tool_output_${toolCallId}.txt`;
206
+ await writeFile(filePath, content, 'utf8');
207
+ return [
208
+ '<persisted-output>',
209
+ `Output too large (${(content.length / 1024).toFixed(1)}KB).`,
210
+ `Full output saved to: ${filePath}`,
211
+ 'If you need the complete content, it is recommended to read it in segments',
212
+ '',
213
+ 'Preview (first 2KB):',
214
+ content.slice(0, PREVIEW_LENGTH),
215
+ '...',
216
+ '</persisted-output>',
217
+ ].join('\n');
218
+ }
@@ -0,0 +1,14 @@
1
+ import chalk from 'chalk';
2
+ import { compressContext } from '../../../agent/index.js';
3
+ export async function handleCompactCommand(threadId) {
4
+ const result = await compressContext(threadId);
5
+ if (!result) {
6
+ console.log(chalk.yellow('没有可压缩的消息,或压缩失败。'));
7
+ return null;
8
+ }
9
+ console.log(chalk.yellow.bold('[提示] Context 已压缩,下一轮对话将使用摘要替代早期历史消息。'));
10
+ if (result.compressionCount >= 3) {
11
+ console.log(chalk.red.bold('[警告] 已多次压缩 Context,信息丢失风险很高,强烈建议输入 /new 命令开启新会话'));
12
+ }
13
+ return result;
14
+ }
@@ -0,0 +1,62 @@
1
+ import { handleCompactCommand } from './compact/index.js';
2
+ import { handleInvalidCommand } from './invalid/index.js';
3
+ import { handleNewCommand } from './new/index.js';
4
+ import { handleRewindCommand } from './rewind/index.js';
5
+ import { handleSessionCommand } from './session/index.js';
6
+ import { handleUnknownCommand } from './unknown/index.js';
7
+ export function parseSlashCommand(input) {
8
+ const trimmed = input.trim();
9
+ if (!trimmed.startsWith('/'))
10
+ return null;
11
+ const [rawName = '', ...args] = trimmed.slice(1).split(/\s+/);
12
+ const name = rawName.toLowerCase();
13
+ if (name === 'new') {
14
+ if (args.length > 0)
15
+ return { type: 'invalid', message: '/new 不接受参数' };
16
+ return { type: 'new' };
17
+ }
18
+ if (name === 'session') {
19
+ if (args.length > 0)
20
+ return { type: 'invalid', message: '/session 不接受参数' };
21
+ return { type: 'session' };
22
+ }
23
+ if (name === 'rewind') {
24
+ if (args.length !== 1 || !args[0]) {
25
+ return { type: 'invalid', message: '/rewind 需要 1 个参数:thread_id' };
26
+ }
27
+ return { type: 'rewind', threadId: args[0] };
28
+ }
29
+ if (name === 'compact') {
30
+ if (args.length > 0)
31
+ return { type: 'invalid', message: '/compact 不接受参数' };
32
+ return { type: 'compact' };
33
+ }
34
+ return { type: 'unknown', name, args };
35
+ }
36
+ export async function handleSlashCommand(input, threadId) {
37
+ const slashCommand = parseSlashCommand(input);
38
+ if (!slashCommand)
39
+ return { handled: false };
40
+ switch (slashCommand.type) {
41
+ case 'new': {
42
+ const result = handleNewCommand();
43
+ return { handled: true, session: result.session };
44
+ }
45
+ case 'session':
46
+ handleSessionCommand();
47
+ return { handled: true };
48
+ case 'rewind': {
49
+ const result = handleRewindCommand(slashCommand.threadId);
50
+ return { handled: true, session: result.session };
51
+ }
52
+ case 'compact':
53
+ await handleCompactCommand(threadId);
54
+ return { handled: true };
55
+ case 'invalid':
56
+ handleInvalidCommand(slashCommand.message);
57
+ return { handled: true };
58
+ case 'unknown':
59
+ handleUnknownCommand(slashCommand.name);
60
+ return { handled: true };
61
+ }
62
+ }
@@ -0,0 +1,4 @@
1
+ import chalk from 'chalk';
2
+ export function handleInvalidCommand(message) {
3
+ console.log(chalk.yellow(message));
4
+ }
@@ -0,0 +1,10 @@
1
+ import * as crypto from 'node:crypto';
2
+ const THREAD_ID_PREFIX = 'user-session';
3
+ export function createThreadId() {
4
+ return `${THREAD_ID_PREFIX}-${crypto.randomUUID()}`;
5
+ }
6
+ export function createNewSession() {
7
+ return {
8
+ threadId: createThreadId(),
9
+ };
10
+ }
@@ -0,0 +1,8 @@
1
+ import chalk from 'chalk';
2
+ import { createNewSession } from './chat-session.js';
3
+ export function handleNewCommand() {
4
+ console.log(chalk.dim('已开启新会话。'));
5
+ return {
6
+ session: createNewSession(),
7
+ };
8
+ }
@@ -0,0 +1,19 @@
1
+ import chalk from 'chalk';
2
+ import { threadExists } from '../../../db/index.js';
3
+ export function handleRewindCommand(threadId) {
4
+ let exists;
5
+ try {
6
+ exists = threadExists(threadId);
7
+ }
8
+ catch (err) {
9
+ console.log(chalk.red(`查询会话失败:${err.message}`));
10
+ return {};
11
+ }
12
+ if (!exists) {
13
+ console.log(chalk.red(`未找到 thread_id:${threadId}`));
14
+ console.log(chalk.dim('可输入 /session 查看最近会话。'));
15
+ return {};
16
+ }
17
+ console.log(chalk.dim(`已恢复至会话 ${threadId}。`));
18
+ return { session: { threadId } };
19
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseSlashCommand } from '../index.js';
3
+ describe('parseSlashCommand /rewind', () => {
4
+ it('识别 /rewind <thread_id>', () => {
5
+ expect(parseSlashCommand('/rewind user-session-1')).toEqual({
6
+ type: 'rewind',
7
+ threadId: 'user-session-1',
8
+ });
9
+ });
10
+ it('/rewind 缺少参数时返回 invalid', () => {
11
+ expect(parseSlashCommand('/rewind')).toEqual({
12
+ type: 'invalid',
13
+ message: '/rewind 需要 1 个参数:thread_id',
14
+ });
15
+ });
16
+ it('/rewind 多余参数时返回 invalid', () => {
17
+ expect(parseSlashCommand('/rewind a b')).toEqual({
18
+ type: 'invalid',
19
+ message: '/rewind 需要 1 个参数:thread_id',
20
+ });
21
+ });
22
+ });
@@ -0,0 +1,32 @@
1
+ // 把 ISO 时间戳格式化成人类友好的相对时间。
2
+ // 阈值:< 1 分钟 → 刚刚;< 60 分钟 → X 分钟前;< 24 小时 → X 小时前;
3
+ // < 7 天 → X 天前;其余 → YYYY-MM-DD。
4
+ export function formatRelativeTime(iso, now = new Date()) {
5
+ const t = new Date(iso).getTime();
6
+ if (Number.isNaN(t))
7
+ return '-';
8
+ const diffSec = Math.max(0, Math.floor((now.getTime() - t) / 1000));
9
+ if (diffSec < 60)
10
+ return '刚刚';
11
+ const diffMin = Math.floor(diffSec / 60);
12
+ if (diffMin < 60)
13
+ return `${diffMin} 分钟前`;
14
+ const diffHour = Math.floor(diffMin / 60);
15
+ if (diffHour < 24)
16
+ return `${diffHour} 小时前`;
17
+ const diffDay = Math.floor(diffHour / 24);
18
+ if (diffDay < 7)
19
+ return `${diffDay} 天前`;
20
+ const d = new Date(iso);
21
+ const yyyy = d.getFullYear();
22
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
23
+ const dd = String(d.getDate()).padStart(2, '0');
24
+ return `${yyyy}-${mm}-${dd}`;
25
+ }
26
+ // 截取前 N 个字符,超出加省略号;先把换行/制表符替成空格防止破坏表格。
27
+ export function truncate(text, max) {
28
+ const flat = text.replace(/\s+/g, ' ').trim();
29
+ if (flat.length <= max)
30
+ return flat;
31
+ return `${flat.slice(0, max)}…`;
32
+ }
@@ -0,0 +1,32 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { fetchRecentSessions } from '../../../db/index.js';
4
+ import { formatRelativeTime, truncate } from './format.js';
5
+ const CONTENT_PREVIEW_LIMIT = 50;
6
+ export function handleSessionCommand() {
7
+ let sessions;
8
+ try {
9
+ sessions = fetchRecentSessions();
10
+ }
11
+ catch (err) {
12
+ console.log(chalk.red(`读取会话记录失败:${err.message}`));
13
+ return;
14
+ }
15
+ if (sessions.length === 0) {
16
+ console.log(chalk.dim('暂无聊天记录。'));
17
+ return;
18
+ }
19
+ const now = new Date();
20
+ const table = new Table({
21
+ head: [chalk.bold('thread_id'), chalk.bold('最后用户输入的问题'), chalk.bold('时间')],
22
+ style: { head: [], border: [] },
23
+ });
24
+ for (const s of sessions) {
25
+ table.push([
26
+ s.threadId,
27
+ truncate(s.lastUserInput, CONTENT_PREVIEW_LIMIT),
28
+ formatRelativeTime(s.ts, now),
29
+ ]);
30
+ }
31
+ console.log(table.toString());
32
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseSlashCommand } from '../index.js';
3
+ import { formatRelativeTime, truncate } from './format.js';
4
+ describe('formatRelativeTime', () => {
5
+ const now = new Date('2026-06-16T10:00:00.000Z');
6
+ it('< 1 分钟显示为「刚刚」', () => {
7
+ expect(formatRelativeTime('2026-06-16T09:59:30.000Z', now)).toBe('刚刚');
8
+ });
9
+ it('1 分钟前', () => {
10
+ expect(formatRelativeTime('2026-06-16T09:59:00.000Z', now)).toBe('1 分钟前');
11
+ });
12
+ it('5 分钟前', () => {
13
+ expect(formatRelativeTime('2026-06-16T09:55:00.000Z', now)).toBe('5 分钟前');
14
+ });
15
+ it('1 小时前', () => {
16
+ expect(formatRelativeTime('2026-06-16T09:00:00.000Z', now)).toBe('1 小时前');
17
+ });
18
+ it('3 天前', () => {
19
+ expect(formatRelativeTime('2026-06-13T10:00:00.000Z', now)).toBe('3 天前');
20
+ });
21
+ it('一周以上显示为日期', () => {
22
+ expect(formatRelativeTime('2026-06-01T10:00:00.000Z', now)).toMatch(/\d{4}-\d{2}-\d{2}/);
23
+ });
24
+ it('无效时间字符串返回 -', () => {
25
+ expect(formatRelativeTime('not-a-date', now)).toBe('-');
26
+ });
27
+ });
28
+ describe('truncate', () => {
29
+ it('短于上限原样返回', () => {
30
+ expect(truncate('hello', 10)).toBe('hello');
31
+ });
32
+ it('长于上限截断并补省略号', () => {
33
+ expect(truncate('a'.repeat(60), 50)).toBe(`${'a'.repeat(50)}…`);
34
+ });
35
+ it('压扁换行/空白', () => {
36
+ expect(truncate('a\nb\t c', 10)).toBe('a b c');
37
+ });
38
+ });
39
+ describe('parseSlashCommand /session', () => {
40
+ it('识别 /session', () => {
41
+ expect(parseSlashCommand('/session')).toEqual({ type: 'session' });
42
+ });
43
+ it('/session 不接受参数', () => {
44
+ expect(parseSlashCommand('/session foo')).toEqual({
45
+ type: 'invalid',
46
+ message: '/session 不接受参数',
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,4 @@
1
+ import chalk from 'chalk';
2
+ export function handleUnknownCommand(name) {
3
+ console.log(chalk.yellow(`未知命令:/${name}`));
4
+ }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ import * as readline from 'node:readline';
3
+ import * as fs from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+ import { compressContext, runAgentStream } from '../agent/index.js';
8
+ import { getModelContextWindow } from '../agent/context/index.js';
9
+ import { getModelConfig } from '../agent/config/index.js';
10
+ import { shouldCompress } from '../agent/context/compress.js';
11
+ import { createNewSession } from '../cli/command/new/chat-session.js';
12
+ import { handleSlashCommand } from '../cli/command/index.js';
13
+ import { formatTokenFooter, formatTokenWarning } from '../agent/context/token.js';
14
+ import { runStopHook, runUserPromptSubmitHook } from '../agent/hooks/index.js';
15
+ const packageJsonPath = fileURLToPath(new URL('../../package.json', import.meta.url));
16
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
17
+ function createInterface() {
18
+ return readline.createInterface({
19
+ input: process.stdin,
20
+ output: process.stdout,
21
+ });
22
+ }
23
+ function prompt(rl, question) {
24
+ return new Promise((resolve) => rl.question(question, resolve));
25
+ }
26
+ async function chat(rl, userInput, threadId) {
27
+ rl.pause();
28
+ const abortController = new AbortController();
29
+ const wasRaw = process.stdin.isTTY ? process.stdin.isRaw : false;
30
+ process.stdout.write(`\n${chalk.cyan.bold('AI:')} `);
31
+ readline.emitKeypressEvents(process.stdin, rl);
32
+ if (process.stdin.isTTY)
33
+ process.stdin.setRawMode(true);
34
+ process.stdin.resume();
35
+ const onKeyPress = (_input, key) => {
36
+ if ((key.name !== 'escape' && key.name !== 'esc') || abortController.signal.aborted)
37
+ return;
38
+ abortController.abort();
39
+ process.stdout.write(`\n${chalk.yellow('[已取消]')}`);
40
+ };
41
+ process.stdin.on('keypress', onKeyPress);
42
+ let usage = null;
43
+ try {
44
+ const result = await runAgentStream(userInput, (content) => {
45
+ process.stdout.write(content);
46
+ }, threadId, {
47
+ signal: abortController.signal,
48
+ onConfirm: async (info) => {
49
+ // 切回 normal mode 以便用户输入
50
+ if (process.stdin.isTTY)
51
+ process.stdin.setRawMode(false);
52
+ process.stdout.write(`\n${chalk.yellow.bold('[确认]')} 模型请求执行工具: ${chalk.cyan(info.toolName)}\n`);
53
+ process.stdout.write(`${chalk.dim('参数:')} ${JSON.stringify(info.args)}\n`);
54
+ const answer = await prompt(rl, chalk.yellow('是否执行? (y/n): '));
55
+ // 恢复 raw mode
56
+ if (process.stdin.isTTY)
57
+ process.stdin.setRawMode(true);
58
+ return answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes';
59
+ },
60
+ });
61
+ usage = result.usage;
62
+ }
63
+ catch (err) {
64
+ if (!abortController.signal.aborted)
65
+ throw err;
66
+ }
67
+ finally {
68
+ process.stdin.off('keypress', onKeyPress);
69
+ if (process.stdin.isTTY)
70
+ process.stdin.setRawMode(wasRaw);
71
+ if (usage && !abortController.signal.aborted) {
72
+ const contextWindow = getModelContextWindow(getModelConfig().model);
73
+ process.stdout.write(`\n${formatTokenFooter(usage, contextWindow)}`);
74
+ const warning = formatTokenWarning(usage, contextWindow);
75
+ if (warning)
76
+ process.stdout.write(`\n${warning}`);
77
+ // token 占比 ≥ 80% 时自动触发 context 压缩。
78
+ if (shouldCompress(usage, contextWindow)) {
79
+ // 触发 context 压缩;具体逻辑封装在 agent 模块,避免 CLI 直接操作状态。
80
+ const result = await compressContext(threadId);
81
+ if (result?.compressed) {
82
+ process.stdout.write(`\n${chalk.yellow.bold('[提示] Context 已压缩,下一轮对话将使用摘要替代早期历史消息。')}`);
83
+ if (result.compressionCount >= 3) {
84
+ process.stdout.write(`\n${chalk.red.bold('[警告] 已多次压缩 Context,信息丢失风险很高,强烈建议输入 /new 命令开启新会话')}`);
85
+ }
86
+ }
87
+ }
88
+ }
89
+ process.stdout.write('\n\n');
90
+ rl.resume(); // 恢复 readline
91
+ }
92
+ }
93
+ export async function startChat() {
94
+ const rl = createInterface();
95
+ let currentSession = createNewSession();
96
+ while (true) {
97
+ const userInput = await prompt(rl, chalk.green.bold('You: '));
98
+ const trimmedInput = userInput.trim();
99
+ if (!trimmedInput)
100
+ continue;
101
+ // UserPromptSubmit hook:在用户消息进入 agent 之前最后一道关卡。
102
+ // 用未 trim 的原文(语义上"用户提交了什么"),block 时打印 stderr 并跳过本轮。
103
+ const userSubmit = await runUserPromptSubmitHook({
104
+ event: 'UserPromptSubmit',
105
+ threadId: currentSession.threadId,
106
+ prompt: userInput,
107
+ });
108
+ if (userSubmit.block) {
109
+ console.log(chalk.red(userSubmit.block.message));
110
+ continue;
111
+ }
112
+ if (trimmedInput.toLowerCase() === 'exit') {
113
+ console.log(chalk.dim('再见!'));
114
+ await runStopHook({ event: 'Stop', threadId: currentSession.threadId, reason: 'exit' });
115
+ rl.close();
116
+ break;
117
+ }
118
+ const slashCommandResult = await handleSlashCommand(trimmedInput, currentSession.threadId);
119
+ if (slashCommandResult.handled) {
120
+ if (slashCommandResult.session) {
121
+ // 旧 threadId 触发 Stop(thread 视角上结束了),新 session 那边由 SessionStart 接上。
122
+ await runStopHook({
123
+ event: 'Stop',
124
+ threadId: currentSession.threadId,
125
+ reason: 'new-session',
126
+ });
127
+ currentSession = slashCommandResult.session;
128
+ }
129
+ continue;
130
+ }
131
+ try {
132
+ await chat(rl, userInput, currentSession.threadId);
133
+ }
134
+ catch (err) {
135
+ console.error(chalk.red.bold('请求出错:'), chalk.red(err.message));
136
+ }
137
+ }
138
+ }
139
+ export function createCommand() {
140
+ return new Command()
141
+ .name(packageJson.name)
142
+ .description(packageJson.description ?? '')
143
+ .version(packageJson.version);
144
+ }
@@ -0,0 +1,15 @@
1
+ import { SqliteSaver } from '@langchain/langgraph-checkpoint-sqlite';
2
+ import { CHECKPOINT_DB_PATH, ensureDataDir } from './path.js';
3
+ import { MEMORY_TABLE_SQL } from './tables/memory.js';
4
+ import { MEMORY_FTS_REBUILD_SQL, MEMORY_FTS_TABLE_SQL, MEMORY_FTS_TRIGGERS_SQL, } from './tables/memory_fts.js';
5
+ export function initCheckpointer() {
6
+ ensureDataDir();
7
+ const checkpointer = SqliteSaver.fromConnString(CHECKPOINT_DB_PATH);
8
+ // 1. 创建主表;2. 创建 FTS5 虚拟表;3. 创建同步 trigger;4. 重建索引填充已有数据
9
+ checkpointer.db.exec(MEMORY_TABLE_SQL);
10
+ checkpointer.db.exec(MEMORY_FTS_TABLE_SQL);
11
+ checkpointer.db.exec(MEMORY_FTS_TRIGGERS_SQL);
12
+ checkpointer.db.exec(MEMORY_FTS_REBUILD_SQL);
13
+ return checkpointer;
14
+ }
15
+ export const checkpointer = initCheckpointer();
@@ -0,0 +1,2 @@
1
+ export { checkpointer } from './checkpointer.js';
2
+ export { fetchRecentSessions, threadExists } from './sessions.js';
@@ -0,0 +1,8 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ export const DATA_DIR = path.join(os.homedir(), '.900', '.data');
5
+ export const CHECKPOINT_DB_PATH = path.join(DATA_DIR, 'checkpointer.db');
6
+ export function ensureDataDir() {
7
+ fs.mkdirSync(DATA_DIR, { recursive: true });
8
+ }