@supercuts-walrus/backend 0.1.1 → 0.1.3

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.
Files changed (54) hide show
  1. package/README.md +28 -5
  2. package/config/phase1.testnet.json +20 -0
  3. package/config/proving-manifest.json +11 -0
  4. package/dist/buyer-setup.js +27 -13
  5. package/dist/cli.js +10 -2
  6. package/dist/dashboard.js +1 -1
  7. package/dist/deal-adapter.js +0 -2
  8. package/dist/deal-engine.d.ts +13 -0
  9. package/dist/deal-engine.js +64 -39
  10. package/dist/deal.d.ts +35 -0
  11. package/dist/deal.js +9 -3
  12. package/dist/embedding-adapter.d.ts +26 -0
  13. package/dist/embedding-adapter.js +116 -0
  14. package/dist/facilitator.js +2 -1
  15. package/dist/funding.js +10 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/live-deal-ports.d.ts +2 -1
  19. package/dist/live-deal-ports.js +31 -5
  20. package/dist/live-runtime.d.ts +1 -1
  21. package/dist/live-runtime.js +27 -11
  22. package/dist/live-seller-transport.js +5 -1
  23. package/dist/main-module.d.ts +5 -0
  24. package/dist/main-module.js +16 -0
  25. package/dist/mandate.js +1 -1
  26. package/dist/mcp-bootstrap.js +3 -3
  27. package/dist/mcp.js +2 -0
  28. package/dist/memory-scorecard.d.ts +32 -0
  29. package/dist/memory-scorecard.js +76 -0
  30. package/dist/memwal.d.ts +28 -0
  31. package/dist/memwal.js +81 -2
  32. package/dist/onboarding-doctor.js +12 -5
  33. package/dist/phase3.d.ts +43 -0
  34. package/dist/phase3.js +147 -32
  35. package/dist/role-lock.d.ts +1 -1
  36. package/dist/role-lock.js +1 -1
  37. package/dist/seam.js +2 -1
  38. package/dist/seller-catalog-generate.js +4 -4
  39. package/dist/seller-directory-cli.js +2 -1
  40. package/dist/seller-live-cli.js +10 -1
  41. package/dist/seller-provision.js +15 -1
  42. package/dist/seller-research.d.ts +50 -0
  43. package/dist/seller-research.js +210 -0
  44. package/dist/seller-scaffold.d.ts +1 -0
  45. package/dist/seller-scaffold.js +35 -0
  46. package/dist/seller-service.d.ts +1 -1
  47. package/dist/seller-service.js +8 -20
  48. package/dist/seller-setup.d.ts +3 -0
  49. package/dist/seller-setup.js +109 -0
  50. package/dist/settlement.d.ts +32 -3
  51. package/dist/settlement.js +56 -11
  52. package/dist/testnet-defaults.d.ts +21 -0
  53. package/dist/testnet-defaults.js +26 -0
  54. package/package.json +4 -2
@@ -48,6 +48,8 @@ async function main() {
48
48
  catch (error) {
49
49
  return fail("BRIEF_INVALID", "market brief must be JSON", error);
50
50
  }
51
+ if (report.reviewStatus !== "approved")
52
+ fail("ARTIFACT_REVIEW_REQUIRED", "market brief must be owner-reviewed before provisioning");
51
53
  if (typeof report.title !== "string" || typeof report.body !== "string" || !Array.isArray(report.citations) || report.citations.length < 2 || report.citations.some((value) => typeof value !== "string" || !/^https:\/\//.test(value)))
52
54
  fail("BRIEF_INVALID", "market brief needs a title, body, and at least two HTTPS citations");
53
55
  }
@@ -56,7 +58,19 @@ async function main() {
56
58
  for (const [briefHash, artifactPath] of Object.entries(capability.variants ?? {})) {
57
59
  if (!/^[0-9a-f]{64}$/.test(briefHash))
58
60
  fail("SELLER_CONFIG_INVALID", "variant brief hash is invalid");
59
- artifacts[`${manifest.capabilityId}:${briefHash}`] = readFileSync(resolve(artifactPath)).toString("base64url");
61
+ const body = readFileSync(resolve(artifactPath));
62
+ if (manifest.capability === "sui.competitor-analysis") {
63
+ let report;
64
+ try {
65
+ report = JSON.parse(body.toString());
66
+ }
67
+ catch (error) {
68
+ return fail("ANALYSIS_INVALID", "competitor analysis must be JSON", error);
69
+ }
70
+ if (report.reviewStatus !== "approved")
71
+ fail("ARTIFACT_REVIEW_REQUIRED", "competitor analysis must be owner-reviewed before provisioning");
72
+ }
73
+ artifacts[`${manifest.capabilityId}:${briefHash}`] = body.toString("base64url");
60
74
  }
61
75
  if (!capability.artifact && !Object.keys(capability.variants ?? {}).length)
62
76
  fail("SELLER_ARTIFACT_MISSING", "capability needs a prepared artifact or brief variants");
@@ -0,0 +1,50 @@
1
+ export type SellerResearchKind = "market-brief" | "competitor-analysis";
2
+ export interface SellerResearchDraft {
3
+ title: string;
4
+ body: string;
5
+ citations: string[];
6
+ preview: {
7
+ summary: string;
8
+ relevance: number;
9
+ sourceQuality: number;
10
+ freshnessDays: number;
11
+ };
12
+ reviewStatus: "draft-needs-owner-review";
13
+ research: {
14
+ model: string;
15
+ generatedAt: string;
16
+ sourceCount: number;
17
+ topic: string;
18
+ kind: SellerResearchKind;
19
+ };
20
+ }
21
+ export interface SellerResearchOptions {
22
+ apiKey?: string;
23
+ model?: string;
24
+ fetch?: typeof globalThis.fetch;
25
+ maxSearches?: number;
26
+ maxTokens?: number;
27
+ allowedDomains?: string[];
28
+ now?: Date;
29
+ }
30
+ /** Generates an owner-review draft. It never publishes or mutates a live seller. */
31
+ export declare class ClaudeSellerResearcher {
32
+ #private;
33
+ readonly model: string;
34
+ constructor(options?: SellerResearchOptions);
35
+ research(input: {
36
+ kind: SellerResearchKind;
37
+ topic: string;
38
+ context?: Record<string, unknown>;
39
+ }): Promise<SellerResearchDraft>;
40
+ }
41
+ export declare function generateSellerResearch(input: {
42
+ kind: string;
43
+ topic: string;
44
+ output: string;
45
+ contextFile?: string;
46
+ allowedDomains?: string;
47
+ apiKey?: string;
48
+ model?: string;
49
+ fetch?: typeof globalThis.fetch;
50
+ }): Promise<Record<string, unknown>>;
@@ -0,0 +1,210 @@
1
+ import { readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { fail } from "./errors.js";
4
+ const asObject = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : fail("SELLER_RESEARCH_INVALID", "Claude research output must be an object");
5
+ const submittedTool = (blocks) => {
6
+ for (const block of blocks)
7
+ if (block?.type === "tool_use" && block.name === "submit_research") {
8
+ if (block.input && typeof block.input === "object" && !Array.isArray(block.input))
9
+ return block;
10
+ if (typeof block.input === "string")
11
+ try {
12
+ const input = JSON.parse(block.input);
13
+ if (input && typeof input === "object" && !Array.isArray(input))
14
+ return { ...block, input };
15
+ }
16
+ catch { }
17
+ }
18
+ return undefined;
19
+ };
20
+ const normalizedUrl = (value) => { const url = new URL(value); url.hash = ""; if (url.pathname !== "/")
21
+ url.pathname = url.pathname.replace(/\/$/, ""); return url.toString(); };
22
+ function sourceUrls(value, found = new Set()) {
23
+ if (Array.isArray(value))
24
+ for (const item of value)
25
+ sourceUrls(item, found);
26
+ else if (value && typeof value === "object")
27
+ for (const [key, item] of Object.entries(value)) {
28
+ if (key === "url" && typeof item === "string") {
29
+ try {
30
+ if (new URL(item).protocol === "https:")
31
+ found.add(normalizedUrl(item));
32
+ }
33
+ catch { }
34
+ }
35
+ sourceUrls(item, found);
36
+ }
37
+ return found;
38
+ }
39
+ function validateDraft(value, evidence, allowedDomains) {
40
+ const draft = asObject(value), preview = asObject(draft.preview);
41
+ if (typeof draft.title !== "string" || draft.title.trim().length < 8 || draft.title.length > 180)
42
+ fail("SELLER_RESEARCH_INVALID", "research title must contain 8 to 180 characters");
43
+ if (typeof draft.body !== "string" || draft.body.trim().length < 800 || draft.body.length > 16_000)
44
+ fail("SELLER_RESEARCH_INVALID", "research body must contain 800 to 16,000 characters");
45
+ if (!Array.isArray(draft.citations))
46
+ fail("SELLER_RESEARCH_INVALID", "research citations must be an array");
47
+ if (draft.citations.length < 3 || draft.citations.length > 32)
48
+ fail("SELLER_RESEARCH_INVALID", `research must contain 3 to 32 citations; received ${draft.citations.length}`);
49
+ let citations = draft.citations.map((item) => {
50
+ if (typeof item !== "string")
51
+ return fail("SELLER_RESEARCH_INVALID", "every citation must be an HTTPS URL");
52
+ let url;
53
+ try {
54
+ url = new URL(item);
55
+ }
56
+ catch (error) {
57
+ return fail("SELLER_RESEARCH_INVALID", "every citation must be a valid URL", error);
58
+ }
59
+ if (url.protocol !== "https:" || url.username || url.password)
60
+ fail("SELLER_RESEARCH_INVALID", "every citation must be a public HTTPS URL without credentials");
61
+ if (allowedDomains.length && !allowedDomains.some((domain) => url.hostname === domain || url.hostname.endsWith(`.${domain}`)))
62
+ fail("SELLER_RESEARCH_SOURCE_UNAPPROVED", `citation host ${url.hostname} is outside --allowed-domains`);
63
+ const normalized = normalizedUrl(item);
64
+ if (!evidence.has(normalized))
65
+ fail("SELLER_RESEARCH_SOURCE_UNVERIFIED", "Claude cited a URL that was not returned by web search");
66
+ return normalized;
67
+ });
68
+ if (new Set(citations).size !== citations.length)
69
+ fail("SELLER_RESEARCH_INVALID", "research citations must be unique");
70
+ let reportBody = draft.body.trim();
71
+ const markers = [...reportBody.matchAll(/\[([1-9][0-9]*)\]/g)].map((match) => Number(match[1]));
72
+ if (markers.some((index) => index > citations.length))
73
+ fail("SELLER_RESEARCH_INVALID", "research body contains an out-of-range citation marker");
74
+ const used = [...new Set(markers)].sort((a, b) => a - b);
75
+ if (used.length < 3)
76
+ fail("SELLER_RESEARCH_INVALID", "research body must reference at least three listed sources inline");
77
+ if (used.length !== citations.length) {
78
+ const remap = new Map(used.map((oldIndex, newIndex) => [oldIndex, newIndex + 1]));
79
+ reportBody = reportBody.replace(/\[([1-9][0-9]*)\]/g, (_marker, raw) => `[${remap.get(Number(raw))}]`);
80
+ citations = used.map((index) => citations[index - 1]);
81
+ }
82
+ if (typeof preview.summary !== "string" || preview.summary.trim().length < 30 || preview.summary.length > 400)
83
+ fail("SELLER_RESEARCH_INVALID", "preview summary must contain 30 to 400 characters");
84
+ for (const key of ["relevance", "sourceQuality"])
85
+ if (typeof preview[key] !== "number" || !Number.isFinite(preview[key]) || preview[key] < 0 || preview[key] > 1)
86
+ fail("SELLER_RESEARCH_INVALID", `preview ${key} must be in [0,1]`);
87
+ if (!Number.isInteger(preview.freshnessDays) || preview.freshnessDays < 0 || preview.freshnessDays > 365)
88
+ fail("SELLER_RESEARCH_INVALID", "preview freshnessDays must be an integer in [0,365]");
89
+ return { title: draft.title.trim(), body: reportBody, citations, preview: { summary: preview.summary.trim(), relevance: preview.relevance, sourceQuality: preview.sourceQuality, freshnessDays: preview.freshnessDays } };
90
+ }
91
+ /** Generates an owner-review draft. It never publishes or mutates a live seller. */
92
+ export class ClaudeSellerResearcher {
93
+ model;
94
+ #apiKey;
95
+ #fetch;
96
+ #maxSearches;
97
+ #maxTokens;
98
+ #allowedDomains;
99
+ #now;
100
+ constructor(options = {}) {
101
+ this.#apiKey = options.apiKey ?? process.env.CLAUDE_API_KEY ?? "";
102
+ if (!this.#apiKey)
103
+ fail("CLAUDE_CREDENTIAL_MISSING", "CLAUDE_API_KEY is required to generate a seller research draft");
104
+ this.model = options.model ?? process.env.CLAUDE_RESEARCH_MODEL ?? process.env.CLAUDE_MODEL ?? "claude-haiku-5-5";
105
+ this.#fetch = options.fetch ?? fetch;
106
+ this.#maxSearches = options.maxSearches ?? 8;
107
+ this.#maxTokens = options.maxTokens ?? 5_000;
108
+ this.#allowedDomains = (options.allowedDomains ?? []).map((item) => item.trim().toLowerCase()).filter(Boolean);
109
+ this.#now = options.now ?? new Date();
110
+ if (!Number.isInteger(this.#maxSearches) || this.#maxSearches < 3 || this.#maxSearches > 12 || !Number.isInteger(this.#maxTokens) || this.#maxTokens < 1_500 || this.#maxTokens > 8_192)
111
+ fail("CLAUDE_LIMIT_INVALID", "seller research bounds are invalid");
112
+ if (this.#allowedDomains.some((item) => !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(item)))
113
+ fail("SELLER_RESEARCH_INVALID", "allowed domains must be bare DNS names");
114
+ }
115
+ async research(input) {
116
+ if (!["market-brief", "competitor-analysis"].includes(input.kind) || !input.topic.trim() || input.topic.length > 500)
117
+ fail("SELLER_RESEARCH_INVALID", "research needs a valid kind and a topic of at most 500 characters");
118
+ const context = input.context ? JSON.stringify(input.context) : "none";
119
+ if (context.length > 40_000)
120
+ fail("SELLER_RESEARCH_INVALID", "matching brief context is too large");
121
+ const schema = {
122
+ type: "object", additionalProperties: false,
123
+ properties: {
124
+ title: { type: "string", maxLength: 180 }, body: { type: "string", maxLength: 16000 }, citations: { type: "array", minItems: 3, maxItems: 32, items: { type: "string" } },
125
+ preview: { type: "object", additionalProperties: false, properties: { summary: { type: "string", maxLength: 400 }, relevance: { type: "number", minimum: 0, maximum: 1 }, sourceQuality: { type: "number", minimum: 0, maximum: 1 }, freshnessDays: { type: "integer", minimum: 0, maximum: 365 } }, required: ["summary", "relevance", "sourceQuality", "freshnessDays"] },
126
+ }, required: ["title", "body", "citations", "preview"],
127
+ };
128
+ const webSearch = { type: "web_search_20250305", name: "web_search", max_uses: this.#maxSearches, allowed_callers: ["direct"] };
129
+ if (this.#allowedDomains.length)
130
+ webSearch.allowed_domains = this.#allowedDomains;
131
+ const system = `<role>You are the research desk for a paid Sui ecosystem intelligence seller.</role>
132
+ <quality_bar>Use web search before writing. Prefer primary sources, official documentation, project announcements, repositories, and dated first-party metrics. Cross-check material claims across independent sources. Separate observed facts from analysis, name dates and measurement windows, surface uncertainty and counter-evidence, and never invent a fact, quotation, number, or URL. The report must be useful to a technically sophisticated buyer, not promotional copy.</quality_bar>
133
+ <format>After research and a final self-check, call submit_research exactly once. Write a cohesive 800-16000 character report. Include inline source markers such as [1] that correspond in order to the citations array. Use only HTTPS URLs actually returned by web_search. Provide at least 3 unique sources and include additional material sources when useful. Preview scores are seller estimates, not independent guarantees.</format>`;
134
+ const prompt = `<today>${this.#now.toISOString().slice(0, 10)}</today><kind>${input.kind}</kind><topic>${input.topic.trim()}</topic><matching_brief_context>${context}</matching_brief_context><instructions>Research the topic broadly enough to identify market structure, concrete evidence, important competitors or alternatives, risks, and what would change the conclusion. For competitor analysis, directly compare the purchased brief's thesis with named competing projects or approaches. Before submission, verify every citation was observed in search results and every quantitative claim is qualified by date and source.</instructions>`;
135
+ const request = async (payload) => {
136
+ let response;
137
+ try {
138
+ response = await this.#fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "content-type": "application/json", "anthropic-version": "2023-06-01", "x-api-key": this.#apiKey }, body: JSON.stringify(payload), signal: AbortSignal.timeout(90_000) });
139
+ }
140
+ catch (error) {
141
+ return fail("CLAUDE_UNAVAILABLE", "Claude research request failed closed", error);
142
+ }
143
+ if (!response.ok) {
144
+ await response.arrayBuffer();
145
+ return fail("CLAUDE_UNAVAILABLE", `Claude research returned status ${response.status}`);
146
+ }
147
+ try {
148
+ return await response.json();
149
+ }
150
+ catch (error) {
151
+ return fail("CLAUDE_OUTPUT_INVALID", "Claude research returned invalid JSON", error);
152
+ }
153
+ };
154
+ const submitTool = { name: "submit_research", description: "Submit the source-checked seller research draft for owner review.", input_schema: schema };
155
+ let messages = [{ role: "user", content: prompt }], selected;
156
+ const blocks = [];
157
+ for (let turn = 0; turn < 3; turn++) {
158
+ const body = await request({ model: this.model, max_tokens: this.#maxTokens, system, messages, tools: [webSearch, submitTool] });
159
+ const current = Array.isArray(body.content) ? body.content : [];
160
+ blocks.push(...current);
161
+ selected = submittedTool(current);
162
+ if (selected || current.some((block) => block?.type === "text" && typeof block.text === "string" && block.text.trim()))
163
+ break;
164
+ if (!current.length || turn === 2)
165
+ break;
166
+ // Anthropic can pause a server-side search turn. Replay its opaque content
167
+ // unchanged so the model can synthesize the already-returned evidence.
168
+ messages = [...messages, { role: "assistant", content: current }, { role: "user", content: "Continue the paused research turn using the returned search evidence, then submit the structured draft." }];
169
+ }
170
+ const searchFailure = blocks.some((block) => block?.type === "web_search_tool_result" && (block.is_error === true || block.content?.type === "web_search_tool_result_error"));
171
+ if (searchFailure)
172
+ fail("SELLER_RESEARCH_SEARCH_FAILED", "Claude web search failed; no draft was written");
173
+ const evidence = sourceUrls(blocks);
174
+ if (evidence.size < 3)
175
+ fail("SELLER_RESEARCH_SOURCE_UNVERIFIED", "Claude returned fewer than three verifiable web-search sources");
176
+ if (selected)
177
+ try {
178
+ const draft = validateDraft(selected.input, evidence, this.#allowedDomains);
179
+ return { ...draft, reviewStatus: "draft-needs-owner-review", research: { model: this.model, generatedAt: this.#now.toISOString(), sourceCount: evidence.size, topic: input.topic.trim(), kind: input.kind } };
180
+ }
181
+ catch { }
182
+ const researchedText = blocks.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n").slice(0, 30_000) || (selected ? JSON.stringify(selected.input).slice(0, 30_000) : "");
183
+ if (!researchedText)
184
+ fail("CLAUDE_OUTPUT_INVALID", "Claude returned search evidence without a research answer");
185
+ const editorSystem = `<role>You are a strict research editor.</role><instructions>Convert or repair the supplied web-researched answer into the submit_research schema. Preserve material uncertainty, dates, comparisons, and counter-evidence. Use only URLs in verified_sources, include at least 3 unique citations and all additional material sources, and do not add facts or sources. The body must be 800-16000 characters and use [1], [2], and subsequent inline markers corresponding to citation order. Ensure preview is an object with summary, relevance, sourceQuality, and integer freshnessDays. Call submit_research exactly once.</instructions>`;
186
+ const edited = await request({ model: this.model, max_tokens: this.#maxTokens, system: editorSystem, messages: [{ role: "user", content: JSON.stringify({ kind: input.kind, topic: input.topic.trim(), researchedAnswer: researchedText, verifiedSources: [...evidence].slice(0, 32) }) }], tools: [submitTool], tool_choice: { type: "tool", name: "submit_research" } });
187
+ const editedBlocks = Array.isArray(edited.content) ? edited.content : [];
188
+ selected = submittedTool(editedBlocks);
189
+ if (!selected)
190
+ fail("CLAUDE_OUTPUT_INVALID", "Claude did not submit a structured research draft");
191
+ const draft = validateDraft(selected.input, evidence, this.#allowedDomains);
192
+ return { ...draft, reviewStatus: "draft-needs-owner-review", research: { model: this.model, generatedAt: this.#now.toISOString(), sourceCount: evidence.size, topic: input.topic.trim(), kind: input.kind } };
193
+ }
194
+ }
195
+ export async function generateSellerResearch(input) {
196
+ const kind = input.kind;
197
+ const output = resolve(input.output || fail("ARGUMENT_REQUIRED", "seller research requires --output"));
198
+ let context;
199
+ if (input.contextFile) {
200
+ const path = resolve(input.contextFile);
201
+ if ((statSync(path).mode & 0o077) !== 0)
202
+ fail("SELLER_CONFIG_PERMISSIONS", "research context file must be owner-only");
203
+ context = asObject(JSON.parse(readFileSync(path, "utf8")));
204
+ }
205
+ const allowedDomains = input.allowedDomains?.split(",").map((item) => item.trim()).filter(Boolean);
206
+ const researcher = new ClaudeSellerResearcher({ ...(input.apiKey ? { apiKey: input.apiKey } : {}), ...(input.model ? { model: input.model } : {}), ...(input.fetch ? { fetch: input.fetch } : {}), ...(allowedDomains?.length ? { allowedDomains } : {}) });
207
+ const draft = await researcher.research({ kind, topic: input.topic, ...(context ? { context } : {}) });
208
+ writeFileSync(output, `${JSON.stringify(draft, null, 2)}\n`, { mode: 0o600, flag: "wx" });
209
+ return { output, kind, topic: input.topic, model: researcher.model, citations: draft.citations.length, reviewStatus: draft.reviewStatus, nextStep: "Review every claim and citation, then set reviewStatus to approved before catalog generation or seller packaging." };
210
+ }
@@ -0,0 +1 @@
1
+ export declare function scaffoldSeller(input: Record<string, string | boolean>): Record<string, unknown>;
@@ -0,0 +1,35 @@
1
+ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { fail } from "./errors.js";
4
+ const idPattern = /^[a-z0-9][a-z0-9._-]{1,127}$/;
5
+ export function scaffoldSeller(input) {
6
+ const home = typeof input.home === "string" ? resolve(input.home) : fail("ARGUMENT_REQUIRED", "seller scaffold requires --home");
7
+ const output = typeof input.output === "string" ? resolve(input.output) : fail("ARGUMENT_REQUIRED", "seller scaffold requires --output");
8
+ const capabilityId = String(input["capability-id"] ?? ""), capability = String(input.capability ?? ""), description = String(input.description ?? ""), origin = String(input.origin ?? ""), price = String(input.price ?? "");
9
+ if (!idPattern.test(capabilityId) || !idPattern.test(capability))
10
+ fail("SELLER_SCAFFOLD_INVALID", "capability identifiers must use 2..128 lowercase letters, digits, dot, underscore, or dash");
11
+ if (!description || description.length > 512 || !/^[1-9][0-9]*$/.test(price))
12
+ fail("SELLER_SCAFFOLD_INVALID", "description and positive MIST --price are required");
13
+ const url = new URL(origin);
14
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(url.hostname)) || url.username || url.password || url.pathname !== "/" || url.search || url.hash)
15
+ fail("SELLER_SCAFFOLD_INVALID", "origin must be an HTTPS or loopback HTTP origin without path or credentials");
16
+ const rails = String(input.rails ?? "x402").split(",").map((rail) => rail.trim()).filter(Boolean);
17
+ if (!rails.length || rails.some((rail) => rail !== "x402" && rail !== "shielded") || new Set(rails).size !== rails.length)
18
+ fail("SELLER_SCAFFOLD_INVALID", "--rails must be x402, shielded, or x402,shielded");
19
+ const days = Number(input["expires-in-days"] ?? "30");
20
+ if (!Number.isSafeInteger(days) || days < 1 || days > 365)
21
+ fail("SELLER_SCAFFOLD_INVALID", "--expires-in-days must be 1..365");
22
+ const listenPort = Number(input["listen-port"] ?? (url.protocol === "http:" && url.port ? url.port : "8788"));
23
+ if (!Number.isSafeInteger(listenPort) || listenPort < 1 || listenPort > 65535)
24
+ fail("SELLER_SCAFFOLD_INVALID", "--listen-port must be 1..65535");
25
+ mkdirSync(output, { recursive: false, mode: 0o700 });
26
+ chmodSync(output, 0o700);
27
+ const specPath = resolve(output, "capability.json"), handlerPath = resolve(output, "handler.mjs"), registrationPath = resolve(output, "registration.json"), servicePath = resolve(output, "seller-service.json");
28
+ const spec = { capabilityId, capability, description, inputSchema: { type: "object", required: ["task"], properties: { task: { type: "string", minLength: 1, maxLength: 4096 } }, additionalProperties: false }, outputSchema: { type: "object", required: ["kind"], properties: { kind: { const: capability } }, additionalProperties: false }, supportedRails: rails, priceBand: { minimum: price, maximum: price }, asset: "0x2::sui::SUI", endpoint: `${url.origin}/v2/resource/${capabilityId}`, expiresAt: Date.now() + days * 86_400_000 };
29
+ const handler = `export async function handle(request) {\n const result = { kind: ${JSON.stringify(capability)}, task: request.input.task, message: "Replace this template with your seller implementation." };\n return { artifact: Buffer.from(JSON.stringify(result)), contentType: "application/json", metadata: { kind: ${JSON.stringify(capability)} } };\n}\n`;
30
+ const service = { origin: url.origin, listenHost: "127.0.0.1", listenPort, ...(rails.includes("x402") ? { facilitatorUrlEnv: "X402_FACILITATOR_URL", facilitatorKeyEnv: "FACILITATOR_SHARED_KEY" } : {}), capabilities: [{ registrationFile: registrationPath, handler: handlerPath }] };
31
+ writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`, { mode: 0o600, flag: "wx" });
32
+ writeFileSync(handlerPath, handler, { mode: 0o600, flag: "wx" });
33
+ writeFileSync(servicePath, `${JSON.stringify(service, null, 2)}\n`, { mode: 0o600, flag: "wx" });
34
+ return { home, output, specFile: specPath, handlerFile: handlerPath, registrationFile: registrationPath, serviceFile: servicePath, expiresAt: spec.expiresAt, nextCommands: [`supercuts-seller-register ${JSON.stringify(home)} ${JSON.stringify(specPath)} ${JSON.stringify(registrationPath)}`, `supercuts-seller-live ${JSON.stringify(home)} ${JSON.stringify(servicePath)}`], note: "Review and replace the generated handler before serving production work." };
35
+ }
@@ -18,7 +18,7 @@ export declare class SellerService {
18
18
  facilitatorUrl: string;
19
19
  facilitatorKey: string;
20
20
  allowedBuyers: ReadonlySet<string>;
21
- claudeKey: string;
21
+ claudeKey?: string;
22
22
  directoryAggregator?: string;
23
23
  }): Promise<SellerService>;
24
24
  private artifact;
@@ -4,7 +4,6 @@ import { createServer } from "node:http";
4
4
  import { mkdtempSync, rmSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
- import { BoundedClaude } from "./claude.js";
8
7
  import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
9
8
  import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
10
9
  import { isValidPersonalMessageSignature } from "@mysten/sui/verify";
@@ -18,6 +17,7 @@ import { captureSellerHome, decryptSellerSnapshot, encryptSellerSnapshot, restor
18
17
  import { X402ResourceService } from "./x402-resource-service.js";
19
18
  import { LiveSellerTransport } from "./live-seller-transport.js";
20
19
  import { registerDirectoryCapability } from "./seller-directory.js";
20
+ import { isMainModule } from "./main-module.js";
21
21
  const isV2 = (row) => row.preview.genericV2 === true;
22
22
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
23
23
  const validId = (value, name) => typeof value === "string" && /^[a-zA-Z0-9._:-]{1,128}$/.test(value) ? value : fail("SELLER_REQUEST_INVALID", `${name} is invalid`);
@@ -35,10 +35,9 @@ export class SellerService {
35
35
  #v2 = new Map();
36
36
  #v2BySeller = new Map();
37
37
  #root = mkdtempSync(join(tmpdir(), "supercuts-sellers-"));
38
- #claude;
39
38
  #queue = Promise.resolve();
40
39
  #healthy = true;
41
- constructor(db, snapshotKey, origin, facilitatorUrl, facilitatorKey, allowedBuyers, claudeKey, directoryAggregator) {
40
+ constructor(db, snapshotKey, origin, facilitatorUrl, facilitatorKey, allowedBuyers, directoryAggregator) {
42
41
  this.db = db;
43
42
  this.snapshotKey = snapshotKey;
44
43
  this.origin = origin;
@@ -46,10 +45,9 @@ export class SellerService {
46
45
  this.facilitatorKey = facilitatorKey;
47
46
  this.allowedBuyers = allowedBuyers;
48
47
  this.directoryAggregator = directoryAggregator;
49
- this.#claude = new BoundedClaude({ apiKey: claudeKey });
50
48
  }
51
49
  static async create(config) {
52
- const service = new SellerService(config.db, config.snapshotKey, config.origin, config.facilitatorUrl, config.facilitatorKey, config.allowedBuyers, config.claudeKey, config.directoryAggregator);
50
+ const service = new SellerService(config.db, config.snapshotKey, config.origin, config.facilitatorUrl, config.facilitatorKey, config.allowedBuyers, config.directoryAggregator);
53
51
  const rows = (await config.db.listCatalog()).filter((row) => !(isV2(row) && row.manifest.expiresAt <= Date.now()));
54
52
  for (const row of rows) {
55
53
  validateCapabilityManifest(row.manifest);
@@ -271,10 +269,6 @@ export class SellerService {
271
269
  const opening = BigInt(String(offer.opening ?? "0")), maximum = BigInt(String(offer.maximum ?? "0")), minimum = BigInt(row.manifest.priceBand.minimum), briefHash = typeof offer.briefHash === "string" ? offer.briefHash : undefined;
272
270
  if (opening <= 0n || opening > maximum || maximum < minimum || maximum > BigInt(row.manifest.priceBand.maximum) || briefHash !== undefined && !/^[0-9a-f]{64}$/.test(briefHash))
273
271
  fail("NEGOTIATION_BOUNDS_INVALID", "buyer offer exceeds approved price bounds");
274
- const task = String(offer.task ?? "").slice(0, 512);
275
- const decision = await this.#claude.decide({ system: "You are a bounded research seller. Present only the fixed price and service terms approved by Supercuts.", task, candidates: [{ action: "counteroffer", capabilityId, amount: minimum.toString() }], schema: { name: "respond_to_private_offer", description: "Present the approved exact price.", properties: { action: { type: "string", enum: ["counteroffer"] } }, required: ["action"] } });
276
- if (decision.action.action !== "counteroffer")
277
- fail("SELLER_DECISION_INVALID", "seller did not choose the approved response");
278
272
  const reply = { capabilityId, amount: minimum.toString(), asset: row.manifest.asset, railOptions: row.manifest.supportedRails, accepted: true };
279
273
  const replyCiphertext = encryptSessionMessage(key, sessionId, `${sessionId}:seller:2`, Buffer.from(JSON.stringify(reply)));
280
274
  const requestBlobId = await this.publishCiphertext(slot, ciphertext), replyBlobId = await this.publishCiphertext(slot, replyCiphertext);
@@ -291,7 +285,7 @@ export class SellerService {
291
285
  this.#healthy = false;
292
286
  throw error;
293
287
  }
294
- await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "negotiation", status: "complete", details: { capabilityId, sessionId, requestBlobId, replyBlobId, transcriptHash, model: decision.model } });
288
+ await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "negotiation", status: "complete", details: { capabilityId, sessionId, requestBlobId, replyBlobId, transcriptHash, decision: "policy-validated-fixed-price" } });
295
289
  return this.reply(response, 200, { sessionId, capabilityId, ciphertext: replyCiphertext, signature, requestBlobId, replyBlobId, transcriptHash });
296
290
  });
297
291
  }
@@ -316,9 +310,6 @@ export class SellerService {
316
310
  const prior = slot.app.store.db.prepare("SELECT operation_id AS operationId,capability_id AS capabilityId,amount,artifact_key AS artifactKey,run_id AS runId FROM remote_quotes WHERE request_id=?").get(requestId);
317
311
  if (prior && JSON.stringify(prior) !== JSON.stringify({ operationId, capabilityId, amount: quote.amount, artifactKey: quote.artifactKey, runId }))
318
312
  fail("SELLER_QUOTE_CONFLICT", "request ID identifies another quote");
319
- const decision = await this.#claude.decide({ system: "You are a bounded seller agent. You may present only the fixed approved quote.", task: String(input.task ?? "Sui market brief").slice(0, 512), candidates: [{ action: "present_quote", capabilityId, amount: quote.amount }], schema: { name: "respond_to_buyer", description: "Present the approved exact price.", properties: { action: { type: "string", enum: ["present_quote"] } }, required: ["action"] } });
320
- if (decision.action.action !== "present_quote")
321
- fail("SELLER_DECISION_INVALID", "seller did not choose the approved quote");
322
313
  slot.app.store.db.prepare("INSERT OR IGNORE INTO remote_quotes(request_id,operation_id,capability_id,amount,artifact_key,run_id) VALUES(?,?,?,?,?,?)").run(requestId, operationId, capabilityId, quote.amount, quote.artifactKey, runId);
323
314
  const artifact = this.artifact(slot, quote.artifactKey), registration = this.resource(slot, row).register(requestId, operationId, artifact);
324
315
  try {
@@ -328,7 +319,7 @@ export class SellerService {
328
319
  this.#healthy = false;
329
320
  throw error;
330
321
  }
331
- await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "quote", status: "complete", details: { capabilityId, amount: quote.amount, model: decision.model } });
322
+ await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "quote", status: "complete", details: { capabilityId, amount: quote.amount, decision: "manifest-fixed-price" } });
332
323
  return this.reply(response, 200, { capabilityId, sellerId: slot.sellerId, amount: quote.amount, asset: row.manifest.asset, requestId, operationId, resourceUrl: registration.url, artifactSha256: registration.artifactSha256 });
333
324
  });
334
325
  }
@@ -356,9 +347,6 @@ export class SellerService {
356
347
  const prior = slot.app.store.db.prepare("SELECT capability_id AS capabilityId,artifact_key AS artifactKey,delivery_public_key AS deliveryPublicKey,run_id AS runId FROM remote_deliveries WHERE session_id=?").get(sessionId);
357
348
  if (prior && JSON.stringify(prior) !== JSON.stringify({ capabilityId, artifactKey, deliveryPublicKey, runId }))
358
349
  fail("AGREEMENT_SESSION_CONFLICT", "session identifies another delivery");
359
- const decision = await this.#claude.decide({ system: "You are a bounded seller agent. Sign only the backend-verified agreement.", task: `Respond to accepted competitor analysis request for ${capabilityId}.`, candidates: [{ action: "sign_approved_agreement", capabilityId }], schema: { name: "respond_to_private_offer", description: "Approve the backend-verified agreement.", properties: { action: { type: "string", enum: ["sign_approved_agreement"] } }, required: ["action"] } });
360
- if (decision.action.action !== "sign_approved_agreement")
361
- fail("SELLER_DECISION_INVALID", "seller did not choose the approved agreement");
362
350
  const signed = await executeMandated(slot.app, loadMandate(slot.home), "offer.counter", { "operation-id": `remote-counter-${sessionId}`, session: sessionId, agreement: encoded });
363
351
  slot.app.store.db.prepare("INSERT OR IGNORE INTO remote_deliveries(session_id,capability_id,artifact_key,delivery_public_key,run_id) VALUES(?,?,?,?,?)").run(sessionId, capabilityId, artifactKey, deliveryPublicKey, runId);
364
352
  try {
@@ -368,7 +356,7 @@ export class SellerService {
368
356
  this.#healthy = false;
369
357
  throw error;
370
358
  }
371
- await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "agreement", status: "complete", details: { capabilityId, sessionId, model: decision.model, dealCommitment: signed.dealCommitment } });
359
+ await this.db.addEvent({ run_id: runId, seller_id: slot.sellerId, stage: "agreement", status: "complete", details: { capabilityId, sessionId, decision: "backend-verified-agreement", dealCommitment: signed.dealCommitment } });
372
360
  return this.reply(response, 200, { sessionId, capabilityId, agreement: signed.agreement, dealCommitment: signed.dealCommitment, artifactSha256 });
373
361
  });
374
362
  }
@@ -412,11 +400,11 @@ export class SellerService {
412
400
  close() { for (const slot of this.#slots.values())
413
401
  slot.app.close(); rmSync(this.#root, { recursive: true, force: true }); }
414
402
  }
415
- if (process.argv[1] && import.meta.url.endsWith(process.argv[1])) {
403
+ if (isMainModule(import.meta.url)) {
416
404
  const required = (key) => process.env[key] ?? fail("SELLER_CONFIG_MISSING", `${key} is required`);
417
405
  const port = Number(process.env.PORT ?? "3000");
418
406
  const origin = required("SELLER_ORIGIN");
419
- SellerService.create({ db: new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY")), snapshotKey: required("SELLER_STATE_KEY"), origin, facilitatorUrl: required("FACILITATOR_URL"), facilitatorKey: required("FACILITATOR_SHARED_KEY"), allowedBuyers: new Set((process.env.SELLER_ALLOWED_BUYERS ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean)), claudeKey: required("CLAUDE_API_KEY"), ...(process.env.DIRECTORY_WALRUS_AGGREGATOR ? { directoryAggregator: process.env.DIRECTORY_WALRUS_AGGREGATOR } : {}) })
407
+ SellerService.create({ db: new SellerDatabase(required("SUPABASE_URL"), required("SUPABASE_SERVICE_ROLE_KEY")), snapshotKey: required("SELLER_STATE_KEY"), origin, facilitatorUrl: required("FACILITATOR_URL"), facilitatorKey: required("FACILITATOR_SHARED_KEY"), allowedBuyers: new Set((process.env.SELLER_ALLOWED_BUYERS ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean)), ...(process.env.DIRECTORY_WALRUS_AGGREGATOR ? { directoryAggregator: process.env.DIRECTORY_WALRUS_AGGREGATOR } : {}) })
420
408
  .then((service) => { const server = service.createServer(); server.listen(port, "0.0.0.0"); const close = () => server.close(() => { service.close(); process.exit(0); }); process.on("SIGINT", close); process.on("SIGTERM", close); })
421
409
  .catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : "seller service failed"}\n`); process.exitCode = 1; });
422
410
  }
@@ -0,0 +1,3 @@
1
+ export declare function createSellerSession(home: string): Record<string, unknown>;
2
+ export declare function setupSeller(home: string, configPath: string): Record<string, unknown>;
3
+ export declare function promptSellerSetup(home: string): Promise<Record<string, unknown>>;
@@ -0,0 +1,109 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
6
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
7
+ import { fail } from "./errors.js";
8
+ import { TESTNET_DEFAULTS, withTestnetDefaults } from "./testnet-defaults.js";
9
+ import { initialize } from "./phase3.js";
10
+ function ownerFile(path) {
11
+ if ((statSync(path).mode & 0o077) !== 0)
12
+ fail("SETUP_PERMISSIONS", `${path} must be owner-only`);
13
+ }
14
+ function validateConfig(config) {
15
+ withTestnetDefaults(config);
16
+ for (const key of ["deploymentConfig", "tatumKeyFile", "writeGrpc", "tatumGrpc", "walrusPublisher", "walrusAggregator"]) {
17
+ if (typeof config[key] !== "string" || !config[key])
18
+ fail("SETUP_INVALID", `${key} is required`);
19
+ }
20
+ ownerFile(resolve(config.tatumKeyFile));
21
+ if (config.suiKeyFile)
22
+ ownerFile(resolve(config.suiKeyFile));
23
+ const deployment = JSON.parse(readFileSync(resolve(config.deploymentConfig), "utf8"));
24
+ if (deployment.network !== "testnet" || deployment.testOnly !== true || deployment.verifierVersion !== 2)
25
+ fail("SETUP_INVALID", "seller setup requires the pinned experimental v2 Testnet deployment");
26
+ return config;
27
+ }
28
+ function readConfig(path) {
29
+ ownerFile(path);
30
+ return validateConfig(JSON.parse(readFileSync(path, "utf8")));
31
+ }
32
+ export function createSellerSession(home) {
33
+ if (!existsSync(join(home, "profile.json")))
34
+ fail("SETUP_REQUIRED", "initialize the seller before creating an agent session");
35
+ const mcpScript = join(dirname(fileURLToPath(import.meta.url)), "mcp-bootstrap.js");
36
+ const mcp = `[mcp_servers.supercuts-seller]\ncommand = ${JSON.stringify(process.execPath)}\nargs = ${JSON.stringify([mcpScript])}\n[mcp_servers.supercuts-seller.env]\nSUPERCUTS_HOME = ${JSON.stringify(home)}\n`;
37
+ const mcpConfig = join(home, "seller-codex.toml");
38
+ writeFileSync(mcpConfig, mcp, { mode: 0o600 });
39
+ chmodSync(mcpConfig, 0o600);
40
+ return { mcpConfig };
41
+ }
42
+ function setupSellerConfig(home, config) {
43
+ if (existsSync(join(home, "profile.json")))
44
+ fail("PROFILE_EXISTS", "seller home already exists; create a separate home for another seller identity");
45
+ mkdirSync(home, { recursive: true, mode: 0o700 });
46
+ chmodSync(home, 0o700);
47
+ const keyPath = config.suiKeyFile ? resolve(config.suiKeyFile) : join(home, "seller.sui.key");
48
+ if (!config.suiKeyFile)
49
+ writeFileSync(keyPath, Ed25519Keypair.generate().getSecretKey(), { mode: 0o600, flag: "wx" });
50
+ ownerFile(keyPath);
51
+ initialize(home, {
52
+ role: "seller",
53
+ "deployment-config": resolve(config.deploymentConfig),
54
+ "sui-key-file": keyPath,
55
+ "tatum-key-file": resolve(config.tatumKeyFile),
56
+ "write-grpc": config.writeGrpc,
57
+ "tatum-grpc": config.tatumGrpc,
58
+ "walrus-publisher": config.walrusPublisher,
59
+ "walrus-aggregator": config.walrusAggregator,
60
+ ...(config.snarkjs ? { snarkjs: resolve(config.snarkjs) } : {}),
61
+ ...(config.converter ? { converter: resolve(config.converter) } : {}),
62
+ ...(config.rapidsnark ? { rapidsnark: resolve(config.rapidsnark) } : {}),
63
+ });
64
+ const decoded = decodeSuiPrivateKey(readFileSync(keyPath, "utf8").trim());
65
+ if (decoded.scheme !== "ED25519")
66
+ fail("SETUP_INVALID", "seller key must use ED25519");
67
+ const address = Ed25519Keypair.fromSecretKey(decoded.secretKey).toSuiAddress();
68
+ return {
69
+ home,
70
+ address,
71
+ fundingRequired: false,
72
+ warning: "Do not faucet-fund the seller. Only a generated buyer address is funded for live purchases.",
73
+ ...createSellerSession(home),
74
+ nextStep: `Run supercuts seller scaffold --home ${JSON.stringify(home)} --output <offer-dir> with the capability, origin, price, rails, and expiry options. Review the handler, then run the printed registration and service commands.`,
75
+ };
76
+ }
77
+ export function setupSeller(home, configPath) {
78
+ return setupSellerConfig(home, readConfig(resolve(configPath)));
79
+ }
80
+ export async function promptSellerSetup(home) {
81
+ if (!process.stdin.isTTY)
82
+ fail("SETUP_INPUT_REQUIRED", "interactive seller setup requires a terminal; use --config with an owner-only JSON file");
83
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
84
+ try {
85
+ const ask = async (label, fallback) => (await rl.question(fallback ? `${label} [${fallback}]: ` : `${label}: `)).trim() || fallback || "";
86
+ process.stderr.write("Press Enter to accept a bracketed default. Credentials are read from owner-only files, never typed here.\n");
87
+ const config = {
88
+ deploymentConfig: await ask("Pinned v2 Testnet deployment config path (blank for bundled config)"),
89
+ tatumKeyFile: await ask("Owner-only Tatum API key file path"),
90
+ writeGrpc: await ask("Sui write gRPC URL", TESTNET_DEFAULTS.writeGrpc),
91
+ tatumGrpc: await ask("Tatum read gRPC URL", TESTNET_DEFAULTS.tatumGrpc),
92
+ walrusPublisher: await ask("Walrus publisher URL", TESTNET_DEFAULTS.walrusPublisher),
93
+ walrusAggregator: await ask("Walrus aggregator URL", TESTNET_DEFAULTS.walrusAggregator),
94
+ };
95
+ const snarkjs = await ask("SnarkJS executable path (blank for deployment-relative default)");
96
+ const converter = await ask("Groth16 converter path (blank for deployment-relative default)");
97
+ const rapidsnark = await ask("RapidSnark executable path (blank to use SnarkJS)");
98
+ if (snarkjs)
99
+ config.snarkjs = snarkjs;
100
+ if (converter)
101
+ config.converter = converter;
102
+ if (rapidsnark)
103
+ config.rapidsnark = rapidsnark;
104
+ return setupSellerConfig(home, validateConfig(config));
105
+ }
106
+ finally {
107
+ rl.close();
108
+ }
109
+ }