hafez 1.2.0 → 1.3.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/batch-ops.d.ts +187 -0
- package/dist/batch-ops.js +322 -0
- package/dist/cli/args.d.ts +15 -0
- package/dist/cli/args.js +49 -0
- package/dist/cli/batch-errors.js +6 -3
- package/dist/cli/commands.d.ts +0 -108
- package/dist/cli/commands.js +154 -252
- package/dist/cli/help-agent.d.ts +0 -5
- package/dist/cli/help-agent.js +20 -5
- package/dist/cli/index.js +45 -65
- package/dist/cli/resolve-vault.js +2 -1
- package/dist/cli/schema-introspect.d.ts +1 -1
- package/dist/cli/schema-introspect.js +6 -22
- package/dist/cli.js +0 -0
- package/dist/contracts.d.ts +6 -0
- package/dist/contracts.js +8 -2
- package/dist/db.js +14 -28
- package/dist/digest.js +2 -0
- package/dist/document.d.ts +108 -0
- package/dist/{sections.js → document.js} +173 -22
- package/dist/export-okf.js +10 -22
- package/dist/git.d.ts +7 -8
- package/dist/git.js +39 -21
- package/dist/index.d.ts +3 -4
- package/dist/index.js +419 -517
- package/dist/knowledge-index.js +9 -7
- package/dist/merge.js +13 -25
- package/dist/query.d.ts +1 -1
- package/dist/query.js +3 -1
- package/dist/types.d.ts +48 -1
- package/dist/vault.d.ts +33 -6
- package/dist/vault.js +65 -8
- package/package.json +2 -2
- package/dist/parser.d.ts +0 -18
- package/dist/parser.js +0 -68
- package/dist/sections.d.ts +0 -54
package/dist/knowledge-index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/knowledge-index.ts
|
|
2
2
|
// Generates the vault-root index.md covering entities (grouped by status) and
|
|
3
3
|
// knowledge (grouped by domain) — local-only, auto-regenerated on every entity/knowledge write.
|
|
4
|
-
import {
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync } from 'fs';
|
|
5
5
|
import { join, basename } from 'path';
|
|
6
|
-
import { parseFilePath } from './vault.js';
|
|
7
|
-
import { deriveSummary } from './
|
|
6
|
+
import { parseFilePath, scanVaultDir, kindDir } from './vault.js';
|
|
7
|
+
import { deriveSummary } from './document.js';
|
|
8
8
|
import { ENTITY_STATUSES } from './contracts.js';
|
|
9
9
|
function truncate(s) {
|
|
10
10
|
return s.length > 100 ? s.slice(0, 97) + '...' : s;
|
|
@@ -19,9 +19,11 @@ function formatEntry(item) {
|
|
|
19
19
|
/** Parse every .md directly in dir (non-recursive — auto-excludes archive/), skipping malformed files. */
|
|
20
20
|
function scanParsed(dir) {
|
|
21
21
|
const docs = [];
|
|
22
|
-
for (const file of
|
|
22
|
+
for (const file of scanVaultDir(dir)) {
|
|
23
23
|
try {
|
|
24
|
-
const
|
|
24
|
+
const parsed = parseFilePath(file);
|
|
25
|
+
const fm = parsed.frontmatter;
|
|
26
|
+
const body = parsed.body;
|
|
25
27
|
if (!fm.name)
|
|
26
28
|
continue;
|
|
27
29
|
docs.push({ slug: basename(file, '.md'), fm, body });
|
|
@@ -55,8 +57,8 @@ export function ensureLocalExclude(vaultPath) {
|
|
|
55
57
|
catch { /* best effort — never fail a vault write over ignore hygiene */ }
|
|
56
58
|
}
|
|
57
59
|
export function generateVaultIndex(vaultPath) {
|
|
58
|
-
const entitiesDir = join(vaultPath, '
|
|
59
|
-
const knowledgeDir = join(vaultPath, 'knowledge');
|
|
60
|
+
const entitiesDir = join(vaultPath, kindDir('entity'));
|
|
61
|
+
const knowledgeDir = join(vaultPath, kindDir('knowledge'));
|
|
60
62
|
const hasEntities = existsSync(entitiesDir);
|
|
61
63
|
const hasKnowledge = existsSync(knowledgeDir);
|
|
62
64
|
if (!hasEntities && !hasKnowledge)
|
package/dist/merge.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/merge.ts
|
|
2
2
|
import { parseContent, serializeFile } from './vault.js';
|
|
3
|
-
import { parseSessionLogHeading } from './
|
|
3
|
+
import { parseSessionLogHeading, splitStructural } from './document.js';
|
|
4
|
+
import { APPEND_ONLY_SECTIONS } from './contracts.js';
|
|
4
5
|
// Fields that are ISO date strings — latest date wins
|
|
5
6
|
const DATE_FIELDS = ['last-touched', 'last-reinforced', 'created'];
|
|
6
7
|
// Fields that are numeric counters — max wins
|
|
@@ -51,41 +52,28 @@ function unionArrays(a, b) {
|
|
|
51
52
|
const arrB = Array.isArray(b) ? b : b ? [b] : [];
|
|
52
53
|
return [...new Set([...arrB, ...arrA])];
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
-
const parts = body.split(/\n(?=## )/g);
|
|
56
|
-
const sections = [];
|
|
57
|
-
let preamble = '';
|
|
58
|
-
for (const part of parts) {
|
|
59
|
-
if (part.startsWith('## ')) {
|
|
60
|
-
const nlIdx = part.indexOf('\n');
|
|
61
|
-
const heading = nlIdx === -1 ? part : part.slice(0, nlIdx);
|
|
62
|
-
const content = nlIdx === -1 ? '' : part.slice(nlIdx + 1);
|
|
63
|
-
sections.push({ heading: heading.trim(), content: content.trim() });
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
preamble = part.trim();
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return { preamble, sections };
|
|
70
|
-
}
|
|
55
|
+
// --- Body merge ---
|
|
71
56
|
function mergeBody(remoteBody, localBody, localWins) {
|
|
72
57
|
if (remoteBody === localBody)
|
|
73
58
|
return localBody;
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
// Structural split (see document.ts): sections are atomic — a user ##
|
|
60
|
+
// heading inside a Brief or a session-log entry body stays with its
|
|
61
|
+
// section instead of being split out and merged under its own policy.
|
|
62
|
+
const remote = splitStructural(remoteBody);
|
|
63
|
+
const local = splitStructural(localBody);
|
|
76
64
|
// Build merged sections starting from local (local section order is canonical)
|
|
77
65
|
const localHeadings = new Set(local.sections.map(s => s.heading));
|
|
78
66
|
const mergedSections = [];
|
|
79
67
|
for (const ls of local.sections) {
|
|
80
68
|
const rs = remote.sections.find(s => s.heading === ls.heading);
|
|
81
|
-
if (ls.heading === '
|
|
69
|
+
if (ls.heading === 'Session Log' && rs) {
|
|
82
70
|
mergedSections.push({ heading: ls.heading, content: mergeSessionLogs(rs.content, ls.content) });
|
|
83
71
|
}
|
|
84
|
-
else if (ls.heading === '
|
|
72
|
+
else if (ls.heading === 'Next Actions' && rs) {
|
|
85
73
|
mergedSections.push({ heading: ls.heading, content: mergeNextActions(rs.content, ls.content) });
|
|
86
74
|
}
|
|
87
|
-
else if ((ls.heading
|
|
88
|
-
// Append-only sections: union lines from both sides, dedup identical
|
|
75
|
+
else if (APPEND_ONLY_SECTIONS.has(ls.heading) && rs) {
|
|
76
|
+
// Append-only sections (contracts.ts): union lines from both sides, dedup identical
|
|
89
77
|
mergedSections.push({ heading: ls.heading, content: mergeAppendOnlyLines(rs.content, ls.content) });
|
|
90
78
|
}
|
|
91
79
|
else if (rs && !localWins) {
|
|
@@ -106,7 +94,7 @@ function mergeBody(remoteBody, localBody, localWins) {
|
|
|
106
94
|
const preamble = localWins
|
|
107
95
|
? (local.preamble || remote.preamble)
|
|
108
96
|
: (remote.preamble || local.preamble);
|
|
109
|
-
const parts = [preamble, ...mergedSections.map(s =>
|
|
97
|
+
const parts = [preamble, ...mergedSections.map(s => `## ${s.heading}\n\n${s.content}`)].filter(Boolean);
|
|
110
98
|
return parts.join('\n\n');
|
|
111
99
|
}
|
|
112
100
|
/** Union merge for append-only free-text sections: local lines first, then
|
package/dist/query.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare function queryChildren(index: HafezIndex, parentSlug: string): {
|
|
|
9
9
|
total: number;
|
|
10
10
|
};
|
|
11
11
|
export declare function queryRelatedTo(index: HafezIndex, slug: string, relation?: string): {
|
|
12
|
-
items:
|
|
12
|
+
items: UnifiedResult[];
|
|
13
13
|
total: number;
|
|
14
14
|
};
|
|
15
15
|
export declare function queryKnowledge(index: HafezIndex, opts?: KnowledgeQueryOpts): {
|
package/dist/query.js
CHANGED
|
@@ -89,7 +89,9 @@ export function queryRelatedTo(index, slug, relation) {
|
|
|
89
89
|
index.syncIfStale();
|
|
90
90
|
const { items, total } = index.queryItems({ relatedTo: slug, relation });
|
|
91
91
|
return {
|
|
92
|
-
items: items.map(i => i.kind === 'entity'
|
|
92
|
+
items: items.map(i => i.kind === 'entity'
|
|
93
|
+
? { ...toQueryResult(i.slug, i), kind: 'entity' }
|
|
94
|
+
: { ...toKnowledgeResult(i.slug, i), kind: 'knowledge' }),
|
|
93
95
|
total,
|
|
94
96
|
};
|
|
95
97
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -61,7 +61,30 @@ export interface KnowledgeFrontmatter {
|
|
|
61
61
|
status?: 'active' | 'done';
|
|
62
62
|
'last-touched'?: string;
|
|
63
63
|
}
|
|
64
|
+
export interface SessionFrontmatter {
|
|
65
|
+
name: string;
|
|
66
|
+
created: string;
|
|
67
|
+
'session-date'?: string;
|
|
68
|
+
related?: string[];
|
|
69
|
+
}
|
|
70
|
+
/** What parseFilePath returns: the kind comes from Layout (the file's
|
|
71
|
+
* directory), so consumers switch on `kind` instead of sniffing keys. */
|
|
72
|
+
export type ParsedVaultFile = {
|
|
73
|
+
kind: 'entity';
|
|
74
|
+
frontmatter: EntityFrontmatter;
|
|
75
|
+
body: string;
|
|
76
|
+
} | {
|
|
77
|
+
kind: 'knowledge';
|
|
78
|
+
frontmatter: KnowledgeFrontmatter;
|
|
79
|
+
body: string;
|
|
80
|
+
} | {
|
|
81
|
+
kind: 'session';
|
|
82
|
+
frontmatter: SessionFrontmatter;
|
|
83
|
+
body: string;
|
|
84
|
+
};
|
|
64
85
|
export interface ParsedFile<T = EntityFrontmatter | KnowledgeFrontmatter> {
|
|
86
|
+
/** Populated by read(): which side of the vault the slug resolved to. */
|
|
87
|
+
kind?: 'entity' | 'knowledge';
|
|
65
88
|
frontmatter: T;
|
|
66
89
|
body: string;
|
|
67
90
|
}
|
|
@@ -281,10 +304,34 @@ export declare class HafezError extends Error {
|
|
|
281
304
|
export interface GitConfig {
|
|
282
305
|
push?: boolean;
|
|
283
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* The persistence seam (see CONTEXT.md: Journal) — everything core needs
|
|
309
|
+
* from the history layer behind vault mutations. Git satisfies it in
|
|
310
|
+
* production (src/git.ts); tests may inject an in-memory adapter.
|
|
311
|
+
*/
|
|
312
|
+
export interface Journal {
|
|
313
|
+
/** Persist one logical change: stage written+deleted paths (vault-relative)
|
|
314
|
+
* and commit under message. Must clean up its own partial state (e.g.
|
|
315
|
+
* staged files) before rethrowing on a commit-stage failure; a push-stage
|
|
316
|
+
* failure surfaces as GIT_PUSH_FAILED with the commit kept. */
|
|
317
|
+
commit(written: string[], deleted: string[], message: string): Promise<void>;
|
|
318
|
+
/** Bidirectional sync with the remote, if any. */
|
|
319
|
+
sync(): Promise<{
|
|
320
|
+
pulled: boolean;
|
|
321
|
+
pushed: boolean;
|
|
322
|
+
remote: boolean;
|
|
323
|
+
}>;
|
|
324
|
+
/** History-derived changelog for entities/ and knowledge/ since a date. */
|
|
325
|
+
changelog(since: string): Promise<ChangelogEntry[]>;
|
|
326
|
+
/** Newest history timestamp per vault file (modified = any change, added = creation). */
|
|
327
|
+
fileTimes(mode: 'modified' | 'added'): Promise<Map<string, string>>;
|
|
328
|
+
}
|
|
284
329
|
export interface HafezConfig {
|
|
285
330
|
vaultPath: string;
|
|
286
331
|
readOnly?: boolean;
|
|
287
332
|
git?: GitConfig;
|
|
333
|
+
/** Override the persistence layer. Defaults to the git adapter. */
|
|
334
|
+
persistence?: Journal;
|
|
288
335
|
onWrite?: (slug: string, operation: string) => void;
|
|
289
336
|
}
|
|
290
337
|
export interface Hafez {
|
|
@@ -318,7 +365,7 @@ export interface Hafez {
|
|
|
318
365
|
total: number;
|
|
319
366
|
}>;
|
|
320
367
|
related_to(slug: string, relation?: string): Promise<{
|
|
321
|
-
items:
|
|
368
|
+
items: UnifiedResult[];
|
|
322
369
|
total: number;
|
|
323
370
|
}>;
|
|
324
371
|
query_knowledge(opts?: KnowledgeQueryOpts): Promise<{
|
package/dist/vault.d.ts
CHANGED
|
@@ -1,11 +1,38 @@
|
|
|
1
|
+
import type { ParsedVaultFile } from './types.js';
|
|
1
2
|
export declare function slugify(name: string): string;
|
|
2
|
-
export
|
|
3
|
+
export type VaultKind = 'entity' | 'knowledge' | 'session';
|
|
4
|
+
/** Kinds that live in the SQLite read index (sessions are files-and-git only). */
|
|
5
|
+
export declare const INDEXED_KINDS: ReadonlyArray<'entity' | 'knowledge'>;
|
|
6
|
+
/** Vault directory for a kind, relative to the vault root. */
|
|
7
|
+
export declare function kindDir(kind: VaultKind): string;
|
|
8
|
+
/** A vault content file: anything a sync/merge must treat semantically. */
|
|
9
|
+
export declare const VAULT_FILE_RE: RegExp;
|
|
10
|
+
/** True for vault-relative paths whose kind lives in the read index. */
|
|
11
|
+
export declare function isIndexedRelPath(relPath: string): boolean;
|
|
12
|
+
/** History pathspecs covering the indexed dirs (changelog, file times). */
|
|
13
|
+
export declare const INDEXED_DIR_PATHSPECS: ReadonlyArray<string>;
|
|
14
|
+
/** Classify a vault-relative path into an indexed kind + slug; null outside
|
|
15
|
+
* the indexed dirs (sessions, non-vault files). Nested paths keep their
|
|
16
|
+
* historical shape: entities/archive/x-log.md → entity 'archive/x-log'. */
|
|
17
|
+
export declare function classifyIndexedRelPath(relPath: string): {
|
|
18
|
+
kind: 'entity' | 'knowledge';
|
|
19
|
+
slug: string;
|
|
20
|
+
} | null;
|
|
21
|
+
/** All .md files directly in dir — non-recursive (archive/ subdirs are
|
|
22
|
+
* invisible by construction) and dotfile-free: dotfile slugs are rejected by
|
|
23
|
+
* resolveFilePath, so listing them would surface files no operation can reach. */
|
|
24
|
+
export declare function scanVaultDir(dirPath: string): string[];
|
|
25
|
+
/** Where an entity's overflow session-log entries are archived. */
|
|
26
|
+
export declare function archiveLogPath(vaultPath: string, slug: string): string;
|
|
27
|
+
export declare function resolveFilePath(vaultPath: string, slug: string, kind: VaultKind): string;
|
|
3
28
|
export declare function kindFromPath(filePath: string): 'entity' | 'knowledge';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
29
|
+
/** Full-kind path classification (kindFromPath only distinguishes the
|
|
30
|
+
* indexed kinds — sessions default to knowledge there for compatibility).
|
|
31
|
+
* Session files live directly in sessions/, so only the immediate parent
|
|
32
|
+
* counts — matching anywhere in the path misclassifies every file of a
|
|
33
|
+
* vault that happens to live under a directory named "sessions". */
|
|
34
|
+
export declare function vaultKindFromPath(filePath: string): VaultKind;
|
|
35
|
+
export declare function parseFilePath(path: string): ParsedVaultFile;
|
|
9
36
|
export declare function parseContent(content: string): {
|
|
10
37
|
frontmatter: Record<string, any>;
|
|
11
38
|
body: string;
|
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,
|
|
4
|
+
import { readFileSync, readdirSync } 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
|
|
@@ -16,6 +16,48 @@ export function slugify(name) {
|
|
|
16
16
|
.replace(/^-|-$/g, '')
|
|
17
17
|
.replace(/-{2,}/g, '-');
|
|
18
18
|
}
|
|
19
|
+
const KIND_DIRS = { entity: 'entities', knowledge: 'knowledge', session: 'sessions' };
|
|
20
|
+
/** Kinds that live in the SQLite read index (sessions are files-and-git only). */
|
|
21
|
+
export const INDEXED_KINDS = ['entity', 'knowledge'];
|
|
22
|
+
/** Vault directory for a kind, relative to the vault root. */
|
|
23
|
+
export function kindDir(kind) {
|
|
24
|
+
return KIND_DIRS[kind];
|
|
25
|
+
}
|
|
26
|
+
/** A vault content file: anything a sync/merge must treat semantically. */
|
|
27
|
+
export const VAULT_FILE_RE = new RegExp(`^(?:${Object.values(KIND_DIRS).join('|')})/.+\\.md$`);
|
|
28
|
+
/** True for vault-relative paths whose kind lives in the read index. */
|
|
29
|
+
export function isIndexedRelPath(relPath) {
|
|
30
|
+
return INDEXED_KINDS.some(k => relPath.startsWith(`${KIND_DIRS[k]}/`));
|
|
31
|
+
}
|
|
32
|
+
/** History pathspecs covering the indexed dirs (changelog, file times). */
|
|
33
|
+
export const INDEXED_DIR_PATHSPECS = INDEXED_KINDS.map(k => `${KIND_DIRS[k]}/`);
|
|
34
|
+
/** Classify a vault-relative path into an indexed kind + slug; null outside
|
|
35
|
+
* the indexed dirs (sessions, non-vault files). Nested paths keep their
|
|
36
|
+
* historical shape: entities/archive/x-log.md → entity 'archive/x-log'. */
|
|
37
|
+
export function classifyIndexedRelPath(relPath) {
|
|
38
|
+
for (const kind of INDEXED_KINDS) {
|
|
39
|
+
const prefix = `${KIND_DIRS[kind]}/`;
|
|
40
|
+
if (relPath.startsWith(prefix)) {
|
|
41
|
+
return { kind, slug: relPath.replace(prefix, '').replace('.md', '') };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** All .md files directly in dir — non-recursive (archive/ subdirs are
|
|
47
|
+
* invisible by construction) and dotfile-free: dotfile slugs are rejected by
|
|
48
|
+
* resolveFilePath, so listing them would surface files no operation can reach. */
|
|
49
|
+
export function scanVaultDir(dirPath) {
|
|
50
|
+
try {
|
|
51
|
+
return readdirSync(dirPath).filter(f => f.endsWith('.md') && !f.startsWith('.')).map(f => join(dirPath, f));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Where an entity's overflow session-log entries are archived. */
|
|
58
|
+
export function archiveLogPath(vaultPath, slug) {
|
|
59
|
+
return join(vaultPath, KIND_DIRS.entity, 'archive', `${slug}-log.md`);
|
|
60
|
+
}
|
|
19
61
|
// Slugs are path components: anything with separators (or a leading dot, or a
|
|
20
62
|
// Windows drive colon) could escape the vault — "../../outside/secret" reads,
|
|
21
63
|
// writes, and deletes files anywhere on disk. Block only those; spaces and
|
|
@@ -25,8 +67,7 @@ export function resolveFilePath(vaultPath, slug, kind) {
|
|
|
25
67
|
if (!slug || slug.startsWith('.') || /[/\\:\0]/.test(slug)) {
|
|
26
68
|
throw new HafezError('VALIDATION_FAILED', `Invalid slug: '${slug}'`);
|
|
27
69
|
}
|
|
28
|
-
|
|
29
|
-
return join(vaultPath, dir, `${slug}.md`);
|
|
70
|
+
return join(vaultPath, KIND_DIRS[kind], `${slug}.md`);
|
|
30
71
|
}
|
|
31
72
|
// Separator-agnostic: OS paths use '/' or '\' depending on platform, and a
|
|
32
73
|
// POSIX-only check silently classifies every entity as knowledge on Windows
|
|
@@ -35,10 +76,6 @@ export function resolveFilePath(vaultPath, slug, kind) {
|
|
|
35
76
|
export function kindFromPath(filePath) {
|
|
36
77
|
return /[/\\]entities[/\\]/.test(filePath) ? 'entity' : 'knowledge';
|
|
37
78
|
}
|
|
38
|
-
export function slugExists(vaultPath, slug) {
|
|
39
|
-
return existsSync(resolveFilePath(vaultPath, slug, 'entity'))
|
|
40
|
-
|| existsSync(resolveFilePath(vaultPath, slug, 'knowledge'));
|
|
41
|
-
}
|
|
42
79
|
// Fields that must always be arrays when present (may be scalar in hand-edited YAML)
|
|
43
80
|
const ARRAY_FIELDS = ['related', 'tags', 'domain'];
|
|
44
81
|
function normalizeFrontmatter(fm) {
|
|
@@ -49,10 +86,30 @@ function normalizeFrontmatter(fm) {
|
|
|
49
86
|
}
|
|
50
87
|
return fm;
|
|
51
88
|
}
|
|
89
|
+
/** Full-kind path classification (kindFromPath only distinguishes the
|
|
90
|
+
* indexed kinds — sessions default to knowledge there for compatibility).
|
|
91
|
+
* Session files live directly in sessions/, so only the immediate parent
|
|
92
|
+
* counts — matching anywhere in the path misclassifies every file of a
|
|
93
|
+
* vault that happens to live under a directory named "sessions". */
|
|
94
|
+
export function vaultKindFromPath(filePath) {
|
|
95
|
+
if (new RegExp(`[/\\\\]${KIND_DIRS.session}[/\\\\][^/\\\\]+$`).test(filePath))
|
|
96
|
+
return 'session';
|
|
97
|
+
return kindFromPath(filePath);
|
|
98
|
+
}
|
|
52
99
|
export function parseFilePath(path) {
|
|
53
100
|
const content = readFileSync(path, 'utf-8');
|
|
54
101
|
const { data, content: body } = matter(content, MATTER_OPTIONS);
|
|
55
|
-
|
|
102
|
+
const fm = normalizeFrontmatter(data);
|
|
103
|
+
const trimmed = body.trim();
|
|
104
|
+
const kind = vaultKindFromPath(path);
|
|
105
|
+
// Types over classes (C7): the frontmatter is normalized once here and
|
|
106
|
+
// asserted to the kind's shape — validation of required fields stays with
|
|
107
|
+
// schema.ts and the index builder, which tolerate malformed files.
|
|
108
|
+
if (kind === 'entity')
|
|
109
|
+
return { kind, frontmatter: fm, body: trimmed };
|
|
110
|
+
if (kind === 'session')
|
|
111
|
+
return { kind, frontmatter: fm, body: trimmed };
|
|
112
|
+
return { kind: 'knowledge', frontmatter: fm, body: trimmed };
|
|
56
113
|
}
|
|
57
114
|
export function parseContent(content) {
|
|
58
115
|
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
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"LICENSE"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "rm -rf dist && tsc",
|
|
20
|
+
"build": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
|
21
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
22
|
"pretest": "npm run build",
|
|
23
23
|
"test": "vitest run",
|
package/dist/parser.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/sections.d.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
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;
|