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.
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/cli/batch-errors.d.ts +24 -0
- package/dist/cli/batch-errors.js +236 -0
- package/dist/cli/commands-schema.d.ts +3 -0
- package/dist/cli/commands-schema.js +80 -0
- package/dist/cli/commands.d.ts +133 -0
- package/dist/cli/commands.js +628 -0
- package/dist/cli/format.d.ts +9 -0
- package/dist/cli/format.js +158 -0
- package/dist/cli/help-agent.d.ts +6 -0
- package/dist/cli/help-agent.js +158 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +304 -0
- package/dist/cli/resolve-vault.d.ts +13 -0
- package/dist/cli/resolve-vault.js +124 -0
- package/dist/cli/schema-introspect.d.ts +70 -0
- package/dist/cli/schema-introspect.js +372 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/contracts.d.ts +26 -0
- package/dist/contracts.js +87 -0
- package/dist/db.d.ts +68 -0
- package/dist/db.js +663 -0
- package/dist/digest.d.ts +29 -0
- package/dist/digest.js +94 -0
- package/dist/export-okf.d.ts +12 -0
- package/dist/export-okf.js +176 -0
- package/dist/git.d.ts +12 -0
- package/dist/git.js +355 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +909 -0
- package/dist/knowledge-index.d.ts +3 -0
- package/dist/knowledge-index.js +135 -0
- package/dist/merge.d.ts +1 -0
- package/dist/merge.js +176 -0
- package/dist/migrate-knowledge-v2.d.ts +11 -0
- package/dist/migrate-knowledge-v2.js +158 -0
- package/dist/migrate-next-actions.d.ts +6 -0
- package/dist/migrate-next-actions.js +32 -0
- package/dist/migrate-types.d.ts +11 -0
- package/dist/migrate-types.js +142 -0
- package/dist/parser.d.ts +18 -0
- package/dist/parser.js +68 -0
- package/dist/query.d.ts +24 -0
- package/dist/query.js +167 -0
- package/dist/schema.d.ts +9 -0
- package/dist/schema.js +59 -0
- package/dist/sections.d.ts +54 -0
- package/dist/sections.js +297 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +39 -0
- package/dist/types.d.ts +321 -0
- package/dist/types.js +11 -0
- package/dist/vault.d.ts +12 -0
- package/dist/vault.js +45 -0
- package/package.json +67 -0
- 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,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;
|