@iamem/amem 0.1.0 → 0.1.1

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.
@@ -9,6 +9,7 @@ import { installClaude, claudeInstallHealth } from "../install/claude.js";
9
9
  import { installCursor, cursorInstallHealth } from "../install/cursor.js";
10
10
  import { hostInstallHealth, installHost } from "../install/hosts.js";
11
11
  import { decorateDraft, decorateDrafts } from "../draft-quality.js";
12
+ import { isUsefulRememberText } from "../capture.js";
12
13
  import { buildSavingsExport, formatSavingsMarkdown, savingsPdf, } from "../savings-export.js";
13
14
  import { assertPlatformAllowed, assertRemoteAllowed, loadPolicy, } from "../policy.js";
14
15
  import { applyProposal, applySupersedes, parseProposalJson, validateProposal } from "../proposal.js";
@@ -412,6 +413,9 @@ function runRemember(repo, body) {
412
413
  const text = bodyField(body, "text");
413
414
  if (!text)
414
415
  return err(400, "text required");
416
+ if (!isUsefulRememberText(text)) {
417
+ return err(400, "text too trivial or secret-like — remember a durable fact with context");
418
+ }
415
419
  const kind = bodyField(body, "kind") ?? "session";
416
420
  const id = bodyField(body, "id") ??
417
421
  `claim.remember_${createHash("sha256").update(text).digest("hex").slice(0, 12)}`;
package/dist/capture.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { type ProposalDraftRow, type RepoRow, type UsageEventRow } from "./db.js";
2
2
  import { type Proposal } from "./proposal.js";
3
+ /** Reject empty / chat-noise / secret-like text before it becomes a claim. */
3
4
  export declare function isUsefulCaptureText(text: string): boolean;
5
+ /** Same guard for explicit amem_remember / API writes (including short “test”). */
6
+ export declare function isUsefulRememberText(text: string): boolean;
4
7
  export declare function extractCaptureAnchors(text: string, repoRoot: string): string[];
5
8
  /** High-quality durable facts apply without waiting for amem_remember. */
6
9
  export declare function shouldAutoApplyProposal(proposal: Proposal): boolean;
package/dist/capture.js CHANGED
@@ -8,9 +8,10 @@ import { applyProposal } from "./proposal.js";
8
8
  import { scoreProposal } from "./draft-quality.js";
9
9
  import { tokenJaccard } from "./search.js";
10
10
  import { isAutoApplyAll } from "./prefs.js";
11
- const TRIVIAL = /^(ok|okay|yes|yep|no|nah|thanks|thank you|continue|go ahead|sure|please)\.?$/i;
11
+ const TRIVIAL = /^(ok|okay|yes|yep|no|nah|thanks|thank you|continue|go ahead|sure|please|test|testing|hello|hi|hey|ping|asdf|foo|bar)\.?$/i;
12
12
  const SECRET = /password|api[_-]?key|secret|token\s*[:=]|begin (rsa |openssh )?private/i;
13
13
  const PATH_RE = /\b(?:[\w.-]+\/)*[\w.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|md|json|yml|yaml|sql)\b/g;
14
+ /** Reject empty / chat-noise / secret-like text before it becomes a claim. */
14
15
  export function isUsefulCaptureText(text) {
15
16
  const t = text.trim();
16
17
  if (t.length < 16)
@@ -21,6 +22,19 @@ export function isUsefulCaptureText(text) {
21
22
  return false;
22
23
  return true;
23
24
  }
25
+ /** Same guard for explicit amem_remember / API writes (including short “test”). */
26
+ export function isUsefulRememberText(text) {
27
+ const t = text.trim();
28
+ if (!t)
29
+ return false;
30
+ if (TRIVIAL.test(t))
31
+ return false;
32
+ if (t.length < 8)
33
+ return false;
34
+ if (SECRET.test(t))
35
+ return false;
36
+ return true;
37
+ }
24
38
  export function extractCaptureAnchors(text, repoRoot) {
25
39
  const found = [...text.matchAll(PATH_RE)].map((m) => m[0]);
26
40
  const unique = [...new Set(found)].slice(0, 6);
package/dist/hygiene.d.ts CHANGED
@@ -23,9 +23,18 @@ export type HygienePreview = {
23
23
  /** Estimated active count after decay + merge */
24
24
  afterCleanup: number;
25
25
  softPaywall: boolean;
26
+ /** Active claims with kind=session (chat noise indicator). */
27
+ sessionCount: number;
28
+ /** sessionCount / active, 0 when empty. */
29
+ sessionRatio: number;
26
30
  };
27
31
  export declare const SOFT_PAYWALL_FACTS = 200;
28
32
  export declare const SOFT_PAYWALL_NOISE = 15;
33
+ /** Soft-paywall when session chat takeaways dominate the graph. */
34
+ export declare const SOFT_PAYWALL_SESSION_RATIO = 0.55;
35
+ export declare const SOFT_PAYWALL_SESSION_MIN = 25;
36
+ /** Unused unpinned session claims become decay candidates sooner than durable kinds. */
37
+ export declare const SESSION_UNUSED_DAYS = 14;
29
38
  /** Free: counts only for soft paywall / banners. Never applies changes. */
30
39
  export declare function hygienePreview(repoId: string, unusedDays?: number): HygienePreview;
31
40
  /** Aggregate preview across all tracked repos (for All memory scope). */
package/dist/hygiene.js CHANGED
@@ -10,6 +10,11 @@ import { tokenJaccard } from "./search.js";
10
10
  import { parseAnchors } from "./freshness.js";
11
11
  export const SOFT_PAYWALL_FACTS = 200;
12
12
  export const SOFT_PAYWALL_NOISE = 15;
13
+ /** Soft-paywall when session chat takeaways dominate the graph. */
14
+ export const SOFT_PAYWALL_SESSION_RATIO = 0.55;
15
+ export const SOFT_PAYWALL_SESSION_MIN = 25;
16
+ /** Unused unpinned session claims become decay candidates sooner than durable kinds. */
17
+ export const SESSION_UNUSED_DAYS = 14;
13
18
  function usedClaimIds(repoId, days) {
14
19
  const ids = new Set();
15
20
  for (const event of listUsageEvents({ repoId, days })) {
@@ -29,14 +34,24 @@ function usedClaimIds(repoId, days) {
29
34
  function computeHygiene(repoId, unusedDays = 90) {
30
35
  const claims = listClaims(repoId);
31
36
  const used = usedClaimIds(repoId, unusedDays);
37
+ const usedSessions = usedClaimIds(repoId, SESSION_UNUSED_DAYS);
32
38
  const cutoff = Date.now() - unusedDays * 86_400_000;
39
+ const sessionCutoff = Date.now() - SESSION_UNUSED_DAYS * 86_400_000;
33
40
  const stale = claims.filter((c) => {
34
41
  if (Number(c.pinned || 0) > 0)
35
42
  return false;
43
+ const updated = Date.parse(c.updated_at);
44
+ if (!Number.isFinite(updated))
45
+ return false;
46
+ const isSession = (c.kind || "").toLowerCase() === "session";
47
+ if (isSession) {
48
+ if (usedSessions.has(c.id))
49
+ return false;
50
+ return updated < sessionCutoff;
51
+ }
36
52
  if (used.has(c.id))
37
53
  return false;
38
- const updated = Date.parse(c.updated_at);
39
- return Number.isFinite(updated) && updated < cutoff;
54
+ return updated < cutoff;
40
55
  });
41
56
  const duplicates = [];
42
57
  for (let i = 0; i < claims.length; i++) {
@@ -63,16 +78,40 @@ function computeHygiene(repoId, unusedDays = 90) {
63
78
  active: claims.length,
64
79
  };
65
80
  }
81
+ function sessionStats(repoId) {
82
+ const claims = listClaims(repoId);
83
+ const active = claims.length;
84
+ const sessionCount = claims.filter((c) => (c.kind || "").toLowerCase() === "session").length;
85
+ const sessionRatio = active > 0 ? sessionCount / active : 0;
86
+ return { sessionCount, sessionRatio, active };
87
+ }
88
+ function softPaywallFrom(preview) {
89
+ if (hasFeature(FEATURE_HYGIENE))
90
+ return false;
91
+ const noise = preview.staleCount + preview.duplicateCount;
92
+ if (preview.active >= SOFT_PAYWALL_FACTS || noise >= SOFT_PAYWALL_NOISE)
93
+ return true;
94
+ if (preview.sessionCount >= SOFT_PAYWALL_SESSION_MIN &&
95
+ preview.sessionRatio >= SOFT_PAYWALL_SESSION_RATIO) {
96
+ return true;
97
+ }
98
+ return false;
99
+ }
66
100
  /** Free: counts only for soft paywall / banners. Never applies changes. */
67
101
  export function hygienePreview(repoId, unusedDays = 90) {
68
102
  const report = computeHygiene(repoId, unusedDays);
103
+ const { sessionCount, sessionRatio } = sessionStats(repoId);
69
104
  const staleCount = report.stale.length;
70
105
  const duplicateCount = report.duplicates.length;
71
106
  const removable = Math.min(report.active, staleCount + duplicateCount);
72
107
  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);
108
+ const softPaywall = softPaywallFrom({
109
+ active: report.active,
110
+ staleCount,
111
+ duplicateCount,
112
+ sessionCount,
113
+ sessionRatio,
114
+ });
76
115
  return {
77
116
  active: report.active,
78
117
  staleCount,
@@ -80,6 +119,8 @@ export function hygienePreview(repoId, unusedDays = 90) {
80
119
  pendingDrafts: report.pendingDrafts,
81
120
  afterCleanup,
82
121
  softPaywall,
122
+ sessionCount,
123
+ sessionRatio,
83
124
  };
84
125
  }
85
126
  /** Aggregate preview across all tracked repos (for All memory scope). */
@@ -88,18 +129,35 @@ export function hygienePreviewAll(unusedDays = 90) {
88
129
  let staleCount = 0;
89
130
  let duplicateCount = 0;
90
131
  let pendingDrafts = 0;
132
+ let sessionCount = 0;
91
133
  for (const repo of listRepos()) {
92
134
  const p = hygienePreview(repo.id, unusedDays);
93
135
  active += p.active;
94
136
  staleCount += p.staleCount;
95
137
  duplicateCount += p.duplicateCount;
96
138
  pendingDrafts += p.pendingDrafts;
139
+ sessionCount += p.sessionCount;
97
140
  }
98
141
  const removable = Math.min(active, staleCount + duplicateCount);
99
142
  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 };
143
+ const sessionRatio = active > 0 ? sessionCount / active : 0;
144
+ const softPaywall = softPaywallFrom({
145
+ active,
146
+ staleCount,
147
+ duplicateCount,
148
+ sessionCount,
149
+ sessionRatio,
150
+ });
151
+ return {
152
+ active,
153
+ staleCount,
154
+ duplicateCount,
155
+ pendingDrafts,
156
+ afterCleanup,
157
+ softPaywall,
158
+ sessionCount,
159
+ sessionRatio,
160
+ };
103
161
  }
104
162
  export function hygieneReport(repoId, unusedDays = 90) {
105
163
  requireFeature(FEATURE_HYGIENE, "Memory hygiene");
package/docs/backlog.md CHANGED
@@ -37,16 +37,16 @@ Updated after completing the Feature Map **Later** phase (local embedding model
37
37
 
38
38
  ## Open
39
39
 
40
- - First npm publish of `@iamem/amem` (org `@iamem` already exists; need valid `NPM_TOKEN` / `npm login`, then tag `v0.1.0` or `npm publish --access public`).
41
40
  - Prompt-pack before/after Stats benchmark; restore wizard polish; IT seat pack.
42
41
  - Decide one-time vs subscription (offline files cannot revoke on cancel unless you add `expires_at` and re-issue).
43
42
  - Optional vendored ONNX/MiniLM weights in a paid pack (external command is the local hook today).
43
+ - Move CI publish to npm Trusted Publishing (OIDC) before Jan 2027 GAT bypass-2FA publish sunset.
44
44
 
45
45
  ## Shipped (go-to-market / upsell)
46
46
 
47
47
  - Public Checkout at **getamem.com** (tryamem redirects); Stripe live webhook.
48
48
  - Mailtrap **live send** (`MAILTRAP_USE_TESTING=false`); thank-you download still works if mail fails.
49
- - Package renamed to `@iamem/amem` (CLI binary still `amem`); shop install copy updated.
49
+ - **`@iamem/amem@0.1.0` on npm** — `npx @iamem/amem setup` / `npm i -g @iamem/amem` (CLI binary `amem`).
50
50
  - UI **Apply license** (paste/drop) + **Turn on Pro retrieval** checklist.
51
51
  - Memory **retrieval showdown** (free hash vs Pro n-gram) + top-bar **Try retrieval**.
52
52
  - Remember-contract guidance: prefer durable kinds; avoid `session` spam.
@@ -30,7 +30,7 @@ The pack includes deny-by-default `policy.toml`, an MDM plist stub, `mdm-offboar
30
30
 
31
31
  ## Install (DevEx / IT)
32
32
 
33
- 1. Ship a pinned amem build (internal npm, pkg, or `npm link` from a mirrored clone). Node 20+ required (`better-sqlite3`).
33
+ 1. Ship a pinned amem build: `npm i -g @iamem/amem` (or internal mirror / pkg). Node 20+ required (`better-sqlite3` prebuilds on common macOS/Linux).
34
34
  2. Deploy policy:
35
35
 
36
36
  ```bash
@@ -78,6 +78,7 @@ That is not a remote public API:
78
78
  - Nothing in `~/.amem` is uploaded or synced.
79
79
  - Binding is loopback-only — LAN/WAN clients cannot reach the UI by default.
80
80
  - Treat any process on the same machine as potentially able to call `127.0.0.1:7843` while the UI is running (same as any local MCP server).
81
+ - Do not expose the UI on a non-loopback bind; policy forces loopback. Tightening `Access-Control-Allow-Origin` to specific Origins is optional and can break some MCP hosts — leave `*` unless you control every client Origin.
81
82
 
82
83
  ## Offboarding
83
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamem/amem",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Local personal agent memory for Cursor and Claude Code. Private to your machine — never shared.",
5
5
  "type": "module",
6
6
  "bin": {
package/ui-static/app.js CHANGED
@@ -320,6 +320,8 @@ function writeUrlState() {
320
320
 
321
321
  function setTab(tab) {
322
322
  if (tab === "stats") tab = "analytics";
323
+ // Paid licenses: Plans is a sell page — don't open it; Setup still has Apply license.
324
+ if (tab === "welcome" && isPaidLicense()) tab = "dashboard";
323
325
  if (tab === "brain") {
324
326
  state.brainAll = true;
325
327
  persistBrainAll(true);
@@ -405,6 +407,7 @@ async function refreshVault() {
405
407
  } catch {
406
408
  state.license = state.status?.license || null;
407
409
  }
410
+ paintPlansNavVisibility(isPaidLicense());
408
411
  try {
409
412
  state.embed = await apiUnscoped("/api/embed");
410
413
  } catch {
@@ -1051,9 +1054,17 @@ function isPaidLicense() {
1051
1054
  const tier = String(state.license?.tier || state.status?.license?.tier || "free").toLowerCase();
1052
1055
  const paid = state.license?.valid !== false && (tier === "pro" || tier === "it");
1053
1056
  if (paid) persistLicenseTier(tier);
1057
+ paintPlansNavVisibility(paid);
1054
1058
  return paid;
1055
1059
  }
1056
1060
 
1061
+ /** Paid users don't need the Plans sell tab in the sidebar (Setup still has Apply license). */
1062
+ function paintPlansNavVisibility(paid) {
1063
+ const btn = document.querySelector('#tabs button[data-tab="welcome"]');
1064
+ if (!btn) return;
1065
+ btn.classList.toggle("hidden", Boolean(paid));
1066
+ }
1067
+
1057
1068
  function licenseApplyHtml(idPrefix = "lic") {
1058
1069
  return `
1059
1070
  <div class="license-apply" id="${idPrefix}ApplyBox">
@@ -2098,14 +2109,20 @@ async function loadSoftPaywall() {
2098
2109
  return;
2099
2110
  }
2100
2111
  const noise = (preview.staleCount || 0) + (preview.duplicateCount || 0);
2112
+ const sessions = Number(preview.sessionCount || 0);
2113
+ const ratio = Number(preview.sessionRatio || 0);
2114
+ const sessionHeavy = sessions >= 25 && ratio >= 0.55;
2101
2115
  const shop = state.shop || {};
2102
2116
  const proUrl = shop.proUrl || "https://getamem.com/buy/pro";
2103
2117
  el.classList.remove("hidden");
2118
+ const why = sessionHeavy
2119
+ ? `${preview.active} facts · <b>${Math.round(ratio * 100)}%</b> are short-lived <code>session</code> kinds (~${sessions}). Free preview → Pro applies Cleanup to archive unused sessions.`
2120
+ : `${preview.active} facts · ~${noise} unused/duplicates → about <b>${preview.afterCleanup}</b> after cleanup. Free still works — this is optional.`;
2104
2121
  el.innerHTML = `
2105
2122
  <div class="soft-paywall-inner">
2106
2123
  <div>
2107
- <strong>Pro can clean this</strong>
2108
- <p class="note" style="margin:0.25rem 0 0">${preview.active} facts · ~${noise} unused/duplicates → about <b>${preview.afterCleanup}</b> after cleanup. Free still works — this is optional.</p>
2124
+ <strong>${sessionHeavy ? "Session noise is crowding retrieval" : "Pro can clean this"}</strong>
2125
+ <p class="note" style="margin:0.25rem 0 0">${why}</p>
2109
2126
  </div>
2110
2127
  <div class="soft-paywall-actions">
2111
2128
  <a class="btn small" href="${esc(proUrl)}" target="_blank" rel="noopener">Buy Pro · Apply cleanup</a>
@@ -3810,7 +3827,7 @@ async function render() {
3810
3827
  document.querySelectorAll("#tabs button[data-tab]").forEach((b) => {
3811
3828
  b.addEventListener("click", () => setTab(b.dataset.tab));
3812
3829
  });
3813
- $("#brandBtn")?.addEventListener("click", () => setTab("welcome"));
3830
+ $("#brandBtn")?.addEventListener("click", () => setTab(isPaidLicense() ? "dashboard" : "welcome"));
3814
3831
 
3815
3832
  $("#brainSearch")?.addEventListener("input", (e) => {
3816
3833
  if (state.tab !== "brain") return;
@@ -1306,6 +1306,10 @@ a.btn {
1306
1306
  display: none;
1307
1307
  }
1308
1308
 
1309
+ .side-tabs button.hidden {
1310
+ display: none !important;
1311
+ }
1312
+
1309
1313
  .hidden {
1310
1314
  display: none !important;
1311
1315
  }