uv-suite 0.28.0 → 0.30.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 +21 -0
- package/README.md +58 -35
- package/agents/claude-code/anti-slop-guard.md +14 -1
- package/agents/claude-code/architect.md +30 -4
- package/agents/claude-code/cartographer.md +18 -6
- package/agents/claude-code/eval-writer.md +7 -2
- package/agents/claude-code/reviewer.md +5 -1
- package/agents/claude-code/spec-writer.md +30 -7
- package/agents/generate.py +88 -0
- package/bin/cli.js +51 -48
- package/hooks/auto-checkpoint-helper.sh +2 -2
- package/hooks/auto-checkpoint.sh +3 -3
- package/hooks/auto-restore-on-start.sh +30 -0
- package/hooks/checkpoint-helper.sh +40 -35
- package/hooks/git-context.sh +41 -0
- package/hooks/lite-mode-inject.sh +26 -0
- package/hooks/session-end-helper.sh +2 -2
- package/hooks/session-end.sh +2 -2
- package/hooks/session-label-nag.sh +2 -2
- package/hooks/session-meta.sh +18 -1
- package/hooks/session-review-reminder.sh +2 -2
- package/hooks/session-start.sh +16 -0
- package/hooks/slop-grep.sh +12 -31
- package/hooks/uv-out-best.sh +20 -0
- package/hooks/uv-out-collect.sh +52 -0
- package/hooks/uv-out-notify.sh +28 -0
- package/hooks/uv-out-pointer.sh +16 -0
- package/hooks/uv-out-session.sh +24 -0
- package/hooks/watchtower-notify.sh +45 -0
- package/hooks/watchtower-send.sh +4 -0
- package/install.sh +93 -42
- package/package.json +2 -2
- package/personas/auto.json +40 -1
- package/personas/professional.json +46 -1
- package/personas/spike.json +32 -2
- package/personas/sport.json +44 -1
- package/settings.json +6 -2
- package/skills/architect/SKILL.md +109 -8
- package/skills/architect/specialists/distributed-systems.md +84 -0
- package/skills/architect/specialists/full-stack.md +92 -0
- package/skills/architect/specialists/llm-ai-engineering.md +86 -0
- package/skills/architect/specialists/ml-systems.md +81 -0
- package/skills/commit/SKILL.md +5 -2
- package/skills/confirm/SKILL.md +3 -3
- package/skills/investigate/SKILL.md +14 -4
- package/skills/lite/SKILL.md +45 -0
- package/skills/qa/SKILL.md +274 -0
- package/skills/review/SKILL.md +187 -8
- package/skills/review/specialists/api-contract.md +122 -0
- package/skills/review/specialists/architecture-trace.md +64 -0
- package/skills/review/specialists/data-migration.md +113 -0
- package/skills/review/specialists/maintainability.md +138 -0
- package/skills/review/specialists/performance.md +115 -0
- package/skills/review/specialists/security.md +132 -0
- package/skills/review/specialists/testing.md +109 -0
- package/skills/session/SKILL.md +87 -0
- package/skills/session/operations/auto.md +22 -0
- package/skills/session/operations/checkpoint.md +43 -0
- package/skills/session/operations/end.md +35 -0
- package/skills/session/operations/init.md +16 -0
- package/skills/session/operations/restore.md +16 -0
- package/skills/spec/SKILL.md +40 -1
- package/skills/test/SKILL.md +89 -0
- package/skills/test/specialists/eval.md +46 -0
- package/skills/test/specialists/integration.md +42 -0
- package/skills/test/specialists/unit.md +39 -0
- package/skills/understand/SKILL.md +118 -0
- package/skills/understand/modes/repo.md +38 -0
- package/skills/understand/modes/stack.md +41 -0
- package/skills/uv-help/SKILL.md +43 -20
- package/uv.sh +36 -3
- package/watchtower/Dockerfile +9 -0
- package/watchtower/README.md +78 -0
- package/watchtower/app/__init__.py +0 -0
- package/watchtower/app/__pycache__/__init__.cpython-314.pyc +0 -0
- package/watchtower/app/__pycache__/db.cpython-314.pyc +0 -0
- package/watchtower/app/__pycache__/main.cpython-314.pyc +0 -0
- package/watchtower/app/__pycache__/models.cpython-314.pyc +0 -0
- package/watchtower/app/db.py +85 -0
- package/watchtower/app/main.py +45 -0
- package/watchtower/app/models.py +49 -0
- package/watchtower/app/routers/__init__.py +0 -0
- package/watchtower/app/routers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/watchtower/app/routers/__pycache__/control.cpython-314.pyc +0 -0
- package/watchtower/app/routers/__pycache__/ingest.cpython-314.pyc +0 -0
- package/watchtower/app/routers/__pycache__/query.cpython-314.pyc +0 -0
- package/watchtower/app/routers/__pycache__/stream.cpython-314.pyc +0 -0
- package/watchtower/app/routers/control.py +144 -0
- package/watchtower/app/routers/ingest.py +102 -0
- package/watchtower/app/routers/query.py +84 -0
- package/watchtower/app/routers/stream.py +30 -0
- package/watchtower/app/services/__init__.py +0 -0
- package/watchtower/app/services/__pycache__/__init__.cpython-314.pyc +0 -0
- package/watchtower/app/services/__pycache__/checkpoint.cpython-314.pyc +0 -0
- package/watchtower/app/services/__pycache__/tmux.cpython-314.pyc +0 -0
- package/watchtower/app/services/checkpoint.py +107 -0
- package/watchtower/app/services/tmux.py +54 -0
- package/watchtower/docker-compose.yml +22 -0
- package/watchtower/events.json +10373 -1
- package/watchtower/{auto-checkpoint-runner.js → legacy/auto-checkpoint-runner.js} +29 -2
- package/watchtower/{dashboard.html → legacy/dashboard.html} +261 -0
- package/watchtower/{server.js → legacy/server.js} +63 -0
- package/watchtower/legacy/snapshot-manager.js +305 -0
- package/watchtower/requirements.txt +3 -0
- package/watchtower/schema.sql +43 -0
- package/watchtower/static/dashboard.html +449 -0
- package/agents/claude-code/devops.md +0 -50
- package/agents/claude-code/security.md +0 -75
- package/agents/codex/anti-slop-guard.toml +0 -12
- package/agents/codex/architect.toml +0 -11
- package/agents/codex/cartographer.toml +0 -16
- package/agents/codex/devops.toml +0 -8
- package/agents/codex/eval-writer.toml +0 -11
- package/agents/codex/prototype-builder.toml +0 -10
- package/agents/codex/reviewer.toml +0 -16
- package/agents/codex/security.toml +0 -14
- package/agents/codex/spec-writer.toml +0 -11
- package/agents/codex/test-writer.toml +0 -13
- package/agents/cursor/anti-slop-guard.mdc +0 -22
- package/agents/cursor/architect.mdc +0 -24
- package/agents/cursor/cartographer.mdc +0 -28
- package/agents/cursor/devops.mdc +0 -16
- package/agents/cursor/eval-writer.mdc +0 -21
- package/agents/cursor/prototype-builder.mdc +0 -25
- package/agents/cursor/reviewer.mdc +0 -26
- package/agents/cursor/security.mdc +0 -20
- package/agents/cursor/spec-writer.mdc +0 -27
- package/agents/cursor/test-writer.mdc +0 -28
- package/agents/portable/anti-slop-guard.md +0 -71
- package/agents/portable/architect.md +0 -83
- package/agents/portable/cartographer.md +0 -64
- package/agents/portable/devops.md +0 -56
- package/agents/portable/eval-writer.md +0 -70
- package/agents/portable/prototype-builder.md +0 -70
- package/agents/portable/reviewer.md +0 -79
- package/agents/portable/security.md +0 -63
- package/agents/portable/spec-writer.md +0 -89
- package/agents/portable/test-writer.md +0 -56
- package/hooks/context-warning.sh +0 -4
- package/skills/auto-checkpoint/SKILL.md +0 -47
- package/skills/checkpoint/SKILL.md +0 -105
- package/skills/map-codebase/SKILL.md +0 -54
- package/skills/map-stack/SKILL.md +0 -121
- package/skills/restore/SKILL.md +0 -55
- package/skills/security-review/SKILL.md +0 -87
- package/skills/session-end/SKILL.md +0 -100
- package/skills/session-init/SKILL.md +0 -45
- package/skills/slop-check/SKILL.md +0 -40
- package/skills/write-evals/SKILL.md +0 -34
- package/skills/write-tests/SKILL.md +0 -54
- /package/watchtower/{auto-checkpoint-prompt.md → legacy/auto-checkpoint-prompt.md} +0 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// UV Suite Watchtower — snapshot + restore.
|
|
2
|
+
//
|
|
3
|
+
// A "snapshot" bundles the manifest of every active session at a moment in
|
|
4
|
+
// time (sid, cwd, persona, latest checkpoint path) plus a copy of the
|
|
5
|
+
// watchtower's recent event store. It also triggers an immediate Tier B
|
|
6
|
+
// auto-checkpoint for each session so the latest summary is captured.
|
|
7
|
+
//
|
|
8
|
+
// "Restore" doesn't restart Claude Code (we can't — processes are dead).
|
|
9
|
+
// On macOS / Linux it spawns a new terminal tab per session via the OS's
|
|
10
|
+
// AppleScript or terminal-emulator command, passing UVS_RESTORE_FROM=<old-sid>.
|
|
11
|
+
// The next `uvs` launch picks that up and auto-runs `/restore <old-sid>` on
|
|
12
|
+
// turn 1 via the SessionStart hook.
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const os = require("os");
|
|
17
|
+
const { spawn } = require("child_process");
|
|
18
|
+
|
|
19
|
+
const SNAPSHOTS_DIR = path.join(os.homedir(), ".uv-suite", "snapshots");
|
|
20
|
+
const ACTIVE_WINDOW_MS = 60 * 60 * 1000; // sessions with activity in last 1h
|
|
21
|
+
|
|
22
|
+
function ensureSnapshotsDir() {
|
|
23
|
+
fs.mkdirSync(SNAPSHOTS_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Group recent events by uvs_session_id, take the most recent metadata for
|
|
27
|
+
// each. Returns one record per active session.
|
|
28
|
+
function activeSessionsFromEvents(events) {
|
|
29
|
+
const cutoff = Date.now() - ACTIVE_WINDOW_MS;
|
|
30
|
+
const bySession = new Map();
|
|
31
|
+
for (const ev of events) {
|
|
32
|
+
const sid = ev.uvs_session_id || ev.session_id;
|
|
33
|
+
if (!sid) continue;
|
|
34
|
+
if ((ev._ts || 0) < cutoff) continue;
|
|
35
|
+
// Skip if session was explicitly terminated
|
|
36
|
+
if (ev.lifecycle === "terminated") {
|
|
37
|
+
bySession.delete(sid);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const existing = bySession.get(sid) || {
|
|
41
|
+
uvs_session_id: sid,
|
|
42
|
+
session_id: ev.session_id || sid,
|
|
43
|
+
cwd: ev.cwd,
|
|
44
|
+
name: ev.session_name || "",
|
|
45
|
+
kind: ev.session_kind || "",
|
|
46
|
+
purpose: ev.session_purpose || "",
|
|
47
|
+
priority: ev.session_priority || "",
|
|
48
|
+
persona: ev.persona || "",
|
|
49
|
+
first_event_ts: ev._ts,
|
|
50
|
+
last_event_ts: ev._ts,
|
|
51
|
+
event_count: 0,
|
|
52
|
+
};
|
|
53
|
+
existing.last_event_ts = Math.max(existing.last_event_ts || 0, ev._ts || 0);
|
|
54
|
+
existing.first_event_ts = Math.min(
|
|
55
|
+
existing.first_event_ts || Infinity,
|
|
56
|
+
ev._ts || Infinity,
|
|
57
|
+
);
|
|
58
|
+
existing.event_count++;
|
|
59
|
+
// Refresh metadata fields from latest event (in case /session-init relabeled)
|
|
60
|
+
if (ev.session_name) existing.name = ev.session_name;
|
|
61
|
+
if (ev.session_kind) existing.kind = ev.session_kind;
|
|
62
|
+
if (ev.session_purpose) existing.purpose = ev.session_purpose;
|
|
63
|
+
if (ev.session_priority) existing.priority = ev.session_priority;
|
|
64
|
+
if (ev.persona) existing.persona = ev.persona;
|
|
65
|
+
if (ev.session_id) existing.session_id = ev.session_id;
|
|
66
|
+
bySession.set(sid, existing);
|
|
67
|
+
}
|
|
68
|
+
return [...bySession.values()];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Find each session's latest checkpoint file (auto-* or final-*)
|
|
72
|
+
function findLatestCheckpoint(cwd, sid) {
|
|
73
|
+
const dir = path.join(cwd, "uv-out", "checkpoints", sid);
|
|
74
|
+
if (!fs.existsSync(dir)) return null;
|
|
75
|
+
try {
|
|
76
|
+
const files = fs
|
|
77
|
+
.readdirSync(dir)
|
|
78
|
+
.filter((f) => f.endsWith(".md") && f !== "latest.md");
|
|
79
|
+
if (files.length === 0) return null;
|
|
80
|
+
// Sort by mtime, newest first
|
|
81
|
+
files.sort((a, b) => {
|
|
82
|
+
const ma = fs.statSync(path.join(dir, a)).mtimeMs;
|
|
83
|
+
const mb = fs.statSync(path.join(dir, b)).mtimeMs;
|
|
84
|
+
return mb - ma;
|
|
85
|
+
});
|
|
86
|
+
return path.join(dir, files[0]);
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Read first few lines of summary section from a checkpoint file for the
|
|
93
|
+
// snapshot's per-session preview.
|
|
94
|
+
function readCheckpointSummary(cpPath) {
|
|
95
|
+
if (!cpPath || !fs.existsSync(cpPath)) return "";
|
|
96
|
+
try {
|
|
97
|
+
const content = fs.readFileSync(cpPath, "utf-8");
|
|
98
|
+
// Strip frontmatter
|
|
99
|
+
const body = content.replace(/^---[\s\S]*?---\s*/m, "");
|
|
100
|
+
// Look for ## Summary section
|
|
101
|
+
const m = body.match(/##\s*Summary\s*\n+([\s\S]*?)(?=\n##\s|\n#\s|$)/);
|
|
102
|
+
if (m) return m[1].trim().slice(0, 600);
|
|
103
|
+
// Fallback: first 400 chars of body
|
|
104
|
+
return body.trim().slice(0, 400);
|
|
105
|
+
} catch {
|
|
106
|
+
return "";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function takeSnapshot({ runner, getEvents, broadcast }) {
|
|
111
|
+
ensureSnapshotsDir();
|
|
112
|
+
|
|
113
|
+
// Trigger an immediate Tier B tick so the snapshot bundles the freshest
|
|
114
|
+
// possible summaries. We don't block on broadcast; just await the tick.
|
|
115
|
+
if (runner && runner.tick) {
|
|
116
|
+
await runner.tick({ getEvents, broadcast });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const events = getEvents();
|
|
120
|
+
const sessions = activeSessionsFromEvents(events);
|
|
121
|
+
|
|
122
|
+
const ts = new Date();
|
|
123
|
+
const id = ts
|
|
124
|
+
.toISOString()
|
|
125
|
+
.replace(/[T:]/g, "-")
|
|
126
|
+
.replace(/\.\d+Z$/, "")
|
|
127
|
+
.replace(/-(\d\d)$/, "$1");
|
|
128
|
+
const dir = path.join(SNAPSHOTS_DIR, id);
|
|
129
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
130
|
+
|
|
131
|
+
for (const s of sessions) {
|
|
132
|
+
s.latest_checkpoint = findLatestCheckpoint(s.cwd, s.uvs_session_id);
|
|
133
|
+
s.summary_preview = readCheckpointSummary(s.latest_checkpoint);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const manifest = {
|
|
137
|
+
id,
|
|
138
|
+
created_at: ts.toISOString(),
|
|
139
|
+
created_at_epoch: Math.floor(ts.getTime() / 1000),
|
|
140
|
+
sessions,
|
|
141
|
+
event_count: events.length,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
fs.writeFileSync(
|
|
145
|
+
path.join(dir, "manifest.json"),
|
|
146
|
+
JSON.stringify(manifest, null, 2),
|
|
147
|
+
);
|
|
148
|
+
fs.writeFileSync(
|
|
149
|
+
path.join(dir, "events.json"),
|
|
150
|
+
JSON.stringify(events, null, 2),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Broadcast a SnapshotTaken event so it shows up in the timeline
|
|
154
|
+
const event = {
|
|
155
|
+
event_type: "SnapshotTaken",
|
|
156
|
+
snapshot_id: id,
|
|
157
|
+
session_count: sessions.length,
|
|
158
|
+
_ts: ts.getTime(),
|
|
159
|
+
};
|
|
160
|
+
broadcast(event);
|
|
161
|
+
|
|
162
|
+
return manifest;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function listSnapshots() {
|
|
166
|
+
ensureSnapshotsDir();
|
|
167
|
+
const entries = [];
|
|
168
|
+
for (const name of fs.readdirSync(SNAPSHOTS_DIR).sort().reverse()) {
|
|
169
|
+
const manifestPath = path.join(SNAPSHOTS_DIR, name, "manifest.json");
|
|
170
|
+
try {
|
|
171
|
+
const m = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
172
|
+
entries.push({
|
|
173
|
+
id: m.id,
|
|
174
|
+
created_at: m.created_at,
|
|
175
|
+
session_count: m.sessions?.length || 0,
|
|
176
|
+
});
|
|
177
|
+
} catch {
|
|
178
|
+
// skip unreadable bundles
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return entries;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getSnapshot(id) {
|
|
185
|
+
const manifestPath = path.join(SNAPSHOTS_DIR, id, "manifest.json");
|
|
186
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
187
|
+
try {
|
|
188
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Spawn a new terminal tab restoring the given session.
|
|
195
|
+
// macOS: iTerm2 if TERM_PROGRAM=iTerm.app (or UVS_TERMINAL_APP=iterm),
|
|
196
|
+
// otherwise Terminal.app. Override either way with UVS_TERMINAL_APP.
|
|
197
|
+
// Linux: prefers gnome-terminal then x-terminal-emulator
|
|
198
|
+
// Other: returns { ok: false, command: <copy-paste string> }
|
|
199
|
+
function buildRestoreCommand(session) {
|
|
200
|
+
const persona = session.persona || "professional";
|
|
201
|
+
const sid = session.uvs_session_id;
|
|
202
|
+
const cwd = session.cwd;
|
|
203
|
+
// The new uvs launch reads UVS_RESTORE_FROM and the SessionStart hook
|
|
204
|
+
// injects "run /restore <sid>" on turn 1.
|
|
205
|
+
return `cd ${shellEscape(cwd)} && UVS_RESTORE_FROM=${shellEscape(sid)} uvs claude ${shellEscape(persona)}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function shellEscape(s) {
|
|
209
|
+
return "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function appleScriptEscape(s) {
|
|
213
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Pick the macOS terminal app. UVS_TERMINAL_APP wins (values: iterm, terminal),
|
|
217
|
+
// else look at $TERM_PROGRAM, else default to Terminal.app.
|
|
218
|
+
function macTerminalApp() {
|
|
219
|
+
const explicit = (process.env.UVS_TERMINAL_APP || "").toLowerCase();
|
|
220
|
+
if (explicit === "iterm" || explicit === "iterm2") return "iterm";
|
|
221
|
+
if (explicit === "terminal") return "terminal";
|
|
222
|
+
const tp = process.env.TERM_PROGRAM || "";
|
|
223
|
+
if (tp === "iTerm.app") return "iterm";
|
|
224
|
+
return "terminal";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// iTerm2 AppleScript: opens a new tab in the front window if one exists,
|
|
228
|
+
// otherwise a new window. Both code paths end with `write text "..."`.
|
|
229
|
+
function iTermScript(command) {
|
|
230
|
+
const esc = appleScriptEscape(command);
|
|
231
|
+
return `
|
|
232
|
+
tell application "iTerm"
|
|
233
|
+
activate
|
|
234
|
+
if (count of windows) = 0 then
|
|
235
|
+
create window with default profile
|
|
236
|
+
else
|
|
237
|
+
tell current window to create tab with default profile
|
|
238
|
+
end if
|
|
239
|
+
tell current session of current window to write text "${esc}"
|
|
240
|
+
end tell
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function terminalAppScript(command) {
|
|
245
|
+
return `tell app "Terminal" to do script "${appleScriptEscape(command)}"`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function openTerminalForSession(session) {
|
|
249
|
+
const command = buildRestoreCommand(session);
|
|
250
|
+
const platform = process.platform;
|
|
251
|
+
|
|
252
|
+
if (platform === "darwin") {
|
|
253
|
+
const app = macTerminalApp();
|
|
254
|
+
const script =
|
|
255
|
+
app === "iterm" ? iTermScript(command) : terminalAppScript(command);
|
|
256
|
+
const child = spawn("osascript", ["-e", script], {
|
|
257
|
+
stdio: "ignore",
|
|
258
|
+
detached: true,
|
|
259
|
+
});
|
|
260
|
+
child.unref();
|
|
261
|
+
return { ok: true, platform, terminal_app: app, command };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (platform === "linux") {
|
|
265
|
+
// Try gnome-terminal first, fall back to x-terminal-emulator
|
|
266
|
+
const tryEmulator = (cmd, args) => {
|
|
267
|
+
try {
|
|
268
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
269
|
+
child.unref();
|
|
270
|
+
return true;
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
if (
|
|
276
|
+
tryEmulator("gnome-terminal", [
|
|
277
|
+
"--",
|
|
278
|
+
"bash",
|
|
279
|
+
"-c",
|
|
280
|
+
command + "; exec bash",
|
|
281
|
+
]) ||
|
|
282
|
+
tryEmulator("x-terminal-emulator", [
|
|
283
|
+
"-e",
|
|
284
|
+
"bash",
|
|
285
|
+
"-c",
|
|
286
|
+
command + "; exec bash",
|
|
287
|
+
])
|
|
288
|
+
) {
|
|
289
|
+
return { ok: true, platform, command };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Windows or unsupported — return the command so the dashboard can offer
|
|
294
|
+
// a "copy to clipboard" fallback.
|
|
295
|
+
return { ok: false, platform, command };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = {
|
|
299
|
+
takeSnapshot,
|
|
300
|
+
listSnapshots,
|
|
301
|
+
getSnapshot,
|
|
302
|
+
openTerminalForSession,
|
|
303
|
+
buildRestoreCommand,
|
|
304
|
+
SNAPSHOTS_DIR,
|
|
305
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- Watchtower schema (idempotent — applied on startup).
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
4
|
+
id text PRIMARY KEY, -- UVS session id
|
|
5
|
+
name text,
|
|
6
|
+
kind text,
|
|
7
|
+
purpose text,
|
|
8
|
+
priority text,
|
|
9
|
+
persona text,
|
|
10
|
+
cwd text,
|
|
11
|
+
worktree text,
|
|
12
|
+
branch text,
|
|
13
|
+
pid integer,
|
|
14
|
+
tmux_target text, -- e.g. "uvs_<sid>" (null if not owned)
|
|
15
|
+
state text DEFAULT 'active', -- active | idle | awaiting_human | terminated
|
|
16
|
+
started_at timestamptz DEFAULT now(),
|
|
17
|
+
ended_at timestamptz
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
21
|
+
id bigserial PRIMARY KEY,
|
|
22
|
+
session_id text,
|
|
23
|
+
event_type text,
|
|
24
|
+
tool_name text,
|
|
25
|
+
command text,
|
|
26
|
+
payload jsonb,
|
|
27
|
+
created_at timestamptz DEFAULT now()
|
|
28
|
+
);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_events_session_created ON events (session_id, created_at DESC);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_events_created ON events (created_at DESC);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS approvals (
|
|
33
|
+
id bigserial PRIMARY KEY,
|
|
34
|
+
session_id text,
|
|
35
|
+
tool_name text,
|
|
36
|
+
command text,
|
|
37
|
+
request jsonb,
|
|
38
|
+
status text DEFAULT 'pending', -- pending | approved | denied | expired
|
|
39
|
+
decided_by text,
|
|
40
|
+
created_at timestamptz DEFAULT now(),
|
|
41
|
+
decided_at timestamptz
|
|
42
|
+
);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_approvals_session_status ON approvals (session_id, status);
|