@ericrisco/rsc 2.1.0 → 3.0.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 +141 -15
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/install-apply.js +21 -0
- package/scripts/rsc.js +67 -1
- package/skills/ftd/SKILL.md +10 -4
- package/skills/ftd/evals/cases.yaml +1 -1
- package/skills/ftd/evals/routing.json +2 -2
- package/skills/init/SKILL.md +10 -1
- package/skills/ship/SKILL.md +6 -0
- package/skills/suggest/SKILL.md +9 -9
- package/skills/worktrees/SKILL.md +1 -0
- package/targets/_md-block.js +7 -1
- package/targets/auto-update.mjs +200 -0
- package/targets/branch-guard.mjs +191 -0
- package/targets/claude.js +22 -3
- package/targets/clone-bootstrap.mjs +1 -1
- package/targets/hook-once.mjs +5 -6
- package/targets/knowledge-sync.mjs +544 -0
- package/targets/knowledge-wiring.js +172 -0
- package/targets/opt-outs.js +4 -0
- package/targets/session-memory-adapter.mjs +20 -2
- package/targets/session-memory-core.mjs +33 -0
- package/targets/session-start.mjs +12 -74
- package/targets/team-safe-start.mjs +115 -0
- package/targets/trunk-policy.mjs +69 -0
- package/targets/update-wiring.js +153 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Wiring for the update check (`auto-update.mjs`). Every assistant gets the module in `.rsc/`; the
|
|
2
|
+
// ones with a session-start event also get a hook that runs it, in the same project-local config
|
|
3
|
+
// files the session memory uses, under a needle of its own so either can be wired or removed
|
|
4
|
+
// without touching the other.
|
|
5
|
+
//
|
|
6
|
+
// Kept apart from `memory.js` on purpose: the memory promises it makes no network request, and a
|
|
7
|
+
// test holds every file it wires to that. The update check exists to ask npm.
|
|
8
|
+
//
|
|
9
|
+
// Claude Code is not in CONFIG: its `session-start.mjs` already imports the module. An assistant
|
|
10
|
+
// with no hook at all is told by the always-on body to run the module on its first turn.
|
|
11
|
+
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const NEEDLE = '.rsc/auto-update.mjs';
|
|
18
|
+
const PLUGIN_MARKER = 'rsc-update:managed';
|
|
19
|
+
|
|
20
|
+
const CONFIG = Object.freeze({
|
|
21
|
+
codex: '.codex/hooks.json',
|
|
22
|
+
cursor: '.cursor/hooks.json',
|
|
23
|
+
gemini: '.gemini/settings.json',
|
|
24
|
+
opencode: '.opencode/plugins/rsc-update.js',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const EVENT = Object.freeze({ codex: 'SessionStart', gemini: 'SessionStart', cursor: 'sessionStart' });
|
|
28
|
+
|
|
29
|
+
/** Assistants where the update runs on its own. Every other one depends on the agent running it. */
|
|
30
|
+
export const UPDATE_HOOK_TARGETS = Object.freeze(['claude', ...Object.keys(CONFIG)].sort());
|
|
31
|
+
|
|
32
|
+
const git = (cwd, args) => {
|
|
33
|
+
try { return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return null; }
|
|
34
|
+
};
|
|
35
|
+
const rel = (cwd, path) => relative(cwd, path).split(sep).join('/');
|
|
36
|
+
const tracked = (cwd, path) => Boolean(git(cwd, ['ls-files', '--', rel(cwd, path)]));
|
|
37
|
+
|
|
38
|
+
function exclude(cwd, path) {
|
|
39
|
+
if (git(cwd, ['rev-parse', '--is-inside-work-tree']) !== 'true') return;
|
|
40
|
+
const value = git(cwd, ['rev-parse', '--git-path', 'info/exclude']);
|
|
41
|
+
if (!value) return;
|
|
42
|
+
const file = isAbsolute(value) ? value : resolve(cwd, value);
|
|
43
|
+
const pattern = `/${rel(cwd, path)}`;
|
|
44
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
45
|
+
const body = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
46
|
+
if (!body.split('\n').includes(pattern)) appendFileSync(file, `${body && !body.endsWith('\n') ? '\n' : ''}${pattern}\n`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readConfig(path) {
|
|
50
|
+
if (!existsSync(path)) return {};
|
|
51
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const validHookConfig = (c) => Boolean(c) && typeof c === 'object' && !Array.isArray(c)
|
|
55
|
+
&& (!Object.hasOwn(c, 'hooks') || (c.hooks && typeof c.hooks === 'object' && !Array.isArray(c.hooks) && Object.values(c.hooks).every(Array.isArray)));
|
|
56
|
+
|
|
57
|
+
function handler(target) {
|
|
58
|
+
const script = target === 'codex'
|
|
59
|
+
? '"$(git rev-parse --show-toplevel)/.rsc/auto-update.mjs"'
|
|
60
|
+
: '".rsc/auto-update.mjs"';
|
|
61
|
+
const value = { type: 'command', command: `node ${script} hook ${target}`, timeout: target === 'gemini' ? 10000 : 10 };
|
|
62
|
+
if (target === 'codex') value.commandWindows = `node "$((git rev-parse --show-toplevel).Trim())/.rsc/auto-update.mjs" hook ${target}`;
|
|
63
|
+
if (target === 'gemini') value.name = 'rsc-update';
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function strip(config) {
|
|
68
|
+
if (!validHookConfig(config) || !config.hooks) return config;
|
|
69
|
+
for (const event of Object.keys(config.hooks)) {
|
|
70
|
+
config.hooks[event] = config.hooks[event].filter((e) => !JSON.stringify(e).replaceAll('\\\\', '/').includes(NEEDLE));
|
|
71
|
+
if (!config.hooks[event].length) delete config.hooks[event];
|
|
72
|
+
}
|
|
73
|
+
if (!Object.keys(config.hooks).length) delete config.hooks;
|
|
74
|
+
return config;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const PLUGIN = `// ${PLUGIN_MARKER}
|
|
78
|
+
// rsc update check for OpenCode: checked once per OpenCode run, and the notice goes to the model to be
|
|
79
|
+
// relayed. It rides on EVERY system prompt of the run, not just the first: OpenCode's first call is
|
|
80
|
+
// its title generator, which would swallow a once-only notice before the agent ever sees it.
|
|
81
|
+
// Turn auto-update off with .rsc/.no-auto-update.
|
|
82
|
+
import { updateNotice } from '../../.rsc/auto-update.mjs';
|
|
83
|
+
|
|
84
|
+
export const RscUpdatePlugin = async ({ directory, worktree }) => {
|
|
85
|
+
const cwd = worktree || directory;
|
|
86
|
+
const pending = process.env.RSC_NO_UPDATE_CHECK ? Promise.resolve('') : updateNotice(cwd).catch(() => '');
|
|
87
|
+
return {
|
|
88
|
+
'experimental.chat.system.transform': async (_input, output) => {
|
|
89
|
+
const said = (await pending).trim();
|
|
90
|
+
if (said && Array.isArray(output?.system)) output.system.push(said + '\\nMention this only in your first reply of the session.');
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
export function updateManagedPaths(target, cwd = process.cwd()) {
|
|
97
|
+
const module = join(cwd, '.rsc', 'auto-update.mjs');
|
|
98
|
+
return CONFIG[target] ? [module, join(cwd, ...CONFIG[target].split('/'))] : [module];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Wire it. Always: being off is the runtime's `.no-auto-update`, and the check itself stays on. */
|
|
102
|
+
export function wireUpdate(target, cwd = process.cwd()) {
|
|
103
|
+
const module = join(cwd, '.rsc', 'auto-update.mjs');
|
|
104
|
+
mkdirSync(dirname(module), { recursive: true });
|
|
105
|
+
copyFileSync(join(HERE, 'auto-update.mjs'), module);
|
|
106
|
+
if (!CONFIG[target]) {
|
|
107
|
+
return { mode: target === 'claude' ? 'hook' : 'agent', paths: [module] };
|
|
108
|
+
}
|
|
109
|
+
const configPath = join(cwd, ...CONFIG[target].split('/'));
|
|
110
|
+
if (tracked(cwd, configPath)) return { mode: 'agent', reason: 'config-tracked', paths: [module] };
|
|
111
|
+
if (target === 'opencode' && existsSync(configPath) && !readFileSync(configPath, 'utf8').includes(PLUGIN_MARKER)) {
|
|
112
|
+
return { mode: 'agent', reason: 'plugin-collision', paths: [module] };
|
|
113
|
+
}
|
|
114
|
+
const config = target === 'opencode' ? null : readConfig(configPath);
|
|
115
|
+
if (target !== 'opencode' && !validHookConfig(config)) return { mode: 'agent', reason: 'config-invalid', paths: [module] };
|
|
116
|
+
|
|
117
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
118
|
+
if (target === 'opencode') writeFileSync(configPath, PLUGIN);
|
|
119
|
+
else {
|
|
120
|
+
strip(config);
|
|
121
|
+
config.hooks ||= {};
|
|
122
|
+
const event = EVENT[target];
|
|
123
|
+
config.hooks[event] ||= [];
|
|
124
|
+
const h = handler(target);
|
|
125
|
+
if (target === 'cursor') { config.version ||= 1; delete h.type; delete h.timeout; config.hooks[event].push(h); }
|
|
126
|
+
else config.hooks[event].push({ hooks: [h] });
|
|
127
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
128
|
+
}
|
|
129
|
+
const paths = updateManagedPaths(target, cwd);
|
|
130
|
+
for (const path of paths) exclude(cwd, path);
|
|
131
|
+
return { mode: 'hook', paths };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function unwireUpdate(target, cwd = process.cwd()) {
|
|
135
|
+
if (!CONFIG[target]) return [];
|
|
136
|
+
const path = join(cwd, ...CONFIG[target].split('/'));
|
|
137
|
+
const touched = [];
|
|
138
|
+
if (target === 'opencode') {
|
|
139
|
+
if (existsSync(path) && readFileSync(path, 'utf8').includes(PLUGIN_MARKER)) { rmSync(path, { force: true }); touched.push(path); }
|
|
140
|
+
} else if (existsSync(path) && readFileSync(path, 'utf8').replaceAll('\\\\', '/').includes(NEEDLE)) {
|
|
141
|
+
const config = readConfig(path);
|
|
142
|
+
if (config) { strip(config); writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`); touched.push(path); }
|
|
143
|
+
}
|
|
144
|
+
return touched;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function updateArtifactsPresent(target, cwd = process.cwd()) {
|
|
148
|
+
if (!CONFIG[target]) return [];
|
|
149
|
+
const path = join(cwd, ...CONFIG[target].split('/'));
|
|
150
|
+
if (!existsSync(path)) return [];
|
|
151
|
+
const body = readFileSync(path, 'utf8').replaceAll('\\\\', '/');
|
|
152
|
+
return body.includes(NEEDLE) || body.includes(PLUGIN_MARKER) ? [path] : [];
|
|
153
|
+
}
|