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.
Files changed (39) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +9 -0
  3. package/LICENSE +21 -0
  4. package/README.md +192 -0
  5. package/dist/adapters/claudeCode.js +96 -0
  6. package/dist/adapters/genericPointer.js +39 -0
  7. package/dist/cli.js +157 -0
  8. package/dist/commands/daemonStart.js +8 -0
  9. package/dist/commands/dashboardToggle.js +17 -0
  10. package/dist/commands/init.js +111 -0
  11. package/dist/commands/load.js +82 -0
  12. package/dist/commands/status.js +24 -0
  13. package/dist/commands/update.js +108 -0
  14. package/dist/core/atomicWrite.js +6 -0
  15. package/dist/core/compressionConfig.js +37 -0
  16. package/dist/core/discovery.js +14 -0
  17. package/dist/core/eventLog.js +4 -0
  18. package/dist/core/gitPorcelain.js +45 -0
  19. package/dist/core/headingMatch.js +44 -0
  20. package/dist/core/lock.js +67 -0
  21. package/dist/core/memoryIndex.js +19 -0
  22. package/dist/core/pathSafety.js +43 -0
  23. package/dist/core/sectionWriter.js +91 -0
  24. package/dist/core/toon.js +118 -0
  25. package/dist/daemon/daemonHandle.js +52 -0
  26. package/dist/daemon/globalPaths.js +15 -0
  27. package/dist/daemon/health.js +14 -0
  28. package/dist/daemon/lifecycle.js +54 -0
  29. package/dist/daemon/registry.js +60 -0
  30. package/dist/daemon/server.js +92 -0
  31. package/dist/daemon/settings.js +13 -0
  32. package/dist/daemon/views/layout.js +233 -0
  33. package/dist/daemon/views/projectPage.js +111 -0
  34. package/dist/daemon/views/registryPage.js +54 -0
  35. package/dist/skill.js +46 -0
  36. package/dist/templates/starterFiles.js +22 -0
  37. package/hooks/hooks.json +11 -0
  38. package/package.json +52 -0
  39. 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
+ [![CI](https://github.com/adeeshsharma/memoryintel/actions/workflows/ci.yml/badge.svg)](https://github.com/adeeshsharma/memoryintel/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/memoryintel.svg)](https://www.npmjs.com/package/memoryintel)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
+ }