@synsci/thesis 0.1.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/LICENSE +21 -0
- package/README.md +46 -0
- package/bin/thesis.mjs +6 -0
- package/package.json +50 -0
- package/skills/thesis/SKILL.md +58 -0
- package/skills/thesis/getting-started/quickstart.md +209 -0
- package/skills/thesis/getting-started/tutorial-overview.md +220 -0
- package/skills/thesis/reference/command-presets.md +273 -0
- package/skills/thesis/reference/experiment-design-protocol.md +200 -0
- package/skills/thesis/reference/thesis-mcp-tool-map.md +201 -0
- package/src/agents.mjs +259 -0
- package/src/cli.mjs +1111 -0
- package/src/mcp-writer.mjs +441 -0
- package/src/setup-auth.mjs +548 -0
- package/src/skill-installer.mjs +467 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
access,
|
|
4
|
+
cp,
|
|
5
|
+
lstat,
|
|
6
|
+
mkdir,
|
|
7
|
+
mkdtemp,
|
|
8
|
+
readFile,
|
|
9
|
+
readdir,
|
|
10
|
+
readlink,
|
|
11
|
+
rm,
|
|
12
|
+
symlink,
|
|
13
|
+
writeFile,
|
|
14
|
+
} from "node:fs/promises";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_SKILL_MCP_SERVER_NAME = "thesis";
|
|
20
|
+
const DEFAULT_SKILL_MCP_SERVER_URL =
|
|
21
|
+
(process.env.THESIS_PUBLIC_BASE_URL || "https://thesis.syntheticsciences.ai") +
|
|
22
|
+
"/mcp-server";
|
|
23
|
+
|
|
24
|
+
export const INSTALL_SKILLS_AGENT_BY_HOST = Object.freeze({
|
|
25
|
+
claude: "claude-code",
|
|
26
|
+
codex: "codex",
|
|
27
|
+
opencode: "opencode",
|
|
28
|
+
cursor: "cursor",
|
|
29
|
+
openclaw: "openclaw",
|
|
30
|
+
"pi-mono": "pi",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const SUPPORTED_INSTALL_SKILL_HOSTS = Object.freeze([
|
|
34
|
+
"codex",
|
|
35
|
+
"claude",
|
|
36
|
+
"opencode",
|
|
37
|
+
"cursor",
|
|
38
|
+
"openclaw",
|
|
39
|
+
"pi-mono",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export function resolvePackageRoot(moduleUrl = import.meta.url) {
|
|
43
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
44
|
+
const moduleDir = path.dirname(modulePath);
|
|
45
|
+
return path.resolve(moduleDir, "..");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function listBundledSkills(packageRoot) {
|
|
49
|
+
let skillEntries;
|
|
50
|
+
try {
|
|
51
|
+
skillEntries = await readdir(path.join(packageRoot, "skills"), {
|
|
52
|
+
withFileTypes: true,
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (
|
|
56
|
+
error &&
|
|
57
|
+
typeof error === "object" &&
|
|
58
|
+
"code" in error &&
|
|
59
|
+
error.code === "ENOENT"
|
|
60
|
+
) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
const bundledSkillNames = [];
|
|
66
|
+
|
|
67
|
+
for (const entry of skillEntries) {
|
|
68
|
+
if (!entry.isDirectory()) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const skillPath = path.join(
|
|
73
|
+
packageRoot,
|
|
74
|
+
"skills",
|
|
75
|
+
entry.name,
|
|
76
|
+
"SKILL.md",
|
|
77
|
+
);
|
|
78
|
+
if (await pathExists(skillPath)) {
|
|
79
|
+
bundledSkillNames.push(entry.name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return bundledSkillNames.sort();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeBundledSkillName(skillName) {
|
|
87
|
+
const normalizedSkillName = String(skillName ?? "").trim();
|
|
88
|
+
if (normalizedSkillName.length === 0) {
|
|
89
|
+
throw new Error("Bundled skill name is required.");
|
|
90
|
+
}
|
|
91
|
+
return normalizedSkillName;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveBundledSkillDir(packageRoot, skillName) {
|
|
95
|
+
return path.join(
|
|
96
|
+
packageRoot,
|
|
97
|
+
"skills",
|
|
98
|
+
normalizeBundledSkillName(skillName),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function resolveSkillsAgentName(hostName) {
|
|
103
|
+
const agentName = INSTALL_SKILLS_AGENT_BY_HOST[hostName];
|
|
104
|
+
if (!agentName) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`--install-skill is not supported for host '${hostName}'.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return agentName;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function formatSupportedInstallSkillHosts() {
|
|
113
|
+
return SUPPORTED_INSTALL_SKILL_HOSTS.join(", ");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function uniqueValues(values) {
|
|
117
|
+
return [...new Set(values)];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeBundledSkillNames(skillNames) {
|
|
121
|
+
if (!Array.isArray(skillNames) || skillNames.length === 0) {
|
|
122
|
+
throw new Error("At least one bundled skill name is required.");
|
|
123
|
+
}
|
|
124
|
+
return uniqueValues(skillNames.map(normalizeBundledSkillName)).sort();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeInstallScope(scope) {
|
|
128
|
+
return scope === "global" ? "global" : "project";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveScopeRoot(projectRoot, scope) {
|
|
132
|
+
return normalizeInstallScope(scope) === "global"
|
|
133
|
+
? os.homedir()
|
|
134
|
+
: projectRoot;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveOpenClawGlobalSkillsRoot(homeDir) {
|
|
138
|
+
if (existsSync(path.join(homeDir, ".openclaw"))) {
|
|
139
|
+
return path.join(homeDir, ".openclaw", "skills");
|
|
140
|
+
}
|
|
141
|
+
if (existsSync(path.join(homeDir, ".clawdbot"))) {
|
|
142
|
+
return path.join(homeDir, ".clawdbot", "skills");
|
|
143
|
+
}
|
|
144
|
+
if (existsSync(path.join(homeDir, ".moltbot"))) {
|
|
145
|
+
return path.join(homeDir, ".moltbot", "skills");
|
|
146
|
+
}
|
|
147
|
+
return path.join(homeDir, ".openclaw", "skills");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveInstalledSkillDirsForHosts(
|
|
151
|
+
projectRoot,
|
|
152
|
+
hostNames = [],
|
|
153
|
+
scope = "project",
|
|
154
|
+
skillName,
|
|
155
|
+
) {
|
|
156
|
+
return uniqueValues(
|
|
157
|
+
hostNames.map((hostName) =>
|
|
158
|
+
resolveInstalledSkillDir(projectRoot, hostName, scope, skillName),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function partitionInstallSkillHosts(hostNames) {
|
|
164
|
+
const supportedHosts = [];
|
|
165
|
+
const unsupportedHosts = [];
|
|
166
|
+
|
|
167
|
+
for (const hostName of uniqueValues(hostNames)) {
|
|
168
|
+
if (hostName in INSTALL_SKILLS_AGENT_BY_HOST) {
|
|
169
|
+
supportedHosts.push(hostName);
|
|
170
|
+
} else {
|
|
171
|
+
unsupportedHosts.push(hostName);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { supportedHosts, unsupportedHosts };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function resolveInstalledSkillDir(
|
|
179
|
+
projectRoot,
|
|
180
|
+
hostName,
|
|
181
|
+
scope = "project",
|
|
182
|
+
skillName,
|
|
183
|
+
) {
|
|
184
|
+
const normalizedScope = normalizeInstallScope(scope);
|
|
185
|
+
const normalizedSkillName = normalizeBundledSkillName(skillName);
|
|
186
|
+
const root = resolveScopeRoot(projectRoot, normalizedScope);
|
|
187
|
+
switch (hostName) {
|
|
188
|
+
case "claude":
|
|
189
|
+
return path.join(root, ".claude", "skills", normalizedSkillName);
|
|
190
|
+
case "codex":
|
|
191
|
+
case "opencode":
|
|
192
|
+
case "cursor":
|
|
193
|
+
return path.join(root, ".agents", "skills", normalizedSkillName);
|
|
194
|
+
case "openclaw": {
|
|
195
|
+
if (normalizedScope === "global") {
|
|
196
|
+
return path.join(
|
|
197
|
+
resolveOpenClawGlobalSkillsRoot(root),
|
|
198
|
+
normalizedSkillName,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return path.join(root, "skills", normalizedSkillName);
|
|
202
|
+
}
|
|
203
|
+
case "pi-mono":
|
|
204
|
+
if (normalizedScope === "global") {
|
|
205
|
+
return path.join(
|
|
206
|
+
root,
|
|
207
|
+
".pi",
|
|
208
|
+
"agent",
|
|
209
|
+
"skills",
|
|
210
|
+
normalizedSkillName,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return path.join(root, ".pi", "skills", normalizedSkillName);
|
|
214
|
+
default:
|
|
215
|
+
throw new Error(
|
|
216
|
+
`--install-skill is not supported for host '${hostName}'.`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resolveProjectSkillsLockPath(projectRoot) {
|
|
222
|
+
return path.join(projectRoot, "skills-lock.json");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function pathExists(targetPath) {
|
|
226
|
+
try {
|
|
227
|
+
await access(targetPath);
|
|
228
|
+
return true;
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function createBackupDirPath(backupRoot) {
|
|
235
|
+
const backupSnapshotRoot = await mkdtemp(
|
|
236
|
+
path.join(backupRoot, "snapshot-"),
|
|
237
|
+
);
|
|
238
|
+
return path.join(backupSnapshotRoot, "artifact");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function inspectInstalledSkillDir({ backupRoot, installedSkillDir }) {
|
|
242
|
+
try {
|
|
243
|
+
const stats = await lstat(installedSkillDir);
|
|
244
|
+
if (stats.isSymbolicLink()) {
|
|
245
|
+
return {
|
|
246
|
+
installedSkillDir,
|
|
247
|
+
hadInstalledSkillDir: true,
|
|
248
|
+
priorArtifactKind: "symlink",
|
|
249
|
+
backupDir: null,
|
|
250
|
+
symlinkTarget: await readlink(installedSkillDir),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (!stats.isDirectory()) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Installed skill path '${installedSkillDir}' must be a directory or symlink.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const backupDir = await createBackupDirPath(backupRoot);
|
|
260
|
+
await cp(installedSkillDir, backupDir, { recursive: true });
|
|
261
|
+
return {
|
|
262
|
+
installedSkillDir,
|
|
263
|
+
hadInstalledSkillDir: true,
|
|
264
|
+
priorArtifactKind: "directory",
|
|
265
|
+
backupDir,
|
|
266
|
+
symlinkTarget: null,
|
|
267
|
+
};
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (
|
|
270
|
+
error &&
|
|
271
|
+
typeof error === "object" &&
|
|
272
|
+
"code" in error &&
|
|
273
|
+
error.code === "ENOENT"
|
|
274
|
+
) {
|
|
275
|
+
return {
|
|
276
|
+
installedSkillDir,
|
|
277
|
+
hadInstalledSkillDir: false,
|
|
278
|
+
priorArtifactKind: null,
|
|
279
|
+
backupDir: null,
|
|
280
|
+
symlinkTarget: null,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function inspectInstalledSkillArtifactsForHosts({
|
|
288
|
+
projectRoot = process.cwd(),
|
|
289
|
+
hostNames = [],
|
|
290
|
+
scope = "project",
|
|
291
|
+
skillNames,
|
|
292
|
+
} = {}) {
|
|
293
|
+
const normalizedScope = normalizeInstallScope(scope);
|
|
294
|
+
const normalizedSkillNames = normalizeBundledSkillNames(skillNames);
|
|
295
|
+
const skillsLockPath =
|
|
296
|
+
normalizedScope === "project"
|
|
297
|
+
? resolveProjectSkillsLockPath(projectRoot)
|
|
298
|
+
: null;
|
|
299
|
+
const hadSkillsLock = skillsLockPath
|
|
300
|
+
? await pathExists(skillsLockPath)
|
|
301
|
+
: false;
|
|
302
|
+
const skillsLockContents =
|
|
303
|
+
hadSkillsLock && skillsLockPath
|
|
304
|
+
? await readFile(skillsLockPath, "utf8").catch(() => null)
|
|
305
|
+
: null;
|
|
306
|
+
const backupRoot = await mkdtemp(
|
|
307
|
+
path.join(os.tmpdir(), "thesis-install-skill-backup-"),
|
|
308
|
+
);
|
|
309
|
+
const skillStates = new Map();
|
|
310
|
+
|
|
311
|
+
for (const skillName of normalizedSkillNames) {
|
|
312
|
+
const installedSkillDirs = resolveInstalledSkillDirsForHosts(
|
|
313
|
+
projectRoot,
|
|
314
|
+
hostNames,
|
|
315
|
+
normalizedScope,
|
|
316
|
+
skillName,
|
|
317
|
+
);
|
|
318
|
+
const skillState = {
|
|
319
|
+
installedSkillDirs: [],
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
for (const installedSkillDir of installedSkillDirs) {
|
|
323
|
+
// eslint-disable-next-line no-await-in-loop
|
|
324
|
+
skillState.installedSkillDirs.push(
|
|
325
|
+
await inspectInstalledSkillDir({
|
|
326
|
+
backupRoot,
|
|
327
|
+
installedSkillDir,
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
skillStates.set(skillName, skillState);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
backupRoot,
|
|
337
|
+
skillStates,
|
|
338
|
+
skillsLockPath,
|
|
339
|
+
hadSkillsLock,
|
|
340
|
+
skillsLockContents,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function installBundledSkillForHosts({
|
|
345
|
+
projectRoot = process.cwd(),
|
|
346
|
+
hostNames = [],
|
|
347
|
+
scope = "project",
|
|
348
|
+
skillNames,
|
|
349
|
+
} = {}) {
|
|
350
|
+
const packageRoot = resolvePackageRoot();
|
|
351
|
+
const normalizedScope = normalizeInstallScope(scope);
|
|
352
|
+
const normalizedSkillNames = normalizeBundledSkillNames(skillNames);
|
|
353
|
+
|
|
354
|
+
for (const skillName of normalizedSkillNames) {
|
|
355
|
+
const bundledSkillDir = resolveBundledSkillDir(packageRoot, skillName);
|
|
356
|
+
if (!(await pathExists(bundledSkillDir))) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`Bundled skill directory not found: ${bundledSkillDir}`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (const hostName of uniqueValues(hostNames)) {
|
|
363
|
+
const destDir = resolveInstalledSkillDir(
|
|
364
|
+
projectRoot,
|
|
365
|
+
hostName,
|
|
366
|
+
normalizedScope,
|
|
367
|
+
skillName,
|
|
368
|
+
);
|
|
369
|
+
// eslint-disable-next-line no-await-in-loop
|
|
370
|
+
await rm(destDir, { force: true, recursive: true });
|
|
371
|
+
// eslint-disable-next-line no-await-in-loop
|
|
372
|
+
await mkdir(path.dirname(destDir), { recursive: true });
|
|
373
|
+
// eslint-disable-next-line no-await-in-loop
|
|
374
|
+
await cp(bundledSkillDir, destDir, { recursive: true });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function cleanupInstalledSkillArtifactsForHosts({
|
|
380
|
+
projectRoot = process.cwd(),
|
|
381
|
+
hostNames = [],
|
|
382
|
+
priorState = null,
|
|
383
|
+
scope = "project",
|
|
384
|
+
skillNames,
|
|
385
|
+
} = {}) {
|
|
386
|
+
const normalizedScope = normalizeInstallScope(scope);
|
|
387
|
+
const normalizedSkillNames = normalizeBundledSkillNames(skillNames);
|
|
388
|
+
const skillStates =
|
|
389
|
+
priorState?.skillStates ??
|
|
390
|
+
new Map(
|
|
391
|
+
normalizedSkillNames.map((skillName) => [
|
|
392
|
+
skillName,
|
|
393
|
+
{
|
|
394
|
+
installedSkillDirs: resolveInstalledSkillDirsForHosts(
|
|
395
|
+
projectRoot,
|
|
396
|
+
hostNames,
|
|
397
|
+
normalizedScope,
|
|
398
|
+
skillName,
|
|
399
|
+
).map((installedSkillDir) => ({
|
|
400
|
+
installedSkillDir,
|
|
401
|
+
hadInstalledSkillDir: false,
|
|
402
|
+
priorArtifactKind: null,
|
|
403
|
+
backupDir: null,
|
|
404
|
+
symlinkTarget: null,
|
|
405
|
+
})),
|
|
406
|
+
},
|
|
407
|
+
]),
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
for (const skillName of normalizedSkillNames) {
|
|
411
|
+
const skillState = skillStates.get(skillName) ?? {
|
|
412
|
+
installedSkillDirs: [],
|
|
413
|
+
};
|
|
414
|
+
for (const installedSkillState of skillState.installedSkillDirs) {
|
|
415
|
+
const {
|
|
416
|
+
installedSkillDir,
|
|
417
|
+
priorArtifactKind,
|
|
418
|
+
backupDir,
|
|
419
|
+
symlinkTarget,
|
|
420
|
+
} = installedSkillState;
|
|
421
|
+
if (priorArtifactKind === "directory") {
|
|
422
|
+
// eslint-disable-next-line no-await-in-loop
|
|
423
|
+
await rm(installedSkillDir, { force: true, recursive: true });
|
|
424
|
+
// eslint-disable-next-line no-await-in-loop
|
|
425
|
+
await mkdir(path.dirname(installedSkillDir), { recursive: true });
|
|
426
|
+
// eslint-disable-next-line no-await-in-loop
|
|
427
|
+
await cp(backupDir, installedSkillDir, { recursive: true });
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (priorArtifactKind === "symlink") {
|
|
431
|
+
// eslint-disable-next-line no-await-in-loop
|
|
432
|
+
await rm(installedSkillDir, { force: true, recursive: true });
|
|
433
|
+
// eslint-disable-next-line no-await-in-loop
|
|
434
|
+
await mkdir(path.dirname(installedSkillDir), { recursive: true });
|
|
435
|
+
// eslint-disable-next-line no-await-in-loop
|
|
436
|
+
await symlink(symlinkTarget, installedSkillDir);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
// eslint-disable-next-line no-await-in-loop
|
|
440
|
+
await rm(installedSkillDir, { force: true, recursive: true });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const removeSkillsLock =
|
|
445
|
+
normalizedScope === "project" &&
|
|
446
|
+
!(priorState?.hadSkillsLock ?? false);
|
|
447
|
+
if (
|
|
448
|
+
normalizedScope === "project" &&
|
|
449
|
+
(priorState?.hadSkillsLock ?? false) &&
|
|
450
|
+
priorState?.skillsLockPath &&
|
|
451
|
+
typeof priorState.skillsLockContents === "string"
|
|
452
|
+
) {
|
|
453
|
+
await writeFile(
|
|
454
|
+
priorState.skillsLockPath,
|
|
455
|
+
priorState.skillsLockContents,
|
|
456
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
457
|
+
);
|
|
458
|
+
} else if (removeSkillsLock && priorState?.skillsLockPath) {
|
|
459
|
+
await rm(priorState.skillsLockPath, { force: true });
|
|
460
|
+
} else if (removeSkillsLock) {
|
|
461
|
+
await rm(resolveProjectSkillsLockPath(projectRoot), { force: true });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (priorState?.backupRoot) {
|
|
465
|
+
await rm(priorState.backupRoot, { force: true, recursive: true });
|
|
466
|
+
}
|
|
467
|
+
}
|