akm-cli 0.9.0-beta.53 → 0.9.0-beta.55

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.
Files changed (123) hide show
  1. package/dist/cli/clack.js +56 -0
  2. package/dist/cli/confirm.js +1 -1
  3. package/dist/cli.js +5 -3
  4. package/dist/commands/agent/contribute-cli.js +2 -3
  5. package/dist/commands/env/env-cli.js +187 -202
  6. package/dist/commands/env/secret-cli.js +109 -121
  7. package/dist/commands/feedback-cli.js +152 -155
  8. package/dist/commands/health/advisories.js +151 -0
  9. package/dist/commands/health/html-report.js +33 -10
  10. package/dist/commands/health/improve-metrics.js +754 -0
  11. package/dist/commands/health/llm-usage.js +65 -0
  12. package/dist/commands/health/md-report.js +103 -0
  13. package/dist/commands/health/metrics.js +278 -0
  14. package/dist/commands/health/task-runs.js +135 -0
  15. package/dist/commands/health/types.js +18 -0
  16. package/dist/commands/health/windows.js +196 -0
  17. package/dist/commands/health.js +15 -1492
  18. package/dist/commands/improve/anti-collapse.js +170 -0
  19. package/dist/commands/improve/collapse-detector.js +3 -2
  20. package/dist/commands/improve/consolidate.js +636 -633
  21. package/dist/commands/improve/dedup.js +1 -1
  22. package/dist/commands/improve/distill/content-repair.js +202 -0
  23. package/dist/commands/improve/distill/promote-memory.js +228 -0
  24. package/dist/commands/improve/distill/quality-gate.js +233 -0
  25. package/dist/commands/improve/distill-guards.js +127 -0
  26. package/dist/commands/improve/distill.js +49 -575
  27. package/dist/commands/improve/extract-cli.js +74 -76
  28. package/dist/commands/improve/extract.js +6 -4
  29. package/dist/commands/improve/hot-probation.js +45 -0
  30. package/dist/commands/improve/improve-auto-accept.js +3 -2
  31. package/dist/commands/improve/improve-cli.js +14 -13
  32. package/dist/commands/improve/improve-result-file.js +2 -1
  33. package/dist/commands/improve/improve.js +6 -5
  34. package/dist/commands/improve/loop-stages.js +19 -21
  35. package/dist/commands/improve/outcome-loop.js +18 -16
  36. package/dist/commands/improve/preparation.js +23 -5
  37. package/dist/commands/improve/procedural.js +10 -31
  38. package/dist/commands/improve/recombine.js +19 -43
  39. package/dist/commands/improve/reflect.js +1 -1
  40. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  41. package/dist/commands/improve/shared.js +48 -0
  42. package/dist/commands/observability-cli.js +4 -4
  43. package/dist/commands/proposal/drain-policies.js +2 -2
  44. package/dist/commands/proposal/drain.js +1 -1
  45. package/dist/commands/proposal/legacy-import.js +115 -0
  46. package/dist/commands/proposal/proposal-cli.js +3 -3
  47. package/dist/commands/proposal/proposal.js +2 -1
  48. package/dist/commands/proposal/propose.js +1 -1
  49. package/dist/commands/proposal/repository.js +829 -0
  50. package/dist/commands/proposal/validators/proposals.js +5 -920
  51. package/dist/commands/read/curate.js +4 -4
  52. package/dist/commands/read/remember-cli.js +132 -137
  53. package/dist/commands/read/search-cli.js +7 -5
  54. package/dist/commands/read/search.js +7 -3
  55. package/dist/commands/read/show.js +3 -5
  56. package/dist/commands/registry-cli.js +76 -87
  57. package/dist/commands/sources/add-cli.js +91 -95
  58. package/dist/commands/sources/history.js +1 -1
  59. package/dist/commands/sources/init.js +12 -0
  60. package/dist/commands/sources/schema-repair.js +1 -1
  61. package/dist/commands/sources/sources-cli.js +3 -3
  62. package/dist/commands/sources/stash-cli.js +2 -2
  63. package/dist/commands/tasks/default-tasks.js +12 -0
  64. package/dist/commands/tasks/tasks-cli.js +1 -2
  65. package/dist/commands/wiki-cli.js +2 -3
  66. package/dist/core/common.js +3 -3
  67. package/dist/core/config/config-schema.js +6 -0
  68. package/dist/core/config/config.js +12 -0
  69. package/dist/core/deep-merge.js +38 -0
  70. package/dist/core/events.js +2 -1
  71. package/dist/core/logs-db.js +8 -13
  72. package/dist/core/paths.js +14 -14
  73. package/dist/core/state-db.js +13 -1140
  74. package/dist/core/warn.js +21 -0
  75. package/dist/indexer/db/db.js +72 -709
  76. package/dist/indexer/db/entry-mapper.js +41 -0
  77. package/dist/indexer/db/schema.js +516 -0
  78. package/dist/indexer/ensure-index.js +3 -2
  79. package/dist/indexer/feedback/utility-policy.js +85 -0
  80. package/dist/indexer/graph/graph-extraction.js +2 -1
  81. package/dist/indexer/index-writer-lock.js +18 -0
  82. package/dist/indexer/indexer.js +94 -27
  83. package/dist/indexer/read-preflight.js +23 -0
  84. package/dist/indexer/search/fts-query.js +51 -0
  85. package/dist/indexer/walk/walker.js +21 -13
  86. package/dist/integrations/agent/detect.js +9 -0
  87. package/dist/integrations/agent/index.js +1 -1
  88. package/dist/integrations/agent/spawn.js +15 -66
  89. package/dist/llm/client.js +12 -0
  90. package/dist/llm/embedder.js +26 -2
  91. package/dist/llm/embedders/local.js +7 -1
  92. package/dist/output/text/helpers.js +13 -0
  93. package/dist/scripts/migrate-storage.js +6903 -7424
  94. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +49 -44
  95. package/dist/setup/detect.js +9 -0
  96. package/dist/setup/legacy-config.js +106 -0
  97. package/dist/setup/prompt.js +57 -0
  98. package/dist/setup/providers.js +14 -0
  99. package/dist/setup/registry-stash-loader.js +12 -0
  100. package/dist/setup/semantic-assets.js +124 -0
  101. package/dist/setup/setup.js +25 -1608
  102. package/dist/setup/steps/connection.js +734 -0
  103. package/dist/setup/steps/output.js +31 -0
  104. package/dist/setup/steps/platforms.js +124 -0
  105. package/dist/setup/steps/semantic.js +27 -0
  106. package/dist/setup/steps/sources.js +222 -0
  107. package/dist/setup/steps/stashdir.js +42 -0
  108. package/dist/setup/steps/tasks.js +152 -0
  109. package/dist/storage/repositories/canaries-repository.js +107 -0
  110. package/dist/storage/repositories/consolidation-repository.js +38 -0
  111. package/dist/storage/repositories/embeddings-repository.js +72 -0
  112. package/dist/storage/repositories/events-repository.js +187 -0
  113. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  114. package/dist/storage/repositories/improve-runs-repository.js +130 -0
  115. package/dist/storage/repositories/index-db.js +4 -7
  116. package/dist/storage/repositories/proposals-repository.js +220 -0
  117. package/dist/storage/repositories/recombine-repository.js +213 -0
  118. package/dist/storage/repositories/task-history-repository.js +93 -0
  119. package/dist/storage/sqlite-pragmas.js +3 -3
  120. package/dist/tasks/backends/index.js +9 -0
  121. package/dist/tasks/runner.js +11 -1
  122. package/package.json +2 -2
  123. package/dist/commands/improve/homeostatic.js +0 -497
@@ -3,27 +3,24 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { defineCommand } from "citty";
5
5
  import { parsePositiveIntFlag } from "../cli/parse-args.js";
6
- import { output, runWithJsonErrors } from "../cli/shared.js";
6
+ import { defineJsonCommand, output } from "../cli/shared.js";
7
7
  import { DEFAULT_CONFIG, loadUserConfig, saveConfig } from "../core/config/config.js";
8
8
  import { UsageError } from "../core/errors.js";
9
9
  import { warn } from "../core/warn.js";
10
- import { getHyphenatedArg, getHyphenatedBoolean } from "../output/context.js";
11
10
  import { buildRegistryIndex, writeRegistryIndex } from "../registry/build-index.js";
12
11
  import { searchRegistry } from "./read/registry-search.js";
13
12
  export const registryCommand = defineCommand({
14
13
  meta: { name: "registry", description: "Manage stash registries" },
15
14
  subCommands: {
16
- list: defineCommand({
15
+ list: defineJsonCommand({
17
16
  meta: { name: "list", description: "List configured registries" },
18
- run() {
19
- return runWithJsonErrors(() => {
20
- const config = loadUserConfig();
21
- const registries = config.registries ?? DEFAULT_CONFIG.registries;
22
- output("registry-list", { registries });
23
- });
17
+ async run() {
18
+ const config = loadUserConfig();
19
+ const registries = config.registries ?? DEFAULT_CONFIG.registries;
20
+ output("registry-list", { registries });
24
21
  },
25
22
  }),
26
- add: defineCommand({
23
+ add: defineJsonCommand({
27
24
  meta: { name: "add", description: "Add a registry by URL" },
28
25
  args: {
29
26
  url: { type: "positional", description: "Registry index URL", required: true },
@@ -36,75 +33,71 @@ export const registryCommand = defineCommand({
36
33
  default: false,
37
34
  },
38
35
  },
39
- run({ args }) {
40
- return runWithJsonErrors(() => {
41
- if (!args.url.startsWith("http")) {
42
- throw new UsageError("Registry URL must start with http:// or https://");
43
- }
44
- if (args.url.startsWith("http://")) {
45
- const allowInsecure = getHyphenatedBoolean(args, "allow-insecure");
46
- if (!allowInsecure) {
47
- throw new UsageError("Registry URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious index. " +
48
- "Use https:// or pass --allow-insecure if you have explicitly accepted the risk.");
49
- }
50
- warn("Warning: registry URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious index.");
36
+ async run({ args }) {
37
+ if (!args.url.startsWith("http")) {
38
+ throw new UsageError("Registry URL must start with http:// or https://");
39
+ }
40
+ if (args.url.startsWith("http://")) {
41
+ const allowInsecure = args["allow-insecure"];
42
+ if (!allowInsecure) {
43
+ throw new UsageError("Registry URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious index. " +
44
+ "Use https:// or pass --allow-insecure if you have explicitly accepted the risk.");
51
45
  }
52
- const config = loadUserConfig();
53
- const registries = [...(config.registries ?? [])];
54
- // Deduplicate by URL
55
- if (registries.some((r) => r.url === args.url)) {
56
- output("registry-add", { registries, added: false, message: "Registry URL already configured" });
57
- return;
46
+ warn("Warning: registry URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious index.");
47
+ }
48
+ const config = loadUserConfig();
49
+ const registries = [...(config.registries ?? [])];
50
+ // Deduplicate by URL
51
+ if (registries.some((r) => r.url === args.url)) {
52
+ output("registry-add", { registries, added: false, message: "Registry URL already configured" });
53
+ return;
54
+ }
55
+ const entry = { url: args.url };
56
+ if (args.name)
57
+ entry.name = args.name;
58
+ if (args.provider)
59
+ entry.provider = args.provider;
60
+ if (args.options) {
61
+ try {
62
+ entry.options = JSON.parse(args.options);
58
63
  }
59
- const entry = { url: args.url };
60
- if (args.name)
61
- entry.name = args.name;
62
- if (args.provider)
63
- entry.provider = args.provider;
64
- if (args.options) {
65
- try {
66
- entry.options = JSON.parse(args.options);
67
- }
68
- catch {
69
- throw new UsageError("--options must be valid JSON");
70
- }
64
+ catch {
65
+ throw new UsageError("--options must be valid JSON");
71
66
  }
72
- registries.push(entry);
73
- saveConfig({ ...config, registries });
74
- output("registry-add", { registries, added: true });
75
- });
67
+ }
68
+ registries.push(entry);
69
+ saveConfig({ ...config, registries });
70
+ output("registry-add", { registries, added: true });
76
71
  },
77
72
  }),
78
- remove: defineCommand({
73
+ remove: defineJsonCommand({
79
74
  meta: { name: "remove", description: "Remove a registry by URL or name" },
80
75
  args: {
81
76
  target: { type: "positional", description: "Registry URL or name to remove", required: true },
82
77
  yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
83
78
  },
84
- run({ args }) {
85
- return runWithJsonErrors(async () => {
86
- const config = loadUserConfig();
87
- const registries = [...(config.registries ?? [])];
88
- const idx = registries.findIndex((r) => r.url === args.target || r.name === args.target);
89
- if (idx === -1) {
90
- output("registry-remove", { registries, removed: false, message: "No matching registry found" });
91
- return;
92
- }
93
- const { confirmDestructive } = await import("../cli/confirm.js");
94
- const confirmed = await confirmDestructive(`Remove registry "${args.target}"? This cannot be undone.`, {
95
- yes: args.yes === true,
96
- });
97
- if (!confirmed) {
98
- process.stderr.write("Aborted.\n");
99
- return;
100
- }
101
- const removed = registries.splice(idx, 1)[0];
102
- saveConfig({ ...config, registries });
103
- output("registry-remove", { registries, removed: true, entry: removed });
79
+ async run({ args }) {
80
+ const config = loadUserConfig();
81
+ const registries = [...(config.registries ?? [])];
82
+ const idx = registries.findIndex((r) => r.url === args.target || r.name === args.target);
83
+ if (idx === -1) {
84
+ output("registry-remove", { registries, removed: false, message: "No matching registry found" });
85
+ return;
86
+ }
87
+ const { confirmDestructive } = await import("../cli/confirm.js");
88
+ const confirmed = await confirmDestructive(`Remove registry "${args.target}"? This cannot be undone.`, {
89
+ yes: args.yes === true,
104
90
  });
91
+ if (!confirmed) {
92
+ process.stderr.write("Aborted.\n");
93
+ return;
94
+ }
95
+ const removed = registries.splice(idx, 1)[0];
96
+ saveConfig({ ...config, registries });
97
+ output("registry-remove", { registries, removed: true, entry: removed });
105
98
  },
106
99
  }),
107
- search: defineCommand({
100
+ search: defineJsonCommand({
108
101
  meta: { name: "search", description: "Search enabled registries for stashes" },
109
102
  args: {
110
103
  query: { type: "positional", description: "Search query", required: true },
@@ -112,14 +105,12 @@ export const registryCommand = defineCommand({
112
105
  assets: { type: "boolean", description: "Include asset-level search results", default: false },
113
106
  },
114
107
  async run({ args }) {
115
- await runWithJsonErrors(async () => {
116
- const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
117
- const result = await searchRegistry(args.query, { limit: limitRaw, includeAssets: args.assets });
118
- output("registry-search", result);
119
- });
108
+ const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
109
+ const result = await searchRegistry(args.query, { limit: limitRaw, includeAssets: args.assets });
110
+ output("registry-search", result);
120
111
  },
121
112
  }),
122
- "build-index": defineCommand({
113
+ "build-index": defineJsonCommand({
123
114
  meta: { name: "build-index", description: "Build a v2 registry index from discovery and manual entries" },
124
115
  args: {
125
116
  out: { type: "string", description: "Output path for the generated index" },
@@ -128,21 +119,19 @@ export const registryCommand = defineCommand({
128
119
  "github-api": { type: "string", description: "Override GitHub API base URL" },
129
120
  },
130
121
  async run({ args }) {
131
- await runWithJsonErrors(async () => {
132
- const result = await buildRegistryIndex({
133
- manualEntriesPath: args.manual,
134
- npmRegistryBase: getHyphenatedArg(args, "npm-registry"),
135
- githubApiBase: getHyphenatedArg(args, "github-api"),
136
- });
137
- const outPath = writeRegistryIndex(result.index, args.out);
138
- output("registry-build-index", {
139
- outPath,
140
- version: result.index.version,
141
- updatedAt: result.index.updatedAt,
142
- totalKits: result.counts.total,
143
- counts: result.counts,
144
- manualEntriesPath: result.paths.manualEntriesPath,
145
- });
122
+ const result = await buildRegistryIndex({
123
+ manualEntriesPath: args.manual,
124
+ npmRegistryBase: args["npm-registry"],
125
+ githubApiBase: args["github-api"],
126
+ });
127
+ const outPath = writeRegistryIndex(result.index, args.out);
128
+ output("registry-build-index", {
129
+ outPath,
130
+ version: result.index.version,
131
+ updatedAt: result.index.updatedAt,
132
+ totalKits: result.counts.total,
133
+ counts: result.counts,
134
+ manualEntriesPath: result.paths.manualEntriesPath,
146
135
  });
147
136
  },
148
137
  }),
@@ -3,13 +3,11 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
- import * as p from "@clack/prompts";
7
- import { defineCommand } from "citty";
8
- import { output, runWithJsonErrors } from "../../cli/shared.js";
6
+ import * as p from "../../cli/clack.js";
7
+ import { defineJsonCommand, output } from "../../cli/shared.js";
9
8
  import { UsageError } from "../../core/errors.js";
10
9
  import { appendEvent } from "../../core/events.js";
11
10
  import { warn } from "../../core/warn.js";
12
- import { getHyphenatedBoolean } from "../../output/context.js";
13
11
  import { akmRemove } from "./installed-stashes.js";
14
12
  import { akmAdd } from "./source-add.js";
15
13
  import { addStash } from "./source-manage.js";
@@ -149,7 +147,7 @@ export async function auditInstalledStashForDangerousKeys(opts) {
149
147
  return { blocked: true, exitCode: 1 };
150
148
  }
151
149
  // ── Command definition ────────────────────────────────────────────────────────
152
- export const addCommand = defineCommand({
150
+ export const addCommand = defineJsonCommand({
153
151
  meta: {
154
152
  name: "add",
155
153
  description: "Add a source (local directory, website, npm package, GitHub repo, git URL, or remote provider)",
@@ -181,48 +179,11 @@ export const addCommand = defineCommand({
181
179
  },
182
180
  },
183
181
  async run({ args }) {
184
- await runWithJsonErrors(async () => {
185
- const ref = args.ref.trim();
186
- const allowInsecure = getHyphenatedBoolean(args, "allow-insecure");
187
- const allowDangerousKeys = allowInsecure;
188
- // URL with --provider → stash source (remote or git provider)
189
- if (args.provider) {
190
- if (shouldWarnOnPlainHttp(ref)) {
191
- if (!allowInsecure) {
192
- throw new UsageError("Source URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious payload. " +
193
- "Use https:// or pass --allow-insecure if you have explicitly accepted the risk.", "INVALID_FLAG_VALUE", "Re-run with `--allow-insecure` only after confirming the URL is trusted.");
194
- }
195
- warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious payload.");
196
- }
197
- let parsedOptions;
198
- if (args.options) {
199
- try {
200
- const parsed = JSON.parse(args.options);
201
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
202
- throw new UsageError("--options must be a JSON object");
203
- }
204
- parsedOptions = parsed;
205
- }
206
- catch (err) {
207
- if (err instanceof UsageError)
208
- throw err;
209
- throw new UsageError("--options must be valid JSON");
210
- }
211
- }
212
- const result = addStash({
213
- target: ref,
214
- name: args.name,
215
- providerType: args.provider,
216
- options: parsedOptions,
217
- writable: args.writable,
218
- });
219
- appendEvent({
220
- eventType: "add",
221
- metadata: { target: ref, provider: args.provider, name: args.name ?? null, writable: args.writable === true },
222
- });
223
- output("add", result);
224
- return;
225
- }
182
+ const ref = args.ref.trim();
183
+ const allowInsecure = args["allow-insecure"];
184
+ const allowDangerousKeys = allowInsecure;
185
+ // URL with --provider → stash source (remote or git provider)
186
+ if (args.provider) {
226
187
  if (shouldWarnOnPlainHttp(ref)) {
227
188
  if (!allowInsecure) {
228
189
  throw new UsageError("Source URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious payload. " +
@@ -230,64 +191,99 @@ export const addCommand = defineCommand({
230
191
  }
231
192
  warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious payload.");
232
193
  }
233
- const websiteOptions = buildWebsiteOptions(args);
234
- if (args.type === "wiki") {
235
- const { registerWikiSource } = await import("./source-add.js");
236
- const result = await registerWikiSource({
237
- ref,
238
- name: args.name,
239
- options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
240
- writable: args.writable,
241
- });
242
- appendEvent({
243
- eventType: "add",
244
- metadata: { target: ref, type: "wiki", name: args.name ?? null, writable: args.writable === true },
245
- });
246
- output("add", result);
247
- return;
194
+ let parsedOptions;
195
+ if (args.options) {
196
+ try {
197
+ const parsed = JSON.parse(args.options);
198
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
199
+ throw new UsageError("--options must be a JSON object");
200
+ }
201
+ parsedOptions = parsed;
202
+ }
203
+ catch (err) {
204
+ if (err instanceof UsageError)
205
+ throw err;
206
+ throw new UsageError("--options must be valid JSON");
207
+ }
248
208
  }
249
- const result = await akmAdd({
209
+ const result = addStash({
210
+ target: ref,
211
+ name: args.name,
212
+ providerType: args.provider,
213
+ options: parsedOptions,
214
+ writable: args.writable,
215
+ });
216
+ appendEvent({
217
+ eventType: "add",
218
+ metadata: { target: ref, provider: args.provider, name: args.name ?? null, writable: args.writable === true },
219
+ });
220
+ output("add", result);
221
+ return;
222
+ }
223
+ if (shouldWarnOnPlainHttp(ref)) {
224
+ if (!allowInsecure) {
225
+ throw new UsageError("Source URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious payload. " +
226
+ "Use https:// or pass --allow-insecure if you have explicitly accepted the risk.", "INVALID_FLAG_VALUE", "Re-run with `--allow-insecure` only after confirming the URL is trusted.");
227
+ }
228
+ warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious payload.");
229
+ }
230
+ const websiteOptions = buildWebsiteOptions(args);
231
+ if (args.type === "wiki") {
232
+ const { registerWikiSource } = await import("./source-add.js");
233
+ const result = await registerWikiSource({
250
234
  ref,
251
235
  name: args.name,
252
- overrideType: args.type,
253
236
  options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
254
237
  writable: args.writable,
255
238
  });
256
239
  appendEvent({
257
240
  eventType: "add",
258
- metadata: {
259
- target: ref,
260
- name: args.name ?? null,
261
- overrideType: args.type ?? null,
262
- writable: args.writable === true,
263
- },
241
+ metadata: { target: ref, type: "wiki", name: args.name ?? null, writable: args.writable === true },
264
242
  });
265
- // ── Post-install env key audit ──────────────────────────────────────────
266
- // Resolve the stash root from the install result and scan any env files
267
- // for dangerous env var keys. When findings are present the install is
268
- // gated: TTY → interactive confirmation prompt; non-TTY without
269
- // --allow-insecure → hard failure (exit 1). Pass
270
- // --allow-insecure to skip the prompt non-interactively.
271
- const installedStashRoot = result.installed?.stashRoot ??
272
- (result.sourceAdded && "stashRoot" in result.sourceAdded ? result.sourceAdded.stashRoot : undefined);
273
- if (installedStashRoot) {
274
- // Use the canonical installed id (most reliably resolved by akmRemove) rather
275
- // than the raw user-supplied ref which may not match after URL normalisation.
276
- const rollbackTarget = result.installed?.id ?? result.sourceAdded?.stashRoot ?? ref;
277
- // The audit RETURNS its decision; we decide `process.exit` here, OUTSIDE
278
- // any catch, so the abort cannot be lost to a swallowed exception (C3).
279
- const decision = await auditInstalledStashForDangerousKeys({
280
- installedStashRoot,
281
- ref,
282
- allowDangerousKeys,
283
- rollbackTarget,
284
- isTTY: process.stdin.isTTY === true,
285
- });
286
- if (decision.blocked) {
287
- process.exit(decision.exitCode);
288
- }
289
- }
290
243
  output("add", result);
244
+ return;
245
+ }
246
+ const result = await akmAdd({
247
+ ref,
248
+ name: args.name,
249
+ overrideType: args.type,
250
+ options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
251
+ writable: args.writable,
252
+ });
253
+ appendEvent({
254
+ eventType: "add",
255
+ metadata: {
256
+ target: ref,
257
+ name: args.name ?? null,
258
+ overrideType: args.type ?? null,
259
+ writable: args.writable === true,
260
+ },
291
261
  });
262
+ // ── Post-install env key audit ──────────────────────────────────────────
263
+ // Resolve the stash root from the install result and scan any env files
264
+ // for dangerous env var keys. When findings are present the install is
265
+ // gated: TTY → interactive confirmation prompt; non-TTY without
266
+ // --allow-insecure → hard failure (exit 1). Pass
267
+ // --allow-insecure to skip the prompt non-interactively.
268
+ const installedStashRoot = result.installed?.stashRoot ??
269
+ (result.sourceAdded && "stashRoot" in result.sourceAdded ? result.sourceAdded.stashRoot : undefined);
270
+ if (installedStashRoot) {
271
+ // Use the canonical installed id (most reliably resolved by akmRemove) rather
272
+ // than the raw user-supplied ref which may not match after URL normalisation.
273
+ const rollbackTarget = result.installed?.id ?? result.sourceAdded?.stashRoot ?? ref;
274
+ // The audit RETURNS its decision; we decide `process.exit` here, OUTSIDE
275
+ // any catch, so the abort cannot be lost to a swallowed exception (C3).
276
+ const decision = await auditInstalledStashForDangerousKeys({
277
+ installedStashRoot,
278
+ ref,
279
+ allowDangerousKeys,
280
+ rollbackTarget,
281
+ isTTY: process.stdin.isTTY === true,
282
+ });
283
+ if (decision.blocked) {
284
+ process.exit(decision.exitCode);
285
+ }
286
+ }
287
+ output("add", result);
292
288
  },
293
289
  });
@@ -22,7 +22,7 @@ import { readEvents } from "../../core/events.js";
22
22
  import { isoToSqlite, parseSinceToIso } from "../../core/time.js";
23
23
  import { closeDatabase, openExistingDatabase } from "../../indexer/db/db.js";
24
24
  import { getUsageEvents } from "../../indexer/usage/usage-events.js";
25
- import { listProposals } from "../proposal/validators/proposals.js";
25
+ import { listProposals } from "../proposal/repository.js";
26
26
  // Proposal lifecycle event types emitted by the proposal substrate (#225).
27
27
  const PROPOSAL_EVENT_TYPES = new Set(["promoted", "rejected"]);
28
28
  // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -55,7 +55,19 @@ function assertInitSandbox(stashDir, dirExplicitlyProvided) {
55
55
  function isUnderTestRunner() {
56
56
  return process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
57
57
  }
58
+ // ── Test seam ────────────────────────────────────────────────────────────────
59
+ // Swap-and-restore override. Inert in production; only tests call the setter.
60
+ let akmInitOverride;
61
+ /** TEST-ONLY. Swap the implementation of `akmInit`; pass undefined to restore. */
62
+ export function _setAkmInitForTests(fake) {
63
+ akmInitOverride = fake;
64
+ }
58
65
  export async function akmInit(options) {
66
+ if (akmInitOverride)
67
+ return akmInitOverride(options);
68
+ return akmInitReal(options);
69
+ }
70
+ async function akmInitReal(options) {
59
71
  const dirExplicitlyProvided = options?.dir != null;
60
72
  const setDefault = options?.setDefault === true;
61
73
  const stashDir = options?.dir ? path.resolve(options.dir) : getDefaultStashDir();
@@ -23,7 +23,7 @@ import { resolveStandardsContext } from "../../core/standards/resolve-standards-
23
23
  import { info } from "../../core/warn.js";
24
24
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
25
25
  import { chatCompletion, parseEmbeddedJsonResponse } from "../../llm/client.js";
26
- import { createProposal, isProposalSkipped } from "../proposal/validators/proposals.js";
26
+ import { createProposal, isProposalSkipped } from "../proposal/repository.js";
27
27
  // ── Constants ────────────────────────────────────────────────────────────────
28
28
  /** Minimum gap between schema-repair attempts on the same asset. */
29
29
  const SCHEMA_REPAIR_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
@@ -23,7 +23,7 @@ import { loadConfig } from "../../core/config/config.js";
23
23
  import { UsageError } from "../../core/errors.js";
24
24
  import { appendEvent } from "../../core/events.js";
25
25
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
26
- import { getHyphenatedBoolean, parseFlagValue } from "../../output/context.js";
26
+ import { parseFlagValue } from "../../output/context.js";
27
27
  import { resolveWritableOverride, saveGitStash } from "../../sources/providers/git.js";
28
28
  import { pkgVersion } from "../../version.js";
29
29
  import { akmHistory } from "./history.js";
@@ -125,8 +125,8 @@ export const upgradeCommand = defineJsonCommand({
125
125
  output("upgrade", check);
126
126
  return;
127
127
  }
128
- const skipChecksum = getHyphenatedBoolean(args, "skip-checksum");
129
- const skipPostUpgrade = getHyphenatedBoolean(args, "skip-post-upgrade");
128
+ const skipChecksum = args["skip-checksum"];
129
+ const skipPostUpgrade = args["skip-post-upgrade"];
130
130
  const result = await performUpgrade(check, { force: args.force, skipChecksum, skipPostUpgrade });
131
131
  output("upgrade", result);
132
132
  },
@@ -25,8 +25,8 @@
25
25
  * SIGINT/SIGTERM handlers in a try/finally — left byte-for-byte untouched.
26
26
  */
27
27
  import path from "node:path";
28
- import * as p from "@clack/prompts";
29
28
  import { defineCommand } from "citty";
29
+ import * as p from "../../cli/clack.js";
30
30
  import { defineJsonCommand, output, runWithJsonErrors } from "../../cli/shared.js";
31
31
  import { assertFlatAssetName } from "../../core/asset/asset-create.js";
32
32
  import { isHttpUrl } from "../../core/common.js";
@@ -60,7 +60,7 @@ export const initCommand = defineJsonCommand({
60
60
  const legacyDir = parseFlagValue(process.argv, "--stashDir") ?? parseFlagValue(process.argv, "--stash-dir");
61
61
  const result = await akmInit({
62
62
  dir: args.dir ?? legacyDir,
63
- setDefault: getHyphenatedBoolean(args, "set-default"),
63
+ setDefault: args["set-default"],
64
64
  });
65
65
  output("init", result);
66
66
  },
@@ -76,12 +76,19 @@ const DEFAULT_DEPS = {
76
76
  list: akmTasksList,
77
77
  add: akmTasksAdd,
78
78
  };
79
+ let defaultTasksOverrides;
80
+ /** TEST-ONLY. Swap the CI/server/register functions; pass undefined to restore. */
81
+ export function _setDefaultTasksForTests(fakes) {
82
+ defaultTasksOverrides = fakes;
83
+ }
79
84
  /**
80
85
  * Decide whether `akm setup` is running in a CI environment, where it must
81
86
  * register NO scheduled tasks. Mirrors the common `CI=true` convention used by
82
87
  * GitHub Actions, GitLab CI, CircleCI, etc.
83
88
  */
84
89
  export function isCiEnvironment(env = process.env) {
90
+ if (defaultTasksOverrides?.isCiEnvironment)
91
+ return defaultTasksOverrides.isCiEnvironment(env);
85
92
  const ci = env.CI;
86
93
  if (ci === undefined || ci === null)
87
94
  return false;
@@ -95,6 +102,8 @@ export function isCiEnvironment(env = process.env) {
95
102
  * Used as the default when setup is non-interactive (no TTY / --yes / CI).
96
103
  */
97
104
  export function detectServerDefault() {
105
+ if (defaultTasksOverrides?.detectServerDefault)
106
+ return defaultTasksOverrides.detectServerDefault();
98
107
  if (os.platform() !== "linux")
99
108
  return false;
100
109
  // A laptop exposes a battery under /sys/class/power_supply/BAT*. Absence of
@@ -121,6 +130,9 @@ export function detectServerDefault() {
121
130
  * never re-disable a user-enabled task).
122
131
  */
123
132
  export async function registerDefaultTasks(options = {}) {
133
+ if (defaultTasksOverrides?.registerDefaultTasks) {
134
+ return defaultTasksOverrides.registerDefaultTasks(options);
135
+ }
124
136
  if (isCiEnvironment()) {
125
137
  return { skipped: true, reason: "ci", created: [], existing: [], toggled: [] };
126
138
  }
@@ -15,7 +15,6 @@
15
15
  import { defineCommand } from "citty";
16
16
  import { parsePositiveIntFlag } from "../../cli/parse-args.js";
17
17
  import { defineGroupCommand, defineJsonCommand, output, runWithJsonErrors } from "../../cli/shared.js";
18
- import { getHyphenatedArg } from "../../output/context.js";
19
18
  import { detectServerDefault, registerDefaultTasks } from "./default-tasks.js";
20
19
  import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksList, akmTasksRemove, akmTasksRun, akmTasksSetEnabled, akmTasksShow, akmTasksSync, parseTaskRef, } from "./tasks.js";
21
20
  const tasksAddCommand = defineJsonCommand({
@@ -51,7 +50,7 @@ const tasksAddCommand = defineJsonCommand({
51
50
  profile: args.profile,
52
51
  params: args.params,
53
52
  name: args.name,
54
- when_to_use: getHyphenatedArg(args, "when-to-use"),
53
+ when_to_use: args["when-to-use"],
55
54
  description: args.description,
56
55
  tags: args.tags
57
56
  ? args.tags
@@ -16,7 +16,6 @@ import { defineGroupCommand, defineJsonCommand, output, runWithJsonErrors } from
16
16
  import { resolveStashDir } from "../core/common.js";
17
17
  import { loadConfig, resolveConfiguredSources } from "../core/config/config.js";
18
18
  import { ConfigError, UsageError } from "../core/errors.js";
19
- import { getHyphenatedArg, getHyphenatedBoolean } from "../output/context.js";
20
19
  import { akmAgentDispatch } from "./agent/agent-dispatch.js";
21
20
  import { readKnowledgeInput } from "./read/knowledge.js";
22
21
  import { buildWebsiteOptions } from "./sources/add-cli.js";
@@ -108,7 +107,7 @@ const wikiRemoveCommand = defineJsonCommand({
108
107
  process.stderr.write("Aborted.\n");
109
108
  return;
110
109
  }
111
- const withSources = getHyphenatedBoolean(args, "with-sources");
110
+ const withSources = args["with-sources"];
112
111
  const { removeWiki } = await import("../wiki/wiki.js");
113
112
  const { akmIndex } = await import("../indexer/indexer.js");
114
113
  const stashDir = resolveStashDir();
@@ -244,7 +243,7 @@ const wikiIngestCommand = defineJsonCommand({
244
243
  if (!profileName) {
245
244
  throw new UsageError("akm wiki ingest requires an agent profile. Pass --profile <name> or set defaults.agent in config.", "MISSING_REQUIRED_ARGUMENT", "Available profiles are listed under profiles.agent in your config. Run `akm config get profiles.agent` to inspect.");
246
245
  }
247
- const timeoutMs = parsePositiveIntFlag(getHyphenatedArg(args, "timeout-ms"), "--timeout-ms");
246
+ const timeoutMs = parsePositiveIntFlag(args["timeout-ms"], "--timeout-ms");
248
247
  const model = getStringArg(args, "model");
249
248
  const { getDefaultLlmConfig } = await import("../core/config/config.js");
250
249
  const dispatchResult = await akmAgentDispatch({
@@ -132,9 +132,9 @@ export function writeFileAtomic(target, content, mode) {
132
132
  *
133
133
  * Throws if no valid stash directory is found.
134
134
  */
135
- export function resolveStashDir(_options) {
135
+ export function resolveStashDir(_options, env = process.env) {
136
136
  // 1. Env var override (for CI, scripts, testing)
137
- const envDir = process.env.AKM_STASH_DIR?.trim();
137
+ const envDir = env.AKM_STASH_DIR?.trim();
138
138
  if (envDir) {
139
139
  return validateStashDir(envDir);
140
140
  }
@@ -143,7 +143,7 @@ export function resolveStashDir(_options) {
143
143
  if (configStashDir)
144
144
  return validateStashDir(configStashDir);
145
145
  // 3. Platform default — use it if it exists
146
- const defaultDir = getDefaultStashDir();
146
+ const defaultDir = getDefaultStashDir(env);
147
147
  if (isValidDirectory(defaultDir)) {
148
148
  return defaultDir;
149
149
  }