akm-cli 0.7.5 → 0.8.0-rc.6
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} +113 -2
- package/README.md +20 -4
- 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.js +1995 -551
- 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 +1531 -0
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +990 -75
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +5 -23
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +400 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +77 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-profiles.js +146 -0
- package/dist/commands/improve-result-file.js +103 -0
- package/dist/commands/improve.js +2175 -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/index.js +183 -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/vault-key-rules.js +139 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal.js +66 -12
- package/dist/commands/propose.js +86 -31
- package/dist/commands/reflect.js +1119 -73
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember.js +69 -6
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +144 -25
- 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 +438 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/commands/vault.js +130 -77
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +7 -0
- package/dist/core/asset-registry.js +7 -16
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +22 -0
- package/dist/core/common.js +157 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +625 -0
- package/dist/core/config-schema.js +501 -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 +327 -987
- package/dist/core/errors.js +40 -19
- package/dist/core/events.js +91 -138
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +3 -6
- 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 +326 -14
- package/dist/core/proposal-quality-validators.js +364 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +498 -42
- package/dist/core/state-db.js +927 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/warn.js +62 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +152 -253
- package/dist/indexer/db.js +933 -103
- package/dist/indexer/ensure-index.js +64 -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 -124
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +506 -291
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +148 -160
- package/dist/indexer/memory-inference.js +99 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +255 -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 +5 -16
- 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 +150 -74
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +118 -23
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +68 -0
- package/dist/integrations/session-logs/providers/claude-code.js +59 -0
- package/dist/integrations/session-logs/providers/opencode.js +55 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +72 -124
- package/dist/llm/embedder.js +3 -19
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +3 -0
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +89 -48
- package/dist/llm/graph-extract.js +676 -70
- package/dist/llm/index-passes.js +9 -23
- package/dist/llm/memory-infer.js +52 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +281 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +5 -318
- package/dist/output/context.js +3 -0
- package/dist/output/renderers.js +223 -256
- package/dist/output/shapes.js +150 -105
- package/dist/output/text.js +318 -30
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +3 -0
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +70 -49
- 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 +17307 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -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 +7 -5
- 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 +211 -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 +140 -10
- 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 +62 -91
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +9 -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.5.md +2 -2
- 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 +20 -8
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -381
- package/dist/templates/wiki-templates.js +0 -100
package/dist/cli.js
CHANGED
|
@@ -1,40 +1,83 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
// Runtime guard: akm-cli 0.8 is Bun-only. The `preinstall` hook in
|
|
6
|
+
// package.json blocks `npm install`, but it does not protect against a
|
|
7
|
+
// stale node-resolved shebang, a wrong PATH entry, or someone running
|
|
8
|
+
// `node dist/cli.js` directly from a clone. In any of those cases the
|
|
9
|
+
// next line — `import { spawnSync } from "node:child_process";` — would
|
|
10
|
+
// itself succeed under node, only to die a few imports later with a
|
|
11
|
+
// confusing `ERR_MODULE_NOT_FOUND` for our extensionless internal paths.
|
|
12
|
+
// Catch the wrong-runtime case here with a friendly message instead of
|
|
13
|
+
// a stack trace. Cross-runtime support is planned for 0.9 (issue #465).
|
|
14
|
+
if (typeof globalThis.Bun === "undefined") {
|
|
15
|
+
console.error("\n ERROR: akm-cli 0.8 requires the Bun runtime (https://bun.sh) or the prebuilt binary.\n" +
|
|
16
|
+
" Running under Node.js is not supported in this release.\n" +
|
|
17
|
+
" Install options:\n" +
|
|
18
|
+
" 1. Bun: curl -fsSL https://bun.sh/install | bash && bun install -g akm-cli\n" +
|
|
19
|
+
" 2. Binary: curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh | bash\n" +
|
|
20
|
+
" Cross-runtime support is planned for 0.9.0.\n");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
2
23
|
import { spawnSync } from "node:child_process";
|
|
3
24
|
import fs from "node:fs";
|
|
4
25
|
import path from "node:path";
|
|
5
26
|
import * as p from "@clack/prompts";
|
|
6
27
|
import { defineCommand, runMain } from "citty";
|
|
28
|
+
import { getStringArg, hasSubcommand, parseAutoAcceptFlag, parseNonNegativeIntFlag, parsePositiveIntFlag, } from "./cli/parse-args";
|
|
29
|
+
import { akmAgentDispatch } from "./commands/agent-dispatch";
|
|
7
30
|
import { generateBashCompletions, installBashCompletions } from "./commands/completions";
|
|
8
31
|
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./commands/config-cli";
|
|
9
32
|
import { akmCurate } from "./commands/curate";
|
|
10
|
-
import {
|
|
33
|
+
import { akmDbBackups } from "./commands/db-cli";
|
|
11
34
|
import { akmEventsList, akmEventsTail } from "./commands/events";
|
|
35
|
+
import { akmGraphEntities, akmGraphEntity, akmGraphExport, akmGraphOrphans, akmGraphRelated, akmGraphRelations, akmGraphSummary, akmGraphUpdate, } from "./commands/graph";
|
|
36
|
+
import { akmHealth } from "./commands/health";
|
|
12
37
|
import { akmHistory } from "./commands/history";
|
|
38
|
+
import { akmImprove } from "./commands/improve";
|
|
39
|
+
import { buildImproveRunId, relativeImproveResultPath, writeImproveResultFile } from "./commands/improve-result-file";
|
|
13
40
|
import { assembleInfo } from "./commands/info";
|
|
14
41
|
import { akmInit } from "./commands/init";
|
|
15
42
|
import { akmListSources, akmRemove, akmUpdate } from "./commands/installed-stashes";
|
|
43
|
+
import { inferAssetName, readKnowledgeInput, writeMarkdownAsset } from "./commands/knowledge";
|
|
44
|
+
import { akmLint } from "./commands/lint";
|
|
16
45
|
import { renderMigrationHelp } from "./commands/migration-help";
|
|
17
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the event source from the environment. When `AKM_EVENT_SOURCE` is
|
|
48
|
+
* set (e.g. by `akm improve` for agent subprocesses), events are tagged so
|
|
49
|
+
* they can be filtered out of user-facing history.
|
|
50
|
+
*/
|
|
51
|
+
function resolveEventSource() {
|
|
52
|
+
const raw = process.env.AKM_EVENT_SOURCE;
|
|
53
|
+
if (raw === "improve")
|
|
54
|
+
return "improve";
|
|
55
|
+
if (raw === "user")
|
|
56
|
+
return "user";
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
import { akmProposalAccept, akmProposalDiff, akmProposalList, akmProposalReject, akmProposalRevert, akmProposalShow, } from "./commands/proposal";
|
|
18
60
|
import { akmPropose } from "./commands/propose";
|
|
19
|
-
import { akmReflect } from "./commands/reflect";
|
|
20
61
|
import { searchRegistry } from "./commands/registry-search";
|
|
21
|
-
import { buildMemoryFrontmatter, parseDuration, readMemoryContent, runAutoHeuristics, runLlmEnrich, } from "./commands/remember";
|
|
22
|
-
import { akmSearch, parseScopeFilterFlags, parseSearchSource } from "./commands/search";
|
|
62
|
+
import { buildMemoryFrontmatter, parseDuration, readMemoryContent, resolveRememberContentArg, runAutoHeuristics, runLlmEnrich, } from "./commands/remember";
|
|
63
|
+
import { akmSearch, parseBeliefFilterMode, parseScopeFilterFlags, parseSearchSource } from "./commands/search";
|
|
23
64
|
import { checkForUpdate, performUpgrade } from "./commands/self-update";
|
|
24
|
-
import { akmShowUnified } from "./commands/show";
|
|
65
|
+
import { akmShowUnified, normalizeShowArgv } from "./commands/show";
|
|
25
66
|
import { akmAdd } from "./commands/source-add";
|
|
26
67
|
import { akmClone } from "./commands/source-clone";
|
|
27
68
|
import { addStash } from "./commands/source-manage";
|
|
69
|
+
import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksList, akmTasksRemove, akmTasksRun, akmTasksSetEnabled, akmTasksShow, akmTasksSync, parseTaskRef, } from "./commands/tasks";
|
|
28
70
|
import { parseAssetRef } from "./core/asset-ref";
|
|
71
|
+
import { assembleAsset } from "./core/asset-serialize";
|
|
29
72
|
import { deriveCanonicalAssetName, resolveAssetPathFromName } from "./core/asset-spec";
|
|
30
|
-
import { isHttpUrl, isWithin, resolveStashDir,
|
|
31
|
-
import { DEFAULT_CONFIG,
|
|
73
|
+
import { isHttpUrl, isWithin, resolveStashDir, writeFileAtomic } from "./core/common";
|
|
74
|
+
import { DEFAULT_CONFIG, FEEDBACK_FAILURE_MODES, loadConfig, loadUserConfig, resolveConfiguredSources, saveConfig, } from "./core/config";
|
|
32
75
|
import { ConfigError, NotFoundError, UsageError } from "./core/errors";
|
|
33
76
|
import { appendEvent } from "./core/events";
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import { closeDatabase, findEntryIdByRef, openExistingDatabase } from "./indexer/db";
|
|
77
|
+
import { parseFrontmatter, parseFrontmatterBlock } from "./core/frontmatter";
|
|
78
|
+
import { getCacheDir, getConfigPath, getDbPath, getDefaultStashDir } from "./core/paths";
|
|
79
|
+
import { clearLogFile, info, isQuiet, setLogFile, setQuiet, setVerbose, warn } from "./core/warn";
|
|
80
|
+
import { applyFeedbackToUtilityScore, closeDatabase, findEntryIdByRef, openExistingDatabase } from "./indexer/db";
|
|
38
81
|
import { ensureIndex } from "./indexer/ensure-index";
|
|
39
82
|
import { akmIndex } from "./indexer/indexer";
|
|
40
83
|
import { resolveSourceEntries } from "./indexer/search-source";
|
|
@@ -47,16 +90,22 @@ import { buildRegistryIndex, writeRegistryIndex } from "./registry/build-index";
|
|
|
47
90
|
import { resolveSourcesForOrigin } from "./registry/origin-resolve";
|
|
48
91
|
import { saveGitStash } from "./sources/providers/git";
|
|
49
92
|
import { resolveAssetPath } from "./sources/resolve";
|
|
50
|
-
import { fetchWebsiteMarkdownSnapshot } from "./sources/website-ingest";
|
|
51
93
|
import { pkgVersion } from "./version";
|
|
52
94
|
import { createWorkflowAsset, formatWorkflowErrors, getWorkflowTemplate, validateWorkflowSource, } from "./workflows/authoring";
|
|
53
95
|
import { hasWorkflowSubcommand, parseWorkflowJsonObject, parseWorkflowStepState, WORKFLOW_STEP_STATES, } from "./workflows/cli";
|
|
54
96
|
import { completeWorkflowStep, getNextWorkflowStep, getWorkflowStatus, listWorkflowRuns, resumeWorkflowRun, startWorkflowRun, } from "./workflows/runs";
|
|
55
|
-
const MAX_CAPTURED_ASSET_SLUG_LENGTH = 64;
|
|
56
97
|
const SKILLS_SH_NAME = "skills.sh";
|
|
57
98
|
const SKILLS_SH_URL = "https://skills.sh";
|
|
58
99
|
const SKILLS_SH_PROVIDER = "skills-sh";
|
|
59
100
|
import { stringify as yamlStringify } from "yaml";
|
|
101
|
+
function applyEarlyStderrFlags(argv) {
|
|
102
|
+
if (argv.includes("--quiet") || argv.includes("-q")) {
|
|
103
|
+
setQuiet(true);
|
|
104
|
+
}
|
|
105
|
+
if (argv.includes("--verbose")) {
|
|
106
|
+
setVerbose(true);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
60
109
|
/**
|
|
61
110
|
* Collect all occurrences of a repeatable flag from process.argv.
|
|
62
111
|
* Citty's StringArgDef only exposes the last value when a flag is repeated,
|
|
@@ -80,6 +129,43 @@ function parseAllFlagValues(flag) {
|
|
|
80
129
|
}
|
|
81
130
|
return values;
|
|
82
131
|
}
|
|
132
|
+
function resolveHelpMigrateVersionArg(version) {
|
|
133
|
+
if (version === undefined)
|
|
134
|
+
return undefined;
|
|
135
|
+
const parsedFormat = parseFlagValue(process.argv, "--format");
|
|
136
|
+
if (parsedFormat !== undefined &&
|
|
137
|
+
version === parsedFormat &&
|
|
138
|
+
wasHelpMigrateFlagValueConsumedAsVersion(version, parsedFormat, "--format")) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const parsedDetail = parseFlagValue(process.argv, "--detail");
|
|
142
|
+
if (parsedDetail !== undefined &&
|
|
143
|
+
version === parsedDetail &&
|
|
144
|
+
wasHelpMigrateFlagValueConsumedAsVersion(version, parsedDetail, "--detail")) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return version;
|
|
148
|
+
}
|
|
149
|
+
function wasHelpMigrateFlagValueConsumedAsVersion(version, flagValue, flagName) {
|
|
150
|
+
const argv = process.argv.slice(2);
|
|
151
|
+
const helpIndex = argv.indexOf("help");
|
|
152
|
+
const tokens = helpIndex >= 0 ? argv.slice(helpIndex + 1) : argv;
|
|
153
|
+
const migrateIndex = tokens.indexOf("migrate");
|
|
154
|
+
const relevant = migrateIndex >= 0 ? tokens.slice(migrateIndex + 1) : tokens;
|
|
155
|
+
let flagIndex = -1;
|
|
156
|
+
for (let i = 0; i < relevant.length; i += 1) {
|
|
157
|
+
const token = relevant[i];
|
|
158
|
+
if (token === flagName || token === `${flagName}=${flagValue}`) {
|
|
159
|
+
flagIndex = i;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (flagIndex === -1)
|
|
164
|
+
return false;
|
|
165
|
+
if (relevant.slice(0, flagIndex).includes(version))
|
|
166
|
+
return false;
|
|
167
|
+
return relevant[flagIndex] === flagName ? relevant[flagIndex + 1] === version : true;
|
|
168
|
+
}
|
|
83
169
|
function output(command, result) {
|
|
84
170
|
const mode = getOutputMode();
|
|
85
171
|
const shaped = shapeForCommand(command, result, mode.detail, mode.forAgent);
|
|
@@ -101,6 +187,30 @@ function output(command, result) {
|
|
|
101
187
|
}
|
|
102
188
|
}
|
|
103
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Stderr-only human-friendly hint after a non-interactive `setup` invocation.
|
|
192
|
+
* Default --format is `json`, so a CI or piped consumer sees only the JSON on
|
|
193
|
+
* stdout. But an interactive user running `akm setup --yes` would otherwise
|
|
194
|
+
* see only the JSON blob with no obvious next step. When stderr is a TTY and
|
|
195
|
+
* the JSON went to stdout, print a two-line summary to stderr telling the
|
|
196
|
+
* user (a) where the stash landed and (b) what to run next.
|
|
197
|
+
*
|
|
198
|
+
* Silent when: stderr is not a TTY (CI, pipes), --format=text/yaml (the user
|
|
199
|
+
* already gets readable output), --quiet, or the result is missing fields.
|
|
200
|
+
*/
|
|
201
|
+
function printSetupTtyHint(result) {
|
|
202
|
+
if (!process.stderr.isTTY)
|
|
203
|
+
return;
|
|
204
|
+
const mode = getOutputMode();
|
|
205
|
+
if (mode.format !== "json" && mode.format !== "jsonl")
|
|
206
|
+
return;
|
|
207
|
+
if (isQuiet())
|
|
208
|
+
return;
|
|
209
|
+
if (!result?.stashDir)
|
|
210
|
+
return;
|
|
211
|
+
console.error(`\n✓ Stash created at ${result.stashDir}\n` +
|
|
212
|
+
` Next: \`akm add github:itlackey/akm-stash\` then \`akm index\` to populate the stash.`);
|
|
213
|
+
}
|
|
104
214
|
/**
|
|
105
215
|
* Module Naming:
|
|
106
216
|
* - sources/* : Asset operations (search, show, add, clone)
|
|
@@ -111,12 +221,82 @@ function output(command, result) {
|
|
|
111
221
|
const setupCommand = defineCommand({
|
|
112
222
|
meta: {
|
|
113
223
|
name: "setup",
|
|
114
|
-
description: "Interactive configuration wizard
|
|
224
|
+
description: "Interactive configuration wizard. Configures embeddings/LLM connections (for indexing/enrichment), agent profiles (CLI agent, embedded SDK, or none), sources, and registries. Shows which features are enabled at the end. Use --config <json> or --yes for non-interactive/scripting mode.",
|
|
115
225
|
},
|
|
116
|
-
|
|
226
|
+
args: {
|
|
227
|
+
config: {
|
|
228
|
+
type: "string",
|
|
229
|
+
description: 'Config JSON to apply non-interactively, e.g. \'{"llm":{"endpoint":"...","model":"..."}}\'',
|
|
230
|
+
},
|
|
231
|
+
from: {
|
|
232
|
+
type: "string",
|
|
233
|
+
description: "Path to a config file (JSON or YAML) to bootstrap from. Skips prompts for keys present in the file.",
|
|
234
|
+
},
|
|
235
|
+
yes: {
|
|
236
|
+
type: "boolean",
|
|
237
|
+
default: false,
|
|
238
|
+
description: "Accept all defaults, skip all prompts. Idempotent — safe to run in CI.",
|
|
239
|
+
},
|
|
240
|
+
dir: {
|
|
241
|
+
type: "string",
|
|
242
|
+
description: "Stash directory path (overrides stashDir in config or --config JSON)",
|
|
243
|
+
},
|
|
244
|
+
probe: {
|
|
245
|
+
type: "boolean",
|
|
246
|
+
default: false,
|
|
247
|
+
description: "Probe LLM/embedding endpoints after writing config to verify connectivity",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
async run({ args }) {
|
|
117
251
|
await runWithJsonErrors(async () => {
|
|
118
|
-
const
|
|
119
|
-
|
|
252
|
+
const noInit = getHyphenatedBoolean(args, "no-init");
|
|
253
|
+
if (args.from && args.config) {
|
|
254
|
+
throw new UsageError("Pass either --from <file> or --config <json>, not both.", "INVALID_FLAG_VALUE");
|
|
255
|
+
}
|
|
256
|
+
if (args.from) {
|
|
257
|
+
// File-based bootstrap. `loadSetupConfigFromFile` expands a leading
|
|
258
|
+
// `~`, resolves relative paths against cwd, picks the YAML or JSON
|
|
259
|
+
// parser based on the file extension, and surfaces any
|
|
260
|
+
// read/parse/shape errors as ConfigError("INVALID_CONFIG_FILE").
|
|
261
|
+
const { loadSetupConfigFromFile, runSetupFromConfig } = await import("./setup/setup");
|
|
262
|
+
const loaded = await loadSetupConfigFromFile(args.from);
|
|
263
|
+
const result = await runSetupFromConfig({
|
|
264
|
+
configJson: loaded.configJson,
|
|
265
|
+
dir: args.dir,
|
|
266
|
+
noInit,
|
|
267
|
+
probe: args.probe,
|
|
268
|
+
});
|
|
269
|
+
output("setup", result);
|
|
270
|
+
printSetupTtyHint(result);
|
|
271
|
+
}
|
|
272
|
+
else if (args.config) {
|
|
273
|
+
// Non-interactive config mode
|
|
274
|
+
const { runSetupFromConfig } = await import("./setup/setup");
|
|
275
|
+
const result = await runSetupFromConfig({
|
|
276
|
+
configJson: args.config,
|
|
277
|
+
dir: args.dir,
|
|
278
|
+
noInit,
|
|
279
|
+
probe: args.probe,
|
|
280
|
+
});
|
|
281
|
+
output("setup", result);
|
|
282
|
+
printSetupTtyHint(result);
|
|
283
|
+
}
|
|
284
|
+
else if (args.yes) {
|
|
285
|
+
// Defaults mode — no prompts
|
|
286
|
+
const { runSetupWithDefaults } = await import("./setup/setup");
|
|
287
|
+
const result = await runSetupWithDefaults({
|
|
288
|
+
dir: args.dir,
|
|
289
|
+
noInit,
|
|
290
|
+
probe: args.probe,
|
|
291
|
+
});
|
|
292
|
+
output("setup", result);
|
|
293
|
+
printSetupTtyHint(result);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
// Interactive wizard
|
|
297
|
+
const { runSetupWizard } = await import("./setup/setup");
|
|
298
|
+
await runSetupWizard({ dir: args.dir, noInit });
|
|
299
|
+
}
|
|
120
300
|
});
|
|
121
301
|
},
|
|
122
302
|
});
|
|
@@ -142,16 +322,33 @@ const indexCommand = defineCommand({
|
|
|
142
322
|
meta: { name: "index", description: "Build search index (incremental by default; --full forces full reindex)" },
|
|
143
323
|
args: {
|
|
144
324
|
full: { type: "boolean", description: "Force full reindex", default: false },
|
|
145
|
-
enrich: { type: "boolean", description: "Enable LLM inference and enrichment passes", default: false },
|
|
146
325
|
verbose: { type: "boolean", description: "Print phase-by-phase indexing progress to stderr", default: false },
|
|
326
|
+
clean: {
|
|
327
|
+
type: "boolean",
|
|
328
|
+
description: "After indexing, remove any entries whose source file no longer exists on disk.",
|
|
329
|
+
default: false,
|
|
330
|
+
},
|
|
331
|
+
"dry-run": {
|
|
332
|
+
type: "boolean",
|
|
333
|
+
description: "When combined with --clean, report stale entries without deleting them.",
|
|
334
|
+
default: false,
|
|
335
|
+
},
|
|
147
336
|
},
|
|
148
337
|
async run({ args }) {
|
|
149
338
|
await runWithJsonErrors(async () => {
|
|
339
|
+
if (getHyphenatedBoolean(args, "enrich") || parseFlagValue(process.argv, "--enrich") !== undefined) {
|
|
340
|
+
throw new UsageError("`akm index --enrich` has been removed. Plain `akm index` now performs metadata enrichment by default.");
|
|
341
|
+
}
|
|
342
|
+
if (getHyphenatedBoolean(args, "re-enrich") || parseFlagValue(process.argv, "--re-enrich") !== undefined) {
|
|
343
|
+
throw new UsageError("`akm index --re-enrich` has been removed. Re-enrichment of index-time LLM passes is not exposed in this slice.");
|
|
344
|
+
}
|
|
150
345
|
const outputMode = getOutputMode();
|
|
151
346
|
const controller = new AbortController();
|
|
152
347
|
const abort = () => controller.abort(new Error("index interrupted"));
|
|
153
348
|
process.once("SIGINT", abort);
|
|
154
349
|
process.once("SIGTERM", abort);
|
|
350
|
+
const indexLogFile = path.join(getCacheDir(), "logs", "index", `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
|
|
351
|
+
setLogFile(indexLogFile);
|
|
155
352
|
const spin = !args.verbose && outputMode.format === "text" ? p.spinner() : null;
|
|
156
353
|
if (spin) {
|
|
157
354
|
spin.start(`Building search index${args.full ? " (full rebuild)" : ""}...`);
|
|
@@ -160,12 +357,13 @@ const indexCommand = defineCommand({
|
|
|
160
357
|
try {
|
|
161
358
|
const result = await akmIndex({
|
|
162
359
|
full: args.full,
|
|
163
|
-
|
|
164
|
-
|
|
360
|
+
clean: args.clean,
|
|
361
|
+
dryRun: args["dry-run"],
|
|
362
|
+
onProgress: ({ phase, message, processed, total }) => {
|
|
165
363
|
latestMessage = message;
|
|
166
364
|
const progressPrefix = processed !== undefined && total !== undefined ? `[${processed}/${total}] ` : "";
|
|
167
365
|
if (args.verbose) {
|
|
168
|
-
|
|
366
|
+
info(`[index:${phase}] ${progressPrefix}${message}`);
|
|
169
367
|
}
|
|
170
368
|
else if (spin) {
|
|
171
369
|
spin.stop(`${progressPrefix}${message}`);
|
|
@@ -186,6 +384,7 @@ const indexCommand = defineCommand({
|
|
|
186
384
|
throw error;
|
|
187
385
|
}
|
|
188
386
|
finally {
|
|
387
|
+
clearLogFile();
|
|
189
388
|
process.off("SIGINT", abort);
|
|
190
389
|
process.off("SIGTERM", abort);
|
|
191
390
|
}
|
|
@@ -201,6 +400,193 @@ const infoCommand = defineCommand({
|
|
|
201
400
|
});
|
|
202
401
|
},
|
|
203
402
|
});
|
|
403
|
+
const healthCommand = defineCommand({
|
|
404
|
+
meta: { name: "health", description: "Check akm runtime health, artifacts, and improve metrics" },
|
|
405
|
+
args: {
|
|
406
|
+
since: {
|
|
407
|
+
type: "string",
|
|
408
|
+
description: "Rolling window start (ISO timestamp, date, epoch ms, or shorthand like 24h / 7d)",
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
async run({ args }) {
|
|
412
|
+
// Capture the health result so we can propagate its overall status into the
|
|
413
|
+
// process exit code AFTER the JSON envelope is flushed to stdout. The
|
|
414
|
+
// CHANGELOG advertises `akm health` as a CI/runtime monitoring command —
|
|
415
|
+
// callers rely on `akm health && deploy` style chaining, which requires
|
|
416
|
+
// non-zero exit on failure (and parseable JSON on stdout for diagnostics).
|
|
417
|
+
let resultStatus;
|
|
418
|
+
await runWithJsonErrors(() => {
|
|
419
|
+
const result = akmHealth({ since: args.since });
|
|
420
|
+
resultStatus = result.status;
|
|
421
|
+
output("health", result);
|
|
422
|
+
});
|
|
423
|
+
if (resultStatus === "fail") {
|
|
424
|
+
process.exit(EXIT_GENERAL);
|
|
425
|
+
}
|
|
426
|
+
if (resultStatus === "warn") {
|
|
427
|
+
process.exit(EXIT_HEALTH_WARN);
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
const graphCommand = defineCommand({
|
|
432
|
+
meta: { name: "graph", description: "Inspect the indexed entity graph stored in SQLite" },
|
|
433
|
+
subCommands: {
|
|
434
|
+
summary: defineCommand({
|
|
435
|
+
meta: { name: "summary", description: "Show entity-graph counts and quality telemetry" },
|
|
436
|
+
args: {
|
|
437
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
438
|
+
},
|
|
439
|
+
run({ args }) {
|
|
440
|
+
return runWithJsonErrors(() => {
|
|
441
|
+
output("graph-summary", akmGraphSummary({ source: args.source }));
|
|
442
|
+
});
|
|
443
|
+
},
|
|
444
|
+
}),
|
|
445
|
+
entities: defineCommand({
|
|
446
|
+
meta: { name: "entities", description: "List entities with per-file occurrence counts" },
|
|
447
|
+
args: {
|
|
448
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
449
|
+
limit: { type: "string", description: "Maximum entities to return" },
|
|
450
|
+
},
|
|
451
|
+
run({ args }) {
|
|
452
|
+
return runWithJsonErrors(() => {
|
|
453
|
+
output("graph-entities", akmGraphEntities({ source: args.source, limit: parsePositiveIntFlag(args.limit ?? undefined) }));
|
|
454
|
+
});
|
|
455
|
+
},
|
|
456
|
+
}),
|
|
457
|
+
relations: defineCommand({
|
|
458
|
+
meta: { name: "relations", description: "List relations with occurrence counts" },
|
|
459
|
+
args: {
|
|
460
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
461
|
+
limit: { type: "string", description: "Maximum relations to return" },
|
|
462
|
+
},
|
|
463
|
+
run({ args }) {
|
|
464
|
+
return runWithJsonErrors(() => {
|
|
465
|
+
output("graph-relations", akmGraphRelations({ source: args.source, limit: parsePositiveIntFlag(args.limit ?? undefined) }));
|
|
466
|
+
});
|
|
467
|
+
},
|
|
468
|
+
}),
|
|
469
|
+
related: defineCommand({
|
|
470
|
+
meta: { name: "related", description: "Show graph-related neighboring assets for a ref" },
|
|
471
|
+
args: {
|
|
472
|
+
ref: { type: "positional", description: "Asset ref", required: true },
|
|
473
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
474
|
+
limit: { type: "string", description: "Maximum related assets to return" },
|
|
475
|
+
},
|
|
476
|
+
async run({ args }) {
|
|
477
|
+
return runWithJsonErrors(async () => {
|
|
478
|
+
output("graph-related", await akmGraphRelated({
|
|
479
|
+
ref: args.ref ?? "",
|
|
480
|
+
source: args.source,
|
|
481
|
+
limit: parsePositiveIntFlag(args.limit ?? undefined),
|
|
482
|
+
}));
|
|
483
|
+
});
|
|
484
|
+
},
|
|
485
|
+
}),
|
|
486
|
+
entity: defineCommand({
|
|
487
|
+
meta: { name: "entity", description: "List assets that contain the given entity" },
|
|
488
|
+
args: {
|
|
489
|
+
name: { type: "positional", description: "Entity name", required: true },
|
|
490
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
491
|
+
limit: { type: "string", description: "Maximum matches to return" },
|
|
492
|
+
},
|
|
493
|
+
run({ args }) {
|
|
494
|
+
return runWithJsonErrors(() => {
|
|
495
|
+
output("graph-entity", akmGraphEntity({
|
|
496
|
+
name: args.name ?? "",
|
|
497
|
+
source: args.source,
|
|
498
|
+
limit: parsePositiveIntFlag(args.limit ?? undefined),
|
|
499
|
+
}));
|
|
500
|
+
});
|
|
501
|
+
},
|
|
502
|
+
}),
|
|
503
|
+
orphans: defineCommand({
|
|
504
|
+
meta: { name: "orphans", description: "List assets with no extracted graph entities" },
|
|
505
|
+
args: {
|
|
506
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
507
|
+
limit: { type: "string", description: "Maximum orphans to return" },
|
|
508
|
+
},
|
|
509
|
+
run({ args }) {
|
|
510
|
+
return runWithJsonErrors(() => {
|
|
511
|
+
output("graph-orphans", akmGraphOrphans({ source: args.source, limit: parsePositiveIntFlag(args.limit ?? undefined) }));
|
|
512
|
+
});
|
|
513
|
+
},
|
|
514
|
+
}),
|
|
515
|
+
export: defineCommand({
|
|
516
|
+
meta: { name: "export", description: "Export graph artifact as JSON or JSONL" },
|
|
517
|
+
args: {
|
|
518
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
519
|
+
out: { type: "string", description: "Output path" },
|
|
520
|
+
format: { type: "string", description: "Export format (json|jsonl)", default: "json" },
|
|
521
|
+
},
|
|
522
|
+
run({ args }) {
|
|
523
|
+
return runWithJsonErrors(() => {
|
|
524
|
+
output("graph-export", akmGraphExport({
|
|
525
|
+
source: args.source,
|
|
526
|
+
out: args.out ?? "",
|
|
527
|
+
format: args.format,
|
|
528
|
+
}));
|
|
529
|
+
});
|
|
530
|
+
},
|
|
531
|
+
}),
|
|
532
|
+
update: defineCommand({
|
|
533
|
+
meta: { name: "update", description: "Re-run graph extraction, optionally scoped to specific asset refs" },
|
|
534
|
+
args: {
|
|
535
|
+
refs: {
|
|
536
|
+
type: "positional",
|
|
537
|
+
description: "Zero or more asset refs to scope extraction (omit for a full re-extract)",
|
|
538
|
+
required: false,
|
|
539
|
+
default: "",
|
|
540
|
+
},
|
|
541
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
542
|
+
},
|
|
543
|
+
async run({ args }) {
|
|
544
|
+
return runWithJsonErrors(async () => {
|
|
545
|
+
// `refs` is a single positional; collect remaining argv tokens as well.
|
|
546
|
+
const rawRefs = [args.refs, ...(Array.isArray(args._) ? args._ : [])].filter((r) => typeof r === "string" && r.trim().length > 0);
|
|
547
|
+
output("graph-update", await akmGraphUpdate({ refs: rawRefs.length > 0 ? rawRefs : undefined, source: args.source }));
|
|
548
|
+
});
|
|
549
|
+
},
|
|
550
|
+
}),
|
|
551
|
+
},
|
|
552
|
+
run({ args }) {
|
|
553
|
+
return runWithJsonErrors(() => {
|
|
554
|
+
if (hasSubcommand(args, GRAPH_SUBCOMMAND_SET))
|
|
555
|
+
return;
|
|
556
|
+
output("graph-summary", akmGraphSummary());
|
|
557
|
+
});
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
// MVP DB administration. Currently only `akm db backups`; restore is manual —
|
|
561
|
+
// stop akm and run `scripts/migrations/restore-data-dir.sh <backup>`.
|
|
562
|
+
const DB_SUBCOMMAND_SET = new Set(["backups"]);
|
|
563
|
+
const dbCommand = defineCommand({
|
|
564
|
+
meta: {
|
|
565
|
+
name: "db",
|
|
566
|
+
description: "Inspect the AKM SQLite data directory. Currently exposes `backups`; to restore from a snapshot, stop akm and run scripts/migrations/restore-data-dir.sh against the chosen backup.",
|
|
567
|
+
},
|
|
568
|
+
subCommands: {
|
|
569
|
+
backups: defineCommand({
|
|
570
|
+
meta: {
|
|
571
|
+
name: "backups",
|
|
572
|
+
description: "List pre-upgrade snapshots of the data directory (newest first). Backups are created automatically before destructive DB version upgrades unless AKM_DB_BACKUP=0.",
|
|
573
|
+
},
|
|
574
|
+
run() {
|
|
575
|
+
return runWithJsonErrors(() => {
|
|
576
|
+
output("db-backups", akmDbBackups());
|
|
577
|
+
});
|
|
578
|
+
},
|
|
579
|
+
}),
|
|
580
|
+
},
|
|
581
|
+
run({ args }) {
|
|
582
|
+
return runWithJsonErrors(() => {
|
|
583
|
+
if (hasSubcommand(args, DB_SUBCOMMAND_SET))
|
|
584
|
+
return;
|
|
585
|
+
// Default action: list backups.
|
|
586
|
+
output("db-backups", akmDbBackups());
|
|
587
|
+
});
|
|
588
|
+
},
|
|
589
|
+
});
|
|
204
590
|
const searchCommand = defineCommand({
|
|
205
591
|
meta: { name: "search", description: "Search the stash" },
|
|
206
592
|
args: {
|
|
@@ -220,29 +606,49 @@ const searchCommand = defineCommand({
|
|
|
220
606
|
description: 'Include entries with quality:"proposed" in the result set. Excluded by default (v1 spec §4.2).',
|
|
221
607
|
default: false,
|
|
222
608
|
},
|
|
609
|
+
belief: {
|
|
610
|
+
type: "string",
|
|
611
|
+
description: "Memory belief filter: all|current|historical. current keeps active memory beliefs; historical keeps contradicted/superseded/archived memory beliefs.",
|
|
612
|
+
default: "all",
|
|
613
|
+
},
|
|
223
614
|
format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
|
|
224
615
|
detail: { type: "string", description: "Detail level (brief|normal|full|summary|agent)" },
|
|
616
|
+
"no-project-context": {
|
|
617
|
+
type: "boolean",
|
|
618
|
+
description: "Disable the automatic project-context ranking boost (also disabled by AKM_DISABLE_PROJECT_CONTEXT=1).",
|
|
619
|
+
default: false,
|
|
620
|
+
},
|
|
225
621
|
},
|
|
226
622
|
async run({ args }) {
|
|
227
623
|
await runWithJsonErrors(async () => {
|
|
228
|
-
// An empty query enumerates all indexed assets (list mode).
|
|
229
|
-
// The guard that rejected empty queries was removed; akmSearch handles
|
|
230
|
-
// empty strings end-to-end via getAllEntries (DB path) and the
|
|
231
|
-
// substring-search fallback's query-less branch.
|
|
232
624
|
const query = (args.query ?? "").trim();
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
|
|
236
|
-
throw new UsageError(`Invalid --limit value: "${args.limit}". Must be a positive integer.`);
|
|
625
|
+
if (!query) {
|
|
626
|
+
throw new UsageError('A search query is required. Usage: akm search "<query>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT", 'Pass a query like `akm search "docker"` or `akm search "code review" --type skill`.');
|
|
237
627
|
}
|
|
238
|
-
const
|
|
628
|
+
const type = args.type;
|
|
629
|
+
const limit = parsePositiveIntFlag(args.limit ?? undefined);
|
|
239
630
|
const source = parseSearchSource(args.source);
|
|
240
631
|
// Repeatable; citty exposes only the last `--filter` value, so read all
|
|
241
632
|
// occurrences directly from argv (same pattern as `--tag`).
|
|
242
633
|
const filterTokens = parseAllFlagValues("--filter");
|
|
243
634
|
const filters = parseScopeFilterFlags(filterTokens, "--filter");
|
|
244
635
|
const includeProposed = args["include-proposed"] === true;
|
|
245
|
-
const
|
|
636
|
+
const belief = parseBeliefFilterMode(typeof args.belief === "string" ? args.belief : undefined);
|
|
637
|
+
const noProjectContext = getHyphenatedBoolean(args, "no-project-context");
|
|
638
|
+
// --no-project-context sets env so searchDatabase picks it up without
|
|
639
|
+
// threading the flag through the entire call stack.
|
|
640
|
+
if (noProjectContext)
|
|
641
|
+
process.env.AKM_DISABLE_PROJECT_CONTEXT = "1";
|
|
642
|
+
const result = await akmSearch({
|
|
643
|
+
query,
|
|
644
|
+
type,
|
|
645
|
+
limit,
|
|
646
|
+
source,
|
|
647
|
+
filters,
|
|
648
|
+
includeProposed,
|
|
649
|
+
belief,
|
|
650
|
+
eventSource: resolveEventSource(),
|
|
651
|
+
});
|
|
246
652
|
output("search", result);
|
|
247
653
|
});
|
|
248
654
|
},
|
|
@@ -267,11 +673,8 @@ const curateCommand = defineCommand({
|
|
|
267
673
|
throw new UsageError('A curate query is required. Usage: akm curate "<task or prompt>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT", 'Describe the task you want assets for, e.g. `akm curate "deploy to prod"`.');
|
|
268
674
|
}
|
|
269
675
|
const type = args.type;
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
throw new UsageError(`Invalid --limit value: "${args.limit}". Must be a positive integer.`);
|
|
273
|
-
}
|
|
274
|
-
const limit = limitRaw && limitRaw > 0 ? limitRaw : 4;
|
|
676
|
+
const limitParsed = parsePositiveIntFlag(args.limit ?? undefined);
|
|
677
|
+
const limit = limitParsed && limitParsed > 0 ? limitParsed : 4;
|
|
275
678
|
const source = parseSearchSource(args.source ?? "stash");
|
|
276
679
|
const curated = await akmCurate({ query: args.query, type, limit, source });
|
|
277
680
|
output("curate", curated);
|
|
@@ -297,11 +700,6 @@ const addCommand = defineCommand({
|
|
|
297
700
|
description: "Mark a git stash as writable so changes can be pushed back",
|
|
298
701
|
default: false,
|
|
299
702
|
},
|
|
300
|
-
trust: {
|
|
301
|
-
type: "boolean",
|
|
302
|
-
description: "Bypass install-audit blocking for this add invocation only",
|
|
303
|
-
default: false,
|
|
304
|
-
},
|
|
305
703
|
type: {
|
|
306
704
|
type: "string",
|
|
307
705
|
description: "Override asset type for all files in this stash (currently supports: wiki)",
|
|
@@ -310,7 +708,7 @@ const addCommand = defineCommand({
|
|
|
310
708
|
"max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
|
|
311
709
|
"allow-insecure": {
|
|
312
710
|
type: "boolean",
|
|
313
|
-
description: "Allow a plain HTTP source URL
|
|
711
|
+
description: "Allow a plain HTTP source URL and skip confirmation for dangerous vault keys (e.g. LD_PRELOAD, PATH). Use only after explicitly reviewing the stash.",
|
|
314
712
|
default: false,
|
|
315
713
|
},
|
|
316
714
|
},
|
|
@@ -318,6 +716,7 @@ const addCommand = defineCommand({
|
|
|
318
716
|
await runWithJsonErrors(async () => {
|
|
319
717
|
const ref = args.ref.trim();
|
|
320
718
|
const allowInsecure = getHyphenatedBoolean(args, "allow-insecure");
|
|
719
|
+
const allowDangerousKeys = allowInsecure;
|
|
321
720
|
// URL with --provider → stash source (remote or git provider)
|
|
322
721
|
if (args.provider) {
|
|
323
722
|
if (shouldWarnOnPlainHttp(ref)) {
|
|
@@ -370,7 +769,6 @@ const addCommand = defineCommand({
|
|
|
370
769
|
ref,
|
|
371
770
|
name: args.name,
|
|
372
771
|
options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
|
|
373
|
-
trustThisInstall: args.trust,
|
|
374
772
|
writable: args.writable,
|
|
375
773
|
});
|
|
376
774
|
appendEvent({
|
|
@@ -385,7 +783,6 @@ const addCommand = defineCommand({
|
|
|
385
783
|
name: args.name,
|
|
386
784
|
overrideType: args.type,
|
|
387
785
|
options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
|
|
388
|
-
trustThisInstall: args.trust,
|
|
389
786
|
writable: args.writable,
|
|
390
787
|
});
|
|
391
788
|
appendEvent({
|
|
@@ -397,6 +794,120 @@ const addCommand = defineCommand({
|
|
|
397
794
|
writable: args.writable === true,
|
|
398
795
|
},
|
|
399
796
|
});
|
|
797
|
+
// ── Post-install vault key audit ────────────────────────────────────────
|
|
798
|
+
// Resolve the stash root from the install result and scan any vault files
|
|
799
|
+
// for dangerous env var keys. When findings are present the install is
|
|
800
|
+
// gated: TTY → interactive confirmation prompt; non-TTY without
|
|
801
|
+
// --allow-insecure → hard failure (exit 1). Pass
|
|
802
|
+
// --allow-insecure to skip the prompt non-interactively.
|
|
803
|
+
try {
|
|
804
|
+
const installedStashRoot = result.installed?.stashRoot ??
|
|
805
|
+
(result.sourceAdded && "stashRoot" in result.sourceAdded ? result.sourceAdded.stashRoot : undefined);
|
|
806
|
+
if (installedStashRoot) {
|
|
807
|
+
const { checkVaultForDangerousKeys } = await import("./commands/lint/vault-key-rules.js");
|
|
808
|
+
const vaultsDir = path.join(installedStashRoot, "vaults");
|
|
809
|
+
if (fs.existsSync(vaultsDir)) {
|
|
810
|
+
const envFiles = fs.readdirSync(vaultsDir).filter((f) => f.endsWith(".env"));
|
|
811
|
+
// Collect all dangerous-key findings across every vault file.
|
|
812
|
+
const allFindings = [];
|
|
813
|
+
for (const envFile of envFiles) {
|
|
814
|
+
const vaultPath = path.join(vaultsDir, envFile);
|
|
815
|
+
const baseName = path.basename(envFile, ".env");
|
|
816
|
+
const vaultRef = baseName === "" ? "vault:default" : `vault:${baseName}`;
|
|
817
|
+
const relPath = path.join("vaults", envFile);
|
|
818
|
+
const findings = checkVaultForDangerousKeys(vaultPath, relPath, vaultRef);
|
|
819
|
+
for (const finding of findings) {
|
|
820
|
+
// Extract the key name from the detail string for the summary line.
|
|
821
|
+
const keyMatch = finding.detail.match(/Vault key `([^`]+)`/);
|
|
822
|
+
const keyName = keyMatch ? keyMatch[1] : finding.file;
|
|
823
|
+
allFindings.push({ vaultRef, keyName, relPath });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (allFindings.length > 0) {
|
|
827
|
+
if (allowDangerousKeys) {
|
|
828
|
+
// Operator has explicitly accepted the risk — warn and continue.
|
|
829
|
+
for (const f of allFindings) {
|
|
830
|
+
warn(`[dangerous-vault-key] ${f.relPath}: key \`${f.keyName}\` in ${f.vaultRef} can hijack process execution via \`akm vault run\`. Proceeding because --allow-insecure was set.`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
else if (process.stdin.isTTY) {
|
|
834
|
+
// Interactive path: show findings and ask the user to confirm.
|
|
835
|
+
// Guard on stdin (not stdout) because p.confirm() reads from stdin;
|
|
836
|
+
// stdout may be a TTY while stdin is piped, which would cause a hang.
|
|
837
|
+
const stashLabel = ref;
|
|
838
|
+
const groupedByVault = new Map();
|
|
839
|
+
for (const f of allFindings) {
|
|
840
|
+
const existing = groupedByVault.get(f.vaultRef) ?? [];
|
|
841
|
+
existing.push(f.keyName);
|
|
842
|
+
groupedByVault.set(f.vaultRef, existing);
|
|
843
|
+
}
|
|
844
|
+
for (const [vaultRef, keys] of groupedByVault) {
|
|
845
|
+
warn(`[warn] Vault "${vaultRef}" in stash "${stashLabel}" contains potentially dangerous keys:`);
|
|
846
|
+
for (const key of keys) {
|
|
847
|
+
warn(` - ${key}: can hijack process execution via \`akm vault run\``);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const confirmed = await p.confirm({
|
|
851
|
+
message: "Install anyway?",
|
|
852
|
+
initialValue: false,
|
|
853
|
+
});
|
|
854
|
+
if (p.isCancel(confirmed) || confirmed !== true) {
|
|
855
|
+
// Roll back the install before aborting.
|
|
856
|
+
// Use the canonical installed id (most reliably resolved by akmRemove) rather
|
|
857
|
+
// than the raw user-supplied ref which may not match after URL normalisation.
|
|
858
|
+
const rollbackTarget = result.installed?.id ?? result.sourceAdded?.stashRoot ?? ref;
|
|
859
|
+
let rollbackWarning;
|
|
860
|
+
try {
|
|
861
|
+
await akmRemove({ target: rollbackTarget });
|
|
862
|
+
}
|
|
863
|
+
catch (_rollbackErr) {
|
|
864
|
+
rollbackWarning =
|
|
865
|
+
`Rollback failed — stash may still be installed at ${installedStashRoot}. ` +
|
|
866
|
+
`Remove it manually with: akm remove ${rollbackTarget}`;
|
|
867
|
+
}
|
|
868
|
+
console.error(JSON.stringify({
|
|
869
|
+
ok: false,
|
|
870
|
+
error: "Install aborted: stash contains dangerous vault keys. Remove the keys or re-run with --allow-insecure to bypass.",
|
|
871
|
+
code: "DANGEROUS_VAULT_KEY",
|
|
872
|
+
...(rollbackWarning ? { rollbackWarning } : {}),
|
|
873
|
+
}, null, 2));
|
|
874
|
+
process.exit(1);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
// Non-interactive path without bypass flag: fail hard.
|
|
879
|
+
// Roll back the install before exiting.
|
|
880
|
+
// Use the canonical installed id (most reliably resolved by akmRemove) rather
|
|
881
|
+
// than the raw user-supplied ref which may not match after URL normalisation.
|
|
882
|
+
const rollbackTarget = result.installed?.id ?? result.sourceAdded?.stashRoot ?? ref;
|
|
883
|
+
let rollbackWarning;
|
|
884
|
+
try {
|
|
885
|
+
await akmRemove({ target: rollbackTarget });
|
|
886
|
+
}
|
|
887
|
+
catch (_rollbackErr) {
|
|
888
|
+
rollbackWarning =
|
|
889
|
+
`Rollback failed — stash may still be installed at ${installedStashRoot}. ` +
|
|
890
|
+
`Remove it manually with: akm remove ${rollbackTarget}`;
|
|
891
|
+
}
|
|
892
|
+
const keyList = allFindings.map((f) => ` - ${f.keyName} (${f.vaultRef})`).join("\n");
|
|
893
|
+
console.error(JSON.stringify({
|
|
894
|
+
ok: false,
|
|
895
|
+
error: `Install blocked: stash "${ref}" contains dangerous vault keys that can hijack process execution via \`akm vault run\`:\n${keyList}\nRe-run with --allow-insecure to bypass this check after reviewing the vault.`,
|
|
896
|
+
code: "DANGEROUS_VAULT_KEY",
|
|
897
|
+
...(rollbackWarning ? { rollbackWarning } : {}),
|
|
898
|
+
}, null, 2));
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
catch (auditErr) {
|
|
906
|
+
// Only swallow errors that are NOT our intentional process.exit calls.
|
|
907
|
+
if (auditErr instanceof Error && auditErr.message === "process.exit called")
|
|
908
|
+
throw auditErr;
|
|
909
|
+
// Vault key audit is best-effort; never fail the install on unexpected audit errors.
|
|
910
|
+
}
|
|
400
911
|
output("add", result);
|
|
401
912
|
});
|
|
402
913
|
},
|
|
@@ -454,9 +965,18 @@ const removeCommand = defineCommand({
|
|
|
454
965
|
meta: { name: "remove", description: "Remove a source by id, ref, path, URL, or name" },
|
|
455
966
|
args: {
|
|
456
967
|
target: { type: "positional", description: "Source to remove (id, ref, path, URL, or name)", required: true },
|
|
968
|
+
yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
|
|
457
969
|
},
|
|
458
970
|
async run({ args }) {
|
|
459
971
|
await runWithJsonErrors(async () => {
|
|
972
|
+
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
973
|
+
const confirmed = await confirmDestructive(`Remove source "${args.target}"? This cannot be undone.`, {
|
|
974
|
+
yes: args.yes === true,
|
|
975
|
+
});
|
|
976
|
+
if (!confirmed) {
|
|
977
|
+
process.stderr.write("Aborted.\n");
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
460
980
|
const result = await akmRemove({ target: args.target });
|
|
461
981
|
appendEvent({
|
|
462
982
|
eventType: "remove",
|
|
@@ -545,15 +1065,17 @@ const showCommand = defineCommand({
|
|
|
545
1065
|
},
|
|
546
1066
|
async run({ args }) {
|
|
547
1067
|
await runWithJsonErrors(async () => {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
throw new UsageError(error.message, "INVALID_FLAG_VALUE", error.hint());
|
|
1068
|
+
const subcommand = Array.isArray(args._) ? args._[0] : undefined;
|
|
1069
|
+
if (subcommand === "proposal") {
|
|
1070
|
+
const proposalId = Array.isArray(args._) ? args._[1] : undefined;
|
|
1071
|
+
if (typeof proposalId !== "string" || !proposalId.trim()) {
|
|
1072
|
+
throw new UsageError("Usage: akm show proposal <id>", "MISSING_REQUIRED_ARGUMENT");
|
|
554
1073
|
}
|
|
555
|
-
|
|
1074
|
+
const result = akmProposalShow({ id: proposalId.trim() });
|
|
1075
|
+
output("proposal-show", result);
|
|
1076
|
+
return;
|
|
556
1077
|
}
|
|
1078
|
+
parseAssetRef(args.ref);
|
|
557
1079
|
// The knowledge-view positional syntax (`akm show knowledge:foo section "Auth"`)
|
|
558
1080
|
// is rewritten to `--akmView` / `--akmHeading` / `--akmStart` / `--akmEnd`
|
|
559
1081
|
// by `normalizeShowArgv` before citty parses argv. We read those values
|
|
@@ -592,7 +1114,13 @@ const showCommand = defineCommand({
|
|
|
592
1114
|
// every occurrence directly from argv (same pattern as `--filter`).
|
|
593
1115
|
const scopeTokens = parseAllFlagValues("--scope");
|
|
594
1116
|
const scope = parseScopeFilterFlags(scopeTokens, "--scope");
|
|
595
|
-
const result = await akmShowUnified({
|
|
1117
|
+
const result = await akmShowUnified({
|
|
1118
|
+
ref: args.ref,
|
|
1119
|
+
view,
|
|
1120
|
+
detail: showDetail,
|
|
1121
|
+
scope,
|
|
1122
|
+
eventSource: resolveEventSource(),
|
|
1123
|
+
});
|
|
596
1124
|
output("show", result);
|
|
597
1125
|
});
|
|
598
1126
|
},
|
|
@@ -642,6 +1170,14 @@ const configCommand = defineCommand({
|
|
|
642
1170
|
});
|
|
643
1171
|
},
|
|
644
1172
|
}),
|
|
1173
|
+
show: defineCommand({
|
|
1174
|
+
meta: { name: "show", description: "Alias for `akm config list` — list current configuration" },
|
|
1175
|
+
run() {
|
|
1176
|
+
return runWithJsonErrors(() => {
|
|
1177
|
+
output("config", listConfig(loadConfig()));
|
|
1178
|
+
});
|
|
1179
|
+
},
|
|
1180
|
+
}),
|
|
645
1181
|
get: defineCommand({
|
|
646
1182
|
meta: { name: "get", description: "Get a configuration value by key" },
|
|
647
1183
|
args: {
|
|
@@ -658,12 +1194,36 @@ const configCommand = defineCommand({
|
|
|
658
1194
|
args: {
|
|
659
1195
|
key: { type: "positional", required: true, description: "Config key (for example: embedding, llm)" },
|
|
660
1196
|
value: { type: "positional", required: true, description: "Config value" },
|
|
1197
|
+
// #463: stable machine-friendly entry point for plugins / hooks.
|
|
1198
|
+
// `--silent` suppresses the config dump on stdout so hook-driven
|
|
1199
|
+
// writes don't pollute their host's output stream.
|
|
1200
|
+
silent: {
|
|
1201
|
+
type: "boolean",
|
|
1202
|
+
description: "Suppress the post-write config dump on stdout. Use from hooks and CI scripts; the write still happens and errors still print.",
|
|
1203
|
+
default: false,
|
|
1204
|
+
},
|
|
1205
|
+
// #463: explicit layer flag for forward-compat. User layer is the only
|
|
1206
|
+
// settable layer today; the flag exists so plugin authors can encode
|
|
1207
|
+
// intent and the surface stays stable if project-layer writes return.
|
|
1208
|
+
layer: {
|
|
1209
|
+
type: "string",
|
|
1210
|
+
description: "Config layer to write to. Currently only `user` is supported.",
|
|
1211
|
+
default: "user",
|
|
1212
|
+
},
|
|
661
1213
|
},
|
|
662
1214
|
run({ args }) {
|
|
663
1215
|
return runWithJsonErrors(() => {
|
|
664
|
-
|
|
1216
|
+
if (args.layer && args.layer !== "user") {
|
|
1217
|
+
throw new UsageError(`Unsupported --layer "${args.layer}". Only "user" is settable in 0.8.0.`, "INVALID_FLAG_VALUE");
|
|
1218
|
+
}
|
|
1219
|
+
// Use loadConfig (not loadUserConfig) so the project-config
|
|
1220
|
+
// deprecation warning fires consistently with `akm config get`
|
|
1221
|
+
// (#457). Effective merged shape is identical post-0.8.0.
|
|
1222
|
+
const updated = setConfigValue(loadConfig(), args.key, args.value);
|
|
665
1223
|
saveConfig(updated);
|
|
666
|
-
|
|
1224
|
+
if (!args.silent) {
|
|
1225
|
+
output("config", listConfig(updated));
|
|
1226
|
+
}
|
|
667
1227
|
});
|
|
668
1228
|
},
|
|
669
1229
|
}),
|
|
@@ -671,19 +1231,66 @@ const configCommand = defineCommand({
|
|
|
671
1231
|
meta: { name: "unset", description: "Unset an optional configuration key or whole embedding/llm section" },
|
|
672
1232
|
args: {
|
|
673
1233
|
key: { type: "positional", required: true, description: "Config key to unset" },
|
|
1234
|
+
silent: {
|
|
1235
|
+
type: "boolean",
|
|
1236
|
+
description: "Suppress the post-write config dump on stdout.",
|
|
1237
|
+
default: false,
|
|
1238
|
+
},
|
|
1239
|
+
layer: {
|
|
1240
|
+
type: "string",
|
|
1241
|
+
description: "Config layer to write to. Currently only `user` is supported.",
|
|
1242
|
+
default: "user",
|
|
1243
|
+
},
|
|
674
1244
|
},
|
|
675
1245
|
run({ args }) {
|
|
676
1246
|
return runWithJsonErrors(() => {
|
|
677
|
-
|
|
1247
|
+
if (args.layer && args.layer !== "user") {
|
|
1248
|
+
throw new UsageError(`Unsupported --layer "${args.layer}". Only "user" is settable in 0.8.0.`, "INVALID_FLAG_VALUE");
|
|
1249
|
+
}
|
|
1250
|
+
const updated = unsetConfigValue(loadConfig(), args.key);
|
|
678
1251
|
saveConfig(updated);
|
|
679
|
-
|
|
1252
|
+
if (!args.silent) {
|
|
1253
|
+
output("config", listConfig(updated));
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
},
|
|
1257
|
+
}),
|
|
1258
|
+
validate: defineCommand({
|
|
1259
|
+
meta: {
|
|
1260
|
+
name: "validate",
|
|
1261
|
+
description: "Validate the on-disk config file against the schema. Exits non-zero on errors.",
|
|
1262
|
+
},
|
|
1263
|
+
async run() {
|
|
1264
|
+
return runWithJsonErrors(async () => {
|
|
1265
|
+
const { runConfigValidate } = await import("./cli/config-validate.js");
|
|
1266
|
+
await runConfigValidate();
|
|
1267
|
+
});
|
|
1268
|
+
},
|
|
1269
|
+
}),
|
|
1270
|
+
migrate: defineCommand({
|
|
1271
|
+
meta: {
|
|
1272
|
+
name: "migrate",
|
|
1273
|
+
description: "Migrate the config file to the current schema version. Use --dry-run to preview without writing.",
|
|
1274
|
+
},
|
|
1275
|
+
args: {
|
|
1276
|
+
"dry-run": { type: "boolean", description: "Preview the migration result without writing.", default: false },
|
|
1277
|
+
"print-diff": {
|
|
1278
|
+
type: "boolean",
|
|
1279
|
+
description: "Print a unified diff of old vs new config alongside the migration output.",
|
|
1280
|
+
default: false,
|
|
1281
|
+
},
|
|
1282
|
+
},
|
|
1283
|
+
async run({ args }) {
|
|
1284
|
+
return runWithJsonErrors(async () => {
|
|
1285
|
+
const { runConfigMigrate } = await import("./cli/config-migrate.js");
|
|
1286
|
+
await runConfigMigrate({ dryRun: Boolean(args["dry-run"]), printDiff: Boolean(args["print-diff"]) });
|
|
680
1287
|
});
|
|
681
1288
|
},
|
|
682
1289
|
}),
|
|
683
1290
|
},
|
|
684
1291
|
run({ args }) {
|
|
685
1292
|
return runWithJsonErrors(() => {
|
|
686
|
-
if (
|
|
1293
|
+
if (hasSubcommand(args, CONFIG_SUBCOMMAND_SET))
|
|
687
1294
|
return;
|
|
688
1295
|
if (args.list) {
|
|
689
1296
|
output("config", listConfig(loadConfig()));
|
|
@@ -903,10 +1510,7 @@ const registryCommand = defineCommand({
|
|
|
903
1510
|
},
|
|
904
1511
|
async run({ args }) {
|
|
905
1512
|
await runWithJsonErrors(async () => {
|
|
906
|
-
const limitRaw =
|
|
907
|
-
if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
|
|
908
|
-
throw new UsageError(`Invalid --limit value: "${args.limit}". Must be a positive integer.`);
|
|
909
|
-
}
|
|
1513
|
+
const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
|
|
910
1514
|
const result = await searchRegistry(args.query, { limit: limitRaw, includeAssets: args.assets });
|
|
911
1515
|
output("registry-search", result);
|
|
912
1516
|
});
|
|
@@ -971,7 +1575,7 @@ const feedbackCommand = defineCommand({
|
|
|
971
1575
|
description: "Record positive or negative feedback for any indexed stash asset.\n\n" +
|
|
972
1576
|
"Positive feedback boosts an asset's EMA utility score, making it rank higher\n" +
|
|
973
1577
|
"in future searches without requiring a full reindex.\n\n" +
|
|
974
|
-
"Negative feedback records a negative signal in usage_events and events
|
|
1578
|
+
"Negative feedback records a negative signal in usage_events and state.db events.\n" +
|
|
975
1579
|
"It does NOT immediately lower the asset's ranking — the EMA utility score is\n" +
|
|
976
1580
|
"updated the next time `akm index` runs (incremental or full). Run `akm index`\n" +
|
|
977
1581
|
"after recording negative feedback to have it reflected in search results.",
|
|
@@ -988,11 +1592,27 @@ const feedbackCommand = defineCommand({
|
|
|
988
1592
|
"Reindexing is required for the signal to affect search results.",
|
|
989
1593
|
default: false,
|
|
990
1594
|
},
|
|
991
|
-
|
|
1595
|
+
reason: {
|
|
1596
|
+
type: "string",
|
|
1597
|
+
description: "Reason for the feedback (required for negative feedback by default; used by distillation)",
|
|
1598
|
+
},
|
|
1599
|
+
note: { type: "string", description: "Alias for --reason (backward-compatible, prefer --reason)" },
|
|
1600
|
+
"failure-mode": {
|
|
1601
|
+
type: "string",
|
|
1602
|
+
description: `Structured failure-mode taxonomy for negative feedback (F-3 / #384). ` +
|
|
1603
|
+
`Accepted values: ${FEEDBACK_FAILURE_MODES.join(", ")}. ` +
|
|
1604
|
+
"Stored alongside --reason in event metadata for aggregation by the distill pipeline.",
|
|
1605
|
+
},
|
|
992
1606
|
tag: {
|
|
993
1607
|
type: "string",
|
|
994
1608
|
description: "Tag to attach to the feedback (repeatable, e.g. --tag slice:train --tag team:platform)",
|
|
995
1609
|
},
|
|
1610
|
+
"applied-to": {
|
|
1611
|
+
type: "string",
|
|
1612
|
+
description: "Credit a lesson that helped resolve this task. Accepts a `lesson:<name>` ref. " +
|
|
1613
|
+
"When combined with --positive, appends this feedback ref to the target lesson's " +
|
|
1614
|
+
"`lessonStrength[]` frontmatter array (dedup, idempotent). Ignored on non-lesson targets.",
|
|
1615
|
+
},
|
|
996
1616
|
},
|
|
997
1617
|
run({ args }) {
|
|
998
1618
|
return runWithJsonErrors(async () => {
|
|
@@ -1008,11 +1628,39 @@ const feedbackCommand = defineCommand({
|
|
|
1008
1628
|
throw new UsageError("Specify --positive or --negative.");
|
|
1009
1629
|
}
|
|
1010
1630
|
const signal = args.positive ? "positive" : "negative";
|
|
1631
|
+
const reason = args.reason ?? args.note;
|
|
1632
|
+
// F-3 / #384: Validate --failure-mode against the curated enum.
|
|
1633
|
+
const failureMode = args["failure-mode"]?.trim() || undefined;
|
|
1634
|
+
if (failureMode) {
|
|
1635
|
+
if (args.positive) {
|
|
1636
|
+
throw new UsageError("--failure-mode is only valid for negative feedback.", "INVALID_FLAG_VALUE", "Remove --failure-mode or switch to --negative.");
|
|
1637
|
+
}
|
|
1638
|
+
const cfg = loadConfig();
|
|
1639
|
+
const allowedModes = cfg.feedback?.allowedFailureModes ?? FEEDBACK_FAILURE_MODES;
|
|
1640
|
+
if (allowedModes.length > 0 && !allowedModes.includes(failureMode)) {
|
|
1641
|
+
throw new UsageError(`Invalid --failure-mode "${failureMode}". Accepted values: ${allowedModes.join(", ")}.`, "INVALID_FLAG_VALUE", `Use one of: ${allowedModes.join(", ")}`);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
if (args.negative === true && !reason?.trim()) {
|
|
1645
|
+
// F-3 / #384: Default requireReason is now true. Load config to allow
|
|
1646
|
+
// operators to opt out via feedback.requireReason: false in akm.json.
|
|
1647
|
+
const cfg = loadConfig();
|
|
1648
|
+
const requireReason = cfg.feedback?.requireReason ?? true; // Default: true (F-3 / #384)
|
|
1649
|
+
if (requireReason) {
|
|
1650
|
+
throw new UsageError("Negative feedback requires --reason (structured failure signals are needed for distillation). " +
|
|
1651
|
+
"Use --failure-mode for a curated taxonomy or --reason for free text. " +
|
|
1652
|
+
"Set feedback.requireReason: false in akm.json to downgrade to a warning.", "MISSING_REQUIRED_ARGUMENT", `Hint: akm feedback ${ref} --negative --reason "..." [--failure-mode incorrect|outdated|dangerous|incomplete|redundant]`);
|
|
1653
|
+
}
|
|
1654
|
+
else {
|
|
1655
|
+
warn("Warning: negative feedback without --reason provides less distillation signal.");
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1011
1658
|
const rawTags = parseAllFlagValues("--tag");
|
|
1012
1659
|
const validatedTags = validateFeedbackTags(rawTags);
|
|
1013
1660
|
const metadataObj = {
|
|
1014
1661
|
signal,
|
|
1015
|
-
...(
|
|
1662
|
+
...(reason?.trim() ? { reason: reason.trim() } : {}),
|
|
1663
|
+
...(failureMode ? { failureMode } : {}),
|
|
1016
1664
|
...(validatedTags.length > 0 ? { tags: validatedTags } : {}),
|
|
1017
1665
|
};
|
|
1018
1666
|
const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
|
|
@@ -1021,6 +1669,7 @@ const feedbackCommand = defineCommand({
|
|
|
1021
1669
|
if (sources.length > 0) {
|
|
1022
1670
|
await ensureIndex(sources[0].path);
|
|
1023
1671
|
}
|
|
1672
|
+
let utilityResult;
|
|
1024
1673
|
const db = openExistingDatabase();
|
|
1025
1674
|
try {
|
|
1026
1675
|
const entryId = findEntryIdByRef(db, ref);
|
|
@@ -1040,6 +1689,26 @@ const feedbackCommand = defineCommand({
|
|
|
1040
1689
|
signal,
|
|
1041
1690
|
metadata: metadataStr,
|
|
1042
1691
|
});
|
|
1692
|
+
// Apply feedback-derived utility score adjustment immediately so that
|
|
1693
|
+
// positive/negative signals influence search ranking without requiring
|
|
1694
|
+
// a full reindex. We query the total accumulated feedback counts from
|
|
1695
|
+
// usage_events so the delta reflects the entire signal history.
|
|
1696
|
+
// Uses MemRL bounded-step EMA (F-5 / #386, arXiv:2601.03192).
|
|
1697
|
+
try {
|
|
1698
|
+
const counts = db
|
|
1699
|
+
.prepare(`SELECT
|
|
1700
|
+
SUM(CASE WHEN signal = 'positive' THEN 1 ELSE 0 END) AS pos,
|
|
1701
|
+
SUM(CASE WHEN signal = 'negative' THEN 1 ELSE 0 END) AS neg
|
|
1702
|
+
FROM usage_events
|
|
1703
|
+
WHERE event_type = 'feedback' AND entry_id = ?`)
|
|
1704
|
+
.get(entryId);
|
|
1705
|
+
const pos = counts?.pos ?? 0;
|
|
1706
|
+
const neg = counts?.neg ?? 0;
|
|
1707
|
+
utilityResult = applyFeedbackToUtilityScore(db, entryId, pos, neg);
|
|
1708
|
+
}
|
|
1709
|
+
catch {
|
|
1710
|
+
// best-effort — feedback recording succeeds even if utility update fails
|
|
1711
|
+
}
|
|
1043
1712
|
}
|
|
1044
1713
|
finally {
|
|
1045
1714
|
closeDatabase(db);
|
|
@@ -1049,129 +1718,187 @@ const feedbackCommand = defineCommand({
|
|
|
1049
1718
|
ref,
|
|
1050
1719
|
metadata: metadataObj,
|
|
1051
1720
|
});
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1721
|
+
// F-5 / #386: When a high-utility asset crosses below the review threshold,
|
|
1722
|
+
// auto-create a review-needed escalation proposal so a human can confirm
|
|
1723
|
+
// whether the negative feedback is valid before the asset falls out of
|
|
1724
|
+
// the improve loop. Best-effort — failure is logged but does not fail the
|
|
1725
|
+
// feedback command.
|
|
1726
|
+
// Emit a structured event rather than a proposal so the review-needed
|
|
1727
|
+
// signal is queryable via `akm events list --type improve_review_needed`
|
|
1728
|
+
// without risking accidental asset overwrite if the proposal is accepted.
|
|
1729
|
+
if (utilityResult?.crossedReviewThreshold) {
|
|
1730
|
+
try {
|
|
1731
|
+
appendEvent({
|
|
1732
|
+
eventType: "improve_review_needed",
|
|
1733
|
+
ref,
|
|
1734
|
+
metadata: {
|
|
1735
|
+
previousUtility: utilityResult.previousUtility,
|
|
1736
|
+
nextUtility: utilityResult.nextUtility,
|
|
1737
|
+
reason: reason?.trim() ?? null,
|
|
1738
|
+
failureMode: failureMode ?? null,
|
|
1739
|
+
},
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
catch (escalationErr) {
|
|
1743
|
+
warn(`[feedback] Could not emit review-needed event for ${ref}: ${escalationErr instanceof Error ? escalationErr.message : String(escalationErr)}`);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
// Phase 7A / Advantage D4b: --applied-to credits a lesson. When the
|
|
1747
|
+
// target is a `lesson:<name>` ref and the signal is positive, append
|
|
1748
|
+
// the feedback ref to the target lesson's `lessonStrength[]`
|
|
1749
|
+
// frontmatter array (dedup, idempotent). Non-lesson targets are
|
|
1750
|
+
// ignored. Failures here are warnings — feedback recording is the
|
|
1751
|
+
// primary contract and must not regress on lesson-write errors.
|
|
1752
|
+
const appliedToRaw = args["applied-to"]?.trim();
|
|
1753
|
+
let appliedToResult = null;
|
|
1754
|
+
if (appliedToRaw && signal === "positive") {
|
|
1755
|
+
try {
|
|
1756
|
+
const parsedApplied = parseAssetRef(appliedToRaw);
|
|
1757
|
+
if (parsedApplied.type === "lesson") {
|
|
1758
|
+
const updated = appendLessonStrength(parsedApplied.type, parsedApplied.name, ref);
|
|
1759
|
+
if (updated) {
|
|
1760
|
+
appliedToResult = { lessonRef: appliedToRaw, strength: updated.strength };
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
catch (err) {
|
|
1765
|
+
warn(`[feedback] --applied-to failed for ${appliedToRaw}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
else if (appliedToRaw && signal !== "positive") {
|
|
1769
|
+
warn("[feedback] --applied-to is ignored without --positive; lesson credit is only recorded on positive signals.");
|
|
1770
|
+
}
|
|
1771
|
+
output("feedback", {
|
|
1772
|
+
ok: true,
|
|
1773
|
+
ref,
|
|
1774
|
+
signal,
|
|
1775
|
+
reason: reason?.trim() ?? null,
|
|
1776
|
+
failureMode: failureMode ?? null,
|
|
1777
|
+
tags: validatedTags,
|
|
1778
|
+
...(appliedToResult
|
|
1779
|
+
? { appliedTo: { ref: appliedToResult.lessonRef, lessonStrength: appliedToResult.strength } }
|
|
1780
|
+
: {}),
|
|
1781
|
+
});
|
|
1782
|
+
});
|
|
1783
|
+
},
|
|
1784
|
+
});
|
|
1785
|
+
/**
|
|
1786
|
+
* Phase 7A: append a feedback ref to a lesson's `lessonStrength[]`
|
|
1787
|
+
* frontmatter array. Returns `{ strength }` (post-update count) on success,
|
|
1788
|
+
* or `null` when the lesson cannot be located. Idempotent: if the ref is
|
|
1789
|
+
* already credited, no write occurs.
|
|
1790
|
+
*
|
|
1791
|
+
* The function looks up the lesson's file via the indexer DB so the write
|
|
1792
|
+
* targets the canonical on-disk location. Frontmatter is rewritten in
|
|
1793
|
+
* place (no asset-spec round-trip) because we're modifying a single key on
|
|
1794
|
+
* an existing asset — the same pattern memory-inference uses for
|
|
1795
|
+
* `inferenceProcessed`.
|
|
1796
|
+
*/
|
|
1797
|
+
function appendLessonStrength(type, name, feedbackRef) {
|
|
1798
|
+
const ref = `${type}:${name}`;
|
|
1799
|
+
let filePath;
|
|
1800
|
+
const db = openExistingDatabase();
|
|
1801
|
+
try {
|
|
1802
|
+
const entryId = findEntryIdByRef(db, ref);
|
|
1803
|
+
if (entryId === undefined) {
|
|
1804
|
+
warn(`[feedback] --applied-to: lesson ${ref} is not in the index.`);
|
|
1805
|
+
return null;
|
|
1806
|
+
}
|
|
1807
|
+
const row = db.prepare("SELECT file_path FROM entries WHERE id = ?").get(entryId);
|
|
1808
|
+
if (!row?.file_path) {
|
|
1809
|
+
warn(`[feedback] --applied-to: cannot resolve file path for ${ref}.`);
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
filePath = row.file_path;
|
|
1813
|
+
}
|
|
1814
|
+
finally {
|
|
1815
|
+
closeDatabase(db);
|
|
1816
|
+
}
|
|
1817
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
1818
|
+
warn(`[feedback] --applied-to: lesson file missing on disk for ${ref}.`);
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1821
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
1822
|
+
const parsed = parseFrontmatter(raw);
|
|
1823
|
+
const data = { ...parsed.data };
|
|
1824
|
+
const existing = data.lessonStrength;
|
|
1825
|
+
const strengthList = Array.isArray(existing)
|
|
1826
|
+
? existing.filter((x) => typeof x === "string" && x.trim().length > 0).map((x) => x.trim())
|
|
1827
|
+
: typeof existing === "string" && existing.trim().length > 0
|
|
1828
|
+
? [existing.trim()]
|
|
1829
|
+
: [];
|
|
1830
|
+
if (strengthList.includes(feedbackRef)) {
|
|
1831
|
+
// Already credited — idempotent no-op.
|
|
1832
|
+
return { strength: strengthList.length };
|
|
1833
|
+
}
|
|
1834
|
+
strengthList.push(feedbackRef);
|
|
1835
|
+
data.lessonStrength = strengthList;
|
|
1836
|
+
const block = parseFrontmatterBlock(raw);
|
|
1837
|
+
const body = block?.content ?? raw;
|
|
1838
|
+
const next = assembleAsset(data, body);
|
|
1839
|
+
try {
|
|
1840
|
+
// Preserve the existing file's permission bits (markdown assets are
|
|
1841
|
+
// typically 0o644); writeFileAtomic defaults to 0o600 otherwise.
|
|
1842
|
+
const mode = fs.statSync(filePath).mode & 0o777;
|
|
1843
|
+
writeFileAtomic(filePath, next, mode);
|
|
1844
|
+
}
|
|
1845
|
+
catch (err) {
|
|
1846
|
+
warn(`[feedback] --applied-to: failed to write ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1847
|
+
return null;
|
|
1848
|
+
}
|
|
1849
|
+
return { strength: strengthList.length };
|
|
1850
|
+
}
|
|
1851
|
+
const historyCommand = defineCommand({
|
|
1852
|
+
meta: {
|
|
1853
|
+
name: "history",
|
|
1854
|
+
description: "Show mutation/usage history for a single asset (--ref) or stash-wide.\n\n" +
|
|
1855
|
+
"Event sources:\n" +
|
|
1061
1856
|
" usage_events (default): search, show, and feedback events from the local index.\n" +
|
|
1062
|
-
"
|
|
1063
|
-
" emitted by `akm
|
|
1857
|
+
" state.db events (--include-proposals): proposal lifecycle events (promoted, rejected)\n" +
|
|
1858
|
+
" emitted by `akm accept` / `akm reject`.\n\n" +
|
|
1064
1859
|
"Results from all active sources are merged and sorted chronologically.",
|
|
1065
1860
|
},
|
|
1066
1861
|
args: {
|
|
1067
1862
|
ref: { type: "string", description: "Asset ref (type:name). Omit for stash-wide history." },
|
|
1068
1863
|
since: { type: "string", description: "ISO timestamp or epoch ms — only events on/after this time" },
|
|
1864
|
+
source: {
|
|
1865
|
+
type: "string",
|
|
1866
|
+
description: 'Filter by event source: "user" (default) or "improve" (akm improve operations).',
|
|
1867
|
+
},
|
|
1069
1868
|
"include-proposals": {
|
|
1070
1869
|
type: "boolean",
|
|
1071
|
-
description: "Also include proposal lifecycle events (promoted, rejected) from events.
|
|
1870
|
+
description: "Also include proposal lifecycle events (promoted, rejected) from state.db events. " +
|
|
1072
1871
|
"Default: false (usage_events only).",
|
|
1073
1872
|
default: false,
|
|
1074
1873
|
},
|
|
1874
|
+
"accept-rate-by-source": {
|
|
1875
|
+
type: "boolean",
|
|
1876
|
+
description: "Compute accept-rate-per-source metrics from the proposal store and include them in the output (F-4 / #385). " +
|
|
1877
|
+
"Useful for measuring which generators (reflect, distill, …) produce the most accepted proposals.",
|
|
1878
|
+
default: false,
|
|
1879
|
+
},
|
|
1075
1880
|
format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
|
|
1076
1881
|
},
|
|
1077
1882
|
run({ args }) {
|
|
1078
1883
|
return runWithJsonErrors(async () => {
|
|
1884
|
+
const sourceFlag = args.source;
|
|
1885
|
+
if (sourceFlag !== undefined && sourceFlag !== "user" && sourceFlag !== "improve") {
|
|
1886
|
+
throw new UsageError(`Invalid --source value: "${args.source}". Must be "user" or "improve".`, "INVALID_FLAG_VALUE");
|
|
1887
|
+
}
|
|
1888
|
+
const sources = resolveSourceEntries();
|
|
1889
|
+
const stashDir = sources[0]?.path;
|
|
1079
1890
|
const result = await akmHistory({
|
|
1080
1891
|
ref: args.ref,
|
|
1081
1892
|
since: args.since,
|
|
1893
|
+
source: sourceFlag,
|
|
1082
1894
|
includeProposals: args["include-proposals"],
|
|
1895
|
+
acceptRateBySource: args["accept-rate-by-source"],
|
|
1896
|
+
stashDir,
|
|
1083
1897
|
});
|
|
1084
1898
|
output("history", result);
|
|
1085
1899
|
});
|
|
1086
1900
|
},
|
|
1087
1901
|
});
|
|
1088
|
-
function normalizeMarkdownAssetName(name, fallback) {
|
|
1089
|
-
const trimmed = (name ?? fallback)
|
|
1090
|
-
.trim()
|
|
1091
|
-
.replace(/\\/g, "/")
|
|
1092
|
-
.replace(/^\/+|\/+$/g, "")
|
|
1093
|
-
.replace(/\.md$/i, "");
|
|
1094
|
-
if (!trimmed)
|
|
1095
|
-
throw new UsageError("Asset name cannot be empty.");
|
|
1096
|
-
const segments = trimmed.split("/");
|
|
1097
|
-
if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
|
|
1098
|
-
throw new UsageError("Asset name must be a relative path without '.' or '..' segments.");
|
|
1099
|
-
}
|
|
1100
|
-
return trimmed;
|
|
1101
|
-
}
|
|
1102
|
-
function slugifyAssetName(value, fallbackPrefix) {
|
|
1103
|
-
const slug = value
|
|
1104
|
-
.toLowerCase()
|
|
1105
|
-
.replace(/^[#>\-\s]+/, "")
|
|
1106
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
1107
|
-
.replace(/^-+|-+$/g, "")
|
|
1108
|
-
.slice(0, MAX_CAPTURED_ASSET_SLUG_LENGTH);
|
|
1109
|
-
return slug || `${fallbackPrefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
1110
|
-
}
|
|
1111
|
-
function inferAssetName(content, fallbackPrefix, preferred) {
|
|
1112
|
-
const firstNonEmptyLine = content
|
|
1113
|
-
.split(/\r?\n/)
|
|
1114
|
-
.map((line) => line.trim())
|
|
1115
|
-
.find((line) => line.length > 0);
|
|
1116
|
-
const basis = preferred?.trim() || firstNonEmptyLine || fallbackPrefix;
|
|
1117
|
-
return slugifyAssetName(basis, fallbackPrefix);
|
|
1118
|
-
}
|
|
1119
|
-
function readKnowledgeContent(source) {
|
|
1120
|
-
if (source === "-") {
|
|
1121
|
-
const content = tryReadStdinText();
|
|
1122
|
-
if (!content?.trim()) {
|
|
1123
|
-
throw new UsageError("No stdin content received. Pipe a document into stdin or pass a file path.");
|
|
1124
|
-
}
|
|
1125
|
-
return { content };
|
|
1126
|
-
}
|
|
1127
|
-
const resolvedSource = path.resolve(source);
|
|
1128
|
-
let stat;
|
|
1129
|
-
try {
|
|
1130
|
-
stat = fs.statSync(resolvedSource);
|
|
1131
|
-
}
|
|
1132
|
-
catch {
|
|
1133
|
-
throw new UsageError(`Knowledge source not found: "${source}". Pass a readable file path or "-" for stdin.`);
|
|
1134
|
-
}
|
|
1135
|
-
if (!stat.isFile()) {
|
|
1136
|
-
throw new UsageError(`Knowledge source must be a file: "${source}".`);
|
|
1137
|
-
}
|
|
1138
|
-
return {
|
|
1139
|
-
content: fs.readFileSync(resolvedSource, "utf8"),
|
|
1140
|
-
preferredName: path.basename(resolvedSource, path.extname(resolvedSource)),
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
|
-
async function readKnowledgeInput(source) {
|
|
1144
|
-
if (!isHttpUrl(source))
|
|
1145
|
-
return readKnowledgeContent(source);
|
|
1146
|
-
const snapshot = await fetchWebsiteMarkdownSnapshot(source);
|
|
1147
|
-
return { content: snapshot.content, preferredName: snapshot.preferredName };
|
|
1148
|
-
}
|
|
1149
|
-
async function writeMarkdownAsset(options) {
|
|
1150
|
-
// Resolve write target via the v1 precedence chain (`--target` →
|
|
1151
|
-
// `defaultWriteTarget` → working stash). Per spec §10 step 5, this is the
|
|
1152
|
-
// single dispatch point — `core/write-source.ts` owns all kind-branching.
|
|
1153
|
-
const cfg = loadConfig();
|
|
1154
|
-
const { source, config } = resolveWriteTarget(cfg, options.target);
|
|
1155
|
-
const typeRoot = path.join(source.path, options.type === "knowledge" ? "knowledge" : "memories");
|
|
1156
|
-
const normalizedName = normalizeMarkdownAssetName(options.name, inferAssetName(options.content, options.fallbackPrefix, options.preferredName));
|
|
1157
|
-
// Pre-flight: existence + force semantics. The helper itself overwrites
|
|
1158
|
-
// unconditionally; the CLI surfaces a friendlier UsageError before any
|
|
1159
|
-
// disk activity when --force is absent.
|
|
1160
|
-
const assetPath = resolveAssetPathFromName(options.type, typeRoot, normalizedName);
|
|
1161
|
-
if (!isWithin(assetPath, typeRoot)) {
|
|
1162
|
-
throw new UsageError(`Resolved ${options.type} path escapes the stash: "${normalizedName}"`);
|
|
1163
|
-
}
|
|
1164
|
-
if (fs.existsSync(assetPath) && !options.force) {
|
|
1165
|
-
throw new UsageError(`${options.type === "knowledge" ? "Knowledge" : "Memory"} "${normalizedName}" already exists. Re-run with --force to overwrite it.`, "RESOURCE_ALREADY_EXISTS");
|
|
1166
|
-
}
|
|
1167
|
-
// Delegate the actual write (and optional git commit/push) to the helper.
|
|
1168
|
-
const result = await writeAssetToSource(source, config, { type: options.type, name: normalizedName }, options.content);
|
|
1169
|
-
return {
|
|
1170
|
-
ref: result.ref,
|
|
1171
|
-
path: result.path,
|
|
1172
|
-
stashDir: source.path,
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
1902
|
const workflowStartCommand = defineCommand({
|
|
1176
1903
|
meta: {
|
|
1177
1904
|
name: "start",
|
|
@@ -1196,16 +1923,20 @@ const workflowNextCommand = defineCommand({
|
|
|
1196
1923
|
args: {
|
|
1197
1924
|
target: { type: "positional", description: "Workflow run id or workflow ref", required: true },
|
|
1198
1925
|
params: { type: "string", description: "Workflow parameters as a JSON object (only for auto-started runs)" },
|
|
1926
|
+
"dry-run": { type: "boolean", description: "Not supported — rejected with an error", default: false },
|
|
1199
1927
|
},
|
|
1200
1928
|
async run({ args }) {
|
|
1201
1929
|
await runWithJsonErrors(async () => {
|
|
1930
|
+
if (getHyphenatedBoolean(args, "dry-run")) {
|
|
1931
|
+
throw new UsageError("`akm workflow next` does not support --dry-run. Remove the flag to start or resume a run.", "INVALID_FLAG_VALUE");
|
|
1932
|
+
}
|
|
1202
1933
|
const parsedParams = args.params ? parseWorkflowJsonObject(args.params, "--params") : undefined;
|
|
1203
1934
|
// If the target looks like a UUID-style run id (no `:` and matches the
|
|
1204
1935
|
// run-id shape), short-circuit with a structured WORKFLOW_NOT_FOUND
|
|
1205
1936
|
// error before parseAssetRef gets to throw an unhelpful ref-parse error.
|
|
1206
1937
|
if (looksLikeWorkflowRunId(args.target)) {
|
|
1207
1938
|
const { hasWorkflowRun } = await import("./workflows/runs.js");
|
|
1208
|
-
if (!hasWorkflowRun(args.target)) {
|
|
1939
|
+
if (!(await hasWorkflowRun(args.target))) {
|
|
1209
1940
|
throw new NotFoundError(`Workflow run "${args.target}" not found.`, "WORKFLOW_NOT_FOUND", "Run `akm workflow list --active` to see runs.");
|
|
1210
1941
|
}
|
|
1211
1942
|
}
|
|
@@ -1250,7 +1981,7 @@ const workflowCompleteCommand = defineCommand({
|
|
|
1250
1981
|
},
|
|
1251
1982
|
async run({ args }) {
|
|
1252
1983
|
await runWithJsonErrors(async () => {
|
|
1253
|
-
const result = completeWorkflowStep({
|
|
1984
|
+
const result = await completeWorkflowStep({
|
|
1254
1985
|
runId: args.runId,
|
|
1255
1986
|
stepId: args.step,
|
|
1256
1987
|
status: parseWorkflowStepState(args.state),
|
|
@@ -1270,7 +2001,7 @@ const workflowStatusCommand = defineCommand({
|
|
|
1270
2001
|
target: { type: "positional", description: "Workflow run id or workflow ref (workflow:<name>)", required: true },
|
|
1271
2002
|
},
|
|
1272
2003
|
run({ args }) {
|
|
1273
|
-
return runWithJsonErrors(() => {
|
|
2004
|
+
return runWithJsonErrors(async () => {
|
|
1274
2005
|
const target = args.target;
|
|
1275
2006
|
// Check if target looks like a workflow ref
|
|
1276
2007
|
const parsed = (() => {
|
|
@@ -1283,18 +2014,18 @@ const workflowStatusCommand = defineCommand({
|
|
|
1283
2014
|
})();
|
|
1284
2015
|
if (parsed?.type === "workflow") {
|
|
1285
2016
|
const ref = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`;
|
|
1286
|
-
const { runs } = listWorkflowRuns({ workflowRef: ref });
|
|
2017
|
+
const { runs } = await listWorkflowRuns({ workflowRef: ref });
|
|
1287
2018
|
if (runs.length === 0) {
|
|
1288
2019
|
throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
|
|
1289
2020
|
}
|
|
1290
2021
|
const mostRecent = runs[0];
|
|
1291
2022
|
if (!mostRecent)
|
|
1292
2023
|
throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
|
|
1293
|
-
const result = getWorkflowStatus(mostRecent.id);
|
|
2024
|
+
const result = await getWorkflowStatus(mostRecent.id);
|
|
1294
2025
|
output("workflow-status", result);
|
|
1295
2026
|
}
|
|
1296
2027
|
else {
|
|
1297
|
-
const result = getWorkflowStatus(target);
|
|
2028
|
+
const result = await getWorkflowStatus(target);
|
|
1298
2029
|
output("workflow-status", result);
|
|
1299
2030
|
}
|
|
1300
2031
|
});
|
|
@@ -1310,8 +2041,8 @@ const workflowListCommand = defineCommand({
|
|
|
1310
2041
|
active: { type: "boolean", description: "Only show active runs", default: false },
|
|
1311
2042
|
},
|
|
1312
2043
|
run({ args }) {
|
|
1313
|
-
return runWithJsonErrors(() => {
|
|
1314
|
-
const result = listWorkflowRuns({ workflowRef: args.ref, activeOnly: args.active });
|
|
2044
|
+
return runWithJsonErrors(async () => {
|
|
2045
|
+
const result = await listWorkflowRuns({ workflowRef: args.ref, activeOnly: args.active });
|
|
1315
2046
|
output("workflow-list", result);
|
|
1316
2047
|
});
|
|
1317
2048
|
},
|
|
@@ -1424,8 +2155,8 @@ const workflowResumeCommand = defineCommand({
|
|
|
1424
2155
|
runId: { type: "positional", description: "Workflow run id", required: true },
|
|
1425
2156
|
},
|
|
1426
2157
|
run({ args }) {
|
|
1427
|
-
return runWithJsonErrors(() => {
|
|
1428
|
-
const result = resumeWorkflowRun(args.runId);
|
|
2158
|
+
return runWithJsonErrors(async () => {
|
|
2159
|
+
const result = await resumeWorkflowRun(args.runId);
|
|
1429
2160
|
output("workflow-resume", result);
|
|
1430
2161
|
});
|
|
1431
2162
|
},
|
|
@@ -1447,10 +2178,10 @@ const workflowCommand = defineCommand({
|
|
|
1447
2178
|
validate: workflowValidateCommand,
|
|
1448
2179
|
},
|
|
1449
2180
|
run({ args }) {
|
|
1450
|
-
return runWithJsonErrors(() => {
|
|
2181
|
+
return runWithJsonErrors(async () => {
|
|
1451
2182
|
if (hasWorkflowSubcommand(args))
|
|
1452
2183
|
return;
|
|
1453
|
-
output("workflow-list", listWorkflowRuns({ activeOnly: true }));
|
|
2184
|
+
output("workflow-list", await listWorkflowRuns({ activeOnly: true }));
|
|
1454
2185
|
});
|
|
1455
2186
|
},
|
|
1456
2187
|
});
|
|
@@ -1520,6 +2251,10 @@ const rememberCommand = defineCommand({
|
|
|
1520
2251
|
type: "string",
|
|
1521
2252
|
description: "Scope this memory to a channel name (persisted as `scope_channel` frontmatter)",
|
|
1522
2253
|
},
|
|
2254
|
+
showSimilar: {
|
|
2255
|
+
type: "boolean",
|
|
2256
|
+
description: "Return top-3 similar existing memories in output (opt-in)",
|
|
2257
|
+
},
|
|
1523
2258
|
},
|
|
1524
2259
|
async run({ args }) {
|
|
1525
2260
|
return runWithJsonErrors(async () => {
|
|
@@ -1541,14 +2276,25 @@ const rememberCommand = defineCommand({
|
|
|
1541
2276
|
if (typeof args.channel === "string" && args.channel.trim())
|
|
1542
2277
|
scopeFields.channel = args.channel.trim();
|
|
1543
2278
|
const hasScope = Object.keys(scopeFields).length > 0;
|
|
1544
|
-
const hasTagRequiringArgs = rawTags.length > 0 || !!args.expires || !!args.source || !!args.description
|
|
2279
|
+
const hasTagRequiringArgs = rawTags.length > 0 || !!args.expires || !!args.source || !!args.description;
|
|
1545
2280
|
const hasStructuredArgs = hasTagRequiringArgs || hasScope || args.auto;
|
|
1546
2281
|
if (!hasStructuredArgs) {
|
|
2282
|
+
// Phase 1B / Rec 7: even the zero-flag hot-path emits
|
|
2283
|
+
// `captureMode: hot` + `beliefState: asserted` so user-supplied
|
|
2284
|
+
// memories outrank background-derived ones during ranking.
|
|
2285
|
+
const frontmatterBlock = buildMemoryFrontmatter({
|
|
2286
|
+
captureMode: "hot",
|
|
2287
|
+
beliefState: "asserted",
|
|
2288
|
+
});
|
|
2289
|
+
const contentWithFrontmatter = `${frontmatterBlock}\n${body}`;
|
|
2290
|
+
// Derive the asset slug from the body (not the frontmatter block);
|
|
2291
|
+
// otherwise inferAssetName would key off the leading `---` delimiter.
|
|
1547
2292
|
const result = await writeMarkdownAsset({
|
|
1548
2293
|
type: "memory",
|
|
1549
|
-
content:
|
|
2294
|
+
content: contentWithFrontmatter,
|
|
1550
2295
|
name: args.name,
|
|
1551
2296
|
fallbackPrefix: "memory",
|
|
2297
|
+
preferredName: inferAssetName(body, "memory"),
|
|
1552
2298
|
force: args.force,
|
|
1553
2299
|
target: args.target,
|
|
1554
2300
|
});
|
|
@@ -1557,7 +2303,13 @@ const rememberCommand = defineCommand({
|
|
|
1557
2303
|
ref: result.ref,
|
|
1558
2304
|
metadata: { path: result.path, force: args.force === true },
|
|
1559
2305
|
});
|
|
1560
|
-
|
|
2306
|
+
if (args.showSimilar) {
|
|
2307
|
+
const similar = await fetchSimilarMemories(body.slice(0, 500), result.ref);
|
|
2308
|
+
output("remember", { ok: true, ...result, similar });
|
|
2309
|
+
}
|
|
2310
|
+
else {
|
|
2311
|
+
output("remember", { ok: true, ...result });
|
|
2312
|
+
}
|
|
1561
2313
|
return;
|
|
1562
2314
|
}
|
|
1563
2315
|
// ── Accumulate metadata from all three modes ──────────────────────────
|
|
@@ -1616,6 +2368,10 @@ const rememberCommand = defineCommand({
|
|
|
1616
2368
|
"Provide them via --tag <value>, --auto (heuristics), or --enrich (LLM).");
|
|
1617
2369
|
}
|
|
1618
2370
|
// ── Build frontmatter and write ───────────────────────────────────────
|
|
2371
|
+
// Phase 1B / Rec 7: the hot-path CLI write always marks the memory as
|
|
2372
|
+
// `captureMode: hot` and `beliefState: asserted`. Ranking applies a
|
|
2373
|
+
// hot-capture boost so user-supplied memories outrank otherwise-equal
|
|
2374
|
+
// background-derived ones.
|
|
1619
2375
|
const frontmatterBlock = buildMemoryFrontmatter({
|
|
1620
2376
|
description,
|
|
1621
2377
|
tags,
|
|
@@ -1623,6 +2379,8 @@ const rememberCommand = defineCommand({
|
|
|
1623
2379
|
observed_at,
|
|
1624
2380
|
expires,
|
|
1625
2381
|
subjective,
|
|
2382
|
+
captureMode: "hot",
|
|
2383
|
+
beliefState: "asserted",
|
|
1626
2384
|
...(hasScope ? { scope: scopeFields } : {}),
|
|
1627
2385
|
});
|
|
1628
2386
|
const contentWithFrontmatter = `${frontmatterBlock}\n${body}`;
|
|
@@ -1646,53 +2404,31 @@ const rememberCommand = defineCommand({
|
|
|
1646
2404
|
...(hasScope ? { scope: scopeFields } : {}),
|
|
1647
2405
|
},
|
|
1648
2406
|
});
|
|
1649
|
-
|
|
2407
|
+
if (args.showSimilar) {
|
|
2408
|
+
const similar = await fetchSimilarMemories((body ?? args.content ?? "").slice(0, 500), result.ref);
|
|
2409
|
+
output("remember", { ok: true, ...result, similar });
|
|
2410
|
+
}
|
|
2411
|
+
else {
|
|
2412
|
+
output("remember", { ok: true, ...result });
|
|
2413
|
+
}
|
|
1650
2414
|
});
|
|
1651
2415
|
},
|
|
1652
2416
|
});
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
return
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
content === parsedDetail &&
|
|
1665
|
-
wasRememberFlagValueConsumedAsContent(content, parsedDetail, "--detail")) {
|
|
1666
|
-
return undefined;
|
|
2417
|
+
/**
|
|
2418
|
+
* Best-effort top-3 similar memory search for `--show-similar`.
|
|
2419
|
+
* Scoped to memory: type; excludes the just-written ref.
|
|
2420
|
+
*/
|
|
2421
|
+
async function fetchSimilarMemories(query, excludeRef) {
|
|
2422
|
+
try {
|
|
2423
|
+
const result = await akmSearch({ query, type: "memory", limit: 4 });
|
|
2424
|
+
return (result.hits ?? [])
|
|
2425
|
+
.filter((h) => "ref" in h && h.ref !== excludeRef)
|
|
2426
|
+
.slice(0, 3)
|
|
2427
|
+
.map((h) => ({ ref: h.ref, ...(h.name ? { title: h.name } : {}) }));
|
|
1667
2428
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
function wasRememberFlagValueConsumedAsContent(content, flagValue, flagName) {
|
|
1671
|
-
const argv = process.argv.slice(2);
|
|
1672
|
-
const rememberIndex = argv.indexOf("remember");
|
|
1673
|
-
const tokens = rememberIndex >= 0 ? argv.slice(rememberIndex + 1) : argv;
|
|
1674
|
-
let flagIndex = -1;
|
|
1675
|
-
let flagConsumesNextToken = false;
|
|
1676
|
-
for (let i = 0; i < tokens.length; i += 1) {
|
|
1677
|
-
const token = tokens[i];
|
|
1678
|
-
if (token === flagName) {
|
|
1679
|
-
flagIndex = i;
|
|
1680
|
-
flagConsumesNextToken = true;
|
|
1681
|
-
break;
|
|
1682
|
-
}
|
|
1683
|
-
if (token === `${flagName}=${flagValue}`) {
|
|
1684
|
-
flagIndex = i;
|
|
1685
|
-
break;
|
|
1686
|
-
}
|
|
2429
|
+
catch {
|
|
2430
|
+
return [];
|
|
1687
2431
|
}
|
|
1688
|
-
if (flagIndex === -1)
|
|
1689
|
-
return false;
|
|
1690
|
-
if (tokens.slice(0, flagIndex).includes(content))
|
|
1691
|
-
return false;
|
|
1692
|
-
const firstTokenAfterFlag = flagIndex + (flagConsumesNextToken ? 2 : 1);
|
|
1693
|
-
if (tokens.slice(firstTokenAfterFlag).includes(content))
|
|
1694
|
-
return false;
|
|
1695
|
-
return true;
|
|
1696
2432
|
}
|
|
1697
2433
|
const importKnowledgeCommand = defineCommand({
|
|
1698
2434
|
meta: {
|
|
@@ -1782,10 +2518,11 @@ const helpCommand = defineCommand({
|
|
|
1782
2518
|
},
|
|
1783
2519
|
run({ args }) {
|
|
1784
2520
|
return runWithJsonErrors(() => {
|
|
1785
|
-
|
|
2521
|
+
const version = resolveHelpMigrateVersionArg(typeof args.version === "string" ? args.version : undefined);
|
|
2522
|
+
if (!version?.trim()) {
|
|
1786
2523
|
throw new UsageError("Usage: akm help migrate <version>.", "MISSING_REQUIRED_ARGUMENT", "Pass a version like `0.6.0`, `v0.6.0`, `0.6.0-rc1`, or `latest`.");
|
|
1787
2524
|
}
|
|
1788
|
-
process.stdout.write(renderMigrationHelp(
|
|
2525
|
+
process.stdout.write(renderMigrationHelp(version));
|
|
1789
2526
|
});
|
|
1790
2527
|
},
|
|
1791
2528
|
}),
|
|
@@ -1815,8 +2552,8 @@ const completionsCommand = defineCommand({
|
|
|
1815
2552
|
const script = generateBashCompletions(main);
|
|
1816
2553
|
if (args.install) {
|
|
1817
2554
|
const dest = installBashCompletions(script);
|
|
1818
|
-
|
|
1819
|
-
|
|
2555
|
+
info(`Completions installed to ${dest}`);
|
|
2556
|
+
info(`Restart your shell or run: source ${dest}`);
|
|
1820
2557
|
}
|
|
1821
2558
|
else {
|
|
1822
2559
|
process.stdout.write(script);
|
|
@@ -1912,6 +2649,13 @@ function resolveVaultPath(ref) {
|
|
|
1912
2649
|
const source = findVaultSource(parsed.origin);
|
|
1913
2650
|
const typeRoot = path.join(source.path, "vaults");
|
|
1914
2651
|
const absPath = resolveAssetPathFromName("vault", typeRoot, parsed.name);
|
|
2652
|
+
// Defense-in-depth: ensure the resolved path stays inside the vaults directory.
|
|
2653
|
+
// validateName already rejects traversal patterns like "../../foo", but an
|
|
2654
|
+
// absolute-path override or symlink-based attack could still escape without
|
|
2655
|
+
// this second check.
|
|
2656
|
+
if (!isWithin(absPath, typeRoot)) {
|
|
2657
|
+
throw new UsageError(`Vault name "${parsed.name}" escapes the vault directory.`);
|
|
2658
|
+
}
|
|
1915
2659
|
return { name: parsed.name, absPath, source, parsedRef: parsed };
|
|
1916
2660
|
}
|
|
1917
2661
|
/**
|
|
@@ -1940,6 +2684,10 @@ function listVaultsRecursive(listKeysFn) {
|
|
|
1940
2684
|
const canonical = deriveCanonicalAssetName("vault", vaultsDir, full);
|
|
1941
2685
|
if (!canonical)
|
|
1942
2686
|
continue;
|
|
2687
|
+
// Skip sensitive vaults: presence of a sibling .sensitive marker file suppresses listing.
|
|
2688
|
+
const markerPath = full.replace(/\.env$/, ".sensitive");
|
|
2689
|
+
if (fs.existsSync(markerPath))
|
|
2690
|
+
continue;
|
|
1943
2691
|
const { keys } = listKeysFn(full);
|
|
1944
2692
|
result.push({ ref: makeVaultRef(canonical, source), path: full, keys });
|
|
1945
2693
|
}
|
|
@@ -1962,6 +2710,9 @@ function splitVaultRunTarget(target) {
|
|
|
1962
2710
|
if (!key) {
|
|
1963
2711
|
throw new UsageError("Expected vault run target in the form <ref> or <ref/KEY>.");
|
|
1964
2712
|
}
|
|
2713
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
2714
|
+
throw new UsageError(`"${key}" is not a valid environment variable name.`, "INVALID_FLAG_VALUE");
|
|
2715
|
+
}
|
|
1965
2716
|
const resolved = resolveVaultPath(refPart);
|
|
1966
2717
|
if (!fs.existsSync(resolved.absPath)) {
|
|
1967
2718
|
throw new NotFoundError(`Vault not found: ${makeVaultRef(resolved.name, resolved.source)}`);
|
|
@@ -1982,48 +2733,100 @@ const vaultCreateCommand = defineCommand({
|
|
|
1982
2733
|
meta: { name: "create", description: "Create an empty vault file (no-op if it already exists)" },
|
|
1983
2734
|
args: {
|
|
1984
2735
|
name: { type: "positional", description: "Vault name (e.g. prod) — file becomes <name>.env", required: true },
|
|
2736
|
+
sensitive: {
|
|
2737
|
+
type: "boolean",
|
|
2738
|
+
description: "Exclude this vault from vault list output and the search index",
|
|
2739
|
+
default: false,
|
|
2740
|
+
},
|
|
1985
2741
|
},
|
|
1986
2742
|
run({ args }) {
|
|
1987
2743
|
return runWithJsonErrors(async () => {
|
|
1988
2744
|
const { createVault } = await import("./commands/vault.js");
|
|
1989
2745
|
const { name, absPath, source } = resolveVaultPath(args.name);
|
|
1990
2746
|
createVault(absPath);
|
|
1991
|
-
|
|
2747
|
+
if (args.sensitive) {
|
|
2748
|
+
const markerPath = absPath.replace(/\.env$/, ".sensitive");
|
|
2749
|
+
if (!fs.existsSync(markerPath)) {
|
|
2750
|
+
fs.writeFileSync(markerPath, "", { mode: 0o600 });
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
output("vault-create", { ref: makeVaultRef(name, source) });
|
|
1992
2754
|
});
|
|
1993
2755
|
},
|
|
1994
2756
|
});
|
|
1995
2757
|
const vaultSetCommand = defineCommand({
|
|
1996
2758
|
meta: {
|
|
1997
2759
|
name: "set",
|
|
1998
|
-
description: 'Set a key in a vault. Value is
|
|
2760
|
+
description: 'Set a key in a vault. Value is read from stdin by default (never via argv, avoiding /proc/cmdline exposure). Use --from-env <VAR> to read from an environment variable instead. Optionally attach a comment with --comment "description".',
|
|
1999
2761
|
},
|
|
2000
2762
|
args: {
|
|
2001
2763
|
ref: { type: "positional", description: "Vault ref (e.g. vault:prod or just prod)", required: true },
|
|
2002
|
-
key: { type: "positional", description: "Key name (e.g. DB_URL)
|
|
2003
|
-
value: {
|
|
2004
|
-
type: "positional",
|
|
2005
|
-
description: "Value to store (omit when using KEY=VALUE combined form)",
|
|
2006
|
-
required: false,
|
|
2007
|
-
},
|
|
2764
|
+
key: { type: "positional", description: "Key name (e.g. DB_URL)", required: true },
|
|
2008
2765
|
comment: { type: "string", description: "Optional comment written above the key line", required: false },
|
|
2766
|
+
"from-env": {
|
|
2767
|
+
type: "string",
|
|
2768
|
+
description: "Read value from the named environment variable instead of stdin",
|
|
2769
|
+
},
|
|
2009
2770
|
},
|
|
2010
2771
|
run({ args }) {
|
|
2011
2772
|
return runWithJsonErrors(async () => {
|
|
2773
|
+
// Trap the legacy 3-positional / KEY=VALUE forms removed in 0.8.0.
|
|
2774
|
+
//
|
|
2775
|
+
// Without this trap, citty silently accepts the extra positional and the
|
|
2776
|
+
// command falls through to read stdin. In cron/CI where stdin is empty,
|
|
2777
|
+
// the existing secret would be silently overwritten with an empty string
|
|
2778
|
+
// (silent data loss). Detect both removed forms and hard-error before
|
|
2779
|
+
// touching the vault file.
|
|
2780
|
+
//
|
|
2781
|
+
// Case 1: `akm vault set <ref> KEY=VALUE` — the second positional
|
|
2782
|
+
// contains `=`. Argv looks like:
|
|
2783
|
+
// [..., "vault", "set", "<ref>", "KEY=VALUE", ...]
|
|
2784
|
+
// citty binds `args.key = "KEY=VALUE"`.
|
|
2785
|
+
// Case 2: `akm vault set <ref> KEY VALUE` — a third positional follows
|
|
2786
|
+
// the key. citty mirrors every positional (including the
|
|
2787
|
+
// declared ones) into `args._`, so we detect this by length:
|
|
2788
|
+
// more than 2 positionals means an extra `<VALUE>` was passed.
|
|
2789
|
+
const allPositionals = Array.isArray(args._) ? args._.filter((v) => typeof v === "string") : [];
|
|
2790
|
+
const hasExtraPositional = allPositionals.length > 2;
|
|
2791
|
+
const keyHasEquals = typeof args.key === "string" && args.key.includes("=");
|
|
2792
|
+
if (hasExtraPositional || keyHasEquals) {
|
|
2793
|
+
throw new UsageError("'akm vault set' no longer accepts the value via argv (removed in 0.8.0 for security).\n" +
|
|
2794
|
+
" Pass the value via stdin or --from-env <VAR> instead:\n" +
|
|
2795
|
+
" printf '%s' \"$SECRET\" | akm vault set <ref> <KEY>\n" +
|
|
2796
|
+
' AKM_VALUE="$SECRET" akm vault set <ref> <KEY> --from-env AKM_VALUE', "INVALID_FLAG_VALUE");
|
|
2797
|
+
}
|
|
2012
2798
|
const { setKey } = await import("./commands/vault.js");
|
|
2013
2799
|
const { name, absPath, source } = resolveVaultPath(args.ref);
|
|
2014
|
-
|
|
2800
|
+
const fromEnv = getHyphenatedArg(args, "from-env");
|
|
2015
2801
|
let realValue;
|
|
2016
|
-
if (
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
|
|
2802
|
+
if (fromEnv !== undefined) {
|
|
2803
|
+
const envVal = process.env[fromEnv];
|
|
2804
|
+
if (envVal === undefined) {
|
|
2805
|
+
throw new UsageError(`Environment variable "${fromEnv}" is not set.`, "INVALID_FLAG_VALUE");
|
|
2806
|
+
}
|
|
2807
|
+
realValue = envVal;
|
|
2020
2808
|
}
|
|
2021
2809
|
else {
|
|
2022
|
-
|
|
2023
|
-
|
|
2810
|
+
// Print a prompt when stdin is attached to a terminal so an
|
|
2811
|
+
// interactive invocation doesn't silently hang with no indication
|
|
2812
|
+
// that input is being awaited.
|
|
2813
|
+
if (process.stdin.isTTY) {
|
|
2814
|
+
process.stderr.write(`Enter value for "${args.key}" (Ctrl-D when done):\n`);
|
|
2815
|
+
}
|
|
2816
|
+
const MAX_VAULT_VALUE_BYTES = 1024 * 1024; // 1 MB
|
|
2817
|
+
let totalBytes = 0;
|
|
2818
|
+
const chunks = [];
|
|
2819
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
2820
|
+
totalBytes += chunk.byteLength;
|
|
2821
|
+
if (totalBytes > MAX_VAULT_VALUE_BYTES) {
|
|
2822
|
+
throw new UsageError("Vault value exceeds 1 MB limit. Values must be provided via stdin.");
|
|
2823
|
+
}
|
|
2824
|
+
chunks.push(chunk);
|
|
2825
|
+
}
|
|
2826
|
+
realValue = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
2024
2827
|
}
|
|
2025
|
-
setKey(absPath,
|
|
2026
|
-
output("vault-set", { ref: makeVaultRef(name, source), key:
|
|
2828
|
+
setKey(absPath, args.key, realValue, args.comment);
|
|
2829
|
+
output("vault-set", { ref: makeVaultRef(name, source), key: args.key });
|
|
2027
2830
|
});
|
|
2028
2831
|
},
|
|
2029
2832
|
});
|
|
@@ -2032,16 +2835,24 @@ const vaultUnsetCommand = defineCommand({
|
|
|
2032
2835
|
args: {
|
|
2033
2836
|
ref: { type: "positional", description: "Vault ref", required: true },
|
|
2034
2837
|
key: { type: "positional", description: "Key name to remove", required: true },
|
|
2838
|
+
yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
|
|
2035
2839
|
},
|
|
2036
2840
|
run({ args }) {
|
|
2037
2841
|
return runWithJsonErrors(async () => {
|
|
2038
|
-
|
|
2842
|
+
// Validate path first so traversal errors surface before the confirmation prompt.
|
|
2039
2843
|
const { name, absPath, source } = resolveVaultPath(args.ref);
|
|
2844
|
+
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
2845
|
+
const confirmed = await confirmDestructive(`Remove key "${args.key}" from vault "${args.ref}"? This cannot be undone.`, { yes: args.yes === true });
|
|
2846
|
+
if (!confirmed) {
|
|
2847
|
+
process.stderr.write("Aborted.\n");
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
const { unsetKey } = await import("./commands/vault.js");
|
|
2040
2851
|
if (!fs.existsSync(absPath)) {
|
|
2041
2852
|
throw new NotFoundError(`Vault not found: ${makeVaultRef(name, source)}`);
|
|
2042
2853
|
}
|
|
2043
2854
|
const removed = unsetKey(absPath, args.key);
|
|
2044
|
-
output("vault-unset", { ref: makeVaultRef(name, source), key: args.key, removed
|
|
2855
|
+
output("vault-unset", { ref: makeVaultRef(name, source), key: args.key, removed });
|
|
2045
2856
|
});
|
|
2046
2857
|
},
|
|
2047
2858
|
});
|
|
@@ -2097,6 +2908,15 @@ const vaultRunCommand = defineCommand({
|
|
|
2097
2908
|
mergedEnv[envKey] = envValue;
|
|
2098
2909
|
}
|
|
2099
2910
|
}
|
|
2911
|
+
// Emit vault access event (keys only, no values) for audit trail.
|
|
2912
|
+
// Best-effort: never block vault run on event write failure.
|
|
2913
|
+
appendEvent({
|
|
2914
|
+
eventType: "vault_access",
|
|
2915
|
+
ref: makeVaultRef(name, source),
|
|
2916
|
+
metadata: {
|
|
2917
|
+
keys: key ? [key] : Object.keys(envValues),
|
|
2918
|
+
},
|
|
2919
|
+
});
|
|
2100
2920
|
const result = spawnSync(command[0], command.slice(1), {
|
|
2101
2921
|
stdio: "inherit",
|
|
2102
2922
|
env: mergedEnv,
|
|
@@ -2122,7 +2942,7 @@ const vaultCommand = defineCommand({
|
|
|
2122
2942
|
},
|
|
2123
2943
|
run({ args }) {
|
|
2124
2944
|
return runWithJsonErrors(async () => {
|
|
2125
|
-
if (
|
|
2945
|
+
if (hasSubcommand(args, VAULT_SUBCOMMAND_SET))
|
|
2126
2946
|
return;
|
|
2127
2947
|
// Default action: list all vaults
|
|
2128
2948
|
const { listKeys } = await import("./commands/vault.js");
|
|
@@ -2158,11 +2978,6 @@ const wikiRegisterCommand = defineCommand({
|
|
|
2158
2978
|
description: "Mark a git-backed source as writable so changes can be pushed back",
|
|
2159
2979
|
default: false,
|
|
2160
2980
|
},
|
|
2161
|
-
trust: {
|
|
2162
|
-
type: "boolean",
|
|
2163
|
-
description: "Bypass install-audit blocking for this registration only",
|
|
2164
|
-
default: false,
|
|
2165
|
-
},
|
|
2166
2981
|
"max-pages": { type: "string", description: "Maximum pages to crawl for website sources (default: 50)" },
|
|
2167
2982
|
"max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
|
|
2168
2983
|
},
|
|
@@ -2173,7 +2988,6 @@ const wikiRegisterCommand = defineCommand({
|
|
|
2173
2988
|
ref: args.ref.trim(),
|
|
2174
2989
|
name: args.name,
|
|
2175
2990
|
options: Object.keys(buildWebsiteOptions(args)).length > 0 ? buildWebsiteOptions(args) : undefined,
|
|
2176
|
-
trustThisInstall: args.trust,
|
|
2177
2991
|
writable: args.writable,
|
|
2178
2992
|
});
|
|
2179
2993
|
output("wiki-register", result);
|
|
@@ -2286,12 +3100,39 @@ const wikiStashCommand = defineCommand({
|
|
|
2286
3100
|
name: { type: "positional", description: "Wiki name", required: true },
|
|
2287
3101
|
source: { type: "positional", description: "Source file path, URL, or '-' to read from stdin", required: true },
|
|
2288
3102
|
as: { type: "string", description: "Preferred slug base (defaults to source filename or first-line slug)" },
|
|
3103
|
+
target: {
|
|
3104
|
+
type: "string",
|
|
3105
|
+
description: "Name of a writable stash source to write into instead of the default stash. Must match a configured source name (run `akm list` to see sources).",
|
|
3106
|
+
},
|
|
2289
3107
|
},
|
|
2290
3108
|
run({ args }) {
|
|
2291
3109
|
return runWithJsonErrors(async () => {
|
|
2292
3110
|
const { stashRaw } = await import("./wiki/wiki.js");
|
|
2293
|
-
const { content, preferredName } = await
|
|
2294
|
-
|
|
3111
|
+
const { content, preferredName } = await (async () => {
|
|
3112
|
+
if (!isHttpUrl(args.source))
|
|
3113
|
+
return readKnowledgeInput(args.source);
|
|
3114
|
+
const { fetchWebsiteMarkdownSnapshot } = await import("./sources/website-ingest");
|
|
3115
|
+
const snapshot = await fetchWebsiteMarkdownSnapshot(args.source);
|
|
3116
|
+
return { content: snapshot.content, preferredName: args.as ?? snapshot.preferredName };
|
|
3117
|
+
})();
|
|
3118
|
+
let stashDir;
|
|
3119
|
+
if (args.target) {
|
|
3120
|
+
// Resolve the named source to its filesystem path.
|
|
3121
|
+
const cfg = loadConfig();
|
|
3122
|
+
const sources = resolveConfiguredSources(cfg);
|
|
3123
|
+
const match = sources.find((s) => s.name === args.target);
|
|
3124
|
+
if (!match) {
|
|
3125
|
+
throw new UsageError(`--target must reference a configured source name. No source named "${args.target}" found. Run \`akm list\` to see available sources.`, "INVALID_FLAG_VALUE");
|
|
3126
|
+
}
|
|
3127
|
+
const spec = match.source;
|
|
3128
|
+
if (spec.type !== "filesystem" && spec.type !== "local") {
|
|
3129
|
+
throw new ConfigError(`Source "${args.target}" is not a filesystem source and cannot be used as a wiki stash target.`, "INVALID_CONFIG_FILE", `Use a source with type "filesystem" or "local", or omit --target to use the default stash.`);
|
|
3130
|
+
}
|
|
3131
|
+
stashDir = spec.path;
|
|
3132
|
+
}
|
|
3133
|
+
else {
|
|
3134
|
+
stashDir = resolveStashDir();
|
|
3135
|
+
}
|
|
2295
3136
|
const result = stashRaw({
|
|
2296
3137
|
stashDir,
|
|
2297
3138
|
wikiName: args.name,
|
|
@@ -2327,17 +3168,52 @@ const wikiLintCommand = defineCommand({
|
|
|
2327
3168
|
const wikiIngestCommand = defineCommand({
|
|
2328
3169
|
meta: {
|
|
2329
3170
|
name: "ingest",
|
|
2330
|
-
description: "
|
|
3171
|
+
description: "Dispatch an agent to execute the ingest workflow for this wiki. Uses --profile or config.defaults.agent.",
|
|
2331
3172
|
},
|
|
2332
3173
|
args: {
|
|
2333
3174
|
name: { type: "positional", description: "Wiki name", required: true },
|
|
3175
|
+
profile: {
|
|
3176
|
+
type: "string",
|
|
3177
|
+
description: "Agent profile to use (default: config.defaults.agent).",
|
|
3178
|
+
},
|
|
3179
|
+
model: {
|
|
3180
|
+
type: "string",
|
|
3181
|
+
description: "Model override — accepts aliases (opus, sonnet, haiku) or exact platform model IDs.",
|
|
3182
|
+
},
|
|
3183
|
+
"timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds." },
|
|
2334
3184
|
},
|
|
2335
3185
|
run({ args }) {
|
|
2336
3186
|
return runWithJsonErrors(async () => {
|
|
2337
3187
|
const { buildIngestWorkflow } = await import("./wiki/wiki.js");
|
|
2338
3188
|
const stashDir = resolveStashDir();
|
|
2339
|
-
const
|
|
2340
|
-
|
|
3189
|
+
const built = buildIngestWorkflow(stashDir, args.name);
|
|
3190
|
+
const config = loadConfig();
|
|
3191
|
+
const profileName = getStringArg(args, "profile") ?? config.defaults?.agent;
|
|
3192
|
+
if (!profileName) {
|
|
3193
|
+
throw new UsageError("akm wiki ingest requires an agent profile. Pass --profile <name> or set defaults.agent in config.", "MISSING_REQUIRED_ARGUMENT", "Available profiles are listed under profiles.agent in your config. Run `akm config get profiles.agent` to inspect.");
|
|
3194
|
+
}
|
|
3195
|
+
const timeoutMs = parsePositiveIntFlag(getHyphenatedArg(args, "timeout-ms"), "--timeout-ms");
|
|
3196
|
+
const model = getStringArg(args, "model");
|
|
3197
|
+
const { getDefaultLlmConfig } = await import("./core/config.js");
|
|
3198
|
+
const dispatchResult = await akmAgentDispatch({
|
|
3199
|
+
profileName,
|
|
3200
|
+
agentConfig: config,
|
|
3201
|
+
llmConfig: getDefaultLlmConfig(config),
|
|
3202
|
+
prompt: built.workflow,
|
|
3203
|
+
dispatch: {
|
|
3204
|
+
prompt: built.workflow,
|
|
3205
|
+
...(model !== undefined ? { model } : {}),
|
|
3206
|
+
},
|
|
3207
|
+
...(timeoutMs !== undefined && Number.isFinite(timeoutMs) ? { timeoutMs } : {}),
|
|
3208
|
+
});
|
|
3209
|
+
output("wiki-ingest", {
|
|
3210
|
+
wiki: built.wiki,
|
|
3211
|
+
path: built.path,
|
|
3212
|
+
schemaPath: built.schemaPath,
|
|
3213
|
+
dispatched: true,
|
|
3214
|
+
profile: profileName,
|
|
3215
|
+
agentResult: dispatchResult,
|
|
3216
|
+
});
|
|
2341
3217
|
});
|
|
2342
3218
|
},
|
|
2343
3219
|
});
|
|
@@ -2360,7 +3236,7 @@ const wikiCommand = defineCommand({
|
|
|
2360
3236
|
},
|
|
2361
3237
|
run({ args }) {
|
|
2362
3238
|
return runWithJsonErrors(async () => {
|
|
2363
|
-
if (
|
|
3239
|
+
if (hasSubcommand(args, WIKI_SUBCOMMAND_SET))
|
|
2364
3240
|
return;
|
|
2365
3241
|
// Default action: list wikis
|
|
2366
3242
|
const { listWikis } = await import("./wiki/wiki.js");
|
|
@@ -2369,15 +3245,15 @@ const wikiCommand = defineCommand({
|
|
|
2369
3245
|
},
|
|
2370
3246
|
});
|
|
2371
3247
|
// ── `akm events` ────────────────────────────────────────────────────────────
|
|
2372
|
-
// Append-only events stream surface (#204). `list` reads
|
|
2373
|
-
// with optional --since/--type/--ref filters; `tail` follows the
|
|
3248
|
+
// Append-only events stream surface (#204). `list` reads state.db events
|
|
3249
|
+
// with optional --since/--type/--ref filters; `tail` follows the table via
|
|
2374
3250
|
// a polling loop and prints each event as a single JSONL line.
|
|
2375
3251
|
const eventsListCommand = defineCommand({
|
|
2376
|
-
meta: { name: "list", description: "List events from the append-only
|
|
3252
|
+
meta: { name: "list", description: "List events from the append-only state.db events stream" },
|
|
2377
3253
|
args: {
|
|
2378
3254
|
since: {
|
|
2379
3255
|
type: "string",
|
|
2380
|
-
description: "ISO timestamp / epoch ms, OR `@offset:<
|
|
3256
|
+
description: "ISO timestamp / epoch ms, OR `@offset:<id>` for a durable row-id cursor (resume across processes)",
|
|
2381
3257
|
},
|
|
2382
3258
|
type: { type: "string", description: "Filter by event type (add, remove, remember, feedback, ...)" },
|
|
2383
3259
|
ref: { type: "string", description: "Filter by asset ref (type:name)" },
|
|
@@ -2406,11 +3282,11 @@ const eventsListCommand = defineCommand({
|
|
|
2406
3282
|
},
|
|
2407
3283
|
});
|
|
2408
3284
|
const eventsTailCommand = defineCommand({
|
|
2409
|
-
meta: { name: "tail", description: "Follow the append-only
|
|
3285
|
+
meta: { name: "tail", description: "Follow the append-only state.db events stream (polling)" },
|
|
2410
3286
|
args: {
|
|
2411
3287
|
since: {
|
|
2412
3288
|
type: "string",
|
|
2413
|
-
description: "ISO timestamp / epoch ms, OR `@offset:<
|
|
3289
|
+
description: "ISO timestamp / epoch ms, OR `@offset:<id>` for a durable row-id cursor (resume across processes)",
|
|
2414
3290
|
},
|
|
2415
3291
|
type: { type: "string", description: "Filter by event type" },
|
|
2416
3292
|
ref: { type: "string", description: "Filter by asset ref (type:name)" },
|
|
@@ -2428,9 +3304,9 @@ const eventsTailCommand = defineCommand({
|
|
|
2428
3304
|
},
|
|
2429
3305
|
async run({ args }) {
|
|
2430
3306
|
await runWithJsonErrors(async () => {
|
|
2431
|
-
const intervalMs =
|
|
2432
|
-
const maxDurationMs =
|
|
2433
|
-
const maxEvents =
|
|
3307
|
+
const intervalMs = parsePositiveIntFlag(getHyphenatedArg(args, "interval-ms"), "--interval-ms");
|
|
3308
|
+
const maxDurationMs = parsePositiveIntFlag(getHyphenatedArg(args, "max-duration-ms"), "--max-duration-ms");
|
|
3309
|
+
const maxEvents = parsePositiveIntFlag(getHyphenatedArg(args, "max-events"), "--max-events");
|
|
2434
3310
|
const mode = getOutputMode();
|
|
2435
3311
|
// In streaming text mode we want each event to print as soon as it
|
|
2436
3312
|
// arrives. The polling loop emits via `onEvent`; the final result is
|
|
@@ -2484,39 +3360,102 @@ const eventsTailCommand = defineCommand({
|
|
|
2484
3360
|
});
|
|
2485
3361
|
},
|
|
2486
3362
|
});
|
|
2487
|
-
function parsePositiveInt(raw, flag) {
|
|
2488
|
-
if (raw === undefined)
|
|
2489
|
-
return undefined;
|
|
2490
|
-
const trimmed = raw.trim();
|
|
2491
|
-
if (!trimmed)
|
|
2492
|
-
return undefined;
|
|
2493
|
-
const value = Number.parseInt(trimmed, 10);
|
|
2494
|
-
if (Number.isNaN(value) || value <= 0) {
|
|
2495
|
-
throw new UsageError(`Invalid ${flag} value: "${raw}". Must be a positive integer.`, "INVALID_FLAG_VALUE");
|
|
2496
|
-
}
|
|
2497
|
-
return value;
|
|
2498
|
-
}
|
|
2499
3363
|
const eventsCommand = defineCommand({
|
|
2500
3364
|
meta: {
|
|
2501
3365
|
name: "events",
|
|
2502
|
-
description: "Read or follow the append-only
|
|
3366
|
+
description: "Read or follow the append-only state.db events stream (mutations, feedback, indexing)",
|
|
2503
3367
|
},
|
|
2504
3368
|
subCommands: {
|
|
2505
3369
|
list: eventsListCommand,
|
|
2506
3370
|
tail: eventsTailCommand,
|
|
2507
3371
|
},
|
|
2508
3372
|
});
|
|
3373
|
+
// ── lessons subcommands (Phase 7A / Advantage D4c) ──────────────────────────
|
|
3374
|
+
const lessonsCoverageCommand = defineCommand({
|
|
3375
|
+
meta: {
|
|
3376
|
+
name: "coverage",
|
|
3377
|
+
description: "Report tags that exist on indexed assets but are NOT yet covered by any lesson.\n\n" +
|
|
3378
|
+
"Useful for spotting topics where the stash has skills/commands/scripts but no\n" +
|
|
3379
|
+
"crystallized lesson — a signal that the team has tacit knowledge worth distilling.\n\n" +
|
|
3380
|
+
"Default output is JSON: { uncoveredTags: string[], lessonTagCount: number, totalTagCount: number }.\n" +
|
|
3381
|
+
"Pass --format text for a plain-text bulleted list.",
|
|
3382
|
+
},
|
|
3383
|
+
args: {},
|
|
3384
|
+
run() {
|
|
3385
|
+
return runWithJsonErrors(() => {
|
|
3386
|
+
const db = openExistingDatabase();
|
|
3387
|
+
try {
|
|
3388
|
+
const allTagSet = collectTagSetFromEntries(db, undefined);
|
|
3389
|
+
const lessonTagSet = collectTagSetFromEntries(db, "lesson");
|
|
3390
|
+
const uncovered = [];
|
|
3391
|
+
for (const tag of allTagSet) {
|
|
3392
|
+
if (!lessonTagSet.has(tag))
|
|
3393
|
+
uncovered.push(tag);
|
|
3394
|
+
}
|
|
3395
|
+
uncovered.sort((a, b) => a.localeCompare(b));
|
|
3396
|
+
output("lessons-coverage", {
|
|
3397
|
+
ok: true,
|
|
3398
|
+
uncoveredTags: uncovered,
|
|
3399
|
+
lessonTagCount: lessonTagSet.size,
|
|
3400
|
+
totalTagCount: allTagSet.size,
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
finally {
|
|
3404
|
+
closeDatabase(db);
|
|
3405
|
+
}
|
|
3406
|
+
});
|
|
3407
|
+
},
|
|
3408
|
+
});
|
|
3409
|
+
/**
|
|
3410
|
+
* Walk indexed entries and collect a deduplicated set of tags. When
|
|
3411
|
+
* `entryType` is provided, only entries of that type contribute tags.
|
|
3412
|
+
*
|
|
3413
|
+
* Pure read; never mutates the DB. Used by `akm lessons coverage` (Phase 7A)
|
|
3414
|
+
* to compute the diff between all-asset tags and lesson tags.
|
|
3415
|
+
*/
|
|
3416
|
+
function collectTagSetFromEntries(db, entryType) {
|
|
3417
|
+
const tags = new Set();
|
|
3418
|
+
const stmt = entryType
|
|
3419
|
+
? db.prepare("SELECT entry_json FROM entries WHERE entry_type = ?")
|
|
3420
|
+
: db.prepare("SELECT entry_json FROM entries");
|
|
3421
|
+
const rows = (entryType ? stmt.all(entryType) : stmt.all());
|
|
3422
|
+
for (const row of rows) {
|
|
3423
|
+
let parsed;
|
|
3424
|
+
try {
|
|
3425
|
+
parsed = JSON.parse(row.entry_json);
|
|
3426
|
+
}
|
|
3427
|
+
catch {
|
|
3428
|
+
continue;
|
|
3429
|
+
}
|
|
3430
|
+
if (!Array.isArray(parsed.tags))
|
|
3431
|
+
continue;
|
|
3432
|
+
for (const tag of parsed.tags) {
|
|
3433
|
+
if (typeof tag === "string" && tag.trim().length > 0) {
|
|
3434
|
+
tags.add(tag.trim().toLowerCase());
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
return tags;
|
|
3439
|
+
}
|
|
3440
|
+
const lessonsCommand = defineCommand({
|
|
3441
|
+
meta: {
|
|
3442
|
+
name: "lessons",
|
|
3443
|
+
description: "Lesson-asset tooling: tag-coverage gaps, strength queries.",
|
|
3444
|
+
},
|
|
3445
|
+
subCommands: {
|
|
3446
|
+
coverage: lessonsCoverageCommand,
|
|
3447
|
+
},
|
|
3448
|
+
});
|
|
2509
3449
|
// ── proposal substrate (#225) ────────────────────────────────────────────────
|
|
2510
|
-
const
|
|
2511
|
-
meta: { name: "
|
|
3450
|
+
const proposalsCommand = defineCommand({
|
|
3451
|
+
meta: { name: "proposals", description: "List proposal queue entries" },
|
|
2512
3452
|
args: {
|
|
2513
|
-
status: {
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
type: "boolean",
|
|
2517
|
-
description: "Include accepted/rejected proposals from the archive",
|
|
2518
|
-
default: false,
|
|
3453
|
+
status: {
|
|
3454
|
+
type: "string",
|
|
3455
|
+
description: "Filter by status (pending|accepted|rejected|reverted)",
|
|
2519
3456
|
},
|
|
3457
|
+
ref: { type: "string", description: "Filter by asset ref (type:name)" },
|
|
3458
|
+
type: { type: "string", description: "Filter by asset type" },
|
|
2520
3459
|
},
|
|
2521
3460
|
run({ args }) {
|
|
2522
3461
|
return runWithJsonErrors(() => {
|
|
@@ -2524,54 +3463,203 @@ const proposalListCommand = defineCommand({
|
|
|
2524
3463
|
const result = akmProposalList({
|
|
2525
3464
|
status,
|
|
2526
3465
|
ref: args.ref,
|
|
2527
|
-
includeArchive:
|
|
3466
|
+
includeArchive: status === "accepted" || status === "rejected" || status === "reverted",
|
|
2528
3467
|
});
|
|
2529
3468
|
output("proposal-list", result);
|
|
2530
3469
|
});
|
|
2531
3470
|
},
|
|
2532
3471
|
});
|
|
2533
|
-
const
|
|
2534
|
-
meta: { name: "
|
|
2535
|
-
args: {
|
|
2536
|
-
id: { type: "positional", description: "Proposal id (uuid)", required: true },
|
|
2537
|
-
},
|
|
2538
|
-
run({ args }) {
|
|
2539
|
-
return runWithJsonErrors(() => {
|
|
2540
|
-
const result = akmProposalShow({ id: args.id });
|
|
2541
|
-
output("proposal-show", result);
|
|
2542
|
-
});
|
|
2543
|
-
},
|
|
2544
|
-
});
|
|
2545
|
-
const proposalAcceptCommand = defineCommand({
|
|
2546
|
-
meta: { name: "accept", description: "Validate and promote a proposal to a real asset" },
|
|
3472
|
+
const acceptCommand = defineCommand({
|
|
3473
|
+
meta: { name: "accept", description: "Accept a proposal and promote it into the stash" },
|
|
2547
3474
|
args: {
|
|
2548
|
-
id: {
|
|
3475
|
+
id: {
|
|
3476
|
+
type: "positional",
|
|
3477
|
+
description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream). Optional when --source is provided.",
|
|
3478
|
+
required: false,
|
|
3479
|
+
},
|
|
2549
3480
|
target: { type: "string", description: "Override the write target by source name" },
|
|
3481
|
+
// F-6 / #393: Batch accept by source, diff size, or age.
|
|
3482
|
+
source: {
|
|
3483
|
+
type: "string",
|
|
3484
|
+
description: "F-6: Bulk-accept all pending proposals from this source (e.g. reflect, distill). Requires no positional id.",
|
|
3485
|
+
},
|
|
3486
|
+
"max-diff-lines": {
|
|
3487
|
+
type: "string",
|
|
3488
|
+
description: "F-6: When bulk-accepting, only accept proposals whose content is <= this many lines. Skips larger proposals.",
|
|
3489
|
+
},
|
|
3490
|
+
"older-than": {
|
|
3491
|
+
type: "string",
|
|
3492
|
+
description: "F-6: When bulk-accepting, only accept proposals created more than this many days ago (e.g. '7' for 7 days).",
|
|
3493
|
+
},
|
|
3494
|
+
"dry-run": {
|
|
3495
|
+
type: "boolean",
|
|
3496
|
+
description: "F-6: List proposals that would be bulk-accepted without accepting them.",
|
|
3497
|
+
default: false,
|
|
3498
|
+
},
|
|
2550
3499
|
},
|
|
2551
3500
|
async run({ args }) {
|
|
2552
3501
|
await runWithJsonErrors(async () => {
|
|
3502
|
+
// F-6 / #393: Bulk-accept when --source is provided without a positional id.
|
|
3503
|
+
if (args.source && !args.id) {
|
|
3504
|
+
const { listProposals } = await import("./core/proposals");
|
|
3505
|
+
const stashDir = resolveStashDir();
|
|
3506
|
+
const rawMaxDiff = args["max-diff-lines"] ? Number.parseInt(String(args["max-diff-lines"]), 10) : undefined;
|
|
3507
|
+
if (rawMaxDiff !== undefined && (Number.isNaN(rawMaxDiff) || rawMaxDiff < 0)) {
|
|
3508
|
+
throw new UsageError("--max-diff-lines must be a non-negative integer", "INVALID_FLAG_VALUE");
|
|
3509
|
+
}
|
|
3510
|
+
const rawOlderThan = args["older-than"] ? Number.parseInt(String(args["older-than"]), 10) : undefined;
|
|
3511
|
+
if (rawOlderThan !== undefined && (Number.isNaN(rawOlderThan) || rawOlderThan < 0)) {
|
|
3512
|
+
throw new UsageError("--older-than must be a non-negative integer (days)", "INVALID_FLAG_VALUE");
|
|
3513
|
+
}
|
|
3514
|
+
const maxDiffLines = rawMaxDiff;
|
|
3515
|
+
const olderThanMs = rawOlderThan !== undefined ? rawOlderThan * 86_400_000 : undefined;
|
|
3516
|
+
const pending = listProposals(stashDir, { status: "pending" }).filter((p) => {
|
|
3517
|
+
if (p.source !== args.source)
|
|
3518
|
+
return false;
|
|
3519
|
+
if (maxDiffLines !== undefined) {
|
|
3520
|
+
const lines = (p.payload.content ?? "").split("\n").length;
|
|
3521
|
+
if (lines > maxDiffLines)
|
|
3522
|
+
return false;
|
|
3523
|
+
}
|
|
3524
|
+
if (olderThanMs !== undefined) {
|
|
3525
|
+
const age = Date.now() - new Date(p.createdAt).getTime();
|
|
3526
|
+
if (age < olderThanMs)
|
|
3527
|
+
return false;
|
|
3528
|
+
}
|
|
3529
|
+
return true;
|
|
3530
|
+
});
|
|
3531
|
+
const results = [];
|
|
3532
|
+
for (const proposal of pending) {
|
|
3533
|
+
if (args["dry-run"]) {
|
|
3534
|
+
results.push({ id: proposal.id, ref: proposal.ref, source: proposal.source, dryRun: true });
|
|
3535
|
+
}
|
|
3536
|
+
else {
|
|
3537
|
+
const result = await akmProposalAccept({ id: proposal.id, target: args.target });
|
|
3538
|
+
results.push(result);
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
output("proposal-accept-batch", { accepted: results.length, results, dryRun: args["dry-run"] });
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
if (!args.id) {
|
|
3545
|
+
throw new UsageError("Usage: akm accept <id> OR akm accept --source <source>", "MISSING_REQUIRED_ARGUMENT");
|
|
3546
|
+
}
|
|
2553
3547
|
const result = await akmProposalAccept({ id: args.id, target: args.target });
|
|
2554
3548
|
output("proposal-accept", result);
|
|
2555
3549
|
});
|
|
2556
3550
|
},
|
|
2557
3551
|
});
|
|
2558
|
-
const
|
|
2559
|
-
meta: { name: "reject", description: "
|
|
3552
|
+
const rejectCommand = defineCommand({
|
|
3553
|
+
meta: { name: "reject", description: "Reject a proposal and record the reason" },
|
|
2560
3554
|
args: {
|
|
2561
|
-
id: {
|
|
2562
|
-
|
|
3555
|
+
id: {
|
|
3556
|
+
type: "positional",
|
|
3557
|
+
description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream). Optional when --source is provided.",
|
|
3558
|
+
required: false,
|
|
3559
|
+
},
|
|
3560
|
+
reason: { type: "string", description: "Reason for rejection (required)" },
|
|
3561
|
+
// F-6 / #393: Batch reject by source, diff size, or age.
|
|
3562
|
+
source: {
|
|
3563
|
+
type: "string",
|
|
3564
|
+
description: "F-6: Bulk-reject all pending proposals from this source (e.g. reflect, distill). Requires no positional id.",
|
|
3565
|
+
},
|
|
3566
|
+
"max-diff-lines": {
|
|
3567
|
+
type: "string",
|
|
3568
|
+
description: "F-6: When bulk-rejecting, only reject proposals whose content is <= this many lines. Skips larger proposals.",
|
|
3569
|
+
},
|
|
3570
|
+
"older-than": {
|
|
3571
|
+
type: "string",
|
|
3572
|
+
description: "F-6: When bulk-rejecting, only reject proposals created more than this many days ago (e.g. '7' for 7 days).",
|
|
3573
|
+
},
|
|
3574
|
+
"dry-run": {
|
|
3575
|
+
type: "boolean",
|
|
3576
|
+
description: "F-6: List proposals that would be bulk-rejected without rejecting them.",
|
|
3577
|
+
default: false,
|
|
3578
|
+
},
|
|
3579
|
+
yes: {
|
|
3580
|
+
type: "boolean",
|
|
3581
|
+
alias: "y",
|
|
3582
|
+
description: "Skip confirmation prompt (required in non-interactive mode)",
|
|
3583
|
+
default: false,
|
|
3584
|
+
},
|
|
2563
3585
|
},
|
|
2564
3586
|
run({ args }) {
|
|
2565
|
-
return runWithJsonErrors(() => {
|
|
2566
|
-
|
|
2567
|
-
|
|
3587
|
+
return runWithJsonErrors(async () => {
|
|
3588
|
+
if (!args.reason || !String(args.reason).trim()) {
|
|
3589
|
+
throw new UsageError("Usage: akm reject <id> --reason '<reason>' OR akm reject --source <source> --reason '<reason>'", "MISSING_REQUIRED_ARGUMENT");
|
|
3590
|
+
}
|
|
3591
|
+
// F-6 / #393: Bulk-reject when --source is provided without a positional id.
|
|
3592
|
+
if (args.source && !args.id) {
|
|
3593
|
+
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
3594
|
+
const confirmed = await confirmDestructive(`Bulk-reject all matching proposals from source "${args.source}"? This cannot be undone.`, { yes: args.yes === true || args["dry-run"] === true });
|
|
3595
|
+
if (!confirmed) {
|
|
3596
|
+
process.stderr.write("Aborted.\n");
|
|
3597
|
+
return;
|
|
3598
|
+
}
|
|
3599
|
+
const { listProposals } = await import("./core/proposals");
|
|
3600
|
+
const stashDir = resolveStashDir();
|
|
3601
|
+
const rawMaxDiff = args["max-diff-lines"] ? Number.parseInt(String(args["max-diff-lines"]), 10) : undefined;
|
|
3602
|
+
if (rawMaxDiff !== undefined && (Number.isNaN(rawMaxDiff) || rawMaxDiff < 0)) {
|
|
3603
|
+
throw new UsageError("--max-diff-lines must be a non-negative integer", "INVALID_FLAG_VALUE");
|
|
3604
|
+
}
|
|
3605
|
+
const rawOlderThan = args["older-than"] ? Number.parseInt(String(args["older-than"]), 10) : undefined;
|
|
3606
|
+
if (rawOlderThan !== undefined && (Number.isNaN(rawOlderThan) || rawOlderThan < 0)) {
|
|
3607
|
+
throw new UsageError("--older-than must be a non-negative integer (days)", "INVALID_FLAG_VALUE");
|
|
3608
|
+
}
|
|
3609
|
+
const maxDiffLines = rawMaxDiff;
|
|
3610
|
+
const olderThanMs = rawOlderThan !== undefined ? rawOlderThan * 86_400_000 : undefined;
|
|
3611
|
+
const pending = listProposals(stashDir, { status: "pending" }).filter((p) => {
|
|
3612
|
+
if (p.source !== args.source)
|
|
3613
|
+
return false;
|
|
3614
|
+
if (maxDiffLines !== undefined) {
|
|
3615
|
+
const lines = (p.payload.content ?? "").split("\n").length;
|
|
3616
|
+
if (lines > maxDiffLines)
|
|
3617
|
+
return false;
|
|
3618
|
+
}
|
|
3619
|
+
if (olderThanMs !== undefined) {
|
|
3620
|
+
const age = Date.now() - new Date(p.createdAt).getTime();
|
|
3621
|
+
if (age < olderThanMs)
|
|
3622
|
+
return false;
|
|
3623
|
+
}
|
|
3624
|
+
return true;
|
|
3625
|
+
});
|
|
3626
|
+
const results = [];
|
|
3627
|
+
for (const proposal of pending) {
|
|
3628
|
+
if (args["dry-run"]) {
|
|
3629
|
+
results.push({ id: proposal.id, ref: proposal.ref, source: proposal.source, dryRun: true });
|
|
3630
|
+
}
|
|
3631
|
+
else {
|
|
3632
|
+
const result = akmProposalReject({ id: proposal.id, reason: String(args.reason) });
|
|
3633
|
+
results.push(result);
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
output("proposal-reject-batch", { rejected: results.length, results, dryRun: args["dry-run"] });
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
if (!args.id) {
|
|
3640
|
+
throw new UsageError("Usage: akm reject <id> --reason '<reason>' OR akm reject --source <source> --reason '<reason>'", "MISSING_REQUIRED_ARGUMENT");
|
|
3641
|
+
}
|
|
3642
|
+
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
3643
|
+
const confirmed = await confirmDestructive(`Reject proposal "${args.id}"? This cannot be undone.`, {
|
|
3644
|
+
yes: args.yes === true,
|
|
3645
|
+
});
|
|
3646
|
+
if (!confirmed) {
|
|
3647
|
+
process.stderr.write("Aborted.\n");
|
|
3648
|
+
return;
|
|
3649
|
+
}
|
|
3650
|
+
const result = akmProposalReject({ id: args.id, reason: String(args.reason) });
|
|
3651
|
+
output("proposal-reject", result);
|
|
2568
3652
|
});
|
|
2569
3653
|
},
|
|
2570
3654
|
});
|
|
2571
|
-
const
|
|
2572
|
-
meta: { name: "diff", description: "Show the diff
|
|
3655
|
+
const diffCommand = defineCommand({
|
|
3656
|
+
meta: { name: "diff", description: "Show the diff for a proposal (accepts full UUID, UUID prefix, or asset ref)" },
|
|
2573
3657
|
args: {
|
|
2574
|
-
id: {
|
|
3658
|
+
id: {
|
|
3659
|
+
type: "positional",
|
|
3660
|
+
description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream)",
|
|
3661
|
+
required: true,
|
|
3662
|
+
},
|
|
2575
3663
|
target: { type: "string", description: "Override the write target by source name" },
|
|
2576
3664
|
},
|
|
2577
3665
|
run({ args }) {
|
|
@@ -2581,152 +3669,327 @@ const proposalDiffCommand = defineCommand({
|
|
|
2581
3669
|
});
|
|
2582
3670
|
},
|
|
2583
3671
|
});
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
diff: proposalDiffCommand,
|
|
2595
|
-
},
|
|
2596
|
-
});
|
|
2597
|
-
// ── distill (#228) ──────────────────────────────────────────────────────────
|
|
2598
|
-
const distillCommand = defineCommand({
|
|
3672
|
+
// Phase 6C (Advantage D6c): revert an accepted proposal.
|
|
3673
|
+
//
|
|
3674
|
+
// Exit codes (mapped by `runWithJsonErrors` from the typed errors thrown by
|
|
3675
|
+
// `akmProposalRevert` / `revertProposal`):
|
|
3676
|
+
// 0 — success; prior content restored.
|
|
3677
|
+
// 1 — generic error (also used by `UsageError("INVALID_FLAG_VALUE")` and
|
|
3678
|
+
// `UsageError("MISSING_REQUIRED_ARGUMENT")` when the proposal is not
|
|
3679
|
+
// accepted, or no backup is available).
|
|
3680
|
+
// 1 — `NotFoundError("FILE_NOT_FOUND")` when the proposal id does not resolve.
|
|
3681
|
+
const revertCommand = defineCommand({
|
|
2599
3682
|
meta: {
|
|
2600
|
-
name: "
|
|
2601
|
-
description: "
|
|
3683
|
+
name: "revert",
|
|
3684
|
+
description: "Revert an accepted proposal: restore the prior asset content from the backup captured at promotion time. " +
|
|
3685
|
+
"Errors if the proposal is not accepted or has no backup (new-asset proposals leave no backup). " +
|
|
3686
|
+
"Accepts the full proposal UUID or the asset ref. UUID prefixes are not supported for archived proposals — use the full UUID.",
|
|
2602
3687
|
},
|
|
2603
3688
|
args: {
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
},
|
|
2609
|
-
"exclude-feedback-from": {
|
|
2610
|
-
type: "string",
|
|
2611
|
-
description: "Comma-separated asset refs whose feedback events MUST be filtered out before the LLM input is built. Falls back to AKM_DISTILL_EXCLUDE_FEEDBACK_FROM when omitted.",
|
|
2612
|
-
},
|
|
2613
|
-
"exclude-tags": {
|
|
2614
|
-
type: "string",
|
|
2615
|
-
description: "Exclude feedback events matching these tags (repeatable, e.g. --exclude-tags slice:eval)",
|
|
2616
|
-
},
|
|
2617
|
-
"include-tags": {
|
|
2618
|
-
type: "string",
|
|
2619
|
-
description: "Only include feedback events with ALL these tags (repeatable)",
|
|
3689
|
+
id: {
|
|
3690
|
+
type: "positional",
|
|
3691
|
+
description: "Proposal id (full uuid) or asset ref (e.g. skill:akm-dream). UUID prefixes are not supported for archived proposals — use the full UUID.",
|
|
3692
|
+
required: true,
|
|
2620
3693
|
},
|
|
3694
|
+
target: { type: "string", description: "Override the write target by source name" },
|
|
2621
3695
|
},
|
|
2622
3696
|
async run({ args }) {
|
|
2623
3697
|
await runWithJsonErrors(async () => {
|
|
2624
|
-
const
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
const excludeRaw = excludeFlag ?? excludeEnv;
|
|
2628
|
-
const excludeFeedbackFromRefs = parseExcludeFeedbackFromRefs(excludeRaw);
|
|
2629
|
-
const excludeTagsRaw = parseAllFlagValues("--exclude-tags");
|
|
2630
|
-
const excludeTagsEnv = process.env.AKM_DISTILL_EXCLUDE_TAGS;
|
|
2631
|
-
const excludeTags = [
|
|
2632
|
-
...new Set([
|
|
2633
|
-
...excludeTagsRaw,
|
|
2634
|
-
...(excludeTagsEnv
|
|
2635
|
-
? excludeTagsEnv
|
|
2636
|
-
.split(",")
|
|
2637
|
-
.map((s) => s.trim())
|
|
2638
|
-
.filter(Boolean)
|
|
2639
|
-
: []),
|
|
2640
|
-
]),
|
|
2641
|
-
];
|
|
2642
|
-
const includeTagsRaw = parseAllFlagValues("--include-tags");
|
|
2643
|
-
const includeTagsEnv = process.env.AKM_DISTILL_INCLUDE_TAGS;
|
|
2644
|
-
const includeTags = [
|
|
2645
|
-
...new Set([
|
|
2646
|
-
...includeTagsRaw,
|
|
2647
|
-
...(includeTagsEnv
|
|
2648
|
-
? includeTagsEnv
|
|
2649
|
-
.split(",")
|
|
2650
|
-
.map((s) => s.trim())
|
|
2651
|
-
.filter(Boolean)
|
|
2652
|
-
: []),
|
|
2653
|
-
]),
|
|
2654
|
-
];
|
|
2655
|
-
const result = await akmDistill({
|
|
2656
|
-
ref: args.ref,
|
|
2657
|
-
sourceRun: getHyphenatedArg(args, "source-run"),
|
|
2658
|
-
...(excludeFeedbackFromRefs.length > 0 ? { excludeFeedbackFromRefs } : {}),
|
|
2659
|
-
...(excludeTags.length > 0 ? { excludeTags } : {}),
|
|
2660
|
-
...(includeTags.length > 0 ? { includeTags } : {}),
|
|
3698
|
+
const result = await akmProposalRevert({
|
|
3699
|
+
id: args.id,
|
|
3700
|
+
target: args.target,
|
|
2661
3701
|
});
|
|
2662
|
-
output("
|
|
3702
|
+
output("proposal-revert", result);
|
|
2663
3703
|
});
|
|
2664
3704
|
},
|
|
2665
3705
|
});
|
|
2666
|
-
|
|
2667
|
-
* Parse a comma-separated list of asset refs (#267 — `--exclude-feedback-from`
|
|
2668
|
-
* and `AKM_DISTILL_EXCLUDE_FEEDBACK_FROM`). Each entry is validated against
|
|
2669
|
-
* the canonical `[origin//]type:name` grammar via `parseAssetRef`; an
|
|
2670
|
-
* invalid entry surfaces as a UsageError → exit 2.
|
|
2671
|
-
*/
|
|
2672
|
-
function parseExcludeFeedbackFromRefs(raw) {
|
|
2673
|
-
if (raw === undefined || raw.trim() === "")
|
|
2674
|
-
return [];
|
|
2675
|
-
const refs = raw
|
|
2676
|
-
.split(",")
|
|
2677
|
-
.map((part) => part.trim())
|
|
2678
|
-
.filter((part) => part.length > 0);
|
|
2679
|
-
for (const ref of refs) {
|
|
2680
|
-
try {
|
|
2681
|
-
parseAssetRef(ref);
|
|
2682
|
-
}
|
|
2683
|
-
catch (err) {
|
|
2684
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2685
|
-
throw new UsageError(`Invalid --exclude-feedback-from ref "${ref}": ${message}`, "INVALID_FLAG_VALUE", "Each ref must match `[origin//]type:name`, e.g. skill:deploy or team//memory:auth-tips.");
|
|
2686
|
-
}
|
|
2687
|
-
}
|
|
2688
|
-
return refs;
|
|
2689
|
-
}
|
|
3706
|
+
// ── distill (#228) ──────────────────────────────────────────────────────────
|
|
2690
3707
|
function parseProposalStatus(raw) {
|
|
2691
3708
|
if (raw === undefined)
|
|
2692
3709
|
return undefined;
|
|
2693
3710
|
const trimmed = raw.trim();
|
|
2694
3711
|
if (!trimmed)
|
|
2695
3712
|
return undefined;
|
|
2696
|
-
if (trimmed === "pending" || trimmed === "accepted" || trimmed === "rejected")
|
|
3713
|
+
if (trimmed === "pending" || trimmed === "accepted" || trimmed === "rejected" || trimmed === "reverted") {
|
|
2697
3714
|
return trimmed;
|
|
2698
|
-
|
|
3715
|
+
}
|
|
3716
|
+
throw new UsageError(`Invalid --status value: "${raw}". Expected one of: pending, accepted, rejected, reverted.`, "INVALID_FLAG_VALUE");
|
|
2699
3717
|
}
|
|
2700
|
-
|
|
2701
|
-
const reflectCommand = defineCommand({
|
|
3718
|
+
const agentCommand = defineCommand({
|
|
2702
3719
|
meta: {
|
|
2703
|
-
name: "
|
|
2704
|
-
description: "
|
|
3720
|
+
name: "agent",
|
|
3721
|
+
description: "Dispatch an agent CLI (opencode, claude, …) with an optional agent asset that provides the system prompt, model, and tool policy. Use <agent-ref> to embody a stash agent, --model to override the model, and --prompt/--command/--workflow to provide the task.",
|
|
2705
3722
|
},
|
|
2706
3723
|
args: {
|
|
2707
|
-
|
|
3724
|
+
profile: {
|
|
2708
3725
|
type: "positional",
|
|
2709
|
-
description: "
|
|
3726
|
+
description: "Agent profile / platform to use (opencode, claude, …)",
|
|
2710
3727
|
required: false,
|
|
2711
3728
|
},
|
|
2712
|
-
|
|
2713
|
-
|
|
3729
|
+
"agent-ref": {
|
|
3730
|
+
type: "positional",
|
|
3731
|
+
description: "Optional agent asset ref (e.g. agent:code-reviewer). Loads system prompt, model, and tool policy from the stash asset.",
|
|
3732
|
+
required: false,
|
|
3733
|
+
},
|
|
3734
|
+
prompt: { type: "string", description: "Task prompt to pass to the agent" },
|
|
3735
|
+
command: { type: "string", description: "Load prompt from a command: asset" },
|
|
3736
|
+
workflow: { type: "string", description: "Load prompt from a workflow: asset" },
|
|
3737
|
+
model: {
|
|
3738
|
+
type: "string",
|
|
3739
|
+
description: "Model override — accepts aliases (opus, sonnet, haiku) or exact platform model IDs. Overrides the model specified in the agent asset.",
|
|
3740
|
+
},
|
|
2714
3741
|
"timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds" },
|
|
2715
3742
|
},
|
|
2716
3743
|
async run({ args }) {
|
|
2717
3744
|
await runWithJsonErrors(async () => {
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
3745
|
+
if (!args.profile) {
|
|
3746
|
+
throw new UsageError("Usage: akm agent <profile> [<agent-ref>] [--prompt <text>] [--model <model>]", "MISSING_REQUIRED_ARGUMENT", "Provide the agent profile name. Available profiles are listed in profiles.agent.");
|
|
3747
|
+
}
|
|
3748
|
+
const timeoutMs = parsePositiveIntFlag(getHyphenatedArg(args, "timeout-ms"), "--timeout-ms");
|
|
3749
|
+
const config = loadConfig();
|
|
3750
|
+
const { getDefaultLlmConfig } = await import("./core/config.js");
|
|
3751
|
+
// After 0.8.0 the agent block IS the loaded AkmConfig.
|
|
3752
|
+
const agentConfig = config;
|
|
3753
|
+
// Resolve agent asset ref → extract system prompt, model, and tool policy.
|
|
3754
|
+
const agentRef = getStringArg(args, "agent-ref");
|
|
3755
|
+
let systemPrompt;
|
|
3756
|
+
let assetModel;
|
|
3757
|
+
let assetTools;
|
|
3758
|
+
if (agentRef) {
|
|
3759
|
+
const { akmShowUnified } = await import("./commands/show.js");
|
|
3760
|
+
const asset = await akmShowUnified({ ref: agentRef, detail: "full" });
|
|
3761
|
+
systemPrompt = typeof asset.content === "string" ? asset.content : undefined;
|
|
3762
|
+
assetModel = typeof asset.modelHint === "string" ? asset.modelHint : undefined;
|
|
3763
|
+
assetTools = asset.toolPolicy;
|
|
3764
|
+
}
|
|
3765
|
+
// --model flag wins over the asset's modelHint.
|
|
3766
|
+
const model = getStringArg(args, "model") ?? assetModel;
|
|
3767
|
+
const promptText = getStringArg(args, "prompt");
|
|
3768
|
+
const commandRef = getStringArg(args, "command");
|
|
3769
|
+
const workflowRef = getStringArg(args, "workflow");
|
|
3770
|
+
// Only build a dispatch request when there is something to dispatch — a
|
|
3771
|
+
// prompt, an agent asset, or a model override. When none of these are
|
|
3772
|
+
// present the agent is launched interactively (no injected prompt, no
|
|
3773
|
+
// platform-specific flags beyond the profile's base args).
|
|
3774
|
+
const hasDispatchContent = !!(promptText ?? commandRef ?? workflowRef ?? systemPrompt ?? model ?? assetTools);
|
|
3775
|
+
const result = await akmAgentDispatch({
|
|
3776
|
+
profileName: String(args.profile),
|
|
3777
|
+
prompt: promptText,
|
|
3778
|
+
commandRef,
|
|
3779
|
+
workflowRef,
|
|
3780
|
+
agentConfig,
|
|
3781
|
+
llmConfig: getDefaultLlmConfig(config),
|
|
3782
|
+
...(hasDispatchContent
|
|
3783
|
+
? {
|
|
3784
|
+
dispatch: {
|
|
3785
|
+
prompt: promptText ?? "",
|
|
3786
|
+
systemPrompt,
|
|
3787
|
+
model,
|
|
3788
|
+
tools: assetTools,
|
|
3789
|
+
},
|
|
3790
|
+
}
|
|
3791
|
+
: {}),
|
|
2724
3792
|
...(timeoutMs !== undefined && Number.isFinite(timeoutMs) ? { timeoutMs } : {}),
|
|
2725
3793
|
});
|
|
2726
|
-
output("
|
|
2727
|
-
if (result.ok
|
|
3794
|
+
output("agent-result", result);
|
|
3795
|
+
if (!result.ok) {
|
|
3796
|
+
process.exit(EXIT_GENERAL);
|
|
3797
|
+
}
|
|
3798
|
+
});
|
|
3799
|
+
},
|
|
3800
|
+
});
|
|
3801
|
+
const lintCommand = defineCommand({
|
|
3802
|
+
meta: {
|
|
3803
|
+
name: "lint",
|
|
3804
|
+
description: "Scan stash .md files for structural issues (unquoted colons, missing updated field, orphaned stubs, placeholder stubs, missing name/type, stale paths). Use --fix to auto-fix Tier 1 issues.",
|
|
3805
|
+
},
|
|
3806
|
+
args: {
|
|
3807
|
+
fix: { type: "boolean", description: "Apply auto-fixes in place", default: false },
|
|
3808
|
+
dir: { type: "string", description: "Override stash root directory (default: from config)" },
|
|
3809
|
+
},
|
|
3810
|
+
async run({ args }) {
|
|
3811
|
+
await runWithJsonErrors(async () => {
|
|
3812
|
+
const result = akmLint({
|
|
3813
|
+
fix: args.fix ?? false,
|
|
3814
|
+
dir: getStringArg(args, "dir"),
|
|
3815
|
+
});
|
|
3816
|
+
output("lint", result);
|
|
3817
|
+
if (!result.ok)
|
|
2728
3818
|
process.exit(EXIT_GENERAL);
|
|
3819
|
+
});
|
|
3820
|
+
},
|
|
3821
|
+
});
|
|
3822
|
+
const improveCommand = defineCommand({
|
|
3823
|
+
meta: {
|
|
3824
|
+
name: "improve",
|
|
3825
|
+
description: "Analyze existing AKM assets and generate improvement proposals; also consolidates memories when profiles.improve.default.processes.consolidate.enabled is true",
|
|
3826
|
+
},
|
|
3827
|
+
args: {
|
|
3828
|
+
scope: {
|
|
3829
|
+
type: "positional",
|
|
3830
|
+
description: "Optional asset type or asset ref to improve",
|
|
3831
|
+
required: false,
|
|
3832
|
+
},
|
|
3833
|
+
task: { type: "string", description: "Add extra guidance for this improvement pass" },
|
|
3834
|
+
"dry-run": { type: "boolean", description: "Show planned actions without writing", default: false },
|
|
3835
|
+
target: { type: "string", description: "Override the write target for accepted proposals" },
|
|
3836
|
+
"auto-accept": {
|
|
3837
|
+
type: "string",
|
|
3838
|
+
description: "Auto-accept proposals at or above this confidence threshold (0-100). Default: disabled. Pass a value 0-100 to enable. 'safe' is an alias for 90. Pass 'false' to be explicit.",
|
|
3839
|
+
},
|
|
3840
|
+
limit: { type: "string", description: "Maximum number of assets to process (highest utility first)" },
|
|
3841
|
+
"timeout-ms": {
|
|
3842
|
+
type: "string",
|
|
3843
|
+
description: "Wall-clock budget for the entire run in milliseconds (default: 7200000 = 2 hours)",
|
|
3844
|
+
},
|
|
3845
|
+
"ignore-cooldown": {
|
|
3846
|
+
type: "boolean",
|
|
3847
|
+
description: "Ignore all cooldown periods (equivalent to --reflect-cooldown-days 0 --distill-cooldown-days 0 --consolidate-cooldown-days 0)",
|
|
3848
|
+
default: false,
|
|
3849
|
+
},
|
|
3850
|
+
"reflect-cooldown-days": {
|
|
3851
|
+
type: "string",
|
|
3852
|
+
description: "Override reflect cooldown for this run only, applying uniformly to all asset types. Per-type defaults (memory=2d, lesson=7d, workflow/skill/agent/command/knowledge/script/wiki=30d, task=60d) can be configured via profiles.improve.<name>.processes.reflect.cooldownByType. Set 0 to disable.",
|
|
3853
|
+
},
|
|
3854
|
+
"distill-cooldown-days": {
|
|
3855
|
+
type: "string",
|
|
3856
|
+
description: "Override distill cooldown for this run only (default: 1, 0 to disable)",
|
|
3857
|
+
},
|
|
3858
|
+
"consolidate-cooldown-days": {
|
|
3859
|
+
type: "string",
|
|
3860
|
+
description: "Override consolidate cooldown for this run only (default: 14, 0 to disable)",
|
|
3861
|
+
},
|
|
3862
|
+
"consolidate-recovery": {
|
|
3863
|
+
type: "string",
|
|
3864
|
+
description: "How to handle stale/incomplete consolidation journals: abort (default) or clean (remove stale journal artifacts)",
|
|
3865
|
+
},
|
|
3866
|
+
"require-feedback-signal": {
|
|
3867
|
+
type: "boolean",
|
|
3868
|
+
description: "Only process assets with recent feedback signals (disables retrieval fallback)",
|
|
3869
|
+
default: false,
|
|
3870
|
+
},
|
|
3871
|
+
"min-retrieval-count": {
|
|
3872
|
+
type: "string",
|
|
3873
|
+
description: "Minimum retrieval count for zero-feedback fallback eligibility (default: 1, set 0 to include all assets regardless of retrieval history)",
|
|
3874
|
+
},
|
|
3875
|
+
"json-to-stdout": {
|
|
3876
|
+
type: "boolean",
|
|
3877
|
+
description: "Emit the full JSON result on stdout (legacy behaviour). (0.8.0+: full result is recorded in the improve_runs table of state.db and stdout is empty; use this flag for the prior behaviour, e.g. `akm improve --json-to-stdout | jq`.)",
|
|
3878
|
+
default: false,
|
|
3879
|
+
},
|
|
3880
|
+
profile: {
|
|
3881
|
+
type: "string",
|
|
3882
|
+
description: "Named improve profile from profiles.improve or built-in profiles (default, quick, thorough, memory-focus). Controls which sub-processes run and which asset types are processed.",
|
|
3883
|
+
},
|
|
3884
|
+
},
|
|
3885
|
+
async run({ args }) {
|
|
3886
|
+
await runWithJsonErrors(async () => {
|
|
3887
|
+
const formatFlagValue = parseFlagValue(process.argv, "--format");
|
|
3888
|
+
if (formatFlagValue !== undefined) {
|
|
3889
|
+
throw new UsageError(`akm improve does not accept --format. That flag controls output formatting for other commands (search, show, etc.).\n` +
|
|
3890
|
+
`Did you mean: akm improve (no --format flag)?`, "INVALID_FLAG_VALUE");
|
|
3891
|
+
}
|
|
3892
|
+
const jsonToStdout = getHyphenatedBoolean(args, "json-to-stdout");
|
|
3893
|
+
const autoAcceptRaw = getHyphenatedArg(args, "auto-accept");
|
|
3894
|
+
const autoAccept = parseAutoAcceptFlag(autoAcceptRaw);
|
|
3895
|
+
const targetArg = getStringArg(args, "target");
|
|
3896
|
+
const taskArg = getStringArg(args, "task");
|
|
3897
|
+
const dryRun = getHyphenatedBoolean(args, "dry-run");
|
|
3898
|
+
const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
|
|
3899
|
+
const timeoutMs = parsePositiveIntFlag(getHyphenatedArg(args, "timeout-ms"), "--timeout-ms");
|
|
3900
|
+
const ignoreCooldown = getHyphenatedBoolean(args, "ignore-cooldown");
|
|
3901
|
+
const reflectCooldownRaw = getHyphenatedArg(args, "reflect-cooldown-days");
|
|
3902
|
+
const reflectCooldownDays = ignoreCooldown
|
|
3903
|
+
? 0
|
|
3904
|
+
: parseNonNegativeIntFlag(reflectCooldownRaw, "--reflect-cooldown-days");
|
|
3905
|
+
const distillCooldownRaw = getHyphenatedArg(args, "distill-cooldown-days");
|
|
3906
|
+
const distillCooldownDays = ignoreCooldown
|
|
3907
|
+
? 0
|
|
3908
|
+
: parseNonNegativeIntFlag(distillCooldownRaw, "--distill-cooldown-days");
|
|
3909
|
+
const consolidateCooldownRaw = getHyphenatedArg(args, "consolidate-cooldown-days");
|
|
3910
|
+
const consolidateCooldownDays = ignoreCooldown
|
|
3911
|
+
? 0
|
|
3912
|
+
: parseNonNegativeIntFlag(consolidateCooldownRaw, "--consolidate-cooldown-days");
|
|
3913
|
+
const consolidateRecoveryRaw = getHyphenatedArg(args, "consolidate-recovery");
|
|
3914
|
+
const consolidateRecovery = consolidateRecoveryRaw === undefined
|
|
3915
|
+
? undefined
|
|
3916
|
+
: consolidateRecoveryRaw.trim().toLowerCase();
|
|
3917
|
+
if (consolidateRecovery !== undefined && consolidateRecovery !== "abort" && consolidateRecovery !== "clean") {
|
|
3918
|
+
throw new UsageError(`Invalid --consolidate-recovery value: "${consolidateRecoveryRaw}". Must be one of: abort, clean.`, "INVALID_FLAG_VALUE");
|
|
3919
|
+
}
|
|
3920
|
+
const minRetrievalCountRaw = getHyphenatedArg(args, "min-retrieval-count");
|
|
3921
|
+
const minRetrievalCount = parseNonNegativeIntFlag(minRetrievalCountRaw, "--min-retrieval-count");
|
|
3922
|
+
const requireFeedbackSignal = getHyphenatedBoolean(args, "require-feedback-signal");
|
|
3923
|
+
const profileArg = getStringArg(args, "profile");
|
|
3924
|
+
const improveLogFile = path.join(getCacheDir(), "logs", "improve", `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
|
|
3925
|
+
setLogFile(improveLogFile);
|
|
3926
|
+
const startedAtMs = Date.now();
|
|
3927
|
+
let improveResult;
|
|
3928
|
+
try {
|
|
3929
|
+
improveResult = await akmImprove({
|
|
3930
|
+
scope: getStringArg(args, "scope"),
|
|
3931
|
+
task: taskArg,
|
|
3932
|
+
dryRun,
|
|
3933
|
+
target: targetArg,
|
|
3934
|
+
autoAccept,
|
|
3935
|
+
...(limitRaw !== undefined ? { limit: limitRaw } : {}),
|
|
3936
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
3937
|
+
...(reflectCooldownDays !== undefined ? { reflectCooldownDays } : {}),
|
|
3938
|
+
...(distillCooldownDays !== undefined ? { distillCooldownDays } : {}),
|
|
3939
|
+
...(consolidateCooldownDays !== undefined ? { consolidateCooldownDays } : {}),
|
|
3940
|
+
...(minRetrievalCount !== undefined ? { minRetrievalCount } : {}),
|
|
3941
|
+
...(requireFeedbackSignal ? { requireFeedbackSignal } : {}),
|
|
3942
|
+
...(profileArg !== undefined ? { profile: profileArg } : {}),
|
|
3943
|
+
consolidateOptions: {
|
|
3944
|
+
target: targetArg,
|
|
3945
|
+
dryRun,
|
|
3946
|
+
autoAccept,
|
|
3947
|
+
task: taskArg,
|
|
3948
|
+
...(consolidateRecovery !== undefined ? { recoveryMode: consolidateRecovery } : {}),
|
|
3949
|
+
},
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
finally {
|
|
3953
|
+
clearLogFile();
|
|
2729
3954
|
}
|
|
3955
|
+
const durationMs = Date.now() - startedAtMs;
|
|
3956
|
+
if (jsonToStdout) {
|
|
3957
|
+
// Legacy / escape-hatch mode: full JSON on stdout, no file write.
|
|
3958
|
+
// Kept for scripts/agents that already pipe to jq.
|
|
3959
|
+
output("improve", improveResult);
|
|
3960
|
+
process.exit(0);
|
|
3961
|
+
}
|
|
3962
|
+
// Default mode (0.8.0+): persist the full result as a row in the
|
|
3963
|
+
// `improve_runs` table of state.db (migration 003) and emit NOTHING
|
|
3964
|
+
// on stdout. The verbose JSON would otherwise scroll earlier progress
|
|
3965
|
+
// logs out of the terminal buffer. The existing `[improve] ...`
|
|
3966
|
+
// progress log lines on stderr remain the canonical console UX —
|
|
3967
|
+
// do NOT add any new console output here.
|
|
3968
|
+
//
|
|
3969
|
+
// Pre-0.8.0 wrote `<stash>/.akm/runs/<run-id>/improve-result.json`;
|
|
3970
|
+
// those files are no longer authored. Query recent runs with:
|
|
3971
|
+
// sqlite3 "$AKM_DATA_DIR/state.db" \
|
|
3972
|
+
// "SELECT id, started_at, ok, dry_run FROM improve_runs \
|
|
3973
|
+
// ORDER BY started_at DESC LIMIT 10"
|
|
3974
|
+
const runId = buildImproveRunId();
|
|
3975
|
+
const primaryStashDir = resolveSourceEntries(undefined, loadConfig())[0]?.path;
|
|
3976
|
+
const resultRef = relativeImproveResultPath(runId);
|
|
3977
|
+
if (primaryStashDir) {
|
|
3978
|
+
try {
|
|
3979
|
+
writeImproveResultFile(primaryStashDir, runId, improveResult);
|
|
3980
|
+
}
|
|
3981
|
+
catch (err) {
|
|
3982
|
+
// Stderr warning on the failure path is preferable to crashing
|
|
3983
|
+
// the run after all the work has completed.
|
|
3984
|
+
process.stderr.write(`warning: failed to record improve run ${resultRef}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
else {
|
|
3988
|
+
process.stderr.write(`warning: no writable stash directory resolved; improve result not persisted to state.db (use --json-to-stdout to capture)\n`);
|
|
3989
|
+
}
|
|
3990
|
+
// durationMs reserved for future use (no console emission today).
|
|
3991
|
+
void durationMs;
|
|
3992
|
+
process.exit(0);
|
|
2730
3993
|
});
|
|
2731
3994
|
},
|
|
2732
3995
|
});
|
|
@@ -2742,6 +4005,7 @@ const proposeCommand = defineCommand({
|
|
|
2742
4005
|
type: { type: "positional", description: "Asset type (skill, command, knowledge, lesson, ...)", required: false },
|
|
2743
4006
|
name: { type: "positional", description: "Asset name (slug or path under the type dir)", required: false },
|
|
2744
4007
|
task: { type: "string", description: "Task description for the agent (what should the asset do?)" },
|
|
4008
|
+
file: { type: "string", description: "Read the task or prompt text from a UTF-8 file" },
|
|
2745
4009
|
profile: { type: "string", description: "Override the agent profile (defaults to agent.default)" },
|
|
2746
4010
|
"timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds" },
|
|
2747
4011
|
},
|
|
@@ -2750,17 +4014,22 @@ const proposeCommand = defineCommand({
|
|
|
2750
4014
|
// citty silently shows help and exits 0 when required positionals are
|
|
2751
4015
|
// omitted. Re-validate explicitly so the exit code is 2 (USAGE) and a
|
|
2752
4016
|
// structured JSON error reaches scripted callers.
|
|
2753
|
-
|
|
2754
|
-
|
|
4017
|
+
const taskFromFlag = typeof args.task === "string" ? args.task : undefined;
|
|
4018
|
+
const fileFromFlag = typeof args.file === "string" ? args.file : undefined;
|
|
4019
|
+
if (!args.type || !args.name || (!taskFromFlag && !fileFromFlag)) {
|
|
4020
|
+
throw new UsageError("Usage: akm propose <type> <name> (--task '<task>' | --file <path>).", "MISSING_REQUIRED_ARGUMENT", "Provide the asset type, name, and exactly one of --task or --file.");
|
|
4021
|
+
}
|
|
4022
|
+
if (taskFromFlag && fileFromFlag) {
|
|
4023
|
+
throw new UsageError("Pass exactly one of --task or --file.", "INVALID_FLAG_VALUE");
|
|
2755
4024
|
}
|
|
2756
|
-
const
|
|
2757
|
-
const timeoutMs =
|
|
4025
|
+
const taskText = fileFromFlag ? fs.readFileSync(path.resolve(fileFromFlag), "utf8") : (taskFromFlag ?? "");
|
|
4026
|
+
const timeoutMs = parsePositiveIntFlag(getHyphenatedArg(args, "timeout-ms"), "--timeout-ms");
|
|
2758
4027
|
const result = await akmPropose({
|
|
2759
4028
|
type: String(args.type),
|
|
2760
4029
|
name: String(args.name),
|
|
2761
|
-
task:
|
|
2762
|
-
profile:
|
|
2763
|
-
...(timeoutMs !== undefined
|
|
4030
|
+
task: taskText,
|
|
4031
|
+
profile: getStringArg(args, "profile"),
|
|
4032
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
2764
4033
|
});
|
|
2765
4034
|
output("propose", result);
|
|
2766
4035
|
if (result.ok === false) {
|
|
@@ -2769,6 +4038,207 @@ const proposeCommand = defineCommand({
|
|
|
2769
4038
|
});
|
|
2770
4039
|
},
|
|
2771
4040
|
});
|
|
4041
|
+
const TASKS_SUBCOMMAND_SET = new Set([
|
|
4042
|
+
"add",
|
|
4043
|
+
"list",
|
|
4044
|
+
"show",
|
|
4045
|
+
"remove",
|
|
4046
|
+
"enable",
|
|
4047
|
+
"disable",
|
|
4048
|
+
"run",
|
|
4049
|
+
"history",
|
|
4050
|
+
"sync",
|
|
4051
|
+
"doctor",
|
|
4052
|
+
]);
|
|
4053
|
+
const GRAPH_SUBCOMMAND_SET = new Set([
|
|
4054
|
+
"summary",
|
|
4055
|
+
"entities",
|
|
4056
|
+
"entity",
|
|
4057
|
+
"relations",
|
|
4058
|
+
"related",
|
|
4059
|
+
"orphans",
|
|
4060
|
+
"export",
|
|
4061
|
+
"update",
|
|
4062
|
+
]);
|
|
4063
|
+
const tasksAddCommand = defineCommand({
|
|
4064
|
+
meta: { name: "add", description: "Register a new scheduled task and install it in the OS scheduler" },
|
|
4065
|
+
args: {
|
|
4066
|
+
id: { type: "positional", description: "Task id (used as filename and scheduler entry)", required: true },
|
|
4067
|
+
schedule: { type: "string", description: 'Cron-style schedule, e.g. "0 9 * * *" or "@daily"', required: true },
|
|
4068
|
+
workflow: { type: "string", description: "Workflow ref to invoke (e.g. workflow:my-flow)" },
|
|
4069
|
+
prompt: {
|
|
4070
|
+
type: "string",
|
|
4071
|
+
description: "Prompt for the configured agent harness — inline text, an asset ref like agent:foo, or ./path.md",
|
|
4072
|
+
},
|
|
4073
|
+
command: {
|
|
4074
|
+
type: "string",
|
|
4075
|
+
description: 'Shell command to run on the schedule (no AI agent), e.g. "akm improve --auto-accept safe". Split on whitespace; quote the whole flag value.',
|
|
4076
|
+
},
|
|
4077
|
+
profile: { type: "string", description: "Agent profile to use for prompt targets (default: defaults.agent)" },
|
|
4078
|
+
params: { type: "string", description: "Workflow params as a JSON object" },
|
|
4079
|
+
name: { type: "string", description: "Human-readable name for the task" },
|
|
4080
|
+
"when-to-use": { type: "string", description: "Guidance on when this task runs or should be used" },
|
|
4081
|
+
description: { type: "string", description: "Human-readable description" },
|
|
4082
|
+
tags: { type: "string", description: "Comma-separated tags" },
|
|
4083
|
+
disabled: { type: "boolean", description: "Register but leave disabled in the OS scheduler", default: false },
|
|
4084
|
+
force: { type: "boolean", description: "Overwrite an existing task with the same id", default: false },
|
|
4085
|
+
},
|
|
4086
|
+
async run({ args }) {
|
|
4087
|
+
await runWithJsonErrors(async () => {
|
|
4088
|
+
const result = await akmTasksAdd({
|
|
4089
|
+
id: args.id,
|
|
4090
|
+
schedule: args.schedule,
|
|
4091
|
+
workflow: args.workflow,
|
|
4092
|
+
prompt: args.prompt,
|
|
4093
|
+
command: args.command,
|
|
4094
|
+
profile: args.profile,
|
|
4095
|
+
params: args.params,
|
|
4096
|
+
name: args.name,
|
|
4097
|
+
when_to_use: getHyphenatedArg(args, "when-to-use"),
|
|
4098
|
+
description: args.description,
|
|
4099
|
+
tags: args.tags
|
|
4100
|
+
? args.tags
|
|
4101
|
+
.split(/[\s,]+/)
|
|
4102
|
+
.map((s) => s.trim())
|
|
4103
|
+
.filter(Boolean)
|
|
4104
|
+
: undefined,
|
|
4105
|
+
disabled: args.disabled === true,
|
|
4106
|
+
force: args.force === true,
|
|
4107
|
+
});
|
|
4108
|
+
output("tasks-add", result);
|
|
4109
|
+
});
|
|
4110
|
+
},
|
|
4111
|
+
});
|
|
4112
|
+
const tasksListCommand = defineCommand({
|
|
4113
|
+
meta: { name: "list", description: "List scheduled tasks in the stash" },
|
|
4114
|
+
async run() {
|
|
4115
|
+
await runWithJsonErrors(async () => {
|
|
4116
|
+
const result = await akmTasksList();
|
|
4117
|
+
output("tasks-list", result);
|
|
4118
|
+
});
|
|
4119
|
+
},
|
|
4120
|
+
});
|
|
4121
|
+
const tasksShowCommand = defineCommand({
|
|
4122
|
+
meta: { name: "show", description: "Show a parsed task definition" },
|
|
4123
|
+
args: { id: { type: "positional", description: "Task id or task:<id>", required: true } },
|
|
4124
|
+
async run({ args }) {
|
|
4125
|
+
await runWithJsonErrors(async () => {
|
|
4126
|
+
const { id } = parseTaskRef(args.id);
|
|
4127
|
+
const result = await akmTasksShow(id);
|
|
4128
|
+
output("tasks-show", result);
|
|
4129
|
+
});
|
|
4130
|
+
},
|
|
4131
|
+
});
|
|
4132
|
+
const tasksRemoveCommand = defineCommand({
|
|
4133
|
+
meta: { name: "remove", description: "Delete a task file and uninstall it from the OS scheduler" },
|
|
4134
|
+
args: { id: { type: "positional", description: "Task id", required: true } },
|
|
4135
|
+
async run({ args }) {
|
|
4136
|
+
await runWithJsonErrors(async () => {
|
|
4137
|
+
const { id } = parseTaskRef(args.id);
|
|
4138
|
+
const result = await akmTasksRemove(id);
|
|
4139
|
+
output("tasks-remove", result);
|
|
4140
|
+
});
|
|
4141
|
+
},
|
|
4142
|
+
});
|
|
4143
|
+
function makeTasksToggleCommand(enabled) {
|
|
4144
|
+
const verb = enabled ? "enable" : "disable";
|
|
4145
|
+
const description = enabled
|
|
4146
|
+
? "Enable a previously-disabled task"
|
|
4147
|
+
: "Disable a task in the OS scheduler without removing the file";
|
|
4148
|
+
return defineCommand({
|
|
4149
|
+
meta: { name: verb, description },
|
|
4150
|
+
args: { id: { type: "positional", description: "Task id", required: true } },
|
|
4151
|
+
async run({ args }) {
|
|
4152
|
+
await runWithJsonErrors(async () => {
|
|
4153
|
+
const { id } = parseTaskRef(args.id);
|
|
4154
|
+
const result = await akmTasksSetEnabled(id, enabled);
|
|
4155
|
+
output(`tasks-${verb}`, result);
|
|
4156
|
+
});
|
|
4157
|
+
},
|
|
4158
|
+
});
|
|
4159
|
+
}
|
|
4160
|
+
const tasksEnableCommand = makeTasksToggleCommand(true);
|
|
4161
|
+
const tasksDisableCommand = makeTasksToggleCommand(false);
|
|
4162
|
+
const tasksRunCommand = defineCommand({
|
|
4163
|
+
meta: {
|
|
4164
|
+
name: "run",
|
|
4165
|
+
description: "Execute a task now (this is what cron / launchd / schtasks invoke at the scheduled time)",
|
|
4166
|
+
},
|
|
4167
|
+
args: { id: { type: "positional", description: "Task id", required: true } },
|
|
4168
|
+
async run({ args }) {
|
|
4169
|
+
await runWithJsonErrors(async () => {
|
|
4170
|
+
const { id } = parseTaskRef(args.id);
|
|
4171
|
+
const envelope = await akmTasksRun(id);
|
|
4172
|
+
output("tasks-run", envelope);
|
|
4173
|
+
if (envelope.exitCode !== 0)
|
|
4174
|
+
process.exit(envelope.exitCode);
|
|
4175
|
+
});
|
|
4176
|
+
},
|
|
4177
|
+
});
|
|
4178
|
+
const tasksHistoryCommand = defineCommand({
|
|
4179
|
+
meta: { name: "history", description: "Show recent task run history" },
|
|
4180
|
+
args: {
|
|
4181
|
+
id: { type: "string", description: "Filter to one task id" },
|
|
4182
|
+
limit: { type: "string", description: "Maximum rows to return (default 50)" },
|
|
4183
|
+
},
|
|
4184
|
+
async run({ args }) {
|
|
4185
|
+
await runWithJsonErrors(async () => {
|
|
4186
|
+
const limit = parsePositiveIntFlag(args.limit ?? undefined);
|
|
4187
|
+
const result = await akmTasksHistory({ id: args.id, limit });
|
|
4188
|
+
output("tasks-history", result);
|
|
4189
|
+
});
|
|
4190
|
+
},
|
|
4191
|
+
});
|
|
4192
|
+
const tasksSyncCommand = defineCommand({
|
|
4193
|
+
meta: {
|
|
4194
|
+
name: "sync",
|
|
4195
|
+
description: "Reconcile the on-disk task files with the OS scheduler",
|
|
4196
|
+
},
|
|
4197
|
+
async run() {
|
|
4198
|
+
await runWithJsonErrors(async () => {
|
|
4199
|
+
const result = await akmTasksSync();
|
|
4200
|
+
output("tasks-sync", result);
|
|
4201
|
+
});
|
|
4202
|
+
},
|
|
4203
|
+
});
|
|
4204
|
+
const tasksDoctorCommand = defineCommand({
|
|
4205
|
+
meta: {
|
|
4206
|
+
name: "doctor",
|
|
4207
|
+
description: "Report the active scheduler backend, akm bin path, log dir, and supported schedule subset",
|
|
4208
|
+
},
|
|
4209
|
+
async run() {
|
|
4210
|
+
await runWithJsonErrors(async () => {
|
|
4211
|
+
const result = await akmTasksDoctor();
|
|
4212
|
+
output("tasks-doctor", result);
|
|
4213
|
+
});
|
|
4214
|
+
},
|
|
4215
|
+
});
|
|
4216
|
+
const tasksCommand = defineCommand({
|
|
4217
|
+
meta: {
|
|
4218
|
+
name: "tasks",
|
|
4219
|
+
description: "Schedule workflows or prompts via the OS-native scheduler (cron / launchd / schtasks)",
|
|
4220
|
+
},
|
|
4221
|
+
subCommands: {
|
|
4222
|
+
add: tasksAddCommand,
|
|
4223
|
+
list: tasksListCommand,
|
|
4224
|
+
show: tasksShowCommand,
|
|
4225
|
+
remove: tasksRemoveCommand,
|
|
4226
|
+
enable: tasksEnableCommand,
|
|
4227
|
+
disable: tasksDisableCommand,
|
|
4228
|
+
run: tasksRunCommand,
|
|
4229
|
+
history: tasksHistoryCommand,
|
|
4230
|
+
sync: tasksSyncCommand,
|
|
4231
|
+
doctor: tasksDoctorCommand,
|
|
4232
|
+
},
|
|
4233
|
+
run({ args }) {
|
|
4234
|
+
return runWithJsonErrors(async () => {
|
|
4235
|
+
if (hasSubcommand(args, TASKS_SUBCOMMAND_SET))
|
|
4236
|
+
return;
|
|
4237
|
+
const result = await akmTasksList();
|
|
4238
|
+
output("tasks-list", result);
|
|
4239
|
+
});
|
|
4240
|
+
},
|
|
4241
|
+
});
|
|
2772
4242
|
const main = defineCommand({
|
|
2773
4243
|
meta: {
|
|
2774
4244
|
name: "akm",
|
|
@@ -2777,8 +4247,21 @@ const main = defineCommand({
|
|
|
2777
4247
|
},
|
|
2778
4248
|
args: {
|
|
2779
4249
|
format: { type: "string", description: "Output format (json|jsonl|text|yaml)", default: "json" },
|
|
2780
|
-
detail: {
|
|
2781
|
-
|
|
4250
|
+
detail: {
|
|
4251
|
+
type: "string",
|
|
4252
|
+
description: "Detail level. Stable: brief|normal|full. Experimental: summary|agent " +
|
|
4253
|
+
"(supported on a subset of commands; coverage will expand or these will be replaced — " +
|
|
4254
|
+
"see STABILITY.md). Default: brief.",
|
|
4255
|
+
default: "brief",
|
|
4256
|
+
},
|
|
4257
|
+
quiet: {
|
|
4258
|
+
type: "boolean",
|
|
4259
|
+
alias: "q",
|
|
4260
|
+
description: "Suppress non-essential stderr output (banners, spinners, progress info). " +
|
|
4261
|
+
"Safety-critical output is never suppressed: errors, destructive-action confirmation prompts, " +
|
|
4262
|
+
"and auto-migration banners always appear regardless of --quiet.",
|
|
4263
|
+
default: false,
|
|
4264
|
+
},
|
|
2782
4265
|
verbose: {
|
|
2783
4266
|
type: "boolean",
|
|
2784
4267
|
description: "Print per-spec diagnostics to stderr (also honours AKM_VERBOSE env var)",
|
|
@@ -2789,7 +4272,10 @@ const main = defineCommand({
|
|
|
2789
4272
|
setup: setupCommand,
|
|
2790
4273
|
init: initCommand,
|
|
2791
4274
|
index: indexCommand,
|
|
4275
|
+
health: healthCommand,
|
|
2792
4276
|
info: infoCommand,
|
|
4277
|
+
graph: graphCommand,
|
|
4278
|
+
db: dbCommand,
|
|
2793
4279
|
add: addCommand,
|
|
2794
4280
|
list: listCommand,
|
|
2795
4281
|
remove: removeCommand,
|
|
@@ -2810,18 +4296,25 @@ const main = defineCommand({
|
|
|
2810
4296
|
feedback: feedbackCommand,
|
|
2811
4297
|
history: historyCommand,
|
|
2812
4298
|
events: eventsCommand,
|
|
2813
|
-
|
|
2814
|
-
|
|
4299
|
+
lessons: lessonsCommand,
|
|
4300
|
+
agent: agentCommand,
|
|
4301
|
+
lint: lintCommand,
|
|
4302
|
+
improve: improveCommand,
|
|
2815
4303
|
propose: proposeCommand,
|
|
2816
|
-
|
|
4304
|
+
proposals: proposalsCommand,
|
|
4305
|
+
accept: acceptCommand,
|
|
4306
|
+
reject: rejectCommand,
|
|
4307
|
+
diff: diffCommand,
|
|
4308
|
+
revert: revertCommand,
|
|
2817
4309
|
help: helpCommand,
|
|
2818
4310
|
hints: hintsCommand,
|
|
2819
4311
|
completions: completionsCommand,
|
|
2820
4312
|
vault: vaultCommand,
|
|
2821
4313
|
wiki: wikiCommand,
|
|
4314
|
+
tasks: tasksCommand,
|
|
2822
4315
|
},
|
|
2823
4316
|
});
|
|
2824
|
-
const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
|
|
4317
|
+
const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "show", "get", "set", "unset"]);
|
|
2825
4318
|
const VAULT_SUBCOMMAND_SET = new Set(["list", "path", "run", "create", "set", "unset"]);
|
|
2826
4319
|
const WIKI_SUBCOMMAND_SET = new Set([
|
|
2827
4320
|
"create",
|
|
@@ -2835,10 +4328,13 @@ const WIKI_SUBCOMMAND_SET = new Set([
|
|
|
2835
4328
|
"lint",
|
|
2836
4329
|
"ingest",
|
|
2837
4330
|
]);
|
|
2838
|
-
const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
|
|
2839
4331
|
// ── Exit codes ──────────────────────────────────────────────────────────────
|
|
2840
4332
|
const EXIT_GENERAL = 1;
|
|
2841
4333
|
const EXIT_USAGE = 2;
|
|
4334
|
+
/** `akm health` warn status — emitted when overall status is "warn" (advisories
|
|
4335
|
+
* fired but no hard failure). Chosen as 4 to avoid colliding with EXIT_GENERAL
|
|
4336
|
+
* (1 / fail) and EXIT_USAGE (2). CI monitors can map: 0=pass, 4=warn, 1=fail. */
|
|
4337
|
+
const EXIT_HEALTH_WARN = 4;
|
|
2842
4338
|
const EXIT_CONFIG = 78;
|
|
2843
4339
|
// citty reads process.argv directly and does not accept a custom argv array,
|
|
2844
4340
|
// so we must replace process.argv with the normalized version before runMain.
|
|
@@ -2849,18 +4345,52 @@ process.argv = normalizeShowArgv(process.argv);
|
|
|
2849
4345
|
// invalid; surface it through the same JSON-error path the rest of the CLI uses
|
|
2850
4346
|
// rather than letting the raw exception escape with a stack trace.
|
|
2851
4347
|
try {
|
|
4348
|
+
applyEarlyStderrFlags(process.argv);
|
|
2852
4349
|
initOutputMode(process.argv, loadConfig().output ?? {});
|
|
2853
4350
|
}
|
|
2854
4351
|
catch (error) {
|
|
2855
|
-
|
|
2856
|
-
const hint = extractHint(error);
|
|
2857
|
-
const exitCode = classifyExitCode(error);
|
|
2858
|
-
const code = error instanceof UsageError || error instanceof ConfigError || error instanceof NotFoundError
|
|
2859
|
-
? error.code
|
|
2860
|
-
: undefined;
|
|
2861
|
-
console.error(JSON.stringify({ ok: false, error: message, ...(code ? { code } : {}), hint }, null, 2));
|
|
2862
|
-
process.exit(exitCode);
|
|
4352
|
+
emitJsonError(error);
|
|
2863
4353
|
}
|
|
4354
|
+
// One-time cleanup of stale 0.7.x index file at the old cache location.
|
|
4355
|
+
// 0.8.0 moved the index to $XDG_DATA_HOME/akm/index.db (getDataDir()).
|
|
4356
|
+
// If the old file exists at $XDG_CACHE_HOME/akm/index.db, remove it so the
|
|
4357
|
+
// user isn't confused by a phantom DB. Best-effort; never fatal.
|
|
4358
|
+
try {
|
|
4359
|
+
const oldIndexPath = path.join(getCacheDir(), "index.db");
|
|
4360
|
+
if (fs.existsSync(oldIndexPath)) {
|
|
4361
|
+
fs.rmSync(oldIndexPath, { force: true });
|
|
4362
|
+
fs.rmSync(`${oldIndexPath}-shm`, { force: true });
|
|
4363
|
+
fs.rmSync(`${oldIndexPath}-wal`, { force: true });
|
|
4364
|
+
warn(`Cleaned up stale 0.7.x index from ${oldIndexPath}. Canonical path is now ${getDbPath()}.`);
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
catch {
|
|
4368
|
+
// Non-fatal; one-time warning only.
|
|
4369
|
+
}
|
|
4370
|
+
// First-time-user breadcrumb: when run with no subcommand AND no config
|
|
4371
|
+
// exists yet AND stderr is a TTY, print a friendly pointer to `akm setup`
|
|
4372
|
+
// above citty's auto-generated usage block. Triggers only when stdin/stderr
|
|
4373
|
+
// are interactive (so JSON-output users / CI consumers see nothing extra)
|
|
4374
|
+
// and stays silent for any flag-only invocation citty would handle itself
|
|
4375
|
+
// (--help, --version).
|
|
4376
|
+
(function maybePrintFirstTimeBanner() {
|
|
4377
|
+
const argv = process.argv.slice(2);
|
|
4378
|
+
// Fire only on completely bare `akm` invocation. Any explicit flag or
|
|
4379
|
+
// subcommand means the user knows what they want.
|
|
4380
|
+
if (argv.length > 0)
|
|
4381
|
+
return;
|
|
4382
|
+
if (!process.stderr.isTTY)
|
|
4383
|
+
return;
|
|
4384
|
+
try {
|
|
4385
|
+
if (fs.existsSync(getConfigPath()))
|
|
4386
|
+
return;
|
|
4387
|
+
}
|
|
4388
|
+
catch {
|
|
4389
|
+
// If we can't resolve the config path, assume non-fresh and stay silent.
|
|
4390
|
+
return;
|
|
4391
|
+
}
|
|
4392
|
+
console.error("👋 First time with akm? Run `akm setup` to get started.\n" + " Docs: https://github.com/itlackey/akm#readme\n");
|
|
4393
|
+
})();
|
|
2864
4394
|
runMain(main);
|
|
2865
4395
|
function classifyExitCode(error) {
|
|
2866
4396
|
if (error instanceof UsageError)
|
|
@@ -2871,33 +4401,6 @@ function classifyExitCode(error) {
|
|
|
2871
4401
|
return EXIT_GENERAL;
|
|
2872
4402
|
return EXIT_GENERAL;
|
|
2873
4403
|
}
|
|
2874
|
-
async function runWithJsonErrors(fn) {
|
|
2875
|
-
try {
|
|
2876
|
-
// Apply --quiet flag early so warnings inside the command are suppressed
|
|
2877
|
-
if (process.argv.includes("--quiet") || process.argv.includes("-q")) {
|
|
2878
|
-
setQuiet(true);
|
|
2879
|
-
}
|
|
2880
|
-
// Apply --verbose flag early so per-spec diagnostics (gated behind
|
|
2881
|
-
// `isVerbose()` in src/core/warn.ts) are restored. The `AKM_VERBOSE`
|
|
2882
|
-
// env var still wins regardless — see warn.ts for the precedence rule.
|
|
2883
|
-
if (process.argv.includes("--verbose")) {
|
|
2884
|
-
setVerbose(true);
|
|
2885
|
-
}
|
|
2886
|
-
await fn();
|
|
2887
|
-
}
|
|
2888
|
-
catch (error) {
|
|
2889
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2890
|
-
const hint = extractHint(error);
|
|
2891
|
-
const exitCode = classifyExitCode(error);
|
|
2892
|
-
// Surface machine-readable error code from typed errors when present so
|
|
2893
|
-
// scripts can branch on `.code` instead of message-string matching.
|
|
2894
|
-
const code = error instanceof UsageError || error instanceof ConfigError || error instanceof NotFoundError
|
|
2895
|
-
? error.code
|
|
2896
|
-
: undefined;
|
|
2897
|
-
console.error(JSON.stringify({ ok: false, error: message, ...(code ? { code } : {}), hint }, null, 2));
|
|
2898
|
-
process.exit(exitCode);
|
|
2899
|
-
}
|
|
2900
|
-
}
|
|
2901
4404
|
/**
|
|
2902
4405
|
* Extract an actionable hint from an error instance. Hints live on the error
|
|
2903
4406
|
* classes themselves (see src/errors.ts) — either supplied explicitly at the
|
|
@@ -2909,86 +4412,27 @@ function extractHint(error) {
|
|
|
2909
4412
|
}
|
|
2910
4413
|
return undefined;
|
|
2911
4414
|
}
|
|
2912
|
-
function hasConfigSubcommand(args) {
|
|
2913
|
-
const command = Array.isArray(args._) ? args._[0] : undefined;
|
|
2914
|
-
return typeof command === "string" && CONFIG_SUBCOMMAND_SET.has(command);
|
|
2915
|
-
}
|
|
2916
|
-
function hasVaultSubcommand(args) {
|
|
2917
|
-
const command = Array.isArray(args._) ? args._[0] : undefined;
|
|
2918
|
-
return typeof command === "string" && VAULT_SUBCOMMAND_SET.has(command);
|
|
2919
|
-
}
|
|
2920
|
-
function hasWikiSubcommand(args) {
|
|
2921
|
-
const command = Array.isArray(args._) ? args._[0] : undefined;
|
|
2922
|
-
return typeof command === "string" && WIKI_SUBCOMMAND_SET.has(command);
|
|
2923
|
-
}
|
|
2924
4415
|
/**
|
|
2925
|
-
*
|
|
2926
|
-
*
|
|
2927
|
-
*
|
|
2928
|
-
* Converts:
|
|
2929
|
-
* akm show knowledge:guide.md toc → akm show knowledge:guide.md --akmView toc
|
|
2930
|
-
* akm show knowledge:guide.md section Auth → akm show knowledge:guide.md --akmView section --akmHeading Auth
|
|
2931
|
-
* akm show knowledge:guide.md lines 1 50 → akm show knowledge:guide.md --akmView lines --akmStart 1 --akmEnd 50
|
|
2932
|
-
*
|
|
2933
|
-
* Legacy `--view` is intentionally unsupported.
|
|
2934
|
-
* Returns a new array; the input is never modified.
|
|
4416
|
+
* Serialize an error to the standard JSON envelope and exit.
|
|
4417
|
+
* Used in both the startup try/catch and `runWithJsonErrors`.
|
|
2935
4418
|
*/
|
|
2936
|
-
function
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
const arg = rest[i];
|
|
2950
|
-
if (arg === "--quiet" || arg === "-q" || arg === "--for-agent" || arg === "--for-agent=true") {
|
|
2951
|
-
globalFlags.push(arg);
|
|
2952
|
-
continue;
|
|
2953
|
-
}
|
|
2954
|
-
if (arg.startsWith("--format=") || arg.startsWith("--detail=")) {
|
|
2955
|
-
globalFlags.push(arg);
|
|
2956
|
-
continue;
|
|
2957
|
-
}
|
|
2958
|
-
if (arg === "--format" || arg === "--detail") {
|
|
2959
|
-
globalFlags.push(arg);
|
|
2960
|
-
if (rest[i + 1] !== undefined) {
|
|
2961
|
-
globalFlags.push(rest[i + 1]);
|
|
2962
|
-
i++;
|
|
2963
|
-
}
|
|
2964
|
-
continue;
|
|
2965
|
-
}
|
|
2966
|
-
showArgs.push(arg);
|
|
2967
|
-
}
|
|
2968
|
-
// showArgs[0] = ref, showArgs[1] = potential view mode, showArgs[2..] = view params
|
|
2969
|
-
const ref = showArgs[0];
|
|
2970
|
-
const viewMode = showArgs[1];
|
|
2971
|
-
if (!ref || !viewMode || !SHOW_VIEW_MODES.has(viewMode)) {
|
|
2972
|
-
return argv;
|
|
2973
|
-
}
|
|
2974
|
-
const result = [...prefix, ref, "--akmView", viewMode];
|
|
2975
|
-
if (viewMode === "section") {
|
|
2976
|
-
// Next arg is the heading name; pass empty string when missing so the
|
|
2977
|
-
// show handler can produce a clear "section not found" error.
|
|
2978
|
-
const heading = showArgs[2] ?? "";
|
|
2979
|
-
result.push("--akmHeading", heading);
|
|
4419
|
+
function emitJsonError(error) {
|
|
4420
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4421
|
+
const hint = extractHint(error);
|
|
4422
|
+
const exitCode = classifyExitCode(error);
|
|
4423
|
+
const code = error instanceof UsageError || error instanceof ConfigError || error instanceof NotFoundError
|
|
4424
|
+
? error.code
|
|
4425
|
+
: undefined;
|
|
4426
|
+
console.error(JSON.stringify({ ok: false, error: message, ...(code ? { code } : {}), hint }, null, 2));
|
|
4427
|
+
process.exit(exitCode);
|
|
4428
|
+
}
|
|
4429
|
+
async function runWithJsonErrors(fn) {
|
|
4430
|
+
try {
|
|
4431
|
+
await fn();
|
|
2980
4432
|
}
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
const start = showArgs[2];
|
|
2984
|
-
const end = showArgs[3];
|
|
2985
|
-
if (start)
|
|
2986
|
-
result.push("--akmStart", start);
|
|
2987
|
-
if (end)
|
|
2988
|
-
result.push("--akmEnd", end);
|
|
4433
|
+
catch (error) {
|
|
4434
|
+
emitJsonError(error);
|
|
2989
4435
|
}
|
|
2990
|
-
result.push(...globalFlags);
|
|
2991
|
-
return result;
|
|
2992
4436
|
}
|
|
2993
4437
|
// ── Hints (embedded AGENTS.md) ──────────────────────────────────────────────
|
|
2994
4438
|
function loadHints(detail = "normal") {
|