@ericrisco/rsc 3.0.1 → 3.0.2
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/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/doctor.js +23 -1
- package/targets/knowledge-wiring.js +33 -9
- package/targets/opencode-memory-plugin.js +67 -22
- package/targets/update-wiring.js +18 -3
package/manifest.json
CHANGED
package/package.json
CHANGED
package/scripts/doctor.js
CHANGED
|
@@ -256,16 +256,38 @@ export function doctor({ target, home, cwd }) {
|
|
|
256
256
|
missingCommands,
|
|
257
257
|
commandOrphans,
|
|
258
258
|
commandCollisions: state.commandCollisions || [],
|
|
259
|
+
// #289 — OpenCode 2.x only loads a plugin from a default export with an id and setup(ctx); the
|
|
260
|
+
// V1-only files older versions wrote fail to load and nothing on rsc's side noticed. A plugin
|
|
261
|
+
// without the V2 entry point is a problem, and `sync` rewrites it.
|
|
262
|
+
opencodePluginsOutdated: target === 'opencode' ? opencodePluginsOutdated(root) : [],
|
|
259
263
|
};
|
|
260
264
|
// #277 — the three "missing" lists share one shape, { id, path, action }, so whoever builds on top
|
|
261
265
|
// of doctor reads them the same way; and `healthy` is what the exit code says.
|
|
262
266
|
for (const [id, e] of Object.entries(state.skills)) {
|
|
263
267
|
for (const f of e.files) if (!existsSync(f)) report.missing.push({ id, path: f, action: 'Run `npx @ericrisco/rsc sync` to restore this managed skill.' });
|
|
264
268
|
}
|
|
265
|
-
report.healthy = report.hookWired && !report.missing.length && !missingAgents.length && !missingCommands.length
|
|
269
|
+
report.healthy = report.hookWired && !report.missing.length && !missingAgents.length && !missingCommands.length
|
|
270
|
+
&& !report.opencodePluginsOutdated.length;
|
|
266
271
|
return report;
|
|
267
272
|
}
|
|
268
273
|
|
|
274
|
+
/** rsc's OpenCode plugins that OpenCode 2.x cannot load (no default export with id + setup). */
|
|
275
|
+
export function opencodePluginsOutdated(root) {
|
|
276
|
+
const dir = join(root, '.opencode', 'plugins');
|
|
277
|
+
const out = [];
|
|
278
|
+
for (const name of ['rsc-update.js', 'rsc-knowledge.js', 'rsc-memory.js']) {
|
|
279
|
+
const path = join(dir, name);
|
|
280
|
+
if (!existsSync(path)) continue;
|
|
281
|
+
let body = '';
|
|
282
|
+
try { body = readFileSync(path, 'utf8'); } catch { continue; }
|
|
283
|
+
if (!/:managed/.test(body)) continue; // hand-written: not ours to judge
|
|
284
|
+
if (!/export default\s*\{/.test(body) || !/\bsetup\s*\(/.test(body)) {
|
|
285
|
+
out.push({ path, action: 'OpenCode 2.x cannot load this plugin (V1 API only). Run `npx @ericrisco/rsc@latest sync` to rewrite it.' });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
|
|
269
291
|
// ---------------------------------------------------------------------------------------
|
|
270
292
|
// Context budget — what the harness costs in tokens before the user has typed anything.
|
|
271
293
|
//
|
|
@@ -83,24 +83,48 @@ const PLUGIN = `// ${PLUGIN_MARKER}
|
|
|
83
83
|
// rsc knowledge sync for OpenCode. OpenCode has no prompt event and no channel to the person, so what
|
|
84
84
|
// sync has to say goes to the model, to be relayed. Cheap on every call: the fetch is throttled and
|
|
85
85
|
// every notice is said once. Turn it off with \`rsc knowledge-sync off\`.
|
|
86
|
+
// One file for both plugin APIs (issue #289): V1 calls the named export or server(); V2 reads id + setup.
|
|
86
87
|
import { onRequest, onTurn } from '../../.rsc/knowledge-sync.mjs';
|
|
87
88
|
|
|
89
|
+
const local = () => process.env.RSC_REMOTE_AGENT !== '1' && process.env.OPENCODE_REMOTE !== '1';
|
|
90
|
+
const tell = (cwd) => {
|
|
91
|
+
if (!local()) return '';
|
|
92
|
+
try { const said = onRequest(cwd); return said ? \`Cuéntale esto al usuario en una línea:\\n\${said}\` : ''; } catch { return ''; }
|
|
93
|
+
};
|
|
94
|
+
const turn = (cwd) => { if (local()) { try { onTurn(cwd); } catch { /* fail open */ } } };
|
|
95
|
+
|
|
88
96
|
export const RscKnowledgePlugin = async ({ directory, worktree }) => {
|
|
89
97
|
const cwd = worktree || directory;
|
|
90
|
-
const local = () => process.env.RSC_REMOTE_AGENT !== '1' && process.env.OPENCODE_REMOTE !== '1';
|
|
91
98
|
return {
|
|
92
99
|
'experimental.chat.system.transform': async (_input, output) => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const said = onRequest(cwd);
|
|
96
|
-
if (said && Array.isArray(output?.system)) output.system.push(\`Cuéntale esto al usuario en una línea:\\n\${said}\`);
|
|
97
|
-
} catch { /* fail open */ }
|
|
98
|
-
},
|
|
99
|
-
event: async ({ event }) => {
|
|
100
|
-
if (event?.type === 'session.idle' && local()) { try { onTurn(cwd); } catch { /* fail open */ } }
|
|
100
|
+
const text = tell(cwd);
|
|
101
|
+
if (text && Array.isArray(output?.system)) output.system.push(text);
|
|
101
102
|
},
|
|
103
|
+
event: async ({ event }) => { if (event?.type === 'session.idle') turn(cwd); },
|
|
102
104
|
};
|
|
103
105
|
};
|
|
106
|
+
|
|
107
|
+
// V2 has no session.idle: a turn ends with session.execution.succeeded (or .failed).
|
|
108
|
+
export default {
|
|
109
|
+
id: 'rsc.knowledge',
|
|
110
|
+
server: RscKnowledgePlugin,
|
|
111
|
+
async setup(ctx) {
|
|
112
|
+
const cwd = ctx.location?.project?.directory || ctx.location?.directory || process.cwd();
|
|
113
|
+
await ctx.session.hook('context', async (event) => {
|
|
114
|
+
const text = tell(cwd);
|
|
115
|
+
if (text && Array.isArray(event?.system)) event.system.push({ type: 'text', text });
|
|
116
|
+
});
|
|
117
|
+
const stop = new AbortController();
|
|
118
|
+
(async () => {
|
|
119
|
+
try {
|
|
120
|
+
for await (const event of ctx.event.subscribe({ signal: stop.signal })) {
|
|
121
|
+
if (event?.type === 'session.execution.succeeded' || event?.type === 'session.execution.failed') turn(cwd);
|
|
122
|
+
}
|
|
123
|
+
} catch { /* unloading, or the stream ended: fail open */ }
|
|
124
|
+
})();
|
|
125
|
+
return () => stop.abort();
|
|
126
|
+
},
|
|
127
|
+
};
|
|
104
128
|
`;
|
|
105
129
|
|
|
106
130
|
export function knowledgeManagedPaths(target, cwd = process.cwd()) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// rsc-memory:managed
|
|
2
|
+
// One file for both OpenCode plugin APIs (issue #289). V1 (1.x) calls the named export, or the default
|
|
3
|
+
// export's server(); V2 (2.x) reads the default export's id + setup(ctx) and ignores the rest.
|
|
2
4
|
import { readFileSync } from 'node:fs';
|
|
3
5
|
import { join } from 'node:path';
|
|
4
6
|
import { capture, resume } from '../../.rsc/session-memory-core.mjs';
|
|
@@ -15,51 +17,94 @@ function memorySettings(cwd) {
|
|
|
15
17
|
|
|
16
18
|
function sessionId(input) {
|
|
17
19
|
return input?.sessionID || input?.session_id || input?.event?.properties?.info?.id
|
|
18
|
-
|| input?.event?.properties?.sessionID || null;
|
|
20
|
+
|| input?.event?.properties?.sessionID || input?.event?.data?.sessionID || null;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
/** What the plugin does, whichever API delivers the moment. `put(text)` adds to the system prompt. */
|
|
24
|
+
function memory(cwd) {
|
|
23
25
|
const injected = new Set();
|
|
24
26
|
const local = () => process.env.RSC_REMOTE_AGENT !== '1' && process.env.OPENCODE_REMOTE !== '1';
|
|
25
27
|
const save = (id, event, editDelta = 0) => {
|
|
26
28
|
if (!id || !local()) return;
|
|
27
29
|
try { capture({ cwd, sessionId: id, target: 'opencode', event, editDelta, settings: memorySettings(cwd) }); } catch { /* fail open */ }
|
|
28
30
|
};
|
|
29
|
-
|
|
30
31
|
return {
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
save,
|
|
33
|
+
context(id, put) {
|
|
33
34
|
if (!id || injected.has(id) || !local()) return;
|
|
34
35
|
injected.add(id);
|
|
35
36
|
try {
|
|
36
|
-
const config = memorySettings(cwd);
|
|
37
37
|
save(id, 'start');
|
|
38
|
-
const result = resume({ cwd, target: 'opencode', settings:
|
|
39
|
-
if (
|
|
40
|
-
if (output.system.length === 0) output.system.push(result.context);
|
|
41
|
-
else output.system[0] = `${output.system[0]}\n\n${result.context}`;
|
|
38
|
+
const result = resume({ cwd, target: 'opencode', settings: memorySettings(cwd) });
|
|
39
|
+
if (result.context) put(result.context);
|
|
42
40
|
} catch { /* fail open */ }
|
|
43
41
|
},
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
save(sessionId(input), 'boundary', /write|edit|patch/u.test(tool) ? 1 : 0);
|
|
42
|
+
tool(id, name) {
|
|
43
|
+
save(id, 'boundary', /write|edit|patch/u.test(String(name || '').toLowerCase()) ? 1 : 0);
|
|
47
44
|
},
|
|
48
|
-
|
|
49
|
-
const id = sessionId(input);
|
|
45
|
+
compact(id, put) {
|
|
50
46
|
save(id, 'compact');
|
|
51
47
|
try {
|
|
52
48
|
const result = resume({ cwd, target: 'opencode', settings: memorySettings(cwd) });
|
|
53
|
-
if (result.context
|
|
49
|
+
if (result.context) put(result.context);
|
|
54
50
|
} catch { /* fail open */ }
|
|
55
51
|
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------- OpenCode 1.x (V1 plugin API)
|
|
56
|
+
|
|
57
|
+
export const RscMemoryPlugin = async ({ directory, worktree }) => {
|
|
58
|
+
const m = memory(worktree || directory);
|
|
59
|
+
return {
|
|
60
|
+
'experimental.chat.system.transform': async (input, output) => {
|
|
61
|
+
if (!Array.isArray(output?.system)) return;
|
|
62
|
+
m.context(sessionId(input), (text) => {
|
|
63
|
+
if (output.system.length === 0) output.system.push(text);
|
|
64
|
+
else output.system[0] = `${output.system[0]}\n\n${text}`;
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
'tool.execute.after': async (input) => m.tool(sessionId(input), input?.tool),
|
|
68
|
+
'experimental.session.compacting': async (input, output) => {
|
|
69
|
+
m.compact(sessionId(input), (text) => { if (Array.isArray(output?.context)) output.context.push(text); });
|
|
70
|
+
},
|
|
56
71
|
event: async ({ event }) => {
|
|
57
72
|
const id = sessionId({ event });
|
|
58
|
-
if (event?.type === 'session.created') save(id, 'start');
|
|
59
|
-
else if (event?.type === 'file.edited') save(id, 'edit', 1);
|
|
60
|
-
else if (event?.type === 'session.compacted') save(id, 'compact');
|
|
61
|
-
else if (event?.type === 'session.idle') save(id, 'turn');
|
|
62
|
-
else if (event?.type === 'session.deleted') save(id, 'sessionEnd');
|
|
73
|
+
if (event?.type === 'session.created') m.save(id, 'start');
|
|
74
|
+
else if (event?.type === 'file.edited') m.save(id, 'edit', 1);
|
|
75
|
+
else if (event?.type === 'session.compacted') m.save(id, 'compact');
|
|
76
|
+
else if (event?.type === 'session.idle') m.save(id, 'turn');
|
|
77
|
+
else if (event?.type === 'session.deleted') m.save(id, 'sessionEnd');
|
|
63
78
|
},
|
|
64
79
|
};
|
|
65
80
|
};
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------- OpenCode 2.x (V2 plugin API)
|
|
83
|
+
// Hooks register through the context; the end of a turn is `session.execution.*` (there is no
|
|
84
|
+
// `session.idle`), and every event carries its session in `data.sessionID`.
|
|
85
|
+
|
|
86
|
+
const TURN_END = new Set(['session.execution.succeeded', 'session.execution.failed']);
|
|
87
|
+
|
|
88
|
+
export default {
|
|
89
|
+
id: 'rsc.memory',
|
|
90
|
+
server: RscMemoryPlugin,
|
|
91
|
+
async setup(ctx) {
|
|
92
|
+
const m = memory(ctx.location?.project?.directory || ctx.location?.directory || process.cwd());
|
|
93
|
+
const part = (event) => (text) => { if (Array.isArray(event?.system)) event.system.push({ type: 'text', text }); };
|
|
94
|
+
await ctx.session.hook('context', async (event) => m.context(event?.sessionID, part(event)));
|
|
95
|
+
await ctx.session.hook('compaction', async (event) => m.compact(event?.sessionID, part(event)));
|
|
96
|
+
await ctx.tool.hook('execute.after', async (event) => m.tool(event?.sessionID, event?.tool));
|
|
97
|
+
const stop = new AbortController();
|
|
98
|
+
(async () => {
|
|
99
|
+
try {
|
|
100
|
+
for await (const event of ctx.event.subscribe({ signal: stop.signal })) {
|
|
101
|
+
const id = event?.data?.sessionID;
|
|
102
|
+
if (event?.type === 'session.created') m.save(id, 'start');
|
|
103
|
+
else if (TURN_END.has(event?.type)) m.save(id, 'turn');
|
|
104
|
+
else if (event?.type === 'session.deleted') m.save(id, 'sessionEnd');
|
|
105
|
+
}
|
|
106
|
+
} catch { /* unloading, or the stream ended: fail open */ }
|
|
107
|
+
})();
|
|
108
|
+
return () => stop.abort();
|
|
109
|
+
},
|
|
110
|
+
};
|
package/targets/update-wiring.js
CHANGED
|
@@ -78,19 +78,34 @@ const PLUGIN = `// ${PLUGIN_MARKER}
|
|
|
78
78
|
// rsc update check for OpenCode: checked once per OpenCode run, and the notice goes to the model to be
|
|
79
79
|
// relayed. It rides on EVERY system prompt of the run, not just the first: OpenCode's first call is
|
|
80
80
|
// its title generator, which would swallow a once-only notice before the agent ever sees it.
|
|
81
|
+
// One file for both plugin APIs (issue #289): V1 calls the named export or server(); V2 reads id + setup.
|
|
81
82
|
// Turn auto-update off with .rsc/.no-auto-update.
|
|
82
83
|
import { updateNotice } from '../../.rsc/auto-update.mjs';
|
|
83
84
|
|
|
85
|
+
const NOTE = '\\nMention this only in your first reply of the session.';
|
|
86
|
+
const check = (cwd) => (process.env.RSC_NO_UPDATE_CHECK ? Promise.resolve('') : updateNotice(cwd).catch(() => ''));
|
|
87
|
+
|
|
84
88
|
export const RscUpdatePlugin = async ({ directory, worktree }) => {
|
|
85
|
-
const
|
|
86
|
-
const pending = process.env.RSC_NO_UPDATE_CHECK ? Promise.resolve('') : updateNotice(cwd).catch(() => '');
|
|
89
|
+
const pending = check(worktree || directory);
|
|
87
90
|
return {
|
|
88
91
|
'experimental.chat.system.transform': async (_input, output) => {
|
|
89
92
|
const said = (await pending).trim();
|
|
90
|
-
if (said && Array.isArray(output?.system)) output.system.push(said +
|
|
93
|
+
if (said && Array.isArray(output?.system)) output.system.push(said + NOTE);
|
|
91
94
|
},
|
|
92
95
|
};
|
|
93
96
|
};
|
|
97
|
+
|
|
98
|
+
export default {
|
|
99
|
+
id: 'rsc.update',
|
|
100
|
+
server: RscUpdatePlugin,
|
|
101
|
+
async setup(ctx) {
|
|
102
|
+
const pending = check(ctx.location?.project?.directory || ctx.location?.directory || process.cwd());
|
|
103
|
+
await ctx.session.hook('context', async (event) => {
|
|
104
|
+
const said = (await pending).trim();
|
|
105
|
+
if (said && Array.isArray(event?.system)) event.system.push({ type: 'text', text: said + NOTE });
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
};
|
|
94
109
|
`;
|
|
95
110
|
|
|
96
111
|
export function updateManagedPaths(target, cwd = process.cwd()) {
|