@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,376 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { listClaims, listComponents, listEdges, listFlows, nowIso, openDb, reindexClaimsSearch, } from "./db.js";
3
+ import { compiledDenyPatterns } from "./policy.js";
4
+ import { newId } from "./repo-identity.js";
5
+ import { parseAnchors } from "./freshness.js";
6
+ import { tokenJaccard } from "./search.js";
7
+ const OBJECT_TYPES = new Set(["claim", "flow", "component"]);
8
+ const CONFLICT_JACCARD = 0.45;
9
+ export function parseProposalJson(raw) {
10
+ const data = JSON.parse(raw);
11
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
12
+ throw new Error("Proposal must be a JSON object");
13
+ }
14
+ return data;
15
+ }
16
+ function normalizeSupersedes(claim) {
17
+ if (!claim.supersedes)
18
+ return [];
19
+ return [...new Set(claim.supersedes.filter((id) => typeof id === "string" && id.trim()))];
20
+ }
21
+ /** Collect explicit supersede targets from claims + supersedes edges. */
22
+ export function collectSupersedeTargets(proposal) {
23
+ // oldId -> newId
24
+ const map = new Map();
25
+ for (const claim of proposal.claims ?? []) {
26
+ for (const oldId of normalizeSupersedes(claim)) {
27
+ if (oldId !== claim.id)
28
+ map.set(oldId, claim.id);
29
+ }
30
+ }
31
+ for (const edge of proposal.edges ?? []) {
32
+ if (edge.kind === "supersedes" && edge.from_type === "claim" && edge.to_type === "claim") {
33
+ if (edge.from_id && edge.to_id && edge.from_id !== edge.to_id) {
34
+ map.set(edge.to_id, edge.from_id);
35
+ }
36
+ }
37
+ }
38
+ return map;
39
+ }
40
+ function shareAnchor(a, b) {
41
+ const set = new Set(a);
42
+ return b.some((x) => set.has(x));
43
+ }
44
+ /**
45
+ * Likely conflict: share ≥1 code_anchor and high text overlap, different ids,
46
+ * and the older claim is not explicitly superseded by this proposal.
47
+ */
48
+ export function findClaimConflicts(proposal, existingActive) {
49
+ const conflicts = [];
50
+ const supersedeTargets = collectSupersedeTargets(proposal);
51
+ const incomingIds = new Set((proposal.claims ?? []).map((c) => c.id));
52
+ for (const claim of proposal.claims ?? []) {
53
+ const anchors = claim.code_anchors ?? [];
54
+ if (anchors.length === 0 || !claim.text)
55
+ continue;
56
+ for (const other of existingActive) {
57
+ if (other.id === claim.id)
58
+ continue;
59
+ if (incomingIds.has(other.id) && (proposal.claims ?? []).some((c) => c.id === other.id)) {
60
+ continue;
61
+ }
62
+ if (supersedeTargets.get(other.id) === claim.id)
63
+ continue;
64
+ if (supersedeTargets.has(other.id))
65
+ continue;
66
+ const otherAnchors = parseAnchors(other.code_anchors);
67
+ if (!shareAnchor(anchors, otherAnchors))
68
+ continue;
69
+ const sim = tokenJaccard(claim.text, other.text);
70
+ if (sim >= CONFLICT_JACCARD) {
71
+ conflicts.push({
72
+ claimId: claim.id,
73
+ otherId: other.id,
74
+ otherText: other.text,
75
+ similarity: sim,
76
+ sharedAnchors: anchors.filter((a) => otherAnchors.includes(a)),
77
+ withinProposal: false,
78
+ });
79
+ }
80
+ }
81
+ for (const peer of proposal.claims ?? []) {
82
+ if (peer.id <= claim.id)
83
+ continue;
84
+ const peerAnchors = peer.code_anchors ?? [];
85
+ if (!shareAnchor(anchors, peerAnchors))
86
+ continue;
87
+ if (normalizeSupersedes(claim).includes(peer.id) || normalizeSupersedes(peer).includes(claim.id)) {
88
+ continue;
89
+ }
90
+ const sim = tokenJaccard(claim.text, peer.text ?? "");
91
+ if (sim >= CONFLICT_JACCARD) {
92
+ conflicts.push({
93
+ claimId: claim.id,
94
+ otherId: peer.id,
95
+ otherText: peer.text ?? "",
96
+ similarity: sim,
97
+ sharedAnchors: anchors.filter((a) => peerAnchors.includes(a)),
98
+ withinProposal: true,
99
+ });
100
+ }
101
+ }
102
+ }
103
+ return conflicts;
104
+ }
105
+ export function findConflictWarnings(proposal, existingActive) {
106
+ return findClaimConflicts(proposal, existingActive).map((c) => c.withinProposal
107
+ ? `claims ${c.claimId} and ${c.otherId} look conflicting in the same proposal — set supersedes on the winner.`
108
+ : `claim ${c.claimId} may conflict with active ${c.otherId} (shared anchors, text similarity ${(c.similarity * 100).toFixed(0)}%). Add "supersedes": ["${c.otherId}"] if this replaces it.`);
109
+ }
110
+ /** Attach supersede targets to the first claim (Memory “replace older facts”). */
111
+ export function applySupersedes(proposal, otherIds) {
112
+ const extra = [...new Set(otherIds.filter((id) => id.trim()))];
113
+ if (extra.length === 0)
114
+ return proposal;
115
+ const claims = (proposal.claims ?? []).map((claim, index) => {
116
+ if (index !== 0)
117
+ return claim;
118
+ return {
119
+ ...claim,
120
+ supersedes: [...new Set([...normalizeSupersedes(claim), ...extra.filter((id) => id !== claim.id)])],
121
+ };
122
+ });
123
+ return { ...proposal, claims };
124
+ }
125
+ export function validateProposal(proposal, policy, opts = {}) {
126
+ const errors = [];
127
+ const warnings = [];
128
+ const componentIds = new Set();
129
+ const flowIds = new Set();
130
+ const claimIds = new Set();
131
+ for (const c of proposal.components ?? []) {
132
+ if (!c.id || !c.name)
133
+ errors.push("component requires id and name");
134
+ else
135
+ componentIds.add(c.id);
136
+ }
137
+ for (const f of proposal.flows ?? []) {
138
+ if (!f.id || !f.name)
139
+ errors.push("flow requires id and name");
140
+ else
141
+ flowIds.add(f.id);
142
+ }
143
+ for (const claim of proposal.claims ?? []) {
144
+ if (!claim.id || !claim.kind || !claim.text) {
145
+ errors.push(`claim ${claim.id ?? "(missing id)"} requires id, kind, and text`);
146
+ }
147
+ else {
148
+ claimIds.add(claim.id);
149
+ if (!claim.code_anchors || claim.code_anchors.length === 0) {
150
+ errors.push(`claim ${claim.id} should include at least one code_anchor`);
151
+ }
152
+ for (const oldId of normalizeSupersedes(claim)) {
153
+ if (oldId === claim.id) {
154
+ errors.push(`claim ${claim.id} cannot supersede itself`);
155
+ }
156
+ }
157
+ }
158
+ }
159
+ for (const edge of proposal.edges ?? []) {
160
+ if (!OBJECT_TYPES.has(edge.from_type) || !OBJECT_TYPES.has(edge.to_type)) {
161
+ errors.push(`edge has invalid types: ${edge.from_type} -> ${edge.to_type}`);
162
+ continue;
163
+ }
164
+ if (!edge.from_id || !edge.to_id || !edge.kind) {
165
+ errors.push("edge requires from_id, to_id, and kind");
166
+ }
167
+ }
168
+ if ((proposal.components?.length ?? 0) === 0 &&
169
+ (proposal.flows?.length ?? 0) === 0 &&
170
+ (proposal.claims?.length ?? 0) === 0 &&
171
+ (proposal.edges?.length ?? 0) === 0) {
172
+ errors.push("proposal is empty");
173
+ }
174
+ errors.push(...checkProposalAgainstPolicy(proposal, policy));
175
+ const existing = opts.existingClaims ?? [];
176
+ const activeExisting = existing.filter((c) => (c.status ?? "active") === "active");
177
+ warnings.push(...findConflictWarnings(proposal, activeExisting));
178
+ // dangling supersedes targets (informational)
179
+ const existingIds = new Set(existing.map((c) => c.id));
180
+ for (const claim of proposal.claims ?? []) {
181
+ for (const oldId of normalizeSupersedes(claim)) {
182
+ if (!existingIds.has(oldId) && !claimIds.has(oldId)) {
183
+ warnings.push(`claim ${claim.id} supersedes unknown id ${oldId} (will no-op if missing at apply)`);
184
+ }
185
+ }
186
+ }
187
+ return { ok: errors.length === 0, errors, warnings, proposal };
188
+ }
189
+ /** Block claims that look like secrets / credentials (builtin + policy patterns). */
190
+ export function checkProposalAgainstPolicy(proposal, policy) {
191
+ const errors = [];
192
+ let patterns;
193
+ try {
194
+ patterns = compiledDenyPatterns(policy);
195
+ }
196
+ catch (err) {
197
+ return [err instanceof Error ? err.message : String(err)];
198
+ }
199
+ for (const claim of proposal.claims ?? []) {
200
+ const haystacks = [
201
+ claim.id ?? "",
202
+ claim.text ?? "",
203
+ claim.source_ref ?? "",
204
+ ...(claim.code_anchors ?? []),
205
+ ];
206
+ for (const re of patterns) {
207
+ for (const text of haystacks) {
208
+ if (text && re.test(text)) {
209
+ errors.push(`claim ${claim.id ?? "(missing id)"} blocked by deny_claim_patterns /${re.source}/i`);
210
+ break;
211
+ }
212
+ }
213
+ }
214
+ }
215
+ return errors;
216
+ }
217
+ export function loadProposalFile(path) {
218
+ return parseProposalJson(readFileSync(path, "utf8"));
219
+ }
220
+ function anchorsEqual(a, b) {
221
+ let left = [];
222
+ try {
223
+ left = a ? JSON.parse(a) : [];
224
+ }
225
+ catch {
226
+ left = [];
227
+ }
228
+ const right = b ?? [];
229
+ if (left.length !== right.length)
230
+ return false;
231
+ const s = new Set(left);
232
+ return right.every((x) => s.has(x));
233
+ }
234
+ /** Preview what apply would change against current active (+ superseded targets). */
235
+ export function diffProposal(repoId, proposal) {
236
+ const existing = listClaims(repoId, { includeSuperseded: true });
237
+ const byId = new Map(existing.map((c) => [c.id, c]));
238
+ const components = listComponents(repoId);
239
+ const flows = listFlows(repoId);
240
+ const componentIds = new Set(components.map((c) => c.id));
241
+ const flowIds = new Set(flows.map((f) => f.id));
242
+ const claimsAdded = [];
243
+ const claimsUpdated = [];
244
+ const claimsUnchanged = [];
245
+ for (const claim of proposal.claims ?? []) {
246
+ const prior = byId.get(claim.id);
247
+ if (!prior) {
248
+ claimsAdded.push(claim.id);
249
+ continue;
250
+ }
251
+ const same = prior.text === claim.text &&
252
+ prior.kind === claim.kind &&
253
+ anchorsEqual(prior.code_anchors, claim.code_anchors) &&
254
+ (prior.status ?? "active") === "active";
255
+ if (same)
256
+ claimsUnchanged.push(claim.id);
257
+ else
258
+ claimsUpdated.push(claim.id);
259
+ }
260
+ const willSupersede = [...collectSupersedeTargets(proposal).keys()].filter((id) => {
261
+ const row = byId.get(id);
262
+ return row && (row.status ?? "active") === "active";
263
+ });
264
+ return {
265
+ claimsAdded,
266
+ claimsUpdated,
267
+ claimsUnchanged,
268
+ willSupersede,
269
+ componentsAdded: (proposal.components ?? [])
270
+ .map((c) => c.id)
271
+ .filter((id) => !componentIds.has(id)),
272
+ flowsAdded: (proposal.flows ?? []).map((f) => f.id).filter((id) => !flowIds.has(id)),
273
+ };
274
+ }
275
+ export function formatProposalDiff(diff) {
276
+ const lines = ["Diff:"];
277
+ const push = (label, ids) => {
278
+ if (!ids.length)
279
+ return;
280
+ lines.push(`- ${label}: ${ids.map((id) => `\`${id}\``).join(", ")}`);
281
+ };
282
+ push("add claims", diff.claimsAdded);
283
+ push("update claims", diff.claimsUpdated);
284
+ push("unchanged claims", diff.claimsUnchanged);
285
+ push("supersede", diff.willSupersede);
286
+ push("add components", diff.componentsAdded);
287
+ push("add flows", diff.flowsAdded);
288
+ if (lines.length === 1)
289
+ lines.push("- (no claim/component/flow changes detected)");
290
+ return lines.join("\n");
291
+ }
292
+ export function applyProposal(repoId, proposal, policy) {
293
+ const existing = listClaims(repoId, { includeSuperseded: true });
294
+ const validated = validateProposal(proposal, policy, { existingClaims: existing });
295
+ if (!validated.ok) {
296
+ throw new Error(`Invalid proposal:\n- ${validated.errors.join("\n- ")}`);
297
+ }
298
+ const db = openDb();
299
+ const ts = nowIso();
300
+ let components = 0;
301
+ let flows = 0;
302
+ let claims = 0;
303
+ let edges = 0;
304
+ let superseded = 0;
305
+ const upsertComponent = db.prepare(`INSERT INTO components (repo_id, id, name, code_anchor)
306
+ VALUES (?, ?, ?, ?)
307
+ ON CONFLICT(repo_id, id) DO UPDATE SET
308
+ name = excluded.name,
309
+ code_anchor = excluded.code_anchor`);
310
+ const upsertFlow = db.prepare(`INSERT INTO flows (repo_id, id, name)
311
+ VALUES (?, ?, ?)
312
+ ON CONFLICT(repo_id, id) DO UPDATE SET name = excluded.name`);
313
+ const upsertClaim = db.prepare(`INSERT INTO claims (repo_id, id, kind, text, code_anchors, source_ref, created_at, updated_at, status, superseded_by, pinned)
314
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active', NULL, 0)
315
+ ON CONFLICT(repo_id, id) DO UPDATE SET
316
+ kind = excluded.kind,
317
+ text = excluded.text,
318
+ code_anchors = excluded.code_anchors,
319
+ source_ref = COALESCE(excluded.source_ref, claims.source_ref),
320
+ updated_at = excluded.updated_at,
321
+ status = 'active',
322
+ superseded_by = NULL`);
323
+ const markSuperseded = db.prepare(`UPDATE claims SET status = 'superseded', superseded_by = ?, updated_at = ?
324
+ WHERE repo_id = ? AND id = ? AND id != ?`);
325
+ const upsertEdge = db.prepare(`INSERT INTO edges (repo_id, id, from_id, from_type, to_id, to_type, kind)
326
+ VALUES (?, ?, ?, ?, ?, ?, ?)
327
+ ON CONFLICT(repo_id, id) DO UPDATE SET
328
+ from_id = excluded.from_id,
329
+ from_type = excluded.from_type,
330
+ to_id = excluded.to_id,
331
+ to_type = excluded.to_type,
332
+ kind = excluded.kind`);
333
+ const supersedeMap = collectSupersedeTargets(proposal);
334
+ db.exec("BEGIN");
335
+ try {
336
+ for (const c of proposal.components ?? []) {
337
+ upsertComponent.run(repoId, c.id, c.name, c.code_anchor ?? null);
338
+ components += 1;
339
+ }
340
+ for (const f of proposal.flows ?? []) {
341
+ upsertFlow.run(repoId, f.id, f.name);
342
+ flows += 1;
343
+ }
344
+ for (const claim of proposal.claims ?? []) {
345
+ const prior = db
346
+ .prepare("SELECT created_at FROM claims WHERE repo_id = ? AND id = ?")
347
+ .get(repoId, claim.id);
348
+ upsertClaim.run(repoId, claim.id, claim.kind, claim.text, JSON.stringify(claim.code_anchors ?? []), claim.source_ref ?? null, prior?.created_at ?? ts, ts);
349
+ claims += 1;
350
+ }
351
+ for (const [oldId, newId] of supersedeMap) {
352
+ const result = markSuperseded.run(newId, ts, repoId, oldId, newId);
353
+ superseded += Number(result.changes ?? 0);
354
+ }
355
+ for (const edge of proposal.edges ?? []) {
356
+ const id = edge.id ?? newId("edge");
357
+ upsertEdge.run(repoId, id, edge.from_id, edge.from_type, edge.to_id, edge.to_type, edge.kind);
358
+ edges += 1;
359
+ }
360
+ db.exec("COMMIT");
361
+ }
362
+ catch (err) {
363
+ db.exec("ROLLBACK");
364
+ throw err;
365
+ }
366
+ reindexClaimsSearch(repoId);
367
+ return { components, flows, claims, edges, superseded };
368
+ }
369
+ export function exportRepoMemory(repoId) {
370
+ return {
371
+ components: listComponents(repoId),
372
+ flows: listFlows(repoId),
373
+ claims: listClaims(repoId, { includeSuperseded: true }),
374
+ edges: listEdges(repoId),
375
+ };
376
+ }
@@ -0,0 +1,28 @@
1
+ /** Paths that must be inside the published tarball for `npx @iamem/amem setup`. */
2
+ export declare const REQUIRED_PACK_PATHS: readonly ["dist/cli.js", "dist/mcp.js", "dist/remember-contract.js", "ui-static/index.html", "ui-static/app.js", "ui-static/styles.css", "docs/remember-contract.md", "docs/agent-install-prompt.md", "docs/npm-release.md", "skills/amem-update-working-memory/SKILL.md", "scripts/mdm-offboard.sh"];
3
+ export declare const PUBLISH_PACKAGE_NAME = "@iamem/amem";
4
+ export type PublishReady = {
5
+ root: string;
6
+ name: string;
7
+ version: string;
8
+ bin: string;
9
+ engines: string;
10
+ filesField: string[];
11
+ missing: string[];
12
+ ok: boolean;
13
+ };
14
+ export declare function packageRoot(): string;
15
+ export declare function readPackageJson(root?: string): {
16
+ name?: string;
17
+ version?: string;
18
+ bin?: Record<string, string> | string;
19
+ engines?: {
20
+ node?: string;
21
+ };
22
+ files?: string[];
23
+ publishConfig?: {
24
+ access?: string;
25
+ };
26
+ };
27
+ export declare function inspectPublishReady(root?: string): PublishReady;
28
+ export declare function assertPublishReady(root?: string): PublishReady;
@@ -0,0 +1,57 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /** Paths that must be inside the published tarball for `npx @iamem/amem setup`. */
5
+ export const REQUIRED_PACK_PATHS = [
6
+ "dist/cli.js",
7
+ "dist/mcp.js",
8
+ "dist/remember-contract.js",
9
+ "ui-static/index.html",
10
+ "ui-static/app.js",
11
+ "ui-static/styles.css",
12
+ "docs/remember-contract.md",
13
+ "docs/agent-install-prompt.md",
14
+ "docs/npm-release.md",
15
+ "skills/amem-update-working-memory/SKILL.md",
16
+ "scripts/mdm-offboard.sh",
17
+ ];
18
+ export const PUBLISH_PACKAGE_NAME = "@iamem/amem";
19
+ export function packageRoot() {
20
+ return join(dirname(fileURLToPath(import.meta.url)), "..");
21
+ }
22
+ export function readPackageJson(root = packageRoot()) {
23
+ return JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
24
+ }
25
+ export function inspectPublishReady(root = packageRoot()) {
26
+ const pkg = readPackageJson(root);
27
+ const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.amem ?? "";
28
+ const missing = REQUIRED_PACK_PATHS.filter((rel) => !existsSync(join(root, rel)));
29
+ if (!bin)
30
+ missing.push("package.json#bin.amem");
31
+ if (pkg.publishConfig?.access !== "public")
32
+ missing.push("package.json#publishConfig.access=public");
33
+ if (!pkg.engines?.node)
34
+ missing.push("package.json#engines.node");
35
+ const filesField = pkg.files ?? [];
36
+ for (const required of ["dist", "ui-static", "docs", "skills"]) {
37
+ if (!filesField.includes(required))
38
+ missing.push(`package.json#files missing ${required}`);
39
+ }
40
+ return {
41
+ root,
42
+ name: pkg.name ?? "",
43
+ version: pkg.version ?? "",
44
+ bin,
45
+ engines: pkg.engines?.node ?? "",
46
+ filesField,
47
+ missing,
48
+ ok: missing.length === 0 && pkg.name === PUBLISH_PACKAGE_NAME && Boolean(bin),
49
+ };
50
+ }
51
+ export function assertPublishReady(root = packageRoot()) {
52
+ const info = inspectPublishReady(root);
53
+ if (!info.ok) {
54
+ throw new Error(`Package is not npx-ready:\n${info.missing.map((m) => ` - ${m}`).join("\n")}`);
55
+ }
56
+ return info;
57
+ }
@@ -0,0 +1,23 @@
1
+ /** Generic host recipe: read then write. Not tied to any one MCP client. */
2
+ export declare const REMEMBER_CONTRACT_VERSION = "1";
3
+ export declare const MCP_URL_TEMPLATE = "http://127.0.0.1:7843/mcp?workspace=<slug>";
4
+ export type RememberRule = {
5
+ id: string;
6
+ must: boolean;
7
+ text: string;
8
+ };
9
+ export type RememberContract = {
10
+ version: string;
11
+ title: string;
12
+ mcpUrlTemplate: string;
13
+ tools: {
14
+ name: string;
15
+ role: "read" | "write" | "meta";
16
+ when: string;
17
+ }[];
18
+ rules: RememberRule[];
19
+ markdown: string;
20
+ paste: string;
21
+ };
22
+ export declare function rememberContract(): RememberContract;
23
+ export declare function rememberContractMustIds(): string[];
@@ -0,0 +1,117 @@
1
+ /** Generic host recipe: read then write. Not tied to any one MCP client. */
2
+ export const REMEMBER_CONTRACT_VERSION = "1";
3
+ export const MCP_URL_TEMPLATE = "http://127.0.0.1:7843/mcp?workspace=<slug>";
4
+ const TOOLS = [
5
+ {
6
+ name: "amem_context",
7
+ role: "read",
8
+ when: "Before exploring files or sending a large prompt. Pass workspace=<slug>.",
9
+ },
10
+ {
11
+ name: "amem_remember",
12
+ role: "write",
13
+ when: "After a durable outcome: a decision, constraint, owner, or gotcha that should survive this chat.",
14
+ },
15
+ {
16
+ name: "amem_recipe",
17
+ role: "meta",
18
+ when: "If you are unsure when to read or write — fetch this contract.",
19
+ },
20
+ ];
21
+ const RULES = [
22
+ {
23
+ id: "read-first",
24
+ must: true,
25
+ text: "Call amem_context at the start of a task. Do not treat a successful read as a substitute for writing later.",
26
+ },
27
+ {
28
+ id: "write-outcomes",
29
+ must: true,
30
+ text: "Call amem_remember when the user confirms a durable fact, or when you discover a constraint that the next session will need.",
31
+ },
32
+ {
33
+ id: "local-only",
34
+ must: true,
35
+ text: "Memory stays on this machine under ~/.amem. Do not upload claims, paste them into shared docs, or commit them to product git.",
36
+ },
37
+ {
38
+ id: "no-secrets",
39
+ must: true,
40
+ text: "Never remember passwords, API keys, tokens, or private key material.",
41
+ },
42
+ {
43
+ id: "no-prompt-strategy",
44
+ must: true,
45
+ text: "Store repo or workspace facts with file anchors — not proprietary prompting strategy.",
46
+ },
47
+ {
48
+ id: "durable-kinds",
49
+ must: true,
50
+ text: "Prefer kinds constraint, gotcha, structure, howto, or owner. Use kind=session only for short-lived chat takeaways — session spam drowns retrieval.",
51
+ },
52
+ {
53
+ id: "workspace",
54
+ must: true,
55
+ text: "Named workspaces are not git repos. Always pass the workspace slug on context and remember.",
56
+ },
57
+ ];
58
+ function renderMarkdown() {
59
+ const toolLines = TOOLS.map((t) => `- \`${t.name}\` (${t.role}) — ${t.when}`).join("\n");
60
+ const ruleLines = RULES.map((r) => `- ${r.must ? "Must:" : "Should:"} ${r.text}`).join("\n");
61
+ return `# amem remember contract (v${REMEMBER_CONTRACT_VERSION})
62
+
63
+ Generic MCP host recipe. Same tools for every client — do not fork per product.
64
+
65
+ ## Connect
66
+
67
+ Keep \`amem ui\` running, then attach:
68
+
69
+ \`${MCP_URL_TEMPLATE}\`
70
+
71
+ GUI hosts often cannot see Homebrew on PATH. Prefer the HTTP URL over a bare \`amem\` command.
72
+
73
+ ## Tools
74
+
75
+ ${toolLines}
76
+
77
+ ## Rules
78
+
79
+ ${ruleLines}
80
+
81
+ ## Example
82
+
83
+ 1. \`amem_context\` query="What should I know before changing auth?" workspace=my-app
84
+ 2. Do the work, verify files.
85
+ 3. \`amem_remember\` text="Auth mode is checked in src/auth.ts before Drive sync" workspace=my-app kind=constraint anchors=["src/auth.ts"]
86
+ `;
87
+ }
88
+ function renderPaste() {
89
+ return `amem remember contract v${REMEMBER_CONTRACT_VERSION}
90
+
91
+ Connect (any MCP host): ${MCP_URL_TEMPLATE}
92
+ Keep amem ui running. Do not use a bare amem command from GUI apps.
93
+
94
+ Every task:
95
+ 1. amem_context — read local memory first (pass workspace=<slug>)
96
+ 2. Do the work
97
+ 3. amem_remember — write durable outcomes (decisions, constraints, owners, gotchas)
98
+
99
+ Never: upload memory, commit claims to product git, store secrets, or store prompting strategy.
100
+ Facts stay in ~/.amem on this machine.
101
+
102
+ If unsure, call amem_recipe.`;
103
+ }
104
+ export function rememberContract() {
105
+ return {
106
+ version: REMEMBER_CONTRACT_VERSION,
107
+ title: "amem remember contract",
108
+ mcpUrlTemplate: MCP_URL_TEMPLATE,
109
+ tools: TOOLS,
110
+ rules: RULES,
111
+ markdown: renderMarkdown(),
112
+ paste: renderPaste(),
113
+ };
114
+ }
115
+ export function rememberContractMustIds() {
116
+ return RULES.filter((r) => r.must).map((r) => r.id);
117
+ }
@@ -0,0 +1,15 @@
1
+ export type RepoIdentity = {
2
+ rootPath: string;
3
+ remoteUrl: string | null;
4
+ repoKey: string;
5
+ repoName: string;
6
+ defaultBranch: string;
7
+ };
8
+ export declare function findGitRoot(startPath?: string): string | null;
9
+ /** Normalize remotes so ssh/https clones of the same repo share a key. */
10
+ export declare function normalizeRemoteUrl(url: string): string;
11
+ export declare function detectRepoIdentity(cwd?: string): RepoIdentity;
12
+ export declare function slugifyWorkspace(name: string): string;
13
+ export declare function workspaceIdentity(name: string, rootPath: string): RepoIdentity;
14
+ export declare function parseWorkspaceSlug(remoteUrl: string | null | undefined): string | null;
15
+ export declare function newId(prefix?: string): string;