akm-cli 0.9.0-beta.50 → 0.9.0-beta.52
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 +2 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/wiki/ingest-workflow-template.md +34 -12
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli.js +12 -6
- package/dist/commands/config-cli.js +18 -2
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +17 -2
- package/dist/commands/env/secret-cli.js +24 -2
- package/dist/commands/health/checks.js +1 -1
- package/dist/commands/improve/improve-auto-accept.js +30 -2
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-result-file.js +9 -2
- package/dist/commands/improve/preparation.js +10 -2
- package/dist/commands/improve/recombine.js +52 -15
- package/dist/commands/lint/env-key-rules.js +4 -0
- package/dist/commands/read/knowledge.js +5 -2
- package/dist/commands/read/search-cli.js +2 -4
- package/dist/commands/read/search.js +9 -6
- package/dist/commands/read/show.js +19 -5
- package/dist/commands/sources/init.js +13 -8
- package/dist/commands/sources/installed-stashes.js +6 -2
- package/dist/commands/sources/schema-repair.js +33 -47
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/tasks/tasks.js +38 -10
- package/dist/core/asset/asset-registry.js +1 -1
- package/dist/core/asset/asset-spec.js +4 -2
- package/dist/core/config/config-migration.js +12 -11
- package/dist/indexer/passes/memory-inference.js +3 -2
- package/dist/indexer/search/db-search.js +6 -4
- package/dist/indexer/search/search-source.js +15 -2
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -135
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/renderers.js +1 -1
- package/dist/scripts/migrate-storage.js +463 -347
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +99 -99
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/git-install.js +10 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/website.js +8 -3
- package/dist/sources/website-ingest.js +136 -20
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +15 -11
- package/docs/data-and-telemetry.md +2 -2
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +8 -8
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
+
import { isIP } from "node:net";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { fetchWithRetry, ResponseTooLargeError, readBodyWithByteCap, resolveStashDir } from "../core/common.js";
|
|
8
9
|
import { ConfigError, UsageError } from "../core/errors.js";
|
|
@@ -31,6 +32,20 @@ const WEBSITE_PAGE_BYTE_CAP = 5 * 1024 * 1024;
|
|
|
31
32
|
* whole crawl and return what we have when time runs out.
|
|
32
33
|
*/
|
|
33
34
|
const WEBSITE_CRAWL_WALL_CLOCK_MS = 10 * 60 * 1000;
|
|
35
|
+
const WEBSITE_MAX_REDIRECTS = 8;
|
|
36
|
+
export function shouldAllowPrivateWebsiteHostsForTests() {
|
|
37
|
+
return process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
|
|
38
|
+
}
|
|
39
|
+
export function shouldAllowPrivateWebsiteUrlForTests(rawUrl) {
|
|
40
|
+
if (!shouldAllowPrivateWebsiteHostsForTests())
|
|
41
|
+
return false;
|
|
42
|
+
try {
|
|
43
|
+
return isLoopbackWebsiteHostname(new URL(rawUrl).hostname.toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
34
49
|
function resolveFetcherStashDir(explicitStashDir) {
|
|
35
50
|
if (explicitStashDir)
|
|
36
51
|
return explicitStashDir;
|
|
@@ -52,7 +67,7 @@ export function getWebsiteCachePaths(siteUrl) {
|
|
|
52
67
|
}
|
|
53
68
|
export async function ensureWebsiteMirror(config, options) {
|
|
54
69
|
const rawUrl = config.url ?? "";
|
|
55
|
-
const normalizedUrl = validateWebsiteUrl(rawUrl);
|
|
70
|
+
const normalizedUrl = validateWebsiteUrl(rawUrl, { allowPrivateHosts: options?.allowPrivateHosts });
|
|
56
71
|
const cachePaths = getWebsiteCachePaths(normalizedUrl);
|
|
57
72
|
const requireStashDir = options?.requireStashDir === true;
|
|
58
73
|
const force = options?.force === true;
|
|
@@ -74,6 +89,7 @@ export async function ensureWebsiteMirror(config, options) {
|
|
|
74
89
|
await scrapeWebsiteToStash(normalizedUrl, cachePaths.stashDir, {
|
|
75
90
|
maxPages: coercePositiveInt(config.options?.maxPages, MAX_PAGES_DEFAULT),
|
|
76
91
|
maxDepth: coercePositiveInt(config.options?.maxDepth, MAX_DEPTH_DEFAULT),
|
|
92
|
+
allowPrivateHosts: options?.allowPrivateHosts,
|
|
77
93
|
});
|
|
78
94
|
fs.writeFileSync(cachePaths.manifestPath, `${JSON.stringify({ url: normalizedUrl, fetchedAt: new Date().toISOString() }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
79
95
|
return cachePaths;
|
|
@@ -126,7 +142,7 @@ async function scrapeWebsiteToStash(startUrl, stashDir, options) {
|
|
|
126
142
|
}
|
|
127
143
|
}
|
|
128
144
|
export async function fetchWebsiteMarkdownSnapshot(rawUrl, options) {
|
|
129
|
-
const normalizedUrl = validateWebsiteInputUrl(rawUrl);
|
|
145
|
+
const normalizedUrl = validateWebsiteInputUrl(rawUrl, { allowPrivateHosts: options?.allowPrivateHosts });
|
|
130
146
|
const parsedUrl = new URL(normalizedUrl);
|
|
131
147
|
const stashDir = resolveFetcherStashDir(options?.stashDir);
|
|
132
148
|
const context = {
|
|
@@ -147,7 +163,7 @@ export async function fetchWebsiteMarkdownSnapshot(rawUrl, options) {
|
|
|
147
163
|
warn("[akm] wiki-fetcher %s threw on %s: %s", fetcher.name, normalizedUrl, error instanceof Error ? error.message : String(error));
|
|
148
164
|
}
|
|
149
165
|
}
|
|
150
|
-
const fetched = await fetchWebsitePage(normalizedUrl);
|
|
166
|
+
const fetched = await fetchWebsitePage(normalizedUrl, { allowPrivateHosts: options?.allowPrivateHosts });
|
|
151
167
|
if (!fetched) {
|
|
152
168
|
throw new UsageError(`No content could be fetched from ${normalizedUrl}`);
|
|
153
169
|
}
|
|
@@ -189,7 +205,7 @@ async function crawlWebsite(startUrl, options) {
|
|
|
189
205
|
if (!normalized || visited.has(normalized))
|
|
190
206
|
continue;
|
|
191
207
|
visited.add(normalized);
|
|
192
|
-
const fetched = await fetchWebsitePage(normalized);
|
|
208
|
+
const fetched = await fetchWebsitePage(normalized, { allowPrivateHosts: options.allowPrivateHosts });
|
|
193
209
|
if (!fetched)
|
|
194
210
|
continue;
|
|
195
211
|
pages.push(fetched.page);
|
|
@@ -211,17 +227,8 @@ async function crawlWebsite(startUrl, options) {
|
|
|
211
227
|
}
|
|
212
228
|
return pages;
|
|
213
229
|
}
|
|
214
|
-
async function fetchWebsitePage(pageUrl) {
|
|
215
|
-
const
|
|
216
|
-
if (parsedUrl.hostname.endsWith(".invalid")) {
|
|
217
|
-
throw new Error(`Refusing to fetch reserved invalid hostname: ${parsedUrl.hostname}`);
|
|
218
|
-
}
|
|
219
|
-
const response = await fetchWithRetry(pageUrl, {
|
|
220
|
-
headers: {
|
|
221
|
-
Accept: "text/html, text/markdown, text/plain;q=0.9, application/xhtml+xml;q=0.8",
|
|
222
|
-
"User-Agent": "akm-cli website provider",
|
|
223
|
-
},
|
|
224
|
-
}, { timeout: 15_000, retries: 1 });
|
|
230
|
+
async function fetchWebsitePage(pageUrl, options) {
|
|
231
|
+
const response = await fetchWebsiteResponse(pageUrl, 0, options);
|
|
225
232
|
if (!response.ok) {
|
|
226
233
|
if (response.status === 404)
|
|
227
234
|
return null;
|
|
@@ -238,6 +245,7 @@ async function fetchWebsitePage(pageUrl) {
|
|
|
238
245
|
throw err;
|
|
239
246
|
}
|
|
240
247
|
const finalUrl = normalizeCrawlUrl(response.url || pageUrl) ?? pageUrl;
|
|
248
|
+
assertWebsiteRequestUrl(finalUrl, Error, options);
|
|
241
249
|
if (contentType.includes("text/html") || contentType.includes("application/xhtml+xml") || looksLikeMarkup(body)) {
|
|
242
250
|
const title = extractHtmlTitle(body) || new URL(finalUrl).hostname;
|
|
243
251
|
return {
|
|
@@ -258,6 +266,29 @@ async function fetchWebsitePage(pageUrl) {
|
|
|
258
266
|
links: [],
|
|
259
267
|
};
|
|
260
268
|
}
|
|
269
|
+
async function fetchWebsiteResponse(pageUrl, redirectCount = 0, options) {
|
|
270
|
+
assertWebsiteRequestUrl(pageUrl, Error, options);
|
|
271
|
+
const response = await fetchWithRetry(pageUrl, {
|
|
272
|
+
headers: {
|
|
273
|
+
Accept: "text/html, text/markdown, text/plain;q=0.9, application/xhtml+xml;q=0.8",
|
|
274
|
+
"User-Agent": "akm-cli website provider",
|
|
275
|
+
},
|
|
276
|
+
redirect: "manual",
|
|
277
|
+
}, { timeout: 15_000, retries: 1 });
|
|
278
|
+
if (response.status >= 300 && response.status < 400) {
|
|
279
|
+
if (redirectCount >= WEBSITE_MAX_REDIRECTS) {
|
|
280
|
+
throw new Error(`Too many redirects while fetching ${pageUrl}`);
|
|
281
|
+
}
|
|
282
|
+
const location = response.headers.get("location");
|
|
283
|
+
if (!location) {
|
|
284
|
+
throw new Error(`Redirect response from ${pageUrl} did not include a Location header`);
|
|
285
|
+
}
|
|
286
|
+
const nextUrl = new URL(location, pageUrl).toString();
|
|
287
|
+
assertWebsiteRequestUrl(nextUrl, Error, options);
|
|
288
|
+
return fetchWebsiteResponse(nextUrl, redirectCount + 1, options);
|
|
289
|
+
}
|
|
290
|
+
return response;
|
|
291
|
+
}
|
|
261
292
|
function buildMarkdownSnapshot(page, slug, tags) {
|
|
262
293
|
const title = sanitizeString(page.title, 200) || slug;
|
|
263
294
|
const description = sanitizeString(`Snapshot of ${page.url}`, 500);
|
|
@@ -282,13 +313,13 @@ function buildMarkdownSnapshot(page, slug, tags) {
|
|
|
282
313
|
"",
|
|
283
314
|
].join("\n");
|
|
284
315
|
}
|
|
285
|
-
export function validateWebsiteUrl(rawUrl) {
|
|
286
|
-
return validateWebsiteUrlWithError(rawUrl, ConfigError);
|
|
316
|
+
export function validateWebsiteUrl(rawUrl, options) {
|
|
317
|
+
return validateWebsiteUrlWithError(rawUrl, ConfigError, options);
|
|
287
318
|
}
|
|
288
|
-
export function validateWebsiteInputUrl(rawUrl) {
|
|
289
|
-
return validateWebsiteUrlWithError(rawUrl, UsageError);
|
|
319
|
+
export function validateWebsiteInputUrl(rawUrl, options) {
|
|
320
|
+
return validateWebsiteUrlWithError(rawUrl, UsageError, options);
|
|
290
321
|
}
|
|
291
|
-
function validateWebsiteUrlWithError(rawUrl, ErrorType) {
|
|
322
|
+
function validateWebsiteUrlWithError(rawUrl, ErrorType, options) {
|
|
292
323
|
if (!rawUrl) {
|
|
293
324
|
throw new ErrorType("Website provider requires a URL");
|
|
294
325
|
}
|
|
@@ -305,6 +336,7 @@ function validateWebsiteUrlWithError(rawUrl, ErrorType) {
|
|
|
305
336
|
if (parsed.username || parsed.password) {
|
|
306
337
|
throw new ErrorType("Website URL must not contain embedded credentials");
|
|
307
338
|
}
|
|
339
|
+
assertWebsiteRequestUrl(parsed.toString(), ErrorType, options);
|
|
308
340
|
parsed.hash = "";
|
|
309
341
|
return normalizeSiteUrl(parsed.toString());
|
|
310
342
|
}
|
|
@@ -503,6 +535,90 @@ function isAssetLikePath(pathname) {
|
|
|
503
535
|
function isSafeLinkUrl(url) {
|
|
504
536
|
return url.protocol === "http:" || url.protocol === "https:";
|
|
505
537
|
}
|
|
538
|
+
function assertWebsiteRequestUrl(rawUrl, ErrorType = Error, options) {
|
|
539
|
+
const parsedUrl = new URL(rawUrl);
|
|
540
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
541
|
+
if (hostname.endsWith(".invalid")) {
|
|
542
|
+
throw new ErrorType(`Refusing to fetch reserved invalid hostname: ${parsedUrl.hostname}`);
|
|
543
|
+
}
|
|
544
|
+
if (isForbiddenWebsiteHostname(hostname, options)) {
|
|
545
|
+
throw new ErrorType(`Refusing to fetch non-public website host: ${parsedUrl.hostname}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// WHATWG URL.hostname wraps IPv6 literals in brackets (e.g. "[::1]"), but
|
|
549
|
+
// node:net's isIP() only recognizes the bare address form and returns 0 for
|
|
550
|
+
// anything bracketed — silently skipping all IPv6 forbidden-host checks
|
|
551
|
+
// below for every hostname parsed off a URL. Strip the brackets before any
|
|
552
|
+
// isIP()/isForbiddenIpv6() call so those checks actually run.
|
|
553
|
+
function stripIpv6Brackets(hostname) {
|
|
554
|
+
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
555
|
+
}
|
|
556
|
+
function isForbiddenWebsiteHostname(hostname, options) {
|
|
557
|
+
if (options?.allowPrivateHosts === true)
|
|
558
|
+
return false;
|
|
559
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "metadata.google.internal") {
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
const bareHostname = stripIpv6Brackets(hostname);
|
|
563
|
+
const ipVersion = isIP(bareHostname);
|
|
564
|
+
if (ipVersion === 4)
|
|
565
|
+
return isForbiddenIpv4(bareHostname);
|
|
566
|
+
if (ipVersion === 6)
|
|
567
|
+
return isForbiddenIpv6(bareHostname);
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
function isLoopbackWebsiteHostname(hostname) {
|
|
571
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost"))
|
|
572
|
+
return true;
|
|
573
|
+
const bareHostname = stripIpv6Brackets(hostname);
|
|
574
|
+
const ipVersion = isIP(bareHostname);
|
|
575
|
+
if (ipVersion === 4)
|
|
576
|
+
return bareHostname.startsWith("127.");
|
|
577
|
+
if (ipVersion === 6)
|
|
578
|
+
return bareHostname === "::1";
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
function isForbiddenIpv4(hostname) {
|
|
582
|
+
const parts = hostname.split(".").map((part) => Number.parseInt(part, 10));
|
|
583
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
|
|
584
|
+
return true;
|
|
585
|
+
const [a, b] = parts;
|
|
586
|
+
return (a === 0 ||
|
|
587
|
+
a === 10 ||
|
|
588
|
+
a === 127 ||
|
|
589
|
+
(a === 169 && b === 254) ||
|
|
590
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
591
|
+
(a === 192 && b === 168));
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Extracts the embedded IPv4 address from an IPv4-mapped IPv6 literal
|
|
595
|
+
* (`::ffff:a.b.c.d` or its canonical hex form `::ffff:xxxx:yyyy`), or
|
|
596
|
+
* returns null if `hostname` isn't one.
|
|
597
|
+
*/
|
|
598
|
+
function extractIpv4MappedAddress(normalizedHostname) {
|
|
599
|
+
const match = normalizedHostname.match(/^::ffff:(?:(\d{1,3}(?:\.\d{1,3}){3})|([0-9a-f]{1,4}):([0-9a-f]{1,4}))$/);
|
|
600
|
+
if (!match)
|
|
601
|
+
return null;
|
|
602
|
+
if (match[1])
|
|
603
|
+
return match[1];
|
|
604
|
+
const high = Number.parseInt(match[2], 16);
|
|
605
|
+
const low = Number.parseInt(match[3], 16);
|
|
606
|
+
return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
|
|
607
|
+
}
|
|
608
|
+
function isForbiddenIpv6(hostname) {
|
|
609
|
+
const normalized = hostname.toLowerCase();
|
|
610
|
+
const mappedIpv4 = extractIpv4MappedAddress(normalized);
|
|
611
|
+
if (mappedIpv4)
|
|
612
|
+
return isForbiddenIpv4(mappedIpv4);
|
|
613
|
+
return (normalized === "::" ||
|
|
614
|
+
normalized === "::1" ||
|
|
615
|
+
normalized.startsWith("fc") ||
|
|
616
|
+
normalized.startsWith("fd") ||
|
|
617
|
+
normalized.startsWith("fe8") ||
|
|
618
|
+
normalized.startsWith("fe9") ||
|
|
619
|
+
normalized.startsWith("fea") ||
|
|
620
|
+
normalized.startsWith("feb"));
|
|
621
|
+
}
|
|
506
622
|
function stripDangerousBlockTag(value, tagName) {
|
|
507
623
|
const pattern = new RegExp(`<${tagName}\\b[^>]*>[\\s\\S]*?<\\/${tagName}\\s*>`, "gi");
|
|
508
624
|
return value.replace(pattern, "");
|
|
File without changes
|
package/dist/wiki/wiki.js
CHANGED
|
@@ -58,6 +58,8 @@ export const SCHEMA_MD = "schema.md";
|
|
|
58
58
|
export const INDEX_MD = "index.md";
|
|
59
59
|
export const LOG_MD = "log.md";
|
|
60
60
|
export const RAW_SUBDIR = "raw";
|
|
61
|
+
/** Canonical location for agent-authored pages, mirroring RAW_SUBDIR. */
|
|
62
|
+
export const PAGES_SUBDIR = "pages";
|
|
61
63
|
/** Files at a wiki root that are not pages. */
|
|
62
64
|
const WIKI_SPECIAL_FILES = new Set([SCHEMA_MD, INDEX_MD, LOG_MD]);
|
|
63
65
|
const WIKI_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -406,18 +408,20 @@ export function createWiki(stashDir, name) {
|
|
|
406
408
|
fs.writeFileSync(absPath, content, "utf8");
|
|
407
409
|
created.push(absPath);
|
|
408
410
|
}
|
|
409
|
-
// Ensure raw/
|
|
410
|
-
// Handle the dir-exists-but-no-.gitkeep case too (partial scaffolds,
|
|
411
|
+
// Ensure raw/ and pages/ exist with a .gitkeep so empty wikis survive clean
|
|
412
|
+
// clones. Handle the dir-exists-but-no-.gitkeep case too (partial scaffolds,
|
|
411
413
|
// user-created directories) so the invariant always holds after `create`.
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
414
|
+
for (const subdir of [RAW_SUBDIR, PAGES_SUBDIR]) {
|
|
415
|
+
const dir = path.join(wikiDir, subdir);
|
|
416
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
417
|
+
const gitkeepPath = path.join(dir, ".gitkeep");
|
|
418
|
+
if (fs.existsSync(gitkeepPath)) {
|
|
419
|
+
skipped.push(gitkeepPath);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
fs.writeFileSync(gitkeepPath, "", "utf8");
|
|
423
|
+
created.push(gitkeepPath);
|
|
424
|
+
}
|
|
421
425
|
}
|
|
422
426
|
return { name, ref: `wiki:${name}`, path: wikiDir, created, skipped };
|
|
423
427
|
}
|
|
@@ -63,7 +63,7 @@ Override: set `AKM_CACHE_DIR` or `XDG_CACHE_HOME`.
|
|
|
63
63
|
|
|
64
64
|
| Path | Contents | Safe to delete? |
|
|
65
65
|
|---|---|---|
|
|
66
|
-
| `<stash>/` | All your asset files: agents, skills, commands, knowledge, workflows, memories,
|
|
66
|
+
| `<stash>/` | All your asset files: agents, skills, commands, knowledge, workflows, memories, env files, secrets, wikis, lessons, facts | **No** — this is YOUR data |
|
|
67
67
|
| `<stash>/.akm/` | Hidden AKM metadata (v0.7 proposals, legacy runs) | Caution — check for pending proposals first |
|
|
68
68
|
|
|
69
69
|
Override: set `AKM_STASH_DIR` (or configure `stashDir` in `config.json`).
|
|
@@ -130,7 +130,7 @@ An append-only log of every mutating action you perform with AKM. Events are sto
|
|
|
130
130
|
|
|
131
131
|
### 2. Proposals Table
|
|
132
132
|
|
|
133
|
-
The proposal queue: pending, accepted, and rejected improvement proposals for your stash assets. Generated by `akm
|
|
133
|
+
The proposal queue: pending, accepted, and rejected improvement proposals for your stash assets. Generated by `akm improve`, `akm propose`, and related proposal-producing flows.
|
|
134
134
|
|
|
135
135
|
Contents:
|
|
136
136
|
- Proposal UUID (primary key)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Migration notes for akm v0.9.0
|
|
2
|
+
|
|
3
|
+
This is the final 0.9.0 cut. If you were already running a 0.9.0 beta, the
|
|
4
|
+
main upgrade work is to align scripts, prompts, and stash layout with the
|
|
5
|
+
released command surface.
|
|
6
|
+
|
|
7
|
+
Key operator-facing changes:
|
|
8
|
+
|
|
9
|
+
- Node.js is supported again for the published CLI. Install with Bun, Node.js
|
|
10
|
+
>= 20.12, or the prebuilt binary.
|
|
11
|
+
- The legacy `vault` asset type and `akm vault ...` command family are gone.
|
|
12
|
+
Use `env:` for whole `.env` groups and `secret:` for a single sensitive
|
|
13
|
+
value.
|
|
14
|
+
- `akm-migrate-storage` still ships and still performs the non-destructive
|
|
15
|
+
`vaults/` -> `env/` copy for older stashes. Run it before indexing if you are
|
|
16
|
+
upgrading from a stash that still stores `.env` files only under `vaults/`.
|
|
17
|
+
- Proposal workflow is fully consolidated around `akm improve`, `akm propose`,
|
|
18
|
+
and `akm proposal ...`. Update any old `akm reflect`, `akm distill`,
|
|
19
|
+
`akm accept`, `akm reject`, or `akm proposals` usage.
|
|
20
|
+
|
|
21
|
+
Primary public command family for 0.9.0:
|
|
22
|
+
|
|
23
|
+
- `akm improve <ref> [--task "..."]`
|
|
24
|
+
- `akm propose <type> <name> (--task "..." | --file <path>)`
|
|
25
|
+
- `akm proposal list`
|
|
26
|
+
- `akm proposal show <id>`
|
|
27
|
+
- `akm proposal diff <id>`
|
|
28
|
+
- `akm proposal accept <id>`
|
|
29
|
+
- `akm proposal reject <id> --reason "..."`
|
|
30
|
+
|
|
31
|
+
Recommended post-upgrade checks:
|
|
32
|
+
|
|
33
|
+
- Run `akm help migrate 0.9.0` on every machine where the CLI is installed.
|
|
34
|
+
- Run `akm-migrate-storage --yes` once if the stash ever used `vaults/`.
|
|
35
|
+
- Rebuild the index with `akm index`.
|
|
36
|
+
- Review agent instructions and docs for old `vault`, `reflect`, and `distill`
|
|
37
|
+
examples.
|
|
38
|
+
|
|
39
|
+
Full changelog: https://github.com/itlackey/akm/blob/main/CHANGELOG.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.52",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"docs/data-and-telemetry.md"
|
|
47
47
|
],
|
|
48
48
|
"bin": {
|
|
49
|
-
"akm": "dist/
|
|
50
|
-
"akm-migrate-storage": "dist/
|
|
49
|
+
"akm": "dist/akm",
|
|
50
|
+
"akm-migrate-storage": "dist/akm-migrate-storage"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
|
-
"preinstall": "node -e \"var ua=process.env.npm_config_user_agent||'';var
|
|
53
|
+
"preinstall": "node -e \"var ua=process.env.npm_config_user_agent||'';var v=(process.versions.node||'0').split('.').map(function(n){return parseInt(n,10)||0});var ok=v[0]>20||(v[0]===20&&(v[1]>12||(v[1]===12&&v[2]>=0)));if(process.versions.bun||ua.startsWith('bun/')||process.env.BUN_INSTALL||ok){process.exit(0)}console.error('\\n ERROR: akm-cli requires the Bun runtime (https://bun.sh), Node.js >= 20.12, or the prebuilt binary.\\n Install options:\\n 1. Bun: curl -fsSL https://bun.sh/install | bash && bun install -g akm-cli\\n 2. Node: upgrade to Node.js 20.12 or newer (https://nodejs.org)\\n 3. Binary: curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh | bash\\n');process.exit(1)\"",
|
|
54
54
|
"build": "rm -rf dist && bun scripts/gen-config-schema.ts &&bun run tsc --project ./tsconfig.build.json && bun scripts/copy-assets.ts && bun scripts/fix-esm-extensions.ts",
|
|
55
55
|
"check": "bun run lint && bunx tsc --noEmit && bun run test:unit && bun run test:integration",
|
|
56
56
|
"check:fast": "bun run lint && bunx tsc --noEmit && bun run test:unit",
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"sweep:tmp": "bun scripts/sweep-test-tmp.ts",
|
|
59
59
|
"test": "bash scripts/test-unit.sh",
|
|
60
60
|
"test:unit": "bash scripts/test-unit.sh",
|
|
61
|
-
"test:integration": "bun run sweep:tmp && bun test --
|
|
62
|
-
"test:unit:shard": "bun run sweep:tmp && bun test --
|
|
63
|
-
"test:integration:shard": "bun run sweep:tmp && bun test --
|
|
61
|
+
"test:integration": "bun run sweep:tmp && bun test --isolate --timeout=30000 ./tests/integration ./tests/commands ./tests/workflows",
|
|
62
|
+
"test:unit:shard": "bun run sweep:tmp && bun test --isolate --timeout=30000 ./tests --path-ignore-patterns=tests/integration --shard=${SHARD:?set SHARD=k/N}",
|
|
63
|
+
"test:integration:shard": "bun run sweep:tmp && bun test --isolate --timeout=30000 ./tests/integration ./tests/commands ./tests/workflows --shard=${SHARD:?set SHARD=k/N}",
|
|
64
64
|
"test:node-smoke": "bun scripts/node-smoke.ts",
|
|
65
65
|
"test:node-compat": "AKM_NODE_COMPAT_TESTS=1 bun test --timeout=120000 tests/integration/node-compat.test.ts",
|
|
66
66
|
"test:time": "bun scripts/test-timing-report.ts",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
},
|
|
96
96
|
"engines": {
|
|
97
97
|
"bun": ">=1.0.0",
|
|
98
|
-
"node": ">=20.
|
|
98
|
+
"node": ">=20.12.0"
|
|
99
99
|
},
|
|
100
100
|
"dependencies": {
|
|
101
101
|
"@clack/prompts": "^1.3.0",
|