@vishnuvmx/recall-memory 0.0.0-stage → 0.1.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +386 -2
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.js +451 -0
  5. package/dist/core/agent.d.ts +21 -0
  6. package/dist/core/agent.js +140 -0
  7. package/dist/core/checkpoint.d.ts +42 -0
  8. package/dist/core/checkpoint.js +233 -0
  9. package/dist/core/config.d.ts +5 -0
  10. package/dist/core/config.js +36 -0
  11. package/dist/core/cost.d.ts +19 -0
  12. package/dist/core/cost.js +78 -0
  13. package/dist/core/doctor.d.ts +7 -0
  14. package/dist/core/doctor.js +69 -0
  15. package/dist/core/git.d.ts +27 -0
  16. package/dist/core/git.js +107 -0
  17. package/dist/core/index-builder.d.ts +11 -0
  18. package/dist/core/index-builder.js +84 -0
  19. package/dist/core/init.d.ts +13 -0
  20. package/dist/core/init.js +52 -0
  21. package/dist/core/landmarks.d.ts +19 -0
  22. package/dist/core/landmarks.js +323 -0
  23. package/dist/core/mapper.d.ts +28 -0
  24. package/dist/core/mapper.js +357 -0
  25. package/dist/core/memory.d.ts +44 -0
  26. package/dist/core/memory.js +199 -0
  27. package/dist/core/project.d.ts +12 -0
  28. package/dist/core/project.js +34 -0
  29. package/dist/core/retrieval.d.ts +27 -0
  30. package/dist/core/retrieval.js +180 -0
  31. package/dist/core/seed.d.ts +25 -0
  32. package/dist/core/seed.js +196 -0
  33. package/dist/core/service.d.ts +76 -0
  34. package/dist/core/service.js +228 -0
  35. package/dist/core/where.d.ts +14 -0
  36. package/dist/core/where.js +86 -0
  37. package/dist/index.d.ts +11 -0
  38. package/dist/index.js +11 -0
  39. package/dist/mcp/server.d.ts +65 -0
  40. package/dist/mcp/server.js +121 -0
  41. package/dist/schemas/index.d.ts +68 -0
  42. package/dist/schemas/index.js +43 -0
  43. package/dist/templates/index.d.ts +21 -0
  44. package/dist/templates/index.js +62 -0
  45. package/dist/utils/fs.d.ts +15 -0
  46. package/dist/utils/fs.js +73 -0
  47. package/dist/utils/managed.d.ts +5 -0
  48. package/dist/utils/managed.js +41 -0
  49. package/dist/utils/secrets.d.ts +3 -0
  50. package/dist/utils/secrets.js +23 -0
  51. package/package.json +65 -4
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Always-on instruction installed into the agent file (AGENTS.md by default).
3
+ *
4
+ * TOKEN BUDGET: this text is injected into EVERY agent turn, so it is capped at
5
+ * ~120 tokens and enforced by tests/budget.test.ts. It must also be STATIC
6
+ * (never embed dynamic state): changing always-on text invalidates provider
7
+ * prompt caches and silently multiplies cost. Detail belongs in the on-demand
8
+ * skill (ANTIGRAVITY_SKILL_BODY), not here.
9
+ */
10
+ export declare const AGENT_BLOCK = "## Project memory (Recall)\nMemory is in `.recall/`. Don't read it upfront; skip it for routine edits.\n- Unfamiliar area or design question: `recall context \"<topic>\"`\n- Find a symbol or file: `recall where <name>`\n- Learned a non-obvious rule or bug cause: `recall remember \"<text>\" -t \"<title>\" --type <decision|convention|lesson>`\nSessions are logged automatically; no wrap-up needed.";
11
+ /** Kept so `recall init` can recognize and migrate files written by older versions. */
12
+ export declare const LEGACY_SKILL_MARKER = "# Recall Agent Protocol";
13
+ export declare const MEMORY_MD = "# Project Memory\n\nShort overview of this project. Edit this file by hand; Recall never overwrites it.\n\n## Critical constraints\n\n- (Add the few rules that must never be broken.)\n\n## Where to look\n\nDetailed knowledge is indexed in [INDEX.md](INDEX.md). Search it with `recall context \"<task>\"`.\n";
14
+ export declare const RULE_TEMPLATES: Record<string, string>;
15
+ export declare const CHANGELOG_MD = "# Recall changelog\n\nLocal log of memory operations. Git history is authoritative when available.\n\n";
16
+ /**
17
+ * On-demand skill: only its name + description are always visible to the agent;
18
+ * the body is loaded when the agent decides it is relevant. Detail lives here.
19
+ */
20
+ export declare const SKILL_FRONTMATTER = "---\nname: recall\ndescription: Project memory lookups (context, where) and recording learnings.\n---";
21
+ export declare const ANTIGRAVITY_SKILL_BODY = "# Recall\n\nProject memory lives in `.recall/` as Markdown. Use it to avoid re-discovering things, not as a ritual.\n\n## Look things up (pull, never push)\n- `recall context \"<topic>\"`: past decisions, conventions, and lessons. Output is capped (~400 tokens). Verify claims against current code.\n- `recall where <symbol-or-path>`: `file:line` locations from a cached symbol scan. Open only the lines you need.\n- Continuing earlier work: read `.recall/CURRENT_STATE.md`. It is history, not a to-do list.\n\n## Record what future sessions need\n- `recall remember \"<text>\" -t \"<title>\" --type <decision|convention|lesson|bugfix|failed_approach>`\n- Record only non-obvious knowledge: why a design was chosen, invariants, root causes, dead ends.\n- Re-recording a similar title updates the existing memory instead of duplicating it.\n- Never store secrets (writes containing them are rejected).\n\n## Don't\n- Don't read `.recall/` files wholesale or \"orient\" before simple tasks.\n- Don't run a checkpoint at the end of a session; a hook records it automatically.";
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Always-on instruction installed into the agent file (AGENTS.md by default).
3
+ *
4
+ * TOKEN BUDGET: this text is injected into EVERY agent turn, so it is capped at
5
+ * ~120 tokens and enforced by tests/budget.test.ts. It must also be STATIC
6
+ * (never embed dynamic state): changing always-on text invalidates provider
7
+ * prompt caches and silently multiplies cost. Detail belongs in the on-demand
8
+ * skill (ANTIGRAVITY_SKILL_BODY), not here.
9
+ */
10
+ export const AGENT_BLOCK = `## Project memory (Recall)
11
+ Memory is in \`.recall/\`. Don't read it upfront; skip it for routine edits.
12
+ - Unfamiliar area or design question: \`recall context "<topic>"\`
13
+ - Find a symbol or file: \`recall where <name>\`
14
+ - Learned a non-obvious rule or bug cause: \`recall remember "<text>" -t "<title>" --type <decision|convention|lesson>\`
15
+ Sessions are logged automatically; no wrap-up needed.`;
16
+ /** Kept so `recall init` can recognize and migrate files written by older versions. */
17
+ export const LEGACY_SKILL_MARKER = '# Recall Agent Protocol';
18
+ export const MEMORY_MD = `# Project Memory
19
+
20
+ Short overview of this project. Edit this file by hand; Recall never overwrites it.
21
+
22
+ ## Critical constraints
23
+
24
+ - (Add the few rules that must never be broken.)
25
+
26
+ ## Where to look
27
+
28
+ Detailed knowledge is indexed in [INDEX.md](INDEX.md). Search it with \`recall context "<task>"\`.
29
+ `;
30
+ export const RULE_TEMPLATES = {
31
+ 'architecture.md': '# Architecture rules\n\nDescribe the architecture constraints agents must preserve.\n',
32
+ 'coding-style.md': '# Coding style\n\nDescribe naming, formatting, and structure conventions.\n',
33
+ 'design-system.md': '# Design system\n\nList design tokens/components and the checks that enforce them.\n',
34
+ 'workflows.md': '# Workflows\n\nDescribe build, test, and release commands agents should use.\n',
35
+ };
36
+ export const CHANGELOG_MD = '# Recall changelog\n\nLocal log of memory operations. Git history is authoritative when available.\n\n';
37
+ /**
38
+ * On-demand skill: only its name + description are always visible to the agent;
39
+ * the body is loaded when the agent decides it is relevant. Detail lives here.
40
+ */
41
+ export const SKILL_FRONTMATTER = `---
42
+ name: recall
43
+ description: Project memory lookups (context, where) and recording learnings.
44
+ ---`;
45
+ export const ANTIGRAVITY_SKILL_BODY = `# Recall
46
+
47
+ Project memory lives in \`.recall/\` as Markdown. Use it to avoid re-discovering things, not as a ritual.
48
+
49
+ ## Look things up (pull, never push)
50
+ - \`recall context "<topic>"\`: past decisions, conventions, and lessons. Output is capped (~400 tokens). Verify claims against current code.
51
+ - \`recall where <symbol-or-path>\`: \`file:line\` locations from a cached symbol scan. Open only the lines you need.
52
+ - Continuing earlier work: read \`.recall/CURRENT_STATE.md\`. It is history, not a to-do list.
53
+
54
+ ## Record what future sessions need
55
+ - \`recall remember "<text>" -t "<title>" --type <decision|convention|lesson|bugfix|failed_approach>\`
56
+ - Record only non-obvious knowledge: why a design was chosen, invariants, root causes, dead ends.
57
+ - Re-recording a similar title updates the existing memory instead of duplicating it.
58
+ - Never store secrets (writes containing them are rejected).
59
+
60
+ ## Don't
61
+ - Don't read \`.recall/\` files wholesale or "orient" before simple tasks.
62
+ - Don't run a checkpoint at the end of a session; a hook records it automatically.`;
@@ -0,0 +1,15 @@
1
+ export declare class RecallError extends Error {
2
+ readonly hint?: string | undefined;
3
+ constructor(message: string, hint?: string | undefined);
4
+ }
5
+ /** Write via temp file + rename so readers never see partial content. */
6
+ export declare function atomicWrite(file: string, data: string): void;
7
+ /** Create a file only if it does not exist. Returns false when it already exists. */
8
+ export declare function writeNew(file: string, data: string): boolean;
9
+ /**
10
+ * Resolve `parts` under `base`, rejecting traversal and symlink escapes.
11
+ * Works for paths that do not exist yet (checks the deepest existing ancestor).
12
+ */
13
+ export declare function resolveInside(base: string, ...parts: string[]): string;
14
+ export declare function toPosix(p: string): string;
15
+ export declare function today(): string;
@@ -0,0 +1,73 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export class RecallError extends Error {
4
+ hint;
5
+ constructor(message, hint) {
6
+ super(message);
7
+ this.hint = hint;
8
+ this.name = 'RecallError';
9
+ }
10
+ }
11
+ /** Write via temp file + rename so readers never see partial content. */
12
+ export function atomicWrite(file, data) {
13
+ fs.mkdirSync(path.dirname(file), { recursive: true });
14
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
15
+ try {
16
+ fs.writeFileSync(tmp, data, 'utf8');
17
+ fs.renameSync(tmp, file);
18
+ }
19
+ catch (err) {
20
+ try {
21
+ fs.rmSync(tmp, { force: true });
22
+ }
23
+ catch {
24
+ /* ignore */
25
+ }
26
+ throw err;
27
+ }
28
+ }
29
+ /** Create a file only if it does not exist. Returns false when it already exists. */
30
+ export function writeNew(file, data) {
31
+ fs.mkdirSync(path.dirname(file), { recursive: true });
32
+ try {
33
+ fs.writeFileSync(file, data, { encoding: 'utf8', flag: 'wx' });
34
+ return true;
35
+ }
36
+ catch (err) {
37
+ if (err.code === 'EEXIST')
38
+ return false;
39
+ throw err;
40
+ }
41
+ }
42
+ function isInside(base, target) {
43
+ const rel = path.relative(base, target);
44
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
45
+ }
46
+ /**
47
+ * Resolve `parts` under `base`, rejecting traversal and symlink escapes.
48
+ * Works for paths that do not exist yet (checks the deepest existing ancestor).
49
+ */
50
+ export function resolveInside(base, ...parts) {
51
+ const target = path.resolve(base, ...parts);
52
+ if (!isInside(path.resolve(base), target)) {
53
+ throw new RecallError(`Path escapes the allowed directory: ${parts.join('/')}`);
54
+ }
55
+ const realBase = fs.realpathSync(base);
56
+ let probe = target;
57
+ while (!fs.existsSync(probe)) {
58
+ const parent = path.dirname(probe);
59
+ if (parent === probe)
60
+ break;
61
+ probe = parent;
62
+ }
63
+ if (!isInside(realBase, fs.realpathSync(probe))) {
64
+ throw new RecallError(`Path resolves outside the allowed directory via a symlink: ${parts.join('/')}`);
65
+ }
66
+ return target;
67
+ }
68
+ export function toPosix(p) {
69
+ return p.split(path.sep).join('/');
70
+ }
71
+ export function today() {
72
+ return new Date().toISOString().slice(0, 10);
73
+ }
@@ -0,0 +1,5 @@
1
+ /** Replace (or append) a marker-delimited block, preserving everything outside it. */
2
+ export declare function applyManagedBlock(existing: string, block: string, name?: string): string;
3
+ export declare function extractManagedBlock(existing: string, name?: string): string | null;
4
+ /** Remove a marker-delimited block (and its markers), preserving everything else. No-op if absent. */
5
+ export declare function removeManagedBlock(existing: string, name?: string): string;
@@ -0,0 +1,41 @@
1
+ import { RecallError } from './fs.js';
2
+ /** Replace (or append) a marker-delimited block, preserving everything outside it. */
3
+ export function applyManagedBlock(existing, block, name = 'recall') {
4
+ const begin = `<!-- ${name}:begin -->`;
5
+ const end = `<!-- ${name}:end -->`;
6
+ const wrapped = `${begin}\n${block.trim()}\n${end}`;
7
+ const b = existing.indexOf(begin);
8
+ const e = existing.indexOf(end);
9
+ if (b === -1 && e === -1) {
10
+ if (existing.trim() === '')
11
+ return `${wrapped}\n`;
12
+ return `${existing.replace(/\s*$/, '')}\n\n${wrapped}\n`;
13
+ }
14
+ if (b === -1 || e === -1 || e < b || existing.indexOf(begin, b + 1) !== -1) {
15
+ throw new RecallError(`Malformed Recall markers (${name}). Refusing to modify the file.`, `Fix or remove the "${begin}" / "${end}" lines manually, then retry.`);
16
+ }
17
+ return existing.slice(0, b) + wrapped + existing.slice(e + end.length);
18
+ }
19
+ export function extractManagedBlock(existing, name = 'recall') {
20
+ const begin = `<!-- ${name}:begin -->`;
21
+ const end = `<!-- ${name}:end -->`;
22
+ const b = existing.indexOf(begin);
23
+ const e = existing.indexOf(end);
24
+ if (b === -1 || e === -1 || e < b)
25
+ return null;
26
+ return existing.slice(b + begin.length, e).trim();
27
+ }
28
+ /** Remove a marker-delimited block (and its markers), preserving everything else. No-op if absent. */
29
+ export function removeManagedBlock(existing, name = 'recall') {
30
+ const begin = `<!-- ${name}:begin -->`;
31
+ const end = `<!-- ${name}:end -->`;
32
+ const b = existing.indexOf(begin);
33
+ const e = existing.indexOf(end);
34
+ if (b === -1 || e === -1 || e < b)
35
+ return existing;
36
+ const before = existing.slice(0, b).replace(/\s*$/, '');
37
+ const after = existing.slice(e + end.length).replace(/^\s*/, '');
38
+ if (!before)
39
+ return after;
40
+ return after ? `${before}\n\n${after}` : `${before}\n`;
41
+ }
@@ -0,0 +1,3 @@
1
+ /** Returns the labels of any secret-looking patterns (never the matched text). */
2
+ export declare function findSecrets(text: string): string[];
3
+ export declare function assertNoSecrets(text: string): void;
@@ -0,0 +1,23 @@
1
+ import { RecallError } from './fs.js';
2
+ const PATTERNS = [
3
+ ['private key', /-----BEGIN [A-Z ]*PRIVATE KEY-----/],
4
+ ['AWS access key', /\bAKIA[0-9A-Z]{16}\b/],
5
+ ['API key (sk-...)', /\bsk-[A-Za-z0-9_-]{20,}/],
6
+ ['GitHub token', /\bgh[pousr]_[A-Za-z0-9]{30,}/],
7
+ ['Google API key', /\bAIza[0-9A-Za-z\-_]{35,}\b/],
8
+ ['Slack token', /\bxox[baprs]-[0-9A-Za-z-]{10,48}\b/],
9
+ ['Stripe API key', /\b[rs]k_(live|test)_[0-9a-zA-Z]{24,}\b/],
10
+ ['JWT token', /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}\b/],
11
+ ['connection string with credentials', /\b[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^:\s]+:[^@\s]+@[a-zA-Z0-9.-]+/],
12
+ ['credential assignment', /\b(api[_-]?key|secret|token|passwd|password)\b\s*[:=]\s*['"]?[A-Za-z0-9/+_-]{16,}/i],
13
+ ];
14
+ /** Returns the labels of any secret-looking patterns (never the matched text). */
15
+ export function findSecrets(text) {
16
+ return PATTERNS.filter(([, re]) => re.test(text)).map(([label]) => label);
17
+ }
18
+ export function assertNoSecrets(text) {
19
+ const hits = findSecrets(text);
20
+ if (hits.length > 0) {
21
+ throw new RecallError(`Refusing to store content that looks like a secret (${hits.join(', ')}).`, 'Remove the credential and describe it instead (e.g. "API key lives in the env var X").');
22
+ }
23
+ }
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "@vishnuvmx/recall-memory",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Zero-maintenance, local-first persistent project memory for AI coding agents.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "recall": "dist/cli/index.js"
9
+ },
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/vishnuvmx/recall-memory.git"
21
+ },
22
+ "homepage": "https://github.com/vishnuvmx/recall-memory#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/vishnuvmx/recall-memory/issues"
25
+ },
26
+ "keywords": [
27
+ "ai",
28
+ "agent",
29
+ "memory",
30
+ "coding-agents",
31
+ "mcp",
32
+ "antigravity",
33
+ "claude-code",
34
+ "cursor"
35
+ ],
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "engines": {
45
+ "node": ">=20"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc -p tsconfig.build.json",
49
+ "typecheck": "tsc --noEmit",
50
+ "test": "vitest run",
51
+ "prebench": "npm run build",
52
+ "bench": "node scripts/benchmark.js",
53
+ "prepare": "git config core.hooksPath .githooks || true",
54
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
55
+ },
56
+ "dependencies": {
57
+ "commander": "^12.1.0",
58
+ "gray-matter": "^4.0.3",
59
+ "ignore": "^7.0.12",
60
+ "zod": "^3.23.8"
61
+ },
62
+ "devDependencies": {
63
+ "@types/node": "^20.14.0",
64
+ "typescript": "^5.6.0",
65
+ "vitest": "^2.1.0"
66
+ }
67
+ }