akm-cli 0.7.4 → 0.8.0-rc.10
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 +224 -1
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +133 -0
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2631 -1440
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +2122 -0
- package/dist/commands/curate.js +45 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +1081 -73
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +15 -24
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +1302 -0
- package/dist/commands/help/help-accept.md +12 -0
- package/dist/commands/help/help-improve.md +69 -0
- package/dist/commands/help/help-proposals.md +18 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +11 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +217 -0
- package/dist/commands/improve-profiles.js +166 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +2373 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +196 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +3 -0
- package/dist/commands/proposal.js +67 -12
- package/dist/commands/propose.js +120 -45
- package/dist/commands/reflect.js +1104 -60
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +70 -7
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +158 -60
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +437 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +17 -2
- package/dist/core/asset-registry.js +12 -17
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +67 -1
- package/dist/core/common.js +182 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +534 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +364 -968
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +105 -135
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -8
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +280 -14
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +512 -42
- package/dist/core/state-db.js +1068 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +64 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +198 -489
- package/dist/indexer/db.js +990 -108
- package/dist/indexer/ensure-index.js +136 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -114
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +547 -309
- package/dist/indexer/llm-cache.js +52 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +167 -160
- package/dist/indexer/memory-inference.js +152 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +275 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +6 -17
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +250 -36
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +183 -35
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +69 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +282 -0
- package/dist/integrations/session-logs/providers/opencode.js +258 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +79 -88
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +95 -48
- package/dist/llm/graph-extract.js +676 -72
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +80 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +292 -0
- package/dist/output/cli-hints-short.md +66 -0
- package/dist/output/cli-hints.js +7 -311
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +306 -258
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +102 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -511
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1039 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +11 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1093
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +71 -50
- package/dist/registry/providers/static-index.js +53 -48
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17750 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +179 -20
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +227 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -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 +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +141 -2
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +11 -3
- package/dist/workflows/runs.js +91 -89
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +79 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +10 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.4.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +20 -0
- package/docs/migration/release-notes/0.8.0.md +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +29 -11
- package/dist/commands/install-audit.js +0 -381
- package/dist/commands/vault.js +0 -333
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parseAssetRef } from "../core/asset-ref";
|
|
7
|
+
import { loadConfig } from "../core/config";
|
|
8
|
+
import { NotFoundError, UsageError } from "../core/errors";
|
|
9
|
+
import { getDbPath } from "../core/paths";
|
|
10
|
+
import { warn } from "../core/warn";
|
|
11
|
+
import { closeDatabase, findEntryIdByRef, getEntryById, openDatabase, openExistingDatabase } from "../indexer/db";
|
|
12
|
+
import { listRelatedPathsForFile } from "../indexer/graph-boost";
|
|
13
|
+
import { loadStoredGraphSnapshot } from "../indexer/graph-db";
|
|
14
|
+
import { runGraphExtractionPass } from "../indexer/graph-extraction";
|
|
15
|
+
import { lookup } from "../indexer/indexer";
|
|
16
|
+
import { resolveAssetPath } from "../indexer/path-resolver";
|
|
17
|
+
import { findSourceForPath, resolveSourceEntries } from "../indexer/search-source";
|
|
18
|
+
function resolveGraphStashPath(source) {
|
|
19
|
+
const sources = resolveSourceEntries(undefined, loadConfig());
|
|
20
|
+
if (sources.length === 0) {
|
|
21
|
+
throw new NotFoundError("No stash sources are configured.", "STASH_NOT_FOUND");
|
|
22
|
+
}
|
|
23
|
+
if (!source || source === "primary")
|
|
24
|
+
return sources[0].path;
|
|
25
|
+
const matched = sources.find((entry) => entry.registryId === source || entry.path === source);
|
|
26
|
+
if (!matched) {
|
|
27
|
+
throw new NotFoundError(`Source not found: ${source}`, "SOURCE_NOT_FOUND", "Run `akm list` to see source names.");
|
|
28
|
+
}
|
|
29
|
+
return matched.path;
|
|
30
|
+
}
|
|
31
|
+
function loadGraph(source) {
|
|
32
|
+
const stashPath = resolveGraphStashPath(source);
|
|
33
|
+
let db;
|
|
34
|
+
try {
|
|
35
|
+
db = openExistingDatabase();
|
|
36
|
+
const snapshot = loadStoredGraphSnapshot(stashPath, db);
|
|
37
|
+
if (!snapshot) {
|
|
38
|
+
throw new NotFoundError(`Graph data not found for source ${stashPath}.`, "FILE_NOT_FOUND", "Run the improvement flow that refreshes graph extraction data.");
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
graph: {
|
|
42
|
+
schemaVersion: snapshot.schemaVersion,
|
|
43
|
+
generatedAt: snapshot.generatedAt,
|
|
44
|
+
stashRoot: snapshot.stashPath,
|
|
45
|
+
files: snapshot.files,
|
|
46
|
+
entities: snapshot.entities,
|
|
47
|
+
relations: snapshot.relations,
|
|
48
|
+
...(snapshot.quality ? { quality: snapshot.quality } : {}),
|
|
49
|
+
...(snapshot.telemetry ? { telemetry: snapshot.telemetry } : {}),
|
|
50
|
+
},
|
|
51
|
+
stashPath,
|
|
52
|
+
graphPath: snapshot.graphPath,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
if (db)
|
|
57
|
+
closeDatabase(db);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function countEntitiesByFile(nodes) {
|
|
61
|
+
const counts = new Map();
|
|
62
|
+
for (const node of nodes) {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
for (const entity of node.entities) {
|
|
65
|
+
if (seen.has(entity))
|
|
66
|
+
continue;
|
|
67
|
+
seen.add(entity);
|
|
68
|
+
counts.set(entity, (counts.get(entity) ?? 0) + 1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return counts;
|
|
72
|
+
}
|
|
73
|
+
function aggregateEntityStats(nodes) {
|
|
74
|
+
const stats = new Map();
|
|
75
|
+
for (const node of nodes) {
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
for (const entity of node.entities) {
|
|
78
|
+
if (seen.has(entity))
|
|
79
|
+
continue;
|
|
80
|
+
seen.add(entity);
|
|
81
|
+
const existing = stats.get(entity);
|
|
82
|
+
const nodeConf = typeof node.confidence === "number" && Number.isFinite(node.confidence) ? node.confidence : undefined;
|
|
83
|
+
if (existing) {
|
|
84
|
+
existing.fileCount += 1;
|
|
85
|
+
if (nodeConf !== undefined && (existing.confidence === undefined || nodeConf > existing.confidence)) {
|
|
86
|
+
existing.confidence = nodeConf;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
stats.set(entity, { fileCount: 1, ...(nodeConf !== undefined ? { confidence: nodeConf } : {}) });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return stats;
|
|
95
|
+
}
|
|
96
|
+
export function akmGraphSummary(options) {
|
|
97
|
+
const { graph, stashPath, graphPath } = loadGraph(options?.source);
|
|
98
|
+
return {
|
|
99
|
+
schemaVersion: 1,
|
|
100
|
+
shape: "graph-summary",
|
|
101
|
+
stashPath,
|
|
102
|
+
graphPath,
|
|
103
|
+
generatedAt: graph.generatedAt,
|
|
104
|
+
fileCount: graph.files.length,
|
|
105
|
+
entityCount: Array.isArray(graph.entities) ? graph.entities.length : countEntitiesByFile(graph.files).size,
|
|
106
|
+
relationCount: Array.isArray(graph.relations)
|
|
107
|
+
? graph.relations.length
|
|
108
|
+
: graph.files.reduce((sum, node) => sum + node.relations.length, 0),
|
|
109
|
+
...(graph.quality ? { quality: graph.quality } : {}),
|
|
110
|
+
...(graph.telemetry ? { telemetry: graph.telemetry } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function akmGraphEntities(options) {
|
|
114
|
+
const { graph, stashPath, graphPath } = loadGraph(options?.source);
|
|
115
|
+
const limit = options?.limit;
|
|
116
|
+
if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
|
|
117
|
+
throw new UsageError("--limit must be a positive integer.", "INVALID_FLAG_VALUE");
|
|
118
|
+
}
|
|
119
|
+
const stats = aggregateEntityStats(graph.files);
|
|
120
|
+
const entities = [...stats.entries()]
|
|
121
|
+
.map(([name, info]) => ({
|
|
122
|
+
name,
|
|
123
|
+
fileCount: info.fileCount,
|
|
124
|
+
...(info.confidence !== undefined ? { confidence: info.confidence } : {}),
|
|
125
|
+
}))
|
|
126
|
+
.sort((a, b) => b.fileCount - a.fileCount || a.name.localeCompare(b.name));
|
|
127
|
+
const sliced = typeof limit === "number" ? entities.slice(0, limit) : entities;
|
|
128
|
+
return {
|
|
129
|
+
schemaVersion: 1,
|
|
130
|
+
shape: "graph-entities",
|
|
131
|
+
stashPath,
|
|
132
|
+
graphPath,
|
|
133
|
+
generatedAt: graph.generatedAt,
|
|
134
|
+
total: entities.length,
|
|
135
|
+
entities: sliced,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function akmGraphRelations(options) {
|
|
139
|
+
const { graph, stashPath, graphPath } = loadGraph(options?.source);
|
|
140
|
+
const limit = options?.limit;
|
|
141
|
+
if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
|
|
142
|
+
throw new UsageError("--limit must be a positive integer.", "INVALID_FLAG_VALUE");
|
|
143
|
+
}
|
|
144
|
+
const counts = new Map();
|
|
145
|
+
for (const node of graph.files) {
|
|
146
|
+
for (const rel of node.relations) {
|
|
147
|
+
const key = `${rel.from}\u0000${rel.to}\u0000${rel.type ?? ""}`;
|
|
148
|
+
const relConf = typeof rel.confidence === "number" && Number.isFinite(rel.confidence) ? rel.confidence : undefined;
|
|
149
|
+
const existing = counts.get(key);
|
|
150
|
+
if (existing) {
|
|
151
|
+
existing.count += 1;
|
|
152
|
+
if (relConf !== undefined && (existing.confidence === undefined || relConf > existing.confidence)) {
|
|
153
|
+
existing.confidence = relConf;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
counts.set(key, {
|
|
158
|
+
from: rel.from,
|
|
159
|
+
to: rel.to,
|
|
160
|
+
...(rel.type ? { type: rel.type } : {}),
|
|
161
|
+
count: 1,
|
|
162
|
+
...(relConf !== undefined ? { confidence: relConf } : {}),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const relations = [...counts.values()].sort((a, b) => b.count - a.count || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
168
|
+
const sliced = typeof limit === "number" ? relations.slice(0, limit) : relations;
|
|
169
|
+
return {
|
|
170
|
+
schemaVersion: 1,
|
|
171
|
+
shape: "graph-relations",
|
|
172
|
+
stashPath,
|
|
173
|
+
graphPath,
|
|
174
|
+
generatedAt: graph.generatedAt,
|
|
175
|
+
total: relations.length,
|
|
176
|
+
relations: sliced,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function akmGraphExport(options) {
|
|
180
|
+
if (!options.out?.trim()) {
|
|
181
|
+
throw new UsageError("`akm graph export` requires --out <path>.", "MISSING_REQUIRED_ARGUMENT");
|
|
182
|
+
}
|
|
183
|
+
const format = options.format ?? "json";
|
|
184
|
+
if (format !== "json" && format !== "jsonl") {
|
|
185
|
+
throw new UsageError("--format must be one of: json, jsonl.", "INVALID_FLAG_VALUE");
|
|
186
|
+
}
|
|
187
|
+
const { graph, stashPath, graphPath } = loadGraph(options.source);
|
|
188
|
+
const outPath = path.resolve(options.out);
|
|
189
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
190
|
+
const payload = format === "json"
|
|
191
|
+
? `${JSON.stringify(graph, null, 2)}\n`
|
|
192
|
+
: `${[
|
|
193
|
+
...graph.files.flatMap((file) => file.entities.map((entity) => JSON.stringify({ kind: "entity", entity, file: file.path }))),
|
|
194
|
+
...graph.files.flatMap((file) => file.relations.map((relation) => JSON.stringify({ kind: "relation", ...relation, file: file.path }))),
|
|
195
|
+
].join("\n")}\n`;
|
|
196
|
+
fs.writeFileSync(outPath, payload, "utf8");
|
|
197
|
+
return {
|
|
198
|
+
schemaVersion: 1,
|
|
199
|
+
shape: "graph-export",
|
|
200
|
+
stashPath,
|
|
201
|
+
graphPath,
|
|
202
|
+
outPath,
|
|
203
|
+
format,
|
|
204
|
+
bytes: Buffer.byteLength(payload, "utf8"),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export async function akmGraphRelated(options) {
|
|
208
|
+
const ref = options.ref.trim();
|
|
209
|
+
if (!ref) {
|
|
210
|
+
throw new UsageError("`akm graph related` requires <ref>.", "MISSING_REQUIRED_ARGUMENT");
|
|
211
|
+
}
|
|
212
|
+
const limit = options.limit;
|
|
213
|
+
if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
|
|
214
|
+
throw new UsageError("--limit must be a positive integer.", "INVALID_FLAG_VALUE");
|
|
215
|
+
}
|
|
216
|
+
const target = await resolveGraphTarget(ref, options.source);
|
|
217
|
+
const { graph, stashPath, graphPath } = loadGraph(target.stashPath);
|
|
218
|
+
let db;
|
|
219
|
+
const related = (() => {
|
|
220
|
+
try {
|
|
221
|
+
db = openExistingDatabase();
|
|
222
|
+
return listRelatedPathsForFile(stashPath, target.filePath, limit ?? 5, db);
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
if (db)
|
|
226
|
+
closeDatabase(db);
|
|
227
|
+
}
|
|
228
|
+
})();
|
|
229
|
+
return {
|
|
230
|
+
schemaVersion: 1,
|
|
231
|
+
shape: "graph-related",
|
|
232
|
+
stashPath,
|
|
233
|
+
graphPath,
|
|
234
|
+
generatedAt: graph.generatedAt,
|
|
235
|
+
ref: target.ref,
|
|
236
|
+
path: target.filePath,
|
|
237
|
+
total: related.length,
|
|
238
|
+
related,
|
|
239
|
+
...(related.length === 0 ? { tip: "No related graph neighbors were found for this asset." } : {}),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function normalizeGraphName(value) {
|
|
243
|
+
return value.trim().toLowerCase();
|
|
244
|
+
}
|
|
245
|
+
function buildRefByPath(stashRoot, db) {
|
|
246
|
+
const rows = db
|
|
247
|
+
.prepare("SELECT file_path, entry_json FROM entries WHERE stash_dir = ? OR file_path LIKE ?")
|
|
248
|
+
.all(stashRoot, `${stashRoot}%`);
|
|
249
|
+
const map = new Map();
|
|
250
|
+
for (const row of rows) {
|
|
251
|
+
if (map.has(row.file_path))
|
|
252
|
+
continue;
|
|
253
|
+
try {
|
|
254
|
+
const entry = JSON.parse(row.entry_json);
|
|
255
|
+
if (typeof entry.type === "string" && typeof entry.name === "string") {
|
|
256
|
+
map.set(row.file_path, { ref: `${entry.type}:${entry.name}`, type: entry.type });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// ignore corrupt entry_json
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return map;
|
|
264
|
+
}
|
|
265
|
+
export function akmGraphEntity(options) {
|
|
266
|
+
const name = options.name?.trim();
|
|
267
|
+
if (!name) {
|
|
268
|
+
throw new UsageError("`akm graph entity` requires <name>.", "MISSING_REQUIRED_ARGUMENT");
|
|
269
|
+
}
|
|
270
|
+
const limit = options.limit;
|
|
271
|
+
if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
|
|
272
|
+
throw new UsageError("--limit must be a positive integer.", "INVALID_FLAG_VALUE");
|
|
273
|
+
}
|
|
274
|
+
const { graph, stashPath, graphPath } = loadGraph(options.source);
|
|
275
|
+
const target = normalizeGraphName(name);
|
|
276
|
+
let db;
|
|
277
|
+
let refByPath;
|
|
278
|
+
try {
|
|
279
|
+
db = openExistingDatabase();
|
|
280
|
+
refByPath = buildRefByPath(stashPath, db);
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
if (db)
|
|
284
|
+
closeDatabase(db);
|
|
285
|
+
}
|
|
286
|
+
const matches = [];
|
|
287
|
+
for (const node of graph.files) {
|
|
288
|
+
const found = node.entities.some((entity) => normalizeGraphName(entity) === target);
|
|
289
|
+
if (!found)
|
|
290
|
+
continue;
|
|
291
|
+
const lookup = refByPath.get(node.path);
|
|
292
|
+
const conf = typeof node.confidence === "number" && Number.isFinite(node.confidence) ? node.confidence : undefined;
|
|
293
|
+
matches.push({
|
|
294
|
+
...(lookup?.ref ? { ref: lookup.ref } : {}),
|
|
295
|
+
path: node.path,
|
|
296
|
+
type: node.type,
|
|
297
|
+
...(conf !== undefined ? { confidence: conf } : {}),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
matches.sort((a, b) => {
|
|
301
|
+
const ca = a.confidence ?? 0;
|
|
302
|
+
const cb = b.confidence ?? 0;
|
|
303
|
+
if (cb !== ca)
|
|
304
|
+
return cb - ca;
|
|
305
|
+
return a.path.localeCompare(b.path);
|
|
306
|
+
});
|
|
307
|
+
const sliced = typeof limit === "number" ? matches.slice(0, limit) : matches;
|
|
308
|
+
return {
|
|
309
|
+
schemaVersion: 1,
|
|
310
|
+
shape: "graph-entity",
|
|
311
|
+
stashPath,
|
|
312
|
+
graphPath,
|
|
313
|
+
generatedAt: graph.generatedAt,
|
|
314
|
+
entity: name,
|
|
315
|
+
total: matches.length,
|
|
316
|
+
matches: sliced,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
export function akmGraphOrphans(options) {
|
|
320
|
+
const limit = options?.limit;
|
|
321
|
+
if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
|
|
322
|
+
throw new UsageError("--limit must be a positive integer.", "INVALID_FLAG_VALUE");
|
|
323
|
+
}
|
|
324
|
+
const { graph, stashPath, graphPath } = loadGraph(options?.source);
|
|
325
|
+
let db;
|
|
326
|
+
let refByPath;
|
|
327
|
+
try {
|
|
328
|
+
db = openExistingDatabase();
|
|
329
|
+
refByPath = buildRefByPath(stashPath, db);
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
if (db)
|
|
333
|
+
closeDatabase(db);
|
|
334
|
+
}
|
|
335
|
+
const orphans = [];
|
|
336
|
+
for (const node of graph.files) {
|
|
337
|
+
if ((node.status ?? (node.entities.length > 0 ? "extracted" : "empty")) === "extracted")
|
|
338
|
+
continue;
|
|
339
|
+
const lookup = refByPath.get(node.path);
|
|
340
|
+
orphans.push({
|
|
341
|
+
...(lookup?.ref ? { ref: lookup.ref } : {}),
|
|
342
|
+
path: node.path,
|
|
343
|
+
type: node.type,
|
|
344
|
+
...(node.status ? { status: node.status } : {}),
|
|
345
|
+
...(node.reason ? { reason: node.reason } : {}),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
orphans.sort((a, b) => a.type.localeCompare(b.type) || a.path.localeCompare(b.path));
|
|
349
|
+
const sliced = typeof limit === "number" ? orphans.slice(0, limit) : orphans;
|
|
350
|
+
return {
|
|
351
|
+
schemaVersion: 1,
|
|
352
|
+
shape: "graph-orphans",
|
|
353
|
+
stashPath,
|
|
354
|
+
graphPath,
|
|
355
|
+
generatedAt: graph.generatedAt,
|
|
356
|
+
totalConsidered: graph.files.length,
|
|
357
|
+
total: orphans.length,
|
|
358
|
+
orphans: sliced,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Re-run graph extraction, optionally scoped to specific asset refs.
|
|
363
|
+
*
|
|
364
|
+
* When `refs` is provided, only those files are re-extracted (incremental).
|
|
365
|
+
* When no refs are given, the full eligible set is re-extracted.
|
|
366
|
+
*/
|
|
367
|
+
export async function akmGraphUpdate(options) {
|
|
368
|
+
const config = options.config ?? loadConfig();
|
|
369
|
+
const sources = resolveSourceEntries(options.stashDir, config);
|
|
370
|
+
if (sources.length === 0) {
|
|
371
|
+
throw new NotFoundError("No stash sources are configured.", "STASH_NOT_FOUND");
|
|
372
|
+
}
|
|
373
|
+
if (options.source && options.source !== "primary") {
|
|
374
|
+
const matched = sources.find((s) => s.registryId === options.source || s.path === options.source);
|
|
375
|
+
if (!matched) {
|
|
376
|
+
throw new NotFoundError(`Source not found: ${options.source}`, "SOURCE_NOT_FOUND", "Run `akm list` to see source names.");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const scoped = Array.isArray(options.refs) && options.refs.length > 0;
|
|
380
|
+
let candidatePaths;
|
|
381
|
+
if (scoped && options.refs) {
|
|
382
|
+
// Resolve each ref to an absolute file path via the index DB.
|
|
383
|
+
const dbPath = getDbPath();
|
|
384
|
+
let db;
|
|
385
|
+
const resolvedPaths = new Set();
|
|
386
|
+
try {
|
|
387
|
+
db = openDatabase(dbPath);
|
|
388
|
+
for (const ref of options.refs) {
|
|
389
|
+
const trimmed = ref.trim();
|
|
390
|
+
if (!trimmed)
|
|
391
|
+
continue;
|
|
392
|
+
const entryId = findEntryIdByRef(db, trimmed);
|
|
393
|
+
if (entryId === undefined) {
|
|
394
|
+
warn(`[graph] ref not found in index, skipping: ${trimmed}`);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const row = getEntryById(db, entryId);
|
|
398
|
+
if (!row?.filePath) {
|
|
399
|
+
warn(`[graph] could not resolve path for ref, skipping: ${trimmed}`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
resolvedPaths.add(row.filePath);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
if (db)
|
|
407
|
+
closeDatabase(db);
|
|
408
|
+
}
|
|
409
|
+
if (resolvedPaths.size === 0) {
|
|
410
|
+
warn("[graph] none of the provided refs resolved to indexed paths — no extraction performed.");
|
|
411
|
+
return {
|
|
412
|
+
shape: "graph-update",
|
|
413
|
+
ok: true,
|
|
414
|
+
filesExtracted: 0,
|
|
415
|
+
entitiesUpserted: 0,
|
|
416
|
+
relationsUpserted: 0,
|
|
417
|
+
durationMs: 0,
|
|
418
|
+
scoped: true,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
candidatePaths = resolvedPaths;
|
|
422
|
+
}
|
|
423
|
+
const extractionFn = options.graphExtractionFn ?? runGraphExtractionPass;
|
|
424
|
+
const passOptions = candidatePaths ? { candidatePaths } : {};
|
|
425
|
+
let db;
|
|
426
|
+
const startMs = Date.now();
|
|
427
|
+
try {
|
|
428
|
+
db = openDatabase(getDbPath());
|
|
429
|
+
const onProgress = (event) => {
|
|
430
|
+
if (!event.currentPath)
|
|
431
|
+
return;
|
|
432
|
+
const file = path.basename(event.currentPath);
|
|
433
|
+
warn(`[graph] extracting ${event.processed}/${event.total} ${file}`);
|
|
434
|
+
};
|
|
435
|
+
const result = await extractionFn(config, sources, undefined, db, false, onProgress, passOptions);
|
|
436
|
+
const durationMs = Date.now() - startMs;
|
|
437
|
+
return {
|
|
438
|
+
shape: "graph-update",
|
|
439
|
+
ok: true,
|
|
440
|
+
filesExtracted: result.quality.extractedFiles,
|
|
441
|
+
entitiesUpserted: result.quality.entityCount,
|
|
442
|
+
relationsUpserted: result.quality.relationCount,
|
|
443
|
+
durationMs,
|
|
444
|
+
scoped,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
finally {
|
|
448
|
+
if (db)
|
|
449
|
+
closeDatabase(db);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function resolveGraphTarget(ref, source) {
|
|
453
|
+
const parsedRef = parseAssetRef(ref);
|
|
454
|
+
const filePath = (await resolveAssetPath(parsedRef, {
|
|
455
|
+
mode: "index-first",
|
|
456
|
+
honorOrigin: true,
|
|
457
|
+
})) ?? (await lookup(parsedRef))?.filePath;
|
|
458
|
+
if (!filePath) {
|
|
459
|
+
throw new NotFoundError(`Asset not found for ref: ${ref}`);
|
|
460
|
+
}
|
|
461
|
+
const allSources = resolveSourceEntries(undefined, loadConfig());
|
|
462
|
+
const matchedSource = findSourceForPath(filePath, allSources);
|
|
463
|
+
const inferredStashPath = matchedSource?.path;
|
|
464
|
+
const stashPath = source ? resolveGraphStashPath(source) : inferredStashPath;
|
|
465
|
+
if (!stashPath) {
|
|
466
|
+
throw new NotFoundError(`Could not determine stash source for ref: ${ref}`, "SOURCE_NOT_FOUND");
|
|
467
|
+
}
|
|
468
|
+
if (!filePath.startsWith(path.resolve(stashPath) + path.sep) && path.resolve(filePath) !== path.resolve(stashPath)) {
|
|
469
|
+
throw new UsageError(`Resolved asset ${ref} is not inside source ${source ?? stashPath}.`, "INVALID_SOURCE_VALUE", "Pass --source for the asset's source, or omit it to infer from the resolved asset path.");
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
ref,
|
|
473
|
+
parsedRef,
|
|
474
|
+
filePath,
|
|
475
|
+
stashPath,
|
|
476
|
+
};
|
|
477
|
+
}
|