hafez 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -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": "1.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/skill/SKILL.md CHANGED
@@ -1,133 +1,71 @@
1
1
  ---
2
2
  name: hafez
3
3
  description: >
4
- Hafez vault — personal knowledge base of captures, entities, projects,
5
- and knowledge notes (with subtypes: insight, plan). Use when the user says /hafez, asks to query the vault
6
- ("what am I working on?", "show active projects", "what's stale?"), wants to
7
- read or create an entity, or references Hafez directly. Also use at session
8
- start to load context for identifiable topics, and at session end to record
9
- a digest. Even if the user just casually mentions a project by name,
10
- use this skill to check if it's a Hafez entity.
4
+ Hafez vault — personal knowledge base of captures, entities, projects, and
5
+ knowledge notes (subtypes: insight, plan). Use when the user mentions Hafez
6
+ (or types /hafez), asks what's in the vault ("what am I working on?", "what's
7
+ stale?"), wants to create or update an entity or knowledge note, at session
8
+ start to load context for an identifiable topic, at session end to record a
9
+ digest, when a casually mentioned project name might be a vault entity, or
10
+ when the user wants to update the tool itself ("update hafez", "upgrade
11
+ hafez", "is hafez outdated").
11
12
  ---
12
13
 
13
14
  # Hafez — Vault Operations
14
15
 
15
- All operations use the `hafez` CLI. No file manipulation — the CLI handles reads, writes, validation, and git.
16
+ All operations go through the `hafez` CLI — it owns reads, writes, validation, and git. Vault files are never edited directly.
16
17
 
17
18
  If `hafez` is not installed, or `hafez init` reports no vault, stop and follow the setup in the project README (github.com/shrvn-r/hafez) before using this skill. If the vault exists but is empty, run `hafez onboard` and follow what it prints.
18
19
 
19
- ## CLI Reference
20
+ ## Situation → Command
20
21
 
21
- Run `hafez help --agent` for the complete, always-current API reference including all commands, flags, batch schema, and valid enum values.
22
-
23
- Quick reference for common operations:
22
+ `hafez help --agent` is the complete, always-current API reference (all commands, flags, batch schema, enum values). This table covers the common situations; consult `help --agent` for anything beyond it.
24
23
 
25
24
  | Situation | Command |
26
25
  |-----------|---------|
27
- | Read an entity | `hafez read <slug>` |
28
- | Active projects | `hafez query --filter active` |
26
+ | Read an entity (incl. a project the user mentions) | `hafez read <slug>` |
27
+ | "What am I working on?" / active projects | `hafez query --filter active` |
29
28
  | Stale items | `hafez query --filter stale` |
30
- | Full-text search | `hafez search "<terms>"` |
31
- | Search knowledge only | `hafez search "<terms>" --kind knowledge` |
32
- | Create entity | `hafez create entity "<name>" --type project [--purpose "text"] [--description "text"] [--resource "uri"] [--brief "text"] [--add-action "task"]` |
33
- | Quick capture | `hafez capture "<name>"` |
29
+ | Full-text search | `hafez search "<terms>"` (`--kind knowledge` to narrow) |
34
30
  | List inbox captures | `hafez query --filter capture` |
35
- | Update entity | `hafez update <slug> --brief "text" --add-action "task"` |
36
- | Complete action | `hafez update <slug> --complete-action "substring"` |
37
- | Session log | `hafez update <slug> --log "progress: summary" --agent claude` |
38
- | Link entities | `hafez link <slug> <target> --relation related` |
31
+ | Children of a project | `hafez query --parent <slug>` |
32
+ | Related entities/knowledge | `hafez query --related-to <slug>` |
33
+ | New workstream | `hafez create entity "<name>" --type project [--brief "context"] [--add-action "task"]` (full flag list in `help --agent`; `--purpose` is project-only) |
34
+ | New plan | `hafez create knowledge "<name>" --subtype plan [--status active\|done]` (plans have Goal/Steps/Dependencies — no `--synthesis`; `--status` is plan-only) |
35
+ | Quick thought, no classification yet | `hafez capture "<name>"` |
39
36
  | Promote | `hafez promote <slug> <target>` (capture→entity/project/knowledge, entity→project) |
40
- | Add synthesis to knowledge | `hafez update <slug> --synthesis "text"` |
41
- | Add evidence to knowledge | `hafez update <slug> --add-evidence "text"` |
42
- | Add source to knowledge | `hafez update <slug> --add-source "url"` |
43
- | Export OKF bundle | `hafez export --okf [--out <dir>]` (re-export overwrites, never deletes) |
37
+ | Set handoff context / summary / URI | `hafez update <slug> --brief "text"` / `--description "summary"` / `--resource "https://..."` |
38
+ | Add / complete a next action | `hafez update <slug> --add-action "task"` / `--complete-action "substring"` |
39
+ | Significant milestone (deployed, merged, bug found) | `hafez update <slug> --log "progress: summary" --agent claude` |
40
+ | Link entities | `hafez link <slug> <target> --relation related` (or `parent` when a thread belongs to a project) |
41
+ | Revise a knowledge insight | `hafez update <slug> --synthesis "text"` (`--insight` is an alias) |
42
+ | Back knowledge with evidence / a source | `hafez update <slug> --add-evidence "text"` / `--add-source "url"` |
44
43
  | Digest session → batch | `echo '{...}' \| hafez digest \| hafez batch` (no pipes? `--file` on both) |
45
44
 
46
- Note: `--insight` is an alias for `--synthesis`.
47
-
48
45
  ## Session Start — Context Loading
49
46
 
50
- When a topic is identifiable from the user's first message:
47
+ Load context only when a specific entity slug or topic is identifiable from the user's first message:
51
48
 
52
49
  1. Run `hafez read <slug>` to load entity context
53
50
  2. If the entity has a `parent`, also read the parent at frontmatter depth: `hafez read <parent> --depth frontmatter`
54
51
  3. Search for related knowledge (`hafez search "<topic>" --kind knowledge`) and read any relevant note: `hafez read <knowledge-slug>`
55
52
  4. Present a brief context line: "Loaded context for {name} (status: {status}, next: {next_action})"
56
53
 
57
- Don't load context speculatively. Only when you can identify a specific entity slug or topic.
58
-
59
- ## Mid-Session — Working With the Vault
60
-
61
- | Situation | Action |
62
- |-----------|--------|
63
- | User asks "what am I working on?" | `hafez query --filter active` |
64
- | User asks about stale items | `hafez query --filter stale` |
65
- | User mentions a project by name | `hafez read <slug>` |
66
- | New workstream emerges | `hafez create entity "<name>" --type project [--brief "context"] [--add-action "task"]` |
67
- | Quick thought, no classification | `hafez capture "<name>"` |
68
- | Promote capture to entity/project | `hafez promote <slug> entity` |
69
- | Insight worth preserving | Search first (`hafez search "<topic>"`), then create or update |
70
- | Entity relates to another | `hafez link <slug> <target> --relation related` |
71
- | Thread belongs to a project | `hafez link <slug> <parent> --relation parent` |
72
- | Knowledge needs new synthesis | `hafez update <slug> --synthesis "text"` |
73
- | Knowledge backed by new evidence | `hafez update <slug> --add-evidence "text"` |
74
- | Knowledge has a source to cite | `hafez update <slug> --add-source "url"` |
75
- | Need children of a project | `hafez query --parent <slug>` |
76
- | Need related entities/knowledge | `hafez query --related-to <slug>` |
77
- | Search for something | `hafez search "<terms>"` |
78
- | Set handoff context | `hafez update <slug> --brief "context"` |
79
- | Set one-line description | `hafez update <slug> --description "summary"` |
80
- | Set resource URI (entity) | `hafez update <slug> --resource "https://..."` |
81
- | Add a next action | `hafez update <slug> --add-action "task"` |
82
- | Complete a next action | `hafez update <slug> --complete-action "task"` |
83
- | Rebuild stale index | `hafez index rebuild` |
84
- | Pull latest from remote | `hafez sync` |
85
-
86
- ### Ingest Reflex — Before Creating New Knowledge
87
-
88
- Before creating a new knowledge note, always check if one already exists:
89
-
90
- ```bash
91
- hafez search "<topic>"
92
- hafez search "<topic>" --kind knowledge
93
- ```
94
-
95
- If a related note exists, update it rather than creating a duplicate. Use `--synthesis` to revise the main insight text, `--add-evidence` to append supporting evidence, or `--add-source` to cite a new source.
96
-
97
- Only create a new note when no existing knowledge covers the topic.
98
-
99
- ### Wiki Links in Knowledge Notes
100
-
101
- When writing or updating knowledge note prose (brief, synthesis, insight body), use `[[wiki links]]` to reference related entities and knowledge naturally. This makes the vault navigable as a wiki:
102
-
103
- - Reference related projects: `[[project-slug]]`
104
- - Reference other knowledge: `[[knowledge-slug]]`
105
- - Reference people or areas: `[[entity-slug]]`
106
-
107
- The CLI does not enforce link syntax — write them naturally in the prose content you pass to `--synthesis`, `--brief`, or `--insight`.
54
+ ## Search-First Ingest
108
55
 
109
- ### Deliberate Ingest Pattern
56
+ A new knowledge note is a last resort. When an insight emerges or a source (document, conversation, research) is being processed:
110
57
 
111
- When processing a source (document, conversation, research) to extract knowledge:
58
+ 1. Search for existing coverage: `hafez search "<topic>"`, narrowing with `--kind knowledge`.
59
+ 2. A note covers the topic → update it: `--synthesis` to revise the main insight, `--add-evidence` to append support, `--add-source` to cite.
60
+ 3. Nothing covers it → create it linked from birth (`hafez create knowledge "<name>" --related slug1,slug2`), then write the body via `--synthesis`.
112
61
 
113
- ```bash
114
- # 1. Search for existing coverage
115
- hafez search "<topic>" --kind knowledge
116
-
117
- # 2a. If exists — update it
118
- hafez update <slug> --synthesis "Revised insight" --add-source "url"
119
- hafez update <slug> --add-evidence "New evidence from source"
120
-
121
- # 2b. If new — create it
122
- hafez create knowledge "<name>" --related slug1,slug2
62
+ Write `[[wiki links]]` in any knowledge prose (`--brief`, `--synthesis`) to reference related projects, entities, and notes — the CLI does not enforce link syntax; write them naturally in the text. This is what keeps the vault navigable as a wiki.
123
63
 
124
- # 3. Then update the note body via --synthesis with wiki links to related notes
125
- hafez update <slug> --synthesis "Insight text referencing [[related-slug]]"
126
- ```
64
+ **Knowledge boundary:** before writing an insight down, ask where it belongs. Useful beyond the current session or repo → vault knowledge note. Specific to one repo's code or workflow → that repo's own docs (CLAUDE.md, README, comments), not the vault.
127
65
 
128
66
  ## Batch Operations
129
67
 
130
- **Rule: Use `hafez batch` when making 2+ mutating calls.** Individual commands auto-commit after each call (and push, when the vault has a remote). Running multiple individual commands back-to-back causes git push races on synced vaults. Batch does all operations in one commit + one push.
68
+ **Rule: Use `hafez batch` when making 2+ mutating calls.** Individual commands auto-commit after each call (and push, when the vault has a remote). Running multiple individual commands back-to-back causes git push races on synced vaults. Batch does all operations in one commit + one push. For a single operation, individual commands are fine — they auto-sync.
131
69
 
132
70
  ```bash
133
71
  echo '[
@@ -147,8 +85,6 @@ On shells that can't pipe stdin to native executables (Windows PowerShell 5.1),
147
85
 
148
86
  **Slug contract.** Slugs derive deterministically from names: lowercase, non-alphanumeric runs become single `-` (`"Auth Refactor!"` → `auth-refactor`). Batch executes sequentially, so later ops can reference the slugs of earlier creates in the same payload — create-then-link works in one batch. Collisions hard-error, so a wrong prediction can't touch the wrong document; `hafez batch --dry-run` reports the derived slug for every create op if you want to verify first.
149
87
 
150
- For a single operation, individual commands are fine — they auto-sync.
151
-
152
88
  ## Session End — Digest
153
89
 
154
90
  At the end of a working session (or a natural breakpoint), record what happened:
@@ -173,24 +109,29 @@ Digest adds a session log entry to each touched entity and creates a session not
173
109
 
174
110
  Digest itself is pure — it only prints batch JSON; `hafez batch` is what writes. If you are unsure the slugs exist, run `hafez digest` alone first: unknown slugs warn on stderr (they are omitted from the output). Fix `entities_touched`, regenerate, and pipe to batch **once** — re-running the full pipe after a successful batch creates a duplicate session note.
175
111
 
176
- **Mid-session updates and the digest coexist.** Point-in-time updates (`--status done`, `--complete-action`, brief changes) happen as they happen — don't defer them to session end. Digest adds the overall narrative on top. Do NOT hand-write session log entries (`--log`) right before digesting — digest builds the session summary itself. Use `--log` mid-session only for significant milestones (deployed, merged, discovered a bug).
112
+ **Mid-session updates and the digest coexist.** Point-in-time updates (`--status done`, `--complete-action`, brief changes) happen as they happen — digest adds the overall narrative on top. Digest builds the session summary itself, so the session's closing log entry comes from digest; reserve hand-written `--log` for significant mid-session milestones (deployed, merged, discovered a bug).
177
113
 
178
- ## Knowledge Maintenance
114
+ Session end is also when knowledge gets written: review what was learned and run Search-First Ingest on anything worth keeping. Knowledge notes are the primary durable output of sessions — not a side effect.
179
115
 
180
- Knowledge notes are the primary durable output of sessions — not a side effect. Treat the vault as a living wiki.
116
+ ## Periodic Lint
181
117
 
182
- **Active maintenance pattern:**
183
- 1. At session start: search for knowledge relevant to the topic
184
- 2. During work: when insights emerge, search before creating — update existing notes first
185
- 3. Use `[[wiki links]]` in knowledge prose to connect related concepts
186
- 4. At session end: review what was learned and update or create knowledge notes
118
+ Run at session end or on demand:
187
119
 
188
- **Periodic lint** (run at session end or on demand):
189
120
  ```bash
190
121
  hafez validate
191
- hafez query --kind knowledge --filter stale
122
+ hafez query --kind knowledge --sort-by last_reinforced --sort-order asc --limit 20
192
123
  ```
193
- Surface orphaned notes (no related links) and stale knowledge for review or archival.
194
124
 
195
- **Knowledge boundary:** before writing an insight down, ask where it belongs. Useful beyond the current session or repo → vault knowledge note. Specific to one repo's code or workflow → that repo's own docs (CLAUDE.md, README, comments), not the vault.
125
+ Surface orphaned notes (no related links) and the least-recently-reinforced knowledge for review or archival. (`--filter` is entity status — knowledge notes have none, so the query command rejects it under `--kind knowledge`.)
126
+
127
+ ## Updating Hafez Itself
128
+
129
+ "update hafez" means the **tool**. `hafez update` is an entity update — a different thing entirely. Don't confuse them.
130
+
131
+ 1. Run `hafez upgrade`. It checks and prints; it never installs.
132
+ 2. Show the user the printed commands **verbatim** and wait for an explicit yes. They install code and overwrite the user's own agent instructions file — never run them unattended, and never run the tarball alternative alongside the npm install.
133
+ 3. Run the approved steps, then re-run `hafez upgrade` to verify.
134
+
135
+ If `hafez upgrade` fails with the literal prefix `Unknown command:`, the installed hafez predates it. Install the current version, re-copy the bundled skill, then run `hafez upgrade` — the README's Updating section (github.com/shrvn-r/hafez) has the exact commands.
196
136
 
137
+ `hafez upgrade` also prints the current integration snippet. Compare it against the Hafez block in the user's global instructions file and offer to reconcile differences — don't rewrite their file unasked.
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
- }