@skillstate/opencode 2.0.2 → 2.0.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.
package/README.md CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/@skillstate/opencode)](https://www.npmjs.com/package/@skillstate/opencode)
8
8
  [![node](https://img.shields.io/node/v/@skillstate/opencode)](https://www.npmjs.com/package/@skillstate/opencode)
9
- [![Tests](https://img.shields.io/badge/tests-755%20passing-brightgreen)](https://github.com/vitalykuzyaev/skillstate)
10
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/vitalykuzyaev/skillstate/blob/main/LICENSE)
9
+ [![Tests](https://img.shields.io/badge/tests-755%20passing-brightgreen)](https://github.com/vitkuz573/skillstate)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/vitkuz573/skillstate/blob/main/LICENSE)
11
11
 
12
12
  </div>
13
13
 
@@ -60,18 +60,85 @@ const saved = await adapter.savePluginCode(
60
60
  );
61
61
  ```
62
62
 
63
+ ## Install into OpenCode (host)
64
+
65
+ Tested end-to-end against OpenCode ≥ 1.17 (`@opencode-ai/plugin` 1.15.x
66
+ hook contracts). Four steps:
67
+
68
+ **1. Generate the plugin file** (one-off, or wire into a build script):
69
+
70
+ ```ts
71
+ // scripts/gen-plugin.mjs (run with: node scripts/gen-plugin.mjs)
72
+ import { OpenCodeAdapter } from '@skillstate/opencode';
73
+ import { writeFileSync } from 'node:fs';
74
+
75
+ const adapter = new OpenCodeAdapter();
76
+ const statePath = '/abs/path/to/.skillstate.json';
77
+ const code = adapter.generatePluginCode(statePath, { maxHistoryMessages: 3 });
78
+ writeFileSync('/abs/path/to/skillstate.plugin.ts', code);
79
+ ```
80
+
81
+ Put the generated `skillstate.plugin.ts` anywhere stable (absolute path is
82
+ safest), e.g. `<project>/.opencode-runtime/skillstate.plugin.ts`.
83
+
84
+ **2. Create the initial state file** (`statePath` from step 1), matching your
85
+ spec schema, e.g. for `INTERCODE_CTF_SPEC`:
86
+
87
+ ```json
88
+ {
89
+ "discovered_flags": [],
90
+ "tested_hypotheses": [],
91
+ "active_files": [],
92
+ "working_dir": "/abs/work/dir",
93
+ "cmd_summary": "initialized"
94
+ }
95
+ ```
96
+
97
+ **3. Register the plugin in `opencode.jsonc`** — add a `file://` entry to the
98
+ `plugin` array (local paths are resolved by OpenCode and imported directly;
99
+ TypeScript is supported because plugins load under Bun):
100
+
101
+ ```jsonc
102
+ {
103
+ "plugin": [
104
+ "@ai-sdk/anthropic",
105
+ "file:///abs/path/to/skillstate.plugin.ts"
106
+ ]
107
+ }
108
+ ```
109
+
110
+ **4. Install the SKILL.md** — write `adapter.generateSkillMd(spec, statePath)`
111
+ to `~/.config/opencode/skills/skillstate/SKILL.md` (global) or
112
+ `.opencode/skills/skillstate/SKILL.md` (project). Keep the frontmatter fields
113
+ `name` (must match the folder name) and a one-line `description`; the
114
+ generated frontmatter may need a manual trim to those two fields.
115
+
116
+ Verify with `opencode debug config` (plugin entry shows under `plugin`) and
117
+ `opencode debug skill` (your skill is listed). Hook notes for OpenCode ≥ 1.17:
118
+ `messages.transform` receives `{ info: Message, parts: Part[] }` entries and
119
+ must mutate `output.messages` **in place**; the plugin injects state as a
120
+ synthetic `{ info, parts }` message.
121
+
63
122
  ## API / Exports
64
123
 
65
- Root path `@skillstate/opencode` exports one thing: `OpenCodeAdapter`.
124
+ Root path `@skillstate/opencode` exports the adapter and the static plugin:
66
125
 
67
126
  - `new OpenCodeAdapter()` — implements `PlatformAdapter` (`name = 'opencode'`).
68
127
  - `generateSkillMd(spec, statePath?): string` — a `SKILL.md` body with
69
128
  frontmatter and a state-based process description.
70
- - `generatePluginCode(statePath, options?): string` — an OpenCode plugin
71
- (`options.maxHistoryMessages`, default 3). Hooks:
129
+ - `generatePluginCode(statePath, options?): string` — a thin plugin loader by
130
+ default (`import { createSkillStatePlugin } from '@skillstate/opencode'` with
131
+ the state path baked in; `options.maxHistoryMessages`, default 3).
132
+ `options.standalone: true` inlines the full self-contained plugin — escape
133
+ hatch for environments without npm resolution of this package. Hooks:
72
134
  `experimental.chat.messages.transform` (real history trimming),
73
135
  `experimental.session.compacting` (inject state into compaction context),
74
136
  `tool.execute.after` (persist `state_patch` to disk).
137
+ - `createSkillStatePlugin({ statePath, maxHistoryMessages? })` — the static
138
+ plugin factory (single source of truth for the hook logic); re-exported from
139
+ `@skillstate/opencode/plugin` as well.
140
+ - `readSkillState` / `saveSkillState` / `mergePatch` / `extractPatch` — the
141
+ plugin's state helpers, shared by the static plugin.
75
142
  - `savePluginCode(target, statePath, options?): Promise<string>` — writes the
76
143
  plugin atomically and returns the destination.
77
144
  - `injectState(state, spec): string` / `formatPrompt(state, observation, spec): string`.
@@ -86,8 +153,12 @@ Both `generatePluginCode`/`savePluginCode` accept a raw path (legacy) or a
86
153
  `experimental.chat.messages.transform`, so the plugin drops old messages
87
154
  instead of just hiding them — only the last N non-system messages plus an
88
155
  injected state message reach the LLM.
89
- - The generated plugin is a self-contained ESM/TS module; it reads and writes
90
- the state file directly and applies the paper ⊕ null-deletion merge.
156
+ - The default generated plugin is a **thin loader**: it imports
157
+ `createSkillStatePlugin` from `@skillstate/opencode` (one source of truth)
158
+ and bakes in the state path. The `{ standalone: true }` template is a
159
+ self-contained ESM/TS escape hatch for environments without npm resolution;
160
+ hook logic lives only in `src/plugin.ts` — regenerate rather than editing
161
+ generated files.
91
162
  - Depends on [`@skillstate/core`](../core) for `PromptTransformer`,
92
163
  `atomicWriteFile`, and `resolveStatePath`.
93
164
 
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './opencode-adapter.js';
2
+ export * from './plugin.js';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  // @skillstate/opencode — OpenCode platform adapter.
2
2
  export * from './opencode-adapter.js';
3
+ export * from './plugin.js';
3
4
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC"}
@@ -14,6 +14,16 @@
14
14
  */
15
15
  import type { SkillState, StatePatch, ProceduralSpec, Observation, PlatformAdapter } from '@skillstate/core';
16
16
  import type { StatePathRef } from '@skillstate/core';
17
+ /** Options for {@link OpenCodeAdapter.generatePluginCode}. */
18
+ export interface GeneratePluginOptions {
19
+ /** Non-system messages kept by the plugin (default 3). */
20
+ maxHistoryMessages?: number;
21
+ /**
22
+ * Inline the full plugin instead of the thin loader — escape hatch for
23
+ * environments without npm resolution of `@skillstate/opencode`.
24
+ */
25
+ standalone?: boolean;
26
+ }
17
27
  /**
18
28
  * OpenCode platform adapter (@non-paper; see module doc).
19
29
  */
@@ -51,27 +61,30 @@ export declare class OpenCodeAdapter implements PlatformAdapter {
51
61
  */
52
62
  generateSkillMd(spec: ProceduralSpec, statePath?: string): string;
53
63
  /**
54
- * Generate an OpenCode plugin with real O(1) prompt economy.
64
+ * Generate an OpenCode plugin file.
65
+ *
66
+ * Default (thin) mode: a loader that imports the STATIC plugin
67
+ * (`createSkillStatePlugin` from `@skillstate/opencode`) — the single
68
+ * source of truth for hook logic. `statePath`/`maxHistoryMessages` are
69
+ * baked into the loader call.
70
+ *
71
+ * `{ standalone: true }` inlines the full plugin (self-contained template,
72
+ * escape hatch for environments without npm resolution of
73
+ * `@skillstate/opencode`).
55
74
  *
56
- * Hooks:
57
- * - `experimental.chat.messages.transform`: trims history to the last
58
- * `maxHistoryMessages` non-system messages, injects a synthetic state
59
- * message — real O(1) prompt footprint.
60
- * - `experimental.session.compacting`: injects state into the compaction
61
- * context so the summary preserves it.
62
- * - `tool.execute.after`: persists state updates from LLM responses.
75
+ * Hooks (see `src/plugin.ts`): `experimental.chat.messages.transform`
76
+ * (real O(1) history trimming, `{ info, parts }` entries, in-place
77
+ * mutation), `experimental.session.compacting` (state into compaction
78
+ * context), `tool.execute.after` (persist `state_patch` from
79
+ * `output.output`).
63
80
  */
64
- generatePluginCode(statePath: string, options?: {
65
- maxHistoryMessages?: number;
66
- }): string;
81
+ generatePluginCode(statePath: string, options?: GeneratePluginOptions): string;
67
82
  /**
68
83
  * @non-paper additive overload: accept a `{ root, name }` ref confined
69
84
  * via `resolveStatePath` — `..` escapes throw instead of embedding an
70
85
  * unsafe path into the generated plugin.
71
86
  */
72
- generatePluginCode(stateRef: StatePathRef, options?: {
73
- maxHistoryMessages?: number;
74
- }): string;
87
+ generatePluginCode(stateRef: StatePathRef, options?: GeneratePluginOptions): string;
75
88
  /**
76
89
  * @non-paper additive helper: generate the plugin and persist it via
77
90
  * `atomicWriteFile` (tmp + fsync + rename). Both the destination and the
@@ -79,9 +92,7 @@ export declare class OpenCodeAdapter implements PlatformAdapter {
79
92
  * `{ root, name }` refs confined by `resolveStatePath`. Returns the
80
93
  * absolute destination path.
81
94
  */
82
- savePluginCode(target: string | StatePathRef, statePath: string | StatePathRef, options?: {
83
- maxHistoryMessages?: number;
84
- }): Promise<string>;
95
+ savePluginCode(target: string | StatePathRef, statePath: string | StatePathRef, options?: GeneratePluginOptions): Promise<string>;
85
96
  /**
86
97
  * Describe the schema fields for inclusion in prompts.
87
98
  */
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-adapter.d.ts","sourceRoot":"","sources":["../src/opencode-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,cAAc,EACd,WAAW,EACX,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAM1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD;;GAEG;AACH,qBAAa,eAAgB,YAAW,eAAe;IACrD,QAAQ,CAAC,IAAI,cAAc;IAE3B,OAAO,CAAC,WAAW,CAAmD;IAEtE;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CAsB3D;IAED;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAEhD;IAED;;OAEG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE7C;IAED;;;;;OAKG;IACH,YAAY,CACV,KAAK,EAAE,UAAU,EACjB,WAAW,EAAE,WAAW,EACxB,IAAI,EAAE,cAAc,GACnB,MAAM,CAER;IAED;;;;;;;OAOG;IACH,eAAe,CAAC,IAAI,EAAE,cAAc,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CA0ChE;IAED;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC;IACzF;;;;OAIG;IACH,kBAAkB,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE;QAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC;IAoI9F;;;;;;OAMG;IACG,cAAc,CAClB,MAAM,EAAE,MAAM,GAAG,YAAY,EAC7B,SAAS,EAAE,MAAM,GAAG,YAAY,EAChC,OAAO,CAAC,EAAE;QAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAAE,GACxC,OAAO,CAAC,MAAM,CAAC,CAYjB;IAMD;;OAEG;IACH,OAAO,CAAC,cAAc;CASvB"}
1
+ {"version":3,"file":"opencode-adapter.d.ts","sourceRoot":"","sources":["../src/opencode-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,cAAc,EACd,WAAW,EACX,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAM1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,8DAA8D;AAC9D,MAAM,WAAW,qBAAqB;IACpC,0DAA0D;IAC1D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AACH,qBAAa,eAAgB,YAAW,eAAe;IACrD,QAAQ,CAAC,IAAI,cAAc;IAE3B,OAAO,CAAC,WAAW,CAAmD;IAEtE;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CAsB3D;IAED;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAEhD;IAED;;OAEG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE7C;IAED;;;;;OAKG;IACH,YAAY,CACV,KAAK,EAAE,UAAU,EACjB,WAAW,EAAE,WAAW,EACxB,IAAI,EAAE,cAAc,GACnB,MAAM,CAER;IAED;;;;;;;OAOG;IACH,eAAe,CAAC,IAAI,EAAE,cAAc,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CA0ChE;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAAC;IAC/E;;;;OAIG;IACH,kBAAkB,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAAC;IA4BpF;;;;;;OAMG;IACG,cAAc,CAClB,MAAM,EAAE,MAAM,GAAG,YAAY,EAC7B,SAAS,EAAE,MAAM,GAAG,YAAY,EAChC,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAYjB;IAMD;;OAEG;IACH,OAAO,CAAC,cAAc;CASvB"}
@@ -108,9 +108,63 @@ plugin; only the current state and the latest observation matter.
108
108
  const statePath = typeof statePathOrRef === 'string'
109
109
  ? statePathOrRef
110
110
  : resolveStatePath(statePathOrRef.root, statePathOrRef.name);
111
- const sp = JSON.stringify(statePath);
112
111
  const maxHistory = options?.maxHistoryMessages ?? 3;
113
- return `// OpenCode plugin generated by skillstate.
112
+ if (options?.standalone) {
113
+ return standalonePluginCode(statePath, maxHistory);
114
+ }
115
+ const sp = JSON.stringify(statePath);
116
+ return `// OpenCode plugin (thin loader) generated by skillstate.
117
+ // Hook logic lives in the static plugin — the single source of truth —
118
+ // shipped in the @skillstate/opencode package.
119
+ import { createSkillStatePlugin } from '@skillstate/opencode';
120
+
121
+ export default createSkillStatePlugin({
122
+ statePath: ${sp},
123
+ maxHistoryMessages: ${maxHistory},
124
+ });
125
+ `;
126
+ }
127
+ /**
128
+ * @non-paper additive helper: generate the plugin and persist it via
129
+ * `atomicWriteFile` (tmp + fsync + rename). Both the destination and the
130
+ * embedded state path accept raw strings (legacy behavior) or
131
+ * `{ root, name }` refs confined by `resolveStatePath`. Returns the
132
+ * absolute destination path.
133
+ */
134
+ async savePluginCode(target, statePath, options) {
135
+ const dest = typeof target === 'string'
136
+ ? target
137
+ : resolveStatePath(target.root, target.name);
138
+ const resolvedState = typeof statePath === 'string'
139
+ ? statePath
140
+ : resolveStatePath(statePath.root, statePath.name);
141
+ const plugin = this.generatePluginCode(resolvedState, options);
142
+ await atomicWriteFile(dest, plugin);
143
+ return dest;
144
+ }
145
+ /* ------------------------------------------------------------------ */
146
+ /* Internal helpers */
147
+ /* ------------------------------------------------------------------ */
148
+ /**
149
+ * Describe the schema fields for inclusion in prompts.
150
+ */
151
+ describeSchema(schema) {
152
+ const fields = Object.entries(schema)
153
+ .map(([name, field]) => `- ${name} (${field.type}): ${field.description ?? 'no description'}`)
154
+ .join('\n');
155
+ return `## Schema\n${fields}`;
156
+ }
157
+ }
158
+ /**
159
+ * Standalone (escape-hatch) plugin template — a self-contained inline copy of
160
+ * `src/plugin.ts` for environments without npm resolution of
161
+ * `@skillstate/opencode`. Escape hatch only: `src/plugin.ts` remains the
162
+ * single source of truth; regenerate rather than editing generated files.
163
+ */
164
+ function standalonePluginCode(statePath, maxHistory) {
165
+ const sp = JSON.stringify(statePath);
166
+ return `// OpenCode plugin (standalone escape hatch) generated by skillstate.
167
+ // Self-contained copy of createSkillStatePlugin from @skillstate/opencode.
114
168
  // Real O(1) prompt economy via experimental.chat.messages.transform:
115
169
  // trims history to last ${maxHistory} non-system messages + injected state.
116
170
 
@@ -139,20 +193,20 @@ function saveSkillState(state: Record<string, unknown>): void {
139
193
  }
140
194
  }
141
195
 
196
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
197
+ return typeof value === "object" && value !== null && !Array.isArray(value);
198
+ }
199
+
142
200
  function mergePatch(base: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {
143
201
  const result = { ...base };
144
202
  for (const key of Object.keys(patch)) {
145
- if (patch[key] === null) {
203
+ const value = patch[key];
204
+ if (value === null) {
146
205
  delete result[key];
147
- } else if (
148
- typeof patch[key] === "object" && patch[key] !== null &&
149
- !Array.isArray(patch[key]) &&
150
- typeof result[key] === "object" && result[key] !== null &&
151
- !Array.isArray(result[key])
152
- ) {
153
- result[key] = mergePatch(result[key] as Record<string, unknown>, patch[key] as Record<string, unknown>);
206
+ } else if (isPlainObject(value) && isPlainObject(result[key])) {
207
+ result[key] = mergePatch(result[key] as Record<string, unknown>, value);
154
208
  } else {
155
- result[key] = patch[key];
209
+ result[key] = value;
156
210
  }
157
211
  }
158
212
  return result;
@@ -162,9 +216,9 @@ function extractPatch(response: string): Record<string, unknown> | null {
162
216
  const match = response.match(/\`\`\`json\\s*\\n?([\\s\\S]*?)\\n?\\s*\`\`\`/);
163
217
  if (!match) return null;
164
218
  try {
165
- const parsed = JSON.parse(match[1]);
166
- if (parsed.state_patch && typeof parsed.state_patch === "object" && !Array.isArray(parsed.state_patch)) {
167
- return parsed.state_patch;
219
+ const parsed: unknown = JSON.parse(match[1]);
220
+ if (isPlainObject(parsed) && isPlainObject(parsed["state_patch"])) {
221
+ return parsed["state_patch"];
168
222
  }
169
223
  } catch {
170
224
  // Malformed JSON — ignore.
@@ -172,92 +226,76 @@ function extractPatch(response: string): Record<string, unknown> | null {
172
226
  return null;
173
227
  }
174
228
 
175
- export const SkillStatePlugin: Plugin = async ({ project, client, $ }) => {
229
+ const SkillStatePlugin: Plugin = async () => {
176
230
  return {
177
231
  // ── O(1) history trimming ──────────────────────────────────────────
178
232
  // Filters messages BEFORE each LLM call: keeps all system messages
179
233
  // plus the last ${maxHistory} non-system messages, then injects a
180
- // synthetic user message with the current state. This is real O(1):
181
- // old messages are DROPPED from the prompt, not just hidden.
182
- "experimental.chat.messages.transform": async (input, output) => {
234
+ // synthetic state element. Old messages are DROPPED from the prompt,
235
+ // not just hidden.
236
+ "experimental.chat.messages.transform": async (_input, output) => {
183
237
  const state = readSkillState();
184
- const messages = output.messages ?? [];
185
- const systemMessages = messages.filter((m) => m.role === "system");
186
- const nonSystem = messages.filter((m) => m.role !== "system");
187
- const trimmed = nonSystem.slice(-MAX_HISTORY);
238
+ const messages = output.messages;
239
+ const systemMessages = messages.filter((m) => m.info.role === "system");
240
+ const trimmed = messages
241
+ .filter((m) => m.info.role !== "system")
242
+ .slice(-MAX_HISTORY);
188
243
 
189
244
  const stateMessage = {
190
- role: "user",
191
- content: "Current skill state (JSON): " + JSON.stringify(state),
245
+ info: {
246
+ id: "skillstate-state-inject",
247
+ sessionID: "skillstate",
248
+ role: "user",
249
+ time: { created: 0 },
250
+ agent: "skillstate",
251
+ model: { providerID: "skillstate", modelID: "skillstate" },
252
+ },
253
+ parts: [
254
+ {
255
+ id: "skillstate-state-inject-text",
256
+ sessionID: "skillstate",
257
+ messageID: "skillstate-state-inject",
258
+ type: "text",
259
+ synthetic: true,
260
+ text: "Current skill state (JSON): " + JSON.stringify(state),
261
+ },
262
+ ],
192
263
  };
193
264
 
194
- output.messages = [...systemMessages, ...trimmed, stateMessage];
195
- return output;
265
+ // The pipeline holds the original array reference — mutate in place.
266
+ const kept = [...systemMessages, ...trimmed, stateMessage];
267
+ messages.length = 0;
268
+ messages.push(...kept);
196
269
  },
197
270
 
198
271
  // ── Compaction context injection ───────────────────────────────────
199
272
  // Before compaction, inject the current state into the context so the
200
273
  // compaction summary preserves state even after history is compressed.
201
- "experimental.session.compacting": async (input, output) => {
274
+ "experimental.session.compacting": async (_input, output) => {
202
275
  const state = readSkillState();
203
276
  const stateContext = "Skillstate: " + JSON.stringify(state);
204
277
  if (!Array.isArray(output.context)) {
205
278
  output.context = [];
206
279
  }
207
280
  output.context.push(stateContext);
208
- return output;
209
281
  },
210
282
 
211
283
  // ── State persistence from LLM responses ───────────────────────────
212
- // After tool execution, extract state_patch from the assistant's last
213
- // response, merge it, and save to disk.
214
- "tool.execute.after": async (input, output) => {
215
- const response = output?.result ?? "";
284
+ // After tool execution, extract state_patch from the tool response
285
+ // (output.output), merge it, and save to disk.
286
+ "tool.execute.after": async (_input, output) => {
287
+ const response = output.output ?? "";
216
288
  if (typeof response === "string") {
217
289
  const patch = extractPatch(response);
218
290
  if (patch) {
219
- const current = readSkillState();
220
- const merged = mergePatch(current, patch);
221
- saveSkillState(merged);
291
+ saveSkillState(STATE_PATH, mergePatch(readSkillState(), patch));
222
292
  }
223
293
  }
224
- return output;
225
294
  },
226
295
  };
227
296
  };
228
297
 
229
298
  export default SkillStatePlugin;
230
299
  `;
231
- }
232
- /**
233
- * @non-paper additive helper: generate the plugin and persist it via
234
- * `atomicWriteFile` (tmp + fsync + rename). Both the destination and the
235
- * embedded state path accept raw strings (legacy behavior) or
236
- * `{ root, name }` refs confined by `resolveStatePath`. Returns the
237
- * absolute destination path.
238
- */
239
- async savePluginCode(target, statePath, options) {
240
- const dest = typeof target === 'string'
241
- ? target
242
- : resolveStatePath(target.root, target.name);
243
- const resolvedState = typeof statePath === 'string'
244
- ? statePath
245
- : resolveStatePath(statePath.root, statePath.name);
246
- const plugin = this.generatePluginCode(resolvedState, options);
247
- await atomicWriteFile(dest, plugin);
248
- return dest;
249
- }
250
- /* ------------------------------------------------------------------ */
251
- /* Internal helpers */
252
- /* ------------------------------------------------------------------ */
253
- /**
254
- * Describe the schema fields for inclusion in prompts.
255
- */
256
- describeSchema(schema) {
257
- const fields = Object.entries(schema)
258
- .map(([name, field]) => `- ${name} (${field.type}): ${field.description ?? 'no description'}`)
259
- .join('\n');
260
- return `## Schema\n${fields}`;
261
- }
262
300
  }
263
301
  //# sourceMappingURL=opencode-adapter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-adapter.js","sourceRoot":"","sources":["../src/opencode-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"opencode-adapter.js","sourceRoot":"","sources":["../src/opencode-adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Minimal local types for OpenCode plugin hooks — the `@opencode-ai/plugin`
3
+ * shape, verified against opencode 1.17 (PluginInput/Hooks). Declared locally
4
+ * to keep `@skillstate/opencode` zero-dep; the host supplies the real types
5
+ * at runtime.
6
+ */
7
+ /** Message roles seen by `experimental.chat.messages.transform`. */
8
+ export type OpenCodeMessageRole = 'user' | 'assistant' | 'system';
9
+ /**
10
+ * Message envelope: the transform pipeline hands each message as
11
+ * `{ info, parts }` — the role lives on `info.role`.
12
+ */
13
+ export interface OpenCodeMessage {
14
+ info: {
15
+ role: OpenCodeMessageRole | string;
16
+ [key: string]: unknown;
17
+ };
18
+ parts: Array<{
19
+ type: string;
20
+ text?: string;
21
+ [key: string]: unknown;
22
+ }>;
23
+ }
24
+ /** Output payload of `experimental.chat.messages.transform`. */
25
+ export interface MessagesTransformOutput {
26
+ /** Mutated IN PLACE — the pipeline keeps the original array reference. */
27
+ messages: OpenCodeMessage[];
28
+ }
29
+ /** Output payload of `experimental.session.compacting`. */
30
+ export interface SessionCompactingOutput {
31
+ context: string[];
32
+ prompt?: string;
33
+ }
34
+ /** Output payload of `tool.execute.after`. */
35
+ export interface ToolExecuteAfterOutput {
36
+ title?: string;
37
+ /** The tool's string response (state_patch extraction source). */
38
+ output: unknown;
39
+ metadata?: unknown;
40
+ }
41
+ /** OpenCode hooks used by the skillstate plugin. */
42
+ export interface SkillStateHooks {
43
+ 'experimental.chat.messages.transform'?: (input: Record<string, never>, output: MessagesTransformOutput) => Promise<void>;
44
+ 'experimental.session.compacting'?: (input: {
45
+ sessionID: string;
46
+ }, output: SessionCompactingOutput) => Promise<void>;
47
+ 'tool.execute.after'?: (input: {
48
+ tool: string;
49
+ sessionID: string;
50
+ callID: string;
51
+ args: unknown;
52
+ }, output: ToolExecuteAfterOutput) => Promise<void>;
53
+ }
54
+ /**
55
+ * OpenCode plugin: an async factory receiving the host input and returning
56
+ * the hooks object (`Plugin` from `@opencode-ai/plugin`).
57
+ */
58
+ export type SkillStatePlugin = (input: {
59
+ project?: unknown;
60
+ client?: unknown;
61
+ $?: unknown;
62
+ [key: string]: unknown;
63
+ }) => Promise<SkillStateHooks>;
64
+ //# sourceMappingURL=plugin-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-types.d.ts","sourceRoot":"","sources":["../src/plugin-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,oEAAoE;AACpE,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;AAElE;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE;QAAE,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IACrE,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;CACvE;AAED,gEAAgE;AAChE,MAAM,WAAW,uBAAuB;IACtC,0EAA0E;IAC1E,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,2DAA2D;AAC3D,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8CAA8C;AAC9C,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,oDAAoD;AACpD,MAAM,WAAW,eAAe;IAC9B,sCAAsC,CAAC,EAAE,CACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAC5B,MAAM,EAAE,uBAAuB,KAC5B,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,iCAAiC,CAAC,EAAE,CAClC,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,EAC5B,MAAM,EAAE,uBAAuB,KAC5B,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,oBAAoB,CAAC,EAAE,CACrB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,EACzE,MAAM,EAAE,sBAAsB,KAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,CAAC,CAAC,EAAE,OAAO,CAAC;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Minimal local types for OpenCode plugin hooks — the `@opencode-ai/plugin`
3
+ * shape, verified against opencode 1.17 (PluginInput/Hooks). Declared locally
4
+ * to keep `@skillstate/opencode` zero-dep; the host supplies the real types
5
+ * at runtime.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=plugin-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-types.js","sourceRoot":"","sources":["../src/plugin-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,38 @@
1
+ import type { SkillStatePlugin } from './plugin-types.js';
2
+ export * from './plugin-types.js';
3
+ /** Options for {@link createSkillStatePlugin}. */
4
+ export interface SkillStatePluginOptions {
5
+ /** Absolute path of the persisted state file. */
6
+ statePath: string;
7
+ /** Non-system messages kept in the prompt (default 3). */
8
+ maxHistoryMessages?: number;
9
+ }
10
+ /**
11
+ * Read the state file. Missing or corrupt files yield `{}` (best-effort).
12
+ * The on-disk envelope is `{ version: 1, state }` (migrations-compatible);
13
+ * a bare object is tolerated and treated as the state itself.
14
+ */
15
+ export declare function readSkillState(statePath: string): Record<string, unknown>;
16
+ /**
17
+ * Persist the state file (best-effort: read-only environments are ignored).
18
+ * Writes the `{ version: 1, state }` envelope so `migrate()`/runtime resume
19
+ * read the same file.
20
+ */
21
+ export declare function saveSkillState(statePath: string, state: Record<string, unknown>): void;
22
+ /**
23
+ * Paper ⊕ merge: `null` deletes a key, nested plain objects merge
24
+ * recursively, everything else replaces.
25
+ */
26
+ export declare function mergePatch(base: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
27
+ /**
28
+ * Extract the `state_patch` object from an LLM response's fenced ```json
29
+ * block; `null` when there is no block, it is malformed, or it carries no
30
+ * object-shaped `state_patch`.
31
+ */
32
+ export declare function extractPatch(response: string): Record<string, unknown> | null;
33
+ /**
34
+ * Build the OpenCode plugin function with the same behavior for every host
35
+ * entry point (thin generated loaders, direct imports).
36
+ */
37
+ export declare function createSkillStatePlugin(options: SkillStatePluginOptions): SkillStatePlugin;
38
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,mBAAmB,CAAC;AAE3B,cAAc,mBAAmB,CAAC;AAElC,kDAAkD;AAClD,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAOD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqBzE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAMtF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAazB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAY7E;AAKD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CA+EzF"}
package/dist/plugin.js ADDED
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Static OpenCode plugin — the SINGLE SOURCE OF TRUTH for the skillstate
3
+ * host integration. `OpenCodeAdapter.generatePluginCode` (thin mode, the
4
+ * default) emits a loader that imports `createSkillStatePlugin` from this
5
+ * module; the standalone template is only an escape hatch for environments
6
+ * without npm resolution of `@skillstate/opencode`.
7
+ *
8
+ * Hooks (opencode 1.17 contract, verified on host):
9
+ * - `experimental.chat.messages.transform` — entries are `{ info, parts }`
10
+ * envelopes (role on `info.role`); the pipeline keeps the ORIGINAL array
11
+ * reference, so trimming mutates in place; the state is injected as a
12
+ * synthetic `{ info, parts }` element. Real O(1) prompt footprint.
13
+ * - `experimental.session.compacting` — pushes the state into
14
+ * `output.context` so the compaction summary preserves it.
15
+ * - `tool.execute.after` — the tool response is `output.output`; a fenced
16
+ * ```json `state_patch` block is merged (paper ⊕: null deletes) and saved.
17
+ */
18
+ import * as fs from 'node:fs';
19
+ export * from './plugin-types.js';
20
+ /** True for plain (non-null, non-array) objects. */
21
+ function isPlainObject(value) {
22
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
23
+ }
24
+ /**
25
+ * Read the state file. Missing or corrupt files yield `{}` (best-effort).
26
+ * The on-disk envelope is `{ version: 1, state }` (migrations-compatible);
27
+ * a bare object is tolerated and treated as the state itself.
28
+ */
29
+ export function readSkillState(statePath) {
30
+ try {
31
+ if (fs.existsSync(statePath)) {
32
+ const parsed = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
33
+ if (typeof parsed === 'object' &&
34
+ parsed !== null &&
35
+ 'state' in parsed &&
36
+ typeof parsed['state'] === 'object' &&
37
+ parsed['state'] !== null) {
38
+ return parsed['state'];
39
+ }
40
+ if (typeof parsed === 'object' && parsed !== null) {
41
+ return parsed;
42
+ }
43
+ }
44
+ }
45
+ catch {
46
+ // Corrupt or unreadable state file — fall back to empty state.
47
+ }
48
+ return {};
49
+ }
50
+ /**
51
+ * Persist the state file (best-effort: read-only environments are ignored).
52
+ * Writes the `{ version: 1, state }` envelope so `migrate()`/runtime resume
53
+ * read the same file.
54
+ */
55
+ export function saveSkillState(statePath, state) {
56
+ try {
57
+ fs.writeFileSync(statePath, `${JSON.stringify({ version: 1, state }, null, 2)}\n`);
58
+ }
59
+ catch {
60
+ // Best-effort: read-only environments or permission issues.
61
+ }
62
+ }
63
+ /**
64
+ * Paper ⊕ merge: `null` deletes a key, nested plain objects merge
65
+ * recursively, everything else replaces.
66
+ */
67
+ export function mergePatch(base, patch) {
68
+ const result = { ...base };
69
+ for (const key of Object.keys(patch)) {
70
+ const value = patch[key];
71
+ if (value === null) {
72
+ delete result[key];
73
+ }
74
+ else if (isPlainObject(value) && isPlainObject(result[key])) {
75
+ result[key] = mergePatch(result[key], value);
76
+ }
77
+ else {
78
+ result[key] = value;
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ /**
84
+ * Extract the `state_patch` object from an LLM response's fenced ```json
85
+ * block; `null` when there is no block, it is malformed, or it carries no
86
+ * object-shaped `state_patch`.
87
+ */
88
+ export function extractPatch(response) {
89
+ const match = response.match(/```json\s*\n?([\s\S]*?)\n?\s*```/);
90
+ if (!match)
91
+ return null;
92
+ try {
93
+ const parsed = JSON.parse(match[1]);
94
+ if (isPlainObject(parsed) && isPlainObject(parsed['state_patch'])) {
95
+ return parsed['state_patch'];
96
+ }
97
+ }
98
+ catch {
99
+ // Malformed JSON — ignore.
100
+ }
101
+ return null;
102
+ }
103
+ /** Synthetic message ids for the injected state carrier. */
104
+ const STATE_MESSAGE_ID = 'skillstate-state-inject';
105
+ /**
106
+ * Build the OpenCode plugin function with the same behavior for every host
107
+ * entry point (thin generated loaders, direct imports).
108
+ */
109
+ export function createSkillStatePlugin(options) {
110
+ const statePath = options.statePath;
111
+ const maxHistory = options.maxHistoryMessages ?? 3;
112
+ return async () => {
113
+ return {
114
+ // ── O(1) history trimming ──────────────────────────────────────────
115
+ // Filters messages BEFORE each LLM call: keeps all system messages
116
+ // plus the last `maxHistory` non-system messages, then injects a
117
+ // synthetic state element. Old messages are DROPPED from the prompt,
118
+ // not just hidden.
119
+ 'experimental.chat.messages.transform': async (_input, output) => {
120
+ const state = readSkillState(statePath);
121
+ const messages = output.messages;
122
+ const systemMessages = messages.filter((m) => m.info.role === 'system');
123
+ const trimmed = messages
124
+ .filter((m) => m.info.role !== 'system')
125
+ .slice(-maxHistory);
126
+ // Synthetic state carrier — a `{ info, parts }` envelope whose text
127
+ // part carries the current state JSON.
128
+ const stateMessage = {
129
+ info: {
130
+ id: STATE_MESSAGE_ID,
131
+ sessionID: 'skillstate',
132
+ role: 'user',
133
+ time: { created: 0 },
134
+ agent: 'skillstate',
135
+ model: { providerID: 'skillstate', modelID: 'skillstate' },
136
+ },
137
+ parts: [
138
+ {
139
+ id: `${STATE_MESSAGE_ID}-text`,
140
+ sessionID: 'skillstate',
141
+ messageID: STATE_MESSAGE_ID,
142
+ type: 'text',
143
+ synthetic: true,
144
+ text: `Current skill state (JSON): ${JSON.stringify(state)}`,
145
+ },
146
+ ],
147
+ };
148
+ // The pipeline holds the original array reference — mutate in place
149
+ // (reassigning `output.messages` would not reach the LLM call).
150
+ const kept = [...systemMessages, ...trimmed, stateMessage];
151
+ messages.length = 0;
152
+ messages.push(...kept);
153
+ },
154
+ // ── Compaction context injection ───────────────────────────────────
155
+ // Before compaction, inject the current state into the context so the
156
+ // compaction summary preserves state even after history is compressed.
157
+ 'experimental.session.compacting': async (_input, output) => {
158
+ const state = readSkillState(statePath);
159
+ if (!Array.isArray(output.context)) {
160
+ output.context = [];
161
+ }
162
+ output.context.push(`Skillstate: ${JSON.stringify(state)}`);
163
+ },
164
+ // ── State persistence from LLM responses ───────────────────────────
165
+ // After tool execution, extract state_patch from the tool response
166
+ // (output.output), merge it, and save to disk.
167
+ 'tool.execute.after': async (_input, output) => {
168
+ const response = output.output ?? '';
169
+ if (typeof response !== 'string')
170
+ return;
171
+ const patch = extractPatch(response);
172
+ if (patch) {
173
+ saveSkillState(statePath, mergePatch(readSkillState(statePath), patch));
174
+ }
175
+ },
176
+ };
177
+ };
178
+ }
179
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillstate/opencode",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "OpenCode platform adapter for the skillstate runtime.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,6 +10,10 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "default": "./dist/index.js"
12
12
  },
13
+ "./plugin": {
14
+ "types": "./dist/plugin.d.ts",
15
+ "default": "./dist/plugin.js"
16
+ },
13
17
  "./package.json": "./package.json"
14
18
  },
15
19
  "files": [