@feelingmindful/thinking-graph 1.5.0 → 1.7.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.
@@ -241,6 +241,50 @@ export class ThinkingGraph {
241
241
  if (stats.principleViolations > 0) {
242
242
  lines.push(`## Principle Violations: ${stats.principleViolations}`);
243
243
  }
244
+ // Session utilization — show which tool capabilities were exercised
245
+ const nodeTypes = stats.nodesByType;
246
+ const edgeTypes = stats.edgesByType;
247
+ const totalEdges = stats.totalEdges;
248
+ // Count user-created nodes (exclude pre-seeded principles and skill_result)
249
+ const thinkUsed = (nodeTypes.thought ?? 0) + (nodeTypes.decision ?? 0) +
250
+ (nodeTypes.assumption ?? 0) + (nodeTypes.detection ?? 0) +
251
+ (nodeTypes.tech_debt ?? 0) + (nodeTypes.code_fact ?? 0) > 0;
252
+ const relateUsed = totalEdges > 0;
253
+ const learnUsed = (nodeTypes.insight ?? 0) + (nodeTypes.pattern ?? 0) > 0;
254
+ const researchUsed = (nodeTypes.research ?? 0) > 0;
255
+ const tools = [
256
+ { name: 'think', used: thinkUsed, purpose: 'record reasoning steps' },
257
+ { name: 'relate', used: relateUsed, purpose: 'connect nodes with typed edges' },
258
+ { name: 'recall', used: null, purpose: 'query prior knowledge (not trackable)' },
259
+ { name: 'learn', used: learnUsed, purpose: 'persist insights cross-project' },
260
+ { name: 'research', used: researchUsed, purpose: 'web research via Perplexity/Firecrawl' },
261
+ ];
262
+ lines.push('', '## Tool Utilization');
263
+ for (const t of tools) {
264
+ if (t.used === null) {
265
+ lines.push(`- ${t.name}: ~ (${t.purpose})`);
266
+ }
267
+ else {
268
+ lines.push(`- ${t.name}: ${t.used ? '✓' : '✗ unused'} (${t.purpose})`);
269
+ }
270
+ }
271
+ // Flag underutilization
272
+ const warnings = [];
273
+ if (thinkUsed && !relateUsed) {
274
+ warnings.push('Thoughts are unconnected — use `relate` to link dependencies, contradictions, and supporting evidence');
275
+ }
276
+ if (thinkUsed && !learnUsed) {
277
+ warnings.push('No insights persisted — use `learn` to save conclusions that should carry to future sessions');
278
+ }
279
+ if ((nodeTypes.assumption ?? 0) > 0 && !researchUsed) {
280
+ warnings.push('Assumptions recorded but not verified — use `research` to fact-check before building on them');
281
+ }
282
+ if (warnings.length > 0) {
283
+ lines.push('', '## Underutilization Warnings');
284
+ for (const w of warnings) {
285
+ lines.push(`- ⚠ ${w}`);
286
+ }
287
+ }
244
288
  return lines.join('\n');
245
289
  }
246
290
  // ─── Seeding ──────────────────────────────────────────
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,84 @@
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
+ 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
+ }
@@ -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) {
@@ -72,6 +72,18 @@ function buildSuggestions(type, thoughtNumber, relatedCount, stats) {
72
72
  example: { sourceId: '<this nodeId>', targetId: '?<search term>', type: 'supports' },
73
73
  });
74
74
  }
75
+ // If the agent is think-only (no relates, early in session), nudge toward the reasoning skill
76
+ if (relatedCount === 0 && thoughtNumber <= 2 && stats.totalEdges === 0 && stats.totalNodes > 1) {
77
+ suggestions.push({
78
+ tool: 'skill',
79
+ when: 'Load the reasoning skill for structured thinking: recall → think → relate → research → decide → learn',
80
+ example: { skill: 'premium-core:reasoning' },
81
+ });
82
+ }
83
+ // Append matched skills from the registry
84
+ if (matchedSkills && matchedSkills.length > 0) {
85
+ suggestions.push(...matchedSkills);
86
+ }
75
87
  return suggestions;
76
88
  }
77
89
  export const thinkSchema = z.object({
@@ -126,7 +138,24 @@ export async function thinkHandler(graph, input) {
126
138
  }
127
139
  }
128
140
  const stats = await graph.storage.getStats();
129
- 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);
130
159
  return {
131
160
  content: [{
132
161
  type: 'text',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feelingmindful/thinking-graph",
3
- "version": "1.5.0",
3
+ "version": "1.7.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",
@@ -15,7 +15,7 @@
15
15
  "repository": {
16
16
  "type": "git",
17
17
  "url": "https://github.com/feeling-mindful/feeling-mindful-plugins.git",
18
- "directory": "plugins/premium-core/mcp-servers/thinking-graph"
18
+ "directory": "packages/thinking-graph"
19
19
  },
20
20
  "publishConfig": {
21
21
  "access": "public",