hafez 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -9
- package/dist/apply.d.ts +30 -0
- package/dist/apply.js +582 -0
- package/dist/batch-ops.d.ts +296 -13
- package/dist/batch-ops.js +186 -62
- package/dist/cli/args.d.ts +5 -1
- package/dist/cli/args.js +6 -2
- package/dist/cli/commands.d.ts +24 -0
- package/dist/cli/commands.js +197 -115
- package/dist/cli/help-agent.js +24 -0
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.js +150 -130
- package/dist/cli/upgrade.d.ts +42 -0
- package/dist/cli/upgrade.js +305 -0
- package/dist/contracts.d.ts +49 -6
- package/dist/contracts.js +104 -23
- package/dist/dates.d.ts +10 -0
- package/dist/dates.js +27 -0
- package/dist/db.d.ts +27 -2
- package/dist/db.js +309 -33
- package/dist/document.d.ts +97 -88
- package/dist/document.js +512 -382
- package/dist/export-okf.js +41 -15
- package/dist/git.js +165 -48
- package/dist/index.d.ts +2 -2
- package/dist/index.js +212 -727
- package/dist/knowledge-index.d.ts +23 -1
- package/dist/knowledge-index.js +62 -6
- package/dist/merge.d.ts +1 -1
- package/dist/merge.js +96 -64
- package/dist/schema.js +4 -4
- package/dist/templates.d.ts +6 -0
- package/dist/templates.js +27 -27
- package/dist/types.d.ts +38 -11
- package/dist/vault.d.ts +18 -0
- package/dist/vault.js +50 -4
- package/package.json +1 -1
- package/skill/SKILL.md +51 -110
- package/dist/query.d.ts +0 -24
- package/dist/query.js +0 -169
package/dist/cli/index.js
CHANGED
|
@@ -1,45 +1,67 @@
|
|
|
1
1
|
// src/cli/index.ts
|
|
2
|
+
//
|
|
3
|
+
// Import discipline: this module is loaded for EVERY invocation, so it may
|
|
4
|
+
// only import what every invocation needs — node builtins, the flag parser,
|
|
5
|
+
// and the error type. Everything else (the core factory and its native SQLite
|
|
6
|
+
// binding, zod-backed schema introspection, the markdown formatters, even
|
|
7
|
+
// vault-path resolution) loads on demand from the command that needs it, so
|
|
8
|
+
// `hafez --version` / `help` / `init` no longer pay for the whole program.
|
|
2
9
|
import { readFileSync } from 'fs';
|
|
3
|
-
import { dirname, join
|
|
10
|
+
import { dirname, join } from 'path';
|
|
4
11
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import { createHafez } from '../index.js';
|
|
6
12
|
import { HafezError } from '../types.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const COMMANDS = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
13
|
+
import { UsageError } from './args.js';
|
|
14
|
+
/** Bind a vault command to its handler without importing the handler module
|
|
15
|
+
* until the command actually runs. */
|
|
16
|
+
function lazy(pick) {
|
|
17
|
+
return async (os, args, opts) => pick(await import('./commands.js'))(os, args, opts);
|
|
18
|
+
}
|
|
19
|
+
export const COMMANDS = {
|
|
20
|
+
// --- pre-vault ---
|
|
21
|
+
help: { phase: 'pre-vault', run: async (args) => (args[0] === '--agent' ? (await import('./help-agent.js')).renderAgentHelp() : USAGE) },
|
|
22
|
+
onboard: { phase: 'pre-vault', run: async () => (await import('./onboard.js')).renderOnboard() },
|
|
23
|
+
schema: { phase: 'pre-vault', run: async (args) => (await import('./commands-schema.js')).cmdSchema(null, args) },
|
|
24
|
+
version: { phase: 'pre-vault', run: () => versionLine() },
|
|
25
|
+
init: { phase: 'pre-vault', run: async (args, ctx) => { (await import('./resolve-vault.js')).cmdInit(args, ctx.vaultFlag); return null; } },
|
|
26
|
+
// Binds (args, ctx) explicitly: cmdUpgrade's third parameter is its
|
|
27
|
+
// dependency seam, and PreVaultRun's signature keeps dispatch from ever
|
|
28
|
+
// passing a third argument into it.
|
|
29
|
+
upgrade: { phase: 'pre-vault', run: async (args, ctx) => (await import('./upgrade.js')).cmdUpgrade(args, ctx) },
|
|
30
|
+
// --- vault ---
|
|
31
|
+
read: { phase: 'vault', run: lazy(m => m.cmdRead) },
|
|
32
|
+
sync: { phase: 'vault', run: lazy(m => m.cmdSync) },
|
|
33
|
+
query: { phase: 'vault', run: lazy(m => m.cmdQuery) },
|
|
34
|
+
search: { phase: 'vault', run: lazy(m => m.cmdSearch) },
|
|
35
|
+
create: { phase: 'vault', mutating: true, run: lazy(m => m.cmdCreate) },
|
|
36
|
+
capture: { phase: 'vault', mutating: true, run: lazy(m => m.cmdCapture) },
|
|
37
|
+
update: { phase: 'vault', mutating: true, run: lazy(m => m.cmdUpdate) },
|
|
38
|
+
link: { phase: 'vault', mutating: true, run: lazy(m => m.cmdLink) },
|
|
39
|
+
unlink: { phase: 'vault', mutating: true, run: lazy(m => m.cmdUnlink) },
|
|
40
|
+
promote: { phase: 'vault', mutating: true, run: lazy(m => m.cmdPromote) },
|
|
41
|
+
batch: { phase: 'vault', mutating: true, run: lazy(m => m.cmdBatch) },
|
|
42
|
+
validate: { phase: 'vault', run: lazy(m => m.cmdValidate) },
|
|
43
|
+
index: {
|
|
44
|
+
phase: 'vault',
|
|
45
|
+
run: async (os, args) => {
|
|
46
|
+
if (args[0] === 'rebuild')
|
|
47
|
+
return (await import('./commands.js')).cmdIndexRebuild(os);
|
|
48
|
+
throw new UsageError('Usage: hafez index rebuild');
|
|
49
|
+
},
|
|
31
50
|
},
|
|
32
|
-
stats: cmdStats,
|
|
33
|
-
changelog: cmdChangelog,
|
|
34
|
-
digest: cmdDigest,
|
|
51
|
+
stats: { phase: 'vault', run: lazy(m => m.cmdStats) },
|
|
52
|
+
changelog: { phase: 'vault', run: lazy(m => m.cmdChangelog) },
|
|
53
|
+
digest: { phase: 'vault', run: lazy(m => m.cmdDigest) },
|
|
54
|
+
export: { phase: 'vault', run: lazy(m => m.cmdExport) },
|
|
35
55
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const USAGE = `Usage: hafez <command> [args]
|
|
56
|
+
/** Exported so a guard test can assert every registered command is documented
|
|
57
|
+
* here — a registered-but-undocumented command is otherwise invisible. */
|
|
58
|
+
export const USAGE = `Usage: hafez <command> [args]
|
|
39
59
|
|
|
40
60
|
Commands:
|
|
41
61
|
read <slug> Read entity or knowledge note
|
|
42
62
|
query [--filter ...] ... Filter entities/knowledge
|
|
63
|
+
--kind all Both kinds; --filter matches entity status only,
|
|
64
|
+
so knowledge notes (which have none) always show
|
|
43
65
|
search <terms> Full-text search
|
|
44
66
|
create <kind> <name> Create entity or knowledge note
|
|
45
67
|
capture <name> [--notes] Quick inbox capture
|
|
@@ -69,6 +91,7 @@ Commands:
|
|
|
69
91
|
init --register <path> Register an existing vault
|
|
70
92
|
init Show current vault resolution
|
|
71
93
|
onboard Agent-directed first run: seed the vault, pick integration level
|
|
94
|
+
upgrade Check whether hafez is outdated and print the update steps
|
|
72
95
|
help --agent Full API reference for agents
|
|
73
96
|
|
|
74
97
|
Global flags:
|
|
@@ -80,7 +103,7 @@ Vault discovery: --vault flag > ~/.config/hafez/vault
|
|
|
80
103
|
`;
|
|
81
104
|
/** Exit codes, documented in `hafez help --agent`. 0 = success; 4 is the
|
|
82
105
|
* mutate-succeeded-sync-failed warning path; anything unmapped = 1. */
|
|
83
|
-
const EXIT_CODES = {
|
|
106
|
+
export const EXIT_CODES = {
|
|
84
107
|
NOT_FOUND: 1,
|
|
85
108
|
VALIDATION_FAILED: 2,
|
|
86
109
|
GIT_PUSH_FAILED: 3,
|
|
@@ -88,12 +111,21 @@ const EXIT_CODES = {
|
|
|
88
111
|
GIT_COMMIT_FAILED: 6,
|
|
89
112
|
VAULT_LOCKED: 7,
|
|
90
113
|
};
|
|
114
|
+
/** The pure half of the error → exit adapter: which code an error maps to.
|
|
115
|
+
* Exported so the exit-code contract is testable without spawning the CLI. */
|
|
116
|
+
export function exitCodeFor(err) {
|
|
117
|
+
if (err instanceof UsageError)
|
|
118
|
+
return 2;
|
|
119
|
+
if (err instanceof HafezError)
|
|
120
|
+
return EXIT_CODES[err.code] ?? 1;
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
91
123
|
/** The one error → exit adapter. Usage errors (bad flags/args) are distinct
|
|
92
124
|
* from vault errors and from unknown crashes. */
|
|
93
125
|
function report(err) {
|
|
94
126
|
if (err instanceof UsageError) {
|
|
95
127
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
96
|
-
process.exit(
|
|
128
|
+
process.exit(exitCodeFor(err));
|
|
97
129
|
}
|
|
98
130
|
if (err instanceof HafezError) {
|
|
99
131
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
@@ -101,17 +133,29 @@ function report(err) {
|
|
|
101
133
|
for (const detail of err.details)
|
|
102
134
|
process.stderr.write(` - ${detail}\n`);
|
|
103
135
|
}
|
|
104
|
-
process.exit(
|
|
136
|
+
process.exit(exitCodeFor(err));
|
|
105
137
|
}
|
|
106
138
|
failUnknown(err);
|
|
107
139
|
}
|
|
140
|
+
/** Does this error mean the native module failed to load/build?
|
|
141
|
+
*
|
|
142
|
+
* Matched on the MESSAGE only, and only on the messages a load/ABI failure
|
|
143
|
+
* actually produces. The old test also scanned the stack for "better-sqlite3",
|
|
144
|
+
* which every SqliteError's stack contains — so a plain query error ("near
|
|
145
|
+
* 'abc': syntax error") printed reinstall advice instead of itself. */
|
|
146
|
+
export function isBindingsFailure(err) {
|
|
147
|
+
const message = err?.message;
|
|
148
|
+
if (typeof message !== 'string')
|
|
149
|
+
return false;
|
|
150
|
+
return /Could not locate the bindings file|better_sqlite3\.node|was compiled against a different Node\.js version|NODE_MODULE_VERSION|invalid ELF header/i.test(message);
|
|
151
|
+
}
|
|
108
152
|
// Terminal handler for non-Hafez errors. Special-cases unbuilt better-sqlite3
|
|
109
153
|
// bindings: npm 11+ blocks install scripts by default, so `npm install -g
|
|
110
154
|
// hafez` reports success with the native module unbuilt and the first indexed
|
|
111
155
|
// command dies here — guide instead of stack-tracing.
|
|
112
156
|
function failUnknown(err) {
|
|
113
157
|
const e = err;
|
|
114
|
-
if (
|
|
158
|
+
if (isBindingsFailure(e)) {
|
|
115
159
|
process.stderr.write('Error: the SQLite bindings (better-sqlite3) are not built.\n' +
|
|
116
160
|
'npm 11+ blocks install scripts by default, so the install reports success\n' +
|
|
117
161
|
'while skipping the native build. Fix with:\n\n' +
|
|
@@ -121,108 +165,84 @@ function failUnknown(err) {
|
|
|
121
165
|
process.stderr.write(`Error: ${e.message}\n`);
|
|
122
166
|
process.exit(1);
|
|
123
167
|
}
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
168
|
+
function versionLine() {
|
|
169
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
170
|
+
const pkg = JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf-8'));
|
|
171
|
+
return `hafez ${pkg.version}`;
|
|
172
|
+
}
|
|
173
|
+
/** Extract the global flags (--vault <path>, --json) from anywhere in argv.
|
|
174
|
+
* Exported for the in-process dispatch tests. A flag sitting where the vault
|
|
175
|
+
* path should be means the value is missing — never consume it (the same
|
|
176
|
+
* value-position rule parseArgs applies to declared command flags). */
|
|
177
|
+
export function extractGlobalFlags(args) {
|
|
178
|
+
const rest = [];
|
|
179
|
+
let vaultFlag;
|
|
180
|
+
let json = false;
|
|
181
|
+
for (let i = 0; i < args.length; i++) {
|
|
182
|
+
const a = args[i];
|
|
183
|
+
if (a === '--vault') {
|
|
184
|
+
const v = args[i + 1];
|
|
185
|
+
if (v === undefined || v.startsWith('--'))
|
|
186
|
+
throw new UsageError('--vault requires a path value');
|
|
187
|
+
// The old one-shot extraction left a second --vault in the command args
|
|
188
|
+
// where the declared-flag parser rejected it — keep failing loudly
|
|
189
|
+
// rather than silently letting the last one win (a mutating command
|
|
190
|
+
// would write to a different vault than the first flag named).
|
|
191
|
+
if (vaultFlag !== undefined)
|
|
192
|
+
throw new UsageError('--vault specified more than once');
|
|
193
|
+
vaultFlag = v;
|
|
194
|
+
i++;
|
|
195
|
+
}
|
|
196
|
+
else if (a === '--json') {
|
|
197
|
+
json = true;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
rest.push(a);
|
|
201
|
+
}
|
|
147
202
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
203
|
+
return { rest, vaultFlag, json };
|
|
204
|
+
}
|
|
205
|
+
export async function main(argv) {
|
|
206
|
+
try {
|
|
207
|
+
const { rest: args, vaultFlag, json } = extractGlobalFlags(argv.slice(2));
|
|
208
|
+
const command = args.shift();
|
|
209
|
+
if (!command || command === '--help' || command === '-h') {
|
|
210
|
+
process.stdout.write(USAGE);
|
|
152
211
|
return;
|
|
153
212
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
process.stdout.write(renderOnboard());
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
// schema runs before vault resolution — pure Zod introspection, no vault needed.
|
|
163
|
-
if (command === 'schema') {
|
|
164
|
-
try {
|
|
165
|
-
const result = await cmdSchema(null, args);
|
|
166
|
-
process.stdout.write(result + '\n');
|
|
213
|
+
const name = command === '--version' || command === '-v' ? 'version' : command;
|
|
214
|
+
const spec = COMMANDS[name];
|
|
215
|
+
if (!spec) {
|
|
216
|
+
process.stderr.write(`Unknown command: ${command}\n${USAGE}`);
|
|
217
|
+
process.exit(1);
|
|
167
218
|
}
|
|
168
|
-
|
|
169
|
-
|
|
219
|
+
if (spec.phase === 'pre-vault') {
|
|
220
|
+
const out = await spec.run(args, { json, vaultFlag });
|
|
221
|
+
if (out !== null)
|
|
222
|
+
process.stdout.write(out.endsWith('\n') ? out : out + '\n');
|
|
223
|
+
return;
|
|
170
224
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// Resolve vault
|
|
179
|
-
const resolution = resolveVaultPath(flagValue);
|
|
180
|
-
if (resolution === null) {
|
|
181
|
-
process.stderr.write(noVaultMessage());
|
|
182
|
-
process.exit(1);
|
|
183
|
-
}
|
|
184
|
-
if (typeof resolution === 'object' && 'error' in resolution) {
|
|
185
|
-
process.stderr.write(`Error: ${resolution.error}\n`);
|
|
186
|
-
process.exit(1);
|
|
187
|
-
}
|
|
188
|
-
const vaultPath = resolution;
|
|
189
|
-
// export runs before Hafez instance creation — read-only, no auto-sync
|
|
190
|
-
if (command === 'export') {
|
|
191
|
-
try {
|
|
192
|
-
const EXPORT_USAGE = 'Usage: hafez export --okf [--out <dir>]';
|
|
193
|
-
const { flags } = parseArgs(args, { okf: 'boolean', out: 'string' }, EXPORT_USAGE);
|
|
194
|
-
if (flags.okf !== true)
|
|
195
|
-
throw new UsageError(EXPORT_USAGE);
|
|
196
|
-
const outDir = typeof flags.out === 'string' ? resolve(flags.out) : resolve(process.cwd(), 'okf-export');
|
|
197
|
-
const { exportOkf } = await import('../export-okf.js');
|
|
198
|
-
const exportReport = exportOkf(vaultPath, outDir);
|
|
199
|
-
if (json) {
|
|
200
|
-
process.stdout.write(JSON.stringify(exportReport, null, 2) + '\n');
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
const lines = [`Exported ${exportReport.entities} entities, ${exportReport.knowledge} knowledge, ${exportReport.sessions} sessions to ${exportReport.outDir}`];
|
|
204
|
-
for (const s of exportReport.skipped)
|
|
205
|
-
lines.push(` skipped ${s.file}: ${s.reason}`);
|
|
206
|
-
if (exportReport.unresolvedLinks > 0)
|
|
207
|
-
lines.push(` unresolved links: ${exportReport.unresolvedLinks}`);
|
|
208
|
-
process.stdout.write(lines.join('\n') + '\n');
|
|
209
|
-
}
|
|
225
|
+
// Resolve vault (loaded here, not at module top: the pre-vault commands
|
|
226
|
+
// above return without ever needing the vault machinery)
|
|
227
|
+
const { resolveVaultPath, noVaultMessage } = await import('./resolve-vault.js');
|
|
228
|
+
const resolution = resolveVaultPath(vaultFlag);
|
|
229
|
+
if (resolution === null) {
|
|
230
|
+
process.stderr.write(noVaultMessage());
|
|
231
|
+
process.exit(1);
|
|
210
232
|
}
|
|
211
|
-
|
|
212
|
-
|
|
233
|
+
if (typeof resolution === 'object' && 'error' in resolution) {
|
|
234
|
+
process.stderr.write(`Error: ${resolution.error}\n`);
|
|
235
|
+
process.exit(1);
|
|
213
236
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const os = createHafez({ vaultPath, git: { push: false } });
|
|
222
|
-
try {
|
|
223
|
-
const result = await handler(os, args, { json });
|
|
237
|
+
const vaultPath = resolution;
|
|
238
|
+
// First touch of the core — and so of the native SQLite binding. A
|
|
239
|
+
// failure to load it lands in the catch below and is recognised by
|
|
240
|
+
// isBindingsFailure, exactly as it was when this import was static.
|
|
241
|
+
const { createHafez } = await import('../index.js');
|
|
242
|
+
const os = createHafez({ vaultPath, git: { push: false } });
|
|
243
|
+
const result = await spec.run(os, args, { json, vaultPath });
|
|
224
244
|
// Auto-sync after mutating commands
|
|
225
|
-
if (
|
|
245
|
+
if (spec.mutating) {
|
|
226
246
|
try {
|
|
227
247
|
await os.sync();
|
|
228
248
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type Comparison = 'behind' | 'equal' | 'ahead' | 'unknown';
|
|
2
|
+
/** `command` entries are directly executable (after user approval); `action`
|
|
3
|
+
* entries are prose, so an agent that shells out every entry does not try to
|
|
4
|
+
* run English. */
|
|
5
|
+
export interface UpgradeStep {
|
|
6
|
+
kind: 'command' | 'action';
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
export interface UpgradeState {
|
|
10
|
+
current: string;
|
|
11
|
+
latest: string | null;
|
|
12
|
+
comparison: Comparison;
|
|
13
|
+
registryReachable: boolean;
|
|
14
|
+
npmMajor: number | null;
|
|
15
|
+
install: 'registry' | 'dev-checkout';
|
|
16
|
+
skill: 'present' | 'absent';
|
|
17
|
+
skillSource: string;
|
|
18
|
+
skillTarget: string;
|
|
19
|
+
/** Mutually exclusive with the registry install in `steps` — exposed
|
|
20
|
+
* separately so an agent iterating `steps` cannot run both. */
|
|
21
|
+
alternativeInstall: string | null;
|
|
22
|
+
steps: UpgradeStep[];
|
|
23
|
+
}
|
|
24
|
+
export interface UpgradeDeps {
|
|
25
|
+
fetch: typeof globalThis.fetch;
|
|
26
|
+
registryUrl: string;
|
|
27
|
+
packageRoot: string;
|
|
28
|
+
installedSkillPath: string;
|
|
29
|
+
npmVersion: () => string;
|
|
30
|
+
}
|
|
31
|
+
/** Three-way compare on the numeric segments. Hand-rolled string compare is
|
|
32
|
+
* the trap this exists to avoid: `1.10.0` < `1.9.0` lexicographically.
|
|
33
|
+
* Anything that is not plain X.Y.Z (a prerelease, a build tag) is `unknown` —
|
|
34
|
+
* this tool never guesses at a version it cannot order. */
|
|
35
|
+
export declare function compareVersions(local: string, latest: string): Comparison;
|
|
36
|
+
export declare function buildUpgradeState(deps: UpgradeDeps): Promise<UpgradeState>;
|
|
37
|
+
export declare function renderUpgrade(state: UpgradeState): string;
|
|
38
|
+
/** `deps` is the third parameter, unreachable from dispatch (PreVaultRun is
|
|
39
|
+
* `(args, ctx)`) — the tests inject it directly. */
|
|
40
|
+
export declare function cmdUpgrade(args: string[], ctx: {
|
|
41
|
+
json: boolean;
|
|
42
|
+
}, deps?: UpgradeDeps): Promise<string>;
|