akm-cli 0.7.4 → 0.8.0-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{CHANGELOG.md → .github/CHANGELOG.md} +34 -1
- package/.github/LICENSE +374 -0
- package/dist/cli/parse-args.js +43 -0
- package/dist/cli.js +1007 -593
- package/dist/commands/agent-dispatch.js +102 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +823 -0
- package/dist/commands/curate.js +1 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +250 -48
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +12 -24
- 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 +1170 -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 +251 -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 +107 -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/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/migration-help.js +2 -2
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +113 -43
- package/dist/commands/reflect.js +175 -41
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +55 -1
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +131 -52
- 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 +7 -33
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-registry.js +5 -17
- package/dist/core/asset-spec.js +11 -1
- package/dist/core/common.js +94 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +229 -122
- package/dist/core/events.js +87 -123
- package/dist/core/frontmatter.js +3 -1
- 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 +775 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +86 -472
- package/dist/indexer/db.js +392 -6
- package/dist/indexer/ensure-index.js +133 -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 +417 -74
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +466 -298
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/match-contributors.js +141 -0
- package/dist/indexer/matchers.js +24 -190
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +188 -175
- 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/config.js +175 -3
- package/dist/integrations/agent/index.js +3 -1
- package/dist/integrations/agent/pipeline.js +39 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +114 -29
- package/dist/integrations/agent/runners.js +31 -0
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +136 -28
- 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 +63 -86
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -64
- package/dist/llm/memory-infer.js +52 -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 -309
- package/dist/output/renderers.js +196 -124
- package/dist/output/shapes.js +41 -3
- package/dist/output/text.js +257 -21
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/git.js +44 -2
- 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/db.js +9 -0
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +73 -88
- package/dist/workflows/scope-key.js +76 -0
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +3 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.4.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +20 -0
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +4 -3
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `akm tasks` — register, inspect, run, and remove scheduled task assets.
|
|
3
|
+
*
|
|
4
|
+
* Each handler exported here is a pure function that performs the real work;
|
|
5
|
+
* `src/cli.ts` wraps these in citty `defineCommand`s and shapes their return
|
|
6
|
+
* values via `output()`.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { parseAssetRef } from "../core/asset-ref";
|
|
11
|
+
import { resolveAssetPathFromName } from "../core/asset-spec";
|
|
12
|
+
import { isWithin, resolveStashDir } from "../core/common";
|
|
13
|
+
import { loadConfig } from "../core/config";
|
|
14
|
+
import { ConfigError, NotFoundError, UsageError } from "../core/errors";
|
|
15
|
+
import { getTaskHistoryDir, getTaskLogDir } from "../core/paths";
|
|
16
|
+
import { listAgentProfileNames } from "../integrations/agent";
|
|
17
|
+
import { resolveAssetPath } from "../sources/resolve";
|
|
18
|
+
import { backendNameForPlatform, selectBackend } from "../tasks/backends";
|
|
19
|
+
import { parseTaskDocument } from "../tasks/parser";
|
|
20
|
+
import { resolveAkmInvocation } from "../tasks/resolveAkmBin";
|
|
21
|
+
import { exitCodeForStatus, readTaskHistory, runTask } from "../tasks/runner";
|
|
22
|
+
import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT, translateToCron } from "../tasks/schedule";
|
|
23
|
+
import { validateTaskDocument } from "../tasks/validator";
|
|
24
|
+
export async function akmTasksAdd(input) {
|
|
25
|
+
const id = normaliseTaskId(input.id);
|
|
26
|
+
if ((input.workflow && input.prompt) || (!input.workflow && !input.prompt)) {
|
|
27
|
+
throw new UsageError("Pass exactly one of --workflow <ref> or --prompt <inline|asset-ref|./file.md>.", "INVALID_FLAG_VALUE");
|
|
28
|
+
}
|
|
29
|
+
// Validate the schedule for the active backend before writing anything.
|
|
30
|
+
const backend = backendNameForPlatform();
|
|
31
|
+
parseSchedule(input.schedule, backend);
|
|
32
|
+
const stashDir = resolveStashDir();
|
|
33
|
+
const typeRoot = path.join(stashDir, "tasks");
|
|
34
|
+
fs.mkdirSync(typeRoot, { recursive: true });
|
|
35
|
+
const assetPath = resolveAssetPathFromName("task", typeRoot, id);
|
|
36
|
+
if (!isWithin(assetPath, typeRoot)) {
|
|
37
|
+
throw new UsageError(`Resolved task path escapes the stash: "${id}".`, "PATH_ESCAPE_VIOLATION");
|
|
38
|
+
}
|
|
39
|
+
if (fs.existsSync(assetPath) && !input.force) {
|
|
40
|
+
throw new UsageError(`Task "${id}" already exists. Pass --force to overwrite, or use \`akm tasks remove ${id}\` first.`, "RESOURCE_ALREADY_EXISTS");
|
|
41
|
+
}
|
|
42
|
+
const markdown = renderTaskMarkdown({
|
|
43
|
+
id,
|
|
44
|
+
schedule: input.schedule,
|
|
45
|
+
workflow: input.workflow,
|
|
46
|
+
prompt: input.prompt,
|
|
47
|
+
profile: input.profile,
|
|
48
|
+
params: input.params,
|
|
49
|
+
description: input.description,
|
|
50
|
+
tags: input.tags,
|
|
51
|
+
enabled: input.disabled !== true,
|
|
52
|
+
});
|
|
53
|
+
const task = parseTaskDocument({ markdown, filePath: assetPath, id });
|
|
54
|
+
await validateTaskDocument(task, { backend, stashDir });
|
|
55
|
+
fs.writeFileSync(assetPath, markdown.endsWith("\n") ? markdown : `${markdown}\n`, "utf8");
|
|
56
|
+
// Install in the OS scheduler. If install fails after the file was written,
|
|
57
|
+
// delete the file so the on-disk state never claims a task is registered
|
|
58
|
+
// when it isn't.
|
|
59
|
+
try {
|
|
60
|
+
const sched = selectBackend();
|
|
61
|
+
await sched.install(task);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
try {
|
|
65
|
+
fs.rmSync(assetPath, { force: true });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* ignore */
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
id,
|
|
74
|
+
ref: `task:${id}`,
|
|
75
|
+
path: assetPath,
|
|
76
|
+
stashDir,
|
|
77
|
+
schedule: task.schedule,
|
|
78
|
+
enabled: task.enabled,
|
|
79
|
+
backend,
|
|
80
|
+
target: task.target,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export async function akmTasksList() {
|
|
84
|
+
const stashDir = resolveStashDir();
|
|
85
|
+
const typeRoot = path.join(stashDir, "tasks");
|
|
86
|
+
if (!fs.existsSync(typeRoot))
|
|
87
|
+
return { tasks: [] };
|
|
88
|
+
const files = fs.readdirSync(typeRoot).filter((f) => f.endsWith(".md"));
|
|
89
|
+
const tasks = [];
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
const id = file.slice(0, -3);
|
|
92
|
+
const filePath = path.join(typeRoot, file);
|
|
93
|
+
let task;
|
|
94
|
+
try {
|
|
95
|
+
task = parseTaskDocument({ markdown: fs.readFileSync(filePath, "utf8"), filePath, id });
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
continue; // skip malformed files; `akm tasks show <id>` will surface the error
|
|
99
|
+
}
|
|
100
|
+
tasks.push({
|
|
101
|
+
id: task.id,
|
|
102
|
+
ref: `task:${task.id}`,
|
|
103
|
+
path: filePath,
|
|
104
|
+
schedule: task.schedule,
|
|
105
|
+
enabled: task.enabled,
|
|
106
|
+
target: task.target,
|
|
107
|
+
description: task.description,
|
|
108
|
+
tags: task.tags,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return { tasks };
|
|
112
|
+
}
|
|
113
|
+
export async function akmTasksShow(id) {
|
|
114
|
+
const normalised = normaliseTaskId(id);
|
|
115
|
+
const stashDir = resolveStashDir();
|
|
116
|
+
const filePath = await resolveAssetPath(stashDir, "task", normalised);
|
|
117
|
+
const task = parseTaskDocument({
|
|
118
|
+
markdown: fs.readFileSync(filePath, "utf8"),
|
|
119
|
+
filePath,
|
|
120
|
+
id: normalised,
|
|
121
|
+
});
|
|
122
|
+
const spec = parseSchedule(task.schedule, backendNameForPlatform());
|
|
123
|
+
return {
|
|
124
|
+
id: task.id,
|
|
125
|
+
ref: `task:${task.id}`,
|
|
126
|
+
path: filePath,
|
|
127
|
+
schedule: task.schedule,
|
|
128
|
+
cron: translateToCron(spec),
|
|
129
|
+
enabled: task.enabled,
|
|
130
|
+
target: task.target,
|
|
131
|
+
description: task.description,
|
|
132
|
+
tags: task.tags,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export async function akmTasksRemove(id) {
|
|
136
|
+
const normalised = normaliseTaskId(id);
|
|
137
|
+
const stashDir = resolveStashDir();
|
|
138
|
+
const filePath = await resolveAssetPath(stashDir, "task", normalised);
|
|
139
|
+
const sched = selectBackend();
|
|
140
|
+
try {
|
|
141
|
+
await sched.uninstall(normalised);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
fs.rmSync(filePath, { force: true });
|
|
145
|
+
}
|
|
146
|
+
return { id: normalised, removed: true, backend: sched.name };
|
|
147
|
+
}
|
|
148
|
+
export async function akmTasksSetEnabled(id, enabled) {
|
|
149
|
+
const normalised = normaliseTaskId(id);
|
|
150
|
+
const stashDir = resolveStashDir();
|
|
151
|
+
const filePath = await resolveAssetPath(stashDir, "task", normalised);
|
|
152
|
+
const markdown = fs.readFileSync(filePath, "utf8");
|
|
153
|
+
const updated = setEnabledInMarkdown(markdown, enabled);
|
|
154
|
+
fs.writeFileSync(filePath, updated, "utf8");
|
|
155
|
+
const sched = selectBackend();
|
|
156
|
+
try {
|
|
157
|
+
await sched.setEnabled(normalised, enabled);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
// Roll the file back so the markdown source-of-truth and the OS
|
|
161
|
+
// scheduler don't diverge silently when the backend call fails.
|
|
162
|
+
fs.writeFileSync(filePath, markdown, "utf8");
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
return { id: normalised, enabled, backend: sched.name };
|
|
166
|
+
}
|
|
167
|
+
export async function akmTasksRun(id) {
|
|
168
|
+
const normalised = normaliseTaskId(id);
|
|
169
|
+
const result = await runTask(normalised);
|
|
170
|
+
return {
|
|
171
|
+
ok: result.status === "completed" || result.status === "disabled",
|
|
172
|
+
result,
|
|
173
|
+
exitCode: exitCodeForStatus(result.status),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export async function akmTasksHistory(input) {
|
|
177
|
+
const limit = input.limit !== undefined && input.limit > 0 ? input.limit : 50;
|
|
178
|
+
const id = input.id ? normaliseTaskId(input.id) : undefined;
|
|
179
|
+
return { rows: readTaskHistory({ id, limit }) };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Reconcile the on-disk task files with the OS scheduler.
|
|
183
|
+
* • install missing tasks (after validating them — invalid files are
|
|
184
|
+
* skipped with a per-task reason rather than aborting the whole sync)
|
|
185
|
+
* • remove orphan scheduler entries that no longer have a backing file
|
|
186
|
+
*/
|
|
187
|
+
export async function akmTasksSync() {
|
|
188
|
+
const stashDir = resolveStashDir();
|
|
189
|
+
const typeRoot = path.join(stashDir, "tasks");
|
|
190
|
+
const fileIds = fs.existsSync(typeRoot)
|
|
191
|
+
? fs
|
|
192
|
+
.readdirSync(typeRoot)
|
|
193
|
+
.filter((f) => f.endsWith(".md"))
|
|
194
|
+
.map((f) => f.slice(0, -3))
|
|
195
|
+
: [];
|
|
196
|
+
const sched = selectBackend();
|
|
197
|
+
const backend = backendNameForPlatform();
|
|
198
|
+
const present = new Set((await sched.list()).map((t) => t.id));
|
|
199
|
+
const installed = [];
|
|
200
|
+
const unchanged = [];
|
|
201
|
+
const skipped = [];
|
|
202
|
+
for (const id of fileIds) {
|
|
203
|
+
const filePath = path.join(typeRoot, `${id}.md`);
|
|
204
|
+
let task;
|
|
205
|
+
try {
|
|
206
|
+
task = parseTaskDocument({ markdown: fs.readFileSync(filePath, "utf8"), filePath, id });
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
skipped.push({ id, reason: err instanceof Error ? err.message : String(err) });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
await validateTaskDocument(task, { backend, stashDir });
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
skipped.push({ id, reason: err instanceof Error ? err.message : String(err) });
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (present.has(id)) {
|
|
220
|
+
unchanged.push(id);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
await sched.install(task);
|
|
224
|
+
installed.push(id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const removed = [];
|
|
228
|
+
for (const installedId of present) {
|
|
229
|
+
if (!fileIds.includes(installedId)) {
|
|
230
|
+
await sched.uninstall(installedId);
|
|
231
|
+
removed.push(installedId);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { installed, removed, unchanged, skipped, backend: sched.name };
|
|
235
|
+
}
|
|
236
|
+
export async function akmTasksDoctor() {
|
|
237
|
+
const warnings = [];
|
|
238
|
+
let invocation = { argv: [], via: "unresolved" };
|
|
239
|
+
try {
|
|
240
|
+
const r = resolveAkmInvocation();
|
|
241
|
+
invocation = { argv: r.argv, via: r.via };
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
warnings.push(err instanceof Error ? err.message : String(err));
|
|
245
|
+
}
|
|
246
|
+
const backend = backendNameForPlatform();
|
|
247
|
+
const config = loadConfig();
|
|
248
|
+
const defaultProfile = config.agent?.default;
|
|
249
|
+
const profiles = listAgentProfileNames(config.agent);
|
|
250
|
+
return {
|
|
251
|
+
backend,
|
|
252
|
+
akm: invocation,
|
|
253
|
+
logDir: getTaskLogDir(),
|
|
254
|
+
historyDir: getTaskHistoryDir(),
|
|
255
|
+
agent: { defaultProfile, available: profiles },
|
|
256
|
+
scheduleSubset: SCHEDULE_SUPPORTED_SUBSET_HINT,
|
|
257
|
+
warnings,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
261
|
+
const VALID_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
262
|
+
function normaliseTaskId(raw) {
|
|
263
|
+
const id = raw.trim().replace(/\.md$/, "");
|
|
264
|
+
if (!id) {
|
|
265
|
+
throw new UsageError("Task id must be non-empty.", "MISSING_REQUIRED_ARGUMENT");
|
|
266
|
+
}
|
|
267
|
+
if (!VALID_ID_RE.test(id)) {
|
|
268
|
+
throw new UsageError(`Task id "${id}" is invalid. Use letters, digits, dots, underscores, and dashes only.`, "INVALID_FLAG_VALUE");
|
|
269
|
+
}
|
|
270
|
+
return id;
|
|
271
|
+
}
|
|
272
|
+
function renderTaskMarkdown(input) {
|
|
273
|
+
const lines = ["---"];
|
|
274
|
+
lines.push(`schedule: ${yamlQuote(input.schedule)}`);
|
|
275
|
+
if (input.workflow) {
|
|
276
|
+
lines.push(`workflow: ${yamlQuote(input.workflow)}`);
|
|
277
|
+
if (input.params) {
|
|
278
|
+
const parsed = parseJsonObjectArg(input.params);
|
|
279
|
+
lines.push("params:");
|
|
280
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
281
|
+
lines.push(` ${k}: ${yamlScalarValue(v)}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else if (input.prompt) {
|
|
286
|
+
if (looksLikeAssetRef(input.prompt) || isFilePath(input.prompt) || input.prompt === "inline") {
|
|
287
|
+
lines.push(`prompt: ${yamlQuote(input.prompt)}`);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
lines.push(`prompt: inline`);
|
|
291
|
+
}
|
|
292
|
+
if (input.profile)
|
|
293
|
+
lines.push(`profile: ${yamlQuote(input.profile)}`);
|
|
294
|
+
}
|
|
295
|
+
lines.push(`enabled: ${input.enabled}`);
|
|
296
|
+
if (input.description)
|
|
297
|
+
lines.push(`description: ${yamlQuote(input.description)}`);
|
|
298
|
+
if (input.tags && input.tags.length > 0) {
|
|
299
|
+
lines.push(`tags: [${input.tags.map((t) => yamlQuote(t)).join(", ")}]`);
|
|
300
|
+
}
|
|
301
|
+
lines.push("---", "");
|
|
302
|
+
if (input.workflow) {
|
|
303
|
+
lines.push(`# Task: ${humanise(input.id)}`, "");
|
|
304
|
+
}
|
|
305
|
+
else if (input.prompt) {
|
|
306
|
+
if (looksLikeAssetRef(input.prompt) || isFilePath(input.prompt) || input.prompt === "inline") {
|
|
307
|
+
lines.push(`# Task: ${humanise(input.id)}`, "");
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
// Raw inline prompt — use the body itself.
|
|
311
|
+
lines.push(input.prompt.trim(), "");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return lines.join("\n");
|
|
315
|
+
}
|
|
316
|
+
function yamlQuote(value) {
|
|
317
|
+
if (/^[A-Za-z_][A-Za-z0-9_.\-/:]*$/.test(value))
|
|
318
|
+
return value;
|
|
319
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
320
|
+
}
|
|
321
|
+
function yamlScalarValue(v) {
|
|
322
|
+
if (typeof v === "string")
|
|
323
|
+
return yamlQuote(v);
|
|
324
|
+
if (typeof v === "number" || typeof v === "boolean")
|
|
325
|
+
return String(v);
|
|
326
|
+
if (v === null)
|
|
327
|
+
return "null";
|
|
328
|
+
return JSON.stringify(v);
|
|
329
|
+
}
|
|
330
|
+
function parseJsonObjectArg(raw) {
|
|
331
|
+
let parsed;
|
|
332
|
+
try {
|
|
333
|
+
parsed = JSON.parse(raw);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
throw new UsageError("--params must be valid JSON.", "INVALID_JSON_ARGUMENT");
|
|
337
|
+
}
|
|
338
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
339
|
+
throw new UsageError("--params must be a JSON object.", "INVALID_JSON_ARGUMENT");
|
|
340
|
+
}
|
|
341
|
+
return parsed;
|
|
342
|
+
}
|
|
343
|
+
function looksLikeAssetRef(s) {
|
|
344
|
+
return /^[a-z][a-z0-9_-]*:[^\s]/i.test(s) && !s.startsWith("./") && !s.startsWith("/");
|
|
345
|
+
}
|
|
346
|
+
function isFilePath(s) {
|
|
347
|
+
return s.startsWith("./") || s.startsWith("../") || path.isAbsolute(s);
|
|
348
|
+
}
|
|
349
|
+
function humanise(id) {
|
|
350
|
+
return id.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Toggle the `enabled:` value in a task markdown's frontmatter without doing
|
|
354
|
+
* a round-trip through the parser+renderer (which would lose comments and
|
|
355
|
+
* formatting choices). Inserts the key right before the closing `---` if
|
|
356
|
+
* absent.
|
|
357
|
+
*/
|
|
358
|
+
export function setEnabledInMarkdown(markdown, enabled) {
|
|
359
|
+
const m = markdown.match(/^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r\n|\r|\n|$))([\s\S]*)$/);
|
|
360
|
+
if (!m) {
|
|
361
|
+
throw new UsageError("Task markdown is missing frontmatter; cannot toggle enabled.", "INVALID_FLAG_VALUE");
|
|
362
|
+
}
|
|
363
|
+
const [, openFence, fmBody, closeFence, body] = m;
|
|
364
|
+
const replaced = fmBody.match(/(^|\r?\n)enabled:\s*[^\r\n]*/i)
|
|
365
|
+
? fmBody.replace(/(^|\r?\n)enabled:\s*[^\r\n]*/i, `$1enabled: ${enabled}`)
|
|
366
|
+
: `${fmBody}\nenabled: ${enabled}`;
|
|
367
|
+
return `${openFence}${replaced}${closeFence}${body}`;
|
|
368
|
+
}
|
|
369
|
+
// Re-exported so tests can verify the validator path directly.
|
|
370
|
+
// Re-export error classes consumed by callers that want to instanceof-check.
|
|
371
|
+
// Re-export this so the CLI can decide what process exit code to use after
|
|
372
|
+
// `akm tasks run` completes.
|
|
373
|
+
export { ConfigError, exitCodeForStatus, NotFoundError, parseTaskDocument, UsageError };
|
|
374
|
+
// Helper: ensure the asset-spec resolver agrees with our id rules. If the
|
|
375
|
+
// user passes a ref, we accept the bare name part too.
|
|
376
|
+
export function parseTaskRef(input) {
|
|
377
|
+
if (input.includes(":")) {
|
|
378
|
+
const ref = parseAssetRef(input);
|
|
379
|
+
if (ref.type !== "task") {
|
|
380
|
+
throw new UsageError(`Expected a task id or task:<id> ref, got "${input}".`, "INVALID_FLAG_VALUE");
|
|
381
|
+
}
|
|
382
|
+
return { id: normaliseTaskId(ref.name) };
|
|
383
|
+
}
|
|
384
|
+
return { id: normaliseTaskId(input) };
|
|
385
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const URL_RE = /https?:\/\/[^\s"'<>)\]]+/g;
|
|
2
|
+
const TIMEOUT_MS = 5000;
|
|
3
|
+
const MAX_URLS = 20;
|
|
4
|
+
export async function checkDeadUrls(_stashDir, entries) {
|
|
5
|
+
const urlsToCheck = [];
|
|
6
|
+
for (const entry of entries) {
|
|
7
|
+
if (urlsToCheck.length >= MAX_URLS)
|
|
8
|
+
break;
|
|
9
|
+
const matches = entry.body.match(URL_RE) ?? [];
|
|
10
|
+
for (const url of matches.slice(0, 3)) {
|
|
11
|
+
urlsToCheck.push({ ref: entry.ref, url });
|
|
12
|
+
if (urlsToCheck.length >= MAX_URLS)
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const results = [];
|
|
17
|
+
await Promise.allSettled(urlsToCheck.map(async ({ ref, url }) => {
|
|
18
|
+
try {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
21
|
+
const res = await fetch(url, {
|
|
22
|
+
method: "HEAD",
|
|
23
|
+
signal: controller.signal,
|
|
24
|
+
redirect: "follow",
|
|
25
|
+
});
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
if (res.status >= 400) {
|
|
28
|
+
results.push({ ref, url, status: res.status });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
if (e.name === "AbortError") {
|
|
33
|
+
results.push({ ref, url, status: "timeout" });
|
|
34
|
+
}
|
|
35
|
+
// network errors (ENOTFOUND etc.) — skip, don't report as dead
|
|
36
|
+
}
|
|
37
|
+
}));
|
|
38
|
+
return results;
|
|
39
|
+
}
|
package/dist/commands/vault.js
CHANGED
|
@@ -5,13 +5,9 @@
|
|
|
5
5
|
* the indexer, the `akm show` renderer, or any structured output channel.
|
|
6
6
|
* The supported load paths are:
|
|
7
7
|
*
|
|
8
|
-
* - `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* file, and emits `. <tmp>; rm -f <tmp>` on stdout. Values reach bash
|
|
12
|
-
* only via the temp file, never via akm's stdout.
|
|
13
|
-
* - `injectIntoEnv(vaultPath, target)` — programmatic API for modules that
|
|
14
|
-
* need values in a process environment.
|
|
8
|
+
* - `source "$(akm vault path vault:<name>)"` — direct shell loading path.
|
|
9
|
+
* - `injectIntoEnv(vaultPath, target)` / `loadEnv(vaultPath)` — programmatic
|
|
10
|
+
* APIs for modules that need values in process memory.
|
|
15
11
|
*
|
|
16
12
|
* Value parsing is delegated to the `dotenv` package — we deliberately do not
|
|
17
13
|
* implement our own quoting/escaping rules for security-sensitive content.
|
|
@@ -19,6 +15,7 @@
|
|
|
19
15
|
import fs from "node:fs";
|
|
20
16
|
import path from "node:path";
|
|
21
17
|
import dotenv from "dotenv";
|
|
18
|
+
import { writeFileAtomic } from "../core/common";
|
|
22
19
|
/** Matches a KEY=value assignment line, capturing only the key. */
|
|
23
20
|
const ASSIGN_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
24
21
|
/** Scan lines and return KEY names in file order, without duplicates. */
|
|
@@ -144,8 +141,7 @@ export function injectIntoEnv(vaultPath, target = process.env) {
|
|
|
144
141
|
* non-assignment content, so sourcing the output is safe regardless of what
|
|
145
142
|
* the vault file contains.
|
|
146
143
|
*
|
|
147
|
-
*
|
|
148
|
-
* temp file and emits only the path (never values) on stdout.
|
|
144
|
+
* Retained for programmatic callers/tests that need a literal export script.
|
|
149
145
|
*/
|
|
150
146
|
export function buildShellExportScript(vaultPath) {
|
|
151
147
|
const env = loadEnv(vaultPath);
|
|
@@ -227,7 +223,7 @@ export function setKey(vaultPath, key, value, comment) {
|
|
|
227
223
|
let out = lines.join("\n");
|
|
228
224
|
if (!out.endsWith("\n"))
|
|
229
225
|
out += "\n";
|
|
230
|
-
writeFileAtomic(vaultPath, out);
|
|
226
|
+
writeFileAtomic(vaultPath, out, 0o600);
|
|
231
227
|
}
|
|
232
228
|
/** Remove a key from the vault file. Returns true if the key was present. */
|
|
233
229
|
export function unsetKey(vaultPath, key) {
|
|
@@ -264,7 +260,7 @@ export function createVault(vaultPath) {
|
|
|
264
260
|
* Characters that are safe in an UNquoted dotenv value AND are not
|
|
265
261
|
* metacharacters in POSIX shells. Anything outside this set forces quoting,
|
|
266
262
|
* which is defense-in-depth for any caller that might ever `source` the
|
|
267
|
-
* vault file directly instead of going through `akm vault
|
|
263
|
+
* vault file directly instead of going through `akm vault path`.
|
|
268
264
|
*/
|
|
269
265
|
const UNQUOTED_SAFE_RE = /^[A-Za-z0-9_.:/@%+,-]+$/;
|
|
270
266
|
/**
|
|
@@ -309,25 +305,3 @@ function ensureParentDir(filePath) {
|
|
|
309
305
|
if (!fs.existsSync(dir))
|
|
310
306
|
fs.mkdirSync(dir, { recursive: true });
|
|
311
307
|
}
|
|
312
|
-
function writeFileAtomic(filePath, content) {
|
|
313
|
-
const tmp = `${filePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
314
|
-
try {
|
|
315
|
-
fs.writeFileSync(tmp, content, { encoding: "utf8", mode: 0o600 });
|
|
316
|
-
fs.renameSync(tmp, filePath);
|
|
317
|
-
try {
|
|
318
|
-
fs.chmodSync(filePath, 0o600);
|
|
319
|
-
}
|
|
320
|
-
catch {
|
|
321
|
-
/* best-effort on platforms without chmod */
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
catch (err) {
|
|
325
|
-
try {
|
|
326
|
-
fs.unlinkSync(tmp);
|
|
327
|
-
}
|
|
328
|
-
catch {
|
|
329
|
-
/* ignore cleanup failure */
|
|
330
|
-
}
|
|
331
|
-
throw err;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defaultRendererRegistry } from "./asset-registry";
|
|
2
|
+
function registryActionContributor(registry) {
|
|
3
|
+
return {
|
|
4
|
+
name: "registry-action-contributor",
|
|
5
|
+
appliesTo(ctx) {
|
|
6
|
+
return registry.actionBuilderFor(ctx.type) !== undefined;
|
|
7
|
+
},
|
|
8
|
+
buildAction(ctx) {
|
|
9
|
+
return registry.actionBuilderFor(ctx.type)?.(ctx.ref);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function defaultActionContributors(registry = defaultRendererRegistry) {
|
|
14
|
+
return [registryActionContributor(registry)];
|
|
15
|
+
}
|
|
16
|
+
export function buildActionFromContributors(ctx, contributors = defaultActionContributors()) {
|
|
17
|
+
for (const contributor of contributors) {
|
|
18
|
+
if (!contributor.appliesTo(ctx))
|
|
19
|
+
continue;
|
|
20
|
+
const action = contributor.buildAction(ctx);
|
|
21
|
+
if (action !== undefined)
|
|
22
|
+
return action;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
@@ -18,10 +18,12 @@ export const TYPE_TO_RENDERER = {
|
|
|
18
18
|
command: "command-md",
|
|
19
19
|
agent: "agent-md",
|
|
20
20
|
knowledge: "knowledge-md",
|
|
21
|
+
lesson: "lesson-md",
|
|
21
22
|
memory: "memory-md",
|
|
22
23
|
workflow: "workflow-md",
|
|
23
24
|
vault: "vault-env",
|
|
24
25
|
wiki: "wiki-md",
|
|
26
|
+
task: "task-md",
|
|
25
27
|
};
|
|
26
28
|
/** Map asset types to action builder functions for search results. */
|
|
27
29
|
export const ACTION_BUILDERS = {
|
|
@@ -30,10 +32,12 @@ export const ACTION_BUILDERS = {
|
|
|
30
32
|
command: (ref) => `akm show ${ref} -> fill placeholders and dispatch`,
|
|
31
33
|
agent: (ref) => `akm show ${ref} -> dispatch with full prompt`,
|
|
32
34
|
knowledge: (ref) => `akm show ${ref} -> read reference material`,
|
|
35
|
+
lesson: (ref) => `akm show ${ref} -> read the lesson and apply when_to_use`,
|
|
33
36
|
memory: (ref) => `akm show ${ref} -> recall context`,
|
|
34
37
|
workflow: (ref) => buildWorkflowAction(ref),
|
|
35
|
-
vault: (ref) => `akm
|
|
38
|
+
vault: (ref) => `akm show ${ref} -> inspect keys; source "$(akm vault path ${ref})" -> load values; akm vault run ${ref} -- <command> -> run with injected env`,
|
|
36
39
|
wiki: (ref) => `akm show ${ref} -> read the wiki page`,
|
|
40
|
+
task: (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`,
|
|
37
41
|
};
|
|
38
42
|
/**
|
|
39
43
|
* Register a type-to-renderer mapping.
|
|
@@ -61,19 +65,3 @@ export const defaultRendererRegistry = {
|
|
|
61
65
|
return ACTION_BUILDERS[type];
|
|
62
66
|
},
|
|
63
67
|
};
|
|
64
|
-
/**
|
|
65
|
-
* Build a registry from explicit maps. Useful for tests that need to assert
|
|
66
|
-
* rendering behavior without touching the global singletons.
|
|
67
|
-
*/
|
|
68
|
-
export function createRendererRegistry(maps) {
|
|
69
|
-
const renderers = maps.renderers ?? {};
|
|
70
|
-
const actionBuilders = maps.actionBuilders ?? {};
|
|
71
|
-
return {
|
|
72
|
-
rendererNameFor(type) {
|
|
73
|
-
return renderers[type];
|
|
74
|
-
},
|
|
75
|
-
actionBuilderFor(type) {
|
|
76
|
-
return actionBuilders[type];
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}
|
package/dist/core/asset-spec.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { buildWorkflowAction } from "../output/renderers";
|
|
3
3
|
import { registerActionBuilder, registerTypeRenderer } from "./asset-registry";
|
|
4
4
|
import { toPosix } from "./common";
|
|
5
|
+
const buildTaskAction = (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`;
|
|
5
6
|
const markdownSpec = {
|
|
6
7
|
isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
|
|
7
8
|
toCanonicalName: (typeRoot, filePath) => {
|
|
@@ -82,7 +83,7 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
82
83
|
return path.join(typeRoot, name.endsWith(".env") ? name : `${name}.env`);
|
|
83
84
|
},
|
|
84
85
|
rendererName: "vault-env",
|
|
85
|
-
actionBuilder: (ref) => `akm
|
|
86
|
+
actionBuilder: (ref) => `akm show ${ref} -> inspect keys; source "$(akm vault path ${ref})" -> load values; akm vault run ${ref} -- <command> -> run with injected env`,
|
|
86
87
|
},
|
|
87
88
|
wiki: {
|
|
88
89
|
stashDir: "wikis",
|
|
@@ -102,6 +103,15 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
102
103
|
rendererName: "lesson-md",
|
|
103
104
|
actionBuilder: (ref) => `akm show ${ref} -> read the lesson and apply when_to_use`,
|
|
104
105
|
},
|
|
106
|
+
// Scheduled tasks. A task file pairs a cron-style schedule with a target
|
|
107
|
+
// (workflow ref or prompt) that `akm tasks` registers with the OS-native
|
|
108
|
+
// scheduler (cron / launchd / schtasks). Stored under <stash>/tasks/<id>.md.
|
|
109
|
+
task: {
|
|
110
|
+
stashDir: "tasks",
|
|
111
|
+
...markdownSpec,
|
|
112
|
+
rendererName: "task-md",
|
|
113
|
+
actionBuilder: buildTaskAction,
|
|
114
|
+
},
|
|
105
115
|
};
|
|
106
116
|
export const ASSET_SPECS = ASSET_SPECS_INTERNAL;
|
|
107
117
|
/**
|