@aaronsb/kg-cli 0.9.4 → 0.10.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.
Files changed (42) hide show
  1. package/dist/api/client.d.ts +4 -0
  2. package/dist/api/client.d.ts.map +1 -1
  3. package/dist/api/client.js +10 -0
  4. package/dist/api/client.js.map +1 -1
  5. package/dist/cli/ai-config/embedding.d.ts +2 -2
  6. package/dist/cli/ai-config/embedding.d.ts.map +1 -1
  7. package/dist/cli/ai-config/embedding.js +262 -129
  8. package/dist/cli/ai-config/embedding.js.map +1 -1
  9. package/dist/cli/program.d.ts.map +1 -1
  10. package/dist/cli/program.js +7 -1
  11. package/dist/cli/program.js.map +1 -1
  12. package/dist/lib/config.d.ts +18 -0
  13. package/dist/lib/config.d.ts.map +1 -1
  14. package/dist/lib/config.js +24 -0
  15. package/dist/lib/config.js.map +1 -1
  16. package/dist/lib/table.d.ts.map +1 -1
  17. package/dist/lib/table.js +1 -0
  18. package/dist/lib/table.js.map +1 -1
  19. package/dist/mcp/formatters/concept.d.ts.map +1 -1
  20. package/dist/mcp/formatters/concept.js +6 -1
  21. package/dist/mcp/formatters/concept.js.map +1 -1
  22. package/dist/mcp/formatters/index.d.ts +1 -0
  23. package/dist/mcp/formatters/index.d.ts.map +1 -1
  24. package/dist/mcp/formatters/index.js +5 -1
  25. package/dist/mcp/formatters/index.js.map +1 -1
  26. package/dist/mcp/formatters/job.js +5 -5
  27. package/dist/mcp/formatters/job.js.map +1 -1
  28. package/dist/mcp/formatters/session.d.ts +19 -0
  29. package/dist/mcp/formatters/session.d.ts.map +1 -0
  30. package/dist/mcp/formatters/session.js +39 -0
  31. package/dist/mcp/formatters/session.js.map +1 -0
  32. package/dist/mcp/graph-operations.d.ts.map +1 -1
  33. package/dist/mcp/graph-operations.js +15 -7
  34. package/dist/mcp/graph-operations.js.map +1 -1
  35. package/dist/mcp-server.js +203 -16
  36. package/dist/mcp-server.js.map +1 -1
  37. package/dist/types/index.d.ts +1 -1
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/version.d.ts +3 -3
  40. package/dist/version.js +3 -3
  41. package/dist/version.js.map +1 -1
  42. package/package.json +1 -1
@@ -47,6 +47,7 @@ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
47
47
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
48
48
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
49
49
  const client_js_1 = require("./api/client.js");
50
+ const config_js_1 = require("./lib/config.js");
50
51
  const auth_client_js_1 = require("./lib/auth/auth-client.js");
51
52
  const mcp_allowlist_js_1 = require("./lib/mcp-allowlist.js");
52
53
  const index_js_2 = require("./mcp/formatters/index.js");
@@ -220,6 +221,62 @@ function createAuthenticatedClient() {
220
221
  }
221
222
  // Client instance - will be initialized in main() before server starts
222
223
  let client;
224
+ /**
225
+ * Fetch recent concepts from the graph for session context.
226
+ * Returns a compact summary and structured data. Fails gracefully if API is unreachable.
227
+ */
228
+ async function fetchRecentConcepts(limit, ontology) {
229
+ try {
230
+ let query = 'MATCH (c:Concept) WHERE c.created_at_epoch IS NOT NULL';
231
+ if (ontology) {
232
+ // Whitelist validation — ontology names are slugified strings
233
+ if (!/^[a-zA-Z0-9_\-. ()]+$/.test(ontology)) {
234
+ return { summary: `Invalid ontology name: ${ontology}`, concepts: [] };
235
+ }
236
+ query += ` AND c.ontology = '${ontology}'`;
237
+ }
238
+ query += ' RETURN c ORDER BY c.created_at_epoch DESC';
239
+ const result = await client.executeProgram({
240
+ program: {
241
+ version: 1,
242
+ statements: [{
243
+ op: '+',
244
+ operation: { type: 'cypher', query, limit },
245
+ label: 'recent_concepts',
246
+ }],
247
+ },
248
+ });
249
+ const nodes = result.result.nodes || [];
250
+ const concepts = nodes.map((n) => ({
251
+ label: n.label,
252
+ concept_id: n.concept_id,
253
+ ontology: n.ontology ?? null,
254
+ created_at_epoch: n.properties?.created_at_epoch ?? 0,
255
+ }));
256
+ if (concepts.length === 0) {
257
+ return { summary: 'No concepts in the knowledge graph yet.', concepts: [] };
258
+ }
259
+ const labels = concepts.map((c) => c.label).join(' | ');
260
+ return { summary: `Recent concepts (${concepts.length}): ${labels}`, concepts };
261
+ }
262
+ catch {
263
+ return { summary: 'Knowledge graph unavailable — API server may not be running.', concepts: [] };
264
+ }
265
+ }
266
+ /**
267
+ * Resolve the ontology name for session ingest.
268
+ * Priority: config oauth_client_name > env KG_OAUTH_CLIENT_ID > 'sessions'
269
+ */
270
+ function getSessionOntology() {
271
+ const config = (0, config_js_1.getConfig)();
272
+ const clientName = config.get('auth.oauth_client_name');
273
+ if (clientName)
274
+ return clientName;
275
+ const envClientId = process.env.KG_OAUTH_CLIENT_ID;
276
+ if (envClientId)
277
+ return envClientId;
278
+ return 'sessions';
279
+ }
223
280
  /**
224
281
  * Tool Definitions
225
282
  *
@@ -227,6 +284,28 @@ let client;
227
284
  */
228
285
  // List available tools
229
286
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
287
+ // Build dynamic session context
288
+ const config = (0, config_js_1.getConfig)();
289
+ let sessionContextDescription;
290
+ if (config.getSessionContextEnabled()) {
291
+ const { summary } = await fetchRecentConcepts(config.getSessionContextLimit(), config.getSessionContextOntology());
292
+ sessionContextDescription = `Recent knowledge graph activity. This tool provides active, unsolicited state information about the knowledge graph — what concepts were most recently created or encountered.
293
+
294
+ ${summary}
295
+
296
+ Call this tool to refresh context, adjust the limit, or filter by ontology. Results include concept labels, IDs, ontology, and creation epoch.`;
297
+ }
298
+ else {
299
+ sessionContextDescription = 'Read recent knowledge graph activity. Session context is currently disabled in config (~/.config/kg/config.json session.context_enabled).';
300
+ }
301
+ const sessionOntology = getSessionOntology();
302
+ const sessionIngestDescription = `Save a session summary to the knowledge graph for cross-session memory.
303
+
304
+ **IMPORTANT: Before your session ends or context compacts, call this tool with a summary of key topics, decisions, and insights from this session.**
305
+
306
+ Text is ingested into the "${sessionOntology}" ontology, creating concepts that will appear in session_context on your next connection. The ingest job is auto-approved.
307
+
308
+ Input: text (required) — your session summary. Optional: filename for source tracking.`;
230
309
  return {
231
310
  tools: [
232
311
  {
@@ -257,6 +336,10 @@ RECOMMENDED WORKFLOW: After search, use concept (action: "connect") to find HOW
257
336
 
258
337
  For multi-step exploration, compose searches into a GraphProgram (program tool) instead of making individual calls. One program can seed from search, expand relationships, and filter — all server-side in a single round-trip. Use program (action: "list") to find reusable stored programs, or read the program/syntax resource for composition examples.
259
338
 
339
+ ESCALATION: For analytical questions or exploring more than 2 concepts, go directly to the program tool — one composed query replaces many individual calls.
340
+
341
+ To verify a result, use source to retrieve the original text behind any evidence, or concept (action: "details") to see all evidence and relationships for a concept.
342
+
260
343
  Use 2-3 word phrases (e.g., "linear thinking patterns").`,
261
344
  inputSchema: {
262
345
  type: 'object',
@@ -298,9 +381,11 @@ Use 2-3 word phrases (e.g., "linear thinking patterns").`,
298
381
  name: 'concept',
299
382
  description: `Work with concepts: get details (ALL evidence + relationships), find related concepts (neighborhood exploration), or discover connections (paths between concepts).
300
383
 
301
- For "connect" action, defaults (threshold=0.5, max_hops=5) match the CLI and work well for most queries. Use higher thresholds (0.75+) only if you need to narrow results for precision.
384
+ For "connect" action, defaults (threshold=0.5, max_hops=5) match the CLI and work well for most queries. Use higher thresholds (0.75+) only if you need to narrow results for precision. Note: connect traverses semantic edges only (IMPLIES, SUPPORTS, CONTRADICTS, etc.) — manually-created edges with no traversal history may not appear in paths. Use program/Cypher for comprehensive traversal.
385
+
386
+ If connect returns no paths or you need to combine multiple lookups, escalate to the program tool — one composed query replaces many individual calls. Do not repeat connect hoping for different results.
302
387
 
303
- For multi-step workflows (search → connect → expand → filter), compose these into a GraphProgram instead of making individual calls. See the program tool and program/syntax resource.`,
388
+ For multi-step workflows (search → connect → expand → filter), compose these into a GraphProgram instead of making individual calls. For example, seed from a search then expand via Cypher using $W_IDS to reference accumulated concept IDs. See the program tool and program/syntax resource for this and other composition patterns.`,
304
389
  inputSchema: {
305
390
  type: 'object',
306
391
  properties: {
@@ -343,7 +428,7 @@ For multi-step workflows (search → connect → expand → filter), compose the
343
428
  relationship_types: {
344
429
  type: 'array',
345
430
  items: { type: 'string' },
346
- description: 'Filter relationships (e.g., ["SUPPORTS", "CONTRADICTS"])',
431
+ description: 'Filter relationships (e.g., ["SUPPORTS", "CONTRADICTS"]). Constrains traversal, not just results — if the first hop is structural, filtering to semantic types may return empty. Omit for broadest results, then narrow.',
347
432
  },
348
433
  // ADR-065: Epistemic status filtering (for related and connect)
349
434
  include_epistemic_status: {
@@ -626,7 +711,9 @@ Use when you need to:
626
711
  - Verify extracted concepts against original source
627
712
  - Get the full context of a text passage
628
713
  - Retrieve images for visual analysis
629
- - Check character offsets for highlighting`,
714
+ - Check character offsets for highlighting
715
+
716
+ Source IDs appear in search results and concept details evidence. Use concept (action: "details") to see all evidence for a concept, or search (type: "sources") to find passages directly.`,
630
717
  inputSchema: {
631
718
  type: 'object',
632
719
  properties: {
@@ -655,7 +742,9 @@ EPISTEMIC STATUS CLASSIFICATIONS:
655
742
  - INSUFFICIENT_DATA: <3 successful measurements
656
743
  - UNCLASSIFIED: Doesn't fit known patterns
657
744
 
658
- Use for filtering relationships by epistemic reliability, identifying contested knowledge areas, and curating high-confidence vs exploratory subgraphs.`,
745
+ Use for filtering relationships by epistemic reliability, identifying contested knowledge areas, and curating high-confidence vs exploratory subgraphs.
746
+
747
+ Concept (action: "related") and connect accept include_epistemic_status/exclude_epistemic_status filters to narrow traversals by reliability. Use search to find concepts in contested areas, then epistemic_status to understand why.`,
659
748
  inputSchema: {
660
749
  type: 'object',
661
750
  properties: {
@@ -711,7 +800,9 @@ Use Cases:
711
800
  - Explore conceptual spectrums and gradients
712
801
  - Identify position-grounding correlation patterns
713
802
  - Discover concepts balanced between opposing ideas
714
- - Map semantic dimensions in the knowledge graph`,
803
+ - Map semantic dimensions in the knowledge graph
804
+
805
+ Requires concept IDs for poles — use search to find opposing concepts first. Use concept (action: "details") to inspect pole concepts before analysis.`,
715
806
  inputSchema: {
716
807
  type: 'object',
717
808
  properties: {
@@ -811,7 +902,7 @@ Three actions available:
811
902
  - "concepts": Get all concepts extracted from a document
812
903
 
813
904
  Documents are aggregated from source chunks and stored in Garage (S3-compatible storage).
814
- Use search tool with type="documents" to find documents semantically.`,
905
+ Use search tool with type="documents" to find documents semantically. Use document (action: "concepts") to see what was extracted, then concept (action: "details") or source to drill into specifics.`,
815
906
  inputSchema: {
816
907
  type: 'object',
817
908
  properties: {
@@ -875,7 +966,8 @@ Use for manual curation, agent-driven knowledge building, and precise graph mani
875
966
 
876
967
  **Semantic Resolution:**
877
968
  - Use \`from_label\`/\`to_label\` to reference concepts by name instead of ID
878
- - Resolution uses vector similarity (85% threshold) to find matching concepts
969
+ - Resolution uses vector similarity (75% threshold) to find matching concepts
970
+ - Near-misses (60-75%) return "Did you mean?" suggestions with concept IDs
879
971
 
880
972
  **Examples:**
881
973
  - Create concept: \`{action: "create", entity: "concept", label: "CAP Theorem", ontology: "distributed-systems"}\`
@@ -894,7 +986,7 @@ Use for manual curation, agent-driven knowledge building, and precise graph mani
894
986
  ]
895
987
  }
896
988
  \`\`\`
897
- Queue executes sequentially, stops on first error (unless continue_on_error=true). Max 20 operations.`,
989
+ Queue executes sequentially, continues past errors by default (set continue_on_error=false to stop on first error). Max 20 operations.`,
898
990
  inputSchema: {
899
991
  type: 'object',
900
992
  properties: {
@@ -939,8 +1031,8 @@ Queue executes sequentially, stops on first error (unless continue_on_error=true
939
1031
  },
940
1032
  continue_on_error: {
941
1033
  type: 'boolean',
942
- description: 'For queue: continue executing after errors (default: false, stop on first error)',
943
- default: false,
1034
+ description: 'For queue: continue executing after errors (default: true). Set false to stop on first error.',
1035
+ default: true,
944
1036
  },
945
1037
  // Concept fields
946
1038
  label: {
@@ -1042,6 +1134,8 @@ Queue executes sequentially, stops on first error (unless continue_on_error=true
1042
1134
  name: 'program',
1043
1135
  description: `Compose and execute GraphProgram queries against the knowledge graph (ADR-500).
1044
1136
 
1137
+ Use search/connect/related for quick lookups (one concept, one path). Use program when you need the neighborhood of more than 2 concepts, want to combine search with traversal, or are asking an analytical question about graph structure. If you've made 3+ individual tool calls without converging, you should already be here.
1138
+
1045
1139
  Programs are JSON ASTs that compose Cypher queries and API calls using set-algebra operators.
1046
1140
  Each statement applies an operator to merge/filter results into a mutable Working Graph (W).
1047
1141
 
@@ -1067,6 +1161,11 @@ Each statement applies an operator to merge/filter results into a mutable Workin
1067
1161
  { type: "cypher", query: "MATCH (c:Concept)-[r]->(t:Concept) RETURN c, r, t", limit?: 20 }
1068
1162
  Queries must be read-only (no CREATE/SET/DELETE/MERGE). RETURN nodes and relationships.
1069
1163
 
1164
+ **AGE Cypher gotchas** (Apache AGE openCypher differs from Neo4j):
1165
+ - Filter relationship types with WHERE, not inline: \`WHERE type(r) IN ['SUPPORTS', 'CONTRADICTS']\` (NOT \`[r:SUPPORTS|CONTRADICTS]\`)
1166
+ - Reference working graph concepts: \`WHERE c.concept_id IN $W_IDS\`
1167
+ - Always RETURN both nodes and relationships for full path data
1168
+
1070
1169
  **ApiOp** — call internal service functions (no HTTP):
1071
1170
  { type: "api", endpoint: "/search/concepts", params: { query: "...", limit: 10 } }
1072
1171
 
@@ -1074,7 +1173,7 @@ Each statement applies an operator to merge/filter results into a mutable Workin
1074
1173
  /search/concepts — params: query (required), min_similarity?, limit?
1075
1174
  /search/sources — params: query (required), min_similarity?, limit?
1076
1175
  /concepts/details — params: concept_id (required)
1077
- /concepts/related — params: concept_id (required), max_depth?, relationship_types?
1176
+ /concepts/related — params: concept_id (required), max_depth?, relationship_types? [returns nodes + edges in programs]
1078
1177
  /concepts/batch — params: concept_ids (required, list)
1079
1178
  /vocabulary/status — params: relationship_type?, status_filter?
1080
1179
 
@@ -1087,6 +1186,9 @@ Each statement applies an operator to merge/filter results into a mutable Workin
1087
1186
  label: "expand relationships" }
1088
1187
  ]}
1089
1188
 
1189
+ **Alternative** — API-only composition (no Cypher needed):
1190
+ Use /concepts/related to expand from a known concept ID. Inside programs, related returns both nodes and edges (topology), making it suitable for graph exploration without writing Cypher.
1191
+
1090
1192
  Read the program/syntax resource for the complete language reference with more examples.`,
1091
1193
  inputSchema: {
1092
1194
  type: 'object',
@@ -1136,6 +1238,43 @@ Read the program/syntax resource for the complete language reference with more e
1136
1238
  required: ['action'],
1137
1239
  },
1138
1240
  },
1241
+ {
1242
+ name: 'session_context',
1243
+ description: sessionContextDescription,
1244
+ inputSchema: {
1245
+ type: 'object',
1246
+ properties: {
1247
+ limit: {
1248
+ type: 'number',
1249
+ description: 'Maximum concepts to return (default: 10, max: 50)',
1250
+ default: 10,
1251
+ },
1252
+ ontology: {
1253
+ type: 'string',
1254
+ description: 'Filter by ontology name (default: all ontologies)',
1255
+ },
1256
+ },
1257
+ required: [],
1258
+ },
1259
+ },
1260
+ {
1261
+ name: 'session_ingest',
1262
+ description: sessionIngestDescription,
1263
+ inputSchema: {
1264
+ type: 'object',
1265
+ properties: {
1266
+ text: {
1267
+ type: 'string',
1268
+ description: 'Session summary text to ingest into the knowledge graph',
1269
+ },
1270
+ filename: {
1271
+ type: 'string',
1272
+ description: 'Optional filename for source tracking (e.g., "session-2025-01-15.md")',
1273
+ },
1274
+ },
1275
+ required: ['text'],
1276
+ },
1277
+ },
1139
1278
  ],
1140
1279
  };
1141
1280
  });
@@ -2347,7 +2486,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
2347
2486
  }
2348
2487
  case 'queue': {
2349
2488
  const operations = toolArgs.operations;
2350
- const continueOnError = toolArgs.continue_on_error === true;
2489
+ const continueOnError = toolArgs.continue_on_error !== false;
2351
2490
  if (!operations || !Array.isArray(operations)) {
2352
2491
  throw new Error('operations array is required for queue action');
2353
2492
  }
@@ -2483,6 +2622,11 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
2483
2622
  });
2484
2623
  const lines = [];
2485
2624
  const totalMs = result.log.reduce((sum, e) => sum + e.duration_ms, 0);
2625
+ // Build id → label map for readable link output
2626
+ const nodeLabels = new Map();
2627
+ for (const node of result.result.nodes) {
2628
+ nodeLabels.set(node.concept_id, node.label);
2629
+ }
2486
2630
  if (result.aborted) {
2487
2631
  lines.push(`✗ Aborted at statement ${result.aborted.statement}: ${result.aborted.reason}`);
2488
2632
  }
@@ -2516,7 +2660,9 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
2516
2660
  if (result.result.links.length > 0) {
2517
2661
  lines.push('\nLinks:');
2518
2662
  for (const link of result.result.links.slice(0, 30)) {
2519
- lines.push(` ${link.from_id} ${link.relationship_type} → ${link.to_id}`);
2663
+ const from = nodeLabels.get(link.from_id) || link.from_id;
2664
+ const to = nodeLabels.get(link.to_id) || link.to_id;
2665
+ lines.push(` ${from} → ${link.relationship_type} → ${to}`);
2520
2666
  }
2521
2667
  if (result.result.links.length > 30) {
2522
2668
  lines.push(` ... and ${result.result.links.length - 30} more`);
@@ -2579,6 +2725,11 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
2579
2725
  try {
2580
2726
  const result = await client.chainPrograms(toolArgs.deck);
2581
2727
  const lines = [];
2728
+ // Build id → label map for readable link output
2729
+ const nodeLabels = new Map();
2730
+ for (const node of result.result.nodes) {
2731
+ nodeLabels.set(node.concept_id, node.label);
2732
+ }
2582
2733
  if (result.aborted) {
2583
2734
  lines.push(`✗ Chain aborted at statement ${result.aborted.statement}: ${result.aborted.reason}`);
2584
2735
  }
@@ -2609,7 +2760,9 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
2609
2760
  if (result.result.links.length > 0) {
2610
2761
  lines.push('\nLinks:');
2611
2762
  for (const link of result.result.links.slice(0, 30)) {
2612
- lines.push(` ${link.from_id} ${link.relationship_type} → ${link.to_id}`);
2763
+ const from = nodeLabels.get(link.from_id) || link.from_id;
2764
+ const to = nodeLabels.get(link.to_id) || link.to_id;
2765
+ lines.push(` ${from} → ${link.relationship_type} → ${to}`);
2613
2766
  }
2614
2767
  if (result.result.links.length > 30) {
2615
2768
  lines.push(` ... and ${result.result.links.length - 30} more`);
@@ -2634,6 +2787,39 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
2634
2787
  throw new Error(`Unknown program action: ${action}. Use: validate, create, get, list, execute, or chain`);
2635
2788
  }
2636
2789
  }
2790
+ case 'session_context': {
2791
+ const limit = Math.min(toolArgs.limit || 10, 50);
2792
+ const ontology = toolArgs.ontology || null;
2793
+ const { summary, concepts } = await fetchRecentConcepts(limit, ontology);
2794
+ return {
2795
+ content: [{
2796
+ type: 'text',
2797
+ text: (0, index_js_2.formatSessionContext)(concepts, summary),
2798
+ }],
2799
+ };
2800
+ }
2801
+ case 'session_ingest': {
2802
+ const text = toolArgs.text;
2803
+ if (!text) {
2804
+ throw new Error('text is required');
2805
+ }
2806
+ const ontology = getSessionOntology();
2807
+ const result = await client.ingestText(text, {
2808
+ ontology,
2809
+ filename: toolArgs.filename || undefined,
2810
+ auto_approve: true,
2811
+ force: false,
2812
+ processing_mode: 'serial',
2813
+ options: {
2814
+ target_words: 1000,
2815
+ overlap_words: 200,
2816
+ },
2817
+ source_type: 'mcp',
2818
+ });
2819
+ return {
2820
+ content: [{ type: 'text', text: (0, index_js_2.formatSessionIngest)(result) }],
2821
+ };
2822
+ }
2637
2823
  default:
2638
2824
  throw new Error(`Unknown tool: ${name}`);
2639
2825
  }
@@ -2850,7 +3036,8 @@ Allowed endpoints and their parameters:
2850
3036
  /concepts/related
2851
3037
  Required: concept_id (string)
2852
3038
  Optional: max_depth (integer, default 2), relationship_types (string array)
2853
- Returns: neighborhood concept nodes
3039
+ Returns: neighborhood concept nodes + edges between them (full topology).
3040
+ Note: relationship_types constrains traversal — omit for broadest results.
2854
3041
 
2855
3042
  /concepts/batch
2856
3043
  Required: concept_ids (string array)