sinapse-ai 1.25.2 → 1.26.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/.agents/skills/react-bits-frontend/SKILL.md +13 -4
- package/.claude/skill-manifest.json +1 -0
- package/.claude/skills/react-bits-frontend/SKILL.md +75 -0
- package/.codex/scripts/sync-codex-native.js +12 -0
- package/.sinapse-ai/data/entity-registry.yaml +39 -14
- package/.sinapse-ai/install-manifest.yaml +3 -3
- package/CHANGELOG.md +8 -7
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/bin/commands/install.js +25 -4
- package/bin/commands/uninstall.js +1 -1
- package/bin/commands/update.js +19 -3
- package/bin/lib/command-generator.js +60 -36
- package/bin/lib/global-provider-adapters.js +257 -50
- package/bin/lib/provider-contract.js +25 -0
- package/bin/lib/provider-parity.js +67 -10
- package/docs/guides/ide-integration.md +1 -1
- package/package.json +1 -1
- package/packages/installer/src/installer/react-bits-corpus.js +90 -0
- package/packages/installer/src/installer/sinapse-ai-installer.js +9 -0
- package/scripts/sync-provider-adapters.js +7 -2
- package/scripts/validate-provider-adapters.js +21 -1
- package/squads/squad-animations/agents/animations-orqx.md +7 -0
- package/squads/squad-animations/squad.yaml +1 -1
|
@@ -2,17 +2,74 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const {
|
|
7
|
+
MASTER_ALIAS_ENTRY_POINTS,
|
|
8
|
+
GLOBAL_PROVIDER_SKILL_IDS,
|
|
9
|
+
GLOBAL_SUPPLEMENTAL_PROVIDER_SKILL_IDS,
|
|
10
|
+
SUPREME_ORCHESTRATOR_ID,
|
|
11
|
+
SUPREME_PUBLIC_ID,
|
|
12
|
+
} = require('./provider-contract');
|
|
5
13
|
|
|
6
14
|
const NOFOLLOW_READ_FLAGS = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
|
|
7
15
|
|
|
16
|
+
function isPathInside(root, candidate) {
|
|
17
|
+
const relative = path.relative(root, candidate);
|
|
18
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureSafeDirectoryWithinHome(home, directory) {
|
|
22
|
+
const absoluteHome = path.resolve(home);
|
|
23
|
+
const absoluteDirectory = path.resolve(directory);
|
|
24
|
+
if (!isPathInside(absoluteHome, absoluteDirectory)) {
|
|
25
|
+
throw new Error(`Refusing provider write outside HOME: ${absoluteDirectory}`);
|
|
26
|
+
}
|
|
27
|
+
fs.mkdirSync(absoluteHome, { recursive: true });
|
|
28
|
+
const homeStat = fs.lstatSync(absoluteHome);
|
|
29
|
+
if (homeStat.isSymbolicLink() || !homeStat.isDirectory()) {
|
|
30
|
+
throw new Error(`Refusing provider write through unsafe HOME: ${absoluteHome}`);
|
|
31
|
+
}
|
|
32
|
+
const realHome = fs.realpathSync.native(absoluteHome);
|
|
33
|
+
let current = absoluteHome;
|
|
34
|
+
const relative = path.relative(absoluteHome, absoluteDirectory);
|
|
35
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
36
|
+
current = path.join(current, segment);
|
|
37
|
+
if (!fs.existsSync(current)) fs.mkdirSync(current);
|
|
38
|
+
const stat = fs.lstatSync(current);
|
|
39
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
40
|
+
throw new Error(`Refusing provider write through link/reparse path: ${current}`);
|
|
41
|
+
}
|
|
42
|
+
const realCurrent = fs.realpathSync.native(current);
|
|
43
|
+
if (!isPathInside(realHome, realCurrent)) {
|
|
44
|
+
throw new Error(`Refusing provider write outside HOME through reparse path: ${current}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return absoluteDirectory;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assertSafeFileWithinHome(home, filePath) {
|
|
51
|
+
ensureSafeDirectoryWithinHome(home, path.dirname(filePath));
|
|
52
|
+
if (fs.existsSync(filePath)) {
|
|
53
|
+
const stat = fs.lstatSync(filePath);
|
|
54
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
55
|
+
throw new Error(`Refusing provider write through unsafe file: ${filePath}`);
|
|
56
|
+
}
|
|
57
|
+
return stat;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
8
62
|
function readRegularFileNoFollowSync(filePath, encoding = null) {
|
|
9
63
|
let handle;
|
|
10
64
|
try {
|
|
11
65
|
handle = fs.openSync(filePath, NOFOLLOW_READ_FLAGS);
|
|
12
|
-
|
|
66
|
+
const opened = fs.fstatSync(handle);
|
|
67
|
+
const after = fs.lstatSync(filePath);
|
|
68
|
+
if (!opened.isFile() || after.isSymbolicLink() || !after.isFile()) return null;
|
|
69
|
+
if (after.dev !== opened.dev || after.ino !== opened.ino) return null;
|
|
13
70
|
return fs.readFileSync(handle, encoding || undefined);
|
|
14
71
|
} catch (error) {
|
|
15
|
-
if (['ENOENT', 'ELOOP', 'EISDIR'].includes(error.code)) return null;
|
|
72
|
+
if (['ENOENT', 'ELOOP', 'EISDIR', 'EINVAL', 'UNKNOWN'].includes(error.code)) return null;
|
|
16
73
|
throw error;
|
|
17
74
|
} finally {
|
|
18
75
|
if (handle !== undefined) fs.closeSync(handle);
|
|
@@ -36,14 +93,21 @@ function renderCodexToml(name, description, instructions) {
|
|
|
36
93
|
|
|
37
94
|
function markGlobalAgent(markdown, fileName = '') {
|
|
38
95
|
let content = markdown;
|
|
39
|
-
if (fileName ===
|
|
40
|
-
content = content.replace(
|
|
96
|
+
if (fileName === `${SUPREME_ORCHESTRATOR_ID}.md`) {
|
|
97
|
+
content = content.replace(
|
|
98
|
+
new RegExp(`^name:\\s*${SUPREME_ORCHESTRATOR_ID}\\s*$`, 'm'),
|
|
99
|
+
`name: ${SUPREME_PUBLIC_ID}`,
|
|
100
|
+
);
|
|
41
101
|
}
|
|
42
102
|
if (content.includes('SINAPSE-MANAGED:global-agent')) return content;
|
|
43
103
|
return `${content.trimEnd()}\n\n<!-- SINAPSE-MANAGED:global-agent -->\n`;
|
|
44
104
|
}
|
|
45
105
|
|
|
46
|
-
function renderGlobalSkill(skillId) {
|
|
106
|
+
function renderGlobalSkill(skillId, sourceContent = null) {
|
|
107
|
+
if (skillId === 'react-bits-frontend') {
|
|
108
|
+
if (!sourceContent) throw new Error('React Bits global skill source is required');
|
|
109
|
+
return `${String(sourceContent).trimEnd()}\n\n<!-- SINAPSE-MANAGED:global-skill -->\n`;
|
|
110
|
+
}
|
|
47
111
|
const generic = skillId === 'sinapse-agent';
|
|
48
112
|
return [
|
|
49
113
|
'---',
|
|
@@ -54,137 +118,280 @@ function renderGlobalSkill(skillId) {
|
|
|
54
118
|
generic ? '# SINAPSE Global Agent Activator' : '# SINAPSE Global Orchestrator', '',
|
|
55
119
|
generic
|
|
56
120
|
? 'Resolve the requested ID through the native agent catalog of the active provider; reject unknown IDs without guessing.'
|
|
57
|
-
:
|
|
121
|
+
: `Activate the public adapter \`${SUPREME_PUBLIC_ID}\`, backed by canonical source ID \`${SUPREME_ORCHESTRATOR_ID}\`, and preserve its delegation-only authority.`,
|
|
58
122
|
'Canonical global persona and task sources live under `~/.sinapse/`.',
|
|
59
123
|
'Use native Codex delegation and never start a nested CLI.', '',
|
|
60
124
|
].join('\n');
|
|
61
125
|
}
|
|
62
126
|
|
|
63
|
-
function
|
|
64
|
-
|
|
127
|
+
function captureParentBinding(home, parentPath) {
|
|
128
|
+
ensureSafeDirectoryWithinHome(home, parentPath);
|
|
129
|
+
const handle = fs.openSync(parentPath, fs.constants.O_RDONLY);
|
|
130
|
+
const opened = fs.fstatSync(handle);
|
|
131
|
+
const current = fs.lstatSync(parentPath);
|
|
132
|
+
if (!opened.isDirectory() || current.isSymbolicLink() || !current.isDirectory()
|
|
133
|
+
|| opened.dev !== current.dev || opened.ino !== current.ino) {
|
|
134
|
+
fs.closeSync(handle);
|
|
135
|
+
throw new Error(`Refusing provider write through unstable parent: ${parentPath}`);
|
|
136
|
+
}
|
|
137
|
+
return { handle, dev: opened.dev, ino: opened.ino, realPath: fs.realpathSync.native(parentPath) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function assertParentBinding(home, parentPath, binding) {
|
|
141
|
+
ensureSafeDirectoryWithinHome(home, parentPath);
|
|
142
|
+
const current = fs.lstatSync(parentPath);
|
|
143
|
+
const opened = fs.fstatSync(binding.handle);
|
|
144
|
+
const realPath = fs.realpathSync.native(parentPath);
|
|
145
|
+
if (current.isSymbolicLink() || !current.isDirectory() || !opened.isDirectory()
|
|
146
|
+
|| current.dev !== binding.dev || current.ino !== binding.ino
|
|
147
|
+
|| opened.dev !== binding.dev || opened.ino !== binding.ino
|
|
148
|
+
|| realPath !== binding.realPath) {
|
|
149
|
+
throw new Error(`Refusing provider publish after parent path changed: ${parentPath}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function unlinkFileWithBinding(home, filePath, binding, expected, beforeDelete) {
|
|
154
|
+
if (typeof beforeDelete === 'function') beforeDelete({ filePath, parentPath: path.dirname(filePath) });
|
|
155
|
+
assertParentBinding(home, path.dirname(filePath), binding);
|
|
156
|
+
let current;
|
|
157
|
+
try {
|
|
158
|
+
current = fs.lstatSync(filePath);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error.code === 'ENOENT') return false;
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
if (current.isSymbolicLink() || !current.isFile()
|
|
164
|
+
|| current.dev !== expected.dev || current.ino !== expected.ino) {
|
|
165
|
+
throw new Error(`Refusing provider delete after file identity changed: ${filePath}`);
|
|
166
|
+
}
|
|
167
|
+
fs.unlinkSync(filePath);
|
|
168
|
+
assertParentBinding(home, path.dirname(filePath), binding);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function writeFileAtomically(filePath, content, home = path.dirname(filePath), options = {}) {
|
|
173
|
+
const destinationIdentity = assertSafeFileWithinHome(home, filePath);
|
|
174
|
+
const parentPath = path.dirname(filePath);
|
|
175
|
+
const binding = captureParentBinding(home, parentPath);
|
|
176
|
+
const temporaryPath = path.join(
|
|
177
|
+
parentPath,
|
|
178
|
+
`.${path.basename(filePath)}.${crypto.randomBytes(24).toString('hex')}.tmp`,
|
|
179
|
+
);
|
|
65
180
|
let handle;
|
|
181
|
+
let temporaryIdentity;
|
|
182
|
+
let temporaryExists = false;
|
|
66
183
|
try {
|
|
67
184
|
handle = fs.openSync(temporaryPath, 'wx', 0o600);
|
|
185
|
+
temporaryIdentity = fs.fstatSync(handle);
|
|
186
|
+
temporaryExists = true;
|
|
68
187
|
fs.writeFileSync(handle, content, 'utf8');
|
|
69
188
|
fs.fsyncSync(handle);
|
|
70
189
|
fs.closeSync(handle);
|
|
71
190
|
handle = undefined;
|
|
72
|
-
|
|
191
|
+
if (typeof options.beforePublish === 'function') {
|
|
192
|
+
options.beforePublish({ filePath, parentPath, temporaryPath });
|
|
193
|
+
}
|
|
194
|
+
assertParentBinding(home, parentPath, binding);
|
|
195
|
+
if (options.exclusive) {
|
|
196
|
+
fs.linkSync(temporaryPath, filePath);
|
|
197
|
+
assertParentBinding(home, parentPath, binding);
|
|
198
|
+
unlinkFileWithBinding(home, temporaryPath, binding, temporaryIdentity, options.beforeTempCleanup);
|
|
199
|
+
temporaryExists = false;
|
|
200
|
+
} else {
|
|
201
|
+
const currentDestination = assertSafeFileWithinHome(home, filePath);
|
|
202
|
+
if (destinationIdentity && (!currentDestination
|
|
203
|
+
|| currentDestination.dev !== destinationIdentity.dev
|
|
204
|
+
|| currentDestination.ino !== destinationIdentity.ino)) {
|
|
205
|
+
throw new Error(`Refusing provider overwrite after file identity changed: ${filePath}`);
|
|
206
|
+
}
|
|
207
|
+
if (!destinationIdentity && currentDestination) {
|
|
208
|
+
throw new Error(`Refusing provider overwrite of a newly appeared file: ${filePath}`);
|
|
209
|
+
}
|
|
210
|
+
assertParentBinding(home, parentPath, binding);
|
|
211
|
+
fs.renameSync(temporaryPath, filePath);
|
|
212
|
+
temporaryExists = false;
|
|
213
|
+
}
|
|
214
|
+
assertParentBinding(home, parentPath, binding);
|
|
73
215
|
} finally {
|
|
74
216
|
if (handle !== undefined) fs.closeSync(handle);
|
|
75
|
-
|
|
217
|
+
let parentStillBound = false;
|
|
218
|
+
try {
|
|
219
|
+
assertParentBinding(home, parentPath, binding);
|
|
220
|
+
parentStillBound = true;
|
|
221
|
+
} catch { /* never traverse the temporary pathname after an ancestor swap */ }
|
|
222
|
+
if (parentStillBound && temporaryExists) {
|
|
223
|
+
try {
|
|
224
|
+
unlinkFileWithBinding(home, temporaryPath, binding, temporaryIdentity, options.beforeTempCleanup);
|
|
225
|
+
} catch { /* fail closed: leave an untrusted temporary path untouched */ }
|
|
226
|
+
}
|
|
227
|
+
fs.closeSync(binding.handle);
|
|
76
228
|
}
|
|
77
229
|
}
|
|
78
230
|
|
|
79
|
-
function writeGlobalSkillIfManaged(skillPath, content) {
|
|
80
|
-
|
|
231
|
+
function writeGlobalSkillIfManaged(skillPath, content, home = path.dirname(skillPath), options = {}) {
|
|
232
|
+
ensureSafeDirectoryWithinHome(home, path.dirname(skillPath));
|
|
81
233
|
|
|
82
234
|
let handle;
|
|
83
235
|
try {
|
|
236
|
+
try {
|
|
237
|
+
const existingStat = fs.lstatSync(skillPath);
|
|
238
|
+
if (existingStat.isSymbolicLink() || !existingStat.isFile()) return false;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error.code !== 'ENOENT') throw error;
|
|
241
|
+
}
|
|
84
242
|
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
85
243
|
handle = fs.openSync(skillPath, fs.constants.O_RDONLY | noFollow);
|
|
86
244
|
} catch (error) {
|
|
87
245
|
if (error.code === 'ELOOP') return false;
|
|
88
246
|
if (error.code !== 'ENOENT') throw error;
|
|
89
247
|
try {
|
|
90
|
-
|
|
248
|
+
writeFileAtomically(skillPath, content, home, { ...options, exclusive: true });
|
|
249
|
+
return true;
|
|
91
250
|
} catch (createError) {
|
|
92
|
-
if (createError.code === 'EEXIST') return writeGlobalSkillIfManaged(skillPath, content);
|
|
251
|
+
if (createError.code === 'EEXIST') return writeGlobalSkillIfManaged(skillPath, content, home, options);
|
|
93
252
|
throw createError;
|
|
94
253
|
}
|
|
95
|
-
try {
|
|
96
|
-
fs.writeFileSync(handle, content, 'utf8');
|
|
97
|
-
return true;
|
|
98
|
-
} finally {
|
|
99
|
-
fs.closeSync(handle);
|
|
100
|
-
}
|
|
101
254
|
}
|
|
102
255
|
|
|
103
256
|
try {
|
|
104
|
-
|
|
257
|
+
const opened = fs.fstatSync(handle);
|
|
258
|
+
const current = fs.lstatSync(skillPath);
|
|
259
|
+
if (!opened.isFile() || current.isSymbolicLink() || !current.isFile()) return false;
|
|
260
|
+
if (current.dev !== opened.dev || current.ino !== opened.ino) return false;
|
|
105
261
|
const existing = fs.readFileSync(handle, 'utf8');
|
|
106
262
|
if (!existing.includes('SINAPSE-MANAGED:global-skill')) return false;
|
|
107
263
|
fs.closeSync(handle);
|
|
108
264
|
handle = undefined;
|
|
109
|
-
writeFileAtomically(skillPath, content);
|
|
265
|
+
writeFileAtomically(skillPath, content, home);
|
|
110
266
|
return true;
|
|
111
267
|
} finally {
|
|
112
268
|
if (handle !== undefined) fs.closeSync(handle);
|
|
113
269
|
}
|
|
114
270
|
}
|
|
115
271
|
|
|
116
|
-
function
|
|
272
|
+
function isValidGlobalSkill(skillId, skillPath, sourceContent = null) {
|
|
273
|
+
const content = readRegularFileNoFollowSync(skillPath, 'utf8');
|
|
274
|
+
return content !== null && content === renderGlobalSkill(skillId, sourceContent);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function removeStaleManagedAgents(targetDir, expectedFiles, extension, options = {}) {
|
|
117
278
|
if (!fs.existsSync(targetDir)) return [];
|
|
279
|
+
const home = options.home || path.dirname(targetDir);
|
|
280
|
+
const binding = captureParentBinding(home, targetDir);
|
|
118
281
|
const expected = new Set(expectedFiles);
|
|
119
282
|
const removed = [];
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
283
|
+
try {
|
|
284
|
+
for (const file of fs.readdirSync(targetDir).filter((name) => name.endsWith(extension))) {
|
|
285
|
+
if (expected.has(file)) continue;
|
|
286
|
+
const filePath = path.join(targetDir, file);
|
|
287
|
+
const identity = fs.lstatSync(filePath);
|
|
288
|
+
const content = readRegularFileNoFollowSync(filePath, 'utf8');
|
|
289
|
+
if (content === null) continue;
|
|
290
|
+
const explicitlyManaged = content.includes('SINAPSE-MANAGED:global-agent');
|
|
291
|
+
const legacyManaged = content.includes('ACTIVATION-NOTICE: This command activates an agent from sinapse.')
|
|
292
|
+
&& /\.sinapse[\\/]sinapse[\\/]agents[\\/]/.test(content)
|
|
293
|
+
&& content.includes('Load the squad manifest');
|
|
294
|
+
if (!explicitlyManaged && !legacyManaged) continue;
|
|
295
|
+
if (unlinkFileWithBinding(home, filePath, binding, identity, options.beforeDelete)) removed.push(file);
|
|
296
|
+
}
|
|
297
|
+
} finally {
|
|
298
|
+
fs.closeSync(binding.handle);
|
|
132
299
|
}
|
|
133
300
|
return removed;
|
|
134
301
|
}
|
|
135
302
|
|
|
136
|
-
function
|
|
303
|
+
function removeManagedFileWithBinding(home, filePath, beforeDelete) {
|
|
304
|
+
let identity;
|
|
305
|
+
try {
|
|
306
|
+
identity = fs.lstatSync(filePath);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
if (error.code === 'ENOENT') return false;
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
const content = readRegularFileNoFollowSync(filePath, 'utf8');
|
|
312
|
+
if (content === null || !content.includes('SINAPSE-MANAGED:global-agent')) return false;
|
|
313
|
+
const afterRead = fs.lstatSync(filePath);
|
|
314
|
+
if (afterRead.isSymbolicLink() || !afterRead.isFile()
|
|
315
|
+
|| afterRead.dev !== identity.dev || afterRead.ino !== identity.ino) {
|
|
316
|
+
throw new Error(`Refusing provider delete after file identity changed: ${filePath}`);
|
|
317
|
+
}
|
|
318
|
+
const binding = captureParentBinding(home, path.dirname(filePath));
|
|
319
|
+
try {
|
|
320
|
+
return unlinkFileWithBinding(home, filePath, binding, identity, beforeDelete);
|
|
321
|
+
} finally {
|
|
322
|
+
fs.closeSync(binding.handle);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function deliverGlobalProviderAdapters({ llmChoice, home, commandsDir, reactBitsSkillPath, testHooks = {} }) {
|
|
137
327
|
let commandFiles = fs.existsSync(commandsDir)
|
|
138
328
|
? fs.readdirSync(commandsDir).filter((file) => file.endsWith('.md')).sort()
|
|
139
329
|
: [];
|
|
140
|
-
if (commandFiles.includes(
|
|
141
|
-
const aliasFiles = new Set(
|
|
330
|
+
if (commandFiles.includes(`${SUPREME_ORCHESTRATOR_ID}.md`)) {
|
|
331
|
+
const aliasFiles = new Set(MASTER_ALIAS_ENTRY_POINTS.map((id) => `${id}.md`));
|
|
142
332
|
commandFiles = commandFiles.filter((file) => !aliasFiles.has(file));
|
|
143
333
|
}
|
|
144
|
-
const written = {
|
|
334
|
+
const written = {
|
|
335
|
+
claude: [],
|
|
336
|
+
claudeSkills: [],
|
|
337
|
+
claudeAvailableSkills: [],
|
|
338
|
+
codex: [],
|
|
339
|
+
skills: [],
|
|
340
|
+
availableSkills: [],
|
|
341
|
+
};
|
|
342
|
+
let reactBitsSkillContent = null;
|
|
343
|
+
const providerSkillIds = [...GLOBAL_PROVIDER_SKILL_IDS];
|
|
344
|
+
if (reactBitsSkillPath) {
|
|
345
|
+
reactBitsSkillContent = readRegularFileNoFollowSync(reactBitsSkillPath, 'utf8');
|
|
346
|
+
if (reactBitsSkillContent === null) throw new Error(`React Bits global skill is missing: ${reactBitsSkillPath}`);
|
|
347
|
+
providerSkillIds.push(...GLOBAL_SUPPLEMENTAL_PROVIDER_SKILL_IDS);
|
|
348
|
+
}
|
|
145
349
|
|
|
146
350
|
if (llmChoice === 'claude-code' || llmChoice === 'both') {
|
|
147
351
|
const targetDir = path.join(home, '.claude', 'agents');
|
|
148
|
-
|
|
149
|
-
removeStaleManagedAgents(targetDir, commandFiles, '.md');
|
|
352
|
+
ensureSafeDirectoryWithinHome(home, targetDir);
|
|
353
|
+
removeStaleManagedAgents(targetDir, commandFiles, '.md', { home, beforeDelete: testHooks.beforeStaleAgentDelete });
|
|
150
354
|
for (const file of commandFiles) {
|
|
151
355
|
const markdown = markGlobalAgent(fs.readFileSync(path.join(commandsDir, file), 'utf8'), file);
|
|
152
|
-
|
|
356
|
+
writeFileAtomically(path.join(targetDir, file), markdown, home);
|
|
153
357
|
written.claude.push(file);
|
|
154
358
|
}
|
|
155
359
|
const skillsRoot = path.join(home, '.claude', 'skills');
|
|
156
|
-
for (const skillId of
|
|
360
|
+
for (const skillId of providerSkillIds) {
|
|
157
361
|
const skillPath = path.join(skillsRoot, skillId, 'SKILL.md');
|
|
158
|
-
if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId))) {
|
|
362
|
+
if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId, reactBitsSkillContent), home, { beforePublish: testHooks.beforeSkillPublish })) {
|
|
159
363
|
written.claudeSkills.push(skillId);
|
|
160
364
|
}
|
|
365
|
+
if (isValidGlobalSkill(skillId, skillPath, reactBitsSkillContent)) written.claudeAvailableSkills.push(skillId);
|
|
161
366
|
}
|
|
162
367
|
}
|
|
163
368
|
if (llmChoice === 'codex' || llmChoice === 'both') {
|
|
164
369
|
const targetDir = path.join(home, '.codex', 'agents');
|
|
165
|
-
|
|
370
|
+
ensureSafeDirectoryWithinHome(home, targetDir);
|
|
166
371
|
removeStaleManagedAgents(
|
|
167
372
|
targetDir,
|
|
168
373
|
commandFiles.map((file) => file.replace(/\.md$/, '.toml')),
|
|
169
374
|
'.toml',
|
|
375
|
+
{ home, beforeDelete: testHooks.beforeStaleAgentDelete },
|
|
170
376
|
);
|
|
171
377
|
for (const file of commandFiles) {
|
|
172
378
|
const markdown = markGlobalAgent(fs.readFileSync(path.join(commandsDir, file), 'utf8'), file);
|
|
173
379
|
const id = file.replace(/\.md$/, '');
|
|
174
380
|
const metadata = parseAgentMarkdown(markdown, id);
|
|
175
381
|
const tomlName = `${id}.toml`;
|
|
176
|
-
|
|
382
|
+
writeFileAtomically(path.join(targetDir, tomlName), renderCodexToml(metadata.name, metadata.description, markdown), home);
|
|
177
383
|
const staleMarkdown = path.join(targetDir, file);
|
|
178
|
-
|
|
384
|
+
removeManagedFileWithBinding(home, staleMarkdown, testHooks.beforeStaleMarkdownDelete);
|
|
179
385
|
written.codex.push(tomlName);
|
|
180
386
|
}
|
|
181
387
|
const skillsRoot = path.join(home, '.agents', 'skills');
|
|
182
|
-
for (const skillId of
|
|
388
|
+
for (const skillId of providerSkillIds) {
|
|
183
389
|
const skillDir = path.join(skillsRoot, skillId);
|
|
184
390
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
185
|
-
if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId))) {
|
|
391
|
+
if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId, reactBitsSkillContent), home, { beforePublish: testHooks.beforeSkillPublish })) {
|
|
186
392
|
written.skills.push(skillId);
|
|
187
393
|
}
|
|
394
|
+
if (isValidGlobalSkill(skillId, skillPath, reactBitsSkillContent)) written.availableSkills.push(skillId);
|
|
188
395
|
}
|
|
189
396
|
}
|
|
190
397
|
return written;
|
|
@@ -196,4 +403,4 @@ function getGlobalCommandStagingDir({ llmChoice, sinapseHome, claudeCommandsDir
|
|
|
196
403
|
return path.join(sinapseHome, '.generated', 'agents');
|
|
197
404
|
}
|
|
198
405
|
|
|
199
|
-
module.exports = { parseAgentMarkdown, renderCodexToml, markGlobalAgent, renderGlobalSkill, writeGlobalSkillIfManaged, removeStaleManagedAgents, deliverGlobalProviderAdapters, getGlobalCommandStagingDir };
|
|
406
|
+
module.exports = { parseAgentMarkdown, renderCodexToml, markGlobalAgent, renderGlobalSkill, readRegularFileNoFollowSync, writeFileAtomically, writeGlobalSkillIfManaged, isValidGlobalSkill, ensureSafeDirectoryWithinHome, removeStaleManagedAgents, deliverGlobalProviderAdapters, getGlobalCommandStagingDir };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CANONICAL_AGENT_COUNT = 172;
|
|
4
|
+
const SUPREME_ORCHESTRATOR_ID = 'snps-orqx';
|
|
5
|
+
const SUPREME_PUBLIC_ID = SUPREME_ORCHESTRATOR_ID.replace(/^snps/, 'sinapse');
|
|
6
|
+
const MASTER_ALIAS_ENTRY_POINTS = Object.freeze([
|
|
7
|
+
SUPREME_PUBLIC_ID.replace(/-orqx$/, ''),
|
|
8
|
+
SUPREME_PUBLIC_ID,
|
|
9
|
+
SUPREME_ORCHESTRATOR_ID.replace(/-orqx$/, ''),
|
|
10
|
+
]);
|
|
11
|
+
const GLOBAL_PROVIDER_SKILL_IDS = Object.freeze([
|
|
12
|
+
...MASTER_ALIAS_ENTRY_POINTS,
|
|
13
|
+
SUPREME_ORCHESTRATOR_ID,
|
|
14
|
+
'sinapse-agent',
|
|
15
|
+
]);
|
|
16
|
+
const GLOBAL_SUPPLEMENTAL_PROVIDER_SKILL_IDS = Object.freeze(['react-bits-frontend']);
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
CANONICAL_AGENT_COUNT,
|
|
20
|
+
SUPREME_ORCHESTRATOR_ID,
|
|
21
|
+
SUPREME_PUBLIC_ID,
|
|
22
|
+
MASTER_ALIAS_ENTRY_POINTS,
|
|
23
|
+
GLOBAL_PROVIDER_SKILL_IDS,
|
|
24
|
+
GLOBAL_SUPPLEMENTAL_PROVIDER_SKILL_IDS,
|
|
25
|
+
};
|
|
@@ -1,22 +1,79 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const {
|
|
4
|
+
CANONICAL_AGENT_COUNT,
|
|
5
|
+
GLOBAL_PROVIDER_SKILL_IDS,
|
|
6
|
+
GLOBAL_SUPPLEMENTAL_PROVIDER_SKILL_IDS,
|
|
7
|
+
} = require('./provider-contract');
|
|
8
|
+
|
|
9
|
+
const REQUIRED_GLOBAL_PROVIDER_SKILL_IDS = Object.freeze([
|
|
10
|
+
...GLOBAL_PROVIDER_SKILL_IDS,
|
|
11
|
+
...GLOBAL_SUPPLEMENTAL_PROVIDER_SKILL_IDS,
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function normalizeCanonicalCatalog(canonicalCatalog, extension) {
|
|
15
|
+
if (!canonicalCatalog || typeof canonicalCatalog === 'number') {
|
|
16
|
+
throw new Error('Provider adapter parity requires the canonical agent ID catalog');
|
|
17
|
+
}
|
|
18
|
+
const entries = [...canonicalCatalog].map((entry) => (typeof entry === 'string'
|
|
19
|
+
? { id: entry.replace(/\.md$/, ''), file: entry }
|
|
20
|
+
: entry));
|
|
21
|
+
const sourceIds = entries.map((entry) => entry.id);
|
|
22
|
+
const sourceSet = new Set(sourceIds);
|
|
23
|
+
if (sourceSet.size !== sourceIds.length) {
|
|
24
|
+
const duplicates = [...sourceSet]
|
|
25
|
+
.filter((id) => sourceIds.filter((item) => item === id).length > 1);
|
|
26
|
+
throw new Error(`Canonical agent catalog contains duplicate IDs: ${duplicates.join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
if (entries.length !== CANONICAL_AGENT_COUNT) {
|
|
29
|
+
throw new Error(`Canonical agent catalog must contain exactly ${CANONICAL_AGENT_COUNT} unique IDs, found ${entries.length}`);
|
|
30
|
+
}
|
|
31
|
+
const files = entries.map((entry) => entry.file.replace(/\.md$/, extension));
|
|
32
|
+
return { count: CANONICAL_AGENT_COUNT, files: new Set(files) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function describeCatalogDivergence(provider, actualFiles, canonical) {
|
|
36
|
+
const actual = new Set(actualFiles);
|
|
37
|
+
const problems = [];
|
|
38
|
+
if (actualFiles.length !== canonical.count) {
|
|
39
|
+
problems.push(`${provider}: ${actualFiles.length}/${canonical.count}`);
|
|
40
|
+
}
|
|
41
|
+
if (canonical.files) {
|
|
42
|
+
if (actual.size !== actualFiles.length) {
|
|
43
|
+
const duplicates = [...actual].filter((file) => actualFiles.filter((item) => item === file).length > 1);
|
|
44
|
+
problems.push(`${provider} duplicate: ${duplicates.join(', ')}`);
|
|
45
|
+
}
|
|
46
|
+
const missing = [...canonical.files].filter((file) => !actual.has(file));
|
|
47
|
+
const orphaned = [...actual].filter((file) => !canonical.files.has(file));
|
|
48
|
+
if (missing.length) problems.push(`${provider} missing: ${missing.join(', ')}`);
|
|
49
|
+
if (orphaned.length) problems.push(`${provider} orphaned: ${orphaned.join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
return problems;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertProviderAdapterParity(llmChoice, adapters, canonicalCatalog) {
|
|
4
55
|
if (!['claude-code', 'codex', 'both'].includes(llmChoice)) {
|
|
5
56
|
throw new Error(`Invalid LLM choice for provider parity: ${llmChoice}`);
|
|
6
57
|
}
|
|
7
|
-
const
|
|
58
|
+
const problems = [];
|
|
59
|
+
const claudeCatalog = normalizeCanonicalCatalog(canonicalCatalog, '.md');
|
|
60
|
+
const codexCatalog = normalizeCanonicalCatalog(canonicalCatalog, '.toml');
|
|
8
61
|
if (llmChoice === 'claude-code' || llmChoice === 'both') {
|
|
9
|
-
|
|
62
|
+
problems.push(...describeCatalogDivergence('Claude Code', adapters.claude, claudeCatalog));
|
|
63
|
+
const claudeSkills = new Set(adapters.claudeAvailableSkills || adapters.claudeSkills || []);
|
|
64
|
+
const missingSkills = REQUIRED_GLOBAL_PROVIDER_SKILL_IDS.filter((skillId) => !claudeSkills.has(skillId));
|
|
65
|
+
if (missingSkills.length) problems.push(`Claude Code global skills missing: ${missingSkills.join(', ')}`);
|
|
10
66
|
}
|
|
11
67
|
if (llmChoice === 'codex' || llmChoice === 'both') {
|
|
12
|
-
|
|
68
|
+
problems.push(...describeCatalogDivergence('Codex', adapters.codex, codexCatalog));
|
|
69
|
+
const codexSkills = new Set(adapters.availableSkills || adapters.skills || []);
|
|
70
|
+
const missingSkills = REQUIRED_GLOBAL_PROVIDER_SKILL_IDS.filter((skillId) => !codexSkills.has(skillId));
|
|
71
|
+
if (missingSkills.length) problems.push(`Codex global skills missing: ${missingSkills.join(', ')}`);
|
|
13
72
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const details = divergent.map(([provider, count]) => `${provider}: ${count}/${canonicalCount}`).join(', ');
|
|
17
|
-
throw new Error(`Provider adapter parity failed (${details})`);
|
|
73
|
+
if (problems.length) {
|
|
74
|
+
throw new Error(`Provider adapter parity failed (${problems.join('; ')})`);
|
|
18
75
|
}
|
|
19
|
-
return
|
|
76
|
+
return claudeCatalog.count;
|
|
20
77
|
}
|
|
21
78
|
|
|
22
|
-
module.exports = { assertProviderAdapterParity };
|
|
79
|
+
module.exports = { assertProviderAdapterParity, REQUIRED_GLOBAL_PROVIDER_SKILL_IDS };
|
|
@@ -32,7 +32,7 @@ Both providers expose native lifecycle surfaces. Their registration models diffe
|
|
|
32
32
|
| Measured surface | Claude Code | Codex CLI |
|
|
33
33
|
| --- | --- | --- |
|
|
34
34
|
| Native agents | 172 | 172 |
|
|
35
|
-
| Installed skills |
|
|
35
|
+
| Installed skills | 37 | 37 |
|
|
36
36
|
| Registered hooks | 20 native registrations | 9 lifecycle events through the bridge |
|
|
37
37
|
| Activation | `@agent-name` | `$snps` or `$sinapse-agent <id>` |
|
|
38
38
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const REACT_BITS_CORPUS_RELATIVE_PATH = path.join('docs', 'framework', 'react-bits');
|
|
7
|
+
const REACT_BITS_SKILL_RELATIVE_PATH = path.join('.agents', 'skills', 'react-bits-frontend', 'SKILL.md');
|
|
8
|
+
const REQUIRED_REACT_BITS_CORPUS_FILES = Object.freeze([
|
|
9
|
+
'animations.md',
|
|
10
|
+
'audit-findings.md',
|
|
11
|
+
'backgrounds.md',
|
|
12
|
+
'catalog-summary.json',
|
|
13
|
+
'components.md',
|
|
14
|
+
'implementation-playbook.md',
|
|
15
|
+
'index.md',
|
|
16
|
+
'inventory.json',
|
|
17
|
+
'text-animations.md',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function isRegularFile(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
return fs.lstatSync(filePath).isFile();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error.code === 'ENOENT') return false;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listFilesRecursively(directory, root = directory) {
|
|
30
|
+
if (!fs.existsSync(directory)) return [];
|
|
31
|
+
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
32
|
+
const sourcePath = path.join(directory, entry.name);
|
|
33
|
+
if (entry.isDirectory()) return listFilesRecursively(sourcePath, root);
|
|
34
|
+
return isRegularFile(sourcePath) ? [path.relative(root, sourcePath)] : [];
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getMissingReactBitsCorpusFiles(root) {
|
|
39
|
+
return REQUIRED_REACT_BITS_CORPUS_FILES.filter((file) => !isRegularFile(
|
|
40
|
+
path.join(root, REACT_BITS_CORPUS_RELATIVE_PATH, file),
|
|
41
|
+
));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Copy only the shipped React Bits corpus. Existing destination-only files are
|
|
46
|
+
* intentionally left untouched so a project or global installation never
|
|
47
|
+
* deletes user research alongside the managed corpus.
|
|
48
|
+
*/
|
|
49
|
+
function copyReactBitsCorpusSync(sourceRoot, destinationRoot) {
|
|
50
|
+
const source = path.join(sourceRoot, REACT_BITS_CORPUS_RELATIVE_PATH);
|
|
51
|
+
const destination = path.join(destinationRoot, REACT_BITS_CORPUS_RELATIVE_PATH);
|
|
52
|
+
const missingFiles = getMissingReactBitsCorpusFiles(sourceRoot);
|
|
53
|
+
if (missingFiles.length > 0) {
|
|
54
|
+
throw new Error(`Incomplete React Bits corpus source: ${missingFiles.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
const files = listFilesRecursively(source);
|
|
57
|
+
for (const relativePath of files) {
|
|
58
|
+
const target = path.join(destination, relativePath);
|
|
59
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
60
|
+
fs.copyFileSync(path.join(source, relativePath), target);
|
|
61
|
+
}
|
|
62
|
+
return files.map((relativePath) => path.join(REACT_BITS_CORPUS_RELATIVE_PATH, relativePath));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hasCompleteReactBitsCorpus(root) {
|
|
66
|
+
return getMissingReactBitsCorpusFiles(root).length === 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Copy the corpus plus its canonical skill into a global SINAPSE home. */
|
|
70
|
+
function copyReactBitsCapabilitySync(sourceRoot, destinationRoot) {
|
|
71
|
+
const files = copyReactBitsCorpusSync(sourceRoot, destinationRoot);
|
|
72
|
+
const sourceSkill = path.join(sourceRoot, REACT_BITS_SKILL_RELATIVE_PATH);
|
|
73
|
+
const destinationSkill = path.join(destinationRoot, REACT_BITS_SKILL_RELATIVE_PATH);
|
|
74
|
+
if (!fs.existsSync(sourceSkill)) throw new Error(`Missing React Bits skill: ${sourceSkill}`);
|
|
75
|
+
fs.mkdirSync(path.dirname(destinationSkill), { recursive: true });
|
|
76
|
+
fs.copyFileSync(sourceSkill, destinationSkill);
|
|
77
|
+
return { files, skillPath: destinationSkill };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
REACT_BITS_CORPUS_RELATIVE_PATH,
|
|
82
|
+
REACT_BITS_SKILL_RELATIVE_PATH,
|
|
83
|
+
REQUIRED_REACT_BITS_CORPUS_FILES,
|
|
84
|
+
isRegularFile,
|
|
85
|
+
listFilesRecursively,
|
|
86
|
+
getMissingReactBitsCorpusFiles,
|
|
87
|
+
copyReactBitsCorpusSync,
|
|
88
|
+
copyReactBitsCapabilitySync,
|
|
89
|
+
hasCompleteReactBitsCorpus,
|
|
90
|
+
};
|