@ystemsrx/cfshare 0.1.1 → 0.1.3

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/src/cli.ts ADDED
@@ -0,0 +1,484 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { CfshareManager, type CfshareRuntimeApi } from "./manager.js";
8
+ import type { CfsharePluginConfig } from "./types.js";
9
+
10
+ type CliOptions = {
11
+ command?: string;
12
+ paramsJson?: string;
13
+ paramsFile?: string;
14
+ configJson?: string;
15
+ configFile?: string;
16
+ workspaceDir?: string;
17
+ keepAlive?: boolean;
18
+ compact?: boolean;
19
+ help?: boolean;
20
+ version?: boolean;
21
+ };
22
+
23
+ const TOOL_NAMES = new Set([
24
+ "env_check",
25
+ "expose_port",
26
+ "expose_files",
27
+ "exposure_list",
28
+ "exposure_get",
29
+ "exposure_stop",
30
+ "exposure_logs",
31
+ "maintenance",
32
+ "audit_query",
33
+ "audit_export",
34
+ ]);
35
+
36
+ function normalizeCommand(input: string): string {
37
+ return input.trim().toLowerCase().replace(/-/g, "_");
38
+ }
39
+
40
+ function expandHome(input: string): string {
41
+ if (input === "~") {
42
+ return os.homedir();
43
+ }
44
+ if (input.startsWith("~/")) {
45
+ return path.join(os.homedir(), input.slice(2));
46
+ }
47
+ return input;
48
+ }
49
+
50
+ function resolvePathFromCwd(input: string): string {
51
+ const expanded = expandHome(input);
52
+ return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(process.cwd(), expanded);
53
+ }
54
+
55
+ function printHelp() {
56
+ const lines = [
57
+ "CFShare CLI",
58
+ "",
59
+ "Usage:",
60
+ " cfshare <tool> [params-json] [options]",
61
+ "",
62
+ "Tools:",
63
+ " env_check",
64
+ " expose_port",
65
+ " expose_files",
66
+ " exposure_list",
67
+ " exposure_get",
68
+ " exposure_stop",
69
+ " exposure_logs",
70
+ " maintenance",
71
+ " audit_query",
72
+ " audit_export",
73
+ "",
74
+ "Options:",
75
+ " --params <json> Tool parameters as JSON",
76
+ " --params-file <path> Read tool parameters from JSON file",
77
+ " --config <json> Runtime config JSON (same as plugin config)",
78
+ " --config-file <path> Read runtime config from JSON file",
79
+ " --workspace-dir <dir> Workspace dir for expose_files context",
80
+ " --keep-alive Keep process running after expose_*",
81
+ " --no-keep-alive Exit immediately after expose_* result",
82
+ " --compact Compact JSON output",
83
+ " -h, --help Show help",
84
+ " -v, --version Show version",
85
+ "",
86
+ "Examples:",
87
+ " cfshare env_check",
88
+ " cfshare expose_port '{\"port\":3000,\"opts\":{\"access\":\"token\"}}'",
89
+ " cfshare expose_files --params '{\"paths\":[\"./build\"],\"opts\":{\"access\":\"none\"}}'",
90
+ " cfshare exposure_stop --params '{\"id\":\"all\"}'",
91
+ ];
92
+ process.stdout.write(`${lines.join("\n")}\n`);
93
+ }
94
+
95
+ function assertValue(args: string[], index: number, flag: string): string {
96
+ const value = args[index];
97
+ if (!value || value.startsWith("-")) {
98
+ throw new Error(`missing value for ${flag}`);
99
+ }
100
+ return value;
101
+ }
102
+
103
+ function parseArgs(argv: string[]): CliOptions {
104
+ const opts: CliOptions = {};
105
+ const positionals: string[] = [];
106
+
107
+ for (let i = 0; i < argv.length; i += 1) {
108
+ const token = argv[i];
109
+ if (!token) {
110
+ continue;
111
+ }
112
+ if (token === "-h" || token === "--help") {
113
+ opts.help = true;
114
+ continue;
115
+ }
116
+ if (token === "-v" || token === "--version") {
117
+ opts.version = true;
118
+ continue;
119
+ }
120
+ if (token === "--params") {
121
+ opts.paramsJson = assertValue(argv, i + 1, token);
122
+ i += 1;
123
+ continue;
124
+ }
125
+ if (token === "--params-file") {
126
+ opts.paramsFile = assertValue(argv, i + 1, token);
127
+ i += 1;
128
+ continue;
129
+ }
130
+ if (token === "--config") {
131
+ opts.configJson = assertValue(argv, i + 1, token);
132
+ i += 1;
133
+ continue;
134
+ }
135
+ if (token === "--config-file") {
136
+ opts.configFile = assertValue(argv, i + 1, token);
137
+ i += 1;
138
+ continue;
139
+ }
140
+ if (token === "--workspace-dir") {
141
+ opts.workspaceDir = assertValue(argv, i + 1, token);
142
+ i += 1;
143
+ continue;
144
+ }
145
+ if (token === "--keep-alive") {
146
+ opts.keepAlive = true;
147
+ continue;
148
+ }
149
+ if (token === "--no-keep-alive") {
150
+ opts.keepAlive = false;
151
+ continue;
152
+ }
153
+ if (token === "--compact") {
154
+ opts.compact = true;
155
+ continue;
156
+ }
157
+ if (token.startsWith("-")) {
158
+ throw new Error(`unknown option: ${token}`);
159
+ }
160
+ positionals.push(token);
161
+ }
162
+
163
+ if (positionals.length > 0) {
164
+ opts.command = positionals[0];
165
+ }
166
+ if (positionals.length > 1) {
167
+ if (opts.paramsJson || opts.paramsFile) {
168
+ throw new Error("params-json conflicts with --params/--params-file");
169
+ }
170
+ opts.paramsJson = positionals[1];
171
+ }
172
+ if (positionals.length > 2) {
173
+ throw new Error("too many positional arguments");
174
+ }
175
+
176
+ return opts;
177
+ }
178
+
179
+ async function parseJsonInput(source: string, label: string): Promise<unknown> {
180
+ try {
181
+ return JSON.parse(source);
182
+ } catch (error) {
183
+ throw new Error(`failed to parse ${label}: ${String(error)}`);
184
+ }
185
+ }
186
+
187
+ async function parseJsonFile(filePath: string, label: string): Promise<unknown> {
188
+ const resolved = resolvePathFromCwd(filePath);
189
+ const content = await fs.readFile(resolved, "utf8");
190
+ return await parseJsonInput(content, `${label} (${resolved})`);
191
+ }
192
+
193
+ function asObject(input: unknown, label: string): Record<string, unknown> {
194
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
195
+ throw new Error(`${label} must be a JSON object`);
196
+ }
197
+ return input as Record<string, unknown>;
198
+ }
199
+
200
+ function createRuntimeApi(config: CfsharePluginConfig): CfshareRuntimeApi {
201
+ const stringifyArgs = (args: unknown[]) =>
202
+ args
203
+ .map((value) => {
204
+ if (typeof value === "string") {
205
+ return value;
206
+ }
207
+ try {
208
+ return JSON.stringify(value);
209
+ } catch {
210
+ return String(value);
211
+ }
212
+ })
213
+ .join(" ");
214
+
215
+ const logger = {
216
+ info: (...args: unknown[]) => {
217
+ if (process.env.CFSHARE_LOG_LEVEL === "info" || process.env.CFSHARE_LOG_LEVEL === "debug") {
218
+ process.stderr.write(`[cfshare] ${stringifyArgs(args)}\n`);
219
+ }
220
+ },
221
+ warn: (...args: unknown[]) => {
222
+ process.stderr.write(`[cfshare][warn] ${stringifyArgs(args)}\n`);
223
+ },
224
+ error: (...args: unknown[]) => {
225
+ process.stderr.write(`[cfshare][error] ${stringifyArgs(args)}\n`);
226
+ },
227
+ debug: (...args: unknown[]) => {
228
+ if (process.env.CFSHARE_LOG_LEVEL === "debug") {
229
+ process.stderr.write(`[cfshare][debug] ${stringifyArgs(args)}\n`);
230
+ }
231
+ },
232
+ } as unknown as CfshareRuntimeApi["logger"];
233
+
234
+ return {
235
+ logger,
236
+ resolvePath: resolvePathFromCwd,
237
+ pluginConfig: config,
238
+ };
239
+ }
240
+
241
+ function shouldKeepAlive(command: string, keepAliveFlag: boolean | undefined): boolean {
242
+ if (typeof keepAliveFlag === "boolean") {
243
+ return keepAliveFlag;
244
+ }
245
+ return command === "expose_port" || command === "expose_files";
246
+ }
247
+
248
+ async function waitUntilExposureStops(manager: CfshareManager, id: string): Promise<void> {
249
+ await new Promise<void>((resolve, reject) => {
250
+ let stopping = false;
251
+ let interval: NodeJS.Timeout | undefined;
252
+
253
+ const shutdown = async (reason: string) => {
254
+ if (stopping) {
255
+ return;
256
+ }
257
+ stopping = true;
258
+ if (interval) {
259
+ clearInterval(interval);
260
+ }
261
+ process.removeListener("SIGINT", onSigint);
262
+ process.removeListener("SIGTERM", onSigterm);
263
+ try {
264
+ await manager.stopExposure(id, { reason });
265
+ } catch {
266
+ // best effort cleanup on signal
267
+ } finally {
268
+ resolve();
269
+ }
270
+ };
271
+
272
+ const onSigint = () => {
273
+ void shutdown("cli interrupted");
274
+ };
275
+ const onSigterm = () => {
276
+ void shutdown("cli terminated");
277
+ };
278
+ process.once("SIGINT", onSigint);
279
+ process.once("SIGTERM", onSigterm);
280
+
281
+ interval = setInterval(async () => {
282
+ try {
283
+ const detail = (await manager.exposureGet({ id })) as { status?: unknown };
284
+ const statusValue = detail.status;
285
+ const state =
286
+ typeof statusValue === "string"
287
+ ? statusValue
288
+ : typeof statusValue === "object" && statusValue && "state" in statusValue
289
+ ? String((statusValue as { state?: unknown }).state ?? "")
290
+ : "";
291
+ if (state === "stopped" || state === "expired" || state === "error" || state === "not_found") {
292
+ clearInterval(interval);
293
+ process.removeListener("SIGINT", onSigint);
294
+ process.removeListener("SIGTERM", onSigterm);
295
+ resolve();
296
+ }
297
+ } catch (error) {
298
+ clearInterval(interval);
299
+ process.removeListener("SIGINT", onSigint);
300
+ process.removeListener("SIGTERM", onSigterm);
301
+ reject(error);
302
+ }
303
+ }, 1000);
304
+ });
305
+ }
306
+
307
+ async function runTool(
308
+ manager: CfshareManager,
309
+ command: string,
310
+ params: Record<string, unknown>,
311
+ opts: CliOptions,
312
+ ): Promise<unknown> {
313
+ if (command === "env_check") {
314
+ return await manager.envCheck();
315
+ }
316
+ if (command === "expose_port") {
317
+ return await manager.exposePort(params as { port: number; opts?: Record<string, unknown> });
318
+ }
319
+ if (command === "expose_files") {
320
+ const ctx = opts.workspaceDir ? { workspaceDir: opts.workspaceDir } : undefined;
321
+ return await manager.exposeFiles(
322
+ params as {
323
+ paths: string[];
324
+ opts?: {
325
+ mode?: "normal" | "zip";
326
+ presentation?: "download" | "preview" | "raw";
327
+ ttl_seconds?: number;
328
+ access?: "token" | "basic" | "none";
329
+ max_downloads?: number;
330
+ };
331
+ },
332
+ ctx,
333
+ );
334
+ }
335
+ if (command === "exposure_list") {
336
+ return manager.exposureList();
337
+ }
338
+ if (command === "exposure_get") {
339
+ return await manager.exposureGet(
340
+ params as {
341
+ id?: string;
342
+ ids?: string[];
343
+ filter?: {
344
+ status?: "starting" | "running" | "stopped" | "error" | "expired";
345
+ type?: "port" | "files";
346
+ };
347
+ fields?: Array<
348
+ | "id"
349
+ | "type"
350
+ | "status"
351
+ | "port"
352
+ | "public_url"
353
+ | "expires_at"
354
+ | "local_url"
355
+ | "stats"
356
+ | "file_sharing"
357
+ | "last_error"
358
+ | "manifest"
359
+ | "created_at"
360
+ >;
361
+ opts?: {
362
+ probe_public?: boolean;
363
+ };
364
+ },
365
+ );
366
+ }
367
+ if (command === "exposure_stop") {
368
+ const stopParams = params as { id?: string; ids?: string[]; opts?: { reason?: string } };
369
+ const target = stopParams.ids ?? stopParams.id;
370
+ if (!target) {
371
+ throw new Error("exposure_stop requires id or ids");
372
+ }
373
+ return await manager.stopExposure(target, stopParams.opts);
374
+ }
375
+ if (command === "exposure_logs") {
376
+ const logParams = params as {
377
+ id?: string;
378
+ ids?: string[];
379
+ opts?: { lines?: number; since_seconds?: number; component?: "tunnel" | "origin" | "all" };
380
+ };
381
+ const target = logParams.ids ?? logParams.id;
382
+ if (!target) {
383
+ throw new Error("exposure_logs requires id or ids");
384
+ }
385
+ return manager.exposureLogs(target, logParams.opts);
386
+ }
387
+ if (command === "maintenance") {
388
+ const maintenanceParams = params as {
389
+ action: "start_guard" | "run_gc" | "set_policy";
390
+ opts?: { policy?: unknown; ignore_patterns?: string[] };
391
+ };
392
+ return await manager.maintenance(maintenanceParams.action, maintenanceParams.opts);
393
+ }
394
+ if (command === "audit_query") {
395
+ const queryParams = params as {
396
+ filters?: {
397
+ id?: string;
398
+ event?: string;
399
+ type?: "port" | "files";
400
+ from_ts?: string;
401
+ to_ts?: string;
402
+ limit?: number;
403
+ };
404
+ };
405
+ return await manager.auditQuery(queryParams.filters);
406
+ }
407
+ if (command === "audit_export") {
408
+ const exportParams = params as {
409
+ range?: {
410
+ from_ts?: string;
411
+ to_ts?: string;
412
+ id?: string;
413
+ event?: string;
414
+ type?: "port" | "files";
415
+ output_path?: string;
416
+ };
417
+ };
418
+ return await manager.auditExport(exportParams.range);
419
+ }
420
+ throw new Error(`unsupported command: ${command}`);
421
+ }
422
+
423
+ async function readVersion(): Promise<string> {
424
+ const packagePath = new URL("../../package.json", import.meta.url);
425
+ const content = await fs.readFile(packagePath, "utf8");
426
+ const parsed = JSON.parse(content) as { version?: string };
427
+ return parsed.version ?? "unknown";
428
+ }
429
+
430
+ async function main() {
431
+ const options = parseArgs(process.argv.slice(2));
432
+
433
+ if (options.version) {
434
+ process.stdout.write(`${await readVersion()}\n`);
435
+ return;
436
+ }
437
+
438
+ if (options.help || !options.command) {
439
+ printHelp();
440
+ process.exit(options.help ? 0 : 1);
441
+ }
442
+
443
+ const command = normalizeCommand(options.command);
444
+ if (!TOOL_NAMES.has(command)) {
445
+ throw new Error(`unknown tool: ${options.command}`);
446
+ }
447
+
448
+ const paramsInput =
449
+ options.paramsJson !== undefined
450
+ ? await parseJsonInput(options.paramsJson, "--params")
451
+ : options.paramsFile
452
+ ? await parseJsonFile(options.paramsFile, "--params-file")
453
+ : {};
454
+ const configInput =
455
+ options.configJson !== undefined
456
+ ? await parseJsonInput(options.configJson, "--config")
457
+ : options.configFile
458
+ ? await parseJsonFile(options.configFile, "--config-file")
459
+ : {};
460
+
461
+ const params = asObject(paramsInput, "params");
462
+ const config = asObject(configInput, "config") as CfsharePluginConfig;
463
+ const manager = new CfshareManager(createRuntimeApi(config));
464
+
465
+ const result = await runTool(manager, command, params, options);
466
+ process.stdout.write(`${JSON.stringify(result, null, options.compact ? undefined : 2)}\n`);
467
+
468
+ if (shouldKeepAlive(command, options.keepAlive)) {
469
+ const exposureId = typeof result === "object" && result ? (result as { id?: unknown }).id : undefined;
470
+ if (typeof exposureId !== "string" || !exposureId) {
471
+ return;
472
+ }
473
+ process.stderr.write(
474
+ `cfshare: exposure ${exposureId} is running. Press Ctrl+C to stop or use --no-keep-alive.\n`,
475
+ );
476
+ await waitUntilExposureStops(manager, exposureId);
477
+ }
478
+ }
479
+
480
+ void main().catch((error) => {
481
+ const message = error instanceof Error ? error.message : String(error);
482
+ process.stderr.write(`cfshare error: ${message}\n`);
483
+ process.exit(1);
484
+ });
package/src/manager.ts CHANGED
@@ -30,6 +30,8 @@ import type {
30
30
  RateLimitPolicy,
31
31
  } from "./types.js";
32
32
 
33
+ export type CfshareRuntimeApi = Pick<OpenClawPluginApi, "logger" | "resolvePath" | "pluginConfig">;
34
+
33
35
  const MAX_LOG_LINES = 4000;
34
36
  const MAX_RESPONSE_MANIFEST_ITEMS = 200;
35
37
  const MAX_RESPONSE_MANIFEST_ITEMS_MULTI_GET = 20;
@@ -282,7 +284,22 @@ function normalizeWorkspaceRelativePath(input: string | undefined): string | und
282
284
  }
283
285
 
284
286
  function sanitizeFilename(input: string): string {
285
- return input.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/_+/g, "_");
287
+ // Keep Unicode to preserve original filenames, but replace characters that are
288
+ // invalid or problematic across common filesystems (Windows in particular).
289
+ // We only sanitize a single path segment (basename), not a full path.
290
+ //
291
+ // Reference set:
292
+ // - ASCII control chars: 0x00-0x1F and 0x7F
293
+ // - Windows reserved: <>:"/\\|?*
294
+ // - Trailing dots/spaces are invalid on Windows
295
+ const cleaned = input
296
+ .replace(/[\u0000-\u001F\u007F]/g, "_")
297
+ .replace(/[<>:"/\\|?*]/g, "_")
298
+ .replace(/_+/g, "_")
299
+ .replace(/[. ]+$/g, "_");
300
+
301
+ const trimmed = cleaned.trim();
302
+ return trimmed || "item";
286
303
  }
287
304
 
288
305
  function ensureString(input: unknown): string | undefined {
@@ -700,7 +717,7 @@ function matchAuditFilters(
700
717
  }
701
718
 
702
719
  export class CfshareManager {
703
- private readonly logger: OpenClawPluginApi["logger"];
720
+ private readonly logger: CfshareRuntimeApi["logger"];
704
721
  private readonly resolvePath: (input: string) => string;
705
722
  private readonly pluginConfig: CfsharePluginConfig;
706
723
  private readonly cloudflaredPathInput: string;
@@ -721,7 +738,7 @@ export class CfshareManager {
721
738
  private guardTimer?: NodeJS.Timeout;
722
739
  private readonly sessions = new Map<string, ExposureSession>();
723
740
 
724
- constructor(api: OpenClawPluginApi) {
741
+ constructor(api: CfshareRuntimeApi) {
725
742
  this.logger = api.logger;
726
743
  this.resolvePath = api.resolvePath;
727
744
  this.pluginConfig = (api.pluginConfig ?? {}) as CfsharePluginConfig;
@@ -878,7 +895,7 @@ export class CfshareManager {
878
895
  return queryToken === access.token || headerToken === access.token || bearer === access.token;
879
896
  }
880
897
  const basic = parseBasicAuth(req.headers.authorization);
881
- return basic?.username === access.username && basic.password === access.password;
898
+ return basic?.username === access.username && basic?.password === access.password;
882
899
  }
883
900
 
884
901
  private async startReverseProxy(params: {
@@ -1097,10 +1114,12 @@ export class CfshareManager {
1097
1114
  zip.outputStream.pipe(out);
1098
1115
 
1099
1116
  for (const relPath of files) {
1100
- if (relPath === path.basename(zipPath)) {
1117
+ // Zip entries should use "/" separators regardless of OS.
1118
+ const zipEntry = relPath.split(path.sep).join("/");
1119
+ if (zipEntry === path.basename(zipPath)) {
1101
1120
  continue;
1102
1121
  }
1103
- zip.addFile(path.join(workspaceDir, relPath), relPath);
1122
+ zip.addFile(path.join(workspaceDir, relPath), zipEntry);
1104
1123
  }
1105
1124
  zip.end();
1106
1125
  });
@@ -1316,6 +1335,7 @@ export class CfshareManager {
1316
1335
  res,
1317
1336
  session: params.session,
1318
1337
  filePath: zipBundle.zipPath,
1338
+ downloadName: "download.zip",
1319
1339
  presentation: "download",
1320
1340
  countAsDownload: true,
1321
1341
  });
@@ -1421,10 +1441,23 @@ export class CfshareManager {
1421
1441
 
1422
1442
  const sourceStat = await fs.stat(real);
1423
1443
  const baseName = sanitizeFilename(path.basename(real) || "item");
1424
- let target = path.join(workspaceDir, baseName);
1444
+ const makeCandidate = (n: number) => {
1445
+ if (n === 0) {
1446
+ return baseName;
1447
+ }
1448
+ // For directories, treat dots as part of the name (do not split extension).
1449
+ if (sourceStat.isDirectory()) {
1450
+ return `${baseName}_${n}`;
1451
+ }
1452
+ // For files, keep extension stable: "a.txt" -> "a_1.txt".
1453
+ const parsed = path.parse(baseName);
1454
+ return `${parsed.name || "item"}_${n}${parsed.ext || ""}`;
1455
+ };
1456
+
1457
+ let target = path.join(workspaceDir, makeCandidate(0));
1425
1458
  let seq = 1;
1426
1459
  while (await fileExists(target)) {
1427
- target = path.join(workspaceDir, `${baseName}_${seq}`);
1460
+ target = path.join(workspaceDir, makeCandidate(seq));
1428
1461
  seq += 1;
1429
1462
  }
1430
1463
 
@@ -1578,7 +1611,7 @@ export class CfshareManager {
1578
1611
  this.appendLog(session, "tunnel", `spawn: ${cloudflaredBin} ${args.join(" ")}`);
1579
1612
 
1580
1613
  const proc = spawn(cloudflaredBin, args, {
1581
- stdio: ["ignore", "pipe", "pipe"],
1614
+ stdio: ["pipe", "pipe", "pipe"],
1582
1615
  });
1583
1616
 
1584
1617
  let settled = false;
@@ -1694,6 +1727,9 @@ export class CfshareManager {
1694
1727
  return;
1695
1728
  }
1696
1729
  const pid = proc.pid;
1730
+ if (!pid) {
1731
+ return;
1732
+ }
1697
1733
  try {
1698
1734
  process.kill(pid, 0);
1699
1735
  } catch {
package/src/shims.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ declare module "openclaw/plugin-sdk" {
2
+ import type { TSchema } from "@sinclair/typebox";
3
+
4
+ export type OpenClawToolContext = {
5
+ workspaceDir?: string;
6
+ };
7
+
8
+ export type OpenClawToolDefinition = {
9
+ name: string;
10
+ label: string;
11
+ description: string;
12
+ parameters?: TSchema | Record<string, unknown>;
13
+ execute: (...args: any[]) => unknown | Promise<unknown>;
14
+ };
15
+
16
+ export type OpenClawPluginApi = {
17
+ logger: {
18
+ info: (...args: unknown[]) => void;
19
+ warn: (...args: unknown[]) => void;
20
+ error: (...args: unknown[]) => void;
21
+ debug: (...args: unknown[]) => void;
22
+ };
23
+ resolvePath: (input: string) => string;
24
+ pluginConfig?: Record<string, unknown>;
25
+ registerTool: (
26
+ factory:
27
+ | ((ctx: OpenClawToolContext) => OpenClawToolDefinition[])
28
+ | ((ctx: OpenClawToolContext) => Promise<OpenClawToolDefinition[]>),
29
+ options?: {
30
+ names?: string[];
31
+ },
32
+ ) => void;
33
+ };
34
+
35
+ export function emptyPluginConfigSchema(): Record<string, unknown>;
36
+ export function jsonResult<T>(value: T): T;
37
+ export function stringEnum<const T extends readonly string[]>(
38
+ values: T,
39
+ options?: Record<string, unknown>,
40
+ ): TSchema;
41
+ }
42
+
43
+ declare module "yazl" {
44
+ class ZipFile {
45
+ outputStream: NodeJS.ReadableStream;
46
+ addFile(realPath: string, metadataPath: string, options?: Record<string, unknown>): void;
47
+ end(options?: Record<string, unknown>, callback?: () => void): void;
48
+ }
49
+
50
+ const yazl: {
51
+ ZipFile: typeof ZipFile;
52
+ };
53
+
54
+ export default yazl;
55
+ }
package/src/tools.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { TSchema } from "@sinclair/typebox";
1
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
3
  import { jsonResult } from "openclaw/plugin-sdk";
3
4
  import { CfshareManager } from "./manager.js";
@@ -27,7 +28,15 @@ type ToolContext = {
27
28
  workspaceDir?: string;
28
29
  };
29
30
 
30
- function registerToolsForContext(api: OpenClawPluginApi, ctx: ToolContext) {
31
+ type RegisteredTool = {
32
+ name: string;
33
+ label: string;
34
+ description: string;
35
+ parameters: TSchema | Record<string, unknown>;
36
+ execute: (...args: any[]) => Promise<unknown>;
37
+ };
38
+
39
+ function registerToolsForContext(api: OpenClawPluginApi, ctx: ToolContext): RegisteredTool[] {
31
40
  const manager = getManager(api);
32
41
 
33
42
  return [
@@ -250,7 +259,7 @@ export function registerCfshareTools(api: OpenClawPluginApi) {
250
259
  "audit_export",
251
260
  ];
252
261
 
253
- api.registerTool((ctx) => registerToolsForContext(api, ctx), {
262
+ api.registerTool((ctx: ToolContext) => registerToolsForContext(api, ctx), {
254
263
  names,
255
264
  });
256
265
  }