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/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,31 @@ 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}`);
|
|
84
|
+
// --- The 1.x section ---
|
|
85
|
+
/** The heading 1.2–1.4 `onboard` had the agent paste, with no markers (#157). */
|
|
86
|
+
const LEGACY_HEADING = '## Hafez — Session Memory';
|
|
87
|
+
/** The 1-based line of a 1.x section outside the Block region, or null. With
|
|
88
|
+
* broken markers (`region` undefined) the region is unknown: every line is read. */
|
|
89
|
+
function findLegacy(file, region) {
|
|
90
|
+
let offset = 0;
|
|
91
|
+
const lines = file.split('\n');
|
|
92
|
+
for (let i = 0; i < lines.length; i++) {
|
|
93
|
+
const inside = region != null && offset >= region.start && offset < region.next;
|
|
94
|
+
if (!inside && lines[i].trimEnd() === LEGACY_HEADING)
|
|
95
|
+
return i + 1;
|
|
96
|
+
offset += lines[i].length + 1;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/** What to do about a 1.x section: Hafez cannot tell where it ends. */
|
|
101
|
+
export const removeLegacy = (path, line) => `Remove the Hafez 1.x section in ${path}, from its heading \`${LEGACY_HEADING}\` (line ${line}) to the end of that section, after the user's yes. It has no markers, so Hafez does not delete it.`;
|
|
102
|
+
/** The status line for a 1.x section. */
|
|
103
|
+
export const legacyLine = (path, line) => `Legacy section in ${path}, line ${line}: \`${LEGACY_HEADING}\``;
|
|
78
104
|
/** A line diff of the user's Block against the render: ` ` both, `-` theirs
|
|
79
105
|
* only, `+` the render only. */
|
|
80
106
|
function lineDiff(theirs, ours) {
|
|
@@ -149,14 +175,22 @@ function skillSteps(home, want) {
|
|
|
149
175
|
return { line: `~/${rel}: ${state === 'absent' ? 'written' : `replaced (was ${state})`}`, write: () => write(path, rendered) };
|
|
150
176
|
});
|
|
151
177
|
}
|
|
152
|
-
const
|
|
153
|
-
|
|
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 }) {
|
|
154
187
|
const path = join(home, rel);
|
|
155
188
|
const label = `Block in ~/${rel}`;
|
|
156
189
|
const file = readInstalled(path);
|
|
157
190
|
const text = file ?? '';
|
|
158
191
|
const region = findRegion(text, `~/${rel}`);
|
|
159
|
-
const
|
|
192
|
+
const target = rendered(want === 'write' ? vaultRepo : region?.vaultRepo);
|
|
193
|
+
const state = hashState(region, target.text);
|
|
160
194
|
if (region && state === 'hand-edited' && !confirm) {
|
|
161
195
|
return {
|
|
162
196
|
line: `${label}: hand-edited, left alone`,
|
|
@@ -166,7 +200,7 @@ function blockStep({ home, rel, want, confirm, rerun, suffix = '' }) {
|
|
|
166
200
|
rerun,
|
|
167
201
|
'',
|
|
168
202
|
'````diff',
|
|
169
|
-
lineDiff(region.body,
|
|
203
|
+
lineDiff(region.body, target.body),
|
|
170
204
|
'````',
|
|
171
205
|
].join('\n'),
|
|
172
206
|
};
|
|
@@ -180,11 +214,11 @@ function blockStep({ home, rel, want, confirm, rerun, suffix = '' }) {
|
|
|
180
214
|
return { line: `${label}: current` };
|
|
181
215
|
if (!region) {
|
|
182
216
|
const before = text === '' || text.endsWith('\n') ? text : `${text}\n`;
|
|
183
|
-
return { line: `${label}: written`, write: () => write(path, before + renderBlock()) };
|
|
217
|
+
return { line: `${label}: written`, write: () => write(path, before + renderBlock(vaultRepo)) };
|
|
184
218
|
}
|
|
185
219
|
return {
|
|
186
220
|
line: `${label}: replaced (was ${state})`,
|
|
187
|
-
write: () => write(path, text.slice(0, region.start) +
|
|
221
|
+
write: () => write(path, text.slice(0, region.start) + target.text + text.slice(region.end)),
|
|
188
222
|
};
|
|
189
223
|
}
|
|
190
224
|
/** Codex reads an `AGENTS.override.md` in place of `AGENTS.md` when one
|
|
@@ -207,14 +241,21 @@ function run({ header, steps, tail = [], unchanged, listAll }) {
|
|
|
207
241
|
export function installState(home, harness) {
|
|
208
242
|
const { instructions } = HARNESSES[harness];
|
|
209
243
|
let block = null;
|
|
244
|
+
let legacy = null;
|
|
245
|
+
let vaultRepo;
|
|
210
246
|
if (instructions !== null) {
|
|
211
247
|
const rel = instructionsFile(home, instructions);
|
|
248
|
+
const text = readInstalled(join(home, rel)) ?? '';
|
|
249
|
+
let region;
|
|
212
250
|
try {
|
|
213
|
-
|
|
251
|
+
region = findRegion(text, `~/${rel}`);
|
|
252
|
+
vaultRepo = region?.vaultRepo;
|
|
253
|
+
block = hashState(region, rendered(vaultRepo).text);
|
|
214
254
|
}
|
|
215
255
|
catch {
|
|
216
256
|
block = 'broken';
|
|
217
257
|
}
|
|
258
|
+
legacy = findLegacy(text, region);
|
|
218
259
|
}
|
|
219
260
|
const skills = SKILL_DIRS.flatMap(dir => SKILLS.map(({ name, render }) => {
|
|
220
261
|
const text = readInstalled(join(home, dir, name, 'SKILL.md'));
|
|
@@ -224,15 +265,18 @@ export function installState(home, harness) {
|
|
|
224
265
|
const states = new Set(skills.map(s => s.state));
|
|
225
266
|
const [only] = states;
|
|
226
267
|
const merged = states.size === 1 ? only : states.has('hand-edited') ? 'hand-edited' : 'stale';
|
|
227
|
-
return { block, skills: merged, onboarded: skills.some(s => s.hashed) };
|
|
268
|
+
return { block, skills: merged, onboarded: skills.some(s => s.hashed), legacy, ...(vaultRepo !== undefined && { vaultRepo }) };
|
|
228
269
|
}
|
|
229
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). */
|
|
230
273
|
export function install(opts) {
|
|
231
|
-
const { home, harness, mode, confirm } = opts;
|
|
274
|
+
const { home, harness, mode, confirm, vaultRepo } = opts;
|
|
232
275
|
const { label, instructions } = HARNESSES[harness];
|
|
233
276
|
const steps = skillSteps(home, 'write');
|
|
234
277
|
const tail = [];
|
|
235
|
-
|
|
278
|
+
const rel = instructions === null ? null : instructionsFile(home, instructions);
|
|
279
|
+
if (rel === null) {
|
|
236
280
|
if (mode === 'always-on') {
|
|
237
281
|
steps.push({ line: `Block: printed below, for ${label}'s User Rules` });
|
|
238
282
|
tail.push(`${label} keeps its User Rules in Settings, not a file, so the Block was not written. Show the user the Block below and ask them to paste it into ${label} Settings → Rules → User Rules, marker lines included.`, `\`\`\`\`\n${renderBlock()}\`\`\`\``);
|
|
@@ -244,24 +288,33 @@ export function install(opts) {
|
|
|
244
288
|
else {
|
|
245
289
|
steps.push(blockStep({
|
|
246
290
|
home,
|
|
247
|
-
rel
|
|
291
|
+
rel,
|
|
248
292
|
want: mode === 'always-on' ? 'write' : 'remove',
|
|
249
293
|
confirm,
|
|
250
|
-
rerun: `hafez onboard --install --harness ${harness} --mode ${mode} --confirm`,
|
|
294
|
+
rerun: `hafez onboard --install --harness ${harness} --mode ${mode}${vaultRepoFlag(vaultRepo)} --confirm`,
|
|
251
295
|
suffix: mode === 'invoke-only' ? ' (invoke-only)' : '',
|
|
296
|
+
vaultRepo,
|
|
252
297
|
}));
|
|
253
298
|
}
|
|
254
|
-
|
|
299
|
+
const out = run({ header: `${label}, ${mode}:`, steps, tail, unchanged: 'Nothing changed: the install is current.', listAll: true });
|
|
300
|
+
if (rel === null)
|
|
301
|
+
return out;
|
|
302
|
+
// A 1.x section loads beside the Block, or in place of invoke-only: named,
|
|
303
|
+
// never touched (#162). Read after the writes, so its line is where it is now.
|
|
304
|
+
const text = readInstalled(join(home, rel)) ?? '';
|
|
305
|
+
const line = findLegacy(text, findRegion(text, `~/${rel}`));
|
|
306
|
+
return line === null ? out : `${out}\n\n${removeLegacy(`~/${rel}`, line)}`;
|
|
255
307
|
}
|
|
256
308
|
/** Both skills in both directories, and the Block in the instructions file
|
|
257
309
|
* each Harness reads — the one `--install` would write. Only Blocks that are
|
|
258
|
-
* there are listed: none means invoke-only. */
|
|
310
|
+
* there are listed: none, and no 1.x section, means invoke-only. */
|
|
259
311
|
export function checkInstall(home) {
|
|
260
312
|
const skills = skillFiles(home).map(({ rel, render, text }) => ({
|
|
261
313
|
path: `~/${rel}`,
|
|
262
314
|
state: hashState(text === null ? null : parseSkill(text), render()),
|
|
263
315
|
}));
|
|
264
316
|
const blocks = [];
|
|
317
|
+
const legacy = [];
|
|
265
318
|
for (const [harness, { instructions }] of Object.entries(HARNESSES)) {
|
|
266
319
|
if (instructions === null)
|
|
267
320
|
continue;
|
|
@@ -276,14 +329,18 @@ export function checkInstall(home) {
|
|
|
276
329
|
}
|
|
277
330
|
catch {
|
|
278
331
|
blocks.push({ harness, path, state: 'broken', renderChanged: false });
|
|
279
|
-
continue;
|
|
280
332
|
}
|
|
333
|
+
const line = findLegacy(text, region);
|
|
334
|
+
if (line !== null)
|
|
335
|
+
legacy.push({ harness, path, line });
|
|
281
336
|
if (!region)
|
|
282
337
|
continue;
|
|
283
|
-
const
|
|
284
|
-
|
|
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 }) });
|
|
285
342
|
}
|
|
286
|
-
return { skills, blocks };
|
|
343
|
+
return { skills, blocks, legacy };
|
|
287
344
|
}
|
|
288
345
|
/** Remove everything Hafez wrote: both skills from both directories, and the
|
|
289
346
|
* Block region from every instructions file a Harness reads. Nothing else. */
|
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, remove } 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')})`,
|
|
@@ -159,6 +180,9 @@ export function renderOnboard({ home, harness, vault }) {
|
|
|
159
180
|
...[{ ...HARNESSES[harness], state }, ...others]
|
|
160
181
|
.filter(h => h.state.block === 'broken')
|
|
161
182
|
.map(h => fixBrokenMarkers(`~/${instructionsFile(home, h.instructions)}`, 'hafez onboard')),
|
|
183
|
+
...[{ ...HARNESSES[harness], state }, ...others]
|
|
184
|
+
.filter(h => h.state.legacy !== null)
|
|
185
|
+
.map(h => `${legacyLine(`~/${instructionsFile(home, h.instructions)}`, h.state.legacy)}, from Hafez 1.x, with no markers.`),
|
|
162
186
|
];
|
|
163
187
|
// With the install current there is no install step: switching the Mode is
|
|
164
188
|
// walked here, and re-running `onboard` afterwards walks what it changed.
|
|
@@ -187,9 +211,11 @@ export function renderOnboard({ home, harness, vault }) {
|
|
|
187
211
|
steps.push({
|
|
188
212
|
title: 'Install',
|
|
189
213
|
body: [
|
|
214
|
+
// Either install leaves the 1.x section in place, beside the Block or alone.
|
|
215
|
+
...(state.legacy === null ? [] : [`${removeLegacy(instructionsRel, state.legacy)} Do this before either install below.`, '']),
|
|
190
216
|
`Always-on: ${showBlock}`,
|
|
191
217
|
'',
|
|
192
|
-
|
|
218
|
+
`hafez onboard --install --mode always-on${vaultRepoFlag(state.vaultRepo)}`,
|
|
193
219
|
'',
|
|
194
220
|
'Invoke-only: no file of theirs is edited. Run:',
|
|
195
221
|
'',
|
|
@@ -201,6 +227,9 @@ export function renderOnboard({ home, harness, vault }) {
|
|
|
201
227
|
].join('\n'),
|
|
202
228
|
});
|
|
203
229
|
}
|
|
230
|
+
if (installCurrent && state.legacy !== null) {
|
|
231
|
+
steps.push({ title: 'Remove the Hafez 1.x section', body: removeLegacy(instructionsRel, state.legacy) });
|
|
232
|
+
}
|
|
204
233
|
if (!config.allowlist) {
|
|
205
234
|
steps.push({ title: 'Allow the command', body: ITEMS.harnessPermissions.render({ harness, vault }) });
|
|
206
235
|
}
|
package/dist/cli/upgrade.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BlockCheck, type SkillCheck } from './install.js';
|
|
1
|
+
import { type BlockCheck, type LegacyCheck, type SkillCheck } from './install.js';
|
|
2
2
|
export type Comparison = 'behind' | 'equal' | 'ahead' | 'unknown';
|
|
3
3
|
/** `command` entries are directly executable (after user approval); `action`
|
|
4
4
|
* entries are prose, so an agent that shells out every entry does not try to
|
|
@@ -21,11 +21,13 @@ export interface UpgradeState {
|
|
|
21
21
|
alternativeInstall: string | null;
|
|
22
22
|
steps: UpgradeStep[];
|
|
23
23
|
}
|
|
24
|
-
/** Mode is not stored: a Block present means always-on.
|
|
24
|
+
/** Mode is not stored: a Block present means always-on, and so does a 1.x
|
|
25
|
+
* section with no markers (`legacy`). */
|
|
25
26
|
export interface Integration {
|
|
26
27
|
mode: 'always-on' | 'invoke-only';
|
|
27
28
|
blocks: BlockCheck[];
|
|
28
29
|
skills: SkillCheck[];
|
|
30
|
+
legacy: LegacyCheck[];
|
|
29
31
|
}
|
|
30
32
|
export interface UpgradeDeps {
|
|
31
33
|
fetch: typeof globalThis.fetch;
|
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 } 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;
|
|
@@ -131,8 +131,8 @@ function devCheckoutStep(packageRoot) {
|
|
|
131
131
|
}
|
|
132
132
|
function readIntegration(home) {
|
|
133
133
|
try {
|
|
134
|
-
const { skills, blocks } = checkInstall(home);
|
|
135
|
-
return { mode: blocks.length > 0 ? 'always-on' : 'invoke-only', blocks, skills };
|
|
134
|
+
const { skills, blocks, legacy } = checkInstall(home);
|
|
135
|
+
return { mode: blocks.length > 0 || legacy.length > 0 ? 'always-on' : 'invoke-only', blocks, skills, legacy };
|
|
136
136
|
}
|
|
137
137
|
catch {
|
|
138
138
|
return null;
|
|
@@ -155,7 +155,18 @@ function integrationSteps(integration, upgrading, env) {
|
|
|
155
155
|
text: fixBrokenMarkers(b.path, 'hafez upgrade'),
|
|
156
156
|
}));
|
|
157
157
|
}
|
|
158
|
-
const { blocks, skills } = integration;
|
|
158
|
+
const { blocks, skills, legacy } = integration;
|
|
159
|
+
// The 1.x section goes first: the always-on install writes a Block beside
|
|
160
|
+
// it, and both would be loaded.
|
|
161
|
+
const removals = legacy.map(l => ({ kind: 'action', text: removeLegacy(l.path, l.line) }));
|
|
162
|
+
const unblocked = legacy
|
|
163
|
+
.filter(l => !blocks.some(b => b.harness === l.harness))
|
|
164
|
+
.map(l => ({ kind: 'command', text: `hafez onboard --install --mode always-on --harness ${l.harness}` }));
|
|
165
|
+
// With no Block, the always-on install refreshes the skills too.
|
|
166
|
+
const rest = blocks.length === 0 && legacy.length > 0 ? [] : blockSteps(integration, upgrading, env);
|
|
167
|
+
return [...removals, ...unblocked, ...rest];
|
|
168
|
+
}
|
|
169
|
+
function blockSteps({ blocks, skills }, upgrading, env) {
|
|
159
170
|
const skillDrift = skills.some(s => s.state === 'stale' || s.state === 'hand-edited');
|
|
160
171
|
const blockDrift = (b) => b.state === 'stale' || (b.state === 'hand-edited' && b.renderChanged);
|
|
161
172
|
const installed = blocks.length > 0 || skills.some(s => s.state !== 'absent');
|
|
@@ -168,7 +179,7 @@ function integrationSteps(integration, upgrading, env) {
|
|
|
168
179
|
const targets = upgrading ? blocks : blocks.filter(blockDrift);
|
|
169
180
|
return (targets.length > 0 ? targets : skillsOnly).map(b => ({
|
|
170
181
|
kind: 'command',
|
|
171
|
-
text: `hafez onboard --install --mode always-on --harness ${b.harness}`,
|
|
182
|
+
text: `hafez onboard --install --mode always-on --harness ${b.harness}${vaultRepoFlag(b.vaultRepo)}`,
|
|
172
183
|
}));
|
|
173
184
|
}
|
|
174
185
|
const harness = detectAgent(env);
|
|
@@ -181,7 +192,8 @@ function integrationSteps(integration, upgrading, env) {
|
|
|
181
192
|
}
|
|
182
193
|
/** The action list, in execution order. Every command is built from static
|
|
183
194
|
* constants; the only interpolated values are the regex-validated version,
|
|
184
|
-
* 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`. */
|
|
185
197
|
function deriveSteps(state, ctx) {
|
|
186
198
|
const steps = [];
|
|
187
199
|
let alternativeInstall = null;
|
|
@@ -263,13 +275,15 @@ function integrationLines(integration) {
|
|
|
263
275
|
if (integration === null)
|
|
264
276
|
return ['Skills and Block: could not be read.'];
|
|
265
277
|
const lines = integration.mode === 'always-on'
|
|
266
|
-
? ['Mode: always-on (a Block is installed).']
|
|
278
|
+
? [integration.blocks.length > 0 ? 'Mode: always-on (a Block is installed).' : 'Mode: always-on (legacy: a Hafez 1.x section with no markers).']
|
|
267
279
|
: [
|
|
268
280
|
'Mode: invoke-only (no Block in any instructions file Hafez writes); installing a Block is not offered.',
|
|
269
281
|
"A Block pasted into Cursor's User Rules lives in its Settings and is not checked here.",
|
|
270
282
|
];
|
|
271
283
|
for (const b of integration.blocks)
|
|
272
284
|
lines.push(`- Block in ${b.path}: ${BLOCK_STATE[b.state](b)}`);
|
|
285
|
+
for (const l of integration.legacy)
|
|
286
|
+
lines.push(`- ${legacyLine(l.path, l.line)}, from Hafez 1.x, with no markers`);
|
|
273
287
|
for (const s of integration.skills) {
|
|
274
288
|
const lost = s.state === 'hand-edited' ? ' — `onboard --install` replaces it, and the edit is lost' : '';
|
|
275
289
|
lines.push(`- ${s.path}: ${s.state}${lost}`);
|
package/dist/contracts.js
CHANGED
|
@@ -195,6 +195,8 @@ export const WRITE_LINE_OMITS = {
|
|
|
195
195
|
status: 'a lifecycle change, not a write-back: the `hafez` skill\'s rare ops carry the status line',
|
|
196
196
|
agent: 'detected from the Harness; the override is never shown',
|
|
197
197
|
add_action: 'the one-line form of add_actions, written by the same flag',
|
|
198
|
+
complete_actions: 'the repeated form of complete_action, written by the same flag',
|
|
199
|
+
remove_actions: 'the repeated form of remove_action, written by the same flag',
|
|
198
200
|
clear_actions: 'drops lines unseen; the Footer drops one line by its Handle',
|
|
199
201
|
description: SET_AT_CREATE,
|
|
200
202
|
resource: SET_AT_CREATE,
|
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 {};
|