hafez 1.2.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 +470 -0
- package/dist/batch-ops.js +446 -0
- package/dist/cli/args.d.ts +19 -0
- package/dist/cli/args.js +53 -0
- package/dist/cli/batch-errors.js +6 -3
- package/dist/cli/commands.d.ts +24 -108
- package/dist/cli/commands.js +297 -313
- package/dist/cli/help-agent.d.ts +0 -5
- package/dist/cli/help-agent.js +44 -5
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.js +172 -172
- package/dist/cli/resolve-vault.js +2 -1
- package/dist/cli/schema-introspect.d.ts +1 -1
- package/dist/cli/schema-introspect.js +6 -22
- package/dist/cli/upgrade.d.ts +42 -0
- package/dist/cli/upgrade.js +305 -0
- package/dist/cli.js +0 -0
- package/dist/contracts.d.ts +55 -6
- package/dist/contracts.js +110 -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 +318 -56
- package/dist/digest.js +2 -0
- package/dist/document.d.ts +117 -0
- package/dist/document.js +578 -0
- package/dist/export-okf.js +48 -34
- package/dist/git.d.ts +7 -8
- package/dist/git.js +198 -63
- package/dist/index.d.ts +3 -4
- package/dist/index.js +302 -915
- package/dist/knowledge-index.d.ts +23 -1
- package/dist/knowledge-index.js +71 -13
- package/dist/merge.d.ts +1 -1
- package/dist/merge.js +106 -86
- package/dist/schema.js +4 -4
- package/dist/templates.d.ts +6 -0
- package/dist/templates.js +27 -27
- package/dist/types.d.ts +86 -12
- package/dist/vault.d.ts +51 -6
- package/dist/vault.js +114 -11
- package/package.json +2 -2
- package/skill/SKILL.md +51 -110
- package/dist/parser.d.ts +0 -18
- package/dist/parser.js +0 -68
- package/dist/query.d.ts +0 -24
- package/dist/query.js +0 -167
- package/dist/sections.d.ts +0 -54
- package/dist/sections.js +0 -297
package/dist/cli/help-agent.d.ts
CHANGED
package/dist/cli/help-agent.js
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
// session and then produce valid batch JSON on first try.
|
|
6
6
|
//
|
|
7
7
|
// Hand-written sections: Preamble + Hard rules (prose, free-form).
|
|
8
|
-
// Generated sections (wrapped in <!-- SECTION:name --> markers so
|
|
9
|
-
//
|
|
8
|
+
// Generated sections (wrapped in <!-- SECTION:name --> markers so tests can
|
|
9
|
+
// parse them): op-catalog, critical-enums, batch-examples, digest.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// those come from
|
|
11
|
+
// Generated sections derive from the Op Spec table (batch-ops.ts via
|
|
12
|
+
// schema-introspect.ts), so they cannot drift from the Zod schemas. Do NOT
|
|
13
|
+
// enumerate enum values inside the preamble or hard rules — those come from
|
|
14
|
+
// schema-introspect.ts.
|
|
14
15
|
import { listOps, getEnumValues, getOpExample, getOpSchema } from './schema-introspect.js';
|
|
15
16
|
const PREAMBLE = `# Hafez — Agent Prompt Gate
|
|
16
17
|
|
|
@@ -143,6 +144,29 @@ function renderDigestSection() {
|
|
|
143
144
|
lines.push('<!-- /SECTION:digest -->');
|
|
144
145
|
return lines.join('\n');
|
|
145
146
|
}
|
|
147
|
+
const UPGRADE_SECTION = `## Upgrading hafez itself
|
|
148
|
+
|
|
149
|
+
\`hafez upgrade\` — checks whether the installed hafez is outdated and prints
|
|
150
|
+
the steps. It NEVER installs: the steps install code and overwrite the user's
|
|
151
|
+
agent instructions, so show them verbatim, get an explicit yes, and only then
|
|
152
|
+
run them. Re-run to verify. Every check outcome exits 0 — being outdated is a
|
|
153
|
+
report, not a gate — so read the output, not the exit code. A non-zero exit
|
|
154
|
+
means the invocation was wrong (exit 2, \`upgrade\` takes no arguments), never
|
|
155
|
+
that a check failed. Not to be confused with \`hafez update\`, which updates
|
|
156
|
+
an entity.
|
|
157
|
+
|
|
158
|
+
\`--json\` keys: current, latest (null when unverified), comparison
|
|
159
|
+
(behind|equal|ahead|unknown), registryReachable, npmMajor, install
|
|
160
|
+
(registry|dev-checkout), skill (present|absent), skillSource, skillTarget,
|
|
161
|
+
alternativeInstall, steps. \`steps\` is the action list in execution order;
|
|
162
|
+
\`kind: 'command'\` entries are executable, \`kind: 'action'\` entries are
|
|
163
|
+
prose — do not shell out to them. \`alternativeInstall\` is mutually exclusive
|
|
164
|
+
with the install command in \`steps\`; never run both.
|
|
165
|
+
|
|
166
|
+
Checks the npm registry only (override with \`HAFEZ_REGISTRY_URL\`), so a
|
|
167
|
+
just-cut GitHub Release can read as \`equal\` until it is published.
|
|
168
|
+
|
|
169
|
+
`;
|
|
146
170
|
const FOOTER = `## Lookup pointers
|
|
147
171
|
|
|
148
172
|
- \`hafez schema\` — list all ops
|
|
@@ -155,6 +179,18 @@ const FOOTER = `## Lookup pointers
|
|
|
155
179
|
* measured as char/3.5. The drift test in tests/cli-help-agent.test.ts
|
|
156
180
|
* enforces the ceiling.
|
|
157
181
|
*/
|
|
182
|
+
const EXIT_CODES_SECTION = `## Exit codes
|
|
183
|
+
|
|
184
|
+
| code | meaning |
|
|
185
|
+
|------|---------|
|
|
186
|
+
| 0 | success |
|
|
187
|
+
| 1 | not found / unknown error |
|
|
188
|
+
| 2 | validation or usage error (bad flags, bad payload, core rejected the write) |
|
|
189
|
+
| 3 | git push failed — the write IS committed locally |
|
|
190
|
+
| 4 | write succeeded but the auto-sync after it failed |
|
|
191
|
+
| 5 | slug already exists |
|
|
192
|
+
| 6 | git commit failed — nothing was written to history |
|
|
193
|
+
| 7 | vault locked by another hafez process — retry when it finishes |`;
|
|
158
194
|
export function renderAgentHelp() {
|
|
159
195
|
return [
|
|
160
196
|
PREAMBLE,
|
|
@@ -166,6 +202,9 @@ export function renderAgentHelp() {
|
|
|
166
202
|
'',
|
|
167
203
|
renderDigestSection(),
|
|
168
204
|
'',
|
|
205
|
+
EXIT_CODES_SECTION,
|
|
206
|
+
'',
|
|
207
|
+
UPGRADE_SECTION,
|
|
169
208
|
FOOTER,
|
|
170
209
|
].join('\n');
|
|
171
210
|
}
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1 +1,42 @@
|
|
|
1
|
+
import type { CommandOpts } from './commands.js';
|
|
2
|
+
type PreVaultRun = (args: string[], ctx: {
|
|
3
|
+
json: boolean;
|
|
4
|
+
vaultFlag?: string;
|
|
5
|
+
}) => Promise<string | null> | string | null;
|
|
6
|
+
type VaultRun = (os: any, args: string[], opts: CommandOpts) => Promise<string>;
|
|
7
|
+
export type CommandSpec = {
|
|
8
|
+
phase: 'pre-vault';
|
|
9
|
+
run: PreVaultRun;
|
|
10
|
+
} | {
|
|
11
|
+
phase: 'vault';
|
|
12
|
+
mutating?: boolean;
|
|
13
|
+
run: VaultRun;
|
|
14
|
+
};
|
|
15
|
+
export declare const COMMANDS: Record<string, CommandSpec>;
|
|
16
|
+
/** Exported so a guard test can assert every registered command is documented
|
|
17
|
+
* here — a registered-but-undocumented command is otherwise invisible. */
|
|
18
|
+
export declare const USAGE = "Usage: hafez <command> [args]\n\nCommands:\n read <slug> Read entity or knowledge note\n query [--filter ...] ... Filter entities/knowledge\n --kind all Both kinds; --filter matches entity status only,\n so knowledge notes (which have none) always show\n search <terms> Full-text search\n create <kind> <name> Create entity or knowledge note\n capture <name> [--notes] Quick inbox capture\n promote <slug> <target> Promote capture\u2192entity/project/knowledge, entity\u2192project\n update <slug> [--status] Update an entity or knowledge note\n --brief \"text\" Set/replace Brief (use --brief \"\" to clear)\n --description \"text\" Set one-line description (use --description \"\" to clear)\n --resource \"uri\" Set canonical resource URI, entities only (use --resource \"\" to clear)\n --add-action \"text\" Add a next action\n --complete-action \"text\" Complete matching action\n --remove-action \"text\" Remove matching action\n --clear-actions Remove all next actions\n --synthesis \"text\" Set/replace Synthesis section (--insight is an alias)\n --add-evidence \"text\" Append to Evidence section\n --add-source \"text\" Append to Sources section\n batch Apply multiple operations atomically (JSON on stdin)\n digest Apply digest input (JSON stdin) \u2192 batch payload (JSON stdout)\n export --okf [--out <dir>] Export vault as an OKF v0.1 bundle (read-only)\n sync Pull remote changes and push local commits\n link <slug> <target> --relation <rel> Add a link\n unlink <slug> <target> --relation <rel> Remove a link\n stats Vault summary: counts, stale items, recents\n changelog --since <when> Git-derived change history (e.g. --since 7.days.ago)\n schema [op] [--examples] Machine-readable JSON schema for batch operations\n validate Check vault integrity\n index rebuild Rebuild the SQLite index\n init --register <path> Register an existing vault\n init Show current vault resolution\n onboard Agent-directed first run: seed the vault, pick integration level\n upgrade Check whether hafez is outdated and print the update steps\n help --agent Full API reference for agents\n\nGlobal flags:\n --vault <path> Vault path (overrides config)\n --json JSON output (structured, machine-readable)\n --version Print the hafez version\n\nVault discovery: --vault flag > ~/.config/hafez/vault\n";
|
|
19
|
+
/** Exit codes, documented in `hafez help --agent`. 0 = success; 4 is the
|
|
20
|
+
* mutate-succeeded-sync-failed warning path; anything unmapped = 1. */
|
|
21
|
+
export declare const EXIT_CODES: Record<string, number>;
|
|
22
|
+
/** The pure half of the error → exit adapter: which code an error maps to.
|
|
23
|
+
* Exported so the exit-code contract is testable without spawning the CLI. */
|
|
24
|
+
export declare function exitCodeFor(err: unknown): number;
|
|
25
|
+
/** Does this error mean the native module failed to load/build?
|
|
26
|
+
*
|
|
27
|
+
* Matched on the MESSAGE only, and only on the messages a load/ABI failure
|
|
28
|
+
* actually produces. The old test also scanned the stack for "better-sqlite3",
|
|
29
|
+
* which every SqliteError's stack contains — so a plain query error ("near
|
|
30
|
+
* 'abc': syntax error") printed reinstall advice instead of itself. */
|
|
31
|
+
export declare function isBindingsFailure(err: unknown): boolean;
|
|
32
|
+
/** Extract the global flags (--vault <path>, --json) from anywhere in argv.
|
|
33
|
+
* Exported for the in-process dispatch tests. A flag sitting where the vault
|
|
34
|
+
* path should be means the value is missing — never consume it (the same
|
|
35
|
+
* value-position rule parseArgs applies to declared command flags). */
|
|
36
|
+
export declare function extractGlobalFlags(args: string[]): {
|
|
37
|
+
rest: string[];
|
|
38
|
+
vaultFlag?: string;
|
|
39
|
+
json: boolean;
|
|
40
|
+
};
|
|
1
41
|
export declare function main(argv: string[]): Promise<void>;
|
|
42
|
+
export {};
|
package/dist/cli/index.js
CHANGED
|
@@ -1,43 +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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
},
|
|
29
50
|
},
|
|
30
|
-
stats: cmdStats,
|
|
31
|
-
changelog: cmdChangelog,
|
|
32
|
-
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) },
|
|
33
55
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
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]
|
|
37
59
|
|
|
38
60
|
Commands:
|
|
39
61
|
read <slug> Read entity or knowledge note
|
|
40
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
|
|
41
65
|
search <terms> Full-text search
|
|
42
66
|
create <kind> <name> Create entity or knowledge note
|
|
43
67
|
capture <name> [--notes] Quick inbox capture
|
|
@@ -67,6 +91,7 @@ Commands:
|
|
|
67
91
|
init --register <path> Register an existing vault
|
|
68
92
|
init Show current vault resolution
|
|
69
93
|
onboard Agent-directed first run: seed the vault, pick integration level
|
|
94
|
+
upgrade Check whether hafez is outdated and print the update steps
|
|
70
95
|
help --agent Full API reference for agents
|
|
71
96
|
|
|
72
97
|
Global flags:
|
|
@@ -76,13 +101,61 @@ Global flags:
|
|
|
76
101
|
|
|
77
102
|
Vault discovery: --vault flag > ~/.config/hafez/vault
|
|
78
103
|
`;
|
|
104
|
+
/** Exit codes, documented in `hafez help --agent`. 0 = success; 4 is the
|
|
105
|
+
* mutate-succeeded-sync-failed warning path; anything unmapped = 1. */
|
|
106
|
+
export const EXIT_CODES = {
|
|
107
|
+
NOT_FOUND: 1,
|
|
108
|
+
VALIDATION_FAILED: 2,
|
|
109
|
+
GIT_PUSH_FAILED: 3,
|
|
110
|
+
SLUG_EXISTS: 5,
|
|
111
|
+
GIT_COMMIT_FAILED: 6,
|
|
112
|
+
VAULT_LOCKED: 7,
|
|
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
|
+
}
|
|
123
|
+
/** The one error → exit adapter. Usage errors (bad flags/args) are distinct
|
|
124
|
+
* from vault errors and from unknown crashes. */
|
|
125
|
+
function report(err) {
|
|
126
|
+
if (err instanceof UsageError) {
|
|
127
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
128
|
+
process.exit(exitCodeFor(err));
|
|
129
|
+
}
|
|
130
|
+
if (err instanceof HafezError) {
|
|
131
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
132
|
+
if (err.details?.length) {
|
|
133
|
+
for (const detail of err.details)
|
|
134
|
+
process.stderr.write(` - ${detail}\n`);
|
|
135
|
+
}
|
|
136
|
+
process.exit(exitCodeFor(err));
|
|
137
|
+
}
|
|
138
|
+
failUnknown(err);
|
|
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
|
+
}
|
|
79
152
|
// Terminal handler for non-Hafez errors. Special-cases unbuilt better-sqlite3
|
|
80
153
|
// bindings: npm 11+ blocks install scripts by default, so `npm install -g
|
|
81
154
|
// hafez` reports success with the native module unbuilt and the first indexed
|
|
82
155
|
// command dies here — guide instead of stack-tracing.
|
|
83
156
|
function failUnknown(err) {
|
|
84
157
|
const e = err;
|
|
85
|
-
if (
|
|
158
|
+
if (isBindingsFailure(e)) {
|
|
86
159
|
process.stderr.write('Error: the SQLite bindings (better-sqlite3) are not built.\n' +
|
|
87
160
|
'npm 11+ blocks install scripts by default, so the install reports success\n' +
|
|
88
161
|
'while skipping the native build. Fix with:\n\n' +
|
|
@@ -92,147 +165,84 @@ function failUnknown(err) {
|
|
|
92
165
|
process.stderr.write(`Error: ${e.message}\n`);
|
|
93
166
|
process.exit(1);
|
|
94
167
|
}
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
process.stdout.write(renderAgentHelp());
|
|
123
|
-
return;
|
|
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++;
|
|
124
195
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
// onboard runs before vault resolution — static agent-directed text
|
|
129
|
-
if (command === 'onboard') {
|
|
130
|
-
process.stdout.write(renderOnboard());
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
// schema runs before vault resolution — pure Zod introspection, no vault needed.
|
|
134
|
-
if (command === 'schema') {
|
|
135
|
-
try {
|
|
136
|
-
const result = await cmdSchema(null, args);
|
|
137
|
-
process.stdout.write(result + '\n');
|
|
196
|
+
else if (a === '--json') {
|
|
197
|
+
json = true;
|
|
138
198
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
process.stderr.write(`Error: ${err.message}\n`);
|
|
142
|
-
if (err.details?.length) {
|
|
143
|
-
for (const detail of err.details)
|
|
144
|
-
process.stderr.write(` - ${detail}\n`);
|
|
145
|
-
}
|
|
146
|
-
process.exit(2);
|
|
147
|
-
}
|
|
148
|
-
failUnknown(err);
|
|
199
|
+
else {
|
|
200
|
+
rest.push(a);
|
|
149
201
|
}
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
// init runs before vault resolution
|
|
153
|
-
if (command === 'init') {
|
|
154
|
-
cmdInit(args, flagValue);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
// Resolve vault
|
|
158
|
-
const resolution = resolveVaultPath(flagValue);
|
|
159
|
-
if (resolution === null) {
|
|
160
|
-
process.stderr.write(noVaultMessage());
|
|
161
|
-
process.exit(1);
|
|
162
|
-
}
|
|
163
|
-
if (typeof resolution === 'object' && 'error' in resolution) {
|
|
164
|
-
process.stderr.write(`Error: ${resolution.error}\n`);
|
|
165
|
-
process.exit(1);
|
|
166
202
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
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}`);
|
|
172
217
|
process.exit(1);
|
|
173
218
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
process.stderr.write('Usage: hafez export --okf [--out <dir>]\n');
|
|
180
|
-
process.exit(1);
|
|
181
|
-
}
|
|
182
|
-
outDir = resolve(outValue);
|
|
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;
|
|
183
224
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const lines = [`Exported ${report.entities} entities, ${report.knowledge} knowledge, ${report.sessions} sessions to ${report.outDir}`];
|
|
192
|
-
for (const s of report.skipped)
|
|
193
|
-
lines.push(` skipped ${s.file}: ${s.reason}`);
|
|
194
|
-
if (report.unresolvedLinks > 0)
|
|
195
|
-
lines.push(` unresolved links: ${report.unresolvedLinks}`);
|
|
196
|
-
process.stdout.write(lines.join('\n') + '\n');
|
|
197
|
-
}
|
|
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);
|
|
198
232
|
}
|
|
199
|
-
|
|
200
|
-
|
|
233
|
+
if (typeof resolution === 'object' && 'error' in resolution) {
|
|
234
|
+
process.stderr.write(`Error: ${resolution.error}\n`);
|
|
235
|
+
process.exit(1);
|
|
201
236
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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');
|
|
206
242
|
const os = createHafez({ vaultPath, git: { push: false } });
|
|
207
|
-
|
|
208
|
-
const result = await cmdSync(os, args);
|
|
209
|
-
process.stdout.write(result + '\n');
|
|
210
|
-
}
|
|
211
|
-
catch (err) {
|
|
212
|
-
if (err instanceof HafezError) {
|
|
213
|
-
process.stderr.write(`Error: ${err.message}\n`);
|
|
214
|
-
if (err.details?.length) {
|
|
215
|
-
for (const detail of err.details) {
|
|
216
|
-
process.stderr.write(` - ${detail}\n`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
const exitCodes = { NOT_FOUND: 1, VALIDATION_FAILED: 2, GIT_PUSH_FAILED: 3 };
|
|
220
|
-
process.exit(exitCodes[err.code] ?? 1);
|
|
221
|
-
}
|
|
222
|
-
failUnknown(err);
|
|
223
|
-
}
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
const handler = COMMANDS[command];
|
|
227
|
-
if (!handler) {
|
|
228
|
-
process.stderr.write(`Unknown command: ${command}\n${USAGE}`);
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
231
|
-
const os = createHafez({ vaultPath, git: { push: false } });
|
|
232
|
-
try {
|
|
233
|
-
const result = await handler(os, args, { json });
|
|
243
|
+
const result = await spec.run(os, args, { json, vaultPath });
|
|
234
244
|
// Auto-sync after mutating commands
|
|
235
|
-
if (
|
|
245
|
+
if (spec.mutating) {
|
|
236
246
|
try {
|
|
237
247
|
await os.sync();
|
|
238
248
|
}
|
|
@@ -246,16 +256,6 @@ export async function main(argv) {
|
|
|
246
256
|
process.stdout.write(result + '\n');
|
|
247
257
|
}
|
|
248
258
|
catch (err) {
|
|
249
|
-
|
|
250
|
-
process.stderr.write(`Error: ${err.message}\n`);
|
|
251
|
-
if (err.details?.length) {
|
|
252
|
-
for (const detail of err.details) {
|
|
253
|
-
process.stderr.write(` - ${detail}\n`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const exitCodes = { NOT_FOUND: 1, VALIDATION_FAILED: 2, GIT_PUSH_FAILED: 3 };
|
|
257
|
-
process.exit(exitCodes[err.code] ?? 1);
|
|
258
|
-
}
|
|
259
|
-
failUnknown(err);
|
|
259
|
+
report(err);
|
|
260
260
|
}
|
|
261
261
|
}
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs
|
|
|
3
3
|
import { join, resolve } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
|
+
import { kindDir } from '../vault.js';
|
|
6
7
|
export function getConfigDir() {
|
|
7
8
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
8
9
|
return join(xdg || join(homedir(), '.config'), 'hafez');
|
|
@@ -28,7 +29,7 @@ export function saveVaultConfig(vaultPath) {
|
|
|
28
29
|
writeFileSync(join(dir, 'vault'), resolve(vaultPath) + '\n');
|
|
29
30
|
}
|
|
30
31
|
function isVault(dirPath) {
|
|
31
|
-
return existsSync(dirPath) && statSync(dirPath).isDirectory() && existsSync(join(dirPath, '
|
|
32
|
+
return existsSync(dirPath) && statSync(dirPath).isDirectory() && existsSync(join(dirPath, kindDir('entity')));
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
35
|
* Resolve vault path. Returns string or null.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// types actually used in BatchOperationSchema plus a few forward-looking ones
|
|
11
11
|
// (`default`, `effects`, `union`) so the walker does not explode when the
|
|
12
12
|
// schema is extended.
|
|
13
|
-
import { BatchOperationSchema, SessionLogSchema, UpdateFieldsSchema, } from '
|
|
13
|
+
import { BatchOperationSchema, SessionLogSchema, UpdateFieldsSchema, OP_SPECS, } from '../batch-ops.js';
|
|
14
14
|
import { DigestInputSchema } from '../digest.js';
|
|
15
15
|
import { ALIAS_KEYS } from '../contracts.js';
|
|
16
16
|
// --- Walker ---
|
|
@@ -155,16 +155,8 @@ export const DIGEST_EXAMPLE = {
|
|
|
155
155
|
session_date: '2026-01-15',
|
|
156
156
|
agent: 'claude',
|
|
157
157
|
};
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
'create-entity': 'Create a new entity (capture / entity / project).',
|
|
161
|
-
'create-knowledge': 'Create a new knowledge note (insight / plan subtype).',
|
|
162
|
-
'create-session': 'Create a new session note linked to an entity.',
|
|
163
|
-
'capture': 'Quick capture — a raw inbox item with no structure required.',
|
|
164
|
-
'link': 'Add a parent or related link between two entities.',
|
|
165
|
-
'unlink': 'Remove a parent or related link between two entities.',
|
|
166
|
-
'promote': 'Promote a capture to entity/project/knowledge, or an entity to project.',
|
|
167
|
-
};
|
|
158
|
+
// Descriptions live in the Op Spec table (src/batch-ops.ts) — one row per op.
|
|
159
|
+
const OP_DESCRIPTIONS = Object.fromEntries(OP_SPECS.map(s => [s.name, s.description]));
|
|
168
160
|
export function listOps() {
|
|
169
161
|
return enumerateBranches().map(({ op, kind }) => {
|
|
170
162
|
const name = kind ? `${op}-${kind}` : op;
|
|
@@ -293,17 +285,9 @@ function shouldStrip(path, _key) {
|
|
|
293
285
|
return Object.prototype.hasOwnProperty.call(ALIAS_KEYS, path);
|
|
294
286
|
}
|
|
295
287
|
function isDefaultIncluded(op, kind, fieldKey) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return true;
|
|
300
|
-
if (op === 'create' && kind === 'session' && fieldKey === 'synthesis')
|
|
301
|
-
return true;
|
|
302
|
-
if (op === 'update' && fieldKey === 'status')
|
|
303
|
-
return true;
|
|
304
|
-
if (op === 'capture' && fieldKey === 'notes')
|
|
305
|
-
return true;
|
|
306
|
-
return false;
|
|
288
|
+
// Example-inclusion hints live in the Op Spec table, one row per op.
|
|
289
|
+
const spec = OP_SPECS.find(s => s.op === op && s.kind === kind);
|
|
290
|
+
return spec ? spec.exampleInclude.includes(fieldKey) : false;
|
|
307
291
|
}
|
|
308
292
|
function sampleValue(field) {
|
|
309
293
|
switch (field.type) {
|