@hoverlover/cc-discord 0.1.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.
Files changed (46) hide show
  1. package/.claude/settings.template.json +94 -0
  2. package/.env.example +41 -0
  3. package/.env.relay.example +46 -0
  4. package/.env.worker.example +40 -0
  5. package/README.md +313 -0
  6. package/hooks/check-discord-messages.ts +204 -0
  7. package/hooks/cleanup-attachment.ts +47 -0
  8. package/hooks/safe-bash.ts +157 -0
  9. package/hooks/steer-send.ts +108 -0
  10. package/hooks/track-activity.ts +220 -0
  11. package/memory/README.md +60 -0
  12. package/memory/core/MemoryCoordinator.ts +703 -0
  13. package/memory/core/MemoryStore.ts +72 -0
  14. package/memory/core/session-key.ts +14 -0
  15. package/memory/core/types.ts +59 -0
  16. package/memory/index.ts +19 -0
  17. package/memory/providers/sqlite/SqliteMemoryStore.ts +838 -0
  18. package/memory/providers/sqlite/index.ts +1 -0
  19. package/package.json +45 -0
  20. package/prompts/autoreply-system.md +32 -0
  21. package/prompts/channel-system.md +22 -0
  22. package/prompts/orchestrator-system.md +56 -0
  23. package/scripts/channel-agent.sh +159 -0
  24. package/scripts/generate-settings.sh +17 -0
  25. package/scripts/load-env.sh +79 -0
  26. package/scripts/migrate-memory-to-channel-keys.ts +148 -0
  27. package/scripts/orchestrator.sh +325 -0
  28. package/scripts/parse-claude-stream.ts +349 -0
  29. package/scripts/start-orchestrator.sh +82 -0
  30. package/scripts/start-relay.sh +17 -0
  31. package/scripts/start.sh +175 -0
  32. package/server/attachment.ts +182 -0
  33. package/server/busy-notify.ts +69 -0
  34. package/server/config.ts +121 -0
  35. package/server/db.ts +249 -0
  36. package/server/index.ts +311 -0
  37. package/server/memory.ts +88 -0
  38. package/server/messages.ts +111 -0
  39. package/server/trace-thread.ts +340 -0
  40. package/server/typing.ts +101 -0
  41. package/tools/memory-inspect.ts +94 -0
  42. package/tools/memory-smoke.ts +173 -0
  43. package/tools/send-discord +2 -0
  44. package/tools/send-discord.ts +82 -0
  45. package/tools/wait-for-discord-messages +2 -0
  46. package/tools/wait-for-discord-messages.ts +369 -0
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Hook to track current agent activity for UX/status signaling.
4
+ *
5
+ * Writes to data/messages.db -> agent_activity table:
6
+ * - PreToolUse: status=busy with tool summary
7
+ * - PostToolUse/Stop/SessionStart: status=idle
8
+ *
9
+ * Produces no stdout output (state-only hook).
10
+ */
11
+
12
+ // Suppress Node.js ExperimentalWarning (SQLite) to keep hook output clean
13
+ const _origEmit = process.emit;
14
+ process.emit = function (event: string, ...args: any[]) {
15
+ if (event === "warning" && args[0]?.name === "ExperimentalWarning") return false;
16
+ return _origEmit.call(this, event, ...args) as any;
17
+ };
18
+
19
+ import { Database as DatabaseSync } from "bun:sqlite";
20
+ import { dirname, join } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const ROOT_DIR = process.env.ORCHESTRATOR_DIR || join(__dirname, "..");
25
+
26
+ const agentId = process.env.AGENT_ID || process.env.CLAUDE_AGENT_ID || "claude";
27
+ const sessionId =
28
+ process.env.DISCORD_SESSION_ID || process.env.BROKER_SESSION_ID || process.env.SESSION_ID || "default";
29
+ const traceEnabled = String(process.env.TRACE_THREAD_ENABLED || "true").toLowerCase() !== "false";
30
+ // In channel routing mode, agent_id IS the Discord channel ID
31
+ const traceChannelId = agentId;
32
+
33
+ const dbPath = join(ROOT_DIR, "data", "messages.db");
34
+
35
+ const input = await readHookInput();
36
+ if (!input) process.exit(0);
37
+
38
+ const hookEvent = input.hook_event_name || input.hookEventName || "";
39
+ const toolName = input.tool_name || input.toolName || null;
40
+ const toolInput = input.tool_input || input.toolInput || null;
41
+
42
+ let db: InstanceType<typeof DatabaseSync>;
43
+ try {
44
+ db = new DatabaseSync(dbPath);
45
+ } catch {
46
+ process.exit(0);
47
+ }
48
+
49
+ try {
50
+ db.exec(`
51
+ CREATE TABLE IF NOT EXISTS agent_activity (
52
+ session_id TEXT NOT NULL,
53
+ agent_id TEXT NOT NULL,
54
+ status TEXT NOT NULL DEFAULT 'idle',
55
+ activity_type TEXT,
56
+ activity_summary TEXT,
57
+ started_at TEXT,
58
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
59
+ PRIMARY KEY (session_id, agent_id)
60
+ );
61
+ CREATE INDEX IF NOT EXISTS idx_agent_activity_status
62
+ ON agent_activity(status);
63
+
64
+ CREATE TABLE IF NOT EXISTS trace_events (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
67
+ session_id TEXT NOT NULL,
68
+ agent_id TEXT NOT NULL,
69
+ channel_id TEXT,
70
+ event_type TEXT NOT NULL,
71
+ tool_name TEXT,
72
+ summary TEXT,
73
+ posted INTEGER DEFAULT 0
74
+ );
75
+ CREATE INDEX IF NOT EXISTS idx_trace_events_pending
76
+ ON trace_events(posted, created_at);
77
+ `);
78
+
79
+ const now = new Date().toISOString();
80
+
81
+ if (hookEvent === "PreToolUse") {
82
+ const summary = summarizeTool(toolName, toolInput);
83
+
84
+ db.prepare(`
85
+ INSERT INTO agent_activity (
86
+ session_id,
87
+ agent_id,
88
+ status,
89
+ activity_type,
90
+ activity_summary,
91
+ started_at,
92
+ updated_at
93
+ ) VALUES (?, ?, 'busy', ?, ?, ?, ?)
94
+ ON CONFLICT(session_id, agent_id) DO UPDATE SET
95
+ status = 'busy',
96
+ activity_type = excluded.activity_type,
97
+ activity_summary = excluded.activity_summary,
98
+ started_at = excluded.started_at,
99
+ updated_at = excluded.updated_at
100
+ `).run(sessionId, agentId, toolName || "tool", summary, now, now);
101
+
102
+ // Write trace event for the live trace thread
103
+ if (traceEnabled) {
104
+ try {
105
+ db.prepare(`
106
+ INSERT INTO trace_events (session_id, agent_id, channel_id, event_type, tool_name, summary)
107
+ VALUES (?, ?, ?, 'tool_start', ?, ?)
108
+ `).run(sessionId, agentId, traceChannelId, toolName || "tool", summary);
109
+ } catch { /* fail-open */ }
110
+ }
111
+
112
+ process.exit(0);
113
+ }
114
+
115
+ if (hookEvent === "PostToolUse" || hookEvent === "Stop" || hookEvent === "SessionStart") {
116
+ // Read started_at BEFORE clearing it (needed for elapsed time calculation)
117
+ let elapsedTag = "";
118
+ if (traceEnabled && hookEvent === "PostToolUse") {
119
+ try {
120
+ const row = db.prepare(`
121
+ SELECT started_at FROM agent_activity
122
+ WHERE session_id = ? AND agent_id = ?
123
+ `).get(sessionId, agentId) as { started_at?: string } | undefined;
124
+ if (row?.started_at) {
125
+ const elapsedMs = Date.now() - new Date(row.started_at).getTime();
126
+ if (elapsedMs >= 0 && elapsedMs < 600_000) {
127
+ elapsedTag = `elapsed:${elapsedMs}|`;
128
+ }
129
+ }
130
+ } catch { /* best-effort */ }
131
+ }
132
+
133
+ db.prepare(`
134
+ INSERT INTO agent_activity (
135
+ session_id,
136
+ agent_id,
137
+ status,
138
+ activity_type,
139
+ activity_summary,
140
+ started_at,
141
+ updated_at
142
+ ) VALUES (?, ?, 'idle', NULL, NULL, NULL, ?)
143
+ ON CONFLICT(session_id, agent_id) DO UPDATE SET
144
+ status = 'idle',
145
+ activity_type = NULL,
146
+ activity_summary = NULL,
147
+ started_at = NULL,
148
+ updated_at = excluded.updated_at
149
+ `).run(sessionId, agentId, now);
150
+
151
+ // Write trace event for the live trace thread (with elapsed time)
152
+ if (traceEnabled && hookEvent === "PostToolUse") {
153
+ try {
154
+ const summary = summarizeTool(toolName, toolInput);
155
+ db.prepare(`
156
+ INSERT INTO trace_events (session_id, agent_id, channel_id, event_type, tool_name, summary)
157
+ VALUES (?, ?, ?, 'tool_end', ?, ?)
158
+ `).run(sessionId, agentId, traceChannelId, toolName || "tool", `${elapsedTag}${summary}`);
159
+ } catch { /* fail-open */ }
160
+ }
161
+ }
162
+ } catch {
163
+ // fail-open
164
+ } finally {
165
+ try {
166
+ db.close();
167
+ } catch {
168
+ /* ignore */
169
+ }
170
+ }
171
+
172
+ process.exit(0);
173
+
174
+ async function readHookInput(): Promise<any> {
175
+ try {
176
+ const chunks: Buffer[] = [];
177
+ for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
178
+ const raw = Buffer.concat(chunks).toString();
179
+ return raw ? JSON.parse(raw) : {};
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+
185
+ function summarizeTool(toolName: string | null, toolInput: any): string {
186
+ const name = String(toolName || "Tool");
187
+
188
+ if (!toolInput || typeof toolInput !== "object") return name;
189
+
190
+ if (name === "Bash") {
191
+ const command = String(toolInput.command || "")
192
+ .replace(/\s+/g, " ")
193
+ .trim();
194
+ if (!command) return "Bash";
195
+ return command;
196
+ }
197
+
198
+ if (name === "Read") {
199
+ const target = toolInput.file_path || toolInput.path || "";
200
+ return target ? `Read ${target}` : "Read";
201
+ }
202
+
203
+ if (name === "Edit" || name === "Write") {
204
+ const target = toolInput.file_path || toolInput.path || "";
205
+ return target ? `${name} ${target}` : name;
206
+ }
207
+
208
+ if (name === "Task") {
209
+ const desc = String(toolInput.description || toolInput.prompt || "").trim();
210
+ return desc ? `Task: ${desc}` : "Task";
211
+ }
212
+
213
+ return name;
214
+ }
215
+
216
+ function truncate(text: string, maxLen: number): string {
217
+ const str = String(text || "");
218
+ if (str.length <= maxLen) return str;
219
+ return `${str.slice(0, maxLen - 1)}…`;
220
+ }
@@ -0,0 +1,60 @@
1
+ # Memory module (v1 foundation)
2
+
3
+ This module introduces a pluggable memory architecture with SQLite as the primary store.
4
+
5
+ ## Structure
6
+
7
+ - `core/types.js` — canonical memory model helpers
8
+ - `core/MemoryStore.js` — base class contract
9
+ - `core/MemoryCoordinator.js` — context assembly + relevance selection
10
+ - `core/session-key.js` — deterministic session key helper
11
+ - `providers/sqlite/SqliteMemoryStore.js` — SQLite implementation (schema + CRUD)
12
+
13
+ ## Current capabilities
14
+
15
+ - Initialize SQLite memory schema (`memory_*` tables)
16
+ - Idempotent batch writes via `batchId`
17
+ - Store and list session turns
18
+ - Store and read latest session snapshot
19
+ - Store/query memory cards
20
+ - Persist compaction cursor state
21
+ - Persist runtime context state (`memory_runtime_state`) for `/new`-aware filtering
22
+ - Prepare sync job queue table for future secondary-store fan-out
23
+ - Assemble context-aware memory packets that prioritize **relevant, out-of-window** memories
24
+
25
+ ### Retrieval policy (current)
26
+
27
+ `MemoryCoordinator.assembleContext()`:
28
+ 1. resolves current runtime context (`runtime_context_id`)
29
+ 2. treats turns from the same runtime as already-in-context and excludes them from recall
30
+ 3. exception for compaction: only post-compaction turns in current runtime are excluded; compacted turns can be recalled via snapshot/older memory
31
+ 4. filters out overlapping memory cards (based on source turn IDs)
32
+ 5. ranks remaining cards/older turns by query overlap + novelty + confidence/pinned boosts
33
+ 6. returns only top matches within configured limits
34
+
35
+ ## Basic usage
36
+
37
+ ```js
38
+ import { SqliteMemoryStore } from './providers/sqlite/SqliteMemoryStore.js'
39
+
40
+ const store = new SqliteMemoryStore({ dbPath: './data/memory.db' })
41
+ await store.init()
42
+
43
+ await store.writeBatch({
44
+ batchId: 'batch-123',
45
+ sessionKey: 'discord:server:channel:thread',
46
+ turns: [{ role: 'user', content: 'hello' }],
47
+ })
48
+
49
+ const turns = await store.listTurns({ sessionKey: 'discord:server:channel:thread', limit: 20 })
50
+ ```
51
+
52
+ ## Quick smoke test
53
+
54
+ ```bash
55
+ node tools/memory-smoke.js
56
+ ```
57
+
58
+ If successful, it prints:
59
+
60
+ `Memory smoke test passed (...)`