jupytermind 0.3.0 → 0.3.1
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/bin/ai-data-scientist.js +138 -0
- package/package.json +3 -2
package/bin/ai-data-scientist.js
CHANGED
|
@@ -29,6 +29,20 @@ const VENV_PYTHON = path.join(
|
|
|
29
29
|
);
|
|
30
30
|
const PYPROJECT_PATH = path.join(PACKAGE_ROOT, "pyproject.toml");
|
|
31
31
|
const MARKER_PATH = path.join(VENV_DIR, ".ai-data-scientist-setup-complete.json");
|
|
32
|
+
const SKILLS_SOURCE_DIR = path.join(PACKAGE_ROOT, ".github", "skills");
|
|
33
|
+
const SKILL_ALLOWLIST = Object.freeze([
|
|
34
|
+
"ai-data-scientist",
|
|
35
|
+
"ai-chemistry-scientist",
|
|
36
|
+
"ai-genomics-scientist",
|
|
37
|
+
"ai-materials-scientist",
|
|
38
|
+
"ai-structural-biology-scientist",
|
|
39
|
+
"ai-scientist",
|
|
40
|
+
"tech-writer",
|
|
41
|
+
"japanese-prose",
|
|
42
|
+
"presentation-planner",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
class SkillDeployError extends Error {}
|
|
32
46
|
|
|
33
47
|
function log(message) {
|
|
34
48
|
process.stderr.write(`[ai-data-scientist] ${message}\n`);
|
|
@@ -66,6 +80,128 @@ function readMarker() {
|
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
82
|
|
|
83
|
+
function realpathNative(targetPath) {
|
|
84
|
+
return fs.realpathSync.native(targetPath);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isContained(candidateReal, rootReal) {
|
|
88
|
+
return candidateReal === rootReal || candidateReal.startsWith(rootReal + path.sep);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function lstatIfExists(targetPath) {
|
|
92
|
+
try {
|
|
93
|
+
return fs.lstatSync(targetPath);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (err.code === "ENOENT") return null;
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Walks `targetDir` upward to the nearest already-existing ancestor and
|
|
101
|
+
// verifies that ancestor's real path is contained within `cwdReal` before
|
|
102
|
+
// anything beneath it may be created.
|
|
103
|
+
function resolveContainedAncestor(targetDir, cwdReal) {
|
|
104
|
+
let current = targetDir;
|
|
105
|
+
while (!fs.existsSync(current)) {
|
|
106
|
+
const parent = path.dirname(current);
|
|
107
|
+
if (parent === current) {
|
|
108
|
+
throw new SkillDeployError(`No existing ancestor directory found for ${targetDir}`);
|
|
109
|
+
}
|
|
110
|
+
current = parent;
|
|
111
|
+
}
|
|
112
|
+
const existingAncestorReal = realpathNative(current);
|
|
113
|
+
if (!isContained(existingAncestorReal, cwdReal)) {
|
|
114
|
+
throw new SkillDeployError(
|
|
115
|
+
`Refusing to use ${current} as an ancestor of ${targetDir}: it resolves ` +
|
|
116
|
+
`outside the current working directory (${existingAncestorReal})`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const missingComponents =
|
|
120
|
+
targetDir === current
|
|
121
|
+
? []
|
|
122
|
+
: path.relative(current, targetDir).split(path.sep).filter(Boolean);
|
|
123
|
+
return { existingAncestorPath: current, existingAncestorReal, missingComponents };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Creates `targetDir` one path segment at a time, re-verifying containment
|
|
127
|
+
// against `cwdReal` after creating each segment, before descending to the
|
|
128
|
+
// next one. Returns `targetDir`'s own verified real path.
|
|
129
|
+
function ensureContainedDir(targetDir, cwdReal) {
|
|
130
|
+
const { existingAncestorPath, missingComponents } = resolveContainedAncestor(targetDir, cwdReal);
|
|
131
|
+
let currentPath = existingAncestorPath;
|
|
132
|
+
for (const component of missingComponents) {
|
|
133
|
+
currentPath = path.join(currentPath, component);
|
|
134
|
+
fs.mkdirSync(currentPath);
|
|
135
|
+
const currentReal = realpathNative(currentPath);
|
|
136
|
+
if (!isContained(currentReal, cwdReal)) {
|
|
137
|
+
throw new SkillDeployError(
|
|
138
|
+
`Refusing to continue past ${currentPath}: it resolves outside the ` +
|
|
139
|
+
`current working directory (${currentReal})`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return realpathNative(targetDir);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** @id CODE-AIDS-148
|
|
147
|
+
* @implements REQ-AIDS-097
|
|
148
|
+
* @design DES-AIDS-095
|
|
149
|
+
*/
|
|
150
|
+
// Deploys the 9 allowlisted skills from the installed package's
|
|
151
|
+
// `.github/skills/` into `<cwd>/.github/skills/`. Runs a full preflight of
|
|
152
|
+
// all 9 destinations (symlink-identity rejection plus canonical-skills-root
|
|
153
|
+
// containment) before removing or copying any of them, so a failure on a
|
|
154
|
+
// later skill never destroys an earlier, already-verified one.
|
|
155
|
+
function deploySkills() {
|
|
156
|
+
const cwd = process.cwd();
|
|
157
|
+
const cwdReal = realpathNative(cwd);
|
|
158
|
+
const skillsRootReal = ensureContainedDir(path.join(cwd, ".github", "skills"), cwdReal);
|
|
159
|
+
|
|
160
|
+
const plan = [];
|
|
161
|
+
for (const name of SKILL_ALLOWLIST) {
|
|
162
|
+
const destination = path.join(skillsRootReal, name);
|
|
163
|
+
const lst = lstatIfExists(destination);
|
|
164
|
+
if (lst) {
|
|
165
|
+
if (lst.isSymbolicLink()) {
|
|
166
|
+
throw new SkillDeployError(
|
|
167
|
+
`Refusing to deploy skill "${name}": destination ${destination} is itself a symlink`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const destinationReal = realpathNative(destination);
|
|
171
|
+
if (!isContained(destinationReal, skillsRootReal)) {
|
|
172
|
+
throw new SkillDeployError(
|
|
173
|
+
`Refusing to deploy skill "${name}": destination ${destination} resolves ` +
|
|
174
|
+
`outside the skills root (${destinationReal})`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
plan.push({ name, destination });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const { name, destination } of plan) {
|
|
182
|
+
const source = path.join(SKILLS_SOURCE_DIR, name);
|
|
183
|
+
if (fs.existsSync(destination)) {
|
|
184
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
fs.cpSync(source, destination, { recursive: true });
|
|
187
|
+
log(`Deployed skill "${name}" to ${destination}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** @id CODE-AIDS-149
|
|
192
|
+
* @implements REQ-AIDS-097
|
|
193
|
+
* @design DES-AIDS-095
|
|
194
|
+
*/
|
|
195
|
+
// Verifies the application Python modules the 9 deployed skills' documented
|
|
196
|
+
// entry points depend on are importable from the bootstrap venv.
|
|
197
|
+
function verifySkillPythonModules() {
|
|
198
|
+
run(VENV_PYTHON, [
|
|
199
|
+
"-c",
|
|
200
|
+
"import ai_data_scientist, ai_chemistry_scientist, ai_genomics_scientist, " +
|
|
201
|
+
"ai_materials_scientist, ai_structural_biology_scientist, ai_scientist",
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
|
|
69
205
|
function isSetupCurrent() {
|
|
70
206
|
if (!fs.existsSync(VENV_PYTHON)) return false;
|
|
71
207
|
const marker = readMarker();
|
|
@@ -102,6 +238,8 @@ function main() {
|
|
|
102
238
|
ensureSetup();
|
|
103
239
|
|
|
104
240
|
if (!command || command === "setup") {
|
|
241
|
+
deploySkills();
|
|
242
|
+
verifySkillPythonModules();
|
|
105
243
|
log("Environment ready (環境は準備済みです).");
|
|
106
244
|
return;
|
|
107
245
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupytermind",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "GitHub Copilot Agent Skill: AI Data Scientist over Jupyter MCP (npm bootstrap for the Python implementation)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ai-data-scientist": "bin/ai-data-scientist.js"
|
|
7
|
+
"ai-data-scientist": "bin/ai-data-scientist.js",
|
|
8
|
+
"jupytermind": "bin/ai-data-scientist.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"bin",
|