akm-cli 0.9.31 → 0.10.261002-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +420 -0
  2. package/STABILITY.md +99 -57
  3. package/dist/api-node.mjs +20 -0
  4. package/dist/api.d.ts +23 -0
  5. package/dist/api.js +45 -0
  6. package/dist/assets/hints/cli-hints-full.md +15 -29
  7. package/dist/assets/hints/cli-hints-short.md +2 -2
  8. package/dist/assets/improve-strategies/default.json +3 -3
  9. package/dist/assets/prompts/consolidate-pair.md +9 -7
  10. package/dist/assets/stash-skeleton/README.md +1 -1
  11. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
  12. package/dist/cli/shared.js +31 -48
  13. package/dist/cli/unknown-flags.js +6 -43
  14. package/dist/cli.js +8 -34
  15. package/dist/commands/agent/contribute-cli.js +1 -1
  16. package/dist/commands/completions.js +1 -2
  17. package/dist/commands/config-cli.js +5 -28
  18. package/dist/commands/env/env-cli.js +5 -5
  19. package/dist/commands/env/secret-cli.js +2 -2
  20. package/dist/commands/feedback-cli.js +75 -49
  21. package/dist/commands/health/checks.js +41 -16
  22. package/dist/commands/health/plugin-staleness.js +52 -22
  23. package/dist/commands/improve/autonomy-gate.js +5 -3
  24. package/dist/commands/improve/consolidate/chunking.js +3 -10
  25. package/dist/commands/improve/consolidate/continuity-check.js +4 -4
  26. package/dist/commands/improve/consolidate/pair-pass.js +29 -10
  27. package/dist/commands/improve/consolidate.js +10 -65
  28. package/dist/commands/improve/distill/lesson-update.js +91 -0
  29. package/dist/commands/improve/distill-guards.js +1 -43
  30. package/dist/commands/improve/distill.js +148 -21
  31. package/dist/commands/improve/eligibility.js +14 -1
  32. package/dist/commands/improve/extract-cli.js +1 -1
  33. package/dist/commands/improve/extract-prompt.js +17 -7
  34. package/dist/commands/improve/extract.js +5 -7
  35. package/dist/commands/improve/improve-cli.js +92 -106
  36. package/dist/commands/improve/improve-strategies.js +8 -12
  37. package/dist/commands/improve/improve.js +134 -52
  38. package/dist/commands/improve/loop-stages.js +18 -5
  39. package/dist/commands/improve/planner.js +0 -1
  40. package/dist/commands/improve/preparation.js +40 -99
  41. package/dist/commands/improve/proactive-maintenance.js +2 -2
  42. package/dist/commands/improve/reflect-noise.js +3 -55
  43. package/dist/commands/improve/reflect.js +29 -34
  44. package/dist/commands/improve/retrieval-queries.js +37 -0
  45. package/dist/commands/improve/retrieval-scope.js +2 -2
  46. package/dist/commands/improve/salience.js +2 -9
  47. package/dist/commands/improve/session-asset.js +4 -1
  48. package/dist/commands/improve/stage.js +23 -12
  49. package/dist/commands/lint/index.js +5 -20
  50. package/dist/commands/observability-cli.js +3 -9
  51. package/dist/commands/proposal/drain.js +43 -6
  52. package/dist/commands/proposal/proposal-cli.js +11 -31
  53. package/dist/commands/proposal/repository.js +20 -14
  54. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -3
  55. package/dist/commands/read/knowledge.js +3 -3
  56. package/dist/commands/read/remember-cli.js +0 -13
  57. package/dist/commands/read/search-cli.js +29 -97
  58. package/dist/commands/read/search.js +0 -11
  59. package/dist/commands/read/show.js +0 -19
  60. package/dist/commands/sources/plugin-upgrade.js +164 -101
  61. package/dist/commands/sources/sources-cli.js +1 -15
  62. package/dist/commands/sources/stash-cli.js +6 -14
  63. package/dist/commands/tasks/tasks-cli.js +4 -115
  64. package/dist/commands/workflow/plan.js +11 -33
  65. package/dist/commands/workflow-cli.js +2 -7
  66. package/dist/core/adapter/adapter-ids.js +1 -2
  67. package/dist/core/adapter/adapters/agent-skills-adapter.js +0 -4
  68. package/dist/core/adapter/adapters/akm-adapter.js +9 -48
  69. package/dist/core/adapter/adapters/akm-lint.js +10 -26
  70. package/dist/core/adapter/adapters/akm-metadata.js +2 -72
  71. package/dist/core/adapter/adapters/akm-task-adapter.js +0 -5
  72. package/dist/core/adapter/adapters/akm-workflow-adapter.js +9 -28
  73. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -20
  74. package/dist/core/adapter/adapters/generic-files-adapter.js +0 -10
  75. package/dist/core/adapter/adapters/llm-wiki-adapter.js +0 -4
  76. package/dist/core/adapter/adapters/tool-dir-shared.js +0 -24
  77. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -4
  78. package/dist/core/adapter/recognize-match.js +3 -4
  79. package/dist/core/common.js +18 -2
  80. package/dist/core/config/config-schema.js +12 -15
  81. package/dist/core/config/config-types.js +6 -15
  82. package/dist/core/config/config-walker.js +6 -23
  83. package/dist/core/config/config.js +17 -80
  84. package/dist/core/config/engine-semantics.js +7 -3
  85. package/dist/core/config/schema/engines.js +7 -7
  86. package/dist/core/config/schema/improve-processes.js +10 -63
  87. package/dist/core/config/schema/improve.js +2 -21
  88. package/dist/core/config/schema/index-config.js +6 -39
  89. package/dist/core/env-secret-ref.js +2 -45
  90. package/dist/core/errors.js +1 -2
  91. package/dist/core/extra-params.js +2 -86
  92. package/dist/core/file-lock.js +2 -6
  93. package/dist/core/improve-result.js +13 -1
  94. package/dist/core/mutation-target.js +2 -2
  95. package/dist/core/paths.js +4 -0
  96. package/dist/core/recognition-util.js +4 -5
  97. package/dist/core/run-lock.js +1 -1
  98. package/dist/core/subprocess.js +49 -0
  99. package/dist/core/warn.js +21 -4
  100. package/dist/core/write-source.js +3 -3
  101. package/dist/execution/target-ref.js +1 -2
  102. package/dist/indexer/indexer.js +15 -6
  103. package/dist/indexer/passes/memory-inference.js +15 -6
  104. package/dist/indexer/search/db-search.js +49 -36
  105. package/dist/indexer/search/fts-query.js +0 -19
  106. package/dist/indexer/search/ranking.js +29 -1
  107. package/dist/indexer/usage/usage-events.js +21 -16
  108. package/dist/integrations/agent/engine-resolution.js +5 -2
  109. package/dist/integrations/agent/index.js +1 -1
  110. package/dist/integrations/agent/profiles.js +41 -98
  111. package/dist/integrations/agent/request-lowering.js +3 -3
  112. package/dist/integrations/harnesses/aider/index.js +5 -0
  113. package/dist/integrations/harnesses/amazonq/index.js +1 -0
  114. package/dist/integrations/harnesses/claude/config-import.js +2 -3
  115. package/dist/integrations/harnesses/claude/index.js +5 -0
  116. package/dist/integrations/harnesses/codex/index.js +1 -0
  117. package/dist/integrations/harnesses/copilot/index.js +1 -0
  118. package/dist/integrations/harnesses/gemini/index.js +1 -0
  119. package/dist/integrations/harnesses/index.js +18 -39
  120. package/dist/integrations/harnesses/opencode/agent-builder.js +60 -35
  121. package/dist/integrations/harnesses/opencode/config-import.js +121 -52
  122. package/dist/integrations/harnesses/opencode/index.js +22 -0
  123. package/dist/integrations/harnesses/opencode/plugin-layout.js +68 -0
  124. package/dist/integrations/harnesses/opencode/session-log-types.js +12 -0
  125. package/dist/integrations/harnesses/opencode/session-log-v1.js +72 -0
  126. package/dist/integrations/harnesses/opencode/session-log-v2.js +140 -0
  127. package/dist/integrations/harnesses/opencode/session-log.js +104 -115
  128. package/dist/integrations/harnesses/opencode/version.js +98 -0
  129. package/dist/integrations/harnesses/opencode-sdk/{harness.js → index.js} +5 -17
  130. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +105 -206
  131. package/dist/integrations/harnesses/opencode-sdk/v1-adapter.js +199 -0
  132. package/dist/integrations/harnesses/opencode-sdk/v2-adapter.js +254 -0
  133. package/dist/integrations/harnesses/opencode-sdk/wire.js +66 -0
  134. package/dist/integrations/harnesses/openhands/index.js +1 -0
  135. package/dist/integrations/harnesses/pi/index.js +1 -0
  136. package/dist/integrations/harnesses/shared.js +2 -0
  137. package/dist/integrations/harnesses/types.js +2 -0
  138. package/dist/llm/client.js +41 -11
  139. package/dist/output/cli-hints.js +1 -1
  140. package/dist/output/context.js +10 -19
  141. package/dist/output/format-exempt.js +5 -8
  142. package/dist/output/shapes/helpers.js +1 -22
  143. package/dist/output/shapes/passthrough.js +0 -1
  144. package/dist/output/shapes.js +3 -31
  145. package/dist/output/stdout.js +5 -1
  146. package/dist/output/text/command-format.js +6 -1
  147. package/dist/output/text/workflow-format.js +4 -22
  148. package/dist/scripts/akm-migrate-node.js +15745 -16711
  149. package/dist/scripts/akm-migrate.js +16318 -17280
  150. package/dist/setup/detect.js +10 -4
  151. package/dist/setup/harness-config-import.js +5 -14
  152. package/dist/setup/setup.js +1 -1
  153. package/dist/setup/steps/tasks.js +12 -13
  154. package/dist/sources/providers/git-stash.js +113 -16
  155. package/dist/storage/repositories/index-utility-repository.js +2 -32
  156. package/dist/storage/repositories/index-vec-repository.js +4 -0
  157. package/dist/storage/repositories/task-history-repository.js +0 -5
  158. package/dist/storage/repositories/workflow-runs-repository.js +5 -15
  159. package/dist/tasks/embedded.js +2 -1
  160. package/dist/tasks/run/run-command-task.js +9 -1
  161. package/dist/tasks/run/run-workflow-task.js +4 -3
  162. package/dist/tasks/run/task-history.js +0 -2
  163. package/dist/tasks/run/task-log.js +38 -1
  164. package/dist/tasks/schedule-gates.js +39 -0
  165. package/dist/tasks/scheduler-binding.js +0 -16
  166. package/dist/tasks/scheduler-sync.js +3 -97
  167. package/dist/tasks/source/bounded-document.js +1 -1
  168. package/dist/tasks/source/task-source-v4.js +2 -12
  169. package/dist/tasks/task-run-reserved-flags.js +2 -31
  170. package/dist/workflows/authoring/authoring.js +6 -6
  171. package/dist/workflows/compile.js +8 -65
  172. package/dist/workflows/exec/child-workflow.js +22 -3
  173. package/dist/workflows/exec/native-executor.js +12 -147
  174. package/dist/workflows/exec/run-workflow.js +31 -103
  175. package/dist/workflows/exec/step-work.js +9 -178
  176. package/dist/workflows/exec/unit-dispatch.js +0 -10
  177. package/dist/workflows/freeze/environment.js +3 -44
  178. package/dist/workflows/freeze/freeze.js +8 -41
  179. package/dist/workflows/freeze/targets/child-workflow.js +27 -63
  180. package/dist/workflows/freeze/targets/command.js +2 -16
  181. package/dist/workflows/freeze/targets/shell.js +0 -2
  182. package/dist/workflows/freeze/task-bindings.js +2 -28
  183. package/dist/workflows/ir/params.js +12 -2
  184. package/dist/workflows/parser.js +82 -232
  185. package/dist/workflows/plan.js +1 -1
  186. package/dist/workflows/renderer.js +10 -19
  187. package/dist/workflows/runtime/run-plan.js +11 -53
  188. package/dist/workflows/runtime/runs.js +4 -27
  189. package/dist/workflows/source-files.js +12 -16
  190. package/dist/workflows/source-semantics.js +1 -93
  191. package/docs/README.md +1 -1
  192. package/docs/agents/README.md +3 -3
  193. package/docs/agents/agent-install.md +1 -1
  194. package/docs/architecture/0.10-process-rules.md +827 -0
  195. package/docs/architecture/adapters.md +17 -31
  196. package/docs/architecture/architecture.md +4 -5
  197. package/docs/architecture/improvement.md +115 -76
  198. package/docs/architecture/internals/improve-workflow.md +22 -16
  199. package/docs/architecture/internals/indexing.md +1 -1
  200. package/docs/architecture/internals/search.md +1 -2
  201. package/docs/architecture/internals/storage-locations.md +6 -6
  202. package/docs/architecture/persisted-data-compat.md +13 -4
  203. package/docs/architecture/specs/0.9.0-decisions.md +6 -2
  204. package/docs/architecture/specs/improve-collapse-churn-detector-design.md +2 -2
  205. package/docs/architecture/specs/workflow-format-unification.md +1 -6
  206. package/docs/architecture/testing/manual-testing-checklist.md +21 -30
  207. package/docs/architecture/testing/task-workflow-manual-tests.md +106 -203
  208. package/docs/architecture/testing/testing-workflow.md +1 -1
  209. package/docs/architecture/workflow-engine.md +5 -56
  210. package/docs/guides/author-workflows.md +39 -69
  211. package/docs/guides/bundles.md +3 -1
  212. package/docs/guides/capture-knowledge.md +1 -1
  213. package/docs/guides/claude-code-vs-akm-workflows.md +1 -2
  214. package/docs/guides/discover-and-load.md +4 -4
  215. package/docs/guides/getting-started.md +1 -1
  216. package/docs/guides/improve-the-library.md +10 -9
  217. package/docs/guides/recipes/headless-install.md +2 -2
  218. package/docs/guides/run-workflows.md +1 -1
  219. package/docs/guides/scheduling.md +9 -1
  220. package/docs/guides/use-with-any-agent.md +49 -4
  221. package/docs/integration/bundling-akm.md +7 -6
  222. package/docs/maintainers/curate-workmap.md +1 -2
  223. package/docs/maintainers/release-checklist.md +33 -1
  224. package/docs/migration/README.md +2 -0
  225. package/docs/migration/release-notes/0.10.0.md +201 -0
  226. package/docs/migration/release-notes/README.md +1 -0
  227. package/docs/migration/v0.9-to-v0.10.md +507 -0
  228. package/docs/posts/akm-command-tour.md +3 -3
  229. package/docs/product-surface.md +1 -1
  230. package/docs/reference/README.md +1 -0
  231. package/docs/reference/api.md +65 -0
  232. package/docs/reference/asset-types.md +3 -5
  233. package/docs/reference/bundle-types.md +2 -4
  234. package/docs/reference/cli.md +160 -188
  235. package/docs/reference/configuration.md +101 -20
  236. package/docs/reference/data-and-telemetry.md +2 -2
  237. package/docs/reference/env-and-secrets.md +2 -2
  238. package/docs/reference/supported-formats.md +3 -4
  239. package/docs/reference/tasks.md +12 -19
  240. package/docs/reference/website-sources.md +1 -1
  241. package/docs/reference/workflow-schema.md +89 -317
  242. package/docs/reference/workflows.md +20 -48
  243. package/package.json +12 -2
  244. package/schemas/akm-config.json +2 -243
  245. package/schemas/akm-task.json +2 -3
  246. package/schemas/akm-workflow.json +47 -84
  247. package/dist/assets/improve-strategies/catchup.json +0 -41
  248. package/dist/assets/improve-strategies/proactive-maintenance.json +0 -24
  249. package/dist/assets/improve-strategies/quick.json +0 -17
  250. package/dist/assets/improve-strategies/reflect-distill.json +0 -29
  251. package/dist/assets/improve-strategies/thorough.json +0 -49
  252. package/dist/assets/prompts/retrieval-relevance-judge.md +0 -6
  253. package/dist/assets/tasks/improve/akm-improve-catchup.yml +0 -6
  254. package/dist/assets/tasks/improve/akm-improve-frequent.yml +0 -4
  255. package/dist/assets/tasks/improve/akm-improve-nightly.yml +0 -4
  256. package/dist/cli/retired-commands.js +0 -118
  257. package/dist/commands/improve/retrieval-gate.js +0 -132
  258. package/dist/commands/tasks/validate.js +0 -176
  259. package/dist/core/config/legacy-source-shape-shim.js +0 -117
  260. package/dist/indexer/feedback/utility-policy.js +0 -75
  261. package/dist/integrations/harnesses/ids.js +0 -28
  262. package/dist/tasks/source/task-source-v3-frozen.js +0 -424
  263. package/dist/tasks/source/task-to-v4.js +0 -863
  264. package/dist/workflows/exec/worktree.js +0 -372
  265. package/dist/workflows/freeze/child-output-references.js +0 -83
  266. package/dist/workflows/freeze/identity.js +0 -22
  267. package/dist/workflows/freeze/targets/script.js +0 -50
  268. package/dist/workflows/freeze/targets/task.js +0 -124
  269. package/dist/workflows/github-yaml.js +0 -506
  270. package/dist/workflows/runtime/run-outputs.js +0 -80
@@ -6,75 +6,144 @@
6
6
  * #564).
7
7
  *
8
8
  * Detects an OpenCode installation (filesystem only, no network) and reads its
9
- * config to extract LLM connection details. API key VALUES are never stored —
10
- * only the env var name that holds them. The pluggable registry
11
- * (`HARNESS_CONFIG_IMPORTERS`) and the Claude importer stay in their respective
12
- * modules; `setup/harness-config-import.ts` imports this importer back.
9
+ * config to extract the connection details of the user's SELECTED model. It is
10
+ * a pure reader, so one implementation serves OpenCode 1 and 2 configs and
11
+ * writes nothing into either. API key VALUES are never stored: only the name
12
+ * of the environment variable a `{env:NAME}` / `$NAME` reference points at.
13
+ * A literal key or a `{file:path}` reference yields no credential (a literal
14
+ * must not be copied, and akm's setup has no file-reference slot to carry it).
13
15
  *
14
- * Behaviour-preserving relocation.
16
+ * Config shapes (checked against OpenCode 1.18.34 and 2.0.26):
17
+ * - files: `opencode.json` / `opencode.jsonc` in the global config dir and
18
+ * the project (cwd, `.opencode/`); V1 also reads a legacy `config.json`.
19
+ * - `model`: `"provider/model"` string (V1, and accepted by V2) or
20
+ * `{providerID, model}` (V2).
21
+ * - V1 providers: `provider.<id>.options.{baseURL,apiKey}`, `models.<k>.id`.
22
+ * - V2 providers: `providers.<id>.settings.{baseURL,apiKey}`, `env: [NAME]`,
23
+ * `models.<k>.modelID`. V2 also accepts the V1 `provider` key, so both are
24
+ * read per file and merged by provider id.
25
+ * The provider is the one `model` names. When no model is selected, or it
26
+ * names a provider the config does not define, nothing else is substituted.
15
27
  */
16
28
  import fs from "node:fs";
17
29
  import path from "node:path";
30
+ import { asNonEmptyString, asRecord, parseJsonc, toErrorMessage } from "../../../core/common.js";
31
+ import { warnOnce } from "../../../core/warn.js";
18
32
  import { homeDir } from "../shared.js";
33
+ const asObject = asRecord;
34
+ const str = asNonEmptyString;
35
+ function configDir() {
36
+ const xdg = process.env.XDG_CONFIG_HOME;
37
+ return path.join(xdg && xdg.length > 0 ? xdg : path.join(homeDir(), ".config"), "opencode");
38
+ }
39
+ /** Config files in increasing precedence: global, explicit env overrides, then the project. */
40
+ function candidateFiles() {
41
+ const dirs = [configDir(), process.env.OPENCODE_CONFIG_DIR].filter((d) => !!d);
42
+ const files = [path.join(configDir(), "config.json"), path.join(homeDir(), ".opencode", "config.json")];
43
+ for (const dir of dirs)
44
+ files.push(path.join(dir, "opencode.json"), path.join(dir, "opencode.jsonc"));
45
+ if (process.env.OPENCODE_CONFIG)
46
+ files.push(process.env.OPENCODE_CONFIG);
47
+ const cwd = process.cwd();
48
+ files.push(path.join(cwd, "opencode.json"), path.join(cwd, "opencode.jsonc"), path.join(cwd, ".opencode", "config.json"), path.join(cwd, ".opencode", "opencode.json"), path.join(cwd, ".opencode", "opencode.jsonc"));
49
+ return [...new Set(files)];
50
+ }
51
+ /** `provider/model` or `{providerID, model}` -> the selection, or undefined when none is made. */
52
+ function selectedModel(model) {
53
+ const obj = asObject(model);
54
+ if (obj) {
55
+ const providerId = str(obj.providerID);
56
+ const modelKey = str(obj.model);
57
+ return providerId && modelKey ? { providerId, modelKey } : undefined;
58
+ }
59
+ const text = str(model);
60
+ const slash = text?.indexOf("/") ?? -1;
61
+ if (!text || slash < 1 || slash === text.length - 1)
62
+ return undefined;
63
+ return { providerId: text.slice(0, slash), modelKey: text.slice(slash + 1) };
64
+ }
19
65
  /**
20
- * Imports LLM config from an OpenCode installation.
21
- *
22
- * OpenCode stores config in `~/.config/opencode/config.json` or
23
- * `~/.opencode/config.json`. Its schema has a `providers` array and a
24
- * `model` field. API keys in providers appear as `$ENV_VAR_NAME` references.
66
+ * Add one file's providers, from both the V2 (`providers`/`settings`) and V1
67
+ * (`provider`/`options`) keys, into `out`: a later file's values win per field.
25
68
  */
69
+ function readProviders(raw, out) {
70
+ for (const [key, settingsKey] of [
71
+ ["provider", "options"],
72
+ ["providers", "settings"],
73
+ ]) {
74
+ const map = asObject(raw[key]);
75
+ if (!map)
76
+ continue;
77
+ for (const [id, value] of Object.entries(map)) {
78
+ const p = asObject(value);
79
+ if (!p)
80
+ continue;
81
+ const settings = asObject(p[settingsKey]);
82
+ const entry = out.get(id) ?? { models: new Map() };
83
+ entry.baseUrl = str(settings?.baseURL) ?? str(settings?.baseUrl) ?? entry.baseUrl;
84
+ entry.apiKey = str(settings?.apiKey) ?? entry.apiKey;
85
+ if (Array.isArray(p.env))
86
+ entry.env = p.env.filter((e) => typeof e === "string");
87
+ for (const [modelKey, m] of Object.entries(asObject(p.models) ?? {})) {
88
+ const mo = asObject(m);
89
+ entry.models.set(modelKey, str(mo?.modelID) ?? str(mo?.id) ?? entry.models.get(modelKey));
90
+ }
91
+ out.set(id, entry);
92
+ }
93
+ }
94
+ }
95
+ /** The env var an apiKey reference names: `{env:NAME}`, `$NAME` or `${NAME}`. Literals and `{file:}` give undefined. */
96
+ function envVarOf(apiKey) {
97
+ if (!apiKey)
98
+ return undefined;
99
+ const m = /^(?:\{env:([A-Za-z_][A-Za-z0-9_]*)\}|\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*))$/.exec(apiKey);
100
+ return m?.[1] ?? m?.[2] ?? m?.[3];
101
+ }
102
+ /** Imports the selected model's LLM connection from an OpenCode 1 or 2 config. */
26
103
  export const openCodeImporter = {
27
104
  harnessName: "OpenCode",
28
105
  detect() {
29
- const home = homeDir();
30
- return fs.existsSync(path.join(home, ".config", "opencode")) || fs.existsSync(path.join(home, ".opencode"));
106
+ return fs.existsSync(configDir()) || fs.existsSync(path.join(homeDir(), ".opencode"));
31
107
  },
32
108
  importConfig() {
33
- const home = homeDir();
34
- const candidates = [
35
- path.join(home, ".config", "opencode", "config.json"),
36
- path.join(home, ".opencode", "config.json"),
37
- path.join(process.cwd(), ".opencode", "config.json"),
38
- ];
39
- for (const filePath of candidates) {
109
+ let model;
110
+ let read = false;
111
+ const providers = new Map();
112
+ for (const filePath of candidateFiles()) {
113
+ let raw;
40
114
  try {
41
115
  if (!fs.existsSync(filePath))
42
116
  continue;
43
- const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
44
- // OpenCode config shape: { model?: string, providers?: Array<{id, apiKey, baseUrl, ...}> }
45
- const model = typeof raw.model === "string" ? raw.model : undefined;
46
- // Extract provider info from the first entry in the providers array
47
- let provider;
48
- let baseUrl;
49
- let apiKeyEnvVar;
50
- const providers = Array.isArray(raw.providers) ? raw.providers : [];
51
- if (providers.length > 0) {
52
- const first = providers[0];
53
- provider = typeof first?.id === "string" ? first.id : undefined;
54
- baseUrl =
55
- typeof first?.baseUrl === "string"
56
- ? first.baseUrl
57
- : typeof first?.base_url === "string"
58
- ? first.base_url
59
- : undefined;
60
- // apiKey is an env var reference like "$OPENAI_API_KEY" — extract the var name
61
- const apiKeyVal = typeof first?.apiKey === "string" ? first.apiKey : "";
62
- if (apiKeyVal.startsWith("$")) {
63
- apiKeyEnvVar = apiKeyVal.slice(1);
64
- }
65
- }
66
- return {
67
- harnessName: "OpenCode",
68
- provider,
69
- model,
70
- baseUrl,
71
- apiKeyEnvVar,
72
- };
117
+ raw = asObject(parseJsonc(fs.readFileSync(filePath, "utf8")));
73
118
  }
74
- catch {
75
- // try next candidate
119
+ catch (error) {
120
+ warnOnce(`opencode-config-import:${filePath}`, `OpenCode config ${filePath} could not be read (${toErrorMessage(error)}); skipped.`);
121
+ continue;
76
122
  }
123
+ if (!raw)
124
+ continue;
125
+ read = true;
126
+ if (raw.model !== undefined)
127
+ model = raw.model;
128
+ readProviders(raw, providers);
129
+ }
130
+ if (!read)
131
+ return null;
132
+ const result = { harnessName: "OpenCode" };
133
+ const selection = selectedModel(model);
134
+ if (!selection)
135
+ return result;
136
+ const entry = providers.get(selection.providerId);
137
+ result.provider = selection.providerId;
138
+ result.model = entry?.models.get(selection.modelKey) ?? selection.modelKey;
139
+ if (entry?.baseUrl)
140
+ result.baseUrl = entry.baseUrl;
141
+ const envVar = envVarOf(entry?.apiKey) ?? (!entry?.apiKey && entry?.env?.length === 1 ? entry.env[0] : undefined);
142
+ if (envVar)
143
+ result.apiKeyEnvVar = envVar;
144
+ else if (entry?.apiKey) {
145
+ warnOnce(`opencode-config-import:credential:${selection.providerId}`, `OpenCode provider "${selection.providerId}" has an API key that is not an environment-variable reference; it was not imported. Export the key as an environment variable and reference it there.`);
77
146
  }
78
- return null;
147
+ return result;
79
148
  },
80
149
  };
@@ -1,9 +1,27 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * OpenCode harness (#564).
6
+ *
7
+ * Per-harness barrel that gathers the OpenCode integration surfaces previously
8
+ * scattered across the codebase:
9
+ * - session-log reader → ./session-log.ts (OpenCodeProvider)
10
+ * - agent command builder → ./agent-builder.ts (opencodeBuilder)
11
+ * - config importer → ./config-import.ts (openCodeImporter)
12
+ *
13
+ * It also defines {@link OpencodeHarness}, the {@link AkmHarness} descriptor
14
+ * that `HARNESS_REGISTRY` registers.
15
+ *
16
+ * id normalization: OpenCode's canonical id (`'opencode'`) is also its runtime
17
+ * identity and session-log provider name — there is no historical split (unlike
18
+ * Claude Code's 'claude' vs 'claude'), so no alias bridge is needed.
19
+ */
20
+ import { OPENCODE_ENV_PASSTHROUGH } from "../../../core/spawn-env.js";
4
21
  import { caps } from "../shared.js";
5
22
  import { BaseHarness } from "../types.js";
6
23
  import { opencodeBuilder } from "./agent-builder.js";
24
+ import { openCodeImporter } from "./config-import.js";
7
25
  import { OpenCodeProvider } from "./session-log.js";
8
26
  export { opencodeBuilder } from "./agent-builder.js";
9
27
  export { openCodeImporter } from "./config-import.js";
@@ -20,6 +38,8 @@ export class OpencodeHarness extends BaseHarness {
20
38
  // session-log provider, so offering it as a stash source is functional.
21
39
  setupDetectionDir = ".config/opencode";
22
40
  agentBuilder = opencodeBuilder;
41
+ configImporter = openCodeImporter;
42
+ profile = { bin: "opencode", args: ["run"], envPassthrough: OPENCODE_ENV_PASSTHROUGH };
23
43
  // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
24
44
  // Session-id env marker for run attribution.
25
45
  identityEnv = ["OPENCODE_SESSION_ID"];
@@ -28,6 +48,8 @@ export class OpencodeHarness extends BaseHarness {
28
48
  sessionLogs: true,
29
49
  agentDispatch: true,
30
50
  detection: true,
51
+ modelWork: true,
52
+ nativeAgent: true,
31
53
  configImport: true,
32
54
  });
33
55
  }
@@ -0,0 +1,68 @@
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
+ /**
5
+ * Where each OpenCode major installs the akm plugin (#1049). `akm upgrade`
6
+ * (`commands/sources/plugin-upgrade.ts`) refreshes these caches and `akm health`
7
+ * (`commands/health/plugin-staleness.ts`) reads the akm-cli bundled in them, so
8
+ * the layout is written down once, here.
9
+ *
10
+ * OpenCode 1 and OpenCode 2 share `~/.config/opencode` and `~/.cache/opencode`
11
+ * but lay both out differently, and each major only loads its own package, so a
12
+ * host is handled per flavor and a flavor never touches the other's package,
13
+ * cache or config entry.
14
+ */
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { tryParseJson } from "../../../core/common.js";
18
+ /** The newest numbered build folder (`<epoch-ms>`) under an OpenCode 2 cache folder. */
19
+ export function newestBuildDir(folder) {
20
+ try {
21
+ const builds = fs.readdirSync(folder).filter((name) => /^\d+$/.test(name));
22
+ builds.sort((a, b) => Number(b) - Number(a));
23
+ return builds[0] ? path.join(folder, builds[0]) : undefined;
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ /** A package.json's `version`, or undefined when the file is missing, unreadable or has none. */
30
+ export function readPackageVersion(file) {
31
+ let text;
32
+ try {
33
+ text = fs.readFileSync(file, "utf8");
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ const pkg = tryParseJson(text);
39
+ return typeof pkg?.version === "string" ? pkg.version : undefined;
40
+ }
41
+ export const OPENCODE_V1 = {
42
+ harness: "opencode",
43
+ pkg: "akm-opencode",
44
+ major: 1,
45
+ configKeys: ["plugin"],
46
+ globalConfigOnly: false,
47
+ // OpenCode 1: <cache>/opencode/packages/<spec>/node_modules/<pkg>
48
+ cacheFolder: (spec) => path.join("packages", spec),
49
+ nodeModulesDir: (folder) => path.join(folder, "node_modules"),
50
+ // Any OpenCode command that resolves the config installs the plugin again.
51
+ prefetch: () => ["debug", "config"],
52
+ };
53
+ export const OPENCODE_V2 = {
54
+ harness: "opencode-v2",
55
+ pkg: "akm-opencode-v2",
56
+ major: 2,
57
+ // OpenCode 2.0.26 reads both keys; its own `plugin add` writes `plugins`.
58
+ configKeys: ["plugins", "plugin"],
59
+ globalConfigOnly: true,
60
+ // OpenCode 2: <cache>/opencode/npm/<spec>/<epoch-ms>/node_modules/<pkg>
61
+ cacheFolder: (spec) => path.join("npm", spec),
62
+ nodeModulesDir: (folder) => {
63
+ const build = newestBuildDir(folder);
64
+ return build ? path.join(build, "node_modules") : undefined;
65
+ },
66
+ // `debug config` does not resolve plugins in OpenCode 2; `plugin add` of a configured spec only reinstalls it.
67
+ prefetch: (spec) => ["plugin", "add", spec],
68
+ };
@@ -0,0 +1,12 @@
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
+ export function toMeta(r, sessionId) {
5
+ return {
6
+ sessionId,
7
+ startedAt: typeof r.time_created === "number" ? r.time_created : undefined,
8
+ endedAt: typeof r.time_updated === "number" ? r.time_updated : undefined,
9
+ projectHint: typeof r.directory === "string" && r.directory.length > 0 ? r.directory : undefined,
10
+ title: typeof r.title === "string" && r.title.length > 0 ? r.title : undefined,
11
+ };
12
+ }
@@ -0,0 +1,72 @@
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 { extractInlineRefMentions } from "../../session-logs/inline-refs.js";
5
+ import { toMeta } from "./session-log-types.js";
6
+ /** Listing needs only `session`; reading a session needs `message` and `part`, and fails loudly without them. */
7
+ export const V1_TABLES = ["session"];
8
+ /** Every V1 session updated at or after `sinceMs`, newest first. Throws on an incompatible schema. */
9
+ export function listV1Sessions(db, sinceMs) {
10
+ const rows = db
11
+ .prepare("SELECT id, title, directory, time_created, time_updated FROM session WHERE time_updated >= ? ORDER BY time_updated DESC")
12
+ .all(sinceMs);
13
+ return rows.map((r) => toMeta(r, r.id));
14
+ }
15
+ /** Whether a V1 `session` row exists for `sessionId`. */
16
+ export function hasV1Session(db, sessionId) {
17
+ return db.prepare("SELECT id FROM session WHERE id = ?").get(sessionId) !== undefined;
18
+ }
19
+ /**
20
+ * One event per message, text-parts concatenated in time order. Throws on an
21
+ * incompatible schema; individual unparsable rows are skipped.
22
+ */
23
+ export function readV1Session(db, harness, sessionId, filePath) {
24
+ const meta = db
25
+ .prepare("SELECT title, directory, time_created, time_updated FROM session WHERE id = ?")
26
+ .get(sessionId);
27
+ const messages = db
28
+ .prepare("SELECT id, data, time_created FROM message WHERE session_id = ? ORDER BY time_created ASC, id ASC")
29
+ .all(sessionId);
30
+ const parts = db
31
+ .prepare("SELECT message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC, id ASC")
32
+ .all(sessionId);
33
+ const textByMessage = new Map();
34
+ for (const part of parts) {
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(part.data);
38
+ }
39
+ catch {
40
+ continue;
41
+ }
42
+ if (parsed?.type !== "text")
43
+ continue;
44
+ const text = parsed.text;
45
+ if (typeof text !== "string" || text.length < 1)
46
+ continue;
47
+ const bucket = textByMessage.get(part.message_id) ?? [];
48
+ bucket.push(text);
49
+ textByMessage.set(part.message_id, bucket);
50
+ }
51
+ const events = [];
52
+ const inlineRefs = [];
53
+ for (const message of messages) {
54
+ let mdata = {};
55
+ try {
56
+ mdata = JSON.parse(message.data);
57
+ }
58
+ catch {
59
+ // role/timing unavailable — fall through with defaults
60
+ }
61
+ const role = typeof mdata.role === "string" ? mdata.role : "unknown";
62
+ const mtime = mdata.time?.created;
63
+ const ts = typeof mtime === "number" ? mtime : typeof message.time_created === "number" ? message.time_created : undefined;
64
+ const text = (textByMessage.get(message.id) ?? []).join("\n").trim();
65
+ if (text.length < 1)
66
+ continue;
67
+ events.push({ harness, text, ts, sessionId, role, filePath });
68
+ inlineRefs.push(...extractInlineRefMentions(text, ts));
69
+ }
70
+ events.sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
71
+ return { meta: meta ? toMeta(meta, sessionId) : { sessionId }, events, inlineRefs };
72
+ }
@@ -0,0 +1,140 @@
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
+ /**
5
+ * OpenCode 2 session-store adapter. V2 keeps one `session_v2` row per session
6
+ * and the ordered conversation as `session_message` rows (`type` + JSON `data`
7
+ * minus `id`/`type`, shapes from `@opencode/client` `SessionMessageInfo`).
8
+ * Read-only: the database is never migrated; V1 rows a native upgrade copied
9
+ * here arrive as the same shapes under the same session id.
10
+ *
11
+ * Mapping into the session-log contract follows the Codex reader: user and
12
+ * assistant text become events of that role, a tool call becomes
13
+ * `[tool:<name>] <input>` and its outcome a `tool` event
14
+ * `[tool_result] <output>`, so the inline-ref scanner sees shell commands.
15
+ * Reasoning, compaction, synthetic/system and bookkeeping messages are skipped.
16
+ */
17
+ import { asRecord, tryParseJson } from "../../../core/common.js";
18
+ import { warnOnce } from "../../../core/warn.js";
19
+ import { extractInlineRefMentions } from "../../session-logs/inline-refs.js";
20
+ import { toMeta } from "./session-log-types.js";
21
+ export const V2_TABLES = ["session_v2", "session_message"];
22
+ /** Every V2 session updated at or after `sinceMs`, newest first. Throws on an incompatible schema. */
23
+ export function listV2Sessions(db, sinceMs) {
24
+ const rows = db
25
+ .prepare("SELECT id, title, directory, time_created, time_updated FROM session_v2 WHERE time_updated >= ? ORDER BY time_updated DESC")
26
+ .all(sinceMs);
27
+ return rows.map((r) => toMeta(r, r.id));
28
+ }
29
+ /** Whether `sessionId` has a `session_v2` row, and how many `session_message` rows it owns. */
30
+ export function v2SessionState(db, sessionId) {
31
+ const row = db
32
+ .prepare("SELECT (SELECT COUNT(*) FROM session_message WHERE session_id = s.id) AS n FROM session_v2 s WHERE s.id = ?")
33
+ .get(sessionId);
34
+ return row ? { exists: true, messages: row.n } : { exists: false, messages: 0 };
35
+ }
36
+ const asObject = asRecord;
37
+ function asNumber(v) {
38
+ return typeof v === "number" ? v : undefined;
39
+ }
40
+ /** A tool input as text: a shell command stays a bare command line so inline-ref scanning matches it. */
41
+ function toolInputText(input) {
42
+ const fields = asObject(input);
43
+ const cmd = fields?.command ?? fields?.cmd;
44
+ if (typeof cmd === "string")
45
+ return cmd;
46
+ if (typeof input === "string")
47
+ return input;
48
+ return JSON.stringify(input ?? {}) ?? "";
49
+ }
50
+ /** Tool output blocks as text: text blocks verbatim, file blocks as a `[file: uri]` marker. */
51
+ function toolContentText(content) {
52
+ if (!Array.isArray(content))
53
+ return "";
54
+ const out = [];
55
+ for (const block of content) {
56
+ const b = asObject(block);
57
+ if (!b)
58
+ continue;
59
+ if (b.type === "text" && typeof b.text === "string")
60
+ out.push(b.text);
61
+ else if (b.type === "file" && typeof b.uri === "string")
62
+ out.push(`[file: ${b.uri}]`);
63
+ }
64
+ return out.join("\n");
65
+ }
66
+ function structuredErrorText(err) {
67
+ const e = asObject(err);
68
+ if (typeof e?.message === "string")
69
+ return e.message;
70
+ return typeof e?.type === "string" ? e.type : "error";
71
+ }
72
+ /**
73
+ * Rebuild one session's events in `seq` order. Throws on an incompatible
74
+ * schema; a message whose JSON cannot be read is skipped with one warning.
75
+ */
76
+ export function readV2Session(db, harness, sessionId, filePath) {
77
+ const meta = db
78
+ .prepare("SELECT title, directory, time_created, time_updated FROM session_v2 WHERE id = ?")
79
+ .get(sessionId);
80
+ const rows = db
81
+ .prepare("SELECT id, type, data, time_created FROM session_message WHERE session_id = ? ORDER BY seq ASC, time_created ASC, id ASC")
82
+ .all(sessionId);
83
+ const events = [];
84
+ const inlineRefs = [];
85
+ const push = (role, text, ts) => {
86
+ const trimmed = text.trim();
87
+ if (trimmed.length < 1)
88
+ return;
89
+ events.push({ harness, text: trimmed, ts, sessionId, role, filePath });
90
+ inlineRefs.push(...extractInlineRefMentions(trimmed, ts));
91
+ };
92
+ for (const row of rows) {
93
+ const data = asObject(tryParseJson(row.data));
94
+ if (!data) {
95
+ warnOnce(`opencode-v2-message:${filePath}:${row.id}`, `OpenCode 2 history: skipped unreadable message ${row.id} in session ${sessionId} (${filePath}).`);
96
+ continue;
97
+ }
98
+ const ts = asNumber(asObject(data.time)?.created) ?? asNumber(row.time_created);
99
+ if (row.type === "user") {
100
+ if (typeof data.text === "string")
101
+ push("user", data.text, ts);
102
+ }
103
+ else if (row.type === "assistant") {
104
+ const content = Array.isArray(data.content) ? data.content : [];
105
+ const text = content
106
+ .map((c) => asObject(c))
107
+ .filter((c) => c?.type === "text" && typeof c.text === "string")
108
+ .map((c) => c.text)
109
+ .join("\n");
110
+ push("assistant", text, ts);
111
+ for (const item of content) {
112
+ const tool = asObject(item);
113
+ if (tool?.type !== "tool")
114
+ continue;
115
+ const state = asObject(tool.state);
116
+ const name = typeof tool.name === "string" ? tool.name : "tool";
117
+ const toolTime = asObject(tool.time);
118
+ const callTs = asNumber(toolTime?.created) ?? ts;
119
+ push("assistant", `[tool:${name}] ${toolInputText(state?.input)}`, callTs);
120
+ // Streaming/running calls are incomplete history: the call is kept, no result is invented.
121
+ const doneTs = asNumber(toolTime?.completed) ?? callTs;
122
+ if (state?.status === "completed") {
123
+ push("tool", `[tool_result] ${toolContentText(state.content)}`, doneTs);
124
+ }
125
+ else if (state?.status === "error") {
126
+ const body = toolContentText(state.content) || structuredErrorText(state.error);
127
+ push("tool", `[tool_result] ${body}`, doneTs);
128
+ }
129
+ }
130
+ }
131
+ else if (row.type === "shell") {
132
+ if (typeof data.command === "string")
133
+ push("assistant", `[tool:shell] ${data.command}`, ts);
134
+ const out = asObject(data.output)?.output;
135
+ if (typeof out === "string")
136
+ push("tool", `[tool_result] ${out}`, ts);
137
+ }
138
+ }
139
+ return { meta: meta ? toMeta(meta, sessionId) : { sessionId }, events, inlineRefs };
140
+ }