@aperdomoll90/ledger-ai 1.0.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 (37) hide show
  1. package/README.md +120 -0
  2. package/dist/cli.js +153 -0
  3. package/dist/commands/backup.js +89 -0
  4. package/dist/commands/check.js +79 -0
  5. package/dist/commands/config.js +115 -0
  6. package/dist/commands/export.js +21 -0
  7. package/dist/commands/ingest.js +221 -0
  8. package/dist/commands/init.js +142 -0
  9. package/dist/commands/onboard.js +190 -0
  10. package/dist/commands/pull.js +75 -0
  11. package/dist/commands/push.js +21 -0
  12. package/dist/commands/restore.js +72 -0
  13. package/dist/commands/setup.js +159 -0
  14. package/dist/commands/show.js +34 -0
  15. package/dist/hooks/block-env.sh +54 -0
  16. package/dist/hooks/hooks/block-env.sh +54 -0
  17. package/dist/hooks/hooks/post-write-ledger.sh +40 -0
  18. package/dist/hooks/hooks/session-end-check.sh +23 -0
  19. package/dist/hooks/post-write-ledger.sh +40 -0
  20. package/dist/hooks/session-end-check.sh +23 -0
  21. package/dist/lib/config.js +63 -0
  22. package/dist/lib/errors.js +23 -0
  23. package/dist/lib/generators.js +115 -0
  24. package/dist/lib/hash.js +4 -0
  25. package/dist/lib/migrate.js +23 -0
  26. package/dist/lib/notes.js +105 -0
  27. package/dist/lib/prompt.js +65 -0
  28. package/dist/mcp-server.js +348 -0
  29. package/dist/migrations/000-tracking.sql +4 -0
  30. package/dist/migrations/001-schema.sql +27 -0
  31. package/dist/migrations/002-functions.sql +14 -0
  32. package/dist/migrations/003-rls.sql +5 -0
  33. package/dist/migrations/migrations/000-tracking.sql +4 -0
  34. package/dist/migrations/migrations/001-schema.sql +27 -0
  35. package/dist/migrations/migrations/002-functions.sql +14 -0
  36. package/dist/migrations/migrations/003-rls.sql +5 -0
  37. package/package.json +60 -0
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @aperdomoll90/ledger-ai
2
+
3
+ Your AI, everywhere. One identity, one memory, every device.
4
+
5
+ Ledger stores who you are, how you work, and what you know in a single knowledge base. Connect any AI agent — Claude, OpenClaw, ChatGPT — and it immediately knows your rules, preferences, and context. Switch devices and pick up where you left off.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @aperdomoll90/ledger-ai
11
+ ```
12
+
13
+ Requires Node.js 20+.
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # 1. Set up credentials and database
19
+ ledger init
20
+
21
+ # 2. Connect your AI agent
22
+ ledger setup claude # Claude Code (live sync, MCP, hooks)
23
+ ledger setup openclaw # OpenClaw (persona files, CLI sync)
24
+ ledger setup chatgpt # ChatGPT (static system prompt export)
25
+
26
+ # 3. Create your persona
27
+ ledger onboard
28
+ ```
29
+
30
+ Second device? Same Ledger, same persona:
31
+
32
+ ```bash
33
+ npm install -g @aperdomoll90/ledger-ai
34
+ ledger init # connect to existing Supabase project
35
+ ledger setup claude # pull persona, install hooks
36
+ ```
37
+
38
+ ## What It Does
39
+
40
+ **For you:** Define who you are, how you want AI to behave, your coding conventions, communication style. Do it once. Every AI agent on every device follows the same rules.
41
+
42
+ **For your agents:** Semantic search over your knowledge base. Notes are embedded with OpenAI and stored in Postgres with pgvector. Agents find relevant context by meaning, not keywords.
43
+
44
+ **For your workflow:** Automatic sync at session start, conflict detection, session-end checks. Hooks enforce rules (block credential file access, auto-ingest notes to Ledger).
45
+
46
+ ## Commands
47
+
48
+ | Command | Description |
49
+ |---|---|
50
+ | `ledger init` | Set up credentials and database schema |
51
+ | `ledger setup <platform>` | Configure an agent (claude, openclaw, chatgpt) |
52
+ | `ledger onboard` | Create your persona (interactive wizard) |
53
+ | `ledger pull` | Download notes from Ledger to local cache |
54
+ | `ledger push <file>` | Upload a local file to Ledger |
55
+ | `ledger check` | Compare local files vs Ledger |
56
+ | `ledger show <query>` | Search by meaning, open matching note |
57
+ | `ledger export <query>` | Download a note to any path (untracked) |
58
+ | `ledger ingest [file]` | Add files to Ledger with duplicate detection |
59
+ | `ledger backup` | Backup all notes to ~/.ledger/backups/ |
60
+ | `ledger restore <file>` | Restore from backup |
61
+ | `ledger config list` | View settings |
62
+ | `ledger config set <key> <value>` | Change settings |
63
+
64
+ ## Stack
65
+
66
+ - **Database:** Supabase (hosted Postgres + pgvector) — free tier
67
+ - **Embeddings:** OpenAI `text-embedding-3-small`
68
+ - **Protocol:** MCP (Model Context Protocol) for Claude Code
69
+ - **CLI:** Commander, TypeScript, ES modules
70
+ - **Sync:** SHA-256 content hashing, conflict detection
71
+
72
+ ## How It Works
73
+
74
+ ```
75
+ Your devices / agents
76
+ |
77
+ v
78
+ ledger CLI ←→ Supabase (Postgres + pgvector)
79
+ |
80
+ ├── pull: download notes → local cache files + CLAUDE.md
81
+ ├── push: upload local changes → Ledger (re-embeds)
82
+ ├── check: compare hashes, detect drift
83
+ ├── show: semantic search → open in editor
84
+ └── MCP server: Claude Code talks to Ledger natively
85
+ ```
86
+
87
+ Notes are stored with content + metadata + vector embeddings. Search finds relevant notes by meaning. Sync uses SHA-256 content hashes to detect changes without markers.
88
+
89
+ ## Platforms
90
+
91
+ | Platform | Connection | Sync |
92
+ |---|---|---|
93
+ | Claude Code | MCP (live) + hooks | Bidirectional, automatic |
94
+ | OpenClaw | CLI | Bidirectional via `ledger` commands |
95
+ | ChatGPT | None | Static snapshot, re-run to update |
96
+
97
+ ## Requirements
98
+
99
+ - Node.js 20+
100
+ - Supabase project (free tier works)
101
+ - OpenAI API key (for embeddings, ~$0.02/1M tokens)
102
+
103
+ ## Known Limitations
104
+
105
+ - **Embeddings require OpenAI** — currently uses `text-embedding-3-small` only. Supabase built-in embeddings (free, no API key) and multi-provider support planned for v2.0.
106
+ - **Anthropic has no embedding API** — Claude is for text generation. Even Claude users need an OpenAI key for embeddings.
107
+ - **English-optimized** — semantic search works best with English content. Multilingual support depends on the embedding model.
108
+
109
+ ## Roadmap
110
+
111
+ - Multi-provider embeddings (Supabase built-in, OpenAI, others)
112
+ - Note versioning / history
113
+ - Soft delete (trash)
114
+ - Multi-format ingest (PDF, Excel, images, audio)
115
+ - Web dashboard
116
+ - VS Code extension
117
+
118
+ ## License
119
+
120
+ ISC
package/dist/cli.js ADDED
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { loadConfig } from './lib/config.js';
4
+ import { pull } from './commands/pull.js';
5
+ import { push } from './commands/push.js';
6
+ import { check } from './commands/check.js';
7
+ import { show } from './commands/show.js';
8
+ import { exportNote } from './commands/export.js';
9
+ import { ingest } from './commands/ingest.js';
10
+ import { init } from './commands/init.js';
11
+ import { setupClaudeCode, setupOpenclaw, setupChatgpt } from './commands/setup.js';
12
+ import { backup, enableBackupCron, disableBackupCron } from './commands/backup.js';
13
+ import { restore } from './commands/restore.js';
14
+ import { onboard } from './commands/onboard.js';
15
+ import { configGet, configSet, configList } from './commands/config.js';
16
+ process.on('unhandledRejection', (err) => {
17
+ console.error(err instanceof Error ? err.message : String(err));
18
+ process.exit(1);
19
+ });
20
+ const program = new Command();
21
+ program
22
+ .name('ledger')
23
+ .description('AI identity and memory system — sync knowledge across agents and devices')
24
+ .version('1.0.0');
25
+ program
26
+ .command('pull')
27
+ .description('Download notes from Ledger to local cache')
28
+ .option('-q, --quiet', 'suppress non-conflict output')
29
+ .option('-f, --force', 'overwrite local changes without conflict check')
30
+ .action(async (options) => {
31
+ const config = loadConfig();
32
+ await pull(config, { quiet: options.quiet ?? false, force: options.force ?? false });
33
+ });
34
+ program
35
+ .command('push <file>')
36
+ .description('Upload a local file to Ledger')
37
+ .action(async (file) => {
38
+ const config = loadConfig();
39
+ await push(config, file);
40
+ });
41
+ program
42
+ .command('check')
43
+ .description('Compare local files vs Ledger, report sync status')
44
+ .action(async () => {
45
+ const config = loadConfig();
46
+ await check(config);
47
+ });
48
+ program
49
+ .command('show <query...>')
50
+ .description('Search Ledger by meaning, open matching note')
51
+ .option('-t, --type <type>', 'filter by note type (e.g. feedback, reference)')
52
+ .option('-p, --project <project>', 'filter by project name')
53
+ .action(async (queryParts, options) => {
54
+ const config = loadConfig();
55
+ await show(config, queryParts.join(' '), { type: options.type, project: options.project });
56
+ });
57
+ program
58
+ .command('export <query...>')
59
+ .description('Download a note to a custom location (untracked)')
60
+ .option('-o, --output <path>', 'output directory (default: current directory)')
61
+ .action(async (queryParts, options) => {
62
+ const config = loadConfig();
63
+ await exportNote(config, queryParts.join(' '), options.output);
64
+ });
65
+ program
66
+ .command('ingest [file]')
67
+ .description('Scan for unknown files and add them to Ledger with duplicate detection')
68
+ .option('-a, --auto', 'auto-ingest without prompts (for hooks)')
69
+ .action(async (file, options) => {
70
+ const config = loadConfig();
71
+ await ingest(config, { file, auto: options.auto ?? false });
72
+ });
73
+ program
74
+ .command('backup')
75
+ .description('Backup all notes to ~/.ledger/backups/')
76
+ .option('-q, --quiet', 'suppress output unless error')
77
+ .option('--enable-cron', 'enable daily backup at 1am')
78
+ .option('--disable-cron', 'disable daily backup cron')
79
+ .action(async (options) => {
80
+ if (options.enableCron) {
81
+ enableBackupCron();
82
+ return;
83
+ }
84
+ if (options.disableCron) {
85
+ disableBackupCron();
86
+ return;
87
+ }
88
+ const config = loadConfig();
89
+ await backup(config, { quiet: options.quiet ?? false });
90
+ });
91
+ program
92
+ .command('restore <file>')
93
+ .description('Restore notes from a backup JSON file')
94
+ .action(async (file) => {
95
+ const config = loadConfig();
96
+ await restore(config, file);
97
+ });
98
+ const configCmd = program
99
+ .command('config')
100
+ .description('View or change Ledger settings');
101
+ configCmd
102
+ .command('get <key>')
103
+ .description('Get a config value (or "all" for full config)')
104
+ .action(async (key) => {
105
+ await configGet(key);
106
+ });
107
+ configCmd
108
+ .command('set <key> <value>')
109
+ .description('Set a config value')
110
+ .action(async (key, value) => {
111
+ await configSet(key, value);
112
+ });
113
+ configCmd
114
+ .command('list')
115
+ .description('Show all settings')
116
+ .action(async () => {
117
+ await configList();
118
+ });
119
+ program
120
+ .command('onboard')
121
+ .description('Create your AI persona (profile, communication style, rules)')
122
+ .action(async () => {
123
+ const config = loadConfig();
124
+ await onboard(config);
125
+ });
126
+ program
127
+ .command('init')
128
+ .description('Set up Ledger on this machine (credentials, database schema)')
129
+ .action(async () => {
130
+ await init();
131
+ });
132
+ const setupCmd = program
133
+ .command('setup')
134
+ .description('Configure an agent platform to use Ledger');
135
+ setupCmd
136
+ .command('claude-code')
137
+ .description('Register MCP, install hooks, pull cache (live sync)')
138
+ .action(async () => {
139
+ await setupClaudeCode();
140
+ });
141
+ setupCmd
142
+ .command('openclaw [path]')
143
+ .description('Generate persona files for OpenClaw (live sync via CLI)')
144
+ .action(async (path) => {
145
+ await setupOpenclaw(path);
146
+ });
147
+ setupCmd
148
+ .command('chatgpt')
149
+ .description('Generate system prompt for ChatGPT (static snapshot)')
150
+ .action(async () => {
151
+ await setupChatgpt();
152
+ });
153
+ program.parse();
@@ -0,0 +1,89 @@
1
+ import { writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { execFileSync, spawnSync } from 'child_process';
4
+ import { getLedgerDir } from '../lib/config.js';
5
+ export async function backup(config, options) {
6
+ const { quiet } = options;
7
+ const backupDir = resolve(getLedgerDir(), 'backups');
8
+ mkdirSync(backupDir, { recursive: true });
9
+ // Fetch all notes (not just cached)
10
+ const { data, error } = await config.supabase
11
+ .from('notes')
12
+ .select('id, content, metadata, created_at, updated_at')
13
+ .order('id', { ascending: true });
14
+ if (error) {
15
+ console.error(`Backup failed: ${error.message}`);
16
+ process.exit(1);
17
+ }
18
+ if (!data || data.length === 0) {
19
+ if (!quiet)
20
+ console.error('No notes to backup.');
21
+ return;
22
+ }
23
+ const date = new Date().toISOString().split('T')[0];
24
+ const filePath = resolve(backupDir, `${date}.json`);
25
+ writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
26
+ // Keep last 5 backups, delete older
27
+ const backups = readdirSync(backupDir)
28
+ .filter(f => f.endsWith('.json'))
29
+ .sort()
30
+ .reverse();
31
+ for (const old of backups.slice(5)) {
32
+ unlinkSync(resolve(backupDir, old));
33
+ if (!quiet)
34
+ console.error(` deleted old backup: ${old}`);
35
+ }
36
+ if (!quiet) {
37
+ console.error(`Backed up ${data.length} notes to ${filePath}`);
38
+ }
39
+ console.log(filePath);
40
+ }
41
+ export function enableBackupCron() {
42
+ const cronLine = '0 1 * * * ledger backup --quiet';
43
+ // Check if already in crontab
44
+ let existing = '';
45
+ try {
46
+ existing = execFileSync('crontab', ['-l'], { encoding: 'utf-8' });
47
+ }
48
+ catch {
49
+ // No crontab yet
50
+ }
51
+ if (existing.includes('ledger backup')) {
52
+ console.error('Backup cron already enabled.');
53
+ return;
54
+ }
55
+ const newCrontab = existing.trimEnd() + '\n' + cronLine + '\n';
56
+ try {
57
+ const result = spawnSync('crontab', ['-'], { input: newCrontab, stdio: ['pipe', 'pipe', 'pipe'] });
58
+ if (result.status !== 0)
59
+ throw new Error(result.stderr?.toString() || 'crontab failed');
60
+ console.error('Daily backup enabled (1am). View with `crontab -l`.');
61
+ }
62
+ catch (e) {
63
+ console.error(`Failed to set cron: ${e.message}`);
64
+ console.error(`Add manually: ${cronLine}`);
65
+ }
66
+ }
67
+ export function disableBackupCron() {
68
+ let existing = '';
69
+ try {
70
+ existing = execFileSync('crontab', ['-l'], { encoding: 'utf-8' });
71
+ }
72
+ catch {
73
+ console.error('No crontab found.');
74
+ return;
75
+ }
76
+ const filtered = existing
77
+ .split('\n')
78
+ .filter(line => !line.includes('ledger backup'))
79
+ .join('\n');
80
+ try {
81
+ const result = spawnSync('crontab', ['-'], { input: filtered, stdio: ['pipe', 'pipe', 'pipe'] });
82
+ if (result.status !== 0)
83
+ throw new Error(result.stderr?.toString() || 'crontab failed');
84
+ console.error('Backup cron disabled.');
85
+ }
86
+ catch (e) {
87
+ console.error(`Failed to update cron: ${e.message}`);
88
+ }
89
+ }
@@ -0,0 +1,79 @@
1
+ import { readFileSync, readdirSync, existsSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { fetchNoteHashes } from '../lib/notes.js';
4
+ import { contentHash } from '../lib/hash.js';
5
+ export async function check(config) {
6
+ const result = {
7
+ files: [],
8
+ clean: 0,
9
+ modified: 0,
10
+ upstream: 0,
11
+ conflicts: 0,
12
+ unknown: 0,
13
+ deleted: 0,
14
+ };
15
+ if (!existsSync(config.memoryDir)) {
16
+ console.error('Memory directory not found. Run `ledger pull` first.');
17
+ return result;
18
+ }
19
+ const noteHashes = await fetchNoteHashes(config.supabase);
20
+ const notesByFile = new Map(noteHashes.map(n => [n.localFile, n]));
21
+ const localFiles = readdirSync(config.memoryDir)
22
+ .filter(f => f.endsWith('.md') && f !== 'MEMORY.md');
23
+ for (const file of localFiles) {
24
+ const filePath = resolve(config.memoryDir, file);
25
+ const localContent = readFileSync(filePath, 'utf-8').trim();
26
+ const localHash = contentHash(localContent);
27
+ const note = notesByFile.get(file);
28
+ if (!note) {
29
+ console.error(` ${file} — unknown (not in Ledger)`);
30
+ result.files.push({ file, state: 'unknown' });
31
+ result.unknown++;
32
+ notesByFile.delete(file);
33
+ continue;
34
+ }
35
+ const ledgerHash = contentHash(note.content);
36
+ const storedHash = note.contentHash;
37
+ const localChanged = localHash !== storedHash;
38
+ const ledgerChanged = ledgerHash !== storedHash;
39
+ if (!localChanged && !ledgerChanged) {
40
+ console.error(` ${file} — in sync`);
41
+ result.files.push({ file, state: 'clean', noteId: note.id });
42
+ result.clean++;
43
+ }
44
+ else if (localChanged && !ledgerChanged) {
45
+ console.error(` ${file} — modified locally`);
46
+ result.files.push({ file, state: 'modified', noteId: note.id });
47
+ result.modified++;
48
+ }
49
+ else if (!localChanged && ledgerChanged) {
50
+ console.error(` ${file} — updated in Ledger`);
51
+ result.files.push({ file, state: 'upstream', noteId: note.id });
52
+ result.upstream++;
53
+ }
54
+ else {
55
+ console.error(` ${file} — CONFLICT (both changed)`);
56
+ result.files.push({ file, state: 'conflict', noteId: note.id });
57
+ result.conflicts++;
58
+ }
59
+ notesByFile.delete(file);
60
+ }
61
+ for (const [file, note] of notesByFile) {
62
+ console.error(` ${file} — missing locally (exists in Ledger)`);
63
+ result.files.push({ file, state: 'deleted', noteId: note.id });
64
+ result.deleted++;
65
+ }
66
+ const summary = [
67
+ `${result.clean} clean`,
68
+ result.modified > 0 ? `${result.modified} modified` : null,
69
+ result.upstream > 0 ? `${result.upstream} upstream` : null,
70
+ result.conflicts > 0 ? `${result.conflicts} conflicts` : null,
71
+ result.unknown > 0 ? `${result.unknown} unknown` : null,
72
+ result.deleted > 0 ? `${result.deleted} deleted` : null,
73
+ ].filter(Boolean).join(', ');
74
+ console.log(`Check: ${summary}`);
75
+ if (result.modified === 0 && result.conflicts === 0 && result.unknown === 0 && result.upstream === 0 && result.deleted === 0) {
76
+ console.log('All synced.');
77
+ }
78
+ return result;
79
+ }
@@ -0,0 +1,115 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { getLedgerDir } from '../lib/config.js';
4
+ import { confirm } from '../lib/prompt.js';
5
+ const CONFIG_PATH = resolve(getLedgerDir(), 'config.json');
6
+ function loadFullConfig() {
7
+ if (existsSync(CONFIG_PATH)) {
8
+ try {
9
+ return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
10
+ }
11
+ catch {
12
+ return {};
13
+ }
14
+ }
15
+ return {};
16
+ }
17
+ function saveConfig(config) {
18
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
19
+ }
20
+ const SECURITY_WARNINGS = {
21
+ envBlocking: 'This disables .env file protection.\nYour API keys and credentials will be readable by the AI agent.',
22
+ mcpJsonBlocking: 'This allows direct editing of mcp.json.\nMCP servers should be registered via `claude mcp add`, not by editing config files.',
23
+ };
24
+ const DESCRIPTIONS = {
25
+ envBlocking: 'Block reading/writing .env and credential files',
26
+ mcpJsonBlocking: 'Block direct editing of mcp.json',
27
+ writeInterception: 'Auto-ingest files written to memory directory into Ledger',
28
+ sessionEndCheck: 'Check for unsynced files at session end',
29
+ };
30
+ export async function configGet(key) {
31
+ const config = loadFullConfig();
32
+ if (key === 'all') {
33
+ console.log(JSON.stringify(config, null, 2));
34
+ return;
35
+ }
36
+ // Check hooks
37
+ const hookKey = key;
38
+ if (hookKey in (config.hooks || {})) {
39
+ console.log(`${key}: ${config.hooks?.[hookKey]}`);
40
+ return;
41
+ }
42
+ // Check top-level
43
+ if (key in config) {
44
+ console.log(`${key}: ${config[key]}`);
45
+ return;
46
+ }
47
+ // Show default
48
+ const defaults = {
49
+ envBlocking: true,
50
+ mcpJsonBlocking: true,
51
+ writeInterception: true,
52
+ sessionEndCheck: true,
53
+ };
54
+ if (key in defaults) {
55
+ console.log(`${key}: ${defaults[key]} (default)`);
56
+ return;
57
+ }
58
+ console.error(`Unknown config key: ${key}`);
59
+ console.error(`Available: ${Object.keys(DESCRIPTIONS).join(', ')}, memoryDir, claudeMdPath, all`);
60
+ process.exit(1);
61
+ }
62
+ export async function configSet(key, value) {
63
+ const config = loadFullConfig();
64
+ // Handle hook settings
65
+ const hookKeys = ['envBlocking', 'mcpJsonBlocking', 'writeInterception', 'sessionEndCheck'];
66
+ if (hookKeys.includes(key)) {
67
+ const boolValue = value === 'true';
68
+ // Disabling a security feature — warn and double confirm
69
+ if (!boolValue && key in SECURITY_WARNINGS) {
70
+ console.error(`\nWARNING: ${SECURITY_WARNINGS[key]}\n`);
71
+ const first = await confirm('Are you sure?');
72
+ if (!first) {
73
+ console.error('Cancelled.');
74
+ return;
75
+ }
76
+ const second = await confirm(`Confirm: disable ${key}?`);
77
+ if (!second) {
78
+ console.error('Cancelled.');
79
+ return;
80
+ }
81
+ }
82
+ if (!config.hooks)
83
+ config.hooks = {};
84
+ config.hooks[key] = boolValue;
85
+ saveConfig(config);
86
+ const state = boolValue ? 'enabled' : 'disabled';
87
+ console.error(`${key}: ${state}`);
88
+ console.error('Run `ledger setup claude-code` to apply hook changes.');
89
+ return;
90
+ }
91
+ // Handle path settings
92
+ if (key === 'memoryDir' || key === 'claudeMdPath') {
93
+ config[key] = value;
94
+ saveConfig(config);
95
+ console.error(`${key}: ${value}`);
96
+ return;
97
+ }
98
+ console.error(`Unknown config key: ${key}`);
99
+ console.error(`Available: ${hookKeys.join(', ')}, memoryDir, claudeMdPath`);
100
+ process.exit(1);
101
+ }
102
+ export async function configList() {
103
+ const config = loadFullConfig();
104
+ const hooks = config.hooks || {};
105
+ console.error('Hook settings:');
106
+ for (const [key, desc] of Object.entries(DESCRIPTIONS)) {
107
+ const value = hooks[key] ?? true;
108
+ const state = value ? 'enabled' : 'DISABLED';
109
+ console.error(` ${key}: ${state} — ${desc}`);
110
+ }
111
+ console.error('\nPaths:');
112
+ console.error(` memoryDir: ${config.memoryDir || '(default)'}`);
113
+ console.error(` claudeMdPath: ${config.claudeMdPath || '(default)'}`);
114
+ console.error('\nConfig file: ' + CONFIG_PATH);
115
+ }
@@ -0,0 +1,21 @@
1
+ import { writeFileSync, mkdirSync } from 'fs';
2
+ import { resolve, dirname } from 'path';
3
+ import { searchNotes } from '../lib/notes.js';
4
+ import { fatal, ExitCode } from '../lib/errors.js';
5
+ export async function exportNote(config, query, outputPath) {
6
+ const results = await searchNotes(config.supabase, config.openai, query);
7
+ if (results.length === 0) {
8
+ fatal('No matching notes found.', ExitCode.NOTE_NOT_FOUND);
9
+ }
10
+ const note = results[0];
11
+ const upsertKey = note.metadata.upsert_key || `note-${note.id}`;
12
+ const filename = `${upsertKey}.md`;
13
+ const targetPath = outputPath
14
+ ? resolve(outputPath, filename)
15
+ : resolve(process.cwd(), filename);
16
+ mkdirSync(dirname(targetPath), { recursive: true });
17
+ writeFileSync(targetPath, note.content + '\n', 'utf-8');
18
+ // No hash stored — export is untracked
19
+ console.log(`Exported "${upsertKey}" → ${targetPath}`);
20
+ console.log(targetPath);
21
+ }