akm-cli 0.7.5 → 0.8.0-rc.3
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 +1 -1
- package/dist/cli/parse-args.js +86 -0
- package/dist/cli.js +1023 -521
- package/dist/commands/agent-dispatch.js +107 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +812 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +218 -43
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +2 -23
- package/dist/commands/graph.js +222 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +53 -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 +3 -30
- package/dist/commands/improve.js +1161 -0
- package/dist/commands/info.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/install-audit.js +5 -1
- package/dist/commands/installed-stashes.js +118 -138
- package/dist/commands/knowledge.js +133 -0
- package/dist/commands/lint/agent-linter.js +46 -0
- package/dist/commands/lint/base-linter.js +291 -0
- package/dist/commands/lint/command-linter.js +46 -0
- package/dist/commands/lint/default-linter.js +13 -0
- package/dist/commands/lint/index.js +145 -0
- package/dist/commands/lint/knowledge-linter.js +13 -0
- package/dist/commands/lint/memory-linter.js +58 -0
- package/dist/commands/lint/registry.js +33 -0
- package/dist/commands/lint/skill-linter.js +42 -0
- package/dist/commands/lint/task-linter.js +47 -0
- package/dist/commands/lint/types.js +1 -0
- package/dist/commands/lint/vault-key-rules.js +67 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +71 -28
- package/dist/commands/reflect.js +135 -35
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +54 -0
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +125 -20
- package/dist/commands/source-add.js +10 -10
- package/dist/commands/source-manage.js +11 -19
- package/dist/commands/tasks.js +385 -0
- package/dist/commands/url-checker.js +39 -0
- package/dist/commands/vault.js +168 -77
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-ref.js +4 -0
- package/dist/core/asset-registry.js +4 -16
- package/dist/core/asset-spec.js +10 -0
- package/dist/core/common.js +100 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +233 -133
- package/dist/core/events.js +73 -126
- package/dist/core/frontmatter.js +0 -6
- package/dist/core/markdown.js +17 -0
- package/dist/core/memory-improve.js +678 -0
- package/dist/core/parse.js +155 -0
- package/dist/core/paths.js +101 -3
- package/dist/core/proposal-validators.js +61 -0
- package/dist/core/proposals.js +49 -38
- package/dist/core/state-db.js +731 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +52 -238
- package/dist/indexer/db.js +403 -54
- package/dist/indexer/ensure-index.js +61 -0
- package/dist/indexer/graph-boost.js +247 -94
- package/dist/indexer/graph-db.js +201 -0
- package/dist/indexer/graph-dedup.js +99 -0
- package/dist/indexer/graph-extraction.js +409 -76
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +456 -290
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/matchers.js +124 -160
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +196 -197
- package/dist/indexer/path-resolver.js +89 -0
- package/dist/indexer/ranking-contributors.js +204 -0
- package/dist/indexer/ranking.js +74 -0
- package/dist/indexer/search-hit-enrichers.js +22 -0
- package/dist/indexer/search-source.js +24 -9
- package/dist/indexer/semantic-status.js +2 -16
- package/dist/indexer/walker.js +25 -0
- package/dist/integrations/agent/builders.js +109 -0
- package/dist/integrations/agent/config.js +203 -3
- package/dist/integrations/agent/index.js +5 -2
- package/dist/integrations/agent/model-aliases.js +63 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +77 -72
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +93 -22
- package/dist/integrations/lockfile.js +10 -18
- package/dist/integrations/session-logs/index.js +65 -0
- package/dist/integrations/session-logs/providers/claude-code.js +56 -0
- package/dist/integrations/session-logs/providers/opencode.js +52 -0
- package/dist/integrations/session-logs/types.js +1 -0
- package/dist/llm/call-ai.js +74 -0
- package/dist/llm/client.js +61 -122
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -62
- package/dist/llm/memory-infer.js +49 -71
- package/dist/llm/metadata-enhance.js +39 -22
- package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
- package/dist/output/cli-hints-full.md +277 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +2 -318
- package/dist/output/renderers.js +220 -256
- package/dist/output/shapes.js +101 -93
- package/dist/output/text.js +256 -17
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/registry/resolve.js +8 -16
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/filesystem.js +16 -23
- package/dist/sources/providers/git.js +4 -5
- package/dist/sources/providers/website.js +15 -22
- package/dist/sources/website-ingest.js +4 -0
- package/dist/tasks/backends/cron.js +200 -0
- package/dist/tasks/backends/exec-utils.js +25 -0
- package/dist/tasks/backends/index.js +32 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +184 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +212 -0
- package/dist/tasks/parser.js +198 -0
- package/dist/tasks/resolveAkmBin.js +84 -0
- package/dist/tasks/runner.js +432 -0
- package/dist/tasks/schedule.js +208 -0
- package/dist/tasks/schema.js +13 -0
- package/dist/tasks/validator.js +59 -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 +12 -0
- package/dist/wiki/wiki.js +10 -61
- package/dist/workflows/authoring.js +5 -25
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +59 -91
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +5 -2
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +3 -2
- package/dist/templates/wiki-templates.js +0 -100
package/dist/cli.js
CHANGED
|
@@ -4,37 +4,42 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
6
6
|
import { defineCommand, runMain } from "citty";
|
|
7
|
+
import { getStringArg, hasSubcommand, parseNonNegativeIntFlag, parsePositiveIntFlag } from "./cli/parse-args";
|
|
8
|
+
import { akmAgentDispatch } from "./commands/agent-dispatch";
|
|
7
9
|
import { generateBashCompletions, installBashCompletions } from "./commands/completions";
|
|
8
10
|
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./commands/config-cli";
|
|
9
11
|
import { akmCurate } from "./commands/curate";
|
|
10
|
-
import { akmDistill } from "./commands/distill";
|
|
11
12
|
import { akmEventsList, akmEventsTail } from "./commands/events";
|
|
13
|
+
import { akmGraphEntities, akmGraphExport, akmGraphRelated, akmGraphRelations, akmGraphSummary, } from "./commands/graph";
|
|
14
|
+
import { akmHealth } from "./commands/health";
|
|
12
15
|
import { akmHistory } from "./commands/history";
|
|
16
|
+
import { akmImprove } from "./commands/improve";
|
|
13
17
|
import { assembleInfo } from "./commands/info";
|
|
14
18
|
import { akmInit } from "./commands/init";
|
|
15
19
|
import { akmListSources, akmRemove, akmUpdate } from "./commands/installed-stashes";
|
|
20
|
+
import { readKnowledgeInput, writeMarkdownAsset } from "./commands/knowledge";
|
|
21
|
+
import { akmLint } from "./commands/lint";
|
|
16
22
|
import { renderMigrationHelp } from "./commands/migration-help";
|
|
17
23
|
import { akmProposalAccept, akmProposalDiff, akmProposalList, akmProposalReject, akmProposalShow, } from "./commands/proposal";
|
|
18
24
|
import { akmPropose } from "./commands/propose";
|
|
19
|
-
import { akmReflect } from "./commands/reflect";
|
|
20
25
|
import { searchRegistry } from "./commands/registry-search";
|
|
21
|
-
import { buildMemoryFrontmatter, parseDuration, readMemoryContent, runAutoHeuristics, runLlmEnrich, } from "./commands/remember";
|
|
22
|
-
import { akmSearch, parseScopeFilterFlags, parseSearchSource } from "./commands/search";
|
|
26
|
+
import { buildMemoryFrontmatter, parseDuration, readMemoryContent, resolveRememberContentArg, runAutoHeuristics, runLlmEnrich, } from "./commands/remember";
|
|
27
|
+
import { akmSearch, parseBeliefFilterMode, parseScopeFilterFlags, parseSearchSource } from "./commands/search";
|
|
23
28
|
import { checkForUpdate, performUpgrade } from "./commands/self-update";
|
|
24
|
-
import { akmShowUnified } from "./commands/show";
|
|
29
|
+
import { akmShowUnified, normalizeShowArgv } from "./commands/show";
|
|
25
30
|
import { akmAdd } from "./commands/source-add";
|
|
26
31
|
import { akmClone } from "./commands/source-clone";
|
|
27
32
|
import { addStash } from "./commands/source-manage";
|
|
33
|
+
import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksList, akmTasksRemove, akmTasksRun, akmTasksSetEnabled, akmTasksShow, akmTasksSync, parseTaskRef, } from "./commands/tasks";
|
|
28
34
|
import { parseAssetRef } from "./core/asset-ref";
|
|
29
35
|
import { deriveCanonicalAssetName, resolveAssetPathFromName } from "./core/asset-spec";
|
|
30
|
-
import { isHttpUrl, isWithin, resolveStashDir
|
|
31
|
-
import { DEFAULT_CONFIG,
|
|
36
|
+
import { isHttpUrl, isWithin, resolveStashDir } from "./core/common";
|
|
37
|
+
import { DEFAULT_CONFIG, loadConfig, loadUserConfig, resolveConfiguredSources, saveConfig } from "./core/config";
|
|
32
38
|
import { ConfigError, NotFoundError, UsageError } from "./core/errors";
|
|
33
39
|
import { appendEvent } from "./core/events";
|
|
34
|
-
import { getCacheDir, getDbPath, getDefaultStashDir } from "./core/paths";
|
|
35
|
-
import { setQuiet, setVerbose, warn } from "./core/warn";
|
|
36
|
-
import {
|
|
37
|
-
import { closeDatabase, findEntryIdByRef, openExistingDatabase } from "./indexer/db";
|
|
40
|
+
import { getCacheDir, getConfigPath, getDbPath, getDefaultStashDir } from "./core/paths";
|
|
41
|
+
import { clearLogFile, info, setLogFile, setQuiet, setVerbose, warn } from "./core/warn";
|
|
42
|
+
import { applyFeedbackToUtilityScore, closeDatabase, findEntryIdByRef, openExistingDatabase } from "./indexer/db";
|
|
38
43
|
import { ensureIndex } from "./indexer/ensure-index";
|
|
39
44
|
import { akmIndex } from "./indexer/indexer";
|
|
40
45
|
import { resolveSourceEntries } from "./indexer/search-source";
|
|
@@ -47,16 +52,22 @@ import { buildRegistryIndex, writeRegistryIndex } from "./registry/build-index";
|
|
|
47
52
|
import { resolveSourcesForOrigin } from "./registry/origin-resolve";
|
|
48
53
|
import { saveGitStash } from "./sources/providers/git";
|
|
49
54
|
import { resolveAssetPath } from "./sources/resolve";
|
|
50
|
-
import { fetchWebsiteMarkdownSnapshot } from "./sources/website-ingest";
|
|
51
55
|
import { pkgVersion } from "./version";
|
|
52
56
|
import { createWorkflowAsset, formatWorkflowErrors, getWorkflowTemplate, validateWorkflowSource, } from "./workflows/authoring";
|
|
53
57
|
import { hasWorkflowSubcommand, parseWorkflowJsonObject, parseWorkflowStepState, WORKFLOW_STEP_STATES, } from "./workflows/cli";
|
|
54
58
|
import { completeWorkflowStep, getNextWorkflowStep, getWorkflowStatus, listWorkflowRuns, resumeWorkflowRun, startWorkflowRun, } from "./workflows/runs";
|
|
55
|
-
const MAX_CAPTURED_ASSET_SLUG_LENGTH = 64;
|
|
56
59
|
const SKILLS_SH_NAME = "skills.sh";
|
|
57
60
|
const SKILLS_SH_URL = "https://skills.sh";
|
|
58
61
|
const SKILLS_SH_PROVIDER = "skills-sh";
|
|
59
62
|
import { stringify as yamlStringify } from "yaml";
|
|
63
|
+
function applyEarlyStderrFlags(argv) {
|
|
64
|
+
if (argv.includes("--quiet") || argv.includes("-q")) {
|
|
65
|
+
setQuiet(true);
|
|
66
|
+
}
|
|
67
|
+
if (argv.includes("--verbose")) {
|
|
68
|
+
setVerbose(true);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
60
71
|
/**
|
|
61
72
|
* Collect all occurrences of a repeatable flag from process.argv.
|
|
62
73
|
* Citty's StringArgDef only exposes the last value when a flag is repeated,
|
|
@@ -80,6 +91,43 @@ function parseAllFlagValues(flag) {
|
|
|
80
91
|
}
|
|
81
92
|
return values;
|
|
82
93
|
}
|
|
94
|
+
function resolveHelpMigrateVersionArg(version) {
|
|
95
|
+
if (version === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
const parsedFormat = parseFlagValue(process.argv, "--format");
|
|
98
|
+
if (parsedFormat !== undefined &&
|
|
99
|
+
version === parsedFormat &&
|
|
100
|
+
wasHelpMigrateFlagValueConsumedAsVersion(version, parsedFormat, "--format")) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const parsedDetail = parseFlagValue(process.argv, "--detail");
|
|
104
|
+
if (parsedDetail !== undefined &&
|
|
105
|
+
version === parsedDetail &&
|
|
106
|
+
wasHelpMigrateFlagValueConsumedAsVersion(version, parsedDetail, "--detail")) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return version;
|
|
110
|
+
}
|
|
111
|
+
function wasHelpMigrateFlagValueConsumedAsVersion(version, flagValue, flagName) {
|
|
112
|
+
const argv = process.argv.slice(2);
|
|
113
|
+
const helpIndex = argv.indexOf("help");
|
|
114
|
+
const tokens = helpIndex >= 0 ? argv.slice(helpIndex + 1) : argv;
|
|
115
|
+
const migrateIndex = tokens.indexOf("migrate");
|
|
116
|
+
const relevant = migrateIndex >= 0 ? tokens.slice(migrateIndex + 1) : tokens;
|
|
117
|
+
let flagIndex = -1;
|
|
118
|
+
for (let i = 0; i < relevant.length; i += 1) {
|
|
119
|
+
const token = relevant[i];
|
|
120
|
+
if (token === flagName || token === `${flagName}=${flagValue}`) {
|
|
121
|
+
flagIndex = i;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (flagIndex === -1)
|
|
126
|
+
return false;
|
|
127
|
+
if (relevant.slice(0, flagIndex).includes(version))
|
|
128
|
+
return false;
|
|
129
|
+
return relevant[flagIndex] === flagName ? relevant[flagIndex + 1] === version : true;
|
|
130
|
+
}
|
|
83
131
|
function output(command, result) {
|
|
84
132
|
const mode = getOutputMode();
|
|
85
133
|
const shaped = shapeForCommand(command, result, mode.detail, mode.forAgent);
|
|
@@ -111,12 +159,57 @@ function output(command, result) {
|
|
|
111
159
|
const setupCommand = defineCommand({
|
|
112
160
|
meta: {
|
|
113
161
|
name: "setup",
|
|
114
|
-
description: "Interactive configuration wizard
|
|
162
|
+
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
163
|
},
|
|
116
|
-
|
|
164
|
+
args: {
|
|
165
|
+
config: {
|
|
166
|
+
type: "string",
|
|
167
|
+
description: 'Config JSON to apply non-interactively, e.g. \'{"llm":{"endpoint":"...","model":"..."}}\'',
|
|
168
|
+
},
|
|
169
|
+
yes: {
|
|
170
|
+
type: "boolean",
|
|
171
|
+
default: false,
|
|
172
|
+
description: "Accept all defaults, skip all prompts. Idempotent — safe to run in CI.",
|
|
173
|
+
},
|
|
174
|
+
dir: {
|
|
175
|
+
type: "string",
|
|
176
|
+
description: "Stash directory path (overrides stashDir in config or --config JSON)",
|
|
177
|
+
},
|
|
178
|
+
probe: {
|
|
179
|
+
type: "boolean",
|
|
180
|
+
default: false,
|
|
181
|
+
description: "Probe LLM/embedding endpoints after writing config to verify connectivity",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
async run({ args }) {
|
|
117
185
|
await runWithJsonErrors(async () => {
|
|
118
|
-
const
|
|
119
|
-
|
|
186
|
+
const noInit = getHyphenatedBoolean(args, "no-init");
|
|
187
|
+
if (args.config) {
|
|
188
|
+
// Non-interactive config mode
|
|
189
|
+
const { runSetupFromConfig } = await import("./setup/setup");
|
|
190
|
+
const result = await runSetupFromConfig({
|
|
191
|
+
configJson: args.config,
|
|
192
|
+
dir: args.dir,
|
|
193
|
+
noInit,
|
|
194
|
+
probe: args.probe,
|
|
195
|
+
});
|
|
196
|
+
output("setup", result);
|
|
197
|
+
}
|
|
198
|
+
else if (args.yes) {
|
|
199
|
+
// Defaults mode — no prompts
|
|
200
|
+
const { runSetupWithDefaults } = await import("./setup/setup");
|
|
201
|
+
const result = await runSetupWithDefaults({
|
|
202
|
+
dir: args.dir,
|
|
203
|
+
noInit,
|
|
204
|
+
probe: args.probe,
|
|
205
|
+
});
|
|
206
|
+
output("setup", result);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
// Interactive wizard
|
|
210
|
+
const { runSetupWizard } = await import("./setup/setup");
|
|
211
|
+
await runSetupWizard({ dir: args.dir, noInit });
|
|
212
|
+
}
|
|
120
213
|
});
|
|
121
214
|
},
|
|
122
215
|
});
|
|
@@ -142,16 +235,23 @@ const indexCommand = defineCommand({
|
|
|
142
235
|
meta: { name: "index", description: "Build search index (incremental by default; --full forces full reindex)" },
|
|
143
236
|
args: {
|
|
144
237
|
full: { type: "boolean", description: "Force full reindex", default: false },
|
|
145
|
-
enrich: { type: "boolean", description: "Enable LLM inference and enrichment passes", default: false },
|
|
146
238
|
verbose: { type: "boolean", description: "Print phase-by-phase indexing progress to stderr", default: false },
|
|
147
239
|
},
|
|
148
240
|
async run({ args }) {
|
|
149
241
|
await runWithJsonErrors(async () => {
|
|
242
|
+
if (getHyphenatedBoolean(args, "enrich") || parseFlagValue(process.argv, "--enrich") !== undefined) {
|
|
243
|
+
throw new UsageError("`akm index --enrich` has been removed. Plain `akm index` now performs metadata enrichment by default.");
|
|
244
|
+
}
|
|
245
|
+
if (getHyphenatedBoolean(args, "re-enrich") || parseFlagValue(process.argv, "--re-enrich") !== undefined) {
|
|
246
|
+
throw new UsageError("`akm index --re-enrich` has been removed. Re-enrichment of index-time LLM passes is not exposed in this slice.");
|
|
247
|
+
}
|
|
150
248
|
const outputMode = getOutputMode();
|
|
151
249
|
const controller = new AbortController();
|
|
152
250
|
const abort = () => controller.abort(new Error("index interrupted"));
|
|
153
251
|
process.once("SIGINT", abort);
|
|
154
252
|
process.once("SIGTERM", abort);
|
|
253
|
+
const indexLogFile = path.join(getCacheDir(), "logs", "index", `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
|
|
254
|
+
setLogFile(indexLogFile);
|
|
155
255
|
const spin = !args.verbose && outputMode.format === "text" ? p.spinner() : null;
|
|
156
256
|
if (spin) {
|
|
157
257
|
spin.start(`Building search index${args.full ? " (full rebuild)" : ""}...`);
|
|
@@ -160,12 +260,11 @@ const indexCommand = defineCommand({
|
|
|
160
260
|
try {
|
|
161
261
|
const result = await akmIndex({
|
|
162
262
|
full: args.full,
|
|
163
|
-
|
|
164
|
-
onProgress: ({ message, processed, total }) => {
|
|
263
|
+
onProgress: ({ phase, message, processed, total }) => {
|
|
165
264
|
latestMessage = message;
|
|
166
265
|
const progressPrefix = processed !== undefined && total !== undefined ? `[${processed}/${total}] ` : "";
|
|
167
266
|
if (args.verbose) {
|
|
168
|
-
|
|
267
|
+
info(`[index:${phase}] ${progressPrefix}${message}`);
|
|
169
268
|
}
|
|
170
269
|
else if (spin) {
|
|
171
270
|
spin.stop(`${progressPrefix}${message}`);
|
|
@@ -186,6 +285,7 @@ const indexCommand = defineCommand({
|
|
|
186
285
|
throw error;
|
|
187
286
|
}
|
|
188
287
|
finally {
|
|
288
|
+
clearLogFile();
|
|
189
289
|
process.off("SIGINT", abort);
|
|
190
290
|
process.off("SIGTERM", abort);
|
|
191
291
|
}
|
|
@@ -201,6 +301,102 @@ const infoCommand = defineCommand({
|
|
|
201
301
|
});
|
|
202
302
|
},
|
|
203
303
|
});
|
|
304
|
+
const healthCommand = defineCommand({
|
|
305
|
+
meta: { name: "health", description: "Check akm runtime health, artifacts, and improve metrics" },
|
|
306
|
+
args: {
|
|
307
|
+
since: {
|
|
308
|
+
type: "string",
|
|
309
|
+
description: "Rolling window start (ISO timestamp, date, epoch ms, or shorthand like 24h / 7d)",
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
run({ args }) {
|
|
313
|
+
return runWithJsonErrors(() => {
|
|
314
|
+
const result = akmHealth({ since: args.since });
|
|
315
|
+
output("health", result);
|
|
316
|
+
});
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
const graphCommand = defineCommand({
|
|
320
|
+
meta: { name: "graph", description: "Inspect the indexed entity graph stored in SQLite" },
|
|
321
|
+
subCommands: {
|
|
322
|
+
summary: defineCommand({
|
|
323
|
+
meta: { name: "summary", description: "Show entity-graph counts and quality telemetry" },
|
|
324
|
+
args: {
|
|
325
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
326
|
+
},
|
|
327
|
+
run({ args }) {
|
|
328
|
+
return runWithJsonErrors(() => {
|
|
329
|
+
output("graph-summary", akmGraphSummary({ source: args.source }));
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
}),
|
|
333
|
+
entities: defineCommand({
|
|
334
|
+
meta: { name: "entities", description: "List entities with per-file occurrence counts" },
|
|
335
|
+
args: {
|
|
336
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
337
|
+
limit: { type: "string", description: "Maximum entities to return" },
|
|
338
|
+
},
|
|
339
|
+
run({ args }) {
|
|
340
|
+
return runWithJsonErrors(() => {
|
|
341
|
+
output("graph-entities", akmGraphEntities({ source: args.source, limit: parsePositiveIntFlag(args.limit ?? undefined) }));
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
}),
|
|
345
|
+
relations: defineCommand({
|
|
346
|
+
meta: { name: "relations", description: "List relations with occurrence counts" },
|
|
347
|
+
args: {
|
|
348
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
349
|
+
limit: { type: "string", description: "Maximum relations to return" },
|
|
350
|
+
},
|
|
351
|
+
run({ args }) {
|
|
352
|
+
return runWithJsonErrors(() => {
|
|
353
|
+
output("graph-relations", akmGraphRelations({ source: args.source, limit: parsePositiveIntFlag(args.limit ?? undefined) }));
|
|
354
|
+
});
|
|
355
|
+
},
|
|
356
|
+
}),
|
|
357
|
+
related: defineCommand({
|
|
358
|
+
meta: { name: "related", description: "Show graph-related neighboring assets for a ref" },
|
|
359
|
+
args: {
|
|
360
|
+
ref: { type: "positional", description: "Asset ref", required: true },
|
|
361
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
362
|
+
limit: { type: "string", description: "Maximum related assets to return" },
|
|
363
|
+
},
|
|
364
|
+
async run({ args }) {
|
|
365
|
+
return runWithJsonErrors(async () => {
|
|
366
|
+
output("graph-related", await akmGraphRelated({
|
|
367
|
+
ref: args.ref ?? "",
|
|
368
|
+
source: args.source,
|
|
369
|
+
limit: parsePositiveIntFlag(args.limit ?? undefined),
|
|
370
|
+
}));
|
|
371
|
+
});
|
|
372
|
+
},
|
|
373
|
+
}),
|
|
374
|
+
export: defineCommand({
|
|
375
|
+
meta: { name: "export", description: "Export graph artifact as JSON or JSONL" },
|
|
376
|
+
args: {
|
|
377
|
+
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
378
|
+
out: { type: "string", description: "Output path" },
|
|
379
|
+
format: { type: "string", description: "Export format (json|jsonl)", default: "json" },
|
|
380
|
+
},
|
|
381
|
+
run({ args }) {
|
|
382
|
+
return runWithJsonErrors(() => {
|
|
383
|
+
output("graph-export", akmGraphExport({
|
|
384
|
+
source: args.source,
|
|
385
|
+
out: args.out ?? "",
|
|
386
|
+
format: args.format,
|
|
387
|
+
}));
|
|
388
|
+
});
|
|
389
|
+
},
|
|
390
|
+
}),
|
|
391
|
+
},
|
|
392
|
+
run({ args }) {
|
|
393
|
+
return runWithJsonErrors(() => {
|
|
394
|
+
if (hasSubcommand(args, GRAPH_SUBCOMMAND_SET))
|
|
395
|
+
return;
|
|
396
|
+
output("graph-summary", akmGraphSummary());
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
});
|
|
204
400
|
const searchCommand = defineCommand({
|
|
205
401
|
meta: { name: "search", description: "Search the stash" },
|
|
206
402
|
args: {
|
|
@@ -220,29 +416,30 @@ const searchCommand = defineCommand({
|
|
|
220
416
|
description: 'Include entries with quality:"proposed" in the result set. Excluded by default (v1 spec §4.2).',
|
|
221
417
|
default: false,
|
|
222
418
|
},
|
|
419
|
+
belief: {
|
|
420
|
+
type: "string",
|
|
421
|
+
description: "Memory belief filter: all|current|historical. current keeps active memory beliefs; historical keeps contradicted/superseded/archived memory beliefs.",
|
|
422
|
+
default: "all",
|
|
423
|
+
},
|
|
223
424
|
format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
|
|
224
425
|
detail: { type: "string", description: "Detail level (brief|normal|full|summary|agent)" },
|
|
225
426
|
},
|
|
226
427
|
async run({ args }) {
|
|
227
428
|
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
429
|
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.`);
|
|
430
|
+
if (!query) {
|
|
431
|
+
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
432
|
}
|
|
238
|
-
const
|
|
433
|
+
const type = args.type;
|
|
434
|
+
const limit = parsePositiveIntFlag(args.limit ?? undefined);
|
|
239
435
|
const source = parseSearchSource(args.source);
|
|
240
436
|
// Repeatable; citty exposes only the last `--filter` value, so read all
|
|
241
437
|
// occurrences directly from argv (same pattern as `--tag`).
|
|
242
438
|
const filterTokens = parseAllFlagValues("--filter");
|
|
243
439
|
const filters = parseScopeFilterFlags(filterTokens, "--filter");
|
|
244
440
|
const includeProposed = args["include-proposed"] === true;
|
|
245
|
-
const
|
|
441
|
+
const belief = parseBeliefFilterMode(typeof args.belief === "string" ? args.belief : undefined);
|
|
442
|
+
const result = await akmSearch({ query, type, limit, source, filters, includeProposed, belief });
|
|
246
443
|
output("search", result);
|
|
247
444
|
});
|
|
248
445
|
},
|
|
@@ -267,11 +464,8 @@ const curateCommand = defineCommand({
|
|
|
267
464
|
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
465
|
}
|
|
269
466
|
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;
|
|
467
|
+
const limitParsed = parsePositiveIntFlag(args.limit ?? undefined);
|
|
468
|
+
const limit = limitParsed && limitParsed > 0 ? limitParsed : 4;
|
|
275
469
|
const source = parseSearchSource(args.source ?? "stash");
|
|
276
470
|
const curated = await akmCurate({ query: args.query, type, limit, source });
|
|
277
471
|
output("curate", curated);
|
|
@@ -310,7 +504,7 @@ const addCommand = defineCommand({
|
|
|
310
504
|
"max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
|
|
311
505
|
"allow-insecure": {
|
|
312
506
|
type: "boolean",
|
|
313
|
-
description: "Allow a plain HTTP source URL
|
|
507
|
+
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
508
|
default: false,
|
|
315
509
|
},
|
|
316
510
|
},
|
|
@@ -318,6 +512,7 @@ const addCommand = defineCommand({
|
|
|
318
512
|
await runWithJsonErrors(async () => {
|
|
319
513
|
const ref = args.ref.trim();
|
|
320
514
|
const allowInsecure = getHyphenatedBoolean(args, "allow-insecure");
|
|
515
|
+
const allowDangerousKeys = allowInsecure;
|
|
321
516
|
// URL with --provider → stash source (remote or git provider)
|
|
322
517
|
if (args.provider) {
|
|
323
518
|
if (shouldWarnOnPlainHttp(ref)) {
|
|
@@ -397,6 +592,118 @@ const addCommand = defineCommand({
|
|
|
397
592
|
writable: args.writable === true,
|
|
398
593
|
},
|
|
399
594
|
});
|
|
595
|
+
// ── Post-install vault key audit ────────────────────────────────────────
|
|
596
|
+
// Resolve the stash root from the install result and scan any vault files
|
|
597
|
+
// for dangerous env var keys. When findings are present the install is
|
|
598
|
+
// gated: TTY → interactive confirmation prompt; non-TTY without
|
|
599
|
+
// --allow-insecure → hard failure (exit 1). Pass
|
|
600
|
+
// --allow-insecure to skip the prompt non-interactively.
|
|
601
|
+
try {
|
|
602
|
+
const installedStashRoot = result.installed?.stashRoot ??
|
|
603
|
+
(result.sourceAdded && "stashRoot" in result.sourceAdded ? result.sourceAdded.stashRoot : undefined);
|
|
604
|
+
if (installedStashRoot) {
|
|
605
|
+
const { checkVaultForDangerousKeys } = await import("./commands/lint/vault-key-rules.js");
|
|
606
|
+
const vaultsDir = path.join(installedStashRoot, "vaults");
|
|
607
|
+
if (fs.existsSync(vaultsDir)) {
|
|
608
|
+
const envFiles = fs.readdirSync(vaultsDir).filter((f) => f.endsWith(".env"));
|
|
609
|
+
// Collect all dangerous-key findings across every vault file.
|
|
610
|
+
const allFindings = [];
|
|
611
|
+
for (const envFile of envFiles) {
|
|
612
|
+
const vaultPath = path.join(vaultsDir, envFile);
|
|
613
|
+
const baseName = path.basename(envFile, ".env");
|
|
614
|
+
const vaultRef = baseName === "" ? "vault:default" : `vault:${baseName}`;
|
|
615
|
+
const relPath = path.join("vaults", envFile);
|
|
616
|
+
const findings = checkVaultForDangerousKeys(vaultPath, relPath, vaultRef);
|
|
617
|
+
for (const finding of findings) {
|
|
618
|
+
// Extract the key name from the detail string for the summary line.
|
|
619
|
+
const keyMatch = finding.detail.match(/Vault key `([^`]+)`/);
|
|
620
|
+
const keyName = keyMatch ? keyMatch[1] : finding.file;
|
|
621
|
+
allFindings.push({ vaultRef, keyName, relPath });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (allFindings.length > 0) {
|
|
625
|
+
if (allowDangerousKeys) {
|
|
626
|
+
// Operator has explicitly accepted the risk — warn and continue.
|
|
627
|
+
for (const f of allFindings) {
|
|
628
|
+
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.`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
else if (process.stdout.isTTY) {
|
|
632
|
+
// Interactive path: show findings and ask the user to confirm.
|
|
633
|
+
const stashLabel = ref;
|
|
634
|
+
const groupedByVault = new Map();
|
|
635
|
+
for (const f of allFindings) {
|
|
636
|
+
const existing = groupedByVault.get(f.vaultRef) ?? [];
|
|
637
|
+
existing.push(f.keyName);
|
|
638
|
+
groupedByVault.set(f.vaultRef, existing);
|
|
639
|
+
}
|
|
640
|
+
for (const [vaultRef, keys] of groupedByVault) {
|
|
641
|
+
warn(`[warn] Vault "${vaultRef}" in stash "${stashLabel}" contains potentially dangerous keys:`);
|
|
642
|
+
for (const key of keys) {
|
|
643
|
+
warn(` - ${key}: can hijack process execution via \`akm vault run\``);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const confirmed = await p.confirm({
|
|
647
|
+
message: "Install anyway?",
|
|
648
|
+
initialValue: false,
|
|
649
|
+
});
|
|
650
|
+
if (p.isCancel(confirmed) || confirmed !== true) {
|
|
651
|
+
// Roll back the install before aborting.
|
|
652
|
+
// Use the canonical installed id (most reliably resolved by akmRemove) rather
|
|
653
|
+
// than the raw user-supplied ref which may not match after URL normalisation.
|
|
654
|
+
const rollbackTarget = result.installed?.id ?? result.sourceAdded?.stashRoot ?? ref;
|
|
655
|
+
let rollbackWarning;
|
|
656
|
+
try {
|
|
657
|
+
await akmRemove({ target: rollbackTarget });
|
|
658
|
+
}
|
|
659
|
+
catch (_rollbackErr) {
|
|
660
|
+
rollbackWarning =
|
|
661
|
+
`Rollback failed — stash may still be installed at ${installedStashRoot}. ` +
|
|
662
|
+
`Remove it manually with: akm remove ${rollbackTarget}`;
|
|
663
|
+
}
|
|
664
|
+
console.error(JSON.stringify({
|
|
665
|
+
ok: false,
|
|
666
|
+
error: "Install aborted: stash contains dangerous vault keys. Remove the keys or re-run with --allow-insecure to bypass.",
|
|
667
|
+
code: "DANGEROUS_VAULT_KEY",
|
|
668
|
+
...(rollbackWarning ? { rollbackWarning } : {}),
|
|
669
|
+
}, null, 2));
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// Non-interactive path without bypass flag: fail hard.
|
|
675
|
+
// Roll back the install before exiting.
|
|
676
|
+
// Use the canonical installed id (most reliably resolved by akmRemove) rather
|
|
677
|
+
// than the raw user-supplied ref which may not match after URL normalisation.
|
|
678
|
+
const rollbackTarget = result.installed?.id ?? result.sourceAdded?.stashRoot ?? ref;
|
|
679
|
+
let rollbackWarning;
|
|
680
|
+
try {
|
|
681
|
+
await akmRemove({ target: rollbackTarget });
|
|
682
|
+
}
|
|
683
|
+
catch (_rollbackErr) {
|
|
684
|
+
rollbackWarning =
|
|
685
|
+
`Rollback failed — stash may still be installed at ${installedStashRoot}. ` +
|
|
686
|
+
`Remove it manually with: akm remove ${rollbackTarget}`;
|
|
687
|
+
}
|
|
688
|
+
const keyList = allFindings.map((f) => ` - ${f.keyName} (${f.vaultRef})`).join("\n");
|
|
689
|
+
console.error(JSON.stringify({
|
|
690
|
+
ok: false,
|
|
691
|
+
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.`,
|
|
692
|
+
code: "DANGEROUS_VAULT_KEY",
|
|
693
|
+
...(rollbackWarning ? { rollbackWarning } : {}),
|
|
694
|
+
}, null, 2));
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch (auditErr) {
|
|
702
|
+
// Only swallow errors that are NOT our intentional process.exit calls.
|
|
703
|
+
if (auditErr instanceof Error && auditErr.message === "process.exit called")
|
|
704
|
+
throw auditErr;
|
|
705
|
+
// Vault key audit is best-effort; never fail the install on unexpected audit errors.
|
|
706
|
+
}
|
|
400
707
|
output("add", result);
|
|
401
708
|
});
|
|
402
709
|
},
|
|
@@ -545,15 +852,17 @@ const showCommand = defineCommand({
|
|
|
545
852
|
},
|
|
546
853
|
async run({ args }) {
|
|
547
854
|
await runWithJsonErrors(async () => {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
throw new UsageError(error.message, "INVALID_FLAG_VALUE", error.hint());
|
|
855
|
+
const subcommand = Array.isArray(args._) ? args._[0] : undefined;
|
|
856
|
+
if (subcommand === "proposal") {
|
|
857
|
+
const proposalId = Array.isArray(args._) ? args._[1] : undefined;
|
|
858
|
+
if (typeof proposalId !== "string" || !proposalId.trim()) {
|
|
859
|
+
throw new UsageError("Usage: akm show proposal <id>", "MISSING_REQUIRED_ARGUMENT");
|
|
554
860
|
}
|
|
555
|
-
|
|
861
|
+
const result = akmProposalShow({ id: proposalId.trim() });
|
|
862
|
+
output("proposal-show", result);
|
|
863
|
+
return;
|
|
556
864
|
}
|
|
865
|
+
parseAssetRef(args.ref);
|
|
557
866
|
// The knowledge-view positional syntax (`akm show knowledge:foo section "Auth"`)
|
|
558
867
|
// is rewritten to `--akmView` / `--akmHeading` / `--akmStart` / `--akmEnd`
|
|
559
868
|
// by `normalizeShowArgv` before citty parses argv. We read those values
|
|
@@ -642,6 +951,14 @@ const configCommand = defineCommand({
|
|
|
642
951
|
});
|
|
643
952
|
},
|
|
644
953
|
}),
|
|
954
|
+
show: defineCommand({
|
|
955
|
+
meta: { name: "show", description: "Alias for `akm config list` — list current configuration" },
|
|
956
|
+
run() {
|
|
957
|
+
return runWithJsonErrors(() => {
|
|
958
|
+
output("config", listConfig(loadConfig()));
|
|
959
|
+
});
|
|
960
|
+
},
|
|
961
|
+
}),
|
|
645
962
|
get: defineCommand({
|
|
646
963
|
meta: { name: "get", description: "Get a configuration value by key" },
|
|
647
964
|
args: {
|
|
@@ -683,7 +1000,7 @@ const configCommand = defineCommand({
|
|
|
683
1000
|
},
|
|
684
1001
|
run({ args }) {
|
|
685
1002
|
return runWithJsonErrors(() => {
|
|
686
|
-
if (
|
|
1003
|
+
if (hasSubcommand(args, CONFIG_SUBCOMMAND_SET))
|
|
687
1004
|
return;
|
|
688
1005
|
if (args.list) {
|
|
689
1006
|
output("config", listConfig(loadConfig()));
|
|
@@ -903,10 +1220,7 @@ const registryCommand = defineCommand({
|
|
|
903
1220
|
},
|
|
904
1221
|
async run({ args }) {
|
|
905
1222
|
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
|
-
}
|
|
1223
|
+
const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
|
|
910
1224
|
const result = await searchRegistry(args.query, { limit: limitRaw, includeAssets: args.assets });
|
|
911
1225
|
output("registry-search", result);
|
|
912
1226
|
});
|
|
@@ -971,7 +1285,7 @@ const feedbackCommand = defineCommand({
|
|
|
971
1285
|
description: "Record positive or negative feedback for any indexed stash asset.\n\n" +
|
|
972
1286
|
"Positive feedback boosts an asset's EMA utility score, making it rank higher\n" +
|
|
973
1287
|
"in future searches without requiring a full reindex.\n\n" +
|
|
974
|
-
"Negative feedback records a negative signal in usage_events and events
|
|
1288
|
+
"Negative feedback records a negative signal in usage_events and state.db events.\n" +
|
|
975
1289
|
"It does NOT immediately lower the asset's ranking — the EMA utility score is\n" +
|
|
976
1290
|
"updated the next time `akm index` runs (incremental or full). Run `akm index`\n" +
|
|
977
1291
|
"after recording negative feedback to have it reflected in search results.",
|
|
@@ -988,7 +1302,11 @@ const feedbackCommand = defineCommand({
|
|
|
988
1302
|
"Reindexing is required for the signal to affect search results.",
|
|
989
1303
|
default: false,
|
|
990
1304
|
},
|
|
991
|
-
|
|
1305
|
+
reason: {
|
|
1306
|
+
type: "string",
|
|
1307
|
+
description: "Reason for the feedback (recommended for negative feedback, used by distillation)",
|
|
1308
|
+
},
|
|
1309
|
+
note: { type: "string", description: "Alias for --reason (backward-compatible, prefer --reason)" },
|
|
992
1310
|
tag: {
|
|
993
1311
|
type: "string",
|
|
994
1312
|
description: "Tag to attach to the feedback (repeatable, e.g. --tag slice:train --tag team:platform)",
|
|
@@ -1008,11 +1326,21 @@ const feedbackCommand = defineCommand({
|
|
|
1008
1326
|
throw new UsageError("Specify --positive or --negative.");
|
|
1009
1327
|
}
|
|
1010
1328
|
const signal = args.positive ? "positive" : "negative";
|
|
1329
|
+
const reason = args.reason ?? args.note;
|
|
1330
|
+
if (args.negative === true && !reason?.trim()) {
|
|
1331
|
+
const cfg = loadConfig();
|
|
1332
|
+
if (cfg.feedback?.requireReason === true) {
|
|
1333
|
+
throw new UsageError("Negative feedback requires --reason (feedback.requireReason is enabled).", "MISSING_REQUIRED_ARGUMENT");
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
warn("Warning: negative feedback without --reason provides less distillation signal.");
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1011
1339
|
const rawTags = parseAllFlagValues("--tag");
|
|
1012
1340
|
const validatedTags = validateFeedbackTags(rawTags);
|
|
1013
1341
|
const metadataObj = {
|
|
1014
1342
|
signal,
|
|
1015
|
-
...(
|
|
1343
|
+
...(reason?.trim() ? { reason: reason.trim() } : {}),
|
|
1016
1344
|
...(validatedTags.length > 0 ? { tags: validatedTags } : {}),
|
|
1017
1345
|
};
|
|
1018
1346
|
const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
|
|
@@ -1040,6 +1368,25 @@ const feedbackCommand = defineCommand({
|
|
|
1040
1368
|
signal,
|
|
1041
1369
|
metadata: metadataStr,
|
|
1042
1370
|
});
|
|
1371
|
+
// Apply feedback-derived utility score adjustment immediately so that
|
|
1372
|
+
// positive/negative signals influence search ranking without requiring
|
|
1373
|
+
// a full reindex. We query the total accumulated feedback counts from
|
|
1374
|
+
// usage_events so the delta reflects the entire signal history.
|
|
1375
|
+
try {
|
|
1376
|
+
const counts = db
|
|
1377
|
+
.prepare(`SELECT
|
|
1378
|
+
SUM(CASE WHEN signal = 'positive' THEN 1 ELSE 0 END) AS pos,
|
|
1379
|
+
SUM(CASE WHEN signal = 'negative' THEN 1 ELSE 0 END) AS neg
|
|
1380
|
+
FROM usage_events
|
|
1381
|
+
WHERE event_type = 'feedback' AND entry_id = ?`)
|
|
1382
|
+
.get(entryId);
|
|
1383
|
+
const pos = counts?.pos ?? 0;
|
|
1384
|
+
const neg = counts?.neg ?? 0;
|
|
1385
|
+
applyFeedbackToUtilityScore(db, entryId, pos, neg);
|
|
1386
|
+
}
|
|
1387
|
+
catch {
|
|
1388
|
+
// best-effort — feedback recording succeeds even if utility update fails
|
|
1389
|
+
}
|
|
1043
1390
|
}
|
|
1044
1391
|
finally {
|
|
1045
1392
|
closeDatabase(db);
|
|
@@ -1049,7 +1396,7 @@ const feedbackCommand = defineCommand({
|
|
|
1049
1396
|
ref,
|
|
1050
1397
|
metadata: metadataObj,
|
|
1051
1398
|
});
|
|
1052
|
-
output("feedback", { ok: true, ref, signal,
|
|
1399
|
+
output("feedback", { ok: true, ref, signal, reason: reason?.trim() ?? null, tags: validatedTags });
|
|
1053
1400
|
});
|
|
1054
1401
|
},
|
|
1055
1402
|
});
|
|
@@ -1059,8 +1406,8 @@ const historyCommand = defineCommand({
|
|
|
1059
1406
|
description: "Show mutation/usage history for a single asset (--ref) or stash-wide.\n\n" +
|
|
1060
1407
|
"Event sources:\n" +
|
|
1061
1408
|
" usage_events (default): search, show, and feedback events from the local index.\n" +
|
|
1062
|
-
"
|
|
1063
|
-
" emitted by `akm
|
|
1409
|
+
" state.db events (--include-proposals): proposal lifecycle events (promoted, rejected)\n" +
|
|
1410
|
+
" emitted by `akm accept` / `akm reject`.\n\n" +
|
|
1064
1411
|
"Results from all active sources are merged and sorted chronologically.",
|
|
1065
1412
|
},
|
|
1066
1413
|
args: {
|
|
@@ -1068,7 +1415,7 @@ const historyCommand = defineCommand({
|
|
|
1068
1415
|
since: { type: "string", description: "ISO timestamp or epoch ms — only events on/after this time" },
|
|
1069
1416
|
"include-proposals": {
|
|
1070
1417
|
type: "boolean",
|
|
1071
|
-
description: "Also include proposal lifecycle events (promoted, rejected) from events.
|
|
1418
|
+
description: "Also include proposal lifecycle events (promoted, rejected) from state.db events. " +
|
|
1072
1419
|
"Default: false (usage_events only).",
|
|
1073
1420
|
default: false,
|
|
1074
1421
|
},
|
|
@@ -1085,93 +1432,6 @@ const historyCommand = defineCommand({
|
|
|
1085
1432
|
});
|
|
1086
1433
|
},
|
|
1087
1434
|
});
|
|
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
1435
|
const workflowStartCommand = defineCommand({
|
|
1176
1436
|
meta: {
|
|
1177
1437
|
name: "start",
|
|
@@ -1196,16 +1456,20 @@ const workflowNextCommand = defineCommand({
|
|
|
1196
1456
|
args: {
|
|
1197
1457
|
target: { type: "positional", description: "Workflow run id or workflow ref", required: true },
|
|
1198
1458
|
params: { type: "string", description: "Workflow parameters as a JSON object (only for auto-started runs)" },
|
|
1459
|
+
"dry-run": { type: "boolean", description: "Not supported — rejected with an error", default: false },
|
|
1199
1460
|
},
|
|
1200
1461
|
async run({ args }) {
|
|
1201
1462
|
await runWithJsonErrors(async () => {
|
|
1463
|
+
if (getHyphenatedBoolean(args, "dry-run")) {
|
|
1464
|
+
throw new UsageError("`akm workflow next` does not support --dry-run. Remove the flag to start or resume a run.", "INVALID_FLAG_VALUE");
|
|
1465
|
+
}
|
|
1202
1466
|
const parsedParams = args.params ? parseWorkflowJsonObject(args.params, "--params") : undefined;
|
|
1203
1467
|
// If the target looks like a UUID-style run id (no `:` and matches the
|
|
1204
1468
|
// run-id shape), short-circuit with a structured WORKFLOW_NOT_FOUND
|
|
1205
1469
|
// error before parseAssetRef gets to throw an unhelpful ref-parse error.
|
|
1206
1470
|
if (looksLikeWorkflowRunId(args.target)) {
|
|
1207
1471
|
const { hasWorkflowRun } = await import("./workflows/runs.js");
|
|
1208
|
-
if (!hasWorkflowRun(args.target)) {
|
|
1472
|
+
if (!(await hasWorkflowRun(args.target))) {
|
|
1209
1473
|
throw new NotFoundError(`Workflow run "${args.target}" not found.`, "WORKFLOW_NOT_FOUND", "Run `akm workflow list --active` to see runs.");
|
|
1210
1474
|
}
|
|
1211
1475
|
}
|
|
@@ -1250,7 +1514,7 @@ const workflowCompleteCommand = defineCommand({
|
|
|
1250
1514
|
},
|
|
1251
1515
|
async run({ args }) {
|
|
1252
1516
|
await runWithJsonErrors(async () => {
|
|
1253
|
-
const result = completeWorkflowStep({
|
|
1517
|
+
const result = await completeWorkflowStep({
|
|
1254
1518
|
runId: args.runId,
|
|
1255
1519
|
stepId: args.step,
|
|
1256
1520
|
status: parseWorkflowStepState(args.state),
|
|
@@ -1270,7 +1534,7 @@ const workflowStatusCommand = defineCommand({
|
|
|
1270
1534
|
target: { type: "positional", description: "Workflow run id or workflow ref (workflow:<name>)", required: true },
|
|
1271
1535
|
},
|
|
1272
1536
|
run({ args }) {
|
|
1273
|
-
return runWithJsonErrors(() => {
|
|
1537
|
+
return runWithJsonErrors(async () => {
|
|
1274
1538
|
const target = args.target;
|
|
1275
1539
|
// Check if target looks like a workflow ref
|
|
1276
1540
|
const parsed = (() => {
|
|
@@ -1283,18 +1547,18 @@ const workflowStatusCommand = defineCommand({
|
|
|
1283
1547
|
})();
|
|
1284
1548
|
if (parsed?.type === "workflow") {
|
|
1285
1549
|
const ref = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`;
|
|
1286
|
-
const { runs } = listWorkflowRuns({ workflowRef: ref });
|
|
1550
|
+
const { runs } = await listWorkflowRuns({ workflowRef: ref });
|
|
1287
1551
|
if (runs.length === 0) {
|
|
1288
1552
|
throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
|
|
1289
1553
|
}
|
|
1290
1554
|
const mostRecent = runs[0];
|
|
1291
1555
|
if (!mostRecent)
|
|
1292
1556
|
throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
|
|
1293
|
-
const result = getWorkflowStatus(mostRecent.id);
|
|
1557
|
+
const result = await getWorkflowStatus(mostRecent.id);
|
|
1294
1558
|
output("workflow-status", result);
|
|
1295
1559
|
}
|
|
1296
1560
|
else {
|
|
1297
|
-
const result = getWorkflowStatus(target);
|
|
1561
|
+
const result = await getWorkflowStatus(target);
|
|
1298
1562
|
output("workflow-status", result);
|
|
1299
1563
|
}
|
|
1300
1564
|
});
|
|
@@ -1310,8 +1574,8 @@ const workflowListCommand = defineCommand({
|
|
|
1310
1574
|
active: { type: "boolean", description: "Only show active runs", default: false },
|
|
1311
1575
|
},
|
|
1312
1576
|
run({ args }) {
|
|
1313
|
-
return runWithJsonErrors(() => {
|
|
1314
|
-
const result = listWorkflowRuns({ workflowRef: args.ref, activeOnly: args.active });
|
|
1577
|
+
return runWithJsonErrors(async () => {
|
|
1578
|
+
const result = await listWorkflowRuns({ workflowRef: args.ref, activeOnly: args.active });
|
|
1315
1579
|
output("workflow-list", result);
|
|
1316
1580
|
});
|
|
1317
1581
|
},
|
|
@@ -1424,8 +1688,8 @@ const workflowResumeCommand = defineCommand({
|
|
|
1424
1688
|
runId: { type: "positional", description: "Workflow run id", required: true },
|
|
1425
1689
|
},
|
|
1426
1690
|
run({ args }) {
|
|
1427
|
-
return runWithJsonErrors(() => {
|
|
1428
|
-
const result = resumeWorkflowRun(args.runId);
|
|
1691
|
+
return runWithJsonErrors(async () => {
|
|
1692
|
+
const result = await resumeWorkflowRun(args.runId);
|
|
1429
1693
|
output("workflow-resume", result);
|
|
1430
1694
|
});
|
|
1431
1695
|
},
|
|
@@ -1447,10 +1711,10 @@ const workflowCommand = defineCommand({
|
|
|
1447
1711
|
validate: workflowValidateCommand,
|
|
1448
1712
|
},
|
|
1449
1713
|
run({ args }) {
|
|
1450
|
-
return runWithJsonErrors(() => {
|
|
1714
|
+
return runWithJsonErrors(async () => {
|
|
1451
1715
|
if (hasWorkflowSubcommand(args))
|
|
1452
1716
|
return;
|
|
1453
|
-
output("workflow-list", listWorkflowRuns({ activeOnly: true }));
|
|
1717
|
+
output("workflow-list", await listWorkflowRuns({ activeOnly: true }));
|
|
1454
1718
|
});
|
|
1455
1719
|
},
|
|
1456
1720
|
});
|
|
@@ -1520,6 +1784,10 @@ const rememberCommand = defineCommand({
|
|
|
1520
1784
|
type: "string",
|
|
1521
1785
|
description: "Scope this memory to a channel name (persisted as `scope_channel` frontmatter)",
|
|
1522
1786
|
},
|
|
1787
|
+
showSimilar: {
|
|
1788
|
+
type: "boolean",
|
|
1789
|
+
description: "Return top-3 similar existing memories in output (opt-in)",
|
|
1790
|
+
},
|
|
1523
1791
|
},
|
|
1524
1792
|
async run({ args }) {
|
|
1525
1793
|
return runWithJsonErrors(async () => {
|
|
@@ -1541,7 +1809,7 @@ const rememberCommand = defineCommand({
|
|
|
1541
1809
|
if (typeof args.channel === "string" && args.channel.trim())
|
|
1542
1810
|
scopeFields.channel = args.channel.trim();
|
|
1543
1811
|
const hasScope = Object.keys(scopeFields).length > 0;
|
|
1544
|
-
const hasTagRequiringArgs = rawTags.length > 0 || !!args.expires || !!args.source || !!args.description
|
|
1812
|
+
const hasTagRequiringArgs = rawTags.length > 0 || !!args.expires || !!args.source || !!args.description;
|
|
1545
1813
|
const hasStructuredArgs = hasTagRequiringArgs || hasScope || args.auto;
|
|
1546
1814
|
if (!hasStructuredArgs) {
|
|
1547
1815
|
const result = await writeMarkdownAsset({
|
|
@@ -1557,7 +1825,13 @@ const rememberCommand = defineCommand({
|
|
|
1557
1825
|
ref: result.ref,
|
|
1558
1826
|
metadata: { path: result.path, force: args.force === true },
|
|
1559
1827
|
});
|
|
1560
|
-
|
|
1828
|
+
if (args.showSimilar) {
|
|
1829
|
+
const similar = await fetchSimilarMemories(body.slice(0, 500), result.ref);
|
|
1830
|
+
output("remember", { ok: true, ...result, similar });
|
|
1831
|
+
}
|
|
1832
|
+
else {
|
|
1833
|
+
output("remember", { ok: true, ...result });
|
|
1834
|
+
}
|
|
1561
1835
|
return;
|
|
1562
1836
|
}
|
|
1563
1837
|
// ── Accumulate metadata from all three modes ──────────────────────────
|
|
@@ -1646,53 +1920,31 @@ const rememberCommand = defineCommand({
|
|
|
1646
1920
|
...(hasScope ? { scope: scopeFields } : {}),
|
|
1647
1921
|
},
|
|
1648
1922
|
});
|
|
1649
|
-
|
|
1923
|
+
if (args.showSimilar) {
|
|
1924
|
+
const similar = await fetchSimilarMemories((body ?? args.content ?? "").slice(0, 500), result.ref);
|
|
1925
|
+
output("remember", { ok: true, ...result, similar });
|
|
1926
|
+
}
|
|
1927
|
+
else {
|
|
1928
|
+
output("remember", { ok: true, ...result });
|
|
1929
|
+
}
|
|
1650
1930
|
});
|
|
1651
1931
|
},
|
|
1652
1932
|
});
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
return
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
content === parsedDetail &&
|
|
1665
|
-
wasRememberFlagValueConsumedAsContent(content, parsedDetail, "--detail")) {
|
|
1666
|
-
return undefined;
|
|
1933
|
+
/**
|
|
1934
|
+
* Best-effort top-3 similar memory search for `--show-similar`.
|
|
1935
|
+
* Scoped to memory: type; excludes the just-written ref.
|
|
1936
|
+
*/
|
|
1937
|
+
async function fetchSimilarMemories(query, excludeRef) {
|
|
1938
|
+
try {
|
|
1939
|
+
const result = await akmSearch({ query, type: "memory", limit: 4 });
|
|
1940
|
+
return (result.hits ?? [])
|
|
1941
|
+
.filter((h) => "ref" in h && h.ref !== excludeRef)
|
|
1942
|
+
.slice(0, 3)
|
|
1943
|
+
.map((h) => ({ ref: h.ref, ...(h.name ? { title: h.name } : {}) }));
|
|
1667
1944
|
}
|
|
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
|
-
}
|
|
1945
|
+
catch {
|
|
1946
|
+
return [];
|
|
1687
1947
|
}
|
|
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
1948
|
}
|
|
1697
1949
|
const importKnowledgeCommand = defineCommand({
|
|
1698
1950
|
meta: {
|
|
@@ -1782,10 +2034,11 @@ const helpCommand = defineCommand({
|
|
|
1782
2034
|
},
|
|
1783
2035
|
run({ args }) {
|
|
1784
2036
|
return runWithJsonErrors(() => {
|
|
1785
|
-
|
|
2037
|
+
const version = resolveHelpMigrateVersionArg(typeof args.version === "string" ? args.version : undefined);
|
|
2038
|
+
if (!version?.trim()) {
|
|
1786
2039
|
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
2040
|
}
|
|
1788
|
-
process.stdout.write(renderMigrationHelp(
|
|
2041
|
+
process.stdout.write(renderMigrationHelp(version));
|
|
1789
2042
|
});
|
|
1790
2043
|
},
|
|
1791
2044
|
}),
|
|
@@ -1815,8 +2068,8 @@ const completionsCommand = defineCommand({
|
|
|
1815
2068
|
const script = generateBashCompletions(main);
|
|
1816
2069
|
if (args.install) {
|
|
1817
2070
|
const dest = installBashCompletions(script);
|
|
1818
|
-
|
|
1819
|
-
|
|
2071
|
+
info(`Completions installed to ${dest}`);
|
|
2072
|
+
info(`Restart your shell or run: source ${dest}`);
|
|
1820
2073
|
}
|
|
1821
2074
|
else {
|
|
1822
2075
|
process.stdout.write(script);
|
|
@@ -1912,6 +2165,13 @@ function resolveVaultPath(ref) {
|
|
|
1912
2165
|
const source = findVaultSource(parsed.origin);
|
|
1913
2166
|
const typeRoot = path.join(source.path, "vaults");
|
|
1914
2167
|
const absPath = resolveAssetPathFromName("vault", typeRoot, parsed.name);
|
|
2168
|
+
// Defense-in-depth: ensure the resolved path stays inside the vaults directory.
|
|
2169
|
+
// validateName already rejects traversal patterns like "../../foo", but an
|
|
2170
|
+
// absolute-path override or symlink-based attack could still escape without
|
|
2171
|
+
// this second check.
|
|
2172
|
+
if (!isWithin(absPath, typeRoot)) {
|
|
2173
|
+
throw new UsageError(`Vault name "${parsed.name}" escapes the vault directory.`);
|
|
2174
|
+
}
|
|
1915
2175
|
return { name: parsed.name, absPath, source, parsedRef: parsed };
|
|
1916
2176
|
}
|
|
1917
2177
|
/**
|
|
@@ -1940,6 +2200,10 @@ function listVaultsRecursive(listKeysFn) {
|
|
|
1940
2200
|
const canonical = deriveCanonicalAssetName("vault", vaultsDir, full);
|
|
1941
2201
|
if (!canonical)
|
|
1942
2202
|
continue;
|
|
2203
|
+
// Skip sensitive vaults: presence of a sibling .sensitive marker file suppresses listing.
|
|
2204
|
+
const markerPath = full.replace(/\.env$/, ".sensitive");
|
|
2205
|
+
if (fs.existsSync(markerPath))
|
|
2206
|
+
continue;
|
|
1943
2207
|
const { keys } = listKeysFn(full);
|
|
1944
2208
|
result.push({ ref: makeVaultRef(canonical, source), path: full, keys });
|
|
1945
2209
|
}
|
|
@@ -1962,6 +2226,9 @@ function splitVaultRunTarget(target) {
|
|
|
1962
2226
|
if (!key) {
|
|
1963
2227
|
throw new UsageError("Expected vault run target in the form <ref> or <ref/KEY>.");
|
|
1964
2228
|
}
|
|
2229
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
2230
|
+
throw new UsageError(`"${key}" is not a valid environment variable name.`, "INVALID_FLAG_VALUE");
|
|
2231
|
+
}
|
|
1965
2232
|
const resolved = resolveVaultPath(refPart);
|
|
1966
2233
|
if (!fs.existsSync(resolved.absPath)) {
|
|
1967
2234
|
throw new NotFoundError(`Vault not found: ${makeVaultRef(resolved.name, resolved.source)}`);
|
|
@@ -1982,48 +2249,69 @@ const vaultCreateCommand = defineCommand({
|
|
|
1982
2249
|
meta: { name: "create", description: "Create an empty vault file (no-op if it already exists)" },
|
|
1983
2250
|
args: {
|
|
1984
2251
|
name: { type: "positional", description: "Vault name (e.g. prod) — file becomes <name>.env", required: true },
|
|
2252
|
+
sensitive: {
|
|
2253
|
+
type: "boolean",
|
|
2254
|
+
description: "Exclude this vault from vault list output and the search index",
|
|
2255
|
+
default: false,
|
|
2256
|
+
},
|
|
1985
2257
|
},
|
|
1986
2258
|
run({ args }) {
|
|
1987
2259
|
return runWithJsonErrors(async () => {
|
|
1988
2260
|
const { createVault } = await import("./commands/vault.js");
|
|
1989
2261
|
const { name, absPath, source } = resolveVaultPath(args.name);
|
|
1990
2262
|
createVault(absPath);
|
|
1991
|
-
|
|
2263
|
+
if (args.sensitive) {
|
|
2264
|
+
const markerPath = absPath.replace(/\.env$/, ".sensitive");
|
|
2265
|
+
if (!fs.existsSync(markerPath)) {
|
|
2266
|
+
fs.writeFileSync(markerPath, "", { mode: 0o600 });
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
output("vault-create", { ref: makeVaultRef(name, source) });
|
|
1992
2270
|
});
|
|
1993
2271
|
},
|
|
1994
2272
|
});
|
|
1995
2273
|
const vaultSetCommand = defineCommand({
|
|
1996
2274
|
meta: {
|
|
1997
2275
|
name: "set",
|
|
1998
|
-
description: 'Set a key in a vault. Value is
|
|
2276
|
+
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
2277
|
},
|
|
2000
2278
|
args: {
|
|
2001
2279
|
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
|
-
},
|
|
2280
|
+
key: { type: "positional", description: "Key name (e.g. DB_URL)", required: true },
|
|
2008
2281
|
comment: { type: "string", description: "Optional comment written above the key line", required: false },
|
|
2282
|
+
"from-env": {
|
|
2283
|
+
type: "string",
|
|
2284
|
+
description: "Read value from the named environment variable instead of stdin",
|
|
2285
|
+
},
|
|
2009
2286
|
},
|
|
2010
2287
|
run({ args }) {
|
|
2011
2288
|
return runWithJsonErrors(async () => {
|
|
2012
2289
|
const { setKey } = await import("./commands/vault.js");
|
|
2013
2290
|
const { name, absPath, source } = resolveVaultPath(args.ref);
|
|
2014
|
-
|
|
2291
|
+
const fromEnv = getHyphenatedArg(args, "from-env");
|
|
2015
2292
|
let realValue;
|
|
2016
|
-
if (
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
|
|
2293
|
+
if (fromEnv !== undefined) {
|
|
2294
|
+
const envVal = process.env[fromEnv];
|
|
2295
|
+
if (envVal === undefined) {
|
|
2296
|
+
throw new UsageError(`Environment variable "${fromEnv}" is not set.`, "INVALID_FLAG_VALUE");
|
|
2297
|
+
}
|
|
2298
|
+
realValue = envVal;
|
|
2020
2299
|
}
|
|
2021
2300
|
else {
|
|
2022
|
-
|
|
2023
|
-
|
|
2301
|
+
const MAX_VAULT_VALUE_BYTES = 1024 * 1024; // 1 MB
|
|
2302
|
+
let totalBytes = 0;
|
|
2303
|
+
const chunks = [];
|
|
2304
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
2305
|
+
totalBytes += chunk.byteLength;
|
|
2306
|
+
if (totalBytes > MAX_VAULT_VALUE_BYTES) {
|
|
2307
|
+
throw new UsageError("Vault value exceeds 1 MB limit. Values must be provided via stdin.");
|
|
2308
|
+
}
|
|
2309
|
+
chunks.push(chunk);
|
|
2310
|
+
}
|
|
2311
|
+
realValue = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
2024
2312
|
}
|
|
2025
|
-
setKey(absPath,
|
|
2026
|
-
output("vault-set", { ref: makeVaultRef(name, source), key:
|
|
2313
|
+
setKey(absPath, args.key, realValue, args.comment);
|
|
2314
|
+
output("vault-set", { ref: makeVaultRef(name, source), key: args.key });
|
|
2027
2315
|
});
|
|
2028
2316
|
},
|
|
2029
2317
|
});
|
|
@@ -2041,7 +2329,7 @@ const vaultUnsetCommand = defineCommand({
|
|
|
2041
2329
|
throw new NotFoundError(`Vault not found: ${makeVaultRef(name, source)}`);
|
|
2042
2330
|
}
|
|
2043
2331
|
const removed = unsetKey(absPath, args.key);
|
|
2044
|
-
output("vault-unset", { ref: makeVaultRef(name, source), key: args.key, removed
|
|
2332
|
+
output("vault-unset", { ref: makeVaultRef(name, source), key: args.key, removed });
|
|
2045
2333
|
});
|
|
2046
2334
|
},
|
|
2047
2335
|
});
|
|
@@ -2097,6 +2385,15 @@ const vaultRunCommand = defineCommand({
|
|
|
2097
2385
|
mergedEnv[envKey] = envValue;
|
|
2098
2386
|
}
|
|
2099
2387
|
}
|
|
2388
|
+
// Emit vault access event (keys only, no values) for audit trail.
|
|
2389
|
+
// Best-effort: never block vault run on event write failure.
|
|
2390
|
+
appendEvent({
|
|
2391
|
+
eventType: "vault_access",
|
|
2392
|
+
ref: makeVaultRef(name, source),
|
|
2393
|
+
metadata: {
|
|
2394
|
+
keys: key ? [key] : Object.keys(envValues),
|
|
2395
|
+
},
|
|
2396
|
+
});
|
|
2100
2397
|
const result = spawnSync(command[0], command.slice(1), {
|
|
2101
2398
|
stdio: "inherit",
|
|
2102
2399
|
env: mergedEnv,
|
|
@@ -2122,7 +2419,7 @@ const vaultCommand = defineCommand({
|
|
|
2122
2419
|
},
|
|
2123
2420
|
run({ args }) {
|
|
2124
2421
|
return runWithJsonErrors(async () => {
|
|
2125
|
-
if (
|
|
2422
|
+
if (hasSubcommand(args, VAULT_SUBCOMMAND_SET))
|
|
2126
2423
|
return;
|
|
2127
2424
|
// Default action: list all vaults
|
|
2128
2425
|
const { listKeys } = await import("./commands/vault.js");
|
|
@@ -2286,12 +2583,39 @@ const wikiStashCommand = defineCommand({
|
|
|
2286
2583
|
name: { type: "positional", description: "Wiki name", required: true },
|
|
2287
2584
|
source: { type: "positional", description: "Source file path, URL, or '-' to read from stdin", required: true },
|
|
2288
2585
|
as: { type: "string", description: "Preferred slug base (defaults to source filename or first-line slug)" },
|
|
2586
|
+
target: {
|
|
2587
|
+
type: "string",
|
|
2588
|
+
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).",
|
|
2589
|
+
},
|
|
2289
2590
|
},
|
|
2290
2591
|
run({ args }) {
|
|
2291
2592
|
return runWithJsonErrors(async () => {
|
|
2292
2593
|
const { stashRaw } = await import("./wiki/wiki.js");
|
|
2293
|
-
const { content, preferredName } = await
|
|
2294
|
-
|
|
2594
|
+
const { content, preferredName } = await (async () => {
|
|
2595
|
+
if (!isHttpUrl(args.source))
|
|
2596
|
+
return readKnowledgeInput(args.source);
|
|
2597
|
+
const { fetchWebsiteMarkdownSnapshot } = await import("./sources/website-ingest");
|
|
2598
|
+
const snapshot = await fetchWebsiteMarkdownSnapshot(args.source);
|
|
2599
|
+
return { content: snapshot.content, preferredName: args.as ?? snapshot.preferredName };
|
|
2600
|
+
})();
|
|
2601
|
+
let stashDir;
|
|
2602
|
+
if (args.target) {
|
|
2603
|
+
// Resolve the named source to its filesystem path.
|
|
2604
|
+
const cfg = loadConfig();
|
|
2605
|
+
const sources = resolveConfiguredSources(cfg);
|
|
2606
|
+
const match = sources.find((s) => s.name === args.target);
|
|
2607
|
+
if (!match) {
|
|
2608
|
+
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");
|
|
2609
|
+
}
|
|
2610
|
+
const spec = match.source;
|
|
2611
|
+
if (spec.type !== "filesystem" && spec.type !== "local") {
|
|
2612
|
+
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.`);
|
|
2613
|
+
}
|
|
2614
|
+
stashDir = spec.path;
|
|
2615
|
+
}
|
|
2616
|
+
else {
|
|
2617
|
+
stashDir = resolveStashDir();
|
|
2618
|
+
}
|
|
2295
2619
|
const result = stashRaw({
|
|
2296
2620
|
stashDir,
|
|
2297
2621
|
wikiName: args.name,
|
|
@@ -2360,7 +2684,7 @@ const wikiCommand = defineCommand({
|
|
|
2360
2684
|
},
|
|
2361
2685
|
run({ args }) {
|
|
2362
2686
|
return runWithJsonErrors(async () => {
|
|
2363
|
-
if (
|
|
2687
|
+
if (hasSubcommand(args, WIKI_SUBCOMMAND_SET))
|
|
2364
2688
|
return;
|
|
2365
2689
|
// Default action: list wikis
|
|
2366
2690
|
const { listWikis } = await import("./wiki/wiki.js");
|
|
@@ -2369,15 +2693,15 @@ const wikiCommand = defineCommand({
|
|
|
2369
2693
|
},
|
|
2370
2694
|
});
|
|
2371
2695
|
// ── `akm events` ────────────────────────────────────────────────────────────
|
|
2372
|
-
// Append-only events stream surface (#204). `list` reads
|
|
2373
|
-
// with optional --since/--type/--ref filters; `tail` follows the
|
|
2696
|
+
// Append-only events stream surface (#204). `list` reads state.db events
|
|
2697
|
+
// with optional --since/--type/--ref filters; `tail` follows the table via
|
|
2374
2698
|
// a polling loop and prints each event as a single JSONL line.
|
|
2375
2699
|
const eventsListCommand = defineCommand({
|
|
2376
|
-
meta: { name: "list", description: "List events from the append-only
|
|
2700
|
+
meta: { name: "list", description: "List events from the append-only state.db events stream" },
|
|
2377
2701
|
args: {
|
|
2378
2702
|
since: {
|
|
2379
2703
|
type: "string",
|
|
2380
|
-
description: "ISO timestamp / epoch ms, OR `@offset:<
|
|
2704
|
+
description: "ISO timestamp / epoch ms, OR `@offset:<id>` for a durable row-id cursor (resume across processes)",
|
|
2381
2705
|
},
|
|
2382
2706
|
type: { type: "string", description: "Filter by event type (add, remove, remember, feedback, ...)" },
|
|
2383
2707
|
ref: { type: "string", description: "Filter by asset ref (type:name)" },
|
|
@@ -2406,11 +2730,11 @@ const eventsListCommand = defineCommand({
|
|
|
2406
2730
|
},
|
|
2407
2731
|
});
|
|
2408
2732
|
const eventsTailCommand = defineCommand({
|
|
2409
|
-
meta: { name: "tail", description: "Follow the append-only
|
|
2733
|
+
meta: { name: "tail", description: "Follow the append-only state.db events stream (polling)" },
|
|
2410
2734
|
args: {
|
|
2411
2735
|
since: {
|
|
2412
2736
|
type: "string",
|
|
2413
|
-
description: "ISO timestamp / epoch ms, OR `@offset:<
|
|
2737
|
+
description: "ISO timestamp / epoch ms, OR `@offset:<id>` for a durable row-id cursor (resume across processes)",
|
|
2414
2738
|
},
|
|
2415
2739
|
type: { type: "string", description: "Filter by event type" },
|
|
2416
2740
|
ref: { type: "string", description: "Filter by asset ref (type:name)" },
|
|
@@ -2428,9 +2752,9 @@ const eventsTailCommand = defineCommand({
|
|
|
2428
2752
|
},
|
|
2429
2753
|
async run({ args }) {
|
|
2430
2754
|
await runWithJsonErrors(async () => {
|
|
2431
|
-
const intervalMs =
|
|
2432
|
-
const maxDurationMs =
|
|
2433
|
-
const maxEvents =
|
|
2755
|
+
const intervalMs = parsePositiveIntFlag(getHyphenatedArg(args, "interval-ms"), "--interval-ms");
|
|
2756
|
+
const maxDurationMs = parsePositiveIntFlag(getHyphenatedArg(args, "max-duration-ms"), "--max-duration-ms");
|
|
2757
|
+
const maxEvents = parsePositiveIntFlag(getHyphenatedArg(args, "max-events"), "--max-events");
|
|
2434
2758
|
const mode = getOutputMode();
|
|
2435
2759
|
// In streaming text mode we want each event to print as soon as it
|
|
2436
2760
|
// arrives. The polling loop emits via `onEvent`; the final result is
|
|
@@ -2484,22 +2808,10 @@ const eventsTailCommand = defineCommand({
|
|
|
2484
2808
|
});
|
|
2485
2809
|
},
|
|
2486
2810
|
});
|
|
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
2811
|
const eventsCommand = defineCommand({
|
|
2500
2812
|
meta: {
|
|
2501
2813
|
name: "events",
|
|
2502
|
-
description: "Read or follow the append-only
|
|
2814
|
+
description: "Read or follow the append-only state.db events stream (mutations, feedback, indexing)",
|
|
2503
2815
|
},
|
|
2504
2816
|
subCommands: {
|
|
2505
2817
|
list: eventsListCommand,
|
|
@@ -2507,16 +2819,12 @@ const eventsCommand = defineCommand({
|
|
|
2507
2819
|
},
|
|
2508
2820
|
});
|
|
2509
2821
|
// ── proposal substrate (#225) ────────────────────────────────────────────────
|
|
2510
|
-
const
|
|
2511
|
-
meta: { name: "
|
|
2822
|
+
const proposalsCommand = defineCommand({
|
|
2823
|
+
meta: { name: "proposals", description: "List proposal queue entries" },
|
|
2512
2824
|
args: {
|
|
2513
2825
|
status: { type: "string", description: "Filter by status (pending|accepted|rejected)" },
|
|
2514
2826
|
ref: { type: "string", description: "Filter by asset ref (type:name)" },
|
|
2515
|
-
"
|
|
2516
|
-
type: "boolean",
|
|
2517
|
-
description: "Include accepted/rejected proposals from the archive",
|
|
2518
|
-
default: false,
|
|
2519
|
-
},
|
|
2827
|
+
type: { type: "string", description: "Filter by asset type" },
|
|
2520
2828
|
},
|
|
2521
2829
|
run({ args }) {
|
|
2522
2830
|
return runWithJsonErrors(() => {
|
|
@@ -2524,28 +2832,20 @@ const proposalListCommand = defineCommand({
|
|
|
2524
2832
|
const result = akmProposalList({
|
|
2525
2833
|
status,
|
|
2526
2834
|
ref: args.ref,
|
|
2527
|
-
includeArchive:
|
|
2835
|
+
includeArchive: status === "accepted" || status === "rejected",
|
|
2528
2836
|
});
|
|
2529
2837
|
output("proposal-list", result);
|
|
2530
2838
|
});
|
|
2531
2839
|
},
|
|
2532
2840
|
});
|
|
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" },
|
|
2841
|
+
const acceptCommand = defineCommand({
|
|
2842
|
+
meta: { name: "accept", description: "Accept a proposal and promote it into the stash" },
|
|
2547
2843
|
args: {
|
|
2548
|
-
id: {
|
|
2844
|
+
id: {
|
|
2845
|
+
type: "positional",
|
|
2846
|
+
description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream)",
|
|
2847
|
+
required: true,
|
|
2848
|
+
},
|
|
2549
2849
|
target: { type: "string", description: "Override the write target by source name" },
|
|
2550
2850
|
},
|
|
2551
2851
|
async run({ args }) {
|
|
@@ -2555,23 +2855,34 @@ const proposalAcceptCommand = defineCommand({
|
|
|
2555
2855
|
});
|
|
2556
2856
|
},
|
|
2557
2857
|
});
|
|
2558
|
-
const
|
|
2559
|
-
meta: { name: "reject", description: "
|
|
2858
|
+
const rejectCommand = defineCommand({
|
|
2859
|
+
meta: { name: "reject", description: "Reject a proposal and record the reason" },
|
|
2560
2860
|
args: {
|
|
2561
|
-
id: {
|
|
2562
|
-
|
|
2861
|
+
id: {
|
|
2862
|
+
type: "positional",
|
|
2863
|
+
description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream)",
|
|
2864
|
+
required: true,
|
|
2865
|
+
},
|
|
2866
|
+
reason: { type: "string", description: "Reason for rejection (required)" },
|
|
2563
2867
|
},
|
|
2564
2868
|
run({ args }) {
|
|
2565
2869
|
return runWithJsonErrors(() => {
|
|
2870
|
+
if (!args.reason || !String(args.reason).trim()) {
|
|
2871
|
+
throw new UsageError("Usage: akm reject <id> --reason '<reason>'", "MISSING_REQUIRED_ARGUMENT");
|
|
2872
|
+
}
|
|
2566
2873
|
const result = akmProposalReject({ id: args.id, reason: args.reason });
|
|
2567
2874
|
output("proposal-reject", result);
|
|
2568
2875
|
});
|
|
2569
2876
|
},
|
|
2570
2877
|
});
|
|
2571
|
-
const
|
|
2572
|
-
meta: { name: "diff", description: "Show the diff
|
|
2878
|
+
const diffCommand = defineCommand({
|
|
2879
|
+
meta: { name: "diff", description: "Show the diff for a proposal (accepts full UUID, UUID prefix, or asset ref)" },
|
|
2573
2880
|
args: {
|
|
2574
|
-
id: {
|
|
2881
|
+
id: {
|
|
2882
|
+
type: "positional",
|
|
2883
|
+
description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream)",
|
|
2884
|
+
required: true,
|
|
2885
|
+
},
|
|
2575
2886
|
target: { type: "string", description: "Override the write target by source name" },
|
|
2576
2887
|
},
|
|
2577
2888
|
run({ args }) {
|
|
@@ -2581,112 +2892,7 @@ const proposalDiffCommand = defineCommand({
|
|
|
2581
2892
|
});
|
|
2582
2893
|
},
|
|
2583
2894
|
});
|
|
2584
|
-
const proposalCommand = defineCommand({
|
|
2585
|
-
meta: {
|
|
2586
|
-
name: "proposal",
|
|
2587
|
-
description: "Review and promote queued asset proposals (durable storage under .akm/proposals/)",
|
|
2588
|
-
},
|
|
2589
|
-
subCommands: {
|
|
2590
|
-
list: proposalListCommand,
|
|
2591
|
-
show: proposalShowCommand,
|
|
2592
|
-
accept: proposalAcceptCommand,
|
|
2593
|
-
reject: proposalRejectCommand,
|
|
2594
|
-
diff: proposalDiffCommand,
|
|
2595
|
-
},
|
|
2596
|
-
});
|
|
2597
2895
|
// ── distill (#228) ──────────────────────────────────────────────────────────
|
|
2598
|
-
const distillCommand = defineCommand({
|
|
2599
|
-
meta: {
|
|
2600
|
-
name: "distill",
|
|
2601
|
-
description: "Distil feedback for an asset into a queued lesson proposal (gated on llm.features.feedback_distillation)",
|
|
2602
|
-
},
|
|
2603
|
-
args: {
|
|
2604
|
-
ref: { type: "positional", description: "Asset ref (type:name) to distil from", required: true },
|
|
2605
|
-
"source-run": {
|
|
2606
|
-
type: "string",
|
|
2607
|
-
description: "Optional run id propagated onto the queued proposal for traceability",
|
|
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)",
|
|
2620
|
-
},
|
|
2621
|
-
},
|
|
2622
|
-
async run({ args }) {
|
|
2623
|
-
await runWithJsonErrors(async () => {
|
|
2624
|
-
const excludeFlag = getHyphenatedArg(args, "exclude-feedback-from");
|
|
2625
|
-
const excludeEnv = process.env.AKM_DISTILL_EXCLUDE_FEEDBACK_FROM;
|
|
2626
|
-
// CLI flag takes precedence over the env var when both are present.
|
|
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 } : {}),
|
|
2661
|
-
});
|
|
2662
|
-
output("distill", result);
|
|
2663
|
-
});
|
|
2664
|
-
},
|
|
2665
|
-
});
|
|
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
|
-
}
|
|
2690
2896
|
function parseProposalStatus(raw) {
|
|
2691
2897
|
if (raw === undefined)
|
|
2692
2898
|
return undefined;
|
|
@@ -2697,39 +2903,232 @@ function parseProposalStatus(raw) {
|
|
|
2697
2903
|
return trimmed;
|
|
2698
2904
|
throw new UsageError(`Invalid --status value: "${raw}". Expected one of: pending, accepted, rejected.`, "INVALID_FLAG_VALUE");
|
|
2699
2905
|
}
|
|
2700
|
-
|
|
2701
|
-
const reflectCommand = defineCommand({
|
|
2906
|
+
const agentCommand = defineCommand({
|
|
2702
2907
|
meta: {
|
|
2703
|
-
name: "
|
|
2704
|
-
description: "
|
|
2908
|
+
name: "agent",
|
|
2909
|
+
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
2910
|
},
|
|
2706
2911
|
args: {
|
|
2707
|
-
|
|
2912
|
+
profile: {
|
|
2708
2913
|
type: "positional",
|
|
2709
|
-
description: "
|
|
2914
|
+
description: "Agent profile / platform to use (opencode, claude, …)",
|
|
2710
2915
|
required: false,
|
|
2711
2916
|
},
|
|
2712
|
-
|
|
2713
|
-
|
|
2917
|
+
"agent-ref": {
|
|
2918
|
+
type: "positional",
|
|
2919
|
+
description: "Optional agent asset ref (e.g. agent:code-reviewer). Loads system prompt, model, and tool policy from the stash asset.",
|
|
2920
|
+
required: false,
|
|
2921
|
+
},
|
|
2922
|
+
prompt: { type: "string", description: "Task prompt to pass to the agent" },
|
|
2923
|
+
command: { type: "string", description: "Load prompt from a command: asset" },
|
|
2924
|
+
workflow: { type: "string", description: "Load prompt from a workflow: asset" },
|
|
2925
|
+
model: {
|
|
2926
|
+
type: "string",
|
|
2927
|
+
description: "Model override — accepts aliases (opus, sonnet, haiku) or exact platform model IDs. Overrides the model specified in the agent asset.",
|
|
2928
|
+
},
|
|
2714
2929
|
"timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds" },
|
|
2715
2930
|
},
|
|
2716
2931
|
async run({ args }) {
|
|
2717
2932
|
await runWithJsonErrors(async () => {
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2933
|
+
if (!args.profile) {
|
|
2934
|
+
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 config.agent.profiles.");
|
|
2935
|
+
}
|
|
2936
|
+
const timeoutMs = parsePositiveIntFlag(getHyphenatedArg(args, "timeout-ms"), "--timeout-ms");
|
|
2937
|
+
const config = loadConfig();
|
|
2938
|
+
const { parseAgentConfig } = await import("./integrations/agent/config.js");
|
|
2939
|
+
const agentConfig = parseAgentConfig(config.agent);
|
|
2940
|
+
// Resolve agent asset ref → extract system prompt, model, and tool policy.
|
|
2941
|
+
const agentRef = getStringArg(args, "agent-ref");
|
|
2942
|
+
let systemPrompt;
|
|
2943
|
+
let assetModel;
|
|
2944
|
+
let assetTools;
|
|
2945
|
+
if (agentRef) {
|
|
2946
|
+
const { akmShowUnified } = await import("./commands/show.js");
|
|
2947
|
+
const asset = await akmShowUnified({ ref: agentRef, detail: "full" });
|
|
2948
|
+
systemPrompt = typeof asset.content === "string" ? asset.content : undefined;
|
|
2949
|
+
assetModel = typeof asset.modelHint === "string" ? asset.modelHint : undefined;
|
|
2950
|
+
assetTools = asset.toolPolicy;
|
|
2951
|
+
}
|
|
2952
|
+
// --model flag wins over the asset's modelHint.
|
|
2953
|
+
const model = getStringArg(args, "model") ?? assetModel;
|
|
2954
|
+
const promptText = getStringArg(args, "prompt");
|
|
2955
|
+
const commandRef = getStringArg(args, "command");
|
|
2956
|
+
const workflowRef = getStringArg(args, "workflow");
|
|
2957
|
+
// Only build a dispatch request when there is something to dispatch — a
|
|
2958
|
+
// prompt, an agent asset, or a model override. When none of these are
|
|
2959
|
+
// present the agent is launched interactively (no injected prompt, no
|
|
2960
|
+
// platform-specific flags beyond the profile's base args).
|
|
2961
|
+
const hasDispatchContent = !!(promptText ?? commandRef ?? workflowRef ?? systemPrompt ?? model ?? assetTools);
|
|
2962
|
+
const result = await akmAgentDispatch({
|
|
2963
|
+
profileName: String(args.profile),
|
|
2964
|
+
prompt: promptText,
|
|
2965
|
+
commandRef,
|
|
2966
|
+
workflowRef,
|
|
2967
|
+
agentConfig,
|
|
2968
|
+
llmConfig: config.llm,
|
|
2969
|
+
...(hasDispatchContent
|
|
2970
|
+
? {
|
|
2971
|
+
dispatch: {
|
|
2972
|
+
prompt: promptText ?? "",
|
|
2973
|
+
systemPrompt,
|
|
2974
|
+
model,
|
|
2975
|
+
tools: assetTools,
|
|
2976
|
+
},
|
|
2977
|
+
}
|
|
2978
|
+
: {}),
|
|
2724
2979
|
...(timeoutMs !== undefined && Number.isFinite(timeoutMs) ? { timeoutMs } : {}),
|
|
2725
2980
|
});
|
|
2726
|
-
output("
|
|
2727
|
-
if (result.ok
|
|
2981
|
+
output("agent-result", result);
|
|
2982
|
+
if (!result.ok) {
|
|
2728
2983
|
process.exit(EXIT_GENERAL);
|
|
2729
2984
|
}
|
|
2730
2985
|
});
|
|
2731
2986
|
},
|
|
2732
2987
|
});
|
|
2988
|
+
const lintCommand = defineCommand({
|
|
2989
|
+
meta: {
|
|
2990
|
+
name: "lint",
|
|
2991
|
+
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.",
|
|
2992
|
+
},
|
|
2993
|
+
args: {
|
|
2994
|
+
fix: { type: "boolean", description: "Apply auto-fixes in place", default: false },
|
|
2995
|
+
dir: { type: "string", description: "Override stash root directory (default: from config)" },
|
|
2996
|
+
},
|
|
2997
|
+
async run({ args }) {
|
|
2998
|
+
await runWithJsonErrors(async () => {
|
|
2999
|
+
const result = akmLint({
|
|
3000
|
+
fix: args.fix ?? false,
|
|
3001
|
+
dir: getStringArg(args, "dir"),
|
|
3002
|
+
});
|
|
3003
|
+
output("lint", result);
|
|
3004
|
+
if (!result.ok)
|
|
3005
|
+
process.exit(EXIT_GENERAL);
|
|
3006
|
+
});
|
|
3007
|
+
},
|
|
3008
|
+
});
|
|
3009
|
+
const improveCommand = defineCommand({
|
|
3010
|
+
meta: {
|
|
3011
|
+
name: "improve",
|
|
3012
|
+
description: "Analyze existing AKM assets and generate improvement proposals; also consolidates memories when llm.features.memory_consolidation is enabled",
|
|
3013
|
+
},
|
|
3014
|
+
args: {
|
|
3015
|
+
scope: {
|
|
3016
|
+
type: "positional",
|
|
3017
|
+
description: "Optional asset type or asset ref to improve",
|
|
3018
|
+
required: false,
|
|
3019
|
+
},
|
|
3020
|
+
task: { type: "string", description: "Add extra guidance for this improvement pass" },
|
|
3021
|
+
"dry-run": { type: "boolean", description: "Show planned actions without writing", default: false },
|
|
3022
|
+
target: { type: "string", description: "Override the write target for accepted proposals" },
|
|
3023
|
+
"auto-accept": {
|
|
3024
|
+
type: "string",
|
|
3025
|
+
description: "Automatically accept low-risk proposals (only 'safe' is supported)",
|
|
3026
|
+
},
|
|
3027
|
+
limit: { type: "string", description: "Maximum number of assets to process (highest utility first)" },
|
|
3028
|
+
"timeout-ms": {
|
|
3029
|
+
type: "string",
|
|
3030
|
+
description: "Wall-clock budget for the entire run in milliseconds (default: 7200000 = 2 hours)",
|
|
3031
|
+
},
|
|
3032
|
+
"ignore-cooldown": {
|
|
3033
|
+
type: "boolean",
|
|
3034
|
+
description: "Ignore all cooldown periods (equivalent to --reflect-cooldown-days 0 --distill-cooldown-days 0 --consolidate-cooldown-days 0)",
|
|
3035
|
+
default: false,
|
|
3036
|
+
},
|
|
3037
|
+
"reflect-cooldown-days": {
|
|
3038
|
+
type: "string",
|
|
3039
|
+
description: "Override reflect cooldown for this run only (default: 7, 0 to disable)",
|
|
3040
|
+
},
|
|
3041
|
+
"distill-cooldown-days": {
|
|
3042
|
+
type: "string",
|
|
3043
|
+
description: "Override distill cooldown for this run only (default: 30, 0 to disable)",
|
|
3044
|
+
},
|
|
3045
|
+
"consolidate-cooldown-days": {
|
|
3046
|
+
type: "string",
|
|
3047
|
+
description: "Override consolidate cooldown for this run only (default: 14, 0 to disable)",
|
|
3048
|
+
},
|
|
3049
|
+
"consolidate-recovery": {
|
|
3050
|
+
type: "string",
|
|
3051
|
+
description: "How to handle stale/incomplete consolidation journals: abort (default) or clean (remove stale journal artifacts)",
|
|
3052
|
+
},
|
|
3053
|
+
"require-feedback-signal": {
|
|
3054
|
+
type: "boolean",
|
|
3055
|
+
description: "Only process assets with recent feedback signals (disables retrieval fallback)",
|
|
3056
|
+
default: false,
|
|
3057
|
+
},
|
|
3058
|
+
"min-retrieval-count": {
|
|
3059
|
+
type: "string",
|
|
3060
|
+
description: "Minimum retrieval count for zero-feedback fallback eligibility (default: 5)",
|
|
3061
|
+
},
|
|
3062
|
+
},
|
|
3063
|
+
async run({ args }) {
|
|
3064
|
+
await runWithJsonErrors(async () => {
|
|
3065
|
+
const autoAcceptRaw = getHyphenatedArg(args, "auto-accept");
|
|
3066
|
+
if (autoAcceptRaw !== undefined && autoAcceptRaw !== "safe") {
|
|
3067
|
+
throw new UsageError("--auto-accept only supports the value 'safe'.", "INVALID_FLAG_VALUE");
|
|
3068
|
+
}
|
|
3069
|
+
const targetArg = getStringArg(args, "target");
|
|
3070
|
+
const taskArg = getStringArg(args, "task");
|
|
3071
|
+
const dryRun = getHyphenatedBoolean(args, "dry-run");
|
|
3072
|
+
const autoAccept = autoAcceptRaw === "safe" ? "safe" : undefined;
|
|
3073
|
+
const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
|
|
3074
|
+
const timeoutMs = parsePositiveIntFlag(getHyphenatedArg(args, "timeout-ms"), "--timeout-ms");
|
|
3075
|
+
const ignoreCooldown = getHyphenatedBoolean(args, "ignore-cooldown");
|
|
3076
|
+
const reflectCooldownRaw = getHyphenatedArg(args, "reflect-cooldown-days");
|
|
3077
|
+
const reflectCooldownDays = ignoreCooldown
|
|
3078
|
+
? 0
|
|
3079
|
+
: parseNonNegativeIntFlag(reflectCooldownRaw, "--reflect-cooldown-days");
|
|
3080
|
+
const distillCooldownRaw = getHyphenatedArg(args, "distill-cooldown-days");
|
|
3081
|
+
const distillCooldownDays = ignoreCooldown
|
|
3082
|
+
? 0
|
|
3083
|
+
: parseNonNegativeIntFlag(distillCooldownRaw, "--distill-cooldown-days");
|
|
3084
|
+
const consolidateCooldownRaw = getHyphenatedArg(args, "consolidate-cooldown-days");
|
|
3085
|
+
const consolidateCooldownDays = ignoreCooldown
|
|
3086
|
+
? 0
|
|
3087
|
+
: parseNonNegativeIntFlag(consolidateCooldownRaw, "--consolidate-cooldown-days");
|
|
3088
|
+
const consolidateRecoveryRaw = getHyphenatedArg(args, "consolidate-recovery");
|
|
3089
|
+
const consolidateRecovery = consolidateRecoveryRaw === undefined
|
|
3090
|
+
? undefined
|
|
3091
|
+
: consolidateRecoveryRaw.trim().toLowerCase();
|
|
3092
|
+
if (consolidateRecovery !== undefined && consolidateRecovery !== "abort" && consolidateRecovery !== "clean") {
|
|
3093
|
+
throw new UsageError(`Invalid --consolidate-recovery value: "${consolidateRecoveryRaw}". Must be one of: abort, clean.`, "INVALID_FLAG_VALUE");
|
|
3094
|
+
}
|
|
3095
|
+
const minRetrievalCountRaw = getHyphenatedArg(args, "min-retrieval-count");
|
|
3096
|
+
const minRetrievalCount = parseNonNegativeIntFlag(minRetrievalCountRaw, "--min-retrieval-count");
|
|
3097
|
+
const requireFeedbackSignal = getHyphenatedBoolean(args, "require-feedback-signal");
|
|
3098
|
+
const improveLogFile = path.join(getCacheDir(), "logs", "improve", `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
|
|
3099
|
+
setLogFile(improveLogFile);
|
|
3100
|
+
let improveResult;
|
|
3101
|
+
try {
|
|
3102
|
+
improveResult = await akmImprove({
|
|
3103
|
+
scope: getStringArg(args, "scope"),
|
|
3104
|
+
task: taskArg,
|
|
3105
|
+
dryRun,
|
|
3106
|
+
target: targetArg,
|
|
3107
|
+
autoAccept,
|
|
3108
|
+
...(limitRaw !== undefined ? { limit: limitRaw } : {}),
|
|
3109
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
3110
|
+
...(reflectCooldownDays !== undefined ? { reflectCooldownDays } : {}),
|
|
3111
|
+
...(distillCooldownDays !== undefined ? { distillCooldownDays } : {}),
|
|
3112
|
+
...(consolidateCooldownDays !== undefined ? { consolidateCooldownDays } : {}),
|
|
3113
|
+
...(minRetrievalCount !== undefined ? { minRetrievalCount } : {}),
|
|
3114
|
+
...(requireFeedbackSignal ? { requireFeedbackSignal } : {}),
|
|
3115
|
+
consolidateOptions: {
|
|
3116
|
+
target: targetArg,
|
|
3117
|
+
dryRun,
|
|
3118
|
+
autoAccept,
|
|
3119
|
+
task: taskArg,
|
|
3120
|
+
...(consolidateRecovery !== undefined ? { recoveryMode: consolidateRecovery } : {}),
|
|
3121
|
+
},
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
finally {
|
|
3125
|
+
clearLogFile();
|
|
3126
|
+
}
|
|
3127
|
+
output("improve", improveResult);
|
|
3128
|
+
process.exit(0);
|
|
3129
|
+
});
|
|
3130
|
+
},
|
|
3131
|
+
});
|
|
2733
3132
|
const proposeCommand = defineCommand({
|
|
2734
3133
|
meta: {
|
|
2735
3134
|
name: "propose",
|
|
@@ -2742,6 +3141,7 @@ const proposeCommand = defineCommand({
|
|
|
2742
3141
|
type: { type: "positional", description: "Asset type (skill, command, knowledge, lesson, ...)", required: false },
|
|
2743
3142
|
name: { type: "positional", description: "Asset name (slug or path under the type dir)", required: false },
|
|
2744
3143
|
task: { type: "string", description: "Task description for the agent (what should the asset do?)" },
|
|
3144
|
+
file: { type: "string", description: "Read the task or prompt text from a UTF-8 file" },
|
|
2745
3145
|
profile: { type: "string", description: "Override the agent profile (defaults to agent.default)" },
|
|
2746
3146
|
"timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds" },
|
|
2747
3147
|
},
|
|
@@ -2750,17 +3150,22 @@ const proposeCommand = defineCommand({
|
|
|
2750
3150
|
// citty silently shows help and exits 0 when required positionals are
|
|
2751
3151
|
// omitted. Re-validate explicitly so the exit code is 2 (USAGE) and a
|
|
2752
3152
|
// structured JSON error reaches scripted callers.
|
|
2753
|
-
|
|
2754
|
-
|
|
3153
|
+
const taskFromFlag = typeof args.task === "string" ? args.task : undefined;
|
|
3154
|
+
const fileFromFlag = typeof args.file === "string" ? args.file : undefined;
|
|
3155
|
+
if (!args.type || !args.name || (!taskFromFlag && !fileFromFlag)) {
|
|
3156
|
+
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.");
|
|
3157
|
+
}
|
|
3158
|
+
if (taskFromFlag && fileFromFlag) {
|
|
3159
|
+
throw new UsageError("Pass exactly one of --task or --file.", "INVALID_FLAG_VALUE");
|
|
2755
3160
|
}
|
|
2756
|
-
const
|
|
2757
|
-
const timeoutMs =
|
|
3161
|
+
const taskText = fileFromFlag ? fs.readFileSync(path.resolve(fileFromFlag), "utf8") : (taskFromFlag ?? "");
|
|
3162
|
+
const timeoutMs = parsePositiveIntFlag(getHyphenatedArg(args, "timeout-ms"), "--timeout-ms");
|
|
2758
3163
|
const result = await akmPropose({
|
|
2759
3164
|
type: String(args.type),
|
|
2760
3165
|
name: String(args.name),
|
|
2761
|
-
task:
|
|
2762
|
-
profile:
|
|
2763
|
-
...(timeoutMs !== undefined
|
|
3166
|
+
task: taskText,
|
|
3167
|
+
profile: getStringArg(args, "profile"),
|
|
3168
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
2764
3169
|
});
|
|
2765
3170
|
output("propose", result);
|
|
2766
3171
|
if (result.ok === false) {
|
|
@@ -2769,6 +3174,189 @@ const proposeCommand = defineCommand({
|
|
|
2769
3174
|
});
|
|
2770
3175
|
},
|
|
2771
3176
|
});
|
|
3177
|
+
const TASKS_SUBCOMMAND_SET = new Set([
|
|
3178
|
+
"add",
|
|
3179
|
+
"list",
|
|
3180
|
+
"show",
|
|
3181
|
+
"remove",
|
|
3182
|
+
"enable",
|
|
3183
|
+
"disable",
|
|
3184
|
+
"run",
|
|
3185
|
+
"history",
|
|
3186
|
+
"sync",
|
|
3187
|
+
"doctor",
|
|
3188
|
+
]);
|
|
3189
|
+
const GRAPH_SUBCOMMAND_SET = new Set(["summary", "entities", "relations", "related", "export"]);
|
|
3190
|
+
const tasksAddCommand = defineCommand({
|
|
3191
|
+
meta: { name: "add", description: "Register a new scheduled task and install it in the OS scheduler" },
|
|
3192
|
+
args: {
|
|
3193
|
+
id: { type: "positional", description: "Task id (used as filename and scheduler entry)", required: true },
|
|
3194
|
+
schedule: { type: "string", description: 'Cron-style schedule, e.g. "0 9 * * *" or "@daily"', required: true },
|
|
3195
|
+
workflow: { type: "string", description: "Workflow ref to invoke (e.g. workflow:my-flow)" },
|
|
3196
|
+
prompt: {
|
|
3197
|
+
type: "string",
|
|
3198
|
+
description: "Prompt for the configured agent harness — inline text, an asset ref like agent:foo, or ./path.md",
|
|
3199
|
+
},
|
|
3200
|
+
profile: { type: "string", description: "Agent profile to use for prompt targets (default: config.agent.default)" },
|
|
3201
|
+
params: { type: "string", description: "Workflow params as a JSON object" },
|
|
3202
|
+
description: { type: "string", description: "Human-readable description" },
|
|
3203
|
+
tags: { type: "string", description: "Comma-separated tags" },
|
|
3204
|
+
disabled: { type: "boolean", description: "Register but leave disabled in the OS scheduler", default: false },
|
|
3205
|
+
force: { type: "boolean", description: "Overwrite an existing task with the same id", default: false },
|
|
3206
|
+
},
|
|
3207
|
+
async run({ args }) {
|
|
3208
|
+
await runWithJsonErrors(async () => {
|
|
3209
|
+
const result = await akmTasksAdd({
|
|
3210
|
+
id: args.id,
|
|
3211
|
+
schedule: args.schedule,
|
|
3212
|
+
workflow: args.workflow,
|
|
3213
|
+
prompt: args.prompt,
|
|
3214
|
+
profile: args.profile,
|
|
3215
|
+
params: args.params,
|
|
3216
|
+
description: args.description,
|
|
3217
|
+
tags: args.tags
|
|
3218
|
+
? args.tags
|
|
3219
|
+
.split(/[\s,]+/)
|
|
3220
|
+
.map((s) => s.trim())
|
|
3221
|
+
.filter(Boolean)
|
|
3222
|
+
: undefined,
|
|
3223
|
+
disabled: args.disabled === true,
|
|
3224
|
+
force: args.force === true,
|
|
3225
|
+
});
|
|
3226
|
+
output("tasks-add", result);
|
|
3227
|
+
});
|
|
3228
|
+
},
|
|
3229
|
+
});
|
|
3230
|
+
const tasksListCommand = defineCommand({
|
|
3231
|
+
meta: { name: "list", description: "List scheduled tasks in the stash" },
|
|
3232
|
+
async run() {
|
|
3233
|
+
await runWithJsonErrors(async () => {
|
|
3234
|
+
const result = await akmTasksList();
|
|
3235
|
+
output("tasks-list", result);
|
|
3236
|
+
});
|
|
3237
|
+
},
|
|
3238
|
+
});
|
|
3239
|
+
const tasksShowCommand = defineCommand({
|
|
3240
|
+
meta: { name: "show", description: "Show a parsed task definition" },
|
|
3241
|
+
args: { id: { type: "positional", description: "Task id or task:<id>", required: true } },
|
|
3242
|
+
async run({ args }) {
|
|
3243
|
+
await runWithJsonErrors(async () => {
|
|
3244
|
+
const { id } = parseTaskRef(args.id);
|
|
3245
|
+
const result = await akmTasksShow(id);
|
|
3246
|
+
output("tasks-show", result);
|
|
3247
|
+
});
|
|
3248
|
+
},
|
|
3249
|
+
});
|
|
3250
|
+
const tasksRemoveCommand = defineCommand({
|
|
3251
|
+
meta: { name: "remove", description: "Delete a task file and uninstall it from the OS scheduler" },
|
|
3252
|
+
args: { id: { type: "positional", description: "Task id", required: true } },
|
|
3253
|
+
async run({ args }) {
|
|
3254
|
+
await runWithJsonErrors(async () => {
|
|
3255
|
+
const { id } = parseTaskRef(args.id);
|
|
3256
|
+
const result = await akmTasksRemove(id);
|
|
3257
|
+
output("tasks-remove", result);
|
|
3258
|
+
});
|
|
3259
|
+
},
|
|
3260
|
+
});
|
|
3261
|
+
function makeTasksToggleCommand(enabled) {
|
|
3262
|
+
const verb = enabled ? "enable" : "disable";
|
|
3263
|
+
const description = enabled
|
|
3264
|
+
? "Enable a previously-disabled task"
|
|
3265
|
+
: "Disable a task in the OS scheduler without removing the file";
|
|
3266
|
+
return defineCommand({
|
|
3267
|
+
meta: { name: verb, description },
|
|
3268
|
+
args: { id: { type: "positional", description: "Task id", required: true } },
|
|
3269
|
+
async run({ args }) {
|
|
3270
|
+
await runWithJsonErrors(async () => {
|
|
3271
|
+
const { id } = parseTaskRef(args.id);
|
|
3272
|
+
const result = await akmTasksSetEnabled(id, enabled);
|
|
3273
|
+
output(`tasks-${verb}`, result);
|
|
3274
|
+
});
|
|
3275
|
+
},
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
const tasksEnableCommand = makeTasksToggleCommand(true);
|
|
3279
|
+
const tasksDisableCommand = makeTasksToggleCommand(false);
|
|
3280
|
+
const tasksRunCommand = defineCommand({
|
|
3281
|
+
meta: {
|
|
3282
|
+
name: "run",
|
|
3283
|
+
description: "Execute a task now (this is what cron / launchd / schtasks invoke at the scheduled time)",
|
|
3284
|
+
},
|
|
3285
|
+
args: { id: { type: "positional", description: "Task id", required: true } },
|
|
3286
|
+
async run({ args }) {
|
|
3287
|
+
await runWithJsonErrors(async () => {
|
|
3288
|
+
const { id } = parseTaskRef(args.id);
|
|
3289
|
+
const envelope = await akmTasksRun(id);
|
|
3290
|
+
output("tasks-run", envelope);
|
|
3291
|
+
if (envelope.exitCode !== 0)
|
|
3292
|
+
process.exit(envelope.exitCode);
|
|
3293
|
+
});
|
|
3294
|
+
},
|
|
3295
|
+
});
|
|
3296
|
+
const tasksHistoryCommand = defineCommand({
|
|
3297
|
+
meta: { name: "history", description: "Show recent task run history" },
|
|
3298
|
+
args: {
|
|
3299
|
+
id: { type: "string", description: "Filter to one task id" },
|
|
3300
|
+
limit: { type: "string", description: "Maximum rows to return (default 50)" },
|
|
3301
|
+
},
|
|
3302
|
+
async run({ args }) {
|
|
3303
|
+
await runWithJsonErrors(async () => {
|
|
3304
|
+
const limit = parsePositiveIntFlag(args.limit ?? undefined);
|
|
3305
|
+
const result = await akmTasksHistory({ id: args.id, limit });
|
|
3306
|
+
output("tasks-history", result);
|
|
3307
|
+
});
|
|
3308
|
+
},
|
|
3309
|
+
});
|
|
3310
|
+
const tasksSyncCommand = defineCommand({
|
|
3311
|
+
meta: {
|
|
3312
|
+
name: "sync",
|
|
3313
|
+
description: "Reconcile the on-disk task files with the OS scheduler",
|
|
3314
|
+
},
|
|
3315
|
+
async run() {
|
|
3316
|
+
await runWithJsonErrors(async () => {
|
|
3317
|
+
const result = await akmTasksSync();
|
|
3318
|
+
output("tasks-sync", result);
|
|
3319
|
+
});
|
|
3320
|
+
},
|
|
3321
|
+
});
|
|
3322
|
+
const tasksDoctorCommand = defineCommand({
|
|
3323
|
+
meta: {
|
|
3324
|
+
name: "doctor",
|
|
3325
|
+
description: "Report the active scheduler backend, akm bin path, log dir, and supported schedule subset",
|
|
3326
|
+
},
|
|
3327
|
+
async run() {
|
|
3328
|
+
await runWithJsonErrors(async () => {
|
|
3329
|
+
const result = await akmTasksDoctor();
|
|
3330
|
+
output("tasks-doctor", result);
|
|
3331
|
+
});
|
|
3332
|
+
},
|
|
3333
|
+
});
|
|
3334
|
+
const tasksCommand = defineCommand({
|
|
3335
|
+
meta: {
|
|
3336
|
+
name: "tasks",
|
|
3337
|
+
description: "Schedule workflows or prompts via the OS-native scheduler (cron / launchd / schtasks)",
|
|
3338
|
+
},
|
|
3339
|
+
subCommands: {
|
|
3340
|
+
add: tasksAddCommand,
|
|
3341
|
+
list: tasksListCommand,
|
|
3342
|
+
show: tasksShowCommand,
|
|
3343
|
+
remove: tasksRemoveCommand,
|
|
3344
|
+
enable: tasksEnableCommand,
|
|
3345
|
+
disable: tasksDisableCommand,
|
|
3346
|
+
run: tasksRunCommand,
|
|
3347
|
+
history: tasksHistoryCommand,
|
|
3348
|
+
sync: tasksSyncCommand,
|
|
3349
|
+
doctor: tasksDoctorCommand,
|
|
3350
|
+
},
|
|
3351
|
+
run({ args }) {
|
|
3352
|
+
return runWithJsonErrors(async () => {
|
|
3353
|
+
if (hasSubcommand(args, TASKS_SUBCOMMAND_SET))
|
|
3354
|
+
return;
|
|
3355
|
+
const result = await akmTasksList();
|
|
3356
|
+
output("tasks-list", result);
|
|
3357
|
+
});
|
|
3358
|
+
},
|
|
3359
|
+
});
|
|
2772
3360
|
const main = defineCommand({
|
|
2773
3361
|
meta: {
|
|
2774
3362
|
name: "akm",
|
|
@@ -2789,7 +3377,9 @@ const main = defineCommand({
|
|
|
2789
3377
|
setup: setupCommand,
|
|
2790
3378
|
init: initCommand,
|
|
2791
3379
|
index: indexCommand,
|
|
3380
|
+
health: healthCommand,
|
|
2792
3381
|
info: infoCommand,
|
|
3382
|
+
graph: graphCommand,
|
|
2793
3383
|
add: addCommand,
|
|
2794
3384
|
list: listCommand,
|
|
2795
3385
|
remove: removeCommand,
|
|
@@ -2810,18 +3400,23 @@ const main = defineCommand({
|
|
|
2810
3400
|
feedback: feedbackCommand,
|
|
2811
3401
|
history: historyCommand,
|
|
2812
3402
|
events: eventsCommand,
|
|
2813
|
-
|
|
2814
|
-
|
|
3403
|
+
agent: agentCommand,
|
|
3404
|
+
lint: lintCommand,
|
|
3405
|
+
improve: improveCommand,
|
|
2815
3406
|
propose: proposeCommand,
|
|
2816
|
-
|
|
3407
|
+
proposals: proposalsCommand,
|
|
3408
|
+
accept: acceptCommand,
|
|
3409
|
+
reject: rejectCommand,
|
|
3410
|
+
diff: diffCommand,
|
|
2817
3411
|
help: helpCommand,
|
|
2818
3412
|
hints: hintsCommand,
|
|
2819
3413
|
completions: completionsCommand,
|
|
2820
3414
|
vault: vaultCommand,
|
|
2821
3415
|
wiki: wikiCommand,
|
|
3416
|
+
tasks: tasksCommand,
|
|
2822
3417
|
},
|
|
2823
3418
|
});
|
|
2824
|
-
const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
|
|
3419
|
+
const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "show", "get", "set", "unset"]);
|
|
2825
3420
|
const VAULT_SUBCOMMAND_SET = new Set(["list", "path", "run", "create", "set", "unset"]);
|
|
2826
3421
|
const WIKI_SUBCOMMAND_SET = new Set([
|
|
2827
3422
|
"create",
|
|
@@ -2835,7 +3430,6 @@ const WIKI_SUBCOMMAND_SET = new Set([
|
|
|
2835
3430
|
"lint",
|
|
2836
3431
|
"ingest",
|
|
2837
3432
|
]);
|
|
2838
|
-
const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
|
|
2839
3433
|
// ── Exit codes ──────────────────────────────────────────────────────────────
|
|
2840
3434
|
const EXIT_GENERAL = 1;
|
|
2841
3435
|
const EXIT_USAGE = 2;
|
|
@@ -2849,17 +3443,11 @@ process.argv = normalizeShowArgv(process.argv);
|
|
|
2849
3443
|
// invalid; surface it through the same JSON-error path the rest of the CLI uses
|
|
2850
3444
|
// rather than letting the raw exception escape with a stack trace.
|
|
2851
3445
|
try {
|
|
3446
|
+
applyEarlyStderrFlags(process.argv);
|
|
2852
3447
|
initOutputMode(process.argv, loadConfig().output ?? {});
|
|
2853
3448
|
}
|
|
2854
3449
|
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);
|
|
3450
|
+
emitJsonError(error);
|
|
2863
3451
|
}
|
|
2864
3452
|
runMain(main);
|
|
2865
3453
|
function classifyExitCode(error) {
|
|
@@ -2871,33 +3459,6 @@ function classifyExitCode(error) {
|
|
|
2871
3459
|
return EXIT_GENERAL;
|
|
2872
3460
|
return EXIT_GENERAL;
|
|
2873
3461
|
}
|
|
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
3462
|
/**
|
|
2902
3463
|
* Extract an actionable hint from an error instance. Hints live on the error
|
|
2903
3464
|
* classes themselves (see src/errors.ts) — either supplied explicitly at the
|
|
@@ -2909,86 +3470,27 @@ function extractHint(error) {
|
|
|
2909
3470
|
}
|
|
2910
3471
|
return undefined;
|
|
2911
3472
|
}
|
|
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
3473
|
/**
|
|
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.
|
|
3474
|
+
* Serialize an error to the standard JSON envelope and exit.
|
|
3475
|
+
* Used in both the startup try/catch and `runWithJsonErrors`.
|
|
2935
3476
|
*/
|
|
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);
|
|
3477
|
+
function emitJsonError(error) {
|
|
3478
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3479
|
+
const hint = extractHint(error);
|
|
3480
|
+
const exitCode = classifyExitCode(error);
|
|
3481
|
+
const code = error instanceof UsageError || error instanceof ConfigError || error instanceof NotFoundError
|
|
3482
|
+
? error.code
|
|
3483
|
+
: undefined;
|
|
3484
|
+
console.error(JSON.stringify({ ok: false, error: message, ...(code ? { code } : {}), hint }, null, 2));
|
|
3485
|
+
process.exit(exitCode);
|
|
3486
|
+
}
|
|
3487
|
+
async function runWithJsonErrors(fn) {
|
|
3488
|
+
try {
|
|
3489
|
+
await fn();
|
|
2980
3490
|
}
|
|
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);
|
|
3491
|
+
catch (error) {
|
|
3492
|
+
emitJsonError(error);
|
|
2989
3493
|
}
|
|
2990
|
-
result.push(...globalFlags);
|
|
2991
|
-
return result;
|
|
2992
3494
|
}
|
|
2993
3495
|
// ── Hints (embedded AGENTS.md) ──────────────────────────────────────────────
|
|
2994
3496
|
function loadHints(detail = "normal") {
|