@iamem/amem 0.1.2 → 0.2.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.
- package/README.md +64 -1
- package/dist/api/routes.js +337 -3
- package/dist/attest.d.ts +13 -0
- package/dist/attest.js +44 -0
- package/dist/capture.js +14 -5
- package/dist/cli.js +291 -6
- package/dist/context.d.ts +10 -1
- package/dist/context.js +105 -3
- package/dist/db.d.ts +141 -0
- package/dist/db.js +398 -0
- package/dist/embed.js +5 -14
- package/dist/estimate.d.ts +25 -1
- package/dist/estimate.js +36 -3
- package/dist/freshness.d.ts +7 -0
- package/dist/freshness.js +8 -1
- package/dist/hook.js +8 -1
- package/dist/hygiene.d.ts +26 -2
- package/dist/hygiene.js +42 -3
- package/dist/install/hosts.d.ts +15 -0
- package/dist/install/hosts.js +82 -4
- package/dist/install/skills.js +10 -5
- package/dist/kinds.d.ts +18 -0
- package/dist/kinds.js +80 -3
- package/dist/license.d.ts +1 -0
- package/dist/license.js +21 -19
- package/dist/mcp.js +221 -0
- package/dist/platforms.js +6 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.js +16 -1
- package/dist/remember-contract.js +13 -4
- package/dist/repo-identity.d.ts +10 -0
- package/dist/repo-identity.js +20 -1
- package/dist/skill-capture.d.ts +43 -0
- package/dist/skill-capture.js +146 -0
- package/dist/skills.d.ts +106 -0
- package/dist/skills.js +422 -0
- package/docs/backlog.md +9 -0
- package/package.json +2 -1
- package/scripts/mcp-launch.sh +26 -0
- package/skills/amem-tasks/SKILL.md +100 -0
- package/skills/amem-write-skill/SKILL.md +99 -0
- package/templates/cursor-rule.mdc +16 -6
- package/templates/policy.deny-default.toml +5 -0
- package/templates/policy.example.toml +8 -0
- package/ui-static/app.js +458 -233
- package/ui-static/index.html +11 -34
- package/ui-static/styles.css +310 -1
package/dist/estimate.js
CHANGED
|
@@ -3,11 +3,42 @@ export function estimateTokensFromText(text) {
|
|
|
3
3
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Assumed cost of one file the agent did not have to open. This is a MODELLED
|
|
7
|
+
* constant, not a measurement: it credits a saving whether or not the agent
|
|
8
|
+
* would actually have read that file. It dominates the headline number, so
|
|
9
|
+
* treat any total built from it as an upper bound until calibrated against
|
|
10
|
+
* real reported savings (`amem usage report --saved <n>`).
|
|
11
|
+
*/
|
|
12
|
+
export const ASSUMED_TOKENS_PER_FILE = 4000;
|
|
13
|
+
export const ASSUMED_TOKENS_PER_CLAIM = 200;
|
|
14
|
+
/**
|
|
15
|
+
* Proxy for exploration avoided, net of what the packet itself cost:
|
|
7
16
|
* anchors_returned * 4000 + claims_returned * 200 - packet_tokens
|
|
17
|
+
*
|
|
18
|
+
* Deliberately NOT clamped at zero. A packet that returns little and still
|
|
19
|
+
* costs input tokens is a net loss, and the metric has to be able to say so —
|
|
20
|
+
* clamping made the dashboard structurally incapable of reporting that amem
|
|
21
|
+
* ever cost anything, which is not a property you want in your own numbers.
|
|
8
22
|
*/
|
|
9
23
|
export function estimateTokensSaved(input) {
|
|
10
|
-
return
|
|
24
|
+
return (input.anchorsCount * ASSUMED_TOKENS_PER_FILE +
|
|
25
|
+
input.claimsCount * ASSUMED_TOKENS_PER_CLAIM -
|
|
26
|
+
input.packetTokens);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* How a savings figure should be presented. "measured" only once real reported
|
|
30
|
+
* savings exist; until then the number is a model and must be labelled as one.
|
|
31
|
+
*/
|
|
32
|
+
export function savingsBasis(reportedTokensSaved) {
|
|
33
|
+
const calibrated = Number(reportedTokensSaved) > 0;
|
|
34
|
+
return {
|
|
35
|
+
// Distinct from pricing.basis ("input"), which is about which side of the
|
|
36
|
+
// token bill is being priced, not about how trustworthy the figure is.
|
|
37
|
+
savingsBasis: calibrated ? "measured" : "modelled",
|
|
38
|
+
calibrated,
|
|
39
|
+
assumedTokensPerFile: ASSUMED_TOKENS_PER_FILE,
|
|
40
|
+
assumedTokensPerClaim: ASSUMED_TOKENS_PER_CLAIM,
|
|
41
|
+
};
|
|
11
42
|
}
|
|
12
43
|
/** Typical Cursor/Claude tool round-trip to read a file (~1.2s) plus a little per claim. */
|
|
13
44
|
export const MS_PER_FILE_ROUNDTRIP = 1200;
|
|
@@ -18,7 +49,9 @@ export function estimateMsSaved(input) {
|
|
|
18
49
|
/** Mid-range frontier *input* $/1M tokens (Sonnet-class). Avoided exploration is input-side. Not a bill. */
|
|
19
50
|
export const USD_PER_MILLION_INPUT_TOKENS = 3;
|
|
20
51
|
export function estimateUsdSaved(tokens, usdPerMillion = USD_PER_MILLION_INPUT_TOKENS) {
|
|
21
|
-
|
|
52
|
+
// Unclamped for the same reason as estimateTokensSaved: a negative here is
|
|
53
|
+
// real information, not an error to be hidden.
|
|
54
|
+
return (Number(tokens) / 1_000_000) * usdPerMillion;
|
|
22
55
|
}
|
|
23
56
|
export function eventKind(claimsCount, notesCount = 0) {
|
|
24
57
|
return claimsCount > 0 || notesCount > 0 ? "local_hit" : "server_trip";
|
package/dist/freshness.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ export type ClaimFreshness = {
|
|
|
5
5
|
staleAnchors: string[];
|
|
6
6
|
missingAnchors: string[];
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* capture.ts falls back to this when it cannot extract a real file anchor.
|
|
10
|
+
* README churns constantly, so honouring it as a real anchor marks those claims
|
|
11
|
+
* stale forever and drowns the genuine staleness signal. Treat it as no anchor.
|
|
12
|
+
* (capture.ts already filters it out of "does this claim have real anchors".)
|
|
13
|
+
*/
|
|
14
|
+
export declare const PLACEHOLDER_ANCHOR = "README.md";
|
|
8
15
|
export declare function parseAnchors(codeAnchorsJson: string): string[];
|
|
9
16
|
/**
|
|
10
17
|
* Compare claim.updated_at to filesystem mtimes of code_anchors.
|
package/dist/freshness.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* capture.ts falls back to this when it cannot extract a real file anchor.
|
|
5
|
+
* README churns constantly, so honouring it as a real anchor marks those claims
|
|
6
|
+
* stale forever and drowns the genuine staleness signal. Treat it as no anchor.
|
|
7
|
+
* (capture.ts already filters it out of "does this claim have real anchors".)
|
|
8
|
+
*/
|
|
9
|
+
export const PLACEHOLDER_ANCHOR = "README.md";
|
|
3
10
|
export function parseAnchors(codeAnchorsJson) {
|
|
4
11
|
try {
|
|
5
12
|
const parsed = JSON.parse(codeAnchorsJson);
|
|
@@ -21,7 +28,7 @@ function resolveAnchorPath(rootPath, anchor) {
|
|
|
21
28
|
* Missing paths → missing_anchor; any newer file → stale.
|
|
22
29
|
*/
|
|
23
30
|
export function assessClaimFreshness(rootPath, claim) {
|
|
24
|
-
const anchors = parseAnchors(claim.code_anchors);
|
|
31
|
+
const anchors = parseAnchors(claim.code_anchors).filter((a) => a !== PLACEHOLDER_ANCHOR);
|
|
25
32
|
if (anchors.length === 0) {
|
|
26
33
|
return { status: "unanchored", staleAnchors: [], missingAnchors: [] };
|
|
27
34
|
}
|
package/dist/hook.js
CHANGED
|
@@ -4,6 +4,7 @@ import { logContextUsage } from "./api/routes.js";
|
|
|
4
4
|
import { captureMissLearnDraft, captureSessionDraft, findRecentContextMisses, isUsefulCaptureText, } from "./capture.js";
|
|
5
5
|
import { getRepoByCwd, insertConversationNote, listConversationNotes, upsertRepo, } from "./db.js";
|
|
6
6
|
import { detectRepoIdentity } from "./repo-identity.js";
|
|
7
|
+
import { captureSkillRevision, captureSkillSuggestion } from "./skill-capture.js";
|
|
7
8
|
const SECRET = /password|api[_-]?key|secret|token\s*[:=]|begin (rsa |openssh )?private/i;
|
|
8
9
|
function workspaceRoot(payload) {
|
|
9
10
|
const roots = payload.workspace_roots;
|
|
@@ -63,8 +64,9 @@ function injectPacket(repo, query, session, platform) {
|
|
|
63
64
|
sessionId: session,
|
|
64
65
|
query: query || "(session start)",
|
|
65
66
|
});
|
|
66
|
-
if (packet.claims.length === 0 && packet.notes.length === 0)
|
|
67
|
+
if (packet.claims.length === 0 && packet.notes.length === 0 && (packet.tasks?.length ?? 0) === 0) {
|
|
67
68
|
return null;
|
|
69
|
+
}
|
|
68
70
|
return cap(markdown);
|
|
69
71
|
}
|
|
70
72
|
export function handleHookPayload(raw) {
|
|
@@ -171,6 +173,11 @@ function handleHookPayloadInner(raw) {
|
|
|
171
173
|
notes: recent.slice(0, 8).map((n) => ({ role: n.role, text: n.text })),
|
|
172
174
|
});
|
|
173
175
|
}
|
|
176
|
+
// Procedural memory: was this session a workflow worth writing up, or evidence that a
|
|
177
|
+
// skill we already followed is wrong? Chronological order matters to the heuristics.
|
|
178
|
+
const ordered = [...recent].reverse().map((n) => ({ role: n.role, text: n.text }));
|
|
179
|
+
captureSkillRevision({ repo, sessionId: sid, notes: ordered }) ??
|
|
180
|
+
captureSkillSuggestion({ repo, sessionId: sid, notes: ordered });
|
|
174
181
|
return {};
|
|
175
182
|
}
|
|
176
183
|
return { continue: true };
|
package/dist/hygiene.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local memory hygiene: decay unused facts, find near-duplicates, review inbox.
|
|
3
|
-
*
|
|
4
|
-
* Nothing is uploaded.
|
|
3
|
+
* Completely free and open — runs on-device, nothing uploaded.
|
|
5
4
|
*/
|
|
6
5
|
import { type ClaimRow } from "./db.js";
|
|
7
6
|
export type HygieneDuplicate = {
|
|
@@ -68,3 +67,28 @@ export declare function runScheduledHygiene(unusedDays?: number): {
|
|
|
68
67
|
error?: string;
|
|
69
68
|
}>;
|
|
70
69
|
};
|
|
70
|
+
export type NonFactClaim = {
|
|
71
|
+
repoId: string;
|
|
72
|
+
repoName: string;
|
|
73
|
+
id: string;
|
|
74
|
+
kind: string;
|
|
75
|
+
reason: string;
|
|
76
|
+
preview: string;
|
|
77
|
+
};
|
|
78
|
+
/** Stored claims that are clear junk. Pinned claims are never included. */
|
|
79
|
+
export declare function findNonFactClaims(opts?: {
|
|
80
|
+
repoId?: string;
|
|
81
|
+
}): NonFactClaim[];
|
|
82
|
+
/**
|
|
83
|
+
* Delete clear-junk claims. Always run with dryRun first — deletion is not
|
|
84
|
+
* reversible from inside amem, only from a backup.
|
|
85
|
+
*/
|
|
86
|
+
export declare function purgeNonFactClaims(opts?: {
|
|
87
|
+
repoId?: string;
|
|
88
|
+
dryRun?: boolean;
|
|
89
|
+
}): {
|
|
90
|
+
scanned: number;
|
|
91
|
+
matched: NonFactClaim[];
|
|
92
|
+
deleted: number;
|
|
93
|
+
dryRun: boolean;
|
|
94
|
+
};
|
package/dist/hygiene.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local memory hygiene: decay unused facts, find near-duplicates, review inbox.
|
|
3
|
-
*
|
|
4
|
-
* Nothing is uploaded.
|
|
3
|
+
* Completely free and open — runs on-device, nothing uploaded.
|
|
5
4
|
*/
|
|
6
|
-
import { getClaim, listClaims, listProposalDrafts, listRepos, listUsageEvents, setClaimStatus, } from "./db.js";
|
|
5
|
+
import { deleteClaim, getClaim, listClaims, listClaimsAll, listProposalDrafts, listRepos, listUsageEvents, setClaimStatus, } from "./db.js";
|
|
7
6
|
import { FEATURE_HYGIENE, hasFeature, requireFeature } from "./license.js";
|
|
8
7
|
import { applyProposal, applySupersedes } from "./proposal.js";
|
|
9
8
|
import { tokenJaccard } from "./search.js";
|
|
10
9
|
import { parseAnchors } from "./freshness.js";
|
|
10
|
+
import { nonFactReason } from "./kinds.js";
|
|
11
11
|
export const SOFT_PAYWALL_FACTS = 200;
|
|
12
12
|
export const SOFT_PAYWALL_NOISE = 15;
|
|
13
13
|
/** Soft-paywall when session chat takeaways dominate the graph. */
|
|
@@ -252,3 +252,42 @@ export function runScheduledHygiene(unusedDays = 90) {
|
|
|
252
252
|
}
|
|
253
253
|
return { repos };
|
|
254
254
|
}
|
|
255
|
+
/** Stored claims that are clear junk. Pinned claims are never included. */
|
|
256
|
+
export function findNonFactClaims(opts = {}) {
|
|
257
|
+
const names = new Map(listRepos().map((r) => [r.id, r.repo_name]));
|
|
258
|
+
const claims = opts.repoId ? listClaims(opts.repoId) : listClaimsAll();
|
|
259
|
+
const out = [];
|
|
260
|
+
for (const claim of claims) {
|
|
261
|
+
if (claim.pinned)
|
|
262
|
+
continue;
|
|
263
|
+
const reason = nonFactReason(claim.text);
|
|
264
|
+
if (!reason)
|
|
265
|
+
continue;
|
|
266
|
+
out.push({
|
|
267
|
+
repoId: claim.repo_id,
|
|
268
|
+
repoName: names.get(claim.repo_id) ?? claim.repo_id,
|
|
269
|
+
id: claim.id,
|
|
270
|
+
kind: claim.kind,
|
|
271
|
+
reason,
|
|
272
|
+
preview: claim.text.replace(/\s+/g, " ").slice(0, 120),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Delete clear-junk claims. Always run with dryRun first — deletion is not
|
|
279
|
+
* reversible from inside amem, only from a backup.
|
|
280
|
+
*/
|
|
281
|
+
export function purgeNonFactClaims(opts = {}) {
|
|
282
|
+
const dryRun = opts.dryRun !== false;
|
|
283
|
+
const scanned = (opts.repoId ? listClaims(opts.repoId) : listClaimsAll()).length;
|
|
284
|
+
const matched = findNonFactClaims({ repoId: opts.repoId });
|
|
285
|
+
let deleted = 0;
|
|
286
|
+
if (!dryRun) {
|
|
287
|
+
for (const row of matched) {
|
|
288
|
+
if (deleteClaim(row.repoId, row.id))
|
|
289
|
+
deleted += 1;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { scanned, matched, deleted, dryRun };
|
|
293
|
+
}
|
package/dist/install/hosts.d.ts
CHANGED
|
@@ -11,6 +11,21 @@ export declare function installContinue(workspace?: string): HostInstallResult;
|
|
|
11
11
|
export declare function installAider(repoRoot: string): HostInstallResult;
|
|
12
12
|
/** Zed: settings.json context_servers / mcp style. */
|
|
13
13
|
export declare function installZed(workspace?: string): HostInstallResult;
|
|
14
|
+
/** Absolute path to the POSIX launcher shipped with the package. */
|
|
15
|
+
export declare function mcpLauncherPath(): string;
|
|
16
|
+
/** Claude Desktop stores MCP config here (also read by Cowork). */
|
|
17
|
+
export declare function claudeDesktopConfigPath(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Claude Desktop / Cowork: stdio MCP entry in claude_desktop_config.json.
|
|
20
|
+
*
|
|
21
|
+
* GUI apps are not launched from a login shell, so they inherit a minimal PATH
|
|
22
|
+
* with no Homebrew and no nvm. A bare `amem` — or even `node` — registers as a
|
|
23
|
+
* connector but never completes tool discovery, and the host reports no reason.
|
|
24
|
+
* Point at the launcher, which re-resolves node at spawn time; fall back to an
|
|
25
|
+
* absolute node + cli.js pair where a shell script cannot run (Windows).
|
|
26
|
+
*/
|
|
27
|
+
export declare function installClaudeDesktop(workspace?: string): HostInstallResult;
|
|
28
|
+
export declare function claudeDesktopInstallHealth(): string[];
|
|
14
29
|
export declare function continueInstallHealth(): string[];
|
|
15
30
|
export declare function zedInstallHealth(): string[];
|
|
16
31
|
export declare function windsurfInstallHealth(): string[];
|
package/dist/install/hosts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
2
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";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { mcpClientConfig, stdioMcpLaunch } from "../mcp.js";
|
|
5
|
+
import { getPackageRoot, resolveAmemBin, writeJson, readJsonObject } from "./skills.js";
|
|
6
6
|
function ensureDir(path) {
|
|
7
7
|
mkdirSync(path, { recursive: true });
|
|
8
8
|
}
|
|
@@ -74,6 +74,14 @@ Before large exploration, run:
|
|
|
74
74
|
amem context "\${user question}"
|
|
75
75
|
\`\`\`
|
|
76
76
|
|
|
77
|
+
Deferred tasks & Kanban:
|
|
78
|
+
|
|
79
|
+
\`\`\`bash
|
|
80
|
+
amem task list # list pending tasks
|
|
81
|
+
amem task add "..." --body "..." # add task to backlog
|
|
82
|
+
amem task complete <id> # mark finished task as done
|
|
83
|
+
\`\`\`
|
|
84
|
+
|
|
77
85
|
After durable discoveries:
|
|
78
86
|
|
|
79
87
|
\`\`\`bash
|
|
@@ -122,6 +130,72 @@ export function installZed(workspace = "personal") {
|
|
|
122
130
|
],
|
|
123
131
|
};
|
|
124
132
|
}
|
|
133
|
+
/** Absolute path to the POSIX launcher shipped with the package. */
|
|
134
|
+
export function mcpLauncherPath() {
|
|
135
|
+
return join(getPackageRoot(), "scripts", "mcp-launch.sh");
|
|
136
|
+
}
|
|
137
|
+
/** Claude Desktop stores MCP config here (also read by Cowork). */
|
|
138
|
+
export function claudeDesktopConfigPath() {
|
|
139
|
+
if (process.platform === "darwin") {
|
|
140
|
+
return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
141
|
+
}
|
|
142
|
+
if (process.platform === "win32") {
|
|
143
|
+
const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
|
|
144
|
+
return join(appData, "Claude", "claude_desktop_config.json");
|
|
145
|
+
}
|
|
146
|
+
return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Claude Desktop / Cowork: stdio MCP entry in claude_desktop_config.json.
|
|
150
|
+
*
|
|
151
|
+
* GUI apps are not launched from a login shell, so they inherit a minimal PATH
|
|
152
|
+
* with no Homebrew and no nvm. A bare `amem` — or even `node` — registers as a
|
|
153
|
+
* connector but never completes tool discovery, and the host reports no reason.
|
|
154
|
+
* Point at the launcher, which re-resolves node at spawn time; fall back to an
|
|
155
|
+
* absolute node + cli.js pair where a shell script cannot run (Windows).
|
|
156
|
+
*/
|
|
157
|
+
export function installClaudeDesktop(workspace = "personal") {
|
|
158
|
+
const path = claudeDesktopConfigPath();
|
|
159
|
+
ensureDir(dirname(path));
|
|
160
|
+
const existing = readJsonObject(path);
|
|
161
|
+
const servers = existing.mcpServers ?? {};
|
|
162
|
+
const launcher = mcpLauncherPath();
|
|
163
|
+
const usable = process.platform !== "win32" && existsSync(launcher);
|
|
164
|
+
if (usable) {
|
|
165
|
+
try {
|
|
166
|
+
chmodSync(launcher, 0o755);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// npm preserves the exec bit; a read-only install is not fatal here
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const stdio = stdioMcpLaunch(workspace);
|
|
173
|
+
servers.amem = usable
|
|
174
|
+
? { command: launcher, args: [], env: { AMEM_WORKSPACE: workspace } }
|
|
175
|
+
: {
|
|
176
|
+
command: stdio.command,
|
|
177
|
+
args: stdio.args,
|
|
178
|
+
env: { ...stdio.env, AMEM_WORKSPACE: workspace },
|
|
179
|
+
};
|
|
180
|
+
writeJson(path, { ...existing, mcpServers: servers });
|
|
181
|
+
const shown = usable ? launcher : `${stdio.command} ${stdio.args.join(" ")}`;
|
|
182
|
+
return {
|
|
183
|
+
host: "claude-desktop",
|
|
184
|
+
paths: [path],
|
|
185
|
+
notes: [
|
|
186
|
+
`Claude Desktop MCP amem \u2192 ${shown} (workspace ${workspace}).`,
|
|
187
|
+
"Quit Claude Desktop fully (Cmd+Q) and relaunch \u2014 config is read at startup, closing the window is not enough.",
|
|
188
|
+
"stdio needs no daemon: amem ui does not have to be running.",
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
export function claudeDesktopInstallHealth() {
|
|
193
|
+
const path = claudeDesktopConfigPath();
|
|
194
|
+
if (!fileMentionsAmem(path)) {
|
|
195
|
+
return ["Claude Desktop has no amem MCP entry \u2014 run amem init --platform claude-desktop"];
|
|
196
|
+
}
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
125
199
|
function fileMentionsAmem(path) {
|
|
126
200
|
if (!existsSync(path))
|
|
127
201
|
return false;
|
|
@@ -159,6 +233,8 @@ export function windsurfInstallHealth() {
|
|
|
159
233
|
}
|
|
160
234
|
export function hostInstallHealth(host) {
|
|
161
235
|
switch (host) {
|
|
236
|
+
case "claude-desktop":
|
|
237
|
+
return claudeDesktopInstallHealth();
|
|
162
238
|
case "continue":
|
|
163
239
|
return continueInstallHealth();
|
|
164
240
|
case "zed":
|
|
@@ -172,6 +248,8 @@ export function hostInstallHealth(host) {
|
|
|
172
248
|
export function installHost(host, opts = {}) {
|
|
173
249
|
const ws = opts.workspace || "personal";
|
|
174
250
|
switch (host) {
|
|
251
|
+
case "claude-desktop":
|
|
252
|
+
return installClaudeDesktop(ws);
|
|
175
253
|
case "windsurf":
|
|
176
254
|
return installWindsurf(ws);
|
|
177
255
|
case "continue":
|
package/dist/install/skills.js
CHANGED
|
@@ -17,14 +17,19 @@ export function copyBundledSkills(targetSkillsDir) {
|
|
|
17
17
|
mkdirSync(targetSkillsDir, { recursive: true });
|
|
18
18
|
const source = skillsSourceDir();
|
|
19
19
|
const installed = [];
|
|
20
|
-
|
|
20
|
+
const skillNames = [
|
|
21
|
+
"amem-bootstrap",
|
|
22
|
+
"amem-update-working-memory",
|
|
23
|
+
"amem-tasks",
|
|
24
|
+
"amem-write-skill",
|
|
25
|
+
];
|
|
26
|
+
for (const name of skillNames) {
|
|
21
27
|
const from = join(source, name);
|
|
22
28
|
const to = join(targetSkillsDir, name);
|
|
23
|
-
if (
|
|
24
|
-
|
|
29
|
+
if (existsSync(from)) {
|
|
30
|
+
cpSync(from, to, { recursive: true });
|
|
31
|
+
installed.push(to);
|
|
25
32
|
}
|
|
26
|
-
cpSync(from, to, { recursive: true });
|
|
27
|
-
installed.push(to);
|
|
28
33
|
}
|
|
29
34
|
return installed;
|
|
30
35
|
}
|
package/dist/kinds.d.ts
CHANGED
|
@@ -15,4 +15,22 @@ export declare function compactFromNotes(notes: Array<{
|
|
|
15
15
|
prompt: string;
|
|
16
16
|
answer: string;
|
|
17
17
|
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* Is this a durable statement about the code, or conversational residue?
|
|
20
|
+
*
|
|
21
|
+
* Auto-capture previously accepted any sentence >= 48 chars that mentioned a
|
|
22
|
+
* path, which stored the user's own questions as facts ("so use luna mcp to
|
|
23
|
+
* connect to amem and that will do the trick?"). A question is a request, not
|
|
24
|
+
* a fact, however many files it names.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Why an ALREADY STORED claim is clear junk, or null to keep it.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately narrower than isFactLike: that gate decides what to admit and
|
|
30
|
+
* can afford false negatives, this one decides what to DELETE and a false
|
|
31
|
+
* positive destroys memory. Short, lowercase or oddly-formatted claims are
|
|
32
|
+
* kept here even though capture would now reject them.
|
|
33
|
+
*/
|
|
34
|
+
export declare function nonFactReason(text: string): string | null;
|
|
35
|
+
export declare function isFactLike(text: string): boolean;
|
|
18
36
|
export declare function isDurableCapture(prompt: string, answer: string | undefined, anchorCount: number): boolean;
|
package/dist/kinds.js
CHANGED
|
@@ -47,8 +47,14 @@ export function compactClaimText(prompt, answer) {
|
|
|
47
47
|
const head = sentences.slice(0, 2).join(" ").slice(0, 400);
|
|
48
48
|
return head || takeaway.slice(0, 400);
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
// Short answer: the fact is still in the ANSWER, so lead with it and keep the
|
|
51
|
+
// prompt only as trailing context. Leading with the prompt is how questions
|
|
52
|
+
// ended up stored as durable claims; the trailing "?" is dropped so the
|
|
53
|
+
// result reads as a statement rather than tripping the question filter.
|
|
54
|
+
const q = prompt.replace(/\s+/g, " ").trim().replace(/\?+\s*$/, "").slice(0, 280);
|
|
55
|
+
if (!takeaway)
|
|
56
|
+
return q.slice(0, 400);
|
|
57
|
+
return `${takeaway.slice(0, 200)}${q ? ` (context: ${q})` : ""}`.slice(0, 400);
|
|
52
58
|
}
|
|
53
59
|
/** Multi-turn: fold several notes into one compact fact string. */
|
|
54
60
|
export function compactFromNotes(notes) {
|
|
@@ -72,10 +78,81 @@ function isUsefulish(text) {
|
|
|
72
78
|
function scrubCodeNoise(text) {
|
|
73
79
|
return text
|
|
74
80
|
.replace(/```[\s\S]*?```/g, " ")
|
|
75
|
-
.
|
|
81
|
+
// Keep what is INSIDE an inline span. Deleting it removed the file paths
|
|
82
|
+
// and identifiers that make a claim worth storing ("lives in `src/x.ts`"
|
|
83
|
+
// became "lives in and"), and it ran before pickFactSentences, which
|
|
84
|
+
// scores sentences on exactly those paths.
|
|
85
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
76
86
|
.replace(/\s+/g, " ")
|
|
77
87
|
.trim();
|
|
78
88
|
}
|
|
89
|
+
const QUESTION_LEAD = /^(can|could|would|should|is|are|was|were|do|does|did|how|what|why|when|where|who|which|will|shall|may|might|am|have|has|had)\b/i;
|
|
90
|
+
const CHAT_LEAD = /^(so|ok|okay|and|but|also|im|i'm|let's|lets|please|thanks|thx|yeah|yep|nope|hmm|wait|actually|btw|oh|hey)\b/i;
|
|
91
|
+
/** Residue of an earlier scrub: "lives in and is", "wired via and included". */
|
|
92
|
+
const DANGLING = /\b(in|at|from|to|via|inside|under|with|into)\s+(and|is|was|the file|what)\b/i;
|
|
93
|
+
/** Interrogative opener plus a subject — a question missing its "?". */
|
|
94
|
+
const QUESTION_SHAPE = /^(can|could|should|would|do|does|did|is|are|will|shall|how|what|why|when|where|who|which)\s+(?:(?:do|does|did|can|could|should|would|is|are|will|shall)\s+)?(we|you|i|amem)\b/i;
|
|
95
|
+
/** Preposition running into punctuation or end of string: the object was deleted. */
|
|
96
|
+
const TRAILING_PREPOSITION = /\b(in|at|from|to|via|inside|under|with|into|through)\s*([.,;:]|$)/i;
|
|
97
|
+
/** A leading token that looks like code (path, dotted or snake_case name). */
|
|
98
|
+
const CODE_LEAD = /^[\w@./-]*[._/][\w@./-]*(\s|$)/;
|
|
99
|
+
/**
|
|
100
|
+
* Is this a durable statement about the code, or conversational residue?
|
|
101
|
+
*
|
|
102
|
+
* Auto-capture previously accepted any sentence >= 48 chars that mentioned a
|
|
103
|
+
* path, which stored the user's own questions as facts ("so use luna mcp to
|
|
104
|
+
* connect to amem and that will do the trick?"). A question is a request, not
|
|
105
|
+
* a fact, however many files it names.
|
|
106
|
+
*/
|
|
107
|
+
/**
|
|
108
|
+
* Why an ALREADY STORED claim is clear junk, or null to keep it.
|
|
109
|
+
*
|
|
110
|
+
* Deliberately narrower than isFactLike: that gate decides what to admit and
|
|
111
|
+
* can afford false negatives, this one decides what to DELETE and a false
|
|
112
|
+
* positive destroys memory. Short, lowercase or oddly-formatted claims are
|
|
113
|
+
* kept here even though capture would now reject them.
|
|
114
|
+
*/
|
|
115
|
+
export function nonFactReason(text) {
|
|
116
|
+
const t = (text ?? "").replace(/\s+/g, " ").trim();
|
|
117
|
+
if (t.length === 0)
|
|
118
|
+
return "empty";
|
|
119
|
+
// A question is a request someone typed, not a fact about the code.
|
|
120
|
+
if (t.includes("?"))
|
|
121
|
+
return "question";
|
|
122
|
+
// ...and plenty were typed without the question mark ("can we expose amem
|
|
123
|
+
// data via mcp"). Require an interrogative opener FOLLOWED by a subject, so
|
|
124
|
+
// this cannot swallow a declarative sentence that merely starts with "Is".
|
|
125
|
+
if (QUESTION_SHAPE.test(t))
|
|
126
|
+
return "question";
|
|
127
|
+
// Left behind by the old scrub that deleted inline code spans.
|
|
128
|
+
if (DANGLING.test(t))
|
|
129
|
+
return "scrub-residue";
|
|
130
|
+
// Same damage, different shape: the span sat at the end of the clause, so a
|
|
131
|
+
// preposition now runs straight into punctuation ("memory lives under .").
|
|
132
|
+
if (TRAILING_PREPOSITION.test(t))
|
|
133
|
+
return "scrub-residue";
|
|
134
|
+
if (CHAT_LEAD.test(t))
|
|
135
|
+
return "chat-fragment";
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
export function isFactLike(text) {
|
|
139
|
+
const t = (text ?? "").replace(/\s+/g, " ").trim();
|
|
140
|
+
if (t.length < 40)
|
|
141
|
+
return false;
|
|
142
|
+
if (t.includes("?"))
|
|
143
|
+
return false;
|
|
144
|
+
if (QUESTION_LEAD.test(t))
|
|
145
|
+
return false;
|
|
146
|
+
if (CHAT_LEAD.test(t))
|
|
147
|
+
return false;
|
|
148
|
+
if (DANGLING.test(t))
|
|
149
|
+
return false;
|
|
150
|
+
// Mid-thought lowercase openings are transcript fragments — unless the
|
|
151
|
+
// sentence opens on an identifier, which is normal for a real fact.
|
|
152
|
+
if (/^[a-z]/.test(t) && !CODE_LEAD.test(t))
|
|
153
|
+
return false;
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
79
156
|
function pickFactSentences(text) {
|
|
80
157
|
const sentences = text.split(/(?<=[.!?])\s+/).filter((s) => s.length > 25);
|
|
81
158
|
const scored = sentences.map((s) => {
|
package/dist/license.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare const FEATURE_LOCAL_EMBED = "local_embed_model";
|
|
|
27
27
|
export declare const FEATURE_ATTEST_SKU = "attest_sku";
|
|
28
28
|
export declare const FEATURE_HYGIENE = "hygiene";
|
|
29
29
|
export declare const FEATURE_RULES_SYNC = "rules_sync";
|
|
30
|
+
export declare const ALL_FEATURES: string[];
|
|
30
31
|
/** Vendor verify key (SPKI DER hex). Issue signed files with AMEM_LICENSE_PRIVKEY. */
|
|
31
32
|
export declare const DEFAULT_LICENSE_PUBKEY_HEX = "302a300506032b6570032100b1e01cdb2d1ec60b372bb4307fa48ed743caba09a67633e4689aaa22221080fa";
|
|
32
33
|
export declare function licensePath(): string;
|
package/dist/license.js
CHANGED
|
@@ -10,19 +10,25 @@ export const FEATURE_LOCAL_EMBED = "local_embed_model";
|
|
|
10
10
|
export const FEATURE_ATTEST_SKU = "attest_sku";
|
|
11
11
|
export const FEATURE_HYGIENE = "hygiene";
|
|
12
12
|
export const FEATURE_RULES_SYNC = "rules_sync";
|
|
13
|
+
export const ALL_FEATURES = [
|
|
14
|
+
FEATURE_LOCAL_EMBED,
|
|
15
|
+
FEATURE_HYGIENE,
|
|
16
|
+
FEATURE_RULES_SYNC,
|
|
17
|
+
FEATURE_ATTEST_SKU,
|
|
18
|
+
];
|
|
13
19
|
/** Vendor verify key (SPKI DER hex). Issue signed files with AMEM_LICENSE_PRIVKEY. */
|
|
14
20
|
export const DEFAULT_LICENSE_PUBKEY_HEX = "302a300506032b6570032100b1e01cdb2d1ec60b372bb4307fa48ed743caba09a67633e4689aaa22221080fa";
|
|
15
|
-
const BUY_HINT = "
|
|
21
|
+
const BUY_HINT = "amem is completely free and open with all features included.";
|
|
16
22
|
const TIER_FEATURES = {
|
|
17
|
-
free:
|
|
18
|
-
pro:
|
|
19
|
-
it:
|
|
23
|
+
free: ALL_FEATURES,
|
|
24
|
+
pro: ALL_FEATURES,
|
|
25
|
+
it: ALL_FEATURES,
|
|
20
26
|
};
|
|
21
27
|
export function licensePath() {
|
|
22
28
|
return join(amemHome(), "license.json");
|
|
23
29
|
}
|
|
24
30
|
export function featuresForTier(tier) {
|
|
25
|
-
return [...
|
|
31
|
+
return [...ALL_FEATURES];
|
|
26
32
|
}
|
|
27
33
|
export function generateLicenseKeys() {
|
|
28
34
|
const pair = generateKeyPairSync("ed25519");
|
|
@@ -102,7 +108,7 @@ export function licenseStatus(now = new Date()) {
|
|
|
102
108
|
return {
|
|
103
109
|
tier: "free",
|
|
104
110
|
kind: "none",
|
|
105
|
-
features: [],
|
|
111
|
+
features: [...ALL_FEATURES],
|
|
106
112
|
valid: true,
|
|
107
113
|
transferable: false,
|
|
108
114
|
path,
|
|
@@ -112,36 +118,34 @@ export function licenseStatus(now = new Date()) {
|
|
|
112
118
|
if (expired(file.payload, now))
|
|
113
119
|
issues.push("license expired");
|
|
114
120
|
issues.push(...verifySignedLicense(file));
|
|
115
|
-
const tier =
|
|
116
|
-
const features = [...new Set([...(file.payload.features ?? []), ...
|
|
121
|
+
const tier = file.payload.tier || "free";
|
|
122
|
+
const features = [...new Set([...(file.payload.features ?? []), ...ALL_FEATURES])];
|
|
117
123
|
return {
|
|
118
124
|
tier,
|
|
119
125
|
kind: "signed",
|
|
120
126
|
subject: file.payload.subject,
|
|
121
127
|
expires_at: file.payload.expires_at,
|
|
122
128
|
features,
|
|
123
|
-
valid:
|
|
129
|
+
valid: true,
|
|
124
130
|
transferable: true,
|
|
125
131
|
path,
|
|
126
|
-
issues,
|
|
132
|
+
issues: [],
|
|
127
133
|
};
|
|
128
134
|
}
|
|
129
135
|
catch (error) {
|
|
130
|
-
issues.push(error instanceof Error ? error.message : String(error));
|
|
131
136
|
return {
|
|
132
137
|
tier: "free",
|
|
133
138
|
kind: "none",
|
|
134
|
-
features: [],
|
|
135
|
-
valid:
|
|
139
|
+
features: [...ALL_FEATURES],
|
|
140
|
+
valid: true,
|
|
136
141
|
transferable: false,
|
|
137
142
|
path,
|
|
138
|
-
issues,
|
|
143
|
+
issues: [],
|
|
139
144
|
};
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
export function hasFeature(feature) {
|
|
143
|
-
|
|
144
|
-
return status.valid && status.features.includes(feature);
|
|
148
|
+
return true;
|
|
145
149
|
}
|
|
146
150
|
export function writeLicense(file, path = licensePath()) {
|
|
147
151
|
mkdirSync(amemHome(), { recursive: true, mode: 0o700 });
|
|
@@ -166,7 +170,5 @@ export function clearLicense() {
|
|
|
166
170
|
unlinkSync(path);
|
|
167
171
|
}
|
|
168
172
|
export function requireFeature(feature, label = feature) {
|
|
169
|
-
|
|
170
|
-
return;
|
|
171
|
-
throw new Error(`${label} needs an amem Pro or IT license. ${BUY_HINT}`);
|
|
173
|
+
// All features are included and free by default
|
|
172
174
|
}
|