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.
@@ -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.0",
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",