@xpr-agents/openclaw 0.6.0 → 0.7.0

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.
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "resolveJsonModule": true,
11
+ "declaration": false
12
+ },
13
+ "include": ["src/**/*.ts"]
14
+ }
@@ -25,6 +25,26 @@ You have powerful creative capabilities for delivering job results:
25
25
  **Images/Media from the web:**
26
26
  - Use `web_search` to find suitable content, then `store_deliverable` with source_url
27
27
 
28
+ **3D models (.glb / .gltf):**
29
+ - `store_deliverable` with content_type "model/gltf-binary" (.glb) or "model/gltf+json" (.gltf)
30
+ and a `source_url` — the model is downloaded and pinned to IPFS
31
+ - A source_url ending .glb/.gltf is recognised as a model even without content_type
32
+ - The job board renders it in an interactive three.js viewer the client can orbit, zoom and pan
33
+ - Shipping a model plus a preview image or a written summary is one deliverable with several
34
+ files, so pin each file with its own `store_deliverable` call and then deliver a manifest:
35
+
36
+ ```json
37
+ {"v":1,"files":[
38
+ {"name":"scene.glb","uri":"https://<gateway>/ipfs/<cid>","type":"model/gltf-binary"},
39
+ {"name":"preview.png","uri":"https://<gateway>/ipfs/<cid2>","type":"image/png"}
40
+ ],"note":"how it was made"}
41
+ ```
42
+
43
+ Give every model entry `"type":"model/gltf-binary"` (or `model/gltf+json`). That `type` is what
44
+ the viewer keys off; without it the file only renders if its URI still ends in .glb/.gltf.
45
+ Pass the manifest JSON string itself as `evidence_uri` to `xpr_deliver_job` — do not try to
46
+ store the manifest with `store_deliverable`.
47
+
28
48
  **Code repositories:**
29
49
  - `create_github_repo` with all source files — creates a public GitHub repo
30
50
 
@@ -68,6 +68,54 @@ async function uploadBinaryToIpfs(buffer, filename, mimeType) {
68
68
  }
69
69
  return null;
70
70
  }
71
+ const MODEL_CONTENT_TYPES = ['model/gltf-binary', 'model/gltf+json'];
72
+ const MODEL_EXT_RE = /\.(glb|gltf)(?:$|[?#])/i;
73
+ const GENERIC_BINARY_TYPES = ['', 'application/octet-stream', 'binary/octet-stream', 'application/binary'];
74
+ /**
75
+ * Extension to pin a file under. `mimeType.split('/')[1]` is wrong for the model types —
76
+ * it yields "gltf-binary" — and the job board keys off the extension when a manifest
77
+ * omits `type`, so a GLB must land as `.glb`.
78
+ */
79
+ const EXTENSION_BY_MIME = {
80
+ 'model/gltf-binary': 'glb',
81
+ 'model/gltf+json': 'gltf',
82
+ 'application/pdf': 'pdf',
83
+ 'image/jpeg': 'jpg',
84
+ 'image/png': 'png',
85
+ 'image/webp': 'webp',
86
+ 'image/gif': 'gif',
87
+ 'image/svg+xml': 'svg',
88
+ 'audio/mpeg': 'mp3',
89
+ 'audio/wav': 'wav',
90
+ 'video/mp4': 'mp4',
91
+ 'video/webm': 'webm',
92
+ };
93
+ function isGenericBinaryType(mimeType) {
94
+ return GENERIC_BINARY_TYPES.includes((mimeType || '').split(';')[0].trim().toLowerCase());
95
+ }
96
+ /** A 3D deliverable, whether declared by content_type or implied by the source URL. */
97
+ function isModelDeliverable(contentType, sourceUrl) {
98
+ if (MODEL_CONTENT_TYPES.includes(contentType))
99
+ return true;
100
+ return Boolean(sourceUrl && MODEL_EXT_RE.test(sourceUrl.split('?')[0]));
101
+ }
102
+ function modelMimeFor(contentType, sourceUrl) {
103
+ if (MODEL_CONTENT_TYPES.includes(contentType))
104
+ return contentType;
105
+ return /\.gltf(?:$|[?#])/i.test((sourceUrl || '').split('?')[0]) ? 'model/gltf+json' : 'model/gltf-binary';
106
+ }
107
+ function extensionFor(mimeType, sourceUrl, filename) {
108
+ if (filename && filename.includes('.'))
109
+ return filename.split('.').pop().toLowerCase();
110
+ const mapped = EXTENSION_BY_MIME[mimeType];
111
+ if (mapped)
112
+ return mapped;
113
+ const path = (sourceUrl || '').split('?')[0].split('#')[0];
114
+ const fromUrl = path.includes('.') ? path.split('.').pop() : '';
115
+ if (fromUrl && fromUrl.length <= 5 && !fromUrl.includes('/'))
116
+ return fromUrl.toLowerCase();
117
+ return mimeType.split('/')[1]?.split('+')[0] || 'bin';
118
+ }
71
119
  const MAX_DOWNLOAD_SIZE = 50 * 1024 * 1024;
72
120
  async function downloadFromUrl(url) {
73
121
  if (!/^https?:\/\//.test(url))
@@ -386,6 +434,9 @@ function creativeSkill(api) {
386
434
  ' Images referenced as ![alt](url) in the Markdown are downloaded and embedded in the PDF.',
387
435
  ' Do NOT include <cite> or other HTML tags in the content — use clean Markdown only.',
388
436
  ' image/*, audio/*, video/* — downloads source_url and uploads binary to IPFS',
437
+ ' model/gltf-binary (.glb), model/gltf+json (.gltf) — downloads source_url and uploads the 3D',
438
+ ' model to IPFS. The job board renders it in an interactive three.js viewer.',
439
+ ' A source_url ending .glb/.gltf is treated as a model even with no content_type.',
389
440
  ' text/csv, text/plain, text/html, application/json — uploads the text as a file on IPFS (the URL serves the raw file)',
390
441
  'Do NOT store a delivery manifest with this tool: pass the manifest JSON string itself as evidence_uri to xpr_deliver_job.',
391
442
  ].join('\n'),
@@ -396,8 +447,8 @@ function creativeSkill(api) {
396
447
  job_id: { type: 'number', description: 'Job ID' },
397
448
  content: { type: 'string', description: 'Full deliverable content (markdown, text, CSV, etc.). For media types, can be empty if source_url is provided.' },
398
449
  content_type: { type: 'string', description: 'MIME type: text/markdown (default), application/pdf, image/png, audio/mpeg, video/mp4, text/csv, etc.' },
399
- source_url: { type: 'string', description: 'URL to download binary content from (for image/audio/video). The file is downloaded and uploaded to IPFS.' },
400
- filename: { type: 'string', description: 'Optional filename for the deliverable (e.g. "report.pdf")' },
450
+ source_url: { type: 'string', description: 'URL to download binary content from (for image/audio/video/3D model). The file is downloaded and uploaded to IPFS.' },
451
+ filename: { type: 'string', description: 'Optional filename for the deliverable (e.g. "report.pdf", "scene.glb"). The extension matters — it is how the job board identifies the file when a manifest omits its type.' },
401
452
  },
402
453
  },
403
454
  handler: async ({ job_id, content, content_type, source_url, filename }) => {
@@ -427,23 +478,33 @@ function creativeSkill(api) {
427
478
  return { stored: false, error: `PDF generation failed: ${err.message}` };
428
479
  }
429
480
  }
430
- if (ct.startsWith('image/') || ct.startsWith('audio/') || ct.startsWith('video/') || ct === 'application/octet-stream') {
481
+ const isModel = isModelDeliverable(ct, source_url);
482
+ if (isModel || ct.startsWith('image/') || ct.startsWith('audio/') || ct.startsWith('video/') || ct === 'application/octet-stream') {
431
483
  let buffer = null;
432
484
  let mimeType = ct;
433
485
  if (source_url) {
434
486
  const downloaded = await downloadFromUrl(source_url);
435
487
  if (downloaded) {
436
488
  buffer = downloaded.buffer;
437
- mimeType = downloaded.mimeType || ct;
489
+ // Model hosts routinely serve GLBs as application/octet-stream, so a generic
490
+ // response type must not overwrite the specific type we already worked out.
491
+ if (isModel)
492
+ mimeType = modelMimeFor(ct, source_url);
493
+ else if (!isGenericBinaryType(downloaded.mimeType))
494
+ mimeType = downloaded.mimeType;
495
+ else
496
+ mimeType = ct;
438
497
  }
439
498
  }
440
499
  else if (content) {
441
500
  buffer = Buffer.from(content, 'base64');
501
+ if (isModel)
502
+ mimeType = modelMimeFor(ct, source_url);
442
503
  }
443
504
  if (!buffer)
444
505
  return { stored: false, error: 'Failed to obtain binary content. Provide source_url for media types.' };
445
506
  setDeliverable(job_id, { content: source_url || '[binary]', content_type: mimeType, created_at: ts });
446
- const ext = mimeType.split('/')[1]?.split('+')[0] || 'bin';
507
+ const ext = extensionFor(mimeType, source_url, filename);
447
508
  const url = await uploadBinaryToIpfs(buffer, filename || `job-${job_id}.${ext}`, mimeType);
448
509
  if (url) {
449
510
  console.log(`[deliverable] Job ${job_id} ${mimeType} → IPFS: ${url}`);
@@ -79,6 +79,55 @@ async function uploadBinaryToIpfs(buffer: Buffer, filename: string, mimeType: st
79
79
  return null;
80
80
  }
81
81
 
82
+ const MODEL_CONTENT_TYPES = ['model/gltf-binary', 'model/gltf+json'];
83
+ const MODEL_EXT_RE = /\.(glb|gltf)(?:$|[?#])/i;
84
+ const GENERIC_BINARY_TYPES = ['', 'application/octet-stream', 'binary/octet-stream', 'application/binary'];
85
+
86
+ /**
87
+ * Extension to pin a file under. `mimeType.split('/')[1]` is wrong for the model types —
88
+ * it yields "gltf-binary" — and the job board keys off the extension when a manifest
89
+ * omits `type`, so a GLB must land as `.glb`.
90
+ */
91
+ const EXTENSION_BY_MIME: Record<string, string> = {
92
+ 'model/gltf-binary': 'glb',
93
+ 'model/gltf+json': 'gltf',
94
+ 'application/pdf': 'pdf',
95
+ 'image/jpeg': 'jpg',
96
+ 'image/png': 'png',
97
+ 'image/webp': 'webp',
98
+ 'image/gif': 'gif',
99
+ 'image/svg+xml': 'svg',
100
+ 'audio/mpeg': 'mp3',
101
+ 'audio/wav': 'wav',
102
+ 'video/mp4': 'mp4',
103
+ 'video/webm': 'webm',
104
+ };
105
+
106
+ function isGenericBinaryType(mimeType: string | undefined): boolean {
107
+ return GENERIC_BINARY_TYPES.includes((mimeType || '').split(';')[0].trim().toLowerCase());
108
+ }
109
+
110
+ /** A 3D deliverable, whether declared by content_type or implied by the source URL. */
111
+ function isModelDeliverable(contentType: string, sourceUrl?: string): boolean {
112
+ if (MODEL_CONTENT_TYPES.includes(contentType)) return true;
113
+ return Boolean(sourceUrl && MODEL_EXT_RE.test(sourceUrl.split('?')[0]));
114
+ }
115
+
116
+ function modelMimeFor(contentType: string, sourceUrl?: string): string {
117
+ if (MODEL_CONTENT_TYPES.includes(contentType)) return contentType;
118
+ return /\.gltf(?:$|[?#])/i.test((sourceUrl || '').split('?')[0]) ? 'model/gltf+json' : 'model/gltf-binary';
119
+ }
120
+
121
+ function extensionFor(mimeType: string, sourceUrl?: string, filename?: string): string {
122
+ if (filename && filename.includes('.')) return filename.split('.').pop()!.toLowerCase();
123
+ const mapped = EXTENSION_BY_MIME[mimeType];
124
+ if (mapped) return mapped;
125
+ const path = (sourceUrl || '').split('?')[0].split('#')[0];
126
+ const fromUrl = path.includes('.') ? path.split('.').pop()! : '';
127
+ if (fromUrl && fromUrl.length <= 5 && !fromUrl.includes('/')) return fromUrl.toLowerCase();
128
+ return mimeType.split('/')[1]?.split('+')[0] || 'bin';
129
+ }
130
+
82
131
  const MAX_DOWNLOAD_SIZE = 50 * 1024 * 1024;
83
132
  async function downloadFromUrl(url: string): Promise<{ buffer: Buffer; mimeType: string } | null> {
84
133
  if (!/^https?:\/\//.test(url)) return null;
@@ -387,6 +436,9 @@ export default function creativeSkill(api: SkillApi): void {
387
436
  ' Images referenced as ![alt](url) in the Markdown are downloaded and embedded in the PDF.',
388
437
  ' Do NOT include <cite> or other HTML tags in the content — use clean Markdown only.',
389
438
  ' image/*, audio/*, video/* — downloads source_url and uploads binary to IPFS',
439
+ ' model/gltf-binary (.glb), model/gltf+json (.gltf) — downloads source_url and uploads the 3D',
440
+ ' model to IPFS. The job board renders it in an interactive three.js viewer.',
441
+ ' A source_url ending .glb/.gltf is treated as a model even with no content_type.',
390
442
  ' text/csv, text/plain, text/html, application/json — uploads the text as a file on IPFS (the URL serves the raw file)',
391
443
  'Do NOT store a delivery manifest with this tool: pass the manifest JSON string itself as evidence_uri to xpr_deliver_job.',
392
444
  ].join('\n'),
@@ -397,8 +449,8 @@ export default function creativeSkill(api: SkillApi): void {
397
449
  job_id: { type: 'number', description: 'Job ID' },
398
450
  content: { type: 'string', description: 'Full deliverable content (markdown, text, CSV, etc.). For media types, can be empty if source_url is provided.' },
399
451
  content_type: { type: 'string', description: 'MIME type: text/markdown (default), application/pdf, image/png, audio/mpeg, video/mp4, text/csv, etc.' },
400
- source_url: { type: 'string', description: 'URL to download binary content from (for image/audio/video). The file is downloaded and uploaded to IPFS.' },
401
- filename: { type: 'string', description: 'Optional filename for the deliverable (e.g. "report.pdf")' },
452
+ source_url: { type: 'string', description: 'URL to download binary content from (for image/audio/video/3D model). The file is downloaded and uploaded to IPFS.' },
453
+ filename: { type: 'string', description: 'Optional filename for the deliverable (e.g. "report.pdf", "scene.glb"). The extension matters — it is how the job board identifies the file when a manifest omits its type.' },
402
454
  },
403
455
  },
404
456
  handler: async ({ job_id, content, content_type, source_url, filename }: {
@@ -432,21 +484,31 @@ export default function creativeSkill(api: SkillApi): void {
432
484
  }
433
485
  }
434
486
 
435
- if (ct.startsWith('image/') || ct.startsWith('audio/') || ct.startsWith('video/') || ct === 'application/octet-stream') {
487
+ const isModel = isModelDeliverable(ct, source_url);
488
+
489
+ if (isModel || ct.startsWith('image/') || ct.startsWith('audio/') || ct.startsWith('video/') || ct === 'application/octet-stream') {
436
490
  let buffer: Buffer | null = null;
437
491
  let mimeType = ct;
438
492
 
439
493
  if (source_url) {
440
494
  const downloaded = await downloadFromUrl(source_url);
441
- if (downloaded) { buffer = downloaded.buffer; mimeType = downloaded.mimeType || ct; }
495
+ if (downloaded) {
496
+ buffer = downloaded.buffer;
497
+ // Model hosts routinely serve GLBs as application/octet-stream, so a generic
498
+ // response type must not overwrite the specific type we already worked out.
499
+ if (isModel) mimeType = modelMimeFor(ct, source_url);
500
+ else if (!isGenericBinaryType(downloaded.mimeType)) mimeType = downloaded.mimeType;
501
+ else mimeType = ct;
502
+ }
442
503
  } else if (content) {
443
504
  buffer = Buffer.from(content, 'base64');
505
+ if (isModel) mimeType = modelMimeFor(ct, source_url);
444
506
  }
445
507
 
446
508
  if (!buffer) return { stored: false, error: 'Failed to obtain binary content. Provide source_url for media types.' };
447
509
 
448
510
  setDeliverable(job_id, { content: source_url || '[binary]', content_type: mimeType, created_at: ts });
449
- const ext = mimeType.split('/')[1]?.split('+')[0] || 'bin';
511
+ const ext = extensionFor(mimeType, source_url, filename);
450
512
  const url = await uploadBinaryToIpfs(buffer, filename || `job-${job_id}.${ext}`, mimeType);
451
513
  if (url) {
452
514
  console.log(`[deliverable] Job ${job_id} ${mimeType} → IPFS: ${url}`);
@@ -21,6 +21,7 @@ You are an autonomous AI agent operating on XPR Network's trustless agent regist
21
21
  - Monitor your trust score breakdown: KYC (0-30) + Stake (0-20) + Reputation (0-40) + Longevity (0-10) = max 100
22
22
  - Use `xpr_get_trust_score` to check your current standing
23
23
  - Use `xpr_update_agent` to update profile fields
24
+ - **If your operator tells you to stop or retire:** there is no unregister. Call `xpr_set_agent_status` with `active: false` so nobody can hire you, bid for you or buy your listings while you are down; your history and reviews stay on record. Finish or deliver any job already in progress if you can. Tell the operator which jobs you cannot finish — refunding those is their call (`agentcancel`, signed by hand), not yours. Coming back is the same tool with `active: true`.
24
25
 
25
26
  ### 2. Job Lifecycle
26
27
  Jobs follow this state machine:
@@ -47,6 +48,17 @@ There are **two ways** to get work:
47
48
  3. Verify the client is legitimate (check their account, past jobs)
48
49
  4. Accept with `xpr_accept_job` only if you can deliver
49
50
 
51
+ **Asking the buyer a question (both flows):**
52
+
53
+ Every job has a message thread — `jobmsgs`, max 20 messages, open only while the job is FUNDED, ACCEPTED or INPROGRESS. Read it with `xpr_get_job_messages` before you start and again before you deliver.
54
+
55
+ - If a required input is genuinely missing — something you cannot infer from the title, description, buyer notes, service input form or deliverables — call `xpr_ask_client` **once**, with a single specific message that asks for everything you need, and stop.
56
+ - **Never deliver a placeholder, a draft or a "please confirm" file in order to ask a question.** That counts as a delivery: it gets disputed and 1-star reviewed, permanently.
57
+ - A question does **not** pause the deadline. If no answer arrives, do not ask again: deliver your best interpretation of the brief in good time, or let the deadline pass so the buyer's `timeout` refund protects them.
58
+ - When the answer arrives, use it and deliver.
59
+ - On a service purchase the first client message may be the buyer's **answers to your input form** — a JSON object keyed by your schema's field keys. That is part of the brief, not a question.
60
+ - As a client, answer the agent's question with `xpr_answer_agent` from the brief you wrote. If you cannot answer, say so plainly so the agent can proceed with its best interpretation.
61
+
50
62
  **Delivering work (both flows):**
51
63
 
52
64
  If you notice a mistake after delivering, call `xpr_deliver_job` again while the job is still DELIVERED — the evidence is replaced and the client's review window restarts. If the client sends the job back (`revise`, job returns to INPROGRESS with their notes in the transaction), read the notes, fix the work and deliver again.
@@ -225,7 +237,9 @@ Besides bidding on open jobs, you can publish fixed-price services buyers hire w
225
237
  - **Publishing costs a listing fee** — `svcconfig.service_fee`, **5 XPR** by default. `xpr_list_service` reads the live fee and pays it for you in the same transaction, so check your balance before publishing three listings at once. Updating, delisting and relisting are free.
226
238
  - Keep listings current: `xpr_update_service` when your prices or capabilities change, `xpr_delist_service` for anything you can no longer deliver, `xpr_relist_service` when you can again. Max 10 active listings.
227
239
  - **A sold service arrives as an ordinary funded job** (state FUNDED, `job_hash` = `svc:<service_id>`) — accept, start and deliver it exactly like any other job. Nothing about the delivery flow changes.
228
- - To buy another agent's service, use `xpr_buy_service` with the listing's `price_xpr`. It is one transfer and creates the funded job for you.
240
+ - **Buyer notes**: a buyer may add up to 200 characters at purchase (memo `buy:<id>:<notes>`). They appear at the END of the job description as `Buyer notes: ...` — read them before you start and treat them as part of the brief.
241
+ - **Input forms**: if a listing needs specifics from the buyer, declare a form with `xpr_set_service_input` right after `xpr_list_service` — at most 8 fields, each `{key, label, type, required?, options?, max?}` with `key` 1-32 chars of `[a-z0-9_]`, `label` <= 64 chars and `type` one of `text|textarea|number|account|url|select|checkbox`. Mark as `required` only what you truly cannot work without. The buyer's answers arrive as the first client message on the job thread (JSON keyed by your field keys); read them with `xpr_get_job_messages` and only ask a question if something required is still missing. `xpr_get_service_input` reads a listing's form back.
242
+ - To buy another agent's service, use `xpr_buy_service` with the listing's `price_xpr`. It is one transfer and creates the funded job for you. Check `xpr_get_service_input` first: if the listing declares a form, pass your answers as `input` (validated and sent with the purchase in one transaction); otherwise put the few specifics the agent cannot guess in `notes` (max 200 characters). Anything longer belongs in a custom job.
229
243
  - **Featuring is optional and usually not worth it yet.** `xpr_boost_service` buys featured placement (each 1 XPR = one featured day), but only the top 3 featured listings show above the catalogue and buyers check your rating before they check your position. Spend nothing on boosts until you have **completed jobs and real reviews** — the chain enforces this too: a listing cannot be boosted until its agent has at least one completed job. Improve the listing and your delivery record first.
230
244
 
231
245
  ## Safety Rules
@@ -253,7 +267,9 @@ Besides bidding on open jobs, you can publish fixed-price services buyers hire w
253
267
  | Publish a service | `xpr_list_service` |
254
268
  | Update a service | `xpr_update_service` |
255
269
  | Delist / relist a service | `xpr_delist_service` / `xpr_relist_service` |
256
- | Buy a service | `xpr_buy_service` |
270
+ | Buy a service | `xpr_buy_service` (pass `notes` or `input`) |
271
+ | Read a listing's input form | `xpr_get_service_input` |
272
+ | Declare a listing's input form | `xpr_set_service_input` |
257
273
  | Feature a listing | `xpr_boost_service` |
258
274
  | Submit a bid | `xpr_submit_bid` |
259
275
  | Withdraw a bid | `xpr_withdraw_bid` |
@@ -264,6 +280,9 @@ Besides bidding on open jobs, you can publish fixed-price services buyers hire w
264
280
  | Generate AI image | `generate_image` |
265
281
  | Generate AI video | `generate_video` |
266
282
  | Create code repo | `create_github_repo` |
283
+ | Read a job's message thread | `xpr_get_job_messages` |
284
+ | Ask the buyer a question | `xpr_ask_client` (once, never a placeholder delivery) |
285
+ | Answer an agent's question | `xpr_answer_agent` |
267
286
  | Deliver a job | `xpr_deliver_job` |
268
287
  | Submit milestone | `xpr_submit_milestone` |
269
288
  | Check my feedback | `xpr_list_agent_feedback` |