akm-cli 0.9.0-beta.27 → 0.9.0-beta.28
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 +18 -0
- package/dist/cli.js +2 -0
- package/dist/commands/agent/contribute-cli.js +20 -0
- package/dist/commands/fact/fact-cli.js +155 -0
- package/dist/commands/fact/fact-context.js +149 -0
- package/dist/commands/read/knowledge.js +7 -4
- package/dist/output/shapes/passthrough.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.0-beta.28] — 2026-06-20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **#630 — `fact` asset type phase 2: pinned-core assembly + `akm fact` CLI.**
|
|
14
|
+
Phase 1 (#629) added the `fact` type; phase 2 delivers the "pinned core +
|
|
15
|
+
just-in-time retrieval" model. `collectPinnedFacts()` reads `fact` entries from
|
|
16
|
+
the index (pre-filtered on the `pinned` search hint), excludes
|
|
17
|
+
`stale`/`superseded`/`archived` facts from the core (they stay searchable), and
|
|
18
|
+
**fails soft** — a missing index or unreadable file yields an empty list, so
|
|
19
|
+
collection never blocks a dispatch. `buildPinnedFactsBlock()` assembles a
|
|
20
|
+
deterministic, category-grouped `## Stash facts` block, and `akm agent`
|
|
21
|
+
prepends it to the system prompt for user-facing runs (only when a task or agent
|
|
22
|
+
asset is present; bare interactive launches stay clean; opt out with
|
|
23
|
+
`--no-facts`). New `akm fact` CLI: `add <name> [body] --category <c> [--pinned]`
|
|
24
|
+
(hot-capture à la `akm remember`), `list [--category] [--pinned]`, and `context`
|
|
25
|
+
(print the assembled pinned core). Design note: `docs/design/fact-asset-type.md`.
|
|
26
|
+
|
|
9
27
|
## [0.9.0-beta.27] — 2026-06-20
|
|
10
28
|
|
|
11
29
|
All new behavior is **opt-in / default-preserving** — default runs are byte-identical.
|
package/dist/cli.js
CHANGED
|
@@ -63,6 +63,7 @@ import { generateBashCompletions, installBashCompletions } from "./commands/comp
|
|
|
63
63
|
import { configCommand } from "./commands/config-cli.js";
|
|
64
64
|
import { envCommand } from "./commands/env/env-cli.js";
|
|
65
65
|
import { secretCommand } from "./commands/env/secret-cli.js";
|
|
66
|
+
import { factCommand } from "./commands/fact/fact-cli.js";
|
|
66
67
|
import { feedbackCommand } from "./commands/feedback-cli.js";
|
|
67
68
|
import { graphCommand } from "./commands/graph/graph-cli.js";
|
|
68
69
|
import { akmHealth, parseWindowSpec, renderRunsDetailMd, renderWindowCompareMd, } from "./commands/health.js";
|
|
@@ -519,6 +520,7 @@ export const main = defineCommand({
|
|
|
519
520
|
secret: secretCommand,
|
|
520
521
|
wiki: wikiCommand,
|
|
521
522
|
tasks: tasksCommand,
|
|
523
|
+
fact: factCommand,
|
|
522
524
|
},
|
|
523
525
|
});
|
|
524
526
|
// ── Exit codes ──────────────────────────────────────────────────────────────
|
|
@@ -29,6 +29,7 @@ import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "
|
|
|
29
29
|
import { loadConfig } from "../../core/config/config.js";
|
|
30
30
|
import { UsageError } from "../../core/errors.js";
|
|
31
31
|
import { getHyphenatedArg } from "../../output/context.js";
|
|
32
|
+
import { buildPinnedFactsContext } from "../fact/fact-context.js";
|
|
32
33
|
import { akmLint } from "../lint.js";
|
|
33
34
|
import { akmPropose } from "../proposal/propose.js";
|
|
34
35
|
import { akmAgentDispatch } from "./agent-dispatch.js";
|
|
@@ -57,6 +58,11 @@ export const agentCommand = defineCommand({
|
|
|
57
58
|
description: "Model override — accepts aliases (opus, sonnet, haiku) or exact platform model IDs. Overrides the model specified in the agent asset.",
|
|
58
59
|
},
|
|
59
60
|
"timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds" },
|
|
61
|
+
"no-facts": {
|
|
62
|
+
type: "boolean",
|
|
63
|
+
description: "Do not prepend the pinned stash facts (fact: assets with `pinned: true`) to the system prompt",
|
|
64
|
+
default: false,
|
|
65
|
+
},
|
|
60
66
|
},
|
|
61
67
|
async run({ args }) {
|
|
62
68
|
await runWithJsonErrors(async () => {
|
|
@@ -85,6 +91,20 @@ export const agentCommand = defineCommand({
|
|
|
85
91
|
const promptText = getStringArg(args, "prompt");
|
|
86
92
|
const commandRef = getStringArg(args, "command");
|
|
87
93
|
const workflowRef = getStringArg(args, "workflow");
|
|
94
|
+
// Phase 2: prepend the pinned-fact "core" (fact assets with
|
|
95
|
+
// `pinned: true`) to the system prompt for user-facing agent runs, so
|
|
96
|
+
// durable stash facts and conventions are always in context. Only injects
|
|
97
|
+
// when there is actually a task or agent asset — bare interactive launches
|
|
98
|
+
// (`akm agent claude`) stay clean. Opt out with --no-facts. Fails soft:
|
|
99
|
+
// buildPinnedFactsContext never throws, so fact collection can't block a
|
|
100
|
+
// dispatch.
|
|
101
|
+
if (getHyphenatedArg(args, "no-facts") !== true &&
|
|
102
|
+
(promptText ?? commandRef ?? workflowRef ?? agentRef)) {
|
|
103
|
+
const factsBlock = buildPinnedFactsContext();
|
|
104
|
+
if (factsBlock) {
|
|
105
|
+
systemPrompt = systemPrompt ? `${factsBlock}\n\n${systemPrompt}` : factsBlock;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
88
108
|
// Only build a dispatch request when there is something to dispatch — a
|
|
89
109
|
// prompt, an agent asset, or a model override. When none of these are
|
|
90
110
|
// present the agent is launched interactively (no injected prompt, no
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* `akm fact` — manage durable stash-level facts (the `fact` asset type, phase 2).
|
|
6
|
+
*
|
|
7
|
+
* Subcommands:
|
|
8
|
+
* - `add <name> [content]` — hot-capture a fact (à la `akm remember`), writing
|
|
9
|
+
* `facts/<category>/<name>.md` with `category` + optional `pinned`.
|
|
10
|
+
* - `list` — list indexed facts, optionally filtered by `--category` / `--pinned`.
|
|
11
|
+
* - `context` — print the assembled "pinned core" block (the same block the
|
|
12
|
+
* `akm agent` dispatch prepends to a system prompt). Useful for previewing
|
|
13
|
+
* or piping into an AGENTS.md / CLAUDE.md.
|
|
14
|
+
*/
|
|
15
|
+
import { defineCommand } from "citty";
|
|
16
|
+
import { defineJsonCommand, output } from "../../cli/shared.js";
|
|
17
|
+
import { serializeFrontmatter } from "../../core/asset/asset-serialize.js";
|
|
18
|
+
import { toErrorMessage, tryReadStdinText } from "../../core/common.js";
|
|
19
|
+
import { UsageError } from "../../core/errors.js";
|
|
20
|
+
import { appendEvent } from "../../core/events.js";
|
|
21
|
+
import { closeDatabase, getAllEntries, openExistingDatabase } from "../../indexer/db/db.js";
|
|
22
|
+
import { assertFlatAssetName, writeMarkdownAsset } from "../read/knowledge.js";
|
|
23
|
+
import { buildPinnedFactsBlock, collectPinnedFacts } from "./fact-context.js";
|
|
24
|
+
/** Recommended categories (mirrors the fact linter). Free values are allowed but linted. */
|
|
25
|
+
const KNOWN_CATEGORIES = ["personal", "team", "project", "convention", "meta"];
|
|
26
|
+
function buildFactFrontmatter(fields) {
|
|
27
|
+
const obj = { category: fields.category };
|
|
28
|
+
if (fields.description?.trim())
|
|
29
|
+
obj.description = fields.description.trim();
|
|
30
|
+
if (fields.pinned)
|
|
31
|
+
obj.pinned = true;
|
|
32
|
+
obj.updated = new Date().toISOString().slice(0, 10);
|
|
33
|
+
return `---\n${serializeFrontmatter(obj)}\n---`;
|
|
34
|
+
}
|
|
35
|
+
const addCommand = defineJsonCommand({
|
|
36
|
+
meta: { name: "add", description: "Record a durable stash fact (facts/<category>/<name>.md)" },
|
|
37
|
+
args: {
|
|
38
|
+
name: {
|
|
39
|
+
type: "positional",
|
|
40
|
+
description: "Fact name (flat, no '/'; use --category for the subdirectory)",
|
|
41
|
+
required: false,
|
|
42
|
+
},
|
|
43
|
+
content: { type: "positional", description: "Fact body. Omit to read markdown from stdin.", required: false },
|
|
44
|
+
category: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: `Fact category — scopes the fact (one of: ${KNOWN_CATEGORIES.join(", ")}; free values allowed but linted)`,
|
|
47
|
+
},
|
|
48
|
+
pinned: { type: "boolean", description: "Mark this fact as always-injected core context", default: false },
|
|
49
|
+
description: { type: "string", description: "Short description written to frontmatter" },
|
|
50
|
+
path: { type: "string", description: "Subdirectory under facts/ (defaults to --category)" },
|
|
51
|
+
force: { type: "boolean", description: "Overwrite an existing fact with the same name", default: false },
|
|
52
|
+
target: { type: "string", description: "Override the write destination (a configured source name)" },
|
|
53
|
+
},
|
|
54
|
+
async run({ args }) {
|
|
55
|
+
if (!args.name || typeof args.name !== "string") {
|
|
56
|
+
throw new UsageError("Usage: akm fact add <name> [content] --category <category>", "MISSING_REQUIRED_ARGUMENT", "Provide a flat fact name; pass the body as a quoted argument or via stdin.");
|
|
57
|
+
}
|
|
58
|
+
assertFlatAssetName(args.name);
|
|
59
|
+
const category = typeof args.category === "string" ? args.category.trim() : "";
|
|
60
|
+
if (!category) {
|
|
61
|
+
throw new UsageError(`A --category is required (one of: ${KNOWN_CATEGORIES.join(", ")}).`, "MISSING_REQUIRED_ARGUMENT");
|
|
62
|
+
}
|
|
63
|
+
const body = (typeof args.content === "string" ? args.content : undefined) ?? tryReadStdinText();
|
|
64
|
+
if (!body?.trim()) {
|
|
65
|
+
throw new UsageError("Fact content is required. Pass quoted text or pipe markdown into stdin.");
|
|
66
|
+
}
|
|
67
|
+
const description = typeof args.description === "string" ? args.description : undefined;
|
|
68
|
+
const pinned = args.pinned === true;
|
|
69
|
+
const frontmatter = buildFactFrontmatter({ description, category, pinned });
|
|
70
|
+
const subPath = typeof args.path === "string" && args.path.trim() ? args.path.trim() : category;
|
|
71
|
+
const result = await writeMarkdownAsset({
|
|
72
|
+
type: "fact",
|
|
73
|
+
content: `${frontmatter}\n${body}`,
|
|
74
|
+
name: args.name,
|
|
75
|
+
fallbackPrefix: "fact",
|
|
76
|
+
force: args.force === true,
|
|
77
|
+
target: typeof args.target === "string" ? args.target : undefined,
|
|
78
|
+
path: subPath,
|
|
79
|
+
});
|
|
80
|
+
appendEvent({
|
|
81
|
+
eventType: "fact_add",
|
|
82
|
+
ref: result.ref,
|
|
83
|
+
metadata: { path: result.path, category, pinned, force: args.force === true },
|
|
84
|
+
});
|
|
85
|
+
output("fact-add", { ok: true, ...result, category, pinned });
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
/** Pull the `category:<x>` value out of an entry's search hints, if present. */
|
|
89
|
+
function categoryFromHints(hints) {
|
|
90
|
+
const hit = hints?.find((h) => h.startsWith("category:"));
|
|
91
|
+
return hit ? hit.slice("category:".length) : undefined;
|
|
92
|
+
}
|
|
93
|
+
const listCommand = defineJsonCommand({
|
|
94
|
+
meta: { name: "list", description: "List indexed facts (optionally filtered by --category / --pinned)" },
|
|
95
|
+
args: {
|
|
96
|
+
category: { type: "string", description: "Only list facts in this category" },
|
|
97
|
+
pinned: { type: "boolean", description: "Only list pinned (always-injected core) facts", default: false },
|
|
98
|
+
},
|
|
99
|
+
run({ args }) {
|
|
100
|
+
const categoryFilter = typeof args.category === "string" ? args.category.trim() : undefined;
|
|
101
|
+
const pinnedOnly = args.pinned === true;
|
|
102
|
+
let db;
|
|
103
|
+
let facts = [];
|
|
104
|
+
try {
|
|
105
|
+
db = openExistingDatabase();
|
|
106
|
+
facts = getAllEntries(db, "fact")
|
|
107
|
+
.map((row) => {
|
|
108
|
+
const category = categoryFromHints(row.entry.searchHints);
|
|
109
|
+
return {
|
|
110
|
+
ref: `fact:${row.entry.name}`,
|
|
111
|
+
name: row.entry.name,
|
|
112
|
+
...(category ? { category } : {}),
|
|
113
|
+
pinned: row.entry.searchHints?.includes("pinned") ?? false,
|
|
114
|
+
...(row.entry.description ? { description: row.entry.description } : {}),
|
|
115
|
+
};
|
|
116
|
+
})
|
|
117
|
+
.filter((f) => (categoryFilter ? f.category === categoryFilter : true))
|
|
118
|
+
.filter((f) => (pinnedOnly ? f.pinned : true))
|
|
119
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
// No index yet → empty list rather than a hard error.
|
|
123
|
+
void toErrorMessage(err);
|
|
124
|
+
facts = [];
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
if (db)
|
|
128
|
+
closeDatabase(db);
|
|
129
|
+
}
|
|
130
|
+
output("fact-list", { ok: true, facts, totalCount: facts.length });
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const contextCommand = defineJsonCommand({
|
|
134
|
+
meta: {
|
|
135
|
+
name: "context",
|
|
136
|
+
description: "Print the assembled pinned-fact core block (what `akm agent` injects into the system prompt)",
|
|
137
|
+
},
|
|
138
|
+
run() {
|
|
139
|
+
const facts = collectPinnedFacts();
|
|
140
|
+
output("fact-context", { ok: true, content: buildPinnedFactsBlock(facts), count: facts.length });
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
const factSubCommands = {
|
|
144
|
+
add: addCommand,
|
|
145
|
+
list: listCommand,
|
|
146
|
+
context: contextCommand,
|
|
147
|
+
};
|
|
148
|
+
export const FACT_SUBCOMMAND_SET = new Set(Object.keys(factSubCommands));
|
|
149
|
+
export const factCommand = defineCommand({
|
|
150
|
+
meta: {
|
|
151
|
+
name: "fact",
|
|
152
|
+
description: "Manage durable stash-level facts (the `fact` asset type): personal/team/project details, coding conventions, and stash-meta. Subcommands: add, list, context.",
|
|
153
|
+
},
|
|
154
|
+
subCommands: factSubCommands,
|
|
155
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Pinned-fact context assembly (fact asset type, phase 2).
|
|
6
|
+
*
|
|
7
|
+
* The `fact` type (see docs/design/fact-asset-type.md) stores durable
|
|
8
|
+
* stash-level semantic knowledge. Facts marked `pinned: true` form the small
|
|
9
|
+
* always-injected "core" — the handful of high-signal facts an agent should
|
|
10
|
+
* always have in context (personal identity, team conventions, the
|
|
11
|
+
* "constitution"). Everything else stays on "disk" and is reached via normal
|
|
12
|
+
* `akm search` / `akm curate` just-in-time retrieval.
|
|
13
|
+
*
|
|
14
|
+
* This module collects the pinned core from the index and assembles it into a
|
|
15
|
+
* compact markdown block that the `akm agent` dispatch prepends to the agent's
|
|
16
|
+
* system prompt. The pure assembly + parse helpers are exported so they can be
|
|
17
|
+
* unit-tested without a database.
|
|
18
|
+
*/
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
21
|
+
import { asNonEmptyString } from "../../core/common.js";
|
|
22
|
+
import { closeDatabase, getAllEntries, openExistingDatabase } from "../../indexer/db/db.js";
|
|
23
|
+
/**
|
|
24
|
+
* Fact `status` values that EXCLUDE a fact from the always-injected core.
|
|
25
|
+
* Durable facts go stale; rather than delete them, an author can mark a fact
|
|
26
|
+
* `status: stale` (or `superseded`) and it drops out of the pinned context
|
|
27
|
+
* while remaining searchable. (Design note phase 2 — staleness handling.)
|
|
28
|
+
*/
|
|
29
|
+
const EXCLUDED_STATUSES = new Set(["stale", "superseded", "archived"]);
|
|
30
|
+
/**
|
|
31
|
+
* Parse a single fact file into a {@link PinnedFact}, or return `null` when the
|
|
32
|
+
* fact is NOT part of the pinned core: not `pinned: true`, or carrying an
|
|
33
|
+
* excluded `status`. Pure — takes the raw file content so it is unit-testable
|
|
34
|
+
* without touching disk.
|
|
35
|
+
*/
|
|
36
|
+
export function parsePinnedFact(name, raw) {
|
|
37
|
+
const parsed = parseFrontmatter(raw);
|
|
38
|
+
const fm = parsed.data;
|
|
39
|
+
if (fm.pinned !== true)
|
|
40
|
+
return null;
|
|
41
|
+
const status = asNonEmptyString(fm.status)?.toLowerCase();
|
|
42
|
+
if (status && EXCLUDED_STATUSES.has(status))
|
|
43
|
+
return null;
|
|
44
|
+
const body = parsed.content.trim();
|
|
45
|
+
return {
|
|
46
|
+
ref: `fact:${name}`,
|
|
47
|
+
name,
|
|
48
|
+
...(asNonEmptyString(fm.category) ? { category: asNonEmptyString(fm.category) } : {}),
|
|
49
|
+
...(asNonEmptyString(fm.description) ? { description: asNonEmptyString(fm.description) } : {}),
|
|
50
|
+
body,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Render a list of pinned facts into a compact markdown context block, grouped
|
|
55
|
+
* by category. Returns `""` for an empty list so callers can treat "no facts"
|
|
56
|
+
* uniformly. Pure.
|
|
57
|
+
*/
|
|
58
|
+
export function buildPinnedFactsBlock(facts) {
|
|
59
|
+
if (facts.length === 0)
|
|
60
|
+
return "";
|
|
61
|
+
// Stable order: by category then name, so the block is deterministic.
|
|
62
|
+
const sorted = [...facts].sort((a, b) => {
|
|
63
|
+
const ca = a.category ?? "";
|
|
64
|
+
const cb = b.category ?? "";
|
|
65
|
+
if (ca !== cb)
|
|
66
|
+
return ca.localeCompare(cb);
|
|
67
|
+
return a.name.localeCompare(b.name);
|
|
68
|
+
});
|
|
69
|
+
const lines = [
|
|
70
|
+
"## Stash facts",
|
|
71
|
+
"",
|
|
72
|
+
"Durable, authoritative facts about this user / team / project. Treat them as background context and follow any conventions they state.",
|
|
73
|
+
"",
|
|
74
|
+
];
|
|
75
|
+
let lastCategory;
|
|
76
|
+
for (const fact of sorted) {
|
|
77
|
+
const category = fact.category ?? "general";
|
|
78
|
+
if (category !== lastCategory) {
|
|
79
|
+
lines.push(`### ${category}`, "");
|
|
80
|
+
lastCategory = category;
|
|
81
|
+
}
|
|
82
|
+
const heading = fact.description ? `${fact.name} — ${fact.description}` : fact.name;
|
|
83
|
+
lines.push(`- **${heading}**`);
|
|
84
|
+
if (fact.body) {
|
|
85
|
+
// Indent the body two spaces so it reads as part of the bullet.
|
|
86
|
+
for (const bodyLine of fact.body.split("\n")) {
|
|
87
|
+
lines.push(bodyLine.length > 0 ? ` ${bodyLine}` : "");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
lines.push("");
|
|
91
|
+
}
|
|
92
|
+
return lines.join("\n").trimEnd();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Collect the pinned core from the index. Opens the existing index DB (unless
|
|
96
|
+
* one is injected for tests), reads every `fact` entry whose `pinned` search
|
|
97
|
+
* hint is set, then parses each file from disk. Fails soft: any error (missing
|
|
98
|
+
* index, unreadable file) yields an empty list rather than throwing, so callers
|
|
99
|
+
* on the hot path (agent dispatch) are never broken by fact collection.
|
|
100
|
+
*/
|
|
101
|
+
export function collectPinnedFacts(injectedDb) {
|
|
102
|
+
let db;
|
|
103
|
+
let ownsDb = false;
|
|
104
|
+
try {
|
|
105
|
+
db = injectedDb ?? openExistingDatabase();
|
|
106
|
+
ownsDb = injectedDb === undefined;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const entries = getAllEntries(db, "fact");
|
|
113
|
+
const facts = [];
|
|
114
|
+
for (const row of entries) {
|
|
115
|
+
// Fast pre-filter on the indexed `pinned` search hint so we only read
|
|
116
|
+
// files for facts that are actually part of the core.
|
|
117
|
+
if (!row.entry.searchHints?.includes("pinned"))
|
|
118
|
+
continue;
|
|
119
|
+
let raw;
|
|
120
|
+
try {
|
|
121
|
+
raw = fs.readFileSync(row.filePath, "utf8");
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const fact = parsePinnedFact(row.entry.name, raw);
|
|
127
|
+
if (fact)
|
|
128
|
+
facts.push(fact);
|
|
129
|
+
}
|
|
130
|
+
return facts;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
if (ownsDb && db)
|
|
137
|
+
closeDatabase(db);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Convenience for the agent-dispatch seam: collect the pinned core and return
|
|
142
|
+
* the assembled markdown block, or `undefined` when there are no pinned facts.
|
|
143
|
+
*/
|
|
144
|
+
export function buildPinnedFactsContext(injectedDb) {
|
|
145
|
+
const facts = collectPinnedFacts(injectedDb);
|
|
146
|
+
if (facts.length === 0)
|
|
147
|
+
return undefined;
|
|
148
|
+
return buildPinnedFactsBlock(facts);
|
|
149
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import fs from "node:fs";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../../core/asset/asset-create.js";
|
|
14
|
-
import { resolveAssetPathFromName } from "../../core/asset/asset-spec.js";
|
|
14
|
+
import { resolveAssetPathFromName, TYPE_DIRS } from "../../core/asset/asset-spec.js";
|
|
15
15
|
import { isHttpUrl, isWithin, tryReadStdinText } from "../../core/common.js";
|
|
16
16
|
import { loadConfig } from "../../core/config/config.js";
|
|
17
17
|
import { UsageError } from "../../core/errors.js";
|
|
@@ -109,7 +109,9 @@ export async function readKnowledgeInput(source) {
|
|
|
109
109
|
}
|
|
110
110
|
// ── Asset writing ────────────────────────────────────────────────────────────
|
|
111
111
|
/**
|
|
112
|
-
* Write a markdown asset (knowledge or
|
|
112
|
+
* Write a markdown asset (knowledge, memory, or fact) to the resolved write
|
|
113
|
+
* target. The type's stash subdirectory is resolved from the asset registry
|
|
114
|
+
* (`TYPE_DIRS`), so adding a markdown type here needs no further changes.
|
|
113
115
|
*
|
|
114
116
|
* Resolves the write target via the v1 precedence chain (`--target` →
|
|
115
117
|
* `defaultWriteTarget` → working stash), validates the path is within the
|
|
@@ -120,7 +122,7 @@ export async function writeMarkdownAsset(options) {
|
|
|
120
122
|
const cfg = loadConfig();
|
|
121
123
|
const target = resolveWriteTarget(cfg, options.target);
|
|
122
124
|
const { source, config } = target;
|
|
123
|
-
const typeRoot = path.join(source.path, options.type
|
|
125
|
+
const typeRoot = path.join(source.path, TYPE_DIRS[options.type] ?? options.type);
|
|
124
126
|
// `--name` is the flat asset name; `--path` is the subdirectory under the
|
|
125
127
|
// type root. Combine them into the nested name the path resolver expects.
|
|
126
128
|
const subPath = normalizeCreateSubPath(options.path);
|
|
@@ -134,7 +136,8 @@ export async function writeMarkdownAsset(options) {
|
|
|
134
136
|
throw new UsageError(`Resolved ${options.type} path escapes the stash: "${normalizedName}"`);
|
|
135
137
|
}
|
|
136
138
|
if (fs.existsSync(assetPath) && !options.force) {
|
|
137
|
-
|
|
139
|
+
const label = `${options.type.charAt(0).toUpperCase()}${options.type.slice(1)}`;
|
|
140
|
+
throw new UsageError(`${label} "${normalizedName}" already exists. Re-run with --force to overwrite it.`, "RESOURCE_ALREADY_EXISTS");
|
|
138
141
|
}
|
|
139
142
|
const ref = { type: options.type, name: normalizedName };
|
|
140
143
|
const result = await writeAssetToSource(source, config, ref, options.content);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|