akm-cli 0.4.1 → 0.5.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.
- package/CHANGELOG.md +232 -0
- package/dist/asset-registry.js +7 -0
- package/dist/asset-spec.js +35 -0
- package/dist/cli.js +1153 -32
- package/dist/completions.js +2 -2
- package/dist/config-cli.js +41 -0
- package/dist/config.js +62 -0
- package/dist/file-context.js +2 -1
- package/dist/github.js +20 -1
- package/dist/indexer.js +60 -6
- package/dist/init.js +11 -0
- package/dist/install-audit.js +53 -8
- package/dist/installed-kits.js +2 -0
- package/dist/llm.js +64 -23
- package/dist/local-search.js +3 -1
- package/dist/matchers.js +56 -4
- package/dist/metadata.js +102 -4
- package/dist/migration-help.js +110 -0
- package/dist/paths.js +3 -0
- package/dist/registry-install.js +36 -7
- package/dist/registry-resolve.js +25 -0
- package/dist/renderers.js +182 -2
- package/dist/search-fields.js +4 -0
- package/dist/search-source.js +12 -8
- package/dist/self-update.js +86 -10
- package/dist/setup.js +158 -33
- package/dist/stash-add.js +84 -11
- package/dist/stash-providers/git.js +182 -44
- package/dist/stash-show.js +56 -1
- package/dist/stash-source-manage.js +14 -4
- package/dist/templates/wiki-templates.js +100 -0
- package/dist/vault.js +290 -0
- package/dist/wiki.js +886 -0
- package/dist/workflow-authoring.js +131 -0
- package/dist/workflow-cli.js +44 -0
- package/dist/workflow-db.js +55 -0
- package/dist/workflow-markdown.js +251 -0
- package/dist/workflow-runs.js +364 -0
- package/package.json +3 -1
package/dist/completions.js
CHANGED
|
@@ -4,8 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import { getAssetTypes } from "./asset-spec";
|
|
5
5
|
// ── Known flag values ────────────────────────────────────────────────────────
|
|
6
6
|
const FLAG_VALUES = {
|
|
7
|
-
"--format": ["json", "text", "yaml"],
|
|
8
|
-
"--detail": ["brief", "normal", "full"],
|
|
7
|
+
"--format": ["json", "text", "yaml", "jsonl"],
|
|
8
|
+
"--detail": ["brief", "normal", "full", "summary"],
|
|
9
9
|
"--type": () => [...getAssetTypes(), "any"],
|
|
10
10
|
"--source": ["stash", "registry", "both"],
|
|
11
11
|
"--shell": ["bash"],
|
package/dist/config-cli.js
CHANGED
|
@@ -36,6 +36,8 @@ export function parseConfigValue(key, value) {
|
|
|
36
36
|
return { security: { installAudit: { registryAllowlist: parseStringArrayValue(value, key) } } };
|
|
37
37
|
case "security.installAudit.registryWhitelist":
|
|
38
38
|
return { security: { installAudit: { registryAllowlist: parseStringArrayValue(value, key) } } };
|
|
39
|
+
case "security.installAudit.allowedFindings":
|
|
40
|
+
return { security: { installAudit: { allowedFindings: parseAllowedFindingsValue(value, key) } } };
|
|
39
41
|
default:
|
|
40
42
|
throw new UsageError(`Unknown config key: ${key}`);
|
|
41
43
|
}
|
|
@@ -70,6 +72,8 @@ export function getConfigValue(config, key) {
|
|
|
70
72
|
return getInstallAuditAllowlist(config);
|
|
71
73
|
case "security.installAudit.registryWhitelist":
|
|
72
74
|
return getInstallAuditAllowlist(config);
|
|
75
|
+
case "security.installAudit.allowedFindings":
|
|
76
|
+
return config.security?.installAudit?.allowedFindings ?? null;
|
|
73
77
|
default:
|
|
74
78
|
throw new UsageError(`Unknown config key: ${key}`);
|
|
75
79
|
}
|
|
@@ -89,6 +93,7 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
89
93
|
case "security.installAudit.blockUnlistedRegistries":
|
|
90
94
|
case "security.installAudit.registryAllowlist":
|
|
91
95
|
case "security.installAudit.registryWhitelist":
|
|
96
|
+
case "security.installAudit.allowedFindings":
|
|
92
97
|
return mergeConfigValue(config, parseConfigValue(key, rawValue));
|
|
93
98
|
default:
|
|
94
99
|
throw new UsageError(`Unknown config key: ${key}`);
|
|
@@ -132,6 +137,13 @@ export function unsetConfigValue(config, key) {
|
|
|
132
137
|
installAudit: { registryAllowlist: undefined, registryWhitelist: undefined },
|
|
133
138
|
}),
|
|
134
139
|
};
|
|
140
|
+
case "security.installAudit.allowedFindings":
|
|
141
|
+
return {
|
|
142
|
+
...config,
|
|
143
|
+
security: mergeSecurityConfig(config.security, {
|
|
144
|
+
installAudit: { allowedFindings: undefined },
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
135
147
|
default:
|
|
136
148
|
throw new UsageError(`Unknown or unsupported unset key: ${key}`);
|
|
137
149
|
}
|
|
@@ -213,6 +225,35 @@ function parseStringArrayValue(value, key) {
|
|
|
213
225
|
function getInstallAuditAllowlist(config) {
|
|
214
226
|
return config.security?.installAudit?.registryAllowlist ?? config.security?.installAudit?.registryWhitelist ?? null;
|
|
215
227
|
}
|
|
228
|
+
function parseAllowedFindingsValue(value, key) {
|
|
229
|
+
let parsed;
|
|
230
|
+
try {
|
|
231
|
+
parsed = JSON.parse(value);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
throw new UsageError(`Invalid value for ${key}: expected a JSON array of {id, ref?, path?, reason?} objects`);
|
|
235
|
+
}
|
|
236
|
+
if (!Array.isArray(parsed)) {
|
|
237
|
+
throw new UsageError(`Invalid value for ${key}: expected a JSON array`);
|
|
238
|
+
}
|
|
239
|
+
return parsed.map((entry, i) => {
|
|
240
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
241
|
+
throw new UsageError(`Invalid value for ${key}[${i}]: expected an object with an "id" field`);
|
|
242
|
+
}
|
|
243
|
+
const obj = entry;
|
|
244
|
+
if (typeof obj.id !== "string" || !obj.id) {
|
|
245
|
+
throw new UsageError(`Invalid value for ${key}[${i}]: "id" is required`);
|
|
246
|
+
}
|
|
247
|
+
const result = { id: obj.id };
|
|
248
|
+
if (typeof obj.ref === "string" && obj.ref)
|
|
249
|
+
result.ref = obj.ref;
|
|
250
|
+
if (typeof obj.path === "string" && obj.path)
|
|
251
|
+
result.path = obj.path;
|
|
252
|
+
if (typeof obj.reason === "string" && obj.reason)
|
|
253
|
+
result.reason = obj.reason;
|
|
254
|
+
return result;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
216
257
|
function parseRegistriesValue(value) {
|
|
217
258
|
if (value === "null" || value === "")
|
|
218
259
|
return undefined;
|
package/dist/config.js
CHANGED
|
@@ -187,6 +187,9 @@ function pickKnownKeys(raw) {
|
|
|
187
187
|
const output = parseOutputConfig(raw.output);
|
|
188
188
|
if (output)
|
|
189
189
|
config.output = output;
|
|
190
|
+
if (typeof raw.writable === "boolean") {
|
|
191
|
+
config.writable = raw.writable;
|
|
192
|
+
}
|
|
190
193
|
return config;
|
|
191
194
|
}
|
|
192
195
|
function readNormalizedConfig(configPath) {
|
|
@@ -401,6 +404,24 @@ function parseLlmConfig(value) {
|
|
|
401
404
|
if (typeof obj.apiKey === "string" && obj.apiKey) {
|
|
402
405
|
result.apiKey = obj.apiKey;
|
|
403
406
|
}
|
|
407
|
+
if (typeof obj.contextWindow === "number" &&
|
|
408
|
+
Number.isFinite(obj.contextWindow) &&
|
|
409
|
+
Number.isInteger(obj.contextWindow) &&
|
|
410
|
+
obj.contextWindow > 0) {
|
|
411
|
+
result.contextWindow = obj.contextWindow;
|
|
412
|
+
}
|
|
413
|
+
if (typeof obj.capabilities === "object" && obj.capabilities !== null && !Array.isArray(obj.capabilities)) {
|
|
414
|
+
const capsRaw = obj.capabilities;
|
|
415
|
+
const caps = {};
|
|
416
|
+
if (typeof capsRaw.structuredOutput === "boolean")
|
|
417
|
+
caps.structuredOutput = capsRaw.structuredOutput;
|
|
418
|
+
if (typeof capsRaw.longContext === "boolean")
|
|
419
|
+
caps.longContext = capsRaw.longContext;
|
|
420
|
+
if (typeof capsRaw.toolUse === "boolean")
|
|
421
|
+
caps.toolUse = capsRaw.toolUse;
|
|
422
|
+
if (Object.keys(caps).length > 0)
|
|
423
|
+
result.capabilities = caps;
|
|
424
|
+
}
|
|
404
425
|
return result;
|
|
405
426
|
}
|
|
406
427
|
function parseInstalledEntries(value) {
|
|
@@ -433,12 +454,17 @@ function parseInstalledKitEntry(value) {
|
|
|
433
454
|
cacheDir,
|
|
434
455
|
installedAt,
|
|
435
456
|
};
|
|
457
|
+
if (typeof obj.writable === "boolean")
|
|
458
|
+
entry.writable = obj.writable;
|
|
436
459
|
const resolvedVersion = asNonEmptyString(obj.resolvedVersion);
|
|
437
460
|
if (resolvedVersion)
|
|
438
461
|
entry.resolvedVersion = resolvedVersion;
|
|
439
462
|
const resolvedRevision = asNonEmptyString(obj.resolvedRevision);
|
|
440
463
|
if (resolvedRevision)
|
|
441
464
|
entry.resolvedRevision = resolvedRevision;
|
|
465
|
+
const wikiName = asNonEmptyString(obj.wikiName);
|
|
466
|
+
if (wikiName)
|
|
467
|
+
entry.wikiName = wikiName;
|
|
442
468
|
return entry;
|
|
443
469
|
}
|
|
444
470
|
function asNonEmptyString(value) {
|
|
@@ -491,8 +517,39 @@ function parseInstallAuditConfig(value) {
|
|
|
491
517
|
if (rawAllowlist) {
|
|
492
518
|
config.registryAllowlist = rawAllowlist;
|
|
493
519
|
}
|
|
520
|
+
const allowedFindings = parseInstallAuditAllowedFindings(obj.allowedFindings);
|
|
521
|
+
if (allowedFindings) {
|
|
522
|
+
config.allowedFindings = allowedFindings;
|
|
523
|
+
}
|
|
494
524
|
return Object.keys(config).length > 0 ? config : undefined;
|
|
495
525
|
}
|
|
526
|
+
function parseInstallAuditAllowedFindings(value) {
|
|
527
|
+
if (!Array.isArray(value))
|
|
528
|
+
return undefined;
|
|
529
|
+
const findings = value
|
|
530
|
+
.map((entry) => parseInstallAuditAllowedFinding(entry))
|
|
531
|
+
.filter((entry) => entry !== undefined);
|
|
532
|
+
return findings.length > 0 ? findings : undefined;
|
|
533
|
+
}
|
|
534
|
+
function parseInstallAuditAllowedFinding(value) {
|
|
535
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
536
|
+
return undefined;
|
|
537
|
+
const obj = value;
|
|
538
|
+
const id = asNonEmptyString(obj.id);
|
|
539
|
+
if (!id)
|
|
540
|
+
return undefined;
|
|
541
|
+
const finding = { id };
|
|
542
|
+
const ref = asNonEmptyString(obj.ref);
|
|
543
|
+
if (ref)
|
|
544
|
+
finding.ref = ref;
|
|
545
|
+
const entryPath = asNonEmptyString(obj.path);
|
|
546
|
+
if (entryPath)
|
|
547
|
+
finding.path = entryPath;
|
|
548
|
+
const reason = asNonEmptyString(obj.reason);
|
|
549
|
+
if (reason)
|
|
550
|
+
finding.reason = reason;
|
|
551
|
+
return finding;
|
|
552
|
+
}
|
|
496
553
|
function parseStashConfigEntry(value) {
|
|
497
554
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
498
555
|
return undefined;
|
|
@@ -512,9 +569,14 @@ function parseStashConfigEntry(value) {
|
|
|
512
569
|
entry.name = name;
|
|
513
570
|
if (typeof obj.enabled === "boolean")
|
|
514
571
|
entry.enabled = obj.enabled;
|
|
572
|
+
if (typeof obj.writable === "boolean")
|
|
573
|
+
entry.writable = obj.writable;
|
|
515
574
|
if (typeof obj.options === "object" && obj.options !== null && !Array.isArray(obj.options)) {
|
|
516
575
|
entry.options = obj.options;
|
|
517
576
|
}
|
|
577
|
+
const wikiName = asNonEmptyString(obj.wikiName);
|
|
578
|
+
if (wikiName)
|
|
579
|
+
entry.wikiName = wikiName;
|
|
518
580
|
return entry;
|
|
519
581
|
}
|
|
520
582
|
function parseRegistryConfigEntry(value) {
|
package/dist/file-context.js
CHANGED
|
@@ -155,10 +155,11 @@ export async function runMatchers(ctx) {
|
|
|
155
155
|
* Build a RenderContext by merging a FileContext with its winning MatchResult
|
|
156
156
|
* and the list of stash search paths.
|
|
157
157
|
*/
|
|
158
|
-
export function buildRenderContext(ctx, match, stashDirs) {
|
|
158
|
+
export function buildRenderContext(ctx, match, stashDirs, origin) {
|
|
159
159
|
return {
|
|
160
160
|
...ctx,
|
|
161
161
|
matchResult: match,
|
|
162
162
|
stashDirs,
|
|
163
|
+
origin,
|
|
163
164
|
};
|
|
164
165
|
}
|
package/dist/github.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
|
+
import * as childProcess from "node:child_process";
|
|
1
2
|
export const GITHUB_API_BASE = "https://api.github.com";
|
|
2
3
|
const GITHUB_TOKEN_DOMAINS = new Set(["api.github.com", "github.com", "uploads.github.com"]);
|
|
4
|
+
function readGithubTokenFromEnv() {
|
|
5
|
+
const token = process.env.GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim();
|
|
6
|
+
return token || undefined;
|
|
7
|
+
}
|
|
8
|
+
function readGithubTokenFromGhCli() {
|
|
9
|
+
const result = childProcess.spawnSync("gh", ["auth", "token"], {
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
timeout: 5_000,
|
|
12
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
13
|
+
});
|
|
14
|
+
if (result.status !== 0)
|
|
15
|
+
return undefined;
|
|
16
|
+
const token = result.stdout.trim();
|
|
17
|
+
return token || undefined;
|
|
18
|
+
}
|
|
19
|
+
function resolveGithubToken() {
|
|
20
|
+
return readGithubTokenFromEnv() ?? readGithubTokenFromGhCli();
|
|
21
|
+
}
|
|
3
22
|
/**
|
|
4
23
|
* Build headers for GitHub API requests.
|
|
5
24
|
* When a `url` is provided, the Authorization header is only included if the
|
|
@@ -7,7 +26,7 @@ const GITHUB_TOKEN_DOMAINS = new Set(["api.github.com", "github.com", "uploads.g
|
|
|
7
26
|
* to third-party hosts.
|
|
8
27
|
*/
|
|
9
28
|
export function githubHeaders(url) {
|
|
10
|
-
const token =
|
|
29
|
+
const token = resolveGithubToken();
|
|
11
30
|
const headers = {
|
|
12
31
|
Accept: "application/vnd.github+json",
|
|
13
32
|
"User-Agent": "akm-registry",
|
package/dist/indexer.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { isHttpUrl, resolveStashDir } from "./common";
|
|
4
4
|
import { closeDatabase, deleteEntriesByDir, deleteEntriesByStashDir, getEmbeddingCount, getEntriesByDir, getEntryCount, getMeta, isVecAvailable, openDatabase, rebuildFts, setMeta, upsertEmbedding, upsertEntry, upsertUtilityScore, warnIfVecMissing, } from "./db";
|
|
5
|
-
import { generateMetadataFlat, loadStashFile } from "./metadata";
|
|
5
|
+
import { generateMetadataFlat, loadStashFile, shouldIndexStashFile } from "./metadata";
|
|
6
6
|
import { getDbPath } from "./paths";
|
|
7
7
|
import { buildSearchText } from "./search-fields";
|
|
8
8
|
import { classifySemanticFailure, clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "./semantic-status";
|
|
@@ -18,9 +18,10 @@ export async function akmIndex(options) {
|
|
|
18
18
|
const config = loadConfig();
|
|
19
19
|
// Ensure git stash caches are extracted before resolving stash dirs,
|
|
20
20
|
// so their content directories exist on disk for the walker to discover.
|
|
21
|
-
const { ensureStashCaches,
|
|
21
|
+
const { ensureStashCaches, resolveStashSources } = await import("./search-source.js");
|
|
22
22
|
await ensureStashCaches(config);
|
|
23
|
-
const
|
|
23
|
+
const allStashSources = resolveStashSources(stashDir, config);
|
|
24
|
+
const allStashDirs = allStashSources.map((s) => s.path);
|
|
24
25
|
const t0 = Date.now();
|
|
25
26
|
// Open database — pass embedding dimension from config if available
|
|
26
27
|
const dbPath = getDbPath();
|
|
@@ -79,7 +80,7 @@ export async function akmIndex(options) {
|
|
|
79
80
|
// doFullDelete=true merges the wipe into the same transaction as the
|
|
80
81
|
// inserts so readers never see an empty database mid-rebuild.
|
|
81
82
|
const doFullDelete = options?.full || !isIncremental;
|
|
82
|
-
const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm } = await indexEntries(db,
|
|
83
|
+
const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm } = await indexEntries(db, allStashSources, isIncremental, builtAtMs, doFullDelete);
|
|
83
84
|
onProgress({
|
|
84
85
|
phase: "scan",
|
|
85
86
|
message: `Scanned ${scannedDirs} ${scannedDirs === 1 ? "directory" : "directories"} and skipped ${skippedDirs}.`,
|
|
@@ -117,6 +118,18 @@ export async function akmIndex(options) {
|
|
|
117
118
|
}
|
|
118
119
|
// Recompute utility scores from usage_events after FTS rebuild
|
|
119
120
|
recomputeUtilityScores(db);
|
|
121
|
+
// Regenerate each wiki's index.md from its pages' frontmatter. Best-effort
|
|
122
|
+
// — errors are caught inside regenerateAllWikiIndexes and never block the
|
|
123
|
+
// index run. The primary stash is the only target: additional sources
|
|
124
|
+
// are read-only caches, and regenerating their indexes would mutate
|
|
125
|
+
// cache content.
|
|
126
|
+
try {
|
|
127
|
+
const { regenerateAllWikiIndexes } = await import("./wiki.js");
|
|
128
|
+
regenerateAllWikiIndexes(stashDir);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* best-effort */
|
|
132
|
+
}
|
|
120
133
|
// Generate embeddings if semantic search is enabled
|
|
121
134
|
const embeddingResult = await generateEmbeddingsForDb(db, config, onProgress);
|
|
122
135
|
const tEmbedEnd = Date.now();
|
|
@@ -168,15 +181,54 @@ export async function akmIndex(options) {
|
|
|
168
181
|
}
|
|
169
182
|
}
|
|
170
183
|
// ── Extracted helpers for indexing ────────────────────────────────────────────
|
|
171
|
-
async function indexEntries(db,
|
|
184
|
+
async function indexEntries(db, allStashSources, isIncremental, builtAtMs, doFullDelete = false) {
|
|
172
185
|
let scannedDirs = 0;
|
|
173
186
|
let skippedDirs = 0;
|
|
174
187
|
let generatedCount = 0;
|
|
175
188
|
const seenPaths = new Set();
|
|
176
189
|
const dirsNeedingLlm = [];
|
|
177
190
|
const dirRecords = [];
|
|
178
|
-
for (const
|
|
191
|
+
for (const stashSource of allStashSources) {
|
|
192
|
+
const currentStashDir = stashSource.path;
|
|
179
193
|
const fileContexts = walkStashFlat(currentStashDir);
|
|
194
|
+
// Wiki-root stashes: all .md files are indexed as wiki pages under wikiName
|
|
195
|
+
if (stashSource.wikiName) {
|
|
196
|
+
const wikiName = stashSource.wikiName;
|
|
197
|
+
const wikiDirGroups = new Map();
|
|
198
|
+
for (const ctx of fileContexts) {
|
|
199
|
+
if (ctx.ext !== ".md")
|
|
200
|
+
continue;
|
|
201
|
+
if (!shouldIndexStashFile(currentStashDir, ctx.absPath, { treatStashRootAsWikiRoot: true }))
|
|
202
|
+
continue;
|
|
203
|
+
const relNoExt = ctx.relPath.replace(/\.md$/, "");
|
|
204
|
+
const entry = {
|
|
205
|
+
name: `${wikiName}/${relNoExt}`,
|
|
206
|
+
type: "wiki",
|
|
207
|
+
filename: ctx.fileName,
|
|
208
|
+
description: ctx.frontmatter()?.description,
|
|
209
|
+
source: "frontmatter",
|
|
210
|
+
};
|
|
211
|
+
const dir = ctx.parentDirAbs;
|
|
212
|
+
const group = wikiDirGroups.get(dir);
|
|
213
|
+
if (group) {
|
|
214
|
+
group.files.push(ctx.absPath);
|
|
215
|
+
group.entries.push(entry);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
wikiDirGroups.set(dir, { files: [ctx.absPath], entries: [entry] });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const [dirPath, { files, entries }] of wikiDirGroups) {
|
|
222
|
+
if (seenPaths.has(path.resolve(dirPath))) {
|
|
223
|
+
dirRecords.push({ dirPath, currentStashDir, files, stash: null, skip: true });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
seenPaths.add(path.resolve(dirPath));
|
|
227
|
+
scannedDirs++;
|
|
228
|
+
dirRecords.push({ dirPath, currentStashDir, files, stash: { entries }, skip: false });
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
180
232
|
const dirGroups = new Map();
|
|
181
233
|
for (const ctx of fileContexts) {
|
|
182
234
|
const dir = ctx.parentDirAbs;
|
|
@@ -278,6 +330,8 @@ async function indexEntries(db, allStashDirs, isIncremental, builtAtMs, doFullDe
|
|
|
278
330
|
: matchEntryToFile(entry.name, fileBasenameMap, files);
|
|
279
331
|
if (!entryPath)
|
|
280
332
|
continue; // skip unresolvable entries
|
|
333
|
+
if (!shouldIndexStashFile(currentStashDir, entryPath))
|
|
334
|
+
continue;
|
|
281
335
|
// Skip if a higher-priority stash root already indexed this asset
|
|
282
336
|
const basename = path.basename(entryPath);
|
|
283
337
|
const identityKey = `${entry.type}\0${basename}\0${entry.description ?? ""}`;
|
package/dist/init.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Creates the working stash directory structure, persists the stashDir
|
|
5
5
|
* in config.json, and ensures ripgrep is available.
|
|
6
6
|
*/
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
7
8
|
import fs from "node:fs";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import { TYPE_DIRS } from "./asset-spec";
|
|
@@ -23,6 +24,8 @@ export async function akmInit(options) {
|
|
|
23
24
|
fs.mkdirSync(subDir, { recursive: true });
|
|
24
25
|
}
|
|
25
26
|
}
|
|
27
|
+
// Ensure the default stash is a local git repo (no remote required)
|
|
28
|
+
ensureGitRepo(stashDir);
|
|
26
29
|
// Persist stashDir in config.json
|
|
27
30
|
const configPath = getConfigPath();
|
|
28
31
|
const existing = loadUserConfig();
|
|
@@ -41,3 +44,11 @@ export async function akmInit(options) {
|
|
|
41
44
|
}
|
|
42
45
|
return { stashDir, created, configPath, ripgrep };
|
|
43
46
|
}
|
|
47
|
+
/** Initialise `dir` as a git repository if it is not already one. */
|
|
48
|
+
function ensureGitRepo(dir) {
|
|
49
|
+
const gitDir = path.join(dir, ".git");
|
|
50
|
+
if (fs.existsSync(gitDir))
|
|
51
|
+
return;
|
|
52
|
+
// Non-fatal: git may not be available in all environments
|
|
53
|
+
spawnSync("git", ["init", dir], { encoding: "utf8", timeout: 15_000 });
|
|
54
|
+
}
|
package/dist/install-audit.js
CHANGED
|
@@ -6,6 +6,7 @@ const DEFAULT_INSTALL_AUDIT_CONFIG = {
|
|
|
6
6
|
blockOnCritical: true,
|
|
7
7
|
blockUnlistedRegistries: false,
|
|
8
8
|
registryAllowlist: [],
|
|
9
|
+
allowedFindings: [],
|
|
9
10
|
};
|
|
10
11
|
const MAX_SCANNED_FILE_BYTES = 256 * 1024;
|
|
11
12
|
const LIFECYCLE_SCRIPT_NAMES = new Set([
|
|
@@ -36,6 +37,7 @@ const TEXT_FILE_EXTENSIONS = new Set([
|
|
|
36
37
|
".yaml",
|
|
37
38
|
".yml",
|
|
38
39
|
]);
|
|
40
|
+
const BLOCKED_PACKAGE_DIRECTORIES = new Set(["node_modules", "venv", ".venv", "site-packages"]);
|
|
39
41
|
const CONTENT_RULES = [
|
|
40
42
|
{
|
|
41
43
|
id: "prompt-ignore-previous-instructions",
|
|
@@ -49,7 +51,7 @@ const CONTENT_RULES = [
|
|
|
49
51
|
severity: "critical",
|
|
50
52
|
category: "prompt-injection",
|
|
51
53
|
message: "Contains instructions to reveal hidden prompts or secrets.",
|
|
52
|
-
pattern: /\b(reveal|print|dump|show|exfiltrat(?:e|ion))\b[^.\n]{0,
|
|
54
|
+
pattern: /\b(?:reveal|print|dump|show|output|return|exfiltrat(?:e|ion))\b[^.\n]{0,60}\b(?:your|the)\b[^.\n]{0,40}\b(system prompt|hidden instructions?|developer message|api key|token|secret|password)\b/i,
|
|
53
55
|
},
|
|
54
56
|
{
|
|
55
57
|
id: "prompt-bypass-guardrails",
|
|
@@ -97,6 +99,7 @@ export function resolveInstallAuditConfig(config) {
|
|
|
97
99
|
blockOnCritical: installAudit?.blockOnCritical ?? DEFAULT_INSTALL_AUDIT_CONFIG.blockOnCritical,
|
|
98
100
|
blockUnlistedRegistries: installAudit?.blockUnlistedRegistries ?? DEFAULT_INSTALL_AUDIT_CONFIG.blockUnlistedRegistries,
|
|
99
101
|
registryAllowlist: allowlist.map((entry) => entry.trim().toLowerCase()),
|
|
102
|
+
allowedFindings: installAudit?.allowedFindings ?? DEFAULT_INSTALL_AUDIT_CONFIG.allowedFindings,
|
|
100
103
|
};
|
|
101
104
|
}
|
|
102
105
|
export function enforceRegistryInstallPolicy(registryLabels, config, ref) {
|
|
@@ -118,6 +121,7 @@ export function auditInstallCandidate(input) {
|
|
|
118
121
|
enabled: false,
|
|
119
122
|
passed: true,
|
|
120
123
|
blocked: false,
|
|
124
|
+
trusted: false,
|
|
121
125
|
registryLabels: [...input.registryLabels],
|
|
122
126
|
findings: [],
|
|
123
127
|
scannedFiles: 0,
|
|
@@ -128,17 +132,20 @@ export function auditInstallCandidate(input) {
|
|
|
128
132
|
const findings = [];
|
|
129
133
|
const counters = { scannedFiles: 0, scannedBytes: 0 };
|
|
130
134
|
scanDirectory(input.rootDir, input.rootDir, findings, counters);
|
|
131
|
-
const
|
|
132
|
-
const
|
|
135
|
+
const { findings: activeFindings, waivedFindings } = splitAllowedFindings(findings, input.ref, resolved.allowedFindings);
|
|
136
|
+
const summary = buildSummary(activeFindings);
|
|
137
|
+
const blocked = !input.trustThisInstall && resolved.blockOnCritical && summary.critical > 0;
|
|
133
138
|
return {
|
|
134
139
|
enabled: true,
|
|
135
|
-
passed:
|
|
140
|
+
passed: activeFindings.length === 0,
|
|
136
141
|
blocked,
|
|
142
|
+
trusted: Boolean(input.trustThisInstall),
|
|
137
143
|
registryLabels: [...input.registryLabels],
|
|
138
|
-
findings,
|
|
144
|
+
findings: activeFindings,
|
|
139
145
|
scannedFiles: counters.scannedFiles,
|
|
140
146
|
scannedBytes: counters.scannedBytes,
|
|
141
147
|
summary,
|
|
148
|
+
...(waivedFindings.length > 0 ? { waivedFindings } : {}),
|
|
142
149
|
};
|
|
143
150
|
}
|
|
144
151
|
export function formatInstallAuditFailure(ref, report) {
|
|
@@ -149,7 +156,8 @@ export function formatInstallAuditFailure(ref, report) {
|
|
|
149
156
|
if (report.findings.length > 5) {
|
|
150
157
|
lines.push(`- ${report.findings.length - 5} more finding(s) omitted`);
|
|
151
158
|
}
|
|
152
|
-
lines.push("Disable blocking with `security.installAudit.blockOnCritical = false`, or disable audits with `security.installAudit.enabled = false`."
|
|
159
|
+
lines.push("Disable blocking with `security.installAudit.blockOnCritical = false`, or disable audits with `security.installAudit.enabled = false`." +
|
|
160
|
+
" Or pass --trust on a one-off 'akm add' to bypass this audit for this install only.");
|
|
153
161
|
return lines.join("\n");
|
|
154
162
|
}
|
|
155
163
|
export function formatInstallAuditSummary(report) {
|
|
@@ -165,7 +173,9 @@ export function formatInstallAuditSummary(report) {
|
|
|
165
173
|
if (report.summary.low > 0)
|
|
166
174
|
severitySummary.push(`${report.summary.low} low`);
|
|
167
175
|
const detail = severitySummary.length > 0 ? severitySummary.join(", ") : "no findings";
|
|
168
|
-
|
|
176
|
+
const status = report.blocked ? "blocked" : report.passed ? "passed" : report.trusted ? "trusted" : "warnings";
|
|
177
|
+
const waived = report.waivedFindings?.length ? `; waived ${report.waivedFindings.length}` : "";
|
|
178
|
+
return `Audit: ${status} (${detail}; scanned ${report.scannedFiles} file${report.scannedFiles === 1 ? "" : "s"}${waived})`;
|
|
169
179
|
}
|
|
170
180
|
export function deriveRegistryLabels(input) {
|
|
171
181
|
const labels = new Set();
|
|
@@ -190,10 +200,22 @@ function scanDirectory(dir, rootDir, findings, counters) {
|
|
|
190
200
|
return;
|
|
191
201
|
}
|
|
192
202
|
for (const entry of entries) {
|
|
193
|
-
if (entry.name === ".git"
|
|
203
|
+
if (entry.name === ".git")
|
|
194
204
|
continue;
|
|
195
205
|
const fullPath = path.join(dir, entry.name);
|
|
196
206
|
if (entry.isDirectory()) {
|
|
207
|
+
if (BLOCKED_PACKAGE_DIRECTORIES.has(entry.name)) {
|
|
208
|
+
const relativePath = path.relative(rootDir, fullPath) || entry.name;
|
|
209
|
+
findings.push({
|
|
210
|
+
id: "bundled-package-directory",
|
|
211
|
+
severity: "critical",
|
|
212
|
+
category: "vendored-dependency",
|
|
213
|
+
message: `Contains bundled dependency directory "${entry.name}".`,
|
|
214
|
+
file: relativePath,
|
|
215
|
+
snippet: relativePath,
|
|
216
|
+
});
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
197
219
|
scanDirectory(fullPath, rootDir, findings, counters);
|
|
198
220
|
continue;
|
|
199
221
|
}
|
|
@@ -311,6 +333,29 @@ function buildSummary(findings) {
|
|
|
311
333
|
}
|
|
312
334
|
return summary;
|
|
313
335
|
}
|
|
336
|
+
function splitAllowedFindings(findings, ref, allowedFindings) {
|
|
337
|
+
const active = [];
|
|
338
|
+
const waived = [];
|
|
339
|
+
for (const finding of findings) {
|
|
340
|
+
if (matchesAllowedFinding(finding, ref, allowedFindings)) {
|
|
341
|
+
waived.push(finding);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
active.push(finding);
|
|
345
|
+
}
|
|
346
|
+
return { findings: active, waivedFindings: waived };
|
|
347
|
+
}
|
|
348
|
+
function matchesAllowedFinding(finding, ref, allowedFindings) {
|
|
349
|
+
return allowedFindings.some((allowed) => {
|
|
350
|
+
if (allowed.id !== finding.id)
|
|
351
|
+
return false;
|
|
352
|
+
if (allowed.ref && allowed.ref !== ref)
|
|
353
|
+
return false;
|
|
354
|
+
if (allowed.path && allowed.path !== finding.file)
|
|
355
|
+
return false;
|
|
356
|
+
return true;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
314
359
|
function addUrlLabels(labels, rawUrl) {
|
|
315
360
|
if (!rawUrl)
|
|
316
361
|
return;
|
package/dist/installed-kits.js
CHANGED
|
@@ -32,6 +32,7 @@ export async function akmListSources(input) {
|
|
|
32
32
|
path: stash.path,
|
|
33
33
|
provider: isRemote ? stash.type : undefined,
|
|
34
34
|
updatable: false,
|
|
35
|
+
writable: stash.writable === true,
|
|
35
36
|
status: { exists: stash.path ? directoryExists(stash.path) : true },
|
|
36
37
|
});
|
|
37
38
|
}
|
|
@@ -47,6 +48,7 @@ export async function akmListSources(input) {
|
|
|
47
48
|
ref: entry.ref,
|
|
48
49
|
version: entry.resolvedVersion,
|
|
49
50
|
updatable: true,
|
|
51
|
+
writable: entry.writable === true,
|
|
50
52
|
status: { exists: directoryExists(entry.stashRoot) },
|
|
51
53
|
});
|
|
52
54
|
}
|
package/dist/llm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchWithTimeout } from "./common";
|
|
2
|
-
async function chatCompletion(config, messages) {
|
|
2
|
+
export async function chatCompletion(config, messages, options) {
|
|
3
3
|
const headers = { "Content-Type": "application/json" };
|
|
4
4
|
if (config.apiKey) {
|
|
5
5
|
headers.Authorization = `Bearer ${config.apiKey}`;
|
|
@@ -10,8 +10,8 @@ async function chatCompletion(config, messages) {
|
|
|
10
10
|
body: JSON.stringify({
|
|
11
11
|
model: config.model,
|
|
12
12
|
messages,
|
|
13
|
-
temperature: config.temperature ?? 0.3,
|
|
14
|
-
max_tokens: config.maxTokens ?? 512,
|
|
13
|
+
temperature: options?.temperature ?? config.temperature ?? 0.3,
|
|
14
|
+
max_tokens: options?.maxTokens ?? config.maxTokens ?? 512,
|
|
15
15
|
}),
|
|
16
16
|
});
|
|
17
17
|
if (!response.ok) {
|
|
@@ -21,6 +21,23 @@ async function chatCompletion(config, messages) {
|
|
|
21
21
|
const json = (await response.json());
|
|
22
22
|
return json.choices?.[0]?.message?.content?.trim() ?? "";
|
|
23
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
|
+
}
|
|
24
41
|
// ── Metadata Enhancement ────────────────────────────────────────────────────
|
|
25
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.`;
|
|
26
43
|
/**
|
|
@@ -50,28 +67,22 @@ Return ONLY the JSON object, no explanation.`;
|
|
|
50
67
|
{ role: "system", content: SYSTEM_PROMPT },
|
|
51
68
|
{ role: "user", content: userPrompt },
|
|
52
69
|
]);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const cleaned = raw.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "");
|
|
56
|
-
const parsed = JSON.parse(cleaned);
|
|
57
|
-
const result = {};
|
|
58
|
-
if (typeof parsed.description === "string" && parsed.description) {
|
|
59
|
-
result.description = parsed.description;
|
|
60
|
-
}
|
|
61
|
-
if (Array.isArray(parsed.searchHints)) {
|
|
62
|
-
result.searchHints = parsed.searchHints
|
|
63
|
-
.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
64
|
-
.slice(0, 8);
|
|
65
|
-
}
|
|
66
|
-
if (Array.isArray(parsed.tags)) {
|
|
67
|
-
result.tags = parsed.tags.filter((s) => typeof s === "string" && s.trim().length > 0).slice(0, 10);
|
|
68
|
-
}
|
|
69
|
-
return result;
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
// LLM returned unparseable output, return empty
|
|
70
|
+
const parsed = parseJsonResponse(raw);
|
|
71
|
+
if (!parsed)
|
|
73
72
|
return {};
|
|
73
|
+
const result = {};
|
|
74
|
+
if (typeof parsed.description === "string" && parsed.description) {
|
|
75
|
+
result.description = parsed.description;
|
|
74
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;
|
|
75
86
|
}
|
|
76
87
|
/**
|
|
77
88
|
* Check if the LLM endpoint is reachable.
|
|
@@ -85,3 +96,33 @@ export async function isLlmAvailable(config) {
|
|
|
85
96
|
return false;
|
|
86
97
|
}
|
|
87
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
|
+
}
|