akm-cli 0.8.0-rc2 → 0.8.1
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} +238 -3
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/assets/help/help-accept.md +12 -0
- package/dist/assets/help/help-improve.md +81 -0
- package/dist/{commands → assets}/help/help-proposals.md +7 -4
- package/dist/assets/help/help-reject.md +11 -0
- package/dist/{output → assets/hints}/cli-hints-full.md +60 -32
- package/dist/{output → assets/hints}/cli-hints-short.md +10 -7
- package/dist/assets/profiles/default.json +15 -0
- package/dist/assets/profiles/graph-refresh.json +13 -0
- package/dist/assets/profiles/memory-focus.json +12 -0
- package/dist/assets/profiles/quick.json +15 -0
- package/dist/assets/profiles/thorough.json +15 -0
- package/dist/assets/prompts/extract-session.md +80 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +10 -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 +1557 -147
- 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 +217 -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 +1042 -55
- 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 +138 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1736 -346
- package/dist/commands/info.js +26 -28
- 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 +86 -16
- 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 +402 -31
- 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/output/cli-hints.js +7 -4
- 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 +4 -1
- package/dist/tasks/backends/schtasks.js +4 -1
- 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 +6 -3
- package/dist/wiki/wiki.js +4 -1
- package/dist/workflows/authoring.js +4 -1
- 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/help/help-accept.md +0 -9
- package/dist/commands/help/help-improve.md +0 -53
- package/dist/commands/help/help-reject.md +0 -8
- 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
- package/dist/llm/prompts/graph-extract-user-prompt.md +0 -12
- /package/dist/{tasks → assets}/backends/launchd-template.xml +0 -0
- /package/dist/{tasks → assets}/backends/schtasks-template.xml +0 -0
- /package/dist/{commands → assets}/help/help-propose.md +0 -0
- /package/dist/{wiki → assets/wiki}/index-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/ingest-workflow-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/log-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/schema-template.md +0 -0
- /package/dist/{workflows → assets/workflows}/workflow-template.md +0 -0
|
@@ -1,6 +1,11 @@
|
|
|
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
|
import fs from "node:fs";
|
|
2
5
|
import path from "node:path";
|
|
3
6
|
import { writeFileAtomic } from "../core/common";
|
|
7
|
+
import { rethrowIfTestIsolationError } from "../core/errors";
|
|
8
|
+
import { probeLock, releaseLock, tryAcquireLockSync } from "../core/file-lock";
|
|
4
9
|
import { getDataDir } from "../core/paths";
|
|
5
10
|
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
6
11
|
const LOCKFILE_NAME = "akm.lock";
|
|
@@ -16,64 +21,26 @@ function getLockSentinelPath() {
|
|
|
16
21
|
}
|
|
17
22
|
async function acquireLockSentinel() {
|
|
18
23
|
const sentinelPath = getLockSentinelPath();
|
|
19
|
-
// Ensure the directory exists before attempting to create the sentinel
|
|
24
|
+
// Ensure the directory exists before attempting to create the sentinel.
|
|
20
25
|
fs.mkdirSync(path.dirname(sentinelPath), { recursive: true });
|
|
21
26
|
for (let attempt = 0; attempt < LOCK_MAX_RETRIES; attempt++) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return true; // Sentinel created — we own the lock
|
|
27
|
+
if (tryAcquireLockSync(sentinelPath, String(process.pid))) {
|
|
28
|
+
return true; // Sentinel created — we own the lock.
|
|
25
29
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Check for stale lock — if the owning PID is no longer running, reclaim it
|
|
30
|
-
if (tryReclaimStaleSentinel(sentinelPath)) {
|
|
31
|
-
continue; // Sentinel removed — retry immediately
|
|
32
|
-
}
|
|
33
|
-
// Another process holds the lock — wait briefly before retrying
|
|
34
|
-
if (attempt < LOCK_MAX_RETRIES - 1) {
|
|
35
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
|
|
36
|
-
}
|
|
30
|
+
if (probeLock(sentinelPath).state === "stale") {
|
|
31
|
+
releaseLock(sentinelPath);
|
|
32
|
+
continue; // Reclaimed — retry immediately.
|
|
37
33
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Check if the sentinel was left by a dead process and remove it if so.
|
|
44
|
-
* Returns true if the sentinel was reclaimed (removed).
|
|
45
|
-
*/
|
|
46
|
-
function tryReclaimStaleSentinel(sentinelPath) {
|
|
47
|
-
try {
|
|
48
|
-
const content = fs.readFileSync(sentinelPath, "utf8").trim();
|
|
49
|
-
const pid = parseInt(content, 10);
|
|
50
|
-
if (Number.isNaN(pid) || pid <= 0) {
|
|
51
|
-
// Invalid PID in sentinel — reclaim it
|
|
52
|
-
fs.unlinkSync(sentinelPath);
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
// Check if the process is still alive (signal 0 doesn't kill, just checks)
|
|
56
|
-
try {
|
|
57
|
-
process.kill(pid, 0);
|
|
58
|
-
return false; // Process is alive — lock is valid
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
// Process is dead — reclaim the stale lock
|
|
62
|
-
fs.unlinkSync(sentinelPath);
|
|
63
|
-
return true;
|
|
34
|
+
// Another process holds the lock — wait briefly before retrying.
|
|
35
|
+
if (attempt < LOCK_MAX_RETRIES - 1) {
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
|
|
64
37
|
}
|
|
65
38
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
39
|
+
// Best-effort: proceed without the lock rather than failing the install.
|
|
40
|
+
return false;
|
|
69
41
|
}
|
|
70
42
|
function releaseLockSentinel() {
|
|
71
|
-
|
|
72
|
-
fs.unlinkSync(getLockSentinelPath());
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
/* ignore — sentinel may already be gone */
|
|
76
|
-
}
|
|
43
|
+
releaseLock(getLockSentinelPath());
|
|
77
44
|
}
|
|
78
45
|
// ── Read / Write ────────────────────────────────────────────────────────────
|
|
79
46
|
export function readLockfile() {
|
|
@@ -84,7 +51,11 @@ export function readLockfile() {
|
|
|
84
51
|
return [];
|
|
85
52
|
return raw.filter(isValidLockfileEntry);
|
|
86
53
|
}
|
|
87
|
-
catch {
|
|
54
|
+
catch (err) {
|
|
55
|
+
// Defense-in-depth: getLockfilePath() is outside this try block, but a
|
|
56
|
+
// future refactor that pushes a getDataDir() call inside must not mask
|
|
57
|
+
// the bun-test isolation guard as "empty lockfile".
|
|
58
|
+
rethrowIfTestIsolationError(err);
|
|
88
59
|
return [];
|
|
89
60
|
}
|
|
90
61
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
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
|
import { ClaudeCodeProvider } from "./providers/claude-code";
|
|
2
5
|
import { OpenCodeProvider } from "./providers/opencode";
|
|
6
|
+
export { extractInlineRefMentions } from "./inline-refs";
|
|
3
7
|
const HARNESSES = [new ClaudeCodeProvider(), new OpenCodeProvider()];
|
|
4
8
|
const ERROR_PATTERNS = /error|failed|exception|cannot|undefined|null pointer|ENOENT|timeout/i;
|
|
5
9
|
/**
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
const REMEMBER_RE = /\bakm\s+remember\s+(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)')/g;
|
|
5
|
+
const FEEDBACK_RE = /\bakm\s+feedback\s+(\S+)(?:\s+--[a-z-]+)*\s+(?:--note|-n)\s+(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)')/g;
|
|
6
|
+
export function extractInlineRefMentions(text, ts) {
|
|
7
|
+
if (!text || text.length < 10)
|
|
8
|
+
return [];
|
|
9
|
+
const out = [];
|
|
10
|
+
REMEMBER_RE.lastIndex = 0;
|
|
11
|
+
for (const m of text.matchAll(REMEMBER_RE)) {
|
|
12
|
+
const body = m[1] ?? m[2] ?? "";
|
|
13
|
+
if (!body.trim())
|
|
14
|
+
continue;
|
|
15
|
+
out.push({
|
|
16
|
+
kind: "remember",
|
|
17
|
+
text: body,
|
|
18
|
+
...(ts !== undefined ? { ts } : {}),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
FEEDBACK_RE.lastIndex = 0;
|
|
22
|
+
for (const m of text.matchAll(FEEDBACK_RE)) {
|
|
23
|
+
const ref = m[1] ?? "";
|
|
24
|
+
const note = m[2] ?? m[3] ?? "";
|
|
25
|
+
if (!ref)
|
|
26
|
+
continue;
|
|
27
|
+
out.push({
|
|
28
|
+
kind: "feedback",
|
|
29
|
+
ref,
|
|
30
|
+
text: note,
|
|
31
|
+
...(ts !== undefined ? { ts } : {}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,7 +1,80 @@
|
|
|
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
|
import fs from "node:fs";
|
|
2
5
|
import os from "node:os";
|
|
3
6
|
import path from "node:path";
|
|
7
|
+
import { extractInlineRefMentions } from "../inline-refs";
|
|
4
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
|
+
}
|
|
5
78
|
export class ClaudeCodeProvider {
|
|
6
79
|
name = "claude-code";
|
|
7
80
|
isAvailable() {
|
|
@@ -39,6 +112,159 @@ export class ClaudeCodeProvider {
|
|
|
39
112
|
return;
|
|
40
113
|
}
|
|
41
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
|
+
}
|
|
42
268
|
*#walkJsonl(dir) {
|
|
43
269
|
try {
|
|
44
270
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|