@feelingmindful/thinking-graph 1.10.0 → 1.10.1

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,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { ThinkingGraph } from '../engine/graph.js';
3
- import type { VaultBridge } from '../vault/bridge.js';
3
+ import { type VaultBridge } from '../vault/bridge.js';
4
4
  export declare const learnSchema: z.ZodObject<{
5
5
  content: z.ZodString;
6
6
  type: z.ZodEnum<["thought", "decision", "insight", "code_fact", "assumption", "detection", "tech_debt", "principle", "pattern", "skill_result", "research"]>;
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { NODE_TYPES, EDGE_TYPES } from '../engine/types.js';
3
+ import { deriveTitle } from '../vault/bridge.js';
3
4
  export const learnSchema = z.object({
4
5
  content: z.string().describe('What was learned'),
5
6
  type: z.enum(NODE_TYPES).describe('Node type'),
@@ -81,7 +82,7 @@ export async function learnHandler(graph, input, vault, projectSlug) {
81
82
  let vaultPath = null;
82
83
  if (vault && projectSlug) {
83
84
  try {
84
- const title = input.content.slice(0, 80).replace(/[^a-zA-Z0-9 ]/g, '').trim() || input.type;
85
+ const title = deriveTitle(input.content, input.type);
85
86
  vaultPath = vault.write({
86
87
  title,
87
88
  type: input.type,
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { ThinkingGraph } from '../engine/graph.js';
3
- import type { VaultBridge } from '../vault/bridge.js';
3
+ import { type VaultBridge } from '../vault/bridge.js';
4
4
  export declare const researchSchema: z.ZodObject<{
5
5
  query: z.ZodString;
6
6
  intent: z.ZodDefault<z.ZodEnum<["fact_check", "explore", "compare", "how_to", "current_state"]>>;
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { deriveTitle } from '../vault/bridge.js';
2
3
  const coerceBool = z.preprocess((v) => (v === 'true' ? true : v === 'false' ? false : v), z.boolean());
3
4
  const RESEARCH_INTENTS = [
4
5
  'fact_check', // Verify a claim or assumption
@@ -162,8 +163,7 @@ export async function researchHandler(graph, input, vault, projectSlug) {
162
163
  // Write finding to Obsidian vault
163
164
  if (vault && projectSlug) {
164
165
  try {
165
- const title = finding.content.slice(0, 80).replace(/[^a-zA-Z0-9 ]/g, '').trim()
166
- || `Research ${input.intent}`;
166
+ const title = deriveTitle(finding.content, `Research ${input.intent}`);
167
167
  const vaultPath = vault.write({
168
168
  title,
169
169
  type: 'research',
@@ -29,6 +29,11 @@ export interface VaultWriteOpts {
29
29
  projectSlug: string;
30
30
  metadata?: Record<string, unknown>;
31
31
  }
32
+ /**
33
+ * Derive a short, meaningful title from freeform content.
34
+ * Takes the first sentence (or first N words) and sanitizes it.
35
+ */
36
+ export declare function deriveTitle(content: string, fallback: string): string;
32
37
  export declare class VaultBridge {
33
38
  private vaultRoot;
34
39
  constructor(vaultPath: string);
@@ -33,12 +33,37 @@ function expandHome(p) {
33
33
  return join(homedir(), p.slice(2));
34
34
  return p;
35
35
  }
36
+ // Obsidian titles should stay under 60 chars for clean sidebar display.
37
+ // macOS allows 255 bytes but long titles break Obsidian search and links.
38
+ const MAX_TITLE_LENGTH = 60;
36
39
  function sanitizeFilename(name) {
37
40
  return name
38
- .replace(/[<>:"/\\|?*]/g, '')
41
+ .replace(/[<>:"/\\|?*#^\[\]]/g, '') // filesystem + Obsidian-reserved chars
39
42
  .replace(/\s+/g, ' ')
40
43
  .trim()
41
- .slice(0, 120);
44
+ .slice(0, MAX_TITLE_LENGTH);
45
+ }
46
+ /**
47
+ * Derive a short, meaningful title from freeform content.
48
+ * Takes the first sentence (or first N words) and sanitizes it.
49
+ */
50
+ export function deriveTitle(content, fallback) {
51
+ // Strip leading markdown headings/bullets
52
+ const cleaned = content.replace(/^[#\-*>\s]+/, '').trim();
53
+ if (!cleaned)
54
+ return sanitizeFilename(fallback);
55
+ // Take first sentence (up to period, newline, or em-dash)
56
+ const firstSentence = cleaned.split(/[.\n—]/, 1)[0].trim();
57
+ // If the sentence is short enough, use it
58
+ if (firstSentence.length > 0 && firstSentence.length <= MAX_TITLE_LENGTH) {
59
+ const title = firstSentence.replace(/[^a-zA-Z0-9 \-_]/g, '').trim();
60
+ if (title.length >= 5)
61
+ return sanitizeFilename(title);
62
+ }
63
+ // Otherwise take first ~8 words
64
+ const words = cleaned.replace(/[^a-zA-Z0-9 \-_]/g, '').split(/\s+/).filter(Boolean);
65
+ const shortTitle = words.slice(0, 8).join(' ');
66
+ return sanitizeFilename(shortTitle || fallback);
42
67
  }
43
68
  /**
44
69
  * Walk a directory tree and yield .md files.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feelingmindful/thinking-graph",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
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",