akm-cli 0.10.261005-beta → 0.10.261007-beta
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 +65 -0
- package/STABILITY.md +9 -0
- package/dist/assets/ui/app.js +91 -0
- package/dist/assets/ui/asset.js +67 -0
- package/dist/assets/ui/bundles.js +80 -0
- package/dist/assets/ui/changes.js +32 -0
- package/dist/assets/ui/charts.js +93 -0
- package/dist/assets/ui/config.js +69 -0
- package/dist/assets/ui/diff-view.js +202 -0
- package/dist/assets/ui/diff.js +261 -0
- package/dist/assets/ui/index.html +30 -0
- package/dist/assets/ui/lib.js +238 -0
- package/dist/assets/ui/markdown.js +103 -0
- package/dist/assets/ui/overview.js +145 -0
- package/dist/assets/ui/proposal-actions.js +273 -0
- package/dist/assets/ui/proposals.js +219 -0
- package/dist/assets/ui/search.js +62 -0
- package/dist/assets/ui/styles.css +1415 -0
- package/dist/cli.js +3 -0
- package/dist/commands/health/checks.js +5 -3
- package/dist/commands/health/egress.js +5 -1
- package/dist/commands/health/engine-usage.js +2 -2
- package/dist/commands/health/llm-usage.js +2 -2
- package/dist/commands/health/windows.js +5 -5
- package/dist/commands/health.js +40 -8
- package/dist/commands/improve/reflect-noise.js +4 -0
- package/dist/commands/improve/reflect.js +14 -5
- package/dist/commands/metrics/metrics-cli.js +3 -3
- package/dist/commands/proposal/propose-cli.js +5 -0
- package/dist/commands/proposal/propose.js +6 -2
- package/dist/commands/proposal/repository.js +34 -29
- package/dist/commands/read/search.js +2 -1
- package/dist/commands/read/show.js +18 -12
- package/dist/commands/sources/source-clone.js +123 -11
- package/dist/commands/ui/api.js +556 -0
- package/dist/commands/ui/git-read.js +101 -0
- package/dist/commands/ui/open-browser.js +30 -0
- package/dist/commands/ui/proposal-actions.js +179 -0
- package/dist/commands/ui/redact.js +88 -0
- package/dist/commands/ui/server.js +352 -0
- package/dist/commands/ui/ui-cli.js +62 -0
- package/dist/core/asset/resolve-ref.js +7 -3
- package/dist/indexer/indexer.js +14 -2
- package/dist/indexer/search/db-search.js +7 -3
- package/dist/output/format-exempt.js +3 -0
- package/dist/storage/repositories/improve-runs-repository.js +6 -0
- package/dist/storage/repositories/index-db.js +10 -2
- package/dist/storage/repositories/metrics-repository.js +17 -0
- package/docs/architecture/0.10-process-rules.md +3 -4
- package/docs/architecture/improvement.md +2 -1
- package/docs/architecture/internals/improve-workflow.md +2 -2
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +68 -0
- package/docs/reference/ui-api.md +576 -0
- package/package.json +1 -1
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import { isDeepStrictEqual } from "node:util";
|
|
8
|
+
import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
|
|
9
|
+
import { adapterForId } from "../../core/adapter/registry.js";
|
|
10
|
+
import { placementSpecFor, stashDirFor } from "../../core/asset/asset-placement.js";
|
|
8
11
|
import { isBundleSlug } from "../../core/asset/asset-ref.js";
|
|
9
|
-
import {
|
|
12
|
+
import { serializeFrontmatter } from "../../core/asset/asset-serialize.js";
|
|
13
|
+
import { parseFrontmatter, replaceFrontmatterBlocks } from "../../core/asset/frontmatter.js";
|
|
14
|
+
import { conceptIdFromTypeName, displayRef, parseQualifiedRefInput } from "../../core/asset/resolve-ref.js";
|
|
10
15
|
import { isWithin } from "../../core/common.js";
|
|
11
16
|
import { loadConfig } from "../../core/config/config.js";
|
|
12
17
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
@@ -15,10 +20,13 @@ import { warn } from "../../core/warn.js";
|
|
|
15
20
|
import { commitWriteTargetBoundary, prepareWriteTargetForMutation, resolveWriteTarget, } from "../../core/write-source.js";
|
|
16
21
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
17
22
|
import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
|
|
23
|
+
import { resolveAdapterConceptOwner } from "../../indexer/lookup/adapter-concept-owner.js";
|
|
18
24
|
import { findSourceForPath, resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
25
|
+
import { buildFileContext } from "../../indexer/walk/file-context.js";
|
|
19
26
|
import { isRemoteOrigin, resolveSourcesForLocator, resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
|
|
20
27
|
import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
|
|
21
28
|
import { resolveAssetPath } from "../../sources/resolve.js";
|
|
29
|
+
import { inferAssetName } from "../read/knowledge.js";
|
|
22
30
|
function destinationRef(config, target, type, name) {
|
|
23
31
|
if (!config || !target)
|
|
24
32
|
return undefined;
|
|
@@ -77,11 +85,25 @@ export async function akmClone(options) {
|
|
|
77
85
|
cacheDir: installResult.cacheDir,
|
|
78
86
|
};
|
|
79
87
|
}
|
|
88
|
+
// A ref whose conceptId is not under a known type directory (a flat skill
|
|
89
|
+
// repo, a website or wiki page) is opaque: its source and type come from the
|
|
90
|
+
// adapter that owns the concept, not from the shape of the ref.
|
|
91
|
+
const known = placementSpecFor(parsed.type) !== undefined;
|
|
80
92
|
let sourcePath;
|
|
93
|
+
let opaqueType;
|
|
81
94
|
let lastError;
|
|
82
95
|
for (const source of searchSources) {
|
|
83
96
|
try {
|
|
84
|
-
|
|
97
|
+
if (known) {
|
|
98
|
+
sourcePath = await resolveAssetPath(source.path, parsed.type, parsed.name);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const owned = resolveOpaqueAsset(source, parsed.name);
|
|
102
|
+
if (!owned)
|
|
103
|
+
throw new NotFoundError(`Source asset not found for ref: ${options.sourceRef}`, "ASSET_NOT_FOUND");
|
|
104
|
+
sourcePath = owned.path;
|
|
105
|
+
opaqueType = owned.type;
|
|
106
|
+
}
|
|
85
107
|
break;
|
|
86
108
|
}
|
|
87
109
|
catch (err) {
|
|
@@ -95,8 +117,20 @@ export async function akmClone(options) {
|
|
|
95
117
|
throw lastError ?? new NotFoundError(`Source asset not found for ref: ${options.sourceRef}`, "ASSET_NOT_FOUND");
|
|
96
118
|
}
|
|
97
119
|
const sourceSource = findSourceForPath(sourcePath, allSources);
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
// Where the clone lands: a known-type ref keeps its type and name; an opaque
|
|
121
|
+
// one is placed by what the source file is.
|
|
122
|
+
const sourceExt = path.extname(sourcePath);
|
|
123
|
+
const cloneType = known ? parsed.type : placementTypeForOpaque(options.sourceRef, sourcePath, opaqueType);
|
|
124
|
+
const defaultName = known ? parsed.name : slugName(parsed.name);
|
|
125
|
+
const destName = options.newName ?? defaultName;
|
|
126
|
+
const typeDir = stashDirFor(cloneType);
|
|
127
|
+
// A ref's name often omits its extension (`knowledge/<name>` names `<name>.md`), and an opaque one is a slug:
|
|
128
|
+
// either way the copy keeps the source file's extension, or it would be written as a file the index ignores.
|
|
129
|
+
const destFileName = path.extname(destName) ? destName : destName + sourceExt;
|
|
130
|
+
const isSkill = cloneType === "skill";
|
|
131
|
+
// A concept id is the path without its markdown extension.
|
|
132
|
+
const originConceptId = (known ? conceptIdFromTypeName(parsed.type, parsed.name) : parsed.name).replace(/\.md$/i, "");
|
|
133
|
+
const originRef = sourceSource?.registryId ? `${sourceSource.registryId}//${originConceptId}` : undefined;
|
|
100
134
|
// Validate destName to prevent path traversal (parsed.name is already
|
|
101
135
|
// validated by the ref parser, but newName comes directly from user input).
|
|
102
136
|
// Run whenever newName is provided, including empty string.
|
|
@@ -128,7 +162,7 @@ export async function akmClone(options) {
|
|
|
128
162
|
? `in target "${writeTarget.selector}"`
|
|
129
163
|
: "in working stash";
|
|
130
164
|
// Guard against self-clone
|
|
131
|
-
if (
|
|
165
|
+
if (isSkill) {
|
|
132
166
|
const sourceSkillDir = path.resolve(path.dirname(sourcePath));
|
|
133
167
|
const destSkillDir = path.resolve(path.join(destRoot, typeDir, destName));
|
|
134
168
|
if (sourceSkillDir === destSkillDir) {
|
|
@@ -137,8 +171,7 @@ export async function akmClone(options) {
|
|
|
137
171
|
}
|
|
138
172
|
else {
|
|
139
173
|
const resolvedSource = path.resolve(sourcePath);
|
|
140
|
-
const
|
|
141
|
-
const effectiveDestName = !path.extname(destName) && sourceExt ? destName + sourceExt : destName;
|
|
174
|
+
const effectiveDestName = !path.extname(destFileName) && sourceExt ? destFileName + sourceExt : destFileName;
|
|
142
175
|
const resolvedDest = path.resolve(path.join(destRoot, typeDir, effectiveDestName));
|
|
143
176
|
if (resolvedSource === resolvedDest) {
|
|
144
177
|
throw new Error(`Source and destination are the same path. Use --name to provide a new name for the clone.`);
|
|
@@ -148,7 +181,7 @@ export async function akmClone(options) {
|
|
|
148
181
|
let destPath;
|
|
149
182
|
let overwritten;
|
|
150
183
|
let operationPaths;
|
|
151
|
-
if (
|
|
184
|
+
if (isSkill) {
|
|
152
185
|
const sourceSkillDir = path.dirname(sourcePath);
|
|
153
186
|
const destSkillDir = path.join(destRoot, typeDir, destName);
|
|
154
187
|
assertNoDestinationSymlinkParent(destRoot, destSkillDir);
|
|
@@ -163,6 +196,7 @@ export async function akmClone(options) {
|
|
|
163
196
|
const stagingDir = `${destSkillDir}.tmp-${randomUUID()}`;
|
|
164
197
|
try {
|
|
165
198
|
fs.cpSync(sourceSkillDir, stagingDir, { recursive: true, errorOnExist: true, force: false });
|
|
199
|
+
stampCloneOrigin(path.join(stagingDir, "SKILL.md"), originRef);
|
|
166
200
|
if (overwritten)
|
|
167
201
|
fs.rmSync(destSkillDir, { recursive: true, force: true });
|
|
168
202
|
fs.renameSync(stagingDir, destSkillDir);
|
|
@@ -175,7 +209,7 @@ export async function akmClone(options) {
|
|
|
175
209
|
operationPaths = [...new Set([...replacedPaths, ...listTreeFiles(destSkillDir)])];
|
|
176
210
|
}
|
|
177
211
|
else {
|
|
178
|
-
destPath = path.join(destRoot, typeDir,
|
|
212
|
+
destPath = path.join(destRoot, typeDir, destFileName);
|
|
179
213
|
assertNoDestinationSymlinkParent(destRoot, destPath);
|
|
180
214
|
const existing = lstatIfExists(destPath);
|
|
181
215
|
overwritten = existing !== undefined;
|
|
@@ -186,6 +220,8 @@ export async function akmClone(options) {
|
|
|
186
220
|
const stagingPath = `${destPath}.tmp-${randomUUID()}`;
|
|
187
221
|
try {
|
|
188
222
|
fs.copyFileSync(sourcePath, stagingPath, fs.constants.COPYFILE_EXCL);
|
|
223
|
+
if (cloneType !== "workflow" && sourceExt.toLowerCase() === ".md")
|
|
224
|
+
stampCloneOrigin(stagingPath, originRef);
|
|
189
225
|
if (overwritten)
|
|
190
226
|
fs.rmSync(destPath, { recursive: true, force: true });
|
|
191
227
|
fs.renameSync(stagingPath, destPath);
|
|
@@ -196,7 +232,7 @@ export async function akmClone(options) {
|
|
|
196
232
|
}
|
|
197
233
|
operationPaths = [destPath];
|
|
198
234
|
}
|
|
199
|
-
const ref = destinationRef(config, writeTarget,
|
|
235
|
+
const ref = destinationRef(config, writeTarget, cloneType, destName);
|
|
200
236
|
if (writeTarget) {
|
|
201
237
|
const commitRoot = writeTarget.source.repoPath ?? writeTarget.source.path;
|
|
202
238
|
const commitPaths = operationPaths.map((filePath) => path.relative(commitRoot, filePath).replaceAll(path.sep, "/"));
|
|
@@ -213,6 +249,82 @@ export async function akmClone(options) {
|
|
|
213
249
|
};
|
|
214
250
|
});
|
|
215
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Find an opaque concept (one whose id is not under a known type directory) in
|
|
254
|
+
* a source through the adapter that owns it, and name the type that adapter
|
|
255
|
+
* recognizes the file as. The same ownership + recognition `akm show` and the
|
|
256
|
+
* index use, so a clone sees exactly the file the index holds for the ref.
|
|
257
|
+
*/
|
|
258
|
+
function resolveOpaqueAsset(source, conceptId) {
|
|
259
|
+
const adapterId = source.adapterId ?? detectAdapterId(source.path);
|
|
260
|
+
const owner = resolveAdapterConceptOwner(source.path, adapterId, conceptId, { mode: "read" });
|
|
261
|
+
if (!owner)
|
|
262
|
+
return undefined;
|
|
263
|
+
let type;
|
|
264
|
+
try {
|
|
265
|
+
const component = { id: adapterId, adapter: adapterId, root: source.path, writable: false };
|
|
266
|
+
type = adapterForId(adapterId)?.recognize(component, buildFileContext(source.path, owner.path))?.type;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// An adapter that cannot recognize the file leaves its type unknown.
|
|
270
|
+
}
|
|
271
|
+
return { path: owner.realPath, type };
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Where an opaque asset is cloned: a skill (SKILL.md) as a directory under
|
|
275
|
+
* `skills/`, any other recognized placement type under its own directory, and
|
|
276
|
+
* markdown of an unknown type (a website or wiki page, a flat doc) as
|
|
277
|
+
* `knowledge/`. Anything else cannot be placed.
|
|
278
|
+
*/
|
|
279
|
+
function placementTypeForOpaque(sourceRef, sourcePath, indexedType) {
|
|
280
|
+
if (indexedType === "skill" && path.basename(sourcePath) === "SKILL.md")
|
|
281
|
+
return "skill";
|
|
282
|
+
if (indexedType !== undefined && indexedType !== "skill" && placementSpecFor(indexedType) !== undefined) {
|
|
283
|
+
return indexedType;
|
|
284
|
+
}
|
|
285
|
+
if (path.extname(sourcePath).toLowerCase() === ".md")
|
|
286
|
+
return "knowledge";
|
|
287
|
+
throw new UsageError(`cannot clone ${sourceRef}: its type${indexedType ? ` "${indexedType}"` : ""} has no place in a bundle and ` +
|
|
288
|
+
`${path.basename(sourcePath)} is not markdown; use \`akm import <path>\` or copy the file.`);
|
|
289
|
+
}
|
|
290
|
+
/** Clone name of an opaque asset: the last segment of its concept id, slugified like every other asset name. */
|
|
291
|
+
function slugName(conceptId) {
|
|
292
|
+
const last = conceptId.split("/").pop() ?? conceptId;
|
|
293
|
+
const stem = last.replace(/\.[A-Za-z0-9]{1,5}$/, "");
|
|
294
|
+
return inferAssetName("", "clone", stem);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Record where a cloned markdown asset came from by merging the origin ref into
|
|
298
|
+
* its `sources:` frontmatter. `sources:` is free-form provenance that `akm lint` does
|
|
299
|
+
* not resolve, unlike `xrefs:`: the origin bundle is often not installed where the
|
|
300
|
+
* clone is read (a shared domain bundle), and an `xrefs:` entry would be reported as
|
|
301
|
+
* a missing ref there. Only the `sources` block is rewritten; every other
|
|
302
|
+
* byte stays as written. A file whose frontmatter cannot be edited that way
|
|
303
|
+
* (and still parse back to exactly the intended mapping) is left untouched.
|
|
304
|
+
*/
|
|
305
|
+
function stampCloneOrigin(filePath, originRef) {
|
|
306
|
+
if (!originRef)
|
|
307
|
+
return;
|
|
308
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
309
|
+
const parsed = parseFrontmatter(raw);
|
|
310
|
+
const existing = parsed.data.sources;
|
|
311
|
+
const prior = Array.isArray(existing) ? existing : typeof existing === "string" ? [existing] : [];
|
|
312
|
+
if (existing !== undefined && prior.length === 0)
|
|
313
|
+
return;
|
|
314
|
+
if (prior.includes(originRef))
|
|
315
|
+
return;
|
|
316
|
+
const sources = [...prior, originRef];
|
|
317
|
+
let next;
|
|
318
|
+
if (parsed.frontmatter === null) {
|
|
319
|
+
next = `---\n${serializeFrontmatter({ sources })}\n---\n${raw}`;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
next = replaceFrontmatterBlocks(raw, ["sources"], serializeFrontmatter({ sources }).split("\n"));
|
|
323
|
+
if (next === null || !isDeepStrictEqual(parseFrontmatter(next).data, { ...parsed.data, sources }))
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
fs.writeFileSync(filePath, next);
|
|
327
|
+
}
|
|
216
328
|
function lstatIfExists(filePath) {
|
|
217
329
|
try {
|
|
218
330
|
return fs.lstatSync(filePath);
|