hafez 2.0.0 → 2.2.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 +24 -4
- package/dist/apply.d.ts +2 -1
- package/dist/apply.js +61 -40
- package/dist/batch-ops.d.ts +7 -1
- package/dist/batch-ops.js +9 -5
- package/dist/cli/block-hook.js +24 -9
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +108 -50
- package/dist/cli/format.d.ts +8 -3
- package/dist/cli/format.js +42 -4
- package/dist/cli/index.js +32 -20
- package/dist/cli/install.d.ts +22 -1
- package/dist/cli/install.js +78 -21
- package/dist/cli/onboard.js +38 -9
- package/dist/cli/upgrade.d.ts +4 -2
- package/dist/cli/upgrade.js +21 -7
- package/dist/contracts.js +2 -0
- package/dist/dates.d.ts +4 -0
- package/dist/dates.js +12 -1
- package/dist/db.d.ts +18 -8
- package/dist/db.js +96 -52
- package/dist/git.js +74 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30 -10
- package/dist/knowledge-index.d.ts +1 -1
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +11 -0
- package/dist/surfaces/items.d.ts +56 -1
- package/dist/surfaces/items.js +94 -11
- package/dist/surfaces/surfaces.d.ts +8 -5
- package/dist/surfaces/surfaces.js +31 -16
- package/dist/types.d.ts +67 -19
- package/dist/validate.js +44 -56
- package/dist/vault.d.ts +20 -7
- package/dist/vault.js +29 -8
- package/package.json +2 -2
- package/skill/hafez/SKILL.md +5 -2
package/dist/cli/commands.js
CHANGED
|
@@ -7,18 +7,18 @@ import { BatchOperationSchema, normalizeAliases, opErrorLine, batchInvalidHeader
|
|
|
7
7
|
import { formatBatchError } from './batch-errors.js';
|
|
8
8
|
import { parseDigestInput, digest } from '../digest.js';
|
|
9
9
|
import { detectAgent } from '../harness.js';
|
|
10
|
-
import { getActionItems } from '../document.js';
|
|
10
|
+
import { getActionItems, parseSessionLogHeading } from '../document.js';
|
|
11
11
|
import { resemblances } from '../near-match.js';
|
|
12
12
|
import { actionAge, agedActions, staleHandles, stalenessDaysOf } from '../stale.js';
|
|
13
13
|
import { slugify } from '../vault.js';
|
|
14
14
|
import { ITEMS, footerOp } from '../surfaces/items.js';
|
|
15
15
|
import { placeIn } from '../surfaces/surfaces.js';
|
|
16
|
-
import { formatEntityHeader, formatKnowledgeHeader, formatQueryTable, formatKnowledgeTable, formatSearchResults, formatValidation, formatStats, formatChangelog } from './format.js';
|
|
16
|
+
import { formatEntityHeader, formatKnowledgeHeader, formatSessionHeader, formatQueryTable, formatKnowledgeTable, formatSessionTable, formatSearchResults, formatValidation, formatStats, formatChangelog } from './format.js';
|
|
17
17
|
function jsonOut(data) {
|
|
18
18
|
return JSON.stringify(data, null, 2);
|
|
19
19
|
}
|
|
20
|
-
const READ_USAGE = 'Usage: hafez read <slug> [--depth frontmatter|summary|full]';
|
|
21
|
-
const READ_FLAGS = { depth: 'string' };
|
|
20
|
+
const READ_USAGE = 'Usage: hafez read <slug> [--depth frontmatter|summary|full] [--archive]';
|
|
21
|
+
const READ_FLAGS = { depth: 'string', archive: 'boolean' };
|
|
22
22
|
export async function cmdRead(os, args, opts = {}) {
|
|
23
23
|
const { flags, positionals } = parseArgs(args, READ_FLAGS, READ_USAGE);
|
|
24
24
|
const slug = positionals[0];
|
|
@@ -33,23 +33,52 @@ export async function cmdRead(os, args, opts = {}) {
|
|
|
33
33
|
? new RenderedError('NOT_FOUND', jsonOut({ slug, closest }), 'stdout')
|
|
34
34
|
: new RenderedError('NOT_FOUND', ITEMS.slugMissRecovery.render({ slug, closest }), 'stderr');
|
|
35
35
|
});
|
|
36
|
+
if (flags.archive) {
|
|
37
|
+
// The archive is an entity's: reached from it on purpose (#34).
|
|
38
|
+
if (result.kind !== 'entity')
|
|
39
|
+
throw new UsageError(`--archive reads an entity's archived log entries; '${slug}' is a ${result.kind === 'session' ? 'session note' : 'knowledge note'}\n${READ_USAGE}`);
|
|
40
|
+
const archive = await os.archive(slug);
|
|
41
|
+
if (opts.json)
|
|
42
|
+
return jsonOut({ slug, archive });
|
|
43
|
+
return archive ?? `no archive for ${slug}`;
|
|
44
|
+
}
|
|
45
|
+
// A session note is read, never written: no Footer (#35).
|
|
46
|
+
if (result.kind === 'session') {
|
|
47
|
+
if (opts.json)
|
|
48
|
+
return jsonOut(result);
|
|
49
|
+
return formatSessionHeader(result.frontmatter, result.body);
|
|
50
|
+
}
|
|
36
51
|
const footer = await footerFor(os, slug, result, depth);
|
|
52
|
+
const pointers = result.kind === 'entity' ? await pointersFor(os, slug) : null;
|
|
37
53
|
if (opts.json) {
|
|
38
54
|
const writes = footerOp(footer);
|
|
39
55
|
// A frontmatter-depth read carries no body, so it has no actions to list.
|
|
40
56
|
if (result.kind !== 'entity' || depth === 'frontmatter')
|
|
41
|
-
return jsonOut({ ...result, writes });
|
|
42
|
-
return jsonOut({ ...result, actions: agedActions(result.body, result.frontmatter), writes });
|
|
57
|
+
return jsonOut({ ...result, ...pointers, writes });
|
|
58
|
+
return jsonOut({ ...result, actions: agedActions(result.body, result.frontmatter), ...pointers, writes });
|
|
43
59
|
}
|
|
44
60
|
const fm = result.frontmatter;
|
|
45
61
|
const days = stalenessDaysOf(fm);
|
|
46
62
|
const staleMark = (added) => ITEMS.staleMarker.render(actionAge(added, days));
|
|
47
63
|
const text = result.kind === 'entity'
|
|
48
|
-
? formatEntityHeader(fm, ITEMS.handles.render(result.body, staleMark), await parentLine(os, fm.parent)
|
|
64
|
+
? formatEntityHeader(fm, ITEMS.handles.render(result.body, staleMark), await parentLine(os, fm.parent), [
|
|
65
|
+
ITEMS.sessionsLine.render({ slug, ...pointers.sessions }),
|
|
66
|
+
ITEMS.archivePointer.render({ slug, entries: pointers.archive_entries }),
|
|
67
|
+
].filter(Boolean))
|
|
49
68
|
: formatKnowledgeHeader(fm, result.body);
|
|
50
69
|
// Trailing newlines only: the Footer brings its own blank line.
|
|
51
70
|
return `${text.replace(/\n+$/, '')}\n${ITEMS.writeLine.render(footer)}`;
|
|
52
71
|
}
|
|
72
|
+
/** What hangs off an entity elsewhere: the sessions that touched it (the
|
|
73
|
+
* look back, #35) and its archived log entries (#34). Counted at read time,
|
|
74
|
+
* nothing written. */
|
|
75
|
+
async function pointersFor(os, slug) {
|
|
76
|
+
const sessions = (await os.related_to(slug)).items.filter((i) => i.kind === 'session');
|
|
77
|
+
const latest = sessions.reduce((max, s) => (max === null || s.session_date > max ? s.session_date : max), null);
|
|
78
|
+
const archive = await os.archive(slug);
|
|
79
|
+
const entries = archive ? archive.split('\n').filter(line => parseSessionLogHeading(line)).length : 0;
|
|
80
|
+
return { sessions: { count: sessions.length, latest }, archive_entries: entries };
|
|
81
|
+
}
|
|
53
82
|
/** The `parent:` line's value: the slug with the parent's description. The
|
|
54
83
|
* one thing `read` takes from another file; a parent that cannot be read
|
|
55
84
|
* (a broken slug) is named by its slug alone. */
|
|
@@ -75,7 +104,7 @@ async function staleAfterWrite(os, slug) {
|
|
|
75
104
|
}
|
|
76
105
|
/** The Footer for a file `read` returned at `depth`. The Footer is the same
|
|
77
106
|
* at every depth, so its Handle comes from the whole file, not from the body
|
|
78
|
-
* this depth shows. */
|
|
107
|
+
* this depth shows. A session note has none: it is never written to. */
|
|
79
108
|
async function footerFor(os, slug, result, depth) {
|
|
80
109
|
return { slug, agent: detectAgent(), ...(result.kind === 'entity'
|
|
81
110
|
? {
|
|
@@ -84,14 +113,16 @@ async function footerFor(os, slug, result, depth) {
|
|
|
84
113
|
}
|
|
85
114
|
: { target: { kind: 'knowledge', subtype: result.frontmatter.subtype } }) };
|
|
86
115
|
}
|
|
87
|
-
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]
|
|
116
|
+
const QUERY_USAGE = `Usage: hafez query [--kind entity|knowledge|session|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]
|
|
88
117
|
|
|
89
|
-
--filter active|paused|done matches entity status; knowledge
|
|
90
|
-
status, so under --kind all they are always included
|
|
91
|
-
entities. --filter stale|capture are entity-only
|
|
92
|
-
knowledge. --filter/--type/--parent are rejected with
|
|
93
|
-
--confidence/--subtype are rejected with --kind
|
|
94
|
-
silently dropped. --
|
|
118
|
+
--filter active|paused|done matches entity status; knowledge and session
|
|
119
|
+
notes carry no status, so under --kind all they are always included
|
|
120
|
+
alongside the matching entities. --filter stale|capture are entity-only
|
|
121
|
+
views and return no knowledge. --filter/--type/--parent are rejected with
|
|
122
|
+
--kind knowledge, and --confidence/--subtype are rejected with --kind
|
|
123
|
+
entity, rather than being silently dropped. --kind session lists session
|
|
124
|
+
notes newest first and takes the time window, sort and paging flags only.
|
|
125
|
+
--related-to takes --kind to narrow to one kind and no other query flag.`;
|
|
95
126
|
const QUERY_FLAGS = {
|
|
96
127
|
kind: 'string', filter: 'string', type: 'string', parent: 'string', 'related-to': 'string',
|
|
97
128
|
domain: 'string', confidence: 'string', subtype: 'string', tag: 'string',
|
|
@@ -107,8 +138,9 @@ export const QUERY_FLAG_NAMES = Object.keys(QUERY_FLAGS);
|
|
|
107
138
|
export const QUERY_FLAG_SUPPORT = {
|
|
108
139
|
entity: ['kind', 'filter', 'type', 'parent', 'domain', 'tag', 'since', 'before', 'created-since', 'created-before', 'sort-by', 'sort-order', 'limit', 'offset'],
|
|
109
140
|
knowledge: ['kind', 'domain', 'confidence', 'subtype', 'tag', 'since', 'before', 'created-since', 'created-before', 'sort-by', 'sort-order', 'limit', 'offset'],
|
|
141
|
+
session: ['kind', 'since', 'before', 'created-since', 'created-before', 'sort-by', 'sort-order', 'limit', 'offset'],
|
|
110
142
|
all: ['kind', 'filter', 'type', 'parent', 'domain', 'confidence', 'subtype', 'tag', 'since', 'before', 'created-since', 'created-before', 'sort-by', 'sort-order', 'limit', 'offset'],
|
|
111
|
-
'related-to': ['related-to'],
|
|
143
|
+
'related-to': ['related-to', 'kind'],
|
|
112
144
|
};
|
|
113
145
|
const FLAG_SCOPE_REASON = {
|
|
114
146
|
filter: 'it selects entity status (or the entity-only stale/capture views); knowledge notes carry no status',
|
|
@@ -117,16 +149,17 @@ const FLAG_SCOPE_REASON = {
|
|
|
117
149
|
confidence: 'knowledge notes only',
|
|
118
150
|
subtype: 'knowledge notes only',
|
|
119
151
|
};
|
|
120
|
-
const QUERY_KINDS = ['entity', 'knowledge', 'all'];
|
|
152
|
+
const QUERY_KINDS = ['entity', 'knowledge', 'session', 'all'];
|
|
121
153
|
function rejectUnsupportedFlags(branch, flags) {
|
|
122
154
|
const supported = new Set(QUERY_FLAG_SUPPORT[branch]);
|
|
123
155
|
for (const [name, value] of Object.entries(flags)) {
|
|
124
156
|
if (value === undefined || supported.has(name))
|
|
125
157
|
continue;
|
|
126
158
|
const where = branch === 'related-to'
|
|
127
|
-
? 'with --related-to (it returns every entity and
|
|
159
|
+
? 'with --related-to (it returns every entity, knowledge note and session note linked to the slug)'
|
|
128
160
|
: `with --kind ${branch}`;
|
|
129
|
-
const
|
|
161
|
+
const scope = branch === 'session' && (name === 'domain' || name === 'tag') ? 'a session note carries none' : FLAG_SCOPE_REASON[name];
|
|
162
|
+
const reason = scope ? ` — ${scope}` : '';
|
|
130
163
|
throw new UsageError(`--${name} is not supported ${where}${reason}\n${QUERY_USAGE}`);
|
|
131
164
|
}
|
|
132
165
|
}
|
|
@@ -142,9 +175,10 @@ function intFlag(raw, name) {
|
|
|
142
175
|
export async function cmdQuery(os, args, opts = {}) {
|
|
143
176
|
const { flags } = parseArgs(args, QUERY_FLAGS, QUERY_USAGE);
|
|
144
177
|
const relatedTo = str(flags['related-to']);
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
178
|
+
const kindFlag = str(flags.kind);
|
|
179
|
+
const branch = relatedTo !== undefined ? 'related-to' : (kindFlag ?? 'entity');
|
|
180
|
+
if (kindFlag !== undefined && !QUERY_KINDS.includes(kindFlag)) {
|
|
181
|
+
throw new UsageError(`--kind must be one of ${QUERY_KINDS.join(', ')} (got "${kindFlag}")\n${QUERY_USAGE}`);
|
|
148
182
|
}
|
|
149
183
|
rejectUnsupportedFlags(branch, flags);
|
|
150
184
|
const filter = (str(flags.filter) ?? 'active');
|
|
@@ -165,20 +199,16 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
165
199
|
const offset = intFlag(str(flags.offset), 'offset');
|
|
166
200
|
const sortOpts = { sort_by: sortBy, sort_order: sortOrder, limit, offset };
|
|
167
201
|
const timeOpts = { since, before, createdSince, createdBefore };
|
|
168
|
-
// --related-to:
|
|
202
|
+
// --related-to: every kind linked to the slug, the sessions that touched
|
|
203
|
+
// an entity included; --kind narrows to one (the entity header's
|
|
204
|
+
// `--kind session --related-to` line)
|
|
169
205
|
if (relatedTo) {
|
|
170
|
-
const
|
|
206
|
+
const linked = await os.related_to(relatedTo);
|
|
207
|
+
const items = kindFlag && kindFlag !== 'all' ? linked.items.filter(i => i.kind === kindFlag) : linked.items;
|
|
208
|
+
const result = { items, total: items.length };
|
|
171
209
|
if (opts.json)
|
|
172
210
|
return jsonOut(result);
|
|
173
|
-
|
|
174
|
-
const entities = results.filter(r => r.kind === 'entity');
|
|
175
|
-
const knowledge = results.filter(r => r.kind === 'knowledge');
|
|
176
|
-
const sections = [];
|
|
177
|
-
if (entities.length > 0)
|
|
178
|
-
sections.push('## Entities\n\n' + formatQueryTable(entities, total));
|
|
179
|
-
if (knowledge.length > 0)
|
|
180
|
-
sections.push('## Knowledge\n\n' + formatKnowledgeTable(knowledge, total));
|
|
181
|
-
return sections.length > 0 ? sections.join('\n\n') : 'No results.';
|
|
211
|
+
return formatUnifiedSections(result.items, result.total);
|
|
182
212
|
}
|
|
183
213
|
const queryOpts = { filter, type, parent, domain, ...timeOpts, tags, ...sortOpts };
|
|
184
214
|
// --kind knowledge: query knowledge only. The time window applies here too —
|
|
@@ -190,22 +220,19 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
190
220
|
return jsonOut(result);
|
|
191
221
|
return formatKnowledgeTable(result.items, result.total);
|
|
192
222
|
}
|
|
193
|
-
// --kind
|
|
223
|
+
// --kind session: the session notes alone, newest first
|
|
224
|
+
if (branch === 'session') {
|
|
225
|
+
const result = await os.query_sessions({ ...timeOpts, ...sortOpts });
|
|
226
|
+
if (opts.json)
|
|
227
|
+
return jsonOut(result);
|
|
228
|
+
return formatSessionTable(result.items, result.total);
|
|
229
|
+
}
|
|
230
|
+
// --kind all: every kind via the unified query, sessions included
|
|
194
231
|
if (branch === 'all') {
|
|
195
232
|
const result = await os.queryUnified({ ...queryOpts, kind: 'all', confidence, subtype });
|
|
196
233
|
if (opts.json)
|
|
197
234
|
return jsonOut(result);
|
|
198
|
-
|
|
199
|
-
const entities = items.filter(i => i.kind === 'entity');
|
|
200
|
-
const knowledge = items.filter(i => i.kind === 'knowledge');
|
|
201
|
-
const sections = [];
|
|
202
|
-
if (entities.length > 0)
|
|
203
|
-
sections.push('## Entities\n\n' + formatQueryTable(entities));
|
|
204
|
-
if (knowledge.length > 0)
|
|
205
|
-
sections.push('## Knowledge\n\n' + formatKnowledgeTable(knowledge));
|
|
206
|
-
if (items.length < total)
|
|
207
|
-
sections.push(`\nShowing ${items.length} of ${total}`);
|
|
208
|
-
return sections.length > 0 ? sections.join('\n\n') : 'No results.';
|
|
235
|
+
return formatUnifiedSections(result.items, result.total);
|
|
209
236
|
}
|
|
210
237
|
// --kind entity (default): query entities
|
|
211
238
|
const result = await os.query(queryOpts);
|
|
@@ -213,14 +240,38 @@ export async function cmdQuery(os, args, opts = {}) {
|
|
|
213
240
|
return jsonOut(result);
|
|
214
241
|
return formatQueryTable(result.items, result.total);
|
|
215
242
|
}
|
|
216
|
-
|
|
243
|
+
/** One section per kind present, in the order entities, knowledge,
|
|
244
|
+
* sessions; a page line when the page is short of the total. */
|
|
245
|
+
function formatUnifiedSections(items, total) {
|
|
246
|
+
const entities = items.filter((i) => i.kind === 'entity');
|
|
247
|
+
const knowledge = items.filter((i) => i.kind === 'knowledge');
|
|
248
|
+
const sessions = items.filter((i) => i.kind === 'session');
|
|
249
|
+
const sections = [];
|
|
250
|
+
if (entities.length > 0)
|
|
251
|
+
sections.push('## Entities\n\n' + formatQueryTable(entities));
|
|
252
|
+
if (knowledge.length > 0)
|
|
253
|
+
sections.push('## Knowledge\n\n' + formatKnowledgeTable(knowledge));
|
|
254
|
+
if (sessions.length > 0)
|
|
255
|
+
sections.push('## Sessions\n\n' + formatSessionTable(sessions));
|
|
256
|
+
if (items.length < total)
|
|
257
|
+
sections.push(`\nShowing ${items.length} of ${total}`);
|
|
258
|
+
return sections.length > 0 ? sections.join('\n\n') : 'No results.';
|
|
259
|
+
}
|
|
260
|
+
const SEARCH_USAGE = `Usage: hafez search <terms> [--kind entity|knowledge|session|all]
|
|
261
|
+
|
|
262
|
+
Without --kind, entities and knowledge notes are searched. --kind all adds
|
|
263
|
+
the session notes; --kind session searches them alone.`;
|
|
217
264
|
const SEARCH_FLAGS = { kind: 'string' };
|
|
265
|
+
const SEARCH_KINDS = ['entity', 'knowledge', 'session', 'all'];
|
|
218
266
|
export async function cmdSearch(os, args, opts = {}) {
|
|
219
267
|
const { flags, positionals } = parseArgs(args, SEARCH_FLAGS, SEARCH_USAGE);
|
|
220
268
|
const terms = positionals.join(' ');
|
|
221
269
|
if (!terms)
|
|
222
270
|
throw new UsageError(SEARCH_USAGE);
|
|
223
|
-
const kind =
|
|
271
|
+
const kind = str(flags.kind);
|
|
272
|
+
if (kind !== undefined && !SEARCH_KINDS.includes(kind)) {
|
|
273
|
+
throw new UsageError(`--kind must be one of ${SEARCH_KINDS.join(', ')} (got "${kind}")\n${SEARCH_USAGE}`);
|
|
274
|
+
}
|
|
224
275
|
const results = await os.search(terms, kind);
|
|
225
276
|
if (opts.json)
|
|
226
277
|
return jsonOut(results);
|
|
@@ -356,11 +407,11 @@ export async function cmdCreate(os, args, opts = {}) {
|
|
|
356
407
|
}
|
|
357
408
|
throw new UsageError(`Unknown kind: ${kind}. Use entity or knowledge.`);
|
|
358
409
|
}
|
|
359
|
-
const UPDATE_USAGE = 'Usage: hafez update <slug> [--brief text] [--description text] [--resource uri] [--add-action text (repeats)] [--complete-action handle-or-text] [--remove-action handle-or-text] [--clear-actions] [--status <active|paused|done>] [--parent slug] [--log "<progress|decision|blocker|research>: summary" [--body text]]';
|
|
410
|
+
const UPDATE_USAGE = 'Usage: hafez update <slug> [--brief text] [--description text] [--resource uri] [--add-action text (repeats)] [--complete-action handle-or-text (repeats)] [--remove-action handle-or-text (repeats)] [--clear-actions] [--status <active|paused|done>] [--parent slug] [--log "<progress|decision|blocker|research>: summary" [--body text]]';
|
|
360
411
|
/** Exported so tests can assert the table covers the update schema. */
|
|
361
412
|
export const UPDATE_FLAGS = {
|
|
362
413
|
status: 'string', 'current-state': 'string', brief: 'string',
|
|
363
|
-
'add-action': 'repeat', 'complete-action': '
|
|
414
|
+
'add-action': 'repeat', 'complete-action': 'repeat', 'remove-action': 'repeat',
|
|
364
415
|
'clear-actions': 'boolean', parent: 'string',
|
|
365
416
|
description: 'string', resource: 'string', domain: 'string', confidence: 'string',
|
|
366
417
|
tags: 'string', related: 'string', insight: 'string', synthesis: 'string',
|
|
@@ -462,7 +513,14 @@ async function updateFailure(os, slug, err, opts) {
|
|
|
462
513
|
return err;
|
|
463
514
|
let retry;
|
|
464
515
|
try {
|
|
465
|
-
|
|
516
|
+
const file = await os.read(slug, 'full');
|
|
517
|
+
// The Op Spec resolves no session note, so the update missed; the slug
|
|
518
|
+
// names one all the same. Say what it is instead of offering a Footer
|
|
519
|
+
// it will never have.
|
|
520
|
+
if (file.kind === 'session') {
|
|
521
|
+
return new RenderedError('VALIDATION_FAILED', `'${slug}' is a session note: written once and read, never updated. What matters from it belongs on the entity it touched.`, 'stderr');
|
|
522
|
+
}
|
|
523
|
+
retry = { footer: await footerFor(os, slug, file, 'full') };
|
|
466
524
|
}
|
|
467
525
|
catch (readErr) {
|
|
468
526
|
if (!(readErr instanceof HafezError) || readErr.code !== 'NOT_FOUND')
|
package/dist/cli/format.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import type { QueryResult, KnowledgeQueryResult, ValidationReport, SearchResult, VaultStats, ChangelogEntry } from '../types.js';
|
|
1
|
+
import type { QueryResult, KnowledgeQueryResult, SessionQueryResult, ValidationReport, SearchResult, VaultStats, ChangelogEntry } from '../types.js';
|
|
2
2
|
/** `parent` is the rendered value of the `parent:` line (the slug with the
|
|
3
|
-
* parent's description); absent, the slug alone.
|
|
4
|
-
|
|
3
|
+
* parent's description); absent, the slug alone. `pointers` are the lines
|
|
4
|
+
* naming what hangs off the entity elsewhere (its sessions, its archive). */
|
|
5
|
+
export declare function formatEntityHeader(fm: Record<string, any>, body: string, parent?: string | undefined, pointers?: string[]): string;
|
|
6
|
+
/** A session note: its day, then the entities it touched. */
|
|
7
|
+
export declare function formatSessionHeader(fm: Record<string, any>, body: string): string;
|
|
5
8
|
export declare function formatKnowledgeHeader(fm: Record<string, any>, body: string): string;
|
|
6
9
|
export type QueryTableRow = Pick<QueryResult, 'slug' | 'name' | 'type' | 'status' | 'next_action' | 'next_action_count'>;
|
|
7
10
|
export declare function formatQueryTable(results: QueryTableRow[], total?: number): string;
|
|
8
11
|
export type KnowledgeTableRow = Pick<KnowledgeQueryResult, 'slug' | 'name' | 'confidence' | 'domain' | 'reinforcement_count'>;
|
|
9
12
|
export declare function formatKnowledgeTable(results: KnowledgeTableRow[], total?: number): string;
|
|
13
|
+
export type SessionTableRow = Pick<SessionQueryResult, 'slug' | 'name' | 'session_date' | 'related'>;
|
|
14
|
+
export declare function formatSessionTable(results: SessionTableRow[], total?: number): string;
|
|
10
15
|
export declare function formatSearchResults(results: SearchResult[]): string;
|
|
11
16
|
export declare function formatStats(stats: VaultStats): string;
|
|
12
17
|
export declare function formatChangelog(entries: ChangelogEntry[]): string;
|
package/dist/cli/format.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** `parent` is the rendered value of the `parent:` line (the slug with the
|
|
2
|
-
* parent's description); absent, the slug alone.
|
|
3
|
-
|
|
2
|
+
* parent's description); absent, the slug alone. `pointers` are the lines
|
|
3
|
+
* naming what hangs off the entity elsewhere (its sessions, its archive). */
|
|
4
|
+
export function formatEntityHeader(fm, body, parent = fm.parent, pointers = []) {
|
|
4
5
|
const lines = [`# ${fm.name}`];
|
|
5
6
|
if (fm.description)
|
|
6
7
|
lines.push(`> ${fm.description}`);
|
|
@@ -29,6 +30,23 @@ export function formatEntityHeader(fm, body, parent = fm.parent) {
|
|
|
29
30
|
lines.push(`tags: ${fm.tags.join(', ')}`);
|
|
30
31
|
if (fm.resource)
|
|
31
32
|
lines.push(`resource: ${fm.resource}`);
|
|
33
|
+
lines.push(...pointers);
|
|
34
|
+
lines.push('');
|
|
35
|
+
lines.push(body);
|
|
36
|
+
return lines.join('\n');
|
|
37
|
+
}
|
|
38
|
+
/** A session note: its day, then the entities it touched. */
|
|
39
|
+
export function formatSessionHeader(fm, body) {
|
|
40
|
+
const lines = [`# ${fm.name}`];
|
|
41
|
+
const meta = [];
|
|
42
|
+
if (fm['session-date'])
|
|
43
|
+
meta.push(`session-date: ${fm['session-date']}`);
|
|
44
|
+
if (fm.created)
|
|
45
|
+
meta.push(`created: ${fm.created}`);
|
|
46
|
+
if (meta.length)
|
|
47
|
+
lines.push(meta.join(' | '));
|
|
48
|
+
if (fm.related?.length)
|
|
49
|
+
lines.push(`related: ${fm.related.join(', ')}`);
|
|
32
50
|
lines.push('');
|
|
33
51
|
lines.push(body);
|
|
34
52
|
return lines.join('\n');
|
|
@@ -100,11 +118,24 @@ export function formatKnowledgeTable(results, total) {
|
|
|
100
118
|
}
|
|
101
119
|
return lines.join('\n');
|
|
102
120
|
}
|
|
121
|
+
export function formatSessionTable(results, total) {
|
|
122
|
+
if (results.length === 0)
|
|
123
|
+
return 'No results.';
|
|
124
|
+
const lines = ['| slug | name | session-date | related |', '|------|------|--------------|---------|'];
|
|
125
|
+
for (const r of results) {
|
|
126
|
+
lines.push(`| ${r.slug} | ${r.name} | ${r.session_date} | ${r.related.join(', ')} |`);
|
|
127
|
+
}
|
|
128
|
+
if (total != null && results.length < total) {
|
|
129
|
+
lines.push(`\nShowing ${results.length} of ${total}`);
|
|
130
|
+
}
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
}
|
|
103
133
|
export function formatSearchResults(results) {
|
|
104
134
|
if (results.length === 0)
|
|
105
135
|
return 'No results.';
|
|
106
136
|
const entities = results.filter(r => r.kind === 'entity');
|
|
107
137
|
const knowledge = results.filter(r => r.kind === 'knowledge');
|
|
138
|
+
const sessions = results.filter(r => r.kind === 'session');
|
|
108
139
|
const sections = [];
|
|
109
140
|
if (entities.length > 0) {
|
|
110
141
|
const lines = ['## Entities', '', '| slug | name | type | match |', '|------|------|------|-------|'];
|
|
@@ -118,6 +149,12 @@ export function formatSearchResults(results) {
|
|
|
118
149
|
lines.push(`| ${r.slug} | ${r.name} | ${r.confidence ?? ''} | ${r.snippet} |`);
|
|
119
150
|
sections.push(lines.join('\n'));
|
|
120
151
|
}
|
|
152
|
+
if (sessions.length > 0) {
|
|
153
|
+
const lines = ['## Sessions', '', '| slug | name | match |', '|------|------|-------|'];
|
|
154
|
+
for (const r of sessions)
|
|
155
|
+
lines.push(`| ${r.slug} | ${r.name} | ${r.snippet} |`);
|
|
156
|
+
sections.push(lines.join('\n'));
|
|
157
|
+
}
|
|
121
158
|
return sections.join('\n\n');
|
|
122
159
|
}
|
|
123
160
|
export function formatStats(stats) {
|
|
@@ -162,10 +199,11 @@ export function formatChangelog(entries) {
|
|
|
162
199
|
export function formatValidation(report) {
|
|
163
200
|
const issues = report.broken_slugs.length + report.orphaned_knowledge.length +
|
|
164
201
|
report.missing_fields.length;
|
|
202
|
+
const counts = `${report.total_entities} entities, ${report.total_knowledge} knowledge, ${report.total_sessions} sessions`;
|
|
165
203
|
if (issues === 0) {
|
|
166
|
-
return `Vault OK — ${
|
|
204
|
+
return `Vault OK — ${counts}, 0 issues`;
|
|
167
205
|
}
|
|
168
|
-
const sections = [`## Vault Validation — ${
|
|
206
|
+
const sections = [`## Vault Validation — ${counts}`];
|
|
169
207
|
const addSection = (title, items) => {
|
|
170
208
|
if (items.length === 0)
|
|
171
209
|
return;
|
package/dist/cli/index.js
CHANGED
|
@@ -68,6 +68,7 @@ export const EXIT_TABLE = [
|
|
|
68
68
|
{ exit: 5, meaning: 'slug already exists', errorCodes: ['SLUG_EXISTS'] },
|
|
69
69
|
{ exit: 6, meaning: 'git commit failed — nothing was written to history', errorCodes: ['GIT_COMMIT_FAILED'] },
|
|
70
70
|
{ exit: 7, meaning: 'vault locked by another hafez process — retry when it finishes', errorCodes: ['VAULT_LOCKED'] },
|
|
71
|
+
{ exit: 8, meaning: "git read failed (changelog, stats) — the vault's git is unusable", errorCodes: ['GIT_READ_FAILED'] },
|
|
71
72
|
];
|
|
72
73
|
export const EXIT_CODES = Object.fromEntries(EXIT_TABLE.flatMap(row => (row.errorCodes ?? []).map(code => [code, row.exit])));
|
|
73
74
|
/** Exported so a guard test can assert every registered command is documented
|
|
@@ -75,11 +76,14 @@ export const EXIT_CODES = Object.fromEntries(EXIT_TABLE.flatMap(row => (row.erro
|
|
|
75
76
|
export const USAGE = `Usage: hafez <command> [args]
|
|
76
77
|
|
|
77
78
|
Commands:
|
|
78
|
-
read <slug> Read entity or
|
|
79
|
+
read <slug> Read an entity, knowledge note or session note
|
|
80
|
+
--archive An entity's archived log entries
|
|
79
81
|
query [--filter ...] ... Filter entities/knowledge
|
|
80
|
-
--kind all
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
--kind all Every kind, sessions included; --filter matches
|
|
83
|
+
entity status only, so the rest always show
|
|
84
|
+
--kind session Session notes alone, newest first
|
|
85
|
+
search <terms> Full-text search of entities and knowledge
|
|
86
|
+
--kind session|all Session notes alone, or every kind
|
|
83
87
|
create <kind> <name> Create entity or knowledge note
|
|
84
88
|
capture <name> [--notes] Quick inbox capture
|
|
85
89
|
promote <slug> <target> Promote capture→entity/project/knowledge, entity→project
|
|
@@ -88,9 +92,9 @@ Commands:
|
|
|
88
92
|
--brief "text" Set/replace Brief (use --brief "" to clear)
|
|
89
93
|
--description "text" Set one-line description (use --description "" to clear)
|
|
90
94
|
--resource "uri" Set canonical resource URI, entities only (use --resource "" to clear)
|
|
91
|
-
--add-action "text" Add a next action
|
|
92
|
-
--complete-action handle-or-text Complete the action with that Handle (as read shows it), else the matching one
|
|
93
|
-
--remove-action handle-or-text Remove the action with that Handle, else the matching one
|
|
95
|
+
--add-action "text" Add a next action (repeats)
|
|
96
|
+
--complete-action handle-or-text Complete the action with that Handle (as read shows it), else the matching one (repeats)
|
|
97
|
+
--remove-action handle-or-text Remove the action with that Handle, else the matching one (repeats)
|
|
94
98
|
--clear-actions Remove all next actions
|
|
95
99
|
--synthesis "text" Set/replace Synthesis section (--insight is an alias)
|
|
96
100
|
--add-evidence "text" Append to Evidence section
|
|
@@ -109,11 +113,14 @@ Commands:
|
|
|
109
113
|
init --register <path> Register an existing vault
|
|
110
114
|
init Show current vault resolution
|
|
111
115
|
onboard Setup status, then the script your agent follows to set Hafez up
|
|
112
|
-
--install --mode <always-on|invoke-only> [--harness <h>] [--confirm]
|
|
116
|
+
--install --mode <always-on|invoke-only> [--harness <h>] [--vault-repo <o/r>] [--confirm]
|
|
113
117
|
Write both skills and, always-on, the Block
|
|
118
|
+
(--vault-repo: the cloud Block, which attaches that repo)
|
|
114
119
|
--remove [--confirm] Remove the skills and the Block region
|
|
115
120
|
--block [--harness <h>] Print the Block and the entities matching this folder
|
|
116
121
|
(the SessionStart hook; prints nothing on any failure)
|
|
122
|
+
--cloud --vault-repo <o/r>
|
|
123
|
+
Print the setup script for Claude Code cloud sessions
|
|
117
124
|
upgrade Check whether hafez is outdated and print the update steps
|
|
118
125
|
help --agent The hafez skill body: the reference when the skill is not installed
|
|
119
126
|
|
|
@@ -280,20 +287,25 @@ export async function main(argv) {
|
|
|
280
287
|
// isBindingsFailure, exactly as it was when this import was static.
|
|
281
288
|
const { createHafez } = await import('../index.js');
|
|
282
289
|
const os = createHafez({ vaultPath, git: { push: false } });
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
290
|
+
try {
|
|
291
|
+
const result = await spec.run(os, args, { json, vaultPath });
|
|
292
|
+
// Auto-sync after mutating commands
|
|
293
|
+
if (spec.mutating) {
|
|
294
|
+
try {
|
|
295
|
+
await os.sync();
|
|
296
|
+
}
|
|
297
|
+
catch (syncErr) {
|
|
298
|
+
// Operation succeeded but sync failed — emit result with warning, exit 4
|
|
299
|
+
process.stdout.write(result + '\n');
|
|
300
|
+
process.stderr.write(`Warning: ${syncErr.message ?? syncErr.message}\n`);
|
|
301
|
+
process.exit(4);
|
|
302
|
+
}
|
|
294
303
|
}
|
|
304
|
+
process.stdout.write(result + '\n');
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
os.close();
|
|
295
308
|
}
|
|
296
|
-
process.stdout.write(result + '\n');
|
|
297
309
|
}
|
|
298
310
|
catch (err) {
|
|
299
311
|
if (hook)
|
package/dist/cli/install.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export declare const SKILL_DIRS: string[];
|
|
|
6
6
|
export type HashState = 'absent' | 'current' | 'stale' | 'hand-edited';
|
|
7
7
|
/** What to do about broken markers: a hand edit, then `rerun`. */
|
|
8
8
|
export declare const fixBrokenMarkers: (path: string, rerun: string) => string;
|
|
9
|
+
/** The flag that keeps an install a cloud one, for every printed install line. */
|
|
10
|
+
export declare const vaultRepoFlag: (vaultRepo: string | undefined) => string;
|
|
11
|
+
/** What to do about a 1.x section: Hafez cannot tell where it ends. */
|
|
12
|
+
export declare const removeLegacy: (path: string, line: number) => string;
|
|
13
|
+
/** The status line for a 1.x section. */
|
|
14
|
+
export declare const legacyLine: (path: string, line: number) => string;
|
|
9
15
|
/** Codex reads an `AGENTS.override.md` in place of `AGENTS.md` when one
|
|
10
16
|
* exists: the first candidate present, else the last. */
|
|
11
17
|
export declare const instructionsFile: (home: string, candidates: readonly string[]) => string;
|
|
@@ -18,13 +24,20 @@ export interface InstallState {
|
|
|
18
24
|
skills: HashState;
|
|
19
25
|
/** Some skill file carries a hash: `onboard --install` has run here. */
|
|
20
26
|
onboarded: boolean;
|
|
27
|
+
/** The line of a 1.x section in the instructions file, else null. */
|
|
28
|
+
legacy: number | null;
|
|
29
|
+
/** The Block is a cloud Block, for this vault repo. */
|
|
30
|
+
vaultRepo?: string;
|
|
21
31
|
}
|
|
22
32
|
export declare function installState(home: string, harness: HarnessId): InstallState;
|
|
33
|
+
/** `vaultRepo`: the install is for a cloud container, whose Block carries
|
|
34
|
+
* the rule that attaches that repo as the vault (#160). */
|
|
23
35
|
export declare function install(opts: {
|
|
24
36
|
home: string;
|
|
25
37
|
harness: HarnessId;
|
|
26
38
|
mode: Mode;
|
|
27
39
|
confirm: boolean;
|
|
40
|
+
vaultRepo?: string;
|
|
28
41
|
}): string;
|
|
29
42
|
export interface SkillCheck {
|
|
30
43
|
path: string;
|
|
@@ -37,13 +50,21 @@ export interface BlockCheck {
|
|
|
37
50
|
state: Exclude<HashState, 'absent'> | 'broken';
|
|
38
51
|
/** The Block Hafez writes has changed since this one was written. */
|
|
39
52
|
renderChanged: boolean;
|
|
53
|
+
/** A cloud Block: the vault repo it was written for. */
|
|
54
|
+
vaultRepo?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface LegacyCheck {
|
|
57
|
+
harness: HarnessId;
|
|
58
|
+
path: string;
|
|
59
|
+
line: number;
|
|
40
60
|
}
|
|
41
61
|
/** Both skills in both directories, and the Block in the instructions file
|
|
42
62
|
* each Harness reads — the one `--install` would write. Only Blocks that are
|
|
43
|
-
* there are listed: none means invoke-only. */
|
|
63
|
+
* there are listed: none, and no 1.x section, means invoke-only. */
|
|
44
64
|
export declare function checkInstall(home: string): {
|
|
45
65
|
skills: SkillCheck[];
|
|
46
66
|
blocks: BlockCheck[];
|
|
67
|
+
legacy: LegacyCheck[];
|
|
47
68
|
};
|
|
48
69
|
/** Remove everything Hafez wrote: both skills from both directories, and the
|
|
49
70
|
* Block region from every instructions file a Harness reads. Nothing else. */
|