memoryintel 1.0.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 +19 -0
- package/.claude-plugin/plugin.json +9 -0
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/adapters/claudeCode.js +96 -0
- package/dist/adapters/genericPointer.js +39 -0
- package/dist/cli.js +157 -0
- package/dist/commands/daemonStart.js +8 -0
- package/dist/commands/dashboardToggle.js +17 -0
- package/dist/commands/init.js +111 -0
- package/dist/commands/load.js +82 -0
- package/dist/commands/status.js +24 -0
- package/dist/commands/update.js +108 -0
- package/dist/core/atomicWrite.js +6 -0
- package/dist/core/compressionConfig.js +37 -0
- package/dist/core/discovery.js +14 -0
- package/dist/core/eventLog.js +4 -0
- package/dist/core/gitPorcelain.js +45 -0
- package/dist/core/headingMatch.js +44 -0
- package/dist/core/lock.js +67 -0
- package/dist/core/memoryIndex.js +19 -0
- package/dist/core/pathSafety.js +43 -0
- package/dist/core/sectionWriter.js +91 -0
- package/dist/core/toon.js +118 -0
- package/dist/daemon/daemonHandle.js +52 -0
- package/dist/daemon/globalPaths.js +15 -0
- package/dist/daemon/health.js +14 -0
- package/dist/daemon/lifecycle.js +54 -0
- package/dist/daemon/registry.js +60 -0
- package/dist/daemon/server.js +92 -0
- package/dist/daemon/settings.js +13 -0
- package/dist/daemon/views/layout.js +233 -0
- package/dist/daemon/views/projectPage.js +111 -0
- package/dist/daemon/views/registryPage.js +54 -0
- package/dist/skill.js +46 -0
- package/dist/templates/starterFiles.js +22 -0
- package/hooks/hooks.json +11 -0
- package/package.json +52 -0
- package/skills/memoryintel/SKILL.md +55 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memoryintel",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Adeesh Sharma",
|
|
5
|
+
"url": "https://github.com/adeeshsharma"
|
|
6
|
+
},
|
|
7
|
+
"metadata": {
|
|
8
|
+
"description": "Persistent, cross-session project memory for AI coding agents.",
|
|
9
|
+
"version": "1.0.0"
|
|
10
|
+
},
|
|
11
|
+
"plugins": [
|
|
12
|
+
{
|
|
13
|
+
"name": "memoryintel",
|
|
14
|
+
"source": "./",
|
|
15
|
+
"description": "Persistent project memory for AI coding agents — initialize once, then agents automatically load and update project understanding across sessions.",
|
|
16
|
+
"version": "1.0.0"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memoryintel",
|
|
3
|
+
"description": "Persistent project memory for AI coding agents — initialize once, then agents automatically load and update project understanding across sessions.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Adeesh Sharma",
|
|
7
|
+
"url": "https://github.com/adeeshsharma"
|
|
8
|
+
}
|
|
9
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adeesh Sharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Memory Intel
|
|
2
|
+
|
|
3
|
+
[](https://github.com/adeeshsharma/memoryintel/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/memoryintel)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Persistent, cross-session project memory for AI coding agents. Set it up once per project; after
|
|
8
|
+
that, agents automatically load and update project understanding — architecture, decisions,
|
|
9
|
+
progress, a running "mental model" — across new chats, new sessions, and even across tools
|
|
10
|
+
(Claude Code, Cursor, Codex, Gemini CLI).
|
|
11
|
+
|
|
12
|
+
This repository is itself running Memory Intel on itself — see `.memoryintel/` for the tool's own
|
|
13
|
+
current state, decisions, and open todo items. Any agent with the skill below active will read it
|
|
14
|
+
automatically.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx skills add adeeshsharma/memoryintel --skill memoryintel # teaches an agent the shape of it
|
|
20
|
+
npm install -g memoryintel # the CLI those instructions call
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then, in any project, ask an agent to "set up persistent memory here" once. From then on, `load`/
|
|
24
|
+
`update` fire automatically via Claude Code's `SessionStart`/`Stop` hooks (see "Point Claude Code
|
|
25
|
+
at the plugin" below) — nothing else to remember. See `.memoryintel/instructions.md` in a given
|
|
26
|
+
project for its own specific guidance once initialized.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
**The CLI** is on the npm registry:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g memoryintel
|
|
34
|
+
# or run it without installing:
|
|
35
|
+
npx memoryintel status
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**The Claude Code plugin** — this is what actually wires up automatic `SessionStart`/`Stop`
|
|
39
|
+
hooks, not just agent-readable instructions. This repo is its own marketplace
|
|
40
|
+
(`.claude-plugin/marketplace.json`), so no separate hosting or git clone is needed:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
claude plugin marketplace add adeeshsharma/memoryintel
|
|
44
|
+
claude plugin install memoryintel@memoryintel
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That's a one-time install — restart Claude Code and the hooks are active for every session from
|
|
48
|
+
then on, in every project. See "Skill vs. plugin" below if it's not obvious why this is a plugin
|
|
49
|
+
and not just a skill.
|
|
50
|
+
|
|
51
|
+
**Just the skill, no automation** — if you want the agent-facing instructions (what `memoryintel
|
|
52
|
+
init`/`load`/`update` are and when to use them) without the automatic hooks, install it standalone:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx skills add adeeshsharma/memoryintel --skill memoryintel
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
<details>
|
|
59
|
+
<summary>Developing/testing this plugin locally, without the marketplace</summary>
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
claude --plugin-dir "$(npm root -g)/memoryintel" # or any local checkout
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`--plugin-dir` is Claude Code's own documented flag for loading a plugin from a specific
|
|
66
|
+
directory for one session, bypassing marketplaces entirely (see
|
|
67
|
+
[Create plugins](https://code.claude.com/docs/en/plugins)). Needs to be passed every launch; a
|
|
68
|
+
shell alias avoids retyping it:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
alias claude-mi='claude --plugin-dir "$(npm root -g)/memoryintel"'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
</details>
|
|
75
|
+
|
|
76
|
+
### Skill vs. plugin
|
|
77
|
+
|
|
78
|
+
A **skill** is just instructions loaded into an agent's context — it teaches an agent *what to
|
|
79
|
+
do*, and the agent decides on its own judgment *whether* to act on it. A **plugin** is a bundle
|
|
80
|
+
that can include a skill *and* hooks — commands Claude Code itself runs automatically at fixed
|
|
81
|
+
lifecycle moments (`SessionStart`, `Stop`), no agent judgment involved. Memory Intel's plugin
|
|
82
|
+
bundles both: the skill (`skills/memoryintel/SKILL.md`) plus `hooks/hooks.json`, which runs
|
|
83
|
+
`memoryintel load` at the start of every session and `memoryintel check-stop` at the end of every
|
|
84
|
+
one — the latter can even block finishing until memory's been updated. The standalone skill
|
|
85
|
+
install above gives an agent the knowledge; only the plugin gives you the automation that doesn't
|
|
86
|
+
depend on the agent noticing anything.
|
|
87
|
+
|
|
88
|
+
### If you don't want to touch Claude Code's plugin system at all
|
|
89
|
+
|
|
90
|
+
The CLI works standalone, with no plugin/skill/hook involved — useful for scripting, for other
|
|
91
|
+
tools, or just to try it out:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
memoryintel init # once per project — scaffolds .memoryintel/, installs pointer files
|
|
95
|
+
# for tools without native hooks (Cursor, Codex, Gemini CLI, opencode)
|
|
96
|
+
memoryintel load # print resolved context to stdout
|
|
97
|
+
memoryintel update plan.toon # apply an update-plan
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`memoryintel init` never touches a project's own `.claude/settings.json` — Claude Code automation
|
|
101
|
+
comes entirely from the plugin's own `hooks/hooks.json` in this repo, active once the plugin
|
|
102
|
+
itself is active. From then on, agents load and update project memory on their own, per that
|
|
103
|
+
project's own `.memoryintel/instructions.md`.
|
|
104
|
+
|
|
105
|
+
If a shared local dashboard is running (a read-only view of every initialized project on this
|
|
106
|
+
machine), turn it off any time with `memoryintel dashboard disable` — or back on with
|
|
107
|
+
`memoryintel dashboard enable`.
|
|
108
|
+
|
|
109
|
+
## Prerequisites
|
|
110
|
+
|
|
111
|
+
Node.js ≥18 — actually verified as the real floor (CI runs the full suite on Node 18), not an
|
|
112
|
+
assumed default.
|
|
113
|
+
|
|
114
|
+
## How it works
|
|
115
|
+
|
|
116
|
+
Full design docs live in `docs/superpowers/specs/`; a diagram-heavy architecture reference lives
|
|
117
|
+
in `docs/architecture/memory-intel-architecture.html`. In short: `.memoryintel/` is a structured,
|
|
118
|
+
git-committed set of markdown/JSON files an agent reads at session start and selectively updates
|
|
119
|
+
when something meaningful changes — never a changelog, always a maintained understanding of the
|
|
120
|
+
project as it currently is. See `.memoryintel/context/decisions.md` in this very repository for
|
|
121
|
+
the specific design decisions behind that, with rationale.
|
|
122
|
+
|
|
123
|
+
## Benchmarks: with vs. without
|
|
124
|
+
|
|
125
|
+
Measured on a real second project ([distilled-docs](https://github.com/adeeshsharma/distilled-docs),
|
|
126
|
+
an 8-phase, single-day build), not a synthetic one. Methodology: real file sizes from that
|
|
127
|
+
project's actual `.memoryintel/` state, tokens estimated at ~4 chars/token (a standard, slightly
|
|
128
|
+
conservative approximation — not measured API telemetry, since neither path logs real token
|
|
129
|
+
counts from the model provider).
|
|
130
|
+
|
|
131
|
+
**Per-session context bootstrap:**
|
|
132
|
+
|
|
133
|
+
| | Chars | Tokens (est.) |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `memoryintel load` (curated: mental model + active context + technical domain) | 9,223 | ~2,300 |
|
|
136
|
+
| No memory, conservative (skim one architecture doc) | 6,126 | ~1,500 |
|
|
137
|
+
| No memory, realistic (doc + git log + a handful of source files) | ~28,000 | ~7,000 |
|
|
138
|
+
| No memory, worst case (re-derive from the full source tree, 49 files) | 113,652 | ~28,400 |
|
|
139
|
+
|
|
140
|
+
That's **67–92% fewer tokens per session bootstrap**, depending on how much of the codebase an
|
|
141
|
+
agent would otherwise need to re-read to reach equivalent situational awareness — and that range
|
|
142
|
+
brackets the same order of magnitude as published numbers from purpose-built memory systems for
|
|
143
|
+
chat agents ([Mem0](https://arxiv.org/pdf/2504.19413): ~90% vs. full context; Letta/MemGPT-class
|
|
144
|
+
systems: 85–93%), despite solving a different problem (durable project state, not conversation
|
|
145
|
+
history).
|
|
146
|
+
|
|
147
|
+
**Why the gap widens over time, not just per-call:** `.memoryintel/` content is self-compressing,
|
|
148
|
+
capped at ~300 lines per file by default — load cost stays roughly flat as a project grows. The
|
|
149
|
+
no-memory alternative doesn't; it scales with total codebase size. A project one day old and one
|
|
150
|
+
a year old cost about the same to bootstrap with Memory Intel. Without it, the older project costs
|
|
151
|
+
more, every single session.
|
|
152
|
+
|
|
153
|
+
**Not just tokens:** `context/decisions.md` and `context/learnings.md` hold things a memory-less
|
|
154
|
+
agent would otherwise silently redo or get wrong twice — a real example from that same build: a
|
|
155
|
+
subtle bundler bug (a literal-string dynamic `import()` statically resolved by esbuild instead of
|
|
156
|
+
treated as a runtime URL) got fixed once and recorded, not re-debugged on the next session that
|
|
157
|
+
touched that code path.
|
|
158
|
+
|
|
159
|
+
These numbers are reproducible for your own project: every `memoryintel load` call logs a
|
|
160
|
+
`session-load` event (domain, files, character/line counts) to `memory-events.jsonl`, visible in
|
|
161
|
+
the dashboard's "Session activity" section or queryable directly from the event log.
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
Working on this repo itself, rather than just using the published package:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
git clone https://github.com/adeeshsharma/memoryintel.git
|
|
169
|
+
cd memoryintel
|
|
170
|
+
npm install
|
|
171
|
+
npm run build # compiles dist/, regenerates skills/memoryintel/SKILL.md from src/skill.ts
|
|
172
|
+
npm link # makes `memoryintel` resolve to this exact checkout instead of the published one
|
|
173
|
+
npm test # 207 tests, vitest
|
|
174
|
+
npm run build:skill:check # fails if skills/memoryintel/SKILL.md has drifted from src/skill.ts
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
CI (`.github/workflows/ci.yml`) runs the full suite on Ubuntu, Windows, and macOS on every
|
|
178
|
+
push/PR to `master`, and is required via branch protection.
|
|
179
|
+
|
|
180
|
+
## Releasing
|
|
181
|
+
|
|
182
|
+
Automated with [release-please](https://github.com/googleapis/release-please) and npm [Trusted
|
|
183
|
+
Publishing](https://docs.npmjs.com/trusted-publishers/) — see [RELEASING.md](RELEASING.md).
|
|
184
|
+
`CHANGELOG.md` is generated automatically starting with the first automated release.
|
|
185
|
+
|
|
186
|
+
## Contributing
|
|
187
|
+
|
|
188
|
+
Issues and pull requests are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { runGitStatusPorcelain, porcelainPath, runGitRevParseHead } from '../core/gitPorcelain.js';
|
|
4
|
+
function readMarker(markerPath) {
|
|
5
|
+
if (!existsSync(markerPath))
|
|
6
|
+
return { lastFlaggedDiffSignature: null };
|
|
7
|
+
try {
|
|
8
|
+
const raw = readFileSync(markerPath, 'utf-8').trim();
|
|
9
|
+
if (raw.length === 0)
|
|
10
|
+
return { lastFlaggedDiffSignature: null };
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
return { lastFlaggedDiffSignature: typeof parsed.lastFlaggedDiffSignature === 'string' ? parsed.lastFlaggedDiffSignature : null };
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return { lastFlaggedDiffSignature: null };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function writeMarker(markerPath, marker) {
|
|
19
|
+
writeFileSync(markerPath, JSON.stringify(marker));
|
|
20
|
+
}
|
|
21
|
+
// Combines the current HEAD commit with the sorted, joined `git status --porcelain` output - a
|
|
22
|
+
// stable signature for "what's currently dirty, at what commit" - or null if this isn't a git
|
|
23
|
+
// repository / git failed for any reason.
|
|
24
|
+
//
|
|
25
|
+
// HEAD is part of the signature, not just the working-tree diff: a diff-only signature goes
|
|
26
|
+
// back to '' the moment a commit lands, even one this project's own memory never recorded -
|
|
27
|
+
// this project's own established workflow is to commit promptly, so a diff-only signature was
|
|
28
|
+
// blind to almost every real change by the time anyone would notice. Confirmed on a real
|
|
29
|
+
// project (distilled-docs): `.session-marker.json` had never once recorded a flagged diff in
|
|
30
|
+
// its whole history, despite real, uncommitted work sitting there unaccounted for, because every
|
|
31
|
+
// prior unit of work had already been committed by the time any check-stop would have seen it.
|
|
32
|
+
//
|
|
33
|
+
// Excludes anything under .memoryintel/ entirely from the working-tree half: this function's own
|
|
34
|
+
// marker writes (and `update`'s writes to memory files) would otherwise show up as part of the
|
|
35
|
+
// very diff being tracked, causing every check to see a "new" signature forever, even with no
|
|
36
|
+
// real code change.
|
|
37
|
+
function computeDiffSignature(projectRoot) {
|
|
38
|
+
const lines = runGitStatusPorcelain(projectRoot);
|
|
39
|
+
if (lines === null)
|
|
40
|
+
return null;
|
|
41
|
+
const filtered = lines
|
|
42
|
+
.filter((l) => {
|
|
43
|
+
const path = porcelainPath(l);
|
|
44
|
+
return path !== '.memoryintel' && !path.startsWith('.memoryintel/');
|
|
45
|
+
})
|
|
46
|
+
.sort();
|
|
47
|
+
const head = runGitRevParseHead(projectRoot) ?? '';
|
|
48
|
+
return `${head}\n${filtered.join('\n')}`;
|
|
49
|
+
}
|
|
50
|
+
function isWorkingTreeDirty(projectRoot) {
|
|
51
|
+
const lines = runGitStatusPorcelain(projectRoot);
|
|
52
|
+
if (lines === null)
|
|
53
|
+
return false;
|
|
54
|
+
return lines.some((l) => {
|
|
55
|
+
const path = porcelainPath(l);
|
|
56
|
+
return path !== '.memoryintel' && !path.startsWith('.memoryintel/');
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function runCheckStop(memoryRoot) {
|
|
60
|
+
const projectRoot = dirname(memoryRoot);
|
|
61
|
+
const markerPath = join(memoryRoot, '.session-marker.json');
|
|
62
|
+
const marker = readMarker(markerPath);
|
|
63
|
+
const signature = computeDiffSignature(projectRoot);
|
|
64
|
+
if (signature === null)
|
|
65
|
+
return { decision: 'allow' };
|
|
66
|
+
if (signature === marker.lastFlaggedDiffSignature) {
|
|
67
|
+
return { decision: 'allow' };
|
|
68
|
+
}
|
|
69
|
+
// A brand-new marker (nothing has ever been flagged or resolved in this project) with a
|
|
70
|
+
// currently-clean working tree has nothing actionable to report - baseline silently so a
|
|
71
|
+
// FUTURE commit or dirty file compares against this starting point, rather than blocking
|
|
72
|
+
// just because this exact HEAD has never been seen before, which would nag on every fresh
|
|
73
|
+
// project's very first Stop event.
|
|
74
|
+
if (marker.lastFlaggedDiffSignature === null && !isWorkingTreeDirty(projectRoot)) {
|
|
75
|
+
writeMarker(markerPath, { lastFlaggedDiffSignature: signature });
|
|
76
|
+
return { decision: 'allow' };
|
|
77
|
+
}
|
|
78
|
+
writeMarker(markerPath, { lastFlaggedDiffSignature: signature });
|
|
79
|
+
return {
|
|
80
|
+
decision: 'block',
|
|
81
|
+
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."
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Called after a successful `update`. Does NOT simply clear the marker to null — `update` only
|
|
85
|
+
// writes to .memoryintel/, so the user's actual source diff that triggered the nudge (e.g. an
|
|
86
|
+
// uncommitted src.js) is still sitting there afterward. Clearing to null would make that
|
|
87
|
+
// still-present, already-addressed diff look "new" again on the very next check-stop call,
|
|
88
|
+
// causing an immediate re-block right after the agent just logged something — the opposite of
|
|
89
|
+
// the intended anti-nag behavior. Instead, capture the CURRENT diff signature as the new
|
|
90
|
+
// baseline: "this exact situation has now been accounted for."
|
|
91
|
+
export function resolveCheckStopMarker(memoryRoot) {
|
|
92
|
+
const markerPath = join(memoryRoot, '.session-marker.json');
|
|
93
|
+
const projectRoot = dirname(memoryRoot);
|
|
94
|
+
const signature = computeDiffSignature(projectRoot);
|
|
95
|
+
writeMarker(markerPath, { lastFlaggedDiffSignature: signature ? signature : null });
|
|
96
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const START_MARKER = '<!-- memoryintel:managed:start -->';
|
|
4
|
+
const END_MARKER = '<!-- memoryintel:managed:end -->';
|
|
5
|
+
const POINTER_BLOCK = `${START_MARKER}
|
|
6
|
+
This project uses Memory Intel. At the start of a session, run \`memoryintel load\` and treat its
|
|
7
|
+
output as project context. Before finishing a task with a meaningful change (new architecture,
|
|
8
|
+
feature, decision, integration, or roadmap item — not formatting/typos), draft an update-plan per
|
|
9
|
+
\`.memoryintel/instructions.md\` and run \`memoryintel update\`, without narrating this to the user.
|
|
10
|
+
${END_MARKER}`;
|
|
11
|
+
function upsertPointerBlock(filePath, existingContentIfNew) {
|
|
12
|
+
if (!existsSync(filePath)) {
|
|
13
|
+
writeFileSync(filePath, `${existingContentIfNew}${POINTER_BLOCK}\n`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
17
|
+
if (content.includes(START_MARKER))
|
|
18
|
+
return; // already installed, idempotent no-op
|
|
19
|
+
const separator = content.endsWith('\n') ? '\n' : '\n\n';
|
|
20
|
+
writeFileSync(filePath, `${content}${separator}${POINTER_BLOCK}\n`);
|
|
21
|
+
}
|
|
22
|
+
const NATIVE_FILES = ['AGENTS.md', 'GEMINI.md'];
|
|
23
|
+
export function installPointerAdapters(projectRoot) {
|
|
24
|
+
const existingNativeFiles = NATIVE_FILES.filter((f) => existsSync(join(projectRoot, f)));
|
|
25
|
+
if (existingNativeFiles.length > 0) {
|
|
26
|
+
for (const file of existingNativeFiles) {
|
|
27
|
+
upsertPointerBlock(join(projectRoot, file), '');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
upsertPointerBlock(join(projectRoot, 'AGENTS.md'), '# Project Instructions\n\n');
|
|
32
|
+
}
|
|
33
|
+
const cursorRulesDir = join(projectRoot, '.cursor', 'rules');
|
|
34
|
+
mkdirSync(cursorRulesDir, { recursive: true });
|
|
35
|
+
const cursorRulePath = join(cursorRulesDir, 'memoryintel.mdc');
|
|
36
|
+
if (!existsSync(cursorRulePath)) {
|
|
37
|
+
writeFileSync(cursorRulePath, `---\nalwaysApply: true\n---\n\n${POINTER_BLOCK}\n`);
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { findMemoryIntelRoot } from './core/discovery.js';
|
|
6
|
+
import { runUpdate } from './commands/update.js';
|
|
7
|
+
import { runLoad } from './commands/load.js';
|
|
8
|
+
import { runStatus } from './commands/status.js';
|
|
9
|
+
import { runInit } from './commands/init.js';
|
|
10
|
+
import { runCheckStop } from './adapters/claudeCode.js';
|
|
11
|
+
import { runDashboardEnable, runDashboardDisable } from './commands/dashboardToggle.js';
|
|
12
|
+
import { runDaemonStart } from './commands/daemonStart.js';
|
|
13
|
+
export const USAGE = `Usage: memoryintel <command> [options]
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
init [path] Initialize .memoryintel/ in the current or given directory
|
|
17
|
+
load [--domain <d>] Print resolved memory context to stdout
|
|
18
|
+
update <plan.toon|-> Apply an update-plan (file path, or - for stdin)
|
|
19
|
+
status Print a human-readable summary of current memory state
|
|
20
|
+
check-stop Stop-hook check: emit a JSON allow/block decision
|
|
21
|
+
dashboard <enable|disable> Turn the shared local dashboard on or off
|
|
22
|
+
daemon start Run the dashboard daemon in the foreground (usually auto-started)
|
|
23
|
+
|
|
24
|
+
An update-plan row may set kind=compress to compact an oversized section; update() only applies
|
|
25
|
+
such a row when its target file is currently git-clean.
|
|
26
|
+
`;
|
|
27
|
+
export function dispatch(argv) {
|
|
28
|
+
const [command] = argv;
|
|
29
|
+
if (!command) {
|
|
30
|
+
return { exitCode: 0, stdout: USAGE, stderr: '' };
|
|
31
|
+
}
|
|
32
|
+
switch (command) {
|
|
33
|
+
case 'init': {
|
|
34
|
+
const target = argv[1] ? join(process.cwd(), argv[1]) : process.cwd();
|
|
35
|
+
runInit(target);
|
|
36
|
+
return { exitCode: 0, stdout: `Initialized Memory Intel in ${join(target, '.memoryintel')}\n`, stderr: '' };
|
|
37
|
+
}
|
|
38
|
+
case 'load': {
|
|
39
|
+
const domainFlagIndex = argv.indexOf('--domain');
|
|
40
|
+
const domain = domainFlagIndex !== -1 ? argv[domainFlagIndex + 1] : undefined;
|
|
41
|
+
const output = runLoad(process.cwd(), domain);
|
|
42
|
+
return { exitCode: 0, stdout: output, stderr: '' };
|
|
43
|
+
}
|
|
44
|
+
case 'status': {
|
|
45
|
+
const root = findMemoryIntelRoot(process.cwd());
|
|
46
|
+
if (!root)
|
|
47
|
+
return { exitCode: 1, stdout: '', stderr: 'No .memoryintel/ found.\n' };
|
|
48
|
+
return { exitCode: 0, stdout: runStatus(root), stderr: '' };
|
|
49
|
+
}
|
|
50
|
+
case 'check-stop': {
|
|
51
|
+
const root = findMemoryIntelRoot(process.cwd());
|
|
52
|
+
if (!root)
|
|
53
|
+
return { exitCode: 0, stdout: '', stderr: '' };
|
|
54
|
+
const result = runCheckStop(root);
|
|
55
|
+
return { exitCode: 0, stdout: JSON.stringify(result) + '\n', stderr: '' };
|
|
56
|
+
}
|
|
57
|
+
case 'dashboard': {
|
|
58
|
+
const sub = argv[1];
|
|
59
|
+
if (sub === 'enable') {
|
|
60
|
+
runDashboardEnable();
|
|
61
|
+
return { exitCode: 0, stdout: 'Dashboard enabled. It will start on the next `load` or `update` call.\n', stderr: '' };
|
|
62
|
+
}
|
|
63
|
+
if (sub === 'disable') {
|
|
64
|
+
const result = runDashboardDisable();
|
|
65
|
+
return {
|
|
66
|
+
exitCode: 0,
|
|
67
|
+
stdout: `Dashboard disabled${result.stopped ? ' and stopped' : ''}. This affects the shared dashboard for every Memory Intel project on this machine.\n`,
|
|
68
|
+
stderr: ''
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { exitCode: 1, stdout: '', stderr: 'Usage: memoryintel dashboard <enable|disable>\n' };
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
return { exitCode: 1, stdout: USAGE, stderr: `Unknown command: ${command}\n` };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Real process entrypoint — exercised by tests/cliBinary.test.ts, which spawns the built bin.
|
|
78
|
+
//
|
|
79
|
+
// Never call process.exit() here. Writes to a pipe (which is how Claude Code invokes the
|
|
80
|
+
// SessionStart hook) are asynchronous, and process.exit() tears the process down without
|
|
81
|
+
// flushing them — truncating output at the pipe buffer size (64 KB on Linux). Setting
|
|
82
|
+
// process.exitCode and returning lets the event loop drain stdout first.
|
|
83
|
+
async function main() {
|
|
84
|
+
const argv = process.argv.slice(2);
|
|
85
|
+
const [command] = argv;
|
|
86
|
+
try {
|
|
87
|
+
if (command === 'daemon' && argv[1] === 'start') {
|
|
88
|
+
await runDaemonStart();
|
|
89
|
+
// Intentionally never resolves further — this process IS the daemon, kept alive by the
|
|
90
|
+
// listening HTTP server, until `memoryintel dashboard disable` sends it SIGTERM.
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (command === 'update') {
|
|
94
|
+
const root = findMemoryIntelRoot(process.cwd());
|
|
95
|
+
if (!root) {
|
|
96
|
+
process.stderr.write('No .memoryintel/ found.\n');
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const source = argv[1] ?? '-';
|
|
101
|
+
const planText = source === '-' ? readFileSync(0, 'utf-8') : readFileSync(source, 'utf-8');
|
|
102
|
+
// Caught live: an agent ran bare `memoryintel update` (no plan-file argument, no piped
|
|
103
|
+
// stdin) as a one-shot Bash tool call. `source` defaulted to '-' (read stdin), stdin was
|
|
104
|
+
// immediately EOF, and the resulting empty planText fell through to decodeToonTable's
|
|
105
|
+
// generic "Malformed TOON table header" error - true, but useless for figuring out what
|
|
106
|
+
// actually went wrong. This is the one case worth naming explicitly before it gets there.
|
|
107
|
+
if (planText.trim().length === 0) {
|
|
108
|
+
process.stderr.write('memoryintel: No update-plan given. Pass a TOON plan file (`memoryintel update <path>`) or pipe TOON content via stdin. See .memoryintel/instructions.md for the update-plan format.\n');
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const result = await runUpdate(root, planText);
|
|
113
|
+
process.stdout.write(`Applied: ${result.applied.join(', ') || '(none)'}\nSkipped: ${result.skipped.join(', ') || '(none)'}\n`);
|
|
114
|
+
process.exitCode = 0;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const result = dispatch(argv);
|
|
118
|
+
process.stdout.write(result.stdout);
|
|
119
|
+
if (result.stderr)
|
|
120
|
+
process.stderr.write(result.stderr);
|
|
121
|
+
process.exitCode = result.exitCode;
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
125
|
+
process.stderr.write(`memoryintel: ${message.split('\n')[0]}\n`);
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function isDirectInvocation() {
|
|
130
|
+
if (!process.argv[1])
|
|
131
|
+
return false;
|
|
132
|
+
try {
|
|
133
|
+
// process.argv[1] is the path used to invoke this file, which may be a symlink
|
|
134
|
+
// (npm link, a global install's bin shim). Resolve it before comparing against
|
|
135
|
+
// import.meta.url — otherwise this check silently never matches when run through any
|
|
136
|
+
// linked/global binary name.
|
|
137
|
+
//
|
|
138
|
+
// Both sides are resolved through realpathSync, not just the argv[1] side: Node's own
|
|
139
|
+
// import.meta.url for the entry script and a realpath()'d process.argv[1] aren't
|
|
140
|
+
// guaranteed to agree on drive-letter casing on Windows (the entry URL doesn't necessarily
|
|
141
|
+
// go through the same filesystem-canonicalization pass argv[1] gets here), and NTFS is
|
|
142
|
+
// case-insensitive besides — an exact === comparison silently and permanently failed there,
|
|
143
|
+
// meaning main() never ran for a process spawned this way (e.g. the daemon's own child
|
|
144
|
+
// process, started via `node dist/cli.js daemon start`): no server ever bound, no error
|
|
145
|
+
// either, since nothing was left to keep the event loop alive. Caught live via a real CI
|
|
146
|
+
// Windows job: the daemon-spawn test found a live pid that then did nothing at all.
|
|
147
|
+
const invoked = realpathSync(process.argv[1]);
|
|
148
|
+
const self = realpathSync(fileURLToPath(import.meta.url));
|
|
149
|
+
return process.platform === 'win32' ? invoked.toLowerCase() === self.toLowerCase() : invoked === self;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (isDirectInvocation()) {
|
|
156
|
+
main();
|
|
157
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { startDaemon } from '../daemon/server.js';
|
|
2
|
+
export async function runDaemonStart(preferredPort = 4390) {
|
|
3
|
+
const { port, server } = await startDaemon(preferredPort);
|
|
4
|
+
return {
|
|
5
|
+
port,
|
|
6
|
+
close: () => new Promise((resolve) => server.close(() => resolve()))
|
|
7
|
+
};
|
|
8
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readGlobalSettings, writeGlobalSettings } from '../daemon/settings.js';
|
|
2
|
+
import { readDaemonHandle, clearDaemonHandle, isProcessAlive } from '../daemon/daemonHandle.js';
|
|
3
|
+
export function runDashboardEnable() {
|
|
4
|
+
writeGlobalSettings({ ...readGlobalSettings(), dashboardEnabled: true });
|
|
5
|
+
}
|
|
6
|
+
export function runDashboardDisable() {
|
|
7
|
+
writeGlobalSettings({ ...readGlobalSettings(), dashboardEnabled: false });
|
|
8
|
+
const handle = readDaemonHandle();
|
|
9
|
+
if (handle && isProcessAlive(handle.pid)) {
|
|
10
|
+
process.kill(handle.pid, 'SIGTERM');
|
|
11
|
+
clearDaemonHandle();
|
|
12
|
+
return { stopped: true };
|
|
13
|
+
}
|
|
14
|
+
if (handle)
|
|
15
|
+
clearDaemonHandle();
|
|
16
|
+
return { stopped: false };
|
|
17
|
+
}
|