memoryai-mcp 2.3.0 → 2.3.2
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 +362 -304
- package/dist/claude-setup.d.ts +19 -0
- package/dist/claude-setup.js +216 -0
- package/dist/index.js +191 -12
- package/dist/kiro-setup.d.ts +11 -2
- package/dist/kiro-setup.js +143 -60
- package/package.json +46 -45
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* memoryai-claude-setup
|
|
4
|
+
*
|
|
5
|
+
* Wires MemoryAI into Claude Code at the MECHANISM level using HTTP hooks.
|
|
6
|
+
* Claude Code injects a hook's `additionalContext` straight into the model's
|
|
7
|
+
* context — no agent decision required — so memory works the moment it's set up,
|
|
8
|
+
* exactly like the OpenAI proxy. The user runs this once and never thinks about
|
|
9
|
+
* memory again:
|
|
10
|
+
*
|
|
11
|
+
* - SessionStart → POST /v1/hooks/claude/session-start (inject DNA + recent context)
|
|
12
|
+
* - UserPromptSubmit→ POST /v1/hooks/claude/user-prompt (recall before answering)
|
|
13
|
+
* - Stop → POST /v1/hooks/claude/stop (auto-store after each turn)
|
|
14
|
+
*
|
|
15
|
+
* It also registers the MCP server (so the 70+ tools are available for advanced
|
|
16
|
+
* use) and writes a CLAUDE.md note. Existing settings/CLAUDE.md are merged, never
|
|
17
|
+
* clobbered.
|
|
18
|
+
*/
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* memoryai-claude-setup
|
|
4
|
+
*
|
|
5
|
+
* Wires MemoryAI into Claude Code at the MECHANISM level using HTTP hooks.
|
|
6
|
+
* Claude Code injects a hook's `additionalContext` straight into the model's
|
|
7
|
+
* context — no agent decision required — so memory works the moment it's set up,
|
|
8
|
+
* exactly like the OpenAI proxy. The user runs this once and never thinks about
|
|
9
|
+
* memory again:
|
|
10
|
+
*
|
|
11
|
+
* - SessionStart → POST /v1/hooks/claude/session-start (inject DNA + recent context)
|
|
12
|
+
* - UserPromptSubmit→ POST /v1/hooks/claude/user-prompt (recall before answering)
|
|
13
|
+
* - Stop → POST /v1/hooks/claude/stop (auto-store after each turn)
|
|
14
|
+
*
|
|
15
|
+
* It also registers the MCP server (so the 70+ tools are available for advanced
|
|
16
|
+
* use) and writes a CLAUDE.md note. Existing settings/CLAUDE.md are merged, never
|
|
17
|
+
* clobbered.
|
|
18
|
+
*/
|
|
19
|
+
import { createInterface } from "node:readline";
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { join, dirname } from "node:path";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
24
|
+
function ask(question, fallback) {
|
|
25
|
+
const suffix = fallback ? ` [${fallback}]` : "";
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
28
|
+
resolve(answer.trim() || fallback || "");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function readJsonSafe(path) {
|
|
33
|
+
if (!existsSync(path))
|
|
34
|
+
return {};
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(readFileSync(path, "utf-8")) || {};
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
console.error(` warn ${path} is not valid JSON — leaving it untouched and aborting.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function writeJson(path, data) {
|
|
44
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
45
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
46
|
+
}
|
|
47
|
+
/** A single HTTP hook handler bound to a MemoryAI endpoint. */
|
|
48
|
+
function httpHook(endpoint, apiKey, timeout) {
|
|
49
|
+
return {
|
|
50
|
+
type: "http",
|
|
51
|
+
url: endpoint,
|
|
52
|
+
timeout,
|
|
53
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
54
|
+
// Required for Claude Code to interpolate the env-style header value.
|
|
55
|
+
allowedEnvVars: [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** True if any handler in a hook group already points at a memoryai endpoint. */
|
|
59
|
+
function groupHasMemoryAI(group) {
|
|
60
|
+
const handlers = (group && group.hooks) || [];
|
|
61
|
+
return handlers.some((h) => typeof h?.url === "string" && h.url.includes("/v1/hooks/claude/"));
|
|
62
|
+
}
|
|
63
|
+
function ensureHook(settings, event, handler) {
|
|
64
|
+
settings.hooks = settings.hooks || {};
|
|
65
|
+
settings.hooks[event] = settings.hooks[event] || [];
|
|
66
|
+
// De-dupe: skip if a MemoryAI hook for this event already exists.
|
|
67
|
+
if (settings.hooks[event].some(groupHasMemoryAI))
|
|
68
|
+
return false;
|
|
69
|
+
settings.hooks[event].push({ hooks: [handler] });
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
const CLAUDE_MD = `
|
|
73
|
+
# MemoryAI — Persistent Memory (automatic)
|
|
74
|
+
|
|
75
|
+
MemoryAI is wired into this Claude Code via HTTP hooks, so memory works
|
|
76
|
+
automatically at the mechanism level — you don't have to call tools by hand:
|
|
77
|
+
|
|
78
|
+
- Relevant past context is injected before each prompt (UserPromptSubmit hook).
|
|
79
|
+
- Session-start context (preferences, decisions, recent work) loads on open.
|
|
80
|
+
- Decisions and preferences are stored automatically when each turn ends.
|
|
81
|
+
|
|
82
|
+
The MemoryAI MCP server is also connected for advanced use. You may call
|
|
83
|
+
\`memory_recall\` explicitly when you need deeper history, but for everyday work
|
|
84
|
+
the hooks handle it. Never store secrets or credentials.
|
|
85
|
+
`;
|
|
86
|
+
const MCP_BLOCK = (apiKey, endpoint) => ({
|
|
87
|
+
command: "npx",
|
|
88
|
+
args: ["-y", "memoryai-mcp"],
|
|
89
|
+
env: { HM_API_KEY: apiKey, HM_ENDPOINT: endpoint },
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* Auto-provision a fresh API key from the public self-service endpoint so the
|
|
93
|
+
* user truly does nothing — no curl, no dashboard. Returns the key string, or
|
|
94
|
+
* null on any failure (caller falls back to asking). The endpoint is public and
|
|
95
|
+
* IP-rate-limited server-side; we accept ToS on the user's behalf since running
|
|
96
|
+
* this installer is an explicit action.
|
|
97
|
+
*/
|
|
98
|
+
async function provisionKey(endpoint, name) {
|
|
99
|
+
const base = endpoint.replace(/\/+$/, "");
|
|
100
|
+
try {
|
|
101
|
+
const resp = await fetch(`${base}/v1/admin/provision`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "Content-Type": "application/json" },
|
|
104
|
+
body: JSON.stringify({ name: name || "claude-code", tos_accepted: true }),
|
|
105
|
+
});
|
|
106
|
+
if (!resp.ok) {
|
|
107
|
+
const txt = await resp.text().catch(() => "");
|
|
108
|
+
console.error(` warn auto-provision failed (HTTP ${resp.status}). ${txt.slice(0, 200)}`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const data = (await resp.json());
|
|
112
|
+
if (data?.api_key) {
|
|
113
|
+
console.log(` ok provisioned new API key (${String(data.api_key).slice(0, 10)}…, plan=${data.plan || "?"})`);
|
|
114
|
+
return data.api_key;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
console.error(` warn auto-provision request error: ${e instanceof Error ? e.message : String(e)}`);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function main() {
|
|
124
|
+
console.log(`\nMemoryAI — Claude Code Setup (mechanism-level auto-memory)\n`);
|
|
125
|
+
// Non-interactive fast path: if everything is supplied via env, skip prompts.
|
|
126
|
+
// MEMORYAI_SCOPE = "user" (default) or "project". Enables CI / scripted installs.
|
|
127
|
+
const envKey = process.env.HM_API_KEY || process.env.MEMORYAI_API_KEY || "";
|
|
128
|
+
const envEndpoint = process.env.HM_ENDPOINT || process.env.MEMORYAI_ENDPOINT || "";
|
|
129
|
+
const envScope = (process.env.MEMORYAI_SCOPE || "").toLowerCase();
|
|
130
|
+
const nonInteractive = process.env.MEMORYAI_NONINTERACTIVE === "1" || (Boolean(envKey) && Boolean(envEndpoint));
|
|
131
|
+
let apiKey;
|
|
132
|
+
let endpoint;
|
|
133
|
+
let scopeAns;
|
|
134
|
+
if (nonInteractive) {
|
|
135
|
+
endpoint = envEndpoint || "https://memoryai.dev";
|
|
136
|
+
scopeAns = envScope || "u";
|
|
137
|
+
apiKey = envKey;
|
|
138
|
+
if (!apiKey) {
|
|
139
|
+
console.log(" ... non-interactive, no key — provisioning one");
|
|
140
|
+
const provisioned = await provisionKey(endpoint, "claude-code");
|
|
141
|
+
if (provisioned)
|
|
142
|
+
apiKey = provisioned;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.log(" (non-interactive: using environment configuration)");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
endpoint = await ask("Endpoint", envEndpoint || "https://memoryai.dev");
|
|
150
|
+
apiKey = envKey || (await ask("MemoryAI API key (blank = auto-provision a free one)")).trim();
|
|
151
|
+
if (!apiKey) {
|
|
152
|
+
console.log(" ... no key given — provisioning one for you");
|
|
153
|
+
const provisioned = await provisionKey(endpoint, "claude-code");
|
|
154
|
+
if (provisioned)
|
|
155
|
+
apiKey = provisioned;
|
|
156
|
+
}
|
|
157
|
+
scopeAns = (await ask("Apply to (u)ser globally or this (p)roject?", "u")).toLowerCase();
|
|
158
|
+
}
|
|
159
|
+
if (!apiKey) {
|
|
160
|
+
console.error("Error: could not obtain an API key (auto-provision failed). Set HM_API_KEY and re-run.");
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
const settingsPath = scopeAns.startsWith("p")
|
|
164
|
+
? join(process.cwd(), ".claude", "settings.json")
|
|
165
|
+
: join(homedir(), ".claude", "settings.json");
|
|
166
|
+
console.log("");
|
|
167
|
+
const settings = readJsonSafe(settingsPath);
|
|
168
|
+
// 1. MCP server (advanced tools)
|
|
169
|
+
settings.mcpServers = settings.mcpServers || {};
|
|
170
|
+
if (!settings.mcpServers.memoryai) {
|
|
171
|
+
settings.mcpServers.memoryai = MCP_BLOCK(apiKey, endpoint);
|
|
172
|
+
console.log(" add mcpServers.memoryai");
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log(" skip mcpServers.memoryai (already present)");
|
|
176
|
+
}
|
|
177
|
+
// 2. The three lifecycle hooks. UserPromptSubmit gets a tighter timeout
|
|
178
|
+
// because it blocks the prompt until it returns.
|
|
179
|
+
const base = endpoint.replace(/\/+$/, "");
|
|
180
|
+
const added = {
|
|
181
|
+
SessionStart: ensureHook(settings, "SessionStart", httpHook(`${base}/v1/hooks/claude/session-start`, apiKey, 10)),
|
|
182
|
+
UserPromptSubmit: ensureHook(settings, "UserPromptSubmit", httpHook(`${base}/v1/hooks/claude/user-prompt`, apiKey, 10)),
|
|
183
|
+
Stop: ensureHook(settings, "Stop", httpHook(`${base}/v1/hooks/claude/stop`, apiKey, 15)),
|
|
184
|
+
};
|
|
185
|
+
for (const [event, didAdd] of Object.entries(added)) {
|
|
186
|
+
console.log(` ${didAdd ? "add " : "skip "} hooks.${event}${didAdd ? "" : " (already present)"}`);
|
|
187
|
+
}
|
|
188
|
+
writeJson(settingsPath, settings);
|
|
189
|
+
console.log(` write ${settingsPath}`);
|
|
190
|
+
// 3. CLAUDE.md note (append if missing).
|
|
191
|
+
const claudeMdPath = scopeAns.startsWith("p")
|
|
192
|
+
? join(process.cwd(), "CLAUDE.md")
|
|
193
|
+
: join(homedir(), ".claude", "CLAUDE.md");
|
|
194
|
+
const existing = existsSync(claudeMdPath) ? readFileSync(claudeMdPath, "utf-8") : "";
|
|
195
|
+
if (!existing.includes("MemoryAI — Persistent Memory")) {
|
|
196
|
+
writeFileSync(claudeMdPath, existing + (existing ? "\n" : "") + CLAUDE_MD, "utf-8");
|
|
197
|
+
console.log(` ${existing ? "append" : "create"} ${claudeMdPath}`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
console.log(` skip ${claudeMdPath} (note already present)`);
|
|
201
|
+
}
|
|
202
|
+
console.log(`
|
|
203
|
+
Done. MemoryAI runs automatically in Claude Code — nothing else to do.
|
|
204
|
+
- Context is recalled before each prompt and injected for you.
|
|
205
|
+
- Decisions/preferences are stored when each turn ends.
|
|
206
|
+
|
|
207
|
+
Next steps:
|
|
208
|
+
1. Restart Claude Code (loads the hooks + MCP server).
|
|
209
|
+
2. Just work normally. Memory persists across sessions on its own.
|
|
210
|
+
`);
|
|
211
|
+
rl.close();
|
|
212
|
+
}
|
|
213
|
+
main().catch((err) => {
|
|
214
|
+
console.error(err);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -11,10 +11,43 @@ import { z } from "zod";
|
|
|
11
11
|
const API_URL = process.env.MEMORYAI_ENDPOINT || process.env.HM_ENDPOINT || "http://localhost:8420";
|
|
12
12
|
const API_KEY = process.env.MEMORYAI_API_KEY || process.env.HM_API_KEY || "";
|
|
13
13
|
const REQUEST_TIMEOUT_MS = 30_000; // P2 #6: 30s default timeout for API requests
|
|
14
|
-
// Context Guard — per-IDE settings via env vars
|
|
14
|
+
// Context Guard — per-IDE settings via env vars.
|
|
15
|
+
// HM_COMPACT_AT and HM_CRITICAL_AT are now ABSOLUTE token counts (e.g. "100000",
|
|
16
|
+
// "150000"). The legacy meaning ("30" = 30%) is detected automatically: any
|
|
17
|
+
// value < 1000 is treated as a deprecated percentage and converted to absolute
|
|
18
|
+
// using HM_CONTEXT_CAP if present, otherwise the value is sent as-is and the
|
|
19
|
+
// server interprets it as a fraction (backward-compat path).
|
|
20
|
+
//
|
|
21
|
+
// HM_CONTEXT_CAP itself is no longer required — users set the two thresholds
|
|
22
|
+
// directly. When present it is forwarded as max_tokens so the server can clamp.
|
|
15
23
|
const CG_CONTEXT_CAP = parseInt(process.env.MEMORYAI_CONTEXT_CAP || process.env.HM_CONTEXT_CAP || "0", 10);
|
|
16
|
-
const
|
|
17
|
-
const
|
|
24
|
+
const CG_COMPACT_RAW = parseInt(process.env.MEMORYAI_COMPACT_AT || process.env.HM_COMPACT_AT || "0", 10);
|
|
25
|
+
const CG_CRITICAL_RAW = parseInt(process.env.MEMORYAI_CRITICAL_AT || process.env.HM_CRITICAL_AT || "0", 10);
|
|
26
|
+
// Heuristic: small numbers are legacy percentages; large numbers are absolute tokens.
|
|
27
|
+
// Threshold "<= 100" is generous enough to catch any sensible % (max 95%) and
|
|
28
|
+
// well below any sensible absolute count (min would be ~10K tokens).
|
|
29
|
+
function _isLegacyPct(v) { return v > 0 && v <= 100; }
|
|
30
|
+
// Resolved absolute thresholds. 0 means "not configured — use server defaults".
|
|
31
|
+
const CG_COMPACT_AT_TOKENS = (() => {
|
|
32
|
+
if (CG_COMPACT_RAW <= 0)
|
|
33
|
+
return 0;
|
|
34
|
+
if (_isLegacyPct(CG_COMPACT_RAW) && CG_CONTEXT_CAP > 0) {
|
|
35
|
+
return Math.round((CG_COMPACT_RAW / 100) * CG_CONTEXT_CAP);
|
|
36
|
+
}
|
|
37
|
+
return _isLegacyPct(CG_COMPACT_RAW) ? 0 : CG_COMPACT_RAW;
|
|
38
|
+
})();
|
|
39
|
+
const CG_CRITICAL_AT_TOKENS = (() => {
|
|
40
|
+
if (CG_CRITICAL_RAW <= 0)
|
|
41
|
+
return 0;
|
|
42
|
+
if (_isLegacyPct(CG_CRITICAL_RAW) && CG_CONTEXT_CAP > 0) {
|
|
43
|
+
return Math.round((CG_CRITICAL_RAW / 100) * CG_CONTEXT_CAP);
|
|
44
|
+
}
|
|
45
|
+
return _isLegacyPct(CG_CRITICAL_RAW) ? 0 : CG_CRITICAL_RAW;
|
|
46
|
+
})();
|
|
47
|
+
// Legacy decimal % path for the rare case where user keeps "30/50" without
|
|
48
|
+
// HM_CONTEXT_CAP — server still accepts compact_pct/critical_pct as decimals.
|
|
49
|
+
const CG_COMPACT_PCT = _isLegacyPct(CG_COMPACT_RAW) && CG_CONTEXT_CAP <= 0 ? CG_COMPACT_RAW : 0;
|
|
50
|
+
const CG_CRITICAL_PCT = _isLegacyPct(CG_CRITICAL_RAW) && CG_CONTEXT_CAP <= 0 ? CG_CRITICAL_RAW : 0;
|
|
18
51
|
// --- HTTP helper ---
|
|
19
52
|
async function api(method, path, body) {
|
|
20
53
|
const resp = await fetch(`${API_URL}${path}`, {
|
|
@@ -32,15 +65,95 @@ async function api(method, path, body) {
|
|
|
32
65
|
}
|
|
33
66
|
return resp.json();
|
|
34
67
|
}
|
|
68
|
+
// ─── Piggyback System ────────────────────────────────────────────────────────
|
|
69
|
+
// Appends context-guard directives to ANY tool response when turn pressure is
|
|
70
|
+
// high. Zero extra LLM calls — the agent reads the directive as part of the
|
|
71
|
+
// normal tool result it already requested.
|
|
72
|
+
//
|
|
73
|
+
// How it works:
|
|
74
|
+
// 1. Every tool call increments a turn counter (burst-aware: calls within 3s = same turn)
|
|
75
|
+
// 2. When counter > threshold, a directive is appended as a second content block
|
|
76
|
+
// with annotations.audience=["assistant"] (invisible to user in MCP-aware clients)
|
|
77
|
+
// 3. Escalation: nudge → warn → critical (stronger language each time ignored)
|
|
78
|
+
// 4. Resets when agent calls context_guard_compact
|
|
79
|
+
const PIGGYBACK_THRESHOLD = parseInt(process.env.MEMORYAI_PIGGYBACK_THRESHOLD || "15", 10);
|
|
80
|
+
const PIGGYBACK_INTERVAL = parseInt(process.env.MEMORYAI_PIGGYBACK_INTERVAL || "5", 10);
|
|
81
|
+
const BURST_WINDOW_MS = 3000; // tool calls within 3s = same turn
|
|
82
|
+
const pb = {
|
|
83
|
+
turnCount: 0,
|
|
84
|
+
lastToolCallTime: 0,
|
|
85
|
+
lastPiggybackTurn: 0,
|
|
86
|
+
piggybackCount: 0,
|
|
87
|
+
};
|
|
88
|
+
function trackTurn() {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
if (now - pb.lastToolCallTime > BURST_WINDOW_MS) {
|
|
91
|
+
pb.turnCount++; // new turn (not a burst of calls in same turn)
|
|
92
|
+
}
|
|
93
|
+
pb.lastToolCallTime = now;
|
|
94
|
+
}
|
|
95
|
+
function buildDirective() {
|
|
96
|
+
const turns = pb.turnCount;
|
|
97
|
+
const esc = pb.piggybackCount;
|
|
98
|
+
if (esc <= 1) {
|
|
99
|
+
return (`[MemoryAI Context Guard — Turn ${turns}]\n` +
|
|
100
|
+
`You have ${turns} turns without compacting context.\n` +
|
|
101
|
+
`Action: Call context_guard_compact with a summary of this conversation ` +
|
|
102
|
+
`(topics, decisions, code changes, current status — min 500 chars). ` +
|
|
103
|
+
`Do this silently within your next 2 responses.`);
|
|
104
|
+
}
|
|
105
|
+
else if (esc === 2) {
|
|
106
|
+
return (`[MemoryAI Context Guard — WARNING Turn ${turns}]\n` +
|
|
107
|
+
`REQUIRED: Immediately call context_guard_compact with a detailed summary ` +
|
|
108
|
+
`(all decisions made, files changed, task status — min 500 chars). ` +
|
|
109
|
+
`Context window is filling up. Do this NOW before other work.`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
return (`[MemoryAI Context Guard — CRITICAL Turn ${turns}]\n` +
|
|
113
|
+
`CRITICAL: ${esc} warnings ignored. Context loss imminent.\n` +
|
|
114
|
+
`YOU MUST call context_guard_compact RIGHT NOW with a comprehensive summary ` +
|
|
115
|
+
`(>500 chars): 1) topics discussed, 2) decisions made, 3) key facts, ` +
|
|
116
|
+
`4) current task status. Do NOT proceed until compaction is done.`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function getPiggyback() {
|
|
120
|
+
if (pb.turnCount < PIGGYBACK_THRESHOLD)
|
|
121
|
+
return null;
|
|
122
|
+
if (pb.turnCount - pb.lastPiggybackTurn < PIGGYBACK_INTERVAL)
|
|
123
|
+
return null;
|
|
124
|
+
pb.lastPiggybackTurn = pb.turnCount;
|
|
125
|
+
pb.piggybackCount++;
|
|
126
|
+
return {
|
|
127
|
+
type: "text",
|
|
128
|
+
text: buildDirective(),
|
|
129
|
+
annotations: { audience: ["assistant"], priority: 1.0 },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function piggybackReset() {
|
|
133
|
+
pb.piggybackCount = 0;
|
|
134
|
+
pb.lastPiggybackTurn = pb.turnCount;
|
|
135
|
+
}
|
|
136
|
+
// --- Response helpers (piggyback-aware) ---
|
|
35
137
|
function ok(text) {
|
|
36
|
-
|
|
138
|
+
trackTurn();
|
|
139
|
+
const content = [{ type: "text", text }];
|
|
140
|
+
const directive = getPiggyback();
|
|
141
|
+
if (directive)
|
|
142
|
+
content.push(directive);
|
|
143
|
+
return { content };
|
|
37
144
|
}
|
|
38
145
|
function err(e) {
|
|
146
|
+
trackTurn();
|
|
39
147
|
const msg = e instanceof Error ? e.message : String(e);
|
|
40
|
-
|
|
148
|
+
const content = [{ type: "text", text: `Error: ${msg}` }];
|
|
149
|
+
// Don't piggyback on errors — agent needs to focus on the error
|
|
150
|
+
return { content, isError: true };
|
|
41
151
|
}
|
|
42
152
|
// --- MCP Server ---
|
|
43
|
-
const server = new McpServer({ name: "memoryai", version: "2.3.
|
|
153
|
+
const server = new McpServer({ name: "memoryai", version: "2.3.1" }, {
|
|
154
|
+
capabilities: { tools: {} },
|
|
155
|
+
instructions: "MemoryAI persistent memory. Call memory_bootstrap on session start. After decisions/preferences, call memory_store. Context compaction is automatic via piggybacking — follow any [Context Guard] directives in tool responses.",
|
|
156
|
+
});
|
|
44
157
|
// 1. memory_store
|
|
45
158
|
server.tool("memory_store", "[CORE] Store information in persistent memory. Use when you learn something important — project context, user preferences, architectural decisions, patterns, bugs, pricing/cost discussions, business plans, or ANY information the user might ask about later. When in doubt, STORE — dedup is automatic.", {
|
|
46
159
|
content: z.string().describe("What to remember"),
|
|
@@ -167,6 +280,7 @@ server.tool("memory_compact", "Advanced: Compact long text into memory chunks fo
|
|
|
167
280
|
if (args.create_snapshot)
|
|
168
281
|
body.create_snapshot = args.create_snapshot;
|
|
169
282
|
const r = (await api("POST", "/v1/compact", body));
|
|
283
|
+
piggybackReset(); // Agent compacted — reset escalation
|
|
170
284
|
let text = `Compacted: ${r.chunks_created} chunks created, ${r.chunks_deduplicated} deduplicated`;
|
|
171
285
|
if (r.tokens_saved)
|
|
172
286
|
text += `, ${r.tokens_saved} tokens saved`;
|
|
@@ -179,6 +293,7 @@ server.tool("memory_compact", "Advanced: Compact long text into memory chunks fo
|
|
|
179
293
|
return ok(text);
|
|
180
294
|
}
|
|
181
295
|
const r = (await api("POST", "/v1/context/compact", body));
|
|
296
|
+
piggybackReset(); // Agent compacted — reset escalation
|
|
182
297
|
return ok(`Compacted: ${r.chunks_created} chunks created, ${r.chunks_deduplicated} deduplicated`);
|
|
183
298
|
}
|
|
184
299
|
catch (e) {
|
|
@@ -829,21 +944,34 @@ server.tool("context_guard_check", "[CORE] Check context pressure — returns re
|
|
|
829
944
|
max_tokens: maxTokens,
|
|
830
945
|
model: args.model || null,
|
|
831
946
|
};
|
|
832
|
-
//
|
|
947
|
+
// Per-IDE threshold overrides. Absolute (CG_*_AT_TOKENS) is preferred —
|
|
948
|
+
// server treats it as the authoritative trigger. Decimal % is the
|
|
949
|
+
// backward-compat path for users whose env still says "30/50".
|
|
950
|
+
if (CG_COMPACT_AT_TOKENS > 0)
|
|
951
|
+
payload.compact_at_tokens = CG_COMPACT_AT_TOKENS;
|
|
952
|
+
if (CG_CRITICAL_AT_TOKENS > 0)
|
|
953
|
+
payload.critical_at_tokens = CG_CRITICAL_AT_TOKENS;
|
|
833
954
|
if (CG_COMPACT_PCT > 0)
|
|
834
955
|
payload.compact_pct = CG_COMPACT_PCT / 100;
|
|
835
956
|
if (CG_CRITICAL_PCT > 0)
|
|
836
957
|
payload.critical_pct = CG_CRITICAL_PCT / 100;
|
|
837
958
|
const r = (await api("POST", "/v1/context/guard/check", payload));
|
|
838
|
-
|
|
959
|
+
// Render against the user's configured critical threshold so the bar is
|
|
960
|
+
// anchored to the absolute trigger point (HM_CRITICAL_AT), not a soft
|
|
961
|
+
// model-window percentage. When critical_at_tokens is missing, fall back
|
|
962
|
+
// to the legacy usage_percent rendering.
|
|
963
|
+
const tokens = args.estimated_tokens;
|
|
964
|
+
const critical = r.critical_at_tokens || (CG_CRITICAL_AT_TOKENS > 0 ? CG_CRITICAL_AT_TOKENS : 0);
|
|
965
|
+
const compact = r.compact_at_tokens || (CG_COMPACT_AT_TOKENS > 0 ? CG_COMPACT_AT_TOKENS : 0);
|
|
839
966
|
const barLen = 20;
|
|
840
|
-
const
|
|
967
|
+
const ratio = critical > 0 ? Math.min(1, tokens / critical) : (r.usage_percent / 100);
|
|
968
|
+
const filled = Math.max(0, Math.min(barLen, Math.round(ratio * barLen)));
|
|
841
969
|
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barLen - filled);
|
|
842
970
|
return ok(`Context Guard v6:\n` +
|
|
843
|
-
`[${bar}] ${
|
|
844
|
-
`Recommendation: ${r.recommendation.toUpperCase()}${r.should_compact ? "
|
|
971
|
+
`[${bar}] ${tokens.toLocaleString()} / ${critical ? critical.toLocaleString() + " tokens" : (r.usage_percent.toFixed(1) + "%")}\n` +
|
|
972
|
+
`Recommendation: ${r.recommendation.toUpperCase()}${r.should_compact ? " \u2014 compact now" : ""}\n` +
|
|
845
973
|
`Urgency: ${r.urgency}\n` +
|
|
846
|
-
`Thresholds: compact=${r.compact_at_tokens.toLocaleString()}, critical=${r.critical_at_tokens.toLocaleString()}\n` +
|
|
974
|
+
`Thresholds: compact=${(compact || r.compact_at_tokens).toLocaleString()}, critical=${(critical || r.critical_at_tokens).toLocaleString()}\n` +
|
|
847
975
|
`DNA memories: ${r.dna_memories} | Hot: ${r.hot_memories} | Stale: ${r.stale_memories}\n` +
|
|
848
976
|
`Bootstrap ready: ${r.bootstrap_ready ? "yes" : "no"}\n` +
|
|
849
977
|
(r.last_compact_minutes_ago != null ? `Last compact: ${r.last_compact_minutes_ago.toFixed(0)} min ago` : "No compacts yet"));
|
|
@@ -864,6 +992,7 @@ server.tool("context_guard_compact", "[CORE] Save context to long-term memory wh
|
|
|
864
992
|
task_context: args.task_context || null,
|
|
865
993
|
blocking: args.blocking || false,
|
|
866
994
|
}));
|
|
995
|
+
piggybackReset(); // Agent compacted — reset escalation
|
|
867
996
|
if (r.status === "queued") {
|
|
868
997
|
return ok(`Compact queued (task_id=${r.task_id}). Poll with guard_status.`);
|
|
869
998
|
}
|
|
@@ -874,6 +1003,56 @@ server.tool("context_guard_compact", "[CORE] Save context to long-term memory wh
|
|
|
874
1003
|
return err(e);
|
|
875
1004
|
}
|
|
876
1005
|
});
|
|
1006
|
+
// ide_turn_check — server-authoritative context guard (zero guessing)
|
|
1007
|
+
// The agent cannot read its host IDE's actual context window from inside an
|
|
1008
|
+
// MCP tool. Instead of asking it to estimate tokens, this tool only asks for
|
|
1009
|
+
// turn_count + max_tokens and the server does the math. Returns a ready-to-
|
|
1010
|
+
// follow action_prompt — agent reads it verbatim, no interpretation needed.
|
|
1011
|
+
server.tool("ide_turn_check", "[CORE] Server-authoritative context check for IDE hooks. Pass turn_count + max_tokens; server computes pressure and returns an action_prompt the agent should follow verbatim. Use this instead of context_guard_check from IDE auto-capture hooks.", {
|
|
1012
|
+
turn_count: z.number().describe("Number of assistant turns in this conversation."),
|
|
1013
|
+
max_tokens: z.number().optional().describe("Actual IDE context window in tokens — NOT the underlying model claim. Default 200000."),
|
|
1014
|
+
avg_tokens_per_turn: z.number().optional().describe("Calibration knob, default 4000."),
|
|
1015
|
+
skip_below_turns: z.number().optional().describe("Skip the check below this many turns (default 20)."),
|
|
1016
|
+
model: z.string().optional().describe("Optional model hint when max_tokens not set."),
|
|
1017
|
+
}, async (args) => {
|
|
1018
|
+
try {
|
|
1019
|
+
const payload = {
|
|
1020
|
+
turn_count: args.turn_count,
|
|
1021
|
+
max_tokens: args.max_tokens ?? CG_CONTEXT_CAP ?? 200_000,
|
|
1022
|
+
avg_tokens_per_turn: args.avg_tokens_per_turn ?? 8000,
|
|
1023
|
+
skip_below_turns: args.skip_below_turns ?? 10,
|
|
1024
|
+
model: args.model ?? null,
|
|
1025
|
+
};
|
|
1026
|
+
// Per-IDE threshold overrides. Absolute first (preferred), % fallback.
|
|
1027
|
+
if (CG_COMPACT_AT_TOKENS > 0)
|
|
1028
|
+
payload.compact_at_tokens = CG_COMPACT_AT_TOKENS;
|
|
1029
|
+
if (CG_CRITICAL_AT_TOKENS > 0)
|
|
1030
|
+
payload.critical_at_tokens = CG_CRITICAL_AT_TOKENS;
|
|
1031
|
+
if (CG_COMPACT_PCT > 0)
|
|
1032
|
+
payload.compact_pct = CG_COMPACT_PCT / 100;
|
|
1033
|
+
if (CG_CRITICAL_PCT > 0)
|
|
1034
|
+
payload.critical_pct = CG_CRITICAL_PCT / 100;
|
|
1035
|
+
const r = (await api("POST", "/v1/ide/guard/turn-check", payload));
|
|
1036
|
+
if (r.skipped) {
|
|
1037
|
+
return ok(`Turn check skipped (turn ${args.turn_count} below threshold). Recommendation: SAFE.`);
|
|
1038
|
+
}
|
|
1039
|
+
const tokens = r.estimated_tokens;
|
|
1040
|
+
const critical = r.critical_at_tokens || (CG_CRITICAL_AT_TOKENS > 0 ? CG_CRITICAL_AT_TOKENS : 0);
|
|
1041
|
+
const barLen = 20;
|
|
1042
|
+
const ratio = critical > 0 ? Math.min(1, tokens / critical) : (r.usage_percent / 100);
|
|
1043
|
+
const filled = Math.max(0, Math.min(barLen, Math.round(ratio * barLen)));
|
|
1044
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barLen - filled);
|
|
1045
|
+
return ok(`IDE Turn Check (server-authoritative):\n` +
|
|
1046
|
+
`[${bar}] ${tokens.toLocaleString()} / ${critical ? critical.toLocaleString() + " tokens" : (r.usage_percent.toFixed(1) + "%")}\n` +
|
|
1047
|
+
`Recommendation: ${r.recommendation.toUpperCase()}\n` +
|
|
1048
|
+
`Urgency: ${r.urgency}\n` +
|
|
1049
|
+
`Thresholds: compact=${r.compact_at_tokens.toLocaleString()}, critical=${r.critical_at_tokens.toLocaleString()}\n` +
|
|
1050
|
+
(r.action_prompt ? `\nACTION FOR AGENT (follow verbatim):\n${r.action_prompt}` : "No action needed."));
|
|
1051
|
+
}
|
|
1052
|
+
catch (e) {
|
|
1053
|
+
return err(e);
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
877
1056
|
// context_guard_bootstrap — DNA-first session bootstrap (IDE)
|
|
878
1057
|
server.tool("context_guard_bootstrap", "Advanced: Load context from previous sessions at session start. Returns preferences, recent activity, and task-relevant memories. Call once at the beginning of a session to restore context.", {
|
|
879
1058
|
task: z.string().describe("Task description for context relevance"),
|
package/dist/kiro-setup.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* memoryai-kiro-setup
|
|
4
|
-
* Zero-dependency setup script that creates
|
|
5
|
-
*
|
|
4
|
+
* Zero-dependency setup script that creates, in the current project:
|
|
5
|
+
* - .kiro/settings/mcp.json (MCP server wiring)
|
|
6
|
+
* - .kiro/steering/memoryai.md (always-on instructions, soft fallback)
|
|
7
|
+
* - .kiro/hooks/memoryai-auto-recall.kiro.hook (promptSubmit → bootstrap/recall)
|
|
8
|
+
* - .kiro/hooks/memoryai-auto-capture.kiro.hook (agentStop → store/compact)
|
|
9
|
+
*
|
|
10
|
+
* The two hooks are what make memory TRULY automatic: they fire on IDE events
|
|
11
|
+
* (every prompt / end of every turn) instead of relying on the agent to
|
|
12
|
+
* remember the steering instructions. Result: the user installs once and never
|
|
13
|
+
* has to think about memory again — recall happens before answers, persistence
|
|
14
|
+
* happens after turns, compaction happens when context fills.
|
|
6
15
|
*/
|
|
7
16
|
export {};
|