akm-cli 0.8.0-rc2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
- 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 +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2141 -1268
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1533 -144
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- 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 +260 -5
- package/dist/commands/health.js +977 -51
- package/dist/commands/help/help-accept.md +6 -3
- package/dist/commands/help/help-improve.md +36 -8
- package/dist/commands/help/help-proposals.md +7 -4
- package/dist/commands/help/help-reject.md +5 -2
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +184 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1725 -332
- package/dist/commands/info.js +3 -0
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +199 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +13 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -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 +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +661 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +84 -14
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +110 -50
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -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 +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- 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 +92 -56
- package/dist/llm/graph-extract.js +401 -30
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
- package/dist/output/cli-hints-full.md +60 -32
- package/dist/output/cli-hints-short.md +10 -7
- package/dist/output/cli-hints.js +5 -2
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- 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 +105 -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 -549
- 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 +1059 -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 +12 -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 -1329
- 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 +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- 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 +17767 -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 +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- 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 +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +3 -0
- package/dist/tasks/backends/schtasks.js +3 -0
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +3 -0
- package/dist/wiki/wiki.js +3 -0
- package/dist/workflows/authoring.js +3 -0
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -310
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
|
@@ -0,0 +1,447 @@
|
|
|
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 { createHash } from "node:crypto";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { assembleAsset } from "../core/asset-serialize";
|
|
8
|
+
import { concurrentMap } from "../core/concurrent";
|
|
9
|
+
import { parseFrontmatter, parseFrontmatterBlock } from "../core/frontmatter";
|
|
10
|
+
import { warn } from "../core/warn";
|
|
11
|
+
import { resolveValidationRunner } from "../integrations/agent/runner";
|
|
12
|
+
import { chatCompletion } from "../llm/client";
|
|
13
|
+
import { isProcessEnabled } from "../llm/feature-gate";
|
|
14
|
+
import { findEntryIdByRef } from "./db";
|
|
15
|
+
import { withLlmCache } from "./llm-cache";
|
|
16
|
+
import { walkMarkdownFiles } from "./walker";
|
|
17
|
+
/** Frontmatter keys this pass touches. Constants so a future rename only needs to touch one site. */
|
|
18
|
+
const FM_BELIEF_STATE = "beliefState";
|
|
19
|
+
const FM_SUPERSEDED_BY = "supersededBy";
|
|
20
|
+
const FM_LAST_CONFIRMED_AT = "lastConfirmedAt";
|
|
21
|
+
/** Cache variant for `withLlmCache`. Keeps staleness results isolated from memory-inference cache rows. */
|
|
22
|
+
const CACHE_VARIANT = "staleness_detect";
|
|
23
|
+
/** Belief states excluded from staleness detection — already historical / archived. */
|
|
24
|
+
const EXCLUDED_BELIEF_STATES = new Set(["contradicted", "archived", "deprecated"]);
|
|
25
|
+
/** Default threshold in days before a memory is re-evaluated. */
|
|
26
|
+
const DEFAULT_THRESHOLD_DAYS = 90;
|
|
27
|
+
/** Top-K similar memories included in the LLM prompt. */
|
|
28
|
+
const TOP_K_SIMILAR = 5;
|
|
29
|
+
/**
|
|
30
|
+
* Top-level entry point. Returns a zero-counters result when the feature is
|
|
31
|
+
* disabled or no validation-tier runner is configured.
|
|
32
|
+
*/
|
|
33
|
+
export async function runStalenessDetectionPass(config, sources, signal, db) {
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
const result = {
|
|
36
|
+
considered: 0,
|
|
37
|
+
deprecated: 0,
|
|
38
|
+
confirmed: 0,
|
|
39
|
+
skipped: 0,
|
|
40
|
+
durationMs: 0,
|
|
41
|
+
warnings: [],
|
|
42
|
+
};
|
|
43
|
+
// Feature gate — default OFF.
|
|
44
|
+
if (!isProcessEnabled("index", "staleness_detection", config)) {
|
|
45
|
+
result.durationMs = Date.now() - start;
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
// The pass only writes to the primary (writable) stash. Read-only sources
|
|
49
|
+
// would be clobbered by the next sync(), so we skip them entirely.
|
|
50
|
+
const primary = sources[0];
|
|
51
|
+
if (!primary) {
|
|
52
|
+
result.durationMs = Date.now() - start;
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
const runner = resolveValidationRunner(config);
|
|
56
|
+
if (!runner) {
|
|
57
|
+
result.warnings.push("staleness_detection: no validation runner configured; skipping pass");
|
|
58
|
+
result.durationMs = Date.now() - start;
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
if (runner.kind !== "llm") {
|
|
62
|
+
// MVP scope: only the LLM runner kind is supported. Agent/SDK runners
|
|
63
|
+
// would require a different prompt-dispatch path that is out of scope
|
|
64
|
+
// for the initial Phase 4A implementation.
|
|
65
|
+
result.warnings.push(`staleness_detection: validation runner kind "${runner.kind}" not supported by MVP; configure an llm-kind validation profile`);
|
|
66
|
+
result.durationMs = Date.now() - start;
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
const configuredThreshold = config.index?.stalenessDetection?.thresholdDays;
|
|
70
|
+
const thresholdDays = typeof configuredThreshold === "number" && configuredThreshold >= 0 ? configuredThreshold : DEFAULT_THRESHOLD_DAYS;
|
|
71
|
+
const thresholdMs = thresholdDays * 24 * 60 * 60 * 1000;
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const candidates = collectStaleCandidates(primary.path, now, thresholdMs);
|
|
74
|
+
result.considered = candidates.length;
|
|
75
|
+
if (candidates.length === 0) {
|
|
76
|
+
result.durationMs = Date.now() - start;
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
const allMemories = collectAllMemoriesForSimilarity(primary.path);
|
|
80
|
+
const nowIso = new Date(now).toISOString();
|
|
81
|
+
const concurrency = runner.connection.concurrency ?? 1;
|
|
82
|
+
const validate = (raw) => {
|
|
83
|
+
if (!raw || typeof raw !== "object")
|
|
84
|
+
return undefined;
|
|
85
|
+
const r = raw;
|
|
86
|
+
if (r.decision === "deprecated") {
|
|
87
|
+
if (typeof r.supersededBy === "string" && r.supersededBy.trim().length > 0) {
|
|
88
|
+
return { decision: "deprecated", supersededBy: r.supersededBy.trim() };
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
if (r.decision === "confirmed")
|
|
93
|
+
return { decision: "confirmed" };
|
|
94
|
+
return undefined;
|
|
95
|
+
};
|
|
96
|
+
const perResult = await concurrentMap(candidates, async (candidate) => {
|
|
97
|
+
if (signal?.aborted)
|
|
98
|
+
return undefined;
|
|
99
|
+
const cacheKey = candidate.filePath;
|
|
100
|
+
const cacheBody = `${candidate.filePath}\n${candidate.body}`;
|
|
101
|
+
const decision = db
|
|
102
|
+
? await withLlmCache(db, cacheKey, cacheBody, false, () => askValidator(runner.connection, candidate, allMemories, signal, runner.timeoutMs), validate, undefined, CACHE_VARIANT)
|
|
103
|
+
: await askValidator(runner.connection, candidate, allMemories, signal, runner.timeoutMs);
|
|
104
|
+
return { candidate, decision };
|
|
105
|
+
}, concurrency);
|
|
106
|
+
for (const entry of perResult) {
|
|
107
|
+
if (!entry)
|
|
108
|
+
continue;
|
|
109
|
+
const { candidate, decision } = entry;
|
|
110
|
+
if (!decision) {
|
|
111
|
+
result.skipped += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (decision.decision === "deprecated") {
|
|
115
|
+
const targetRef = decision.supersededBy ?? "";
|
|
116
|
+
const validatedRef = validateSupersedingRef(targetRef, primary.path, db);
|
|
117
|
+
if (!validatedRef) {
|
|
118
|
+
// Spec line 153: never mark deprecated unless SUPERSEDED_BY exists.
|
|
119
|
+
// Refresh lastConfirmedAt instead — the candidate is still our best
|
|
120
|
+
// record until a real superseder shows up — and emit a warning.
|
|
121
|
+
result.warnings.push(`staleness_detection: ${candidate.ref} reported superseded by "${targetRef}" but that ref does not exist; refreshing instead`);
|
|
122
|
+
try {
|
|
123
|
+
writeLastConfirmed(candidate, nowIso);
|
|
124
|
+
result.confirmed += 1;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
result.warnings.push(`staleness_detection: failed to refresh ${candidate.ref}: ${err instanceof Error ? err.message : String(err)}`);
|
|
128
|
+
result.skipped += 1;
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
writeDeprecated(candidate, validatedRef, nowIso);
|
|
134
|
+
result.deprecated += 1;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
result.warnings.push(`staleness_detection: failed to deprecate ${candidate.ref}: ${err instanceof Error ? err.message : String(err)}`);
|
|
138
|
+
result.skipped += 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
try {
|
|
143
|
+
writeLastConfirmed(candidate, nowIso);
|
|
144
|
+
result.confirmed += 1;
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
result.warnings.push(`staleness_detection: failed to refresh ${candidate.ref}: ${err instanceof Error ? err.message : String(err)}`);
|
|
148
|
+
result.skipped += 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
result.durationMs = Date.now() - start;
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
function collectStaleCandidates(stashRoot, now, thresholdMs) {
|
|
156
|
+
const memoriesDir = path.join(stashRoot, "memories");
|
|
157
|
+
if (!fs.existsSync(memoriesDir))
|
|
158
|
+
return [];
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const filePath of walkMarkdownFiles(memoriesDir)) {
|
|
161
|
+
let raw;
|
|
162
|
+
let stat;
|
|
163
|
+
try {
|
|
164
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
165
|
+
stat = fs.statSync(filePath);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const parsed = parseFrontmatter(raw);
|
|
171
|
+
const belief = typeof parsed.data[FM_BELIEF_STATE] === "string" ? parsed.data[FM_BELIEF_STATE] : "";
|
|
172
|
+
if (EXCLUDED_BELIEF_STATES.has(belief))
|
|
173
|
+
continue;
|
|
174
|
+
const lastConfirmedMs = parseDateMs(parsed.data[FM_LAST_CONFIRMED_AT]);
|
|
175
|
+
const signalMs = lastConfirmedMs ?? stat.mtimeMs;
|
|
176
|
+
const ageMs = now - signalMs;
|
|
177
|
+
if (ageMs < thresholdMs)
|
|
178
|
+
continue;
|
|
179
|
+
const name = toMemoryName(memoriesDir, filePath);
|
|
180
|
+
if (!name)
|
|
181
|
+
continue;
|
|
182
|
+
out.push({
|
|
183
|
+
filePath,
|
|
184
|
+
ref: `memory:${name}`,
|
|
185
|
+
name,
|
|
186
|
+
data: parsed.data,
|
|
187
|
+
body: parsed.content,
|
|
188
|
+
lastSignalMs: signalMs,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
function collectAllMemoriesForSimilarity(stashRoot) {
|
|
194
|
+
const memoriesDir = path.join(stashRoot, "memories");
|
|
195
|
+
if (!fs.existsSync(memoriesDir))
|
|
196
|
+
return [];
|
|
197
|
+
const out = [];
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
for (const filePath of walkMarkdownFiles(memoriesDir)) {
|
|
200
|
+
let raw;
|
|
201
|
+
let stat;
|
|
202
|
+
try {
|
|
203
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
204
|
+
stat = fs.statSync(filePath);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const parsed = parseFrontmatter(raw);
|
|
210
|
+
const name = toMemoryName(memoriesDir, filePath);
|
|
211
|
+
if (!name)
|
|
212
|
+
continue;
|
|
213
|
+
const title = typeof parsed.data.title === "string" ? parsed.data.title : "";
|
|
214
|
+
const description = typeof parsed.data.description === "string" ? parsed.data.description : "";
|
|
215
|
+
const body = parsed.content;
|
|
216
|
+
out.push({
|
|
217
|
+
ref: `memory:${name}`,
|
|
218
|
+
name,
|
|
219
|
+
filePath,
|
|
220
|
+
body,
|
|
221
|
+
title,
|
|
222
|
+
description,
|
|
223
|
+
tokens: tokenize(`${title} ${description} ${body}`),
|
|
224
|
+
ageMs: now - stat.mtimeMs,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
function toMemoryName(memoriesDir, filePath) {
|
|
230
|
+
const rel = path.relative(memoriesDir, filePath);
|
|
231
|
+
if (!rel || rel.startsWith(".."))
|
|
232
|
+
return undefined;
|
|
233
|
+
return rel.replace(/\\/g, "/").replace(/\.md$/i, "");
|
|
234
|
+
}
|
|
235
|
+
function parseDateMs(value) {
|
|
236
|
+
if (typeof value !== "string")
|
|
237
|
+
return undefined;
|
|
238
|
+
const ms = Date.parse(value);
|
|
239
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
240
|
+
}
|
|
241
|
+
// ── Similarity (lightweight FTS-style fallback) ─────────────────────────────
|
|
242
|
+
/**
|
|
243
|
+
* Token-overlap similarity is the FTS fallback for the prompt's "similar
|
|
244
|
+
* memories" payload. Embedding-aware nearest-neighbor lookup is a future
|
|
245
|
+
* enhancement; the prompt only needs enough context to ground a YES/NO
|
|
246
|
+
* decision, which token overlap of body + description provides.
|
|
247
|
+
*/
|
|
248
|
+
function tokenize(text) {
|
|
249
|
+
const out = new Set();
|
|
250
|
+
const lower = text.toLowerCase();
|
|
251
|
+
const tokens = lower.match(/[a-z0-9][a-z0-9_-]{2,}/g);
|
|
252
|
+
if (!tokens)
|
|
253
|
+
return out;
|
|
254
|
+
for (const t of tokens)
|
|
255
|
+
out.add(t);
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
function pickSimilar(candidate, all) {
|
|
259
|
+
const candTokens = tokenize(`${typeof candidate.data.title === "string" ? candidate.data.title : ""} ${typeof candidate.data.description === "string" ? candidate.data.description : ""} ${candidate.body}`);
|
|
260
|
+
const candMs = candidate.lastSignalMs;
|
|
261
|
+
const scored = [];
|
|
262
|
+
for (const snap of all) {
|
|
263
|
+
if (snap.ref === candidate.ref)
|
|
264
|
+
continue;
|
|
265
|
+
// Prefer memories more recent than the candidate so the validator can
|
|
266
|
+
// see what may have superseded it.
|
|
267
|
+
if (snap.ageMs >= Date.now() - candMs)
|
|
268
|
+
continue;
|
|
269
|
+
let overlap = 0;
|
|
270
|
+
for (const t of candTokens)
|
|
271
|
+
if (snap.tokens.has(t))
|
|
272
|
+
overlap += 1;
|
|
273
|
+
if (overlap === 0)
|
|
274
|
+
continue;
|
|
275
|
+
scored.push({ snap, score: overlap });
|
|
276
|
+
}
|
|
277
|
+
scored.sort((a, b) => b.score - a.score);
|
|
278
|
+
return scored.slice(0, TOP_K_SIMILAR).map((s) => s.snap);
|
|
279
|
+
}
|
|
280
|
+
// ── LLM dispatch ────────────────────────────────────────────────────────────
|
|
281
|
+
const SYSTEM_PROMPT = "You are a belief-state classifier for a memory store. Given a candidate memory and a list of more-recent similar memories from the same store, decide whether the candidate is still current or has been superseded.\n\n" +
|
|
282
|
+
"Respond on the first line with exactly YES or NO.\n" +
|
|
283
|
+
"If YES, the second line MUST be of the form `SUPERSEDED_BY: <ref>` where <ref> is the exact ref of the superseding memory from the list provided. Do NOT invent refs.\n" +
|
|
284
|
+
"If NO, do not include any additional lines.\n" +
|
|
285
|
+
"No prose, no preamble, no markdown.";
|
|
286
|
+
async function askValidator(connection, candidate, allMemories, signal, timeoutMs) {
|
|
287
|
+
const similar = pickSimilar(candidate, allMemories);
|
|
288
|
+
if (similar.length === 0) {
|
|
289
|
+
// No more-recent similar memories — there is nothing the candidate could
|
|
290
|
+
// have been superseded by. Treat as confirmed without paying for an LLM call.
|
|
291
|
+
return { decision: "confirmed" };
|
|
292
|
+
}
|
|
293
|
+
const messages = [
|
|
294
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
295
|
+
{ role: "user", content: buildPrompt(candidate, similar) },
|
|
296
|
+
];
|
|
297
|
+
let raw;
|
|
298
|
+
try {
|
|
299
|
+
raw = await chatCompletion(connection, messages, {
|
|
300
|
+
...(typeof timeoutMs === "number" ? { timeoutMs } : {}),
|
|
301
|
+
...(signal ? { signal } : {}),
|
|
302
|
+
temperature: 0,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
warn(`[improve] staleness detection LLM call failed for ${candidate.ref}: ${err instanceof Error ? err.message : String(err)}`);
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
return parseStalenessResponse(raw);
|
|
310
|
+
}
|
|
311
|
+
export function buildPrompt(candidate, similar) {
|
|
312
|
+
const lines = [];
|
|
313
|
+
lines.push(`Candidate memory: ${candidate.ref}`);
|
|
314
|
+
if (typeof candidate.data.title === "string" && candidate.data.title.trim().length > 0) {
|
|
315
|
+
lines.push(`Title: ${candidate.data.title.trim()}`);
|
|
316
|
+
}
|
|
317
|
+
if (typeof candidate.data.description === "string" && candidate.data.description.trim().length > 0) {
|
|
318
|
+
lines.push(`Description: ${candidate.data.description.trim()}`);
|
|
319
|
+
}
|
|
320
|
+
lines.push("Body:");
|
|
321
|
+
lines.push(candidate.body.trim());
|
|
322
|
+
lines.push("");
|
|
323
|
+
lines.push(`Similar more-recent memories (top ${similar.length}):`);
|
|
324
|
+
for (const s of similar) {
|
|
325
|
+
lines.push("---");
|
|
326
|
+
lines.push(`Ref: ${s.ref}`);
|
|
327
|
+
if (s.title)
|
|
328
|
+
lines.push(`Title: ${s.title}`);
|
|
329
|
+
if (s.description)
|
|
330
|
+
lines.push(`Description: ${s.description}`);
|
|
331
|
+
lines.push("Body:");
|
|
332
|
+
lines.push(s.body.trim());
|
|
333
|
+
}
|
|
334
|
+
lines.push("");
|
|
335
|
+
lines.push("Question: Given the more-recent similar memories above, has the candidate memory been superseded, or is it still current?");
|
|
336
|
+
return lines.join("\n");
|
|
337
|
+
}
|
|
338
|
+
/** Exported for direct unit testing. */
|
|
339
|
+
export function parseStalenessResponse(raw) {
|
|
340
|
+
if (typeof raw !== "string")
|
|
341
|
+
return undefined;
|
|
342
|
+
const trimmed = raw.trim();
|
|
343
|
+
if (trimmed.length === 0)
|
|
344
|
+
return undefined;
|
|
345
|
+
const lines = trimmed
|
|
346
|
+
.split(/\r?\n/)
|
|
347
|
+
.map((l) => l.trim())
|
|
348
|
+
.filter((l) => l.length > 0);
|
|
349
|
+
if (lines.length === 0)
|
|
350
|
+
return undefined;
|
|
351
|
+
const head = lines[0]?.toUpperCase();
|
|
352
|
+
if (head === "NO")
|
|
353
|
+
return { decision: "confirmed" };
|
|
354
|
+
if (head === "YES") {
|
|
355
|
+
// Find the SUPERSEDED_BY line; tolerate it being anywhere in the body
|
|
356
|
+
// but require an exact prefix match.
|
|
357
|
+
for (const line of lines.slice(1)) {
|
|
358
|
+
const m = line.match(/^SUPERSEDED_BY:\s*(\S.*)$/i);
|
|
359
|
+
if (m?.[1]) {
|
|
360
|
+
const ref = m[1].trim();
|
|
361
|
+
if (ref.length > 0)
|
|
362
|
+
return { decision: "deprecated", supersededBy: ref };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return undefined; // YES without a SUPERSEDED_BY line is a parse error.
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
// ── Ref validation ──────────────────────────────────────────────────────────
|
|
370
|
+
/**
|
|
371
|
+
* Validate that the proposed `supersededBy` ref actually exists. We first try
|
|
372
|
+
* the indexed DB (canonical source of truth when available), then fall back to
|
|
373
|
+
* an on-disk filesystem probe under `<stash>/memories/<name>.md` so the pass
|
|
374
|
+
* works even on stashes that have never been indexed.
|
|
375
|
+
*/
|
|
376
|
+
function validateSupersedingRef(refStr, stashRoot, db) {
|
|
377
|
+
const trimmed = refStr.trim();
|
|
378
|
+
if (!trimmed)
|
|
379
|
+
return undefined;
|
|
380
|
+
if (db) {
|
|
381
|
+
try {
|
|
382
|
+
const id = findEntryIdByRef(db, trimmed);
|
|
383
|
+
if (typeof id === "number")
|
|
384
|
+
return trimmed;
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Fall through to filesystem probe.
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const m = trimmed.match(/^memory:(.+)$/);
|
|
391
|
+
if (!m)
|
|
392
|
+
return undefined;
|
|
393
|
+
const filePath = path.join(stashRoot, "memories", `${m[1]}.md`);
|
|
394
|
+
if (fs.existsSync(filePath))
|
|
395
|
+
return trimmed;
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
// ── Frontmatter writes ──────────────────────────────────────────────────────
|
|
399
|
+
/**
|
|
400
|
+
* Write `beliefState: deprecated`, `supersededBy`, and `lastConfirmedAt` to
|
|
401
|
+
* the candidate's frontmatter. All other fields are preserved verbatim. Uses
|
|
402
|
+
* the same atomic-write shape as `markParentProcessed()` in
|
|
403
|
+
* `memory-inference.ts`: re-read the file from disk, parse the YAML block,
|
|
404
|
+
* stitch a new block in front of the original body bytes.
|
|
405
|
+
*/
|
|
406
|
+
function writeDeprecated(candidate, supersededByRef, nowIso) {
|
|
407
|
+
const raw = fs.readFileSync(candidate.filePath, "utf8");
|
|
408
|
+
const block = parseFrontmatterBlock(raw);
|
|
409
|
+
const baseFm = block ? { ...parseFrontmatter(raw).data } : {};
|
|
410
|
+
const nextFm = {
|
|
411
|
+
...baseFm,
|
|
412
|
+
[FM_BELIEF_STATE]: "deprecated",
|
|
413
|
+
[FM_SUPERSEDED_BY]: dedupeStringArray([...stringArrayOrEmpty(baseFm[FM_SUPERSEDED_BY]), supersededByRef]),
|
|
414
|
+
[FM_LAST_CONFIRMED_AT]: nowIso,
|
|
415
|
+
};
|
|
416
|
+
writeFrontmatterAtomic(candidate.filePath, nextFm, block?.content ?? raw);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Strict additive frontmatter write: ONLY `lastConfirmedAt` is touched.
|
|
420
|
+
* Every other field in the file is preserved as-is.
|
|
421
|
+
*/
|
|
422
|
+
function writeLastConfirmed(candidate, nowIso) {
|
|
423
|
+
const raw = fs.readFileSync(candidate.filePath, "utf8");
|
|
424
|
+
const block = parseFrontmatterBlock(raw);
|
|
425
|
+
const baseFm = block ? { ...parseFrontmatter(raw).data } : {};
|
|
426
|
+
const nextFm = {
|
|
427
|
+
...baseFm,
|
|
428
|
+
[FM_LAST_CONFIRMED_AT]: nowIso,
|
|
429
|
+
};
|
|
430
|
+
writeFrontmatterAtomic(candidate.filePath, nextFm, block?.content ?? raw);
|
|
431
|
+
}
|
|
432
|
+
function writeFrontmatterAtomic(filePath, frontmatter, body) {
|
|
433
|
+
fs.writeFileSync(filePath, assembleAsset(frontmatter, body), "utf8");
|
|
434
|
+
}
|
|
435
|
+
function stringArrayOrEmpty(value) {
|
|
436
|
+
if (!Array.isArray(value))
|
|
437
|
+
return [];
|
|
438
|
+
return value.filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
439
|
+
}
|
|
440
|
+
function dedupeStringArray(values) {
|
|
441
|
+
return [...new Set(values.map((v) => v.trim()).filter((v) => v.length > 0))];
|
|
442
|
+
}
|
|
443
|
+
// ── Body hash helper (exported for tests) ───────────────────────────────────
|
|
444
|
+
/** Internal helper exported only for testing — mirrors `computeBodyHash`. */
|
|
445
|
+
export function _stalenessBodyHash(filePath, body) {
|
|
446
|
+
return createHash("sha256").update(`${filePath}\n${body}`).digest("hex");
|
|
447
|
+
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Schema (created by ensureUsageEventsSchema):
|
|
5
|
-
* id, event_type, query, entry_id (nullable), entry_ref, signal, metadata, created_at
|
|
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/.
|
|
7
4
|
// ── Schema ──────────────────────────────────────────────────────────────────
|
|
8
5
|
export function ensureUsageEventsSchema(db) {
|
|
9
6
|
db.exec(`
|
|
@@ -15,11 +12,13 @@ export function ensureUsageEventsSchema(db) {
|
|
|
15
12
|
entry_ref TEXT,
|
|
16
13
|
signal TEXT,
|
|
17
14
|
metadata TEXT,
|
|
15
|
+
source TEXT NOT NULL DEFAULT 'user',
|
|
18
16
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
19
17
|
);
|
|
20
18
|
CREATE INDEX IF NOT EXISTS idx_usage_events_entry ON usage_events(entry_id);
|
|
21
19
|
CREATE INDEX IF NOT EXISTS idx_usage_events_type ON usage_events(event_type);
|
|
22
20
|
CREATE INDEX IF NOT EXISTS idx_usage_events_ref ON usage_events(entry_ref);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_source ON usage_events(source);
|
|
23
22
|
`);
|
|
24
23
|
}
|
|
25
24
|
// ── Insert ───────────────────────────────────────────────────────────────────
|
|
@@ -29,8 +28,8 @@ export function ensureUsageEventsSchema(db) {
|
|
|
29
28
|
*/
|
|
30
29
|
export function insertUsageEvent(db, event) {
|
|
31
30
|
try {
|
|
32
|
-
db.prepare(`INSERT INTO usage_events (event_type, query, entry_id, entry_ref, signal, metadata)
|
|
33
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(event.event_type, event.query ?? null, event.entry_id ?? null, event.entry_ref ?? null, event.signal ?? null, event.metadata ?? null);
|
|
31
|
+
db.prepare(`INSERT INTO usage_events (event_type, query, entry_id, entry_ref, signal, metadata, source)
|
|
32
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(event.event_type, event.query ?? null, event.entry_id ?? null, event.entry_ref ?? null, event.signal ?? null, event.metadata ?? null, event.source ?? "user");
|
|
34
33
|
}
|
|
35
34
|
catch {
|
|
36
35
|
/* fire-and-forget: silently ignore errors */
|
|
@@ -51,8 +50,12 @@ export function getUsageEvents(db, filters) {
|
|
|
51
50
|
conditions.push("entry_ref = ?");
|
|
52
51
|
params.push(filters.entry_ref);
|
|
53
52
|
}
|
|
53
|
+
if (filters?.source) {
|
|
54
|
+
conditions.push("source = ?");
|
|
55
|
+
params.push(filters.source);
|
|
56
|
+
}
|
|
54
57
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
55
|
-
const sql = `SELECT id, event_type, query, entry_id, entry_ref, signal, metadata, created_at
|
|
58
|
+
const sql = `SELECT id, event_type, query, entry_id, entry_ref, signal, metadata, source, created_at
|
|
56
59
|
FROM usage_events ${where}
|
|
57
60
|
ORDER BY id ASC`;
|
|
58
61
|
return db.prepare(sql).all(...params);
|
package/dist/indexer/walker.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
|
* Shared filesystem walker for akm stash directories.
|
|
3
6
|
*
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
* Agent command builder strategy (v1 spec §12.2).
|
|
6
|
+
*
|
|
7
|
+
* Each supported agent CLI platform has its own `AgentCommandBuilder` that
|
|
8
|
+
* translates a platform-agnostic `AgentDispatchRequest` into the exact argv
|
|
9
|
+
* the CLI expects. This keeps all per-platform arg differences out of the
|
|
10
|
+
* spawn wrapper and profiles.
|
|
11
|
+
*
|
|
12
|
+
* Adding a new platform: implement `AgentCommandBuilder`, add to
|
|
13
|
+
* `BUILTIN_BUILDERS`. Nothing else changes.
|
|
14
|
+
*/
|
|
15
|
+
import { UsageError } from "../../core/errors";
|
|
16
|
+
import { resolveModel } from "./model-aliases";
|
|
17
|
+
// ── Validation helpers ────────────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Guard against values that start with `--`, which would be mis-interpreted as
|
|
20
|
+
* CLI flags by the spawned process when used as flag values (model, systemPrompt).
|
|
21
|
+
* Bun.spawn uses array argv so there is no shell injection, but a `--`-prefixed
|
|
22
|
+
* value passed as the argument to `--model` or `--system-prompt` can still
|
|
23
|
+
* confuse the CLI parser of the target process.
|
|
24
|
+
*/
|
|
25
|
+
function assertNotFlag(value, field) {
|
|
26
|
+
if (value?.trimStart().startsWith("--")) {
|
|
27
|
+
throw new UsageError(`${field} must not start with "--": ${JSON.stringify(value.slice(0, 60))}`, "INVALID_FLAG_VALUE");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// ── Tool normalization ────────────────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* Normalize a toolPolicy value to a comma-separated string suitable for a
|
|
33
|
+
* CLI flag. Structured policy objects are JSON-serialized.
|
|
34
|
+
*/
|
|
35
|
+
function normalizeTools(tools) {
|
|
36
|
+
if (typeof tools === "string")
|
|
37
|
+
return tools;
|
|
38
|
+
if (Array.isArray(tools))
|
|
39
|
+
return tools.join(",");
|
|
40
|
+
return JSON.stringify(tools);
|
|
41
|
+
}
|
|
42
|
+
// ── Platform builders ─────────────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* OpenCode builder.
|
|
45
|
+
* Command shape: opencode run [--system-prompt "..."] [--model <m>] "<prompt>"
|
|
46
|
+
*
|
|
47
|
+
* Tool policy is omitted — opencode manages tool access through its own agent
|
|
48
|
+
* config files, not via CLI flags.
|
|
49
|
+
*/
|
|
50
|
+
const opencodeBuilder = {
|
|
51
|
+
platform: "opencode",
|
|
52
|
+
build(profile, req) {
|
|
53
|
+
assertNotFlag(req.systemPrompt, "systemPrompt");
|
|
54
|
+
assertNotFlag(req.model, "model");
|
|
55
|
+
const args = [...profile.args]; // starts with ["run"]
|
|
56
|
+
if (req.systemPrompt) {
|
|
57
|
+
args.push("--system-prompt", req.systemPrompt);
|
|
58
|
+
}
|
|
59
|
+
if (req.model) {
|
|
60
|
+
const resolved = resolveModel(req.model, "opencode", profile.modelAliases);
|
|
61
|
+
args.push("--model", resolved);
|
|
62
|
+
}
|
|
63
|
+
args.push("--");
|
|
64
|
+
args.push(req.prompt);
|
|
65
|
+
return { argv: [profile.bin, ...args] };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Claude Code builder.
|
|
70
|
+
* Command shape: claude [--system-prompt "..."] [--model <m>] [--allowedTools <t>] --print "<prompt>"
|
|
71
|
+
*
|
|
72
|
+
* --print switches Claude Code to non-interactive captured output mode.
|
|
73
|
+
*/
|
|
74
|
+
const claudeBuilder = {
|
|
75
|
+
platform: "claude",
|
|
76
|
+
build(profile, req) {
|
|
77
|
+
assertNotFlag(req.systemPrompt, "systemPrompt");
|
|
78
|
+
assertNotFlag(req.model, "model");
|
|
79
|
+
const args = [...profile.args];
|
|
80
|
+
if (req.systemPrompt) {
|
|
81
|
+
args.push("--system-prompt", req.systemPrompt);
|
|
82
|
+
}
|
|
83
|
+
if (req.model) {
|
|
84
|
+
const resolved = resolveModel(req.model, "claude", profile.modelAliases);
|
|
85
|
+
args.push("--model", resolved);
|
|
86
|
+
}
|
|
87
|
+
if (req.tools) {
|
|
88
|
+
args.push("--allowedTools", normalizeTools(req.tools));
|
|
89
|
+
}
|
|
90
|
+
// --print = non-interactive, outputs to stdout — required for captured mode
|
|
91
|
+
args.push("--print");
|
|
92
|
+
args.push("--");
|
|
93
|
+
args.push(req.prompt);
|
|
94
|
+
return { argv: [profile.bin, ...args] };
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Default builder — used for custom profiles and any platform without a
|
|
99
|
+
* dedicated builder. Passes systemPrompt and model via the same flags as
|
|
100
|
+
* the builtin builders so custom profiles benefit from agent asset metadata.
|
|
101
|
+
* Tools are omitted — no standard cross-platform flag exists.
|
|
102
|
+
*/
|
|
103
|
+
const defaultBuilder = {
|
|
104
|
+
platform: "default",
|
|
105
|
+
build(profile, req) {
|
|
106
|
+
assertNotFlag(req.systemPrompt, "systemPrompt");
|
|
107
|
+
assertNotFlag(req.model, "model");
|
|
108
|
+
const args = [...profile.args];
|
|
109
|
+
if (req.systemPrompt) {
|
|
110
|
+
args.push("--system-prompt", req.systemPrompt);
|
|
111
|
+
}
|
|
112
|
+
if (req.model) {
|
|
113
|
+
const resolved = resolveModel(req.model, profile.name, profile.modelAliases);
|
|
114
|
+
args.push("--model", resolved);
|
|
115
|
+
}
|
|
116
|
+
args.push("--");
|
|
117
|
+
args.push(req.prompt);
|
|
118
|
+
return { argv: [profile.bin, ...args] };
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
// ── Registry ──────────────────────────────────────────────────────────────────
|
|
122
|
+
const BUILTIN_BUILDERS = {
|
|
123
|
+
opencode: opencodeBuilder,
|
|
124
|
+
"opencode-headless": opencodeBuilder,
|
|
125
|
+
claude: claudeBuilder,
|
|
126
|
+
"claude-headless": claudeBuilder,
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Return the builder for the given platform name, falling back to the default
|
|
130
|
+
* builder for unknown platforms. Custom builders injected via tests can be
|
|
131
|
+
* passed as `registry`.
|
|
132
|
+
*/
|
|
133
|
+
export function getCommandBuilder(platform, registry = BUILTIN_BUILDERS) {
|
|
134
|
+
return registry[platform] ?? defaultBuilder;
|
|
135
|
+
}
|