@iamem/amem 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 (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +534 -0
  3. package/dist/activity.d.ts +27 -0
  4. package/dist/activity.js +202 -0
  5. package/dist/api/routes.d.ts +31 -0
  6. package/dist/api/routes.js +1345 -0
  7. package/dist/attest.d.ts +52 -0
  8. package/dist/attest.js +192 -0
  9. package/dist/backup-schedule.d.ts +25 -0
  10. package/dist/backup-schedule.js +216 -0
  11. package/dist/capture.d.ts +34 -0
  12. package/dist/capture.js +257 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +1007 -0
  15. package/dist/context.d.ts +46 -0
  16. package/dist/context.js +334 -0
  17. package/dist/crypto.d.ts +39 -0
  18. package/dist/crypto.js +166 -0
  19. package/dist/db.d.ts +192 -0
  20. package/dist/db.js +666 -0
  21. package/dist/draft-quality.d.ts +19 -0
  22. package/dist/draft-quality.js +85 -0
  23. package/dist/embed.d.ts +76 -0
  24. package/dist/embed.js +331 -0
  25. package/dist/estimate.d.ts +32 -0
  26. package/dist/estimate.js +69 -0
  27. package/dist/freshness.d.ts +15 -0
  28. package/dist/freshness.js +93 -0
  29. package/dist/hook.d.ts +16 -0
  30. package/dist/hook.js +177 -0
  31. package/dist/hygiene-schedule.d.ts +28 -0
  32. package/dist/hygiene-schedule.js +221 -0
  33. package/dist/hygiene.d.ts +61 -0
  34. package/dist/hygiene.js +196 -0
  35. package/dist/install/claude.d.ts +6 -0
  36. package/dist/install/claude.js +69 -0
  37. package/dist/install/cursor.d.ts +7 -0
  38. package/dist/install/cursor.js +80 -0
  39. package/dist/install/hosts.d.ts +21 -0
  40. package/dist/install/hosts.js +186 -0
  41. package/dist/install/skills.d.ts +8 -0
  42. package/dist/install/skills.js +67 -0
  43. package/dist/it-pack.d.ts +20 -0
  44. package/dist/it-pack.js +84 -0
  45. package/dist/kinds.d.ts +18 -0
  46. package/dist/kinds.js +106 -0
  47. package/dist/license.d.ts +48 -0
  48. package/dist/license.js +172 -0
  49. package/dist/mcp.d.ts +40 -0
  50. package/dist/mcp.js +435 -0
  51. package/dist/paths.d.ts +11 -0
  52. package/dist/paths.js +55 -0
  53. package/dist/personal.d.ts +7 -0
  54. package/dist/personal.js +44 -0
  55. package/dist/platforms.d.ts +11 -0
  56. package/dist/platforms.js +32 -0
  57. package/dist/policy.d.ts +46 -0
  58. package/dist/policy.js +254 -0
  59. package/dist/prefs.d.ts +6 -0
  60. package/dist/prefs.js +11 -0
  61. package/dist/proposal.d.ts +90 -0
  62. package/dist/proposal.js +376 -0
  63. package/dist/publish.d.ts +28 -0
  64. package/dist/publish.js +57 -0
  65. package/dist/remember-contract.d.ts +23 -0
  66. package/dist/remember-contract.js +117 -0
  67. package/dist/repo-identity.d.ts +15 -0
  68. package/dist/repo-identity.js +82 -0
  69. package/dist/rules-sync.d.ts +7 -0
  70. package/dist/rules-sync.js +47 -0
  71. package/dist/savings-export.d.ts +49 -0
  72. package/dist/savings-export.js +141 -0
  73. package/dist/scan.d.ts +16 -0
  74. package/dist/scan.js +109 -0
  75. package/dist/search.d.ts +25 -0
  76. package/dist/search.js +150 -0
  77. package/dist/service.d.ts +20 -0
  78. package/dist/service.js +254 -0
  79. package/dist/shop.d.ts +9 -0
  80. package/dist/shop.js +15 -0
  81. package/dist/ui/server.d.ts +21 -0
  82. package/dist/ui/server.js +268 -0
  83. package/dist/vault.d.ts +25 -0
  84. package/dist/vault.js +42 -0
  85. package/dist/workspace-setup.d.ts +8 -0
  86. package/dist/workspace-setup.js +55 -0
  87. package/docs/agent-install-prompt.md +41 -0
  88. package/docs/backlog.md +59 -0
  89. package/docs/enterprise-endpoint.md +98 -0
  90. package/docs/license.md +54 -0
  91. package/docs/npm-release.md +38 -0
  92. package/docs/remember-contract.md +42 -0
  93. package/package.json +64 -0
  94. package/scripts/mdm-offboard.sh +14 -0
  95. package/skills/amem-bootstrap/SKILL.md +83 -0
  96. package/skills/amem-update-working-memory/SKILL.md +54 -0
  97. package/templates/cursor-rule.mdc +23 -0
  98. package/templates/mdm/co.amem.managed.plist +33 -0
  99. package/templates/policy.deny-default.toml +20 -0
  100. package/templates/policy.example.toml +22 -0
  101. package/ui-static/app.js +3966 -0
  102. package/ui-static/index.html +178 -0
  103. package/ui-static/orbit.js +389 -0
  104. package/ui-static/styles.css +2411 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Local memory hygiene: decay unused facts, find near-duplicates, review inbox.
3
+ * Apply/schedule is Pro/IT only. Preview counts are free (soft paywall).
4
+ * Nothing is uploaded.
5
+ */
6
+ import { getClaim, listClaims, listProposalDrafts, listRepos, listUsageEvents, setClaimStatus, } from "./db.js";
7
+ import { FEATURE_HYGIENE, hasFeature, requireFeature } from "./license.js";
8
+ import { applyProposal, applySupersedes } from "./proposal.js";
9
+ import { tokenJaccard } from "./search.js";
10
+ import { parseAnchors } from "./freshness.js";
11
+ export const SOFT_PAYWALL_FACTS = 200;
12
+ export const SOFT_PAYWALL_NOISE = 15;
13
+ function usedClaimIds(repoId, days) {
14
+ const ids = new Set();
15
+ for (const event of listUsageEvents({ repoId, days })) {
16
+ try {
17
+ for (const id of JSON.parse(event.claim_ids || "[]")) {
18
+ if (id)
19
+ ids.add(id);
20
+ }
21
+ }
22
+ catch {
23
+ // ignore
24
+ }
25
+ }
26
+ return ids;
27
+ }
28
+ /** Shared heuristics — no license gate (used by free preview + Pro report). */
29
+ function computeHygiene(repoId, unusedDays = 90) {
30
+ const claims = listClaims(repoId);
31
+ const used = usedClaimIds(repoId, unusedDays);
32
+ const cutoff = Date.now() - unusedDays * 86_400_000;
33
+ const stale = claims.filter((c) => {
34
+ if (Number(c.pinned || 0) > 0)
35
+ return false;
36
+ if (used.has(c.id))
37
+ return false;
38
+ const updated = Date.parse(c.updated_at);
39
+ return Number.isFinite(updated) && updated < cutoff;
40
+ });
41
+ const duplicates = [];
42
+ for (let i = 0; i < claims.length; i++) {
43
+ for (let j = i + 1; j < claims.length; j++) {
44
+ const a = claims[i];
45
+ const b = claims[j];
46
+ const sim = tokenJaccard(a.text, b.text);
47
+ if (sim < 0.72)
48
+ continue;
49
+ const aAnchors = new Set(parseAnchors(a.code_anchors));
50
+ const bAnchors = parseAnchors(b.code_anchors);
51
+ const share = bAnchors.some((p) => aAnchors.has(p));
52
+ if (!share && sim < 0.85)
53
+ continue;
54
+ const keep = Number(a.pinned || 0) >= Number(b.pinned || 0) && a.text.length >= b.text.length ? a : b;
55
+ const drop = keep.id === a.id ? b : a;
56
+ duplicates.push({ keepId: keep.id, dropId: drop.id, similarity: sim });
57
+ }
58
+ }
59
+ return {
60
+ stale,
61
+ duplicates: duplicates.slice(0, 40),
62
+ pendingDrafts: listProposalDrafts(repoId, { status: "pending" }).length,
63
+ active: claims.length,
64
+ };
65
+ }
66
+ /** Free: counts only for soft paywall / banners. Never applies changes. */
67
+ export function hygienePreview(repoId, unusedDays = 90) {
68
+ const report = computeHygiene(repoId, unusedDays);
69
+ const staleCount = report.stale.length;
70
+ const duplicateCount = report.duplicates.length;
71
+ const removable = Math.min(report.active, staleCount + duplicateCount);
72
+ const afterCleanup = Math.max(0, report.active - removable);
73
+ const thresholdHit = report.active >= SOFT_PAYWALL_FACTS || staleCount + duplicateCount >= SOFT_PAYWALL_NOISE;
74
+ // Paid (hygiene unlocked) — never surface an unpaid soft-paywall signal.
75
+ const softPaywall = thresholdHit && !hasFeature(FEATURE_HYGIENE);
76
+ return {
77
+ active: report.active,
78
+ staleCount,
79
+ duplicateCount,
80
+ pendingDrafts: report.pendingDrafts,
81
+ afterCleanup,
82
+ softPaywall,
83
+ };
84
+ }
85
+ /** Aggregate preview across all tracked repos (for All memory scope). */
86
+ export function hygienePreviewAll(unusedDays = 90) {
87
+ let active = 0;
88
+ let staleCount = 0;
89
+ let duplicateCount = 0;
90
+ let pendingDrafts = 0;
91
+ for (const repo of listRepos()) {
92
+ const p = hygienePreview(repo.id, unusedDays);
93
+ active += p.active;
94
+ staleCount += p.staleCount;
95
+ duplicateCount += p.duplicateCount;
96
+ pendingDrafts += p.pendingDrafts;
97
+ }
98
+ const removable = Math.min(active, staleCount + duplicateCount);
99
+ const afterCleanup = Math.max(0, active - removable);
100
+ const thresholdHit = active >= SOFT_PAYWALL_FACTS || staleCount + duplicateCount >= SOFT_PAYWALL_NOISE;
101
+ const softPaywall = thresholdHit && !hasFeature(FEATURE_HYGIENE);
102
+ return { active, staleCount, duplicateCount, pendingDrafts, afterCleanup, softPaywall };
103
+ }
104
+ export function hygieneReport(repoId, unusedDays = 90) {
105
+ requireFeature(FEATURE_HYGIENE, "Memory hygiene");
106
+ return computeHygiene(repoId, unusedDays);
107
+ }
108
+ export function decayStaleClaims(repoId, unusedDays = 90) {
109
+ requireFeature(FEATURE_HYGIENE, "Memory hygiene");
110
+ const decayed = [];
111
+ for (const claim of computeHygiene(repoId, unusedDays).stale) {
112
+ if (setClaimStatus(repoId, claim.id, "decayed"))
113
+ decayed.push(claim.id);
114
+ }
115
+ return { decayed };
116
+ }
117
+ export function mergeDuplicate(repoId, keepId, dropId) {
118
+ requireFeature(FEATURE_HYGIENE, "Memory hygiene");
119
+ const keep = getClaim(repoId, keepId);
120
+ const drop = getClaim(repoId, dropId);
121
+ if (!keep || !drop)
122
+ throw new Error("Both claims must exist to merge");
123
+ if (keep.id === drop.id)
124
+ throw new Error("Cannot merge a claim into itself");
125
+ const anchors = [...new Set([...parseAnchors(keep.code_anchors), ...parseAnchors(drop.code_anchors)])];
126
+ const proposal = applySupersedes({
127
+ claims: [
128
+ {
129
+ id: keep.id,
130
+ kind: keep.kind,
131
+ text: keep.text,
132
+ code_anchors: anchors,
133
+ supersedes: [drop.id],
134
+ },
135
+ ],
136
+ }, [drop.id]);
137
+ applyProposal(repoId, proposal);
138
+ return { keepId: keep.id, dropId: drop.id };
139
+ }
140
+ /** Decay unused + merge near-duplicates in one step (Pro/IT). */
141
+ export function acceptSafeCleanups(repoId, unusedDays = 90) {
142
+ requireFeature(FEATURE_HYGIENE, "Memory hygiene");
143
+ const report = computeHygiene(repoId, unusedDays);
144
+ const decayed = [];
145
+ for (const claim of report.stale) {
146
+ if (setClaimStatus(repoId, claim.id, "decayed"))
147
+ decayed.push(claim.id);
148
+ }
149
+ const decayedSet = new Set(decayed);
150
+ const gone = new Set(decayed);
151
+ const merged = [];
152
+ const ordered = [...report.duplicates].sort((a, b) => b.similarity - a.similarity);
153
+ for (const d of ordered) {
154
+ if (gone.has(d.keepId) || gone.has(d.dropId))
155
+ continue;
156
+ if (decayedSet.has(d.keepId) || decayedSet.has(d.dropId))
157
+ continue;
158
+ try {
159
+ mergeDuplicate(repoId, d.keepId, d.dropId);
160
+ merged.push(d);
161
+ gone.add(d.dropId);
162
+ }
163
+ catch {
164
+ // claim may have been removed mid-loop
165
+ }
166
+ }
167
+ return { decayed, merged };
168
+ }
169
+ /** Scheduled job: clean every tracked repo when licensed. */
170
+ export function runScheduledHygiene(unusedDays = 90) {
171
+ if (!hasFeature(FEATURE_HYGIENE)) {
172
+ return { skipped: true, reason: "Pro/IT license required for hygiene", repos: [] };
173
+ }
174
+ const repos = [];
175
+ for (const repo of listRepos()) {
176
+ try {
177
+ const result = acceptSafeCleanups(repo.id, unusedDays);
178
+ repos.push({
179
+ repoId: repo.id,
180
+ name: repo.repo_name,
181
+ decayed: result.decayed.length,
182
+ merged: result.merged.length,
183
+ });
184
+ }
185
+ catch (error) {
186
+ repos.push({
187
+ repoId: repo.id,
188
+ name: repo.repo_name,
189
+ decayed: 0,
190
+ merged: 0,
191
+ error: error instanceof Error ? error.message : String(error),
192
+ });
193
+ }
194
+ }
195
+ return { repos };
196
+ }
@@ -0,0 +1,6 @@
1
+ export type InstallResult = {
2
+ skills: string[];
3
+ settingsPath?: string;
4
+ };
5
+ export declare function installClaude(_repoRoot: string): InstallResult;
6
+ export declare function claudeInstallHealth(): string[];
@@ -0,0 +1,69 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { copyBundledSkills, readJsonObject, resolveAmemBin, writeJson } from "./skills.js";
5
+ function claudeHome() {
6
+ return process.env.CLAUDE_HOME ?? join(homedir(), ".claude");
7
+ }
8
+ function mergeClaudeHooks(settingsPath) {
9
+ const amem = resolveAmemBin();
10
+ const settings = readJsonObject(settingsPath);
11
+ const hooks = settings.hooks ?? {};
12
+ const track = {
13
+ type: "command",
14
+ command: `${amem} session touch --platform claude`,
15
+ };
16
+ const fullHook = {
17
+ type: "command",
18
+ command: `${amem} hook`,
19
+ };
20
+ const ensureEvent = (name, entry) => {
21
+ const existing = Array.isArray(hooks[name]) ? hooks[name] : [];
22
+ const filtered = existing.filter((row) => {
23
+ if (!row || typeof row !== "object")
24
+ return true;
25
+ const matcher = row;
26
+ const cmds = matcher.hooks ?? [];
27
+ return !cmds.some((h) => (h.command ?? "").includes("amem session touch") ||
28
+ (h.command ?? "").includes("amem hook"));
29
+ });
30
+ hooks[name] = [
31
+ ...filtered,
32
+ {
33
+ matcher: "",
34
+ hooks: [entry],
35
+ },
36
+ ];
37
+ };
38
+ // Full hook pipeline (inject + notes + drafts). Keep Stop on the same path as Cursor.
39
+ ensureEvent("UserPromptSubmit", fullHook);
40
+ ensureEvent("Stop", fullHook);
41
+ // Lightweight session touch remains useful if UserPromptSubmit payload is sparse
42
+ ensureEvent("Notification", track);
43
+ settings.hooks = hooks;
44
+ writeJson(settingsPath, settings);
45
+ return settingsPath;
46
+ }
47
+ export function installClaude(_repoRoot) {
48
+ const skills = copyBundledSkills(join(claudeHome(), "skills"));
49
+ const settingsPath = join(claudeHome(), "settings.json");
50
+ mergeClaudeHooks(settingsPath);
51
+ return { skills, settingsPath };
52
+ }
53
+ export function claudeInstallHealth() {
54
+ const issues = [];
55
+ const skill = join(claudeHome(), "skills", "amem-bootstrap", "SKILL.md");
56
+ if (!existsSync(skill))
57
+ issues.push(`Missing Claude skill: ${skill}`);
58
+ const settingsPath = join(claudeHome(), "settings.json");
59
+ if (!existsSync(settingsPath)) {
60
+ issues.push(`Missing Claude settings: ${settingsPath}`);
61
+ }
62
+ else {
63
+ const raw = JSON.stringify(readJsonObject(settingsPath));
64
+ if (!raw.includes("amem hook") && !/cli\.js\s+hook/.test(raw)) {
65
+ issues.push("Claude settings.json does not call `amem hook` — re-run amem init --platform claude");
66
+ }
67
+ }
68
+ return issues;
69
+ }
@@ -0,0 +1,7 @@
1
+ export type InstallResult = {
2
+ skills: string[];
3
+ rulePath?: string;
4
+ hooksPath?: string;
5
+ };
6
+ export declare function installCursor(repoRoot: string): InstallResult;
7
+ export declare function cursorInstallHealth(repoRoot: string): string[];
@@ -0,0 +1,80 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { copyBundledSkills, readJsonObject, resolveAmemBin, templatePath, writeJson, } from "./skills.js";
5
+ import { readFileSync } from "node:fs";
6
+ function cursorHome() {
7
+ return process.env.CURSOR_HOME ?? join(homedir(), ".cursor");
8
+ }
9
+ function writeCursorRule(repoRoot) {
10
+ const rulesDir = join(repoRoot, ".cursor", "rules");
11
+ mkdirSync(rulesDir, { recursive: true });
12
+ let rulePath = join(rulesDir, "amem.mdc");
13
+ if (existsSync(rulePath)) {
14
+ const existing = readFileSync(rulePath, "utf8");
15
+ if (!existing.includes("Generated by amem")) {
16
+ rulePath = join(rulesDir, "amem-1.mdc");
17
+ }
18
+ }
19
+ const template = readFileSync(templatePath("cursor-rule.mdc"), "utf8");
20
+ writeFileSync(rulePath, template, "utf8");
21
+ return rulePath;
22
+ }
23
+ function writeCursorHooks() {
24
+ const hooksPath = join(cursorHome(), "hooks.json");
25
+ const amem = resolveAmemBin();
26
+ const existing = readJsonObject(hooksPath);
27
+ existing.version = 1;
28
+ const hooks = existing.hooks ?? {};
29
+ const hookCmd = `${amem} hook`;
30
+ const entry = {
31
+ command: hookCmd,
32
+ description: "amem: inject and store local memory (Generated by amem)",
33
+ };
34
+ const filterAmem = (entries) => entries.filter((e) => {
35
+ if (!e || typeof e !== "object")
36
+ return true;
37
+ const desc = e.description ?? "";
38
+ const command = e.command ?? "";
39
+ return !desc.includes("Generated by amem") && !command.includes("amem session touch") && !command.includes("amem hook");
40
+ });
41
+ for (const event of ["sessionStart", "beforeSubmitPrompt", "afterAgentResponse", "stop", "sessionEnd"]) {
42
+ const current = Array.isArray(hooks[event]) ? hooks[event] : [];
43
+ hooks[event] = [...filterAmem(current), entry];
44
+ }
45
+ existing.hooks = hooks;
46
+ writeJson(hooksPath, existing);
47
+ return hooksPath;
48
+ }
49
+ export function installCursor(repoRoot) {
50
+ const skills = copyBundledSkills(join(cursorHome(), "skills"));
51
+ const rulePath = writeCursorRule(repoRoot);
52
+ const hooksPath = writeCursorHooks();
53
+ return { skills, rulePath, hooksPath };
54
+ }
55
+ export function cursorInstallHealth(repoRoot) {
56
+ const issues = [];
57
+ const skill = join(cursorHome(), "skills", "amem-bootstrap", "SKILL.md");
58
+ if (!existsSync(skill))
59
+ issues.push(`Missing Cursor skill: ${skill}`);
60
+ const rule = join(repoRoot, ".cursor", "rules", "amem.mdc");
61
+ const ruleAlt = join(repoRoot, ".cursor", "rules", "amem-1.mdc");
62
+ if (!existsSync(rule) && !existsSync(ruleAlt)) {
63
+ issues.push(`Missing Cursor rule under ${join(repoRoot, ".cursor", "rules")}`);
64
+ }
65
+ const hooksPath = join(cursorHome(), "hooks.json");
66
+ if (!existsSync(hooksPath)) {
67
+ issues.push(`Missing Cursor hooks: ${hooksPath}`);
68
+ }
69
+ else {
70
+ const raw = readFileSync(hooksPath, "utf8");
71
+ // PATH install: `…/amem hook`; linked-from-source: `node …/cli.js hook`
72
+ const hasHook = raw.includes("amem hook") ||
73
+ /cli\.js\s+hook/.test(raw) ||
74
+ raw.includes("Generated by amem");
75
+ if (!hasHook) {
76
+ issues.push("Cursor hooks.json does not call `amem hook` — re-run amem init --platform cursor");
77
+ }
78
+ }
79
+ return issues;
80
+ }
@@ -0,0 +1,21 @@
1
+ export type HostInstallResult = {
2
+ host: string;
3
+ paths: string[];
4
+ notes: string[];
5
+ };
6
+ /** Windsurf: MCP in ~/.codeium/windsurf/mcp_config.json (common layout). */
7
+ export declare function installWindsurf(workspace?: string): HostInstallResult;
8
+ /** Continue.dev: ~/.continue/config.json plus mcpServers/amem.yaml (1.x HTTP). */
9
+ export declare function installContinue(workspace?: string): HostInstallResult;
10
+ /** Aider: drop a small CONVENTIONS hint + shell helper (Aider has no MCP). */
11
+ export declare function installAider(repoRoot: string): HostInstallResult;
12
+ /** Zed: settings.json context_servers / mcp style. */
13
+ export declare function installZed(workspace?: string): HostInstallResult;
14
+ export declare function continueInstallHealth(): string[];
15
+ export declare function zedInstallHealth(): string[];
16
+ export declare function windsurfInstallHealth(): string[];
17
+ export declare function hostInstallHealth(host: string): string[];
18
+ export declare function installHost(host: string, opts?: {
19
+ repoRoot?: string;
20
+ workspace?: string;
21
+ }): HostInstallResult;
@@ -0,0 +1,186 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { mcpClientConfig } from "../mcp.js";
5
+ import { resolveAmemBin, writeJson, readJsonObject } from "./skills.js";
6
+ function ensureDir(path) {
7
+ mkdirSync(path, { recursive: true });
8
+ }
9
+ /** Windsurf: MCP in ~/.codeium/windsurf/mcp_config.json (common layout). */
10
+ export function installWindsurf(workspace = "personal") {
11
+ const dir = join(homedir(), ".codeium", "windsurf");
12
+ ensureDir(dir);
13
+ const path = join(dir, "mcp_config.json");
14
+ const existing = readJsonObject(path);
15
+ const servers = existing.mcpServers ?? {};
16
+ const cfg = mcpClientConfig(workspace);
17
+ servers.amem = {
18
+ url: cfg.http.url,
19
+ };
20
+ writeJson(path, { ...existing, mcpServers: servers });
21
+ return {
22
+ host: "windsurf",
23
+ paths: [path],
24
+ notes: [`MCP server amem → ${cfg.http.url}. Keep amem ui running. Restart Windsurf if needed.`],
25
+ };
26
+ }
27
+ function continueMcpYamlPath() {
28
+ return join(homedir(), ".continue", "mcpServers", "amem.yaml");
29
+ }
30
+ /** Continue.dev: ~/.continue/config.json plus mcpServers/amem.yaml (1.x HTTP). */
31
+ export function installContinue(workspace = "personal") {
32
+ const dir = join(homedir(), ".continue");
33
+ ensureDir(dir);
34
+ ensureDir(join(dir, "mcpServers"));
35
+ const path = join(dir, "config.json");
36
+ const existing = existsSync(path) ? readJsonObject(path) : { models: [] };
37
+ const experimental = existing.experimental ?? {};
38
+ const modelContext = experimental.modelContextProtocolServers ?? [];
39
+ const filtered = modelContext.filter((e) => {
40
+ if (!e || typeof e !== "object")
41
+ return true;
42
+ const name = e.name;
43
+ return name !== "amem";
44
+ });
45
+ const cfg = mcpClientConfig(workspace);
46
+ filtered.push({
47
+ name: "amem",
48
+ url: cfg.http.url,
49
+ });
50
+ experimental.modelContextProtocolServers = filtered;
51
+ writeJson(path, { ...existing, experimental });
52
+ const yaml = continueMcpYamlPath();
53
+ writeFileSync(yaml, `name: amem
54
+ type: sse
55
+ url: ${cfg.http.url}
56
+ `, "utf8");
57
+ return {
58
+ host: "continue",
59
+ paths: [path, yaml],
60
+ notes: [
61
+ `Continue MCP amem → ${cfg.http.url}. Keep amem ui running.`,
62
+ `Drop-in: ${yaml}`,
63
+ ],
64
+ };
65
+ }
66
+ /** Aider: drop a small CONVENTIONS hint + shell helper (Aider has no MCP). */
67
+ export function installAider(repoRoot) {
68
+ const path = join(repoRoot, ".aider.amem.md");
69
+ const body = `# amem + Aider
70
+
71
+ Before large exploration, run:
72
+
73
+ \`\`\`bash
74
+ amem context "\${user question}"
75
+ \`\`\`
76
+
77
+ After durable discoveries:
78
+
79
+ \`\`\`bash
80
+ amem remember "…" --anchor path/to/file
81
+ \`\`\`
82
+
83
+ Memory stays in ~/.amem (personal, local).
84
+ `;
85
+ writeFileSync(path, body, "utf8");
86
+ return {
87
+ host: "aider",
88
+ paths: [path],
89
+ notes: [
90
+ "Aider has no MCP — added .aider.amem.md with CLI hints. Consider: aider --read .aider.amem.md",
91
+ ],
92
+ };
93
+ }
94
+ /** Zed: settings.json context_servers / mcp style. */
95
+ export function installZed(workspace = "personal") {
96
+ const dir = process.platform === "darwin"
97
+ ? join(homedir(), "Library", "Application Support", "Zed")
98
+ : join(homedir(), ".config", "zed");
99
+ ensureDir(dir);
100
+ const path = join(dir, "settings.json");
101
+ const existing = existsSync(path) ? readJsonObject(path) : {};
102
+ const servers = existing.context_servers ?? {};
103
+ const amem = resolveAmemBin();
104
+ const cfg = mcpClientConfig(workspace);
105
+ servers.amem = {
106
+ source: "custom",
107
+ command: amem.split(/\s+/)[0],
108
+ args: amem.split(/\s+/).slice(1).concat(["mcp", "--workspace", workspace]),
109
+ env: {},
110
+ url: cfg.http.url,
111
+ };
112
+ writeJson(path, {
113
+ ...existing,
114
+ context_servers: servers,
115
+ amem_http_mcp: cfg.http,
116
+ });
117
+ return {
118
+ host: "zed",
119
+ paths: [path],
120
+ notes: [
121
+ `Zed context_servers.amem → ${cfg.http.url} (HTTP while amem ui runs; command is the fallback).`,
122
+ ],
123
+ };
124
+ }
125
+ function fileMentionsAmem(path) {
126
+ if (!existsSync(path))
127
+ return false;
128
+ try {
129
+ return readFileSync(path, "utf8").includes("amem");
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ }
135
+ export function continueInstallHealth() {
136
+ const json = join(homedir(), ".continue", "config.json");
137
+ const yaml = continueMcpYamlPath();
138
+ if (!fileMentionsAmem(json) && !fileMentionsAmem(yaml)) {
139
+ return ["Continue has no amem MCP entry — run amem init --platform continue"];
140
+ }
141
+ return [];
142
+ }
143
+ export function zedInstallHealth() {
144
+ const dir = process.platform === "darwin"
145
+ ? join(homedir(), "Library", "Application Support", "Zed")
146
+ : join(homedir(), ".config", "zed");
147
+ const path = join(dir, "settings.json");
148
+ if (!fileMentionsAmem(path)) {
149
+ return ["Zed settings have no amem context server — run amem init --platform zed"];
150
+ }
151
+ return [];
152
+ }
153
+ export function windsurfInstallHealth() {
154
+ const path = join(homedir(), ".codeium", "windsurf", "mcp_config.json");
155
+ if (!fileMentionsAmem(path)) {
156
+ return ["Windsurf has no amem MCP entry — run amem init --platform windsurf"];
157
+ }
158
+ return [];
159
+ }
160
+ export function hostInstallHealth(host) {
161
+ switch (host) {
162
+ case "continue":
163
+ return continueInstallHealth();
164
+ case "zed":
165
+ return zedInstallHealth();
166
+ case "windsurf":
167
+ return windsurfInstallHealth();
168
+ default:
169
+ return [];
170
+ }
171
+ }
172
+ export function installHost(host, opts = {}) {
173
+ const ws = opts.workspace || "personal";
174
+ switch (host) {
175
+ case "windsurf":
176
+ return installWindsurf(ws);
177
+ case "continue":
178
+ return installContinue(ws);
179
+ case "aider":
180
+ return installAider(opts.repoRoot || process.cwd());
181
+ case "zed":
182
+ return installZed(ws);
183
+ default:
184
+ throw new Error(`No thin installer for host: ${host}`);
185
+ }
186
+ }
@@ -0,0 +1,8 @@
1
+ export declare function getPackageRoot(): string;
2
+ export declare function skillsSourceDir(): string;
3
+ export declare function templatePath(...parts: string[]): string;
4
+ export declare function copyBundledSkills(targetSkillsDir: string): string[];
5
+ export declare function readJsonObject(path: string): Record<string, unknown>;
6
+ export declare function writeJson(path: string, value: unknown): void;
7
+ export declare function resolveAmemBin(): string;
8
+ export declare function resolveAmemProgramArgs(...extra: string[]): string[];
@@ -0,0 +1,67 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ // dist/install/skills.js -> package root is ../..
6
+ const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
7
+ export function getPackageRoot() {
8
+ return packageRoot;
9
+ }
10
+ export function skillsSourceDir() {
11
+ return join(packageRoot, "skills");
12
+ }
13
+ export function templatePath(...parts) {
14
+ return join(packageRoot, "templates", ...parts);
15
+ }
16
+ export function copyBundledSkills(targetSkillsDir) {
17
+ mkdirSync(targetSkillsDir, { recursive: true });
18
+ const source = skillsSourceDir();
19
+ const installed = [];
20
+ for (const name of ["amem-bootstrap", "amem-update-working-memory"]) {
21
+ const from = join(source, name);
22
+ const to = join(targetSkillsDir, name);
23
+ if (!existsSync(from)) {
24
+ throw new Error(`Missing bundled skill: ${from}`);
25
+ }
26
+ cpSync(from, to, { recursive: true });
27
+ installed.push(to);
28
+ }
29
+ return installed;
30
+ }
31
+ export function readJsonObject(path) {
32
+ if (!existsSync(path))
33
+ return {};
34
+ try {
35
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
36
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
37
+ return parsed;
38
+ }
39
+ }
40
+ catch {
41
+ // replace corrupt file carefully by returning empty and letting caller rewrite
42
+ }
43
+ return {};
44
+ }
45
+ export function writeJson(path, value) {
46
+ mkdirSync(dirname(path), { recursive: true });
47
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
48
+ }
49
+ export function resolveAmemBin() {
50
+ const args = resolveAmemProgramArgs();
51
+ return args.length === 1 ? args[0] : args.map((a) => (a.includes(" ") ? JSON.stringify(a) : a)).join(" ");
52
+ }
53
+ export function resolveAmemProgramArgs(...extra) {
54
+ try {
55
+ const found = execFileSync("which", ["amem"], {
56
+ encoding: "utf8",
57
+ stdio: ["ignore", "pipe", "ignore"],
58
+ }).trim();
59
+ if (found)
60
+ return [found, ...extra];
61
+ }
62
+ catch {
63
+ // fall through
64
+ }
65
+ const cli = join(getPackageRoot(), "dist", "cli.js");
66
+ return [process.execPath, cli, ...extra];
67
+ }
@@ -0,0 +1,20 @@
1
+ export type SbomComponent = {
2
+ name: string;
3
+ version: string;
4
+ type: "application" | "library";
5
+ };
6
+ export type Sbom = {
7
+ bomFormat: "CycloneDX";
8
+ specVersion: "1.5";
9
+ serialNumber: string;
10
+ metadata: {
11
+ timestamp: string;
12
+ component: SbomComponent;
13
+ };
14
+ components: SbomComponent[];
15
+ };
16
+ export declare function buildSbom(): Sbom;
17
+ export declare function writeItPack(outDir: string): {
18
+ dir: string;
19
+ files: string[];
20
+ };