claude-nb 0.3.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/LICENSE +38 -0
- package/Makefile +60 -0
- package/README.md +63 -0
- package/VERSION +1 -0
- package/bin/_pip_entry.py +25 -0
- package/bin/board +287 -0
- package/bin/cnb +150 -0
- package/bin/cnb.js +33 -0
- package/bin/dispatcher +151 -0
- package/bin/dispatcher-watchdog +57 -0
- package/bin/doctor +328 -0
- package/bin/init +316 -0
- package/bin/registry +347 -0
- package/bin/swarm +896 -0
- package/lib/__init__.py +1 -0
- package/lib/board_admin.py +128 -0
- package/lib/board_bbs.py +99 -0
- package/lib/board_bug.py +161 -0
- package/lib/board_db.py +262 -0
- package/lib/board_lock.py +113 -0
- package/lib/board_mailbox.py +145 -0
- package/lib/board_maintenance.py +237 -0
- package/lib/board_msg.py +230 -0
- package/lib/board_task.py +200 -0
- package/lib/board_view.py +366 -0
- package/lib/board_vote.py +164 -0
- package/lib/build_lock.py +221 -0
- package/lib/cli.py +34 -0
- package/lib/common.py +285 -0
- package/lib/concerns/__init__.py +42 -0
- package/lib/concerns/adaptive_throttle.py +26 -0
- package/lib/concerns/base.py +25 -0
- package/lib/concerns/bug_sla_checker.py +32 -0
- package/lib/concerns/config.py +22 -0
- package/lib/concerns/coral_manager.py +61 -0
- package/lib/concerns/coral_poker.py +57 -0
- package/lib/concerns/file_watcher.py +127 -0
- package/lib/concerns/health_checker.py +72 -0
- package/lib/concerns/helpers.py +152 -0
- package/lib/concerns/idle_detector.py +56 -0
- package/lib/concerns/idle_killer.py +41 -0
- package/lib/concerns/idle_nudger.py +38 -0
- package/lib/concerns/inbox_nudger.py +34 -0
- package/lib/concerns/resource_monitor.py +47 -0
- package/lib/concerns/session_keepalive.py +23 -0
- package/lib/concerns/time_announcer.py +34 -0
- package/lib/crypto.py +92 -0
- package/lib/health.py +187 -0
- package/lib/inject.py +164 -0
- package/lib/migrate.py +109 -0
- package/lib/monitor.py +373 -0
- package/lib/panel.py +137 -0
- package/lib/resources.py +341 -0
- package/migrations/001_foreign_keys.sql +77 -0
- package/migrations/002_session_persona.sql +1 -0
- package/migrations/003_mailbox.sql +9 -0
- package/package.json +28 -0
- package/pyproject.toml +71 -0
- package/registry/0001-meridian.json +12 -0
- package/registry/0002-forge.json +12 -0
- package/registry/0003-lead.json +12 -0
- package/registry/0004-ms-encrypted-mailbox-live.json +12 -0
- package/registry/GENESIS.json +9 -0
- package/registry/pubkeys.json +5 -0
- package/schema.sql +138 -0
package/lib/resources.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""resources.py -- Unified resource monitoring (battery + memory + CPU).
|
|
3
|
+
|
|
4
|
+
Detects anomalies and notifies via board.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
./lib/resources.py # One-shot status check
|
|
8
|
+
./lib/resources.py --watch # Continuous monitoring (30s interval)
|
|
9
|
+
./lib/resources.py --json # Machine-readable output
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Thresholds
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
BATTERY_LOW = 30
|
|
26
|
+
BATTERY_CRITICAL = 15
|
|
27
|
+
MEMORY_WARN_PCT = 80
|
|
28
|
+
CPU_SATURATED = 90
|
|
29
|
+
CPU_SUSTAIN_CHECKS = 2
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Data classes
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class BatteryInfo:
|
|
39
|
+
status: str # AC, ON_BATTERY, LOW, CRITICAL, N/A
|
|
40
|
+
pct: int
|
|
41
|
+
on_battery: bool
|
|
42
|
+
remaining: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class MemoryInfo:
|
|
47
|
+
status: str # OK, WARNING, CRITICAL
|
|
48
|
+
used_pct: int
|
|
49
|
+
pressure: str # normal, warn, critical
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class CPUInfo:
|
|
54
|
+
status: str # OK, SATURATED
|
|
55
|
+
usage: int
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Detection helpers (macOS-specific)
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _run(cmd: str, default: str = "") -> str:
|
|
64
|
+
"""Run a shell command and return stdout, or *default* on failure."""
|
|
65
|
+
try:
|
|
66
|
+
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
|
|
67
|
+
return r.stdout.strip() if r.returncode == 0 else default
|
|
68
|
+
except Exception:
|
|
69
|
+
return default
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def check_battery() -> BatteryInfo:
|
|
73
|
+
if not shutil.which("pmset"):
|
|
74
|
+
return BatteryInfo(status="N/A", pct=100, on_battery=False, remaining="—")
|
|
75
|
+
|
|
76
|
+
batt_info = _run("pmset -g batt")
|
|
77
|
+
on_battery = "Battery Power" in batt_info
|
|
78
|
+
|
|
79
|
+
m = re.search(r"(\d+)%", batt_info)
|
|
80
|
+
pct = int(m.group(1)) if m else 100
|
|
81
|
+
|
|
82
|
+
m_rem = re.search(r"(\d+:\d+ remaining)", batt_info)
|
|
83
|
+
remaining = m_rem.group(1) if m_rem else "—"
|
|
84
|
+
|
|
85
|
+
if on_battery:
|
|
86
|
+
if pct < BATTERY_CRITICAL:
|
|
87
|
+
status = "CRITICAL"
|
|
88
|
+
elif pct < BATTERY_LOW:
|
|
89
|
+
status = "LOW"
|
|
90
|
+
else:
|
|
91
|
+
status = "ON_BATTERY"
|
|
92
|
+
else:
|
|
93
|
+
status = "AC"
|
|
94
|
+
|
|
95
|
+
return BatteryInfo(status=status, pct=pct, on_battery=on_battery, remaining=remaining)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def check_memory() -> MemoryInfo:
|
|
99
|
+
status = "OK"
|
|
100
|
+
used_pct = 0
|
|
101
|
+
pressure = "normal"
|
|
102
|
+
|
|
103
|
+
if shutil.which("memory_pressure"):
|
|
104
|
+
mp_out = _run("memory_pressure 2>/dev/null | tail -1")
|
|
105
|
+
if "critical" in mp_out.lower():
|
|
106
|
+
pressure = "critical"
|
|
107
|
+
status = "CRITICAL"
|
|
108
|
+
elif "warn" in mp_out.lower():
|
|
109
|
+
pressure = "warn"
|
|
110
|
+
status = "WARNING"
|
|
111
|
+
|
|
112
|
+
if shutil.which("vm_stat"):
|
|
113
|
+
vm_out = _run("vm_stat")
|
|
114
|
+
lines = vm_out.splitlines()
|
|
115
|
+
if lines:
|
|
116
|
+
# page size
|
|
117
|
+
ps_match = re.search(r"(\d+)", lines[0])
|
|
118
|
+
page_size = int(ps_match.group(1)) if ps_match else 4096
|
|
119
|
+
|
|
120
|
+
def _extract(label: str) -> int:
|
|
121
|
+
for ln in lines:
|
|
122
|
+
if label in ln:
|
|
123
|
+
m = re.search(r"(\d+)", ln.split(":")[-1])
|
|
124
|
+
return int(m.group(1)) if m else 0
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
pages_free = _extract("Pages free")
|
|
128
|
+
pages_speculative = _extract("Pages speculative")
|
|
129
|
+
|
|
130
|
+
total_bytes_str = _run("sysctl -n hw.memsize", "0")
|
|
131
|
+
total_bytes = int(total_bytes_str) if total_bytes_str.isdigit() else 0
|
|
132
|
+
|
|
133
|
+
if total_bytes > 0:
|
|
134
|
+
total_mb = total_bytes // (1024 * 1024)
|
|
135
|
+
free_pages = pages_free + pages_speculative
|
|
136
|
+
free_mb = (free_pages * page_size) // (1024 * 1024)
|
|
137
|
+
if total_mb > 0:
|
|
138
|
+
used_pct = (total_mb - free_mb) * 100 // total_mb
|
|
139
|
+
|
|
140
|
+
return MemoryInfo(status=status, used_pct=used_pct, pressure=pressure)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def check_cpu() -> CPUInfo:
|
|
144
|
+
status = "OK"
|
|
145
|
+
usage = 0
|
|
146
|
+
|
|
147
|
+
if shutil.which("top"):
|
|
148
|
+
top_out = _run("top -l 1 -n 0 2>/dev/null")
|
|
149
|
+
for line in top_out.splitlines():
|
|
150
|
+
if "CPU usage" in line:
|
|
151
|
+
m = re.search(r"([\d.]+)%\s*idle", line)
|
|
152
|
+
if m:
|
|
153
|
+
idle = float(m.group(1))
|
|
154
|
+
usage = int(round(100 - idle))
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
if usage >= CPU_SATURATED:
|
|
158
|
+
status = "SATURATED"
|
|
159
|
+
|
|
160
|
+
return CPUInfo(status=status, usage=usage)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# State tracking for notification dedup
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _state_file() -> Path:
|
|
169
|
+
try:
|
|
170
|
+
from lib.common import find_claudes_dir
|
|
171
|
+
|
|
172
|
+
return find_claudes_dir() / "resource-monitor-state"
|
|
173
|
+
except Exception:
|
|
174
|
+
return Path("/tmp/resource-monitor-state")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _load_prev_state() -> str:
|
|
178
|
+
sf = _state_file()
|
|
179
|
+
if sf.exists():
|
|
180
|
+
return sf.read_text().strip()
|
|
181
|
+
return "AC|normal|OK"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _save_state(state: str) -> None:
|
|
185
|
+
_state_file().write_text(state + "\n")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def notify_if_changed(batt: BatteryInfo, mem: MemoryInfo, cpu: CPUInfo, board_cmd: str | None = None) -> None:
|
|
189
|
+
"""Send board notifications only on state transitions."""
|
|
190
|
+
current = f"{batt.status}|{mem.status}|{cpu.status}"
|
|
191
|
+
prev = _load_prev_state()
|
|
192
|
+
if current == prev:
|
|
193
|
+
return
|
|
194
|
+
_save_state(current)
|
|
195
|
+
|
|
196
|
+
if board_cmd is None:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
def _send(msg: str) -> None:
|
|
200
|
+
try:
|
|
201
|
+
subprocess.run(
|
|
202
|
+
[board_cmd, "--as", "monitor", "send", "All", msg],
|
|
203
|
+
capture_output=True,
|
|
204
|
+
timeout=10,
|
|
205
|
+
)
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
# Battery transitions
|
|
210
|
+
if batt.status == "CRITICAL":
|
|
211
|
+
_send(f"[BATTERY CRITICAL] {batt.pct}% remaining. Suspending non-essential sessions recommended.")
|
|
212
|
+
elif batt.status == "LOW":
|
|
213
|
+
_send(f"[BATTERY LOW] {batt.pct}%. Consider reducing active sessions.")
|
|
214
|
+
elif batt.status == "ON_BATTERY" and prev.startswith("AC"):
|
|
215
|
+
_send(f"[BATTERY] Switched to battery power ({batt.pct}%).")
|
|
216
|
+
|
|
217
|
+
# Memory transitions
|
|
218
|
+
if mem.status == "CRITICAL" and "CRITICAL" not in prev:
|
|
219
|
+
_send("[MEMORY CRITICAL] System under memory pressure. Save state + reduce sessions.")
|
|
220
|
+
elif mem.status == "WARNING" and ("normal" in prev or "OK" in prev):
|
|
221
|
+
_send("[MEMORY WARNING] Memory pressure rising. Monitor closely.")
|
|
222
|
+
|
|
223
|
+
# CPU transitions
|
|
224
|
+
if cpu.status == "SATURATED" and "SATURATED" not in prev:
|
|
225
|
+
_send(f"[CPU SATURATED] CPU > {CPU_SATURATED}%. Avoid concurrent builds.")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# JSON output helper (used by dispatcher)
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def to_json(batt: BatteryInfo, mem: MemoryInfo, cpu: CPUInfo) -> str:
|
|
234
|
+
return json.dumps(
|
|
235
|
+
{
|
|
236
|
+
"battery": {
|
|
237
|
+
"status": batt.status,
|
|
238
|
+
"pct": batt.pct,
|
|
239
|
+
"on_battery": batt.on_battery,
|
|
240
|
+
"remaining": batt.remaining,
|
|
241
|
+
},
|
|
242
|
+
"memory": {
|
|
243
|
+
"status": mem.status,
|
|
244
|
+
"used_pct": mem.used_pct,
|
|
245
|
+
"pressure": mem.pressure,
|
|
246
|
+
},
|
|
247
|
+
"cpu": {
|
|
248
|
+
"status": cpu.status,
|
|
249
|
+
"usage": cpu.usage,
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_all() -> tuple:
|
|
256
|
+
"""Return (BatteryInfo, MemoryInfo, CPUInfo) tuple."""
|
|
257
|
+
return check_battery(), check_memory(), check_cpu()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# CLI
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def print_status(mode: str = "status") -> None:
|
|
266
|
+
batt, mem, cpu = get_all()
|
|
267
|
+
|
|
268
|
+
if mode == "json":
|
|
269
|
+
print(to_json(batt, mem, cpu))
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
print("Resource Monitor")
|
|
273
|
+
print("================")
|
|
274
|
+
print()
|
|
275
|
+
|
|
276
|
+
line = f"Battery: {batt.status}"
|
|
277
|
+
if batt.status != "N/A":
|
|
278
|
+
line += f" ({batt.pct}%)"
|
|
279
|
+
if batt.remaining != "—":
|
|
280
|
+
line += f" {batt.remaining}"
|
|
281
|
+
print(line)
|
|
282
|
+
print(f"Memory: {mem.status} ({mem.used_pct}% used, pressure: {mem.pressure})")
|
|
283
|
+
print(f"CPU: {cpu.status} ({cpu.usage}% usage)")
|
|
284
|
+
print()
|
|
285
|
+
|
|
286
|
+
has_issue = False
|
|
287
|
+
if batt.status == "CRITICAL":
|
|
288
|
+
print("! CRITICAL: Suspend all non-essential sessions NOW.")
|
|
289
|
+
has_issue = True
|
|
290
|
+
elif batt.status == "LOW":
|
|
291
|
+
print("! Battery low: reduce to 2-3 sessions.")
|
|
292
|
+
has_issue = True
|
|
293
|
+
elif batt.status == "ON_BATTERY":
|
|
294
|
+
print("* Running on battery. Monitor usage.")
|
|
295
|
+
has_issue = True
|
|
296
|
+
|
|
297
|
+
if mem.status == "CRITICAL":
|
|
298
|
+
print("! CRITICAL: Memory pressure critical. Restart largest session.")
|
|
299
|
+
has_issue = True
|
|
300
|
+
elif mem.status == "WARNING":
|
|
301
|
+
print("! Memory pressure elevated. Consider suspending idle sessions.")
|
|
302
|
+
has_issue = True
|
|
303
|
+
|
|
304
|
+
if cpu.status == "SATURATED":
|
|
305
|
+
print("! CPU saturated. Avoid concurrent builds.")
|
|
306
|
+
has_issue = True
|
|
307
|
+
|
|
308
|
+
if not has_issue:
|
|
309
|
+
print("All resources nominal.")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def main() -> None:
|
|
313
|
+
mode = "status"
|
|
314
|
+
for arg in sys.argv[1:]:
|
|
315
|
+
if arg == "--watch":
|
|
316
|
+
mode = "watch"
|
|
317
|
+
elif arg == "--json":
|
|
318
|
+
mode = "json"
|
|
319
|
+
|
|
320
|
+
if mode == "watch":
|
|
321
|
+
print("Resource monitor started (interval: 30s)")
|
|
322
|
+
board_cmd = None
|
|
323
|
+
try:
|
|
324
|
+
from lib.common import find_claudes_dir
|
|
325
|
+
|
|
326
|
+
bc = find_claudes_dir().parent / "board"
|
|
327
|
+
if bc.exists():
|
|
328
|
+
board_cmd = str(bc)
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
while True:
|
|
333
|
+
batt, mem, cpu = get_all()
|
|
334
|
+
notify_if_changed(batt, mem, cpu, board_cmd)
|
|
335
|
+
time.sleep(30)
|
|
336
|
+
else:
|
|
337
|
+
print_status(mode)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
if __name__ == "__main__":
|
|
341
|
+
main()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
-- 001_foreign_keys.sql
|
|
2
|
+
-- Add foreign key constraints to prevent orphaned data.
|
|
3
|
+
-- All cascading rules use CASCADE so that deleting a session/proposal/thread
|
|
4
|
+
-- automatically cleans up related rows.
|
|
5
|
+
|
|
6
|
+
PRAGMA foreign_keys = ON;
|
|
7
|
+
|
|
8
|
+
-- Create a pseudo-session 'all' so FK on messages.recipient works.
|
|
9
|
+
-- 'all' is the broadcast pseudo-recipient used by send-to-all.
|
|
10
|
+
INSERT OR IGNORE INTO sessions(name, status, updated_at)
|
|
11
|
+
VALUES ('all', 'system', strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime'));
|
|
12
|
+
|
|
13
|
+
-- inbox: must reference valid session + valid message
|
|
14
|
+
-- Rebuild with FK (SQLite requires recreating the table)
|
|
15
|
+
CREATE TABLE IF NOT EXISTS inbox_new (
|
|
16
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
17
|
+
session TEXT NOT NULL REFERENCES sessions(name) ON DELETE CASCADE,
|
|
18
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
19
|
+
delivered_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M', 'now', 'localtime')),
|
|
20
|
+
read INTEGER DEFAULT 0
|
|
21
|
+
);
|
|
22
|
+
INSERT INTO inbox_new SELECT * FROM inbox;
|
|
23
|
+
DROP TABLE inbox;
|
|
24
|
+
ALTER TABLE inbox_new RENAME TO inbox;
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_inbox ON inbox(session, read);
|
|
26
|
+
|
|
27
|
+
-- votes: must reference valid proposal
|
|
28
|
+
CREATE TABLE IF NOT EXISTS votes_new (
|
|
29
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
+
proposal_id INTEGER NOT NULL REFERENCES proposals(id) ON DELETE CASCADE,
|
|
31
|
+
voter TEXT NOT NULL,
|
|
32
|
+
decision TEXT NOT NULL,
|
|
33
|
+
reason TEXT DEFAULT '',
|
|
34
|
+
ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M', 'now', 'localtime')),
|
|
35
|
+
UNIQUE(proposal_id, voter)
|
|
36
|
+
);
|
|
37
|
+
INSERT INTO votes_new SELECT * FROM votes;
|
|
38
|
+
DROP TABLE votes;
|
|
39
|
+
ALTER TABLE votes_new RENAME TO votes;
|
|
40
|
+
|
|
41
|
+
-- thread_replies: must reference valid thread
|
|
42
|
+
CREATE TABLE IF NOT EXISTS thread_replies_new (
|
|
43
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
|
|
45
|
+
author TEXT NOT NULL,
|
|
46
|
+
body TEXT NOT NULL,
|
|
47
|
+
ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M', 'now', 'localtime'))
|
|
48
|
+
);
|
|
49
|
+
INSERT INTO thread_replies_new SELECT * FROM thread_replies;
|
|
50
|
+
DROP TABLE thread_replies;
|
|
51
|
+
ALTER TABLE thread_replies_new RENAME TO thread_replies;
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_replies ON thread_replies(thread_id);
|
|
53
|
+
|
|
54
|
+
-- suspended: must reference valid session
|
|
55
|
+
CREATE TABLE IF NOT EXISTS suspended_new (
|
|
56
|
+
name TEXT PRIMARY KEY REFERENCES sessions(name) ON DELETE CASCADE,
|
|
57
|
+
suspended_by TEXT NOT NULL,
|
|
58
|
+
ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M', 'now', 'localtime'))
|
|
59
|
+
);
|
|
60
|
+
INSERT INTO suspended_new SELECT * FROM suspended;
|
|
61
|
+
DROP TABLE suspended;
|
|
62
|
+
ALTER TABLE suspended_new RENAME TO suspended;
|
|
63
|
+
|
|
64
|
+
-- tasks: must reference valid session
|
|
65
|
+
CREATE TABLE IF NOT EXISTS tasks_new (
|
|
66
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
+
session TEXT NOT NULL REFERENCES sessions(name) ON DELETE CASCADE,
|
|
68
|
+
description TEXT NOT NULL,
|
|
69
|
+
status TEXT DEFAULT 'pending',
|
|
70
|
+
priority INTEGER DEFAULT 0,
|
|
71
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M', 'now', 'localtime')),
|
|
72
|
+
done_at TEXT DEFAULT NULL
|
|
73
|
+
);
|
|
74
|
+
INSERT INTO tasks_new SELECT * FROM tasks;
|
|
75
|
+
DROP TABLE tasks;
|
|
76
|
+
ALTER TABLE tasks_new RENAME TO tasks;
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_tasks ON tasks(session, status);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE sessions ADD COLUMN persona TEXT DEFAULT '';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS mailbox(
|
|
2
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3
|
+
ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M','now','localtime')),
|
|
4
|
+
sender TEXT NOT NULL,
|
|
5
|
+
recipient TEXT NOT NULL,
|
|
6
|
+
encrypted_body TEXT NOT NULL,
|
|
7
|
+
read INTEGER DEFAULT 0
|
|
8
|
+
);
|
|
9
|
+
CREATE INDEX IF NOT EXISTS idx_mailbox ON mailbox(recipient, read);
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-nb",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Multi-agent coordination framework for Claude Code sessions",
|
|
5
|
+
"bin": {
|
|
6
|
+
"cnb": "./bin/cnb.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"lib/",
|
|
11
|
+
"!lib/**/__pycache__/",
|
|
12
|
+
"!lib/**/*.pyc",
|
|
13
|
+
"migrations/",
|
|
14
|
+
"registry/",
|
|
15
|
+
"schema.sql",
|
|
16
|
+
"pyproject.toml",
|
|
17
|
+
"VERSION",
|
|
18
|
+
"Makefile"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"claude",
|
|
22
|
+
"multi-agent",
|
|
23
|
+
"coordination",
|
|
24
|
+
"ai",
|
|
25
|
+
"coding-agent"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT"
|
|
28
|
+
}
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "claude-nb"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Multi-agent coordination framework for Claude Code sessions"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "OpenAll-1.0"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"cryptography>=41.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
cnb = "lib.cli:main"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
include = ["lib*"]
|
|
20
|
+
|
|
21
|
+
# ---------- pytest ----------
|
|
22
|
+
[tool.pytest.ini_options]
|
|
23
|
+
testpaths = ["tests"]
|
|
24
|
+
python_files = ["test_*.py"]
|
|
25
|
+
python_classes = ["Test*"]
|
|
26
|
+
python_functions = ["test_*"]
|
|
27
|
+
addopts = "-v --tb=short"
|
|
28
|
+
markers = [
|
|
29
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
30
|
+
"integration: marks integration tests that may need external services",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# ---------- ruff ----------
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
target-version = "py311"
|
|
36
|
+
line-length = 120
|
|
37
|
+
|
|
38
|
+
[tool.ruff.lint]
|
|
39
|
+
select = [
|
|
40
|
+
"E", # pycodestyle errors
|
|
41
|
+
"W", # pycodestyle warnings
|
|
42
|
+
"F", # pyflakes
|
|
43
|
+
"I", # isort
|
|
44
|
+
"UP", # pyupgrade
|
|
45
|
+
"B", # flake8-bugbear
|
|
46
|
+
"SIM", # flake8-simplify
|
|
47
|
+
"RUF", # ruff-specific
|
|
48
|
+
]
|
|
49
|
+
ignore = [
|
|
50
|
+
"E501", # line too long (handled by formatter)
|
|
51
|
+
"E741", # ambiguous variable name (short names like s/o are intentional)
|
|
52
|
+
"B904", # raise ... from err in except (too noisy for existing code)
|
|
53
|
+
"SIM105", # contextlib.suppress (try/except/pass is fine here)
|
|
54
|
+
"SIM108", # ternary operator (readability preference)
|
|
55
|
+
"RUF001", # ambiguous unicode (project uses Chinese strings)
|
|
56
|
+
"RUF015", # unnecessary iterable allocation (next(iter()) not always clearer)
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
"bin/*" = ["E402"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint.isort]
|
|
63
|
+
known-first-party = ["lib"]
|
|
64
|
+
|
|
65
|
+
# ---------- mypy ----------
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
python_version = "3.11"
|
|
68
|
+
warn_return_any = true
|
|
69
|
+
warn_unused_configs = true
|
|
70
|
+
disallow_untyped_defs = false
|
|
71
|
+
check_untyped_defs = true
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"block": 1,
|
|
3
|
+
"name": "meridian",
|
|
4
|
+
"display_name": "Claude Meridian",
|
|
5
|
+
"type": "agent",
|
|
6
|
+
"role": "lead",
|
|
7
|
+
"description": "统筹协调,社区维护",
|
|
8
|
+
"created": "2026-05-06",
|
|
9
|
+
"prev": "9b0f017ebc29a808",
|
|
10
|
+
"chain": "82a167d",
|
|
11
|
+
"content_hash": "93545ea3438c107c"
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"block": 2,
|
|
3
|
+
"name": "forge",
|
|
4
|
+
"display_name": "Claude Forge",
|
|
5
|
+
"type": "agent",
|
|
6
|
+
"role": "active-dev",
|
|
7
|
+
"description": "活跃开发者,GitHub 社区维护",
|
|
8
|
+
"created": "2026-05-06",
|
|
9
|
+
"prev": "93545ea3438c107c",
|
|
10
|
+
"chain": "4a3c92e",
|
|
11
|
+
"content_hash": "ca0ad406fd4e42ad"
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"block": 3,
|
|
3
|
+
"name": "lead",
|
|
4
|
+
"display_name": "Claude Lead",
|
|
5
|
+
"type": "agent",
|
|
6
|
+
"role": "active-dev",
|
|
7
|
+
"description": "用户侧团队 lead,产品方向与可行性评估",
|
|
8
|
+
"created": "2026-05-06",
|
|
9
|
+
"prev": "ca0ad406fd4e42ad",
|
|
10
|
+
"chain": "e665a7e",
|
|
11
|
+
"content_hash": "2db15fd7267a8dcc"
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"block": 4,
|
|
3
|
+
"name": "encrypted-mailbox-live",
|
|
4
|
+
"type": "project",
|
|
5
|
+
"milestone": true,
|
|
6
|
+
"created": "2026-05-06",
|
|
7
|
+
"description": "加密信箱端到端验证通过",
|
|
8
|
+
"detail": "基于 X25519 sealed-box 的异步加密消息机制上线。密钥对本地生成,公钥写入 pubkeys.json,加密消息通过 GitHub Release assets 投递。meridian↔lead 首次成功互通。",
|
|
9
|
+
"prev": "2db15fd7267a8dcc",
|
|
10
|
+
"chain": null,
|
|
11
|
+
"content_hash": "fcaf497872180e90"
|
|
12
|
+
}
|