@dvmkit/dvmctl 0.3.2-rc.1 → 0.3.4-rc.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.
- package/README.md +4 -2
- package/dist/dvmctl.js +15 -15
- package/package.json +9 -6
- package/skills/build-dvm/SKILL.md +2 -2
- package/skills/build-dvm/references/local-cashu-test.md +33 -6
- package/skills/build-dvm/references/local-tempo-test.md +22 -5
- package/skills/build-dvm/references/local-x402-test.md +22 -5
- package/skills/build-dvm/references/running-deployment.md +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dvmkit/dvmctl",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4-rc.1",
|
|
4
4
|
"description": "Builder CLI for creating, testing, and deploying Digital Vending Machines",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -29,14 +29,16 @@
|
|
|
29
29
|
"node": ">=22"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"build": "tsup --metafile && node scripts/check-bundled-notices.mjs",
|
|
32
|
+
"build": "node scripts/check-source-boundary.mjs && node scripts/check-bundled-skills.mjs && tsup --metafile && node scripts/check-bundled-notices.mjs",
|
|
33
33
|
"lint": "eslint . --max-warnings 0",
|
|
34
34
|
"typecheck": "tsc --noEmit",
|
|
35
|
-
"test": "
|
|
36
|
-
"verify": "npm run build && npm run lint && npm run typecheck && npm test"
|
|
35
|
+
"test": "npm run test:source && npm run test:package",
|
|
36
|
+
"verify": "npm run build && npm run lint && npm run typecheck && npm test",
|
|
37
|
+
"test:source": "vitest run",
|
|
38
|
+
"test:package": "node --test test/*.test.mjs"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"@dvmkit/sdk": "0.1
|
|
41
|
+
"@dvmkit/sdk": "0.2.1-rc.9"
|
|
40
42
|
},
|
|
41
43
|
"optionalDependencies": {
|
|
42
44
|
"pg": "8.20.0",
|
|
@@ -61,6 +63,7 @@
|
|
|
61
63
|
"@eslint/js": "^10.0.1",
|
|
62
64
|
"eslint": "^10.0.2",
|
|
63
65
|
"globals": "^17.7.0",
|
|
64
|
-
"typescript-eslint": "^8.56.1"
|
|
66
|
+
"typescript-eslint": "^8.56.1",
|
|
67
|
+
"vitest": "5.0.2"
|
|
65
68
|
}
|
|
66
69
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: build-dvm
|
|
3
3
|
description: Build a first DVM with dvmkit. Use when asked to turn an idea into a paid HTTPS capability, scaffold or hand-write a DVM, implement a `configureDVM` handler, run it locally, verify Cashu, x402, or Tempo test payments, or prepare it for deployment. Do not use for changing the dvmkit platform, SDK internals, or a caller wallet unrelated to testing a DVM.
|
|
4
|
-
compatibility: Node.js 22+, npm, the invited `@dvmkit/dvmctl` release, `@dvmkit/sdk@0.1
|
|
4
|
+
compatibility: Node.js 22+, npm, the invited `@dvmkit/dvmctl` release, `@dvmkit/sdk@0.2.1-rc.9`, and `@dvmkit/dvm-cli@0.3.0`. Docker is needed only when this skill starts its local Cashu FakeWallet mint or performs a container build.
|
|
5
5
|
allowed-tools: Bash Read Write Edit Glob Grep
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -64,7 +64,7 @@ Scaffold when it helps; a hand-written Node 22 project with `@dvmkit/sdk` is equ
|
|
|
64
64
|
For a hand-written project, pin the admitted SDK exactly:
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
|
-
npm install --save-exact @dvmkit/sdk@0.1
|
|
67
|
+
npm install --save-exact @dvmkit/sdk@0.2.1-rc.9
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
```bash
|
|
@@ -84,18 +84,45 @@ dvm request --endpoint "$dvm_endpoint" --data "$DVM_INPUT" --rail cashu --mint "
|
|
|
84
84
|
--max-increment "$DVM_PAYMENT_CAP" --max-payments 1 </dev/null >"$test_root/paid.json"
|
|
85
85
|
dvm wallet show >"$test_root/after.json"
|
|
86
86
|
job_id="$(node -p 'require(process.argv[1]).jobId' "$test_root/paid.json")"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
result_dir="$PWD/.dvmkit-test-results/$job_id"
|
|
88
|
+
mkdir -p -m 700 "$result_dir"
|
|
89
|
+
cp "$test_root/unpaid.json" "$test_root/before.json" "$test_root/after.json" "$test_root/paid.json" "$result_dir/"
|
|
90
|
+
dvm messages "$job_id" --no-stream >/dev/null
|
|
91
|
+
TMPDIR="$result_dir" dvm messages "$job_id" --no-stream --human | tee "$result_dir/artifacts.txt"
|
|
92
|
+
retained_config="$result_dir/caller"
|
|
93
|
+
node - "$DVM_CONFIG_DIR" "$retained_config" "$result_dir/messages.json" "$job_id" <<'NODE'
|
|
94
|
+
const { mkdirSync, readFileSync, writeFileSync } = require("node:fs");
|
|
95
|
+
const { join } = require("node:path");
|
|
96
|
+
const [source, destination, resultPath, jobId] = process.argv.slice(2);
|
|
97
|
+
const job = JSON.parse(readFileSync(join(source, "jobs.json"), "utf8")).find(item => item.id === jobId);
|
|
98
|
+
if (!job) throw Error(`caller state did not retain ${jobId}`);
|
|
99
|
+
const receipts = readFileSync(join(source, "receipts.jsonl"), "utf8").trim().split("\n").filter(Boolean).filter(line => JSON.parse(line).receipt?.job_id === jobId);
|
|
100
|
+
if (receipts.length !== 1) throw Error(`expected one receipt for ${jobId}, found ${receipts.length}`);
|
|
101
|
+
const messages = readFileSync(join(source, "messages", `${jobId}.jsonl`), "utf8").trim().split("\n").filter(Boolean).map(line => JSON.parse(line));
|
|
102
|
+
const retainedMessages = messages.filter(message => ["text", "artifact", "complete", "cancel"].includes(message.type));
|
|
103
|
+
if (!retainedMessages.some(message => message.type === "complete" || message.type === "cancel")) throw Error("caller state did not retain a terminal message");
|
|
104
|
+
const retainedJob = { id: job.id, endpoint: job.endpoint, status: job.status, submittedAt: job.submittedAt, completedAt: job.completedAt, completion: job.completion, paymentsHonoured: job.paymentsHonoured, policy: job.policy };
|
|
105
|
+
mkdirSync(join(destination, "messages"), { recursive: true, mode: 0o700 });
|
|
106
|
+
writeFileSync(join(destination, "jobs.json"), `${JSON.stringify([retainedJob])}\n`, { mode: 0o600 });
|
|
107
|
+
writeFileSync(join(destination, "receipts.jsonl"), `${receipts[0]}\n`, { mode: 0o600 });
|
|
108
|
+
writeFileSync(join(destination, "messages", `${jobId}.jsonl`), `${retainedMessages.map(message => JSON.stringify(message)).join("\n")}\n`, { mode: 0o600 });
|
|
109
|
+
writeFileSync(resultPath, `${JSON.stringify({ jobId, messages: retainedMessages })}\n`, { mode: 0o600 });
|
|
110
|
+
NODE
|
|
111
|
+
DVM_CONFIG_DIR="$retained_config" dvm receipts verify "$job_id" >"$result_dir/verified.json"
|
|
112
|
+
node - "$result_dir/unpaid.json" "$result_dir/before.json" "$result_dir/after.json" "$result_dir/paid.json" "$result_dir/messages.json" "$result_dir/verified.json" <<'NODE'
|
|
113
|
+
const [challengePath, beforePath, afterPath, paidPath, messagesPath, verifiedPath] = process.argv.slice(2);
|
|
114
|
+
const challenge=require(challengePath), before=require(beforePath), after=require(afterPath), paid=require(paidPath), messages=require(messagesPath), verified=require(verifiedPath);
|
|
91
115
|
const validReceiptStatuses = new Set(["verified", "verified_unattested"]);
|
|
92
116
|
const expectedSats=challenge.required_msats/1000;
|
|
93
117
|
if (before.total_sats-after.total_sats !== expectedSats) throw Error(`Cashu debit was ${before.total_sats-after.total_sats}, quote was ${expectedSats} sats`);
|
|
94
118
|
if (!paid.jobId || paid.next_action !== null || !validReceiptStatuses.has(paid.receipt_verified)) throw Error("paid result is not terminal with a valid receipt signature");
|
|
95
119
|
if (paid.receipt?.outcome !== "completed" || paid.receipt?.paid?.rail !== "cashu" || paid.receipt.paid.msats !== challenge.required_msats) throw Error("receipt is not bound to the 402 quote");
|
|
96
|
-
if (
|
|
120
|
+
if (!messages.messages?.some(message => message.type === "complete")) throw Error("paid messages did not complete");
|
|
121
|
+
if ((paid.has_artifacts || paid.artifact_count > 0) && !messages.messages?.some(message => message.type === "artifact")) throw Error("paid result advertised an artifact that was not retrieved");
|
|
122
|
+
if (verified.job_id !== paid.jobId || !validReceiptStatuses.has(verified.receipt_verified) || verified.receipt_checks?.result_hash !== "ok") throw Error("stored receipt and paid result hash did not verify");
|
|
97
123
|
NODE
|
|
98
|
-
cat "$
|
|
124
|
+
cat "$result_dir/paid.json"
|
|
125
|
+
printf '\nPaid result and artifact evidence retained in %s\n' "$result_dir"
|
|
99
126
|
```
|
|
100
127
|
|
|
101
128
|
The observed hosted-mint acceptance for a `$0.01` call quoted `12000` msats and debited `12` test sats. That is evidence from that rate snapshot, not a constant for future runs.
|
|
@@ -56,17 +56,34 @@ dvm request --endpoint "$dvm_endpoint" --data "$DVM_INPUT" --rail tempo \
|
|
|
56
56
|
--budget "$DVM_PAYMENT_CAP" --auto-pay-below "$DVM_PAYMENT_CAP" \
|
|
57
57
|
--max-increment "$DVM_PAYMENT_CAP" --max-payments 1 </dev/null >"$test_root/paid.json"
|
|
58
58
|
job_id="$(node -p 'require(process.argv[1]).jobId' "$test_root/paid.json")"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
result_dir="$PWD/.dvmkit-test-results/$job_id"
|
|
60
|
+
mkdir -p -m 700 "$result_dir"
|
|
61
|
+
cp "$test_root/unpaid.json" "$test_root/paid.json" "$result_dir/"
|
|
62
|
+
dvm messages "$job_id" --no-stream >/dev/null
|
|
63
|
+
TMPDIR="$result_dir" dvm messages "$job_id" --no-stream --human | tee "$result_dir/artifacts.txt"
|
|
64
|
+
retained_config="$result_dir/caller"
|
|
65
|
+
node - "$DVM_CONFIG_DIR" "$retained_config" "$result_dir/messages.json" "$job_id" <<'NODE'
|
|
66
|
+
const { mkdirSync, readFileSync, writeFileSync }=require("node:fs");const { join }=require("node:path");const [source,destination,resultPath,jobId]=process.argv.slice(2);
|
|
67
|
+
const job=JSON.parse(readFileSync(join(source,"jobs.json"),"utf8")).find(item=>item.id===jobId);if(!job)throw Error(`caller state did not retain ${jobId}`);
|
|
68
|
+
const receipts=readFileSync(join(source,"receipts.jsonl"),"utf8").trim().split("\n").filter(Boolean).filter(line=>JSON.parse(line).receipt?.job_id===jobId);if(receipts.length!==1)throw Error(`expected one receipt for ${jobId}, found ${receipts.length}`);
|
|
69
|
+
const messages=readFileSync(join(source,"messages",`${jobId}.jsonl`),"utf8").trim().split("\n").filter(Boolean).map(line=>JSON.parse(line));const retainedMessages=messages.filter(message=>["text","artifact","complete","cancel"].includes(message.type));if(!retainedMessages.some(message=>message.type==="complete"||message.type==="cancel"))throw Error("caller state did not retain a terminal message");
|
|
70
|
+
const retainedJob={id:job.id,endpoint:job.endpoint,status:job.status,submittedAt:job.submittedAt,completedAt:job.completedAt,completion:job.completion,paymentsHonoured:job.paymentsHonoured,policy:job.policy};mkdirSync(join(destination,"messages"),{recursive:true,mode:0o700});
|
|
71
|
+
writeFileSync(join(destination,"jobs.json"),`${JSON.stringify([retainedJob])}\n`,{mode:0o600});writeFileSync(join(destination,"receipts.jsonl"),`${receipts[0]}\n`,{mode:0o600});writeFileSync(join(destination,"messages",`${jobId}.jsonl`),`${retainedMessages.map(message=>JSON.stringify(message)).join("\n")}\n`,{mode:0o600});writeFileSync(resultPath,`${JSON.stringify({jobId,messages:retainedMessages})}\n`,{mode:0o600});
|
|
72
|
+
NODE
|
|
73
|
+
DVM_CONFIG_DIR="$retained_config" dvm receipts verify "$job_id" >"$result_dir/verified.json"
|
|
74
|
+
node - "$result_dir/unpaid.json" "$result_dir/paid.json" "$result_dir/messages.json" "$result_dir/verified.json" <<'NODE'
|
|
75
|
+
const [challengePath,paidPath,messagesPath,verifiedPath]=process.argv.slice(2);const challenge=require(challengePath),paid=require(paidPath),messages=require(messagesPath),verified=require(verifiedPath);
|
|
62
76
|
const validReceiptStatuses=new Set(["verified","verified_unattested"]);
|
|
63
77
|
if(!paid.jobId || paid.next_action!==null || !validReceiptStatuses.has(paid.receipt_verified)) throw Error("paid result is not terminal with a valid receipt signature");
|
|
64
78
|
const receipt=paid.receipt;
|
|
65
79
|
if(receipt?.outcome!=="completed" || receipt?.paid?.rail!=="tempo" || receipt.paid.msats!==challenge.required_msats) throw Error("receipt is not quote-bound Tempo");
|
|
66
80
|
if(receipt.paid.native_amount!==10000 || receipt.paid.native_asset!=="usdc") throw Error("$0.01 must settle as exactly 10000 pathUSD micro-units");
|
|
67
|
-
if(
|
|
81
|
+
if(!messages.messages?.some(message=>message.type==="complete")) throw Error("paid messages did not complete");
|
|
82
|
+
if((paid.has_artifacts || paid.artifact_count>0) && !messages.messages?.some(message=>message.type==="artifact")) throw Error("paid result advertised an artifact that was not retrieved");
|
|
83
|
+
if(verified.job_id!==paid.jobId || !validReceiptStatuses.has(verified.receipt_verified) || verified.receipt_checks?.result_hash!=="ok") throw Error("stored receipt and paid result hash did not verify");
|
|
68
84
|
NODE
|
|
69
|
-
cat "$
|
|
85
|
+
cat "$result_dir/paid.json"
|
|
86
|
+
printf '\nPaid result and artifact evidence retained in %s\n' "$result_dir"
|
|
70
87
|
```
|
|
71
88
|
|
|
72
89
|
Stop if the Moderato chain check, faucet, balance, or cap fails. Do not switch to mainnet or fund a production wallet as a workaround.
|
|
@@ -58,17 +58,34 @@ dvm request --endpoint "$dvm_endpoint" --data "$DVM_INPUT" --rail x402 \
|
|
|
58
58
|
--budget "$DVM_PAYMENT_CAP" --auto-pay-below "$DVM_PAYMENT_CAP" \
|
|
59
59
|
--max-increment "$DVM_PAYMENT_CAP" --max-payments 1 </dev/null >"$test_root/paid.json"
|
|
60
60
|
job_id="$(node -p 'require(process.argv[1]).jobId' "$test_root/paid.json")"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
result_dir="$PWD/.dvmkit-test-results/$job_id"
|
|
62
|
+
mkdir -p -m 700 "$result_dir"
|
|
63
|
+
cp "$test_root/unpaid.json" "$test_root/paid.json" "$result_dir/"
|
|
64
|
+
dvm messages "$job_id" --no-stream >/dev/null
|
|
65
|
+
TMPDIR="$result_dir" dvm messages "$job_id" --no-stream --human | tee "$result_dir/artifacts.txt"
|
|
66
|
+
retained_config="$result_dir/caller"
|
|
67
|
+
node - "$DVM_CONFIG_DIR" "$retained_config" "$result_dir/messages.json" "$job_id" <<'NODE'
|
|
68
|
+
const { mkdirSync, readFileSync, writeFileSync }=require("node:fs");const { join }=require("node:path");const [source,destination,resultPath,jobId]=process.argv.slice(2);
|
|
69
|
+
const job=JSON.parse(readFileSync(join(source,"jobs.json"),"utf8")).find(item=>item.id===jobId);if(!job)throw Error(`caller state did not retain ${jobId}`);
|
|
70
|
+
const receipts=readFileSync(join(source,"receipts.jsonl"),"utf8").trim().split("\n").filter(Boolean).filter(line=>JSON.parse(line).receipt?.job_id===jobId);if(receipts.length!==1)throw Error(`expected one receipt for ${jobId}, found ${receipts.length}`);
|
|
71
|
+
const messages=readFileSync(join(source,"messages",`${jobId}.jsonl`),"utf8").trim().split("\n").filter(Boolean).map(line=>JSON.parse(line));const retainedMessages=messages.filter(message=>["text","artifact","complete","cancel"].includes(message.type));if(!retainedMessages.some(message=>message.type==="complete"||message.type==="cancel"))throw Error("caller state did not retain a terminal message");
|
|
72
|
+
const retainedJob={id:job.id,endpoint:job.endpoint,status:job.status,submittedAt:job.submittedAt,completedAt:job.completedAt,completion:job.completion,paymentsHonoured:job.paymentsHonoured,policy:job.policy};mkdirSync(join(destination,"messages"),{recursive:true,mode:0o700});
|
|
73
|
+
writeFileSync(join(destination,"jobs.json"),`${JSON.stringify([retainedJob])}\n`,{mode:0o600});writeFileSync(join(destination,"receipts.jsonl"),`${receipts[0]}\n`,{mode:0o600});writeFileSync(join(destination,"messages",`${jobId}.jsonl`),`${retainedMessages.map(message=>JSON.stringify(message)).join("\n")}\n`,{mode:0o600});writeFileSync(resultPath,`${JSON.stringify({jobId,messages:retainedMessages})}\n`,{mode:0o600});
|
|
74
|
+
NODE
|
|
75
|
+
DVM_CONFIG_DIR="$retained_config" dvm receipts verify "$job_id" >"$result_dir/verified.json"
|
|
76
|
+
node - "$result_dir/unpaid.json" "$result_dir/paid.json" "$result_dir/messages.json" "$result_dir/verified.json" <<'NODE'
|
|
77
|
+
const [challengePath,paidPath,messagesPath,verifiedPath]=process.argv.slice(2);const challenge=require(challengePath),paid=require(paidPath),messages=require(messagesPath),verified=require(verifiedPath);
|
|
64
78
|
const validReceiptStatuses=new Set(["verified","verified_unattested"]);
|
|
65
79
|
if(!paid.jobId || paid.next_action!==null || !validReceiptStatuses.has(paid.receipt_verified)) throw Error("paid result is not terminal with a valid receipt signature");
|
|
66
80
|
const receipt=paid.receipt;
|
|
67
81
|
if(receipt?.outcome!=="completed" || receipt?.paid?.rail!=="x402" || receipt.paid.msats!==challenge.required_msats) throw Error("receipt is not quote-bound x402");
|
|
68
82
|
if(receipt.paid.native_amount!==10000 || receipt.paid.native_asset!=="usdc") throw Error("$0.01 must settle as exactly 10000 micro-USDC");
|
|
69
|
-
if(
|
|
83
|
+
if(!messages.messages?.some(message=>message.type==="complete")) throw Error("paid messages did not complete");
|
|
84
|
+
if((paid.has_artifacts || paid.artifact_count>0) && !messages.messages?.some(message=>message.type==="artifact")) throw Error("paid result advertised an artifact that was not retrieved");
|
|
85
|
+
if(verified.job_id!==paid.jobId || !validReceiptStatuses.has(verified.receipt_verified) || verified.receipt_checks?.result_hash!=="ok") throw Error("stored receipt and paid result hash did not verify");
|
|
70
86
|
NODE
|
|
71
|
-
cat "$
|
|
87
|
+
cat "$result_dir/paid.json"
|
|
88
|
+
printf '\nPaid result and artifact evidence retained in %s\n' "$result_dir"
|
|
72
89
|
```
|
|
73
90
|
|
|
74
91
|
Stop on a missing balance, RPC/facilitator failure, or cap refusal. Do not replace the network, key, or cap without new approval.
|
|
@@ -150,6 +150,8 @@ stage. VM resources go in `package.json`'s `dvmkit` field (`{ "dvmkit": { "memor
|
|
|
150
150
|
|
|
151
151
|
### Environment variables
|
|
152
152
|
|
|
153
|
+
For cloud compute deployment automation, `dvmctl deploy --target cloud --env-stdin` accepts a piped JSON object of string environment values. They override the project `.env` for this deployment without rewriting it. Input must finish within 5 seconds and fit in 64 KiB. Send secret values through stdin; local, self-host, connect-tier, and dry-run deployments reject this flag.
|
|
154
|
+
|
|
153
155
|
The SDK reads only `DVMKIT_*`-prefixed variables. Old unprefixed names (`MINTS`,
|
|
154
156
|
`MPP_TEMPO_RECIPIENT`, `X402_PAY_TO`, etc.) and the legacy `DVMKIT_FIRST_PARTY` /
|
|
155
157
|
`DVMKIT_ACCESS_TOKENS` / `DVMKIT_SKIP_PAYMENT` are gone — don't use them.
|