onkol 0.5.2 → 0.5.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/index.js CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  const __dirname = dirname(fileURLToPath(import.meta.url));
5
5
  import { program } from 'commander';
6
6
  import chalk from 'chalk';
7
- import { mkdirSync, writeFileSync, readFileSync, copyFileSync, existsSync } from 'fs';
7
+ import { mkdirSync, writeFileSync, readFileSync, copyFileSync, existsSync, unlinkSync } from 'fs';
8
8
  import { resolve } from 'path';
9
9
  import { execSync } from 'child_process';
10
10
  import { runSetupPrompts } from './prompts.js';
@@ -34,7 +34,6 @@ function saveCheckpoint(homeDir, checkpoint) {
34
34
  function clearCheckpoint(homeDir) {
35
35
  const p = resolve(homeDir, '.onkol-setup-checkpoint.json');
36
36
  if (existsSync(p)) {
37
- const { unlinkSync } = require('fs');
38
37
  unlinkSync(p);
39
38
  }
40
39
  }
@@ -570,8 +569,8 @@ program
570
569
  console.log(chalk.cyan('[1/3] Updating files from npm package...'));
571
570
  try {
572
571
  // 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, '..');
572
+ // __dirname is dist/cli/, so pkgRoot is two levels up (dist/cli -> dist -> package root)
573
+ const pkgRoot = resolve(__dirname, '../..');
575
574
  const { readdirSync, chmodSync } = await import('fs');
576
575
  // Try src/plugin first (has .ts files), then dist/plugin (.js files)
577
576
  let pluginUpdated = false;
@@ -2,9 +2,11 @@ import { Client, GatewayIntentBits } from 'discord.js';
2
2
  import { mkdirSync, writeFileSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
- const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg']);
6
5
  const ATTACHMENT_DIR = join(tmpdir(), 'onkol-attachments');
7
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'];
8
10
  export function shouldForwardMessage(messageChannelId, authorId, isBot, targetChannelId, allowedUsers) {
9
11
  if (isBot)
10
12
  return false;
@@ -14,38 +16,37 @@ export function shouldForwardMessage(messageChannelId, authorId, isBot, targetCh
14
16
  return false;
15
17
  return true;
16
18
  }
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('.')));
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
25
  }
26
- // Resolve all attachments: text gets inlined, images get downloaded to temp files
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.
27
28
  async function resolveAttachments(message) {
28
29
  let content = message.content;
29
30
  for (const attachment of message.attachments.values()) {
30
31
  try {
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
- }
32
+ const res = await fetch(attachment.url);
33
+ if (!res.ok)
34
+ continue;
35
+ if (shouldInline(attachment)) {
36
+ // Inline small text files directly into the message
37
+ const text = await res.text();
38
+ const label = attachment.name ? `[${attachment.name}]` : '';
39
+ content = content ? `${content}\n\n${label}\n${text}` : `${label}\n${text}`;
37
40
  }
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
- }
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)})`);
49
50
  }
50
51
  }
51
52
  catch (err) {
@@ -54,6 +55,13 @@ async function resolveAttachments(message) {
54
55
  }
55
56
  return content;
56
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
+ }
57
65
  export function createDiscordClient(config, onMessage) {
58
66
  const client = new Client({
59
67
  intents: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onkol",
3
- "version": "0.5.2",
3
+ "version": "0.5.5",
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"
@@ -3,10 +3,13 @@ import { mkdirSync, writeFileSync } from 'fs'
3
3
  import { join } from 'path'
4
4
  import { tmpdir } from 'os'
5
5
 
6
- const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'])
7
6
  const ATTACHMENT_DIR = join(tmpdir(), 'onkol-attachments')
8
7
  mkdirSync(ATTACHMENT_DIR, { recursive: true })
9
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']
12
+
10
13
  export interface DiscordClientConfig {
11
14
  botToken: string
12
15
  channelId: string
@@ -26,39 +29,38 @@ export function shouldForwardMessage(
26
29
  return true
27
30
  }
28
31
 
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('.')))
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
37
38
  }
38
39
 
39
- // Resolve all attachments: text gets inlined, images get downloaded to temp files
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.
40
42
  async function resolveAttachments(message: Message): Promise<string> {
41
43
  let content = message.content
42
44
 
43
45
  for (const attachment of message.attachments.values()) {
44
46
  try {
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
- }
47
+ const res = await fetch(attachment.url)
48
+ if (!res.ok) continue
49
+
50
+ if (shouldInline(attachment)) {
51
+ // Inline small text files directly into the message
52
+ const text = await res.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)})`)
62
64
  }
63
65
  } catch (err) {
64
66
  console.error(`[discord-filtered] Failed to fetch attachment ${attachment.name}: ${err}`)
@@ -68,6 +70,12 @@ async function resolveAttachments(message: Message): Promise<string> {
68
70
  return content
69
71
  }
70
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
+
71
79
  export function createDiscordClient(
72
80
  config: DiscordClientConfig,
73
81
  onMessage: (content: string, message: Message) => void