hafez 1.0.1

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -0
  3. package/dist/cli/batch-errors.d.ts +24 -0
  4. package/dist/cli/batch-errors.js +236 -0
  5. package/dist/cli/commands-schema.d.ts +3 -0
  6. package/dist/cli/commands-schema.js +80 -0
  7. package/dist/cli/commands.d.ts +133 -0
  8. package/dist/cli/commands.js +628 -0
  9. package/dist/cli/format.d.ts +9 -0
  10. package/dist/cli/format.js +158 -0
  11. package/dist/cli/help-agent.d.ts +6 -0
  12. package/dist/cli/help-agent.js +158 -0
  13. package/dist/cli/index.d.ts +1 -0
  14. package/dist/cli/index.js +304 -0
  15. package/dist/cli/resolve-vault.d.ts +13 -0
  16. package/dist/cli/resolve-vault.js +124 -0
  17. package/dist/cli/schema-introspect.d.ts +70 -0
  18. package/dist/cli/schema-introspect.js +372 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +3 -0
  21. package/dist/contracts.d.ts +26 -0
  22. package/dist/contracts.js +87 -0
  23. package/dist/db.d.ts +68 -0
  24. package/dist/db.js +663 -0
  25. package/dist/digest.d.ts +29 -0
  26. package/dist/digest.js +94 -0
  27. package/dist/export-okf.d.ts +12 -0
  28. package/dist/export-okf.js +176 -0
  29. package/dist/git.d.ts +12 -0
  30. package/dist/git.js +355 -0
  31. package/dist/index.d.ts +7 -0
  32. package/dist/index.js +909 -0
  33. package/dist/knowledge-index.d.ts +3 -0
  34. package/dist/knowledge-index.js +135 -0
  35. package/dist/merge.d.ts +1 -0
  36. package/dist/merge.js +176 -0
  37. package/dist/migrate-knowledge-v2.d.ts +11 -0
  38. package/dist/migrate-knowledge-v2.js +158 -0
  39. package/dist/migrate-next-actions.d.ts +6 -0
  40. package/dist/migrate-next-actions.js +32 -0
  41. package/dist/migrate-types.d.ts +11 -0
  42. package/dist/migrate-types.js +142 -0
  43. package/dist/parser.d.ts +18 -0
  44. package/dist/parser.js +68 -0
  45. package/dist/query.d.ts +24 -0
  46. package/dist/query.js +167 -0
  47. package/dist/schema.d.ts +9 -0
  48. package/dist/schema.js +59 -0
  49. package/dist/sections.d.ts +54 -0
  50. package/dist/sections.js +297 -0
  51. package/dist/templates.d.ts +8 -0
  52. package/dist/templates.js +39 -0
  53. package/dist/types.d.ts +321 -0
  54. package/dist/types.js +11 -0
  55. package/dist/vault.d.ts +12 -0
  56. package/dist/vault.js +45 -0
  57. package/package.json +67 -0
  58. package/skill/SKILL.md +190 -0
@@ -0,0 +1,158 @@
1
+ export function formatEntityHeader(fm, body) {
2
+ const lines = [`# ${fm.name}`];
3
+ if (fm.description)
4
+ lines.push(`> ${fm.description}`);
5
+ const meta = [];
6
+ if (fm.status)
7
+ meta.push(`status: ${fm.status}`);
8
+ if (fm.type)
9
+ meta.push(`type: ${fm.type}`);
10
+ if (fm.domain?.length)
11
+ meta.push(`domain: ${fm.domain.join(', ')}`);
12
+ if (meta.length)
13
+ lines.push(meta.join(' | '));
14
+ const meta2 = [];
15
+ if (fm.parent)
16
+ meta2.push(`parent: ${fm.parent}`);
17
+ if (fm['last-touched'])
18
+ meta2.push(`last-touched: ${fm['last-touched']}`);
19
+ if (fm.created)
20
+ meta2.push(`created: ${fm.created}`);
21
+ if (meta2.length)
22
+ lines.push(meta2.join(' | '));
23
+ if (fm.related?.length)
24
+ lines.push(`related: ${fm.related.join(', ')}`);
25
+ if (fm.tags?.length)
26
+ lines.push(`tags: ${fm.tags.join(', ')}`);
27
+ if (fm.resource)
28
+ lines.push(`resource: ${fm.resource}`);
29
+ lines.push('');
30
+ lines.push(body);
31
+ return lines.join('\n');
32
+ }
33
+ export function formatKnowledgeHeader(fm, body) {
34
+ const lines = [`# ${fm.name}`];
35
+ if (fm.description)
36
+ lines.push(`> ${fm.description}`);
37
+ const meta = [];
38
+ if (fm.confidence)
39
+ meta.push(`confidence: ${fm.confidence}`);
40
+ if (fm.domain?.length)
41
+ meta.push(`domain: ${Array.isArray(fm.domain) ? fm.domain.join(', ') : fm.domain}`);
42
+ if (meta.length)
43
+ lines.push(meta.join(' | '));
44
+ const meta2 = [];
45
+ if (fm['reinforcement-count'] != null)
46
+ meta2.push(`reinforcement-count: ${fm['reinforcement-count']}`);
47
+ if (fm['last-reinforced'])
48
+ meta2.push(`last-reinforced: ${fm['last-reinforced']}`);
49
+ if (meta2.length)
50
+ lines.push(meta2.join(' | '));
51
+ if (fm.related?.length)
52
+ lines.push(`related: ${fm.related.join(', ')}`);
53
+ lines.push('');
54
+ lines.push(body);
55
+ return lines.join('\n');
56
+ }
57
+ export function formatQueryTable(results, total) {
58
+ if (results.length === 0)
59
+ return 'No results.';
60
+ const lines = ['| slug | name | type | status | next-action |', '|------|------|------|--------|-------------|'];
61
+ for (const r of results) {
62
+ let actionDisplay = r.next_action ?? '';
63
+ if (r.next_action && r.next_action_count > 1) {
64
+ actionDisplay = `${r.next_action} (+${r.next_action_count - 1} more)`;
65
+ }
66
+ lines.push(`| ${r.slug} | ${r.name} | ${r.type} | ${r.status} | ${actionDisplay} |`);
67
+ }
68
+ if (total != null && results.length < total) {
69
+ lines.push(`\nShowing ${results.length} of ${total}`);
70
+ }
71
+ return lines.join('\n');
72
+ }
73
+ export function formatKnowledgeTable(results, total) {
74
+ if (results.length === 0)
75
+ return 'No results.';
76
+ const lines = ['| slug | name | confidence | domains | reinforcements |', '|------|------|------------|---------|----------------|'];
77
+ for (const r of results) {
78
+ lines.push(`| ${r.slug} | ${r.name} | ${r.confidence} | ${r.domain.join(', ')} | ${r.reinforcement_count} |`);
79
+ }
80
+ if (total != null && results.length < total) {
81
+ lines.push(`\nShowing ${results.length} of ${total}`);
82
+ }
83
+ return lines.join('\n');
84
+ }
85
+ export function formatSearchResults(results) {
86
+ if (results.length === 0)
87
+ return 'No results.';
88
+ const entities = results.filter(r => r.kind === 'entity');
89
+ const knowledge = results.filter(r => r.kind === 'knowledge');
90
+ const sections = [];
91
+ if (entities.length > 0) {
92
+ const lines = ['## Entities', '', '| slug | name | type | match |', '|------|------|------|-------|'];
93
+ for (const r of entities)
94
+ lines.push(`| ${r.slug} | ${r.name} | ${r.type ?? ''} | ${r.snippet} |`);
95
+ sections.push(lines.join('\n'));
96
+ }
97
+ if (knowledge.length > 0) {
98
+ const lines = ['## Knowledge', '', '| slug | name | confidence | match |', '|------|------|------------|-------|'];
99
+ for (const r of knowledge)
100
+ lines.push(`| ${r.slug} | ${r.name} | ${r.confidence ?? ''} | ${r.snippet} |`);
101
+ sections.push(lines.join('\n'));
102
+ }
103
+ return sections.join('\n\n');
104
+ }
105
+ export function formatStats(stats) {
106
+ const lines = [];
107
+ lines.push('## Status Counts');
108
+ lines.push(`Active: ${stats.counts.active} | Paused: ${stats.counts.paused} | Done: ${stats.counts.done}`);
109
+ lines.push('\n## Type Counts');
110
+ lines.push(`Project: ${stats.by_type.project} | Entity: ${stats.by_type.entity} | Capture: ${stats.by_type.capture}`);
111
+ if (stats.stale.length > 0) {
112
+ lines.push('\n## Stale');
113
+ lines.push('| Slug | Type | Days |');
114
+ lines.push('|------|------|------|');
115
+ for (const s of stats.stale)
116
+ lines.push(`| ${s.slug} | ${s.type} | ${s.days_since_touched} |`);
117
+ }
118
+ if (stats.no_next_action.length > 0) {
119
+ lines.push('\n## Active Without Next Actions');
120
+ for (const s of stats.no_next_action)
121
+ lines.push(`- ${s.slug} (${s.type})`);
122
+ }
123
+ lines.push('\n## Recently Touched');
124
+ for (const s of stats.recently_touched)
125
+ lines.push(`- ${s.slug} (${s.last_touched})`);
126
+ lines.push('\n## Recently Created');
127
+ for (const s of stats.recently_created)
128
+ lines.push(`- ${s.slug} (${s.created})`);
129
+ return lines.join('\n');
130
+ }
131
+ export function formatChangelog(entries) {
132
+ if (entries.length === 0)
133
+ return 'No changes found.';
134
+ const header = '| Slug | Kind | Op | Date | Message |';
135
+ const sep = '|------|------|-----|------|---------|';
136
+ const rows = entries.map(e => `| ${e.slug} | ${e.kind} | ${e.operation} | ${e.timestamp.slice(0, 10)} | ${e.commit_message} |`);
137
+ return [header, sep, ...rows].join('\n');
138
+ }
139
+ export function formatValidation(report) {
140
+ const issues = report.broken_slugs.length + report.orphaned_knowledge.length +
141
+ report.oversized_related.length + report.missing_fields.length;
142
+ if (issues === 0) {
143
+ return `Vault OK — ${report.total_entities} entities, ${report.total_knowledge} knowledge, 0 issues`;
144
+ }
145
+ const sections = [`## Vault Validation — ${report.total_entities} entities, ${report.total_knowledge} knowledge`];
146
+ const addSection = (title, items) => {
147
+ if (items.length === 0)
148
+ return;
149
+ sections.push(`\n### ${title} (${items.length})`);
150
+ for (const item of items)
151
+ sections.push(`- ${item}`);
152
+ };
153
+ addSection('Broken Slugs', report.broken_slugs.map(i => `${i.slug}: ${i.issue}`));
154
+ addSection('Orphaned Knowledge', report.orphaned_knowledge.map(s => `${s} (no related links, not referenced by any entity)`));
155
+ addSection('Oversized Related', report.oversized_related.map(i => `${i.slug}: ${i.issue}`));
156
+ addSection('Missing Fields', report.missing_fields.map(i => `${i.slug}: ${i.issue}`));
157
+ return sections.join('\n');
158
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Render the full prompt-gate output. Token budget: 1500-3000 tokens when
3
+ * measured as char/3.5. The drift test in tests/cli-help-agent.test.ts
4
+ * enforces the ceiling.
5
+ */
6
+ export declare function renderAgentHelp(): string;
@@ -0,0 +1,158 @@
1
+ // src/cli/help-agent.ts
2
+ //
3
+ // `hafez help --agent` — the prompt-gate. Plain text, no ANSI, no markdown
4
+ // decoration. Designed for an LLM with zero Hafez context to read once per
5
+ // session and then produce valid batch JSON on first try.
6
+ //
7
+ // Hand-written sections: Preamble + Hard rules (prose, free-form).
8
+ // Generated sections (wrapped in <!-- SECTION:name --> markers so the drift
9
+ // test can parse them): op-catalog, critical-enums, batch-examples, digest.
10
+ //
11
+ // If the generated sections drift from the Zod schema, the drift test fails
12
+ // loudly. Do NOT enumerate enum values inside the preamble or hard rules —
13
+ // those come from schema-introspect.ts.
14
+ import { listOps, getEnumValues, getOpExample, getOpSchema } from './schema-introspect.js';
15
+ const PREAMBLE = `# Hafez — Agent Prompt Gate
16
+
17
+ Hafez is your persistent knowledge graph for projects, entities, captures,
18
+ knowledge notes, and session notes. The \`hafez\` CLI is the ONLY supported
19
+ write path into the vault — never edit files under \`entities/\`, \`knowledge/\`,
20
+ or \`sessions/\` by hand. Every mutation goes through the CLI so the git history,
21
+ SQLite index, and link integrity stay in sync.
22
+
23
+ Mental model:
24
+
25
+ - An \`entity\` is tracked work with substance — a bug, a feature, a decision.
26
+ Entities have status, can have children, and carry a session log. The three
27
+ entity types are \`capture\` (raw inbox), \`entity\` (standalone tracked item),
28
+ and \`project\` (scoped effort with deliverables, can have child entities).
29
+ - A \`knowledge\` note is an insight or a plan. Insights distil a pattern or
30
+ principle and mature through added evidence. Plans are structured steps
31
+ toward a goal. Knowledge is searchable, linkable, and timestamped.
32
+ - A \`session\` note records what happened in a working session and references
33
+ the entities it touched. Session notes are created via \`create/session\`
34
+ and are not indexed in SQLite — they are for narrative, not query.
35
+ - \`capture\` is the quick-inbox path: grab a link or thought, no structure.
36
+ A capture can later be promoted to entity, project, or knowledge.
37
+ - Entities relate to each other through links: \`parent\` (hierarchy) or
38
+ \`related\` (soft reference). Use \`link\` / \`unlink\` ops to manage them.
39
+ - Every meaningful update should include a session_log entry so the history
40
+ of the work is preserved. Sessions are how future-you reconstructs why a
41
+ decision was made.
42
+
43
+ Single source of truth: the exact field names, enum values, and op signatures
44
+ below are generated at runtime from the live Zod schema. If a field is not
45
+ listed here, it does not exist — do not invent fields.
46
+
47
+ ## Hard rules (read before writing a batch)
48
+
49
+ - ALWAYS batch 2+ mutations into one \`hafez batch\` call. Many single-op
50
+ calls fragment git history and clobber the vault index's atomicity guarantee.
51
+ - NEVER invent field names. If a field is not in \`hafez schema <op>\`, it
52
+ does not exist. Unknown fields are rejected by the validator with a pointer
53
+ to the right schema.
54
+ - NEVER write to vault files directly. Always go through the CLI.
55
+ - STRONGLY PREFERRED: include a \`session_log\` on update ops that reflect
56
+ meaningful work. The validator does not enforce this — omitting it is a
57
+ code smell, not a hard error.
58
+ - When in doubt, run \`hafez schema <op>\` before writing a batch. Cheap,
59
+ deterministic, no side effects.
60
+ - Use \`hafez batch --dry-run\` to validate a payload without touching the
61
+ vault. Same errors, zero filesystem mutation.
62
+ `;
63
+ function renderOpCatalog() {
64
+ const lines = ['<!-- SECTION:op-catalog -->'];
65
+ lines.push('Ops (run `hafez schema <name>` for the full field list):');
66
+ for (const op of listOps()) {
67
+ const desc = op.description ? ` — ${op.description}` : '';
68
+ lines.push(` ${op.name}${desc}`);
69
+ lines.push(` → hafez schema ${op.name}`);
70
+ }
71
+ lines.push('<!-- /SECTION:op-catalog -->');
72
+ return lines.join('\n');
73
+ }
74
+ function renderCriticalEnums() {
75
+ const entries = [
76
+ ['update.fields.status', 'update.fields.status'],
77
+ ['update.fields.session_log.type', 'update.fields.session_log.type'],
78
+ ['update.fields.confidence', 'update.fields.confidence'],
79
+ ['create-entity.fields.type', 'create-entity.fields.type'],
80
+ ['create-knowledge.fields.subtype', 'create-knowledge.fields.subtype'],
81
+ ['link.relation', 'link.relation'],
82
+ ];
83
+ const lines = ['<!-- SECTION:critical-enums -->'];
84
+ lines.push('Critical enums (value lists — values are authoritative):');
85
+ for (const [label, path] of entries) {
86
+ const values = getEnumValues(path);
87
+ if (!values)
88
+ continue;
89
+ lines.push(` ${label} = [${values.join(', ')}]`);
90
+ }
91
+ lines.push('<!-- /SECTION:critical-enums -->');
92
+ return lines.join('\n');
93
+ }
94
+ function renderBatchExamples() {
95
+ const lines = ['<!-- SECTION:batch-examples -->'];
96
+ lines.push('Batch JSON shape — wrap one or more ops in an array and pipe to stdin:');
97
+ lines.push('');
98
+ lines.push(' echo \'[...]\' | hafez batch');
99
+ lines.push('');
100
+ lines.push('Minimal example per op (canonical field names; aliased keys stripped):');
101
+ lines.push('');
102
+ for (const op of listOps()) {
103
+ const example = getOpExample(op.name);
104
+ if (!example)
105
+ continue;
106
+ lines.push(`# ${op.name}`);
107
+ lines.push(JSON.stringify(example, null, 2));
108
+ lines.push('');
109
+ }
110
+ lines.push('<!-- /SECTION:batch-examples -->');
111
+ return lines.join('\n');
112
+ }
113
+ function renderDigestSection() {
114
+ const lines = ['<!-- SECTION:digest -->'];
115
+ lines.push('Session digest — turn end-of-session context into a batch payload.');
116
+ lines.push('NOT a batch op: it reads its own JSON from stdin and prints batch JSON.');
117
+ lines.push('');
118
+ lines.push(" echo '<digest-json>' | hafez digest | hafez batch");
119
+ lines.push('');
120
+ lines.push('Input fields (`hafez schema digest` for the full schema):');
121
+ const schema = getOpSchema('digest');
122
+ for (const [name, field] of Object.entries(schema?.fields ?? {})) {
123
+ const req = field.optional ? 'optional' : 'required';
124
+ const desc = field.description ? ` — ${field.description}` : '';
125
+ lines.push(` ${name} (${req})${desc}`);
126
+ }
127
+ lines.push('');
128
+ lines.push('Example input:');
129
+ lines.push(JSON.stringify(getOpExample('digest'), null, 2));
130
+ lines.push('<!-- /SECTION:digest -->');
131
+ return lines.join('\n');
132
+ }
133
+ const FOOTER = `## Lookup pointers
134
+
135
+ - \`hafez schema\` — list all ops
136
+ - \`hafez schema <op>\` — full JSON schema for one op
137
+ - \`hafez schema <op> --examples\` — working JSON examples per enum value
138
+ - \`hafez batch --dry-run\` — validate without touching the vault
139
+ `;
140
+ /**
141
+ * Render the full prompt-gate output. Token budget: 1500-3000 tokens when
142
+ * measured as char/3.5. The drift test in tests/cli-help-agent.test.ts
143
+ * enforces the ceiling.
144
+ */
145
+ export function renderAgentHelp() {
146
+ return [
147
+ PREAMBLE,
148
+ renderOpCatalog(),
149
+ '',
150
+ renderCriticalEnums(),
151
+ '',
152
+ renderBatchExamples(),
153
+ '',
154
+ renderDigestSection(),
155
+ '',
156
+ FOOTER,
157
+ ].join('\n');
158
+ }
@@ -0,0 +1 @@
1
+ export declare function main(argv: string[]): Promise<void>;
@@ -0,0 +1,304 @@
1
+ // src/cli/index.ts
2
+ import { readFileSync } from 'fs';
3
+ import { dirname, join, resolve } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { createHafez } from '../index.js';
6
+ import { HafezError } from '../types.js';
7
+ import { cmdRead, cmdQuery, cmdSearch, cmdCreate, cmdUpdate, cmdLink, cmdUnlink, cmdPromote, cmdValidate, cmdIndexRebuild, cmdBatch, cmdSync, cmdStats, cmdChangelog, cmdDigest, cmdMigrateKnowledgeV2, cmdCapture } from './commands.js';
8
+ import { cmdSchema } from './commands-schema.js';
9
+ import { renderAgentHelp } from './help-agent.js';
10
+ import { resolveVaultPath, noVaultMessage, cmdInit } from './resolve-vault.js';
11
+ const COMMANDS = {
12
+ read: cmdRead,
13
+ query: cmdQuery,
14
+ search: cmdSearch,
15
+ create: cmdCreate,
16
+ capture: cmdCapture,
17
+ update: cmdUpdate,
18
+ link: cmdLink,
19
+ unlink: cmdUnlink,
20
+ promote: cmdPromote,
21
+ batch: cmdBatch,
22
+ schema: cmdSchema,
23
+ validate: cmdValidate,
24
+ index: (os, args) => {
25
+ if (args[0] === 'rebuild')
26
+ return cmdIndexRebuild(os);
27
+ throw new Error('Usage: hafez index rebuild');
28
+ },
29
+ stats: cmdStats,
30
+ changelog: cmdChangelog,
31
+ digest: cmdDigest,
32
+ };
33
+ // Commands that modify vault files — auto-sync after these
34
+ const MUTATING_COMMANDS = new Set(['update', 'create', 'capture', 'link', 'unlink', 'batch', 'promote']);
35
+ const USAGE = `Usage: hafez <command> [args]
36
+
37
+ Commands:
38
+ read <slug> Read entity or knowledge note
39
+ query [--filter ...] ... Filter entities/knowledge
40
+ search <terms> Full-text search
41
+ create <kind> <name> Create entity, knowledge, or inbox
42
+ capture <name> [--notes] Quick inbox capture
43
+ promote <slug> <target> Promote capture→entity/project/knowledge, entity→project
44
+ update <slug> [--status] Update an entity or knowledge note
45
+ --brief "text" Set/replace Brief (use --brief "" to clear)
46
+ --description "text" Set one-line description (use --description "" to clear)
47
+ --resource "uri" Set canonical resource URI, entities only (use --resource "" to clear)
48
+ --add-action "text" Add a next action
49
+ --complete-action "text" Complete matching action
50
+ --remove-action "text" Remove matching action
51
+ --clear-actions Remove all next actions
52
+ --synthesis "text" Set/replace Synthesis section (--insight is an alias)
53
+ --add-evidence "text" Append to Evidence section
54
+ --add-source "text" Append to Sources section
55
+ batch Apply multiple operations atomically (JSON on stdin)
56
+ digest Apply digest input (JSON stdin) → batch payload (JSON stdout)
57
+ export --okf [--out <dir>] Export vault as an OKF v0.1 bundle (read-only)
58
+ sync Pull remote changes and push local commits
59
+ migrate next-actions Migrate next-action frontmatter to body sections
60
+ migrate types Migrate vault to v2 type system (dry-run by default)
61
+ migrate knowledge-v2 Migrate knowledge notes to v2 section structure (dry-run by default)
62
+ --apply Execute migration (writes files, does not commit)
63
+ link <slug> <target> <rel> Add a link
64
+ unlink <slug> <target> <rel> Remove a link
65
+ stats Vault summary: counts, stale items, recents
66
+ changelog --since <when> Git-derived change history (e.g. --since 7.days.ago)
67
+ schema [op] [--examples] Machine-readable JSON schema for batch operations
68
+ validate Check vault integrity
69
+ index rebuild Rebuild the SQLite index
70
+ init --register <path> Register an existing vault
71
+ init Show current vault resolution
72
+ help --agent Full API reference for agents
73
+
74
+ Global flags:
75
+ --vault <path> Vault path (overrides config)
76
+ --json JSON output (structured, machine-readable)
77
+ --version Print the hafez version
78
+
79
+ Vault discovery: --vault flag > ~/.config/hafez/vault
80
+ `;
81
+ export async function main(argv) {
82
+ const args = argv.slice(2);
83
+ // Extract global flags
84
+ const vaultIdx = args.indexOf('--vault');
85
+ let flagValue;
86
+ if (vaultIdx !== -1) {
87
+ flagValue = args[vaultIdx + 1];
88
+ args.splice(vaultIdx, 2);
89
+ }
90
+ const jsonIdx = args.indexOf('--json');
91
+ const json = jsonIdx !== -1;
92
+ if (json)
93
+ args.splice(jsonIdx, 1);
94
+ const command = args.shift();
95
+ if (!command || command === '--help' || command === '-h') {
96
+ process.stdout.write(USAGE);
97
+ return;
98
+ }
99
+ if (command === '--version' || command === '-v' || command === 'version') {
100
+ const here = dirname(fileURLToPath(import.meta.url));
101
+ const pkg = JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf-8'));
102
+ process.stdout.write(`hafez ${pkg.version}\n`);
103
+ return;
104
+ }
105
+ // help runs before vault resolution — works without a registered vault
106
+ if (command === 'help') {
107
+ if (args[0] === '--agent') {
108
+ process.stdout.write(renderAgentHelp());
109
+ return;
110
+ }
111
+ process.stdout.write(USAGE);
112
+ return;
113
+ }
114
+ // schema runs before vault resolution — pure Zod introspection, no vault needed.
115
+ if (command === 'schema') {
116
+ try {
117
+ const result = await cmdSchema(null, args);
118
+ process.stdout.write(result + '\n');
119
+ }
120
+ catch (err) {
121
+ if (err instanceof HafezError) {
122
+ process.stderr.write(`Error: ${err.message}\n`);
123
+ if (err.details?.length) {
124
+ for (const detail of err.details)
125
+ process.stderr.write(` - ${detail}\n`);
126
+ }
127
+ process.exit(2);
128
+ }
129
+ process.stderr.write(`Error: ${err.message}\n`);
130
+ process.exit(1);
131
+ }
132
+ return;
133
+ }
134
+ // init runs before vault resolution
135
+ if (command === 'init') {
136
+ cmdInit(args, flagValue);
137
+ return;
138
+ }
139
+ // Resolve vault
140
+ const resolution = resolveVaultPath(flagValue);
141
+ if (resolution === null) {
142
+ process.stderr.write(noVaultMessage());
143
+ process.exit(1);
144
+ }
145
+ if (typeof resolution === 'object' && 'error' in resolution) {
146
+ process.stderr.write(`Error: ${resolution.error}\n`);
147
+ process.exit(1);
148
+ }
149
+ const vaultPath = resolution;
150
+ // export runs before Hafez instance creation — read-only, no auto-sync
151
+ if (command === 'export') {
152
+ if (!args.includes('--okf')) {
153
+ process.stderr.write('Usage: hafez export --okf [--out <dir>]\n');
154
+ process.exit(1);
155
+ }
156
+ const outIdx = args.indexOf('--out');
157
+ let outDir = resolve(process.cwd(), 'okf-export');
158
+ if (outIdx !== -1) {
159
+ const outValue = args[outIdx + 1];
160
+ if (!outValue) {
161
+ process.stderr.write('Usage: hafez export --okf [--out <dir>]\n');
162
+ process.exit(1);
163
+ }
164
+ outDir = resolve(outValue);
165
+ }
166
+ const { exportOkf } = await import('../export-okf.js');
167
+ try {
168
+ const report = exportOkf(vaultPath, outDir);
169
+ if (json) {
170
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
171
+ }
172
+ else {
173
+ const lines = [`Exported ${report.entities} entities, ${report.knowledge} knowledge, ${report.sessions} sessions to ${report.outDir}`];
174
+ for (const s of report.skipped)
175
+ lines.push(` skipped ${s.file}: ${s.reason}`);
176
+ if (report.unresolvedLinks > 0)
177
+ lines.push(` unresolved links: ${report.unresolvedLinks}`);
178
+ process.stdout.write(lines.join('\n') + '\n');
179
+ }
180
+ }
181
+ catch (err) {
182
+ process.stderr.write(`Error: ${err.message}\n`);
183
+ process.exit(1);
184
+ }
185
+ return;
186
+ }
187
+ // migrate runs before Hafez instance creation
188
+ if (command === 'migrate') {
189
+ const subcommand = args[0];
190
+ if (subcommand === 'next-actions') {
191
+ const { migrateNextActions } = await import('../migrate-next-actions.js');
192
+ const apply = args.includes('--apply');
193
+ const report = migrateNextActions(vaultPath, apply);
194
+ const lines = [];
195
+ if (!apply)
196
+ lines.push('DRY RUN — use --apply to execute');
197
+ lines.push(`Candidates: ${report.candidates}`);
198
+ if (apply)
199
+ lines.push(`Migrated: ${report.migrated}`);
200
+ for (const d of report.details)
201
+ lines.push(` ${d}`);
202
+ process.stdout.write(lines.join('\n') + '\n');
203
+ return;
204
+ }
205
+ if (subcommand === 'types') {
206
+ const { migrateTypes } = await import('../migrate-types.js');
207
+ const apply = args.includes('--apply');
208
+ const report = migrateTypes(vaultPath, apply);
209
+ const lines = [];
210
+ if (!apply) {
211
+ lines.push('DRY RUN — use --apply to execute');
212
+ lines.push('');
213
+ }
214
+ lines.push(`Type renames: ${report.typeChanges}`);
215
+ lines.push(`Status renames: ${report.statusChanges}`);
216
+ lines.push(`Subtypes added: ${report.knowledgeSubtypeAdded}`);
217
+ lines.push(`Body restructures: ${report.bodySectionsRestructured}`);
218
+ lines.push(`Malformed (skipped): ${report.malformed.length}`);
219
+ if (report.malformed.length > 0) {
220
+ lines.push('');
221
+ lines.push('Malformed files (no type field):');
222
+ for (const f of report.malformed)
223
+ lines.push(` ⚠ ${f}`);
224
+ }
225
+ if (report.details.length > 0) {
226
+ lines.push('');
227
+ lines.push('Changes:');
228
+ for (const d of report.details)
229
+ lines.push(` ${d}`);
230
+ }
231
+ if (apply) {
232
+ lines.push('');
233
+ lines.push('Migration applied. Run `hafez index rebuild` to refresh the index.');
234
+ }
235
+ process.stdout.write(lines.join('\n') + '\n');
236
+ return;
237
+ }
238
+ if (subcommand === 'knowledge-v2') {
239
+ await cmdMigrateKnowledgeV2(args.slice(1), vaultPath);
240
+ return;
241
+ }
242
+ process.stderr.write('Usage: hafez migrate <next-actions|types|knowledge-v2> [--apply]\n');
243
+ process.exit(1);
244
+ }
245
+ // sync is handled specially — always runs, not gated by MUTATING_COMMANDS
246
+ if (command === 'sync') {
247
+ const os = createHafez({ vaultPath, git: { push: false } });
248
+ try {
249
+ const result = await cmdSync(os, args);
250
+ process.stdout.write(result + '\n');
251
+ }
252
+ catch (err) {
253
+ if (err instanceof HafezError) {
254
+ process.stderr.write(`Error: ${err.message}\n`);
255
+ if (err.details?.length) {
256
+ for (const detail of err.details) {
257
+ process.stderr.write(` - ${detail}\n`);
258
+ }
259
+ }
260
+ const exitCodes = { NOT_FOUND: 1, VALIDATION_FAILED: 2, GIT_PUSH_FAILED: 3 };
261
+ process.exit(exitCodes[err.code] ?? 1);
262
+ }
263
+ process.stderr.write(`Error: ${err.message}\n`);
264
+ process.exit(1);
265
+ }
266
+ return;
267
+ }
268
+ const handler = COMMANDS[command];
269
+ if (!handler) {
270
+ process.stderr.write(`Unknown command: ${command}\n${USAGE}`);
271
+ process.exit(1);
272
+ }
273
+ const os = createHafez({ vaultPath, git: { push: false } });
274
+ try {
275
+ const result = await handler(os, args, { json });
276
+ // Auto-sync after mutating commands
277
+ if (MUTATING_COMMANDS.has(command)) {
278
+ try {
279
+ await os.sync();
280
+ }
281
+ catch (syncErr) {
282
+ // Operation succeeded but sync failed — emit result with warning, exit 4
283
+ process.stdout.write(result + '\n');
284
+ process.stderr.write(`Warning: ${syncErr.message ?? syncErr.message}\n`);
285
+ process.exit(4);
286
+ }
287
+ }
288
+ process.stdout.write(result + '\n');
289
+ }
290
+ catch (err) {
291
+ if (err instanceof HafezError) {
292
+ process.stderr.write(`Error: ${err.message}\n`);
293
+ if (err.details?.length) {
294
+ for (const detail of err.details) {
295
+ process.stderr.write(` - ${detail}\n`);
296
+ }
297
+ }
298
+ const exitCodes = { NOT_FOUND: 1, VALIDATION_FAILED: 2, GIT_PUSH_FAILED: 3 };
299
+ process.exit(exitCodes[err.code] ?? 1);
300
+ }
301
+ process.stderr.write(`Error: ${err.message}\n`);
302
+ process.exit(1);
303
+ }
304
+ }
@@ -0,0 +1,13 @@
1
+ export declare function getConfigDir(): string;
2
+ export declare function getConfigPath(): string;
3
+ export declare function readVaultConfig(): string | null;
4
+ export declare function saveVaultConfig(vaultPath: string): void;
5
+ /**
6
+ * Resolve vault path. Returns string or null.
7
+ * If config exists but points to a bad path, returns an error object instead.
8
+ */
9
+ export declare function resolveVaultPath(flagValue?: string): string | null | {
10
+ error: string;
11
+ };
12
+ export declare function noVaultMessage(): string;
13
+ export declare function cmdInit(args: string[], flagValue?: string): void;