mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
fix(skills): replace sqlite3 CLI with in-tree better-sqlite3 wrapper
Setup deliberately avoids the sqlite3 CLI (`setup/verify.ts:5` calls this out: "Uses better-sqlite3 directly (no sqlite3 CLI)") and never installs or probes for the binary. Despite that, 13 skills shelled out to `sqlite3 ...` directly, breaking on hosts where the CLI isn't preinstalled — the same root cause as #2191 but spread across the skill surface. Add `scripts/q.ts`, a ~30-LOC wrapper over the `better-sqlite3` dep that setup already installs and verifies. Default output matches `sqlite3 -list` (pipe-separated, no header) so existing skill text reads identically — only the binary changes. SELECT/WITH queries go through `db.prepare().all()`; everything else (INSERT/UPDATE/DELETE, including compound statements) goes through `db.exec()`. Migrate every in-tree caller: - 17 hardcoded invocations across 8 SKILL.md files (init-first-agent, add-deltachat, add-signal, add-emacs, add-whatsapp, add-ollama-provider, debug, add-parallel) plus add-deltachat/VERIFY.md. - `manage-channels/SKILL.md` shows canonical SQL but never prescribed a tool, so the assistant defaulted to `sqlite3` and silently failed. Add a one-line wrapper hint above the SQL block. - `migrate-v2.sh` schema/count probes (was the original #2191 case). Replace `.tables` with `SELECT name FROM sqlite_master`. - Document the wrapper convention in root `CLAUDE.md` under "Central DB". Add `scripts/q.test.ts` with 6 vitest cases covering both modes, NULL rendering, empty-result, compound mutations, and arg validation. Wire `scripts/**/*.test.ts` into `vitest.config.ts`. Out of scope (flagged for follow-up): - `debug` and `add-parallel` still reference the v1-only path `store/messages.db`. Routing through the wrapper now produces a cleaner "no such file" error, but the surrounding sections are v1-era throughout — a v1-content cleanup is its own PR. - `cleanup-sessions.sh` is being addressed in #1889 (different style, hard-fail rather than wrap); left untouched here to avoid stepping on that author's work. Closes #2191. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -140,7 +140,7 @@ After accepting, DeltaChat exchanges keys and creates the chat automatically.
|
||||
Once the first message arrives the router auto-creates a `messaging_groups` row. Look up the chat ID:
|
||||
|
||||
```bash
|
||||
sqlite3 data/v2.db \
|
||||
pnpm exec tsx scripts/q.ts data/v2.db \
|
||||
"SELECT platform_id, name FROM messaging_groups WHERE channel_type='deltachat' AND is_group=0 ORDER BY created_at DESC LIMIT 5"
|
||||
```
|
||||
|
||||
@@ -226,7 +226,7 @@ Set `DC_SMTP_SECURITY=1` and `DC_SMTP_PORT=465` in `.env`, then restart.
|
||||
1. Check the service is running and the adapter started: `grep "Channel adapter started.*deltachat" logs/nanoclaw.log`
|
||||
2. Check connectivity: `grep "DeltaChat: IO started" logs/nanoclaw.log`
|
||||
3. Check the sender has been granted access — run `/init-first-agent` to create their user record and wire the chat
|
||||
4. Verify the messaging group is wired: `sqlite3 data/v2.db "SELECT mg.platform_id, mga.agent_group_id FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id = mga.messaging_group_id WHERE mg.channel_type='deltachat'"`
|
||||
4. Verify the messaging group is wired: `pnpm exec tsx scripts/q.ts data/v2.db "SELECT mg.platform_id, mga.agent_group_id FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id = mga.messaging_group_id WHERE mg.channel_type='deltachat'"`
|
||||
|
||||
### Stale lock file after crash
|
||||
|
||||
@@ -248,7 +248,7 @@ grep "DeltaChat" logs/nanoclaw.error.log | tail -20
|
||||
The messaging group exists but may not be wired to an agent group. Run:
|
||||
|
||||
```bash
|
||||
sqlite3 data/v2.db "SELECT id, platform_id, name FROM messaging_groups WHERE channel_type='deltachat'"
|
||||
pnpm exec tsx scripts/q.ts data/v2.db "SELECT id, platform_id, name FROM messaging_groups WHERE channel_type='deltachat'"
|
||||
```
|
||||
|
||||
If the group has no entry in `messaging_group_agents`, wire it with `/manage-channels`.
|
||||
|
||||
@@ -37,7 +37,7 @@ grep "DeltaChat" logs/nanoclaw.error.log | tail -10
|
||||
## 4. Check messaging group was created
|
||||
|
||||
```bash
|
||||
sqlite3 data/v2.db \
|
||||
pnpm exec tsx scripts/q.ts data/v2.db \
|
||||
"SELECT id, platform_id, name FROM messaging_groups WHERE channel_type='deltachat' ORDER BY created_at DESC LIMIT 5"
|
||||
```
|
||||
|
||||
@@ -48,7 +48,7 @@ If a row appears, the inbound routing is working. If not, the adapter isn't rece
|
||||
If the message arrived but the agent didn't respond, the sender may not have access:
|
||||
|
||||
```bash
|
||||
sqlite3 data/v2.db "SELECT id, display_name FROM users WHERE id LIKE 'deltachat:%'"
|
||||
pnpm exec tsx scripts/q.ts data/v2.db "SELECT id, display_name FROM users WHERE id LIKE 'deltachat:%'"
|
||||
```
|
||||
|
||||
Grant access as shown in the SKILL.md "Grant user access" section.
|
||||
|
||||
@@ -241,7 +241,7 @@ grep -q "import './emacs.js'" src/channels/index.ts && echo "imported" || echo "
|
||||
### No response from agent
|
||||
|
||||
1. NanoClaw running: `launchctl list | grep nanoclaw` (macOS) / `systemctl --user status nanoclaw` (Linux)
|
||||
2. Messaging group wired: `sqlite3 data/v2.db "SELECT mg.platform_id, ag.folder FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id = mga.messaging_group_id JOIN agent_groups ag ON ag.id = mga.agent_group_id WHERE mg.channel_type = 'emacs'"`
|
||||
2. Messaging group wired: `pnpm exec tsx scripts/q.ts data/v2.db "SELECT mg.platform_id, ag.folder FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id = mga.messaging_group_id JOIN agent_groups ag ON ag.id = mga.agent_group_id WHERE mg.channel_type = 'emacs'"`
|
||||
3. Logs show inbound: `grep 'channel_type=emacs\|Emacs' logs/nanoclaw.log | tail -20`
|
||||
|
||||
If no messaging group row exists, run the `register` command above.
|
||||
@@ -292,5 +292,5 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
|
||||
# Remove the NanoClaw block from your Emacs config
|
||||
# Optionally clean up the messaging group:
|
||||
sqlite3 data/v2.db "DELETE FROM messaging_group_agents WHERE messaging_group_id IN (SELECT id FROM messaging_groups WHERE channel_type='emacs'); DELETE FROM messaging_groups WHERE channel_type='emacs';"
|
||||
pnpm exec tsx scripts/q.ts data/v2.db "DELETE FROM messaging_group_agents WHERE messaging_group_id IN (SELECT id FROM messaging_groups WHERE channel_type='emacs'); DELETE FROM messaging_groups WHERE channel_type='emacs';"
|
||||
```
|
||||
|
||||
@@ -76,7 +76,7 @@ Then rebuild the container image: `./container/build.sh`
|
||||
|
||||
Ask the user (plain text, not AskUserQuestion):
|
||||
|
||||
1. **Which agent group?** List available groups: `sqlite3 data/v2.db "SELECT folder, name FROM agent_groups;"`
|
||||
1. **Which agent group?** List available groups: `pnpm exec tsx scripts/q.ts data/v2.db "SELECT folder, name FROM agent_groups;"`
|
||||
2. **Which Ollama model?** List available: `curl -s http://localhost:11434/api/tags | grep '"name"'`
|
||||
3. **Block Anthropic API?** Recommended yes — prevents accidental spend if config drifts.
|
||||
|
||||
@@ -111,7 +111,7 @@ Read the agent group's shared Claude settings:
|
||||
|
||||
```bash
|
||||
# Find the agent group ID
|
||||
AG_ID=$(sqlite3 data/v2.db "SELECT id FROM agent_groups WHERE folder='<FOLDER>';")
|
||||
AG_ID=$(pnpm exec tsx scripts/q.ts data/v2.db "SELECT id FROM agent_groups WHERE folder='<FOLDER>';")
|
||||
SETTINGS=data/v2-sessions/$AG_ID/.claude-shared/settings.json
|
||||
```
|
||||
|
||||
|
||||
@@ -275,7 +275,7 @@ Look for: `Parallel AI MCP servers configured`
|
||||
- Check agent-runner logs for "Parallel AI MCP servers configured" message
|
||||
|
||||
**Task polling not working:**
|
||||
- Verify scheduled task was created: `sqlite3 store/messages.db "SELECT * FROM scheduled_tasks"`
|
||||
- Verify scheduled task was created: `pnpm exec tsx scripts/q.ts store/messages.db "SELECT * FROM scheduled_tasks"`
|
||||
- Check task runs: `tail -f logs/nanoclaw.log | grep "scheduled task"`
|
||||
- Ensure task prompt includes proper Parallel MCP tool names
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ systemctl --user restart nanoclaw
|
||||
After the service starts, send any message to the Signal number from your personal Signal app. The router auto-creates a `messaging_groups` row. Then:
|
||||
|
||||
```bash
|
||||
sqlite3 data/v2.db \
|
||||
pnpm exec tsx scripts/q.ts data/v2.db \
|
||||
"SELECT id, platform_id FROM messaging_groups WHERE channel_type='signal' ORDER BY created_at DESC LIMIT 5"
|
||||
```
|
||||
|
||||
@@ -212,7 +212,7 @@ Add the Signal number to a group from your phone, send any message, then wire th
|
||||
|
||||
```bash
|
||||
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
||||
sqlite3 data/v2.db "
|
||||
pnpm exec tsx scripts/q.ts data/v2.db "
|
||||
INSERT OR IGNORE INTO messaging_group_agents
|
||||
(id, messaging_group_id, agent_group_id, session_mode, priority, created_at)
|
||||
VALUES
|
||||
@@ -226,7 +226,7 @@ New Signal users (including the owner's Signal identity) are silently dropped wi
|
||||
|
||||
```bash
|
||||
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
||||
sqlite3 data/v2.db "
|
||||
pnpm exec tsx scripts/q.ts data/v2.db "
|
||||
INSERT OR REPLACE INTO user_roles (user_id, role, agent_group_id, granted_by, granted_at)
|
||||
VALUES ('signal:UUID', 'owner', NULL, 'system', '$NOW');
|
||||
INSERT OR IGNORE INTO agent_group_members (user_id, agent_group_id, added_by, added_at)
|
||||
@@ -282,7 +282,7 @@ If you see `Signal daemon not reachable at 127.0.0.1:7583` and `SIGNAL_MANAGE_DA
|
||||
### Bot not responding
|
||||
|
||||
1. Channel initialized: `grep "Signal channel connected" logs/nanoclaw.log | tail -1`
|
||||
2. Channel wired: `sqlite3 data/v2.db "SELECT mg.platform_id, mg.name FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id = mga.messaging_group_id WHERE mg.channel_type='signal'"`
|
||||
2. Channel wired: `pnpm exec tsx scripts/q.ts data/v2.db "SELECT mg.platform_id, mg.name FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id = mga.messaging_group_id WHERE mg.channel_type='signal'"`
|
||||
3. Service running: `launchctl print gui/$(id -u)/com.nanoclaw` (macOS) / `systemctl --user status nanoclaw` (Linux)
|
||||
|
||||
### Lost connection mid-session
|
||||
|
||||
@@ -200,7 +200,7 @@ Otherwise, run `/manage-channels` to wire this channel to an agent group.
|
||||
|
||||
- **type**: `whatsapp`
|
||||
- **terminology**: WhatsApp calls them "groups" and "chats." A "chat" is a 1:1 DM; a "group" has multiple members.
|
||||
- **how-to-find-id**: DMs use `<phone>@s.whatsapp.net` (e.g. `14155551234@s.whatsapp.net`). Groups use `<id>@g.us`. To find your number: `node -e "const c=JSON.parse(require('fs').readFileSync('store/auth/creds.json','utf-8'));console.log(c.me?.id?.split(':')[0]+'@s.whatsapp.net')"`. Groups are auto-discovered — check `sqlite3 data/v2.db "SELECT platform_id, name FROM messaging_groups WHERE channel_type='whatsapp' AND is_group=1"`.
|
||||
- **how-to-find-id**: DMs use `<phone>@s.whatsapp.net` (e.g. `14155551234@s.whatsapp.net`). Groups use `<id>@g.us`. To find your number: `node -e "const c=JSON.parse(require('fs').readFileSync('store/auth/creds.json','utf-8'));console.log(c.me?.id?.split(':')[0]+'@s.whatsapp.net')"`. Groups are auto-discovered — check `pnpm exec tsx scripts/q.ts data/v2.db "SELECT platform_id, name FROM messaging_groups WHERE channel_type='whatsapp' AND is_group=1"`.
|
||||
- **supports-threads**: no
|
||||
- **typical-use**: Interactive chat — direct messages or small groups
|
||||
- **default-isolation**: Same agent group if you're the only participant across multiple chats. Separate agent group if different people are in different groups.
|
||||
@@ -256,7 +256,7 @@ systemctl --user start nanoclaw
|
||||
|
||||
1. Auth exists: `test -f store/auth/creds.json`
|
||||
2. Connected: `grep "Connected to WhatsApp" logs/nanoclaw.log | tail -1`
|
||||
3. Channel wired: `sqlite3 data/v2.db "SELECT mg.platform_id, mg.name FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id=mga.messaging_group_id WHERE mg.channel_type='whatsapp'"`
|
||||
3. Channel wired: `pnpm exec tsx scripts/q.ts data/v2.db "SELECT mg.platform_id, mg.name FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id=mga.messaging_group_id WHERE mg.channel_type='whatsapp'"`
|
||||
4. Service running: `systemctl --user status nanoclaw`
|
||||
|
||||
### "conflict" disconnection
|
||||
|
||||
@@ -279,7 +279,7 @@ rm -rf data/sessions/
|
||||
rm -rf data/sessions/{groupFolder}/.claude/
|
||||
|
||||
# Also clear the session ID from NanoClaw's tracking (stored in SQLite)
|
||||
sqlite3 store/messages.db "DELETE FROM sessions WHERE group_folder = '{groupFolder}'"
|
||||
pnpm exec tsx scripts/q.ts store/messages.db "DELETE FROM sessions WHERE group_folder = '{groupFolder}'"
|
||||
```
|
||||
|
||||
To verify session resumption is working, check the logs for the same session ID across messages:
|
||||
|
||||
@@ -54,7 +54,7 @@ Tell the user:
|
||||
Wait for the user's confirmation. Then look up the most recent DM messaging groups:
|
||||
|
||||
```bash
|
||||
sqlite3 data/v2.db "SELECT id, platform_id, name, created_at FROM messaging_groups WHERE channel_type='${CHANNEL}' AND is_group=0 ORDER BY created_at DESC LIMIT 5"
|
||||
pnpm exec tsx scripts/q.ts data/v2.db "SELECT id, platform_id, name, created_at FROM messaging_groups WHERE channel_type='${CHANNEL}' AND is_group=0 ORDER BY created_at DESC LIMIT 5"
|
||||
```
|
||||
|
||||
Show the top rows to the user and confirm which `platform_id` is theirs (usually the most recent). Record as `PLATFORM_ID`. If none appeared, check `logs/nanoclaw.log` for `unknown_sender` drops — the adapter might be rejecting inbound due to connection or permission issues.
|
||||
@@ -103,7 +103,7 @@ Wait for the user's reply. If they confirm receipt, the skill is done.
|
||||
|
||||
If they say it didn't arrive, then diagnose using the DB directly (no waiting loops required — the message either delivered or it didn't):
|
||||
|
||||
- `sqlite3 data/v2-sessions/<agent-group-id>/sessions/<session-id>/outbound.db "SELECT id, status, created_at FROM messages_out ORDER BY created_at DESC LIMIT 5"` — check for stuck `pending` rows. Replace `<agent-group-id>` and `<session-id>` with the values from the script's output.
|
||||
- `pnpm exec tsx scripts/q.ts data/v2-sessions/<agent-group-id>/sessions/<session-id>/outbound.db "SELECT id, status, created_at FROM messages_out ORDER BY created_at DESC LIMIT 5"` — check for stuck `pending` rows. Replace `<agent-group-id>` and `<session-id>` with the values from the script's output.
|
||||
- `grep -E 'Unauthorized channel destination|container.*exited|error' logs/nanoclaw.log | tail -20` — look for ACL rejections or container crashes.
|
||||
- `ls data/v2-sessions/<agent-group-id>/sessions/*/outbound.db` — confirm the session exists.
|
||||
|
||||
|
||||
@@ -11,7 +11,13 @@ Privilege is a **user-level** concept, not a channel-level one (see `src/db/user
|
||||
|
||||
## Assess Current State
|
||||
|
||||
Read the central DB (`data/v2.db`) using these canonical queries (column names match the schema, not the CLI flags — the `register` command's `--assistant-name` is stored in `agent_groups.name`):
|
||||
Read the central DB (`data/v2.db`) using these canonical queries (column names match the schema, not the CLI flags — the `register` command's `--assistant-name` is stored in `agent_groups.name`).
|
||||
|
||||
Run each via the in-tree wrapper — the host setup deliberately ships no `sqlite3` CLI:
|
||||
|
||||
```bash
|
||||
pnpm exec tsx scripts/q.ts data/v2.db "<query>"
|
||||
```
|
||||
|
||||
```sql
|
||||
SELECT id, name AS assistant_name, folder, agent_provider FROM agent_groups;
|
||||
|
||||
@@ -53,6 +53,8 @@ Exactly one writer per file — no cross-mount lock contention. Heartbeat is a f
|
||||
|
||||
`data/v2.db` holds everything that isn't per-session: users, user_roles, agent_groups, messaging_groups, wiring, pending_approvals, user_dms, chat_sdk_* (for the Chat SDK bridge), schema_version. Migrations live at `src/db/migrations/`.
|
||||
|
||||
For ad-hoc queries from skills or scripts, use the in-tree wrapper rather than the `sqlite3` CLI: `pnpm exec tsx scripts/q.ts <db> "<sql>"`. The host setup intentionally avoids depending on the `sqlite3` binary (`setup/verify.ts:5`); the wrapper goes through the `better-sqlite3` dep that setup already installs and verifies. Default-output format matches `sqlite3 -list` (pipe-separated, no header) so existing skill text reads identically.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|
||||
+8
-4
@@ -242,8 +242,12 @@ fi
|
||||
|
||||
V1_DB="$V1_PATH/store/messages.db"
|
||||
|
||||
# Quick schema check — make sure the tables we need exist
|
||||
TABLES=$(sqlite3 "$V1_DB" ".tables" 2>/dev/null || true)
|
||||
# Quick schema check — make sure the tables we need exist.
|
||||
# Uses the in-tree wrapper instead of the sqlite3 CLI: setup.sh (run via
|
||||
# phase 0a above) installs Node + better-sqlite3 but NOT the sqlite3 CLI,
|
||||
# and #2191 documented how a missing CLI here used to surface as a
|
||||
# misleading "registered_groups missing" abort.
|
||||
TABLES=$(pnpm exec tsx scripts/q.ts "$V1_DB" "SELECT name FROM sqlite_master WHERE type='table'" 2>/dev/null || true)
|
||||
|
||||
if echo "$TABLES" | grep -q "registered_groups"; then
|
||||
step_ok "v1 database has registered_groups"
|
||||
@@ -253,8 +257,8 @@ else
|
||||
fi
|
||||
|
||||
# Show what we found
|
||||
GROUP_COUNT=$(sqlite3 "$V1_DB" "SELECT COUNT(*) FROM registered_groups" 2>/dev/null || echo 0)
|
||||
TASK_COUNT=$(sqlite3 "$V1_DB" "SELECT COUNT(*) FROM scheduled_tasks WHERE status='active'" 2>/dev/null || echo 0)
|
||||
GROUP_COUNT=$(pnpm exec tsx scripts/q.ts "$V1_DB" "SELECT COUNT(*) FROM registered_groups" 2>/dev/null || echo 0)
|
||||
TASK_COUNT=$(pnpm exec tsx scripts/q.ts "$V1_DB" "SELECT COUNT(*) FROM scheduled_tasks WHERE status='active'" 2>/dev/null || echo 0)
|
||||
ENV_KEYS=0
|
||||
if [ -f "$V1_PATH/.env" ]; then
|
||||
ENV_KEYS=$(grep -c '=' "$V1_PATH/.env" 2>/dev/null || echo 0)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
/**
|
||||
* Smoke tests for the q.ts sqlite-CLI replacement wrapper.
|
||||
*
|
||||
* Verifies the two modes (SELECT prints rows in sqlite3 default "list"
|
||||
* format; mutation runs via db.exec) and a few edge cases that real
|
||||
* skill invocations rely on.
|
||||
*/
|
||||
|
||||
const Q = path.resolve(__dirname, 'q.ts');
|
||||
|
||||
describe('scripts/q.ts', () => {
|
||||
let tempDir: string;
|
||||
let dbPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'q-test-'));
|
||||
dbPath = path.join(tempDir, 'test.db');
|
||||
const db = new Database(dbPath);
|
||||
db.exec(`
|
||||
CREATE TABLE t (id INTEGER, name TEXT, note TEXT);
|
||||
INSERT INTO t (id, name, note) VALUES (1, 'alice', 'hi'), (2, 'bob', NULL);
|
||||
`);
|
||||
db.close();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function run(sql: string): { stdout: string; stderr: string; status: number } {
|
||||
const r = spawnSync('pnpm', ['exec', 'tsx', Q, dbPath, sql], {
|
||||
encoding: 'utf-8',
|
||||
cwd: path.resolve(__dirname, '..'),
|
||||
});
|
||||
return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', status: r.status ?? -1 };
|
||||
}
|
||||
|
||||
it('SELECT prints pipe-separated rows in default order', () => {
|
||||
const r = run('SELECT id, name FROM t ORDER BY id');
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout.trim()).toBe('1|alice\n2|bob');
|
||||
});
|
||||
|
||||
it('SELECT renders NULL as empty string (matches sqlite3 default mode)', () => {
|
||||
const r = run('SELECT id, note FROM t ORDER BY id');
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout.trim()).toBe('1|hi\n2|');
|
||||
});
|
||||
|
||||
it('SELECT with no rows prints nothing', () => {
|
||||
const r = run("SELECT id FROM t WHERE name = 'nobody'");
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toBe('');
|
||||
});
|
||||
|
||||
it('INSERT runs via db.exec and persists', () => {
|
||||
const r = run("INSERT INTO t (id, name) VALUES (3, 'carol')");
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toBe('');
|
||||
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
const row = db.prepare('SELECT name FROM t WHERE id = 3').get() as { name: string };
|
||||
db.close();
|
||||
expect(row.name).toBe('carol');
|
||||
});
|
||||
|
||||
it('compound mutation statements execute together', () => {
|
||||
const r = run("DELETE FROM t WHERE id = 1; INSERT INTO t (id, name) VALUES (9, 'zed');");
|
||||
expect(r.status).toBe(0);
|
||||
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
const ids = (db.prepare('SELECT id FROM t ORDER BY id').all() as { id: number }[]).map(
|
||||
(r) => r.id,
|
||||
);
|
||||
db.close();
|
||||
expect(ids).toEqual([2, 9]);
|
||||
});
|
||||
|
||||
it('exits 2 with usage when args are missing', () => {
|
||||
const r = spawnSync('pnpm', ['exec', 'tsx', Q], {
|
||||
encoding: 'utf-8',
|
||||
cwd: path.resolve(__dirname, '..'),
|
||||
});
|
||||
expect(r.status).toBe(2);
|
||||
expect(r.stderr).toMatch(/Usage/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* scripts/q.ts — sqlite3 CLI replacement for skill SQL invocations.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm exec tsx scripts/q.ts <db-path> "<sql>"
|
||||
*
|
||||
* Detects SELECT vs mutation on the first keyword. SELECT/WITH queries
|
||||
* print rows in sqlite3 CLI default ("list") format — pipe-separated,
|
||||
* no header — so existing skill text reads identically. Anything else
|
||||
* runs through db.exec() and prints nothing on success.
|
||||
*
|
||||
* Why this exists: setup/verify.ts:5 codifies that NanoClaw avoids
|
||||
* depending on the sqlite3 CLI binary; setup never installs or probes
|
||||
* for it. Skills that shell out to `sqlite3` therefore fail on hosts
|
||||
* where it isn't preinstalled (common on fresh Ubuntu — see #2191).
|
||||
* This wrapper preserves the skill-text shape (path then SQL string)
|
||||
* while routing through the better-sqlite3 dep that setup already
|
||||
* installs and verifies.
|
||||
*/
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
const [, , dbPath, sql] = process.argv;
|
||||
|
||||
if (!dbPath || sql === undefined) {
|
||||
console.error('Usage: pnpm exec tsx scripts/q.ts <db-path> "<sql>"');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
try {
|
||||
const firstKeyword = sql.trim().split(/\s+/)[0]?.toUpperCase() ?? '';
|
||||
if (firstKeyword === 'SELECT' || firstKeyword === 'WITH') {
|
||||
const rows = db.prepare(sql).all() as Record<string, unknown>[];
|
||||
for (const row of rows) {
|
||||
console.log(
|
||||
Object.values(row)
|
||||
.map((v) => (v === null ? '' : String(v)))
|
||||
.join('|'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
db.exec(sql);
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
+1
-1
@@ -4,6 +4,6 @@ export default defineConfig({
|
||||
test: {
|
||||
// container/agent-runner tests run under Bun (they depend on bun:sqlite).
|
||||
// See container/agent-runner/package.json "test" script.
|
||||
include: ['src/**/*.test.ts', 'setup/**/*.test.ts'],
|
||||
include: ['src/**/*.test.ts', 'setup/**/*.test.ts', 'scripts/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user