akm-cli 0.7.5 → 0.8.0-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CHANGELOG.md +1 -1
- package/dist/cli/parse-args.js +43 -0
- package/dist/cli.js +804 -461
- package/dist/commands/agent-dispatch.js +102 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +823 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +244 -52
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +2 -23
- package/dist/commands/graph.js +222 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +53 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +3 -30
- package/dist/commands/improve.js +1170 -0
- package/dist/commands/info.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/install-audit.js +5 -1
- package/dist/commands/installed-stashes.js +118 -138
- package/dist/commands/knowledge.js +133 -0
- package/dist/commands/lint/agent-linter.js +46 -0
- package/dist/commands/lint/base-linter.js +251 -0
- package/dist/commands/lint/command-linter.js +46 -0
- package/dist/commands/lint/default-linter.js +13 -0
- package/dist/commands/lint/index.js +107 -0
- package/dist/commands/lint/knowledge-linter.js +13 -0
- package/dist/commands/lint/memory-linter.js +58 -0
- package/dist/commands/lint/registry.js +33 -0
- package/dist/commands/lint/skill-linter.js +42 -0
- package/dist/commands/lint/task-linter.js +47 -0
- package/dist/commands/lint/types.js +1 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +78 -28
- package/dist/commands/reflect.js +143 -35
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +54 -0
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +121 -17
- package/dist/commands/source-add.js +10 -10
- package/dist/commands/source-manage.js +11 -19
- package/dist/commands/tasks.js +385 -0
- package/dist/commands/url-checker.js +39 -0
- package/dist/commands/vault.js +2 -23
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-registry.js +4 -16
- package/dist/core/asset-spec.js +10 -0
- package/dist/core/common.js +94 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +222 -128
- package/dist/core/events.js +73 -126
- package/dist/core/frontmatter.js +3 -1
- package/dist/core/markdown.js +17 -0
- package/dist/core/memory-improve.js +678 -0
- package/dist/core/parse.js +155 -0
- package/dist/core/paths.js +101 -3
- package/dist/core/proposal-validators.js +61 -0
- package/dist/core/proposals.js +49 -38
- package/dist/core/state-db.js +775 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +52 -238
- package/dist/indexer/db.js +377 -1
- package/dist/indexer/ensure-index.js +61 -0
- package/dist/indexer/graph-boost.js +247 -94
- package/dist/indexer/graph-db.js +201 -0
- package/dist/indexer/graph-dedup.js +99 -0
- package/dist/indexer/graph-extraction.js +409 -76
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +442 -290
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/match-contributors.js +141 -0
- package/dist/indexer/matchers.js +24 -190
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +188 -175
- package/dist/indexer/path-resolver.js +89 -0
- package/dist/indexer/ranking-contributors.js +204 -0
- package/dist/indexer/ranking.js +74 -0
- package/dist/indexer/search-hit-enrichers.js +22 -0
- package/dist/indexer/search-source.js +24 -9
- package/dist/indexer/semantic-status.js +2 -16
- package/dist/indexer/walker.js +25 -0
- package/dist/integrations/agent/config.js +175 -3
- package/dist/integrations/agent/index.js +3 -1
- package/dist/integrations/agent/pipeline.js +39 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +77 -72
- package/dist/integrations/agent/runners.js +31 -0
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +71 -16
- package/dist/integrations/lockfile.js +10 -18
- package/dist/integrations/session-logs/index.js +65 -0
- package/dist/integrations/session-logs/providers/claude-code.js +56 -0
- package/dist/integrations/session-logs/providers/opencode.js +52 -0
- package/dist/integrations/session-logs/types.js +1 -0
- package/dist/llm/call-ai.js +74 -0
- package/dist/llm/client.js +61 -122
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -62
- package/dist/llm/memory-infer.js +49 -71
- package/dist/llm/metadata-enhance.js +39 -22
- package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
- package/dist/output/cli-hints-full.md +277 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +2 -318
- package/dist/output/renderers.js +190 -123
- package/dist/output/shapes.js +33 -0
- package/dist/output/text.js +239 -2
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/website-ingest.js +4 -0
- package/dist/tasks/backends/cron.js +200 -0
- package/dist/tasks/backends/exec-utils.js +25 -0
- package/dist/tasks/backends/index.js +32 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +184 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +212 -0
- package/dist/tasks/parser.js +198 -0
- package/dist/tasks/resolveAkmBin.js +84 -0
- package/dist/tasks/runner.js +432 -0
- package/dist/tasks/schedule.js +208 -0
- package/dist/tasks/schema.js +13 -0
- package/dist/tasks/validator.js +59 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +12 -0
- package/dist/wiki/wiki.js +10 -61
- package/dist/workflows/authoring.js +5 -25
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +59 -91
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +3 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +3 -2
- package/dist/templates/wiki-templates.js +0 -100
package/dist/commands/info.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { getAssetTypes } from "../core/asset-spec";
|
|
3
|
-
import { loadConfig } from "../core/config";
|
|
3
|
+
import { getSources, loadConfig } from "../core/config";
|
|
4
4
|
import { getDbPath } from "../core/paths";
|
|
5
5
|
import { closeDatabase, getEntryCount, getMeta, isVecAvailable, openExistingDatabase } from "../indexer/db";
|
|
6
6
|
import { getEffectiveSemanticStatus, readSemanticStatus } from "../indexer/semantic-status";
|
|
@@ -32,7 +32,7 @@ export function assembleInfo(options) {
|
|
|
32
32
|
// Stash providers — prefer `sources[]`; fall back to `stashDir` when the
|
|
33
33
|
// user has not yet migrated to the sources[] config shape so that info
|
|
34
34
|
// always reflects at least one provider when a stash is configured.
|
|
35
|
-
const configuredSources = config
|
|
35
|
+
const configuredSources = getSources(config);
|
|
36
36
|
const stashesList = configuredSources.length === 0 && config.stashDir
|
|
37
37
|
? [{ type: "filesystem", path: config.stashDir, name: "primary" }]
|
|
38
38
|
: configuredSources;
|
package/dist/commands/init.js
CHANGED
|
@@ -8,8 +8,8 @@ import { spawnSync } from "node:child_process";
|
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { TYPE_DIRS } from "../core/asset-spec";
|
|
11
|
-
import {
|
|
12
|
-
import { getBinDir, getDefaultStashDir } from "../core/paths";
|
|
11
|
+
import { loadUserConfig, saveConfig } from "../core/config";
|
|
12
|
+
import { getBinDir, getConfigPath, getDefaultStashDir } from "../core/paths";
|
|
13
13
|
import { ensureRg } from "../setup/ripgrep-install";
|
|
14
14
|
export async function akmInit(options) {
|
|
15
15
|
const stashDir = options?.dir ? path.resolve(options.dir) : getDefaultStashDir();
|
|
@@ -149,6 +149,9 @@ export function auditInstallCandidate(input) {
|
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
export function formatInstallAuditFailure(ref, report) {
|
|
152
|
+
return formatInstallAuditFailureForAction(ref, report, "add");
|
|
153
|
+
}
|
|
154
|
+
export function formatInstallAuditFailureForAction(ref, report, action) {
|
|
152
155
|
const lines = [`Security audit failed for ${ref}.`, formatInstallAuditSummary(report)];
|
|
153
156
|
for (const finding of report.findings.slice(0, 5)) {
|
|
154
157
|
lines.push(`- [${finding.severity}] ${finding.message}${finding.file ? ` (${finding.file})` : ""}`);
|
|
@@ -156,8 +159,9 @@ export function formatInstallAuditFailure(ref, report) {
|
|
|
156
159
|
if (report.findings.length > 5) {
|
|
157
160
|
lines.push(`- ${report.findings.length - 5} more finding(s) omitted`);
|
|
158
161
|
}
|
|
162
|
+
const trustCommand = action === "update" ? `akm update ${ref} --trust` : `akm add ${ref} --trust`;
|
|
159
163
|
lines.push("Disable blocking with `security.installAudit.blockOnCritical = false`, or disable audits with `security.installAudit.enabled = false`." +
|
|
160
|
-
|
|
164
|
+
` Or pass --trust on a one-off '${trustCommand}' to bypass this audit for this ${action} only.`);
|
|
161
165
|
return lines.join("\n");
|
|
162
166
|
}
|
|
163
167
|
export function formatInstallAuditSummary(report) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { isWithin, resolveStashDir } from "../core/common";
|
|
10
|
-
import { loadConfig } from "../core/config";
|
|
10
|
+
import { getSources, loadConfig } from "../core/config";
|
|
11
11
|
import { NotFoundError, UsageError } from "../core/errors";
|
|
12
12
|
import { akmIndex } from "../indexer/indexer";
|
|
13
13
|
import { removeLockEntry, upsertLockEntry } from "../integrations/lockfile";
|
|
@@ -16,7 +16,7 @@ import { parseGitRepoUrl, syncMirroredRepo } from "../sources/providers/git";
|
|
|
16
16
|
import { syncFromRef } from "../sources/providers/sync-from-ref";
|
|
17
17
|
import { ensureWebsiteMirror } from "../sources/website-ingest";
|
|
18
18
|
import { listWikis, resolveWikisRoot } from "../wiki/wiki";
|
|
19
|
-
import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy,
|
|
19
|
+
import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailureForAction, } from "./install-audit";
|
|
20
20
|
import { removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./source-add";
|
|
21
21
|
import { removeStash } from "./source-manage";
|
|
22
22
|
export async function akmListSources(input) {
|
|
@@ -26,7 +26,7 @@ export async function akmListSources(input) {
|
|
|
26
26
|
const sources = [];
|
|
27
27
|
// Stash entries — each entry exposes its provider type as kind (spec §2.1).
|
|
28
28
|
// Writable defaults: true for filesystem, false for git/npm/website (CLAUDE.md "Writes").
|
|
29
|
-
for (const stash of config
|
|
29
|
+
for (const stash of getSources(config)) {
|
|
30
30
|
const kind = stash.type ?? "filesystem";
|
|
31
31
|
if (kindFilter && !kindFilter.includes(kind))
|
|
32
32
|
continue;
|
|
@@ -119,7 +119,7 @@ export async function akmRemove(input) {
|
|
|
119
119
|
stashRoot: entry.stashRoot,
|
|
120
120
|
},
|
|
121
121
|
config: {
|
|
122
|
-
sourceCount: (updatedConfig
|
|
122
|
+
sourceCount: getSources(updatedConfig).length,
|
|
123
123
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
124
124
|
},
|
|
125
125
|
index: {
|
|
@@ -150,7 +150,7 @@ export async function akmRemove(input) {
|
|
|
150
150
|
stashRoot: removedEntry.path ?? "",
|
|
151
151
|
},
|
|
152
152
|
config: {
|
|
153
|
-
sourceCount: (updatedConfig
|
|
153
|
+
sourceCount: getSources(updatedConfig).length,
|
|
154
154
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
155
155
|
},
|
|
156
156
|
index: {
|
|
@@ -161,6 +161,109 @@ export async function akmRemove(input) {
|
|
|
161
161
|
},
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
|
+
// ── akmUpdate helpers ────────────────────────────────────────────────────────
|
|
165
|
+
/** Build a standard UpdateResponse summary block from the current config and index run. */
|
|
166
|
+
async function buildUpdateResponse(stashDir, target, all, processed, full = false) {
|
|
167
|
+
const index = await akmIndex({ stashDir, ...(full ? { full: true } : {}) });
|
|
168
|
+
const finalConfig = loadConfig();
|
|
169
|
+
return {
|
|
170
|
+
schemaVersion: 1,
|
|
171
|
+
stashDir,
|
|
172
|
+
target,
|
|
173
|
+
all,
|
|
174
|
+
processed,
|
|
175
|
+
config: {
|
|
176
|
+
sourceCount: getSources(finalConfig).length,
|
|
177
|
+
installedKitCount: finalConfig.installed?.length ?? 0,
|
|
178
|
+
},
|
|
179
|
+
index: {
|
|
180
|
+
mode: index.mode,
|
|
181
|
+
totalEntries: index.totalEntries,
|
|
182
|
+
directoriesScanned: index.directoriesScanned,
|
|
183
|
+
directoriesSkipped: index.directoriesSkipped,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** Sync a git-mirrored source and return an UpdateResponse. */
|
|
188
|
+
async function updateGitSource(stashDir, target, all, gitSource) {
|
|
189
|
+
await syncMirroredRepo(gitSource, { force: true, writable: gitSource.writable === true });
|
|
190
|
+
return buildUpdateResponse(stashDir, target, all, [], true);
|
|
191
|
+
}
|
|
192
|
+
/** Re-crawl a website source and return an UpdateResponse. */
|
|
193
|
+
async function updateWebsiteSource(stashDir, target, all, websiteSource) {
|
|
194
|
+
// TODO: full incremental re-crawl with delta tracking (#19)
|
|
195
|
+
await ensureWebsiteMirror(websiteSource, { requireStashDir: true, force: true });
|
|
196
|
+
return buildUpdateResponse(stashDir, target, all, []);
|
|
197
|
+
}
|
|
198
|
+
/** Sync a single installed registry entry and return the processed record. */
|
|
199
|
+
async function updateRegistryEntry(entry, force, auditConfig) {
|
|
200
|
+
if (force && shouldCleanupCache(entry)) {
|
|
201
|
+
cleanupDirectoryBestEffort(entry.cacheDir);
|
|
202
|
+
}
|
|
203
|
+
const synced = await syncFromRef(entry.ref, { force });
|
|
204
|
+
// Mirror the post-sync audit hook from akmAdd so `akm update` can't
|
|
205
|
+
// silently land malicious content during refresh.
|
|
206
|
+
const registryLabels = deriveRegistryLabels({
|
|
207
|
+
source: synced.source,
|
|
208
|
+
ref: synced.ref,
|
|
209
|
+
artifactUrl: synced.artifactUrl,
|
|
210
|
+
});
|
|
211
|
+
enforceRegistryInstallPolicy(registryLabels, auditConfig, entry.ref);
|
|
212
|
+
const audit = auditInstallCandidate({
|
|
213
|
+
rootDir: synced.extractedDir,
|
|
214
|
+
source: synced.source,
|
|
215
|
+
ref: synced.ref,
|
|
216
|
+
registryLabels,
|
|
217
|
+
config: auditConfig,
|
|
218
|
+
});
|
|
219
|
+
if (audit.blocked) {
|
|
220
|
+
throw new UsageError(formatInstallAuditFailureForAction(synced.ref, audit, "update"), "INVALID_FLAG_VALUE", `Re-run with \`akm update ${synced.ref} --trust\` only if you intentionally trust this source.`);
|
|
221
|
+
}
|
|
222
|
+
const installedEntry = {
|
|
223
|
+
id: synced.id,
|
|
224
|
+
source: synced.source,
|
|
225
|
+
ref: synced.ref,
|
|
226
|
+
artifactUrl: synced.artifactUrl,
|
|
227
|
+
resolvedVersion: synced.resolvedVersion,
|
|
228
|
+
resolvedRevision: synced.resolvedRevision,
|
|
229
|
+
stashRoot: synced.contentDir,
|
|
230
|
+
cacheDir: synced.cacheDir,
|
|
231
|
+
installedAt: synced.syncedAt,
|
|
232
|
+
writable: synced.writable ?? entry.writable,
|
|
233
|
+
...(entry.wikiName ? { wikiName: entry.wikiName } : {}),
|
|
234
|
+
};
|
|
235
|
+
upsertInstalledRegistryEntry(installedEntry);
|
|
236
|
+
await upsertLockEntry({
|
|
237
|
+
id: synced.id,
|
|
238
|
+
source: synced.source,
|
|
239
|
+
ref: synced.ref,
|
|
240
|
+
resolvedVersion: synced.resolvedVersion,
|
|
241
|
+
resolvedRevision: synced.resolvedRevision,
|
|
242
|
+
integrity: synced.integrity ?? (synced.source === "local" ? "local" : undefined),
|
|
243
|
+
});
|
|
244
|
+
if (entry.cacheDir !== synced.cacheDir && shouldCleanupCache(entry)) {
|
|
245
|
+
cleanupDirectoryBestEffort(entry.cacheDir);
|
|
246
|
+
}
|
|
247
|
+
const versionChanged = (entry.resolvedVersion ?? "") !== (synced.resolvedVersion ?? "");
|
|
248
|
+
const revisionChanged = (entry.resolvedRevision ?? "") !== (synced.resolvedRevision ?? "");
|
|
249
|
+
return {
|
|
250
|
+
id: entry.id,
|
|
251
|
+
source: entry.source,
|
|
252
|
+
ref: entry.ref,
|
|
253
|
+
previous: {
|
|
254
|
+
resolvedVersion: entry.resolvedVersion,
|
|
255
|
+
resolvedRevision: entry.resolvedRevision,
|
|
256
|
+
cacheDir: entry.cacheDir,
|
|
257
|
+
},
|
|
258
|
+
installed: { ...installedEntry, extractedDir: synced.extractedDir, audit },
|
|
259
|
+
changed: {
|
|
260
|
+
version: versionChanged,
|
|
261
|
+
revision: revisionChanged,
|
|
262
|
+
any: versionChanged || revisionChanged,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// ── akmUpdate dispatcher ─────────────────────────────────────────────────────
|
|
164
267
|
export async function akmUpdate(input) {
|
|
165
268
|
const stashDir = input?.stashDir ?? resolveStashDir();
|
|
166
269
|
const target = input?.target?.trim();
|
|
@@ -168,10 +271,10 @@ export async function akmUpdate(input) {
|
|
|
168
271
|
const force = input?.force === true;
|
|
169
272
|
const config = loadConfig();
|
|
170
273
|
const installedEntries = config.installed ?? [];
|
|
171
|
-
// Check if the target refers to a website source — those are
|
|
172
|
-
//
|
|
274
|
+
// Check if the target refers to a git or website source — those are stored
|
|
275
|
+
// in sources[] not installed[] and need a different update path.
|
|
173
276
|
if (target && !all) {
|
|
174
|
-
const stashes = config
|
|
277
|
+
const stashes = getSources(config);
|
|
175
278
|
const isUrl = target.startsWith("http://") || target.startsWith("https://");
|
|
176
279
|
const resolvedPath = !isUrl ? path.resolve(target) : undefined;
|
|
177
280
|
const gitMatch = stashes.find((s) => {
|
|
@@ -195,28 +298,8 @@ export async function akmUpdate(input) {
|
|
|
195
298
|
}
|
|
196
299
|
return false;
|
|
197
300
|
});
|
|
198
|
-
if (gitMatch)
|
|
199
|
-
|
|
200
|
-
const index = await akmIndex({ stashDir, full: true });
|
|
201
|
-
const updatedConfig = loadConfig();
|
|
202
|
-
return {
|
|
203
|
-
schemaVersion: 1,
|
|
204
|
-
stashDir,
|
|
205
|
-
target,
|
|
206
|
-
all,
|
|
207
|
-
processed: [],
|
|
208
|
-
config: {
|
|
209
|
-
sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
|
|
210
|
-
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
211
|
-
},
|
|
212
|
-
index: {
|
|
213
|
-
mode: index.mode,
|
|
214
|
-
totalEntries: index.totalEntries,
|
|
215
|
-
directoriesScanned: index.directoriesScanned,
|
|
216
|
-
directoriesSkipped: index.directoriesSkipped,
|
|
217
|
-
},
|
|
218
|
-
};
|
|
219
|
-
}
|
|
301
|
+
if (gitMatch)
|
|
302
|
+
return updateGitSource(stashDir, target, all, gitMatch);
|
|
220
303
|
const websiteMatch = stashes.find((s) => {
|
|
221
304
|
if (s.type !== "website")
|
|
222
305
|
return false;
|
|
@@ -228,119 +311,16 @@ export async function akmUpdate(input) {
|
|
|
228
311
|
return true;
|
|
229
312
|
return false;
|
|
230
313
|
});
|
|
231
|
-
if (websiteMatch)
|
|
232
|
-
|
|
233
|
-
await ensureWebsiteMirror(websiteMatch, { requireStashDir: true, force: true });
|
|
234
|
-
const index = await akmIndex({ stashDir });
|
|
235
|
-
const updatedConfig = loadConfig();
|
|
236
|
-
return {
|
|
237
|
-
schemaVersion: 1,
|
|
238
|
-
stashDir,
|
|
239
|
-
target,
|
|
240
|
-
all,
|
|
241
|
-
processed: [],
|
|
242
|
-
config: {
|
|
243
|
-
sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
|
|
244
|
-
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
245
|
-
},
|
|
246
|
-
index: {
|
|
247
|
-
mode: index.mode,
|
|
248
|
-
totalEntries: index.totalEntries,
|
|
249
|
-
directoriesScanned: index.directoriesScanned,
|
|
250
|
-
directoriesSkipped: index.directoriesSkipped,
|
|
251
|
-
},
|
|
252
|
-
};
|
|
253
|
-
}
|
|
314
|
+
if (websiteMatch)
|
|
315
|
+
return updateWebsiteSource(stashDir, target, all, websiteMatch);
|
|
254
316
|
}
|
|
255
317
|
const selectedEntries = selectTargets(installedEntries, target, all);
|
|
256
318
|
const auditConfig = config;
|
|
257
319
|
const processed = [];
|
|
258
320
|
for (const entry of selectedEntries) {
|
|
259
|
-
|
|
260
|
-
cleanupDirectoryBestEffort(entry.cacheDir);
|
|
261
|
-
}
|
|
262
|
-
const synced = await syncFromRef(entry.ref, { force });
|
|
263
|
-
// Mirror the post-sync audit hook from akmAdd so `akm update` can't
|
|
264
|
-
// silently land malicious content during refresh.
|
|
265
|
-
const registryLabels = deriveRegistryLabels({
|
|
266
|
-
source: synced.source,
|
|
267
|
-
ref: synced.ref,
|
|
268
|
-
artifactUrl: synced.artifactUrl,
|
|
269
|
-
});
|
|
270
|
-
enforceRegistryInstallPolicy(registryLabels, auditConfig, entry.ref);
|
|
271
|
-
const audit = auditInstallCandidate({
|
|
272
|
-
rootDir: synced.extractedDir,
|
|
273
|
-
source: synced.source,
|
|
274
|
-
ref: synced.ref,
|
|
275
|
-
registryLabels,
|
|
276
|
-
config: auditConfig,
|
|
277
|
-
});
|
|
278
|
-
if (audit.blocked) {
|
|
279
|
-
throw new Error(formatInstallAuditFailure(synced.ref, audit));
|
|
280
|
-
}
|
|
281
|
-
const installedEntry = {
|
|
282
|
-
id: synced.id,
|
|
283
|
-
source: synced.source,
|
|
284
|
-
ref: synced.ref,
|
|
285
|
-
artifactUrl: synced.artifactUrl,
|
|
286
|
-
resolvedVersion: synced.resolvedVersion,
|
|
287
|
-
resolvedRevision: synced.resolvedRevision,
|
|
288
|
-
stashRoot: synced.contentDir,
|
|
289
|
-
cacheDir: synced.cacheDir,
|
|
290
|
-
installedAt: synced.syncedAt,
|
|
291
|
-
writable: synced.writable ?? entry.writable,
|
|
292
|
-
...(entry.wikiName ? { wikiName: entry.wikiName } : {}),
|
|
293
|
-
};
|
|
294
|
-
upsertInstalledRegistryEntry(installedEntry);
|
|
295
|
-
await upsertLockEntry({
|
|
296
|
-
id: synced.id,
|
|
297
|
-
source: synced.source,
|
|
298
|
-
ref: synced.ref,
|
|
299
|
-
resolvedVersion: synced.resolvedVersion,
|
|
300
|
-
resolvedRevision: synced.resolvedRevision,
|
|
301
|
-
integrity: synced.integrity ?? (synced.source === "local" ? "local" : undefined),
|
|
302
|
-
});
|
|
303
|
-
if (entry.cacheDir !== synced.cacheDir && shouldCleanupCache(entry)) {
|
|
304
|
-
cleanupDirectoryBestEffort(entry.cacheDir);
|
|
305
|
-
}
|
|
306
|
-
const versionChanged = (entry.resolvedVersion ?? "") !== (synced.resolvedVersion ?? "");
|
|
307
|
-
const revisionChanged = (entry.resolvedRevision ?? "") !== (synced.resolvedRevision ?? "");
|
|
308
|
-
processed.push({
|
|
309
|
-
id: entry.id,
|
|
310
|
-
source: entry.source,
|
|
311
|
-
ref: entry.ref,
|
|
312
|
-
previous: {
|
|
313
|
-
resolvedVersion: entry.resolvedVersion,
|
|
314
|
-
resolvedRevision: entry.resolvedRevision,
|
|
315
|
-
cacheDir: entry.cacheDir,
|
|
316
|
-
},
|
|
317
|
-
installed: { ...installedEntry, extractedDir: synced.extractedDir, audit },
|
|
318
|
-
changed: {
|
|
319
|
-
version: versionChanged,
|
|
320
|
-
revision: revisionChanged,
|
|
321
|
-
any: versionChanged || revisionChanged,
|
|
322
|
-
},
|
|
323
|
-
});
|
|
321
|
+
processed.push(await updateRegistryEntry(entry, force, auditConfig));
|
|
324
322
|
}
|
|
325
|
-
|
|
326
|
-
const finalConfig = loadConfig();
|
|
327
|
-
return {
|
|
328
|
-
schemaVersion: 1,
|
|
329
|
-
stashDir,
|
|
330
|
-
target,
|
|
331
|
-
all,
|
|
332
|
-
processed,
|
|
333
|
-
config: {
|
|
334
|
-
sourceCount: (finalConfig.sources ?? finalConfig.stashes ?? []).length,
|
|
335
|
-
installedKitCount: finalConfig.installed?.length ?? 0,
|
|
336
|
-
},
|
|
337
|
-
index: {
|
|
338
|
-
mode: index.mode,
|
|
339
|
-
totalEntries: index.totalEntries,
|
|
340
|
-
directoriesScanned: index.directoriesScanned,
|
|
341
|
-
directoriesSkipped: index.directoriesSkipped,
|
|
342
|
-
},
|
|
343
|
-
};
|
|
323
|
+
return buildUpdateResponse(stashDir, target, all, processed);
|
|
344
324
|
}
|
|
345
325
|
function selectTargets(installed, target, all) {
|
|
346
326
|
if (all && target) {
|
|
@@ -356,7 +336,7 @@ function selectTargets(installed, target, all) {
|
|
|
356
336
|
return [found];
|
|
357
337
|
// Check if target matches a stash source and give a helpful message
|
|
358
338
|
const config = loadConfig();
|
|
359
|
-
const stashes = config
|
|
339
|
+
const stashes = getSources(config);
|
|
360
340
|
const isUrl = target.startsWith("http://") || target.startsWith("https://");
|
|
361
341
|
const resolvedPath = !isUrl ? path.resolve(target) : undefined;
|
|
362
342
|
const stashMatch = stashes.find((s) => {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge-command helpers extracted from `src/cli.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Covers the shared pipeline for reading, naming, and writing markdown assets
|
|
5
|
+
* (knowledge and memory) from the CLI. Extracted to keep the CLI entry point
|
|
6
|
+
* focused on command definitions and routing.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { resolveAssetPathFromName } from "../core/asset-spec";
|
|
11
|
+
import { isHttpUrl, isWithin, tryReadStdinText } from "../core/common";
|
|
12
|
+
import { loadConfig } from "../core/config";
|
|
13
|
+
import { UsageError } from "../core/errors";
|
|
14
|
+
import { resolveWriteTarget, writeAssetToSource } from "../core/write-source";
|
|
15
|
+
import { fetchWebsiteMarkdownSnapshot } from "../sources/website-ingest";
|
|
16
|
+
const MAX_CAPTURED_ASSET_SLUG_LENGTH = 64;
|
|
17
|
+
// ── Asset-name normalisation ─────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Validate and normalise a markdown asset name supplied by the user.
|
|
20
|
+
*
|
|
21
|
+
* Strips the `.md` extension, rejects empty names, and guards against path
|
|
22
|
+
* traversal (`..` segments). The `fallback` is used when `name` is undefined.
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeMarkdownAssetName(name, fallback) {
|
|
25
|
+
const trimmed = (name ?? fallback)
|
|
26
|
+
.trim()
|
|
27
|
+
.replace(/\\/g, "/")
|
|
28
|
+
.replace(/^\/+|\/+$/g, "")
|
|
29
|
+
.replace(/\.md$/i, "");
|
|
30
|
+
if (!trimmed)
|
|
31
|
+
throw new UsageError("Asset name cannot be empty.");
|
|
32
|
+
const segments = trimmed.split("/");
|
|
33
|
+
if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
|
|
34
|
+
throw new UsageError("Asset name must be a relative path without '.' or '..' segments.");
|
|
35
|
+
}
|
|
36
|
+
return trimmed;
|
|
37
|
+
}
|
|
38
|
+
function slugifyAssetName(value, fallbackPrefix) {
|
|
39
|
+
const slug = value
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/^[#>\-\s]+/, "")
|
|
42
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
43
|
+
.replace(/^-+|-+$/g, "")
|
|
44
|
+
.slice(0, MAX_CAPTURED_ASSET_SLUG_LENGTH);
|
|
45
|
+
return slug || `${fallbackPrefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Derive a slug-style asset name from `content` and an optional `preferred`
|
|
49
|
+
* hint (e.g. a URL-derived page title or the source filename stem).
|
|
50
|
+
*/
|
|
51
|
+
export function inferAssetName(content, fallbackPrefix, preferred) {
|
|
52
|
+
const firstNonEmptyLine = content
|
|
53
|
+
.split(/\r?\n/)
|
|
54
|
+
.map((line) => line.trim())
|
|
55
|
+
.find((line) => line.length > 0);
|
|
56
|
+
const basis = preferred?.trim() || firstNonEmptyLine || fallbackPrefix;
|
|
57
|
+
return slugifyAssetName(basis, fallbackPrefix);
|
|
58
|
+
}
|
|
59
|
+
// ── Content reading ──────────────────────────────────────────────────────────
|
|
60
|
+
/**
|
|
61
|
+
* Read knowledge content from a local file path or stdin (`"-"`).
|
|
62
|
+
*
|
|
63
|
+
* Returns the raw text and an optional `preferredName` derived from the
|
|
64
|
+
* source filename stem (used as a slug fallback when no `--name` flag was
|
|
65
|
+
* supplied).
|
|
66
|
+
*/
|
|
67
|
+
export function readKnowledgeContent(source) {
|
|
68
|
+
if (source === "-") {
|
|
69
|
+
const content = tryReadStdinText();
|
|
70
|
+
if (!content?.trim()) {
|
|
71
|
+
throw new UsageError("No stdin content received. Pipe a document into stdin or pass a file path.");
|
|
72
|
+
}
|
|
73
|
+
return { content };
|
|
74
|
+
}
|
|
75
|
+
const resolvedSource = path.resolve(source);
|
|
76
|
+
let stat;
|
|
77
|
+
try {
|
|
78
|
+
stat = fs.statSync(resolvedSource);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new UsageError(`Knowledge source not found: "${source}". Pass a readable file path or "-" for stdin.`);
|
|
82
|
+
}
|
|
83
|
+
if (!stat.isFile()) {
|
|
84
|
+
throw new UsageError(`Knowledge source must be a file: "${source}".`);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
content: fs.readFileSync(resolvedSource, "utf8"),
|
|
88
|
+
preferredName: path.basename(resolvedSource, path.extname(resolvedSource)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read knowledge content from a local path, stdin (`"-"`), or a remote URL.
|
|
93
|
+
*
|
|
94
|
+
* URLs are fetched via `fetchWebsiteMarkdownSnapshot`; local sources delegate
|
|
95
|
+
* to `readKnowledgeContent`.
|
|
96
|
+
*/
|
|
97
|
+
export async function readKnowledgeInput(source) {
|
|
98
|
+
if (!isHttpUrl(source))
|
|
99
|
+
return readKnowledgeContent(source);
|
|
100
|
+
const snapshot = await fetchWebsiteMarkdownSnapshot(source);
|
|
101
|
+
return { content: snapshot.content, preferredName: snapshot.preferredName };
|
|
102
|
+
}
|
|
103
|
+
// ── Asset writing ────────────────────────────────────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* Write a markdown asset (knowledge or memory) to the resolved write target.
|
|
106
|
+
*
|
|
107
|
+
* Resolves the write target via the v1 precedence chain (`--target` →
|
|
108
|
+
* `defaultWriteTarget` → working stash), validates the path is within the
|
|
109
|
+
* type root, enforces `--force` semantics, and delegates the actual write
|
|
110
|
+
* to `writeAssetToSource`.
|
|
111
|
+
*/
|
|
112
|
+
export async function writeMarkdownAsset(options) {
|
|
113
|
+
const cfg = loadConfig();
|
|
114
|
+
const { source, config } = resolveWriteTarget(cfg, options.target);
|
|
115
|
+
const typeRoot = path.join(source.path, options.type === "knowledge" ? "knowledge" : "memories");
|
|
116
|
+
const normalizedName = normalizeMarkdownAssetName(options.name, inferAssetName(options.content, options.fallbackPrefix, options.preferredName));
|
|
117
|
+
// Pre-flight: existence + force semantics. The helper itself overwrites
|
|
118
|
+
// unconditionally; the CLI surfaces a friendlier UsageError before any
|
|
119
|
+
// disk activity when --force is absent.
|
|
120
|
+
const assetPath = resolveAssetPathFromName(options.type, typeRoot, normalizedName);
|
|
121
|
+
if (!isWithin(assetPath, typeRoot)) {
|
|
122
|
+
throw new UsageError(`Resolved ${options.type} path escapes the stash: "${normalizedName}"`);
|
|
123
|
+
}
|
|
124
|
+
if (fs.existsSync(assetPath) && !options.force) {
|
|
125
|
+
throw new UsageError(`${options.type === "knowledge" ? "Knowledge" : "Memory"} "${normalizedName}" already exists. Re-run with --force to overwrite it.`, "RESOURCE_ALREADY_EXISTS");
|
|
126
|
+
}
|
|
127
|
+
const result = await writeAssetToSource(source, config, { type: options.type, name: normalizedName }, options.content);
|
|
128
|
+
return {
|
|
129
|
+
ref: result.ref,
|
|
130
|
+
path: result.path,
|
|
131
|
+
stashDir: source.path,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { BaseLinter } from "./base-linter";
|
|
3
|
+
/**
|
|
4
|
+
* Linter for `agents/` assets.
|
|
5
|
+
*
|
|
6
|
+
* Extra check beyond base:
|
|
7
|
+
* - `missing-name-or-type`: frontmatter exists but `name` or `type` field is
|
|
8
|
+
* absent. Not auto-fixable; detail includes a suggested slug.
|
|
9
|
+
*/
|
|
10
|
+
export class AgentLinter extends BaseLinter {
|
|
11
|
+
types = ["agents"];
|
|
12
|
+
lint(ctx) {
|
|
13
|
+
const issues = this.runBaseChecks(ctx);
|
|
14
|
+
const missingFieldDetail = this.#checkMissingNameOrType(ctx.data, ctx.frontmatter);
|
|
15
|
+
if (missingFieldDetail) {
|
|
16
|
+
const slug = this.#suggestSlug(ctx.filePath);
|
|
17
|
+
issues.push({
|
|
18
|
+
file: ctx.relPath,
|
|
19
|
+
issue: "missing-name-or-type",
|
|
20
|
+
detail: `${missingFieldDetail}; suggested slug: ${slug}`,
|
|
21
|
+
fixed: false,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return issues;
|
|
25
|
+
}
|
|
26
|
+
#checkMissingNameOrType(data, frontmatterText) {
|
|
27
|
+
if (!frontmatterText)
|
|
28
|
+
return null;
|
|
29
|
+
const missingFields = [];
|
|
30
|
+
if (!("name" in data) || !data.name)
|
|
31
|
+
missingFields.push("name");
|
|
32
|
+
if (!("type" in data) || !data.type)
|
|
33
|
+
missingFields.push("type");
|
|
34
|
+
if (missingFields.length === 0)
|
|
35
|
+
return null;
|
|
36
|
+
return `missing fields: ${missingFields.join(", ")}`;
|
|
37
|
+
}
|
|
38
|
+
#suggestSlug(filePath) {
|
|
39
|
+
return path
|
|
40
|
+
.basename(filePath, ".md")
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
43
|
+
.replace(/-+/g, "-")
|
|
44
|
+
.replace(/^-|-$/g, "");
|
|
45
|
+
}
|
|
46
|
+
}
|