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.
- package/README.md +43 -0
- package/dist/agent/config/index.js +85 -0
- package/dist/agent/context/compress.js +145 -0
- package/dist/agent/context/index.js +68 -0
- package/dist/agent/context/token.js +30 -0
- package/dist/agent/hooks/config.js +41 -0
- package/dist/agent/hooks/core.js +177 -0
- package/dist/agent/hooks/events/index.js +9 -0
- package/dist/agent/hooks/events/post-tool-use-hook.js +7 -0
- package/dist/agent/hooks/events/pre-tool-use-hook.js +8 -0
- package/dist/agent/hooks/events/session-start-hook.js +8 -0
- package/dist/agent/hooks/events/stop-hook.js +10 -0
- package/dist/agent/hooks/events/user-prompt-submit-hook.js +7 -0
- package/dist/agent/hooks/index.js +7 -0
- package/dist/agent/hooks/types.js +32 -0
- package/dist/agent/index.js +465 -0
- package/dist/agent/llm/index.js +35 -0
- package/dist/agent/mcp/client.js +87 -0
- package/dist/agent/mcp/config.js +51 -0
- package/dist/agent/mcp/index.js +90 -0
- package/dist/agent/mcp/schema.js +56 -0
- package/dist/agent/permission/exec.js +71 -0
- package/dist/agent/permission/network.js +37 -0
- package/dist/agent/permission/read.js +27 -0
- package/dist/agent/permission/util/command-changes-directory.js +29 -0
- package/dist/agent/permission/util/dangerous-path.json +116 -0
- package/dist/agent/permission/util/detect-dangerous-operation.js +230 -0
- package/dist/agent/permission/util/detect-language-interpreter.js +66 -0
- package/dist/agent/permission/util/detect-safe-command.js +61 -0
- package/dist/agent/permission/util/index.js +16 -0
- package/dist/agent/permission/util/is-dangerous-path.js +98 -0
- package/dist/agent/permission/util/is-inside-cwd.js +83 -0
- package/dist/agent/permission/util/is-safe-domains.js +47 -0
- package/dist/agent/permission/write.js +31 -0
- package/dist/agent/prompt/index.js +42 -0
- package/dist/agent/skills/planner/SKILL.md +90 -0
- package/dist/agent/skills/programmer-resume/SKILL.md +113 -0
- package/dist/agent/skills/skill-creator/LICENSE.txt +202 -0
- package/dist/agent/skills/skill-creator/SKILL.md +495 -0
- package/dist/agent/skills/skill-creator/agents/analyzer.md +274 -0
- package/dist/agent/skills/skill-creator/agents/comparator.md +202 -0
- package/dist/agent/skills/skill-creator/agents/grader.md +223 -0
- package/dist/agent/skills/skill-creator/assets/eval_review.html +146 -0
- package/dist/agent/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/dist/agent/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/dist/agent/skills/skill-creator/references/schemas.md +430 -0
- package/dist/agent/skills/skill-creator/scripts/__init__.py +0 -0
- package/dist/agent/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/dist/agent/skills/skill-creator/scripts/generate_report.py +326 -0
- package/dist/agent/skills/skill-creator/scripts/improve_description.py +247 -0
- package/dist/agent/skills/skill-creator/scripts/package_skill.py +136 -0
- package/dist/agent/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/dist/agent/skills/skill-creator/scripts/run_eval.py +310 -0
- package/dist/agent/skills/skill-creator/scripts/run_loop.py +328 -0
- package/dist/agent/skills/skill-creator/scripts/utils.py +47 -0
- package/dist/agent/skills.js +129 -0
- package/dist/agent/tools/agent_tool/agent_tool.test.js +64 -0
- package/dist/agent/tools/agent_tool/index.js +33 -0
- package/dist/agent/tools/exec_tool/exec_tool.test.js +48 -0
- package/dist/agent/tools/exec_tool/index.js +44 -0
- package/dist/agent/tools/load_skill_tool/index.js +5 -0
- package/dist/agent/tools/load_skill_tool/load_skill_tool.test.js +122 -0
- package/dist/agent/tools/memory_create_tool/index.js +8 -0
- package/dist/agent/tools/memory_create_tool/memory_create_tool.test.js +54 -0
- package/dist/agent/tools/memory_delete_tool/index.js +10 -0
- package/dist/agent/tools/memory_delete_tool/memory_delete_tool.test.js +39 -0
- package/dist/agent/tools/memory_retrieve_tool/index.js +61 -0
- package/dist/agent/tools/memory_retrieve_tool/memory_retrieve_tool.test.js +102 -0
- package/dist/agent/tools/profile_update_tool/index.js +30 -0
- package/dist/agent/tools/profile_update_tool/profile_update_tool.test.js +49 -0
- package/dist/agent/tools/read_file_tool/index.js +24 -0
- package/dist/agent/tools/read_file_tool/read_file_tool.test.js +43 -0
- package/dist/agent/tools/run_js_tool/index.js +48 -0
- package/dist/agent/tools/run_js_tool/run_js_tool.test.js +67 -0
- package/dist/agent/tools/run_py_tool/index.js +48 -0
- package/dist/agent/tools/run_py_tool/run_py_tool.test.js +67 -0
- package/dist/agent/tools/tool_logger.js +16 -0
- package/dist/agent/tools/tool_logger.test.js +22 -0
- package/dist/agent/tools/web_fetch_url/index.js +76 -0
- package/dist/agent/tools/web_fetch_url/web_fetch_url.test.js +102 -0
- package/dist/agent/tools/web_search_tool/index.js +26 -0
- package/dist/agent/tools/web_search_tool/web_search_tool.test.js +61 -0
- package/dist/agent/tools/write_file_tool/index.js +22 -0
- package/dist/agent/tools/write_file_tool/write_file_tool.test.js +46 -0
- package/dist/agent/tools.js +218 -0
- package/dist/cli/command/compact/index.js +14 -0
- package/dist/cli/command/index.js +62 -0
- package/dist/cli/command/invalid/index.js +4 -0
- package/dist/cli/command/new/chat-session.js +10 -0
- package/dist/cli/command/new/index.js +8 -0
- package/dist/cli/command/rewind/index.js +19 -0
- package/dist/cli/command/rewind/rewind-command.test.js +22 -0
- package/dist/cli/command/session/format.js +32 -0
- package/dist/cli/command/session/index.js +32 -0
- package/dist/cli/command/session/session-command.test.js +49 -0
- package/dist/cli/command/unknown/index.js +4 -0
- package/dist/cli/index.js +144 -0
- package/dist/db/checkpointer.js +15 -0
- package/dist/db/index.js +2 -0
- package/dist/db/path.js +8 -0
- package/dist/db/sessions.js +81 -0
- package/dist/db/tables/memory.js +12 -0
- package/dist/db/tables/memory_fts.js +29 -0
- package/dist/index.js +87 -0
- package/dist/install.js +154 -0
- package/package.json +51 -0
- package/pnpm-workspace.yaml +3 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const OUTSIDE_CWD_ERROR = 'Can only execute commands inside the current working directory.';
|
|
7
|
+
async function resolveWorkingDirectory(cwd) {
|
|
8
|
+
const root = await realpath(process.cwd());
|
|
9
|
+
const requestedCwd = path.resolve(root, cwd);
|
|
10
|
+
const targetCwd = await realpath(requestedCwd);
|
|
11
|
+
const relativePath = path.relative(root, targetCwd);
|
|
12
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
13
|
+
throw new Error(OUTSIDE_CWD_ERROR);
|
|
14
|
+
}
|
|
15
|
+
const stats = await stat(targetCwd);
|
|
16
|
+
if (!stats.isDirectory()) {
|
|
17
|
+
throw new Error('Working directory must be a directory.');
|
|
18
|
+
}
|
|
19
|
+
return targetCwd;
|
|
20
|
+
}
|
|
21
|
+
function formatOutput(stdout, stderr) {
|
|
22
|
+
return `Exit code: 0\n\nstdout:\n${stdout}\n\nstderr:\n${stderr}`;
|
|
23
|
+
}
|
|
24
|
+
export async function execTool(command, cwd = '.') {
|
|
25
|
+
if (!command.trim()) {
|
|
26
|
+
throw new Error('Command is required.');
|
|
27
|
+
}
|
|
28
|
+
const targetCwd = await resolveWorkingDirectory(cwd);
|
|
29
|
+
try {
|
|
30
|
+
const { stdout, stderr } = await execFileAsync('/bin/sh', ['-c', command], {
|
|
31
|
+
cwd: targetCwd,
|
|
32
|
+
timeout: 10_000,
|
|
33
|
+
maxBuffer: 1024 * 1024,
|
|
34
|
+
});
|
|
35
|
+
return formatOutput(stdout, stderr);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const execError = error;
|
|
39
|
+
const stdout = execError.stdout ?? '';
|
|
40
|
+
const stderr = execError.stderr ?? '';
|
|
41
|
+
const code = execError.code ?? 'unknown';
|
|
42
|
+
throw new Error(`Command failed with exit code ${code}.\n\nstdout:\n${stdout}\n\nstderr:\n${stderr}`, { cause: error });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { loadSkillsFromRoot, loadSkillByName } from '../../../agent/skills.js';
|
|
6
|
+
import { loadSkillTool } from './index.js';
|
|
7
|
+
let tempRoot;
|
|
8
|
+
let skillsRoot;
|
|
9
|
+
async function writeSkill(folderName, markdown) {
|
|
10
|
+
const skillDir = path.join(skillsRoot, folderName);
|
|
11
|
+
await mkdir(skillDir, { recursive: true });
|
|
12
|
+
await writeFile(path.join(skillDir, 'SKILL.md'), markdown);
|
|
13
|
+
}
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
tempRoot = await mkdtemp(path.join(tmpdir(), 'argos-load-skill-'));
|
|
16
|
+
skillsRoot = path.join(tempRoot, 'skills');
|
|
17
|
+
await mkdir(skillsRoot);
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
describe('loadSkillTool', () => {
|
|
24
|
+
it('loads skill summaries from SKILL.md frontmatter in name order', async () => {
|
|
25
|
+
await writeSkill('beta-folder', `---
|
|
26
|
+
name: beta
|
|
27
|
+
description: Beta skill.
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
# Beta
|
|
31
|
+
`);
|
|
32
|
+
await writeSkill('alpha-folder', `---
|
|
33
|
+
name: alpha
|
|
34
|
+
description: Alpha skill.
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
# Alpha
|
|
38
|
+
`);
|
|
39
|
+
const registry = loadSkillsFromRoot(skillsRoot);
|
|
40
|
+
expect(registry.skills.map(({ name, description }) => ({ name, description }))).toEqual([
|
|
41
|
+
{ name: 'alpha', description: 'Alpha skill.' },
|
|
42
|
+
{ name: 'beta', description: 'Beta skill.' },
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
it('loads the full SKILL.md for a named skill', async () => {
|
|
46
|
+
const markdown = `---
|
|
47
|
+
name: alpha
|
|
48
|
+
description: Alpha skill.
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
# Alpha
|
|
52
|
+
|
|
53
|
+
Full skill content.
|
|
54
|
+
`;
|
|
55
|
+
await writeSkill('alpha-folder', markdown);
|
|
56
|
+
const registry = loadSkillsFromRoot(skillsRoot);
|
|
57
|
+
expect(loadSkillByName('alpha', registry)).toBe(markdown);
|
|
58
|
+
});
|
|
59
|
+
it('loads skill content when called', async () => {
|
|
60
|
+
const markdown = `---
|
|
61
|
+
name: alpha
|
|
62
|
+
description: Alpha skill.
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
# Alpha
|
|
66
|
+
`;
|
|
67
|
+
await writeSkill('alpha-folder', markdown);
|
|
68
|
+
const registry = loadSkillsFromRoot(skillsRoot);
|
|
69
|
+
expect(loadSkillTool('alpha', registry)).toBe(markdown);
|
|
70
|
+
});
|
|
71
|
+
it('rejects empty skill names', async () => {
|
|
72
|
+
const registry = loadSkillsFromRoot(skillsRoot);
|
|
73
|
+
expect(() => loadSkillTool('', registry)).toThrow('Skill name is required.');
|
|
74
|
+
expect(() => loadSkillTool(' ', registry)).toThrow('Skill name is required.');
|
|
75
|
+
});
|
|
76
|
+
it('rejects unknown skills and includes available skill names', async () => {
|
|
77
|
+
await writeSkill('alpha-folder', `---
|
|
78
|
+
name: alpha
|
|
79
|
+
description: Alpha skill.
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
# Alpha
|
|
83
|
+
`);
|
|
84
|
+
const registry = loadSkillsFromRoot(skillsRoot);
|
|
85
|
+
expect(() => loadSkillTool('missing', registry)).toThrow('Unknown skill: missing. Available skills: alpha.');
|
|
86
|
+
});
|
|
87
|
+
it('rejects skill files missing name', async () => {
|
|
88
|
+
await writeSkill('alpha-folder', `---
|
|
89
|
+
description: Alpha skill.
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
# Alpha
|
|
93
|
+
`);
|
|
94
|
+
expect(() => loadSkillsFromRoot(skillsRoot)).toThrow('Skill file is missing name:');
|
|
95
|
+
});
|
|
96
|
+
it('rejects skill files missing description', async () => {
|
|
97
|
+
await writeSkill('alpha-folder', `---
|
|
98
|
+
name: alpha
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
# Alpha
|
|
102
|
+
`);
|
|
103
|
+
expect(() => loadSkillsFromRoot(skillsRoot)).toThrow('Skill file is missing description:');
|
|
104
|
+
});
|
|
105
|
+
it('rejects duplicate skill names', async () => {
|
|
106
|
+
await writeSkill('alpha-one', `---
|
|
107
|
+
name: alpha
|
|
108
|
+
description: Alpha one.
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
# Alpha One
|
|
112
|
+
`);
|
|
113
|
+
await writeSkill('alpha-two', `---
|
|
114
|
+
name: alpha
|
|
115
|
+
description: Alpha two.
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
# Alpha Two
|
|
119
|
+
`);
|
|
120
|
+
expect(() => loadSkillsFromRoot(skillsRoot)).toThrow('Duplicate skill name: alpha');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { checkpointer } from '../../../db/checkpointer.js';
|
|
2
|
+
export async function memoryCreateTool(type, content, keywords, importance, sessionId) {
|
|
3
|
+
const db = checkpointer.db;
|
|
4
|
+
const stmt = db.prepare(`INSERT INTO memory (type, content, keywords, importance, session_id)
|
|
5
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
6
|
+
stmt.run(type, content, keywords ? JSON.stringify(keywords) : null, importance ?? 3, sessionId ?? null);
|
|
7
|
+
return 'Memory saved successfully.';
|
|
8
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { memoryCreateTool } from './index.js';
|
|
3
|
+
const { prepareMock, runMock } = vi.hoisted(() => ({
|
|
4
|
+
prepareMock: vi.fn(),
|
|
5
|
+
runMock: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('@/db/checkpointer.js', () => ({
|
|
8
|
+
checkpointer: {
|
|
9
|
+
db: {
|
|
10
|
+
prepare: prepareMock,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
prepareMock.mockReset();
|
|
16
|
+
runMock.mockReset();
|
|
17
|
+
prepareMock.mockReturnValue({ run: runMock });
|
|
18
|
+
});
|
|
19
|
+
describe('memoryCreateTool', () => {
|
|
20
|
+
it('inserts a memory with all fields provided', async () => {
|
|
21
|
+
const result = await memoryCreateTool('fact', 'User likes hiking', ['hiking', 'outdoor'], 5, 'thread-123');
|
|
22
|
+
expect(result).toBe('Memory saved successfully.');
|
|
23
|
+
expect(prepareMock).toHaveBeenCalledWith(`INSERT INTO memory (type, content, keywords, importance, session_id)
|
|
24
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
25
|
+
expect(runMock).toHaveBeenCalledWith('fact', 'User likes hiking', JSON.stringify(['hiking', 'outdoor']), 5, 'thread-123');
|
|
26
|
+
});
|
|
27
|
+
it('uses default importance when not provided', async () => {
|
|
28
|
+
await memoryCreateTool('preference', 'User prefers dark mode');
|
|
29
|
+
expect(runMock).toHaveBeenCalledWith('preference', 'User prefers dark mode', null, 3, null);
|
|
30
|
+
});
|
|
31
|
+
it('uses default importance when importance is undefined', async () => {
|
|
32
|
+
await memoryCreateTool('skill', 'User knows Python', ['python'], undefined, undefined);
|
|
33
|
+
expect(runMock).toHaveBeenCalledWith('skill', 'User knows Python', JSON.stringify(['python']), 3, null);
|
|
34
|
+
});
|
|
35
|
+
it('handles null keywords correctly', async () => {
|
|
36
|
+
await memoryCreateTool('event', 'User joined the team');
|
|
37
|
+
expect(runMock).toHaveBeenCalledWith('event', 'User joined the team', null, 3, null);
|
|
38
|
+
});
|
|
39
|
+
it('handles empty keywords array', async () => {
|
|
40
|
+
await memoryCreateTool('fact', 'Empty keywords', []);
|
|
41
|
+
expect(runMock).toHaveBeenCalledWith('fact', 'Empty keywords', JSON.stringify([]), 3, null);
|
|
42
|
+
});
|
|
43
|
+
it('handles special characters in content', async () => {
|
|
44
|
+
const content = 'User said: "Hello, world!" \n New line \t tab';
|
|
45
|
+
await memoryCreateTool('fact', content, ['quote', 'newline']);
|
|
46
|
+
expect(runMock).toHaveBeenCalledWith('fact', content, JSON.stringify(['quote', 'newline']), 3, null);
|
|
47
|
+
});
|
|
48
|
+
it('wraps database errors', async () => {
|
|
49
|
+
runMock.mockImplementation(() => {
|
|
50
|
+
throw new Error('database is locked');
|
|
51
|
+
});
|
|
52
|
+
await expect(memoryCreateTool('fact', 'test')).rejects.toThrow('database is locked');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { checkpointer } from '../../../db/checkpointer.js';
|
|
2
|
+
export async function memoryDeleteTool(id) {
|
|
3
|
+
const db = checkpointer.db;
|
|
4
|
+
const stmt = db.prepare('DELETE FROM memory WHERE id = ?');
|
|
5
|
+
const result = stmt.run(id);
|
|
6
|
+
if (result.changes === 0) {
|
|
7
|
+
return `No memory found with id ${id}.`;
|
|
8
|
+
}
|
|
9
|
+
return `Memory ${id} deleted successfully.`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { memoryDeleteTool } from './index.js';
|
|
3
|
+
const { prepareMock, runMock } = vi.hoisted(() => ({
|
|
4
|
+
prepareMock: vi.fn(),
|
|
5
|
+
runMock: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('@/db/checkpointer.js', () => ({
|
|
8
|
+
checkpointer: {
|
|
9
|
+
db: {
|
|
10
|
+
prepare: prepareMock,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
prepareMock.mockReset();
|
|
16
|
+
runMock.mockReset();
|
|
17
|
+
prepareMock.mockReturnValue({ run: runMock });
|
|
18
|
+
});
|
|
19
|
+
describe('memoryDeleteTool', () => {
|
|
20
|
+
it('deletes an existing memory', async () => {
|
|
21
|
+
runMock.mockReturnValue({ changes: 1 });
|
|
22
|
+
const result = await memoryDeleteTool(42);
|
|
23
|
+
expect(result).toBe('Memory 42 deleted successfully.');
|
|
24
|
+
expect(prepareMock).toHaveBeenCalledWith('DELETE FROM memory WHERE id = ?');
|
|
25
|
+
expect(runMock).toHaveBeenCalledWith(42);
|
|
26
|
+
});
|
|
27
|
+
it('returns not-found message when id does not exist', async () => {
|
|
28
|
+
runMock.mockReturnValue({ changes: 0 });
|
|
29
|
+
const result = await memoryDeleteTool(999);
|
|
30
|
+
expect(result).toBe('No memory found with id 999.');
|
|
31
|
+
expect(runMock).toHaveBeenCalledWith(999);
|
|
32
|
+
});
|
|
33
|
+
it('wraps database errors', async () => {
|
|
34
|
+
runMock.mockImplementation(() => {
|
|
35
|
+
throw new Error('database is locked');
|
|
36
|
+
});
|
|
37
|
+
await expect(memoryDeleteTool(1)).rejects.toThrow('database is locked');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { checkpointer } from '../../../db/checkpointer.js';
|
|
2
|
+
export async function memoryRetrieveTool(query, limit = 10) {
|
|
3
|
+
const db = checkpointer.db;
|
|
4
|
+
const trimmedQuery = query.trim();
|
|
5
|
+
if (!trimmedQuery) {
|
|
6
|
+
throw new Error('Search query is required.');
|
|
7
|
+
}
|
|
8
|
+
// 拆分为独立关键词,空格/逗号/顿号分隔均可
|
|
9
|
+
const keywords = trimmedQuery
|
|
10
|
+
.split(/[\s,,、]+/)
|
|
11
|
+
.map((k) => k.trim())
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
if (keywords.length === 0) {
|
|
14
|
+
throw new Error('Search query is required.');
|
|
15
|
+
}
|
|
16
|
+
// 构建 OR 条件:每个关键词匹配 content 或 keywords 列
|
|
17
|
+
const conditions = keywords
|
|
18
|
+
.map(() => '(m.content LIKE ? OR m.keywords LIKE ?)')
|
|
19
|
+
.join(' OR ');
|
|
20
|
+
const params = keywords.flatMap((k) => [`%${k}%`, `%${k}%`]);
|
|
21
|
+
try {
|
|
22
|
+
const stmt = db.prepare(`
|
|
23
|
+
SELECT
|
|
24
|
+
m.id,
|
|
25
|
+
m.type,
|
|
26
|
+
m.content,
|
|
27
|
+
m.keywords,
|
|
28
|
+
m.importance,
|
|
29
|
+
m.session_id,
|
|
30
|
+
m.created_at
|
|
31
|
+
FROM memory m
|
|
32
|
+
WHERE ${conditions}
|
|
33
|
+
ORDER BY m.importance DESC, m.created_at DESC
|
|
34
|
+
LIMIT ?
|
|
35
|
+
`);
|
|
36
|
+
const rows = stmt.all(...params, limit);
|
|
37
|
+
if (rows.length === 0) {
|
|
38
|
+
return 'No matching memories found.';
|
|
39
|
+
}
|
|
40
|
+
const lines = [`Found ${rows.length} memory(s):`, ''];
|
|
41
|
+
for (const row of rows) {
|
|
42
|
+
lines.push(`[ID: ${row.id}] Type: ${row.type} | Importance: ${row.importance}`);
|
|
43
|
+
lines.push(`Content: ${row.content}`);
|
|
44
|
+
if (row.keywords) {
|
|
45
|
+
lines.push(`Keywords: ${row.keywords}`);
|
|
46
|
+
}
|
|
47
|
+
if (row.session_id) {
|
|
48
|
+
lines.push(`Session: ${row.session_id}`);
|
|
49
|
+
}
|
|
50
|
+
lines.push(`Created: ${row.created_at}`);
|
|
51
|
+
lines.push('');
|
|
52
|
+
}
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof Error) {
|
|
57
|
+
throw new Error(`Memory retrieval failed: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
throw new Error('Memory retrieval failed due to an unknown error.');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { memoryRetrieveTool } from './index.js';
|
|
3
|
+
const { prepareMock } = vi.hoisted(() => ({
|
|
4
|
+
prepareMock: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
vi.mock('@/db/checkpointer.js', () => ({
|
|
7
|
+
checkpointer: {
|
|
8
|
+
db: {
|
|
9
|
+
prepare: prepareMock,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
prepareMock.mockReset();
|
|
15
|
+
});
|
|
16
|
+
function createMockStmt(rows) {
|
|
17
|
+
return {
|
|
18
|
+
all: vi.fn().mockReturnValue(rows),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
describe('memoryRetrieveTool', () => {
|
|
22
|
+
it('returns formatted memories when matches found', async () => {
|
|
23
|
+
const rows = [
|
|
24
|
+
{
|
|
25
|
+
id: 1,
|
|
26
|
+
type: 'preference',
|
|
27
|
+
content: '喜欢健身',
|
|
28
|
+
keywords: '["健身","爱好"]',
|
|
29
|
+
importance: 4,
|
|
30
|
+
session_id: 's1',
|
|
31
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
prepareMock.mockReturnValue(createMockStmt(rows));
|
|
35
|
+
const result = await memoryRetrieveTool('健身');
|
|
36
|
+
expect(result).toContain('Found 1 memory(s):');
|
|
37
|
+
expect(result).toContain('[ID: 1] Type: preference | Importance: 4');
|
|
38
|
+
expect(result).toContain('Content: 喜欢健身');
|
|
39
|
+
expect(result).toContain('Keywords: ["健身","爱好"]');
|
|
40
|
+
expect(result).toContain('Session: s1');
|
|
41
|
+
expect(result).toContain('Created: 2025-01-01T00:00:00Z');
|
|
42
|
+
});
|
|
43
|
+
it('returns no-match message when result is empty', async () => {
|
|
44
|
+
prepareMock.mockReturnValue(createMockStmt([]));
|
|
45
|
+
await expect(memoryRetrieveTool('不存在的关键词')).resolves.toBe('No matching memories found.');
|
|
46
|
+
});
|
|
47
|
+
it('rejects empty query', async () => {
|
|
48
|
+
await expect(memoryRetrieveTool('')).rejects.toThrow('Search query is required.');
|
|
49
|
+
await expect(memoryRetrieveTool(' ')).rejects.toThrow('Search query is required.');
|
|
50
|
+
});
|
|
51
|
+
it('splits query into multiple keywords and uses OR conditions', async () => {
|
|
52
|
+
const rows = [
|
|
53
|
+
{ id: 1, type: 'fact', content: 'a', keywords: null, importance: 3, session_id: null, created_at: '2025-01-01T00:00:00Z' },
|
|
54
|
+
{ id: 2, type: 'fact', content: 'b', keywords: null, importance: 3, session_id: null, created_at: '2025-01-01T00:00:00Z' },
|
|
55
|
+
];
|
|
56
|
+
prepareMock.mockReturnValue(createMockStmt(rows));
|
|
57
|
+
await memoryRetrieveTool('关键词1 关键词2');
|
|
58
|
+
const sql = prepareMock.mock.calls[0][0];
|
|
59
|
+
expect(sql).toContain("(m.content LIKE ? OR m.keywords LIKE ?)");
|
|
60
|
+
expect(sql).toContain(" OR ");
|
|
61
|
+
expect(sql).toContain('ORDER BY m.importance DESC, m.created_at DESC');
|
|
62
|
+
expect(sql).toContain('LIMIT ?');
|
|
63
|
+
});
|
|
64
|
+
it('respects custom limit', async () => {
|
|
65
|
+
prepareMock.mockReturnValue(createMockStmt([]));
|
|
66
|
+
await memoryRetrieveTool('测试', 5);
|
|
67
|
+
const stmt = prepareMock.mock.results[0].value;
|
|
68
|
+
expect(stmt.all).toHaveBeenCalledWith('%测试%', '%测试%', 5);
|
|
69
|
+
});
|
|
70
|
+
it('wraps database errors', async () => {
|
|
71
|
+
prepareMock.mockImplementation(() => {
|
|
72
|
+
throw new Error('database is locked');
|
|
73
|
+
});
|
|
74
|
+
await expect(memoryRetrieveTool('测试')).rejects.toThrow('Memory retrieval failed: database is locked');
|
|
75
|
+
});
|
|
76
|
+
it('omits optional fields when null', async () => {
|
|
77
|
+
const rows = [
|
|
78
|
+
{
|
|
79
|
+
id: 1,
|
|
80
|
+
type: 'fact',
|
|
81
|
+
content: '简洁内容',
|
|
82
|
+
keywords: null,
|
|
83
|
+
importance: 3,
|
|
84
|
+
session_id: null,
|
|
85
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
prepareMock.mockReturnValue(createMockStmt(rows));
|
|
89
|
+
const result = await memoryRetrieveTool('测试');
|
|
90
|
+
expect(result).toContain('Content: 简洁内容');
|
|
91
|
+
expect(result).not.toContain('Keywords:');
|
|
92
|
+
expect(result).not.toContain('Session:');
|
|
93
|
+
});
|
|
94
|
+
it('handles comma and顿号 separated keywords', async () => {
|
|
95
|
+
prepareMock.mockReturnValue(createMockStmt([]));
|
|
96
|
+
await memoryRetrieveTool('关键词1,关键词2、关键词3');
|
|
97
|
+
const sql = prepareMock.mock.calls[0][0];
|
|
98
|
+
// 3 个关键词应生成 3 组 OR 条件
|
|
99
|
+
const orCount = (sql.match(/\(m\.content LIKE \? OR m\.keywords LIKE \?\)/g) || []).length;
|
|
100
|
+
expect(orCount).toBe(3);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
function getProfileDir() {
|
|
6
|
+
return path.join(os.homedir(), '.900', '.data');
|
|
7
|
+
}
|
|
8
|
+
function getProfilePath() {
|
|
9
|
+
return path.join(getProfileDir(), 'profile.md');
|
|
10
|
+
}
|
|
11
|
+
const MIN_CONTENT_RATIO = 0.5;
|
|
12
|
+
export async function profileUpdateTool(content) {
|
|
13
|
+
const trimmedContent = content.trim();
|
|
14
|
+
if (!trimmedContent) {
|
|
15
|
+
throw new Error('Profile content is required.');
|
|
16
|
+
}
|
|
17
|
+
const profileDir = getProfileDir();
|
|
18
|
+
const profilePath = getProfilePath();
|
|
19
|
+
await mkdir(profileDir, { recursive: true });
|
|
20
|
+
if (existsSync(profilePath)) {
|
|
21
|
+
const existingContent = await readFile(profilePath, 'utf8');
|
|
22
|
+
if (existingContent.length > 0 && trimmedContent.length < existingContent.length * MIN_CONTENT_RATIO) {
|
|
23
|
+
throw new Error(`Profile update rejected: new content (${trimmedContent.length} chars) is significantly shorter than existing content (${existingContent.length} chars). Please include the full profile content, not just the changes.`);
|
|
24
|
+
}
|
|
25
|
+
const backupPath = path.join(profileDir, `profile.${Date.now()}.md`);
|
|
26
|
+
await copyFile(profilePath, backupPath);
|
|
27
|
+
}
|
|
28
|
+
await writeFile(profilePath, trimmedContent, 'utf8');
|
|
29
|
+
return 'Profile updated successfully.';
|
|
30
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { profileUpdateTool } from './index.js';
|
|
6
|
+
let originalCwd;
|
|
7
|
+
let tempRoot;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
originalCwd = process.cwd();
|
|
10
|
+
tempRoot = await mkdtemp(path.join(tmpdir(), 'argos-profile-'));
|
|
11
|
+
process.chdir(tempRoot);
|
|
12
|
+
vi.stubEnv('HOME', tempRoot);
|
|
13
|
+
vi.stubEnv('USERPROFILE', tempRoot);
|
|
14
|
+
});
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
process.chdir(originalCwd);
|
|
17
|
+
vi.unstubAllEnvs();
|
|
18
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
describe('profileUpdateTool', () => {
|
|
21
|
+
it('creates profile file when it does not exist', async () => {
|
|
22
|
+
const content = 'Name: lyvvq\nHobby: hiking';
|
|
23
|
+
const result = await profileUpdateTool(content);
|
|
24
|
+
expect(result).toBe('Profile updated successfully.');
|
|
25
|
+
await expect(readFile(path.join(tempRoot, '.900', '.data', 'profile.md'), 'utf8')).resolves.toBe(content);
|
|
26
|
+
});
|
|
27
|
+
it('backs up existing file before updating', async () => {
|
|
28
|
+
const profileDir = path.join(tempRoot, '.900', '.data');
|
|
29
|
+
await mkdir(profileDir, { recursive: true });
|
|
30
|
+
await writeFile(path.join(profileDir, 'profile.md'), 'old content', 'utf8');
|
|
31
|
+
const result = await profileUpdateTool('new content');
|
|
32
|
+
expect(result).toBe('Profile updated successfully.');
|
|
33
|
+
await expect(readFile(path.join(profileDir, 'profile.md'), 'utf8')).resolves.toBe('new content');
|
|
34
|
+
const files = await readdir(profileDir);
|
|
35
|
+
const backups = files.filter((f) => f.startsWith('profile.') && f.endsWith('.md') && f !== 'profile.md');
|
|
36
|
+
expect(backups.length).toBe(1);
|
|
37
|
+
await expect(readFile(path.join(profileDir, backups[0]), 'utf8')).resolves.toBe('old content');
|
|
38
|
+
});
|
|
39
|
+
it('trims content before writing', async () => {
|
|
40
|
+
const result = await profileUpdateTool(' trimmed content \n\n');
|
|
41
|
+
expect(result).toBe('Profile updated successfully.');
|
|
42
|
+
await expect(readFile(path.join(tempRoot, '.900', '.data', 'profile.md'), 'utf8')).resolves.toBe('trimmed content');
|
|
43
|
+
});
|
|
44
|
+
it('rejects empty content', async () => {
|
|
45
|
+
await expect(profileUpdateTool('')).rejects.toThrow('Profile content is required.');
|
|
46
|
+
await expect(profileUpdateTool(' ')).rejects.toThrow('Profile content is required.');
|
|
47
|
+
await expect(profileUpdateTool('\n\n')).rejects.toThrow('Profile content is required.');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFile, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const TOOL_OUTPUT_DIR = path.join(os.homedir(), '.900', '.tool_output');
|
|
5
|
+
function resolveToolOutputPath(inputPath) {
|
|
6
|
+
const normalized = path.normalize(inputPath);
|
|
7
|
+
if (normalized === '.tool_output' || normalized.startsWith('.tool_output' + path.sep)) {
|
|
8
|
+
const rest = normalized.slice('.tool_output'.length + 1);
|
|
9
|
+
return path.join(TOOL_OUTPUT_DIR, rest);
|
|
10
|
+
}
|
|
11
|
+
return inputPath;
|
|
12
|
+
}
|
|
13
|
+
export async function readFileTool(filePath) {
|
|
14
|
+
if (!filePath.trim()) {
|
|
15
|
+
throw new Error('File path is required.');
|
|
16
|
+
}
|
|
17
|
+
const resolvedPath = resolveToolOutputPath(filePath);
|
|
18
|
+
const target = await realpath(path.resolve(resolvedPath));
|
|
19
|
+
const stats = await stat(target);
|
|
20
|
+
if (!stats.isFile()) {
|
|
21
|
+
throw new Error('Can only read regular files.');
|
|
22
|
+
}
|
|
23
|
+
return readFile(target, 'utf8');
|
|
24
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { readFileTool } from './index.js';
|
|
6
|
+
let originalCwd;
|
|
7
|
+
let tempRoot;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
originalCwd = process.cwd();
|
|
10
|
+
tempRoot = await mkdtemp(path.join(tmpdir(), 'argos-read-file-'));
|
|
11
|
+
process.chdir(tempRoot);
|
|
12
|
+
});
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
process.chdir(originalCwd);
|
|
15
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
describe('readFileTool', () => {
|
|
18
|
+
it('reads a file by absolute path', async () => {
|
|
19
|
+
const filePath = path.join(tempRoot, 'hello.txt');
|
|
20
|
+
await writeFile(filePath, 'hello argos');
|
|
21
|
+
await expect(readFileTool(filePath)).resolves.toBe('hello argos');
|
|
22
|
+
});
|
|
23
|
+
it('reads a nested file', async () => {
|
|
24
|
+
const nestedDir = path.join(tempRoot, 'nested');
|
|
25
|
+
const filePath = path.join(nestedDir, 'hello.txt');
|
|
26
|
+
await mkdir(nestedDir);
|
|
27
|
+
await writeFile(filePath, 'nested hello');
|
|
28
|
+
await expect(readFileTool(filePath)).resolves.toBe('nested hello');
|
|
29
|
+
});
|
|
30
|
+
it('reads a file by relative path', async () => {
|
|
31
|
+
await writeFile('hello-relative.txt', 'hello relative');
|
|
32
|
+
await expect(readFileTool('hello-relative.txt')).resolves.toBe('hello relative');
|
|
33
|
+
});
|
|
34
|
+
it('rejects directories', async () => {
|
|
35
|
+
const dirPath = path.join(tempRoot, 'nested');
|
|
36
|
+
await mkdir(dirPath);
|
|
37
|
+
await expect(readFileTool(dirPath)).rejects.toThrow('Can only read regular files.');
|
|
38
|
+
});
|
|
39
|
+
it('rejects empty paths', async () => {
|
|
40
|
+
await expect(readFileTool('')).rejects.toThrow('File path is required.');
|
|
41
|
+
await expect(readFileTool(' ')).rejects.toThrow('File path is required.');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const OUTSIDE_CWD_ERROR = 'Can only execute JavaScript inside the current working directory.';
|
|
7
|
+
const NODE_NOT_FOUND_MESSAGE = 'Node.js is required to run JavaScript code, but the "node" executable was not found. Please install Node.js and try again.';
|
|
8
|
+
async function resolveWorkingDirectory(cwd) {
|
|
9
|
+
const root = await realpath(process.cwd());
|
|
10
|
+
const requestedCwd = path.resolve(root, cwd);
|
|
11
|
+
const targetCwd = await realpath(requestedCwd);
|
|
12
|
+
const relativePath = path.relative(root, targetCwd);
|
|
13
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
14
|
+
throw new Error(OUTSIDE_CWD_ERROR);
|
|
15
|
+
}
|
|
16
|
+
const stats = await stat(targetCwd);
|
|
17
|
+
if (!stats.isDirectory()) {
|
|
18
|
+
throw new Error('Working directory must be a directory.');
|
|
19
|
+
}
|
|
20
|
+
return targetCwd;
|
|
21
|
+
}
|
|
22
|
+
function formatSuccess(stdout, stderr) {
|
|
23
|
+
return `Exit code: 0\n\nstdout:\n${stdout}\n\nstderr:\n${stderr}`;
|
|
24
|
+
}
|
|
25
|
+
function formatFailure(code, stdout, stderr) {
|
|
26
|
+
return `JavaScript execution failed with exit code ${code}.\n\nstdout:\n${stdout}\n\nstderr:\n${stderr}`;
|
|
27
|
+
}
|
|
28
|
+
export async function runJsTool(code, cwd = '.') {
|
|
29
|
+
if (!code.trim()) {
|
|
30
|
+
throw new Error('JavaScript code is required.');
|
|
31
|
+
}
|
|
32
|
+
const targetCwd = await resolveWorkingDirectory(cwd);
|
|
33
|
+
try {
|
|
34
|
+
const { stdout, stderr } = await execFileAsync('node', ['-e', code], {
|
|
35
|
+
cwd: targetCwd,
|
|
36
|
+
timeout: 10_000,
|
|
37
|
+
maxBuffer: 1024 * 1024,
|
|
38
|
+
});
|
|
39
|
+
return formatSuccess(stdout, stderr);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const execError = error;
|
|
43
|
+
if (execError.code === 'ENOENT') {
|
|
44
|
+
return NODE_NOT_FOUND_MESSAGE;
|
|
45
|
+
}
|
|
46
|
+
return formatFailure(execError.code ?? 'unknown', execError.stdout ?? '', execError.stderr ?? '');
|
|
47
|
+
}
|
|
48
|
+
}
|