pantheon-opencode 1.6.0-beta.3 → 1.6.0-beta.4

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 (36) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +19 -7
  3. package/README.pt-BR.md +17 -7
  4. package/package.json +12 -10
  5. package/plugin.json +1 -1
  6. package/pyproject.toml +1 -1
  7. package/scripts/code_mode_server.py +1 -1
  8. package/scripts/cost.mjs +244 -41
  9. package/scripts/install/agents-md.mjs +2 -2
  10. package/scripts/install/health-check.mjs +9 -2
  11. package/scripts/install/shared.mjs +5 -5
  12. package/scripts/mcp_persistence_server.py +1 -1
  13. package/scripts/mcp_resources_server.py +1 -1
  14. package/scripts/memory_mcp_server.py +1 -1
  15. package/scripts/probe-context-rehydrate.mjs +6 -0
  16. package/scripts/probe-pantheon-cost.mjs +71 -15
  17. package/scripts/validate-routing.mjs +3 -3
  18. package/src/mcp/code_mode_server.py +1 -1
  19. package/src/mcp/mcp_persistence_server.py +1 -1
  20. package/src/mcp/mcp_resources_server.py +1 -1
  21. package/src/mcp/memory_mcp_server.py +1 -1
  22. package/src/mcp/pantheon_vision_server.py +1 -1
  23. package/src/mcp/requirements-mcp.txt +6 -4
  24. package/src/mcp/requirements-vision.txt +2 -2
  25. package/src/pantheon/cost-command.ts +399 -101
  26. package/src/pantheon/presets.mjs +3 -3
  27. package/src/plugin-v2.ts +131 -58
  28. package/src/plugins/tui/dist/server.d.ts +1 -2
  29. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  30. package/src/plugins/tui/dist/tui.d.ts +74 -74
  31. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  32. package/src/plugins/tui/dist/tui.js +17 -20
  33. package/src/plugins/tui/package-lock.json +364 -317
  34. package/src/plugins/tui/package.json +8 -5
  35. package/src/plugins/tui/tsconfig.build.json +5 -0
  36. package/src/plugins/tui/tsdown.config.ts +3 -0
package/src/plugin-v2.ts CHANGED
@@ -26,21 +26,44 @@
26
26
  * `execute` MUST resolve to a value carrying `output`. The host enforces the
27
27
  * two as a pair — see `V2ToolDraft` for the exact contract.
28
28
  *
29
- * The transitional SDK types used here expose these domains:
30
- * ctx.agent / ctx.command / ctx.model / ctx.reference / ctx.skill /
31
- * ctx.tool / ctx.event / ctx.permission / ctx.session
29
+ * ## The four domains the SDK types do not carry
32
30
  *
33
- * A separate OpenCode 2.0.18 host runtime probe confirmed ctx.integration and
34
- * ctx.skill have callable `.transform` methods; only ctx.catalog was absent.
35
- * The probe's callback effects were
36
- * not observed for any domain. This runtime probe is not an SDK compatibility
37
- * claim. Pantheon's Phase 1/config transform
38
- * registrations cover agent, command, and reference; `ctx.tool.transform` is
39
- * attempted separately in Phase 2. Integration and skill transform callbacks
40
- * are not registered by Pantheon, so their effects remain unproven. The
41
- * narrower `SkillEditor.source()` helper is absent from the inspected SDK
42
- * editor shape, so directory-source registration remains unsupported; that
43
- * does not imply that `ctx.skill` or its transform is absent.
31
+ * `@opencode-ai/plugin` 1.18.33 types `PluginContext` as exactly:
32
+ * options · agent · aisdk · catalog · command · integration · plugin ·
33
+ * reference · skill
34
+ * — no `tool`, `event`, `permission` or `session`. 1.18.34 (`latest`) is the
35
+ * same, so following the stable SDK line does not resolve this: it is a
36
+ * STRUCTURAL DECLARATION task, not a pin reconciliation.
37
+ *
38
+ * A live opencode 2.0.22 host DOES have all four, with the shapes this module
39
+ * uses. Measured by `tests/canary/plugin-v2-tool-canary.test.mjs`, which loads
40
+ * this file through the real loader and records `Object.keys(ctx)` from inside
41
+ * the host:
42
+ *
43
+ * agent aisdk app command event experimental generate integration location
44
+ * mcp model options permission plugin provider reference rpc session shell
45
+ * skill storage tool vcs websearch worktree
46
+ *
47
+ * ctx.tool.transform function
48
+ * ctx.tool.hook function
49
+ * ctx.tool.list function
50
+ * ctx.event.subscribe function
51
+ * ctx.permission.hook function
52
+ * ctx.session.hook function
53
+ * ctx.catalog absent
54
+ *
55
+ * `HostPluginContext` below declares exactly the minimum of that, and
56
+ * `hostDomains` is the SINGLE documented cast that reaches them — replacing the
57
+ * six repeated `as unknown as Record<string, unknown>` casts this file carried
58
+ * (Phases 2, 3, 4, 5, 6 and 7 — every phase except Phase 1, which reads the
59
+ * SDK-typed domains directly), each of which type-checked nothing at all. The
60
+ * per-domain definition checks stay: `registerV2Tools` still returns `false`
61
+ * and marks `tool-transform` when `ctx.tool.transform` is missing, and likewise
62
+ * for the others. The cast is not a licence to assume: the canary above is what
63
+ * proves the domains exist on a real host, and it fails if they stop existing.
64
+ *
65
+ * If the SDK ever exposes these four, replacing `hostDomains` with the real
66
+ * type is a one-line change. Do NOT make that change while 1.18.x lacks them.
44
67
  *
45
68
  * V2_UNSUPPORTED_FEATURES distinguishes adapter limitations from observed
46
69
  * host-absent APIs; setup handles optional registrations on a best-effort basis.
@@ -82,15 +105,19 @@ import { type HookPayload, runHook } from './plugins/hook-runner.ts'
82
105
  */
83
106
  export const V2_UNSUPPORTED_FEATURES: string[] = [
84
107
  'legacy-hooks',
85
- // `catalog` alone was absent in the OpenCode 2.0.18 runtime probe.
108
+ // `ctx.catalog` is the one domain a live 2.0.22 host does NOT have: it is
109
+ // absent from `Object.keys(ctx)`, measured in
110
+ // tests/canary/plugin-v2-tool-canary.test.mjs. (`catalog` IS in the 1.18.33
111
+ // SDK's PluginContext, so the SDK types it and the host does not provide it.)
86
112
  'catalog-transform',
87
- // The host exposes callable transforms for integration and skill, but
88
- // Pantheon does not register/use them and no host callback effects were
89
- // observed. These entries describe adapter support, not host availability.
113
+ // Adapter support, NOT host availability: 2.0.22 exposes ctx.integration and
114
+ // ctx.skill with callable transforms, and Pantheon deliberately registers
115
+ // neither. These entries record what this plugin does not implement.
90
116
  'integration-transform',
91
- // The inspected SkillEditor shape has no `source()` helper for adding a
92
- // directory source. This is narrower than (and distinct from) ctx.skill's
93
- // host availability or callable transform.
117
+ // The inspected SkillEditor shape has no `source()` helper — i.e. no
118
+ // `SkillEditor.source()` — for adding a directory source. This is narrower
119
+ // than (and distinct from) ctx.skill's host availability or callable
120
+ // transform, which a live 2.0.22 host does provide.
94
121
  'skill-transform',
95
122
  // Adapter limitation, not a host gap: the goal loop needs a GoalStore, a
96
123
  // GoalLoopClient and a BackgroundJobBoard, none of which the V2
@@ -189,6 +216,68 @@ function markUnsupported(feature: string): void {
189
216
  }
190
217
  }
191
218
 
219
+ // ─── Host context domains the SDK types do not carry ────────────────────
220
+
221
+ /**
222
+ * The minimal shape of `ctx.tool` this module uses, as a live opencode 2.0.22
223
+ * host provides it. `transform` registers the Pantheon tools, `hook` installs
224
+ * the `execute.before`/`execute.after` guards, `list` is the host-built
225
+ * descriptor dump the canary asserts on.
226
+ */
227
+ interface V2ToolDomain {
228
+ transform?: (cb: (draft: V2ToolDraft) => void) => Promise<unknown>
229
+ hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
230
+ list?: () => Promise<unknown>
231
+ }
232
+
233
+ /** The minimal shape of `ctx.event`: one async-iterable subscription. */
234
+ interface V2EventDomain {
235
+ subscribe?: (opts?: { signal?: AbortSignal }) => AsyncIterable<V2SessionEvent>
236
+ }
237
+
238
+ /**
239
+ * The minimal shape of `ctx.permission`.
240
+ *
241
+ * Only a hook name and a handler: the plugin registers `evaluate` as a
242
+ * registration point and deliberately runs no V2-side permission logic, so no
243
+ * richer payload type is claimed here.
244
+ */
245
+ interface V2PermissionDomain {
246
+ hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
247
+ }
248
+
249
+ /**
250
+ * The minimal shape of `ctx.session`.
251
+ *
252
+ * Only `hook(name, handler)`. The host accepts any string and silently drops an
253
+ * unknown one — which is why the hook names below were a live defect once and
254
+ * why the host-backed hook canary exists.
255
+ */
256
+ interface V2SessionDomain {
257
+ hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
258
+ }
259
+
260
+ /** `PluginContext` plus the four domains 1.18.x omits. See the module header. */
261
+ interface HostPluginContext extends PluginContext {
262
+ tool?: V2ToolDomain
263
+ event?: V2EventDomain
264
+ permission?: V2PermissionDomain
265
+ session?: V2SessionDomain
266
+ }
267
+
268
+ /**
269
+ * THE cast to the host's context. Every optional member stays optional and every
270
+ * caller re-checks before use, so this narrows nothing away: it replaces five
271
+ * anonymous `Record<string, unknown>` casts with one named, documented,
272
+ * type-checked boundary.
273
+ *
274
+ * When the SDK ships these four domains, delete this function and use
275
+ * `PluginContext` directly.
276
+ */
277
+ function hostDomains(context: PluginContext): HostPluginContext {
278
+ return context as HostPluginContext
279
+ }
280
+
192
281
  // ─── V1 Bridge Integration ──────────────────────────────────────────────
193
282
 
194
283
  /**
@@ -334,9 +423,7 @@ function transformReferences(draft: ReferenceDraft): void {
334
423
  * Returns true if tools were registered, false if the API is unavailable.
335
424
  */
336
425
  async function registerV2Tools(context: PluginContext): Promise<boolean> {
337
- const toolCtx = (context as unknown as Record<string, unknown>).tool as
338
- | { transform?: (cb: (draft: V2ToolDraft) => void) => Promise<unknown> }
339
- | undefined
426
+ const toolCtx = hostDomains(context).tool
340
427
 
341
428
  if (!toolCtx?.transform) {
342
429
  return false
@@ -387,9 +474,7 @@ async function registerV2Tools(context: PluginContext): Promise<boolean> {
387
474
  * Returns a cleanup function, or undefined if the API is unavailable.
388
475
  */
389
476
  function subscribeV2Events(context: PluginContext): (() => void) | undefined {
390
- const eventCtx = (context as unknown as Record<string, unknown>).event as
391
- | { subscribe?: (opts?: { signal?: AbortSignal }) => AsyncIterable<V2SessionEvent> }
392
- | undefined
477
+ const eventCtx = hostDomains(context).event
393
478
 
394
479
  if (!eventCtx?.subscribe) {
395
480
  return undefined
@@ -505,11 +590,7 @@ async function onSessionCompacted(_event: V2SessionEvent): Promise<void> {}
505
590
  * Returns true if any hooks were registered.
506
591
  */
507
592
  async function registerV2SessionHooks(context: PluginContext): Promise<boolean> {
508
- const sessionCtx = (context as unknown as Record<string, unknown>).session as
509
- | {
510
- hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
511
- }
512
- | undefined
593
+ const sessionCtx = hostDomains(context).session
513
594
 
514
595
  if (!sessionCtx?.hook) {
515
596
  return false
@@ -600,16 +681,21 @@ function maskSecret(text: string): string {
600
681
  /**
601
682
  * Message thrown when scan-secrets.sh exits 2 (a high-confidence token match).
602
683
  *
603
- * This is the only deliberate throw on the V2 surface, and it mirrors the V1
604
- * message (src/plugins/pantheon-hooks.ts `SECRET_BLOCK_MESSAGE`). opencode's
684
+ * This is the only deliberate throw on the V2 surface. opencode's
605
685
  * Plugin.trigger has no catch around hook invocations, so the rejection
606
686
  * propagates and fails the tool's execute promise BEFORE the tool body runs —
607
- * the same hard block V1 had, which was inert on a V2-only install because
687
+ * the same hard block V1 has (src/plugins/pantheon-hooks.ts
688
+ * `SECRET_BLOCK_MESSAGE`), which was inert on a V2-only install because
608
689
  * pantheon-hooks.ts is never loaded there (opencode.json `plugins` declares only
609
690
  * `src/plugin-v2`).
691
+ *
692
+ * English, like every other message this module emits: this string surfaces in
693
+ * the user's session as the tool error, so it is read by whoever hit the block,
694
+ * not by the code that raised it. A Portuguese release note elsewhere in the
695
+ * repo is not a reason for the runtime message to be Portuguese too.
610
696
  */
611
697
  const SECRET_BLOCK_MESSAGE =
612
- '[plugin-v2] Bloqueado: segredo de alta confiança detectado no tool input — consulte .pantheon/logs/hooks.log'
698
+ '[plugin-v2] Blocked: high-confidence secret detected in tool input — see .pantheon/logs/hooks.log'
613
699
 
614
700
  /**
615
701
  * Append one masked, ISO-stamped line to .pantheon/logs/hooks.log under the
@@ -740,11 +826,7 @@ function adaptV2ExecuteBeforeEvent(event: unknown): {
740
826
  * Returns true if any hooks were registered.
741
827
  */
742
828
  async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
743
- const toolCtx = (context as unknown as Record<string, unknown>).tool as
744
- | {
745
- hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
746
- }
747
- | undefined
829
+ const toolCtx = hostDomains(context).tool
748
830
 
749
831
  if (!toolCtx?.hook) {
750
832
  return false
@@ -842,11 +924,7 @@ async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
842
924
  * Returns true if the hook was registered.
843
925
  */
844
926
  async function registerV2PermissionHook(context: PluginContext): Promise<boolean> {
845
- const permCtx = (context as unknown as Record<string, unknown>).permission as
846
- | {
847
- hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
848
- }
849
- | undefined
927
+ const permCtx = hostDomains(context).permission
850
928
 
851
929
  if (!permCtx?.hook) {
852
930
  markUnsupported('permission-hook')
@@ -920,10 +998,12 @@ export const plugin = define({
920
998
  //
921
999
  // Pantheon's Phase 1/config transform registrations cover agent, command,
922
1000
  // and reference; ctx.tool.transform is attempted separately in Phase 2.
923
- // The 2.0.18 runtime probe found ctx.integration and ctx.skill with
924
- // callable transforms, but no callback effects were observed; neither is
925
- // registered here. Only ctx.catalog was absent in that probe.
926
- // The hook canary tests hook firing only, not transform callback effects.
1001
+ // ctx.integration and ctx.skill are present on a live 2.0.22 host and neither
1002
+ // is registered here — that is a Pantheon scope choice, not a host gap.
1003
+ // ctx.catalog is the one domain the host does not provide.
1004
+ // Phase 1 registers against the SDK-typed `context.agent` / `.command` /
1005
+ // `.reference` directly; the four domains the SDK omits are reached only
1006
+ // through `hostDomains` (see the module header).
927
1007
  const phase1Transforms: Array<{ feature: string; register: () => Promise<unknown> }> = [
928
1008
  { feature: 'agent-transform', register: () => context.agent.transform(transformAgents) },
929
1009
  {
@@ -976,14 +1056,7 @@ export const plugin = define({
976
1056
  await registerV2PermissionHook(context).catch(() => false)
977
1057
 
978
1058
  // ─── Phase 7: V2 Compaction Hook (best-effort) ───────────────────
979
- const compactionCtx = (context as unknown as Record<string, unknown>).session as
980
- | {
981
- hook?: (
982
- name: string,
983
- handler: (event: unknown) => void | Promise<void>,
984
- ) => Promise<unknown>
985
- }
986
- | undefined
1059
+ const compactionCtx = hostDomains(context).session
987
1060
  if (compactionCtx?.hook) {
988
1061
  try {
989
1062
  // 2.0.16 `SessionHooks` key is `compaction` (the migration doc maps
@@ -11,7 +11,6 @@
11
11
  * This stub satisfies the loader contract; pantheon-tui is TUI-only and has
12
12
  * no background server work of its own.
13
13
  */
14
- declare function server(): {};
14
+ export default function server(): {};
15
15
  //#endregion
16
- export { server as default };
17
16
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;;;;;;;;;iBAYwB"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;;;;;;;;;wBAYwB"}