skyloom 1.13.6 → 1.13.8
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/.github/workflows/ci.yml +36 -36
- package/README.md +220 -159
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/commands_md.d.ts +41 -0
- package/dist/cli/commands_md.d.ts.map +1 -0
- package/dist/cli/commands_md.js +140 -0
- package/dist/cli/commands_md.js.map +1 -0
- package/dist/cli/input_macros.d.ts +28 -0
- package/dist/cli/input_macros.d.ts.map +1 -0
- package/dist/cli/input_macros.js +120 -0
- package/dist/cli/input_macros.js.map +1 -0
- package/dist/cli/loom.d.ts +220 -0
- package/dist/cli/loom.d.ts.map +1 -0
- package/dist/cli/loom.js +1094 -0
- package/dist/cli/loom.js.map +1 -0
- package/dist/cli/loom_chat.d.ts +20 -0
- package/dist/cli/loom_chat.d.ts.map +1 -0
- package/dist/cli/loom_chat.js +685 -0
- package/dist/cli/loom_chat.js.map +1 -0
- package/dist/cli/main.js +310 -14
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +7 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent.d.ts +20 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +199 -16
- package/dist/core/agent.js.map +1 -1
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +34 -2
- package/dist/core/factory.js.map +1 -1
- package/dist/core/file_checkpoint.d.ts +57 -0
- package/dist/core/file_checkpoint.d.ts.map +1 -0
- package/dist/core/file_checkpoint.js +162 -0
- package/dist/core/file_checkpoint.js.map +1 -0
- package/dist/core/hooks.d.ts +43 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +110 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +15 -9
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/mcp.d.ts +16 -0
- package/dist/core/mcp.d.ts.map +1 -1
- package/dist/core/mcp.js +55 -0
- package/dist/core/mcp.js.map +1 -1
- package/dist/core/model_config.d.ts +40 -0
- package/dist/core/model_config.d.ts.map +1 -0
- package/dist/core/model_config.js +191 -0
- package/dist/core/model_config.js.map +1 -0
- package/dist/core/skill.d.ts +7 -0
- package/dist/core/skill.d.ts.map +1 -1
- package/dist/core/skill.js +47 -0
- package/dist/core/skill.js.map +1 -1
- package/dist/core/skymd.d.ts +39 -0
- package/dist/core/skymd.d.ts.map +1 -0
- package/dist/core/skymd.js +177 -0
- package/dist/core/skymd.js.map +1 -0
- package/dist/core/tool.d.ts +12 -0
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +30 -0
- package/dist/core/tool.js.map +1 -1
- package/dist/core/verify.d.ts +27 -0
- package/dist/core/verify.d.ts.map +1 -0
- package/dist/core/verify.js +62 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/skills/loader.d.ts +22 -2
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +45 -15
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +13 -3
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/model_tool.d.ts +11 -0
- package/dist/tools/model_tool.d.ts.map +1 -0
- package/dist/tools/model_tool.js +71 -0
- package/dist/tools/model_tool.js.map +1 -0
- package/dist/tools/todo.d.ts +30 -0
- package/dist/tools/todo.d.ts.map +1 -0
- package/dist/tools/todo.js +78 -0
- package/dist/tools/todo.js.map +1 -0
- package/docs/AESTHETIC_DESIGN.md +152 -144
- package/docs/OPTIMIZATION_PLAN.md +178 -178
- package/package.json +68 -68
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/commands_md.ts +112 -0
- package/src/cli/input_macros.ts +83 -0
- package/src/cli/loom.ts +982 -0
- package/src/cli/loom_chat.ts +598 -0
- package/src/cli/main.ts +255 -9
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +228 -222
- package/src/core/agent/guard.ts +134 -134
- package/src/core/agent/task.ts +100 -100
- package/src/core/agent.ts +195 -16
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -178
- package/src/core/checkpoint.ts +94 -94
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +31 -2
- package/src/core/file_checkpoint.ts +136 -0
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/hooks.ts +126 -0
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +15 -9
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp.ts +48 -0
- package/src/core/mcp_server.ts +176 -176
- package/src/core/model_config.ts +157 -0
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +42 -0
- package/src/core/skymd.ts +143 -0
- package/src/core/theme.ts +65 -65
- package/src/core/tool.ts +30 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/verify.ts +71 -0
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +45 -16
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +13 -3
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/tools/model_tool.ts +74 -0
- package/src/tools/todo.ts +76 -0
- package/src/web/tts.ts +93 -93
- package/tests/agent.test.ts +159 -159
- package/tests/agent_helpers.test.ts +48 -48
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -86
- package/tests/checkpoint_commands.test.ts +124 -0
- package/tests/claude_compat.test.ts +110 -0
- package/tests/config.test.ts +41 -41
- package/tests/guard.test.ts +75 -75
- package/tests/icons.test.ts +45 -45
- package/tests/loom.test.ts +248 -0
- package/tests/memory.test.ts +170 -170
- package/tests/model_config.test.ts +109 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/skymd.test.ts +146 -0
- package/tests/task.test.ts +60 -60
- package/tests/todo_toolstats.test.ts +94 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/tests/tui.test.ts +67 -67
- package/vitest.config.ts +17 -17
- package/=12 +0 -0
- package/=8 +0 -0
package/tests/bus.test.ts
CHANGED
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the event bus.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
-
import { MessageBus, Event, EventType } from '../src/core/bus';
|
|
6
|
-
|
|
7
|
-
describe('MessageBus', () => {
|
|
8
|
-
it('subscribe and publish broadcasts to subscribers', async () => {
|
|
9
|
-
const bus = new MessageBus();
|
|
10
|
-
const received: Event[] = [];
|
|
11
|
-
|
|
12
|
-
async function handler(event: Event) {
|
|
13
|
-
received.push(event);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
bus.subscribe('test_agent', handler);
|
|
17
|
-
const event = new Event(EventType.SYSTEM_EVENT, 'system', null, { msg: 'hello' });
|
|
18
|
-
await bus.publish(event);
|
|
19
|
-
|
|
20
|
-
expect(received).toHaveLength(1);
|
|
21
|
-
expect(received[0].type).toBe(EventType.SYSTEM_EVENT);
|
|
22
|
-
expect(received[0].data).toEqual({ msg: 'hello' });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('direct message goes to target agent only', async () => {
|
|
26
|
-
const bus = new MessageBus();
|
|
27
|
-
const received: Event[] = [];
|
|
28
|
-
|
|
29
|
-
async function handler(event: Event) {
|
|
30
|
-
received.push(event);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
bus.subscribe('target_agent', handler);
|
|
34
|
-
const event = new Event(EventType.TASK_ASSIGNED, 'snow', 'target_agent', { task: 'test' });
|
|
35
|
-
await bus.publish(event);
|
|
36
|
-
|
|
37
|
-
expect(received).toHaveLength(1);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('unsubscribe removes handler', async () => {
|
|
41
|
-
const bus = new MessageBus();
|
|
42
|
-
const received: Event[] = [];
|
|
43
|
-
|
|
44
|
-
async function handler(event: Event) {
|
|
45
|
-
received.push(event);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
bus.subscribe('agent', handler);
|
|
49
|
-
bus.unsubscribe('agent');
|
|
50
|
-
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'system'));
|
|
51
|
-
expect(received).toHaveLength(0);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('state listener receives state change events', async () => {
|
|
55
|
-
const bus = new MessageBus();
|
|
56
|
-
const received: Event[] = [];
|
|
57
|
-
|
|
58
|
-
async function listener(event: Event) {
|
|
59
|
-
received.push(event);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
bus.onStateChange(listener);
|
|
63
|
-
await bus.notifyStateChange(
|
|
64
|
-
new Event(EventType.STATE_CHANGE, 'fog', null, { old_state: 'idle', new_state: 'thinking' })
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
expect(received).toHaveLength(1);
|
|
68
|
-
expect(received[0].type).toBe(EventType.STATE_CHANGE);
|
|
69
|
-
expect(received[0].source).toBe('fog');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('state listener ignores non-state events', async () => {
|
|
73
|
-
const bus = new MessageBus();
|
|
74
|
-
const received: Event[] = [];
|
|
75
|
-
|
|
76
|
-
async function listener(event: Event) {
|
|
77
|
-
received.push(event);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
bus.onStateChange(listener);
|
|
81
|
-
await bus.notifyStateChange(new Event(EventType.TASK_ASSIGNED, 'snow'));
|
|
82
|
-
expect(received).toHaveLength(0);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('history tracks published events', async () => {
|
|
86
|
-
const bus = new MessageBus();
|
|
87
|
-
for (let i = 0; i < 5; i++) {
|
|
88
|
-
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'system'));
|
|
89
|
-
}
|
|
90
|
-
expect(bus.getHistory()).toHaveLength(5);
|
|
91
|
-
expect(bus.getHistory(undefined, undefined, 2)).toHaveLength(2);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('broadcast skips source agent', async () => {
|
|
95
|
-
const bus = new MessageBus();
|
|
96
|
-
const called: string[] = [];
|
|
97
|
-
|
|
98
|
-
async function fogHandler(_event: Event) { called.push('fog'); }
|
|
99
|
-
async function rainHandler(_event: Event) { called.push('rain'); }
|
|
100
|
-
|
|
101
|
-
bus.subscribe('fog', fogHandler);
|
|
102
|
-
bus.subscribe('rain', rainHandler);
|
|
103
|
-
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'fog'));
|
|
104
|
-
|
|
105
|
-
expect(called).not.toContain('fog');
|
|
106
|
-
expect(called).toContain('rain');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('handler exception does not block other handlers', async () => {
|
|
110
|
-
const bus = new MessageBus();
|
|
111
|
-
const good: Event[] = [];
|
|
112
|
-
|
|
113
|
-
async function failing(_event: Event) { throw new Error('failed'); }
|
|
114
|
-
async function ok(event: Event) { good.push(event); }
|
|
115
|
-
|
|
116
|
-
bus.subscribe('rain', failing);
|
|
117
|
-
bus.subscribe('dew', ok);
|
|
118
|
-
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'fog'));
|
|
119
|
-
expect(good).toHaveLength(1);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the event bus.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { MessageBus, Event, EventType } from '../src/core/bus';
|
|
6
|
+
|
|
7
|
+
describe('MessageBus', () => {
|
|
8
|
+
it('subscribe and publish broadcasts to subscribers', async () => {
|
|
9
|
+
const bus = new MessageBus();
|
|
10
|
+
const received: Event[] = [];
|
|
11
|
+
|
|
12
|
+
async function handler(event: Event) {
|
|
13
|
+
received.push(event);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
bus.subscribe('test_agent', handler);
|
|
17
|
+
const event = new Event(EventType.SYSTEM_EVENT, 'system', null, { msg: 'hello' });
|
|
18
|
+
await bus.publish(event);
|
|
19
|
+
|
|
20
|
+
expect(received).toHaveLength(1);
|
|
21
|
+
expect(received[0].type).toBe(EventType.SYSTEM_EVENT);
|
|
22
|
+
expect(received[0].data).toEqual({ msg: 'hello' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('direct message goes to target agent only', async () => {
|
|
26
|
+
const bus = new MessageBus();
|
|
27
|
+
const received: Event[] = [];
|
|
28
|
+
|
|
29
|
+
async function handler(event: Event) {
|
|
30
|
+
received.push(event);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
bus.subscribe('target_agent', handler);
|
|
34
|
+
const event = new Event(EventType.TASK_ASSIGNED, 'snow', 'target_agent', { task: 'test' });
|
|
35
|
+
await bus.publish(event);
|
|
36
|
+
|
|
37
|
+
expect(received).toHaveLength(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('unsubscribe removes handler', async () => {
|
|
41
|
+
const bus = new MessageBus();
|
|
42
|
+
const received: Event[] = [];
|
|
43
|
+
|
|
44
|
+
async function handler(event: Event) {
|
|
45
|
+
received.push(event);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
bus.subscribe('agent', handler);
|
|
49
|
+
bus.unsubscribe('agent');
|
|
50
|
+
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'system'));
|
|
51
|
+
expect(received).toHaveLength(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('state listener receives state change events', async () => {
|
|
55
|
+
const bus = new MessageBus();
|
|
56
|
+
const received: Event[] = [];
|
|
57
|
+
|
|
58
|
+
async function listener(event: Event) {
|
|
59
|
+
received.push(event);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
bus.onStateChange(listener);
|
|
63
|
+
await bus.notifyStateChange(
|
|
64
|
+
new Event(EventType.STATE_CHANGE, 'fog', null, { old_state: 'idle', new_state: 'thinking' })
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(received).toHaveLength(1);
|
|
68
|
+
expect(received[0].type).toBe(EventType.STATE_CHANGE);
|
|
69
|
+
expect(received[0].source).toBe('fog');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('state listener ignores non-state events', async () => {
|
|
73
|
+
const bus = new MessageBus();
|
|
74
|
+
const received: Event[] = [];
|
|
75
|
+
|
|
76
|
+
async function listener(event: Event) {
|
|
77
|
+
received.push(event);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
bus.onStateChange(listener);
|
|
81
|
+
await bus.notifyStateChange(new Event(EventType.TASK_ASSIGNED, 'snow'));
|
|
82
|
+
expect(received).toHaveLength(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('history tracks published events', async () => {
|
|
86
|
+
const bus = new MessageBus();
|
|
87
|
+
for (let i = 0; i < 5; i++) {
|
|
88
|
+
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'system'));
|
|
89
|
+
}
|
|
90
|
+
expect(bus.getHistory()).toHaveLength(5);
|
|
91
|
+
expect(bus.getHistory(undefined, undefined, 2)).toHaveLength(2);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('broadcast skips source agent', async () => {
|
|
95
|
+
const bus = new MessageBus();
|
|
96
|
+
const called: string[] = [];
|
|
97
|
+
|
|
98
|
+
async function fogHandler(_event: Event) { called.push('fog'); }
|
|
99
|
+
async function rainHandler(_event: Event) { called.push('rain'); }
|
|
100
|
+
|
|
101
|
+
bus.subscribe('fog', fogHandler);
|
|
102
|
+
bus.subscribe('rain', rainHandler);
|
|
103
|
+
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'fog'));
|
|
104
|
+
|
|
105
|
+
expect(called).not.toContain('fog');
|
|
106
|
+
expect(called).toContain('rain');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('handler exception does not block other handlers', async () => {
|
|
110
|
+
const bus = new MessageBus();
|
|
111
|
+
const good: Event[] = [];
|
|
112
|
+
|
|
113
|
+
async function failing(_event: Event) { throw new Error('failed'); }
|
|
114
|
+
async function ok(event: Event) { good.push(event); }
|
|
115
|
+
|
|
116
|
+
bus.subscribe('rain', failing);
|
|
117
|
+
bus.subscribe('dew', ok);
|
|
118
|
+
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'fog'));
|
|
119
|
+
expect(good).toHaveLength(1);
|
|
120
|
+
});
|
|
121
|
+
});
|
package/tests/catalog.test.ts
CHANGED
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
loadCatalog,
|
|
4
|
-
listProviders,
|
|
5
|
-
modelsFor,
|
|
6
|
-
allModels,
|
|
7
|
-
getModelInfo,
|
|
8
|
-
isKnownModel,
|
|
9
|
-
providerLabel,
|
|
10
|
-
validateModel,
|
|
11
|
-
resetCatalogCache,
|
|
12
|
-
PROVIDER_META,
|
|
13
|
-
} from "../src/core/catalog";
|
|
14
|
-
|
|
15
|
-
describe("catalog", () => {
|
|
16
|
-
beforeEach(() => resetCatalogCache());
|
|
17
|
-
|
|
18
|
-
it("loads providers from models.yaml", () => {
|
|
19
|
-
const providers = listProviders();
|
|
20
|
-
expect(providers.length).toBeGreaterThan(0);
|
|
21
|
-
expect(providers).toContain("openai");
|
|
22
|
-
expect(providers).toContain("deepseek");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("orders providers by wizard order", () => {
|
|
26
|
-
const providers = listProviders();
|
|
27
|
-
const orders = providers.map((p) => PROVIDER_META[p]?.order ?? 99);
|
|
28
|
-
const sorted = [...orders].sort((a, b) => a - b);
|
|
29
|
-
expect(orders).toEqual(sorted);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("parses real model fields (context + cost)", () => {
|
|
33
|
-
const gpt4o = getModelInfo("gpt-4o");
|
|
34
|
-
expect(gpt4o).not.toBeNull();
|
|
35
|
-
expect(gpt4o!.provider).toBe("openai");
|
|
36
|
-
expect(gpt4o!.context).toBeGreaterThan(0);
|
|
37
|
-
expect(gpt4o!.costIn).toBeGreaterThan(0);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("contains all real, API-verified deepseek models", () => {
|
|
41
|
-
// Verified live against https://api.deepseek.com/v1/models
|
|
42
|
-
expect(isKnownModel("deepseek-chat")).toBe(true);
|
|
43
|
-
expect(isKnownModel("deepseek-reasoner")).toBe(true);
|
|
44
|
-
expect(isKnownModel("deepseek-v4-flash")).toBe(true);
|
|
45
|
-
expect(isKnownModel("deepseek-v4-pro")).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("still rejects genuinely unknown model ids", () => {
|
|
49
|
-
expect(isKnownModel("deepseek-v9-ultra")).toBe(false);
|
|
50
|
-
expect(isKnownModel("gpt-5-imaginary")).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("marks ollama / zero-cost models as local", () => {
|
|
54
|
-
const local = allModels().filter((m) => m.local);
|
|
55
|
-
expect(local.every((m) => m.costIn === 0 && m.costOut === 0)).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("tolerates provider/ prefix when resolving", () => {
|
|
59
|
-
// openrouter lists "openai/gpt-4.1"; bare "gpt-4.1" should also resolve
|
|
60
|
-
const direct = getModelInfo("gpt-4.1");
|
|
61
|
-
expect(direct).not.toBeNull();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("validateModel passes for a real model and fails with suggestions otherwise", () => {
|
|
65
|
-
expect(validateModel("gpt-4o").ok).toBe(true);
|
|
66
|
-
const bad = validateModel("totally-made-up-model");
|
|
67
|
-
expect(bad.ok).toBe(false);
|
|
68
|
-
expect(bad.suggestions.length).toBeGreaterThan(0);
|
|
69
|
-
expect(bad.suggestions.every((s) => isKnownModel(s))).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("provides display labels", () => {
|
|
73
|
-
expect(providerLabel("openai")).toBe("OpenAI");
|
|
74
|
-
expect(providerLabel("unknown-xyz")).toBe("unknown-xyz");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("modelsFor returns empty for unknown provider", () => {
|
|
78
|
-
expect(modelsFor("nope")).toEqual([]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("caches the catalog across calls", () => {
|
|
82
|
-
const a = loadCatalog();
|
|
83
|
-
const b = loadCatalog();
|
|
84
|
-
expect(a).toBe(b);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
loadCatalog,
|
|
4
|
+
listProviders,
|
|
5
|
+
modelsFor,
|
|
6
|
+
allModels,
|
|
7
|
+
getModelInfo,
|
|
8
|
+
isKnownModel,
|
|
9
|
+
providerLabel,
|
|
10
|
+
validateModel,
|
|
11
|
+
resetCatalogCache,
|
|
12
|
+
PROVIDER_META,
|
|
13
|
+
} from "../src/core/catalog";
|
|
14
|
+
|
|
15
|
+
describe("catalog", () => {
|
|
16
|
+
beforeEach(() => resetCatalogCache());
|
|
17
|
+
|
|
18
|
+
it("loads providers from models.yaml", () => {
|
|
19
|
+
const providers = listProviders();
|
|
20
|
+
expect(providers.length).toBeGreaterThan(0);
|
|
21
|
+
expect(providers).toContain("openai");
|
|
22
|
+
expect(providers).toContain("deepseek");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("orders providers by wizard order", () => {
|
|
26
|
+
const providers = listProviders();
|
|
27
|
+
const orders = providers.map((p) => PROVIDER_META[p]?.order ?? 99);
|
|
28
|
+
const sorted = [...orders].sort((a, b) => a - b);
|
|
29
|
+
expect(orders).toEqual(sorted);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("parses real model fields (context + cost)", () => {
|
|
33
|
+
const gpt4o = getModelInfo("gpt-4o");
|
|
34
|
+
expect(gpt4o).not.toBeNull();
|
|
35
|
+
expect(gpt4o!.provider).toBe("openai");
|
|
36
|
+
expect(gpt4o!.context).toBeGreaterThan(0);
|
|
37
|
+
expect(gpt4o!.costIn).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("contains all real, API-verified deepseek models", () => {
|
|
41
|
+
// Verified live against https://api.deepseek.com/v1/models
|
|
42
|
+
expect(isKnownModel("deepseek-chat")).toBe(true);
|
|
43
|
+
expect(isKnownModel("deepseek-reasoner")).toBe(true);
|
|
44
|
+
expect(isKnownModel("deepseek-v4-flash")).toBe(true);
|
|
45
|
+
expect(isKnownModel("deepseek-v4-pro")).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("still rejects genuinely unknown model ids", () => {
|
|
49
|
+
expect(isKnownModel("deepseek-v9-ultra")).toBe(false);
|
|
50
|
+
expect(isKnownModel("gpt-5-imaginary")).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("marks ollama / zero-cost models as local", () => {
|
|
54
|
+
const local = allModels().filter((m) => m.local);
|
|
55
|
+
expect(local.every((m) => m.costIn === 0 && m.costOut === 0)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("tolerates provider/ prefix when resolving", () => {
|
|
59
|
+
// openrouter lists "openai/gpt-4.1"; bare "gpt-4.1" should also resolve
|
|
60
|
+
const direct = getModelInfo("gpt-4.1");
|
|
61
|
+
expect(direct).not.toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("validateModel passes for a real model and fails with suggestions otherwise", () => {
|
|
65
|
+
expect(validateModel("gpt-4o").ok).toBe(true);
|
|
66
|
+
const bad = validateModel("totally-made-up-model");
|
|
67
|
+
expect(bad.ok).toBe(false);
|
|
68
|
+
expect(bad.suggestions.length).toBeGreaterThan(0);
|
|
69
|
+
expect(bad.suggestions.every((s) => isKnownModel(s))).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("provides display labels", () => {
|
|
73
|
+
expect(providerLabel("openai")).toBe("OpenAI");
|
|
74
|
+
expect(providerLabel("unknown-xyz")).toBe("unknown-xyz");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("modelsFor returns empty for unknown provider", () => {
|
|
78
|
+
expect(modelsFor("nope")).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("caches the catalog across calls", () => {
|
|
82
|
+
const a = loadCatalog();
|
|
83
|
+
const b = loadCatalog();
|
|
84
|
+
expect(a).toBe(b);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { getFileCheckpoints } from "../src/core/file_checkpoint";
|
|
6
|
+
import { loadCustomCommands, substituteArgs, resolveCustomCommand } from "../src/cli/commands_md";
|
|
7
|
+
|
|
8
|
+
let tmp: string;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), "skycp-"));
|
|
11
|
+
getFileCheckpoints().clear();
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
|
|
14
|
+
|
|
15
|
+
describe("文件级检查点 /rewind", () => {
|
|
16
|
+
it("snapshots before mutation and restores on rewind", () => {
|
|
17
|
+
const cp = getFileCheckpoints();
|
|
18
|
+
const f = path.join(tmp, "a.txt");
|
|
19
|
+
fs.writeFileSync(f, "原始内容");
|
|
20
|
+
|
|
21
|
+
cp.beginTurn("改 a.txt");
|
|
22
|
+
cp.snapshot(f);
|
|
23
|
+
fs.writeFileSync(f, "agent 改坏了");
|
|
24
|
+
|
|
25
|
+
const r = cp.rewind(1);
|
|
26
|
+
expect(r.turns).toBe(1);
|
|
27
|
+
expect(r.restored).toEqual([f]);
|
|
28
|
+
expect(fs.readFileSync(f, "utf-8")).toBe("原始内容");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("deletes files that did not exist before the turn", () => {
|
|
32
|
+
const cp = getFileCheckpoints();
|
|
33
|
+
const f = path.join(tmp, "new.txt");
|
|
34
|
+
|
|
35
|
+
cp.beginTurn("新建文件");
|
|
36
|
+
cp.snapshot(f); // 不存在 → content null
|
|
37
|
+
fs.writeFileSync(f, "agent 新建的");
|
|
38
|
+
|
|
39
|
+
const r = cp.rewind(1);
|
|
40
|
+
expect(r.deleted).toEqual([f]);
|
|
41
|
+
expect(fs.existsSync(f)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("first touch per turn wins; multi-turn rewind restores the oldest state", () => {
|
|
45
|
+
const cp = getFileCheckpoints();
|
|
46
|
+
const f = path.join(tmp, "a.txt");
|
|
47
|
+
fs.writeFileSync(f, "v1");
|
|
48
|
+
|
|
49
|
+
cp.beginTurn("第一轮");
|
|
50
|
+
cp.snapshot(f);
|
|
51
|
+
fs.writeFileSync(f, "v2");
|
|
52
|
+
cp.snapshot(f); // 同轮第二次:忽略
|
|
53
|
+
fs.writeFileSync(f, "v2b");
|
|
54
|
+
|
|
55
|
+
cp.beginTurn("第二轮");
|
|
56
|
+
cp.snapshot(f);
|
|
57
|
+
fs.writeFileSync(f, "v3");
|
|
58
|
+
|
|
59
|
+
const r = cp.rewind(2);
|
|
60
|
+
expect(r.turns).toBe(2);
|
|
61
|
+
expect(fs.readFileSync(f, "utf-8")).toBe("v1");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("rewound turns are consumed; empty turns don't stack", () => {
|
|
65
|
+
const cp = getFileCheckpoints();
|
|
66
|
+
const f = path.join(tmp, "a.txt");
|
|
67
|
+
fs.writeFileSync(f, "v1");
|
|
68
|
+
cp.beginTurn("空轮"); // 无快照
|
|
69
|
+
cp.beginTurn("有改动");
|
|
70
|
+
cp.snapshot(f);
|
|
71
|
+
fs.writeFileSync(f, "v2");
|
|
72
|
+
expect(cp.list().length).toBe(1);
|
|
73
|
+
cp.rewind(1);
|
|
74
|
+
expect(cp.list().length).toBe(0);
|
|
75
|
+
expect(cp.rewind(1).turns).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("pathToSnapshot only matches mutating file tools", () => {
|
|
79
|
+
const cp = getFileCheckpoints();
|
|
80
|
+
expect(cp.pathToSnapshot("write_file", { path: "x.txt" })).toBe("x.txt");
|
|
81
|
+
expect(cp.pathToSnapshot("edit_file", { path: "x.txt" })).toBe("x.txt");
|
|
82
|
+
expect(cp.pathToSnapshot("delete_file", { path: "x.txt" })).toBe("x.txt");
|
|
83
|
+
expect(cp.pathToSnapshot("read_file", { path: "x.txt" })).toBeNull();
|
|
84
|
+
expect(cp.pathToSnapshot("run_bash", { command: "rm x" })).toBeNull();
|
|
85
|
+
expect(cp.pathToSnapshot("write_file", {})).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("自定义斜杠命令", () => {
|
|
90
|
+
function writeCmd(rel: string, content: string) {
|
|
91
|
+
const f = path.join(tmp, ".sky", "commands", rel);
|
|
92
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
93
|
+
fs.writeFileSync(f, content);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
it("loads commands with frontmatter; subdirectories namespace", () => {
|
|
97
|
+
writeCmd("fix-issue.md", "---\ndescription: 修复 issue\nagent: rain\n---\n修复 issue #$ARGUMENTS");
|
|
98
|
+
writeCmd("git/commit.md", "规范化提交");
|
|
99
|
+
const cmds = loadCustomCommands(tmp);
|
|
100
|
+
const names = cmds.map(c => c.name);
|
|
101
|
+
expect(names).toContain("fix-issue");
|
|
102
|
+
expect(names).toContain("git:commit");
|
|
103
|
+
const fix = cmds.find(c => c.name === "fix-issue")!;
|
|
104
|
+
expect(fix.description).toBe("修复 issue");
|
|
105
|
+
expect(fix.agent).toBe("rain");
|
|
106
|
+
// 无 frontmatter:首行作描述
|
|
107
|
+
expect(cmds.find(c => c.name === "git:commit")!.description).toBe("规范化提交");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("substitutes $ARGUMENTS and positional $1..$9", () => {
|
|
111
|
+
expect(substituteArgs("issue #$ARGUMENTS", "123 high")).toBe("issue #123 high");
|
|
112
|
+
expect(substituteArgs("from $1 to $2 ($3)", "a b")).toBe("from a to b ()");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("resolveCustomCommand matches name and expands args", () => {
|
|
116
|
+
writeCmd("review.md", "---\ndescription: 审查\n---\n审查 $1 的改动");
|
|
117
|
+
const cmds = loadCustomCommands(tmp);
|
|
118
|
+
const hit = resolveCustomCommand("/review src/core", cmds);
|
|
119
|
+
expect(hit).not.toBeNull();
|
|
120
|
+
expect(hit!.prompt).toBe("审查 src/core 的改动");
|
|
121
|
+
expect(resolveCustomCommand("/nonexistent", cmds)).toBeNull();
|
|
122
|
+
expect(resolveCustomCommand("not-slash", cmds)).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { SkillRegistry } from "../src/core/skill";
|
|
6
|
+
import { dynamicSkillDirs } from "../src/skills/loader";
|
|
7
|
+
import { loadProjectMcpJson, expandEnvRefs } from "../src/core/mcp";
|
|
8
|
+
|
|
9
|
+
let tmp: string;
|
|
10
|
+
beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), "skycompat-")); });
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
13
|
+
delete process.env.SKY_TEST_TOKEN;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/** Write a Claude Code-style skill folder. */
|
|
17
|
+
function writeSkill(root: string, name: string, frontmatter: string, body: string, extras: Record<string, string> = {}) {
|
|
18
|
+
const dir = path.join(root, name);
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
fs.writeFileSync(path.join(dir, "SKILL.md"), `---\n${frontmatter}\n---\n${body}`);
|
|
21
|
+
for (const [rel, content] of Object.entries(extras)) {
|
|
22
|
+
const f = path.join(dir, rel);
|
|
23
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
24
|
+
fs.writeFileSync(f, content);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("Claude Code skills 零迁移兼容", () => {
|
|
29
|
+
it("loads folder-style skills (<root>/<name>/SKILL.md), ignoring sibling reference files", () => {
|
|
30
|
+
writeSkill(tmp, "thesis-chart", "name: thesis-chart\ndescription: 学术图表绘制", "## 工作流程\n按学术配色出图", {
|
|
31
|
+
"reference.md": "# 详细参考(不是独立技能)",
|
|
32
|
+
"scripts/plot.py": "print('hi')",
|
|
33
|
+
});
|
|
34
|
+
const reg = new SkillRegistry();
|
|
35
|
+
const loaded = reg.loadSkillFolders(tmp);
|
|
36
|
+
expect(loaded.map(s => s.name)).toEqual(["thesis-chart"]); // reference.md 没被误注册
|
|
37
|
+
const skill = reg.get("thesis-chart")!;
|
|
38
|
+
expect(skill.description).toBe("学术图表绘制");
|
|
39
|
+
expect(skill.systemPrompt).toContain("学术配色");
|
|
40
|
+
expect(skill.resourceDir).toBe(path.join(tmp, "thesis-chart")); // 相对资源可解析
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("maps Claude Code allowed-tools names to sky tools", () => {
|
|
44
|
+
writeSkill(tmp, "deploy", "name: deploy\ndescription: 部署\nallowed-tools: Bash, Read, WebFetch", "做部署");
|
|
45
|
+
const reg = new SkillRegistry();
|
|
46
|
+
reg.loadSkillFolders(tmp);
|
|
47
|
+
const skill = reg.get("deploy")!;
|
|
48
|
+
expect(skill.allowedTools).toContain("run_bash");
|
|
49
|
+
expect(skill.allowedTools).toContain("read_file");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("re-scan picks up live edits (Claude Code live change detection)", () => {
|
|
53
|
+
writeSkill(tmp, "x", "name: x\ndescription: v1", "body1");
|
|
54
|
+
const reg = new SkillRegistry();
|
|
55
|
+
reg.loadSkillFolders(tmp);
|
|
56
|
+
expect(reg.get("x")!.description).toBe("v1");
|
|
57
|
+
writeSkill(tmp, "x", "name: x\ndescription: v2", "body2");
|
|
58
|
+
reg.loadSkillFolders(tmp); // same call list_skills makes
|
|
59
|
+
expect(reg.get("x")!.description).toBe("v2");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("dynamic skill dirs include both .claude (compat) and .sky (native) locations", () => {
|
|
63
|
+
const dirs = dynamicSkillDirs("/proj");
|
|
64
|
+
expect(dirs).toContain(path.join(os.homedir(), ".claude", "skills"));
|
|
65
|
+
expect(dirs).toContain(path.join("/proj", ".claude", "skills"));
|
|
66
|
+
expect(dirs).toContain(path.join("/proj", ".sky", "skills"));
|
|
67
|
+
// project dirs come after user dirs → later registration wins
|
|
68
|
+
expect(dirs.indexOf(path.join("/proj", ".sky", "skills"))).toBeGreaterThan(
|
|
69
|
+
dirs.indexOf(path.join(os.homedir(), ".claude", "skills")));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("Claude Code .mcp.json 兼容", () => {
|
|
74
|
+
it("parses the standard mcpServers schema (stdio + http)", () => {
|
|
75
|
+
fs.writeFileSync(path.join(tmp, ".mcp.json"), JSON.stringify({
|
|
76
|
+
mcpServers: {
|
|
77
|
+
github: { type: "http", url: "https://api.githubcopilot.com/mcp/" },
|
|
78
|
+
db: { command: "npx", args: ["-y", "@bytebase/dbhub"], env: { DB_URL: "postgres://x" } },
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
const servers = loadProjectMcpJson(tmp);
|
|
82
|
+
expect(servers).toHaveLength(2);
|
|
83
|
+
const gh = servers.find(s => s.name === "github")!;
|
|
84
|
+
expect(gh.url).toBe("https://api.githubcopilot.com/mcp/");
|
|
85
|
+
const db = servers.find(s => s.name === "db")!;
|
|
86
|
+
expect(db.command).toBe("npx");
|
|
87
|
+
expect(db.args).toEqual(["-y", "@bytebase/dbhub"]);
|
|
88
|
+
expect(db.env).toEqual({ DB_URL: "postgres://x" });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("expands ${ENV_VAR} references so secrets stay out of the file", () => {
|
|
92
|
+
process.env.SKY_TEST_TOKEN = "sk-secret";
|
|
93
|
+
expect(expandEnvRefs("Bearer ${SKY_TEST_TOKEN}")).toBe("Bearer sk-secret");
|
|
94
|
+
expect(expandEnvRefs("${MISSING_VAR_XYZ}")).toBe("");
|
|
95
|
+
fs.writeFileSync(path.join(tmp, ".mcp.json"), JSON.stringify({
|
|
96
|
+
mcpServers: { api: { command: "run", env: { TOKEN: "${SKY_TEST_TOKEN}" } } },
|
|
97
|
+
}));
|
|
98
|
+
expect(loadProjectMcpJson(tmp)[0].env).toEqual({ TOKEN: "sk-secret" });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("tolerates missing/invalid files and unsupported entries", () => {
|
|
102
|
+
expect(loadProjectMcpJson(tmp)).toEqual([]);
|
|
103
|
+
fs.writeFileSync(path.join(tmp, ".mcp.json"), "not json{");
|
|
104
|
+
expect(loadProjectMcpJson(tmp)).toEqual([]);
|
|
105
|
+
fs.writeFileSync(path.join(tmp, ".mcp.json"), JSON.stringify({
|
|
106
|
+
mcpServers: { weird: { type: "carrier-pigeon" } },
|
|
107
|
+
}));
|
|
108
|
+
expect(loadProjectMcpJson(tmp)).toEqual([]); // 无 command/url 的条目跳过
|
|
109
|
+
});
|
|
110
|
+
});
|