akm-cli 0.8.0-rc1 → 0.8.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/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2162 -1258
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1533 -144
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +260 -5
- package/dist/commands/health.js +977 -51
- package/dist/commands/help/help-accept.md +6 -3
- package/dist/commands/help/help-improve.md +36 -8
- package/dist/commands/help/help-proposals.md +7 -4
- package/dist/commands/help/help-reject.md +5 -2
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +184 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1725 -332
- package/dist/commands/info.js +3 -0
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +233 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +17 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +662 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +84 -14
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +114 -48
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +92 -56
- package/dist/llm/graph-extract.js +401 -30
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
- package/dist/output/cli-hints-full.md +60 -32
- package/dist/output/cli-hints-short.md +10 -7
- package/dist/output/cli-hints.js +5 -2
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +105 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -549
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1059 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +12 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1329
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17767 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +3 -0
- package/dist/tasks/backends/schtasks.js +3 -0
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +3 -0
- package/dist/wiki/wiki.js +3 -0
- package/dist/workflows/authoring.js +3 -0
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -307
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
|
@@ -1,52 +1,258 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import fs from "node:fs";
|
|
2
5
|
import os from "node:os";
|
|
3
6
|
import path from "node:path";
|
|
4
|
-
|
|
7
|
+
import { extractInlineRefMentions } from "../inline-refs";
|
|
8
|
+
function getOpenCodeBaseDir() {
|
|
5
9
|
if (process.platform === "darwin") {
|
|
6
10
|
return path.join(os.homedir(), "Library", "Application Support", "opencode");
|
|
7
11
|
}
|
|
8
12
|
return path.join(os.homedir(), ".local", "share", "opencode");
|
|
9
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Opencode storage layout (observed 2026-05):
|
|
16
|
+
* <base>/storage/session/<projectId>/<sessionId>.json — metadata
|
|
17
|
+
* <base>/storage/message/<sessionId>/<messageId>.json — one per message
|
|
18
|
+
*
|
|
19
|
+
* Older builds wrote logs directly into `<base>/log/` and `<base>/*.log`;
|
|
20
|
+
* those are still scanned by {@link OpenCodeProvider.readEvents} for
|
|
21
|
+
* backward compatibility with the existing failure-pattern aggregator.
|
|
22
|
+
*/
|
|
10
23
|
export class OpenCodeProvider {
|
|
11
24
|
name = "opencode";
|
|
12
|
-
#
|
|
25
|
+
#baseDir = getOpenCodeBaseDir();
|
|
13
26
|
isAvailable() {
|
|
14
|
-
return fs.existsSync(this.#
|
|
27
|
+
return fs.existsSync(this.#baseDir);
|
|
15
28
|
}
|
|
16
29
|
*readEvents(input) {
|
|
30
|
+
// Legacy behavior: stream raw log lines from the top-level dir and `log/`
|
|
31
|
+
// subdirectory. Kept to keep `getExecutionLogCandidates` working without
|
|
32
|
+
// a coordinated change to its caller. New code should use
|
|
33
|
+
// {@link listSessions} + {@link readSession} instead.
|
|
34
|
+
const candidates = [this.#baseDir, path.join(this.#baseDir, "log")];
|
|
35
|
+
for (const dir of candidates) {
|
|
36
|
+
if (!fs.existsSync(dir))
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
for (const file of fs.readdirSync(dir)) {
|
|
40
|
+
const full = path.join(dir, file);
|
|
41
|
+
let stat;
|
|
42
|
+
try {
|
|
43
|
+
stat = fs.statSync(full);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!stat.isFile())
|
|
49
|
+
continue;
|
|
50
|
+
if (stat.mtimeMs < input.sinceMs)
|
|
51
|
+
continue;
|
|
52
|
+
if (!file.endsWith(".json") && !file.endsWith(".jsonl") && !file.endsWith(".log"))
|
|
53
|
+
continue;
|
|
54
|
+
const content = fs.readFileSync(full, "utf8");
|
|
55
|
+
const lines = content.includes("\n") ? content.split("\n") : [content];
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
try {
|
|
58
|
+
const entry = JSON.parse(line);
|
|
59
|
+
const text = entry?.content ?? entry?.message ?? entry?.text ?? "";
|
|
60
|
+
if (typeof text !== "string" || text.length < 10)
|
|
61
|
+
continue;
|
|
62
|
+
yield {
|
|
63
|
+
harness: this.name,
|
|
64
|
+
text,
|
|
65
|
+
ts: typeof entry?.timestamp === "number" ? entry.timestamp : stat.mtimeMs,
|
|
66
|
+
sessionId: typeof entry?.sessionId === "string" ? entry.sessionId : undefined,
|
|
67
|
+
role: typeof entry?.role === "string" ? entry.role : "unknown",
|
|
68
|
+
filePath: full,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// skip malformed
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// unreadable dir — skip
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
listSessions(input = {}) {
|
|
83
|
+
const base = input.location ?? this.#baseDir;
|
|
84
|
+
const sinceMs = input.sinceMs ?? 0;
|
|
85
|
+
const sessionRoot = path.join(base, "storage", "session");
|
|
86
|
+
if (!fs.existsSync(sessionRoot))
|
|
87
|
+
return [];
|
|
88
|
+
const summaries = [];
|
|
17
89
|
try {
|
|
18
|
-
for (const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
90
|
+
for (const projectId of fs.readdirSync(sessionRoot)) {
|
|
91
|
+
const projectDir = path.join(sessionRoot, projectId);
|
|
92
|
+
let pstat;
|
|
93
|
+
try {
|
|
94
|
+
pstat = fs.statSync(projectDir);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
22
97
|
continue;
|
|
23
|
-
|
|
98
|
+
}
|
|
99
|
+
if (!pstat.isDirectory())
|
|
24
100
|
continue;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
101
|
+
for (const file of fs.readdirSync(projectDir)) {
|
|
102
|
+
if (!file.endsWith(".json"))
|
|
103
|
+
continue;
|
|
104
|
+
const filePath = path.join(projectDir, file);
|
|
105
|
+
let stat;
|
|
28
106
|
try {
|
|
29
|
-
|
|
30
|
-
const text = entry?.content ?? entry?.message ?? entry?.text ?? "";
|
|
31
|
-
if (typeof text !== "string" || text.length < 10)
|
|
32
|
-
continue;
|
|
33
|
-
yield {
|
|
34
|
-
harness: this.name,
|
|
35
|
-
text,
|
|
36
|
-
ts: typeof entry?.timestamp === "number" ? entry.timestamp : stat.mtimeMs,
|
|
37
|
-
sessionId: typeof entry?.sessionId === "string" ? entry.sessionId : undefined,
|
|
38
|
-
role: typeof entry?.role === "string" ? entry.role : "unknown",
|
|
39
|
-
filePath: full,
|
|
40
|
-
};
|
|
107
|
+
stat = fs.statSync(filePath);
|
|
41
108
|
}
|
|
42
109
|
catch {
|
|
43
|
-
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (stat.mtimeMs < sinceMs)
|
|
113
|
+
continue;
|
|
114
|
+
let meta;
|
|
115
|
+
try {
|
|
116
|
+
meta = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
44
117
|
}
|
|
118
|
+
catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const sessionId = typeof meta?.id === "string" ? meta.id : path.basename(file, ".json");
|
|
122
|
+
const time = meta?.time ?? undefined;
|
|
123
|
+
const startedAt = typeof time?.created === "number" ? time.created : stat.ctimeMs;
|
|
124
|
+
const endedAt = typeof time?.updated === "number" ? time.updated : stat.mtimeMs;
|
|
125
|
+
const title = typeof meta?.title === "string" ? meta.title : undefined;
|
|
126
|
+
const projectHint = typeof meta?.directory === "string" ? meta.directory : projectId;
|
|
127
|
+
summaries.push({
|
|
128
|
+
harness: this.name,
|
|
129
|
+
sessionId,
|
|
130
|
+
filePath,
|
|
131
|
+
startedAt,
|
|
132
|
+
endedAt,
|
|
133
|
+
projectHint,
|
|
134
|
+
...(title ? { title } : {}),
|
|
135
|
+
});
|
|
45
136
|
}
|
|
46
137
|
}
|
|
47
138
|
}
|
|
48
139
|
catch {
|
|
49
|
-
return
|
|
140
|
+
// unreadable session root — return what we have
|
|
141
|
+
}
|
|
142
|
+
return summaries.sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0));
|
|
143
|
+
}
|
|
144
|
+
readSession(ref) {
|
|
145
|
+
let meta = {};
|
|
146
|
+
try {
|
|
147
|
+
meta = JSON.parse(fs.readFileSync(ref.filePath, "utf8"));
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// metadata missing — proceed with empty defaults
|
|
151
|
+
}
|
|
152
|
+
const time = meta.time ?? undefined;
|
|
153
|
+
const startedAt = typeof time?.created === "number" ? time.created : undefined;
|
|
154
|
+
const endedAt = typeof time?.updated === "number" ? time.updated : undefined;
|
|
155
|
+
const title = typeof meta.title === "string" ? meta.title : undefined;
|
|
156
|
+
const projectHint = typeof meta.directory === "string" ? meta.directory : undefined;
|
|
157
|
+
const events = [];
|
|
158
|
+
const inlineRefs = [];
|
|
159
|
+
// Resolve message directory: <baseDir>/storage/message/<sessionId>/
|
|
160
|
+
const inferredBase = this.#inferBaseFromSessionPath(ref.filePath) ?? this.#baseDir;
|
|
161
|
+
const msgDir = path.join(inferredBase, "storage", "message", ref.sessionId);
|
|
162
|
+
if (fs.existsSync(msgDir)) {
|
|
163
|
+
try {
|
|
164
|
+
const files = fs.readdirSync(msgDir).filter((f) => f.endsWith(".json"));
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
const full = path.join(msgDir, file);
|
|
167
|
+
let msg;
|
|
168
|
+
try {
|
|
169
|
+
msg = JSON.parse(fs.readFileSync(full, "utf8"));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (!msg)
|
|
175
|
+
continue;
|
|
176
|
+
const evt = this.#messageToEvent(msg, ref.sessionId, full);
|
|
177
|
+
if (evt) {
|
|
178
|
+
events.push(evt);
|
|
179
|
+
inlineRefs.push(...extractInlineRefMentions(evt.text, evt.ts));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// unreadable msg dir — skip
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
events.sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
|
|
188
|
+
return {
|
|
189
|
+
ref: {
|
|
190
|
+
harness: this.name,
|
|
191
|
+
sessionId: ref.sessionId,
|
|
192
|
+
filePath: ref.filePath,
|
|
193
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
194
|
+
...(endedAt !== undefined ? { endedAt } : {}),
|
|
195
|
+
...(projectHint ? { projectHint } : {}),
|
|
196
|
+
...(title ? { title } : {}),
|
|
197
|
+
},
|
|
198
|
+
events,
|
|
199
|
+
inlineRefs,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Derive opencode base dir from a session metadata file path so a caller
|
|
204
|
+
* passing a custom `--location` can still find the message dir.
|
|
205
|
+
* Layout: `<base>/storage/session/<projectId>/<id>.json` → base.
|
|
206
|
+
*/
|
|
207
|
+
#inferBaseFromSessionPath(filePath) {
|
|
208
|
+
// Walk up: <id>.json → <projectId> → session → storage → <base>
|
|
209
|
+
const dir = path.dirname(filePath);
|
|
210
|
+
const parts = dir.split(path.sep);
|
|
211
|
+
if (parts.length < 3)
|
|
212
|
+
return undefined;
|
|
213
|
+
const last = parts[parts.length - 1];
|
|
214
|
+
const sndLast = parts[parts.length - 2];
|
|
215
|
+
const thirdLast = parts[parts.length - 3];
|
|
216
|
+
if (sndLast !== "session" || thirdLast !== "storage" || !last)
|
|
217
|
+
return undefined;
|
|
218
|
+
return parts.slice(0, parts.length - 3).join(path.sep);
|
|
219
|
+
}
|
|
220
|
+
#messageToEvent(msg, sessionId, filePath) {
|
|
221
|
+
const time = msg.time ?? undefined;
|
|
222
|
+
const ts = typeof time?.created === "number" ? time.created : typeof msg.timestamp === "number" ? msg.timestamp : 0;
|
|
223
|
+
const role = typeof msg.role === "string" ? msg.role : "unknown";
|
|
224
|
+
// Opencode message bodies live in summary.title / summary.diffs[].before/after /
|
|
225
|
+
// parts (referenced from storage/part/<msg-id>/). For listing+extraction
|
|
226
|
+
// purposes the summary block is sufficient — it's what the platform itself
|
|
227
|
+
// surfaces as the message preview.
|
|
228
|
+
const summary = msg.summary;
|
|
229
|
+
const parts = [];
|
|
230
|
+
if (typeof summary?.title === "string")
|
|
231
|
+
parts.push(summary.title);
|
|
232
|
+
if (Array.isArray(summary?.parts)) {
|
|
233
|
+
for (const p of summary.parts) {
|
|
234
|
+
if (typeof p === "string")
|
|
235
|
+
parts.push(p);
|
|
236
|
+
else if (p && typeof p === "object") {
|
|
237
|
+
const text = p.text;
|
|
238
|
+
if (typeof text === "string")
|
|
239
|
+
parts.push(text);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
50
242
|
}
|
|
243
|
+
// content field for some opencode versions
|
|
244
|
+
if (typeof msg.content === "string")
|
|
245
|
+
parts.push(msg.content);
|
|
246
|
+
const text = parts.join("\n").trim();
|
|
247
|
+
if (text.length < 1)
|
|
248
|
+
return undefined;
|
|
249
|
+
return {
|
|
250
|
+
harness: this.name,
|
|
251
|
+
text,
|
|
252
|
+
ts: ts || undefined,
|
|
253
|
+
sessionId,
|
|
254
|
+
role,
|
|
255
|
+
filePath,
|
|
256
|
+
};
|
|
51
257
|
}
|
|
52
258
|
}
|
package/dist/llm/call-ai.js
CHANGED
|
@@ -1,37 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* NOT for use by background indexer passes — those call `chatCompletion`
|
|
6
|
-
* directly to avoid the agent-CLI overhead and to stay on the HTTP path that
|
|
7
|
-
* the indexer was designed around.
|
|
8
|
-
*/
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { getDefaultLlmConfig } from "../core/config";
|
|
9
5
|
import { warn } from "../core/warn";
|
|
10
6
|
import { resolveAgentProfile, runAgent } from "../integrations/agent";
|
|
11
7
|
import { chatCompletion } from "./client";
|
|
12
8
|
/**
|
|
13
|
-
* Unified AI call: prefers
|
|
14
|
-
*
|
|
9
|
+
* Unified AI call: prefers the default agent profile, falls back to the
|
|
10
|
+
* default LLM profile. When neither is configured, returns a structured
|
|
15
11
|
* error pointing the user at `akm setup`.
|
|
16
|
-
*
|
|
17
|
-
* NOT for use by background indexer passes — those call `chatCompletion`
|
|
18
|
-
* directly.
|
|
19
12
|
*/
|
|
20
13
|
export async function callAi(config, prompt, opts = {}) {
|
|
21
|
-
|
|
14
|
+
const defaultAgentName = config.defaults?.agent;
|
|
15
|
+
if (defaultAgentName) {
|
|
22
16
|
try {
|
|
23
|
-
const
|
|
24
|
-
if (!defaultName) {
|
|
25
|
-
return {
|
|
26
|
-
ok: false,
|
|
27
|
-
error: "No default agent profile configured. Set `agent.default` in config.json or run `akm setup`.",
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
const profile = resolveAgentProfile(defaultName, config.agent.profiles?.[defaultName]);
|
|
17
|
+
const profile = resolveAgentProfile(defaultAgentName, config.profiles?.agent?.[defaultAgentName]);
|
|
31
18
|
if (!profile) {
|
|
32
19
|
return {
|
|
33
20
|
ok: false,
|
|
34
|
-
error: `Agent profile "${
|
|
21
|
+
error: `Agent profile "${defaultAgentName}" is not built-in and has no \`bin\` override.`,
|
|
35
22
|
};
|
|
36
23
|
}
|
|
37
24
|
const result = await runAgent(profile, prompt, {
|
|
@@ -47,7 +34,8 @@ export async function callAi(config, prompt, opts = {}) {
|
|
|
47
34
|
return { ok: false, error: String(e) };
|
|
48
35
|
}
|
|
49
36
|
}
|
|
50
|
-
|
|
37
|
+
const llmConfig = getDefaultLlmConfig(config);
|
|
38
|
+
if (llmConfig) {
|
|
51
39
|
if (opts.draftFilePath) {
|
|
52
40
|
warn("[akm] No agent CLI configured — falling back to LLM API. " +
|
|
53
41
|
"File-write contract unavailable; expecting JSON in stdout. " +
|
|
@@ -58,7 +46,7 @@ export async function callAi(config, prompt, opts = {}) {
|
|
|
58
46
|
messages.push({ role: "system", content: opts.systemPrompt });
|
|
59
47
|
messages.push({ role: "user", content: prompt });
|
|
60
48
|
try {
|
|
61
|
-
const content = await chatCompletion(
|
|
49
|
+
const content = await chatCompletion(llmConfig, messages, {
|
|
62
50
|
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
63
51
|
});
|
|
64
52
|
return { ok: true, content, path: "llm-http" };
|
|
@@ -69,6 +57,6 @@ export async function callAi(config, prompt, opts = {}) {
|
|
|
69
57
|
}
|
|
70
58
|
return {
|
|
71
59
|
ok: false,
|
|
72
|
-
error: "No AI connection configured. Run `akm setup` or set `agent
|
|
60
|
+
error: "No AI connection configured. Run `akm setup` or set `defaults.agent`/`defaults.llm`.",
|
|
73
61
|
};
|
|
74
62
|
}
|
package/dist/llm/client.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* Low-level OpenAI-compatible chat completions client and capability probing.
|
|
3
6
|
*
|
|
@@ -8,6 +11,7 @@
|
|
|
8
11
|
* `llm.ts` re-exports everything from this module for backward compatibility.
|
|
9
12
|
*/
|
|
10
13
|
import { fetchWithTimeout } from "../core/common";
|
|
14
|
+
import { resolveSecret } from "../core/config";
|
|
11
15
|
import { escapeJsonStringControls, parseJsonResponse, stripCodeFences, stripThinkBlocks } from "../core/parse";
|
|
12
16
|
// Re-export shared parse utilities so existing importers of `client.ts` continue
|
|
13
17
|
// to resolve `parseJsonResponse` and `parseEmbeddedJsonResponse` from this module.
|
|
@@ -55,13 +59,17 @@ export class LlmCallError extends Error {
|
|
|
55
59
|
export async function chatCompletion(config, messages, options) {
|
|
56
60
|
const timeoutMs = options?.timeoutMs ?? config.timeoutMs ?? 120_000;
|
|
57
61
|
const headers = { "Content-Type": "application/json" };
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
const resolvedKey = resolveSecret(config.apiKey);
|
|
63
|
+
if (resolvedKey) {
|
|
64
|
+
headers.Authorization = `Bearer ${resolvedKey}`;
|
|
60
65
|
}
|
|
61
66
|
// Only include max_tokens when explicitly set. The model/API knows its own
|
|
62
67
|
// limits; a hardcoded default creates silent truncation failures when the
|
|
63
68
|
// guess is wrong. Users who need a cap can set llm.maxTokens in config.
|
|
64
69
|
const resolvedMaxTokens = options?.maxTokens ?? config.maxTokens;
|
|
70
|
+
const responseFormat = options?.responseSchema && config.supportsJsonSchema
|
|
71
|
+
? { response_format: { type: "json_schema", json_schema: { schema: options.responseSchema, strict: true } } }
|
|
72
|
+
: {};
|
|
65
73
|
let response;
|
|
66
74
|
try {
|
|
67
75
|
response = await fetchWithTimeout(config.endpoint, {
|
|
@@ -72,6 +80,12 @@ export async function chatCompletion(config, messages, options) {
|
|
|
72
80
|
messages,
|
|
73
81
|
temperature: options?.temperature ?? config.temperature ?? 0.3,
|
|
74
82
|
...(resolvedMaxTokens !== undefined ? { max_tokens: resolvedMaxTokens } : {}),
|
|
83
|
+
...responseFormat,
|
|
84
|
+
...(options?.enableThinking !== undefined
|
|
85
|
+
? { enable_thinking: options.enableThinking }
|
|
86
|
+
: config.enableThinking !== undefined
|
|
87
|
+
? { enable_thinking: config.enableThinking }
|
|
88
|
+
: {}),
|
|
75
89
|
...config.extraParams,
|
|
76
90
|
}),
|
|
77
91
|
}, timeoutMs, options?.signal);
|
package/dist/llm/embedder.js
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* The implementation has been split into:
|
|
5
|
-
* - `./embedders/types` — `EmbeddingVector`, `Embedder`, `EmbeddingCheckResult`
|
|
6
|
-
* - `./embedders/local` — `LocalEmbedder`, `DEFAULT_LOCAL_MODEL`,
|
|
7
|
-
* `isTransformersAvailable`
|
|
8
|
-
* - `./embedders/remote` — `RemoteEmbedder`, `hasRemoteEndpoint`
|
|
9
|
-
* - `./embedders/cache` — LRU `embedCache`, `clearEmbeddingCache`,
|
|
10
|
-
* `embedCacheKey`
|
|
11
|
-
*
|
|
12
|
-
* This module wires them together: it picks the right implementation from the
|
|
13
|
-
* (optional) embedding config, applies the cache layer, and re-exports the
|
|
14
|
-
* existing public API so call sites (`db-search.ts`, `indexer.ts`, `db.ts`,
|
|
15
|
-
* `setup.ts`, `semantic-status.ts`, tests) keep working unmodified.
|
|
16
|
-
*
|
|
17
|
-
* Tests can construct fresh `LocalEmbedder` / `RemoteEmbedder` instances
|
|
18
|
-
* directly from their submodules to avoid module-level state pollution.
|
|
19
|
-
*/
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
20
4
|
import { embedCacheKey, getCachedEmbedding, setCachedEmbedding } from "./embedders/cache";
|
|
21
5
|
import { isTransformersAvailable, LocalEmbedder } from "./embedders/local";
|
|
22
6
|
import { hasRemoteEndpoint, RemoteEmbedder } from "./embedders/remote";
|
|
@@ -24,18 +8,25 @@ import { hasRemoteEndpoint, RemoteEmbedder } from "./embedders/remote";
|
|
|
24
8
|
export { clearEmbeddingCache } from "./embedders/cache";
|
|
25
9
|
export { DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "./embedders/local";
|
|
26
10
|
// ── Singleton local embedder ────────────────────────────────────────────────
|
|
27
|
-
// `
|
|
28
|
-
// @huggingface/transformers pipeline is
|
|
29
|
-
// + WASM compilation) and is safe to
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
11
|
+
// `_localEmbedder` is an intentional module-level singleton but constructed
|
|
12
|
+
// lazily on first use. The underlying @huggingface/transformers pipeline is
|
|
13
|
+
// expensive to initialise (model download + WASM compilation) and is safe to
|
|
14
|
+
// share across calls because it is stateless once created. Deferring
|
|
15
|
+
// construction to first call keeps the module side-effect-free at import time,
|
|
16
|
+
// which matters for the test suite (single Bun process, ~120 test files).
|
|
17
|
+
let _localEmbedder;
|
|
18
|
+
function getLocalEmbedder() {
|
|
19
|
+
if (!_localEmbedder) {
|
|
20
|
+
_localEmbedder = new LocalEmbedder();
|
|
21
|
+
}
|
|
22
|
+
return _localEmbedder;
|
|
23
|
+
}
|
|
33
24
|
/**
|
|
34
25
|
* Reset the cached local embedder pipeline. Used by tests that want a fresh
|
|
35
26
|
* pipeline construction (e.g. to assert the dtype-fallback retry logic).
|
|
36
27
|
*/
|
|
37
28
|
export function resetLocalEmbedder() {
|
|
38
|
-
|
|
29
|
+
getLocalEmbedder().reset();
|
|
39
30
|
}
|
|
40
31
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
41
32
|
/**
|
|
@@ -54,7 +45,7 @@ export async function embed(text, embeddingConfig, signal) {
|
|
|
54
45
|
return cached;
|
|
55
46
|
const result = embeddingConfig && hasRemoteEndpoint(embeddingConfig)
|
|
56
47
|
? await new RemoteEmbedder(embeddingConfig).embed(text, signal)
|
|
57
|
-
: await
|
|
48
|
+
: await getLocalEmbedder().embed(text, signal);
|
|
58
49
|
setCachedEmbedding(key, result);
|
|
59
50
|
return result;
|
|
60
51
|
}
|
|
@@ -76,7 +67,7 @@ export async function embedBatch(texts, embeddingConfig, signal) {
|
|
|
76
67
|
if (signal?.aborted) {
|
|
77
68
|
throw signal.reason instanceof Error ? signal.reason : new Error("embedding interrupted");
|
|
78
69
|
}
|
|
79
|
-
results.push(await
|
|
70
|
+
results.push(await getLocalEmbedder().embedWithModel(text, localModel));
|
|
80
71
|
}
|
|
81
72
|
return results;
|
|
82
73
|
}
|
|
@@ -113,7 +104,7 @@ export async function checkEmbeddingAvailability(embeddingConfig) {
|
|
|
113
104
|
};
|
|
114
105
|
}
|
|
115
106
|
try {
|
|
116
|
-
await
|
|
107
|
+
await getLocalEmbedder().getPipeline(embeddingConfig?.localModel);
|
|
117
108
|
return { available: true };
|
|
118
109
|
}
|
|
119
110
|
catch (err) {
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Caches query embeddings to avoid redundant computation for repeated
|
|
5
|
-
* queries. Uses a simple Map with LRU eviction (delete + re-insert to move
|
|
6
|
-
* an entry to the most-recently-used end).
|
|
7
|
-
*/
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
8
4
|
const EMBED_CACHE_MAX = 100;
|
|
9
5
|
const embedCache = new Map();
|
|
10
6
|
/**
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* Local @huggingface/transformers embedder.
|
|
3
6
|
*
|
|
@@ -25,6 +28,31 @@ const LOCAL_EMBEDDER_FALLBACK_DTYPE = "auto";
|
|
|
25
28
|
function resolveLocalModelName(overrideModel) {
|
|
26
29
|
return overrideModel || DEFAULT_LOCAL_MODEL;
|
|
27
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Detect whether the current process is running from a Bun-compiled binary
|
|
33
|
+
* (i.e. `bun build --compile` produced a single executable). Bun marks the
|
|
34
|
+
* compiled binary with a synthesized `process.execPath` that ends in the
|
|
35
|
+
* binary name rather than `bun`, AND sets a flag we can probe.
|
|
36
|
+
*
|
|
37
|
+
* Used to gate the "install @huggingface/transformers" hint — that advice
|
|
38
|
+
* is impossible to follow from a single-binary install, so we replace it
|
|
39
|
+
* with the only working remediation (switch to npm/Bun install, or turn
|
|
40
|
+
* semantic search off). See #482.
|
|
41
|
+
*/
|
|
42
|
+
function isCompiledBinary() {
|
|
43
|
+
try {
|
|
44
|
+
const flag = Bun.embeddedFiles;
|
|
45
|
+
if (flag !== undefined)
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Bun not available (under Node tests, for example) — treat as not-binary.
|
|
50
|
+
}
|
|
51
|
+
const exec = (process.execPath || "").toLowerCase();
|
|
52
|
+
if (exec.endsWith("/akm") || exec.endsWith("\\akm.exe"))
|
|
53
|
+
return true;
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
28
56
|
export class LocalEmbedder {
|
|
29
57
|
defaultModel;
|
|
30
58
|
/**
|
|
@@ -93,7 +121,20 @@ export class LocalEmbedder {
|
|
|
93
121
|
catch (importError) {
|
|
94
122
|
const msg = importError instanceof Error ? importError.message : String(importError);
|
|
95
123
|
if (/Cannot find module|MODULE_NOT_FOUND|Cannot resolve/i.test(msg)) {
|
|
96
|
-
|
|
124
|
+
// #482: the prebuilt binary build is invoked with
|
|
125
|
+
// `bun install --omit optional` (release.yml), so binary users
|
|
126
|
+
// can NEVER load @huggingface/transformers. Telling them to
|
|
127
|
+
// `bun add` it is a dead-end — there is no install target.
|
|
128
|
+
// Detect the binary execution path and give the only working
|
|
129
|
+
// remediation: switch to the npm/Bun install of akm-cli, or
|
|
130
|
+
// turn off semantic search.
|
|
131
|
+
const isBinary = isCompiledBinary();
|
|
132
|
+
const hint = isBinary
|
|
133
|
+
? "You are running the prebuilt akm binary, which cannot load optional native dependencies. " +
|
|
134
|
+
"To enable semantic search, install akm-cli via Bun: `curl -fsSL https://bun.sh/install | bash && bun install -g akm-cli`. " +
|
|
135
|
+
"To keep using the binary, set `semanticSearchMode: off` in your config and use keyword-only FTS."
|
|
136
|
+
: "Install it with: `bun add @huggingface/transformers` (or `npm install @huggingface/transformers`).";
|
|
137
|
+
throw new Error(`Semantic search requires @huggingface/transformers. ${hint}`);
|
|
97
138
|
}
|
|
98
139
|
throw new Error(`Failed to load embedding runtime: ${msg}. Check platform compatibility.`);
|
|
99
140
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* OpenAI-compatible remote embedder.
|
|
3
6
|
*
|
|
@@ -5,6 +8,7 @@
|
|
|
5
8
|
* vectors so the scoring pipeline's L2-to-cosine conversion is correct.
|
|
6
9
|
*/
|
|
7
10
|
import { fetchWithTimeout, isHttpUrl } from "../../core/common";
|
|
11
|
+
import { resolveSecret } from "../../core/config";
|
|
8
12
|
const DEFAULT_REMOTE_BATCH_SIZE = 100;
|
|
9
13
|
/** Cheap token estimator: 4 chars ≈ 1 token. Used in verbose logging and error messages. */
|
|
10
14
|
export function estimateTokenCount(text) {
|
|
@@ -12,14 +16,21 @@ export function estimateTokenCount(text) {
|
|
|
12
16
|
}
|
|
13
17
|
export class RemoteEmbedder {
|
|
14
18
|
config;
|
|
19
|
+
endpoint;
|
|
20
|
+
model;
|
|
15
21
|
constructor(config) {
|
|
16
22
|
this.config = config;
|
|
23
|
+
if (!config.endpoint || !config.model) {
|
|
24
|
+
throw new Error("RemoteEmbedder requires both endpoint and model on the embedding config.");
|
|
25
|
+
}
|
|
26
|
+
this.endpoint = config.endpoint;
|
|
27
|
+
this.model = config.model;
|
|
17
28
|
}
|
|
18
29
|
async embed(text, signal) {
|
|
19
30
|
const headers = this.buildHeaders();
|
|
20
31
|
const body = {
|
|
21
32
|
input: text,
|
|
22
|
-
model: this.
|
|
33
|
+
model: this.model,
|
|
23
34
|
};
|
|
24
35
|
if (this.config.dimension) {
|
|
25
36
|
body.dimensions = this.config.dimension;
|
|
@@ -28,7 +39,7 @@ export class RemoteEmbedder {
|
|
|
28
39
|
if (ollamaOpts) {
|
|
29
40
|
body.options = ollamaOpts;
|
|
30
41
|
}
|
|
31
|
-
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.
|
|
42
|
+
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.endpoint), {
|
|
32
43
|
method: "POST",
|
|
33
44
|
headers,
|
|
34
45
|
body: JSON.stringify(body),
|
|
@@ -40,7 +51,7 @@ export class RemoteEmbedder {
|
|
|
40
51
|
}
|
|
41
52
|
const json = (await response.json());
|
|
42
53
|
if (!json.data?.[0]?.embedding) {
|
|
43
|
-
throw new Error(`Unexpected embedding response format: missing data[0].embedding.${embeddingEndpointPathHint(this.
|
|
54
|
+
throw new Error(`Unexpected embedding response format: missing data[0].embedding.${embeddingEndpointPathHint(this.endpoint)}`);
|
|
44
55
|
}
|
|
45
56
|
return l2Normalize(json.data[0].embedding);
|
|
46
57
|
}
|
|
@@ -55,7 +66,7 @@ export class RemoteEmbedder {
|
|
|
55
66
|
const batch = texts.slice(i, i + batchSize);
|
|
56
67
|
const body = {
|
|
57
68
|
input: batch,
|
|
58
|
-
model: this.
|
|
69
|
+
model: this.model,
|
|
59
70
|
};
|
|
60
71
|
if (this.config.dimension) {
|
|
61
72
|
body.dimensions = this.config.dimension;
|
|
@@ -63,7 +74,7 @@ export class RemoteEmbedder {
|
|
|
63
74
|
if (ollamaOpts) {
|
|
64
75
|
body.options = ollamaOpts;
|
|
65
76
|
}
|
|
66
|
-
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.
|
|
77
|
+
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.endpoint), {
|
|
67
78
|
method: "POST",
|
|
68
79
|
headers,
|
|
69
80
|
body: JSON.stringify(body),
|
|
@@ -75,7 +86,7 @@ export class RemoteEmbedder {
|
|
|
75
86
|
}
|
|
76
87
|
const json = (await response.json());
|
|
77
88
|
if (!json.data || json.data.length !== batch.length) {
|
|
78
|
-
throw new Error(`Unexpected embedding batch response: expected ${batch.length} embeddings, got ${json.data?.length ?? 0}.${embeddingEndpointPathHint(this.
|
|
89
|
+
throw new Error(`Unexpected embedding batch response: expected ${batch.length} embeddings, got ${json.data?.length ?? 0}.${embeddingEndpointPathHint(this.endpoint)}`);
|
|
79
90
|
}
|
|
80
91
|
// Sort by index to guarantee correct order (OpenAI API doesn't guarantee order)
|
|
81
92
|
const sorted = [...json.data].sort((a, b) => a.index - b.index);
|
|
@@ -90,8 +101,9 @@ export class RemoteEmbedder {
|
|
|
90
101
|
}
|
|
91
102
|
buildHeaders() {
|
|
92
103
|
const headers = { "Content-Type": "application/json" };
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
const resolvedKey = resolveSecret(this.config.apiKey);
|
|
105
|
+
if (resolvedKey) {
|
|
106
|
+
headers.Authorization = `Bearer ${resolvedKey}`;
|
|
95
107
|
}
|
|
96
108
|
return headers;
|
|
97
109
|
}
|