@ijfw/memory-server 1.6.2 → 1.6.3

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": "@ijfw/memory-server",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "Cross-platform persistent memory server for IJFW. 14 MCP tools (memory + admin/update + brain). Works with 15 platforms: 14 via MCP (Claude Code, Codex, Gemini CLI, Cursor, Windsurf, Copilot, Hermes, Wayland, OpenCode, QwenCode, Cline, KimiCode, OpenClaw, Antigravity) plus Aider via the rules-only tier.",
5
5
  "author": "Sean Donahoe",
6
6
  "contributors": [
@@ -21,6 +21,7 @@ import {
21
21
  } from 'node:fs';
22
22
  import { join, dirname } from 'node:path';
23
23
  import { resolveBrainPaths } from './paths.js';
24
+ import { shouldSeedProject } from './seed-gate.js';
24
25
  import { scanInbox, writeManifest, commitProcessed, isProcessed } from './dump-ingest.js';
25
26
  import { extractFile } from './extractors/index.js';
26
27
  import { BudgetGuard } from './budget-guard.js';
@@ -217,9 +218,24 @@ function isProcessedDouble(db, processedDir, fileName) {
217
218
  export async function runDreamCycle({ db, repoRoot, env = process.env, cycleId, extractFacts } = {}) {
218
219
  if (!db) throw new Error('dream-pipeline: db required');
219
220
  if (!repoRoot) throw new Error('dream-pipeline: repoRoot required');
221
+ // Seed gate: the dream cycle materializes the VISIBLE `ijfw/` layer
222
+ // (dump/inbox, dump/processed, wiki/...). Do not create it in a directory
223
+ // that is not a real project -- a throwaway scratch dir or an ephemeral
224
+ // "temporary space" (Wayland) running a one-shot chat should stay clean.
225
+ // A project marker (.git, a manifest) or an explicit `ijfw init` re-enables
226
+ // it. Memory recall still works in-session; only the on-disk content layer
227
+ // is withheld. Honest no-op return so callers/receipts see why nothing ran.
228
+ const cid0 = cycleId || `cycle-${Date.now()}`;
229
+ if (!shouldSeedProject(repoRoot)) {
230
+ return {
231
+ processed: 0, pagesCompiled: 0, factsInserted: 0,
232
+ budgetExhausted: false, cycleId: cid0, errors: [],
233
+ skipped: 'no-project-marker',
234
+ };
235
+ }
220
236
  ensureFactsTable(db);
221
237
  const paths = resolveBrainPaths(repoRoot);
222
- const cid = cycleId || `cycle-${Date.now()}`;
238
+ const cid = cid0;
223
239
  // Parse budget caps from env explicitly so zero is respected (Number('0')||default
224
240
  // would silently fall back to the default; we need the caller's $0 to mean $0).
225
241
  const cycleUsdRaw = env.IJFW_DREAM_BUDGET_USD != null ? Number(env.IJFW_DREAM_BUDGET_USD) : undefined;
@@ -0,0 +1,108 @@
1
+ // IJFW seed gate -- "should we materialize on-disk content in this directory?"
2
+ //
3
+ // Single rule, shared across the whole product: IJFW only writes project
4
+ // artifacts (the visible `ijfw/` brain layer, AGENTS.md, CLAUDE.md, the
5
+ // codebase index, the `.ijfw/project.type` cold scan) into a directory that is
6
+ // actually a project. "A project" means it carries a recognized marker (a VCS
7
+ // dir, a language manifest) OR the operator explicitly blessed it with
8
+ // `ijfw init` (which drops `.ijfw/project`).
9
+ //
10
+ // Why this exists: session-start hooks fire on EVERY new chat, including
11
+ // throwaway scratch dirs and ephemeral "temporary spaces" (e.g. Wayland). The
12
+ // old behavior seeded those unconditionally, so a one-shot `print(7*6)` chat
13
+ // littered `ijfw/`, AGENTS.md, and CLAUDE.md into a dir the user never meant to
14
+ // keep. Memory recall still works in-session for those dirs -- we just don't
15
+ // write anything to disk until the dir proves it's a real project.
16
+ //
17
+ // This is the JS mirror of the bash `ijfw_should_seed` (seed-gate.sh) and the
18
+ // indexer's `ijfw_has_project_marker` (scripts/build-codebase-index.sh). The
19
+ // three marker lists are kept identical by a drift test
20
+ // (test/brain/test-seed-gate-drift.js). Edit all three together or the test
21
+ // fails.
22
+
23
+ import { existsSync, realpathSync } from 'node:fs';
24
+ import { join, dirname, parse as parsePath, sep } from 'node:path';
25
+ import { homedir } from 'node:os';
26
+
27
+ // Canonical project-marker list. MUST stay byte-identical (same set) to the
28
+ // bash list in seed-gate.sh and the indexer's ijfw_has_project_marker. The
29
+ // `.ijfw/project` entry is the `ijfw init` override.
30
+ export const PROJECT_MARKERS = Object.freeze([
31
+ '.git',
32
+ 'package.json',
33
+ 'go.mod',
34
+ 'Cargo.toml',
35
+ 'pyproject.toml',
36
+ 'setup.py',
37
+ 'tsconfig.json',
38
+ 'pom.xml',
39
+ 'build.gradle',
40
+ 'build.gradle.kts',
41
+ 'Gemfile',
42
+ 'composer.json',
43
+ 'deno.json',
44
+ 'deno.jsonc',
45
+ 'mix.exs',
46
+ 'Package.swift',
47
+ 'requirements.txt',
48
+ '.hg',
49
+ '.svn',
50
+ '.ijfw/project',
51
+ ]);
52
+
53
+ // True when `dir` carries any recognized project marker. Pure existence checks;
54
+ // never throws.
55
+ export function hasProjectMarker(dir) {
56
+ if (!dir || typeof dir !== 'string') return false;
57
+ for (const m of PROJECT_MARKERS) {
58
+ try {
59
+ // `.ijfw/project` is a nested path; join handles both flat and nested.
60
+ if (existsSync(join(dir, ...m.split('/')))) return true;
61
+ } catch {
62
+ // Unreadable candidate -- treat as absent, keep scanning.
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+
68
+ // Resolve the physical path of a directory, falling back to the input on error
69
+ // so callers always get a usable string.
70
+ function physOf(dir) {
71
+ try {
72
+ return realpathSync(dir);
73
+ } catch {
74
+ return dir;
75
+ }
76
+ }
77
+
78
+ // The full seed decision: refuse the filesystem root, the home directory, and
79
+ // any ancestor of home (the issue #16 privacy hole -- seeding /Users or /home
80
+ // would walk every user's home), THEN require a project marker.
81
+ //
82
+ // Returns true only when `dir` is a real, safe project directory to write into.
83
+ export function shouldSeedProject(dir) {
84
+ if (!dir || typeof dir !== 'string') return false;
85
+ const phys = physOf(dir);
86
+ if (!phys || phys === sep) return false;
87
+
88
+ // A filesystem/drive root is its own parent (covers POSIX '/' and Windows
89
+ // drive roots like 'C:\\').
90
+ const root = parsePath(phys).root;
91
+ if (phys === root) return false;
92
+ if (dirname(phys) === phys) return false;
93
+
94
+ // Refuse the home directory and any ancestor of it. Fail closed when home is
95
+ // unresolvable -- we cannot prove the target isn't home, so do not seed.
96
+ let homePhys;
97
+ try {
98
+ homePhys = realpathSync(homedir());
99
+ } catch {
100
+ homePhys = homedir();
101
+ }
102
+ if (!homePhys) return false;
103
+ if (phys === homePhys) return false;
104
+ // phys is an ancestor of home when home lives under phys/.
105
+ if ((homePhys + sep).startsWith(phys + sep)) return false;
106
+
107
+ return hasProjectMarker(phys);
108
+ }