akm-cli 0.9.0-beta.5 → 0.9.0-beta.50
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 +709 -0
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +6 -2
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +17 -10
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +15 -5
- package/dist/commands/agent/agent-dispatch.js +2 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +17 -4
- package/dist/commands/env/env-cli.js +16 -24
- package/dist/commands/env/secret-cli.js +12 -20
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- package/dist/commands/health/checks.js +48 -0
- package/dist/commands/health/html-report.js +422 -80
- package/dist/commands/health.js +386 -9
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +81 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +635 -660
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +159 -69
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +124 -2
- package/dist/commands/improve/extract-prompt.js +39 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +389 -40
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +109 -6
- package/dist/commands/improve/improve-cli.js +35 -60
- package/dist/commands/improve/improve-profiles.js +14 -0
- package/dist/commands/improve/improve-result-file.js +5 -23
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2498
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1083 -0
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/preparation.js +1966 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +418 -0
- package/dist/commands/improve/recombine.js +813 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +183 -40
- package/dist/commands/improve/salience.js +438 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +34 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain-policies.js +5 -0
- package/dist/commands/proposal/drain.js +43 -50
- package/dist/commands/proposal/proposal-cli.js +21 -31
- package/dist/commands/proposal/proposal.js +5 -0
- package/dist/commands/proposal/propose.js +7 -2
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +189 -63
- package/dist/commands/read/curate.js +414 -94
- package/dist/commands/read/knowledge.js +2 -2
- package/dist/commands/read/search-cli.js +7 -0
- package/dist/commands/read/search.js +1 -0
- package/dist/commands/read/show.js +67 -2
- package/dist/commands/sources/init.js +36 -9
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/schema-repair.js +13 -1
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/stash-cli.js +28 -40
- package/dist/commands/sources/stash-skeleton.js +23 -8
- package/dist/commands/tasks/tasks-cli.js +19 -27
- package/dist/commands/tasks/tasks.js +1 -1
- package/dist/commands/wiki-cli.js +21 -35
- package/dist/core/asset/asset-registry.js +2 -0
- package/dist/core/asset/asset-spec.js +14 -0
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +0 -5
- package/dist/core/config/config-schema.js +340 -56
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +28 -7
- package/dist/core/events.js +3 -7
- package/dist/core/improve-types.js +11 -8
- package/dist/core/logs-db.js +10 -66
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +3 -0
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +714 -0
- package/dist/core/state-db.js +525 -474
- package/dist/indexer/db/db.js +439 -247
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +218 -4
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +123 -221
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +10 -3
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +15 -4
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/search/search-source.js +10 -24
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/config.js +6 -53
- package/dist/integrations/agent/index.js +2 -18
- package/dist/integrations/agent/prompts.js +74 -8
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +45 -15
- package/dist/llm/embedder.js +42 -3
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/feature-gate.js +8 -4
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer.js +38 -30
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +73 -1
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/passthrough.js +0 -1
- package/dist/output/text/helpers.js +16 -1
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +22 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2136 -1596
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +682 -433
- package/dist/setup/setup.js +29 -8
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +206 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/sources/providers/website.js +2 -3
- package/dist/sources/website-ingest.js +51 -9
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/registry-cache.js +92 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +5 -13
- package/dist/wiki/wiki.js +37 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +1 -117
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +1 -0
- package/package.json +9 -7
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
|
@@ -20,6 +20,7 @@ export function normalizeFtsScores(results) {
|
|
|
20
20
|
export function combineSearchScores(options) {
|
|
21
21
|
const FTS_WEIGHT = 0.7;
|
|
22
22
|
const VEC_WEIGHT = 0.3;
|
|
23
|
+
const excludeTypeSet = options.excludeTypes && options.excludeTypes.length > 0 ? new Set(options.excludeTypes) : null;
|
|
23
24
|
const scored = [];
|
|
24
25
|
const seenIds = new Set();
|
|
25
26
|
for (const [id, { score: ftsScore, result }] of options.ftsScoreMap) {
|
|
@@ -42,6 +43,9 @@ export function combineSearchScores(options) {
|
|
|
42
43
|
continue;
|
|
43
44
|
if (options.typeFilter && found.entry.type !== options.typeFilter)
|
|
44
45
|
continue;
|
|
46
|
+
// #627 — drop vector-only neighbors whose type is excluded on the default path.
|
|
47
|
+
if (excludeTypeSet?.has(found.entry.type))
|
|
48
|
+
continue;
|
|
45
49
|
scored.push({
|
|
46
50
|
id,
|
|
47
51
|
entry: found.entry,
|
|
@@ -5,13 +5,11 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { resolveStashDir } from "../../core/common.js";
|
|
7
7
|
import { getSources, loadConfig } from "../../core/config/config.js";
|
|
8
|
-
import { resolveSourceProviderFactory } from "../../sources/provider-factory.js";
|
|
8
|
+
import { resolveSourceProviderFactory, resolveSourceProviders } from "../../sources/provider-factory.js";
|
|
9
9
|
// Eager side-effect imports so all built-in source providers self-register
|
|
10
10
|
// before resolveEntryContentDir() runs.
|
|
11
11
|
import "../../sources/providers/index.js";
|
|
12
12
|
import { warn } from "../../core/warn.js";
|
|
13
|
-
import { ensureGitMirror, getCachePaths, parseGitRepoUrl } from "../../sources/providers/git.js";
|
|
14
|
-
import { ensureWebsiteMirror } from "../../sources/website-ingest.js";
|
|
15
13
|
// Legacy "context-hub" / "github" type aliases are normalized to "git" at
|
|
16
14
|
// config-load time (see src/config.ts), so this set only contains the canonical
|
|
17
15
|
// type.
|
|
@@ -261,31 +259,19 @@ function isValidDirectory(dir) {
|
|
|
261
259
|
export async function ensureSourceCaches(config, options) {
|
|
262
260
|
const cfg = config ?? loadConfig();
|
|
263
261
|
const force = options?.force === true;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
// Polymorphic refresh: walk every enabled source through its registered
|
|
263
|
+
// provider and call `sync()`. Every cache-backed kind (git, website, npm)
|
|
264
|
+
// refreshes the same way — a bad source warns and is skipped without
|
|
265
|
+
// aborting the others. The git content/-subdir layout convention stays in
|
|
266
|
+
// resolveEntryContentDir.
|
|
267
|
+
for (const provider of resolveSourceProviders(cfg)) {
|
|
268
|
+
if (!provider.sync)
|
|
267
269
|
continue;
|
|
268
270
|
try {
|
|
269
|
-
|
|
270
|
-
const cachePaths = getCachePaths(repo.canonicalUrl);
|
|
271
|
-
await ensureGitMirror(repo, cachePaths, {
|
|
272
|
-
requireRepoDir: true,
|
|
273
|
-
writable: entry.writable === true,
|
|
274
|
-
force,
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
catch (err) {
|
|
278
|
-
warn(`Warning: failed to refresh git mirror for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
for (const entry of entries) {
|
|
282
|
-
if (entry.type !== "website" || !entry.url || entry.enabled === false)
|
|
283
|
-
continue;
|
|
284
|
-
try {
|
|
285
|
-
await ensureWebsiteMirror(entry, { requireStashDir: true, force });
|
|
271
|
+
await provider.sync({ force });
|
|
286
272
|
}
|
|
287
273
|
catch (err) {
|
|
288
|
-
warn(`Warning: failed to refresh
|
|
274
|
+
warn(`Warning: failed to refresh ${provider.kind} source "${provider.name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
289
275
|
}
|
|
290
276
|
}
|
|
291
277
|
}
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import { writeFileAtomic } from "../../core/common.js";
|
|
6
6
|
import { getCacheDir, getSemanticStatusPath } from "../../core/paths.js";
|
|
7
|
+
import { DETERMINISTIC_EMBED_MODEL_ID, isDeterministicEmbedEnabled } from "../../llm/embedders/deterministic.js";
|
|
7
8
|
import { DEFAULT_LOCAL_MODEL } from "../../llm/embedders/local.js";
|
|
8
9
|
export function deriveSemanticProviderFingerprint(embedding) {
|
|
10
|
+
if (isDeterministicEmbedEnabled()) {
|
|
11
|
+
return `deterministic:${DETERMINISTIC_EMBED_MODEL_ID}`;
|
|
12
|
+
}
|
|
9
13
|
if (embedding?.endpoint) {
|
|
10
14
|
return `remote:${embedding.endpoint}|${embedding.model}|${embedding.dimension ?? "default"}`;
|
|
11
15
|
}
|
|
@@ -78,6 +78,15 @@ const DIR_TYPE_MAP = [
|
|
|
78
78
|
type: "session",
|
|
79
79
|
test: (ext) => ext === ".md",
|
|
80
80
|
},
|
|
81
|
+
{
|
|
82
|
+
// Durable stash-level facts live under `facts/<category>/<name>.md`.
|
|
83
|
+
// classifyByDirectory walks every ancestor dir, so nested category
|
|
84
|
+
// subdirs still match. Without this entry a fact file would fall through
|
|
85
|
+
// to classifyBySmartMd and be mistyped as `knowledge`.
|
|
86
|
+
dir: "facts",
|
|
87
|
+
type: "fact",
|
|
88
|
+
test: (ext) => ext === ".md",
|
|
89
|
+
},
|
|
81
90
|
];
|
|
82
91
|
const COMMAND_PLACEHOLDER_RE = /\$ARGUMENTS|\$[123]\b/;
|
|
83
92
|
// Files that should never be treated as the typed asset for the surrounding
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import { VALID_HARNESS_IDS } from "../../core/config/config.js";
|
|
5
4
|
import { ConfigError } from "../../core/errors.js";
|
|
6
|
-
import { warn } from "../../core/warn.js";
|
|
7
5
|
import { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile, listBuiltinAgentProfiles, } from "./profiles.js";
|
|
8
6
|
/**
|
|
9
7
|
* Default hard timeout for an agent CLI (60s — matches the value used in
|
|
@@ -41,7 +39,12 @@ export function resolveAgentProfile(name, overrides) {
|
|
|
41
39
|
stdio: base.stdio,
|
|
42
40
|
env: base.env,
|
|
43
41
|
envPassthrough: base.envPassthrough,
|
|
44
|
-
|
|
42
|
+
// Honor a user-configured `profiles.agent.<name>.timeoutMs` override; fall
|
|
43
|
+
// back to the built-in profile's value. (Previously always used base, so
|
|
44
|
+
// the documented config override was silently ignored — callers had to pass
|
|
45
|
+
// a CLI flag like `--timeout-ms`.) runAgent (spawn.ts) reads profile.timeoutMs
|
|
46
|
+
// when no per-call timeout is supplied, so this makes the config knob work.
|
|
47
|
+
timeoutMs: overrides.timeoutMs ?? base.timeoutMs,
|
|
45
48
|
parseOutput: base.parseOutput,
|
|
46
49
|
...(sdkMode ? { sdkMode: true } : {}),
|
|
47
50
|
model: overrides.model ?? base.model,
|
|
@@ -134,53 +137,3 @@ export function listResolvedAgentProfiles(config) {
|
|
|
134
137
|
}
|
|
135
138
|
return resolved;
|
|
136
139
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Parse the v2 `profiles.agent` map (AgentProfileConfigV2 shape with required
|
|
139
|
-
* `platform` field). Returns a map of profile name → AgentProfileConfigV2.
|
|
140
|
-
*/
|
|
141
|
-
export function parseAgentProfilesMapV2(value) {
|
|
142
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
143
|
-
return undefined;
|
|
144
|
-
const out = {};
|
|
145
|
-
// Derives from the canonical harness-id source of truth (#565).
|
|
146
|
-
const VALID_PLATFORMS = VALID_HARNESS_IDS;
|
|
147
|
-
for (const [name, raw] of Object.entries(value)) {
|
|
148
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
149
|
-
warn(`[akm] Ignoring profiles.agent["${name}"]: expected an object.`);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
const obj = raw;
|
|
153
|
-
if (!VALID_PLATFORMS.includes(obj.platform)) {
|
|
154
|
-
warn(`[akm] Ignoring profiles.agent["${name}"]: missing or invalid "platform" (must be one of: ${VALID_PLATFORMS.join(", ")}).`);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
const profile = {
|
|
158
|
-
platform: obj.platform,
|
|
159
|
-
};
|
|
160
|
-
if (typeof obj.bin === "string" && obj.bin.trim())
|
|
161
|
-
profile.bin = obj.bin.trim();
|
|
162
|
-
if (Array.isArray(obj.args) && obj.args.every((a) => typeof a === "string")) {
|
|
163
|
-
profile.args = obj.args;
|
|
164
|
-
}
|
|
165
|
-
if (typeof obj.workspace === "string" && obj.workspace.trim())
|
|
166
|
-
profile.workspace = obj.workspace.trim();
|
|
167
|
-
if (typeof obj.model === "string" && obj.model.trim())
|
|
168
|
-
profile.model = obj.model.trim();
|
|
169
|
-
out[name] = profile;
|
|
170
|
-
}
|
|
171
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Stub kept for source-compat with callers that previously used the v1 agent
|
|
175
|
-
* config parser. After 0.8.0 there is no separate `agent` block to parse — the
|
|
176
|
-
* loaded `AkmConfig` already carries the agent data on `profiles.agent` and
|
|
177
|
-
* `defaults.agent`. This function is a no-op alias for those callers.
|
|
178
|
-
*
|
|
179
|
-
* @deprecated v0.8.0 — the unified `AkmConfig` IS the agent config. Use the
|
|
180
|
-
* profile/defaults accessors above instead.
|
|
181
|
-
*/
|
|
182
|
-
export function parseAgentConfig(_value) {
|
|
183
|
-
// No-op: there is no separate agent block in 0.8.0. Callers should pass
|
|
184
|
-
// their loaded `AkmConfig` directly to `requireAgentProfile` etc.
|
|
185
|
-
return undefined;
|
|
186
|
-
}
|
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/**
|
|
5
|
-
* Internal entry point for the `agent` integration. CLI-only project — no
|
|
6
|
-
* public exports map. Other akm modules import from this barrel for the
|
|
7
|
-
* sake of grouping imports.
|
|
8
|
-
*
|
|
9
|
-
* Surface:
|
|
10
|
-
* • Types: AgentProfile, AgentConfig, AgentRunResult, AgentFailureReason.
|
|
11
|
-
* • Profiles: getBuiltinAgentProfile, listBuiltinAgentProfiles, BUILTIN_AGENT_PROFILE_NAMES.
|
|
12
|
-
* • Config: parseAgentConfig, resolveProfileFromConfig, requireAgentProfile, listResolvedAgentProfiles, listAgentProfileNames.
|
|
13
|
-
* • Spawn: runAgent. Builders: getCommandBuilder, AgentCommandBuilder, AgentDispatchRequest — platform-specific argv construction.
|
|
14
|
-
* • Detection: detectAgentCliProfiles, pickDefaultAgentProfile, defaultWhich.
|
|
15
|
-
*/
|
|
16
|
-
// The OpenCode SDK runner moved to its harness directory in #564
|
|
17
|
-
// (`harnesses/opencode-sdk/`). Re-exported here so existing `agent/index`
|
|
18
|
-
// import sites keep working.
|
|
19
|
-
export { runAgentSdk } from "../harnesses/opencode-sdk/index.js";
|
|
20
4
|
export { getCommandBuilder } from "./builders.js";
|
|
21
|
-
export { DEFAULT_AGENT_TIMEOUT_MS, listAgentProfileNames, listResolvedAgentProfiles,
|
|
5
|
+
export { DEFAULT_AGENT_TIMEOUT_MS, listAgentProfileNames, listResolvedAgentProfiles, requireAgentProfile, resolveAgentProfile, resolveDefaultProfileName, resolveProfileFromConfig, } from "./config.js";
|
|
22
6
|
export { defaultWhich, detectAgentCliProfiles, pickDefaultAgentProfile } from "./detect.js";
|
|
23
7
|
export { listBuiltinModelAliases, resolveModel } from "./model-aliases.js";
|
|
24
8
|
export { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile, listBuiltinAgentProfiles, } from "./profiles.js";
|
|
25
|
-
export { buildProposePrompt, buildReflectPrompt, buildSchemaRepairPrompt, extractDraftConfidence, parseAgentProposalPayload,
|
|
9
|
+
export { buildProposePrompt, buildReflectPrompt, buildSchemaRepairPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "./prompts.js";
|
|
26
10
|
export { runAgent } from "./spawn.js";
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* during validation. We carry it through if the agent supplies it.
|
|
27
27
|
*/
|
|
28
28
|
import { TYPE_DIRS } from "../../core/asset/asset-spec.js";
|
|
29
|
+
import { authoringRulesForType, DESCRIPTION_MAX_CHARS, DESCRIPTION_MIN_CHARS, requiresDescription, } from "../../core/authoring-rules.js";
|
|
29
30
|
import { parseEmbeddedJsonResponse, stripCodeFences, stripThinkBlocks } from "../../core/parse.js";
|
|
30
31
|
/**
|
|
31
32
|
* Per-asset-type frontmatter / authoring hints surfaced in the prompt so
|
|
@@ -43,6 +44,7 @@ const TYPE_HINTS = {
|
|
|
43
44
|
script: "script assets are executable text files. Include a shebang and minimal usage comment.",
|
|
44
45
|
env: "env assets are `.env` files holding a group of related CONFIGURATION for an app/service (KEY=VALUE pairs, `#` comments) — URLs, flags, and any credentials it needs. Values may or may not be sensitive; all are protected (key names discoverable, values stay on disk). Inject with `akm env run env:<name> -- <cmd>` (the safe path — values never reach stdout/your context); do NOT run `akm env export` and read its output, as that prints values. For a single sensitive value used on its own for authentication (token, key, cert) use a `secret` instead. Never echo values back to the user.",
|
|
45
46
|
wiki: "wiki assets are markdown reference pages with `# Title` and structured headings.",
|
|
47
|
+
fact: "fact assets are durable stash-level facts (personal/team/project details, coding conventions, stash-meta). Frontmatter SHOULD include `description` and a `category` (personal|team|project|convention|meta); set `pinned: true` only for the small always-injected core. Keep each fact short, high-signal, and self-contained — it is durable context, not an episodic note.",
|
|
46
48
|
};
|
|
47
49
|
function hintForType(type) {
|
|
48
50
|
return TYPE_HINTS[type] ?? `assets of type "${type}" — produce sensible markdown with optional frontmatter.`;
|
|
@@ -109,6 +111,30 @@ export function extractDraftConfidence(stdout) {
|
|
|
109
111
|
return undefined;
|
|
110
112
|
return value;
|
|
111
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Whether the source asset content has a non-empty `description:` key in its
|
|
116
|
+
* YAML frontmatter. Used by {@link buildReflectPrompt} (#636) to decide whether
|
|
117
|
+
* to inject the synthesize-a-description instruction. Uses an inline regex to
|
|
118
|
+
* avoid pulling the full YAML parser into the prompt module (mirrors the
|
|
119
|
+
* existing inline frontmatter handling here).
|
|
120
|
+
*/
|
|
121
|
+
function sourceHasNonEmptyDescription(assetContent) {
|
|
122
|
+
if (!assetContent)
|
|
123
|
+
return false;
|
|
124
|
+
const fmMatch = assetContent.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
125
|
+
if (!fmMatch)
|
|
126
|
+
return false;
|
|
127
|
+
const fmBlock = fmMatch[1] ?? "";
|
|
128
|
+
// Match a top-level `description:` line and capture its inline value.
|
|
129
|
+
const descMatch = fmBlock.match(/^description\s*:\s*(.*)$/m);
|
|
130
|
+
if (!descMatch)
|
|
131
|
+
return false;
|
|
132
|
+
const value = (descMatch[1] ?? "")
|
|
133
|
+
.trim()
|
|
134
|
+
.replace(/^['"]|['"]$/g, "")
|
|
135
|
+
.trim();
|
|
136
|
+
return value.length > 0;
|
|
137
|
+
}
|
|
112
138
|
/**
|
|
113
139
|
* Build the prompt for `akm reflect [ref]`. Asks the agent to review an
|
|
114
140
|
* existing asset (plus any negative feedback / lint findings) and propose
|
|
@@ -155,6 +181,34 @@ export function buildReflectPrompt(input) {
|
|
|
155
181
|
// ref is set but no feedback — explicitly constrain scope to schema compliance
|
|
156
182
|
sections.push("No usage feedback recorded. Limit your proposal to schema and structural improvements only: missing required frontmatter fields, unclear `when_to_use`, ambiguous description, or broken formatting. Do not speculate about runtime weaknesses you have not observed.");
|
|
157
183
|
}
|
|
184
|
+
if (input.standardsContext?.trim()) {
|
|
185
|
+
sections.push("Standards to follow (the rulebook for this target):");
|
|
186
|
+
sections.push(input.standardsContext.trim());
|
|
187
|
+
}
|
|
188
|
+
{
|
|
189
|
+
const resolvedType = input.type ?? (input.ref?.includes(":") ? input.ref.split(":")[0] : "");
|
|
190
|
+
const authoringRules = resolvedType ? authoringRulesForType(resolvedType) : "";
|
|
191
|
+
if (authoringRules) {
|
|
192
|
+
sections.push(authoringRules);
|
|
193
|
+
}
|
|
194
|
+
// #636 — synthesize-a-description instruction. Many source assets (notably
|
|
195
|
+
// scraped docs: `source`/`title`/`scraped`) carry frontmatter but NO
|
|
196
|
+
// `description`. Reflect echoes the source frontmatter, so the proposal
|
|
197
|
+
// inherits the missing description and the promote-time validator
|
|
198
|
+
// (isValidDescription, 20–400 chars) rejects it. The fix is at GENERATION
|
|
199
|
+
// time: when the source lacks a non-empty `description` and the type
|
|
200
|
+
// requires one, tell the model — unmissably — that it MUST author a valid
|
|
201
|
+
// `description`. (The validator/promote path is NOT changed: it must never
|
|
202
|
+
// fabricate content to pass itself.)
|
|
203
|
+
if (resolvedType && requiresDescription(resolvedType) && !sourceHasNonEmptyDescription(input.assetContent)) {
|
|
204
|
+
sections.push([
|
|
205
|
+
"REQUIRED — synthesize a `description` (the source asset has none):",
|
|
206
|
+
`- The source frontmatter does NOT include a non-empty \`description\`, but a ${resolvedType} asset REQUIRES one or the proposal will be rejected at promote time.`,
|
|
207
|
+
`- You MUST author a valid \`description\` in the proposal frontmatter: ${DESCRIPTION_MIN_CHARS}–${DESCRIPTION_MAX_CHARS} characters of plain-prose sentence summarizing what this asset is about.`,
|
|
208
|
+
'- Synthesize it from the asset\'s `title:` frontmatter, its first `# Heading`, or the opening body sentence. Do NOT copy a bare heading fragment (e.g. "Overview", "Named Page", "Key Insight") and do NOT emit a truncated phrase that ends on `:`/`;`/`,` or a hanging connector word.',
|
|
209
|
+
].join("\n"));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
158
212
|
if (input.assetContent?.trim()) {
|
|
159
213
|
// Cap at 12 000 chars to stay well under OS ARG_MAX when the prompt is
|
|
160
214
|
// passed as a CLI argument to opencode/claude. Large assets (wiki snapshots,
|
|
@@ -288,6 +342,16 @@ export function buildProposePrompt(input) {
|
|
|
288
342
|
for (const line of input.schemaHints)
|
|
289
343
|
sections.push(`- ${line}`);
|
|
290
344
|
}
|
|
345
|
+
if (input.standardsContext?.trim()) {
|
|
346
|
+
sections.push("Standards to follow (the rulebook for this target):");
|
|
347
|
+
sections.push(input.standardsContext.trim());
|
|
348
|
+
}
|
|
349
|
+
{
|
|
350
|
+
const authoringRules = authoringRulesForType(input.type);
|
|
351
|
+
if (authoringRules) {
|
|
352
|
+
sections.push(authoringRules);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
291
355
|
sections.push("Produce a single proposal that, if accepted, would land as the asset described above.");
|
|
292
356
|
sections.push(input.draftFilePath ? fileWriteContract(input.draftFilePath) : RESPONSE_CONTRACT_JSON);
|
|
293
357
|
return sections.join("\n\n");
|
|
@@ -304,6 +368,16 @@ export function buildSchemaRepairPrompt(input) {
|
|
|
304
368
|
`while preserving all existing content.`);
|
|
305
369
|
sections.push(`Target ref: ${input.ref}`);
|
|
306
370
|
sections.push(`Schema requirements for ${input.type} assets: ${hintForType(input.type)}`);
|
|
371
|
+
if (input.standardsContext?.trim()) {
|
|
372
|
+
sections.push("Standards to follow (the rulebook for this target):");
|
|
373
|
+
sections.push(input.standardsContext.trim());
|
|
374
|
+
}
|
|
375
|
+
{
|
|
376
|
+
const authoringRules = authoringRulesForType(input.type);
|
|
377
|
+
if (authoringRules) {
|
|
378
|
+
sections.push(authoringRules);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
307
381
|
const CONTENT_CAP = 3000;
|
|
308
382
|
const body = input.assetContent.trimEnd();
|
|
309
383
|
const truncated = body.length > CONTENT_CAP;
|
|
@@ -370,11 +444,3 @@ export function parseAgentProposalPayload(stdout) {
|
|
|
370
444
|
}
|
|
371
445
|
return out;
|
|
372
446
|
}
|
|
373
|
-
/**
|
|
374
|
-
* Strip `\`\`\`json … \`\`\`` fences and `<think>…</think>` reasoning blocks
|
|
375
|
-
* from agent output. Thin wrapper around `core/parse` helpers, kept exported
|
|
376
|
-
* for backward compatibility (re-exported from `integrations/agent/index.ts`).
|
|
377
|
-
*/
|
|
378
|
-
export function stripJsonFences(text) {
|
|
379
|
-
return stripCodeFences(stripThinkBlocks(text));
|
|
380
|
-
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
* X3 — the ONE dispatch seam for the {@link RunnerSpec} tagged union.
|
|
6
|
+
*
|
|
7
|
+
* The improve slice dispatches a `RunnerSpec` (`llm | agent | sdk`) in several
|
|
8
|
+
* places (`reflect.ts`, `proposal/drain.ts`, …). Before this module each site
|
|
9
|
+
* re-rolled the identical 3-arm switch and re-declared its own per-kind test
|
|
10
|
+
* seams (`chat`, `runAgentFn`, `runSdkFn`). `executeRunner` collapses that into
|
|
11
|
+
* one switch + one {@link RunnerSeams} object.
|
|
12
|
+
*
|
|
13
|
+
* Scoping (behavior-preserving):
|
|
14
|
+
* - The `agent` and `sdk` arms are byte-identical across call sites: invoke
|
|
15
|
+
* the profile runner (`runAgent` / `runOpencodeSdk`) with the per-call
|
|
16
|
+
* `RunAgentOptions` the caller passes. Those default runners live here so
|
|
17
|
+
* callers stop importing `runAgent` / `runOpencodeSdk` for dispatch. The
|
|
18
|
+
* `opts` (incl. any `timeoutMs`) is constructed by the caller and passed
|
|
19
|
+
* through unchanged, so each site keeps its exact option set.
|
|
20
|
+
* - The `llm` arm is irreducibly caller-specific (reflect wraps
|
|
21
|
+
* `runReflectViaLlm`, which returns reflect's iteration shape; drain wraps a
|
|
22
|
+
* plain `chatCompletion`). It is therefore a REQUIRED seam — there is no
|
|
23
|
+
* default `llm` handler — so neither caller's bespoke behavior is changed.
|
|
24
|
+
* - The `assertNever` exhaustiveness arm is kept so a 4th `RunnerSpec` kind is
|
|
25
|
+
* a compile error here instead of a silent runtime fall-through.
|
|
26
|
+
*
|
|
27
|
+
* The return type is {@link AgentRunResult} so a later `callStructured` layer
|
|
28
|
+
* (X2) can wrap `executeRunner` without changing this contract.
|
|
29
|
+
*/
|
|
30
|
+
import { assertNever } from "../../core/assert.js";
|
|
31
|
+
import { runOpencodeSdk } from "../harnesses/opencode-sdk/index.js";
|
|
32
|
+
import { runAgent } from "./spawn.js";
|
|
33
|
+
/**
|
|
34
|
+
* Dispatch a {@link RunnerSpec} to its runner and return the raw
|
|
35
|
+
* {@link AgentRunResult}. `opts` is the {@link RunAgentOptions} for the profile
|
|
36
|
+
* (`agent` / `sdk`) arms; it is passed through unchanged so each caller keeps
|
|
37
|
+
* its exact option set (incl. any `timeoutMs` the caller chose to apply).
|
|
38
|
+
*/
|
|
39
|
+
export async function executeRunner(spec, prompt, opts, seams = {}) {
|
|
40
|
+
switch (spec.kind) {
|
|
41
|
+
case "llm": {
|
|
42
|
+
if (!seams.llm) {
|
|
43
|
+
throw new Error("executeRunner: an `llm` runner requires a `seams.llm` handler (no default LLM dispatch).");
|
|
44
|
+
}
|
|
45
|
+
return seams.llm(spec, prompt);
|
|
46
|
+
}
|
|
47
|
+
case "agent": {
|
|
48
|
+
const run = seams.runAgent ?? runAgent;
|
|
49
|
+
return run(spec.profile, prompt, opts);
|
|
50
|
+
}
|
|
51
|
+
case "sdk": {
|
|
52
|
+
const run = seams.runSdk ?? runOpencodeSdk;
|
|
53
|
+
return run(spec.profile, prompt, opts);
|
|
54
|
+
}
|
|
55
|
+
default:
|
|
56
|
+
// Exhaustiveness arm: a 4th RunnerSpec kind becomes a compile error here.
|
|
57
|
+
return assertNever(spec);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -107,6 +107,16 @@ export class ClaudeCodeProvider {
|
|
|
107
107
|
isAvailable() {
|
|
108
108
|
return fs.existsSync(claudeProjectsDir());
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Directory holding Claude Code's per-project session JSONL files
|
|
112
|
+
* (`~/.claude/projects`, honoring `AKM_CLAUDE_PROJECTS_DIR`). Returns `[]`
|
|
113
|
+
* when the directory does not exist on this machine. See {@link
|
|
114
|
+
* SessionLogHarness.watchRoots}.
|
|
115
|
+
*/
|
|
116
|
+
watchRoots() {
|
|
117
|
+
const dir = claudeProjectsDir();
|
|
118
|
+
return fs.existsSync(dir) ? [dir] : [];
|
|
119
|
+
}
|
|
110
120
|
*readEvents(input) {
|
|
111
121
|
try {
|
|
112
122
|
for (const jsonlPath of this.#walkJsonl(claudeProjectsDir())) {
|
|
@@ -298,7 +308,7 @@ export class ClaudeCodeProvider {
|
|
|
298
308
|
const full = path.join(dir, entry.name);
|
|
299
309
|
if (entry.isDirectory())
|
|
300
310
|
yield* this.#walkJsonl(full);
|
|
301
|
-
else if (entry.name.endsWith(".jsonl"))
|
|
311
|
+
else if (entry.name.endsWith(".jsonl") && entry.name !== "journal.jsonl")
|
|
302
312
|
yield full;
|
|
303
313
|
}
|
|
304
314
|
}
|
|
@@ -60,9 +60,8 @@ const HARNESS_BY_ANY_ID = (() => {
|
|
|
60
60
|
})();
|
|
61
61
|
/**
|
|
62
62
|
* Canonical, ordered list of valid harness / platform ids. The Zod
|
|
63
|
-
* `AgentPlatformSchema` enum, the `
|
|
64
|
-
*
|
|
65
|
-
* union all derive from this so they cannot drift.
|
|
63
|
+
* `AgentPlatformSchema` enum, the `AgentProfileConfig` platform union, and
|
|
64
|
+
* setup's `DetectedHarness` union all derive from this so they cannot drift.
|
|
66
65
|
*/
|
|
67
66
|
export const VALID_HARNESS_IDS = Object.freeze(HARNESS_REGISTRY.map((h) => h.id));
|
|
68
67
|
/** Harnesses that expose readable native session logs. */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { openDatabase } from "../../../storage/database.js";
|
|
7
8
|
import { extractInlineRefMentions } from "../../session-logs/inline-refs.js";
|
|
8
9
|
function getOpenCodeBaseDir() {
|
|
9
10
|
if (process.platform === "darwin") {
|
|
@@ -12,20 +13,49 @@ function getOpenCodeBaseDir() {
|
|
|
12
13
|
return path.join(os.homedir(), ".local", "share", "opencode");
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
|
-
* Opencode storage
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Opencode storage layouts:
|
|
17
|
+
*
|
|
18
|
+
* SQLite (current, observed 2026-06): `<base>/opencode.db` — a Drizzle-managed
|
|
19
|
+
* database with `session` / `message` / `part` tables. Message text lives in
|
|
20
|
+
* `part` rows (`data` JSON, `type: "text"`); `message.data` holds role/timing.
|
|
21
|
+
* This is the layout current opencode builds write; it is preferred whenever
|
|
22
|
+
* `opencode.db` exists.
|
|
23
|
+
*
|
|
24
|
+
* JSON files (legacy, observed 2026-05): `<base>/storage/session/<projectId>/
|
|
25
|
+
* <sessionId>.json` (metadata) + `<base>/storage/message/<sessionId>/
|
|
26
|
+
* <messageId>.json` (one per message). Read only when `opencode.db` is absent.
|
|
18
27
|
*
|
|
19
28
|
* Older builds wrote logs directly into `<base>/log/` and `<base>/*.log`;
|
|
20
29
|
* those are still scanned by {@link OpenCodeProvider.readEvents} for
|
|
21
30
|
* backward compatibility with the existing failure-pattern aggregator.
|
|
22
31
|
*/
|
|
32
|
+
/** Filename of opencode's SQLite session store, relative to its base dir. */
|
|
33
|
+
const OPENCODE_DB_FILENAME = "opencode.db";
|
|
23
34
|
export class OpenCodeProvider {
|
|
24
35
|
name = "opencode";
|
|
25
36
|
#baseDir = getOpenCodeBaseDir();
|
|
26
37
|
isAvailable() {
|
|
27
38
|
return fs.existsSync(this.#baseDir);
|
|
28
39
|
}
|
|
40
|
+
/** Absolute path to opencode's SQLite store under `base`. */
|
|
41
|
+
#dbPath(base) {
|
|
42
|
+
return path.join(base, OPENCODE_DB_FILENAME);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Directories/files opencode writes session data under. Returns the base dir
|
|
46
|
+
* when the SQLite store (`opencode.db`) exists, the legacy JSON session root
|
|
47
|
+
* (`<base>/storage/session`) when present, or both during a migration overlap.
|
|
48
|
+
* Empty when neither exists. See {@link SessionLogHarness.watchRoots}.
|
|
49
|
+
*/
|
|
50
|
+
watchRoots() {
|
|
51
|
+
const roots = [];
|
|
52
|
+
if (fs.existsSync(this.#dbPath(this.#baseDir)))
|
|
53
|
+
roots.push(this.#baseDir);
|
|
54
|
+
const sessionRoot = path.join(this.#baseDir, "storage", "session");
|
|
55
|
+
if (fs.existsSync(sessionRoot))
|
|
56
|
+
roots.push(sessionRoot);
|
|
57
|
+
return roots;
|
|
58
|
+
}
|
|
29
59
|
*readEvents(input) {
|
|
30
60
|
// Legacy behavior: stream raw log lines from the top-level dir and `log/`
|
|
31
61
|
// subdirectory. Kept to keep `getExecutionLogCandidates` working without
|
|
@@ -82,6 +112,9 @@ export class OpenCodeProvider {
|
|
|
82
112
|
listSessions(input = {}) {
|
|
83
113
|
const base = input.location ?? this.#baseDir;
|
|
84
114
|
const sinceMs = input.sinceMs ?? 0;
|
|
115
|
+
const dbPath = this.#dbPath(base);
|
|
116
|
+
if (fs.existsSync(dbPath))
|
|
117
|
+
return this.#listSessionsFromDb(dbPath, sinceMs);
|
|
85
118
|
const sessionRoot = path.join(base, "storage", "session");
|
|
86
119
|
if (!fs.existsSync(sessionRoot))
|
|
87
120
|
return [];
|
|
@@ -142,6 +175,8 @@ export class OpenCodeProvider {
|
|
|
142
175
|
return summaries.sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0));
|
|
143
176
|
}
|
|
144
177
|
readSession(ref) {
|
|
178
|
+
if (path.basename(ref.filePath) === OPENCODE_DB_FILENAME)
|
|
179
|
+
return this.#readSessionFromDb(ref);
|
|
145
180
|
let meta = {};
|
|
146
181
|
try {
|
|
147
182
|
meta = JSON.parse(fs.readFileSync(ref.filePath, "utf8"));
|
|
@@ -199,6 +234,141 @@ export class OpenCodeProvider {
|
|
|
199
234
|
inlineRefs,
|
|
200
235
|
};
|
|
201
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* List sessions from the SQLite store. `filePath` on each summary is the
|
|
239
|
+
* `opencode.db` path so {@link readSession} can route back to the DB reader.
|
|
240
|
+
* Returns `[]` (never throws) when the DB is unreadable or lacks the expected
|
|
241
|
+
* schema — callers treat a missing harness as "no sessions".
|
|
242
|
+
*/
|
|
243
|
+
#listSessionsFromDb(dbPath, sinceMs) {
|
|
244
|
+
let db;
|
|
245
|
+
try {
|
|
246
|
+
db = openDatabase(dbPath, { readonly: true, create: false });
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const rows = db
|
|
253
|
+
.prepare("SELECT id, title, directory, time_created, time_updated FROM session WHERE time_updated >= ? ORDER BY time_updated DESC")
|
|
254
|
+
.all(sinceMs);
|
|
255
|
+
return rows.map((r) => {
|
|
256
|
+
const startedAt = typeof r.time_created === "number" ? r.time_created : undefined;
|
|
257
|
+
const endedAt = typeof r.time_updated === "number" ? r.time_updated : undefined;
|
|
258
|
+
const title = typeof r.title === "string" && r.title.length > 0 ? r.title : undefined;
|
|
259
|
+
const projectHint = typeof r.directory === "string" && r.directory.length > 0 ? r.directory : undefined;
|
|
260
|
+
return {
|
|
261
|
+
harness: this.name,
|
|
262
|
+
sessionId: r.id,
|
|
263
|
+
filePath: dbPath,
|
|
264
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
265
|
+
...(endedAt !== undefined ? { endedAt } : {}),
|
|
266
|
+
...(projectHint ? { projectHint } : {}),
|
|
267
|
+
...(title ? { title } : {}),
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// Missing `session` table / unexpected schema — treat as no sessions.
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
db.close();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Read one session from the SQLite store. Message text lives in `part` rows
|
|
281
|
+
* (`type: "text"`); `message.data` carries role + timing. One event per
|
|
282
|
+
* message, text-parts concatenated in time order. Returns empty events
|
|
283
|
+
* (never throws) when the DB is unreadable.
|
|
284
|
+
*/
|
|
285
|
+
#readSessionFromDb(ref) {
|
|
286
|
+
const emptyRef = { harness: this.name, sessionId: ref.sessionId, filePath: ref.filePath };
|
|
287
|
+
let db;
|
|
288
|
+
try {
|
|
289
|
+
db = openDatabase(ref.filePath, { readonly: true, create: false });
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return { ref: emptyRef, events: [], inlineRefs: [] };
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const meta = db
|
|
296
|
+
.prepare("SELECT title, directory, time_created, time_updated FROM session WHERE id = ?")
|
|
297
|
+
.get(ref.sessionId);
|
|
298
|
+
const startedAt = typeof meta?.time_created === "number" ? meta.time_created : undefined;
|
|
299
|
+
const endedAt = typeof meta?.time_updated === "number" ? meta.time_updated : undefined;
|
|
300
|
+
const title = typeof meta?.title === "string" && meta.title.length > 0 ? meta.title : undefined;
|
|
301
|
+
const projectHint = typeof meta?.directory === "string" && meta.directory.length > 0 ? meta.directory : undefined;
|
|
302
|
+
const messages = db
|
|
303
|
+
.prepare("SELECT id, data, time_created FROM message WHERE session_id = ? ORDER BY time_created ASC, id ASC")
|
|
304
|
+
.all(ref.sessionId);
|
|
305
|
+
const parts = db
|
|
306
|
+
.prepare("SELECT message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC, id ASC")
|
|
307
|
+
.all(ref.sessionId);
|
|
308
|
+
// Group text-part bodies by their parent message.
|
|
309
|
+
const textByMessage = new Map();
|
|
310
|
+
for (const part of parts) {
|
|
311
|
+
let parsed;
|
|
312
|
+
try {
|
|
313
|
+
parsed = JSON.parse(part.data);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (parsed?.type !== "text")
|
|
319
|
+
continue;
|
|
320
|
+
const text = parsed.text;
|
|
321
|
+
if (typeof text !== "string" || text.length < 1)
|
|
322
|
+
continue;
|
|
323
|
+
const bucket = textByMessage.get(part.message_id) ?? [];
|
|
324
|
+
bucket.push(text);
|
|
325
|
+
textByMessage.set(part.message_id, bucket);
|
|
326
|
+
}
|
|
327
|
+
const events = [];
|
|
328
|
+
const inlineRefs = [];
|
|
329
|
+
for (const message of messages) {
|
|
330
|
+
let mdata = {};
|
|
331
|
+
try {
|
|
332
|
+
mdata = JSON.parse(message.data);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// role/timing unavailable — fall through with defaults
|
|
336
|
+
}
|
|
337
|
+
const role = typeof mdata.role === "string" ? mdata.role : "unknown";
|
|
338
|
+
const mtime = mdata.time?.created;
|
|
339
|
+
const ts = typeof mtime === "number"
|
|
340
|
+
? mtime
|
|
341
|
+
: typeof message.time_created === "number"
|
|
342
|
+
? message.time_created
|
|
343
|
+
: undefined;
|
|
344
|
+
const text = (textByMessage.get(message.id) ?? []).join("\n").trim();
|
|
345
|
+
if (text.length < 1)
|
|
346
|
+
continue;
|
|
347
|
+
events.push({ harness: this.name, text, ts, sessionId: ref.sessionId, role, filePath: ref.filePath });
|
|
348
|
+
inlineRefs.push(...extractInlineRefMentions(text, ts));
|
|
349
|
+
}
|
|
350
|
+
events.sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
|
|
351
|
+
return {
|
|
352
|
+
ref: {
|
|
353
|
+
harness: this.name,
|
|
354
|
+
sessionId: ref.sessionId,
|
|
355
|
+
filePath: ref.filePath,
|
|
356
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
357
|
+
...(endedAt !== undefined ? { endedAt } : {}),
|
|
358
|
+
...(projectHint ? { projectHint } : {}),
|
|
359
|
+
...(title ? { title } : {}),
|
|
360
|
+
},
|
|
361
|
+
events,
|
|
362
|
+
inlineRefs,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return { ref: emptyRef, events: [], inlineRefs: [] };
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
db.close();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
202
372
|
/**
|
|
203
373
|
* Derive opencode base dir from a session metadata file path so a caller
|
|
204
374
|
* passing a custom `--location` can still find the message dir.
|