@ericrisco/rsc 1.1.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -14
- package/manifest.json +888 -231
- package/package.json +3 -2
- package/schema/ecc-originality.json +16 -0
- package/schema/ecc-originality.u64 +0 -0
- package/scripts/build-manifest.js +43 -3
- package/scripts/doctor.js +47 -1
- package/scripts/install-apply.js +148 -31
- package/scripts/lib/capabilities.js +21 -0
- package/scripts/lib/domains.js +3 -3
- package/scripts/lib/manifest-file.js +4 -1
- package/scripts/originality-check.js +137 -0
- package/scripts/rsc.js +122 -14
- package/skills/design/SKILL.md +3 -3
- package/skills/design/references/design-systems.md +1 -1
- package/skills/design/references/inspiration-sources.md +2 -2
- package/skills/design/references/motion-and-interaction.md +2 -2
- package/skills/design-dna/evals/cases.yaml +1 -1
- package/skills/design-loop/SKILL.md +3 -3
- package/skills/design-loop/evals/cases.yaml +3 -3
- package/skills/expo/SKILL.md +1 -1
- package/skills/motion-craft/SKILL.md +125 -0
- package/skills/motion-craft/evals/cases.yaml +38 -0
- package/skills/motion-craft/scripts/verify.sh +75 -0
- package/skills/react/SKILL.md +2 -2
- package/skills/react-native/SKILL.md +1 -1
- package/skills/swift-ios/SKILL.md +2 -2
- package/skills/ui-engineering/SKILL.md +114 -0
- package/skills/ui-engineering/evals/cases.yaml +38 -0
- package/skills/variant-explorer/SKILL.md +93 -0
- package/skills/variant-explorer/evals/cases.yaml +38 -0
- package/targets/agent-catalog.js +220 -0
- package/targets/agent-sources.json +48 -0
- package/targets/agents.js +61 -13
- package/targets/commands.js +212 -0
- package/targets/memory.js +276 -0
- package/targets/opencode-memory-plugin.js +65 -0
- package/targets/session-memory-adapter.mjs +117 -0
- package/targets/session-memory-core.mjs +477 -0
- package/targets/session-memory.mjs +27 -0
- package/skills/animate/SKILL.md +0 -851
- package/skills/animate/evals/cases.yaml +0 -36
- package/skills/animate/references/recipes.md +0 -324
- package/skills/animate-expo/SKILL.md +0 -281
- package/skills/animate-expo/evals/cases.yaml +0 -36
- package/skills/animate-expo/references/recipes.md +0 -385
- package/skills/animation-vocabulary/SKILL.md +0 -189
- package/skills/animation-vocabulary/evals/cases.yaml +0 -31
- package/skills/apple-design/SKILL.md +0 -301
- package/skills/apple-design/evals/cases.yaml +0 -33
- package/skills/ask-sonner/SKILL.md +0 -104
- package/skills/ask-sonner/evals/cases.yaml +0 -33
- package/skills/ask-sonner/references/api.md +0 -64
- package/skills/design-eng/SKILL.md +0 -88
- package/skills/design-eng/evals/cases.yaml +0 -36
- package/skills/find-animation-opportunities/SKILL.md +0 -150
- package/skills/find-animation-opportunities/evals/cases.yaml +0 -31
- package/skills/improve-animations/SKILL.md +0 -126
- package/skills/improve-animations/evals/cases.yaml +0 -31
- package/skills/improve-animations/references/audit.md +0 -115
- package/skills/improve-animations/references/plan-template.md +0 -73
- package/skills/pick-ui-library/SKILL.md +0 -101
- package/skills/pick-ui-library/evals/cases.yaml +0 -33
- package/skills/pick-ui-library/references/component-galleries.md +0 -59
- package/skills/prototype/SKILL.md +0 -113
- package/skills/prototype/evals/cases.yaml +0 -31
- package/skills/prototype/references/picker.md +0 -197
- package/skills/review-animations/SKILL.md +0 -188
- package/skills/review-animations/evals/cases.yaml +0 -34
- package/skills/review-animations/references/standards.md +0 -187
- package/skills/review-animations/scripts/verify.sh +0 -164
- package/skills/write-swift/SKILL.md +0 -413
- package/skills/write-swift/evals/cases.yaml +0 -36
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync,
|
|
3
|
+
} from 'node:fs';
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
export { handleLifecycle, contextFromNativeOutput } from './session-memory-adapter.mjs';
|
|
8
|
+
|
|
9
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const NEEDLE = '.rsc/session-memory-adapter.mjs';
|
|
11
|
+
const PLUGIN_MARKER = 'rsc-memory:managed';
|
|
12
|
+
|
|
13
|
+
export const MEMORY_TARGETS = Object.freeze({
|
|
14
|
+
claude: 'full',
|
|
15
|
+
codex: 'full',
|
|
16
|
+
// Cursor documents sessionStart as fire-and-forget. Its best-effort hook is useful,
|
|
17
|
+
// but only the always-on rule can make the read-before-action obligation explicit.
|
|
18
|
+
cursor: 'assisted',
|
|
19
|
+
gemini: 'full',
|
|
20
|
+
opencode: 'full',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const CONFIG = Object.freeze({
|
|
24
|
+
claude: '.claude/settings.local.json',
|
|
25
|
+
codex: '.codex/hooks.json',
|
|
26
|
+
cursor: '.cursor/hooks.json',
|
|
27
|
+
gemini: '.gemini/settings.json',
|
|
28
|
+
opencode: '.opencode/plugins/rsc-memory.js',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const memoryModeFor = (target) => MEMORY_TARGETS[target] || 'unsupported';
|
|
32
|
+
|
|
33
|
+
export function memoryEnabledForProject(cwd = process.cwd()) {
|
|
34
|
+
try { return JSON.parse(readFileSync(join(cwd, '.rsc.json'), 'utf8')).memory !== false; } catch { return true; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function git(cwd, args) {
|
|
38
|
+
try { return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return null; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rel(cwd, path) {
|
|
42
|
+
return relative(cwd, path).split(sep).join('/');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function tracked(cwd, path) {
|
|
46
|
+
const output = git(cwd, ['ls-files', '--', rel(cwd, path)]);
|
|
47
|
+
return Boolean(output);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function exclude(cwd, path) {
|
|
51
|
+
if (git(cwd, ['rev-parse', '--is-inside-work-tree']) !== 'true') return;
|
|
52
|
+
const value = git(cwd, ['rev-parse', '--git-path', 'info/exclude']);
|
|
53
|
+
if (!value) return;
|
|
54
|
+
const file = isAbsolute(value) ? value : resolve(cwd, value);
|
|
55
|
+
const pattern = `/${rel(cwd, path)}`;
|
|
56
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
57
|
+
const body = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
58
|
+
if (!body.split('\n').includes(pattern)) appendFileSync(file, `${body && !body.endsWith('\n') ? '\n' : ''}${pattern}\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readConfig(path) {
|
|
62
|
+
if (!existsSync(path)) return {};
|
|
63
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validHookConfig(config) {
|
|
67
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) return false;
|
|
68
|
+
if (!Object.hasOwn(config, 'hooks')) return true;
|
|
69
|
+
if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) return false;
|
|
70
|
+
return Object.values(config.hooks).every(Array.isArray);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function handler(target, event) {
|
|
74
|
+
const unixScript = target === 'claude'
|
|
75
|
+
? '"${CLAUDE_PROJECT_DIR}/.rsc/session-memory-adapter.mjs"'
|
|
76
|
+
: target === 'codex'
|
|
77
|
+
? '"$(git rev-parse --show-toplevel)/.rsc/session-memory-adapter.mjs"'
|
|
78
|
+
: '".rsc/session-memory-adapter.mjs"';
|
|
79
|
+
const value = {
|
|
80
|
+
type: 'command',
|
|
81
|
+
command: `node ${unixScript} ${target} ${event}`,
|
|
82
|
+
timeout: target === 'gemini' ? 5000 : event === 'end' && target === 'codex' ? 3 : 5,
|
|
83
|
+
};
|
|
84
|
+
if (target === 'codex') {
|
|
85
|
+
value.commandWindows = `node \"$((git rev-parse --show-toplevel).Trim())/.rsc/session-memory-adapter.mjs\" ${target} ${event}`;
|
|
86
|
+
value.additionalContextLimit = 1400;
|
|
87
|
+
}
|
|
88
|
+
if (target === 'gemini') value.name = `rsc-memory-${event}`;
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const nestedEvents = {
|
|
93
|
+
claude: [
|
|
94
|
+
['SessionStart', 'start'], ['UserPromptSubmit', 'request'], ['PostToolUse', 'edit', 'Edit|Write|NotebookEdit'],
|
|
95
|
+
['PostToolUse', 'boundary', 'Bash'], ['Stop', 'turn'], ['PreCompact', 'compact'], ['SessionEnd', 'end'],
|
|
96
|
+
],
|
|
97
|
+
codex: [
|
|
98
|
+
['SessionStart', 'start', 'startup|resume|clear|compact'], ['UserPromptSubmit', 'request'],
|
|
99
|
+
['PostToolUse', 'edit', 'apply_patch|Edit|Write'], ['PostToolUse', 'boundary', 'Bash'],
|
|
100
|
+
['Stop', 'turn'], ['PreCompact', 'compact'], ['SessionEnd', 'end'],
|
|
101
|
+
],
|
|
102
|
+
gemini: [
|
|
103
|
+
['SessionStart', 'start'], ['BeforeAgent', 'request'], ['AfterTool', 'edit', 'write_file|replace'],
|
|
104
|
+
['AfterTool', 'boundary', 'run_shell_command'], ['AfterAgent', 'turn'], ['PreCompress', 'compact'], ['SessionEnd', 'end'],
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const cursorEvents = [
|
|
109
|
+
['sessionStart', 'start'], ['beforeSubmitPrompt', 'request'], ['afterFileEdit', 'edit'],
|
|
110
|
+
['afterShellExecution', 'boundary'], ['afterAgentResponse', 'turn'], ['preCompact', 'compact'], ['sessionEnd', 'end'],
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
function stripManaged(config) {
|
|
114
|
+
if (!validHookConfig(config) || !config.hooks) return config;
|
|
115
|
+
for (const event of Object.keys(config.hooks)) {
|
|
116
|
+
config.hooks[event] = (config.hooks[event] || []).filter((entry) => !JSON.stringify(entry).replaceAll('\\\\', '/').includes(NEEDLE));
|
|
117
|
+
if (!config.hooks[event].length) delete config.hooks[event];
|
|
118
|
+
}
|
|
119
|
+
if (!Object.keys(config.hooks).length) delete config.hooks;
|
|
120
|
+
return config;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function writeJsonWiring(target, path, config) {
|
|
124
|
+
stripManaged(config);
|
|
125
|
+
config.hooks ||= {};
|
|
126
|
+
if (target === 'cursor') {
|
|
127
|
+
config.version ||= 1;
|
|
128
|
+
for (const [event, operation] of cursorEvents) {
|
|
129
|
+
config.hooks[event] ||= [];
|
|
130
|
+
const entry = handler(target, operation);
|
|
131
|
+
delete entry.type;
|
|
132
|
+
delete entry.timeout;
|
|
133
|
+
config.hooks[event].push(entry);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
for (const [event, operation, matcher] of nestedEvents[target]) {
|
|
137
|
+
config.hooks[event] ||= [];
|
|
138
|
+
config.hooks[event].push({ ...(matcher ? { matcher } : {}), hooks: [handler(target, operation)] });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
142
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function commonPaths(cwd) {
|
|
146
|
+
return [
|
|
147
|
+
join(cwd, '.rsc', 'session-memory-core.mjs'),
|
|
148
|
+
join(cwd, '.rsc', 'session-memory-adapter.mjs'),
|
|
149
|
+
join(cwd, '.rsc', 'session-memory.mjs'),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function memoryManagedPaths(target, cwd = process.cwd()) {
|
|
154
|
+
if (!MEMORY_TARGETS[target]) return [];
|
|
155
|
+
const paths = [...commonPaths(cwd), join(cwd, ...CONFIG[target].split('/'))];
|
|
156
|
+
if (target === 'cursor') paths.push(join(cwd, '.cursor', 'rules', 'rsc-memory.mdc'));
|
|
157
|
+
return paths;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function wireMemory(target, cwd = process.cwd()) {
|
|
161
|
+
const mode = memoryModeFor(target);
|
|
162
|
+
if (mode === 'unsupported') return { mode, reason: 'no-local-lifecycle', paths: [] };
|
|
163
|
+
if (!memoryEnabledForProject(cwd)) {
|
|
164
|
+
unwireMemory(target, cwd);
|
|
165
|
+
return { mode: 'disabled', reason: 'project-disabled', paths: [] };
|
|
166
|
+
}
|
|
167
|
+
const configPath = join(cwd, ...CONFIG[target].split('/'));
|
|
168
|
+
if (tracked(cwd, configPath)) return { mode: 'degraded', reason: 'config-tracked', paths: [] };
|
|
169
|
+
const config = target === 'opencode' ? null : readConfig(configPath);
|
|
170
|
+
if (target !== 'opencode' && !validHookConfig(config)) return { mode: 'degraded', reason: 'config-invalid', paths: [] };
|
|
171
|
+
if (target === 'opencode' && existsSync(configPath) && !readFileSync(configPath, 'utf8').includes(PLUGIN_MARKER)) {
|
|
172
|
+
return { mode: 'degraded', reason: 'plugin-collision', paths: [] };
|
|
173
|
+
}
|
|
174
|
+
const assistedRule = join(cwd, '.cursor', 'rules', 'rsc-memory.mdc');
|
|
175
|
+
if (target === 'cursor' && tracked(cwd, assistedRule)) return { mode: 'degraded', reason: 'rule-tracked', paths: [] };
|
|
176
|
+
if (target === 'cursor' && existsSync(assistedRule) && !readFileSync(assistedRule, 'utf8').includes('rsc-memory:managed')) {
|
|
177
|
+
return { mode: 'degraded', reason: 'rule-collision', paths: [] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const [core, adapter, cli] = commonPaths(cwd);
|
|
181
|
+
mkdirSync(dirname(core), { recursive: true });
|
|
182
|
+
copyFileSync(join(HERE, 'session-memory-core.mjs'), core);
|
|
183
|
+
copyFileSync(join(HERE, 'session-memory-adapter.mjs'), adapter);
|
|
184
|
+
copyFileSync(join(HERE, 'session-memory.mjs'), cli);
|
|
185
|
+
if (target === 'opencode') {
|
|
186
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
187
|
+
copyFileSync(join(HERE, 'opencode-memory-plugin.js'), configPath);
|
|
188
|
+
} else {
|
|
189
|
+
writeJsonWiring(target, configPath, config);
|
|
190
|
+
}
|
|
191
|
+
if (target === 'cursor') {
|
|
192
|
+
mkdirSync(dirname(assistedRule), { recursive: true });
|
|
193
|
+
writeFileSync(assistedRule, `---\ndescription: Read rsc local continuation before the first action in a new desktop session.\nalwaysApply: true\n---\n<!-- rsc-memory:managed -->\nOn the first turn of a local desktop session, run \`node .rsc/session-memory.mjs resume\` and read its \`context\` field before acting. Skip this rule for background or cloud agents.\n`);
|
|
194
|
+
}
|
|
195
|
+
const paths = memoryManagedPaths(target, cwd);
|
|
196
|
+
for (const path of paths) exclude(cwd, path);
|
|
197
|
+
return {
|
|
198
|
+
mode,
|
|
199
|
+
reason: target === 'codex' ? 'hook-trust-required' : target === 'cursor' ? 'start-hook-fire-and-forget' : 'wired',
|
|
200
|
+
paths,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function inspectMemoryWiring(target, cwd = process.cwd(), recorded = null, installed = false) {
|
|
205
|
+
const supportedMode = memoryModeFor(target);
|
|
206
|
+
if (supportedMode === 'unsupported') return { supportedMode, mode: 'unsupported', status: 'unsupported', reason: 'no-local-lifecycle', missing: [], tracked: [] };
|
|
207
|
+
const mode = recorded?.mode || 'not-installed';
|
|
208
|
+
if (mode === 'disabled' || !memoryEnabledForProject(cwd)) {
|
|
209
|
+
return { supportedMode, mode: 'disabled', status: 'disabled', reason: 'project-disabled', missing: [], tracked: [] };
|
|
210
|
+
}
|
|
211
|
+
// A capability that has never been installed is absent, not broken. Keep the
|
|
212
|
+
// degraded state for pre-memory installs so `sync` can repair those projects.
|
|
213
|
+
if (!recorded && !installed) {
|
|
214
|
+
return { supportedMode, mode, status: 'not-installed', reason: 'not-wired', missing: [], tracked: [] };
|
|
215
|
+
}
|
|
216
|
+
const paths = recorded?.paths?.length ? recorded.paths : memoryManagedPaths(target, cwd);
|
|
217
|
+
const configPath = join(cwd, ...CONFIG[target].split('/'));
|
|
218
|
+
const cursorRule = join(cwd, '.cursor', 'rules', 'rsc-memory.mdc');
|
|
219
|
+
const missing = paths.filter((path) => {
|
|
220
|
+
if (!existsSync(path)) return true;
|
|
221
|
+
if (path === configPath) {
|
|
222
|
+
const body = readFileSync(path, 'utf8').replaceAll('\\', '/');
|
|
223
|
+
return target === 'opencode' ? !body.includes(PLUGIN_MARKER) : !body.includes(NEEDLE);
|
|
224
|
+
}
|
|
225
|
+
if (target === 'cursor' && path === cursorRule) return !readFileSync(path, 'utf8').includes(PLUGIN_MARKER);
|
|
226
|
+
return false;
|
|
227
|
+
});
|
|
228
|
+
const trackedPaths = paths.filter((path) => tracked(cwd, path));
|
|
229
|
+
const status = mode === 'degraded' || mode === 'not-installed' || missing.length || trackedPaths.length ? 'degraded' : 'ready';
|
|
230
|
+
return {
|
|
231
|
+
supportedMode,
|
|
232
|
+
mode,
|
|
233
|
+
status,
|
|
234
|
+
reason: mode === 'degraded' ? (recorded?.reason || 'adapter-degraded')
|
|
235
|
+
: trackedPaths.length ? 'memory-path-tracked'
|
|
236
|
+
: missing.length ? 'memory-path-missing'
|
|
237
|
+
: recorded?.reason || 'wired',
|
|
238
|
+
missing,
|
|
239
|
+
tracked: trackedPaths,
|
|
240
|
+
action: status === 'degraded' ? 'Run `npx @ericrisco/rsc sync`; if a config is tracked, move memory hooks to an untracked local config first.' : null,
|
|
241
|
+
trustRequired: target === 'codex' && status === 'ready',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function unwireMemory(target, cwd = process.cwd()) {
|
|
246
|
+
if (!MEMORY_TARGETS[target]) return [];
|
|
247
|
+
const path = join(cwd, ...CONFIG[target].split('/'));
|
|
248
|
+
const touched = [];
|
|
249
|
+
if (target === 'opencode') {
|
|
250
|
+
if (existsSync(path) && readFileSync(path, 'utf8').includes(PLUGIN_MARKER)) { rmSync(path, { force: true }); touched.push(path); }
|
|
251
|
+
} else {
|
|
252
|
+
const raw = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
253
|
+
const config = raw.replaceAll('\\\\', '/').includes(NEEDLE) ? readConfig(path) : null;
|
|
254
|
+
if (config) { stripManaged(config); writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`); touched.push(path); }
|
|
255
|
+
}
|
|
256
|
+
if (target === 'cursor') {
|
|
257
|
+
const rule = join(cwd, '.cursor', 'rules', 'rsc-memory.mdc');
|
|
258
|
+
if (existsSync(rule) && readFileSync(rule, 'utf8').includes('rsc-memory:managed')) { rmSync(rule, { force: true }); touched.push(rule); }
|
|
259
|
+
}
|
|
260
|
+
return touched;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function memoryArtifactsPresent(target, cwd = process.cwd()) {
|
|
264
|
+
if (!MEMORY_TARGETS[target]) return [];
|
|
265
|
+
const out = commonPaths(cwd).filter(existsSync);
|
|
266
|
+
const config = join(cwd, ...CONFIG[target].split('/'));
|
|
267
|
+
if (existsSync(config)) {
|
|
268
|
+
const body = readFileSync(config, 'utf8').replaceAll('\\\\', '/');
|
|
269
|
+
if (body.includes(NEEDLE) || body.includes(PLUGIN_MARKER)) out.push(config);
|
|
270
|
+
}
|
|
271
|
+
if (target === 'cursor') {
|
|
272
|
+
const rule = join(cwd, '.cursor', 'rules', 'rsc-memory.mdc');
|
|
273
|
+
if (existsSync(rule) && readFileSync(rule, 'utf8').includes('rsc-memory:managed')) out.push(rule);
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// rsc-memory:managed
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { capture, resume } from '../../.rsc/session-memory-core.mjs';
|
|
5
|
+
|
|
6
|
+
function memorySettings(cwd) {
|
|
7
|
+
try {
|
|
8
|
+
const value = JSON.parse(readFileSync(join(cwd, '.rsc.json'), 'utf8')).memory;
|
|
9
|
+
if (value === false) return { enabled: false };
|
|
10
|
+
return value && typeof value === 'object' ? value : {};
|
|
11
|
+
} catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sessionId(input) {
|
|
17
|
+
return input?.sessionID || input?.session_id || input?.event?.properties?.info?.id
|
|
18
|
+
|| input?.event?.properties?.sessionID || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const RscMemoryPlugin = async ({ directory, worktree }) => {
|
|
22
|
+
const cwd = worktree || directory;
|
|
23
|
+
const injected = new Set();
|
|
24
|
+
const local = () => process.env.RSC_REMOTE_AGENT !== '1' && process.env.OPENCODE_REMOTE !== '1';
|
|
25
|
+
const save = (id, event, editDelta = 0) => {
|
|
26
|
+
if (!id || !local()) return;
|
|
27
|
+
try { capture({ cwd, sessionId: id, target: 'opencode', event, editDelta, settings: memorySettings(cwd) }); } catch { /* fail open */ }
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
'experimental.chat.system.transform': async (input, output) => {
|
|
32
|
+
const id = sessionId(input);
|
|
33
|
+
if (!id || injected.has(id) || !local()) return;
|
|
34
|
+
injected.add(id);
|
|
35
|
+
try {
|
|
36
|
+
const config = memorySettings(cwd);
|
|
37
|
+
save(id, 'start');
|
|
38
|
+
const result = resume({ cwd, target: 'opencode', settings: config });
|
|
39
|
+
if (!result.context || !Array.isArray(output?.system)) return;
|
|
40
|
+
if (output.system.length === 0) output.system.push(result.context);
|
|
41
|
+
else output.system[0] = `${output.system[0]}\n\n${result.context}`;
|
|
42
|
+
} catch { /* fail open */ }
|
|
43
|
+
},
|
|
44
|
+
'tool.execute.after': async (input) => {
|
|
45
|
+
const tool = String(input?.tool || '').toLowerCase();
|
|
46
|
+
save(sessionId(input), 'boundary', /write|edit|patch/u.test(tool) ? 1 : 0);
|
|
47
|
+
},
|
|
48
|
+
'experimental.session.compacting': async (input, output) => {
|
|
49
|
+
const id = sessionId(input);
|
|
50
|
+
save(id, 'compact');
|
|
51
|
+
try {
|
|
52
|
+
const result = resume({ cwd, target: 'opencode', settings: memorySettings(cwd) });
|
|
53
|
+
if (result.context && Array.isArray(output?.context)) output.context.push(result.context);
|
|
54
|
+
} catch { /* fail open */ }
|
|
55
|
+
},
|
|
56
|
+
event: async ({ event }) => {
|
|
57
|
+
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');
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve, join } from 'node:path';
|
|
4
|
+
import { capture, resume } from './session-memory-core.mjs';
|
|
5
|
+
|
|
6
|
+
const LOCAL_TARGETS = new Set(['claude', 'codex', 'cursor', 'gemini', 'opencode']);
|
|
7
|
+
|
|
8
|
+
function projectSettings(cwd) {
|
|
9
|
+
try {
|
|
10
|
+
const manifest = JSON.parse(readFileSync(join(cwd, '.rsc.json'), 'utf8'));
|
|
11
|
+
if (manifest.memory === false) return { enabled: false };
|
|
12
|
+
return manifest.memory && typeof manifest.memory === 'object' ? manifest.memory : {};
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sessionId(native, target) {
|
|
19
|
+
return native?.session_id || native?.sessionId || native?.sessionID || native?.conversation_id
|
|
20
|
+
|| native?.thread_id || `${target}-${process.ppid}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isRemote(target, native) {
|
|
24
|
+
if (target === 'cursor' && (native?.is_background_agent === true || native?.isBackgroundAgent === true)) return true;
|
|
25
|
+
return process.env.RSC_REMOTE_AGENT === '1'
|
|
26
|
+
|| process.env.CURSOR_CLOUD_AGENT === '1'
|
|
27
|
+
|| process.env.CODEX_CLOUD_AGENT === '1';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hookEventName(target, event, native) {
|
|
31
|
+
if (native?.hook_event_name) return native.hook_event_name;
|
|
32
|
+
const names = {
|
|
33
|
+
start: target === 'cursor' ? 'sessionStart' : 'SessionStart',
|
|
34
|
+
request: target === 'gemini' ? 'BeforeAgent' : 'UserPromptSubmit',
|
|
35
|
+
edit: target === 'cursor' ? 'afterFileEdit' : target === 'gemini' ? 'AfterTool' : 'PostToolUse',
|
|
36
|
+
boundary: target === 'cursor' ? 'afterShellExecution' : target === 'gemini' ? 'AfterTool' : 'PostToolUse',
|
|
37
|
+
turn: target === 'cursor' ? 'afterAgentResponse' : target === 'gemini' ? 'AfterAgent' : 'Stop',
|
|
38
|
+
compact: target === 'cursor' ? 'preCompact' : target === 'gemini' ? 'PreCompress' : 'PreCompact',
|
|
39
|
+
end: target === 'cursor' ? 'sessionEnd' : 'SessionEnd',
|
|
40
|
+
};
|
|
41
|
+
return names[event] || event;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function nativeOutput(target, eventName, context = '', notice = null, compactionHint = false) {
|
|
45
|
+
const hint = compactionHint ? 'rsc memory: consider compacting at the next phase boundary; nothing was compacted automatically.' : '';
|
|
46
|
+
const extra = [context, hint].filter(Boolean).join('\n');
|
|
47
|
+
if (!extra && !notice) return {};
|
|
48
|
+
if (target === 'cursor') return { ...(extra ? { additional_context: extra } : {}), ...(notice ? { user_message: notice } : {}) };
|
|
49
|
+
if (target === 'opencode') return { context: extra, notice };
|
|
50
|
+
return {
|
|
51
|
+
...(notice ? { systemMessage: notice } : {}),
|
|
52
|
+
...(extra ? { hookSpecificOutput: { hookEventName: eventName, additionalContext: extra } } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function contextFromNativeOutput(target, output = {}) {
|
|
57
|
+
if (target === 'cursor') return output.additional_context || '';
|
|
58
|
+
if (target === 'opencode') return output.context || '';
|
|
59
|
+
return output.hookSpecificOutput?.additionalContext || '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function handleLifecycle({ target, event, native = {}, cwd, settings } = {}) {
|
|
63
|
+
try {
|
|
64
|
+
if (!LOCAL_TARGETS.has(target)) throw new Error(`unsupported memory target: ${target}`);
|
|
65
|
+
if (isRemote(target, native)) return { output: {}, capture: null, remote: true, degraded: false };
|
|
66
|
+
const project = resolve(cwd || native.cwd || process.env.RSC_PROJECT_CWD || process.cwd());
|
|
67
|
+
if (!existsSync(project)) throw new Error('project directory unavailable');
|
|
68
|
+
const config = settings || projectSettings(project);
|
|
69
|
+
if (config.enabled === false) return { output: {}, capture: null, remote: false, degraded: false };
|
|
70
|
+
const id = sessionId(native, target);
|
|
71
|
+
const eventName = hookEventName(target, event, native);
|
|
72
|
+
if (event === 'start') {
|
|
73
|
+
const started = capture({ cwd: project, sessionId: id, target, event: 'start', settings: config });
|
|
74
|
+
const resumed = resume({ cwd: project, target, settings: config });
|
|
75
|
+
return {
|
|
76
|
+
output: nativeOutput(target, eventName, resumed.context),
|
|
77
|
+
capture: started,
|
|
78
|
+
resume: resumed,
|
|
79
|
+
remote: false,
|
|
80
|
+
degraded: false,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const captureInput = {
|
|
84
|
+
cwd: project,
|
|
85
|
+
sessionId: id,
|
|
86
|
+
target,
|
|
87
|
+
event: event === 'end' ? 'sessionEnd' : event,
|
|
88
|
+
editDelta: event === 'edit' ? 1 : 0,
|
|
89
|
+
settings: config,
|
|
90
|
+
};
|
|
91
|
+
if (typeof native.cost === 'number') captureInput.cost = native.cost;
|
|
92
|
+
if (Number.isInteger(native.tool_calls)) captureInput.toolCalls = native.tool_calls;
|
|
93
|
+
const captured = capture(captureInput);
|
|
94
|
+
return {
|
|
95
|
+
output: nativeOutput(target, eventName, '', captured.notice, captured.compactionHint),
|
|
96
|
+
capture: captured,
|
|
97
|
+
remote: false,
|
|
98
|
+
degraded: false,
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return { output: {}, capture: null, remote: false, degraded: true, error: error instanceof Error ? error.message : String(error) };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stdinJson() {
|
|
106
|
+
try {
|
|
107
|
+
const raw = readFileSync(0, 'utf8');
|
|
108
|
+
return raw.trim() ? JSON.parse(raw) : {};
|
|
109
|
+
} catch {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
115
|
+
const result = handleLifecycle({ target: process.argv[2], event: process.argv[3], native: stdinJson() });
|
|
116
|
+
process.stdout.write(`${JSON.stringify(result.output)}\n`);
|
|
117
|
+
}
|