hafez 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -32
- package/dist/apply.d.ts +33 -0
- package/dist/apply.js +693 -0
- package/dist/batch-ops.d.ts +372 -26
- package/dist/batch-ops.js +355 -73
- package/dist/cli/args.d.ts +48 -6
- package/dist/cli/args.js +74 -9
- package/dist/cli/batch-errors.js +19 -11
- package/dist/cli/block-hook.d.ts +9 -0
- package/dist/cli/block-hook.js +90 -0
- package/dist/cli/commands-schema.js +2 -0
- package/dist/cli/commands.d.ts +28 -3
- package/dist/cli/commands.js +440 -157
- package/dist/cli/format.d.ts +7 -3
- package/dist/cli/format.js +27 -10
- package/dist/cli/index.d.ts +55 -0
- package/dist/cli/index.js +215 -153
- package/dist/cli/install.d.ts +53 -0
- package/dist/cli/install.js +297 -0
- package/dist/cli/onboard.d.ts +15 -2
- package/dist/cli/onboard.js +265 -90
- package/dist/cli/resolve-vault.d.ts +11 -4
- package/dist/cli/resolve-vault.js +13 -11
- package/dist/cli/schema-introspect.js +4 -3
- package/dist/cli/upgrade.d.ts +51 -0
- package/dist/cli/upgrade.js +351 -0
- package/dist/closest.d.ts +8 -0
- package/dist/closest.js +65 -0
- package/dist/contracts.d.ts +65 -6
- package/dist/contracts.js +148 -23
- package/dist/dates.d.ts +10 -0
- package/dist/dates.js +27 -0
- package/dist/db.d.ts +40 -2
- package/dist/db.js +363 -66
- package/dist/digest.d.ts +6 -4
- package/dist/digest.js +28 -17
- package/dist/document.d.ts +135 -88
- package/dist/document.js +665 -379
- package/dist/export-okf.d.ts +13 -0
- package/dist/export-okf.js +50 -20
- package/dist/git.js +165 -48
- package/dist/harness.d.ts +58 -0
- package/dist/harness.js +50 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +301 -826
- package/dist/knowledge-index.d.ts +23 -1
- package/dist/knowledge-index.js +62 -6
- package/dist/merge.d.ts +1 -1
- package/dist/merge.js +98 -64
- package/dist/near-match.d.ts +9 -0
- package/dist/near-match.js +34 -0
- package/dist/schema.js +4 -4
- package/dist/stale.d.ts +41 -0
- package/dist/stale.js +43 -0
- package/dist/stopwords.d.ts +6 -0
- package/dist/stopwords.js +25 -0
- package/dist/surfaces/items.d.ts +366 -0
- package/dist/surfaces/items.js +523 -0
- package/dist/surfaces/surfaces.d.ts +32 -0
- package/dist/surfaces/surfaces.js +230 -0
- package/dist/templates.d.ts +6 -0
- package/dist/templates.js +27 -27
- package/dist/types.d.ts +78 -22
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +109 -0
- package/dist/vault.d.ts +18 -0
- package/dist/vault.js +51 -5
- package/package.json +4 -3
- package/skill/hafez/SKILL.md +68 -0
- package/skill/hafez-save/SKILL.md +29 -0
- package/dist/cli/help-agent.d.ts +0 -1
- package/dist/cli/help-agent.js +0 -186
- package/dist/query.d.ts +0 -24
- package/dist/query.js +0 -169
- package/skill/SKILL.md +0 -196
package/dist/digest.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// src/digest.ts
|
|
2
|
+
import { createHash } from 'crypto';
|
|
2
3
|
import { z } from 'zod';
|
|
4
|
+
import { HafezError } from './types.js';
|
|
5
|
+
import { detectAgent } from './harness.js';
|
|
6
|
+
import { ITEMS } from './surfaces/items.js';
|
|
3
7
|
// --- Schema ---
|
|
4
8
|
export const DigestInputSchema = z.object({
|
|
5
9
|
entities_touched: z.array(z.string())
|
|
6
|
-
.describe('Slugs of entities worked on. Each gets a session log entry; the session note links them all.
|
|
10
|
+
.describe('Slugs of entities worked on. Each gets a session log entry; the session note links them all. An unknown slug fails the digest and is named.'),
|
|
7
11
|
decisions: z.array(z.string())
|
|
8
12
|
.describe('Key decisions made this session (may be empty). Recorded in the session note.')
|
|
9
13
|
.default([]),
|
|
@@ -12,8 +16,8 @@ export const DigestInputSchema = z.object({
|
|
|
12
16
|
session_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Must be YYYY-MM-DD')
|
|
13
17
|
.describe('Session date, YYYY-MM-DD.'),
|
|
14
18
|
agent: z.string()
|
|
15
|
-
.describe('Agent identifier recorded in session logs.')
|
|
16
|
-
.
|
|
19
|
+
.describe('Agent identifier recorded in session logs. Detected from the Harness when absent.')
|
|
20
|
+
.optional(),
|
|
17
21
|
});
|
|
18
22
|
/**
|
|
19
23
|
* Validate and parse raw digest input from stdin.
|
|
@@ -39,27 +43,32 @@ export function parseDigestInput(raw) {
|
|
|
39
43
|
/**
|
|
40
44
|
* Convert session context into a hafez batch payload.
|
|
41
45
|
* Pure function — no file I/O, no vault access.
|
|
42
|
-
*
|
|
46
|
+
* An unknown slug in entities_touched is a NOT_FOUND naming it (and only the
|
|
47
|
+
* unknown ones), never a session log that points nowhere. A knowledge slug is
|
|
48
|
+
* known: the note links it, but a session log is entity-only.
|
|
43
49
|
*/
|
|
44
|
-
export function digest(input,
|
|
50
|
+
export function digest(input, entitySlugs, knowledgeSlugs = new Set()) {
|
|
51
|
+
const touched = [...new Set(input.entities_touched)];
|
|
52
|
+
const unknown = touched.filter(s => !entitySlugs.has(s) && !knowledgeSlugs.has(s));
|
|
53
|
+
if (unknown.length > 0)
|
|
54
|
+
throw new HafezError('NOT_FOUND', ITEMS.unknownSlugAtSave.render(unknown));
|
|
45
55
|
const ops = [];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
for (const slug of
|
|
56
|
+
const agent = input.agent || detectAgent();
|
|
57
|
+
// 1. Session log updates for each touched entity
|
|
58
|
+
for (const slug of touched.filter(s => entitySlugs.has(s))) {
|
|
49
59
|
const sessionLogSummary = buildSessionLogSummary(input);
|
|
50
60
|
const fields = {
|
|
51
61
|
session_log: {
|
|
52
62
|
type: 'progress',
|
|
53
63
|
summary: sessionLogSummary,
|
|
54
|
-
agent
|
|
64
|
+
agent,
|
|
55
65
|
},
|
|
56
66
|
};
|
|
57
67
|
ops.push({ op: 'update', slug, fields });
|
|
58
68
|
}
|
|
59
|
-
// 2. Session note
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const related = knownTouched;
|
|
69
|
+
// 2. Session note
|
|
70
|
+
const noteBody = buildSessionNoteBody(input, touched);
|
|
71
|
+
const related = touched;
|
|
63
72
|
// Typechecked against the Op Spec's session-create schema so digest output
|
|
64
73
|
// can never silently diverge from what batch validation accepts.
|
|
65
74
|
const noteFields = {
|
|
@@ -67,7 +76,9 @@ export function digest(input, existingSlugs, now = new Date()) {
|
|
|
67
76
|
'session-date': input.session_date,
|
|
68
77
|
...(related.length > 0 ? { related } : {}),
|
|
69
78
|
};
|
|
70
|
-
|
|
79
|
+
// Named by the session date and its note, so a second save of the same
|
|
80
|
+
// session derives the same slug and batch skips it as a duplicate.
|
|
81
|
+
const noteName = `Session ${input.session_date} ${createHash('sha256').update(noteBody).digest('hex').slice(0, 8)}`;
|
|
71
82
|
ops.push({ op: 'create', kind: 'session', name: noteName, fields: noteFields });
|
|
72
83
|
return ops;
|
|
73
84
|
}
|
|
@@ -79,7 +90,7 @@ function buildSessionLogSummary(input) {
|
|
|
79
90
|
}
|
|
80
91
|
return parts.join(' | ');
|
|
81
92
|
}
|
|
82
|
-
function buildSessionNoteBody(input,
|
|
93
|
+
function buildSessionNoteBody(input, touched) {
|
|
83
94
|
const lines = [input.narrative.trim()];
|
|
84
95
|
if (input.decisions.length > 0) {
|
|
85
96
|
lines.push('');
|
|
@@ -88,9 +99,9 @@ function buildSessionNoteBody(input, knownTouched) {
|
|
|
88
99
|
lines.push(`- ${d}`);
|
|
89
100
|
}
|
|
90
101
|
}
|
|
91
|
-
if (
|
|
102
|
+
if (touched.length > 0) {
|
|
92
103
|
lines.push('');
|
|
93
|
-
lines.push(`**Entities touched:** ${
|
|
104
|
+
lines.push(`**Entities touched:** ${touched.join(', ')}`);
|
|
94
105
|
}
|
|
95
106
|
return lines.join('\n');
|
|
96
107
|
}
|
package/dist/document.d.ts
CHANGED
|
@@ -1,96 +1,151 @@
|
|
|
1
|
-
import type
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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[];
|
|
1
|
+
import { type SessionLogEntry, type SessionLogType } from './types.js';
|
|
2
|
+
/** The action text without its trailing added-date stamp, if any. */
|
|
3
|
+
export declare function stripActionStamp(text: string): string;
|
|
4
|
+
/** The day an action's stamp names; null when the line is undated. */
|
|
5
|
+
export declare function actionAdded(text: string): string | null;
|
|
6
|
+
/** Stamp an action with its added date; text that already ends in a stamp is
|
|
7
|
+
* returned as is, so a carried-over line is never double-stamped. */
|
|
8
|
+
export declare function stampAction(text: string, day: string): string;
|
|
9
|
+
/** One checkbox line of Next Actions as a reader sees it: unstamped text, and
|
|
10
|
+
* a Handle and added day (null when undated) when the line is open. */
|
|
11
|
+
export type ActionItem = {
|
|
12
|
+
handle: string;
|
|
13
|
+
text: string;
|
|
14
|
+
added: string | null;
|
|
15
|
+
} | {
|
|
16
|
+
handle?: undefined;
|
|
17
|
+
text: string;
|
|
48
18
|
};
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*/
|
|
53
|
-
export
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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;
|
|
19
|
+
/** The open action a query named: its line's position within the Next
|
|
20
|
+
* Actions section, and its unstamped text. Editing by position rather than
|
|
21
|
+
* re-matching the text keeps a Handle-named line unambiguous when its text
|
|
22
|
+
* is a substring of another line's, or a duplicate of one. */
|
|
23
|
+
export interface ActionMatch {
|
|
24
|
+
line: number;
|
|
25
|
+
text: string;
|
|
26
|
+
}
|
|
60
27
|
export declare function parseSessionLogHeading(line: string): {
|
|
61
28
|
date: string;
|
|
62
29
|
agent: string;
|
|
63
30
|
type: SessionLogType;
|
|
64
31
|
} | null;
|
|
65
32
|
export declare function formatSessionLogEntry(entry: SessionLogEntry): string;
|
|
66
|
-
/**
|
|
67
|
-
*
|
|
68
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Split a Session Log section's content into raw entry blocks, each beginning
|
|
35
|
+
* with a parseable entry heading. Non-entry text before the first heading is
|
|
36
|
+
* not an entry and is dropped. Document owns entry block splitting — merge's
|
|
37
|
+
* dedup/sort policy stays in merge.ts.
|
|
38
|
+
*/
|
|
39
|
+
export declare function splitLogEntries(content: string): string[];
|
|
40
|
+
export declare function firstNonEmptyLine(s: string | null | undefined): string;
|
|
41
|
+
/** The one wiki-link syntax the vault recognizes: [[slug]] or
|
|
42
|
+
* [[slug|display text]] — slug in group 1, display text in group 2.
|
|
43
|
+
* Padding inside the brackets is Obsidian-idiomatic ([[ slug ]], [[slug | Label]])
|
|
44
|
+
* and is absorbed by the pattern, so both captures come out already trimmed —
|
|
45
|
+
* every consumer (Related coverage, mention harvesting, export) sees the bare slug.
|
|
46
|
+
* Non-global — wrap with `new RegExp(WIKI_LINK_RE.source, 'g')` for iteration. */
|
|
47
|
+
export declare const WIKI_LINK_RE: RegExp;
|
|
48
|
+
/** What documentFor needs to know about a file to bind its section set.
|
|
49
|
+
*
|
|
50
|
+
* KIND is the whole context — deliberately no type/subtype narrowing. A file
|
|
51
|
+
* can legitimately carry sections outside its current type's contract (a
|
|
52
|
+
* promote race merged in a capture's Notes, a plan-shaped note never had its
|
|
53
|
+
* subtype persisted, a hand edit added Open Questions to a plain entity), and
|
|
54
|
+
* a narrower binding demotes those headings to non-structural text that the
|
|
55
|
+
* next section rewrite silently deletes. The kind-level union makes every
|
|
56
|
+
* same-kind heading a boundary, so no same-kind section is ever swallowed;
|
|
57
|
+
* the cost is that a same-kind heading quoted at line start inside a section
|
|
58
|
+
* strands as its own section on rewrite — visible and recoverable, unlike
|
|
59
|
+
* deletion. Cross-KIND headings (a session-only ## Decisions quoted in an
|
|
60
|
+
* entity Brief) are still not boundaries. */
|
|
61
|
+
export interface DocContext {
|
|
62
|
+
kind: 'entity' | 'knowledge' | 'session';
|
|
63
|
+
}
|
|
64
|
+
/** The structural set a context binds — exposed for introspection and the
|
|
65
|
+
* contract-coverage tests; runtime consumers go through documentFor. */
|
|
66
|
+
export declare function structuralSectionsFor(ctx: DocContext): ReadonlySet<string>;
|
|
67
|
+
/** The Document operations bound to one file's structural section set. */
|
|
68
|
+
export declare function documentFor(ctx: DocContext): Doc;
|
|
69
|
+
/** Bind from a parsed vault file — kind from Layout. */
|
|
70
|
+
export declare function documentForFile(parsed: {
|
|
71
|
+
kind: 'entity' | 'knowledge' | 'session';
|
|
72
|
+
}): Doc;
|
|
73
|
+
export interface DocSection {
|
|
74
|
+
heading: string;
|
|
75
|
+
content: string;
|
|
76
|
+
}
|
|
77
|
+
export interface Doc {
|
|
78
|
+
findNextStructuralHeading(body: string, from: number): number;
|
|
79
|
+
findSection(body: string, heading: string): {
|
|
80
|
+
start: number;
|
|
81
|
+
headingEnd: number;
|
|
82
|
+
end: number;
|
|
83
|
+
content: string;
|
|
84
|
+
} | null;
|
|
85
|
+
splitStructural(body: string): {
|
|
86
|
+
preamble: string;
|
|
87
|
+
sections: DocSection[];
|
|
88
|
+
};
|
|
89
|
+
setSection(body: string, heading: string, content: string, insertBefore?: string): string;
|
|
90
|
+
appendToSection(body: string, heading: string, content: string): string;
|
|
91
|
+
renameSection(body: string, from: string, to: string): string;
|
|
92
|
+
ensureSection(body: string, heading: string, opts?: {
|
|
93
|
+
after?: string;
|
|
94
|
+
}): string;
|
|
95
|
+
prependSessionLogEntry(body: string, entry: string): string;
|
|
96
|
+
bodyBeforeSessionLog(body: string): string;
|
|
97
|
+
countSessionLogEntries(body: string): number;
|
|
98
|
+
parseSessionLog(body: string): Array<{
|
|
99
|
+
date: string;
|
|
100
|
+
agent: string;
|
|
101
|
+
type: SessionLogType;
|
|
102
|
+
summary: string;
|
|
103
|
+
}>;
|
|
104
|
+
extractOldestSessionLogEntry(body: string): {
|
|
105
|
+
entry: string;
|
|
106
|
+
remaining: string;
|
|
107
|
+
} | null;
|
|
108
|
+
getBrief(body: string): string | null;
|
|
109
|
+
setBrief(body: string, content: string): string;
|
|
110
|
+
removeBrief(body: string): string;
|
|
111
|
+
getNextActions(body: string): string[];
|
|
112
|
+
getActionItems(body: string): ActionItem[];
|
|
113
|
+
renderActionHandles(body: string, mark?: (added: string | null) => string): string;
|
|
114
|
+
addNextAction(body: string, action: string): string;
|
|
115
|
+
matchNextAction(body: string, query: string): ActionMatch;
|
|
116
|
+
completeNextAction(body: string, action: string | ActionMatch): {
|
|
117
|
+
matched: string;
|
|
118
|
+
body: string;
|
|
119
|
+
};
|
|
120
|
+
removeNextAction(body: string, action: string | ActionMatch): {
|
|
121
|
+
matched: string;
|
|
122
|
+
line: string;
|
|
123
|
+
body: string;
|
|
124
|
+
};
|
|
125
|
+
actionLines(body: string): string[];
|
|
126
|
+
extractOldestTickedActions(body: string, keep: number): {
|
|
127
|
+
lines: string[];
|
|
128
|
+
body: string;
|
|
129
|
+
};
|
|
130
|
+
pruneNextActions(body: string): string;
|
|
131
|
+
clearNextActions(body: string): string;
|
|
132
|
+
stripNextActionStamps(body: string): string;
|
|
133
|
+
syncRelatedSection(body: string, frontmatter: {
|
|
134
|
+
parent?: string;
|
|
135
|
+
related?: string[];
|
|
136
|
+
}): string;
|
|
137
|
+
}
|
|
138
|
+
export declare function getBrief(body: string): string | null;
|
|
139
|
+
export declare function getNextActions(body: string): string[];
|
|
140
|
+
export declare function getActionItems(body: string): ActionItem[];
|
|
141
|
+
export declare function renderActionHandles(body: string, mark?: (added: string | null) => string): string;
|
|
69
142
|
export declare function parseSessionLog(body: string): Array<{
|
|
70
143
|
date: string;
|
|
71
144
|
agent: string;
|
|
72
145
|
type: SessionLogType;
|
|
73
146
|
summary: string;
|
|
74
147
|
}>;
|
|
75
|
-
export declare function
|
|
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;
|
|
148
|
+
export declare function countSessionLogEntries(body: string): number;
|
|
94
149
|
/**
|
|
95
150
|
* One-line summary for a doc: frontmatter description first, then the first
|
|
96
151
|
* non-empty line of the kind's primary section (entity: Brief; knowledge:
|
|
@@ -98,11 +153,3 @@ export declare function firstNonEmptyLine(s: string | null | undefined): string;
|
|
|
98
153
|
* Shared by the vault index and the OKF export so the two surfaces agree.
|
|
99
154
|
*/
|
|
100
155
|
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;
|