@clawhub-guilherme-funchal-9b4c8b8021
Generates images from text prompts using AtlasCloud Nanobanana 2 model, requiring an AtlasCloud API token and specific JSON parameters without media_resolution.
---
name: atlas-banana-textimage
description: Generates images from text prompts using the AtlasCloud Nanobanana 2 model (google/nano-banana-2/text-to-image). Use this skill whenever the user wants to create, render, or generate an image from a text description using Nanobanana or AtlasCloud. Triggers on phrases like: "gerar imagem", "generate image", "create image", "criar imagem com prompt", "draw a scene", or any request to produce a visual from a text prompt. Always use this skill when the user mentions Nanobanana, AtlasCloud image generation, or wants to produce an image from descriptive text.
metadata:
{
"openclaw":
{
"emoji": "🍌",
"requires": { "bins": ["node"] },
"install": []
}
}
---
# Atlas Nanobanana Text-to-Image 🍌
Generates images using the AtlasCloud **Nanobanana 2** model (`google/nano-banana-2/text-to-image`).
---
## Token Setup
Before generating images, you need the user's AtlasCloud API token.
- Check memory for `atlascloud_token`.
- If not found, ask the user: *"Please provide your AtlasCloud API token to get started."*
- Save the token to memory as `atlascloud_token` so it is not needed again.
---
## How to Generate an Image
**Step 1:** Write the params to `{baseDir}/params.json`.
**Step 2:** Run the script:
```bash
node {baseDir}/generate.js <TOKEN> {baseDir}/params.json
```
**Step 3:** In the script output, find the line that starts with `IMAGE_URL:` between the two rows of `=` signs:
```
============================================================
IMAGE_URL: https://atlas-media.oss-us-west-1.aliyuncs.com/images/xxxx.png
============================================================
```
> ⚠️ **CRITICAL**: Use **exactly** the URL that appears in the `IMAGE_URL:` line of this execution. Never use a URL from the conversation history, previous executions, or memory. Each execution generates a different URL.
Report this URL to the user.
---
## params.json — Payload Correto
> ⚠️ **IMPORTANT**: **Never include `media_resolution`** in the payload — it causes an HTTP 500 error.
```json
{
"prompt": "descrição detalhada da imagem",
"aspect_ratio": "16:9",
"output_format": "png",
"resolution": "2k",
"enable_base64_output": false,
"enable_sync_mode": false,
"enable_web_search": false,
"enable_image_search": false
}
```
### Available fields
| Field | Required | Default | Options |
|---|---|---|---|
| `prompt` | ✅ yes | — | any text |
| `aspect_ratio` | no | `16:9` | `1:1` | `4:3` | `3:4` | `16:9` | `9:16` | `21:9` |
| `resolution` | no | `2k` | `1k` | `2k` | `4k` |
| `output_format` | no | `png` | `png` | `jpeg` |
| `enable_web_search` | no | `false` | `true` | `false` |
| `enable_image_search` | no | `false` | `true` | `false` | | `enable_base64_output` | no | `false` | `true` | `false` |
| `enable_sync_mode` | no | `false` | `true` | `false` |
> **NÃO inclua** `media_resolution` — causa erro 500.
---
## Error Handling
| Erro | Causa provável | Solução |
|---|---|---|
| HTTP 500 | `media_resolution` presente no payload | Remova `media_resolution` do params.json |
| HTTP 500 | Token inválido ou expirado | Solicitar novo token ao usuário e atualizar memória |
| Link não atualiza | URL lida de lugar errado | Buscar a linha `IMAGE_URL:` no output desta execução |
| Timeout | Resolução muito alta | Tentar novamente com `"resolution": "1k"` |
| Job `failed` | Prompt inválido ou API instável | Simplificar o prompt e tentar novamente |
---
## Quando usar esta skill
- "Generate an image of..."
- "Create a picture of..."
- "Draw a scene with..."
- "Generate an image of..."
- "Create a photo of..."
- "Create an image with prompt..."
FILE:test.js
#!/usr/bin/env node
/**
* test.js — Default test for atlas-banana-textimage
* Usage: node test.js <TOKEN>
*/
const { spawnSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const os = require("os");
const token = process.argv[2] || process.env.ATLASCLOUD_API_KEY;
if (!token) {
console.error("❌ Token required: node test.js <TOKEN>");
process.exit(1);
}
const params = {
prompt: "Create an avatar of a blonde, slim girl with blue eyes and average height, wearing a bikini.",
aspect_ratio: "16:9",
enable_base64_output: false,
enable_sync_mode: false,
enable_web_search: false,
enable_image_search: false,
output_format: "png",
resolution: "1k",
// media_resolution removed — causes HTTP 500 when sent with resolution
};
const tmpFile = path.join(os.tmpdir(), `nanobanana_params_Date.now().json`);
fs.writeFileSync(tmpFile, JSON.stringify(params, null, 2));
console.log("🧪 Test: atlas-banana-textimage");
console.log(JSON.stringify(params, null, 2));
console.log();
const result = spawnSync(
process.execPath,
[path.join(__dirname, "generate.js"), token, tmpFile],
{ stdio: "inherit", env: { ...process.env } }
);
fs.unlinkSync(tmpFile);
// Print last_url.txt after script finishes
const urlPath = path.join(__dirname, "last_url.txt");
if (fs.existsSync(urlPath)) {
console.log("\n" + "=".repeat(60));
console.log("RESULT URL: " + fs.readFileSync(urlPath, "utf8").trim());
console.log("=".repeat(60));
}
process.exit(result.status ?? 0);
FILE:package-lock.json
{
"name": "atlas-banana-textimage",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "atlas-banana-textimage",
"version": "1.0.0",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
}
}
}
FILE:last_result.json
FILE:generate.js
#!/usr/bin/env node
/**
* atlas-banana-textimage
* Generates images from text prompts using the AtlasCloud Nanobanana model.
*
* Usage:
* node generate.js <TOKEN> <PARAMS_JSON_FILE>
*/
const https = require("https");
const http = require("http");
const fs = require("fs");
const path = require("path");
const BASE_URL = "https://api.atlascloud.ai/api/v1";
const MODEL = "google/nano-banana-2/text-to-image";
function log(...args) { console.log(...args); }
// ─── API request ──────────────────────────────────────────────────────────────
function apiRequest(url, method, apiKey, body) {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const lib = parsed.protocol === "https:" ? https : http;
const options = {
hostname: parsed.hostname,
path: parsed.pathname + parsed.search,
method,
headers: {
Authorization: `Bearer apiKey`,
"Content-Type": "application/json",
},
};
const req = lib.request(options, (res) => {
let data = "";
res.on("data", (c) => (data += c));
res.on("end", () => {
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
catch { resolve({ status: res.statusCode, body: data }); }
});
});
req.on("error", reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
// ─── Step 1: Submit ───────────────────────────────────────────────────────────
async function createJob(params, apiKey) {
// NOTE: never send media_resolution together with resolution — causes HTTP 500
const payload = {
model: MODEL,
prompt: params.prompt,
aspect_ratio: params.aspect_ratio ?? "16:9",
enable_base64_output: params.enable_base64_output ?? false,
enable_sync_mode: params.enable_sync_mode ?? false,
enable_web_search: params.enable_web_search ?? false,
enable_image_search: params.enable_image_search ?? false,
output_format: params.output_format ?? "png",
resolution: params.resolution ?? "2k",
};
log("Submitting request...");
log(`Prompt: payload.prompt.substring(0, 80)""`);
const res = await apiRequest(`BASE_URL/model/generateImage`, "POST", apiKey, payload);
const predictionId = res.body?.data?.id;
if (!predictionId) {
console.error("Failed to start job:", JSON.stringify(res.body, null, 2));
process.exit(1);
}
log(`Job started: predictionId`);
return predictionId;
}
// ─── Step 2: Poll ─────────────────────────────────────────────────────────────
async function pollResult(predictionId, apiKey, maxAttempts = 60, intervalMs = 3000) {
log("Polling for result...");
for (let i = 1; i <= maxAttempts; i++) {
await sleep(intervalMs);
const res = await apiRequest(`BASE_URL/model/prediction/predictionId`, "GET", apiKey);
const data = res.body?.data ?? res.body;
const status = data?.status ?? "unknown";
// Log the full GET response on every poll
log(`Attempt i/maxAttempts | status: status | response: JSON.stringify(res.body)`);
if (status === "completed" || status === "succeeded") {
const imageUrl = (data?.outputs ?? [])[0];
if (!imageUrl) {
console.error("Completed but no output URL:", JSON.stringify(res.body, null, 2));
process.exit(1);
}
log("=".repeat(60));
log("IMAGE_URL: " + imageUrl);
log("=".repeat(60));
return imageUrl;
}
if (status === "failed" || status === "error") {
console.error("Job failed:", JSON.stringify(res.body, null, 2));
process.exit(1);
}
}
console.error(`Timed out after maxAttempts attempts.`);
process.exit(1);
}
// ─── Main ─────────────────────────────────────────────────────────────────────
async function main() {
const apiKey = process.argv[2];
const paramsFile = process.argv[3];
if (!apiKey) {
console.error("Missing token. Usage: node generate.js <TOKEN> <PARAMS_JSON_FILE>");
process.exit(1);
}
if (!paramsFile || !fs.existsSync(paramsFile)) {
console.error("Missing or invalid params file. Usage: node generate.js <TOKEN> <PARAMS_JSON_FILE>");
process.exit(1);
}
const params = JSON.parse(fs.readFileSync(paramsFile, "utf8"));
if (!params.prompt) {
console.error("'prompt' is required in params file.");
process.exit(1);
}
const predictionId = await createJob(params, apiKey);
const imageUrl = await pollResult(predictionId, apiKey);
// Save result files next to this script
const resultPath = path.join(__dirname, "last_result.json");
const urlPath = path.join(__dirname, "last_url.txt");
fs.writeFileSync(resultPath, JSON.stringify({
prediction_id: predictionId,
image_url: imageUrl,
timestamp: new Date().toISOString(),
}, null, 2));
fs.writeFileSync(urlPath, imageUrl + "\n");
log("Result saved to: " + resultPath);
log("URL saved to: " + urlPath);
}
main().catch((err) => {
console.error("Unexpected error:", err.message);
process.exit(1);
});
FILE:package.json
{
"name": "atlas-banana-textimage",
"version": "1.0.0",
"description": "Generate images from text prompts using AtlasCloud Nanobanana model",
"main": "generate.js",
"scripts": {
"test": "node test.js",
"start": "node test.js",
"generate": "node generate.js --params-file params.json"
},
"engines": {
"node": ">=16.0.0"
},
"keywords": ["atlascloud", "nanobanana", "text-to-image", "ai", "image-generation"],
"license": "MIT",
"dependencies": {}
}
Edit or combine images by applying styles or elements from one image to another while preserving identity, pose, and lighting using AtlasCloud Nanobanana model.
---
name: atlas-banana-imagetoimage
description: Edit or combine images using the AtlasCloud Nanobanana Edit model. Use when the user wants to modify an image based on another, such as swapping clothes, merging styles, or applying visual elements from one image to another. Triggers on phrases like: "Changing clothes in the photo", "swap clothes", "edit image with reference", "combine images", "apply the style of one image to another".
metadata:
{
"openclaw":
{
"emoji": "🖼️",
"requires": { "bins": ["node", "npm"] },
"install":
[
{
"id": "npm",
"kind": "npm",
"package": "axios",
"bins": ["axios"],
"label": "Install axios for HTTP requests"
}
]
}
}
---
# Atlas Nanobanana Image-to-Image 🖼️
Edits and combines images using the AtlasCloud **Nanobanana 2 Edit** model (`google/nano-banana-2/edit`).
---
## Token Setup
Before generating images, you need the user's AtlasCloud API token.
- Check memory for `atlascloud_token`.
- If not found, ask the user: *"Please provide your AtlasCloud API token to get started."*
- Save the token to memory as `atlascloud_token` so it is not needed again.
---
## How to Generate an Image
**Step 1:** Write the params to `{baseDir}/params.json`.
**Step 2:** Run the script:
```bash
node {baseDir}/generate.js <TOKEN> {baseDir}/params.json
```
**Step 3 — REQUIRED:** After the script finishes, run this bash command to read the generated URL:
```bash
cat {baseDir}/last_url.txt
```
⚠️ **CRITICAL**: Step 3 is **mandatory and irreplaceable**. The correct URL is ONLY in `last_url.txt`. Run `cat` as a separate bash command and use the exact text returned. Never use a URL from the conversation history, previous files in the context, or any other source.
Report the URL from `last_url.txt` to the user.
---
## params.json — Payload Correto
⚠️ **IMPORTANT**: **Never include `media_resolution`** in the payload — it causes an HTTP 500 error.
```json
{
"prompt": "Replace the dress on the model in image 0 with the dress from image 1. Preserve identity, face, pose, and lighting.",
"images": [
"https://url-of-base-image.png",
"https://url-of-reference-image.png"
],
"aspect_ratio": "16:9",
"output_format": "png",
"resolution": "1k",
"enable_base64_output": false,
"enable_sync_mode": false,
"enable_web_search": false,
"enable_image_search": false
}
```
## Available Fields
| Field | Required | Default | Options |
|---|---|---|---|
| `prompt` | ✅ yes | — | any text |
| `images` | ✅ yes | — | array of 1–4 URLs |
| `aspect_ratio` | no | `16:9` | `1:1` | `4:3` | `3:4` | `16:9` | `9:16` | `21:9` |
| `resolution` | no | `1k` | `1k` | `2k` | `4k` |
| `output_format` | no | `png` | `png` | `jpeg` |
| `enable_web_search` | no | `false` | `true` | `false` |
| `enable_image_search` | no | `false` | `true` | `false` |
| `enable_base64_output` | no | `false` | `true` | `false` |
| `enable_sync_mode` | no | `false` | `true` | `false` |
**Do not include** `media_resolution` — it causes a 500 error.
---
## Prompt Tips for Image-to-Image
- Refer to images by position: "image 0" (base), "image 1" (reference).
- State clearly what to **preserve**: face, pose, proportions, lighting, background.
- State clearly what to **replace**: the clothing, the background, the style.
- Use negative instructions: "DO NOT change the face", "DO NOT transfer human elements from image 1".
---
## Error Handling
| Error | Probable Cause | Solution |
|---|---|---|
| HTTP 500 | `media_resolution` present in payload | Remove `media_resolution` from params.json |
| HTTP 500 | Invalid or expired token | Request a new token from the user and refresh memory |
| Link does not update | Step 3 was not executed | Run `cat {baseDir}/last_url.txt` as a bash command |
| Timeout | Resolution too high | Try again with `"resolution": "1k"` |
| Job `failed` | Image URLs inaccessible | Check if images are public |
---
## When to use this skill:
- "swap the clothes in this photo"
- "apply the style from image 1 to image 0"
- "edit this image using another as a reference"
- "change clothes in the photo"
- "combine two images with AI"
FILE:.env
ATLASCLOUD_API_KEY=apikey-8e638200e8554e6691207441b78bcd79
FILE:last_url.txt
https://atlas-media.oss-us-west-1.aliyuncs.com/images/1724989b-952e-4e5b-9761-4a4a16d5c041.png
FILE:package-lock.json
{
"name": "atlas-banana-imagetoimage",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "atlas-banana-imagetoimage",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"axios": "^1.6.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
}
}
}
FILE:last_result.json
{
"prediction_id": "0d2799c527ab4b9fbf87eb25218dc3e5",
"image_url": "https://atlas-media.oss-us-west-1.aliyuncs.com/images/1724989b-952e-4e5b-9761-4a4a16d5c041.png",
"timestamp": "2026-04-04T22:38:34.825Z",
"images": [
"https://atlas-img.oss-accelerate-overseas.aliyuncs.com/images/2de5930e-7d52-43e2-8e0b-d4b891e84c55.png",
"https://atlas-img.oss-accelerate-overseas.aliyuncs.com/images/4635a6c8-0d91-4176-817e-8f22ea5f86b7.png"
]
}
FILE:generate.js
#!/usr/bin/env node
/**
* atlas-banana-imagetoimage
* Edits/combines images using the AtlasCloud Nanobanana Edit model.
*
* Usage:
* node generate.js <TOKEN> <PARAMS_JSON_FILE>
*/
const axios = require("axios");
const fs = require("fs");
const path = require("path");
const BASE_URL = "https://api.atlascloud.ai/api/v1";
const MODEL = "google/nano-banana-2/edit";
function log(...args) { console.log(...args); }
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
// ─── Step 1: Submit ───────────────────────────────────────────────────────────
async function createJob(params, apiKey) {
// NOTE: never send media_resolution together with resolution — causes HTTP 500
const payload = {
model: MODEL,
prompt: params.prompt,
images: params.images,
aspect_ratio: params.aspect_ratio ?? "16:9",
enable_base64_output: params.enable_base64_output ?? false,
enable_sync_mode: params.enable_sync_mode ?? false,
enable_web_search: params.enable_web_search ?? false,
enable_image_search: params.enable_image_search ?? false,
output_format: params.output_format ?? "png",
resolution: params.resolution ?? "1k",
};
log("Submitting request...");
log(`Model : MODEL`);
log(`Images : payload.images.length URL(s)`);
payload.images.forEach((u, i) => log(` [i] u.substring(0, 80)""`));
log(`Prompt : payload.prompt.substring(0, 80)""`);
const res = await axios.post(`BASE_URL/model/generateImage`, payload, {
headers: { Authorization: `Bearer apiKey`, "Content-Type": "application/json" },
});
const predictionId = res.data?.data?.id;
if (!predictionId) {
console.error("Failed to start job:", JSON.stringify(res.data, null, 2));
process.exit(1);
}
log(`Job started: predictionId`);
return predictionId;
}
// ─── Step 2: Poll ─────────────────────────────────────────────────────────────
async function pollResult(predictionId, apiKey, maxAttempts = 60, intervalMs = 3000) {
log("Polling for result...");
for (let i = 1; i <= maxAttempts; i++) {
await sleep(intervalMs);
const res = await axios.get(`BASE_URL/model/prediction/predictionId`, {
headers: { Authorization: `Bearer apiKey` },
});
const data = res.data?.data ?? res.data;
const status = data?.status ?? "unknown";
// Log the full GET response on every poll
log(`Attempt i/maxAttempts | status: status | response: JSON.stringify(res.data)`);
if (status === "completed" || status === "succeeded") {
const imageUrl = (data?.outputs ?? [])[0];
if (!imageUrl) {
console.error("Completed but no output URL:", JSON.stringify(res.data, null, 2));
process.exit(1);
}
log("=".repeat(60));
log("IMAGE_URL: " + imageUrl);
log("=".repeat(60));
return imageUrl;
}
if (status === "failed" || status === "error") {
console.error("Job failed:", JSON.stringify(res.data, null, 2));
process.exit(1);
}
}
console.error(`Timed out after maxAttempts attempts.`);
process.exit(1);
}
// ─── Main ─────────────────────────────────────────────────────────────────────
async function main() {
const apiKey = process.argv[2];
const paramsFile = process.argv[3];
if (!apiKey) {
console.error("Missing token. Usage: node generate.js <TOKEN> <PARAMS_JSON_FILE>");
process.exit(1);
}
if (!paramsFile || !fs.existsSync(paramsFile)) {
console.error("Missing or invalid params file. Usage: node generate.js <TOKEN> <PARAMS_JSON_FILE>");
process.exit(1);
}
const params = JSON.parse(fs.readFileSync(paramsFile, "utf8"));
if (!params.prompt) {
console.error("'prompt' is required in params file.");
process.exit(1);
}
if (!Array.isArray(params.images) || params.images.length < 1) {
console.error("'images' must be an array with at least 1 URL.");
process.exit(1);
}
const predictionId = await createJob(params, apiKey);
const imageUrl = await pollResult(predictionId, apiKey);
// Save result files next to this script
const resultPath = path.join(__dirname, "last_result.json");
const urlPath = path.join(__dirname, "last_url.txt");
fs.writeFileSync(resultPath, JSON.stringify({
prediction_id: predictionId,
image_url: imageUrl,
timestamp: new Date().toISOString(),
images: params.images,
}, null, 2));
fs.writeFileSync(urlPath, imageUrl + "\n");
log("Result saved to: " + resultPath);
log("URL saved to: " + urlPath);
}
main().catch((err) => {
const msg = err.response?.data ? JSON.stringify(err.response.data, null, 2) : err.message;
console.error("Unexpected error:", msg);
process.exit(1);
});
FILE:package.json
{
"name": "atlas-banana-imagetoimage",
"version": "1.0.0",
"description": "Generate images from images e prompts using AtlasCloud Nanobanana model",
"main": "generate.js",
"scripts": {
"test": "node test.js",
"start": "node generate.js",
"generate": "node generate.js --params-file params.json"
},
"engines": {
"node": ">=16.0.0"
},
"author": "Guilherme Funchal",
"license": "MIT",
"keywords": ["atlascloud", "nanobanana", "image-to-image", "ai", "image-generation"],
"dependencies": {
"axios": "^1.6.0"
}
}
Looks up address data for a Brazilian CEP using the ViaCEP API.
---
name: cep-lookup
description: Looks up address data for a Brazilian CEP using the ViaCEP API.
metadata:
{
"openclaw":
{
"emoji": "📮",
"requires": { "bins": ["node", "npm"] },
"install":
[
{
"id": "npm",
"kind": "npm",
"package": "axios",
"bins": ["axios"],
"label": "Install axios for HTTP requests",
}
],
},
}
---
# CEP Lookup
Looks up the full address for a Brazilian postal code (CEP) using the public [ViaCEP](https://viacep.com.br) API. No authentication required.
## Triggers
Use this skill when the user mentions a CEP in any of these formats:
- `cep 01001-000`
- `CEP 20040020`
- `details for CEP 30140-110`
## Output
The skill returns:
- Street (logradouro)
- Neighborhood (bairro)
- City (localidade)
- State (UF)
- Complement, if available (complemento)
## Error handling
- Invalid CEP format → usage instruction
- CEP not found in ViaCEP database → informative message
- Network failure → generic error message
FILE:skill.json
{
"name": "CEP Lookup",
"description": "Skill that looks up address data for a Brazilian CEP using ViaCEP",
"version": "1.0.0",
"main": "index.js",
"triggers": [
{
"type": "message",
"pattern": "cep (\\d{5}-?\\d{3})"
}
]
}
FILE:test.js
/**
* test.js — Local test runner for the CEP Lookup skill
*
* Usage:
* node test.js → runs with real ViaCEP API calls
* node test.js --mock → runs with mocked responses (no internet needed)
*/
const MOCK_MODE = process.argv.includes('--mock');
// --- Mock axios in mock mode ---
if (MOCK_MODE) {
const Module = require('module');
const originalLoad = Module._load;
Module._load = function (request, ...args) {
if (request === 'axios') {
return {
get: async (url) => {
if (url.includes('01001000')) {
return {
data: {
logradouro: 'Praça da Sé',
bairro: 'Sé',
localidade: 'São Paulo',
uf: 'SP',
complemento: 'lado ímpar',
},
};
}
if (url.includes('00000000')) {
return { data: { erro: true } };
}
throw new Error('Network error');
},
};
}
return originalLoad(request, ...args);
};
}
const skill = require('./index');
// Helper: simulate the OpenClaw context object
function makeContext(text) {
return { message: { text } };
}
// --- Test cases ---
const tests = [
{
description: 'Valid CEP with dash',
input: 'cep 01001-000',
expect: (result) => result.includes('São Paulo'),
},
{
description: 'Valid CEP without dash',
input: 'cep 01001000',
expect: (result) => result.includes('São Paulo'),
},
{
description: 'English-style trigger',
input: 'What are the details for CEP 01001-000',
expect: (result) => result.includes('São Paulo'),
},
{
description: 'Uppercase CEP keyword',
input: 'CEP 01001-000',
expect: (result) => result.includes('São Paulo'),
},
{
description: 'CEP not found',
input: 'cep 00000-000',
expect: (result) => result.includes('not found'),
},
{
description: 'Invalid format (too few digits)',
input: 'cep 1234',
expect: (result) => result.includes('Invalid CEP format'),
},
{
description: 'No CEP in message',
input: 'hello there',
expect: (result) => result.includes('Invalid CEP format'),
},
];
// --- Runner ---
async function run() {
const mode = MOCK_MODE ? 'MOCK' : 'LIVE';
console.log(`\nRunning CEP Skill tests... [mode mode]\n`);
let passed = 0;
let failed = 0;
for (const test of tests) {
process.stdout.write(` test.description ... `);
try {
const result = await skill(makeContext(test.input));
const ok = test.expect(result);
if (ok) {
console.log('✅ PASS');
passed++;
} else {
console.log('❌ FAIL');
failed++;
}
// Always show the input and result
console.log(` Input: "test.input"`);
console.log(` Result:`);
result.split('\n').forEach((line) => console.log(` line`));
} catch (err) {
console.log('💥 ERROR');
console.log(` err.message`);
failed++;
}
}
console.log(`\nResults: passed passed, failed failed out of tests.length tests.\n`);
process.exit(failed > 0 ? 1 : 0);
}
run();
FILE:index.js
const axios = require('axios');
module.exports = async function(context) {
const message = context.message.text;
// Supports: "cep 01001-000", "CEP 01001000" and "details for CEP 01001-000"
const match = message.match(/cep[:\s]+(\d{5}-?\d{3})/i);
if (!match) {
return "Invalid CEP format. Try something like: cep 01001-000";
}
const cep = match[1].replace("-", "");
try {
const response = await axios.get(`https://viacep.com.br/ws/cep/json/`);
const data = response.data;
if (data.erro) {
return `CEP cep not found.`;
}
return `Details for CEP cep:
- Street: data.logradouro
- Neighborhood: data.bairro
- City: data.localidade
- State: data.uf
- Complement: data.complemento || "none"`;
} catch (err) {
console.error(err);
return "Error looking up CEP. Please try again later.";
}
};
FILE:README.md
# CEP Lookup Skill for OpenClaw 📮
This OpenClaw skill allows users to look up Brazilian postal codes (CEP) quickly and easily using the [ViaCEP API](https://viacep.com.br).
It returns detailed address information including **street (logradouro), neighborhood (bairro), city (localidade), state (uf), and complement**.
---
## Features
- Query any valid Brazilian CEP.
- Returns complete address information.
- Easy to install and use with OpenClaw.
- No API key required – uses the public ViaCEP service.
---
## Installation
1. Copy the folder to the OpenClaw skills directory:
```bash
cp -r cepSkill ~/.openclaw/workspace/skills/
cd ~/.openclaw/workspace/skills/cepSkill
npm install
```
---
## Usage
Once installed, you can use the skill by simply asking:
```
cep 01001-000
```
or
```
What are the details for CEP 20040-020
```
The skill will return a message like:
```
Details for CEP 01001000:
- Street: Praça da Sé
- Neighborhood: Sé
- City: São Paulo
- State: SP
- Complement: none
```
## Example Queries
```
cep 30140-110
What are the details for CEP 20040-020
```
## Notes
- The skill only works with valid Brazilian CEPs.
- Ensure your OpenClaw instance has internet access to reach the ViaCEP API.
- No authentication is required to use the ViaCEP service.
- The trigger pattern supports both Portuguese (`cep XXXXX-XXX`) and English (`details for CEP XXXXX-XXX`).
## Dependencies
- [axios](https://www.npmjs.com/package/axios) – for HTTP requests.
Install dependencies with:
```bash
npm install
```
## Contributing
Feel free to fork the repository, improve the skill, or add additional features for Brazilian address lookup.
## License
MIT License
FILE:package.json
{
"name": "cep-skill",
"version": "1.0.0",
"main": "index.js",
"author": "Guilherme Funchal",
"license": "MIT",
"dependencies": {
"axios": "^1.6.0"
}
}