akm-cli 0.7.4 → 0.8.0-rc.10
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 +224 -1
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +133 -0
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2631 -1440
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +2122 -0
- package/dist/commands/curate.js +45 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +1081 -73
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +15 -24
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +1302 -0
- package/dist/commands/help/help-accept.md +12 -0
- package/dist/commands/help/help-improve.md +69 -0
- package/dist/commands/help/help-proposals.md +18 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +11 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +217 -0
- package/dist/commands/improve-profiles.js +166 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +2373 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +196 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +3 -0
- package/dist/commands/proposal.js +67 -12
- package/dist/commands/propose.js +120 -45
- package/dist/commands/reflect.js +1104 -60
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +70 -7
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +158 -60
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +437 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +17 -2
- package/dist/core/asset-registry.js +12 -17
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +67 -1
- package/dist/core/common.js +182 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +534 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +364 -968
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +105 -135
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -8
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +280 -14
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +512 -42
- package/dist/core/state-db.js +1068 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +64 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +198 -489
- package/dist/indexer/db.js +990 -108
- package/dist/indexer/ensure-index.js +136 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -114
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +547 -309
- package/dist/indexer/llm-cache.js +52 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +167 -160
- package/dist/indexer/memory-inference.js +152 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +275 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +6 -17
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +250 -36
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +183 -35
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +69 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +282 -0
- package/dist/integrations/session-logs/providers/opencode.js +258 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +79 -88
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +95 -48
- package/dist/llm/graph-extract.js +676 -72
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +80 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +292 -0
- package/dist/output/cli-hints-short.md +66 -0
- package/dist/output/cli-hints.js +7 -311
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +306 -258
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +102 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -511
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1039 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +11 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1093
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +71 -50
- package/dist/registry/providers/static-index.js +53 -48
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17750 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +179 -20
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +227 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +141 -2
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +11 -3
- package/dist/workflows/runs.js +91 -89
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +79 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +10 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.4.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +20 -0
- package/docs/migration/release-notes/0.8.0.md +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +29 -11
- package/dist/commands/install-audit.js +0 -381
- package/dist/commands/vault.js +0 -333
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
* Environment asset type (`env`) — whole `.env` file storage.
|
|
6
|
+
*
|
|
7
|
+
* An `env` asset holds a GROUP of related CONFIGURATION for an app or service
|
|
8
|
+
* (URLs, feature flags, and any credentials it needs) in a single `.env` file,
|
|
9
|
+
* sourced/injected wholesale. Values may or may not be sensitive — akm protects
|
|
10
|
+
* them all the same. For a single sensitive value used on its own for
|
|
11
|
+
* authentication (a token, key, or cert), use the `secret` type instead.
|
|
12
|
+
*
|
|
13
|
+
* Unlike the deprecated `vault` type it replaces, akm does NOT manage individual
|
|
14
|
+
* KEY=value entries (no `set`/`unset`/quoting): you edit the `.env` file with
|
|
15
|
+
* your own editor, and akm loads it. The simplification removes the
|
|
16
|
+
* hand-rolled quoting/escaping surface; the safety guarantee moves to the READ
|
|
17
|
+
* path instead (see `buildShellExportScript` + `akm env export`).
|
|
18
|
+
*
|
|
19
|
+
* Invariant: env values must never be written to stdout, returned through the
|
|
20
|
+
* indexer, the `akm show` renderer, or any structured output channel. Key
|
|
21
|
+
* NAMES and start-of-line comments ARE surfaced by design (discoverability) —
|
|
22
|
+
* only values are secret. The supported value-load paths are:
|
|
23
|
+
*
|
|
24
|
+
* - `akm env run <ref> -- <command>` — values injected into the child
|
|
25
|
+
* process env (never via a shell), see `injectIntoEnv` / `loadEnv`. This is
|
|
26
|
+
* the primary path and the only one safe for AI agents (no values ever
|
|
27
|
+
* reach stdout). For an interactive shell, `akm env run <ref> -- $SHELL`.
|
|
28
|
+
* - `akm env export <ref> --out <file>` — write parse-then-reserialized safe
|
|
29
|
+
* `export KEY='value'` lines to a file (mode 0600) for `source`-ing. Values
|
|
30
|
+
* are re-emitted single-quoted so a raw `.env` containing `X=$(cmd)` cannot
|
|
31
|
+
* execute on load. `export` never prints values to stdout (would leak into
|
|
32
|
+
* an agent's context); `path` prints only the file path.
|
|
33
|
+
*
|
|
34
|
+
* Value parsing is delegated to the `dotenv` package — we deliberately do not
|
|
35
|
+
* implement our own quoting/escaping rules for security-sensitive content.
|
|
36
|
+
*/
|
|
37
|
+
import fs from "node:fs";
|
|
38
|
+
import path from "node:path";
|
|
39
|
+
import dotenv from "dotenv";
|
|
40
|
+
import { writeFileAtomic } from "../core/common";
|
|
41
|
+
/** Matches a KEY=value assignment line, capturing only the key. */
|
|
42
|
+
const ASSIGN_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
43
|
+
/** Scan lines and return KEY names in file order, without duplicates. */
|
|
44
|
+
function scanKeys(text) {
|
|
45
|
+
const keys = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for (const line of text.split(/\r?\n/)) {
|
|
48
|
+
const m = line.match(ASSIGN_RE);
|
|
49
|
+
if (!m)
|
|
50
|
+
continue;
|
|
51
|
+
const key = m[1];
|
|
52
|
+
if (seen.has(key))
|
|
53
|
+
continue;
|
|
54
|
+
seen.add(key);
|
|
55
|
+
keys.push(key);
|
|
56
|
+
}
|
|
57
|
+
return keys;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Scan lines and return start-of-line `#` comments (with the leading `#` and
|
|
61
|
+
* any leading whitespace stripped). Inline/trailing `#` after an assignment is
|
|
62
|
+
* never extracted.
|
|
63
|
+
*/
|
|
64
|
+
function scanComments(text) {
|
|
65
|
+
const comments = [];
|
|
66
|
+
for (const line of text.split(/\r?\n/)) {
|
|
67
|
+
const trimmed = line.trimStart();
|
|
68
|
+
if (trimmed.startsWith("#")) {
|
|
69
|
+
comments.push(trimmed.slice(1).trimStart());
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return comments;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read and return ONLY non-secret metadata (keys + start-of-line comments).
|
|
76
|
+
*
|
|
77
|
+
* The function reads the whole file into memory (same as any dotenv parser)
|
|
78
|
+
* but deliberately does not parse values — the LHS-only regex scanners above
|
|
79
|
+
* ensure no value content is retained or returned. The guarantee is that
|
|
80
|
+
* values never leave this function.
|
|
81
|
+
*/
|
|
82
|
+
export function listKeys(envPath) {
|
|
83
|
+
if (!fs.existsSync(envPath))
|
|
84
|
+
return { keys: [], comments: [] };
|
|
85
|
+
const text = fs.readFileSync(envPath, "utf8");
|
|
86
|
+
return { keys: scanKeys(text), comments: scanComments(text) };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Return structured `entries` pairing each key with the nearest preceding
|
|
90
|
+
* comment line (if any). This is an easier-to-consume shape than the parallel
|
|
91
|
+
* `keys[]` + `comments[]` of `listKeys` (QA #35).
|
|
92
|
+
*
|
|
93
|
+
* Values are never included — the same privacy guarantee as `listKeys`.
|
|
94
|
+
*/
|
|
95
|
+
export function listEntries(envPath) {
|
|
96
|
+
if (!fs.existsSync(envPath))
|
|
97
|
+
return [];
|
|
98
|
+
const text = fs.readFileSync(envPath, "utf8");
|
|
99
|
+
const lines = text.split(/\r?\n/);
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
const entries = [];
|
|
102
|
+
let pendingComment;
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
const trimmed = line.trimStart();
|
|
105
|
+
if (trimmed.startsWith("#")) {
|
|
106
|
+
// Capture the most recent comment before a key
|
|
107
|
+
pendingComment = trimmed.slice(1).trimStart() || undefined;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const m = line.match(ASSIGN_RE);
|
|
111
|
+
if (m) {
|
|
112
|
+
const key = m[1];
|
|
113
|
+
if (!seen.has(key)) {
|
|
114
|
+
seen.add(key);
|
|
115
|
+
const entry = { key };
|
|
116
|
+
if (pendingComment)
|
|
117
|
+
entry.comment = pendingComment;
|
|
118
|
+
entries.push(entry);
|
|
119
|
+
}
|
|
120
|
+
pendingComment = undefined;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Any non-comment, non-assignment line (including blank lines)
|
|
124
|
+
// breaks "nearest preceding comment line" association.
|
|
125
|
+
pendingComment = undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return entries;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Read all KEY=value pairs from an env file. Intended for programmatic callers
|
|
132
|
+
* that need to inject values into a process environment. Callers MUST NOT write
|
|
133
|
+
* the returned values to stdout or any logged output.
|
|
134
|
+
*
|
|
135
|
+
* Value parsing (quoting, escapes, multi-line, etc.) is delegated to dotenv.
|
|
136
|
+
*/
|
|
137
|
+
export function loadEnv(envPath) {
|
|
138
|
+
if (!fs.existsSync(envPath))
|
|
139
|
+
return {};
|
|
140
|
+
const buf = fs.readFileSync(envPath);
|
|
141
|
+
return dotenv.parse(buf);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Load an env file and assign its values into `target` (defaults to
|
|
145
|
+
* `process.env`). Returns the list of keys that were set so the caller can
|
|
146
|
+
* log/observe without touching values.
|
|
147
|
+
*
|
|
148
|
+
* Existing keys in `target` are overwritten — callers who want to preserve
|
|
149
|
+
* pre-existing environment variables should filter before calling.
|
|
150
|
+
*/
|
|
151
|
+
export function injectIntoEnv(envPath, target = process.env) {
|
|
152
|
+
const env = loadEnv(envPath);
|
|
153
|
+
for (const [key, value] of Object.entries(env)) {
|
|
154
|
+
target[key] = value;
|
|
155
|
+
}
|
|
156
|
+
return Object.keys(env);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Serialise an env file's values as a POSIX shell script of `export KEY='value'`
|
|
160
|
+
* lines, with single-quote escaping (`'\''`). Every line is an assignment of a
|
|
161
|
+
* literal string — there is no expansion, command substitution, or
|
|
162
|
+
* non-assignment content, so `eval`-ing the output is safe regardless of what
|
|
163
|
+
* the source file contains.
|
|
164
|
+
*
|
|
165
|
+
* This is the trust boundary for shell loading: a raw `.env` may contain
|
|
166
|
+
* `X=$(rm -rf ~)`, which would execute if `source`d directly, but dotenv parses
|
|
167
|
+
* it to the literal string `$(rm -rf ~)` and we re-emit it single-quoted. This
|
|
168
|
+
* backs `akm env export <ref> --out <file>` (file-only; never printed to stdout).
|
|
169
|
+
*/
|
|
170
|
+
export function buildShellExportScript(envPath) {
|
|
171
|
+
const env = loadEnv(envPath);
|
|
172
|
+
const lines = [];
|
|
173
|
+
for (const [key, value] of Object.entries(env)) {
|
|
174
|
+
// Defence in depth: dotenv already validates key shape, but reject any
|
|
175
|
+
// key we wouldn't be able to export safely.
|
|
176
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
177
|
+
continue;
|
|
178
|
+
const escaped = value.replace(/'/g, "'\\''");
|
|
179
|
+
lines.push(`export ${key}='${escaped}'`);
|
|
180
|
+
}
|
|
181
|
+
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
182
|
+
}
|
|
183
|
+
/** Create an empty env file (does nothing if it already exists). */
|
|
184
|
+
export function createEnv(envPath) {
|
|
185
|
+
ensureParentDir(envPath);
|
|
186
|
+
if (fs.existsSync(envPath))
|
|
187
|
+
return;
|
|
188
|
+
writeFileAtomic(envPath, "", 0o600);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Write (create or overwrite) an env file with the given text content,
|
|
192
|
+
* atomically at mode 0600. Used to ingest an existing `.env` file
|
|
193
|
+
* (`env create --from-file` / `--from-stdin`).
|
|
194
|
+
*/
|
|
195
|
+
export function writeEnv(envPath, content) {
|
|
196
|
+
ensureParentDir(envPath);
|
|
197
|
+
writeFileAtomic(envPath, content, 0o600);
|
|
198
|
+
}
|
|
199
|
+
/** Remove an env file (and its `.sensitive` marker, if present). Returns true if it existed. */
|
|
200
|
+
export function removeEnv(envPath) {
|
|
201
|
+
if (!fs.existsSync(envPath))
|
|
202
|
+
return false;
|
|
203
|
+
fs.rmSync(envPath);
|
|
204
|
+
const marker = `${envPath}.sensitive`;
|
|
205
|
+
if (fs.existsSync(marker))
|
|
206
|
+
fs.rmSync(marker);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
function ensureParentDir(filePath) {
|
|
210
|
+
const dir = path.dirname(filePath);
|
|
211
|
+
if (!fs.existsSync(dir))
|
|
212
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
213
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { writeFileAtomic } from "../core/common";
|
|
7
|
+
export function writeEvalCase(stashDir, evalCase) {
|
|
8
|
+
const evalDir = path.join(stashDir, ".akm", "eval-cases");
|
|
9
|
+
fs.mkdirSync(evalDir, { recursive: true });
|
|
10
|
+
const fileName = `${evalCase.slug}.md`;
|
|
11
|
+
const filePath = path.join(evalDir, fileName);
|
|
12
|
+
const content = `---
|
|
13
|
+
ref: ${evalCase.ref}
|
|
14
|
+
failureReason: ${evalCase.failureReason}
|
|
15
|
+
assetType: ${evalCase.assetType}
|
|
16
|
+
rejectedAt: ${evalCase.rejectedAt}
|
|
17
|
+
source: ${evalCase.source}
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Eval Case: ${evalCase.ref}
|
|
21
|
+
|
|
22
|
+
**Failure reason:** ${evalCase.failureReason}
|
|
23
|
+
**Source:** ${evalCase.source}
|
|
24
|
+
**Asset type:** ${evalCase.assetType}
|
|
25
|
+
|
|
26
|
+
This case was automatically captured when a distillation or proposal was rejected.
|
|
27
|
+
Use it as a regression test: future improve runs on this ref should not produce
|
|
28
|
+
output that would be rejected for the same reason.
|
|
29
|
+
`;
|
|
30
|
+
writeFileAtomic(filePath, content);
|
|
31
|
+
return filePath;
|
|
32
|
+
}
|
|
33
|
+
export function countEvalCases(stashDir) {
|
|
34
|
+
const evalDir = path.join(stashDir, ".akm", "eval-cases");
|
|
35
|
+
if (!fs.existsSync(evalDir))
|
|
36
|
+
return 0;
|
|
37
|
+
try {
|
|
38
|
+
return fs.readdirSync(evalDir).filter((f) => f.endsWith(".md")).length;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/commands/events.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* `akm events list` and `akm events tail` (#204).
|
|
3
6
|
*
|
|
@@ -10,6 +13,7 @@
|
|
|
10
13
|
import { parseAssetRef } from "../core/asset-ref";
|
|
11
14
|
import { UsageError } from "../core/errors";
|
|
12
15
|
import { readEvents, tailEvents } from "../core/events";
|
|
16
|
+
import { parseSinceToIso } from "../core/time";
|
|
13
17
|
/**
|
|
14
18
|
* Parse `--since` accepting either a byte-offset cursor (`@offset:<int>`) for
|
|
15
19
|
* cross-process resumption, or a timestamp / epoch-ms (the existing form).
|
|
@@ -30,7 +34,7 @@ function parseSinceFlag(since) {
|
|
|
30
34
|
}
|
|
31
35
|
return { sinceOffset: value };
|
|
32
36
|
}
|
|
33
|
-
return { since:
|
|
37
|
+
return { since: parseSinceToIso(trimmed) };
|
|
34
38
|
}
|
|
35
39
|
function validateRef(ref) {
|
|
36
40
|
if (ref === undefined)
|
|
@@ -42,32 +46,17 @@ function validateRef(ref) {
|
|
|
42
46
|
parseAssetRef(trimmed);
|
|
43
47
|
return trimmed;
|
|
44
48
|
}
|
|
45
|
-
function normalizeSince(since) {
|
|
46
|
-
if (since === undefined)
|
|
47
|
-
return undefined;
|
|
48
|
-
const trimmed = since.trim();
|
|
49
|
-
if (!trimmed) {
|
|
50
|
-
throw new UsageError("--since cannot be empty.", "INVALID_FLAG_VALUE");
|
|
51
|
-
}
|
|
52
|
-
// Accept ISO timestamp (preferred), epoch ms, or plain date.
|
|
53
|
-
if (/^\d+$/.test(trimmed)) {
|
|
54
|
-
const ms = Number.parseInt(trimmed, 10);
|
|
55
|
-
const d = new Date(ms);
|
|
56
|
-
if (Number.isNaN(d.getTime())) {
|
|
57
|
-
throw new UsageError(`Invalid --since value: ${since}`, "INVALID_FLAG_VALUE");
|
|
58
|
-
}
|
|
59
|
-
return d.toISOString();
|
|
60
|
-
}
|
|
61
|
-
const parsed = new Date(trimmed);
|
|
62
|
-
if (Number.isNaN(parsed.getTime())) {
|
|
63
|
-
throw new UsageError(`Invalid --since value: ${since}. Expected ISO timestamp (e.g. 2026-04-01T00:00:00Z) or epoch ms.`, "INVALID_FLAG_VALUE");
|
|
64
|
-
}
|
|
65
|
-
return parsed.toISOString();
|
|
66
|
-
}
|
|
67
49
|
export function akmEventsList(options = {}) {
|
|
68
50
|
const ref = validateRef(options.ref);
|
|
69
51
|
const parsed = parseSinceFlag(options.since);
|
|
70
|
-
const result = readEvents({
|
|
52
|
+
const result = readEvents({
|
|
53
|
+
since: parsed.since,
|
|
54
|
+
sinceOffset: parsed.sinceOffset,
|
|
55
|
+
type: options.type,
|
|
56
|
+
ref,
|
|
57
|
+
excludeTags: options.excludeTags,
|
|
58
|
+
includeTags: options.includeTags,
|
|
59
|
+
}, options.ctx);
|
|
71
60
|
return {
|
|
72
61
|
schemaVersion: 1,
|
|
73
62
|
totalCount: result.events.length,
|
|
@@ -92,6 +81,8 @@ export async function akmEventsTail(options = {}) {
|
|
|
92
81
|
maxEvents: options.maxEvents,
|
|
93
82
|
signal: options.signal,
|
|
94
83
|
onEvent: options.onEvent,
|
|
84
|
+
excludeTags: options.excludeTags,
|
|
85
|
+
includeTags: options.includeTags,
|
|
95
86
|
};
|
|
96
87
|
const result = await tailEvents(tailOptions, options.ctx);
|
|
97
88
|
return {
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
* CLI surface for `akm extract`.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* akm extract --type claude-code --session-id <id>
|
|
9
|
+
* akm extract --type claude-code --since 24h
|
|
10
|
+
* akm extract --type opencode --since 7d --dry-run
|
|
11
|
+
* akm extract --auto # iterate all available harnesses
|
|
12
|
+
* akm extract --type claude-code --location /custom/path --session-id <id>
|
|
13
|
+
*
|
|
14
|
+
* Output is the AkmExtractResult JSON envelope (or an aggregated one when
|
|
15
|
+
* `--auto` runs multiple harnesses).
|
|
16
|
+
*/
|
|
17
|
+
import { defineCommand } from "citty";
|
|
18
|
+
import { output, runWithJsonErrors } from "../cli/shared";
|
|
19
|
+
import { UsageError } from "../core/errors";
|
|
20
|
+
import { getAvailableHarnesses } from "../integrations/session-logs";
|
|
21
|
+
import { akmExtract } from "./extract";
|
|
22
|
+
export const extractCommand = defineCommand({
|
|
23
|
+
meta: {
|
|
24
|
+
name: "extract",
|
|
25
|
+
description: "Extract durable insights from native session files (claude-code, opencode) and queue them as proposals. Replaces the legacy session-checkpoint hook.",
|
|
26
|
+
},
|
|
27
|
+
args: {
|
|
28
|
+
type: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Harness name (claude-code, opencode). Required unless --auto.",
|
|
31
|
+
},
|
|
32
|
+
"session-id": {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Process only this session ID. When absent, discover sessions via --since.",
|
|
35
|
+
},
|
|
36
|
+
location: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Override the harness's default session-discovery location.",
|
|
39
|
+
},
|
|
40
|
+
since: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Discovery cutoff. ISO timestamp or duration (24h, 7d, 30m). Default 24h.",
|
|
43
|
+
},
|
|
44
|
+
auto: {
|
|
45
|
+
type: "boolean",
|
|
46
|
+
description: "Iterate every available harness with default --since. Mutually exclusive with --type.",
|
|
47
|
+
default: false,
|
|
48
|
+
},
|
|
49
|
+
"dry-run": {
|
|
50
|
+
type: "boolean",
|
|
51
|
+
description: "Show candidates without queuing proposals.",
|
|
52
|
+
default: false,
|
|
53
|
+
},
|
|
54
|
+
force: {
|
|
55
|
+
type: "boolean",
|
|
56
|
+
description: "Re-process sessions even if they were already extracted and have no new events. Default: skip already-seen sessions.",
|
|
57
|
+
default: false,
|
|
58
|
+
},
|
|
59
|
+
"timeout-ms": {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "Per-session LLM timeout in ms (default 60000).",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
async run({ args }) {
|
|
65
|
+
await runWithJsonErrors(async () => {
|
|
66
|
+
const type = typeof args.type === "string" ? args.type.trim() : "";
|
|
67
|
+
const sessionId = typeof args["session-id"] === "string" ? args["session-id"].trim() : "";
|
|
68
|
+
const location = typeof args.location === "string" ? args.location.trim() : "";
|
|
69
|
+
const since = typeof args.since === "string" ? args.since.trim() : "";
|
|
70
|
+
const auto = args.auto === true;
|
|
71
|
+
const dryRun = args["dry-run"] === true;
|
|
72
|
+
const force = args.force === true;
|
|
73
|
+
const timeoutMs = typeof args["timeout-ms"] === "string" && args["timeout-ms"] !== ""
|
|
74
|
+
? Number.parseInt(args["timeout-ms"], 10)
|
|
75
|
+
: undefined;
|
|
76
|
+
if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
|
|
77
|
+
throw new UsageError(`--timeout-ms must be a positive integer (got "${args["timeout-ms"]}").`, "INVALID_FLAG_VALUE");
|
|
78
|
+
}
|
|
79
|
+
if (auto && type) {
|
|
80
|
+
throw new UsageError("--auto and --type are mutually exclusive. Pick one.", "INVALID_FLAG_VALUE");
|
|
81
|
+
}
|
|
82
|
+
if (!auto && !type) {
|
|
83
|
+
throw new UsageError("--type is required (or pass --auto to try every available harness).", "MISSING_REQUIRED_ARGUMENT");
|
|
84
|
+
}
|
|
85
|
+
const commonOptions = {
|
|
86
|
+
...(sessionId ? { sessionId } : {}),
|
|
87
|
+
...(location ? { location } : {}),
|
|
88
|
+
...(since ? { since } : {}),
|
|
89
|
+
dryRun,
|
|
90
|
+
force,
|
|
91
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
92
|
+
};
|
|
93
|
+
if (auto) {
|
|
94
|
+
const harnesses = getAvailableHarnesses();
|
|
95
|
+
if (harnesses.length === 0) {
|
|
96
|
+
output("extract", {
|
|
97
|
+
schemaVersion: 1,
|
|
98
|
+
ok: false,
|
|
99
|
+
shape: "extract-auto-result",
|
|
100
|
+
warnings: ["no available harnesses found on this machine"],
|
|
101
|
+
results: [],
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const results = [];
|
|
106
|
+
for (const h of harnesses) {
|
|
107
|
+
const result = await akmExtract({ type: h.name, ...commonOptions });
|
|
108
|
+
results.push(result);
|
|
109
|
+
}
|
|
110
|
+
const ok = results.every((r) => r.ok);
|
|
111
|
+
const totalProposals = results.reduce((sum, r) => sum + r.proposals.length, 0);
|
|
112
|
+
output("extract", {
|
|
113
|
+
schemaVersion: 1,
|
|
114
|
+
ok,
|
|
115
|
+
shape: "extract-auto-result",
|
|
116
|
+
dryRun,
|
|
117
|
+
harnessesProcessed: results.length,
|
|
118
|
+
totalProposals,
|
|
119
|
+
results,
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const result = await akmExtract({ type, ...commonOptions });
|
|
124
|
+
output("extract", result);
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
import promptTemplate from "../llm/prompts/extract-session.md" with { type: "text" };
|
|
5
|
+
/**
|
|
6
|
+
* JSON Schema for the structured extract output. Passed to `chatCompletion`
|
|
7
|
+
* when the configured LLM connection has `supportsJsonSchema: true`.
|
|
8
|
+
*
|
|
9
|
+
* Shape:
|
|
10
|
+
* {
|
|
11
|
+
* "candidates": [{type, name, description, when_to_use?, body, confidence, evidence}, ...],
|
|
12
|
+
* "rationale_if_empty"?: string
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* `additionalProperties: false` at each level so any hallucinated keys are
|
|
16
|
+
* dropped before parsing.
|
|
17
|
+
*/
|
|
18
|
+
export const EXTRACT_JSON_SCHEMA = {
|
|
19
|
+
type: "object",
|
|
20
|
+
required: ["candidates"],
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
properties: {
|
|
23
|
+
candidates: {
|
|
24
|
+
type: "array",
|
|
25
|
+
description: "Zero or more durable-insight candidates extracted from the session.",
|
|
26
|
+
items: {
|
|
27
|
+
type: "object",
|
|
28
|
+
required: ["type", "name", "description", "body", "confidence", "evidence"],
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
properties: {
|
|
31
|
+
type: {
|
|
32
|
+
type: "string",
|
|
33
|
+
enum: ["memory", "lesson", "knowledge"],
|
|
34
|
+
description: "Asset type the candidate would land as.",
|
|
35
|
+
},
|
|
36
|
+
name: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Kebab-case slug for the new asset.",
|
|
39
|
+
pattern: "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
|
|
40
|
+
},
|
|
41
|
+
description: {
|
|
42
|
+
type: "string",
|
|
43
|
+
minLength: 20,
|
|
44
|
+
maxLength: 400,
|
|
45
|
+
description: "One-sentence summary of the candidate.",
|
|
46
|
+
},
|
|
47
|
+
when_to_use: {
|
|
48
|
+
type: "string",
|
|
49
|
+
minLength: 15,
|
|
50
|
+
maxLength: 400,
|
|
51
|
+
description: "Trigger sentence for the candidate; REQUIRED when type=lesson.",
|
|
52
|
+
},
|
|
53
|
+
body: {
|
|
54
|
+
type: "string",
|
|
55
|
+
minLength: 50,
|
|
56
|
+
description: "Markdown body of the candidate asset.",
|
|
57
|
+
},
|
|
58
|
+
confidence: {
|
|
59
|
+
type: "number",
|
|
60
|
+
minimum: 0,
|
|
61
|
+
maximum: 1,
|
|
62
|
+
description: "Self-rated confidence in [0, 1] that this candidate is a real durable insight.",
|
|
63
|
+
},
|
|
64
|
+
evidence: {
|
|
65
|
+
type: "string",
|
|
66
|
+
minLength: 5,
|
|
67
|
+
description: "One-line pointer to the moment in the session that supports this candidate.",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
rationale_if_empty: {
|
|
73
|
+
type: "string",
|
|
74
|
+
minLength: 10,
|
|
75
|
+
description: "Required when `candidates` is empty — explains why nothing rose to durable-insight level.",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Format inline refs as a bullet list for the "Already preserved" section.
|
|
81
|
+
* If empty, returns a sentinel string so the LLM knows the agent saved
|
|
82
|
+
* nothing inline.
|
|
83
|
+
*/
|
|
84
|
+
function formatAlreadyPreserved(inlineRefs) {
|
|
85
|
+
if (inlineRefs.length === 0) {
|
|
86
|
+
return "(none — the agent did not call `akm remember` or `akm feedback` during this session)";
|
|
87
|
+
}
|
|
88
|
+
return inlineRefs
|
|
89
|
+
.map((ref) => {
|
|
90
|
+
const prefix = ref.kind === "remember" ? "- remember:" : `- feedback ${ref.ref ?? "<ref>"}:`;
|
|
91
|
+
const body = ref.text.trim().slice(0, 200);
|
|
92
|
+
return `${prefix} ${body}${ref.text.length > 200 ? "…" : ""}`;
|
|
93
|
+
})
|
|
94
|
+
.join("\n");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Format pre-filtered events as a transcript snippet. Each event becomes:
|
|
98
|
+
* [<role> @ <iso>] <text>
|
|
99
|
+
* Events are already truncated/cleaned by the pre-filter; this is purely
|
|
100
|
+
* a render step.
|
|
101
|
+
*/
|
|
102
|
+
function formatTranscript(events) {
|
|
103
|
+
if (events.length === 0)
|
|
104
|
+
return "(empty — pre-filter removed all events as noise)";
|
|
105
|
+
return events
|
|
106
|
+
.map((e) => {
|
|
107
|
+
const tsLabel = e.ts ? new Date(e.ts).toISOString() : "unknown-ts";
|
|
108
|
+
const roleLabel = e.role ?? "unknown";
|
|
109
|
+
return `[${roleLabel} @ ${tsLabel}] ${e.text}`;
|
|
110
|
+
})
|
|
111
|
+
.join("\n\n");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build the user-prompt body for the extract LLM call by interpolating
|
|
115
|
+
* session metadata, already-preserved refs, and the filtered transcript
|
|
116
|
+
* into the template.
|
|
117
|
+
*/
|
|
118
|
+
export function buildExtractPrompt(input) {
|
|
119
|
+
const ref = input.data.ref;
|
|
120
|
+
const startedAt = ref.startedAt ? new Date(ref.startedAt).toISOString() : "unknown";
|
|
121
|
+
const endedAt = ref.endedAt ? new Date(ref.endedAt).toISOString() : "unknown";
|
|
122
|
+
return promptTemplate
|
|
123
|
+
.replace("{{HARNESS}}", ref.harness)
|
|
124
|
+
.replace("{{TITLE}}", ref.title ?? "(no title)")
|
|
125
|
+
.replace("{{STARTED_AT}}", startedAt)
|
|
126
|
+
.replace("{{ENDED_AT}}", endedAt)
|
|
127
|
+
.replace("{{PROJECT_HINT}}", ref.projectHint ?? "(no project hint)")
|
|
128
|
+
.replace("{{ALREADY_PRESERVED}}", formatAlreadyPreserved(input.inlineRefs))
|
|
129
|
+
.replace("{{TRANSCRIPT}}", formatTranscript(input.events));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Parse the LLM's JSON response into a structured {@link ExtractPayload}.
|
|
133
|
+
* Defensive — drops candidates that violate the shape rather than failing
|
|
134
|
+
* the whole call. Returns the empty-candidates payload when nothing parses.
|
|
135
|
+
*/
|
|
136
|
+
export function parseExtractPayload(stdout) {
|
|
137
|
+
if (!stdout || stdout.trim().length === 0) {
|
|
138
|
+
return { candidates: [], rationale_if_empty: "LLM returned empty response" };
|
|
139
|
+
}
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(stdout);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Tolerate prose preamble/postamble by extracting the first balanced
|
|
146
|
+
// top-level JSON object.
|
|
147
|
+
const start = stdout.indexOf("{");
|
|
148
|
+
const end = stdout.lastIndexOf("}");
|
|
149
|
+
if (start === -1 || end <= start) {
|
|
150
|
+
return { candidates: [], rationale_if_empty: `LLM response was not parseable JSON` };
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(stdout.slice(start, end + 1));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return { candidates: [], rationale_if_empty: `LLM response was not parseable JSON` };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!parsed || typeof parsed !== "object") {
|
|
160
|
+
return { candidates: [], rationale_if_empty: "LLM response was not an object" };
|
|
161
|
+
}
|
|
162
|
+
const obj = parsed;
|
|
163
|
+
const rawCandidates = Array.isArray(obj.candidates) ? obj.candidates : [];
|
|
164
|
+
const candidates = [];
|
|
165
|
+
for (const raw of rawCandidates) {
|
|
166
|
+
if (!raw || typeof raw !== "object")
|
|
167
|
+
continue;
|
|
168
|
+
const c = raw;
|
|
169
|
+
const type = c.type;
|
|
170
|
+
if (type !== "memory" && type !== "lesson" && type !== "knowledge")
|
|
171
|
+
continue;
|
|
172
|
+
if (typeof c.name !== "string" || !/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(c.name))
|
|
173
|
+
continue;
|
|
174
|
+
if (typeof c.description !== "string" || c.description.trim().length < 20)
|
|
175
|
+
continue;
|
|
176
|
+
if (typeof c.body !== "string" || c.body.trim().length < 50)
|
|
177
|
+
continue;
|
|
178
|
+
if (typeof c.confidence !== "number" || !Number.isFinite(c.confidence))
|
|
179
|
+
continue;
|
|
180
|
+
if (typeof c.evidence !== "string" || c.evidence.trim().length < 5)
|
|
181
|
+
continue;
|
|
182
|
+
if (type === "lesson") {
|
|
183
|
+
if (typeof c.when_to_use !== "string" || c.when_to_use.trim().length < 15)
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const confidence = Math.max(0, Math.min(1, c.confidence));
|
|
187
|
+
const candidate = {
|
|
188
|
+
type,
|
|
189
|
+
name: c.name,
|
|
190
|
+
description: c.description.trim(),
|
|
191
|
+
body: c.body,
|
|
192
|
+
confidence,
|
|
193
|
+
evidence: c.evidence.trim(),
|
|
194
|
+
};
|
|
195
|
+
if (typeof c.when_to_use === "string")
|
|
196
|
+
candidate.when_to_use = c.when_to_use.trim();
|
|
197
|
+
candidates.push(candidate);
|
|
198
|
+
}
|
|
199
|
+
const result = { candidates };
|
|
200
|
+
if (typeof obj.rationale_if_empty === "string") {
|
|
201
|
+
result.rationale_if_empty = obj.rationale_if_empty.trim();
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|