@feelingmindful/thinking-graph 1.2.0 → 1.4.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.
@@ -1,20 +1,15 @@
1
1
  import { z } from 'zod';
2
2
  import type { ThinkingGraph } from '../engine/graph.js';
3
- /**
4
- * Backward compatibility shim for the original `sequentialthinking` tool.
5
- * Maps the old API (thought, thoughtNumber, totalThoughts, nextThoughtNeeded)
6
- * to the new `think` tool, preserving the original response format.
7
- */
8
3
  export declare const sequentialSchema: z.ZodObject<{
9
4
  thought: z.ZodString;
10
5
  thoughtNumber: z.ZodNumber;
11
6
  totalThoughts: z.ZodNumber;
12
- nextThoughtNeeded: z.ZodBoolean;
13
- isRevision: z.ZodOptional<z.ZodBoolean>;
7
+ nextThoughtNeeded: z.ZodEffects<z.ZodBoolean, boolean, unknown>;
8
+ isRevision: z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
14
9
  revisesThought: z.ZodOptional<z.ZodNumber>;
15
10
  branchFromThought: z.ZodOptional<z.ZodNumber>;
16
11
  branchId: z.ZodOptional<z.ZodString>;
17
- needsMoreThoughts: z.ZodOptional<z.ZodBoolean>;
12
+ needsMoreThoughts: z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
18
13
  }, "strip", z.ZodTypeAny, {
19
14
  thought: string;
20
15
  thoughtNumber: number;
@@ -29,12 +24,12 @@ export declare const sequentialSchema: z.ZodObject<{
29
24
  thought: string;
30
25
  thoughtNumber: number;
31
26
  totalThoughts: number;
32
- nextThoughtNeeded: boolean;
33
27
  branchId?: string | undefined;
34
- isRevision?: boolean | undefined;
28
+ isRevision?: unknown;
35
29
  revisesThought?: number | undefined;
30
+ nextThoughtNeeded?: unknown;
36
31
  branchFromThought?: number | undefined;
37
- needsMoreThoughts?: boolean | undefined;
32
+ needsMoreThoughts?: unknown;
38
33
  }>;
39
34
  export type SequentialInput = z.infer<typeof sequentialSchema>;
40
35
  export declare function compatHandler(graph: ThinkingGraph, input: SequentialInput): Promise<{
@@ -4,16 +4,17 @@ import { z } from 'zod';
4
4
  * Maps the old API (thought, thoughtNumber, totalThoughts, nextThoughtNeeded)
5
5
  * to the new `think` tool, preserving the original response format.
6
6
  */
7
+ const coerceBool = z.preprocess((v) => (v === 'true' ? true : v === 'false' ? false : v), z.boolean());
7
8
  export const sequentialSchema = z.object({
8
9
  thought: z.string().describe('The current thinking step'),
9
- thoughtNumber: z.number().int().min(1).describe('Current thought number'),
10
- totalThoughts: z.number().int().min(1).describe('Estimated total thoughts'),
11
- nextThoughtNeeded: z.boolean().describe('Whether another step is needed'),
12
- isRevision: z.boolean().optional(),
13
- revisesThought: z.number().int().min(1).optional(),
14
- branchFromThought: z.number().int().min(1).optional(),
10
+ thoughtNumber: z.coerce.number().int().min(1).describe('Current thought number'),
11
+ totalThoughts: z.coerce.number().int().min(1).describe('Estimated total thoughts'),
12
+ nextThoughtNeeded: coerceBool.describe('Whether another step is needed'),
13
+ isRevision: coerceBool.optional(),
14
+ revisesThought: z.coerce.number().int().min(1).optional(),
15
+ branchFromThought: z.coerce.number().int().min(1).optional(),
15
16
  branchId: z.string().optional(),
16
- needsMoreThoughts: z.boolean().optional(),
17
+ needsMoreThoughts: coerceBool.optional(),
17
18
  });
18
19
  export async function compatHandler(graph, input) {
19
20
  const session = await graph.getCurrentSession();
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { relateSchema, relateHandler } from './tools/relate.js';
9
9
  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
- import { sequentialSchema, compatHandler } from './compat/sequential.js';
12
+ // Legacy compat shim removed use `think` tool directly
13
13
  // ─── Storage setup ───────────────────────────────────────
14
14
  const memoryOnly = process.env.THINKING_GRAPH_MEMORY_ONLY === 'true';
15
15
  const storage = memoryOnly
@@ -30,8 +30,6 @@ server.tool('relate', 'Create a typed, directional relationship between two node
30
30
  server.tool('recall', 'Query the thinking graph — search by text, filter by type, traverse relationships, or search across projects.', recallSchema.shape, async (input) => recallHandler(graph, input));
31
31
  server.tool('learn', 'Store durable knowledge — code facts, tech debt, insights, principles. Deduplicates similar content.', learnSchema.shape, async (input) => learnHandler(graph, input));
32
32
  server.tool('export', 'Export the thinking graph as JSON or a human-readable markdown summary.', exportSchema.shape, async (input) => exportHandler(graph, input));
33
- // Backward compat: register the original tool name
34
- server.tool('sequentialthinking', 'Record a sequential thinking step (backward compatible with @modelcontextprotocol/server-sequential-thinking).', sequentialSchema.shape, async (input) => compatHandler(graph, input));
35
33
  // ─── Startup ─────────────────────────────────────────────
36
34
  async function main() {
37
35
  await storage.initialize();
@@ -9,6 +9,10 @@ export declare class SQLiteAdapter implements StorageAdapter {
9
9
  constructor(opts: SQLiteAdapterOpts);
10
10
  initialize(): Promise<void>;
11
11
  close(): Promise<void>;
12
+ private persist;
13
+ private runSql;
14
+ private getSingle;
15
+ private getAll;
12
16
  insertNode(node: Node): Promise<void>;
13
17
  getNode(id: string): Promise<Node | null>;
14
18
  queryNodes(query: NodeQuery): Promise<PaginatedResult<Node>>;
@@ -1,10 +1,17 @@
1
- import Database from 'better-sqlite3';
2
- import { readFileSync } from 'fs';
1
+ import initSqlJs from 'sql.js';
2
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
4
  import { mkdirSync } from 'fs';
5
5
  import { fileURLToPath } from 'url';
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
7
  const MIGRATION_PATH = join(__dirname, '../../migrations/001-initial.sql');
8
+ // Cache the WASM module so multiple instances don't re-initialize Emscripten
9
+ let sqlJsPromise = null;
10
+ function getSqlJs() {
11
+ if (!sqlJsPromise)
12
+ sqlJsPromise = initSqlJs();
13
+ return sqlJsPromise;
14
+ }
8
15
  export class SQLiteAdapter {
9
16
  db;
10
17
  dbPath;
@@ -12,30 +19,85 @@ export class SQLiteAdapter {
12
19
  this.dbPath = opts.dbPath;
13
20
  }
14
21
  async initialize() {
15
- // Ensure parent directory exists
16
22
  const dir = dirname(this.dbPath);
17
23
  mkdirSync(dir, { recursive: true });
18
- this.db = new Database(this.dbPath);
19
- this.db.pragma('journal_mode = WAL');
20
- this.db.pragma('foreign_keys = ON');
21
- // Run migration
24
+ const SQL = await getSqlJs();
25
+ if (existsSync(this.dbPath)) {
26
+ const fileBuffer = readFileSync(this.dbPath);
27
+ this.db = new SQL.Database(new Uint8Array(fileBuffer));
28
+ }
29
+ else {
30
+ this.db = new SQL.Database();
31
+ }
32
+ this.db.run('PRAGMA foreign_keys = ON');
33
+ // Run migration (multi-statement)
22
34
  const sql = readFileSync(MIGRATION_PATH, 'utf-8');
23
35
  this.db.exec(sql);
36
+ this.persist();
24
37
  }
25
38
  async close() {
39
+ this.persist();
26
40
  this.db.close();
27
41
  }
42
+ persist() {
43
+ const data = this.db.export();
44
+ const buffer = Buffer.from(data);
45
+ writeFileSync(this.dbPath, buffer);
46
+ }
47
+ // ─── Helpers ──────────────────────────────────────────
48
+ runSql(sql, params = []) {
49
+ this.db.run(sql, params);
50
+ this.persist();
51
+ }
52
+ getSingle(sql, params = []) {
53
+ const stmt = this.db.prepare(sql);
54
+ stmt.bind(params);
55
+ if (stmt.step()) {
56
+ const columns = stmt.getColumnNames();
57
+ const values = stmt.get();
58
+ stmt.free();
59
+ const row = {};
60
+ for (let i = 0; i < columns.length; i++) {
61
+ row[columns[i]] = values[i];
62
+ }
63
+ return row;
64
+ }
65
+ stmt.free();
66
+ return undefined;
67
+ }
68
+ getAll(sql, params = []) {
69
+ const stmt = this.db.prepare(sql);
70
+ stmt.bind(params);
71
+ const rows = [];
72
+ while (stmt.step()) {
73
+ const columns = stmt.getColumnNames();
74
+ const values = stmt.get();
75
+ const row = {};
76
+ for (let i = 0; i < columns.length; i++) {
77
+ row[columns[i]] = values[i];
78
+ }
79
+ rows.push(row);
80
+ }
81
+ stmt.free();
82
+ return rows;
83
+ }
28
84
  // ─── Nodes ─────────────────────────────────────────────
29
85
  async insertNode(node) {
30
- this.db.prepare(`
86
+ this.runSql(`
31
87
  INSERT INTO nodes (id, type, content, session_id, project_id, metadata,
32
88
  created_at, updated_at, thought_number, total_thoughts, branch_id,
33
89
  is_revision, revises_thought)
34
90
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
35
- `).run(node.id, node.type, node.content, node.sessionId, node.projectId ?? null, JSON.stringify(node.metadata), node.createdAt, node.updatedAt, node.thoughtNumber ?? null, node.totalThoughts ?? null, node.branchId ?? null, node.isRevision ? 1 : 0, node.revisesThought ?? null);
91
+ `, [
92
+ node.id, node.type, node.content, node.sessionId, node.projectId ?? null,
93
+ JSON.stringify(node.metadata), node.createdAt, node.updatedAt,
94
+ node.thoughtNumber ?? null, node.totalThoughts ?? null,
95
+ node.branchId ?? null, node.isRevision ? 1 : 0,
96
+ node.revisesThought ?? null,
97
+ ]);
36
98
  }
37
99
  async getNode(id) {
38
- const row = this.db.prepare('SELECT * FROM nodes WHERE id = ?').get(id);
100
+ const row = this.getSingle('SELECT * FROM nodes WHERE id = ?', [id]);
39
101
  return row ? this.rowToNode(row) : null;
40
102
  }
41
103
  async queryNodes(query) {
@@ -59,16 +121,15 @@ export class SQLiteAdapter {
59
121
  params.push(query.since);
60
122
  }
61
123
  if (query.query) {
62
- // Use FTS for text search
63
- conditions.push('rowid IN (SELECT rowid FROM nodes_fts WHERE nodes_fts MATCH ?)');
64
- params.push(query.query);
124
+ conditions.push('content LIKE ?');
125
+ params.push(`%${query.query}%`);
65
126
  }
66
127
  const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
67
- const countRow = this.db.prepare(`SELECT COUNT(*) as cnt FROM nodes ${where}`).get(...params);
128
+ const countRow = this.getSingle(`SELECT COUNT(*) as cnt FROM nodes ${where}`, params);
68
129
  const totalCount = countRow.cnt;
69
130
  const limit = query.limit ?? 20;
70
131
  const offset = query.offset ?? 0;
71
- const rows = this.db.prepare(`SELECT * FROM nodes ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
132
+ const rows = this.getAll(`SELECT * FROM nodes ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, limit, offset]);
72
133
  return {
73
134
  items: rows.map(r => this.rowToNode(r)),
74
135
  totalCount,
@@ -76,22 +137,24 @@ export class SQLiteAdapter {
76
137
  };
77
138
  }
78
139
  async searchContent(text, limit = 20) {
79
- const rows = this.db.prepare(`
80
- SELECT n.* FROM nodes_fts fts
81
- JOIN nodes n ON n.rowid = fts.rowid
82
- WHERE nodes_fts MATCH ?
83
- ORDER BY rank
140
+ const rows = this.getAll(`
141
+ SELECT * FROM nodes
142
+ WHERE content LIKE ?
143
+ ORDER BY created_at DESC
84
144
  LIMIT ?
85
- `).all(text, limit);
145
+ `, [`%${text}%`, limit]);
86
146
  return rows.map(r => this.rowToNode(r));
87
147
  }
88
148
  // ─── Edges ─────────────────────────────────────────────
89
149
  async insertEdge(edge) {
90
150
  try {
91
- this.db.prepare(`
151
+ this.runSql(`
92
152
  INSERT INTO edges (id, source_id, target_id, type, weight, reasoning, created_at)
93
153
  VALUES (?, ?, ?, ?, ?, ?, ?)
94
- `).run(edge.id, edge.sourceId, edge.targetId, edge.type, edge.weight, edge.reasoning ?? null, edge.createdAt);
154
+ `, [
155
+ edge.id, edge.sourceId, edge.targetId, edge.type,
156
+ edge.weight, edge.reasoning ?? null, edge.createdAt,
157
+ ]);
95
158
  return true;
96
159
  }
97
160
  catch (err) {
@@ -106,7 +169,7 @@ export class SQLiteAdapter {
106
169
  ? 'SELECT * FROM edges WHERE source_id = ? AND type = ?'
107
170
  : 'SELECT * FROM edges WHERE source_id = ?';
108
171
  const params = type ? [nodeId, type] : [nodeId];
109
- const rows = this.db.prepare(sql).all(...params);
172
+ const rows = this.getAll(sql, params);
110
173
  return rows.map(r => this.rowToEdge(r));
111
174
  }
112
175
  async getEdgesTo(nodeId, type) {
@@ -114,11 +177,11 @@ export class SQLiteAdapter {
114
177
  ? 'SELECT * FROM edges WHERE target_id = ? AND type = ?'
115
178
  : 'SELECT * FROM edges WHERE target_id = ?';
116
179
  const params = type ? [nodeId, type] : [nodeId];
117
- const rows = this.db.prepare(sql).all(...params);
180
+ const rows = this.getAll(sql, params);
118
181
  return rows.map(r => this.rowToEdge(r));
119
182
  }
120
183
  async traverseEdges(startId, type, maxDepth) {
121
- const rows = this.db.prepare(`
184
+ const rows = this.getAll(`
122
185
  WITH RECURSIVE chain(id, depth) AS (
123
186
  SELECT target_id, 1
124
187
  FROM edges
@@ -132,18 +195,21 @@ export class SQLiteAdapter {
132
195
  SELECT DISTINCT n.* FROM nodes n
133
196
  JOIN chain c ON n.id = c.id
134
197
  ORDER BY c.depth
135
- `).all(startId, type, type, maxDepth);
198
+ `, [startId, type, type, maxDepth]);
136
199
  return rows.map(r => this.rowToNode(r));
137
200
  }
138
201
  // ─── Sessions ──────────────────────────────────────────
139
202
  async insertSession(session) {
140
- this.db.prepare(`
203
+ this.runSql(`
141
204
  INSERT INTO sessions (id, project_id, project_path, description, started_at, last_active)
142
205
  VALUES (?, ?, ?, ?, ?, ?)
143
- `).run(session.id, session.projectId ?? null, session.projectPath ?? null, session.description ?? null, session.startedAt, session.lastActiveAt);
206
+ `, [
207
+ session.id, session.projectId ?? null, session.projectPath ?? null,
208
+ session.description ?? null, session.startedAt, session.lastActiveAt,
209
+ ]);
144
210
  }
145
211
  async getSession(id) {
146
- const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
212
+ const row = this.getSingle('SELECT * FROM sessions WHERE id = ?', [id]);
147
213
  return row ? this.rowToSession(row) : null;
148
214
  }
149
215
  async updateSession(id, fields) {
@@ -159,16 +225,20 @@ export class SQLiteAdapter {
159
225
  }
160
226
  if (sets.length > 0) {
161
227
  params.push(id);
162
- this.db.prepare(`UPDATE sessions SET ${sets.join(', ')} WHERE id = ?`).run(...params);
228
+ this.runSql(`UPDATE sessions SET ${sets.join(', ')} WHERE id = ?`, params);
163
229
  }
164
230
  }
165
231
  // ─── Skill Registry ───────────────────────────────────
166
232
  async insertSkill(entry) {
167
- this.db.prepare(`
233
+ this.runSql(`
168
234
  INSERT OR IGNORE INTO skill_registry
169
235
  (id, plugin_name, skill_name, verb, invocation, areas, detects, produces, invokes, platform)
170
236
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
171
- `).run(entry.id, entry.pluginName, entry.skillName, entry.verb ?? null, entry.invocation, JSON.stringify(entry.areas), JSON.stringify(entry.detects), JSON.stringify(entry.produces), JSON.stringify(entry.invokes), entry.platform ?? null);
237
+ `, [
238
+ entry.id, entry.pluginName, entry.skillName, entry.verb ?? null,
239
+ entry.invocation, JSON.stringify(entry.areas), JSON.stringify(entry.detects),
240
+ JSON.stringify(entry.produces), JSON.stringify(entry.invokes), entry.platform ?? null,
241
+ ]);
172
242
  }
173
243
  async querySkills(filter) {
174
244
  const conditions = [];
@@ -194,7 +264,7 @@ export class SQLiteAdapter {
194
264
  params.push(filter.platform);
195
265
  }
196
266
  const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
197
- const rows = this.db.prepare(`SELECT * FROM skill_registry ${where}`).all(...params);
267
+ const rows = this.getAll(`SELECT * FROM skill_registry ${where}`, params);
198
268
  return rows.map(r => ({
199
269
  id: r.id,
200
270
  pluginName: r.plugin_name,
@@ -226,18 +296,18 @@ export class SQLiteAdapter {
226
296
  params.push(...types);
227
297
  }
228
298
  const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
229
- const nodeRows = this.db.prepare(`SELECT * FROM nodes ${where}`).all(...params);
299
+ const nodeRows = this.getAll(`SELECT * FROM nodes ${where}`, params);
230
300
  const nodes = nodeRows.map(r => this.rowToNode(r));
231
301
  let edges = [];
232
302
  if (opts?.includeEdges !== false) {
233
303
  const nodeIds = nodes.map(n => n.id);
234
304
  if (nodeIds.length > 0) {
235
305
  const placeholders = nodeIds.map(() => '?').join(',');
236
- const edgeRows = this.db.prepare(`SELECT * FROM edges WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`).all(...nodeIds, ...nodeIds);
306
+ const edgeRows = this.getAll(`SELECT * FROM edges WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`, [...nodeIds, ...nodeIds]);
237
307
  edges = edgeRows.map(r => this.rowToEdge(r));
238
308
  }
239
309
  }
240
- const sessionRows = this.db.prepare('SELECT * FROM sessions').all();
310
+ const sessionRows = this.getAll('SELECT * FROM sessions');
241
311
  return {
242
312
  exportedAt: new Date().toISOString(),
243
313
  nodeCount: nodes.length,
@@ -250,15 +320,15 @@ export class SQLiteAdapter {
250
320
  // ─── Stats ─────────────────────────────────────────────
251
321
  async getStats() {
252
322
  const nodesByType = {};
253
- const ntRows = this.db.prepare('SELECT type, COUNT(*) as cnt FROM nodes GROUP BY type').all();
323
+ const ntRows = this.getAll('SELECT type, COUNT(*) as cnt FROM nodes GROUP BY type');
254
324
  for (const r of ntRows)
255
325
  nodesByType[r.type] = r.cnt;
256
326
  const edgesByType = {};
257
- const etRows = this.db.prepare('SELECT type, COUNT(*) as cnt FROM edges GROUP BY type').all();
327
+ const etRows = this.getAll('SELECT type, COUNT(*) as cnt FROM edges GROUP BY type');
258
328
  for (const r of etRows)
259
329
  edgesByType[r.type] = r.cnt;
260
- const totalNodes = this.db.prepare('SELECT COUNT(*) as cnt FROM nodes').get().cnt;
261
- const totalEdges = this.db.prepare('SELECT COUNT(*) as cnt FROM edges').get().cnt;
330
+ const totalNodes = this.getSingle('SELECT COUNT(*) as cnt FROM nodes').cnt;
331
+ const totalEdges = this.getSingle('SELECT COUNT(*) as cnt FROM edges').cnt;
262
332
  return {
263
333
  totalNodes,
264
334
  totalEdges,
@@ -5,7 +5,7 @@ export declare const exportSchema: z.ZodObject<{
5
5
  sessionId: z.ZodOptional<z.ZodString>;
6
6
  projectId: z.ZodOptional<z.ZodString>;
7
7
  type: z.ZodOptional<z.ZodUnion<[z.ZodEnum<["thought", "decision", "insight", "code_fact", "assumption", "detection", "tech_debt", "principle", "pattern", "skill_result", "research"]>, z.ZodArray<z.ZodEnum<["thought", "decision", "insight", "code_fact", "assumption", "detection", "tech_debt", "principle", "pattern", "skill_result", "research"]>, "many">]>>;
8
- includeEdges: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
8
+ includeEdges: z.ZodDefault<z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>>;
9
9
  outputPath: z.ZodOptional<z.ZodString>;
10
10
  }, "strip", z.ZodTypeAny, {
11
11
  format: "json" | "summary";
@@ -19,7 +19,7 @@ export declare const exportSchema: z.ZodObject<{
19
19
  type?: "thought" | "decision" | "insight" | "code_fact" | "assumption" | "detection" | "tech_debt" | "principle" | "pattern" | "skill_result" | "research" | ("thought" | "decision" | "insight" | "code_fact" | "assumption" | "detection" | "tech_debt" | "principle" | "pattern" | "skill_result" | "research")[] | undefined;
20
20
  sessionId?: string | undefined;
21
21
  projectId?: string | undefined;
22
- includeEdges?: boolean | undefined;
22
+ includeEdges?: unknown;
23
23
  outputPath?: string | undefined;
24
24
  }>;
25
25
  export type ExportInput = z.infer<typeof exportSchema>;
@@ -6,7 +6,7 @@ export const exportSchema = z.object({
6
6
  sessionId: z.string().optional(),
7
7
  projectId: z.string().optional(),
8
8
  type: z.union([z.enum(NODE_TYPES), z.array(z.enum(NODE_TYPES))]).optional(),
9
- includeEdges: z.boolean().optional().default(true),
9
+ includeEdges: z.preprocess((v) => (v === 'true' ? true : v === 'false' ? false : v), z.boolean()).optional().default(true),
10
10
  outputPath: z.string().optional().describe('Write to file'),
11
11
  });
12
12
  export async function exportHandler(graph, input) {
@@ -5,7 +5,7 @@ export const learnSchema = z.object({
5
5
  type: z.enum(NODE_TYPES).describe('Node type'),
6
6
  projectId: z.string().optional(),
7
7
  filePath: z.string().optional().describe('For code_facts'),
8
- lineRange: z.tuple([z.number(), z.number()]).optional().describe('Line range [start, end]'),
8
+ lineRange: z.tuple([z.coerce.number(), z.coerce.number()]).optional().describe('Line range [start, end]'),
9
9
  severity: z.enum(['critical', 'high', 'medium', 'low']).optional().describe('For tech_debt'),
10
10
  effort: z.string().optional().describe('Estimated fix effort'),
11
11
  impact: z.string().optional().describe('What breaks if unfixed'),
@@ -5,7 +5,7 @@ export declare const recallSchema: z.ZodObject<{
5
5
  type: z.ZodOptional<z.ZodUnion<[z.ZodEnum<["thought", "decision", "insight", "code_fact", "assumption", "detection", "tech_debt", "principle", "pattern", "skill_result", "research"]>, z.ZodArray<z.ZodEnum<["thought", "decision", "insight", "code_fact", "assumption", "detection", "tech_debt", "principle", "pattern", "skill_result", "research"]>, "many">]>>;
6
6
  sessionId: z.ZodOptional<z.ZodString>;
7
7
  projectId: z.ZodOptional<z.ZodString>;
8
- crossProject: z.ZodOptional<z.ZodBoolean>;
8
+ crossProject: z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
9
9
  relatedTo: z.ZodOptional<z.ZodString>;
10
10
  edgeType: z.ZodOptional<z.ZodUnion<[z.ZodEnum<["depends_on", "contradicts", "supports", "refines", "supersedes", "similar_to", "located_in", "violates", "addresses", "detected_by", "invoked_by"]>, z.ZodArray<z.ZodEnum<["depends_on", "contradicts", "supports", "refines", "supersedes", "similar_to", "located_in", "violates", "addresses", "detected_by", "invoked_by"]>, "many">]>>;
11
11
  direction: z.ZodOptional<z.ZodEnum<["outgoing", "incoming", "both"]>>;
@@ -34,7 +34,7 @@ export declare const recallSchema: z.ZodObject<{
34
34
  projectId?: string | undefined;
35
35
  metadata?: Record<string, unknown> | undefined;
36
36
  query?: string | undefined;
37
- crossProject?: boolean | undefined;
37
+ crossProject?: unknown;
38
38
  relatedTo?: string | undefined;
39
39
  edgeType?: "depends_on" | "contradicts" | "supports" | "refines" | "supersedes" | "similar_to" | "located_in" | "violates" | "addresses" | "detected_by" | "invoked_by" | ("depends_on" | "contradicts" | "supports" | "refines" | "supersedes" | "similar_to" | "located_in" | "violates" | "addresses" | "detected_by" | "invoked_by")[] | undefined;
40
40
  direction?: "outgoing" | "incoming" | "both" | undefined;
@@ -1,19 +1,20 @@
1
1
  import { z } from 'zod';
2
2
  import { NODE_TYPES, EDGE_TYPES } from '../engine/types.js';
3
+ const coerceBool = z.preprocess((v) => (v === 'true' ? true : v === 'false' ? false : v), z.boolean());
3
4
  export const recallSchema = z.object({
4
5
  query: z.string().optional().describe('Full-text search'),
5
6
  type: z.union([z.enum(NODE_TYPES), z.array(z.enum(NODE_TYPES))]).optional(),
6
7
  sessionId: z.string().optional(),
7
8
  projectId: z.string().optional(),
8
- crossProject: z.boolean().optional().describe('Search across all projects'),
9
+ crossProject: coerceBool.optional().describe('Search across all projects'),
9
10
  relatedTo: z.string().optional().describe('Find nodes connected to this ID'),
10
11
  edgeType: z.union([z.enum(EDGE_TYPES), z.array(z.enum(EDGE_TYPES))]).optional(),
11
12
  direction: z.enum(['outgoing', 'incoming', 'both']).optional(),
12
- depth: z.number().int().min(1).optional().describe('Traversal depth'),
13
+ depth: z.coerce.number().int().min(1).optional().describe('Traversal depth'),
13
14
  since: z.string().optional().describe('ISO timestamp'),
14
15
  metadata: z.record(z.unknown()).optional(),
15
- limit: z.number().int().min(1).max(100).optional(),
16
- offset: z.number().int().min(0).optional(),
16
+ limit: z.coerce.number().int().min(1).max(100).optional(),
17
+ offset: z.coerce.number().int().min(0).optional(),
17
18
  });
18
19
  export async function recallHandler(graph, input) {
19
20
  // If relatedTo is specified, use graph traversal
@@ -6,7 +6,7 @@ export declare const relateSchema: z.ZodObject<{
6
6
  type: z.ZodEnum<["depends_on", "contradicts", "supports", "refines", "supersedes", "similar_to", "located_in", "violates", "addresses", "detected_by", "invoked_by"]>;
7
7
  weight: z.ZodOptional<z.ZodNumber>;
8
8
  reasoning: z.ZodOptional<z.ZodString>;
9
- bidirectional: z.ZodOptional<z.ZodBoolean>;
9
+ bidirectional: z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
10
10
  }, "strip", z.ZodTypeAny, {
11
11
  type: "depends_on" | "contradicts" | "supports" | "refines" | "supersedes" | "similar_to" | "located_in" | "violates" | "addresses" | "detected_by" | "invoked_by";
12
12
  sourceId: string;
@@ -20,7 +20,7 @@ export declare const relateSchema: z.ZodObject<{
20
20
  targetId: string;
21
21
  weight?: number | undefined;
22
22
  reasoning?: string | undefined;
23
- bidirectional?: boolean | undefined;
23
+ bidirectional?: unknown;
24
24
  }>;
25
25
  export type RelateInput = z.infer<typeof relateSchema>;
26
26
  export declare function relateHandler(graph: ThinkingGraph, input: RelateInput): Promise<{
@@ -1,12 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  import { EDGE_TYPES } from '../engine/types.js';
3
+ const coerceBool = z.preprocess((v) => (v === 'true' ? true : v === 'false' ? false : v), z.boolean());
3
4
  export const relateSchema = z.object({
4
5
  sourceId: z.string().describe('Node ID (prefix with "?" for content search)'),
5
6
  targetId: z.string().describe('Node ID, content search, or principle name'),
6
7
  type: z.enum(EDGE_TYPES).describe('Relationship type'),
7
- weight: z.number().min(0).max(1).optional().describe('Confidence 0-1'),
8
+ weight: z.coerce.number().min(0).max(1).optional().describe('Confidence 0-1'),
8
9
  reasoning: z.string().optional().describe('Why this relationship exists'),
9
- bidirectional: z.boolean().optional().describe('Create reverse edge too'),
10
+ bidirectional: coerceBool.optional().describe('Create reverse edge too'),
10
11
  });
11
12
  async function resolveId(graph, id) {
12
13
  if (id.startsWith('?')) {
@@ -5,7 +5,7 @@ export declare const thinkSchema: z.ZodObject<{
5
5
  type: z.ZodDefault<z.ZodEnum<["thought", "decision", "insight", "code_fact", "assumption", "detection", "tech_debt", "principle", "pattern", "skill_result", "research"]>>;
6
6
  thoughtNumber: z.ZodNumber;
7
7
  totalThoughts: z.ZodNumber;
8
- nextThoughtNeeded: z.ZodBoolean;
8
+ nextThoughtNeeded: z.ZodEffects<z.ZodBoolean, boolean, unknown>;
9
9
  sessionId: z.ZodOptional<z.ZodString>;
10
10
  projectId: z.ZodOptional<z.ZodString>;
11
11
  relates: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -22,11 +22,11 @@ export declare const thinkSchema: z.ZodObject<{
22
22
  reasoning?: string | undefined;
23
23
  }>, "many">>;
24
24
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
25
- isRevision: z.ZodOptional<z.ZodBoolean>;
25
+ isRevision: z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
26
26
  revisesThought: z.ZodOptional<z.ZodNumber>;
27
27
  branchFromThought: z.ZodOptional<z.ZodNumber>;
28
28
  branchId: z.ZodOptional<z.ZodString>;
29
- needsMoreThoughts: z.ZodOptional<z.ZodBoolean>;
29
+ needsMoreThoughts: z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
30
30
  }, "strip", z.ZodTypeAny, {
31
31
  thought: string;
32
32
  type: "thought" | "decision" | "insight" | "code_fact" | "assumption" | "detection" | "tech_debt" | "principle" | "pattern" | "skill_result" | "research";
@@ -50,21 +50,21 @@ export declare const thinkSchema: z.ZodObject<{
50
50
  thought: string;
51
51
  thoughtNumber: number;
52
52
  totalThoughts: number;
53
- nextThoughtNeeded: boolean;
54
53
  type?: "thought" | "decision" | "insight" | "code_fact" | "assumption" | "detection" | "tech_debt" | "principle" | "pattern" | "skill_result" | "research" | undefined;
55
54
  sessionId?: string | undefined;
56
55
  projectId?: string | undefined;
57
56
  metadata?: Record<string, unknown> | undefined;
58
57
  branchId?: string | undefined;
59
- isRevision?: boolean | undefined;
58
+ isRevision?: unknown;
60
59
  revisesThought?: number | undefined;
60
+ nextThoughtNeeded?: unknown;
61
61
  relates?: {
62
62
  type: "depends_on" | "contradicts" | "supports" | "refines" | "supersedes" | "similar_to" | "located_in" | "violates" | "addresses" | "detected_by" | "invoked_by";
63
63
  targetId: string;
64
64
  reasoning?: string | undefined;
65
65
  }[] | undefined;
66
66
  branchFromThought?: number | undefined;
67
- needsMoreThoughts?: boolean | undefined;
67
+ needsMoreThoughts?: unknown;
68
68
  }>;
69
69
  export type ThinkInput = z.infer<typeof thinkSchema>;
70
70
  export declare function thinkHandler(graph: ThinkingGraph, input: ThinkInput): Promise<{
@@ -1,11 +1,12 @@
1
1
  import { z } from 'zod';
2
2
  import { NODE_TYPES, EDGE_TYPES } from '../engine/types.js';
3
+ const coerceBool = z.preprocess((v) => (v === 'true' ? true : v === 'false' ? false : v), z.boolean());
3
4
  export const thinkSchema = z.object({
4
5
  thought: z.string().describe('The reasoning content'),
5
6
  type: z.enum(NODE_TYPES).default('thought').describe('Node type'),
6
- thoughtNumber: z.number().int().min(1).describe('Current thought number'),
7
- totalThoughts: z.number().int().min(1).describe('Estimated total thoughts'),
8
- nextThoughtNeeded: z.boolean().describe('Whether another step is needed'),
7
+ thoughtNumber: z.coerce.number().int().min(1).describe('Current thought number'),
8
+ totalThoughts: z.coerce.number().int().min(1).describe('Estimated total thoughts'),
9
+ nextThoughtNeeded: coerceBool.describe('Whether another step is needed'),
9
10
  sessionId: z.string().optional().describe('Session ID (auto-generated if omitted)'),
10
11
  projectId: z.string().optional().describe('Project ID (detected from cwd if omitted)'),
11
12
  relates: z.array(z.object({
@@ -15,11 +16,11 @@ export const thinkSchema = z.object({
15
16
  })).optional().describe('Inline relationships'),
16
17
  metadata: z.record(z.unknown()).optional().describe('Flexible metadata'),
17
18
  // Backward compat
18
- isRevision: z.boolean().optional(),
19
- revisesThought: z.number().int().min(1).optional(),
20
- branchFromThought: z.number().int().min(1).optional(),
19
+ isRevision: coerceBool.optional(),
20
+ revisesThought: z.coerce.number().int().min(1).optional(),
21
+ branchFromThought: z.coerce.number().int().min(1).optional(),
21
22
  branchId: z.string().optional(),
22
- needsMoreThoughts: z.boolean().optional(),
23
+ needsMoreThoughts: coerceBool.optional(),
23
24
  });
24
25
  export async function thinkHandler(graph, input) {
25
26
  const session = input.sessionId
@@ -1,7 +1,6 @@
1
1
  -- Thinking Graph Schema v1
2
- -- Sessions, Nodes (with FTS5), Edges, Skill Registry
2
+ -- Sessions, Nodes, Edges, Skill Registry
3
3
 
4
- PRAGMA journal_mode = WAL;
5
4
  PRAGMA foreign_keys = ON;
6
5
 
7
6
  -- ─── Sessions ────────────────────────────────────────────
@@ -47,27 +46,6 @@ CREATE INDEX IF NOT EXISTS idx_nodes_project ON nodes(project_id);
47
46
  CREATE INDEX IF NOT EXISTS idx_nodes_branch ON nodes(branch_id);
48
47
  CREATE INDEX IF NOT EXISTS idx_nodes_created ON nodes(created_at);
49
48
 
50
- -- Full-text search
51
- CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
52
- content,
53
- content='nodes',
54
- content_rowid='rowid'
55
- );
56
-
57
- -- Keep FTS in sync
58
- CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN
59
- INSERT INTO nodes_fts(rowid, content) VALUES (new.rowid, new.content);
60
- END;
61
-
62
- CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes BEGIN
63
- INSERT INTO nodes_fts(nodes_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
64
- INSERT INTO nodes_fts(rowid, content) VALUES (new.rowid, new.content);
65
- END;
66
-
67
- CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN
68
- INSERT INTO nodes_fts(nodes_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
69
- END;
70
-
71
49
  -- ─── Edges ───────────────────────────────────────────────
72
50
 
73
51
  CREATE TABLE IF NOT EXISTS edges (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feelingmindful/thinking-graph",
3
- "version": "1.2.0",
3
+ "version": "1.4.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",
@@ -31,14 +31,14 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@modelcontextprotocol/sdk": "^1.12.0",
34
- "better-sqlite3": "^11.0.0",
35
- "zod": "^3.23.0",
36
- "uuid": "^10.0.0"
34
+ "sql.js": "^1.12.0",
35
+ "uuid": "^10.0.0",
36
+ "zod": "^3.23.0"
37
37
  },
38
38
  "devDependencies": {
39
- "vitest": "^3.0.0",
39
+ "@types/node": "^25.5.0",
40
+ "@types/uuid": "^10.0.0",
40
41
  "typescript": "^5.5.0",
41
- "@types/better-sqlite3": "^7.6.0",
42
- "@types/uuid": "^10.0.0"
42
+ "vitest": "^3.0.0"
43
43
  }
44
44
  }
@@ -90,74 +90,74 @@
90
90
  "produces": [], "invokes": ["premium-android:research", "premium-android:init", "premium-android:configure", "premium-android:refactor", "premium-android:create", "premium-core:growth", "premium-core:humanize", "premium-android:audit"], "platform": "android"
91
91
  },
92
92
  {
93
- "id": "nextjs-research", "pluginName": "premium-nextjs", "skillName": "research", "verb": "research",
94
- "invocation": "/premium-nextjs:research", "areas": [], "detects": [],
93
+ "id": "nextjs-research", "pluginName": "premium-web", "skillName": "nextjs", "verb": "research",
94
+ "invocation": "/premium-web:nextjs", "areas": [], "detects": [],
95
95
  "produces": ["research"], "invokes": ["premium-core:research"], "platform": "nextjs"
96
96
  },
97
97
  {
98
- "id": "nextjs-init", "pluginName": "premium-nextjs", "skillName": "init", "verb": "init",
99
- "invocation": "/premium-nextjs:init", "areas": ["infra", "design-tokens", "monetization", "observability"], "detects": ["missing"],
98
+ "id": "nextjs-init", "pluginName": "premium-web", "skillName": "nextjs", "verb": "init",
99
+ "invocation": "/premium-web:nextjs", "areas": ["infra", "design-tokens", "monetization", "observability"], "detects": ["missing"],
100
100
  "produces": ["code_fact", "decision"], "invokes": [], "platform": "nextjs"
101
101
  },
102
102
  {
103
- "id": "nextjs-configure", "pluginName": "premium-nextjs", "skillName": "configure", "verb": "configure",
104
- "invocation": "/premium-nextjs:configure", "areas": ["infra", "security", "observability"], "detects": ["needs-work"],
103
+ "id": "nextjs-configure", "pluginName": "premium-web", "skillName": "nextjs", "verb": "configure",
104
+ "invocation": "/premium-web:nextjs", "areas": ["infra", "security", "observability"], "detects": ["needs-work"],
105
105
  "produces": ["code_fact"], "invokes": [], "platform": "nextjs"
106
106
  },
107
107
  {
108
- "id": "nextjs-refactor", "pluginName": "premium-nextjs", "skillName": "refactor", "verb": "refactor",
109
- "invocation": "/premium-nextjs:refactor", "areas": ["architecture", "design-tokens", "icons", "copy"], "detects": ["needs-work"],
108
+ "id": "nextjs-refactor", "pluginName": "premium-web", "skillName": "nextjs", "verb": "refactor",
109
+ "invocation": "/premium-web:nextjs", "areas": ["architecture", "design-tokens", "icons", "copy"], "detects": ["needs-work"],
110
110
  "produces": ["tech_debt", "code_fact", "decision"], "invokes": ["premium-core:humanize"], "platform": "nextjs"
111
111
  },
112
112
  {
113
- "id": "nextjs-create", "pluginName": "premium-nextjs", "skillName": "create", "verb": "create",
114
- "invocation": "/premium-nextjs:create", "areas": ["design-tokens", "animations", "typography", "icons", "monetization"], "detects": ["missing"],
113
+ "id": "nextjs-create", "pluginName": "premium-web", "skillName": "nextjs", "verb": "create",
114
+ "invocation": "/premium-web:nextjs", "areas": ["design-tokens", "animations", "typography", "icons", "monetization"], "detects": ["missing"],
115
115
  "produces": ["code_fact", "decision"], "invokes": [], "platform": "nextjs"
116
116
  },
117
117
  {
118
- "id": "nextjs-audit", "pluginName": "premium-nextjs", "skillName": "audit", "verb": "audit",
119
- "invocation": "/premium-nextjs:audit", "areas": ["architecture", "design-tokens", "typography", "icons", "copy", "monetization", "security", "observability", "seo"], "detects": ["missing", "needs-work", "good"],
118
+ "id": "nextjs-audit", "pluginName": "premium-web", "skillName": "nextjs", "verb": "audit",
119
+ "invocation": "/premium-web:nextjs", "areas": ["architecture", "design-tokens", "typography", "icons", "copy", "monetization", "security", "observability", "seo"], "detects": ["missing", "needs-work", "good"],
120
120
  "produces": ["detection", "tech_debt"], "invokes": ["premium-core:audit"], "platform": "nextjs"
121
121
  },
122
122
  {
123
- "id": "nextjs-full", "pluginName": "premium-nextjs", "skillName": "full", "verb": "full",
124
- "invocation": "/premium-nextjs:full", "areas": [], "detects": [],
125
- "produces": [], "invokes": ["premium-nextjs:research", "premium-nextjs:init", "premium-nextjs:configure", "premium-nextjs:refactor", "premium-nextjs:create", "premium-core:growth", "premium-core:humanize", "premium-nextjs:audit"], "platform": "nextjs"
123
+ "id": "nextjs-full", "pluginName": "premium-web", "skillName": "nextjs", "verb": "full",
124
+ "invocation": "/premium-web:nextjs", "areas": [], "detects": [],
125
+ "produces": [], "invokes": ["premium-web:nextjs", "premium-core:growth", "premium-core:humanize"], "platform": "nextjs"
126
126
  },
127
127
  {
128
- "id": "astro-research", "pluginName": "premium-astro", "skillName": "research", "verb": "research",
129
- "invocation": "/premium-astro:research", "areas": [], "detects": [],
128
+ "id": "astro-research", "pluginName": "premium-web", "skillName": "astro", "verb": "research",
129
+ "invocation": "/premium-web:astro", "areas": [], "detects": [],
130
130
  "produces": ["research"], "invokes": ["premium-core:research"], "platform": "astro"
131
131
  },
132
132
  {
133
- "id": "astro-init", "pluginName": "premium-astro", "skillName": "init", "verb": "init",
134
- "invocation": "/premium-astro:init", "areas": ["infra", "design-tokens", "monetization", "observability"], "detects": ["missing"],
133
+ "id": "astro-init", "pluginName": "premium-web", "skillName": "astro", "verb": "init",
134
+ "invocation": "/premium-web:astro", "areas": ["infra", "design-tokens", "monetization", "observability"], "detects": ["missing"],
135
135
  "produces": ["code_fact", "decision"], "invokes": [], "platform": "astro"
136
136
  },
137
137
  {
138
- "id": "astro-configure", "pluginName": "premium-astro", "skillName": "configure", "verb": "configure",
139
- "invocation": "/premium-astro:configure", "areas": ["infra", "security", "observability"], "detects": ["needs-work"],
138
+ "id": "astro-configure", "pluginName": "premium-web", "skillName": "astro", "verb": "configure",
139
+ "invocation": "/premium-web:astro", "areas": ["infra", "security", "observability"], "detects": ["needs-work"],
140
140
  "produces": ["code_fact"], "invokes": [], "platform": "astro"
141
141
  },
142
142
  {
143
- "id": "astro-refactor", "pluginName": "premium-astro", "skillName": "refactor", "verb": "refactor",
144
- "invocation": "/premium-astro:refactor", "areas": ["architecture", "design-tokens", "islands-hydration", "copy"], "detects": ["needs-work"],
143
+ "id": "astro-refactor", "pluginName": "premium-web", "skillName": "astro", "verb": "refactor",
144
+ "invocation": "/premium-web:astro", "areas": ["architecture", "design-tokens", "islands-hydration", "copy"], "detects": ["needs-work"],
145
145
  "produces": ["tech_debt", "code_fact", "decision"], "invokes": ["premium-core:humanize"], "platform": "astro"
146
146
  },
147
147
  {
148
- "id": "astro-create", "pluginName": "premium-astro", "skillName": "create", "verb": "create",
149
- "invocation": "/premium-astro:create", "areas": ["design-tokens", "animations", "typography", "icons", "monetization"], "detects": ["missing"],
148
+ "id": "astro-create", "pluginName": "premium-web", "skillName": "astro", "verb": "create",
149
+ "invocation": "/premium-web:astro", "areas": ["design-tokens", "animations", "typography", "icons", "monetization"], "detects": ["missing"],
150
150
  "produces": ["code_fact", "decision"], "invokes": [], "platform": "astro"
151
151
  },
152
152
  {
153
- "id": "astro-audit", "pluginName": "premium-astro", "skillName": "audit", "verb": "audit",
154
- "invocation": "/premium-astro:audit", "areas": ["architecture", "design-tokens", "islands-hydration", "content-collections", "copy", "monetization", "security", "observability", "seo"], "detects": ["missing", "needs-work", "good"],
153
+ "id": "astro-audit", "pluginName": "premium-web", "skillName": "astro", "verb": "audit",
154
+ "invocation": "/premium-web:astro", "areas": ["architecture", "design-tokens", "islands-hydration", "content-collections", "copy", "monetization", "security", "observability", "seo"], "detects": ["missing", "needs-work", "good"],
155
155
  "produces": ["detection", "tech_debt"], "invokes": ["premium-core:audit"], "platform": "astro"
156
156
  },
157
157
  {
158
- "id": "astro-full", "pluginName": "premium-astro", "skillName": "full", "verb": "full",
159
- "invocation": "/premium-astro:full", "areas": [], "detects": [],
160
- "produces": [], "invokes": ["premium-astro:research", "premium-astro:init", "premium-astro:configure", "premium-astro:refactor", "premium-astro:create", "premium-core:growth", "premium-core:humanize", "premium-astro:audit"], "platform": "astro"
158
+ "id": "astro-full", "pluginName": "premium-web", "skillName": "astro", "verb": "full",
159
+ "invocation": "/premium-web:astro", "areas": [], "detects": [],
160
+ "produces": [], "invokes": ["premium-web:astro", "premium-core:growth", "premium-core:humanize"], "platform": "astro"
161
161
  },
162
162
  {
163
163
  "id": "marketing-product-context", "pluginName": "marketing-skills", "skillName": "product-marketing-context", "verb": "context",