@brianli/kimaki 0.4.73-brianli.1 → 0.4.73-brianli.3

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 (3) hide show
  1. package/dist/cli.js +77 -58
  2. package/package.json +4 -4
  3. package/src/cli.ts +97 -73
package/dist/cli.js CHANGED
@@ -43,6 +43,8 @@ const KIMAKI_GATEWAY_PROXY_URL = process.env.KIMAKI_GATEWAY_PROXY_URL ||
43
43
  const KIMAKI_GATEWAY_PROXY_REST_BASE_URL = getGatewayProxyRestBaseUrl({
44
44
  gatewayUrl: KIMAKI_GATEWAY_PROXY_URL,
45
45
  });
46
+ const KIMAKI_CLIENT_ID = process.env.KIMAKI_CLIENT_ID?.trim() || undefined;
47
+ const KIMAKI_CLIENT_SECRET = process.env.KIMAKI_CLIENT_SECRET?.trim() || undefined;
46
48
  // Strip bracketed paste escape sequences from terminal input.
47
49
  // iTerm2 and other terminals wrap pasted content with \x1b[200~ and \x1b[201~
48
50
  // which can cause validation to fail on macOS. See: https://github.com/remorses/kimaki/issues/18
@@ -903,70 +905,82 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
903
905
  cliLogger.error('Gateway mode is not available yet. KIMAKI_SHARED_APP_ID is not configured.');
904
906
  process.exit(EXIT_NO_RESTART);
905
907
  }
906
- // Generate client credentials
907
- const clientId = crypto.randomUUID();
908
- const clientSecret = crypto.randomBytes(32).toString('hex');
909
- const statePayload = JSON.stringify({ clientId, clientSecret });
910
- const oauthUrl = generateBotInstallUrl({
911
- clientId: KIMAKI_SHARED_APP_ID,
912
- state: statePayload,
913
- redirectUri: `${KIMAKI_WEBSITE_URL}/api/auth/callback/discord`,
914
- responseType: 'code',
915
- });
916
- note(`Open this URL to install the Kimaki bot in your Discord server:\n\n${oauthUrl}\n\nDo not share this URL with anyone — it contains your credentials.\n\nIf you don't have a server, create one first (+ button in the Discord sidebar).`, 'Install Bot');
917
- // Open URL in default browser
918
- const { exec } = await import('node:child_process');
919
- const openCmd = process.platform === 'darwin'
920
- ? 'open'
921
- : process.platform === 'win32'
922
- ? 'start'
923
- : 'xdg-open';
924
- exec(`${openCmd} "${oauthUrl}"`);
925
- // Poll until the user installs the bot in a Discord server.
926
- // 600 attempts x 2s = 20 minutes timeout.
927
- const s = spinner();
928
- s.start('Waiting for a Discord server with the bot installed...');
929
- const pollUrl = new URL('/api/onboarding/status', KIMAKI_WEBSITE_URL);
930
- pollUrl.searchParams.set('client_id', clientId);
931
- pollUrl.searchParams.set('secret', clientSecret);
932
- let guildId;
933
- for (let attempt = 0; attempt < 600; attempt++) {
934
- await new Promise((resolve) => {
935
- setTimeout(resolve, 2000);
908
+ const hasGatewayClientId = Boolean(KIMAKI_CLIENT_ID);
909
+ const hasGatewayClientSecret = Boolean(KIMAKI_CLIENT_SECRET);
910
+ if (hasGatewayClientId !== hasGatewayClientSecret) {
911
+ cliLogger.error('Gateway credential env vars must be set together: KIMAKI_CLIENT_ID and KIMAKI_CLIENT_SECRET.');
912
+ process.exit(EXIT_NO_RESTART);
913
+ }
914
+ const clientId = KIMAKI_CLIENT_ID || crypto.randomUUID();
915
+ const clientSecret = KIMAKI_CLIENT_SECRET
916
+ || crypto.randomBytes(32).toString('hex');
917
+ const hasGatewayEnvCredentials = hasGatewayClientId && hasGatewayClientSecret;
918
+ if (hasGatewayEnvCredentials) {
919
+ cliLogger.log('Using KIMAKI_CLIENT_ID and KIMAKI_CLIENT_SECRET from environment for gateway mode. Skipping OAuth onboarding.');
920
+ }
921
+ if (!hasGatewayEnvCredentials) {
922
+ const statePayload = JSON.stringify({ clientId, clientSecret });
923
+ const oauthUrl = generateBotInstallUrl({
924
+ clientId: KIMAKI_SHARED_APP_ID,
925
+ state: statePayload,
926
+ redirectUri: `${KIMAKI_WEBSITE_URL}/api/auth/callback/discord`,
927
+ responseType: 'code',
936
928
  });
937
- // Progressive hints for users who may be stuck
938
- if (attempt === 15) {
939
- // ~30s
940
- s.message('Still waiting... Select a server in the Discord authorization page and click "Authorize"');
941
- }
942
- else if (attempt === 45) {
943
- // ~90s
944
- s.message(`Still waiting... If you don't see any servers, create one first (+ button in Discord sidebar), then reopen the URL above`);
945
- }
946
- else if (attempt === 150) {
947
- // ~5min
948
- s.message(`Still waiting... Reopen the install URL if you closed it:\n${oauthUrl}`);
949
- }
950
- try {
951
- const resp = await fetch(pollUrl.toString());
952
- if (resp.ok) {
953
- const data = (await resp.json());
954
- if (data.guild_id) {
955
- guildId = data.guild_id;
956
- break;
929
+ note(`Open this URL to install the Kimaki bot in your Discord server:\n\n${oauthUrl}\n\nDo not share this URL with anyone — it contains your credentials.\n\nIf you don't have a server, create one first (+ button in the Discord sidebar).`, 'Install Bot');
930
+ // Open URL in default browser
931
+ const { exec } = await import('node:child_process');
932
+ const openCmd = process.platform === 'darwin'
933
+ ? 'open'
934
+ : process.platform === 'win32'
935
+ ? 'start'
936
+ : 'xdg-open';
937
+ exec(`${openCmd} "${oauthUrl}"`);
938
+ // Poll until the user installs the bot in a Discord server.
939
+ // 600 attempts x 2s = 20 minutes timeout.
940
+ const s = spinner();
941
+ s.start('Waiting for a Discord server with the bot installed...');
942
+ const pollUrl = new URL('/api/onboarding/status', KIMAKI_WEBSITE_URL);
943
+ pollUrl.searchParams.set('client_id', clientId);
944
+ pollUrl.searchParams.set('secret', clientSecret);
945
+ let guildId;
946
+ for (let attempt = 0; attempt < 600; attempt++) {
947
+ await new Promise((resolve) => {
948
+ setTimeout(resolve, 2000);
949
+ });
950
+ // Progressive hints for users who may be stuck
951
+ if (attempt === 15) {
952
+ // ~30s
953
+ s.message('Still waiting... Select a server in the Discord authorization page and click "Authorize"');
954
+ }
955
+ else if (attempt === 45) {
956
+ // ~90s
957
+ s.message(`Still waiting... If you don't see any servers, create one first (+ button in Discord sidebar), then reopen the URL above`);
958
+ }
959
+ else if (attempt === 150) {
960
+ // ~5min
961
+ s.message(`Still waiting... Reopen the install URL if you closed it:\n${oauthUrl}`);
962
+ }
963
+ try {
964
+ const resp = await fetch(pollUrl.toString());
965
+ if (resp.ok) {
966
+ const data = (await resp.json());
967
+ if (data.guild_id) {
968
+ guildId = data.guild_id;
969
+ break;
970
+ }
957
971
  }
958
972
  }
973
+ catch {
974
+ // Network error, retry
975
+ }
959
976
  }
960
- catch {
961
- // Network error, retry
977
+ if (!guildId) {
978
+ s.stop('Authorization timed out');
979
+ cliLogger.error('Bot authorization timed out after 20 minutes. Please try again.');
980
+ process.exit(EXIT_NO_RESTART);
962
981
  }
982
+ s.stop('Bot authorized successfully!');
963
983
  }
964
- if (!guildId) {
965
- s.stop('Authorization timed out');
966
- cliLogger.error('Bot authorization timed out after 20 minutes. Please try again.');
967
- process.exit(EXIT_NO_RESTART);
968
- }
969
- s.stop('Bot authorized successfully!');
970
984
  await setBotMode({
971
985
  appId: KIMAKI_SHARED_APP_ID,
972
986
  mode: 'gateway',
@@ -1056,6 +1070,11 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
1056
1070
  // ensures they always get the bot that's actually running.
1057
1071
  await touchBotTokenTimestamp(appId);
1058
1072
  const shouldAddChannels = !isQuickStart || forceRestartOnboarding || Boolean(addChannels);
1073
+ store.setState({
1074
+ discordBaseUrl: isGatewayMode
1075
+ ? KIMAKI_GATEWAY_PROXY_REST_BASE_URL
1076
+ : 'https://discord.com',
1077
+ });
1059
1078
  // Start OpenCode server EARLY - let it initialize in parallel with Discord login.
1060
1079
  // This is the biggest startup bottleneck (can take 1-30 seconds to spawn and wait for ready)
1061
1080
  const currentDir = process.cwd();
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@brianli/kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.73-brianli.1",
5
+ "version": "0.4.73-brianli.3",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
8
  "files": [
@@ -22,8 +22,8 @@
22
22
  "prisma": "7.3.0",
23
23
  "tsx": "^4.20.5",
24
24
  "discord-digital-twin": "^0.0.1",
25
- "db": "^0.0.0",
26
25
  "opencode-cached-provider": "^0.0.1",
26
+ "db": "^0.0.0",
27
27
  "opencode-deterministic-provider": "^0.0.1"
28
28
  },
29
29
  "dependencies": {
@@ -57,8 +57,8 @@
57
57
  "xdg-basedir": "^5.1.0",
58
58
  "zod": "^4.3.6",
59
59
  "zustand": "^5.0.11",
60
- "errore": "^0.14.0",
61
- "traforo": "^0.0.9"
60
+ "traforo": "^0.0.9",
61
+ "errore": "^0.14.0"
62
62
  },
63
63
  "optionalDependencies": {
64
64
  "@discordjs/opus": "^0.10.0",
package/src/cli.ts CHANGED
@@ -133,6 +133,8 @@ const KIMAKI_GATEWAY_PROXY_URL =
133
133
  const KIMAKI_GATEWAY_PROXY_REST_BASE_URL = getGatewayProxyRestBaseUrl({
134
134
  gatewayUrl: KIMAKI_GATEWAY_PROXY_URL,
135
135
  })
136
+ const KIMAKI_CLIENT_ID = process.env.KIMAKI_CLIENT_ID?.trim() || undefined
137
+ const KIMAKI_CLIENT_SECRET = process.env.KIMAKI_CLIENT_SECRET?.trim() || undefined
136
138
 
137
139
  // Strip bracketed paste escape sequences from terminal input.
138
140
  // iTerm2 and other terminals wrap pasted content with \x1b[200~ and \x1b[201~
@@ -1303,89 +1305,105 @@ async function run({
1303
1305
  process.exit(EXIT_NO_RESTART)
1304
1306
  }
1305
1307
 
1306
- // Generate client credentials
1307
- const clientId = crypto.randomUUID()
1308
- const clientSecret = crypto.randomBytes(32).toString('hex')
1308
+ const hasGatewayClientId = Boolean(KIMAKI_CLIENT_ID)
1309
+ const hasGatewayClientSecret = Boolean(KIMAKI_CLIENT_SECRET)
1310
+ if (hasGatewayClientId !== hasGatewayClientSecret) {
1311
+ cliLogger.error(
1312
+ 'Gateway credential env vars must be set together: KIMAKI_CLIENT_ID and KIMAKI_CLIENT_SECRET.',
1313
+ )
1314
+ process.exit(EXIT_NO_RESTART)
1315
+ }
1316
+ const clientId = KIMAKI_CLIENT_ID || crypto.randomUUID()
1317
+ const clientSecret = KIMAKI_CLIENT_SECRET
1318
+ || crypto.randomBytes(32).toString('hex')
1319
+ const hasGatewayEnvCredentials = hasGatewayClientId && hasGatewayClientSecret
1320
+ if (hasGatewayEnvCredentials) {
1321
+ cliLogger.log(
1322
+ 'Using KIMAKI_CLIENT_ID and KIMAKI_CLIENT_SECRET from environment for gateway mode. Skipping OAuth onboarding.',
1323
+ )
1324
+ }
1309
1325
 
1310
- const statePayload = JSON.stringify({ clientId, clientSecret } satisfies GatewayOAuthState)
1311
- const oauthUrl = generateBotInstallUrl({
1312
- clientId: KIMAKI_SHARED_APP_ID,
1313
- state: statePayload,
1314
- redirectUri: `${KIMAKI_WEBSITE_URL}/api/auth/callback/discord`,
1315
- responseType: 'code',
1316
- })
1326
+ if (!hasGatewayEnvCredentials) {
1327
+ const statePayload = JSON.stringify({ clientId, clientSecret } satisfies GatewayOAuthState)
1328
+ const oauthUrl = generateBotInstallUrl({
1329
+ clientId: KIMAKI_SHARED_APP_ID,
1330
+ state: statePayload,
1331
+ redirectUri: `${KIMAKI_WEBSITE_URL}/api/auth/callback/discord`,
1332
+ responseType: 'code',
1333
+ })
1317
1334
 
1318
- note(
1319
- `Open this URL to install the Kimaki bot in your Discord server:\n\n${oauthUrl}\n\nDo not share this URL with anyone — it contains your credentials.\n\nIf you don't have a server, create one first (+ button in the Discord sidebar).`,
1320
- 'Install Bot',
1321
- )
1335
+ note(
1336
+ `Open this URL to install the Kimaki bot in your Discord server:\n\n${oauthUrl}\n\nDo not share this URL with anyone — it contains your credentials.\n\nIf you don't have a server, create one first (+ button in the Discord sidebar).`,
1337
+ 'Install Bot',
1338
+ )
1322
1339
 
1323
- // Open URL in default browser
1324
- const { exec } = await import('node:child_process')
1325
- const openCmd =
1326
- process.platform === 'darwin'
1327
- ? 'open'
1328
- : process.platform === 'win32'
1329
- ? 'start'
1330
- : 'xdg-open'
1331
- exec(`${openCmd} "${oauthUrl}"`)
1332
-
1333
- // Poll until the user installs the bot in a Discord server.
1334
- // 600 attempts x 2s = 20 minutes timeout.
1335
- const s = spinner()
1336
- s.start('Waiting for a Discord server with the bot installed...')
1337
-
1338
- const pollUrl = new URL('/api/onboarding/status', KIMAKI_WEBSITE_URL)
1339
- pollUrl.searchParams.set('client_id', clientId)
1340
- pollUrl.searchParams.set('secret', clientSecret)
1341
-
1342
- let guildId: string | undefined
1343
- for (let attempt = 0; attempt < 600; attempt++) {
1344
- await new Promise((resolve) => {
1345
- setTimeout(resolve, 2000)
1346
- })
1340
+ // Open URL in default browser
1341
+ const { exec } = await import('node:child_process')
1342
+ const openCmd =
1343
+ process.platform === 'darwin'
1344
+ ? 'open'
1345
+ : process.platform === 'win32'
1346
+ ? 'start'
1347
+ : 'xdg-open'
1348
+ exec(`${openCmd} "${oauthUrl}"`)
1349
+
1350
+ // Poll until the user installs the bot in a Discord server.
1351
+ // 600 attempts x 2s = 20 minutes timeout.
1352
+ const s = spinner()
1353
+ s.start('Waiting for a Discord server with the bot installed...')
1354
+
1355
+ const pollUrl = new URL('/api/onboarding/status', KIMAKI_WEBSITE_URL)
1356
+ pollUrl.searchParams.set('client_id', clientId)
1357
+ pollUrl.searchParams.set('secret', clientSecret)
1358
+
1359
+ let guildId: string | undefined
1360
+ for (let attempt = 0; attempt < 600; attempt++) {
1361
+ await new Promise((resolve) => {
1362
+ setTimeout(resolve, 2000)
1363
+ })
1347
1364
 
1348
- // Progressive hints for users who may be stuck
1349
- if (attempt === 15) {
1350
- // ~30s
1351
- s.message(
1352
- 'Still waiting... Select a server in the Discord authorization page and click "Authorize"',
1353
- )
1354
- } else if (attempt === 45) {
1355
- // ~90s
1356
- s.message(
1357
- `Still waiting... If you don't see any servers, create one first (+ button in Discord sidebar), then reopen the URL above`,
1358
- )
1359
- } else if (attempt === 150) {
1360
- // ~5min
1361
- s.message(
1362
- `Still waiting... Reopen the install URL if you closed it:\n${oauthUrl}`,
1363
- )
1364
- }
1365
+ // Progressive hints for users who may be stuck
1366
+ if (attempt === 15) {
1367
+ // ~30s
1368
+ s.message(
1369
+ 'Still waiting... Select a server in the Discord authorization page and click "Authorize"',
1370
+ )
1371
+ } else if (attempt === 45) {
1372
+ // ~90s
1373
+ s.message(
1374
+ `Still waiting... If you don't see any servers, create one first (+ button in Discord sidebar), then reopen the URL above`,
1375
+ )
1376
+ } else if (attempt === 150) {
1377
+ // ~5min
1378
+ s.message(
1379
+ `Still waiting... Reopen the install URL if you closed it:\n${oauthUrl}`,
1380
+ )
1381
+ }
1365
1382
 
1366
- try {
1367
- const resp = await fetch(pollUrl.toString())
1368
- if (resp.ok) {
1369
- const data = (await resp.json()) as { guild_id?: string }
1370
- if (data.guild_id) {
1371
- guildId = data.guild_id
1372
- break
1383
+ try {
1384
+ const resp = await fetch(pollUrl.toString())
1385
+ if (resp.ok) {
1386
+ const data = (await resp.json()) as { guild_id?: string }
1387
+ if (data.guild_id) {
1388
+ guildId = data.guild_id
1389
+ break
1390
+ }
1373
1391
  }
1392
+ } catch {
1393
+ // Network error, retry
1374
1394
  }
1375
- } catch {
1376
- // Network error, retry
1377
1395
  }
1378
- }
1379
1396
 
1380
- if (!guildId) {
1381
- s.stop('Authorization timed out')
1382
- cliLogger.error(
1383
- 'Bot authorization timed out after 20 minutes. Please try again.',
1384
- )
1385
- process.exit(EXIT_NO_RESTART)
1386
- }
1397
+ if (!guildId) {
1398
+ s.stop('Authorization timed out')
1399
+ cliLogger.error(
1400
+ 'Bot authorization timed out after 20 minutes. Please try again.',
1401
+ )
1402
+ process.exit(EXIT_NO_RESTART)
1403
+ }
1387
1404
 
1388
- s.stop('Bot authorized successfully!')
1405
+ s.stop('Bot authorized successfully!')
1406
+ }
1389
1407
 
1390
1408
  await setBotMode({
1391
1409
  appId: KIMAKI_SHARED_APP_ID,
@@ -1505,6 +1523,12 @@ async function run({
1505
1523
  const shouldAddChannels =
1506
1524
  !isQuickStart || forceRestartOnboarding || Boolean(addChannels)
1507
1525
 
1526
+ store.setState({
1527
+ discordBaseUrl: isGatewayMode
1528
+ ? KIMAKI_GATEWAY_PROXY_REST_BASE_URL
1529
+ : 'https://discord.com',
1530
+ })
1531
+
1508
1532
  // Start OpenCode server EARLY - let it initialize in parallel with Discord login.
1509
1533
  // This is the biggest startup bottleneck (can take 1-30 seconds to spawn and wait for ready)
1510
1534
  const currentDir = process.cwd()