@lythos/agent-adapter 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/adapters/claude-cli.test.ts +124 -0
- package/src/adapters/kimi.test.ts +15 -3
- package/src/adapters/kimi.ts +4 -2
- package/src/index.test.ts +7 -7
package/package.json
CHANGED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { buildToolPrompt, extractJson, buildClaudeCommand, DEFAULT_ALLOWED_TOOLS, DEFAULT_DISALLOWED_TOOLS } from './claude-cli.js'
|
|
3
|
+
|
|
4
|
+
// ── buildToolPrompt ─────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
describe('buildToolPrompt', () => {
|
|
7
|
+
test('includes tool name and description', () => {
|
|
8
|
+
const result = buildToolPrompt(
|
|
9
|
+
{ name: 'search', description: 'Search the web', input_schema: { type: 'object', properties: {} } },
|
|
10
|
+
'Do the thing',
|
|
11
|
+
)
|
|
12
|
+
expect(result).toContain('Do the thing')
|
|
13
|
+
expect(result).toContain('## Tool: search')
|
|
14
|
+
expect(result).toContain('Search the web')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('includes JSON schema in code fence', () => {
|
|
18
|
+
const result = buildToolPrompt(
|
|
19
|
+
{ name: 'calc', description: 'Calculate', input_schema: { type: 'object', properties: { expr: { type: 'string' } } } },
|
|
20
|
+
'Compute',
|
|
21
|
+
)
|
|
22
|
+
expect(result).toContain('```json')
|
|
23
|
+
expect(result).toContain('"expr"')
|
|
24
|
+
expect(result).toContain('```')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('ends with return-only-JSON instruction', () => {
|
|
28
|
+
const result = buildToolPrompt(
|
|
29
|
+
{ name: 'x', description: 'x', input_schema: {} },
|
|
30
|
+
'prompt',
|
|
31
|
+
)
|
|
32
|
+
expect(result).toContain('Return ONLY the JSON object')
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// ── extractJson ─────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe('extractJson', () => {
|
|
39
|
+
test('extracts JSON from code fence', () => {
|
|
40
|
+
const result = extractJson('```json\n{"key":"value"}\n```')
|
|
41
|
+
expect(result).toEqual({ key: 'value' })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('extracts JSON without code fence', () => {
|
|
45
|
+
const result = extractJson('{"key":"value"}')
|
|
46
|
+
expect(result).toEqual({ key: 'value' })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('extracts nested JSON', () => {
|
|
50
|
+
const result = extractJson('```json\n{"nested":{"deep":true}}\n```')
|
|
51
|
+
expect(result).toEqual({ nested: { deep: true } })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('handles text before and after code fence', () => {
|
|
55
|
+
const result = extractJson('Here is the result:\n```json\n{"a":1}\n```\nHope that helps!')
|
|
56
|
+
expect(result).toEqual({ a: 1 })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('throws on invalid JSON', () => {
|
|
60
|
+
expect(() => extractJson('not json')).toThrow()
|
|
61
|
+
expect(() => extractJson('```json\nnot json\n```')).toThrow()
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// ── buildClaudeCommand ──────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
describe('buildClaudeCommand', () => {
|
|
68
|
+
test('uses sh -c with correct claude CLI flags', () => {
|
|
69
|
+
const cmd = buildClaudeCommand({ brief: 'test', cwd: '/tmp' })
|
|
70
|
+
expect(cmd.cmd).toBe('sh')
|
|
71
|
+
expect(cmd.args[0]).toBe('-c')
|
|
72
|
+
expect(cmd.args[1]).toContain('claude -p')
|
|
73
|
+
expect(cmd.args[1]).toContain('--output-format json')
|
|
74
|
+
expect(cmd.args[1]).toContain('--permission-mode bypassPermissions')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('includes allowed and disallowed tools', () => {
|
|
78
|
+
const cmd = buildClaudeCommand({ brief: 'test', cwd: '/tmp' })
|
|
79
|
+
expect(cmd.args[1]).toContain(DEFAULT_ALLOWED_TOOLS)
|
|
80
|
+
expect(cmd.args[1]).toContain(DEFAULT_DISALLOWED_TOOLS)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('custom tool lists override defaults', () => {
|
|
84
|
+
const cmd = buildClaudeCommand({
|
|
85
|
+
brief: 'test', cwd: '/tmp',
|
|
86
|
+
allowedTools: 'Read,Write',
|
|
87
|
+
disallowedTools: 'Bash(rm *)',
|
|
88
|
+
})
|
|
89
|
+
expect(cmd.args[1]).toContain('Read,Write')
|
|
90
|
+
expect(cmd.args[1]).not.toContain(DEFAULT_ALLOWED_TOOLS)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('modelTier: fast → --model haiku', () => {
|
|
94
|
+
const cmd = buildClaudeCommand({ brief: 'test', cwd: '/tmp', modelTier: 'fast' })
|
|
95
|
+
expect(cmd.args[1]).toContain('--model haiku')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('modelTier: deep → --model opus', () => {
|
|
99
|
+
const cmd = buildClaudeCommand({ brief: 'test', cwd: '/tmp', modelTier: 'deep' })
|
|
100
|
+
expect(cmd.args[1]).toContain('--model opus')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('modelTier: balanced (default) → no model flag', () => {
|
|
104
|
+
const cmd = buildClaudeCommand({ brief: 'test', cwd: '/tmp' })
|
|
105
|
+
expect(cmd.args[1]).not.toContain('--model')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('promptFile is a temp file path', () => {
|
|
109
|
+
const cmd = buildClaudeCommand({ brief: 'test', cwd: '/tmp' })
|
|
110
|
+
expect(cmd.promptFile).toContain('claude-prompt-')
|
|
111
|
+
expect(cmd.promptFile).toContain('.txt')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('stdin contains the brief prompt', () => {
|
|
115
|
+
const cmd = buildClaudeCommand({ brief: 'do the task', cwd: '/tmp' })
|
|
116
|
+
expect(cmd.stdin).toBe('do the task')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('shell command redirects from prompt file', () => {
|
|
120
|
+
const cmd = buildClaudeCommand({ brief: 'test', cwd: '/tmp' })
|
|
121
|
+
expect(cmd.args[1]).toContain('<')
|
|
122
|
+
expect(cmd.args[1]).toContain(cmd.promptFile)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -12,18 +12,30 @@ describe('detectKimiBinary', () => {
|
|
|
12
12
|
|
|
13
13
|
describe('buildKimiCommand', () => {
|
|
14
14
|
it('builds kimi CLI args without shell wrapper', () => {
|
|
15
|
-
const cmd = buildKimiCommand()
|
|
16
|
-
expect(cmd[0]).
|
|
15
|
+
const cmd = buildKimiCommand(undefined, 'kimi-cli')
|
|
16
|
+
expect(cmd[0]).toBe('kimi-cli')
|
|
17
|
+
expect(cmd).toContain('--print')
|
|
18
|
+
expect(cmd).toContain('--output-format')
|
|
19
|
+
expect(cmd).toContain('stream-json')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('builds kimi CLI args for kimi binary', () => {
|
|
23
|
+
const cmd = buildKimiCommand(undefined, 'kimi')
|
|
24
|
+
expect(cmd[0]).toBe('kimi')
|
|
17
25
|
expect(cmd).toContain('--print')
|
|
18
26
|
expect(cmd).toContain('--output-format')
|
|
19
27
|
expect(cmd).toContain('stream-json')
|
|
20
28
|
})
|
|
21
29
|
|
|
22
30
|
it('does not use shell or redirect (injection-safe)', () => {
|
|
23
|
-
const cmd = buildKimiCommand()
|
|
31
|
+
const cmd = buildKimiCommand(undefined, 'kimi-cli')
|
|
24
32
|
expect(cmd).not.toContain('sh')
|
|
25
33
|
expect(cmd).not.toContain('-c')
|
|
26
34
|
})
|
|
35
|
+
|
|
36
|
+
it('throws when no binary is available', () => {
|
|
37
|
+
expect(() => buildKimiCommand(undefined, '')).toThrow('No kimi binary found in PATH')
|
|
38
|
+
})
|
|
27
39
|
})
|
|
28
40
|
|
|
29
41
|
describe('parseKimiStreamJson', () => {
|
package/src/adapters/kimi.ts
CHANGED
|
@@ -21,8 +21,10 @@ export function detectKimiBinary(): string {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** Build kimi CLI command args (no shell wrapper — safe from injection). */
|
|
24
|
-
export function buildKimiCommand(
|
|
25
|
-
|
|
24
|
+
export function buildKimiCommand(
|
|
25
|
+
_modelTier?: 'fast' | 'balanced' | 'deep',
|
|
26
|
+
binary = detectKimiBinary(),
|
|
27
|
+
): string[] {
|
|
26
28
|
if (!binary) {
|
|
27
29
|
throw new Error('No kimi binary found in PATH. Install: https://github.com/MoonshotAI/kimi-cli')
|
|
28
30
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
2
|
import { useAgent, registerAgent, listAgents, type AgentAdapter } from './index'
|
|
3
3
|
|
|
4
4
|
describe('@lythos/agent-adapter', () => {
|
|
5
|
-
|
|
5
|
+
it('built-in adapters are auto-registered on import', () => {
|
|
6
6
|
const agents = listAgents()
|
|
7
7
|
expect(agents).toContain('kimi')
|
|
8
8
|
expect(agents).toContain('claude')
|
|
9
9
|
expect(agents).toContain('claude-cli')
|
|
10
10
|
})
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
it('useAgent returns kimi adapter', () => {
|
|
13
13
|
const adapter = useAgent('kimi')
|
|
14
14
|
expect(adapter.name).toBe('kimi')
|
|
15
15
|
expect(typeof adapter.spawn).toBe('function')
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
it('useAgent returns claude-cli adapter', () => {
|
|
19
19
|
const adapter = useAgent('claude-cli')
|
|
20
20
|
expect(adapter.name).toBe('claude')
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
it('useAgent throws for unknown agent', () => {
|
|
24
24
|
expect(() => useAgent('gpt-5')).toThrow('Unknown agent: "gpt-5"')
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
it('registerAgent adds custom adapter', () => {
|
|
28
28
|
const mockAdapter: AgentAdapter = {
|
|
29
29
|
name: 'mock',
|
|
30
30
|
async spawn(opts) {
|
|
@@ -36,7 +36,7 @@ describe('@lythos/agent-adapter', () => {
|
|
|
36
36
|
expect(useAgent('mock').name).toBe('mock')
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
it('spawn throws clear error when binary is missing', async () => {
|
|
40
40
|
const adapter = useAgent('kimi')
|
|
41
41
|
const hasKimi = !!Bun.which('kimi')
|
|
42
42
|
if (!hasKimi) {
|