@infinitedusky/indusk-mcp 1.7.0 → 1.7.3
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/bin/commands/init.js +43 -3
- package/dist/lib/graphiti-client.d.ts +93 -0
- package/dist/lib/graphiti-client.js +209 -0
- package/dist/tools/graph-tools.js +31 -0
- package/extensions/graphiti/manifest.json +24 -0
- package/extensions/graphiti/skill.md +91 -0
- package/extensions/nextjs/manifest.json +1 -13
- package/extensions/nextjs/skill.md +0 -20
- package/extensions/otel/skill.md +4 -11
- package/package.json +1 -1
- package/skills/handoff.md +2 -9
|
@@ -215,6 +215,45 @@ export async function init(projectRoot, options = {}) {
|
|
|
215
215
|
else {
|
|
216
216
|
console.info(" skip: codegraphcontext MCP server (already exists)");
|
|
217
217
|
}
|
|
218
|
+
// 4b. Check infrastructure container
|
|
219
|
+
console.info("\n[Infrastructure]");
|
|
220
|
+
try {
|
|
221
|
+
const infraStatus = execSync("docker inspect --format='{{.State.Running}}' indusk-infra", {
|
|
222
|
+
encoding: "utf-8",
|
|
223
|
+
timeout: 5000,
|
|
224
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
225
|
+
}).trim();
|
|
226
|
+
if (infraStatus === "true") {
|
|
227
|
+
console.info(" ok: indusk-infra container is running");
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.info(" starting: indusk-infra container...");
|
|
231
|
+
execSync("docker start indusk-infra", {
|
|
232
|
+
timeout: 15000,
|
|
233
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
234
|
+
});
|
|
235
|
+
console.info(" started: indusk-infra");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
console.info(" skip: indusk-infra container not found");
|
|
240
|
+
console.info(" To set up infrastructure: indusk infra start");
|
|
241
|
+
}
|
|
242
|
+
// 4c. Copy Graphiti extension manifest
|
|
243
|
+
const graphitiExtDir = join(projectRoot, ".indusk/extensions/graphiti");
|
|
244
|
+
const graphitiManifest = join(graphitiExtDir, "manifest.json");
|
|
245
|
+
if (existsSync(graphitiManifest) && !force) {
|
|
246
|
+
console.info(" skip: graphiti extension (already exists)");
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
mkdirSync(graphitiExtDir, { recursive: true });
|
|
250
|
+
cpSync(join(packageRoot, "extensions/graphiti/manifest.json"), graphitiManifest);
|
|
251
|
+
const skillSource = join(packageRoot, "extensions/graphiti/skill.md");
|
|
252
|
+
if (existsSync(skillSource)) {
|
|
253
|
+
cpSync(skillSource, join(graphitiExtDir, "skill.md"));
|
|
254
|
+
}
|
|
255
|
+
console.info(" create: .indusk/extensions/graphiti/ (manifest + skill)");
|
|
256
|
+
}
|
|
218
257
|
// 5. Generate .vscode/settings.json
|
|
219
258
|
console.info("\n[Editor]");
|
|
220
259
|
const vscodePath = join(projectRoot, ".vscode/settings.json");
|
|
@@ -568,7 +607,8 @@ export async function init(projectRoot, options = {}) {
|
|
|
568
607
|
console.info("\nDone!");
|
|
569
608
|
console.info("\n⚠ Restart Claude Code to load the updated MCP server and skills.");
|
|
570
609
|
console.info("\nNext steps:");
|
|
571
|
-
console.info(" 1.
|
|
572
|
-
console.info(" 2.
|
|
573
|
-
console.info(" 3.
|
|
610
|
+
console.info(" 1. Set up infrastructure (if not done): indusk infra start");
|
|
611
|
+
console.info(" 2. Restart Claude Code");
|
|
612
|
+
console.info(" 3. Edit CLAUDE.md with your project details");
|
|
613
|
+
console.info(" 4. Start planning: /plan your-first-feature");
|
|
574
614
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
interface AddEpisodeOptions {
|
|
2
|
+
groupId?: string;
|
|
3
|
+
source?: string;
|
|
4
|
+
sourceDescription?: string;
|
|
5
|
+
}
|
|
6
|
+
interface SearchOptions {
|
|
7
|
+
groupIds?: string[];
|
|
8
|
+
maxResults?: number;
|
|
9
|
+
}
|
|
10
|
+
interface GraphitiNode {
|
|
11
|
+
name: string;
|
|
12
|
+
uuid: string;
|
|
13
|
+
summary: string;
|
|
14
|
+
group_id: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
interface GraphitiFact {
|
|
18
|
+
uuid: string;
|
|
19
|
+
fact: string;
|
|
20
|
+
valid_at: string | null;
|
|
21
|
+
invalid_at: string | null;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* MCP client wrapper for the Graphiti temporal knowledge graph.
|
|
26
|
+
*
|
|
27
|
+
* Connects to the Graphiti MCP server running inside the indusk-infra container.
|
|
28
|
+
* Lazy connection — connects on first call, not at construction.
|
|
29
|
+
* Graceful degradation — returns null/empty on connection failure, never throws.
|
|
30
|
+
*/
|
|
31
|
+
export declare class GraphitiClient {
|
|
32
|
+
private client;
|
|
33
|
+
private transport;
|
|
34
|
+
private connected;
|
|
35
|
+
private connecting;
|
|
36
|
+
private serverUrl;
|
|
37
|
+
private projectName;
|
|
38
|
+
constructor(projectName: string, serverUrl?: string);
|
|
39
|
+
/**
|
|
40
|
+
* Lazily connect to the Graphiti MCP server.
|
|
41
|
+
* Returns true if connected, false if connection failed.
|
|
42
|
+
* Multiple concurrent calls share the same connection attempt.
|
|
43
|
+
*/
|
|
44
|
+
connect(): Promise<boolean>;
|
|
45
|
+
private doConnect;
|
|
46
|
+
private getClient;
|
|
47
|
+
/**
|
|
48
|
+
* Add an episode to the knowledge graph.
|
|
49
|
+
*
|
|
50
|
+
* @param name - Short name for the episode (e.g., "auth-refactor decision")
|
|
51
|
+
* @param body - Episode content to extract entities and facts from
|
|
52
|
+
* @param options - groupId defaults to project name, source defaults to "text"
|
|
53
|
+
* @returns Success response or null on failure
|
|
54
|
+
*/
|
|
55
|
+
addEpisode(name: string, body: string, options?: AddEpisodeOptions): Promise<{
|
|
56
|
+
success: boolean;
|
|
57
|
+
} | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Search for entity nodes in the knowledge graph.
|
|
60
|
+
*
|
|
61
|
+
* @param query - Natural language search query
|
|
62
|
+
* @param options - groupIds defaults to [projectName, "shared"] for cross-group search
|
|
63
|
+
* @returns Array of matching nodes, or empty array on failure
|
|
64
|
+
*/
|
|
65
|
+
searchNodes(query: string, options?: SearchOptions): Promise<GraphitiNode[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Search for facts (relationships between entities) in the knowledge graph.
|
|
68
|
+
*
|
|
69
|
+
* @param query - Natural language search query
|
|
70
|
+
* @param options - groupIds defaults to [projectName, "shared"] for cross-group search
|
|
71
|
+
* @returns Array of matching facts, or empty array on failure
|
|
72
|
+
*/
|
|
73
|
+
searchFacts(query: string, options?: SearchOptions): Promise<GraphitiFact[]>;
|
|
74
|
+
/**
|
|
75
|
+
* Check if the Graphiti server is reachable and healthy.
|
|
76
|
+
*
|
|
77
|
+
* @returns Status object or null if unreachable
|
|
78
|
+
*/
|
|
79
|
+
getStatus(): Promise<{
|
|
80
|
+
status: string;
|
|
81
|
+
} | null>;
|
|
82
|
+
/**
|
|
83
|
+
* Resolve group IDs for search — always includes "shared" for cross-group search.
|
|
84
|
+
* If caller provides groupIds, uses those. Otherwise defaults to [projectName, "shared"].
|
|
85
|
+
* Deduplicates "shared" if already present.
|
|
86
|
+
*/
|
|
87
|
+
private resolveGroupIds;
|
|
88
|
+
/**
|
|
89
|
+
* Disconnect from the Graphiti MCP server.
|
|
90
|
+
*/
|
|
91
|
+
disconnect(): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { getInfraConfig } from "./infra-config.js";
|
|
4
|
+
/**
|
|
5
|
+
* MCP client wrapper for the Graphiti temporal knowledge graph.
|
|
6
|
+
*
|
|
7
|
+
* Connects to the Graphiti MCP server running inside the indusk-infra container.
|
|
8
|
+
* Lazy connection — connects on first call, not at construction.
|
|
9
|
+
* Graceful degradation — returns null/empty on connection failure, never throws.
|
|
10
|
+
*/
|
|
11
|
+
export class GraphitiClient {
|
|
12
|
+
client = null;
|
|
13
|
+
transport = null;
|
|
14
|
+
connected = false;
|
|
15
|
+
connecting = null;
|
|
16
|
+
serverUrl;
|
|
17
|
+
projectName;
|
|
18
|
+
constructor(projectName, serverUrl) {
|
|
19
|
+
this.projectName = projectName;
|
|
20
|
+
this.serverUrl = serverUrl ?? getInfraConfig().graphiti.url;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Lazily connect to the Graphiti MCP server.
|
|
24
|
+
* Returns true if connected, false if connection failed.
|
|
25
|
+
* Multiple concurrent calls share the same connection attempt.
|
|
26
|
+
*/
|
|
27
|
+
async connect() {
|
|
28
|
+
if (this.connected)
|
|
29
|
+
return true;
|
|
30
|
+
if (this.connecting)
|
|
31
|
+
return this.connecting;
|
|
32
|
+
this.connecting = this.doConnect();
|
|
33
|
+
const result = await this.connecting;
|
|
34
|
+
this.connecting = null;
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
async doConnect() {
|
|
38
|
+
try {
|
|
39
|
+
// Normalize URL — Graphiti redirects /mcp/ to /mcp
|
|
40
|
+
const url = this.serverUrl.endsWith("/") ? this.serverUrl.slice(0, -1) : this.serverUrl;
|
|
41
|
+
this.transport = new StreamableHTTPClientTransport(new URL(url));
|
|
42
|
+
this.client = new Client({ name: "indusk-mcp", version: "1.0.0" });
|
|
43
|
+
await this.client.connect(this.transport);
|
|
44
|
+
this.connected = true;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
this.client = null;
|
|
49
|
+
this.transport = null;
|
|
50
|
+
this.connected = false;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
getClient() {
|
|
55
|
+
if (!this.client)
|
|
56
|
+
throw new Error("Not connected");
|
|
57
|
+
return this.client;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Add an episode to the knowledge graph.
|
|
61
|
+
*
|
|
62
|
+
* @param name - Short name for the episode (e.g., "auth-refactor decision")
|
|
63
|
+
* @param body - Episode content to extract entities and facts from
|
|
64
|
+
* @param options - groupId defaults to project name, source defaults to "text"
|
|
65
|
+
* @returns Success response or null on failure
|
|
66
|
+
*/
|
|
67
|
+
async addEpisode(name, body, options) {
|
|
68
|
+
if (!(await this.connect()))
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
const result = await this.getClient().callTool({
|
|
72
|
+
name: "add_memory",
|
|
73
|
+
arguments: {
|
|
74
|
+
name,
|
|
75
|
+
episode_body: body,
|
|
76
|
+
group_id: options?.groupId ?? this.projectName,
|
|
77
|
+
source: options?.source ?? "text",
|
|
78
|
+
source_description: options?.sourceDescription ?? "",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
return { success: !result.isError };
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Search for entity nodes in the knowledge graph.
|
|
89
|
+
*
|
|
90
|
+
* @param query - Natural language search query
|
|
91
|
+
* @param options - groupIds defaults to [projectName, "shared"] for cross-group search
|
|
92
|
+
* @returns Array of matching nodes, or empty array on failure
|
|
93
|
+
*/
|
|
94
|
+
async searchNodes(query, options) {
|
|
95
|
+
if (!(await this.connect()))
|
|
96
|
+
return [];
|
|
97
|
+
const groupIds = this.resolveGroupIds(options?.groupIds);
|
|
98
|
+
try {
|
|
99
|
+
const result = await this.getClient().callTool({
|
|
100
|
+
name: "search_nodes",
|
|
101
|
+
arguments: {
|
|
102
|
+
query,
|
|
103
|
+
group_ids: groupIds,
|
|
104
|
+
max_nodes: options?.maxResults ?? 10,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
if (result.isError)
|
|
108
|
+
return [];
|
|
109
|
+
const content = result.content;
|
|
110
|
+
const text = content?.[0];
|
|
111
|
+
if (text?.text) {
|
|
112
|
+
const parsed = JSON.parse(text.text);
|
|
113
|
+
return parsed.nodes ?? parsed ?? [];
|
|
114
|
+
}
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Search for facts (relationships between entities) in the knowledge graph.
|
|
123
|
+
*
|
|
124
|
+
* @param query - Natural language search query
|
|
125
|
+
* @param options - groupIds defaults to [projectName, "shared"] for cross-group search
|
|
126
|
+
* @returns Array of matching facts, or empty array on failure
|
|
127
|
+
*/
|
|
128
|
+
async searchFacts(query, options) {
|
|
129
|
+
if (!(await this.connect()))
|
|
130
|
+
return [];
|
|
131
|
+
const groupIds = this.resolveGroupIds(options?.groupIds);
|
|
132
|
+
try {
|
|
133
|
+
const result = await this.getClient().callTool({
|
|
134
|
+
name: "search_memory_facts",
|
|
135
|
+
arguments: {
|
|
136
|
+
query,
|
|
137
|
+
group_ids: groupIds,
|
|
138
|
+
max_facts: options?.maxResults ?? 10,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
if (result.isError)
|
|
142
|
+
return [];
|
|
143
|
+
const content = result.content;
|
|
144
|
+
const text = content?.[0];
|
|
145
|
+
if (text?.text) {
|
|
146
|
+
const parsed = JSON.parse(text.text);
|
|
147
|
+
return parsed.facts ?? parsed ?? [];
|
|
148
|
+
}
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check if the Graphiti server is reachable and healthy.
|
|
157
|
+
*
|
|
158
|
+
* @returns Status object or null if unreachable
|
|
159
|
+
*/
|
|
160
|
+
async getStatus() {
|
|
161
|
+
if (!(await this.connect()))
|
|
162
|
+
return null;
|
|
163
|
+
try {
|
|
164
|
+
const result = await this.getClient().callTool({
|
|
165
|
+
name: "get_status",
|
|
166
|
+
arguments: {},
|
|
167
|
+
});
|
|
168
|
+
if (result.isError)
|
|
169
|
+
return null;
|
|
170
|
+
const content = result.content;
|
|
171
|
+
const text = content?.[0];
|
|
172
|
+
if (text?.text) {
|
|
173
|
+
return JSON.parse(text.text);
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Resolve group IDs for search — always includes "shared" for cross-group search.
|
|
183
|
+
* If caller provides groupIds, uses those. Otherwise defaults to [projectName, "shared"].
|
|
184
|
+
* Deduplicates "shared" if already present.
|
|
185
|
+
*/
|
|
186
|
+
resolveGroupIds(groupIds) {
|
|
187
|
+
const ids = groupIds ?? [this.projectName];
|
|
188
|
+
if (!ids.includes("shared")) {
|
|
189
|
+
ids.push("shared");
|
|
190
|
+
}
|
|
191
|
+
return ids;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Disconnect from the Graphiti MCP server.
|
|
195
|
+
*/
|
|
196
|
+
async disconnect() {
|
|
197
|
+
if (this.client) {
|
|
198
|
+
try {
|
|
199
|
+
await this.client.close();
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// ignore
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
this.client = null;
|
|
206
|
+
this.transport = null;
|
|
207
|
+
this.connected = false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -412,6 +412,37 @@ export function registerGraphTools(server, projectRoot) {
|
|
|
412
412
|
});
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
|
+
// 5. Check Graphiti health
|
|
416
|
+
if (steps.every((s) => s.step !== "falkordb-container" || s.status !== "error")) {
|
|
417
|
+
try {
|
|
418
|
+
const health = execSync("curl -sf http://localhost:8100/health", {
|
|
419
|
+
encoding: "utf-8",
|
|
420
|
+
timeout: 5000,
|
|
421
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
422
|
+
}).trim();
|
|
423
|
+
if (health.includes("healthy")) {
|
|
424
|
+
steps.push({
|
|
425
|
+
step: "graphiti-health",
|
|
426
|
+
status: "ok",
|
|
427
|
+
detail: "Graphiti MCP server healthy",
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
steps.push({
|
|
432
|
+
step: "graphiti-health",
|
|
433
|
+
status: "error",
|
|
434
|
+
detail: "Graphiti responded but not healthy",
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
steps.push({
|
|
440
|
+
step: "graphiti-health",
|
|
441
|
+
status: "error",
|
|
442
|
+
detail: "Graphiti MCP server not reachable on localhost:8100 — may still be starting (takes ~90s)",
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
415
446
|
const hasErrors = steps.some((s) => s.status === "error");
|
|
416
447
|
const hasFixed = steps.some((s) => s.status === "fixed");
|
|
417
448
|
return {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "graphiti",
|
|
3
|
+
"description": "Graphiti temporal knowledge graph — episodic memory, contradiction detection, semantic search via FalkorDB",
|
|
4
|
+
"provides": {
|
|
5
|
+
"skill": true,
|
|
6
|
+
"health_checks": [
|
|
7
|
+
{
|
|
8
|
+
"name": "indusk-infra-running",
|
|
9
|
+
"command": "docker inspect --format='{{.State.Running}}' indusk-infra 2>/dev/null | grep true"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "graphiti-server-reachable",
|
|
13
|
+
"command": "curl -sf http://localhost:8100/health > /dev/null 2>&1"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "falkordb-reachable",
|
|
17
|
+
"command": "redis-cli -h localhost ping 2>/dev/null | grep PONG"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"detect": {
|
|
22
|
+
"command": "docker inspect --format='{{.State.Running}}' indusk-infra 2>/dev/null | grep true"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Graphiti — Temporal Knowledge Graph
|
|
2
|
+
|
|
3
|
+
Graphiti is an episodic memory system backed by FalkorDB. It extracts entities and facts from text, detects contradictions, and supports semantic search across project-specific and shared knowledge.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
- **Episode capture**: After a decision, retro finding, or correction — anything worth remembering across sessions
|
|
8
|
+
- **Search**: Before making assumptions — check what's already known about a topic
|
|
9
|
+
- **Context retrieval**: At session start or when working in an unfamiliar area
|
|
10
|
+
|
|
11
|
+
## Core Concepts
|
|
12
|
+
|
|
13
|
+
### Episodes
|
|
14
|
+
An episode is a chunk of text that Graphiti processes into entities and facts. Think of it as "something that happened" — a decision was made, a bug was found, a convention was established.
|
|
15
|
+
|
|
16
|
+
### Group IDs
|
|
17
|
+
Every episode belongs to a group. Groups isolate knowledge:
|
|
18
|
+
|
|
19
|
+
| Group | Purpose | Example |
|
|
20
|
+
|-------|---------|---------|
|
|
21
|
+
| `{project-name}` | Project-specific knowledge | `infinitedusky`, `numero` |
|
|
22
|
+
| `shared` | Cross-project conventions | Developer preferences, universal patterns |
|
|
23
|
+
|
|
24
|
+
When searching, always include both the project group and `shared` to get the full picture. The `GraphitiClient` does this automatically.
|
|
25
|
+
|
|
26
|
+
### Entities and Facts
|
|
27
|
+
Graphiti extracts:
|
|
28
|
+
- **Entities**: Named things (tools, patterns, files, concepts)
|
|
29
|
+
- **Facts**: Relationships between entities with temporal validity
|
|
30
|
+
|
|
31
|
+
Facts can be contradicted — if you add "the parser handles three gate types" and later "the parser handles four gate types", Graphiti invalidates the old fact.
|
|
32
|
+
|
|
33
|
+
## Patterns
|
|
34
|
+
|
|
35
|
+
### Capturing a Decision
|
|
36
|
+
After an ADR is accepted or a significant choice is made:
|
|
37
|
+
```
|
|
38
|
+
addEpisode("auth-approach-decision",
|
|
39
|
+
"We chose JWT with refresh tokens over session cookies because the API serves both web and mobile clients. Session cookies don't work well with React Native.",
|
|
40
|
+
{ groupId: "myproject" })
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Capturing a Correction
|
|
44
|
+
When someone corrects the agent or a mistake is found:
|
|
45
|
+
```
|
|
46
|
+
addEpisode("correction-test-database",
|
|
47
|
+
"Integration tests must use a real database, not mocks. We got burned when mocked tests passed but the production migration failed.",
|
|
48
|
+
{ groupId: "shared" })
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Searching Before Acting
|
|
52
|
+
Before making assumptions about how something works:
|
|
53
|
+
```
|
|
54
|
+
searchNodes("authentication middleware")
|
|
55
|
+
searchFacts("how does auth work in this project")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Capturing a Retrospective Finding
|
|
59
|
+
After a plan retrospective surfaces a useful insight:
|
|
60
|
+
```
|
|
61
|
+
addEpisode("retro-gate-enforcement",
|
|
62
|
+
"Plan gates need hook-based enforcement, not just instructions. The agent skipped gates when they were advisory only. PreToolUse hooks that block phase transitions are the fix.",
|
|
63
|
+
{ groupId: "myproject" })
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## What NOT to Capture
|
|
67
|
+
|
|
68
|
+
- Code structure (CGC handles this)
|
|
69
|
+
- Git history (git log handles this)
|
|
70
|
+
- Ephemeral state (current task, in-progress work)
|
|
71
|
+
- Things already in CLAUDE.md or lessons
|
|
72
|
+
|
|
73
|
+
Graphiti is for knowledge that has temporal context — decisions that might change, facts that might be contradicted, insights that accumulate over time.
|
|
74
|
+
|
|
75
|
+
## Infrastructure
|
|
76
|
+
|
|
77
|
+
Graphiti runs inside the `indusk-infra` container alongside FalkorDB:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
indusk infra start # start the container
|
|
81
|
+
indusk infra status # check health
|
|
82
|
+
indusk infra stop # stop (preserves data)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Global config (API keys, OTel): `~/.indusk/config.env`
|
|
86
|
+
|
|
87
|
+
### Graceful Degradation
|
|
88
|
+
If the `indusk-infra` container is down:
|
|
89
|
+
- CGC graph tools still check FalkorDB directly
|
|
90
|
+
- Graphiti client methods return null/empty (never throw)
|
|
91
|
+
- The agent continues working with flat-file context (CLAUDE.md, lessons, skills)
|
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nextjs",
|
|
3
3
|
"description": "Next.js 13+ patterns — App Router, server components, caching, performance",
|
|
4
|
-
"provides": {
|
|
5
|
-
"skill": true,
|
|
6
|
-
"health_checks": [
|
|
7
|
-
{
|
|
8
|
-
"name": "no-turbopack-in-dev-scripts",
|
|
9
|
-
"command": "! grep -rl '\\-\\-turbopack' apps/*/package.json 2>/dev/null | head -1 | grep -q ."
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"name": "webpack-aggregate-timeout-configured",
|
|
13
|
-
"command": "for f in apps/*/next.config.*; do [ -f \"$f\" ] && grep -q 'aggregateTimeout' \"$f\" || { echo \"missing aggregateTimeout: $f\"; exit 1; }; done"
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
},
|
|
4
|
+
"provides": { "skill": true },
|
|
17
5
|
"detect": { "dependency": "next" }
|
|
18
6
|
}
|
|
@@ -24,26 +24,6 @@ You are working in a Next.js project. Follow these patterns.
|
|
|
24
24
|
- Dynamic imports (`next/dynamic`) for heavy client components
|
|
25
25
|
- Minimize `"use client"` boundaries — push them as deep in the component tree as possible
|
|
26
26
|
|
|
27
|
-
## Dev Server — Webpack Only
|
|
28
|
-
|
|
29
|
-
- **Do not use Turbopack** — its 1ms file watcher debounce on macOS causes crashes when files are written by external tools (MCP servers, code generation, etc.)
|
|
30
|
-
- **Next.js < 16:** plain `next dev` uses webpack by default — just don't add `--turbopack`
|
|
31
|
-
- **Next.js 16+:** Turbopack is the default — use `next dev --webpack` to opt out
|
|
32
|
-
- **Always include `aggregateTimeout: 600`** in webpack watchOptions in `next.config.*`
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
// next.config.ts
|
|
36
|
-
const nextConfig = {
|
|
37
|
-
webpack: (config) => {
|
|
38
|
-
config.watchOptions = {
|
|
39
|
-
...config.watchOptions,
|
|
40
|
-
aggregateTimeout: 600,
|
|
41
|
-
};
|
|
42
|
-
return config;
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
```
|
|
46
|
-
|
|
47
27
|
## Common Gotchas
|
|
48
28
|
|
|
49
29
|
- `"use client"` doesn't mean the component only renders on the client — it still SSRs. It means it hydrates.
|
package/extensions/otel/skill.md
CHANGED
|
@@ -189,19 +189,17 @@ Wrap this in a helper so every log call includes trace context automatically. Wi
|
|
|
189
189
|
|
|
190
190
|
## Error Propagation
|
|
191
191
|
|
|
192
|
-
Errors must always include trace context. Never swallow silently
|
|
193
|
-
|
|
194
|
-
**Important: Span events (`AddEvent`, `RecordException`) are deprecated as of March 2026.** Use log-based events correlated with the active span instead. The log automatically inherits trace context when emitted inside an active span.
|
|
192
|
+
Errors must always include trace context. Never swallow silently:
|
|
195
193
|
|
|
196
194
|
```typescript
|
|
197
195
|
try {
|
|
198
196
|
await processSettlement(hand);
|
|
199
197
|
} catch (err) {
|
|
200
|
-
// 1.
|
|
198
|
+
// 1. Record on span
|
|
199
|
+
span.recordException(err);
|
|
201
200
|
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
202
201
|
|
|
203
|
-
// 2. Log
|
|
204
|
-
// This replaces span.recordException(err) which is deprecated
|
|
202
|
+
// 2. Log with correlation
|
|
205
203
|
logger.error({ err, ...getTraceContext() }, 'settlement failed');
|
|
206
204
|
|
|
207
205
|
// 3. Re-throw — don't swallow
|
|
@@ -213,11 +211,6 @@ Every catch block should either:
|
|
|
213
211
|
1. Re-throw with context preserved
|
|
214
212
|
2. Log with trace correlation and handle completely
|
|
215
213
|
|
|
216
|
-
**Migration from span events to logs:**
|
|
217
|
-
- `span.addEvent('name', attrs)` → `logger.info({ ...attrs, ...getTraceContext() }, 'name')`
|
|
218
|
-
- `span.recordException(err)` → `logger.error({ err, ...getTraceContext() }, 'error message')`
|
|
219
|
-
- Span status (`setStatus`) is NOT deprecated — still use it to mark spans as errored
|
|
220
|
-
|
|
221
214
|
## Sensitive Data
|
|
222
215
|
|
|
223
216
|
Never attach these to spans, logs, or metrics:
|
package/package.json
CHANGED
package/skills/handoff.md
CHANGED
|
@@ -46,18 +46,11 @@ Create or overwrite `.claude/handoff.md` with:
|
|
|
46
46
|
|
|
47
47
|
## When to Write a Handoff
|
|
48
48
|
|
|
49
|
+
- Before ending any session where work was done
|
|
49
50
|
- When the user says "let's stop here", "wrap up", "hand off"
|
|
51
|
+
- When you're about to run out of context
|
|
50
52
|
- `/handoff` explicitly
|
|
51
53
|
|
|
52
|
-
## When to Suggest a Handoff
|
|
53
|
-
|
|
54
|
-
Only suggest a handoff in these situations — never ask unprompted otherwise:
|
|
55
|
-
|
|
56
|
-
- **You are running low on context** and can feel the conversation getting long. Say so plainly: "I'm getting close to context limits — want me to write a handoff?"
|
|
57
|
-
- **A major milestone just completed** (e.g., a full plan phase, a feature landed, a retrospective finished) and there's no obvious next task queued up. Mention it once — if the user keeps going, don't ask again.
|
|
58
|
-
|
|
59
|
-
Do **not** suggest a handoff after every task, at natural pauses, or as a closing question. The user decides when to stop.
|
|
60
|
-
|
|
61
54
|
## Rules
|
|
62
55
|
|
|
63
56
|
- **Be specific.** "Working on Phase 3" is useless. "Phase 3, item 4: refactored check_health to use extensions. extensions_status MCP tool created. Next: refactor init to remove hardcoded FalkorDB/CGC." is useful.
|