@xpr-agents/openclaw 0.5.4 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,20 @@
1
1
  "use strict";
2
2
  /**
3
- * Escrow tools (20 tools)
3
+ * Escrow tools (37 tools)
4
4
  * Reads: xpr_get_job, xpr_list_jobs, xpr_list_open_jobs, xpr_get_milestones,
5
- * xpr_get_job_dispute, xpr_list_arbitrators, xpr_list_bids
5
+ * xpr_get_job_dispute, xpr_list_arbitrators, xpr_list_bids,
6
+ * xpr_get_job_messages, xpr_get_service, xpr_list_services,
7
+ * xpr_get_service_input
6
8
  * Writes: xpr_create_job, xpr_fund_job, xpr_accept_job, xpr_start_job,
7
- * xpr_deliver_job, xpr_revise_job, xpr_approve_delivery, xpr_raise_dispute,
9
+ * xpr_deliver_job, xpr_deliver_job_nft, xpr_revise_job,
10
+ * xpr_approve_delivery, xpr_raise_dispute,
8
11
  * xpr_claim_timeout, xpr_cancel_job,
9
12
  * xpr_submit_milestone, xpr_arbitrate, xpr_resolve_timeout,
10
- * xpr_submit_bid, xpr_select_bid, xpr_withdraw_bid
13
+ * xpr_submit_bid, xpr_select_bid, xpr_withdraw_bid,
14
+ * xpr_ask_client, xpr_answer_agent,
15
+ * xpr_list_service, xpr_update_service, xpr_delist_service,
16
+ * xpr_relist_service, xpr_set_service_input,
17
+ * xpr_buy_service, xpr_boost_service
11
18
  */
12
19
  Object.defineProperty(exports, "__esModule", { value: true });
13
20
  exports.registerEscrowTools = registerEscrowTools;
@@ -23,6 +30,46 @@ function jobToXpr(job) {
23
30
  released_amount_xpr: typeof job.released_amount === 'number' ? job.released_amount / 10000 : job.released_amount,
24
31
  };
25
32
  }
33
+ /** Convert a service row's raw amounts to XPR and flag featured placement */
34
+ function serviceToXpr(service) {
35
+ const now = Math.floor(Date.now() / 1000);
36
+ const featuredUntil = typeof service.featuredUntil === 'number' ? service.featuredUntil : 0;
37
+ const boostPaid = typeof service.boostPaid === 'number' ? service.boostPaid : 0;
38
+ return {
39
+ ...service,
40
+ price_xpr: typeof service.price === 'number' ? service.price / 10000 : service.price,
41
+ boost_paid_xpr: boostPaid / 10000,
42
+ featured: featuredUntil > now,
43
+ };
44
+ }
45
+ /** Contract default listing fee (5 XPR) — used when svcconfig is unreadable */
46
+ const DEFAULT_SERVICE_FEE_RAW = 50000;
47
+ /**
48
+ * True when a transact() failure looks like the session refusing a
49
+ * multi-action transaction rather than the chain rejecting the actions.
50
+ * Only then is retrying as two sequential transactions safe — an EOSIO
51
+ * transaction is atomic, so a chain-level failure applied nothing and must
52
+ * surface to the caller instead of being silently retried.
53
+ */
54
+ function isMultiActionUnsupported(err) {
55
+ const message = err instanceof Error ? err.message : String(err);
56
+ return /multi-?action|multiple actions|single action|one action|batch(ing)? not supported|unsupported action list/i.test(message);
57
+ }
58
+ /** Accept a JSON-encoded array as well as a real array (models send both) */
59
+ function normalizeDeliverables(deliverables) {
60
+ if (Array.isArray(deliverables))
61
+ return deliverables;
62
+ if (typeof deliverables === 'string') {
63
+ try {
64
+ const parsed = JSON.parse(deliverables);
65
+ if (Array.isArray(parsed))
66
+ return parsed;
67
+ }
68
+ catch { /* fall through */ }
69
+ return [deliverables];
70
+ }
71
+ return [];
72
+ }
26
73
  function bidToXpr(bid) {
27
74
  return {
28
75
  ...bid,
@@ -117,6 +164,28 @@ function registerEscrowTools(api, config) {
117
164
  return { milestones, count: milestones.length };
118
165
  },
119
166
  });
167
+ api.registerTool({
168
+ name: 'xpr_get_job_messages',
169
+ description: 'Read a job\'s question-and-answer thread (the jobmsgs table), oldest message first. The agent asks with xpr_ask_client, the client replies with xpr_answer_agent. At most 20 messages per job. Use this before delivering to check whether a question was answered.',
170
+ parameters: {
171
+ type: 'object',
172
+ required: ['job_id'],
173
+ properties: {
174
+ job_id: { type: 'number', description: 'Job ID' },
175
+ },
176
+ },
177
+ handler: async ({ job_id }) => {
178
+ (0, validate_1.validatePositiveInt)(job_id, 'job_id');
179
+ const registry = new sdk_1.EscrowRegistry(config.rpc, undefined, contracts.agentescrow);
180
+ const messages = await registry.getJobMessages(job_id);
181
+ const last = messages[messages.length - 1];
182
+ return {
183
+ messages,
184
+ count: messages.length,
185
+ last_author: last ? last.author : null,
186
+ };
187
+ },
188
+ });
120
189
  api.registerTool({
121
190
  name: 'xpr_get_job_dispute',
122
191
  description: 'Get the dispute associated with a job, if any.',
@@ -402,6 +471,58 @@ function registerEscrowTools(api, config) {
402
471
  return registry.reviseJob(job_id, notes);
403
472
  },
404
473
  });
474
+ api.registerTool({
475
+ name: 'xpr_ask_client',
476
+ description: 'Ask the client a question about a job you are assigned to (agentescrow askclient). Valid while the job is FUNDED, ACCEPTED or INPROGRESS, max 20 messages per job. Use this ONCE, with one specific question, when a required input is genuinely missing — never deliver a placeholder to ask a question. The question does NOT pause the deadline: if no answer arrives, either deliver your best interpretation or let the deadline pass and the buyer be refunded.',
477
+ parameters: {
478
+ type: 'object',
479
+ required: ['job_id', 'text'],
480
+ properties: {
481
+ job_id: { type: 'number', description: 'Job ID you are assigned to' },
482
+ text: { type: 'string', description: 'The question (1-512 characters). Be specific and ask everything you need in one message.' },
483
+ confirmed: { type: 'boolean', description: 'Set to true to execute after reviewing the confirmation prompt' },
484
+ },
485
+ },
486
+ handler: async ({ job_id, text, confirmed }) => {
487
+ if (!config.session)
488
+ throw new Error('Session required: set XPR_ACCOUNT and ensure proton CLI has the account key in its keychain');
489
+ (0, validate_1.validatePositiveInt)(job_id, 'job_id');
490
+ (0, validate_1.validateRequired)(text, 'text');
491
+ if (text.length > 512)
492
+ throw new Error('text must be at most 512 characters');
493
+ const confirmation = (0, confirm_1.needsConfirmation)(config.confirmHighRisk, confirmed, 'Ask Client', { job_id, text }, `Post a public question to the client of job #${job_id}`);
494
+ if (confirmation)
495
+ return confirmation;
496
+ const registry = new sdk_1.EscrowRegistry(config.rpc, config.session, contracts.agentescrow);
497
+ return registry.askClient(job_id, text);
498
+ },
499
+ });
500
+ api.registerTool({
501
+ name: 'xpr_answer_agent',
502
+ description: 'Answer the agent\'s question on a job you created (agentescrow answer). Valid while the job is FUNDED, ACCEPTED or INPROGRESS, max 20 messages per job. Answer from the job brief; if you cannot answer, say so plainly so the agent can proceed with its best interpretation.',
503
+ parameters: {
504
+ type: 'object',
505
+ required: ['job_id', 'text'],
506
+ properties: {
507
+ job_id: { type: 'number', description: 'Job ID you created (you are the client)' },
508
+ text: { type: 'string', description: 'The answer (1-512 characters)' },
509
+ confirmed: { type: 'boolean', description: 'Set to true to execute after reviewing the confirmation prompt' },
510
+ },
511
+ },
512
+ handler: async ({ job_id, text, confirmed }) => {
513
+ if (!config.session)
514
+ throw new Error('Session required: set XPR_ACCOUNT and ensure proton CLI has the account key in its keychain');
515
+ (0, validate_1.validatePositiveInt)(job_id, 'job_id');
516
+ (0, validate_1.validateRequired)(text, 'text');
517
+ if (text.length > 512)
518
+ throw new Error('text must be at most 512 characters');
519
+ const confirmation = (0, confirm_1.needsConfirmation)(config.confirmHighRisk, confirmed, 'Answer Agent', { job_id, text }, `Post a public answer to the agent on job #${job_id}`);
520
+ if (confirmation)
521
+ return confirmation;
522
+ const registry = new sdk_1.EscrowRegistry(config.rpc, config.session, contracts.agentescrow);
523
+ return registry.answerAgent(job_id, text);
524
+ },
525
+ });
405
526
  api.registerTool({
406
527
  name: 'xpr_raise_dispute',
407
528
  description: 'Raise a dispute on a job. Either client or agent can dispute.',
@@ -659,5 +780,416 @@ function registerEscrowTools(api, config) {
659
780
  return registry.withdrawBid(bid_id);
660
781
  },
661
782
  });
783
+ // ---- SERVICES ----
784
+ api.registerTool({
785
+ name: 'xpr_get_service',
786
+ description: 'Get a fixed-price service listing by ID. price_xpr is the price in XPR, turnaround is in seconds. Buying a service creates and funds a direct-hire job for the listing agent in one step.',
787
+ parameters: {
788
+ type: 'object',
789
+ required: ['id'],
790
+ properties: {
791
+ id: { type: 'number', description: 'Service listing ID' },
792
+ },
793
+ },
794
+ handler: async ({ id }) => {
795
+ (0, validate_1.validatePositiveInt)(id, 'id');
796
+ const registry = new sdk_1.EscrowRegistry(config.rpc, undefined, contracts.agentescrow);
797
+ const service = await registry.getService(id);
798
+ if (!service) {
799
+ return { error: `Service #${id} not found` };
800
+ }
801
+ return serviceToXpr(service);
802
+ },
803
+ });
804
+ api.registerTool({
805
+ name: 'xpr_list_services',
806
+ description: 'Browse the services catalogue. Filter by agent (their own listings, including delisted ones) or category. Prices are returned as price_xpr in XPR.',
807
+ parameters: {
808
+ type: 'object',
809
+ properties: {
810
+ agent: { type: 'string', description: 'Filter by selling agent account' },
811
+ category: {
812
+ type: 'string',
813
+ description: 'Filter by category slug (image, data, code, writing, research, nft, defi, other)',
814
+ },
815
+ active: { type: 'boolean', description: 'Only active listings (default true)' },
816
+ limit: { type: 'number', description: 'Max results (default 20, max 100)' },
817
+ },
818
+ },
819
+ handler: async ({ agent, category, active = true, limit = 20 }) => {
820
+ if (agent)
821
+ (0, validate_1.validateAccountName)(agent, 'agent');
822
+ const capped = Math.min(limit, 100);
823
+ const registry = new sdk_1.EscrowRegistry(config.rpc, undefined, contracts.agentescrow);
824
+ let services;
825
+ let hasMore = false;
826
+ if (agent) {
827
+ services = await registry.listServicesByAgent(agent);
828
+ if (active)
829
+ services = services.filter(s => s.active);
830
+ }
831
+ else {
832
+ const result = await registry.listServices({ limit: capped, activeOnly: active });
833
+ services = result.items;
834
+ hasMore = result.hasMore;
835
+ }
836
+ if (category) {
837
+ services = services.filter(s => s.category === category);
838
+ }
839
+ return {
840
+ items: services.slice(0, capped).map(s => serviceToXpr(s)),
841
+ count: Math.min(services.length, capped),
842
+ hasMore,
843
+ };
844
+ },
845
+ });
846
+ api.registerTool({
847
+ name: 'xpr_list_service',
848
+ description: 'Publish a fixed-price service listing so buyers can hire you with one click. A purchase arrives as an already-funded direct-hire job — accept, start, deliver as usual. Max 10 active listings per agent. Price is in XPR, turnaround is in seconds (3600 minimum, 31536000 maximum).',
849
+ parameters: {
850
+ type: 'object',
851
+ required: ['title', 'description', 'deliverables', 'price', 'turnaround'],
852
+ properties: {
853
+ title: { type: 'string', description: 'Service title (1-128 chars)' },
854
+ description: { type: 'string', description: 'What the buyer gets (1-2048 chars)' },
855
+ deliverables: {
856
+ type: 'array',
857
+ items: { type: 'string' },
858
+ description: 'Exact artifacts you will deliver, e.g. ["logo.svg", "logo.png"]',
859
+ },
860
+ price: { type: 'number', description: 'Fixed price in XPR (e.g. 250)' },
861
+ turnaround: { type: 'number', description: 'Delivery time in seconds (becomes the job deadline)' },
862
+ category: {
863
+ type: 'string',
864
+ description: 'Category slug: image, data, code, writing, research, nft, defi, other',
865
+ },
866
+ sample_uri: { type: 'string', description: 'Example output — IPFS/https URL or a JSON manifest' },
867
+ confirmed: { type: 'boolean', description: 'Set to true to execute after reviewing the confirmation prompt' },
868
+ },
869
+ },
870
+ handler: async (params) => {
871
+ if (!config.session)
872
+ throw new Error('Session required: set XPR_ACCOUNT and ensure proton CLI has the account key in its keychain');
873
+ (0, validate_1.validateRequired)(params.title, 'title');
874
+ (0, validate_1.validateRequired)(params.description, 'description');
875
+ if (params.price <= 0)
876
+ throw new Error('price must be positive');
877
+ (0, validate_1.validatePositiveInt)(params.turnaround, 'turnaround');
878
+ // Publishing costs config.service_fee, paid as a `svcfee:` deposit that
879
+ // listsvc then consumes. Read the live fee so a config change doesn't
880
+ // silently underpay; fall back to the contract default if svcconfig is
881
+ // unset or the RPC read fails.
882
+ const registry = new sdk_1.EscrowRegistry(config.rpc, config.session, contracts.agentescrow);
883
+ let feeRaw = DEFAULT_SERVICE_FEE_RAW;
884
+ try {
885
+ feeRaw = (await registry.getServiceConfig()).service_fee;
886
+ }
887
+ catch {
888
+ // svcconfig unreadable — the default matches the contract's own default
889
+ }
890
+ // Same enforcement path as xpr_fund_job / xpr_buy_service.
891
+ (0, validate_1.validateAmount)(feeRaw, config.maxTransferAmount);
892
+ const confirmation = (0, confirm_1.needsConfirmation)(config.confirmHighRisk, params.confirmed, 'List Service', {
893
+ title: params.title,
894
+ price: `${params.price} XPR`,
895
+ turnaround: params.turnaround,
896
+ listing_fee: `${feeRaw / 10000} XPR`,
897
+ }, `Publish "${params.title}" at ${params.price} XPR with a ${params.turnaround}s turnaround — costs a ${feeRaw / 10000} XPR listing fee`);
898
+ if (confirmation)
899
+ return confirmation;
900
+ const data = {
901
+ title: params.title,
902
+ description: params.description,
903
+ deliverables: normalizeDeliverables(params.deliverables),
904
+ price: (0, validate_1.xprToSmallestUnits)(params.price),
905
+ turnaround: params.turnaround,
906
+ category: params.category || '',
907
+ sampleUri: params.sample_uri || '',
908
+ };
909
+ // One atomic transaction is the safe path: if listsvc fails, the fee
910
+ // transfer rolls back with it and no orphaned deposit is left behind.
911
+ try {
912
+ const result = await registry.listServiceWithFee(feeRaw, data);
913
+ return { ...result, listing_fee_xpr: feeRaw / 10000, fee_transaction: 'combined' };
914
+ }
915
+ catch (err) {
916
+ if (!isMultiActionUnsupported(err))
917
+ throw err;
918
+ // Session can't batch actions — pay the deposit, then list. The deposit
919
+ // is reclaimable with refundsvcfee if the second step fails.
920
+ const feeResult = await registry.payServiceFee(feeRaw);
921
+ const listResult = await registry.listService(data);
922
+ return {
923
+ ...listResult,
924
+ listing_fee_xpr: feeRaw / 10000,
925
+ fee_transaction: feeResult.transaction_id,
926
+ };
927
+ }
928
+ },
929
+ });
930
+ api.registerTool({
931
+ name: 'xpr_update_service',
932
+ description: 'Update one of your service listings. All fields are replaced, so send the full listing. Does not change active status or sales count.',
933
+ parameters: {
934
+ type: 'object',
935
+ required: ['service_id', 'title', 'description', 'deliverables', 'price', 'turnaround'],
936
+ properties: {
937
+ service_id: { type: 'number', description: 'Service listing ID to update' },
938
+ title: { type: 'string', description: 'Service title (1-128 chars)' },
939
+ description: { type: 'string', description: 'What the buyer gets (1-2048 chars)' },
940
+ deliverables: {
941
+ type: 'array',
942
+ items: { type: 'string' },
943
+ description: 'Exact artifacts you will deliver',
944
+ },
945
+ price: { type: 'number', description: 'Fixed price in XPR' },
946
+ turnaround: { type: 'number', description: 'Delivery time in seconds' },
947
+ category: { type: 'string', description: 'Category slug' },
948
+ sample_uri: { type: 'string', description: 'Example output URI' },
949
+ confirmed: { type: 'boolean', description: 'Set to true to execute after reviewing the confirmation prompt' },
950
+ },
951
+ },
952
+ handler: async (params) => {
953
+ if (!config.session)
954
+ throw new Error('Session required: set XPR_ACCOUNT and ensure proton CLI has the account key in its keychain');
955
+ (0, validate_1.validatePositiveInt)(params.service_id, 'service_id');
956
+ (0, validate_1.validateRequired)(params.title, 'title');
957
+ (0, validate_1.validateRequired)(params.description, 'description');
958
+ if (params.price <= 0)
959
+ throw new Error('price must be positive');
960
+ (0, validate_1.validatePositiveInt)(params.turnaround, 'turnaround');
961
+ const confirmation = (0, confirm_1.needsConfirmation)(config.confirmHighRisk, params.confirmed, 'Update Service', { service_id: params.service_id, title: params.title, price: `${params.price} XPR` }, `Replace listing #${params.service_id} with "${params.title}" at ${params.price} XPR`);
962
+ if (confirmation)
963
+ return confirmation;
964
+ const registry = new sdk_1.EscrowRegistry(config.rpc, config.session, contracts.agentescrow);
965
+ return registry.updateService(params.service_id, {
966
+ title: params.title,
967
+ description: params.description,
968
+ deliverables: normalizeDeliverables(params.deliverables),
969
+ price: (0, validate_1.xprToSmallestUnits)(params.price),
970
+ turnaround: params.turnaround,
971
+ category: params.category || '',
972
+ sampleUri: params.sample_uri || '',
973
+ });
974
+ },
975
+ });
976
+ api.registerTool({
977
+ name: 'xpr_delist_service',
978
+ description: 'Take one of your service listings off the catalogue. The row is kept for history and can be relisted later.',
979
+ parameters: {
980
+ type: 'object',
981
+ required: ['service_id'],
982
+ properties: {
983
+ service_id: { type: 'number', description: 'Service listing ID to delist' },
984
+ confirmed: { type: 'boolean', description: 'Set to true to execute after reviewing the confirmation prompt' },
985
+ },
986
+ },
987
+ handler: async ({ service_id, confirmed }) => {
988
+ if (!config.session)
989
+ throw new Error('Session required: set XPR_ACCOUNT and ensure proton CLI has the account key in its keychain');
990
+ (0, validate_1.validatePositiveInt)(service_id, 'service_id');
991
+ const confirmation = (0, confirm_1.needsConfirmation)(config.confirmHighRisk, confirmed, 'Delist Service', { service_id }, `Remove listing #${service_id} from the services catalogue`);
992
+ if (confirmation)
993
+ return confirmation;
994
+ const registry = new sdk_1.EscrowRegistry(config.rpc, config.session, contracts.agentescrow);
995
+ return registry.delistService(service_id);
996
+ },
997
+ });
998
+ api.registerTool({
999
+ name: 'xpr_relist_service',
1000
+ description: 'Put a previously delisted service back on the catalogue. The 10-active-listing limit applies.',
1001
+ parameters: {
1002
+ type: 'object',
1003
+ required: ['service_id'],
1004
+ properties: {
1005
+ service_id: { type: 'number', description: 'Service listing ID to relist' },
1006
+ confirmed: { type: 'boolean', description: 'Set to true to execute after reviewing the confirmation prompt' },
1007
+ },
1008
+ },
1009
+ handler: async ({ service_id, confirmed }) => {
1010
+ if (!config.session)
1011
+ throw new Error('Session required: set XPR_ACCOUNT and ensure proton CLI has the account key in its keychain');
1012
+ (0, validate_1.validatePositiveInt)(service_id, 'service_id');
1013
+ const confirmation = (0, confirm_1.needsConfirmation)(config.confirmHighRisk, confirmed, 'Relist Service', { service_id }, `Put listing #${service_id} back on the services catalogue`);
1014
+ if (confirmation)
1015
+ return confirmation;
1016
+ const registry = new sdk_1.EscrowRegistry(config.rpc, config.session, contracts.agentescrow);
1017
+ return registry.relistService(service_id);
1018
+ },
1019
+ });
1020
+ api.registerTool({
1021
+ name: 'xpr_get_service_input',
1022
+ description: 'Read the input form a service listing declares (the svcinputs schema): the questions a buyer answers at purchase. Returns null when the seller has not declared one. Call this before xpr_buy_service so you know what `input` to pass.',
1023
+ parameters: {
1024
+ type: 'object',
1025
+ required: ['service_id'],
1026
+ properties: {
1027
+ service_id: { type: 'number', description: 'Service listing ID' },
1028
+ },
1029
+ },
1030
+ handler: async ({ service_id }) => {
1031
+ (0, validate_1.validatePositiveInt)(service_id, 'service_id');
1032
+ const registry = new sdk_1.EscrowRegistry(config.rpc, undefined, contracts.agentescrow);
1033
+ const schema = await registry.getServiceInput(service_id);
1034
+ return { service_id, schema, has_schema: schema !== null };
1035
+ },
1036
+ });
1037
+ api.registerTool({
1038
+ name: 'xpr_set_service_input',
1039
+ description: 'Declare the input form for a listing you own (agentescrow setsvcinput). Buyers answer it at purchase and the answers arrive as the first message on the job thread, so you start with everything you need instead of having to ask. Schema shape: {"v":1,"fields":[{"key":"account","label":"XPR account to analyze","type":"account","required":true}]} — at most 8 fields, key 1-32 chars of a-z/0-9/_, label <= 64 chars, type text|textarea|number|account|url|select|checkbox (select needs options), optional max (characters). Pass an empty string to remove the form. Call this right after xpr_list_service for any listing that needs specifics from the buyer.',
1040
+ parameters: {
1041
+ type: 'object',
1042
+ required: ['service_id', 'schema'],
1043
+ properties: {
1044
+ service_id: { type: 'number', description: 'Service listing ID you own' },
1045
+ schema: { description: 'The schema object (or its JSON string), or "" to remove the form' },
1046
+ confirmed: { type: 'boolean', description: 'Set to true to execute after reviewing the confirmation prompt' },
1047
+ },
1048
+ },
1049
+ handler: async ({ service_id, schema, confirmed }) => {
1050
+ if (!config.session)
1051
+ throw new Error('Session required: set XPR_ACCOUNT and ensure proton CLI has the account key in its keychain');
1052
+ (0, validate_1.validatePositiveInt)(service_id, 'service_id');
1053
+ const removing = schema === '' || schema === null || schema === undefined;
1054
+ let schemaJson = '';
1055
+ let fieldCount = 0;
1056
+ if (!removing) {
1057
+ const check = (0, sdk_1.validateServiceInputSchema)(schema);
1058
+ if (!check.valid) {
1059
+ return { error: `Invalid input schema: ${check.errors.join('; ')}` };
1060
+ }
1061
+ schemaJson = check.json;
1062
+ fieldCount = JSON.parse(check.json).fields.length;
1063
+ }
1064
+ const confirmation = (0, confirm_1.needsConfirmation)(config.confirmHighRisk, confirmed, removing ? 'Remove Service Input Form' : 'Set Service Input Form', { service_id, fields: fieldCount, schema: schemaJson }, removing
1065
+ ? `Remove the input form from listing #${service_id}`
1066
+ : `Publish a ${fieldCount}-field input form on listing #${service_id}`);
1067
+ if (confirmation)
1068
+ return confirmation;
1069
+ const registry = new sdk_1.EscrowRegistry(config.rpc, config.session, contracts.agentescrow);
1070
+ const result = await registry.setServiceInput(service_id, schemaJson);
1071
+ return { ...result, service_id, fields: fieldCount, removed: removing };
1072
+ },
1073
+ });
1074
+ api.registerTool({
1075
+ name: 'xpr_buy_service',
1076
+ description: 'Buy a service listing with a single XPR transfer (memo buy:<id>, or buy:<id>:<notes> when you pass notes). The contract creates and funds a direct-hire job for the selling agent in the same transaction — track it with xpr_list_jobs. Pass the price you saw on the listing (in XPR); the purchase is rejected if the on-chain price is higher. Use `notes` for the few specifics the agent cannot guess (brand name, colours, target audience); anything longer than 200 characters belongs in a custom job instead. If the listing declares an input form (xpr_get_service_input), answer it with `input` instead — the answers travel with the purchase in the same transaction.',
1077
+ parameters: {
1078
+ type: 'object',
1079
+ required: ['service_id', 'price'],
1080
+ properties: {
1081
+ service_id: { type: 'number', description: 'Service listing ID to buy' },
1082
+ price: { type: 'number', description: 'Price in XPR as shown on the listing (price_xpr from xpr_get_service)' },
1083
+ notes: { type: 'string', description: 'Optional brief for the agent (max 200 characters). Appended to the job description as "Buyer notes: ...".' },
1084
+ input: { type: 'object', description: 'Optional answers to the listing\'s input form, keyed by field key (see xpr_get_service_input). Sent with the purchase in one transaction and delivered as the first message on the job thread. Packed JSON must be at most 512 characters.' },
1085
+ confirmed: { type: 'boolean', description: 'Set to true to execute after reviewing the confirmation prompt' },
1086
+ },
1087
+ },
1088
+ handler: async ({ service_id, price, notes, input, confirmed }) => {
1089
+ if (!config.session)
1090
+ throw new Error('Session required: set XPR_ACCOUNT and ensure proton CLI has the account key in its keychain');
1091
+ (0, validate_1.validatePositiveInt)(service_id, 'service_id');
1092
+ if (price <= 0)
1093
+ throw new Error('price must be positive');
1094
+ if (notes && notes.trim().length > 200)
1095
+ throw new Error('notes must be at most 200 characters');
1096
+ // Same enforcement path as xpr_fund_job: per-call cap + aggregate session cap.
1097
+ (0, validate_1.validateAmount)((0, validate_1.xprToSmallestUnits)(price), config.maxTransferAmount);
1098
+ const registry = new sdk_1.EscrowRegistry(config.rpc, config.session, contracts.agentescrow);
1099
+ const service = await registry.getService(service_id);
1100
+ if (!service)
1101
+ return { error: `Service #${service_id} not found` };
1102
+ if (!service.active)
1103
+ return { error: `Service #${service_id} is delisted and cannot be bought` };
1104
+ if (service.price > (0, validate_1.xprToSmallestUnits)(price)) {
1105
+ return {
1106
+ error: `Service #${service_id} now costs ${service.price / 10000} XPR, more than the ${price} XPR you approved. Re-read the listing and try again.`,
1107
+ };
1108
+ }
1109
+ // Answers to the listing's input form travel with the purchase in one
1110
+ // transaction. Validate them against the seller's schema first — a
1111
+ // rejected svcinput would roll the transfer back with it.
1112
+ let answersJson = '';
1113
+ if (input !== undefined && input !== null && input !== '') {
1114
+ let answers;
1115
+ if (typeof input === 'string') {
1116
+ try {
1117
+ answers = JSON.parse(input);
1118
+ }
1119
+ catch {
1120
+ return { error: 'input must be an object (or a JSON object string) keyed by the form\'s field keys' };
1121
+ }
1122
+ }
1123
+ else {
1124
+ answers = input;
1125
+ }
1126
+ const schema = await registry.getServiceInput(service_id);
1127
+ const check = (0, sdk_1.validateServiceInput)(schema, answers);
1128
+ if (!check.valid) {
1129
+ return { error: `Input does not match the listing's form: ${check.errors.join('; ')}`, schema };
1130
+ }
1131
+ answersJson = JSON.stringify(answers);
1132
+ if (answersJson.length > sdk_1.MAX_SERVICE_INPUT_ANSWERS_LENGTH) {
1133
+ return { error: `Packed input must be at most ${sdk_1.MAX_SERVICE_INPUT_ANSWERS_LENGTH} characters (got ${answersJson.length}) — shorten your answers or commission a custom job` };
1134
+ }
1135
+ }
1136
+ const confirmation = (0, confirm_1.needsConfirmation)(config.confirmHighRisk, confirmed, 'Buy Service', { service_id, title: service.title, agent: service.agent, price: `${service.price / 10000} XPR`, notes: notes || '', input: answersJson }, `Send ${service.price / 10000} XPR to buy "${service.title}" from ${service.agent} — this creates and funds a job`);
1137
+ if (confirmation)
1138
+ return confirmation;
1139
+ if (answersJson) {
1140
+ // transfer(buy:<id>) + svcinput, signed once
1141
+ return registry.buyServiceWithInput(service_id, service.price, answersJson);
1142
+ }
1143
+ return registry.buyService(service_id, service.price, notes);
1144
+ },
1145
+ });
1146
+ api.registerTool({
1147
+ name: 'xpr_boost_service',
1148
+ description: 'Boost a service listing into featured placement with an XPR transfer (memo boost:<id>). Each boost_rate of XPR (1 XPR by default) buys one featured day, added on top of any time already bought. Anyone can boost any listing, but the listing must be active and its agent must have completed at least one job. Only the top 3 featured listings show above the organic catalogue, ranked by lifetime boost_paid — featuring is rarely worth it before you have completed jobs and reviews.',
1149
+ parameters: {
1150
+ type: 'object',
1151
+ required: ['service_id', 'amount'],
1152
+ properties: {
1153
+ service_id: { type: 'number', description: 'Service listing ID to feature' },
1154
+ amount: { type: 'number', description: 'Boost amount in XPR (must be at least boost_min, 1 XPR by default)' },
1155
+ confirmed: { type: 'boolean', description: 'Set to true to execute after reviewing the confirmation prompt' },
1156
+ },
1157
+ },
1158
+ handler: async ({ service_id, amount, confirmed }) => {
1159
+ if (!config.session)
1160
+ throw new Error('Session required: set XPR_ACCOUNT and ensure proton CLI has the account key in its keychain');
1161
+ (0, validate_1.validatePositiveInt)(service_id, 'service_id');
1162
+ if (amount <= 0)
1163
+ throw new Error('amount must be positive');
1164
+ // Same enforcement path as xpr_fund_job / xpr_buy_service.
1165
+ const amountRaw = (0, validate_1.xprToSmallestUnits)(amount);
1166
+ (0, validate_1.validateAmount)(amountRaw, config.maxTransferAmount);
1167
+ const registry = new sdk_1.EscrowRegistry(config.rpc, config.session, contracts.agentescrow);
1168
+ const service = await registry.getService(service_id);
1169
+ if (!service)
1170
+ return { error: `Service #${service_id} not found` };
1171
+ if (!service.active)
1172
+ return { error: `Service #${service_id} is delisted and cannot be boosted` };
1173
+ let boostMin = 10000;
1174
+ let boostRate = 10000;
1175
+ try {
1176
+ const svcConfig = await registry.getServiceConfig();
1177
+ boostMin = svcConfig.boost_min;
1178
+ boostRate = svcConfig.boost_rate;
1179
+ }
1180
+ catch {
1181
+ // svcconfig unreadable — the defaults match the contract's own
1182
+ }
1183
+ if (amountRaw < boostMin) {
1184
+ return { error: `Boost must be at least ${boostMin / 10000} XPR (boost_min)` };
1185
+ }
1186
+ const days = Math.floor(amountRaw / boostRate);
1187
+ const confirmation = (0, confirm_1.needsConfirmation)(config.confirmHighRisk, confirmed, 'Boost Service', { service_id, title: service.title, amount: `${amount} XPR`, featured_days: days }, `Send ${amount} XPR to feature "${service.title}" for about ${days} day(s)`);
1188
+ if (confirmation)
1189
+ return confirmation;
1190
+ const result = await registry.boostService(service_id, amountRaw);
1191
+ return { ...result, featured_days: days, boost_xpr: amount };
1192
+ },
1193
+ });
662
1194
  }
663
1195
  //# sourceMappingURL=escrow.js.map