akm-cli 0.10.261020-beta → 0.10.261021
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 +29 -1
- package/STABILITY.md +0 -8
- package/dist/assets/templates/html/metrics.html +14 -8
- package/dist/assets/ui/overview.js +2 -2
- package/dist/commands/health/llm-usage.js +16 -2
- package/dist/commands/health/stash-exposure.js +23 -5
- package/dist/commands/health/type-directory-check.js +17 -49
- package/dist/commands/health.js +45 -15
- package/dist/commands/metrics/collect.js +9 -1
- package/dist/commands/metrics/metrics-cli.js +1 -1
- package/dist/commands/metrics/report-view.js +20 -9
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/core/common.js +4 -1
- package/dist/indexer/walk/matchers.js +10 -0
- package/dist/scripts/akm-migrate-node.js +56 -33
- package/dist/scripts/akm-migrate.js +56 -33
- package/dist/workflows/exec/param-secrets.js +29 -1
- package/docs/architecture/internals/health-advisories.md +1 -1
- package/docs/migration/release-notes/README.md +0 -1
- package/docs/reference/cli.md +2 -2
- package/package.json +1 -1
- package/docs/migration/release-notes/0.10.261020.md +0 -22
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## [0.10.
|
|
9
|
+
## [0.10.261021] - 2026-10-30
|
|
10
10
|
|
|
11
11
|
### Added
|
|
12
12
|
|
|
@@ -15,8 +15,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
15
15
|
writing backups. `--check` shows channel mismatches; an older target requires
|
|
16
16
|
`--allow-downgrade` (#1285).
|
|
17
17
|
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `akm metrics` replaces `usage.selectRate` with `usage.selectsPerSearch` and
|
|
21
|
+
adds `usage.totals.searchesWithHits`. Selects carry no source or search id, so
|
|
22
|
+
the figure is an unbounded ratio, not a conversion percentage: eight selects
|
|
23
|
+
after one search read 8.00, not 800% (#1293).
|
|
24
|
+
|
|
18
25
|
### Fixed
|
|
19
26
|
|
|
27
|
+
- A model call whose configured deadline is longer than five minutes is no
|
|
28
|
+
longer cut off at five minutes by Bun's socket-idle timer (#1295).
|
|
29
|
+
- Workflow params that are ordinary file paths (absolute, relative, Windows,
|
|
30
|
+
with dates, digits or a UUID directory) are no longer warned about or redacted
|
|
31
|
+
as secrets; credential-named keys, known token prefixes and token-shaped path
|
|
32
|
+
segments still are (#1292).
|
|
33
|
+
- `akm bundle add --writable` on a bundle installed read-only from Git makes a
|
|
34
|
+
fresh checkout instead of failing with "is not a checkout", keeping the
|
|
35
|
+
bundle's name, components, order and credential (#1296).
|
|
36
|
+
- `akm health` checks every configured git bundle for tracked `env/` or
|
|
37
|
+
`secrets/` files with a remote, not only the working stash, and names the
|
|
38
|
+
bundle in each warning. A bundle whose `.gitignore` carries
|
|
39
|
+
`# akm: intentionally track env and secrets` is not reported; the warning
|
|
40
|
+
names that line as the way to acknowledge intentional tracking (#1291).
|
|
41
|
+
- `akm health`'s `type-directory-disagreement` warning uses the indexer's own
|
|
42
|
+
directory rules, so `scripts/README.md` or `knowledge/example.sh` no longer
|
|
43
|
+
warn, and it checks only `akm`-adapter entries inside their component root (#1288).
|
|
44
|
+
- `akm metrics` reports token usage as unknown, with the number of calls that
|
|
45
|
+
reported it, instead of zero when the provider sent none. A call reporting
|
|
46
|
+
prompt and completion tokens but no total now counts their sum in the LLM
|
|
47
|
+
usage aggregate `akm health` shares (#1294).
|
|
20
48
|
- Claude plugin health uses native installation, scope and enabled metadata
|
|
21
49
|
instead of choosing the highest cached version. It reads `AKM_MIN_VERSION`
|
|
22
50
|
as well as the legacy range, and reports uncertain selections as unknown (#1286).
|
package/STABILITY.md
CHANGED
|
@@ -4,14 +4,6 @@
|
|
|
4
4
|
**with one caveat**: while the version is 0.x, minor releases (0.x → 0.x+1)
|
|
5
5
|
may include breaking changes.
|
|
6
6
|
|
|
7
|
-
**0.9.x series exception.** The 0.9.x releases are a deliberate refactoring
|
|
8
|
-
and clean-up series: the goal is to pay off all remaining technical debt and
|
|
9
|
-
land every planned breaking change before 0.10. While that work completes,
|
|
10
|
-
**0.9.x patch releases may also contain breaking changes** — each one called
|
|
11
|
-
out in the CHANGELOG with a migration note. The 0.10.x series returns to bug
|
|
12
|
-
fixes and tuning, and aims to restore the normal discipline of breaking
|
|
13
|
-
changes only in major and minor releases.
|
|
14
|
-
|
|
15
7
|
**Version floor (0.10, #1091).** 0.10 reads what 0.9.15 and later wrote: config,
|
|
16
8
|
`state.db`, task sources, frozen workflow plans and scheduler rows. It does not
|
|
17
9
|
convert anything older. An install on an earlier release runs `akm migrate
|
|
@@ -363,17 +363,19 @@ function aggregate(rows, opts) {
|
|
|
363
363
|
// and carry no ref, source or event type, so only the date range applies.
|
|
364
364
|
function aggregateLlm(rows, f) {
|
|
365
365
|
var by = { stage: Object.create(null), process: Object.create(null), engine: Object.create(null) }, daily = new Map();
|
|
366
|
-
var t = { calls: 0, tokens: 0, failures: 0, durationMs: 0 };
|
|
366
|
+
var t = { calls: 0, tokens: 0, failures: 0, durationMs: 0, usageCalls: 0 };
|
|
367
367
|
rows.forEach(function (r) {
|
|
368
368
|
var d = dayOf(r.day);
|
|
369
369
|
if (f.from && d < f.from) return;
|
|
370
370
|
if (f.to && d > f.to) return;
|
|
371
371
|
var tok = tokensOf(r), calls = r.calls || 0, failed = r.outcome === 'error' ? calls : 0;
|
|
372
|
-
|
|
372
|
+
// Rows from a report that predates usage coverage count as fully measured.
|
|
373
|
+
var measured = r.usageCalls != null ? r.usageCalls : calls;
|
|
374
|
+
t.calls += calls; t.tokens += tok; t.durationMs += r.durationMs || 0; t.failures += failed; t.usageCalls += measured;
|
|
373
375
|
[['stage', r.stage], ['process', r.process], ['engine', r.engine]].forEach(function (p) {
|
|
374
376
|
var k = p[1] || 'unattributed';
|
|
375
|
-
var e = by[p[0]][k] || (by[p[0]][k] = { name: k, calls: 0, tokens: 0, failures: 0 });
|
|
376
|
-
e.calls += calls; e.tokens += tok; e.failures += failed;
|
|
377
|
+
var e = by[p[0]][k] || (by[p[0]][k] = { name: k, calls: 0, tokens: 0, failures: 0, usageCalls: 0 });
|
|
378
|
+
e.calls += calls; e.tokens += tok; e.failures += failed; e.usageCalls += measured;
|
|
377
379
|
});
|
|
378
380
|
var day = daily.get(d) || { day: d, calls: 0, durationMs: 0 };
|
|
379
381
|
day.calls += calls; day.durationMs += r.durationMs || 0;
|
|
@@ -621,7 +623,7 @@ function toCsv(columns, rows) {
|
|
|
621
623
|
var agg = useRows ? aggregateLlm(LLM, { from: state.from, to: state.to }) : null;
|
|
622
624
|
$('llm-hint').textContent = useRows ? 'follows the date range only' : 'window-wide aggregate (no per-call rows)';
|
|
623
625
|
function fromAggregate(o) {
|
|
624
|
-
return Object.keys(o || {}).map(function (k) { return { name: k, tokens: o[k].totalTokens, calls: o[k].calls, failures: o[k].failures }; })
|
|
626
|
+
return Object.keys(o || {}).map(function (k) { return { name: k, tokens: o[k].totalTokens, calls: o[k].calls, failures: o[k].failures, usageCalls: o[k].usageCalls != null ? o[k].usageCalls : o[k].calls }; })
|
|
625
627
|
.sort(function (a, b) { return b.tokens - a.tokens; });
|
|
626
628
|
}
|
|
627
629
|
var dims = [
|
|
@@ -633,7 +635,7 @@ function toCsv(columns, rows) {
|
|
|
633
635
|
var list = d[1].slice(0, 15);
|
|
634
636
|
if (!hasData(d[0], list.length > 0, 'No LLM calls recorded')) return;
|
|
635
637
|
chart(d[0], Object.assign(barOption(list.map(function (x) { return x.name; }),
|
|
636
|
-
[{ name: 'tokens', type: 'bar', itemStyle: { color: css('--purple') }, data: list.map(function (x) { return x.tokens; }) }], true), {}));
|
|
638
|
+
[{ name: 'tokens', type: 'bar', itemStyle: { color: css('--purple') }, data: list.map(function (x) { return x.calls > 0 && x.usageCalls === 0 ? null : x.tokens; }) }], true), {}));
|
|
637
639
|
});
|
|
638
640
|
if (hasData('ch-llm-time', useRows && agg.daily.length > 0, 'No per-call rows in this report')) {
|
|
639
641
|
var days = agg.daily.map(function (d) { return d.day; });
|
|
@@ -744,15 +746,19 @@ function toCsv(columns, rows) {
|
|
|
744
746
|
var t = agg.totals;
|
|
745
747
|
var tokens = llm ? llm.totals.tokens : DATA.llm.totalTokens;
|
|
746
748
|
var calls = llm ? llm.totals.calls : DATA.llm.calls;
|
|
749
|
+
var usageCalls = llm ? llm.totals.usageCalls : (DATA.llm.usageCalls != null ? DATA.llm.usageCalls : calls);
|
|
750
|
+
// Calls whose provider sent no usage are unknown, not zero tokens.
|
|
751
|
+
var llmSub = fmtN(calls) + ' calls' + (usageCalls < calls ? ', ' + fmtN(usageCalls) + ' reported usage' : '');
|
|
747
752
|
var tasks = DATA.tasks || {};
|
|
748
753
|
var cards = [
|
|
749
754
|
['Searches', fmtN(t.searches), fmtN(t.curates) + ' curates', ''],
|
|
750
755
|
['Shows', fmtN(t.shows), '', ''],
|
|
751
|
-
['
|
|
756
|
+
['Selects per search', DATA.usage.selectsPerSearch == null ? '—' : DATA.usage.selectsPerSearch.toFixed(2),
|
|
757
|
+
fmtN(DATA.usage.totals.selects) + ' selects / ' + fmtN(DATA.usage.totals.searchesWithHits) + ' searches with hits, window-wide, selects not source-filtered', ''],
|
|
752
758
|
['Zero-result rate', fmtPct(t.searches ? t.zeroResult / t.searches : null), fmtN(t.zeroResult) + ' searches', t.zeroResult ? 'yellow' : ''],
|
|
753
759
|
['Feedback', '<span class="pos">+' + fmtN(t.positive) + '</span> / <span class="neg">−' + fmtN(t.negative) + '</span>',
|
|
754
760
|
'net ' + (t.positive - t.negative >= 0 ? '+' : '−') + Math.abs(t.positive - t.negative), ''],
|
|
755
|
-
['LLM tokens', fmtN(tokens),
|
|
761
|
+
['LLM tokens', calls > 0 && usageCalls === 0 ? 'unknown' : fmtN(tokens), llmSub, usageCalls < calls ? 'yellow' : ''],
|
|
756
762
|
['Task fail rate', fmtPct(tasks.failRate), fmtN(tasks.failed) + ' of ' + fmtN(tasks.runs) + ' runs, window-wide', tasks.failed ? 'red' : '']
|
|
757
763
|
];
|
|
758
764
|
$('kpis').innerHTML = cards.map(function (c) {
|
|
@@ -70,10 +70,10 @@ function metricsSection(m) {
|
|
|
70
70
|
${tile("Searches", fmtNum(u.totals.searches), `${fmtNum(u.totals.zeroResultSearches)} with no results`)}
|
|
71
71
|
${tile("Shows", fmtNum(u.totals.shows), `${fmtNum(u.totals.distinctAssets)} distinct assets`)}
|
|
72
72
|
${tile("Curates", fmtNum(u.totals.curates))}
|
|
73
|
-
${tile("
|
|
73
|
+
${tile("Selects per search", u.selectsPerSearch == null ? "—" : u.selectsPerSearch.toFixed(2), `${fmtNum(u.totals.selects)} selects / ${fmtNum(u.totals.searchesWithHits)} searches with hits`)}
|
|
74
74
|
${tile("Median search", fmtMs(u.searchMedianMs))}
|
|
75
75
|
${tile("Feedback", `+${fmtNum(m.feedback.totals.positive)} / -${fmtNum(m.feedback.totals.negative)}`, "positive / negative")}
|
|
76
|
-
${tile("LLM calls", fmtNum(m.llm.calls), `${fmtNum(m.llm.failures)} failed, ${fmtNum(m.llm.totalTokens)} tokens`)}
|
|
76
|
+
${tile("LLM calls", fmtNum(m.llm.calls), `${fmtNum(m.llm.failures)} failed, ${m.llm.calls > 0 && m.llm.usageCalls === 0 ? "unknown" : fmtNum(m.llm.totalTokens)} tokens`)}
|
|
77
77
|
${tile("Pending proposals", fmtNum(m.proposals.byStatus.pending ?? 0))}
|
|
78
78
|
${tile("Task runs", fmtNum(m.tasks.runs), `${fmtNum(m.tasks.failed)} failed`)}
|
|
79
79
|
${tile("Index builds", fmtNum(m.index.runs), `median ${fmtMs(m.index.medianMs)}`)}
|
|
@@ -21,6 +21,13 @@ function emptyLlmUsageStageAggregate() {
|
|
|
21
21
|
failures: 0,
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
/** True when the provider response carried any token count. Absent usage is unknown, not zero. */
|
|
25
|
+
export function hasTokenUsage(record) {
|
|
26
|
+
return (record.promptTokens !== undefined ||
|
|
27
|
+
record.completionTokens !== undefined ||
|
|
28
|
+
record.totalTokens !== undefined ||
|
|
29
|
+
record.reasoningTokens !== undefined);
|
|
30
|
+
}
|
|
24
31
|
/** A zeroed aggregate — also the value health reports when it could not read state.db at all (#791). */
|
|
25
32
|
export function emptyLlmUsageAggregate() {
|
|
26
33
|
return { ...emptyLlmUsageStageAggregate(), byStage: {}, byProcess: {}, byEngine: {} };
|
|
@@ -28,7 +35,7 @@ export function emptyLlmUsageAggregate() {
|
|
|
28
35
|
/**
|
|
29
36
|
* Aggregate `llm_usage` events (#576) into a window total plus a per-stage
|
|
30
37
|
* breakdown of call count, wall-time, and token usage. Token fields absent from
|
|
31
|
-
* a best-effort record
|
|
38
|
+
* a best-effort record add nothing and the call is left out of `usageCalls`. Calls with no `stage` land under
|
|
32
39
|
* {@link UNATTRIBUTED_STAGE}.
|
|
33
40
|
*/
|
|
34
41
|
export function summarizeLlmUsage(events) {
|
|
@@ -47,11 +54,18 @@ export function summarizeLlmUsage(events) {
|
|
|
47
54
|
dimensions.push(groups[key]);
|
|
48
55
|
}
|
|
49
56
|
for (const target of [aggregate, ...dimensions]) {
|
|
57
|
+
target.usageCalls ??= 0;
|
|
58
|
+
target.reasoningCalls ??= 0;
|
|
50
59
|
target.calls += 1;
|
|
60
|
+
if (hasTokenUsage(record))
|
|
61
|
+
target.usageCalls += 1;
|
|
62
|
+
if (record.reasoningTokens !== undefined)
|
|
63
|
+
target.reasoningCalls += 1;
|
|
51
64
|
target.totalDurationMs += record.durationMs;
|
|
52
65
|
target.promptTokens += record.promptTokens ?? 0;
|
|
53
66
|
target.completionTokens += record.completionTokens ?? 0;
|
|
54
|
-
|
|
67
|
+
// A call without a total counts as prompt + completion, as the per-day metrics rows do.
|
|
68
|
+
target.totalTokens += record.totalTokens ?? (record.promptTokens ?? 0) + (record.completionTokens ?? 0);
|
|
55
69
|
target.reasoningTokens += record.reasoningTokens ?? 0;
|
|
56
70
|
if (record.outcome === "error")
|
|
57
71
|
target.failures += 1;
|
|
@@ -9,18 +9,35 @@
|
|
|
9
9
|
* is when secret assets are git-TRACKED **and** a remote is configured — only
|
|
10
10
|
* then can a `git push` exfiltrate tokens/keys. Warn on exactly that
|
|
11
11
|
* combination; stay silent on the tracked-but-no-remote opt-in so the advisory
|
|
12
|
-
* catches the exposure without nagging the intentional backup.
|
|
12
|
+
* catches the exposure without nagging the intentional backup. A stash whose
|
|
13
|
+
* `.gitignore` carries the intentional-tracking marker has acknowledged the
|
|
14
|
+
* risk, so it is not warned about at all.
|
|
13
15
|
*/
|
|
14
16
|
import { spawnSync } from "node:child_process";
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { STASH_GITIGNORE_TRACK_SECRETS_MARKER } from "../sources/stash-skeleton.js";
|
|
15
20
|
const realGit = (stashDir, args) => {
|
|
16
21
|
const result = spawnSync("git", ["-C", stashDir, ...args], { encoding: "utf8", timeout: 5_000 });
|
|
17
22
|
return { ok: result.status === 0, stdout: (result.stdout ?? "").trim() };
|
|
18
23
|
};
|
|
24
|
+
/** True when the stash `.gitignore` carries the explicit intentional-tracking marker. */
|
|
25
|
+
function hasIntentionalTrackingMarker(stashDir) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.readFileSync(path.join(stashDir, ".gitignore"), "utf8").includes(STASH_GITIGNORE_TRACK_SECRETS_MARKER);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
19
33
|
/**
|
|
20
34
|
* Build the `stash-git-exposure` advisory, or `undefined` when there is nothing
|
|
21
|
-
* to warn about (
|
|
35
|
+
* to warn about (tracking acknowledged by the marker, not a git repo, no
|
|
36
|
+
* tracked env/secret assets, or no remote).
|
|
22
37
|
*/
|
|
23
38
|
export function collectStashExposureAdvisory(stashDir, git = realGit) {
|
|
39
|
+
if (hasIntentionalTrackingMarker(stashDir))
|
|
40
|
+
return undefined;
|
|
24
41
|
// Not a git work tree → nothing to expose.
|
|
25
42
|
if (!git(stashDir, ["rev-parse", "--is-inside-work-tree"]).ok)
|
|
26
43
|
return undefined;
|
|
@@ -37,10 +54,11 @@ export function collectStashExposureAdvisory(stashDir, git = realGit) {
|
|
|
37
54
|
kind: "deterministic",
|
|
38
55
|
status: "warn",
|
|
39
56
|
confidence: "high",
|
|
40
|
-
message:
|
|
57
|
+
message: `Bundle at ${stashDir}: ${trackedFiles.length} env/secret file(s) are git-tracked AND a remote is configured — ` +
|
|
41
58
|
`a 'git push' can leak tokens/keys (${preview}). ` +
|
|
42
59
|
"Run 'git rm --cached' on them (a .gitignore rule alone does NOT untrack already-tracked " +
|
|
43
|
-
"files) and then add env/+secrets/ to .gitignore to prevent recurrence (akm bundle create scaffolds it)
|
|
44
|
-
|
|
60
|
+
"files) and then add env/+secrets/ to .gitignore to prevent recurrence (akm bundle create scaffolds it), " +
|
|
61
|
+
`or, to keep tracking them on purpose, add '${STASH_GITIGNORE_TRACK_SECRETS_MARKER}' to .gitignore.`,
|
|
62
|
+
evidence: { bundleRoot: stashDir, trackedSecretFiles: trackedFiles },
|
|
45
63
|
};
|
|
46
64
|
}
|
|
@@ -22,28 +22,21 @@
|
|
|
22
22
|
import fs from "node:fs";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
25
|
+
import { buildFileContext } from "../../indexer/walk/file-context.js";
|
|
26
|
+
import { declaredTypeForPath } from "../../indexer/walk/matchers.js";
|
|
25
27
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* extensions it accepts. Keep in sync if `DIR_TYPE_MAP` gains, renames, or
|
|
30
|
-
* removes a directory.
|
|
28
|
+
* The type the entry's location declares under the `akm` stash rules, derived
|
|
29
|
+
* with the indexer's own directory predicates (see `declaredTypeForPath`).
|
|
30
|
+
* Other adapters, and paths outside the component root, declare nothing.
|
|
31
31
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
sessions: "session",
|
|
41
|
-
instructions: "instruction",
|
|
42
|
-
scripts: "script",
|
|
43
|
-
env: "env",
|
|
44
|
-
secrets: "secret",
|
|
45
|
-
tasks: "task",
|
|
46
|
-
};
|
|
32
|
+
function declaredTypeFor(entry) {
|
|
33
|
+
if (entry.adapterId !== "akm" || !entry.root)
|
|
34
|
+
return undefined;
|
|
35
|
+
const rel = path.relative(entry.root, entry.filePath);
|
|
36
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel))
|
|
37
|
+
return undefined;
|
|
38
|
+
return declaredTypeForPath(buildFileContext(entry.root, entry.filePath));
|
|
39
|
+
}
|
|
47
40
|
const realReadFile = (absPath) => {
|
|
48
41
|
try {
|
|
49
42
|
return fs.readFileSync(absPath, "utf8");
|
|
@@ -52,31 +45,6 @@ const realReadFile = (absPath) => {
|
|
|
52
45
|
return undefined;
|
|
53
46
|
}
|
|
54
47
|
};
|
|
55
|
-
/**
|
|
56
|
-
* The type a file's directory declares, mirroring the `classifyByDirectory` /
|
|
57
|
-
* `classifyByParentDirHint` precedence in matchers.ts: the immediate parent
|
|
58
|
-
* directory wins when it is itself typed (parentDirHint, specificity 15);
|
|
59
|
-
* otherwise the outermost typed ancestor wins (directoryMatcher, specificity
|
|
60
|
-
* 10, which walks root-to-leaf and returns on the first hit).
|
|
61
|
-
*/
|
|
62
|
-
function declaredTypeForPath(absPath) {
|
|
63
|
-
const segments = path
|
|
64
|
-
.dirname(absPath)
|
|
65
|
-
.split(path.sep)
|
|
66
|
-
.filter((seg) => seg.length > 0);
|
|
67
|
-
const immediateParent = segments.at(-1);
|
|
68
|
-
if (immediateParent) {
|
|
69
|
-
const parentType = DECLARED_DIR_TYPES[immediateParent];
|
|
70
|
-
if (parentType)
|
|
71
|
-
return { dir: immediateParent, type: parentType };
|
|
72
|
-
}
|
|
73
|
-
for (const seg of segments) {
|
|
74
|
-
const type = DECLARED_DIR_TYPES[seg];
|
|
75
|
-
if (type)
|
|
76
|
-
return { dir: seg, type };
|
|
77
|
-
}
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
48
|
/**
|
|
81
49
|
* Best-effort explanation for why `classifyBySmartMd` (matchers.ts) would
|
|
82
50
|
* have produced `resolvedType` for this content, in the SAME precedence
|
|
@@ -110,7 +78,7 @@ function explainOverride(resolvedType, content) {
|
|
|
110
78
|
}
|
|
111
79
|
/**
|
|
112
80
|
* Compare every indexed entry's resolved type against the type its
|
|
113
|
-
* directory declares (see {@link
|
|
81
|
+
* directory declares (see {@link declaredTypeFor}), and return one
|
|
114
82
|
* {@link TypeDirectoryDisagreement} per mismatch, sorted by path. Entries
|
|
115
83
|
* outside any declared-type directory are not checked — this is only the
|
|
116
84
|
* "directory declares type" invariant.
|
|
@@ -118,15 +86,15 @@ function explainOverride(resolvedType, content) {
|
|
|
118
86
|
export function collectTypeDirectoryDisagreements(entries, readFile = realReadFile) {
|
|
119
87
|
const disagreements = [];
|
|
120
88
|
for (const entry of entries) {
|
|
121
|
-
const declared =
|
|
122
|
-
if (!declared || declared
|
|
89
|
+
const declared = declaredTypeFor(entry);
|
|
90
|
+
if (!declared || declared === entry.type)
|
|
123
91
|
continue;
|
|
124
92
|
const content = readFile(entry.filePath);
|
|
125
93
|
const explanation = content === undefined ? undefined : explainOverride(entry.type, content);
|
|
126
94
|
disagreements.push({
|
|
127
95
|
path: entry.filePath,
|
|
128
96
|
resolved: entry.type,
|
|
129
|
-
expected: declared
|
|
97
|
+
expected: declared,
|
|
130
98
|
winner: explanation?.winner ?? "unknown",
|
|
131
99
|
knownGoodOverride: explanation?.knownGoodOverride ?? false,
|
|
132
100
|
});
|
package/dist/commands/health.js
CHANGED
|
@@ -10,6 +10,7 @@ import { classifyPathAccess, describeInaccessiblePath } from "../core/path-acces
|
|
|
10
10
|
import { getConfigPath, getDataDir, getDbPath, getStateDbPathInDataDir } from "../core/paths.js";
|
|
11
11
|
import { listExistingTableNames, listPendingStateMigrations, openStateDatabaseWithReport, withStateDb, } from "../core/state-db.js";
|
|
12
12
|
import { DURATION_UNITS, parseDuration, parseSinceToIso } from "../core/time.js";
|
|
13
|
+
import { resolveSourceEntries } from "../indexer/search/search-source.js";
|
|
13
14
|
import { probeLlmEndpoint } from "../llm/client.js";
|
|
14
15
|
import { openDatabase } from "../storage/database.js";
|
|
15
16
|
import { insertEvent } from "../storage/repositories/events-repository.js";
|
|
@@ -251,24 +252,36 @@ function gatherAncillaryAdvisories(db, options, egressConfigView) {
|
|
|
251
252
|
const indexStateMismatch = detectIndexStateGenerationMismatch(db);
|
|
252
253
|
if (indexStateMismatch)
|
|
253
254
|
advisories.push(indexStateMismatch);
|
|
254
|
-
// Surface a `stash-git-exposure` advisory
|
|
255
|
-
// git-tracked AND a remote
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
255
|
+
// Surface a `stash-git-exposure` advisory for each bundle whose env/secret
|
|
256
|
+
// assets are git-tracked AND that has a remote (the leak moment). Every
|
|
257
|
+
// configured bundle is checked, not just the working stash; each honours the
|
|
258
|
+
// intentional-tracking marker in its own `.gitignore`. Best-effort.
|
|
259
|
+
// Cheap guard: only shell out to git for a bundle root with its OWN `.git`
|
|
260
|
+
// (or when a test injected a fake seam), so the hot path never spawns for
|
|
261
|
+
// non-git bundles. Trade-off: a bundle pointed at a bare subdirectory of a
|
|
262
|
+
// parent git repo (no `.git` of its own) is not checked; akm-init and
|
|
263
|
+
// writable git installs put `.git` at the bundle root.
|
|
264
|
+
let exposureRoots = [];
|
|
262
265
|
try {
|
|
263
266
|
const exposureStashDir = options.stashDir ?? resolveStashDir();
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (stashExposure)
|
|
267
|
-
advisories.push(stashExposure);
|
|
268
|
-
}
|
|
267
|
+
exposureRoots = [exposureStashDir];
|
|
268
|
+
exposureRoots = resolveSourceEntries(exposureStashDir).map((source) => source.path);
|
|
269
269
|
}
|
|
270
270
|
catch {
|
|
271
|
-
//
|
|
271
|
+
// An unreadable working stash skips the advisory; an unreadable bundle
|
|
272
|
+
// config still checks the working stash. Neither aborts the report.
|
|
273
|
+
}
|
|
274
|
+
for (const root of new Set(exposureRoots.map((dir) => path.resolve(dir)))) {
|
|
275
|
+
try {
|
|
276
|
+
if (options.stashExposureGit || fs.existsSync(path.join(root, ".git"))) {
|
|
277
|
+
const stashExposure = collectStashExposureAdvisory(root, options.stashExposureGit);
|
|
278
|
+
if (stashExposure)
|
|
279
|
+
advisories.push(stashExposure);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// Non-fatal — a git/probe failure must not abort the health report.
|
|
284
|
+
}
|
|
272
285
|
}
|
|
273
286
|
// 08 surfaces: the remaining read-only advisory group (binary-config-skew,
|
|
274
287
|
// egress-endpoints). Best-effort — a filesystem probe failure must not abort
|
|
@@ -346,7 +359,24 @@ function detectTypeDirectoryDisagreements(stashRoot) {
|
|
|
346
359
|
indexDb = openReadonlyExistingDatabase(getDbPath());
|
|
347
360
|
if (!indexDb)
|
|
348
361
|
return undefined;
|
|
349
|
-
|
|
362
|
+
// Directory intent is read only from inside the component root that owns the entry.
|
|
363
|
+
// Both spellings of each root: indexed paths may be realpaths (a symlinked
|
|
364
|
+
// stash, macOS /var -> /private/var) while config holds the configured path.
|
|
365
|
+
const roots = resolveSourceEntries(stashRoot).flatMap((source) => {
|
|
366
|
+
const resolved = path.resolve(source.path);
|
|
367
|
+
try {
|
|
368
|
+
return [resolved, fs.realpathSync.native(resolved)];
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return [resolved];
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
const entries = getAllEntries(indexDb).map((entry) => ({
|
|
375
|
+
filePath: entry.filePath,
|
|
376
|
+
type: entry.type,
|
|
377
|
+
adapterId: entry.adapterId,
|
|
378
|
+
root: roots.filter((root) => entry.filePath.startsWith(root + path.sep)).sort((a, b) => b.length - a.length)[0] ?? "",
|
|
379
|
+
}));
|
|
350
380
|
return buildTypeDirectoryAdvisory(entries, undefined, (absPath) => absPath.startsWith(stashRoot) ? path.relative(stashRoot, absPath) : absPath);
|
|
351
381
|
}
|
|
352
382
|
catch {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { decodeLlmUsageRecord } from "../../llm/usage-telemetry.js";
|
|
5
5
|
import { decodeTaskHistoryMetadata } from "../../storage/repositories/task-history-repository.js";
|
|
6
6
|
import { computeWallTimeStats } from "../health/improve-metrics.js";
|
|
7
|
+
import { hasTokenUsage } from "../health/llm-usage.js";
|
|
7
8
|
import { computeValenceScore } from "../improve/feedback-valence.js";
|
|
8
9
|
const DAY_MS = 86_400_000;
|
|
9
10
|
/** Utility is a score in [0, 1], reported in ten equal buckets. */
|
|
@@ -111,6 +112,8 @@ export function llmRowsFromEvents(events) {
|
|
|
111
112
|
completionTokens: 0,
|
|
112
113
|
totalTokens: 0,
|
|
113
114
|
reasoningTokens: 0,
|
|
115
|
+
usageCalls: 0,
|
|
116
|
+
reasoningCalls: 0,
|
|
114
117
|
};
|
|
115
118
|
rows.set(key, row);
|
|
116
119
|
}
|
|
@@ -121,6 +124,10 @@ export function llmRowsFromEvents(events) {
|
|
|
121
124
|
// A call without a total counts as prompt + completion, as the dashboard did per call.
|
|
122
125
|
row.totalTokens += record.totalTokens ?? (record.promptTokens ?? 0) + (record.completionTokens ?? 0);
|
|
123
126
|
row.reasoningTokens += record.reasoningTokens ?? 0;
|
|
127
|
+
if (hasTokenUsage(record))
|
|
128
|
+
row.usageCalls += 1;
|
|
129
|
+
if (record.reasoningTokens !== undefined)
|
|
130
|
+
row.reasoningCalls += 1;
|
|
124
131
|
}
|
|
125
132
|
return [...rows.values()];
|
|
126
133
|
}
|
|
@@ -260,10 +267,11 @@ function buildUsage(input, rows) {
|
|
|
260
267
|
curates,
|
|
261
268
|
selects: input.selects.length,
|
|
262
269
|
zeroResultSearches,
|
|
270
|
+
searchesWithHits,
|
|
263
271
|
distinctAssets: assets.size,
|
|
264
272
|
distinctQueries: queries.size,
|
|
265
273
|
},
|
|
266
|
-
|
|
274
|
+
selectsPerSearch: searchesWithHits > 0 ? input.selects.length / searchesWithHits : null,
|
|
267
275
|
searchMedianMs: median(searchMs),
|
|
268
276
|
daily: [...days.values()].sort((a, b) => a.day.localeCompare(b.day)),
|
|
269
277
|
topAssets: [...assets.values()]
|
|
@@ -131,7 +131,7 @@ export async function akmMetrics(options = {}) {
|
|
|
131
131
|
if (index.note)
|
|
132
132
|
notes.push(index.note);
|
|
133
133
|
const filter = { sinceIso, untilIso, source: SOURCE };
|
|
134
|
-
notes.push("Selects come from the events stream, which records no source, so they are counted for every source.");
|
|
134
|
+
notes.push("Selects come from the events stream, which records no source or search id, so they are counted for every source and selects per search is a ratio, not a conversion rate.");
|
|
135
135
|
const input = {
|
|
136
136
|
window: { since: sinceIso, until: untilIso },
|
|
137
137
|
source: SOURCE,
|
|
@@ -20,18 +20,27 @@ function sourceFacts(counts) {
|
|
|
20
20
|
const entries = Object.entries(counts);
|
|
21
21
|
return entries.length === 0 ? "-" : entries.map(([k, v]) => `${k}=${v}`).join(" ");
|
|
22
22
|
}
|
|
23
|
+
/** A token sum, or `unknown` when calls were made and none reported usage (never a measured zero). */
|
|
24
|
+
function tokens(n, a) {
|
|
25
|
+
return a.calls > 0 && a.usageCalls === 0 ? "unknown" : int(n);
|
|
26
|
+
}
|
|
27
|
+
/** `reported/calls`; `-` for an aggregate that carries no coverage. */
|
|
28
|
+
function usageCoverage(a) {
|
|
29
|
+
return a.usageCalls === undefined ? "-" : `${a.usageCalls}/${a.calls}`;
|
|
30
|
+
}
|
|
23
31
|
function llmRows(group) {
|
|
24
32
|
return Object.entries(group).map(([name, a]) => [
|
|
25
33
|
name,
|
|
26
34
|
int(a.calls),
|
|
27
35
|
int(a.failures),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
tokens(a.promptTokens, a),
|
|
37
|
+
tokens(a.completionTokens, a),
|
|
38
|
+
tokens(a.totalTokens, a),
|
|
39
|
+
usageCoverage(a),
|
|
31
40
|
ms(a.totalDurationMs),
|
|
32
41
|
]);
|
|
33
42
|
}
|
|
34
|
-
const LLM_HEADERS = ["name", "calls", "failed", "prompt", "completion", "total", "time"];
|
|
43
|
+
const LLM_HEADERS = ["name", "calls", "failed", "prompt", "completion", "total", "calls with usage", "time"];
|
|
35
44
|
export function buildMetricsView(r, detail) {
|
|
36
45
|
const brief = detail === "brief";
|
|
37
46
|
const cut = (items) => (brief ? items.slice(0, BRIEF_LIST_LIMIT) : items);
|
|
@@ -45,7 +54,8 @@ export function buildMetricsView(r, detail) {
|
|
|
45
54
|
["shows", int(usage.totals.shows)],
|
|
46
55
|
["curates", int(usage.totals.curates)],
|
|
47
56
|
["selects", int(usage.totals.selects)],
|
|
48
|
-
["
|
|
57
|
+
["searches with hits", int(usage.totals.searchesWithHits)],
|
|
58
|
+
["selects per search with hits", fixed(usage.selectsPerSearch, 2)],
|
|
49
59
|
[
|
|
50
60
|
"zero-result searches",
|
|
51
61
|
`${usage.totals.zeroResultSearches} (${pct(searches === 0 ? null : usage.totals.zeroResultSearches / searches)})`,
|
|
@@ -154,10 +164,11 @@ export function buildMetricsView(r, detail) {
|
|
|
154
164
|
facts: [
|
|
155
165
|
["calls", int(llm.calls)],
|
|
156
166
|
["failed", int(llm.failures)],
|
|
157
|
-
["
|
|
158
|
-
["
|
|
159
|
-
["
|
|
160
|
-
["
|
|
167
|
+
["calls with token usage", usageCoverage(llm)],
|
|
168
|
+
["prompt tokens", tokens(llm.promptTokens, llm)],
|
|
169
|
+
["completion tokens", tokens(llm.completionTokens, llm)],
|
|
170
|
+
["reasoning tokens", llm.calls > 0 && llm.reasoningCalls === 0 ? "unknown" : tokens(llm.reasoningTokens, llm)],
|
|
171
|
+
["total tokens", tokens(llm.totalTokens, llm)],
|
|
161
172
|
["time", ms(llm.totalDurationMs)],
|
|
162
173
|
],
|
|
163
174
|
tables: [
|
|
@@ -219,7 +219,9 @@ async function addRegistryStash(ref, stashDir, explicitName, writable, position
|
|
|
219
219
|
: [];
|
|
220
220
|
const synced = await syncFromRef(ref, {
|
|
221
221
|
writable: effectiveWritable,
|
|
222
|
-
|
|
222
|
+
// Reuse a checkout only when the install is already writable; promoting a
|
|
223
|
+
// read-only snapshot (#1296) materializes a fresh checkout instead.
|
|
224
|
+
...(existingWritable && priorLock?.localRoot ? { writableRoot: priorLock.localRoot } : {}),
|
|
223
225
|
...(requiredRoots.length > 0 ? { writableRequiredRoots: requiredRoots } : {}),
|
|
224
226
|
...(credentialRef
|
|
225
227
|
? { credential: resolveSecret(credentialRef, storeSecretResolver.resolveSecret) }
|
|
@@ -253,8 +255,10 @@ async function addRegistryStash(ref, stashDir, explicitName, writable, position
|
|
|
253
255
|
localRoot: synced.contentDir,
|
|
254
256
|
installedAt: synced.syncedAt,
|
|
255
257
|
});
|
|
256
|
-
// Clean up the old materialized root on re-install (moved cache).
|
|
257
|
-
|
|
258
|
+
// Clean up the old materialized root on re-install (moved cache). A writable
|
|
259
|
+
// checkout may hold local work and is never removed; a read-only snapshot
|
|
260
|
+
// (including one just promoted to writable, #1296) is.
|
|
261
|
+
if (!existingWritable && priorLocalRoot && path.resolve(priorLocalRoot) !== path.resolve(synced.contentDir)) {
|
|
258
262
|
try {
|
|
259
263
|
fs.rmSync(priorLocalRoot, { recursive: true, force: true });
|
|
260
264
|
}
|
package/dist/core/common.js
CHANGED
|
@@ -495,7 +495,10 @@ export async function fetchWithTimeout(url, opts, timeoutMs = 30_000, signal) {
|
|
|
495
495
|
try {
|
|
496
496
|
if (controller.signal.aborted)
|
|
497
497
|
throw controller.signal.reason ?? new Error(`Request aborted: ${url}`);
|
|
498
|
-
|
|
498
|
+
// `timeout: false` turns off Bun's independent five-minute socket-idle timer,
|
|
499
|
+
// which would otherwise cut a longer AKM deadline short. The AbortController
|
|
500
|
+
// signal (deadline + caller cancel) remains the only bound on the request.
|
|
501
|
+
return await fetch(url, { ...opts, signal: controller.signal, timeout: false });
|
|
499
502
|
}
|
|
500
503
|
catch (err) {
|
|
501
504
|
if (err instanceof DOMException && err.name === "AbortError") {
|
|
@@ -194,6 +194,16 @@ function classifyByParentDirHint(ctx) {
|
|
|
194
194
|
}
|
|
195
195
|
return matchDirectoryHint(parentDir, ctx, 15);
|
|
196
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* The type a file's location declares, from the same directory rules the
|
|
199
|
+
* indexer applies: parent-directory hint (specificity 15) over the outermost
|
|
200
|
+
* typed ancestor (10), each honouring the rule's extension predicate, the
|
|
201
|
+
* `README.md` documentation exception and nested skill resources. `undefined`
|
|
202
|
+
* when no directory rule claims the file. Path fields only; no bytes are read.
|
|
203
|
+
*/
|
|
204
|
+
export function declaredTypeForPath(ctx) {
|
|
205
|
+
return (classifyByParentDirHint(ctx) ?? classifyByDirectory(ctx))?.type;
|
|
206
|
+
}
|
|
197
207
|
function classifyBySmartMd(ctx) {
|
|
198
208
|
if (ctx.ext !== ".md")
|
|
199
209
|
return null;
|
|
@@ -37171,12 +37171,24 @@ function keyLooksSecret(key) {
|
|
|
37171
37171
|
const lower = key.toLowerCase();
|
|
37172
37172
|
return SECRET_KEY_HINTS.some((hint) => lower.includes(hint));
|
|
37173
37173
|
}
|
|
37174
|
+
function segmentLooksSecret(seg) {
|
|
37175
|
+
if (SECRET_VALUE_PREFIX.test(seg))
|
|
37176
|
+
return true;
|
|
37177
|
+
const eq = seg.indexOf("=");
|
|
37178
|
+
if (eq > 0 && keyLooksSecret(seg.slice(0, eq)) && seg.length - eq > 8)
|
|
37179
|
+
return true;
|
|
37180
|
+
if (seg.length >= 32 && !UUID.test(seg) && /^[A-Za-z0-9+_=-]+$/.test(seg) && /\d/.test(seg) && /[a-zA-Z]/.test(seg))
|
|
37181
|
+
return true;
|
|
37182
|
+
return seg.length >= 20 && /^[A-Za-z0-9]+$/.test(seg) && /[a-z]/.test(seg) && /[A-Z]/.test(seg) && /\d/.test(seg);
|
|
37183
|
+
}
|
|
37174
37184
|
function valueLooksSecret(value) {
|
|
37175
37185
|
const s = value.trim();
|
|
37176
37186
|
if (s.length < 20 || /\s/.test(s))
|
|
37177
37187
|
return false;
|
|
37178
37188
|
if (SECRET_VALUE_PREFIX.test(s))
|
|
37179
37189
|
return true;
|
|
37190
|
+
if (PATH_LIKE.test(s))
|
|
37191
|
+
return s.split(/[/\\?&#]/).some(segmentLooksSecret);
|
|
37180
37192
|
const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((re) => re.test(s)).length;
|
|
37181
37193
|
if (s.length >= 24 && classes >= 3)
|
|
37182
37194
|
return true;
|
|
@@ -37218,7 +37230,7 @@ function walkSecretShaped(params, hit) {
|
|
|
37218
37230
|
for (const [k, v] of Object.entries(params))
|
|
37219
37231
|
walk(v, k, k);
|
|
37220
37232
|
}
|
|
37221
|
-
var SECRET_KEY_HINTS, SECRET_VALUE_PREFIX, MOVE_TO_ENV;
|
|
37233
|
+
var SECRET_KEY_HINTS, SECRET_VALUE_PREFIX, PATH_LIKE, UUID, MOVE_TO_ENV;
|
|
37222
37234
|
var init_param_secrets = __esm(() => {
|
|
37223
37235
|
SECRET_KEY_HINTS = [
|
|
37224
37236
|
"secret",
|
|
@@ -37239,6 +37251,8 @@ var init_param_secrets = __esm(() => {
|
|
|
37239
37251
|
"client_secret"
|
|
37240
37252
|
];
|
|
37241
37253
|
SECRET_VALUE_PREFIX = /^(sk-|rk-|ghp_|gho_|ghu_|ghs_|ghr_|github_pat_|xox[baprs]-|AKIA|ASIA|AIza|ya29\.|-----BEGIN)/;
|
|
37254
|
+
PATH_LIKE = /^(\/|\.{1,2}[\\/]|~[\\/]|[A-Za-z]:[\\/]|\\\\)|^[\w.-]+[\\/][^?#]*\.[A-Za-z0-9]{1,8}$/;
|
|
37255
|
+
UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
37242
37256
|
MOVE_TO_ENV = "Workflow params are copied verbatim into every native unit execution context and returned in `akm workflow run` and " + "`akm workflow status` output — move secrets to an env binding (`env:` ref), whose value native execution resolves " + "only at dispatch instead of storing it as a run param.";
|
|
37243
37257
|
});
|
|
37244
37258
|
|
|
@@ -41924,10 +41938,10 @@ var init_marked_esm = __esm(() => {
|
|
|
41924
41938
|
|
|
41925
41939
|
// src/core/asset/markdown-link-targets.ts
|
|
41926
41940
|
function markdownLinkTargets(body, references) {
|
|
41927
|
-
const
|
|
41941
|
+
const tokens2 = markdown.lexer(body);
|
|
41928
41942
|
const definitions = new Map;
|
|
41929
41943
|
if (references) {
|
|
41930
|
-
markdown.walkTokens(
|
|
41944
|
+
markdown.walkTokens(tokens2, (token) => {
|
|
41931
41945
|
if (token.type !== "def")
|
|
41932
41946
|
return;
|
|
41933
41947
|
const definition = /^\s*\[([^\]]+)\]:\s*(\S+)/.exec(token.raw);
|
|
@@ -41936,7 +41950,7 @@ function markdownLinkTargets(body, references) {
|
|
|
41936
41950
|
});
|
|
41937
41951
|
}
|
|
41938
41952
|
const targets = [];
|
|
41939
|
-
markdown.walkTokens(
|
|
41953
|
+
markdown.walkTokens(tokens2, (token) => {
|
|
41940
41954
|
if (token.type !== "link" && token.type !== "image")
|
|
41941
41955
|
return;
|
|
41942
41956
|
const inline = /^!?\[[\s\S]*\]\(/.test(token.raw);
|
|
@@ -62450,7 +62464,7 @@ var require_HTMLParser = __commonJS((exports, module) => {
|
|
|
62450
62464
|
}
|
|
62451
62465
|
}
|
|
62452
62466
|
htmlparser.testTokenizer = function(input2, initialState, lastStartTag, charbychar) {
|
|
62453
|
-
var
|
|
62467
|
+
var tokens2 = [];
|
|
62454
62468
|
switch (initialState) {
|
|
62455
62469
|
case "PCDATA state":
|
|
62456
62470
|
tokenizer = data_state;
|
|
@@ -62472,16 +62486,16 @@ var require_HTMLParser = __commonJS((exports, module) => {
|
|
|
62472
62486
|
flushText();
|
|
62473
62487
|
switch (t) {
|
|
62474
62488
|
case 1:
|
|
62475
|
-
if (
|
|
62476
|
-
|
|
62489
|
+
if (tokens2.length > 0 && tokens2[tokens2.length - 1][0] === "Character") {
|
|
62490
|
+
tokens2[tokens2.length - 1][1] += value;
|
|
62477
62491
|
} else
|
|
62478
|
-
|
|
62492
|
+
tokens2.push(["Character", value]);
|
|
62479
62493
|
break;
|
|
62480
62494
|
case 4:
|
|
62481
|
-
|
|
62495
|
+
tokens2.push(["Comment", value]);
|
|
62482
62496
|
break;
|
|
62483
62497
|
case 5:
|
|
62484
|
-
|
|
62498
|
+
tokens2.push([
|
|
62485
62499
|
"DOCTYPE",
|
|
62486
62500
|
value,
|
|
62487
62501
|
arg3 === undefined ? null : arg3,
|
|
@@ -62502,10 +62516,10 @@ var require_HTMLParser = __commonJS((exports, module) => {
|
|
|
62502
62516
|
var token = ["StartTag", value, attrs];
|
|
62503
62517
|
if (arg4)
|
|
62504
62518
|
token.push(true);
|
|
62505
|
-
|
|
62519
|
+
tokens2.push(token);
|
|
62506
62520
|
break;
|
|
62507
62521
|
case 3:
|
|
62508
|
-
|
|
62522
|
+
tokens2.push(["EndTag", value]);
|
|
62509
62523
|
break;
|
|
62510
62524
|
case -1:
|
|
62511
62525
|
break;
|
|
@@ -62519,7 +62533,7 @@ var require_HTMLParser = __commonJS((exports, module) => {
|
|
|
62519
62533
|
}
|
|
62520
62534
|
this.parse("", true);
|
|
62521
62535
|
}
|
|
62522
|
-
return
|
|
62536
|
+
return tokens2;
|
|
62523
62537
|
};
|
|
62524
62538
|
return htmlparser;
|
|
62525
62539
|
}
|
|
@@ -68893,18 +68907,25 @@ function sourceFacts(counts) {
|
|
|
68893
68907
|
const entries = Object.entries(counts);
|
|
68894
68908
|
return entries.length === 0 ? "-" : entries.map(([k, v]) => `${k}=${v}`).join(" ");
|
|
68895
68909
|
}
|
|
68910
|
+
function tokens(n, a) {
|
|
68911
|
+
return a.calls > 0 && a.usageCalls === 0 ? "unknown" : int(n);
|
|
68912
|
+
}
|
|
68913
|
+
function usageCoverage(a) {
|
|
68914
|
+
return a.usageCalls === undefined ? "-" : `${a.usageCalls}/${a.calls}`;
|
|
68915
|
+
}
|
|
68896
68916
|
function llmRows(group) {
|
|
68897
68917
|
return Object.entries(group).map(([name, a]) => [
|
|
68898
68918
|
name,
|
|
68899
68919
|
int(a.calls),
|
|
68900
68920
|
int(a.failures),
|
|
68901
|
-
|
|
68902
|
-
|
|
68903
|
-
|
|
68921
|
+
tokens(a.promptTokens, a),
|
|
68922
|
+
tokens(a.completionTokens, a),
|
|
68923
|
+
tokens(a.totalTokens, a),
|
|
68924
|
+
usageCoverage(a),
|
|
68904
68925
|
ms(a.totalDurationMs)
|
|
68905
68926
|
]);
|
|
68906
68927
|
}
|
|
68907
|
-
var LLM_HEADERS = ["name", "calls", "failed", "prompt", "completion", "total", "time"];
|
|
68928
|
+
var LLM_HEADERS = ["name", "calls", "failed", "prompt", "completion", "total", "calls with usage", "time"];
|
|
68908
68929
|
function buildMetricsView(r, detail) {
|
|
68909
68930
|
const brief = detail === "brief";
|
|
68910
68931
|
const cut = (items) => brief ? items.slice(0, BRIEF_LIST_LIMIT) : items;
|
|
@@ -68918,7 +68939,8 @@ function buildMetricsView(r, detail) {
|
|
|
68918
68939
|
["shows", int(usage.totals.shows)],
|
|
68919
68940
|
["curates", int(usage.totals.curates)],
|
|
68920
68941
|
["selects", int(usage.totals.selects)],
|
|
68921
|
-
["
|
|
68942
|
+
["searches with hits", int(usage.totals.searchesWithHits)],
|
|
68943
|
+
["selects per search with hits", fixed(usage.selectsPerSearch, 2)],
|
|
68922
68944
|
[
|
|
68923
68945
|
"zero-result searches",
|
|
68924
68946
|
`${usage.totals.zeroResultSearches} (${pct(searches === 0 ? null : usage.totals.zeroResultSearches / searches)})`
|
|
@@ -69025,10 +69047,11 @@ function buildMetricsView(r, detail) {
|
|
|
69025
69047
|
facts: [
|
|
69026
69048
|
["calls", int(llm.calls)],
|
|
69027
69049
|
["failed", int(llm.failures)],
|
|
69028
|
-
["
|
|
69029
|
-
["
|
|
69030
|
-
["
|
|
69031
|
-
["
|
|
69050
|
+
["calls with token usage", usageCoverage(llm)],
|
|
69051
|
+
["prompt tokens", tokens(llm.promptTokens, llm)],
|
|
69052
|
+
["completion tokens", tokens(llm.completionTokens, llm)],
|
|
69053
|
+
["reasoning tokens", llm.calls > 0 && llm.reasoningCalls === 0 ? "unknown" : tokens(llm.reasoningTokens, llm)],
|
|
69054
|
+
["total tokens", tokens(llm.totalTokens, llm)],
|
|
69032
69055
|
["time", ms(llm.totalDurationMs)]
|
|
69033
69056
|
],
|
|
69034
69057
|
tables: [
|
|
@@ -72919,15 +72942,15 @@ var SHELL_ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
|
72919
72942
|
function shellStableV2Executable(executable) {
|
|
72920
72943
|
return executable === "akm" || executable.includes("/");
|
|
72921
72944
|
}
|
|
72922
|
-
function skipEnvAssignmentPrefix(
|
|
72923
|
-
if (
|
|
72924
|
-
return { tokens, envWrapped: false };
|
|
72945
|
+
function skipEnvAssignmentPrefix(tokens2) {
|
|
72946
|
+
if (tokens2[0] !== "env")
|
|
72947
|
+
return { tokens: tokens2, envWrapped: false };
|
|
72925
72948
|
let index = 1;
|
|
72926
|
-
while (index <
|
|
72949
|
+
while (index < tokens2.length && SHELL_ASSIGNMENT_WORD.test(tokens2[index]))
|
|
72927
72950
|
index += 1;
|
|
72928
|
-
if (index >=
|
|
72929
|
-
return { tokens, envWrapped: false };
|
|
72930
|
-
return { tokens:
|
|
72951
|
+
if (index >= tokens2.length)
|
|
72952
|
+
return { tokens: tokens2, envWrapped: false };
|
|
72953
|
+
return { tokens: tokens2.slice(index), envWrapped: true };
|
|
72931
72954
|
}
|
|
72932
72955
|
var KNOWN_PROMPT_REF_FAMILIES = new Set([
|
|
72933
72956
|
"agents",
|
|
@@ -73359,16 +73382,16 @@ function migratedObject(data) {
|
|
|
73359
73382
|
const command = exactString(data.command, "command", true).trim();
|
|
73360
73383
|
if (/['"\\]/.test(command))
|
|
73361
73384
|
return "shell-quoting-changes-v2-whitespace-split-semantics";
|
|
73362
|
-
const
|
|
73363
|
-
if (
|
|
73385
|
+
const tokens2 = command.split(/\s+/).filter(Boolean);
|
|
73386
|
+
if (tokens2.length === 0 || tokens2.some((token) => !SAFE_V2_COMMAND_TOKEN.test(token))) {
|
|
73364
73387
|
return "shell-operators-change-v2-literal-argv-semantics";
|
|
73365
73388
|
}
|
|
73366
|
-
const { tokens: targetTokens, envWrapped } = skipEnvAssignmentPrefix(
|
|
73389
|
+
const { tokens: targetTokens, envWrapped } = skipEnvAssignmentPrefix(tokens2);
|
|
73367
73390
|
const executable = targetTokens[0];
|
|
73368
73391
|
if (SHELL_ASSIGNMENT_WORD.test(executable) || !envWrapped && !shellStableV2Executable(executable)) {
|
|
73369
73392
|
return "shell-command-resolution-changes-v2-literal-argv-semantics";
|
|
73370
73393
|
}
|
|
73371
|
-
output2.run =
|
|
73394
|
+
output2.run = tokens2.join(" ");
|
|
73372
73395
|
addSharedNonPromptOverrides(data, akm);
|
|
73373
73396
|
}
|
|
73374
73397
|
output2.akm = akm;
|
|
@@ -37170,12 +37170,24 @@ function keyLooksSecret(key) {
|
|
|
37170
37170
|
const lower = key.toLowerCase();
|
|
37171
37171
|
return SECRET_KEY_HINTS.some((hint) => lower.includes(hint));
|
|
37172
37172
|
}
|
|
37173
|
+
function segmentLooksSecret(seg) {
|
|
37174
|
+
if (SECRET_VALUE_PREFIX.test(seg))
|
|
37175
|
+
return true;
|
|
37176
|
+
const eq = seg.indexOf("=");
|
|
37177
|
+
if (eq > 0 && keyLooksSecret(seg.slice(0, eq)) && seg.length - eq > 8)
|
|
37178
|
+
return true;
|
|
37179
|
+
if (seg.length >= 32 && !UUID.test(seg) && /^[A-Za-z0-9+_=-]+$/.test(seg) && /\d/.test(seg) && /[a-zA-Z]/.test(seg))
|
|
37180
|
+
return true;
|
|
37181
|
+
return seg.length >= 20 && /^[A-Za-z0-9]+$/.test(seg) && /[a-z]/.test(seg) && /[A-Z]/.test(seg) && /\d/.test(seg);
|
|
37182
|
+
}
|
|
37173
37183
|
function valueLooksSecret(value) {
|
|
37174
37184
|
const s = value.trim();
|
|
37175
37185
|
if (s.length < 20 || /\s/.test(s))
|
|
37176
37186
|
return false;
|
|
37177
37187
|
if (SECRET_VALUE_PREFIX.test(s))
|
|
37178
37188
|
return true;
|
|
37189
|
+
if (PATH_LIKE.test(s))
|
|
37190
|
+
return s.split(/[/\\?&#]/).some(segmentLooksSecret);
|
|
37179
37191
|
const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((re) => re.test(s)).length;
|
|
37180
37192
|
if (s.length >= 24 && classes >= 3)
|
|
37181
37193
|
return true;
|
|
@@ -37217,7 +37229,7 @@ function walkSecretShaped(params, hit) {
|
|
|
37217
37229
|
for (const [k, v] of Object.entries(params))
|
|
37218
37230
|
walk(v, k, k);
|
|
37219
37231
|
}
|
|
37220
|
-
var SECRET_KEY_HINTS, SECRET_VALUE_PREFIX, MOVE_TO_ENV;
|
|
37232
|
+
var SECRET_KEY_HINTS, SECRET_VALUE_PREFIX, PATH_LIKE, UUID, MOVE_TO_ENV;
|
|
37221
37233
|
var init_param_secrets = __esm(() => {
|
|
37222
37234
|
SECRET_KEY_HINTS = [
|
|
37223
37235
|
"secret",
|
|
@@ -37238,6 +37250,8 @@ var init_param_secrets = __esm(() => {
|
|
|
37238
37250
|
"client_secret"
|
|
37239
37251
|
];
|
|
37240
37252
|
SECRET_VALUE_PREFIX = /^(sk-|rk-|ghp_|gho_|ghu_|ghs_|ghr_|github_pat_|xox[baprs]-|AKIA|ASIA|AIza|ya29\.|-----BEGIN)/;
|
|
37253
|
+
PATH_LIKE = /^(\/|\.{1,2}[\\/]|~[\\/]|[A-Za-z]:[\\/]|\\\\)|^[\w.-]+[\\/][^?#]*\.[A-Za-z0-9]{1,8}$/;
|
|
37254
|
+
UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
37241
37255
|
MOVE_TO_ENV = "Workflow params are copied verbatim into every native unit execution context and returned in `akm workflow run` and " + "`akm workflow status` output \u2014 move secrets to an env binding (`env:` ref), whose value native execution resolves " + "only at dispatch instead of storing it as a run param.";
|
|
37242
37256
|
});
|
|
37243
37257
|
|
|
@@ -41923,10 +41937,10 @@ var init_marked_esm = __esm(() => {
|
|
|
41923
41937
|
|
|
41924
41938
|
// src/core/asset/markdown-link-targets.ts
|
|
41925
41939
|
function markdownLinkTargets(body, references) {
|
|
41926
|
-
const
|
|
41940
|
+
const tokens2 = markdown.lexer(body);
|
|
41927
41941
|
const definitions = new Map;
|
|
41928
41942
|
if (references) {
|
|
41929
|
-
markdown.walkTokens(
|
|
41943
|
+
markdown.walkTokens(tokens2, (token) => {
|
|
41930
41944
|
if (token.type !== "def")
|
|
41931
41945
|
return;
|
|
41932
41946
|
const definition = /^\s*\[([^\]]+)\]:\s*(\S+)/.exec(token.raw);
|
|
@@ -41935,7 +41949,7 @@ function markdownLinkTargets(body, references) {
|
|
|
41935
41949
|
});
|
|
41936
41950
|
}
|
|
41937
41951
|
const targets = [];
|
|
41938
|
-
markdown.walkTokens(
|
|
41952
|
+
markdown.walkTokens(tokens2, (token) => {
|
|
41939
41953
|
if (token.type !== "link" && token.type !== "image")
|
|
41940
41954
|
return;
|
|
41941
41955
|
const inline = /^!?\[[\s\S]*\]\(/.test(token.raw);
|
|
@@ -62449,7 +62463,7 @@ var require_HTMLParser = __commonJS((exports, module) => {
|
|
|
62449
62463
|
}
|
|
62450
62464
|
}
|
|
62451
62465
|
htmlparser.testTokenizer = function(input2, initialState, lastStartTag, charbychar) {
|
|
62452
|
-
var
|
|
62466
|
+
var tokens2 = [];
|
|
62453
62467
|
switch (initialState) {
|
|
62454
62468
|
case "PCDATA state":
|
|
62455
62469
|
tokenizer = data_state;
|
|
@@ -62471,16 +62485,16 @@ var require_HTMLParser = __commonJS((exports, module) => {
|
|
|
62471
62485
|
flushText();
|
|
62472
62486
|
switch (t) {
|
|
62473
62487
|
case 1:
|
|
62474
|
-
if (
|
|
62475
|
-
|
|
62488
|
+
if (tokens2.length > 0 && tokens2[tokens2.length - 1][0] === "Character") {
|
|
62489
|
+
tokens2[tokens2.length - 1][1] += value;
|
|
62476
62490
|
} else
|
|
62477
|
-
|
|
62491
|
+
tokens2.push(["Character", value]);
|
|
62478
62492
|
break;
|
|
62479
62493
|
case 4:
|
|
62480
|
-
|
|
62494
|
+
tokens2.push(["Comment", value]);
|
|
62481
62495
|
break;
|
|
62482
62496
|
case 5:
|
|
62483
|
-
|
|
62497
|
+
tokens2.push([
|
|
62484
62498
|
"DOCTYPE",
|
|
62485
62499
|
value,
|
|
62486
62500
|
arg3 === undefined ? null : arg3,
|
|
@@ -62501,10 +62515,10 @@ var require_HTMLParser = __commonJS((exports, module) => {
|
|
|
62501
62515
|
var token = ["StartTag", value, attrs];
|
|
62502
62516
|
if (arg4)
|
|
62503
62517
|
token.push(true);
|
|
62504
|
-
|
|
62518
|
+
tokens2.push(token);
|
|
62505
62519
|
break;
|
|
62506
62520
|
case 3:
|
|
62507
|
-
|
|
62521
|
+
tokens2.push(["EndTag", value]);
|
|
62508
62522
|
break;
|
|
62509
62523
|
case -1:
|
|
62510
62524
|
break;
|
|
@@ -62518,7 +62532,7 @@ var require_HTMLParser = __commonJS((exports, module) => {
|
|
|
62518
62532
|
}
|
|
62519
62533
|
this.parse("", true);
|
|
62520
62534
|
}
|
|
62521
|
-
return
|
|
62535
|
+
return tokens2;
|
|
62522
62536
|
};
|
|
62523
62537
|
return htmlparser;
|
|
62524
62538
|
}
|
|
@@ -68891,18 +68905,25 @@ function sourceFacts(counts) {
|
|
|
68891
68905
|
const entries = Object.entries(counts);
|
|
68892
68906
|
return entries.length === 0 ? "-" : entries.map(([k, v]) => `${k}=${v}`).join(" ");
|
|
68893
68907
|
}
|
|
68908
|
+
function tokens(n, a) {
|
|
68909
|
+
return a.calls > 0 && a.usageCalls === 0 ? "unknown" : int(n);
|
|
68910
|
+
}
|
|
68911
|
+
function usageCoverage(a) {
|
|
68912
|
+
return a.usageCalls === undefined ? "-" : `${a.usageCalls}/${a.calls}`;
|
|
68913
|
+
}
|
|
68894
68914
|
function llmRows(group) {
|
|
68895
68915
|
return Object.entries(group).map(([name, a]) => [
|
|
68896
68916
|
name,
|
|
68897
68917
|
int(a.calls),
|
|
68898
68918
|
int(a.failures),
|
|
68899
|
-
|
|
68900
|
-
|
|
68901
|
-
|
|
68919
|
+
tokens(a.promptTokens, a),
|
|
68920
|
+
tokens(a.completionTokens, a),
|
|
68921
|
+
tokens(a.totalTokens, a),
|
|
68922
|
+
usageCoverage(a),
|
|
68902
68923
|
ms(a.totalDurationMs)
|
|
68903
68924
|
]);
|
|
68904
68925
|
}
|
|
68905
|
-
var LLM_HEADERS = ["name", "calls", "failed", "prompt", "completion", "total", "time"];
|
|
68926
|
+
var LLM_HEADERS = ["name", "calls", "failed", "prompt", "completion", "total", "calls with usage", "time"];
|
|
68906
68927
|
function buildMetricsView(r, detail) {
|
|
68907
68928
|
const brief = detail === "brief";
|
|
68908
68929
|
const cut = (items) => brief ? items.slice(0, BRIEF_LIST_LIMIT) : items;
|
|
@@ -68916,7 +68937,8 @@ function buildMetricsView(r, detail) {
|
|
|
68916
68937
|
["shows", int(usage.totals.shows)],
|
|
68917
68938
|
["curates", int(usage.totals.curates)],
|
|
68918
68939
|
["selects", int(usage.totals.selects)],
|
|
68919
|
-
["
|
|
68940
|
+
["searches with hits", int(usage.totals.searchesWithHits)],
|
|
68941
|
+
["selects per search with hits", fixed(usage.selectsPerSearch, 2)],
|
|
68920
68942
|
[
|
|
68921
68943
|
"zero-result searches",
|
|
68922
68944
|
`${usage.totals.zeroResultSearches} (${pct(searches === 0 ? null : usage.totals.zeroResultSearches / searches)})`
|
|
@@ -69023,10 +69045,11 @@ function buildMetricsView(r, detail) {
|
|
|
69023
69045
|
facts: [
|
|
69024
69046
|
["calls", int(llm.calls)],
|
|
69025
69047
|
["failed", int(llm.failures)],
|
|
69026
|
-
["
|
|
69027
|
-
["
|
|
69028
|
-
["
|
|
69029
|
-
["
|
|
69048
|
+
["calls with token usage", usageCoverage(llm)],
|
|
69049
|
+
["prompt tokens", tokens(llm.promptTokens, llm)],
|
|
69050
|
+
["completion tokens", tokens(llm.completionTokens, llm)],
|
|
69051
|
+
["reasoning tokens", llm.calls > 0 && llm.reasoningCalls === 0 ? "unknown" : tokens(llm.reasoningTokens, llm)],
|
|
69052
|
+
["total tokens", tokens(llm.totalTokens, llm)],
|
|
69030
69053
|
["time", ms(llm.totalDurationMs)]
|
|
69031
69054
|
],
|
|
69032
69055
|
tables: [
|
|
@@ -72882,15 +72905,15 @@ var SHELL_ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
|
72882
72905
|
function shellStableV2Executable(executable) {
|
|
72883
72906
|
return executable === "akm" || executable.includes("/");
|
|
72884
72907
|
}
|
|
72885
|
-
function skipEnvAssignmentPrefix(
|
|
72886
|
-
if (
|
|
72887
|
-
return { tokens, envWrapped: false };
|
|
72908
|
+
function skipEnvAssignmentPrefix(tokens2) {
|
|
72909
|
+
if (tokens2[0] !== "env")
|
|
72910
|
+
return { tokens: tokens2, envWrapped: false };
|
|
72888
72911
|
let index = 1;
|
|
72889
|
-
while (index <
|
|
72912
|
+
while (index < tokens2.length && SHELL_ASSIGNMENT_WORD.test(tokens2[index]))
|
|
72890
72913
|
index += 1;
|
|
72891
|
-
if (index >=
|
|
72892
|
-
return { tokens, envWrapped: false };
|
|
72893
|
-
return { tokens:
|
|
72914
|
+
if (index >= tokens2.length)
|
|
72915
|
+
return { tokens: tokens2, envWrapped: false };
|
|
72916
|
+
return { tokens: tokens2.slice(index), envWrapped: true };
|
|
72894
72917
|
}
|
|
72895
72918
|
var KNOWN_PROMPT_REF_FAMILIES = new Set([
|
|
72896
72919
|
"agents",
|
|
@@ -73322,16 +73345,16 @@ function migratedObject(data) {
|
|
|
73322
73345
|
const command = exactString(data.command, "command", true).trim();
|
|
73323
73346
|
if (/['"\\]/.test(command))
|
|
73324
73347
|
return "shell-quoting-changes-v2-whitespace-split-semantics";
|
|
73325
|
-
const
|
|
73326
|
-
if (
|
|
73348
|
+
const tokens2 = command.split(/\s+/).filter(Boolean);
|
|
73349
|
+
if (tokens2.length === 0 || tokens2.some((token) => !SAFE_V2_COMMAND_TOKEN.test(token))) {
|
|
73327
73350
|
return "shell-operators-change-v2-literal-argv-semantics";
|
|
73328
73351
|
}
|
|
73329
|
-
const { tokens: targetTokens, envWrapped } = skipEnvAssignmentPrefix(
|
|
73352
|
+
const { tokens: targetTokens, envWrapped } = skipEnvAssignmentPrefix(tokens2);
|
|
73330
73353
|
const executable = targetTokens[0];
|
|
73331
73354
|
if (SHELL_ASSIGNMENT_WORD.test(executable) || !envWrapped && !shellStableV2Executable(executable)) {
|
|
73332
73355
|
return "shell-command-resolution-changes-v2-literal-argv-semantics";
|
|
73333
73356
|
}
|
|
73334
|
-
output2.run =
|
|
73357
|
+
output2.run = tokens2.join(" ");
|
|
73335
73358
|
addSharedNonPromptOverrides(data, akm);
|
|
73336
73359
|
}
|
|
73337
73360
|
output2.akm = akm;
|
|
@@ -38,10 +38,36 @@ function keyLooksSecret(key) {
|
|
|
38
38
|
const lower = key.toLowerCase();
|
|
39
39
|
return SECRET_KEY_HINTS.some((hint) => lower.includes(hint));
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Unix (`/`, `./`, `../`, `~/`) or Windows (`C:\`, `\\server`) filesystem path,
|
|
43
|
+
* or a bare relative path whose last segment carries a file extension
|
|
44
|
+
* (`reports/2026-10-30/summary.md`). Base64 never contains `.`, so the
|
|
45
|
+
* extension keeps slash-bearing base64 blobs on the whole-string check.
|
|
46
|
+
*/
|
|
47
|
+
const PATH_LIKE = /^(\/|\.{1,2}[\\/]|~[\\/]|[A-Za-z]:[\\/]|\\\\)|^[\w.-]+[\\/][^?#]*\.[A-Za-z0-9]{1,8}$/;
|
|
48
|
+
/** A canonical UUID — an ordinary run/session directory name, not a credential. */
|
|
49
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
50
|
+
/**
|
|
51
|
+
* Whether one path segment or query piece looks like a credential: a known
|
|
52
|
+
* prefix, a secret-named query key (`token=...`), or a long unbroken token
|
|
53
|
+
* (hex/base64 run, or mixed case with digits).
|
|
54
|
+
*/
|
|
55
|
+
function segmentLooksSecret(seg) {
|
|
56
|
+
if (SECRET_VALUE_PREFIX.test(seg))
|
|
57
|
+
return true;
|
|
58
|
+
const eq = seg.indexOf("=");
|
|
59
|
+
if (eq > 0 && keyLooksSecret(seg.slice(0, eq)) && seg.length - eq > 8)
|
|
60
|
+
return true;
|
|
61
|
+
if (seg.length >= 32 && !UUID.test(seg) && /^[A-Za-z0-9+_=-]+$/.test(seg) && /\d/.test(seg) && /[a-zA-Z]/.test(seg))
|
|
62
|
+
return true;
|
|
63
|
+
return seg.length >= 20 && /^[A-Za-z0-9]+$/.test(seg) && /[a-z]/.test(seg) && /[A-Z]/.test(seg) && /\d/.test(seg);
|
|
64
|
+
}
|
|
41
65
|
/**
|
|
42
66
|
* A string value is secret-shaped when it is long, whitespace-free, and either
|
|
43
67
|
* carries a known credential prefix or has high character-class diversity /
|
|
44
|
-
* base64-hex shape — an entropy proxy, not a real entropy computation.
|
|
68
|
+
* base64-hex shape — an entropy proxy, not a real entropy computation. A
|
|
69
|
+
* filesystem path is judged per segment instead, so separators, dates and
|
|
70
|
+
* digits do not count as diversity.
|
|
45
71
|
*/
|
|
46
72
|
function valueLooksSecret(value) {
|
|
47
73
|
const s = value.trim();
|
|
@@ -49,6 +75,8 @@ function valueLooksSecret(value) {
|
|
|
49
75
|
return false;
|
|
50
76
|
if (SECRET_VALUE_PREFIX.test(s))
|
|
51
77
|
return true;
|
|
78
|
+
if (PATH_LIKE.test(s))
|
|
79
|
+
return s.split(/[/\\?&#]/).some(segmentLooksSecret);
|
|
52
80
|
const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((re) => re.test(s)).length;
|
|
53
81
|
if (s.length >= 24 && classes >= 3)
|
|
54
82
|
return true;
|
|
@@ -56,7 +56,7 @@ and `src/commands/health/version-drift.ts`.
|
|
|
56
56
|
| `cli-version` | `warn`: a newer akm release exists on GitHub. `unknown`: `--no-probe`, or the release lookup failed. | Run `akm upgrade`. |
|
|
57
57
|
| `scheduler-binary` | `warn`: the akm binary recorded in the scheduler reports a different version than the running CLI. `unknown`: `--no-probe`, no scheduled task installed, or the recorded binary cannot be run. | Run `akm task sync` to rebind the schedule. |
|
|
58
58
|
| `index-state-generation` | `warn`: usage links in state.db point at index entries that no longer match (an index and state.db from different generations). | Stop concurrent writers and run `akm index --full`. |
|
|
59
|
-
| `stash-git-exposure` | `warn
|
|
59
|
+
| `stash-git-exposure` | `warn`, one per bundle: `env/` or `secrets/` files are tracked by git in a configured bundle (the working stash included) and that bundle has a remote. Skipped for a bundle with no `.git` at its root. | Run `git rm --cached` on the listed files and add `env/` and `secrets/` to `.gitignore`. A `.gitignore` rule alone does not untrack files. To keep tracking them on purpose, add `# akm: intentionally track env and secrets` to the stash `.gitignore`; the advisory is then not reported. |
|
|
60
60
|
| `binary-config-skew` | `warn`: `config.json` has a `configVersion` newer than this binary knows. | Upgrade this install. |
|
|
61
61
|
| `egress-endpoints` | Every remote destination in the effective config, as one informational entry: enabled registries, git and website bundles, each connection's endpoint and the embedding endpoint (credentials in a URL are masked). Silent when nothing remote is configured. | Eyeball it against what you expect; a destination you did not configure came from an `extends` base or a shared bundle. |
|
|
62
62
|
| `type-directory-disagreement` | `warn`: an indexed asset's resolved type differs from the type its directory declares. Each line gives the file, both types, and the classifier signal that won (`winner`). | Review the listed files. Intended overrides (for example a `knowledge/` file containing `$ARGUMENTS`) are marked `known-good override`. |
|
|
@@ -7,7 +7,6 @@ live one level up in `docs/migration/`.
|
|
|
7
7
|
|
|
8
8
|
## Available notes
|
|
9
9
|
|
|
10
|
-
- [0.10.261020](0.10.261020.md) — Coordinated beta/stable channel switching, native Claude health, and automatic search-projection refresh.
|
|
11
10
|
- [0.10.261019](0.10.261019.md) — The 0.10 series: 0.9.15 version floor, merged CLI spellings, `default`/`consolidate` strategies only, OpenCode 2, feedback-scaled ranking, and `0.10.YYMMNN` versions.
|
|
12
11
|
- [0.9.19](0.9.19.md) — Bundle-scoped improve planning, proposal reopen, and consolidate/distill guardrails.
|
|
13
12
|
- [0.9.17](0.9.17.md) — Retire proposals, declared links replacing the LLM entity graph, and scoped improve planning.
|
package/docs/reference/cli.md
CHANGED
|
@@ -541,11 +541,11 @@ Result sections (`schemaVersion: 1`):
|
|
|
541
541
|
| Field | Description |
|
|
542
542
|
| --- | --- |
|
|
543
543
|
| `window`, `filters` | The resolved window and the usage source counted |
|
|
544
|
-
| `usage` | Search, show, curate and select totals, `
|
|
544
|
+
| `usage` | Search, show, curate and select totals, `searchesWithHits` and `selectsPerSearch` (selects divided by searches that returned a hit: a plain ratio that can exceed 1, not a conversion percentage, since selects carry no source or search id), `searchMedianMs`, a daily series, top assets, top and zero-result queries, and rows by source |
|
|
545
545
|
| `feedback` | Positive and negative totals, per-asset valence, per-tag counts, and the most recent negatives with their reasons |
|
|
546
546
|
| `utility` | A ten-bucket histogram of `utility_scores`, the lowest and highest assets, and how many indexed entries were never used |
|
|
547
547
|
| `outcomes` | The assets with the lowest `outcome_score` |
|
|
548
|
-
| `llm` | `akm health`'s LLM usage aggregate (`byStage`, `byProcess`, and `byEngine`, keyed by the name of the connection or harness) |
|
|
548
|
+
| `llm` | `akm health`'s LLM usage aggregate (`byStage`, `byProcess`, and `byEngine`, keyed by the name of the connection or harness). `usageCalls` and `reasoningCalls` count the calls whose provider response reported token usage; a call without it adds no tokens and the report shows `unknown`, not zero, when none reported |
|
|
549
549
|
| `index`, `tasks`, `proposals`, `workflows` | Index runs and median time, task runs and fail rate, proposals by status and accept rate by source, workflow runs and tokens by model |
|
|
550
550
|
| `rows` | The raw usage and LLM rows of the window; see below |
|
|
551
551
|
| `notes` | Anything that limits what the numbers mean |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.261021",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
|
|
6
6
|
"keywords": [
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# akm v0.10.261020 migration notes
|
|
2
|
-
|
|
3
|
-
This beta fixes Claude plugin health reporting, adds coordinated channel
|
|
4
|
-
switching, and reduces repeated fenced identifiers in search content.
|
|
5
|
-
|
|
6
|
-
The next index pass refreshes existing content projections and changed embedding
|
|
7
|
-
inputs automatically. No manual database or configuration migration is required
|
|
8
|
-
when upgrading from 0.10.261019.
|
|
9
|
-
|
|
10
|
-
After installing this beta, `akm upgrade --channel next` switches the CLI and
|
|
11
|
-
installed Claude, Codex and OpenCode plugins together. The existing `--next`
|
|
12
|
-
flag keeps its prior behavior. To return to stable, use
|
|
13
|
-
`akm upgrade --channel latest`; if the target version is older, add
|
|
14
|
-
`--allow-downgrade`. Changed settings receive backups, and the command reports
|
|
15
|
-
any harness that could not finish switching. Restart the harness after updates.
|
|
16
|
-
|
|
17
|
-
Claude health now uses native installation metadata. It reports conflicting or
|
|
18
|
-
incomplete installation selections as unknown instead of choosing a cached
|
|
19
|
-
version. This can change a previous warning without changing the installation.
|
|
20
|
-
|
|
21
|
-
If upgrading from 0.9, first follow the
|
|
22
|
-
[0.9 to 0.10 migration guide](../v0.9-to-v0.10.md).
|