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,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/cli/import-rules.mjs
|
|
3
|
+
*
|
|
4
|
+
* CLI handler for: npx total-recall import [--dir <path>] [--force] [--dry-run]
|
|
5
|
+
*
|
|
6
|
+
* Detects existing agent rule files and imports them into the vault.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { detectAndImport } from '../core/import-rules.mjs';
|
|
11
|
+
import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
|
|
12
|
+
|
|
13
|
+
export async function run(argv = []) {
|
|
14
|
+
const args = argv.slice(3); // strip node, script, 'import'
|
|
15
|
+
|
|
16
|
+
const dirs = [];
|
|
17
|
+
let force = false;
|
|
18
|
+
let dryRun = false;
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < args.length; i++) {
|
|
21
|
+
if ((args[i] === '--dir' || args[i] === '-d') && args[i + 1]) {
|
|
22
|
+
dirs.push(path.resolve(args[++i]));
|
|
23
|
+
} else if (args[i] === '--force' || args[i] === '-f') {
|
|
24
|
+
force = true;
|
|
25
|
+
} else if (args[i] === '--dry-run') {
|
|
26
|
+
dryRun = true;
|
|
27
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
28
|
+
printHelp();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Default: search cwd
|
|
34
|
+
if (dirs.length === 0) dirs.push(process.cwd());
|
|
35
|
+
|
|
36
|
+
const brainDir = resolveBrainDir();
|
|
37
|
+
const vaultDir = path.join(brainDir, 'memory-vault');
|
|
38
|
+
|
|
39
|
+
console.log(`\n🔍 Scanning for rule files in: ${dirs.join(', ')}\n`);
|
|
40
|
+
|
|
41
|
+
const result = detectAndImport({ dirs, force, vaultDir, dryRun });
|
|
42
|
+
|
|
43
|
+
if (result.detected.length === 0) {
|
|
44
|
+
console.log('ℹ️ No rule files detected. Nothing to import.');
|
|
45
|
+
console.log(' (Looked for: AGENTS.md, CLAUDE.md, .cursorrules, .clauderules, etc.)\n');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`📋 Detected ${result.detected.length} rule file(s):\n`);
|
|
50
|
+
for (const f of result.detected) {
|
|
51
|
+
const status = f.alreadyImported ? ' [already imported]' : '';
|
|
52
|
+
console.log(` ${f.alreadyImported ? '⚪' : '✅'} ${f.filename.padEnd(30)} ${f.label}${status}`);
|
|
53
|
+
console.log(` ${f.absolutePath}`);
|
|
54
|
+
console.log(` ${(f.size / 1024).toFixed(1)}KB — preview: ${f.preview.slice(0, 80).replace(/\n/g, ' ')}…\n`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (dryRun) {
|
|
58
|
+
console.log(`🔎 Dry run — would import ${result.toImport.length} file(s). Run without --dry-run to proceed.\n`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (result.imported.length > 0) {
|
|
63
|
+
console.log(`\n✅ Imported ${result.imported.length} file(s) into vault:\n`);
|
|
64
|
+
for (const n of result.imported) {
|
|
65
|
+
console.log(` → ${n.slug} (${(n.size / 1024).toFixed(1)}KB)`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (result.skipped.length > 0) {
|
|
70
|
+
console.log(`\n⚪ Skipped ${result.skipped.length} (already imported — use --force to re-import)`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (result.failed.length > 0) {
|
|
74
|
+
console.log(`\n❌ Failed ${result.failed.length}:`);
|
|
75
|
+
for (const f of result.failed) console.log(` ${f.filename}: ${f.error}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (result.imported.length > 0) {
|
|
79
|
+
console.log('\n💡 Run npx total-recall compile to rebuild INSTRUCTIONS.md with your imported rules.\n');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function printHelp() {
|
|
84
|
+
console.log(`
|
|
85
|
+
npx total-recall import [options]
|
|
86
|
+
|
|
87
|
+
Detects existing agent rule files (AGENTS.md, .cursorrules, CLAUDE.md, etc.)
|
|
88
|
+
and imports them into your Total Recall memory vault so they are included in
|
|
89
|
+
all future AI sessions.
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
--dir <path> Directory to search (default: current directory)
|
|
93
|
+
--force Re-import files that were previously imported
|
|
94
|
+
--dry-run Show what would be imported without writing anything
|
|
95
|
+
--help Show this help
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
npx total-recall import
|
|
99
|
+
npx total-recall import --dir ~/my-project
|
|
100
|
+
npx total-recall import --force --dry-run
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Takeout Ingestion — CLI entry point
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the ingestion of Google Takeout export data into the
|
|
5
|
+
* Total Recall memory vault. Walks the Takeout directory, detects
|
|
6
|
+
* available data types, runs parsers, deduplicates, and writes nodes.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx total-recall ingest google-takeout <path> [options]
|
|
10
|
+
*
|
|
11
|
+
* Options:
|
|
12
|
+
* --dry-run Preview what would be ingested without writing
|
|
13
|
+
* --types <comma-list> Only process specified data types
|
|
14
|
+
* --brain <id> Target brain ID (default: auto-detect)
|
|
15
|
+
* --max-age <duration> Only include entries newer than duration (e.g. 90d, 1y)
|
|
16
|
+
* --help, -h Show help
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { walkTakeoutDir } from './utils/takeout-walker.mjs';
|
|
22
|
+
import { dedup } from './utils/dedup.mjs';
|
|
23
|
+
import { parseSearchHistory } from './parsers/search-history.mjs';
|
|
24
|
+
import { parseChromeBookmarks } from './parsers/chrome-bookmarks.mjs';
|
|
25
|
+
import { parseGoogleKeep } from './parsers/google-keep.mjs';
|
|
26
|
+
import { parseYoutubeHistory } from './parsers/youtube-history.mjs';
|
|
27
|
+
import { writeNode, loadNodes } from '../../core/vault.mjs';
|
|
28
|
+
import { resolveBrainDir, parseLayerFlag, getBothBrains, defaultLayerForCategory } from '../../cli/agent-dir.mjs';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parser registry: maps data type names to their directory detection
|
|
32
|
+
* signatures and parser functions.
|
|
33
|
+
*/
|
|
34
|
+
const PARSERS = {
|
|
35
|
+
'search-history': { detect: 'My Activity/Search', parser: parseSearchHistory },
|
|
36
|
+
'chrome-bookmarks': { detect: 'Chrome/Bookmarks', parser: parseChromeBookmarks },
|
|
37
|
+
'google-keep': { detect: 'Keep', parser: parseGoogleKeep },
|
|
38
|
+
'youtube-history': { detect: 'YouTube and YouTube Music/history', parser: parseYoutubeHistory },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse a human-readable duration string into a Date threshold.
|
|
43
|
+
* Supports: 30d, 90d, 1y, 6m, etc.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} duration
|
|
46
|
+
* @returns {Date}
|
|
47
|
+
*/
|
|
48
|
+
function parseDuration(duration) {
|
|
49
|
+
const match = duration.match(/^(\d+)\s*(d|m|y|w)$/i);
|
|
50
|
+
if (!match) throw new Error(`Invalid duration format: "${duration}". Use e.g. 30d, 6m, 1y`);
|
|
51
|
+
|
|
52
|
+
const value = parseInt(match[1], 10);
|
|
53
|
+
const unit = match[2].toLowerCase();
|
|
54
|
+
const now = new Date();
|
|
55
|
+
|
|
56
|
+
switch (unit) {
|
|
57
|
+
case 'd': now.setDate(now.getDate() - value); break;
|
|
58
|
+
case 'w': now.setDate(now.getDate() - (value * 7)); break;
|
|
59
|
+
case 'm': now.setMonth(now.getMonth() - value); break;
|
|
60
|
+
case 'y': now.setFullYear(now.getFullYear() - value); break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return now;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse CLI arguments for the google-takeout ingest command.
|
|
68
|
+
*
|
|
69
|
+
* @param {string[]} args — CLI arguments after 'ingest google-takeout'.
|
|
70
|
+
* @returns {{ takeoutPath: string|null, dryRun: boolean, types: string[]|null, brain: string|null, maxAge: Date|null, help: boolean, layer: string }}
|
|
71
|
+
*/
|
|
72
|
+
function parseArgs(args) {
|
|
73
|
+
const { layer, remainingArgs } = parseLayerFlag(args);
|
|
74
|
+
const opts = { takeoutPath: null, dryRun: false, types: null, brain: null, maxAge: null, help: false, layer };
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < remainingArgs.length; i++) {
|
|
77
|
+
const arg = remainingArgs[i];
|
|
78
|
+
const val = remainingArgs[i + 1];
|
|
79
|
+
|
|
80
|
+
switch (arg) {
|
|
81
|
+
case '--dry-run':
|
|
82
|
+
opts.dryRun = true;
|
|
83
|
+
break;
|
|
84
|
+
case '--types':
|
|
85
|
+
if (val) { opts.types = val.split(',').map(t => t.trim()); i++; }
|
|
86
|
+
break;
|
|
87
|
+
case '--brain':
|
|
88
|
+
if (val) { opts.brain = val; i++; }
|
|
89
|
+
break;
|
|
90
|
+
case '--max-age':
|
|
91
|
+
if (val) { opts.maxAge = parseDuration(val); i++; }
|
|
92
|
+
break;
|
|
93
|
+
case '--help': case '-h':
|
|
94
|
+
opts.help = true;
|
|
95
|
+
break;
|
|
96
|
+
default:
|
|
97
|
+
if (!arg.startsWith('-') && !opts.takeoutPath) {
|
|
98
|
+
opts.takeoutPath = arg;
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return opts;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Print help text for the google-takeout ingest command.
|
|
109
|
+
*/
|
|
110
|
+
function printHelp() {
|
|
111
|
+
console.log(`
|
|
112
|
+
total-recall ingest google-takeout — Ingest Google Takeout data
|
|
113
|
+
|
|
114
|
+
Walks a Google Takeout export directory, detects available data types,
|
|
115
|
+
parses them into SSSS memory nodes, deduplicates against the existing
|
|
116
|
+
vault, and writes new nodes.
|
|
117
|
+
|
|
118
|
+
Usage: total-recall ingest google-takeout <path> [options]
|
|
119
|
+
|
|
120
|
+
Supported data types:
|
|
121
|
+
search-history Google Search history (My Activity/Search/)
|
|
122
|
+
chrome-bookmarks Chrome bookmarks (Chrome/Bookmarks.html or .json)
|
|
123
|
+
google-keep Google Keep notes (Keep/*.json)
|
|
124
|
+
youtube-history YouTube watch history (YouTube and YouTube Music/history/)
|
|
125
|
+
|
|
126
|
+
Options:
|
|
127
|
+
--dry-run Preview what would be ingested without writing
|
|
128
|
+
--types <comma-list> Only process specified types (e.g. "search-history,google-keep")
|
|
129
|
+
--brain <id> Target brain ID
|
|
130
|
+
--max-age <duration> Only include entries newer than duration (30d, 6m, 1y)
|
|
131
|
+
--global Write to global brain
|
|
132
|
+
--project Write to project brain
|
|
133
|
+
--help, -h Show this help
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
npx total-recall ingest google-takeout ~/Downloads/Takeout
|
|
137
|
+
npx total-recall ingest google-takeout ~/Downloads/Takeout --dry-run
|
|
138
|
+
npx total-recall ingest google-takeout ~/Downloads/Takeout --types search-history,google-keep
|
|
139
|
+
npx total-recall ingest google-takeout ~/Downloads/Takeout --max-age 90d
|
|
140
|
+
`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Main ingestion function for Google Takeout data.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} takeoutPath — Path to the Takeout export directory.
|
|
147
|
+
* @param {object} options
|
|
148
|
+
* @param {boolean} [options.dryRun=false] — Preview mode; don't write nodes.
|
|
149
|
+
* @param {string[]|null} [options.types=null] — Filter to specific data types.
|
|
150
|
+
* @param {string|null} [options.brain=null] — Target brain ID.
|
|
151
|
+
* @param {Date|null} [options.maxAge=null] — Only include entries newer than this.
|
|
152
|
+
* @param {string} [options.layer='auto'] — Brain layer target.
|
|
153
|
+
* @returns {Promise<{ detected: number, parsed: number, deduplicated: number, written: number, stats: object }>}
|
|
154
|
+
*/
|
|
155
|
+
export async function ingestGoogleTakeout(takeoutPath, options = {}) {
|
|
156
|
+
const { dryRun = false, types = null, maxAge = null, layer = 'auto' } = options;
|
|
157
|
+
|
|
158
|
+
// Validate path exists
|
|
159
|
+
const resolvedPath = path.resolve(takeoutPath);
|
|
160
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
161
|
+
throw new Error(`Takeout directory not found: ${resolvedPath}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Resolve brain target
|
|
165
|
+
let effectiveLayer = layer;
|
|
166
|
+
if (effectiveLayer === 'auto') {
|
|
167
|
+
effectiveLayer = defaultLayerForCategory('facts');
|
|
168
|
+
const project = getBothBrains().project;
|
|
169
|
+
if (effectiveLayer === 'project' && !project) {
|
|
170
|
+
effectiveLayer = 'global';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const brainDir = resolveBrainDir(effectiveLayer);
|
|
175
|
+
const vaultDir = path.join(brainDir, 'memory-vault');
|
|
176
|
+
|
|
177
|
+
// Walk directory to detect available data types
|
|
178
|
+
console.error('\n 🔍 Scanning Takeout directory...\n');
|
|
179
|
+
const detected = walkTakeoutDir(resolvedPath);
|
|
180
|
+
|
|
181
|
+
if (detected.length === 0) {
|
|
182
|
+
console.error(' ⚠️ No recognized Takeout data types found.\n');
|
|
183
|
+
return { detected: 0, parsed: 0, deduplicated: 0, written: 0, stats: {} };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Filter by requested types
|
|
187
|
+
const toProcess = types
|
|
188
|
+
? detected.filter(d => types.includes(d.type))
|
|
189
|
+
: detected;
|
|
190
|
+
|
|
191
|
+
if (toProcess.length === 0) {
|
|
192
|
+
console.error(' ⚠️ No matching data types found for the specified filter.\n');
|
|
193
|
+
return { detected: detected.length, parsed: 0, deduplicated: 0, written: 0, stats: {} };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.error(` 📦 Detected ${detected.length} data type(s):`);
|
|
197
|
+
for (const d of detected) {
|
|
198
|
+
const included = toProcess.some(t => t.type === d.type);
|
|
199
|
+
const icon = included ? '🟢' : '⚪';
|
|
200
|
+
console.error(` ${icon} ${d.type} → ${d.path}`);
|
|
201
|
+
}
|
|
202
|
+
console.error('');
|
|
203
|
+
|
|
204
|
+
// Load existing vault nodes for dedup
|
|
205
|
+
let existingNodes = [];
|
|
206
|
+
if (fs.existsSync(vaultDir)) {
|
|
207
|
+
existingNodes = loadNodes(vaultDir);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Run parsers for each detected type
|
|
211
|
+
const stats = {};
|
|
212
|
+
let allNewNodes = [];
|
|
213
|
+
|
|
214
|
+
for (const source of toProcess) {
|
|
215
|
+
const parserEntry = PARSERS[source.type];
|
|
216
|
+
if (!parserEntry) {
|
|
217
|
+
stats[source.type] = { parsed: 0, error: 'No parser available' };
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.error(` ⏳ Parsing ${source.type}...`);
|
|
222
|
+
try {
|
|
223
|
+
const parserOpts = maxAge ? { maxAge } : {};
|
|
224
|
+
const nodes = parserEntry.parser(source.path, parserOpts);
|
|
225
|
+
stats[source.type] = { parsed: nodes.length };
|
|
226
|
+
allNewNodes.push(...nodes);
|
|
227
|
+
console.error(` ✅ ${nodes.length} entries parsed`);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
stats[source.type] = { parsed: 0, error: err.message };
|
|
230
|
+
console.error(` ❌ Error: ${err.message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Dedup against existing vault
|
|
235
|
+
console.error('\n 🔄 Deduplicating against existing vault...');
|
|
236
|
+
const { unique, duplicateCount } = dedup(allNewNodes, existingNodes);
|
|
237
|
+
console.error(` Found ${duplicateCount} duplicate(s), ${unique.length} new node(s)\n`);
|
|
238
|
+
|
|
239
|
+
// Write nodes
|
|
240
|
+
let written = 0;
|
|
241
|
+
if (dryRun) {
|
|
242
|
+
console.error(' 🏷️ DRY RUN — No nodes written.\n');
|
|
243
|
+
} else {
|
|
244
|
+
// Ensure vault directory exists
|
|
245
|
+
if (!fs.existsSync(vaultDir)) {
|
|
246
|
+
fs.mkdirSync(vaultDir, { recursive: true });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const node of unique) {
|
|
250
|
+
try {
|
|
251
|
+
writeNode(node, vaultDir);
|
|
252
|
+
written++;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error(` ⚠️ Failed to write ${node.slug}: ${err.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
console.error(` ✅ Wrote ${written} node(s) to vault\n`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Print summary report
|
|
261
|
+
const layerLabel = effectiveLayer === 'project' ? '[project]' : '[global]';
|
|
262
|
+
console.error(' ═══════════════════════════════════════');
|
|
263
|
+
console.error(' 📊 Ingestion Summary');
|
|
264
|
+
console.error(' ═══════════════════════════════════════');
|
|
265
|
+
console.error(` Brain: ${layerLabel} ${brainDir}`);
|
|
266
|
+
console.error(` Detected: ${detected.length} data type(s)`);
|
|
267
|
+
console.error(` Processed: ${toProcess.length} data type(s)`);
|
|
268
|
+
console.error(` Parsed: ${allNewNodes.length} total entries`);
|
|
269
|
+
console.error(` Duplicates: ${duplicateCount}`);
|
|
270
|
+
console.error(` Written: ${dryRun ? `${unique.length} (dry run)` : written}`);
|
|
271
|
+
console.error(' ───────────────────────────────────────');
|
|
272
|
+
for (const [type, info] of Object.entries(stats)) {
|
|
273
|
+
const status = info.error ? `❌ ${info.error}` : `${info.parsed} entries`;
|
|
274
|
+
console.error(` ${type}: ${status}`);
|
|
275
|
+
}
|
|
276
|
+
console.error(' ═══════════════════════════════════════\n');
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
detected: detected.length,
|
|
280
|
+
parsed: allNewNodes.length,
|
|
281
|
+
deduplicated: duplicateCount,
|
|
282
|
+
written: dryRun ? 0 : written,
|
|
283
|
+
stats,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* CLI handler for `npx total-recall ingest google-takeout <path>`.
|
|
289
|
+
*
|
|
290
|
+
* @param {string[]} args — CLI arguments after 'ingest google-takeout'.
|
|
291
|
+
*/
|
|
292
|
+
export async function runGoogleTakeout(args) {
|
|
293
|
+
const opts = parseArgs(args);
|
|
294
|
+
|
|
295
|
+
if (opts.help) {
|
|
296
|
+
printHelp();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!opts.takeoutPath) {
|
|
301
|
+
console.error(' ❌ Error: No Takeout directory path specified.\n');
|
|
302
|
+
printHelp();
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
await ingestGoogleTakeout(opts.takeoutPath, {
|
|
308
|
+
dryRun: opts.dryRun,
|
|
309
|
+
types: opts.types,
|
|
310
|
+
brain: opts.brain,
|
|
311
|
+
maxAge: opts.maxAge,
|
|
312
|
+
layer: opts.layer,
|
|
313
|
+
});
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.error(` ❌ Error: ${err.message}\n`);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chrome-bookmarks.mjs — Parse Chrome bookmarks from Takeout export
|
|
3
|
+
*
|
|
4
|
+
* Reads Chrome bookmarks from the Takeout "Chrome/" directory.
|
|
5
|
+
* Supports both JSON format (Chrome/Bookmarks) and HTML format
|
|
6
|
+
* (Chrome/Bookmarks.html) exported by Google Takeout.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import crypto from 'node:crypto';
|
|
12
|
+
import { collectFiles } from '../utils/takeout-walker.mjs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract bookmarks from Chrome's JSON bookmark format.
|
|
16
|
+
* The structure nests bookmarks under roots.bookmark_bar / roots.other / roots.synced.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} jsonData — Parsed JSON bookmark file.
|
|
19
|
+
* @returns {{ name: string, url: string, folder: string }[]}
|
|
20
|
+
*/
|
|
21
|
+
function extractFromJson(jsonData) {
|
|
22
|
+
const bookmarks = [];
|
|
23
|
+
|
|
24
|
+
function walk(node, folderPath) {
|
|
25
|
+
if (!node) return;
|
|
26
|
+
|
|
27
|
+
if (node.type === 'url' && node.url) {
|
|
28
|
+
bookmarks.push({
|
|
29
|
+
name: node.name || 'Untitled',
|
|
30
|
+
url: node.url,
|
|
31
|
+
folder: folderPath || 'Other',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (node.children && Array.isArray(node.children)) {
|
|
36
|
+
const currentFolder = node.name || folderPath;
|
|
37
|
+
for (const child of node.children) {
|
|
38
|
+
walk(child, currentFolder);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Chrome bookmarks JSON has a "roots" key with bookmark_bar, other, synced
|
|
44
|
+
const roots = jsonData.roots || jsonData;
|
|
45
|
+
if (roots.bookmark_bar) walk(roots.bookmark_bar, 'Bookmarks Bar');
|
|
46
|
+
if (roots.other) walk(roots.other, 'Other Bookmarks');
|
|
47
|
+
if (roots.synced) walk(roots.synced, 'Mobile Bookmarks');
|
|
48
|
+
|
|
49
|
+
// Fallback: if none of the above, try treating the root as a node
|
|
50
|
+
if (bookmarks.length === 0) {
|
|
51
|
+
walk(roots, 'Bookmarks');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return bookmarks;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract bookmarks from Chrome's HTML export format.
|
|
59
|
+
* Parses <DT><A HREF="url">name</A> patterns and tracks folder hierarchy.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} htmlContent — Raw HTML string.
|
|
62
|
+
* @returns {{ name: string, url: string, folder: string }[]}
|
|
63
|
+
*/
|
|
64
|
+
function extractFromHtml(htmlContent) {
|
|
65
|
+
const bookmarks = [];
|
|
66
|
+
const folderStack = ['Bookmarks'];
|
|
67
|
+
|
|
68
|
+
const lines = htmlContent.split('\n');
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
|
|
72
|
+
// Detect folder headers: <DT><H3 ...>Folder Name</H3>
|
|
73
|
+
const folderMatch = trimmed.match(/<H3[^>]*>([^<]+)<\/H3>/i);
|
|
74
|
+
if (folderMatch) {
|
|
75
|
+
folderStack.push(folderMatch[1]);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Detect end of folder list
|
|
80
|
+
if (trimmed === '</DL><p>' || trimmed === '</DL>') {
|
|
81
|
+
if (folderStack.length > 1) folderStack.pop();
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Detect bookmark links: <DT><A HREF="url" ...>name</A>
|
|
86
|
+
const linkMatch = trimmed.match(/<A\s+HREF="([^"]+)"[^>]*>([^<]*)<\/A>/i);
|
|
87
|
+
if (linkMatch) {
|
|
88
|
+
bookmarks.push({
|
|
89
|
+
name: linkMatch[2] || 'Untitled',
|
|
90
|
+
url: linkMatch[1],
|
|
91
|
+
folder: folderStack[folderStack.length - 1] || 'Bookmarks',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return bookmarks;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate a slug-safe string from a bookmark name.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} name
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
function slugify(name) {
|
|
106
|
+
return name
|
|
107
|
+
.toLowerCase()
|
|
108
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
109
|
+
.replace(/^-|-$/g, '')
|
|
110
|
+
.slice(0, 50);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extract domain from a URL for tagging.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} url
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
function extractDomain(url) {
|
|
120
|
+
try {
|
|
121
|
+
return new URL(url).hostname.replace(/^www\./, '');
|
|
122
|
+
} catch {
|
|
123
|
+
return 'unknown';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse Chrome bookmarks from the given directory path.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} dirPath — Path to "Chrome/" directory.
|
|
131
|
+
* @param {object} [options]
|
|
132
|
+
* @returns {object[]} Array of SSSS-compatible memory node objects.
|
|
133
|
+
*/
|
|
134
|
+
export function parseChromeBookmarks(dirPath, options = {}) {
|
|
135
|
+
let rawBookmarks = [];
|
|
136
|
+
|
|
137
|
+
// Try JSON first
|
|
138
|
+
const jsonFiles = collectFiles(dirPath, ['.json']);
|
|
139
|
+
for (const file of jsonFiles) {
|
|
140
|
+
if (path.basename(file).toLowerCase().includes('bookmark')) {
|
|
141
|
+
try {
|
|
142
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
143
|
+
const data = JSON.parse(raw);
|
|
144
|
+
rawBookmarks.push(...extractFromJson(data));
|
|
145
|
+
} catch {
|
|
146
|
+
// Skip unparseable files
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Try HTML if no JSON results
|
|
152
|
+
if (rawBookmarks.length === 0) {
|
|
153
|
+
const htmlFiles = collectFiles(dirPath, ['.html']);
|
|
154
|
+
for (const file of htmlFiles) {
|
|
155
|
+
if (path.basename(file).toLowerCase().includes('bookmark')) {
|
|
156
|
+
try {
|
|
157
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
158
|
+
rawBookmarks.push(...extractFromHtml(raw));
|
|
159
|
+
} catch {
|
|
160
|
+
// Skip unparseable files
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Convert to SSSS nodes
|
|
167
|
+
const now = new Date().toISOString();
|
|
168
|
+
const nodes = [];
|
|
169
|
+
const seenUrls = new Set();
|
|
170
|
+
|
|
171
|
+
for (const bm of rawBookmarks) {
|
|
172
|
+
if (!bm.url || seenUrls.has(bm.url)) continue;
|
|
173
|
+
seenUrls.add(bm.url);
|
|
174
|
+
|
|
175
|
+
// Skip internal Chrome URLs
|
|
176
|
+
if (bm.url.startsWith('chrome://') || bm.url.startsWith('chrome-extension://')) continue;
|
|
177
|
+
|
|
178
|
+
const suffix = crypto.randomBytes(3).toString('hex');
|
|
179
|
+
const slugBase = slugify(bm.name) || 'bookmark';
|
|
180
|
+
const slug = `bookmark-${slugBase}-${suffix}`;
|
|
181
|
+
const domain = extractDomain(bm.url);
|
|
182
|
+
const folderTag = bm.folder
|
|
183
|
+
? bm.folder.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
|
|
184
|
+
: '';
|
|
185
|
+
|
|
186
|
+
const tags = ['google-takeout', 'chrome-bookmarks'];
|
|
187
|
+
if (folderTag) tags.push(folderTag);
|
|
188
|
+
|
|
189
|
+
nodes.push({
|
|
190
|
+
type: 'memory',
|
|
191
|
+
slug,
|
|
192
|
+
title: bm.name,
|
|
193
|
+
category: 'facts',
|
|
194
|
+
body: `Bookmark: ${bm.name}\nURL: ${bm.url}\nFolder: ${bm.folder}\nDomain: ${domain}`,
|
|
195
|
+
status: 'active',
|
|
196
|
+
confidence: 0.9,
|
|
197
|
+
importance: 2,
|
|
198
|
+
created: now,
|
|
199
|
+
updated: now,
|
|
200
|
+
last_accessed: now,
|
|
201
|
+
source: {
|
|
202
|
+
type: 'chrome-bookmarks',
|
|
203
|
+
session_id: `takeout-bookmark-${suffix}`,
|
|
204
|
+
evidence_count: 1,
|
|
205
|
+
},
|
|
206
|
+
supersedes: [],
|
|
207
|
+
superseded_by: null,
|
|
208
|
+
contradicts: [],
|
|
209
|
+
tags,
|
|
210
|
+
related: [],
|
|
211
|
+
routes_to_skills: [],
|
|
212
|
+
sentiment_polarity: 'descriptive',
|
|
213
|
+
sentiment_target: bm.name.slice(0, 40),
|
|
214
|
+
modality: 'should',
|
|
215
|
+
subject: 'user',
|
|
216
|
+
predicate: 'bookmarked',
|
|
217
|
+
object: bm.name.slice(0, 120),
|
|
218
|
+
decay: { half_life_days: 90, access_count: 0 },
|
|
219
|
+
schema_version: 2,
|
|
220
|
+
x_temporal_context: now,
|
|
221
|
+
x_citations: [{
|
|
222
|
+
source: 'chrome-bookmarks',
|
|
223
|
+
title: bm.name,
|
|
224
|
+
url: bm.url,
|
|
225
|
+
published: now,
|
|
226
|
+
relevance: 1.0,
|
|
227
|
+
accessed: now,
|
|
228
|
+
}],
|
|
229
|
+
x_browser_context: {
|
|
230
|
+
url: bm.url,
|
|
231
|
+
domain,
|
|
232
|
+
title: bm.name,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return nodes;
|
|
238
|
+
}
|