talon-agent 1.6.1 → 1.7.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/README.md +1 -1
- package/package.json +2 -2
- package/src/__tests__/chat-settings.test.ts +47 -36
- package/src/__tests__/claude-sdk-models.test.ts +157 -0
- package/src/__tests__/claude-sdk-options.test.ts +118 -0
- package/src/__tests__/config.test.ts +112 -8
- package/src/__tests__/dream.test.ts +3 -3
- package/src/__tests__/fuzz.test.ts +15 -15
- package/src/__tests__/plugin.test.ts +155 -2
- package/src/__tests__/telegram-helpers.test.ts +113 -0
- package/src/backend/claude-sdk/models.ts +385 -68
- package/src/backend/claude-sdk/options.ts +6 -4
- package/src/backend/claude-sdk/stream.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/core/models.ts +49 -5
- package/src/core/plugin.ts +207 -118
- package/src/frontend/telegram/callbacks.ts +16 -10
- package/src/frontend/telegram/commands.ts +19 -10
- package/src/frontend/telegram/helpers.ts +78 -7
- package/src/plugins/playwright/index.ts +54 -20
- package/src/util/config.ts +98 -15
package/README.md
CHANGED
|
@@ -209,7 +209,7 @@ Config file: `~/.talon/config.json`
|
|
|
209
209
|
| `frontend` | `"telegram"` | `"telegram"`, `"terminal"`, `"teams"`, or an array |
|
|
210
210
|
| `backend` | `"claude"` | `"claude"` or `"opencode"` |
|
|
211
211
|
| `botToken` | --- | Telegram bot token |
|
|
212
|
-
| `model` | `"
|
|
212
|
+
| `model` | `"default"` | Default Claude model. Legacy `claude-*` aliases are still accepted. |
|
|
213
213
|
| `concurrency` | `1` | Max concurrent AI queries (1--20) |
|
|
214
214
|
| `pulse` | `true` | Periodic group engagement |
|
|
215
215
|
| `heartbeat` | `false` | Background maintenance agent |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
|
|
5
5
|
"author": "Dylan Neve",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"format:check": "prettier --check src/ prompts/"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
54
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.108",
|
|
55
55
|
"@brave/brave-search-mcp-server": "^2.0.75",
|
|
56
56
|
"@clack/prompts": "^1.2.0",
|
|
57
57
|
"@grammyjs/auto-retry": "^2.0.2",
|
|
@@ -38,6 +38,12 @@ const { registerClaudeModelsStatic, CLAUDE_MODELS_STATIC } =
|
|
|
38
38
|
await import("../backend/claude-sdk/models.js");
|
|
39
39
|
registerClaudeModelsStatic(CLAUDE_MODELS_STATIC);
|
|
40
40
|
|
|
41
|
+
const SDK_MODEL_IDS = {
|
|
42
|
+
sonnet: "default",
|
|
43
|
+
opus: "opus",
|
|
44
|
+
haiku: "haiku",
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
41
47
|
describe("chat-settings", () => {
|
|
42
48
|
describe("getChatSettings", () => {
|
|
43
49
|
it("returns empty object for unknown chat", () => {
|
|
@@ -85,62 +91,67 @@ describe("chat-settings", () => {
|
|
|
85
91
|
});
|
|
86
92
|
|
|
87
93
|
describe("resolveModelName", () => {
|
|
88
|
-
it("resolves 'sonnet' to
|
|
89
|
-
expect(resolveModelName("sonnet")).toBe(
|
|
94
|
+
it("resolves 'sonnet' to the SDK default model ID", () => {
|
|
95
|
+
expect(resolveModelName("sonnet")).toBe(SDK_MODEL_IDS.sonnet);
|
|
90
96
|
});
|
|
91
97
|
|
|
92
|
-
it("resolves 'opus' to
|
|
93
|
-
expect(resolveModelName("opus")).toBe(
|
|
98
|
+
it("resolves 'opus' to the SDK Opus model ID", () => {
|
|
99
|
+
expect(resolveModelName("opus")).toBe(SDK_MODEL_IDS.opus);
|
|
94
100
|
});
|
|
95
101
|
|
|
96
|
-
it("resolves 'haiku' to
|
|
97
|
-
expect(resolveModelName("haiku")).toBe(
|
|
102
|
+
it("resolves 'haiku' to the SDK Haiku model ID", () => {
|
|
103
|
+
expect(resolveModelName("haiku")).toBe(SDK_MODEL_IDS.haiku);
|
|
98
104
|
});
|
|
99
105
|
|
|
100
106
|
it("resolves versioned aliases", () => {
|
|
101
|
-
expect(resolveModelName("sonnet-4.6")).toBe(
|
|
102
|
-
expect(resolveModelName("opus-4.6")).toBe(
|
|
103
|
-
expect(resolveModelName("haiku-4.5")).toBe(
|
|
107
|
+
expect(resolveModelName("sonnet-4.6")).toBe(SDK_MODEL_IDS.sonnet);
|
|
108
|
+
expect(resolveModelName("opus-4.6")).toBe(SDK_MODEL_IDS.opus);
|
|
109
|
+
expect(resolveModelName("haiku-4.5")).toBe(SDK_MODEL_IDS.haiku);
|
|
104
110
|
});
|
|
105
111
|
|
|
106
112
|
it("resolves dash-separated aliases", () => {
|
|
107
|
-
expect(resolveModelName("sonnet-4-6")).toBe(
|
|
108
|
-
expect(resolveModelName("opus-4-6")).toBe(
|
|
109
|
-
expect(resolveModelName("haiku-4-5")).toBe(
|
|
113
|
+
expect(resolveModelName("sonnet-4-6")).toBe(SDK_MODEL_IDS.sonnet);
|
|
114
|
+
expect(resolveModelName("opus-4-6")).toBe(SDK_MODEL_IDS.opus);
|
|
115
|
+
expect(resolveModelName("haiku-4-5")).toBe(SDK_MODEL_IDS.haiku);
|
|
110
116
|
});
|
|
111
117
|
|
|
112
118
|
it("is case-insensitive", () => {
|
|
113
|
-
expect(resolveModelName("Sonnet")).toBe(
|
|
114
|
-
expect(resolveModelName("OPUS")).toBe(
|
|
119
|
+
expect(resolveModelName("Sonnet")).toBe(SDK_MODEL_IDS.sonnet);
|
|
120
|
+
expect(resolveModelName("OPUS")).toBe(SDK_MODEL_IDS.opus);
|
|
115
121
|
});
|
|
116
122
|
|
|
117
123
|
it("trims whitespace", () => {
|
|
118
|
-
expect(resolveModelName(" sonnet ")).toBe(
|
|
124
|
+
expect(resolveModelName(" sonnet ")).toBe(SDK_MODEL_IDS.sonnet);
|
|
119
125
|
});
|
|
120
126
|
|
|
121
127
|
it("passes through unknown model names unchanged", () => {
|
|
122
128
|
expect(resolveModelName("gpt-4")).toBe("gpt-4");
|
|
123
|
-
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("resolves legacy claude-* aliases to the current SDK IDs", () => {
|
|
132
|
+
expect(resolveModelName("claude-sonnet-4-6")).toBe(SDK_MODEL_IDS.sonnet);
|
|
133
|
+
expect(resolveModelName("claude-opus-4-6")).toBe(SDK_MODEL_IDS.opus);
|
|
134
|
+
expect(resolveModelName("claude-haiku-4-5")).toBe(SDK_MODEL_IDS.haiku);
|
|
124
135
|
});
|
|
125
136
|
});
|
|
126
137
|
|
|
127
138
|
describe("resolveModelName — exhaustive alias coverage", () => {
|
|
128
139
|
it("resolves all base aliases correctly", () => {
|
|
129
|
-
expect(resolveModelName("sonnet")).toBe(
|
|
130
|
-
expect(resolveModelName("opus")).toBe(
|
|
131
|
-
expect(resolveModelName("haiku")).toBe(
|
|
140
|
+
expect(resolveModelName("sonnet")).toBe(SDK_MODEL_IDS.sonnet);
|
|
141
|
+
expect(resolveModelName("opus")).toBe(SDK_MODEL_IDS.opus);
|
|
142
|
+
expect(resolveModelName("haiku")).toBe(SDK_MODEL_IDS.haiku);
|
|
132
143
|
});
|
|
133
144
|
|
|
134
145
|
it("resolves all dot-separated version aliases", () => {
|
|
135
|
-
expect(resolveModelName("sonnet-4.6")).toBe(
|
|
136
|
-
expect(resolveModelName("opus-4.6")).toBe(
|
|
137
|
-
expect(resolveModelName("haiku-4.5")).toBe(
|
|
146
|
+
expect(resolveModelName("sonnet-4.6")).toBe(SDK_MODEL_IDS.sonnet);
|
|
147
|
+
expect(resolveModelName("opus-4.6")).toBe(SDK_MODEL_IDS.opus);
|
|
148
|
+
expect(resolveModelName("haiku-4.5")).toBe(SDK_MODEL_IDS.haiku);
|
|
138
149
|
});
|
|
139
150
|
|
|
140
151
|
it("resolves all dash-separated version aliases", () => {
|
|
141
|
-
expect(resolveModelName("sonnet-4-6")).toBe(
|
|
142
|
-
expect(resolveModelName("opus-4-6")).toBe(
|
|
143
|
-
expect(resolveModelName("haiku-4-5")).toBe(
|
|
152
|
+
expect(resolveModelName("sonnet-4-6")).toBe(SDK_MODEL_IDS.sonnet);
|
|
153
|
+
expect(resolveModelName("opus-4-6")).toBe(SDK_MODEL_IDS.opus);
|
|
154
|
+
expect(resolveModelName("haiku-4-5")).toBe(SDK_MODEL_IDS.haiku);
|
|
144
155
|
});
|
|
145
156
|
|
|
146
157
|
it("passes through completely unknown model names unchanged", () => {
|
|
@@ -149,10 +160,10 @@ describe("chat-settings", () => {
|
|
|
149
160
|
expect(resolveModelName("mistral-large")).toBe("mistral-large");
|
|
150
161
|
});
|
|
151
162
|
|
|
152
|
-
it("
|
|
153
|
-
expect(resolveModelName("claude-sonnet-4-6")).toBe(
|
|
154
|
-
expect(resolveModelName("claude-opus-4-6")).toBe(
|
|
155
|
-
expect(resolveModelName("claude-haiku-4-5")).toBe(
|
|
163
|
+
it("maps full claude compatibility aliases to the current SDK IDs", () => {
|
|
164
|
+
expect(resolveModelName("claude-sonnet-4-6")).toBe(SDK_MODEL_IDS.sonnet);
|
|
165
|
+
expect(resolveModelName("claude-opus-4-6")).toBe(SDK_MODEL_IDS.opus);
|
|
166
|
+
expect(resolveModelName("claude-haiku-4-5")).toBe(SDK_MODEL_IDS.haiku);
|
|
156
167
|
});
|
|
157
168
|
|
|
158
169
|
it("preserves original casing for unknown models", () => {
|
|
@@ -171,16 +182,16 @@ describe("chat-settings", () => {
|
|
|
171
182
|
});
|
|
172
183
|
|
|
173
184
|
describe("model alias resolution (via registry)", () => {
|
|
174
|
-
it("resolves short aliases to
|
|
175
|
-
expect(resolveModelName("sonnet")).toBe(
|
|
176
|
-
expect(resolveModelName("opus")).toBe(
|
|
177
|
-
expect(resolveModelName("haiku")).toBe(
|
|
185
|
+
it("resolves short aliases to SDK model IDs", () => {
|
|
186
|
+
expect(resolveModelName("sonnet")).toBe(SDK_MODEL_IDS.sonnet);
|
|
187
|
+
expect(resolveModelName("opus")).toBe(SDK_MODEL_IDS.opus);
|
|
188
|
+
expect(resolveModelName("haiku")).toBe(SDK_MODEL_IDS.haiku);
|
|
178
189
|
});
|
|
179
190
|
|
|
180
191
|
it("resolves versioned aliases", () => {
|
|
181
|
-
expect(resolveModelName("sonnet-4-6")).toBe(
|
|
182
|
-
expect(resolveModelName("opus-4.6")).toBe(
|
|
183
|
-
expect(resolveModelName("haiku-4.5")).toBe(
|
|
192
|
+
expect(resolveModelName("sonnet-4-6")).toBe(SDK_MODEL_IDS.sonnet);
|
|
193
|
+
expect(resolveModelName("opus-4.6")).toBe(SDK_MODEL_IDS.opus);
|
|
194
|
+
expect(resolveModelName("haiku-4.5")).toBe(SDK_MODEL_IDS.haiku);
|
|
184
195
|
});
|
|
185
196
|
|
|
186
197
|
it("passes through unknown names unchanged", () => {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mockSupportedModels = vi.fn();
|
|
4
|
+
|
|
5
|
+
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
|
6
|
+
query: vi.fn(() => ({
|
|
7
|
+
supportedModels: mockSupportedModels,
|
|
8
|
+
[Symbol.asyncIterator]() {
|
|
9
|
+
return {
|
|
10
|
+
next: async () => ({ done: true, value: undefined }),
|
|
11
|
+
};
|
|
12
|
+
},
|
|
13
|
+
})),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const sdkModels = [
|
|
17
|
+
{
|
|
18
|
+
value: "default",
|
|
19
|
+
displayName: "Default (recommended)",
|
|
20
|
+
description: "Sonnet 4.6 · Best for everyday tasks",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
value: "sonnet[1m]",
|
|
24
|
+
displayName: "Sonnet (1M context)",
|
|
25
|
+
description:
|
|
26
|
+
"Sonnet 4.6 with 1M context · Billed as extra usage · $3/$15 per Mtok",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
value: "opus",
|
|
30
|
+
displayName: "Opus",
|
|
31
|
+
description: "Opus 4.6 · Most capable for complex work",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
value: "opus[1m]",
|
|
35
|
+
displayName: "Opus (1M context)",
|
|
36
|
+
description:
|
|
37
|
+
"Opus 4.6 with 1M context · Billed as extra usage · $5/$25 per Mtok",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
value: "haiku",
|
|
41
|
+
displayName: "Haiku",
|
|
42
|
+
description: "Haiku 4.5 · Fastest for quick answers",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
value: "claude-sonnet-4-6",
|
|
46
|
+
displayName: "Sonnet 4.6",
|
|
47
|
+
description: "claude-sonnet-4-6",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
describe("registerClaudeModels", () => {
|
|
52
|
+
beforeEach(async () => {
|
|
53
|
+
vi.resetModules();
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
mockSupportedModels.mockResolvedValue(sdkModels);
|
|
56
|
+
|
|
57
|
+
const { clearModels } = await import("../core/models.js");
|
|
58
|
+
clearModels();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("keeps SDK IDs/display names and maps 1M upgrades explicitly", async () => {
|
|
62
|
+
const { registerClaudeModels } =
|
|
63
|
+
await import("../backend/claude-sdk/models.js");
|
|
64
|
+
const {
|
|
65
|
+
get1mContextModelId,
|
|
66
|
+
getModels,
|
|
67
|
+
resolveModelId,
|
|
68
|
+
supports1mContext,
|
|
69
|
+
} = await import("../core/models.js");
|
|
70
|
+
|
|
71
|
+
await registerClaudeModels({ model: "default" });
|
|
72
|
+
|
|
73
|
+
const anthropicModels = getModels("anthropic");
|
|
74
|
+
expect(anthropicModels.map((model) => model.id)).toEqual([
|
|
75
|
+
"opus",
|
|
76
|
+
"opus[1m]",
|
|
77
|
+
"default",
|
|
78
|
+
"sonnet[1m]",
|
|
79
|
+
"haiku",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
expect(
|
|
83
|
+
anthropicModels.find((model) => model.id === "default")?.displayName,
|
|
84
|
+
).toBe("Default (recommended)");
|
|
85
|
+
expect(
|
|
86
|
+
anthropicModels.find((model) => model.id === "sonnet[1m]")?.displayName,
|
|
87
|
+
).toBe("Sonnet (1M context)");
|
|
88
|
+
expect(
|
|
89
|
+
anthropicModels.some((model) => model.id === "claude-sonnet-4-6"),
|
|
90
|
+
).toBe(false);
|
|
91
|
+
|
|
92
|
+
expect(resolveModelId("claude-sonnet-4-6")).toBe("default");
|
|
93
|
+
expect(resolveModelId("claude-sonnet-4-6[1m]")).toBe("sonnet[1m]");
|
|
94
|
+
expect(resolveModelId("claude-opus-4-6")).toBe("opus");
|
|
95
|
+
|
|
96
|
+
expect(get1mContextModelId("default")).toBe("sonnet[1m]");
|
|
97
|
+
expect(get1mContextModelId("claude-sonnet-4-6")).toBe("sonnet[1m]");
|
|
98
|
+
expect(get1mContextModelId("opus")).toBe("opus[1m]");
|
|
99
|
+
expect(get1mContextModelId("haiku")).toBeNull();
|
|
100
|
+
|
|
101
|
+
expect(supports1mContext("claude-sonnet-4-6")).toBe(true);
|
|
102
|
+
expect(supports1mContext("haiku")).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("derives compatibility aliases from SDK metadata instead of hardcoded versions", async () => {
|
|
106
|
+
mockSupportedModels.mockResolvedValue([
|
|
107
|
+
{
|
|
108
|
+
value: "default",
|
|
109
|
+
displayName: "Default (recommended)",
|
|
110
|
+
description: "Sonnet 5.0 · Best for everyday tasks",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
value: "sonnet[1m]",
|
|
114
|
+
displayName: "Sonnet (1M context)",
|
|
115
|
+
description:
|
|
116
|
+
"Sonnet 5.0 with 1M context · Billed as extra usage · $3/$15 per Mtok",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
value: "opus",
|
|
120
|
+
displayName: "Opus",
|
|
121
|
+
description: "Opus 5.0 · Most capable for complex work",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
value: "opus[1m]",
|
|
125
|
+
displayName: "Opus (1M context)",
|
|
126
|
+
description:
|
|
127
|
+
"Opus 5.0 with 1M context · Billed as extra usage · $5/$25 per Mtok",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
value: "haiku",
|
|
131
|
+
displayName: "Haiku",
|
|
132
|
+
description: "Haiku 5.0 · Fastest for quick answers",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
value: "claude-sonnet-5-0",
|
|
136
|
+
displayName: "Sonnet 5.0",
|
|
137
|
+
description: "claude-sonnet-5-0",
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const { registerClaudeModels } =
|
|
142
|
+
await import("../backend/claude-sdk/models.js");
|
|
143
|
+
const { get1mContextModelId, resolveModelId } =
|
|
144
|
+
await import("../core/models.js");
|
|
145
|
+
|
|
146
|
+
await registerClaudeModels({ model: "default" });
|
|
147
|
+
|
|
148
|
+
expect(resolveModelId("claude-sonnet-5-0")).toBe("default");
|
|
149
|
+
expect(resolveModelId("claude-sonnet-4-6")).toBe("default");
|
|
150
|
+
expect(resolveModelId("claude-opus-5-0")).toBe("opus");
|
|
151
|
+
expect(resolveModelId("claude-opus-4-6")).toBe("opus");
|
|
152
|
+
expect(resolveModelId("claude-haiku-5-0")).toBe("haiku");
|
|
153
|
+
expect(resolveModelId("claude-haiku-4-5")).toBe("haiku");
|
|
154
|
+
expect(get1mContextModelId("claude-sonnet-4-6")).toBe("sonnet[1m]");
|
|
155
|
+
expect(get1mContextModelId("claude-sonnet-5-0")).toBe("sonnet[1m]");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mockGetSession = vi.fn();
|
|
4
|
+
const mockGetChatSettings = vi.fn();
|
|
5
|
+
const mockGetPluginMcpServers = vi.fn();
|
|
6
|
+
const mockGetConfig = vi.fn();
|
|
7
|
+
const mockGetBridgePort = vi.fn();
|
|
8
|
+
|
|
9
|
+
vi.mock("../storage/sessions.js", () => ({
|
|
10
|
+
getSession: (...args: unknown[]) =>
|
|
11
|
+
mockGetSession(...(args as Parameters<typeof mockGetSession>)),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("../storage/chat-settings.js", () => ({
|
|
15
|
+
getChatSettings: (...args: unknown[]) =>
|
|
16
|
+
mockGetChatSettings(...(args as Parameters<typeof mockGetChatSettings>)),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("../core/plugin.js", () => ({
|
|
20
|
+
getPluginMcpServers: (...args: unknown[]) =>
|
|
21
|
+
mockGetPluginMcpServers(
|
|
22
|
+
...(args as Parameters<typeof mockGetPluginMcpServers>),
|
|
23
|
+
),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("../backend/claude-sdk/state.js", () => ({
|
|
27
|
+
getConfig: (...args: unknown[]) =>
|
|
28
|
+
mockGetConfig(...(args as Parameters<typeof mockGetConfig>)),
|
|
29
|
+
getBridgePort: (...args: unknown[]) =>
|
|
30
|
+
mockGetBridgePort(...(args as Parameters<typeof mockGetBridgePort>)),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
describe("buildSdkOptions", () => {
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
vi.resetModules();
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
|
|
38
|
+
mockGetSession.mockReturnValue({ sessionId: null });
|
|
39
|
+
mockGetChatSettings.mockReturnValue({});
|
|
40
|
+
mockGetPluginMcpServers.mockReturnValue({});
|
|
41
|
+
mockGetConfig.mockReturnValue({
|
|
42
|
+
model: "claude-sonnet-4-6",
|
|
43
|
+
frontend: "terminal",
|
|
44
|
+
systemPrompt: "test prompt",
|
|
45
|
+
workspace: "/tmp/workspace",
|
|
46
|
+
});
|
|
47
|
+
mockGetBridgePort.mockReturnValue(19876);
|
|
48
|
+
|
|
49
|
+
const { clearModels, registerModels } = await import("../core/models.js");
|
|
50
|
+
clearModels();
|
|
51
|
+
registerModels([
|
|
52
|
+
{
|
|
53
|
+
id: "default",
|
|
54
|
+
displayName: "Default (recommended)",
|
|
55
|
+
description: "Sonnet 4.6 · Best for everyday tasks",
|
|
56
|
+
aliases: ["claude-sonnet-4-6"],
|
|
57
|
+
provider: "anthropic",
|
|
58
|
+
capabilities: {
|
|
59
|
+
supports1mContext: true,
|
|
60
|
+
oneMillionContextModelId: "sonnet[1m]",
|
|
61
|
+
},
|
|
62
|
+
tier: "balanced",
|
|
63
|
+
fallback: "haiku",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "sonnet[1m]",
|
|
67
|
+
displayName: "Sonnet (1M context)",
|
|
68
|
+
description:
|
|
69
|
+
"Sonnet 4.6 with 1M context · Billed as extra usage · $3/$15 per Mtok",
|
|
70
|
+
aliases: ["claude-sonnet-4-6[1m]"],
|
|
71
|
+
provider: "anthropic",
|
|
72
|
+
capabilities: { supports1mContext: true },
|
|
73
|
+
tier: "balanced",
|
|
74
|
+
fallback: "haiku",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "haiku",
|
|
78
|
+
displayName: "Haiku",
|
|
79
|
+
description: "Haiku 4.5 · Fastest for quick answers",
|
|
80
|
+
aliases: ["claude-haiku-4-5"],
|
|
81
|
+
provider: "anthropic",
|
|
82
|
+
capabilities: { supports1mContext: false },
|
|
83
|
+
tier: "economy",
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("uses the exact mapped 1M SDK model for legacy Sonnet IDs", async () => {
|
|
89
|
+
const { buildSdkOptions } =
|
|
90
|
+
await import("../backend/claude-sdk/options.js");
|
|
91
|
+
|
|
92
|
+
const { activeModel, options } = buildSdkOptions("chat-1");
|
|
93
|
+
|
|
94
|
+
expect(activeModel).toBe("claude-sonnet-4-6");
|
|
95
|
+
expect(options.model).toBe("sonnet[1m]");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("leaves models without a mapped 1M variant unchanged", async () => {
|
|
99
|
+
mockGetChatSettings.mockReturnValue({ model: "haiku" });
|
|
100
|
+
|
|
101
|
+
const { buildSdkOptions } =
|
|
102
|
+
await import("../backend/claude-sdk/options.js");
|
|
103
|
+
const { options } = buildSdkOptions("chat-2");
|
|
104
|
+
|
|
105
|
+
expect(options.model).toBe("haiku");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("resolves legacy 1M aliases to canonical SDK model IDs", async () => {
|
|
109
|
+
mockGetChatSettings.mockReturnValue({ model: "claude-sonnet-4-6[1m]" });
|
|
110
|
+
|
|
111
|
+
const { buildSdkOptions } =
|
|
112
|
+
await import("../backend/claude-sdk/options.js");
|
|
113
|
+
const { activeModel, options } = buildSdkOptions("chat-3");
|
|
114
|
+
|
|
115
|
+
expect(activeModel).toBe("claude-sonnet-4-6[1m]");
|
|
116
|
+
expect(options.model).toBe("sonnet[1m]");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -94,7 +94,7 @@ describe("config", () => {
|
|
|
94
94
|
const { loadConfig } = await import("../util/config.js");
|
|
95
95
|
const config = loadConfig();
|
|
96
96
|
expect(config.frontend).toBe("terminal");
|
|
97
|
-
expect(config.model).toBe("
|
|
97
|
+
expect(config.model).toBe("default");
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
it("throws when telegram frontend has no botToken", async () => {
|
|
@@ -118,7 +118,7 @@ describe("config", () => {
|
|
|
118
118
|
|
|
119
119
|
const { loadConfig } = await import("../util/config.js");
|
|
120
120
|
const config = loadConfig();
|
|
121
|
-
expect(config.model).toBe("
|
|
121
|
+
expect(config.model).toBe("default");
|
|
122
122
|
expect(config.maxMessageLength).toBe(4000);
|
|
123
123
|
expect(config.concurrency).toBe(1);
|
|
124
124
|
expect(config.pulse).toBe(true);
|
|
@@ -195,10 +195,91 @@ describe("config", () => {
|
|
|
195
195
|
const { loadConfig } = await import("../util/config.js");
|
|
196
196
|
const config = loadConfig();
|
|
197
197
|
expect(config.plugins).toHaveLength(2);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
expect(
|
|
201
|
-
|
|
198
|
+
const [firstPlugin, secondPlugin] = config.plugins;
|
|
199
|
+
|
|
200
|
+
expect("path" in firstPlugin).toBe(true);
|
|
201
|
+
if ("path" in firstPlugin) {
|
|
202
|
+
expect(firstPlugin.path).toBe("./plugins/my-plugin");
|
|
203
|
+
expect(firstPlugin.config).toEqual({ key: "value" });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
expect("path" in secondPlugin).toBe(true);
|
|
207
|
+
if ("path" in secondPlugin) {
|
|
208
|
+
expect(secondPlugin.path).toBe("./plugins/another");
|
|
209
|
+
expect(secondPlugin.config).toBeUndefined();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("parses standalone MCP plugins in config", async () => {
|
|
214
|
+
mockFs({
|
|
215
|
+
frontend: "terminal",
|
|
216
|
+
plugins: [
|
|
217
|
+
{
|
|
218
|
+
name: "polymarket",
|
|
219
|
+
command: "node",
|
|
220
|
+
args: ["/tmp/polymarket.js"],
|
|
221
|
+
env: { POLYMARKET_PRIVATE_KEY: "0x123" },
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const { loadConfig } = await import("../util/config.js");
|
|
227
|
+
const config = loadConfig();
|
|
228
|
+
|
|
229
|
+
expect(config.plugins).toEqual([
|
|
230
|
+
{
|
|
231
|
+
name: "polymarket",
|
|
232
|
+
command: "node",
|
|
233
|
+
args: ["/tmp/polymarket.js"],
|
|
234
|
+
env: { POLYMARKET_PRIVATE_KEY: "0x123" },
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("rejects plugin entries that mix path and standalone MCP fields", async () => {
|
|
240
|
+
mockFs({
|
|
241
|
+
frontend: "terminal",
|
|
242
|
+
plugins: [
|
|
243
|
+
{
|
|
244
|
+
path: "./plugins/extras",
|
|
245
|
+
name: "extras",
|
|
246
|
+
command: "node",
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const { loadConfig } = await import("../util/config.js");
|
|
252
|
+
expect(() => loadConfig()).toThrow("exactly one format");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("rejects standalone MCP entries missing required fields", async () => {
|
|
256
|
+
mockFs({
|
|
257
|
+
frontend: "terminal",
|
|
258
|
+
plugins: [{ name: "polymarket" }],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const { loadConfig } = await import("../util/config.js");
|
|
262
|
+
expect(() => loadConfig()).toThrow(
|
|
263
|
+
"MCP plugin entries must include 'command'",
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("rejects standalone MCP entries with config blocks", async () => {
|
|
268
|
+
mockFs({
|
|
269
|
+
frontend: "terminal",
|
|
270
|
+
plugins: [
|
|
271
|
+
{
|
|
272
|
+
name: "polymarket",
|
|
273
|
+
command: "node",
|
|
274
|
+
config: { market: "crypto" },
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const { loadConfig } = await import("../util/config.js");
|
|
280
|
+
expect(() => loadConfig()).toThrow(
|
|
281
|
+
"MCP plugin entries cannot include 'config'",
|
|
282
|
+
);
|
|
202
283
|
});
|
|
203
284
|
|
|
204
285
|
it("defaults plugins to empty array", async () => {
|
|
@@ -235,6 +316,29 @@ describe("config", () => {
|
|
|
235
316
|
const config = loadConfig();
|
|
236
317
|
expect(config.frontend).toEqual(["terminal"]);
|
|
237
318
|
});
|
|
319
|
+
|
|
320
|
+
it("preserves Playwright endpoint settings from config", async () => {
|
|
321
|
+
mockFs({
|
|
322
|
+
frontend: "terminal",
|
|
323
|
+
playwright: {
|
|
324
|
+
enabled: true,
|
|
325
|
+
browser: "firefox",
|
|
326
|
+
endpoint: "ws://127.0.0.1:9222/devtools/browser/test",
|
|
327
|
+
endpointFile: "/tmp/camoufox-endpoint.txt",
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const { loadConfig } = await import("../util/config.js");
|
|
332
|
+
const config = loadConfig();
|
|
333
|
+
|
|
334
|
+
expect(config.playwright).toEqual({
|
|
335
|
+
enabled: true,
|
|
336
|
+
browser: "firefox",
|
|
337
|
+
headless: true,
|
|
338
|
+
endpoint: "ws://127.0.0.1:9222/devtools/browser/test",
|
|
339
|
+
endpointFile: "/tmp/camoufox-endpoint.txt",
|
|
340
|
+
});
|
|
341
|
+
});
|
|
238
342
|
});
|
|
239
343
|
|
|
240
344
|
describe("system prompt", () => {
|
|
@@ -533,12 +637,12 @@ describe("config", () => {
|
|
|
533
637
|
expect(() => loadConfig()).toThrow();
|
|
534
638
|
});
|
|
535
639
|
|
|
536
|
-
it("
|
|
640
|
+
it("defaults the canonical Claude model to default", async () => {
|
|
537
641
|
mockFs({ frontend: "terminal" });
|
|
538
642
|
|
|
539
643
|
const { loadConfig } = await import("../util/config.js");
|
|
540
644
|
const config = loadConfig();
|
|
541
|
-
expect(config.model).toBe("
|
|
645
|
+
expect(config.model).toBe("default");
|
|
542
646
|
});
|
|
543
647
|
|
|
544
648
|
it("default pulse is exactly true", async () => {
|
|
@@ -881,7 +881,7 @@ describe("dream error paths", () => {
|
|
|
881
881
|
);
|
|
882
882
|
});
|
|
883
883
|
|
|
884
|
-
it("model defaults to '
|
|
884
|
+
it("model defaults to 'default' when neither dreamModel nor model set (line 135 FALSE??FALSE branch)", async () => {
|
|
885
885
|
vi.doMock("node:fs", () => ({
|
|
886
886
|
existsSync: vi.fn(() => false),
|
|
887
887
|
readFileSync: vi.fn(() => "dream prompt"),
|
|
@@ -913,14 +913,14 @@ describe("dream error paths", () => {
|
|
|
913
913
|
vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({ query: queryMock }));
|
|
914
914
|
|
|
915
915
|
const mod = await import("../core/dream.js");
|
|
916
|
-
// No model or dreamModel → falls through to
|
|
916
|
+
// No model or dreamModel → falls through to the canonical SDK default model
|
|
917
917
|
mod.initDream({ workspace: "/fake/ws" });
|
|
918
918
|
await mod.forceDream();
|
|
919
919
|
|
|
920
920
|
const callArgs = (queryMock.mock.calls[0] as unknown[])[0] as {
|
|
921
921
|
options: Record<string, unknown>;
|
|
922
922
|
};
|
|
923
|
-
expect(callArgs.options).toHaveProperty("model", "
|
|
923
|
+
expect(callArgs.options).toHaveProperty("model", "default");
|
|
924
924
|
});
|
|
925
925
|
});
|
|
926
926
|
|
|
@@ -324,25 +324,25 @@ describe("fuzz: resolveModelName()", () => {
|
|
|
324
324
|
);
|
|
325
325
|
});
|
|
326
326
|
|
|
327
|
-
it("known aliases
|
|
328
|
-
const
|
|
329
|
-
"sonnet",
|
|
330
|
-
"opus",
|
|
331
|
-
"haiku",
|
|
332
|
-
"sonnet-4.6",
|
|
333
|
-
"opus-4.6",
|
|
334
|
-
"haiku-4.5",
|
|
335
|
-
"sonnet-4-6",
|
|
336
|
-
"opus-4-6",
|
|
337
|
-
"haiku-4-5",
|
|
338
|
-
];
|
|
327
|
+
it("known aliases resolve to the expected SDK model IDs", () => {
|
|
328
|
+
const aliasMappings = [
|
|
329
|
+
["sonnet", "default"],
|
|
330
|
+
["opus", "opus"],
|
|
331
|
+
["haiku", "haiku"],
|
|
332
|
+
["sonnet-4.6", "default"],
|
|
333
|
+
["opus-4.6", "opus"],
|
|
334
|
+
["haiku-4.5", "haiku"],
|
|
335
|
+
["sonnet-4-6", "default"],
|
|
336
|
+
["opus-4-6", "opus"],
|
|
337
|
+
["haiku-4-5", "haiku"],
|
|
338
|
+
] as const;
|
|
339
339
|
fc.assert(
|
|
340
340
|
fc.property(
|
|
341
|
-
fc.constantFrom(...
|
|
341
|
+
fc.constantFrom(...aliasMappings),
|
|
342
342
|
fc.constantFrom("", " ", " "),
|
|
343
|
-
(alias, padding) => {
|
|
343
|
+
([alias, expectedModelId], padding) => {
|
|
344
344
|
const result = resolveModelName(padding + alias + padding);
|
|
345
|
-
expect(result).
|
|
345
|
+
expect(result).toBe(expectedModelId);
|
|
346
346
|
},
|
|
347
347
|
),
|
|
348
348
|
fcParams,
|