akm-cli 0.7.4 → 0.7.5
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 → .github/CHANGELOG.md} +33 -0
- package/.github/LICENSE +374 -0
- package/dist/cli.js +241 -170
- package/dist/commands/curate.js +1 -0
- package/dist/commands/distill.js +14 -4
- package/dist/commands/events.js +10 -1
- package/dist/commands/migration-help.js +2 -2
- package/dist/commands/propose.js +36 -16
- package/dist/commands/reflect.js +40 -14
- package/dist/commands/remember.js +1 -1
- package/dist/commands/show.js +19 -44
- package/dist/commands/vault.js +5 -10
- package/dist/core/asset-registry.js +1 -1
- package/dist/core/asset-spec.js +1 -1
- package/dist/core/config.js +13 -0
- package/dist/core/events.js +19 -2
- package/dist/indexer/db-search.js +35 -235
- package/dist/indexer/db.js +15 -5
- package/dist/indexer/ensure-index.js +72 -0
- package/dist/indexer/graph-extraction.js +10 -0
- package/dist/indexer/indexer.js +38 -22
- package/dist/integrations/agent/prompts.js +95 -15
- package/dist/integrations/agent/spawn.js +65 -12
- package/dist/llm/client.js +40 -2
- package/dist/llm/graph-extract.js +2 -4
- package/dist/llm/memory-infer.js +7 -4
- package/dist/output/cli-hints.js +17 -8
- package/dist/output/renderers.js +6 -1
- package/dist/output/shapes.js +8 -3
- package/dist/output/text.js +18 -19
- package/dist/sources/providers/git.js +43 -1
- package/dist/workflows/db.js +9 -0
- package/dist/workflows/runs.js +25 -8
- package/dist/workflows/scope-key.js +76 -0
- package/docs/migration/release-notes/0.7.4.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +20 -0
- package/package.json +2 -2
package/dist/commands/distill.js
CHANGED
|
@@ -152,7 +152,12 @@ export async function akmDistill(options) {
|
|
|
152
152
|
catch {
|
|
153
153
|
assetContent = null;
|
|
154
154
|
}
|
|
155
|
-
const { events } = readEventsImpl({
|
|
155
|
+
const { events } = readEventsImpl({
|
|
156
|
+
ref: inputRef,
|
|
157
|
+
type: "feedback",
|
|
158
|
+
excludeTags: options.excludeTags,
|
|
159
|
+
includeTags: options.includeTags,
|
|
160
|
+
});
|
|
156
161
|
// #267 — feedback exclusion. Filter events whose `ref` matches the
|
|
157
162
|
// exclusion list BEFORE the prompt is built. The original event stream
|
|
158
163
|
// is never mutated; only the `feedback` slice that reaches the LLM is
|
|
@@ -274,10 +279,15 @@ async function defaultLookup(ref) {
|
|
|
274
279
|
}
|
|
275
280
|
/** Best-effort fence stripping. Keeps the body intact when no fence is present. */
|
|
276
281
|
function stripMarkdownFences(raw) {
|
|
277
|
-
|
|
282
|
+
// Strip <think>…</think> reasoning blocks first — local LLMs (e.g. Qwen3)
|
|
283
|
+
// emit these before the content, which breaks YAML frontmatter detection.
|
|
284
|
+
const stripped = raw
|
|
285
|
+
.trim()
|
|
286
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
287
|
+
.trim();
|
|
278
288
|
// Only strip outer triple-fence pairs — leave inner code blocks alone.
|
|
279
|
-
const fence =
|
|
289
|
+
const fence = stripped.match(/^```(?:markdown|md)?\s*\n([\s\S]*?)\n```\s*$/i);
|
|
280
290
|
if (fence)
|
|
281
291
|
return fence[1].trim();
|
|
282
|
-
return
|
|
292
|
+
return stripped;
|
|
283
293
|
}
|
package/dist/commands/events.js
CHANGED
|
@@ -67,7 +67,14 @@ function normalizeSince(since) {
|
|
|
67
67
|
export function akmEventsList(options = {}) {
|
|
68
68
|
const ref = validateRef(options.ref);
|
|
69
69
|
const parsed = parseSinceFlag(options.since);
|
|
70
|
-
const result = readEvents({
|
|
70
|
+
const result = readEvents({
|
|
71
|
+
since: parsed.since,
|
|
72
|
+
sinceOffset: parsed.sinceOffset,
|
|
73
|
+
type: options.type,
|
|
74
|
+
ref,
|
|
75
|
+
excludeTags: options.excludeTags,
|
|
76
|
+
includeTags: options.includeTags,
|
|
77
|
+
}, options.ctx);
|
|
71
78
|
return {
|
|
72
79
|
schemaVersion: 1,
|
|
73
80
|
totalCount: result.events.length,
|
|
@@ -92,6 +99,8 @@ export async function akmEventsTail(options = {}) {
|
|
|
92
99
|
maxEvents: options.maxEvents,
|
|
93
100
|
signal: options.signal,
|
|
94
101
|
onEvent: options.onEvent,
|
|
102
|
+
excludeTags: options.excludeTags,
|
|
103
|
+
includeTags: options.includeTags,
|
|
95
104
|
};
|
|
96
105
|
const result = await tailEvents(tailOptions, options.ctx);
|
|
97
106
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main/CHANGELOG.md";
|
|
3
|
+
const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main/.github/CHANGELOG.md";
|
|
4
4
|
const MIGRATION_DOC_URL = "https://github.com/itlackey/akm/blob/main/docs/migration/v0.5-to-v0.6.md";
|
|
5
5
|
/**
|
|
6
6
|
* Directory containing per-version release notes. Resolved relative to
|
|
@@ -14,7 +14,7 @@ function releaseNotesDir() {
|
|
|
14
14
|
}
|
|
15
15
|
function loadChangelog() {
|
|
16
16
|
try {
|
|
17
|
-
const changelogPath = path.resolve(import.meta.dir, "
|
|
17
|
+
const changelogPath = path.resolve(import.meta.dir, "../../.github/CHANGELOG.md");
|
|
18
18
|
if (fs.existsSync(changelogPath)) {
|
|
19
19
|
return fs.readFileSync(changelogPath, "utf8");
|
|
20
20
|
}
|
package/dist/commands/propose.js
CHANGED
|
@@ -78,14 +78,21 @@ export async function akmPropose(options) {
|
|
|
78
78
|
throw err;
|
|
79
79
|
}
|
|
80
80
|
// 3. Build prompt.
|
|
81
|
+
// Synthesize a temp draft path so opencode can write the asset content
|
|
82
|
+
// directly using its file tools rather than returning JSON via stdout.
|
|
83
|
+
const draftFilePath = import("node:os").then((os) => import("node:path").then((path) => path.join(os.tmpdir(), `akm-propose-${options.type}-${options.name.replace(/[^a-z0-9_-]/gi, "_")}-${Date.now()}.md`)));
|
|
84
|
+
const resolvedDraftPath = await draftFilePath;
|
|
81
85
|
const prompt = buildProposePrompt({
|
|
82
86
|
type: options.type,
|
|
83
87
|
name: options.name,
|
|
84
88
|
task: options.task,
|
|
89
|
+
draftFilePath: resolvedDraftPath,
|
|
85
90
|
});
|
|
86
91
|
// 4. Spawn the agent.
|
|
92
|
+
// Real agent runs use interactive mode so file tools can write the draft.
|
|
93
|
+
// Injected/custom spawns still need captured stdout for JSON payload tests.
|
|
87
94
|
const runOptions = {
|
|
88
|
-
stdio: "captured",
|
|
95
|
+
stdio: options.runAgentOptions?.spawn ? "captured" : "interactive",
|
|
89
96
|
parseOutput: "text",
|
|
90
97
|
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
91
98
|
...(options.runAgentOptions ?? {}),
|
|
@@ -94,24 +101,37 @@ export async function akmPropose(options) {
|
|
|
94
101
|
if (!result.ok) {
|
|
95
102
|
return failureEnvelope(result, options.type, options.name);
|
|
96
103
|
}
|
|
97
|
-
// 5.
|
|
104
|
+
// 5. Resolve the proposal content.
|
|
105
|
+
// Path A: opencode wrote the draft file — read it directly (no stdout parse).
|
|
106
|
+
// Path B: fallback to stdout JSON parse for non-file-writing agents.
|
|
107
|
+
const fs = await import("node:fs");
|
|
98
108
|
let payload;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
ok: false,
|
|
106
|
-
reason: "parse_error",
|
|
107
|
-
error: err instanceof Error ? err.message : String(err),
|
|
108
|
-
type: options.type,
|
|
109
|
-
name: options.name,
|
|
110
|
-
exitCode: result.exitCode,
|
|
111
|
-
stdout: result.stdout,
|
|
112
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
109
|
+
if (fs.existsSync(resolvedDraftPath)) {
|
|
110
|
+
const draftContent = fs.readFileSync(resolvedDraftPath, "utf8");
|
|
111
|
+
fs.unlinkSync(resolvedDraftPath);
|
|
112
|
+
payload = {
|
|
113
|
+
ref: `${options.type}:${options.name}`,
|
|
114
|
+
content: draftContent,
|
|
113
115
|
};
|
|
114
116
|
}
|
|
117
|
+
else {
|
|
118
|
+
try {
|
|
119
|
+
payload = parseAgentProposalPayload(result.stdout ?? "");
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return {
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
ok: false,
|
|
125
|
+
reason: "parse_error",
|
|
126
|
+
error: err instanceof Error ? err.message : String(err),
|
|
127
|
+
type: options.type,
|
|
128
|
+
name: options.name,
|
|
129
|
+
exitCode: result.exitCode,
|
|
130
|
+
stdout: result.stdout,
|
|
131
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
115
135
|
// 6. Insert the proposal. Note: we allow the agent's `ref` to normalise the
|
|
116
136
|
// asset name (e.g. path-cleanup), but only after validating that the ref is
|
|
117
137
|
// well-formed and the type still matches the requested type.
|
package/dist/commands/reflect.js
CHANGED
|
@@ -68,6 +68,24 @@ function buildSchemaHints(type, content) {
|
|
|
68
68
|
const report = lintLessonContent(content, "reflect");
|
|
69
69
|
return report.findings.map((f) => `[${f.kind}] ${f.message}`);
|
|
70
70
|
}
|
|
71
|
+
function fallbackPayloadFromRawContent(stdout, ref) {
|
|
72
|
+
if (!ref)
|
|
73
|
+
return undefined;
|
|
74
|
+
const trimmed = stripMarkdownFence(stdout).trim();
|
|
75
|
+
if (!trimmed)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (!looksLikeAssetContent(trimmed))
|
|
78
|
+
return undefined;
|
|
79
|
+
return { ref, content: trimmed };
|
|
80
|
+
}
|
|
81
|
+
function stripMarkdownFence(stdout) {
|
|
82
|
+
const trimmed = stdout.trim();
|
|
83
|
+
const match = trimmed.match(/^```(?:markdown|md)?\s*\n([\s\S]*?)\n```$/i);
|
|
84
|
+
return match?.[1] ?? trimmed;
|
|
85
|
+
}
|
|
86
|
+
function looksLikeAssetContent(value) {
|
|
87
|
+
return value.startsWith("#") || value.startsWith("---");
|
|
88
|
+
}
|
|
71
89
|
function loadAgentConfigFromDisk() {
|
|
72
90
|
const config = loadConfig();
|
|
73
91
|
return parseAgentConfig(config.agent);
|
|
@@ -130,6 +148,9 @@ export async function akmReflect(options = {}) {
|
|
|
130
148
|
throw err;
|
|
131
149
|
}
|
|
132
150
|
// 4. Build the prompt.
|
|
151
|
+
// Keep reflect on the same captured JSON path the bench harness already
|
|
152
|
+
// uses successfully. The draft-file interactive path proved brittle with
|
|
153
|
+
// local opencode models and caused proposal generation failures.
|
|
133
154
|
const feedback = readRecentFeedback(options.ref);
|
|
134
155
|
const schemaHints = buildSchemaHints(parsedRef?.type ?? "", assetContent);
|
|
135
156
|
const prompt = buildReflectPrompt({
|
|
@@ -141,8 +162,7 @@ export async function akmReflect(options = {}) {
|
|
|
141
162
|
...(schemaHints.length > 0 ? { schemaHints } : {}),
|
|
142
163
|
...(options.task ? { task: options.task } : {}),
|
|
143
164
|
});
|
|
144
|
-
// 5. Spawn the agent.
|
|
145
|
-
// the structured payload without confusing terminal control codes.
|
|
165
|
+
// 5. Spawn the agent.
|
|
146
166
|
const runOptions = {
|
|
147
167
|
stdio: "captured",
|
|
148
168
|
parseOutput: "text",
|
|
@@ -153,22 +173,28 @@ export async function akmReflect(options = {}) {
|
|
|
153
173
|
if (!result.ok) {
|
|
154
174
|
return failureEnvelope(result, options.ref);
|
|
155
175
|
}
|
|
156
|
-
// 6.
|
|
176
|
+
// 6. Resolve the proposal content from stdout JSON.
|
|
157
177
|
let payload;
|
|
158
178
|
try {
|
|
159
|
-
payload = parseAgentProposalPayload(result.stdout);
|
|
179
|
+
payload = parseAgentProposalPayload(result.stdout ?? "");
|
|
160
180
|
}
|
|
161
181
|
catch (err) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
182
|
+
const fallback = fallbackPayloadFromRawContent(result.stdout ?? "", options.ref);
|
|
183
|
+
if (fallback) {
|
|
184
|
+
payload = fallback;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
return {
|
|
188
|
+
schemaVersion: 1,
|
|
189
|
+
ok: false,
|
|
190
|
+
reason: "parse_error",
|
|
191
|
+
error: err instanceof Error ? err.message : String(err),
|
|
192
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
193
|
+
exitCode: result.exitCode,
|
|
194
|
+
stdout: result.stdout,
|
|
195
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
172
198
|
}
|
|
173
199
|
// 7. Create the proposal. The proposal queue is the ONLY thing reflect
|
|
174
200
|
// writes — promotion to a real asset is gated by `akm proposal accept`.
|
|
@@ -138,7 +138,7 @@ export async function runLlmEnrich(body) {
|
|
|
138
138
|
return { tags: [] };
|
|
139
139
|
}
|
|
140
140
|
const llmConfig = config.llm;
|
|
141
|
-
const { chatCompletion, parseJsonResponse } = await import("../llm/client");
|
|
141
|
+
const { chatCompletion, parseEmbeddedJsonResponse: parseJsonResponse } = await import("../llm/client");
|
|
142
142
|
const prompt = `You are a memory tagger for a developer knowledge base.
|
|
143
143
|
Given the memory text below, return ONLY a JSON object with these fields:
|
|
144
144
|
- "tags": array of 1-5 short lowercase keyword tags
|
package/dist/commands/show.js
CHANGED
|
@@ -9,15 +9,9 @@
|
|
|
9
9
|
* edit-hints, summary-detail truncation) lives below in this file. The flow:
|
|
10
10
|
*
|
|
11
11
|
* 1. Special-case wiki-root refs (`wiki:<name>` with no page path).
|
|
12
|
-
* 2.
|
|
13
|
-
* 3.
|
|
14
|
-
* no matching row — covers the "indexed yet?" gap when the user has
|
|
15
|
-
* just added a file and not run `akm index`.
|
|
12
|
+
* 2. Auto-index when stale so the index is current.
|
|
13
|
+
* 3. Ask `indexer.lookup(ref)` for the row in the FTS index.
|
|
16
14
|
* 4. Render the file via the matcher/renderer pipeline.
|
|
17
|
-
*
|
|
18
|
-
* Step (2) is the v1 spec change: reading is the indexer's job. Step (3) is a
|
|
19
|
-
* pragmatic safety net (NOT remote provider fallback, which the spec
|
|
20
|
-
* forbids — "Show: Local FTS5 index only. No remote provider fallback.").
|
|
21
15
|
*/
|
|
22
16
|
import fs from "node:fs";
|
|
23
17
|
import path from "node:path";
|
|
@@ -27,6 +21,7 @@ import { NotFoundError, UsageError } from "../core/errors";
|
|
|
27
21
|
import { appendEvent, readEvents } from "../core/events";
|
|
28
22
|
import { parseFrontmatter, toStringOrUndefined } from "../core/frontmatter";
|
|
29
23
|
import { closeDatabase, findEntryIdByRef, openExistingDatabase } from "../indexer/db";
|
|
24
|
+
import { ensureIndex } from "../indexer/ensure-index";
|
|
30
25
|
import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "../indexer/file-context";
|
|
31
26
|
import { lookup } from "../indexer/indexer";
|
|
32
27
|
import { buildEditHint, findSourceForPath, isEditable, resolveSourceEntries } from "../indexer/search-source";
|
|
@@ -34,8 +29,8 @@ import { insertUsageEvent } from "../indexer/usage-events";
|
|
|
34
29
|
import { resolveSourcesForOrigin } from "../registry/origin-resolve";
|
|
35
30
|
// Eagerly import source providers to trigger self-registration.
|
|
36
31
|
import "../sources/providers/index";
|
|
37
|
-
import { resolveAssetPath } from "../sources/resolve";
|
|
38
32
|
import { getActiveWorkflowRun } from "../workflows/runs";
|
|
33
|
+
import { getCurrentWorkflowScopeKey } from "../workflows/scope-key";
|
|
39
34
|
/**
|
|
40
35
|
* Show a wiki root (no page path) — returns the same payload as
|
|
41
36
|
* `akm wiki show <name>`.
|
|
@@ -128,7 +123,12 @@ export async function akmShowUnified(input) {
|
|
|
128
123
|
new NotFoundError(`Wiki not found: ${parsed.name}. Run \`akm wiki create ${parsed.name}\` to create it.`));
|
|
129
124
|
}
|
|
130
125
|
}
|
|
131
|
-
//
|
|
126
|
+
// Auto-index when stale so the index is current before lookup.
|
|
127
|
+
const allSources = resolveSourceEntries();
|
|
128
|
+
if (allSources.length > 0) {
|
|
129
|
+
await ensureIndex(allSources[0].path);
|
|
130
|
+
}
|
|
131
|
+
// Try local filesystem (FTS5 index lookup)
|
|
132
132
|
const result = await showLocal(input);
|
|
133
133
|
// Scope filter narrows resolution: if --scope was supplied, the asset's
|
|
134
134
|
// frontmatter scope must satisfy every supplied key. We re-read the file
|
|
@@ -220,38 +220,16 @@ function logShowEvent(ref, existingDb) {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
/**
|
|
223
|
-
* Resolve an asset path
|
|
224
|
-
* 1. `indexer.lookup(ref)` — the spec's primary path (§6.2).
|
|
225
|
-
* 2. On-disk type-dir traversal — fallback for files not yet indexed.
|
|
223
|
+
* Resolve an asset path via the FTS5 index only. Spec §6.2's primary path.
|
|
226
224
|
*
|
|
227
|
-
* Returns `undefined` if
|
|
225
|
+
* Returns `undefined` if the index has no matching row.
|
|
228
226
|
*/
|
|
229
|
-
async function
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (entry) {
|
|
234
|
-
return { assetPath: entry.filePath };
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
catch (err) {
|
|
238
|
-
// Index unavailable (e.g. DB doesn't exist yet) — fall back to disk walk.
|
|
239
|
-
if (!(err instanceof NotFoundError)) {
|
|
240
|
-
// continue to disk fallback
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
// Step 2: on-disk type-dir traversal
|
|
244
|
-
let lastError;
|
|
245
|
-
for (const dir of searchSourceDirs) {
|
|
246
|
-
try {
|
|
247
|
-
const assetPath = await resolveAssetPath(dir, parsed.type, parsed.name);
|
|
248
|
-
return { assetPath, lastError };
|
|
249
|
-
}
|
|
250
|
-
catch (err) {
|
|
251
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
252
|
-
}
|
|
227
|
+
async function resolvePathViaIndex(parsed) {
|
|
228
|
+
const entry = await lookup(parsed);
|
|
229
|
+
if (entry) {
|
|
230
|
+
return { assetPath: entry.filePath };
|
|
253
231
|
}
|
|
254
|
-
return
|
|
232
|
+
return undefined;
|
|
255
233
|
}
|
|
256
234
|
/** @internal Use akmShowUnified() for all external callers. */
|
|
257
235
|
export async function showLocal(input) {
|
|
@@ -273,13 +251,10 @@ export async function showLocal(input) {
|
|
|
273
251
|
}
|
|
274
252
|
}
|
|
275
253
|
if (!assetPath) {
|
|
276
|
-
const resolved = await
|
|
254
|
+
const resolved = await resolvePathViaIndex(parsed);
|
|
277
255
|
if (resolved?.assetPath) {
|
|
278
256
|
assetPath = resolved.assetPath;
|
|
279
257
|
}
|
|
280
|
-
else if (resolved?.lastError) {
|
|
281
|
-
lastError = resolved.lastError;
|
|
282
|
-
}
|
|
283
258
|
}
|
|
284
259
|
if (!assetPath && parsed.origin && searchSources.length === 0) {
|
|
285
260
|
const installCmd = `akm add ${parsed.origin}`;
|
|
@@ -319,7 +294,7 @@ export async function showLocal(input) {
|
|
|
319
294
|
editable,
|
|
320
295
|
...(!editable ? { editHint: buildEditHint(assetPath, parsed.type, parsed.name, source?.registryId) } : {}),
|
|
321
296
|
};
|
|
322
|
-
const activeRun = getActiveWorkflowRun();
|
|
297
|
+
const activeRun = getActiveWorkflowRun(getCurrentWorkflowScopeKey());
|
|
323
298
|
if (activeRun) {
|
|
324
299
|
fullResponse.activeRun = activeRun;
|
|
325
300
|
}
|
package/dist/commands/vault.js
CHANGED
|
@@ -5,13 +5,9 @@
|
|
|
5
5
|
* the indexer, the `akm show` renderer, or any structured output channel.
|
|
6
6
|
* The supported load paths are:
|
|
7
7
|
*
|
|
8
|
-
* - `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* file, and emits `. <tmp>; rm -f <tmp>` on stdout. Values reach bash
|
|
12
|
-
* only via the temp file, never via akm's stdout.
|
|
13
|
-
* - `injectIntoEnv(vaultPath, target)` — programmatic API for modules that
|
|
14
|
-
* need values in a process environment.
|
|
8
|
+
* - `source "$(akm vault path vault:<name>)"` — direct shell loading path.
|
|
9
|
+
* - `injectIntoEnv(vaultPath, target)` / `loadEnv(vaultPath)` — programmatic
|
|
10
|
+
* APIs for modules that need values in process memory.
|
|
15
11
|
*
|
|
16
12
|
* Value parsing is delegated to the `dotenv` package — we deliberately do not
|
|
17
13
|
* implement our own quoting/escaping rules for security-sensitive content.
|
|
@@ -144,8 +140,7 @@ export function injectIntoEnv(vaultPath, target = process.env) {
|
|
|
144
140
|
* non-assignment content, so sourcing the output is safe regardless of what
|
|
145
141
|
* the vault file contains.
|
|
146
142
|
*
|
|
147
|
-
*
|
|
148
|
-
* temp file and emits only the path (never values) on stdout.
|
|
143
|
+
* Retained for programmatic callers/tests that need a literal export script.
|
|
149
144
|
*/
|
|
150
145
|
export function buildShellExportScript(vaultPath) {
|
|
151
146
|
const env = loadEnv(vaultPath);
|
|
@@ -264,7 +259,7 @@ export function createVault(vaultPath) {
|
|
|
264
259
|
* Characters that are safe in an UNquoted dotenv value AND are not
|
|
265
260
|
* metacharacters in POSIX shells. Anything outside this set forces quoting,
|
|
266
261
|
* which is defense-in-depth for any caller that might ever `source` the
|
|
267
|
-
* vault file directly instead of going through `akm vault
|
|
262
|
+
* vault file directly instead of going through `akm vault path`.
|
|
268
263
|
*/
|
|
269
264
|
const UNQUOTED_SAFE_RE = /^[A-Za-z0-9_.:/@%+,-]+$/;
|
|
270
265
|
/**
|
|
@@ -32,7 +32,7 @@ export const ACTION_BUILDERS = {
|
|
|
32
32
|
knowledge: (ref) => `akm show ${ref} -> read reference material`,
|
|
33
33
|
memory: (ref) => `akm show ${ref} -> recall context`,
|
|
34
34
|
workflow: (ref) => buildWorkflowAction(ref),
|
|
35
|
-
vault: (ref) => `akm
|
|
35
|
+
vault: (ref) => `akm show ${ref} -> inspect keys; source "$(akm vault path ${ref})" -> load values; akm vault run ${ref} -- <command> -> run with injected env`,
|
|
36
36
|
wiki: (ref) => `akm show ${ref} -> read the wiki page`,
|
|
37
37
|
};
|
|
38
38
|
/**
|
package/dist/core/asset-spec.js
CHANGED
|
@@ -82,7 +82,7 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
82
82
|
return path.join(typeRoot, name.endsWith(".env") ? name : `${name}.env`);
|
|
83
83
|
},
|
|
84
84
|
rendererName: "vault-env",
|
|
85
|
-
actionBuilder: (ref) => `akm
|
|
85
|
+
actionBuilder: (ref) => `akm show ${ref} -> inspect keys; source "$(akm vault path ${ref})" -> load values; akm vault run ${ref} -- <command> -> run with injected env`,
|
|
86
86
|
},
|
|
87
87
|
wiki: {
|
|
88
88
|
stashDir: "wikis",
|
package/dist/core/config.js
CHANGED
|
@@ -485,6 +485,10 @@ function parseLlmConfig(value) {
|
|
|
485
485
|
warn(`[akm] Ignoring llm config: endpoint must start with http:// or https://, got "${obj.endpoint}"`);
|
|
486
486
|
return undefined;
|
|
487
487
|
}
|
|
488
|
+
if (!obj.endpoint.endsWith("/chat/completions")) {
|
|
489
|
+
warn(`[akm] llm.endpoint "${obj.endpoint}" does not end in /chat/completions. ` +
|
|
490
|
+
`Did you mean "${obj.endpoint.replace(/\/+$/, "")}/chat/completions"?`);
|
|
491
|
+
}
|
|
488
492
|
const model = typeof obj.model === "string" ? obj.model : "";
|
|
489
493
|
const result = {
|
|
490
494
|
endpoint: obj.endpoint,
|
|
@@ -496,6 +500,15 @@ function parseLlmConfig(value) {
|
|
|
496
500
|
if (typeof obj.temperature === "number" && Number.isFinite(obj.temperature)) {
|
|
497
501
|
result.temperature = obj.temperature;
|
|
498
502
|
}
|
|
503
|
+
if ("timeoutMs" in obj) {
|
|
504
|
+
if (typeof obj.timeoutMs !== "number" ||
|
|
505
|
+
!Number.isFinite(obj.timeoutMs) ||
|
|
506
|
+
!Number.isInteger(obj.timeoutMs) ||
|
|
507
|
+
obj.timeoutMs <= 0) {
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
result.timeoutMs = obj.timeoutMs;
|
|
511
|
+
}
|
|
499
512
|
if ("maxTokens" in obj) {
|
|
500
513
|
if (typeof obj.maxTokens !== "number" ||
|
|
501
514
|
!Number.isFinite(obj.maxTokens) ||
|
package/dist/core/events.js
CHANGED
|
@@ -158,6 +158,11 @@ function matchesFilter(envelope, options) {
|
|
|
158
158
|
return false;
|
|
159
159
|
if (options.since && envelope.ts && envelope.ts < options.since)
|
|
160
160
|
return false;
|
|
161
|
+
const tags = envelope.metadata?.tags ?? [];
|
|
162
|
+
if (options.excludeTags?.some((t) => tags.includes(t)))
|
|
163
|
+
return false;
|
|
164
|
+
if (options.includeTags && !options.includeTags.every((t) => tags.includes(t)))
|
|
165
|
+
return false;
|
|
161
166
|
return true;
|
|
162
167
|
}
|
|
163
168
|
/**
|
|
@@ -178,7 +183,13 @@ export async function tailEvents(options = {}, ctx) {
|
|
|
178
183
|
// we start polling. This matches the documented behaviour of `tail
|
|
179
184
|
// --since`: emit existing events that match, then follow.
|
|
180
185
|
if (options.sinceOffset === undefined) {
|
|
181
|
-
const initial = readEvents({
|
|
186
|
+
const initial = readEvents({
|
|
187
|
+
since: options.since,
|
|
188
|
+
type: options.type,
|
|
189
|
+
ref: options.ref,
|
|
190
|
+
excludeTags: options.excludeTags,
|
|
191
|
+
includeTags: options.includeTags,
|
|
192
|
+
}, ctx);
|
|
182
193
|
for (const event of initial.events) {
|
|
183
194
|
collected.push(event);
|
|
184
195
|
options.onEvent?.(event);
|
|
@@ -202,7 +213,13 @@ export async function tailEvents(options = {}, ctx) {
|
|
|
202
213
|
}
|
|
203
214
|
function tick() {
|
|
204
215
|
try {
|
|
205
|
-
const result = readEvents({
|
|
216
|
+
const result = readEvents({
|
|
217
|
+
sinceOffset: cursor,
|
|
218
|
+
type: options.type,
|
|
219
|
+
ref: options.ref,
|
|
220
|
+
excludeTags: options.excludeTags,
|
|
221
|
+
includeTags: options.includeTags,
|
|
222
|
+
}, ctx);
|
|
206
223
|
cursor = result.nextOffset;
|
|
207
224
|
for (const event of result.events) {
|
|
208
225
|
// Apply --since filter inside the polling loop too — the cursor is
|