hafez 1.0.1 → 1.1.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 +29 -17
- package/dist/cli/commands.d.ts +4 -8
- package/dist/cli/commands.js +12 -65
- package/dist/cli/format.js +5 -0
- package/dist/cli/index.js +11 -66
- package/dist/cli/onboard.d.ts +2 -0
- package/dist/cli/onboard.js +88 -0
- package/dist/db.js +48 -32
- package/dist/export-okf.js +1 -1
- package/dist/git.d.ts +7 -0
- package/dist/git.js +175 -163
- package/dist/index.js +77 -16
- package/dist/merge.js +36 -9
- package/dist/types.d.ts +1 -3
- package/dist/vault.js +12 -1
- package/package.json +6 -3
- package/skill/SKILL.md +1 -1
- package/dist/migrate-knowledge-v2.d.ts +0 -11
- package/dist/migrate-knowledge-v2.js +0 -158
- package/dist/migrate-next-actions.d.ts +0 -6
- package/dist/migrate-next-actions.js +0 -32
- package/dist/migrate-types.d.ts +0 -11
- package/dist/migrate-types.js +0 -142
package/README.md
CHANGED
|
@@ -16,22 +16,23 @@ Claude Code (or any agent with shell access):
|
|
|
16
16
|
```text
|
|
17
17
|
Set up Hafez, an agent-native knowledge vault (github.com/shrvn-r/hafez):
|
|
18
18
|
|
|
19
|
-
1. Install the CLI (needs Node.js 22+ and git):
|
|
20
|
-
npm
|
|
21
|
-
|
|
22
|
-
(npm 12 blocks URL-tarball installs and install scripts by default;
|
|
23
|
-
the SQLite bindings need their install script).
|
|
19
|
+
1. Install the CLI (needs Node.js 22+ and git): npm install -g hafez
|
|
20
|
+
On npm 12+, append --allow-scripts=better-sqlite3 (npm 12 blocks install
|
|
21
|
+
scripts by default; the SQLite bindings need theirs).
|
|
24
22
|
2. Check git identity is configured (git config user.name and user.email);
|
|
25
23
|
if not, ask me for a name and email and set them with git config --global.
|
|
26
24
|
3. Create my vault — a git repo with entities/ and knowledge/ directories and an
|
|
27
25
|
initial commit. Suggest ~/vault but ask me where I want it first.
|
|
28
26
|
4. Register it: hafez init --register <vault-path>
|
|
29
|
-
5.
|
|
30
|
-
package ("$(npm root -g)/hafez/skill/SKILL.md") to
|
|
27
|
+
5. If you are Claude Code, install the bundled skill: copy skill/SKILL.md
|
|
28
|
+
from the installed package ("$(npm root -g)/hafez/skill/SKILL.md") to
|
|
29
|
+
~/.claude/skills/hafez/SKILL.md. Any other harness: skip this — step 7
|
|
30
|
+
covers integration.
|
|
31
31
|
6. Read hafez help --agent, then verify end-to-end: create a test capture,
|
|
32
32
|
hafez read it back, and show me hafez stats.
|
|
33
|
-
7.
|
|
34
|
-
|
|
33
|
+
7. Run hafez onboard and follow what it prints: interview me to seed the
|
|
34
|
+
vault with my current projects, then help me choose how deeply to
|
|
35
|
+
integrate Hafez into my agent setup.
|
|
35
36
|
```
|
|
36
37
|
|
|
37
38
|
From then on, just mention your projects by name in any session — the skill
|
|
@@ -43,8 +44,8 @@ The same thing by hand:
|
|
|
43
44
|
|
|
44
45
|
```bash
|
|
45
46
|
# Install (Node.js 22+ and git required).
|
|
46
|
-
# On npm 12+, append: --allow-
|
|
47
|
-
npm install -g
|
|
47
|
+
# On npm 12+, append: --allow-scripts=better-sqlite3
|
|
48
|
+
npm install -g hafez
|
|
48
49
|
|
|
49
50
|
# One-time, if you've never used git on this machine:
|
|
50
51
|
# git config --global user.name "Your Name"
|
|
@@ -56,14 +57,18 @@ cd ~/vault && git init && git commit --allow-empty -m "init vault"
|
|
|
56
57
|
hafez init --register ~/vault # remember this vault (one-time)
|
|
57
58
|
hafez init # show current vault resolution
|
|
58
59
|
|
|
59
|
-
# Claude Code
|
|
60
|
+
# Claude Code only (optional): the bundled skill
|
|
60
61
|
mkdir -p ~/.claude/skills/hafez
|
|
61
62
|
cp "$(npm root -g)/hafez/skill/SKILL.md" ~/.claude/skills/hafez/SKILL.md
|
|
63
|
+
|
|
64
|
+
# Then, in your agent — any harness: "run hafez onboard and follow it" —
|
|
65
|
+
# guided first run (seed the vault with your current projects, choose an
|
|
66
|
+
# integration level)
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
Prefer installing without the npm registry? The GitHub release tarball is the
|
|
70
|
+
same artifact: `npm install -g https://github.com/shrvn-r/hafez/releases/latest/download/hafez.tgz`
|
|
71
|
+
(npm 12+ additionally needs `--allow-remote=root`).
|
|
67
72
|
|
|
68
73
|
## Quick start
|
|
69
74
|
|
|
@@ -133,6 +138,7 @@ Links (`parent`, `related`) are validated — they must point at slugs that exis
|
|
|
133
138
|
| `hafez changelog --since 7.days.ago` | Git-derived change history |
|
|
134
139
|
| `hafez sync` | Pull remote, push local commits (semantic merge on conflicts) |
|
|
135
140
|
| `hafez validate` / `hafez index rebuild` | Integrity check / rebuild the read index |
|
|
141
|
+
| `hafez onboard` | Agent-directed first run: seeding interview + integration setup |
|
|
136
142
|
| `hafez schema [op] [--examples]` | Machine-readable JSON schema for every operation |
|
|
137
143
|
| `hafez export --okf [--out <dir>]` | Read-only OKF v0.1 bundle export |
|
|
138
144
|
|
|
@@ -168,10 +174,16 @@ Writes are serialized through an internal mutex, so a single `Hafez` instance is
|
|
|
168
174
|
|
|
169
175
|
## Agent integration
|
|
170
176
|
|
|
171
|
-
Hafez is built to be operated by agents
|
|
177
|
+
Hafez is built to be operated by agents — any agent. The CLI is the whole
|
|
178
|
+
interface, so every harness that can run shell commands integrates the same
|
|
179
|
+
way: through its instructions file — CLAUDE.md for Claude Code, AGENTS.md
|
|
180
|
+
for everything on the [AGENTS.md standard](https://agents.md) (Codex, Cursor,
|
|
181
|
+
Copilot, …). This repo follows the same convention: `AGENTS.md` is canonical
|
|
182
|
+
and `CLAUDE.md` imports it.
|
|
172
183
|
|
|
173
|
-
-
|
|
184
|
+
- **`hafez onboard`** — an agent-directed first-run guide: a seeding interview for the empty vault, then a choice of integration level (an always-active block for the harness's global instructions file, or invoke-only).
|
|
174
185
|
- **`hafez help --agent`** — a complete API reference designed to be loaded into an agent's context.
|
|
186
|
+
- **Claude Code skill** — an optional adapter for one harness: the package bundles a skill (installed in [Setup](#setup)) covering the full session lifecycle: loading context at session start, vault ops mid-session, and a session-end digest.
|
|
175
187
|
- **`hafez schema <op> --examples`** — JSON schemas with working examples for every batch operation, so agents can self-correct.
|
|
176
188
|
- **`hafez batch`** — apply a whole session's worth of updates atomically from a single JSON payload. Validation errors report every bad op at once with did-you-mean hints.
|
|
177
189
|
- **Markdown-native output** by default, `--json` everywhere for machine consumption.
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -10,10 +10,7 @@ export declare function cmdCreate(os: Hafez, args: string[], opts?: CommandOpts)
|
|
|
10
10
|
export declare function cmdUpdate(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
11
11
|
export declare function cmdLink(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
12
12
|
export declare function cmdUnlink(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
13
|
-
/**
|
|
14
|
-
* Top-level capture command — aliases to the same handler used by
|
|
15
|
-
* `hafez create inbox`. Added to align the CLI verb with the batch op name.
|
|
16
|
-
*/
|
|
13
|
+
/** Top-level capture command — aligns the CLI verb with the batch op name. */
|
|
17
14
|
export declare function cmdCapture(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
18
15
|
export declare function cmdPromote(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
19
16
|
export declare function cmdValidate(os: Hafez, _args: string[], opts?: CommandOpts): Promise<string>;
|
|
@@ -107,27 +104,26 @@ export declare const BatchOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
107
104
|
slug: z.ZodString;
|
|
108
105
|
target: z.ZodString;
|
|
109
106
|
relation: z.ZodEnum<{
|
|
110
|
-
related: "related";
|
|
111
107
|
parent: "parent";
|
|
108
|
+
related: "related";
|
|
112
109
|
}>;
|
|
113
110
|
}, z.core.$strict>, z.ZodObject<{
|
|
114
111
|
op: z.ZodLiteral<"unlink">;
|
|
115
112
|
slug: z.ZodString;
|
|
116
113
|
target: z.ZodString;
|
|
117
114
|
relation: z.ZodEnum<{
|
|
118
|
-
related: "related";
|
|
119
115
|
parent: "parent";
|
|
116
|
+
related: "related";
|
|
120
117
|
}>;
|
|
121
118
|
}, z.core.$strict>, z.ZodObject<{
|
|
122
119
|
op: z.ZodLiteral<"promote">;
|
|
123
120
|
slug: z.ZodString;
|
|
124
121
|
target: z.ZodEnum<{
|
|
125
122
|
entity: "entity";
|
|
126
|
-
knowledge: "knowledge";
|
|
127
123
|
project: "project";
|
|
124
|
+
knowledge: "knowledge";
|
|
128
125
|
}>;
|
|
129
126
|
}, z.core.$strict>], "op">;
|
|
130
127
|
export declare function parseBatchInput(json: string): BatchOperation[];
|
|
131
128
|
export declare function cmdBatch(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
132
129
|
export declare function cmdDigest(os: Hafez, _args: string[], _opts?: CommandOpts): Promise<string>;
|
|
133
|
-
export declare function cmdMigrateKnowledgeV2(args: string[], vaultPath: string): Promise<void>;
|
package/dist/cli/commands.js
CHANGED
|
@@ -5,7 +5,6 @@ import { ENTITY_TYPES, ENTITY_STATUSES, SESSION_LOG_TYPES, CONFIDENCE_LEVELS, VA
|
|
|
5
5
|
import { formatBatchError } from './batch-errors.js';
|
|
6
6
|
import { parseDigestInput, digest } from '../digest.js';
|
|
7
7
|
import { formatEntityHeader, formatKnowledgeHeader, formatQueryTable, formatKnowledgeTable, formatSearchResults, formatValidation, formatStats, formatChangelog } from './format.js';
|
|
8
|
-
import { migrateKnowledgeV2 } from '../migrate-knowledge-v2.js';
|
|
9
8
|
function jsonOut(data) {
|
|
10
9
|
return JSON.stringify(data, null, 2);
|
|
11
10
|
}
|
|
@@ -105,16 +104,7 @@ export async function cmdSearch(os, args, opts = {}) {
|
|
|
105
104
|
export async function cmdCreate(os, args, opts = {}) {
|
|
106
105
|
const kind = args[0];
|
|
107
106
|
if (!kind)
|
|
108
|
-
throw new Error('Usage: hafez create <entity|knowledge
|
|
109
|
-
if (kind === 'inbox') {
|
|
110
|
-
const name = positionalArgs(args.slice(1)).join(' ');
|
|
111
|
-
if (!name)
|
|
112
|
-
throw new Error('Usage: hafez create inbox <name> [--notes "text"]');
|
|
113
|
-
const notes = getFlag(args, '--notes');
|
|
114
|
-
process.stderr.write('Warning: `hafez create inbox` is deprecated. Use `hafez capture <name>` instead.\n');
|
|
115
|
-
const slug = await os.capture(name, notes);
|
|
116
|
-
return opts.json ? jsonOut({ slug }) : slug;
|
|
117
|
-
}
|
|
107
|
+
throw new Error('Usage: hafez create <entity|knowledge> <name> [options]');
|
|
118
108
|
const name = positionalArgs(args.slice(1)).join(' ');
|
|
119
109
|
if (!name)
|
|
120
110
|
throw new Error(`Usage: hafez create ${kind} <name> [options]`);
|
|
@@ -147,7 +137,7 @@ export async function cmdCreate(os, args, opts = {}) {
|
|
|
147
137
|
});
|
|
148
138
|
return opts.json ? jsonOut({ slug }) : slug;
|
|
149
139
|
}
|
|
150
|
-
throw new Error(`Unknown kind: ${kind}. Use entity
|
|
140
|
+
throw new Error(`Unknown kind: ${kind}. Use entity or knowledge.`);
|
|
151
141
|
}
|
|
152
142
|
export async function cmdUpdate(os, args, opts = {}) {
|
|
153
143
|
const slug = args[0];
|
|
@@ -157,13 +147,11 @@ export async function cmdUpdate(os, args, opts = {}) {
|
|
|
157
147
|
const status = getFlag(args, '--status');
|
|
158
148
|
if (status)
|
|
159
149
|
fields.status = status;
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (nextAction === '')
|
|
166
|
-
fields.clear_actions = true;
|
|
150
|
+
// Removed flag must fail loudly, not silently drop the value: older
|
|
151
|
+
// installed skills/scripts still pass it, and "Updated <slug>" with the
|
|
152
|
+
// action discarded is a false success.
|
|
153
|
+
if (args.includes('--next-action')) {
|
|
154
|
+
throw new Error('--next-action was removed in v1.0.2. Use --add-action (and --clear-actions) instead.');
|
|
167
155
|
}
|
|
168
156
|
const currentState = getFlag(args, '--current-state');
|
|
169
157
|
if (currentState)
|
|
@@ -235,18 +223,10 @@ export async function cmdUpdate(os, args, opts = {}) {
|
|
|
235
223
|
}
|
|
236
224
|
return lines.join('\n');
|
|
237
225
|
}
|
|
238
|
-
/**
|
|
239
|
-
* Parse link/unlink args, supporting both the legacy positional form
|
|
240
|
-
* (`hafez link <slug> <target> <parent|related>`) and the new
|
|
241
|
-
* `--relation <value>` named flag. Positional remains accepted as a
|
|
242
|
-
* deprecated alias — emits a one-line warning to stderr.
|
|
243
|
-
*/
|
|
244
226
|
function parseLinkArgs(args, verb) {
|
|
245
|
-
const
|
|
227
|
+
const relation = getFlag(args, '--relation');
|
|
246
228
|
// After getFlag the relation value is still at its original positional slot
|
|
247
|
-
// (getFlag does NOT splice), so strip it manually
|
|
248
|
-
// mistaken for a positional if someone passes both forms. Here we only
|
|
249
|
-
// consume the flag once above.
|
|
229
|
+
// (getFlag does NOT splice), so strip it manually before reading positionals.
|
|
250
230
|
const positionals = args.filter((a, i) => {
|
|
251
231
|
if (a === '--relation')
|
|
252
232
|
return false;
|
|
@@ -254,18 +234,10 @@ function parseLinkArgs(args, verb) {
|
|
|
254
234
|
return false;
|
|
255
235
|
return !a.startsWith('--');
|
|
256
236
|
});
|
|
257
|
-
const [slug, target
|
|
258
|
-
if (!slug || !target) {
|
|
237
|
+
const [slug, target] = positionals;
|
|
238
|
+
if (!slug || !target || !relation) {
|
|
259
239
|
throw new Error(`Usage: hafez ${verb} <slug> <target> --relation <parent|related>`);
|
|
260
240
|
}
|
|
261
|
-
let relation = relationFlag;
|
|
262
|
-
if (!relation) {
|
|
263
|
-
if (!positionalRelation) {
|
|
264
|
-
throw new Error(`Usage: hafez ${verb} <slug> <target> --relation <parent|related>`);
|
|
265
|
-
}
|
|
266
|
-
relation = positionalRelation;
|
|
267
|
-
process.stderr.write(`Warning: positional relation is deprecated. Use: hafez ${verb} ${slug} ${target} --relation ${relation}\n`);
|
|
268
|
-
}
|
|
269
241
|
if (!['parent', 'related'].includes(relation)) {
|
|
270
242
|
throw new Error(`Invalid relation: ${relation}. Must be parent or related.`);
|
|
271
243
|
}
|
|
@@ -281,10 +253,7 @@ export async function cmdUnlink(os, args, opts = {}) {
|
|
|
281
253
|
await os.unlink(slug, target, relation);
|
|
282
254
|
return opts.json ? jsonOut({ slug, target, relation }) : `Unlinked ${slug} — ${target} (${relation})`;
|
|
283
255
|
}
|
|
284
|
-
/**
|
|
285
|
-
* Top-level capture command — aliases to the same handler used by
|
|
286
|
-
* `hafez create inbox`. Added to align the CLI verb with the batch op name.
|
|
287
|
-
*/
|
|
256
|
+
/** Top-level capture command — aligns the CLI verb with the batch op name. */
|
|
288
257
|
export async function cmdCapture(os, args, opts = {}) {
|
|
289
258
|
const name = positionalArgs(args).join(' ');
|
|
290
259
|
if (!name)
|
|
@@ -584,28 +553,6 @@ export async function cmdDigest(os, _args, _opts = {}) {
|
|
|
584
553
|
const ops = digest(result.data, existingSlugs);
|
|
585
554
|
return JSON.stringify(ops, null, 2);
|
|
586
555
|
}
|
|
587
|
-
export async function cmdMigrateKnowledgeV2(args, vaultPath) {
|
|
588
|
-
const apply = args.includes('--apply');
|
|
589
|
-
const report = migrateKnowledgeV2(vaultPath, { apply });
|
|
590
|
-
console.log(`## Migration: knowledge-v2 ${apply ? '(APPLIED)' : '(dry run)'}`);
|
|
591
|
-
console.log(`- Sessions to move: ${report.sessionsToMove.length}`);
|
|
592
|
-
for (const f of report.sessionsToMove)
|
|
593
|
-
console.log(` - ${f}`);
|
|
594
|
-
console.log(`- Empty notes to delete: ${report.emptyToDelete.length}`);
|
|
595
|
-
for (const f of report.emptyToDelete)
|
|
596
|
-
console.log(` - ${f}`);
|
|
597
|
-
console.log(`- ## Insight → ## Synthesis: ${report.insightToRename.length}`);
|
|
598
|
-
for (const f of report.insightToRename)
|
|
599
|
-
console.log(` - ${f}`);
|
|
600
|
-
console.log(`- ## Related to generate: ${report.relatedToGenerate.length}`);
|
|
601
|
-
for (const f of report.relatedToGenerate)
|
|
602
|
-
console.log(` - ${f}`);
|
|
603
|
-
console.log(`- .gitignore needs update: ${report.gitignoreNeeded}`);
|
|
604
|
-
if (apply && report.gitCommitFailed)
|
|
605
|
-
console.log('⚠ Warning: git commit failed — changes are applied but not committed.');
|
|
606
|
-
if (!apply)
|
|
607
|
-
console.log('\nRun with --apply to execute migration.');
|
|
608
|
-
}
|
|
609
556
|
// --- Arg helpers ---
|
|
610
557
|
function getFlag(args, flag) {
|
|
611
558
|
const idx = args.indexOf(flag);
|
package/dist/cli/format.js
CHANGED
|
@@ -104,6 +104,11 @@ export function formatSearchResults(results) {
|
|
|
104
104
|
}
|
|
105
105
|
export function formatStats(stats) {
|
|
106
106
|
const lines = [];
|
|
107
|
+
const totalEntities = stats.counts.active + stats.counts.paused + stats.counts.done;
|
|
108
|
+
if (totalEntities === 0 && stats.knowledge_count === 0) {
|
|
109
|
+
lines.push('Vault is empty — run `hafez onboard` for the guided first run (seeding interview + agent integration).');
|
|
110
|
+
lines.push('');
|
|
111
|
+
}
|
|
107
112
|
lines.push('## Status Counts');
|
|
108
113
|
lines.push(`Active: ${stats.counts.active} | Paused: ${stats.counts.paused} | Done: ${stats.counts.done}`);
|
|
109
114
|
lines.push('\n## Type Counts');
|
package/dist/cli/index.js
CHANGED
|
@@ -4,9 +4,10 @@ import { dirname, join, resolve } from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { createHafez } from '../index.js';
|
|
6
6
|
import { HafezError } from '../types.js';
|
|
7
|
-
import { cmdRead, cmdQuery, cmdSearch, cmdCreate, cmdUpdate, cmdLink, cmdUnlink, cmdPromote, cmdValidate, cmdIndexRebuild, cmdBatch, cmdSync, cmdStats, cmdChangelog, cmdDigest,
|
|
7
|
+
import { cmdRead, cmdQuery, cmdSearch, cmdCreate, cmdUpdate, cmdLink, cmdUnlink, cmdPromote, cmdValidate, cmdIndexRebuild, cmdBatch, cmdSync, cmdStats, cmdChangelog, cmdDigest, cmdCapture } from './commands.js';
|
|
8
8
|
import { cmdSchema } from './commands-schema.js';
|
|
9
9
|
import { renderAgentHelp } from './help-agent.js';
|
|
10
|
+
import { renderOnboard } from './onboard.js';
|
|
10
11
|
import { resolveVaultPath, noVaultMessage, cmdInit } from './resolve-vault.js';
|
|
11
12
|
const COMMANDS = {
|
|
12
13
|
read: cmdRead,
|
|
@@ -38,7 +39,7 @@ Commands:
|
|
|
38
39
|
read <slug> Read entity or knowledge note
|
|
39
40
|
query [--filter ...] ... Filter entities/knowledge
|
|
40
41
|
search <terms> Full-text search
|
|
41
|
-
create <kind> <name> Create entity
|
|
42
|
+
create <kind> <name> Create entity or knowledge note
|
|
42
43
|
capture <name> [--notes] Quick inbox capture
|
|
43
44
|
promote <slug> <target> Promote capture→entity/project/knowledge, entity→project
|
|
44
45
|
update <slug> [--status] Update an entity or knowledge note
|
|
@@ -56,12 +57,8 @@ Commands:
|
|
|
56
57
|
digest Apply digest input (JSON stdin) → batch payload (JSON stdout)
|
|
57
58
|
export --okf [--out <dir>] Export vault as an OKF v0.1 bundle (read-only)
|
|
58
59
|
sync Pull remote changes and push local commits
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
migrate knowledge-v2 Migrate knowledge notes to v2 section structure (dry-run by default)
|
|
62
|
-
--apply Execute migration (writes files, does not commit)
|
|
63
|
-
link <slug> <target> <rel> Add a link
|
|
64
|
-
unlink <slug> <target> <rel> Remove a link
|
|
60
|
+
link <slug> <target> --relation <rel> Add a link
|
|
61
|
+
unlink <slug> <target> --relation <rel> Remove a link
|
|
65
62
|
stats Vault summary: counts, stale items, recents
|
|
66
63
|
changelog --since <when> Git-derived change history (e.g. --since 7.days.ago)
|
|
67
64
|
schema [op] [--examples] Machine-readable JSON schema for batch operations
|
|
@@ -69,6 +66,7 @@ Commands:
|
|
|
69
66
|
index rebuild Rebuild the SQLite index
|
|
70
67
|
init --register <path> Register an existing vault
|
|
71
68
|
init Show current vault resolution
|
|
69
|
+
onboard Agent-directed first run: seed the vault, pick integration level
|
|
72
70
|
help --agent Full API reference for agents
|
|
73
71
|
|
|
74
72
|
Global flags:
|
|
@@ -111,6 +109,11 @@ export async function main(argv) {
|
|
|
111
109
|
process.stdout.write(USAGE);
|
|
112
110
|
return;
|
|
113
111
|
}
|
|
112
|
+
// onboard runs before vault resolution — static agent-directed text
|
|
113
|
+
if (command === 'onboard') {
|
|
114
|
+
process.stdout.write(renderOnboard());
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
114
117
|
// schema runs before vault resolution — pure Zod introspection, no vault needed.
|
|
115
118
|
if (command === 'schema') {
|
|
116
119
|
try {
|
|
@@ -184,64 +187,6 @@ export async function main(argv) {
|
|
|
184
187
|
}
|
|
185
188
|
return;
|
|
186
189
|
}
|
|
187
|
-
// migrate runs before Hafez instance creation
|
|
188
|
-
if (command === 'migrate') {
|
|
189
|
-
const subcommand = args[0];
|
|
190
|
-
if (subcommand === 'next-actions') {
|
|
191
|
-
const { migrateNextActions } = await import('../migrate-next-actions.js');
|
|
192
|
-
const apply = args.includes('--apply');
|
|
193
|
-
const report = migrateNextActions(vaultPath, apply);
|
|
194
|
-
const lines = [];
|
|
195
|
-
if (!apply)
|
|
196
|
-
lines.push('DRY RUN — use --apply to execute');
|
|
197
|
-
lines.push(`Candidates: ${report.candidates}`);
|
|
198
|
-
if (apply)
|
|
199
|
-
lines.push(`Migrated: ${report.migrated}`);
|
|
200
|
-
for (const d of report.details)
|
|
201
|
-
lines.push(` ${d}`);
|
|
202
|
-
process.stdout.write(lines.join('\n') + '\n');
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (subcommand === 'types') {
|
|
206
|
-
const { migrateTypes } = await import('../migrate-types.js');
|
|
207
|
-
const apply = args.includes('--apply');
|
|
208
|
-
const report = migrateTypes(vaultPath, apply);
|
|
209
|
-
const lines = [];
|
|
210
|
-
if (!apply) {
|
|
211
|
-
lines.push('DRY RUN — use --apply to execute');
|
|
212
|
-
lines.push('');
|
|
213
|
-
}
|
|
214
|
-
lines.push(`Type renames: ${report.typeChanges}`);
|
|
215
|
-
lines.push(`Status renames: ${report.statusChanges}`);
|
|
216
|
-
lines.push(`Subtypes added: ${report.knowledgeSubtypeAdded}`);
|
|
217
|
-
lines.push(`Body restructures: ${report.bodySectionsRestructured}`);
|
|
218
|
-
lines.push(`Malformed (skipped): ${report.malformed.length}`);
|
|
219
|
-
if (report.malformed.length > 0) {
|
|
220
|
-
lines.push('');
|
|
221
|
-
lines.push('Malformed files (no type field):');
|
|
222
|
-
for (const f of report.malformed)
|
|
223
|
-
lines.push(` ⚠ ${f}`);
|
|
224
|
-
}
|
|
225
|
-
if (report.details.length > 0) {
|
|
226
|
-
lines.push('');
|
|
227
|
-
lines.push('Changes:');
|
|
228
|
-
for (const d of report.details)
|
|
229
|
-
lines.push(` ${d}`);
|
|
230
|
-
}
|
|
231
|
-
if (apply) {
|
|
232
|
-
lines.push('');
|
|
233
|
-
lines.push('Migration applied. Run `hafez index rebuild` to refresh the index.');
|
|
234
|
-
}
|
|
235
|
-
process.stdout.write(lines.join('\n') + '\n');
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (subcommand === 'knowledge-v2') {
|
|
239
|
-
await cmdMigrateKnowledgeV2(args.slice(1), vaultPath);
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
process.stderr.write('Usage: hafez migrate <next-actions|types|knowledge-v2> [--apply]\n');
|
|
243
|
-
process.exit(1);
|
|
244
|
-
}
|
|
245
190
|
// sync is handled specially — always runs, not gated by MUTATING_COMMANDS
|
|
246
191
|
if (command === 'sync') {
|
|
247
192
|
const os = createHafez({ vaultPath, git: { push: false } });
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const INTEGRATION_SNIPPET = "## Hafez \u2014 Session Memory\n\n- Session start: if the first message names a project or topic, load context\n with `hafez read <slug>` (or `hafez search \"<topic>\"`). Don't load\n speculatively.\n- Mid-session: new workstream -> create an entity. Durable insight -> create\n or update a knowledge note (search first). User mentions a project by\n name -> check if it's a Hafez entity. Use `hafez batch` for 2+ mutations.\n- Session end: record a digest (`hafez digest | hafez batch`) with the\n entities touched, decisions made, and a short narrative.\n- Knowledge boundary: useful beyond this repo/session -> vault knowledge\n note. Specific to one repo's code -> that repo's own docs, not the vault.";
|
|
2
|
+
export declare function renderOnboard(): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/cli/onboard.ts
|
|
2
|
+
//
|
|
3
|
+
// `hafez onboard` — agent-directed first-run guide. Same pattern as
|
|
4
|
+
// `hafez help --agent`: plain text printed for the agent operating on the
|
|
5
|
+
// user's behalf. Covers the two things a fresh install leaves open — seeding
|
|
6
|
+
// the empty vault and choosing how deeply the agent integrates.
|
|
7
|
+
//
|
|
8
|
+
// Static by design, and deliberately NOT part of the skill: the skill loads
|
|
9
|
+
// into every session forever, this is read once. Living in the CLI also means
|
|
10
|
+
// it works before the skill is installed.
|
|
11
|
+
export const INTEGRATION_SNIPPET = `## Hafez — Session Memory
|
|
12
|
+
|
|
13
|
+
- Session start: if the first message names a project or topic, load context
|
|
14
|
+
with \`hafez read <slug>\` (or \`hafez search "<topic>"\`). Don't load
|
|
15
|
+
speculatively.
|
|
16
|
+
- Mid-session: new workstream -> create an entity. Durable insight -> create
|
|
17
|
+
or update a knowledge note (search first). User mentions a project by
|
|
18
|
+
name -> check if it's a Hafez entity. Use \`hafez batch\` for 2+ mutations.
|
|
19
|
+
- Session end: record a digest (\`hafez digest | hafez batch\`) with the
|
|
20
|
+
entities touched, decisions made, and a short narrative.
|
|
21
|
+
- Knowledge boundary: useful beyond this repo/session -> vault knowledge
|
|
22
|
+
note. Specific to one repo's code -> that repo's own docs, not the vault.`;
|
|
23
|
+
export function renderOnboard() {
|
|
24
|
+
return `# Hafez — First Run
|
|
25
|
+
|
|
26
|
+
You are an agent reading this on behalf of your user. Walk them through the
|
|
27
|
+
steps below conversationally — do not dump this document on them.
|
|
28
|
+
|
|
29
|
+
Prerequisites: \`hafez init\` resolves a vault, and you have read
|
|
30
|
+
\`hafez help --agent\` (do that now if not). The CLI is the whole interface —
|
|
31
|
+
no other pieces are required. If your harness is Claude Code, also install
|
|
32
|
+
the bundled skill — see the README Setup (github.com/shrvn-r/hafez).
|
|
33
|
+
|
|
34
|
+
## Step 1 — Seed the vault
|
|
35
|
+
|
|
36
|
+
An empty vault gives the agent nothing to trigger on, so the memory loop
|
|
37
|
+
never starts. The goal here is a small set of high-quality entries — not a
|
|
38
|
+
bulk import.
|
|
39
|
+
|
|
40
|
+
1. Interview the user: "What are you actively working on right now?" Aim for
|
|
41
|
+
3-7 projects or areas. For each one, draw out:
|
|
42
|
+
- a one-line description (\`description\`)
|
|
43
|
+
- current state / handoff context, 2-4 sentences (\`brief\`)
|
|
44
|
+
- 1-2 concrete next actions (\`add_action\`)
|
|
45
|
+
2. Offer to mine sources they already have — a repo's README, existing notes,
|
|
46
|
+
a TODO list. Read what they point you at, propose entries, and let them
|
|
47
|
+
approve before writing anything.
|
|
48
|
+
3. Anything vague, aspirational, or someday-maybe becomes a capture, not a
|
|
49
|
+
project. Captures are the inbox; promotion comes later. When in doubt,
|
|
50
|
+
capture.
|
|
51
|
+
4. Write everything in ONE \`hafez batch\` call (see
|
|
52
|
+
\`hafez schema create-entity --examples\`), then show the user
|
|
53
|
+
\`hafez stats\`.
|
|
54
|
+
|
|
55
|
+
Quality bar: every project entry should let a future agent with zero context
|
|
56
|
+
pick the work up from the brief alone. If a brief would not do that, ask one
|
|
57
|
+
more question.
|
|
58
|
+
|
|
59
|
+
## Step 2 — Choose the integration level
|
|
60
|
+
|
|
61
|
+
Ask the user how they want Hafez wired into their sessions. Recommend the
|
|
62
|
+
first option.
|
|
63
|
+
|
|
64
|
+
RECOMMENDED — always active. Add the block below to the user's global agent
|
|
65
|
+
instructions file — Claude Code: \`~/.claude/CLAUDE.md\`; any harness that
|
|
66
|
+
follows the AGENTS.md standard (Codex, Cursor, Copilot, ...): its global
|
|
67
|
+
AGENTS.md, e.g. \`~/.codex/AGENTS.md\` for Codex CLI. This makes memory
|
|
68
|
+
automatic: context loads at session start, the vault is updated as work
|
|
69
|
+
happens, and a digest records each session. Show them the block, get a yes,
|
|
70
|
+
then append it verbatim:
|
|
71
|
+
|
|
72
|
+
${INTEGRATION_SNIPPET}
|
|
73
|
+
|
|
74
|
+
FALLBACK — invoke only. If the user prefers to stay in control, add nothing.
|
|
75
|
+
Explain how invocation works: the agent touches the vault only when they
|
|
76
|
+
mention Hafez or name a project ("what am I working on?", "read <slug>").
|
|
77
|
+
Nothing is loaded or written unless asked. They can upgrade later
|
|
78
|
+
by re-running \`hafez onboard\`.
|
|
79
|
+
|
|
80
|
+
## Step 3 — Hand over
|
|
81
|
+
|
|
82
|
+
Finish with three example asks tailored to what was just seeded, e.g.:
|
|
83
|
+
|
|
84
|
+
- "What am I working on?"
|
|
85
|
+
- "Read <their-project-slug> and pick up where we left off."
|
|
86
|
+
- "Capture: <idea> — I'll triage it later."
|
|
87
|
+
`;
|
|
88
|
+
}
|
package/dist/db.js
CHANGED
|
@@ -88,7 +88,9 @@ function hashContent(content) {
|
|
|
88
88
|
}
|
|
89
89
|
function scanDir(dirPath) {
|
|
90
90
|
try {
|
|
91
|
-
|
|
91
|
+
// Skip dotfiles: their slugs are rejected by resolveFilePath, so indexing
|
|
92
|
+
// them would list entities that read/update can never reach.
|
|
93
|
+
return readdirSync(dirPath).filter(f => f.endsWith('.md') && !f.startsWith('.')).map(f => join(dirPath, f));
|
|
92
94
|
}
|
|
93
95
|
catch {
|
|
94
96
|
return [];
|
|
@@ -401,46 +403,65 @@ export function createIndex(vaultPath, opts) {
|
|
|
401
403
|
// The db files are a disposable local cache — keep them out of the user's
|
|
402
404
|
// `git status` without dirtying their vault with a tracked .gitignore.
|
|
403
405
|
ensureLocalExclude(vaultPath);
|
|
406
|
+
function removeDbFiles() {
|
|
407
|
+
// The -wal/-shm siblings must go too: a fresh db next to a stale WAL is
|
|
408
|
+
// itself corrupt.
|
|
409
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
410
|
+
try {
|
|
411
|
+
unlinkSync(dbPath + suffix);
|
|
412
|
+
}
|
|
413
|
+
catch { /* may not exist */ }
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function openDb() {
|
|
417
|
+
const fresh = new Database(dbPath);
|
|
418
|
+
try {
|
|
419
|
+
fresh.pragma('journal_mode = WAL');
|
|
420
|
+
fresh.pragma('busy_timeout = 5000');
|
|
421
|
+
fresh.pragma('foreign_keys = ON');
|
|
422
|
+
fresh.exec(SCHEMA);
|
|
423
|
+
return fresh;
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
// Close before the caller unlinks — on filesystems without POSIX
|
|
427
|
+
// delete-while-open semantics, unlinking an open db fails silently and
|
|
428
|
+
// recovery would just reopen the same corrupt file.
|
|
429
|
+
try {
|
|
430
|
+
fresh.close();
|
|
431
|
+
}
|
|
432
|
+
catch { /* already unusable */ }
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
404
436
|
let db;
|
|
405
437
|
try {
|
|
406
|
-
db =
|
|
438
|
+
db = openDb();
|
|
407
439
|
}
|
|
408
440
|
catch {
|
|
409
|
-
// Corrupted DB
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
441
|
+
// Corrupted DB (garbage bytes, truncated WAL checkpoint). better-sqlite3
|
|
442
|
+
// doesn't touch the file until the first statement, so corruption surfaces
|
|
443
|
+
// at the pragma/exec inside openDb, not at the constructor — the whole
|
|
444
|
+
// open sequence must be inside this catch. The index is a disposable
|
|
445
|
+
// cache: delete and rebuild.
|
|
446
|
+
removeDbFiles();
|
|
447
|
+
db = openDb();
|
|
415
448
|
}
|
|
416
|
-
db.pragma('journal_mode = WAL');
|
|
417
|
-
db.pragma('busy_timeout = 5000');
|
|
418
|
-
db.pragma('foreign_keys = ON');
|
|
419
|
-
db.exec(SCHEMA);
|
|
420
449
|
// Schema migration: if next_action_count column missing, delete DB and recreate
|
|
421
450
|
try {
|
|
422
451
|
db.prepare('SELECT next_action_count FROM items LIMIT 0').run();
|
|
423
452
|
}
|
|
424
453
|
catch {
|
|
425
454
|
db.close();
|
|
426
|
-
|
|
427
|
-
db =
|
|
428
|
-
db.pragma('journal_mode = WAL');
|
|
429
|
-
db.pragma('busy_timeout = 5000');
|
|
430
|
-
db.pragma('foreign_keys = ON');
|
|
431
|
-
db.exec(SCHEMA);
|
|
455
|
+
removeDbFiles();
|
|
456
|
+
db = openDb();
|
|
432
457
|
}
|
|
433
458
|
// Schema migration: if domain_entity column exists, delete DB and recreate
|
|
434
459
|
try {
|
|
435
460
|
db.prepare('SELECT domain_entity FROM items LIMIT 0').run();
|
|
436
461
|
// Column exists — old schema, needs rebuild
|
|
437
462
|
db.close();
|
|
438
|
-
|
|
439
|
-
db =
|
|
440
|
-
db.pragma('journal_mode = WAL');
|
|
441
|
-
db.pragma('busy_timeout = 5000');
|
|
442
|
-
db.pragma('foreign_keys = ON');
|
|
443
|
-
db.exec(SCHEMA);
|
|
463
|
+
removeDbFiles();
|
|
464
|
+
db = openDb();
|
|
444
465
|
}
|
|
445
466
|
catch {
|
|
446
467
|
// Column doesn't exist — correct schema, continue
|
|
@@ -449,12 +470,8 @@ export function createIndex(vaultPath, opts) {
|
|
|
449
470
|
const versionRow = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
450
471
|
if (!versionRow || versionRow.value !== SCHEMA_VERSION) {
|
|
451
472
|
db.close();
|
|
452
|
-
|
|
453
|
-
db =
|
|
454
|
-
db.pragma('journal_mode = WAL');
|
|
455
|
-
db.pragma('busy_timeout = 5000');
|
|
456
|
-
db.pragma('foreign_keys = ON');
|
|
457
|
-
db.exec(SCHEMA);
|
|
473
|
+
removeDbFiles();
|
|
474
|
+
db = openDb();
|
|
458
475
|
db.prepare("INSERT INTO meta (key, value) VALUES ('schema_version', ?)").run(SCHEMA_VERSION);
|
|
459
476
|
}
|
|
460
477
|
// Prepared statements
|
|
@@ -496,9 +513,8 @@ export function createIndex(vaultPath, opts) {
|
|
|
496
513
|
process.stderr.write(`Warning: skipping ${kind}/${slug}.md: missing required field(s): ${missing}\n`);
|
|
497
514
|
return;
|
|
498
515
|
}
|
|
499
|
-
// Parse next actions from body (new), fall back to frontmatter (legacy pre-migration)
|
|
500
516
|
const bodyActions = getNextActions(body);
|
|
501
|
-
const nextAction = bodyActions.length > 0 ? bodyActions[0] :
|
|
517
|
+
const nextAction = bodyActions.length > 0 ? bodyActions[0] : null;
|
|
502
518
|
const nextActionCount = bodyActions.length;
|
|
503
519
|
const brief = getBrief(body)?.slice(0, 300) ?? null;
|
|
504
520
|
const logs = parseSessionLog(body);
|
package/dist/export-okf.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/export-okf.ts
|
|
2
2
|
// Read-only export of the vault as an OKF v0.1 bundle (entities + knowledge + sessions).
|
|
3
3
|
// Spec: https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md
|
|
4
|
-
// Pure sync function — no Hafez instance, index, git, or lock
|
|
4
|
+
// Pure sync function — no Hafez instance, index, git, or lock.
|
|
5
5
|
// Re-export overwrites a prior bundle in place but never deletes: files for vault
|
|
6
6
|
// docs renamed or removed since the last export linger (unreferenced by the
|
|
7
7
|
// regenerated indexes) until the bundle directory is deleted and re-exported.
|
package/dist/git.d.ts
CHANGED
|
@@ -8,5 +8,12 @@ export declare function gitSync(vaultPath: string): Promise<{
|
|
|
8
8
|
pushed: boolean;
|
|
9
9
|
remote: boolean;
|
|
10
10
|
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Unstage files (best effort). Used by batch rollback: a commit that fails
|
|
13
|
+
* after `git add` leaves the new blobs staged in .git/index, and the next
|
|
14
|
+
* successful commit would publish the rolled-back content under the wrong
|
|
15
|
+
* message. Clearing the index entries restores worktree/HEAD/index agreement.
|
|
16
|
+
*/
|
|
17
|
+
export declare function gitUnstage(vaultPath: string, files: string[]): Promise<void>;
|
|
11
18
|
export declare function gitChangelog(vaultPath: string, since: string): Promise<ChangelogEntry[]>;
|
|
12
19
|
export declare function gitCommitAndPush(vaultPath: string, files: string[], message: string, config?: GitConfig): Promise<void>;
|