opencode-fractal-memory 0.2.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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +493 -0
  3. package/agent/memory-hints.md +98 -0
  4. package/agent/memory-researcher.md +56 -0
  5. package/commands/memory-auto-test.md +10 -0
  6. package/commands/memory-cache-status.md +13 -0
  7. package/commands/memory-check-context.md +4 -0
  8. package/commands/memory-compress.md +13 -0
  9. package/commands/memory-dashboard.md +23 -0
  10. package/commands/memory-delete.md +24 -0
  11. package/commands/memory-detect-topics.md +28 -0
  12. package/commands/memory-distill.md +35 -0
  13. package/commands/memory-drilldown-query.md +28 -0
  14. package/commands/memory-drilldown.md +11 -0
  15. package/commands/memory-extract-patterns.md +4 -0
  16. package/commands/memory-generate-embeddings.md +26 -0
  17. package/commands/memory-get.md +26 -0
  18. package/commands/memory-help.md +55 -0
  19. package/commands/memory-injection-feedback.md +26 -0
  20. package/commands/memory-injection-stats.md +11 -0
  21. package/commands/memory-list.md +4 -0
  22. package/commands/memory-llm-compress.md +34 -0
  23. package/commands/memory-mcp.md +20 -0
  24. package/commands/memory-prune.md +4 -0
  25. package/commands/memory-rate.md +48 -0
  26. package/commands/memory-reflect.md +37 -0
  27. package/commands/memory-replace.md +26 -0
  28. package/commands/memory-retrieve.md +34 -0
  29. package/commands/memory-search.md +28 -0
  30. package/commands/memory-session-stats.md +4 -0
  31. package/commands/memory-set.md +31 -0
  32. package/commands/memory-stats.md +11 -0
  33. package/commands/memory-summarize.md +29 -0
  34. package/commands/memory-tool-stats.md +4 -0
  35. package/commands/memory-total-tokens.md +10 -0
  36. package/commands/memory-verify.md +4 -0
  37. package/commands/memory-version.md +9 -0
  38. package/dist/cache.js +39 -0
  39. package/dist/config.js +120 -0
  40. package/dist/embeddings.js +125 -0
  41. package/dist/ensure-models.js +70 -0
  42. package/dist/file-summary.js +143 -0
  43. package/dist/frontmatter.js +28 -0
  44. package/dist/hnsw-index.js +138 -0
  45. package/dist/hooks/auto-discover.js +4 -0
  46. package/dist/hooks/auto-distill.js +120 -0
  47. package/dist/hooks/auto-retrieve/content.js +47 -0
  48. package/dist/hooks/auto-retrieve/detection.js +50 -0
  49. package/dist/hooks/auto-retrieve/formatting.js +19 -0
  50. package/dist/hooks/auto-retrieve/index.js +163 -0
  51. package/dist/hooks/auto-retrieve/scoring.js +56 -0
  52. package/dist/hooks/auto-retrieve.js +1 -0
  53. package/dist/hooks/index.js +4 -0
  54. package/dist/hooks/predictive-rating.js +87 -0
  55. package/dist/journal.js +279 -0
  56. package/dist/logging.js +147 -0
  57. package/dist/management/helpers.js +227 -0
  58. package/dist/management/router.js +48 -0
  59. package/dist/management/routes.js +197 -0
  60. package/dist/management-server.js +4 -0
  61. package/dist/management-standalone.js +31 -0
  62. package/dist/mcp/logging.js +57 -0
  63. package/dist/mcp/server.js +251 -0
  64. package/dist/mcp/transform.js +48 -0
  65. package/dist/mcp-server.js +18 -0
  66. package/dist/memory.js +2 -0
  67. package/dist/ollama.js +74 -0
  68. package/dist/plugin/hooks.js +168 -0
  69. package/dist/plugin/index.js +28 -0
  70. package/dist/plugin/init.js +109 -0
  71. package/dist/plugin/state.js +75 -0
  72. package/dist/plugin/tools.js +45 -0
  73. package/dist/plugin.js +2 -0
  74. package/dist/procedural/store.js +1 -0
  75. package/dist/procedural/types.js +1 -0
  76. package/dist/seed-nodes.js +804 -0
  77. package/dist/storage/compress-ops.js +129 -0
  78. package/dist/storage/compression/formatters.js +243 -0
  79. package/dist/storage/compression/index.js +107 -0
  80. package/dist/storage/compression/patterns.js +138 -0
  81. package/dist/storage/expiration.js +66 -0
  82. package/dist/storage/index.js +1 -0
  83. package/dist/storage/injection-events.js +82 -0
  84. package/dist/storage/lifecycle.js +65 -0
  85. package/dist/storage/maintenance.js +60 -0
  86. package/dist/storage/migrations/definitions.js +374 -0
  87. package/dist/storage/migrations/index.js +21 -0
  88. package/dist/storage/navigation.js +98 -0
  89. package/dist/storage/queries/base.js +44 -0
  90. package/dist/storage/queries/links.js +32 -0
  91. package/dist/storage/queries/nodes.js +189 -0
  92. package/dist/storage/queries/search-helpers.js +239 -0
  93. package/dist/storage/scoring.js +36 -0
  94. package/dist/storage/search.js +233 -0
  95. package/dist/storage/session-tracking.js +180 -0
  96. package/dist/storage/sqlite.js +329 -0
  97. package/dist/storage/tool-usage.js +56 -0
  98. package/dist/storage/types.js +1 -0
  99. package/dist/storage/utils.js +94 -0
  100. package/dist/tools/auto-test.js +24 -0
  101. package/dist/tools/cache-status.js +36 -0
  102. package/dist/tools/compress.js +186 -0
  103. package/dist/tools/core.js +307 -0
  104. package/dist/tools/dashboard.js +97 -0
  105. package/dist/tools/help.js +59 -0
  106. package/dist/tools/index.js +12 -0
  107. package/dist/tools/inject.js +91 -0
  108. package/dist/tools/injection-debug.js +48 -0
  109. package/dist/tools/journal.js +105 -0
  110. package/dist/tools/llm-compress.js +41 -0
  111. package/dist/tools/middle-term.js +68 -0
  112. package/dist/tools/playbook.js +64 -0
  113. package/dist/tools/reflect.js +291 -0
  114. package/dist/tools/search.js +188 -0
  115. package/dist/tools/session.js +189 -0
  116. package/dist/tools/shared.js +74 -0
  117. package/dist/tools/skill.js +37 -0
  118. package/dist/tools/stats.js +256 -0
  119. package/dist/tools/version.js +13 -0
  120. package/dist/tools.js +18 -0
  121. package/dist/utils/hybridScore.js +67 -0
  122. package/management/public/app.js +1529 -0
  123. package/management/public/index.html +486 -0
  124. package/management/public/three.min.js +6 -0
  125. package/package.json +65 -0
  126. package/scripts/download-models.ts +16 -0
  127. package/scripts/postinstall.cjs +30 -0
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Show complete token analysis (memory + conversation)
3
+ ---
4
+ Show me a complete token analysis including:
5
+ - Memory system tokens (L0, L1, L2+ summaries)
6
+ - Conversation tokens (input, output, reasoning, cache)
7
+ - Total tokens and percentage of context limit
8
+ - Recommendations for compression if needed
9
+
10
+ Use memory_total_tokens with include_messages=false for summary, or include_messages=true for detailed message breakdown.
@@ -0,0 +1,4 @@
1
+ ---
2
+ description: Verify node correctness to boost confidence
3
+ ---
4
+ Confirm that the information in a memory node is correct. This increases the node's confidence score by 0.2 and records the verification timestamp. Use memory_verify with the node's id or label. Verified nodes rank higher in semantic search results.
@@ -0,0 +1,9 @@
1
+ ---
2
+ title: memory-version
3
+ description: Show the installed version of the Fractal Memory plugin
4
+ usage: /memory-version
5
+ examples:
6
+ - memory-version
7
+ tags:
8
+ - version
9
+ - info
package/dist/cache.js ADDED
@@ -0,0 +1,39 @@
1
+ const workingMemoryCache = new Map();
2
+ let CACHE_MAX_SIZE = 8;
3
+ let CACHE_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours
4
+ export function getWorkingCache(sessionId) {
5
+ const cache = workingMemoryCache.get(sessionId) || [];
6
+ const now = Date.now();
7
+ // Filter expired entries
8
+ return cache.filter(n => now - n.cachedAt < CACHE_TTL_MS);
9
+ }
10
+ export function addToWorkingCache(sessionId, node) {
11
+ let cache = workingMemoryCache.get(sessionId) || [];
12
+ // Remove if already exists
13
+ cache = cache.filter(n => n.id !== node.id);
14
+ // Add to front (most recent)
15
+ cache.unshift({
16
+ id: node.id,
17
+ label: node.label ?? node.id.slice(0, 8),
18
+ content: node.content,
19
+ importance: node.importance ?? 0.5,
20
+ cachedAt: Date.now()
21
+ });
22
+ // Trim to max size
23
+ if (cache.length > CACHE_MAX_SIZE) {
24
+ cache = cache.slice(0, CACHE_MAX_SIZE);
25
+ }
26
+ workingMemoryCache.set(sessionId, cache);
27
+ }
28
+ export function invalidateWorkingCache(sessionId, nodeId) {
29
+ if (!nodeId) {
30
+ workingMemoryCache.delete(sessionId);
31
+ return;
32
+ }
33
+ const cache = workingMemoryCache.get(sessionId) || [];
34
+ workingMemoryCache.set(sessionId, cache.filter(n => n.id !== nodeId));
35
+ }
36
+ export function setCacheConfig(maxSize, ttlHours) {
37
+ CACHE_MAX_SIZE = maxSize;
38
+ CACHE_TTL_MS = ttlHours * 60 * 60 * 1000;
39
+ }
package/dist/config.js ADDED
@@ -0,0 +1,120 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { z } from "zod";
5
+ import { memLog } from "./logging";
6
+ const AutoRetrieveSchema = z.object({
7
+ enabled: z.boolean().default(false),
8
+ candidateCount: z.number().positive().int().default(30),
9
+ maxInjectNodes: z.number().positive().int().default(5),
10
+ maxInjectPlaybooks: z.number().positive().int().default(3),
11
+ });
12
+ const OllamaSchema = z.object({
13
+ enabled: z.boolean().default(false),
14
+ baseUrl: z.string().default("http://localhost:11434"),
15
+ model: z.string().default("qwen2.5-coder:1.5b"),
16
+ mode: z.enum(["binary", "score"]).default("binary"),
17
+ });
18
+ const LlmCompressionSchema = z.object({
19
+ enabled: z.boolean().default(false),
20
+ model: z.string().optional(),
21
+ maxSummaryTokens: z.number().positive().int().default(500),
22
+ });
23
+ const AutoDistillSchema = z.object({
24
+ enabled: z.boolean().default(false),
25
+ minLessons: z.number().positive().int().default(3),
26
+ useLlm: z.boolean().default(false),
27
+ });
28
+ const AutoDiscoverSchema = z.object({
29
+ enabled: z.boolean().default(false),
30
+ minSequenceLength: z.number().positive().int().default(3),
31
+ minRepeatCount: z.number().positive().int().default(2),
32
+ maxInjectPlaybooks: z.number().positive().int().default(3),
33
+ });
34
+ const PredictiveRatingSchema = z.object({
35
+ enabled: z.boolean().default(false),
36
+ decayDays: z.number().positive().default(7),
37
+ confidenceThreshold: z.number().min(0).max(1).default(0.3),
38
+ positiveBoost: z.number().min(0).max(1).default(0.1),
39
+ negativePenalty: z.number().min(0).max(1).default(0.05),
40
+ });
41
+ const DEFAULT_CONFIG = {
42
+ maxInjectionTokens: 8000,
43
+ coreInjectionTokens: 2000,
44
+ cacheSize: 8,
45
+ cacheTTLHours: 2,
46
+ autoCompressThreshold: 0.7,
47
+ highContextThreshold: 0.6,
48
+ criticalContextThreshold: 0.8,
49
+ enableMiddleTermCapture: true,
50
+ defaultTtlDays: 0,
51
+ autoFileSummarization: {
52
+ enabled: false,
53
+ },
54
+ autoRetrieve: {
55
+ enabled: false,
56
+ candidateCount: 30,
57
+ maxInjectNodes: 5,
58
+ maxInjectPlaybooks: 3,
59
+ },
60
+ ollama: {
61
+ enabled: false,
62
+ baseUrl: "http://localhost:11434",
63
+ model: "qwen2.5-coder:1.5b",
64
+ mode: "binary",
65
+ },
66
+ llmCompression: {
67
+ enabled: false,
68
+ maxSummaryTokens: 500,
69
+ },
70
+ autoDistill: {
71
+ enabled: false,
72
+ minLessons: 3,
73
+ useLlm: false,
74
+ },
75
+ predictiveRating: {
76
+ enabled: false,
77
+ decayDays: 7,
78
+ confidenceThreshold: 0.3,
79
+ positiveBoost: 0.1,
80
+ negativePenalty: 0.05,
81
+ },
82
+ autoDiscover: {
83
+ enabled: false,
84
+ minSequenceLength: 3,
85
+ minRepeatCount: 2,
86
+ maxInjectPlaybooks: 3,
87
+ },
88
+ };
89
+ const MemConfigSchema = z.object({
90
+ maxInjectionTokens: z.number().positive().int().default(8000),
91
+ coreInjectionTokens: z.number().positive().int().default(2000),
92
+ cacheSize: z.number().positive().int().default(8),
93
+ cacheTTLHours: z.number().positive().default(2),
94
+ autoCompressThreshold: z.number().min(0).max(1).default(0.7),
95
+ highContextThreshold: z.number().min(0).max(1).default(0.6),
96
+ criticalContextThreshold: z.number().min(0).max(1).default(0.8),
97
+ enableMiddleTermCapture: z.boolean().default(true),
98
+ defaultTtlDays: z.number().int().min(0).default(0),
99
+ autoFileSummarization: z.object({
100
+ enabled: z.boolean().default(false),
101
+ }).optional(),
102
+ autoRetrieve: AutoRetrieveSchema.optional(),
103
+ ollama: OllamaSchema.optional(),
104
+ llmCompression: LlmCompressionSchema.optional(),
105
+ autoDistill: AutoDistillSchema.optional(),
106
+ predictiveRating: PredictiveRatingSchema.optional(),
107
+ autoDiscover: AutoDiscoverSchema.optional(),
108
+ }).default(DEFAULT_CONFIG);
109
+ export async function loadMemConfig(_projectRoot) {
110
+ const configPath = path.join(os.homedir(), ".config", "opencode", "opencode-mem.json");
111
+ try {
112
+ const raw = await fs.promises.readFile(configPath, "utf-8");
113
+ const parsed = MemConfigSchema.parse(JSON.parse(raw));
114
+ return parsed;
115
+ }
116
+ catch (err) {
117
+ memLog("warn", "config", "Failed to load config, using defaults", { error: String(err), configPath });
118
+ return DEFAULT_CONFIG;
119
+ }
120
+ }
@@ -0,0 +1,125 @@
1
+ import { InferenceSession, Tensor } from "onnxruntime-web";
2
+ import { Tokenizer } from "@huggingface/tokenizers";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join } from "path";
5
+ import { homedir } from "os";
6
+ const MODELS_DIR = join(homedir(), ".config", "opencode", "models", "Xenova", "all-MiniLM-L6-v2");
7
+ const MODEL_PATH = join(MODELS_DIR, "onnx", "model_quantized.onnx");
8
+ const TOKENIZER_JSON_PATH = join(MODELS_DIR, "tokenizer.json");
9
+ const TOKENIZER_CONFIG_PATH = join(MODELS_DIR, "tokenizer_config.json");
10
+ let session;
11
+ let tokenizer;
12
+ async function getSession() {
13
+ if (!session) {
14
+ session = await InferenceSession.create(MODEL_PATH, {
15
+ executionProviders: ["wasm"],
16
+ });
17
+ }
18
+ return session;
19
+ }
20
+ async function loadTokenizer() {
21
+ if (!tokenizer) {
22
+ const [tokenizerJsonRaw, tokenizerConfigRaw] = await Promise.all([
23
+ readFile(TOKENIZER_JSON_PATH, "utf-8"),
24
+ readFile(TOKENIZER_CONFIG_PATH, "utf-8"),
25
+ ]);
26
+ const tokenizerJson = JSON.parse(tokenizerJsonRaw);
27
+ const tokenizerConfig = JSON.parse(tokenizerConfigRaw);
28
+ tokenizer = new Tokenizer(tokenizerJson, tokenizerConfig);
29
+ }
30
+ return tokenizer;
31
+ }
32
+ // Synchronous version for internal use (tokenizer must be loaded first)
33
+ function getTokenizerSync() {
34
+ if (!tokenizer) {
35
+ throw new Error("Tokenizer not loaded. Call loadTokenizer() first.");
36
+ }
37
+ return tokenizer;
38
+ }
39
+ export async function generateEmbedding(text) {
40
+ const [session, tok] = await Promise.all([
41
+ getSession(),
42
+ loadTokenizer()
43
+ ]);
44
+ const encoded = tok.encode(text);
45
+ const ids = encoded.ids;
46
+ const attentionMask = encoded.attention_mask;
47
+ const seqLen = Math.min(ids.length, 256);
48
+ const inputIds = new BigInt64Array(seqLen);
49
+ const mask = new BigInt64Array(seqLen);
50
+ for (let i = 0; i < seqLen; i++) {
51
+ inputIds[i] = BigInt(ids[i]);
52
+ mask[i] = BigInt(attentionMask[i]);
53
+ }
54
+ const feeds = {
55
+ input_ids: new Tensor("int64", inputIds, [1, seqLen]),
56
+ attention_mask: new Tensor("int64", mask, [1, seqLen]),
57
+ token_type_ids: new Tensor("int64", new BigInt64Array(seqLen), [1, seqLen]),
58
+ };
59
+ const results = await session.run(feeds);
60
+ const output = results["last_hidden_state"];
61
+ if (!output)
62
+ throw new Error("No output from model");
63
+ const lastHiddenState = output.data;
64
+ const hiddenSize = 384;
65
+ let count = 0;
66
+ for (let i = 0; i < seqLen; i++) {
67
+ if (mask[i] === BigInt(1))
68
+ count++;
69
+ }
70
+ const pooled = new Array(hiddenSize).fill(0);
71
+ for (let i = 0; i < seqLen; i++) {
72
+ if (mask[i] === BigInt(1)) {
73
+ for (let j = 0; j < hiddenSize; j++) {
74
+ pooled[j] = (pooled[j] ?? 0) + (lastHiddenState[i * hiddenSize + j] ?? 0);
75
+ }
76
+ }
77
+ }
78
+ if (count > 0) {
79
+ for (let j = 0; j < hiddenSize; j++) {
80
+ pooled[j] = (pooled[j] ?? 0) / count;
81
+ }
82
+ }
83
+ let norm = 0;
84
+ for (const v of pooled) {
85
+ norm += v * v;
86
+ }
87
+ norm = Math.sqrt(norm);
88
+ const normalized = [];
89
+ if (norm > 0) {
90
+ for (const v of pooled) {
91
+ normalized.push(v / norm);
92
+ }
93
+ }
94
+ return normalized;
95
+ }
96
+ export function estimateTokens(text) {
97
+ // If the tokenizer hasn't been loaded (e.g., in unit tests), fall back to a simple word‑count heuristic.
98
+ if (!tokenizer) {
99
+ // Approximate token count as 1.5 tokens per whitespace‑separated word.
100
+ const words = text.trim().split(/\s+/).filter(Boolean).length;
101
+ return Math.max(1, Math.ceil(words * 1.5));
102
+ }
103
+ const tok = getTokenizerSync();
104
+ const encoded = tok.encode(text);
105
+ return encoded.ids.length;
106
+ }
107
+ export function cosineSimilarity(a, b) {
108
+ if (a.length !== b.length) {
109
+ throw new Error(`Embedding dimension mismatch: ${a.length} vs ${b.length}`);
110
+ }
111
+ let dotProduct = 0;
112
+ let normA = 0;
113
+ let normB = 0;
114
+ for (let i = 0; i < a.length; i++) {
115
+ const ai = a[i];
116
+ const bi = b[i];
117
+ dotProduct += ai * bi;
118
+ normA += ai * ai;
119
+ normB += bi * bi;
120
+ }
121
+ const denominator = Math.sqrt(normA) * Math.sqrt(normB);
122
+ if (denominator === 0)
123
+ return 0;
124
+ return dotProduct / denominator;
125
+ }
@@ -0,0 +1,70 @@
1
+ import { mkdir, writeFile, access, cp } from "node:fs/promises";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { homedir } from "os";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const BASE = join(homedir(), ".config", "opencode", "models", "Xenova", "all-MiniLM-L6-v2");
8
+ const ONNX_DIR = join(BASE, "onnx");
9
+ const FILES = [
10
+ {
11
+ path: join(ONNX_DIR, "model_quantized.onnx"),
12
+ url: "https://huggingface.co/Xenova/all-MiniLM-L6-v2/resolve/main/onnx/model_quantized.onnx",
13
+ },
14
+ {
15
+ path: join(BASE, "tokenizer.json"),
16
+ url: "https://huggingface.co/Xenova/all-MiniLM-L6-v2/resolve/main/tokenizer.json",
17
+ },
18
+ {
19
+ path: join(BASE, "tokenizer_config.json"),
20
+ url: "https://huggingface.co/Xenova/all-MiniLM-L6-v2/resolve/main/tokenizer_config.json",
21
+ },
22
+ ];
23
+ async function fileExists(path) {
24
+ try {
25
+ await access(path);
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ async function download(url, dest) {
33
+ const response = await fetch(url);
34
+ if (!response.ok)
35
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
36
+ const buffer = await response.arrayBuffer();
37
+ await writeFile(dest, new Uint8Array(buffer));
38
+ }
39
+ export async function ensureModels() {
40
+ let needsDownload = false;
41
+ for (const f of FILES) {
42
+ if (!(await fileExists(f.path))) {
43
+ needsDownload = true;
44
+ break;
45
+ }
46
+ }
47
+ if (!needsDownload)
48
+ return;
49
+ await mkdir(ONNX_DIR, { recursive: true });
50
+ for (const f of FILES) {
51
+ const name = f.url.split("/").pop();
52
+ process.stdout.write(`Downloading ${name} ... `);
53
+ await download(f.url, f.path);
54
+ const stat = await import("node:fs/promises").then(m => m.stat(f.path));
55
+ const mb = (stat.size / 1024 / 1024).toFixed(1);
56
+ process.stdout.write(`${mb} MB\n`);
57
+ }
58
+ }
59
+ export async function ensureAgentFiles() {
60
+ const srcAgent = join(__dirname, "..", "agent");
61
+ const dstAgent = join(homedir(), ".config", "opencode", "agent");
62
+ await mkdir(dstAgent, { recursive: true });
63
+ await cp(srcAgent, dstAgent, { recursive: true, force: true });
64
+ }
65
+ export async function ensureCommandFiles() {
66
+ const srcCommands = join(__dirname, "..", "commands");
67
+ const dstCommands = join(homedir(), ".config", "opencode", "commands");
68
+ await mkdir(dstCommands, { recursive: true });
69
+ await cp(srcCommands, dstCommands, { recursive: true, force: true });
70
+ }
@@ -0,0 +1,143 @@
1
+ export const SOURCE_FILE_EXTENSIONS = [
2
+ 'ts', 'js', 'tsx', 'jsx', 'py', 'rs', 'go', 'java', 'c', 'cpp', 'h', 'hpp',
3
+ 'cs', 'rb', 'php', 'swift', 'kt', 'scala', 'md', 'txt',
4
+ ];
5
+ export function generateFileLabel(filePath) {
6
+ const fileName = filePath.split('/').pop() ?? filePath;
7
+ const maxFileNameLen = 30;
8
+ const truncatedName = fileName.length > maxFileNameLen ? fileName.slice(-maxFileNameLen) : fileName;
9
+ const pathHash = Math.abs(filePath.split('').reduce((a, c) => a + c.charCodeAt(0), 0) % 46656).toString(36);
10
+ return `file:${truncatedName}:${pathHash}`;
11
+ }
12
+ export function generateFileSummary(fileName, filePath, content, fileExt) {
13
+ const lines = content.split('\n');
14
+ const summary = [];
15
+ summary.push(`## File: ${fileName}`);
16
+ summary.push(`Path: ${filePath}`);
17
+ summary.push(`Lines: ${lines.length}`);
18
+ summary.push("");
19
+ if (['ts', 'js', 'tsx', 'jsx'].includes(fileExt)) {
20
+ const exports = [];
21
+ const imports = [];
22
+ const functions = [];
23
+ const classes = [];
24
+ const interfaces = [];
25
+ for (let i = 0; i < lines.length; i++) {
26
+ const line = lines[i] ?? '';
27
+ const trimmed = line.trim();
28
+ const lineNum = i + 1;
29
+ const typeImportMatch = trimmed.match(/^import\s+type\s+\{[^}]*\}\s+from\s+['"]([^'"]+)['"]/);
30
+ if (typeImportMatch?.[1])
31
+ imports.push(typeImportMatch[1]);
32
+ const importMatch = trimmed.match(/^import\s+(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"]/);
33
+ if (importMatch?.[1])
34
+ imports.push(importMatch[1]);
35
+ const reExportMatch = trimmed.match(/^export\s+\{[^}]*\}\s+from\s+['"]([^'"]+)['"]/);
36
+ if (reExportMatch?.[1])
37
+ imports.push(reExportMatch[1]);
38
+ const exportMatch = trimmed.match(/^export\s+(?:const|let|var|function|class|interface|type)\s+(\w+)/);
39
+ if (exportMatch?.[1])
40
+ exports.push(exportMatch[1]);
41
+ const defaultExportMatch = trimmed.match(/^export\s+default\s+(\w+)/);
42
+ if (defaultExportMatch?.[1])
43
+ exports.push(defaultExportMatch[1] + ' (default)');
44
+ const funcMatch = trimmed.match(/^(?:export\s+)?function\s+(\w+)/);
45
+ if (funcMatch?.[1])
46
+ functions.push({ name: funcMatch[1], line: lineNum });
47
+ const constFuncMatch = trimmed.match(/^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(/);
48
+ if (constFuncMatch?.[1])
49
+ functions.push({ name: constFuncMatch[1], line: lineNum });
50
+ const classMatch = trimmed.match(/^(?:export\s+)?class\s+(\w+)/);
51
+ if (classMatch?.[1])
52
+ classes.push({ name: classMatch[1], line: lineNum });
53
+ const interfaceMatch = trimmed.match(/^(?:export\s+)?interface\s+(\w+)/);
54
+ if (interfaceMatch?.[1])
55
+ interfaces.push({ name: interfaceMatch[1], line: lineNum });
56
+ const typeMatch = trimmed.match(/^(?:export\s+)?type\s+(\w+)/);
57
+ if (typeMatch?.[1])
58
+ interfaces.push({ name: typeMatch[1] + ' (type)', line: lineNum });
59
+ }
60
+ if (imports.length > 0) {
61
+ summary.push("### Imports");
62
+ summary.push(imports.slice(0, 10).join(', '));
63
+ if (imports.length > 10)
64
+ summary.push(`... +${imports.length - 10} more`);
65
+ summary.push("");
66
+ }
67
+ if (interfaces.length > 0) {
68
+ summary.push("### Interfaces/Types");
69
+ summary.push(interfaces.map(i => `${i.name}:${i.line}`).join(', '));
70
+ summary.push("");
71
+ }
72
+ if (classes.length > 0) {
73
+ summary.push("### Classes");
74
+ summary.push(classes.map(c => `${c.name}:${c.line}`).join(', '));
75
+ summary.push("");
76
+ }
77
+ if (functions.length > 0) {
78
+ summary.push("### Functions");
79
+ summary.push(functions.map(f => `${f.name}:${f.line}`).join(', '));
80
+ summary.push("");
81
+ }
82
+ if (exports.length > 0) {
83
+ summary.push("### Exports");
84
+ summary.push(exports.join(', '));
85
+ summary.push("");
86
+ }
87
+ }
88
+ if (fileExt === 'py') {
89
+ const functions = [];
90
+ const classes = [];
91
+ const imports = [];
92
+ for (let i = 0; i < lines.length; i++) {
93
+ const line = lines[i] ?? '';
94
+ const trimmed = line.trim();
95
+ const lineNum = i + 1;
96
+ const importMatch = trimmed.match(/^(?:from\s+(\S+)\s+)?import\s+(\S+)/);
97
+ if (importMatch?.[1] && importMatch[2])
98
+ imports.push(importMatch[1] + '.' + importMatch[2]);
99
+ if (importMatch?.[1] === undefined && importMatch?.[2])
100
+ imports.push(importMatch[2]);
101
+ const funcMatch = trimmed.match(/^(?:def\s+)(\w+)/);
102
+ if (funcMatch?.[1] && !trimmed.startsWith('#'))
103
+ functions.push({ name: funcMatch[1], line: lineNum });
104
+ const classMatch = trimmed.match(/^class\s+(\w+)/);
105
+ if (classMatch?.[1])
106
+ classes.push({ name: classMatch[1], line: lineNum });
107
+ }
108
+ if (imports.length > 0) {
109
+ summary.push("### Imports");
110
+ summary.push(imports.slice(0, 10).join(', '));
111
+ summary.push("");
112
+ }
113
+ if (classes.length > 0) {
114
+ summary.push("### Classes");
115
+ summary.push(classes.map(c => `${c.name}:${c.line}`).join(', '));
116
+ summary.push("");
117
+ }
118
+ if (functions.length > 0) {
119
+ summary.push("### Functions");
120
+ summary.push(functions.map(f => `${f.name}:${f.line}`).join(', '));
121
+ summary.push("");
122
+ }
123
+ }
124
+ if (fileExt === 'md') {
125
+ const headings = [];
126
+ for (const line of lines) {
127
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
128
+ if (headingMatch?.[1] && headingMatch[2])
129
+ headings.push({ text: headingMatch[2], level: headingMatch[1].length });
130
+ }
131
+ if (headings.length > 0) {
132
+ summary.push("### Structure");
133
+ summary.push(headings.map(h => ' '.repeat(h.level - 1) + h.text).join('\n'));
134
+ summary.push("");
135
+ }
136
+ }
137
+ const preview = lines.slice(0, 5).filter((l) => l.trim()).join('\n');
138
+ if (preview) {
139
+ summary.push("### Preview");
140
+ summary.push("```\n" + preview + "\n```");
141
+ }
142
+ return summary.join('\n');
143
+ }
@@ -0,0 +1,28 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import yaml from "js-yaml";
4
+ export function splitFrontmatter(text) {
5
+ if (!text.startsWith("---\n")) {
6
+ return { frontmatterText: undefined, body: text };
7
+ }
8
+ const endIndex = text.indexOf("\n---\n", 4);
9
+ if (endIndex === -1) {
10
+ return { frontmatterText: undefined, body: text };
11
+ }
12
+ const frontmatterText = text.slice(4, endIndex);
13
+ const body = text.slice(endIndex + "\n---\n".length);
14
+ return { frontmatterText, body };
15
+ }
16
+ export function buildFrontmatterDocument(frontmatter, body) {
17
+ const frontmatterYaml = yaml.dump(frontmatter, {
18
+ lineWidth: 120,
19
+ noRefs: true,
20
+ sortKeys: true,
21
+ });
22
+ return `---\n${frontmatterYaml}---\n${body.trim()}\n`;
23
+ }
24
+ export async function atomicWriteFile(filePath, content) {
25
+ const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.tmp`);
26
+ await fs.writeFile(tempPath, content, "utf-8");
27
+ await fs.rename(tempPath, filePath);
28
+ }