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/cli/index.js CHANGED
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { createHafez } from '../index.js';
6
6
  import { HafezError } from '../types.js';
7
+ import { parseArgs, UsageError } from './args.js';
7
8
  import { cmdRead, cmdQuery, cmdSearch, cmdCreate, cmdUpdate, cmdLink, cmdUnlink, cmdPromote, cmdValidate, cmdIndexRebuild, cmdBatch, cmdSync, cmdStats, cmdChangelog, cmdDigest, cmdCapture } from './commands.js';
8
9
  import { cmdSchema } from './commands-schema.js';
9
10
  import { renderAgentHelp } from './help-agent.js';
@@ -11,6 +12,7 @@ import { renderOnboard } from './onboard.js';
11
12
  import { resolveVaultPath, noVaultMessage, cmdInit } from './resolve-vault.js';
12
13
  const COMMANDS = {
13
14
  read: cmdRead,
15
+ sync: cmdSync,
14
16
  query: cmdQuery,
15
17
  search: cmdSearch,
16
18
  create: cmdCreate,
@@ -25,7 +27,7 @@ const COMMANDS = {
25
27
  index: (os, args) => {
26
28
  if (args[0] === 'rebuild')
27
29
  return cmdIndexRebuild(os);
28
- throw new Error('Usage: hafez index rebuild');
30
+ throw new UsageError('Usage: hafez index rebuild');
29
31
  },
30
32
  stats: cmdStats,
31
33
  changelog: cmdChangelog,
@@ -76,6 +78,33 @@ Global flags:
76
78
 
77
79
  Vault discovery: --vault flag > ~/.config/hafez/vault
78
80
  `;
81
+ /** Exit codes, documented in `hafez help --agent`. 0 = success; 4 is the
82
+ * mutate-succeeded-sync-failed warning path; anything unmapped = 1. */
83
+ const EXIT_CODES = {
84
+ NOT_FOUND: 1,
85
+ VALIDATION_FAILED: 2,
86
+ GIT_PUSH_FAILED: 3,
87
+ SLUG_EXISTS: 5,
88
+ GIT_COMMIT_FAILED: 6,
89
+ VAULT_LOCKED: 7,
90
+ };
91
+ /** The one error → exit adapter. Usage errors (bad flags/args) are distinct
92
+ * from vault errors and from unknown crashes. */
93
+ function report(err) {
94
+ if (err instanceof UsageError) {
95
+ process.stderr.write(`Error: ${err.message}\n`);
96
+ process.exit(2);
97
+ }
98
+ if (err instanceof HafezError) {
99
+ process.stderr.write(`Error: ${err.message}\n`);
100
+ if (err.details?.length) {
101
+ for (const detail of err.details)
102
+ process.stderr.write(` - ${detail}\n`);
103
+ }
104
+ process.exit(EXIT_CODES[err.code] ?? 1);
105
+ }
106
+ failUnknown(err);
107
+ }
79
108
  // Terminal handler for non-Hafez errors. Special-cases unbuilt better-sqlite3
80
109
  // bindings: npm 11+ blocks install scripts by default, so `npm install -g
81
110
  // hafez` reports success with the native module unbuilt and the first indexed
@@ -137,15 +166,7 @@ export async function main(argv) {
137
166
  process.stdout.write(result + '\n');
138
167
  }
139
168
  catch (err) {
140
- if (err instanceof HafezError) {
141
- process.stderr.write(`Error: ${err.message}\n`);
142
- if (err.details?.length) {
143
- for (const detail of err.details)
144
- process.stderr.write(` - ${detail}\n`);
145
- }
146
- process.exit(2);
147
- }
148
- failUnknown(err);
169
+ report(err);
149
170
  }
150
171
  return;
151
172
  }
@@ -167,59 +188,28 @@ export async function main(argv) {
167
188
  const vaultPath = resolution;
168
189
  // export runs before Hafez instance creation — read-only, no auto-sync
169
190
  if (command === 'export') {
170
- if (!args.includes('--okf')) {
171
- process.stderr.write('Usage: hafez export --okf [--out <dir>]\n');
172
- process.exit(1);
173
- }
174
- const outIdx = args.indexOf('--out');
175
- let outDir = resolve(process.cwd(), 'okf-export');
176
- if (outIdx !== -1) {
177
- const outValue = args[outIdx + 1];
178
- if (!outValue) {
179
- process.stderr.write('Usage: hafez export --okf [--out <dir>]\n');
180
- process.exit(1);
181
- }
182
- outDir = resolve(outValue);
183
- }
184
- const { exportOkf } = await import('../export-okf.js');
185
191
  try {
186
- const report = exportOkf(vaultPath, outDir);
192
+ const EXPORT_USAGE = 'Usage: hafez export --okf [--out <dir>]';
193
+ const { flags } = parseArgs(args, { okf: 'boolean', out: 'string' }, EXPORT_USAGE);
194
+ if (flags.okf !== true)
195
+ throw new UsageError(EXPORT_USAGE);
196
+ const outDir = typeof flags.out === 'string' ? resolve(flags.out) : resolve(process.cwd(), 'okf-export');
197
+ const { exportOkf } = await import('../export-okf.js');
198
+ const exportReport = exportOkf(vaultPath, outDir);
187
199
  if (json) {
188
- process.stdout.write(JSON.stringify(report, null, 2) + '\n');
200
+ process.stdout.write(JSON.stringify(exportReport, null, 2) + '\n');
189
201
  }
190
202
  else {
191
- const lines = [`Exported ${report.entities} entities, ${report.knowledge} knowledge, ${report.sessions} sessions to ${report.outDir}`];
192
- for (const s of report.skipped)
203
+ const lines = [`Exported ${exportReport.entities} entities, ${exportReport.knowledge} knowledge, ${exportReport.sessions} sessions to ${exportReport.outDir}`];
204
+ for (const s of exportReport.skipped)
193
205
  lines.push(` skipped ${s.file}: ${s.reason}`);
194
- if (report.unresolvedLinks > 0)
195
- lines.push(` unresolved links: ${report.unresolvedLinks}`);
206
+ if (exportReport.unresolvedLinks > 0)
207
+ lines.push(` unresolved links: ${exportReport.unresolvedLinks}`);
196
208
  process.stdout.write(lines.join('\n') + '\n');
197
209
  }
198
210
  }
199
211
  catch (err) {
200
- failUnknown(err);
201
- }
202
- return;
203
- }
204
- // sync is handled specially — always runs, not gated by MUTATING_COMMANDS
205
- if (command === 'sync') {
206
- const os = createHafez({ vaultPath, git: { push: false } });
207
- try {
208
- const result = await cmdSync(os, args);
209
- process.stdout.write(result + '\n');
210
- }
211
- catch (err) {
212
- if (err instanceof HafezError) {
213
- process.stderr.write(`Error: ${err.message}\n`);
214
- if (err.details?.length) {
215
- for (const detail of err.details) {
216
- process.stderr.write(` - ${detail}\n`);
217
- }
218
- }
219
- const exitCodes = { NOT_FOUND: 1, VALIDATION_FAILED: 2, GIT_PUSH_FAILED: 3 };
220
- process.exit(exitCodes[err.code] ?? 1);
221
- }
222
- failUnknown(err);
212
+ report(err);
223
213
  }
224
214
  return;
225
215
  }
@@ -246,16 +236,6 @@ export async function main(argv) {
246
236
  process.stdout.write(result + '\n');
247
237
  }
248
238
  catch (err) {
249
- if (err instanceof HafezError) {
250
- process.stderr.write(`Error: ${err.message}\n`);
251
- if (err.details?.length) {
252
- for (const detail of err.details) {
253
- process.stderr.write(` - ${detail}\n`);
254
- }
255
- }
256
- const exitCodes = { NOT_FOUND: 1, VALIDATION_FAILED: 2, GIT_PUSH_FAILED: 3 };
257
- process.exit(exitCodes[err.code] ?? 1);
258
- }
259
- failUnknown(err);
239
+ report(err);
260
240
  }
261
241
  }
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs
3
3
  import { join, resolve } from 'path';
4
4
  import { homedir } from 'os';
5
5
  import { execSync } from 'child_process';
6
+ import { kindDir } from '../vault.js';
6
7
  export function getConfigDir() {
7
8
  const xdg = process.env.XDG_CONFIG_HOME;
8
9
  return join(xdg || join(homedir(), '.config'), 'hafez');
@@ -28,7 +29,7 @@ export function saveVaultConfig(vaultPath) {
28
29
  writeFileSync(join(dir, 'vault'), resolve(vaultPath) + '\n');
29
30
  }
30
31
  function isVault(dirPath) {
31
- return existsSync(dirPath) && statSync(dirPath).isDirectory() && existsSync(join(dirPath, 'entities'));
32
+ return existsSync(dirPath) && statSync(dirPath).isDirectory() && existsSync(join(dirPath, kindDir('entity')));
32
33
  }
33
34
  /**
34
35
  * Resolve vault path. Returns string or null.
@@ -1,4 +1,4 @@
1
- import { BatchOperationSchema, SessionLogSchema, UpdateFieldsSchema } from './commands.js';
1
+ import { BatchOperationSchema, SessionLogSchema, UpdateFieldsSchema } from '../batch-ops.js';
2
2
  export interface FieldSchema {
3
3
  type: string;
4
4
  optional?: boolean;
@@ -10,7 +10,7 @@
10
10
  // types actually used in BatchOperationSchema plus a few forward-looking ones
11
11
  // (`default`, `effects`, `union`) so the walker does not explode when the
12
12
  // schema is extended.
13
- import { BatchOperationSchema, SessionLogSchema, UpdateFieldsSchema, } from './commands.js';
13
+ import { BatchOperationSchema, SessionLogSchema, UpdateFieldsSchema, OP_SPECS, } from '../batch-ops.js';
14
14
  import { DigestInputSchema } from '../digest.js';
15
15
  import { ALIAS_KEYS } from '../contracts.js';
16
16
  // --- Walker ---
@@ -155,16 +155,8 @@ export const DIGEST_EXAMPLE = {
155
155
  session_date: '2026-01-15',
156
156
  agent: 'claude',
157
157
  };
158
- const OP_DESCRIPTIONS = {
159
- 'update': 'Update an entity or knowledge note (status, brief, next actions, session log, synthesis, …).',
160
- 'create-entity': 'Create a new entity (capture / entity / project).',
161
- 'create-knowledge': 'Create a new knowledge note (insight / plan subtype).',
162
- 'create-session': 'Create a new session note linked to an entity.',
163
- 'capture': 'Quick capture — a raw inbox item with no structure required.',
164
- 'link': 'Add a parent or related link between two entities.',
165
- 'unlink': 'Remove a parent or related link between two entities.',
166
- 'promote': 'Promote a capture to entity/project/knowledge, or an entity to project.',
167
- };
158
+ // Descriptions live in the Op Spec table (src/batch-ops.ts) — one row per op.
159
+ const OP_DESCRIPTIONS = Object.fromEntries(OP_SPECS.map(s => [s.name, s.description]));
168
160
  export function listOps() {
169
161
  return enumerateBranches().map(({ op, kind }) => {
170
162
  const name = kind ? `${op}-${kind}` : op;
@@ -293,17 +285,9 @@ function shouldStrip(path, _key) {
293
285
  return Object.prototype.hasOwnProperty.call(ALIAS_KEYS, path);
294
286
  }
295
287
  function isDefaultIncluded(op, kind, fieldKey) {
296
- if (op === 'create' && kind === 'entity' && fieldKey === 'type')
297
- return true;
298
- if (op === 'create' && kind === 'knowledge' && (fieldKey === 'subtype' || fieldKey === 'synthesis'))
299
- return true;
300
- if (op === 'create' && kind === 'session' && fieldKey === 'synthesis')
301
- return true;
302
- if (op === 'update' && fieldKey === 'status')
303
- return true;
304
- if (op === 'capture' && fieldKey === 'notes')
305
- return true;
306
- return false;
288
+ // Example-inclusion hints live in the Op Spec table, one row per op.
289
+ const spec = OP_SPECS.find(s => s.op === op && s.kind === kind);
290
+ return spec ? spec.exampleInclude.includes(fieldKey) : false;
307
291
  }
308
292
  function sampleValue(field) {
309
293
  switch (field.type) {
package/dist/cli.js CHANGED
File without changes
@@ -24,3 +24,9 @@ export declare const ALIAS_KEYS: {
24
24
  readonly 'create.session.fields.insight': "synthesis";
25
25
  };
26
26
  export declare const SUBTYPE_SECTIONS: Record<KnowledgeSubtype, string[]>;
27
+ /** Sections whose content only ever grows — sync merges union their lines
28
+ * instead of letting the newer side replace the older (see merge.ts). */
29
+ export declare const APPEND_ONLY_SECTIONS: ReadonlySet<string>;
30
+ /** Session-file sections. Sessions have no TypeContract (they are not typed
31
+ * entities), but their headings are still structural for Document splitting. */
32
+ export declare const SESSION_SECTIONS: readonly ["Summary", "Entities Touched", "Decisions"];
package/dist/contracts.js CHANGED
@@ -63,8 +63,8 @@ export function hasCapability(type, section) {
63
63
  return contract.defaultSections.includes(section) || contract.optionalSections.includes(section);
64
64
  }
65
65
  // --- Canonical enum tuples (single source of truth) ---
66
- // Every consumer (file validators, CLI input validator, help renderer) imports these.
67
- // The cli-schema-parity test enforces that no other file declares local copies.
66
+ // Every consumer (file validators, the Op Spec table, help renderer) imports
67
+ // these — no other file may declare local copies.
68
68
  export const ENTITY_TYPES = ['capture', 'entity', 'project'];
69
69
  export const ENTITY_STATUSES = ['active', 'paused', 'done'];
70
70
  export const SESSION_LOG_TYPES = ['progress', 'decision', 'blocker', 'research'];
@@ -85,3 +85,9 @@ export const SUBTYPE_SECTIONS = {
85
85
  insight: ['Synthesis', 'Evidence', 'Sources'],
86
86
  plan: ['Goal', 'Steps', 'Dependencies'],
87
87
  };
88
+ /** Sections whose content only ever grows — sync merges union their lines
89
+ * instead of letting the newer side replace the older (see merge.ts). */
90
+ export const APPEND_ONLY_SECTIONS = new Set(['Evidence', 'Sources']);
91
+ /** Session-file sections. Sessions have no TypeContract (they are not typed
92
+ * entities), but their headings are still structural for Document splitting. */
93
+ export const SESSION_SECTIONS = ['Summary', 'Entities Touched', 'Decisions'];
package/dist/db.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import { createHash } from 'crypto';
3
- import { readdirSync, readFileSync, statSync, existsSync, unlinkSync } from 'fs';
3
+ import { readFileSync, statSync, existsSync, unlinkSync } from 'fs';
4
4
  import { join, basename } from 'path';
5
- import { parseFilePath } from './vault.js';
5
+ import { parseFilePath, scanVaultDir, kindDir, INDEXED_KINDS } from './vault.js';
6
6
  import { ensureLocalExclude } from './knowledge-index.js';
7
- import { getNextActions, getBrief, WIKI_LINK_RE, findSection } from './sections.js';
8
- import { parseSessionLog } from './parser.js';
7
+ import { getNextActions, getBrief, WIKI_LINK_RE, findSection } from './document.js';
8
+ import { parseSessionLog } from './document.js';
9
9
  // Bump this when indexing logic changes to force a full rebuild.
10
10
  // v5: wiki link harvesting (mention relations), parent link indexing, relation filter on queries.
11
11
  // v6: description column on items.
@@ -86,16 +86,6 @@ const STOPWORDS = new Set([
86
86
  function hashContent(content) {
87
87
  return createHash('sha256').update(content).digest('hex');
88
88
  }
89
- function scanDir(dirPath) {
90
- try {
91
- // Skip dotfiles: their slugs are rejected by resolveFilePath, so indexing
92
- // them would list entities that read/update can never reach.
93
- return readdirSync(dirPath).filter(f => f.endsWith('.md') && !f.startsWith('.')).map(f => join(dirPath, f));
94
- }
95
- catch {
96
- return [];
97
- }
98
- }
99
89
  // --- Shared query functions (used by both write and readonly paths) ---
100
90
  function sanitizeFtsQuery(raw) {
101
91
  const cleaned = raw.replace(/[^\w\s]/g, ' ');
@@ -583,8 +573,8 @@ export function createIndex(vaultPath, opts) {
583
573
  while ((wikiMatch = linkRe.exec(authoredBody)) !== null) {
584
574
  const targetSlug = wikiMatch[1];
585
575
  // Check if target file exists on disk (not in index — target may not be indexed yet)
586
- const targetEntityPath = join(vaultPath, 'entities', `${targetSlug}.md`);
587
- const targetKnowledgePath = join(vaultPath, 'knowledge', `${targetSlug}.md`);
576
+ const targetEntityPath = join(vaultPath, kindDir('entity'), `${targetSlug}.md`);
577
+ const targetKnowledgePath = join(vaultPath, kindDir('knowledge'), `${targetSlug}.md`);
588
578
  const targetExists = knownSlugs
589
579
  ? knownSlugs.has(targetSlug)
590
580
  : existsSync(targetEntityPath) || existsSync(targetKnowledgePath);
@@ -598,16 +588,16 @@ export function createIndex(vaultPath, opts) {
598
588
  db.exec('DELETE FROM links');
599
589
  // Collect all vault slugs for wiki link target validation
600
590
  const allVaultSlugs = new Set();
601
- for (const file of scanDir(join(vaultPath, 'entities'))) {
591
+ for (const file of scanVaultDir(join(vaultPath, kindDir('entity')))) {
602
592
  allVaultSlugs.add(basename(file, '.md'));
603
593
  }
604
- for (const file of scanDir(join(vaultPath, 'knowledge'))) {
594
+ for (const file of scanVaultDir(join(vaultPath, kindDir('knowledge')))) {
605
595
  allVaultSlugs.add(basename(file, '.md'));
606
596
  }
607
- for (const file of scanDir(join(vaultPath, 'entities'))) {
597
+ for (const file of scanVaultDir(join(vaultPath, kindDir('entity')))) {
608
598
  indexFile(file, 'entity', allVaultSlugs);
609
599
  }
610
- for (const file of scanDir(join(vaultPath, 'knowledge'))) {
600
+ for (const file of scanVaultDir(join(vaultPath, kindDir('knowledge')))) {
611
601
  indexFile(file, 'knowledge', allVaultSlugs);
612
602
  }
613
603
  db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('last_sync', ?)")
@@ -624,12 +614,9 @@ export function createIndex(vaultPath, opts) {
624
614
  }
625
615
  const lastSyncTime = parseInt(lastSync.value, 10);
626
616
  let changed = false;
627
- const dirs = [
628
- { dir: 'entities', kind: 'entity' },
629
- { dir: 'knowledge', kind: 'knowledge' },
630
- ];
617
+ const dirs = INDEXED_KINDS.map(kind => ({ dir: kindDir(kind), kind }));
631
618
  for (const { dir, kind } of dirs) {
632
- for (const file of scanDir(join(vaultPath, dir))) {
619
+ for (const file of scanVaultDir(join(vaultPath, dir))) {
633
620
  if (statSync(file).mtimeMs > lastSyncTime) {
634
621
  indexFile(file, kind);
635
622
  changed = true;
@@ -640,7 +627,7 @@ export function createIndex(vaultPath, opts) {
640
627
  const dbSlugs = db.prepare('SELECT slug FROM items').all().map(r => r.slug);
641
628
  const fileSlugs = new Set();
642
629
  for (const { dir } of dirs) {
643
- for (const file of scanDir(join(vaultPath, dir))) {
630
+ for (const file of scanVaultDir(join(vaultPath, dir))) {
644
631
  fileSlugs.add(basename(file, '.md'));
645
632
  }
646
633
  }
@@ -656,8 +643,7 @@ export function createIndex(vaultPath, opts) {
656
643
  }
657
644
  }
658
645
  function upsertFromFile(slug, kind) {
659
- const dir = kind === 'entity' ? 'entities' : 'knowledge';
660
- const filePath = join(vaultPath, dir, `${slug}.md`);
646
+ const filePath = join(vaultPath, kindDir(kind), `${slug}.md`);
661
647
  if (existsSync(filePath)) {
662
648
  // Force re-index by clearing cached hash
663
649
  deleteItemStmt.run(slug);
package/dist/digest.js CHANGED
@@ -60,6 +60,8 @@ export function digest(input, existingSlugs, now = new Date()) {
60
60
  // leak into the note body (the schema promises they are omitted).
61
61
  const noteBody = buildSessionNoteBody(input, knownTouched);
62
62
  const related = knownTouched;
63
+ // Typechecked against the Op Spec's session-create schema so digest output
64
+ // can never silently diverge from what batch validation accepts.
63
65
  const noteFields = {
64
66
  synthesis: noteBody,
65
67
  'session-date': input.session_date,
@@ -0,0 +1,108 @@
1
+ import type { SessionLogEntry, SessionLogType } from './types.js';
2
+ /**
3
+ * Known structural section headings in Hafez entities, knowledge notes, and
4
+ * session files. Derived from contracts.ts (defaultSections + optionalSections
5
+ * + session sections) plus auto-generated sections not tracked in contracts
6
+ * (Related). Only these are treated as section boundaries. Any other `## `
7
+ * heading inside section content (e.g. in a brief or session log) is
8
+ * preserved as-is.
9
+ */
10
+ export declare const STRUCTURAL_SECTIONS: Set<string>;
11
+ /**
12
+ * Find the next structural `## Heading` boundary after a given position.
13
+ * Skips `## ` headings that are not known structural sections (content headings).
14
+ * Returns the index of the `\n` before the `## `, or -1 if no structural heading found.
15
+ */
16
+ export declare function findNextStructuralHeading(body: string, from: number): number;
17
+ /**
18
+ * Find a ## Heading section and its content.
19
+ * Returns { start, headingEnd, end, content } or null if not found.
20
+ * Matches heading exactly (e.g. "## Brief" won't match "## Briefing").
21
+ * Trailing whitespace on the heading line is tolerated — the splitter's
22
+ * boundary tokenizer trims headings, so anything it treats as a boundary
23
+ * must also be findable here (a hand-edited `## Session Log ` otherwise
24
+ * gets a duplicate section and archival never fires).
25
+ * Only treats known structural headings as section boundaries -- content
26
+ * inside a section may contain arbitrary `## ` headings without corruption.
27
+ */
28
+ export declare function findSection(body: string, heading: string): {
29
+ start: number;
30
+ headingEnd: number;
31
+ end: number;
32
+ content: string;
33
+ } | null;
34
+ export interface DocSection {
35
+ heading: string;
36
+ content: string;
37
+ }
38
+ /**
39
+ * Split a body into preamble + structural sections. Non-structural `## `
40
+ * headings stay inside the enclosing section's content — sections are
41
+ * atomic units of authored content, never split at user headings.
42
+ * Headings and content come back trimmed; reassemble with
43
+ * `## ${heading}\n\n${content}` joined by blank lines.
44
+ */
45
+ export declare function splitStructural(body: string): {
46
+ preamble: string;
47
+ sections: DocSection[];
48
+ };
49
+ /**
50
+ * Replace a structural section's content, or create the section: before
51
+ * `insertBefore` when that section exists, at EOF otherwise.
52
+ */
53
+ export declare function setSection(body: string, heading: string, content: string, insertBefore?: string): string;
54
+ /** Append content at the end of a structural section (creating it at EOF if absent). */
55
+ export declare function appendToSection(body: string, heading: string, content: string): string;
56
+ /** Insert a formatted session-log entry newest-first (creating the section at EOF if absent). */
57
+ export declare function prependSessionLogEntry(body: string, entry: string): string;
58
+ /** Body up to the real Session Log section — the `summary` read depth. */
59
+ export declare function bodyBeforeSessionLog(body: string): string;
60
+ export declare function parseSessionLogHeading(line: string): {
61
+ date: string;
62
+ agent: string;
63
+ type: SessionLogType;
64
+ } | null;
65
+ export declare function formatSessionLogEntry(entry: SessionLogEntry): string;
66
+ /** Entries inside the real Session Log section only — entry-shaped lines
67
+ * elsewhere in the body (e.g. quoted in a Brief) do not count. */
68
+ export declare function countSessionLogEntries(body: string): number;
69
+ export declare function parseSessionLog(body: string): Array<{
70
+ date: string;
71
+ agent: string;
72
+ type: SessionLogType;
73
+ summary: string;
74
+ }>;
75
+ export declare function extractOldestSessionLogEntry(body: string): {
76
+ entry: string;
77
+ remaining: string;
78
+ } | null;
79
+ export declare function getBrief(body: string): string | null;
80
+ export declare function setBrief(body: string, content: string): string;
81
+ export declare function removeBrief(body: string): string;
82
+ export declare function getNextActions(body: string): string[];
83
+ export declare function addNextAction(body: string, action: string): string;
84
+ export declare function completeNextAction(body: string, query: string): {
85
+ matched: string;
86
+ body: string;
87
+ };
88
+ export declare function removeNextAction(body: string, query: string): {
89
+ matched: string;
90
+ body: string;
91
+ };
92
+ export declare function clearNextActions(body: string): string;
93
+ export declare function firstNonEmptyLine(s: string | null | undefined): string;
94
+ /**
95
+ * One-line summary for a doc: frontmatter description first, then the first
96
+ * non-empty line of the kind's primary section (entity: Brief; knowledge:
97
+ * Synthesis, falling back to Goal for plans; session: Summary).
98
+ * Shared by the vault index and the OKF export so the two surfaces agree.
99
+ */
100
+ export declare function deriveSummary(kind: 'entity' | 'knowledge' | 'session', fm: Record<string, any>, body: string): string;
101
+ /** The one wiki-link syntax the vault recognizes: [[slug]] or
102
+ * [[slug|display text]] — slug in group 1, display text in group 2.
103
+ * Non-global — wrap with `new RegExp(WIKI_LINK_RE.source, 'g')` for iteration. */
104
+ export declare const WIKI_LINK_RE: RegExp;
105
+ export declare function syncRelatedSection(body: string, frontmatter: {
106
+ parent?: string;
107
+ related?: string[];
108
+ }): string;