@thesammykins/tether 1.0.0

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/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @thesammykins/tether
3
+ *
4
+ * Discord bot that bridges messages to AI agent sessions.
5
+ * Supports Claude Code, OpenCode, and Codex CLI.
6
+ *
7
+ * Usage:
8
+ * bunx @thesammykins/tether start # Start bot + worker
9
+ * bunx @thesammykins/tether setup # Interactive setup
10
+ *
11
+ * Or programmatically:
12
+ * import { adapters, features } from '@thesammykins/tether'
13
+ */
14
+
15
+ // Adapters
16
+ export { ClaudeAdapter } from './src/adapters/claude.ts';
17
+ export { OpenCodeAdapter } from './src/adapters/opencode.ts';
18
+ export { CodexAdapter } from './src/adapters/codex.ts';
19
+ export { getAdapter } from './src/adapters/registry.ts';
20
+ export type { AgentAdapter, SpawnResult } from './src/adapters/types.ts';
21
+
22
+ // Features
23
+ export { acknowledgeMessage } from './src/features/ack.ts';
24
+ export { getChannelContext } from './src/features/channel-context.ts';
25
+ export { generateThreadName } from './src/features/thread-naming.ts';
26
+ export { checkSessionLimits } from './src/features/session-limits.ts';
27
+ export { handlePauseResume, isThreadPausedExport } from './src/features/pause-resume.ts';
28
+ export { isAway, setBrb, setBack, isBrbMessage, isBackMessage } from './src/features/brb.ts';
29
+
30
+ // Infrastructure
31
+ export { claudeQueue } from './src/queue.ts';
32
+ export { db, getChannelConfigCached, setChannelConfig } from './src/db.ts';
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@thesammykins/tether",
3
+ "version": "1.0.0",
4
+ "description": "Discord bot that bridges messages to AI agent sessions (Claude, OpenCode, Codex)",
5
+ "license": "MIT",
6
+ "author": "thesammykins",
7
+ "homepage": "https://github.com/thesammykins/tether#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/thesammykins/tether"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/thesammykins/tether/issues"
14
+ },
15
+ "keywords": [
16
+ "discord",
17
+ "bot",
18
+ "ai",
19
+ "claude",
20
+ "opencode",
21
+ "codex",
22
+ "agent",
23
+ "bridge",
24
+ "bun"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "type": "module",
30
+ "module": "index.ts",
31
+ "main": "index.ts",
32
+ "bin": {
33
+ "tether": "bin/tether.ts"
34
+ },
35
+ "files": [
36
+ "src/",
37
+ "bin/tether.ts",
38
+ "index.ts",
39
+ "README.md",
40
+ "LICENSE",
41
+ ".env.example"
42
+ ],
43
+ "scripts": {
44
+ "start": "bun run bin/tether.ts start",
45
+ "bot": "bun run src/bot.ts",
46
+ "worker": "bun run src/worker.ts",
47
+ "setup": "bun run bin/tether.ts setup",
48
+ "test": "bun test"
49
+ },
50
+ "dependencies": {
51
+ "bullmq": "^5.67.1",
52
+ "discord.js": "^14.25.1",
53
+ "ioredis": "^5.9.2"
54
+ },
55
+ "devDependencies": {
56
+ "@types/bun": "^1.3.9"
57
+ },
58
+ "peerDependencies": {
59
+ "typescript": "^5"
60
+ },
61
+ "engines": {
62
+ "bun": ">=1.1.0"
63
+ }
64
+ }
@@ -0,0 +1,107 @@
1
+ import type { AgentAdapter, SpawnOptions, SpawnResult } from './types.js';
2
+
3
+ /**
4
+ * Claude CLI Adapter
5
+ *
6
+ * Wraps the Claude Code CLI with proper session handling and output parsing.
7
+ *
8
+ * Key flags:
9
+ * - `--print`: Non-interactive mode, returns output
10
+ * - `--session-id UUID`: Set session ID for new sessions
11
+ * - `--resume UUID`: Resume an existing session (for follow-ups)
12
+ * - `--append-system-prompt`: Inject context that survives compaction
13
+ * - `-p "prompt"`: The actual prompt to send
14
+ * - `--output-format json`: Structured output (if supported)
15
+ */
16
+
17
+ const TIMEZONE = process.env.TZ || 'UTC';
18
+
19
+ function getDatetimeContext(): string {
20
+ const now = new Date();
21
+ return now.toLocaleString('en-US', {
22
+ weekday: 'long',
23
+ year: 'numeric',
24
+ month: 'long',
25
+ day: 'numeric',
26
+ hour: 'numeric',
27
+ minute: '2-digit',
28
+ hour12: true,
29
+ timeZone: TIMEZONE,
30
+ });
31
+ }
32
+
33
+ export class ClaudeAdapter implements AgentAdapter {
34
+ readonly name = 'claude';
35
+
36
+ async spawn(options: SpawnOptions): Promise<SpawnResult> {
37
+ const { prompt, sessionId, resume, systemPrompt, workingDir } = options;
38
+
39
+ const cwd = workingDir || process.env.CLAUDE_WORKING_DIR || process.cwd();
40
+
41
+ // Build CLI arguments
42
+ const args = ['claude'];
43
+
44
+ // Non-interactive mode
45
+ args.push('--print');
46
+
47
+ // Output format
48
+ args.push('--output-format', 'json');
49
+
50
+ // Session handling
51
+ if (resume) {
52
+ // Resume existing session for follow-up messages
53
+ args.push('--resume', sessionId);
54
+ } else {
55
+ // New session - set the ID upfront
56
+ args.push('--session-id', sessionId);
57
+ }
58
+
59
+ // Inject datetime context (survives session compaction)
60
+ const datetimeContext = `Current date/time: ${getDatetimeContext()}`;
61
+ const fullSystemPrompt = systemPrompt
62
+ ? `${datetimeContext}\n\n${systemPrompt}`
63
+ : datetimeContext;
64
+
65
+ args.push('--append-system-prompt', fullSystemPrompt);
66
+
67
+ // The actual prompt
68
+ args.push('-p', prompt);
69
+
70
+ // Spawn the process
71
+ const proc = Bun.spawn(args, {
72
+ cwd,
73
+ env: {
74
+ ...process.env,
75
+ TZ: TIMEZONE,
76
+ },
77
+ stdout: 'pipe',
78
+ stderr: 'pipe',
79
+ });
80
+
81
+ // Collect output
82
+ const stdout = await new Response(proc.stdout).text();
83
+ const stderr = await new Response(proc.stderr).text();
84
+
85
+ // Wait for process to exit
86
+ const exitCode = await proc.exited;
87
+
88
+ if (exitCode !== 0) {
89
+ throw new Error(`Claude CLI failed (exit ${exitCode}): ${stderr || 'Unknown error'}`);
90
+ }
91
+
92
+ // Parse JSON output or fall back to raw text
93
+ let output = stdout.trim();
94
+ try {
95
+ const parsed = JSON.parse(stdout);
96
+ output = parsed.response || parsed.output || stdout.trim();
97
+ } catch {
98
+ // Not JSON, use raw output
99
+ output = stdout.trim();
100
+ }
101
+
102
+ return {
103
+ output,
104
+ sessionId,
105
+ };
106
+ }
107
+ }
@@ -0,0 +1,77 @@
1
+ import type { AgentAdapter, SpawnOptions, SpawnResult } from './types.js';
2
+
3
+ /**
4
+ * Codex CLI Adapter
5
+ *
6
+ * Wraps the Codex CLI with session handling and JSON output parsing.
7
+ *
8
+ * Key commands:
9
+ * - `codex exec "<prompt>"`: Execute a new prompt
10
+ * - `codex exec resume <sessionId> "<prompt>"`: Resume existing session
11
+ * - `--json`: Structured output
12
+ */
13
+
14
+ export class CodexAdapter implements AgentAdapter {
15
+ readonly name = 'codex';
16
+
17
+ async spawn(options: SpawnOptions): Promise<SpawnResult> {
18
+ const { prompt, sessionId, resume, workingDir } = options;
19
+
20
+ const args = ['codex', 'exec'];
21
+
22
+ // Session handling
23
+ if (resume) {
24
+ // Resume existing session
25
+ args.push('resume', sessionId);
26
+ }
27
+
28
+ // JSON output format
29
+ args.push('--json');
30
+
31
+ // The prompt (always last)
32
+ args.push(prompt);
33
+
34
+ // Spawn the process
35
+ const proc = Bun.spawn(args, {
36
+ cwd: workingDir || process.cwd(),
37
+ env: process.env,
38
+ stdout: 'pipe',
39
+ stderr: 'pipe',
40
+ });
41
+
42
+ // Collect output
43
+ const stdout = await new Response(proc.stdout).text();
44
+ const stderr = await new Response(proc.stderr).text();
45
+
46
+ // Wait for process to exit
47
+ const exitCode = await proc.exited;
48
+
49
+ if (exitCode !== 0) {
50
+ throw new Error(`Codex CLI failed (exit ${exitCode}): ${stderr || 'Unknown error'}`);
51
+ }
52
+
53
+ // Parse JSON output
54
+ let output = stdout.trim();
55
+ let finalSessionId = sessionId;
56
+
57
+ try {
58
+ const parsed = JSON.parse(stdout);
59
+ output = parsed.output || parsed.response || parsed.result || stdout.trim();
60
+
61
+ // Extract session ID from output (Codex auto-assigns)
62
+ if (parsed.sessionId) {
63
+ finalSessionId = parsed.sessionId;
64
+ } else if (parsed.session_id) {
65
+ finalSessionId = parsed.session_id;
66
+ }
67
+ } catch {
68
+ // Not JSON, use raw output
69
+ output = stdout.trim();
70
+ }
71
+
72
+ return {
73
+ output,
74
+ sessionId: finalSessionId,
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,83 @@
1
+ import type { AgentAdapter, SpawnOptions, SpawnResult } from './types.js';
2
+
3
+ /**
4
+ * OpenCode CLI Adapter
5
+ *
6
+ * Wraps the OpenCode CLI with session handling and JSON output parsing.
7
+ *
8
+ * Key flags:
9
+ * - `run "<prompt>"`: Execute a prompt
10
+ * - `--session <id>` or `--continue`: Resume existing session
11
+ * - `--format json`: Structured output
12
+ * - `--cwd <path>`: Set working directory
13
+ */
14
+
15
+ export class OpenCodeAdapter implements AgentAdapter {
16
+ readonly name = 'opencode';
17
+
18
+ async spawn(options: SpawnOptions): Promise<SpawnResult> {
19
+ const { prompt, sessionId, resume, workingDir } = options;
20
+
21
+ const args = ['opencode', 'run'];
22
+
23
+ // Format as JSON for structured output
24
+ args.push('--format', 'json');
25
+
26
+ // Session handling
27
+ if (resume) {
28
+ // Resume existing session
29
+ args.push('--session', sessionId);
30
+ }
31
+
32
+ // Working directory
33
+ if (workingDir) {
34
+ args.push('--cwd', workingDir);
35
+ }
36
+
37
+ // The prompt (always last)
38
+ args.push(prompt);
39
+
40
+ // Spawn the process
41
+ const proc = Bun.spawn(args, {
42
+ cwd: workingDir || process.cwd(),
43
+ env: process.env,
44
+ stdout: 'pipe',
45
+ stderr: 'pipe',
46
+ });
47
+
48
+ // Collect output
49
+ const stdout = await new Response(proc.stdout).text();
50
+ const stderr = await new Response(proc.stderr).text();
51
+
52
+ // Wait for process to exit
53
+ const exitCode = await proc.exited;
54
+
55
+ if (exitCode !== 0) {
56
+ throw new Error(`OpenCode CLI failed (exit ${exitCode}): ${stderr || 'Unknown error'}`);
57
+ }
58
+
59
+ // Parse JSON output
60
+ let output = stdout.trim();
61
+ let finalSessionId = sessionId;
62
+
63
+ try {
64
+ const parsed = JSON.parse(stdout);
65
+ output = parsed.output || parsed.response || parsed.result || stdout.trim();
66
+
67
+ // Extract session ID from output if not resuming
68
+ if (!resume && parsed.sessionId) {
69
+ finalSessionId = parsed.sessionId;
70
+ } else if (!resume && parsed.session_id) {
71
+ finalSessionId = parsed.session_id;
72
+ }
73
+ } catch {
74
+ // Not JSON, use raw output
75
+ output = stdout.trim();
76
+ }
77
+
78
+ return {
79
+ output,
80
+ sessionId: finalSessionId,
81
+ };
82
+ }
83
+ }
@@ -0,0 +1,23 @@
1
+ import type { AgentAdapter } from './types.js';
2
+ import { ClaudeAdapter } from './claude.js';
3
+ import { OpenCodeAdapter } from './opencode.js';
4
+ import { CodexAdapter } from './codex.js';
5
+
6
+ export function getAdapter(type?: string): AgentAdapter {
7
+ const adapterType = type || process.env.AGENT_TYPE || 'claude';
8
+
9
+ switch (adapterType.toLowerCase()) {
10
+ case 'claude':
11
+ return new ClaudeAdapter();
12
+ case 'opencode':
13
+ return new OpenCodeAdapter();
14
+ case 'codex':
15
+ return new CodexAdapter();
16
+ default:
17
+ throw new Error(`Unknown adapter type: ${adapterType}`);
18
+ }
19
+ }
20
+
21
+ export function getSupportedAdapters(): string[] {
22
+ return ['claude', 'opencode', 'codex'];
23
+ }
@@ -0,0 +1,17 @@
1
+ export interface SpawnOptions {
2
+ prompt: string;
3
+ sessionId: string;
4
+ resume: boolean;
5
+ systemPrompt?: string;
6
+ workingDir?: string;
7
+ }
8
+
9
+ export interface SpawnResult {
10
+ output: string;
11
+ sessionId: string;
12
+ }
13
+
14
+ export interface AgentAdapter {
15
+ readonly name: string;
16
+ spawn(options: SpawnOptions): Promise<SpawnResult>;
17
+ }