@dai_ming/plugin-deliverables 1.1.9 → 1.2.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/index.js CHANGED
@@ -308,28 +308,28 @@ function shouldIgnorePath(candidatePath) {
308
308
  return IGNORED_FILE_NAMES.has(path.basename(normalized));
309
309
  }
310
310
 
311
- function safeStat(candidatePath) {
311
+ async function safeStat(candidatePath) {
312
312
  try {
313
- return fs.statSync(candidatePath);
313
+ return await fs.promises.stat(candidatePath);
314
314
  } catch (_err) {
315
315
  return null;
316
316
  }
317
317
  }
318
318
 
319
- function workspaceRoots() {
319
+ async function workspaceRoots() {
320
320
  const roots = [];
321
- ["/home/node/.openclaw/workspace-main", "/home/node/.openclaw/workspace"].forEach((root) => {
322
- if (safeStat(root)) {
321
+ for (const root of ["/home/node/.openclaw/workspace-main", "/home/node/.openclaw/workspace"]) {
322
+ if (await safeStat(root)) {
323
323
  roots.push(root);
324
324
  }
325
- });
325
+ }
326
326
  try {
327
- for (const entry of fs.readdirSync("/data")) {
327
+ for (const entry of await fs.promises.readdir("/data")) {
328
328
  if (!entry || entry.indexOf("workspace-") !== 0) {
329
329
  continue;
330
330
  }
331
331
  const root = path.join("/data", entry);
332
- if (safeStat(root)) {
332
+ if (await safeStat(root)) {
333
333
  roots.push(root);
334
334
  }
335
335
  }
@@ -338,12 +338,12 @@ function workspaceRoots() {
338
338
  }
339
339
  const openclawRoot = path.resolve(__dirname, "..", "..");
340
340
  try {
341
- for (const entry of fs.readdirSync(openclawRoot)) {
341
+ for (const entry of await fs.promises.readdir(openclawRoot)) {
342
342
  if (!entry || entry.indexOf("workspace") !== 0) {
343
343
  continue;
344
344
  }
345
345
  const root = path.join(openclawRoot, entry);
346
- if (safeStat(root) && roots.indexOf(root) === -1) {
346
+ if ((await safeStat(root)) && roots.indexOf(root) === -1) {
347
347
  roots.push(root);
348
348
  }
349
349
  }
@@ -364,12 +364,12 @@ function resolveCandidatePath(rawPath) {
364
364
  return cleaned;
365
365
  }
366
366
 
367
- function findFileByRelativeOrBaseName(rawRef) {
367
+ async function findFileByRelativeOrBaseName(rawRef) {
368
368
  const cleaned = stripTrailingPathPunctuation(trimString(rawRef)).replace(/^\.\//, "");
369
369
  if (!cleaned || isURLLike(cleaned) || !hasFileExtension(cleaned)) {
370
370
  return "";
371
371
  }
372
- const roots = workspaceRoots();
372
+ const roots = await workspaceRoots();
373
373
  const directCandidates = [];
374
374
  for (const root of roots) {
375
375
  directCandidates.push(path.join(root, cleaned));
@@ -378,7 +378,7 @@ function findFileByRelativeOrBaseName(rawRef) {
378
378
  directCandidates.push(path.join(root, "output", path.basename(cleaned)));
379
379
  }
380
380
  for (const candidate of directCandidates) {
381
- const stat = safeStat(candidate);
381
+ const stat = await safeStat(candidate);
382
382
  if (stat && (stat.isFile() || stat.isDirectory()) && isAllowedWorkspacePath(candidate) && !shouldIgnorePath(candidate)) {
383
383
  return path.resolve(candidate);
384
384
  }
@@ -387,13 +387,13 @@ function findFileByRelativeOrBaseName(rawRef) {
387
387
  const basename = path.basename(cleaned);
388
388
  const found = [];
389
389
  let visited = 0;
390
- function scan(dir, depth) {
390
+ async function scan(dir, depth) {
391
391
  if (depth < 0 || visited > AUTO_UPLOAD_SCAN_MAX_ENTRIES || shouldIgnorePath(dir)) {
392
392
  return;
393
393
  }
394
394
  let entries;
395
395
  try {
396
- entries = fs.readdirSync(dir, { withFileTypes: true });
396
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
397
397
  } catch (_err) {
398
398
  return;
399
399
  }
@@ -407,11 +407,11 @@ function findFileByRelativeOrBaseName(rawRef) {
407
407
  continue;
408
408
  }
409
409
  if (entry.isDirectory()) {
410
- scan(full, depth - 1);
410
+ await scan(full, depth - 1);
411
411
  continue;
412
412
  }
413
413
  if (entry.isFile() && entry.name === basename) {
414
- const stat = safeStat(full);
414
+ const stat = await safeStat(full);
415
415
  if (stat) {
416
416
  found.push({ path: full, mtimeMs: stat.mtimeMs });
417
417
  }
@@ -419,7 +419,7 @@ function findFileByRelativeOrBaseName(rawRef) {
419
419
  }
420
420
  }
421
421
  for (const root of roots) {
422
- scan(root, AUTO_UPLOAD_SCAN_DEPTH);
422
+ await scan(root, AUTO_UPLOAD_SCAN_DEPTH);
423
423
  }
424
424
  found.sort((a, b) => b.mtimeMs - a.mtimeMs);
425
425
  return found.length > 0 ? path.resolve(found[0].path) : "";
@@ -457,16 +457,16 @@ function extractFileReferencesFromText(text) {
457
457
  return refs;
458
458
  }
459
459
 
460
- function resolveFileReference(ref) {
460
+ async function resolveFileReference(ref) {
461
461
  if (!ref || !ref.value) {
462
462
  return null;
463
463
  }
464
464
  const candidate = resolveCandidatePath(ref.value);
465
- const resolved = path.isAbsolute(candidate) ? candidate : findFileByRelativeOrBaseName(candidate);
465
+ const resolved = path.isAbsolute(candidate) ? candidate : await findFileByRelativeOrBaseName(candidate);
466
466
  if (!resolved) {
467
467
  return null;
468
468
  }
469
- const stat = safeStat(resolved);
469
+ const stat = await safeStat(resolved);
470
470
  if (!stat || (!stat.isFile() && !stat.isDirectory())) {
471
471
  return null;
472
472
  }
@@ -483,17 +483,17 @@ function resolveFileReference(ref) {
483
483
  };
484
484
  }
485
485
 
486
- function collectDirectoryFiles(dirPath) {
486
+ async function collectDirectoryFiles(dirPath) {
487
487
  const files = [];
488
488
  let total = 0;
489
489
  let visited = 0;
490
- function scan(current, depth) {
490
+ async function scan(current, depth) {
491
491
  if (depth < 0 || visited > AUTO_UPLOAD_SCAN_MAX_ENTRIES || shouldIgnorePath(current)) {
492
492
  return;
493
493
  }
494
494
  let entries;
495
495
  try {
496
- entries = fs.readdirSync(current, { withFileTypes: true });
496
+ entries = await fs.promises.readdir(current, { withFileTypes: true });
497
497
  } catch (_err) {
498
498
  return;
499
499
  }
@@ -507,26 +507,26 @@ function collectDirectoryFiles(dirPath) {
507
507
  continue;
508
508
  }
509
509
  if (entry.isDirectory()) {
510
- scan(full, depth - 1);
510
+ await scan(full, depth - 1);
511
511
  continue;
512
512
  }
513
513
  if (!entry.isFile()) {
514
514
  continue;
515
515
  }
516
- const stat = safeStat(full);
516
+ const stat = await safeStat(full);
517
517
  if (!stat || total + stat.size > AUTO_UPLOAD_MAX_BYTES) {
518
518
  continue;
519
519
  }
520
520
  total += stat.size;
521
521
  const rel = normalizeSlash(path.relative(dirPath, full));
522
522
  if (isTextLikeFile(full)) {
523
- files.push({ name: rel, contentText: fs.readFileSync(full, "utf8") });
523
+ files.push({ name: rel, contentText: await fs.promises.readFile(full, "utf8") });
524
524
  } else {
525
- files.push({ name: rel, contentBase64: fs.readFileSync(full).toString("base64") });
525
+ files.push({ name: rel, contentBase64: (await fs.promises.readFile(full)).toString("base64") });
526
526
  }
527
527
  }
528
528
  }
529
- scan(dirPath, AUTO_UPLOAD_SCAN_DEPTH);
529
+ await scan(dirPath, AUTO_UPLOAD_SCAN_DEPTH);
530
530
  return files;
531
531
  }
532
532
 
@@ -689,7 +689,7 @@ function rememberPendingFile(event) {
689
689
  });
690
690
  }
691
691
 
692
- function pendingCandidatesForPayload(body) {
692
+ async function pendingCandidatesForPayload(body) {
693
693
  const resourceId = payloadResourceId(body);
694
694
  if (!resourceId) {
695
695
  return [];
@@ -702,7 +702,7 @@ function pendingCandidatesForPayload(body) {
702
702
  continue;
703
703
  }
704
704
  const ref = { raw: entry.path, value: entry.path, kind: "pending" };
705
- const resolved = resolveFileReference(ref);
705
+ const resolved = await resolveFileReference(ref);
706
706
  if (resolved) {
707
707
  out.push(resolved);
708
708
  }
@@ -880,6 +880,7 @@ function kbMultipartUpload(fileBuffers, fields, pathsArray) {
880
880
  "X-API-Key": kbApiKey(),
881
881
  "Content-Length": body.length,
882
882
  },
883
+ timeout: 10000,
883
884
  },
884
885
  (res) => {
885
886
  const chunks = [];
@@ -905,6 +906,9 @@ function kbMultipartUpload(fileBuffers, fields, pathsArray) {
905
906
  });
906
907
  },
907
908
  );
909
+ req.on("timeout", () => {
910
+ req.destroy(new Error("kb upload timeout after 10s"));
911
+ });
908
912
  req.on("error", reject);
909
913
  req.write(body);
910
914
  req.end();
@@ -1151,11 +1155,11 @@ function normalizeNativeToolFiles(files) {
1151
1155
  }));
1152
1156
  }
1153
1157
 
1154
- function buildNativeUploadRequestBody(args) {
1158
+ async function buildNativeUploadRequestBody(args) {
1155
1159
  const rawFilePath = trimString(args && args.file_path);
1156
1160
  let fileCandidate = null;
1157
1161
  if (rawFilePath) {
1158
- fileCandidate = resolveFileReference({ raw: rawFilePath, value: rawFilePath, kind: "tool" });
1162
+ fileCandidate = await resolveFileReference({ raw: rawFilePath, value: rawFilePath, kind: "tool" });
1159
1163
  if (!fileCandidate) {
1160
1164
  throw new Error(`file_path is not readable or not under an OpenClaw workspace: ${rawFilePath}`);
1161
1165
  }
@@ -1164,7 +1168,7 @@ function buildNativeUploadRequestBody(args) {
1164
1168
  const body = {};
1165
1169
 
1166
1170
  if (fileCandidate) {
1167
- const stat = safeStat(fileCandidate.path);
1171
+ const stat = await safeStat(fileCandidate.path);
1168
1172
  const isDirectory = !!(stat && stat.isDirectory());
1169
1173
  const requestedType = trimString(args.type);
1170
1174
  body.type =
@@ -1173,15 +1177,15 @@ function buildNativeUploadRequestBody(args) {
1173
1177
  : requestedType || deliverableTypeForPath(fileCandidate.path, isDirectory);
1174
1178
  body.fileName = normalizeDeliverableFileName(args, fileCandidate.fileName);
1175
1179
  if (isDirectory) {
1176
- const files = collectDirectoryFiles(fileCandidate.path);
1180
+ const files = await collectDirectoryFiles(fileCandidate.path);
1177
1181
  if (files.length === 0) {
1178
1182
  throw new Error(`directory has no uploadable files: ${fileCandidate.fileName}`);
1179
1183
  }
1180
1184
  body.files = files;
1181
1185
  } else if (isTextLikeFile(fileCandidate.path)) {
1182
- body.contentText = fs.readFileSync(fileCandidate.path, "utf8");
1186
+ body.contentText = await fs.promises.readFile(fileCandidate.path, "utf8");
1183
1187
  } else {
1184
- body.contentBase64 = fs.readFileSync(fileCandidate.path).toString("base64");
1188
+ body.contentBase64 = (await fs.promises.readFile(fileCandidate.path)).toString("base64");
1185
1189
  }
1186
1190
  return { body, isDirectory };
1187
1191
  }
@@ -1219,7 +1223,7 @@ function buildNativeUploadRequestBody(args) {
1219
1223
  }
1220
1224
 
1221
1225
  async function uploadDeliverable(args) {
1222
- const { body, isDirectory } = buildNativeUploadRequestBody(args || {});
1226
+ const { body, isDirectory } = await buildNativeUploadRequestBody(args || {});
1223
1227
  const userId = payloadUserId(args) || "default";
1224
1228
  const groupId = payloadGroupId(args) || DEFAULT_GROUP_ID;
1225
1229
  const groupName = extractStringField(args || {}, ["group_name", "groupName", "group_subject", "groupSubject"]);
@@ -1270,7 +1274,7 @@ async function uploadCandidate(candidate, body) {
1270
1274
  return cached.result;
1271
1275
  }
1272
1276
 
1273
- const stat = safeStat(candidate.path);
1277
+ const stat = await safeStat(candidate.path);
1274
1278
  if (!stat) {
1275
1279
  throw new Error(`file no longer exists: ${candidate.fileName}`);
1276
1280
  }
@@ -1282,7 +1286,7 @@ async function uploadCandidate(candidate, body) {
1282
1286
 
1283
1287
  let fileBuffers;
1284
1288
  if (stat.isDirectory()) {
1285
- const files = collectDirectoryFiles(candidate.path);
1289
+ const files = await collectDirectoryFiles(candidate.path);
1286
1290
  if (files.length === 0) {
1287
1291
  throw new Error(`directory has no uploadable files: ${candidate.fileName}`);
1288
1292
  }
@@ -1294,7 +1298,7 @@ async function uploadCandidate(candidate, body) {
1294
1298
  if (stat.size > AUTO_UPLOAD_MAX_BYTES) {
1295
1299
  throw new Error(`file exceeds auto-upload limit: ${candidate.fileName}`);
1296
1300
  }
1297
- fileBuffers = [{ fileName, content: fs.readFileSync(candidate.path) }];
1301
+ fileBuffers = [{ fileName, content: await fs.promises.readFile(candidate.path) }];
1298
1302
  }
1299
1303
 
1300
1304
  const response = await kbMultipartUpload(fileBuffers, {
@@ -1393,8 +1397,8 @@ async function autoUploadAndRewritePayload(body, api) {
1393
1397
  return body;
1394
1398
  }
1395
1399
  const refs = extractFileReferencesFromText(body.content);
1396
- const explicitCandidates = refs.map(resolveFileReference).filter(Boolean);
1397
- const candidates = dedupeCandidates(explicitCandidates.concat(pendingCandidatesForPayload(body)));
1400
+ const explicitCandidates = (await Promise.all(refs.map(resolveFileReference))).filter(Boolean);
1401
+ const candidates = dedupeCandidates(explicitCandidates.concat(await pendingCandidatesForPayload(body)));
1398
1402
  if (candidates.length === 0) {
1399
1403
  return body;
1400
1404
  }
@@ -2290,6 +2294,13 @@ const plugin = {
2290
2294
  name: "Deliverables",
2291
2295
  description: "Upload-first runtime guard for generated file deliverables.",
2292
2296
  register(api) {
2297
+ // Warm the config cache once at startup so the synchronous read in
2298
+ // loadDeliverableConfig() never lands on the message hot path.
2299
+ try {
2300
+ loadDeliverableConfig();
2301
+ } catch (err) {
2302
+ api.logger.warn?.(`[plugin-deliverables] config preload failed: ${err.message}`);
2303
+ }
2293
2304
  installPalzFetchPatch(api);
2294
2305
 
2295
2306
  if (api && typeof api.registerTool === "function") {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-deliverables",
3
- "version": "1.1.9",
3
+ "version": "1.2.1",
4
4
  "npm_package": "@dai_ming/plugin-deliverables",
5
5
  "description": "Deliverables plugin: native upload tool + skill + AGENTS rules for AI-generated file uploads",
6
6
  "skills": {
@@ -2,7 +2,7 @@
2
2
  "id": "plugin-deliverables",
3
3
  "name": "Deliverables",
4
4
  "description": "Deliverables runtime guard for upload-first file delivery with Palz split-send diagnostics.",
5
- "version": "1.1.9",
5
+ "version": "1.2.1",
6
6
  "skills": ["./skills"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dai_ming/plugin-deliverables",
3
- "version": "1.1.9",
3
+ "version": "1.2.1",
4
4
  "description": "OpenClaw deliverables native plugin — upload AI-generated files to OSS and return shareable preview/download links",
5
5
  "keywords": [
6
6
  "openclaw",