@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.
- package/dist/api/routes.js +4 -0
- package/dist/capture.d.ts +3 -0
- package/dist/capture.js +15 -1
- package/dist/hygiene.d.ts +9 -0
- package/dist/hygiene.js +66 -8
- package/docs/backlog.md +2 -2
- package/docs/enterprise-endpoint.md +2 -1
- package/package.json +1 -1
- package/ui-static/app.js +20 -3
- package/ui-static/styles.css +4 -0
package/dist/api/routes.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
101
|
-
const softPaywall =
|
|
102
|
-
|
|
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
|
-
-
|
|
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
|
|
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
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
|
|
2108
|
-
<p class="note" style="margin:0.25rem 0 0">${
|
|
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;
|