From b699738c9d33ad672f32612b2a007aaf782e68d7 Mon Sep 17 00:00:00 2001 From: Greg Soucy Date: Thu, 23 Apr 2026 19:06:35 -0400 Subject: [PATCH] Add high-level commandlayer API and quickstart onboarding --- README.md | 18 ++++++- examples/quickstart-node/README.md | 14 +++++ examples/quickstart-node/index.js | 13 +++++ examples/quickstart-node/package.json | 11 ++++ typescript-sdk/README.md | 18 ++++++- typescript-sdk/src/index.ts | 47 +++++++++++++++-- typescript-sdk/tests/high-level-api.test.mjs | 54 ++++++++++++++++++++ 7 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 examples/quickstart-node/README.md create mode 100644 examples/quickstart-node/index.js create mode 100644 examples/quickstart-node/package.json create mode 100644 typescript-sdk/tests/high-level-api.test.mjs diff --git a/README.md b/README.md index 50af6f0..b413715 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,22 @@ # CommandLayer SDK -Official SDK repo for the current-line CommandLayer Commons receipt contract (`1.1.0`). +CommandLayer turns AI/runtime actions into verifiable receipts you can run and audit from one SDK. + +```bash +npm install @commandlayer/sdk +``` + +```ts +import { commandlayer } from "@commandlayer/sdk"; +const receipt = await commandlayer.run("summarize", { + text: "Agent receipts prove what happened." +}); +const result = await commandlayer.verify(receipt); +console.log(receipt); +console.log(result.valid ?? result.ok); +``` + +For advanced usage (custom clients, ENS/public key verification, and protocol details), see the docs below. ## What this repo now treats as canonical diff --git a/examples/quickstart-node/README.md b/examples/quickstart-node/README.md new file mode 100644 index 0000000..22f88de --- /dev/null +++ b/examples/quickstart-node/README.md @@ -0,0 +1,14 @@ +# Quickstart (Node.js) + +Install dependencies and run: + +```bash +npm install +COMMANDLAYER_PUBLIC_KEY='ed25519:BASE64_PUBLIC_KEY' npm start +``` + +This example: +1. Runs a sample `summarize` action with `commandlayer.run(...)`. +2. Prints the returned receipt payload. +3. Verifies the receipt with `commandlayer.verify(...)`. +4. Prints whether verification passed. diff --git a/examples/quickstart-node/index.js b/examples/quickstart-node/index.js new file mode 100644 index 0000000..062d578 --- /dev/null +++ b/examples/quickstart-node/index.js @@ -0,0 +1,13 @@ +import { commandlayer } from "@commandlayer/sdk"; + +const receipt = await commandlayer.run("summarize", { + text: "Agent receipts prove what happened." +}); + +console.log("Receipt:", JSON.stringify(receipt, null, 2)); + +const verification = await commandlayer.verify(receipt, { + publicKey: process.env.COMMANDLAYER_PUBLIC_KEY +}); + +console.log("Verification passed:", verification.ok); diff --git a/examples/quickstart-node/package.json b/examples/quickstart-node/package.json new file mode 100644 index 0000000..744d4e0 --- /dev/null +++ b/examples/quickstart-node/package.json @@ -0,0 +1,11 @@ +{ + "name": "commandlayer-quickstart-node", + "private": true, + "type": "module", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "@commandlayer/sdk": "latest" + } +} diff --git a/typescript-sdk/README.md b/typescript-sdk/README.md index 115f5be..3ab667d 100644 --- a/typescript-sdk/README.md +++ b/typescript-sdk/README.md @@ -1,6 +1,22 @@ # CommandLayer TypeScript SDK -Current-line TypeScript SDK for the CommandLayer Commons receipt contract (`1.1.0`). +CommandLayer turns AI/runtime actions into verifiable receipts with a simple run + verify API. + +```bash +npm install @commandlayer/sdk +``` + +```ts +import { commandlayer } from "@commandlayer/sdk"; +const receipt = await commandlayer.run("summarize", { + text: "Agent receipts prove what happened." +}); +const result = await commandlayer.verify(receipt); +console.log(receipt); +console.log(result.valid ?? result.ok); +``` + +For advanced usage and lower-level APIs, continue below. ## What is canonical diff --git a/typescript-sdk/src/index.ts b/typescript-sdk/src/index.ts index ec80d4e..5815b24 100644 --- a/typescript-sdk/src/index.ts +++ b/typescript-sdk/src/index.ts @@ -64,6 +64,12 @@ export type CanonicalReceipt = { [k: string]: unknown; }; +export type ReceiptProtocolMetadata = { + verb: string; + version: string; + [k: string]: unknown; +}; + export type RuntimeMetadata = { trace_id?: string; parent_trace_id?: string | null; @@ -77,11 +83,11 @@ export type RuntimeMetadata = { }; export type CommandResponse = { - receipt: CanonicalReceipt; + receipt: CanonicalReceipt; runtime_metadata?: RuntimeMetadata; }; -export type LegacyBlendedReceipt = CanonicalReceipt & { +export type LegacyBlendedReceipt = CanonicalReceipt & { trace?: RuntimeMetadata; }; @@ -115,11 +121,13 @@ export type VerifyResult = { verify_error?: string | null; }; }; +export type HighLevelVerifyResult = VerifyResult & { valid: boolean }; export type EnsVerifyOptions = { name: string; rpcUrl: string }; export type SignerKeyResolution = { algorithm: "ed25519"; kid: string; rawPublicKeyBytes: Uint8Array }; export type VerifyOptions = { publicKey?: string; ens?: EnsVerifyOptions }; export type ClientOptions = { + baseUrl?: string; runtime?: string; actor?: string; timeoutMs?: number; @@ -127,6 +135,9 @@ export type ClientOptions = { verifyReceipts?: boolean; verify?: VerifyOptions; }; +export type RunOptions = { + actor?: string; +}; export type CommonsRequestEnvelope = Record> = { x402: ReceiptProtocolMetadata; @@ -295,6 +306,7 @@ function extractReceipt(subject: CanonicalReceipt | CommandResponse | LegacyBlen export function extractReceiptVerb(subject: CanonicalReceipt | CommandResponse | LegacyBlendedReceipt): string | null { const receipt = extractReceipt(subject); + if (typeof receipt.verb === "string") return receipt.verb; return isRecord(receipt.x402) && typeof receipt.x402.verb === "string" ? receipt.x402.verb : null; } @@ -349,6 +361,7 @@ export async function verifyReceipt(receiptLike: CanonicalReceipt | CommandRespo const { hash_sha256: recomputedHash } = recomputeReceiptHashSha256(receipt); const hashMatches = claimedHash === recomputedHash; const receiptId = typeof receipt.metadata?.receipt_id === "string" ? receipt.metadata.receipt_id : null; + const receiptIdPresent = receiptId !== null; const receiptIdMatches = !receiptId || !claimedHash ? true : receiptId === claimedHash; let pubkey: Uint8Array | null = null; @@ -396,7 +409,7 @@ export async function verifyReceipt(receiptLike: CanonicalReceipt | CommandRespo }, values: { verb: getReceiptVerb(receipt), - signer_id, + signer_id: signerId, alg, canonical, claimed_hash: claimedHash, @@ -444,7 +457,7 @@ export class CommandLayerClient { verifyDefaults?: VerifyOptions; constructor(opts: ClientOptions = {}) { - this.runtime = normalizeBase(opts.runtime || DEFAULT_RUNTIME); + this.runtime = normalizeBase(opts.baseUrl || opts.runtime || DEFAULT_RUNTIME); this.actor = opts.actor || "sdk-user"; this.timeoutMs = opts.timeoutMs ?? 30_000; this.fetchImpl = opts.fetchImpl || fetch; @@ -509,6 +522,20 @@ export class CommandLayerClient { return this.call("fetch", { input: { source: opts.source, ...(opts.query !== undefined ? { query: opts.query } : {}), ...(opts.include_metadata !== undefined ? { include_metadata: opts.include_metadata } : {}) }, limits: { max_output_tokens: opts.maxTokens ?? 1000 } }); } + async run(action: Verb, input: Record, options: RunOptions = {}): Promise { + return this.call(action, { + ...(options.actor ? { actor: options.actor } : {}), + input + }); + } + + async verify(receipt: CanonicalReceipt | CommandResponse, options: VerifyOptions = {}): Promise { + return verifyReceipt(receipt, { + ...(this.verifyDefaults || {}), + ...options + }); + } + async call(verb: Verb, body: Record): Promise { if (!isVerb(verb)) throw new CommandLayerError(`Unsupported verb: ${verb}`, 400); this.ensureVerifyConfigIfEnabled(); @@ -559,3 +586,15 @@ export class CommandLayerClient { export function createClient(opts: ClientOptions = {}) { return new CommandLayerClient(opts); } + +const defaultClient = createClient(); + +export const commandlayer = { + run(action: Verb, input: Record, options?: RunOptions) { + return defaultClient.run(action, input, options); + }, + async verify(receipt: CanonicalReceipt | CommandResponse, options?: VerifyOptions): Promise { + const result = await defaultClient.verify(receipt, options); + return { ...result, valid: result.ok }; + } +}; diff --git a/typescript-sdk/tests/high-level-api.test.mjs b/typescript-sdk/tests/high-level-api.test.mjs new file mode 100644 index 0000000..48d4b64 --- /dev/null +++ b/typescript-sdk/tests/high-level-api.test.mjs @@ -0,0 +1,54 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const { createClient, commandlayer, CommandLayerClient, verifyReceipt } = require("../dist/index.cjs"); + +test("createClient uses default baseUrl", () => { + const client = createClient(); + assert.equal(client.runtime, "https://runtime.commandlayer.org"); +}); + +test("custom baseUrl override works", () => { + const client = createClient({ baseUrl: "https://example.com/runtime/" }); + assert.equal(client.runtime, "https://example.com/runtime"); +}); + +test("commandlayer.run delegates to client.run", async () => { + const originalRun = CommandLayerClient.prototype.run; + CommandLayerClient.prototype.run = async function(action, input, options) { + return { receipt: { status: "success" }, runtime_metadata: { action, input, options } }; + }; + + try { + const result = await commandlayer.run("summarize", { text: "hello" }); + assert.equal(result.runtime_metadata.action, "summarize"); + assert.deepEqual(result.runtime_metadata.input, { text: "hello" }); + } finally { + CommandLayerClient.prototype.run = originalRun; + } +}); + +test("commandlayer.verify delegates to verification logic", async () => { + const originalVerify = CommandLayerClient.prototype.verify; + const sentinel = { ok: true, checks: {}, values: {}, errors: {} }; + + CommandLayerClient.prototype.verify = async function(receipt, options) { + assert.equal(typeof verifyReceipt, "function"); + assert.deepEqual(receipt, { receipt: { status: "success" } }); + assert.deepEqual(options, { publicKey: "ed25519:abc" }); + return sentinel; + }; + + try { + const result = await commandlayer.verify( + { receipt: { status: "success" } }, + { publicKey: "ed25519:abc" } + ); + assert.equal(result.ok, true); + assert.equal(result.valid, true); + } finally { + CommandLayerClient.prototype.verify = originalVerify; + } +});