kimaki 0.4.84 → 0.4.85

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.
@@ -185,7 +185,11 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
185
185
  await setupHandlers(discordClient);
186
186
  }
187
187
  else {
188
- discordClient.once(Events.ClientReady, setupHandlers);
188
+ discordClient.once(Events.ClientReady, (readyClient) => {
189
+ void setupHandlers(readyClient).catch((error) => {
190
+ discordLogger.error(`[GATEWAY] ClientReady handler failed: ${formatErrorWithStack(error)}`);
191
+ });
192
+ });
189
193
  }
190
194
  discordClient.on(Events.Error, (error) => {
191
195
  discordLogger.error('[GATEWAY] Client error:', formatErrorWithStack(error));
@@ -19,7 +19,7 @@ function isSyntheticTextPart(part) {
19
19
  return candidate.synthetic === true;
20
20
  }
21
21
  function parseDiscordOriginMetadata(text) {
22
- const match = text.match(/^<discord-user\s+([^>]+)\s*\/>$/);
22
+ const match = text.match(/<discord-user\s+([^>]+)\s*\/>/);
23
23
  if (!match?.[1]) {
24
24
  return null;
25
25
  }
@@ -42,17 +42,17 @@ function parseDiscordOriginMetadata(text) {
42
42
  };
43
43
  }
44
44
  function getDiscordOriginMetadataFromMessage({ message, }) {
45
- const syntheticTexts = message.parts.flatMap((part) => {
46
- if (part.type !== 'text') {
47
- return [];
48
- }
49
- if (!isSyntheticTextPart(part)) {
50
- return [];
51
- }
52
- return [part.text || ''];
45
+ const textParts = message.parts.filter((p) => {
46
+ return p.type === 'text';
53
47
  });
54
- for (const text of syntheticTexts) {
55
- const metadata = parseDiscordOriginMetadata(text);
48
+ // Synthetic parts first (normal promptAsync path), then non-synthetic
49
+ // (session.command() path where the tag is embedded in arguments text).
50
+ const sorted = [
51
+ ...textParts.filter((p) => { return isSyntheticTextPart(p); }),
52
+ ...textParts.filter((p) => { return !isSyntheticTextPart(p); }),
53
+ ];
54
+ for (const part of sorted) {
55
+ const metadata = parseDiscordOriginMetadata(part.text || '');
56
56
  if (metadata) {
57
57
  return metadata;
58
58
  }
@@ -2667,12 +2667,18 @@ export class ThreadSessionRuntime {
2667
2667
  if (input.command) {
2668
2668
  const queuedCommand = input.command;
2669
2669
  const commandSignal = AbortSignal.timeout(30_000);
2670
+ // session.command() only accepts FilePart in parts, not text parts.
2671
+ // Append <discord-user /> tag to arguments so external sync can
2672
+ // detect this message came from Discord (same tag as promptAsync).
2673
+ const discordTag = input.username
2674
+ ? `\n<discord-user name="${input.username}" />`
2675
+ : '';
2670
2676
  const commandResponse = await errore.tryAsync(() => {
2671
2677
  return getClient().session.command({
2672
2678
  sessionID: session.id,
2673
2679
  directory: this.sdkDirectory,
2674
2680
  command: queuedCommand.name,
2675
- arguments: queuedCommand.arguments,
2681
+ arguments: queuedCommand.arguments + discordTag,
2676
2682
  agent: earlyAgentPreference,
2677
2683
  ...variantField,
2678
2684
  }, { signal: commandSignal });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.84",
5
+ "version": "0.4.85",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
8
  "files": [
@@ -25,8 +25,8 @@
25
25
  "prisma": "7.4.2",
26
26
  "tsx": "^4.20.5",
27
27
  "discord-digital-twin": "^0.1.0",
28
- "opencode-cached-provider": "^0.0.1",
29
28
  "opencode-deterministic-provider": "^0.0.1",
29
+ "opencode-cached-provider": "^0.0.1",
30
30
  "db": "^0.0.0"
31
31
  },
32
32
  "dependencies": {
@@ -316,7 +316,13 @@ export async function startDiscordBot({
316
316
  if (discordClient.isReady()) {
317
317
  await setupHandlers(discordClient)
318
318
  } else {
319
- discordClient.once(Events.ClientReady, setupHandlers)
319
+ discordClient.once(Events.ClientReady, (readyClient) => {
320
+ void setupHandlers(readyClient).catch((error) => {
321
+ discordLogger.error(
322
+ `[GATEWAY] ClientReady handler failed: ${formatErrorWithStack(error)}`,
323
+ )
324
+ })
325
+ })
320
326
  }
321
327
 
322
328
  discordClient.on(Events.Error, (error) => {
@@ -86,7 +86,7 @@ function isSyntheticTextPart(part: Extract<Part, { type: 'text' }>): boolean {
86
86
  }
87
87
 
88
88
  function parseDiscordOriginMetadata(text: string): DiscordOriginMetadata | null {
89
- const match = text.match(/^<discord-user\s+([^>]+)\s*\/>$/)
89
+ const match = text.match(/<discord-user\s+([^>]+)\s*\/>/)
90
90
  if (!match?.[1]) {
91
91
  return null
92
92
  }
@@ -117,23 +117,21 @@ function getDiscordOriginMetadataFromMessage({
117
117
  }: {
118
118
  message: SessionMessageLike
119
119
  }): DiscordOriginMetadata | null {
120
- const syntheticTexts = message.parts.flatMap((part) => {
121
- if (part.type !== 'text') {
122
- return [] as string[]
123
- }
124
- if (!isSyntheticTextPart(part)) {
125
- return [] as string[]
126
- }
127
- return [part.text || '']
120
+ const textParts = message.parts.filter((p): p is Extract<typeof p, { type: 'text' }> => {
121
+ return p.type === 'text'
128
122
  })
129
-
130
- for (const text of syntheticTexts) {
131
- const metadata = parseDiscordOriginMetadata(text)
123
+ // Synthetic parts first (normal promptAsync path), then non-synthetic
124
+ // (session.command() path where the tag is embedded in arguments text).
125
+ const sorted = [
126
+ ...textParts.filter((p) => { return isSyntheticTextPart(p) }),
127
+ ...textParts.filter((p) => { return !isSyntheticTextPart(p) }),
128
+ ]
129
+ for (const part of sorted) {
130
+ const metadata = parseDiscordOriginMetadata(part.text || '')
132
131
  if (metadata) {
133
132
  return metadata
134
133
  }
135
134
  }
136
-
137
135
  return null
138
136
  }
139
137
 
@@ -3510,13 +3510,20 @@ export class ThreadSessionRuntime {
3510
3510
  if (input.command) {
3511
3511
  const queuedCommand = input.command
3512
3512
  const commandSignal = AbortSignal.timeout(30_000)
3513
+ // session.command() only accepts FilePart in parts, not text parts.
3514
+ // Append <discord-user /> tag to arguments so external sync can
3515
+ // detect this message came from Discord (same tag as promptAsync).
3516
+ const discordTag = input.username
3517
+ ? `\n<discord-user name="${input.username}" />`
3518
+ : ''
3513
3519
  const commandResponse = await errore.tryAsync(() => {
3514
3520
  return getClient().session.command(
3515
3521
  {
3516
3522
  sessionID: session.id,
3523
+
3517
3524
  directory: this.sdkDirectory,
3518
3525
  command: queuedCommand.name,
3519
- arguments: queuedCommand.arguments,
3526
+ arguments: queuedCommand.arguments + discordTag,
3520
3527
  agent: earlyAgentPreference,
3521
3528
  ...variantField,
3522
3529
  },