@brianli/kimaki 0.4.73-brianli.2 → 0.4.73-brianli.5

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/cli.js CHANGED
@@ -874,6 +874,28 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
874
874
  else if (forceRestartOnboarding && existingBot) {
875
875
  note('Ignoring saved credentials due to --restart-onboarding flag', 'Restart Onboarding');
876
876
  }
877
+ // 3b. Gateway env vars present — skip all interactive prompts entirely.
878
+ if (KIMAKI_CLIENT_ID && KIMAKI_CLIENT_SECRET && !forceRestartOnboarding) {
879
+ if (!KIMAKI_SHARED_APP_ID) {
880
+ cliLogger.error('Gateway mode is not available yet. KIMAKI_SHARED_APP_ID is not configured.');
881
+ process.exit(EXIT_NO_RESTART);
882
+ }
883
+ cliLogger.log('Using KIMAKI_CLIENT_ID and KIMAKI_CLIENT_SECRET from environment. Skipping onboarding.');
884
+ await setBotMode({
885
+ appId: KIMAKI_SHARED_APP_ID,
886
+ mode: 'gateway',
887
+ clientId: KIMAKI_CLIENT_ID,
888
+ clientSecret: KIMAKI_CLIENT_SECRET,
889
+ proxyUrl: KIMAKI_GATEWAY_PROXY_REST_BASE_URL,
890
+ });
891
+ return {
892
+ appId: KIMAKI_SHARED_APP_ID,
893
+ token: `${KIMAKI_CLIENT_ID}:${KIMAKI_CLIENT_SECRET}`,
894
+ isQuickStart: false,
895
+ isGatewayMode: true,
896
+ previousAppId: existingBot?.appId,
897
+ };
898
+ }
877
899
  // When --gateway is passed, skip the mode selector and go straight to gateway mode.
878
900
  const modeChoice = forceGateway
879
901
  ? 'gateway'
@@ -914,70 +936,73 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
914
936
  const clientId = KIMAKI_CLIENT_ID || crypto.randomUUID();
915
937
  const clientSecret = KIMAKI_CLIENT_SECRET
916
938
  || crypto.randomBytes(32).toString('hex');
917
- if (KIMAKI_CLIENT_ID && KIMAKI_CLIENT_SECRET) {
918
- cliLogger.log('Using KIMAKI_CLIENT_ID and KIMAKI_CLIENT_SECRET from environment for gateway mode.');
919
- }
920
- const statePayload = JSON.stringify({ clientId, clientSecret });
921
- const oauthUrl = generateBotInstallUrl({
922
- clientId: KIMAKI_SHARED_APP_ID,
923
- state: statePayload,
924
- redirectUri: `${KIMAKI_WEBSITE_URL}/api/auth/callback/discord`,
925
- responseType: 'code',
926
- });
927
- 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');
928
- // Open URL in default browser
929
- const { exec } = await import('node:child_process');
930
- const openCmd = process.platform === 'darwin'
931
- ? 'open'
932
- : process.platform === 'win32'
933
- ? 'start'
934
- : 'xdg-open';
935
- exec(`${openCmd} "${oauthUrl}"`);
936
- // Poll until the user installs the bot in a Discord server.
937
- // 600 attempts x 2s = 20 minutes timeout.
938
- const s = spinner();
939
- s.start('Waiting for a Discord server with the bot installed...');
940
- const pollUrl = new URL('/api/onboarding/status', KIMAKI_WEBSITE_URL);
941
- pollUrl.searchParams.set('client_id', clientId);
942
- pollUrl.searchParams.set('secret', clientSecret);
943
- let guildId;
944
- for (let attempt = 0; attempt < 600; attempt++) {
945
- await new Promise((resolve) => {
946
- setTimeout(resolve, 2000);
939
+ const hasGatewayEnvCredentials = hasGatewayClientId && hasGatewayClientSecret;
940
+ if (hasGatewayEnvCredentials) {
941
+ cliLogger.log('Using KIMAKI_CLIENT_ID and KIMAKI_CLIENT_SECRET from environment for gateway mode. Skipping OAuth onboarding.');
942
+ }
943
+ if (!hasGatewayEnvCredentials) {
944
+ const statePayload = JSON.stringify({ clientId, clientSecret });
945
+ const oauthUrl = generateBotInstallUrl({
946
+ clientId: KIMAKI_SHARED_APP_ID,
947
+ state: statePayload,
948
+ redirectUri: `${KIMAKI_WEBSITE_URL}/api/auth/callback/discord`,
949
+ responseType: 'code',
947
950
  });
948
- // Progressive hints for users who may be stuck
949
- if (attempt === 15) {
950
- // ~30s
951
- s.message('Still waiting... Select a server in the Discord authorization page and click "Authorize"');
952
- }
953
- else if (attempt === 45) {
954
- // ~90s
955
- s.message(`Still waiting... If you don't see any servers, create one first (+ button in Discord sidebar), then reopen the URL above`);
956
- }
957
- else if (attempt === 150) {
958
- // ~5min
959
- s.message(`Still waiting... Reopen the install URL if you closed it:\n${oauthUrl}`);
960
- }
961
- try {
962
- const resp = await fetch(pollUrl.toString());
963
- if (resp.ok) {
964
- const data = (await resp.json());
965
- if (data.guild_id) {
966
- guildId = data.guild_id;
967
- break;
951
+ 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');
952
+ // Open URL in default browser
953
+ const { exec } = await import('node:child_process');
954
+ const openCmd = process.platform === 'darwin'
955
+ ? 'open'
956
+ : process.platform === 'win32'
957
+ ? 'start'
958
+ : 'xdg-open';
959
+ exec(`${openCmd} "${oauthUrl}"`);
960
+ // Poll until the user installs the bot in a Discord server.
961
+ // 600 attempts x 2s = 20 minutes timeout.
962
+ const s = spinner();
963
+ s.start('Waiting for a Discord server with the bot installed...');
964
+ const pollUrl = new URL('/api/onboarding/status', KIMAKI_WEBSITE_URL);
965
+ pollUrl.searchParams.set('client_id', clientId);
966
+ pollUrl.searchParams.set('secret', clientSecret);
967
+ let guildId;
968
+ for (let attempt = 0; attempt < 600; attempt++) {
969
+ await new Promise((resolve) => {
970
+ setTimeout(resolve, 2000);
971
+ });
972
+ // Progressive hints for users who may be stuck
973
+ if (attempt === 15) {
974
+ // ~30s
975
+ s.message('Still waiting... Select a server in the Discord authorization page and click "Authorize"');
976
+ }
977
+ else if (attempt === 45) {
978
+ // ~90s
979
+ s.message(`Still waiting... If you don't see any servers, create one first (+ button in Discord sidebar), then reopen the URL above`);
980
+ }
981
+ else if (attempt === 150) {
982
+ // ~5min
983
+ s.message(`Still waiting... Reopen the install URL if you closed it:\n${oauthUrl}`);
984
+ }
985
+ try {
986
+ const resp = await fetch(pollUrl.toString());
987
+ if (resp.ok) {
988
+ const data = (await resp.json());
989
+ if (data.guild_id) {
990
+ guildId = data.guild_id;
991
+ break;
992
+ }
968
993
  }
969
994
  }
995
+ catch {
996
+ // Network error, retry
997
+ }
970
998
  }
971
- catch {
972
- // Network error, retry
999
+ if (!guildId) {
1000
+ s.stop('Authorization timed out');
1001
+ cliLogger.error('Bot authorization timed out after 20 minutes. Please try again.');
1002
+ process.exit(EXIT_NO_RESTART);
973
1003
  }
1004
+ s.stop('Bot authorized successfully!');
974
1005
  }
975
- if (!guildId) {
976
- s.stop('Authorization timed out');
977
- cliLogger.error('Bot authorization timed out after 20 minutes. Please try again.');
978
- process.exit(EXIT_NO_RESTART);
979
- }
980
- s.stop('Bot authorized successfully!');
981
1006
  await setBotMode({
982
1007
  appId: KIMAKI_SHARED_APP_ID,
983
1008
  mode: 'gateway',
@@ -1067,6 +1092,11 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
1067
1092
  // ensures they always get the bot that's actually running.
1068
1093
  await touchBotTokenTimestamp(appId);
1069
1094
  const shouldAddChannels = !isQuickStart || forceRestartOnboarding || Boolean(addChannels);
1095
+ store.setState({
1096
+ discordBaseUrl: isGatewayMode
1097
+ ? KIMAKI_GATEWAY_PROXY_REST_BASE_URL
1098
+ : 'https://discord.com',
1099
+ });
1070
1100
  // Start OpenCode server EARLY - let it initialize in parallel with Discord login.
1071
1101
  // This is the biggest startup bottleneck (can take 1-30 seconds to spawn and wait for ready)
1072
1102
  const currentDir = process.cwd();
@@ -0,0 +1,77 @@
1
+ // Debounced async callback with centralized shutdown flushing.
2
+ // Used for persistence paths that should batch writes during runtime
3
+ // while allowing the bot's single SIGTERM/SIGINT handler to flush all callbacks.
4
+ const processFlushCallbacks = new Set();
5
+ export async function flushDebouncedProcessCallbacks() {
6
+ const callbacks = [...processFlushCallbacks];
7
+ await Promise.allSettled(callbacks.map((callback) => {
8
+ return callback();
9
+ }));
10
+ }
11
+ export function createDebouncedProcessFlush({ waitMs, callback, onError, }) {
12
+ let timeout;
13
+ let inFlight;
14
+ let dirty = false;
15
+ async function run() {
16
+ if (!dirty) {
17
+ return;
18
+ }
19
+ if (inFlight) {
20
+ await inFlight;
21
+ if (!dirty) {
22
+ return;
23
+ }
24
+ }
25
+ dirty = false;
26
+ const current = Promise.resolve()
27
+ .then(() => {
28
+ return callback();
29
+ })
30
+ .catch((error) => {
31
+ if (onError) {
32
+ const wrappedError = error instanceof Error
33
+ ? error
34
+ : new Error('Debounced process flush failed', { cause: error });
35
+ onError(wrappedError);
36
+ }
37
+ });
38
+ inFlight = current;
39
+ await current;
40
+ if (inFlight === current) {
41
+ inFlight = undefined;
42
+ }
43
+ if (dirty) {
44
+ await run();
45
+ }
46
+ }
47
+ function trigger() {
48
+ dirty = true;
49
+ if (timeout) {
50
+ return;
51
+ }
52
+ timeout = setTimeout(() => {
53
+ timeout = undefined;
54
+ void run();
55
+ }, waitMs);
56
+ }
57
+ async function flush() {
58
+ if (timeout) {
59
+ clearTimeout(timeout);
60
+ timeout = undefined;
61
+ }
62
+ await run();
63
+ }
64
+ const processFlushCallback = async () => {
65
+ await flush();
66
+ };
67
+ processFlushCallbacks.add(processFlushCallback);
68
+ async function dispose() {
69
+ processFlushCallbacks.delete(processFlushCallback);
70
+ await flush();
71
+ }
72
+ return {
73
+ trigger,
74
+ flush,
75
+ dispose,
76
+ };
77
+ }