kimaki 0.8.0 → 0.9.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 +1 -1
- package/dist/anthropic-auth-plugin.js +16 -17
- package/dist/anthropic-auth-plugin.test.js +59 -98
- package/dist/anthropic-auth-state.js +20 -173
- package/dist/channel-management.js +5 -6
- package/dist/cli-commands/bot.js +228 -0
- package/dist/cli-commands/core.js +1909 -0
- package/dist/cli-commands/maintenance.js +132 -0
- package/dist/cli-commands/misc.js +76 -0
- package/dist/cli-commands/project.js +386 -0
- package/dist/cli-commands/send.js +495 -0
- package/dist/cli-commands/session.js +482 -0
- package/dist/cli-commands/task.js +166 -0
- package/dist/cli-commands/user.js +145 -0
- package/dist/cli-parsing.test.js +12 -8
- package/dist/cli-runner.js +1361 -0
- package/dist/cli-send-thread.e2e.test.js +124 -9
- package/dist/cli.js +39 -3222
- package/dist/commands/agent.js +1 -2
- package/dist/commands/compact.js +3 -3
- package/dist/commands/context-usage.js +1 -1
- package/dist/commands/login.js +10 -3
- package/dist/commands/mention-mode.js +5 -6
- package/dist/commands/merge-worktree.js +5 -1
- package/dist/commands/model-variant.js +1 -2
- package/dist/commands/model.js +1 -2
- package/dist/commands/multioauth.js +256 -0
- package/dist/commands/new-worktree.js +2 -3
- package/dist/commands/resume.js +2 -3
- package/dist/commands/session.js +3 -4
- package/dist/commands/unset-model.js +1 -2
- package/dist/commands/worktree-settings.js +6 -7
- package/dist/db.js +6 -0
- package/dist/discord-bot.js +40 -9
- package/dist/errors.js +5 -0
- package/dist/event-stream-real-capture.e2e.test.js +1 -1
- package/dist/external-opencode-sync.js +1 -1
- package/dist/forum-sync/discord-operations.js +5 -2
- package/dist/gateway-proxy-reconnect.e2e.test.js +2 -2
- package/dist/gateway-proxy.e2e.test.js +1 -1
- package/dist/genai-worker-wrapper.js +1 -1
- package/dist/generated/internal/class.js +4 -4
- package/dist/generated/internal/prismaNamespace.js +1 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +1 -0
- package/dist/hrana-server.js +3 -2
- package/dist/hrana-server.test.js +1 -1
- package/dist/html-components.js +2 -2
- package/dist/kimaki-digital-twin.e2e.test.js +1 -1
- package/dist/markdown.js +1 -2
- package/dist/oauth-rotation-shared.js +216 -0
- package/dist/openai-auth-plugin.js +123 -0
- package/dist/openai-auth-state.js +216 -0
- package/dist/opencode.js +1 -1
- package/dist/queue-advanced-e2e-setup.js +1 -1
- package/dist/runtime-lifecycle.e2e.test.js +1 -1
- package/dist/session-handler/event-stream-state.js +4 -4
- package/dist/session-handler/thread-runtime-state.js +9 -0
- package/dist/session-handler/thread-session-runtime.js +93 -12
- package/dist/session-title-rename.test.js +39 -1
- package/dist/startup-time.e2e.test.js +2 -2
- package/dist/system-message.js +10 -8
- package/dist/thread-message-queue.e2e.test.js +1 -1
- package/dist/undo-redo.e2e.test.js +1 -1
- package/dist/unnest-code-blocks.js +1 -1
- package/dist/upgrade.js +1 -1
- package/dist/voice-message.e2e.test.js +1 -1
- package/dist/voice.js +3 -1
- package/dist/worktree-lifecycle.e2e.test.js +1 -1
- package/dist/worktrees.js +13 -71
- package/dist/worktrees.test.js +32 -1
- package/package.json +5 -5
- package/schema.prisma +5 -4
- package/src/agent-model.e2e.test.ts +1 -1
- package/src/ai-tool-to-genai.ts +2 -2
- package/src/anthropic-auth-plugin.test.ts +81 -0
- package/src/anthropic-auth-plugin.ts +18 -23
- package/src/anthropic-auth-state.ts +45 -221
- package/src/channel-management.ts +6 -7
- package/src/cli-commands/bot.ts +340 -0
- package/src/cli-commands/maintenance.ts +191 -0
- package/src/cli-commands/misc.ts +117 -0
- package/src/cli-commands/project.ts +508 -0
- package/src/cli-commands/send.ts +676 -0
- package/src/cli-commands/session.ts +658 -0
- package/src/cli-commands/task.ts +217 -0
- package/src/cli-commands/user.ts +196 -0
- package/src/cli-parsing.test.ts +12 -8
- package/src/cli-runner.ts +1868 -0
- package/src/cli-send-thread.e2e.test.ts +154 -8
- package/src/cli.ts +200 -4596
- package/src/commands/agent.ts +1 -2
- package/src/commands/compact.ts +3 -3
- package/src/commands/context-usage.ts +1 -1
- package/src/commands/login.ts +10 -3
- package/src/commands/mention-mode.ts +5 -6
- package/src/commands/merge-worktree.ts +13 -2
- package/src/commands/model-variant.ts +1 -2
- package/src/commands/model.ts +1 -2
- package/src/commands/multioauth.ts +314 -0
- package/src/commands/new-worktree.ts +2 -4
- package/src/commands/remove-project.ts +2 -2
- package/src/commands/resume.ts +2 -4
- package/src/commands/session.ts +3 -5
- package/src/commands/unset-model.ts +1 -2
- package/src/commands/user-command.ts +2 -2
- package/src/commands/verbosity.ts +1 -1
- package/src/commands/worktree-settings.ts +6 -7
- package/src/database.ts +1 -1
- package/src/db.ts +8 -0
- package/src/discord-bot.ts +40 -10
- package/src/discord-utils.ts +3 -3
- package/src/errors.ts +7 -0
- package/src/event-stream-real-capture.e2e.test.ts +1 -1
- package/src/external-opencode-sync.ts +1 -1
- package/src/forum-sync/discord-operations.ts +6 -2
- package/src/gateway-proxy-reconnect.e2e.test.ts +2 -2
- package/src/gateway-proxy.e2e.test.ts +1 -1
- package/src/genai-worker-wrapper.ts +1 -1
- package/src/genai-worker.ts +1 -1
- package/src/genai.ts +2 -5
- package/src/generated/commonInputTypes.ts +73 -73
- package/src/generated/internal/class.ts +4 -4
- package/src/generated/internal/prismaNamespace.ts +1 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +1 -0
- package/src/generated/models/bot_tokens.ts +0 -4
- package/src/generated/models/thread_sessions.ts +53 -1
- package/src/hrana-server.test.ts +1 -1
- package/src/hrana-server.ts +4 -3
- package/src/html-components.ts +4 -4
- package/src/kimaki-digital-twin.e2e.test.ts +1 -1
- package/src/markdown.ts +1 -2
- package/src/oauth-rotation-shared.ts +295 -0
- package/src/openai-auth-plugin.ts +155 -0
- package/src/openai-auth-state.ts +277 -0
- package/src/openai-realtime.ts +2 -2
- package/src/opencode-interrupt-plugin.ts +2 -2
- package/src/opencode.ts +1 -1
- package/src/queue-advanced-e2e-setup.ts +1 -1
- package/src/runtime-lifecycle.e2e.test.ts +1 -1
- package/src/schema.sql +1 -0
- package/src/session-handler/event-stream-state.ts +5 -5
- package/src/session-handler/thread-runtime-state.ts +11 -0
- package/src/session-handler/thread-session-runtime.ts +124 -12
- package/src/session-title-rename.test.ts +51 -1
- package/src/startup-time.e2e.test.ts +2 -2
- package/src/system-message.ts +11 -8
- package/src/thread-message-queue.e2e.test.ts +1 -1
- package/src/tools.ts +1 -1
- package/src/undo-redo.e2e.test.ts +1 -1
- package/src/unnest-code-blocks.ts +1 -1
- package/src/upgrade.ts +2 -2
- package/src/voice-handler.ts +1 -1
- package/src/voice-message.e2e.test.ts +1 -1
- package/src/voice.ts +7 -5
- package/src/worktree-lifecycle.e2e.test.ts +1 -1
- package/src/worktrees.test.ts +40 -0
- package/src/worktrees.ts +22 -84
- package/src/xml.ts +2 -2
|
@@ -64,10 +64,10 @@ const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
64
64
|
const CLAUDE_CODE_VERSION = "2.1.75";
|
|
65
65
|
const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
66
66
|
const OPENCODE_IDENTITY = "You are OpenCode, the best coding agent on the planet.";
|
|
67
|
-
const ANTHROPIC_PROMPT_MARKER = "Skills provide specialized instructions";
|
|
68
67
|
// Subagent prompts don't contain OPENCODE_IDENTITY; opencode appends this
|
|
69
68
|
// line + an <env> block instead. We strip from here to </env> inclusive.
|
|
70
69
|
const SUBAGENT_MODEL_IDENTITY = "You are powered by the model named";
|
|
70
|
+
const ENV_CLOSE_TAG = "</env>";
|
|
71
71
|
const CLAUDE_CODE_BETA = "claude-code-20250219";
|
|
72
72
|
const OAUTH_BETA = "oauth-2025-04-20";
|
|
73
73
|
const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
|
|
@@ -465,42 +465,41 @@ function toClaudeCodeToolName(name) {
|
|
|
465
465
|
return OPENCODE_TO_CLAUDE_CODE_TOOL_NAME[name.toLowerCase()] ?? name;
|
|
466
466
|
}
|
|
467
467
|
/**
|
|
468
|
-
* Strips the OpenCode identity
|
|
469
|
-
*
|
|
470
|
-
* re-injects essential environment context as a small XML tag.
|
|
468
|
+
* Strips the OpenCode identity and its adjacent <env> block, then re-injects
|
|
469
|
+
* essential environment context as a small XML tag.
|
|
471
470
|
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
471
|
+
* OpenCode can place project instructions before or after skills depending on
|
|
472
|
+
* version. Keep the rewrite scoped to the env block so configured instruction
|
|
473
|
+
* files remain visible to Anthropic.
|
|
475
474
|
*
|
|
476
475
|
* Original OpenCode Anthropic prompt structure (for reference):
|
|
477
476
|
* "You are OpenCode, the best coding agent on the planet."
|
|
478
477
|
* + environment block (cwd, OS, shell, date, etc.)
|
|
479
|
-
* +
|
|
478
|
+
* + instructions and/or skills
|
|
480
479
|
*/
|
|
481
480
|
function sanitizeAnthropicSystemText(text, onError) {
|
|
482
481
|
const startIdx = text.indexOf(OPENCODE_IDENTITY);
|
|
483
482
|
if (startIdx !== -1) {
|
|
484
|
-
// Main session path: strip from OpenCode identity
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
onError?.("sanitizeAnthropicSystemText: could not find Anthropic prompt marker after OpenCode identity");
|
|
483
|
+
// Main session path: strip from OpenCode identity through its env block.
|
|
484
|
+
const envCloseIdx = text.indexOf(ENV_CLOSE_TAG, startIdx);
|
|
485
|
+
if (envCloseIdx === -1) {
|
|
486
|
+
onError?.("sanitizeAnthropicSystemText: could not find </env> after OpenCode identity");
|
|
489
487
|
return text;
|
|
490
488
|
}
|
|
491
|
-
|
|
489
|
+
const endIdx = envCloseIdx + ENV_CLOSE_TAG.length;
|
|
490
|
+
const afterEnd = text[endIdx] === "\n" ? endIdx + 1 : endIdx;
|
|
491
|
+
return replaceBlockWithCompactEnv(text, startIdx, afterEnd);
|
|
492
492
|
}
|
|
493
493
|
// Subagent path: opencode appends "You are powered by the model named ..."
|
|
494
494
|
// followed by an <env> block. Strip from that line through </env>.
|
|
495
495
|
const subagentIdx = text.indexOf(SUBAGENT_MODEL_IDENTITY);
|
|
496
496
|
if (subagentIdx !== -1) {
|
|
497
|
-
const
|
|
498
|
-
const envCloseIdx = text.indexOf(envCloseTag, subagentIdx);
|
|
497
|
+
const envCloseIdx = text.indexOf(ENV_CLOSE_TAG, subagentIdx);
|
|
499
498
|
if (envCloseIdx === -1) {
|
|
500
499
|
onError?.("sanitizeAnthropicSystemText: could not find </env> after subagent model identity");
|
|
501
500
|
return text;
|
|
502
501
|
}
|
|
503
|
-
const endIdx = envCloseIdx +
|
|
502
|
+
const endIdx = envCloseIdx + ENV_CLOSE_TAG.length;
|
|
504
503
|
// Skip trailing newline so the join is clean
|
|
505
504
|
const afterEnd = text[endIdx] === "\n" ? endIdx + 1 : endIdx;
|
|
506
505
|
return replaceBlockWithCompactEnv(text, subagentIdx, afterEnd);
|
|
@@ -1,109 +1,70 @@
|
|
|
1
|
-
// Tests Anthropic
|
|
1
|
+
// Tests Anthropic system prompt rewriting so project instructions survive OpenCode prompt layout changes.
|
|
2
2
|
import { describe, expect, test } from 'vitest';
|
|
3
|
-
import { replacer
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { replacer } from './anthropic-auth-plugin.js';
|
|
4
|
+
async function transformSystem(systemText) {
|
|
5
|
+
const plugin = await replacer({});
|
|
6
|
+
const transform = plugin['experimental.chat.system.transform'];
|
|
7
|
+
if (!transform)
|
|
8
|
+
throw new Error('missing system transform hook');
|
|
9
|
+
const output = { system: [systemText] };
|
|
10
|
+
await transform({
|
|
11
|
+
model: { providerID: 'anthropic' },
|
|
12
|
+
}, output);
|
|
13
|
+
return output.system.join('\n');
|
|
9
14
|
}
|
|
10
|
-
describe('
|
|
11
|
-
test('
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
"text": "You are Claude Code, Anthropic's official CLI for Claude.",
|
|
25
|
-
"type": "text",
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"text": "
|
|
15
|
+
describe('Anthropic system prompt rewriting', () => {
|
|
16
|
+
test('preserves instructions when OpenCode places them before skills', async () => {
|
|
17
|
+
const transformed = await transformSystem(`You are OpenCode, the best coding agent on the planet.
|
|
18
|
+
<env>
|
|
19
|
+
Working directory: /repo/site
|
|
20
|
+
Platform: darwin
|
|
21
|
+
</env>
|
|
22
|
+
Instructions from: /repo/site/SOUL.md
|
|
23
|
+
I am Extra Chill Bot.
|
|
24
|
+
Skills provide specialized instructions and workflows.
|
|
25
|
+
Use skills wisely.`);
|
|
26
|
+
expect(transformed).toMatchInlineSnapshot(`
|
|
27
|
+
"
|
|
29
28
|
<environment>
|
|
30
|
-
<cwd>/
|
|
29
|
+
<cwd>/repo/site</cwd>
|
|
31
30
|
</environment>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"name": "Read",
|
|
39
|
-
"type": "tool",
|
|
40
|
-
},
|
|
41
|
-
"tools": [
|
|
42
|
-
{
|
|
43
|
-
"name": "Read",
|
|
44
|
-
},
|
|
45
|
-
],
|
|
46
|
-
}
|
|
31
|
+
Read, write, and edit files under /repo/site.
|
|
32
|
+
|
|
33
|
+
Instructions from: /repo/site/SOUL.md
|
|
34
|
+
I am Extra Chill Bot.
|
|
35
|
+
Skills provide specialized instructions and workflows.
|
|
36
|
+
Use skills wisely."
|
|
47
37
|
`);
|
|
48
38
|
});
|
|
49
|
-
test('
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
],
|
|
62
|
-
}));
|
|
63
|
-
const payload = parseRewrittenBody(rewritten.body);
|
|
64
|
-
expect(payload.system).toMatchInlineSnapshot(`
|
|
65
|
-
[
|
|
66
|
-
{
|
|
67
|
-
"text": "You are Claude Code, Anthropic's official CLI for Claude.",
|
|
68
|
-
"type": "text",
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"text": "<environment>
|
|
72
|
-
<cwd>/repo</cwd>
|
|
73
|
-
</environment>
|
|
74
|
-
Skills provide specialized instructions",
|
|
75
|
-
"type": "text",
|
|
76
|
-
},
|
|
77
|
-
]
|
|
78
|
-
`);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
describe('replacer', () => {
|
|
82
|
-
test('sanitizes system text only for anthropic provider metadata', async () => {
|
|
83
|
-
const plugin = await replacer({});
|
|
84
|
-
const transform = plugin['experimental.chat.system.transform'];
|
|
85
|
-
if (!transform) {
|
|
86
|
-
throw new Error('Expected experimental.chat.system.transform hook');
|
|
87
|
-
}
|
|
88
|
-
const output = {
|
|
89
|
-
system: [
|
|
90
|
-
"You are OpenCode, the best coding agent on the planet.\nOS: macOS\nSkills provide specialized instructions\nUse opencode tools carefully.",
|
|
91
|
-
],
|
|
92
|
-
};
|
|
93
|
-
await transform({
|
|
94
|
-
model: {
|
|
95
|
-
providerID: 'anthropic',
|
|
96
|
-
},
|
|
97
|
-
}, output);
|
|
98
|
-
expect(output.system).toMatchInlineSnapshot(`
|
|
99
|
-
[
|
|
100
|
-
"
|
|
39
|
+
test('preserves instructions when OpenCode places skills before them', async () => {
|
|
40
|
+
const transformed = await transformSystem(`You are OpenCode, the best coding agent on the planet.
|
|
41
|
+
<env>
|
|
42
|
+
Working directory: /repo/site
|
|
43
|
+
Platform: darwin
|
|
44
|
+
</env>
|
|
45
|
+
Skills provide specialized instructions and workflows.
|
|
46
|
+
Use skills wisely.
|
|
47
|
+
Instructions from: /repo/site/SOUL.md
|
|
48
|
+
I am Extra Chill Bot.`);
|
|
49
|
+
expect(transformed).toMatchInlineSnapshot(`
|
|
50
|
+
"
|
|
101
51
|
<environment>
|
|
102
|
-
<cwd>/
|
|
52
|
+
<cwd>/repo/site</cwd>
|
|
103
53
|
</environment>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
54
|
+
Read, write, and edit files under /repo/site.
|
|
55
|
+
|
|
56
|
+
Skills provide specialized instructions and workflows.
|
|
57
|
+
Use skills wisely.
|
|
58
|
+
Instructions from: /repo/site/SOUL.md
|
|
59
|
+
I am Extra Chill Bot."
|
|
107
60
|
`);
|
|
108
61
|
});
|
|
62
|
+
test('leaves text unchanged when the OpenCode env block is incomplete', async () => {
|
|
63
|
+
const prompt = `You are OpenCode, the best coding agent on the planet.
|
|
64
|
+
<env>
|
|
65
|
+
Working directory: /repo/site
|
|
66
|
+
Instructions from: /repo/site/SOUL.md
|
|
67
|
+
I am Extra Chill Bot.`;
|
|
68
|
+
await expect(transformSystem(prompt)).resolves.toBe(prompt);
|
|
69
|
+
});
|
|
109
70
|
});
|
|
@@ -1,95 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic OAuth account store and rotation.
|
|
3
|
+
* Uses shared utilities from oauth-rotation-shared.ts for file locking,
|
|
4
|
+
* store I/O, and account management. Anthropic-specific: store file path,
|
|
5
|
+
* identity normalization via AnthropicAccountIdentity.
|
|
6
|
+
*/
|
|
2
7
|
import { homedir } from 'node:os';
|
|
3
8
|
import path from 'node:path';
|
|
4
9
|
import { normalizeAnthropicAccountIdentity, } from './anthropic-account-identity.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
return fallback;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
async function writeJson(filePath, value) {
|
|
16
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
17
|
-
await fs.writeFile(filePath, JSON.stringify(value, null, 2), 'utf8');
|
|
18
|
-
await fs.chmod(filePath, 0o600);
|
|
19
|
-
}
|
|
20
|
-
function getErrorCode(error) {
|
|
21
|
-
if (!(error instanceof Error))
|
|
22
|
-
return undefined;
|
|
23
|
-
return error.code;
|
|
24
|
-
}
|
|
25
|
-
async function sleep(ms) {
|
|
26
|
-
await new Promise((resolve) => {
|
|
27
|
-
setTimeout(resolve, ms);
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
export function authFilePath() {
|
|
31
|
-
if (process.env.XDG_DATA_HOME) {
|
|
32
|
-
return path.join(process.env.XDG_DATA_HOME, 'opencode', 'auth.json');
|
|
33
|
-
}
|
|
34
|
-
return path.join(homedir(), '.local', 'share', 'opencode', 'auth.json');
|
|
35
|
-
}
|
|
10
|
+
import { accountLabel, authFilePath, findCurrentAccountIndex, isOAuthStored, normalizeAccountStore, readJson, upsertAccount as sharedUpsertAccount, withAuthStateLock, writeJson, shouldRotateAuth, } from './oauth-rotation-shared.js';
|
|
11
|
+
export { accountLabel, authFilePath, withAuthStateLock, shouldRotateAuth };
|
|
12
|
+
// --- Store file path ---
|
|
36
13
|
export function accountsFilePath() {
|
|
37
14
|
if (process.env.XDG_DATA_HOME) {
|
|
38
15
|
return path.join(process.env.XDG_DATA_HOME, 'opencode', 'anthropic-oauth-accounts.json');
|
|
39
16
|
}
|
|
40
17
|
return path.join(homedir(), '.local', 'share', 'opencode', 'anthropic-oauth-accounts.json');
|
|
41
18
|
}
|
|
42
|
-
|
|
43
|
-
const file = authFilePath();
|
|
44
|
-
const lockDir = `${file}.lock`;
|
|
45
|
-
const deadline = Date.now() + AUTH_LOCK_STALE_MS;
|
|
46
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
47
|
-
while (true) {
|
|
48
|
-
try {
|
|
49
|
-
await fs.mkdir(lockDir);
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
const code = getErrorCode(error);
|
|
54
|
-
if (code !== 'EEXIST') {
|
|
55
|
-
throw error;
|
|
56
|
-
}
|
|
57
|
-
const stats = await fs.stat(lockDir).catch(() => {
|
|
58
|
-
return null;
|
|
59
|
-
});
|
|
60
|
-
if (stats && Date.now() - stats.mtimeMs > AUTH_LOCK_STALE_MS) {
|
|
61
|
-
await fs.rm(lockDir, { force: true, recursive: true }).catch(() => { });
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
if (Date.now() >= deadline) {
|
|
65
|
-
throw new Error(`Timed out waiting for auth lock: ${lockDir}`);
|
|
66
|
-
}
|
|
67
|
-
await sleep(AUTH_LOCK_RETRY_MS);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
try {
|
|
71
|
-
return await fn();
|
|
72
|
-
}
|
|
73
|
-
finally {
|
|
74
|
-
await fs.rm(lockDir, { force: true, recursive: true }).catch(() => { });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
export function normalizeAccountStore(input) {
|
|
78
|
-
const accounts = Array.isArray(input?.accounts)
|
|
79
|
-
? input.accounts.filter((account) => !!account &&
|
|
80
|
-
account.type === 'oauth' &&
|
|
81
|
-
typeof account.refresh === 'string' &&
|
|
82
|
-
typeof account.access === 'string' &&
|
|
83
|
-
typeof account.expires === 'number' &&
|
|
84
|
-
(typeof account.email === 'undefined' || typeof account.email === 'string') &&
|
|
85
|
-
(typeof account.accountId === 'undefined' || typeof account.accountId === 'string') &&
|
|
86
|
-
typeof account.addedAt === 'number' &&
|
|
87
|
-
typeof account.lastUsed === 'number')
|
|
88
|
-
: [];
|
|
89
|
-
const rawIndex = typeof input?.activeIndex === 'number' ? Math.floor(input.activeIndex) : 0;
|
|
90
|
-
const activeIndex = accounts.length === 0 ? 0 : ((rawIndex % accounts.length) + accounts.length) % accounts.length;
|
|
91
|
-
return { version: 1, activeIndex, accounts };
|
|
92
|
-
}
|
|
19
|
+
// --- Store I/O ---
|
|
93
20
|
export async function loadAccountStore() {
|
|
94
21
|
const raw = await readJson(accountsFilePath(), null);
|
|
95
22
|
return normalizeAccountStore(raw);
|
|
@@ -97,77 +24,16 @@ export async function loadAccountStore() {
|
|
|
97
24
|
export async function saveAccountStore(store) {
|
|
98
25
|
await writeJson(accountsFilePath(), normalizeAccountStore(store));
|
|
99
26
|
}
|
|
100
|
-
|
|
101
|
-
export function accountLabel(account, index) {
|
|
102
|
-
const accountWithIdentity = account;
|
|
103
|
-
const identity = accountWithIdentity.email || accountWithIdentity.accountId;
|
|
104
|
-
const r = account.refresh;
|
|
105
|
-
const short = r.length > 12 ? `${r.slice(0, 8)}...${r.slice(-4)}` : r;
|
|
106
|
-
if (identity) {
|
|
107
|
-
return index !== undefined ? `#${index + 1} (${identity})` : identity;
|
|
108
|
-
}
|
|
109
|
-
return index !== undefined ? `#${index + 1} (${short})` : short;
|
|
110
|
-
}
|
|
111
|
-
function findCurrentAccountIndex(store, auth) {
|
|
112
|
-
if (!store.accounts.length)
|
|
113
|
-
return 0;
|
|
114
|
-
const byRefresh = store.accounts.findIndex((account) => {
|
|
115
|
-
return account.refresh === auth.refresh;
|
|
116
|
-
});
|
|
117
|
-
if (byRefresh >= 0)
|
|
118
|
-
return byRefresh;
|
|
119
|
-
const byAccess = store.accounts.findIndex((account) => {
|
|
120
|
-
return account.access === auth.access;
|
|
121
|
-
});
|
|
122
|
-
if (byAccess >= 0)
|
|
123
|
-
return byAccess;
|
|
124
|
-
return store.activeIndex;
|
|
125
|
-
}
|
|
27
|
+
// --- Upsert with Anthropic identity normalization ---
|
|
126
28
|
export function upsertAccount(store, auth, now = Date.now()) {
|
|
127
29
|
const authWithIdentity = auth;
|
|
128
30
|
const identity = normalizeAnthropicAccountIdentity({
|
|
129
31
|
email: authWithIdentity.email,
|
|
130
32
|
accountId: authWithIdentity.accountId,
|
|
131
33
|
});
|
|
132
|
-
|
|
133
|
-
if (account.refresh === auth.refresh || account.access === auth.access) {
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
if (identity?.accountId && account.accountId === identity.accountId) {
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
if (identity?.email && account.email === identity.email) {
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
return false;
|
|
143
|
-
});
|
|
144
|
-
const nextAccount = {
|
|
145
|
-
type: 'oauth',
|
|
146
|
-
refresh: auth.refresh,
|
|
147
|
-
access: auth.access,
|
|
148
|
-
expires: auth.expires,
|
|
149
|
-
...identity,
|
|
150
|
-
addedAt: now,
|
|
151
|
-
lastUsed: now,
|
|
152
|
-
};
|
|
153
|
-
if (index < 0) {
|
|
154
|
-
store.accounts.push(nextAccount);
|
|
155
|
-
store.activeIndex = store.accounts.length - 1;
|
|
156
|
-
return store.activeIndex;
|
|
157
|
-
}
|
|
158
|
-
const existing = store.accounts[index];
|
|
159
|
-
if (!existing)
|
|
160
|
-
return index;
|
|
161
|
-
store.accounts[index] = {
|
|
162
|
-
...existing,
|
|
163
|
-
...nextAccount,
|
|
164
|
-
addedAt: existing.addedAt,
|
|
165
|
-
email: nextAccount.email || existing.email,
|
|
166
|
-
accountId: nextAccount.accountId || existing.accountId,
|
|
167
|
-
};
|
|
168
|
-
store.activeIndex = index;
|
|
169
|
-
return index;
|
|
34
|
+
return sharedUpsertAccount(store, { ...auth, ...identity }, now);
|
|
170
35
|
}
|
|
36
|
+
// --- Remember new login ---
|
|
171
37
|
export async function rememberAnthropicOAuth(auth, identity) {
|
|
172
38
|
await withAuthStateLock(async () => {
|
|
173
39
|
const store = await loadAccountStore();
|
|
@@ -175,6 +41,7 @@ export async function rememberAnthropicOAuth(auth, identity) {
|
|
|
175
41
|
await saveAccountStore(store);
|
|
176
42
|
});
|
|
177
43
|
}
|
|
44
|
+
// --- Auth file write + SDK sync ---
|
|
178
45
|
async function writeAnthropicAuthFile(auth) {
|
|
179
46
|
const file = authFilePath();
|
|
180
47
|
const data = await readJson(file, {});
|
|
@@ -186,16 +53,11 @@ async function writeAnthropicAuthFile(auth) {
|
|
|
186
53
|
}
|
|
187
54
|
await writeJson(file, data);
|
|
188
55
|
}
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
const record = value;
|
|
194
|
-
return (record.type === 'oauth' &&
|
|
195
|
-
typeof record.refresh === 'string' &&
|
|
196
|
-
typeof record.access === 'string' &&
|
|
197
|
-
typeof record.expires === 'number');
|
|
56
|
+
export async function setAnthropicAuth(auth, client) {
|
|
57
|
+
await writeAnthropicAuthFile(auth);
|
|
58
|
+
await client.auth.set({ path: { id: 'anthropic' }, body: auth });
|
|
198
59
|
}
|
|
60
|
+
// --- Current account ---
|
|
199
61
|
export async function getCurrentAnthropicAccount() {
|
|
200
62
|
const authJson = await readJson(authFilePath(), {});
|
|
201
63
|
const auth = authJson.anthropic;
|
|
@@ -217,10 +79,7 @@ export async function getCurrentAnthropicAccount() {
|
|
|
217
79
|
index,
|
|
218
80
|
};
|
|
219
81
|
}
|
|
220
|
-
|
|
221
|
-
await writeAnthropicAuthFile(auth);
|
|
222
|
-
await client.auth.set({ path: { id: 'anthropic' }, body: auth });
|
|
223
|
-
}
|
|
82
|
+
// --- Rotation ---
|
|
224
83
|
export async function rotateAnthropicAccount(auth, client) {
|
|
225
84
|
return withAuthStateLock(async () => {
|
|
226
85
|
const store = await loadAccountStore();
|
|
@@ -254,6 +113,7 @@ export async function rotateAnthropicAccount(auth, client) {
|
|
|
254
113
|
};
|
|
255
114
|
});
|
|
256
115
|
}
|
|
116
|
+
// --- Remove account ---
|
|
257
117
|
export async function removeAccount(index) {
|
|
258
118
|
return withAuthStateLock(async () => {
|
|
259
119
|
const store = await loadAccountStore();
|
|
@@ -288,16 +148,3 @@ export async function removeAccount(index) {
|
|
|
288
148
|
return { store, active: nextAuth };
|
|
289
149
|
});
|
|
290
150
|
}
|
|
291
|
-
export function shouldRotateAuth(status, bodyText) {
|
|
292
|
-
const haystack = bodyText.toLowerCase();
|
|
293
|
-
if (status === 429)
|
|
294
|
-
return true;
|
|
295
|
-
if (status === 401 || status === 403)
|
|
296
|
-
return true;
|
|
297
|
-
return (haystack.includes('rate_limit') ||
|
|
298
|
-
haystack.includes('rate limit') ||
|
|
299
|
-
haystack.includes('invalid api key') ||
|
|
300
|
-
haystack.includes('authentication_error') ||
|
|
301
|
-
haystack.includes('permission_error') ||
|
|
302
|
-
haystack.includes('oauth'));
|
|
303
|
-
}
|
|
@@ -86,15 +86,14 @@ export async function createProjectChannels({ guild, projectDirectory, botName,
|
|
|
86
86
|
}
|
|
87
87
|
export async function getChannelsWithDescriptions(guild) {
|
|
88
88
|
const channels = [];
|
|
89
|
-
const textChannels = guild.channels.cache.filter((channel) => channel.
|
|
89
|
+
const textChannels = guild.channels.cache.filter((channel) => channel.type === ChannelType.GuildText);
|
|
90
90
|
for (const channel of textChannels.values()) {
|
|
91
|
-
const
|
|
92
|
-
const description = textChannel.topic || null;
|
|
91
|
+
const description = channel.topic || null;
|
|
93
92
|
// Get channel config from database instead of parsing XML from topic
|
|
94
|
-
const channelConfig = await getChannelDirectory(
|
|
93
|
+
const channelConfig = await getChannelDirectory(channel.id);
|
|
95
94
|
channels.push({
|
|
96
|
-
id:
|
|
97
|
-
name:
|
|
95
|
+
id: channel.id,
|
|
96
|
+
name: channel.name,
|
|
98
97
|
description,
|
|
99
98
|
kimakiDirectory: channelConfig?.directory,
|
|
100
99
|
});
|