@feelingmindful/thinking-graph 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { recallSchema, recallHandler } from './tools/recall.js';
10
10
  import { learnSchema, learnHandler } from './tools/learn.js';
11
11
  import { exportSchema, exportHandler } from './tools/export.js';
12
12
  import { researchSchema, researchHandler } from './tools/research.js';
13
+ import { recommendSkillsSchema, recommendSkillsHandler } from './tools/recommend-skills.js';
13
14
  // Legacy compat shim removed — use `think` tool directly
14
15
  // ─── Storage setup ───────────────────────────────────────
15
16
  const memoryOnly = process.env.THINKING_GRAPH_MEMORY_ONLY === 'true';
@@ -32,6 +33,7 @@ server.tool('recall', 'Query the thinking graph — search by text, filter by ty
32
33
  server.tool('learn', 'Store durable knowledge — code facts, tech debt, insights, principles. Deduplicates similar content.', learnSchema.shape, async (input) => learnHandler(graph, input));
33
34
  server.tool('export', 'Export the thinking graph as JSON or a human-readable markdown summary.', exportSchema.shape, async (input) => exportHandler(graph, input));
34
35
  server.tool('research', 'Research a topic using Perplexity/Firecrawl, then ingest findings into the graph. Two-phase: call once to get an action plan, then again with findings to store them.', researchSchema.shape, async (input) => researchHandler(graph, input));
36
+ server.tool('recommend-skills', 'Recommend installed marketplace skills by area, verb, platform, or what they produce/detect. Use during reasoning to find skills that can help with the current task.', recommendSkillsSchema.shape, async (input) => recommendSkillsHandler(graph, input));
35
37
  // ─── Startup ─────────────────────────────────────────────
36
38
  async function main() {
37
39
  await storage.initialize();
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import type { ThinkingGraph } from '../engine/graph.js';
3
+ export declare const findSkillsSchema: z.ZodObject<{
4
+ query: z.ZodOptional<z.ZodString>;
5
+ area: z.ZodOptional<z.ZodString>;
6
+ verb: z.ZodOptional<z.ZodString>;
7
+ platform: z.ZodOptional<z.ZodEnum<["ios", "android", "web", "all"]>>;
8
+ produces: z.ZodOptional<z.ZodString>;
9
+ detects: z.ZodOptional<z.ZodString>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ verb?: string | undefined;
12
+ detects?: string | undefined;
13
+ produces?: string | undefined;
14
+ platform?: "all" | "web" | "ios" | "android" | undefined;
15
+ area?: string | undefined;
16
+ query?: string | undefined;
17
+ }, {
18
+ verb?: string | undefined;
19
+ detects?: string | undefined;
20
+ produces?: string | undefined;
21
+ platform?: "all" | "web" | "ios" | "android" | undefined;
22
+ area?: string | undefined;
23
+ query?: string | undefined;
24
+ }>;
25
+ export type FindSkillsInput = z.infer<typeof findSkillsSchema>;
26
+ export declare function findSkillsHandler(graph: ThinkingGraph, input: FindSkillsInput): Promise<{
27
+ content: {
28
+ type: "text";
29
+ text: string;
30
+ }[];
31
+ }>;
@@ -0,0 +1,84 @@
1
+ import { z } from 'zod';
2
+ export const findSkillsSchema = z.object({
3
+ query: z.string().optional().describe('Free-text search across skill names, areas, and verbs'),
4
+ area: z.string().optional().describe('Filter by area (e.g., accessibility, security, monetization, copy, architecture)'),
5
+ verb: z.string().optional().describe('Filter by action verb (e.g., audit, create, refactor, research)'),
6
+ platform: z.enum(['ios', 'android', 'web', 'all']).optional().describe('Filter by platform'),
7
+ produces: z.string().optional().describe('Filter by node type the skill produces (e.g., detection, tech_debt, code_fact, decision)'),
8
+ detects: z.string().optional().describe('Filter by what the skill detects (e.g., missing, needs-work)'),
9
+ });
10
+ function matchesQuery(skill, query) {
11
+ const q = query.toLowerCase();
12
+ return (skill.skillName.toLowerCase().includes(q) ||
13
+ skill.pluginName.toLowerCase().includes(q) ||
14
+ (skill.verb?.toLowerCase().includes(q) ?? false) ||
15
+ skill.areas.some(a => a.toLowerCase().includes(q)) ||
16
+ skill.detects.some(d => d.toLowerCase().includes(q)) ||
17
+ skill.produces.some(p => p.toLowerCase().includes(q)));
18
+ }
19
+ export async function findSkillsHandler(graph, input) {
20
+ // Build filter for structured fields
21
+ const filter = {};
22
+ if (input.verb)
23
+ filter.verb = input.verb;
24
+ if (input.platform)
25
+ filter.platform = input.platform;
26
+ // Query the registry
27
+ let results = await graph.storage.querySkills(filter);
28
+ // Apply free-text search
29
+ if (input.query) {
30
+ results = results.filter(s => matchesQuery(s, input.query));
31
+ }
32
+ // Apply area filter (areas is an array, need partial match)
33
+ if (input.area) {
34
+ const area = input.area.toLowerCase();
35
+ results = results.filter(s => s.areas.some(a => a.toLowerCase().includes(area)));
36
+ }
37
+ // Apply produces filter
38
+ if (input.produces) {
39
+ const produces = input.produces.toLowerCase();
40
+ results = results.filter(s => s.produces.some(p => p.toLowerCase().includes(produces)));
41
+ }
42
+ // Apply detects filter
43
+ if (input.detects) {
44
+ const detects = input.detects.toLowerCase();
45
+ results = results.filter(s => s.detects.some(d => d.toLowerCase().includes(detects)));
46
+ }
47
+ // Format results with invocation commands
48
+ const skills = results.map(s => ({
49
+ id: s.id,
50
+ plugin: s.pluginName,
51
+ skill: s.skillName,
52
+ invocation: s.invocation,
53
+ verb: s.verb,
54
+ areas: s.areas,
55
+ detects: s.detects,
56
+ produces: s.produces,
57
+ invokes: s.invokes,
58
+ platform: s.platform,
59
+ }));
60
+ // Build suggestions for the agent
61
+ const suggestions = skills.slice(0, 3).map(s => ({
62
+ tool: 'skill',
63
+ when: `Invoke ${s.plugin}:${s.skill} to ${s.verb ?? 'run'} ${s.areas.length > 0 ? `(covers: ${s.areas.join(', ')})` : ''}`,
64
+ example: { skill: `${s.plugin}:${s.skill}` },
65
+ }));
66
+ // If skills produce node types, suggest piping results back via learn
67
+ if (skills.some(s => s.produces.length > 0)) {
68
+ suggestions.push({
69
+ tool: 'learn',
70
+ when: 'After invoking a skill, store its key findings back in the graph',
71
+ example: { skill: 'learn', content: '<skill findings>', type: 'skill_result' },
72
+ });
73
+ }
74
+ return {
75
+ content: [{
76
+ type: 'text',
77
+ text: JSON.stringify({
78
+ matchCount: skills.length,
79
+ skills,
80
+ ...(suggestions.length > 0 && { suggestions }),
81
+ }),
82
+ }],
83
+ };
84
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import type { ThinkingGraph } from '../engine/graph.js';
3
+ export declare const recommendSkillsSchema: z.ZodObject<{
4
+ query: z.ZodOptional<z.ZodString>;
5
+ area: z.ZodOptional<z.ZodString>;
6
+ verb: z.ZodOptional<z.ZodString>;
7
+ platform: z.ZodOptional<z.ZodEnum<["ios", "android", "web", "all"]>>;
8
+ produces: z.ZodOptional<z.ZodString>;
9
+ detects: z.ZodOptional<z.ZodString>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ verb?: string | undefined;
12
+ detects?: string | undefined;
13
+ produces?: string | undefined;
14
+ platform?: "all" | "web" | "ios" | "android" | undefined;
15
+ area?: string | undefined;
16
+ query?: string | undefined;
17
+ }, {
18
+ verb?: string | undefined;
19
+ detects?: string | undefined;
20
+ produces?: string | undefined;
21
+ platform?: "all" | "web" | "ios" | "android" | undefined;
22
+ area?: string | undefined;
23
+ query?: string | undefined;
24
+ }>;
25
+ export type RecommendSkillsInput = z.infer<typeof recommendSkillsSchema>;
26
+ export declare function recommendSkillsHandler(graph: ThinkingGraph, input: RecommendSkillsInput): Promise<{
27
+ content: {
28
+ type: "text";
29
+ text: string;
30
+ }[];
31
+ }>;
@@ -0,0 +1,93 @@
1
+ import { z } from 'zod';
2
+ export const recommendSkillsSchema = z.object({
3
+ query: z.string().optional().describe('Free-text search across skill names, areas, and verbs'),
4
+ area: z.string().optional().describe('Filter by area (e.g., accessibility, security, monetization, copy, architecture)'),
5
+ verb: z.string().optional().describe('Filter by action verb (e.g., audit, create, refactor, research)'),
6
+ platform: z.enum(['ios', 'android', 'web', 'all']).optional().describe('Filter by platform'),
7
+ produces: z.string().optional().describe('Filter by node type the skill produces (e.g., detection, tech_debt, code_fact, decision)'),
8
+ detects: z.string().optional().describe('Filter by what the skill detects (e.g., missing, needs-work)'),
9
+ });
10
+ function matchesQuery(skill, query) {
11
+ const q = query.toLowerCase();
12
+ return (skill.skillName.toLowerCase().includes(q) ||
13
+ skill.pluginName.toLowerCase().includes(q) ||
14
+ (skill.verb?.toLowerCase().includes(q) ?? false) ||
15
+ skill.areas.some(a => a.toLowerCase().includes(q)) ||
16
+ skill.detects.some(d => d.toLowerCase().includes(q)) ||
17
+ skill.produces.some(p => p.toLowerCase().includes(q)));
18
+ }
19
+ export async function recommendSkillsHandler(graph, input) {
20
+ // Build filter for structured fields
21
+ const filter = {};
22
+ if (input.verb)
23
+ filter.verb = input.verb;
24
+ if (input.platform)
25
+ filter.platform = input.platform;
26
+ // Query the registry
27
+ let results = await graph.storage.querySkills(filter);
28
+ // Apply free-text search
29
+ if (input.query) {
30
+ results = results.filter(s => matchesQuery(s, input.query));
31
+ }
32
+ // Apply area filter (areas is an array, need partial match)
33
+ if (input.area) {
34
+ const area = input.area.toLowerCase();
35
+ results = results.filter(s => s.areas.some(a => a.toLowerCase().includes(area)));
36
+ }
37
+ // Apply produces filter
38
+ if (input.produces) {
39
+ const produces = input.produces.toLowerCase();
40
+ results = results.filter(s => s.produces.some(p => p.toLowerCase().includes(produces)));
41
+ }
42
+ // Apply detects filter
43
+ if (input.detects) {
44
+ const detects = input.detects.toLowerCase();
45
+ results = results.filter(s => s.detects.some(d => d.toLowerCase().includes(detects)));
46
+ }
47
+ // Format results with invocation commands
48
+ const skills = results.map(s => ({
49
+ id: s.id,
50
+ plugin: s.pluginName,
51
+ skill: s.skillName,
52
+ invocation: s.invocation,
53
+ verb: s.verb,
54
+ areas: s.areas,
55
+ detects: s.detects,
56
+ produces: s.produces,
57
+ invokes: s.invokes,
58
+ platform: s.platform,
59
+ }));
60
+ // Build suggestions for the agent
61
+ const suggestions = skills.slice(0, 3).map(s => ({
62
+ tool: 'skill',
63
+ when: `Invoke ${s.plugin}:${s.skill} to ${s.verb ?? 'run'} ${s.areas.length > 0 ? `(covers: ${s.areas.join(', ')})` : ''}`,
64
+ example: { skill: `${s.plugin}:${s.skill}` },
65
+ }));
66
+ // If skills produce node types, suggest piping results back via learn
67
+ if (skills.some(s => s.produces.length > 0)) {
68
+ suggestions.push({
69
+ tool: 'learn',
70
+ when: 'After invoking a skill, store its key findings back in the graph',
71
+ example: { skill: 'learn', content: '<skill findings>', type: 'skill_result' },
72
+ });
73
+ }
74
+ // If no local matches, suggest external skill discovery
75
+ if (skills.length === 0) {
76
+ const searchQuery = input.query ?? input.area ?? input.verb ?? '';
77
+ suggestions.push({
78
+ tool: 'skill',
79
+ when: 'No installed skills match — search the marketplace for skills to install',
80
+ example: { skill: 'find-skills', args: searchQuery },
81
+ });
82
+ }
83
+ return {
84
+ content: [{
85
+ type: 'text',
86
+ text: JSON.stringify({
87
+ matchCount: skills.length,
88
+ skills,
89
+ ...(suggestions.length > 0 && { suggestions }),
90
+ }),
91
+ }],
92
+ };
93
+ }
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { NODE_TYPES, EDGE_TYPES, GLOBAL_NODE_TYPES } from '../engine/types.js';
3
3
  const coerceBool = z.preprocess((v) => (v === 'true' ? true : v === 'false' ? false : v), z.boolean());
4
- function buildSuggestions(type, thoughtNumber, relatedCount, stats) {
4
+ function buildSuggestions(type, thoughtNumber, relatedCount, stats, matchedSkills) {
5
5
  const suggestions = [];
6
6
  // First thought in session — suggest recall to check prior knowledge
7
7
  if (thoughtNumber === 1 && stats.totalNodes > 1) {
@@ -80,6 +80,10 @@ function buildSuggestions(type, thoughtNumber, relatedCount, stats) {
80
80
  example: { skill: 'premium-core:reasoning' },
81
81
  });
82
82
  }
83
+ // Append matched skills from the registry
84
+ if (matchedSkills && matchedSkills.length > 0) {
85
+ suggestions.push(...matchedSkills);
86
+ }
83
87
  return suggestions;
84
88
  }
85
89
  export const thinkSchema = z.object({
@@ -134,7 +138,24 @@ export async function thinkHandler(graph, input) {
134
138
  }
135
139
  }
136
140
  const stats = await graph.storage.getStats();
137
- const suggestions = buildSuggestions(input.type ?? 'thought', input.thoughtNumber, relatedCount, stats);
141
+ // Query skill registry for relevant skills when detection/tech_debt nodes are created
142
+ let matchedSkills;
143
+ const nodeType = input.type ?? 'thought';
144
+ if (nodeType === 'detection' || nodeType === 'tech_debt' || nodeType === 'code_fact') {
145
+ const allSkills = await graph.storage.querySkills({});
146
+ const content = input.thought.toLowerCase();
147
+ // Match skills whose areas overlap with the thought content
148
+ const matches = allSkills.filter(s => s.areas.some(area => content.includes(area.toLowerCase())) ||
149
+ (s.verb && content.includes(s.verb.toLowerCase())));
150
+ if (matches.length > 0) {
151
+ matchedSkills = matches.slice(0, 2).map(s => ({
152
+ tool: 'skill',
153
+ when: `${s.verb ?? 'Run'} with ${s.pluginName}:${s.skillName} (covers: ${s.areas.join(', ')})`,
154
+ example: { skill: `${s.pluginName}:${s.skillName}` },
155
+ }));
156
+ }
157
+ }
158
+ const suggestions = buildSuggestions(nodeType, input.thoughtNumber, relatedCount, stats, matchedSkills);
138
159
  return {
139
160
  content: [{
140
161
  type: 'text',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feelingmindful/thinking-graph",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Persistent graph-based MCP thinking server for the feeling-mindful plugin marketplace",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",