hafez 1.4.0 → 2.0.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 +26 -33
- package/dist/apply.d.ts +5 -2
- package/dist/apply.js +225 -114
- package/dist/batch-ops.d.ts +89 -26
- package/dist/batch-ops.js +176 -18
- package/dist/cli/args.d.ts +43 -5
- package/dist/cli/args.js +69 -8
- package/dist/cli/batch-errors.js +19 -11
- package/dist/cli/block-hook.d.ts +9 -0
- package/dist/cli/block-hook.js +90 -0
- package/dist/cli/commands-schema.js +2 -0
- package/dist/cli/commands.d.ts +4 -3
- package/dist/cli/commands.js +300 -99
- package/dist/cli/format.d.ts +7 -3
- package/dist/cli/format.js +27 -10
- package/dist/cli/index.d.ts +18 -4
- package/dist/cli/index.js +72 -30
- package/dist/cli/install.d.ts +53 -0
- package/dist/cli/install.js +297 -0
- package/dist/cli/onboard.d.ts +15 -2
- package/dist/cli/onboard.js +265 -90
- package/dist/cli/resolve-vault.d.ts +11 -4
- package/dist/cli/resolve-vault.js +13 -11
- package/dist/cli/schema-introspect.js +4 -3
- package/dist/cli/upgrade.d.ts +13 -4
- package/dist/cli/upgrade.js +90 -44
- package/dist/closest.d.ts +8 -0
- package/dist/closest.js +65 -0
- package/dist/contracts.d.ts +17 -1
- package/dist/contracts.js +44 -0
- package/dist/db.d.ts +15 -2
- package/dist/db.js +69 -48
- package/dist/digest.d.ts +6 -4
- package/dist/digest.js +28 -17
- package/dist/document.d.ts +41 -3
- package/dist/document.js +218 -62
- package/dist/export-okf.d.ts +13 -0
- package/dist/export-okf.js +11 -7
- package/dist/harness.d.ts +58 -0
- package/dist/harness.js +50 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +94 -104
- package/dist/merge.js +7 -5
- package/dist/near-match.d.ts +9 -0
- package/dist/near-match.js +34 -0
- package/dist/stale.d.ts +41 -0
- package/dist/stale.js +43 -0
- package/dist/stopwords.d.ts +6 -0
- package/dist/stopwords.js +25 -0
- package/dist/surfaces/items.d.ts +366 -0
- package/dist/surfaces/items.js +523 -0
- package/dist/surfaces/surfaces.d.ts +32 -0
- package/dist/surfaces/surfaces.js +230 -0
- package/dist/types.d.ts +45 -16
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +109 -0
- package/dist/vault.js +1 -1
- package/package.json +4 -3
- package/skill/hafez/SKILL.md +68 -0
- package/skill/hafez-save/SKILL.md +29 -0
- package/dist/cli/help-agent.d.ts +0 -1
- package/dist/cli/help-agent.js +0 -210
- package/skill/SKILL.md +0 -137
package/dist/cli/onboard.js
CHANGED
|
@@ -1,94 +1,269 @@
|
|
|
1
1
|
// src/cli/onboard.ts
|
|
2
2
|
//
|
|
3
|
-
// `hafez onboard` —
|
|
4
|
-
//
|
|
5
|
-
// user
|
|
6
|
-
//
|
|
3
|
+
// `hafez onboard` — the user side of the Agent Contract (onboard decision
|
|
4
|
+
// #58). Bare, it prints where things stand, then the script the agent follows
|
|
5
|
+
// for its user: ask the Mode, install, allow the command, offer the hook and
|
|
6
|
+
// any other Harness found, seed an empty vault, hand over with the teaching
|
|
7
|
+
// lines. The files are the state: re-running walks only what is not current.
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
the
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
9
|
+
// `--install` and `--remove` write the files Hafez owns outside the vault
|
|
10
|
+
// (install.ts). The binary has no interactive prompts: the agent asks, then
|
|
11
|
+
// passes the answer as a flag. `--block` is the SessionStart hook's render
|
|
12
|
+
// (block-hook.ts) and writes nothing.
|
|
13
|
+
import { existsSync, readFileSync } from 'fs';
|
|
14
|
+
import { homedir } from 'os';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { UsageError, parseArgs, str } from './args.js';
|
|
17
|
+
import { HARNESSES, ONBOARD_HARNESSES, detectAgent, isHarness, isOnboardHarness } from '../harness.js';
|
|
18
|
+
import { MODES, SKILL_DIRS, fixBrokenMarkers, install, installState, instructionsFile, isMode, remove } from './install.js';
|
|
19
|
+
import { noVaultMessage, resolveVaultPath } from './resolve-vault.js';
|
|
20
|
+
import { HafezError } from '../types.js';
|
|
21
|
+
import { kindDir, scanVaultDir } from '../vault.js';
|
|
22
|
+
import { ITEMS } from '../surfaces/items.js';
|
|
23
|
+
import { renderBlock } from '../surfaces/surfaces.js';
|
|
24
|
+
import { blockHook } from './block-hook.js';
|
|
25
|
+
const HARNESS_IDS = Object.keys(HARNESSES).join('|');
|
|
26
|
+
const MODE_IDS = MODES.join('|');
|
|
27
|
+
const ONBOARD_USAGE = `Usage:
|
|
28
|
+
hafez onboard
|
|
29
|
+
hafez onboard --install --mode <${MODE_IDS}> [--harness <${HARNESS_IDS}>] [--confirm]
|
|
30
|
+
hafez onboard --remove [--confirm]
|
|
31
|
+
hafez onboard --block [--harness <${HARNESS_IDS}>]`;
|
|
32
|
+
/** `ctx` is the temp-home seam: tests pass a temporary home and environment;
|
|
33
|
+
* the install functions are given the home, never read it. `vaultFlag` is the
|
|
34
|
+
* global `--vault`; without it the bare form reports on the registered vault.
|
|
35
|
+
* `--block` returns a promise of its render, or of null when it must print nothing. */
|
|
36
|
+
export function cmdOnboard(args, ctx = {}) {
|
|
37
|
+
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);
|
|
39
|
+
const fail = (why) => {
|
|
40
|
+
throw new UsageError(`${why}\n${ONBOARD_USAGE}`);
|
|
41
|
+
};
|
|
42
|
+
if (positionals.length)
|
|
43
|
+
fail(`Unexpected argument: ${positionals[0]}`);
|
|
44
|
+
const mode = str(flags.mode);
|
|
45
|
+
const harnessFlag = str(flags.harness);
|
|
46
|
+
const confirm = flags.confirm === true;
|
|
47
|
+
if (flags.install && flags.remove)
|
|
48
|
+
fail('--install and --remove are separate commands');
|
|
49
|
+
if (flags.block) {
|
|
50
|
+
if (flags.install || flags.remove || mode !== undefined || confirm)
|
|
51
|
+
fail('--block takes only --harness: it prints the Block and writes nothing');
|
|
52
|
+
if (harnessFlag !== undefined && !isHarness(harnessFlag))
|
|
53
|
+
fail(`Unknown harness: ${harnessFlag}`);
|
|
54
|
+
return blockHook({ home, env, cwd, vaultFlag });
|
|
55
|
+
}
|
|
56
|
+
if (flags.remove) {
|
|
57
|
+
if (mode !== undefined || harnessFlag !== undefined)
|
|
58
|
+
fail('--remove takes no --mode or --harness: it removes everything Hafez wrote');
|
|
59
|
+
return remove({ home, confirm });
|
|
60
|
+
}
|
|
61
|
+
if (flags.install) {
|
|
62
|
+
if (mode === undefined)
|
|
63
|
+
return fail(`--install needs --mode <${MODE_IDS}>`);
|
|
64
|
+
if (!isMode(mode))
|
|
65
|
+
return fail(`Unknown mode: ${mode}`);
|
|
66
|
+
const harness = harnessFlag ?? detectAgent(env);
|
|
67
|
+
if (!isHarness(harness)) {
|
|
68
|
+
return fail(harnessFlag === undefined ? `No harness detected: pass --harness <${HARNESS_IDS}>` : `Unknown harness: ${harnessFlag}`);
|
|
69
|
+
}
|
|
70
|
+
return install({ home, harness, mode, confirm });
|
|
71
|
+
}
|
|
72
|
+
if (mode !== undefined || harnessFlag !== undefined || confirm)
|
|
73
|
+
fail('--mode, --harness and --confirm go with --install, --remove or --block');
|
|
74
|
+
const harness = detectAgent(env);
|
|
75
|
+
if (!isOnboardHarness(harness)) {
|
|
76
|
+
return fail('No harness detected: run `hafez onboard` from inside Claude Code or Codex. Another harness: `hafez onboard --install --harness <h>`');
|
|
77
|
+
}
|
|
78
|
+
return renderOnboard({ home, harness, vault: resolvedVault(vaultFlag) });
|
|
79
|
+
}
|
|
80
|
+
function resolvedVault(flag) {
|
|
81
|
+
const resolution = resolveVaultPath(flag);
|
|
82
|
+
if (resolution === null)
|
|
83
|
+
throw new HafezError('NOT_FOUND', noVaultMessage());
|
|
84
|
+
if (typeof resolution === 'object')
|
|
85
|
+
throw new HafezError('NOT_FOUND', resolution.error);
|
|
86
|
+
return resolution;
|
|
87
|
+
}
|
|
88
|
+
// --- Bare onboard: status, then the script ---
|
|
89
|
+
const read = (home, rel) => {
|
|
90
|
+
try {
|
|
91
|
+
return readFileSync(join(home, rel), 'utf-8');
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/** Whether the SessionStart hook and the permission entries are already in
|
|
98
|
+
* the Harness's config: present when found, absent otherwise (an unreadable
|
|
99
|
+
* file included). Claude Code's settings are JSON; Codex's rule and TOML are
|
|
100
|
+
* matched as text. */
|
|
101
|
+
function harnessConfig(home, harness, vault) {
|
|
102
|
+
const isHook = (command) => typeof command === 'string' && command.includes('hafez onboard --block');
|
|
103
|
+
if (harness === 'claude-code') {
|
|
104
|
+
const { hooks, allowEntry } = ONBOARD_HARNESSES[harness];
|
|
105
|
+
let settings = null;
|
|
106
|
+
try {
|
|
107
|
+
settings = JSON.parse(read(home, hooks) ?? 'null');
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
settings = null;
|
|
111
|
+
}
|
|
112
|
+
const groups = Array.isArray(settings?.hooks?.SessionStart) ? settings.hooks.SessionStart : [];
|
|
113
|
+
const allow = Array.isArray(settings?.permissions?.allow) ? settings.permissions.allow : [];
|
|
114
|
+
return {
|
|
115
|
+
hook: groups.some((group) => Array.isArray(group?.hooks) && group.hooks.some((h) => isHook(h?.command))),
|
|
116
|
+
allowlist: allow.includes(allowEntry),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const { hooks, rules, config } = ONBOARD_HARNESSES[harness];
|
|
120
|
+
const toml = read(home, config) ?? '';
|
|
121
|
+
return {
|
|
122
|
+
hook: isHook(read(home, hooks)) || isHook(toml),
|
|
123
|
+
allowlist: /prefix_rule\(\s*pattern\s*=\s*\[\s*"hafez"\s*\]/.test(read(home, rules) ?? '') && toml.includes(JSON.stringify(vault)),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const hookEntry = (harness) => `{"matcher": "startup|compact", "hooks": [{"type": "command", "command": "hafez onboard --block --harness ${harness}"}]}`;
|
|
127
|
+
/** Mode is never stored: a Block present means always-on, skills `onboard`
|
|
128
|
+
* installed without one mean invoke-only. Neither is a first run, a setup from
|
|
129
|
+
* before `onboard` (a hand-pasted section, a skill with no hash) included. */
|
|
130
|
+
const modeOf = ({ block, onboarded }) => block !== null && block !== 'absent' ? 'always-on' : onboarded ? 'invoke-only' : null;
|
|
131
|
+
/** Current in its Mode: the skills current, and the Block too where there is
|
|
132
|
+
* one. A Harness with no Block file (Cursor) is current on its skills. */
|
|
133
|
+
const isCurrent = (state) => state.skills === 'current' && (state.block === null || state.block === 'current' || modeOf(state) === 'invoke-only');
|
|
134
|
+
const statusLine = (label, { block, skills }, extra = []) => `${label}: ${[...(block === null ? [] : [`Block ${block === 'broken' ? 'broken markers' : block}`]), `skills ${skills}`, ...extra].join(' | ')}`;
|
|
135
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
136
|
+
const listed = (names) => (names.length === 1 ? names[0] : `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`);
|
|
137
|
+
export function renderOnboard({ home, harness, vault }) {
|
|
138
|
+
const { label, instructions } = HARNESSES[harness];
|
|
139
|
+
const state = installState(home, harness);
|
|
140
|
+
const config = harnessConfig(home, harness, vault);
|
|
141
|
+
const others = Object.keys(HARNESSES)
|
|
142
|
+
.filter(id => id !== harness && existsSync(join(home, HARNESSES[id].dir)))
|
|
143
|
+
.map(id => ({ id, ...HARNESSES[id], state: installState(home, id) }));
|
|
144
|
+
const entities = scanVaultDir(join(vault, kindDir('entity'))).length;
|
|
145
|
+
const knowledge = scanVaultDir(join(vault, kindDir('knowledge'))).length;
|
|
146
|
+
const mode = modeOf(state);
|
|
147
|
+
const otherMode = mode === 'always-on' ? 'invoke-only' : 'always-on';
|
|
148
|
+
const installCurrent = isCurrent(state);
|
|
149
|
+
const teaching = (m) => ITEMS.teachingLines.render({ harness, mode: m });
|
|
150
|
+
const line2 = (m) => ITEMS.teachingLines.render({ harness, mode: m, line2Only: true });
|
|
151
|
+
const instructionsRel = `~/${instructionsFile(home, instructions)}`;
|
|
152
|
+
const block = `\`\`\`\`\n${renderBlock()}\`\`\`\``;
|
|
153
|
+
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
|
+
const status = [
|
|
155
|
+
`Vault: ${vault} (${plural(entities, 'entity', 'entities')}, ${plural(knowledge, 'knowledge note', 'knowledge notes')})`,
|
|
156
|
+
statusLine(`${label} (this harness)`, state, [`hook ${config.hook ? 'present' : 'absent'}`, `allowlist ${config.allowlist ? 'present' : 'absent'}`]),
|
|
157
|
+
...others.map(o => statusLine(`${o.label} (found at ~/${o.dir})`, o.state)),
|
|
158
|
+
// `--install` and `--remove` refuse broken markers: only a hand fix walks on.
|
|
159
|
+
...[{ ...HARNESSES[harness], state }, ...others]
|
|
160
|
+
.filter(h => h.state.block === 'broken')
|
|
161
|
+
.map(h => fixBrokenMarkers(`~/${instructionsFile(home, h.instructions)}`, 'hafez onboard')),
|
|
162
|
+
];
|
|
163
|
+
// With the install current there is no install step: switching the Mode is
|
|
164
|
+
// walked here, and re-running `onboard` afterwards walks what it changed.
|
|
165
|
+
const switchMode = !installCurrent || mode === null
|
|
166
|
+
? []
|
|
167
|
+
: otherMode === 'always-on'
|
|
168
|
+
? [`To switch to always-on, ${showBlock}`, '', 'hafez onboard --install --mode always-on', '', 'then `hafez onboard` again for the session-start hook.', '', block]
|
|
169
|
+
: [
|
|
170
|
+
'To switch to invoke-only, run:',
|
|
171
|
+
'',
|
|
172
|
+
'hafez onboard --install --mode invoke-only',
|
|
173
|
+
...(config.hook ? ['', `and remove the hafez entry from hooks.SessionStart in ~/${ONBOARD_HARNESSES[harness].hooks}.`] : []),
|
|
174
|
+
];
|
|
175
|
+
const steps = [];
|
|
176
|
+
steps.push({
|
|
177
|
+
title: 'Ask how Hafez should run',
|
|
178
|
+
body: [
|
|
179
|
+
'Ask one question and recommend the first answer.',
|
|
180
|
+
'',
|
|
181
|
+
'- Always-on: you load the vault at the start of a session and write to it as work lands.',
|
|
182
|
+
'- Invoke-only: nothing happens unless the user asks. No load at start, no writes during the session.',
|
|
183
|
+
...(mode === null ? [] : ['', `It runs ${mode} now.${switchMode.length ? ` ${switchMode[0]}` : ''}`, ...switchMode.slice(1)]),
|
|
184
|
+
].join('\n'),
|
|
185
|
+
});
|
|
186
|
+
if (!installCurrent && state.block !== 'broken') {
|
|
187
|
+
steps.push({
|
|
188
|
+
title: 'Install',
|
|
189
|
+
body: [
|
|
190
|
+
`Always-on: ${showBlock}`,
|
|
191
|
+
'',
|
|
192
|
+
'hafez onboard --install --mode always-on',
|
|
193
|
+
'',
|
|
194
|
+
'Invoke-only: no file of theirs is edited. Run:',
|
|
195
|
+
'',
|
|
196
|
+
'hafez onboard --install --mode invoke-only',
|
|
197
|
+
'',
|
|
198
|
+
`Both install two skills, \`hafez\` and \`hafez-save\`, into ${SKILL_DIRS.map(d => `~/${d}`).join(' and ')}.`,
|
|
199
|
+
'',
|
|
200
|
+
block,
|
|
201
|
+
].join('\n'),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (!config.allowlist) {
|
|
205
|
+
steps.push({ title: 'Allow the command', body: ITEMS.harnessPermissions.render({ harness, vault }) });
|
|
206
|
+
}
|
|
207
|
+
if (!config.hook && mode !== 'invoke-only') {
|
|
208
|
+
steps.push({
|
|
209
|
+
title: 'Offer the session-start hook (always-on only)',
|
|
210
|
+
body: 'Ask: should the Block be delivered again at the start of every session and after the context is compacted? '
|
|
211
|
+
+ `If yes, show this entry, then add it to ~/${ONBOARD_HARNESSES[harness].hooks} under hooks.SessionStart:\n\n${hookEntry(harness)}`
|
|
212
|
+
// Live run (#101): Codex asks once whether to trust a new hook.
|
|
213
|
+
+ (harness === 'codex' ? '\n\nTell the user Codex will ask once, at the next session start, whether to trust this hook: the answer is yes.' : ''),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const offered = others.filter(o => !isCurrent(o.state) && o.state.block !== 'broken');
|
|
217
|
+
if (offered.length > 0) {
|
|
218
|
+
const one = offered.length === 1;
|
|
219
|
+
steps.push({
|
|
220
|
+
title: 'Offer the other harness',
|
|
221
|
+
body: [
|
|
222
|
+
`${listed(offered.map(o => o.label))} ${one ? 'is' : 'are'} installed too. Ask once whether to set ${one ? 'it' : 'them'} up the same way. If yes:`,
|
|
223
|
+
'',
|
|
224
|
+
// A first run cannot know the answer to step 1 yet; the agent carries it.
|
|
225
|
+
...offered.map(o => `hafez onboard --install --harness ${o.id} --mode ${mode ?? 'always-on'}`),
|
|
226
|
+
].join('\n'),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
steps.push(entities + knowledge === 0
|
|
230
|
+
? {
|
|
231
|
+
title: 'Seed the vault',
|
|
232
|
+
body: [
|
|
233
|
+
'An empty vault gives you nothing to load. Ask: "What are you working on right now?" Aim for three to seven pieces of work. For each, draw out one sentence of description, where things stand, and the next step, then write it:',
|
|
234
|
+
'',
|
|
235
|
+
ITEMS.createLines.render('<name>'),
|
|
236
|
+
'',
|
|
237
|
+
'Anything vague or someday: `hafez capture "<name>"`. Read a README or notes only if the user points you at them. Do not ask for approval before writing. When done, show them `hafez stats`.',
|
|
238
|
+
].join('\n'),
|
|
239
|
+
}
|
|
240
|
+
: { title: 'Seed the vault (skipped: the vault is not empty)' });
|
|
241
|
+
// On a re-run only line 2's subject can have changed: the Mode, if the
|
|
242
|
+
// user switches it in step 1.
|
|
243
|
+
steps.push({
|
|
244
|
+
title: 'Hand over',
|
|
245
|
+
body: mode === null
|
|
246
|
+
? [
|
|
247
|
+
'Say these to the user in your own words, then give three example asks drawn from their vault, each naming an entity or note in it.',
|
|
248
|
+
'',
|
|
249
|
+
'Always-on:',
|
|
250
|
+
'',
|
|
251
|
+
teaching('always-on'),
|
|
252
|
+
'',
|
|
253
|
+
'Invoke-only, line 2:',
|
|
254
|
+
'',
|
|
255
|
+
line2('invoke-only'),
|
|
256
|
+
].join('\n')
|
|
257
|
+
: `The user has heard the teaching lines before. Only if they switched to ${otherMode} in step 1, tell them in your own words:\n\n${line2(otherMode)}`,
|
|
258
|
+
});
|
|
259
|
+
return [
|
|
260
|
+
'# Hafez setup',
|
|
261
|
+
'',
|
|
262
|
+
'You are an agent reading this for your user. Talk them through it; do not paste this to them.',
|
|
263
|
+
'',
|
|
264
|
+
'## Where things stand',
|
|
265
|
+
'',
|
|
266
|
+
...status,
|
|
267
|
+
...steps.flatMap(({ title, body }, i) => ['', `## ${i + 1}. ${title}`, ...(body === undefined ? [] : ['', body])]),
|
|
268
|
+
].join('\n');
|
|
94
269
|
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
1
|
+
/** Where the config is looked for. The environment and home are inputs so the
|
|
2
|
+
* `onboard --block` hook runs against a temporary home in tests. */
|
|
3
|
+
export interface ConfigHome {
|
|
4
|
+
env: Record<string, string | undefined>;
|
|
5
|
+
home: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function getConfigDir({ env, home }?: ConfigHome): string;
|
|
8
|
+
export declare function getConfigPath(at?: ConfigHome): string;
|
|
9
|
+
export declare function readVaultConfig(at?: ConfigHome): string | null;
|
|
4
10
|
export declare function saveVaultConfig(vaultPath: string): void;
|
|
11
|
+
export declare function isVault(dirPath: string): boolean;
|
|
5
12
|
/**
|
|
6
13
|
* Resolve vault path. Returns string or null.
|
|
7
14
|
* If config exists but points to a bad path, returns an error object instead.
|
|
8
15
|
*/
|
|
9
|
-
export declare function resolveVaultPath(flagValue?: string): string | null | {
|
|
16
|
+
export declare function resolveVaultPath(flagValue?: string, at?: ConfigHome): string | null | {
|
|
10
17
|
error: string;
|
|
11
18
|
};
|
|
12
19
|
export declare function noVaultMessage(): string;
|
|
@@ -4,15 +4,16 @@ import { join, resolve } from 'path';
|
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
6
|
import { kindDir } from '../vault.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { helpIfAsked } from './args.js';
|
|
8
|
+
const processHome = () => ({ env: process.env, home: homedir() });
|
|
9
|
+
export function getConfigDir({ env, home } = processHome()) {
|
|
10
|
+
return join(env.XDG_CONFIG_HOME || join(home, '.config'), 'hafez');
|
|
10
11
|
}
|
|
11
|
-
export function getConfigPath() {
|
|
12
|
-
return join(getConfigDir(), 'vault');
|
|
12
|
+
export function getConfigPath(at = processHome()) {
|
|
13
|
+
return join(getConfigDir(at), 'vault');
|
|
13
14
|
}
|
|
14
|
-
export function readVaultConfig() {
|
|
15
|
-
const configPath = getConfigPath();
|
|
15
|
+
export function readVaultConfig(at = processHome()) {
|
|
16
|
+
const configPath = getConfigPath(at);
|
|
16
17
|
if (!existsSync(configPath))
|
|
17
18
|
return null;
|
|
18
19
|
try {
|
|
@@ -28,25 +29,25 @@ export function saveVaultConfig(vaultPath) {
|
|
|
28
29
|
mkdirSync(dir, { recursive: true });
|
|
29
30
|
writeFileSync(join(dir, 'vault'), resolve(vaultPath) + '\n');
|
|
30
31
|
}
|
|
31
|
-
function isVault(dirPath) {
|
|
32
|
+
export function isVault(dirPath) {
|
|
32
33
|
return existsSync(dirPath) && statSync(dirPath).isDirectory() && existsSync(join(dirPath, kindDir('entity')));
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
36
|
* Resolve vault path. Returns string or null.
|
|
36
37
|
* If config exists but points to a bad path, returns an error object instead.
|
|
37
38
|
*/
|
|
38
|
-
export function resolveVaultPath(flagValue) {
|
|
39
|
+
export function resolveVaultPath(flagValue, at = processHome()) {
|
|
39
40
|
// 1. Explicit flag
|
|
40
41
|
if (flagValue)
|
|
41
42
|
return flagValue;
|
|
42
43
|
// 2. Config file
|
|
43
|
-
const configValue = readVaultConfig();
|
|
44
|
+
const configValue = readVaultConfig(at);
|
|
44
45
|
if (configValue) {
|
|
45
46
|
if (isVault(configValue))
|
|
46
47
|
return configValue;
|
|
47
48
|
// Config exists but points to bad path — loud failure
|
|
48
49
|
return {
|
|
49
|
-
error: `Config (${getConfigPath()}) points to ${configValue} but it doesn't look like a vault.\nRun 'hafez init --register /correct/path' to fix.`,
|
|
50
|
+
error: `Config (${getConfigPath(at)}) points to ${configValue} but it doesn't look like a vault.\nRun 'hafez init --register /correct/path' to fix.`,
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
return null;
|
|
@@ -65,6 +66,7 @@ Get started:
|
|
|
65
66
|
`;
|
|
66
67
|
}
|
|
67
68
|
export function cmdInit(args, flagValue) {
|
|
69
|
+
helpIfAsked(args, 'Usage: hafez init [--register <path>]');
|
|
68
70
|
const registerIdx = args.indexOf('--register');
|
|
69
71
|
if (registerIdx !== -1) {
|
|
70
72
|
// hafez init --register <path>
|
|
@@ -47,8 +47,10 @@ function walk(node) {
|
|
|
47
47
|
return inner;
|
|
48
48
|
}
|
|
49
49
|
if (type === 'pipe' || type === 'effects' || type === 'transform' || type === 'readonly') {
|
|
50
|
-
// Zod effects / transforms / pipes — descend to the inner schema.
|
|
51
|
-
|
|
50
|
+
// Zod effects / transforms / pipes — descend to the inner schema. A
|
|
51
|
+
// z.preprocess is a pipe whose input is the bare transform: its shape is
|
|
52
|
+
// the output side.
|
|
53
|
+
const inner = def.in?._def?.type === 'transform' ? def.out : def.in ?? def.schema ?? def.innerType;
|
|
52
54
|
if (inner)
|
|
53
55
|
return walk(inner);
|
|
54
56
|
}
|
|
@@ -153,7 +155,6 @@ export const DIGEST_EXAMPLE = {
|
|
|
153
155
|
decisions: ['Decision made'],
|
|
154
156
|
narrative: 'One to three sentence cross-cutting summary.',
|
|
155
157
|
session_date: '2026-01-15',
|
|
156
|
-
agent: 'claude',
|
|
157
158
|
};
|
|
158
159
|
// Descriptions live in the Op Spec table (src/batch-ops.ts) — one row per op.
|
|
159
160
|
const OP_DESCRIPTIONS = Object.fromEntries(OP_SPECS.map(s => [s.name, s.description]));
|
package/dist/cli/upgrade.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type BlockCheck, type SkillCheck } from './install.js';
|
|
1
2
|
export type Comparison = 'behind' | 'equal' | 'ahead' | 'unknown';
|
|
2
3
|
/** `command` entries are directly executable (after user approval); `action`
|
|
3
4
|
* entries are prose, so an agent that shells out every entry does not try to
|
|
@@ -13,19 +14,27 @@ export interface UpgradeState {
|
|
|
13
14
|
registryReachable: boolean;
|
|
14
15
|
npmMajor: number | null;
|
|
15
16
|
install: 'registry' | 'dev-checkout';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
skillTarget: string;
|
|
17
|
+
/** Null when the installed files could not be read. */
|
|
18
|
+
integration: Integration | null;
|
|
19
19
|
/** Mutually exclusive with the registry install in `steps` — exposed
|
|
20
20
|
* separately so an agent iterating `steps` cannot run both. */
|
|
21
21
|
alternativeInstall: string | null;
|
|
22
22
|
steps: UpgradeStep[];
|
|
23
23
|
}
|
|
24
|
+
/** Mode is not stored: a Block present means always-on. */
|
|
25
|
+
export interface Integration {
|
|
26
|
+
mode: 'always-on' | 'invoke-only';
|
|
27
|
+
blocks: BlockCheck[];
|
|
28
|
+
skills: SkillCheck[];
|
|
29
|
+
}
|
|
24
30
|
export interface UpgradeDeps {
|
|
25
31
|
fetch: typeof globalThis.fetch;
|
|
26
32
|
registryUrl: string;
|
|
27
33
|
packageRoot: string;
|
|
28
|
-
|
|
34
|
+
/** The temp-home seam, as onboard's. */
|
|
35
|
+
home: string;
|
|
36
|
+
/** Names the Harness for an invoke-only `onboard --install`. */
|
|
37
|
+
env: Record<string, string | undefined>;
|
|
29
38
|
npmVersion: () => string;
|
|
30
39
|
}
|
|
31
40
|
/** Three-way compare on the numeric segments. Hand-rolled string compare is
|