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.
Files changed (75) hide show
  1. package/README.md +83 -32
  2. package/dist/apply.d.ts +33 -0
  3. package/dist/apply.js +693 -0
  4. package/dist/batch-ops.d.ts +372 -26
  5. package/dist/batch-ops.js +355 -73
  6. package/dist/cli/args.d.ts +48 -6
  7. package/dist/cli/args.js +74 -9
  8. package/dist/cli/batch-errors.js +19 -11
  9. package/dist/cli/block-hook.d.ts +9 -0
  10. package/dist/cli/block-hook.js +90 -0
  11. package/dist/cli/commands-schema.js +2 -0
  12. package/dist/cli/commands.d.ts +28 -3
  13. package/dist/cli/commands.js +440 -157
  14. package/dist/cli/format.d.ts +7 -3
  15. package/dist/cli/format.js +27 -10
  16. package/dist/cli/index.d.ts +55 -0
  17. package/dist/cli/index.js +215 -153
  18. package/dist/cli/install.d.ts +53 -0
  19. package/dist/cli/install.js +297 -0
  20. package/dist/cli/onboard.d.ts +15 -2
  21. package/dist/cli/onboard.js +265 -90
  22. package/dist/cli/resolve-vault.d.ts +11 -4
  23. package/dist/cli/resolve-vault.js +13 -11
  24. package/dist/cli/schema-introspect.js +4 -3
  25. package/dist/cli/upgrade.d.ts +51 -0
  26. package/dist/cli/upgrade.js +351 -0
  27. package/dist/closest.d.ts +8 -0
  28. package/dist/closest.js +65 -0
  29. package/dist/contracts.d.ts +65 -6
  30. package/dist/contracts.js +148 -23
  31. package/dist/dates.d.ts +10 -0
  32. package/dist/dates.js +27 -0
  33. package/dist/db.d.ts +40 -2
  34. package/dist/db.js +363 -66
  35. package/dist/digest.d.ts +6 -4
  36. package/dist/digest.js +28 -17
  37. package/dist/document.d.ts +135 -88
  38. package/dist/document.js +665 -379
  39. package/dist/export-okf.d.ts +13 -0
  40. package/dist/export-okf.js +50 -20
  41. package/dist/git.js +165 -48
  42. package/dist/harness.d.ts +58 -0
  43. package/dist/harness.js +50 -0
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.js +301 -826
  46. package/dist/knowledge-index.d.ts +23 -1
  47. package/dist/knowledge-index.js +62 -6
  48. package/dist/merge.d.ts +1 -1
  49. package/dist/merge.js +98 -64
  50. package/dist/near-match.d.ts +9 -0
  51. package/dist/near-match.js +34 -0
  52. package/dist/schema.js +4 -4
  53. package/dist/stale.d.ts +41 -0
  54. package/dist/stale.js +43 -0
  55. package/dist/stopwords.d.ts +6 -0
  56. package/dist/stopwords.js +25 -0
  57. package/dist/surfaces/items.d.ts +366 -0
  58. package/dist/surfaces/items.js +523 -0
  59. package/dist/surfaces/surfaces.d.ts +32 -0
  60. package/dist/surfaces/surfaces.js +230 -0
  61. package/dist/templates.d.ts +6 -0
  62. package/dist/templates.js +27 -27
  63. package/dist/types.d.ts +78 -22
  64. package/dist/validate.d.ts +7 -0
  65. package/dist/validate.js +109 -0
  66. package/dist/vault.d.ts +18 -0
  67. package/dist/vault.js +51 -5
  68. package/package.json +4 -3
  69. package/skill/hafez/SKILL.md +68 -0
  70. package/skill/hafez-save/SKILL.md +29 -0
  71. package/dist/cli/help-agent.d.ts +0 -1
  72. package/dist/cli/help-agent.js +0 -186
  73. package/dist/query.d.ts +0 -24
  74. package/dist/query.js +0 -169
  75. package/skill/SKILL.md +0 -196
package/dist/vault.d.ts CHANGED
@@ -25,13 +25,31 @@ export declare function scanVaultDir(dirPath: string): string[];
25
25
  /** Where an entity's overflow session-log entries are archived. */
26
26
  export declare function archiveLogPath(vaultPath: string, slug: string): string;
27
27
  export declare function resolveFilePath(vaultPath: string, slug: string, kind: VaultKind): string;
28
+ /** The file a slug names, or null — entities are resolved before knowledge,
29
+ * the one resolution order every operation sees. */
30
+ export declare function findVaultFile(vaultPath: string, slug: string): string | null;
31
+ /** Does this slug name a vault file? An invalid slug definitionally doesn't
32
+ * exist — reference checks (create parent/related, link targets, validate)
33
+ * must report "does not exist", not crash. validate() in particular exists to
34
+ * REPORT malformed refs. */
35
+ export declare function vaultFileExists(vaultPath: string, slug: string): boolean;
28
36
  export declare function kindFromPath(filePath: string): 'entity' | 'knowledge';
37
+ /** Fields that must always be arrays when present (may be scalar in
38
+ * hand-edited YAML). Parsing coerces them here; merge.ts unions them on
39
+ * conflict — one list, so a new list-valued field cannot be normalized on
40
+ * read and then overwritten wholesale on merge. */
41
+ export declare const ARRAY_FIELDS: string[];
29
42
  /** Full-kind path classification (kindFromPath only distinguishes the
30
43
  * indexed kinds — sessions default to knowledge there for compatibility).
31
44
  * Session files live directly in sessions/, so only the immediate parent
32
45
  * counts — matching anywhere in the path misclassifies every file of a
33
46
  * vault that happens to live under a directory named "sessions". */
34
47
  export declare function vaultKindFromPath(filePath: string): VaultKind;
48
+ /** Total kind classification for a vault-relative content file (anything
49
+ * matching VAULT_FILE_RE) — derived from KIND_DIRS, so adding a vault dir
50
+ * can't silently fall a new kind through to the wrong one. Throws on paths
51
+ * outside the vault dirs; callers gate on VAULT_FILE_RE first. */
52
+ export declare function vaultKindFromRelPath(relPath: string): VaultKind;
35
53
  export declare function parseFilePath(path: string): ParsedVaultFile;
36
54
  export declare function parseContent(content: string): {
37
55
  frontmatter: Record<string, any>;
package/dist/vault.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/vault.ts
2
2
  import matter from 'gray-matter';
3
3
  import yaml from 'js-yaml';
4
- import { readFileSync, readdirSync } from 'fs';
4
+ import { readFileSync, readdirSync, existsSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import { HafezError } from './types.js';
7
7
  // Prevent gray-matter from auto-coercing YAML date strings into JS Date objects
@@ -11,7 +11,7 @@ export function slugify(name) {
11
11
  // real slug instead of collapsing to ''.
12
12
  return name
13
13
  .toLowerCase()
14
- .replace(/['']/g, '')
14
+ .replace(/['’]/g, '')
15
15
  .replace(/[^\p{L}\p{N}]+/gu, '-')
16
16
  .replace(/^-|-$/g, '')
17
17
  .replace(/-{2,}/g, '-');
@@ -69,15 +69,50 @@ export function resolveFilePath(vaultPath, slug, kind) {
69
69
  }
70
70
  return join(vaultPath, KIND_DIRS[kind], `${slug}.md`);
71
71
  }
72
+ /** The file a slug names, or null — entities are resolved before knowledge,
73
+ * the one resolution order every operation sees. */
74
+ export function findVaultFile(vaultPath, slug) {
75
+ const entity = resolveFilePath(vaultPath, slug, 'entity');
76
+ if (existsSync(entity))
77
+ return entity;
78
+ const knowledge = resolveFilePath(vaultPath, slug, 'knowledge');
79
+ if (existsSync(knowledge))
80
+ return knowledge;
81
+ return null;
82
+ }
83
+ /** Does this slug name a vault file? An invalid slug definitionally doesn't
84
+ * exist — reference checks (create parent/related, link targets, validate)
85
+ * must report "does not exist", not crash. validate() in particular exists to
86
+ * REPORT malformed refs. */
87
+ export function vaultFileExists(vaultPath, slug) {
88
+ try {
89
+ return findVaultFile(vaultPath, slug) !== null;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
72
95
  // Separator-agnostic: OS paths use '/' or '\' depending on platform, and a
73
96
  // POSIX-only check silently classifies every entity as knowledge on Windows
74
97
  // (all updates then fail on kind rules). Exported pure so win32 paths are
75
98
  // unit-testable from any platform.
99
+ // The DEEPEST kind-dir occurrence wins: the in-vault dir is always deeper
100
+ // than any ancestor, so a vault living under a directory named 'entities'
101
+ // (e.g. ~/data/entities/myvault) can't shadow its knowledge/ files.
76
102
  export function kindFromPath(filePath) {
77
- return /[/\\]entities[/\\]/.test(filePath) ? 'entity' : 'knowledge';
103
+ const lastMatch = (dir) => {
104
+ let idx = -1;
105
+ for (const m of filePath.matchAll(new RegExp(`[/\\\\]${dir}[/\\\\]`, 'g')))
106
+ idx = m.index;
107
+ return idx;
108
+ };
109
+ return lastMatch(KIND_DIRS.entity) > lastMatch(KIND_DIRS.knowledge) ? 'entity' : 'knowledge';
78
110
  }
79
- // Fields that must always be arrays when present (may be scalar in hand-edited YAML)
80
- const ARRAY_FIELDS = ['related', 'tags', 'domain'];
111
+ /** Fields that must always be arrays when present (may be scalar in
112
+ * hand-edited YAML). Parsing coerces them here; merge.ts unions them on
113
+ * conflict — one list, so a new list-valued field cannot be normalized on
114
+ * read and then overwritten wholesale on merge. */
115
+ export const ARRAY_FIELDS = ['related', 'tags', 'domain'];
81
116
  function normalizeFrontmatter(fm) {
82
117
  for (const field of ARRAY_FIELDS) {
83
118
  if (field in fm && typeof fm[field] === 'string') {
@@ -96,6 +131,17 @@ export function vaultKindFromPath(filePath) {
96
131
  return 'session';
97
132
  return kindFromPath(filePath);
98
133
  }
134
+ /** Total kind classification for a vault-relative content file (anything
135
+ * matching VAULT_FILE_RE) — derived from KIND_DIRS, so adding a vault dir
136
+ * can't silently fall a new kind through to the wrong one. Throws on paths
137
+ * outside the vault dirs; callers gate on VAULT_FILE_RE first. */
138
+ export function vaultKindFromRelPath(relPath) {
139
+ for (const [kind, dir] of Object.entries(KIND_DIRS)) {
140
+ if (relPath.startsWith(`${dir}/`))
141
+ return kind;
142
+ }
143
+ throw new HafezError('VALIDATION_FAILED', `Not a vault content path: ${relPath}`);
144
+ }
99
145
  export function parseFilePath(path) {
100
146
  const content = readFileSync(path, 'utf-8');
101
147
  const { data, content: body } = matter(content, MATTER_OPTIONS);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hafez",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -18,8 +18,9 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "build": "rm -rf dist && tsc && chmod +x dist/cli.js",
21
- "install:local": "chmod +x \"$(pwd)/dist/cli.js\" && mkdir -p ~/.local/bin && ln -sf \"$(pwd)/dist/cli.js\" ~/.local/bin/hafez && rm -f ~/.claude/skills/hafez/SKILL.md && mkdir -p ~/.claude/skills/hafez && cp \"$(pwd)/skill/SKILL.md\" ~/.claude/skills/hafez/SKILL.md",
22
- "pretest": "npm run build",
21
+ "install:local": "chmod +x \"$(pwd)/dist/cli.js\" && mkdir -p ~/.local/bin && ln -sf \"$(pwd)/dist/cli.js\" ~/.local/bin/hafez && for d in ~/.claude/skills ~/.agents/skills; do for s in hafez hafez-save; do mkdir -p \"$d/$s\" && cp \"$(pwd)/skill/$s/SKILL.md\" \"$d/$s/SKILL.md\"; done; done",
22
+ "typecheck": "tsc -p tsconfig.test.json",
23
+ "pretest": "npm run build && npm run typecheck",
23
24
  "test": "vitest run",
24
25
  "test:watch": "vitest",
25
26
  "prepare": "npm run build"
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: hafez
3
+ description: >
4
+ Hafez vault: the less common operations and the tool itself. Use when the
5
+ user asks what is in the vault ("what am I working on?", "what's stale?"),
6
+ wants to promote, link, unlink, delete, change a status or validate, or
7
+ wants to update or reconfigure Hafez ("upgrade hafez", "set up hafez").
8
+ Also the reference to read when no Hafez block is in your instructions.
9
+ hash: 01a30ae1e209
10
+ ---
11
+
12
+ # Hafez
13
+
14
+ - Update an entity or note: `hafez read <slug>`. The end of its output shows the write command.
15
+ - Create an entity or a knowledge note: the two `hafez create` lines below.
16
+ - Save the session: the `hafez-save` skill. No skill: `hafez schema digest`.
17
+
18
+ Never ask the user whether to write to the vault. Write, then say so in one line naming the slug and what you did.
19
+ Update only what you have read this session. Not read yet: read it first.
20
+ Open actions on an entity you read are yours: tick what landed, drop what is dead. Lines `read` marks stale are due.
21
+
22
+ ## Create
23
+
24
+ ```
25
+ hafez create entity "<name>" --type <project|entity> --description "<one sentence>" --brief "<where things stand>" --add-action "<next step>"
26
+ hafez create knowledge "<name>" --synthesis "<the insight>" --confidence <observation|pattern|principle> --related <slug>
27
+ ```
28
+
29
+ A plan is `--subtype plan` without `--synthesis`. Cannot tell entity from knowledge: `hafez capture "<name>"`.
30
+
31
+ ## Find
32
+
33
+ ```
34
+ hafez query --filter <active|paused|done|stale|capture>
35
+ hafez search "<terms>" --kind <entity|knowledge>
36
+ hafez stats
37
+ ```
38
+
39
+ ## Less common operations
40
+
41
+ ```
42
+ hafez promote <slug> <entity|project|knowledge>
43
+ hafez link <slug> <target> --relation <parent|related>
44
+ hafez unlink <slug> <target> --relation <parent|related>
45
+ hafez update <slug> --status <active|paused|done>
46
+ hafez delete <slug>
47
+ hafez validate
48
+ ```
49
+
50
+ A plan's status is `active|done`. `delete` removes a knowledge or session note only; an entity is retired with `--status done`.
51
+
52
+ Several writes that must land together go in one batch: a bare array, no wrapper. A slug is the name lowercased, apostrophes dropped, each other run of characters that are not letters or digits turned into one `-` and none left at either end, so a later op can name what an earlier one creates.
53
+
54
+ ```
55
+ hafez batch --data '[
56
+ {"op":"create","kind":"entity","name":"Token Rotation","fields":{"type":"entity"}},
57
+ {"op":"link","slug":"token-rotation","target":"auth-refactor","relation":"parent"}
58
+ ]'
59
+ ```
60
+
61
+ In the JSON, write each apostrophe as `’` (`isn’t`): a straight `'` ends the quoted argument and the batch fails.
62
+
63
+ No pipe (PowerShell 5.1): `hafez batch --file ops.json`. Machine-readable shape of any op: `hafez schema <op>`, with `--examples` for a sample.
64
+
65
+ ## The tool itself
66
+
67
+ - Upgrade: `hafez upgrade`. It checks and prints, it never installs. Show the user the printed steps and run them only after a yes.
68
+ - Set up or reconfigure: `hafez onboard`.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: hafez-save
3
+ description: >
4
+ Save this session to the Hafez vault. Use when the user invokes hafez-save
5
+ or says "save", "save the session" or "wrap up". Never run it unprompted.
6
+ hash: 91351681e779
7
+ ---
8
+
9
+ # Save the session
10
+
11
+ 1. List the slugs you read, updated or created this session. Use only those; a slug you did not see in `hafez` output this session does not go in. No slugs: tell the user there is nothing to save and stop.
12
+ 2. Run this once, with today's date:
13
+
14
+ ```
15
+ hafez digest --data '{
16
+ "entities_touched": ["<slug>", "<slug>"],
17
+ "decisions": ["<a decision made this session>"],
18
+ "narrative": "<one to three sentences: what happened and how it connects>",
19
+ "session_date": "<YYYY-MM-DD>"
20
+ }' | hafez batch
21
+ ```
22
+
23
+ In the JSON, write each apostrophe as `’` (`isn’t`): a straight `'` ends the quoted argument and the save fails.
24
+
25
+ No pipe (PowerShell 5.1): put the JSON in `summary.json`, then `hafez digest --file summary.json > ops.json`, then `hafez batch --file ops.json`.
26
+
27
+ 3. Tell the user in one line: the session note's slug and the entities it logged to.
28
+
29
+ `decisions` may be an empty array. Each slug in `entities_touched` gets a session log entry and the session note links them all. An unknown slug fails the save and names the slug: fix the list and run it again. A session already saved is skipped, so a second run does no harm.
@@ -1 +0,0 @@
1
- export declare function renderAgentHelp(): string;
@@ -1,186 +0,0 @@
1
- // src/cli/help-agent.ts
2
- //
3
- // `hafez help --agent` — the prompt-gate. Plain text, no ANSI, no markdown
4
- // decoration. Designed for an LLM with zero Hafez context to read once per
5
- // session and then produce valid batch JSON on first try.
6
- //
7
- // Hand-written sections: Preamble + Hard rules (prose, free-form).
8
- // Generated sections (wrapped in <!-- SECTION:name --> markers so tests can
9
- // parse them): op-catalog, critical-enums, batch-examples, digest.
10
- //
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.
15
- import { listOps, getEnumValues, getOpExample, getOpSchema } from './schema-introspect.js';
16
- const PREAMBLE = `# Hafez — Agent Prompt Gate
17
-
18
- Hafez is your persistent knowledge graph for projects, entities, captures,
19
- knowledge notes, and session notes. The \`hafez\` CLI is the ONLY supported
20
- write path into the vault — never edit files under \`entities/\`, \`knowledge/\`,
21
- or \`sessions/\` by hand. Every mutation goes through the CLI so the git history,
22
- SQLite index, and link integrity stay in sync.
23
-
24
- Mental model:
25
-
26
- - An \`entity\` is tracked work with substance — a bug, a feature, a decision.
27
- Entities have status, can have children, and carry a session log. The three
28
- entity types are \`capture\` (raw inbox), \`entity\` (standalone tracked item),
29
- and \`project\` (scoped effort with deliverables, can have child entities).
30
- - A \`knowledge\` note is an insight or a plan. Insights distil a pattern or
31
- principle and mature through added evidence. Plans are structured steps
32
- toward a goal. Knowledge is searchable, linkable, and timestamped.
33
- - A \`session\` note records what happened in a working session and references
34
- the entities it touched. Session notes are created via \`create/session\`
35
- and are not indexed in SQLite — they are for narrative, not query.
36
- - \`capture\` is the quick-inbox path: grab a link or thought, no structure.
37
- A capture can later be promoted to entity, project, or knowledge.
38
- - Entities relate to each other through links: \`parent\` (hierarchy) or
39
- \`related\` (soft reference). Use \`link\` / \`unlink\` ops to manage them.
40
- - Every meaningful update should include a session_log entry so the history
41
- of the work is preserved. Sessions are how future-you reconstructs why a
42
- decision was made.
43
-
44
- Single source of truth: the exact field names, enum values, and op signatures
45
- below are generated at runtime from the live Zod schema. If a field is not
46
- listed here, it does not exist — do not invent fields.
47
-
48
- ## Hard rules (read before writing a batch)
49
-
50
- - ALWAYS batch 2+ mutations into one \`hafez batch\` call. Many single-op
51
- calls fragment git history and clobber the vault index's atomicity guarantee.
52
- - NEVER invent field names. If a field is not in \`hafez schema <op>\`, it
53
- does not exist. Unknown fields are rejected by the validator with a pointer
54
- to the right schema.
55
- - NEVER write to vault files directly. Always go through the CLI.
56
- - STRONGLY PREFERRED: include a \`session_log\` on update ops that reflect
57
- meaningful work. The validator does not enforce this — omitting it is a
58
- code smell, not a hard error.
59
- - When in doubt, run \`hafez schema <op>\` before writing a batch. Cheap,
60
- deterministic, no side effects.
61
- - Use \`hafez batch --dry-run\` to validate a payload without touching the
62
- vault. Same validation as a real apply, zero filesystem mutation.
63
-
64
- ## Slug contract (create-then-link in one batch)
65
-
66
- Slugs are deterministic: slug = slugify(name) — lowercase, non-alphanumeric
67
- runs collapse to single \`-\`, edges trimmed (\`"Auth Refactor!"\` →
68
- \`auth-refactor\`). A batch executes sequentially, so later ops may reference
69
- the slugs of earlier creates in the same batch (\`create\` then \`link\`/\`update\`
70
- works in one payload). Collisions hard-error — a wrong slug prediction can
71
- never silently hit an existing document. To verify predictions first:
72
- \`hafez batch --dry-run\` reports the derived slug for every create op.
73
- `;
74
- function renderOpCatalog() {
75
- const lines = ['<!-- SECTION:op-catalog -->'];
76
- lines.push('Ops (run `hafez schema <name>` for the full field list):');
77
- for (const op of listOps()) {
78
- const desc = op.description ? ` — ${op.description}` : '';
79
- lines.push(` ${op.name}${desc}`);
80
- lines.push(` → hafez schema ${op.name}`);
81
- }
82
- lines.push('<!-- /SECTION:op-catalog -->');
83
- return lines.join('\n');
84
- }
85
- function renderCriticalEnums() {
86
- const entries = [
87
- ['update.fields.status', 'update.fields.status'],
88
- ['update.fields.session_log.type', 'update.fields.session_log.type'],
89
- ['update.fields.confidence', 'update.fields.confidence'],
90
- ['create-entity.fields.type', 'create-entity.fields.type'],
91
- ['create-knowledge.fields.subtype', 'create-knowledge.fields.subtype'],
92
- ['link.relation', 'link.relation'],
93
- ];
94
- const lines = ['<!-- SECTION:critical-enums -->'];
95
- lines.push('Critical enums (value lists — values are authoritative):');
96
- for (const [label, path] of entries) {
97
- const values = getEnumValues(path);
98
- if (!values)
99
- continue;
100
- lines.push(` ${label} = [${values.join(', ')}]`);
101
- }
102
- lines.push('<!-- /SECTION:critical-enums -->');
103
- return lines.join('\n');
104
- }
105
- function renderBatchExamples() {
106
- const lines = ['<!-- SECTION:batch-examples -->'];
107
- lines.push('Batch JSON shape — wrap one or more ops in an array:');
108
- lines.push('');
109
- lines.push(' echo \'[...]\' | hafez batch # POSIX shells');
110
- lines.push(' hafez batch --file payload.json # any platform (PowerShell 5.1 cannot pipe stdin)');
111
- lines.push('');
112
- lines.push('Minimal example per op (canonical field names; aliased keys stripped):');
113
- lines.push('');
114
- for (const op of listOps()) {
115
- const example = getOpExample(op.name);
116
- if (!example)
117
- continue;
118
- lines.push(`# ${op.name}`);
119
- lines.push(JSON.stringify(example, null, 2));
120
- lines.push('');
121
- }
122
- lines.push('<!-- /SECTION:batch-examples -->');
123
- return lines.join('\n');
124
- }
125
- function renderDigestSection() {
126
- const lines = ['<!-- SECTION:digest -->'];
127
- lines.push('Session digest — turn end-of-session context into a batch payload.');
128
- lines.push('NOT a batch op: it reads its own JSON and prints batch JSON.');
129
- lines.push('');
130
- lines.push(" echo '<digest-json>' | hafez digest | hafez batch");
131
- lines.push(' # No stdin pipes (PowerShell 5.1): hafez digest --file summary.json > ops.json,');
132
- lines.push(' # then hafez batch --file ops.json');
133
- lines.push('');
134
- lines.push('Input fields (`hafez schema digest` for the full schema):');
135
- const schema = getOpSchema('digest');
136
- for (const [name, field] of Object.entries(schema?.fields ?? {})) {
137
- const req = field.optional ? 'optional' : 'required';
138
- const desc = field.description ? ` — ${field.description}` : '';
139
- lines.push(` ${name} (${req})${desc}`);
140
- }
141
- lines.push('');
142
- lines.push('Example input:');
143
- lines.push(JSON.stringify(getOpExample('digest'), null, 2));
144
- lines.push('<!-- /SECTION:digest -->');
145
- return lines.join('\n');
146
- }
147
- const FOOTER = `## Lookup pointers
148
-
149
- - \`hafez schema\` — list all ops
150
- - \`hafez schema <op>\` — full JSON schema for one op
151
- - \`hafez schema <op> --examples\` — working JSON examples per enum value
152
- - \`hafez batch --dry-run\` — validate without touching the vault
153
- `;
154
- /**
155
- * Render the full prompt-gate output. Token budget: 1500-3000 tokens when
156
- * measured as char/3.5. The drift test in tests/cli-help-agent.test.ts
157
- * enforces the ceiling.
158
- */
159
- const EXIT_CODES_SECTION = `## Exit codes
160
-
161
- | code | meaning |
162
- |------|---------|
163
- | 0 | success |
164
- | 1 | not found / unknown error |
165
- | 2 | validation or usage error (bad flags, bad payload, core rejected the write) |
166
- | 3 | git push failed — the write IS committed locally |
167
- | 4 | write succeeded but the auto-sync after it failed |
168
- | 5 | slug already exists |
169
- | 6 | git commit failed — nothing was written to history |
170
- | 7 | vault locked by another hafez process — retry when it finishes |`;
171
- export function renderAgentHelp() {
172
- return [
173
- PREAMBLE,
174
- renderOpCatalog(),
175
- '',
176
- renderCriticalEnums(),
177
- '',
178
- renderBatchExamples(),
179
- '',
180
- renderDigestSection(),
181
- '',
182
- EXIT_CODES_SECTION,
183
- '',
184
- FOOTER,
185
- ].join('\n');
186
- }
package/dist/query.d.ts DELETED
@@ -1,24 +0,0 @@
1
- import type { QueryResult, KnowledgeQueryResult, EntityQueryOpts, KnowledgeQueryOpts, UnifiedResult } from './types.js';
2
- import type { HafezIndex } from './db.js';
3
- export declare function queryEntities(index: HafezIndex, opts?: EntityQueryOpts): {
4
- items: QueryResult[];
5
- total: number;
6
- };
7
- export declare function queryChildren(index: HafezIndex, parentSlug: string): {
8
- items: QueryResult[];
9
- total: number;
10
- };
11
- export declare function queryRelatedTo(index: HafezIndex, slug: string, relation?: string): {
12
- items: UnifiedResult[];
13
- total: number;
14
- };
15
- export declare function queryKnowledge(index: HafezIndex, opts?: KnowledgeQueryOpts): {
16
- items: KnowledgeQueryResult[];
17
- total: number;
18
- };
19
- export declare function queryUnified(index: HafezIndex, opts?: EntityQueryOpts & {
20
- kind?: 'entity' | 'knowledge' | 'all';
21
- }): {
22
- items: UnifiedResult[];
23
- total: number;
24
- };
package/dist/query.js DELETED
@@ -1,169 +0,0 @@
1
- import { HafezError } from './types.js';
2
- function toQueryResult(slug, row) {
3
- return {
4
- slug,
5
- name: row.name,
6
- type: row.type,
7
- status: row.status,
8
- description: row.description ?? null,
9
- resource: row.resource ?? null,
10
- domain: row.domain ?? [],
11
- last_touched: row.last_touched,
12
- next_action: row.next_action ?? null,
13
- next_action_count: row.next_action_count ?? 0,
14
- parent: row.parent ?? null,
15
- related: row.related ?? [],
16
- staleness_days: row.staleness_days ?? null,
17
- tags: row.tags ?? [],
18
- brief: row.brief ?? null,
19
- session_log_count: row.session_log_count ?? 0,
20
- last_session_date: row.last_session_date ?? null,
21
- last_session_type: row.last_session_type ?? null,
22
- last_session_summary: row.last_session_summary ?? null,
23
- };
24
- }
25
- function toKnowledgeResult(slug, row) {
26
- return {
27
- slug,
28
- name: row.name,
29
- description: row.description ?? null,
30
- domain: row.domain ?? [],
31
- confidence: row.confidence ?? 'observation',
32
- related: row.related ?? [],
33
- reinforcement_count: row.reinforcement_count ?? 0,
34
- last_reinforced: row.last_reinforced ?? null,
35
- last_touched: row.last_touched ?? null,
36
- };
37
- }
38
- const STATUS_FILTERS = new Set(['active', 'paused', 'done']);
39
- const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
40
- function validateDateOpts(opts) {
41
- const fields = ['since', 'before', 'createdSince', 'createdBefore'];
42
- for (const f of fields) {
43
- if (opts?.[f] !== undefined && !ISO_DATE.test(opts[f])) {
44
- throw new HafezError('VALIDATION_FAILED', `Invalid date format for ${f}: "${opts[f]}". Must be YYYY-MM-DD.`);
45
- }
46
- }
47
- }
48
- export function queryEntities(index, opts) {
49
- index.syncIfStale();
50
- validateDateOpts(opts);
51
- const qopts = { kind: 'entity' };
52
- if (opts?.filter && STATUS_FILTERS.has(opts.filter))
53
- qopts.status = opts.filter;
54
- if (opts?.filter === 'stale')
55
- qopts.filter = 'stale';
56
- if (opts?.filter === 'capture')
57
- qopts.filter = 'capture';
58
- if (opts?.type)
59
- qopts.type = opts.type;
60
- if (opts?.parent)
61
- qopts.parent = opts.parent;
62
- if (opts?.domain)
63
- qopts.domain = opts.domain;
64
- if (opts?.since)
65
- qopts.since = opts.since;
66
- if (opts?.before)
67
- qopts.before = opts.before;
68
- if (opts?.createdSince)
69
- qopts.createdSince = opts.createdSince;
70
- if (opts?.createdBefore)
71
- qopts.createdBefore = opts.createdBefore;
72
- if (opts?.tags)
73
- qopts.tags = opts.tags;
74
- if (opts?.sort_by)
75
- qopts.sort_by = opts.sort_by;
76
- if (opts?.sort_order)
77
- qopts.sort_order = opts.sort_order;
78
- if (opts?.limit != null)
79
- qopts.limit = opts.limit;
80
- if (opts?.offset != null)
81
- qopts.offset = opts.offset;
82
- const { items, total } = index.queryItems(qopts);
83
- return { items: items.map(i => toQueryResult(i.slug, i)), total };
84
- }
85
- export function queryChildren(index, parentSlug) {
86
- return queryEntities(index, { parent: parentSlug });
87
- }
88
- export function queryRelatedTo(index, slug, relation) {
89
- index.syncIfStale();
90
- const { items, total } = index.queryItems({ relatedTo: slug, relation });
91
- return {
92
- items: items.map(i => i.kind === 'entity'
93
- ? { ...toQueryResult(i.slug, i), kind: 'entity' }
94
- : { ...toKnowledgeResult(i.slug, i), kind: 'knowledge' }),
95
- total,
96
- };
97
- }
98
- export function queryKnowledge(index, opts) {
99
- index.syncIfStale();
100
- const qopts = { kind: 'knowledge' };
101
- if (opts?.domain)
102
- qopts.domain = opts.domain;
103
- if (opts?.confidence)
104
- qopts.confidence = opts.confidence;
105
- if (opts?.subtype)
106
- qopts.subtype = opts.subtype;
107
- if (opts?.tags)
108
- qopts.tags = opts.tags;
109
- if (opts?.sort_by)
110
- qopts.sort_by = opts.sort_by;
111
- if (opts?.sort_order)
112
- qopts.sort_order = opts.sort_order;
113
- if (opts?.limit != null)
114
- qopts.limit = opts.limit;
115
- if (opts?.offset != null)
116
- qopts.offset = opts.offset;
117
- const { items, total } = index.queryItems(qopts);
118
- return { items: items.map(i => toKnowledgeResult(i.slug, i)), total };
119
- }
120
- export function queryUnified(index, opts) {
121
- index.syncIfStale();
122
- validateDateOpts(opts);
123
- const qopts = {};
124
- if (opts?.kind && opts.kind !== 'all')
125
- qopts.kind = opts.kind;
126
- // When kind is 'all' or unset, no kind filter — both entities and knowledge are returned
127
- if (opts?.filter && STATUS_FILTERS.has(opts.filter))
128
- qopts.status = opts.filter;
129
- if (opts?.filter === 'stale')
130
- qopts.filter = 'stale';
131
- if (opts?.filter === 'capture')
132
- qopts.filter = 'capture';
133
- if (opts?.type)
134
- qopts.type = opts.type;
135
- if (opts?.parent)
136
- qopts.parent = opts.parent;
137
- if (opts?.domain)
138
- qopts.domain = opts.domain;
139
- if (opts?.confidence)
140
- qopts.confidence = opts.confidence;
141
- if (opts?.since)
142
- qopts.since = opts.since;
143
- if (opts?.before)
144
- qopts.before = opts.before;
145
- if (opts?.createdSince)
146
- qopts.createdSince = opts.createdSince;
147
- if (opts?.createdBefore)
148
- qopts.createdBefore = opts.createdBefore;
149
- if (opts?.tags)
150
- qopts.tags = opts.tags;
151
- if (opts?.sort_by)
152
- qopts.sort_by = opts.sort_by;
153
- if (opts?.sort_order)
154
- qopts.sort_order = opts.sort_order;
155
- if (opts?.limit !== undefined)
156
- qopts.limit = opts.limit;
157
- if (opts?.offset !== undefined)
158
- qopts.offset = opts.offset;
159
- const result = index.queryItems(qopts);
160
- const items = result.items.map(i => {
161
- if (i.kind === 'entity') {
162
- return { ...toQueryResult(i.slug, i), kind: 'entity' };
163
- }
164
- else {
165
- return { ...toKnowledgeResult(i.slug, i), kind: 'knowledge' };
166
- }
167
- });
168
- return { items, total: result.total };
169
- }