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.
Files changed (158) hide show
  1. package/dist/agent-model.e2e.test.js +1 -1
  2. package/dist/anthropic-auth-plugin.js +16 -17
  3. package/dist/anthropic-auth-plugin.test.js +59 -98
  4. package/dist/anthropic-auth-state.js +20 -173
  5. package/dist/channel-management.js +5 -6
  6. package/dist/cli-commands/bot.js +228 -0
  7. package/dist/cli-commands/core.js +1909 -0
  8. package/dist/cli-commands/maintenance.js +132 -0
  9. package/dist/cli-commands/misc.js +76 -0
  10. package/dist/cli-commands/project.js +386 -0
  11. package/dist/cli-commands/send.js +495 -0
  12. package/dist/cli-commands/session.js +482 -0
  13. package/dist/cli-commands/task.js +166 -0
  14. package/dist/cli-commands/user.js +145 -0
  15. package/dist/cli-parsing.test.js +12 -8
  16. package/dist/cli-runner.js +1361 -0
  17. package/dist/cli-send-thread.e2e.test.js +124 -9
  18. package/dist/cli.js +39 -3222
  19. package/dist/commands/agent.js +1 -2
  20. package/dist/commands/compact.js +3 -3
  21. package/dist/commands/context-usage.js +1 -1
  22. package/dist/commands/login.js +10 -3
  23. package/dist/commands/mention-mode.js +5 -6
  24. package/dist/commands/merge-worktree.js +5 -1
  25. package/dist/commands/model-variant.js +1 -2
  26. package/dist/commands/model.js +1 -2
  27. package/dist/commands/multioauth.js +256 -0
  28. package/dist/commands/new-worktree.js +2 -3
  29. package/dist/commands/resume.js +2 -3
  30. package/dist/commands/session.js +3 -4
  31. package/dist/commands/unset-model.js +1 -2
  32. package/dist/commands/worktree-settings.js +6 -7
  33. package/dist/db.js +6 -0
  34. package/dist/discord-bot.js +40 -9
  35. package/dist/errors.js +5 -0
  36. package/dist/event-stream-real-capture.e2e.test.js +1 -1
  37. package/dist/external-opencode-sync.js +1 -1
  38. package/dist/forum-sync/discord-operations.js +5 -2
  39. package/dist/gateway-proxy-reconnect.e2e.test.js +2 -2
  40. package/dist/gateway-proxy.e2e.test.js +1 -1
  41. package/dist/genai-worker-wrapper.js +1 -1
  42. package/dist/generated/internal/class.js +4 -4
  43. package/dist/generated/internal/prismaNamespace.js +1 -0
  44. package/dist/generated/internal/prismaNamespaceBrowser.js +1 -0
  45. package/dist/hrana-server.js +3 -2
  46. package/dist/hrana-server.test.js +1 -1
  47. package/dist/html-components.js +2 -2
  48. package/dist/kimaki-digital-twin.e2e.test.js +1 -1
  49. package/dist/markdown.js +1 -2
  50. package/dist/oauth-rotation-shared.js +216 -0
  51. package/dist/openai-auth-plugin.js +123 -0
  52. package/dist/openai-auth-state.js +216 -0
  53. package/dist/opencode.js +1 -1
  54. package/dist/queue-advanced-e2e-setup.js +1 -1
  55. package/dist/runtime-lifecycle.e2e.test.js +1 -1
  56. package/dist/session-handler/event-stream-state.js +4 -4
  57. package/dist/session-handler/thread-runtime-state.js +9 -0
  58. package/dist/session-handler/thread-session-runtime.js +93 -12
  59. package/dist/session-title-rename.test.js +39 -1
  60. package/dist/startup-time.e2e.test.js +2 -2
  61. package/dist/system-message.js +10 -8
  62. package/dist/thread-message-queue.e2e.test.js +1 -1
  63. package/dist/undo-redo.e2e.test.js +1 -1
  64. package/dist/unnest-code-blocks.js +1 -1
  65. package/dist/upgrade.js +1 -1
  66. package/dist/voice-message.e2e.test.js +1 -1
  67. package/dist/voice.js +3 -1
  68. package/dist/worktree-lifecycle.e2e.test.js +1 -1
  69. package/dist/worktrees.js +13 -71
  70. package/dist/worktrees.test.js +32 -1
  71. package/package.json +5 -5
  72. package/schema.prisma +5 -4
  73. package/src/agent-model.e2e.test.ts +1 -1
  74. package/src/ai-tool-to-genai.ts +2 -2
  75. package/src/anthropic-auth-plugin.test.ts +81 -0
  76. package/src/anthropic-auth-plugin.ts +18 -23
  77. package/src/anthropic-auth-state.ts +45 -221
  78. package/src/channel-management.ts +6 -7
  79. package/src/cli-commands/bot.ts +340 -0
  80. package/src/cli-commands/maintenance.ts +191 -0
  81. package/src/cli-commands/misc.ts +117 -0
  82. package/src/cli-commands/project.ts +508 -0
  83. package/src/cli-commands/send.ts +676 -0
  84. package/src/cli-commands/session.ts +658 -0
  85. package/src/cli-commands/task.ts +217 -0
  86. package/src/cli-commands/user.ts +196 -0
  87. package/src/cli-parsing.test.ts +12 -8
  88. package/src/cli-runner.ts +1868 -0
  89. package/src/cli-send-thread.e2e.test.ts +154 -8
  90. package/src/cli.ts +200 -4596
  91. package/src/commands/agent.ts +1 -2
  92. package/src/commands/compact.ts +3 -3
  93. package/src/commands/context-usage.ts +1 -1
  94. package/src/commands/login.ts +10 -3
  95. package/src/commands/mention-mode.ts +5 -6
  96. package/src/commands/merge-worktree.ts +13 -2
  97. package/src/commands/model-variant.ts +1 -2
  98. package/src/commands/model.ts +1 -2
  99. package/src/commands/multioauth.ts +314 -0
  100. package/src/commands/new-worktree.ts +2 -4
  101. package/src/commands/remove-project.ts +2 -2
  102. package/src/commands/resume.ts +2 -4
  103. package/src/commands/session.ts +3 -5
  104. package/src/commands/unset-model.ts +1 -2
  105. package/src/commands/user-command.ts +2 -2
  106. package/src/commands/verbosity.ts +1 -1
  107. package/src/commands/worktree-settings.ts +6 -7
  108. package/src/database.ts +1 -1
  109. package/src/db.ts +8 -0
  110. package/src/discord-bot.ts +40 -10
  111. package/src/discord-utils.ts +3 -3
  112. package/src/errors.ts +7 -0
  113. package/src/event-stream-real-capture.e2e.test.ts +1 -1
  114. package/src/external-opencode-sync.ts +1 -1
  115. package/src/forum-sync/discord-operations.ts +6 -2
  116. package/src/gateway-proxy-reconnect.e2e.test.ts +2 -2
  117. package/src/gateway-proxy.e2e.test.ts +1 -1
  118. package/src/genai-worker-wrapper.ts +1 -1
  119. package/src/genai-worker.ts +1 -1
  120. package/src/genai.ts +2 -5
  121. package/src/generated/commonInputTypes.ts +73 -73
  122. package/src/generated/internal/class.ts +4 -4
  123. package/src/generated/internal/prismaNamespace.ts +1 -0
  124. package/src/generated/internal/prismaNamespaceBrowser.ts +1 -0
  125. package/src/generated/models/bot_tokens.ts +0 -4
  126. package/src/generated/models/thread_sessions.ts +53 -1
  127. package/src/hrana-server.test.ts +1 -1
  128. package/src/hrana-server.ts +4 -3
  129. package/src/html-components.ts +4 -4
  130. package/src/kimaki-digital-twin.e2e.test.ts +1 -1
  131. package/src/markdown.ts +1 -2
  132. package/src/oauth-rotation-shared.ts +295 -0
  133. package/src/openai-auth-plugin.ts +155 -0
  134. package/src/openai-auth-state.ts +277 -0
  135. package/src/openai-realtime.ts +2 -2
  136. package/src/opencode-interrupt-plugin.ts +2 -2
  137. package/src/opencode.ts +1 -1
  138. package/src/queue-advanced-e2e-setup.ts +1 -1
  139. package/src/runtime-lifecycle.e2e.test.ts +1 -1
  140. package/src/schema.sql +1 -0
  141. package/src/session-handler/event-stream-state.ts +5 -5
  142. package/src/session-handler/thread-runtime-state.ts +11 -0
  143. package/src/session-handler/thread-session-runtime.ts +124 -12
  144. package/src/session-title-rename.test.ts +51 -1
  145. package/src/startup-time.e2e.test.ts +2 -2
  146. package/src/system-message.ts +11 -8
  147. package/src/thread-message-queue.e2e.test.ts +1 -1
  148. package/src/tools.ts +1 -1
  149. package/src/undo-redo.e2e.test.ts +1 -1
  150. package/src/unnest-code-blocks.ts +1 -1
  151. package/src/upgrade.ts +2 -2
  152. package/src/voice-handler.ts +1 -1
  153. package/src/voice-message.e2e.test.ts +1 -1
  154. package/src/voice.ts +7 -5
  155. package/src/worktree-lifecycle.e2e.test.ts +1 -1
  156. package/src/worktrees.test.ts +40 -0
  157. package/src/worktrees.ts +22 -84
  158. package/src/xml.ts +2 -2
@@ -274,7 +274,7 @@ describe('agent model resolution', () => {
274
274
  });
275
275
  }
276
276
  if (botClient) {
277
- botClient.destroy();
277
+ void botClient.destroy();
278
278
  }
279
279
  await stopOpencodeServer();
280
280
  await Promise.all([
@@ -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 block (from "You are OpenCode…" up to the
469
- * Anthropic prompt marker "Skills provide specialized instructions") and
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
- * The original OpenCode prompt between those markers contains the current
473
- * working directory and other runtime context. Stripping it wholesale loses
474
- * that info, so we add back what the model needs (cwd) in a compact form.
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
- * + "Skills provide specialized instructions …"
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 to the Anthropic prompt marker.
485
- // Keep the marker aligned with the current OpenCode Anthropic prompt.
486
- const endIdx = text.indexOf(ANTHROPIC_PROMPT_MARKER, startIdx);
487
- if (endIdx === -1) {
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
- return replaceBlockWithCompactEnv(text, startIdx, endIdx);
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 envCloseTag = "</env>";
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 + envCloseTag.length;
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 request-time prompt rewriting and transform fallback behavior.
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, rewriteAnthropicRequestPayload, } from './anthropic-auth-plugin.js';
4
- function parseRewrittenBody(body) {
5
- if (!body) {
6
- throw new Error('Expected rewritten body');
7
- }
8
- return JSON.parse(body);
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('rewriteAnthropicRequestPayload', () => {
11
- test('sanitizes raw opencode system text at request time', () => {
12
- const rewritten = rewriteAnthropicRequestPayload(JSON.stringify({
13
- model: 'claude-sonnet-4-5',
14
- system: "You are OpenCode, the best coding agent on the planet.\nOS: macOS\nCWD: /repo\nSkills provide specialized instructions\nUse opencode tools carefully.",
15
- tool_choice: { type: 'tool', name: 'read' },
16
- tools: [{ name: 'read' }],
17
- }));
18
- const payload = parseRewrittenBody(rewritten.body);
19
- expect(payload).toMatchInlineSnapshot(`
20
- {
21
- "model": "claude-sonnet-4-5",
22
- "system": [
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>/Users/morse/Documents/GitHub/kimakivoice/cli</cwd>
29
+ <cwd>/repo/site</cwd>
31
30
  </environment>
32
- Skills provide specialized instructions
33
- Use openc0de tools carefully.",
34
- "type": "text",
35
- },
36
- ],
37
- "tool_choice": {
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('does not duplicate claude code identity when request was already sanitized', () => {
50
- const rewritten = rewriteAnthropicRequestPayload(JSON.stringify({
51
- model: 'claude-sonnet-4-5',
52
- system: [
53
- {
54
- type: 'text',
55
- text: "You are Claude Code, Anthropic's official CLI for Claude.",
56
- },
57
- {
58
- type: 'text',
59
- text: '<environment>\n<cwd>/repo</cwd>\n</environment>\nSkills provide specialized instructions',
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>/Users/morse/Documents/GitHub/kimakivoice/cli</cwd>
52
+ <cwd>/repo/site</cwd>
103
53
  </environment>
104
- Skills provide specialized instructions
105
- Use openc0de tools carefully.",
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
- import * as fs from 'node:fs/promises';
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
- const AUTH_LOCK_STALE_MS = 30_000;
6
- const AUTH_LOCK_RETRY_MS = 100;
7
- async function readJson(filePath, fallback) {
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
- export async function withAuthStateLock(fn) {
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
- /** Short label for an account: first 8 + last 4 chars of refresh token. */
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
- const index = store.accounts.findIndex((account) => {
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 isOAuthStored(value) {
190
- if (!value || typeof value !== 'object') {
191
- return false;
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
- export async function setAnthropicAuth(auth, client) {
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.isTextBased());
89
+ const textChannels = guild.channels.cache.filter((channel) => channel.type === ChannelType.GuildText);
90
90
  for (const channel of textChannels.values()) {
91
- const textChannel = channel;
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(textChannel.id);
93
+ const channelConfig = await getChannelDirectory(channel.id);
95
94
  channels.push({
96
- id: textChannel.id,
97
- name: textChannel.name,
95
+ id: channel.id,
96
+ name: channel.name,
98
97
  description,
99
98
  kimakiDirectory: channelConfig?.directory,
100
99
  });