Skip to content

Commit d336a29

Browse files
feat: wire agent memory integrations
1 parent 0936c94 commit d336a29

13 files changed

Lines changed: 269 additions & 38 deletions

File tree

README.md

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This README is only for setting up Kairo as a user.
1717
## Install
1818

1919
```sh
20-
npm install -g @kairohq/cli
20+
npm install -g @kairohq/cli @kairohq/mcp
2121
```
2222

2323
## Initialize A Project
@@ -28,8 +28,9 @@ Run this inside the project you want Kairo to remember.
2828
kairo init
2929
```
3030

31-
Guided setup can import supported local assistant transcripts and configure an
32-
optional AI provider. You can skip both and still use local project memory.
31+
`kairo init` creates local Kairo state, installs Claude Code and Codex capture
32+
hooks, and registers the local Kairo MCP server so those assistants can query
33+
project memory. No Kairo AI provider is required by default.
3334

3435
## Ingest Existing History
3536

@@ -51,14 +52,8 @@ kairo doctor
5152
kairo ask "what changed in the dashboard data source?"
5253
```
5354

54-
## Optional MCP Server
55-
56-
Install this only when an MCP-compatible assistant should query Kairo memory.
57-
58-
```sh
59-
npm install -g @kairohq/mcp
60-
kairo-mcp
61-
```
55+
Claude Code and Codex can also ask Kairo through the MCP setup written by
56+
`kairo init`.
6257

6358
## Where To Read More
6459

apps/cli/src/commands/doctor.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ describe("runDoctor", () => {
3535
{ label: ".git directory", ok: true },
3636
{ label: "Claude Code hook", ok: true },
3737
{ label: "Codex hook", ok: true },
38+
{ label: "Kairo MCP config", ok: true },
39+
{ label: "Codex MCP config", ok: true },
40+
{ label: "Agent memory guidance", ok: true },
41+
{ label: "Claude memory guidance", ok: true },
3842
]);
3943
expect(result.gateways.map((gateway) => [gateway.name, gateway.readiness])).toEqual([
4044
["codex", "ready"],

apps/cli/src/commands/doctor.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ export async function runDoctor(
4444
{ label: ".git directory", ok: existsSync(join(cwd, ".git")) },
4545
{ label: "Claude Code hook", ok: existsSync(join(cwd, ".claude/hooks.json")) },
4646
{ label: "Codex hook", ok: existsSync(join(cwd, ".codex/hooks.json")) },
47+
{ label: "Kairo MCP config", ok: existsSync(join(cwd, ".mcp.json")) },
48+
{ label: "Codex MCP config", ok: existsSync(join(cwd, ".codex/config.toml")) },
49+
{ label: "Agent memory guidance", ok: existsSync(join(cwd, "AGENTS.md")) },
50+
{ label: "Claude memory guidance", ok: existsSync(join(cwd, "CLAUDE.md")) },
4751
],
4852
gateways,
4953
};

apps/cli/src/commands/init.test.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ describe("runInit", () => {
1919
const first = runInit({ name: "demo" }, projectRoot);
2020
const claudePath = join(projectRoot, ".claude", "hooks.json");
2121
const codexPath = join(projectRoot, ".codex", "hooks.json");
22+
const mcpPath = join(projectRoot, ".mcp.json");
23+
const agentsPath = join(projectRoot, "AGENTS.md");
24+
const claudeGuidePath = join(projectRoot, "CLAUDE.md");
2225
const firstClaude = readFileSync(claudePath, "utf8");
2326
const firstCodex = readFileSync(codexPath, "utf8");
27+
const firstMcp = readFileSync(mcpPath, "utf8");
28+
const firstAgents = readFileSync(agentsPath, "utf8");
29+
const firstClaudeGuide = readFileSync(claudeGuidePath, "utf8");
2430

2531
const second = runInit({ name: "ignored" }, projectRoot);
2632

@@ -35,19 +41,19 @@ describe("runInit", () => {
3541
});
3642
expect(readFileSync(claudePath, "utf8")).toBe(firstClaude);
3743
expect(readFileSync(codexPath, "utf8")).toBe(firstCodex);
44+
expect(readFileSync(mcpPath, "utf8")).toBe(firstMcp);
45+
expect(readFileSync(agentsPath, "utf8")).toBe(firstAgents);
46+
expect(readFileSync(claudeGuidePath, "utf8")).toBe(firstClaudeGuide);
47+
expect(JSON.parse(firstMcp).mcpServers.kairo.command).toBe("kairo-mcp");
48+
expect(firstAgents).toContain("Kairo Project Memory");
49+
expect(firstClaudeGuide).toContain("Kairo Project Memory");
3850
});
3951

40-
it("writes MiniMax AI config by default", () => {
52+
it("uses local memory without AI by default", () => {
4153
runInit({ name: "demo" }, projectRoot);
4254
const config = JSON.parse(readFileSync(join(projectRoot, ".kairo", "config.json"), "utf8"));
4355

44-
expect(config.ai).toEqual({
45-
provider: "minimax",
46-
model: "MiniMax-M2.7",
47-
apiKeyEnv: "MINIMAX_API_KEY",
48-
authMode: "api-key",
49-
baseUrl: "https://api.minimax.io/v1",
50-
});
56+
expect(config.ai).toBeNull();
5157
expect(config.agentIngest).toEqual({ enabled: false, providers: [] });
5258
});
5359

apps/cli/src/commands/init.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ async function withOnboarding(opts: InitOptions): Promise<InitOptions> {
180180
const wantsProvider = await confirm(
181181
readline,
182182
"Use an AI provider for natural-language answers and summaries?",
183-
true,
183+
false,
184184
);
185185
if (!wantsProvider) return { ...opts, agentProviders, aiProvider: "none" };
186186

@@ -209,7 +209,7 @@ function agentIngestConfigFromOptions(agentProviders: string | undefined) {
209209
}
210210

211211
function aiConfigFromOptions(
212-
providerChoice = "minimax",
212+
providerChoice = "none",
213213
authChoice?: string,
214214
apiKeyEnvChoice?: string,
215215
): WorkspaceAiConfigType | null {

apps/mcp/src/tools/ask.test.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,7 @@ describe("askProjectMemory", () => {
3636
store.close();
3737
}
3838

39-
const answer = await askProjectMemory(
40-
{ workspace },
41-
"Why did dashboard routing change?",
42-
5,
43-
false,
44-
);
39+
const answer = await askProjectMemory({ workspace }, "Why did dashboard routing change?", 5);
4540

4641
expect(answer?.answer).toContain("Dashboard routing split");
4742
expect(answer?.citations[0]).toMatchObject({

apps/mcp/src/tools/ask.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export async function askProjectMemory(
99
context: ToolContext,
1010
question: string,
1111
limit: number,
12-
useAi = true,
12+
useAi = false,
1313
): Promise<MemoryAnswer | null> {
1414
const trimmed = question.trim();
1515
if (!context.workspace || trimmed.length === 0) return null;
@@ -40,7 +40,7 @@ export function registerAskTool(server: McpServer, context: ToolContext): void {
4040
{
4141
question: z.string(),
4242
limit: z.number().int().positive().max(10).default(5),
43-
useAi: z.boolean().default(true),
43+
useAi: z.boolean().default(false),
4444
},
4545
async ({ question, limit, useAi }) => ({
4646
content: [

packages/core/src/hooks/merge-hooks.test.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
55
import {
66
installKairoHooks,
77
mergeKairoHooks,
8+
mergeKairoMcpConfig,
9+
mergeManagedText,
810
removeKairoHooks,
11+
removeKairoMcpConfig,
12+
removeManagedText,
913
uninstallKairoHooks,
1014
} from "./merge-hooks.ts";
1115

@@ -103,18 +107,70 @@ describe("mergeKairoHooks", () => {
103107
});
104108
});
105109

110+
describe("mergeKairoMcpConfig", () => {
111+
it("preserves existing MCP servers and installs Kairo", () => {
112+
expect(
113+
mergeKairoMcpConfig(
114+
{ mcpServers: { custom: { command: "custom-mcp" } } },
115+
{ mcpServers: { kairo: { command: "kairo-mcp", args: [], enabled: true } } },
116+
),
117+
).toEqual({
118+
mcpServers: {
119+
custom: { command: "custom-mcp" },
120+
kairo: { command: "kairo-mcp", args: [], enabled: true },
121+
},
122+
});
123+
});
124+
});
125+
126+
describe("managed text sections", () => {
127+
const marker = {
128+
start: "<!-- KAIRO:TEST:START -->",
129+
end: "<!-- KAIRO:TEST:END -->",
130+
};
131+
const template = "<!-- KAIRO:TEST:START -->\nmanaged\n<!-- KAIRO:TEST:END -->\n";
132+
133+
it("appends and refreshes one managed section", () => {
134+
const once = mergeManagedText("# Existing\n", template, marker);
135+
const twice = mergeManagedText(once, template.replace("managed", "refreshed"), marker);
136+
137+
expect(once).toContain("# Existing");
138+
expect(twice.match(/KAIRO:TEST:START/g)).toHaveLength(1);
139+
expect(twice).toContain("refreshed");
140+
expect(twice).not.toContain("managed\n");
141+
});
142+
143+
it("removes only the managed section", () => {
144+
const merged = mergeManagedText("# Existing\n", template, marker);
145+
146+
expect(removeManagedText(merged, marker)).toBe("# Existing");
147+
});
148+
});
149+
106150
describe("installKairoHooks", () => {
107-
it("reads templates from disk and writes idempotent Claude and Codex hook files", () => {
151+
it("reads templates from disk and writes idempotent agent integration files", () => {
108152
installKairoHooks(projectRoot);
109153
const claudePath = join(projectRoot, ".claude", "hooks.json");
110154
const codexPath = join(projectRoot, ".codex", "hooks.json");
155+
const mcpPath = join(projectRoot, ".mcp.json");
156+
const codexConfigPath = join(projectRoot, ".codex", "config.toml");
157+
const agentsPath = join(projectRoot, "AGENTS.md");
158+
const claudeGuidePath = join(projectRoot, "CLAUDE.md");
111159
const firstClaude = readFileSync(claudePath, "utf8");
112160
const firstCodex = readFileSync(codexPath, "utf8");
161+
const firstMcp = readFileSync(mcpPath, "utf8");
162+
const firstCodexConfig = readFileSync(codexConfigPath, "utf8");
163+
const firstAgents = readFileSync(agentsPath, "utf8");
164+
const firstClaudeGuide = readFileSync(claudeGuidePath, "utf8");
113165

114166
installKairoHooks(projectRoot);
115167

116168
expect(readFileSync(claudePath, "utf8")).toBe(firstClaude);
117169
expect(readFileSync(codexPath, "utf8")).toBe(firstCodex);
170+
expect(readFileSync(mcpPath, "utf8")).toBe(firstMcp);
171+
expect(readFileSync(codexConfigPath, "utf8")).toBe(firstCodexConfig);
172+
expect(readFileSync(agentsPath, "utf8")).toBe(firstAgents);
173+
expect(readFileSync(claudeGuidePath, "utf8")).toBe(firstClaudeGuide);
118174
const claude = JSON.parse(firstClaude);
119175
expect(claude.hooks.PostToolUse[0].kairo).toBe(true);
120176
expect(claude.hooks.PreCompact[0]).toMatchObject({
@@ -126,6 +182,13 @@ describe("installKairoHooks", () => {
126182
hooks: [{ command: "kairo wake --days 7" }],
127183
});
128184
expect(JSON.parse(firstCodex).hooks[0].kairo).toBe(true);
185+
expect(JSON.parse(firstMcp).mcpServers.kairo).toMatchObject({
186+
command: "kairo-mcp",
187+
enabled: true,
188+
});
189+
expect(firstCodexConfig).toContain("[mcp_servers.kairo]");
190+
expect(firstAgents).toContain("Kairo Project Memory");
191+
expect(firstClaudeGuide).toContain("Kairo Project Memory");
129192
});
130193

131194
it("preserves pre-existing non-Kairo hooks when installing", () => {
@@ -151,6 +214,23 @@ describe("installKairoHooks", () => {
151214
});
152215
});
153216

217+
describe("removeKairoMcpConfig", () => {
218+
it("removes only the Kairo MCP server", () => {
219+
expect(
220+
removeKairoMcpConfig({
221+
mcpServers: {
222+
custom: { command: "custom" },
223+
kairo: { command: "kairo-mcp" },
224+
},
225+
}),
226+
).toEqual({
227+
mcpServers: {
228+
custom: { command: "custom" },
229+
},
230+
});
231+
});
232+
});
233+
154234
describe("removeKairoHooks", () => {
155235
it("removes only tagged Claude hooks", () => {
156236
expect(
@@ -194,15 +274,21 @@ describe("uninstallKairoHooks", () => {
194274
installKairoHooks(projectRoot);
195275
const claudePath = join(projectRoot, ".claude", "hooks.json");
196276
const codexPath = join(projectRoot, ".codex", "hooks.json");
277+
const mcpPath = join(projectRoot, ".mcp.json");
278+
const codexConfigPath = join(projectRoot, ".codex", "config.toml");
279+
const agentsPath = join(projectRoot, "AGENTS.md");
197280
const claudeWithCustom = JSON.parse(readFileSync(claudePath, "utf8"));
198281
const codexWithCustom = JSON.parse(readFileSync(codexPath, "utf8"));
282+
const mcpWithCustom = JSON.parse(readFileSync(mcpPath, "utf8"));
199283
claudeWithCustom.hooks.PostToolUse.unshift({
200284
matcher: "Write",
201285
hooks: [{ type: "command", command: "custom" }],
202286
});
203287
codexWithCustom.hooks.unshift({ event: "post_edit", command: "custom" });
288+
mcpWithCustom.mcpServers.custom = { command: "custom" };
204289
writeFileSync(claudePath, JSON.stringify(claudeWithCustom));
205290
writeFileSync(codexPath, JSON.stringify(codexWithCustom));
291+
writeFileSync(mcpPath, JSON.stringify(mcpWithCustom));
206292

207293
uninstallKairoHooks(projectRoot);
208294

@@ -212,5 +298,9 @@ describe("uninstallKairoHooks", () => {
212298
expect(JSON.stringify(codex)).toContain("custom");
213299
expect(JSON.stringify(claude)).not.toContain('"kairo":true');
214300
expect(JSON.stringify(codex)).not.toContain('"kairo":true');
301+
expect(readFileSync(mcpPath, "utf8")).toContain("custom");
302+
expect(readFileSync(mcpPath, "utf8")).not.toContain("kairo-mcp");
303+
expect(readFileSync(codexConfigPath, "utf8")).not.toContain("[mcp_servers.kairo]");
304+
expect(readFileSync(agentsPath, "utf8")).not.toContain("Kairo Project Memory");
215305
});
216306
});

0 commit comments

Comments
 (0)