@jhizzard/termdeck 0.2.5 → 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/README.md +12 -4
- package/package.json +1 -1
- package/packages/cli/src/init-rumen.js +99 -10
- package/packages/client/public/app.js +2315 -0
- package/packages/client/public/index.html +39 -3280
- package/packages/client/public/style.css +1439 -0
- package/packages/server/src/index.js +175 -3
- package/packages/server/src/session.js +5 -1
- package/packages/server/src/setup/rumen/functions/rumen-tick/index.ts +6 -1
- package/packages/server/src/setup/rumen/migrations/001_rumen_tables.sql +39 -3
|
@@ -10,9 +10,37 @@ const fs = require('fs');
|
|
|
10
10
|
const { v4: uuidv4 } = require('uuid');
|
|
11
11
|
|
|
12
12
|
// Conditional imports (graceful fallback if not installed yet)
|
|
13
|
-
let pty, Database;
|
|
13
|
+
let pty, Database, pg;
|
|
14
14
|
try { pty = require('@homebridge/node-pty-prebuilt-multiarch'); } catch { pty = null; }
|
|
15
15
|
try { Database = require('better-sqlite3'); } catch { Database = null; }
|
|
16
|
+
try { pg = require('pg'); } catch { pg = null; }
|
|
17
|
+
|
|
18
|
+
// Module-level singleton Postgres pool for rumen_insights (petvetbid DB).
|
|
19
|
+
// Lazy-initialized on first rumen endpoint hit so startup stays fast and
|
|
20
|
+
// servers without DATABASE_URL never pay the connection cost.
|
|
21
|
+
let _rumenPool = null;
|
|
22
|
+
let _rumenPoolFailed = false;
|
|
23
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
+
function getRumenPool() {
|
|
25
|
+
if (_rumenPool || _rumenPoolFailed) return _rumenPool;
|
|
26
|
+
if (!pg || !process.env.DATABASE_URL) return null;
|
|
27
|
+
try {
|
|
28
|
+
_rumenPool = new pg.Pool({
|
|
29
|
+
connectionString: process.env.DATABASE_URL,
|
|
30
|
+
max: 4,
|
|
31
|
+
idleTimeoutMillis: 30000,
|
|
32
|
+
connectionTimeoutMillis: 5000
|
|
33
|
+
});
|
|
34
|
+
_rumenPool.on('error', (err) => {
|
|
35
|
+
console.warn('[rumen] pg pool error:', err.message);
|
|
36
|
+
});
|
|
37
|
+
return _rumenPool;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.warn('[rumen] failed to create pg pool:', err.message);
|
|
40
|
+
_rumenPoolFailed = true;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
16
44
|
|
|
17
45
|
const { SessionManager } = require('./session');
|
|
18
46
|
const { initDatabase, logCommand, getSessionHistory, getProjectSessions } = require('./database');
|
|
@@ -171,9 +199,13 @@ function createServer(config) {
|
|
|
171
199
|
rag.onStatusChanged(sess, oldStatus, newStatus);
|
|
172
200
|
};
|
|
173
201
|
|
|
174
|
-
// Proactive Mnestra queries on error — fire-and-forget
|
|
202
|
+
// Proactive Mnestra queries on error — fire-and-forget.
|
|
203
|
+
// Independent of rag.enabled — the push loop (rag.js) and the Flashback
|
|
204
|
+
// bridge (mnestra-bridge) are separate systems. rag.enabled gates only
|
|
205
|
+
// the telemetry push loop. Flashback has its own error handling via
|
|
206
|
+
// the catch below and should fire whenever the Mnestra bridge is
|
|
207
|
+
// configured, regardless of the push-loop flag.
|
|
175
208
|
session.onErrorDetected = (sess, ctx) => {
|
|
176
|
-
if (!rag.enabled) return;
|
|
177
209
|
const question = `${sess.meta.type} error ${ctx.lastCommand || ''} ${ctx.tail || ''}`.trim();
|
|
178
210
|
mnestraBridge.queryMnestra({
|
|
179
211
|
question,
|
|
@@ -415,6 +447,146 @@ function createServer(config) {
|
|
|
415
447
|
});
|
|
416
448
|
});
|
|
417
449
|
|
|
450
|
+
// ==================== Rumen insights (Sprint 4 T2) ====================
|
|
451
|
+
// Read-only access to rumen_insights + rumen_jobs in the petvetbid Postgres
|
|
452
|
+
// instance. Contract frozen in docs/sprint-4-rumen-integration/API-CONTRACT.md.
|
|
453
|
+
|
|
454
|
+
function rumenUnreachable(res) {
|
|
455
|
+
return res.status(503).json({ error: 'rumen database unreachable' });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// GET /api/rumen/insights
|
|
459
|
+
app.get('/api/rumen/insights', async (req, res) => {
|
|
460
|
+
const pool = getRumenPool();
|
|
461
|
+
if (!pool) {
|
|
462
|
+
return res.json({ insights: [], total: 0, enabled: false });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
let limit = parseInt(req.query.limit, 10);
|
|
466
|
+
if (!Number.isFinite(limit)) limit = 20;
|
|
467
|
+
limit = Math.max(1, Math.min(100, limit));
|
|
468
|
+
|
|
469
|
+
const project = typeof req.query.project === 'string' && req.query.project.trim()
|
|
470
|
+
? req.query.project.trim() : null;
|
|
471
|
+
const since = typeof req.query.since === 'string' && !Number.isNaN(Date.parse(req.query.since))
|
|
472
|
+
? new Date(req.query.since).toISOString() : null;
|
|
473
|
+
const unseen = typeof req.query.unseen === 'string' &&
|
|
474
|
+
/^(1|true|yes)$/i.test(req.query.unseen);
|
|
475
|
+
|
|
476
|
+
const where = [];
|
|
477
|
+
const params = [];
|
|
478
|
+
if (project) { params.push(project); where.push(`$${params.length} = ANY(projects)`); }
|
|
479
|
+
if (since) { params.push(since); where.push(`created_at >= $${params.length}`); }
|
|
480
|
+
if (unseen) { where.push(`acted_upon = FALSE`); }
|
|
481
|
+
const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const countSql = `SELECT COUNT(*)::int AS n FROM rumen_insights ${whereSql}`;
|
|
485
|
+
const listParams = params.slice();
|
|
486
|
+
listParams.push(limit);
|
|
487
|
+
const listSql =
|
|
488
|
+
`SELECT id, insight_text, confidence, projects, source_memory_ids, created_at, acted_upon
|
|
489
|
+
FROM rumen_insights
|
|
490
|
+
${whereSql}
|
|
491
|
+
ORDER BY created_at DESC
|
|
492
|
+
LIMIT $${listParams.length}`;
|
|
493
|
+
|
|
494
|
+
const [countRes, listRes] = await Promise.all([
|
|
495
|
+
pool.query(countSql, params),
|
|
496
|
+
pool.query(listSql, listParams)
|
|
497
|
+
]);
|
|
498
|
+
|
|
499
|
+
const insights = listRes.rows.map((r) => ({
|
|
500
|
+
id: r.id,
|
|
501
|
+
insight_text: r.insight_text,
|
|
502
|
+
confidence: r.confidence == null ? 0 : Number(r.confidence),
|
|
503
|
+
projects: r.projects || [],
|
|
504
|
+
source_memory_ids: r.source_memory_ids || [],
|
|
505
|
+
created_at: r.created_at instanceof Date ? r.created_at.toISOString() : r.created_at,
|
|
506
|
+
acted_upon: !!r.acted_upon
|
|
507
|
+
}));
|
|
508
|
+
|
|
509
|
+
res.json({ insights, total: countRes.rows[0]?.n || 0 });
|
|
510
|
+
} catch (err) {
|
|
511
|
+
console.warn('[rumen] GET /insights failed:', err.message);
|
|
512
|
+
return rumenUnreachable(res);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// GET /api/rumen/status
|
|
517
|
+
app.get('/api/rumen/status', async (req, res) => {
|
|
518
|
+
const pool = getRumenPool();
|
|
519
|
+
if (!pool) return res.json({ enabled: false });
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
const jobSql =
|
|
523
|
+
`SELECT id, status, completed_at, sessions_processed, insights_generated
|
|
524
|
+
FROM rumen_jobs
|
|
525
|
+
ORDER BY started_at DESC
|
|
526
|
+
LIMIT 1`;
|
|
527
|
+
const insightSql =
|
|
528
|
+
`SELECT
|
|
529
|
+
COUNT(*)::int AS total,
|
|
530
|
+
COUNT(*) FILTER (WHERE acted_upon = FALSE)::int AS unseen,
|
|
531
|
+
MAX(created_at) AS latest
|
|
532
|
+
FROM rumen_insights`;
|
|
533
|
+
|
|
534
|
+
const [jobRes, insightRes] = await Promise.all([
|
|
535
|
+
pool.query(jobSql),
|
|
536
|
+
pool.query(insightSql)
|
|
537
|
+
]);
|
|
538
|
+
|
|
539
|
+
const job = jobRes.rows[0] || null;
|
|
540
|
+
const stat = insightRes.rows[0] || { total: 0, unseen: 0, latest: null };
|
|
541
|
+
|
|
542
|
+
res.json({
|
|
543
|
+
enabled: true,
|
|
544
|
+
last_job_id: job ? job.id : null,
|
|
545
|
+
last_job_status: job ? job.status : null,
|
|
546
|
+
last_job_completed_at: job && job.completed_at
|
|
547
|
+
? (job.completed_at instanceof Date ? job.completed_at.toISOString() : job.completed_at)
|
|
548
|
+
: null,
|
|
549
|
+
last_job_sessions_processed: job ? (job.sessions_processed || 0) : 0,
|
|
550
|
+
last_job_insights_generated: job ? (job.insights_generated || 0) : 0,
|
|
551
|
+
total_insights: stat.total || 0,
|
|
552
|
+
unseen_insights: stat.unseen || 0,
|
|
553
|
+
latest_insight_at: stat.latest
|
|
554
|
+
? (stat.latest instanceof Date ? stat.latest.toISOString() : stat.latest)
|
|
555
|
+
: null
|
|
556
|
+
});
|
|
557
|
+
} catch (err) {
|
|
558
|
+
console.warn('[rumen] GET /status failed:', err.message);
|
|
559
|
+
return rumenUnreachable(res);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// POST /api/rumen/insights/:id/seen
|
|
564
|
+
app.post('/api/rumen/insights/:id/seen', async (req, res) => {
|
|
565
|
+
const pool = getRumenPool();
|
|
566
|
+
if (!pool) return res.status(503).json({ error: 'rumen not configured' });
|
|
567
|
+
|
|
568
|
+
const id = req.params.id;
|
|
569
|
+
if (!UUID_RE.test(id)) {
|
|
570
|
+
return res.status(400).json({ error: 'invalid insight id' });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const result = await pool.query(
|
|
575
|
+
`UPDATE rumen_insights SET acted_upon = TRUE WHERE id = $1
|
|
576
|
+
RETURNING id, acted_upon`,
|
|
577
|
+
[id]
|
|
578
|
+
);
|
|
579
|
+
if (result.rowCount === 0) {
|
|
580
|
+
return res.status(404).json({ error: 'insight not found' });
|
|
581
|
+
}
|
|
582
|
+
const row = result.rows[0];
|
|
583
|
+
res.json({ id: row.id, acted_upon: !!row.acted_upon });
|
|
584
|
+
} catch (err) {
|
|
585
|
+
console.warn('[rumen] POST /insights/:id/seen failed:', err.message);
|
|
586
|
+
return rumenUnreachable(res);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
418
590
|
// POST /api/ai/query - query Mnestra memory via the bridge (direct|webhook|mcp)
|
|
419
591
|
app.post('/api/ai/query', async (req, res) => {
|
|
420
592
|
let { question, sessionId, project } = req.body;
|
|
@@ -51,7 +51,11 @@ const PATTERNS = {
|
|
|
51
51
|
command: /^[\$#%❯>]\s+(.+)$/m
|
|
52
52
|
},
|
|
53
53
|
// Broad error markers across shells, compilers, scripts, and HTTP servers.
|
|
54
|
-
|
|
54
|
+
// Includes the literal "No such file or directory" phrase because many Unix
|
|
55
|
+
// tools (cat, ls, cd, rm, etc.) report filesystem misses in plain English
|
|
56
|
+
// without ever emitting the ENOENT errno code. Flagged as a gap by Rumen's
|
|
57
|
+
// first production kickstart insight on 2026-04-15.
|
|
58
|
+
error: /\b(error|Error|ERROR|exception|Exception|Traceback|fatal|FATAL|segmentation fault|panic|EACCES|ECONNREFUSED|ENOENT|command not found|undefined reference|cannot find module|failed with exit code|No such file or directory|Permission denied|\b5\d\d\b)\b/
|
|
55
59
|
};
|
|
56
60
|
|
|
57
61
|
class Session {
|
|
@@ -20,7 +20,12 @@
|
|
|
20
20
|
// @ts-ignore Deno std import resolved at runtime.
|
|
21
21
|
import { serve } from 'https://deno.land/std@0.224.0/http/server.ts';
|
|
22
22
|
// @ts-ignore npm specifier resolved at runtime.
|
|
23
|
-
|
|
23
|
+
// NOTE: `__RUMEN_VERSION__` is a placeholder. `termdeck init --rumen` reads the
|
|
24
|
+
// current published version from the npm registry at deploy time and rewrites
|
|
25
|
+
// this line in a staged copy of the file before running `supabase functions
|
|
26
|
+
// deploy`. This source file on disk MUST keep the placeholder — do not commit
|
|
27
|
+
// a real version number here. See packages/cli/src/init-rumen.js.
|
|
28
|
+
import { runRumenJob, createPoolFromUrl } from 'npm:@jhizzard/rumen@__RUMEN_VERSION__';
|
|
24
29
|
|
|
25
30
|
// @ts-ignore Deno global available at runtime.
|
|
26
31
|
declare const Deno: { env: { get: (k: string) => string | undefined } };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
-- Rumen
|
|
2
|
-
-- Non-destructive: creates three new tables under the rumen_ namespace
|
|
1
|
+
-- Rumen schema — self-healing migration.
|
|
2
|
+
-- Non-destructive: creates three new tables under the rumen_ namespace and
|
|
3
|
+
-- brings any pre-existing tables (from partial prior installs) up to the
|
|
4
|
+
-- current shape via ALTER TABLE ADD COLUMN IF NOT EXISTS.
|
|
3
5
|
-- Does NOT modify or reference Mnestra's existing memory_items / memory_sessions tables.
|
|
4
6
|
--
|
|
5
7
|
-- Apply with:
|
|
6
|
-
-- psql "$
|
|
8
|
+
-- psql "$DATABASE_URL" -f migrations/001_rumen_tables.sql
|
|
7
9
|
|
|
8
10
|
BEGIN;
|
|
9
11
|
|
|
@@ -23,6 +25,21 @@ CREATE TABLE IF NOT EXISTS rumen_jobs (
|
|
|
23
25
|
completed_at TIMESTAMPTZ
|
|
24
26
|
);
|
|
25
27
|
|
|
28
|
+
-- Backfill columns for schema drift from earlier install attempts.
|
|
29
|
+
-- CREATE TABLE IF NOT EXISTS is a no-op on existing tables, so without this
|
|
30
|
+
-- block the subsequent CREATE INDEX statements would fail on columns that
|
|
31
|
+
-- never got added.
|
|
32
|
+
ALTER TABLE rumen_jobs
|
|
33
|
+
ADD COLUMN IF NOT EXISTS triggered_by TEXT,
|
|
34
|
+
ADD COLUMN IF NOT EXISTS status TEXT,
|
|
35
|
+
ADD COLUMN IF NOT EXISTS sessions_processed INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
ADD COLUMN IF NOT EXISTS insights_generated INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
ADD COLUMN IF NOT EXISTS questions_generated INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
ADD COLUMN IF NOT EXISTS error_message TEXT,
|
|
39
|
+
ADD COLUMN IF NOT EXISTS source_session_ids UUID[] NOT NULL DEFAULT '{}',
|
|
40
|
+
ADD COLUMN IF NOT EXISTS started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
41
|
+
ADD COLUMN IF NOT EXISTS completed_at TIMESTAMPTZ;
|
|
42
|
+
|
|
26
43
|
CREATE INDEX IF NOT EXISTS idx_rumen_jobs_status
|
|
27
44
|
ON rumen_jobs (status);
|
|
28
45
|
|
|
@@ -49,6 +66,15 @@ CREATE TABLE IF NOT EXISTS rumen_insights (
|
|
|
49
66
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
50
67
|
);
|
|
51
68
|
|
|
69
|
+
ALTER TABLE rumen_insights
|
|
70
|
+
ADD COLUMN IF NOT EXISTS job_id UUID,
|
|
71
|
+
ADD COLUMN IF NOT EXISTS source_memory_ids UUID[] NOT NULL DEFAULT '{}',
|
|
72
|
+
ADD COLUMN IF NOT EXISTS projects TEXT[] NOT NULL DEFAULT '{}',
|
|
73
|
+
ADD COLUMN IF NOT EXISTS insight_text TEXT,
|
|
74
|
+
ADD COLUMN IF NOT EXISTS confidence NUMERIC(4, 3) NOT NULL DEFAULT 0.000,
|
|
75
|
+
ADD COLUMN IF NOT EXISTS acted_upon BOOLEAN NOT NULL DEFAULT FALSE,
|
|
76
|
+
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
|
77
|
+
|
|
52
78
|
CREATE INDEX IF NOT EXISTS idx_rumen_insights_job_id
|
|
53
79
|
ON rumen_insights (job_id);
|
|
54
80
|
|
|
@@ -78,6 +104,16 @@ CREATE TABLE IF NOT EXISTS rumen_questions (
|
|
|
78
104
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
79
105
|
);
|
|
80
106
|
|
|
107
|
+
ALTER TABLE rumen_questions
|
|
108
|
+
ADD COLUMN IF NOT EXISTS job_id UUID,
|
|
109
|
+
ADD COLUMN IF NOT EXISTS session_id UUID,
|
|
110
|
+
ADD COLUMN IF NOT EXISTS question TEXT,
|
|
111
|
+
ADD COLUMN IF NOT EXISTS context TEXT,
|
|
112
|
+
ADD COLUMN IF NOT EXISTS asked_at TIMESTAMPTZ,
|
|
113
|
+
ADD COLUMN IF NOT EXISTS answered_at TIMESTAMPTZ,
|
|
114
|
+
ADD COLUMN IF NOT EXISTS answer TEXT,
|
|
115
|
+
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
|
116
|
+
|
|
81
117
|
CREATE INDEX IF NOT EXISTS idx_rumen_questions_job_id
|
|
82
118
|
ON rumen_questions (job_id);
|
|
83
119
|
|