memoryintel 1.1.4 → 1.2.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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Persistent, cross-session project memory for AI coding agents.",
9
- "version": "1.1.4"
9
+ "version": "1.2.0"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "memoryintel",
14
14
  "source": "./",
15
15
  "description": "Persistent project memory for AI coding agents — initialize once, then agents automatically load and update project understanding across sessions.",
16
- "version": "1.1.4"
16
+ "version": "1.2.0"
17
17
  }
18
18
  ]
19
19
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "memoryintel",
3
3
  "description": "Persistent project memory for AI coding agents — initialize once, then agents automatically load and update project understanding across sessions.",
4
- "version": "1.1.4",
4
+ "version": "1.2.0",
5
5
  "author": {
6
6
  "name": "Adeesh Sharma",
7
7
  "url": "https://github.com/adeeshsharma"
package/README.md CHANGED
@@ -122,6 +122,10 @@ If a shared local dashboard is running (a read-only view of every initialized pr
122
122
  machine), turn it off any time with `memoryintel dashboard disable` — or back on with
123
123
  `memoryintel dashboard enable`.
124
124
 
125
+ `memoryintel sync` re-runs automatic stack/integration/deployment fact detection by hand (see
126
+ "How it works" below) — useful for debugging, or to see what it found without waiting for the
127
+ next `load`/`check-stop`.
128
+
125
129
  ## Prerequisites
126
130
 
127
131
  Node.js ≥18 — actually verified as the real floor (CI runs the full suite on Node 18), not an
@@ -139,6 +143,14 @@ an update-plan and calls `update()`, which validates it, writes atomically under
139
143
  and logs the change — never a changelog, always a maintained understanding of the project as it
140
144
  currently is.
141
145
 
146
+ A second, independent mechanism runs alongside the agent-driven one above: every `load` and
147
+ `check-stop` call also auto-detects mechanically-verifiable stack, integration, and deployment
148
+ facts (a dependency in `package.json`, a `vercel.json`, a `Dockerfile`) and writes them straight
149
+ into `technical/techContext.md` / `integrations.md` / `infrastructure.md` — no agent judgment
150
+ involved, no user action required. This is deliberately narrower than the agent-driven mechanism:
151
+ it only ever asserts what it can prove from a file that's actually there. `memoryintel sync` runs
152
+ the same detection by hand, for debugging.
153
+
142
154
  Full design docs live in `docs/superpowers/specs/`; a diagram-heavy architecture reference lives
143
155
  in `docs/architecture/memory-intel-architecture.html`. See `.memoryintel/context/decisions.md` in
144
156
  this very repository for the specific design decisions behind the mechanism above, with rationale.
@@ -1,6 +1,12 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
3
  import { runGitStatusPorcelain, porcelainPath, runGitRevParseHead } from '../core/gitPorcelain.js';
4
+ import { syncDetectedFacts } from '../core/factSync.js';
5
+ // Same manifest filenames detectStack() (core/repoScan.ts) already recognizes - kept as a local
6
+ // copy rather than importing repoScan's own internal list, since this is a small, stable set and
7
+ // a shared-constant module for five string literals would be more machinery than the duplication
8
+ // it avoids.
9
+ const MANIFEST_FILES = new Set(['package.json', 'requirements.txt', 'pyproject.toml', 'go.mod', 'Cargo.toml']);
4
10
  function readMarker(markerPath) {
5
11
  if (!existsSync(markerPath))
6
12
  return { lastFlaggedDiffSignature: null };
@@ -79,10 +85,20 @@ export function runCheckStop(memoryRoot) {
79
85
  return {};
80
86
  }
81
87
  writeMarker(markerPath, { lastFlaggedDiffSignature: signature });
82
- return {
83
- decision: 'block',
84
- reason: "Working tree has changes memory hasn't accounted for. Classify them, write a TOON update-plan, and run `memoryintel update <plan-file>` (see .memoryintel/instructions.md) before finishing - running `memoryintel update` bare, with no plan file, fails. Or finish again to proceed without updating this time."
85
- };
88
+ let reason = "Working tree has changes memory hasn't accounted for. Classify them, write a TOON update-plan, and run `memoryintel update <plan-file>` (see .memoryintel/instructions.md) before finishing - running `memoryintel update` bare, with no plan file, fails. Or finish again to proceed without updating this time.";
89
+ // Mechanism 2 (see docs/superpowers/specs/2026-09-06-automatic-fact-detection-design.md): when
90
+ // the diff touches a manifest file, name specifically what auto-detection just found, so the
91
+ // agent's own follow-up judgment gets a concrete lead instead of only "something changed, go
92
+ // figure out what." This never changes the block/allow decision itself - only the reason text.
93
+ const statusLines = runGitStatusPorcelain(projectRoot);
94
+ const touchesManifest = statusLines !== null && statusLines.some((l) => MANIFEST_FILES.has(porcelainPath(l)));
95
+ if (touchesManifest) {
96
+ const syncResult = syncDetectedFacts(memoryRoot);
97
+ if (syncResult.written.length > 0) {
98
+ reason += ` Also: a manifest file changed — auto-detected facts were just written to ${syncResult.written.join(', ')}. Worth a note elsewhere (e.g. why it was added) if that's not just incidental.`;
99
+ }
100
+ }
101
+ return { decision: 'block', reason };
86
102
  }
87
103
  // Called after a successful `update`. Does NOT simply clear the marker to null — `update` only
88
104
  // writes to .memoryintel/, so the user's actual source diff that triggered the nudge (e.g. an
package/dist/cli.js CHANGED
@@ -13,6 +13,7 @@ import { runCheckStop } from './adapters/claudeCode.js';
13
13
  import { runDashboardEnable, runDashboardDisable } from './commands/dashboardToggle.js';
14
14
  import { runDaemonStart } from './commands/daemonStart.js';
15
15
  import { runDoctor } from './commands/doctor.js';
16
+ import { syncDetectedFacts } from './core/factSync.js';
16
17
  // Every command below that resolves .memoryintel/ by walking up from a starting directory
17
18
  // previously always used process.cwd() with no override - meaning a long, multi-project
18
19
  // session had to religiously `cd` before every single call, with silently-wrong output the
@@ -45,6 +46,9 @@ Commands:
45
46
  update <plan.toon|-> Apply an update-plan (file path, or - for stdin)
46
47
  status Print a human-readable summary of current memory state
47
48
  check-stop Stop-hook check: emit a JSON allow/block decision
49
+ sync Re-scan for stack/integration/deployment facts and write any new
50
+ findings - runs automatically via load/check-stop; this is the
51
+ manual/debugging entry point
48
52
  dashboard <enable|disable> Turn the shared local dashboard on or off
49
53
  doctor [--force] Refresh memoryintel's own generated files (instructions.md, pointer
50
54
  blocks) to the current template wherever it's provably safe;
@@ -88,6 +92,16 @@ export function dispatch(argv) {
88
92
  const result = runCheckStop(root);
89
93
  return { exitCode: 0, stdout: JSON.stringify(result) + '\n', stderr: '' };
90
94
  }
95
+ case 'sync': {
96
+ const root = findMemoryIntelRoot(resolveStartDir(argv));
97
+ if (!root)
98
+ return { exitCode: 1, stdout: '', stderr: 'No .memoryintel/ found.\n' };
99
+ const result = syncDetectedFacts(root);
100
+ const stdout = result.written.length > 0
101
+ ? `root: ${root}\nUpdated: ${result.written.join(', ')}\n`
102
+ : `root: ${root}\nNo changes - detected facts already up to date.\n`;
103
+ return { exitCode: 0, stdout, stderr: '' };
104
+ }
91
105
  case 'doctor': {
92
106
  const root = findMemoryIntelRoot(resolveStartDir(argv));
93
107
  if (!root)
@@ -8,6 +8,7 @@ import { ensureDaemonRunning } from '../daemon/lifecycle.js';
8
8
  import { upsertRegistryEntry } from '../daemon/registry.js';
9
9
  import { appendEvent } from '../core/eventLog.js';
10
10
  import { readIndex } from '../core/memoryIndex.js';
11
+ import { syncDetectedFacts } from '../core/factSync.js';
11
12
  const ALWAYS_LOAD = ['context/currentMentalModel.md', 'context/activeContext.md'];
12
13
  const DOMAIN_FILES = {
13
14
  technical: ['technical/architecture.md', 'technical/techContext.md', 'technical/patterns.md'],
@@ -75,6 +76,13 @@ export function runLoad(cwd, domain) {
75
76
  const root = findMemoryIntelRoot(cwd);
76
77
  if (!root)
77
78
  return '';
79
+ try {
80
+ syncDetectedFacts(root);
81
+ }
82
+ catch {
83
+ // Best-effort, same policy as the daemon-registry call just below - auto-detection must
84
+ // never be the reason a session-start load fails.
85
+ }
78
86
  try {
79
87
  ensureDaemonRunning();
80
88
  upsertRegistryEntry(dirname(root));
@@ -21,7 +21,8 @@ export function runStatus(root) {
21
21
  const eventLines = readFileSync(eventsPath, 'utf-8').trim().split('\n').filter(Boolean);
22
22
  for (const line of eventLines.slice(-5)) {
23
23
  const event = JSON.parse(line);
24
- lines.push(`[${event.timestamp}] ${event.type}: ${event.summary}`);
24
+ const sourceTag = event.source ? ` (${event.source})` : '';
25
+ lines.push(`[${event.timestamp}] ${event.type}${sourceTag}: ${event.summary}`);
25
26
  }
26
27
  }
27
28
  return lines.join('\n') + '\n';
@@ -93,7 +93,8 @@ export async function runUpdate(root, planText) {
93
93
  timestamp: new Date().toISOString(),
94
94
  type: w.eventType,
95
95
  summary: w.reason,
96
- affectedFiles: [w.relFile]
96
+ affectedFiles: [w.relFile],
97
+ source: 'agent'
97
98
  });
98
99
  skipped.push(w.relFile);
99
100
  continue;
@@ -104,7 +105,8 @@ export async function runUpdate(root, planText) {
104
105
  timestamp: new Date().toISOString(),
105
106
  type: w.eventType,
106
107
  summary: w.reason,
107
- affectedFiles: [w.relFile]
108
+ affectedFiles: [w.relFile],
109
+ source: 'agent'
108
110
  });
109
111
  applied.push(w.relFile);
110
112
  // The compression ceiling used to be purely advisory: `load()` would flag a file as
@@ -119,7 +121,8 @@ export async function runUpdate(root, planText) {
119
121
  timestamp: new Date().toISOString(),
120
122
  type: 'over-ceiling',
121
123
  summary: reason,
122
- affectedFiles: [w.relFile]
124
+ affectedFiles: [w.relFile],
125
+ source: 'agent'
123
126
  });
124
127
  overCeiling.push(w.relFile);
125
128
  }
@@ -0,0 +1,58 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { atomicWriteFile } from './atomicWrite.js';
3
+ import { getSectionContent, applySectionUpdate } from './sectionWriter.js';
4
+ export const DETECTED_START = '<!-- memoryintel:detected:start -->';
5
+ export const DETECTED_END = '<!-- memoryintel:detected:end -->';
6
+ function buildBlock(lines) {
7
+ return [DETECTED_START, ...lines, DETECTED_END].join('\n');
8
+ }
9
+ // Inserts or refreshes a memoryintel-owned "detected facts" block at the top of `heading`'s
10
+ // section content in the markdown file at absPath - modeled on the existing pointer-block
11
+ // mechanism in adapters/genericPointer.ts, generalized to work on an arbitrary heading inside an
12
+ // arbitrary file instead of one hardcoded block in one hardcoded set of files.
13
+ //
14
+ // Never creates the file (a directory `init` hasn't touched is left alone - 'missing-file') and
15
+ // never creates the heading either (`init`'s STARTER_FILES already ship every heading this is
16
+ // ever pointed at; a project on an old template without it is simply skipped - 'missing-heading'
17
+ // - rather than this feature mutating a file's structure it was never asked to manage).
18
+ //
19
+ // Content outside the markers - including agent-authored prose elsewhere in the very same
20
+ // section, above or below the block - is preserved byte-for-byte. This is what makes it safe to
21
+ // call this on every session start without ever risking real project understanding: the block is
22
+ // fully machine-owned, and everything else in the file categorically is not.
23
+ export function upsertDetectedBlock(absPath, heading, lines) {
24
+ if (!existsSync(absPath))
25
+ return 'missing-file';
26
+ const markdown = readFileSync(absPath, 'utf-8');
27
+ const sectionContent = getSectionContent(markdown, heading);
28
+ if (sectionContent === null)
29
+ return 'missing-heading';
30
+ const newBlock = buildBlock(lines);
31
+ const startIdx = sectionContent.indexOf(DETECTED_START);
32
+ const endIdx = sectionContent.indexOf(DETECTED_END);
33
+ let newSectionContent;
34
+ if (startIdx !== -1 && endIdx !== -1) {
35
+ const before = sectionContent.slice(0, startIdx);
36
+ const after = sectionContent.slice(endIdx + DETECTED_END.length);
37
+ newSectionContent = `${before}${newBlock}${after}`;
38
+ }
39
+ else {
40
+ // Fresh insertion (no existing block to slot into): `.trim()` here would swallow the blank
41
+ // line this project's own files (and getSectionContent's own single-blank-line stripping
42
+ // convention) always separate a heading, and every section, by - caught via dogfooding this
43
+ // feature on this very repo, where a fresh Stack block landed flush against `## Conventions`
44
+ // with zero blank line between them. The leading/trailing `\n` below combine with
45
+ // applySectionUpdate's own join('\n') separators (one on each side of this section's content)
46
+ // to reconstruct exactly one blank line after the heading and exactly one before whatever
47
+ // comes next (another heading, or end of file) - matching the surrounding blank-line rhythm
48
+ // instead of fighting it.
49
+ const rest = sectionContent.trim();
50
+ const body = rest.length > 0 ? `${newBlock}\n\n${rest}` : newBlock;
51
+ newSectionContent = `\n${body}\n`;
52
+ }
53
+ if (newSectionContent === sectionContent)
54
+ return 'unchanged';
55
+ const updated = applySectionUpdate(markdown, heading, 'replace', newSectionContent);
56
+ atomicWriteFile(absPath, updated);
57
+ return 'written';
58
+ }
@@ -0,0 +1,60 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { detectStack } from './repoScan.js';
4
+ import { matchKnownFacts } from './knownFacts.js';
5
+ import { detectInfraSignals } from './infraSignals.js';
6
+ import { upsertDetectedBlock } from './detectedBlock.js';
7
+ import { upsertIndexEntry } from './memoryIndex.js';
8
+ import { appendEvent } from './eventLog.js';
9
+ import { withLockSync } from './lock.js';
10
+ const TARGETS = [
11
+ { relFile: 'technical/techContext.md', heading: 'Stack' },
12
+ { relFile: 'technical/integrations.md', heading: 'External Services' },
13
+ { relFile: 'technical/infrastructure.md', heading: 'Deployment' }
14
+ ];
15
+ // Automatically detects mechanically-verifiable stack/integration/deployment facts and writes
16
+ // them into their managed detected-block (detectedBlock.ts), with zero agent or user action
17
+ // required - called from both `load` (every session start) and `check-stop` (when the diff
18
+ // touches a manifest file), plus manually via `memoryintel sync`. A no-op (missing-file, no
19
+ // write, no event) on any target file `init` hasn't created - this feature is inert on an
20
+ // uninitialized or partially-initialized project, never scaffolding structure of its own.
21
+ export function syncDetectedFacts(memoryRoot) {
22
+ const projectRoot = dirname(memoryRoot);
23
+ const stack = detectStack(projectRoot);
24
+ const matched = matchKnownFacts(stack.dependencies);
25
+ const infraLines = detectInfraSignals(projectRoot);
26
+ const contentByTarget = {
27
+ 'technical/techContext.md': matched.techContext.length > 0 ? matched.techContext.map((f) => `- ${f}`) : ['_No stack manifest found._'],
28
+ 'technical/integrations.md': matched.integrations.length > 0
29
+ ? matched.integrations.map((f) => `- ${f}`)
30
+ : ['_No known integrations detected._'],
31
+ 'technical/infrastructure.md': infraLines.length > 0 ? infraLines.map((f) => `- ${f}`) : ['_No deployment signal found in repo._']
32
+ };
33
+ const written = [];
34
+ for (const { relFile, heading } of TARGETS) {
35
+ const absPath = join(memoryRoot, relFile);
36
+ // Checked before locking, not just left to upsertDetectedBlock's own 'missing-file' return:
37
+ // withLockSync's openSync(..., O_CREAT) needs the file's parent directory to already exist to
38
+ // create the `.lock` file in - an uninitialized project (no .memoryintel/technical/ at all)
39
+ // would otherwise throw ENOENT here instead of the intended silent no-op.
40
+ if (!existsSync(absPath))
41
+ continue;
42
+ const lockPath = `${absPath}.lock`;
43
+ // Same lock-file naming as update()'s per-file locking (`${absPath}.lock`) - a concurrent
44
+ // agent-driven update() and this auto-sync touching the same file correctly serialize
45
+ // against each other instead of racing.
46
+ const result = withLockSync(lockPath, () => upsertDetectedBlock(absPath, heading, contentByTarget[relFile]));
47
+ if (result === 'written') {
48
+ upsertIndexEntry(join(memoryRoot, 'memory-index.json'), relFile, 'Auto-detected stack/integration facts (memoryintel sync)');
49
+ appendEvent(join(memoryRoot, 'memory-events.jsonl'), {
50
+ timestamp: new Date().toISOString(),
51
+ type: 'fact-sync',
52
+ summary: `Auto-detected facts written to ${relFile}`,
53
+ affectedFiles: [relFile],
54
+ source: 'auto-detect'
55
+ });
56
+ written.push(relFile);
57
+ }
58
+ }
59
+ return { written };
60
+ }
@@ -0,0 +1,75 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ // Presence-only, no version/config parsing. Vercel gets two independent signals
4
+ // (vercel.json and .vercel/) since either alone is common depending on whether the project was
5
+ // ever linked locally via `vercel link` vs. only ever deployed through the dashboard/CI.
6
+ const FILE_SIGNALS = [
7
+ { relPath: 'vercel.json', fact: 'Vercel' },
8
+ { relPath: '.vercel', fact: 'Vercel' },
9
+ { relPath: 'netlify.toml', fact: 'Netlify' },
10
+ { relPath: 'Dockerfile', fact: 'Docker' },
11
+ { relPath: 'docker-compose.yml', fact: 'Docker Compose' },
12
+ { relPath: 'fly.toml', fact: 'Fly.io' },
13
+ { relPath: 'render.yaml', fact: 'Render' },
14
+ { relPath: 'wrangler.toml', fact: 'Cloudflare Workers (via Wrangler)' },
15
+ { relPath: 'Procfile', fact: 'Heroku (or Heroku-compatible, via Procfile)' }
16
+ ];
17
+ // Known deploy-action name fragments to grep for inside .github/workflows/*.yml - deliberately
18
+ // substring matches (not parsing the `uses:` field structurally), since a real YAML parser is
19
+ // more machinery than a best-effort signal needs and every real deploy-action reference contains
20
+ // one of these fragments regardless of version pin or org fork.
21
+ const WORKFLOW_DEPLOY_ACTIONS = [
22
+ { fragment: 'vercel-action', fact: 'Vercel' },
23
+ { fragment: 'amondnet/vercel', fact: 'Vercel' },
24
+ { fragment: 'netlify/actions', fact: 'Netlify' },
25
+ { fragment: 'nwtgck/actions-netlify', fact: 'Netlify' },
26
+ { fragment: 'superfly/flyctl-actions', fact: 'Fly.io' },
27
+ { fragment: 'aws-actions/', fact: 'AWS (via GitHub Actions)' },
28
+ { fragment: 'google-github-actions/', fact: 'Google Cloud (via GitHub Actions)' },
29
+ { fragment: 'azure/webapps-deploy', fact: 'Azure' }
30
+ ];
31
+ function detectWorkflowSignals(targetDir) {
32
+ const workflowsDir = join(targetDir, '.github', 'workflows');
33
+ if (!existsSync(workflowsDir))
34
+ return [];
35
+ let files;
36
+ try {
37
+ files = readdirSync(workflowsDir).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
38
+ }
39
+ catch {
40
+ return [];
41
+ }
42
+ const found = new Set();
43
+ for (const file of files) {
44
+ let content;
45
+ try {
46
+ content = readFileSync(join(workflowsDir, file), 'utf-8');
47
+ }
48
+ catch {
49
+ continue;
50
+ }
51
+ for (const { fragment, fact } of WORKFLOW_DEPLOY_ACTIONS) {
52
+ if (content.includes(fragment)) {
53
+ found.add(`${fact} — via \`${file}\` GitHub Actions workflow`);
54
+ }
55
+ }
56
+ }
57
+ return [...found];
58
+ }
59
+ // Best-effort only, by design: no dependency-based signal exists for "where is this deployed"
60
+ // (a project deployed via a hosting platform's dashboard, connected straight to its git remote,
61
+ // can leave zero trace in the repo itself - confirmed on a real project during this feature's own
62
+ // design). Returns an empty array when nothing is found; the caller decides how to render that
63
+ // as an explicit "no signal" statement rather than silence (see factSync.ts).
64
+ export function detectInfraSignals(targetDir) {
65
+ const found = new Set();
66
+ for (const { relPath, fact } of FILE_SIGNALS) {
67
+ if (existsSync(join(targetDir, relPath))) {
68
+ found.add(`${fact} — via \`${relPath}\``);
69
+ }
70
+ }
71
+ for (const line of detectWorkflowSignals(targetDir)) {
72
+ found.add(line);
73
+ }
74
+ return [...found];
75
+ }
@@ -0,0 +1,53 @@
1
+ // A curated, deliberately small starter set covering common frameworks, CMS, DB, payment, and
2
+ // auth libraries. Extending this is a one-line PR to this file - no plugin system needed at this
3
+ // scale (see spec's "Non-goals").
4
+ export const KNOWN_FACTS = [
5
+ { match: 'next', fact: 'Next.js (React framework)', target: 'techContext' },
6
+ { match: 'react', fact: 'React', target: 'techContext' },
7
+ { match: 'vue', fact: 'Vue', target: 'techContext' },
8
+ { match: 'svelte', fact: 'Svelte', target: 'techContext' },
9
+ { match: 'typescript', fact: 'TypeScript', target: 'techContext' },
10
+ { match: 'express', fact: 'Express', target: 'techContext' },
11
+ { match: 'fastify', fact: 'Fastify', target: 'techContext' },
12
+ { match: 'prisma', fact: 'Prisma (ORM)', target: 'techContext' },
13
+ { match: 'mongoose', fact: 'MongoDB (via Mongoose)', target: 'techContext' },
14
+ { match: 'tailwindcss', fact: 'Tailwind CSS', target: 'techContext' },
15
+ { match: ['@sanity/client', 'sanity'], fact: 'Sanity (headless CMS)', target: 'integrations' },
16
+ { match: 'contentful', fact: 'Contentful (headless CMS)', target: 'integrations' },
17
+ { match: 'stripe', fact: 'Stripe (payments)', target: 'integrations' },
18
+ { match: '@supabase/supabase-js', fact: 'Supabase', target: 'integrations' },
19
+ { match: 'firebase', fact: 'Firebase', target: 'integrations' },
20
+ { match: 'firebase-admin', fact: 'Firebase Admin SDK', target: 'integrations' },
21
+ { match: 'next-auth', fact: 'NextAuth.js (authentication)', target: 'integrations' },
22
+ { match: '@auth0/nextjs-auth0', fact: 'Auth0 (authentication)', target: 'integrations' },
23
+ { match: '@clerk/nextjs', fact: 'Clerk (authentication)', target: 'integrations' },
24
+ { match: '@sendgrid/mail', fact: 'SendGrid (email)', target: 'integrations' },
25
+ { match: 'resend', fact: 'Resend (email)', target: 'integrations' },
26
+ { match: 'twilio', fact: 'Twilio', target: 'integrations' },
27
+ { match: '@aws-sdk/client-s3', fact: 'AWS S3', target: 'integrations' },
28
+ { match: 'openai', fact: 'OpenAI API', target: 'integrations' },
29
+ { match: '@anthropic-ai/sdk', fact: 'Anthropic API', target: 'integrations' },
30
+ { match: '@sentry/node', fact: 'Sentry (error tracking)', target: 'integrations' },
31
+ { match: '@sentry/nextjs', fact: 'Sentry (error tracking)', target: 'integrations' },
32
+ { match: 'posthog-js', fact: 'PostHog (analytics)', target: 'integrations' },
33
+ { match: '@vercel/analytics', fact: 'Vercel Analytics', target: 'integrations' },
34
+ { match: 'graphql', fact: 'GraphQL', target: 'techContext' },
35
+ { match: 'redis', fact: 'Redis', target: 'techContext' },
36
+ { match: 'pg', fact: 'PostgreSQL (via pg)', target: 'techContext' }
37
+ ];
38
+ // Each returned fact line is self-documenting about its evidence (which exact dependency name
39
+ // triggered it) - a reader should never have to take the tool's word for a fact it can't trace
40
+ // back to something real in the repo. `${matched}` is deliberately the specific name that was
41
+ // actually present, not `known.match` itself, since that may be an array of aliases.
42
+ export function matchKnownFacts(dependencies) {
43
+ const deps = new Set(dependencies);
44
+ const result = { techContext: [], integrations: [] };
45
+ for (const known of KNOWN_FACTS) {
46
+ const candidates = Array.isArray(known.match) ? known.match : [known.match];
47
+ const matched = candidates.find((c) => deps.has(c));
48
+ if (matched) {
49
+ result[known.target].push(`${known.fact} — via \`${matched}\` in package.json`);
50
+ }
51
+ }
52
+ return result;
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoryintel",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "Persistent, cross-session project memory for AI coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,6 +47,9 @@ Commands:
47
47
  update <plan.toon|-> Apply an update-plan (file path, or - for stdin)
48
48
  status Print a human-readable summary of current memory state
49
49
  check-stop Stop-hook check: emit a JSON allow/block decision
50
+ sync Re-scan for stack/integration/deployment facts and write any new
51
+ findings - runs automatically via load/check-stop; this is the
52
+ manual/debugging entry point
50
53
  dashboard <enable|disable> Turn the shared local dashboard on or off
51
54
  doctor [--force] Refresh memoryintel's own generated files (instructions.md, pointer
52
55
  blocks) to the current template wherever it's provably safe;