@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 +88 -58
- package/dist/debounced-process-flush.js +77 -0
- package/dist/event-stream-real-capture.e2e.test.js +550 -0
- package/dist/generated/client.js +3 -1
- package/dist/generated/internal/class.js +10 -2
- package/dist/generated/internal/prismaNamespace.js +4 -4
- package/dist/generated/models/session_events.js +1 -0
- package/dist/opencode-interrupt-plugin.js +183 -0
- package/dist/opencode-interrupt-plugin.test.js +263 -0
- package/dist/queue-advanced-abort.e2e.test.js +293 -0
- package/dist/queue-advanced-e2e-setup.js +346 -0
- package/dist/queue-advanced-footer.e2e.test.js +298 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +105 -0
- package/dist/queue-advanced-typing.e2e.test.js +162 -0
- package/dist/session-handler/event-stream-state.js +276 -0
- package/dist/session-handler/event-stream-state.test.js +257 -0
- package/package.json +3 -3
- package/src/cli.ts +108 -72
- package/src/generated/client.ts +3 -1
- package/src/generated/internal/class.ts +17 -5
- package/src/generated/internal/prismaNamespace.ts +4 -4
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
clientId
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
-
|
|
972
|
-
|
|
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
|
+
}
|