@jhizzard/termdeck 1.2.0 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhizzard/termdeck",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
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"
@@ -30,7 +30,7 @@
30
30
  "dev": "node packages/server/src/index.js",
31
31
  "server": "node packages/server/src/index.js",
32
32
  "start": "NODE_ENV=production node packages/cli/src/index.js",
33
- "test": "node --test packages/server/tests/**/*.test.js",
33
+ "test": "node --test packages/server/tests/**/*.test.js packages/cli/tests/**/*.test.js packages/stack-installer/tests/**/*.test.js",
34
34
  "install:app": "bash install.sh",
35
35
  "sync-rumen-functions": "bash scripts/sync-rumen-functions.sh",
36
36
  "sync:agents": "node scripts/sync-agent-instructions.js"
@@ -202,18 +202,27 @@ if (args.includes('--version') || args.includes('-v')) {
202
202
  process.exit(0);
203
203
  }
204
204
 
205
- // Subcommand dispatch — handle `termdeck init --mnestra|--rumen` before
206
- // falling through to the default launcher's flag parsing. The `require` of
207
- // init-*.js is lazy so users running the normal `termdeck` command never pay
208
- // the cost of loading pg / supabase helpers at startup.
205
+ // Subcommand dispatch — handle `termdeck init [--mnestra|--rumen|--project|--auto|--mcp-supabase]`
206
+ // before falling through to the default launcher's flag parsing. The `require`
207
+ // of init-*.js is lazy so users running the normal `termdeck` command never
208
+ // pay the cost of loading pg / supabase helpers at startup.
209
+ //
210
+ // Sprint 64 T1: added the no-subflag default (`termdeck init` with no mode
211
+ // argument or with `--auto` / `--mcp-supabase`) routing to the new
212
+ // `init.js` top-level orchestrator. The orchestrator runs init-mnestra
213
+ // then init-rumen with a unified UX. `--auto` drives MCP-mediated
214
+ // auto-provisioning. Existing modes (--mnestra / --rumen / --project)
215
+ // stay callable independently for advanced users + CI fixtures.
209
216
  if (args[0] === 'init') {
210
217
  const mode = args[1];
211
218
  const rest = args.slice(2);
212
219
 
213
- // Sprint 37 T2: refuse mode-mixing. The dispatch picks args[1] as the
214
- // single mode flag, but a user who writes `init --project foo --mnestra`
220
+ // Sprint 37 T2 + Sprint 64 T1: refuse explicit-mode-mixing. The dispatch picks
221
+ // args[1] as the single mode flag, but a user who writes `init --project foo --mnestra`
215
222
  // probably intended only one of those. Surface the conflict instead of
216
- // silently picking the first.
223
+ // silently picking the first. The `--auto` / `--mcp-supabase` flags are
224
+ // NOT in this list — they're handled by init.js (the orchestrator) and
225
+ // can co-exist with init.js's own flag set.
217
226
  const MODES = ['--project', '--mnestra', '--rumen'];
218
227
  const presentModes = MODES.filter((m) => args.slice(1).includes(m));
219
228
  if (presentModes.length > 1) {
@@ -221,19 +230,19 @@ if (args[0] === 'init') {
221
230
  process.exit(1);
222
231
  }
223
232
 
224
- const run = (modPath) => {
233
+ const run = (modPath, argv) => {
225
234
  const fn = require(modPath);
226
- return fn(rest).then((code) => process.exit(code || 0));
235
+ return fn(argv).then((code) => process.exit(code || 0));
227
236
  };
228
237
  if (mode === '--mnestra') {
229
- run(path.join(__dirname, 'init-mnestra.js')).catch((err) => {
238
+ run(path.join(__dirname, 'init-mnestra.js'), rest).catch((err) => {
230
239
  console.error('[cli] init --mnestra failed:', err && err.stack || err);
231
240
  process.exit(1);
232
241
  });
233
242
  return;
234
243
  }
235
244
  if (mode === '--rumen') {
236
- run(path.join(__dirname, 'init-rumen.js')).catch((err) => {
245
+ run(path.join(__dirname, 'init-rumen.js'), rest).catch((err) => {
237
246
  console.error('[cli] init --rumen failed:', err && err.stack || err);
238
247
  process.exit(1);
239
248
  });
@@ -242,16 +251,44 @@ if (args[0] === 'init') {
242
251
  if (mode === '--project') {
243
252
  // init-project takes the project name as its first positional arg, plus
244
253
  // optional --dry-run / --force flags. Pass `rest` straight through.
245
- run(path.join(__dirname, 'init-project.js')).catch((err) => {
254
+ run(path.join(__dirname, 'init-project.js'), rest).catch((err) => {
246
255
  console.error('[cli] init --project failed:', err && err.stack || err);
247
256
  process.exit(1);
248
257
  });
249
258
  return;
250
259
  }
251
- console.error('Usage: termdeck init --mnestra | --rumen | --project <name>');
252
- console.error(' termdeck init --mnestra Configure Tier 2 memory (Supabase + Mnestra)');
253
- console.error(' termdeck init --rumen Deploy Tier 3 async learning (Rumen)');
254
- console.error(' termdeck init --project <name> Scaffold a new project with CLAUDE.md + orchestration docs');
260
+
261
+ // Sprint 64 T1: default (no mode) OR `--auto` / `--mcp-supabase` route to
262
+ // the unified orchestrator at init.js. It runs init-mnestra + init-rumen +
263
+ // doctor with a single progress UX. Forward ALL post-`init` args (mode
264
+ // arg included so init.js parses --auto / --mcp-supabase itself).
265
+ const orchestratorArgs = args.slice(1);
266
+ if (mode === undefined || mode === '--auto' || mode === '--mcp-supabase'
267
+ || (typeof mode === 'string' && mode.startsWith('-') && mode !== '-h')) {
268
+ // The leading-dash check catches things like `--help`, `--reset`,
269
+ // `--from-env`, `--dry-run` etc. — those are init.js orchestrator flags,
270
+ // not unknown sub-modes. Route to init.js with them intact.
271
+ run(path.join(__dirname, 'init.js'), orchestratorArgs).catch((err) => {
272
+ console.error('[cli] init failed:', err && err.stack || err);
273
+ process.exit(1);
274
+ });
275
+ return;
276
+ }
277
+ // `-h` alone after `init` → orchestrator help
278
+ if (mode === '-h') {
279
+ run(path.join(__dirname, 'init.js'), ['--help']).catch((err) => {
280
+ console.error('[cli] init failed:', err && err.stack || err);
281
+ process.exit(1);
282
+ });
283
+ return;
284
+ }
285
+ console.error('Usage: termdeck init [--auto] | --mnestra | --rumen | --project <name>');
286
+ console.error(' termdeck init Unified setup (Mnestra + Rumen + doctor)');
287
+ console.error(' termdeck init --auto Auto-provision via Supabase MCP (alias: --mcp-supabase)');
288
+ console.error(' termdeck init --mnestra Configure Tier 2 memory (Supabase + Mnestra)');
289
+ console.error(' termdeck init --rumen Deploy Tier 3 async learning (Rumen)');
290
+ console.error(' termdeck init --project <name> Scaffold a new project with CLAUDE.md + orchestration docs');
291
+ console.error(' termdeck init --help Show full flag reference');
255
292
  process.exit(1);
256
293
  }
257
294
 
@@ -684,6 +684,12 @@ const SETTINGS_JSON_PATH = path.join(require('os').homedir(), '.claude', 'settin
684
684
  const HOOK_COMMAND = 'node ~/.claude/hooks/memory-session-end.js';
685
685
  const HOOK_TIMEOUT_SECONDS = 30;
686
686
 
687
+ // Sprint 64 T3 — PreCompact hook (Investigation 2 of CRITICAL-READ-FIRST-
688
+ // 2026-05-07.md). Lives alongside the SessionEnd hook; refreshes via the
689
+ // same Sprint 51.6 T3 version-stamp gate.
690
+ const PRECOMPACT_HOOK_COMMAND = 'node ~/.claude/hooks/memory-pre-compact.js';
691
+ const PRECOMPACT_HOOK_TIMEOUT_SECONDS = 30;
692
+
687
693
  function _isSessionEndHookEntry(entry) {
688
694
  return entry && typeof entry.command === 'string'
689
695
  && entry.command.includes('memory-session-end.js');
@@ -739,6 +745,42 @@ function _mergeSessionEndHookEntry(settings, opts = {}) {
739
745
  return { settings, status: migrated ? 'migrated-from-stop' : 'installed' };
740
746
  }
741
747
 
748
+ // Sprint 64 T3 — PreCompact entry detection + merge. Hoisted mirror of
749
+ // `_isPreCompactHookEntry` / `_mergePreCompactHookEntry` in
750
+ // `packages/stack-installer/src/index.js` (same reasoning as the SessionEnd
751
+ // duplication above: the published @jhizzard/termdeck tarball ships only
752
+ // `packages/stack-installer/assets/hooks/**`, not `.../src/**`).
753
+ function _isPreCompactHookEntry(entry) {
754
+ return entry && typeof entry.command === 'string'
755
+ && entry.command.includes('memory-pre-compact.js');
756
+ }
757
+
758
+ function _mergePreCompactHookEntry(settings, opts = {}) {
759
+ const command = opts.command || PRECOMPACT_HOOK_COMMAND;
760
+ const timeout = opts.timeout != null ? opts.timeout : PRECOMPACT_HOOK_TIMEOUT_SECONDS;
761
+ const entry = { type: 'command', command, timeout };
762
+
763
+ if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
764
+ if (!Array.isArray(settings.hooks.PreCompact)) settings.hooks.PreCompact = [];
765
+
766
+ for (const group of settings.hooks.PreCompact) {
767
+ if (!group || !Array.isArray(group.hooks)) continue;
768
+ if (group.hooks.some(_isPreCompactHookEntry)) {
769
+ return { settings, status: 'already-installed' };
770
+ }
771
+ }
772
+
773
+ const wildcardGroup = settings.hooks.PreCompact.find(
774
+ (g) => g && g.matcher === '*' && Array.isArray(g.hooks)
775
+ );
776
+ if (wildcardGroup) {
777
+ wildcardGroup.hooks.push(entry);
778
+ } else {
779
+ settings.hooks.PreCompact.push({ matcher: '*', hooks: [entry] });
780
+ }
781
+ return { settings, status: 'installed' };
782
+ }
783
+
742
784
  function _readSettingsJson(filePath) {
743
785
  if (!fs.existsSync(filePath)) {
744
786
  return { settings: {}, status: 'no-file' };
@@ -808,6 +850,43 @@ function migrateSettingsJsonHookEntry(opts = {}) {
808
850
  return { status: merge.status, settingsPath, backup };
809
851
  }
810
852
 
853
+ // Sprint 64 T3 — settings.json wiring for the PreCompact hook. Parallel to
854
+ // migrateSettingsJsonHookEntry above but simpler (no Stop→event migration
855
+ // branch; PreCompact didn't exist pre-Sprint-64).
856
+ function migrateSettingsJsonPreCompactEntry(opts = {}) {
857
+ const dryRun = !!opts.dryRun;
858
+ const settingsPath = opts.settingsPath || SETTINGS_JSON_PATH;
859
+
860
+ const read = _readSettingsJson(settingsPath);
861
+ if (read.status === 'malformed') {
862
+ return { status: 'malformed', error: read.error, settingsPath };
863
+ }
864
+
865
+ const before = JSON.stringify(read.settings);
866
+ const merge = _mergePreCompactHookEntry(read.settings);
867
+ const after = JSON.stringify(merge.settings);
868
+ const noChange = before === after;
869
+
870
+ if (merge.status === 'already-installed' || noChange) {
871
+ return { status: 'already-installed', settingsPath };
872
+ }
873
+
874
+ if (dryRun) {
875
+ return { status: 'would-' + merge.status, settingsPath };
876
+ }
877
+
878
+ let backup = null;
879
+ if (read.status === 'ok' || read.status === 'empty') {
880
+ const stamp = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14);
881
+ backup = `${settingsPath}.bak.${stamp}`;
882
+ try { fs.copyFileSync(settingsPath, backup); }
883
+ catch (_) { backup = null; }
884
+ }
885
+
886
+ _writeSettingsJson(settingsPath, merge.settings);
887
+ return { status: merge.status, settingsPath, backup };
888
+ }
889
+
811
890
  function runSettingsJsonMigration({ dryRun = false } = {}) {
812
891
  const debug = !!process.env.TERMDECK_DEBUG_WIREUP;
813
892
  step('Reconciling ~/.claude/settings.json hook event mapping (Stop → SessionEnd)...');
@@ -841,6 +920,29 @@ function runSettingsJsonMigration({ dryRun = false } = {}) {
841
920
  process.stdout.write(` ! settings.json migration failed: ${err.message} (continuing)\n`);
842
921
  if (debug) process.stderr.write(`[wire-up-debug] runSettingsJsonMigration threw: ${err && err.stack || err}\n`);
843
922
  }
923
+
924
+ // Sprint 64 T3 — wire the PreCompact hook into settings.json. Brand new in
925
+ // Sprint 64 (no legacy migration), so the merge either creates the entry or
926
+ // reports already-installed; nothing else to do.
927
+ step('Reconciling ~/.claude/settings.json PreCompact wiring...');
928
+ try {
929
+ const r = migrateSettingsJsonPreCompactEntry({ dryRun });
930
+ if (debug) process.stderr.write(`[wire-up-debug] runSettingsJsonMigration pre-compact return: ${JSON.stringify(r)}\n`);
931
+ if (r.status === 'already-installed') {
932
+ ok('already wired (PreCompact)');
933
+ } else if (r.status === 'installed') {
934
+ ok(r.backup ? `installed (PreCompact; backup: ${path.basename(r.backup)})` : 'installed (PreCompact)');
935
+ } else if (r.status === 'would-installed') {
936
+ ok('would install (PreCompact) (dry-run)');
937
+ } else if (r.status === 'malformed') {
938
+ ok(`(skipped: settings.json malformed: ${r.error})`);
939
+ } else {
940
+ ok(`(${r.status})`);
941
+ }
942
+ } catch (err) {
943
+ process.stdout.write(` ! PreCompact wiring failed: ${err.message} (continuing)\n`);
944
+ if (debug) process.stderr.write(`[wire-up-debug] runSettingsJsonMigration pre-compact threw: ${err && err.stack || err}\n`);
945
+ }
844
946
  }
845
947
 
846
948
  function runHookRefresh({ dryRun = false } = {}) {
@@ -876,6 +978,35 @@ function runHookRefresh({ dryRun = false } = {}) {
876
978
  process.stdout.write(` ! hook refresh failed: ${err.message} (continuing)\n`);
877
979
  if (debug) process.stderr.write(`[wire-up-debug] runHookRefresh threw: ${err && err.stack || err}\n`);
878
980
  }
981
+
982
+ // Sprint 64 T3 — also refresh the PreCompact hook. Re-uses the same
983
+ // version-stamp gate (refreshBundledHookIfNewer is parameterized by
984
+ // destPath + sourcePath; both hooks carry the `@termdeck/stack-installer-
985
+ // hook v<N>` marker the gate reads).
986
+ step('Refreshing ~/.claude/hooks/memory-pre-compact.js (if bundled is newer)...');
987
+ try {
988
+ const HOME = require('os').homedir();
989
+ const PRE_DEST = path.join(HOME, '.claude', 'hooks', 'memory-pre-compact.js');
990
+ const PRE_SOURCE = path.join(__dirname, '..', '..', 'stack-installer', 'assets', 'hooks', 'memory-pre-compact.js');
991
+ const r = refreshBundledHookIfNewer({ dryRun, destPath: PRE_DEST, sourcePath: PRE_SOURCE });
992
+ if (debug) process.stderr.write(`[wire-up-debug] runHookRefresh pre-compact return: ${JSON.stringify(r)}\n`);
993
+ if (r.status === 'refreshed') {
994
+ ok(`refreshed v${r.from ?? 0} → v${r.to} (backup: ${path.basename(r.backup)})`);
995
+ } else if (r.status === 'would-refresh') {
996
+ ok(`would-refresh v${r.from ?? 0} → v${r.to} (dry-run)`);
997
+ } else if (r.status === 'installed') {
998
+ ok(`installed v${r.bundled} (no prior copy)`);
999
+ } else if (r.status === 'would-install') {
1000
+ ok(`would-install v${r.bundled} (dry-run, no prior copy)`);
1001
+ } else if (r.status === 'up-to-date') {
1002
+ ok(`up-to-date (v${r.installed})`);
1003
+ } else {
1004
+ ok(`(${r.status}${r.message ? ': ' + r.message : ''})`);
1005
+ }
1006
+ } catch (err) {
1007
+ process.stdout.write(` ! pre-compact hook refresh failed: ${err.message} (continuing)\n`);
1008
+ if (debug) process.stderr.write(`[wire-up-debug] runHookRefresh pre-compact threw: ${err && err.stack || err}\n`);
1009
+ }
879
1010
  }
880
1011
 
881
1012
  function printNextSteps() {