hafez 1.0.1

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -0
  3. package/dist/cli/batch-errors.d.ts +24 -0
  4. package/dist/cli/batch-errors.js +236 -0
  5. package/dist/cli/commands-schema.d.ts +3 -0
  6. package/dist/cli/commands-schema.js +80 -0
  7. package/dist/cli/commands.d.ts +133 -0
  8. package/dist/cli/commands.js +628 -0
  9. package/dist/cli/format.d.ts +9 -0
  10. package/dist/cli/format.js +158 -0
  11. package/dist/cli/help-agent.d.ts +6 -0
  12. package/dist/cli/help-agent.js +158 -0
  13. package/dist/cli/index.d.ts +1 -0
  14. package/dist/cli/index.js +304 -0
  15. package/dist/cli/resolve-vault.d.ts +13 -0
  16. package/dist/cli/resolve-vault.js +124 -0
  17. package/dist/cli/schema-introspect.d.ts +70 -0
  18. package/dist/cli/schema-introspect.js +372 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +3 -0
  21. package/dist/contracts.d.ts +26 -0
  22. package/dist/contracts.js +87 -0
  23. package/dist/db.d.ts +68 -0
  24. package/dist/db.js +663 -0
  25. package/dist/digest.d.ts +29 -0
  26. package/dist/digest.js +94 -0
  27. package/dist/export-okf.d.ts +12 -0
  28. package/dist/export-okf.js +176 -0
  29. package/dist/git.d.ts +12 -0
  30. package/dist/git.js +355 -0
  31. package/dist/index.d.ts +7 -0
  32. package/dist/index.js +909 -0
  33. package/dist/knowledge-index.d.ts +3 -0
  34. package/dist/knowledge-index.js +135 -0
  35. package/dist/merge.d.ts +1 -0
  36. package/dist/merge.js +176 -0
  37. package/dist/migrate-knowledge-v2.d.ts +11 -0
  38. package/dist/migrate-knowledge-v2.js +158 -0
  39. package/dist/migrate-next-actions.d.ts +6 -0
  40. package/dist/migrate-next-actions.js +32 -0
  41. package/dist/migrate-types.d.ts +11 -0
  42. package/dist/migrate-types.js +142 -0
  43. package/dist/parser.d.ts +18 -0
  44. package/dist/parser.js +68 -0
  45. package/dist/query.d.ts +24 -0
  46. package/dist/query.js +167 -0
  47. package/dist/schema.d.ts +9 -0
  48. package/dist/schema.js +59 -0
  49. package/dist/sections.d.ts +54 -0
  50. package/dist/sections.js +297 -0
  51. package/dist/templates.d.ts +8 -0
  52. package/dist/templates.js +39 -0
  53. package/dist/types.d.ts +321 -0
  54. package/dist/types.js +11 -0
  55. package/dist/vault.d.ts +12 -0
  56. package/dist/vault.js +45 -0
  57. package/package.json +67 -0
  58. package/skill/SKILL.md +190 -0
@@ -0,0 +1,18 @@
1
+ import type { SessionLogEntry, SessionLogType } from './types.js';
2
+ export declare function parseSessionLogHeading(line: string): {
3
+ date: string;
4
+ agent: string;
5
+ type: SessionLogType;
6
+ } | null;
7
+ export declare function formatSessionLogEntry(entry: SessionLogEntry): string;
8
+ export declare function countSessionLogEntries(body: string): number;
9
+ export declare function parseSessionLog(body: string): Array<{
10
+ date: string;
11
+ agent: string;
12
+ type: SessionLogType;
13
+ summary: string;
14
+ }>;
15
+ export declare function extractOldestSessionLogEntry(body: string): {
16
+ entry: string;
17
+ remaining: string;
18
+ } | null;
package/dist/parser.js ADDED
@@ -0,0 +1,68 @@
1
+ import { findNextStructuralHeading } from './sections.js';
2
+ const HEADING_RE = /^### (\d{4}-\d{2}-\d{2}) (?:—|--) (.+?)(?:\s+\[(\w+)\])?$/;
3
+ export function parseSessionLogHeading(line) {
4
+ const match = line.match(HEADING_RE);
5
+ if (!match)
6
+ return null;
7
+ return {
8
+ date: match[1],
9
+ agent: match[2],
10
+ type: match[3] || 'progress'
11
+ };
12
+ }
13
+ export function formatSessionLogEntry(entry) {
14
+ const today = new Date().toISOString().slice(0, 10);
15
+ const lines = [`### ${today} — ${entry.agent} [${entry.type}]`, `Summary: ${entry.summary}`];
16
+ if (entry.body)
17
+ lines.push(entry.body);
18
+ return lines.join('\n');
19
+ }
20
+ export function countSessionLogEntries(body) {
21
+ return (body.match(/^### \d{4}-\d{2}-\d{2} (?:—|--)/gm) || []).length;
22
+ }
23
+ export function parseSessionLog(body) {
24
+ // Inline section extraction to avoid circular import with sections.ts
25
+ const idx = body.indexOf('## Session Log');
26
+ if (idx === -1)
27
+ return [];
28
+ const afterPos = idx + '## Session Log'.length;
29
+ // Find end: next structural ## heading or end of string
30
+ const nextH = findNextStructuralHeading(body, afterPos);
31
+ const sectionContent = nextH !== -1 ? body.slice(afterPos, nextH) : body.slice(afterPos);
32
+ const entries = [];
33
+ const lines = sectionContent.split('\n');
34
+ let current = null;
35
+ let summaryLine = null;
36
+ for (const line of lines) {
37
+ const heading = parseSessionLogHeading(line);
38
+ if (heading) {
39
+ if (current && summaryLine) {
40
+ entries.push({ ...current, summary: summaryLine });
41
+ }
42
+ current = heading;
43
+ summaryLine = null;
44
+ }
45
+ else if (current && !summaryLine && line.startsWith('Summary: ')) {
46
+ summaryLine = line.slice('Summary: '.length);
47
+ }
48
+ }
49
+ if (current && summaryLine) {
50
+ entries.push({ ...current, summary: summaryLine });
51
+ }
52
+ return entries;
53
+ }
54
+ export function extractOldestSessionLogEntry(body) {
55
+ const sectionStart = body.indexOf('## Session Log');
56
+ if (sectionStart === -1)
57
+ return null;
58
+ const afterHeader = body.slice(sectionStart + '## Session Log'.length);
59
+ const entryStarts = [...afterHeader.matchAll(/^### \d{4}-\d{2}-\d{2} (?:—|--)/gm)];
60
+ if (entryStarts.length < 2)
61
+ return null;
62
+ // Entries are newest-first in the file, so the oldest is the last match
63
+ const lastIdx = entryStarts.length - 1;
64
+ const oldestStart = entryStarts[lastIdx].index;
65
+ const entry = afterHeader.slice(oldestStart).trim();
66
+ const remaining = body.slice(0, sectionStart + '## Session Log'.length) + afterHeader.slice(0, oldestStart);
67
+ return { entry, remaining };
68
+ }
@@ -0,0 +1,24 @@
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: (QueryResult | KnowledgeQueryResult)[];
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 ADDED
@@ -0,0 +1,167 @@
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' ? toQueryResult(i.slug, i) : toKnowledgeResult(i.slug, i)),
93
+ total,
94
+ };
95
+ }
96
+ export function queryKnowledge(index, opts) {
97
+ index.syncIfStale();
98
+ const qopts = { kind: 'knowledge' };
99
+ if (opts?.domain)
100
+ qopts.domain = opts.domain;
101
+ if (opts?.confidence)
102
+ qopts.confidence = opts.confidence;
103
+ if (opts?.subtype)
104
+ qopts.subtype = opts.subtype;
105
+ if (opts?.tags)
106
+ qopts.tags = opts.tags;
107
+ if (opts?.sort_by)
108
+ qopts.sort_by = opts.sort_by;
109
+ if (opts?.sort_order)
110
+ qopts.sort_order = opts.sort_order;
111
+ if (opts?.limit != null)
112
+ qopts.limit = opts.limit;
113
+ if (opts?.offset != null)
114
+ qopts.offset = opts.offset;
115
+ const { items, total } = index.queryItems(qopts);
116
+ return { items: items.map(i => toKnowledgeResult(i.slug, i)), total };
117
+ }
118
+ export function queryUnified(index, opts) {
119
+ index.syncIfStale();
120
+ validateDateOpts(opts);
121
+ const qopts = {};
122
+ if (opts?.kind && opts.kind !== 'all')
123
+ qopts.kind = opts.kind;
124
+ // When kind is 'all' or unset, no kind filter — both entities and knowledge are returned
125
+ if (opts?.filter && STATUS_FILTERS.has(opts.filter))
126
+ qopts.status = opts.filter;
127
+ if (opts?.filter === 'stale')
128
+ qopts.filter = 'stale';
129
+ if (opts?.filter === 'capture')
130
+ qopts.filter = 'capture';
131
+ if (opts?.type)
132
+ qopts.type = opts.type;
133
+ if (opts?.parent)
134
+ qopts.parent = opts.parent;
135
+ if (opts?.domain)
136
+ qopts.domain = opts.domain;
137
+ if (opts?.confidence)
138
+ qopts.confidence = opts.confidence;
139
+ if (opts?.since)
140
+ qopts.since = opts.since;
141
+ if (opts?.before)
142
+ qopts.before = opts.before;
143
+ if (opts?.createdSince)
144
+ qopts.createdSince = opts.createdSince;
145
+ if (opts?.createdBefore)
146
+ qopts.createdBefore = opts.createdBefore;
147
+ if (opts?.tags)
148
+ qopts.tags = opts.tags;
149
+ if (opts?.sort_by)
150
+ qopts.sort_by = opts.sort_by;
151
+ if (opts?.sort_order)
152
+ qopts.sort_order = opts.sort_order;
153
+ if (opts?.limit !== undefined)
154
+ qopts.limit = opts.limit;
155
+ if (opts?.offset !== undefined)
156
+ qopts.offset = opts.offset;
157
+ const result = index.queryItems(qopts);
158
+ const items = result.items.map(i => {
159
+ if (i.kind === 'entity') {
160
+ return { ...toQueryResult(i.slug, i), kind: 'entity' };
161
+ }
162
+ else {
163
+ return { ...toKnowledgeResult(i.slug, i), kind: 'knowledge' };
164
+ }
165
+ });
166
+ return { items, total: result.total };
167
+ }
@@ -0,0 +1,9 @@
1
+ export declare function validateEntityFrontmatter(fm: Record<string, any>): string[];
2
+ export declare function validateKnowledgeFrontmatter(fm: Record<string, any>): string[];
3
+ export declare function validateKnowledgeSubtype(fm: Record<string, any>): string[];
4
+ export declare function validateSlugReference(slug: string, field: string, exists: (s: string) => boolean): string | null;
5
+ export declare function validateSessionLogEntry(entry: {
6
+ type?: string;
7
+ summary?: string;
8
+ agent?: string;
9
+ }): string[];
package/dist/schema.js ADDED
@@ -0,0 +1,59 @@
1
+ // src/schema.ts
2
+ import { ENTITY_TYPES, ENTITY_STATUSES, CONFIDENCE_LEVELS, SESSION_LOG_TYPES, VALID_SUBTYPES, } from './contracts.js';
3
+ export function validateEntityFrontmatter(fm) {
4
+ const errors = [];
5
+ if (!fm.name || typeof fm.name !== 'string')
6
+ errors.push('missing required field: name');
7
+ if (!fm.type || !ENTITY_TYPES.includes(fm.type))
8
+ errors.push(`invalid type: ${fm.type} (expected: ${ENTITY_TYPES.join(', ')})`);
9
+ if (!fm.status || !ENTITY_STATUSES.includes(fm.status))
10
+ errors.push(`invalid status: ${fm.status} (expected: ${ENTITY_STATUSES.join(', ')})`);
11
+ if (!fm.created)
12
+ errors.push('missing required field: created');
13
+ if (!fm['last-touched'])
14
+ errors.push('missing required field: last-touched');
15
+ if (fm.domain !== undefined && !Array.isArray(fm.domain))
16
+ errors.push('entity domain must be an array');
17
+ return errors;
18
+ }
19
+ export function validateKnowledgeFrontmatter(fm) {
20
+ const errors = [];
21
+ if (!fm.name || typeof fm.name !== 'string')
22
+ errors.push('missing required field: name');
23
+ if (!fm.created)
24
+ errors.push('missing required field: created');
25
+ if (fm.confidence && !CONFIDENCE_LEVELS.includes(fm.confidence)) {
26
+ errors.push(`invalid confidence: ${fm.confidence} (expected: ${CONFIDENCE_LEVELS.join(', ')})`);
27
+ }
28
+ if (fm.domain !== undefined && !Array.isArray(fm.domain))
29
+ errors.push('knowledge domain must be an array');
30
+ errors.push(...validateKnowledgeSubtype(fm));
31
+ return errors;
32
+ }
33
+ export function validateKnowledgeSubtype(fm) {
34
+ const errors = [];
35
+ if (fm.subtype && !VALID_SUBTYPES.includes(fm.subtype)) {
36
+ errors.push(`invalid subtype: ${fm.subtype} (expected: ${VALID_SUBTYPES.join(', ')})`);
37
+ }
38
+ // Plan-specific: status must be active or done
39
+ if (fm.subtype === 'plan' && fm.status && !['active', 'done'].includes(fm.status)) {
40
+ errors.push(`invalid plan status: ${fm.status} (expected: active, done)`);
41
+ }
42
+ return errors;
43
+ }
44
+ export function validateSlugReference(slug, field, exists) {
45
+ if (!exists(slug))
46
+ return `${field} reference '${slug}' does not exist`;
47
+ return null;
48
+ }
49
+ export function validateSessionLogEntry(entry) {
50
+ const errors = [];
51
+ if (!entry.type || !SESSION_LOG_TYPES.includes(entry.type)) {
52
+ errors.push(`invalid session log type: ${entry.type} (expected: ${SESSION_LOG_TYPES.join(', ')})`);
53
+ }
54
+ if (!entry.summary || typeof entry.summary !== 'string')
55
+ errors.push('session log entry must have a summary');
56
+ if (!entry.agent || typeof entry.agent !== 'string')
57
+ errors.push('session log entry must have an agent');
58
+ return errors;
59
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Known structural section headings in Hafez entities and knowledge notes.
3
+ * Derived from contracts.ts (defaultSections + optionalSections) plus
4
+ * auto-generated sections not tracked in contracts (Related, Summary).
5
+ * Only these are treated as section boundaries. Any other `## ` heading
6
+ * inside section content (e.g. in a brief or session log) is preserved as-is.
7
+ */
8
+ export declare const STRUCTURAL_SECTIONS: Set<string>;
9
+ /**
10
+ * Find the next structural `## Heading` boundary after a given position.
11
+ * Skips `## ` headings that are not known structural sections (content headings).
12
+ * Returns the index of the `\n` before the `## `, or -1 if no structural heading found.
13
+ */
14
+ export declare function findNextStructuralHeading(body: string, from: number): number;
15
+ /**
16
+ * Find a ## Heading section and its content.
17
+ * Returns { start, end, content } or null if not found.
18
+ * Matches heading exactly (e.g. "## Brief" won't match "## Briefing").
19
+ * Only treats known structural headings as section boundaries -- content
20
+ * inside a section may contain arbitrary `## ` headings without corruption.
21
+ */
22
+ export declare function findSection(body: string, heading: string): {
23
+ start: number;
24
+ end: number;
25
+ content: string;
26
+ } | null;
27
+ export declare function getBrief(body: string): string | null;
28
+ export declare function setBrief(body: string, content: string): string;
29
+ export declare function removeBrief(body: string): string;
30
+ export declare function getNextActions(body: string): string[];
31
+ export declare function addNextAction(body: string, action: string): string;
32
+ export declare function completeNextAction(body: string, query: string): {
33
+ matched: string;
34
+ body: string;
35
+ };
36
+ export declare function removeNextAction(body: string, query: string): {
37
+ matched: string;
38
+ body: string;
39
+ };
40
+ export declare function clearNextActions(body: string): string;
41
+ export declare function firstNonEmptyLine(s: string | null | undefined): string;
42
+ /**
43
+ * One-line summary for a doc: frontmatter description first, then the first
44
+ * non-empty line of the kind's primary section (entity: Brief; knowledge:
45
+ * Synthesis, falling back to Goal for plans; session: Summary).
46
+ * Shared by the vault index and the OKF export so the two surfaces agree.
47
+ */
48
+ export declare function deriveSummary(kind: 'entity' | 'knowledge' | 'session', fm: Record<string, any>, body: string): string;
49
+ /** Matches [[slug]] or [[slug|display text]], capturing slug in group 1. Non-global — wrap with `new RegExp(WIKI_LINK_RE.source, 'g')` for iteration. */
50
+ export declare const WIKI_LINK_RE: RegExp;
51
+ export declare function syncRelatedSection(body: string, frontmatter: {
52
+ parent?: string;
53
+ related?: string[];
54
+ }): string;