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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +12 -0
- package/dist/adapters/claudeCode.js +20 -4
- package/dist/cli.js +14 -0
- package/dist/commands/load.js +8 -0
- package/dist/commands/status.js +2 -1
- package/dist/commands/update.js +6 -3
- package/dist/core/detectedBlock.js +58 -0
- package/dist/core/factSync.js +60 -0
- package/dist/core/infraSignals.js +75 -0
- package/dist/core/knownFacts.js +53 -0
- package/package.json +1 -1
- package/skills/memoryintel/SKILL.md +3 -0
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Persistent, cross-session project memory for AI coding agents.",
|
|
9
|
-
"version": "1.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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)
|
package/dist/commands/load.js
CHANGED
|
@@ -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));
|
package/dist/commands/status.js
CHANGED
|
@@ -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
|
-
|
|
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';
|
package/dist/commands/update.js
CHANGED
|
@@ -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
|
@@ -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;
|