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,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `akm agent <profile> [--prompt <text>] [--command <ref>] [--workflow <ref>] [args...]`
|
|
3
|
+
*
|
|
4
|
+
* Dispatch an agent by named profile, optionally injecting a prompt from
|
|
5
|
+
* inline text, a stash command: asset, or a stash workflow: asset.
|
|
6
|
+
*
|
|
7
|
+
* When none of --prompt, --command, or --workflow are given, the agent is
|
|
8
|
+
* launched interactively (no injected prompt).
|
|
9
|
+
*
|
|
10
|
+
* Template placeholders (`{{0}}`, `{{1}}`, ...) in the loaded asset body are
|
|
11
|
+
* filled from the extra positional args in order.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import { parseAssetRef } from "../core/asset-ref";
|
|
15
|
+
import { NotFoundError, UsageError } from "../core/errors";
|
|
16
|
+
import { requireAgentProfile } from "../integrations/agent/config";
|
|
17
|
+
import { runWithAgentRunner } from "../integrations/agent/runners";
|
|
18
|
+
/**
|
|
19
|
+
* Fill `{{0}}`, `{{1}}`, ... placeholders in `template` with the
|
|
20
|
+
* corresponding entries in `args`. Any placeholder index that exceeds the
|
|
21
|
+
* args array is left as-is.
|
|
22
|
+
*/
|
|
23
|
+
function fillPlaceholders(template, args) {
|
|
24
|
+
return template.replace(/\{\{(\d+)\}\}/g, (match, idx) => {
|
|
25
|
+
const i = Number.parseInt(idx, 10);
|
|
26
|
+
return i < args.length ? args[i] : match;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the body of an asset by ref string. The ref must parse as a
|
|
31
|
+
* valid asset ref (e.g. `command:my-cmd`, `workflow:my-flow`). The file
|
|
32
|
+
* must exist on disk (the index provides the file path).
|
|
33
|
+
*
|
|
34
|
+
* Throws `NotFoundError` when the ref cannot be resolved.
|
|
35
|
+
*/
|
|
36
|
+
async function resolveAssetBody(ref) {
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = parseAssetRef(ref);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
throw new UsageError(`Invalid asset ref "${ref}": ${err instanceof Error ? err.message : String(err)}`, "INVALID_FLAG_VALUE");
|
|
43
|
+
}
|
|
44
|
+
// Lazy import to avoid pulling the full indexer at startup.
|
|
45
|
+
const { lookup } = await import("../indexer/indexer.js");
|
|
46
|
+
const entry = await lookup(parsed);
|
|
47
|
+
if (!entry) {
|
|
48
|
+
throw new NotFoundError(`Asset "${ref}" not found in the index. Run \`akm index\` to rebuild the index.`);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return fs.readFileSync(entry.filePath, "utf8");
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
throw new NotFoundError(`Asset "${ref}" is indexed but the file could not be read (${entry.filePath}): ${err instanceof Error ? err.message : String(err)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function akmAgentDispatch(options) {
|
|
58
|
+
if (!options.profileName?.trim()) {
|
|
59
|
+
throw new UsageError("agent: <profile> is required.", "MISSING_REQUIRED_ARGUMENT");
|
|
60
|
+
}
|
|
61
|
+
// Resolve the profile — throws ConfigError with an actionable hint when
|
|
62
|
+
// agent config is absent or the profile is not found.
|
|
63
|
+
const profile = requireAgentProfile(options.agentConfig, options.profileName.trim());
|
|
64
|
+
// Resolve the prompt text from whichever source was provided.
|
|
65
|
+
let prompt;
|
|
66
|
+
if (options.commandRef) {
|
|
67
|
+
const body = await resolveAssetBody(options.commandRef);
|
|
68
|
+
prompt = options.args?.length ? fillPlaceholders(body, options.args) : body;
|
|
69
|
+
}
|
|
70
|
+
else if (options.workflowRef) {
|
|
71
|
+
const body = await resolveAssetBody(options.workflowRef);
|
|
72
|
+
prompt = options.args?.length ? fillPlaceholders(body, options.args) : body;
|
|
73
|
+
}
|
|
74
|
+
else if (options.prompt !== undefined) {
|
|
75
|
+
prompt = options.prompt;
|
|
76
|
+
}
|
|
77
|
+
// When prompt is undefined, the agent is launched interactively.
|
|
78
|
+
const stdio = prompt === undefined && profile.sdkMode !== true ? "interactive" : profile.stdio;
|
|
79
|
+
const result = await runWithAgentRunner({
|
|
80
|
+
profile,
|
|
81
|
+
prompt,
|
|
82
|
+
llmConfig: options.llmConfig,
|
|
83
|
+
runOptions: {
|
|
84
|
+
stdio,
|
|
85
|
+
parseOutput: "text",
|
|
86
|
+
...(options.args?.length && !options.commandRef && !options.workflowRef ? { args: options.args } : {}),
|
|
87
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
schemaVersion: 1,
|
|
92
|
+
ok: result.ok,
|
|
93
|
+
shape: "agent-result",
|
|
94
|
+
profileName: profile.name,
|
|
95
|
+
exitCode: result.exitCode,
|
|
96
|
+
stdout: result.stdout ?? "",
|
|
97
|
+
stderr: result.stderr ?? "",
|
|
98
|
+
durationMs: result.durationMs,
|
|
99
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
100
|
+
...(result.reason !== undefined ? { reason: result.reason } : {}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for agent-based commands (reflect, propose, etc.).
|
|
3
|
+
*
|
|
4
|
+
* Consolidates utility functions that were duplicated byte-for-byte across
|
|
5
|
+
* `reflect.ts` and `propose.ts`. Any command that shells out to an agent
|
|
6
|
+
* profile can import from here rather than copy-pasting.
|
|
7
|
+
*/
|
|
8
|
+
import { loadConfig } from "../core/config";
|
|
9
|
+
import { parseAgentConfig, requireAgentProfile, } from "../integrations/agent";
|
|
10
|
+
// ── Config helpers ───────────────────────────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* Load the agent config block from the on-disk config file.
|
|
13
|
+
* Returns `undefined` when no agent block is present.
|
|
14
|
+
*/
|
|
15
|
+
export function loadAgentConfigFromDisk() {
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
return parseAgentConfig(config.agent);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the agent profile for a command's options.
|
|
21
|
+
* Prefers an injected `agentProfile` (test seam) over the on-disk config.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveAgentProfile(options) {
|
|
24
|
+
if (options.agentProfile)
|
|
25
|
+
return options.agentProfile;
|
|
26
|
+
const agent = options.agentConfig ?? loadAgentConfigFromDisk();
|
|
27
|
+
return requireAgentProfile(agent, options.profile);
|
|
28
|
+
}
|
|
29
|
+
// ── Failure helpers ──────────────────────────────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* Base failure envelope shared by all agent command failures.
|
|
32
|
+
* Each command returns its own typed failure shape — this helper builds the
|
|
33
|
+
* common fields so per-command wrappers only need to add their own fields.
|
|
34
|
+
*/
|
|
35
|
+
export function baseFailureFields(result, fallbackReason = "non_zero_exit") {
|
|
36
|
+
const reason = result.reason ?? fallbackReason;
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: 1,
|
|
39
|
+
ok: false,
|
|
40
|
+
reason,
|
|
41
|
+
error: result.error ?? `agent failure (${reason})`,
|
|
42
|
+
exitCode: result.exitCode,
|
|
43
|
+
...(result.stdout ? { stdout: result.stdout } : {}),
|
|
44
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// ── ENOENT hint ──────────────────────────────────────────────────────────────
|
|
48
|
+
/**
|
|
49
|
+
* Return `true` when a failed agent result looks like the binary was not found
|
|
50
|
+
* on PATH. Used to surface a better error message pointing the user at
|
|
51
|
+
* `akm setup`.
|
|
52
|
+
*/
|
|
53
|
+
export function isEnoentFailure(result) {
|
|
54
|
+
return (result.reason === "spawn_failed" &&
|
|
55
|
+
(!!result.error?.includes("ENOENT") || !!result.error?.toLowerCase().includes("not found")));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build an actionable error message for a spawn-ENOENT failure.
|
|
59
|
+
*/
|
|
60
|
+
export function enoentHintMessage(binName) {
|
|
61
|
+
return `The agent binary '${binName}' was not found on PATH. Run \`akm setup\` to configure an agent CLI, or install ${binName} and retry.`;
|
|
62
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { DEFAULT_CONFIG, } from "../core/config";
|
|
1
|
+
import { DEFAULT_CONFIG, getSources, } from "../core/config";
|
|
2
2
|
import { UsageError } from "../core/errors";
|
|
3
|
+
import { assertWritableAllowedForKind } from "../core/write-source";
|
|
3
4
|
// ── Merge helpers for LLM/embedding subkey set ───────────────────────────────
|
|
4
5
|
function mergeLlmLike(base, patch) {
|
|
5
6
|
return { endpoint: "", model: "", ...(base ?? {}), ...patch };
|
|
@@ -7,6 +8,14 @@ function mergeLlmLike(base, patch) {
|
|
|
7
8
|
function mergeLlmLikeEmbedding(base, patch) {
|
|
8
9
|
return { endpoint: "", model: "", ...(base ?? {}), ...patch };
|
|
9
10
|
}
|
|
11
|
+
function validateSources(entries) {
|
|
12
|
+
if (!entries)
|
|
13
|
+
return undefined;
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
assertWritableAllowedForKind(entry);
|
|
16
|
+
}
|
|
17
|
+
return entries;
|
|
18
|
+
}
|
|
10
19
|
export function parseConfigValue(key, value) {
|
|
11
20
|
switch (key) {
|
|
12
21
|
case "stashDir":
|
|
@@ -45,12 +54,14 @@ export function parseConfigValue(key, value) {
|
|
|
45
54
|
return { llm: mergeLlmLike(undefined, { model: requireNonEmptyString(value, key) }) };
|
|
46
55
|
case "llm.apiKey":
|
|
47
56
|
return { llm: mergeLlmLike(undefined, { apiKey: requireNonEmptyString(value, key) }) };
|
|
57
|
+
case "llm.contextLength":
|
|
58
|
+
return { llm: mergeLlmLike(undefined, { contextLength: parsePositiveInteger(value, key) }) };
|
|
48
59
|
case "registries":
|
|
49
60
|
return { registries: parseRegistriesValue(value) };
|
|
50
61
|
case "sources":
|
|
51
62
|
case "stashes":
|
|
52
63
|
// "stashes" is kept as an alias for backwards-compat; both write to `sources`.
|
|
53
|
-
return { sources: parseStashesValue(value) };
|
|
64
|
+
return { sources: validateSources(parseStashesValue(value)) };
|
|
54
65
|
case "output.format":
|
|
55
66
|
return { output: { format: parseOutputFormat(value) } };
|
|
56
67
|
case "output.detail":
|
|
@@ -100,12 +111,14 @@ export function getConfigValue(config, key) {
|
|
|
100
111
|
return config.llm?.model ?? null;
|
|
101
112
|
case "llm.apiKey":
|
|
102
113
|
return config.llm?.apiKey ?? null;
|
|
114
|
+
case "llm.contextLength":
|
|
115
|
+
return config.llm?.contextLength ?? null;
|
|
103
116
|
case "registries":
|
|
104
117
|
return config.registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
105
118
|
case "sources":
|
|
106
119
|
case "stashes":
|
|
107
120
|
// "stashes" is an alias for "sources" for backwards-compat.
|
|
108
|
-
return config
|
|
121
|
+
return getSources(config);
|
|
109
122
|
case "output.format":
|
|
110
123
|
return config.output?.format ?? null;
|
|
111
124
|
case "output.detail":
|
|
@@ -180,9 +193,11 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
180
193
|
return { ...config, llm: mergeLlmLike(config.llm, { model: requireNonEmptyString(rawValue, key) }) };
|
|
181
194
|
case "llm.apiKey":
|
|
182
195
|
return { ...config, llm: mergeLlmLike(config.llm, { apiKey: requireNonEmptyString(rawValue, key) }) };
|
|
196
|
+
case "llm.contextLength":
|
|
197
|
+
return { ...config, llm: mergeLlmLike(config.llm, { contextLength: parsePositiveInteger(rawValue, key) }) };
|
|
183
198
|
case "defaultWriteTarget": {
|
|
184
199
|
const name = requireNonEmptyString(rawValue, key);
|
|
185
|
-
const knownNames = (config
|
|
200
|
+
const knownNames = getSources(config)
|
|
186
201
|
.map((s) => s.name)
|
|
187
202
|
.filter((n) => typeof n === "string");
|
|
188
203
|
if (knownNames.length > 0 && !knownNames.includes(name)) {
|
|
@@ -237,12 +252,18 @@ export function unsetConfigValue(config, key) {
|
|
|
237
252
|
const { apiKey: _b, ...restLlm } = config.llm;
|
|
238
253
|
return { ...config, llm: restLlm };
|
|
239
254
|
}
|
|
255
|
+
case "llm.contextLength": {
|
|
256
|
+
if (!config.llm)
|
|
257
|
+
return config;
|
|
258
|
+
const { contextLength: _lctx, ...restLlm2 } = config.llm;
|
|
259
|
+
return { ...config, llm: restLlm2 };
|
|
260
|
+
}
|
|
240
261
|
case "registries":
|
|
241
262
|
return { ...config, registries: undefined };
|
|
242
263
|
case "sources":
|
|
243
264
|
case "stashes":
|
|
244
265
|
// "stashes" is kept as an alias for backwards-compat; both clear `sources`.
|
|
245
|
-
return { ...config, sources: undefined
|
|
266
|
+
return { ...config, sources: undefined };
|
|
246
267
|
case "output.format":
|
|
247
268
|
return { ...config, output: mergeOutputConfig(config.output, { format: undefined }) };
|
|
248
269
|
case "output.detail":
|
|
@@ -287,7 +308,7 @@ export function listConfig(config) {
|
|
|
287
308
|
output: mergeOutputConfig(DEFAULT_CONFIG.output, config.output) ?? null,
|
|
288
309
|
stashDir: config.stashDir ?? null,
|
|
289
310
|
installed: config.installed ?? [],
|
|
290
|
-
sources: config
|
|
311
|
+
sources: getSources(config),
|
|
291
312
|
};
|
|
292
313
|
if (config.defaultWriteTarget)
|
|
293
314
|
result.defaultWriteTarget = config.defaultWriteTarget;
|
|
@@ -410,17 +431,8 @@ function parseRegistriesValue(value) {
|
|
|
410
431
|
if (typeof obj.url !== "string" || !obj.url) {
|
|
411
432
|
throw new UsageError(`Invalid value for registries[${i}]: "url" is required`);
|
|
412
433
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
result.name = obj.name;
|
|
416
|
-
if (typeof obj.enabled === "boolean")
|
|
417
|
-
result.enabled = obj.enabled;
|
|
418
|
-
if (typeof obj.provider === "string" && obj.provider)
|
|
419
|
-
result.provider = obj.provider;
|
|
420
|
-
if (typeof obj.options === "object" && obj.options !== null && !Array.isArray(obj.options)) {
|
|
421
|
-
result.options = obj.options;
|
|
422
|
-
}
|
|
423
|
-
return result;
|
|
434
|
+
// Spread the full entry so unknown/future fields round-trip intact.
|
|
435
|
+
return { ...obj };
|
|
424
436
|
});
|
|
425
437
|
}
|
|
426
438
|
function parseEmbeddingConnectionValue(value) {
|
|
@@ -430,30 +442,25 @@ function parseEmbeddingConnectionValue(value) {
|
|
|
430
442
|
endpoint: "http://localhost:11434/v1/embeddings",
|
|
431
443
|
model: "nomic-embed-text",
|
|
432
444
|
});
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
const localOnly = { endpoint: "", model: "", localModel };
|
|
440
|
-
if (typeof parsed.provider === "string" && parsed.provider)
|
|
441
|
-
localOnly.provider = parsed.provider;
|
|
442
|
-
return localOnly;
|
|
445
|
+
// Require either a non-empty endpoint (remote) or a localModel (local-only).
|
|
446
|
+
const hasEndpoint = typeof parsed.endpoint === "string" && parsed.endpoint !== "";
|
|
447
|
+
const hasLocalModel = typeof parsed.localModel === "string" && parsed.localModel !== "";
|
|
448
|
+
if (!hasEndpoint && !hasLocalModel) {
|
|
449
|
+
throw new UsageError(`Invalid value for embedding: "endpoint" is required for remote embeddings, or provide "localModel" for local-only`);
|
|
443
450
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return
|
|
451
|
+
// Validate the types of the required/structural fields that the runtime
|
|
452
|
+
// depends on, but do not reconstruct the object — pass everything through.
|
|
453
|
+
if (parsed.endpoint !== undefined && typeof parsed.endpoint !== "string") {
|
|
454
|
+
throw new UsageError(`Invalid value for embedding: "endpoint" must be a string`);
|
|
455
|
+
}
|
|
456
|
+
if (parsed.model !== undefined && typeof parsed.model !== "string") {
|
|
457
|
+
throw new UsageError(`Invalid value for embedding: "model" must be a string`);
|
|
458
|
+
}
|
|
459
|
+
if (parsed.dimension !== undefined && !Number.isInteger(parsed.dimension)) {
|
|
460
|
+
throw new UsageError(`embedding.dimension: expected a positive integer, got ${parsed.dimension}`, "INVALID_FLAG_VALUE");
|
|
461
|
+
}
|
|
462
|
+
// Spread the full parsed object so unknown/future fields round-trip intact.
|
|
463
|
+
return { endpoint: "", model: "", ...parsed };
|
|
457
464
|
}
|
|
458
465
|
function parseLlmConnectionValue(value) {
|
|
459
466
|
if (value === "null" || value === "")
|
|
@@ -462,19 +469,25 @@ function parseLlmConnectionValue(value) {
|
|
|
462
469
|
endpoint: "http://localhost:11434/v1/chat/completions",
|
|
463
470
|
model: "llama3.2",
|
|
464
471
|
});
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (parsed.
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
472
|
+
if (parsed.endpoint !== undefined && typeof parsed.endpoint !== "string") {
|
|
473
|
+
throw new UsageError(`Invalid value for llm: "endpoint" is a required string field`);
|
|
474
|
+
}
|
|
475
|
+
if (parsed.model !== undefined && typeof parsed.model !== "string") {
|
|
476
|
+
throw new UsageError(`Invalid value for llm: "model" is a required string field`);
|
|
477
|
+
}
|
|
478
|
+
if (typeof parsed.endpoint !== "string" || !parsed.endpoint) {
|
|
479
|
+
throw new UsageError(`Invalid value for llm: "endpoint" is a required string field`);
|
|
480
|
+
}
|
|
481
|
+
if (parsed.model === undefined) {
|
|
482
|
+
return { endpoint: parsed.endpoint, model: "", ...parsed };
|
|
483
|
+
}
|
|
484
|
+
if (!parsed.model) {
|
|
485
|
+
throw new UsageError(`Invalid value for llm: "model" must be a non-empty string when provided`);
|
|
486
|
+
}
|
|
487
|
+
// Spread the full parsed object so unknown/future fields round-trip intact.
|
|
488
|
+
// The config loader (config.ts) handles warn-and-ignore for unknown sub-keys
|
|
489
|
+
// at read time, so we do not need to whitelist here.
|
|
490
|
+
return { ...parsed };
|
|
478
491
|
}
|
|
479
492
|
function parseJsonObject(value, key, example) {
|
|
480
493
|
let parsed;
|
|
@@ -490,30 +503,12 @@ function parseJsonObject(value, key, example) {
|
|
|
490
503
|
}
|
|
491
504
|
return parsed;
|
|
492
505
|
}
|
|
493
|
-
function asRequiredString(value, key, field) {
|
|
494
|
-
if (typeof value !== "string" || !value) {
|
|
495
|
-
throw new UsageError(`Invalid value for ${key}: "${field}" is a required string field`);
|
|
496
|
-
}
|
|
497
|
-
return value;
|
|
498
|
-
}
|
|
499
506
|
function requireNonEmptyString(value, key) {
|
|
500
507
|
if (!value) {
|
|
501
508
|
throw new UsageError(`Invalid value for ${key}: expected a non-empty string`);
|
|
502
509
|
}
|
|
503
510
|
return value;
|
|
504
511
|
}
|
|
505
|
-
function parseUnknownNumber(value, key) {
|
|
506
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
507
|
-
throw new UsageError(`Invalid value for ${key}: expected a number`);
|
|
508
|
-
}
|
|
509
|
-
return value;
|
|
510
|
-
}
|
|
511
|
-
function parseUnknownPositiveInteger(value, key) {
|
|
512
|
-
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
513
|
-
throw new UsageError(`Invalid value for ${key}: expected a positive integer`);
|
|
514
|
-
}
|
|
515
|
-
return value;
|
|
516
|
-
}
|
|
517
512
|
function parsePositiveInteger(value, key) {
|
|
518
513
|
const n = Number(value);
|
|
519
514
|
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
|
|
@@ -542,18 +537,7 @@ function parseStashesValue(value) {
|
|
|
542
537
|
if (typeof obj.type !== "string" || !obj.type) {
|
|
543
538
|
throw new UsageError(`Invalid value for sources[${i}]: "type" is required`);
|
|
544
539
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
result.path = obj.path;
|
|
548
|
-
if (typeof obj.url === "string" && obj.url)
|
|
549
|
-
result.url = obj.url;
|
|
550
|
-
if (typeof obj.name === "string" && obj.name)
|
|
551
|
-
result.name = obj.name;
|
|
552
|
-
if (typeof obj.enabled === "boolean")
|
|
553
|
-
result.enabled = obj.enabled;
|
|
554
|
-
if (typeof obj.options === "object" && obj.options !== null && !Array.isArray(obj.options)) {
|
|
555
|
-
result.options = obj.options;
|
|
556
|
-
}
|
|
557
|
-
return result;
|
|
540
|
+
// Spread the full entry so unknown/future fields round-trip intact.
|
|
541
|
+
return { ...obj };
|
|
558
542
|
});
|
|
559
543
|
}
|