total-recall-brain 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/antigravity.mjs +212 -0
- package/bin/init.mjs +36 -0
- package/bin/oci-sniper.sh +69 -0
- package/bin/total-recall.mjs +229 -0
- package/docs/ARCHITECTURE.md +251 -0
- package/docs/IDE_INTEGRATION.md +112 -0
- package/package.json +71 -0
- package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
- package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
- package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
- package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
- package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
- package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
- package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
- package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
- package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
- package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
- package/src/cli/agent-dir.mjs +105 -0
- package/src/cli/backup.mjs +313 -0
- package/src/cli/brain.mjs +224 -0
- package/src/cli/chat.mjs +74 -0
- package/src/cli/config.mjs +247 -0
- package/src/cli/connect.mjs +872 -0
- package/src/cli/daemon.mjs +228 -0
- package/src/cli/deploy-ui.mjs +1793 -0
- package/src/cli/deploy.mjs +1726 -0
- package/src/cli/doctor.mjs +254 -0
- package/src/cli/dream.mjs +80 -0
- package/src/cli/forget.mjs +92 -0
- package/src/cli/friction.mjs +21 -0
- package/src/cli/generate-pat.mjs +65 -0
- package/src/cli/hash-password.mjs +22 -0
- package/src/cli/help.mjs +153 -0
- package/src/cli/import-rules.mjs +102 -0
- package/src/cli/ingest/index.mjs +318 -0
- package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
- package/src/cli/ingest/parsers/google-keep.mjs +147 -0
- package/src/cli/ingest/parsers/search-history.mjs +142 -0
- package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
- package/src/cli/ingest/utils/dedup.mjs +69 -0
- package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
- package/src/cli/ingest.mjs +102 -0
- package/src/cli/init.mjs +736 -0
- package/src/cli/integration-dispatcher.mjs +128 -0
- package/src/cli/lint.mjs +166 -0
- package/src/cli/map.mjs +194 -0
- package/src/cli/migrate.mjs +87 -0
- package/src/cli/rebuild.mjs +88 -0
- package/src/cli/recall.mjs +219 -0
- package/src/cli/relay.mjs +518 -0
- package/src/cli/remember.mjs +306 -0
- package/src/cli/research.mjs +381 -0
- package/src/cli/reset-password.mjs +75 -0
- package/src/cli/restore.mjs +135 -0
- package/src/cli/setup.mjs +616 -0
- package/src/cli/share.mjs +180 -0
- package/src/cli/skill.mjs +313 -0
- package/src/cli/snapshot.mjs +108 -0
- package/src/cli/start.mjs +63 -0
- package/src/cli/status.mjs +296 -0
- package/src/cli/sync.mjs +383 -0
- package/src/cli/uninstall.mjs +613 -0
- package/src/cli/upgrade.mjs +141 -0
- package/src/cli/wizard.html +3682 -0
- package/src/core/blackboard.mjs +54 -0
- package/src/core/clarity-rewriter.mjs +770 -0
- package/src/core/conclusion-writer.mjs +259 -0
- package/src/core/config.mjs +277 -0
- package/src/core/conflict-detector.mjs +536 -0
- package/src/core/crypto.mjs +107 -0
- package/src/core/daemon-control.mjs +158 -0
- package/src/core/daemon-loop.mjs +364 -0
- package/src/core/dream.mjs +347 -0
- package/src/core/drift-detector.mjs +143 -0
- package/src/core/embeddings.mjs +632 -0
- package/src/core/emergency-alerts.mjs +137 -0
- package/src/core/evolution.mjs +107 -0
- package/src/core/fact-seeker.mjs +1602 -0
- package/src/core/friction.mjs +95 -0
- package/src/core/import-rules.mjs +308 -0
- package/src/core/inference-engine.mjs +367 -0
- package/src/core/logger.mjs +75 -0
- package/src/core/memory-layers.mjs +96 -0
- package/src/core/migrate.mjs +116 -0
- package/src/core/notifications.mjs +77 -0
- package/src/core/operation-validator.mjs +272 -0
- package/src/core/optimizer.mjs +330 -0
- package/src/core/pattern_detector.mjs +113 -0
- package/src/core/post-mortem.mjs +413 -0
- package/src/core/protect-instructions.mjs +148 -0
- package/src/core/quick-capture.mjs +145 -0
- package/src/core/research-queue.mjs +431 -0
- package/src/core/research.mjs +461 -0
- package/src/core/runtime.mjs +515 -0
- package/src/core/sandbox.mjs +163 -0
- package/src/core/scheduler.mjs +569 -0
- package/src/core/schema.mjs +333 -0
- package/src/core/search.mjs +219 -0
- package/src/core/semantic-index.mjs +276 -0
- package/src/core/session-watcher.mjs +850 -0
- package/src/core/snapshot.mjs +133 -0
- package/src/core/source-adapters.mjs +1038 -0
- package/src/core/steering.mjs +137 -0
- package/src/core/surface.mjs +444 -0
- package/src/core/task_runner.mjs +125 -0
- package/src/core/tts.mjs +142 -0
- package/src/core/usage-tracker.mjs +349 -0
- package/src/core/vault-cache.mjs +76 -0
- package/src/core/vault.mjs +273 -0
- package/src/core/watchdog.mjs +236 -0
- package/src/server/api.mjs +944 -0
- package/src/server/auth.mjs +410 -0
- package/src/server/index.mjs +868 -0
- package/src/server/keys.mjs +181 -0
- package/src/server/rest.mjs +1652 -0
- package/src/server/route-manifest.json +77 -0
- package/src/server/routes/_shared.mjs +93 -0
- package/src/server/routes/keys.mjs +55 -0
- package/src/server/routes/memory.mjs +249 -0
- package/src/server/routes/sessions.mjs +336 -0
- package/src/server/routes/share.mjs +138 -0
- package/src/server/tools.mjs +618 -0
- package/templates/Caddyfile +37 -0
- package/templates/com.totalrecall.backup.plist +42 -0
- package/templates/com.totalrecall.daemon.plist +40 -0
- package/templates/com.totalrecall.duckdns.plist +23 -0
- package/templates/com.totalrecall.server.plist +46 -0
- package/templates/default-config/budget.yml +6 -0
- package/templates/default-config/runtime.yml +11 -0
- package/templates/default-config/security.yml +53 -0
- package/templates/default-config/voice.yml +34 -0
- package/templates/obsidian-queries/conflicts.md +26 -0
- package/templates/obsidian-queries/daily.md +17 -0
- package/templates/obsidian-queries/index.md +38 -0
- package/templates/obsidian-queries/skills.md +14 -0
- package/templates/onboarding-interview.md +72 -0
- package/templates/skill_engineering.md +40 -0
- package/templates/total-recall-daemon.service +21 -0
- package/templates/total-recall-server.service +20 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
import { logger } from './logger.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Total Recall Friction Detection
|
|
7
|
+
* Analyzes structured JSONL logs to identify workflow bottlenecks, high failure rates,
|
|
8
|
+
* and performance regressions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export async function detectFriction(logFilePath) {
|
|
12
|
+
if (!fs.existsSync(logFilePath)) {
|
|
13
|
+
logger.warn('friction', `Log file not found at ${logFilePath}`);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const fileStream = fs.createReadStream(logFilePath);
|
|
18
|
+
const rl = readline.createInterface({
|
|
19
|
+
input: fileStream,
|
|
20
|
+
crlfDelay: Infinity
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const stats = {
|
|
24
|
+
totalEvents: 0,
|
|
25
|
+
errors: 0,
|
|
26
|
+
taskFailures: {},
|
|
27
|
+
latencySum: 0,
|
|
28
|
+
latencyCount: 0,
|
|
29
|
+
slowestTasks: [],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for await (const line of rl) {
|
|
33
|
+
if (!line.trim()) continue;
|
|
34
|
+
try {
|
|
35
|
+
const entry = JSON.parse(line);
|
|
36
|
+
stats.totalEvents++;
|
|
37
|
+
|
|
38
|
+
if (entry.level === 'error' || entry.level === 'fatal') {
|
|
39
|
+
stats.errors++;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (entry.event === 'task_failed' && entry.taskId) {
|
|
43
|
+
stats.taskFailures[entry.taskId] = (stats.taskFailures[entry.taskId] || 0) + 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (entry.durationMs) {
|
|
47
|
+
stats.latencySum += entry.durationMs;
|
|
48
|
+
stats.latencyCount++;
|
|
49
|
+
|
|
50
|
+
if (entry.durationMs > 5000) { // arbitrary 5s threshold for "slow"
|
|
51
|
+
stats.slowestTasks.push({ task: entry.taskId || entry.event, duration: entry.durationMs });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// Ignore invalid JSON lines
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
stats.averageLatency = stats.latencyCount > 0 ? stats.latencySum / stats.latencyCount : 0;
|
|
60
|
+
|
|
61
|
+
// Sort slowest tasks descending
|
|
62
|
+
stats.slowestTasks.sort((a, b) => b.duration - a.duration);
|
|
63
|
+
stats.slowestTasks = stats.slowestTasks.slice(0, 10); // keep top 10
|
|
64
|
+
|
|
65
|
+
const report = generateFrictionReport(stats);
|
|
66
|
+
return { stats, report };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function generateFrictionReport(stats) {
|
|
70
|
+
let report = `### Friction Detection Report\n`;
|
|
71
|
+
report += `- **Total Events Analyzed**: ${stats.totalEvents}\n`;
|
|
72
|
+
report += `- **Error Count**: ${stats.errors}\n`;
|
|
73
|
+
report += `- **Average Latency**: ${stats.averageLatency.toFixed(2)} ms\n\n`;
|
|
74
|
+
|
|
75
|
+
const frequentFailures = Object.entries(stats.taskFailures)
|
|
76
|
+
.sort((a, b) => b[1] - a[1])
|
|
77
|
+
.slice(0, 5);
|
|
78
|
+
|
|
79
|
+
if (frequentFailures.length > 0) {
|
|
80
|
+
report += `#### Bottleneck: High Failure Rates\n`;
|
|
81
|
+
frequentFailures.forEach(([taskId, count]) => {
|
|
82
|
+
report += `- Task \`${taskId}\` failed ${count} times.\n`;
|
|
83
|
+
});
|
|
84
|
+
report += `\n`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (stats.slowestTasks.length > 0) {
|
|
88
|
+
report += `#### Bottleneck: Slow Executions\n`;
|
|
89
|
+
stats.slowestTasks.slice(0, 5).forEach(t => {
|
|
90
|
+
report += `- \`${t.task}\` took ${t.duration} ms.\n`;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return report;
|
|
95
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/import-rules.mjs
|
|
3
|
+
*
|
|
4
|
+
* Detect and import existing agent rule files into the Total Recall vault.
|
|
5
|
+
*
|
|
6
|
+
* New users often have AGENTS.md, .cursorrules, CLAUDE.md, etc. already full
|
|
7
|
+
* of their own rules. This module reads those files and writes each as a vault
|
|
8
|
+
* memory node so 'compile' can re-emit them as part of the unified surface.
|
|
9
|
+
*
|
|
10
|
+
* Flow: detect → preview → confirm → writeNode → return results
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
import { writeNode, loadNodes } from './vault.mjs';
|
|
18
|
+
import { agentDir, brainDir } from './config.mjs';
|
|
19
|
+
|
|
20
|
+
const AGENT_DIR = agentDir;
|
|
21
|
+
const VAULT_DIR = path.join(brainDir, 'memory-vault');
|
|
22
|
+
|
|
23
|
+
// ── Candidate rule files we look for ─────────────────────────────────────────
|
|
24
|
+
// Checked relative to the given directory (usually cwd or home).
|
|
25
|
+
|
|
26
|
+
const RULE_FILE_CANDIDATES = [
|
|
27
|
+
{ filename: 'AGENTS.md', label: 'Generic agent rules', ide: 'agents' },
|
|
28
|
+
{ filename: 'AGENTS.override.md', label: 'Codex instructions override', ide: 'agents' },
|
|
29
|
+
{ filename: '.codex/AGENTS.md', label: 'Codex global rules', ide: 'agents' },
|
|
30
|
+
{ filename: '.codex/AGENTS.override.md', label: 'Codex global instructions override', ide: 'agents' },
|
|
31
|
+
{ filename: 'CLAUDE.md', label: 'Claude Code rules', ide: 'claude' },
|
|
32
|
+
{ filename: '.claude/CLAUDE.md',label: 'Claude Code rules (alt)', ide: 'claude' },
|
|
33
|
+
{ filename: 'GEMINI.md', label: 'Gemini CLI rules', ide: 'gemini' },
|
|
34
|
+
{ filename: '.gemini/GEMINI.md',label: 'Gemini global rules', ide: 'gemini' },
|
|
35
|
+
{ filename: '.gemini/system.md',label: 'Gemini system instructions', ide: 'gemini' },
|
|
36
|
+
{ filename: 'system.md', label: 'Project system instructions', ide: 'gemini' },
|
|
37
|
+
{ filename: '.cursorrules', label: 'Cursor rules', ide: 'cursor' },
|
|
38
|
+
{ filename: '.clauderules', label: 'Claude rules', ide: 'claude' },
|
|
39
|
+
{ filename: '.windsurfrules', label: 'Windsurf rules', ide: 'windsurf' },
|
|
40
|
+
{ filename: '.aiderules', label: 'Aider rules', ide: 'aider' },
|
|
41
|
+
{ filename: 'WINDSURF.md', label: 'Windsurf rules (md)', ide: 'windsurf' },
|
|
42
|
+
{ filename: 'COPILOT.md', label: 'GitHub Copilot rules', ide: 'copilot' },
|
|
43
|
+
{ filename: '.github/copilot-instructions.md', label: 'Copilot instructions', ide: 'copilot' },
|
|
44
|
+
{ filename: '.vscode/copilot-instructions.md', label: 'VS Code Copilot instructions', ide: 'copilot' },
|
|
45
|
+
{ filename: 'INSTRUCTIONS.md', label: 'Custom instructions', ide: 'generic' },
|
|
46
|
+
{ filename: '.rules', label: 'Generic rules', ide: 'generic' },
|
|
47
|
+
{ filename: 'RULES.md', label: 'Generic rules (md)', ide: 'generic' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Candidates for directories containing modular rule/prompt files
|
|
51
|
+
const RULE_DIRECTORY_CANDIDATES = [
|
|
52
|
+
{ dirPath: '.claude/rules', ext: '.md', label: 'Claude modular rule', ide: 'claude' },
|
|
53
|
+
{ dirPath: '.cursor/rules', ext: '.mdc', label: 'Cursor modular rule', ide: 'cursor' },
|
|
54
|
+
{ dirPath: '.agent/skills', ext: '.md', label: 'Antigravity/Gemini skill', ide: 'gemini', recursive: true },
|
|
55
|
+
{ dirPath: '.vscode/prompts', ext: '.md', label: 'VS Code modular rule', ide: 'copilot' },
|
|
56
|
+
{ dirPath: '.windsurf/rules', ext: '.md', label: 'Windsurf modular rule', ide: 'windsurf' },
|
|
57
|
+
{ dirPath: '.codex/rules', ext: ['.md', '.rules'], label: 'Codex modular rule', ide: 'agents' },
|
|
58
|
+
{ dirPath: '.github/instructions', ext: '.md', label: 'VS Code Copilot workspace instruction', ide: 'copilot' },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// ── Detection ─────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Scan one or more directories for known rule files.
|
|
65
|
+
* Returns a list of detected files with metadata (path, size, preview).
|
|
66
|
+
*
|
|
67
|
+
* @param {string[]} dirs Directories to search (default: [cwd, homedir])
|
|
68
|
+
* @returns {{ filename, label, ide, absolutePath, size, preview, alreadyImported }[]}
|
|
69
|
+
*/
|
|
70
|
+
export function detectRuleFiles(dirs) {
|
|
71
|
+
const searchDirs = dirs?.length ? dirs : [process.cwd(), os.homedir()];
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
const detected = [];
|
|
74
|
+
|
|
75
|
+
// Load existing vault nodes to detect already-imported files
|
|
76
|
+
let existingSlugs = new Set();
|
|
77
|
+
try {
|
|
78
|
+
existingSlugs = new Set(loadNodes(VAULT_DIR).map(n => n.slug));
|
|
79
|
+
} catch { /* vault may not exist yet */ }
|
|
80
|
+
|
|
81
|
+
for (const dir of searchDirs) {
|
|
82
|
+
if (!fs.existsSync(dir)) continue;
|
|
83
|
+
|
|
84
|
+
// 1. Check individual file candidates
|
|
85
|
+
for (const candidate of RULE_FILE_CANDIDATES) {
|
|
86
|
+
const absPath = path.resolve(dir, candidate.filename);
|
|
87
|
+
if (seen.has(absPath)) continue;
|
|
88
|
+
if (!fs.existsSync(absPath)) continue;
|
|
89
|
+
seen.add(absPath);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const stat = fs.statSync(absPath);
|
|
93
|
+
if (!stat.isFile() || stat.size === 0) continue;
|
|
94
|
+
|
|
95
|
+
const content = fs.readFileSync(absPath, 'utf8').trim();
|
|
96
|
+
if (!content || content.startsWith('# Generated at runtime by: npx total-recall compile')) continue;
|
|
97
|
+
|
|
98
|
+
const slug = makeSlug(candidate.filename, absPath);
|
|
99
|
+
detected.push({
|
|
100
|
+
filename: candidate.filename,
|
|
101
|
+
label: candidate.label,
|
|
102
|
+
ide: candidate.ide,
|
|
103
|
+
absolutePath: absPath,
|
|
104
|
+
size: stat.size,
|
|
105
|
+
preview: content.slice(0, 300),
|
|
106
|
+
slug,
|
|
107
|
+
alreadyImported: existingSlugs.has(slug),
|
|
108
|
+
});
|
|
109
|
+
} catch { /* unreadable */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 2. Scan directories for modular rules
|
|
113
|
+
for (const dirCandidate of RULE_DIRECTORY_CANDIDATES) {
|
|
114
|
+
const absDirPath = path.resolve(dir, dirCandidate.dirPath);
|
|
115
|
+
if (!fs.existsSync(absDirPath)) continue;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const dirStat = fs.statSync(absDirPath);
|
|
119
|
+
if (!dirStat.isDirectory()) continue;
|
|
120
|
+
|
|
121
|
+
const scanDir = (currentPath) => {
|
|
122
|
+
if (!fs.existsSync(currentPath)) return;
|
|
123
|
+
const currentStat = fs.statSync(currentPath);
|
|
124
|
+
if (!currentStat.isDirectory()) return;
|
|
125
|
+
|
|
126
|
+
const entries = fs.readdirSync(currentPath);
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
const entryPath = path.join(currentPath, entry);
|
|
129
|
+
let entryStat;
|
|
130
|
+
try {
|
|
131
|
+
entryStat = fs.lstatSync(entryPath);
|
|
132
|
+
} catch {
|
|
133
|
+
continue; // Skip completely unreadable/broken entries
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (entryStat.isDirectory()) {
|
|
137
|
+
if (dirCandidate.recursive) {
|
|
138
|
+
scanDir(entryPath);
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check extensions
|
|
144
|
+
const extensions = Array.isArray(dirCandidate.ext) ? dirCandidate.ext : [dirCandidate.ext];
|
|
145
|
+
const hasMatchingExt = extensions.some(ext => entry.endsWith(ext) || entry === 'SKILL.md');
|
|
146
|
+
if (!hasMatchingExt) continue;
|
|
147
|
+
|
|
148
|
+
if (seen.has(entryPath)) continue;
|
|
149
|
+
seen.add(entryPath);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
let activePath = entryPath;
|
|
153
|
+
let fileStat;
|
|
154
|
+
try {
|
|
155
|
+
fileStat = fs.statSync(entryPath);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (entryStat.isSymbolicLink()) {
|
|
158
|
+
const target = fs.readlinkSync(entryPath);
|
|
159
|
+
const healedTarget = healSymlinkTarget(target);
|
|
160
|
+
if (fs.existsSync(healedTarget)) {
|
|
161
|
+
activePath = healedTarget;
|
|
162
|
+
fileStat = fs.statSync(healedTarget);
|
|
163
|
+
} else {
|
|
164
|
+
continue; // Skip broken symlink that couldn't be healed
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!fileStat.isFile() || fileStat.size === 0) continue;
|
|
172
|
+
|
|
173
|
+
const content = fs.readFileSync(activePath, 'utf8').trim();
|
|
174
|
+
if (!content || content.startsWith('# Generated at runtime by: npx total-recall compile')) continue;
|
|
175
|
+
|
|
176
|
+
const relFilename = path.relative(dir, entryPath);
|
|
177
|
+
const slug = makeSlug(relFilename, activePath);
|
|
178
|
+
detected.push({
|
|
179
|
+
filename: relFilename,
|
|
180
|
+
label: `${dirCandidate.label} (${entry})`,
|
|
181
|
+
ide: dirCandidate.ide,
|
|
182
|
+
absolutePath: activePath,
|
|
183
|
+
size: fileStat.size,
|
|
184
|
+
preview: content.slice(0, 300),
|
|
185
|
+
slug,
|
|
186
|
+
alreadyImported: existingSlugs.has(slug),
|
|
187
|
+
});
|
|
188
|
+
} catch { /* skip this specific unreadable file, keep scanning others */ }
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
scanDir(absDirPath);
|
|
193
|
+
} catch { /* unreadable or unlisting directory */ }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return detected;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Import ────────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Import a list of detected rule files into the vault as memory nodes.
|
|
204
|
+
* Skips files already imported unless force=true.
|
|
205
|
+
*
|
|
206
|
+
* @param {object[]} files Output of detectRuleFiles() (or a subset)
|
|
207
|
+
* @param {{ force?: boolean, vaultDir?: string }} opts
|
|
208
|
+
* @returns {{ imported: object[], skipped: object[], failed: object[] }}
|
|
209
|
+
*/
|
|
210
|
+
export function importRuleFiles(files, { force = false, vaultDir = VAULT_DIR } = {}) {
|
|
211
|
+
const imported = [], skipped = [], failed = [];
|
|
212
|
+
|
|
213
|
+
for (const file of files) {
|
|
214
|
+
if (file.alreadyImported && !force) {
|
|
215
|
+
skipped.push({ ...file, reason: 'already imported' });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const content = fs.readFileSync(file.absolutePath, 'utf8').trim();
|
|
221
|
+
if (!content) { skipped.push({ ...file, reason: 'empty' }); continue; }
|
|
222
|
+
|
|
223
|
+
const node = {
|
|
224
|
+
type: 'memory',
|
|
225
|
+
slug: file.slug,
|
|
226
|
+
title: `Imported rules: ${file.filename}`,
|
|
227
|
+
category: 'instructions',
|
|
228
|
+
status: 'active',
|
|
229
|
+
confidence: 1.0,
|
|
230
|
+
importance: 5,
|
|
231
|
+
tags: ['imported-rules', file.ide, path.basename(file.filename)],
|
|
232
|
+
created: new Date().toISOString(),
|
|
233
|
+
updated: new Date().toISOString(),
|
|
234
|
+
last_accessed: new Date().toISOString(),
|
|
235
|
+
source: {
|
|
236
|
+
type: 'import',
|
|
237
|
+
file: file.absolutePath,
|
|
238
|
+
imported_at: new Date().toISOString(),
|
|
239
|
+
},
|
|
240
|
+
body: content,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
writeNode(node, vaultDir);
|
|
244
|
+
imported.push({ slug: node.slug, title: node.title, file: file.absolutePath, size: file.size });
|
|
245
|
+
} catch (err) {
|
|
246
|
+
failed.push({ ...file, error: err.message });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { imported, skipped, failed };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* One-shot: detect + import. Returns full result summary.
|
|
255
|
+
*
|
|
256
|
+
* @param {{ dirs?: string[], force?: boolean, vaultDir?: string, dryRun?: boolean }} opts
|
|
257
|
+
*/
|
|
258
|
+
export function detectAndImport({ dirs, force = false, vaultDir = VAULT_DIR, dryRun = false } = {}) {
|
|
259
|
+
const detected = detectRuleFiles(dirs);
|
|
260
|
+
const toImport = detected.filter(f => !f.alreadyImported || force);
|
|
261
|
+
|
|
262
|
+
if (dryRun) {
|
|
263
|
+
return { detected, toImport, imported: [], skipped: detected.filter(f => f.alreadyImported && !force), failed: [], dryRun: true };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const { imported, skipped, failed } = importRuleFiles(toImport, { force, vaultDir });
|
|
267
|
+
return { detected, imported, skipped, failed, dryRun: false };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
function makeSlug(filename, absPath) {
|
|
273
|
+
const base = filename.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase();
|
|
274
|
+
// Include a short hash of the path so two files with the same name in different dirs don't collide
|
|
275
|
+
const hash = crypto.createHash('sha1').update(absPath).digest('hex').slice(0, 6);
|
|
276
|
+
return `imported-rules-${base}-${hash}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function healSymlinkTarget(target) {
|
|
280
|
+
let healed = target.replace('/ultrachat/ultrachat-ai-powered/', '/ultrachat-ai-powered/');
|
|
281
|
+
if (fs.existsSync(healed)) return healed;
|
|
282
|
+
|
|
283
|
+
const skillMatch = healed.match(/\/(\.agents?)\/skills\/([^\/]+)\/(SKILL\.md)$/i);
|
|
284
|
+
if (skillMatch) {
|
|
285
|
+
const agentDirName = skillMatch[1];
|
|
286
|
+
const skillName = skillMatch[2];
|
|
287
|
+
const skillFile = skillMatch[3];
|
|
288
|
+
|
|
289
|
+
const skillsDir = healed.substring(0, healed.indexOf(`/${agentDirName}/skills/`) + `/${agentDirName}/skills/`.length);
|
|
290
|
+
if (fs.existsSync(skillsDir)) {
|
|
291
|
+
try {
|
|
292
|
+
const dirs = fs.readdirSync(skillsDir);
|
|
293
|
+
const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
294
|
+
const targetNorm = normalize(skillName);
|
|
295
|
+
for (const d of dirs) {
|
|
296
|
+
if (normalize(d) === targetNorm) {
|
|
297
|
+
const possiblePath = path.join(skillsDir, d, skillFile);
|
|
298
|
+
if (fs.existsSync(possiblePath)) {
|
|
299
|
+
return possiblePath;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch { /* ignore */ }
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return healed;
|
|
308
|
+
}
|