@otto-assistant/bridge 0.4.96 → 0.4.100

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 (44) hide show
  1. package/dist/agent-model.e2e.test.js +7 -1
  2. package/dist/anthropic-account-identity.js +62 -0
  3. package/dist/anthropic-account-identity.test.js +38 -0
  4. package/dist/anthropic-auth-plugin.js +72 -12
  5. package/dist/anthropic-auth-state.js +28 -3
  6. package/dist/{anthropic-auth-plugin.test.js → anthropic-auth-state.test.js} +21 -2
  7. package/dist/cli-parsing.test.js +12 -9
  8. package/dist/cli-send-thread.e2e.test.js +4 -7
  9. package/dist/cli.js +25 -12
  10. package/dist/commands/screenshare.js +1 -1
  11. package/dist/commands/screenshare.test.js +2 -2
  12. package/dist/commands/vscode.js +269 -0
  13. package/dist/db.js +1 -0
  14. package/dist/discord-command-registration.js +7 -2
  15. package/dist/gateway-proxy-reconnect.e2e.test.js +2 -2
  16. package/dist/interaction-handler.js +4 -0
  17. package/dist/system-message.js +24 -23
  18. package/dist/system-message.test.js +24 -23
  19. package/dist/system-prompt-drift-plugin.js +41 -11
  20. package/dist/utils.js +1 -1
  21. package/dist/worktrees.js +0 -33
  22. package/package.json +1 -1
  23. package/src/agent-model.e2e.test.ts +8 -1
  24. package/src/anthropic-account-identity.test.ts +52 -0
  25. package/src/anthropic-account-identity.ts +77 -0
  26. package/src/anthropic-auth-plugin.ts +82 -12
  27. package/src/{anthropic-auth-plugin.test.ts → anthropic-auth-state.test.ts} +23 -1
  28. package/src/anthropic-auth-state.ts +36 -3
  29. package/src/cli-parsing.test.ts +16 -9
  30. package/src/cli-send-thread.e2e.test.ts +6 -7
  31. package/src/cli.ts +31 -13
  32. package/src/commands/screenshare.test.ts +2 -2
  33. package/src/commands/screenshare.ts +1 -1
  34. package/src/commands/vscode.ts +342 -0
  35. package/src/db.ts +1 -0
  36. package/src/discord-command-registration.ts +9 -2
  37. package/src/gateway-proxy-reconnect.e2e.test.ts +2 -2
  38. package/src/interaction-handler.ts +5 -0
  39. package/src/system-message.test.ts +24 -23
  40. package/src/system-message.ts +24 -23
  41. package/src/system-prompt-drift-plugin.ts +48 -12
  42. package/src/utils.ts +1 -1
  43. package/src/worktrees.test.ts +1 -0
  44. package/src/worktrees.ts +1 -47
@@ -417,7 +417,13 @@ describe('agent model resolution', () => {
417
417
  afterMessageIncludes: 'reply-context-ok',
418
418
  afterAuthorId: discord.botUserId,
419
419
  });
420
- expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
420
+ const threadText = (await discord.thread(thread.id).text())
421
+ .split('\n')
422
+ .filter((line) => {
423
+ return !line.startsWith('⬦ info: Context cache discarded:');
424
+ })
425
+ .join('\n');
426
+ expect(threadText).toMatchInlineSnapshot(`
421
427
  "--- from: user (agent-model-tester)
422
428
  first message in thread
423
429
  Reply with exactly: reply-context-check
@@ -0,0 +1,62 @@
1
+ // Helpers for extracting and normalizing Anthropic OAuth account identity.
2
+ const identityHintKeys = new Set(['user', 'profile', 'account', 'viewer']);
3
+ const idKeys = ['user_id', 'userId', 'account_id', 'accountId', 'id', 'sub'];
4
+ export function normalizeAnthropicAccountIdentity(identity) {
5
+ const email = typeof identity?.email === 'string' && identity.email.trim()
6
+ ? identity.email.trim().toLowerCase()
7
+ : undefined;
8
+ const accountId = typeof identity?.accountId === 'string' && identity.accountId.trim()
9
+ ? identity.accountId.trim()
10
+ : undefined;
11
+ if (!email && !accountId)
12
+ return undefined;
13
+ return {
14
+ ...(email ? { email } : {}),
15
+ ...(accountId ? { accountId } : {}),
16
+ };
17
+ }
18
+ function getCandidateFromRecord(record, path) {
19
+ const email = typeof record.email === 'string' ? record.email : undefined;
20
+ const accountId = idKeys
21
+ .map((key) => {
22
+ const value = record[key];
23
+ return typeof value === 'string' ? value : undefined;
24
+ })
25
+ .find((value) => {
26
+ return Boolean(value);
27
+ });
28
+ const normalized = normalizeAnthropicAccountIdentity({ email, accountId });
29
+ if (!normalized)
30
+ return undefined;
31
+ const hasIdentityHint = path.some((segment) => {
32
+ return identityHintKeys.has(segment);
33
+ });
34
+ return {
35
+ ...normalized,
36
+ score: (normalized.email ? 4 : 0) + (normalized.accountId ? 2 : 0) + (hasIdentityHint ? 2 : 0),
37
+ };
38
+ }
39
+ function collectIdentityCandidates(value, path = []) {
40
+ if (!value || typeof value !== 'object')
41
+ return [];
42
+ if (Array.isArray(value)) {
43
+ return value.flatMap((entry) => {
44
+ return collectIdentityCandidates(entry, path);
45
+ });
46
+ }
47
+ const record = value;
48
+ const nested = Object.entries(record).flatMap(([key, entry]) => {
49
+ return collectIdentityCandidates(entry, [...path, key]);
50
+ });
51
+ const current = getCandidateFromRecord(record, path);
52
+ return current ? [current, ...nested] : nested;
53
+ }
54
+ export function extractAnthropicAccountIdentity(value) {
55
+ const candidates = collectIdentityCandidates(value);
56
+ const best = candidates.sort((a, b) => {
57
+ return b.score - a.score;
58
+ })[0];
59
+ if (!best)
60
+ return undefined;
61
+ return normalizeAnthropicAccountIdentity(best);
62
+ }
@@ -0,0 +1,38 @@
1
+ // Tests Anthropic OAuth account identity parsing and normalization.
2
+ import { describe, expect, test } from 'vitest';
3
+ import { extractAnthropicAccountIdentity, normalizeAnthropicAccountIdentity, } from './anthropic-account-identity.js';
4
+ describe('normalizeAnthropicAccountIdentity', () => {
5
+ test('normalizes email casing and drops empty values', () => {
6
+ expect(normalizeAnthropicAccountIdentity({
7
+ email: ' User@Example.com ',
8
+ accountId: ' user_123 ',
9
+ })).toEqual({
10
+ email: 'user@example.com',
11
+ accountId: 'user_123',
12
+ });
13
+ expect(normalizeAnthropicAccountIdentity({ email: ' ' })).toBeUndefined();
14
+ });
15
+ });
16
+ describe('extractAnthropicAccountIdentity', () => {
17
+ test('prefers nested user profile identity from client_data responses', () => {
18
+ expect(extractAnthropicAccountIdentity({
19
+ organizations: [{ id: 'org_123', name: 'Workspace' }],
20
+ user: {
21
+ id: 'usr_123',
22
+ email: 'User@Example.com',
23
+ },
24
+ })).toEqual({
25
+ accountId: 'usr_123',
26
+ email: 'user@example.com',
27
+ });
28
+ });
29
+ test('falls back to profile-style payloads without email', () => {
30
+ expect(extractAnthropicAccountIdentity({
31
+ profile: {
32
+ user_id: 'usr_456',
33
+ },
34
+ })).toEqual({
35
+ accountId: 'usr_456',
36
+ });
37
+ });
38
+ });
@@ -23,6 +23,7 @@
23
23
  * - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts
24
24
  */
25
25
  import { loadAccountStore, rememberAnthropicOAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, shouldRotateAuth, upsertAccount, withAuthStateLock, } from './anthropic-auth-state.js';
26
+ import { extractAnthropicAccountIdentity, } from './anthropic-account-identity.js';
26
27
  // PKCE (Proof Key for Code Exchange) using Web Crypto API.
27
28
  // Reference: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/pkce.ts
28
29
  function base64urlEncode(bytes) {
@@ -52,6 +53,8 @@ const CLIENT_ID = (() => {
52
53
  })();
53
54
  const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
54
55
  const CREATE_API_KEY_URL = 'https://api.anthropic.com/api/oauth/claude_cli/create_api_key';
56
+ const CLIENT_DATA_URL = 'https://api.anthropic.com/api/oauth/claude_cli/client_data';
57
+ const PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
55
58
  const CALLBACK_PORT = 53692;
56
59
  const CALLBACK_PATH = '/callback';
57
60
  const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
@@ -64,6 +67,7 @@ const CLAUDE_CODE_BETA = 'claude-code-20250219';
64
67
  const OAUTH_BETA = 'oauth-2025-04-20';
65
68
  const FINE_GRAINED_TOOL_STREAMING_BETA = 'fine-grained-tool-streaming-2025-05-14';
66
69
  const INTERLEAVED_THINKING_BETA = 'interleaved-thinking-2025-05-14';
70
+ const TOAST_SESSION_HEADER = 'x-kimaki-session-id';
67
71
  const ANTHROPIC_HOSTS = new Set([
68
72
  'api.anthropic.com',
69
73
  'claude.ai',
@@ -227,6 +231,29 @@ async function createApiKey(accessToken) {
227
231
  const json = JSON.parse(responseText);
228
232
  return { type: 'success', key: json.raw_key };
229
233
  }
234
+ async function fetchAnthropicAccountIdentity(accessToken) {
235
+ const urls = [CLIENT_DATA_URL, PROFILE_URL];
236
+ for (const url of urls) {
237
+ const responseText = await requestText(url, {
238
+ method: 'GET',
239
+ headers: {
240
+ Accept: 'application/json',
241
+ authorization: `Bearer ${accessToken}`,
242
+ 'user-agent': process.env.OPENCODE_ANTHROPIC_USER_AGENT || `claude-cli/${CLAUDE_CODE_VERSION}`,
243
+ 'x-app': 'cli',
244
+ },
245
+ }).catch(() => {
246
+ return undefined;
247
+ });
248
+ if (!responseText)
249
+ continue;
250
+ const parsed = JSON.parse(responseText);
251
+ const identity = extractAnthropicAccountIdentity(parsed);
252
+ if (identity)
253
+ return identity;
254
+ }
255
+ return undefined;
256
+ }
230
257
  async function startCallbackServer(expectedState) {
231
258
  return new Promise((resolve, reject) => {
232
259
  let settle;
@@ -376,12 +403,13 @@ function buildAuthorizeHandler(mode) {
376
403
  if (mode === 'apikey') {
377
404
  return createApiKey(creds.access);
378
405
  }
406
+ const identity = await fetchAnthropicAccountIdentity(creds.access);
379
407
  await rememberAnthropicOAuth({
380
408
  type: 'oauth',
381
409
  refresh: creds.refresh,
382
410
  access: creds.access,
383
411
  expires: creds.expires,
384
- });
412
+ }, identity);
385
413
  return creds;
386
414
  };
387
415
  if (!isRemote) {
@@ -395,8 +423,7 @@ function buildAuthorizeHandler(mode) {
395
423
  const result = await waitForCallback(auth.callbackServer);
396
424
  return await finalize(result);
397
425
  }
398
- catch (error) {
399
- console.error(`[anthropic-auth] ${error}`);
426
+ catch {
400
427
  return { type: 'failed' };
401
428
  }
402
429
  })();
@@ -414,8 +441,7 @@ function buildAuthorizeHandler(mode) {
414
441
  const result = await waitForCallback(auth.callbackServer, input);
415
442
  return await finalize(result);
416
443
  }
417
- catch (error) {
418
- console.error(`[anthropic-auth] ${error}`);
444
+ catch {
419
445
  return { type: 'failed' };
420
446
  }
421
447
  })();
@@ -434,6 +460,8 @@ function sanitizeSystemText(text, onError) {
434
460
  const startIdx = text.indexOf(OPENCODE_IDENTITY);
435
461
  if (startIdx === -1)
436
462
  return text;
463
+ // to find the last heading to match readhttps://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/prompt/anthropic.txt
464
+ // it contains the opencode injected prompt. you must keep the codeRefsMarker updated with that package
437
465
  const codeRefsMarker = '# Code References';
438
466
  const endIdx = text.indexOf(codeRefsMarker, startIdx);
439
467
  if (endIdx === -1) {
@@ -577,6 +605,12 @@ function wrapResponseStream(response, reverseToolNameMap) {
577
605
  headers: response.headers,
578
606
  });
579
607
  }
608
+ function appendToastSessionMarker({ message, sessionId, }) {
609
+ if (!sessionId) {
610
+ return message;
611
+ }
612
+ return `${message} ${sessionId}`;
613
+ }
580
614
  // --- Beta headers ---
581
615
  function getRequiredBetas(modelId) {
582
616
  const betas = [CLAUDE_CODE_BETA, OAUTH_BETA, FINE_GRAINED_TOOL_STREAMING_BETA];
@@ -624,7 +658,19 @@ async function getFreshOAuth(getAuth, client) {
624
658
  await setAnthropicAuth(refreshed, client);
625
659
  const store = await loadAccountStore();
626
660
  if (store.accounts.length > 0) {
627
- upsertAccount(store, refreshed);
661
+ const identity = (() => {
662
+ const currentIndex = store.accounts.findIndex((account) => {
663
+ return account.refresh === latest.refresh || account.access === latest.access;
664
+ });
665
+ const current = currentIndex >= 0 ? store.accounts[currentIndex] : undefined;
666
+ if (!current)
667
+ return undefined;
668
+ return {
669
+ ...(current.email ? { email: current.email } : {}),
670
+ ...(current.accountId ? { accountId: current.accountId } : {}),
671
+ };
672
+ })();
673
+ upsertAccount(store, { ...refreshed, ...identity });
628
674
  await saveAccountStore(store);
629
675
  }
630
676
  return refreshed;
@@ -637,6 +683,12 @@ async function getFreshOAuth(getAuth, client) {
637
683
  // --- Plugin export ---
638
684
  const AnthropicAuthPlugin = async ({ client }) => {
639
685
  return {
686
+ 'chat.headers': async (input, output) => {
687
+ if (input.model.providerID !== 'anthropic') {
688
+ return;
689
+ }
690
+ output.headers[TOAST_SESSION_HEADER] = input.sessionID;
691
+ },
640
692
  auth: {
641
693
  provider: 'anthropic',
642
694
  async loader(getAuth, provider) {
@@ -668,11 +720,6 @@ const AnthropicAuthPlugin = async ({ client }) => {
668
720
  .text()
669
721
  .catch(() => undefined)
670
722
  : undefined;
671
- const rewritten = rewriteRequestPayload(originalBody, (msg) => {
672
- client.tui.showToast({
673
- body: { message: msg, variant: 'error' },
674
- }).catch(() => { });
675
- });
676
723
  const headers = new Headers(init?.headers);
677
724
  if (input instanceof Request) {
678
725
  input.headers.forEach((v, k) => {
@@ -680,9 +727,19 @@ const AnthropicAuthPlugin = async ({ client }) => {
680
727
  headers.set(k, v);
681
728
  });
682
729
  }
730
+ const sessionId = headers.get(TOAST_SESSION_HEADER) ?? undefined;
731
+ const rewritten = rewriteRequestPayload(originalBody, (msg) => {
732
+ client.tui.showToast({
733
+ body: {
734
+ message: appendToastSessionMarker({ message: msg, sessionId }),
735
+ variant: 'error',
736
+ },
737
+ }).catch(() => { });
738
+ });
683
739
  const betas = getRequiredBetas(rewritten.modelId);
684
740
  const runRequest = async (auth) => {
685
741
  const requestHeaders = new Headers(headers);
742
+ requestHeaders.delete(TOAST_SESSION_HEADER);
686
743
  requestHeaders.set('accept', 'application/json');
687
744
  requestHeaders.set('anthropic-beta', mergeBetas(requestHeaders.get('anthropic-beta'), betas));
688
745
  requestHeaders.set('anthropic-dangerous-direct-browser-access', 'true');
@@ -711,7 +768,10 @@ const AnthropicAuthPlugin = async ({ client }) => {
711
768
  // Show toast notification so Discord thread shows the rotation
712
769
  client.tui.showToast({
713
770
  body: {
714
- message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
771
+ message: appendToastSessionMarker({
772
+ message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
773
+ sessionId,
774
+ }),
715
775
  variant: 'info',
716
776
  },
717
777
  }).catch(() => { });
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import path from 'node:path';
4
+ import { normalizeAnthropicAccountIdentity, } from './anthropic-account-identity.js';
4
5
  const AUTH_LOCK_STALE_MS = 30_000;
5
6
  const AUTH_LOCK_RETRY_MS = 100;
6
7
  async function readJson(filePath, fallback) {
@@ -80,6 +81,8 @@ export function normalizeAccountStore(input) {
80
81
  typeof account.refresh === 'string' &&
81
82
  typeof account.access === 'string' &&
82
83
  typeof account.expires === 'number' &&
84
+ (typeof account.email === 'undefined' || typeof account.email === 'string') &&
85
+ (typeof account.accountId === 'undefined' || typeof account.accountId === 'string') &&
83
86
  typeof account.addedAt === 'number' &&
84
87
  typeof account.lastUsed === 'number')
85
88
  : [];
@@ -96,8 +99,13 @@ export async function saveAccountStore(store) {
96
99
  }
97
100
  /** Short label for an account: first 8 + last 4 chars of refresh token. */
98
101
  export function accountLabel(account, index) {
102
+ const accountWithIdentity = account;
103
+ const identity = accountWithIdentity.email || accountWithIdentity.accountId;
99
104
  const r = account.refresh;
100
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
+ }
101
109
  return index !== undefined ? `#${index + 1} (${short})` : short;
102
110
  }
103
111
  function findCurrentAccountIndex(store, auth) {
@@ -116,14 +124,29 @@ function findCurrentAccountIndex(store, auth) {
116
124
  return store.activeIndex;
117
125
  }
118
126
  export function upsertAccount(store, auth, now = Date.now()) {
127
+ const authWithIdentity = auth;
128
+ const identity = normalizeAnthropicAccountIdentity({
129
+ email: authWithIdentity.email,
130
+ accountId: authWithIdentity.accountId,
131
+ });
119
132
  const index = store.accounts.findIndex((account) => {
120
- return account.refresh === auth.refresh || account.access === auth.access;
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;
121
143
  });
122
144
  const nextAccount = {
123
145
  type: 'oauth',
124
146
  refresh: auth.refresh,
125
147
  access: auth.access,
126
148
  expires: auth.expires,
149
+ ...identity,
127
150
  addedAt: now,
128
151
  lastUsed: now,
129
152
  };
@@ -139,14 +162,16 @@ export function upsertAccount(store, auth, now = Date.now()) {
139
162
  ...existing,
140
163
  ...nextAccount,
141
164
  addedAt: existing.addedAt,
165
+ email: nextAccount.email || existing.email,
166
+ accountId: nextAccount.accountId || existing.accountId,
142
167
  };
143
168
  store.activeIndex = index;
144
169
  return index;
145
170
  }
146
- export async function rememberAnthropicOAuth(auth) {
171
+ export async function rememberAnthropicOAuth(auth, identity) {
147
172
  await withAuthStateLock(async () => {
148
173
  const store = await loadAccountStore();
149
- upsertAccount(store, auth);
174
+ upsertAccount(store, { ...auth, ...normalizeAnthropicAccountIdentity(identity) });
150
175
  await saveAccountStore(store);
151
176
  });
152
177
  }
@@ -1,9 +1,9 @@
1
- // Tests for Anthropic OAuth multi-account persistence and rotation.
1
+ // Tests Anthropic OAuth account persistence, deduplication, and rotation.
2
2
  import { mkdtemp, readFile, rm, mkdir, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { afterEach, beforeEach, describe, expect, test } from 'vitest';
6
- import { authFilePath, loadAccountStore, rememberAnthropicOAuth, removeAccount, rotateAnthropicAccount, saveAccountStore, shouldRotateAuth, } from './anthropic-auth-state.js';
6
+ import { accountLabel, authFilePath, loadAccountStore, rememberAnthropicOAuth, removeAccount, rotateAnthropicAccount, saveAccountStore, shouldRotateAuth, } from './anthropic-auth-state.js';
7
7
  const firstAccount = {
8
8
  type: 'oauth',
9
9
  refresh: 'refresh-first',
@@ -45,6 +45,25 @@ describe('rememberAnthropicOAuth', () => {
45
45
  expires: 3,
46
46
  });
47
47
  });
48
+ test('deduplicates new tokens by email or account ID', async () => {
49
+ await rememberAnthropicOAuth(firstAccount, {
50
+ email: 'user@example.com',
51
+ accountId: 'usr_123',
52
+ });
53
+ await rememberAnthropicOAuth(secondAccount, {
54
+ email: 'User@example.com',
55
+ accountId: 'usr_123',
56
+ });
57
+ const store = await loadAccountStore();
58
+ expect(store.accounts).toHaveLength(1);
59
+ expect(store.accounts[0]).toMatchObject({
60
+ refresh: 'refresh-second',
61
+ access: 'access-second',
62
+ email: 'user@example.com',
63
+ accountId: 'usr_123',
64
+ });
65
+ expect(accountLabel(store.accounts[0])).toBe('user@example.com');
66
+ });
48
67
  });
49
68
  describe('rotateAnthropicAccount', () => {
50
69
  test('rotates to the next stored account and syncs auth state', async () => {
@@ -22,8 +22,8 @@ function createCliForIdParsing() {
22
22
  .command('add-project', 'Add a project')
23
23
  .option('-g, --guild <guildId>', 'Discord guild/server ID');
24
24
  cli.command('task delete <id>', 'Delete task');
25
- cli.command('anthropic-accounts list', 'List stored Anthropic accounts').hidden();
26
- cli.command('anthropic-accounts remove <index>', 'Remove stored Anthropic account').hidden();
25
+ cli.command('anthropic-accounts list', 'List stored Anthropic accounts');
26
+ cli.command('anthropic-accounts remove <indexOrEmail>', 'Remove stored Anthropic account');
27
27
  return cli;
28
28
  }
29
29
  describe('goke CLI ID parsing', () => {
@@ -113,13 +113,16 @@ describe('goke CLI ID parsing', () => {
113
113
  expect(result.args[0]).toBe(taskId);
114
114
  expect(typeof result.args[0]).toBe('string');
115
115
  });
116
- test('hidden anthropic account commands still parse', () => {
116
+ test('anthropic account remove parses index and email as strings', () => {
117
117
  const cli = createCliForIdParsing();
118
- const result = cli.parse(['node', 'kimaki', 'anthropic-accounts', 'remove', '2'], { run: false });
119
- expect(result.args[0]).toBe('2');
120
- expect(typeof result.args[0]).toBe('string');
118
+ const indexResult = cli.parse(['node', 'kimaki', 'anthropic-accounts', 'remove', '2'], { run: false });
119
+ const emailResult = cli.parse(['node', 'kimaki', 'anthropic-accounts', 'remove', 'user@example.com'], { run: false });
120
+ expect(indexResult.args[0]).toBe('2');
121
+ expect(typeof indexResult.args[0]).toBe('string');
122
+ expect(emailResult.args[0]).toBe('user@example.com');
123
+ expect(typeof emailResult.args[0]).toBe('string');
121
124
  });
122
- test('hidden anthropic account commands are excluded from help output', () => {
125
+ test('anthropic account commands are included in help output', () => {
123
126
  const stdout = {
124
127
  text: '',
125
128
  write(data) {
@@ -128,10 +131,10 @@ describe('goke CLI ID parsing', () => {
128
131
  };
129
132
  const cli = goke('kimaki', { stdout: stdout });
130
133
  cli.command('send', 'Send a message');
131
- cli.command('anthropic-accounts list', 'List stored Anthropic accounts').hidden();
134
+ cli.command('anthropic-accounts list', 'List stored Anthropic accounts');
132
135
  cli.help();
133
136
  cli.parse(['node', 'kimaki', '--help'], { run: false });
134
137
  expect(stdout.text).toContain('send');
135
- expect(stdout.text).not.toContain('anthropic-accounts');
138
+ expect(stdout.text).toContain('anthropic-accounts');
136
139
  });
137
140
  });
@@ -257,14 +257,11 @@ describe('kimaki send --channel thread creation', () => {
257
257
  return m.author.id === discord.botUserId && m.id !== starterMessage.id;
258
258
  });
259
259
  const allContent = botReplies.map((m) => {
260
- return m.content.slice(0, 200);
260
+ return m.content;
261
261
  });
262
- expect(allContent).toMatchInlineSnapshot(`
263
- [
264
- "✗ opencode session error: Command not found: "hello-test". Available commands: init, review, goke, security-review, jitter, proxyman, gitchamber, event-sourcing-state, usecomputer, spiceflow, batch, x",
265
- "✗ OpenCode API error: Command not found: "hello-test". Available commands: init, review, goke, security-review, jitter, proxyman, gitchamber, event-sourcing-state, usecomputer, spiceflow, batch, x-art",
266
- ]
267
- `);
262
+ expect(allContent.some((content) => {
263
+ return content.includes('Command not found: "hello-test"');
264
+ })).toBe(true);
268
265
  }
269
266
  finally {
270
267
  store.setState({ registeredUserCommands: prevCommands });
package/dist/cli.js CHANGED
@@ -41,7 +41,7 @@ const cliLogger = createLogger(LogPrefix.CLI);
41
41
  // We derive REST base from this URL by swapping ws/wss to http/https.
42
42
  // These are hardcoded because they're deploy-time constants for the gateway infrastructure.
43
43
  const KIMAKI_GATEWAY_PROXY_URL = process.env.KIMAKI_GATEWAY_PROXY_URL ||
44
- 'wss://discord-gateway.kimaki.xyz';
44
+ 'wss://discord-gateway.kimaki.dev';
45
45
  const KIMAKI_GATEWAY_PROXY_REST_BASE_URL = getGatewayProxyRestBaseUrl({
46
46
  gatewayUrl: KIMAKI_GATEWAY_PROXY_URL,
47
47
  });
@@ -662,7 +662,8 @@ async function resolveCredentials({ forceRestartOnboarding, forceGateway, gatewa
662
662
  options: [
663
663
  {
664
664
  value: 'gateway',
665
- label: 'Gateway (pre-built Kimaki bot — no setup needed)',
665
+ disabled: true,
666
+ label: 'Gateway (pre-built Kimaki bot, currently disabled because of Discord verification process. will be re-enabled soon)',
666
667
  },
667
668
  {
668
669
  value: 'self_hosted',
@@ -2224,7 +2225,6 @@ cli
2224
2225
  });
2225
2226
  cli
2226
2227
  .command('anthropic-accounts list', 'List stored Anthropic OAuth accounts used for automatic rotation')
2227
- .hidden()
2228
2228
  .action(async () => {
2229
2229
  const store = await loadAccountStore();
2230
2230
  console.log(`Store: ${accountsFilePath()}`);
@@ -2239,16 +2239,29 @@ cli
2239
2239
  process.exit(0);
2240
2240
  });
2241
2241
  cli
2242
- .command('anthropic-accounts remove <index>', 'Remove a stored Anthropic OAuth account from the rotation pool')
2243
- .hidden()
2244
- .action(async (index) => {
2245
- const value = Number(index);
2246
- if (!Number.isInteger(value) || value < 1) {
2247
- cliLogger.error('Usage: kimaki anthropic-accounts remove <index>');
2242
+ .command('anthropic-accounts remove <indexOrEmail>', 'Remove a stored Anthropic OAuth account from the rotation pool by index or email')
2243
+ .action(async (indexOrEmail) => {
2244
+ const value = Number(indexOrEmail);
2245
+ const store = await loadAccountStore();
2246
+ const resolvedIndex = (() => {
2247
+ if (Number.isInteger(value) && value >= 1) {
2248
+ return value - 1;
2249
+ }
2250
+ const email = indexOrEmail.trim().toLowerCase();
2251
+ if (!email) {
2252
+ return -1;
2253
+ }
2254
+ return store.accounts.findIndex((account) => {
2255
+ return account.email?.toLowerCase() === email;
2256
+ });
2257
+ })();
2258
+ if (resolvedIndex < 0) {
2259
+ cliLogger.error('Usage: kimaki anthropic-accounts remove <index-or-email>');
2248
2260
  process.exit(EXIT_NO_RESTART);
2249
2261
  }
2250
- await removeAccount(value - 1);
2251
- cliLogger.log(`Removed Anthropic account ${value}`);
2262
+ const removed = store.accounts[resolvedIndex];
2263
+ await removeAccount(resolvedIndex);
2264
+ cliLogger.log(`Removed Anthropic account ${removed ? accountLabel(removed, resolvedIndex) : indexOrEmail}`);
2252
2265
  process.exit(0);
2253
2266
  });
2254
2267
  cli
@@ -2682,7 +2695,7 @@ cli
2682
2695
  port,
2683
2696
  tunnelId: options.tunnelId,
2684
2697
  localHost: options.host,
2685
- baseDomain: 'kimaki.xyz',
2698
+ baseDomain: 'kimaki.dev',
2686
2699
  serverUrl: options.server,
2687
2700
  command: command.length > 0 ? command : undefined,
2688
2701
  kill: options.kill,
@@ -21,7 +21,7 @@ const activeSessions = new Map();
21
21
  const VNC_PORT = 5900;
22
22
  const MAX_SESSION_MINUTES = 30;
23
23
  const MAX_SESSION_MS = MAX_SESSION_MINUTES * 60 * 1000;
24
- const TUNNEL_BASE_DOMAIN = 'kimaki.xyz';
24
+ const TUNNEL_BASE_DOMAIN = 'kimaki.dev';
25
25
  const SCREENSHARE_TUNNEL_ID_BYTES = 16;
26
26
  // Public noVNC client — we point it at our tunnel URL
27
27
  export function buildNoVncUrl({ tunnelHost }) {
@@ -11,9 +11,9 @@ describe('screenshare security defaults', () => {
11
11
  }
12
12
  });
13
13
  test('builds a secure noVNC URL', () => {
14
- const url = new URL(buildNoVncUrl({ tunnelHost: '0123456789abcdef-tunnel.kimaki.xyz' }));
14
+ const url = new URL(buildNoVncUrl({ tunnelHost: '0123456789abcdef-tunnel.kimaki.dev' }));
15
15
  expect(url.origin).toBe('https://novnc.com');
16
- expect(url.searchParams.get('host')).toBe('0123456789abcdef-tunnel.kimaki.xyz');
16
+ expect(url.searchParams.get('host')).toBe('0123456789abcdef-tunnel.kimaki.dev');
17
17
  expect(url.searchParams.get('port')).toBe('443');
18
18
  expect(url.searchParams.get('encrypt')).toBe('1');
19
19
  });