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,628 @@
|
|
|
1
|
+
// src/cli/commands.ts
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { HafezError } from '../types.js';
|
|
4
|
+
import { ENTITY_TYPES, ENTITY_STATUSES, SESSION_LOG_TYPES, CONFIDENCE_LEVELS, VALID_SUBTYPES, } from '../contracts.js';
|
|
5
|
+
import { formatBatchError } from './batch-errors.js';
|
|
6
|
+
import { parseDigestInput, digest } from '../digest.js';
|
|
7
|
+
import { formatEntityHeader, formatKnowledgeHeader, formatQueryTable, formatKnowledgeTable, formatSearchResults, formatValidation, formatStats, formatChangelog } from './format.js';
|
|
8
|
+
import { migrateKnowledgeV2 } from '../migrate-knowledge-v2.js';
|
|
9
|
+
function jsonOut(data) {
|
|
10
|
+
return JSON.stringify(data, null, 2);
|
|
11
|
+
}
|
|
12
|
+
export async function cmdRead(os, args, opts = {}) {
|
|
13
|
+
const slug = args[0];
|
|
14
|
+
if (!slug)
|
|
15
|
+
throw new Error('Usage: hafez read <slug> [--depth frontmatter|summary|full]');
|
|
16
|
+
const depth = (getFlag(args, '--depth') ?? 'summary');
|
|
17
|
+
const result = await os.read(slug, depth);
|
|
18
|
+
if (opts.json)
|
|
19
|
+
return jsonOut(result);
|
|
20
|
+
const fm = result.frontmatter;
|
|
21
|
+
if ('type' in fm)
|
|
22
|
+
return formatEntityHeader(fm, result.body);
|
|
23
|
+
return formatKnowledgeHeader(fm, result.body);
|
|
24
|
+
}
|
|
25
|
+
export async function cmdQuery(os, args, opts = {}) {
|
|
26
|
+
const kind = getFlag(args, '--kind') ?? 'entity';
|
|
27
|
+
const filter = (getFlag(args, '--filter') ?? 'active');
|
|
28
|
+
const type = getFlag(args, '--type');
|
|
29
|
+
const parent = getFlag(args, '--parent');
|
|
30
|
+
const relatedTo = getFlag(args, '--related-to');
|
|
31
|
+
const domain = getFlag(args, '--domain');
|
|
32
|
+
const confidence = getFlag(args, '--confidence');
|
|
33
|
+
const since = getFlag(args, '--since');
|
|
34
|
+
const before = getFlag(args, '--before');
|
|
35
|
+
const createdSince = getFlag(args, '--created-since');
|
|
36
|
+
const createdBefore = getFlag(args, '--created-before');
|
|
37
|
+
const tagRaw = getFlag(args, '--tag');
|
|
38
|
+
const tags = tagRaw ? tagRaw.split(',').map(t => t.trim()).filter(Boolean) : undefined;
|
|
39
|
+
const sortBy = getFlag(args, '--sort-by');
|
|
40
|
+
const sortOrder = getFlag(args, '--sort-order');
|
|
41
|
+
const limitRaw = getFlag(args, '--limit');
|
|
42
|
+
const offsetRaw = getFlag(args, '--offset');
|
|
43
|
+
const limit = limitRaw != null ? parseInt(limitRaw, 10) : undefined;
|
|
44
|
+
const offset = offsetRaw != null ? parseInt(offsetRaw, 10) : undefined;
|
|
45
|
+
const sortOpts = { sort_by: sortBy, sort_order: sortOrder, limit, offset };
|
|
46
|
+
// --related-to: always returns both entities and knowledge
|
|
47
|
+
if (relatedTo) {
|
|
48
|
+
const result = await os.related_to(relatedTo);
|
|
49
|
+
if (opts.json)
|
|
50
|
+
return jsonOut(result);
|
|
51
|
+
const { items: results, total } = result;
|
|
52
|
+
const entities = results.filter((r) => 'type' in r);
|
|
53
|
+
const knowledge = results.filter((r) => 'confidence' in r);
|
|
54
|
+
const sections = [];
|
|
55
|
+
if (entities.length > 0)
|
|
56
|
+
sections.push('## Entities\n\n' + formatQueryTable(entities, total));
|
|
57
|
+
if (knowledge.length > 0)
|
|
58
|
+
sections.push('## Knowledge\n\n' + formatKnowledgeTable(knowledge, total));
|
|
59
|
+
return sections.length > 0 ? sections.join('\n\n') : 'No results.';
|
|
60
|
+
}
|
|
61
|
+
const queryOpts = { filter, type, parent, domain, since, before, createdSince, createdBefore, tags, ...sortOpts };
|
|
62
|
+
// --kind knowledge: query knowledge only
|
|
63
|
+
// Note: time flags only apply to entity queries.
|
|
64
|
+
if (kind === 'knowledge') {
|
|
65
|
+
const subtype = getFlag(args, '--subtype');
|
|
66
|
+
const kOpts = { domain: domain ?? undefined, confidence, subtype, tags, ...sortOpts };
|
|
67
|
+
const result = await os.query_knowledge(kOpts);
|
|
68
|
+
if (opts.json)
|
|
69
|
+
return jsonOut(result);
|
|
70
|
+
return formatKnowledgeTable(result.items, result.total);
|
|
71
|
+
}
|
|
72
|
+
// --kind all: query both entities and knowledge via unified query
|
|
73
|
+
if (kind === 'all') {
|
|
74
|
+
const result = await os.queryUnified({ ...queryOpts, kind: 'all', confidence });
|
|
75
|
+
if (opts.json)
|
|
76
|
+
return jsonOut(result);
|
|
77
|
+
const { items, total } = result;
|
|
78
|
+
const entities = items.filter(i => i.kind === 'entity');
|
|
79
|
+
const knowledge = items.filter(i => i.kind === 'knowledge');
|
|
80
|
+
const sections = [];
|
|
81
|
+
if (entities.length > 0)
|
|
82
|
+
sections.push('## Entities\n\n' + formatQueryTable(entities));
|
|
83
|
+
if (knowledge.length > 0)
|
|
84
|
+
sections.push('## Knowledge\n\n' + formatKnowledgeTable(knowledge));
|
|
85
|
+
if (items.length < total)
|
|
86
|
+
sections.push(`\nShowing ${items.length} of ${total}`);
|
|
87
|
+
return sections.length > 0 ? sections.join('\n\n') : 'No results.';
|
|
88
|
+
}
|
|
89
|
+
// --kind entity (default): query entities
|
|
90
|
+
const result = await os.query(queryOpts);
|
|
91
|
+
if (opts.json)
|
|
92
|
+
return jsonOut(result);
|
|
93
|
+
return formatQueryTable(result.items, result.total);
|
|
94
|
+
}
|
|
95
|
+
export async function cmdSearch(os, args, opts = {}) {
|
|
96
|
+
const terms = positionalArgs(args).join(' ');
|
|
97
|
+
if (!terms)
|
|
98
|
+
throw new Error('Usage: hafez search <terms> [--kind entity|knowledge|all]');
|
|
99
|
+
const kind = (getFlag(args, '--kind') ?? 'all');
|
|
100
|
+
const results = await os.search(terms, kind);
|
|
101
|
+
if (opts.json)
|
|
102
|
+
return jsonOut(results);
|
|
103
|
+
return formatSearchResults(results);
|
|
104
|
+
}
|
|
105
|
+
export async function cmdCreate(os, args, opts = {}) {
|
|
106
|
+
const kind = args[0];
|
|
107
|
+
if (!kind)
|
|
108
|
+
throw new Error('Usage: hafez create <entity|knowledge|inbox> <name> [options]');
|
|
109
|
+
if (kind === 'inbox') {
|
|
110
|
+
const name = positionalArgs(args.slice(1)).join(' ');
|
|
111
|
+
if (!name)
|
|
112
|
+
throw new Error('Usage: hafez create inbox <name> [--notes "text"]');
|
|
113
|
+
const notes = getFlag(args, '--notes');
|
|
114
|
+
process.stderr.write('Warning: `hafez create inbox` is deprecated. Use `hafez capture <name>` instead.\n');
|
|
115
|
+
const slug = await os.capture(name, notes);
|
|
116
|
+
return opts.json ? jsonOut({ slug }) : slug;
|
|
117
|
+
}
|
|
118
|
+
const name = positionalArgs(args.slice(1)).join(' ');
|
|
119
|
+
if (!name)
|
|
120
|
+
throw new Error(`Usage: hafez create ${kind} <name> [options]`);
|
|
121
|
+
if (kind === 'entity') {
|
|
122
|
+
const type = getFlag(args, '--type');
|
|
123
|
+
if (!type)
|
|
124
|
+
throw new Error('--type is required for entity creation');
|
|
125
|
+
const slug = await os.create('entity', name, {
|
|
126
|
+
type,
|
|
127
|
+
purpose: getFlag(args, '--purpose'),
|
|
128
|
+
description: getFlag(args, '--description'),
|
|
129
|
+
resource: getFlag(args, '--resource'),
|
|
130
|
+
domain: getFlag(args, '--domain')?.split(',').map(d => d.trim()),
|
|
131
|
+
parent: getFlag(args, '--parent'),
|
|
132
|
+
related: getFlag(args, '--related')?.split(','),
|
|
133
|
+
tags: getFlag(args, '--tags')?.split(','),
|
|
134
|
+
brief: getFlag(args, '--brief'),
|
|
135
|
+
add_action: getFlag(args, '--add-action'),
|
|
136
|
+
});
|
|
137
|
+
return opts.json ? jsonOut({ slug }) : slug;
|
|
138
|
+
}
|
|
139
|
+
if (kind === 'knowledge') {
|
|
140
|
+
const slug = await os.create('knowledge', name, {
|
|
141
|
+
subtype: getFlag(args, '--subtype'),
|
|
142
|
+
synthesis: getFlag(args, '--synthesis') ?? getFlag(args, '--insight'),
|
|
143
|
+
description: getFlag(args, '--description'),
|
|
144
|
+
domain: getFlag(args, '--domain')?.split(','),
|
|
145
|
+
related: getFlag(args, '--related')?.split(','),
|
|
146
|
+
tags: getFlag(args, '--tags')?.split(','),
|
|
147
|
+
});
|
|
148
|
+
return opts.json ? jsonOut({ slug }) : slug;
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Unknown kind: ${kind}. Use entity, knowledge, or inbox.`);
|
|
151
|
+
}
|
|
152
|
+
export async function cmdUpdate(os, args, opts = {}) {
|
|
153
|
+
const slug = args[0];
|
|
154
|
+
if (!slug)
|
|
155
|
+
throw new Error('Usage: hafez update <slug> [--brief text] [--description text] [--resource uri] [--add-action text] [--complete-action text] [--remove-action text] [--clear-actions] [--status s] [--log "type: summary"]');
|
|
156
|
+
const fields = {};
|
|
157
|
+
const status = getFlag(args, '--status');
|
|
158
|
+
if (status)
|
|
159
|
+
fields.status = status;
|
|
160
|
+
// Legacy --next-action: deprecation notice
|
|
161
|
+
const nextAction = getFlag(args, '--next-action');
|
|
162
|
+
if (nextAction !== undefined) {
|
|
163
|
+
process.stderr.write('Warning: --next-action is deprecated. Use --add-action instead.\n');
|
|
164
|
+
fields.add_action = nextAction === '' ? undefined : nextAction;
|
|
165
|
+
if (nextAction === '')
|
|
166
|
+
fields.clear_actions = true;
|
|
167
|
+
}
|
|
168
|
+
const currentState = getFlag(args, '--current-state');
|
|
169
|
+
if (currentState)
|
|
170
|
+
fields.current_state = currentState;
|
|
171
|
+
// Brief
|
|
172
|
+
const brief = getFlag(args, '--brief');
|
|
173
|
+
if (brief !== undefined) {
|
|
174
|
+
fields.brief = brief === '' ? null : brief;
|
|
175
|
+
}
|
|
176
|
+
// Next Actions
|
|
177
|
+
const addAction = getFlag(args, '--add-action');
|
|
178
|
+
if (addAction)
|
|
179
|
+
fields.add_action = addAction;
|
|
180
|
+
const completeAction = getFlag(args, '--complete-action');
|
|
181
|
+
if (completeAction)
|
|
182
|
+
fields.complete_action = completeAction;
|
|
183
|
+
const removeAction = getFlag(args, '--remove-action');
|
|
184
|
+
if (removeAction)
|
|
185
|
+
fields.remove_action = removeAction;
|
|
186
|
+
if (args.includes('--clear-actions'))
|
|
187
|
+
fields.clear_actions = true;
|
|
188
|
+
const log = getFlag(args, '--log');
|
|
189
|
+
if (log) {
|
|
190
|
+
const colonIdx = log.indexOf(':');
|
|
191
|
+
if (colonIdx === -1)
|
|
192
|
+
throw new Error('--log format: "type: summary". Types: progress, decision, blocker, research');
|
|
193
|
+
const type = log.slice(0, colonIdx).trim();
|
|
194
|
+
const summary = log.slice(colonIdx + 1).trim();
|
|
195
|
+
const agent = getFlag(args, '--agent') ?? 'claude';
|
|
196
|
+
fields.session_log = { type, summary, agent };
|
|
197
|
+
}
|
|
198
|
+
// Shared metadata (entity + knowledge)
|
|
199
|
+
const description = getFlag(args, '--description');
|
|
200
|
+
if (description !== undefined)
|
|
201
|
+
fields.description = description;
|
|
202
|
+
const resource = getFlag(args, '--resource');
|
|
203
|
+
if (resource !== undefined)
|
|
204
|
+
fields.resource = resource;
|
|
205
|
+
const domainRaw = getFlag(args, '--domain');
|
|
206
|
+
if (domainRaw)
|
|
207
|
+
fields.domain = domainRaw.split(',').map((d) => d.trim());
|
|
208
|
+
const conf = getFlag(args, '--confidence');
|
|
209
|
+
if (conf)
|
|
210
|
+
fields.confidence = conf;
|
|
211
|
+
const tagsRaw = getFlag(args, '--tags');
|
|
212
|
+
if (tagsRaw)
|
|
213
|
+
fields.tags = tagsRaw.split(',').map((t) => t.trim());
|
|
214
|
+
const relatedRaw = getFlag(args, '--related');
|
|
215
|
+
if (relatedRaw)
|
|
216
|
+
fields.related = relatedRaw.split(',').map((r) => r.trim());
|
|
217
|
+
const insight = getFlag(args, '--insight');
|
|
218
|
+
if (insight)
|
|
219
|
+
fields.synthesis = insight;
|
|
220
|
+
const synthesis = getFlag(args, '--synthesis');
|
|
221
|
+
if (synthesis)
|
|
222
|
+
fields.synthesis = synthesis;
|
|
223
|
+
const addEvidence = getFlag(args, '--add-evidence');
|
|
224
|
+
if (addEvidence)
|
|
225
|
+
fields.add_evidence = addEvidence;
|
|
226
|
+
const addSource = getFlag(args, '--add-source');
|
|
227
|
+
if (addSource)
|
|
228
|
+
fields.add_source = addSource;
|
|
229
|
+
const result = await os.update(slug, fields);
|
|
230
|
+
if (opts.json)
|
|
231
|
+
return jsonOut({ slug, ...result });
|
|
232
|
+
const lines = [`Updated ${slug}`];
|
|
233
|
+
if (result.matched_action) {
|
|
234
|
+
lines.push(`matched action: ${result.matched_action}`);
|
|
235
|
+
}
|
|
236
|
+
return lines.join('\n');
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Parse link/unlink args, supporting both the legacy positional form
|
|
240
|
+
* (`hafez link <slug> <target> <parent|related>`) and the new
|
|
241
|
+
* `--relation <value>` named flag. Positional remains accepted as a
|
|
242
|
+
* deprecated alias — emits a one-line warning to stderr.
|
|
243
|
+
*/
|
|
244
|
+
function parseLinkArgs(args, verb) {
|
|
245
|
+
const relationFlag = getFlag(args, '--relation');
|
|
246
|
+
// After getFlag the relation value is still at its original positional slot
|
|
247
|
+
// (getFlag does NOT splice), so strip it manually to prevent it from being
|
|
248
|
+
// mistaken for a positional if someone passes both forms. Here we only
|
|
249
|
+
// consume the flag once above.
|
|
250
|
+
const positionals = args.filter((a, i) => {
|
|
251
|
+
if (a === '--relation')
|
|
252
|
+
return false;
|
|
253
|
+
if (i > 0 && args[i - 1] === '--relation')
|
|
254
|
+
return false;
|
|
255
|
+
return !a.startsWith('--');
|
|
256
|
+
});
|
|
257
|
+
const [slug, target, positionalRelation] = positionals;
|
|
258
|
+
if (!slug || !target) {
|
|
259
|
+
throw new Error(`Usage: hafez ${verb} <slug> <target> --relation <parent|related>`);
|
|
260
|
+
}
|
|
261
|
+
let relation = relationFlag;
|
|
262
|
+
if (!relation) {
|
|
263
|
+
if (!positionalRelation) {
|
|
264
|
+
throw new Error(`Usage: hafez ${verb} <slug> <target> --relation <parent|related>`);
|
|
265
|
+
}
|
|
266
|
+
relation = positionalRelation;
|
|
267
|
+
process.stderr.write(`Warning: positional relation is deprecated. Use: hafez ${verb} ${slug} ${target} --relation ${relation}\n`);
|
|
268
|
+
}
|
|
269
|
+
if (!['parent', 'related'].includes(relation)) {
|
|
270
|
+
throw new Error(`Invalid relation: ${relation}. Must be parent or related.`);
|
|
271
|
+
}
|
|
272
|
+
return { slug, target, relation };
|
|
273
|
+
}
|
|
274
|
+
export async function cmdLink(os, args, opts = {}) {
|
|
275
|
+
const { slug, target, relation } = parseLinkArgs(args, 'link');
|
|
276
|
+
await os.link(slug, target, relation);
|
|
277
|
+
return opts.json ? jsonOut({ slug, target, relation }) : `Linked ${slug} → ${target} (${relation})`;
|
|
278
|
+
}
|
|
279
|
+
export async function cmdUnlink(os, args, opts = {}) {
|
|
280
|
+
const { slug, target, relation } = parseLinkArgs(args, 'unlink');
|
|
281
|
+
await os.unlink(slug, target, relation);
|
|
282
|
+
return opts.json ? jsonOut({ slug, target, relation }) : `Unlinked ${slug} — ${target} (${relation})`;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Top-level capture command — aliases to the same handler used by
|
|
286
|
+
* `hafez create inbox`. Added to align the CLI verb with the batch op name.
|
|
287
|
+
*/
|
|
288
|
+
export async function cmdCapture(os, args, opts = {}) {
|
|
289
|
+
const name = positionalArgs(args).join(' ');
|
|
290
|
+
if (!name)
|
|
291
|
+
throw new Error('Usage: hafez capture <name> [--notes "text"]');
|
|
292
|
+
const notes = getFlag(args, '--notes');
|
|
293
|
+
const slug = await os.capture(name, notes);
|
|
294
|
+
return opts.json ? jsonOut({ slug }) : slug;
|
|
295
|
+
}
|
|
296
|
+
export async function cmdPromote(os, args, opts = {}) {
|
|
297
|
+
const slug = args[0];
|
|
298
|
+
const target = args[1];
|
|
299
|
+
if (!slug || !target)
|
|
300
|
+
throw new Error('Usage: hafez promote <slug> <entity|project|knowledge>');
|
|
301
|
+
if (!['entity', 'project', 'knowledge'].includes(target)) {
|
|
302
|
+
throw new Error(`Invalid target: ${target}. Must be entity, project, or knowledge.`);
|
|
303
|
+
}
|
|
304
|
+
await os.promote(slug, target);
|
|
305
|
+
if (opts.json)
|
|
306
|
+
return jsonOut({ slug, target });
|
|
307
|
+
return `Promoted ${slug} → ${target}`;
|
|
308
|
+
}
|
|
309
|
+
export async function cmdValidate(os, _args, opts = {}) {
|
|
310
|
+
const report = await os.validate();
|
|
311
|
+
if (opts.json)
|
|
312
|
+
return jsonOut(report);
|
|
313
|
+
return formatValidation(report);
|
|
314
|
+
}
|
|
315
|
+
export async function cmdIndexRebuild(os) {
|
|
316
|
+
await os.rebuildIndex();
|
|
317
|
+
return 'Index rebuilt.';
|
|
318
|
+
}
|
|
319
|
+
export async function cmdSync(os, _args) {
|
|
320
|
+
const result = await os.sync();
|
|
321
|
+
if (!result.remote)
|
|
322
|
+
return 'No remote configured — vault is local-only. Every write is already committed to git.';
|
|
323
|
+
if (result.pulled && result.pushed)
|
|
324
|
+
return 'Synced: pulled remote changes and pushed local commits.';
|
|
325
|
+
if (result.pulled)
|
|
326
|
+
return 'Synced: pulled remote changes.';
|
|
327
|
+
if (result.pushed)
|
|
328
|
+
return 'Synced: pushed local commits.';
|
|
329
|
+
return 'Already up to date.';
|
|
330
|
+
}
|
|
331
|
+
export async function cmdStats(os, _args, opts = {}) {
|
|
332
|
+
const stats = await os.stats();
|
|
333
|
+
if (opts.json)
|
|
334
|
+
return jsonOut(stats);
|
|
335
|
+
return formatStats(stats);
|
|
336
|
+
}
|
|
337
|
+
export async function cmdChangelog(os, args, opts = {}) {
|
|
338
|
+
const since = getFlag(args, '--since');
|
|
339
|
+
if (!since)
|
|
340
|
+
throw new Error('Usage: hafez changelog --since <date|relative>\nExamples: --since 2026-03-20, --since 7.days.ago, --since 1.week.ago');
|
|
341
|
+
const entries = await os.changelog(since);
|
|
342
|
+
if (opts.json)
|
|
343
|
+
return jsonOut(entries);
|
|
344
|
+
return formatChangelog(entries);
|
|
345
|
+
}
|
|
346
|
+
// --- Batch ---
|
|
347
|
+
// All enums are built from contracts.ts tuples so help-renderer, file validators,
|
|
348
|
+
// and CLI-input validator share a single source of truth. cli-schema-parity.test.ts
|
|
349
|
+
// enforces this on every build.
|
|
350
|
+
// Every z.object(...) is .strict() — unknown keys become `unrecognized_keys` issues
|
|
351
|
+
// (not silently dropped), which is load-bearing for the error-formatter's did-you-mean
|
|
352
|
+
// hints and for catching typos like `{"kind": "parent"}` on a link op.
|
|
353
|
+
export const SessionLogSchema = z.object({
|
|
354
|
+
type: z.enum([...SESSION_LOG_TYPES]),
|
|
355
|
+
summary: z.string(),
|
|
356
|
+
body: z.string().optional(),
|
|
357
|
+
agent: z.string(),
|
|
358
|
+
}).strict();
|
|
359
|
+
export const UpdateFieldsSchema = z.object({
|
|
360
|
+
status: z.enum([...ENTITY_STATUSES]).optional(),
|
|
361
|
+
current_state: z.string().optional(),
|
|
362
|
+
session_log: SessionLogSchema.optional(),
|
|
363
|
+
brief: z.string().nullable().optional(),
|
|
364
|
+
add_action: z.string().optional(),
|
|
365
|
+
add_actions: z.array(z.string()).optional(),
|
|
366
|
+
complete_action: z.string().optional(),
|
|
367
|
+
remove_action: z.string().optional(),
|
|
368
|
+
clear_actions: z.boolean().optional(),
|
|
369
|
+
// Knowledge metadata
|
|
370
|
+
description: z.string().nullable().optional(),
|
|
371
|
+
resource: z.string().nullable().optional(),
|
|
372
|
+
domain: z.array(z.string()).optional(),
|
|
373
|
+
confidence: z.enum([...CONFIDENCE_LEVELS]).optional(),
|
|
374
|
+
tags: z.array(z.string()).optional(),
|
|
375
|
+
related: z.array(z.string()).optional(),
|
|
376
|
+
synthesis: z.string().optional(),
|
|
377
|
+
insight: z.string().optional(), // alias for synthesis (normalized in parseBatchInput)
|
|
378
|
+
add_evidence: z.string().optional(),
|
|
379
|
+
add_source: z.string().optional(),
|
|
380
|
+
}).strict();
|
|
381
|
+
const CreateEntityFieldsSchema = z.object({
|
|
382
|
+
type: z.enum([...ENTITY_TYPES]),
|
|
383
|
+
purpose: z.string().optional(),
|
|
384
|
+
description: z.string().optional(),
|
|
385
|
+
resource: z.string().optional(),
|
|
386
|
+
domain: z.array(z.string()).optional(),
|
|
387
|
+
parent: z.string().optional(),
|
|
388
|
+
related: z.array(z.string()).optional(),
|
|
389
|
+
tags: z.array(z.string()).optional(),
|
|
390
|
+
brief: z.string().optional(),
|
|
391
|
+
add_action: z.string().optional(),
|
|
392
|
+
}).strict();
|
|
393
|
+
const CreateKnowledgeFieldsSchema = z.object({
|
|
394
|
+
subtype: z.enum([...VALID_SUBTYPES]).optional(),
|
|
395
|
+
synthesis: z.string().optional(),
|
|
396
|
+
insight: z.string().optional(),
|
|
397
|
+
description: z.string().optional(),
|
|
398
|
+
domain: z.array(z.string()).optional(),
|
|
399
|
+
related: z.array(z.string()).optional(),
|
|
400
|
+
tags: z.array(z.string()).optional(),
|
|
401
|
+
'session-date': z.string().optional(),
|
|
402
|
+
}).strict();
|
|
403
|
+
const CreateSessionFieldsSchema = z.object({
|
|
404
|
+
synthesis: z.string().optional(),
|
|
405
|
+
insight: z.string().optional(),
|
|
406
|
+
related: z.array(z.string()).optional(),
|
|
407
|
+
'session-date': z.string().optional(),
|
|
408
|
+
}).strict();
|
|
409
|
+
const CreateEntityBranch = z.object({
|
|
410
|
+
op: z.literal('create'),
|
|
411
|
+
kind: z.literal('entity'),
|
|
412
|
+
name: z.string(),
|
|
413
|
+
fields: CreateEntityFieldsSchema,
|
|
414
|
+
}).strict();
|
|
415
|
+
const CreateKnowledgeBranch = z.object({
|
|
416
|
+
op: z.literal('create'),
|
|
417
|
+
kind: z.literal('knowledge'),
|
|
418
|
+
name: z.string(),
|
|
419
|
+
fields: CreateKnowledgeFieldsSchema.optional(),
|
|
420
|
+
}).strict();
|
|
421
|
+
const CreateSessionBranch = z.object({
|
|
422
|
+
op: z.literal('create'),
|
|
423
|
+
kind: z.literal('session'),
|
|
424
|
+
name: z.string(),
|
|
425
|
+
fields: CreateSessionFieldsSchema.optional(),
|
|
426
|
+
}).strict();
|
|
427
|
+
// Nested discriminated union: outer on `op`, inner on `kind` for create branch.
|
|
428
|
+
// A flat `discriminatedUnion('op', [...])` cannot work because three branches share
|
|
429
|
+
// op=create — Zod requires unique discriminator values per branch and throws at
|
|
430
|
+
// schema construction time if violated.
|
|
431
|
+
const CreateBranch = z.discriminatedUnion('kind', [
|
|
432
|
+
CreateEntityBranch,
|
|
433
|
+
CreateKnowledgeBranch,
|
|
434
|
+
CreateSessionBranch,
|
|
435
|
+
]);
|
|
436
|
+
export const BatchOperationSchema = z.discriminatedUnion('op', [
|
|
437
|
+
z.object({
|
|
438
|
+
op: z.literal('update'),
|
|
439
|
+
slug: z.string(),
|
|
440
|
+
fields: UpdateFieldsSchema,
|
|
441
|
+
}).strict(),
|
|
442
|
+
// create is a nested discriminatedUnion on `kind` — see CreateBranch above.
|
|
443
|
+
// discriminatedUnion accepts a ZodDiscriminatedUnion as a branch via option-flattening.
|
|
444
|
+
CreateBranch,
|
|
445
|
+
z.object({
|
|
446
|
+
op: z.literal('capture'),
|
|
447
|
+
name: z.string(),
|
|
448
|
+
notes: z.string().optional(),
|
|
449
|
+
}).strict(),
|
|
450
|
+
z.object({
|
|
451
|
+
op: z.literal('link'),
|
|
452
|
+
slug: z.string(),
|
|
453
|
+
target: z.string(),
|
|
454
|
+
relation: z.enum(['parent', 'related']),
|
|
455
|
+
}).strict(),
|
|
456
|
+
z.object({
|
|
457
|
+
op: z.literal('unlink'),
|
|
458
|
+
slug: z.string(),
|
|
459
|
+
target: z.string(),
|
|
460
|
+
relation: z.enum(['parent', 'related']),
|
|
461
|
+
}).strict(),
|
|
462
|
+
z.object({
|
|
463
|
+
op: z.literal('promote'),
|
|
464
|
+
slug: z.string(),
|
|
465
|
+
target: z.enum(['entity', 'project', 'knowledge']),
|
|
466
|
+
}).strict(),
|
|
467
|
+
]);
|
|
468
|
+
export function parseBatchInput(json) {
|
|
469
|
+
let parsed;
|
|
470
|
+
try {
|
|
471
|
+
parsed = JSON.parse(json);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
throw new HafezError('VALIDATION_FAILED', 'Invalid JSON input for batch');
|
|
475
|
+
}
|
|
476
|
+
if (!Array.isArray(parsed)) {
|
|
477
|
+
throw new HafezError('VALIDATION_FAILED', 'Batch input must be a JSON array');
|
|
478
|
+
}
|
|
479
|
+
if (parsed.length === 0) {
|
|
480
|
+
throw new HafezError('VALIDATION_FAILED', 'Batch input must not be empty');
|
|
481
|
+
}
|
|
482
|
+
// Multi-error collect: iterate every op, accumulate formatted failure blocks,
|
|
483
|
+
// throw once at the end with all N errors. Zero ops apply if any fail.
|
|
484
|
+
const operations = [];
|
|
485
|
+
const failureBlocks = [];
|
|
486
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
487
|
+
const raw = parsed[i];
|
|
488
|
+
// Deprecated op: synthesize a failure block so it joins the multi-error
|
|
489
|
+
// array rather than short-circuiting.
|
|
490
|
+
if (raw?.op === 'reinforce') {
|
|
491
|
+
const slugPart = raw.slug ? String(raw.slug) : '...';
|
|
492
|
+
failureBlocks.push([
|
|
493
|
+
`[index ${i}] op=reinforce`,
|
|
494
|
+
` field: op`,
|
|
495
|
+
` issue: "reinforce" is deprecated`,
|
|
496
|
+
` use: {"op": "update", "slug": "${slugPart}", "fields": {"add_evidence": "your evidence"}}`,
|
|
497
|
+
` tip: run \`hafez schema update\` for the full field list`,
|
|
498
|
+
].join('\n'));
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const result = BatchOperationSchema.safeParse(raw);
|
|
502
|
+
if (!result.success) {
|
|
503
|
+
failureBlocks.push(formatBatchError(result.error.issues, i, raw));
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const op = result.data;
|
|
507
|
+
// Alias: insight → synthesis for create knowledge/session and update ops.
|
|
508
|
+
// Runs only on successfully-parsed ops, preserving prior semantics.
|
|
509
|
+
if (op.fields) {
|
|
510
|
+
if (op.fields.insight && !op.fields.synthesis) {
|
|
511
|
+
op.fields.synthesis = op.fields.insight;
|
|
512
|
+
delete op.fields.insight;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
operations.push(op);
|
|
516
|
+
}
|
|
517
|
+
if (failureBlocks.length > 0) {
|
|
518
|
+
const header = `Batch contains ${failureBlocks.length} invalid operation${failureBlocks.length === 1 ? '' : 's'} (0 of ${parsed.length} applied, batch aborted):`;
|
|
519
|
+
throw new HafezError('VALIDATION_FAILED', header, failureBlocks);
|
|
520
|
+
}
|
|
521
|
+
return operations;
|
|
522
|
+
}
|
|
523
|
+
function readStdin() {
|
|
524
|
+
return new Promise((resolve, reject) => {
|
|
525
|
+
let data = '';
|
|
526
|
+
process.stdin.setEncoding('utf-8');
|
|
527
|
+
process.stdin.on('data', chunk => { data += chunk; });
|
|
528
|
+
process.stdin.on('end', () => resolve(data));
|
|
529
|
+
process.stdin.on('error', reject);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
export async function cmdBatch(os, args, opts = {}) {
|
|
533
|
+
const dryRun = args.includes('--dry-run');
|
|
534
|
+
const input = await readStdin();
|
|
535
|
+
if (!input.trim()) {
|
|
536
|
+
throw new HafezError('VALIDATION_FAILED', 'No input received. Pipe JSON array to stdin: echo \'[...]\' | hafez batch');
|
|
537
|
+
}
|
|
538
|
+
const operations = parseBatchInput(input);
|
|
539
|
+
if (dryRun) {
|
|
540
|
+
if (opts.json)
|
|
541
|
+
return jsonOut({ valid: true, operations: operations.length });
|
|
542
|
+
return `Batch valid: ${operations.length} operations would be applied`;
|
|
543
|
+
}
|
|
544
|
+
const results = await os.batch(operations);
|
|
545
|
+
if (opts.json)
|
|
546
|
+
return jsonOut(results);
|
|
547
|
+
const created = results.filter(r => r.created).map(r => r.slug);
|
|
548
|
+
let output = `Batch complete: ${operations.length} operations`;
|
|
549
|
+
if (created.length > 0)
|
|
550
|
+
output += ` (created: ${created.join(', ')})`;
|
|
551
|
+
return output;
|
|
552
|
+
}
|
|
553
|
+
export async function cmdDigest(os, _args, _opts = {}) {
|
|
554
|
+
// Read stdin — reuses the existing readStdin() defined in this file
|
|
555
|
+
const raw = await readStdin();
|
|
556
|
+
if (!raw.trim()) {
|
|
557
|
+
throw new Error('No input received. Pipe JSON object to stdin: echo \'{"entities_touched":...}\' | hafez digest');
|
|
558
|
+
}
|
|
559
|
+
// Parse JSON
|
|
560
|
+
let parsed;
|
|
561
|
+
try {
|
|
562
|
+
parsed = JSON.parse(raw);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
throw new Error('Invalid JSON on stdin: ' + (raw.slice(0, 80)));
|
|
566
|
+
}
|
|
567
|
+
const result = parseDigestInput(parsed);
|
|
568
|
+
if (!result.success) {
|
|
569
|
+
const fields = result.error.issues.map(i => i.path.join('.')).join(', ');
|
|
570
|
+
throw new Error(`Invalid digest input — missing or invalid fields: ${fields}`);
|
|
571
|
+
}
|
|
572
|
+
// Warn about unknown fields
|
|
573
|
+
if (result.unknownFields.length > 0) {
|
|
574
|
+
process.stderr.write(`Warning: digest input contains unknown fields (may indicate Zod safeParse silent-strip): ${result.unknownFields.join(', ')}\n`);
|
|
575
|
+
}
|
|
576
|
+
// Resolve existing entity slugs — digest only generates update ops for entities
|
|
577
|
+
const { items: entityItems } = await os.query({ filter: 'all', limit: 9999 });
|
|
578
|
+
const existingSlugs = new Set(entityItems.map(i => i.slug));
|
|
579
|
+
// Warn about unknown entity slugs
|
|
580
|
+
const unknownSlugs = result.data.entities_touched.filter(s => !existingSlugs.has(s));
|
|
581
|
+
for (const slug of unknownSlugs) {
|
|
582
|
+
process.stderr.write(`Warning: entity slug '${slug}' not found in vault — skipping session log update\n`);
|
|
583
|
+
}
|
|
584
|
+
const ops = digest(result.data, existingSlugs);
|
|
585
|
+
return JSON.stringify(ops, null, 2);
|
|
586
|
+
}
|
|
587
|
+
export async function cmdMigrateKnowledgeV2(args, vaultPath) {
|
|
588
|
+
const apply = args.includes('--apply');
|
|
589
|
+
const report = migrateKnowledgeV2(vaultPath, { apply });
|
|
590
|
+
console.log(`## Migration: knowledge-v2 ${apply ? '(APPLIED)' : '(dry run)'}`);
|
|
591
|
+
console.log(`- Sessions to move: ${report.sessionsToMove.length}`);
|
|
592
|
+
for (const f of report.sessionsToMove)
|
|
593
|
+
console.log(` - ${f}`);
|
|
594
|
+
console.log(`- Empty notes to delete: ${report.emptyToDelete.length}`);
|
|
595
|
+
for (const f of report.emptyToDelete)
|
|
596
|
+
console.log(` - ${f}`);
|
|
597
|
+
console.log(`- ## Insight → ## Synthesis: ${report.insightToRename.length}`);
|
|
598
|
+
for (const f of report.insightToRename)
|
|
599
|
+
console.log(` - ${f}`);
|
|
600
|
+
console.log(`- ## Related to generate: ${report.relatedToGenerate.length}`);
|
|
601
|
+
for (const f of report.relatedToGenerate)
|
|
602
|
+
console.log(` - ${f}`);
|
|
603
|
+
console.log(`- .gitignore needs update: ${report.gitignoreNeeded}`);
|
|
604
|
+
if (apply && report.gitCommitFailed)
|
|
605
|
+
console.log('⚠ Warning: git commit failed — changes are applied but not committed.');
|
|
606
|
+
if (!apply)
|
|
607
|
+
console.log('\nRun with --apply to execute migration.');
|
|
608
|
+
}
|
|
609
|
+
// --- Arg helpers ---
|
|
610
|
+
function getFlag(args, flag) {
|
|
611
|
+
const idx = args.indexOf(flag);
|
|
612
|
+
if (idx === -1)
|
|
613
|
+
return undefined;
|
|
614
|
+
return args[idx + 1];
|
|
615
|
+
}
|
|
616
|
+
/** Extract positional args by skipping --flag value pairs */
|
|
617
|
+
function positionalArgs(args) {
|
|
618
|
+
const result = [];
|
|
619
|
+
for (let i = 0; i < args.length; i++) {
|
|
620
|
+
if (args[i].startsWith('--')) {
|
|
621
|
+
i++; // skip the flag's value too
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
result.push(args[i]);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return result;
|
|
628
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { QueryResult, KnowledgeQueryResult, ValidationReport, SearchResult, VaultStats, ChangelogEntry } from '../types.js';
|
|
2
|
+
export declare function formatEntityHeader(fm: Record<string, any>, body: string): string;
|
|
3
|
+
export declare function formatKnowledgeHeader(fm: Record<string, any>, body: string): string;
|
|
4
|
+
export declare function formatQueryTable(results: QueryResult[], total?: number): string;
|
|
5
|
+
export declare function formatKnowledgeTable(results: KnowledgeQueryResult[], total?: number): string;
|
|
6
|
+
export declare function formatSearchResults(results: SearchResult[]): string;
|
|
7
|
+
export declare function formatStats(stats: VaultStats): string;
|
|
8
|
+
export declare function formatChangelog(entries: ChangelogEntry[]): string;
|
|
9
|
+
export declare function formatValidation(report: ValidationReport): string;
|