nextclaw 0.9.26 → 0.9.28

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +81 -12
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -199,20 +199,20 @@ async function installMarketplaceSkill(options) {
199
199
  const dirName = options.dir?.trim() || "skills";
200
200
  const destinationDir = isAbsolute(dirName) ? resolve2(dirName, slug) : resolve2(workdir, dirName, slug);
201
201
  const skillFile = join(destinationDir, "SKILL.md");
202
- if (!options.force && existsSync2(destinationDir)) {
203
- if (existsSync2(skillFile)) {
204
- return {
205
- slug,
206
- destinationDir,
207
- alreadyInstalled: true,
208
- source: "marketplace"
209
- };
210
- }
211
- throw new Error(`Skill directory already exists: ${destinationDir} (use --force)`);
212
- }
213
202
  const apiBase = resolveMarketplaceApiBase(options.apiBaseUrl);
214
203
  const item = await fetchMarketplaceSkillItem(apiBase, slug);
215
204
  if (item.install.kind === "builtin") {
205
+ if (!options.force && existsSync2(destinationDir)) {
206
+ if (existsSync2(skillFile)) {
207
+ return {
208
+ slug,
209
+ destinationDir,
210
+ alreadyInstalled: true,
211
+ source: "builtin"
212
+ };
213
+ }
214
+ throw new Error(`Skill directory already exists: ${destinationDir} (use --force)`);
215
+ }
216
216
  if (existsSync2(destinationDir) && options.force) {
217
217
  rmSync2(destinationDir, { recursive: true, force: true });
218
218
  }
@@ -224,6 +224,22 @@ async function installMarketplaceSkill(options) {
224
224
  };
225
225
  }
226
226
  const filesPayload = await fetchMarketplaceSkillFiles(apiBase, slug);
227
+ if (!options.force && existsSync2(destinationDir)) {
228
+ const existingDirState = inspectMarketplaceSkillDirectory(destinationDir, filesPayload.files);
229
+ if (existingDirState === "installed") {
230
+ return {
231
+ slug,
232
+ destinationDir,
233
+ alreadyInstalled: true,
234
+ source: "marketplace"
235
+ };
236
+ }
237
+ if (existingDirState === "recoverable") {
238
+ rmSync2(destinationDir, { recursive: true, force: true });
239
+ } else {
240
+ throw new Error(`Skill directory already exists: ${destinationDir} (use --force)`);
241
+ }
242
+ }
227
243
  if (existsSync2(destinationDir) && options.force) {
228
244
  rmSync2(destinationDir, { recursive: true, force: true });
229
245
  }
@@ -235,7 +251,7 @@ async function installMarketplaceSkill(options) {
235
251
  throw new Error(`Invalid marketplace file path: ${file.path}`);
236
252
  }
237
253
  mkdirSync2(dirname(targetPath), { recursive: true });
238
- const bytes = await fetchMarketplaceSkillFileBlob(apiBase, slug, file);
254
+ const bytes = file.contentBase64 ? decodeMarketplaceFileContent(file.path, file.contentBase64) : await fetchMarketplaceSkillFileBlob(apiBase, slug, file);
239
255
  writeFileSync2(targetPath, bytes);
240
256
  }
241
257
  if (!existsSync2(join(destinationDir, "SKILL.md"))) {
@@ -247,6 +263,49 @@ async function installMarketplaceSkill(options) {
247
263
  source: "marketplace"
248
264
  };
249
265
  }
266
+ function inspectMarketplaceSkillDirectory(destinationDir, files) {
267
+ if (existsSync2(join(destinationDir, "SKILL.md"))) {
268
+ return "installed";
269
+ }
270
+ const discoveredFiles = collectRelativeFiles(destinationDir);
271
+ if (discoveredFiles === null) {
272
+ return "conflict";
273
+ }
274
+ const relevantFiles = discoveredFiles.filter((file) => !isIgnorableMarketplaceResidue(file));
275
+ if (relevantFiles.length === 0) {
276
+ return "recoverable";
277
+ }
278
+ const manifestPaths = new Set(files.map((file) => normalizeMarketplaceRelativePath(file.path)));
279
+ return relevantFiles.every((file) => manifestPaths.has(normalizeMarketplaceRelativePath(file))) ? "recoverable" : "conflict";
280
+ }
281
+ function collectRelativeFiles(rootDir) {
282
+ const output = [];
283
+ const walk = (dir) => {
284
+ const entries = readdirSync(dir, { withFileTypes: true });
285
+ for (const entry of entries) {
286
+ const absolute = join(dir, entry.name);
287
+ if (entry.isDirectory()) {
288
+ if (!walk(absolute)) {
289
+ return false;
290
+ }
291
+ continue;
292
+ }
293
+ if (!entry.isFile()) {
294
+ return false;
295
+ }
296
+ const relativePath = relative(rootDir, absolute);
297
+ output.push(normalizeMarketplaceRelativePath(relativePath));
298
+ }
299
+ return true;
300
+ };
301
+ return walk(rootDir) ? output : null;
302
+ }
303
+ function normalizeMarketplaceRelativePath(path) {
304
+ return path.replace(/\\/g, "/");
305
+ }
306
+ function isIgnorableMarketplaceResidue(path) {
307
+ return path === ".DS_Store";
308
+ }
250
309
  async function publishMarketplaceSkill(options) {
251
310
  const skillDir = resolve2(options.skillDir);
252
311
  if (!existsSync2(skillDir)) {
@@ -376,6 +435,9 @@ async function fetchMarketplaceSkillFiles(apiBase, slug) {
376
435
  if (typeof entry.downloadPath === "string" && entry.downloadPath.trim().length > 0) {
377
436
  normalized.downloadPath = entry.downloadPath.trim();
378
437
  }
438
+ if (typeof entry.contentBase64 === "string" && entry.contentBase64.trim().length > 0) {
439
+ normalized.contentBase64 = entry.contentBase64.trim();
440
+ }
379
441
  return normalized;
380
442
  });
381
443
  return { files };
@@ -394,6 +456,13 @@ async function fetchMarketplaceSkillFileBlob(apiBase, slug, file) {
394
456
  const arrayBuffer = await response.arrayBuffer();
395
457
  return Buffer.from(arrayBuffer);
396
458
  }
459
+ function decodeMarketplaceFileContent(path, contentBase64) {
460
+ const normalized = contentBase64.replace(/\s+/g, "");
461
+ if (!normalized || normalized.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(normalized)) {
462
+ throw new Error(`Invalid marketplace file contentBase64 for path: ${path}`);
463
+ }
464
+ return Buffer.from(normalized, "base64");
465
+ }
397
466
  function resolveSkillFileDownloadUrl(apiBase, slug, file) {
398
467
  const fallback = `${apiBase}/api/v1/skills/items/${encodeURIComponent(slug)}/files/blob?path=${encodeURIComponent(file.path)}`;
399
468
  if (!file.downloadPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextclaw",
3
- "version": "0.9.26",
3
+ "version": "0.9.28",
4
4
  "description": "Lightweight personal AI assistant with CLI, multi-provider routing, and channel integrations.",
5
5
  "private": false,
6
6
  "type": "module",