onkol 0.5.1 → 0.5.4

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,8 +570,8 @@ 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
574
- const pkgRoot = resolve(__dirname, '..');
573
+ // __dirname is dist/cli/, so pkgRoot is two levels up (dist/cli -> dist -> package root)
574
+ const pkgRoot = resolve(__dirname, '../..');
575
575
  const { readdirSync, chmodSync } = await import('fs');
576
576
  // Try src/plugin first (has .ts files), then dist/plugin (.js files)
577
577
  let pluginUpdated = false;
@@ -1,4 +1,12 @@
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 ATTACHMENT_DIR = join(tmpdir(), 'onkol-attachments');
6
+ mkdirSync(ATTACHMENT_DIR, { recursive: true });
7
+ // Small text files get inlined into the message content
8
+ const INLINE_MAX_BYTES = 10_000;
9
+ const INLINE_CONTENT_TYPES = ['text/', 'application/json', 'application/xml', 'application/csv'];
2
10
  export function shouldForwardMessage(messageChannelId, authorId, isBot, targetChannelId, allowedUsers) {
3
11
  if (isBot)
4
12
  return false;
@@ -8,17 +16,37 @@ export function shouldForwardMessage(messageChannelId, authorId, isBot, targetCh
8
16
  return false;
9
17
  return true;
10
18
  }
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) {
19
+ function shouldInline(a) {
20
+ if (a.name?.endsWith('.txt') || a.name?.endsWith('.csv') || a.name?.endsWith('.json') || a.name?.endsWith('.md')) {
21
+ return (a.size || 0) <= INLINE_MAX_BYTES;
22
+ }
23
+ const ct = a.contentType || '';
24
+ return INLINE_CONTENT_TYPES.some(t => ct.startsWith(t)) && (a.size || 0) <= INLINE_MAX_BYTES;
25
+ }
26
+ // Download all attachments: small text gets inlined, everything else saved to disk.
27
+ // Claude Code's Read tool handles images, PDFs, CSVs, notebooks, etc. natively.
28
+ async function resolveAttachments(message) {
14
29
  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()) {
30
+ for (const attachment of message.attachments.values()) {
17
31
  try {
18
32
  const res = await fetch(attachment.url);
19
- if (res.ok) {
33
+ if (!res.ok)
34
+ continue;
35
+ if (shouldInline(attachment)) {
36
+ // Inline small text files directly into the message
20
37
  const text = await res.text();
21
- content = content ? `${content}\n\n${text}` : text;
38
+ const label = attachment.name ? `[${attachment.name}]` : '';
39
+ content = content ? `${content}\n\n${label}\n${text}` : `${label}\n${text}`;
40
+ }
41
+ else {
42
+ // Save to disk — Claude Code can Read images, PDFs, CSVs, etc.
43
+ const buffer = Buffer.from(await res.arrayBuffer());
44
+ const filename = `${message.id}-${attachment.name || 'file'}`;
45
+ const filepath = join(ATTACHMENT_DIR, filename);
46
+ writeFileSync(filepath, buffer);
47
+ const note = `[User sent a file: ${attachment.name || 'file'} (${formatSize(buffer.length)}). Saved to ${filepath} — use the Read tool to view it.]`;
48
+ content = content ? `${content}\n\n${note}` : note;
49
+ console.error(`[discord-filtered] Downloaded: ${filepath} (${formatSize(buffer.length)})`);
22
50
  }
23
51
  }
24
52
  catch (err) {
@@ -27,6 +55,13 @@ async function resolveTextAttachments(message) {
27
55
  }
28
56
  return content;
29
57
  }
58
+ function formatSize(bytes) {
59
+ if (bytes < 1024)
60
+ return `${bytes}B`;
61
+ if (bytes < 1024 * 1024)
62
+ return `${(bytes / 1024).toFixed(1)}KB`;
63
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
64
+ }
30
65
  export function createDiscordClient(config, onMessage) {
31
66
  const client = new Client({
32
67
  intents: [
@@ -37,7 +72,7 @@ export function createDiscordClient(config, onMessage) {
37
72
  });
38
73
  client.on('messageCreate', async (message) => {
39
74
  if (shouldForwardMessage(message.channel.id, message.author.id, message.author.bot, config.channelId, config.allowedUsers)) {
40
- const content = await resolveTextAttachments(message);
75
+ const content = await resolveAttachments(message);
41
76
  if (content) {
42
77
  onMessage(content, message);
43
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onkol",
3
- "version": "0.5.1",
3
+ "version": "0.5.4",
4
4
  "description": "Decentralized on-call agent system powered by Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -64,8 +64,14 @@ if [ -n "$CHANNEL_ID" ] && [ "$CHANNEL_ID" != "null" ]; then
64
64
  echo "Discord channel deleted."
65
65
  fi
66
66
 
67
- # Archive worker directory
68
- ARCHIVE_DIR="$ONKOL_DIR/workers/.archive/${DATE}-${WORKER_NAME}"
67
+ # Archive worker directory (append counter if archive already exists from same day)
68
+ ARCHIVE_BASE="$ONKOL_DIR/workers/.archive/${DATE}-${WORKER_NAME}"
69
+ ARCHIVE_DIR="$ARCHIVE_BASE"
70
+ COUNTER=1
71
+ while [ -d "$ARCHIVE_DIR" ]; do
72
+ ARCHIVE_DIR="${ARCHIVE_BASE}-${COUNTER}"
73
+ COUNTER=$((COUNTER + 1))
74
+ done
69
75
  mkdir -p "$ONKOL_DIR/workers/.archive"
70
76
  mv "$WORKER_DIR" "$ARCHIVE_DIR"
71
77
  echo "Worker directory archived to $ARCHIVE_DIR"
@@ -1,4 +1,14 @@
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 ATTACHMENT_DIR = join(tmpdir(), 'onkol-attachments')
7
+ mkdirSync(ATTACHMENT_DIR, { recursive: true })
8
+
9
+ // Small text files get inlined into the message content
10
+ const INLINE_MAX_BYTES = 10_000
11
+ const INLINE_CONTENT_TYPES = ['text/', 'application/json', 'application/xml', 'application/csv']
2
12
 
3
13
  export interface DiscordClientConfig {
4
14
  botToken: string
@@ -19,27 +29,53 @@ export function shouldForwardMessage(
19
29
  return true
20
30
  }
21
31
 
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> {
32
+ function shouldInline(a: Attachment): boolean {
33
+ if (a.name?.endsWith('.txt') || a.name?.endsWith('.csv') || a.name?.endsWith('.json') || a.name?.endsWith('.md')) {
34
+ return (a.size || 0) <= INLINE_MAX_BYTES
35
+ }
36
+ const ct = a.contentType || ''
37
+ return INLINE_CONTENT_TYPES.some(t => ct.startsWith(t)) && (a.size || 0) <= INLINE_MAX_BYTES
38
+ }
39
+
40
+ // Download all attachments: small text gets inlined, everything else saved to disk.
41
+ // Claude Code's Read tool handles images, PDFs, CSVs, notebooks, etc. natively.
42
+ async function resolveAttachments(message: Message): Promise<string> {
25
43
  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()) {
44
+
45
+ for (const attachment of message.attachments.values()) {
30
46
  try {
31
47
  const res = await fetch(attachment.url)
32
- if (res.ok) {
48
+ if (!res.ok) continue
49
+
50
+ if (shouldInline(attachment)) {
51
+ // Inline small text files directly into the message
33
52
  const text = await res.text()
34
- content = content ? `${content}\n\n${text}` : text
53
+ const label = attachment.name ? `[${attachment.name}]` : ''
54
+ content = content ? `${content}\n\n${label}\n${text}` : `${label}\n${text}`
55
+ } else {
56
+ // Save to disk — Claude Code can Read images, PDFs, CSVs, etc.
57
+ const buffer = Buffer.from(await res.arrayBuffer())
58
+ const filename = `${message.id}-${attachment.name || 'file'}`
59
+ const filepath = join(ATTACHMENT_DIR, filename)
60
+ writeFileSync(filepath, buffer)
61
+ const note = `[User sent a file: ${attachment.name || 'file'} (${formatSize(buffer.length)}). Saved to ${filepath} — use the Read tool to view it.]`
62
+ content = content ? `${content}\n\n${note}` : note
63
+ console.error(`[discord-filtered] Downloaded: ${filepath} (${formatSize(buffer.length)})`)
35
64
  }
36
65
  } catch (err) {
37
66
  console.error(`[discord-filtered] Failed to fetch attachment ${attachment.name}: ${err}`)
38
67
  }
39
68
  }
69
+
40
70
  return content
41
71
  }
42
72
 
73
+ function formatSize(bytes: number): string {
74
+ if (bytes < 1024) return `${bytes}B`
75
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
76
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`
77
+ }
78
+
43
79
  export function createDiscordClient(
44
80
  config: DiscordClientConfig,
45
81
  onMessage: (content: string, message: Message) => void
@@ -62,7 +98,7 @@ export function createDiscordClient(
62
98
  config.allowedUsers
63
99
  )
64
100
  ) {
65
- const content = await resolveTextAttachments(message)
101
+ const content = await resolveAttachments(message)
66
102
  if (content) {
67
103
  onMessage(content, message)
68
104
  }