@jhizzard/termdeck 0.7.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhizzard/termdeck",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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"
@@ -58,7 +58,7 @@ async function _detectInstalled(pkg) {
58
58
  child = spawn('npm', ['ls', '-g', pkg, '--depth=0', '--json'], {
59
59
  stdio: ['ignore', 'pipe', 'pipe'],
60
60
  });
61
- } catch {
61
+ } catch (_err) {
62
62
  return resolve(null);
63
63
  }
64
64
 
@@ -80,7 +80,7 @@ async function _detectInstalled(pkg) {
80
80
  const dep = parsed && parsed.dependencies && parsed.dependencies[pkg];
81
81
  if (dep && typeof dep.version === 'string') return resolve(dep.version);
82
82
  return resolve(null);
83
- } catch {
83
+ } catch (_err) {
84
84
  return resolve(null);
85
85
  }
86
86
  });
@@ -118,13 +118,13 @@ async function _fetchLatest(pkg) {
118
118
  const parsed = JSON.parse(body);
119
119
  if (parsed && typeof parsed.latest === 'string') return done(parsed.latest);
120
120
  return done(null);
121
- } catch {
121
+ } catch (_err) {
122
122
  return done(null);
123
123
  }
124
124
  });
125
125
  res.on('error', () => done(null));
126
126
  });
127
- } catch {
127
+ } catch (_err) {
128
128
  return done(null);
129
129
  }
130
130
  req.on('timeout', () => {
@@ -38,7 +38,7 @@ function defaultPackageVersion() {
38
38
  try {
39
39
  const pkg = require(path.join(__dirname, '..', '..', '..', 'package.json'));
40
40
  return pkg && pkg.version ? String(pkg.version) : null;
41
- } catch {
41
+ } catch (_err) {
42
42
  return null;
43
43
  }
44
44
  }
@@ -68,7 +68,7 @@ function readCache(cachePath) {
68
68
  if (!parsed || typeof parsed !== 'object') return null;
69
69
  if (parsed.version !== CACHE_VERSION) return null;
70
70
  return parsed;
71
- } catch {
71
+ } catch (_err) {
72
72
  return null;
73
73
  }
74
74
  }
@@ -77,7 +77,7 @@ function writeCache(cachePath, data) {
77
77
  try {
78
78
  fs.mkdirSync(path.dirname(cachePath), { recursive: true });
79
79
  fs.writeFileSync(cachePath, JSON.stringify(data, null, 2), 'utf8');
80
- } catch {
80
+ } catch (_err) {
81
81
  // Read-only home, ENOSPC, race with another process — all benign here.
82
82
  }
83
83
  }
@@ -91,7 +91,7 @@ async function fetchLatest(registryUrl) {
91
91
  const json = await res.json();
92
92
  const latest = json && json.latest;
93
93
  return isValidSemver(latest) ? latest : null;
94
- } catch {
94
+ } catch (_err) {
95
95
  return null;
96
96
  } finally {
97
97
  clearTimeout(timeout);
@@ -144,7 +144,7 @@ async function checkAndPrintHint(_config, opts) {
144
144
  ' Or run `termdeck doctor` for the whole stack. ' +
145
145
  'Suppress with TERMDECK_NO_UPDATE_CHECK=1.'
146
146
  );
147
- } catch {
147
+ } catch (_err) {
148
148
  // Never throw from a fire-and-forget hook.
149
149
  }
150
150
  }
@@ -231,13 +231,21 @@ function createBridge(config) {
231
231
  // back to resolving the session's cwd against config.projects so queries
232
232
  // don't leak into unrelated repos via basename collisions.
233
233
  let effectiveProject = project;
234
+ let projectSource = project ? 'explicit' : 'none';
234
235
  if (!effectiveProject) {
235
236
  const ctxCwd = cwd || (sessionContext && sessionContext.cwd);
236
237
  if (ctxCwd) {
237
238
  effectiveProject = resolveProjectName(ctxCwd, config);
239
+ projectSource = effectiveProject ? 'cwd' : 'none';
238
240
  }
239
241
  }
240
242
 
243
+ // Sprint 34 observability: every Flashback query announces its project tag
244
+ // and how it was resolved. If the writer chain is ever mis-emitting a tag
245
+ // (as happened pre-v0.7.2 with the `chopin-nashville` regression from the
246
+ // out-of-repo session-end hook), the mismatch surfaces here at query time.
247
+ console.log(`[mnestra-bridge] query project=${effectiveProject ?? 'ALL'} source=${searchAll ? 'searchAll' : projectSource} mode=${mode}`);
248
+
241
249
  switch (mode) {
242
250
  case 'webhook':
243
251
  return queryWebhook({ question, project: effectiveProject, searchAll });
@@ -97,53 +97,80 @@ class RAGIntegration {
97
97
 
98
98
  // Canonical project tag for a session. Prefers the explicit config.yaml name
99
99
  // (set at session creation), falls back to cwd → config.projects resolution.
100
+ // Returns { tag, source } so callers can audit which resolution path fired —
101
+ // explicit (session.meta.project), cwd (cwd matched a config.projects entry),
102
+ // fallback (cwd basename), or null (no cwd, no config). Sprint 34: the
103
+ // chopin-nashville mis-tag came from an out-of-repo writer, but source
104
+ // attribution here makes any future TermDeck-side regression visible in logs.
105
+ _resolveProjectAttribution(session) {
106
+ if (session.meta.project) return { tag: session.meta.project, source: 'explicit' };
107
+ const tag = resolveProjectName(session.meta.cwd, this.config);
108
+ if (!tag) return { tag: null, source: 'none' };
109
+ const cwdResolved = session.meta.cwd && path.resolve(String(session.meta.cwd).replace(/^~/, os.homedir()));
110
+ const matchedConfig = !!cwdResolved && Object.values((this.config && this.config.projects) || {}).some((def) => {
111
+ if (!def || typeof def.path !== 'string') return false;
112
+ const p = path.resolve(def.path.replace(/^~/, os.homedir()));
113
+ return cwdResolved === p || cwdResolved.startsWith(p + path.sep);
114
+ });
115
+ return { tag, source: matchedConfig ? 'cwd' : 'fallback' };
116
+ }
117
+
100
118
  _projectFor(session) {
101
- if (session.meta.project) return session.meta.project;
102
- return resolveProjectName(session.meta.cwd, this.config);
119
+ return this._resolveProjectAttribution(session).tag;
120
+ }
121
+
122
+ // Single attribution + observability point for session events. Logs once per
123
+ // record() so future drift in the project-resolution chain (e.g. a writer
124
+ // that bypasses _projectFor and stamps a raw path segment) is visible in
125
+ // stdout. Cheap: ~one log line per RAG event, off the hot path.
126
+ _recordForSession(session, eventType, payload) {
127
+ const { tag, source } = this._resolveProjectAttribution(session);
128
+ console.log(`[rag] write project=${tag ?? 'null'} source=${source} session=${session.id} event=${eventType}`);
129
+ this.record(session.id, eventType, payload, tag);
103
130
  }
104
131
 
105
132
  // Event types to record
106
133
  onSessionCreated(session) {
107
- this.record(session.id, 'session_created', {
134
+ this._recordForSession(session, 'session_created', {
108
135
  type: session.meta.type,
109
136
  command: session.meta.command,
110
137
  cwd: session.meta.cwd,
111
138
  reason: session.meta.reason
112
- }, this._projectFor(session));
139
+ });
113
140
  }
114
141
 
115
142
  onCommandExecuted(session, command, outputSnippet) {
116
- this.record(session.id, 'command_executed', {
143
+ this._recordForSession(session, 'command_executed', {
117
144
  command,
118
145
  output_snippet: outputSnippet?.slice(0, 500), // Truncate for storage
119
146
  type: session.meta.type
120
- }, this._projectFor(session));
147
+ });
121
148
  }
122
149
 
123
150
  onStatusChanged(session, oldStatus, newStatus) {
124
- this.record(session.id, 'status_changed', {
151
+ this._recordForSession(session, 'status_changed', {
125
152
  from: oldStatus,
126
153
  to: newStatus,
127
154
  detail: session.meta.statusDetail,
128
155
  type: session.meta.type
129
- }, this._projectFor(session));
156
+ });
130
157
  }
131
158
 
132
159
  onSessionEnded(session) {
133
- this.record(session.id, 'session_ended', {
160
+ this._recordForSession(session, 'session_ended', {
134
161
  type: session.meta.type,
135
162
  duration_ms: Date.now() - new Date(session.meta.createdAt).getTime(),
136
163
  command_count: session.meta.lastCommands.length,
137
164
  exit_code: session.meta.exitCode
138
- }, this._projectFor(session));
165
+ });
139
166
  }
140
167
 
141
168
  onFileEdited(session, filepath, editType) {
142
- this.record(session.id, 'file_edited', {
169
+ this._recordForSession(session, 'file_edited', {
143
170
  filepath,
144
171
  edit_type: editType,
145
172
  type: session.meta.type
146
- }, this._projectFor(session));
173
+ });
147
174
  }
148
175
 
149
176
  // Circuit breaker check — returns true if pushes to this table are disabled.
@@ -69,7 +69,15 @@ const PATTERNS = {
69
69
  // Stricter line-anchored variant for Claude Code, whose tool output (grep
70
70
  // results, test logs, file contents) routinely mentions "Error" mid-line
71
71
  // without representing an actual failure of the agent itself.
72
- errorLineStart: /^\s*(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)\b/m
72
+ errorLineStart: /^\s*(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)\b/m,
73
+ // Sprint 33: PATTERNS.error misses the most common Unix shell errors —
74
+ // `cat: /foo: No such file or directory`, `bash: foo: command not found`,
75
+ // `rm: cannot remove ...: Permission denied`. These have a colon-prefix
76
+ // shape (`<cmd>: ...: <phrase>`) that distinguishes them from prose
77
+ // mentioning the same words. Each branch requires either the colon-prefix
78
+ // structure or a stand-alone anchored keyword. Validated against an
79
+ // adversarial prose suite (see tests/analyzer-error-fixtures.test.js).
80
+ shellError: /(?:^|\n)(?:[^\n]*:\s+(?:.*?:\s+)?(?:No such file or directory|Permission denied|Is a directory|Not a directory|command not found)\b|[^\n]*?\(\d+\)\s+Could not resolve host\b|\s*ModuleNotFoundError:\s+\S|\s*Segmentation fault\b|\s*fatal:\s+\S)/m
73
81
  };
74
82
 
75
83
  class Session {
@@ -345,7 +353,11 @@ class Session {
345
353
  const pattern = this.meta.type === 'claude-code'
346
354
  ? PATTERNS.errorLineStart
347
355
  : PATTERNS.error;
348
- if (!pattern.test(clean)) return;
356
+ // Sprint 33 fix: the structured patterns above miss `cat: /foo: No such
357
+ // file or directory` and friends — the most common Unix shell error
358
+ // shapes Josh hits day-to-day. Fall through to PATTERNS.shellError so
359
+ // the analyzer flips status='errored' and Flashback can fire.
360
+ if (!pattern.test(clean) && !PATTERNS.shellError.test(clean)) return;
349
361
 
350
362
  const oldStatus = this.meta.status;
351
363
  this.meta.status = 'errored';
@@ -40,7 +40,7 @@ function getCurrentConfig() {
40
40
  const { loadConfig } = require('./config');
41
41
  _configCache = { mtimeMs: stat.mtimeMs, value: loadConfig(), frozen: false };
42
42
  return _configCache.value;
43
- } catch {
43
+ } catch (_err) {
44
44
  return _configCache.value || {};
45
45
  }
46
46
  }