akm-cli 0.7.5 → 0.8.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +113 -2
  2. package/README.md +20 -4
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +133 -0
  8. package/dist/cli.js +1995 -551
  9. package/dist/commands/agent-dispatch.js +110 -0
  10. package/dist/commands/agent-support.js +68 -0
  11. package/dist/commands/completions.js +3 -0
  12. package/dist/commands/config-cli.js +130 -534
  13. package/dist/commands/consolidate.js +1531 -0
  14. package/dist/commands/curate.js +44 -3
  15. package/dist/commands/db-cli.js +23 -0
  16. package/dist/commands/distill-promotion-policy.js +660 -0
  17. package/dist/commands/distill.js +990 -75
  18. package/dist/commands/eval-cases.js +43 -0
  19. package/dist/commands/events.js +5 -23
  20. package/dist/commands/graph.js +477 -0
  21. package/dist/commands/health.js +400 -0
  22. package/dist/commands/help/help-accept.md +9 -0
  23. package/dist/commands/help/help-improve.md +77 -0
  24. package/dist/commands/help/help-proposals.md +15 -0
  25. package/dist/commands/help/help-propose.md +17 -0
  26. package/dist/commands/help/help-reject.md +8 -0
  27. package/dist/commands/history.js +54 -46
  28. package/dist/commands/improve-profiles.js +146 -0
  29. package/dist/commands/improve-result-file.js +103 -0
  30. package/dist/commands/improve.js +2175 -0
  31. package/dist/commands/info.js +5 -2
  32. package/dist/commands/init.js +50 -2
  33. package/dist/commands/installed-stashes.js +102 -139
  34. package/dist/commands/knowledge.js +136 -0
  35. package/dist/commands/lint/agent-linter.js +49 -0
  36. package/dist/commands/lint/base-linter.js +479 -0
  37. package/dist/commands/lint/command-linter.js +49 -0
  38. package/dist/commands/lint/default-linter.js +16 -0
  39. package/dist/commands/lint/index.js +183 -0
  40. package/dist/commands/lint/knowledge-linter.js +16 -0
  41. package/dist/commands/lint/markdown-insertion.js +343 -0
  42. package/dist/commands/lint/memory-linter.js +61 -0
  43. package/dist/commands/lint/registry.js +36 -0
  44. package/dist/commands/lint/skill-linter.js +45 -0
  45. package/dist/commands/lint/task-linter.js +50 -0
  46. package/dist/commands/lint/types.js +4 -0
  47. package/dist/commands/lint/vault-key-rules.js +139 -0
  48. package/dist/commands/lint/workflow-linter.js +56 -0
  49. package/dist/commands/lint.js +4 -0
  50. package/dist/commands/migration-help.js +5 -2
  51. package/dist/commands/proposal.js +66 -12
  52. package/dist/commands/propose.js +86 -31
  53. package/dist/commands/reflect.js +1119 -73
  54. package/dist/commands/registry-search.js +5 -2
  55. package/dist/commands/remember.js +69 -6
  56. package/dist/commands/schema-repair.js +203 -0
  57. package/dist/commands/search.js +115 -14
  58. package/dist/commands/self-update.js +3 -0
  59. package/dist/commands/show.js +144 -25
  60. package/dist/commands/source-add.js +17 -45
  61. package/dist/commands/source-clone.js +3 -0
  62. package/dist/commands/source-manage.js +14 -19
  63. package/dist/commands/tasks.js +438 -0
  64. package/dist/commands/url-checker.js +42 -0
  65. package/dist/commands/vault.js +130 -77
  66. package/dist/core/action-contributors.js +28 -0
  67. package/dist/core/asset-ref.js +7 -0
  68. package/dist/core/asset-registry.js +7 -16
  69. package/dist/core/asset-serialize.js +88 -0
  70. package/dist/core/asset-spec.js +22 -0
  71. package/dist/core/common.js +157 -0
  72. package/dist/core/concurrent.js +25 -0
  73. package/dist/core/config-io.js +347 -0
  74. package/dist/core/config-migration.js +625 -0
  75. package/dist/core/config-schema.js +501 -0
  76. package/dist/core/config-sources.js +108 -0
  77. package/dist/core/config-types.js +4 -0
  78. package/dist/core/config-walker.js +337 -0
  79. package/dist/core/config.js +327 -987
  80. package/dist/core/errors.js +40 -19
  81. package/dist/core/events.js +91 -138
  82. package/dist/core/file-lock.js +104 -0
  83. package/dist/core/frontmatter.js +3 -6
  84. package/dist/core/lesson-lint.js +3 -0
  85. package/dist/core/markdown.js +20 -0
  86. package/dist/core/memory-belief.js +62 -0
  87. package/dist/core/memory-contradiction-detect.js +274 -0
  88. package/dist/core/memory-improve.js +806 -0
  89. package/dist/core/parse.js +158 -0
  90. package/dist/core/paths.js +326 -14
  91. package/dist/core/proposal-quality-validators.js +364 -0
  92. package/dist/core/proposal-validators.js +69 -0
  93. package/dist/core/proposals.js +498 -42
  94. package/dist/core/state-db.js +927 -0
  95. package/dist/core/text-truncation.js +107 -0
  96. package/dist/core/time.js +54 -0
  97. package/dist/core/warn.js +62 -1
  98. package/dist/core/write-source.js +3 -0
  99. package/dist/indexer/db-backup.js +391 -0
  100. package/dist/indexer/db-search.js +152 -253
  101. package/dist/indexer/db.js +933 -103
  102. package/dist/indexer/ensure-index.js +64 -0
  103. package/dist/indexer/file-context.js +3 -0
  104. package/dist/indexer/graph-boost.js +376 -101
  105. package/dist/indexer/graph-db.js +391 -0
  106. package/dist/indexer/graph-dedup.js +95 -0
  107. package/dist/indexer/graph-extraction.js +550 -124
  108. package/dist/indexer/index-context.js +4 -0
  109. package/dist/indexer/indexer.js +506 -291
  110. package/dist/indexer/llm-cache.js +47 -0
  111. package/dist/indexer/manifest.js +3 -0
  112. package/dist/indexer/matchers.js +148 -160
  113. package/dist/indexer/memory-inference.js +99 -74
  114. package/dist/indexer/metadata-contributors.js +29 -0
  115. package/dist/indexer/metadata.js +255 -196
  116. package/dist/indexer/path-resolver.js +92 -0
  117. package/dist/indexer/project-context.js +192 -0
  118. package/dist/indexer/ranking-contributors.js +331 -0
  119. package/dist/indexer/ranking.js +81 -0
  120. package/dist/indexer/search-fields.js +5 -9
  121. package/dist/indexer/search-hit-enrichers.js +111 -0
  122. package/dist/indexer/search-source.js +44 -10
  123. package/dist/indexer/semantic-status.js +5 -16
  124. package/dist/indexer/staleness-detect.js +447 -0
  125. package/dist/indexer/usage-events.js +12 -9
  126. package/dist/indexer/walker.js +28 -0
  127. package/dist/integrations/agent/builders.js +135 -0
  128. package/dist/integrations/agent/config.js +122 -230
  129. package/dist/integrations/agent/detect.js +3 -0
  130. package/dist/integrations/agent/index.js +7 -13
  131. package/dist/integrations/agent/model-aliases.js +55 -0
  132. package/dist/integrations/agent/profiles.js +70 -5
  133. package/dist/integrations/agent/prompts.js +150 -74
  134. package/dist/integrations/agent/runner.js +151 -0
  135. package/dist/integrations/agent/sdk-runner.js +126 -0
  136. package/dist/integrations/agent/spawn.js +118 -23
  137. package/dist/integrations/github.js +3 -0
  138. package/dist/integrations/lockfile.js +32 -69
  139. package/dist/integrations/session-logs/index.js +68 -0
  140. package/dist/integrations/session-logs/providers/claude-code.js +59 -0
  141. package/dist/integrations/session-logs/providers/opencode.js +55 -0
  142. package/dist/integrations/session-logs/types.js +4 -0
  143. package/dist/llm/call-ai.js +62 -0
  144. package/dist/llm/client.js +72 -124
  145. package/dist/llm/embedder.js +3 -19
  146. package/dist/llm/embedders/cache.js +3 -7
  147. package/dist/llm/embedders/local.js +3 -0
  148. package/dist/llm/embedders/remote.js +20 -8
  149. package/dist/llm/embedders/types.js +3 -7
  150. package/dist/llm/feature-gate.js +89 -48
  151. package/dist/llm/graph-extract.js +676 -70
  152. package/dist/llm/index-passes.js +9 -23
  153. package/dist/llm/memory-infer.js +52 -71
  154. package/dist/llm/metadata-enhance.js +42 -29
  155. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  156. package/dist/output/cli-hints-full.md +281 -0
  157. package/dist/output/cli-hints-short.md +65 -0
  158. package/dist/output/cli-hints.js +5 -318
  159. package/dist/output/context.js +3 -0
  160. package/dist/output/renderers.js +223 -256
  161. package/dist/output/shapes.js +150 -105
  162. package/dist/output/text.js +318 -30
  163. package/dist/registry/build-index.js +3 -0
  164. package/dist/registry/create-provider-registry.js +3 -0
  165. package/dist/registry/factory.js +3 -0
  166. package/dist/registry/origin-resolve.js +3 -0
  167. package/dist/registry/providers/index.js +3 -0
  168. package/dist/registry/providers/skills-sh.js +70 -49
  169. package/dist/registry/providers/static-index.js +53 -48
  170. package/dist/registry/providers/types.js +3 -24
  171. package/dist/registry/resolve.js +11 -16
  172. package/dist/registry/types.js +3 -0
  173. package/dist/scripts/migrate-storage.js +17307 -0
  174. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -0
  175. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  176. package/dist/setup/detect.js +3 -0
  177. package/dist/setup/ripgrep-install.js +3 -0
  178. package/dist/setup/ripgrep-resolve.js +3 -0
  179. package/dist/setup/setup.js +775 -37
  180. package/dist/setup/steps.js +3 -15
  181. package/dist/sources/include.js +3 -0
  182. package/dist/sources/provider-factory.js +5 -12
  183. package/dist/sources/provider.js +3 -20
  184. package/dist/sources/providers/filesystem.js +19 -23
  185. package/dist/sources/providers/git.js +7 -5
  186. package/dist/sources/providers/index.js +3 -0
  187. package/dist/sources/providers/install-types.js +3 -13
  188. package/dist/sources/providers/npm.js +3 -4
  189. package/dist/sources/providers/provider-utils.js +3 -0
  190. package/dist/sources/providers/sync-from-ref.js +3 -11
  191. package/dist/sources/providers/tar-utils.js +3 -0
  192. package/dist/sources/providers/website.js +18 -22
  193. package/dist/sources/resolve.js +3 -0
  194. package/dist/sources/types.js +3 -0
  195. package/dist/sources/website-ingest.js +7 -0
  196. package/dist/tasks/backends/cron.js +203 -0
  197. package/dist/tasks/backends/exec-utils.js +28 -0
  198. package/dist/tasks/backends/index.js +24 -0
  199. package/dist/tasks/backends/launchd-template.xml +19 -0
  200. package/dist/tasks/backends/launchd.js +187 -0
  201. package/dist/tasks/backends/schtasks-template.xml +29 -0
  202. package/dist/tasks/backends/schtasks.js +215 -0
  203. package/dist/tasks/parser.js +211 -0
  204. package/dist/tasks/resolveAkmBin.js +87 -0
  205. package/dist/tasks/runner.js +458 -0
  206. package/dist/tasks/schedule.js +211 -0
  207. package/dist/tasks/schema.js +15 -0
  208. package/dist/tasks/validator.js +62 -0
  209. package/dist/version.js +3 -0
  210. package/dist/wiki/index-template.md +12 -0
  211. package/dist/wiki/ingest-workflow-template.md +54 -0
  212. package/dist/wiki/log-template.md +8 -0
  213. package/dist/wiki/schema-template.md +61 -0
  214. package/dist/wiki/wiki-templates.js +15 -0
  215. package/dist/wiki/wiki.js +13 -61
  216. package/dist/workflows/authoring.js +8 -25
  217. package/dist/workflows/cli.js +3 -0
  218. package/dist/workflows/db.js +140 -10
  219. package/dist/workflows/document-cache.js +3 -10
  220. package/dist/workflows/parser.js +3 -0
  221. package/dist/workflows/renderer.js +11 -3
  222. package/dist/workflows/runs.js +62 -91
  223. package/dist/workflows/schema.js +3 -0
  224. package/dist/workflows/scope-key.js +3 -0
  225. package/dist/workflows/validator.js +4 -8
  226. package/dist/workflows/workflow-template.md +24 -0
  227. package/docs/README.md +9 -2
  228. package/docs/data-and-telemetry.md +225 -0
  229. package/docs/migration/release-notes/0.7.0.md +1 -1
  230. package/docs/migration/release-notes/0.7.5.md +2 -2
  231. package/docs/migration/release-notes/0.8.0.md +48 -0
  232. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  233. package/package.json +20 -8
  234. package/.github/LICENSE +0 -374
  235. package/dist/commands/install-audit.js +0 -381
  236. package/dist/templates/wiki-templates.js +0 -100
@@ -0,0 +1,438 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * `akm tasks` — register, inspect, run, and remove scheduled task assets.
6
+ *
7
+ * Each handler exported here is a pure function that performs the real work;
8
+ * `src/cli.ts` wraps these in citty `defineCommand`s and shapes their return
9
+ * values via `output()`.
10
+ */
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import { stringify as yamlStringify } from "yaml";
14
+ import { parseAssetRef } from "../core/asset-ref";
15
+ import { resolveAssetPathFromName } from "../core/asset-spec";
16
+ import { isWithin, resolveStashDir } from "../core/common";
17
+ import { loadConfig } from "../core/config";
18
+ import { ConfigError, NotFoundError, UsageError } from "../core/errors";
19
+ import { getTaskHistoryDir, getTaskLogDir } from "../core/paths";
20
+ import { listAgentProfileNames } from "../integrations/agent";
21
+ import { resolveAssetPath } from "../sources/resolve";
22
+ import { backendNameForPlatform, selectBackend } from "../tasks/backends";
23
+ import { parseTaskDocument } from "../tasks/parser";
24
+ import { resolveAkmInvocation } from "../tasks/resolveAkmBin";
25
+ import { exitCodeForStatus, readTaskHistory, runTask } from "../tasks/runner";
26
+ import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT, translateToCron } from "../tasks/schedule";
27
+ import { validateTaskDocument } from "../tasks/validator";
28
+ export async function akmTasksAdd(input) {
29
+ const id = normaliseTaskId(input.id);
30
+ const hasCommand = input.command !== undefined &&
31
+ input.command !== null &&
32
+ !(typeof input.command === "string" && input.command.trim() === "") &&
33
+ !(Array.isArray(input.command) && input.command.length === 0);
34
+ const targetCount = [Boolean(input.workflow), Boolean(input.prompt), hasCommand].filter(Boolean).length;
35
+ if (targetCount !== 1) {
36
+ throw new UsageError("Pass exactly one of --workflow <ref>, --prompt <asset-ref|./file.md|text>, or --command <shell-command>.", "INVALID_FLAG_VALUE");
37
+ }
38
+ // Validate the schedule for the active backend before writing anything.
39
+ const backend = backendNameForPlatform();
40
+ parseSchedule(input.schedule, backend);
41
+ const stashDir = resolveStashDir();
42
+ const typeRoot = path.join(stashDir, "tasks");
43
+ fs.mkdirSync(typeRoot, { recursive: true });
44
+ const assetPath = resolveAssetPathFromName("task", typeRoot, id);
45
+ if (!isWithin(assetPath, typeRoot)) {
46
+ throw new UsageError(`Resolved task path escapes the stash: "${id}".`, "PATH_ESCAPE_VIOLATION");
47
+ }
48
+ if (fs.existsSync(assetPath) && !input.force) {
49
+ throw new UsageError(`Task "${id}" already exists. Pass --force to overwrite, or use \`akm tasks remove ${id}\` first.`, "RESOURCE_ALREADY_EXISTS");
50
+ }
51
+ const yaml = renderTaskYaml({
52
+ id,
53
+ schedule: input.schedule,
54
+ workflow: input.workflow,
55
+ prompt: input.prompt,
56
+ command: input.command,
57
+ profile: input.profile,
58
+ params: input.params,
59
+ name: input.name,
60
+ description: input.description,
61
+ when_to_use: input.when_to_use,
62
+ tags: input.tags,
63
+ enabled: input.disabled !== true,
64
+ });
65
+ const task = parseTaskDocument({ yaml, filePath: assetPath, id });
66
+ await validateTaskDocument(task, { backend, stashDir });
67
+ fs.writeFileSync(assetPath, yaml.endsWith("\n") ? yaml : `${yaml}\n`, "utf8");
68
+ // Install in the OS scheduler. If install fails after the file was written,
69
+ // delete the file so the on-disk state never claims a task is registered
70
+ // when it isn't.
71
+ try {
72
+ const sched = selectBackend();
73
+ await sched.install(task);
74
+ }
75
+ catch (err) {
76
+ try {
77
+ fs.rmSync(assetPath, { force: true });
78
+ }
79
+ catch {
80
+ /* ignore */
81
+ }
82
+ throw err;
83
+ }
84
+ return {
85
+ id,
86
+ ref: `task:${id}`,
87
+ path: assetPath,
88
+ stashDir,
89
+ schedule: task.schedule,
90
+ enabled: task.enabled,
91
+ backend,
92
+ target: task.target,
93
+ };
94
+ }
95
+ /**
96
+ * Emit a single grouped stderr warning for legacy `.md` task files in the
97
+ * tasks directory. 0.8.0 requires task definitions to be pure `.yml`; any
98
+ * leftover `.md` files from 0.7.x would otherwise be silently skipped, which
99
+ * makes scheduled tasks vanish without operator notice. We do NOT auto-migrate
100
+ * — that is a separate workstream — but operators must see the affected files.
101
+ *
102
+ * `seen` is module-level so the warning is emitted at most once per process,
103
+ * even when both `akm tasks list` and `akm tasks sync` are invoked in the same
104
+ * akm run.
105
+ */
106
+ const warnedLegacyMdDirs = new Set();
107
+ function warnLegacyMdTaskFiles(typeRoot) {
108
+ if (warnedLegacyMdDirs.has(typeRoot))
109
+ return;
110
+ let mdFiles;
111
+ try {
112
+ mdFiles = fs.readdirSync(typeRoot).filter((f) => f.endsWith(".md"));
113
+ }
114
+ catch {
115
+ return;
116
+ }
117
+ if (mdFiles.length === 0)
118
+ return;
119
+ warnedLegacyMdDirs.add(typeRoot);
120
+ const affected = mdFiles.map((f) => `tasks/${f}`).join(", ");
121
+ process.stderr.write(`WARNING: ${mdFiles.length} task file(s) use the legacy .md format and were ignored.\n` +
122
+ ` AKM 0.8.0 requires tasks as pure .yml. See docs/migration/v0.7-to-v0.8.md#task-definition-files-mdfrontmatter--yml.\n` +
123
+ ` Affected: ${affected}\n`);
124
+ }
125
+ /**
126
+ * Reset the legacy `.md` task warning de-duplication state. Test-only escape
127
+ * hatch — production code should never call this.
128
+ */
129
+ export function _resetLegacyMdTaskWarningStateForTests() {
130
+ warnedLegacyMdDirs.clear();
131
+ }
132
+ export async function akmTasksList() {
133
+ const stashDir = resolveStashDir();
134
+ const typeRoot = path.join(stashDir, "tasks");
135
+ if (!fs.existsSync(typeRoot))
136
+ return { tasks: [] };
137
+ const entries = fs.readdirSync(typeRoot);
138
+ warnLegacyMdTaskFiles(typeRoot);
139
+ const files = entries.filter((f) => f.endsWith(".yml"));
140
+ const tasks = [];
141
+ for (const file of files) {
142
+ const id = file.slice(0, -4);
143
+ const filePath = path.join(typeRoot, file);
144
+ let task;
145
+ try {
146
+ task = parseTaskDocument({ yaml: fs.readFileSync(filePath, "utf8"), filePath, id });
147
+ }
148
+ catch {
149
+ continue; // skip malformed files; `akm tasks show <id>` will surface the error
150
+ }
151
+ tasks.push({
152
+ id: task.id,
153
+ ref: `task:${task.id}`,
154
+ path: filePath,
155
+ schedule: task.schedule,
156
+ enabled: task.enabled,
157
+ target: task.target,
158
+ name: task.name,
159
+ description: task.description,
160
+ when_to_use: task.when_to_use,
161
+ tags: task.tags,
162
+ });
163
+ }
164
+ return { tasks };
165
+ }
166
+ export async function akmTasksShow(id) {
167
+ const normalised = normaliseTaskId(id);
168
+ const stashDir = resolveStashDir();
169
+ const typeRoot = path.join(stashDir, "tasks");
170
+ if (fs.existsSync(typeRoot))
171
+ warnLegacyMdTaskFiles(typeRoot);
172
+ const filePath = await resolveAssetPath(stashDir, "task", normalised);
173
+ const task = parseTaskDocument({
174
+ yaml: fs.readFileSync(filePath, "utf8"),
175
+ filePath,
176
+ id: normalised,
177
+ });
178
+ const spec = parseSchedule(task.schedule, backendNameForPlatform());
179
+ return {
180
+ id: task.id,
181
+ ref: `task:${task.id}`,
182
+ path: filePath,
183
+ schedule: task.schedule,
184
+ cron: translateToCron(spec),
185
+ enabled: task.enabled,
186
+ target: task.target,
187
+ name: task.name,
188
+ description: task.description,
189
+ when_to_use: task.when_to_use,
190
+ tags: task.tags,
191
+ };
192
+ }
193
+ export async function akmTasksRemove(id) {
194
+ const normalised = normaliseTaskId(id);
195
+ const stashDir = resolveStashDir();
196
+ const typeRoot = path.join(stashDir, "tasks");
197
+ if (fs.existsSync(typeRoot))
198
+ warnLegacyMdTaskFiles(typeRoot);
199
+ const filePath = await resolveAssetPath(stashDir, "task", normalised);
200
+ const sched = selectBackend();
201
+ try {
202
+ await sched.uninstall(normalised);
203
+ }
204
+ finally {
205
+ fs.rmSync(filePath, { force: true });
206
+ }
207
+ return { id: normalised, removed: true, backend: sched.name };
208
+ }
209
+ export async function akmTasksSetEnabled(id, enabled) {
210
+ const normalised = normaliseTaskId(id);
211
+ const stashDir = resolveStashDir();
212
+ const typeRoot = path.join(stashDir, "tasks");
213
+ if (fs.existsSync(typeRoot))
214
+ warnLegacyMdTaskFiles(typeRoot);
215
+ const filePath = await resolveAssetPath(stashDir, "task", normalised);
216
+ const yaml = fs.readFileSync(filePath, "utf8");
217
+ const updated = setEnabledInYaml(yaml, enabled);
218
+ fs.writeFileSync(filePath, updated, "utf8");
219
+ const sched = selectBackend();
220
+ try {
221
+ await sched.setEnabled(normalised, enabled);
222
+ }
223
+ catch (err) {
224
+ // Roll the file back so the YAML source-of-truth and the OS
225
+ // scheduler don't diverge silently when the backend call fails.
226
+ fs.writeFileSync(filePath, yaml, "utf8");
227
+ throw err;
228
+ }
229
+ return { id: normalised, enabled, backend: sched.name };
230
+ }
231
+ export async function akmTasksRun(id) {
232
+ const normalised = normaliseTaskId(id);
233
+ const stashDir = resolveStashDir();
234
+ const typeRoot = path.join(stashDir, "tasks");
235
+ if (fs.existsSync(typeRoot))
236
+ warnLegacyMdTaskFiles(typeRoot);
237
+ const result = await runTask(normalised);
238
+ return {
239
+ ok: result.status === "completed" || result.status === "disabled",
240
+ result,
241
+ exitCode: exitCodeForStatus(result.status),
242
+ };
243
+ }
244
+ export async function akmTasksHistory(input) {
245
+ const limit = input.limit !== undefined && input.limit > 0 ? input.limit : 50;
246
+ const id = input.id ? normaliseTaskId(input.id) : undefined;
247
+ const stashDir = resolveStashDir();
248
+ const typeRoot = path.join(stashDir, "tasks");
249
+ if (fs.existsSync(typeRoot))
250
+ warnLegacyMdTaskFiles(typeRoot);
251
+ return { rows: readTaskHistory({ id, limit }) };
252
+ }
253
+ /**
254
+ * Reconcile the on-disk task files with the OS scheduler.
255
+ * • install missing tasks (after validating them — invalid files are
256
+ * skipped with a per-task reason rather than aborting the whole sync)
257
+ * • remove orphan scheduler entries that no longer have a backing file
258
+ */
259
+ export async function akmTasksSync() {
260
+ const stashDir = resolveStashDir();
261
+ const typeRoot = path.join(stashDir, "tasks");
262
+ if (fs.existsSync(typeRoot))
263
+ warnLegacyMdTaskFiles(typeRoot);
264
+ const fileIds = fs.existsSync(typeRoot)
265
+ ? fs
266
+ .readdirSync(typeRoot)
267
+ .filter((f) => f.endsWith(".yml"))
268
+ .map((f) => f.slice(0, -4))
269
+ : [];
270
+ const sched = selectBackend();
271
+ const backend = backendNameForPlatform();
272
+ const present = new Set((await sched.list()).map((t) => t.id));
273
+ const installed = [];
274
+ const unchanged = [];
275
+ const skipped = [];
276
+ for (const id of fileIds) {
277
+ const filePath = path.join(typeRoot, `${id}.yml`);
278
+ let task;
279
+ try {
280
+ task = parseTaskDocument({ yaml: fs.readFileSync(filePath, "utf8"), filePath, id });
281
+ }
282
+ catch (err) {
283
+ skipped.push({ id, reason: err instanceof Error ? err.message : String(err) });
284
+ continue;
285
+ }
286
+ try {
287
+ await validateTaskDocument(task, { backend, stashDir });
288
+ }
289
+ catch (err) {
290
+ skipped.push({ id, reason: err instanceof Error ? err.message : String(err) });
291
+ continue;
292
+ }
293
+ if (present.has(id)) {
294
+ unchanged.push(id);
295
+ }
296
+ else {
297
+ await sched.install(task);
298
+ installed.push(id);
299
+ }
300
+ }
301
+ const removed = [];
302
+ for (const installedId of present) {
303
+ if (!fileIds.includes(installedId)) {
304
+ await sched.uninstall(installedId);
305
+ removed.push(installedId);
306
+ }
307
+ }
308
+ return { installed, removed, unchanged, skipped, backend: sched.name };
309
+ }
310
+ export async function akmTasksDoctor() {
311
+ const warnings = [];
312
+ let invocation = { argv: [], via: "unresolved" };
313
+ try {
314
+ const r = resolveAkmInvocation();
315
+ invocation = { argv: r.argv, via: r.via };
316
+ }
317
+ catch (err) {
318
+ warnings.push(err instanceof Error ? err.message : String(err));
319
+ }
320
+ try {
321
+ const stashDir = resolveStashDir();
322
+ const typeRoot = path.join(stashDir, "tasks");
323
+ if (fs.existsSync(typeRoot))
324
+ warnLegacyMdTaskFiles(typeRoot);
325
+ }
326
+ catch {
327
+ // doctor must never fail on stash-resolution; the warning is best-effort
328
+ }
329
+ const backend = backendNameForPlatform();
330
+ const config = loadConfig();
331
+ // v2: prefer profiles.agent / defaults.agent; fall back to legacy agent.default
332
+ const defaultProfile = config.defaults?.agent;
333
+ const profiles = config.profiles?.agent ? Object.keys(config.profiles.agent) : listAgentProfileNames(config);
334
+ return {
335
+ backend,
336
+ akm: invocation,
337
+ logDir: getTaskLogDir(),
338
+ historyDir: getTaskHistoryDir(),
339
+ agent: { defaultProfile, available: profiles },
340
+ scheduleSubset: SCHEDULE_SUPPORTED_SUBSET_HINT,
341
+ warnings,
342
+ };
343
+ }
344
+ // ── helpers ─────────────────────────────────────────────────────────────────
345
+ const VALID_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
346
+ function normaliseTaskId(raw) {
347
+ // Accept both .yml and .md suffixes from users so muscle memory from the
348
+ // pre-0.8.0 markdown task format doesn't produce a confusing "task not found".
349
+ const id = raw.trim().replace(/\.(yml|md)$/, "");
350
+ if (!id) {
351
+ throw new UsageError("Task id must be non-empty.", "MISSING_REQUIRED_ARGUMENT");
352
+ }
353
+ if (!VALID_ID_RE.test(id)) {
354
+ throw new UsageError(`Task id "${id}" is invalid. Use letters, digits, dots, underscores, and dashes only.`, "INVALID_FLAG_VALUE");
355
+ }
356
+ return id;
357
+ }
358
+ function renderTaskYaml(input) {
359
+ const obj = { schedule: input.schedule };
360
+ if (input.workflow) {
361
+ obj.workflow = input.workflow;
362
+ if (input.params) {
363
+ obj.params = parseJsonObjectArg(input.params);
364
+ }
365
+ }
366
+ else if (input.prompt) {
367
+ obj.prompt = input.prompt;
368
+ if (input.profile)
369
+ obj.profile = input.profile;
370
+ }
371
+ else if (input.command !== undefined) {
372
+ // Emit a string when given a string, an array when given an array. The
373
+ // parser accepts both forms; preserving the caller's shape keeps the YAML
374
+ // ergonomic for humans editing the file later.
375
+ obj.command = input.command;
376
+ }
377
+ obj.enabled = input.enabled;
378
+ if (input.name)
379
+ obj.name = input.name;
380
+ if (input.description)
381
+ obj.description = input.description;
382
+ if (input.when_to_use)
383
+ obj.when_to_use = input.when_to_use;
384
+ if (input.tags && input.tags.length > 0)
385
+ obj.tags = input.tags;
386
+ return yamlStringify(obj);
387
+ }
388
+ function parseJsonObjectArg(raw) {
389
+ let parsed;
390
+ try {
391
+ parsed = JSON.parse(raw);
392
+ }
393
+ catch {
394
+ throw new UsageError("--params must be valid JSON.", "INVALID_JSON_ARGUMENT");
395
+ }
396
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
397
+ throw new UsageError("--params must be a JSON object.", "INVALID_JSON_ARGUMENT");
398
+ }
399
+ return parsed;
400
+ }
401
+ /**
402
+ * Toggle the `enabled:` value in a task YAML file in-place without a full
403
+ * parse/render round-trip (which would reformat the file). Appends the key
404
+ * if absent.
405
+ *
406
+ * Preserves inline comments (e.g. `enabled: true # important`) and uses
407
+ * case-sensitive matching (YAML keys are case-sensitive).
408
+ */
409
+ export function setEnabledInYaml(yaml, enabled) {
410
+ // Match: key prefix (group 1), value (group 2), optional trailing comment (group 3)
411
+ const pattern = /^(enabled:\s*)([^\s#\r\n][^\r\n]*?)(\s*(?:#[^\r\n]*))?$/m;
412
+ if (pattern.test(yaml)) {
413
+ return yaml.replace(pattern, `$1${enabled}$3`);
414
+ }
415
+ // Handle the case where enabled: has no value yet (bare key)
416
+ const simplePattern = /^(enabled:)\s*$/m;
417
+ if (simplePattern.test(yaml)) {
418
+ return yaml.replace(simplePattern, `$1 ${enabled}`);
419
+ }
420
+ return `${yaml.trimEnd()}\nenabled: ${enabled}\n`;
421
+ }
422
+ // Re-exported so tests can verify the validator path directly.
423
+ // Re-export error classes consumed by callers that want to instanceof-check.
424
+ // Re-export this so the CLI can decide what process exit code to use after
425
+ // `akm tasks run` completes.
426
+ export { ConfigError, exitCodeForStatus, NotFoundError, parseTaskDocument, UsageError };
427
+ // Helper: ensure the asset-spec resolver agrees with our id rules. If the
428
+ // user passes a ref, we accept the bare name part too.
429
+ export function parseTaskRef(input) {
430
+ if (input.includes(":")) {
431
+ const ref = parseAssetRef(input);
432
+ if (ref.type !== "task") {
433
+ throw new UsageError(`Expected a task id or task:<id> ref, got "${input}".`, "INVALID_FLAG_VALUE");
434
+ }
435
+ return { id: normaliseTaskId(ref.name) };
436
+ }
437
+ return { id: normaliseTaskId(input) };
438
+ }
@@ -0,0 +1,42 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ const URL_RE = /https?:\/\/[^\s"'<>)\]]+/g;
5
+ const TIMEOUT_MS = 5000;
6
+ const MAX_URLS = 20;
7
+ export async function checkDeadUrls(_stashDir, entries) {
8
+ const urlsToCheck = [];
9
+ for (const entry of entries) {
10
+ if (urlsToCheck.length >= MAX_URLS)
11
+ break;
12
+ const matches = entry.body.match(URL_RE) ?? [];
13
+ for (const url of matches.slice(0, 3)) {
14
+ urlsToCheck.push({ ref: entry.ref, url });
15
+ if (urlsToCheck.length >= MAX_URLS)
16
+ break;
17
+ }
18
+ }
19
+ const results = [];
20
+ await Promise.allSettled(urlsToCheck.map(async ({ ref, url }) => {
21
+ try {
22
+ const controller = new AbortController();
23
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
24
+ const res = await fetch(url, {
25
+ method: "HEAD",
26
+ signal: controller.signal,
27
+ redirect: "follow",
28
+ });
29
+ clearTimeout(timer);
30
+ if (res.status >= 400) {
31
+ results.push({ ref, url, status: res.status });
32
+ }
33
+ }
34
+ catch (e) {
35
+ if (e.name === "AbortError") {
36
+ results.push({ ref, url, status: "timeout" });
37
+ }
38
+ // network errors (ENOTFOUND etc.) — skip, don't report as dead
39
+ }
40
+ }));
41
+ return results;
42
+ }