hafez 1.2.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/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 +0 -108
- package/dist/cli/commands.js +154 -252
- package/dist/cli/help-agent.d.ts +0 -5
- package/dist/cli/help-agent.js +20 -5
- package/dist/cli/index.js +45 -65
- 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 +0 -0
- package/dist/contracts.d.ts +6 -0
- package/dist/contracts.js +8 -2
- package/dist/db.js +14 -28
- 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 +7 -8
- package/dist/git.js +39 -21
- package/dist/index.d.ts +3 -4
- package/dist/index.js +419 -517
- 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 +48 -1
- package/dist/vault.d.ts +33 -6
- package/dist/vault.js +65 -8
- package/package.json +2 -2
- 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,45 +1,56 @@
|
|
|
1
1
|
// src/cli/commands.ts
|
|
2
|
-
import { z } from 'zod';
|
|
3
2
|
import { readFileSync } from 'fs';
|
|
3
|
+
import { parseArgs, splitList, str, UsageError } from './args.js';
|
|
4
4
|
import { HafezError } from '../types.js';
|
|
5
|
-
import {
|
|
5
|
+
import { BatchOperationSchema } from '../batch-ops.js';
|
|
6
6
|
import { formatBatchError } from './batch-errors.js';
|
|
7
7
|
import { parseDigestInput, digest } from '../digest.js';
|
|
8
8
|
import { formatEntityHeader, formatKnowledgeHeader, formatQueryTable, formatKnowledgeTable, formatSearchResults, formatValidation, formatStats, formatChangelog } from './format.js';
|
|
9
9
|
function jsonOut(data) {
|
|
10
10
|
return JSON.stringify(data, null, 2);
|
|
11
11
|
}
|
|
12
|
+
const READ_USAGE = 'Usage: hafez read <slug> [--depth frontmatter|summary|full]';
|
|
13
|
+
const READ_FLAGS = { depth: 'string' };
|
|
12
14
|
export async function cmdRead(os, args, opts = {}) {
|
|
13
|
-
const
|
|
15
|
+
const { flags, positionals } = parseArgs(args, READ_FLAGS, READ_USAGE);
|
|
16
|
+
const slug = positionals[0];
|
|
14
17
|
if (!slug)
|
|
15
|
-
throw new
|
|
16
|
-
const depth = (
|
|
18
|
+
throw new UsageError(READ_USAGE);
|
|
19
|
+
const depth = (str(flags.depth) ?? 'summary');
|
|
17
20
|
const result = await os.read(slug, depth);
|
|
18
21
|
if (opts.json)
|
|
19
22
|
return jsonOut(result);
|
|
20
23
|
const fm = result.frontmatter;
|
|
21
|
-
if ('
|
|
24
|
+
if (result.kind === 'entity')
|
|
22
25
|
return formatEntityHeader(fm, result.body);
|
|
23
26
|
return formatKnowledgeHeader(fm, result.body);
|
|
24
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
|
+
};
|
|
25
35
|
export async function cmdQuery(os, args, opts = {}) {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
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);
|
|
38
49
|
const tags = tagRaw ? tagRaw.split(',').map(t => t.trim()).filter(Boolean) : undefined;
|
|
39
|
-
const sortBy =
|
|
40
|
-
const sortOrder =
|
|
41
|
-
const limitRaw =
|
|
42
|
-
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);
|
|
43
54
|
const limit = limitRaw != null ? parseInt(limitRaw, 10) : undefined;
|
|
44
55
|
const offset = offsetRaw != null ? parseInt(offsetRaw, 10) : undefined;
|
|
45
56
|
const sortOpts = { sort_by: sortBy, sort_order: sortOrder, limit, offset };
|
|
@@ -49,8 +60,8 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
49
60
|
if (opts.json)
|
|
50
61
|
return jsonOut(result);
|
|
51
62
|
const { items: results, total } = result;
|
|
52
|
-
const entities = results.filter(
|
|
53
|
-
const knowledge = results.filter(
|
|
63
|
+
const entities = results.filter(r => r.kind === 'entity');
|
|
64
|
+
const knowledge = results.filter(r => r.kind === 'knowledge');
|
|
54
65
|
const sections = [];
|
|
55
66
|
if (entities.length > 0)
|
|
56
67
|
sections.push('## Entities\n\n' + formatQueryTable(entities, total));
|
|
@@ -62,7 +73,7 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
62
73
|
// --kind knowledge: query knowledge only
|
|
63
74
|
// Note: time flags only apply to entity queries.
|
|
64
75
|
if (kind === 'knowledge') {
|
|
65
|
-
const subtype =
|
|
76
|
+
const subtype = str(flags.subtype);
|
|
66
77
|
const kOpts = { domain: domain ?? undefined, confidence, subtype, tags, ...sortOpts };
|
|
67
78
|
const result = await os.query_knowledge(kOpts);
|
|
68
79
|
if (opts.json)
|
|
@@ -92,127 +103,149 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
92
103
|
return jsonOut(result);
|
|
93
104
|
return formatQueryTable(result.items, result.total);
|
|
94
105
|
}
|
|
106
|
+
const SEARCH_USAGE = 'Usage: hafez search <terms> [--kind entity|knowledge|all]';
|
|
107
|
+
const SEARCH_FLAGS = { kind: 'string' };
|
|
95
108
|
export async function cmdSearch(os, args, opts = {}) {
|
|
96
|
-
const
|
|
109
|
+
const { flags, positionals } = parseArgs(args, SEARCH_FLAGS, SEARCH_USAGE);
|
|
110
|
+
const terms = positionals.join(' ');
|
|
97
111
|
if (!terms)
|
|
98
|
-
throw new
|
|
99
|
-
const kind = (
|
|
112
|
+
throw new UsageError(SEARCH_USAGE);
|
|
113
|
+
const kind = (str(flags.kind) ?? 'all');
|
|
100
114
|
const results = await os.search(terms, kind);
|
|
101
115
|
if (opts.json)
|
|
102
116
|
return jsonOut(results);
|
|
103
117
|
return formatSearchResults(results);
|
|
104
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
|
+
};
|
|
105
126
|
export async function cmdCreate(os, args, opts = {}) {
|
|
106
|
-
const
|
|
127
|
+
const { flags, positionals } = parseArgs(args, CREATE_FLAGS, CREATE_USAGE);
|
|
128
|
+
const kind = positionals[0];
|
|
107
129
|
if (!kind)
|
|
108
|
-
throw new
|
|
109
|
-
const name =
|
|
130
|
+
throw new UsageError(CREATE_USAGE);
|
|
131
|
+
const name = positionals.slice(1).join(' ');
|
|
110
132
|
if (!name)
|
|
111
|
-
throw new
|
|
133
|
+
throw new UsageError(`Usage: hafez create ${kind} <name> [options]`);
|
|
112
134
|
if (kind === 'entity') {
|
|
113
|
-
const type =
|
|
135
|
+
const type = str(flags.type);
|
|
114
136
|
if (!type)
|
|
115
|
-
throw new
|
|
137
|
+
throw new UsageError('--type is required for entity creation');
|
|
116
138
|
const slug = await os.create('entity', name, {
|
|
117
139
|
type,
|
|
118
|
-
purpose:
|
|
119
|
-
description:
|
|
120
|
-
resource:
|
|
121
|
-
domain:
|
|
122
|
-
parent:
|
|
123
|
-
related:
|
|
124
|
-
tags:
|
|
125
|
-
brief:
|
|
126
|
-
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']),
|
|
127
149
|
});
|
|
128
150
|
return opts.json ? jsonOut({ slug }) : slug;
|
|
129
151
|
}
|
|
130
152
|
if (kind === 'knowledge') {
|
|
131
153
|
const slug = await os.create('knowledge', name, {
|
|
132
|
-
subtype:
|
|
133
|
-
synthesis:
|
|
134
|
-
description:
|
|
135
|
-
domain:
|
|
136
|
-
related:
|
|
137
|
-
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(','),
|
|
138
160
|
});
|
|
139
161
|
return opts.json ? jsonOut({ slug }) : slug;
|
|
140
162
|
}
|
|
141
|
-
throw new
|
|
163
|
+
throw new UsageError(`Unknown kind: ${kind}. Use entity or knowledge.`);
|
|
142
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
|
+
};
|
|
143
178
|
export async function cmdUpdate(os, args, opts = {}) {
|
|
144
|
-
const
|
|
179
|
+
const { flags, positionals } = parseArgs(args, UPDATE_FLAGS, UPDATE_USAGE);
|
|
180
|
+
const slug = positionals[0];
|
|
145
181
|
if (!slug)
|
|
146
|
-
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
|
+
}
|
|
147
186
|
const fields = {};
|
|
148
|
-
const status =
|
|
187
|
+
const status = str(flags.status);
|
|
149
188
|
if (status)
|
|
150
189
|
fields.status = status;
|
|
151
|
-
|
|
152
|
-
// installed skills/scripts still pass it, and "Updated <slug>" with the
|
|
153
|
-
// action discarded is a false success.
|
|
154
|
-
if (args.includes('--next-action')) {
|
|
155
|
-
throw new Error('--next-action was removed in v1.0.2. Use --add-action (and --clear-actions) instead.');
|
|
156
|
-
}
|
|
157
|
-
const currentState = getFlag(args, '--current-state');
|
|
190
|
+
const currentState = str(flags['current-state']);
|
|
158
191
|
if (currentState)
|
|
159
192
|
fields.current_state = currentState;
|
|
160
193
|
// Brief
|
|
161
|
-
const brief =
|
|
194
|
+
const brief = str(flags.brief);
|
|
162
195
|
if (brief !== undefined) {
|
|
163
196
|
fields.brief = brief === '' ? null : brief;
|
|
164
197
|
}
|
|
165
198
|
// Next Actions
|
|
166
|
-
const addAction =
|
|
199
|
+
const addAction = str(flags['add-action']);
|
|
167
200
|
if (addAction)
|
|
168
201
|
fields.add_action = addAction;
|
|
169
|
-
const completeAction =
|
|
202
|
+
const completeAction = str(flags['complete-action']);
|
|
170
203
|
if (completeAction)
|
|
171
204
|
fields.complete_action = completeAction;
|
|
172
|
-
const removeAction =
|
|
205
|
+
const removeAction = str(flags['remove-action']);
|
|
173
206
|
if (removeAction)
|
|
174
207
|
fields.remove_action = removeAction;
|
|
175
|
-
if (
|
|
208
|
+
if (flags['clear-actions'])
|
|
176
209
|
fields.clear_actions = true;
|
|
177
|
-
const log =
|
|
210
|
+
const log = str(flags.log);
|
|
178
211
|
if (log) {
|
|
179
212
|
const colonIdx = log.indexOf(':');
|
|
180
213
|
if (colonIdx === -1)
|
|
181
|
-
throw new
|
|
214
|
+
throw new UsageError('--log format: "type: summary". Types: progress, decision, blocker, research');
|
|
182
215
|
const type = log.slice(0, colonIdx).trim();
|
|
183
216
|
const summary = log.slice(colonIdx + 1).trim();
|
|
184
|
-
const agent =
|
|
217
|
+
const agent = str(flags.agent) ?? 'claude';
|
|
185
218
|
fields.session_log = { type, summary, agent };
|
|
186
219
|
}
|
|
187
220
|
// Shared metadata (entity + knowledge)
|
|
188
|
-
const description =
|
|
221
|
+
const description = str(flags.description);
|
|
189
222
|
if (description !== undefined)
|
|
190
223
|
fields.description = description;
|
|
191
|
-
const resource =
|
|
224
|
+
const resource = str(flags.resource);
|
|
192
225
|
if (resource !== undefined)
|
|
193
226
|
fields.resource = resource;
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
196
|
-
fields.domain =
|
|
197
|
-
const conf =
|
|
227
|
+
const domain = splitList(flags.domain);
|
|
228
|
+
if (domain)
|
|
229
|
+
fields.domain = domain;
|
|
230
|
+
const conf = str(flags.confidence);
|
|
198
231
|
if (conf)
|
|
199
232
|
fields.confidence = conf;
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
202
|
-
fields.tags =
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
205
|
-
fields.related =
|
|
206
|
-
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);
|
|
207
240
|
if (insight)
|
|
208
241
|
fields.synthesis = insight;
|
|
209
|
-
const synthesis =
|
|
242
|
+
const synthesis = str(flags.synthesis);
|
|
210
243
|
if (synthesis)
|
|
211
244
|
fields.synthesis = synthesis;
|
|
212
|
-
const addEvidence =
|
|
245
|
+
const addEvidence = str(flags['add-evidence']);
|
|
213
246
|
if (addEvidence)
|
|
214
247
|
fields.add_evidence = addEvidence;
|
|
215
|
-
const addSource =
|
|
248
|
+
const addSource = str(flags['add-source']);
|
|
216
249
|
if (addSource)
|
|
217
250
|
fields.add_source = addSource;
|
|
218
251
|
const result = await os.update(slug, fields);
|
|
@@ -224,23 +257,17 @@ export async function cmdUpdate(os, args, opts = {}) {
|
|
|
224
257
|
}
|
|
225
258
|
return lines.join('\n');
|
|
226
259
|
}
|
|
260
|
+
const LINK_FLAGS = { relation: 'string' };
|
|
227
261
|
function parseLinkArgs(args, verb) {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const positionals = args.filter((a, i) => {
|
|
232
|
-
if (a === '--relation')
|
|
233
|
-
return false;
|
|
234
|
-
if (i > 0 && args[i - 1] === '--relation')
|
|
235
|
-
return false;
|
|
236
|
-
return !a.startsWith('--');
|
|
237
|
-
});
|
|
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);
|
|
238
265
|
const [slug, target] = positionals;
|
|
239
266
|
if (!slug || !target || !relation) {
|
|
240
|
-
throw new
|
|
267
|
+
throw new UsageError(usage);
|
|
241
268
|
}
|
|
242
269
|
if (!['parent', 'related'].includes(relation)) {
|
|
243
|
-
throw new
|
|
270
|
+
throw new UsageError(`Invalid relation: ${relation}. Must be parent or related.`);
|
|
244
271
|
}
|
|
245
272
|
return { slug, target, relation };
|
|
246
273
|
}
|
|
@@ -255,22 +282,24 @@ export async function cmdUnlink(os, args, opts = {}) {
|
|
|
255
282
|
return opts.json ? jsonOut({ slug, target, relation }) : `Unlinked ${slug} — ${target} (${relation})`;
|
|
256
283
|
}
|
|
257
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' };
|
|
258
287
|
export async function cmdCapture(os, args, opts = {}) {
|
|
259
|
-
const
|
|
288
|
+
const { flags, positionals } = parseArgs(args, CAPTURE_FLAGS, CAPTURE_USAGE);
|
|
289
|
+
const name = positionals.join(' ');
|
|
260
290
|
if (!name)
|
|
261
|
-
throw new
|
|
262
|
-
const notes =
|
|
291
|
+
throw new UsageError(CAPTURE_USAGE);
|
|
292
|
+
const notes = str(flags.notes);
|
|
263
293
|
const slug = await os.capture(name, notes);
|
|
264
294
|
return opts.json ? jsonOut({ slug }) : slug;
|
|
265
295
|
}
|
|
296
|
+
const PROMOTE_USAGE = 'Usage: hafez promote <slug> <entity|project|knowledge>';
|
|
266
297
|
export async function cmdPromote(os, args, opts = {}) {
|
|
267
|
-
const
|
|
268
|
-
const target =
|
|
298
|
+
const { positionals } = parseArgs(args, {}, PROMOTE_USAGE);
|
|
299
|
+
const [slug, target] = positionals;
|
|
269
300
|
if (!slug || !target)
|
|
270
|
-
throw new
|
|
271
|
-
|
|
272
|
-
throw new Error(`Invalid target: ${target}. Must be entity, project, or knowledge.`);
|
|
273
|
-
}
|
|
301
|
+
throw new UsageError(PROMOTE_USAGE);
|
|
302
|
+
// Target validity is core's rule (contract canPromoteTo) — no CLI re-check.
|
|
274
303
|
await os.promote(slug, target);
|
|
275
304
|
if (opts.json)
|
|
276
305
|
return jsonOut({ slug, target });
|
|
@@ -304,10 +333,13 @@ export async function cmdStats(os, _args, opts = {}) {
|
|
|
304
333
|
return jsonOut(stats);
|
|
305
334
|
return formatStats(stats);
|
|
306
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' };
|
|
307
338
|
export async function cmdChangelog(os, args, opts = {}) {
|
|
308
|
-
const
|
|
339
|
+
const { flags } = parseArgs(args, CHANGELOG_FLAGS, CHANGELOG_USAGE);
|
|
340
|
+
const since = str(flags.since);
|
|
309
341
|
if (!since)
|
|
310
|
-
throw new
|
|
342
|
+
throw new UsageError(CHANGELOG_USAGE);
|
|
311
343
|
const entries = await os.changelog(since);
|
|
312
344
|
if (opts.json)
|
|
313
345
|
return jsonOut(entries);
|
|
@@ -315,127 +347,11 @@ export async function cmdChangelog(os, args, opts = {}) {
|
|
|
315
347
|
}
|
|
316
348
|
// --- Batch ---
|
|
317
349
|
// All enums are built from contracts.ts tuples so help-renderer, file validators,
|
|
318
|
-
// and CLI-input validator share a single source of truth
|
|
319
|
-
//
|
|
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).
|
|
320
352
|
// Every z.object(...) is .strict() — unknown keys become `unrecognized_keys` issues
|
|
321
353
|
// (not silently dropped), which is load-bearing for the error-formatter's did-you-mean
|
|
322
354
|
// hints and for catching typos like `{"kind": "parent"}` on a link op.
|
|
323
|
-
export const SessionLogSchema = z.object({
|
|
324
|
-
type: z.enum([...SESSION_LOG_TYPES]),
|
|
325
|
-
summary: z.string(),
|
|
326
|
-
body: z.string().optional(),
|
|
327
|
-
agent: z.string(),
|
|
328
|
-
}).strict();
|
|
329
|
-
export const UpdateFieldsSchema = z.object({
|
|
330
|
-
status: z.enum([...ENTITY_STATUSES]).optional(),
|
|
331
|
-
current_state: z.string().optional(),
|
|
332
|
-
session_log: SessionLogSchema.optional(),
|
|
333
|
-
brief: z.string().nullable().optional(),
|
|
334
|
-
add_action: z.string().optional(),
|
|
335
|
-
add_actions: z.array(z.string()).optional(),
|
|
336
|
-
complete_action: z.string().optional(),
|
|
337
|
-
remove_action: z.string().optional(),
|
|
338
|
-
clear_actions: z.boolean().optional(),
|
|
339
|
-
// Knowledge metadata
|
|
340
|
-
description: z.string().nullable().optional(),
|
|
341
|
-
resource: z.string().nullable().optional(),
|
|
342
|
-
domain: z.array(z.string()).optional(),
|
|
343
|
-
confidence: z.enum([...CONFIDENCE_LEVELS]).optional(),
|
|
344
|
-
tags: z.array(z.string()).optional(),
|
|
345
|
-
related: z.array(z.string()).optional(),
|
|
346
|
-
synthesis: z.string().optional(),
|
|
347
|
-
insight: z.string().optional(), // alias for synthesis (normalized in parseBatchInput)
|
|
348
|
-
add_evidence: z.string().optional(),
|
|
349
|
-
add_source: z.string().optional(),
|
|
350
|
-
}).strict();
|
|
351
|
-
const CreateEntityFieldsSchema = z.object({
|
|
352
|
-
type: z.enum([...ENTITY_TYPES]),
|
|
353
|
-
purpose: z.string().optional(),
|
|
354
|
-
description: z.string().optional(),
|
|
355
|
-
resource: z.string().optional(),
|
|
356
|
-
domain: z.array(z.string()).optional(),
|
|
357
|
-
parent: z.string().optional(),
|
|
358
|
-
related: z.array(z.string()).optional(),
|
|
359
|
-
tags: z.array(z.string()).optional(),
|
|
360
|
-
brief: z.string().optional(),
|
|
361
|
-
add_action: z.string().optional(),
|
|
362
|
-
add_actions: z.array(z.string()).optional(),
|
|
363
|
-
}).strict();
|
|
364
|
-
const CreateKnowledgeFieldsSchema = z.object({
|
|
365
|
-
subtype: z.enum([...VALID_SUBTYPES]).optional(),
|
|
366
|
-
synthesis: z.string().optional(),
|
|
367
|
-
insight: z.string().optional(),
|
|
368
|
-
description: z.string().optional(),
|
|
369
|
-
domain: z.array(z.string()).optional(),
|
|
370
|
-
related: z.array(z.string()).optional(),
|
|
371
|
-
tags: z.array(z.string()).optional(),
|
|
372
|
-
'session-date': z.string().optional(),
|
|
373
|
-
}).strict();
|
|
374
|
-
const CreateSessionFieldsSchema = z.object({
|
|
375
|
-
synthesis: z.string().optional(),
|
|
376
|
-
insight: z.string().optional(),
|
|
377
|
-
related: z.array(z.string()).optional(),
|
|
378
|
-
'session-date': z.string().optional(),
|
|
379
|
-
}).strict();
|
|
380
|
-
const CreateEntityBranch = z.object({
|
|
381
|
-
op: z.literal('create'),
|
|
382
|
-
kind: z.literal('entity'),
|
|
383
|
-
name: z.string(),
|
|
384
|
-
fields: CreateEntityFieldsSchema,
|
|
385
|
-
}).strict();
|
|
386
|
-
const CreateKnowledgeBranch = z.object({
|
|
387
|
-
op: z.literal('create'),
|
|
388
|
-
kind: z.literal('knowledge'),
|
|
389
|
-
name: z.string(),
|
|
390
|
-
fields: CreateKnowledgeFieldsSchema.optional(),
|
|
391
|
-
}).strict();
|
|
392
|
-
const CreateSessionBranch = z.object({
|
|
393
|
-
op: z.literal('create'),
|
|
394
|
-
kind: z.literal('session'),
|
|
395
|
-
name: z.string(),
|
|
396
|
-
fields: CreateSessionFieldsSchema.optional(),
|
|
397
|
-
}).strict();
|
|
398
|
-
// Nested discriminated union: outer on `op`, inner on `kind` for create branch.
|
|
399
|
-
// A flat `discriminatedUnion('op', [...])` cannot work because three branches share
|
|
400
|
-
// op=create — Zod requires unique discriminator values per branch and throws at
|
|
401
|
-
// schema construction time if violated.
|
|
402
|
-
const CreateBranch = z.discriminatedUnion('kind', [
|
|
403
|
-
CreateEntityBranch,
|
|
404
|
-
CreateKnowledgeBranch,
|
|
405
|
-
CreateSessionBranch,
|
|
406
|
-
]);
|
|
407
|
-
export const BatchOperationSchema = z.discriminatedUnion('op', [
|
|
408
|
-
z.object({
|
|
409
|
-
op: z.literal('update'),
|
|
410
|
-
slug: z.string(),
|
|
411
|
-
fields: UpdateFieldsSchema,
|
|
412
|
-
}).strict(),
|
|
413
|
-
// create is a nested discriminatedUnion on `kind` — see CreateBranch above.
|
|
414
|
-
// discriminatedUnion accepts a ZodDiscriminatedUnion as a branch via option-flattening.
|
|
415
|
-
CreateBranch,
|
|
416
|
-
z.object({
|
|
417
|
-
op: z.literal('capture'),
|
|
418
|
-
name: z.string(),
|
|
419
|
-
notes: z.string().optional(),
|
|
420
|
-
}).strict(),
|
|
421
|
-
z.object({
|
|
422
|
-
op: z.literal('link'),
|
|
423
|
-
slug: z.string(),
|
|
424
|
-
target: z.string(),
|
|
425
|
-
relation: z.enum(['parent', 'related']),
|
|
426
|
-
}).strict(),
|
|
427
|
-
z.object({
|
|
428
|
-
op: z.literal('unlink'),
|
|
429
|
-
slug: z.string(),
|
|
430
|
-
target: z.string(),
|
|
431
|
-
relation: z.enum(['parent', 'related']),
|
|
432
|
-
}).strict(),
|
|
433
|
-
z.object({
|
|
434
|
-
op: z.literal('promote'),
|
|
435
|
-
slug: z.string(),
|
|
436
|
-
target: z.enum(['entity', 'project', 'knowledge']),
|
|
437
|
-
}).strict(),
|
|
438
|
-
]);
|
|
439
355
|
export function parseBatchInput(json) {
|
|
440
356
|
let parsed;
|
|
441
357
|
try {
|
|
@@ -486,7 +402,7 @@ export function parseBatchInput(json) {
|
|
|
486
402
|
operations.push(op);
|
|
487
403
|
}
|
|
488
404
|
if (failureBlocks.length > 0) {
|
|
489
|
-
const header = `Batch
|
|
405
|
+
const header = `Batch failed validation: ${failureBlocks.length} invalid operation${failureBlocks.length === 1 ? '' : 's'} (0 of ${parsed.length} applied)`;
|
|
490
406
|
throw new HafezError('VALIDATION_FAILED', header, failureBlocks);
|
|
491
407
|
}
|
|
492
408
|
return operations;
|
|
@@ -517,9 +433,12 @@ function readStdin() {
|
|
|
517
433
|
process.stdin.on('error', reject);
|
|
518
434
|
});
|
|
519
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' };
|
|
520
438
|
export async function cmdBatch(os, args, opts = {}) {
|
|
521
|
-
const
|
|
522
|
-
const
|
|
439
|
+
const { flags } = parseArgs(args, BATCH_FLAGS, BATCH_USAGE);
|
|
440
|
+
const dryRun = flags['dry-run'] === true;
|
|
441
|
+
const file = str(flags.file);
|
|
523
442
|
const input = file ? readPayloadFile(file) : await readStdin();
|
|
524
443
|
if (!input.trim()) {
|
|
525
444
|
throw new HafezError('VALIDATION_FAILED', 'No input received. Pipe JSON array to stdin (echo \'[...]\' | hafez batch) or pass --file payload.json');
|
|
@@ -558,11 +477,14 @@ export async function cmdBatch(os, args, opts = {}) {
|
|
|
558
477
|
output += ` (created: ${created.join(', ')})`;
|
|
559
478
|
return output;
|
|
560
479
|
}
|
|
480
|
+
const DIGEST_USAGE = 'Usage: hafez digest [--file summary.json] (or pipe JSON to stdin)';
|
|
481
|
+
const DIGEST_FLAGS = { file: 'string' };
|
|
561
482
|
export async function cmdDigest(os, args, _opts = {}) {
|
|
562
|
-
const
|
|
483
|
+
const { flags } = parseArgs(args, DIGEST_FLAGS, DIGEST_USAGE);
|
|
484
|
+
const file = str(flags.file);
|
|
563
485
|
const raw = file ? readPayloadFile(file) : await readStdin();
|
|
564
486
|
if (!raw.trim()) {
|
|
565
|
-
throw new
|
|
487
|
+
throw new UsageError('No input received. Pipe JSON object to stdin (echo \'{"entities_touched":...}\' | hafez digest) or pass --file summary.json');
|
|
566
488
|
}
|
|
567
489
|
// Parse JSON
|
|
568
490
|
let parsed;
|
|
@@ -570,12 +492,12 @@ export async function cmdDigest(os, args, _opts = {}) {
|
|
|
570
492
|
parsed = JSON.parse(raw);
|
|
571
493
|
}
|
|
572
494
|
catch {
|
|
573
|
-
throw new
|
|
495
|
+
throw new HafezError('VALIDATION_FAILED', 'Invalid JSON on stdin: ' + (raw.slice(0, 80)));
|
|
574
496
|
}
|
|
575
497
|
const result = parseDigestInput(parsed);
|
|
576
498
|
if (!result.success) {
|
|
577
499
|
const fields = result.error.issues.map(i => i.path.join('.')).join(', ');
|
|
578
|
-
throw new
|
|
500
|
+
throw new HafezError('VALIDATION_FAILED', `Invalid digest input — missing or invalid fields: ${fields}`);
|
|
579
501
|
}
|
|
580
502
|
// Warn about unknown fields
|
|
581
503
|
if (result.unknownFields.length > 0) {
|
|
@@ -592,23 +514,3 @@ export async function cmdDigest(os, args, _opts = {}) {
|
|
|
592
514
|
const ops = digest(result.data, existingSlugs);
|
|
593
515
|
return JSON.stringify(ops, null, 2);
|
|
594
516
|
}
|
|
595
|
-
// --- Arg helpers ---
|
|
596
|
-
function getFlag(args, flag) {
|
|
597
|
-
const idx = args.indexOf(flag);
|
|
598
|
-
if (idx === -1)
|
|
599
|
-
return undefined;
|
|
600
|
-
return args[idx + 1];
|
|
601
|
-
}
|
|
602
|
-
/** Extract positional args by skipping --flag value pairs */
|
|
603
|
-
function positionalArgs(args) {
|
|
604
|
-
const result = [];
|
|
605
|
-
for (let i = 0; i < args.length; i++) {
|
|
606
|
-
if (args[i].startsWith('--')) {
|
|
607
|
-
i++; // skip the flag's value too
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
result.push(args[i]);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
return result;
|
|
614
|
-
}
|
package/dist/cli/help-agent.d.ts
CHANGED
package/dist/cli/help-agent.js
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
// session and then produce valid batch JSON on first try.
|
|
6
6
|
//
|
|
7
7
|
// Hand-written sections: Preamble + Hard rules (prose, free-form).
|
|
8
|
-
// Generated sections (wrapped in <!-- SECTION:name --> markers so
|
|
9
|
-
//
|
|
8
|
+
// Generated sections (wrapped in <!-- SECTION:name --> markers so tests can
|
|
9
|
+
// parse them): op-catalog, critical-enums, batch-examples, digest.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// those come from
|
|
11
|
+
// Generated sections derive from the Op Spec table (batch-ops.ts via
|
|
12
|
+
// schema-introspect.ts), so they cannot drift from the Zod schemas. Do NOT
|
|
13
|
+
// enumerate enum values inside the preamble or hard rules — those come from
|
|
14
|
+
// schema-introspect.ts.
|
|
14
15
|
import { listOps, getEnumValues, getOpExample, getOpSchema } from './schema-introspect.js';
|
|
15
16
|
const PREAMBLE = `# Hafez — Agent Prompt Gate
|
|
16
17
|
|
|
@@ -155,6 +156,18 @@ const FOOTER = `## Lookup pointers
|
|
|
155
156
|
* measured as char/3.5. The drift test in tests/cli-help-agent.test.ts
|
|
156
157
|
* enforces the ceiling.
|
|
157
158
|
*/
|
|
159
|
+
const EXIT_CODES_SECTION = `## Exit codes
|
|
160
|
+
|
|
161
|
+
| code | meaning |
|
|
162
|
+
|------|---------|
|
|
163
|
+
| 0 | success |
|
|
164
|
+
| 1 | not found / unknown error |
|
|
165
|
+
| 2 | validation or usage error (bad flags, bad payload, core rejected the write) |
|
|
166
|
+
| 3 | git push failed — the write IS committed locally |
|
|
167
|
+
| 4 | write succeeded but the auto-sync after it failed |
|
|
168
|
+
| 5 | slug already exists |
|
|
169
|
+
| 6 | git commit failed — nothing was written to history |
|
|
170
|
+
| 7 | vault locked by another hafez process — retry when it finishes |`;
|
|
158
171
|
export function renderAgentHelp() {
|
|
159
172
|
return [
|
|
160
173
|
PREAMBLE,
|
|
@@ -166,6 +179,8 @@ export function renderAgentHelp() {
|
|
|
166
179
|
'',
|
|
167
180
|
renderDigestSection(),
|
|
168
181
|
'',
|
|
182
|
+
EXIT_CODES_SECTION,
|
|
183
|
+
'',
|
|
169
184
|
FOOTER,
|
|
170
185
|
].join('\n');
|
|
171
186
|
}
|