@sashabogi/foundation 2.0.1 → 2.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.
- package/README.md +3 -158
- package/dist/cli.js +363 -255
- package/dist/cli.js.map +1 -1
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +2 -2
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/types.d.ts +1 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/zai.d.ts +17 -8
- package/dist/providers/zai.d.ts.map +1 -1
- package/dist/providers/zai.js +34 -13
- package/dist/providers/zai.js.map +1 -1
- package/dist/services/config.service.d.ts +1 -5
- package/dist/services/config.service.d.ts.map +1 -1
- package/dist/services/config.service.js +0 -20
- package/dist/services/config.service.js.map +1 -1
- package/dist/services/git.service.d.ts +0 -4
- package/dist/services/git.service.d.ts.map +1 -1
- package/dist/services/git.service.js +0 -30
- package/dist/services/git.service.js.map +1 -1
- package/dist/services/storage.service.d.ts +1 -24
- package/dist/services/storage.service.d.ts.map +1 -1
- package/dist/services/storage.service.js +0 -108
- package/dist/services/storage.service.js.map +1 -1
- package/dist/tools/gaia/index.d.ts +5 -8
- package/dist/tools/gaia/index.d.ts.map +1 -1
- package/dist/tools/gaia/index.js +16 -115
- package/dist/tools/gaia/index.js.map +1 -1
- package/dist/tools/gaia/storage.d.ts +15 -0
- package/dist/tools/gaia/storage.d.ts.map +1 -1
- package/dist/tools/gaia/storage.js +296 -1
- package/dist/tools/gaia/storage.js.map +1 -1
- package/dist/tools/seldon/index.d.ts +1 -12
- package/dist/tools/seldon/index.d.ts.map +1 -1
- package/dist/tools/seldon/index.js +1 -183
- package/dist/tools/seldon/index.js.map +1 -1
- package/dist/types/index.d.ts +0 -78
- package/dist/types/index.d.ts.map +1 -1
- package/dist/vault/dashboard.d.ts +10 -0
- package/dist/vault/dashboard.d.ts.map +1 -0
- package/dist/vault/dashboard.js +145 -0
- package/dist/vault/dashboard.js.map +1 -0
- package/dist/vault/sync.d.ts +64 -0
- package/dist/vault/sync.d.ts.map +1 -0
- package/dist/vault/sync.js +360 -0
- package/dist/vault/sync.js.map +1 -0
- package/dist/vault/transform.d.ts +25 -0
- package/dist/vault/transform.d.ts.map +1 -0
- package/dist/vault/transform.js +155 -0
- package/dist/vault/transform.js.map +1 -0
- package/package.json +1 -1
- package/packages/ui/dist/assets/index-BCBAHIpG.css +1 -0
- package/packages/ui/dist/assets/index-CxRK0aqp.js +387 -0
- package/packages/ui/dist/index.html +2 -2
- package/packages/ui/dist/assets/index-oiJTDMJ1.css +0 -1
- package/packages/ui/dist/assets/index-oivszLTx.js +0 -352
package/dist/cli.js
CHANGED
|
@@ -14,23 +14,28 @@
|
|
|
14
14
|
import { Command } from 'commander';
|
|
15
15
|
import chalk from 'chalk';
|
|
16
16
|
import ora from 'ora';
|
|
17
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, createReadStream, readdirSync
|
|
17
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, createReadStream, readdirSync } from 'fs';
|
|
18
18
|
import { execSync } from 'child_process';
|
|
19
19
|
import { homedir } from 'os';
|
|
20
20
|
import { join, dirname } from 'path';
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
22
|
-
import { createServer } from 'http';
|
|
22
|
+
import { createServer, request as httpRequest } from 'http';
|
|
23
23
|
import { extname } from 'path';
|
|
24
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
25
|
const __dirname = dirname(__filename);
|
|
26
26
|
import { ConfigService } from './services/config.service.js';
|
|
27
27
|
import { runSetupWizard, addProvider, testProvider, listProviders, } from './cli/setup-wizard.js';
|
|
28
|
+
import { MemoriaStorage } from './tools/gaia/storage.js';
|
|
29
|
+
import { VaultSync } from './vault/sync.js';
|
|
28
30
|
// Read version from package.json dynamically
|
|
29
31
|
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
30
32
|
const VERSION = packageJson.version;
|
|
31
33
|
const CONFIG_DIR = join(homedir(), '.foundation');
|
|
32
34
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.yaml');
|
|
33
35
|
const PROJECTS_FILE = join(CONFIG_DIR, 'projects.json');
|
|
36
|
+
const VAULT_CONFIG_FILE = join(CONFIG_DIR, 'vault.json');
|
|
37
|
+
const GAIA_DB_PATH = join(CONFIG_DIR, 'gaia-memory.db');
|
|
38
|
+
const VAULT_SYNC_STATE = join(CONFIG_DIR, 'vault-sync.json');
|
|
34
39
|
function loadProjects() {
|
|
35
40
|
try {
|
|
36
41
|
if (existsSync(PROJECTS_FILE)) {
|
|
@@ -66,14 +71,9 @@ function registerProject(projectPath) {
|
|
|
66
71
|
const FOUNDATION_CLAUDE_MD = `
|
|
67
72
|
## Foundation MCP Tools (Demerzel, Seldon, Gaia)
|
|
68
73
|
|
|
69
|
-
> **
|
|
74
|
+
> **CRITICAL: These tools are ALREADY LOADED. Use them directly without ToolSearch.**
|
|
70
75
|
|
|
71
|
-
Foundation provides
|
|
72
|
-
|
|
73
|
-
**Quick load patterns:**
|
|
74
|
-
- \`ToolSearch("+foundation demerzel")\` — loads all Demerzel tools
|
|
75
|
-
- \`ToolSearch("+foundation seldon")\` — loads all Seldon tools
|
|
76
|
-
- \`ToolSearch("+foundation gaia")\` — loads all Gaia tools
|
|
76
|
+
Foundation provides 37 tools for AI-assisted development. All tools use \`mcp__foundation__\` prefix.
|
|
77
77
|
|
|
78
78
|
### Demerzel — Codebase Intelligence
|
|
79
79
|
*"I have been watching for 20,000 years."*
|
|
@@ -89,66 +89,77 @@ Foundation provides 47 tools for AI-assisted development. All tools use \`mcp__f
|
|
|
89
89
|
| \`demerzel_find_importers\` | FREE | What files import X? |
|
|
90
90
|
| \`demerzel_get_deps\` | FREE | What does file X import? |
|
|
91
91
|
| \`demerzel_get_context\` | FREE | Get code around a location |
|
|
92
|
-
| \`demerzel_analyze\` | ~500 tokens | AI-powered
|
|
92
|
+
| \`demerzel_analyze\` | ~500 tokens | AI-powered analysis |
|
|
93
93
|
| \`demerzel_semantic_search\` | ~tokens | Natural language search |
|
|
94
94
|
|
|
95
|
-
**Visualize:** Run \`foundation ui\` to open the codebase graph in your browser.
|
|
96
|
-
|
|
97
95
|
### Seldon — Multi-Agent Orchestration
|
|
98
96
|
*"The future is not set, but it can be guided."*
|
|
99
97
|
|
|
100
98
|
| Tool | Purpose |
|
|
101
99
|
|------|---------|
|
|
102
|
-
| \`seldon_invoke\` | Invoke agent role (coder, critic, reviewer
|
|
103
|
-
| \`seldon_compare\` |
|
|
104
|
-
| \`seldon_critique\` | Get
|
|
100
|
+
| \`seldon_invoke\` | Invoke agent role (coder, critic, reviewer) |
|
|
101
|
+
| \`seldon_compare\` | Compare multiple agent perspectives |
|
|
102
|
+
| \`seldon_critique\` | Get critical review |
|
|
105
103
|
| \`seldon_review\` | Code review |
|
|
106
|
-
| \`seldon_design\` | UI/UX design feedback |
|
|
107
104
|
| \`seldon_plan\` | Generate implementation plan |
|
|
108
|
-
| \`seldon_phase_create\` | Break plan into
|
|
109
|
-
| \`seldon_phase_list\` | List phases and
|
|
110
|
-
| \`seldon_verify\` | Verify implementation
|
|
111
|
-
| \`seldon_fix\` | Generate fixes
|
|
105
|
+
| \`seldon_phase_create\` | Break plan into phases |
|
|
106
|
+
| \`seldon_phase_list\` | List phases and status |
|
|
107
|
+
| \`seldon_verify\` | Verify implementation |
|
|
108
|
+
| \`seldon_fix\` | Generate fixes |
|
|
112
109
|
| \`seldon_execute_verified\` | Execute with verification loop |
|
|
113
|
-
| \`
|
|
114
|
-
| \`
|
|
115
|
-
| \`seldon_pipeline_execute\` | Run a pipeline |
|
|
116
|
-
| \`seldon_pipeline_status\` | Get pipeline execution status |
|
|
117
|
-
| \`seldon_task_execute\` | Execute task in isolated worktree |
|
|
118
|
-
| \`seldon_task_claim\` | Claim next available task |
|
|
119
|
-
| \`seldon_providers_list\` | List configured providers |
|
|
120
|
-
| \`seldon_providers_test\` | Test provider connectivity |
|
|
110
|
+
| \`seldon_providers_list\` | List providers |
|
|
111
|
+
| \`seldon_providers_test\` | Test provider health |
|
|
121
112
|
|
|
122
|
-
### Gaia — Workflow
|
|
113
|
+
### Gaia — Workflow Patterns
|
|
123
114
|
*"We are all one, and one is all."*
|
|
124
115
|
|
|
125
|
-
#### Workflow Tools
|
|
126
|
-
| Tool | Purpose |
|
|
127
|
-
|------|---------|
|
|
128
|
-
| \`gaia_checkpoint\` | Save full structured session state to disk |
|
|
129
|
-
| \`gaia_status\` | Lightweight index card (~150 tokens) |
|
|
130
|
-
| \`gaia_query\` | Keyword search across checkpoint |
|
|
131
|
-
| \`gaia_get_decisions\` | List architectural decisions |
|
|
132
|
-
| \`gaia_get_progress\` | Task progress summary |
|
|
133
|
-
| \`gaia_get_changes\` | Files changed in session |
|
|
134
|
-
| \`gaia_handoff\` | Create session handoff document |
|
|
135
|
-
| \`gaia_observe\` | Auto-detect patterns and observations |
|
|
136
|
-
| \`gaia_migrate\` | Migrate v1 checkpoint data to v2 |
|
|
137
|
-
| \`gaia_learn\` | Record correction for CLAUDE.md |
|
|
138
|
-
| \`gaia_apply\` | Write learnings to CLAUDE.md |
|
|
139
|
-
| \`gaia_review\` | Review accumulated learnings |
|
|
140
|
-
|
|
141
|
-
#### Memory Tools (SQLite + FTS5)
|
|
142
116
|
| Tool | Purpose |
|
|
143
117
|
|------|---------|
|
|
144
|
-
| \`
|
|
145
|
-
| \`
|
|
118
|
+
| \`gaia_checkpoint\` | Save structured session state |
|
|
119
|
+
| \`gaia_status\` | Get checkpoint index card (~150 tokens) |
|
|
120
|
+
| \`gaia_query\` | Search checkpoint by keyword |
|
|
121
|
+
| \`gaia_get_decisions\` | Get architectural decisions |
|
|
122
|
+
| \`gaia_get_progress\` | Get task progress |
|
|
123
|
+
| \`gaia_get_changes\` | Get files changed |
|
|
124
|
+
| \`gaia_handoff\` | Create handoff document |
|
|
125
|
+
| \`gaia_observe\` | Analyze session patterns |
|
|
126
|
+
| \`gaia_migrate\` | Migrate v1 checkpoints to v2 |
|
|
127
|
+
| \`gaia_save\` | Save a memory |
|
|
128
|
+
| \`gaia_search\` | Search memories |
|
|
146
129
|
| \`gaia_get\` | Get memory by ID |
|
|
147
|
-
| \`gaia_delete\` | Delete memory
|
|
148
|
-
| \`gaia_stats\` |
|
|
130
|
+
| \`gaia_delete\` | Delete a memory |
|
|
131
|
+
| \`gaia_stats\` | Get memory statistics |
|
|
149
132
|
| \`gaia_link\` | Create typed link between memories |
|
|
150
|
-
| \`gaia_graph\` | Get link graph
|
|
133
|
+
| \`gaia_graph\` | Get memory link graph |
|
|
151
134
|
`;
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Vault Config Helpers
|
|
137
|
+
// ============================================================================
|
|
138
|
+
function loadVaultConfig() {
|
|
139
|
+
try {
|
|
140
|
+
if (existsSync(VAULT_CONFIG_FILE)) {
|
|
141
|
+
return JSON.parse(readFileSync(VAULT_CONFIG_FILE, 'utf8'));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch { }
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
function saveVaultConfig(config) {
|
|
148
|
+
writeFileSync(VAULT_CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
|
149
|
+
}
|
|
150
|
+
function buildVaultConfig(overrides) {
|
|
151
|
+
const saved = loadVaultConfig();
|
|
152
|
+
const vaultPath = overrides?.vaultPath ?? saved?.vaultPath;
|
|
153
|
+
if (!vaultPath) {
|
|
154
|
+
throw new Error('No vault path configured. Run: foundation vault init <path>');
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
vaultPath,
|
|
158
|
+
gaiaDbPath: GAIA_DB_PATH,
|
|
159
|
+
syncStatePath: VAULT_SYNC_STATE,
|
|
160
|
+
claudeMemoryPath: saved?.claudeMemoryPath ?? undefined,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
152
163
|
const program = new Command();
|
|
153
164
|
program
|
|
154
165
|
.name('foundation')
|
|
@@ -266,16 +277,6 @@ worktrees:
|
|
|
266
277
|
max_count: 10
|
|
267
278
|
auto_cleanup: true
|
|
268
279
|
stale_after_hours: 48
|
|
269
|
-
|
|
270
|
-
learning:
|
|
271
|
-
auto_apply: false
|
|
272
|
-
categories:
|
|
273
|
-
- code_style
|
|
274
|
-
- architecture
|
|
275
|
-
- testing
|
|
276
|
-
- performance
|
|
277
|
-
- security
|
|
278
|
-
- documentation
|
|
279
280
|
`;
|
|
280
281
|
writeFileSync(CONFIG_FILE, defaultConfig);
|
|
281
282
|
spinner.succeed('Foundation initialized!');
|
|
@@ -369,9 +370,9 @@ mcpCommand
|
|
|
369
370
|
console.log(chalk.bold('Foundation is now available in all Claude Code sessions.'));
|
|
370
371
|
console.log();
|
|
371
372
|
console.log('Available tools:');
|
|
372
|
-
console.log(chalk.cyan(' Demerzel (9)') + '
|
|
373
|
-
console.log(chalk.cyan(' Seldon (
|
|
374
|
-
console.log(chalk.cyan(' Gaia (
|
|
373
|
+
console.log(chalk.cyan(' Demerzel (9)') + ' - Codebase intelligence');
|
|
374
|
+
console.log(chalk.cyan(' Seldon (12)') + ' - Multi-agent orchestration');
|
|
375
|
+
console.log(chalk.cyan(' Gaia (16)') + ' - Workflow patterns + memory');
|
|
375
376
|
console.log();
|
|
376
377
|
console.log(chalk.bold('Optional:'));
|
|
377
378
|
console.log(' Run ' + chalk.cyan('foundation hooks install') + ' in a project to enable');
|
|
@@ -414,72 +415,26 @@ mcpCommand
|
|
|
414
415
|
const hooksCommand = program
|
|
415
416
|
.command('hooks')
|
|
416
417
|
.description('Manage Gaia lifecycle hooks');
|
|
417
|
-
//
|
|
418
|
-
const HOOK_SESSION_START = `#!/bin/bash
|
|
419
|
-
# Gaia v2 SessionStart hook
|
|
420
|
-
# Injects the latest checkpoint index card into Claude's context.
|
|
421
|
-
|
|
422
|
-
find_foundation_dir() {
|
|
423
|
-
local dir="\${CLAUDE_PROJECT_DIR:-\$PWD}"
|
|
424
|
-
local count=0
|
|
425
|
-
while [ \$count -lt 5 ]; do
|
|
426
|
-
if [ -f "\$dir/.foundation/sessions/latest/index.txt" ]; then
|
|
427
|
-
echo "\$dir"
|
|
428
|
-
return 0
|
|
429
|
-
fi
|
|
430
|
-
dir="\$(dirname "\$dir")"
|
|
431
|
-
count=\$((count + 1))
|
|
432
|
-
done
|
|
433
|
-
return 1
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
PROJECT_DIR=\$(find_foundation_dir)
|
|
437
|
-
if [ \$? -eq 0 ]; then
|
|
438
|
-
INDEX_FILE="\$PROJECT_DIR/.foundation/sessions/latest/index.txt"
|
|
439
|
-
python3 -c "
|
|
440
|
-
import json, sys
|
|
441
|
-
try:
|
|
442
|
-
with open(sys.argv[1], 'r') as f:
|
|
443
|
-
content = f.read().strip()
|
|
444
|
-
if content:
|
|
445
|
-
output = {'hookSpecificOutput': {'additionalContext': '[Gaia Checkpoint]\\n' + content}}
|
|
446
|
-
print(json.dumps(output))
|
|
447
|
-
except Exception:
|
|
448
|
-
pass
|
|
449
|
-
" "\$INDEX_FILE"
|
|
450
|
-
fi
|
|
451
|
-
`;
|
|
452
|
-
const HOOK_PRE_COMPACT = `#!/bin/bash
|
|
453
|
-
# Gaia v2 PreCompact hook — reminds to checkpoint before context compaction
|
|
454
|
-
printf '{"hookSpecificOutput":{"additionalContext":"[Gaia] Context compaction imminent. If you have unsaved session state (decisions, progress, changes), run gaia_checkpoint now to preserve it before context is lost."}}'
|
|
455
|
-
`;
|
|
456
|
-
const HOOK_POST_TASK = `#!/bin/bash
|
|
457
|
-
# Gaia v2 PostToolUse hook (Task matcher)
|
|
458
|
-
INPUT=\$(cat)
|
|
459
|
-
AGENT_TYPE=\$(echo "\$INPUT" | python3 -c "
|
|
460
|
-
import json, sys
|
|
461
|
-
try:
|
|
462
|
-
data = json.load(sys.stdin)
|
|
463
|
-
tool_input = data.get('tool_input', {})
|
|
464
|
-
print(tool_input.get('subagent_type', ''))
|
|
465
|
-
except Exception:
|
|
466
|
-
print('')
|
|
467
|
-
" 2>/dev/null)
|
|
468
|
-
|
|
469
|
-
if [ "\$AGENT_TYPE" = "Explore" ]; then
|
|
470
|
-
exit 0
|
|
471
|
-
fi
|
|
472
|
-
|
|
473
|
-
printf '{"hookSpecificOutput":{"additionalContext":"[Gaia] Sub-agent completed. Consider updating your checkpoint if significant progress was made."}}'
|
|
474
|
-
`;
|
|
418
|
+
// Inline hook commands (no external .sh files needed)
|
|
475
419
|
const HOOKS_SETTINGS_CONFIG = {
|
|
476
420
|
hooks: {
|
|
477
421
|
SessionStart: [{
|
|
478
422
|
matcher: '',
|
|
479
423
|
hooks: [{
|
|
480
424
|
type: 'command',
|
|
481
|
-
command:
|
|
482
|
-
|
|
425
|
+
command: `python3 -c "
|
|
426
|
+
import json, os, pathlib
|
|
427
|
+
d = os.environ.get('CLAUDE_PROJECT_DIR', os.getcwd())
|
|
428
|
+
for _ in range(5):
|
|
429
|
+
f = os.path.join(d, '.foundation', 'sessions', 'latest', 'index.txt')
|
|
430
|
+
if os.path.isfile(f):
|
|
431
|
+
c = pathlib.Path(f).read_text().strip()
|
|
432
|
+
if c:
|
|
433
|
+
print(json.dumps({'hookSpecificOutput':{'additionalContext':'[Gaia Checkpoint]\\\\n'+c}}))
|
|
434
|
+
break
|
|
435
|
+
d = os.path.dirname(d)
|
|
436
|
+
"`,
|
|
437
|
+
timeout: 5000,
|
|
483
438
|
statusMessage: 'Loading Gaia checkpoint...',
|
|
484
439
|
}],
|
|
485
440
|
}],
|
|
@@ -487,8 +442,8 @@ const HOOKS_SETTINGS_CONFIG = {
|
|
|
487
442
|
matcher: '',
|
|
488
443
|
hooks: [{
|
|
489
444
|
type: 'command',
|
|
490
|
-
command: '.
|
|
491
|
-
timeout:
|
|
445
|
+
command: 'echo \'{"hookSpecificOutput":{"additionalContext":"[Gaia] Context compaction imminent. If you have unsaved session state (decisions, progress, changes), run gaia_checkpoint now to preserve it before context is lost."}}\'',
|
|
446
|
+
timeout: 5000,
|
|
492
447
|
statusMessage: 'Gaia checkpoint reminder',
|
|
493
448
|
}],
|
|
494
449
|
}],
|
|
@@ -496,8 +451,17 @@ const HOOKS_SETTINGS_CONFIG = {
|
|
|
496
451
|
matcher: 'Task',
|
|
497
452
|
hooks: [{
|
|
498
453
|
type: 'command',
|
|
499
|
-
command:
|
|
500
|
-
|
|
454
|
+
command: `python3 -c "
|
|
455
|
+
import json, sys
|
|
456
|
+
try:
|
|
457
|
+
data = json.load(sys.stdin)
|
|
458
|
+
if data.get('tool_input',{}).get('subagent_type','') == 'Explore':
|
|
459
|
+
sys.exit(0)
|
|
460
|
+
except Exception:
|
|
461
|
+
pass
|
|
462
|
+
print(json.dumps({'hookSpecificOutput':{'additionalContext':'[Gaia] Sub-agent completed. Consider updating your checkpoint if significant progress was made.'}}))
|
|
463
|
+
"`,
|
|
464
|
+
timeout: 5000,
|
|
501
465
|
}],
|
|
502
466
|
}],
|
|
503
467
|
},
|
|
@@ -509,23 +473,10 @@ hooksCommand
|
|
|
509
473
|
const spinner = ora('Installing Gaia hooks...').start();
|
|
510
474
|
const cwd = process.cwd();
|
|
511
475
|
try {
|
|
512
|
-
// 1.
|
|
513
|
-
const
|
|
514
|
-
mkdirSync(
|
|
515
|
-
|
|
516
|
-
// 2. Write hook scripts
|
|
517
|
-
const hookFiles = [
|
|
518
|
-
{ name: 'gaia-session-start.sh', content: HOOK_SESSION_START },
|
|
519
|
-
{ name: 'gaia-pre-compact.sh', content: HOOK_PRE_COMPACT },
|
|
520
|
-
{ name: 'gaia-post-task.sh', content: HOOK_POST_TASK },
|
|
521
|
-
];
|
|
522
|
-
for (const hook of hookFiles) {
|
|
523
|
-
const hookPath = join(hooksDir, hook.name);
|
|
524
|
-
writeFileSync(hookPath, hook.content);
|
|
525
|
-
chmodSync(hookPath, 0o755);
|
|
526
|
-
}
|
|
527
|
-
spinner.text = 'Wrote hook scripts';
|
|
528
|
-
// 3. Merge hook configuration into .claude/settings.json
|
|
476
|
+
// 1. Ensure .claude/ directory exists
|
|
477
|
+
const claudeDir = join(cwd, '.claude');
|
|
478
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
479
|
+
// 2. Merge hook configuration into .claude/settings.json
|
|
529
480
|
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
530
481
|
let settings = {};
|
|
531
482
|
try {
|
|
@@ -542,15 +493,12 @@ hooksCommand
|
|
|
542
493
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
543
494
|
spinner.succeed('Gaia hooks installed!');
|
|
544
495
|
console.log();
|
|
545
|
-
console.log(chalk.bold('
|
|
546
|
-
console.log(chalk.green(' ✓') + ' ' + chalk.cyan('
|
|
547
|
-
console.log(chalk.green(' ✓') + ' ' + chalk.cyan('
|
|
548
|
-
console.log(chalk.green(' ✓') + ' ' + chalk.cyan('
|
|
496
|
+
console.log(chalk.bold('Configured hooks (inline commands):'));
|
|
497
|
+
console.log(chalk.green(' ✓') + ' ' + chalk.cyan('SessionStart') + ' — Injects checkpoint on session start');
|
|
498
|
+
console.log(chalk.green(' ✓') + ' ' + chalk.cyan('PreCompact') + ' — Reminds to checkpoint before compaction');
|
|
499
|
+
console.log(chalk.green(' ✓') + ' ' + chalk.cyan('PostToolUse') + ' — Reminds to checkpoint after sub-agent tasks');
|
|
549
500
|
console.log();
|
|
550
|
-
console.log(chalk.bold('
|
|
551
|
-
console.log(' ' + chalk.gray(join(hooksDir, 'gaia-session-start.sh')));
|
|
552
|
-
console.log(' ' + chalk.gray(join(hooksDir, 'gaia-pre-compact.sh')));
|
|
553
|
-
console.log(' ' + chalk.gray(join(hooksDir, 'gaia-post-task.sh')));
|
|
501
|
+
console.log(chalk.bold('Updated:'));
|
|
554
502
|
console.log(' ' + chalk.gray(settingsPath));
|
|
555
503
|
console.log();
|
|
556
504
|
console.log(chalk.gray('Hooks will activate on the next Claude Code session in this project.'));
|
|
@@ -759,12 +707,12 @@ program
|
|
|
759
707
|
'.svg': 'image/svg+xml',
|
|
760
708
|
'.ico': 'image/x-icon',
|
|
761
709
|
};
|
|
762
|
-
const server = createServer(
|
|
710
|
+
const server = createServer((req, res) => {
|
|
763
711
|
const url = req.url || '/';
|
|
764
712
|
// CORS headers for API endpoints
|
|
765
713
|
const corsHeaders = {
|
|
766
714
|
'Access-Control-Allow-Origin': '*',
|
|
767
|
-
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
715
|
+
'Access-Control-Allow-Methods': 'GET, PUT, POST, DELETE, OPTIONS',
|
|
768
716
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
769
717
|
};
|
|
770
718
|
// Handle CORS preflight
|
|
@@ -816,131 +764,135 @@ program
|
|
|
816
764
|
}
|
|
817
765
|
return;
|
|
818
766
|
}
|
|
819
|
-
//
|
|
820
|
-
if (url.startsWith('/api/
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
? join(projectParam, '.foundation', 'sessions', 'latest', 'checkpoint.json')
|
|
825
|
-
: join(cwd, '.foundation', 'sessions', 'latest', 'checkpoint.json');
|
|
826
|
-
if (existsSync(targetPath)) {
|
|
767
|
+
// ---- Memory API endpoints ----
|
|
768
|
+
if (url.startsWith('/api/memories/stats')) {
|
|
769
|
+
try {
|
|
770
|
+
const dbPath = join(homedir(), '.foundation', 'gaia-memory.db');
|
|
771
|
+
const storage = new MemoriaStorage(dbPath);
|
|
827
772
|
try {
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
});
|
|
833
|
-
res.end(content);
|
|
773
|
+
const gaia = storage.getStats();
|
|
774
|
+
const rescue = storage.getRescueStats();
|
|
775
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...corsHeaders });
|
|
776
|
+
res.end(JSON.stringify({ gaia, rescue }));
|
|
834
777
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
res.end(JSON.stringify({ error: 'Failed to read checkpoint' }));
|
|
778
|
+
finally {
|
|
779
|
+
storage.close();
|
|
838
780
|
}
|
|
839
781
|
}
|
|
840
|
-
|
|
841
|
-
res.writeHead(
|
|
842
|
-
res.end(JSON.stringify({
|
|
843
|
-
error: 'Checkpoint not found',
|
|
844
|
-
hint: 'Run `gaia_checkpoint` to save session state',
|
|
845
|
-
}));
|
|
782
|
+
catch (err) {
|
|
783
|
+
res.writeHead(500, { 'Content-Type': 'application/json', ...corsHeaders });
|
|
784
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
846
785
|
}
|
|
847
786
|
return;
|
|
848
787
|
}
|
|
849
|
-
|
|
850
|
-
if (url.startsWith('/api/memories')) {
|
|
851
|
-
const urlObj = new URL(url, `http://localhost:${port}`);
|
|
852
|
-
const tier = urlObj.searchParams.get('tier');
|
|
853
|
-
const q = urlObj.searchParams.get('q');
|
|
854
|
-
const dbPath = join(homedir(), '.foundation', 'gaia-memory.db');
|
|
855
|
-
if (!existsSync(dbPath)) {
|
|
856
|
-
res.writeHead(200, { 'Content-Type': 'application/json', ...corsHeaders });
|
|
857
|
-
res.end(JSON.stringify({ memories: [] }));
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
788
|
+
if (url.startsWith('/api/memories/recent')) {
|
|
860
789
|
try {
|
|
861
|
-
const
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
790
|
+
const urlObj = new URL(url, `http://localhost:${port}`);
|
|
791
|
+
const limit = parseInt(urlObj.searchParams.get('limit') || '50', 10);
|
|
792
|
+
const tierParam = urlObj.searchParams.get('tier');
|
|
793
|
+
const tiers = tierParam ? tierParam.split(',') : undefined;
|
|
794
|
+
const dbPath = join(homedir(), '.foundation', 'gaia-memory.db');
|
|
795
|
+
const storage = new MemoriaStorage(dbPath);
|
|
796
|
+
try {
|
|
797
|
+
const results = storage.getRecent({ limit, tiers });
|
|
798
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...corsHeaders });
|
|
799
|
+
res.end(JSON.stringify(results));
|
|
868
800
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
params.push(`%${q}%`);
|
|
801
|
+
finally {
|
|
802
|
+
storage.close();
|
|
872
803
|
}
|
|
873
|
-
query += ' ORDER BY created_at DESC LIMIT 500';
|
|
874
|
-
const rows = db.prepare(query).all(...params);
|
|
875
|
-
db.close();
|
|
876
|
-
const memories = rows.map((r) => ({
|
|
877
|
-
...r,
|
|
878
|
-
tags: (() => { try {
|
|
879
|
-
return JSON.parse(r.tags);
|
|
880
|
-
}
|
|
881
|
-
catch {
|
|
882
|
-
return [];
|
|
883
|
-
} })(),
|
|
884
|
-
related_files: (() => { try {
|
|
885
|
-
return JSON.parse(r.related_files);
|
|
886
|
-
}
|
|
887
|
-
catch {
|
|
888
|
-
return [];
|
|
889
|
-
} })(),
|
|
890
|
-
metadata: (() => { try {
|
|
891
|
-
return JSON.parse(r.metadata);
|
|
892
|
-
}
|
|
893
|
-
catch {
|
|
894
|
-
return null;
|
|
895
|
-
} })(),
|
|
896
|
-
}));
|
|
897
|
-
res.writeHead(200, { 'Content-Type': 'application/json', ...corsHeaders });
|
|
898
|
-
res.end(JSON.stringify({ memories }));
|
|
899
804
|
}
|
|
900
805
|
catch (err) {
|
|
901
806
|
res.writeHead(500, { 'Content-Type': 'application/json', ...corsHeaders });
|
|
902
|
-
res.end(JSON.stringify({
|
|
807
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
903
808
|
}
|
|
904
809
|
return;
|
|
905
810
|
}
|
|
906
|
-
|
|
907
|
-
if (url.startsWith('/api/memory-graph')) {
|
|
908
|
-
const dbPath = join(homedir(), '.foundation', 'gaia-memory.db');
|
|
909
|
-
if (!existsSync(dbPath)) {
|
|
910
|
-
res.writeHead(200, { 'Content-Type': 'application/json', ...corsHeaders });
|
|
911
|
-
res.end(JSON.stringify({ memories: [], links: [] }));
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
811
|
+
if (url.startsWith('/api/memories/search')) {
|
|
914
812
|
try {
|
|
915
|
-
const
|
|
916
|
-
const
|
|
917
|
-
const
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
813
|
+
const urlObj = new URL(url, `http://localhost:${port}`);
|
|
814
|
+
const query = urlObj.searchParams.get('q') || '';
|
|
815
|
+
const tierParam = urlObj.searchParams.get('tier');
|
|
816
|
+
const tiers = tierParam ? tierParam.split(',') : undefined;
|
|
817
|
+
const limit = parseInt(urlObj.searchParams.get('limit') || '25', 10);
|
|
818
|
+
const sourceFilter = urlObj.searchParams.get('source') || 'all';
|
|
819
|
+
const dbPath = join(homedir(), '.foundation', 'gaia-memory.db');
|
|
820
|
+
const storage = new MemoriaStorage(dbPath);
|
|
821
|
+
try {
|
|
822
|
+
let results;
|
|
823
|
+
if (!query) {
|
|
824
|
+
// No query = return recent
|
|
825
|
+
results = storage.getRecent({ limit, tiers });
|
|
924
826
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
} })(),
|
|
928
|
-
related_files: (() => { try {
|
|
929
|
-
return JSON.parse(r.related_files);
|
|
827
|
+
else {
|
|
828
|
+
results = storage.search({ query, tiers, limit });
|
|
930
829
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
830
|
+
// Apply source filter client-side
|
|
831
|
+
if (sourceFilter === 'gaia') {
|
|
832
|
+
results = results.filter(r => !r.memory.id.startsWith('rescue_'));
|
|
833
|
+
}
|
|
834
|
+
else if (sourceFilter === 'rescued') {
|
|
835
|
+
results = results.filter(r => r.memory.id.startsWith('rescue_'));
|
|
836
|
+
}
|
|
837
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...corsHeaders });
|
|
838
|
+
res.end(JSON.stringify(results));
|
|
839
|
+
}
|
|
840
|
+
finally {
|
|
841
|
+
storage.close();
|
|
842
|
+
}
|
|
937
843
|
}
|
|
938
844
|
catch (err) {
|
|
939
845
|
res.writeHead(500, { 'Content-Type': 'application/json', ...corsHeaders });
|
|
940
|
-
res.end(JSON.stringify({
|
|
846
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
941
847
|
}
|
|
942
848
|
return;
|
|
943
849
|
}
|
|
850
|
+
// ---- Daneel Proxy API endpoints ----
|
|
851
|
+
if (url.startsWith('/api/daneel/')) {
|
|
852
|
+
const daneelPath = url.replace('/api/daneel', '');
|
|
853
|
+
const method = req.method || 'GET';
|
|
854
|
+
// Collect request body for PUT/POST/DELETE
|
|
855
|
+
const bodyChunks = [];
|
|
856
|
+
req.on('data', (chunk) => { bodyChunks.push(chunk); });
|
|
857
|
+
req.on('end', () => {
|
|
858
|
+
const requestBody = Buffer.concat(bodyChunks);
|
|
859
|
+
const proxyReq = httpRequest({
|
|
860
|
+
hostname: '127.0.0.1',
|
|
861
|
+
port: 8889,
|
|
862
|
+
path: daneelPath,
|
|
863
|
+
method,
|
|
864
|
+
timeout: 10000,
|
|
865
|
+
headers: {
|
|
866
|
+
'Content-Type': 'application/json',
|
|
867
|
+
...(requestBody.length > 0 ? { 'Content-Length': requestBody.length.toString() } : {}),
|
|
868
|
+
},
|
|
869
|
+
}, (proxyRes) => {
|
|
870
|
+
let body = '';
|
|
871
|
+
proxyRes.on('data', (chunk) => { body += chunk.toString(); });
|
|
872
|
+
proxyRes.on('end', () => {
|
|
873
|
+
res.writeHead(proxyRes.statusCode || 200, {
|
|
874
|
+
'Content-Type': 'application/json',
|
|
875
|
+
...corsHeaders,
|
|
876
|
+
});
|
|
877
|
+
res.end(body);
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
proxyReq.on('error', () => {
|
|
881
|
+
res.writeHead(503, { 'Content-Type': 'application/json', ...corsHeaders });
|
|
882
|
+
res.end(JSON.stringify({ error: 'Daneel not running' }));
|
|
883
|
+
});
|
|
884
|
+
proxyReq.on('timeout', () => {
|
|
885
|
+
proxyReq.destroy();
|
|
886
|
+
res.writeHead(503, { 'Content-Type': 'application/json', ...corsHeaders });
|
|
887
|
+
res.end(JSON.stringify({ error: 'Daneel not running' }));
|
|
888
|
+
});
|
|
889
|
+
if (requestBody.length > 0) {
|
|
890
|
+
proxyReq.write(requestBody);
|
|
891
|
+
}
|
|
892
|
+
proxyReq.end();
|
|
893
|
+
});
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
944
896
|
// Serve static files
|
|
945
897
|
let filePath = join(uiDistPath, url === '/' ? 'index.html' : url);
|
|
946
898
|
// Handle SPA routing - serve index.html for non-file paths
|
|
@@ -1000,6 +952,162 @@ program
|
|
|
1000
952
|
});
|
|
1001
953
|
});
|
|
1002
954
|
// ============================================================================
|
|
955
|
+
// foundation vault
|
|
956
|
+
// ============================================================================
|
|
957
|
+
const vaultCommand = program
|
|
958
|
+
.command('vault')
|
|
959
|
+
.description('Sync Gaia memories to an Obsidian vault');
|
|
960
|
+
vaultCommand
|
|
961
|
+
.command('init <path>')
|
|
962
|
+
.description('Initialize vault folder structure at <path>')
|
|
963
|
+
.option('--claude-memory <path>', 'Path to Claude Code memory directory')
|
|
964
|
+
.action(async (vaultPath, opts) => {
|
|
965
|
+
const spinner = ora('Initializing vault...').start();
|
|
966
|
+
try {
|
|
967
|
+
// Resolve to absolute path
|
|
968
|
+
const resolvedPath = vaultPath.startsWith('/')
|
|
969
|
+
? vaultPath
|
|
970
|
+
: join(process.cwd(), vaultPath);
|
|
971
|
+
// Warn if path doesn't look like an Obsidian vault
|
|
972
|
+
if (!existsSync(resolvedPath)) {
|
|
973
|
+
spinner.fail(`Path does not exist: ${resolvedPath}`);
|
|
974
|
+
process.exit(1);
|
|
975
|
+
}
|
|
976
|
+
const obsidianDir = join(resolvedPath, '.obsidian');
|
|
977
|
+
if (!existsSync(obsidianDir)) {
|
|
978
|
+
spinner.warn('No .obsidian/ directory found — path may not be an Obsidian vault. Continuing anyway.');
|
|
979
|
+
}
|
|
980
|
+
// Save config
|
|
981
|
+
const config = {
|
|
982
|
+
vaultPath: resolvedPath,
|
|
983
|
+
};
|
|
984
|
+
if (opts.claudeMemory) {
|
|
985
|
+
config.claudeMemoryPath = opts.claudeMemory;
|
|
986
|
+
}
|
|
987
|
+
saveVaultConfig(config);
|
|
988
|
+
// Create folder structure + dashboard
|
|
989
|
+
const vaultConfig = buildVaultConfig({ vaultPath: resolvedPath });
|
|
990
|
+
const vault = new VaultSync(vaultConfig);
|
|
991
|
+
await vault.init();
|
|
992
|
+
spinner.succeed('Vault initialized!');
|
|
993
|
+
console.log();
|
|
994
|
+
console.log(chalk.green('✓') + ' Vault path: ' + chalk.cyan(resolvedPath));
|
|
995
|
+
console.log(chalk.green('✓') + ' Config saved: ' + chalk.gray(VAULT_CONFIG_FILE));
|
|
996
|
+
if (opts.claudeMemory) {
|
|
997
|
+
console.log(chalk.green('✓') + ' Claude memory: ' + chalk.cyan(opts.claudeMemory));
|
|
998
|
+
}
|
|
999
|
+
console.log();
|
|
1000
|
+
console.log(chalk.bold('Folders created:'));
|
|
1001
|
+
console.log(' ' + chalk.cyan('Foundation/Global'));
|
|
1002
|
+
console.log(' ' + chalk.cyan('Foundation/Projects'));
|
|
1003
|
+
console.log(' ' + chalk.cyan('Foundation/Sessions'));
|
|
1004
|
+
console.log(' ' + chalk.cyan('Foundation/Notes'));
|
|
1005
|
+
console.log(' ' + chalk.cyan('Foundation/Observations'));
|
|
1006
|
+
console.log(' ' + chalk.cyan('Knowledge'));
|
|
1007
|
+
console.log(' ' + chalk.cyan('Foundation/_dashboard.md'));
|
|
1008
|
+
console.log();
|
|
1009
|
+
console.log('Run ' + chalk.cyan('foundation vault sync') + ' to populate the vault.');
|
|
1010
|
+
}
|
|
1011
|
+
catch (error) {
|
|
1012
|
+
spinner.fail('Failed to initialize vault');
|
|
1013
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
1014
|
+
process.exit(1);
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
vaultCommand
|
|
1018
|
+
.command('sync')
|
|
1019
|
+
.description('Sync Gaia memories to the vault')
|
|
1020
|
+
.option('--incremental', 'Only write memories whose content has changed since last sync')
|
|
1021
|
+
.action(async (opts) => {
|
|
1022
|
+
const spinner = ora('Syncing memories to vault...').start();
|
|
1023
|
+
let vaultConfig;
|
|
1024
|
+
try {
|
|
1025
|
+
vaultConfig = buildVaultConfig();
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
spinner.fail(error instanceof Error ? error.message : String(error));
|
|
1029
|
+
process.exit(1);
|
|
1030
|
+
}
|
|
1031
|
+
try {
|
|
1032
|
+
const vault = new VaultSync(vaultConfig);
|
|
1033
|
+
if (opts.incremental) {
|
|
1034
|
+
spinner.text = 'Running incremental sync...';
|
|
1035
|
+
const result = await vault.syncIncremental();
|
|
1036
|
+
spinner.succeed('Incremental sync complete');
|
|
1037
|
+
console.log();
|
|
1038
|
+
console.log(chalk.green(' created ') + result.created);
|
|
1039
|
+
console.log(chalk.blue(' updated ') + result.updated);
|
|
1040
|
+
console.log(chalk.red(' deleted ') + result.deleted);
|
|
1041
|
+
console.log(chalk.gray(' skipped ') + result.skipped);
|
|
1042
|
+
if (result.errors.length > 0) {
|
|
1043
|
+
console.log();
|
|
1044
|
+
console.log(chalk.yellow(` ${result.errors.length} error(s):`));
|
|
1045
|
+
for (const err of result.errors) {
|
|
1046
|
+
console.log(chalk.red(' ' + err));
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
spinner.text = 'Running full sync...';
|
|
1052
|
+
const result = await vault.sync();
|
|
1053
|
+
spinner.succeed('Full sync complete');
|
|
1054
|
+
console.log();
|
|
1055
|
+
console.log(chalk.green(' created ') + result.created);
|
|
1056
|
+
console.log(chalk.blue(' updated ') + result.updated);
|
|
1057
|
+
console.log(chalk.red(' deleted ') + result.deleted);
|
|
1058
|
+
console.log(chalk.gray(' skipped ') + result.skipped);
|
|
1059
|
+
if (result.errors.length > 0) {
|
|
1060
|
+
console.log();
|
|
1061
|
+
console.log(chalk.yellow(` ${result.errors.length} error(s):`));
|
|
1062
|
+
for (const err of result.errors) {
|
|
1063
|
+
console.log(chalk.red(' ' + err));
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
spinner.fail('Sync failed');
|
|
1070
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
1071
|
+
process.exit(1);
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
vaultCommand
|
|
1075
|
+
.command('status')
|
|
1076
|
+
.description('Show vault sync status')
|
|
1077
|
+
.action(() => {
|
|
1078
|
+
const config = loadVaultConfig();
|
|
1079
|
+
if (!config) {
|
|
1080
|
+
console.log(chalk.yellow('Not initialized. Run: foundation vault init <path>'));
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
console.log(chalk.bold('Vault Status'));
|
|
1084
|
+
console.log();
|
|
1085
|
+
console.log(' Vault path: ' + chalk.cyan(config.vaultPath));
|
|
1086
|
+
if (config.claudeMemoryPath) {
|
|
1087
|
+
console.log(' Claude memory: ' + chalk.cyan(config.claudeMemoryPath));
|
|
1088
|
+
}
|
|
1089
|
+
try {
|
|
1090
|
+
const vaultConfig = buildVaultConfig();
|
|
1091
|
+
const vault = new VaultSync(vaultConfig);
|
|
1092
|
+
const state = vault.getStatus();
|
|
1093
|
+
if (!state) {
|
|
1094
|
+
console.log(' Last sync: ' + chalk.gray('Never'));
|
|
1095
|
+
console.log(' Memories: ' + chalk.gray('0'));
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
const lastSync = new Date(state.last_full_sync);
|
|
1099
|
+
const isNever = lastSync.getTime() === new Date(0).getTime();
|
|
1100
|
+
const memoriesCount = Object.keys(state.synced_memories).length;
|
|
1101
|
+
console.log(' Last sync: ' +
|
|
1102
|
+
(isNever ? chalk.gray('Never') : chalk.cyan(lastSync.toLocaleString())));
|
|
1103
|
+
console.log(' Memories: ' + chalk.cyan(String(memoriesCount)));
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
console.log(' ' + chalk.red('Error reading sync state: ' + (error instanceof Error ? error.message : String(error))));
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
// ============================================================================
|
|
1003
1111
|
// Parse and run
|
|
1004
1112
|
// ============================================================================
|
|
1005
1113
|
program.parse();
|