vibe-forge 0.8.1 → 0.8.2
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/commands/configure-vcs.md +102 -102
- package/.claude/commands/forge.md +218 -218
- package/.claude/hooks/worker-loop.js +220 -217
- package/.claude/settings.json +89 -89
- package/README.md +149 -191
- package/agents/aegis/personality.md +303 -303
- package/agents/anvil/personality.md +278 -278
- package/agents/architect/personality.md +260 -260
- package/agents/crucible/personality.md +362 -362
- package/agents/crucible-x/personality.md +210 -210
- package/agents/ember/personality.md +293 -293
- package/agents/flux/personality.md +248 -248
- package/agents/furnace/personality.md +342 -342
- package/agents/herald/personality.md +249 -249
- package/agents/oracle/personality.md +284 -284
- package/agents/pixel/personality.md +140 -140
- package/agents/planning-hub/personality.md +473 -473
- package/agents/scribe/personality.md +253 -253
- package/agents/slag/personality.md +268 -268
- package/agents/temper/personality.md +270 -270
- package/bin/cli.js +372 -372
- package/bin/forge-daemon.sh +477 -477
- package/bin/forge-setup.sh +662 -661
- package/bin/forge-spawn.sh +164 -164
- package/bin/forge.sh +566 -566
- package/docs/commands.md +8 -8
- package/package.json +77 -77
- package/{bin → src}/lib/agents.sh +177 -177
- package/{bin → src}/lib/check-aliases.js +50 -50
- package/{bin → src}/lib/colors.sh +45 -44
- package/{bin → src}/lib/config.sh +347 -347
- package/{bin → src}/lib/constants.sh +241 -241
- package/{bin → src}/lib/daemon/budgets.sh +107 -107
- package/{bin → src}/lib/daemon/dependencies.sh +146 -146
- package/{bin → src}/lib/daemon/display.sh +128 -128
- package/{bin → src}/lib/daemon/notifications.sh +273 -273
- package/{bin → src}/lib/daemon/routing.sh +93 -93
- package/{bin → src}/lib/daemon/state.sh +163 -163
- package/{bin → src}/lib/daemon/sync.sh +103 -103
- package/{bin → src}/lib/database.sh +357 -357
- package/{bin → src}/lib/frontmatter.js +106 -106
- package/{bin → src}/lib/heimdall-setup.js +113 -113
- package/{bin → src}/lib/heimdall.js +265 -265
- package/src/lib/index.sh +25 -0
- package/{bin → src}/lib/json.sh +264 -264
- package/{bin → src}/lib/terminal.js +452 -452
- package/{bin → src}/lib/util.sh +126 -126
- package/{bin → src}/lib/vcs.js +349 -349
- package/{context → templates}/project-context-template.md +122 -122
- package/config/task-template.md +0 -159
- package/config/templates/handoff-template.md +0 -40
|
@@ -1,357 +1,357 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# Vibe Forge - SQLite Database Operations
|
|
4
|
-
# Handles daemon state persistence and agent status aggregation
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
# Database file location (set after FORGE_ROOT is known)
|
|
8
|
-
FORGE_DB=""
|
|
9
|
-
|
|
10
|
-
# =============================================================================
|
|
11
|
-
# Security Functions
|
|
12
|
-
# =============================================================================
|
|
13
|
-
|
|
14
|
-
# Escape a string for safe use in SQLite queries
|
|
15
|
-
# Escapes single quotes by doubling them (SQL standard)
|
|
16
|
-
# Also removes null bytes and other dangerous characters
|
|
17
|
-
db_escape() {
|
|
18
|
-
local input="$1"
|
|
19
|
-
# Remove null bytes
|
|
20
|
-
input="${input//$'\0'/}"
|
|
21
|
-
# Escape single quotes by doubling them
|
|
22
|
-
input="${input//\'/\'\'}"
|
|
23
|
-
# Remove backslashes that could escape quotes
|
|
24
|
-
input="${input//\\/}"
|
|
25
|
-
echo "$input"
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
# Validate that input contains only safe characters for identifiers
|
|
29
|
-
# Returns 0 if safe, 1 if unsafe
|
|
30
|
-
db_validate_identifier() {
|
|
31
|
-
local input="$1"
|
|
32
|
-
# Allow only alphanumeric, underscore, hyphen
|
|
33
|
-
if [[ "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
34
|
-
return 0
|
|
35
|
-
fi
|
|
36
|
-
return 1
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
# =============================================================================
|
|
40
|
-
# Database Initialization
|
|
41
|
-
# =============================================================================
|
|
42
|
-
|
|
43
|
-
# Require FORGE_DB to be set before database operations
|
|
44
|
-
# Call this at the start of database functions that need the path
|
|
45
|
-
# Usage: db_require_init || return 1
|
|
46
|
-
db_require_init() {
|
|
47
|
-
if [[ -z "$FORGE_DB" ]]; then
|
|
48
|
-
echo "Error: FORGE_DB not set. Set FORGE_DB path before calling database functions." >&2
|
|
49
|
-
return 1
|
|
50
|
-
fi
|
|
51
|
-
return 0
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
# Check if sqlite3 is available; attempt install if missing
|
|
55
|
-
db_require_sqlite3() {
|
|
56
|
-
if command -v sqlite3 &>/dev/null; then
|
|
57
|
-
return 0
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
echo "sqlite3 not found. Attempting to install..." >&2
|
|
61
|
-
|
|
62
|
-
# Try platform-appropriate package manager
|
|
63
|
-
if command -v apt-get &>/dev/null; then
|
|
64
|
-
echo "Installing sqlite3 via apt (may require sudo password)..." >&2
|
|
65
|
-
sudo apt-get install -y sqlite3 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
66
|
-
elif command -v brew &>/dev/null; then
|
|
67
|
-
echo "Installing sqlite3 via Homebrew..." >&2
|
|
68
|
-
brew install sqlite3 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
69
|
-
elif command -v pacman &>/dev/null; then
|
|
70
|
-
echo "Installing sqlite via pacman (may require sudo password)..." >&2
|
|
71
|
-
sudo pacman -S --noconfirm sqlite 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
72
|
-
elif command -v dnf &>/dev/null; then
|
|
73
|
-
echo "Installing sqlite via dnf (may require sudo password)..." >&2
|
|
74
|
-
sudo dnf install -y sqlite 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
75
|
-
elif command -v winget &>/dev/null; then
|
|
76
|
-
echo "Installing SQLite via winget. You may see a UAC prompt; please approve it." >&2
|
|
77
|
-
winget install --id SQLite.SQLite -e --silent 2>/dev/null
|
|
78
|
-
# winget installs to Program Files but doesn't add to Git Bash PATH
|
|
79
|
-
local winget_sqlite="/c/Program Files/SQLite/sqlite3.exe"
|
|
80
|
-
if [[ -f "$winget_sqlite" ]]; then
|
|
81
|
-
export PATH="$PATH:/c/Program Files/SQLite"
|
|
82
|
-
command -v sqlite3 &>/dev/null && return 0
|
|
83
|
-
fi
|
|
84
|
-
elif command -v choco &>/dev/null; then
|
|
85
|
-
echo "Installing sqlite via Chocolatey. You may see a UAC prompt; please approve it." >&2
|
|
86
|
-
choco install sqlite -y 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
87
|
-
fi
|
|
88
|
-
|
|
89
|
-
echo "ERROR: sqlite3 is required but could not be installed automatically." >&2
|
|
90
|
-
echo "Please install sqlite3 manually:" >&2
|
|
91
|
-
echo " macOS: brew install sqlite3" >&2
|
|
92
|
-
echo " Ubuntu: sudo apt-get install sqlite3" >&2
|
|
93
|
-
echo " Fedora: sudo dnf install sqlite" >&2
|
|
94
|
-
echo " Windows: choco install sqlite" >&2
|
|
95
|
-
echo " or: winget install SQLite.SQLite" >&2
|
|
96
|
-
echo " (then add the install folder to your PATH)" >&2
|
|
97
|
-
return 1
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
# Initialize the database with schema
|
|
101
|
-
# Requires: FORGE_DB must be set before calling
|
|
102
|
-
db_init() {
|
|
103
|
-
db_require_init || return 1
|
|
104
|
-
db_require_sqlite3 || return 1
|
|
105
|
-
local db_dir
|
|
106
|
-
db_dir=$(dirname "$FORGE_DB")
|
|
107
|
-
|
|
108
|
-
# Create directory if needed
|
|
109
|
-
mkdir -p "$db_dir"
|
|
110
|
-
chmod 700 "$db_dir"
|
|
111
|
-
|
|
112
|
-
# Enable WAL mode for concurrent reads without blocking writes.
|
|
113
|
-
# WAL + NORMAL synchronous is the recommended pairing for local tooling:
|
|
114
|
-
# faster than DELETE+FULL while still durable against OS crashes.
|
|
115
|
-
sqlite3 "$FORGE_DB" "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"
|
|
116
|
-
|
|
117
|
-
# Create tables if they don't exist
|
|
118
|
-
sqlite3 "$FORGE_DB" <<'SQL'
|
|
119
|
-
-- Daemon configuration (single row)
|
|
120
|
-
CREATE TABLE IF NOT EXISTS daemon_config (
|
|
121
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
122
|
-
state TEXT DEFAULT 'idle',
|
|
123
|
-
poll_interval_ms INTEGER DEFAULT 5000,
|
|
124
|
-
last_activity_at TEXT,
|
|
125
|
-
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
-- Polling presets
|
|
129
|
-
CREATE TABLE IF NOT EXISTS poll_presets (
|
|
130
|
-
state TEXT PRIMARY KEY,
|
|
131
|
-
interval_ms INTEGER NOT NULL
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
-- Insert default presets if not exist
|
|
135
|
-
INSERT OR IGNORE INTO poll_presets (state, interval_ms) VALUES
|
|
136
|
-
('active', 5000),
|
|
137
|
-
('idle', 30000),
|
|
138
|
-
('stopped', 60000);
|
|
139
|
-
|
|
140
|
-
-- Initialize daemon_config if empty
|
|
141
|
-
INSERT OR IGNORE INTO daemon_config (id, state, poll_interval_ms)
|
|
142
|
-
VALUES (1, 'idle', 30000);
|
|
143
|
-
|
|
144
|
-
-- Agent status (aggregated from JSON files)
|
|
145
|
-
CREATE TABLE IF NOT EXISTS agent_status (
|
|
146
|
-
agent TEXT PRIMARY KEY,
|
|
147
|
-
status TEXT DEFAULT 'unknown',
|
|
148
|
-
task TEXT,
|
|
149
|
-
message TEXT,
|
|
150
|
-
updated_at TEXT,
|
|
151
|
-
file_mtime INTEGER DEFAULT 0
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
-- Status history for metrics (optional, pruned periodically)
|
|
155
|
-
CREATE TABLE IF NOT EXISTS status_history (
|
|
156
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
157
|
-
agent TEXT NOT NULL,
|
|
158
|
-
status TEXT NOT NULL,
|
|
159
|
-
task TEXT,
|
|
160
|
-
recorded_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
-- Index for history queries
|
|
164
|
-
CREATE INDEX IF NOT EXISTS idx_history_agent ON status_history(agent);
|
|
165
|
-
CREATE INDEX IF NOT EXISTS idx_history_recorded ON status_history(recorded_at);
|
|
166
|
-
SQL
|
|
167
|
-
|
|
168
|
-
# Set secure permissions on database file
|
|
169
|
-
chmod 600 "$FORGE_DB" 2>/dev/null || true
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
# =============================================================================
|
|
173
|
-
# Daemon State Operations
|
|
174
|
-
# =============================================================================
|
|
175
|
-
|
|
176
|
-
# Get current daemon state (active/idle/stopped)
|
|
177
|
-
db_get_daemon_state() {
|
|
178
|
-
db_require_init || return 1
|
|
179
|
-
sqlite3 "$FORGE_DB" "SELECT state FROM daemon_config WHERE id = 1;"
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
# Set daemon state and update poll interval accordingly
|
|
183
|
-
db_set_daemon_state() {
|
|
184
|
-
db_require_init || return 1
|
|
185
|
-
local new_state="$1"
|
|
186
|
-
local interval
|
|
187
|
-
|
|
188
|
-
# SECURITY: Validate state is a known value
|
|
189
|
-
case "$new_state" in
|
|
190
|
-
active|idle|stopped) ;;
|
|
191
|
-
*)
|
|
192
|
-
echo "Invalid daemon state: $new_state" >&2
|
|
193
|
-
return 1
|
|
194
|
-
;;
|
|
195
|
-
esac
|
|
196
|
-
|
|
197
|
-
# Get interval for this state from presets (safe - validated above)
|
|
198
|
-
interval=$(sqlite3 "$FORGE_DB" "SELECT interval_ms FROM poll_presets WHERE state = '$new_state';")
|
|
199
|
-
interval="${interval:-30000}"
|
|
200
|
-
|
|
201
|
-
sqlite3 "$FORGE_DB" <<SQL
|
|
202
|
-
UPDATE daemon_config
|
|
203
|
-
SET state = '$new_state',
|
|
204
|
-
poll_interval_ms = $interval,
|
|
205
|
-
updated_at = datetime('now')
|
|
206
|
-
WHERE id = 1;
|
|
207
|
-
SQL
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
# Get current poll interval in milliseconds
|
|
211
|
-
db_get_poll_interval_ms() {
|
|
212
|
-
sqlite3 "$FORGE_DB" "SELECT poll_interval_ms FROM daemon_config WHERE id = 1;"
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
# Update last activity timestamp
|
|
216
|
-
db_touch_activity() {
|
|
217
|
-
sqlite3 "$FORGE_DB" <<SQL
|
|
218
|
-
UPDATE daemon_config
|
|
219
|
-
SET last_activity_at = datetime('now'),
|
|
220
|
-
updated_at = datetime('now')
|
|
221
|
-
WHERE id = 1;
|
|
222
|
-
SQL
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
# =============================================================================
|
|
226
|
-
# Agent Status Operations
|
|
227
|
-
# =============================================================================
|
|
228
|
-
|
|
229
|
-
# Upsert agent status (from JSON file data)
|
|
230
|
-
db_upsert_agent_status() {
|
|
231
|
-
db_require_init || return 1
|
|
232
|
-
local agent="$1"
|
|
233
|
-
local status="$2"
|
|
234
|
-
local task="$3"
|
|
235
|
-
local message="$4"
|
|
236
|
-
local updated_at="$5"
|
|
237
|
-
local file_mtime="$6"
|
|
238
|
-
|
|
239
|
-
# SECURITY: Escape all string inputs to prevent SQL injection
|
|
240
|
-
agent=$(db_escape "$agent")
|
|
241
|
-
status=$(db_escape "$status")
|
|
242
|
-
task=$(db_escape "$task")
|
|
243
|
-
message=$(db_escape "$message")
|
|
244
|
-
updated_at=$(db_escape "$updated_at")
|
|
245
|
-
|
|
246
|
-
# Validate file_mtime is numeric
|
|
247
|
-
if ! [[ "$file_mtime" =~ ^[0-9]+$ ]]; then
|
|
248
|
-
file_mtime=0
|
|
249
|
-
fi
|
|
250
|
-
|
|
251
|
-
sqlite3 "$FORGE_DB" <<SQL
|
|
252
|
-
INSERT INTO agent_status (agent, status, task, message, updated_at, file_mtime)
|
|
253
|
-
VALUES ('$agent', '$status', '$task', '$message', '$updated_at', $file_mtime)
|
|
254
|
-
ON CONFLICT(agent) DO UPDATE SET
|
|
255
|
-
status = excluded.status,
|
|
256
|
-
task = excluded.task,
|
|
257
|
-
message = excluded.message,
|
|
258
|
-
updated_at = excluded.updated_at,
|
|
259
|
-
file_mtime = excluded.file_mtime;
|
|
260
|
-
SQL
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
# Get stored mtime for an agent's status file
|
|
264
|
-
db_get_agent_mtime() {
|
|
265
|
-
local agent="$1"
|
|
266
|
-
# SECURITY: Escape agent name to prevent SQL injection
|
|
267
|
-
agent=$(db_escape "$agent")
|
|
268
|
-
sqlite3 "$FORGE_DB" "SELECT file_mtime FROM agent_status WHERE agent = '$agent';" 2>/dev/null || echo "0"
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
# Get all agent statuses
|
|
272
|
-
db_get_all_agent_statuses() {
|
|
273
|
-
sqlite3 -separator '|' "$FORGE_DB" <<SQL
|
|
274
|
-
SELECT agent, status, task, message, updated_at
|
|
275
|
-
FROM agent_status
|
|
276
|
-
ORDER BY agent;
|
|
277
|
-
SQL
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
# Get agents with specific status
|
|
281
|
-
db_get_agents_by_status() {
|
|
282
|
-
local status="$1"
|
|
283
|
-
# SECURITY: Escape status to prevent SQL injection
|
|
284
|
-
status=$(db_escape "$status")
|
|
285
|
-
sqlite3 "$FORGE_DB" "SELECT agent FROM agent_status WHERE status = '$status';"
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
# Count active workers (status = 'working')
|
|
289
|
-
db_count_active_workers() {
|
|
290
|
-
sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status WHERE status = 'working';"
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
# Delete stale agent status entries
|
|
294
|
-
db_cleanup_stale_agents() {
|
|
295
|
-
local minutes="${1:-30}"
|
|
296
|
-
# SECURITY: Validate minutes is numeric to prevent SQL injection
|
|
297
|
-
if ! [[ "$minutes" =~ ^[0-9]+$ ]]; then
|
|
298
|
-
minutes=30
|
|
299
|
-
fi
|
|
300
|
-
sqlite3 "$FORGE_DB" <<SQL
|
|
301
|
-
DELETE FROM agent_status
|
|
302
|
-
WHERE updated_at < datetime('now', '-$minutes minutes');
|
|
303
|
-
SQL
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
# =============================================================================
|
|
307
|
-
# History Operations (for future metrics)
|
|
308
|
-
# =============================================================================
|
|
309
|
-
|
|
310
|
-
# Record status change in history
|
|
311
|
-
db_record_status_history() {
|
|
312
|
-
local agent="$1"
|
|
313
|
-
local status="$2"
|
|
314
|
-
local task="$3"
|
|
315
|
-
|
|
316
|
-
# SECURITY: Escape all inputs to prevent SQL injection
|
|
317
|
-
agent=$(db_escape "$agent")
|
|
318
|
-
status=$(db_escape "$status")
|
|
319
|
-
task=$(db_escape "$task")
|
|
320
|
-
|
|
321
|
-
sqlite3 "$FORGE_DB" <<SQL
|
|
322
|
-
INSERT INTO status_history (agent, status, task)
|
|
323
|
-
VALUES ('$agent', '$status', '$task');
|
|
324
|
-
SQL
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
# Prune old history entries (keep last N days)
|
|
328
|
-
db_prune_history() {
|
|
329
|
-
local days="${1:-7}"
|
|
330
|
-
# SECURITY: Validate days is numeric to prevent SQL injection
|
|
331
|
-
if ! [[ "$days" =~ ^[0-9]+$ ]]; then
|
|
332
|
-
days=7
|
|
333
|
-
fi
|
|
334
|
-
sqlite3 "$FORGE_DB" <<SQL
|
|
335
|
-
DELETE FROM status_history
|
|
336
|
-
WHERE recorded_at < datetime('now', '-$days days');
|
|
337
|
-
SQL
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
# =============================================================================
|
|
341
|
-
# Utility Functions
|
|
342
|
-
# =============================================================================
|
|
343
|
-
|
|
344
|
-
# Check if database exists and is valid
|
|
345
|
-
db_exists() {
|
|
346
|
-
[[ -n "$FORGE_DB" ]] && [[ -f "$FORGE_DB" ]] && sqlite3 "$FORGE_DB" "SELECT 1 FROM daemon_config LIMIT 1;" &>/dev/null
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
# Get database stats for debugging
|
|
350
|
-
db_stats() {
|
|
351
|
-
db_require_init || return 1
|
|
352
|
-
echo "Database: $FORGE_DB"
|
|
353
|
-
echo "Agent count: $(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status;")"
|
|
354
|
-
echo "History entries: $(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM status_history;")"
|
|
355
|
-
echo "Current state: $(db_get_daemon_state)"
|
|
356
|
-
echo "Poll interval: $(db_get_poll_interval_ms)ms"
|
|
357
|
-
}
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Vibe Forge - SQLite Database Operations
|
|
4
|
+
# Handles daemon state persistence and agent status aggregation
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
# Database file location (set after FORGE_ROOT is known)
|
|
8
|
+
FORGE_DB=""
|
|
9
|
+
|
|
10
|
+
# =============================================================================
|
|
11
|
+
# Security Functions
|
|
12
|
+
# =============================================================================
|
|
13
|
+
|
|
14
|
+
# Escape a string for safe use in SQLite queries
|
|
15
|
+
# Escapes single quotes by doubling them (SQL standard)
|
|
16
|
+
# Also removes null bytes and other dangerous characters
|
|
17
|
+
db_escape() {
|
|
18
|
+
local input="$1"
|
|
19
|
+
# Remove null bytes
|
|
20
|
+
input="${input//$'\0'/}"
|
|
21
|
+
# Escape single quotes by doubling them
|
|
22
|
+
input="${input//\'/\'\'}"
|
|
23
|
+
# Remove backslashes that could escape quotes
|
|
24
|
+
input="${input//\\/}"
|
|
25
|
+
echo "$input"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Validate that input contains only safe characters for identifiers
|
|
29
|
+
# Returns 0 if safe, 1 if unsafe
|
|
30
|
+
db_validate_identifier() {
|
|
31
|
+
local input="$1"
|
|
32
|
+
# Allow only alphanumeric, underscore, hyphen
|
|
33
|
+
if [[ "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
34
|
+
return 0
|
|
35
|
+
fi
|
|
36
|
+
return 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# Database Initialization
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
# Require FORGE_DB to be set before database operations
|
|
44
|
+
# Call this at the start of database functions that need the path
|
|
45
|
+
# Usage: db_require_init || return 1
|
|
46
|
+
db_require_init() {
|
|
47
|
+
if [[ -z "$FORGE_DB" ]]; then
|
|
48
|
+
echo "Error: FORGE_DB not set. Set FORGE_DB path before calling database functions." >&2
|
|
49
|
+
return 1
|
|
50
|
+
fi
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Check if sqlite3 is available; attempt install if missing
|
|
55
|
+
db_require_sqlite3() {
|
|
56
|
+
if command -v sqlite3 &>/dev/null; then
|
|
57
|
+
return 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
echo "sqlite3 not found. Attempting to install..." >&2
|
|
61
|
+
|
|
62
|
+
# Try platform-appropriate package manager
|
|
63
|
+
if command -v apt-get &>/dev/null; then
|
|
64
|
+
echo "Installing sqlite3 via apt (may require sudo password)..." >&2
|
|
65
|
+
sudo apt-get install -y sqlite3 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
66
|
+
elif command -v brew &>/dev/null; then
|
|
67
|
+
echo "Installing sqlite3 via Homebrew..." >&2
|
|
68
|
+
brew install sqlite3 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
69
|
+
elif command -v pacman &>/dev/null; then
|
|
70
|
+
echo "Installing sqlite via pacman (may require sudo password)..." >&2
|
|
71
|
+
sudo pacman -S --noconfirm sqlite 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
72
|
+
elif command -v dnf &>/dev/null; then
|
|
73
|
+
echo "Installing sqlite via dnf (may require sudo password)..." >&2
|
|
74
|
+
sudo dnf install -y sqlite 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
75
|
+
elif command -v winget &>/dev/null; then
|
|
76
|
+
echo "Installing SQLite via winget. You may see a UAC prompt; please approve it." >&2
|
|
77
|
+
winget install --id SQLite.SQLite -e --silent 2>/dev/null
|
|
78
|
+
# winget installs to Program Files but doesn't add to Git Bash PATH
|
|
79
|
+
local winget_sqlite="/c/Program Files/SQLite/sqlite3.exe"
|
|
80
|
+
if [[ -f "$winget_sqlite" ]]; then
|
|
81
|
+
export PATH="$PATH:/c/Program Files/SQLite"
|
|
82
|
+
command -v sqlite3 &>/dev/null && return 0
|
|
83
|
+
fi
|
|
84
|
+
elif command -v choco &>/dev/null; then
|
|
85
|
+
echo "Installing sqlite via Chocolatey. You may see a UAC prompt; please approve it." >&2
|
|
86
|
+
choco install sqlite -y 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
echo "ERROR: sqlite3 is required but could not be installed automatically." >&2
|
|
90
|
+
echo "Please install sqlite3 manually:" >&2
|
|
91
|
+
echo " macOS: brew install sqlite3" >&2
|
|
92
|
+
echo " Ubuntu: sudo apt-get install sqlite3" >&2
|
|
93
|
+
echo " Fedora: sudo dnf install sqlite" >&2
|
|
94
|
+
echo " Windows: choco install sqlite" >&2
|
|
95
|
+
echo " or: winget install SQLite.SQLite" >&2
|
|
96
|
+
echo " (then add the install folder to your PATH)" >&2
|
|
97
|
+
return 1
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Initialize the database with schema
|
|
101
|
+
# Requires: FORGE_DB must be set before calling
|
|
102
|
+
db_init() {
|
|
103
|
+
db_require_init || return 1
|
|
104
|
+
db_require_sqlite3 || return 1
|
|
105
|
+
local db_dir
|
|
106
|
+
db_dir=$(dirname "$FORGE_DB")
|
|
107
|
+
|
|
108
|
+
# Create directory if needed
|
|
109
|
+
mkdir -p "$db_dir"
|
|
110
|
+
chmod 700 "$db_dir"
|
|
111
|
+
|
|
112
|
+
# Enable WAL mode for concurrent reads without blocking writes.
|
|
113
|
+
# WAL + NORMAL synchronous is the recommended pairing for local tooling:
|
|
114
|
+
# faster than DELETE+FULL while still durable against OS crashes.
|
|
115
|
+
sqlite3 "$FORGE_DB" "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"
|
|
116
|
+
|
|
117
|
+
# Create tables if they don't exist
|
|
118
|
+
sqlite3 "$FORGE_DB" <<'SQL'
|
|
119
|
+
-- Daemon configuration (single row)
|
|
120
|
+
CREATE TABLE IF NOT EXISTS daemon_config (
|
|
121
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
122
|
+
state TEXT DEFAULT 'idle',
|
|
123
|
+
poll_interval_ms INTEGER DEFAULT 5000,
|
|
124
|
+
last_activity_at TEXT,
|
|
125
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
-- Polling presets
|
|
129
|
+
CREATE TABLE IF NOT EXISTS poll_presets (
|
|
130
|
+
state TEXT PRIMARY KEY,
|
|
131
|
+
interval_ms INTEGER NOT NULL
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
-- Insert default presets if not exist
|
|
135
|
+
INSERT OR IGNORE INTO poll_presets (state, interval_ms) VALUES
|
|
136
|
+
('active', 5000),
|
|
137
|
+
('idle', 30000),
|
|
138
|
+
('stopped', 60000);
|
|
139
|
+
|
|
140
|
+
-- Initialize daemon_config if empty
|
|
141
|
+
INSERT OR IGNORE INTO daemon_config (id, state, poll_interval_ms)
|
|
142
|
+
VALUES (1, 'idle', 30000);
|
|
143
|
+
|
|
144
|
+
-- Agent status (aggregated from JSON files)
|
|
145
|
+
CREATE TABLE IF NOT EXISTS agent_status (
|
|
146
|
+
agent TEXT PRIMARY KEY,
|
|
147
|
+
status TEXT DEFAULT 'unknown',
|
|
148
|
+
task TEXT,
|
|
149
|
+
message TEXT,
|
|
150
|
+
updated_at TEXT,
|
|
151
|
+
file_mtime INTEGER DEFAULT 0
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
-- Status history for metrics (optional, pruned periodically)
|
|
155
|
+
CREATE TABLE IF NOT EXISTS status_history (
|
|
156
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
157
|
+
agent TEXT NOT NULL,
|
|
158
|
+
status TEXT NOT NULL,
|
|
159
|
+
task TEXT,
|
|
160
|
+
recorded_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
-- Index for history queries
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_history_agent ON status_history(agent);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_history_recorded ON status_history(recorded_at);
|
|
166
|
+
SQL
|
|
167
|
+
|
|
168
|
+
# Set secure permissions on database file
|
|
169
|
+
chmod 600 "$FORGE_DB" 2>/dev/null || true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# =============================================================================
|
|
173
|
+
# Daemon State Operations
|
|
174
|
+
# =============================================================================
|
|
175
|
+
|
|
176
|
+
# Get current daemon state (active/idle/stopped)
|
|
177
|
+
db_get_daemon_state() {
|
|
178
|
+
db_require_init || return 1
|
|
179
|
+
sqlite3 "$FORGE_DB" "SELECT state FROM daemon_config WHERE id = 1;"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Set daemon state and update poll interval accordingly
|
|
183
|
+
db_set_daemon_state() {
|
|
184
|
+
db_require_init || return 1
|
|
185
|
+
local new_state="$1"
|
|
186
|
+
local interval
|
|
187
|
+
|
|
188
|
+
# SECURITY: Validate state is a known value
|
|
189
|
+
case "$new_state" in
|
|
190
|
+
active|idle|stopped) ;;
|
|
191
|
+
*)
|
|
192
|
+
echo "Invalid daemon state: $new_state" >&2
|
|
193
|
+
return 1
|
|
194
|
+
;;
|
|
195
|
+
esac
|
|
196
|
+
|
|
197
|
+
# Get interval for this state from presets (safe - validated above)
|
|
198
|
+
interval=$(sqlite3 "$FORGE_DB" "SELECT interval_ms FROM poll_presets WHERE state = '$new_state';")
|
|
199
|
+
interval="${interval:-30000}"
|
|
200
|
+
|
|
201
|
+
sqlite3 "$FORGE_DB" <<SQL
|
|
202
|
+
UPDATE daemon_config
|
|
203
|
+
SET state = '$new_state',
|
|
204
|
+
poll_interval_ms = $interval,
|
|
205
|
+
updated_at = datetime('now')
|
|
206
|
+
WHERE id = 1;
|
|
207
|
+
SQL
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Get current poll interval in milliseconds
|
|
211
|
+
db_get_poll_interval_ms() {
|
|
212
|
+
sqlite3 "$FORGE_DB" "SELECT poll_interval_ms FROM daemon_config WHERE id = 1;"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Update last activity timestamp
|
|
216
|
+
db_touch_activity() {
|
|
217
|
+
sqlite3 "$FORGE_DB" <<SQL
|
|
218
|
+
UPDATE daemon_config
|
|
219
|
+
SET last_activity_at = datetime('now'),
|
|
220
|
+
updated_at = datetime('now')
|
|
221
|
+
WHERE id = 1;
|
|
222
|
+
SQL
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# =============================================================================
|
|
226
|
+
# Agent Status Operations
|
|
227
|
+
# =============================================================================
|
|
228
|
+
|
|
229
|
+
# Upsert agent status (from JSON file data)
|
|
230
|
+
db_upsert_agent_status() {
|
|
231
|
+
db_require_init || return 1
|
|
232
|
+
local agent="$1"
|
|
233
|
+
local status="$2"
|
|
234
|
+
local task="$3"
|
|
235
|
+
local message="$4"
|
|
236
|
+
local updated_at="$5"
|
|
237
|
+
local file_mtime="$6"
|
|
238
|
+
|
|
239
|
+
# SECURITY: Escape all string inputs to prevent SQL injection
|
|
240
|
+
agent=$(db_escape "$agent")
|
|
241
|
+
status=$(db_escape "$status")
|
|
242
|
+
task=$(db_escape "$task")
|
|
243
|
+
message=$(db_escape "$message")
|
|
244
|
+
updated_at=$(db_escape "$updated_at")
|
|
245
|
+
|
|
246
|
+
# Validate file_mtime is numeric
|
|
247
|
+
if ! [[ "$file_mtime" =~ ^[0-9]+$ ]]; then
|
|
248
|
+
file_mtime=0
|
|
249
|
+
fi
|
|
250
|
+
|
|
251
|
+
sqlite3 "$FORGE_DB" <<SQL
|
|
252
|
+
INSERT INTO agent_status (agent, status, task, message, updated_at, file_mtime)
|
|
253
|
+
VALUES ('$agent', '$status', '$task', '$message', '$updated_at', $file_mtime)
|
|
254
|
+
ON CONFLICT(agent) DO UPDATE SET
|
|
255
|
+
status = excluded.status,
|
|
256
|
+
task = excluded.task,
|
|
257
|
+
message = excluded.message,
|
|
258
|
+
updated_at = excluded.updated_at,
|
|
259
|
+
file_mtime = excluded.file_mtime;
|
|
260
|
+
SQL
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# Get stored mtime for an agent's status file
|
|
264
|
+
db_get_agent_mtime() {
|
|
265
|
+
local agent="$1"
|
|
266
|
+
# SECURITY: Escape agent name to prevent SQL injection
|
|
267
|
+
agent=$(db_escape "$agent")
|
|
268
|
+
sqlite3 "$FORGE_DB" "SELECT file_mtime FROM agent_status WHERE agent = '$agent';" 2>/dev/null || echo "0"
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Get all agent statuses
|
|
272
|
+
db_get_all_agent_statuses() {
|
|
273
|
+
sqlite3 -separator '|' "$FORGE_DB" <<SQL
|
|
274
|
+
SELECT agent, status, task, message, updated_at
|
|
275
|
+
FROM agent_status
|
|
276
|
+
ORDER BY agent;
|
|
277
|
+
SQL
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# Get agents with specific status
|
|
281
|
+
db_get_agents_by_status() {
|
|
282
|
+
local status="$1"
|
|
283
|
+
# SECURITY: Escape status to prevent SQL injection
|
|
284
|
+
status=$(db_escape "$status")
|
|
285
|
+
sqlite3 "$FORGE_DB" "SELECT agent FROM agent_status WHERE status = '$status';"
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# Count active workers (status = 'working')
|
|
289
|
+
db_count_active_workers() {
|
|
290
|
+
sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status WHERE status = 'working';"
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# Delete stale agent status entries
|
|
294
|
+
db_cleanup_stale_agents() {
|
|
295
|
+
local minutes="${1:-30}"
|
|
296
|
+
# SECURITY: Validate minutes is numeric to prevent SQL injection
|
|
297
|
+
if ! [[ "$minutes" =~ ^[0-9]+$ ]]; then
|
|
298
|
+
minutes=30
|
|
299
|
+
fi
|
|
300
|
+
sqlite3 "$FORGE_DB" <<SQL
|
|
301
|
+
DELETE FROM agent_status
|
|
302
|
+
WHERE updated_at < datetime('now', '-$minutes minutes');
|
|
303
|
+
SQL
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# =============================================================================
|
|
307
|
+
# History Operations (for future metrics)
|
|
308
|
+
# =============================================================================
|
|
309
|
+
|
|
310
|
+
# Record status change in history
|
|
311
|
+
db_record_status_history() {
|
|
312
|
+
local agent="$1"
|
|
313
|
+
local status="$2"
|
|
314
|
+
local task="$3"
|
|
315
|
+
|
|
316
|
+
# SECURITY: Escape all inputs to prevent SQL injection
|
|
317
|
+
agent=$(db_escape "$agent")
|
|
318
|
+
status=$(db_escape "$status")
|
|
319
|
+
task=$(db_escape "$task")
|
|
320
|
+
|
|
321
|
+
sqlite3 "$FORGE_DB" <<SQL
|
|
322
|
+
INSERT INTO status_history (agent, status, task)
|
|
323
|
+
VALUES ('$agent', '$status', '$task');
|
|
324
|
+
SQL
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# Prune old history entries (keep last N days)
|
|
328
|
+
db_prune_history() {
|
|
329
|
+
local days="${1:-7}"
|
|
330
|
+
# SECURITY: Validate days is numeric to prevent SQL injection
|
|
331
|
+
if ! [[ "$days" =~ ^[0-9]+$ ]]; then
|
|
332
|
+
days=7
|
|
333
|
+
fi
|
|
334
|
+
sqlite3 "$FORGE_DB" <<SQL
|
|
335
|
+
DELETE FROM status_history
|
|
336
|
+
WHERE recorded_at < datetime('now', '-$days days');
|
|
337
|
+
SQL
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# =============================================================================
|
|
341
|
+
# Utility Functions
|
|
342
|
+
# =============================================================================
|
|
343
|
+
|
|
344
|
+
# Check if database exists and is valid
|
|
345
|
+
db_exists() {
|
|
346
|
+
[[ -n "$FORGE_DB" ]] && [[ -f "$FORGE_DB" ]] && sqlite3 "$FORGE_DB" "SELECT 1 FROM daemon_config LIMIT 1;" &>/dev/null
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# Get database stats for debugging
|
|
350
|
+
db_stats() {
|
|
351
|
+
db_require_init || return 1
|
|
352
|
+
echo "Database: $FORGE_DB"
|
|
353
|
+
echo "Agent count: $(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status;")"
|
|
354
|
+
echo "History entries: $(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM status_history;")"
|
|
355
|
+
echo "Current state: $(db_get_daemon_state)"
|
|
356
|
+
echo "Poll interval: $(db_get_poll_interval_ms)ms"
|
|
357
|
+
}
|