hafez 2.1.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 +16 -19
- package/dist/batch-ops.js +3 -1
- package/dist/cli/block-hook.js +24 -9
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +106 -48
- package/dist/cli/format.d.ts +8 -3
- package/dist/cli/format.js +42 -4
- package/dist/cli/index.js +29 -17
- package/dist/cli/install.d.ts +9 -0
- package/dist/cli/install.js +36 -15
- package/dist/cli/onboard.js +30 -9
- package/dist/cli/upgrade.js +4 -3
- 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 +84 -5
- package/dist/surfaces/surfaces.d.ts +8 -5
- package/dist/surfaces/surfaces.js +31 -16
- package/dist/types.d.ts +63 -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/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
|
|
@@ -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,8 @@ 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;
|
|
9
11
|
/** What to do about a 1.x section: Hafez cannot tell where it ends. */
|
|
10
12
|
export declare const removeLegacy: (path: string, line: number) => string;
|
|
11
13
|
/** The status line for a 1.x section. */
|
|
@@ -24,13 +26,18 @@ export interface InstallState {
|
|
|
24
26
|
onboarded: boolean;
|
|
25
27
|
/** The line of a 1.x section in the instructions file, else null. */
|
|
26
28
|
legacy: number | null;
|
|
29
|
+
/** The Block is a cloud Block, for this vault repo. */
|
|
30
|
+
vaultRepo?: string;
|
|
27
31
|
}
|
|
28
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). */
|
|
29
35
|
export declare function install(opts: {
|
|
30
36
|
home: string;
|
|
31
37
|
harness: HarnessId;
|
|
32
38
|
mode: Mode;
|
|
33
39
|
confirm: boolean;
|
|
40
|
+
vaultRepo?: string;
|
|
34
41
|
}): string;
|
|
35
42
|
export interface SkillCheck {
|
|
36
43
|
path: string;
|
|
@@ -43,6 +50,8 @@ export interface BlockCheck {
|
|
|
43
50
|
state: Exclude<HashState, 'absent'> | 'broken';
|
|
44
51
|
/** The Block Hafez writes has changed since this one was written. */
|
|
45
52
|
renderChanged: boolean;
|
|
53
|
+
/** A cloud Block: the vault repo it was written for. */
|
|
54
|
+
vaultRepo?: string;
|
|
46
55
|
}
|
|
47
56
|
export interface LegacyCheck {
|
|
48
57
|
harness: HarnessId;
|
package/dist/cli/install.js
CHANGED
|
@@ -14,6 +14,7 @@ import { dirname, join } from 'path';
|
|
|
14
14
|
import { HafezError } from '../types.js';
|
|
15
15
|
import { HARNESSES } from '../harness.js';
|
|
16
16
|
import { contentHash, renderBlock, renderHafezSaveSkill, renderHafezSkill } from '../surfaces/surfaces.js';
|
|
17
|
+
import { isVaultRepo } from '../surfaces/items.js';
|
|
17
18
|
export const MODES = ['always-on', 'invoke-only'];
|
|
18
19
|
export const isMode = (value) => MODES.includes(value);
|
|
19
20
|
export const SKILL_DIRS = ['.claude/skills', '.agents/skills'];
|
|
@@ -36,7 +37,8 @@ function hashState(found, rendered) {
|
|
|
36
37
|
return 'stale';
|
|
37
38
|
}
|
|
38
39
|
// --- The Block region ---
|
|
39
|
-
|
|
40
|
+
/** The hash, then a cloud Block's vault repo (#160). */
|
|
41
|
+
const BEGIN = /^<!-- hafez:begin ([0-9a-f]{12})(?: vault-repo=(\S+))? -->$/;
|
|
40
42
|
const END = '<!-- hafez:end -->';
|
|
41
43
|
/** What broken markers are, for a refusal or a report about `path`. */
|
|
42
44
|
const brokenMarkers = (path) => `The Hafez markers in ${path} are broken: expected one "<!-- hafez:begin <hash> -->" line followed by one "${END}" line.`;
|
|
@@ -62,8 +64,9 @@ function findRegion(file, displayPath) {
|
|
|
62
64
|
return null;
|
|
63
65
|
const [begin] = begins;
|
|
64
66
|
const [end] = ends;
|
|
65
|
-
const hash = begin
|
|
66
|
-
if (begins.length !== 1 || ends.length !== 1 || end.start < begin.start || !hash || end.line !== END
|
|
67
|
+
const [, hash, vaultRepo] = (begin && BEGIN.exec(begin.line)) || [];
|
|
68
|
+
if (begins.length !== 1 || ends.length !== 1 || end.start < begin.start || !hash || end.line !== END
|
|
69
|
+
|| (vaultRepo !== undefined && !isVaultRepo(vaultRepo))) {
|
|
67
70
|
throw new HafezError('VALIDATION_FAILED', `${brokenMarkers(displayPath)} Fix or delete them by hand, then re-run.`);
|
|
68
71
|
}
|
|
69
72
|
return {
|
|
@@ -73,8 +76,11 @@ function findRegion(file, displayPath) {
|
|
|
73
76
|
hash,
|
|
74
77
|
body: file.slice(begin.next, end.start),
|
|
75
78
|
text: file.slice(begin.start, end.start + END.length),
|
|
79
|
+
...(vaultRepo !== undefined && { vaultRepo }),
|
|
76
80
|
};
|
|
77
81
|
}
|
|
82
|
+
/** The flag that keeps an install a cloud one, for every printed install line. */
|
|
83
|
+
export const vaultRepoFlag = (vaultRepo) => (vaultRepo === undefined ? '' : ` --vault-repo ${vaultRepo}`);
|
|
78
84
|
// --- The 1.x section ---
|
|
79
85
|
/** The heading 1.2–1.4 `onboard` had the agent paste, with no markers (#157). */
|
|
80
86
|
const LEGACY_HEADING = '## Hafez — Session Memory';
|
|
@@ -169,14 +175,22 @@ function skillSteps(home, want) {
|
|
|
169
175
|
return { line: `~/${rel}: ${state === 'absent' ? 'written' : `replaced (was ${state})`}`, write: () => write(path, rendered) };
|
|
170
176
|
});
|
|
171
177
|
}
|
|
172
|
-
const
|
|
173
|
-
|
|
178
|
+
const RENDERS = new Map();
|
|
179
|
+
/** The Block Hafez writes, as a region; with `vaultRepo`, the cloud Block. A
|
|
180
|
+
* found Block is checked against the render for its own marker's repo. */
|
|
181
|
+
function rendered(vaultRepo) {
|
|
182
|
+
if (!RENDERS.has(vaultRepo))
|
|
183
|
+
RENDERS.set(vaultRepo, findRegion(renderBlock(vaultRepo), 'the render'));
|
|
184
|
+
return RENDERS.get(vaultRepo);
|
|
185
|
+
}
|
|
186
|
+
function blockStep({ home, rel, want, confirm, rerun, suffix = '', vaultRepo }) {
|
|
174
187
|
const path = join(home, rel);
|
|
175
188
|
const label = `Block in ~/${rel}`;
|
|
176
189
|
const file = readInstalled(path);
|
|
177
190
|
const text = file ?? '';
|
|
178
191
|
const region = findRegion(text, `~/${rel}`);
|
|
179
|
-
const
|
|
192
|
+
const target = rendered(want === 'write' ? vaultRepo : region?.vaultRepo);
|
|
193
|
+
const state = hashState(region, target.text);
|
|
180
194
|
if (region && state === 'hand-edited' && !confirm) {
|
|
181
195
|
return {
|
|
182
196
|
line: `${label}: hand-edited, left alone`,
|
|
@@ -186,7 +200,7 @@ function blockStep({ home, rel, want, confirm, rerun, suffix = '' }) {
|
|
|
186
200
|
rerun,
|
|
187
201
|
'',
|
|
188
202
|
'````diff',
|
|
189
|
-
lineDiff(region.body,
|
|
203
|
+
lineDiff(region.body, target.body),
|
|
190
204
|
'````',
|
|
191
205
|
].join('\n'),
|
|
192
206
|
};
|
|
@@ -200,11 +214,11 @@ function blockStep({ home, rel, want, confirm, rerun, suffix = '' }) {
|
|
|
200
214
|
return { line: `${label}: current` };
|
|
201
215
|
if (!region) {
|
|
202
216
|
const before = text === '' || text.endsWith('\n') ? text : `${text}\n`;
|
|
203
|
-
return { line: `${label}: written`, write: () => write(path, before + renderBlock()) };
|
|
217
|
+
return { line: `${label}: written`, write: () => write(path, before + renderBlock(vaultRepo)) };
|
|
204
218
|
}
|
|
205
219
|
return {
|
|
206
220
|
line: `${label}: replaced (was ${state})`,
|
|
207
|
-
write: () => write(path, text.slice(0, region.start) +
|
|
221
|
+
write: () => write(path, text.slice(0, region.start) + target.text + text.slice(region.end)),
|
|
208
222
|
};
|
|
209
223
|
}
|
|
210
224
|
/** Codex reads an `AGENTS.override.md` in place of `AGENTS.md` when one
|
|
@@ -228,13 +242,15 @@ export function installState(home, harness) {
|
|
|
228
242
|
const { instructions } = HARNESSES[harness];
|
|
229
243
|
let block = null;
|
|
230
244
|
let legacy = null;
|
|
245
|
+
let vaultRepo;
|
|
231
246
|
if (instructions !== null) {
|
|
232
247
|
const rel = instructionsFile(home, instructions);
|
|
233
248
|
const text = readInstalled(join(home, rel)) ?? '';
|
|
234
249
|
let region;
|
|
235
250
|
try {
|
|
236
251
|
region = findRegion(text, `~/${rel}`);
|
|
237
|
-
|
|
252
|
+
vaultRepo = region?.vaultRepo;
|
|
253
|
+
block = hashState(region, rendered(vaultRepo).text);
|
|
238
254
|
}
|
|
239
255
|
catch {
|
|
240
256
|
block = 'broken';
|
|
@@ -249,11 +265,13 @@ export function installState(home, harness) {
|
|
|
249
265
|
const states = new Set(skills.map(s => s.state));
|
|
250
266
|
const [only] = states;
|
|
251
267
|
const merged = states.size === 1 ? only : states.has('hand-edited') ? 'hand-edited' : 'stale';
|
|
252
|
-
return { block, skills: merged, onboarded: skills.some(s => s.hashed), legacy };
|
|
268
|
+
return { block, skills: merged, onboarded: skills.some(s => s.hashed), legacy, ...(vaultRepo !== undefined && { vaultRepo }) };
|
|
253
269
|
}
|
|
254
270
|
// --- The two commands ---
|
|
271
|
+
/** `vaultRepo`: the install is for a cloud container, whose Block carries
|
|
272
|
+
* the rule that attaches that repo as the vault (#160). */
|
|
255
273
|
export function install(opts) {
|
|
256
|
-
const { home, harness, mode, confirm } = opts;
|
|
274
|
+
const { home, harness, mode, confirm, vaultRepo } = opts;
|
|
257
275
|
const { label, instructions } = HARNESSES[harness];
|
|
258
276
|
const steps = skillSteps(home, 'write');
|
|
259
277
|
const tail = [];
|
|
@@ -273,8 +291,9 @@ export function install(opts) {
|
|
|
273
291
|
rel,
|
|
274
292
|
want: mode === 'always-on' ? 'write' : 'remove',
|
|
275
293
|
confirm,
|
|
276
|
-
rerun: `hafez onboard --install --harness ${harness} --mode ${mode} --confirm`,
|
|
294
|
+
rerun: `hafez onboard --install --harness ${harness} --mode ${mode}${vaultRepoFlag(vaultRepo)} --confirm`,
|
|
277
295
|
suffix: mode === 'invoke-only' ? ' (invoke-only)' : '',
|
|
296
|
+
vaultRepo,
|
|
278
297
|
}));
|
|
279
298
|
}
|
|
280
299
|
const out = run({ header: `${label}, ${mode}:`, steps, tail, unchanged: 'Nothing changed: the install is current.', listAll: true });
|
|
@@ -316,8 +335,10 @@ export function checkInstall(home) {
|
|
|
316
335
|
legacy.push({ harness, path, line });
|
|
317
336
|
if (!region)
|
|
318
337
|
continue;
|
|
319
|
-
const
|
|
320
|
-
|
|
338
|
+
const { vaultRepo } = region;
|
|
339
|
+
const render = rendered(vaultRepo);
|
|
340
|
+
const state = hashState(region, render.text);
|
|
341
|
+
blocks.push({ harness, path, state, renderChanged: region.hash !== render.hash, ...(vaultRepo !== undefined && { vaultRepo }) });
|
|
321
342
|
}
|
|
322
343
|
return { skills, blocks, legacy };
|
|
323
344
|
}
|
package/dist/cli/onboard.js
CHANGED
|
@@ -9,33 +9,36 @@
|
|
|
9
9
|
// `--install` and `--remove` write the files Hafez owns outside the vault
|
|
10
10
|
// (install.ts). The binary has no interactive prompts: the agent asks, then
|
|
11
11
|
// passes the answer as a flag. `--block` is the SessionStart hook's render
|
|
12
|
-
// (block-hook.ts) and writes nothing.
|
|
12
|
+
// (block-hook.ts) and writes nothing. `--cloud` prints the setup script for a
|
|
13
|
+
// Claude Code cloud environment, whose `--install --vault-repo` writes the
|
|
14
|
+
// Block with the rule that attaches the vault repo (#160).
|
|
13
15
|
import { existsSync, readFileSync } from 'fs';
|
|
14
16
|
import { homedir } from 'os';
|
|
15
17
|
import { join } from 'path';
|
|
16
18
|
import { UsageError, parseArgs, str } from './args.js';
|
|
17
19
|
import { HARNESSES, ONBOARD_HARNESSES, detectAgent, isHarness, isOnboardHarness } from '../harness.js';
|
|
18
|
-
import { MODES, SKILL_DIRS, fixBrokenMarkers, install, installState, instructionsFile, isMode, legacyLine, remove, removeLegacy } from './install.js';
|
|
20
|
+
import { MODES, SKILL_DIRS, fixBrokenMarkers, install, installState, instructionsFile, isMode, legacyLine, remove, removeLegacy, vaultRepoFlag } from './install.js';
|
|
19
21
|
import { noVaultMessage, resolveVaultPath } from './resolve-vault.js';
|
|
20
22
|
import { HafezError } from '../types.js';
|
|
21
23
|
import { kindDir, scanVaultDir } from '../vault.js';
|
|
22
|
-
import { ITEMS } from '../surfaces/items.js';
|
|
24
|
+
import { ITEMS, isVaultRepo } from '../surfaces/items.js';
|
|
23
25
|
import { renderBlock } from '../surfaces/surfaces.js';
|
|
24
26
|
import { blockHook } from './block-hook.js';
|
|
25
27
|
const HARNESS_IDS = Object.keys(HARNESSES).join('|');
|
|
26
28
|
const MODE_IDS = MODES.join('|');
|
|
27
29
|
const ONBOARD_USAGE = `Usage:
|
|
28
30
|
hafez onboard
|
|
29
|
-
hafez onboard --install --mode <${MODE_IDS}> [--harness <${HARNESS_IDS}>] [--confirm]
|
|
31
|
+
hafez onboard --install --mode <${MODE_IDS}> [--harness <${HARNESS_IDS}>] [--vault-repo <owner>/<repo>] [--confirm]
|
|
30
32
|
hafez onboard --remove [--confirm]
|
|
31
|
-
hafez onboard --block [--harness <${HARNESS_IDS}>]
|
|
33
|
+
hafez onboard --block [--harness <${HARNESS_IDS}>]
|
|
34
|
+
hafez onboard --cloud --vault-repo <owner>/<repo>`;
|
|
32
35
|
/** `ctx` is the temp-home seam: tests pass a temporary home and environment;
|
|
33
36
|
* the install functions are given the home, never read it. `vaultFlag` is the
|
|
34
37
|
* global `--vault`; without it the bare form reports on the registered vault.
|
|
35
38
|
* `--block` returns a promise of its render, or of null when it must print nothing. */
|
|
36
39
|
export function cmdOnboard(args, ctx = {}) {
|
|
37
40
|
const { home = homedir(), env = process.env, cwd, vaultFlag } = ctx;
|
|
38
|
-
const { flags, positionals } = parseArgs(args, { install: 'boolean', remove: 'boolean', block: 'boolean', mode: 'string', harness: 'string', confirm: 'boolean' }, ONBOARD_USAGE);
|
|
41
|
+
const { flags, positionals } = parseArgs(args, { install: 'boolean', remove: 'boolean', block: 'boolean', cloud: 'boolean', mode: 'string', harness: 'string', 'vault-repo': 'string', confirm: 'boolean' }, ONBOARD_USAGE);
|
|
39
42
|
const fail = (why) => {
|
|
40
43
|
throw new UsageError(`${why}\n${ONBOARD_USAGE}`);
|
|
41
44
|
};
|
|
@@ -43,9 +46,22 @@ export function cmdOnboard(args, ctx = {}) {
|
|
|
43
46
|
fail(`Unexpected argument: ${positionals[0]}`);
|
|
44
47
|
const mode = str(flags.mode);
|
|
45
48
|
const harnessFlag = str(flags.harness);
|
|
49
|
+
const vaultRepo = str(flags['vault-repo']);
|
|
46
50
|
const confirm = flags.confirm === true;
|
|
47
51
|
if (flags.install && flags.remove)
|
|
48
52
|
fail('--install and --remove are separate commands');
|
|
53
|
+
if (vaultRepo !== undefined && !isVaultRepo(vaultRepo))
|
|
54
|
+
fail(`--vault-repo takes the vault's GitHub repo as <owner>/<repo>, got: ${vaultRepo}`);
|
|
55
|
+
if (vaultRepo !== undefined && !flags.cloud && !flags.install)
|
|
56
|
+
fail('--vault-repo goes with --cloud or --install');
|
|
57
|
+
if (flags.cloud) {
|
|
58
|
+
if (flags.install || flags.remove || flags.block || mode !== undefined || harnessFlag !== undefined || confirm) {
|
|
59
|
+
fail('--cloud takes only --vault-repo: it prints the setup script and writes nothing');
|
|
60
|
+
}
|
|
61
|
+
if (vaultRepo === undefined)
|
|
62
|
+
return fail('--cloud needs --vault-repo <owner>/<repo>, the vault\'s GitHub repo');
|
|
63
|
+
return ITEMS.cloudSetup.render({ vaultRepo });
|
|
64
|
+
}
|
|
49
65
|
if (flags.block) {
|
|
50
66
|
if (flags.install || flags.remove || mode !== undefined || confirm)
|
|
51
67
|
fail('--block takes only --harness: it prints the Block and writes nothing');
|
|
@@ -67,7 +83,11 @@ export function cmdOnboard(args, ctx = {}) {
|
|
|
67
83
|
if (!isHarness(harness)) {
|
|
68
84
|
return fail(harnessFlag === undefined ? `No harness detected: pass --harness <${HARNESS_IDS}>` : `Unknown harness: ${harnessFlag}`);
|
|
69
85
|
}
|
|
70
|
-
|
|
86
|
+
// The cloud Block's rule is Claude Code's: cloud sessions attach repos there.
|
|
87
|
+
if (vaultRepo !== undefined && (mode !== 'always-on' || harness !== 'claude-code')) {
|
|
88
|
+
fail('--vault-repo is for a Claude Code cloud container: --mode always-on --harness claude-code');
|
|
89
|
+
}
|
|
90
|
+
return install({ home, harness, mode, confirm, vaultRepo });
|
|
71
91
|
}
|
|
72
92
|
if (mode !== undefined || harnessFlag !== undefined || confirm)
|
|
73
93
|
fail('--mode, --harness and --confirm go with --install, --remove or --block');
|
|
@@ -149,7 +169,8 @@ export function renderOnboard({ home, harness, vault }) {
|
|
|
149
169
|
const teaching = (m) => ITEMS.teachingLines.render({ harness, mode: m });
|
|
150
170
|
const line2 = (m) => ITEMS.teachingLines.render({ harness, mode: m, line2Only: true });
|
|
151
171
|
const instructionsRel = `~/${instructionsFile(home, instructions)}`;
|
|
152
|
-
|
|
172
|
+
// A cloud Block stays one: its install line and Block keep the vault repo.
|
|
173
|
+
const block = `\`\`\`\`\n${renderBlock(state.vaultRepo)}\`\`\`\``;
|
|
153
174
|
const showBlock = `show the user the Block below word for word, not a summary, say it goes into ${instructionsRel} between two marker lines and nothing else in that file changes, and get a yes. Then run:`;
|
|
154
175
|
const status = [
|
|
155
176
|
`Vault: ${vault} (${plural(entities, 'entity', 'entities')}, ${plural(knowledge, 'knowledge note', 'knowledge notes')})`,
|
|
@@ -194,7 +215,7 @@ export function renderOnboard({ home, harness, vault }) {
|
|
|
194
215
|
...(state.legacy === null ? [] : [`${removeLegacy(instructionsRel, state.legacy)} Do this before either install below.`, '']),
|
|
195
216
|
`Always-on: ${showBlock}`,
|
|
196
217
|
'',
|
|
197
|
-
|
|
218
|
+
`hafez onboard --install --mode always-on${vaultRepoFlag(state.vaultRepo)}`,
|
|
198
219
|
'',
|
|
199
220
|
'Invoke-only: no file of theirs is edited. Run:',
|
|
200
221
|
'',
|
package/dist/cli/upgrade.js
CHANGED
|
@@ -29,7 +29,7 @@ import { dirname, join } from 'path';
|
|
|
29
29
|
import { fileURLToPath } from 'url';
|
|
30
30
|
import { UsageError, helpIfAsked } from './args.js';
|
|
31
31
|
import { HARNESSES, detectAgent, isHarness } from '../harness.js';
|
|
32
|
-
import { checkInstall, fixBrokenMarkers, legacyLine, removeLegacy } from './install.js';
|
|
32
|
+
import { checkInstall, fixBrokenMarkers, legacyLine, removeLegacy, vaultRepoFlag } from './install.js';
|
|
33
33
|
const PLAIN_SEMVER = /^\d+\.\d+\.\d+$/;
|
|
34
34
|
const REGISTRY_TIMEOUT_MS = 3000;
|
|
35
35
|
const NPM_PROBE_TIMEOUT_MS = 3000;
|
|
@@ -179,7 +179,7 @@ function blockSteps({ blocks, skills }, upgrading, env) {
|
|
|
179
179
|
const targets = upgrading ? blocks : blocks.filter(blockDrift);
|
|
180
180
|
return (targets.length > 0 ? targets : skillsOnly).map(b => ({
|
|
181
181
|
kind: 'command',
|
|
182
|
-
text: `hafez onboard --install --mode always-on --harness ${b.harness}`,
|
|
182
|
+
text: `hafez onboard --install --mode always-on --harness ${b.harness}${vaultRepoFlag(b.vaultRepo)}`,
|
|
183
183
|
}));
|
|
184
184
|
}
|
|
185
185
|
const harness = detectAgent(env);
|
|
@@ -192,7 +192,8 @@ function blockSteps({ blocks, skills }, upgrading, env) {
|
|
|
192
192
|
}
|
|
193
193
|
/** The action list, in execution order. Every command is built from static
|
|
194
194
|
* constants; the only interpolated values are the regex-validated version,
|
|
195
|
-
* the quoted package root,
|
|
195
|
+
* the quoted package root, harness ids from the HARNESSES table, and a cloud
|
|
196
|
+
* Block's vault repo, which `findRegion` reads only when it passes `isVaultRepo`. */
|
|
196
197
|
function deriveSteps(state, ctx) {
|
|
197
198
|
const steps = [];
|
|
198
199
|
let alternativeInstall = null;
|
package/dist/dates.d.ts
CHANGED
|
@@ -6,5 +6,9 @@ export declare function localDay(d?: Date): string;
|
|
|
6
6
|
* now"), and any explicit instant must anchor to the same local day boundary
|
|
7
7
|
* the stamps and query comparisons use. Relative forms (`7.days.ago`) are
|
|
8
8
|
* already local and pass through untouched.
|
|
9
|
+
*
|
|
10
|
+
* The offset is spelled out: without one git applies its own idea of the
|
|
11
|
+
* local zone, and Git for Windows ignores an IANA `TZ` that Node honours,
|
|
12
|
+
* so the two disagreed about where the day starts.
|
|
9
13
|
*/
|
|
10
14
|
export declare function normalizeSinceDate(since: string): string;
|
package/dist/dates.js
CHANGED
|
@@ -21,7 +21,18 @@ export function localDay(d = new Date()) {
|
|
|
21
21
|
* now"), and any explicit instant must anchor to the same local day boundary
|
|
22
22
|
* the stamps and query comparisons use. Relative forms (`7.days.ago`) are
|
|
23
23
|
* already local and pass through untouched.
|
|
24
|
+
*
|
|
25
|
+
* The offset is spelled out: without one git applies its own idea of the
|
|
26
|
+
* local zone, and Git for Windows ignores an IANA `TZ` that Node honours,
|
|
27
|
+
* so the two disagreed about where the day starts.
|
|
24
28
|
*/
|
|
25
29
|
export function normalizeSinceDate(since) {
|
|
26
|
-
|
|
30
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(since);
|
|
31
|
+
if (!m)
|
|
32
|
+
return since;
|
|
33
|
+
// The day's first instant: 01:00 where a DST jump skips midnight.
|
|
34
|
+
const start = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
35
|
+
const offset = -start.getTimezoneOffset();
|
|
36
|
+
const zone = `${offset < 0 ? '-' : '+'}${pad(Math.floor(Math.abs(offset) / 60))}${pad(Math.abs(offset) % 60)}`;
|
|
37
|
+
return `${localDay(start)}T${pad(start.getHours())}:${pad(start.getMinutes())}:00${zone}`;
|
|
27
38
|
}
|
package/dist/db.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { type VaultKind } from './vault.js';
|
|
1
2
|
import { type VaultIndexRow } from './knowledge-index.js';
|
|
2
|
-
import type { SearchResult, VaultStats, QueryResult, KnowledgeQueryResult, UnifiedResult, EntityQueryOpts, KnowledgeQueryOpts, UnifiedQueryOpts, SortField } from './types.js';
|
|
3
|
-
export declare const SCHEMA_VERSION = "
|
|
3
|
+
import type { SearchResult, SearchKind, VaultStats, QueryResult, KnowledgeQueryResult, SessionQueryResult, UnifiedResult, EntityQueryOpts, KnowledgeQueryOpts, SessionQueryOpts, UnifiedQueryOpts, SortField } from './types.js';
|
|
4
|
+
export declare const SCHEMA_VERSION = "12";
|
|
4
5
|
interface IndexItem {
|
|
5
6
|
slug: string;
|
|
6
|
-
kind:
|
|
7
|
+
kind: VaultKind;
|
|
7
8
|
name: string;
|
|
8
9
|
type: string | null;
|
|
9
10
|
status: string | null;
|
|
@@ -26,6 +27,7 @@ interface IndexItem {
|
|
|
26
27
|
last_session_date: string | null;
|
|
27
28
|
last_session_type: string | null;
|
|
28
29
|
last_session_summary: string | null;
|
|
30
|
+
session_date: string | null;
|
|
29
31
|
}
|
|
30
32
|
export interface IndexQueryResult {
|
|
31
33
|
items: IndexItem[];
|
|
@@ -34,10 +36,10 @@ export interface IndexQueryResult {
|
|
|
34
36
|
export interface HafezIndex {
|
|
35
37
|
rebuild(): void;
|
|
36
38
|
syncIfStale(): void;
|
|
37
|
-
upsertFromFile(slug: string, kind:
|
|
39
|
+
upsertFromFile(slug: string, kind: VaultKind): void;
|
|
38
40
|
removeItem(slug: string): void;
|
|
39
41
|
queryItems(opts: QueryOpts): IndexQueryResult;
|
|
40
|
-
search(query: string, kind?:
|
|
42
|
+
search(query: string, kind?: SearchKind): SearchResult[];
|
|
41
43
|
getStats(): VaultStats;
|
|
42
44
|
/** Every indexed document, in the shape index.md is rendered from. */
|
|
43
45
|
indexEntries(): VaultIndexRow[];
|
|
@@ -58,13 +60,18 @@ export interface HafezIndex {
|
|
|
58
60
|
items: KnowledgeQueryResult[];
|
|
59
61
|
total: number;
|
|
60
62
|
};
|
|
63
|
+
querySessions(opts?: SessionQueryOpts): {
|
|
64
|
+
items: SessionQueryResult[];
|
|
65
|
+
total: number;
|
|
66
|
+
};
|
|
61
67
|
queryUnified(opts?: UnifiedQueryOpts): {
|
|
62
68
|
items: UnifiedResult[];
|
|
63
69
|
total: number;
|
|
64
70
|
};
|
|
65
71
|
}
|
|
72
|
+
/** `kind` unset is the working set, entity + knowledge; `all` is literal. */
|
|
66
73
|
export interface QueryOpts {
|
|
67
|
-
kind?:
|
|
74
|
+
kind?: VaultKind | 'all';
|
|
68
75
|
status?: string;
|
|
69
76
|
type?: string;
|
|
70
77
|
subtype?: string;
|
|
@@ -79,7 +86,7 @@ export interface QueryOpts {
|
|
|
79
86
|
createdSince?: string;
|
|
80
87
|
createdBefore?: string;
|
|
81
88
|
tags?: string[];
|
|
82
|
-
sort_by?:
|
|
89
|
+
sort_by?: SortField;
|
|
83
90
|
sort_order?: 'asc' | 'desc';
|
|
84
91
|
limit?: number;
|
|
85
92
|
offset?: number;
|
|
@@ -96,11 +103,14 @@ export declare function createIndex(vaultPath: string, opts: {
|
|
|
96
103
|
* Asserted against the CLI's flag matrix in query-surface.test.ts. */
|
|
97
104
|
export declare const ENTITY_ONLY_OPTS: readonly ["filter", "type", "parent"];
|
|
98
105
|
export declare const KNOWLEDGE_ONLY_OPTS: readonly ["confidence", "subtype"];
|
|
106
|
+
/** What a session query cannot map: both sides' opts, and the two every
|
|
107
|
+
* other kind shares — a session note carries no domain and no tags. */
|
|
108
|
+
export declare const SESSION_REJECTED_OPTS: readonly ["filter", "type", "parent", "confidence", "subtype", "domain", "tags"];
|
|
99
109
|
/** Every opt any branch might carry, flattened. Only the unified read uses it,
|
|
100
110
|
* and only to check: the checks must see an opt the caller's own type forbids,
|
|
101
111
|
* because an untyped caller can still send it. */
|
|
102
112
|
export type UnmappableCheckOpts = Omit<EntityQueryOpts, 'sort_by'> & Omit<KnowledgeQueryOpts, 'sort_by'> & {
|
|
103
|
-
kind?:
|
|
113
|
+
kind?: VaultKind | 'all';
|
|
104
114
|
sort_by?: SortField;
|
|
105
115
|
};
|
|
106
116
|
export {};
|