hafez 1.3.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 +83 -32
- package/dist/apply.d.ts +33 -0
- package/dist/apply.js +693 -0
- package/dist/batch-ops.d.ts +372 -26
- package/dist/batch-ops.js +355 -73
- package/dist/cli/args.d.ts +48 -6
- package/dist/cli/args.js +74 -9
- 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 +28 -3
- package/dist/cli/commands.js +440 -157
- package/dist/cli/format.d.ts +7 -3
- package/dist/cli/format.js +27 -10
- package/dist/cli/index.d.ts +55 -0
- package/dist/cli/index.js +215 -153
- 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 +51 -0
- package/dist/cli/upgrade.js +351 -0
- package/dist/closest.d.ts +8 -0
- package/dist/closest.js +65 -0
- package/dist/contracts.d.ts +65 -6
- package/dist/contracts.js +148 -23
- package/dist/dates.d.ts +10 -0
- package/dist/dates.js +27 -0
- package/dist/db.d.ts +40 -2
- package/dist/db.js +363 -66
- package/dist/digest.d.ts +6 -4
- package/dist/digest.js +28 -17
- package/dist/document.d.ts +135 -88
- package/dist/document.js +665 -379
- package/dist/export-okf.d.ts +13 -0
- package/dist/export-okf.js +50 -20
- package/dist/git.js +165 -48
- package/dist/harness.d.ts +58 -0
- package/dist/harness.js +50 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +301 -826
- 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 +98 -64
- package/dist/near-match.d.ts +9 -0
- package/dist/near-match.js +34 -0
- package/dist/schema.js +4 -4
- 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/templates.d.ts +6 -0
- package/dist/templates.js +27 -27
- package/dist/types.d.ts +78 -22
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +109 -0
- package/dist/vault.d.ts +18 -0
- package/dist/vault.js +51 -5
- 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 -186
- package/dist/query.d.ts +0 -24
- package/dist/query.js +0 -169
- package/skill/SKILL.md +0 -196
package/dist/cli/format.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { QueryResult, KnowledgeQueryResult, ValidationReport, SearchResult, VaultStats, ChangelogEntry } from '../types.js';
|
|
2
|
-
|
|
2
|
+
/** `parent` is the rendered value of the `parent:` line (the slug with the
|
|
3
|
+
* parent's description); absent, the slug alone. */
|
|
4
|
+
export declare function formatEntityHeader(fm: Record<string, any>, body: string, parent?: string | undefined): string;
|
|
3
5
|
export declare function formatKnowledgeHeader(fm: Record<string, any>, body: string): string;
|
|
4
|
-
export
|
|
5
|
-
export declare function
|
|
6
|
+
export type QueryTableRow = Pick<QueryResult, 'slug' | 'name' | 'type' | 'status' | 'next_action' | 'next_action_count'>;
|
|
7
|
+
export declare function formatQueryTable(results: QueryTableRow[], total?: number): string;
|
|
8
|
+
export type KnowledgeTableRow = Pick<KnowledgeQueryResult, 'slug' | 'name' | 'confidence' | 'domain' | 'reinforcement_count'>;
|
|
9
|
+
export declare function formatKnowledgeTable(results: KnowledgeTableRow[], total?: number): string;
|
|
6
10
|
export declare function formatSearchResults(results: SearchResult[]): string;
|
|
7
11
|
export declare function formatStats(stats: VaultStats): string;
|
|
8
12
|
export declare function formatChangelog(entries: ChangelogEntry[]): string;
|
package/dist/cli/format.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/** `parent` is the rendered value of the `parent:` line (the slug with the
|
|
2
|
+
* parent's description); absent, the slug alone. */
|
|
3
|
+
export function formatEntityHeader(fm, body, parent = fm.parent) {
|
|
2
4
|
const lines = [`# ${fm.name}`];
|
|
3
5
|
if (fm.description)
|
|
4
6
|
lines.push(`> ${fm.description}`);
|
|
@@ -11,9 +13,10 @@ export function formatEntityHeader(fm, body) {
|
|
|
11
13
|
meta.push(`domain: ${fm.domain.join(', ')}`);
|
|
12
14
|
if (meta.length)
|
|
13
15
|
lines.push(meta.join(' | '));
|
|
16
|
+
// The parent line stands alone: with its description it is a sentence.
|
|
17
|
+
if (parent)
|
|
18
|
+
lines.push(`parent: ${parent}`);
|
|
14
19
|
const meta2 = [];
|
|
15
|
-
if (fm.parent)
|
|
16
|
-
meta2.push(`parent: ${fm.parent}`);
|
|
17
20
|
if (fm['last-touched'])
|
|
18
21
|
meta2.push(`last-touched: ${fm['last-touched']}`);
|
|
19
22
|
if (fm.created)
|
|
@@ -34,18 +37,33 @@ export function formatKnowledgeHeader(fm, body) {
|
|
|
34
37
|
const lines = [`# ${fm.name}`];
|
|
35
38
|
if (fm.description)
|
|
36
39
|
lines.push(`> ${fm.description}`);
|
|
40
|
+
// A plan has a lifecycle and no reinforcement: its subtype, status and
|
|
41
|
+
// dates; an insight its confidence and reinforcement.
|
|
42
|
+
const plan = fm.subtype === 'plan';
|
|
37
43
|
const meta = [];
|
|
38
|
-
if (
|
|
44
|
+
if (plan)
|
|
45
|
+
meta.push('subtype: plan');
|
|
46
|
+
if (plan && fm.status)
|
|
47
|
+
meta.push(`status: ${fm.status}`);
|
|
48
|
+
if (!plan && fm.confidence)
|
|
39
49
|
meta.push(`confidence: ${fm.confidence}`);
|
|
40
50
|
if (fm.domain?.length)
|
|
41
51
|
meta.push(`domain: ${Array.isArray(fm.domain) ? fm.domain.join(', ') : fm.domain}`);
|
|
42
52
|
if (meta.length)
|
|
43
53
|
lines.push(meta.join(' | '));
|
|
44
54
|
const meta2 = [];
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
if (plan) {
|
|
56
|
+
if (fm.created)
|
|
57
|
+
meta2.push(`created: ${fm.created}`);
|
|
58
|
+
if (fm['last-touched'])
|
|
59
|
+
meta2.push(`last-touched: ${fm['last-touched']}`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
if (fm['reinforcement-count'] != null)
|
|
63
|
+
meta2.push(`reinforcement-count: ${fm['reinforcement-count']}`);
|
|
64
|
+
if (fm['last-reinforced'])
|
|
65
|
+
meta2.push(`last-reinforced: ${fm['last-reinforced']}`);
|
|
66
|
+
}
|
|
49
67
|
if (meta2.length)
|
|
50
68
|
lines.push(meta2.join(' | '));
|
|
51
69
|
if (fm.related?.length)
|
|
@@ -143,7 +161,7 @@ export function formatChangelog(entries) {
|
|
|
143
161
|
}
|
|
144
162
|
export function formatValidation(report) {
|
|
145
163
|
const issues = report.broken_slugs.length + report.orphaned_knowledge.length +
|
|
146
|
-
report.
|
|
164
|
+
report.missing_fields.length;
|
|
147
165
|
if (issues === 0) {
|
|
148
166
|
return `Vault OK — ${report.total_entities} entities, ${report.total_knowledge} knowledge, 0 issues`;
|
|
149
167
|
}
|
|
@@ -157,7 +175,6 @@ export function formatValidation(report) {
|
|
|
157
175
|
};
|
|
158
176
|
addSection('Broken Slugs', report.broken_slugs.map(i => `${i.slug}: ${i.issue}`));
|
|
159
177
|
addSection('Orphaned Knowledge', report.orphaned_knowledge.map(s => `${s} (no related links, not referenced by any entity)`));
|
|
160
|
-
addSection('Oversized Related', report.oversized_related.map(i => `${i.slug}: ${i.issue}`));
|
|
161
178
|
addSection('Missing Fields', report.missing_fields.map(i => `${i.slug}: ${i.issue}`));
|
|
162
179
|
return sections.join('\n');
|
|
163
180
|
}
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1 +1,56 @@
|
|
|
1
|
+
import { HafezError, type HafezErrorCode } from '../types.js';
|
|
2
|
+
import { UsageError } from './args.js';
|
|
3
|
+
import type { CommandOpts } from './commands.js';
|
|
4
|
+
type PreVaultRun = (args: string[], ctx: {
|
|
5
|
+
json: boolean;
|
|
6
|
+
vaultFlag?: string;
|
|
7
|
+
}) => Promise<string | null> | string | null;
|
|
8
|
+
type VaultRun = (os: any, args: string[], opts: CommandOpts) => Promise<string>;
|
|
9
|
+
export type CommandSpec = {
|
|
10
|
+
phase: 'pre-vault';
|
|
11
|
+
run: PreVaultRun;
|
|
12
|
+
} | {
|
|
13
|
+
phase: 'vault';
|
|
14
|
+
mutating?: boolean;
|
|
15
|
+
run: VaultRun;
|
|
16
|
+
};
|
|
17
|
+
export declare const COMMANDS: Record<string, CommandSpec>;
|
|
18
|
+
/** The one exit-code table. The error → exit adapter maps HafezError codes
|
|
19
|
+
* through `errorCodes`; `hafez --help` prints every row. 4 is the
|
|
20
|
+
* mutate-succeeded-sync-failed warning path; anything unmapped = 1. */
|
|
21
|
+
export declare const EXIT_TABLE: ReadonlyArray<{
|
|
22
|
+
exit: number;
|
|
23
|
+
meaning: string;
|
|
24
|
+
errorCodes?: readonly HafezErrorCode[];
|
|
25
|
+
}>;
|
|
26
|
+
export declare const EXIT_CODES: Record<string, number>;
|
|
27
|
+
/** Exported so a guard test can assert every registered command is documented
|
|
28
|
+
* here — a registered-but-undocumented command is otherwise invisible. */
|
|
29
|
+
export declare const USAGE: string;
|
|
30
|
+
/** The pure half of the error → exit adapter: which code an error maps to.
|
|
31
|
+
* Exported so the exit-code contract is testable without spawning the CLI. */
|
|
32
|
+
export declare function exitCodeFor(err: unknown): number;
|
|
33
|
+
/** The text half of the error adapter: what it prints for a usage or vault
|
|
34
|
+
* error. A misuse ends with the shape to retry from. */
|
|
35
|
+
export declare function errorText(err: UsageError | HafezError): string;
|
|
36
|
+
/** Does this error mean the native module failed to load/build?
|
|
37
|
+
*
|
|
38
|
+
* Matched on the MESSAGE only, and only on the messages a load/ABI failure
|
|
39
|
+
* actually produces. The old test also scanned the stack for "better-sqlite3",
|
|
40
|
+
* which every SqliteError's stack contains — so a plain query error ("near
|
|
41
|
+
* 'abc': syntax error") printed reinstall advice instead of itself. */
|
|
42
|
+
export declare function isBindingsFailure(err: unknown): boolean;
|
|
43
|
+
/** Extract the global flags (--vault <path>, --json) from anywhere in argv.
|
|
44
|
+
* Exported for the in-process dispatch tests. A flag sitting where the vault
|
|
45
|
+
* path should be means the value is missing — never consume it (the same
|
|
46
|
+
* value-position rule parseArgs applies to declared command flags). */
|
|
47
|
+
export declare function extractGlobalFlags(args: string[]): {
|
|
48
|
+
rest: string[];
|
|
49
|
+
vaultFlag?: string;
|
|
50
|
+
json: boolean;
|
|
51
|
+
};
|
|
52
|
+
/** Is this the SessionStart hook's form, `onboard --block`? Read from the raw
|
|
53
|
+
* args, the token after `--vault` skipped, so it holds when parsing them fails. */
|
|
54
|
+
export declare function isBlockHook(args: string[]): boolean;
|
|
1
55
|
export declare function main(argv: string[]): Promise<void>;
|
|
56
|
+
export {};
|
package/dist/cli/index.js
CHANGED
|
@@ -1,62 +1,102 @@
|
|
|
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 { HelpRequest, MisuseError, RenderedError, UsageError, helpIfAsked } 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('../surfaces/surfaces.js')).renderAgentHelp() : USAGE) },
|
|
22
|
+
onboard: { phase: 'pre-vault', run: async (args, ctx) => (await import('./onboard.js')).cmdOnboard(args, { vaultFlag: ctx.vaultFlag }) },
|
|
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
|
+
delete: { phase: 'vault', mutating: true, run: lazy(m => m.cmdDelete) },
|
|
42
|
+
batch: { phase: 'vault', mutating: true, run: lazy(m => m.cmdBatch) },
|
|
43
|
+
validate: { phase: 'vault', run: lazy(m => m.cmdValidate) },
|
|
44
|
+
index: {
|
|
45
|
+
phase: 'vault',
|
|
46
|
+
run: async (os, args) => {
|
|
47
|
+
const usage = 'Usage: hafez index rebuild';
|
|
48
|
+
helpIfAsked(args, usage);
|
|
49
|
+
if (args[0] === 'rebuild')
|
|
50
|
+
return (await import('./commands.js')).cmdIndexRebuild(os);
|
|
51
|
+
throw new UsageError(usage);
|
|
52
|
+
},
|
|
31
53
|
},
|
|
32
|
-
stats: cmdStats,
|
|
33
|
-
changelog: cmdChangelog,
|
|
34
|
-
digest: cmdDigest,
|
|
54
|
+
stats: { phase: 'vault', run: lazy(m => m.cmdStats) },
|
|
55
|
+
changelog: { phase: 'vault', run: lazy(m => m.cmdChangelog) },
|
|
56
|
+
digest: { phase: 'vault', run: lazy(m => m.cmdDigest) },
|
|
57
|
+
export: { phase: 'vault', run: lazy(m => m.cmdExport) },
|
|
35
58
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
59
|
+
/** The one exit-code table. The error → exit adapter maps HafezError codes
|
|
60
|
+
* through `errorCodes`; `hafez --help` prints every row. 4 is the
|
|
61
|
+
* mutate-succeeded-sync-failed warning path; anything unmapped = 1. */
|
|
62
|
+
export const EXIT_TABLE = [
|
|
63
|
+
{ exit: 0, meaning: 'success' },
|
|
64
|
+
{ exit: 1, meaning: 'not found (slug, command or vault) / unknown error', errorCodes: ['NOT_FOUND'] },
|
|
65
|
+
{ exit: 2, meaning: 'validation or usage error (bad flags, bad payload, core rejected the write)', errorCodes: ['VALIDATION_FAILED'] },
|
|
66
|
+
{ exit: 3, meaning: 'git push failed — the write IS committed locally', errorCodes: ['GIT_PUSH_FAILED'] },
|
|
67
|
+
{ exit: 4, meaning: 'write succeeded but the auto-sync after it failed' },
|
|
68
|
+
{ exit: 5, meaning: 'slug already exists', errorCodes: ['SLUG_EXISTS'] },
|
|
69
|
+
{ exit: 6, meaning: 'git commit failed — nothing was written to history', errorCodes: ['GIT_COMMIT_FAILED'] },
|
|
70
|
+
{ exit: 7, meaning: 'vault locked by another hafez process — retry when it finishes', errorCodes: ['VAULT_LOCKED'] },
|
|
71
|
+
];
|
|
72
|
+
export const EXIT_CODES = Object.fromEntries(EXIT_TABLE.flatMap(row => (row.errorCodes ?? []).map(code => [code, row.exit])));
|
|
73
|
+
/** Exported so a guard test can assert every registered command is documented
|
|
74
|
+
* here — a registered-but-undocumented command is otherwise invisible. */
|
|
75
|
+
export const USAGE = `Usage: hafez <command> [args]
|
|
39
76
|
|
|
40
77
|
Commands:
|
|
41
78
|
read <slug> Read entity or knowledge note
|
|
42
79
|
query [--filter ...] ... Filter entities/knowledge
|
|
80
|
+
--kind all Both kinds; --filter matches entity status only,
|
|
81
|
+
so knowledge notes (which have none) always show
|
|
43
82
|
search <terms> Full-text search
|
|
44
83
|
create <kind> <name> Create entity or knowledge note
|
|
45
84
|
capture <name> [--notes] Quick inbox capture
|
|
46
85
|
promote <slug> <target> Promote capture→entity/project/knowledge, entity→project
|
|
86
|
+
delete <slug> Delete a knowledge or session note (an entity: update --status done)
|
|
47
87
|
update <slug> [--status] Update an entity or knowledge note
|
|
48
88
|
--brief "text" Set/replace Brief (use --brief "" to clear)
|
|
49
89
|
--description "text" Set one-line description (use --description "" to clear)
|
|
50
90
|
--resource "uri" Set canonical resource URI, entities only (use --resource "" to clear)
|
|
51
91
|
--add-action "text" Add a next action
|
|
52
|
-
--complete-action
|
|
53
|
-
--remove-action
|
|
92
|
+
--complete-action handle-or-text Complete the action with that Handle (as read shows it), else the matching one
|
|
93
|
+
--remove-action handle-or-text Remove the action with that Handle, else the matching one
|
|
54
94
|
--clear-actions Remove all next actions
|
|
55
95
|
--synthesis "text" Set/replace Synthesis section (--insight is an alias)
|
|
56
96
|
--add-evidence "text" Append to Evidence section
|
|
57
97
|
--add-source "text" Append to Sources section
|
|
58
|
-
batch Apply multiple operations atomically (JSON on stdin)
|
|
59
|
-
digest Apply digest input (JSON stdin) → batch payload (JSON stdout)
|
|
98
|
+
batch Apply multiple operations atomically (JSON on stdin, --data or --file)
|
|
99
|
+
digest Apply digest input (JSON on stdin, --data or --file) → batch payload (JSON stdout)
|
|
60
100
|
export --okf [--out <dir>] Export vault as an OKF v0.1 bundle (read-only)
|
|
61
101
|
sync Pull remote changes and push local commits
|
|
62
102
|
link <slug> <target> --relation <rel> Add a link
|
|
@@ -68,8 +108,14 @@ Commands:
|
|
|
68
108
|
index rebuild Rebuild the SQLite index
|
|
69
109
|
init --register <path> Register an existing vault
|
|
70
110
|
init Show current vault resolution
|
|
71
|
-
onboard
|
|
72
|
-
|
|
111
|
+
onboard Setup status, then the script your agent follows to set Hafez up
|
|
112
|
+
--install --mode <always-on|invoke-only> [--harness <h>] [--confirm]
|
|
113
|
+
Write both skills and, always-on, the Block
|
|
114
|
+
--remove [--confirm] Remove the skills and the Block region
|
|
115
|
+
--block [--harness <h>] Print the Block and the entities matching this folder
|
|
116
|
+
(the SessionStart hook; prints nothing on any failure)
|
|
117
|
+
upgrade Check whether hafez is outdated and print the update steps
|
|
118
|
+
help --agent The hafez skill body: the reference when the skill is not installed
|
|
73
119
|
|
|
74
120
|
Global flags:
|
|
75
121
|
--vault <path> Vault path (overrides config)
|
|
@@ -77,41 +123,68 @@ Global flags:
|
|
|
77
123
|
--version Print the hafez version
|
|
78
124
|
|
|
79
125
|
Vault discovery: --vault flag > ~/.config/hafez/vault
|
|
126
|
+
|
|
127
|
+
Exit codes:
|
|
128
|
+
${EXIT_TABLE.map(row => ` ${row.exit} ${row.meaning}`).join('\n')}
|
|
80
129
|
`;
|
|
81
|
-
/**
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
130
|
+
/** The pure half of the error → exit adapter: which code an error maps to.
|
|
131
|
+
* Exported so the exit-code contract is testable without spawning the CLI. */
|
|
132
|
+
export function exitCodeFor(err) {
|
|
133
|
+
if (err instanceof HelpRequest)
|
|
134
|
+
return 0;
|
|
135
|
+
if (err instanceof UsageError)
|
|
136
|
+
return 2;
|
|
137
|
+
if (err instanceof HafezError)
|
|
138
|
+
return EXIT_CODES[err.code] ?? 1;
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
/** The text half of the error adapter: what it prints for a usage or vault
|
|
142
|
+
* error. A misuse ends with the shape to retry from. */
|
|
143
|
+
export function errorText(err) {
|
|
144
|
+
const lines = [`Error: ${err.message}`];
|
|
145
|
+
if (err instanceof HafezError)
|
|
146
|
+
for (const detail of err.details ?? [])
|
|
147
|
+
lines.push(` - ${detail}`);
|
|
148
|
+
if (err instanceof MisuseError)
|
|
149
|
+
lines.push('', err.shape);
|
|
150
|
+
return lines.join('\n') + '\n';
|
|
151
|
+
}
|
|
91
152
|
/** The one error → exit adapter. Usage errors (bad flags/args) are distinct
|
|
92
153
|
* from vault errors and from unknown crashes. */
|
|
93
154
|
function report(err) {
|
|
94
|
-
if (err instanceof
|
|
95
|
-
process.
|
|
96
|
-
process.exit(
|
|
155
|
+
if (err instanceof HelpRequest) {
|
|
156
|
+
process.stdout.write(err.usage + '\n');
|
|
157
|
+
process.exit(exitCodeFor(err));
|
|
97
158
|
}
|
|
98
|
-
if (err instanceof
|
|
99
|
-
process.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
process.exit(
|
|
159
|
+
if (err instanceof RenderedError) {
|
|
160
|
+
process[err.stream].write(err.output + '\n');
|
|
161
|
+
process.exit(exitCodeFor(err));
|
|
162
|
+
}
|
|
163
|
+
if (err instanceof UsageError || err instanceof HafezError) {
|
|
164
|
+
process.stderr.write(errorText(err));
|
|
165
|
+
process.exit(exitCodeFor(err));
|
|
105
166
|
}
|
|
106
167
|
failUnknown(err);
|
|
107
168
|
}
|
|
169
|
+
/** Does this error mean the native module failed to load/build?
|
|
170
|
+
*
|
|
171
|
+
* Matched on the MESSAGE only, and only on the messages a load/ABI failure
|
|
172
|
+
* actually produces. The old test also scanned the stack for "better-sqlite3",
|
|
173
|
+
* which every SqliteError's stack contains — so a plain query error ("near
|
|
174
|
+
* 'abc': syntax error") printed reinstall advice instead of itself. */
|
|
175
|
+
export function isBindingsFailure(err) {
|
|
176
|
+
const message = err?.message;
|
|
177
|
+
if (typeof message !== 'string')
|
|
178
|
+
return false;
|
|
179
|
+
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);
|
|
180
|
+
}
|
|
108
181
|
// Terminal handler for non-Hafez errors. Special-cases unbuilt better-sqlite3
|
|
109
182
|
// bindings: npm 11+ blocks install scripts by default, so `npm install -g
|
|
110
183
|
// hafez` reports success with the native module unbuilt and the first indexed
|
|
111
184
|
// command dies here — guide instead of stack-tracing.
|
|
112
185
|
function failUnknown(err) {
|
|
113
186
|
const e = err;
|
|
114
|
-
if (
|
|
187
|
+
if (isBindingsFailure(e)) {
|
|
115
188
|
process.stderr.write('Error: the SQLite bindings (better-sqlite3) are not built.\n' +
|
|
116
189
|
'npm 11+ blocks install scripts by default, so the install reports success\n' +
|
|
117
190
|
'while skipping the native build. Fix with:\n\n' +
|
|
@@ -121,108 +194,95 @@ function failUnknown(err) {
|
|
|
121
194
|
process.stderr.write(`Error: ${e.message}\n`);
|
|
122
195
|
process.exit(1);
|
|
123
196
|
}
|
|
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
|
-
|
|
197
|
+
function versionLine() {
|
|
198
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
199
|
+
const pkg = JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf-8'));
|
|
200
|
+
return `hafez ${pkg.version}`;
|
|
201
|
+
}
|
|
202
|
+
/** Extract the global flags (--vault <path>, --json) from anywhere in argv.
|
|
203
|
+
* Exported for the in-process dispatch tests. A flag sitting where the vault
|
|
204
|
+
* path should be means the value is missing — never consume it (the same
|
|
205
|
+
* value-position rule parseArgs applies to declared command flags). */
|
|
206
|
+
export function extractGlobalFlags(args) {
|
|
207
|
+
const rest = [];
|
|
208
|
+
let vaultFlag;
|
|
209
|
+
let json = false;
|
|
210
|
+
for (let i = 0; i < args.length; i++) {
|
|
211
|
+
const a = args[i];
|
|
212
|
+
if (a === '--vault') {
|
|
213
|
+
const v = args[i + 1];
|
|
214
|
+
if (v === undefined || v.startsWith('--'))
|
|
215
|
+
throw new UsageError('--vault requires a path value');
|
|
216
|
+
// The old one-shot extraction left a second --vault in the command args
|
|
217
|
+
// where the declared-flag parser rejected it — keep failing loudly
|
|
218
|
+
// rather than silently letting the last one win (a mutating command
|
|
219
|
+
// would write to a different vault than the first flag named).
|
|
220
|
+
if (vaultFlag !== undefined)
|
|
221
|
+
throw new UsageError('--vault specified more than once');
|
|
222
|
+
vaultFlag = v;
|
|
223
|
+
i++;
|
|
224
|
+
}
|
|
225
|
+
else if (a === '--json') {
|
|
226
|
+
json = true;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
rest.push(a);
|
|
230
|
+
}
|
|
147
231
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
232
|
+
return { rest, vaultFlag, json };
|
|
233
|
+
}
|
|
234
|
+
/** Is this the SessionStart hook's form, `onboard --block`? Read from the raw
|
|
235
|
+
* args, the token after `--vault` skipped, so it holds when parsing them fails. */
|
|
236
|
+
export function isBlockHook(args) {
|
|
237
|
+
const command = args.find((a, i) => !a.startsWith('-') && args[i - 1] !== '--vault');
|
|
238
|
+
return command === 'onboard' && args.includes('--block');
|
|
239
|
+
}
|
|
240
|
+
export async function main(argv) {
|
|
241
|
+
// The hook must never block a session: under `onboard --block` any failure
|
|
242
|
+
// at all, a bad flag included, prints nothing and exits 0 (#132).
|
|
243
|
+
const hook = isBlockHook(argv.slice(2));
|
|
244
|
+
try {
|
|
245
|
+
const { rest: args, vaultFlag, json } = extractGlobalFlags(argv.slice(2));
|
|
246
|
+
const command = args.shift();
|
|
247
|
+
if (!command || command === '--help' || command === '-h') {
|
|
248
|
+
process.stdout.write(USAGE);
|
|
152
249
|
return;
|
|
153
250
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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');
|
|
251
|
+
const name = command === '--version' || command === '-v' ? 'version' : command;
|
|
252
|
+
const spec = Object.hasOwn(COMMANDS, name) ? COMMANDS[name] : undefined;
|
|
253
|
+
if (!spec) {
|
|
254
|
+
// `list` is the command agents guess for listing the vault (#30).
|
|
255
|
+
const hint = command === 'list' ? '. To list the vault: hafez query --filter active' : '';
|
|
256
|
+
process.stderr.write(`Unknown command: ${command}${hint}\n${USAGE}`);
|
|
257
|
+
process.exit(1);
|
|
167
258
|
}
|
|
168
|
-
|
|
169
|
-
|
|
259
|
+
if (spec.phase === 'pre-vault') {
|
|
260
|
+
const out = await spec.run(args, { json, vaultFlag });
|
|
261
|
+
if (out !== null)
|
|
262
|
+
process.stdout.write(out.endsWith('\n') ? out : out + '\n');
|
|
263
|
+
return;
|
|
170
264
|
}
|
|
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
|
-
}
|
|
265
|
+
// Resolve vault (loaded here, not at module top: the pre-vault commands
|
|
266
|
+
// above return without ever needing the vault machinery)
|
|
267
|
+
const { resolveVaultPath, noVaultMessage } = await import('./resolve-vault.js');
|
|
268
|
+
const resolution = resolveVaultPath(vaultFlag);
|
|
269
|
+
if (resolution === null) {
|
|
270
|
+
process.stderr.write(noVaultMessage());
|
|
271
|
+
process.exit(1);
|
|
210
272
|
}
|
|
211
|
-
|
|
212
|
-
|
|
273
|
+
if (typeof resolution === 'object' && 'error' in resolution) {
|
|
274
|
+
process.stderr.write(`Error: ${resolution.error}\n`);
|
|
275
|
+
process.exit(1);
|
|
213
276
|
}
|
|
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 });
|
|
277
|
+
const vaultPath = resolution;
|
|
278
|
+
// First touch of the core — and so of the native SQLite binding. A
|
|
279
|
+
// failure to load it lands in the catch below and is recognised by
|
|
280
|
+
// isBindingsFailure, exactly as it was when this import was static.
|
|
281
|
+
const { createHafez } = await import('../index.js');
|
|
282
|
+
const os = createHafez({ vaultPath, git: { push: false } });
|
|
283
|
+
const result = await spec.run(os, args, { json, vaultPath });
|
|
224
284
|
// Auto-sync after mutating commands
|
|
225
|
-
if (
|
|
285
|
+
if (spec.mutating) {
|
|
226
286
|
try {
|
|
227
287
|
await os.sync();
|
|
228
288
|
}
|
|
@@ -236,6 +296,8 @@ export async function main(argv) {
|
|
|
236
296
|
process.stdout.write(result + '\n');
|
|
237
297
|
}
|
|
238
298
|
catch (err) {
|
|
299
|
+
if (hook)
|
|
300
|
+
return;
|
|
239
301
|
report(err);
|
|
240
302
|
}
|
|
241
303
|
}
|