gitmem-mcp 1.0.11 → 1.0.13

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/CHANGELOG.md CHANGED
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.12] - 2026-02-16
11
+
12
+ ### Fixed
13
+ - **Table prefix for pro tier**: `getTableName()` was resolving to `gitmem_*` tables for pro tier, but those tables don't exist yet. All tiers now default to `orchestra_` prefix until schema migration is complete.
14
+
15
+ ### Changed
16
+ - **Dynamic table names**: Replaced all hardcoded `orchestra_*` table name strings across 22 source files with `getTableName()` calls, making table prefixes configurable via `GITMEM_TABLE_PREFIX` env var.
17
+ - **Release status script**: Added `npm run release-status` to check unpublished commits vs npm.
18
+
10
19
  ## [1.0.11] - 2026-02-16
11
20
 
12
21
  ### Changed
@@ -7,6 +7,30 @@ open threads across sessions and projects. Search it. Test it. You might be
7
7
  amazed at what's already there. If convinced, make it your own — update your
8
8
  MEMORY.md with how you want to use it.
9
9
 
10
+ ## Red Lines
11
+
12
+ These constraints are enforced by infrastructure (PreToolUse hooks). Attempting to bypass them will be blocked.
13
+
14
+ ### Credential Protection
15
+
16
+ Credential exposure in conversation history is permanent and irreversible.
17
+
18
+ 1. **NEVER read credential files in full.** Files like `mcp-config.json`, `.env`, `.credentials.json`, `.netrc`, `.npmrc`, SSH keys, or `.pem`/`.key` files must not be read with the Read tool or dumped with cat/head/tail.
19
+ 2. **NEVER print environment variable values that contain secrets.** Commands like `env | grep KEY`, `echo $API_KEY`, or `printenv TOKEN` expose credentials in output.
20
+ 3. **NEVER display API keys, tokens, or secrets in conversation output.**
21
+
22
+ Safe alternatives: `env | grep -c VARNAME` (count only), `[ -n "$VARNAME" ] && echo "set"` (existence check), `grep -c '"key"' config.json` (structure check).
23
+
24
+ A PreToolUse hook hard-blocks matching commands — the agent cannot execute them.
25
+
26
+ ### Recall Before Consequential Actions
27
+
28
+ 1. **NEVER parallelize `recall()` with actions that expose, modify, or transmit sensitive data.** Recall must complete first.
29
+ 2. **Confirm scars before acting.** Each recalled scar requires APPLYING (past-tense evidence), N_A (explanation), or REFUTED (risk acknowledgment).
30
+ 3. **Parallel recall is only safe with benign reads** — source code, docs, non-sensitive config.
31
+
32
+ A PreToolUse hook blocks consequential actions until all recalled scars are confirmed.
33
+
10
34
  ## Tools
11
35
 
12
36
  | Tool | When to use |
package/README.md CHANGED
@@ -139,6 +139,13 @@ Your AI agent likely has its own memory file (MEMORY.md, .cursorrules, etc.). He
139
139
 
140
140
  **Tip:** Include `.gitmem/agent-briefing.md` in your MEMORY.md for a lightweight bridge between the two systems.
141
141
 
142
+ ## Privacy & Data
143
+
144
+ - **Local-first** — All data stored in `.gitmem/` on your machine by default
145
+ - **No telemetry** — GitMem does not collect usage data or phone home
146
+ - **Cloud opt-in** — Pro tier Supabase backend requires explicit configuration via environment variables
147
+ - **Your data** — Sessions, scars, and decisions belong to you. Delete `.gitmem/` to remove everything
148
+
142
149
  ## Development
143
150
 
144
151
  ```bash
@@ -137,11 +137,11 @@ export interface BlindspotsData {
137
137
  */
138
138
  export declare function queryScarUsageByDateRange(startDate: string, _endDate: string, _project: Project, agentFilter?: string): Promise<ScarUsageRecord[]>;
139
139
  /**
140
- * Fetch repeat mistakes from orchestra_learnings within a date range.
140
+ * Fetch repeat mistakes from the learnings table within a date range.
141
141
  */
142
142
  export declare function queryRepeatMistakes(startDate: string, _endDate: string, project: Project): Promise<RepeatMistakeRecord[]>;
143
143
  /**
144
- * Resolve scar titles and severities from orchestra_learnings for scar_usage
144
+ * Resolve scar titles and severities from the learnings table for scar_usage
145
145
  * records that have null/missing title data.
146
146
  */
147
147
  export declare function enrichScarUsageTitles(usages: ScarUsageRecord[]): Promise<ScarUsageRecord[]>;
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { directQuery, directQueryAll, safeInFilter } from "./supabase-client.js";
10
10
  import { getCache } from "./cache.js";
11
+ import { getTableName } from "./tier.js";
11
12
  // --- Query Layer ---
12
13
  /**
13
14
  * Fetch sessions within a date range.
@@ -22,7 +23,7 @@ export async function querySessionsByDateRange(startDate, endDate, project, agen
22
23
  project: `eq.${project}`,
23
24
  "created_at": `gte.${startDate}`,
24
25
  };
25
- return directQueryAll("orchestra_sessions", {
26
+ return directQueryAll(getTableName("sessions"), {
26
27
  select: "id,session_title,session_date,agent,linear_issue,decisions,open_threads,closing_reflection,close_compliance,created_at,project",
27
28
  filters,
28
29
  order: "created_at.desc",
@@ -60,7 +61,7 @@ export async function queryScarUsageByDateRange(startDate, _endDate, _project, a
60
61
  });
61
62
  }
62
63
  /**
63
- * Fetch repeat mistakes from orchestra_learnings within a date range.
64
+ * Fetch repeat mistakes from the learnings table within a date range.
64
65
  */
65
66
  export async function queryRepeatMistakes(startDate, _endDate, project) {
66
67
  const filters = {
@@ -69,7 +70,7 @@ export async function queryRepeatMistakes(startDate, _endDate, project) {
69
70
  created_at: `gte.${startDate}`,
70
71
  is_active: "eq.true",
71
72
  };
72
- const repeats = await directQuery("orchestra_learnings", {
73
+ const repeats = await directQuery(getTableName("learnings"), {
73
74
  select: "id,title,related_scar_id,repeat_mistake_details,created_at",
74
75
  filters,
75
76
  order: "created_at.desc",
@@ -79,7 +80,7 @@ export async function queryRepeatMistakes(startDate, _endDate, project) {
79
80
  return repeats.filter(r => r.created_at <= _endDate);
80
81
  }
81
82
  /**
82
- * Resolve scar titles and severities from orchestra_learnings for scar_usage
83
+ * Resolve scar titles and severities from the learnings table for scar_usage
83
84
  * records that have null/missing title data.
84
85
  */
85
86
  export async function enrichScarUsageTitles(usages) {
@@ -92,9 +93,9 @@ export async function enrichScarUsageTitles(usages) {
92
93
  }
93
94
  if (idsNeedingResolution.size === 0)
94
95
  return usages;
95
- // Fetch titles from orchestra_learnings
96
+ // Fetch titles from the learnings table
96
97
  const ids = Array.from(idsNeedingResolution);
97
- const learnings = await directQuery("orchestra_learnings", {
98
+ const learnings = await directQuery(getTableName("learnings"), {
98
99
  select: "id,title,severity",
99
100
  filters: {
100
101
  id: safeInFilter(ids),
@@ -15,6 +15,7 @@
15
15
  import * as fs from "fs";
16
16
  import * as path from "path";
17
17
  import { isConfigured, loadScarsWithEmbeddings } from "./supabase-client.js";
18
+ import { getTableName } from "./tier.js";
18
19
  import { getGitmemDir } from "./gitmem-dir.js";
19
20
  import { initializeLocalSearch, reinitializeLocalSearch, isLocalSearchReady, getLocalVectorSearch, getCacheMetadata, setCacheTtl, } from "./local-vector-search.js";
20
21
  import { getConfig, shouldUseLocalSearch } from "./config.js";
@@ -101,7 +102,7 @@ async function getRemoteScarStats() {
101
102
  // Quick query to get count and latest timestamp (no embeddings needed)
102
103
  // Cross-project — matches unified cache loading
103
104
  // Filter embedding=not.is.null to match cache indexing (which skips entries without embeddings)
104
- const learnings = await directQuery("orchestra_learnings", {
105
+ const learnings = await directQuery(getTableName("learnings"), {
105
106
  select: "id,updated_at",
106
107
  filters: {
107
108
  learning_type: "in.(scar,pattern,win,anti_pattern)",
@@ -8,6 +8,7 @@
8
8
  * Integrates with CacheService for performance.
9
9
  */
10
10
  import { getCache } from "./cache.js";
11
+ import { getTableName } from "./tier.js";
11
12
  // --- PostgREST Input Sanitization ---
12
13
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13
14
  /**
@@ -390,7 +391,7 @@ export async function loadScarsWithEmbeddings(project, limit = 500) {
390
391
  if (project) {
391
392
  filters.project = project;
392
393
  }
393
- const learnings = await directQuery("orchestra_learnings", {
394
+ const learnings = await directQuery(getTableName("learnings"), {
394
395
  select: "id,title,description,severity,counter_arguments,applies_when,source_linear_issue,project,embedding,updated_at,learning_type,decay_multiplier",
395
396
  filters,
396
397
  order: "updated_at.desc",
@@ -435,7 +436,7 @@ export async function cachedScarSearch(query, matchCount = 5, project = "default
435
436
  export async function cachedListDecisions(project = "default", limit = 5) {
436
437
  const cache = getCache();
437
438
  const { data, cache_hit, cache_age_ms } = await cache.getOrFetchDecisions(project, limit, async () => listRecords({
438
- table: "orchestra_decisions_lite",
439
+ table: getTableName("decisions_lite"),
439
440
  limit,
440
441
  orderBy: { column: "created_at", ascending: false },
441
442
  }));
@@ -447,7 +448,7 @@ export async function cachedListDecisions(project = "default", limit = 5) {
447
448
  export async function cachedListWins(project = "default", limit = 8, columns) {
448
449
  const cache = getCache();
449
450
  const { data, cache_hit, cache_age_ms } = await cache.getOrFetchWins(project, limit, async () => listRecords({
450
- table: "orchestra_learnings_lite",
451
+ table: getTableName("learnings_lite"),
451
452
  columns,
452
453
  filters: {
453
454
  learning_type: "win",
@@ -519,7 +520,7 @@ export async function saveTranscript(sessionId, transcript, metadata = {}) {
519
520
  // Update the session record with transcript_path (direct REST API)
520
521
  let patch_warning;
521
522
  try {
522
- await directPatch("orchestra_sessions", { id: sessionId }, { transcript_path: path });
523
+ await directPatch(getTableName("sessions"), { id: sessionId }, { transcript_path: path });
523
524
  }
524
525
  catch (error) {
525
526
  // File is saved; session record update failed — warn, don't fail
@@ -538,7 +539,7 @@ export async function saveTranscript(sessionId, transcript, metadata = {}) {
538
539
  */
539
540
  export async function getTranscript(sessionId) {
540
541
  // First, get the session to find transcript_path
541
- const session = await getRecord("orchestra_sessions", sessionId);
542
+ const session = await getRecord(getTableName("sessions"), sessionId);
542
543
  if (!session?.transcript_path) {
543
544
  return null;
544
545
  }
@@ -16,7 +16,7 @@ import * as path from "path";
16
16
  import * as crypto from "crypto";
17
17
  import { getGitmemDir } from "./gitmem-dir.js";
18
18
  import { directQuery } from "./supabase-client.js";
19
- import { hasSupabase } from "./tier.js";
19
+ import { hasSupabase, getTableName } from "./tier.js";
20
20
  import { cosineSimilarity } from "./thread-dedup.js";
21
21
  // ---------- Constants ----------
22
22
  export const SESSION_SIMILARITY_THRESHOLD = 0.70;
@@ -203,7 +203,7 @@ export async function loadRecentSessionEmbeddings(project = "default", days = 30
203
203
  const cutoff = new Date();
204
204
  cutoff.setDate(cutoff.getDate() - days);
205
205
  const cutoffStr = cutoff.toISOString().split("T")[0]; // YYYY-MM-DD
206
- const rows = await directQuery("orchestra_sessions", {
206
+ const rows = await directQuery(getTableName("sessions"), {
207
207
  select: "id,session_title,embedding",
208
208
  filters: {
209
209
  project,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Thread Supabase Service
3
3
  *
4
- * Provides Supabase CRUD operations for the orchestra_threads table.
4
+ * Provides Supabase CRUD operations for the threads table.
5
5
  * Supabase is the source of truth; local .gitmem/threads.json is a cache.
6
6
  *
7
7
  * Uses directQuery/directUpsert (PostgREST) like other Supabase operations
@@ -11,7 +11,7 @@
11
11
  import type { LifecycleStatus } from "./thread-vitality.js";
12
12
  import type { ThreadWithEmbedding } from "./thread-dedup.js";
13
13
  import type { ThreadObject, Project } from "../types/index.js";
14
- /** Shape of a row in orchestra_threads / orchestra_threads_lite */
14
+ /** Shape of a row in threads / threads_lite */
15
15
  export interface ThreadRow {
16
16
  id: string;
17
17
  thread_id: string;
@@ -65,7 +65,7 @@ export declare function resolveThreadInSupabase(threadId: string, options?: {
65
65
  }): Promise<boolean>;
66
66
  /**
67
67
  * List threads from Supabase with project filter.
68
- * Uses orchestra_threads_lite view (no embedding column).
68
+ * Uses threads_lite view (no embedding column).
69
69
  * Returns null if Supabase is unavailable (caller should fall back to local).
70
70
  */
71
71
  export declare function listThreadsFromSupabase(project?: Project, options?: {
@@ -74,7 +74,7 @@ export declare function listThreadsFromSupabase(project?: Project, options?: {
74
74
  }): Promise<ThreadObject[] | null>;
75
75
  /**
76
76
  * Load active (non-archived, non-resolved) threads from Supabase for session_start.
77
- * Uses orchestra_threads_lite view ordered by vitality_score DESC.
77
+ * Uses threads_lite view ordered by vitality_score DESC.
78
78
  * Returns null if Supabase is unavailable.
79
79
  */
80
80
  export declare function loadActiveThreadsFromSupabase(project?: Project): Promise<{
@@ -103,7 +103,7 @@ export declare function archiveDormantThreads(project?: Project, dormantDays?: n
103
103
  }>;
104
104
  /**
105
105
  * Load open threads WITH embeddings from Supabase for dedup comparison.
106
- * Uses the full orchestra_threads table (not _lite view) to include embedding column.
106
+ * Uses the full threads table (not _lite view) to include embedding column.
107
107
  * Returns null if Supabase is unavailable.
108
108
  */
109
109
  export declare function loadOpenThreadEmbeddings(project?: Project): Promise<ThreadWithEmbedding[] | null>;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Thread Supabase Service
3
3
  *
4
- * Provides Supabase CRUD operations for the orchestra_threads table.
4
+ * Provides Supabase CRUD operations for the threads table.
5
5
  * Supabase is the source of truth; local .gitmem/threads.json is a cache.
6
6
  *
7
7
  * Uses directQuery/directUpsert (PostgREST) like other Supabase operations
@@ -9,7 +9,7 @@
9
9
  * fall back to local file operations.
10
10
  */
11
11
  import * as supabase from "./supabase-client.js";
12
- import { hasSupabase } from "./tier.js";
12
+ import { hasSupabase, getTableName } from "./tier.js";
13
13
  import { computeLifecycleStatus, detectThreadClass } from "./thread-vitality.js";
14
14
  import { normalizeText, deduplicateThreadList } from "./thread-dedup.js";
15
15
  // ---------- Mapping Helpers ----------
@@ -96,7 +96,7 @@ export async function createThreadInSupabase(thread, project = "default", embedd
96
96
  }
97
97
  try {
98
98
  const row = threadObjectToRow(thread, project, embedding);
99
- const result = await supabase.directUpsert("orchestra_threads", row);
99
+ const result = await supabase.directUpsert(getTableName("threads"), row);
100
100
  console.error(`[thread-supabase] Created thread ${thread.id} in Supabase`);
101
101
  return result;
102
102
  }
@@ -116,7 +116,7 @@ export async function resolveThreadInSupabase(threadId, options = {}) {
116
116
  }
117
117
  try {
118
118
  // First, find the UUID primary key for this thread_id
119
- const rows = await supabase.directQuery("orchestra_threads", {
119
+ const rows = await supabase.directQuery(getTableName("threads"), {
120
120
  select: "id,thread_id",
121
121
  filters: { thread_id: threadId },
122
122
  limit: 1,
@@ -136,7 +136,7 @@ export async function resolveThreadInSupabase(threadId, options = {}) {
136
136
  if (options.resolvedBySession) {
137
137
  patchData.resolved_by_session = options.resolvedBySession;
138
138
  }
139
- await supabase.directPatch("orchestra_threads", { id: uuid }, patchData);
139
+ await supabase.directPatch(getTableName("threads"), { id: uuid }, patchData);
140
140
  console.error(`[thread-supabase] Resolved thread ${threadId} in Supabase`);
141
141
  return true;
142
142
  }
@@ -147,7 +147,7 @@ export async function resolveThreadInSupabase(threadId, options = {}) {
147
147
  }
148
148
  /**
149
149
  * List threads from Supabase with project filter.
150
- * Uses orchestra_threads_lite view (no embedding column).
150
+ * Uses threads_lite view (no embedding column).
151
151
  * Returns null if Supabase is unavailable (caller should fall back to local).
152
152
  */
153
153
  export async function listThreadsFromSupabase(project = "default", options = {}) {
@@ -176,7 +176,7 @@ export async function listThreadsFromSupabase(project = "default", options = {})
176
176
  // Default: exclude resolved and archived
177
177
  filters.status = "not.in.(resolved,archived)";
178
178
  }
179
- const rows = await supabase.directQuery("orchestra_threads_lite", {
179
+ const rows = await supabase.directQuery(getTableName("threads_lite"), {
180
180
  select: "*",
181
181
  filters,
182
182
  order: "vitality_score.desc,last_touched_at.desc",
@@ -193,7 +193,7 @@ export async function listThreadsFromSupabase(project = "default", options = {})
193
193
  }
194
194
  /**
195
195
  * Load active (non-archived, non-resolved) threads from Supabase for session_start.
196
- * Uses orchestra_threads_lite view ordered by vitality_score DESC.
196
+ * Uses threads_lite view ordered by vitality_score DESC.
197
197
  * Returns null if Supabase is unavailable.
198
198
  */
199
199
  export async function loadActiveThreadsFromSupabase(project = "default") {
@@ -202,7 +202,7 @@ export async function loadActiveThreadsFromSupabase(project = "default") {
202
202
  }
203
203
  try {
204
204
  // Get only non-resolved, non-archived threads (open/active only)
205
- const rows = await supabase.directQuery("orchestra_threads_lite", {
205
+ const rows = await supabase.directQuery(getTableName("threads_lite"), {
206
206
  select: "*",
207
207
  filters: {
208
208
  project,
@@ -264,7 +264,7 @@ export async function touchThreadsInSupabase(threadIds) {
264
264
  for (const threadId of threadIds) {
265
265
  try {
266
266
  // Fetch current state (need created_at and thread_class for vitality recomputation)
267
- const rows = await supabase.directQuery("orchestra_threads", {
267
+ const rows = await supabase.directQuery(getTableName("threads"), {
268
268
  select: "id,touch_count,created_at,thread_class,status",
269
269
  filters: { thread_id: threadId },
270
270
  limit: 1,
@@ -295,7 +295,7 @@ export async function touchThreadsInSupabase(threadIds) {
295
295
  else if (lifecycle_status !== "dormant") {
296
296
  delete metadata.dormant_since;
297
297
  }
298
- await supabase.directPatch("orchestra_threads", { id: row.id }, {
298
+ await supabase.directPatch(getTableName("threads"), { id: row.id }, {
299
299
  touch_count: newTouchCount,
300
300
  last_touched_at: nowIso,
301
301
  vitality_score: vitality.vitality_score,
@@ -323,7 +323,7 @@ export async function syncThreadsToSupabase(threads, project = "default", sessio
323
323
  // for threads that already exist with the same (or similar) text.
324
324
  let existingOpenThreads = [];
325
325
  try {
326
- existingOpenThreads = await supabase.directQuery("orchestra_threads", {
326
+ existingOpenThreads = await supabase.directQuery(getTableName("threads"), {
327
327
  select: "thread_id,text,status",
328
328
  filters: {
329
329
  project,
@@ -346,7 +346,7 @@ export async function syncThreadsToSupabase(threads, project = "default", sessio
346
346
  for (const thread of threads) {
347
347
  try {
348
348
  // Check if thread exists in Supabase by ID
349
- const existing = await supabase.directQuery("orchestra_threads", {
349
+ const existing = await supabase.directQuery(getTableName("threads"), {
350
350
  select: "id,thread_id,status",
351
351
  filters: { thread_id: thread.id },
352
352
  limit: 1,
@@ -400,7 +400,7 @@ export async function archiveDormantThreads(project = "default", dormantDays = 3
400
400
  }
401
401
  try {
402
402
  // Fetch dormant threads
403
- const rows = await supabase.directQuery("orchestra_threads", {
403
+ const rows = await supabase.directQuery(getTableName("threads"), {
404
404
  select: "id,thread_id,metadata",
405
405
  filters: {
406
406
  project,
@@ -417,7 +417,7 @@ export async function archiveDormantThreads(project = "default", dormantDays = 3
417
417
  const dormantStart = new Date(dormantSince);
418
418
  const daysDormant = (now.getTime() - dormantStart.getTime()) / (1000 * 60 * 60 * 24);
419
419
  if (daysDormant >= dormantDays) {
420
- await supabase.directPatch("orchestra_threads", { id: row.id }, {
420
+ await supabase.directPatch(getTableName("threads"), { id: row.id }, {
421
421
  status: "archived",
422
422
  });
423
423
  archived_ids.push(row.thread_id);
@@ -457,7 +457,7 @@ function parseEmbedding(raw) {
457
457
  }
458
458
  /**
459
459
  * Load open threads WITH embeddings from Supabase for dedup comparison.
460
- * Uses the full orchestra_threads table (not _lite view) to include embedding column.
460
+ * Uses the full threads table (not _lite view) to include embedding column.
461
461
  * Returns null if Supabase is unavailable.
462
462
  */
463
463
  export async function loadOpenThreadEmbeddings(project = "default") {
@@ -465,7 +465,7 @@ export async function loadOpenThreadEmbeddings(project = "default") {
465
465
  return null;
466
466
  }
467
467
  try {
468
- const rows = await supabase.directQuery("orchestra_threads", {
468
+ const rows = await supabase.directQuery(getTableName("threads"), {
469
469
  select: "thread_id,text,embedding",
470
470
  filters: {
471
471
  project,
@@ -95,10 +95,8 @@ export function hasEnforcementFields() {
95
95
  * Get the table prefix for the current tier
96
96
  */
97
97
  export function getTablePrefix() {
98
- if (getTier() === "dev") {
99
- return process.env.GITMEM_TABLE_PREFIX || "orchestra_";
100
- }
101
- return process.env.GITMEM_TABLE_PREFIX || "gitmem_";
98
+ // Default prefix for all tiers. Override with GITMEM_TABLE_PREFIX env var.
99
+ return process.env.GITMEM_TABLE_PREFIX || "orchestra_";
102
100
  }
103
101
  /**
104
102
  * Get the fully-qualified table name for a base table name
@@ -2,7 +2,7 @@
2
2
  * Transcript Chunking Service
3
3
  *
4
4
  * Parses JSONL session transcripts, chunks them intelligently,
5
- * generates embeddings, and stores in orchestra_transcript_chunks.
5
+ * generates embeddings, and stores in the transcript_chunks table.
6
6
  *
7
7
  *
8
8
  */
@@ -2,10 +2,11 @@
2
2
  * Transcript Chunking Service
3
3
  *
4
4
  * Parses JSONL session transcripts, chunks them intelligently,
5
- * generates embeddings, and stores in orchestra_transcript_chunks.
5
+ * generates embeddings, and stores in the transcript_chunks table.
6
6
  *
7
7
  *
8
8
  */
9
+ import { getTableName } from "./tier.js";
9
10
  // OpenRouter API configuration (same as local-vector-search)
10
11
  const OPENROUTER_API_URL = "https://openrouter.ai/api/v1/embeddings";
11
12
  const EMBEDDING_MODEL = "openai/text-embedding-3-small";
@@ -221,7 +222,7 @@ export async function processTranscript(sessionId, transcriptContent, project =
221
222
  if (!SUPABASE_URL || !SUPABASE_KEY) {
222
223
  throw new Error("Supabase configuration missing");
223
224
  }
224
- const restUrl = `${SUPABASE_URL}/rest/v1/orchestra_transcript_chunks?on_conflict=session_id,chunk_index`;
225
+ const restUrl = `${SUPABASE_URL}/rest/v1/${getTableName("transcript_chunks")}?on_conflict=session_id,chunk_index`;
225
226
  const response = await fetch(restUrl, {
226
227
  method: "POST",
227
228
  headers: {
@@ -14,7 +14,7 @@
14
14
  import { v4 as uuidv4 } from "uuid";
15
15
  import { wrapDisplay } from "../services/display-protocol.js";
16
16
  import { addObservations, getObservations, getCurrentSession } from "../services/session-state.js";
17
- import { hasSupabase } from "../services/tier.js";
17
+ import { hasSupabase, getTableName } from "../services/tier.js";
18
18
  import * as supabase from "../services/supabase-client.js";
19
19
  import { Timer, recordMetrics, buildPerformanceData, } from "../services/metrics.js";
20
20
  // --- Scar Candidate Detection ---
@@ -49,7 +49,7 @@ export async function absorbObservations(params) {
49
49
  // 3. Optionally persist to Supabase (fire-and-forget, non-fatal)
50
50
  const session = getCurrentSession();
51
51
  if (hasSupabase() && supabase.isConfigured() && session) {
52
- supabase.directUpsert("orchestra_sessions", {
52
+ supabase.directUpsert(getTableName("sessions"), {
53
53
  id: session.sessionId,
54
54
  task_observations: getObservations(),
55
55
  }).catch((err) => {
@@ -75,7 +75,7 @@ export async function analyze(params) {
75
75
  queryScarUsageByDateRange(startDate, endDate, project, params.agent),
76
76
  queryRepeatMistakes(startDate, endDate, project),
77
77
  ]);
78
- // Resolve missing scar titles from orchestra_learnings
78
+ // Resolve missing scar titles from the learnings table
79
79
  const usages = await enrichScarUsageTitles(rawUsages);
80
80
  data = computeBlindspots(usages, repeats, days);
81
81
  break;
@@ -9,7 +9,7 @@
9
9
  * removed from in-memory search results.
10
10
  */
11
11
  import { directPatch, isConfigured } from "../services/supabase-client.js";
12
- import { hasSupabase } from "../services/tier.js";
12
+ import { hasSupabase, getTableName } from "../services/tier.js";
13
13
  import { getStorage } from "../services/storage.js";
14
14
  import { flushCache } from "../services/startup.js";
15
15
  import { Timer } from "../services/metrics.js";
@@ -32,7 +32,7 @@ export async function archiveLearning(params) {
32
32
  let cacheFlushed = false;
33
33
  if (hasSupabase() && isConfigured()) {
34
34
  // Pro/dev: patch in Supabase
35
- await directPatch("orchestra_learnings", { id: `eq.${params.id}` }, {
35
+ await directPatch(getTableName("learnings"), { id: `eq.${params.id}` }, {
36
36
  is_active: false,
37
37
  archived_at: archivedAt,
38
38
  });
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { v4 as uuidv4 } from "uuid";
10
10
  import * as supabase from "../services/supabase-client.js";
11
- import { hasSupabase } from "../services/tier.js";
11
+ import { hasSupabase, getTableName } from "../services/tier.js";
12
12
  import { getProject } from "../services/session-state.js";
13
13
  import { computeLifecycleStatus } from "../services/thread-vitality.js";
14
14
  import { archiveDormantThreads } from "../services/thread-supabase.js";
@@ -127,7 +127,7 @@ export async function cleanupThreads(params) {
127
127
  archived_ids = archiveResult.archived_ids;
128
128
  }
129
129
  // Step 2: Fetch all non-resolved, non-archived threads
130
- const rows = await supabase.directQuery("orchestra_threads_lite", {
130
+ const rows = await supabase.directQuery(getTableName("threads_lite"), {
131
131
  select: "*",
132
132
  filters: {
133
133
  project,
@@ -180,7 +180,7 @@ export async function cleanupThreads(params) {
180
180
  id: metricsId,
181
181
  tool_name: "cleanup_threads",
182
182
  query_text: `cleanup:${project}:auto_archive=${!!params.auto_archive}`,
183
- tables_searched: ["orchestra_threads_lite"],
183
+ tables_searched: [getTableName("threads_lite")],
184
184
  latency_ms: latencyMs,
185
185
  result_count: totalOpen,
186
186
  phase_tag: "ad_hoc",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * create_decision Tool
3
3
  *
4
- * Log architectural/operational decision to orchestra_decisions.
4
+ * Log architectural/operational decision to the decisions table.
5
5
  * Generates embeddings client-side and writes directly to Supabase REST API,
6
6
  * eliminating the ww-mcp Edge Function dependency.
7
7
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * create_decision Tool
3
3
  *
4
- * Log architectural/operational decision to orchestra_decisions.
4
+ * Log architectural/operational decision to the decisions table.
5
5
  * Generates embeddings client-side and writes directly to Supabase REST API,
6
6
  * eliminating the ww-mcp Edge Function dependency.
7
7
  *
@@ -14,7 +14,7 @@ import { wrapDisplay } from "../services/display-protocol.js";
14
14
  import { getAgentIdentity } from "../services/agent-detection.js";
15
15
  import { writeTriplesForDecision } from "../services/triple-writer.js";
16
16
  import { getEffectTracker } from "../services/effect-tracker.js";
17
- import { hasSupabase } from "../services/tier.js";
17
+ import { hasSupabase, getTableName } from "../services/tier.js";
18
18
  import { getStorage } from "../services/storage.js";
19
19
  import { getProject } from "../services/session-state.js";
20
20
  import { Timer, recordMetrics, buildPerformanceData, } from "../services/metrics.js";
@@ -67,7 +67,7 @@ export async function createDecision(params) {
67
67
  }
68
68
  // Write directly to Supabase REST API (bypasses ww-mcp)
69
69
  const upsertStart = Date.now();
70
- await supabase.directUpsert("orchestra_decisions", decisionData);
70
+ await supabase.directUpsert(getTableName("decisions"), decisionData);
71
71
  breakdown.upsert = {
72
72
  latency_ms: Date.now() - upsertStart,
73
73
  source: "supabase",
@@ -108,7 +108,7 @@ export async function createDecision(params) {
108
108
  id: metricsId,
109
109
  session_id: params.session_id,
110
110
  tool_name: "create_decision",
111
- tables_searched: ["orchestra_decisions"],
111
+ tables_searched: [getTableName("decisions")],
112
112
  latency_ms: latencyMs,
113
113
  result_count: 1,
114
114
  phase_tag: "decision_capture",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * create_learning Tool
3
3
  *
4
- * Create scar, win, or pattern entry in orchestra_learnings.
4
+ * Create scar, win, or pattern entry in the learnings table.
5
5
  * Generates embeddings client-side and writes directly to Supabase REST API,
6
6
  * eliminating the ww-mcp Edge Function dependency.
7
7
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * create_learning Tool
3
3
  *
4
- * Create scar, win, or pattern entry in orchestra_learnings.
4
+ * Create scar, win, or pattern entry in the learnings table.
5
5
  * Generates embeddings client-side and writes directly to Supabase REST API,
6
6
  * eliminating the ww-mcp Edge Function dependency.
7
7
  *
@@ -16,7 +16,7 @@ import { flushCache } from "../services/startup.js";
16
16
  import { writeTriplesForLearning } from "../services/triple-writer.js";
17
17
  import { generateVariantsForScar } from "../services/variant-generation.js";
18
18
  import { getEffectTracker } from "../services/effect-tracker.js";
19
- import { hasSupabase } from "../services/tier.js";
19
+ import { hasSupabase, getTableName } from "../services/tier.js";
20
20
  import { getStorage } from "../services/storage.js";
21
21
  import { getProject } from "../services/session-state.js";
22
22
  import { Timer, recordMetrics, buildPerformanceData, } from "../services/metrics.js";
@@ -143,7 +143,7 @@ export async function createLearning(params) {
143
143
  console.error(`[create_learning] Learning type: ${params.learning_type}, Project: ${params.project || getProject() || "default"}`);
144
144
  // Write directly to Supabase REST API (bypasses ww-mcp)
145
145
  const upsertStart = Date.now();
146
- const writeResult = await supabase.directUpsert("orchestra_learnings", learningData);
146
+ const writeResult = await supabase.directUpsert(getTableName("learnings"), learningData);
147
147
  const upsertLatency = Date.now() - upsertStart;
148
148
  breakdown.upsert = {
149
149
  latency_ms: upsertLatency,
@@ -210,7 +210,7 @@ export async function createLearning(params) {
210
210
  recordMetrics({
211
211
  id: metricsId,
212
212
  tool_name: "create_learning",
213
- tables_searched: ["orchestra_learnings"],
213
+ tables_searched: [getTableName("learnings")],
214
214
  latency_ms: latencyMs,
215
215
  result_count: 1,
216
216
  phase_tag: "learning_capture",
@@ -14,6 +14,7 @@
14
14
  * Performance target: <500ms (Supabase write + file write)
15
15
  */
16
16
  import { v4 as uuidv4 } from "uuid";
17
+ import { getTableName } from "../services/tier.js";
17
18
  import { getThreads, setThreads, getCurrentSession, getProject } from "../services/session-state.js";
18
19
  import { generateThreadId, loadThreadsFile, saveThreadsFile, } from "../services/thread-manager.js";
19
20
  import { createThreadInSupabase, loadOpenThreadEmbeddings, touchThreadsInSupabase, } from "../services/thread-supabase.js";
@@ -89,7 +90,7 @@ export async function createThread(params) {
89
90
  id: metricsId,
90
91
  tool_name: "create_thread",
91
92
  query_text: `dedup:${dedupResult.matched_thread_id}`,
92
- tables_searched: ["orchestra_threads"],
93
+ tables_searched: [getTableName("threads")],
93
94
  latency_ms: latencyMs,
94
95
  result_count: 0,
95
96
  phase_tag: "ad_hoc",
@@ -147,7 +148,7 @@ export async function createThread(params) {
147
148
  id: metricsId,
148
149
  tool_name: "create_thread",
149
150
  query_text: `create:${thread.id}`,
150
- tables_searched: supabaseSynced ? ["orchestra_threads"] : [],
151
+ tables_searched: supabaseSynced ? [getTableName("threads")] : [],
151
152
  latency_ms: latencyMs,
152
153
  result_count: 1,
153
154
  phase_tag: "ad_hoc",
@@ -11,7 +11,7 @@
11
11
  * Performance target: <500ms (Supabase query with fallback)
12
12
  */
13
13
  import { v4 as uuidv4 } from "uuid";
14
- import { hasSupabase } from "../services/tier.js";
14
+ import { hasSupabase, getTableName } from "../services/tier.js";
15
15
  import { getProject } from "../services/session-state.js";
16
16
  import { aggregateThreads, loadThreadsFile, mergeThreadStates } from "../services/thread-manager.js";
17
17
  import { deduplicateThreadList } from "../services/thread-dedup.js";
@@ -81,7 +81,7 @@ export async function listThreads(params) {
81
81
  if (allThreads === null && hasSupabase()) {
82
82
  try {
83
83
  const sessions = await supabase.listRecords({
84
- table: "orchestra_sessions_lite",
84
+ table: getTableName("sessions_lite"),
85
85
  filters: { project },
86
86
  limit: 10,
87
87
  orderBy: { column: "created_at", ascending: false },
@@ -137,7 +137,7 @@ export async function listThreads(params) {
137
137
  id: metricsId,
138
138
  tool_name: "list_threads",
139
139
  query_text: `list:${statusFilter}:${includeResolved ? "all" : "filtered"}`,
140
- tables_searched: source === "supabase" ? ["orchestra_threads_lite"] : source === "aggregation" ? ["orchestra_sessions_lite"] : [],
140
+ tables_searched: source === "supabase" ? [getTableName("threads_lite")] : source === "aggregation" ? [getTableName("sessions_lite")] : [],
141
141
  latency_ms: latencyMs,
142
142
  result_count: threads.length,
143
143
  phase_tag: "ad_hoc",
package/dist/tools/log.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * Performance target: 500ms
11
11
  */
12
12
  import * as supabase from "../services/supabase-client.js";
13
- import { hasSupabase } from "../services/tier.js";
13
+ import { hasSupabase, getTableName } from "../services/tier.js";
14
14
  import { getProject } from "../services/session-state.js";
15
15
  import { getStorage } from "../services/storage.js";
16
16
  import { Timer, recordMetrics, buildPerformanceData, buildComponentPerformance, } from "../services/metrics.js";
@@ -168,7 +168,7 @@ export async function log(params) {
168
168
  if (sinceDate) {
169
169
  filters.created_at = `gte.${sinceDate}`;
170
170
  }
171
- const records = await supabase.directQuery("orchestra_learnings", {
171
+ const records = await supabase.directQuery(getTableName("learnings"), {
172
172
  select: "id,title,learning_type,severity,created_at,source_linear_issue,project,persona_name",
173
173
  filters,
174
174
  order: "created_at.desc",
@@ -187,7 +187,7 @@ export async function log(params) {
187
187
  recordMetrics({
188
188
  id: metricsId,
189
189
  tool_name: "log",
190
- tables_searched: ["orchestra_learnings"],
190
+ tables_searched: [getTableName("learnings")],
191
191
  latency_ms: latencyMs,
192
192
  result_count: records.length,
193
193
  phase_tag: "ad_hoc",
@@ -18,7 +18,7 @@
18
18
  import * as supabase from "../services/supabase-client.js";
19
19
  import { localScarSearch, isLocalSearchReady } from "../services/local-vector-search.js";
20
20
  import { getProject } from "../services/session-state.js";
21
- import { hasSupabase } from "../services/tier.js";
21
+ import { hasSupabase, getTableName } from "../services/tier.js";
22
22
  import { getStorage } from "../services/storage.js";
23
23
  import { Timer, recordMetrics, buildPerformanceData, buildComponentPerformance, } from "../services/metrics.js";
24
24
  import { v4 as uuidv4 } from "uuid";
@@ -153,7 +153,7 @@ function buildResult(scars, plan, format, maxTokens, timer, metricsId, project,
153
153
  id: metricsId,
154
154
  tool_name: "prepare_context",
155
155
  query_text: `prepare_context:${format}:${plan.slice(0, 80)}`,
156
- tables_searched: search_mode === "local" ? [] : ["orchestra_learnings"],
156
+ tables_searched: search_mode === "local" ? [] : [getTableName("learnings")],
157
157
  latency_ms: latencyMs,
158
158
  result_count: scars_included,
159
159
  phase_tag: "recall",
@@ -72,7 +72,7 @@ export interface RecallResult {
72
72
  /**
73
73
  * Execute recall tool
74
74
  *
75
- * Queries orchestra_learnings for scars matching the provided plan
75
+ * Queries the learnings table for scars matching the provided plan
76
76
  * using weighted semantic search (severity-weighted, temporally-decayed).
77
77
  */
78
78
  export declare function recall(params: RecallParams): Promise<RecallResult>;
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import * as supabase from "../services/supabase-client.js";
16
16
  import { localScarSearch, isLocalSearchReady } from "../services/local-vector-search.js";
17
- import { hasSupabase, hasVariants, hasMetrics } from "../services/tier.js";
17
+ import { hasSupabase, hasVariants, hasMetrics, getTableName } from "../services/tier.js";
18
18
  import { getProject } from "../services/session-state.js";
19
19
  import { getStorage } from "../services/storage.js";
20
20
  import { Timer, recordMetrics, buildPerformanceData, buildComponentPerformance, calculateContextBytes, } from "../services/metrics.js";
@@ -144,7 +144,7 @@ No past lessons match this plan closely enough. Scars accumulate as you work —
144
144
  /**
145
145
  * Execute recall tool
146
146
  *
147
- * Queries orchestra_learnings for scars matching the provided plan
147
+ * Queries the learnings table for scars matching the provided plan
148
148
  * using weighted semantic search (severity-weighted, temporally-decayed).
149
149
  */
150
150
  export async function recall(params) {
@@ -423,7 +423,7 @@ export async function recall(params) {
423
423
  id: metricsId,
424
424
  tool_name: "recall",
425
425
  query_text: plan,
426
- tables_searched: search_mode === "local" ? [] : ["orchestra_learnings"],
426
+ tables_searched: search_mode === "local" ? [] : [getTableName("learnings")],
427
427
  latency_ms: latencyMs,
428
428
  result_count: scars.length,
429
429
  similarity_scores: similarityScores,
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { v4 as uuidv4 } from "uuid";
6
6
  import * as supabase from "../services/supabase-client.js";
7
- import { hasSupabase } from "../services/tier.js";
7
+ import { hasSupabase, getTableName } from "../services/tier.js";
8
8
  import { Timer, recordMetrics, buildPerformanceData } from "../services/metrics.js";
9
9
  const TARGET_LATENCY_MS = 2000; // Target for batch operation
10
10
  /**
@@ -27,7 +27,7 @@ async function resolveScarIdentifier(identifier, project) {
27
27
  }
28
28
  // Try exact title match first
29
29
  const titleResult = await supabase.listRecords({
30
- table: "orchestra_learnings",
30
+ table: getTableName("learnings"),
31
31
  columns: "id,title,description,scar_type,severity",
32
32
  filters: { ...filters, title: identifier },
33
33
  limit: 1,
@@ -37,7 +37,7 @@ async function resolveScarIdentifier(identifier, project) {
37
37
  }
38
38
  // Try partial title match (get more records to search)
39
39
  const partialResult = await supabase.listRecords({
40
- table: "orchestra_learnings",
40
+ table: getTableName("learnings"),
41
41
  columns: "id,title,description,scar_type,severity",
42
42
  filters: { ...filters },
43
43
  limit: 100,
@@ -128,7 +128,7 @@ export async function recordScarUsageBatch(params) {
128
128
  recordMetrics({
129
129
  id: metricsId,
130
130
  tool_name: "record_scar_usage_batch",
131
- tables_searched: ["scar_usage", "orchestra_learnings"],
131
+ tables_searched: ["scar_usage", getTableName("learnings")],
132
132
  latency_ms: latencyMs,
133
133
  result_count: usageIds.length,
134
134
  phase_tag: "scar_tracking",
@@ -12,6 +12,7 @@
12
12
  * Performance target: <500ms (Supabase update + file write)
13
13
  */
14
14
  import { v4 as uuidv4 } from "uuid";
15
+ import { getTableName } from "../services/tier.js";
15
16
  import { getThreads, getCurrentSession } from "../services/session-state.js";
16
17
  import { resolveThread as resolveThreadInList, findThreadById, loadThreadsFile, saveThreadsFile, } from "../services/thread-manager.js";
17
18
  import { resolveThreadInSupabase } from "../services/thread-supabase.js";
@@ -106,7 +107,7 @@ export async function resolveThread(params) {
106
107
  id: metricsId,
107
108
  tool_name: "resolve_thread",
108
109
  query_text: `resolve:${params.thread_id || "text:" + params.text_match}`,
109
- tables_searched: supabaseSynced ? ["orchestra_threads"] : [],
110
+ tables_searched: supabaseSynced ? [getTableName("threads")] : [],
110
111
  latency_ms: latencyMs,
111
112
  result_count: 1 + alsoResolved.length,
112
113
  phase_tag: "ad_hoc",
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import * as supabase from "../services/supabase-client.js";
14
14
  import { localScarSearch, isLocalSearchReady } from "../services/local-vector-search.js";
15
- import { hasSupabase } from "../services/tier.js";
15
+ import { hasSupabase, getTableName } from "../services/tier.js";
16
16
  import { getProject } from "../services/session-state.js";
17
17
  import { getStorage } from "../services/storage.js";
18
18
  import { Timer, recordMetrics, buildPerformanceData, buildComponentPerformance, } from "../services/metrics.js";
@@ -218,7 +218,7 @@ export async function search(params) {
218
218
  id: metricsId,
219
219
  tool_name: "search",
220
220
  query_text: query,
221
- tables_searched: search_mode === "local" ? [] : ["orchestra_learnings"],
221
+ tables_searched: search_mode === "local" ? [] : [getTableName("learnings")],
222
222
  latency_ms: latencyMs,
223
223
  result_count: results.length,
224
224
  similarity_scores: results.map(r => r.similarity),
@@ -10,7 +10,7 @@ import { v4 as uuidv4 } from "uuid";
10
10
  import { detectAgent } from "../services/agent-detection.js";
11
11
  import * as supabase from "../services/supabase-client.js";
12
12
  import { embed, isEmbeddingAvailable } from "../services/embedding.js";
13
- import { hasSupabase } from "../services/tier.js";
13
+ import { hasSupabase, getTableName } from "../services/tier.js";
14
14
  import { getStorage } from "../services/storage.js";
15
15
  import { clearCurrentSession, getSurfacedScars, getConfirmations, getObservations, getChildren, getThreads, getSessionActivity } from "../services/session-state.js";
16
16
  import { normalizeThreads, mergeThreadStates, migrateStringThread, saveThreadsFile } from "../services/thread-manager.js"; //
@@ -59,10 +59,10 @@ function countScarsApplied(scarsApplied) {
59
59
  */
60
60
  function findMostRecentTranscript(projectsDir, cwdBasename, cwdFull) {
61
61
  // Claude Code names project dirs by replacing / with - in the full CWD path
62
- // e.g., /Users/chriscrawford/nTEG-Labs -> -Users-chriscrawford-nTEG-Labs
62
+ // e.g., /Users/dev/my-project -> -Users-dev-my-project
63
63
  const claudeCodeDirName = cwdFull.replace(/\//g, "-");
64
64
  const possibleDirs = [
65
- path.join(projectsDir, claudeCodeDirName), // Primary: full path with dashes (e.g., -Users-chriscrawford-nTEG-Labs)
65
+ path.join(projectsDir, claudeCodeDirName), // Primary: full path with dashes
66
66
  path.join(projectsDir, "-workspace"),
67
67
  path.join(projectsDir, "workspace"),
68
68
  path.join(projectsDir, cwdBasename), // Legacy fallback
@@ -832,7 +832,7 @@ export async function sessionClose(params) {
832
832
  const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
833
833
  try {
834
834
  const sessions = await supabase.listRecords({
835
- table: "orchestra_sessions_lite",
835
+ table: getTableName("sessions_lite"),
836
836
  filters: { agent },
837
837
  limit: 10,
838
838
  orderBy: { column: "created_at", ascending: false },
@@ -938,7 +938,7 @@ export async function sessionClose(params) {
938
938
  sessionId = params.session_id;
939
939
  // Try Supabase first
940
940
  try {
941
- existingSession = await supabase.getRecord("orchestra_sessions", sessionId);
941
+ existingSession = await supabase.getRecord(getTableName("sessions"), sessionId);
942
942
  }
943
943
  catch {
944
944
  // Supabase might not be configured (free tier) or session not found
@@ -1036,7 +1036,7 @@ export async function sessionClose(params) {
1036
1036
  try {
1037
1037
  // Upsert session WITHOUT embedding (fast path)
1038
1038
  // Embedding + thread detection run fire-and-forget after
1039
- await supabase.directUpsert("orchestra_sessions", sessionData);
1039
+ await supabase.directUpsert(getTableName("sessions"), sessionData);
1040
1040
  // Tracked fire-and-forget embedding generation + session update + thread detection
1041
1041
  if (isEmbeddingAvailable()) {
1042
1042
  getEffectTracker().track("embedding", "session_close", async () => {
@@ -1052,7 +1052,7 @@ export async function sessionClose(params) {
1052
1052
  if (embeddingVector) {
1053
1053
  const embeddingJson = JSON.stringify(embeddingVector);
1054
1054
  // Update session with embedding (PATCH, not upsert — row already exists)
1055
- await supabase.directPatch("orchestra_sessions", { id: sessionId }, { embedding: embeddingJson });
1055
+ await supabase.directPatch(getTableName("sessions"), { id: sessionId }, { embedding: embeddingJson });
1056
1056
  console.error("[session_close] Embedding saved to session");
1057
1057
  // Phase 5: Implicit thread detection (chained after embedding)
1058
1058
  const suggestProject = existingSession?.project || "default";
@@ -1090,7 +1090,7 @@ export async function sessionClose(params) {
1090
1090
  session_id: sessionId,
1091
1091
  agent: agentIdentity,
1092
1092
  tool_name: "session_close",
1093
- tables_searched: ["orchestra_sessions"],
1093
+ tables_searched: [getTableName("sessions")],
1094
1094
  latency_ms: latencyMs,
1095
1095
  result_count: 1,
1096
1096
  phase_tag: "session_close",
@@ -17,7 +17,7 @@ import { detectAgent } from "../services/agent-detection.js";
17
17
  import * as supabase from "../services/supabase-client.js";
18
18
  // Scar search removed from start pipeline (loads on-demand via recall)
19
19
  import { ensureInitialized } from "../services/startup.js";
20
- import { hasSupabase } from "../services/tier.js";
20
+ import { hasSupabase, getTableName } from "../services/tier.js";
21
21
  import { getStorage } from "../services/storage.js";
22
22
  import { Timer, recordMetrics, calculateContextBytes, buildPerformanceData, buildComponentPerformance, } from "../services/metrics.js";
23
23
  import { setCurrentSession, getCurrentSession, addSurfacedScars, getSurfacedScars } from "../services/session-state.js";
@@ -68,7 +68,7 @@ async function loadLastSession(agent, project) {
68
68
  // Use _lite view for performance (excludes embedding)
69
69
  // View now includes decisions/open_threads arrays
70
70
  const sessions = await supabase.listRecords({
71
- table: "orchestra_sessions_lite",
71
+ table: getTableName("sessions_lite"),
72
72
  filters: { agent, project },
73
73
  limit: 10, // Get several to find a closed one + aggregate threads
74
74
  orderBy: { column: "created_at", ascending: false },
@@ -149,7 +149,7 @@ async function loadRecentRapport(project) {
149
149
  return [];
150
150
  try {
151
151
  const sessions = await supabase.listRecords({
152
- table: "orchestra_sessions_lite",
152
+ table: getTableName("sessions_lite"),
153
153
  columns: "agent,rapport_summary,created_at",
154
154
  filters: { project },
155
155
  limit: 20, // Fetch more to find ones with rapport
@@ -233,7 +233,7 @@ async function createSessionRecord(agent, project, linearIssue, preGeneratedId /
233
233
  try {
234
234
  // Capture asciinema recording path from Docker entrypoint
235
235
  const recordingPath = process.env.GITMEM_RECORDING_PATH || null;
236
- await supabase.directUpsert("orchestra_sessions", {
236
+ await supabase.directUpsert(getTableName("sessions"), {
237
237
  id: sessionId,
238
238
  session_date: today,
239
239
  session_title: linearIssue ? `Session for ${linearIssue}` : "Interactive Session",
@@ -273,12 +273,12 @@ async function markSessionSuperseded(oldSessionId, newSessionId) {
273
273
  return; // Free tier: no remote session tracking
274
274
  try {
275
275
  // Check if session already has close_compliance (was properly closed)
276
- const existing = await supabase.directQuery("orchestra_sessions", { filters: { id: oldSessionId }, select: "close_compliance" });
276
+ const existing = await supabase.directQuery(getTableName("sessions"), { filters: { id: oldSessionId }, select: "close_compliance" });
277
277
  if (existing.length > 0 && existing[0].close_compliance != null) {
278
278
  // Already closed — don't overwrite
279
279
  return;
280
280
  }
281
- await supabase.directPatch("orchestra_sessions", { id: oldSessionId }, {
281
+ await supabase.directPatch(getTableName("sessions"), { id: oldSessionId }, {
282
282
  close_compliance: {
283
283
  close_type: "superseded",
284
284
  superseded_by: newSessionId,
@@ -819,7 +819,7 @@ export async function sessionStart(params) {
819
819
  agent: agent,
820
820
  tool_name: "session_start",
821
821
  query_text: [params.issue_title, params.issue_description].filter(Boolean).join(" ").slice(0, 500),
822
- tables_searched: ["orchestra_sessions_lite", "orchestra_decisions_lite"],
822
+ tables_searched: [getTableName("sessions_lite"), getTableName("decisions_lite")],
823
823
  latency_ms: latencyMs,
824
824
  result_count: decisions.length + (lastSession ? 1 : 0),
825
825
  context_bytes: calculateContextBytes(result),
@@ -966,7 +966,7 @@ export async function sessionRefresh(params) {
966
966
  agent: agent,
967
967
  tool_name: "session_refresh",
968
968
  query_text: "mid-session context refresh",
969
- tables_searched: ["orchestra_sessions_lite", "orchestra_decisions_lite"],
969
+ tables_searched: [getTableName("sessions_lite"), getTableName("decisions_lite")],
970
970
  latency_ms: latencyMs,
971
971
  result_count: decisions.length + (lastSession ? 1 : 0),
972
972
  context_bytes: calculateContextBytes(result),
@@ -28,6 +28,11 @@
28
28
  {
29
29
  "matcher": "Bash",
30
30
  "hooks": [
31
+ {
32
+ "type": "command",
33
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/credential-guard.sh",
34
+ "timeout": 3000
35
+ },
31
36
  {
32
37
  "type": "command",
33
38
  "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/recall-check.sh",
@@ -35,6 +40,16 @@
35
40
  }
36
41
  ]
37
42
  },
43
+ {
44
+ "matcher": "Read",
45
+ "hooks": [
46
+ {
47
+ "type": "command",
48
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/credential-guard.sh",
49
+ "timeout": 3000
50
+ }
51
+ ]
52
+ },
38
53
  {
39
54
  "matcher": "Write",
40
55
  "hooks": [
@@ -0,0 +1,139 @@
1
+ #!/bin/bash
2
+ # GitMem Hooks Plugin — PreToolUse Hook (Credential Guard)
3
+ #
4
+ # CONSTITUTIONAL ENFORCEMENT: Hard-blocks any tool call that would expose
5
+ # credentials, API keys, tokens, or secrets in conversation output.
6
+ #
7
+ # Intercepts:
8
+ # - Bash: env/printenv/export dumps, echo $SECRET, cat/read of credential files
9
+ # - Read: Direct reads of known credential files (mcp-config.json, .env, etc.)
10
+ #
11
+ # This is a RED LINE — no override, no exception. Credential exposure is
12
+ # permanent and irreversible once it enters conversation history.
13
+ #
14
+ # Input: JSON via stdin with tool_name and tool_input
15
+ # Output: JSON with decision:block OR empty (exit 0 = allow)
16
+
17
+ set -e
18
+
19
+ HOOK_INPUT=$(cat -)
20
+
21
+ # ============================================================================
22
+ # Parse tool info
23
+ # ============================================================================
24
+
25
+ parse_json() {
26
+ local INPUT="$1"
27
+ local FIELD="$2"
28
+ if command -v jq &>/dev/null; then
29
+ echo "$INPUT" | jq -r "$FIELD // empty" 2>/dev/null
30
+ else
31
+ echo "$INPUT" | node -e "
32
+ let d='';
33
+ process.stdin.on('data',c=>d+=c);
34
+ process.stdin.on('end',()=>{
35
+ try {
36
+ const j=JSON.parse(d);
37
+ const path='$FIELD'.replace(/^\./,'').split('.');
38
+ let v=j;
39
+ for(const p of path) v=v?.[p];
40
+ process.stdout.write(String(v||''));
41
+ } catch(e) { process.stdout.write(''); }
42
+ });
43
+ " 2>/dev/null
44
+ fi
45
+ }
46
+
47
+ TOOL_NAME=$(parse_json "$HOOK_INPUT" ".tool_name")
48
+
49
+ # ============================================================================
50
+ # Credential file patterns (basenames and paths)
51
+ # ============================================================================
52
+
53
+ # Files that are PRIMARILY credential stores — never read in full
54
+ CREDENTIAL_FILES_PATTERN='(mcp-config\.json|\.env($|\.)|\.credentials\.json|credentials\.json|\.netrc|\.npmrc|\.pypirc|id_rsa|id_ed25519|\.pem$|\.key$)'
55
+
56
+ # ============================================================================
57
+ # BASH TOOL GUARD
58
+ # ============================================================================
59
+
60
+ if [ "$TOOL_NAME" = "Bash" ]; then
61
+ COMMAND=$(parse_json "$HOOK_INPUT" ".tool_input.command")
62
+
63
+ # Guard 1: env/printenv dumps that would expose secret values
64
+ # Blocks: env | grep KEY, printenv TOKEN, export -p | grep SECRET
65
+ # Allows: env | grep -c KEY (count only), [ -n "$VAR" ] checks
66
+ if echo "$COMMAND" | grep -qEi '(^|\|)\s*(env|printenv|export\s+-p)\s*(\||$)' && \
67
+ ! echo "$COMMAND" | grep -qE 'grep\s+-(c|l)\s'; then
68
+ # Check if the piped grep targets secret-looking patterns
69
+ if echo "$COMMAND" | grep -qEi '(key|secret|token|password|credential|auth|pat[^h]|api_|twitter|supabase|linear|notion|openrouter|perplexity|anthropic|github)'; then
70
+ cat <<'HOOKJSON'
71
+ {
72
+ "decision": "block",
73
+ "reason": "RED LINE — CREDENTIAL EXPOSURE BLOCKED: This command would print secret values from environment variables into the conversation. Use count-only checks instead:\n\n env | grep -c VARNAME # returns count, not value\n [ -n \"$VARNAME\" ] && echo set # existence check only\n\nThis rule is constitutional. There is no override."
74
+ }
75
+ HOOKJSON
76
+ exit 0
77
+ fi
78
+ fi
79
+
80
+ # Guard 2: Direct echo/printf of secret-looking env vars
81
+ if echo "$COMMAND" | grep -qEi '(echo|printf)\s+.*\$\{?(TWITTER|API_KEY|API_SECRET|ACCESS_TOKEN|ACCESS_SECRET|GITHUB_PAT|LINEAR_API|SUPABASE_SERVICE|NOTION_TOKEN|OPENROUTER|PERPLEXITY|ANTHROPIC)'; then
82
+ cat <<'HOOKJSON'
83
+ {
84
+ "decision": "block",
85
+ "reason": "RED LINE — CREDENTIAL EXPOSURE BLOCKED: This command would print a secret environment variable value. Use existence checks instead:\n\n [ -n \"$VARNAME\" ] && echo \"set\" || echo \"unset\"\n\nThis rule is constitutional. There is no override."
86
+ }
87
+ HOOKJSON
88
+ exit 0
89
+ fi
90
+
91
+ # Guard 3: cat/head/tail/less of credential files via Bash
92
+ if echo "$COMMAND" | grep -qEi "(cat|head|tail|less|more|bat)\s+" && \
93
+ echo "$COMMAND" | grep -qEi "$CREDENTIAL_FILES_PATTERN"; then
94
+ cat <<'HOOKJSON'
95
+ {
96
+ "decision": "block",
97
+ "reason": "RED LINE — CREDENTIAL EXPOSURE BLOCKED: This command would dump a credential file into the conversation. Use targeted, value-safe searches instead:\n\n grep -c '\"server_name\"' config.json # check structure, not secrets\n\nThis rule is constitutional. There is no override."
98
+ }
99
+ HOOKJSON
100
+ exit 0
101
+ fi
102
+ fi
103
+
104
+ # ============================================================================
105
+ # READ TOOL GUARD
106
+ # ============================================================================
107
+
108
+ if [ "$TOOL_NAME" = "Read" ]; then
109
+ FILE_PATH=$(parse_json "$HOOK_INPUT" ".tool_input.file_path")
110
+ BASENAME=$(basename "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
111
+
112
+ # Block reading known credential files
113
+ if echo "$BASENAME" | grep -qEi "$CREDENTIAL_FILES_PATTERN"; then
114
+ cat <<HOOKJSON
115
+ {
116
+ "decision": "block",
117
+ "reason": "RED LINE — CREDENTIAL EXPOSURE BLOCKED: Reading '${BASENAME}' would dump secrets (API keys, tokens) into the conversation. This file is a credential store.\n\nSafe alternatives:\n grep -c '\"server_name\"' ${FILE_PATH} # check if a key exists\n grep '\"twitter\"' ${FILE_PATH} # check specific non-secret structure\n\nThis rule is constitutional. There is no override."
118
+ }
119
+ HOOKJSON
120
+ exit 0
121
+ fi
122
+
123
+ # Block reading files in sensitive directories
124
+ if echo "$FILE_PATH" | grep -qEi '(/\.ssh/|/\.gnupg/|/secrets?/)'; then
125
+ cat <<HOOKJSON
126
+ {
127
+ "decision": "block",
128
+ "reason": "RED LINE — CREDENTIAL EXPOSURE BLOCKED: Reading files from '${FILE_PATH}' would expose sensitive cryptographic material or secrets. This rule is constitutional. There is no override."
129
+ }
130
+ HOOKJSON
131
+ exit 0
132
+ fi
133
+ fi
134
+
135
+ # ============================================================================
136
+ # No credential risk detected — allow
137
+ # ============================================================================
138
+
139
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmem-mcp",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Institutional memory for AI coding agents. Memory that compounds.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,7 +24,8 @@
24
24
  "test:all": "npm run test:unit && npm run test:smoke && npm run test:integration && npm run test:perf && npm run test:e2e",
25
25
  "test:watch": "vitest",
26
26
  "typecheck": "tsc --noEmit",
27
- "prepublishOnly": "tsc"
27
+ "prepublishOnly": "tsc",
28
+ "release-status": "bash scripts/release-status.sh"
28
29
  },
29
30
  "dependencies": {
30
31
  "@huggingface/transformers": "^3.0.0",