akm-cli 0.10.261008-beta → 0.10.261010-beta
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/CHANGELOG.md +97 -0
- package/STABILITY.md +25 -8
- package/dist/api.d.ts +6 -0
- package/dist/api.js +12 -1
- package/dist/assets/prompts/consolidate-pair-check.md +2 -0
- package/dist/assets/prompts/consolidate-pair-same-age.md +1 -0
- package/dist/assets/prompts/consolidate-pair-terms.md +6 -0
- package/dist/assets/prompts/consolidate-pair.md +4 -12
- package/dist/assets/prompts/memory-infer-system.md +1 -1
- package/dist/assets/prompts/memory-infer-user.md +3 -2
- package/dist/assets/ui/app.js +59 -2
- package/dist/assets/ui/asset.js +71 -6
- package/dist/assets/ui/bundles.js +80 -3
- package/dist/assets/ui/diff-view.js +1 -1
- package/dist/assets/ui/editor.js +103 -0
- package/dist/assets/ui/index.html +2 -0
- package/dist/assets/ui/lib.js +51 -1
- package/dist/assets/ui/markdown.js +86 -19
- package/dist/assets/ui/search.js +25 -5
- package/dist/assets/ui/styles.css +70 -0
- package/dist/commands/health/improve-metrics.js +11 -1
- package/dist/commands/health/md-report.js +3 -3
- package/dist/commands/health.js +1 -2
- package/dist/commands/improve/consolidate/pair-pass.js +59 -31
- package/dist/commands/improve/extract.js +18 -12
- package/dist/commands/improve/loop-stages.js +2 -2
- package/dist/commands/improve/memory/memory-belief.js +2 -2
- package/dist/commands/proposal/repository.js +44 -13
- package/dist/commands/read/curate.js +78 -19
- package/dist/commands/read/knowledge.js +109 -77
- package/dist/commands/read/search-cli.js +25 -11
- package/dist/commands/read/search.js +3 -3
- package/dist/commands/read/show.js +47 -1
- package/dist/commands/sources/bundle-cli.js +4 -6
- package/dist/commands/sources/installed-stashes.js +10 -19
- package/dist/commands/ui/api.js +95 -5
- package/dist/commands/ui/document-source.js +51 -0
- package/dist/commands/ui/git-read.js +18 -2
- package/dist/commands/ui/proposal-actions.js +5 -3
- package/dist/commands/ui/server.js +25 -13
- package/dist/commands/ui/ui-cli.js +3 -3
- package/dist/commands/ui/workspace-actions.js +264 -0
- package/dist/commands/workflow-cli.js +3 -1
- package/dist/core/adapter/adapters/akm-adapter.js +1 -0
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +3 -5
- package/dist/core/adapter/adapters/okf-adapter.js +3 -16
- package/dist/core/asset/akm-markdown.js +19 -3
- package/dist/core/asset/markdown-link-targets.js +43 -0
- package/dist/core/asset/markdown.js +14 -6
- package/dist/core/config/config.js +6 -4
- package/dist/core/non-task-input.js +67 -3
- package/dist/core/write-source.js +2 -2
- package/dist/indexer/index-writer-lock.js +5 -1
- package/dist/indexer/passes/memory-inference.js +28 -6
- package/dist/indexer/passes/metadata.js +17 -0
- package/dist/indexer/scan/doc-to-entry.js +1 -0
- package/dist/indexer/search/db-search.js +16 -7
- package/dist/indexer/search/search-hit-enrichers.js +3 -3
- package/dist/llm/memory-infer.js +23 -14
- package/dist/output/shapes/curate.js +3 -1
- package/dist/output/shapes/helpers.js +6 -0
- package/dist/output/text/show-format.js +7 -0
- package/dist/scripts/akm-migrate-node.js +1820 -329
- package/dist/scripts/akm-migrate.js +1820 -329
- package/dist/storage/repositories/index-entries-repository.js +16 -0
- package/dist/storage/repositories/index-fts-repository.js +28 -6
- package/dist/storage/repositories/index-vec-repository.js +19 -3
- package/dist/workflows/exec/environment.js +4 -8
- package/dist/workflows/parser.js +13 -4
- package/docs/architecture/0.10-process-rules.md +9 -7
- package/docs/architecture/architecture.md +4 -0
- package/docs/architecture/internals/storage-locations.md +1 -1
- package/docs/architecture/persisted-data-compat.md +1 -1
- package/docs/guides/author-workflows.md +78 -28
- package/docs/guides/bundles.md +16 -2
- package/docs/reference/api.md +19 -1
- package/docs/reference/cli.md +90 -18
- package/docs/reference/env-and-secrets.md +11 -0
- package/docs/reference/memory.md +7 -6
- package/docs/reference/ui-api.md +84 -8
- package/docs/reference/workflow-schema.md +63 -32
- package/package.json +2 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Source-first creation and revision-checked editing use the existing Markdown writer.
|
|
2
|
+
import { actionForm, api, assetHref, emptyState, html, mount, pageHeader, post, setUnsaved } from "./lib.js";
|
|
3
|
+
import { markdown, splitFrontmatter } from "./markdown.js";
|
|
4
|
+
|
|
5
|
+
export async function render(ctx) {
|
|
6
|
+
const ref = ctx.query.get("ref");
|
|
7
|
+
if (ref) return edit(ctx, ref);
|
|
8
|
+
const data = await api("/api/bundles");
|
|
9
|
+
const writable = data.bundles.filter((b) => b.enabled && b.writable && b.components.some((c) => c.adapter === "akm"));
|
|
10
|
+
ctx.after.push((root) => {
|
|
11
|
+
actionForm(root, ".create-form", (_form, values) => post("/api/knowledge/create", Object.fromEntries(values)), {
|
|
12
|
+
after: (result) => {
|
|
13
|
+
setUnsaved(false);
|
|
14
|
+
if (result.ref) location.hash = assetHref(result.ref);
|
|
15
|
+
else location.hash = "#/search";
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
const form = root.querySelector(".create-form");
|
|
19
|
+
if (form) {
|
|
20
|
+
const draw = () =>
|
|
21
|
+
mount(root.querySelector("[data-preview]"), markdown(splitFrontmatter(form.elements.content.value).body));
|
|
22
|
+
form.elements.content.addEventListener("input", draw);
|
|
23
|
+
form.addEventListener("input", () => setUnsaved(true));
|
|
24
|
+
draw();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return html`${pageHeader("New knowledge", "Write Markdown and optional frontmatter. Existing files are never overwritten.")}
|
|
28
|
+
<p><a href="#/search">Back to search</a></p>
|
|
29
|
+
${
|
|
30
|
+
writable.length === 0
|
|
31
|
+
? emptyState(
|
|
32
|
+
"No enabled writable AKM bundle is available.",
|
|
33
|
+
html`Check <a href="#/bundles">bundle settings</a>.`,
|
|
34
|
+
)
|
|
35
|
+
: html`
|
|
36
|
+
<form class="create-form form-grid">
|
|
37
|
+
<label>Bundle<select name="bundle">${writable.map((b) => html`<option value="${b.name}" ${b.name === (data.defaultWriteTarget ?? data.defaultBundle) ? html`selected` : ""}>${b.name}</option>`)}</select></label>
|
|
38
|
+
<label>Name<input name="name" required placeholder="restore-database"></label>
|
|
39
|
+
<label>Subdirectory<input name="path" placeholder="Optional, under knowledge/"></label>
|
|
40
|
+
<label>Markdown<textarea name="content" rows="16" required spellcheck="false"># New document
|
|
41
|
+
|
|
42
|
+
</textarea></label>
|
|
43
|
+
<button type="submit">Create knowledge</button><div data-status></div>
|
|
44
|
+
</form>
|
|
45
|
+
<section><h2>Preview</h2><div data-preview></div></section>`
|
|
46
|
+
}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A textarea's value has every line break as LF.
|
|
50
|
+
const asTextarea = (content) => content.replace(/\r\n?/g, "\n");
|
|
51
|
+
|
|
52
|
+
/** The save request for `draft`: an untouched draft sends the exact source; a uniform-CRLF source keeps CRLF. */
|
|
53
|
+
function saveBody(source, draft) {
|
|
54
|
+
const usesCrlf = source.content.includes("\r\n") && !/[\r\n]/.test(source.content.replace(/\r\n/g, ""));
|
|
55
|
+
const content =
|
|
56
|
+
draft === asTextarea(source.content) ? source.content : usesCrlf ? draft.replace(/\r?\n/g, "\r\n") : draft;
|
|
57
|
+
return { ref: source.ref, revision: source.revision, content };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function edit(ctx, ref) {
|
|
61
|
+
let source = await api("/api/asset/source", { ref });
|
|
62
|
+
const context = { ref: source.ref, bundle: source.ref.split("//")[0] };
|
|
63
|
+
ctx.after.push((root) => {
|
|
64
|
+
const input = root.querySelector('[name="content"]');
|
|
65
|
+
// HTML textarea parsing drops an initial LF, so initialize explicitly.
|
|
66
|
+
input.value = source.content;
|
|
67
|
+
actionForm(
|
|
68
|
+
root,
|
|
69
|
+
".edit-form",
|
|
70
|
+
async () => {
|
|
71
|
+
try {
|
|
72
|
+
return await post("/api/asset/save", saveBody(source, input.value));
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error?.code !== "ASSET_CONTENT_CHANGED") throw error;
|
|
75
|
+
// Take the new revision in place; the draft in the textarea stays as typed.
|
|
76
|
+
source = await api("/api/asset/source", { ref: source.ref });
|
|
77
|
+
throw new Error(
|
|
78
|
+
"The document changed on disk since you opened it. Your draft was kept; saving again will overwrite the newer version.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
after: () => {
|
|
84
|
+
setUnsaved(false);
|
|
85
|
+
location.hash = assetHref(source.ref);
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
const draw = () =>
|
|
90
|
+
mount(root.querySelector("[data-preview]"), markdown(splitFrontmatter(input.value).body, context));
|
|
91
|
+
input.addEventListener("input", () => {
|
|
92
|
+
setUnsaved(input.value !== asTextarea(source.content));
|
|
93
|
+
draw();
|
|
94
|
+
});
|
|
95
|
+
draw();
|
|
96
|
+
});
|
|
97
|
+
return html`${pageHeader("Edit Markdown", source.ref)}
|
|
98
|
+
<p><a href="${assetHref(source.ref)}">Back to document</a></p>
|
|
99
|
+
<p class="muted">Edit the full ${source.type} source, including frontmatter. If the file changes on disk meanwhile, the first save is refused and your draft is kept; saving again overwrites the newer version.</p>
|
|
100
|
+
<form class="edit-form form-grid"><label>Markdown<textarea name="content" rows="20" spellcheck="false">${source.content}</textarea></label>
|
|
101
|
+
<button type="submit">Save Markdown</button><div data-status></div>
|
|
102
|
+
</form><section><h2>Preview</h2><div data-preview></div></section>`;
|
|
103
|
+
}
|
|
@@ -20,8 +20,10 @@
|
|
|
20
20
|
<li><a data-route="changes" href="#/changes">Changes</a></li>
|
|
21
21
|
<li><a data-route="search" href="#/search">Search</a></li>
|
|
22
22
|
</ul>
|
|
23
|
+
<button type="button" data-refresh>Refresh view</button>
|
|
23
24
|
<div class="nav-meta" id="nav-meta"></div>
|
|
24
25
|
</nav>
|
|
26
|
+
<div id="unsaved" class="notice notice-warn" role="alert" hidden></div>
|
|
25
27
|
<main id="app" aria-live="polite">
|
|
26
28
|
<noscript><p>akm ui needs JavaScript. It makes no network requests beyond its own local server.</p></noscript>
|
|
27
29
|
</main>
|
package/dist/assets/ui/lib.js
CHANGED
|
@@ -102,7 +102,7 @@ export function api(path, query = {}) {
|
|
|
102
102
|
return request(url, { headers: { "X-Akm-Ui-Token": token ?? "" }, cache: "no-store" });
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/** POST `body` as JSON to
|
|
105
|
+
/** POST `body` as JSON to an explicit action. The token goes in the header, never the URL. */
|
|
106
106
|
export function post(path, body = {}) {
|
|
107
107
|
return request(path, {
|
|
108
108
|
method: "POST",
|
|
@@ -236,3 +236,53 @@ export function pretty(value) {
|
|
|
236
236
|
export function percent(part, total) {
|
|
237
237
|
return total > 0 ? Math.round((part / total) * 1000) / 10 : 0;
|
|
238
238
|
}
|
|
239
|
+
|
|
240
|
+
// ---- unsaved drafts and one-shot status ------------------------------------
|
|
241
|
+
|
|
242
|
+
let unsaved = false;
|
|
243
|
+
/** The editor marks a draft that leaving the page would lose; the router asks before discarding it. */
|
|
244
|
+
export function setUnsaved(value) {
|
|
245
|
+
unsaved = value;
|
|
246
|
+
}
|
|
247
|
+
export function hasUnsaved() {
|
|
248
|
+
return unsaved;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let flash = null;
|
|
252
|
+
/** Show the status a form left before its page re-rendered, once. Called by the router after each render. */
|
|
253
|
+
export function showFlash(root) {
|
|
254
|
+
const pending = flash;
|
|
255
|
+
flash = null;
|
|
256
|
+
const status = pending && root.querySelectorAll(pending.selector)[pending.index]?.querySelector("[data-status]");
|
|
257
|
+
if (status) mount(status, html`<p role="status" class="text-ok">${pending.message}</p>`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Bind an explicit form action; errors stay beside the user's inputs. */
|
|
261
|
+
export function actionForm(root, selector, action, { after } = {}) {
|
|
262
|
+
root.querySelectorAll(selector).forEach((form, index) => {
|
|
263
|
+
form.addEventListener("submit", async (event) => {
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
const status = form.querySelector("[data-status]");
|
|
266
|
+
const button = event.submitter;
|
|
267
|
+
let locked = false;
|
|
268
|
+
if (button) button.disabled = true;
|
|
269
|
+
if (status) mount(status, html`<p role="status">Working…</p>`);
|
|
270
|
+
try {
|
|
271
|
+
const result = await action(form, new FormData(form));
|
|
272
|
+
const message = result?.saved === false ? "No changes." : "Saved.";
|
|
273
|
+
if (status) mount(status, html`<p role="status" class="text-ok">${message}</p>`);
|
|
274
|
+
if (after) after(result);
|
|
275
|
+
else {
|
|
276
|
+
flash = { selector, index, message };
|
|
277
|
+
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
|
278
|
+
}
|
|
279
|
+
} catch (error) {
|
|
280
|
+
// The action may already have happened; the server asks not to repeat it without a reload.
|
|
281
|
+
locked = error?.code === "INVALID_COMMAND_RESPONSE";
|
|
282
|
+
if (status) mount(status, html`<p role="alert" class="text-fail">${error.message}</p>`);
|
|
283
|
+
} finally {
|
|
284
|
+
if (button && !locked) button.disabled = false;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
@@ -1,12 +1,35 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Safe document rendering: headings, lists, tables, task items, fenced code, inline code, bold, links and
|
|
2
|
+
// [[wikilinks]]. Link targets and heading selectors come from AKM's indexed reads.
|
|
2
3
|
// It never touches raw text except through `html`, which escapes every interpolated value.
|
|
3
|
-
|
|
4
4
|
import { assetHref, html } from "./lib.js";
|
|
5
5
|
|
|
6
6
|
const SAFE_URL = /^(https?:\/\/|mailto:|#)/i;
|
|
7
7
|
const INLINE = /`([^`]+)`|\[\[([^\]]+)\]\]|\[([^\]]+)\]\(([^)\s]+)\)|\*\*([^*]+)\*\*/g;
|
|
8
|
+
// The same fence rule as the server's parseMarkdownToc (CommonMark), so both agree on which headings exist:
|
|
9
|
+
// a fence opens with 3+ backticks or tildes indented at most 3 spaces (a backtick fence's info string has
|
|
10
|
+
// no backtick) and closes only on a line of the same character, at least as long, then only whitespace.
|
|
11
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
12
|
+
const opensFence = (line) => {
|
|
13
|
+
const m = FENCE.exec(line);
|
|
14
|
+
return m && !(m[1][0] === "`" && m[2].includes("`")) ? m[1] : null;
|
|
15
|
+
};
|
|
16
|
+
const closesFence = (line, open) => {
|
|
17
|
+
const m = FENCE.exec(line);
|
|
18
|
+
return m !== null && m[1][0] === open[0] && m[1].length >= open.length && !m[2].trim();
|
|
19
|
+
};
|
|
20
|
+
const TABLE_DELIMITER = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)*\|?\s*$/;
|
|
21
|
+
|
|
22
|
+
/** DOM id of a heading: prefixed so a slug like "app" never collides with the page's own ids. */
|
|
23
|
+
export const headingId = (slug) => `h-${slug}`;
|
|
8
24
|
|
|
9
|
-
function link(text, url) {
|
|
25
|
+
function link(text, url, context) {
|
|
26
|
+
if (context.links && Object.hasOwn(context.links, url))
|
|
27
|
+
return html`<a href="${assetHref(context.links[url])}">${text}</a>`;
|
|
28
|
+
if (url.startsWith("#")) {
|
|
29
|
+
// A bare "#x" would be read by the hash router as a page; without an asset to anchor to, show text.
|
|
30
|
+
if (!context.ref) return html`${text}`;
|
|
31
|
+
return html`<a href="${assetHref(context.ref)}§ion=${encodeURIComponent(url.slice(1))}">${text}</a>`;
|
|
32
|
+
}
|
|
10
33
|
if (!SAFE_URL.test(url)) return html`${text}`;
|
|
11
34
|
const external = /^(https?:|mailto:)/i.test(url);
|
|
12
35
|
return external
|
|
@@ -14,16 +37,18 @@ function link(text, url) {
|
|
|
14
37
|
: html`<a href="${url}">${text}</a>`;
|
|
15
38
|
}
|
|
16
39
|
|
|
17
|
-
/** Inline spans of one line of text. */
|
|
18
|
-
export function inline(text) {
|
|
40
|
+
/** Inline spans of one line of text. `context` is the same as for {@link markdown}. */
|
|
41
|
+
export function inline(text, context = {}) {
|
|
19
42
|
const out = [];
|
|
20
43
|
let last = 0;
|
|
21
44
|
for (const m of text.matchAll(INLINE)) {
|
|
22
45
|
out.push(text.slice(last, m.index));
|
|
23
46
|
if (m[1] !== undefined) out.push(html`<code>${m[1]}</code>`);
|
|
24
|
-
else if (m[2] !== undefined)
|
|
25
|
-
|
|
26
|
-
|
|
47
|
+
else if (m[2] !== undefined) {
|
|
48
|
+
const [ref, label] = m[2].split("|");
|
|
49
|
+
const target = context.bundle && !ref.includes("//") ? `${context.bundle}//${ref.trim()}` : ref.trim();
|
|
50
|
+
out.push(html`<a href="${assetHref(target)}"><code>${label ?? ref}</code></a>`);
|
|
51
|
+
} else if (m[3] !== undefined) out.push(link(m[3], m[4], context));
|
|
27
52
|
else out.push(html`<strong>${m[5]}</strong>`);
|
|
28
53
|
last = m.index + m[0].length;
|
|
29
54
|
}
|
|
@@ -31,16 +56,31 @@ export function inline(text) {
|
|
|
31
56
|
return html`${out}`;
|
|
32
57
|
}
|
|
33
58
|
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
const cells = (line) =>
|
|
60
|
+
line
|
|
61
|
+
.trim()
|
|
62
|
+
.replace(/^\|/, "")
|
|
63
|
+
.replace(/\|$/, "")
|
|
64
|
+
.split("|")
|
|
65
|
+
.map((cell) => cell.trim());
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Render markdown text to safe HTML. `context` is optional: `ref` and `bundle` anchor `#section` links and
|
|
69
|
+
* bare [[wikilinks]] to the asset, `links` maps raw link targets to indexed refs, and `headings` is the
|
|
70
|
+
* server's heading list ({line, slug}), matched by `line` against `startLine`, the source line of the
|
|
71
|
+
* text's first line (after frontmatter).
|
|
72
|
+
*/
|
|
73
|
+
export function markdown(source, context = {}) {
|
|
36
74
|
const lines = String(source ?? "")
|
|
37
75
|
.replace(/\r\n?/g, "\n")
|
|
38
76
|
.split("\n");
|
|
39
77
|
const blocks = [];
|
|
40
78
|
let paragraph = [];
|
|
41
79
|
let list = null;
|
|
80
|
+
const slugByLine = new Map((context.headings ?? []).map((h) => [h.line, h.slug]));
|
|
81
|
+
const firstLine = context.startLine ?? 1;
|
|
42
82
|
const flushParagraph = () => {
|
|
43
|
-
if (paragraph.length > 0) blocks.push(html`<p>${inline(paragraph.join(" "))}</p>`);
|
|
83
|
+
if (paragraph.length > 0) blocks.push(html`<p>${inline(paragraph.join(" "), context)}</p>`);
|
|
44
84
|
paragraph = [];
|
|
45
85
|
};
|
|
46
86
|
const flushList = () => {
|
|
@@ -49,21 +89,40 @@ export function markdown(source) {
|
|
|
49
89
|
};
|
|
50
90
|
for (let i = 0; i < lines.length; i++) {
|
|
51
91
|
const line = lines[i];
|
|
52
|
-
const fence =
|
|
92
|
+
const fence = opensFence(line);
|
|
53
93
|
if (fence) {
|
|
54
94
|
flushParagraph();
|
|
55
95
|
flushList();
|
|
56
96
|
const code = [];
|
|
57
|
-
for (i++; i < lines.length &&
|
|
97
|
+
for (i++; i < lines.length && !closesFence(lines[i], fence); i++) code.push(lines[i]);
|
|
58
98
|
blocks.push(html`<pre class="code"><code>${code.join("\n")}</code></pre>`);
|
|
59
99
|
continue;
|
|
60
100
|
}
|
|
61
|
-
const heading = /^(#{1,6})\s+(
|
|
101
|
+
const heading = /^(#{1,6})\s+(.+)$/.exec(line);
|
|
62
102
|
if (heading) {
|
|
63
103
|
flushParagraph();
|
|
64
104
|
flushList();
|
|
65
105
|
const level = Math.min(heading[1].length + 1, 6);
|
|
66
|
-
|
|
106
|
+
const slug = slugByLine.get(firstLine + i);
|
|
107
|
+
blocks.push(
|
|
108
|
+
html`<h${level}${slug ? html` id="${headingId(slug)}"` : ""}>${inline(heading[2].replace(/\s+#+\s*$/, ""), context)}</h${level}>`,
|
|
109
|
+
);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (line.includes("|") && lines[i + 1]?.includes("|") && TABLE_DELIMITER.test(lines[i + 1])) {
|
|
113
|
+
flushParagraph();
|
|
114
|
+
flushList();
|
|
115
|
+
const headers = cells(line);
|
|
116
|
+
i += 2;
|
|
117
|
+
const rows = [];
|
|
118
|
+
while (i < lines.length && lines[i].includes("|") && lines[i].trim()) {
|
|
119
|
+
rows.push(html`<tr>${cells(lines[i]).map((cell) => html`<td>${inline(cell, context)}</td>`)}</tr>`);
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
i--;
|
|
123
|
+
blocks.push(
|
|
124
|
+
html`<div class="table-wrap"><table><thead><tr>${headers.map((cell) => html`<th>${inline(cell, context)}</th>`)}</tr></thead><tbody>${rows}</tbody></table></div>`,
|
|
125
|
+
);
|
|
67
126
|
continue;
|
|
68
127
|
}
|
|
69
128
|
const item = /^\s*(?:([-*+])|(\d+)[.)])\s+(.*)$/.exec(line);
|
|
@@ -72,7 +131,12 @@ export function markdown(source) {
|
|
|
72
131
|
const ordered = item[2] !== undefined;
|
|
73
132
|
if (list && list.ordered !== ordered) flushList();
|
|
74
133
|
list ??= { ordered, items: [] };
|
|
75
|
-
|
|
134
|
+
const task = /^\[([ xX])\]\s+(.*)$/.exec(item[3]);
|
|
135
|
+
list.items.push(
|
|
136
|
+
task
|
|
137
|
+
? html`<li class="task-item"><input type="checkbox" disabled ${task[1].toLowerCase() === "x" ? html`checked` : ""} aria-label="${task[2]}">${inline(task[2], context)}</li>`
|
|
138
|
+
: html`<li>${inline(item[3], context)}</li>`,
|
|
139
|
+
);
|
|
76
140
|
continue;
|
|
77
141
|
}
|
|
78
142
|
if (line.trim() === "") {
|
|
@@ -88,16 +152,19 @@ export function markdown(source) {
|
|
|
88
152
|
return html`<div class="md">${blocks}</div>`;
|
|
89
153
|
}
|
|
90
154
|
|
|
91
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* Split leading `---` frontmatter from `content`: { fields: [[key, value]], body, startLine }, where
|
|
157
|
+
* `startLine` is the 1-based source line the body starts on (pass it to {@link markdown}).
|
|
158
|
+
*/
|
|
92
159
|
export function splitFrontmatter(content) {
|
|
93
160
|
const text = String(content ?? "").replace(/\r\n?/g, "\n");
|
|
94
161
|
const m = /^---\n([\s\S]*?)\n---[ \t]*(?:\n|$)/.exec(text);
|
|
95
|
-
if (!m) return { fields: [], body: text };
|
|
162
|
+
if (!m) return { fields: [], body: text, startLine: 1 };
|
|
96
163
|
const fields = [];
|
|
97
164
|
for (const line of m[1].split("\n")) {
|
|
98
165
|
const kv = /^([A-Za-z0-9_.-]+):\s*(.*)$/.exec(line);
|
|
99
166
|
if (kv) fields.push([kv[1], kv[2]]);
|
|
100
167
|
else if (fields.length > 0 && line.trim() !== "") fields[fields.length - 1][1] += `\n${line}`;
|
|
101
168
|
}
|
|
102
|
-
return { fields, body: text.slice(m[0].length) };
|
|
169
|
+
return { fields, body: text.slice(m[0].length), startLine: m[0].split("\n").length };
|
|
103
170
|
}
|
package/dist/assets/ui/search.js
CHANGED
|
@@ -4,8 +4,13 @@ import { api, assetHref, badge, emptyState, html, pageHeader } from "./lib.js";
|
|
|
4
4
|
|
|
5
5
|
const RESULT_LIMIT = 50;
|
|
6
6
|
|
|
7
|
+
/** Exactly `values` as options, with `selected` chosen. */
|
|
8
|
+
function choices(values, selected) {
|
|
9
|
+
return values.map((v) => html`<option value="${v}" ${v === selected ? html`selected` : ""}>${v}</option>`);
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
function options(values, selected, allLabel) {
|
|
8
|
-
return html`<option value="">${allLabel}</option>${values
|
|
13
|
+
return html`<option value="">${allLabel}</option>${choices(values, selected)}`;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
function hit(h) {
|
|
@@ -21,8 +26,13 @@ export async function render(ctx) {
|
|
|
21
26
|
const q = ctx.query.get("q") ?? "";
|
|
22
27
|
const type = ctx.query.get("type") ?? "";
|
|
23
28
|
const bundle = ctx.query.get("bundle") ?? "";
|
|
29
|
+
const belief = ctx.query.get("belief") || "current";
|
|
30
|
+
const limit = ctx.query.get("limit") || String(RESULT_LIMIT);
|
|
31
|
+
const scope = Object.fromEntries(
|
|
32
|
+
["user", "agent", "run", "channel"].map((key) => [`scope_${key}`, ctx.query.get(`scope_${key}`) ?? ""]),
|
|
33
|
+
);
|
|
24
34
|
const [result, bundles] = await Promise.all([
|
|
25
|
-
api("/api/search", { q, type, bundle, limit
|
|
35
|
+
api("/api/search", { q, type, bundle, belief, ...scope, limit }),
|
|
26
36
|
api("/api/bundles").catch(() => null),
|
|
27
37
|
]);
|
|
28
38
|
const types = new Set(Object.keys(bundles?.index?.byType ?? {}));
|
|
@@ -46,17 +56,27 @@ export async function render(ctx) {
|
|
|
46
56
|
for (const select of form.querySelectorAll("select")) select.addEventListener("change", go);
|
|
47
57
|
});
|
|
48
58
|
|
|
49
|
-
|
|
59
|
+
const groups = new Map();
|
|
60
|
+
for (const h of result.hits) {
|
|
61
|
+
const group = h.ref.slice(0, h.ref.lastIndexOf("/")) || h.type;
|
|
62
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
63
|
+
groups.get(group).push(h);
|
|
64
|
+
}
|
|
65
|
+
return html`${pageHeader("Search", "Search or browse your indexed assets. Browsing is not counted as usage.")}
|
|
66
|
+
<p class="action-links"><a href="#/new">New knowledge</a> <span class="muted small">· Ctrl/Cmd+K opens search</span></p>
|
|
50
67
|
<form class="search-form" role="search">
|
|
51
|
-
<input type="search" name="q" value="${q}" placeholder="Search assets (empty
|
|
68
|
+
<input type="search" name="q" value="${q}" placeholder="Search assets (empty browses the index)" aria-label="Query" autofocus>
|
|
52
69
|
<select name="type" aria-label="Type">${options([...types].sort(), type, "All types")}</select>
|
|
53
70
|
<select name="bundle" aria-label="Bundle">${options(bundleNames, bundle, "All bundles")}</select>
|
|
71
|
+
<select name="belief" aria-label="Belief state">${choices(["current", "all", "historical"], belief)}</select>
|
|
72
|
+
<select name="limit" aria-label="Result limit">${choices(["50", "100", "200"], limit)}</select>
|
|
73
|
+
<details class="scope-controls"><summary>Scope filters</summary><div class="form-grid">${Object.entries(scope).map(([key, value]) => html`<label>${key.slice(6)}<input name="${key}" value="${value}" placeholder="Any"></label>`)}</div></details>
|
|
54
74
|
<button type="submit">Search</button>
|
|
55
75
|
</form>
|
|
56
76
|
${(result.warnings ?? []).map((w) => html`<div class="notice notice-warn">${w}</div>`)}
|
|
57
77
|
${
|
|
58
78
|
result.hits.length === 0
|
|
59
79
|
? emptyState(q ? `No assets match "${q}".` : "No assets found.", result.tip ?? "")
|
|
60
|
-
: html`<p class="muted">${result.hits.length} ${result.hits.length === 1 ? "result" : "results"}${result.hits.length ===
|
|
80
|
+
: html`<p class="muted">${result.hits.length} ${result.hits.length === 1 ? "result" : "results"}${result.hits.length === Number(limit) ? ` (first ${limit}; narrow the query or filters for more specific results)` : ""}, ${result.searchMode ?? "search"} mode${result.timing ? `, ${result.timing.totalMs} ms` : ""}.</p>${q ? html`<ul class="hits">${result.hits.map(hit)}</ul>` : html`<div class="asset-inventory">${[...groups].sort(([a], [b]) => a.localeCompare(b)).map(([name, hits]) => html`<details open><summary>${name} <span class="muted">(${hits.length})</span></summary><ul class="hits">${hits.sort((a, b) => a.ref.localeCompare(b.ref)).map(hit)}</ul></details>`)}</div>`}`
|
|
61
81
|
}`;
|
|
62
82
|
}
|
|
@@ -372,6 +372,21 @@ p {
|
|
|
372
372
|
padding-left: 20px;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
/* Unsaved-draft guard: floats over the page so the editor underneath stays mounted. */
|
|
376
|
+
#unsaved {
|
|
377
|
+
position: fixed;
|
|
378
|
+
top: 16px;
|
|
379
|
+
right: 16px;
|
|
380
|
+
z-index: 10;
|
|
381
|
+
max-width: min(420px, calc(100vw - 32px));
|
|
382
|
+
margin: 0;
|
|
383
|
+
box-shadow: 0 4px 16px rgb(0 0 0 / 0.2);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#unsaved p {
|
|
387
|
+
margin: 0 0 8px;
|
|
388
|
+
}
|
|
389
|
+
|
|
375
390
|
/* ---- badges ---- */
|
|
376
391
|
.badge {
|
|
377
392
|
display: inline-block;
|
|
@@ -1017,6 +1032,8 @@ pre.diff {
|
|
|
1017
1032
|
}
|
|
1018
1033
|
|
|
1019
1034
|
input[type="search"],
|
|
1035
|
+
input:not([type]),
|
|
1036
|
+
textarea,
|
|
1020
1037
|
select,
|
|
1021
1038
|
button {
|
|
1022
1039
|
font: inherit;
|
|
@@ -1027,6 +1044,59 @@ button {
|
|
|
1027
1044
|
padding: 8px 12px;
|
|
1028
1045
|
}
|
|
1029
1046
|
|
|
1047
|
+
.form-grid {
|
|
1048
|
+
display: grid;
|
|
1049
|
+
gap: 12px;
|
|
1050
|
+
margin: 16px 0;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.form-grid label:not(.check) {
|
|
1054
|
+
display: grid;
|
|
1055
|
+
gap: 4px;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.form-grid input:not([type="checkbox"]),
|
|
1059
|
+
.form-grid textarea {
|
|
1060
|
+
width: 100%;
|
|
1061
|
+
min-width: 0;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
.form-grid textarea {
|
|
1065
|
+
resize: vertical;
|
|
1066
|
+
font-family: var(--mono);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.form-grid button {
|
|
1070
|
+
justify-self: start;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
.check {
|
|
1074
|
+
display: flex;
|
|
1075
|
+
gap: 8px;
|
|
1076
|
+
align-items: baseline;
|
|
1077
|
+
flex-wrap: wrap;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.scope-controls {
|
|
1081
|
+
flex-basis: 100%;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
.asset-inventory > details {
|
|
1085
|
+
margin-top: 16px;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
.md [id] {
|
|
1089
|
+
scroll-margin-top: 20px;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
.task-item {
|
|
1093
|
+
list-style: none;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
.task-item input {
|
|
1097
|
+
margin-right: 8px;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1030
1100
|
input[type="search"] {
|
|
1031
1101
|
flex: 1 1 260px;
|
|
1032
1102
|
min-width: 0;
|
|
@@ -108,7 +108,15 @@ export function emptyImproveMetrics() {
|
|
|
108
108
|
totalChunks: 0,
|
|
109
109
|
durationMs: 0,
|
|
110
110
|
},
|
|
111
|
-
memoryInference: {
|
|
111
|
+
memoryInference: {
|
|
112
|
+
considered: 0,
|
|
113
|
+
freshAttempts: 0,
|
|
114
|
+
written: 0,
|
|
115
|
+
noChange: 0,
|
|
116
|
+
skippedNoFacts: 0,
|
|
117
|
+
yieldRate: 0,
|
|
118
|
+
durationMs: 0,
|
|
119
|
+
},
|
|
112
120
|
wallTime: { medianMs: 0, p95Ms: 0 },
|
|
113
121
|
coverage: { acceptedProposals: 0, distinctRefs: 0 },
|
|
114
122
|
};
|
|
@@ -220,6 +228,7 @@ function projectRunMetrics(result) {
|
|
|
220
228
|
// them keeps the yield rate a statement about real inference attempts.
|
|
221
229
|
mi.freshAttempts += Math.max(0, considered - toFiniteNumber(memoryInference.cacheHits) - toFiniteNumber(memoryInference.skippedAborted));
|
|
222
230
|
mi.written += toFiniteNumber(memoryInference.writtenFacts);
|
|
231
|
+
mi.noChange += toFiniteNumber(memoryInference.noChange);
|
|
223
232
|
mi.skippedNoFacts += toFiniteNumber(memoryInference.skippedNoFacts);
|
|
224
233
|
}
|
|
225
234
|
metrics.memoryInference.durationMs += toFiniteNumber(result.memoryInferenceDurationMs);
|
|
@@ -267,6 +276,7 @@ function mergeImproveMetrics(dst, src) {
|
|
|
267
276
|
dst.memoryInference.considered += src.memoryInference.considered;
|
|
268
277
|
dst.memoryInference.freshAttempts += src.memoryInference.freshAttempts;
|
|
269
278
|
dst.memoryInference.written += src.memoryInference.written;
|
|
279
|
+
dst.memoryInference.noChange += src.memoryInference.noChange;
|
|
270
280
|
dst.memoryInference.skippedNoFacts += src.memoryInference.skippedNoFacts;
|
|
271
281
|
dst.memoryInference.durationMs += src.memoryInference.durationMs;
|
|
272
282
|
}
|
|
@@ -20,7 +20,7 @@ function renderTable(headers, rows) {
|
|
|
20
20
|
*
|
|
21
21
|
* Columns: ts | ok | actions | refl_ok/fail/cd/skip |
|
|
22
22
|
* distill_q/llm-fail/qrej/cfg/skip | cons_proc/promo/merge/del |
|
|
23
|
-
* mem_cons/written/skip | orphans | lint_f/fl
|
|
23
|
+
* mem_cons/written/nochange/skip | orphans | lint_f/fl
|
|
24
24
|
*/
|
|
25
25
|
export function renderRunsDetailMd(runs) {
|
|
26
26
|
const headers = [
|
|
@@ -31,7 +31,7 @@ export function renderRunsDetailMd(runs) {
|
|
|
31
31
|
"refl_ok/fail/cd/skip",
|
|
32
32
|
"distill_q/llm-fail/judge/validator/cfg/skip",
|
|
33
33
|
"cons_proc/promo/merge/del",
|
|
34
|
-
"mem_cons/written/skip",
|
|
34
|
+
"mem_cons/written/nochange/skip",
|
|
35
35
|
"orphans",
|
|
36
36
|
"lint_f/fl",
|
|
37
37
|
"result_status",
|
|
@@ -58,7 +58,7 @@ export function renderRunsDetailMd(runs) {
|
|
|
58
58
|
`${r.actions.reflect.ok}/${r.actions.reflect.failed}/${r.actions.reflect.cooldown}/${r.actions.reflect.skipped}`,
|
|
59
59
|
`${r.actions.distill.queued}/${r.actions.distill.llmFailed}/${r.actions.distill.judgeRejected}/${r.actions.distill.validatorRejected}/${r.actions.distill.configDisabled}/${r.actions.distill.skipped}`,
|
|
60
60
|
`${r.consolidation.processed}/${r.consolidation.promoted}/${r.consolidation.merged}/${r.consolidation.deleted}`,
|
|
61
|
-
`${r.memoryInference.considered}/${r.memoryInference.written}/${r.memoryInference.skippedNoFacts}`,
|
|
61
|
+
`${r.memoryInference.considered}/${r.memoryInference.written}/${r.memoryInference.noChange}/${r.memoryInference.skippedNoFacts}`,
|
|
62
62
|
String(r.orphansPurged),
|
|
63
63
|
`${r.lintFixed}/${r.lintFlagged}`,
|
|
64
64
|
r.resultStatus ?? "valid",
|
package/dist/commands/health.js
CHANGED
|
@@ -580,6 +580,7 @@ export async function akmHealth(options = {}) {
|
|
|
580
580
|
}
|
|
581
581
|
const { db } = opened;
|
|
582
582
|
try {
|
|
583
|
+
const { windowResults, deltas } = resolveWindowComparePhase(options, db, stateDbPath, now);
|
|
583
584
|
// Network probes overlap the local database phases below; awaited where consumed.
|
|
584
585
|
const engineProbesPromise = runHealthEngineProbes({ probeReachable: options.probe ? probeLlmEndpoint : undefined });
|
|
585
586
|
engineProbesPromise.catch(() => undefined);
|
|
@@ -666,8 +667,6 @@ export async function akmHealth(options = {}) {
|
|
|
666
667
|
const hardFailure = hardChecks.some((check) => check.status === "fail");
|
|
667
668
|
const deterministicWarnings = [...hardChecks, ...advisories].some((check) => check.status === "warn" && check.kind === "deterministic");
|
|
668
669
|
const status = hardFailure ? "fail" : deterministicWarnings ? "warn" : "pass";
|
|
669
|
-
// ── Window-compare mode (Phase 3) ─────────────────────────────────────
|
|
670
|
-
const { windowResults, deltas } = resolveWindowComparePhase(options, db, stateDbPath, now);
|
|
671
670
|
// ── Per-run mode (Phase 2) ────────────────────────────────────────────
|
|
672
671
|
const runs = options.groupBy === "run" ? perRunSummaries : undefined;
|
|
673
672
|
return {
|