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
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* Agent CLI spawn wrapper (v1 spec §12.2).
|
|
3
6
|
*
|
|
@@ -11,6 +14,10 @@
|
|
|
11
14
|
* NEVER imports an LLM SDK. Agents are reachable only via shell-out;
|
|
12
15
|
* this is a pre-emptive guarantee against the #222 invariant.
|
|
13
16
|
*/
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { getCommandBuilder } from "./builders";
|
|
14
21
|
import { DEFAULT_AGENT_TIMEOUT_MS } from "./config";
|
|
15
22
|
/**
|
|
16
23
|
* Kill the process group of `proc` with `signal`, falling back to
|
|
@@ -21,7 +28,7 @@ import { DEFAULT_AGENT_TIMEOUT_MS } from "./config";
|
|
|
21
28
|
* reaped alongside the node wrapper. The fallback keeps test fakes working
|
|
22
29
|
* without modification.
|
|
23
30
|
*/
|
|
24
|
-
|
|
31
|
+
function killGroup(proc, signal) {
|
|
25
32
|
if (typeof proc.pid === "number") {
|
|
26
33
|
try {
|
|
27
34
|
process.kill(-proc.pid, signal);
|
|
@@ -39,6 +46,60 @@ export function killGroup(proc, signal) {
|
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
const DEFAULT_TIMEOUT_MS = DEFAULT_AGENT_TIMEOUT_MS;
|
|
49
|
+
/**
|
|
50
|
+
* Supplement `existingPath` with well-known user binary directories when
|
|
51
|
+
* running in a scheduler context (cron/launchd) where PATH is stripped.
|
|
52
|
+
*
|
|
53
|
+
* Detection heuristic: if the current PATH does not contain the user's home
|
|
54
|
+
* directory, we are likely in a stripped scheduler env. In an interactive
|
|
55
|
+
* shell the user's home almost always appears (e.g. ~/.bun/bin, ~/.cargo/bin).
|
|
56
|
+
*
|
|
57
|
+
* Only directories that actually exist on disk are prepended, and only if
|
|
58
|
+
* they are not already present, so interactive-shell PATH ordering is never
|
|
59
|
+
* disturbed.
|
|
60
|
+
*/
|
|
61
|
+
export function supplementPathForSchedulerContext(existingPath) {
|
|
62
|
+
const home = os.homedir();
|
|
63
|
+
// If PATH already contains the home directory, we are in an interactive
|
|
64
|
+
// shell — skip supplementation entirely.
|
|
65
|
+
if (existingPath.split(path.delimiter).some((d) => d.startsWith(home))) {
|
|
66
|
+
return existingPath;
|
|
67
|
+
}
|
|
68
|
+
const candidates = pathCandidatesForCurrentPlatform(home);
|
|
69
|
+
const existing = new Set(existingPath.split(path.delimiter).filter(Boolean));
|
|
70
|
+
const toAdd = candidates.filter((d) => !existing.has(d) && fs.existsSync(d));
|
|
71
|
+
if (toAdd.length === 0)
|
|
72
|
+
return existingPath;
|
|
73
|
+
return [...toAdd, existingPath].filter(Boolean).join(path.delimiter);
|
|
74
|
+
}
|
|
75
|
+
function pathCandidatesForCurrentPlatform(home) {
|
|
76
|
+
if (process.platform === "win32") {
|
|
77
|
+
// Windows: Bun + Cargo + Scoop + Chocolatey + system tools. Order favors
|
|
78
|
+
// user-local installs over machine-global so the user's chosen toolchain
|
|
79
|
+
// wins. These paths are commonly stripped from Task Scheduler / service
|
|
80
|
+
// environments, mirroring the cron/launchd problem on POSIX.
|
|
81
|
+
const localAppData = process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local");
|
|
82
|
+
const userProfile = process.env.USERPROFILE ?? home;
|
|
83
|
+
const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
|
|
84
|
+
return [
|
|
85
|
+
path.join(userProfile, ".bun", "bin"),
|
|
86
|
+
path.join(localAppData, "Programs", "bun"),
|
|
87
|
+
path.join(userProfile, ".cargo", "bin"),
|
|
88
|
+
path.join(localAppData, "Programs", "Git", "cmd"),
|
|
89
|
+
path.join(userProfile, "scoop", "shims"),
|
|
90
|
+
path.join(programFiles, "Git", "cmd"),
|
|
91
|
+
"C:\\ProgramData\\chocolatey\\bin",
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
return [
|
|
95
|
+
path.join(home, ".bun", "bin"),
|
|
96
|
+
path.join(home, ".cargo", "bin"),
|
|
97
|
+
path.join(home, ".local", "bin"),
|
|
98
|
+
"/opt/homebrew/bin",
|
|
99
|
+
"/opt/homebrew/sbin",
|
|
100
|
+
"/usr/local/bin",
|
|
101
|
+
];
|
|
102
|
+
}
|
|
42
103
|
function resolveSpawnFn(options) {
|
|
43
104
|
if (options.spawn)
|
|
44
105
|
return options.spawn;
|
|
@@ -54,6 +115,10 @@ function resolveSpawnFn(options) {
|
|
|
54
115
|
* • Every name in `profile.envPassthrough`.
|
|
55
116
|
* • Every entry in `profile.env`.
|
|
56
117
|
* • Every entry in `options.env` (highest precedence).
|
|
118
|
+
*
|
|
119
|
+
* PATH is supplemented with well-known user binary directories when running
|
|
120
|
+
* in a scheduler context (cron/launchd) where the inherited PATH is stripped.
|
|
121
|
+
* See {@link supplementPathForSchedulerContext}.
|
|
57
122
|
*/
|
|
58
123
|
function buildChildEnv(profile, options) {
|
|
59
124
|
const source = options.envSource ?? process.env;
|
|
@@ -63,6 +128,11 @@ function buildChildEnv(profile, options) {
|
|
|
63
128
|
if (value !== undefined)
|
|
64
129
|
env[name] = value;
|
|
65
130
|
}
|
|
131
|
+
// Supplement PATH after passthrough so the scheduler-context fix applies to
|
|
132
|
+
// the value actually coming from the environment source.
|
|
133
|
+
if (env.PATH !== undefined) {
|
|
134
|
+
env.PATH = supplementPathForSchedulerContext(env.PATH);
|
|
135
|
+
}
|
|
66
136
|
if (profile.env) {
|
|
67
137
|
for (const [k, v] of Object.entries(profile.env))
|
|
68
138
|
env[k] = v;
|
|
@@ -109,19 +179,35 @@ async function readStream(stream, opts) {
|
|
|
109
179
|
*/
|
|
110
180
|
export async function runAgent(profile, prompt, options = {}) {
|
|
111
181
|
const stdioMode = options.stdio ?? profile.stdio;
|
|
112
|
-
|
|
182
|
+
// null = explicitly disabled (no kill timer). undefined = inherit from profile/default.
|
|
183
|
+
const timeoutMs = options.timeoutMs !== undefined ? options.timeoutMs : (profile.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
113
184
|
const parseOutput = options.parseOutput ?? profile.parseOutput;
|
|
114
185
|
const setTimeoutImpl = options.setTimeoutFn ?? setTimeout;
|
|
115
186
|
const clearTimeoutImpl = options.clearTimeoutFn ?? clearTimeout;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
187
|
+
// Build argv via the platform-specific builder when dispatch params are
|
|
188
|
+
// provided; fall back to the legacy positional-prompt form otherwise.
|
|
189
|
+
let builtArgv;
|
|
190
|
+
let builtEnv;
|
|
191
|
+
if (options.dispatch !== undefined) {
|
|
192
|
+
const builder = getCommandBuilder(profile.commandBuilder ?? profile.name, options.builderRegistry);
|
|
193
|
+
const built = builder.build(profile, options.dispatch);
|
|
194
|
+
builtArgv = built.argv;
|
|
195
|
+
builtEnv = built.env;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const legacyArgs = [...profile.args, ...(options.args ?? [])];
|
|
199
|
+
if (prompt !== undefined)
|
|
200
|
+
legacyArgs.push(prompt);
|
|
201
|
+
builtArgv = [profile.bin, ...legacyArgs];
|
|
202
|
+
}
|
|
203
|
+
// Extra args (e.g. forwarded CLI positionals) are appended after the builder output.
|
|
204
|
+
const finalArgv = [...builtArgv, ...(options.dispatch ? (options.args ?? []) : [])];
|
|
205
|
+
const env = { ...buildChildEnv(profile, options), ...(builtEnv ?? {}) };
|
|
120
206
|
const start = Date.now();
|
|
121
207
|
let proc;
|
|
122
208
|
try {
|
|
123
209
|
const spawnFn = resolveSpawnFn(options);
|
|
124
|
-
proc = spawnFn(
|
|
210
|
+
proc = spawnFn(finalArgv, {
|
|
125
211
|
stdin: stdioMode === "captured" ? (options.stdin !== undefined ? "pipe" : "ignore") : "inherit",
|
|
126
212
|
stdout: stdioMode === "captured" ? "pipe" : "inherit",
|
|
127
213
|
stderr: stdioMode === "captured" ? "pipe" : "inherit",
|
|
@@ -153,26 +239,34 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
153
239
|
// BUG-M3: only flag `timedOut` when the child has not already exited. A
|
|
154
240
|
// timer firing in the same microtask as `proc.exited` resolving could
|
|
155
241
|
// otherwise label a clean exit as a timeout.
|
|
242
|
+
//
|
|
243
|
+
// When timeoutMs is null the kill timer is skipped entirely — the task runs
|
|
244
|
+
// until the process exits naturally. Intended for long-running local-model
|
|
245
|
+
// tasks where wall-clock time is unpredictable.
|
|
156
246
|
let timedOut = false;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
killGroup(proc, "SIGTERM");
|
|
162
|
-
// Follow up with SIGKILL after 5 s in case the process ignores SIGTERM.
|
|
163
|
-
setTimeoutImpl(() => {
|
|
164
|
-
if (proc.exitCode !== null)
|
|
247
|
+
let timer;
|
|
248
|
+
if (timeoutMs !== null) {
|
|
249
|
+
timer = setTimeoutImpl(() => {
|
|
250
|
+
if (!proc || proc.exitCode !== null)
|
|
165
251
|
return;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
252
|
+
timedOut = true;
|
|
253
|
+
killGroup(proc, "SIGTERM");
|
|
254
|
+
// Follow up with SIGKILL after 5 s in case the process ignores SIGTERM.
|
|
255
|
+
setTimeoutImpl(() => {
|
|
256
|
+
if (!proc || proc.exitCode !== null)
|
|
257
|
+
return;
|
|
258
|
+
killGroup(proc, "SIGKILL");
|
|
259
|
+
}, 5000);
|
|
260
|
+
}, timeoutMs);
|
|
261
|
+
}
|
|
169
262
|
// Stream-drain timeout: the overall wall-clock budget plus a 2 s grace
|
|
170
263
|
// period. When a process is killed via SIGTERM/SIGKILL (from our timeout
|
|
171
264
|
// handler or from outside) some runtimes keep the pipe write-end open in
|
|
172
265
|
// background threads, which would cause `Response.text()` to block forever.
|
|
173
|
-
// Capping stream draining
|
|
174
|
-
//
|
|
175
|
-
|
|
266
|
+
// Capping stream draining ensures the caller never hangs past the wall
|
|
267
|
+
// budget regardless of subprocess pipe behaviour.
|
|
268
|
+
// When there is no kill timer, allow up to 30 s for streams to drain.
|
|
269
|
+
const streamDrainTimeoutMs = timeoutMs !== null ? timeoutMs + 2_000 : 30_000;
|
|
176
270
|
const stdoutPromise = stdioMode === "captured"
|
|
177
271
|
? readStream(proc.stdout ?? null, { timeoutMs: streamDrainTimeoutMs })
|
|
178
272
|
: Promise.resolve("");
|
|
@@ -209,7 +303,8 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
209
303
|
exitCode = await proc.exited;
|
|
210
304
|
}
|
|
211
305
|
catch (err) {
|
|
212
|
-
|
|
306
|
+
if (timer !== undefined)
|
|
307
|
+
clearTimeoutImpl(timer);
|
|
213
308
|
// BUG-H2: drain stream readers before the early return so they don't
|
|
214
309
|
// surface as unhandled rejections after the function resolves.
|
|
215
310
|
// The streams already carry a built-in drain timeout so this allSettled
|
|
@@ -237,7 +332,7 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
237
332
|
stderr,
|
|
238
333
|
durationMs,
|
|
239
334
|
reason: "timeout",
|
|
240
|
-
error: `agent CLI "${profile.name}" timed out after ${timeoutMs}ms`,
|
|
335
|
+
error: `agent CLI "${profile.name}" timed out after ${timeoutMs ?? 0}ms`,
|
|
241
336
|
};
|
|
242
337
|
}
|
|
243
338
|
if (exitCode !== 0) {
|
|
@@ -252,21 +347,74 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
252
347
|
};
|
|
253
348
|
}
|
|
254
349
|
if (parseOutput === "json" && stdioMode === "captured") {
|
|
350
|
+
// Strip <think> blocks and code fences, then try direct parse with
|
|
351
|
+
// embedded-JSON fallback for local LLMs that emit prose around the payload.
|
|
352
|
+
const cleaned = stdout
|
|
353
|
+
.trim()
|
|
354
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
355
|
+
.trim()
|
|
356
|
+
.replace(/^```(?:json)?\s*\n?/, "")
|
|
357
|
+
.replace(/\n?```\s*$/, "")
|
|
358
|
+
.trim();
|
|
359
|
+
let parsed;
|
|
255
360
|
try {
|
|
256
|
-
|
|
257
|
-
return { ok: true, exitCode, stdout, stderr, durationMs, parsed };
|
|
361
|
+
parsed = JSON.parse(cleaned);
|
|
258
362
|
}
|
|
259
|
-
catch
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
363
|
+
catch {
|
|
364
|
+
// Fallback: extract the first balanced {…} from prose output.
|
|
365
|
+
let found;
|
|
366
|
+
for (let s = 0; s < cleaned.length; s++) {
|
|
367
|
+
if (cleaned[s] !== "{")
|
|
368
|
+
continue;
|
|
369
|
+
let depth = 0, inStr = false, esc = false;
|
|
370
|
+
for (let i = s; i < cleaned.length; i++) {
|
|
371
|
+
const c = cleaned[i];
|
|
372
|
+
if (inStr) {
|
|
373
|
+
if (esc) {
|
|
374
|
+
esc = false;
|
|
375
|
+
}
|
|
376
|
+
else if (c === "\\") {
|
|
377
|
+
esc = true;
|
|
378
|
+
}
|
|
379
|
+
else if (c === '"') {
|
|
380
|
+
inStr = false;
|
|
381
|
+
}
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (c === '"') {
|
|
385
|
+
inStr = true;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (c === "{")
|
|
389
|
+
depth++;
|
|
390
|
+
if (c === "}") {
|
|
391
|
+
depth--;
|
|
392
|
+
if (depth === 0) {
|
|
393
|
+
try {
|
|
394
|
+
found = JSON.parse(cleaned.slice(s, i + 1));
|
|
395
|
+
}
|
|
396
|
+
catch { }
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (found !== undefined)
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
if (found === undefined) {
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
exitCode,
|
|
408
|
+
stdout,
|
|
409
|
+
stderr,
|
|
410
|
+
durationMs,
|
|
411
|
+
reason: "parse_error",
|
|
412
|
+
error: "no JSON object found in agent output",
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
parsed = found;
|
|
269
416
|
}
|
|
417
|
+
return { ok: true, exitCode, stdout, stderr, durationMs, parsed };
|
|
270
418
|
}
|
|
271
419
|
return { ok: true, exitCode, stdout, stderr, durationMs };
|
|
272
420
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import * as childProcess from "node:child_process";
|
|
2
5
|
export const GITHUB_API_BASE = "https://api.github.com";
|
|
3
6
|
const GITHUB_TOKEN_DOMAINS = new Set(["api.github.com", "github.com", "uploads.github.com"]);
|
|
@@ -1,77 +1,46 @@
|
|
|
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
|
-
import {
|
|
6
|
+
import { writeFileAtomic } from "../core/common";
|
|
7
|
+
import { rethrowIfTestIsolationError } from "../core/errors";
|
|
8
|
+
import { probeLock, releaseLock, tryAcquireLockSync } from "../core/file-lock";
|
|
9
|
+
import { getDataDir } from "../core/paths";
|
|
4
10
|
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
5
11
|
const LOCKFILE_NAME = "akm.lock";
|
|
6
12
|
function getLockfilePath() {
|
|
7
|
-
return path.join(
|
|
13
|
+
return path.join(getDataDir(), LOCKFILE_NAME);
|
|
8
14
|
}
|
|
9
15
|
// ── Lock sentinel ────────────────────────────────────────────────────────────
|
|
10
16
|
const LOCK_MAX_RETRIES = 3;
|
|
11
17
|
const LOCK_RETRY_DELAY_MS = 100;
|
|
12
18
|
function getLockSentinelPath() {
|
|
13
|
-
|
|
19
|
+
// The sentinel always lives next to the lock file it guards.
|
|
20
|
+
return `${path.join(getDataDir(), LOCKFILE_NAME)}.lck`;
|
|
14
21
|
}
|
|
15
22
|
async function acquireLockSentinel() {
|
|
16
23
|
const sentinelPath = getLockSentinelPath();
|
|
17
|
-
// Ensure the directory exists before attempting to create the sentinel
|
|
24
|
+
// Ensure the directory exists before attempting to create the sentinel.
|
|
18
25
|
fs.mkdirSync(path.dirname(sentinelPath), { recursive: true });
|
|
19
26
|
for (let attempt = 0; attempt < LOCK_MAX_RETRIES; attempt++) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return true; // Sentinel created — we own the lock
|
|
27
|
+
if (tryAcquireLockSync(sentinelPath, String(process.pid))) {
|
|
28
|
+
return true; // Sentinel created — we own the lock.
|
|
23
29
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Check for stale lock — if the owning PID is no longer running, reclaim it
|
|
28
|
-
if (tryReclaimStaleSentinel(sentinelPath)) {
|
|
29
|
-
continue; // Sentinel removed — retry immediately
|
|
30
|
-
}
|
|
31
|
-
// Another process holds the lock — wait briefly before retrying
|
|
32
|
-
if (attempt < LOCK_MAX_RETRIES - 1) {
|
|
33
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
|
|
34
|
-
}
|
|
30
|
+
if (probeLock(sentinelPath).state === "stale") {
|
|
31
|
+
releaseLock(sentinelPath);
|
|
32
|
+
continue; // Reclaimed — retry immediately.
|
|
35
33
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Check if the sentinel was left by a dead process and remove it if so.
|
|
42
|
-
* Returns true if the sentinel was reclaimed (removed).
|
|
43
|
-
*/
|
|
44
|
-
function tryReclaimStaleSentinel(sentinelPath) {
|
|
45
|
-
try {
|
|
46
|
-
const content = fs.readFileSync(sentinelPath, "utf8").trim();
|
|
47
|
-
const pid = parseInt(content, 10);
|
|
48
|
-
if (Number.isNaN(pid) || pid <= 0) {
|
|
49
|
-
// Invalid PID in sentinel — reclaim it
|
|
50
|
-
fs.unlinkSync(sentinelPath);
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
// Check if the process is still alive (signal 0 doesn't kill, just checks)
|
|
54
|
-
try {
|
|
55
|
-
process.kill(pid, 0);
|
|
56
|
-
return false; // Process is alive — lock is valid
|
|
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));
|
|
57
37
|
}
|
|
58
|
-
catch {
|
|
59
|
-
// Process is dead — reclaim the stale lock
|
|
60
|
-
fs.unlinkSync(sentinelPath);
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return false; // Can't read or remove — leave it alone
|
|
66
38
|
}
|
|
39
|
+
// Best-effort: proceed without the lock rather than failing the install.
|
|
40
|
+
return false;
|
|
67
41
|
}
|
|
68
42
|
function releaseLockSentinel() {
|
|
69
|
-
|
|
70
|
-
fs.unlinkSync(getLockSentinelPath());
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
/* ignore — sentinel may already be gone */
|
|
74
|
-
}
|
|
43
|
+
releaseLock(getLockSentinelPath());
|
|
75
44
|
}
|
|
76
45
|
// ── Read / Write ────────────────────────────────────────────────────────────
|
|
77
46
|
export function readLockfile() {
|
|
@@ -82,28 +51,20 @@ export function readLockfile() {
|
|
|
82
51
|
return [];
|
|
83
52
|
return raw.filter(isValidLockfileEntry);
|
|
84
53
|
}
|
|
85
|
-
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);
|
|
86
59
|
return [];
|
|
87
60
|
}
|
|
88
61
|
}
|
|
89
62
|
export function writeLockfile(entries) {
|
|
90
|
-
|
|
63
|
+
// Always write to $DATA — never to the legacy $CONFIG location.
|
|
64
|
+
const lockfilePath = path.join(getDataDir(), LOCKFILE_NAME);
|
|
91
65
|
const dir = path.dirname(lockfilePath);
|
|
92
66
|
fs.mkdirSync(dir, { recursive: true });
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
fs.writeFileSync(tmpPath, `${JSON.stringify(entries, null, 2)}\n`, "utf8");
|
|
96
|
-
fs.renameSync(tmpPath, lockfilePath);
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
try {
|
|
100
|
-
fs.unlinkSync(tmpPath);
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
/* ignore cleanup failure */
|
|
104
|
-
}
|
|
105
|
-
throw err;
|
|
106
|
-
}
|
|
67
|
+
writeFileAtomic(lockfilePath, `${JSON.stringify(entries, null, 2)}\n`);
|
|
107
68
|
}
|
|
108
69
|
export async function upsertLockEntry(entry) {
|
|
109
70
|
const acquired = await acquireLockSentinel();
|
|
@@ -118,6 +79,8 @@ export async function upsertLockEntry(entry) {
|
|
|
118
79
|
}
|
|
119
80
|
}
|
|
120
81
|
export async function removeLockEntry(id) {
|
|
82
|
+
if (!fs.existsSync(getDataDir()))
|
|
83
|
+
return;
|
|
121
84
|
const acquired = await acquireLockSentinel();
|
|
122
85
|
try {
|
|
123
86
|
const entries = readLockfile();
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { ClaudeCodeProvider } from "./providers/claude-code";
|
|
5
|
+
import { OpenCodeProvider } from "./providers/opencode";
|
|
6
|
+
export { extractInlineRefMentions } from "./inline-refs";
|
|
7
|
+
const HARNESSES = [new ClaudeCodeProvider(), new OpenCodeProvider()];
|
|
8
|
+
const ERROR_PATTERNS = /error|failed|exception|cannot|undefined|null pointer|ENOENT|timeout/i;
|
|
9
|
+
/**
|
|
10
|
+
* Returns all available session log harnesses for the current machine.
|
|
11
|
+
* Add new harnesses to HARNESSES to support additional agent runtimes.
|
|
12
|
+
*/
|
|
13
|
+
export function getAvailableHarnesses() {
|
|
14
|
+
return HARNESSES.filter((harness) => harness.isAvailable());
|
|
15
|
+
}
|
|
16
|
+
export function normalizeSessionTopic(text) {
|
|
17
|
+
const normalized = text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
18
|
+
if (normalized.length < 10)
|
|
19
|
+
return undefined;
|
|
20
|
+
return normalized.slice(0, 60);
|
|
21
|
+
}
|
|
22
|
+
export function aggregateSessionEvents(events) {
|
|
23
|
+
const counts = new Map();
|
|
24
|
+
for (const event of events) {
|
|
25
|
+
const topic = normalizeSessionTopic(event.text);
|
|
26
|
+
if (!topic)
|
|
27
|
+
continue;
|
|
28
|
+
const isFailurePattern = ERROR_PATTERNS.test(topic);
|
|
29
|
+
if (!isFailurePattern)
|
|
30
|
+
continue;
|
|
31
|
+
const existing = counts.get(topic) ?? {
|
|
32
|
+
count: 0,
|
|
33
|
+
isFailurePattern,
|
|
34
|
+
sources: new Set(),
|
|
35
|
+
topic,
|
|
36
|
+
};
|
|
37
|
+
existing.count += 1;
|
|
38
|
+
existing.isFailurePattern = existing.isFailurePattern || isFailurePattern;
|
|
39
|
+
existing.sources.add(event.harness);
|
|
40
|
+
counts.set(topic, existing);
|
|
41
|
+
}
|
|
42
|
+
return [...counts.values()]
|
|
43
|
+
.filter((entry) => entry.count >= 2)
|
|
44
|
+
.sort((a, b) => b.count - a.count || a.topic.localeCompare(b.topic))
|
|
45
|
+
.slice(0, 15)
|
|
46
|
+
.map((entry) => ({
|
|
47
|
+
topic: entry.topic,
|
|
48
|
+
frequency: entry.count,
|
|
49
|
+
source: [...entry.sources].sort().join(","),
|
|
50
|
+
isFailurePattern: entry.isFailurePattern,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Scan recent session logs from all available harnesses and return
|
|
55
|
+
* repeated failure patterns that might warrant new AKM assets.
|
|
56
|
+
*/
|
|
57
|
+
export function getExecutionLogCandidates(sinceDays = 7) {
|
|
58
|
+
const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
|
|
59
|
+
const events = [];
|
|
60
|
+
for (const harness of getAvailableHarnesses()) {
|
|
61
|
+
try {
|
|
62
|
+
events.push(...harness.readEvents({ sinceMs }));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// individual harness failures are non-fatal
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return aggregateSessionEvents(events);
|
|
69
|
+
}
|
|
@@ -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
|
+
}
|