akm-cli 0.5.0 → 0.6.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 +32 -5
- package/dist/asset-registry.js +29 -5
- package/dist/asset-spec.js +12 -5
- package/dist/cli-hints.js +300 -0
- package/dist/cli.js +218 -1357
- package/dist/common.js +147 -50
- package/dist/config.js +224 -13
- package/dist/create-provider-registry.js +1 -1
- package/dist/curate.js +258 -0
- package/dist/{local-search.js → db-search.js} +30 -19
- package/dist/db.js +168 -62
- package/dist/embedder.js +49 -273
- package/dist/embedders/cache.js +47 -0
- package/dist/embedders/local.js +152 -0
- package/dist/embedders/remote.js +121 -0
- package/dist/embedders/types.js +39 -0
- package/dist/errors.js +14 -3
- package/dist/frontmatter.js +61 -7
- package/dist/indexer.js +38 -7
- package/dist/info.js +2 -2
- package/dist/install-audit.js +16 -1
- package/dist/{installed-kits.js → installed-stashes.js} +48 -22
- package/dist/llm-client.js +92 -0
- package/dist/llm.js +14 -126
- package/dist/lockfile.js +28 -1
- package/dist/matchers.js +1 -1
- package/dist/metadata-enhance.js +53 -0
- package/dist/migration-help.js +75 -44
- package/dist/output-context.js +77 -0
- package/dist/output-shapes.js +198 -0
- package/dist/output-text.js +520 -0
- package/dist/paths.js +4 -4
- package/dist/providers/index.js +11 -0
- package/dist/providers/skills-sh.js +1 -1
- package/dist/providers/static-index.js +47 -45
- package/dist/registry-build-index.js +36 -29
- package/dist/registry-factory.js +2 -2
- package/dist/registry-resolve.js +8 -4
- package/dist/registry-search.js +62 -5
- package/dist/remember.js +172 -0
- package/dist/renderers.js +52 -0
- package/dist/search-source.js +73 -42
- package/dist/setup-steps.js +45 -0
- package/dist/setup.js +149 -76
- package/dist/stash-add.js +94 -38
- package/dist/stash-clone.js +4 -4
- package/dist/stash-provider-factory.js +2 -2
- package/dist/stash-provider.js +3 -1
- package/dist/stash-providers/filesystem.js +31 -1
- package/dist/stash-providers/git.js +209 -8
- package/dist/stash-providers/index.js +1 -0
- package/dist/stash-providers/npm.js +159 -0
- package/dist/stash-providers/provider-utils.js +162 -0
- package/dist/stash-providers/sync-from-ref.js +45 -0
- package/dist/stash-providers/tar-utils.js +151 -0
- package/dist/stash-providers/website.js +80 -4
- package/dist/stash-resolve.js +5 -5
- package/dist/stash-search.js +4 -4
- package/dist/stash-show.js +3 -3
- package/dist/wiki.js +6 -6
- package/dist/workflow-authoring.js +12 -4
- package/dist/workflow-markdown.js +9 -0
- package/dist/workflow-runs.js +12 -2
- package/docs/README.md +30 -0
- package/docs/migration/release-notes/0.0.13.md +4 -0
- package/docs/migration/release-notes/0.1.0.md +6 -0
- package/docs/migration/release-notes/0.2.0.md +6 -0
- package/docs/migration/release-notes/0.3.0.md +5 -0
- package/docs/migration/release-notes/0.5.0.md +6 -0
- package/docs/migration/release-notes/0.6.0.md +29 -0
- package/docs/migration/release-notes/README.md +21 -0
- package/package.json +3 -2
- package/dist/registry-install.js +0 -532
- /package/dist/{kit-include.js → stash-include.js} +0 -0
package/dist/llm.js
CHANGED
|
@@ -1,128 +1,16 @@
|
|
|
1
|
-
import { fetchWithTimeout } from "./common";
|
|
2
|
-
export async function chatCompletion(config, messages, options) {
|
|
3
|
-
const headers = { "Content-Type": "application/json" };
|
|
4
|
-
if (config.apiKey) {
|
|
5
|
-
headers.Authorization = `Bearer ${config.apiKey}`;
|
|
6
|
-
}
|
|
7
|
-
const response = await fetchWithTimeout(config.endpoint, {
|
|
8
|
-
method: "POST",
|
|
9
|
-
headers,
|
|
10
|
-
body: JSON.stringify({
|
|
11
|
-
model: config.model,
|
|
12
|
-
messages,
|
|
13
|
-
temperature: options?.temperature ?? config.temperature ?? 0.3,
|
|
14
|
-
max_tokens: options?.maxTokens ?? config.maxTokens ?? 512,
|
|
15
|
-
}),
|
|
16
|
-
});
|
|
17
|
-
if (!response.ok) {
|
|
18
|
-
const body = await response.text().catch(() => "");
|
|
19
|
-
throw new Error(`LLM request failed (${response.status}): ${body}`);
|
|
20
|
-
}
|
|
21
|
-
const json = (await response.json());
|
|
22
|
-
return json.choices?.[0]?.message?.content?.trim() ?? "";
|
|
23
|
-
}
|
|
24
|
-
/** Strip leading/trailing markdown code fences from an LLM response. */
|
|
25
|
-
function stripJsonFences(raw) {
|
|
26
|
-
return raw
|
|
27
|
-
.trim()
|
|
28
|
-
.replace(/^```(?:json)?\s*\n?/i, "")
|
|
29
|
-
.replace(/\n?```\s*$/i, "")
|
|
30
|
-
.trim();
|
|
31
|
-
}
|
|
32
|
-
/** Parse a possibly-fenced JSON response. Returns undefined if invalid. */
|
|
33
|
-
export function parseJsonResponse(raw) {
|
|
34
|
-
try {
|
|
35
|
-
return JSON.parse(stripJsonFences(raw));
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
// ── Metadata Enhancement ────────────────────────────────────────────────────
|
|
42
|
-
const SYSTEM_PROMPT = `You are a metadata generator for a developer asset registry. Given a script/skill/command/agent entry, generate improved metadata. Respond with ONLY valid JSON, no markdown fencing.`;
|
|
43
1
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
2
|
+
* Backward-compatible facade for the LLM module.
|
|
3
|
+
*
|
|
4
|
+
* The implementation has been split into:
|
|
5
|
+
* - `./llm-client` — transport (chatCompletion, parseJsonResponse,
|
|
6
|
+
* isLlmAvailable, probeLlmCapabilities, ChatMessage,
|
|
7
|
+
* ChatCompletionOptions, stripJsonFences)
|
|
8
|
+
* - `./metadata-enhance` — higher-level metadata enhancement workflow
|
|
9
|
+
* (enhanceMetadata)
|
|
10
|
+
*
|
|
11
|
+
* New code should import from those modules directly. This re-export barrel
|
|
12
|
+
* exists so existing call sites and tests that import from `./llm` keep
|
|
13
|
+
* working without modification.
|
|
46
14
|
*/
|
|
47
|
-
export
|
|
48
|
-
|
|
49
|
-
if (entry.description)
|
|
50
|
-
contextParts.push(`Current description: ${entry.description}`);
|
|
51
|
-
if (entry.tags?.length)
|
|
52
|
-
contextParts.push(`Current tags: ${entry.tags.join(", ")}`);
|
|
53
|
-
if (fileContent) {
|
|
54
|
-
// Limit content to first 2000 chars to stay within token limits
|
|
55
|
-
const truncated = fileContent.length > 2000 ? `${fileContent.slice(0, 2000)}\n... (truncated)` : fileContent;
|
|
56
|
-
contextParts.push(`File content:\n${truncated}`);
|
|
57
|
-
}
|
|
58
|
-
const userPrompt = `${contextParts.join("\n")}
|
|
59
|
-
|
|
60
|
-
Generate improved metadata for this ${entry.type}. Return JSON with these fields:
|
|
61
|
-
- "description": a clear, concise one-sentence description of what this does
|
|
62
|
-
- "searchHints": an array of 3-6 natural language task phrases an agent might use to find this (e.g. "deploy a docker container", "run database migrations")
|
|
63
|
-
- "tags": an array of 3-8 relevant keyword tags
|
|
64
|
-
|
|
65
|
-
Return ONLY the JSON object, no explanation.`;
|
|
66
|
-
const raw = await chatCompletion(config, [
|
|
67
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
68
|
-
{ role: "user", content: userPrompt },
|
|
69
|
-
]);
|
|
70
|
-
const parsed = parseJsonResponse(raw);
|
|
71
|
-
if (!parsed)
|
|
72
|
-
return {};
|
|
73
|
-
const result = {};
|
|
74
|
-
if (typeof parsed.description === "string" && parsed.description) {
|
|
75
|
-
result.description = parsed.description;
|
|
76
|
-
}
|
|
77
|
-
if (Array.isArray(parsed.searchHints)) {
|
|
78
|
-
result.searchHints = parsed.searchHints
|
|
79
|
-
.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
80
|
-
.slice(0, 8);
|
|
81
|
-
}
|
|
82
|
-
if (Array.isArray(parsed.tags)) {
|
|
83
|
-
result.tags = parsed.tags.filter((s) => typeof s === "string" && s.trim().length > 0).slice(0, 10);
|
|
84
|
-
}
|
|
85
|
-
return result;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Check if the LLM endpoint is reachable.
|
|
89
|
-
*/
|
|
90
|
-
export async function isLlmAvailable(config) {
|
|
91
|
-
try {
|
|
92
|
-
const result = await chatCompletion(config, [{ role: "user", content: "Respond with just the word: ok" }]);
|
|
93
|
-
return result.length > 0;
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
// ── Capability probe ────────────────────────────────────────────────────────
|
|
100
|
-
/**
|
|
101
|
-
* Ask the model to emit a strict JSON object so we know whether the knowledge
|
|
102
|
-
* wiki ingest/lint flows can rely on structured output. Failure is non-fatal —
|
|
103
|
-
* the caller can fall back to assist-only mode.
|
|
104
|
-
*/
|
|
105
|
-
export async function probeLlmCapabilities(config) {
|
|
106
|
-
try {
|
|
107
|
-
const raw = await chatCompletion(config, [
|
|
108
|
-
{
|
|
109
|
-
role: "system",
|
|
110
|
-
content: "You return only valid JSON. No prose, no markdown fences.",
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
role: "user",
|
|
114
|
-
content: 'Return exactly this JSON object and nothing else: {"ok": true, "ingest": true, "lint": true}',
|
|
115
|
-
},
|
|
116
|
-
], { maxTokens: 64, temperature: 0 });
|
|
117
|
-
if (!raw)
|
|
118
|
-
return { reachable: false, structuredOutput: false, error: "empty response" };
|
|
119
|
-
const parsed = parseJsonResponse(raw);
|
|
120
|
-
return {
|
|
121
|
-
reachable: true,
|
|
122
|
-
structuredOutput: Boolean(parsed && parsed.ok === true),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
return { reachable: false, structuredOutput: false, error: err instanceof Error ? err.message : String(err) };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
15
|
+
export * from "./llm-client";
|
|
16
|
+
export { enhanceMetadata } from "./metadata-enhance";
|
package/dist/lockfile.js
CHANGED
|
@@ -2,8 +2,34 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getConfigDir } from "./config";
|
|
4
4
|
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
5
|
+
const LOCKFILE_NAME = "akm.lock";
|
|
6
|
+
const LEGACY_LOCKFILE_NAME = "stash.lock";
|
|
5
7
|
function getLockfilePath() {
|
|
6
|
-
return path.join(getConfigDir(),
|
|
8
|
+
return path.join(getConfigDir(), LOCKFILE_NAME);
|
|
9
|
+
}
|
|
10
|
+
function getLegacyLockfilePath() {
|
|
11
|
+
return path.join(getConfigDir(), LEGACY_LOCKFILE_NAME);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* One-time migration: if the new `akm.lock` does not exist but the legacy
|
|
15
|
+
* `stash.lock` does, copy it across so installed-stash tracking survives the
|
|
16
|
+
* rename. Best-effort; failures are silent because the lockfile loader treats
|
|
17
|
+
* a missing file as an empty lockfile.
|
|
18
|
+
*/
|
|
19
|
+
function migrateLegacyLockfileIfNeeded() {
|
|
20
|
+
const newPath = getLockfilePath();
|
|
21
|
+
const legacyPath = getLegacyLockfilePath();
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(newPath))
|
|
24
|
+
return;
|
|
25
|
+
if (!fs.existsSync(legacyPath))
|
|
26
|
+
return;
|
|
27
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
28
|
+
fs.copyFileSync(legacyPath, newPath);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* best-effort — fall through to empty lockfile */
|
|
32
|
+
}
|
|
7
33
|
}
|
|
8
34
|
// ── Lock sentinel ────────────────────────────────────────────────────────────
|
|
9
35
|
const LOCK_MAX_RETRIES = 3;
|
|
@@ -74,6 +100,7 @@ function releaseLockSentinel() {
|
|
|
74
100
|
}
|
|
75
101
|
// ── Read / Write ────────────────────────────────────────────────────────────
|
|
76
102
|
export function readLockfile() {
|
|
103
|
+
migrateLegacyLockfileIfNeeded();
|
|
77
104
|
const lockfilePath = getLockfilePath();
|
|
78
105
|
try {
|
|
79
106
|
const raw = JSON.parse(fs.readFileSync(lockfilePath, "utf8"));
|
package/dist/matchers.js
CHANGED
|
@@ -51,7 +51,7 @@ export function extensionMatcher(ctx) {
|
|
|
51
51
|
* directory segment from the stash root matches a known type name.
|
|
52
52
|
*
|
|
53
53
|
* The first matching type-like ancestor wins. This preserves intuitive
|
|
54
|
-
* behavior for nested
|
|
54
|
+
* behavior for nested stash layouts such as `agent-stash/agents/blog/foo.md`
|
|
55
55
|
* while still honoring earlier type roots like `commands/agents/foo.md`.
|
|
56
56
|
*/
|
|
57
57
|
export function directoryMatcher(ctx) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-driven metadata enhancement for stash entries.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `llm.ts` so the higher-level workflow (prompting the LLM to
|
|
5
|
+
* improve descriptions/tags/searchHints) lives separately from the low-level
|
|
6
|
+
* transport client in `llm-client.ts`.
|
|
7
|
+
*/
|
|
8
|
+
import { chatCompletion, parseJsonResponse } from "./llm-client";
|
|
9
|
+
const SYSTEM_PROMPT = `You are a metadata generator for a developer asset registry. Given a script/skill/command/agent entry, generate improved metadata. Respond with ONLY valid JSON, no markdown fencing.`;
|
|
10
|
+
/**
|
|
11
|
+
* Use an LLM to enhance a stash entry's metadata: improve description,
|
|
12
|
+
* generate searchHints, and suggest tags.
|
|
13
|
+
*/
|
|
14
|
+
export async function enhanceMetadata(config, entry, fileContent) {
|
|
15
|
+
const contextParts = [`Name: ${entry.name}`, `Type: ${entry.type}`];
|
|
16
|
+
if (entry.description)
|
|
17
|
+
contextParts.push(`Current description: ${entry.description}`);
|
|
18
|
+
if (entry.tags?.length)
|
|
19
|
+
contextParts.push(`Current tags: ${entry.tags.join(", ")}`);
|
|
20
|
+
if (fileContent) {
|
|
21
|
+
// Limit content to first 2000 chars to stay within token limits
|
|
22
|
+
const truncated = fileContent.length > 2000 ? `${fileContent.slice(0, 2000)}\n... (truncated)` : fileContent;
|
|
23
|
+
contextParts.push(`File content:\n${truncated}`);
|
|
24
|
+
}
|
|
25
|
+
const userPrompt = `${contextParts.join("\n")}
|
|
26
|
+
|
|
27
|
+
Generate improved metadata for this ${entry.type}. Return JSON with these fields:
|
|
28
|
+
- "description": a clear, concise one-sentence description of what this does
|
|
29
|
+
- "searchHints": an array of 3-6 natural language task phrases an agent might use to find this (e.g. "deploy a docker container", "run database migrations")
|
|
30
|
+
- "tags": an array of 3-8 relevant keyword tags
|
|
31
|
+
|
|
32
|
+
Return ONLY the JSON object, no explanation.`;
|
|
33
|
+
const raw = await chatCompletion(config, [
|
|
34
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
35
|
+
{ role: "user", content: userPrompt },
|
|
36
|
+
]);
|
|
37
|
+
const parsed = parseJsonResponse(raw);
|
|
38
|
+
if (!parsed)
|
|
39
|
+
return {};
|
|
40
|
+
const result = {};
|
|
41
|
+
if (typeof parsed.description === "string" && parsed.description) {
|
|
42
|
+
result.description = parsed.description;
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(parsed.searchHints)) {
|
|
45
|
+
result.searchHints = parsed.searchHints
|
|
46
|
+
.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
47
|
+
.slice(0, 8);
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(parsed.tags)) {
|
|
50
|
+
result.tags = parsed.tags.filter((s) => typeof s === "string" && s.trim().length > 0).slice(0, 10);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
package/dist/migration-help.js
CHANGED
|
@@ -1,40 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main/CHANGELOG.md";
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- Use \`akm add\`, \`akm list\`, and \`akm remove\` instead of the older split command surfaces.
|
|
16
|
-
- Documentation and examples from older releases should be updated to the unified source model.
|
|
17
|
-
`,
|
|
18
|
-
"0.2.0": `Migration notes for akm v0.2.0
|
|
19
|
-
|
|
20
|
-
- Asset refs are user-facing \`type:name\` values; do not rely on URI-style refs.
|
|
21
|
-
- The old fixed asset-type union was replaced by an extensible asset type system.
|
|
22
|
-
- \`tool\` assets were removed; use \`script\` assets instead.
|
|
23
|
-
- Config and docs should treat remote provider scores and local scores as part of one shared search pipeline.
|
|
24
|
-
`,
|
|
25
|
-
"0.1.0": `Migration notes for akm v0.1.0
|
|
26
|
-
|
|
27
|
-
- The package and project were rebranded from Agent-i-Kit to akm.
|
|
28
|
-
- Update package references from \`agent-i-kit\` to \`akm-cli\`.
|
|
29
|
-
- Update config, registry, plugin, path, and environment-variable references from \`agent-i-kit\` / \`AGENT_I_KIT_*\` to \`akm\` / \`AKM_*\`.
|
|
30
|
-
- The \`tool\` asset type and \`submit\` command were removed.
|
|
31
|
-
`,
|
|
32
|
-
"0.0.13": `Migration notes for akm v0.0.13
|
|
33
|
-
|
|
34
|
-
- Initial public release.
|
|
35
|
-
- No migration steps are required for earlier akm versions.
|
|
36
|
-
`,
|
|
37
|
-
};
|
|
4
|
+
const MIGRATION_DOC_URL = "https://github.com/itlackey/akm/blob/main/docs/migration/v0.5-to-v0.6.md";
|
|
5
|
+
/**
|
|
6
|
+
* Directory containing per-version release notes. Resolved relative to
|
|
7
|
+
* `import.meta.dir` so the lookup works whether this module is running
|
|
8
|
+
* from source (`<repo>/src`) or from the published build (`<pkg>/dist`).
|
|
9
|
+
* The `docs/migration/release-notes/` directory is shipped via the
|
|
10
|
+
* `files[]` array in `package.json`.
|
|
11
|
+
*/
|
|
12
|
+
function releaseNotesDir() {
|
|
13
|
+
return path.resolve(import.meta.dir, "../docs/migration/release-notes");
|
|
14
|
+
}
|
|
38
15
|
function loadChangelog() {
|
|
39
16
|
try {
|
|
40
17
|
const changelogPath = path.resolve(import.meta.dir, "../CHANGELOG.md");
|
|
@@ -43,12 +20,43 @@ function loadChangelog() {
|
|
|
43
20
|
}
|
|
44
21
|
}
|
|
45
22
|
catch {
|
|
46
|
-
// fall through to
|
|
23
|
+
// fall through to bundled notes
|
|
47
24
|
}
|
|
48
25
|
return undefined;
|
|
49
26
|
}
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Load the bundled migration note for a specific version, if one exists.
|
|
29
|
+
* Returns the file body verbatim (no transformations). Missing files,
|
|
30
|
+
* permission errors, and non-file entries all return `undefined` —
|
|
31
|
+
* callers are responsible for the fallback message.
|
|
32
|
+
*/
|
|
33
|
+
function loadReleaseNote(version) {
|
|
34
|
+
if (!isSafeVersionComponent(version))
|
|
35
|
+
return undefined;
|
|
36
|
+
const notePath = path.join(releaseNotesDir(), `${version}.md`);
|
|
37
|
+
try {
|
|
38
|
+
if (!fs.existsSync(notePath))
|
|
39
|
+
return undefined;
|
|
40
|
+
const stat = fs.statSync(notePath);
|
|
41
|
+
if (!stat.isFile())
|
|
42
|
+
return undefined;
|
|
43
|
+
return fs.readFileSync(notePath, "utf8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Restrict version lookups to strings that are safe as a single path
|
|
51
|
+
* segment. Accepts typical semver forms (`0.6.0`, `0.6.0-rc1`,
|
|
52
|
+
* `0.6.0+build.5`) and rejects anything with slashes, `..`, or control
|
|
53
|
+
* characters that would let a crafted input escape the release-notes
|
|
54
|
+
* directory.
|
|
55
|
+
*/
|
|
56
|
+
function isSafeVersionComponent(version) {
|
|
57
|
+
if (!version || version.length > 64)
|
|
58
|
+
return false;
|
|
59
|
+
return /^[A-Za-z0-9._+-]+$/.test(version) && !version.includes("..");
|
|
52
60
|
}
|
|
53
61
|
function normalizeRequestedVersion(input) {
|
|
54
62
|
const value = input.trim();
|
|
@@ -81,11 +89,18 @@ function extractChangelogSection(changelog, version) {
|
|
|
81
89
|
return undefined;
|
|
82
90
|
return `## [${version}]\n${match[1].trim()}\n`;
|
|
83
91
|
}
|
|
92
|
+
function escapeRegexString(value) {
|
|
93
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
94
|
+
}
|
|
84
95
|
function fallbackGuide(version) {
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
return `${
|
|
88
|
-
|
|
96
|
+
const bundled = loadReleaseNote(version);
|
|
97
|
+
if (bundled)
|
|
98
|
+
return `${bundled.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
|
|
99
|
+
const available = listBundledReleaseVersions();
|
|
100
|
+
const availableLine = available.length ? `\nAvailable bundled notes: ${available.join(", ")}\n` : "";
|
|
101
|
+
return (`No dedicated migration note is bundled for akm v${version}.\n${availableLine}\n` +
|
|
102
|
+
`See the full changelog: ${CHANGELOG_URL}\n` +
|
|
103
|
+
`Longform migration guide: ${MIGRATION_DOC_URL}\n`);
|
|
89
104
|
}
|
|
90
105
|
export function renderMigrationHelp(versionInput, changelogText = loadChangelog()) {
|
|
91
106
|
const requested = normalizeRequestedVersion(versionInput);
|
|
@@ -98,13 +113,29 @@ export function renderMigrationHelp(versionInput, changelogText = loadChangelog(
|
|
|
98
113
|
for (const candidate of candidates) {
|
|
99
114
|
const section = extractChangelogSection(changelogText, candidate);
|
|
100
115
|
if (section) {
|
|
101
|
-
const
|
|
102
|
-
if (!
|
|
116
|
+
const bundled = loadReleaseNote(candidate);
|
|
117
|
+
if (!bundled)
|
|
103
118
|
return `${section.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
|
|
104
|
-
return `${
|
|
119
|
+
return `${bundled.trim()}\n\nRelease notes\n-------------\n${section.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
|
|
105
120
|
}
|
|
106
121
|
}
|
|
107
122
|
}
|
|
108
123
|
const fallbackVersion = candidates.find((candidate) => candidate !== "latest") ?? requested;
|
|
109
124
|
return fallbackGuide(fallbackVersion);
|
|
110
125
|
}
|
|
126
|
+
/** Test-only helper — list every version with a bundled release note. */
|
|
127
|
+
export function listBundledReleaseVersions() {
|
|
128
|
+
try {
|
|
129
|
+
const dir = releaseNotesDir();
|
|
130
|
+
if (!fs.existsSync(dir))
|
|
131
|
+
return [];
|
|
132
|
+
return fs
|
|
133
|
+
.readdirSync(dir)
|
|
134
|
+
.filter((name) => name.endsWith(".md") && name !== "README.md")
|
|
135
|
+
.map((name) => name.slice(0, -".md".length))
|
|
136
|
+
.sort();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-level output mode singleton.
|
|
3
|
+
*
|
|
4
|
+
* Output mode (format + detail + forAgent) is parsed once at startup from
|
|
5
|
+
* `process.argv` and the persisted user config. All subsequent `output()`
|
|
6
|
+
* calls read from this in-memory singleton instead of re-scanning argv and
|
|
7
|
+
* re-loading config on every call.
|
|
8
|
+
*
|
|
9
|
+
* Initialized from `cli.ts` before `runMain`.
|
|
10
|
+
*/
|
|
11
|
+
import { UsageError } from "./errors";
|
|
12
|
+
export const OUTPUT_FORMATS = ["json", "yaml", "text", "jsonl"];
|
|
13
|
+
export const DETAIL_LEVELS = ["brief", "normal", "full", "summary", "agent"];
|
|
14
|
+
export function parseOutputFormat(value) {
|
|
15
|
+
if (!value)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (OUTPUT_FORMATS.includes(value))
|
|
18
|
+
return value;
|
|
19
|
+
throw new UsageError(`Invalid value for --format: ${value}. Expected one of: ${OUTPUT_FORMATS.join("|")}`);
|
|
20
|
+
}
|
|
21
|
+
export function parseDetailLevel(value) {
|
|
22
|
+
if (!value)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (DETAIL_LEVELS.includes(value))
|
|
25
|
+
return value;
|
|
26
|
+
throw new UsageError(`Invalid value for --detail: ${value}. Expected one of: ${DETAIL_LEVELS.join("|")}`);
|
|
27
|
+
}
|
|
28
|
+
export function parseFlagValue(argv, flag) {
|
|
29
|
+
for (let i = 0; i < argv.length; i++) {
|
|
30
|
+
const arg = argv[i];
|
|
31
|
+
if (arg === flag)
|
|
32
|
+
return argv[i + 1];
|
|
33
|
+
if (arg.startsWith(`${flag}=`))
|
|
34
|
+
return arg.slice(flag.length + 1);
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
export function hasBooleanFlag(argv, flag) {
|
|
39
|
+
return argv.some((arg) => arg === flag || arg === `${flag}=true`);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve output mode from a synthetic argv array and config defaults.
|
|
43
|
+
* Pure function — no IO. Suitable for unit tests.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveOutputMode(argv, defaults = {}) {
|
|
46
|
+
const format = parseOutputFormat(parseFlagValue(argv, "--format")) ?? defaults?.format ?? "json";
|
|
47
|
+
const detail = parseDetailLevel(parseFlagValue(argv, "--detail")) ?? defaults?.detail ?? "brief";
|
|
48
|
+
// `--detail=agent` is the preferred preset. `--for-agent` is kept for one
|
|
49
|
+
// release cycle as an alias so existing scripts and docs keep working.
|
|
50
|
+
const forAgent = detail === "agent" || hasBooleanFlag(argv, "--for-agent");
|
|
51
|
+
return { format, detail, forAgent };
|
|
52
|
+
}
|
|
53
|
+
let _mode;
|
|
54
|
+
/**
|
|
55
|
+
* Initialize the process-level output mode. Must be called once at startup
|
|
56
|
+
* before any code calls `getOutputMode()`. Subsequent calls overwrite.
|
|
57
|
+
*/
|
|
58
|
+
export function initOutputMode(argv, defaults = {}) {
|
|
59
|
+
_mode = resolveOutputMode(argv, defaults);
|
|
60
|
+
return _mode;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read the process-level output mode. Throws if `initOutputMode()` was not
|
|
64
|
+
* called first — that is a programmer error, not a runtime condition.
|
|
65
|
+
*/
|
|
66
|
+
export function getOutputMode() {
|
|
67
|
+
if (!_mode) {
|
|
68
|
+
throw new Error("OutputMode not initialized. Call initOutputMode() before getOutputMode().");
|
|
69
|
+
}
|
|
70
|
+
return _mode;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reset the singleton. Test-only utility.
|
|
74
|
+
*/
|
|
75
|
+
export function resetOutputMode() {
|
|
76
|
+
_mode = undefined;
|
|
77
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure shaping functions that select and trim fields from command result
|
|
3
|
+
* objects according to the active detail level / agent mode.
|
|
4
|
+
*
|
|
5
|
+
* Every function in this module is side-effect free and operates on plain
|
|
6
|
+
* `Record<string, unknown>` shapes, which makes them trivial to unit test.
|
|
7
|
+
*/
|
|
8
|
+
const NORMAL_DESCRIPTION_LIMIT = 250;
|
|
9
|
+
export function shapeForCommand(command, result, detail, forAgent = false) {
|
|
10
|
+
switch (command) {
|
|
11
|
+
case "search":
|
|
12
|
+
return shapeSearchOutput(result, detail, forAgent);
|
|
13
|
+
case "registry-search":
|
|
14
|
+
return shapeRegistrySearchOutput(result, detail);
|
|
15
|
+
case "show":
|
|
16
|
+
return shapeShowOutput(result, detail, forAgent);
|
|
17
|
+
default:
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function shapeSearchOutput(result, detail, forAgent = false) {
|
|
22
|
+
const hits = Array.isArray(result.hits) ? result.hits : [];
|
|
23
|
+
const registryHits = Array.isArray(result.registryHits) ? result.registryHits : [];
|
|
24
|
+
const shapedHits = forAgent
|
|
25
|
+
? hits.map((hit) => shapeSearchHitForAgent(hit))
|
|
26
|
+
: hits.map((hit) => shapeSearchHit(hit, detail));
|
|
27
|
+
const shapedRegistryHits = forAgent
|
|
28
|
+
? registryHits.map((hit) => shapeSearchHitForAgent(hit))
|
|
29
|
+
: registryHits.map((hit) => shapeSearchHit(hit, detail));
|
|
30
|
+
if (forAgent) {
|
|
31
|
+
return {
|
|
32
|
+
hits: shapedHits,
|
|
33
|
+
...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}),
|
|
34
|
+
...(result.tip ? { tip: result.tip } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (detail === "full") {
|
|
38
|
+
return {
|
|
39
|
+
schemaVersion: result.schemaVersion,
|
|
40
|
+
stashDir: result.stashDir,
|
|
41
|
+
source: result.source,
|
|
42
|
+
hits: shapedHits,
|
|
43
|
+
...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}),
|
|
44
|
+
...(result.semanticSearch ? { semanticSearch: result.semanticSearch } : {}),
|
|
45
|
+
...(result.tip ? { tip: result.tip } : {}),
|
|
46
|
+
...(result.warnings ? { warnings: result.warnings } : {}),
|
|
47
|
+
...(result.timing ? { timing: result.timing } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
hits: shapedHits,
|
|
52
|
+
...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}),
|
|
53
|
+
...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
|
|
54
|
+
...(result.tip ? { tip: result.tip } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function shapeRegistrySearchOutput(result, detail) {
|
|
58
|
+
const hits = Array.isArray(result.hits) ? result.hits : [];
|
|
59
|
+
const assetHits = Array.isArray(result.assetHits) ? result.assetHits : [];
|
|
60
|
+
// Shape stash hits as registry type
|
|
61
|
+
const shapedKitHits = hits.map((hit) => shapeSearchHit({ ...hit, type: "registry" }, detail));
|
|
62
|
+
// Shape asset hits by detail level
|
|
63
|
+
const shapedAssetHits = assetHits.map((hit) => shapeAssetHit(hit, detail));
|
|
64
|
+
const shaped = {
|
|
65
|
+
hits: shapedKitHits,
|
|
66
|
+
...(shapedAssetHits.length > 0 ? { assetHits: shapedAssetHits } : {}),
|
|
67
|
+
...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
|
|
68
|
+
};
|
|
69
|
+
if (detail === "full") {
|
|
70
|
+
shaped.query = result.query;
|
|
71
|
+
}
|
|
72
|
+
return shaped;
|
|
73
|
+
}
|
|
74
|
+
export function shapeAssetHit(hit, detail) {
|
|
75
|
+
if (detail === "brief")
|
|
76
|
+
return pickFields(hit, ["assetName", "assetType", "action", "estimatedTokens"]);
|
|
77
|
+
if (detail === "normal") {
|
|
78
|
+
return capDescription(pickFields(hit, ["assetName", "assetType", "description", "stash", "action", "estimatedTokens"]), NORMAL_DESCRIPTION_LIMIT);
|
|
79
|
+
}
|
|
80
|
+
return hit;
|
|
81
|
+
}
|
|
82
|
+
export function shapeSearchHit(hit, detail) {
|
|
83
|
+
if (hit.type === "registry") {
|
|
84
|
+
if (detail === "brief")
|
|
85
|
+
return pickFields(hit, ["name", "action"]);
|
|
86
|
+
if (detail === "normal") {
|
|
87
|
+
return capDescription(pickFields(hit, ["name", "description", "action", "curated"]), NORMAL_DESCRIPTION_LIMIT);
|
|
88
|
+
}
|
|
89
|
+
return hit;
|
|
90
|
+
}
|
|
91
|
+
// Stash hit (local or remote)
|
|
92
|
+
if (detail === "brief")
|
|
93
|
+
return pickFields(hit, ["type", "name", "action", "estimatedTokens"]);
|
|
94
|
+
if (detail === "normal") {
|
|
95
|
+
return capDescription(pickFields(hit, ["type", "name", "description", "action", "score", "estimatedTokens"]), NORMAL_DESCRIPTION_LIMIT);
|
|
96
|
+
}
|
|
97
|
+
return hit;
|
|
98
|
+
}
|
|
99
|
+
/** Agent-optimized search hit: only fields an LLM agent needs to decide and act */
|
|
100
|
+
export function shapeSearchHitForAgent(hit) {
|
|
101
|
+
const picked = pickFields(hit, ["name", "ref", "type", "description", "action", "score", "estimatedTokens"]);
|
|
102
|
+
return capDescription(picked, NORMAL_DESCRIPTION_LIMIT);
|
|
103
|
+
}
|
|
104
|
+
export function capDescription(hit, limit) {
|
|
105
|
+
if (typeof hit.description !== "string")
|
|
106
|
+
return hit;
|
|
107
|
+
return { ...hit, description: truncateDescription(hit.description, limit) };
|
|
108
|
+
}
|
|
109
|
+
export function truncateDescription(description, limit) {
|
|
110
|
+
const normalized = description.replace(/\s+/g, " ").trim();
|
|
111
|
+
if (normalized.length <= limit)
|
|
112
|
+
return normalized;
|
|
113
|
+
const truncated = normalized.slice(0, limit - 1);
|
|
114
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
115
|
+
const safe = lastSpace >= Math.floor(limit * 0.6) ? truncated.slice(0, lastSpace) : truncated;
|
|
116
|
+
return `${safe.trimEnd()}...`;
|
|
117
|
+
}
|
|
118
|
+
export function shapeShowOutput(result, detail, forAgent = false) {
|
|
119
|
+
if (forAgent) {
|
|
120
|
+
return pickFields(result, [
|
|
121
|
+
"type",
|
|
122
|
+
"name",
|
|
123
|
+
"description",
|
|
124
|
+
"action",
|
|
125
|
+
"content",
|
|
126
|
+
"template",
|
|
127
|
+
"prompt",
|
|
128
|
+
"run",
|
|
129
|
+
"setup",
|
|
130
|
+
"cwd",
|
|
131
|
+
"toolPolicy",
|
|
132
|
+
"modelHint",
|
|
133
|
+
"agent",
|
|
134
|
+
"parameters",
|
|
135
|
+
"workflowTitle",
|
|
136
|
+
"workflowParameters",
|
|
137
|
+
"steps",
|
|
138
|
+
"keys",
|
|
139
|
+
"comments",
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
if (detail === "summary") {
|
|
143
|
+
return pickFields(result, [
|
|
144
|
+
"type",
|
|
145
|
+
"name",
|
|
146
|
+
"description",
|
|
147
|
+
"tags",
|
|
148
|
+
"parameters",
|
|
149
|
+
"workflowTitle",
|
|
150
|
+
"action",
|
|
151
|
+
"run",
|
|
152
|
+
"origin",
|
|
153
|
+
"keys",
|
|
154
|
+
"comments",
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
157
|
+
const base = pickFields(result, [
|
|
158
|
+
"type",
|
|
159
|
+
"name",
|
|
160
|
+
"origin",
|
|
161
|
+
"action",
|
|
162
|
+
"description",
|
|
163
|
+
"tags",
|
|
164
|
+
"content",
|
|
165
|
+
"template",
|
|
166
|
+
"prompt",
|
|
167
|
+
"toolPolicy",
|
|
168
|
+
"modelHint",
|
|
169
|
+
"agent",
|
|
170
|
+
"parameters",
|
|
171
|
+
"workflowTitle",
|
|
172
|
+
"workflowParameters",
|
|
173
|
+
"steps",
|
|
174
|
+
"run",
|
|
175
|
+
"setup",
|
|
176
|
+
"cwd",
|
|
177
|
+
"keys",
|
|
178
|
+
"comments",
|
|
179
|
+
]);
|
|
180
|
+
if (detail !== "full") {
|
|
181
|
+
return base;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
schemaVersion: 1,
|
|
185
|
+
...base,
|
|
186
|
+
...pickFields(result, ["path", "editable", "editHint"]),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export function pickFields(source, fields) {
|
|
190
|
+
const result = {};
|
|
191
|
+
for (const field of fields) {
|
|
192
|
+
if (source[field] !== undefined) {
|
|
193
|
+
result[field] = source[field];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
export { NORMAL_DESCRIPTION_LIMIT };
|