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,152 @@
|
|
|
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
|
+
/** Default cap for any single event's text length. Head+tail summary applies above this. */
|
|
5
|
+
export const DEFAULT_MAX_EVENT_LENGTH = 2000;
|
|
6
|
+
/**
|
|
7
|
+
* Default cap on total transcript characters fed to the LLM. Chosen for a
|
|
8
|
+
* 32K-token context model with room for the prompt scaffolding (~3K chars)
|
|
9
|
+
* and JSON output (~4K chars). Adjust via {@link PreFilterOptions.maxTotalChars}
|
|
10
|
+
* when targeting larger-context models.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_MAX_TOTAL_CHARS = 80_000;
|
|
13
|
+
/**
|
|
14
|
+
* `akm` subcommands that are read-only / introspective — their invocations
|
|
15
|
+
* are operational noise, not engineering signal. Mutating commands (remember,
|
|
16
|
+
* feedback, accept, reject, extract, import, save, ...) are kept.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_AKM_READONLY_OPS = new Set([
|
|
19
|
+
"show",
|
|
20
|
+
"search",
|
|
21
|
+
"curate",
|
|
22
|
+
"history",
|
|
23
|
+
"info",
|
|
24
|
+
"hints",
|
|
25
|
+
"help",
|
|
26
|
+
"list",
|
|
27
|
+
"completions",
|
|
28
|
+
"lessons",
|
|
29
|
+
"graph",
|
|
30
|
+
"db",
|
|
31
|
+
"events",
|
|
32
|
+
"config",
|
|
33
|
+
"health",
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* Regex patterns that identify post-compact / activity-log noise. Conservative
|
|
37
|
+
* — only matches text that's clearly transcript pollution, not engineering
|
|
38
|
+
* content that happens to contain similar words.
|
|
39
|
+
*/
|
|
40
|
+
const NOISE_PATTERNS = [
|
|
41
|
+
// Claude Code injects this caveat block before every bash invocation result.
|
|
42
|
+
/<local-command-caveat>/i,
|
|
43
|
+
// Post-compact dumps embed analysis/summary XML blocks pasted from prior context.
|
|
44
|
+
/<analysis>[\s\S]{200,}<\/analysis>/i,
|
|
45
|
+
/<summary>[\s\S]{200,}<\/summary>/i,
|
|
46
|
+
// System reminders the harness injects every few turns — never carry signal.
|
|
47
|
+
/<system-reminder>/i,
|
|
48
|
+
// Opencode tool-event aggregate dumps look like repeated `akm_search unknown` blocks.
|
|
49
|
+
/^(##\s+\d+.*akm_search unknown\s*\n){3,}/im,
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Apply the drop+truncate rules to a single event. Returns `undefined` when
|
|
53
|
+
* the event should be dropped, or the (possibly truncated) event when kept.
|
|
54
|
+
* The third return tracks why dropped, for stats.
|
|
55
|
+
*/
|
|
56
|
+
function classifyEvent(event, akmReadOnlyOps, maxLen) {
|
|
57
|
+
const text = event.text ?? "";
|
|
58
|
+
if (text.trim().length < 10)
|
|
59
|
+
return { keep: false, reason: "too-short" };
|
|
60
|
+
// Rule 1: read-only akm meta-ops. The flattened tool_use shape from the
|
|
61
|
+
// claude-code provider looks like: `[tool:Bash] akm show knowledge:foo`.
|
|
62
|
+
// Match the verb directly after `akm ` (with or without the `[tool:...]`
|
|
63
|
+
// prefix, since some platforms surface the command differently).
|
|
64
|
+
const akmCallMatch = text.match(/\bakm\s+(\w[\w-]*)\b/);
|
|
65
|
+
if (akmCallMatch) {
|
|
66
|
+
const op = (akmCallMatch[1] ?? "").toLowerCase();
|
|
67
|
+
if (akmReadOnlyOps.has(op)) {
|
|
68
|
+
return { keep: false, reason: `akm-readonly-${op}` };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Rule 2-5: noise patterns
|
|
72
|
+
for (const pattern of NOISE_PATTERNS) {
|
|
73
|
+
if (pattern.test(text)) {
|
|
74
|
+
return { keep: false, reason: `noise-pattern-${pattern.source.slice(0, 24)}` };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Rule 6: bare system events that are pure boilerplate (no engineering content).
|
|
78
|
+
// Heuristic: role=system AND short, OR role=system AND just contains `caveat`/`reminder` markers.
|
|
79
|
+
if (event.role === "system" && (text.length < 200 || /caveat|reminder/i.test(text))) {
|
|
80
|
+
return { keep: false, reason: "system-boilerplate" };
|
|
81
|
+
}
|
|
82
|
+
// Truncate long events to head + tail summary.
|
|
83
|
+
if (text.length > maxLen) {
|
|
84
|
+
const headLen = Math.floor(maxLen * 0.7);
|
|
85
|
+
const tailLen = maxLen - headLen - 32; // 32 chars for the marker
|
|
86
|
+
const truncated = text.slice(0, headLen) +
|
|
87
|
+
`\n... [truncated ${text.length - headLen - tailLen} chars] ...\n` +
|
|
88
|
+
text.slice(text.length - tailLen);
|
|
89
|
+
return { keep: true, event: { ...event, text: truncated }, truncated: true };
|
|
90
|
+
}
|
|
91
|
+
return { keep: true, event, truncated: false };
|
|
92
|
+
}
|
|
93
|
+
export function preFilterSession(data, options = {}) {
|
|
94
|
+
const akmReadOnlyOps = options.akmReadOnlyOps ?? DEFAULT_AKM_READONLY_OPS;
|
|
95
|
+
const maxLen = options.maxEventTextLength ?? DEFAULT_MAX_EVENT_LENGTH;
|
|
96
|
+
const maxTotalChars = options.maxTotalChars ?? DEFAULT_MAX_TOTAL_CHARS;
|
|
97
|
+
const droppedByRule = {};
|
|
98
|
+
const kept = [];
|
|
99
|
+
let truncatedCount = 0;
|
|
100
|
+
const candidates = [];
|
|
101
|
+
for (const event of data.events) {
|
|
102
|
+
const verdict = classifyEvent(event, akmReadOnlyOps, maxLen);
|
|
103
|
+
if (!verdict.keep) {
|
|
104
|
+
droppedByRule[verdict.reason] = (droppedByRule[verdict.reason] ?? 0) + 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
candidates.push({
|
|
108
|
+
event: verdict.event,
|
|
109
|
+
truncated: verdict.truncated,
|
|
110
|
+
chars: verdict.event.text.length,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Second pass: total-budget cap. Walk from the END (most recent first) and
|
|
114
|
+
// accept events until the budget is exhausted. The remaining (head) events
|
|
115
|
+
// are dropped — insight typically emerges later in a session, so this
|
|
116
|
+
// recency-bias is the cheapest sampling heuristic that respects context
|
|
117
|
+
// limits. Maintains original timestamp order in the output.
|
|
118
|
+
let totalChars = 0;
|
|
119
|
+
let budgetDroppedCount = 0;
|
|
120
|
+
const keptIdxFromTail = [];
|
|
121
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
122
|
+
const c = candidates[i];
|
|
123
|
+
if (!c)
|
|
124
|
+
continue;
|
|
125
|
+
if (totalChars + c.chars > maxTotalChars && keptIdxFromTail.length > 0) {
|
|
126
|
+
budgetDroppedCount += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
keptIdxFromTail.push(i);
|
|
130
|
+
totalChars += c.chars;
|
|
131
|
+
}
|
|
132
|
+
keptIdxFromTail.reverse(); // restore timestamp order
|
|
133
|
+
for (const idx of keptIdxFromTail) {
|
|
134
|
+
const c = candidates[idx];
|
|
135
|
+
if (!c)
|
|
136
|
+
continue;
|
|
137
|
+
kept.push(c.event);
|
|
138
|
+
if (c.truncated)
|
|
139
|
+
truncatedCount += 1;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
events: kept,
|
|
143
|
+
stats: {
|
|
144
|
+
inputCount: data.events.length,
|
|
145
|
+
outputCount: kept.length,
|
|
146
|
+
droppedByRule,
|
|
147
|
+
truncatedCount,
|
|
148
|
+
totalChars,
|
|
149
|
+
budgetDroppedCount,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
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 os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { extractInlineRefMentions } from "../inline-refs";
|
|
8
|
+
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
|
|
9
|
+
/**
|
|
10
|
+
* Parse a single Claude Code JSONL event into a normalized {@link SessionEvent}.
|
|
11
|
+
* Returns `undefined` for events that don't carry textual content (file
|
|
12
|
+
* snapshots, attachments, queue metadata). Tool calls are flattened from the
|
|
13
|
+
* `message.content` array into a stable text representation so downstream
|
|
14
|
+
* consumers don't need to know the Anthropic-tool-call shape.
|
|
15
|
+
*/
|
|
16
|
+
function parseClaudeEvent(entry, sessionId, filePath, fallbackTsMs) {
|
|
17
|
+
if (!entry || typeof entry !== "object")
|
|
18
|
+
return undefined;
|
|
19
|
+
const e = entry;
|
|
20
|
+
const tsRaw = e.timestamp;
|
|
21
|
+
const ts = typeof tsRaw === "number" ? tsRaw : typeof tsRaw === "string" ? Date.parse(tsRaw) || fallbackTsMs : fallbackTsMs;
|
|
22
|
+
const message = e.message ?? undefined;
|
|
23
|
+
const role = typeof message?.role === "string"
|
|
24
|
+
? message.role
|
|
25
|
+
: (e.type ?? "unknown");
|
|
26
|
+
const content = message?.content;
|
|
27
|
+
let text = "";
|
|
28
|
+
if (typeof content === "string") {
|
|
29
|
+
text = content;
|
|
30
|
+
}
|
|
31
|
+
else if (Array.isArray(content)) {
|
|
32
|
+
// Assistant messages: array of content blocks. Flatten text/thinking/tool_use
|
|
33
|
+
// into a stable representation. tool_use entries become `[tool: <name>] <input>`
|
|
34
|
+
// so the inline-ref scanner can detect `akm remember` / `akm feedback` calls.
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (const block of content) {
|
|
37
|
+
if (!block || typeof block !== "object")
|
|
38
|
+
continue;
|
|
39
|
+
const b = block;
|
|
40
|
+
if (b.type === "text" && typeof b.text === "string")
|
|
41
|
+
parts.push(b.text);
|
|
42
|
+
else if (b.type === "thinking" && typeof b.thinking === "string")
|
|
43
|
+
parts.push(b.thinking);
|
|
44
|
+
else if (b.type === "tool_use") {
|
|
45
|
+
const toolName = typeof b.name === "string" ? b.name : "tool";
|
|
46
|
+
// For shell-like tools, surface the `command` field directly so
|
|
47
|
+
// inline-ref detection can match `akm remember "..."` without
|
|
48
|
+
// JSON-quote escaping mangling the regex.
|
|
49
|
+
const inputObj = b.input;
|
|
50
|
+
let inputText = "";
|
|
51
|
+
if (inputObj && typeof inputObj === "object") {
|
|
52
|
+
const cmd = inputObj.command;
|
|
53
|
+
inputText = typeof cmd === "string" ? cmd : JSON.stringify(inputObj);
|
|
54
|
+
}
|
|
55
|
+
else if (typeof inputObj === "string") {
|
|
56
|
+
inputText = inputObj;
|
|
57
|
+
}
|
|
58
|
+
parts.push(`[tool:${toolName}] ${inputText}`);
|
|
59
|
+
}
|
|
60
|
+
else if (b.type === "tool_result") {
|
|
61
|
+
const out = typeof b.content === "string" ? b.content : JSON.stringify(b.content ?? "");
|
|
62
|
+
parts.push(`[tool_result] ${out}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
text = parts.join("\n");
|
|
66
|
+
}
|
|
67
|
+
if (!text || text.length < 1)
|
|
68
|
+
return undefined;
|
|
69
|
+
return {
|
|
70
|
+
harness: "claude-code",
|
|
71
|
+
text,
|
|
72
|
+
ts,
|
|
73
|
+
sessionId,
|
|
74
|
+
role,
|
|
75
|
+
filePath,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export class ClaudeCodeProvider {
|
|
79
|
+
name = "claude-code";
|
|
80
|
+
isAvailable() {
|
|
81
|
+
return fs.existsSync(CLAUDE_PROJECTS_DIR);
|
|
82
|
+
}
|
|
83
|
+
*readEvents(input) {
|
|
84
|
+
try {
|
|
85
|
+
for (const jsonlPath of this.#walkJsonl(CLAUDE_PROJECTS_DIR)) {
|
|
86
|
+
const stat = fs.statSync(jsonlPath);
|
|
87
|
+
if (stat.mtimeMs < input.sinceMs)
|
|
88
|
+
continue;
|
|
89
|
+
const lines = fs.readFileSync(jsonlPath, "utf8").split("\n").filter(Boolean);
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
try {
|
|
92
|
+
const entry = JSON.parse(line);
|
|
93
|
+
const text = entry?.message?.content ?? entry?.content ?? "";
|
|
94
|
+
if (typeof text !== "string" || text.length < 10)
|
|
95
|
+
continue;
|
|
96
|
+
yield {
|
|
97
|
+
harness: this.name,
|
|
98
|
+
text,
|
|
99
|
+
ts: typeof entry?.timestamp === "number" ? entry.timestamp : stat.mtimeMs,
|
|
100
|
+
sessionId: typeof entry?.session_id === "string" ? entry.session_id : undefined,
|
|
101
|
+
role: typeof entry?.role === "string" ? entry.role : "unknown",
|
|
102
|
+
filePath: jsonlPath,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// skip malformed lines
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
listSessions(input = {}) {
|
|
116
|
+
const root = input.location ?? CLAUDE_PROJECTS_DIR;
|
|
117
|
+
const sinceMs = input.sinceMs ?? 0;
|
|
118
|
+
const summaries = [];
|
|
119
|
+
try {
|
|
120
|
+
for (const jsonlPath of this.#walkJsonl(root)) {
|
|
121
|
+
let stat;
|
|
122
|
+
try {
|
|
123
|
+
stat = fs.statSync(jsonlPath);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (stat.mtimeMs < sinceMs)
|
|
129
|
+
continue;
|
|
130
|
+
const sessionId = path.basename(jsonlPath, ".jsonl");
|
|
131
|
+
const projectHint = path.basename(path.dirname(jsonlPath));
|
|
132
|
+
// Peek first + last non-empty line to derive start/end timestamps and
|
|
133
|
+
// title. Reading the whole file would be wasteful for listing.
|
|
134
|
+
const peek = this.#peekJsonl(jsonlPath);
|
|
135
|
+
summaries.push({
|
|
136
|
+
harness: this.name,
|
|
137
|
+
sessionId,
|
|
138
|
+
filePath: jsonlPath,
|
|
139
|
+
startedAt: peek.firstTsMs ?? stat.ctimeMs,
|
|
140
|
+
endedAt: peek.lastTsMs ?? stat.mtimeMs,
|
|
141
|
+
projectHint,
|
|
142
|
+
...(peek.title ? { title: peek.title } : {}),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Root missing or unreadable — return what we have.
|
|
148
|
+
}
|
|
149
|
+
return summaries.sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0));
|
|
150
|
+
}
|
|
151
|
+
readSession(ref) {
|
|
152
|
+
const stat = fs.statSync(ref.filePath);
|
|
153
|
+
const lines = fs.readFileSync(ref.filePath, "utf8").split("\n").filter(Boolean);
|
|
154
|
+
const events = [];
|
|
155
|
+
const inlineRefs = [];
|
|
156
|
+
let title;
|
|
157
|
+
let firstTsMs;
|
|
158
|
+
let lastTsMs;
|
|
159
|
+
const projectHint = path.basename(path.dirname(ref.filePath));
|
|
160
|
+
for (const line of lines) {
|
|
161
|
+
let entry;
|
|
162
|
+
try {
|
|
163
|
+
entry = JSON.parse(line);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!entry)
|
|
169
|
+
continue;
|
|
170
|
+
if (entry.type === "custom-title" && typeof entry.customTitle === "string") {
|
|
171
|
+
title = entry.customTitle;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const parsed = parseClaudeEvent(entry, ref.sessionId, ref.filePath, stat.mtimeMs);
|
|
175
|
+
if (!parsed)
|
|
176
|
+
continue;
|
|
177
|
+
events.push(parsed);
|
|
178
|
+
if (firstTsMs === undefined || (parsed.ts ?? 0) < firstTsMs)
|
|
179
|
+
firstTsMs = parsed.ts;
|
|
180
|
+
if (lastTsMs === undefined || (parsed.ts ?? 0) > lastTsMs)
|
|
181
|
+
lastTsMs = parsed.ts;
|
|
182
|
+
// Extract inline akm-remember/feedback invocations from this event's text.
|
|
183
|
+
inlineRefs.push(...extractInlineRefMentions(parsed.text, parsed.ts));
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
ref: {
|
|
187
|
+
harness: this.name,
|
|
188
|
+
sessionId: ref.sessionId,
|
|
189
|
+
filePath: ref.filePath,
|
|
190
|
+
startedAt: firstTsMs ?? stat.ctimeMs,
|
|
191
|
+
endedAt: lastTsMs ?? stat.mtimeMs,
|
|
192
|
+
projectHint,
|
|
193
|
+
...(title ? { title } : {}),
|
|
194
|
+
},
|
|
195
|
+
events,
|
|
196
|
+
inlineRefs,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Cheap metadata peek — reads the first ~4KB to grab the `custom-title`
|
|
201
|
+
* event (always early in the file) and the first event timestamp, then
|
|
202
|
+
* reads the tail (~4KB) for the last timestamp. Avoids slurping multi-MB
|
|
203
|
+
* session files during `listSessions`.
|
|
204
|
+
*/
|
|
205
|
+
#peekJsonl(filePath) {
|
|
206
|
+
const result = {};
|
|
207
|
+
try {
|
|
208
|
+
const fd = fs.openSync(filePath, "r");
|
|
209
|
+
try {
|
|
210
|
+
const stat = fs.fstatSync(fd);
|
|
211
|
+
const headSize = Math.min(stat.size, 4096);
|
|
212
|
+
const head = Buffer.alloc(headSize);
|
|
213
|
+
fs.readSync(fd, head, 0, headSize, 0);
|
|
214
|
+
const headLines = head.toString("utf8").split("\n").filter(Boolean);
|
|
215
|
+
// Walk head: track title, first timestamp, and (if file fits in head)
|
|
216
|
+
// also the last timestamp seen — saves a tail read for small files.
|
|
217
|
+
for (const line of headLines) {
|
|
218
|
+
try {
|
|
219
|
+
const e = JSON.parse(line);
|
|
220
|
+
if (e.type === "custom-title" && typeof e.customTitle === "string") {
|
|
221
|
+
result.title = e.customTitle;
|
|
222
|
+
}
|
|
223
|
+
if (typeof e.timestamp === "string") {
|
|
224
|
+
const t = Date.parse(e.timestamp);
|
|
225
|
+
if (!Number.isNaN(t)) {
|
|
226
|
+
if (result.firstTsMs === undefined)
|
|
227
|
+
result.firstTsMs = t;
|
|
228
|
+
result.lastTsMs = t;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// partial line at buffer boundary — fine, skip
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Large-file tail read overrides lastTsMs with a value closer to EOF.
|
|
237
|
+
if (stat.size > 4096) {
|
|
238
|
+
const tailSize = Math.min(stat.size, 4096);
|
|
239
|
+
const tail = Buffer.alloc(tailSize);
|
|
240
|
+
fs.readSync(fd, tail, 0, tailSize, stat.size - tailSize);
|
|
241
|
+
const tailLines = tail.toString("utf8").split("\n").filter(Boolean);
|
|
242
|
+
for (let i = tailLines.length - 1; i >= 0; i--) {
|
|
243
|
+
try {
|
|
244
|
+
const e = JSON.parse(tailLines[i] ?? "");
|
|
245
|
+
if (typeof e.timestamp === "string") {
|
|
246
|
+
const t = Date.parse(e.timestamp);
|
|
247
|
+
if (!Number.isNaN(t)) {
|
|
248
|
+
result.lastTsMs = t;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// skip partial lines from buffer boundary
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
fs.closeSync(fd);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// unreadable / vanished file — caller falls back to stat times
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
*#walkJsonl(dir) {
|
|
269
|
+
try {
|
|
270
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
271
|
+
const full = path.join(dir, entry.name);
|
|
272
|
+
if (entry.isDirectory())
|
|
273
|
+
yield* this.#walkJsonl(full);
|
|
274
|
+
else if (entry.name.endsWith(".jsonl"))
|
|
275
|
+
yield full;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// permission errors etc.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
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 os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { extractInlineRefMentions } from "../inline-refs";
|
|
8
|
+
function getOpenCodeBaseDir() {
|
|
9
|
+
if (process.platform === "darwin") {
|
|
10
|
+
return path.join(os.homedir(), "Library", "Application Support", "opencode");
|
|
11
|
+
}
|
|
12
|
+
return path.join(os.homedir(), ".local", "share", "opencode");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Opencode storage layout (observed 2026-05):
|
|
16
|
+
* <base>/storage/session/<projectId>/<sessionId>.json — metadata
|
|
17
|
+
* <base>/storage/message/<sessionId>/<messageId>.json — one per message
|
|
18
|
+
*
|
|
19
|
+
* Older builds wrote logs directly into `<base>/log/` and `<base>/*.log`;
|
|
20
|
+
* those are still scanned by {@link OpenCodeProvider.readEvents} for
|
|
21
|
+
* backward compatibility with the existing failure-pattern aggregator.
|
|
22
|
+
*/
|
|
23
|
+
export class OpenCodeProvider {
|
|
24
|
+
name = "opencode";
|
|
25
|
+
#baseDir = getOpenCodeBaseDir();
|
|
26
|
+
isAvailable() {
|
|
27
|
+
return fs.existsSync(this.#baseDir);
|
|
28
|
+
}
|
|
29
|
+
*readEvents(input) {
|
|
30
|
+
// Legacy behavior: stream raw log lines from the top-level dir and `log/`
|
|
31
|
+
// subdirectory. Kept to keep `getExecutionLogCandidates` working without
|
|
32
|
+
// a coordinated change to its caller. New code should use
|
|
33
|
+
// {@link listSessions} + {@link readSession} instead.
|
|
34
|
+
const candidates = [this.#baseDir, path.join(this.#baseDir, "log")];
|
|
35
|
+
for (const dir of candidates) {
|
|
36
|
+
if (!fs.existsSync(dir))
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
for (const file of fs.readdirSync(dir)) {
|
|
40
|
+
const full = path.join(dir, file);
|
|
41
|
+
let stat;
|
|
42
|
+
try {
|
|
43
|
+
stat = fs.statSync(full);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!stat.isFile())
|
|
49
|
+
continue;
|
|
50
|
+
if (stat.mtimeMs < input.sinceMs)
|
|
51
|
+
continue;
|
|
52
|
+
if (!file.endsWith(".json") && !file.endsWith(".jsonl") && !file.endsWith(".log"))
|
|
53
|
+
continue;
|
|
54
|
+
const content = fs.readFileSync(full, "utf8");
|
|
55
|
+
const lines = content.includes("\n") ? content.split("\n") : [content];
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
try {
|
|
58
|
+
const entry = JSON.parse(line);
|
|
59
|
+
const text = entry?.content ?? entry?.message ?? entry?.text ?? "";
|
|
60
|
+
if (typeof text !== "string" || text.length < 10)
|
|
61
|
+
continue;
|
|
62
|
+
yield {
|
|
63
|
+
harness: this.name,
|
|
64
|
+
text,
|
|
65
|
+
ts: typeof entry?.timestamp === "number" ? entry.timestamp : stat.mtimeMs,
|
|
66
|
+
sessionId: typeof entry?.sessionId === "string" ? entry.sessionId : undefined,
|
|
67
|
+
role: typeof entry?.role === "string" ? entry.role : "unknown",
|
|
68
|
+
filePath: full,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// skip malformed
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// unreadable dir — skip
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
listSessions(input = {}) {
|
|
83
|
+
const base = input.location ?? this.#baseDir;
|
|
84
|
+
const sinceMs = input.sinceMs ?? 0;
|
|
85
|
+
const sessionRoot = path.join(base, "storage", "session");
|
|
86
|
+
if (!fs.existsSync(sessionRoot))
|
|
87
|
+
return [];
|
|
88
|
+
const summaries = [];
|
|
89
|
+
try {
|
|
90
|
+
for (const projectId of fs.readdirSync(sessionRoot)) {
|
|
91
|
+
const projectDir = path.join(sessionRoot, projectId);
|
|
92
|
+
let pstat;
|
|
93
|
+
try {
|
|
94
|
+
pstat = fs.statSync(projectDir);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!pstat.isDirectory())
|
|
100
|
+
continue;
|
|
101
|
+
for (const file of fs.readdirSync(projectDir)) {
|
|
102
|
+
if (!file.endsWith(".json"))
|
|
103
|
+
continue;
|
|
104
|
+
const filePath = path.join(projectDir, file);
|
|
105
|
+
let stat;
|
|
106
|
+
try {
|
|
107
|
+
stat = fs.statSync(filePath);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (stat.mtimeMs < sinceMs)
|
|
113
|
+
continue;
|
|
114
|
+
let meta;
|
|
115
|
+
try {
|
|
116
|
+
meta = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const sessionId = typeof meta?.id === "string" ? meta.id : path.basename(file, ".json");
|
|
122
|
+
const time = meta?.time ?? undefined;
|
|
123
|
+
const startedAt = typeof time?.created === "number" ? time.created : stat.ctimeMs;
|
|
124
|
+
const endedAt = typeof time?.updated === "number" ? time.updated : stat.mtimeMs;
|
|
125
|
+
const title = typeof meta?.title === "string" ? meta.title : undefined;
|
|
126
|
+
const projectHint = typeof meta?.directory === "string" ? meta.directory : projectId;
|
|
127
|
+
summaries.push({
|
|
128
|
+
harness: this.name,
|
|
129
|
+
sessionId,
|
|
130
|
+
filePath,
|
|
131
|
+
startedAt,
|
|
132
|
+
endedAt,
|
|
133
|
+
projectHint,
|
|
134
|
+
...(title ? { title } : {}),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// unreadable session root — return what we have
|
|
141
|
+
}
|
|
142
|
+
return summaries.sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0));
|
|
143
|
+
}
|
|
144
|
+
readSession(ref) {
|
|
145
|
+
let meta = {};
|
|
146
|
+
try {
|
|
147
|
+
meta = JSON.parse(fs.readFileSync(ref.filePath, "utf8"));
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// metadata missing — proceed with empty defaults
|
|
151
|
+
}
|
|
152
|
+
const time = meta.time ?? undefined;
|
|
153
|
+
const startedAt = typeof time?.created === "number" ? time.created : undefined;
|
|
154
|
+
const endedAt = typeof time?.updated === "number" ? time.updated : undefined;
|
|
155
|
+
const title = typeof meta.title === "string" ? meta.title : undefined;
|
|
156
|
+
const projectHint = typeof meta.directory === "string" ? meta.directory : undefined;
|
|
157
|
+
const events = [];
|
|
158
|
+
const inlineRefs = [];
|
|
159
|
+
// Resolve message directory: <baseDir>/storage/message/<sessionId>/
|
|
160
|
+
const inferredBase = this.#inferBaseFromSessionPath(ref.filePath) ?? this.#baseDir;
|
|
161
|
+
const msgDir = path.join(inferredBase, "storage", "message", ref.sessionId);
|
|
162
|
+
if (fs.existsSync(msgDir)) {
|
|
163
|
+
try {
|
|
164
|
+
const files = fs.readdirSync(msgDir).filter((f) => f.endsWith(".json"));
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
const full = path.join(msgDir, file);
|
|
167
|
+
let msg;
|
|
168
|
+
try {
|
|
169
|
+
msg = JSON.parse(fs.readFileSync(full, "utf8"));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (!msg)
|
|
175
|
+
continue;
|
|
176
|
+
const evt = this.#messageToEvent(msg, ref.sessionId, full);
|
|
177
|
+
if (evt) {
|
|
178
|
+
events.push(evt);
|
|
179
|
+
inlineRefs.push(...extractInlineRefMentions(evt.text, evt.ts));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// unreadable msg dir — skip
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
events.sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
|
|
188
|
+
return {
|
|
189
|
+
ref: {
|
|
190
|
+
harness: this.name,
|
|
191
|
+
sessionId: ref.sessionId,
|
|
192
|
+
filePath: ref.filePath,
|
|
193
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
194
|
+
...(endedAt !== undefined ? { endedAt } : {}),
|
|
195
|
+
...(projectHint ? { projectHint } : {}),
|
|
196
|
+
...(title ? { title } : {}),
|
|
197
|
+
},
|
|
198
|
+
events,
|
|
199
|
+
inlineRefs,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Derive opencode base dir from a session metadata file path so a caller
|
|
204
|
+
* passing a custom `--location` can still find the message dir.
|
|
205
|
+
* Layout: `<base>/storage/session/<projectId>/<id>.json` → base.
|
|
206
|
+
*/
|
|
207
|
+
#inferBaseFromSessionPath(filePath) {
|
|
208
|
+
// Walk up: <id>.json → <projectId> → session → storage → <base>
|
|
209
|
+
const dir = path.dirname(filePath);
|
|
210
|
+
const parts = dir.split(path.sep);
|
|
211
|
+
if (parts.length < 3)
|
|
212
|
+
return undefined;
|
|
213
|
+
const last = parts[parts.length - 1];
|
|
214
|
+
const sndLast = parts[parts.length - 2];
|
|
215
|
+
const thirdLast = parts[parts.length - 3];
|
|
216
|
+
if (sndLast !== "session" || thirdLast !== "storage" || !last)
|
|
217
|
+
return undefined;
|
|
218
|
+
return parts.slice(0, parts.length - 3).join(path.sep);
|
|
219
|
+
}
|
|
220
|
+
#messageToEvent(msg, sessionId, filePath) {
|
|
221
|
+
const time = msg.time ?? undefined;
|
|
222
|
+
const ts = typeof time?.created === "number" ? time.created : typeof msg.timestamp === "number" ? msg.timestamp : 0;
|
|
223
|
+
const role = typeof msg.role === "string" ? msg.role : "unknown";
|
|
224
|
+
// Opencode message bodies live in summary.title / summary.diffs[].before/after /
|
|
225
|
+
// parts (referenced from storage/part/<msg-id>/). For listing+extraction
|
|
226
|
+
// purposes the summary block is sufficient — it's what the platform itself
|
|
227
|
+
// surfaces as the message preview.
|
|
228
|
+
const summary = msg.summary;
|
|
229
|
+
const parts = [];
|
|
230
|
+
if (typeof summary?.title === "string")
|
|
231
|
+
parts.push(summary.title);
|
|
232
|
+
if (Array.isArray(summary?.parts)) {
|
|
233
|
+
for (const p of summary.parts) {
|
|
234
|
+
if (typeof p === "string")
|
|
235
|
+
parts.push(p);
|
|
236
|
+
else if (p && typeof p === "object") {
|
|
237
|
+
const text = p.text;
|
|
238
|
+
if (typeof text === "string")
|
|
239
|
+
parts.push(text);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// content field for some opencode versions
|
|
244
|
+
if (typeof msg.content === "string")
|
|
245
|
+
parts.push(msg.content);
|
|
246
|
+
const text = parts.join("\n").trim();
|
|
247
|
+
if (text.length < 1)
|
|
248
|
+
return undefined;
|
|
249
|
+
return {
|
|
250
|
+
harness: this.name,
|
|
251
|
+
text,
|
|
252
|
+
ts: ts || undefined,
|
|
253
|
+
sessionId,
|
|
254
|
+
role,
|
|
255
|
+
filePath,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|