akm-cli 0.5.0 → 0.6.0-rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +53 -5
  2. package/README.md +9 -9
  3. package/dist/cli.js +379 -1448
  4. package/dist/{completions.js → commands/completions.js} +1 -1
  5. package/dist/{config-cli.js → commands/config-cli.js} +109 -11
  6. package/dist/commands/curate.js +263 -0
  7. package/dist/{info.js → commands/info.js} +17 -11
  8. package/dist/{init.js → commands/init.js} +4 -4
  9. package/dist/{install-audit.js → commands/install-audit.js} +14 -2
  10. package/dist/{installed-kits.js → commands/installed-stashes.js} +122 -50
  11. package/dist/commands/migration-help.js +141 -0
  12. package/dist/{registry-search.js → commands/registry-search.js} +68 -9
  13. package/dist/commands/remember.js +178 -0
  14. package/dist/{stash-search.js → commands/search.js} +28 -69
  15. package/dist/{self-update.js → commands/self-update.js} +3 -3
  16. package/dist/{stash-show.js → commands/show.js} +106 -81
  17. package/dist/{stash-add.js → commands/source-add.js} +133 -67
  18. package/dist/{stash-clone.js → commands/source-clone.js} +15 -13
  19. package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
  20. package/dist/{vault.js → commands/vault.js} +43 -0
  21. package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
  22. package/dist/{asset-registry.js → core/asset-registry.js} +30 -6
  23. package/dist/{asset-spec.js → core/asset-spec.js} +13 -6
  24. package/dist/{common.js → core/common.js} +147 -50
  25. package/dist/{config.js → core/config.js} +288 -29
  26. package/dist/core/errors.js +90 -0
  27. package/dist/{frontmatter.js → core/frontmatter.js} +64 -8
  28. package/dist/{paths.js → core/paths.js} +4 -4
  29. package/dist/core/write-source.js +280 -0
  30. package/dist/{local-search.js → indexer/db-search.js} +49 -32
  31. package/dist/{db.js → indexer/db.js} +210 -81
  32. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  33. package/dist/{indexer.js → indexer/indexer.js} +153 -30
  34. package/dist/{manifest.js → indexer/manifest.js} +10 -10
  35. package/dist/{matchers.js → indexer/matchers.js} +4 -7
  36. package/dist/{metadata.js → indexer/metadata.js} +9 -5
  37. package/dist/{search-source.js → indexer/search-source.js} +97 -55
  38. package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
  39. package/dist/{walker.js → indexer/walker.js} +1 -1
  40. package/dist/{lockfile.js → integrations/lockfile.js} +29 -2
  41. package/dist/{llm.js → llm/client.js} +12 -48
  42. package/dist/llm/embedder.js +127 -0
  43. package/dist/llm/embedders/cache.js +47 -0
  44. package/dist/llm/embedders/local.js +152 -0
  45. package/dist/llm/embedders/remote.js +121 -0
  46. package/dist/llm/embedders/types.js +39 -0
  47. package/dist/llm/metadata-enhance.js +53 -0
  48. package/dist/output/cli-hints.js +301 -0
  49. package/dist/output/context.js +95 -0
  50. package/dist/{renderers.js → output/renderers.js} +57 -61
  51. package/dist/output/shapes.js +212 -0
  52. package/dist/output/text.js +520 -0
  53. package/dist/{registry-build-index.js → registry/build-index.js} +48 -32
  54. package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
  55. package/dist/registry/factory.js +33 -0
  56. package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
  57. package/dist/registry/providers/index.js +11 -0
  58. package/dist/{providers → registry/providers}/skills-sh.js +60 -4
  59. package/dist/{providers → registry/providers}/static-index.js +126 -56
  60. package/dist/registry/providers/types.js +25 -0
  61. package/dist/{registry-resolve.js → registry/resolve.js} +10 -6
  62. package/dist/{detect.js → setup/detect.js} +0 -27
  63. package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
  64. package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
  65. package/dist/{setup.js → setup/setup.js} +162 -129
  66. package/dist/setup/steps.js +45 -0
  67. package/dist/{kit-include.js → sources/include.js} +1 -1
  68. package/dist/sources/provider-factory.js +36 -0
  69. package/dist/sources/provider.js +21 -0
  70. package/dist/sources/providers/filesystem.js +35 -0
  71. package/dist/{stash-providers → sources/providers}/git.js +218 -28
  72. package/dist/{stash-providers → sources/providers}/index.js +4 -4
  73. package/dist/sources/providers/install-types.js +14 -0
  74. package/dist/sources/providers/npm.js +160 -0
  75. package/dist/sources/providers/provider-utils.js +173 -0
  76. package/dist/sources/providers/sync-from-ref.js +45 -0
  77. package/dist/sources/providers/tar-utils.js +154 -0
  78. package/dist/{stash-providers → sources/providers}/website.js +60 -20
  79. package/dist/{stash-resolve.js → sources/resolve.js} +13 -12
  80. package/dist/{wiki.js → wiki/wiki.js} +18 -17
  81. package/dist/{workflow-authoring.js → workflows/authoring.js} +48 -17
  82. package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
  83. package/dist/{workflow-db.js → workflows/db.js} +1 -1
  84. package/dist/workflows/document-cache.js +20 -0
  85. package/dist/workflows/parser.js +379 -0
  86. package/dist/workflows/renderer.js +78 -0
  87. package/dist/{workflow-runs.js → workflows/runs.js} +84 -30
  88. package/dist/workflows/schema.js +11 -0
  89. package/dist/workflows/validator.js +48 -0
  90. package/docs/README.md +30 -0
  91. package/docs/migration/release-notes/0.0.13.md +4 -0
  92. package/docs/migration/release-notes/0.1.0.md +6 -0
  93. package/docs/migration/release-notes/0.2.0.md +6 -0
  94. package/docs/migration/release-notes/0.3.0.md +5 -0
  95. package/docs/migration/release-notes/0.5.0.md +6 -0
  96. package/docs/migration/release-notes/0.6.0.md +75 -0
  97. package/docs/migration/release-notes/README.md +21 -0
  98. package/package.json +3 -2
  99. package/dist/embedder.js +0 -351
  100. package/dist/errors.js +0 -34
  101. package/dist/migration-help.js +0 -110
  102. package/dist/registry-factory.js +0 -19
  103. package/dist/registry-install.js +0 -532
  104. package/dist/ripgrep.js +0 -2
  105. package/dist/stash-provider-factory.js +0 -35
  106. package/dist/stash-provider.js +0 -1
  107. package/dist/stash-providers/filesystem.js +0 -41
  108. package/dist/stash-providers/openviking.js +0 -348
  109. package/dist/stash-providers/provider-utils.js +0 -11
  110. package/dist/stash-types.js +0 -1
  111. package/dist/workflow-markdown.js +0 -251
  112. /package/dist/{markdown.js → core/markdown.js} +0 -0
  113. /package/dist/{warn.js → core/warn.js} +0 -0
  114. /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
  115. /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
  116. /package/dist/{github.js → integrations/github.js} +0 -0
  117. /package/dist/{registry-provider.js → registry/types.js} +0 -0
  118. /package/dist/{registry-types.js → sources/types.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
+ import { buildWorkflowAction } from "../output/renderers";
2
3
  import { registerActionBuilder, registerTypeRenderer } from "./asset-registry";
3
4
  import { toPosix } from "./common";
4
- import { buildWorkflowAction } from "./renderers";
5
5
  const markdownSpec = {
6
6
  isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
7
7
  toCanonicalName: (typeRoot, filePath) => {
@@ -129,8 +129,6 @@ export const ASSET_SPECS = ASSET_SPECS_INTERNAL;
129
129
  export function registerAssetType(type, spec) {
130
130
  ASSET_SPECS_INTERNAL[type] = spec;
131
131
  TYPE_DIRS[type] = spec.stashDir;
132
- ASSET_TYPES.length = 0;
133
- ASSET_TYPES.push(...getAssetTypes());
134
132
  // Auto-register renderer and action builder if provided in spec
135
133
  if (spec.rendererName) {
136
134
  registerTypeRenderer(type, spec.rendererName);
@@ -139,11 +137,20 @@ export function registerAssetType(type, spec) {
139
137
  registerActionBuilder(type, spec.actionBuilder);
140
138
  }
141
139
  }
140
+ /**
141
+ * Remove a previously-registered asset type.
142
+ *
143
+ * Primarily used by tests for cleanup after `registerAssetType` calls so
144
+ * subsequent tests see a pristine type registry. Built-in types should not
145
+ * normally be deregistered at runtime.
146
+ */
147
+ export function deregisterAssetType(type) {
148
+ delete ASSET_SPECS_INTERNAL[type];
149
+ delete TYPE_DIRS[type];
150
+ }
142
151
  export function getAssetTypes() {
143
152
  return Object.keys(ASSET_SPECS_INTERNAL);
144
153
  }
145
- /** Warning: mutable array — stale if captured before `registerAssetType()` calls. Prefer `getAssetTypes()`. */
146
- export const ASSET_TYPES = getAssetTypes();
147
154
  export const TYPE_DIRS = Object.fromEntries(Object.entries(ASSET_SPECS_INTERNAL).map(([type, spec]) => [type, spec.stashDir]));
148
155
  export function isRelevantAssetFile(assetType, fileName) {
149
156
  return ASSET_SPECS[assetType]?.isRelevantFile(fileName) ?? false;
@@ -158,7 +165,7 @@ export function deriveCanonicalAssetNameFromStashRoot(assetType, stashRoot, file
158
165
  // When the first segment matches the canonical type dir (e.g. "agents"),
159
166
  // use it as the type root so canonical names are relative to it.
160
167
  // Otherwise fall back to stashRoot — this preserves the full relative path
161
- // as the canonical name, which is correct for installed kits that live
168
+ // as the canonical name, which is correct for installed stashes that live
162
169
  // under custom directories (e.g. "tools/agents/svelte-file-editor").
163
170
  const typeRoot = firstSegment === TYPE_DIRS[assetType] ? path.join(stashRoot, firstSegment) : stashRoot;
164
171
  return deriveCanonicalAssetName(assetType, typeRoot, filePath);
@@ -24,21 +24,17 @@ export function isAssetType(type) {
24
24
  * 2. stashDir field in config.json
25
25
  * 3. Platform default (~/akm or ~/Documents/akm on Windows)
26
26
  *
27
- * WARNING: May write to config file as a side effect when AKM_STASH_DIR is set.
28
- * Specifically, when AKM_STASH_DIR is set and `options.readOnly` is not true,
29
- * this function calls `persistStashDirToConfig()` which writes the resolved
30
- * path into config.json on disk.
27
+ * Pure read: never writes to disk. The legacy `readOnly` option is accepted
28
+ * (and ignored) for one release cycle so older callers continue to compile;
29
+ * it can be removed in the next minor bump.
31
30
  *
32
31
  * Throws if no valid stash directory is found.
33
32
  */
34
- export function resolveStashDir(options) {
33
+ export function resolveStashDir(_options) {
35
34
  // 1. Env var override (for CI, scripts, testing)
36
35
  const envDir = process.env.AKM_STASH_DIR?.trim();
37
36
  if (envDir) {
38
- const resolved = validateStashDir(envDir);
39
- if (!options?.readOnly)
40
- persistStashDirToConfig(resolved);
41
- return resolved;
37
+ return validateStashDir(envDir);
42
38
  }
43
39
  // 2. Config file stashDir field
44
40
  const configStashDir = readStashDirFromConfig();
@@ -50,7 +46,7 @@ export function resolveStashDir(options) {
50
46
  return defaultDir;
51
47
  }
52
48
  throw new ConfigError(`No stash directory found. Run "akm init" to create one at ${defaultDir}, ` +
53
- `or set stashDir in ${getConfigPath()}.`);
49
+ `or set stashDir in ${getConfigPath()}.`, "STASH_DIR_NOT_FOUND");
54
50
  }
55
51
  function validateStashDir(raw) {
56
52
  const stashDir = path.resolve(raw);
@@ -59,10 +55,10 @@ function validateStashDir(raw) {
59
55
  stat = fs.statSync(stashDir);
60
56
  }
61
57
  catch {
62
- throw new ConfigError(`Unable to read stash directory at "${stashDir}".`);
58
+ throw new ConfigError(`Unable to read stash directory at "${stashDir}".`, "STASH_DIR_UNREADABLE");
63
59
  }
64
60
  if (!stat.isDirectory()) {
65
- throw new ConfigError(`Stash path must point to a directory: "${stashDir}".`);
61
+ throw new ConfigError(`Stash path must point to a directory: "${stashDir}".`, "STASH_DIR_NOT_A_DIRECTORY");
66
62
  }
67
63
  return stashDir;
68
64
  }
@@ -92,42 +88,6 @@ function readStashDirFromConfig() {
92
88
  }
93
89
  return undefined;
94
90
  }
95
- /**
96
- * Persist stashDir to config.json if not already set, so users can
97
- * transition away from relying on the AKM_STASH_DIR env var.
98
- *
99
- * WARNING: This function writes to disk (config.json). It is called as a side
100
- * effect of `resolveStashDir()` when AKM_STASH_DIR is set and `readOnly` is
101
- * not true. Callers that must not touch the filesystem should pass
102
- * `{ readOnly: true }` to `resolveStashDir()`.
103
- */
104
- function persistStashDirToConfig(stashDir) {
105
- try {
106
- const configPath = getConfigPath();
107
- let raw = {};
108
- try {
109
- const text = fs.readFileSync(configPath, "utf8");
110
- const parsed = JSON.parse(text);
111
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
112
- raw = parsed;
113
- }
114
- }
115
- catch {
116
- // No existing config or invalid — start fresh
117
- }
118
- if (!raw.stashDir) {
119
- raw.stashDir = stashDir;
120
- const dir = path.dirname(configPath);
121
- fs.mkdirSync(dir, { recursive: true });
122
- const tmpPath = `${configPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
123
- fs.writeFileSync(tmpPath, `${JSON.stringify(raw, null, 2)}\n`, "utf8");
124
- fs.renameSync(tmpPath, configPath);
125
- }
126
- }
127
- catch {
128
- // Non-fatal: best-effort persistence
129
- }
130
- }
131
91
  export function toPosix(input) {
132
92
  return input.replace(/\\/g, "/");
133
93
  }
@@ -144,12 +104,39 @@ export function isWithin(candidate, root) {
144
104
  const rel = path.relative(normalizedRoot, normalizedCandidate);
145
105
  return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
146
106
  }
107
+ /**
108
+ * Resolve symlinks on `p`, walking up to the closest existing ancestor when
109
+ * `p` itself does not exist. This ensures that comparisons between an
110
+ * existing directory and a not-yet-created child path inside it are
111
+ * consistent even when the directory hierarchy contains symlinks (e.g.
112
+ * macOS /tmp → /private/tmp, or a HOME that is itself a symlink).
113
+ */
147
114
  function safeRealpath(p) {
115
+ const resolved = path.resolve(p);
148
116
  try {
149
- return fs.realpathSync(path.resolve(p));
117
+ return fs.realpathSync(resolved);
150
118
  }
151
119
  catch {
152
- return path.resolve(p);
120
+ // Path doesn't exist — resolve symlinks on the nearest existing ancestor
121
+ // and reconstruct the full path from there.
122
+ const suffix = [];
123
+ let current = resolved;
124
+ for (;;) {
125
+ const parent = path.dirname(current);
126
+ if (parent === current) {
127
+ // Reached filesystem root without finding an existing entry.
128
+ return resolved;
129
+ }
130
+ suffix.unshift(path.basename(current));
131
+ current = parent;
132
+ try {
133
+ const realParent = fs.realpathSync(current);
134
+ return path.join(realParent, ...suffix);
135
+ }
136
+ catch {
137
+ // parent also doesn't exist; keep walking up
138
+ }
139
+ }
153
140
  }
154
141
  }
155
142
  function normalizeFsPathForComparison(value) {
@@ -207,6 +194,116 @@ export async function fetchWithRetry(url, init, options) {
207
194
  function shouldRetry(status) {
208
195
  return status === 429 || status >= 500;
209
196
  }
197
+ /**
198
+ * Read stdin as UTF-8 text if something is piped in. Returns `undefined`
199
+ * when stdin is a TTY (no pipe) or when the piped content is empty.
200
+ */
201
+ export function tryReadStdinText() {
202
+ if (process.stdin.isTTY)
203
+ return undefined;
204
+ const input = fs.readFileSync(0, "utf8");
205
+ return input.length > 0 ? input : undefined;
206
+ }
207
+ /**
208
+ * Default byte cap for untrusted network responses (10 MB).
209
+ *
210
+ * Applies to website scraping, registry index fetches, and any other
211
+ * response that is read into memory from a source the CLI does not fully
212
+ * control. A compromised or malicious endpoint that streams an unbounded
213
+ * response would otherwise exhaust RAM — this cap ensures the process
214
+ * aborts with a clean error instead of crashing.
215
+ */
216
+ export const DEFAULT_RESPONSE_BYTE_CAP = 10 * 1024 * 1024;
217
+ /**
218
+ * Thrown by {@link readBodyWithByteCap} and its helpers when a response
219
+ * body exceeds the caller's byte cap. Callers can catch this specifically
220
+ * to surface a targeted error to the user.
221
+ */
222
+ export class ResponseTooLargeError extends Error {
223
+ url;
224
+ maxBytes;
225
+ observedBytes;
226
+ constructor(url, maxBytes, observedBytes) {
227
+ const observed = observedBytes === null ? "unknown" : `${observedBytes} bytes`;
228
+ super(`Response body exceeded ${maxBytes} bytes (observed: ${observed}): ${url}`);
229
+ this.name = "ResponseTooLargeError";
230
+ this.url = url;
231
+ this.maxBytes = maxBytes;
232
+ this.observedBytes = observedBytes;
233
+ }
234
+ }
235
+ /**
236
+ * Read a Response body as a UTF-8 string with a byte-count cap.
237
+ *
238
+ * Streams the body so we abort as soon as the cap is exceeded, without
239
+ * buffering the full response first. If the server sent a
240
+ * `Content-Length` larger than the cap, we refuse before reading any
241
+ * bytes. `response.body` is consumed and cancelled on cap breach.
242
+ *
243
+ * `maxBytes` defaults to {@link DEFAULT_RESPONSE_BYTE_CAP} (10 MB).
244
+ */
245
+ export async function readBodyWithByteCap(response, maxBytes = DEFAULT_RESPONSE_BYTE_CAP) {
246
+ const url = response.url || "(unknown URL)";
247
+ const contentLengthHeader = response.headers.get("content-length");
248
+ if (contentLengthHeader) {
249
+ const declared = Number(contentLengthHeader);
250
+ if (Number.isFinite(declared) && declared > maxBytes) {
251
+ // Don't even start reading.
252
+ await response.body?.cancel?.().catch(() => undefined);
253
+ throw new ResponseTooLargeError(url, maxBytes, declared);
254
+ }
255
+ }
256
+ const body = response.body;
257
+ if (!body) {
258
+ // No streaming body available (e.g., some mock environments). Fall
259
+ // back to text() but still enforce the cap post-hoc.
260
+ const text = await response.text();
261
+ if (text.length > maxBytes)
262
+ throw new ResponseTooLargeError(url, maxBytes, text.length);
263
+ return text;
264
+ }
265
+ const reader = body.getReader();
266
+ const chunks = [];
267
+ let total = 0;
268
+ try {
269
+ while (true) {
270
+ const { done, value } = await reader.read();
271
+ if (done)
272
+ break;
273
+ if (!value)
274
+ continue;
275
+ total += value.byteLength;
276
+ if (total > maxBytes) {
277
+ await reader.cancel().catch(() => undefined);
278
+ throw new ResponseTooLargeError(url, maxBytes, total);
279
+ }
280
+ chunks.push(value);
281
+ }
282
+ }
283
+ finally {
284
+ reader.releaseLock?.();
285
+ }
286
+ if (chunks.length === 0)
287
+ return "";
288
+ if (chunks.length === 1)
289
+ return new TextDecoder().decode(chunks[0]);
290
+ const combined = new Uint8Array(total);
291
+ let offset = 0;
292
+ for (const chunk of chunks) {
293
+ combined.set(chunk, offset);
294
+ offset += chunk.byteLength;
295
+ }
296
+ return new TextDecoder().decode(combined);
297
+ }
298
+ /**
299
+ * Parse a Response body as JSON with a byte-count cap. A cheap wrapper
300
+ * around {@link readBodyWithByteCap}; prefer this for registry index
301
+ * fetches, GitHub API responses, and any other untrusted JSON source.
302
+ */
303
+ export async function jsonWithByteCap(response, maxBytes = DEFAULT_RESPONSE_BYTE_CAP) {
304
+ const text = await readBodyWithByteCap(response, maxBytes);
305
+ return JSON.parse(text);
306
+ }
210
307
  function parseRetryAfter(response) {
211
308
  const header = response.headers.get("retry-after");
212
309
  if (!header)