hafez 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/dist/batch-ops.d.ts +187 -0
- package/dist/batch-ops.js +322 -0
- package/dist/cli/args.d.ts +15 -0
- package/dist/cli/args.js +49 -0
- package/dist/cli/batch-errors.js +6 -3
- package/dist/cli/commands.d.ts +1 -109
- package/dist/cli/commands.js +197 -256
- package/dist/cli/help-agent.d.ts +0 -5
- package/dist/cli/help-agent.js +37 -9
- package/dist/cli/index.js +61 -69
- package/dist/cli/onboard.d.ts +1 -1
- package/dist/cli/onboard.js +7 -1
- package/dist/cli/resolve-vault.js +2 -1
- package/dist/cli/schema-introspect.d.ts +1 -1
- package/dist/cli/schema-introspect.js +6 -22
- package/dist/cli.js +20 -2
- package/dist/contracts.d.ts +6 -0
- package/dist/contracts.js +8 -2
- package/dist/db.js +20 -30
- package/dist/digest.js +2 -0
- package/dist/document.d.ts +108 -0
- package/dist/{sections.js → document.js} +173 -22
- package/dist/export-okf.js +10 -22
- package/dist/git.d.ts +13 -7
- package/dist/git.js +79 -21
- package/dist/index.d.ts +3 -4
- package/dist/index.js +508 -406
- package/dist/knowledge-index.js +9 -7
- package/dist/merge.js +13 -25
- package/dist/query.d.ts +1 -1
- package/dist/query.js +3 -1
- package/dist/types.d.ts +62 -1
- package/dist/vault.d.ts +34 -6
- package/dist/vault.js +71 -7
- package/package.json +4 -4
- package/skill/SKILL.md +7 -1
- package/dist/parser.d.ts +0 -18
- package/dist/parser.js +0 -68
- package/dist/sections.d.ts +0 -54
package/dist/cli/commands.js
CHANGED
|
@@ -1,44 +1,56 @@
|
|
|
1
1
|
// src/cli/commands.ts
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { parseArgs, splitList, str, UsageError } from './args.js';
|
|
3
4
|
import { HafezError } from '../types.js';
|
|
4
|
-
import {
|
|
5
|
+
import { BatchOperationSchema } from '../batch-ops.js';
|
|
5
6
|
import { formatBatchError } from './batch-errors.js';
|
|
6
7
|
import { parseDigestInput, digest } from '../digest.js';
|
|
7
8
|
import { formatEntityHeader, formatKnowledgeHeader, formatQueryTable, formatKnowledgeTable, formatSearchResults, formatValidation, formatStats, formatChangelog } from './format.js';
|
|
8
9
|
function jsonOut(data) {
|
|
9
10
|
return JSON.stringify(data, null, 2);
|
|
10
11
|
}
|
|
12
|
+
const READ_USAGE = 'Usage: hafez read <slug> [--depth frontmatter|summary|full]';
|
|
13
|
+
const READ_FLAGS = { depth: 'string' };
|
|
11
14
|
export async function cmdRead(os, args, opts = {}) {
|
|
12
|
-
const
|
|
15
|
+
const { flags, positionals } = parseArgs(args, READ_FLAGS, READ_USAGE);
|
|
16
|
+
const slug = positionals[0];
|
|
13
17
|
if (!slug)
|
|
14
|
-
throw new
|
|
15
|
-
const depth = (
|
|
18
|
+
throw new UsageError(READ_USAGE);
|
|
19
|
+
const depth = (str(flags.depth) ?? 'summary');
|
|
16
20
|
const result = await os.read(slug, depth);
|
|
17
21
|
if (opts.json)
|
|
18
22
|
return jsonOut(result);
|
|
19
23
|
const fm = result.frontmatter;
|
|
20
|
-
if ('
|
|
24
|
+
if (result.kind === 'entity')
|
|
21
25
|
return formatEntityHeader(fm, result.body);
|
|
22
26
|
return formatKnowledgeHeader(fm, result.body);
|
|
23
27
|
}
|
|
28
|
+
const QUERY_USAGE = 'Usage: hafez query [--kind entity|knowledge|all] [--filter ...] [--type t] [--parent slug] [--related-to slug] [--domain d] [--confidence c] [--subtype s] [--tag t] [--since d] [--before d] [--created-since d] [--created-before d] [--sort-by f] [--sort-order asc|desc] [--limit n] [--offset n]';
|
|
29
|
+
const QUERY_FLAGS = {
|
|
30
|
+
kind: 'string', filter: 'string', type: 'string', parent: 'string', 'related-to': 'string',
|
|
31
|
+
domain: 'string', confidence: 'string', subtype: 'string', tag: 'string',
|
|
32
|
+
since: 'string', before: 'string', 'created-since': 'string', 'created-before': 'string',
|
|
33
|
+
'sort-by': 'string', 'sort-order': 'string', limit: 'string', offset: 'string',
|
|
34
|
+
};
|
|
24
35
|
export async function cmdQuery(os, args, opts = {}) {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
36
|
+
const { flags } = parseArgs(args, QUERY_FLAGS, QUERY_USAGE);
|
|
37
|
+
const kind = str(flags.kind) ?? 'entity';
|
|
38
|
+
const filter = (str(flags.filter) ?? 'active');
|
|
39
|
+
const type = str(flags.type);
|
|
40
|
+
const parent = str(flags.parent);
|
|
41
|
+
const relatedTo = str(flags['related-to']);
|
|
42
|
+
const domain = str(flags.domain);
|
|
43
|
+
const confidence = str(flags.confidence);
|
|
44
|
+
const since = str(flags.since);
|
|
45
|
+
const before = str(flags.before);
|
|
46
|
+
const createdSince = str(flags['created-since']);
|
|
47
|
+
const createdBefore = str(flags['created-before']);
|
|
48
|
+
const tagRaw = str(flags.tag);
|
|
37
49
|
const tags = tagRaw ? tagRaw.split(',').map(t => t.trim()).filter(Boolean) : undefined;
|
|
38
|
-
const sortBy =
|
|
39
|
-
const sortOrder =
|
|
40
|
-
const limitRaw =
|
|
41
|
-
const offsetRaw =
|
|
50
|
+
const sortBy = str(flags['sort-by']);
|
|
51
|
+
const sortOrder = str(flags['sort-order']);
|
|
52
|
+
const limitRaw = str(flags.limit);
|
|
53
|
+
const offsetRaw = str(flags.offset);
|
|
42
54
|
const limit = limitRaw != null ? parseInt(limitRaw, 10) : undefined;
|
|
43
55
|
const offset = offsetRaw != null ? parseInt(offsetRaw, 10) : undefined;
|
|
44
56
|
const sortOpts = { sort_by: sortBy, sort_order: sortOrder, limit, offset };
|
|
@@ -48,8 +60,8 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
48
60
|
if (opts.json)
|
|
49
61
|
return jsonOut(result);
|
|
50
62
|
const { items: results, total } = result;
|
|
51
|
-
const entities = results.filter(
|
|
52
|
-
const knowledge = results.filter(
|
|
63
|
+
const entities = results.filter(r => r.kind === 'entity');
|
|
64
|
+
const knowledge = results.filter(r => r.kind === 'knowledge');
|
|
53
65
|
const sections = [];
|
|
54
66
|
if (entities.length > 0)
|
|
55
67
|
sections.push('## Entities\n\n' + formatQueryTable(entities, total));
|
|
@@ -61,7 +73,7 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
61
73
|
// --kind knowledge: query knowledge only
|
|
62
74
|
// Note: time flags only apply to entity queries.
|
|
63
75
|
if (kind === 'knowledge') {
|
|
64
|
-
const subtype =
|
|
76
|
+
const subtype = str(flags.subtype);
|
|
65
77
|
const kOpts = { domain: domain ?? undefined, confidence, subtype, tags, ...sortOpts };
|
|
66
78
|
const result = await os.query_knowledge(kOpts);
|
|
67
79
|
if (opts.json)
|
|
@@ -91,127 +103,149 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
91
103
|
return jsonOut(result);
|
|
92
104
|
return formatQueryTable(result.items, result.total);
|
|
93
105
|
}
|
|
106
|
+
const SEARCH_USAGE = 'Usage: hafez search <terms> [--kind entity|knowledge|all]';
|
|
107
|
+
const SEARCH_FLAGS = { kind: 'string' };
|
|
94
108
|
export async function cmdSearch(os, args, opts = {}) {
|
|
95
|
-
const
|
|
109
|
+
const { flags, positionals } = parseArgs(args, SEARCH_FLAGS, SEARCH_USAGE);
|
|
110
|
+
const terms = positionals.join(' ');
|
|
96
111
|
if (!terms)
|
|
97
|
-
throw new
|
|
98
|
-
const kind = (
|
|
112
|
+
throw new UsageError(SEARCH_USAGE);
|
|
113
|
+
const kind = (str(flags.kind) ?? 'all');
|
|
99
114
|
const results = await os.search(terms, kind);
|
|
100
115
|
if (opts.json)
|
|
101
116
|
return jsonOut(results);
|
|
102
117
|
return formatSearchResults(results);
|
|
103
118
|
}
|
|
119
|
+
const CREATE_USAGE = 'Usage: hafez create <entity|knowledge> <name> [options]';
|
|
120
|
+
const CREATE_FLAGS = {
|
|
121
|
+
type: 'string', purpose: 'string', description: 'string', resource: 'string',
|
|
122
|
+
domain: 'string', parent: 'string', related: 'string', tags: 'string',
|
|
123
|
+
brief: 'string', 'add-action': 'string',
|
|
124
|
+
subtype: 'string', synthesis: 'string', insight: 'string',
|
|
125
|
+
};
|
|
104
126
|
export async function cmdCreate(os, args, opts = {}) {
|
|
105
|
-
const
|
|
127
|
+
const { flags, positionals } = parseArgs(args, CREATE_FLAGS, CREATE_USAGE);
|
|
128
|
+
const kind = positionals[0];
|
|
106
129
|
if (!kind)
|
|
107
|
-
throw new
|
|
108
|
-
const name =
|
|
130
|
+
throw new UsageError(CREATE_USAGE);
|
|
131
|
+
const name = positionals.slice(1).join(' ');
|
|
109
132
|
if (!name)
|
|
110
|
-
throw new
|
|
133
|
+
throw new UsageError(`Usage: hafez create ${kind} <name> [options]`);
|
|
111
134
|
if (kind === 'entity') {
|
|
112
|
-
const type =
|
|
135
|
+
const type = str(flags.type);
|
|
113
136
|
if (!type)
|
|
114
|
-
throw new
|
|
137
|
+
throw new UsageError('--type is required for entity creation');
|
|
115
138
|
const slug = await os.create('entity', name, {
|
|
116
139
|
type,
|
|
117
|
-
purpose:
|
|
118
|
-
description:
|
|
119
|
-
resource:
|
|
120
|
-
domain:
|
|
121
|
-
parent:
|
|
122
|
-
related:
|
|
123
|
-
tags:
|
|
124
|
-
brief:
|
|
125
|
-
add_action:
|
|
140
|
+
purpose: str(flags.purpose),
|
|
141
|
+
description: str(flags.description),
|
|
142
|
+
resource: str(flags.resource),
|
|
143
|
+
domain: splitList(flags.domain),
|
|
144
|
+
parent: str(flags.parent),
|
|
145
|
+
related: str(flags.related)?.split(','),
|
|
146
|
+
tags: str(flags.tags)?.split(','),
|
|
147
|
+
brief: str(flags.brief),
|
|
148
|
+
add_action: str(flags['add-action']),
|
|
126
149
|
});
|
|
127
150
|
return opts.json ? jsonOut({ slug }) : slug;
|
|
128
151
|
}
|
|
129
152
|
if (kind === 'knowledge') {
|
|
130
153
|
const slug = await os.create('knowledge', name, {
|
|
131
|
-
subtype:
|
|
132
|
-
synthesis:
|
|
133
|
-
description:
|
|
134
|
-
domain:
|
|
135
|
-
related:
|
|
136
|
-
tags:
|
|
154
|
+
subtype: str(flags.subtype),
|
|
155
|
+
synthesis: str(flags.synthesis) ?? str(flags.insight),
|
|
156
|
+
description: str(flags.description),
|
|
157
|
+
domain: str(flags.domain)?.split(','),
|
|
158
|
+
related: str(flags.related)?.split(','),
|
|
159
|
+
tags: str(flags.tags)?.split(','),
|
|
137
160
|
});
|
|
138
161
|
return opts.json ? jsonOut({ slug }) : slug;
|
|
139
162
|
}
|
|
140
|
-
throw new
|
|
163
|
+
throw new UsageError(`Unknown kind: ${kind}. Use entity or knowledge.`);
|
|
141
164
|
}
|
|
165
|
+
const UPDATE_USAGE = '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"]';
|
|
166
|
+
const UPDATE_FLAGS = {
|
|
167
|
+
status: 'string', 'current-state': 'string', brief: 'string',
|
|
168
|
+
'add-action': 'string', 'complete-action': 'string', 'remove-action': 'string',
|
|
169
|
+
'clear-actions': 'boolean', log: 'string', agent: 'string',
|
|
170
|
+
description: 'string', resource: 'string', domain: 'string', confidence: 'string',
|
|
171
|
+
tags: 'string', related: 'string', insight: 'string', synthesis: 'string',
|
|
172
|
+
'add-evidence': 'string', 'add-source': 'string',
|
|
173
|
+
// Removed flag, declared so it fails with guidance instead of a generic
|
|
174
|
+
// unknown-flag error: older installed skills/scripts still pass it, and
|
|
175
|
+
// "Updated <slug>" with the action discarded is a false success.
|
|
176
|
+
'next-action': 'string',
|
|
177
|
+
};
|
|
142
178
|
export async function cmdUpdate(os, args, opts = {}) {
|
|
143
|
-
const
|
|
179
|
+
const { flags, positionals } = parseArgs(args, UPDATE_FLAGS, UPDATE_USAGE);
|
|
180
|
+
const slug = positionals[0];
|
|
144
181
|
if (!slug)
|
|
145
|
-
throw new
|
|
182
|
+
throw new UsageError(UPDATE_USAGE);
|
|
183
|
+
if (flags['next-action'] !== undefined) {
|
|
184
|
+
throw new UsageError('--next-action was removed in v1.0.2. Use --add-action (and --clear-actions) instead.');
|
|
185
|
+
}
|
|
146
186
|
const fields = {};
|
|
147
|
-
const status =
|
|
187
|
+
const status = str(flags.status);
|
|
148
188
|
if (status)
|
|
149
189
|
fields.status = status;
|
|
150
|
-
|
|
151
|
-
// installed skills/scripts still pass it, and "Updated <slug>" with the
|
|
152
|
-
// action discarded is a false success.
|
|
153
|
-
if (args.includes('--next-action')) {
|
|
154
|
-
throw new Error('--next-action was removed in v1.0.2. Use --add-action (and --clear-actions) instead.');
|
|
155
|
-
}
|
|
156
|
-
const currentState = getFlag(args, '--current-state');
|
|
190
|
+
const currentState = str(flags['current-state']);
|
|
157
191
|
if (currentState)
|
|
158
192
|
fields.current_state = currentState;
|
|
159
193
|
// Brief
|
|
160
|
-
const brief =
|
|
194
|
+
const brief = str(flags.brief);
|
|
161
195
|
if (brief !== undefined) {
|
|
162
196
|
fields.brief = brief === '' ? null : brief;
|
|
163
197
|
}
|
|
164
198
|
// Next Actions
|
|
165
|
-
const addAction =
|
|
199
|
+
const addAction = str(flags['add-action']);
|
|
166
200
|
if (addAction)
|
|
167
201
|
fields.add_action = addAction;
|
|
168
|
-
const completeAction =
|
|
202
|
+
const completeAction = str(flags['complete-action']);
|
|
169
203
|
if (completeAction)
|
|
170
204
|
fields.complete_action = completeAction;
|
|
171
|
-
const removeAction =
|
|
205
|
+
const removeAction = str(flags['remove-action']);
|
|
172
206
|
if (removeAction)
|
|
173
207
|
fields.remove_action = removeAction;
|
|
174
|
-
if (
|
|
208
|
+
if (flags['clear-actions'])
|
|
175
209
|
fields.clear_actions = true;
|
|
176
|
-
const log =
|
|
210
|
+
const log = str(flags.log);
|
|
177
211
|
if (log) {
|
|
178
212
|
const colonIdx = log.indexOf(':');
|
|
179
213
|
if (colonIdx === -1)
|
|
180
|
-
throw new
|
|
214
|
+
throw new UsageError('--log format: "type: summary". Types: progress, decision, blocker, research');
|
|
181
215
|
const type = log.slice(0, colonIdx).trim();
|
|
182
216
|
const summary = log.slice(colonIdx + 1).trim();
|
|
183
|
-
const agent =
|
|
217
|
+
const agent = str(flags.agent) ?? 'claude';
|
|
184
218
|
fields.session_log = { type, summary, agent };
|
|
185
219
|
}
|
|
186
220
|
// Shared metadata (entity + knowledge)
|
|
187
|
-
const description =
|
|
221
|
+
const description = str(flags.description);
|
|
188
222
|
if (description !== undefined)
|
|
189
223
|
fields.description = description;
|
|
190
|
-
const resource =
|
|
224
|
+
const resource = str(flags.resource);
|
|
191
225
|
if (resource !== undefined)
|
|
192
226
|
fields.resource = resource;
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
195
|
-
fields.domain =
|
|
196
|
-
const conf =
|
|
227
|
+
const domain = splitList(flags.domain);
|
|
228
|
+
if (domain)
|
|
229
|
+
fields.domain = domain;
|
|
230
|
+
const conf = str(flags.confidence);
|
|
197
231
|
if (conf)
|
|
198
232
|
fields.confidence = conf;
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
201
|
-
fields.tags =
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
204
|
-
fields.related =
|
|
205
|
-
const insight =
|
|
233
|
+
const tags = splitList(flags.tags);
|
|
234
|
+
if (tags)
|
|
235
|
+
fields.tags = tags;
|
|
236
|
+
const related = splitList(flags.related);
|
|
237
|
+
if (related)
|
|
238
|
+
fields.related = related;
|
|
239
|
+
const insight = str(flags.insight);
|
|
206
240
|
if (insight)
|
|
207
241
|
fields.synthesis = insight;
|
|
208
|
-
const synthesis =
|
|
242
|
+
const synthesis = str(flags.synthesis);
|
|
209
243
|
if (synthesis)
|
|
210
244
|
fields.synthesis = synthesis;
|
|
211
|
-
const addEvidence =
|
|
245
|
+
const addEvidence = str(flags['add-evidence']);
|
|
212
246
|
if (addEvidence)
|
|
213
247
|
fields.add_evidence = addEvidence;
|
|
214
|
-
const addSource =
|
|
248
|
+
const addSource = str(flags['add-source']);
|
|
215
249
|
if (addSource)
|
|
216
250
|
fields.add_source = addSource;
|
|
217
251
|
const result = await os.update(slug, fields);
|
|
@@ -223,23 +257,17 @@ export async function cmdUpdate(os, args, opts = {}) {
|
|
|
223
257
|
}
|
|
224
258
|
return lines.join('\n');
|
|
225
259
|
}
|
|
260
|
+
const LINK_FLAGS = { relation: 'string' };
|
|
226
261
|
function parseLinkArgs(args, verb) {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const positionals = args.filter((a, i) => {
|
|
231
|
-
if (a === '--relation')
|
|
232
|
-
return false;
|
|
233
|
-
if (i > 0 && args[i - 1] === '--relation')
|
|
234
|
-
return false;
|
|
235
|
-
return !a.startsWith('--');
|
|
236
|
-
});
|
|
262
|
+
const usage = `Usage: hafez ${verb} <slug> <target> --relation <parent|related>`;
|
|
263
|
+
const { flags, positionals } = parseArgs(args, LINK_FLAGS, usage);
|
|
264
|
+
const relation = str(flags.relation);
|
|
237
265
|
const [slug, target] = positionals;
|
|
238
266
|
if (!slug || !target || !relation) {
|
|
239
|
-
throw new
|
|
267
|
+
throw new UsageError(usage);
|
|
240
268
|
}
|
|
241
269
|
if (!['parent', 'related'].includes(relation)) {
|
|
242
|
-
throw new
|
|
270
|
+
throw new UsageError(`Invalid relation: ${relation}. Must be parent or related.`);
|
|
243
271
|
}
|
|
244
272
|
return { slug, target, relation };
|
|
245
273
|
}
|
|
@@ -254,22 +282,24 @@ export async function cmdUnlink(os, args, opts = {}) {
|
|
|
254
282
|
return opts.json ? jsonOut({ slug, target, relation }) : `Unlinked ${slug} — ${target} (${relation})`;
|
|
255
283
|
}
|
|
256
284
|
/** Top-level capture command — aligns the CLI verb with the batch op name. */
|
|
285
|
+
const CAPTURE_USAGE = 'Usage: hafez capture <name> [--notes "text"]';
|
|
286
|
+
const CAPTURE_FLAGS = { notes: 'string' };
|
|
257
287
|
export async function cmdCapture(os, args, opts = {}) {
|
|
258
|
-
const
|
|
288
|
+
const { flags, positionals } = parseArgs(args, CAPTURE_FLAGS, CAPTURE_USAGE);
|
|
289
|
+
const name = positionals.join(' ');
|
|
259
290
|
if (!name)
|
|
260
|
-
throw new
|
|
261
|
-
const notes =
|
|
291
|
+
throw new UsageError(CAPTURE_USAGE);
|
|
292
|
+
const notes = str(flags.notes);
|
|
262
293
|
const slug = await os.capture(name, notes);
|
|
263
294
|
return opts.json ? jsonOut({ slug }) : slug;
|
|
264
295
|
}
|
|
296
|
+
const PROMOTE_USAGE = 'Usage: hafez promote <slug> <entity|project|knowledge>';
|
|
265
297
|
export async function cmdPromote(os, args, opts = {}) {
|
|
266
|
-
const
|
|
267
|
-
const target =
|
|
298
|
+
const { positionals } = parseArgs(args, {}, PROMOTE_USAGE);
|
|
299
|
+
const [slug, target] = positionals;
|
|
268
300
|
if (!slug || !target)
|
|
269
|
-
throw new
|
|
270
|
-
|
|
271
|
-
throw new Error(`Invalid target: ${target}. Must be entity, project, or knowledge.`);
|
|
272
|
-
}
|
|
301
|
+
throw new UsageError(PROMOTE_USAGE);
|
|
302
|
+
// Target validity is core's rule (contract canPromoteTo) — no CLI re-check.
|
|
273
303
|
await os.promote(slug, target);
|
|
274
304
|
if (opts.json)
|
|
275
305
|
return jsonOut({ slug, target });
|
|
@@ -303,10 +333,13 @@ export async function cmdStats(os, _args, opts = {}) {
|
|
|
303
333
|
return jsonOut(stats);
|
|
304
334
|
return formatStats(stats);
|
|
305
335
|
}
|
|
336
|
+
const CHANGELOG_USAGE = 'Usage: hafez changelog --since <date|relative>\nExamples: --since 2026-03-20, --since 7.days.ago, --since 1.week.ago';
|
|
337
|
+
const CHANGELOG_FLAGS = { since: 'string' };
|
|
306
338
|
export async function cmdChangelog(os, args, opts = {}) {
|
|
307
|
-
const
|
|
339
|
+
const { flags } = parseArgs(args, CHANGELOG_FLAGS, CHANGELOG_USAGE);
|
|
340
|
+
const since = str(flags.since);
|
|
308
341
|
if (!since)
|
|
309
|
-
throw new
|
|
342
|
+
throw new UsageError(CHANGELOG_USAGE);
|
|
310
343
|
const entries = await os.changelog(since);
|
|
311
344
|
if (opts.json)
|
|
312
345
|
return jsonOut(entries);
|
|
@@ -314,126 +347,11 @@ export async function cmdChangelog(os, args, opts = {}) {
|
|
|
314
347
|
}
|
|
315
348
|
// --- Batch ---
|
|
316
349
|
// All enums are built from contracts.ts tuples so help-renderer, file validators,
|
|
317
|
-
// and CLI-input validator share a single source of truth
|
|
318
|
-
//
|
|
350
|
+
// and CLI-input validator share a single source of truth — drift is structurally
|
|
351
|
+
// impossible (the Op Spec table in batch-ops.ts is the only consumer).
|
|
319
352
|
// Every z.object(...) is .strict() — unknown keys become `unrecognized_keys` issues
|
|
320
353
|
// (not silently dropped), which is load-bearing for the error-formatter's did-you-mean
|
|
321
354
|
// hints and for catching typos like `{"kind": "parent"}` on a link op.
|
|
322
|
-
export const SessionLogSchema = z.object({
|
|
323
|
-
type: z.enum([...SESSION_LOG_TYPES]),
|
|
324
|
-
summary: z.string(),
|
|
325
|
-
body: z.string().optional(),
|
|
326
|
-
agent: z.string(),
|
|
327
|
-
}).strict();
|
|
328
|
-
export const UpdateFieldsSchema = z.object({
|
|
329
|
-
status: z.enum([...ENTITY_STATUSES]).optional(),
|
|
330
|
-
current_state: z.string().optional(),
|
|
331
|
-
session_log: SessionLogSchema.optional(),
|
|
332
|
-
brief: z.string().nullable().optional(),
|
|
333
|
-
add_action: z.string().optional(),
|
|
334
|
-
add_actions: z.array(z.string()).optional(),
|
|
335
|
-
complete_action: z.string().optional(),
|
|
336
|
-
remove_action: z.string().optional(),
|
|
337
|
-
clear_actions: z.boolean().optional(),
|
|
338
|
-
// Knowledge metadata
|
|
339
|
-
description: z.string().nullable().optional(),
|
|
340
|
-
resource: z.string().nullable().optional(),
|
|
341
|
-
domain: z.array(z.string()).optional(),
|
|
342
|
-
confidence: z.enum([...CONFIDENCE_LEVELS]).optional(),
|
|
343
|
-
tags: z.array(z.string()).optional(),
|
|
344
|
-
related: z.array(z.string()).optional(),
|
|
345
|
-
synthesis: z.string().optional(),
|
|
346
|
-
insight: z.string().optional(), // alias for synthesis (normalized in parseBatchInput)
|
|
347
|
-
add_evidence: z.string().optional(),
|
|
348
|
-
add_source: z.string().optional(),
|
|
349
|
-
}).strict();
|
|
350
|
-
const CreateEntityFieldsSchema = z.object({
|
|
351
|
-
type: z.enum([...ENTITY_TYPES]),
|
|
352
|
-
purpose: z.string().optional(),
|
|
353
|
-
description: z.string().optional(),
|
|
354
|
-
resource: z.string().optional(),
|
|
355
|
-
domain: z.array(z.string()).optional(),
|
|
356
|
-
parent: z.string().optional(),
|
|
357
|
-
related: z.array(z.string()).optional(),
|
|
358
|
-
tags: z.array(z.string()).optional(),
|
|
359
|
-
brief: z.string().optional(),
|
|
360
|
-
add_action: z.string().optional(),
|
|
361
|
-
}).strict();
|
|
362
|
-
const CreateKnowledgeFieldsSchema = z.object({
|
|
363
|
-
subtype: z.enum([...VALID_SUBTYPES]).optional(),
|
|
364
|
-
synthesis: z.string().optional(),
|
|
365
|
-
insight: z.string().optional(),
|
|
366
|
-
description: z.string().optional(),
|
|
367
|
-
domain: z.array(z.string()).optional(),
|
|
368
|
-
related: z.array(z.string()).optional(),
|
|
369
|
-
tags: z.array(z.string()).optional(),
|
|
370
|
-
'session-date': z.string().optional(),
|
|
371
|
-
}).strict();
|
|
372
|
-
const CreateSessionFieldsSchema = z.object({
|
|
373
|
-
synthesis: z.string().optional(),
|
|
374
|
-
insight: z.string().optional(),
|
|
375
|
-
related: z.array(z.string()).optional(),
|
|
376
|
-
'session-date': z.string().optional(),
|
|
377
|
-
}).strict();
|
|
378
|
-
const CreateEntityBranch = z.object({
|
|
379
|
-
op: z.literal('create'),
|
|
380
|
-
kind: z.literal('entity'),
|
|
381
|
-
name: z.string(),
|
|
382
|
-
fields: CreateEntityFieldsSchema,
|
|
383
|
-
}).strict();
|
|
384
|
-
const CreateKnowledgeBranch = z.object({
|
|
385
|
-
op: z.literal('create'),
|
|
386
|
-
kind: z.literal('knowledge'),
|
|
387
|
-
name: z.string(),
|
|
388
|
-
fields: CreateKnowledgeFieldsSchema.optional(),
|
|
389
|
-
}).strict();
|
|
390
|
-
const CreateSessionBranch = z.object({
|
|
391
|
-
op: z.literal('create'),
|
|
392
|
-
kind: z.literal('session'),
|
|
393
|
-
name: z.string(),
|
|
394
|
-
fields: CreateSessionFieldsSchema.optional(),
|
|
395
|
-
}).strict();
|
|
396
|
-
// Nested discriminated union: outer on `op`, inner on `kind` for create branch.
|
|
397
|
-
// A flat `discriminatedUnion('op', [...])` cannot work because three branches share
|
|
398
|
-
// op=create — Zod requires unique discriminator values per branch and throws at
|
|
399
|
-
// schema construction time if violated.
|
|
400
|
-
const CreateBranch = z.discriminatedUnion('kind', [
|
|
401
|
-
CreateEntityBranch,
|
|
402
|
-
CreateKnowledgeBranch,
|
|
403
|
-
CreateSessionBranch,
|
|
404
|
-
]);
|
|
405
|
-
export const BatchOperationSchema = z.discriminatedUnion('op', [
|
|
406
|
-
z.object({
|
|
407
|
-
op: z.literal('update'),
|
|
408
|
-
slug: z.string(),
|
|
409
|
-
fields: UpdateFieldsSchema,
|
|
410
|
-
}).strict(),
|
|
411
|
-
// create is a nested discriminatedUnion on `kind` — see CreateBranch above.
|
|
412
|
-
// discriminatedUnion accepts a ZodDiscriminatedUnion as a branch via option-flattening.
|
|
413
|
-
CreateBranch,
|
|
414
|
-
z.object({
|
|
415
|
-
op: z.literal('capture'),
|
|
416
|
-
name: z.string(),
|
|
417
|
-
notes: z.string().optional(),
|
|
418
|
-
}).strict(),
|
|
419
|
-
z.object({
|
|
420
|
-
op: z.literal('link'),
|
|
421
|
-
slug: z.string(),
|
|
422
|
-
target: z.string(),
|
|
423
|
-
relation: z.enum(['parent', 'related']),
|
|
424
|
-
}).strict(),
|
|
425
|
-
z.object({
|
|
426
|
-
op: z.literal('unlink'),
|
|
427
|
-
slug: z.string(),
|
|
428
|
-
target: z.string(),
|
|
429
|
-
relation: z.enum(['parent', 'related']),
|
|
430
|
-
}).strict(),
|
|
431
|
-
z.object({
|
|
432
|
-
op: z.literal('promote'),
|
|
433
|
-
slug: z.string(),
|
|
434
|
-
target: z.enum(['entity', 'project', 'knowledge']),
|
|
435
|
-
}).strict(),
|
|
436
|
-
]);
|
|
437
355
|
export function parseBatchInput(json) {
|
|
438
356
|
let parsed;
|
|
439
357
|
try {
|
|
@@ -484,11 +402,28 @@ export function parseBatchInput(json) {
|
|
|
484
402
|
operations.push(op);
|
|
485
403
|
}
|
|
486
404
|
if (failureBlocks.length > 0) {
|
|
487
|
-
const header = `Batch
|
|
405
|
+
const header = `Batch failed validation: ${failureBlocks.length} invalid operation${failureBlocks.length === 1 ? '' : 's'} (0 of ${parsed.length} applied)`;
|
|
488
406
|
throw new HafezError('VALIDATION_FAILED', header, failureBlocks);
|
|
489
407
|
}
|
|
490
408
|
return operations;
|
|
491
409
|
}
|
|
410
|
+
// --file payload reader. Exists because Windows PowerShell 5.1 cannot pipe
|
|
411
|
+
// stdin to native executables — the documented pipe idiom fails there, so a
|
|
412
|
+
// file is the portable session-end path. PowerShell's own redirection writes
|
|
413
|
+
// UTF-16LE (and other tools write BOM'd UTF-8), so decode both rather than
|
|
414
|
+
// trading the pipe footgun for an encoding one.
|
|
415
|
+
function readPayloadFile(path) {
|
|
416
|
+
let buf;
|
|
417
|
+
try {
|
|
418
|
+
buf = readFileSync(path);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
throw new HafezError('VALIDATION_FAILED', `Cannot read --file: ${path}`);
|
|
422
|
+
}
|
|
423
|
+
if (buf[0] === 0xff && buf[1] === 0xfe)
|
|
424
|
+
return buf.toString('utf16le').replace(/^\uFEFF/, '');
|
|
425
|
+
return buf.toString('utf-8').replace(/^\uFEFF/, '');
|
|
426
|
+
}
|
|
492
427
|
function readStdin() {
|
|
493
428
|
return new Promise((resolve, reject) => {
|
|
494
429
|
let data = '';
|
|
@@ -498,17 +433,40 @@ function readStdin() {
|
|
|
498
433
|
process.stdin.on('error', reject);
|
|
499
434
|
});
|
|
500
435
|
}
|
|
436
|
+
const BATCH_USAGE = "Usage: hafez batch [--dry-run] [--file payload.json] (or pipe JSON to stdin)";
|
|
437
|
+
const BATCH_FLAGS = { 'dry-run': 'boolean', file: 'string' };
|
|
501
438
|
export async function cmdBatch(os, args, opts = {}) {
|
|
502
|
-
const
|
|
503
|
-
const
|
|
439
|
+
const { flags } = parseArgs(args, BATCH_FLAGS, BATCH_USAGE);
|
|
440
|
+
const dryRun = flags['dry-run'] === true;
|
|
441
|
+
const file = str(flags.file);
|
|
442
|
+
const input = file ? readPayloadFile(file) : await readStdin();
|
|
504
443
|
if (!input.trim()) {
|
|
505
|
-
throw new HafezError('VALIDATION_FAILED', 'No input received. Pipe JSON array to stdin
|
|
444
|
+
throw new HafezError('VALIDATION_FAILED', 'No input received. Pipe JSON array to stdin (echo \'[...]\' | hafez batch) or pass --file payload.json');
|
|
506
445
|
}
|
|
507
446
|
const operations = parseBatchInput(input);
|
|
508
447
|
if (dryRun) {
|
|
448
|
+
// Same validate phase as apply — dry-run can never green-light a payload
|
|
449
|
+
// apply rejects. Reports derived slugs so agents can predict create slugs.
|
|
450
|
+
const report = await os.validateBatch(operations);
|
|
509
451
|
if (opts.json)
|
|
510
|
-
return jsonOut(
|
|
511
|
-
|
|
452
|
+
return jsonOut(report);
|
|
453
|
+
if (!report.valid) {
|
|
454
|
+
const invalidCount = report.operations.filter(o => o.errors.length > 0).length;
|
|
455
|
+
const lines = report.operations.flatMap(o => o.errors.map(e => `op[${o.index}] (${o.op} ${o.slug}): ${e}`));
|
|
456
|
+
throw new HafezError('VALIDATION_FAILED', `Batch invalid: ${invalidCount} of ${operations.length} operation${operations.length === 1 ? '' : 's'} failed validation (0 would be applied)`, lines);
|
|
457
|
+
}
|
|
458
|
+
const out = [`Batch valid: ${operations.length} operations would be applied`];
|
|
459
|
+
const creates = report.operations.filter(o => o.created);
|
|
460
|
+
if (creates.length > 0) {
|
|
461
|
+
out.push('Derived slugs:');
|
|
462
|
+
for (const c of creates)
|
|
463
|
+
out.push(` op[${c.index}] ${c.op} → ${c.slug}`);
|
|
464
|
+
}
|
|
465
|
+
for (const o of report.operations) {
|
|
466
|
+
if (o.warning)
|
|
467
|
+
out.push(`Warning: op[${o.index}] ${o.warning}`);
|
|
468
|
+
}
|
|
469
|
+
return out.join('\n');
|
|
512
470
|
}
|
|
513
471
|
const results = await os.batch(operations);
|
|
514
472
|
if (opts.json)
|
|
@@ -519,11 +477,14 @@ export async function cmdBatch(os, args, opts = {}) {
|
|
|
519
477
|
output += ` (created: ${created.join(', ')})`;
|
|
520
478
|
return output;
|
|
521
479
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
480
|
+
const DIGEST_USAGE = 'Usage: hafez digest [--file summary.json] (or pipe JSON to stdin)';
|
|
481
|
+
const DIGEST_FLAGS = { file: 'string' };
|
|
482
|
+
export async function cmdDigest(os, args, _opts = {}) {
|
|
483
|
+
const { flags } = parseArgs(args, DIGEST_FLAGS, DIGEST_USAGE);
|
|
484
|
+
const file = str(flags.file);
|
|
485
|
+
const raw = file ? readPayloadFile(file) : await readStdin();
|
|
525
486
|
if (!raw.trim()) {
|
|
526
|
-
throw new
|
|
487
|
+
throw new UsageError('No input received. Pipe JSON object to stdin (echo \'{"entities_touched":...}\' | hafez digest) or pass --file summary.json');
|
|
527
488
|
}
|
|
528
489
|
// Parse JSON
|
|
529
490
|
let parsed;
|
|
@@ -531,12 +492,12 @@ export async function cmdDigest(os, _args, _opts = {}) {
|
|
|
531
492
|
parsed = JSON.parse(raw);
|
|
532
493
|
}
|
|
533
494
|
catch {
|
|
534
|
-
throw new
|
|
495
|
+
throw new HafezError('VALIDATION_FAILED', 'Invalid JSON on stdin: ' + (raw.slice(0, 80)));
|
|
535
496
|
}
|
|
536
497
|
const result = parseDigestInput(parsed);
|
|
537
498
|
if (!result.success) {
|
|
538
499
|
const fields = result.error.issues.map(i => i.path.join('.')).join(', ');
|
|
539
|
-
throw new
|
|
500
|
+
throw new HafezError('VALIDATION_FAILED', `Invalid digest input — missing or invalid fields: ${fields}`);
|
|
540
501
|
}
|
|
541
502
|
// Warn about unknown fields
|
|
542
503
|
if (result.unknownFields.length > 0) {
|
|
@@ -553,23 +514,3 @@ export async function cmdDigest(os, _args, _opts = {}) {
|
|
|
553
514
|
const ops = digest(result.data, existingSlugs);
|
|
554
515
|
return JSON.stringify(ops, null, 2);
|
|
555
516
|
}
|
|
556
|
-
// --- Arg helpers ---
|
|
557
|
-
function getFlag(args, flag) {
|
|
558
|
-
const idx = args.indexOf(flag);
|
|
559
|
-
if (idx === -1)
|
|
560
|
-
return undefined;
|
|
561
|
-
return args[idx + 1];
|
|
562
|
-
}
|
|
563
|
-
/** Extract positional args by skipping --flag value pairs */
|
|
564
|
-
function positionalArgs(args) {
|
|
565
|
-
const result = [];
|
|
566
|
-
for (let i = 0; i < args.length; i++) {
|
|
567
|
-
if (args[i].startsWith('--')) {
|
|
568
|
-
i++; // skip the flag's value too
|
|
569
|
-
}
|
|
570
|
-
else {
|
|
571
|
-
result.push(args[i]);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
return result;
|
|
575
|
-
}
|
package/dist/cli/help-agent.d.ts
CHANGED