clawvault 1.11.2 → 2.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 (52) hide show
  1. package/README.md +135 -1
  2. package/bin/clawvault.js +51 -1252
  3. package/bin/command-registration.test.js +148 -0
  4. package/bin/command-runtime.js +42 -0
  5. package/bin/command-runtime.test.js +102 -0
  6. package/bin/help-contract.test.js +23 -0
  7. package/bin/register-core-commands.js +139 -0
  8. package/bin/register-maintenance-commands.js +137 -0
  9. package/bin/register-query-commands.js +225 -0
  10. package/bin/register-resilience-commands.js +147 -0
  11. package/bin/register-session-lifecycle-commands.js +204 -0
  12. package/bin/register-template-commands.js +72 -0
  13. package/bin/register-vault-operations-commands.js +295 -0
  14. package/bin/test-helpers/cli-command-fixtures.js +94 -0
  15. package/dashboard/lib/graph-diff.js +3 -1
  16. package/dashboard/lib/graph-diff.test.js +19 -0
  17. package/dashboard/lib/vault-parser.js +330 -26
  18. package/dashboard/lib/vault-parser.test.js +191 -11
  19. package/dashboard/public/app.js +22 -9
  20. package/dist/chunk-MXSSG3QU.js +42 -0
  21. package/dist/chunk-O5V7SD5C.js +398 -0
  22. package/dist/chunk-PAYUH64O.js +284 -0
  23. package/dist/{chunk-3HFB7EMU.js → chunk-QFBKWDYR.js} +12 -0
  24. package/dist/{chunk-UBRYOIII.js → chunk-TBVI4N53.js} +210 -21
  25. package/dist/chunk-TXO34J3O.js +56 -0
  26. package/dist/commands/compat.d.ts +28 -0
  27. package/dist/commands/compat.js +10 -0
  28. package/dist/commands/context.d.ts +2 -33
  29. package/dist/commands/context.js +3 -2
  30. package/dist/commands/doctor.js +61 -3
  31. package/dist/commands/entities.d.ts +1 -0
  32. package/dist/commands/entities.js +4 -4
  33. package/dist/commands/graph.d.ts +21 -0
  34. package/dist/commands/graph.js +10 -0
  35. package/dist/commands/link.d.ts +1 -0
  36. package/dist/commands/link.js +14 -5
  37. package/dist/commands/sleep.js +7 -6
  38. package/dist/commands/status.d.ts +6 -0
  39. package/dist/commands/status.js +63 -3
  40. package/dist/commands/wake.js +5 -4
  41. package/dist/context-COo8oq1k.d.ts +45 -0
  42. package/dist/index.d.ts +63 -2
  43. package/dist/index.js +53 -15
  44. package/dist/lib/config.d.ts +6 -1
  45. package/dist/lib/config.js +7 -3
  46. package/hooks/clawvault/HOOK.md +6 -1
  47. package/hooks/clawvault/handler.js +44 -3
  48. package/hooks/clawvault/handler.test.js +161 -0
  49. package/package.json +34 -2
  50. package/dashboard/public/graph.js +0 -376
  51. package/dashboard/public/style.css +0 -154
  52. package/dist/chunk-4KDZZW4X.js +0 -13
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Query and context command registrations.
3
+ */
4
+
5
+ export function registerQueryCommands(
6
+ program,
7
+ {
8
+ chalk,
9
+ getVault,
10
+ resolveVaultPath,
11
+ QmdUnavailableError,
12
+ printQmdMissing
13
+ }
14
+ ) {
15
+ // === SEARCH ===
16
+ program
17
+ .command('search <query>')
18
+ .description('Search the vault via qmd (BM25)')
19
+ .option('-n, --limit <n>', 'Max results', '10')
20
+ .option('-c, --category <category>', 'Filter by category')
21
+ .option('--tags <tags>', 'Filter by tags (comma-separated)')
22
+ .option('--recent', 'Boost recent documents')
23
+ .option('--full', 'Include full content in results')
24
+ .option('-v, --vault <path>', 'Vault path')
25
+ .option('--json', 'Output as JSON')
26
+ .action(async (query, options) => {
27
+ try {
28
+ const vault = await getVault(options.vault);
29
+
30
+ const results = await vault.find(query, {
31
+ limit: parseInt(options.limit, 10),
32
+ category: options.category,
33
+ tags: options.tags?.split(',').map((value) => value.trim()),
34
+ fullContent: options.full,
35
+ temporalBoost: options.recent
36
+ });
37
+
38
+ if (options.json) {
39
+ console.log(JSON.stringify(results, null, 2));
40
+ return;
41
+ }
42
+
43
+ if (results.length === 0) {
44
+ console.log(chalk.yellow('No results found.'));
45
+ return;
46
+ }
47
+
48
+ console.log(chalk.cyan(`\n🔍 Found ${results.length} result(s) for "${query}":\n`));
49
+
50
+ for (const result of results) {
51
+ const scoreBar = '█'.repeat(Math.round(result.score * 10)).padEnd(10, '░');
52
+ console.log(chalk.green(`📄 ${result.document.title}`));
53
+ console.log(chalk.dim(` ${result.document.category}/${result.document.id.split('/').pop()}`));
54
+ console.log(chalk.dim(` Score: ${scoreBar} ${(result.score * 100).toFixed(0)}%`));
55
+ if (result.snippet) {
56
+ console.log(chalk.white(` ${result.snippet.split('\n')[0].slice(0, 80)}...`));
57
+ }
58
+ console.log();
59
+ }
60
+ } catch (err) {
61
+ if (err instanceof QmdUnavailableError) {
62
+ printQmdMissing();
63
+ process.exit(1);
64
+ }
65
+ console.error(chalk.red(`Error: ${err.message}`));
66
+ process.exit(1);
67
+ }
68
+ });
69
+
70
+ // === VSEARCH ===
71
+ program
72
+ .command('vsearch <query>')
73
+ .description('Semantic search via qmd (requires qmd installed)')
74
+ .option('-n, --limit <n>', 'Max results', '5')
75
+ .option('-c, --category <category>', 'Filter by category')
76
+ .option('--tags <tags>', 'Filter by tags (comma-separated)')
77
+ .option('--recent', 'Boost recent documents')
78
+ .option('--full', 'Include full content in results')
79
+ .option('-v, --vault <path>', 'Vault path')
80
+ .option('--json', 'Output as JSON')
81
+ .action(async (query, options) => {
82
+ try {
83
+ const vault = await getVault(options.vault);
84
+
85
+ const results = await vault.vsearch(query, {
86
+ limit: parseInt(options.limit, 10),
87
+ category: options.category,
88
+ tags: options.tags?.split(',').map((value) => value.trim()),
89
+ fullContent: options.full,
90
+ temporalBoost: options.recent
91
+ });
92
+
93
+ if (options.json) {
94
+ console.log(JSON.stringify(results, null, 2));
95
+ return;
96
+ }
97
+
98
+ if (results.length === 0) {
99
+ console.log(chalk.yellow('No results found.'));
100
+ return;
101
+ }
102
+
103
+ console.log(chalk.cyan(`\n🧠 Found ${results.length} result(s) for "${query}":\n`));
104
+
105
+ for (const result of results) {
106
+ const scoreBar = '█'.repeat(Math.round(result.score * 10)).padEnd(10, '░');
107
+ console.log(chalk.green(`📄 ${result.document.title}`));
108
+ console.log(chalk.dim(` ${result.document.category}/${result.document.id.split('/').pop()}`));
109
+ console.log(chalk.dim(` Score: ${scoreBar} ${(result.score * 100).toFixed(0)}%`));
110
+ if (result.snippet) {
111
+ console.log(chalk.white(` ${result.snippet.split('\n')[0].slice(0, 80)}...`));
112
+ }
113
+ console.log();
114
+ }
115
+ } catch (err) {
116
+ if (err instanceof QmdUnavailableError) {
117
+ printQmdMissing();
118
+ process.exit(1);
119
+ }
120
+ console.error(chalk.red(`Error: ${err.message}`));
121
+ process.exit(1);
122
+ }
123
+ });
124
+
125
+ // === CONTEXT ===
126
+ program
127
+ .command('context <task>')
128
+ .description('Generate task-relevant context for prompt injection')
129
+ .option('-n, --limit <n>', 'Max results', '5')
130
+ .option('--format <format>', 'Output format (markdown|json)', 'markdown')
131
+ .option('--recent', 'Boost recent documents (enabled by default)', true)
132
+ .option('--include-observations', 'Include observation memories in output', true)
133
+ .option('--budget <number>', 'Optional token budget for assembled context')
134
+ .option('--profile <profile>', 'Context profile (default|planning|incident|handoff|auto)', 'default')
135
+ .option('-v, --vault <path>', 'Vault path')
136
+ .action(async (task, options) => {
137
+ try {
138
+ const vaultPath = resolveVaultPath(options.vault);
139
+ const format = options.format === 'json' ? 'json' : 'markdown';
140
+ const parsedBudget = options.budget ? Number.parseInt(options.budget, 10) : undefined;
141
+ if (options.budget && (!Number.isFinite(parsedBudget) || parsedBudget <= 0)) {
142
+ throw new Error(`Invalid --budget value: ${options.budget}`);
143
+ }
144
+
145
+ const { contextCommand } = await import('../dist/commands/context.js');
146
+ await contextCommand(task, {
147
+ vaultPath,
148
+ limit: parseInt(options.limit, 10),
149
+ format,
150
+ recent: options.recent,
151
+ includeObservations: options.includeObservations,
152
+ budget: parsedBudget,
153
+ profile: options.profile
154
+ });
155
+ } catch (err) {
156
+ if (err instanceof QmdUnavailableError) {
157
+ printQmdMissing();
158
+ process.exit(1);
159
+ }
160
+ console.error(chalk.red(`Error: ${err.message}`));
161
+ process.exit(1);
162
+ }
163
+ });
164
+
165
+ // === OBSERVE ===
166
+ program
167
+ .command('observe')
168
+ .description('Observe session files and build observational memory')
169
+ .option('--watch <path>', 'Watch session file or directory')
170
+ .option('--threshold <n>', 'Compression token threshold', '30000')
171
+ .option('--reflect-threshold <n>', 'Reflection token threshold', '40000')
172
+ .option('--model <model>', 'LLM model override')
173
+ .option('--compress <file>', 'One-shot compression for a conversation file')
174
+ .option('--daemon', 'Run in detached background mode')
175
+ .option('-v, --vault <path>', 'Vault path')
176
+ .action(async (options) => {
177
+ try {
178
+ const { observeCommand } = await import('../dist/commands/observe.js');
179
+ const threshold = Number.parseInt(options.threshold, 10);
180
+ const reflectThreshold = Number.parseInt(options.reflectThreshold, 10);
181
+ if (Number.isNaN(threshold) || threshold <= 0) {
182
+ throw new Error(`Invalid --threshold value: ${options.threshold}`);
183
+ }
184
+ if (Number.isNaN(reflectThreshold) || reflectThreshold <= 0) {
185
+ throw new Error(`Invalid --reflect-threshold value: ${options.reflectThreshold}`);
186
+ }
187
+
188
+ await observeCommand({
189
+ watch: options.watch,
190
+ threshold,
191
+ reflectThreshold,
192
+ model: options.model,
193
+ compress: options.compress,
194
+ daemon: options.daemon,
195
+ vaultPath: resolveVaultPath(options.vault)
196
+ });
197
+ } catch (err) {
198
+ console.error(chalk.red(`Error: ${err.message}`));
199
+ process.exit(1);
200
+ }
201
+ });
202
+
203
+ // === SESSION-RECAP ===
204
+ program
205
+ .command('session-recap <sessionKey>')
206
+ .description('Generate recap from a specific OpenClaw session transcript')
207
+ .option('-n, --limit <n>', 'Number of messages to include', '15')
208
+ .option('--format <format>', 'Output format (markdown|json)', 'markdown')
209
+ .option('-a, --agent <id>', 'Agent ID (default: OPENCLAW_AGENT_ID or clawdious)')
210
+ .action(async (sessionKey, options) => {
211
+ try {
212
+ const { sessionRecapCommand } = await import('../dist/commands/session-recap.js');
213
+ const format = options.format === 'json' ? 'json' : 'markdown';
214
+ const parsedLimit = Number.parseInt(options.limit, 10);
215
+ await sessionRecapCommand(sessionKey, {
216
+ limit: Number.isNaN(parsedLimit) ? 15 : parsedLimit,
217
+ format,
218
+ agentId: options.agent
219
+ });
220
+ } catch (err) {
221
+ console.error(chalk.red(`Error: ${err.message}`));
222
+ process.exit(1);
223
+ }
224
+ });
225
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Context-resilience and session-repair command registrations.
3
+ */
4
+
5
+ export function registerResilienceCommands(program, { chalk, resolveVaultPath }) {
6
+ // === CHECKPOINT ===
7
+ program
8
+ .command('checkpoint')
9
+ .description('Quick state checkpoint for context death resilience')
10
+ .option('--working-on <text>', 'What you are currently working on')
11
+ .option('--focus <text>', 'Current focus area')
12
+ .option('--blocked <text>', 'What is blocking progress')
13
+ .option('--urgent', 'Trigger OpenClaw wake after checkpoint')
14
+ .option('-v, --vault <path>', 'Vault path')
15
+ .option('--json', 'Output as JSON')
16
+ .action(async (options) => {
17
+ try {
18
+ const { checkpoint } = await import('../dist/commands/checkpoint.js');
19
+ const data = await checkpoint({
20
+ vaultPath: resolveVaultPath(options.vault),
21
+ workingOn: options.workingOn,
22
+ focus: options.focus,
23
+ blocked: options.blocked,
24
+ urgent: options.urgent
25
+ });
26
+
27
+ if (options.json) {
28
+ console.log(JSON.stringify(data, null, 2));
29
+ } else {
30
+ console.log(chalk.green('✓ Checkpoint saved'));
31
+ console.log(chalk.dim(` Timestamp: ${data.timestamp}`));
32
+ if (data.workingOn) console.log(chalk.dim(` Working on: ${data.workingOn}`));
33
+ if (data.focus) console.log(chalk.dim(` Focus: ${data.focus}`));
34
+ if (data.blocked) console.log(chalk.dim(` Blocked: ${data.blocked}`));
35
+ if (data.urgent) console.log(chalk.dim(' Urgent: yes'));
36
+ }
37
+ } catch (err) {
38
+ console.error(chalk.red(`Error: ${err.message}`));
39
+ process.exit(1);
40
+ }
41
+ });
42
+
43
+ // === RECOVER ===
44
+ program
45
+ .command('recover')
46
+ .description('Check for context death and recover state')
47
+ .option('--clear', 'Clear the dirty death flag after recovery')
48
+ .option('--verbose', 'Show full checkpoint and handoff content')
49
+ .option('-v, --vault <path>', 'Vault path')
50
+ .option('--json', 'Output as JSON')
51
+ .action(async (options) => {
52
+ try {
53
+ const { recover, formatRecoveryInfo } = await import('../dist/commands/recover.js');
54
+ const info = await recover(resolveVaultPath(options.vault), {
55
+ clearFlag: options.clear,
56
+ verbose: options.verbose
57
+ });
58
+
59
+ if (options.json) {
60
+ console.log(JSON.stringify(info, null, 2));
61
+ } else {
62
+ console.log(formatRecoveryInfo(info, { verbose: options.verbose }));
63
+ }
64
+ } catch (err) {
65
+ console.error(chalk.red(`Error: ${err.message}`));
66
+ process.exit(1);
67
+ }
68
+ });
69
+
70
+ // === STATUS ===
71
+ program
72
+ .command('status')
73
+ .description('Show vault health and status')
74
+ .option('-v, --vault <path>', 'Vault path')
75
+ .option('--json', 'Output as JSON')
76
+ .action(async (options) => {
77
+ try {
78
+ const { statusCommand } = await import('../dist/commands/status.js');
79
+ await statusCommand(resolveVaultPath(options.vault), { json: options.json });
80
+ } catch (err) {
81
+ console.error(chalk.red(`Error: ${err.message}`));
82
+ process.exit(1);
83
+ }
84
+ });
85
+
86
+ // === CLEAN-EXIT ===
87
+ program
88
+ .command('clean-exit')
89
+ .description('Mark session as cleanly exited (clears dirty death flag)')
90
+ .option('-v, --vault <path>', 'Vault path')
91
+ .action(async (options) => {
92
+ try {
93
+ const { cleanExit } = await import('../dist/commands/checkpoint.js');
94
+ await cleanExit(resolveVaultPath(options.vault));
95
+ console.log(chalk.green('✓ Clean exit recorded'));
96
+ } catch (err) {
97
+ console.error(chalk.red(`Error: ${err.message}`));
98
+ process.exit(1);
99
+ }
100
+ });
101
+
102
+ // === REPAIR-SESSION ===
103
+ program
104
+ .command('repair-session')
105
+ .description('Repair corrupted OpenClaw session transcripts')
106
+ .option('-s, --session <id>', 'Session ID (defaults to current main session)')
107
+ .option('-a, --agent <id>', 'Agent ID (defaults to configured agent)')
108
+ .option('--backup', 'Create backup before repair (default: true)', true)
109
+ .option('--no-backup', 'Skip backup creation')
110
+ .option('--dry-run', 'Show what would be repaired without writing')
111
+ .option('--list', 'List available sessions')
112
+ .option('--json', 'Output as JSON')
113
+ .action(async (options) => {
114
+ try {
115
+ const {
116
+ repairSessionCommand,
117
+ formatRepairResult,
118
+ listAgentSessions
119
+ } = await import('../dist/commands/repair-session.js');
120
+
121
+ if (options.list) {
122
+ console.log(listAgentSessions(options.agent));
123
+ return;
124
+ }
125
+
126
+ const result = await repairSessionCommand({
127
+ sessionId: options.session,
128
+ agentId: options.agent,
129
+ backup: options.backup,
130
+ dryRun: options.dryRun
131
+ });
132
+
133
+ if (options.json) {
134
+ console.log(JSON.stringify(result, null, 2));
135
+ } else {
136
+ console.log(formatRepairResult(result, { dryRun: options.dryRun }));
137
+ }
138
+
139
+ if (result.corruptedEntries.length > 0 && !result.repaired) {
140
+ process.exit(1);
141
+ }
142
+ } catch (err) {
143
+ console.error(chalk.red(`Error: ${err.message}`));
144
+ process.exit(1);
145
+ }
146
+ });
147
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Session lifecycle command registrations (wake/sleep/handoff/recap).
3
+ */
4
+
5
+ export function registerSessionLifecycleCommands(
6
+ program,
7
+ { chalk, resolveVaultPath, QmdUnavailableError, printQmdMissing, getVault, runQmd }
8
+ ) {
9
+ // === WAKE (session start) ===
10
+ program
11
+ .command('wake')
12
+ .description('Start a session (recover + recap + summary)')
13
+ .option('-n, --handoff-limit <n>', 'Number of recent handoffs to include', '3')
14
+ .option('--full', 'Show full recap (default: brief)')
15
+ .option('-v, --vault <path>', 'Vault path')
16
+ .action(async (options) => {
17
+ try {
18
+ const vaultPath = resolveVaultPath(options.vault);
19
+ const { wake } = await import('../dist/commands/wake.js');
20
+ const { formatRecoveryInfo } = await import('../dist/commands/recover.js');
21
+ const result = await wake({
22
+ vaultPath,
23
+ handoffLimit: parseInt(options.handoffLimit, 10),
24
+ brief: !options.full
25
+ });
26
+
27
+ console.log(chalk.cyan('\n🌅 ClawVault Wake\n'));
28
+ console.log(formatRecoveryInfo(result.recovery));
29
+ console.log();
30
+ console.log(chalk.cyan('Recap'));
31
+ console.log(result.recapMarkdown.trim());
32
+ console.log();
33
+ console.log(chalk.green(`You were working on: ${result.summary}`));
34
+
35
+ process.exitCode = result.recovery.died ? 1 : 0;
36
+ } catch (err) {
37
+ if (err instanceof QmdUnavailableError) {
38
+ printQmdMissing();
39
+ process.exit(1);
40
+ }
41
+ console.error(chalk.red(`Error: ${err.message}`));
42
+ process.exit(1);
43
+ }
44
+ });
45
+
46
+ // === SLEEP (session end) ===
47
+ program
48
+ .command('sleep <summary>')
49
+ .description('End a session with a handoff (and optional git commit)')
50
+ .option('-n, --next <items>', 'Next steps (comma-separated)')
51
+ .option('-b, --blocked <items>', 'Blocked items (comma-separated)')
52
+ .option('-d, --decisions <items>', 'Key decisions made (comma-separated)')
53
+ .option('-q, --questions <items>', 'Open questions (comma-separated)')
54
+ .option('-f, --feeling <state>', 'Emotional/energy state')
55
+ .option('-s, --session <key>', 'Session key')
56
+ .option('--session-transcript <path>', 'Session transcript path for auto-observe')
57
+ .option('--index', 'Update qmd index after handoff')
58
+ .option('--no-git', 'Skip git commit prompt')
59
+ .option('-v, --vault <path>', 'Vault path')
60
+ .action(async (summary, options) => {
61
+ try {
62
+ const vaultPath = resolveVaultPath(options.vault);
63
+ const { sleep } = await import('../dist/commands/sleep.js');
64
+ const result = await sleep({
65
+ workingOn: summary,
66
+ next: options.next,
67
+ blocked: options.blocked,
68
+ decisions: options.decisions,
69
+ questions: options.questions,
70
+ feeling: options.feeling,
71
+ sessionKey: options.session,
72
+ sessionTranscript: options.sessionTranscript,
73
+ vaultPath,
74
+ index: options.index,
75
+ git: options.git
76
+ });
77
+
78
+ console.log(chalk.green(`✓ Handoff saved: ${result.document.id}`));
79
+ console.log(chalk.dim(` Path: ${result.document.path}`));
80
+ console.log(chalk.dim(` Working on: ${result.handoff.workingOn.join(', ')}`));
81
+ if (result.handoff.nextSteps.length > 0) {
82
+ console.log(chalk.dim(` Next: ${result.handoff.nextSteps.join(', ')}`));
83
+ } else {
84
+ console.log(chalk.dim(' Next: (none)'));
85
+ }
86
+ if (result.handoff.blocked.length > 0) {
87
+ console.log(chalk.dim(` Blocked: ${result.handoff.blocked.join(', ')}`));
88
+ } else {
89
+ console.log(chalk.dim(' Blocked: (none)'));
90
+ }
91
+ if (result.handoff.decisions?.length) {
92
+ console.log(chalk.dim(` Decisions: ${result.handoff.decisions.join(', ')}`));
93
+ }
94
+ if (result.handoff.openQuestions?.length) {
95
+ console.log(chalk.dim(` Questions: ${result.handoff.openQuestions.join(', ')}`));
96
+ }
97
+ if (result.handoff.feeling) {
98
+ console.log(chalk.dim(` Feeling: ${result.handoff.feeling}`));
99
+ }
100
+ if (options.index) {
101
+ console.log(chalk.dim(' qmd: index updated'));
102
+ }
103
+ if (result.git) {
104
+ if (result.git.committed) {
105
+ console.log(chalk.green(`✓ Git commit created${result.git.message ? `: ${result.git.message}` : ''}`));
106
+ } else if (result.git.skippedReason === 'clean') {
107
+ console.log(chalk.dim(' Git: clean'));
108
+ } else if (result.git.skippedReason === 'declined') {
109
+ console.log(chalk.dim(' Git: commit skipped'));
110
+ }
111
+ }
112
+ if (result.observationRoutingSummary) {
113
+ console.log(chalk.dim(` Observe: ${result.observationRoutingSummary}`));
114
+ }
115
+ } catch (err) {
116
+ if (err instanceof QmdUnavailableError) {
117
+ printQmdMissing();
118
+ process.exit(1);
119
+ }
120
+ console.error(chalk.red(`Error: ${err.message}`));
121
+ process.exit(1);
122
+ }
123
+ });
124
+
125
+ // === HANDOFF (session bridge) ===
126
+ program
127
+ .command('handoff')
128
+ .description('Create a session handoff document')
129
+ .requiredOption('-w, --working-on <items>', 'What I was working on (comma-separated)')
130
+ .option('-b, --blocked <items>', 'What is blocked (comma-separated)')
131
+ .option('-n, --next <items>', 'What comes next (comma-separated)')
132
+ .option('-d, --decisions <items>', 'Key decisions made (comma-separated)')
133
+ .option('-q, --questions <items>', 'Open questions (comma-separated)')
134
+ .option('-f, --feeling <state>', 'Emotional/energy state')
135
+ .option('-s, --session <key>', 'Session key')
136
+ .option('-v, --vault <path>', 'Vault path')
137
+ .option('--no-index', 'Skip qmd index update (auto-updates by default)')
138
+ .option('--json', 'Output as JSON')
139
+ .action(async (options) => {
140
+ try {
141
+ const vault = await getVault(options.vault);
142
+
143
+ const handoff = {
144
+ workingOn: options.workingOn.split(',').map((item) => item.trim()),
145
+ blocked: options.blocked ? options.blocked.split(',').map((item) => item.trim()) : [],
146
+ nextSteps: options.next ? options.next.split(',').map((item) => item.trim()) : [],
147
+ decisions: options.decisions ? options.decisions.split(',').map((item) => item.trim()) : undefined,
148
+ openQuestions: options.questions ? options.questions.split(',').map((item) => item.trim()) : undefined,
149
+ feeling: options.feeling,
150
+ sessionKey: options.session
151
+ };
152
+
153
+ const doc = await vault.createHandoff(handoff);
154
+
155
+ if (!options.json) {
156
+ console.log(chalk.green(`✓ Handoff created: ${doc.id}`));
157
+ console.log(chalk.dim(` Path: ${doc.path}`));
158
+ }
159
+
160
+ if (options.index !== false) {
161
+ const collection = vault.getQmdCollection();
162
+ await runQmd(collection ? ['update', '-c', collection] : ['update']);
163
+ }
164
+
165
+ if (options.json) {
166
+ console.log(JSON.stringify({ id: doc.id, path: doc.path, handoff }, null, 2));
167
+ }
168
+ } catch (err) {
169
+ console.error(chalk.red(`Error: ${err.message}`));
170
+ process.exit(1);
171
+ }
172
+ });
173
+
174
+ // === RECAP (session bootstrap) ===
175
+ program
176
+ .command('recap')
177
+ .description('Generate a session recap - who I was (bootstrap hook)')
178
+ .option('-n, --handoff-limit <n>', 'Number of recent handoffs to include', '3')
179
+ .option('-v, --vault <path>', 'Vault path')
180
+ .option('--json', 'Output as JSON')
181
+ .option('--markdown', 'Output as markdown (default)')
182
+ .option('--brief', 'Minimal output for token savings')
183
+ .action(async (options) => {
184
+ try {
185
+ const vault = await getVault(options.vault);
186
+
187
+ const recap = await vault.generateRecap({
188
+ handoffLimit: parseInt(options.handoffLimit, 10),
189
+ brief: options.brief
190
+ });
191
+
192
+ if (options.json) {
193
+ console.log(JSON.stringify(recap, null, 2));
194
+ return;
195
+ }
196
+
197
+ const markdown = vault.formatRecap(recap, { brief: options.brief });
198
+ console.log(markdown);
199
+ } catch (err) {
200
+ console.error(chalk.red(`Error: ${err.message}`));
201
+ process.exit(1);
202
+ }
203
+ });
204
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Template command registrations split from main CLI entrypoint.
3
+ */
4
+
5
+ export function registerTemplateCommands(program, { chalk }) {
6
+ const template = program
7
+ .command('template')
8
+ .description('Manage templates');
9
+
10
+ template
11
+ .command('list')
12
+ .description('List available templates')
13
+ .option('-v, --vault <path>', 'Vault path')
14
+ .action(async (options) => {
15
+ try {
16
+ const { listTemplates } = await import('../dist/commands/template.js');
17
+ const templates = listTemplates({ vaultPath: options.vault });
18
+ if (templates.length === 0) {
19
+ console.log(chalk.yellow('No templates found.'));
20
+ return;
21
+ }
22
+ console.log(chalk.cyan('\n📄 Templates:\n'));
23
+ for (const name of templates) {
24
+ console.log(`- ${name}`);
25
+ }
26
+ console.log();
27
+ } catch (err) {
28
+ console.error(chalk.red(`Error: ${err.message}`));
29
+ process.exit(1);
30
+ }
31
+ });
32
+
33
+ template
34
+ .command('create <name>')
35
+ .description('Create a file from a template')
36
+ .option('-t, --title <title>', 'Document title')
37
+ .option('-v, --vault <path>', 'Vault path')
38
+ .action(async (name, options) => {
39
+ try {
40
+ const { createFromTemplate } = await import('../dist/commands/template.js');
41
+ const result = createFromTemplate(name, {
42
+ title: options.title,
43
+ vaultPath: options.vault
44
+ });
45
+ console.log(chalk.green(`✓ Created from template: ${name}`));
46
+ console.log(chalk.dim(` Output: ${result.outputPath}`));
47
+ } catch (err) {
48
+ console.error(chalk.red(`Error: ${err.message}`));
49
+ process.exit(1);
50
+ }
51
+ });
52
+
53
+ template
54
+ .command('add <file>')
55
+ .description('Add a custom template')
56
+ .requiredOption('--name <name>', 'Template name')
57
+ .option('-v, --vault <path>', 'Vault path')
58
+ .action(async (file, options) => {
59
+ try {
60
+ const { addTemplate } = await import('../dist/commands/template.js');
61
+ const result = addTemplate(file, {
62
+ name: options.name,
63
+ vaultPath: options.vault
64
+ });
65
+ console.log(chalk.green(`✓ Template added: ${result.name}`));
66
+ console.log(chalk.dim(` Path: ${result.templatePath}`));
67
+ } catch (err) {
68
+ console.error(chalk.red(`Error: ${err.message}`));
69
+ process.exit(1);
70
+ }
71
+ });
72
+ }