opencode-harnmem 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +45 -2
- package/index.js +512 -0
- package/lib/engine.mjs +72 -0
- package/lib/format.mjs +119 -0
- package/lib/settings.mjs +131 -0
- package/package.json +23 -3
- package/vendor/core/assets.d.ts +52 -0
- package/vendor/core/assets.d.ts.map +1 -0
- package/vendor/core/assets.js +227 -0
- package/vendor/core/assets.js.map +1 -0
- package/vendor/core/config.d.ts +36 -0
- package/vendor/core/config.d.ts.map +1 -0
- package/vendor/core/config.js +134 -0
- package/vendor/core/config.js.map +1 -0
- package/vendor/core/consolidate.d.ts +14 -0
- package/vendor/core/consolidate.d.ts.map +1 -0
- package/vendor/core/consolidate.js +237 -0
- package/vendor/core/consolidate.js.map +1 -0
- package/vendor/core/data/synonyms.en.txt +61 -0
- package/vendor/core/data/synonyms.zh.txt +157 -0
- package/vendor/core/extract.d.ts +49 -0
- package/vendor/core/extract.d.ts.map +1 -0
- package/vendor/core/extract.js +179 -0
- package/vendor/core/extract.js.map +1 -0
- package/vendor/core/frontmatter.d.ts +31 -0
- package/vendor/core/frontmatter.d.ts.map +1 -0
- package/vendor/core/frontmatter.js +372 -0
- package/vendor/core/frontmatter.js.map +1 -0
- package/vendor/core/harnmem.d.ts +87 -0
- package/vendor/core/harnmem.d.ts.map +1 -0
- package/vendor/core/harnmem.js +918 -0
- package/vendor/core/harnmem.js.map +1 -0
- package/vendor/core/hot-context.d.ts +28 -0
- package/vendor/core/hot-context.d.ts.map +1 -0
- package/vendor/core/hot-context.js +179 -0
- package/vendor/core/hot-context.js.map +1 -0
- package/vendor/core/index.d.ts +16 -0
- package/vendor/core/index.d.ts.map +1 -0
- package/vendor/core/index.js +10 -0
- package/vendor/core/index.js.map +1 -0
- package/vendor/core/indexer.d.ts +107 -0
- package/vendor/core/indexer.d.ts.map +1 -0
- package/vendor/core/indexer.js +282 -0
- package/vendor/core/indexer.js.map +1 -0
- package/vendor/core/inspect.d.ts +278 -0
- package/vendor/core/inspect.d.ts.map +1 -0
- package/vendor/core/inspect.js +643 -0
- package/vendor/core/inspect.js.map +1 -0
- package/vendor/core/lifecycle.d.ts +50 -0
- package/vendor/core/lifecycle.d.ts.map +1 -0
- package/vendor/core/lifecycle.js +249 -0
- package/vendor/core/lifecycle.js.map +1 -0
- package/vendor/core/project.d.ts +41 -0
- package/vendor/core/project.d.ts.map +1 -0
- package/vendor/core/project.js +197 -0
- package/vendor/core/project.js.map +1 -0
- package/vendor/core/prompts/README.md +96 -0
- package/vendor/core/prompts/consolidate-digest.md +31 -0
- package/vendor/core/prompts/extract.md +80 -0
- package/vendor/core/prompts/migrate.md +96 -0
- package/vendor/core/prompts/synonyms.md +43 -0
- package/vendor/core/prompts/write-policy.md +12 -0
- package/vendor/core/retrieval.d.ts +95 -0
- package/vendor/core/retrieval.d.ts.map +1 -0
- package/vendor/core/retrieval.js +569 -0
- package/vendor/core/retrieval.js.map +1 -0
- package/vendor/core/store.d.ts +145 -0
- package/vendor/core/store.d.ts.map +1 -0
- package/vendor/core/store.js +511 -0
- package/vendor/core/store.js.map +1 -0
- package/vendor/core/suggest.d.ts +128 -0
- package/vendor/core/suggest.d.ts.map +1 -0
- package/vendor/core/suggest.js +384 -0
- package/vendor/core/suggest.js.map +1 -0
- package/vendor/core/text.d.ts +80 -0
- package/vendor/core/text.d.ts.map +1 -0
- package/vendor/core/text.js +253 -0
- package/vendor/core/text.js.map +1 -0
- package/vendor/core/types.d.ts +453 -0
- package/vendor/core/types.d.ts.map +1 -0
- package/vendor/core/types.js +14 -0
- package/vendor/core/types.js.map +1 -0
- package/vendor/core/util.d.ts +38 -0
- package/vendor/core/util.d.ts.map +1 -0
- package/vendor/core/util.js +248 -0
- package/vendor/core/util.js.map +1 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { HarnMemConfig, LlmFn, MemoryType, TranscriptMessage } from "./types.js";
|
|
2
|
+
import { type PromptAsset } from "./assets.js";
|
|
3
|
+
export interface ExtractedMemory {
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
content: string;
|
|
7
|
+
type: MemoryType;
|
|
8
|
+
scope?: "user" | "project";
|
|
9
|
+
category?: string;
|
|
10
|
+
tags?: string[];
|
|
11
|
+
entities?: string[];
|
|
12
|
+
importance?: number;
|
|
13
|
+
confidence?: number;
|
|
14
|
+
links?: string[];
|
|
15
|
+
supersedes?: string[];
|
|
16
|
+
evidence?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface ExistingMemoryRef {
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
type: string;
|
|
22
|
+
description: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ExtractionInput {
|
|
25
|
+
messages: TranscriptMessage[];
|
|
26
|
+
existing: ExistingMemoryRef[];
|
|
27
|
+
projectName?: string;
|
|
28
|
+
date: string;
|
|
29
|
+
}
|
|
30
|
+
export declare const DEFAULT_EXTRACT_PROMPT = "You extract durable memory from a coding-assistant session transcript.\nReturn JSON only: {\"memories\": [ {\"title\": string, \"description\": string (one sentence, <=160 chars), \"content\": string (markdown, 3-12 lines), \"type\": \"user\"|\"feedback\"|\"project\"|\"reference\", \"scope\": \"user\"|\"project\", \"category\": string, \"tags\": string[], \"entities\": string[], \"importance\": number (0-10), \"confidence\": number (0-1), \"links\": string[] (existing memory ids only)} ]}\n\nRules:\n1. Only durable, reusable knowledge: user identity/preferences/workflow constraints (type user), corrections and lessons that change future behavior (type feedback; always include **Why:** and **How to apply:** lines in content), project architecture/decisions/conventions/facts (type project), external pointers/URLs/docs (type reference).\n2. Do NOT store: transient task state, code that is in the repo, facts already covered by AGENTS.md/project docs, secrets, or anything the user did not state or confirm.\n3. Same language as the user's conversation (Chinese stays Chinese).\n4. Every entry needs an Observation Date from the transcript; put it as the first line of content: \"Observed: YYYY-MM-DD\".\n5. Avoid duplicating existing memories (see list). If related, link by id instead of restating.\n6. Exhaustive extraction: cover every distinct durable fact in the digest; do not stop after the first topic. Max 6 entries.\n7. If nothing is durable, return {\"memories\": []}.\n8. Never invent ids, versions, numbers or dates.";
|
|
31
|
+
export declare const DEFAULT_CONSOLIDATE_PROMPT = "You consolidate a personal memory store into a topical digest.\nReturn JSON only: {\"digest\": {\"title\": string, \"content\": string (markdown, <=40 lines), \"sources\": string[] (memory ids)}}\nMerge overlapping facts, resolve contradictions in favor of the newest observation, drop stale details, keep concrete numbers and dates. Do not invent facts.";
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a prompt asset: user copy in <home>/prompts first, shipped default
|
|
34
|
+
* second, inline constant last. Returns the parsed asset so callers can use the
|
|
35
|
+
* stable system text and fill the user template with data.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolvePrompt(home: string, name: string, fallbackSystem: string): PromptAsset;
|
|
38
|
+
/** Build the bounded transcript digest sent to the extraction model. */
|
|
39
|
+
export declare function buildDigest(messages: TranscriptMessage[], maxChars?: number): string;
|
|
40
|
+
/** Existing-memory list for dedupe context: `[id] (type) title — description`. */
|
|
41
|
+
export declare function buildExistingList(existing: ExistingMemoryRef[], max?: number): string;
|
|
42
|
+
/** Parse the JSON payload from a (possibly chatty) model response. */
|
|
43
|
+
export declare function parseExtractionResponse(text: string): ExtractedMemory[];
|
|
44
|
+
/** One bounded extraction call. Returns [] when the LLM is absent or output is unusable. */
|
|
45
|
+
export declare function runExtraction(llm: LlmFn | undefined, config: HarnMemConfig, input: ExtractionInput): Promise<ExtractedMemory[]>;
|
|
46
|
+
/** Read a session journal file back into messages (adapter-free harnesses). */
|
|
47
|
+
export declare function readSessionJournal(file: string): Promise<TranscriptMessage[]>;
|
|
48
|
+
export declare function ensurePromptFiles(home: string): Promise<void>;
|
|
49
|
+
//# sourceMappingURL=extract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,KAAK,EAAc,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAClG,OAAO,EAAoD,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAGjG,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,sBAAsB,wgDAWc,CAAC;AAElD,eAAO,MAAM,0BAA0B,uWAEuH,CAAC;AAE/J;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,WAAW,CAI7F;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,QAAQ,SAAO,GAAG,MAAM,CAclF;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,GAAG,SAAK,GAAG,MAAM,CAKjF;AAED,sEAAsE;AACtE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,EAAE,CA0CvE;AAED,4FAA4F;AAC5F,wBAAsB,aAAa,CACjC,GAAG,EAAE,KAAK,GAAG,SAAS,EACtB,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,eAAe,EAAE,CAAC,CAmC5B;AAED,+EAA+E;AAC/E,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAgBnF;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAInE"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-session extraction: one bounded LLM call that turns a session digest
|
|
3
|
+
* into structured memories.
|
|
4
|
+
*
|
|
5
|
+
* Prompt assets are data on disk: `<home>/prompts/extract.md` (user-editable)
|
|
6
|
+
* overrides the shipped `prompts/extract.md`, which overrides the inline default.
|
|
7
|
+
* See `assets.ts` for the file contract and `prompts/README.md` for the format.
|
|
8
|
+
*/
|
|
9
|
+
import { promises as fs } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { fillPrompt, loadPromptAsset, deployDefaultAssets } from "./assets.js";
|
|
12
|
+
import { readTextIfExists, resolveHome } from "./util.js";
|
|
13
|
+
export const DEFAULT_EXTRACT_PROMPT = `You extract durable memory from a coding-assistant session transcript.
|
|
14
|
+
Return JSON only: {"memories": [ {"title": string, "description": string (one sentence, <=160 chars), "content": string (markdown, 3-12 lines), "type": "user"|"feedback"|"project"|"reference", "scope": "user"|"project", "category": string, "tags": string[], "entities": string[], "importance": number (0-10), "confidence": number (0-1), "links": string[] (existing memory ids only)} ]}
|
|
15
|
+
|
|
16
|
+
Rules:
|
|
17
|
+
1. Only durable, reusable knowledge: user identity/preferences/workflow constraints (type user), corrections and lessons that change future behavior (type feedback; always include **Why:** and **How to apply:** lines in content), project architecture/decisions/conventions/facts (type project), external pointers/URLs/docs (type reference).
|
|
18
|
+
2. Do NOT store: transient task state, code that is in the repo, facts already covered by AGENTS.md/project docs, secrets, or anything the user did not state or confirm.
|
|
19
|
+
3. Same language as the user's conversation (Chinese stays Chinese).
|
|
20
|
+
4. Every entry needs an Observation Date from the transcript; put it as the first line of content: "Observed: YYYY-MM-DD".
|
|
21
|
+
5. Avoid duplicating existing memories (see list). If related, link by id instead of restating.
|
|
22
|
+
6. Exhaustive extraction: cover every distinct durable fact in the digest; do not stop after the first topic. Max 6 entries.
|
|
23
|
+
7. If nothing is durable, return {"memories": []}.
|
|
24
|
+
8. Never invent ids, versions, numbers or dates.`;
|
|
25
|
+
export const DEFAULT_CONSOLIDATE_PROMPT = `You consolidate a personal memory store into a topical digest.
|
|
26
|
+
Return JSON only: {"digest": {"title": string, "content": string (markdown, <=40 lines), "sources": string[] (memory ids)}}
|
|
27
|
+
Merge overlapping facts, resolve contradictions in favor of the newest observation, drop stale details, keep concrete numbers and dates. Do not invent facts.`;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a prompt asset: user copy in <home>/prompts first, shipped default
|
|
30
|
+
* second, inline constant last. Returns the parsed asset so callers can use the
|
|
31
|
+
* stable system text and fill the user template with data.
|
|
32
|
+
*/
|
|
33
|
+
export function resolvePrompt(home, name, fallbackSystem) {
|
|
34
|
+
const asset = loadPromptAsset(home, name);
|
|
35
|
+
if (asset && asset.system.trim())
|
|
36
|
+
return asset;
|
|
37
|
+
return { name, version: 0, system: fallbackSystem, user: null, source: "(built-in)" };
|
|
38
|
+
}
|
|
39
|
+
/** Build the bounded transcript digest sent to the extraction model. */
|
|
40
|
+
export function buildDigest(messages, maxChars = 6000) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
let used = 0;
|
|
43
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
44
|
+
const message = messages[i];
|
|
45
|
+
const prefix = message.role === "user" ? "User" : message.role === "assistant" ? "Assistant" : "Tool";
|
|
46
|
+
const text = (message.text ?? "").replace(/\s+/g, " ").trim();
|
|
47
|
+
if (!text)
|
|
48
|
+
continue;
|
|
49
|
+
const line = `${prefix}: ${text.slice(0, 1200)}`;
|
|
50
|
+
if (used + line.length > maxChars)
|
|
51
|
+
break;
|
|
52
|
+
lines.unshift(line);
|
|
53
|
+
used += line.length + 1;
|
|
54
|
+
}
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
/** Existing-memory list for dedupe context: `[id] (type) title — description`. */
|
|
58
|
+
export function buildExistingList(existing, max = 60) {
|
|
59
|
+
return existing
|
|
60
|
+
.slice(0, max)
|
|
61
|
+
.map((m) => `- [${m.id}] (${m.type}) ${m.title} — ${m.description}`)
|
|
62
|
+
.join("\n");
|
|
63
|
+
}
|
|
64
|
+
/** Parse the JSON payload from a (possibly chatty) model response. */
|
|
65
|
+
export function parseExtractionResponse(text) {
|
|
66
|
+
const start = text.indexOf("{");
|
|
67
|
+
const end = text.lastIndexOf("}");
|
|
68
|
+
if (start < 0 || end <= start)
|
|
69
|
+
return [];
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(text.slice(start, end + 1));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const memories = parsed.memories;
|
|
78
|
+
if (!Array.isArray(memories))
|
|
79
|
+
return [];
|
|
80
|
+
const types = ["user", "feedback", "project", "reference"];
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const raw of memories) {
|
|
83
|
+
if (!raw || typeof raw !== "object")
|
|
84
|
+
continue;
|
|
85
|
+
const item = raw;
|
|
86
|
+
const title = typeof item.title === "string" ? item.title.trim() : "";
|
|
87
|
+
const content = typeof item.content === "string" ? item.content.trim() : "";
|
|
88
|
+
if (!title || !content)
|
|
89
|
+
continue;
|
|
90
|
+
const typeRaw = typeof item.type === "string" ? item.type : "project";
|
|
91
|
+
const type = types.includes(typeRaw) ? typeRaw : "project";
|
|
92
|
+
const scopeRaw = typeof item.scope === "string" ? item.scope : type === "user" ? "user" : "project";
|
|
93
|
+
out.push({
|
|
94
|
+
title: title.slice(0, 200),
|
|
95
|
+
description: typeof item.description === "string" ? item.description.slice(0, 200) : title.slice(0, 200),
|
|
96
|
+
content: content.slice(0, 8000),
|
|
97
|
+
type,
|
|
98
|
+
scope: scopeRaw === "user" ? "user" : "project",
|
|
99
|
+
category: typeof item.category === "string" ? item.category : undefined,
|
|
100
|
+
tags: Array.isArray(item.tags) ? item.tags.filter((t) => typeof t === "string").slice(0, 12) : [],
|
|
101
|
+
entities: Array.isArray(item.entities) ? item.entities.filter((t) => typeof t === "string").slice(0, 12) : [],
|
|
102
|
+
importance: typeof item.importance === "number" ? Math.max(0, Math.min(10, item.importance)) : 5,
|
|
103
|
+
confidence: typeof item.confidence === "number" ? Math.max(0, Math.min(1, item.confidence)) : 0.7,
|
|
104
|
+
links: Array.isArray(item.links) ? item.links.filter((t) => typeof t === "string").slice(0, 8) : [],
|
|
105
|
+
supersedes: Array.isArray(item.supersedes)
|
|
106
|
+
? item.supersedes.filter((t) => typeof t === "string" && /^mem_[0-9A-Z]{26}$/i.test(t)).slice(0, 8)
|
|
107
|
+
: [],
|
|
108
|
+
evidence: Array.isArray(item.evidence) ? item.evidence.filter((t) => typeof t === "string").slice(0, 3) : [],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/** One bounded extraction call. Returns [] when the LLM is absent or output is unusable. */
|
|
114
|
+
export async function runExtraction(llm, config, input) {
|
|
115
|
+
if (!llm)
|
|
116
|
+
return [];
|
|
117
|
+
const digest = buildDigest(input.messages);
|
|
118
|
+
if (digest.length < (config.write?.minDigestChars ?? 500))
|
|
119
|
+
return [];
|
|
120
|
+
const home = config.home ?? resolveHome();
|
|
121
|
+
const asset = resolvePrompt(home, "extract", DEFAULT_EXTRACT_PROMPT);
|
|
122
|
+
const existingList = buildExistingList(input.existing) || "(none)";
|
|
123
|
+
const legacyUserPrompt = [
|
|
124
|
+
`Project: ${input.projectName ?? "(unknown)"}`,
|
|
125
|
+
`Session date: ${input.date}`,
|
|
126
|
+
"",
|
|
127
|
+
"Existing memories (link ids, do not duplicate):",
|
|
128
|
+
existingList,
|
|
129
|
+
"",
|
|
130
|
+
"Transcript digest:",
|
|
131
|
+
digest,
|
|
132
|
+
].join("\n");
|
|
133
|
+
const userPrompt = asset.user
|
|
134
|
+
? fillPrompt(asset.user, {
|
|
135
|
+
observation_date: input.date,
|
|
136
|
+
existing_memories: existingList,
|
|
137
|
+
transcript: digest,
|
|
138
|
+
})
|
|
139
|
+
: legacyUserPrompt;
|
|
140
|
+
const request = {
|
|
141
|
+
system: asset.system,
|
|
142
|
+
prompt: userPrompt,
|
|
143
|
+
temperature: 0,
|
|
144
|
+
maxTokens: 2000,
|
|
145
|
+
};
|
|
146
|
+
// LLM/transport failures are surfaced, not swallowed: a silent `[]` used to
|
|
147
|
+
// look like `status:"done", added:0` and hid a broken auxiliary call for
|
|
148
|
+
// days (N2). The caller records `status:"failed"` and can retry later.
|
|
149
|
+
const response = await llm(request);
|
|
150
|
+
return parseExtractionResponse(response.text).slice(0, config.write?.maxPerRun ?? 6);
|
|
151
|
+
}
|
|
152
|
+
/** Read a session journal file back into messages (adapter-free harnesses). */
|
|
153
|
+
export async function readSessionJournal(file) {
|
|
154
|
+
const raw = await readTextIfExists(file);
|
|
155
|
+
if (!raw)
|
|
156
|
+
return [];
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const rawLine of raw.split(/\r?\n/)) {
|
|
159
|
+
// Tolerate a UTF-8 BOM from hand-edited / PowerShell-written journals.
|
|
160
|
+
const line = rawLine.replace(/^\uFEFF/, "");
|
|
161
|
+
if (!line.trim())
|
|
162
|
+
continue;
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(line);
|
|
165
|
+
if (parsed.role && parsed.text)
|
|
166
|
+
out.push({ role: parsed.role, text: parsed.text, ts: parsed.ts });
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// tolerate truncated journal lines
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
export async function ensurePromptFiles(home) {
|
|
175
|
+
await fs.mkdir(join(home, "prompts"), { recursive: true });
|
|
176
|
+
// Copy shipped prompt/data defaults on first run so users have editable copies.
|
|
177
|
+
deployDefaultAssets(home);
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML-subset frontmatter parser/serializer + MemoryRecord mapping.
|
|
3
|
+
*
|
|
4
|
+
* We intentionally do NOT implement full YAML: only the canonical subset the
|
|
5
|
+
* engine writes (scalars, inline arrays, block arrays, one nested map). Parsing
|
|
6
|
+
* is tolerant: unknown keys are preserved in `extra`, malformed lines are kept
|
|
7
|
+
* in `raw` so `doctor` can report them instead of losing data.
|
|
8
|
+
*/
|
|
9
|
+
import type { MemoryRecord } from "./types.js";
|
|
10
|
+
export interface ParsedFrontmatter {
|
|
11
|
+
data: Record<string, unknown>;
|
|
12
|
+
body: string;
|
|
13
|
+
/** True when the file had no frontmatter at all. */
|
|
14
|
+
noFrontmatter: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Minimal YAML subset parser. */
|
|
17
|
+
export declare function parseYamlSubset(yaml: string): {
|
|
18
|
+
data: Record<string, unknown>;
|
|
19
|
+
errors: string[];
|
|
20
|
+
};
|
|
21
|
+
export declare function parseFrontmatter(raw: string): ParsedFrontmatter & {
|
|
22
|
+
errors: string[];
|
|
23
|
+
};
|
|
24
|
+
/** Serialize a MemoryRecord into a markdown file with canonical frontmatter. */
|
|
25
|
+
export declare function serializeRecord(record: MemoryRecord): string;
|
|
26
|
+
export declare function recordFromFrontmatter(data: Record<string, unknown>, body: string, fallbackId: string, now: number): MemoryRecord;
|
|
27
|
+
/** Filename for a record: `<id>.md` is enough; title is in frontmatter. */
|
|
28
|
+
export declare function recordFileName(id: string): string;
|
|
29
|
+
/** Human-readable index line, e.g. `- [Title](mem_x.md) 📌 — description`. */
|
|
30
|
+
export declare function indexLine(record: MemoryRecord): string;
|
|
31
|
+
//# sourceMappingURL=frontmatter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,YAAY,EAAuD,MAAM,YAAY,CAAC;AAGpG,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,aAAa,EAAE,OAAO,CAAC;CACxB;AA8CD,kCAAkC;AAClC,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAkGjG;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG;IAAE,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAKtF;AAwCD,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAkD5D;AA6BD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CA8DhI;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAKtD"}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { normalizeNewlines } from "./util.js";
|
|
2
|
+
const FIELD_ORDER = [
|
|
3
|
+
"schema",
|
|
4
|
+
"id",
|
|
5
|
+
"type",
|
|
6
|
+
"category",
|
|
7
|
+
"scope",
|
|
8
|
+
"project",
|
|
9
|
+
"project_name",
|
|
10
|
+
"title",
|
|
11
|
+
"description",
|
|
12
|
+
"tags",
|
|
13
|
+
"entities",
|
|
14
|
+
"importance",
|
|
15
|
+
"confidence",
|
|
16
|
+
"pinned",
|
|
17
|
+
"status",
|
|
18
|
+
"created",
|
|
19
|
+
"updated",
|
|
20
|
+
"last_used",
|
|
21
|
+
"used_count",
|
|
22
|
+
"valid_from",
|
|
23
|
+
"valid_to",
|
|
24
|
+
"observed_at",
|
|
25
|
+
"evidence",
|
|
26
|
+
"source",
|
|
27
|
+
"provenance",
|
|
28
|
+
"links",
|
|
29
|
+
"supersedes",
|
|
30
|
+
"superseded_by",
|
|
31
|
+
"sensitive",
|
|
32
|
+
"migrated_from",
|
|
33
|
+
];
|
|
34
|
+
function splitFrontmatter(raw) {
|
|
35
|
+
const text = normalizeNewlines(raw);
|
|
36
|
+
if (!text.startsWith("---"))
|
|
37
|
+
return { yaml: "", body: text, hadMarker: false };
|
|
38
|
+
const end = text.indexOf("\n---", 3);
|
|
39
|
+
if (end === -1)
|
|
40
|
+
return { yaml: "", body: text, hadMarker: false };
|
|
41
|
+
const yaml = text.slice(3, end).replace(/^\n/, "");
|
|
42
|
+
let body = text.slice(end + 4);
|
|
43
|
+
if (body.startsWith("\n"))
|
|
44
|
+
body = body.slice(1);
|
|
45
|
+
return { yaml, body, hadMarker: true };
|
|
46
|
+
}
|
|
47
|
+
/** Minimal YAML subset parser. */
|
|
48
|
+
export function parseYamlSubset(yaml) {
|
|
49
|
+
const data = {};
|
|
50
|
+
const errors = [];
|
|
51
|
+
const lines = yaml.split("\n");
|
|
52
|
+
let i = 0;
|
|
53
|
+
const parseScalar = (input) => {
|
|
54
|
+
const value = input.trim();
|
|
55
|
+
if (value === "" || value === "null" || value === "~")
|
|
56
|
+
return null;
|
|
57
|
+
if (value === "true")
|
|
58
|
+
return true;
|
|
59
|
+
if (value === "false")
|
|
60
|
+
return false;
|
|
61
|
+
if (/^-?\d+(\.\d+)?$/.test(value))
|
|
62
|
+
return Number(value);
|
|
63
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
64
|
+
const inner = value.slice(1, -1).trim();
|
|
65
|
+
if (!inner)
|
|
66
|
+
return [];
|
|
67
|
+
return splitInline(inner).map((item) => parseScalar(item));
|
|
68
|
+
}
|
|
69
|
+
if (value.startsWith("{") && value.endsWith("}")) {
|
|
70
|
+
// Nested objects (e.g. source with migratedFrom) are serialized as compact
|
|
71
|
+
// JSON on a single line to keep the YAML subset parser simple. See
|
|
72
|
+
// valueToYamlLines("…nested object inside nested object").
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(value);
|
|
75
|
+
if (parsed && typeof parsed === "object")
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// fall through: keep the raw string for malformed/legacy values
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
83
|
+
return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
};
|
|
87
|
+
const splitInline = (input) => {
|
|
88
|
+
const out = [];
|
|
89
|
+
let cur = "";
|
|
90
|
+
let quote = null;
|
|
91
|
+
for (const ch of input) {
|
|
92
|
+
if (quote) {
|
|
93
|
+
cur += ch;
|
|
94
|
+
if (ch === quote)
|
|
95
|
+
quote = null;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ch === '"' || ch === "'") {
|
|
99
|
+
quote = ch;
|
|
100
|
+
cur += ch;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === ",") {
|
|
104
|
+
out.push(cur.trim());
|
|
105
|
+
cur = "";
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
cur += ch;
|
|
109
|
+
}
|
|
110
|
+
if (cur.trim())
|
|
111
|
+
out.push(cur.trim());
|
|
112
|
+
return out;
|
|
113
|
+
};
|
|
114
|
+
while (i < lines.length) {
|
|
115
|
+
const line = lines[i] ?? "";
|
|
116
|
+
i++;
|
|
117
|
+
if (!line.trim() || line.trimStart().startsWith("#"))
|
|
118
|
+
continue;
|
|
119
|
+
const match = /^([A-Za-z0-9_][A-Za-z0-9_.-]*):(.*)$/.exec(line);
|
|
120
|
+
if (!match) {
|
|
121
|
+
errors.push(`line ${i}: not a key:value line: ${line.slice(0, 120)}`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const key = match[1];
|
|
125
|
+
const rest = (match[2] ?? "").trim();
|
|
126
|
+
if (rest === "") {
|
|
127
|
+
// nested map (one level) or block array
|
|
128
|
+
const nested = {};
|
|
129
|
+
const list = [];
|
|
130
|
+
let sawList = false;
|
|
131
|
+
while (i < lines.length) {
|
|
132
|
+
const next = lines[i] ?? "";
|
|
133
|
+
if (!/^\s+/.test(next))
|
|
134
|
+
break;
|
|
135
|
+
i++;
|
|
136
|
+
const trimmed = next.trim();
|
|
137
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
138
|
+
continue;
|
|
139
|
+
if (trimmed.startsWith("- ")) {
|
|
140
|
+
sawList = true;
|
|
141
|
+
list.push(parseScalar(trimmed.slice(2)));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const kv = /^([A-Za-z0-9_][A-Za-z0-9_.-]*):(.*)$/.exec(trimmed);
|
|
145
|
+
if (!kv) {
|
|
146
|
+
errors.push(`line ${i}: bad nested entry: ${trimmed.slice(0, 120)}`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
nested[kv[1]] = parseScalar(kv[2] ?? "");
|
|
150
|
+
}
|
|
151
|
+
data[key] = sawList ? list : nested;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
data[key] = parseScalar(rest);
|
|
155
|
+
}
|
|
156
|
+
return { data, errors };
|
|
157
|
+
}
|
|
158
|
+
export function parseFrontmatter(raw) {
|
|
159
|
+
const { yaml, body, hadMarker } = splitFrontmatter(raw);
|
|
160
|
+
if (!hadMarker)
|
|
161
|
+
return { data: {}, body, noFrontmatter: true, errors: [] };
|
|
162
|
+
const { data, errors } = parseYamlSubset(yaml);
|
|
163
|
+
return { data, body, noFrontmatter: false, errors };
|
|
164
|
+
}
|
|
165
|
+
function needsQuote(value) {
|
|
166
|
+
if (value === "")
|
|
167
|
+
return true;
|
|
168
|
+
if (/^[\s]|[\s]$/.test(value))
|
|
169
|
+
return true;
|
|
170
|
+
if (/^[-?:,[\]{}#&*!|>'"%@`]/.test(value))
|
|
171
|
+
return true;
|
|
172
|
+
if (value.includes(": ") || value.includes(" #"))
|
|
173
|
+
return true;
|
|
174
|
+
if (value === "true" || value === "false" || value === "null")
|
|
175
|
+
return true;
|
|
176
|
+
if (/^-?\d+(\.\d+)?$/.test(value))
|
|
177
|
+
return true;
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
function scalarToYaml(value) {
|
|
181
|
+
if (value === null || value === undefined)
|
|
182
|
+
return "null";
|
|
183
|
+
if (typeof value === "boolean")
|
|
184
|
+
return value ? "true" : "false";
|
|
185
|
+
if (typeof value === "number")
|
|
186
|
+
return Number.isFinite(value) ? String(value) : "null";
|
|
187
|
+
if (typeof value === "string")
|
|
188
|
+
return needsQuote(value) ? JSON.stringify(value) : value;
|
|
189
|
+
if (Array.isArray(value))
|
|
190
|
+
return `[${value.map((v) => scalarToYaml(v)).join(", ")}]`;
|
|
191
|
+
return JSON.stringify(value);
|
|
192
|
+
}
|
|
193
|
+
function valueToYamlLines(key, value) {
|
|
194
|
+
if (Array.isArray(value)) {
|
|
195
|
+
if (value.length === 0)
|
|
196
|
+
return [`${key}: []`];
|
|
197
|
+
const allScalar = value.every((v) => v === null || typeof v !== "object");
|
|
198
|
+
if (allScalar)
|
|
199
|
+
return [`${key}: ${scalarToYaml(value)}`];
|
|
200
|
+
return [`${key}:`, ...value.map((v) => ` - ${scalarToYaml(v)}`)];
|
|
201
|
+
}
|
|
202
|
+
if (value && typeof value === "object") {
|
|
203
|
+
const entries = Object.entries(value).filter(([, v]) => v !== undefined);
|
|
204
|
+
if (entries.length === 0)
|
|
205
|
+
return [`${key}: {}`];
|
|
206
|
+
if (entries.every(([, v]) => v === null || typeof v !== "object")) {
|
|
207
|
+
return [`${key}:`, ...entries.map(([k, v]) => ` ${k}: ${scalarToYaml(v)}`)];
|
|
208
|
+
}
|
|
209
|
+
// nested object inside nested object: serialize compressed JSON to keep the parser simple
|
|
210
|
+
return [`${key}: ${JSON.stringify(value)}`];
|
|
211
|
+
}
|
|
212
|
+
return [`${key}: ${scalarToYaml(value)}`];
|
|
213
|
+
}
|
|
214
|
+
/** Serialize a MemoryRecord into a markdown file with canonical frontmatter. */
|
|
215
|
+
export function serializeRecord(record) {
|
|
216
|
+
const data = {
|
|
217
|
+
schema: 1,
|
|
218
|
+
id: record.id,
|
|
219
|
+
type: record.type,
|
|
220
|
+
category: record.category,
|
|
221
|
+
scope: record.scope,
|
|
222
|
+
project: record.project,
|
|
223
|
+
project_name: record.projectName,
|
|
224
|
+
title: record.title,
|
|
225
|
+
description: record.description,
|
|
226
|
+
tags: record.tags,
|
|
227
|
+
entities: record.entities,
|
|
228
|
+
importance: record.importance,
|
|
229
|
+
confidence: record.confidence,
|
|
230
|
+
pinned: record.pinned,
|
|
231
|
+
status: record.status,
|
|
232
|
+
created: record.created,
|
|
233
|
+
updated: record.updated,
|
|
234
|
+
last_used: record.lastUsed,
|
|
235
|
+
used_count: record.usedCount,
|
|
236
|
+
valid_from: record.validFrom,
|
|
237
|
+
valid_to: record.validTo,
|
|
238
|
+
observed_at: record.observedAt,
|
|
239
|
+
evidence: record.evidence,
|
|
240
|
+
source: record.source,
|
|
241
|
+
provenance: record.provenance,
|
|
242
|
+
links: record.links,
|
|
243
|
+
supersedes: record.supersedes,
|
|
244
|
+
superseded_by: record.supersededBy,
|
|
245
|
+
sensitive: record.sensitive ?? undefined,
|
|
246
|
+
};
|
|
247
|
+
if (record.source?.migratedFrom)
|
|
248
|
+
data["migrated_from"] = record.source.migratedFrom;
|
|
249
|
+
const extra = record.extra ?? {};
|
|
250
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
251
|
+
if (k in data)
|
|
252
|
+
continue;
|
|
253
|
+
data[k] = v;
|
|
254
|
+
}
|
|
255
|
+
const keys = [
|
|
256
|
+
...FIELD_ORDER.filter((k) => k in data && data[k] !== undefined),
|
|
257
|
+
...Object.keys(data).filter((k) => !FIELD_ORDER.includes(k)),
|
|
258
|
+
];
|
|
259
|
+
const lines = ["---"];
|
|
260
|
+
for (const key of keys) {
|
|
261
|
+
const value = data[key];
|
|
262
|
+
if (value === undefined)
|
|
263
|
+
continue;
|
|
264
|
+
lines.push(...valueToYamlLines(key, value));
|
|
265
|
+
}
|
|
266
|
+
lines.push("---", "", record.content.trimEnd(), "");
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
function asString(value, fallback = "") {
|
|
270
|
+
if (typeof value === "string")
|
|
271
|
+
return value;
|
|
272
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
273
|
+
return String(value);
|
|
274
|
+
return fallback;
|
|
275
|
+
}
|
|
276
|
+
function asNumber(value, fallback) {
|
|
277
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
278
|
+
return value;
|
|
279
|
+
if (typeof value === "string" && /^-?\d+(\.\d+)?$/.test(value.trim()))
|
|
280
|
+
return Number(value);
|
|
281
|
+
return fallback;
|
|
282
|
+
}
|
|
283
|
+
function asBoolean(value, fallback) {
|
|
284
|
+
if (typeof value === "boolean")
|
|
285
|
+
return value;
|
|
286
|
+
if (value === "true")
|
|
287
|
+
return true;
|
|
288
|
+
if (value === "false")
|
|
289
|
+
return false;
|
|
290
|
+
return fallback;
|
|
291
|
+
}
|
|
292
|
+
function asStringArray(value) {
|
|
293
|
+
if (!Array.isArray(value))
|
|
294
|
+
return [];
|
|
295
|
+
return value.map((v) => asString(v)).filter((v) => v.length > 0);
|
|
296
|
+
}
|
|
297
|
+
const TYPES = ["user", "feedback", "project", "reference"];
|
|
298
|
+
const STATUSES = ["active", "superseded", "archived", "dropped"];
|
|
299
|
+
export function recordFromFrontmatter(data, body, fallbackId, now) {
|
|
300
|
+
const id = asString(data["id"], fallbackId);
|
|
301
|
+
const typeRaw = asString(data["type"], "project");
|
|
302
|
+
const type = TYPES.includes(typeRaw) ? typeRaw : "project";
|
|
303
|
+
const scopeRaw = asString(data["scope"], "project");
|
|
304
|
+
const scope = scopeRaw === "user" ? "user" : "project";
|
|
305
|
+
const statusRaw = asString(data["status"], "active");
|
|
306
|
+
const status = STATUSES.includes(statusRaw) ? statusRaw : "active";
|
|
307
|
+
const created = asNumber(data["created"], now);
|
|
308
|
+
const updated = asNumber(data["updated"], created);
|
|
309
|
+
const sourceRaw = (data["source"] ?? {});
|
|
310
|
+
const migrated = (data["migrated_from"] ?? sourceRaw["migrated_from"]);
|
|
311
|
+
const source = {
|
|
312
|
+
harness: asString(sourceRaw["harness"], "unknown"),
|
|
313
|
+
session: asString(sourceRaw["session"]) || undefined,
|
|
314
|
+
turn: typeof sourceRaw["turn"] === "number" ? sourceRaw["turn"] : undefined,
|
|
315
|
+
ts: asNumber(sourceRaw["ts"], created),
|
|
316
|
+
migratedFrom: migrated && typeof migrated === "object"
|
|
317
|
+
? {
|
|
318
|
+
system: asString(migrated["system"], "unknown"),
|
|
319
|
+
path: asString(migrated["path"]) || undefined,
|
|
320
|
+
originalId: asString(migrated["original_id"] ?? migrated["originalId"]) || undefined,
|
|
321
|
+
}
|
|
322
|
+
: undefined,
|
|
323
|
+
};
|
|
324
|
+
const validFrom = asNumber(data["valid_from"], created);
|
|
325
|
+
const validToRaw = data["valid_to"];
|
|
326
|
+
const validTo = validToRaw === null || validToRaw === undefined || validToRaw === "" ? null : asNumber(validToRaw, validFrom);
|
|
327
|
+
return {
|
|
328
|
+
schema: 1,
|
|
329
|
+
id,
|
|
330
|
+
type,
|
|
331
|
+
category: asString(data["category"]) || undefined,
|
|
332
|
+
scope,
|
|
333
|
+
project: asString(data["project"]) || undefined,
|
|
334
|
+
projectName: asString(data["project_name"]) || undefined,
|
|
335
|
+
title: asString(data["title"], "(untitled)"),
|
|
336
|
+
description: asString(data["description"]),
|
|
337
|
+
content: body.trimEnd(),
|
|
338
|
+
tags: asStringArray(data["tags"]),
|
|
339
|
+
entities: asStringArray(data["entities"]),
|
|
340
|
+
importance: asNumber(data["importance"], 5),
|
|
341
|
+
confidence: asNumber(data["confidence"], 0.8),
|
|
342
|
+
pinned: asBoolean(data["pinned"], false),
|
|
343
|
+
status,
|
|
344
|
+
created,
|
|
345
|
+
updated,
|
|
346
|
+
lastUsed: data["last_used"] === undefined || data["last_used"] === null ? undefined : asNumber(data["last_used"], 0),
|
|
347
|
+
usedCount: asNumber(data["used_count"], 0),
|
|
348
|
+
validFrom,
|
|
349
|
+
validTo,
|
|
350
|
+
observedAt: data["observed_at"] === undefined ? undefined : asNumber(data["observed_at"], created),
|
|
351
|
+
evidence: asNumber(data["evidence"], 0),
|
|
352
|
+
source,
|
|
353
|
+
provenance: asStringArray(data["provenance"]),
|
|
354
|
+
links: asStringArray(data["links"]),
|
|
355
|
+
supersedes: asStringArray(data["supersedes"]),
|
|
356
|
+
supersededBy: asString(data["superseded_by"]) || null,
|
|
357
|
+
sensitive: data["sensitive"] === undefined ? undefined : asBoolean(data["sensitive"], false),
|
|
358
|
+
extra: {},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/** Filename for a record: `<id>.md` is enough; title is in frontmatter. */
|
|
362
|
+
export function recordFileName(id) {
|
|
363
|
+
return `${id}.md`;
|
|
364
|
+
}
|
|
365
|
+
/** Human-readable index line, e.g. `- [Title](mem_x.md) 📌 — description`. */
|
|
366
|
+
export function indexLine(record) {
|
|
367
|
+
const pin = record.pinned ? " 📌" : "";
|
|
368
|
+
const type = record.type ? `[${record.type}] ` : "";
|
|
369
|
+
const desc = record.description ? ` — ${record.description}` : "";
|
|
370
|
+
return `- ${type}[${record.title}](${recordFileName(record.id)})${pin}${desc}`;
|
|
371
|
+
}
|
|
372
|
+
//# sourceMappingURL=frontmatter.js.map
|