@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.
- package/.claude/settings.template.json +94 -0
- package/.env.example +41 -0
- package/.env.relay.example +46 -0
- package/.env.worker.example +40 -0
- package/README.md +313 -0
- package/hooks/check-discord-messages.ts +204 -0
- package/hooks/cleanup-attachment.ts +47 -0
- package/hooks/safe-bash.ts +157 -0
- package/hooks/steer-send.ts +108 -0
- package/hooks/track-activity.ts +220 -0
- package/memory/README.md +60 -0
- package/memory/core/MemoryCoordinator.ts +703 -0
- package/memory/core/MemoryStore.ts +72 -0
- package/memory/core/session-key.ts +14 -0
- package/memory/core/types.ts +59 -0
- package/memory/index.ts +19 -0
- package/memory/providers/sqlite/SqliteMemoryStore.ts +838 -0
- package/memory/providers/sqlite/index.ts +1 -0
- package/package.json +45 -0
- package/prompts/autoreply-system.md +32 -0
- package/prompts/channel-system.md +22 -0
- package/prompts/orchestrator-system.md +56 -0
- package/scripts/channel-agent.sh +159 -0
- package/scripts/generate-settings.sh +17 -0
- package/scripts/load-env.sh +79 -0
- package/scripts/migrate-memory-to-channel-keys.ts +148 -0
- package/scripts/orchestrator.sh +325 -0
- package/scripts/parse-claude-stream.ts +349 -0
- package/scripts/start-orchestrator.sh +82 -0
- package/scripts/start-relay.sh +17 -0
- package/scripts/start.sh +175 -0
- package/server/attachment.ts +182 -0
- package/server/busy-notify.ts +69 -0
- package/server/config.ts +121 -0
- package/server/db.ts +249 -0
- package/server/index.ts +311 -0
- package/server/memory.ts +88 -0
- package/server/messages.ts +111 -0
- package/server/trace-thread.ts +340 -0
- package/server/typing.ts +101 -0
- package/tools/memory-inspect.ts +94 -0
- package/tools/memory-smoke.ts +173 -0
- package/tools/send-discord +2 -0
- package/tools/send-discord.ts +82 -0
- package/tools/wait-for-discord-messages +2 -0
- 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
|
+
}
|
package/memory/README.md
ADDED
|
@@ -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 (...)`
|