hafez 1.3.0 → 1.4.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 +67 -9
- package/dist/apply.d.ts +30 -0
- package/dist/apply.js +582 -0
- package/dist/batch-ops.d.ts +296 -13
- package/dist/batch-ops.js +186 -62
- package/dist/cli/args.d.ts +5 -1
- package/dist/cli/args.js +6 -2
- package/dist/cli/commands.d.ts +24 -0
- package/dist/cli/commands.js +197 -115
- package/dist/cli/help-agent.js +24 -0
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.js +150 -130
- package/dist/cli/upgrade.d.ts +42 -0
- package/dist/cli/upgrade.js +305 -0
- package/dist/contracts.d.ts +49 -6
- package/dist/contracts.js +104 -23
- package/dist/dates.d.ts +10 -0
- package/dist/dates.js +27 -0
- package/dist/db.d.ts +27 -2
- package/dist/db.js +309 -33
- package/dist/document.d.ts +97 -88
- package/dist/document.js +512 -382
- package/dist/export-okf.js +41 -15
- package/dist/git.js +165 -48
- package/dist/index.d.ts +2 -2
- package/dist/index.js +212 -727
- package/dist/knowledge-index.d.ts +23 -1
- package/dist/knowledge-index.js +62 -6
- package/dist/merge.d.ts +1 -1
- package/dist/merge.js +96 -64
- package/dist/schema.js +4 -4
- package/dist/templates.d.ts +6 -0
- package/dist/templates.js +27 -27
- package/dist/types.d.ts +38 -11
- package/dist/vault.d.ts +18 -0
- package/dist/vault.js +50 -4
- package/package.json +1 -1
- package/skill/SKILL.md +51 -110
- package/dist/query.d.ts +0 -24
- package/dist/query.js +0 -169
package/dist/cli/commands.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// src/cli/commands.ts
|
|
2
2
|
import { readFileSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
3
4
|
import { parseArgs, splitList, str, UsageError } from './args.js';
|
|
4
5
|
import { HafezError } from '../types.js';
|
|
5
|
-
import { BatchOperationSchema } from '../batch-ops.js';
|
|
6
|
+
import { BatchOperationSchema, normalizeAliases, opErrorLine, batchInvalidHeader } from '../batch-ops.js';
|
|
6
7
|
import { formatBatchError } from './batch-errors.js';
|
|
7
8
|
import { parseDigestInput, digest } from '../digest.js';
|
|
8
9
|
import { formatEntityHeader, formatKnowledgeHeader, formatQueryTable, formatKnowledgeTable, formatSearchResults, formatValidation, formatStats, formatChangelog } from './format.js';
|
|
@@ -25,22 +26,75 @@ export async function cmdRead(os, args, opts = {}) {
|
|
|
25
26
|
return formatEntityHeader(fm, result.body);
|
|
26
27
|
return formatKnowledgeHeader(fm, result.body);
|
|
27
28
|
}
|
|
28
|
-
const QUERY_USAGE =
|
|
29
|
+
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]
|
|
30
|
+
|
|
31
|
+
--filter active|paused|done matches entity status; knowledge notes carry no
|
|
32
|
+
status, so under --kind all they are always included alongside the matching
|
|
33
|
+
entities. --filter stale|capture are entity-only views and return no
|
|
34
|
+
knowledge. --filter/--type/--parent are rejected with --kind knowledge, and
|
|
35
|
+
--confidence/--subtype are rejected with --kind entity, rather than being
|
|
36
|
+
silently dropped. --related-to takes no other query flags.`;
|
|
29
37
|
const QUERY_FLAGS = {
|
|
30
38
|
kind: 'string', filter: 'string', type: 'string', parent: 'string', 'related-to': 'string',
|
|
31
39
|
domain: 'string', confidence: 'string', subtype: 'string', tag: 'string',
|
|
32
40
|
since: 'string', before: 'string', 'created-since': 'string', 'created-before': 'string',
|
|
33
41
|
'sort-by': 'string', 'sort-order': 'string', limit: 'string', offset: 'string',
|
|
34
42
|
};
|
|
43
|
+
/** Every flag the query parser accepts, for the branch-support matrix. */
|
|
44
|
+
export const QUERY_FLAG_NAMES = Object.keys(QUERY_FLAGS);
|
|
45
|
+
/** Which flags each kind branch actually applies. A branch that cannot honour
|
|
46
|
+
* a flag rejects it (below) instead of dropping it: `--kind knowledge --filter
|
|
47
|
+
* stale` used to return every knowledge note with exit 0, and `--confidence`
|
|
48
|
+
* never reached an entity query at all. */
|
|
49
|
+
export const QUERY_FLAG_SUPPORT = {
|
|
50
|
+
entity: ['kind', 'filter', 'type', 'parent', 'domain', 'tag', 'since', 'before', 'created-since', 'created-before', 'sort-by', 'sort-order', 'limit', 'offset'],
|
|
51
|
+
knowledge: ['kind', 'domain', 'confidence', 'subtype', 'tag', 'since', 'before', 'created-since', 'created-before', 'sort-by', 'sort-order', 'limit', 'offset'],
|
|
52
|
+
all: ['kind', 'filter', 'type', 'parent', 'domain', 'confidence', 'subtype', 'tag', 'since', 'before', 'created-since', 'created-before', 'sort-by', 'sort-order', 'limit', 'offset'],
|
|
53
|
+
'related-to': ['related-to'],
|
|
54
|
+
};
|
|
55
|
+
const FLAG_SCOPE_REASON = {
|
|
56
|
+
filter: 'it selects entity status (or the entity-only stale/capture views); knowledge notes carry no status',
|
|
57
|
+
type: 'entity types only',
|
|
58
|
+
parent: 'entity hierarchy only',
|
|
59
|
+
confidence: 'knowledge notes only',
|
|
60
|
+
subtype: 'knowledge notes only',
|
|
61
|
+
};
|
|
62
|
+
const QUERY_KINDS = ['entity', 'knowledge', 'all'];
|
|
63
|
+
function rejectUnsupportedFlags(branch, flags) {
|
|
64
|
+
const supported = new Set(QUERY_FLAG_SUPPORT[branch]);
|
|
65
|
+
for (const [name, value] of Object.entries(flags)) {
|
|
66
|
+
if (value === undefined || supported.has(name))
|
|
67
|
+
continue;
|
|
68
|
+
const where = branch === 'related-to'
|
|
69
|
+
? 'with --related-to (it returns every entity and knowledge note linked to the slug)'
|
|
70
|
+
: `with --kind ${branch}`;
|
|
71
|
+
const reason = FLAG_SCOPE_REASON[name] ? ` — ${FLAG_SCOPE_REASON[name]}` : '';
|
|
72
|
+
throw new UsageError(`--${name} is not supported ${where}${reason}\n${QUERY_USAGE}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Paging flags reach SQL as text — parse them strictly here so `--limit abc`
|
|
76
|
+
* is a usage error rather than a prepare-time SqliteError. */
|
|
77
|
+
function intFlag(raw, name) {
|
|
78
|
+
if (raw === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (!/^\d+$/.test(raw))
|
|
81
|
+
throw new UsageError(`--${name} must be a non-negative integer (got "${raw}")\n${QUERY_USAGE}`);
|
|
82
|
+
return Number(raw);
|
|
83
|
+
}
|
|
35
84
|
export async function cmdQuery(os, args, opts = {}) {
|
|
36
85
|
const { flags } = parseArgs(args, QUERY_FLAGS, QUERY_USAGE);
|
|
37
|
-
const
|
|
86
|
+
const relatedTo = str(flags['related-to']);
|
|
87
|
+
const branch = relatedTo !== undefined ? 'related-to' : (str(flags.kind) ?? 'entity');
|
|
88
|
+
if (branch !== 'related-to' && !QUERY_KINDS.includes(branch)) {
|
|
89
|
+
throw new UsageError(`--kind must be one of ${QUERY_KINDS.join(', ')} (got "${branch}")\n${QUERY_USAGE}`);
|
|
90
|
+
}
|
|
91
|
+
rejectUnsupportedFlags(branch, flags);
|
|
38
92
|
const filter = (str(flags.filter) ?? 'active');
|
|
39
93
|
const type = str(flags.type);
|
|
40
94
|
const parent = str(flags.parent);
|
|
41
|
-
const relatedTo = str(flags['related-to']);
|
|
42
95
|
const domain = str(flags.domain);
|
|
43
96
|
const confidence = str(flags.confidence);
|
|
97
|
+
const subtype = str(flags.subtype);
|
|
44
98
|
const since = str(flags.since);
|
|
45
99
|
const before = str(flags.before);
|
|
46
100
|
const createdSince = str(flags['created-since']);
|
|
@@ -49,11 +103,10 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
49
103
|
const tags = tagRaw ? tagRaw.split(',').map(t => t.trim()).filter(Boolean) : undefined;
|
|
50
104
|
const sortBy = str(flags['sort-by']);
|
|
51
105
|
const sortOrder = str(flags['sort-order']);
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const limit = limitRaw != null ? parseInt(limitRaw, 10) : undefined;
|
|
55
|
-
const offset = offsetRaw != null ? parseInt(offsetRaw, 10) : undefined;
|
|
106
|
+
const limit = intFlag(str(flags.limit), 'limit');
|
|
107
|
+
const offset = intFlag(str(flags.offset), 'offset');
|
|
56
108
|
const sortOpts = { sort_by: sortBy, sort_order: sortOrder, limit, offset };
|
|
109
|
+
const timeOpts = { since, before, createdSince, createdBefore };
|
|
57
110
|
// --related-to: always returns both entities and knowledge
|
|
58
111
|
if (relatedTo) {
|
|
59
112
|
const result = await os.related_to(relatedTo);
|
|
@@ -69,20 +122,19 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
69
122
|
sections.push('## Knowledge\n\n' + formatKnowledgeTable(knowledge, total));
|
|
70
123
|
return sections.length > 0 ? sections.join('\n\n') : 'No results.';
|
|
71
124
|
}
|
|
72
|
-
const queryOpts = { filter, type, parent, domain,
|
|
73
|
-
// --kind knowledge: query knowledge only
|
|
74
|
-
//
|
|
75
|
-
if (
|
|
76
|
-
const
|
|
77
|
-
const kOpts = { domain: domain ?? undefined, confidence, subtype, tags, ...sortOpts };
|
|
125
|
+
const queryOpts = { filter, type, parent, domain, ...timeOpts, tags, ...sortOpts };
|
|
126
|
+
// --kind knowledge: query knowledge only. The time window applies here too —
|
|
127
|
+
// knowledge rows carry created (and last-touched for the plan subtype).
|
|
128
|
+
if (branch === 'knowledge') {
|
|
129
|
+
const kOpts = { domain: domain ?? undefined, confidence, subtype, ...timeOpts, tags, ...sortOpts };
|
|
78
130
|
const result = await os.query_knowledge(kOpts);
|
|
79
131
|
if (opts.json)
|
|
80
132
|
return jsonOut(result);
|
|
81
133
|
return formatKnowledgeTable(result.items, result.total);
|
|
82
134
|
}
|
|
83
135
|
// --kind all: query both entities and knowledge via unified query
|
|
84
|
-
if (
|
|
85
|
-
const result = await os.queryUnified({ ...queryOpts, kind: 'all', confidence });
|
|
136
|
+
if (branch === 'all') {
|
|
137
|
+
const result = await os.queryUnified({ ...queryOpts, kind: 'all', confidence, subtype });
|
|
86
138
|
if (opts.json)
|
|
87
139
|
return jsonOut(result);
|
|
88
140
|
const { items, total } = result;
|
|
@@ -116,13 +168,38 @@ export async function cmdSearch(os, args, opts = {}) {
|
|
|
116
168
|
return jsonOut(results);
|
|
117
169
|
return formatSearchResults(results);
|
|
118
170
|
}
|
|
119
|
-
|
|
120
|
-
|
|
171
|
+
// --status is knowledge/plan only, and --purpose needs --type project: a field
|
|
172
|
+
// whose section the chosen template lacks is rejected, never silently dropped.
|
|
173
|
+
const CREATE_USAGE = 'Usage: hafez create <entity|knowledge> <name> [options]\n entity: --type capture|entity|project [--purpose text (project only)] [--brief text] [--add-action text] …\n knowledge: [--subtype insight|plan] [--status active|done (plan only)] [--synthesis text (insight only)] …';
|
|
174
|
+
/** Exported so tests can assert the flag × kind matrix is exhaustive. */
|
|
175
|
+
export const CREATE_FLAGS = {
|
|
121
176
|
type: 'string', purpose: 'string', description: 'string', resource: 'string',
|
|
122
177
|
domain: 'string', parent: 'string', related: 'string', tags: 'string',
|
|
123
178
|
brief: 'string', 'add-action': 'string',
|
|
124
179
|
subtype: 'string', synthesis: 'string', insight: 'string',
|
|
180
|
+
// knowledge, plan subtype only — insights have no lifecycle
|
|
181
|
+
status: 'string',
|
|
125
182
|
};
|
|
183
|
+
/** Flags whose value is a comma list. Everything else is a plain string. */
|
|
184
|
+
const CREATE_LIST_FLAGS = new Set(['domain', 'related', 'tags']);
|
|
185
|
+
/** Every provided create flag → its field name (dashes become underscores),
|
|
186
|
+
* kind-agnostic on purpose. Which fields a kind accepts is the Op Spec's
|
|
187
|
+
* call, not the CLI's: the per-kind hand-filter this replaces discarded
|
|
188
|
+
* whatever its branch didn't name (--brief on knowledge, --subtype on
|
|
189
|
+
* entity) and still exited 0 with the value nowhere on disk. Only provided
|
|
190
|
+
* flags are included — a present-but-undefined key would trip the same
|
|
191
|
+
* strict shape that does the rejecting. */
|
|
192
|
+
function createFields(flags) {
|
|
193
|
+
const fields = {};
|
|
194
|
+
for (const flag of Object.keys(CREATE_FLAGS)) {
|
|
195
|
+
if (flags[flag] === undefined)
|
|
196
|
+
continue;
|
|
197
|
+
fields[flag.replace(/-/g, '_')] = CREATE_LIST_FLAGS.has(flag)
|
|
198
|
+
? splitList(flags[flag])
|
|
199
|
+
: str(flags[flag]);
|
|
200
|
+
}
|
|
201
|
+
return fields;
|
|
202
|
+
}
|
|
126
203
|
export async function cmdCreate(os, args, opts = {}) {
|
|
127
204
|
const { flags, positionals } = parseArgs(args, CREATE_FLAGS, CREATE_USAGE);
|
|
128
205
|
const kind = positionals[0];
|
|
@@ -131,50 +208,84 @@ export async function cmdCreate(os, args, opts = {}) {
|
|
|
131
208
|
const name = positionals.slice(1).join(' ');
|
|
132
209
|
if (!name)
|
|
133
210
|
throw new UsageError(`Usage: hafez create ${kind} <name> [options]`);
|
|
211
|
+
const fields = createFields(flags);
|
|
134
212
|
if (kind === 'entity') {
|
|
135
|
-
|
|
136
|
-
|
|
213
|
+
// The only per-kind rule the CLI still owns: --type is required, and a
|
|
214
|
+
// missing one deserves better than a shape error naming a JSON field.
|
|
215
|
+
if (fields.type === undefined)
|
|
137
216
|
throw new UsageError('--type is required for entity creation');
|
|
138
|
-
const slug = await os.create('entity', name,
|
|
139
|
-
type,
|
|
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']),
|
|
149
|
-
});
|
|
217
|
+
const slug = await os.create('entity', name, fields);
|
|
150
218
|
return opts.json ? jsonOut({ slug }) : slug;
|
|
151
219
|
}
|
|
152
220
|
if (kind === 'knowledge') {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
related: str(flags.related)?.split(','),
|
|
159
|
-
tags: str(flags.tags)?.split(','),
|
|
160
|
-
});
|
|
221
|
+
// --insight passes through untouched: the Op Spec normalizes the alias
|
|
222
|
+
// (and drops it when --synthesis is also given). On entity it stays
|
|
223
|
+
// `insight` and the kind gate rejects it. --status flows through the
|
|
224
|
+
// same way; the subtype-aware gate accepts it on plans only.
|
|
225
|
+
const slug = await os.create('knowledge', name, fields);
|
|
161
226
|
return opts.json ? jsonOut({ slug }) : slug;
|
|
162
227
|
}
|
|
163
228
|
throw new UsageError(`Unknown kind: ${kind}. Use entity or knowledge.`);
|
|
164
229
|
}
|
|
165
230
|
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
|
-
|
|
231
|
+
/** Exported so tests can assert the table covers the update schema. */
|
|
232
|
+
export const UPDATE_FLAGS = {
|
|
167
233
|
status: 'string', 'current-state': 'string', brief: 'string',
|
|
168
234
|
'add-action': 'string', 'complete-action': 'string', 'remove-action': 'string',
|
|
169
|
-
'clear-actions': 'boolean',
|
|
235
|
+
'clear-actions': 'boolean',
|
|
170
236
|
description: 'string', resource: 'string', domain: 'string', confidence: 'string',
|
|
171
237
|
tags: 'string', related: 'string', insight: 'string', synthesis: 'string',
|
|
172
238
|
'add-evidence': 'string', 'add-source': 'string',
|
|
239
|
+
// Composed, not mapped: --log "type: summary" plus --agent build session_log.
|
|
240
|
+
log: 'string', agent: 'string',
|
|
173
241
|
// Removed flag, declared so it fails with guidance instead of a generic
|
|
174
242
|
// unknown-flag error: older installed skills/scripts still pass it, and
|
|
175
243
|
// "Updated <slug>" with the action discarded is a false success.
|
|
176
244
|
'next-action': 'string',
|
|
177
245
|
};
|
|
246
|
+
/** Flags whose value is a comma list. Everything else is a plain string. */
|
|
247
|
+
const UPDATE_LIST_FLAGS = new Set(['domain', 'tags', 'related']);
|
|
248
|
+
/** Flags where an empty string is the clear signal — exactly the nullable
|
|
249
|
+
* fields of the update shape. Every other flag passes its value through
|
|
250
|
+
* untouched, so `--status ""` reaches the enum and is rejected by name
|
|
251
|
+
* instead of silently no-opping (which is how it read before: the guard
|
|
252
|
+
* each flag got was an accident of which one was typed). */
|
|
253
|
+
const UPDATE_CLEARABLE_FLAGS = new Set(['brief', 'description', 'resource']);
|
|
254
|
+
/** Flags the generic loop skips because they are not a field of the update
|
|
255
|
+
* shape — each is hand-handled in cmdUpdate. */
|
|
256
|
+
export const UPDATE_SPECIAL_FLAGS = new Set(['log', 'agent', 'next-action']);
|
|
257
|
+
/** Update-shape fields with no flag of their own, and why. The exhaustiveness
|
|
258
|
+
* test consults this, so a gap here is a stated decision rather than the
|
|
259
|
+
* silent hole `add_actions` was. */
|
|
260
|
+
export const UPDATE_FIELDS_WITHOUT_FLAGS = {
|
|
261
|
+
session_log: 'composed by --log "type: summary" (+ --agent), never passed raw',
|
|
262
|
+
add_actions: 'array-valued: comma-splitting would mangle action text (actions carry commas), and the parser takes no repeated flags — batch payloads are the interface for multi-action writes',
|
|
263
|
+
};
|
|
264
|
+
/** Every provided update flag → its field name (dashes become underscores).
|
|
265
|
+
* Which fields the target accepts is the Op Spec's call, not the CLI's — the
|
|
266
|
+
* strict shape does all accepting and rejecting, exactly as on create. Only
|
|
267
|
+
* provided flags are included: a present-but-undefined key would trip that
|
|
268
|
+
* same strict shape. */
|
|
269
|
+
function updateFields(flags) {
|
|
270
|
+
const fields = {};
|
|
271
|
+
for (const [flag, type] of Object.entries(UPDATE_FLAGS)) {
|
|
272
|
+
const raw = flags[flag];
|
|
273
|
+
if (raw === undefined || UPDATE_SPECIAL_FLAGS.has(flag))
|
|
274
|
+
continue;
|
|
275
|
+
const field = flag.replace(/-/g, '_');
|
|
276
|
+
if (type === 'boolean') {
|
|
277
|
+
fields[field] = true;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (UPDATE_LIST_FLAGS.has(flag)) {
|
|
281
|
+
fields[field] = splitList(raw);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const value = str(raw);
|
|
285
|
+
fields[field] = value === '' && UPDATE_CLEARABLE_FLAGS.has(flag) ? null : value;
|
|
286
|
+
}
|
|
287
|
+
return fields;
|
|
288
|
+
}
|
|
178
289
|
export async function cmdUpdate(os, args, opts = {}) {
|
|
179
290
|
const { flags, positionals } = parseArgs(args, UPDATE_FLAGS, UPDATE_USAGE);
|
|
180
291
|
const slug = positionals[0];
|
|
@@ -183,71 +294,22 @@ export async function cmdUpdate(os, args, opts = {}) {
|
|
|
183
294
|
if (flags['next-action'] !== undefined) {
|
|
184
295
|
throw new UsageError('--next-action was removed in v1.0.2. Use --add-action (and --clear-actions) instead.');
|
|
185
296
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (currentState)
|
|
192
|
-
fields.current_state = currentState;
|
|
193
|
-
// Brief
|
|
194
|
-
const brief = str(flags.brief);
|
|
195
|
-
if (brief !== undefined) {
|
|
196
|
-
fields.brief = brief === '' ? null : brief;
|
|
197
|
-
}
|
|
198
|
-
// Next Actions
|
|
199
|
-
const addAction = str(flags['add-action']);
|
|
200
|
-
if (addAction)
|
|
201
|
-
fields.add_action = addAction;
|
|
202
|
-
const completeAction = str(flags['complete-action']);
|
|
203
|
-
if (completeAction)
|
|
204
|
-
fields.complete_action = completeAction;
|
|
205
|
-
const removeAction = str(flags['remove-action']);
|
|
206
|
-
if (removeAction)
|
|
207
|
-
fields.remove_action = removeAction;
|
|
208
|
-
if (flags['clear-actions'])
|
|
209
|
-
fields.clear_actions = true;
|
|
297
|
+
// --insight is the declared alias; the Op Spec normalizes it (and drops it
|
|
298
|
+
// when --synthesis is also given).
|
|
299
|
+
const fields = updateFields(flags);
|
|
300
|
+
// The one genuinely CLI-owned convenience: session_log is a nested object,
|
|
301
|
+
// so the flag pair composes it rather than naming a field.
|
|
210
302
|
const log = str(flags.log);
|
|
211
|
-
if (log) {
|
|
303
|
+
if (log !== undefined) {
|
|
212
304
|
const colonIdx = log.indexOf(':');
|
|
213
305
|
if (colonIdx === -1)
|
|
214
306
|
throw new UsageError('--log format: "type: summary". Types: progress, decision, blocker, research');
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
307
|
+
fields.session_log = {
|
|
308
|
+
type: log.slice(0, colonIdx).trim(),
|
|
309
|
+
summary: log.slice(colonIdx + 1).trim(),
|
|
310
|
+
agent: str(flags.agent) ?? 'claude',
|
|
311
|
+
};
|
|
219
312
|
}
|
|
220
|
-
// Shared metadata (entity + knowledge)
|
|
221
|
-
const description = str(flags.description);
|
|
222
|
-
if (description !== undefined)
|
|
223
|
-
fields.description = description;
|
|
224
|
-
const resource = str(flags.resource);
|
|
225
|
-
if (resource !== undefined)
|
|
226
|
-
fields.resource = resource;
|
|
227
|
-
const domain = splitList(flags.domain);
|
|
228
|
-
if (domain)
|
|
229
|
-
fields.domain = domain;
|
|
230
|
-
const conf = str(flags.confidence);
|
|
231
|
-
if (conf)
|
|
232
|
-
fields.confidence = conf;
|
|
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);
|
|
240
|
-
if (insight)
|
|
241
|
-
fields.synthesis = insight;
|
|
242
|
-
const synthesis = str(flags.synthesis);
|
|
243
|
-
if (synthesis)
|
|
244
|
-
fields.synthesis = synthesis;
|
|
245
|
-
const addEvidence = str(flags['add-evidence']);
|
|
246
|
-
if (addEvidence)
|
|
247
|
-
fields.add_evidence = addEvidence;
|
|
248
|
-
const addSource = str(flags['add-source']);
|
|
249
|
-
if (addSource)
|
|
250
|
-
fields.add_source = addSource;
|
|
251
313
|
const result = await os.update(slug, fields);
|
|
252
314
|
if (opts.json)
|
|
253
315
|
return jsonOut({ slug, ...result });
|
|
@@ -391,19 +453,12 @@ export function parseBatchInput(json) {
|
|
|
391
453
|
continue;
|
|
392
454
|
}
|
|
393
455
|
const op = result.data;
|
|
394
|
-
// Alias
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
if (op.fields.insight && !op.fields.synthesis) {
|
|
398
|
-
op.fields.synthesis = op.fields.insight;
|
|
399
|
-
delete op.fields.insight;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
operations.push(op);
|
|
456
|
+
// Alias normalization is the Op Spec's (normalizeAliases) — the CLI just
|
|
457
|
+
// hands back canonical ops so `batch --dry-run` output names real fields.
|
|
458
|
+
operations.push(normalizeAliases(op));
|
|
403
459
|
}
|
|
404
460
|
if (failureBlocks.length > 0) {
|
|
405
|
-
|
|
406
|
-
throw new HafezError('VALIDATION_FAILED', header, failureBlocks);
|
|
461
|
+
throw new HafezError('VALIDATION_FAILED', batchInvalidHeader(failureBlocks.length, parsed.length), failureBlocks);
|
|
407
462
|
}
|
|
408
463
|
return operations;
|
|
409
464
|
}
|
|
@@ -452,7 +507,9 @@ export async function cmdBatch(os, args, opts = {}) {
|
|
|
452
507
|
return jsonOut(report);
|
|
453
508
|
if (!report.valid) {
|
|
454
509
|
const invalidCount = report.operations.filter(o => o.errors.length > 0).length;
|
|
455
|
-
const lines = report.operations.flatMap(o => o.errors.map(e =>
|
|
510
|
+
const lines = report.operations.flatMap(o => o.errors.map(e => opErrorLine(o.index, o.op, o.slug, e)));
|
|
511
|
+
// Dry-run's own header: it reports a hypothetical, so it says "would be
|
|
512
|
+
// applied" rather than the refusal wording batchInvalidHeader carries.
|
|
456
513
|
throw new HafezError('VALIDATION_FAILED', `Batch invalid: ${invalidCount} of ${operations.length} operation${operations.length === 1 ? '' : 's'} failed validation (0 would be applied)`, lines);
|
|
457
514
|
}
|
|
458
515
|
const out = [`Batch valid: ${operations.length} operations would be applied`];
|
|
@@ -475,7 +532,13 @@ export async function cmdBatch(os, args, opts = {}) {
|
|
|
475
532
|
let output = `Batch complete: ${operations.length} operations`;
|
|
476
533
|
if (created.length > 0)
|
|
477
534
|
output += ` (created: ${created.join(', ')})`;
|
|
478
|
-
|
|
535
|
+
// Skipped ops carry the same warning line dry-run printed for them.
|
|
536
|
+
const lines = [output];
|
|
537
|
+
results.forEach((r, i) => {
|
|
538
|
+
if (r.status === 'skipped' && r.warning)
|
|
539
|
+
lines.push(`Warning: op[${i}] ${r.warning}`);
|
|
540
|
+
});
|
|
541
|
+
return lines.join('\n');
|
|
479
542
|
}
|
|
480
543
|
const DIGEST_USAGE = 'Usage: hafez digest [--file summary.json] (or pipe JSON to stdin)';
|
|
481
544
|
const DIGEST_FLAGS = { file: 'string' };
|
|
@@ -514,3 +577,22 @@ export async function cmdDigest(os, args, _opts = {}) {
|
|
|
514
577
|
const ops = digest(result.data, existingSlugs);
|
|
515
578
|
return JSON.stringify(ops, null, 2);
|
|
516
579
|
}
|
|
580
|
+
export async function cmdExport(_os, args, opts = {}) {
|
|
581
|
+
const EXPORT_USAGE = 'Usage: hafez export --okf [--out <dir>]';
|
|
582
|
+
const { flags } = parseArgs(args, { okf: 'boolean', out: 'string' }, EXPORT_USAGE);
|
|
583
|
+
if (flags.okf !== true)
|
|
584
|
+
throw new UsageError(EXPORT_USAGE);
|
|
585
|
+
const outDir = typeof flags.out === 'string' ? resolve(flags.out) : resolve(process.cwd(), 'okf-export');
|
|
586
|
+
if (!opts.vaultPath)
|
|
587
|
+
throw new HafezError('VALIDATION_FAILED', 'export requires a resolved vault path (CommandOpts.vaultPath)');
|
|
588
|
+
const { exportOkf } = await import('../export-okf.js');
|
|
589
|
+
const exportReport = exportOkf(opts.vaultPath, outDir);
|
|
590
|
+
if (opts.json)
|
|
591
|
+
return JSON.stringify(exportReport, null, 2);
|
|
592
|
+
const lines = [`Exported ${exportReport.entities} entities, ${exportReport.knowledge} knowledge, ${exportReport.sessions} sessions to ${exportReport.outDir}`];
|
|
593
|
+
for (const s of exportReport.skipped)
|
|
594
|
+
lines.push(` skipped ${s.file}: ${s.reason}`);
|
|
595
|
+
if (exportReport.unresolvedLinks > 0)
|
|
596
|
+
lines.push(` unresolved links: ${exportReport.unresolvedLinks}`);
|
|
597
|
+
return lines.join('\n');
|
|
598
|
+
}
|
package/dist/cli/help-agent.js
CHANGED
|
@@ -144,6 +144,29 @@ function renderDigestSection() {
|
|
|
144
144
|
lines.push('<!-- /SECTION:digest -->');
|
|
145
145
|
return lines.join('\n');
|
|
146
146
|
}
|
|
147
|
+
const UPGRADE_SECTION = `## Upgrading hafez itself
|
|
148
|
+
|
|
149
|
+
\`hafez upgrade\` — checks whether the installed hafez is outdated and prints
|
|
150
|
+
the steps. It NEVER installs: the steps install code and overwrite the user's
|
|
151
|
+
agent instructions, so show them verbatim, get an explicit yes, and only then
|
|
152
|
+
run them. Re-run to verify. Every check outcome exits 0 — being outdated is a
|
|
153
|
+
report, not a gate — so read the output, not the exit code. A non-zero exit
|
|
154
|
+
means the invocation was wrong (exit 2, \`upgrade\` takes no arguments), never
|
|
155
|
+
that a check failed. Not to be confused with \`hafez update\`, which updates
|
|
156
|
+
an entity.
|
|
157
|
+
|
|
158
|
+
\`--json\` keys: current, latest (null when unverified), comparison
|
|
159
|
+
(behind|equal|ahead|unknown), registryReachable, npmMajor, install
|
|
160
|
+
(registry|dev-checkout), skill (present|absent), skillSource, skillTarget,
|
|
161
|
+
alternativeInstall, steps. \`steps\` is the action list in execution order;
|
|
162
|
+
\`kind: 'command'\` entries are executable, \`kind: 'action'\` entries are
|
|
163
|
+
prose — do not shell out to them. \`alternativeInstall\` is mutually exclusive
|
|
164
|
+
with the install command in \`steps\`; never run both.
|
|
165
|
+
|
|
166
|
+
Checks the npm registry only (override with \`HAFEZ_REGISTRY_URL\`), so a
|
|
167
|
+
just-cut GitHub Release can read as \`equal\` until it is published.
|
|
168
|
+
|
|
169
|
+
`;
|
|
147
170
|
const FOOTER = `## Lookup pointers
|
|
148
171
|
|
|
149
172
|
- \`hafez schema\` — list all ops
|
|
@@ -181,6 +204,7 @@ export function renderAgentHelp() {
|
|
|
181
204
|
'',
|
|
182
205
|
EXIT_CODES_SECTION,
|
|
183
206
|
'',
|
|
207
|
+
UPGRADE_SECTION,
|
|
184
208
|
FOOTER,
|
|
185
209
|
].join('\n');
|
|
186
210
|
}
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1 +1,42 @@
|
|
|
1
|
+
import type { CommandOpts } from './commands.js';
|
|
2
|
+
type PreVaultRun = (args: string[], ctx: {
|
|
3
|
+
json: boolean;
|
|
4
|
+
vaultFlag?: string;
|
|
5
|
+
}) => Promise<string | null> | string | null;
|
|
6
|
+
type VaultRun = (os: any, args: string[], opts: CommandOpts) => Promise<string>;
|
|
7
|
+
export type CommandSpec = {
|
|
8
|
+
phase: 'pre-vault';
|
|
9
|
+
run: PreVaultRun;
|
|
10
|
+
} | {
|
|
11
|
+
phase: 'vault';
|
|
12
|
+
mutating?: boolean;
|
|
13
|
+
run: VaultRun;
|
|
14
|
+
};
|
|
15
|
+
export declare const COMMANDS: Record<string, CommandSpec>;
|
|
16
|
+
/** Exported so a guard test can assert every registered command is documented
|
|
17
|
+
* here — a registered-but-undocumented command is otherwise invisible. */
|
|
18
|
+
export declare const USAGE = "Usage: hafez <command> [args]\n\nCommands:\n read <slug> Read entity or knowledge note\n query [--filter ...] ... Filter entities/knowledge\n --kind all Both kinds; --filter matches entity status only,\n so knowledge notes (which have none) always show\n search <terms> Full-text search\n create <kind> <name> Create entity or knowledge note\n capture <name> [--notes] Quick inbox capture\n promote <slug> <target> Promote capture\u2192entity/project/knowledge, entity\u2192project\n update <slug> [--status] Update an entity or knowledge note\n --brief \"text\" Set/replace Brief (use --brief \"\" to clear)\n --description \"text\" Set one-line description (use --description \"\" to clear)\n --resource \"uri\" Set canonical resource URI, entities only (use --resource \"\" to clear)\n --add-action \"text\" Add a next action\n --complete-action \"text\" Complete matching action\n --remove-action \"text\" Remove matching action\n --clear-actions Remove all next actions\n --synthesis \"text\" Set/replace Synthesis section (--insight is an alias)\n --add-evidence \"text\" Append to Evidence section\n --add-source \"text\" Append to Sources section\n batch Apply multiple operations atomically (JSON on stdin)\n digest Apply digest input (JSON stdin) \u2192 batch payload (JSON stdout)\n export --okf [--out <dir>] Export vault as an OKF v0.1 bundle (read-only)\n sync Pull remote changes and push local commits\n link <slug> <target> --relation <rel> Add a link\n unlink <slug> <target> --relation <rel> Remove a link\n stats Vault summary: counts, stale items, recents\n changelog --since <when> Git-derived change history (e.g. --since 7.days.ago)\n schema [op] [--examples] Machine-readable JSON schema for batch operations\n validate Check vault integrity\n index rebuild Rebuild the SQLite index\n init --register <path> Register an existing vault\n init Show current vault resolution\n onboard Agent-directed first run: seed the vault, pick integration level\n upgrade Check whether hafez is outdated and print the update steps\n help --agent Full API reference for agents\n\nGlobal flags:\n --vault <path> Vault path (overrides config)\n --json JSON output (structured, machine-readable)\n --version Print the hafez version\n\nVault discovery: --vault flag > ~/.config/hafez/vault\n";
|
|
19
|
+
/** Exit codes, documented in `hafez help --agent`. 0 = success; 4 is the
|
|
20
|
+
* mutate-succeeded-sync-failed warning path; anything unmapped = 1. */
|
|
21
|
+
export declare const EXIT_CODES: Record<string, number>;
|
|
22
|
+
/** The pure half of the error → exit adapter: which code an error maps to.
|
|
23
|
+
* Exported so the exit-code contract is testable without spawning the CLI. */
|
|
24
|
+
export declare function exitCodeFor(err: unknown): number;
|
|
25
|
+
/** Does this error mean the native module failed to load/build?
|
|
26
|
+
*
|
|
27
|
+
* Matched on the MESSAGE only, and only on the messages a load/ABI failure
|
|
28
|
+
* actually produces. The old test also scanned the stack for "better-sqlite3",
|
|
29
|
+
* which every SqliteError's stack contains — so a plain query error ("near
|
|
30
|
+
* 'abc': syntax error") printed reinstall advice instead of itself. */
|
|
31
|
+
export declare function isBindingsFailure(err: unknown): boolean;
|
|
32
|
+
/** Extract the global flags (--vault <path>, --json) from anywhere in argv.
|
|
33
|
+
* Exported for the in-process dispatch tests. A flag sitting where the vault
|
|
34
|
+
* path should be means the value is missing — never consume it (the same
|
|
35
|
+
* value-position rule parseArgs applies to declared command flags). */
|
|
36
|
+
export declare function extractGlobalFlags(args: string[]): {
|
|
37
|
+
rest: string[];
|
|
38
|
+
vaultFlag?: string;
|
|
39
|
+
json: boolean;
|
|
40
|
+
};
|
|
1
41
|
export declare function main(argv: string[]): Promise<void>;
|
|
42
|
+
export {};
|