Skill: kadbbz/aios-transfer-file (version 0.0.1)

--- name: aios-transfer-file description: 通过兼容 AWS S3 的 SDK 为 OpenClaw 和 AIOS agent 处理文件传输。当 agent 收到 `file_input://...` URI、需要把输入文件下载到工作区，或需要把本地文件上传后以 `file_output://...` URI 返回给用户时，必须使用这个 skill。不要使用 `aws` CLI 或任何外部 S3 客户端程序完成这些传输。 --- # AIOS 文件传输 在 OpenClaw 中，所有文件传输都必须使用兼容 AWS S3 的 SDK。这适用于附件、`file_input://` 消息，以及任何需要把文件返回给用户的场景。 ## 不可违反的规则 - 始终使用 S3 SDK。不要调用 `aws`、`s3cmd`、`mc` 或任何其他外部客户端程序。 - 本项目统一使用 TypeScript 和 JavaScript。不要为这个 skill 添加 Python 脚本或基于 Python 的传输流程。 - 在处理任何 `file_input://...` URI 之前，或在发送任何本地文件给用户之前，先触发这个 skill。 - 传输操作优先使用内置脚本 `scripts/transfer_s3.mjs`，不要在对话中临时写新的传输脚本。 - 如果工作区中还没有 `file_input` 和 `file_output` 目录，使用前先创建。 - 下载和上传都必须限制在工作区内进行。 - 在 SDK 上传成功之前，不要声称文件已经发送完成。 - 当你需要返回一个可下载文件时，最终回复必须是单独一条 `file_output://...` 消息，不能附带其他文字。 ## 运行时配置 使用 AIOS 环境变量构建 S3 客户端： - `AIOS_S3_ENDPOINT` - `AIOS_S3_REGION` - `AIOS_S3_ACCESS_KEY_ID` - `AIOS_S3_SECRET_ACCESS_KEY` - `AIOS_S3_FORCE_PATH_STYLE` 使用以下 bucket 变量： - 收件箱 bucket：`AIOS_S3_AGENT_INBOX_BUCKET` - 发件箱 bucket：`AIOS_S3_AGENT_OUTBOX_BUCKET` 发件箱变量统一使用 `AIOS_S3_AGENT_OUTBOX_BUCKET`，不要引入其他别名。 构造 SDK 客户端时： - 从 `AIOS_S3_ENDPOINT` 设置自定义 endpoint。 - 从 `AIOS_S3_REGION` 设置 region。 - 显式传入 `AIOS_S3_ACCESS_KEY_ID` 和 `AIOS_S3_SECRET_ACCESS_KEY` 作为访问凭证。 - 当 `AIOS_S3_FORCE_PATH_STYLE` 为真值时启用 path-style，例如 `1`、`true` 或 `TRUE`。 与 `aios-agent-management-console` 保持一致，使用 TypeScript 或 JavaScript，并采用 AWS SDK for JavaScript v3，具体为 `@aws-sdk/client-s3`。 ## 内置脚本 优先使用内置 JavaScript 工具： ```bash node scripts/transfer_s3.mjs download-uri --uri "file_input://bucket/path/to/file.bin" --workspace . node scripts/transfer_s3.mjs upload-file --source "/abs/path/to/file.bin" --workspace . ``` 内置脚本的行为： - 使用 `@aws-sdk/client-s3` 和上面的 AIOS S3 环境变量。 - 与 `aios-agent-management-console` 保持同样风格：ESM、动态导入本地安装的包、显式构造 `S3Client`、显式传入凭证，并且 `AIOS_S3_FORCE_PATH_STYLE` 默认取 `true`。 - 自动创建 `file_input` 和 `file_output` 目录。 - 把 `file_input://...` URI 下载到 `file_input`。 - 把待发送文件复制到 `file_output`，重命名为要求的时间戳前缀格式，并上传到发件箱 bucket 根目录。 - 输出描述传输结果的 JSON。 - 支持 `--uri-only`，用于最终上传回复步骤。 依赖位置： - 这个 skill 在 `package.json` 中声明了 `@aws-sdk/client-s3`。 - 在一个全新的工作区首次传输前，先检查这个 skill 的本地依赖是否已经安装。 - 必须在这个 skill 目录中完成依赖检查，例如确认 `node_modules/@aws-sdk/client-s3` 是否存在，或运行 `npm ls @aws-sdk/client-s3 --depth=0`。 - 如果依赖缺失，在运行内置脚本前，先在这个 skill 目录执行 `npm install` 本地安装依赖。 - 只允许从本地安装位置动态加载该包；不要依赖其他项目中的兜底导入，也不要依赖任意文件路径。 - 如果本地包缺失，要明确报告依赖错误。除非用户明确要求修改内置脚本，否则不要在对话里写一个替代传输脚本。 依赖处理流程： 1. 在检查依赖或执行安装命令前，先切换到这个 skill 目录。 2. 确认 `node` 和 `npm` 可用。 3. 检查 `@aws-sdk/client-s3` 是否已经本地安装。 4. 只有在本地依赖检查确认缺失时，才运行 `npm install`。 5. 安装完成后，仍然要从这个 skill 目录运行内置脚本，确保它能正确解析本地依赖。 ## 接收文件 当消息以 `file_input://` 开头时： 1. 解析 URI，并忽略第一个空格之后的所有内容。 2. 从 URI 中提取 `bucket` 和完整的 `key`。 示例：`file_input://bname/path/to/fname.bin extra text` - bucket: `bname` - key: `path/to/fname.bin` 3. 在下载目标元数据中保留完整 key。识别远端对象时，不要把它压缩成只有 basename。 4. 把对象下载到工作区的 `file_input` 目录。 5. 本地文件名使用原始 basename，但前面加上时间戳前缀，例如 `123456_fname.bin`。 6. 如果 basename 本身已经带有时间戳前缀，则替换原有前缀，不要再叠加一个。 7. 下载完成后，把本地绝对路径加入工作上下文，再继续处理用户请求。 实现要求： - 下载请求必须以解析得到的 URI bucket 和 key 为准。 - 你可以把 URI bucket 与 `AIOS_S3_AGENT_INBOX_BUCKET` 做诊断比较，但下载时不要改写 URI bucket。 - 使用 SDK 流式下载对象，不要通过 shell 调用客户端可执行程序。 - 优先调用 `scripts/transfer_s3.mjs download-uri ...`，不要写一次性的 SDK 代码。 ## 发送文件 当你需要把本地文件发回给用户时： 1. 先用普通 assistant 消息说明文件内容以及用户该如何使用。 2. 从 `AIOS_S3_AGENT_OUTBOX_BUCKET` 解析发件箱 bucket。 3. 把文件复制到工作区的 `file_output` 目录。 4. 将复制后的文件重命名为带时间戳前缀的格式，例如 `123456_fname.bin`。 5. 如果原始 basename 已经有时间戳前缀，则替换原前缀，不要叠加。 6. 将最终文件名中的所有空格替换为 `_`。 7. 远端目标必须使用发件箱 bucket 根目录，不要额外添加子目录。 8. 如果发件箱 bucket 不存在，通过 SDK 先创建，再上传。 9. 使用 SDK 上传复制后的文件。 10. 上传成功后，再发送第二条回复，内容只能是 `file_output://<bucket>/<new_name>`。 实现要求： - 远端 key 必须始终精确等于 bucket 根目录下的时间戳文件名。 - 说明性消息和 `file_output://...` 回复不能合并在同一条 assistant 消息里。 - 在上传成功之前，不要输出 `file_output://...`。 - 优先调用 `scripts/transfer_s3.mjs upload-file ...`，不要写一次性的 SDK 代码。 ## 文件名规则 下载和上传都使用以下规则： - 时间戳前缀格式：`<timestamp>_filename.ext` - 如果文件名已经以 `<digits>_` 开头，则把这个前导数字前缀替换成新的时间戳。 - 除了上传时必须把空格替换为 `_` 之外，文件名其余部分保持原样。 ## SDK 行为 使用 SDK 的标准对象操作，不要拉起外部客户端： - 下载：`GetObject` 或等价接口 - 上传：`PutObject` 或托管上传助手 - 检查 bucket 是否存在：`HeadBucket` 或等价接口 - bucket 缺失时创建：`CreateBucket` 或等价接口 对于大文件，优先使用流式 API；当 SDK 支持流式处理时，避免把整个文件一次性读入内存。 ## 失败处理 - 如果缺少必需的 S3 配置，停止执行，并准确指出缺失的是哪个环境变量。 - 如果 URI 格式错误，无法同时提取 bucket 和 key，停止执行，并报告输入格式有问题。 - 如果下载或上传失败，报告真实失败原因，不要假装传输已经完成。 - 如果上传失败，不要发送 `file_output://...` URI。

path: agents/openai.yaml
content: interface:
  display_name: "AIOS File Transfer"
  short_description: "Transfer OpenClaw files through an S3 SDK"
  default_prompt: "Use $aios-transfer-file to receive `file_input://...` URIs or upload a local file back to the user through an S3-compatible SDK."

policy:
  allow_implicit_invocation: true


path: package-lock.json
content: {
  "name": "aios-transfer-file",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "aios-transfer-file",
      "dependencies": {
        "@aws-sdk/client-s3": "^3.1045.0"
      }
    },
    "node_modules/@aws-crypto/crc32": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-crypto/util": "^5.2.0",
        "@aws-sdk/types": "^3.222.0",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=16.0.0"
      }
    },
    "node_modules/@aws-crypto/crc32c": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-crypto/util": "^5.2.0",
        "@aws-sdk/types": "^3.222.0",
        "tslib": "^2.6.2"
      }
    },
    "node_modules/@aws-crypto/sha1-browser": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-crypto/supports-web-crypto": "^5.2.0",
        "@aws-crypto/util": "^5.2.0",
        "@aws-sdk/types": "^3.222.0",
        "@aws-sdk/util-locate-window": "^3.0.0",
        "@smithy/util-utf8": "^2.0.0",
        "tslib": "^2.6.2"
      }
    },
    "node_modules/@aws-crypto/sha256-browser": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-crypto/sha256-js": "^5.2.0",
        "@aws-crypto/supports-web-crypto": "^5.2.0",
        "@aws-crypto/util": "^5.2.0",
        "@aws-sdk/types": "^3.222.0",
        "@aws-sdk/util-locate-window": "^3.0.0",
        "@smithy/util-utf8": "^2.0.0",
        "tslib": "^2.6.2"
      }
    },
    "node_modules/@aws-crypto/sha256-js": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-crypto/util": "^5.2.0",
        "@aws-sdk/types": "^3.222.0",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=16.0.0"
      }
    },
    "node_modules/@aws-crypto/supports-web-crypto": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "tslib": "^2.6.2"
      }
    },
    "node_modules/@aws-crypto/util": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/types": "^3.222.0",
        "@smithy/util-utf8": "^2.0.0",
        "tslib": "^2.6.2"
      }
    },
    "node_modules/@aws-sdk/client-s3": {
      "version": "3.1046.0",
      "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1046.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-crypto/sha1-browser": "5.2.0",
        "@aws-crypto/sha256-browser": "5.2.0",
        "@aws-crypto/sha256-js": "5.2.0",
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/credential-provider-node": "^3.972.40",
        "@aws-sdk/middleware-bucket-endpoint": "^3.972.11",
        "@aws-sdk/middleware-expect-continue": "^3.972.11",
        "@aws-sdk/middleware-flexible-checksums": "^3.974.17",
        "@aws-sdk/middleware-host-header": "^3.972.11",
        "@aws-sdk/middleware-location-constraint": "^3.972.10",
        "@aws-sdk/middleware-logger": "^3.972.10",
        "@aws-sdk/middleware-recursion-detection": "^3.972.12",
        "@aws-sdk/middleware-sdk-s3": "^3.972.38",
        "@aws-sdk/middleware-ssec": "^3.972.10",
        "@aws-sdk/middleware-user-agent": "^3.972.39",
        "@aws-sdk/region-config-resolver": "^3.972.14",
        "@aws-sdk/signature-v4-multi-region": "^3.996.26",
        "@aws-sdk/types": "^3.973.8",
        "@aws-sdk/util-endpoints": "^3.996.9",
        "@aws-sdk/util-user-agent-browser": "^3.972.11",
        "@aws-sdk/util-user-agent-node": "^3.973.25",
        "@smithy/core": "^3.24.1",
        "@smithy/fetch-http-handler": "^5.4.1",
        "@smithy/node-http-handler": "^4.7.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/core": {
      "version": "3.974.9",
      "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.9.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/types": "^3.973.8",
        "@aws-sdk/xml-builder": "^3.972.23",
        "@smithy/core": "^3.24.1",
        "@smithy/signature-v4": "^5.4.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/crc64-nvme": {
      "version": "3.972.8",
      "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/credential-provider-env": {
      "version": "3.972.35",
      "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.35.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/credential-provider-http": {
      "version": "3.972.37",
      "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.37.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/fetch-http-handler": "^5.4.1",
        "@smithy/node-http-handler": "^4.7.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/credential-provider-ini": {
      "version": "3.972.39",
      "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.39.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/credential-provider-env": "^3.972.35",
        "@aws-sdk/credential-provider-http": "^3.972.37",
        "@aws-sdk/credential-provider-login": "^3.972.39",
        "@aws-sdk/credential-provider-process": "^3.972.35",
        "@aws-sdk/credential-provider-sso": "^3.972.39",
        "@aws-sdk/credential-provider-web-identity": "^3.972.39",
        "@aws-sdk/nested-clients": "^3.997.7",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/credential-provider-imds": "^4.3.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/credential-provider-login": {
      "version": "3.972.39",
      "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.39.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/nested-clients": "^3.997.7",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/credential-provider-node": {
      "version": "3.972.40",
      "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.40.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/credential-provider-env": "^3.972.35",
        "@aws-sdk/credential-provider-http": "^3.972.37",
        "@aws-sdk/credential-provider-ini": "^3.972.39",
        "@aws-sdk/credential-provider-process": "^3.972.35",
        "@aws-sdk/credential-provider-sso": "^3.972.39",
        "@aws-sdk/credential-provider-web-identity": "^3.972.39",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/credential-provider-imds": "^4.3.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/credential-provider-process": {
      "version": "3.972.35",
      "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.35.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/credential-provider-sso": {
      "version": "3.972.39",
      "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.39.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/nested-clients": "^3.997.7",
        "@aws-sdk/token-providers": "3.1046.0",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/credential-provider-web-identity": {
      "version": "3.972.39",
      "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.39.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/nested-clients": "^3.997.7",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/middleware-bucket-endpoint": {
      "version": "3.972.11",
      "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/middleware-expect-continue": {
      "version": "3.972.11",
      "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/middleware-flexible-checksums": {
      "version": "3.974.17",
      "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.17.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-crypto/crc32": "5.2.0",
        "@aws-crypto/crc32c": "5.2.0",
        "@aws-crypto/util": "5.2.0",
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/crc64-nvme": "^3.972.8",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/middleware-host-header": {
      "version": "3.972.11",
      "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/middleware-location-constraint": {
      "version": "3.972.10",
      "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.10.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/types": "^3.973.8",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/middleware-logger": {
      "version": "3.972.10",
      "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/types": "^3.973.8",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/middleware-recursion-detection": {
      "version": "3.972.12",
      "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.12.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/types": "^3.973.8",
        "@aws/lambda-invoke-store": "^0.2.2",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/middleware-sdk-s3": {
      "version": "3.972.38",
      "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.38.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/signature-v4-multi-region": "^3.996.26",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/signature-v4": "^5.4.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/middleware-ssec": {
      "version": "3.972.10",
      "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.10.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/types": "^3.973.8",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/middleware-user-agent": {
      "version": "3.972.39",
      "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.39.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/types": "^3.973.8",
        "@aws-sdk/util-endpoints": "^3.996.9",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/nested-clients": {
      "version": "3.997.7",
      "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-crypto/sha256-browser": "5.2.0",
        "@aws-crypto/sha256-js": "5.2.0",
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/middleware-host-header": "^3.972.11",
        "@aws-sdk/middleware-logger": "^3.972.10",
        "@aws-sdk/middleware-recursion-detection": "^3.972.12",
        "@aws-sdk/middleware-user-agent": "^3.972.39",
        "@aws-sdk/region-config-resolver": "^3.972.14",
        "@aws-sdk/signature-v4-multi-region": "^3.996.26",
        "@aws-sdk/types": "^3.973.8",
        "@aws-sdk/util-endpoints": "^3.996.9",
        "@aws-sdk/util-user-agent-browser": "^3.972.11",
        "@aws-sdk/util-user-agent-node": "^3.973.25",
        "@smithy/core": "^3.24.1",
        "@smithy/fetch-http-handler": "^5.4.1",
        "@smithy/node-http-handler": "^4.7.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/region-config-resolver": {
      "version": "3.972.14",
      "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.14.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/signature-v4-multi-region": {
      "version": "3.996.26",
      "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.26.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/signature-v4": "^5.4.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/token-providers": {
      "version": "3.1046.0",
      "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1046.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/core": "^3.974.9",
        "@aws-sdk/nested-clients": "^3.997.7",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/types": {
      "version": "3.973.8",
      "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/util-endpoints": {
      "version": "3.996.9",
      "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.9.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/util-locate-window": {
      "version": "3.965.5",
      "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws-sdk/util-user-agent-browser": {
      "version": "3.972.11",
      "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/types": "^3.973.8",
        "@smithy/types": "^4.14.1",
        "bowser": "^2.11.0",
        "tslib": "^2.6.2"
      }
    },
    "node_modules/@aws-sdk/util-user-agent-node": {
      "version": "3.973.25",
      "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.25.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-sdk/middleware-user-agent": "^3.972.39",
        "@aws-sdk/types": "^3.973.8",
        "@smithy/core": "^3.24.1",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      },
      "peerDependencies": {
        "aws-crt": ">=1.0.0"
      },
      "peerDependenciesMeta": {
        "aws-crt": {
          "optional": true
        }
      }
    },
    "node_modules/@aws-sdk/xml-builder": {
      "version": "3.972.23",
      "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.23.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@nodable/entities": "2.1.0",
        "@smithy/types": "^4.14.1",
        "fast-xml-parser": "5.7.2",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/@aws/lambda-invoke-store": {
      "version": "0.2.4",
      "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/@nodable/entities": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/nodable"
        }
      ],
      "license": "MIT"
    },
    "node_modules/@smithy/core": {
      "version": "3.24.2",
      "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@aws-crypto/crc32": "5.2.0",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/@smithy/credential-provider-imds": {
      "version": "4.3.2",
      "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@smithy/core": "^3.24.2",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/@smithy/fetch-http-handler": {
      "version": "5.4.2",
      "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@smithy/core": "^3.24.2",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/@smithy/is-array-buffer": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/@smithy/node-http-handler": {
      "version": "4.7.2",
      "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@smithy/core": "^3.24.2",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/@smithy/signature-v4": {
      "version": "5.4.2",
      "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@smithy/core": "^3.24.2",
        "@smithy/types": "^4.14.1",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/@smithy/types": {
      "version": "4.14.1",
      "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/@smithy/util-buffer-from": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@smithy/is-array-buffer": "^2.2.0",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/@smithy/util-utf8": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@smithy/util-buffer-from": "^2.2.0",
        "tslib": "^2.6.2"
      },
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/bowser": {
      "version": "2.14.1",
      "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/fast-xml-builder": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/NaturalIntelligence"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "path-expression-matcher": "^1.5.0",
        "xml-naming": "^0.1.0"
      }
    },
    "node_modules/fast-xml-parser": {
      "version": "5.7.2",
      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/NaturalIntelligence"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "@nodable/entities": "^2.1.0",
        "fast-xml-builder": "^1.1.5",
        "path-expression-matcher": "^1.5.0",
        "strnum": "^2.2.3"
      },
      "bin": {
        "fxparser": "src/cli/cli.js"
      }
    },
    "node_modules/path-expression-matcher": {
      "version": "1.5.0",
      "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/NaturalIntelligence"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/strnum": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/NaturalIntelligence"
        }
      ],
      "license": "MIT"
    },
    "node_modules/tslib": {
      "version": "2.8.1",
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "0BSD"
    },
    "node_modules/xml-naming": {
      "version": "0.1.0",
      "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/NaturalIntelligence"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=16.0.0"
      }
    }
  }
}


path: package.json
content: {
  "name": "aios-transfer-file",
  "private": true,
  "type": "module",
  "dependencies": {
    "@aws-sdk/client-s3": "^3.1045.0"
  }
}


path: README.md
content: # aios-transfer-file

OpenClaw/AIOS 文件传输 skill。使用 `scripts/transfer_s3.mjs` 动态加载当前目录本地安装的 `@aws-sdk/client-s3`，把 `file_input://...` 对象下载到工作区，或把本地文件上传到发件箱 bucket。

## 依赖要求

- Node.js
- `@aws-sdk/client-s3`
- `AIOS_S3_ENDPOINT`
- `AIOS_S3_REGION`
- `AIOS_S3_ACCESS_KEY_ID`
- `AIOS_S3_SECRET_ACCESS_KEY`
- `AIOS_S3_FORCE_PATH_STYLE`
- `AIOS_S3_AGENT_INBOX_BUCKET`
- `AIOS_S3_AGENT_OUTBOX_BUCKET`

## 用法

```bash
npm install
node scripts/transfer_s3.mjs download-uri --uri "file_input://bucket/path/to/file.bin" --workspace .
node scripts/transfer_s3.mjs upload-file --source "/abs/path/to/file.bin" --workspace .
```

## Docker

推荐做法：在 Docker 镜像内预装依赖，但仍然把它作为这个 skill 的本地依赖，而不是使用 `npm install -g`。

Dockerfile 示例模式：

```dockerfile
# 把 skill 复制进镜像
COPY skills/aios-transfer-file /opt/aios/skills/aios-transfer-file

# 在镜像构建阶段安装这个 skill 的本地依赖
WORKDIR /opt/aios/skills/aios-transfer-file
RUN npm ci
```

运行时要求：

- `package.json`、`package-lock.json` 和 `node_modules/` 必须保留在同一个 skill 目录下。
- 从这个 skill 目录运行脚本，或者确保启动脚本的进程模块解析路径能够看到这个本地 `node_modules`。
- 不要依赖 `npm install -g @aws-sdk/client-s3`；这个脚本的设计就是动态导入本地安装的包。


path: scripts/transfer_s3.mjs
content: #!/usr/bin/env node

import { copyFile, mkdir, writeFile } from "node:fs/promises";
import { createReadStream, createWriteStream } from "node:fs";
import path from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";

const TIMESTAMP_PREFIX_RE = /^\d+_/;

function fail(message, code = 1) {
  console.error(message);
  process.exit(code);
}

function readRequiredEnv(name) {
  const value = process.env[name]?.trim();
  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }
  return value;
}

function readOptionalEnv(name, fallback) {
  const value = process.env[name] ?? fallback;
  if (!value || value.trim().length === 0) {
    return undefined;
  }
  return value.trim();
}

function readBooleanEnv(name, fallback) {
  const value = process.env[name];
  if (!value || value.trim().length === 0) {
    return fallback;
  }

  const normalized = value.trim().toLowerCase();
  if (normalized === "true" || normalized === "1" || normalized === "yes") {
    return true;
  }
  if (normalized === "false" || normalized === "0" || normalized === "no") {
    return false;
  }

  throw new Error(`${name} must be one of true,false,1,0,yes,no`);
}

function ensureOptionalUrl(name, value) {
  if (!value) {
    return undefined;
  }

  try {
    const url = new URL(value);
    if (!url.protocol || !url.hostname) {
      throw new Error("missing protocol or hostname");
    }
  } catch (error) {
    const reason = error instanceof Error ? error.message : String(error);
    throw new Error(`${name} must be a valid URL: ${reason}`);
  }

  return value;
}

function parseFileInputUri(rawUri) {
  const trimmed = rawUri.split(" ", 1)[0].trim();
  const match = /^file_input:\/\/([^/]+)\/(.+)$/.exec(trimmed);
  if (!match) {
    throw new Error(`Malformed file input URI: ${rawUri}`);
  }

  const [, bucket, key] = match;
  if (!bucket || !key) {
    throw new Error(`Malformed file input URI: ${rawUri}`);
  }

  return { sourceUri: trimmed, bucket, key };
}

function makeTimestampedName(name, { replaceSpaces }) {
  const normalized = (replaceSpaces ? name.replaceAll(" ", "_") : name).replace(TIMESTAMP_PREFIX_RE, "");
  return `${Math.floor(Date.now() / 1000)}_${normalized}`;
}

function resolveOutboxEnvName() {
  if (readOptionalEnv("AIOS_S3_AGENT_OUTBOX_BUCKET")) {
    return "AIOS_S3_AGENT_OUTBOX_BUCKET";
  }
  throw new Error("Missing required environment variable: AIOS_S3_AGENT_OUTBOX_BUCKET");
}

async function loadAwsSdk() {
  try {
    return await import("@aws-sdk/client-s3");
  } catch {
    throw new Error("Missing dependency: @aws-sdk/client-s3. Run npm install in this skill directory.");
  }
}

async function createS3Client() {
  const sdk = await loadAwsSdk();
  const endpoint = ensureOptionalUrl("AIOS_S3_ENDPOINT", readOptionalEnv("AIOS_S3_ENDPOINT"));
  const region = readOptionalEnv("AIOS_S3_REGION", "us-east-1") ?? "us-east-1";
  const [REDACTED_SECRET]"AIOS_S3_ACCESS_KEY_ID");
  const secretAccessKey = readRequiredEnv("AIOS_S3_SECRET_ACCESS_KEY");
  const forcePathStyle = readBooleanEnv("AIOS_S3_FORCE_PATH_STYLE", true);

  return {
    sdk,
    client: new sdk.S3Client({
      endpoint,
      region,
      forcePathStyle,
      credentials: {
        accessKeyId,
        secretAccessKey
      }
    })
  };
}

async function ensureDirectory(dirPath) {
  await mkdir(dirPath, { recursive: true });
  return dirPath;
}

async function writeBodyToFile(body, destination) {
  if (!body) {
    throw new Error("S3 object body is empty");
  }

  if (typeof body.transformToWebStream === "function") {
    await pipeline(Readable.fromWeb(body.transformToWebStream()), createWriteStream(destination));
    return;
  }

  if (typeof body.transformToByteArray === "function") {
    await writeFile(destination, Buffer.from(await body.transformToByteArray()));
    return;
  }

  if (body instanceof Readable) {
    await pipeline(body, createWriteStream(destination));
    return;
  }

  if (body instanceof Uint8Array) {
    await writeFile(destination, body);
    return;
  }

  throw new Error("Unsupported S3 object body type");
}

async function ensureBucket(clientBundle, bucket) {
  const { sdk, client } = clientBundle;
  try {
    await client.send(new sdk.HeadBucketCommand({ Bucket: bucket }));
    return;
  } catch {}

  const region = readOptionalEnv("AIOS_S3_REGION", "us-east-1") ?? "us-east-1";
  try {
    await client.send(new sdk.CreateBucketCommand(
      region === "us-east-1"
        ? { Bucket: bucket }
        : {
            Bucket: bucket,
            CreateBucketConfiguration: { LocationConstraint: region }
          }
    ));
  } catch (error) {
    if (region !== "us-east-1") {
      await client.send(new sdk.CreateBucketCommand({ Bucket: bucket }));
      return;
    }
    throw error;
  }
}

async function downloadUri(options) {
  const workspace = path.resolve(options.workspace);
  const inputDir = await ensureDirectory(path.join(workspace, options.inputDirName));
  const { sourceUri, bucket, key } = parseFileInputUri(options.uri);
  const basename = path.basename(key);
  if (!basename) {
    throw new Error(`Malformed file input URI: ${options.uri}`);
  }

  const localName = makeTimestampedName(basename, { replaceSpaces: false });
  const localPath = path.join(inputDir, localName);
  const { sdk, client } = await createS3Client();
  const response = await client.send(new sdk.GetObjectCommand({
    Bucket: bucket,
    Key: key
  }));

  await writeBodyToFile(response.Body, localPath);

  console.log(JSON.stringify({
    bucket,
    key,
    sourceUri,
    localPath,
    localName
  }));
}

async function uploadFile(options) {
  const workspace = path.resolve(options.workspace);
  const outputDir = await ensureDirectory(path.join(workspace, options.outputDirName));
  const sourcePath = path.resolve(options.source);
  const outboxEnvName = resolveOutboxEnvName();
  const bucket = readRequiredEnv(outboxEnvName);
  const stagedName = makeTimestampedName(path.basename(sourcePath), { replaceSpaces: true });
  const stagedPath = path.join(outputDir, stagedName);

  await copyFile(sourcePath, stagedPath);

  const clientBundle = await createS3Client();
  await ensureBucket(clientBundle, bucket);
  await clientBundle.client.send(new clientBundle.sdk.PutObjectCommand({
    Bucket: bucket,
    Key: stagedName,
    Body: createReadStream(stagedPath)
  }));

  const result = {
    bucket,
    key: stagedName,
    sourcePath,
    stagedPath,
    uri: `file_output://${bucket}/${stagedName}`
  };
  console.log(options.uriOnly ? result.uri : JSON.stringify(result));
}

function parseArgs(argv) {
  const args = {
    command: undefined,
    workspace: ".",
    inputDirName: "file_input",
    outputDirName: "file_output",
    uri: undefined,
    source: undefined,
    uriOnly: false
  };

  for (let index = 0; index < argv.length; index += 1) {
    const [REDACTED_SECRET];
    if (token === "-h" || token === "--help") {
      printHelp();
      process.exit(0);
    }
    if (!args.command) {
      args.command = token;
      continue;
    }
    if (token === "--workspace") {
      args.workspace = argv[++index] ?? fail("Missing value for --workspace");
      continue;
    }
    if (token === "--input-dir-name") {
      args.inputDirName = argv[++index] ?? fail("Missing value for --input-dir-name");
      continue;
    }
    if (token === "--output-dir-name") {
      args.outputDirName = argv[++index] ?? fail("Missing value for --output-dir-name");
      continue;
    }
    if (token === "--uri") {
      args.uri = argv[++index] ?? fail("Missing value for --uri");
      continue;
    }
    if (token === "--source") {
      args.source = argv[++index] ?? fail("Missing value for --source");
      continue;
    }
    if (token === "--uri-only") {
      args.uriOnly = true;
      continue;
    }

    fail(`Unknown argument: ${token}`);
  }

  return args;
}

function printHelp() {
  console.log(`Usage:
  node scripts/transfer_s3.mjs download-uri --uri "file_input://bucket/path/to/file.bin" [--workspace .] [--input-dir-name file_input]
  node scripts/transfer_s3.mjs upload-file --source "/abs/path/to/file.bin" [--workspace .] [--output-dir-name file_output] [--uri-only]

Commands:
  download-uri   Download a file_input:// URI into the workspace file_input directory.
  upload-file    Copy a local file into file_output and upload it to the outbox bucket root.
`);
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  if (args.command === "download-uri") {
    if (!args.uri) {
      fail("download-uri requires --uri");
    }
    await downloadUri(args);
    return;
  }
  if (args.command === "upload-file") {
    if (!args.source) {
      fail("upload-file requires --source");
    }
    await uploadFile(args);
    return;
  }

  printHelp();
  process.exit(args.command ? 1 : 0);
}

main().catch((error) => {
  const message = error instanceof Error ? error.message : String(error);
  fail(message);
});


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)