kimaki 0.27.0 → 0.28.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/dist/agent-model.e2e.test.js +247 -64
- package/dist/anthropic-account-identity.js +8 -1
- package/dist/anthropic-auth-plugin.js +9 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +6 -1
- package/dist/cli-commands/session.js +139 -82
- package/dist/cli-commands/task.js +21 -14
- package/dist/cli-parsing.test.js +24 -1
- package/dist/cli-runner.js +16 -4
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +158 -24
- package/dist/commands/ask-question.js +2 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/merge-worktree.js +13 -6
- package/dist/commands/model-variant.js +3 -2
- package/dist/commands/model.js +4 -3
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +28 -0
- package/dist/db.js +11 -0
- package/dist/db.test.js +100 -0
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +501 -474
- package/dist/discord-command-registration.js +16 -9
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +84 -0
- package/dist/discord-utils.test.js +135 -1
- package/dist/errors.js +5 -0
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +12 -7
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +2 -2
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/logger.js +1 -0
- package/dist/message-formatting.js +163 -44
- package/dist/message-formatting.test.js +473 -13
- package/dist/message-preprocessing.js +105 -1
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +49 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +29 -3
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +5 -5
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-footer.e2e.test.js +29 -32
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +9 -7
- package/dist/queue-advanced-question.e2e.test.js +2 -2
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +20 -20
- package/dist/queue-question-select-drain.e2e.test.js +7 -7
- package/dist/runtime-lifecycle.e2e.test.js +16 -19
- package/dist/schema.js +1 -1
- package/dist/session-handler/event-stream-state.js +124 -0
- package/dist/session-handler/event-stream-state.test.js +322 -1
- package/dist/session-handler/model-utils.js +16 -3
- package/dist/session-handler/model-utils.test.js +103 -3
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +340 -135
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-title-rename.test.js +30 -0
- package/dist/sleep.e2e.test.js +14 -11
- package/dist/system-message.js +45 -12
- package/dist/system-message.test.js +117 -10
- package/dist/task-runner.js +2 -1
- package/dist/task-schedule.js +32 -2
- package/dist/task-schedule.test.js +79 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -54
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +41 -1
- package/dist/worktree-lifecycle.e2e.test.js +30 -30
- package/package.json +8 -8
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +56 -232
- package/skills/holocron/SKILL.md +117 -17
- package/src/agent-model.e2e.test.ts +293 -66
- package/src/anthropic-account-identity.ts +19 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +11 -16
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +6 -1
- package/src/cli-commands/session.ts +171 -105
- package/src/cli-commands/task.ts +22 -15
- package/src/cli-parsing.test.ts +34 -1
- package/src/cli-runner.ts +16 -3
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +199 -23
- package/src/commands/ask-question.ts +2 -1
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/merge-worktree.ts +13 -6
- package/src/commands/model-variant.ts +3 -2
- package/src/commands/model.ts +4 -3
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +40 -0
- package/src/db.test.ts +105 -0
- package/src/db.ts +12 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +43 -7
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +27 -11
- package/src/discord-utils.test.ts +157 -0
- package/src/discord-utils.ts +131 -1
- package/src/errors.ts +7 -0
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +16 -9
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +5 -5
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/logger.ts +1 -0
- package/src/message-formatting.test.ts +586 -13
- package/src/message-formatting.ts +223 -46
- package/src/message-preprocessing.ts +116 -1
- package/src/opencode-command.test.ts +59 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +44 -4
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +5 -5
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-footer.e2e.test.ts +30 -31
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +9 -7
- package/src/queue-advanced-question.e2e.test.ts +2 -2
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +20 -20
- package/src/queue-question-select-drain.e2e.test.ts +7 -7
- package/src/runtime-lifecycle.e2e.test.ts +16 -18
- package/src/schema.sql +1 -1
- package/src/schema.ts +1 -1
- package/src/session-handler/event-stream-state.test.ts +353 -0
- package/src/session-handler/event-stream-state.ts +179 -0
- package/src/session-handler/model-utils.test.ts +125 -3
- package/src/session-handler/model-utils.ts +36 -4
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +427 -158
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-title-rename.test.ts +45 -0
- package/src/sleep.e2e.test.ts +14 -11
- package/src/store.ts +1 -0
- package/src/system-message.test.ts +141 -10
- package/src/system-message.ts +66 -11
- package/src/task-runner.ts +2 -1
- package/src/task-schedule.test.ts +94 -0
- package/src/task-schedule.ts +47 -2
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -53
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +44 -0
- package/src/voice.ts +34 -6
- package/src/worktree-lifecycle.e2e.test.ts +30 -30
|
@@ -13,20 +13,21 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import url from 'node:url';
|
|
15
15
|
import { describe, beforeAll, afterAll, test, expect, } from 'vitest';
|
|
16
|
-
import { ChannelType, Client, GatewayIntentBits, Partials, REST, Routes
|
|
16
|
+
import { ChannelType, Client, GatewayIntentBits, Partials, REST, Routes } from 'discord.js';
|
|
17
17
|
import { DigitalDiscord } from 'discord-digital-twin/src';
|
|
18
18
|
import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provider';
|
|
19
19
|
import { setDataDir } from './config.js';
|
|
20
20
|
import { store } from './store.js';
|
|
21
21
|
import { startDiscordBot } from './discord-bot.js';
|
|
22
|
-
import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, setChannelAgent, setChannelModel, getThreadSession, getSessionModel, getSessionAgent, getChannelAgent, setSessionModel, } from './database.js';
|
|
22
|
+
import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, setChannelAgent, setChannelModel, getThreadSession, getSessionModel, getSessionAgent, getChannelAgent, getChannelModel, setSessionModel, } from './database.js';
|
|
23
23
|
import { getDb } from './db.js';
|
|
24
24
|
import * as orm from 'drizzle-orm';
|
|
25
25
|
import * as schema from './schema.js';
|
|
26
26
|
import { startHranaServer, stopHranaServer } from './hrana-server.js';
|
|
27
27
|
import { initializeOpencodeForDirectory, stopOpencodeServer } from './opencode.js';
|
|
28
|
-
import { chooseLockPort, cleanupTestSessions, initTestGitRepo, waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
|
|
28
|
+
import { chooseLockPort, cleanupTestSessions, initTestGitRepo, isFooterMessage, waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
|
|
29
29
|
import { buildQuickAgentCommandDescription } from './commands/agent.js';
|
|
30
|
+
import { buildQuickAgentSlashCommand } from './discord-command-registration.js';
|
|
30
31
|
const TEST_USER_ID = '200000000000000920';
|
|
31
32
|
const TEXT_CHANNEL_ID = '200000000000000921';
|
|
32
33
|
const AGENT_MODEL = 'agent-model-v2';
|
|
@@ -265,6 +266,9 @@ describe('agent model resolution', () => {
|
|
|
265
266
|
}
|
|
266
267
|
providerConfig.models[AGENT_MODEL] = { name: AGENT_MODEL };
|
|
267
268
|
providerConfig.models[PLAN_AGENT_MODEL] = { name: PLAN_AGENT_MODEL };
|
|
269
|
+
Object.assign(providerConfig.models[PLAN_AGENT_MODEL], {
|
|
270
|
+
variants: { high: {}, max: {} },
|
|
271
|
+
});
|
|
268
272
|
providerConfig.models[CHANNEL_MODEL] = { name: CHANNEL_MODEL };
|
|
269
273
|
fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
|
|
270
274
|
// Leading /command detection only rewrites when registeredUserCommands is set
|
|
@@ -316,18 +320,13 @@ describe('agent model resolution', () => {
|
|
|
316
320
|
// Register quick agent slash commands so /plan-agent and /test-agent-agent
|
|
317
321
|
// are resolvable by handleQuickAgentCommand via guild.commands.fetch().
|
|
318
322
|
const agentCommands = ['test-agent', 'plan', 'plain'].map((agentName) => {
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
.addStringOption((opt) => opt
|
|
327
|
-
.setName('prompt')
|
|
328
|
-
.setDescription('Send a prompt with this agent')
|
|
329
|
-
.setRequired(false))
|
|
330
|
-
.toJSON();
|
|
323
|
+
return buildQuickAgentSlashCommand({
|
|
324
|
+
commandName: `${agentName}-agent`,
|
|
325
|
+
description: buildQuickAgentCommandDescription({
|
|
326
|
+
agentName,
|
|
327
|
+
description: `Switch to ${agentName} agent`,
|
|
328
|
+
}),
|
|
329
|
+
}).toJSON();
|
|
331
330
|
});
|
|
332
331
|
const rest = new REST({ version: '10', api: discord.restUrl }).setToken(discord.botToken);
|
|
333
332
|
await rest.put(Routes.applicationGuildCommands(discord.botUserId, discord.guildId), { body: agentCommands });
|
|
@@ -394,15 +393,15 @@ describe('agent model resolution', () => {
|
|
|
394
393
|
// Find the footer message (starts with * italic)
|
|
395
394
|
const footerMessage = messages.find((message) => {
|
|
396
395
|
return (message.author.id === discord.botUserId &&
|
|
397
|
-
message.
|
|
396
|
+
isFooterMessage({ message, botUserId: discord.botUserId }));
|
|
398
397
|
});
|
|
399
398
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
400
399
|
"--- from: user (agent-model-tester)
|
|
401
400
|
Reply with exactly: agent-model-check
|
|
402
401
|
--- from: assistant (TestBot)
|
|
403
|
-
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
404
|
-
|
|
405
|
-
*project ⋅ main ⋅
|
|
402
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
403
|
+
> ok
|
|
404
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>"
|
|
406
405
|
`);
|
|
407
406
|
expect(footerMessage).toBeDefined();
|
|
408
407
|
if (!footerMessage) {
|
|
@@ -444,9 +443,9 @@ describe('agent model resolution', () => {
|
|
|
444
443
|
"--- from: user (agent-model-tester)
|
|
445
444
|
Reply with exactly: system-context-check
|
|
446
445
|
--- from: assistant (TestBot)
|
|
447
|
-
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
448
|
-
|
|
449
|
-
*project ⋅ main ⋅
|
|
446
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
447
|
+
> system-context-ok
|
|
448
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>"
|
|
450
449
|
`);
|
|
451
450
|
}, 15_000);
|
|
452
451
|
test('session.command path includes kimaki system prompt on first message', async () => {
|
|
@@ -514,7 +513,7 @@ describe('agent model resolution', () => {
|
|
|
514
513
|
const threadText = await discord.thread(thread.id).text();
|
|
515
514
|
expect(threadText).toContain('first message in thread');
|
|
516
515
|
expect(threadText).toContain('Reply with exactly: reply-context-check');
|
|
517
|
-
expect(threadText).toContain('
|
|
516
|
+
expect(threadText).toContain('ok');
|
|
518
517
|
}, 15_000);
|
|
519
518
|
test('new thread uses channel model when channel model preference is set', async () => {
|
|
520
519
|
// Clear channel agent so model resolution falls through to channel model
|
|
@@ -544,15 +543,15 @@ describe('agent model resolution', () => {
|
|
|
544
543
|
const messages = await discord.thread(thread.id).getMessages();
|
|
545
544
|
const footerMessage = messages.find((message) => {
|
|
546
545
|
return (message.author.id === discord.botUserId &&
|
|
547
|
-
message.
|
|
546
|
+
isFooterMessage({ message, botUserId: discord.botUserId }));
|
|
548
547
|
});
|
|
549
548
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
550
549
|
"--- from: user (agent-model-tester)
|
|
551
550
|
Reply with exactly: channel-model-check
|
|
552
551
|
--- from: assistant (TestBot)
|
|
553
|
-
*using deterministic-provider/channel-model-v2*
|
|
554
|
-
|
|
555
|
-
*project ⋅ main ⋅
|
|
552
|
+
> *using deterministic-provider/channel-model-v2*
|
|
553
|
+
> ok
|
|
554
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ channel-model-v2* <@200000000000000920>"
|
|
556
555
|
`);
|
|
557
556
|
expect(footerMessage).toBeDefined();
|
|
558
557
|
if (!footerMessage) {
|
|
@@ -588,15 +587,15 @@ describe('agent model resolution', () => {
|
|
|
588
587
|
});
|
|
589
588
|
const footer = messages.find((message) => {
|
|
590
589
|
return message.author.id === discord.botUserId
|
|
591
|
-
&& message.
|
|
590
|
+
&& isFooterMessage({ message, botUserId: discord.botUserId });
|
|
592
591
|
});
|
|
593
592
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
594
593
|
"--- from: user (agent-model-tester)
|
|
595
594
|
Reply with exactly: variant-check
|
|
596
595
|
--- from: assistant (TestBot)
|
|
597
|
-
*using deterministic-provider/channel-model-v2*
|
|
598
|
-
|
|
599
|
-
*project ⋅ main ⋅
|
|
596
|
+
> *using deterministic-provider/channel-model-v2*
|
|
597
|
+
> ok
|
|
598
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ channel-model-v2* <@200000000000000920>"
|
|
600
599
|
`);
|
|
601
600
|
expect(footer?.content).toContain(CHANNEL_MODEL);
|
|
602
601
|
expect(footer?.content).not.toContain(DEFAULT_MODEL);
|
|
@@ -662,8 +661,8 @@ describe('agent model resolution', () => {
|
|
|
662
661
|
"--- from: assistant (TestBot)
|
|
663
662
|
Reusing context from <#SOURCE_THREAD> to answer prompt...
|
|
664
663
|
Reply with exactly: btw-model-check
|
|
665
|
-
|
|
666
|
-
*project ⋅ main ⋅
|
|
664
|
+
> ok
|
|
665
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2* <@200000000000000920>"
|
|
667
666
|
`);
|
|
668
667
|
expect(forkedSessionModel).toMatchInlineSnapshot(`
|
|
669
668
|
{
|
|
@@ -696,7 +695,7 @@ describe('agent model resolution', () => {
|
|
|
696
695
|
});
|
|
697
696
|
const firstMessages = await discord.thread(thread.id).getMessages();
|
|
698
697
|
const firstFooter = firstMessages.find((m) => {
|
|
699
|
-
return (m
|
|
698
|
+
return (isFooterMessage({ message: m, botUserId: discord.botUserId }));
|
|
700
699
|
});
|
|
701
700
|
expect(firstFooter).toBeDefined();
|
|
702
701
|
// Verify the first run used test-agent's model
|
|
@@ -726,20 +725,20 @@ describe('agent model resolution', () => {
|
|
|
726
725
|
"--- from: user (agent-model-tester)
|
|
727
726
|
Reply with exactly: first-thread-msg
|
|
728
727
|
--- from: assistant (TestBot)
|
|
729
|
-
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
730
|
-
|
|
731
|
-
*project ⋅ main ⋅
|
|
728
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
729
|
+
> ok
|
|
730
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
732
731
|
--- from: user (agent-model-tester)
|
|
733
732
|
Reply with exactly: second-thread-msg
|
|
734
733
|
--- from: assistant (TestBot)
|
|
735
|
-
|
|
736
|
-
*project ⋅ main ⋅
|
|
734
|
+
> ok
|
|
735
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>"
|
|
737
736
|
`);
|
|
738
737
|
const secondMessages = await discord.thread(thread.id).getMessages();
|
|
739
738
|
const secondFooter = [...secondMessages]
|
|
740
739
|
.reverse()
|
|
741
740
|
.find((m) => {
|
|
742
|
-
return (m
|
|
741
|
+
return (isFooterMessage({ message: m, botUserId: discord.botUserId }));
|
|
743
742
|
});
|
|
744
743
|
expect(secondFooter).toBeDefined();
|
|
745
744
|
// The existing thread should still use test-agent's model (AGENT_MODEL),
|
|
@@ -780,10 +779,94 @@ describe('agent model resolution', () => {
|
|
|
780
779
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
781
780
|
"--- from: assistant (TestBot)
|
|
782
781
|
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-msg
|
|
783
|
-
*using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
784
|
-
|
|
785
|
-
*project ⋅ main ⋅
|
|
782
|
+
> *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
783
|
+
> ok
|
|
784
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
785
|
+
`);
|
|
786
|
+
}, 20_000);
|
|
787
|
+
test('/plan-agent with prompt persists a supported thinking variant', async () => {
|
|
788
|
+
const prompt = 'Reply with exactly: inline-plan-agent-variant-msg';
|
|
789
|
+
const { id: interactionId } = await discord
|
|
790
|
+
.channel(TEXT_CHANNEL_ID)
|
|
791
|
+
.user(TEST_USER_ID)
|
|
792
|
+
.runSlashCommand({
|
|
793
|
+
name: 'plan-agent',
|
|
794
|
+
options: [
|
|
795
|
+
{ name: 'prompt', type: 3, value: prompt },
|
|
796
|
+
{ name: 'variant', type: 3, value: 'high' },
|
|
797
|
+
],
|
|
798
|
+
});
|
|
799
|
+
await discord
|
|
800
|
+
.channel(TEXT_CHANNEL_ID)
|
|
801
|
+
.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
802
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
803
|
+
timeout: 4_000,
|
|
804
|
+
predicate: (t) => t.name === prompt,
|
|
805
|
+
});
|
|
806
|
+
await waitForFooterMessage({
|
|
807
|
+
discord,
|
|
808
|
+
threadId: thread.id,
|
|
809
|
+
timeout: 4_000,
|
|
810
|
+
afterMessageIncludes: 'ok',
|
|
811
|
+
afterAuthorId: discord.botUserId,
|
|
812
|
+
});
|
|
813
|
+
const sessionId = await getThreadSession(thread.id);
|
|
814
|
+
expect(sessionId).toBeDefined();
|
|
815
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
816
|
+
"--- from: assistant (TestBot)
|
|
817
|
+
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-variant-msg
|
|
818
|
+
> *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
819
|
+
> ok
|
|
820
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
821
|
+
`);
|
|
822
|
+
expect(sessionId ? await getSessionModel(sessionId) : undefined).toMatchInlineSnapshot(`
|
|
823
|
+
{
|
|
824
|
+
"modelId": "deterministic-provider/plan-model-v2",
|
|
825
|
+
"variant": "high",
|
|
826
|
+
}
|
|
827
|
+
`);
|
|
828
|
+
}, 20_000);
|
|
829
|
+
test('/plan-agent variant persists on the channel without a prompt', async () => {
|
|
830
|
+
const db = await getDb();
|
|
831
|
+
await db.delete(schema.channel_models).where(orm.eq(schema.channel_models.channel_id, TEXT_CHANNEL_ID));
|
|
832
|
+
const { id: interactionId } = await discord
|
|
833
|
+
.channel(TEXT_CHANNEL_ID)
|
|
834
|
+
.user(TEST_USER_ID)
|
|
835
|
+
.runSlashCommand({
|
|
836
|
+
name: 'plan-agent',
|
|
837
|
+
options: [{ name: 'variant', type: 3, value: 'max' }],
|
|
838
|
+
});
|
|
839
|
+
await discord
|
|
840
|
+
.channel(TEXT_CHANNEL_ID)
|
|
841
|
+
.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
842
|
+
const start = Date.now();
|
|
843
|
+
let confirmation = '';
|
|
844
|
+
while (Date.now() - start < 4_000) {
|
|
845
|
+
const messages = await discord.channel(TEXT_CHANNEL_ID).getMessages();
|
|
846
|
+
const match = [...messages].reverse().find((message) => {
|
|
847
|
+
return (message.author.id === discord.botUserId &&
|
|
848
|
+
message.content.includes('Variant: **max**'));
|
|
849
|
+
});
|
|
850
|
+
if (match) {
|
|
851
|
+
confirmation = match.content;
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
855
|
+
}
|
|
856
|
+
expect(confirmation).toMatchInlineSnapshot(`
|
|
857
|
+
"Switched to **plan** agent for this channel (was **test-agent**)
|
|
858
|
+
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
859
|
+
Variant: **max**
|
|
860
|
+
All new sessions will use this agent."
|
|
861
|
+
`);
|
|
862
|
+
expect(await getChannelAgent(TEXT_CHANNEL_ID)).toBe('plan');
|
|
863
|
+
expect(await getChannelModel(TEXT_CHANNEL_ID)).toMatchInlineSnapshot(`
|
|
864
|
+
{
|
|
865
|
+
"modelId": "deterministic-provider/plan-model-v2",
|
|
866
|
+
"variant": "max",
|
|
867
|
+
}
|
|
786
868
|
`);
|
|
869
|
+
await db.delete(schema.channel_models).where(orm.eq(schema.channel_models.channel_id, TEXT_CHANNEL_ID));
|
|
787
870
|
}, 20_000);
|
|
788
871
|
test('/plan-agent with prompt in an existing thread changes the session agent', async () => {
|
|
789
872
|
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
@@ -825,12 +908,12 @@ describe('agent model resolution', () => {
|
|
|
825
908
|
"--- from: user (agent-model-tester)
|
|
826
909
|
Reply with exactly: inline-existing-first-msg
|
|
827
910
|
--- from: assistant (TestBot)
|
|
828
|
-
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
829
|
-
|
|
830
|
-
*project ⋅ main ⋅
|
|
911
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
912
|
+
> ok
|
|
913
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
831
914
|
» **agent-model-tester** (plan): Reply with exactly: inline-existing-plan-msg
|
|
832
|
-
|
|
833
|
-
*project ⋅ main ⋅
|
|
915
|
+
> ok
|
|
916
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
834
917
|
`);
|
|
835
918
|
}, 20_000);
|
|
836
919
|
test('/plan-agent inside a thread switches the model for that thread', async () => {
|
|
@@ -854,7 +937,7 @@ describe('agent model resolution', () => {
|
|
|
854
937
|
afterAuthorId: discord.botUserId,
|
|
855
938
|
});
|
|
856
939
|
const firstFooter = (await discord.thread(thread.id).getMessages()).find((m) => {
|
|
857
|
-
return (m
|
|
940
|
+
return (isFooterMessage({ message: m, botUserId: discord.botUserId }));
|
|
858
941
|
});
|
|
859
942
|
expect(firstFooter).toBeDefined();
|
|
860
943
|
expect(firstFooter.content).toContain(AGENT_MODEL);
|
|
@@ -879,28 +962,128 @@ describe('agent model resolution', () => {
|
|
|
879
962
|
"--- from: user (agent-model-tester)
|
|
880
963
|
Reply with exactly: switch-in-thread-msg
|
|
881
964
|
--- from: assistant (TestBot)
|
|
882
|
-
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
883
|
-
|
|
884
|
-
*project ⋅ main ⋅
|
|
965
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
966
|
+
> ok
|
|
967
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
885
968
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
886
969
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
887
970
|
The agent will change on the next message.
|
|
888
971
|
--- from: user (agent-model-tester)
|
|
889
972
|
Reply with exactly: after-switch-msg
|
|
890
973
|
--- from: assistant (TestBot)
|
|
891
|
-
|
|
892
|
-
*project ⋅ main ⋅
|
|
974
|
+
> ok
|
|
975
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
893
976
|
`);
|
|
894
977
|
const secondFooter = [...(await discord.thread(thread.id).getMessages())]
|
|
895
978
|
.reverse()
|
|
896
979
|
.find((m) => {
|
|
897
|
-
return (m
|
|
980
|
+
return (isFooterMessage({ message: m, botUserId: discord.botUserId }));
|
|
898
981
|
});
|
|
899
982
|
expect(secondFooter).toBeDefined();
|
|
900
983
|
// After /plan-agent in the thread, model should switch to plan's model
|
|
901
984
|
expect(secondFooter.content).toContain(PLAN_AGENT_MODEL);
|
|
902
985
|
expect(secondFooter.content).not.toContain(AGENT_MODEL);
|
|
903
986
|
}, 20_000);
|
|
987
|
+
test('/plan-agent then an immediate follow-up uses the new agent', async () => {
|
|
988
|
+
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
989
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
990
|
+
content: 'Reply with exactly: race-switch-first-msg',
|
|
991
|
+
});
|
|
992
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
993
|
+
timeout: 4_000,
|
|
994
|
+
predicate: (t) => {
|
|
995
|
+
return t.name === 'Reply with exactly: race-switch-first-msg';
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
await waitForFooterMessage({
|
|
999
|
+
discord,
|
|
1000
|
+
threadId: thread.id,
|
|
1001
|
+
timeout: 4_000,
|
|
1002
|
+
afterMessageIncludes: 'ok',
|
|
1003
|
+
afterAuthorId: discord.botUserId,
|
|
1004
|
+
});
|
|
1005
|
+
const th = discord.thread(thread.id);
|
|
1006
|
+
const { id: interactionId } = await th
|
|
1007
|
+
.user(TEST_USER_ID)
|
|
1008
|
+
.runSlashCommand({ name: 'plan-agent' });
|
|
1009
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
1010
|
+
content: 'Reply with exactly: race-switch-follow-up',
|
|
1011
|
+
});
|
|
1012
|
+
await th.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
1013
|
+
await waitForFooterMessage({
|
|
1014
|
+
discord,
|
|
1015
|
+
threadId: thread.id,
|
|
1016
|
+
timeout: 4_000,
|
|
1017
|
+
afterMessageIncludes: 'race-switch-follow-up',
|
|
1018
|
+
afterAuthorId: TEST_USER_ID,
|
|
1019
|
+
});
|
|
1020
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
1021
|
+
"--- from: user (agent-model-tester)
|
|
1022
|
+
Reply with exactly: race-switch-first-msg
|
|
1023
|
+
--- from: assistant (TestBot)
|
|
1024
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
1025
|
+
> ok
|
|
1026
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
1027
|
+
--- from: user (agent-model-tester)
|
|
1028
|
+
Reply with exactly: race-switch-follow-up
|
|
1029
|
+
--- from: assistant (TestBot)
|
|
1030
|
+
Switched to **plan** agent for this session (was **test-agent**)
|
|
1031
|
+
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
1032
|
+
The agent will change on the next message.
|
|
1033
|
+
> ok
|
|
1034
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
1035
|
+
`);
|
|
1036
|
+
const secondFooter = [...(await discord.thread(thread.id).getMessages())]
|
|
1037
|
+
.reverse()
|
|
1038
|
+
.find((m) => {
|
|
1039
|
+
return (isFooterMessage({ message: m, botUserId: discord.botUserId }));
|
|
1040
|
+
});
|
|
1041
|
+
expect(secondFooter).toBeDefined();
|
|
1042
|
+
expect(secondFooter.content).toContain(PLAN_AGENT_MODEL);
|
|
1043
|
+
expect(secondFooter.content).not.toContain(AGENT_MODEL);
|
|
1044
|
+
}, 20_000);
|
|
1045
|
+
test('/plan-agent in a channel then an immediate message uses the new agent', async () => {
|
|
1046
|
+
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
1047
|
+
const { id: interactionId } = await discord
|
|
1048
|
+
.channel(TEXT_CHANNEL_ID)
|
|
1049
|
+
.user(TEST_USER_ID)
|
|
1050
|
+
.runSlashCommand({ name: 'plan-agent' });
|
|
1051
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
1052
|
+
content: 'Reply with exactly: race-channel-follow-up',
|
|
1053
|
+
});
|
|
1054
|
+
await discord
|
|
1055
|
+
.channel(TEXT_CHANNEL_ID)
|
|
1056
|
+
.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
1057
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
1058
|
+
timeout: 4_000,
|
|
1059
|
+
predicate: (t) => {
|
|
1060
|
+
return t.name === 'Reply with exactly: race-channel-follow-up';
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
1063
|
+
await waitForFooterMessage({
|
|
1064
|
+
discord,
|
|
1065
|
+
threadId: thread.id,
|
|
1066
|
+
timeout: 4_000,
|
|
1067
|
+
afterMessageIncludes: 'ok',
|
|
1068
|
+
afterAuthorId: discord.botUserId,
|
|
1069
|
+
});
|
|
1070
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
1071
|
+
"--- from: user (agent-model-tester)
|
|
1072
|
+
Reply with exactly: race-channel-follow-up
|
|
1073
|
+
--- from: assistant (TestBot)
|
|
1074
|
+
> *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
1075
|
+
> ok
|
|
1076
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
1077
|
+
`);
|
|
1078
|
+
const footer = [...(await discord.thread(thread.id).getMessages())]
|
|
1079
|
+
.reverse()
|
|
1080
|
+
.find((m) => {
|
|
1081
|
+
return isFooterMessage({ message: m, botUserId: discord.botUserId });
|
|
1082
|
+
});
|
|
1083
|
+
expect(footer).toBeDefined();
|
|
1084
|
+
expect(footer.content).toContain(PLAN_AGENT_MODEL);
|
|
1085
|
+
expect(footer.content).not.toContain(AGENT_MODEL);
|
|
1086
|
+
}, 20_000);
|
|
904
1087
|
test('/plan-agent on the same agent refreshes a stale session model', async () => {
|
|
905
1088
|
await setChannelAgent(TEXT_CHANNEL_ID, 'plan');
|
|
906
1089
|
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
@@ -936,9 +1119,9 @@ describe('agent model resolution', () => {
|
|
|
936
1119
|
"--- from: user (agent-model-tester)
|
|
937
1120
|
Reply with exactly: refresh-agent-model-msg
|
|
938
1121
|
--- from: assistant (TestBot)
|
|
939
|
-
*using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
940
|
-
|
|
941
|
-
*project ⋅ main ⋅
|
|
1122
|
+
> *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
1123
|
+
> ok
|
|
1124
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>
|
|
942
1125
|
Using **plan** agent for this session
|
|
943
1126
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
944
1127
|
The agent will change on the next message."
|
|
@@ -978,9 +1161,9 @@ describe('agent model resolution', () => {
|
|
|
978
1161
|
"--- from: user (agent-model-tester)
|
|
979
1162
|
Reply with exactly: channel-vs-agent-msg
|
|
980
1163
|
--- from: assistant (TestBot)
|
|
981
|
-
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
982
|
-
|
|
983
|
-
*project ⋅ main ⋅
|
|
1164
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
1165
|
+
> ok
|
|
1166
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
984
1167
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
985
1168
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
986
1169
|
The agent will change on the next message."
|
|
@@ -1020,9 +1203,9 @@ describe('agent model resolution', () => {
|
|
|
1020
1203
|
"--- from: user (agent-model-tester)
|
|
1021
1204
|
Reply with exactly: plain-agent-override-msg
|
|
1022
1205
|
--- from: assistant (TestBot)
|
|
1023
|
-
*using deterministic-provider/channel-model-v2*
|
|
1024
|
-
|
|
1025
|
-
*project ⋅ main ⋅
|
|
1206
|
+
> *using deterministic-provider/channel-model-v2*
|
|
1207
|
+
> ok
|
|
1208
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ channel-model-v2* <@200000000000000920>
|
|
1026
1209
|
Switched to **plain** agent for this session
|
|
1027
1210
|
Model: *deterministic-provider/channel-model-v2* (channel override)
|
|
1028
1211
|
This model comes from a channel override. Use /model and press Clear override if you want this agent's model.
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// LEGACY rotation, superseded by @subrouter/opencode. See oauth-rotation-shared.ts.
|
|
2
|
-
// Helpers for
|
|
2
|
+
// Helpers for Anthropic OAuth account and Claude Code client identity.
|
|
3
|
+
const CLAUDE_CODE_USER_AGENT = 'claude-cli/2.1.257 (external, cli)';
|
|
4
|
+
export function applyClaudeCodeRequestIdentity({ headers, accessToken, }) {
|
|
5
|
+
headers.set('authorization', `Bearer ${accessToken}`);
|
|
6
|
+
headers.set('user-agent', process.env.OPENCODE_ANTHROPIC_USER_AGENT || CLAUDE_CODE_USER_AGENT);
|
|
7
|
+
headers.set('x-app', 'cli');
|
|
8
|
+
return headers;
|
|
9
|
+
}
|
|
3
10
|
const identityHintKeys = new Set(['user', 'profile', 'account', 'viewer']);
|
|
4
11
|
const idKeys = ['user_id', 'userId', 'account_id', 'accountId', 'id', 'sub'];
|
|
5
12
|
export function normalizeAnthropicAccountIdentity(identity) {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { appendToastSessionMarker } from "./plugin-logger.js";
|
|
27
27
|
import { createPluginClient } from "./plugin-opencode-client.js";
|
|
28
28
|
import { isPermanentOAuthRefreshFailure, loadAccountStore, rememberAnthropicOAuth, removeAccountByAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, shouldRotateAuth, upsertAccount, withAuthStateLock, } from "./anthropic-auth-state.js";
|
|
29
|
-
import { extractAnthropicAccountIdentity, } from "./anthropic-account-identity.js";
|
|
29
|
+
import { applyClaudeCodeRequestIdentity, extractAnthropicAccountIdentity, } from "./anthropic-account-identity.js";
|
|
30
30
|
// PKCE (Proof Key for Code Exchange) using Web Crypto API.
|
|
31
31
|
// Reference: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/pkce.ts
|
|
32
32
|
function base64urlEncode(bytes) {
|
|
@@ -63,7 +63,6 @@ const CALLBACK_PATH = "/callback";
|
|
|
63
63
|
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
64
64
|
const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
65
65
|
const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
66
|
-
const CLAUDE_CODE_VERSION = "2.1.75";
|
|
67
66
|
const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
68
67
|
const OPENCODE_IDENTITY = "You are OpenCode, the best coding agent on the planet.";
|
|
69
68
|
// Subagent prompts don't contain OPENCODE_IDENTITY; opencode appends this
|
|
@@ -243,13 +242,10 @@ async function fetchAnthropicAccountIdentity(accessToken) {
|
|
|
243
242
|
for (const url of urls) {
|
|
244
243
|
const responseText = await requestText(url, {
|
|
245
244
|
method: "GET",
|
|
246
|
-
headers: {
|
|
247
|
-
Accept: "application/json",
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
`claude-cli/${CLAUDE_CODE_VERSION}`,
|
|
251
|
-
"x-app": "cli",
|
|
252
|
-
},
|
|
245
|
+
headers: Object.fromEntries(applyClaudeCodeRequestIdentity({
|
|
246
|
+
headers: new Headers({ Accept: "application/json" }),
|
|
247
|
+
accessToken,
|
|
248
|
+
}).entries()),
|
|
253
249
|
}).catch(() => {
|
|
254
250
|
return undefined;
|
|
255
251
|
});
|
|
@@ -877,10 +873,10 @@ const AnthropicAuthPlugin = async ({ serverUrl, directory }) => {
|
|
|
877
873
|
requestHeaders.set("accept", "application/json");
|
|
878
874
|
requestHeaders.set("anthropic-beta", mergeBetas(requestHeaders.get("anthropic-beta"), betas));
|
|
879
875
|
requestHeaders.set("anthropic-dangerous-direct-browser-access", "true");
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
876
|
+
applyClaudeCodeRequestIdentity({
|
|
877
|
+
headers: requestHeaders,
|
|
878
|
+
accessToken: auth.access,
|
|
879
|
+
});
|
|
884
880
|
requestHeaders.delete("x-api-key");
|
|
885
881
|
return fetch(input, {
|
|
886
882
|
...(init ?? {}),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Tests Anthropic system prompt rewriting so project instructions survive OpenCode prompt layout changes.
|
|
2
2
|
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { applyClaudeCodeRequestIdentity } from './anthropic-account-identity.js';
|
|
3
4
|
import { replacer } from './anthropic-auth-plugin.js';
|
|
4
5
|
async function transformSystem(systemText) {
|
|
5
6
|
const plugin = await replacer({});
|
|
@@ -12,6 +13,22 @@ async function transformSystem(systemText) {
|
|
|
12
13
|
}, output);
|
|
13
14
|
return output.system.join('\n');
|
|
14
15
|
}
|
|
16
|
+
describe('Anthropic OAuth request identity', () => {
|
|
17
|
+
test('applies the current external Claude Code client headers', () => {
|
|
18
|
+
const headers = applyClaudeCodeRequestIdentity({
|
|
19
|
+
headers: new Headers({ Accept: 'application/json' }),
|
|
20
|
+
accessToken: 'access-token',
|
|
21
|
+
});
|
|
22
|
+
expect(Object.fromEntries(headers.entries())).toMatchInlineSnapshot(`
|
|
23
|
+
{
|
|
24
|
+
"accept": "application/json",
|
|
25
|
+
"authorization": "Bearer access-token",
|
|
26
|
+
"user-agent": "claude-cli/2.1.257 (external, cli)",
|
|
27
|
+
"x-app": "cli",
|
|
28
|
+
}
|
|
29
|
+
`);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
15
32
|
describe('Anthropic system prompt rewriting', () => {
|
|
16
33
|
test('preserves instructions when OpenCode places them before skills', async () => {
|
|
17
34
|
const transformed = await transformSystem(`You are OpenCode, the best coding agent on the planet.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Adds description and hasSideEffect to the built-in bash tool schema.
|
|
2
|
+
// Uses tool.definition so OpenCode keeps the real bash execute.
|
|
3
|
+
// Assign jsonSchema only. Leave parameters as the Effect Schema used to decode
|
|
4
|
+
// args before execute. Extra fields stay in the model-facing schema and in
|
|
5
|
+
// Discord input; they never reach the shell.
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
export const bashParameters = z.object({
|
|
8
|
+
command: z.string().describe('The command to execute'),
|
|
9
|
+
description: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe('Short 5-10 word summary shown in Discord when the command is longer than 50 characters'),
|
|
12
|
+
hasSideEffect: z
|
|
13
|
+
.boolean()
|
|
14
|
+
.describe('True if the command writes files, modifies state, installs packages, or triggers external effects'),
|
|
15
|
+
workdir: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Working directory. Defaults to the current directory.'),
|
|
19
|
+
timeout: z.number().int().optional().describe('Optional timeout in milliseconds'),
|
|
20
|
+
});
|
|
21
|
+
export function extendBashToolDefinition(output) {
|
|
22
|
+
output.jsonSchema = z.toJSONSchema(bashParameters);
|
|
23
|
+
}
|
|
24
|
+
export const bashToolSchemaPlugin = async () => {
|
|
25
|
+
return {
|
|
26
|
+
'tool.definition': async (input, output) => {
|
|
27
|
+
if (input.toolID !== 'bash')
|
|
28
|
+
return;
|
|
29
|
+
extendBashToolDefinition(output);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|