onkol 0.5.0 → 0.5.2

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/index.js CHANGED
@@ -570,32 +570,45 @@ program
570
570
  console.log(chalk.cyan('[1/3] Updating files from npm package...'));
571
571
  try {
572
572
  // Find where this CLI is running from — that's the latest package
573
+ // __dirname is dist/cli/, so pkgRoot is the npm package root
573
574
  const pkgRoot = resolve(__dirname, '..');
574
- const pluginSrc = existsSync(resolve(pkgRoot, 'src/plugin'))
575
- ? resolve(pkgRoot, 'src/plugin')
576
- : resolve(pkgRoot, 'dist/plugin');
577
- const scriptsSrc = resolve(pkgRoot, 'scripts');
578
- // Copy plugin files
579
- if (existsSync(pluginSrc)) {
580
- const pluginDest = resolve(dir, 'plugins/discord-filtered');
581
- const { readdirSync } = await import('fs');
582
- for (const f of readdirSync(pluginSrc)) {
583
- if (f.endsWith('.ts') || f.endsWith('.js')) {
584
- copyFileSync(resolve(pluginSrc, f), resolve(pluginDest, f));
575
+ const { readdirSync, chmodSync } = await import('fs');
576
+ // Try src/plugin first (has .ts files), then dist/plugin (.js files)
577
+ let pluginUpdated = false;
578
+ for (const candidate of ['src/plugin', 'dist/plugin']) {
579
+ const pluginSrc = resolve(pkgRoot, candidate);
580
+ if (existsSync(pluginSrc)) {
581
+ const pluginDest = resolve(dir, 'plugins/discord-filtered');
582
+ mkdirSync(pluginDest, { recursive: true });
583
+ for (const f of readdirSync(pluginSrc)) {
584
+ if (f.endsWith('.ts') || f.endsWith('.js')) {
585
+ copyFileSync(resolve(pluginSrc, f), resolve(pluginDest, f));
586
+ }
585
587
  }
588
+ console.log(chalk.green(` ✓ Plugin files updated (from ${candidate})`));
589
+ pluginUpdated = true;
590
+ break;
586
591
  }
587
- console.log(chalk.green(' ✓ Plugin files updated'));
592
+ }
593
+ if (!pluginUpdated) {
594
+ console.log(chalk.yellow(` ⚠ No plugin source found in package (looked in ${pkgRoot})`));
588
595
  }
589
596
  // Copy scripts
597
+ const scriptsSrc = resolve(pkgRoot, 'scripts');
590
598
  if (existsSync(scriptsSrc)) {
591
- const { readdirSync, chmodSync } = await import('fs');
599
+ mkdirSync(resolve(dir, 'scripts'), { recursive: true });
600
+ let count = 0;
592
601
  for (const f of readdirSync(scriptsSrc)) {
593
602
  if (f.endsWith('.sh')) {
594
603
  copyFileSync(resolve(scriptsSrc, f), resolve(dir, 'scripts', f));
595
604
  chmodSync(resolve(dir, 'scripts', f), 0o755);
605
+ count++;
596
606
  }
597
607
  }
598
- console.log(chalk.green('Scripts updated'));
608
+ console.log(chalk.green(`${count} scripts updated`));
609
+ }
610
+ else {
611
+ console.log(chalk.yellow(` ⚠ No scripts dir found at ${scriptsSrc}`));
599
612
  }
600
613
  }
601
614
  catch (err) {
@@ -665,11 +678,17 @@ program
665
678
  --intent "${w.intent}" \
666
679
  ${resumeArg}`;
667
680
  try {
668
- execSync(cmd, { stdio: 'pipe' });
681
+ const output = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
669
682
  console.log(chalk.green(` ✓ ${w.name} respawned${w.sessionId ? ' (resumed)' : ''}`));
683
+ if (output.trim())
684
+ console.log(chalk.gray(` ${output.trim()}`));
670
685
  }
671
686
  catch (err) {
672
- console.log(chalk.red(` ✗ Failed to spawn ${w.name}: ${err instanceof Error ? err.message : err}`));
687
+ console.log(chalk.red(` ✗ Failed to spawn ${w.name}`));
688
+ if (err.stderr)
689
+ console.log(chalk.red(` stderr: ${err.stderr.toString().trim()}`));
690
+ if (err.stdout)
691
+ console.log(chalk.gray(` stdout: ${err.stdout.toString().trim()}`));
673
692
  }
674
693
  // Small delay to avoid Discord rate limits
675
694
  await new Promise(r => setTimeout(r, 2000));
@@ -1,4 +1,10 @@
1
1
  import { Client, GatewayIntentBits } from 'discord.js';
2
+ import { mkdirSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg']);
6
+ const ATTACHMENT_DIR = join(tmpdir(), 'onkol-attachments');
7
+ mkdirSync(ATTACHMENT_DIR, { recursive: true });
2
8
  export function shouldForwardMessage(messageChannelId, authorId, isBot, targetChannelId, allowedUsers) {
3
9
  if (isBot)
4
10
  return false;
@@ -8,17 +14,38 @@ export function shouldForwardMessage(messageChannelId, authorId, isBot, targetCh
8
14
  return false;
9
15
  return true;
10
16
  }
11
- // When a message is too long, Discord auto-converts it to a .txt file attachment
12
- // with empty message content. This fetches the text from those attachments.
13
- async function resolveTextAttachments(message) {
17
+ function isTextAttachment(a) {
18
+ return a.contentType?.startsWith('text/') === true || a.name?.endsWith('.txt') === true;
19
+ }
20
+ function isImageAttachment(a) {
21
+ if (a.contentType?.startsWith('image/'))
22
+ return true;
23
+ const name = (a.name || '').toLowerCase();
24
+ return IMAGE_EXTENSIONS.has(name.slice(name.lastIndexOf('.')));
25
+ }
26
+ // Resolve all attachments: text gets inlined, images get downloaded to temp files
27
+ async function resolveAttachments(message) {
14
28
  let content = message.content;
15
- const textAttachments = message.attachments.filter((a) => a.contentType?.startsWith('text/') || a.name?.endsWith('.txt'));
16
- for (const attachment of textAttachments.values()) {
29
+ for (const attachment of message.attachments.values()) {
17
30
  try {
18
- const res = await fetch(attachment.url);
19
- if (res.ok) {
20
- const text = await res.text();
21
- content = content ? `${content}\n\n${text}` : text;
31
+ if (isTextAttachment(attachment)) {
32
+ const res = await fetch(attachment.url);
33
+ if (res.ok) {
34
+ const text = await res.text();
35
+ content = content ? `${content}\n\n${text}` : text;
36
+ }
37
+ }
38
+ else if (isImageAttachment(attachment)) {
39
+ const res = await fetch(attachment.url);
40
+ if (res.ok) {
41
+ const buffer = Buffer.from(await res.arrayBuffer());
42
+ const filename = `${message.id}-${attachment.name || 'image.png'}`;
43
+ const filepath = join(ATTACHMENT_DIR, filename);
44
+ writeFileSync(filepath, buffer);
45
+ const note = `[User sent an image: ${attachment.name || 'image'}. Saved to ${filepath} — use the Read tool to view it.]`;
46
+ content = content ? `${content}\n\n${note}` : note;
47
+ console.error(`[discord-filtered] Downloaded image: ${filepath} (${buffer.length} bytes)`);
48
+ }
22
49
  }
23
50
  }
24
51
  catch (err) {
@@ -37,7 +64,7 @@ export function createDiscordClient(config, onMessage) {
37
64
  });
38
65
  client.on('messageCreate', async (message) => {
39
66
  if (shouldForwardMessage(message.channel.id, message.author.id, message.author.bot, config.channelId, config.allowedUsers)) {
40
- const content = await resolveTextAttachments(message);
67
+ const content = await resolveAttachments(message);
41
68
  if (content) {
42
69
  onMessage(content, message);
43
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onkol",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Decentralized on-call agent system powered by Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,11 @@
1
1
  import { Client, GatewayIntentBits, type Message, type Attachment } from 'discord.js'
2
+ import { mkdirSync, writeFileSync } from 'fs'
3
+ import { join } from 'path'
4
+ import { tmpdir } from 'os'
5
+
6
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'])
7
+ const ATTACHMENT_DIR = join(tmpdir(), 'onkol-attachments')
8
+ mkdirSync(ATTACHMENT_DIR, { recursive: true })
2
9
 
3
10
  export interface DiscordClientConfig {
4
11
  botToken: string
@@ -19,24 +26,45 @@ export function shouldForwardMessage(
19
26
  return true
20
27
  }
21
28
 
22
- // When a message is too long, Discord auto-converts it to a .txt file attachment
23
- // with empty message content. This fetches the text from those attachments.
24
- async function resolveTextAttachments(message: Message): Promise<string> {
29
+ function isTextAttachment(a: Attachment): boolean {
30
+ return a.contentType?.startsWith('text/') === true || a.name?.endsWith('.txt') === true
31
+ }
32
+
33
+ function isImageAttachment(a: Attachment): boolean {
34
+ if (a.contentType?.startsWith('image/')) return true
35
+ const name = (a.name || '').toLowerCase()
36
+ return IMAGE_EXTENSIONS.has(name.slice(name.lastIndexOf('.')))
37
+ }
38
+
39
+ // Resolve all attachments: text gets inlined, images get downloaded to temp files
40
+ async function resolveAttachments(message: Message): Promise<string> {
25
41
  let content = message.content
26
- const textAttachments = message.attachments.filter(
27
- (a: Attachment) => a.contentType?.startsWith('text/') || a.name?.endsWith('.txt')
28
- )
29
- for (const attachment of textAttachments.values()) {
42
+
43
+ for (const attachment of message.attachments.values()) {
30
44
  try {
31
- const res = await fetch(attachment.url)
32
- if (res.ok) {
33
- const text = await res.text()
34
- content = content ? `${content}\n\n${text}` : text
45
+ if (isTextAttachment(attachment)) {
46
+ const res = await fetch(attachment.url)
47
+ if (res.ok) {
48
+ const text = await res.text()
49
+ content = content ? `${content}\n\n${text}` : text
50
+ }
51
+ } else if (isImageAttachment(attachment)) {
52
+ const res = await fetch(attachment.url)
53
+ if (res.ok) {
54
+ const buffer = Buffer.from(await res.arrayBuffer())
55
+ const filename = `${message.id}-${attachment.name || 'image.png'}`
56
+ const filepath = join(ATTACHMENT_DIR, filename)
57
+ writeFileSync(filepath, buffer)
58
+ const note = `[User sent an image: ${attachment.name || 'image'}. Saved to ${filepath} — use the Read tool to view it.]`
59
+ content = content ? `${content}\n\n${note}` : note
60
+ console.error(`[discord-filtered] Downloaded image: ${filepath} (${buffer.length} bytes)`)
61
+ }
35
62
  }
36
63
  } catch (err) {
37
64
  console.error(`[discord-filtered] Failed to fetch attachment ${attachment.name}: ${err}`)
38
65
  }
39
66
  }
67
+
40
68
  return content
41
69
  }
42
70
 
@@ -62,7 +90,7 @@ export function createDiscordClient(
62
90
  config.allowedUsers
63
91
  )
64
92
  ) {
65
- const content = await resolveTextAttachments(message)
93
+ const content = await resolveAttachments(message)
66
94
  if (content) {
67
95
  onMessage(content, message)
68
96
  }