akm-cli 0.7.0-rc1 → 0.7.0
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/dist/src/cli.js +100 -16
- package/dist/src/commands/config-cli.js +42 -0
- package/dist/src/commands/history.js +78 -7
- package/dist/src/commands/registry-search.js +69 -6
- package/dist/src/commands/search.js +30 -3
- package/dist/src/commands/show.js +29 -0
- package/dist/src/commands/source-add.js +5 -1
- package/dist/src/commands/source-manage.js +7 -1
- package/dist/src/core/config.js +28 -0
- package/dist/src/indexer/db-search.js +1 -0
- package/dist/src/indexer/indexer.js +16 -2
- package/dist/src/indexer/matchers.js +1 -1
- package/dist/src/indexer/search-source.js +4 -2
- package/dist/src/integrations/agent/profiles.js +1 -1
- package/dist/src/integrations/agent/spawn.js +67 -16
- package/dist/src/integrations/github.js +9 -3
- package/dist/src/llm/embedders/remote.js +37 -3
- package/dist/src/output/cli-hints.js +15 -2
- package/dist/src/output/renderers.js +3 -1
- package/dist/src/output/shapes.js +8 -1
- package/dist/src/output/text.js +156 -3
- package/dist/src/registry/build-index.js +5 -4
- package/dist/src/registry/providers/static-index.js +3 -1
- package/dist/src/setup/setup.js +9 -0
- package/dist/src/wiki/wiki.js +54 -6
- package/dist/src/workflows/runs.js +37 -3
- package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +1 -1
- package/dist/tests/bench/attribution.test.js +24 -23
- package/dist/tests/bench/cleanup.js +31 -0
- package/dist/tests/bench/cli.js +366 -31
- package/dist/tests/bench/cli.test.js +282 -14
- package/dist/tests/bench/corpus.js +3 -0
- package/dist/tests/bench/corpus.test.js +10 -10
- package/dist/tests/bench/doctor.js +525 -0
- package/dist/tests/bench/driver.js +77 -22
- package/dist/tests/bench/driver.test.js +142 -1
- package/dist/tests/bench/environment.js +233 -0
- package/dist/tests/bench/environment.test.js +199 -0
- package/dist/tests/bench/evolve.js +67 -0
- package/dist/tests/bench/evolve.test.js +12 -4
- package/dist/tests/bench/failure-modes.test.js +52 -3
- package/dist/tests/bench/feedback-integrity.test.js +3 -2
- package/dist/tests/bench/leakage.test.js +105 -2
- package/dist/tests/bench/learning-curve.test.js +3 -2
- package/dist/tests/bench/metrics.js +102 -26
- package/dist/tests/bench/metrics.test.js +10 -4
- package/dist/tests/bench/opencode-config.js +194 -0
- package/dist/tests/bench/opencode-config.test.js +370 -0
- package/dist/tests/bench/report.js +73 -9
- package/dist/tests/bench/report.test.js +59 -10
- package/dist/tests/bench/run-config.js +355 -0
- package/dist/tests/bench/run-config.test.js +298 -0
- package/dist/tests/bench/run-curate-test.js +32 -0
- package/dist/tests/bench/run-failing-tasks.js +56 -0
- package/dist/tests/bench/run-full-bench.js +51 -0
- package/dist/tests/bench/run-items36-targeted.js +69 -0
- package/dist/tests/bench/run-nano-quick.js +42 -0
- package/dist/tests/bench/run-waveg-targeted.js +62 -0
- package/dist/tests/bench/runner.js +257 -94
- package/dist/tests/bench/tmp.js +90 -0
- package/dist/tests/bench/trajectory.js +2 -2
- package/dist/tests/bench/verifier.js +6 -1
- package/dist/tests/bench/workflow-spec.js +11 -24
- package/dist/tests/bench/workflow-spec.test.js +1 -1
- package/dist/tests/bench/workflow-trace.js +34 -0
- package/dist/tests/cli-errors.test.js +1 -0
- package/dist/tests/commands/history.test.js +195 -0
- package/dist/tests/config.test.js +25 -0
- package/dist/tests/e2e.test.js +23 -2
- package/dist/tests/fixtures/stashes/load.js +1 -1
- package/dist/tests/fixtures/stashes/load.test.js +11 -2
- package/dist/tests/indexer.test.js +12 -1
- package/dist/tests/output-baseline.test.js +2 -1
- package/dist/tests/output-shapes-unit.test.js +3 -1
- package/dist/tests/registry-build-index.test.js +17 -1
- package/dist/tests/registry-providers/static-index.test.js +34 -0
- package/dist/tests/registry-search.test.js +200 -0
- package/dist/tests/remember-frontmatter.test.js +11 -13
- package/dist/tests/source-qa-fixes.test.js +18 -0
- package/dist/tests/source-registry.test.js +3 -3
- package/dist/tests/source-source.test.js +61 -1
- package/dist/tests/workflow-qa-fixes.test.js +18 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { isHttpUrl, resolveStashDir } from "../core/common";
|
|
4
4
|
import { loadConfig, loadUserConfig, saveConfig } from "../core/config";
|
|
5
|
-
import { UsageError } from "../core/errors";
|
|
5
|
+
import { ConfigError, UsageError } from "../core/errors";
|
|
6
6
|
import { warn } from "../core/warn";
|
|
7
7
|
import { akmIndex } from "../indexer/indexer";
|
|
8
8
|
import { upsertLockEntry } from "../integrations/lockfile";
|
|
@@ -190,6 +190,10 @@ async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
|
|
|
190
190
|
* persisting the lock entry.
|
|
191
191
|
*/
|
|
192
192
|
async function addRegistryStash(ref, stashDir, trustThisInstall, writable, wikiName) {
|
|
193
|
+
const parsedRef = parseRegistryRef(ref);
|
|
194
|
+
if (writable === true && parsedRef.source !== "git") {
|
|
195
|
+
throw new ConfigError("writable: true is only supported on filesystem and git sources", "INVALID_CONFIG_FILE");
|
|
196
|
+
}
|
|
193
197
|
// Pre-sync registry-policy enforcement uses just the parsed ref (no fetch needed),
|
|
194
198
|
// so we keep parity with the historical behavior where `enforceRegistryInstallPolicy`
|
|
195
199
|
// ran before `extractTarGzSecure` etc.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { loadConfig, loadUserConfig, saveConfig } from "../core/config";
|
|
3
|
-
import { UsageError } from "../core/errors";
|
|
3
|
+
import { ConfigError, UsageError } from "../core/errors";
|
|
4
4
|
import { resolveSourceEntries } from "../indexer/search-source";
|
|
5
5
|
// ── Operations ──────────────────────────────────────────────────────────────
|
|
6
6
|
/**
|
|
@@ -12,6 +12,12 @@ import { resolveSourceEntries } from "../indexer/search-source";
|
|
|
12
12
|
*/
|
|
13
13
|
export function addStash(opts) {
|
|
14
14
|
const { target, name, providerType, options: providerOptions, writable } = opts;
|
|
15
|
+
if (providerType === "openviking") {
|
|
16
|
+
throw new ConfigError("openviking is not supported in akm v1.", "INVALID_CONFIG_FILE");
|
|
17
|
+
}
|
|
18
|
+
if (writable === true && providerType && providerType !== "filesystem" && providerType !== "git") {
|
|
19
|
+
throw new ConfigError("writable: true is only supported on filesystem and git sources", "INVALID_CONFIG_FILE");
|
|
20
|
+
}
|
|
15
21
|
const config = loadUserConfig();
|
|
16
22
|
const sources = [...(config.sources ?? config.stashes ?? [])];
|
|
17
23
|
const isRemoteUrl = target.startsWith("http://") ||
|
package/dist/src/core/config.js
CHANGED
|
@@ -169,6 +169,9 @@ export function updateConfig(partial) {
|
|
|
169
169
|
*/
|
|
170
170
|
function pickKnownKeys(raw) {
|
|
171
171
|
const config = {};
|
|
172
|
+
if (Array.isArray(raw.stashes)) {
|
|
173
|
+
throw new ConfigError("The legacy `stashes[]` config key is no longer supported; rename it to `sources[]`.", "INVALID_CONFIG_FILE", `Edit ${_getConfigPath()} and replace \`stashes\` with \`sources\`.`);
|
|
174
|
+
}
|
|
172
175
|
if (typeof raw.stashDir === "string" && raw.stashDir.trim()) {
|
|
173
176
|
config.stashDir = raw.stashDir.trim();
|
|
174
177
|
}
|
|
@@ -436,6 +439,28 @@ function parseEmbeddingConfig(value) {
|
|
|
436
439
|
if (localModel) {
|
|
437
440
|
result.localModel = localModel;
|
|
438
441
|
}
|
|
442
|
+
if ("contextLength" in obj) {
|
|
443
|
+
if (typeof obj.contextLength !== "number" ||
|
|
444
|
+
!Number.isFinite(obj.contextLength) ||
|
|
445
|
+
!Number.isInteger(obj.contextLength) ||
|
|
446
|
+
obj.contextLength <= 0) {
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
result.contextLength = obj.contextLength;
|
|
450
|
+
}
|
|
451
|
+
if (typeof obj.ollamaOptions === "object" && obj.ollamaOptions !== null && !Array.isArray(obj.ollamaOptions)) {
|
|
452
|
+
const opts = obj.ollamaOptions;
|
|
453
|
+
const parsed = {};
|
|
454
|
+
if (typeof opts.num_ctx === "number" &&
|
|
455
|
+
Number.isFinite(opts.num_ctx) &&
|
|
456
|
+
Number.isInteger(opts.num_ctx) &&
|
|
457
|
+
opts.num_ctx > 0) {
|
|
458
|
+
parsed.num_ctx = opts.num_ctx;
|
|
459
|
+
}
|
|
460
|
+
if (Object.keys(parsed).length > 0) {
|
|
461
|
+
result.ollamaOptions = parsed;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
439
464
|
return result;
|
|
440
465
|
}
|
|
441
466
|
function parseLlmConfig(value) {
|
|
@@ -619,6 +644,9 @@ function parseInstalledStashEntry(value) {
|
|
|
619
644
|
};
|
|
620
645
|
if (typeof obj.writable === "boolean")
|
|
621
646
|
entry.writable = obj.writable;
|
|
647
|
+
if (entry.writable === true && entry.source !== "git") {
|
|
648
|
+
throw new ConfigError(`writable: true is only supported on filesystem and git sources (got "${entry.source}" on installed entry "${entry.id}").`, "INVALID_CONFIG_FILE", "Remove `writable: true` from the installed entry or re-add it as a git source instead.");
|
|
649
|
+
}
|
|
622
650
|
const resolvedVersion = asNonEmptyString(obj.resolvedVersion);
|
|
623
651
|
if (resolvedVersion)
|
|
624
652
|
entry.resolvedVersion = resolvedVersion;
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { isHttpUrl, resolveStashDir, toErrorMessage } from "../core/common";
|
|
4
4
|
import { getDbPath } from "../core/paths";
|
|
5
|
-
import { isVerbose, warn } from "../core/warn";
|
|
5
|
+
import { isVerbose, warn, warnVerbose } from "../core/warn";
|
|
6
6
|
import { resolveIndexPassLLM } from "../llm/index-passes";
|
|
7
7
|
import { takeWorkflowDocument } from "../workflows/document-cache";
|
|
8
8
|
import { closeDatabase, deleteEntriesByDir, deleteEntriesByStashDir, getEmbeddingCount, getEntriesByDir, getEntryCount, getMeta, isVecAvailable, openDatabase, rebuildFts, setMeta, upsertEmbedding, upsertEntry, upsertUtilityScore, warnIfVecMissing, } from "./db";
|
|
@@ -503,6 +503,7 @@ async function generateEmbeddingsForDb(db, config, onProgress) {
|
|
|
503
503
|
}
|
|
504
504
|
try {
|
|
505
505
|
const { embedBatch } = await import("../llm/embedder.js");
|
|
506
|
+
const { estimateTokenCount } = await import("../llm/embedders/remote.js");
|
|
506
507
|
const allEntries = getAllEntriesForEmbedding(db);
|
|
507
508
|
if (allEntries.length === 0) {
|
|
508
509
|
onProgress({ phase: "embeddings", message: "Embeddings already up to date." });
|
|
@@ -514,6 +515,19 @@ async function generateEmbeddingsForDb(db, config, onProgress) {
|
|
|
514
515
|
message: `Generating embeddings for ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"}.`,
|
|
515
516
|
});
|
|
516
517
|
const texts = allEntries.map((e) => e.searchText);
|
|
518
|
+
// Verbose: log each document before it is sent to the embedding API so
|
|
519
|
+
// operators can see exactly where embedding fails without waiting for an error.
|
|
520
|
+
if (isVerbose()) {
|
|
521
|
+
const EMBED_BATCH_SIZE = 100; // mirrors REMOTE_BATCH_SIZE in remote.ts
|
|
522
|
+
const totalBatches = Math.ceil(texts.length / EMBED_BATCH_SIZE);
|
|
523
|
+
for (let i = 0; i < texts.length; i++) {
|
|
524
|
+
const batchNum = Math.floor(i / EMBED_BATCH_SIZE) + 1;
|
|
525
|
+
const chars = texts[i].length;
|
|
526
|
+
const tokens = estimateTokenCount(texts[i]);
|
|
527
|
+
const ref = allEntries[i].entryKey.split(":").slice(1).join(":"); // strip stashDir prefix
|
|
528
|
+
warnVerbose(`[embed] ${ref} (${chars} chars, est. ${tokens} tokens) → batch ${batchNum}/${totalBatches}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
517
531
|
const embeddings = await embedBatch(texts, config.embedding);
|
|
518
532
|
// Wrap all embedding upserts in a single transaction so partial
|
|
519
533
|
// state is rolled back on failure rather than leaving the table half-filled.
|
|
@@ -547,7 +561,7 @@ async function generateEmbeddingsForDb(db, config, onProgress) {
|
|
|
547
561
|
function getAllEntriesForEmbedding(db) {
|
|
548
562
|
return db
|
|
549
563
|
.prepare(`
|
|
550
|
-
SELECT e.id, e.search_text AS searchText FROM entries e
|
|
564
|
+
SELECT e.id, e.search_text AS searchText, e.entry_key AS entryKey, e.file_path AS filePath FROM entries e
|
|
551
565
|
WHERE NOT EXISTS (SELECT 1 FROM embeddings b WHERE b.id = e.id)
|
|
552
566
|
`)
|
|
553
567
|
.all();
|
|
@@ -97,7 +97,7 @@ export function parentDirHintMatcher(ctx) {
|
|
|
97
97
|
if (parentDir === "scripts" && SCRIPT_EXTENSIONS.has(ext)) {
|
|
98
98
|
return { type: "script", specificity: 15, renderer: "script-source" };
|
|
99
99
|
}
|
|
100
|
-
if (parentDir === "skills" && fileName === "SKILL.md") {
|
|
100
|
+
if (parentDir === "skills" && (fileName === "SKILL.md" || ext === ".md")) {
|
|
101
101
|
return { type: "skill", specificity: 15, renderer: "skill-md" };
|
|
102
102
|
}
|
|
103
103
|
if (parentDir === "agents" && ext === ".md") {
|
|
@@ -224,7 +224,9 @@ function isValidDirectory(dir) {
|
|
|
224
224
|
*/
|
|
225
225
|
export async function ensureSourceCaches(config) {
|
|
226
226
|
const cfg = config ?? loadConfig();
|
|
227
|
-
|
|
227
|
+
// Use sources[] (current key) with fallback to stashes[] (deprecated, one-release compat).
|
|
228
|
+
const entries = cfg.sources ?? cfg.stashes ?? [];
|
|
229
|
+
for (const entry of entries) {
|
|
228
230
|
if (!GIT_STASH_TYPES.has(entry.type) || !entry.url || entry.enabled === false)
|
|
229
231
|
continue;
|
|
230
232
|
try {
|
|
@@ -236,7 +238,7 @@ export async function ensureSourceCaches(config) {
|
|
|
236
238
|
warn(`Warning: failed to refresh git mirror for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
|
-
for (const entry of
|
|
241
|
+
for (const entry of entries) {
|
|
240
242
|
if (entry.type !== "website" || !entry.url || entry.enabled === false)
|
|
241
243
|
continue;
|
|
242
244
|
try {
|
|
@@ -12,6 +12,32 @@
|
|
|
12
12
|
* this is a pre-emptive guarantee against the #222 invariant.
|
|
13
13
|
*/
|
|
14
14
|
import { DEFAULT_AGENT_TIMEOUT_MS } from "./config";
|
|
15
|
+
/**
|
|
16
|
+
* Kill the process group of `proc` with `signal`, falling back to
|
|
17
|
+
* `proc.kill(signal)` when `proc.pid` is unavailable (e.g. test fakes).
|
|
18
|
+
*
|
|
19
|
+
* Passing a negative PID to `process.kill` targets the entire process
|
|
20
|
+
* group, so opencode's child processes (the .opencode binary, etc.) are
|
|
21
|
+
* reaped alongside the node wrapper. The fallback keeps test fakes working
|
|
22
|
+
* without modification.
|
|
23
|
+
*/
|
|
24
|
+
export function killGroup(proc, signal) {
|
|
25
|
+
if (typeof proc.pid === "number") {
|
|
26
|
+
try {
|
|
27
|
+
process.kill(-proc.pid, signal);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Process may have already exited; fall through to direct kill.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
proc.kill(signal);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* ignore */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
15
41
|
const DEFAULT_TIMEOUT_MS = DEFAULT_AGENT_TIMEOUT_MS;
|
|
16
42
|
function resolveSpawnFn(options) {
|
|
17
43
|
if (options.spawn)
|
|
@@ -47,15 +73,21 @@ function buildChildEnv(profile, options) {
|
|
|
47
73
|
}
|
|
48
74
|
return env;
|
|
49
75
|
}
|
|
50
|
-
async function readStream(stream) {
|
|
76
|
+
async function readStream(stream, opts) {
|
|
51
77
|
if (!stream)
|
|
52
78
|
return "";
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
const readPromise = new Response(stream).text().catch(() => "");
|
|
80
|
+
if (!opts?.timeoutMs)
|
|
81
|
+
return readPromise;
|
|
82
|
+
// Race the stream read against a timeout so a process that is killed via
|
|
83
|
+
// SIGTERM/SIGKILL but whose pipe endpoints stay open (e.g. background
|
|
84
|
+
// threads still holding the fd) cannot block the caller indefinitely.
|
|
85
|
+
// On timeout we return whatever we received so far (empty string here since
|
|
86
|
+
// `readPromise` is all-or-nothing with `Response.text()`).
|
|
87
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
88
|
+
setTimeout(() => resolve(""), opts.timeoutMs);
|
|
89
|
+
});
|
|
90
|
+
return Promise.race([readPromise, timeoutPromise]);
|
|
59
91
|
}
|
|
60
92
|
/**
|
|
61
93
|
* Spawn the agent CLI described by `profile` with `prompt` (forwarded as
|
|
@@ -90,11 +122,16 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
90
122
|
try {
|
|
91
123
|
const spawnFn = resolveSpawnFn(options);
|
|
92
124
|
proc = spawnFn([profile.bin, ...args], {
|
|
93
|
-
stdin: stdioMode === "captured" ? "pipe" : "inherit",
|
|
125
|
+
stdin: stdioMode === "captured" ? (options.stdin !== undefined ? "pipe" : "ignore") : "inherit",
|
|
94
126
|
stdout: stdioMode === "captured" ? "pipe" : "inherit",
|
|
95
127
|
stderr: stdioMode === "captured" ? "pipe" : "inherit",
|
|
96
128
|
env,
|
|
97
129
|
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
130
|
+
// Spawn in its own process group so killGroup(-pid, signal) reaches all
|
|
131
|
+
// descendants (e.g. the .opencode binary that opencode's node wrapper forks).
|
|
132
|
+
// Only applied in captured mode — interactive mode inherits the parent
|
|
133
|
+
// terminal's process group intentionally.
|
|
134
|
+
...(stdioMode === "captured" ? { detached: true } : {}),
|
|
98
135
|
});
|
|
99
136
|
}
|
|
100
137
|
catch (err) {
|
|
@@ -121,15 +158,27 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
121
158
|
if (proc.exitCode !== null)
|
|
122
159
|
return;
|
|
123
160
|
timedOut = true;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
161
|
+
killGroup(proc, "SIGTERM");
|
|
162
|
+
// Follow up with SIGKILL after 5 s in case the process ignores SIGTERM.
|
|
163
|
+
setTimeoutImpl(() => {
|
|
164
|
+
if (proc.exitCode !== null)
|
|
165
|
+
return;
|
|
166
|
+
killGroup(proc, "SIGKILL");
|
|
167
|
+
}, 5000);
|
|
130
168
|
}, timeoutMs);
|
|
131
|
-
|
|
132
|
-
|
|
169
|
+
// Stream-drain timeout: the overall wall-clock budget plus a 2 s grace
|
|
170
|
+
// period. When a process is killed via SIGTERM/SIGKILL (from our timeout
|
|
171
|
+
// handler or from outside) some runtimes keep the pipe write-end open in
|
|
172
|
+
// background threads, which would cause `Response.text()` to block forever.
|
|
173
|
+
// Capping stream draining at `timeoutMs + 2 000 ms` ensures the caller
|
|
174
|
+
// never hangs past the wall budget regardless of subprocess pipe behaviour.
|
|
175
|
+
const streamDrainTimeoutMs = timeoutMs + 2_000;
|
|
176
|
+
const stdoutPromise = stdioMode === "captured"
|
|
177
|
+
? readStream(proc.stdout ?? null, { timeoutMs: streamDrainTimeoutMs })
|
|
178
|
+
: Promise.resolve("");
|
|
179
|
+
const stderrPromise = stdioMode === "captured"
|
|
180
|
+
? readStream(proc.stderr ?? null, { timeoutMs: streamDrainTimeoutMs })
|
|
181
|
+
: Promise.resolve("");
|
|
133
182
|
// Optional stdin payload (captured mode only).
|
|
134
183
|
//
|
|
135
184
|
// BUG-H1: race the stdin write/close against `proc.exited` and the
|
|
@@ -163,6 +212,8 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
163
212
|
clearTimeoutImpl(timer);
|
|
164
213
|
// BUG-H2: drain stream readers before the early return so they don't
|
|
165
214
|
// surface as unhandled rejections after the function resolves.
|
|
215
|
+
// The streams already carry a built-in drain timeout so this allSettled
|
|
216
|
+
// will not block indefinitely.
|
|
166
217
|
await Promise.allSettled([stdoutPromise, stderrPromise]);
|
|
167
218
|
const durationMs = Date.now() - start;
|
|
168
219
|
return {
|
|
@@ -2,8 +2,13 @@ import * as childProcess from "node:child_process";
|
|
|
2
2
|
export const GITHUB_API_BASE = "https://api.github.com";
|
|
3
3
|
const GITHUB_TOKEN_DOMAINS = new Set(["api.github.com", "github.com", "uploads.github.com"]);
|
|
4
4
|
function readGithubTokenFromEnv() {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
if (process.env.GITHUB_TOKEN !== undefined) {
|
|
6
|
+
return process.env.GITHUB_TOKEN.trim();
|
|
7
|
+
}
|
|
8
|
+
if (process.env.GH_TOKEN !== undefined) {
|
|
9
|
+
return process.env.GH_TOKEN.trim();
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
7
12
|
}
|
|
8
13
|
function readGithubTokenFromGhCli() {
|
|
9
14
|
const result = childProcess.spawnSync("gh", ["auth", "token"], {
|
|
@@ -17,7 +22,8 @@ function readGithubTokenFromGhCli() {
|
|
|
17
22
|
return token || undefined;
|
|
18
23
|
}
|
|
19
24
|
function resolveGithubToken() {
|
|
20
|
-
|
|
25
|
+
const token = readGithubTokenFromEnv();
|
|
26
|
+
return token !== undefined ? token || undefined : readGithubTokenFromGhCli();
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
29
|
* Build headers for GitHub API requests.
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
* vectors so the scoring pipeline's L2-to-cosine conversion is correct.
|
|
6
6
|
*/
|
|
7
7
|
import { fetchWithTimeout, isHttpUrl } from "../../core/common";
|
|
8
|
-
const
|
|
8
|
+
const DEFAULT_REMOTE_BATCH_SIZE = 100;
|
|
9
|
+
/** Cheap token estimator: 4 chars ≈ 1 token. Used in verbose logging and error messages. */
|
|
10
|
+
export function estimateTokenCount(text) {
|
|
11
|
+
return Math.round(text.length / 4);
|
|
12
|
+
}
|
|
9
13
|
export class RemoteEmbedder {
|
|
10
14
|
config;
|
|
11
15
|
constructor(config) {
|
|
@@ -20,6 +24,10 @@ export class RemoteEmbedder {
|
|
|
20
24
|
if (this.config.dimension) {
|
|
21
25
|
body.dimensions = this.config.dimension;
|
|
22
26
|
}
|
|
27
|
+
const ollamaOpts = resolveOllamaOptions(this.config);
|
|
28
|
+
if (ollamaOpts) {
|
|
29
|
+
body.options = ollamaOpts;
|
|
30
|
+
}
|
|
23
31
|
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.config.endpoint), {
|
|
24
32
|
method: "POST",
|
|
25
33
|
headers,
|
|
@@ -40,8 +48,10 @@ export class RemoteEmbedder {
|
|
|
40
48
|
return [];
|
|
41
49
|
const results = [];
|
|
42
50
|
const headers = this.buildHeaders();
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
const ollamaOpts = resolveOllamaOptions(this.config);
|
|
52
|
+
const batchSize = this.config.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
|
|
53
|
+
for (let i = 0; i < texts.length; i += batchSize) {
|
|
54
|
+
const batch = texts.slice(i, i + batchSize);
|
|
45
55
|
const body = {
|
|
46
56
|
input: batch,
|
|
47
57
|
model: this.config.model,
|
|
@@ -49,6 +59,9 @@ export class RemoteEmbedder {
|
|
|
49
59
|
if (this.config.dimension) {
|
|
50
60
|
body.dimensions = this.config.dimension;
|
|
51
61
|
}
|
|
62
|
+
if (ollamaOpts) {
|
|
63
|
+
body.options = ollamaOpts;
|
|
64
|
+
}
|
|
52
65
|
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.config.endpoint), {
|
|
53
66
|
method: "POST",
|
|
54
67
|
headers,
|
|
@@ -115,6 +128,27 @@ function embeddingEndpointPathHint(endpoint) {
|
|
|
115
128
|
}
|
|
116
129
|
return "";
|
|
117
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve Ollama-native `options` from the embedding config.
|
|
133
|
+
*
|
|
134
|
+
* Resolution order:
|
|
135
|
+
* 1. `ollamaOptions` — forwarded verbatim (explicit opt-in, takes precedence).
|
|
136
|
+
* 2. `contextLength` — wrapped as `{ num_ctx: contextLength }`.
|
|
137
|
+
* 3. Neither set → returns `undefined` (no `options` field in the request body).
|
|
138
|
+
*
|
|
139
|
+
* These options are only meaningful for Ollama's native `/api/embed` endpoint.
|
|
140
|
+
* OpenAI-compatible endpoints ignore unknown request fields, so passing them to
|
|
141
|
+
* other providers is harmless but has no effect.
|
|
142
|
+
*/
|
|
143
|
+
function resolveOllamaOptions(config) {
|
|
144
|
+
if (config.ollamaOptions && Object.keys(config.ollamaOptions).length > 0) {
|
|
145
|
+
return config.ollamaOptions;
|
|
146
|
+
}
|
|
147
|
+
if (config.contextLength) {
|
|
148
|
+
return { num_ctx: config.contextLength };
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
118
152
|
/** Check whether an EmbeddingConnectionConfig has a valid remote endpoint. */
|
|
119
153
|
export function hasRemoteEndpoint(config) {
|
|
120
154
|
return isHttpUrl(config.endpoint);
|
|
@@ -11,6 +11,19 @@ const EMBEDDED_HINTS = `# akm CLI
|
|
|
11
11
|
|
|
12
12
|
You have access to a searchable library of scripts, skills, commands, agents, knowledge documents, workflows, wikis, and memories via \`akm\`. Search your sources first before writing something from scratch.
|
|
13
13
|
|
|
14
|
+
## Agent Task Loop
|
|
15
|
+
|
|
16
|
+
For any task, follow this loop:
|
|
17
|
+
1. \`akm curate "<task>"\` — find the best matching asset
|
|
18
|
+
2. \`akm show <ref>\` — read the schema (field names and structure)
|
|
19
|
+
3. Edit the workspace file using schema field names + task-specific values from your README
|
|
20
|
+
4. \`akm feedback <ref> --positive\` — record success
|
|
21
|
+
|
|
22
|
+
For workflow tasks:
|
|
23
|
+
1. \`akm workflow next workflow:<name>\` — get current step instructions
|
|
24
|
+
2. Do the step work in your workspace
|
|
25
|
+
3. \`akm workflow complete <run-id> --step <step-id>\` — mark done, get next step
|
|
26
|
+
|
|
14
27
|
## Quick Reference
|
|
15
28
|
|
|
16
29
|
\`\`\`sh
|
|
@@ -144,7 +157,7 @@ akm wiki list # List wikis (name, pages, raws,
|
|
|
144
157
|
akm wiki create research # Scaffold a new wiki
|
|
145
158
|
akm wiki register ics-docs ~/code/ics-documentation # Register an external wiki
|
|
146
159
|
akm wiki show research # Path, description, counts, last 3 log entries
|
|
147
|
-
akm wiki pages research # Page refs + descriptions (excludes schema/index/log/
|
|
160
|
+
akm wiki pages research # Page refs + descriptions (excludes schema/index/log; includes raw/)
|
|
148
161
|
akm wiki search research "attention" # Scoped search (equivalent to --type wiki --wiki research)
|
|
149
162
|
akm wiki stash research ./paper.md # Copy source into raw/<slug>.md (never overwrites)
|
|
150
163
|
echo "..." | akm wiki stash research - # stdin form
|
|
@@ -254,7 +267,7 @@ akm registry add <url> --provider skills-sh # Specify provider type
|
|
|
254
267
|
akm registry remove <url-or-name> # Remove a registry
|
|
255
268
|
akm registry search "<query>" # Search all registries
|
|
256
269
|
akm registry search "<query>" --assets # Include asset-level results
|
|
257
|
-
akm registry build-index # Build
|
|
270
|
+
akm registry build-index # Build the default cache-backed index.json
|
|
258
271
|
akm registry build-index --out dist/index.json # Build to a custom path
|
|
259
272
|
\`\`\`
|
|
260
273
|
|
|
@@ -187,12 +187,14 @@ const skillMdRenderer = {
|
|
|
187
187
|
name: "skill-md",
|
|
188
188
|
buildShowResponse(ctx) {
|
|
189
189
|
const name = deriveName(ctx);
|
|
190
|
+
const parsed = parseFrontmatter(ctx.content());
|
|
190
191
|
return {
|
|
191
192
|
type: "skill",
|
|
192
193
|
name,
|
|
193
194
|
path: ctx.absPath,
|
|
194
195
|
action: "Read and follow the instructions below",
|
|
195
|
-
|
|
196
|
+
description: toStringOrUndefined(parsed.data.description),
|
|
197
|
+
content: parsed.content,
|
|
196
198
|
};
|
|
197
199
|
},
|
|
198
200
|
};
|
|
@@ -293,6 +293,10 @@ export function shapeHistoryOutput(result, detail) {
|
|
|
293
293
|
...(result.since !== undefined ? { since: result.since } : {}),
|
|
294
294
|
totalCount: result.totalCount ?? shapedEntries.length,
|
|
295
295
|
entries: shapedEntries,
|
|
296
|
+
// `sources` lists the event sources included in this response.
|
|
297
|
+
// Always contains "usage_events"; also "events.jsonl" when
|
|
298
|
+
// --include-proposals was specified.
|
|
299
|
+
...(Array.isArray(result.sources) ? { sources: result.sources } : {}),
|
|
296
300
|
...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
|
|
297
301
|
};
|
|
298
302
|
}
|
|
@@ -301,6 +305,7 @@ export function shapeHistoryOutput(result, detail) {
|
|
|
301
305
|
...(result.since !== undefined ? { since: result.since } : {}),
|
|
302
306
|
totalCount: result.totalCount ?? shapedEntries.length,
|
|
303
307
|
entries: shapedEntries,
|
|
308
|
+
...(Array.isArray(result.sources) ? { sources: result.sources } : {}),
|
|
304
309
|
...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
|
|
305
310
|
};
|
|
306
311
|
}
|
|
@@ -399,8 +404,10 @@ export function shapeSearchHit(hit, detail) {
|
|
|
399
404
|
return hit;
|
|
400
405
|
}
|
|
401
406
|
// Stash hit (local or remote)
|
|
407
|
+
// `ref` is included at `brief` so agents can run `akm show <ref>` without
|
|
408
|
+
// needing --detail full or --for-agent (REC-03).
|
|
402
409
|
if (detail === "brief")
|
|
403
|
-
return pickFields(hit, ["type", "name", "action", "estimatedTokens"]);
|
|
410
|
+
return pickFields(hit, ["type", "name", "ref", "action", "estimatedTokens"]);
|
|
404
411
|
if (detail === "normal") {
|
|
405
412
|
// `warnings` is projected at `normal` so non-fatal hit-level issues are
|
|
406
413
|
// visible without forcing callers up to `--detail full`. Optional
|