@jhizzard/termdeck 1.0.8 → 1.0.10
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/package.json +1 -1
- package/packages/cli/src/doctor.js +18 -0
- package/packages/cli/src/index.js +12 -0
- package/packages/cli/src/init-rumen.js +51 -11
- package/packages/server/src/index.js +14 -0
- package/packages/server/src/setup/audit-upgrade.js +23 -0
- package/packages/server/src/setup/mnestra-migrations/018_rumen_processed_at.sql +40 -0
- package/packages/server/src/status-merger.js +59 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
|
|
5
5
|
"bin": {
|
|
6
6
|
"termdeck": "./packages/cli/src/index.js"
|
|
@@ -203,6 +203,24 @@ function renderFooter(rows, exitCode) {
|
|
|
203
203
|
` Or upgrade individually: npm install -g @jhizzard/termdeck@latest`
|
|
204
204
|
);
|
|
205
205
|
}
|
|
206
|
+
// Sprint 56 (T1 Cross-Cutting #2 Part A) — logical inversion fix. Pre-
|
|
207
|
+
// Sprint-56 the footer always read "All packages up to date" on exit 0,
|
|
208
|
+
// even when every row was NOT_INSTALLED. That's logically wrong: saying
|
|
209
|
+
// "all up to date" when "all not installed" misleads the user into
|
|
210
|
+
// thinking the stack is healthy. Distinguish the two states explicitly.
|
|
211
|
+
const notInstalled = rows.filter((r) => r.status === STATUS.NOT_INSTALLED).length;
|
|
212
|
+
if (notInstalled === rows.length && rows.length > 0) {
|
|
213
|
+
return (
|
|
214
|
+
`\n No stack packages detected (${rows.length} of ${rows.length} not installed).\n` +
|
|
215
|
+
` To bootstrap the full stack: npx @jhizzard/termdeck-stack`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (notInstalled > 0) {
|
|
219
|
+
return (
|
|
220
|
+
`\n ${notInstalled} of ${rows.length} stack packages not installed; the rest up to date.\n` +
|
|
221
|
+
` To install missing pieces: npx @jhizzard/termdeck-stack`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
206
224
|
return `\n All packages up to date.`;
|
|
207
225
|
}
|
|
208
226
|
|
|
@@ -104,6 +104,18 @@ function checkTranscriptTableHint(databaseUrl) {
|
|
|
104
104
|
// Parse CLI args
|
|
105
105
|
const args = process.argv.slice(2);
|
|
106
106
|
|
|
107
|
+
// Sprint 56 (T1 Cell 18) — `--version` / `-v` handler. Pre-Sprint-56 the
|
|
108
|
+
// flag was silently ignored: the CLI fell through to the launcher's stack
|
|
109
|
+
// boot path and never printed a version. Now mirror the convention every
|
|
110
|
+
// CLI in the world honors: print the package version and exit 0. Done
|
|
111
|
+
// BEFORE the `init` dispatch so `termdeck --version init --mnestra`
|
|
112
|
+
// (nonsensical but unambiguous) still terminates with the version.
|
|
113
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
114
|
+
const pkg = require(path.join(__dirname, '..', '..', '..', 'package.json'));
|
|
115
|
+
process.stdout.write(`@jhizzard/termdeck v${pkg.version}\n`);
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
|
|
107
119
|
// Subcommand dispatch — handle `termdeck init --mnestra|--rumen` before
|
|
108
120
|
// falling through to the default launcher's flag parsing. The `require` of
|
|
109
121
|
// init-*.js is lazy so users running the normal `termdeck` command never pay
|
|
@@ -172,9 +172,28 @@ function preflight() {
|
|
|
172
172
|
const missing = required.filter((k) => !secrets[k]);
|
|
173
173
|
if (missing.length > 0) {
|
|
174
174
|
fail(`missing keys: ${missing.join(', ')}`);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
// Sprint 56 (T1 Cell 10) — distinguish "no secrets file" (current
|
|
176
|
+
// message is correct: run init --mnestra to bootstrap) from "secrets
|
|
177
|
+
// file exists but missing one or more keys" (current message is
|
|
178
|
+
// misleading: user already ran init --mnestra; the actual cause is
|
|
179
|
+
// that ANTHROPIC_API_KEY is OPTIONAL for Mnestra but REQUIRED for
|
|
180
|
+
// Rumen, so a user who skipped it during init --mnestra ends up here).
|
|
181
|
+
const secretsPath = path.join(os.homedir(), '.termdeck', 'secrets.env');
|
|
182
|
+
const fileExists = fs.existsSync(secretsPath);
|
|
183
|
+
if (fileExists) {
|
|
184
|
+
process.stderr.write(
|
|
185
|
+
`\n~/.termdeck/secrets.env exists but is missing the keys above.\n` +
|
|
186
|
+
`${missing.includes('ANTHROPIC_API_KEY')
|
|
187
|
+
? 'Note: ANTHROPIC_API_KEY is optional for Mnestra but REQUIRED for Rumen.\n'
|
|
188
|
+
: ''}` +
|
|
189
|
+
`Re-run \`termdeck init --mnestra\` to add the missing keys, or edit\n` +
|
|
190
|
+
`${secretsPath} directly and re-run \`termdeck init --rumen\`.\n`
|
|
191
|
+
);
|
|
192
|
+
} else {
|
|
193
|
+
process.stderr.write(
|
|
194
|
+
'\nRun `termdeck init --mnestra` first — it writes the keys this wizard needs.\n'
|
|
195
|
+
);
|
|
196
|
+
}
|
|
178
197
|
return null;
|
|
179
198
|
}
|
|
180
199
|
ok();
|
|
@@ -896,7 +915,7 @@ function wireAccessTokenInMcpJson({ token, mcpJsonPath, _testFs } = {}) {
|
|
|
896
915
|
return { status: 'updated', path: targetPath };
|
|
897
916
|
}
|
|
898
917
|
|
|
899
|
-
function printNextSteps(projectRef, vaultResult, llmResult) {
|
|
918
|
+
function printNextSteps(projectRef, vaultResult, llmResult, skipSchedule) {
|
|
900
919
|
const rumenTickUrl = `https://${projectRef}.supabase.co/functions/v1/rumen-tick`;
|
|
901
920
|
const graphInferenceUrl = `https://${projectRef}.supabase.co/functions/v1/graph-inference`;
|
|
902
921
|
const now = new Date();
|
|
@@ -912,13 +931,24 @@ function printNextSteps(projectRef, vaultResult, llmResult) {
|
|
|
912
931
|
? ' Graph edges: classified by Claude Haiku 4.5 (GRAPH_LLM_CLASSIFY=1).'
|
|
913
932
|
: ' Graph edges: untyped (relates_to). To enable: supabase secrets set GRAPH_LLM_CLASSIFY=1';
|
|
914
933
|
|
|
934
|
+
// Sprint 56 (T1 Cell 12) — when --skip-schedule was passed, the cron
|
|
935
|
+
// schedule wasn't applied. Don't print "first run" or the cron-cadence
|
|
936
|
+
// claim — there is no scheduled first run. Show the function URL so
|
|
937
|
+
// the user knows what to manually invoke instead.
|
|
938
|
+
const rumenLine = skipSchedule
|
|
939
|
+
? ` rumen-tick cron NOT scheduled (--skip-schedule). Manual fire only.`
|
|
940
|
+
: ` rumen-tick every 15 min — first run: ${next.toISOString().replace(/\.\d+Z$/, 'Z')}`;
|
|
941
|
+
const graphLine = skipSchedule
|
|
942
|
+
? ` graph-inference cron NOT scheduled (--skip-schedule). Manual fire only.`
|
|
943
|
+
: ` graph-inference daily at 03:00 UTC (Sprint 42 cron)`;
|
|
944
|
+
|
|
915
945
|
process.stdout.write(`
|
|
916
946
|
Rumen is deployed.
|
|
917
947
|
|
|
918
948
|
Edge Functions:
|
|
919
|
-
|
|
949
|
+
${rumenLine}
|
|
920
950
|
${rumenTickUrl}
|
|
921
|
-
|
|
951
|
+
${graphLine}
|
|
922
952
|
${graphInferenceUrl}
|
|
923
953
|
|
|
924
954
|
Next steps:
|
|
@@ -954,10 +984,20 @@ async function main(argv) {
|
|
|
954
984
|
}
|
|
955
985
|
|
|
956
986
|
if (!flags.yes) {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
987
|
+
// Sprint 56 (T1 Cell 13b) — dry-run-aware annotation parity. The
|
|
988
|
+
// graph-classify confirm at line 696-698 already prints "(dry-run,
|
|
989
|
+
// defaulting Y)" when flags.dryRun is set; this prompt didn't, leaving
|
|
990
|
+
// users unsure whether they were authorizing a real deploy or a no-op
|
|
991
|
+
// simulation. Match the same shape: print the prompt with the dry-run
|
|
992
|
+
// annotation, auto-progress without blocking on user input in dry-run.
|
|
993
|
+
if (flags.dryRun) {
|
|
994
|
+
process.stdout.write(`? Proceed with deploy to project ${projectRef}? [Y/n] (dry-run, defaulting Y)\n`);
|
|
995
|
+
} else {
|
|
996
|
+
const go = await prompts.confirm(`? Proceed with deploy to project ${projectRef}?`);
|
|
997
|
+
if (!go) {
|
|
998
|
+
process.stdout.write('Cancelled.\n');
|
|
999
|
+
return 0;
|
|
1000
|
+
}
|
|
961
1001
|
}
|
|
962
1002
|
}
|
|
963
1003
|
|
|
@@ -1053,7 +1093,7 @@ async function main(argv) {
|
|
|
1053
1093
|
process.stdout.write('→ Skipping pg_cron schedule (per --skip-schedule) ✓\n');
|
|
1054
1094
|
}
|
|
1055
1095
|
|
|
1056
|
-
printNextSteps(projectRef, vaultResult, llmResult);
|
|
1096
|
+
printNextSteps(projectRef, vaultResult, llmResult, flags.skipSchedule);
|
|
1057
1097
|
return 0;
|
|
1058
1098
|
}
|
|
1059
1099
|
|
|
@@ -272,6 +272,20 @@ function createServer(config) {
|
|
|
272
272
|
|
|
273
273
|
app.use(express.json());
|
|
274
274
|
|
|
275
|
+
// Sprint 56 (T2 F-T2-1) — malformed-JSON body returns JSON 400, not
|
|
276
|
+
// express's default HTML error page. Pre-Sprint-56 every POST/PATCH
|
|
277
|
+
// endpoint that consumed a JSON body returned `text/html` on parse
|
|
278
|
+
// failure, breaking programmatic clients (the inject script, MCP, CI
|
|
279
|
+
// smoke tests). The status code (400) was correct; only the body
|
|
280
|
+
// shape regressed. Mounted IMMEDIATELY after express.json() so it
|
|
281
|
+
// catches body-parse errors before any route handler runs.
|
|
282
|
+
app.use((err, req, res, next) => {
|
|
283
|
+
if (err && (err.type === 'entity.parse.failed' || err instanceof SyntaxError)) {
|
|
284
|
+
return res.status(400).json({ error: 'Malformed JSON body', detail: err.message });
|
|
285
|
+
}
|
|
286
|
+
return next(err);
|
|
287
|
+
});
|
|
288
|
+
|
|
275
289
|
// First-run detection (Sprint 19 T3): true when ~/.termdeck/config.yaml
|
|
276
290
|
// does not exist. Surfaced on /api/config so the client can offer the
|
|
277
291
|
// setup wizard on first visit. T1's /api/setup endpoint may reuse this.
|
|
@@ -136,6 +136,29 @@ const PROBES = Object.freeze([
|
|
|
136
136
|
" and column_name = 'session_id' limit 1",
|
|
137
137
|
presentWhen: 'rowReturned'
|
|
138
138
|
},
|
|
139
|
+
{
|
|
140
|
+
// Sprint 53 T2 — Rumen picker rewrite. memory_sessions.rumen_processed_at
|
|
141
|
+
// (added by mig 018) is the picker's idempotency stamp.
|
|
142
|
+
// rumen 0.5.0+ filters `WHERE rumen_processed_at IS NULL` in extract.ts
|
|
143
|
+
// and stamps `UPDATE … SET rumen_processed_at = NOW()` between
|
|
144
|
+
// synthesize and surface (rumen/src/index.ts). Without this column,
|
|
145
|
+
// rumen-tick errors on the WHERE clause and produces 0 sessions/tick.
|
|
146
|
+
// Mig 018's ADD COLUMN IF NOT EXISTS + partial index are idempotent
|
|
147
|
+
// on already-stamped installs; safe to apply repeatedly.
|
|
148
|
+
//
|
|
149
|
+
// T4-CODEX 2026-05-04 17:32 ET pre-FIX audit catch — without this probe,
|
|
150
|
+
// a user upgrading TermDeck and only running `init --rumen --yes`
|
|
151
|
+
// could deploy the new picker without the column.
|
|
152
|
+
name: 'memory_sessions.rumen_processed_at',
|
|
153
|
+
kind: 'mnestra',
|
|
154
|
+
migrationFile: '018_rumen_processed_at.sql',
|
|
155
|
+
probeSql:
|
|
156
|
+
"select 1 as present from information_schema.columns " +
|
|
157
|
+
"where table_schema = 'public' " +
|
|
158
|
+
" and table_name = 'memory_sessions' " +
|
|
159
|
+
" and column_name = 'rumen_processed_at' limit 1",
|
|
160
|
+
presentWhen: 'rowReturned'
|
|
161
|
+
},
|
|
139
162
|
{
|
|
140
163
|
name: 'rumen-tick cron schedule',
|
|
141
164
|
kind: 'rumen',
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- Migration 018 — memory_sessions.rumen_processed_at column.
|
|
2
|
+
--
|
|
3
|
+
-- Sprint 53 T2 (Rumen picker rewrite). Adds a tracking column so Rumen's
|
|
4
|
+
-- extract phase can pick candidate sessions directly from memory_sessions
|
|
5
|
+
-- (one row per Claude Code session, post-Sprint-51.6 bundled hook) and
|
|
6
|
+
-- mark them as processed atomically when surface succeeds.
|
|
7
|
+
--
|
|
8
|
+
-- Why a column not a separate table: the picker hot path is "give me the
|
|
9
|
+
-- N most recent sessions Rumen hasn't seen yet." A boolean/timestamp on
|
|
10
|
+
-- memory_sessions answers that with one filtered range scan; a separate
|
|
11
|
+
-- rumen_processed table would force a NOT EXISTS / LEFT JOIN per tick.
|
|
12
|
+
--
|
|
13
|
+
-- Why timestamptz not boolean: the timestamp doubles as a debug aid
|
|
14
|
+
-- ("when did Rumen last touch this session?") and lets a future backfill
|
|
15
|
+
-- script identify the cutoff between pre-Sprint-53 (NULL — never touched
|
|
16
|
+
-- by the new picker) and post-Sprint-53 (stamped) without an extra column.
|
|
17
|
+
--
|
|
18
|
+
-- Idempotent — safe on:
|
|
19
|
+
-- 1. Joshua's daily-driver (pre-Sprint-53; column will be added with
|
|
20
|
+
-- every existing memory_sessions row at NULL → all become candidates
|
|
21
|
+
-- on the first post-deploy tick, which is the desired bootstrap).
|
|
22
|
+
-- 2. Brad's jizzard-brain (Linux SSH; same shape, same null-bootstrap).
|
|
23
|
+
-- 3. Fresh canonical installs (post-mig-017 schema; column added on
|
|
24
|
+
-- first run, no rows to backfill).
|
|
25
|
+
-- 4. Re-runs (ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS).
|
|
26
|
+
--
|
|
27
|
+
-- The partial index is the picker's hot path: SELECT … WHERE
|
|
28
|
+
-- rumen_processed_at IS NULL AND ended_at IS NOT NULL ORDER BY started_at DESC.
|
|
29
|
+
-- Indexing only NULL rows keeps the index tiny (hundreds of rows at any
|
|
30
|
+
-- given moment, since stamped rows drop out) — much smaller than a full
|
|
31
|
+
-- B-tree on rumen_processed_at would be.
|
|
32
|
+
|
|
33
|
+
alter table public.memory_sessions
|
|
34
|
+
add column if not exists rumen_processed_at timestamptz;
|
|
35
|
+
|
|
36
|
+
-- Partial index — covers only unprocessed sessions, ordered by recency.
|
|
37
|
+
-- Picker query plan: index range scan on the partial index, no seqscan.
|
|
38
|
+
create index if not exists memory_sessions_rumen_unprocessed_idx
|
|
39
|
+
on public.memory_sessions(started_at desc nulls last)
|
|
40
|
+
where rumen_processed_at is null;
|
|
@@ -46,6 +46,50 @@ const CANONICAL_TS_RE =
|
|
|
46
46
|
const CANONICAL_NO_TS_RE =
|
|
47
47
|
/^[-*]?\s*(T\d+):\s*(FINDING|FIX-PROPOSED|DONE)\s+[—\-]\s+(.+)$/;
|
|
48
48
|
|
|
49
|
+
// Sprint 56 (T4 Cell 7) — hardening-rule shape canonized 2026-05-04 yet
|
|
50
|
+
// the merger never knew about it. The new shape every 3+1+1 lane post
|
|
51
|
+
// follows is:
|
|
52
|
+
// `### [Tn(-CODEX)?] STAGE-VERB YYYY-MM-DD HH:MM ET — gist`
|
|
53
|
+
// where STAGE-VERB is liberal (BOOT, FINDING, FIX-PROPOSED, DONE, ACK,
|
|
54
|
+
// CHECKPOINT, AUDIT, REOPEN, NOTE, SKIP, PASS, FAIL, KICKOFF,
|
|
55
|
+
// SUB-FINDING, FINDING-MICRO, RETRACTED, …). Old merger only knew
|
|
56
|
+
// FINDING/FIX-PROPOSED/DONE so we'd silently drop everything else.
|
|
57
|
+
//
|
|
58
|
+
// Two patterns to handle:
|
|
59
|
+
// - Lines using `### ` markdown-header prefix (these are normally
|
|
60
|
+
// rejected by HEADER_RE as section headers — we MUST check this
|
|
61
|
+
// pattern BEFORE the HEADER_RE rejection in mergeStatusLine).
|
|
62
|
+
// - Lines using bare `[Tn]` without the `### ` prefix.
|
|
63
|
+
//
|
|
64
|
+
// Output shape: route the verb to the closest legacy STAGE so dashboard
|
|
65
|
+
// consumers expecting the 3-stage taxonomy still parse cleanly. Verbs
|
|
66
|
+
// outside the legacy set get bucketed by best-effort match (DONE for
|
|
67
|
+
// terminal verbs, FINDING for diagnostic, FIX-PROPOSED for action).
|
|
68
|
+
const HARDENING_RULE_RE =
|
|
69
|
+
/^(?:###\s+)?\[(T\d+(?:-[A-Z]+)?)\]\s+([A-Z][A-Z-]*)\s+(\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}\s+ET)\s+[—\-]\s+(.+)$/;
|
|
70
|
+
|
|
71
|
+
// Best-effort normalization: many of the new verbs are introductory
|
|
72
|
+
// (BOOT/KICKOFF/CHECKPOINT/ACK/NOTE → FINDING-shape information), some
|
|
73
|
+
// are terminal (DONE/PASS/FAIL/SKIP/RETRACTED → DONE-shape closure),
|
|
74
|
+
// and the action ones (FIX-PROPOSED/AUDIT/REOPEN) map to FIX-PROPOSED.
|
|
75
|
+
// FINDING/SUB-FINDING/FINDING-MICRO obviously map to FINDING.
|
|
76
|
+
function normalizeHardeningVerb(verb) {
|
|
77
|
+
const v = verb.toUpperCase();
|
|
78
|
+
if (v === 'FINDING' || v === 'SUB-FINDING' || v === 'FINDING-MICRO' ||
|
|
79
|
+
v === 'BOOT' || v === 'KICKOFF' || v === 'CHECKPOINT' ||
|
|
80
|
+
v === 'ACK' || v === 'NOTE') {
|
|
81
|
+
return 'FINDING';
|
|
82
|
+
}
|
|
83
|
+
if (v === 'FIX-PROPOSED' || v === 'AUDIT' || v === 'REOPEN') {
|
|
84
|
+
return 'FIX-PROPOSED';
|
|
85
|
+
}
|
|
86
|
+
if (v === 'DONE' || v === 'PASS' || v === 'FAIL' || v === 'SKIP' ||
|
|
87
|
+
v === 'RETRACTED' || v === 'COMPLETE' || v === 'COMPLETED') {
|
|
88
|
+
return 'DONE';
|
|
89
|
+
}
|
|
90
|
+
return 'FINDING';
|
|
91
|
+
}
|
|
92
|
+
|
|
49
93
|
// Markdown section header — never a status line.
|
|
50
94
|
const HEADER_RE = /^#{1,6}\s/;
|
|
51
95
|
// Bare bracket-like meta lines: `_(no entries yet)_`, `> note`, etc.
|
|
@@ -108,13 +152,27 @@ function mergeStatusLine(rawLine, opts = {}) {
|
|
|
108
152
|
if (typeof rawLine !== 'string') return null;
|
|
109
153
|
const line = rawLine.replace(/\r?\n$/, '').trim();
|
|
110
154
|
if (!line) return null;
|
|
155
|
+
|
|
156
|
+
// Sprint 56 (T4 Cell 7) — check the hardening-rule shape FIRST,
|
|
157
|
+
// before HEADER_RE rejects every `### `-prefixed line as a markdown
|
|
158
|
+
// section header. Sprint 51.6 hardening canonized lane posts as
|
|
159
|
+
// `### [Tn] STAGE-VERB YYYY-MM-DD HH:MM ET — gist`; without this
|
|
160
|
+
// early-return the merger silently dropped every Sprint 53/55 lane
|
|
161
|
+
// post.
|
|
162
|
+
let m = HARDENING_RULE_RE.exec(line);
|
|
163
|
+
if (m) {
|
|
164
|
+
const [, tag, verb, ts, summary] = m;
|
|
165
|
+
const stage = normalizeHardeningVerb(verb);
|
|
166
|
+
return `- ${tag}: ${stage} — ${summary.trim()} — ${ts.trim()}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
111
169
|
if (HEADER_RE.test(line)) return null;
|
|
112
170
|
if (META_RE.test(line)) return null;
|
|
113
171
|
|
|
114
172
|
// 1. Canonical with timestamp — pass through unchanged, normalize only the
|
|
115
173
|
// leading bullet. The body is never trimmed here: real Sprint 46 lines are
|
|
116
174
|
// routinely well over 120 chars and the brief mandates "same line out".
|
|
117
|
-
|
|
175
|
+
m = CANONICAL_TS_RE.exec(line);
|
|
118
176
|
if (m) {
|
|
119
177
|
const [, tag, stage, summary, ts] = m;
|
|
120
178
|
return `- ${tag}: ${stage} — ${summary.trim()} — ${ts.trim()}`;
|