akm-cli 0.7.5 → 0.8.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{.github/CHANGELOG.md → CHANGELOG.md} +113 -2
- package/README.md +20 -4
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +133 -0
- package/dist/cli.js +1995 -551
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +1531 -0
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +990 -75
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +5 -23
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +400 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +77 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-profiles.js +146 -0
- package/dist/commands/improve-result-file.js +103 -0
- package/dist/commands/improve.js +2175 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/index.js +183 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/vault-key-rules.js +139 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal.js +66 -12
- package/dist/commands/propose.js +86 -31
- package/dist/commands/reflect.js +1119 -73
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember.js +69 -6
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +144 -25
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +438 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/commands/vault.js +130 -77
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +7 -0
- package/dist/core/asset-registry.js +7 -16
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +22 -0
- package/dist/core/common.js +157 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +625 -0
- package/dist/core/config-schema.js +501 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +327 -987
- package/dist/core/errors.js +40 -19
- package/dist/core/events.js +91 -138
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +3 -6
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +326 -14
- package/dist/core/proposal-quality-validators.js +364 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +498 -42
- package/dist/core/state-db.js +927 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/warn.js +62 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +152 -253
- package/dist/indexer/db.js +933 -103
- package/dist/indexer/ensure-index.js +64 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -124
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +506 -291
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +148 -160
- package/dist/indexer/memory-inference.js +99 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +255 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +5 -16
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +150 -74
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +118 -23
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +68 -0
- package/dist/integrations/session-logs/providers/claude-code.js +59 -0
- package/dist/integrations/session-logs/providers/opencode.js +55 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +72 -124
- package/dist/llm/embedder.js +3 -19
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +3 -0
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +89 -48
- package/dist/llm/graph-extract.js +676 -70
- package/dist/llm/index-passes.js +9 -23
- package/dist/llm/memory-infer.js +52 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +281 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +5 -318
- package/dist/output/context.js +3 -0
- package/dist/output/renderers.js +223 -256
- package/dist/output/shapes.js +150 -105
- package/dist/output/text.js +318 -30
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +3 -0
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +70 -49
- package/dist/registry/providers/static-index.js +53 -48
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17307 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +7 -5
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +211 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +11 -3
- package/dist/workflows/runs.js +62 -91
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +9 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +20 -8
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -381
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
* launchd backend for `akm tasks` (macOS default).
|
|
6
|
+
*
|
|
7
|
+
* Each task is written as a per-user LaunchAgent plist at
|
|
8
|
+
* `~/Library/LaunchAgents/com.akm.task.<id>.plist` and registered via
|
|
9
|
+
* `launchctl bootstrap gui/<uid> <plist>`. Disabling uses
|
|
10
|
+
* `launchctl disable gui/<uid>/<label>` and re-enabling uses `enable`.
|
|
11
|
+
*
|
|
12
|
+
* Platform notes:
|
|
13
|
+
* • The `bootstrap` / `bootout` / `enable` / `disable` subcommands require
|
|
14
|
+
* macOS 10.10 (Yosemite) or newer. On older systems the equivalents
|
|
15
|
+
* are `launchctl load -w` / `unload -w`. We only target modern macOS.
|
|
16
|
+
* • `gui/<uid>` is the per-user GUI launchd domain — agents in this
|
|
17
|
+
* domain only run while the user is logged in (no background runs at
|
|
18
|
+
* the loginwindow). Tasks that need to run when the user is logged
|
|
19
|
+
* out should be installed as system Daemons, which is out of scope.
|
|
20
|
+
*
|
|
21
|
+
* Tests inject a fake exec + filesystem so the backend can be unit-tested
|
|
22
|
+
* without touching the host launchctl.
|
|
23
|
+
*/
|
|
24
|
+
import fs from "node:fs";
|
|
25
|
+
import os from "node:os";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { ConfigError } from "../../core/errors";
|
|
28
|
+
import { getTaskLogDir } from "../../core/paths";
|
|
29
|
+
import { resolveAkmInvocation } from "../resolveAkmBin";
|
|
30
|
+
import { parseSchedule, translateToLaunchd } from "../schedule";
|
|
31
|
+
import { escapeXml, spawnCommand } from "./exec-utils";
|
|
32
|
+
import launchdTemplate from "./launchd-template.xml" with { type: "text" };
|
|
33
|
+
export const LAUNCHD_LABEL_PREFIX = "com.akm.task.";
|
|
34
|
+
export function LAUNCHD_BACKEND(options = {}) {
|
|
35
|
+
const exec = options.exec ?? defaultLaunchdExec();
|
|
36
|
+
const fsLike = options.fs ?? defaultLaunchdFs();
|
|
37
|
+
const agentsDir = options.agentsDir ?? defaultAgentsDir();
|
|
38
|
+
const logDir = options.logDir ?? getTaskLogDir();
|
|
39
|
+
const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
|
|
40
|
+
const plistPath = (id) => path.join(agentsDir, `${LAUNCHD_LABEL_PREFIX}${id}.plist`);
|
|
41
|
+
const label = (id) => `${LAUNCHD_LABEL_PREFIX}${id}`;
|
|
42
|
+
const target = (id) => `gui/${exec.uid()}/${label(id)}`;
|
|
43
|
+
return {
|
|
44
|
+
name: "launchd",
|
|
45
|
+
install(task) {
|
|
46
|
+
// Capture PATH at install time so launchd (which strips the environment
|
|
47
|
+
// aggressively) can find the same binaries the user sees interactively.
|
|
48
|
+
let pathEnv;
|
|
49
|
+
if (options.envPath === false) {
|
|
50
|
+
pathEnv = undefined;
|
|
51
|
+
}
|
|
52
|
+
else if (typeof options.envPath === "string") {
|
|
53
|
+
pathEnv = options.envPath;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
pathEnv = process.env.PATH ?? "";
|
|
57
|
+
}
|
|
58
|
+
const xml = buildPlistXml(task, akmArgv, logDir, pathEnv);
|
|
59
|
+
fsLike.ensureDir(agentsDir);
|
|
60
|
+
// launchd refuses to start a job when StandardOutPath/StandardErrorPath
|
|
61
|
+
// points at a non-existent directory; create it before bootstrap.
|
|
62
|
+
fsLike.ensureDir(logDir);
|
|
63
|
+
fsLike.writeFile(plistPath(task.id), xml);
|
|
64
|
+
const bootout = exec.run(["launchctl", "bootout", target(task.id)]);
|
|
65
|
+
// bootout returning non-zero is fine — agent might not be loaded.
|
|
66
|
+
void bootout;
|
|
67
|
+
const bootstrap = exec.run(["launchctl", "bootstrap", `gui/${exec.uid()}`, plistPath(task.id)]);
|
|
68
|
+
if (bootstrap.status !== 0) {
|
|
69
|
+
throw new ConfigError(`launchctl bootstrap failed (exit ${bootstrap.status}): ${bootstrap.stderr || bootstrap.stdout || "no output"}.`, "INVALID_CONFIG_FILE", "Ensure `launchctl` is available; on macOS it is part of the base system.");
|
|
70
|
+
}
|
|
71
|
+
if (!task.enabled) {
|
|
72
|
+
const disable = exec.run(["launchctl", "disable", target(task.id)]);
|
|
73
|
+
if (disable.status !== 0) {
|
|
74
|
+
throw new ConfigError(`launchctl disable failed: ${disable.stderr || disable.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
uninstall(id) {
|
|
79
|
+
// Bootout first (may fail if agent never loaded — that's fine).
|
|
80
|
+
exec.run(["launchctl", "bootout", target(id)]);
|
|
81
|
+
const file = plistPath(id);
|
|
82
|
+
if (fsLike.exists(file))
|
|
83
|
+
fsLike.removeFile(file);
|
|
84
|
+
},
|
|
85
|
+
setEnabled(id, enabled) {
|
|
86
|
+
const verb = enabled ? "enable" : "disable";
|
|
87
|
+
const r = exec.run(["launchctl", verb, target(id)]);
|
|
88
|
+
if (r.status !== 0) {
|
|
89
|
+
throw new ConfigError(`launchctl ${verb} failed: ${r.stderr || r.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
list() {
|
|
93
|
+
if (!fsLike.exists(agentsDir))
|
|
94
|
+
return [];
|
|
95
|
+
const ids = [];
|
|
96
|
+
for (const file of fsLike.list(agentsDir)) {
|
|
97
|
+
if (file.startsWith(LAUNCHD_LABEL_PREFIX) && file.endsWith(".plist")) {
|
|
98
|
+
ids.push(file.slice(LAUNCHD_LABEL_PREFIX.length, -".plist".length));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return ids.map((id) => ({ id }));
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// ── XML builder (exported for tests) ────────────────────────────────────────
|
|
106
|
+
export function buildPlistXml(task, akmArgv, logDir, pathEnv) {
|
|
107
|
+
const spec = parseSchedule(task.schedule, "launchd");
|
|
108
|
+
const trigger = translateToLaunchd(spec);
|
|
109
|
+
const argv = [...akmArgv, "tasks", "run", task.id];
|
|
110
|
+
const programArgs = argv.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
111
|
+
const logPath = path.join(logDir, `${task.id}.log`);
|
|
112
|
+
const triggerXml = renderLaunchdTrigger(trigger);
|
|
113
|
+
const envVarsXml = pathEnv !== undefined
|
|
114
|
+
? ` <key>EnvironmentVariables</key>\n <dict>\n <key>PATH</key>\n <string>${escapeXml(pathEnv)}</string>\n </dict>\n`
|
|
115
|
+
: "";
|
|
116
|
+
return launchdTemplate
|
|
117
|
+
.replace("{{LABEL}}", LAUNCHD_LABEL_PREFIX + escapeXml(task.id))
|
|
118
|
+
.replace("{{PROGRAM_ARGS}}", programArgs)
|
|
119
|
+
.replaceAll("{{LOG_PATH}}", escapeXml(logPath))
|
|
120
|
+
.replace("{{ENV_VARS}}", envVarsXml)
|
|
121
|
+
.replace("{{TRIGGER_XML}}", triggerXml);
|
|
122
|
+
}
|
|
123
|
+
function renderLaunchdTrigger(trigger) {
|
|
124
|
+
if (trigger.intervalSeconds !== undefined) {
|
|
125
|
+
return ` <key>StartInterval</key>
|
|
126
|
+
<integer>${trigger.intervalSeconds}</integer>`;
|
|
127
|
+
}
|
|
128
|
+
const cal = trigger.calendar ?? {};
|
|
129
|
+
const lines = [" <key>StartCalendarInterval</key>", " <dict>"];
|
|
130
|
+
if (cal.Minute !== undefined)
|
|
131
|
+
lines.push(` <key>Minute</key><integer>${cal.Minute}</integer>`);
|
|
132
|
+
if (cal.Hour !== undefined)
|
|
133
|
+
lines.push(` <key>Hour</key><integer>${cal.Hour}</integer>`);
|
|
134
|
+
if (cal.Day !== undefined)
|
|
135
|
+
lines.push(` <key>Day</key><integer>${cal.Day}</integer>`);
|
|
136
|
+
if (cal.Month !== undefined)
|
|
137
|
+
lines.push(` <key>Month</key><integer>${cal.Month}</integer>`);
|
|
138
|
+
if (cal.Weekday !== undefined)
|
|
139
|
+
lines.push(` <key>Weekday</key><integer>${cal.Weekday}</integer>`);
|
|
140
|
+
lines.push(" </dict>");
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
function defaultAgentsDir() {
|
|
144
|
+
// launchd's per-user LaunchAgents live under the user's home directory.
|
|
145
|
+
// If we can't determine HOME, refuse rather than silently producing a
|
|
146
|
+
// relative path that would write somewhere unexpected.
|
|
147
|
+
const home = os.homedir();
|
|
148
|
+
if (!home) {
|
|
149
|
+
throw new ConfigError("Cannot determine user home directory; launchd backend requires HOME to locate ~/Library/LaunchAgents.", "INVALID_CONFIG_FILE", "Set $HOME (POSIX) or the equivalent before running `akm tasks` on macOS.");
|
|
150
|
+
}
|
|
151
|
+
return path.join(home, "Library", "LaunchAgents");
|
|
152
|
+
}
|
|
153
|
+
function defaultLaunchdExec() {
|
|
154
|
+
return {
|
|
155
|
+
run(args) {
|
|
156
|
+
return spawnCommand(args);
|
|
157
|
+
},
|
|
158
|
+
uid() {
|
|
159
|
+
const fn = process.getuid;
|
|
160
|
+
return typeof fn === "function" ? fn.call(process) : 0;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function defaultLaunchdFs() {
|
|
165
|
+
return {
|
|
166
|
+
writeFile(file, content) {
|
|
167
|
+
fs.writeFileSync(file, content, { encoding: "utf8" });
|
|
168
|
+
},
|
|
169
|
+
removeFile(file) {
|
|
170
|
+
fs.rmSync(file, { force: true });
|
|
171
|
+
},
|
|
172
|
+
ensureDir(dir) {
|
|
173
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
174
|
+
},
|
|
175
|
+
list(dir) {
|
|
176
|
+
try {
|
|
177
|
+
return fs.readdirSync(dir);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
exists(file) {
|
|
184
|
+
return fs.existsSync(file);
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
3
|
+
<RegistrationInfo>
|
|
4
|
+
<Description>akm scheduled task: {{TASK_ID}}</Description>
|
|
5
|
+
<URI>{{FOLDER}}{{TASK_ID}}</URI>
|
|
6
|
+
</RegistrationInfo>
|
|
7
|
+
<Triggers>
|
|
8
|
+
{{TRIGGER_XML}}
|
|
9
|
+
</Triggers>
|
|
10
|
+
<Principals>
|
|
11
|
+
<Principal id="Author">
|
|
12
|
+
<LogonType>InteractiveToken</LogonType>
|
|
13
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
14
|
+
</Principal>
|
|
15
|
+
</Principals>
|
|
16
|
+
<Settings>
|
|
17
|
+
<Enabled>{{ENABLED}}</Enabled>
|
|
18
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
19
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
20
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
21
|
+
</Settings>
|
|
22
|
+
<Actions Context="Author">
|
|
23
|
+
<Exec>
|
|
24
|
+
<Command>{{COMMAND}}</Command>
|
|
25
|
+
<Arguments>{{ARGS}}</Arguments>
|
|
26
|
+
</Exec>
|
|
27
|
+
</Actions>
|
|
28
|
+
<!-- Log target (informational only; schtasks doesn't redirect): {{LOG_PATH}} -->
|
|
29
|
+
</Task>
|
|
@@ -0,0 +1,215 @@
|
|
|
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
|
+
* schtasks.exe backend for `akm tasks` (Windows default).
|
|
6
|
+
*
|
|
7
|
+
* Each task is registered under the `\akm\` Task Scheduler folder so the
|
|
8
|
+
* backend never touches user-managed tasks. The full task definition is
|
|
9
|
+
* sent through `schtasks /Create /TN \akm\<id> /XML <path>` so we can
|
|
10
|
+
* express triggers/principals/actions without quoting hell.
|
|
11
|
+
*
|
|
12
|
+
* Platform notes:
|
|
13
|
+
* • `LogonType=InteractiveToken` means the task runs in the context of
|
|
14
|
+
* the registering user only when they are logged in — there is no
|
|
15
|
+
* stored password and the task will not fire at the lock screen.
|
|
16
|
+
* • `<Principal>` deliberately omits `<UserId>`; per the Task Scheduler
|
|
17
|
+
* 2.0 schema (`principalType.UserId` minOccurs=0) this is valid and
|
|
18
|
+
* defaults to the registering user.
|
|
19
|
+
* • `<DisallowStartIfOnBatteries>false</…>` and `<StopIfGoingOnBatteries>
|
|
20
|
+
* false</…>` allow the task to run on battery — utility tasks would
|
|
21
|
+
* otherwise be silently skipped on laptops.
|
|
22
|
+
* • `MultipleInstancesPolicy=IgnoreNew` makes overlapping triggers safe:
|
|
23
|
+
* while a task is still running, a new fire is dropped rather than
|
|
24
|
+
* queued or run in parallel.
|
|
25
|
+
* • `/Query /FO CSV /NH` (without `/V`) outputs three columns:
|
|
26
|
+
* `TaskName,Next Run Time,Status` — so the regex anchors on the task
|
|
27
|
+
* name as the leading quoted field. Adding `/V` would shift HostName
|
|
28
|
+
* into column 0; we deliberately don't.
|
|
29
|
+
*
|
|
30
|
+
* Tests inject a fake exec + filesystem.
|
|
31
|
+
*/
|
|
32
|
+
import fs from "node:fs";
|
|
33
|
+
import os from "node:os";
|
|
34
|
+
import path from "node:path";
|
|
35
|
+
import { ConfigError } from "../../core/errors";
|
|
36
|
+
import { getTaskLogDir } from "../../core/paths";
|
|
37
|
+
import { resolveAkmInvocation } from "../resolveAkmBin";
|
|
38
|
+
import { parseSchedule, translateToSchtasks } from "../schedule";
|
|
39
|
+
import { escapeXml, spawnCommand } from "./exec-utils";
|
|
40
|
+
import schtasksTemplate from "./schtasks-template.xml" with { type: "text" };
|
|
41
|
+
export const DEFAULT_FOLDER_PREFIX = "\\akm\\";
|
|
42
|
+
export function SCHTASKS_BACKEND(options = {}) {
|
|
43
|
+
const exec = options.exec ?? defaultSchtasksExec();
|
|
44
|
+
const fsLike = options.fs ?? defaultSchtasksFs();
|
|
45
|
+
const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
|
|
46
|
+
const logDir = options.logDir ?? getTaskLogDir();
|
|
47
|
+
const folder = options.folderPrefix ?? DEFAULT_FOLDER_PREFIX;
|
|
48
|
+
const taskName = (id) => `${folder}${id}`;
|
|
49
|
+
return {
|
|
50
|
+
name: "schtasks",
|
|
51
|
+
install(task) {
|
|
52
|
+
fsLike.ensureDir(logDir);
|
|
53
|
+
const xml = buildSchtasksXml(task, akmArgv, logDir, { folderPrefix: folder });
|
|
54
|
+
const tmpFile = path.join(fsLike.tmpdir(), `akm-task-${task.id}-${Date.now()}.xml`);
|
|
55
|
+
fsLike.writeFile(tmpFile, xml);
|
|
56
|
+
try {
|
|
57
|
+
// /F forces overwrite if a task with the same name exists.
|
|
58
|
+
const r = exec.run(["schtasks", "/Create", "/TN", taskName(task.id), "/XML", tmpFile, "/F"]);
|
|
59
|
+
if (r.status !== 0) {
|
|
60
|
+
throw new ConfigError(`schtasks /Create failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
|
|
61
|
+
}
|
|
62
|
+
if (!task.enabled) {
|
|
63
|
+
const dis = exec.run(["schtasks", "/Change", "/TN", taskName(task.id), "/DISABLE"]);
|
|
64
|
+
if (dis.status !== 0) {
|
|
65
|
+
throw new ConfigError(`schtasks /Change /DISABLE failed: ${dis.stderr || dis.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
fsLike.removeFile(tmpFile);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
uninstall(id) {
|
|
74
|
+
const r = exec.run(["schtasks", "/Delete", "/TN", taskName(id), "/F"]);
|
|
75
|
+
if (r.status !== 0 && !/cannot find/i.test(r.stderr ?? "")) {
|
|
76
|
+
throw new ConfigError(`schtasks /Delete failed: ${r.stderr || r.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
setEnabled(id, enabled) {
|
|
80
|
+
const flag = enabled ? "/ENABLE" : "/DISABLE";
|
|
81
|
+
const r = exec.run(["schtasks", "/Change", "/TN", taskName(id), flag]);
|
|
82
|
+
if (r.status !== 0) {
|
|
83
|
+
throw new ConfigError(`schtasks /Change ${flag} failed: ${r.stderr || r.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
list() {
|
|
87
|
+
const r = exec.run(["schtasks", "/Query", "/FO", "CSV", "/NH"]);
|
|
88
|
+
if (r.status !== 0)
|
|
89
|
+
return [];
|
|
90
|
+
const ids = [];
|
|
91
|
+
for (const line of (r.stdout ?? "").split(/\r?\n/)) {
|
|
92
|
+
const m = line.match(/^"([^"]+)",/);
|
|
93
|
+
if (!m)
|
|
94
|
+
continue;
|
|
95
|
+
const name = m[1];
|
|
96
|
+
if (name.startsWith(folder)) {
|
|
97
|
+
ids.push(name.slice(folder.length));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return ids.map((id) => ({ id }));
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function buildSchtasksXml(task, akmArgv, logDir, options = {}) {
|
|
105
|
+
const folder = options.folderPrefix ?? DEFAULT_FOLDER_PREFIX;
|
|
106
|
+
const now = options.now ? options.now() : new Date();
|
|
107
|
+
const startBoundary = formatStartBoundary(now);
|
|
108
|
+
const spec = parseSchedule(task.schedule, "schtasks");
|
|
109
|
+
const trigger = translateToSchtasks(spec);
|
|
110
|
+
const command = akmArgv[0];
|
|
111
|
+
const args = [...akmArgv.slice(1), "tasks", "run", task.id].map((a) => quoteArg(a)).join(" ");
|
|
112
|
+
const triggerXml = renderSchtasksTrigger(trigger, startBoundary);
|
|
113
|
+
const logPath = path.join(logDir, `${task.id}.log`);
|
|
114
|
+
return schtasksTemplate
|
|
115
|
+
.replaceAll("{{TASK_ID}}", escapeXml(task.id))
|
|
116
|
+
.replaceAll("{{FOLDER}}", escapeXml(folder))
|
|
117
|
+
.replace("{{TRIGGER_XML}}", triggerXml)
|
|
118
|
+
.replace("{{ENABLED}}", task.enabled ? "true" : "false")
|
|
119
|
+
.replace("{{COMMAND}}", escapeXml(command))
|
|
120
|
+
.replace("{{ARGS}}", escapeXml(args))
|
|
121
|
+
.replace("{{LOG_PATH}}", escapeXml(logPath));
|
|
122
|
+
}
|
|
123
|
+
function renderSchtasksTrigger(trigger, startBoundary) {
|
|
124
|
+
switch (trigger.kind) {
|
|
125
|
+
case "minute":
|
|
126
|
+
return ` <TimeTrigger>
|
|
127
|
+
<Repetition>
|
|
128
|
+
<Interval>PT${trigger.everyMinutes}M</Interval>
|
|
129
|
+
</Repetition>
|
|
130
|
+
<StartBoundary>${startBoundary}</StartBoundary>
|
|
131
|
+
<Enabled>true</Enabled>
|
|
132
|
+
</TimeTrigger>`;
|
|
133
|
+
case "hour":
|
|
134
|
+
return ` <TimeTrigger>
|
|
135
|
+
<Repetition>
|
|
136
|
+
<Interval>PT${trigger.everyHours}H</Interval>
|
|
137
|
+
</Repetition>
|
|
138
|
+
<StartBoundary>${startBoundary}</StartBoundary>
|
|
139
|
+
<Enabled>true</Enabled>
|
|
140
|
+
</TimeTrigger>`;
|
|
141
|
+
case "daily":
|
|
142
|
+
return ` <CalendarTrigger>
|
|
143
|
+
<StartBoundary>${pad(startBoundary, trigger.atHour, trigger.atMinute)}</StartBoundary>
|
|
144
|
+
<Enabled>true</Enabled>
|
|
145
|
+
<ScheduleByDay><DaysInterval>1</DaysInterval></ScheduleByDay>
|
|
146
|
+
</CalendarTrigger>`;
|
|
147
|
+
case "weekly": {
|
|
148
|
+
const dayMap = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
149
|
+
const days = trigger.daysOfWeek.map((d) => ` <${dayMap[d]} />`).join("\n");
|
|
150
|
+
return ` <CalendarTrigger>
|
|
151
|
+
<StartBoundary>${pad(startBoundary, trigger.atHour, trigger.atMinute)}</StartBoundary>
|
|
152
|
+
<Enabled>true</Enabled>
|
|
153
|
+
<ScheduleByWeek>
|
|
154
|
+
<DaysOfWeek>
|
|
155
|
+
${days}
|
|
156
|
+
</DaysOfWeek>
|
|
157
|
+
<WeeksInterval>1</WeeksInterval>
|
|
158
|
+
</ScheduleByWeek>
|
|
159
|
+
</CalendarTrigger>`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function pad(base, hour, minute) {
|
|
164
|
+
// Rewrite the time component of an ISO-8601 boundary while preserving
|
|
165
|
+
// the date so daily/weekly triggers fire at the configured wall-clock
|
|
166
|
+
// time rather than the install instant.
|
|
167
|
+
const hh = String(hour).padStart(2, "0");
|
|
168
|
+
const mm = String(minute).padStart(2, "0");
|
|
169
|
+
return base.replace(/T\d\d:\d\d:\d\d$/, `T${hh}:${mm}:00`);
|
|
170
|
+
}
|
|
171
|
+
function formatStartBoundary(d) {
|
|
172
|
+
// Local-time ISO-8601 (no zone suffix) — Task Scheduler interprets a
|
|
173
|
+
// bare boundary in the registering user's timezone, which matches what
|
|
174
|
+
// a user typing "0 9 * * *" intuitively means ("9am local").
|
|
175
|
+
const yyyy = d.getFullYear();
|
|
176
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
177
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
178
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
179
|
+
const mi = String(d.getMinutes()).padStart(2, "0");
|
|
180
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
181
|
+
return `${yyyy}-${mm}-${dd}T${hh}:${mi}:${ss}`;
|
|
182
|
+
}
|
|
183
|
+
function quoteArg(s) {
|
|
184
|
+
if (/^[A-Za-z0-9_\-./@:%=+,\\]+$/.test(s))
|
|
185
|
+
return s;
|
|
186
|
+
return `"${s.replace(/"/g, '\\"')}"`;
|
|
187
|
+
}
|
|
188
|
+
function defaultSchtasksExec() {
|
|
189
|
+
return {
|
|
190
|
+
run(args) {
|
|
191
|
+
return spawnCommand(args);
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function defaultSchtasksFs() {
|
|
196
|
+
return {
|
|
197
|
+
writeFile(file, content) {
|
|
198
|
+
fs.writeFileSync(file, content, { encoding: "utf8" });
|
|
199
|
+
},
|
|
200
|
+
removeFile(file) {
|
|
201
|
+
try {
|
|
202
|
+
fs.rmSync(file, { force: true });
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* ignore */
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
ensureDir(dir) {
|
|
209
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
210
|
+
},
|
|
211
|
+
tmpdir() {
|
|
212
|
+
return os.tmpdir();
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
* Parse a task YAML file into a {@link TaskDocument}.
|
|
6
|
+
*
|
|
7
|
+
* The on-disk shape is a pure YAML file at `<stash>/tasks/<id>.yml`:
|
|
8
|
+
*
|
|
9
|
+
* ```yaml
|
|
10
|
+
* schedule: "0 9 * * *"
|
|
11
|
+
* # one of:
|
|
12
|
+
* workflow: workflow:daily-backup
|
|
13
|
+
* params:
|
|
14
|
+
* region: us-east-1
|
|
15
|
+
* # ...or:
|
|
16
|
+
* prompt: agent:my-agent # asset ref
|
|
17
|
+
* # ...or:
|
|
18
|
+
* prompt: ./prompts/my-prompt.md # relative file path
|
|
19
|
+
* # ...or:
|
|
20
|
+
* prompt: | # inline multi-line prompt (block scalar)
|
|
21
|
+
* Do the thing.
|
|
22
|
+
* And the other thing.
|
|
23
|
+
* # ...or:
|
|
24
|
+
* command: akm improve --auto-accept=90 --limit 25
|
|
25
|
+
* enabled: true # default true
|
|
26
|
+
* name: Daily backup
|
|
27
|
+
* description: …
|
|
28
|
+
* when_to_use: …
|
|
29
|
+
* tags: [scheduled, backup]
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Validation lives in {@link validateTaskDocument}. The parser only enforces
|
|
33
|
+
* shape; cron syntax, target reachability, and profile availability are
|
|
34
|
+
* checked separately so callers can choose how strictly to surface errors.
|
|
35
|
+
*/
|
|
36
|
+
import path from "node:path";
|
|
37
|
+
import { parse as parseYaml } from "yaml";
|
|
38
|
+
import { UsageError } from "../core/errors";
|
|
39
|
+
import { TASK_SCHEMA_VERSION } from "./schema";
|
|
40
|
+
export function parseTaskDocument(input) {
|
|
41
|
+
const { yaml, filePath, id } = input;
|
|
42
|
+
let data;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = parseYaml(yaml);
|
|
45
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
46
|
+
throw new UsageError(`Task "${id}" YAML must be a mapping (key: value pairs). File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
47
|
+
}
|
|
48
|
+
data = parsed;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err instanceof UsageError)
|
|
52
|
+
throw err;
|
|
53
|
+
throw new UsageError(`Task "${id}" has invalid YAML: ${err instanceof Error ? err.message : String(err)}. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
54
|
+
}
|
|
55
|
+
const schedule = readString(data.schedule, "schedule", filePath);
|
|
56
|
+
if (!schedule) {
|
|
57
|
+
throw new UsageError(`Task "${id}" is missing a schedule (YAML key "schedule"). File: ${filePath}`, "MISSING_REQUIRED_ARGUMENT");
|
|
58
|
+
}
|
|
59
|
+
const enabled = data.enabled === undefined ? true : data.enabled === true;
|
|
60
|
+
const name = readString(data.name, "name", filePath);
|
|
61
|
+
const description = readString(data.description, "description", filePath);
|
|
62
|
+
const when_to_use = readString(data.when_to_use, "when_to_use", filePath);
|
|
63
|
+
const tags = readStringArray(data.tags);
|
|
64
|
+
const hasWorkflow = "workflow" in data && data.workflow !== "" && data.workflow != null;
|
|
65
|
+
const hasPrompt = "prompt" in data && data.prompt !== "" && data.prompt != null;
|
|
66
|
+
const hasCommand = "command" in data && data.command !== "" && data.command != null;
|
|
67
|
+
const targetCount = [hasWorkflow, hasPrompt, hasCommand].filter(Boolean).length;
|
|
68
|
+
if (targetCount > 1) {
|
|
69
|
+
throw new UsageError(`Task "${id}" sets more than one of \`workflow\`, \`prompt\`, \`command\`; pick exactly one. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
70
|
+
}
|
|
71
|
+
if (targetCount === 0) {
|
|
72
|
+
throw new UsageError(`Task "${id}" must set one of \`workflow\`, \`prompt\`, or \`command\`. File: ${filePath}`, "MISSING_REQUIRED_ARGUMENT");
|
|
73
|
+
}
|
|
74
|
+
let target;
|
|
75
|
+
if (hasWorkflow) {
|
|
76
|
+
const ref = readString(data.workflow, "workflow", filePath);
|
|
77
|
+
if (!ref) {
|
|
78
|
+
throw new UsageError(`Task "${id}" has empty \`workflow\`. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
79
|
+
}
|
|
80
|
+
target = {
|
|
81
|
+
kind: "workflow",
|
|
82
|
+
ref,
|
|
83
|
+
params: readParams(data.params, filePath),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
else if (hasCommand) {
|
|
87
|
+
const cmd = readCommand(data.command, filePath, id);
|
|
88
|
+
target = { kind: "command", cmd };
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const promptRaw = readString(data.prompt, "prompt", filePath);
|
|
92
|
+
if (!promptRaw) {
|
|
93
|
+
throw new UsageError(`Task "${id}" has empty \`prompt\`. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
94
|
+
}
|
|
95
|
+
const profile = readString(data.profile, "profile", filePath);
|
|
96
|
+
target = {
|
|
97
|
+
kind: "prompt",
|
|
98
|
+
source: resolvePromptSource(promptRaw, filePath, id),
|
|
99
|
+
profile: profile && profile.length > 0 ? profile : undefined,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// null / 0 / negative → disabled (no timeout). Positive number → override.
|
|
103
|
+
// Omitted → undefined (inherits config.agent.timeoutMs).
|
|
104
|
+
let timeoutMs;
|
|
105
|
+
if ("timeoutMs" in data) {
|
|
106
|
+
const raw = data.timeoutMs;
|
|
107
|
+
if (raw === null || raw === "null" || raw === 0 || (typeof raw === "number" && raw < 0)) {
|
|
108
|
+
timeoutMs = null;
|
|
109
|
+
}
|
|
110
|
+
else if (typeof raw === "number" && raw > 0) {
|
|
111
|
+
timeoutMs = raw;
|
|
112
|
+
}
|
|
113
|
+
// non-numeric / unrecognised → leave as undefined (inherit)
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
schemaVersion: TASK_SCHEMA_VERSION,
|
|
117
|
+
id,
|
|
118
|
+
schedule,
|
|
119
|
+
enabled,
|
|
120
|
+
target,
|
|
121
|
+
name: name && name.length > 0 ? name : undefined,
|
|
122
|
+
description: description && description.length > 0 ? description : undefined,
|
|
123
|
+
when_to_use: when_to_use && when_to_use.length > 0 ? when_to_use : undefined,
|
|
124
|
+
tags: tags && tags.length > 0 ? tags : undefined,
|
|
125
|
+
source: { path: filePath },
|
|
126
|
+
timeoutMs,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Resolve a `prompt:` value into a {@link TaskPromptSource} variant.
|
|
131
|
+
*
|
|
132
|
+
* • "<type>:<name>" (asset ref) → asset
|
|
133
|
+
* • "./foo.md", "../foo.md", "/abs" → file
|
|
134
|
+
* • "C:\\abs" (Windows absolute) → file
|
|
135
|
+
* • anything else (incl. block scalars) → inline text
|
|
136
|
+
*/
|
|
137
|
+
function resolvePromptSource(raw, filePath, id) {
|
|
138
|
+
const trimmed = raw.trim();
|
|
139
|
+
if (trimmed.startsWith("./") || trimmed.startsWith("../") || path.isAbsolute(trimmed)) {
|
|
140
|
+
return { kind: "file", path: trimmed };
|
|
141
|
+
}
|
|
142
|
+
if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
143
|
+
return { kind: "file", path: trimmed };
|
|
144
|
+
}
|
|
145
|
+
if (/^[a-z][a-z0-9_-]*:[^\s]/i.test(trimmed)) {
|
|
146
|
+
return { kind: "asset", ref: trimmed };
|
|
147
|
+
}
|
|
148
|
+
if (!trimmed) {
|
|
149
|
+
throw new UsageError(`Task "${id}" has empty \`prompt\`. File: ${filePath}`, "MISSING_REQUIRED_ARGUMENT");
|
|
150
|
+
}
|
|
151
|
+
return { kind: "inline", text: trimmed };
|
|
152
|
+
}
|
|
153
|
+
function readString(value, key, filePath) {
|
|
154
|
+
if (value === undefined || value === null)
|
|
155
|
+
return undefined;
|
|
156
|
+
if (typeof value === "string")
|
|
157
|
+
return value.trim();
|
|
158
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
159
|
+
return String(value);
|
|
160
|
+
throw new UsageError(`Key "${key}" must be a string. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
161
|
+
}
|
|
162
|
+
function readStringArray(value) {
|
|
163
|
+
if (value === undefined || value === null)
|
|
164
|
+
return undefined;
|
|
165
|
+
if (typeof value === "string") {
|
|
166
|
+
return value
|
|
167
|
+
.split(/[\s,]+/)
|
|
168
|
+
.map((s) => s.trim())
|
|
169
|
+
.filter(Boolean);
|
|
170
|
+
}
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
return value.filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
function readCommand(value, filePath, id) {
|
|
177
|
+
if (Array.isArray(value)) {
|
|
178
|
+
const parts = value.filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
179
|
+
if (parts.length === 0) {
|
|
180
|
+
throw new UsageError(`Task "${id}" has empty \`command\` array. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
181
|
+
}
|
|
182
|
+
return parts;
|
|
183
|
+
}
|
|
184
|
+
if (typeof value === "string") {
|
|
185
|
+
const parts = value.trim().split(/\s+/).filter(Boolean);
|
|
186
|
+
if (parts.length === 0) {
|
|
187
|
+
throw new UsageError(`Task "${id}" has empty \`command\`. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
188
|
+
}
|
|
189
|
+
return parts;
|
|
190
|
+
}
|
|
191
|
+
throw new UsageError(`Key "command" must be a string or array of strings. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
192
|
+
}
|
|
193
|
+
function readParams(value, filePath) {
|
|
194
|
+
if (value === undefined || value === null)
|
|
195
|
+
return {};
|
|
196
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
if (typeof value === "string" && value.trim()) {
|
|
200
|
+
try {
|
|
201
|
+
const parsed = JSON.parse(value);
|
|
202
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
203
|
+
return parsed;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// fall through
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
throw new UsageError(`Key "params" must be a mapping or a JSON object. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
211
|
+
}
|