@noimosai/platform-mcp 0.1.3 → 0.1.5
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 +17 -48
- package/bin/noimos-platform-mcp.mjs +4 -5
- package/dist/cli/cli.js +565 -0
- package/dist/cli/command-help.js +395 -0
- package/dist/cli/download.js +79 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/input-error.js +48 -0
- package/dist/cli/local-agents.js +199 -0
- package/dist/cli/mcp-supervisor.js +136 -0
- package/dist/cli/noimos-cli.js +72 -0
- package/dist/cli/project-archive.js +27 -0
- package/dist/cli/project-files.js +25 -0
- package/dist/cli/project-sync.js +265 -0
- package/dist/cli/setup-lock.js +1 -0
- package/dist/cli/setup.js +347 -0
- package/dist/cli/skills.js +142 -0
- package/dist/cli/team-harness-targets.js +337 -0
- package/dist/cli/team-harness.js +1562 -0
- package/dist/cli/team-resources.js +222 -0
- package/dist/cli/team-skills.js +457 -0
- package/dist/cli/update-bootstrap.js +437 -0
- package/dist/cli/update-release.js +212 -0
- package/dist/cli/version.js +1 -0
- package/dist/cli/wallet.js +85 -0
- package/dist/client/agent-run.js +1425 -0
- package/dist/client/browser.js +911 -0
- package/dist/client/distribution.js +2 -0
- package/dist/client/evm-wallet.js +214 -0
- package/dist/client/execution-context.js +14 -0
- package/dist/client/failure-recovery.js +128 -0
- package/dist/client/index.js +4 -225
- package/dist/client/local-input.js +33 -12
- package/dist/client/local-profile-lock.js +52 -0
- package/dist/client/local-profile.js +181 -0
- package/dist/client/local-projects.js +131 -0
- package/dist/client/local-setup.js +93 -0
- package/dist/client/realtime-voice.js +184 -0
- package/dist/client/tool-name-hints.js +48 -0
- package/dist/client/wallet-access.js +214 -0
- package/dist/mcp/agent-server.js +345 -0
- package/dist/mcp/context.js +86 -0
- package/dist/mcp/host-schema.js +76 -0
- package/dist/mcp/icon.js +1 -0
- package/dist/mcp/instructions.js +26 -0
- package/dist/mcp/local-projects.js +42 -0
- package/dist/mcp/local-upload.js +7 -13
- package/dist/mcp/noimos-runtime.js +18 -0
- package/dist/mcp/noimos-server.js +38 -0
- package/dist/mcp/operations.js +41 -0
- package/dist/mcp/parallel-result.js +17 -0
- package/dist/mcp/project-sync.js +105 -0
- package/dist/mcp/results-reply.js +15 -0
- package/dist/mcp/runtime.js +65 -11
- package/dist/mcp/server.js +382 -40
- package/dist/mcp/team-skill.js +63 -0
- package/dist/mcp/update-notice.js +75 -0
- package/dist/mcp/version.js +1 -0
- package/dist/schema/accountRequestMethods.js +1 -0
- package/dist/schema/boundedBody.js +30 -0
- package/dist/schema/cronValidator.js +150 -0
- package/dist/schema/platformApiKey.js +14 -0
- package/dist/schema/platformAppAuth.js +11 -0
- package/dist/schema/platformBillingConstants.js +4 -0
- package/dist/schema/platformCapabilityFamilies.js +293 -0
- package/dist/schema/platformConnectionCatalog.js +39 -0
- package/dist/schema/platformConnectionPolicy.js +33 -0
- package/dist/schema/platformCustomTools.js +148 -0
- package/dist/schema/platformEndUserApi.js +62 -0
- package/dist/schema/platformFunding.js +10 -0
- package/dist/schema/platformHttpConstraints.js +12 -0
- package/dist/schema/platformLocalSetup.js +25 -0
- package/dist/schema/platformMcpTools.js +7 -0
- package/dist/schema/platformMetaTools.js +63 -0
- package/dist/schema/platformMonitorSchedule.js +123 -0
- package/dist/schema/platformMonitors.js +159 -0
- package/dist/schema/platformOAuth.js +13 -0
- package/dist/schema/platformProjectPermissions.js +33 -0
- package/dist/schema/platformPublicExecution.js +202 -21
- package/dist/schema/platformRoutes.js +122 -0
- package/dist/schema/platformSkills.js +11 -0
- package/dist/schema/platformTeamSecretName.js +3 -0
- package/dist/schema/platformTeamSkillSlug.js +1 -0
- package/dist/schema/platformTeamSkills.js +302 -0
- package/dist/schema/platformToolAccess.js +62 -0
- package/dist/schema/platformTriggers.js +153 -0
- package/dist/schema/platformWorkflows.js +100 -0
- package/dist/schema/scheduleTimeZone.js +15 -0
- package/dist/schema/toolDiscovery.js +71 -0
- package/dist/schema/toolInputRepair.js +122 -0
- package/dist/schema/toolResultContext.js +51 -0
- package/dist/schema/untrustedContent.js +35 -0
- package/npm-shrinkwrap.json +73 -0
- package/package.json +5 -3
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export const PLATFORM_DISTRIBUTIONS = {"noimos":{"product":"noimos","name":"NoimosAI Platform","cliCommand":"noimos-platform","mcpServerName":"noimos-platform","icon":{"src":"https://platform.noimosai.com/icon.svg","mimeType":"image/svg+xml","sizes":["any"]}}};
|
|
2
|
+
export const PLATFORM_DISTRIBUTION = PLATFORM_DISTRIBUTIONS.noimos;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { createECDH, createHash, createPrivateKey, sign } from "node:crypto";
|
|
2
|
+
const P = 2n ** 256n - 2n ** 32n - 977n;
|
|
3
|
+
const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
4
|
+
const G = {
|
|
5
|
+
x: 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,
|
|
6
|
+
y: 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,
|
|
7
|
+
};
|
|
8
|
+
export function keccak256(data) {
|
|
9
|
+
return createHash("keccak-256").update(data).digest();
|
|
10
|
+
}
|
|
11
|
+
function mod(a, m = P) {
|
|
12
|
+
const r = a % m;
|
|
13
|
+
return r >= 0n ? r : r + m;
|
|
14
|
+
}
|
|
15
|
+
function modPow(base, exponent, m) {
|
|
16
|
+
let result = 1n, b = mod(base, m), e = exponent;
|
|
17
|
+
while (e > 0n) {
|
|
18
|
+
if (e & 1n)
|
|
19
|
+
result = (result * b) % m;
|
|
20
|
+
b = (b * b) % m;
|
|
21
|
+
e >>= 1n;
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
function invert(a, m) {
|
|
26
|
+
let [r0, r1, t0, t1] = [m, mod(a, m), 0n, 1n];
|
|
27
|
+
while (r1 !== 0n) {
|
|
28
|
+
const q = r0 / r1;
|
|
29
|
+
[r0, r1] = [r1, r0 - q * r1];
|
|
30
|
+
[t0, t1] = [t1, t0 - q * t1];
|
|
31
|
+
}
|
|
32
|
+
return mod(t0, m);
|
|
33
|
+
}
|
|
34
|
+
function addPoints(a, b) {
|
|
35
|
+
if (!a)
|
|
36
|
+
return b;
|
|
37
|
+
if (!b)
|
|
38
|
+
return a;
|
|
39
|
+
let slope;
|
|
40
|
+
if (a.x === b.x) {
|
|
41
|
+
if (mod(a.y + b.y) === 0n)
|
|
42
|
+
return null;
|
|
43
|
+
slope = mod(3n * a.x * a.x * invert(2n * a.y, P));
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
slope = mod((b.y - a.y) * invert(b.x - a.x, P));
|
|
47
|
+
}
|
|
48
|
+
const x = mod(slope * slope - a.x - b.x);
|
|
49
|
+
return { x, y: mod(slope * (a.x - x) - a.y) };
|
|
50
|
+
}
|
|
51
|
+
function multiply(point, scalar) {
|
|
52
|
+
let result = null, addend = point, k = scalar;
|
|
53
|
+
while (k > 0n) {
|
|
54
|
+
if (k & 1n)
|
|
55
|
+
result = addPoints(result, addend);
|
|
56
|
+
addend = addPoints(addend, addend);
|
|
57
|
+
k >>= 1n;
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
const toBytes32 = (value) => Buffer.from(value.toString(16).padStart(64, "0"), "hex");
|
|
62
|
+
const toBigInt = (bytes) => BigInt(`0x${Buffer.from(bytes).toString("hex") || "0"}`);
|
|
63
|
+
const hex = (bytes) => `0x${Buffer.from(bytes).toString("hex")}`;
|
|
64
|
+
function addressOf(publicKey) {
|
|
65
|
+
return checksumAddress(`0x${keccak256(Buffer.concat([toBytes32(publicKey.x), toBytes32(publicKey.y)])).subarray(12).toString("hex")}`);
|
|
66
|
+
}
|
|
67
|
+
export function checksumAddress(address) {
|
|
68
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address))
|
|
69
|
+
throw new Error("Not an EVM address");
|
|
70
|
+
const lower = address.slice(2).toLowerCase();
|
|
71
|
+
const hash = keccak256(lower).toString("hex");
|
|
72
|
+
let out = "0x";
|
|
73
|
+
for (let i = 0; i < lower.length; i += 1)
|
|
74
|
+
out += parseInt(hash[i], 16) >= 8 ? lower[i].toUpperCase() : lower[i];
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
function recoverPoint(digest, r, s, recovery) {
|
|
78
|
+
if (r <= 0n || r >= N || s <= 0n || s >= N)
|
|
79
|
+
return null;
|
|
80
|
+
const ySquared = mod(r ** 3n + 7n);
|
|
81
|
+
let y = modPow(ySquared, (P + 1n) / 4n, P);
|
|
82
|
+
if (mod(y * y) !== ySquared)
|
|
83
|
+
return null;
|
|
84
|
+
if (Number(y & 1n) !== recovery)
|
|
85
|
+
y = P - y;
|
|
86
|
+
const rInverse = invert(r, N);
|
|
87
|
+
const u1 = mod(-toBigInt(digest) * rInverse, N), u2 = mod(s * rInverse, N);
|
|
88
|
+
return addPoints(multiply(G, u1), multiply({ x: r, y }, u2));
|
|
89
|
+
}
|
|
90
|
+
export function recoverAddress(digest, signature) {
|
|
91
|
+
if (digest.length !== 32 || !/^0x[0-9a-fA-F]{130}$/.test(signature))
|
|
92
|
+
throw new Error("Expected a 32-byte digest and a 65-byte signature");
|
|
93
|
+
const bytes = Buffer.from(signature.slice(2), "hex"), v = bytes[64];
|
|
94
|
+
if (v !== 27 && v !== 28)
|
|
95
|
+
throw new Error("Signature recovery id must be 27 or 28");
|
|
96
|
+
const point = recoverPoint(Buffer.from(digest), toBigInt(bytes.subarray(0, 32)), toBigInt(bytes.subarray(32, 64)), v - 27);
|
|
97
|
+
if (!point)
|
|
98
|
+
throw new Error("Signature does not recover a public key");
|
|
99
|
+
return addressOf(point);
|
|
100
|
+
}
|
|
101
|
+
export function hashPersonalMessage(message) {
|
|
102
|
+
return keccak256(personalMessageBytes(message));
|
|
103
|
+
}
|
|
104
|
+
function personalMessageBytes(message) {
|
|
105
|
+
const body = Buffer.from(message, "utf8");
|
|
106
|
+
return Buffer.concat([Buffer.from(`\x19Ethereum Signed Message:\n${body.length}`, "utf8"), body]);
|
|
107
|
+
}
|
|
108
|
+
const ATOMIC = new Set(["address", "uint256", "bytes32", "string"]);
|
|
109
|
+
const DOMAIN_FIELDS = [
|
|
110
|
+
{ name: "name", type: "string" }, { name: "version", type: "string" },
|
|
111
|
+
{ name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" },
|
|
112
|
+
];
|
|
113
|
+
function dependencies(primary, types, found = new Set()) {
|
|
114
|
+
if (found.has(primary))
|
|
115
|
+
return found;
|
|
116
|
+
const fields = types[primary];
|
|
117
|
+
if (!fields)
|
|
118
|
+
throw new Error(`Unknown EIP-712 type ${primary}`);
|
|
119
|
+
found.add(primary);
|
|
120
|
+
for (const { type } of fields)
|
|
121
|
+
if (!ATOMIC.has(type))
|
|
122
|
+
dependencies(type, types, found);
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
125
|
+
export function encodeType(primary, types) {
|
|
126
|
+
const others = [...dependencies(primary, types)].filter((name) => name !== primary).sort();
|
|
127
|
+
return [primary, ...others].map((name) => `${name}(${types[name].map((field) => `${field.type} ${field.name}`).join(",")})`).join("");
|
|
128
|
+
}
|
|
129
|
+
export const typeHash = (primary, types) => keccak256(encodeType(primary, types));
|
|
130
|
+
function encodeAtomic(type, value) {
|
|
131
|
+
if (type === "string") {
|
|
132
|
+
if (typeof value !== "string")
|
|
133
|
+
throw new Error("EIP-712 string value must be a string");
|
|
134
|
+
return keccak256(Buffer.from(value, "utf8"));
|
|
135
|
+
}
|
|
136
|
+
if (type === "address") {
|
|
137
|
+
if (typeof value !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(value))
|
|
138
|
+
throw new Error("EIP-712 address value is invalid");
|
|
139
|
+
return Buffer.concat([Buffer.alloc(12), Buffer.from(value.slice(2), "hex")]);
|
|
140
|
+
}
|
|
141
|
+
if (type === "bytes32") {
|
|
142
|
+
if (typeof value !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(value))
|
|
143
|
+
throw new Error("EIP-712 bytes32 value is invalid");
|
|
144
|
+
return Buffer.from(value.slice(2), "hex");
|
|
145
|
+
}
|
|
146
|
+
if (typeof value === "object" || typeof value === "string" && !/^(?:0x[0-9a-fA-F]+|\d+)$/.test(value) || typeof value === "number" && !Number.isSafeInteger(value))
|
|
147
|
+
throw new Error("EIP-712 uint256 value is invalid");
|
|
148
|
+
const number = BigInt(value);
|
|
149
|
+
if (number < 0n || number >= 2n ** 256n)
|
|
150
|
+
throw new Error("EIP-712 uint256 value is out of range");
|
|
151
|
+
return toBytes32(number);
|
|
152
|
+
}
|
|
153
|
+
export function hashStruct(primary, types, data) {
|
|
154
|
+
const parts = [typeHash(primary, types)];
|
|
155
|
+
for (const { name, type } of types[primary] ?? []) {
|
|
156
|
+
const value = data[name];
|
|
157
|
+
if (value === undefined)
|
|
158
|
+
throw new Error(`EIP-712 value for ${name} is missing`);
|
|
159
|
+
if (ATOMIC.has(type))
|
|
160
|
+
parts.push(encodeAtomic(type, value));
|
|
161
|
+
else if (typeof value === "object")
|
|
162
|
+
parts.push(hashStruct(type, types, value));
|
|
163
|
+
else
|
|
164
|
+
throw new Error(`EIP-712 value for ${name} must be a ${type} object`);
|
|
165
|
+
}
|
|
166
|
+
return keccak256(Buffer.concat(parts));
|
|
167
|
+
}
|
|
168
|
+
export function domainSeparator(domain) {
|
|
169
|
+
const fields = DOMAIN_FIELDS.filter(({ name }) => domain[name] !== undefined);
|
|
170
|
+
const data = {};
|
|
171
|
+
for (const { name } of fields)
|
|
172
|
+
data[name] = domain[name];
|
|
173
|
+
return hashStruct("EIP712Domain", { EIP712Domain: fields }, data);
|
|
174
|
+
}
|
|
175
|
+
export function hashTypedData(domain, types, primaryType, message) {
|
|
176
|
+
if (Object.hasOwn(types, "EIP712Domain"))
|
|
177
|
+
throw new Error("Pass the domain separately; types must not define EIP712Domain");
|
|
178
|
+
return keccak256(Buffer.concat([Buffer.from("1901", "hex"), domainSeparator(domain), hashStruct(primaryType, types, message)]));
|
|
179
|
+
}
|
|
180
|
+
export function loadEvmWallet(privateKeyHex) {
|
|
181
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(privateKeyHex))
|
|
182
|
+
throw new Error("The wallet private key must be 0x followed by 64 hex characters.");
|
|
183
|
+
const d = Buffer.from(privateKeyHex.slice(2), "hex"), scalar = toBigInt(d);
|
|
184
|
+
if (scalar <= 0n || scalar >= N)
|
|
185
|
+
throw new Error("The wallet private key is out of range.");
|
|
186
|
+
const ecdh = createECDH("secp256k1");
|
|
187
|
+
ecdh.setPrivateKey(d);
|
|
188
|
+
const uncompressed = ecdh.getPublicKey();
|
|
189
|
+
const publicKey = { x: toBigInt(uncompressed.subarray(1, 33)), y: toBigInt(uncompressed.subarray(33, 65)) };
|
|
190
|
+
const address = addressOf(publicKey);
|
|
191
|
+
const key = createPrivateKey({ key: { kty: "EC", crv: "secp256k1", d: d.toString("base64url"),
|
|
192
|
+
x: toBytes32(publicKey.x).toString("base64url"), y: toBytes32(publicKey.y).toString("base64url") }, format: "jwk" });
|
|
193
|
+
const signPreimage = (preimage) => {
|
|
194
|
+
const raw = sign("keccak-256", preimage, { key, dsaEncoding: "ieee-p1363" });
|
|
195
|
+
const r = toBigInt(raw.subarray(0, 32)), rawS = toBigInt(raw.subarray(32));
|
|
196
|
+
const s = rawS > N >> 1n ? N - rawS : rawS;
|
|
197
|
+
const digest = keccak256(preimage), body = `${hex(toBytes32(r))}${toBytes32(s).toString("hex")}`;
|
|
198
|
+
for (const v of ["1b", "1c"]) {
|
|
199
|
+
const point = recoverPoint(digest, r, s, v === "1b" ? 0 : 1);
|
|
200
|
+
if (point && point.x === publicKey.x && point.y === publicKey.y)
|
|
201
|
+
return `${body}${v}`;
|
|
202
|
+
}
|
|
203
|
+
throw new Error("No recovery id reproduces the wallet address.");
|
|
204
|
+
};
|
|
205
|
+
return {
|
|
206
|
+
address,
|
|
207
|
+
signPersonalMessage: (message) => signPreimage(personalMessageBytes(message)),
|
|
208
|
+
signTypedData: (domain, types, primaryType, message) => {
|
|
209
|
+
if (Object.hasOwn(types, "EIP712Domain"))
|
|
210
|
+
throw new Error("Pass the domain separately; types must not define EIP712Domain");
|
|
211
|
+
return signPreimage(Buffer.concat([Buffer.from("1901", "hex"), domainSeparator(domain), hashStruct(primaryType, types, message)]));
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { platformExecutionReceiptContext, platformExecutionResultPageContext } from "../schema/platformPublicExecution.js";
|
|
2
|
+
import { TOOL_RESULT_CONTEXT_MAX_BYTES } from "../schema/toolResultContext.js";
|
|
3
|
+
import { platformExecutionGuidance } from "./failure-recovery.js";
|
|
4
|
+
const executionContextBudget = TOOL_RESULT_CONTEXT_MAX_BYTES - 256;
|
|
5
|
+
export function executionReceiptContext(result, options = {}) {
|
|
6
|
+
const guidance = platformExecutionGuidance(result.data);
|
|
7
|
+
const recoveredBytes = guidance ? new TextEncoder().encode(JSON.stringify(guidance)).byteLength : 0;
|
|
8
|
+
const receipt = platformExecutionReceiptContext(result, { maximumBytes: (options.maximumBytes ?? executionContextBudget) - recoveredBytes,
|
|
9
|
+
readWith: options.readWith ?? "executionContext/readExecutionResult" });
|
|
10
|
+
return guidance ? { ...receipt, data: { ...receipt.data, ...guidance } } : receipt;
|
|
11
|
+
}
|
|
12
|
+
export function executionPageContext(result, options = {}) {
|
|
13
|
+
return platformExecutionResultPageContext(result, { maximumBytes: options.maximumBytes ?? executionContextBudget, readWith: options.readWith ?? "readExecutionResult" });
|
|
14
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { PLATFORM_DISTRIBUTION } from "./distribution.js";
|
|
2
|
+
const CONNECT = "Ask user to connect via url or connect_app(apps). Never ask for a key in chat. Do not retry before confirmation.";
|
|
3
|
+
const TRY_AGAIN = "This run ended without a usable answer. Run it once more with a new idempotency key, or use another tool for the same task. If it fails again, say this part could not be done.";
|
|
4
|
+
const RATE_LIMITED = "The provider refused this request for its rate limit before running it. Use another tool for the same task now, or run this one again after about a minute; do not retry it at once.";
|
|
5
|
+
const REFUSED_INPUTS = "The provider refused these inputs. Compare them with this tool's schema, change them, then run with a new idempotency key. Do not retry unchanged.";
|
|
6
|
+
const UNREADABLE_PAGE = "The page itself answered with an error, so nothing was read. Check the URL or read a different page with a new idempotency key. Do not retry it unchanged or answer as if it had been read.";
|
|
7
|
+
function providerHttpRecovery(code) {
|
|
8
|
+
const status = Number(/^platform_provider_http_(\d{3})$/.exec(code ?? "")?.[1]);
|
|
9
|
+
if (status === 429)
|
|
10
|
+
return { action: "select_provider", instruction: RATE_LIMITED };
|
|
11
|
+
if (status === 404 || status === 410)
|
|
12
|
+
return { action: "change_input", instruction: "The provider has no such record. A dated link or old ID may have expired: check it, or list current ones with a related tool, then run with a new idempotency key. Do not retry unchanged." };
|
|
13
|
+
if (status === 401 || status === 403)
|
|
14
|
+
return { action: "select_provider", instruction: "The provider refused access to this data, and retrying will not change that. Use another tool for the same task, or say this part could not be done." };
|
|
15
|
+
if (status === 408 || status >= 500)
|
|
16
|
+
return { action: "try_again", instruction: TRY_AGAIN };
|
|
17
|
+
if (status >= 400)
|
|
18
|
+
return { action: "change_input", instruction: REFUSED_INPUTS };
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
export function platformFailureGuidance(code, context = {}) {
|
|
22
|
+
let action, instruction;
|
|
23
|
+
switch (code) {
|
|
24
|
+
case "platform_provider_own_key_required":
|
|
25
|
+
case "platform_connection_required":
|
|
26
|
+
action = "connect";
|
|
27
|
+
instruction = CONNECT;
|
|
28
|
+
break;
|
|
29
|
+
case "platform_provider_credential_unavailable":
|
|
30
|
+
action = "request_access";
|
|
31
|
+
instruction = "Ask the project owner/admin to check this provider credential in project integrations and restore authorized access. Never collect keys in chat. Do not retry unchanged.";
|
|
32
|
+
break;
|
|
33
|
+
case "platform_connection_reauthorize":
|
|
34
|
+
case "platform_connection_scope_required":
|
|
35
|
+
action = "reconnect";
|
|
36
|
+
instruction = "Ask user to reauthorize with required permissions via url or connect_app. If the page does not list the permission, they need their own OAuth app there. Never collect credentials in chat. Do not retry before confirmation.";
|
|
37
|
+
break;
|
|
38
|
+
case "platform_connection_access_denied":
|
|
39
|
+
case "platform_connection_authority_removed":
|
|
40
|
+
case "platform_connection_policy_denied":
|
|
41
|
+
case "platform_tool_access_denied":
|
|
42
|
+
case "platform_api_key_scope_required":
|
|
43
|
+
case "platform_approval_required":
|
|
44
|
+
action = "request_access";
|
|
45
|
+
instruction = "Show url and request required permission/approval from owner/admin. Do not retry until granted or switch accounts automatically.";
|
|
46
|
+
break;
|
|
47
|
+
case "platform_connection_choice_required":
|
|
48
|
+
case "platform_connection_provider_mismatch":
|
|
49
|
+
case "platform_connection_auth_method_required":
|
|
50
|
+
action = "select_connection";
|
|
51
|
+
instruction = "Choose an authorized connection with platform_connections; ask user if ambiguous. Do not retry unchanged.";
|
|
52
|
+
break;
|
|
53
|
+
case "platform_provider_not_configured":
|
|
54
|
+
case "platform_connection_not_configured":
|
|
55
|
+
action = "configure";
|
|
56
|
+
instruction = "Ask an operator to finish server setup. Do not ask the user to reconnect. Do not retry before setup changes.";
|
|
57
|
+
break;
|
|
58
|
+
case "platform_no_compatible_provider":
|
|
59
|
+
case "platform_provider_not_supported":
|
|
60
|
+
action = "select_provider";
|
|
61
|
+
instruction = "No provider this project runs accepts these inputs together; sourceRefusals, when present, names the inputs each could not apply and the only ones it applies, and an empty inputs list means it did not say which: then drop optional filters. Run again without the provider pin or such an input the user did not ask for, with a new idempotency key; if a condition the user asked for cannot be applied, say which. Offer connecting a provider only if the user wants one. Do not retry unchanged or infer an outage.";
|
|
62
|
+
break;
|
|
63
|
+
case "platform_provider_rate_limited":
|
|
64
|
+
action = "select_provider";
|
|
65
|
+
instruction = RATE_LIMITED;
|
|
66
|
+
break;
|
|
67
|
+
case "platform_tool_failed":
|
|
68
|
+
case "platform_tool_timeout":
|
|
69
|
+
case "platform_provider_response_unknown":
|
|
70
|
+
case "platform_execution_unavailable":
|
|
71
|
+
action = "try_again";
|
|
72
|
+
instruction = TRY_AGAIN;
|
|
73
|
+
break;
|
|
74
|
+
case "platform_output_too_large":
|
|
75
|
+
action = "try_again";
|
|
76
|
+
instruction = "The answer was larger than a run can keep. Run it again with a new idempotency key and narrower inputs or a smaller limit.";
|
|
77
|
+
break;
|
|
78
|
+
case "platform_scrape_target_error":
|
|
79
|
+
action = "change_input";
|
|
80
|
+
instruction = UNREADABLE_PAGE;
|
|
81
|
+
break;
|
|
82
|
+
case "platform_provider_declared_error":
|
|
83
|
+
case "platform_scrape_not_a_web_page":
|
|
84
|
+
case "platform_crawl4ai_not_found":
|
|
85
|
+
case "platform_crawl4ai_page_too_large":
|
|
86
|
+
case "platform_video_not_playable":
|
|
87
|
+
case "platform_invalid_media_input":
|
|
88
|
+
case "platform_generation_filtered":
|
|
89
|
+
action = "change_input";
|
|
90
|
+
instruction = REFUSED_INPUTS;
|
|
91
|
+
break;
|
|
92
|
+
default: {
|
|
93
|
+
const recovery = providerHttpRecovery(code);
|
|
94
|
+
if (!recovery)
|
|
95
|
+
return undefined;
|
|
96
|
+
({ action, instruction } = recovery);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const providers = context.providers ?? (context.provider ? [context.provider] : []);
|
|
100
|
+
const apps = [...new Set(providers.map(provider => provider.replace(/^app:/, "")))], url = context.reconnectUrl ?? context.actionUrl;
|
|
101
|
+
return { retryable: action === "try_again", recovery: { action, instruction, ...(url ? { url } : {}), ...(apps.length ? { apps } : {}) } };
|
|
102
|
+
}
|
|
103
|
+
export function platformUpstreamRefusalNext(refusal, reconnectUrl) {
|
|
104
|
+
const wait = refusal.retryAfterSeconds ?? 60;
|
|
105
|
+
if (refusal.kind === "rate_limited")
|
|
106
|
+
return `The app's rate limit refused this call, so result holds that refusal, not data. Run it again after ${wait} seconds with a new idempotency key, or get the same data in fewer calls, such as with the app's search. Do not send a burst of calls, and say what is missing if you stop here.`;
|
|
107
|
+
const scopes = refusal.neededScopes?.length ? ` (${refusal.neededScopes.join(", ")})` : "";
|
|
108
|
+
if (refusal.kind === "permission")
|
|
109
|
+
return `The app refused this call for a permission the connection lacks${scopes}, so result holds that refusal, not data. ${reconnectUrl
|
|
110
|
+
? "Show the user reconnectUrl to grant it, then run this again." : "Tell the user which permission is missing; reconnecting the app may grant it."} Another operation may still work on this connection. Say this part is incomplete.`;
|
|
111
|
+
return "The app refused this call, so result holds its answer, not the requested data. Read the error there, then fix the input or say the request could not be done. Do not report it as done.";
|
|
112
|
+
}
|
|
113
|
+
export function unexecutedCallBilling() {
|
|
114
|
+
return { scope: "this_call", chargedMicros: 0, executionStarted: false,
|
|
115
|
+
note: "Tell user: this attempt was not executed and incurred no charge. Earlier calls may have charges." };
|
|
116
|
+
}
|
|
117
|
+
export function platformExecutionGuidance(execution) {
|
|
118
|
+
if (execution.status === "needs_review")
|
|
119
|
+
return { retryable: false, recovery: { action: "wait_for_review",
|
|
120
|
+
instruction: "The outcome is unknown until an operator settles it, so it may or may not have run and its charge is not final. Do not run it again or say it worked. Tell the user it is held for review and continue with what you have." } };
|
|
121
|
+
if (execution.status !== "failed")
|
|
122
|
+
return undefined;
|
|
123
|
+
const guidance = platformFailureGuidance(execution.errorCode, execution) ?? { retryable: true, recovery: { action: "try_again", instruction: TRY_AGAIN } };
|
|
124
|
+
const billing = typeof execution.chargedMicros === "number" ? { scope: "this_execution", chargedMicros: execution.chargedMicros,
|
|
125
|
+
note: execution.chargedMicros === 0 ? `Tell user: no ${PLATFORM_DISTRIBUTION.name} charge for this execution. Earlier calls may have charges.`
|
|
126
|
+
: `Report this execution's ${PLATFORM_DISTRIBUTION.name} charge; do not describe the failed attempt as free.` } : undefined;
|
|
127
|
+
return { ...guidance, ...(billing ? { billing } : {}) };
|
|
128
|
+
}
|
package/dist/client/index.js
CHANGED
|
@@ -1,225 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export {
|
|
4
|
-
|
|
5
|
-
export { platformCreateExecutionSchema, platformToolInputSchema, platformExecutionSchema, platformArtifactIdSchema, platformArtifactSchema, platformExecutionListQuerySchema } from "../schema/platformPublicExecution.js";
|
|
6
|
-
export const projectResultSchema = z.object({ data: z.object({ developerOrganizationId: z.string(), developerProjectId: z.string(), name: z.string() }), requestId: z.string() });
|
|
7
|
-
export const usageResultSchema = z.object({ data: z.object({ month: z.string(), requests: z.number().int().nonnegative(), amountCents: z.number().nonnegative(), currency: z.literal("usd") }), requestId: z.string() });
|
|
8
|
-
export const executionResultSchema = z.object({ data: platformExecutionSchema });
|
|
9
|
-
const catalogResultSchema = z.object({ data: platformCatalogSchema });
|
|
10
|
-
export const platformToolDiscoverySchema = z.object({
|
|
11
|
-
query: z.string().trim().min(1).max(200).optional().describe("Case-insensitive substring of a tool's name or description."),
|
|
12
|
-
category: platformCatalogSchema.shape.tools.element.shape.category.optional(),
|
|
13
|
-
availableOnly: z.boolean().optional().describe("Only tools with a configured provider; check executionEnabled separately."),
|
|
14
|
-
summary: z.boolean().optional().describe("Omit input schemas and examples; inspect a selected tool for those details."),
|
|
15
|
-
}).strict();
|
|
16
|
-
const quoteResultSchema = z.object({ data: platformQuoteSchema });
|
|
17
|
-
const executionIdSchema = z.string().regex(/^[A-Za-z0-9_-]{1,128}$/);
|
|
18
|
-
const idempotencySchema = z.string().regex(/^[A-Za-z0-9._:-]{8,128}$/);
|
|
19
|
-
const publicErrors = {
|
|
20
|
-
platform_quote_changed: "The quote changed. Request a new quote before submitting another execution.",
|
|
21
|
-
platform_idempotency_key_used_for_another_request: "This idempotency key was already used for another request. Keep the original body to retrieve its result.",
|
|
22
|
-
platform_executions_disabled: "Paid executions are paused.",
|
|
23
|
-
platform_provider_not_configured: "This tool's provider is unavailable.",
|
|
24
|
-
platform_insufficient_balance_or_spending_limit_reached: "The prepaid balance or monthly spending limit is insufficient.",
|
|
25
|
-
platform_request_rate_exceeded: "The API key's request limit was reached. Wait before retrying.",
|
|
26
|
-
platform_api_key_scope_required: "The API key is missing the required scope.",
|
|
27
|
-
platform_artifact_unavailable: "This output file is unavailable or its retention period has ended.",
|
|
28
|
-
};
|
|
29
|
-
export class PlatformApiError extends Error {
|
|
30
|
-
status;
|
|
31
|
-
code;
|
|
32
|
-
constructor(status, code) {
|
|
33
|
-
super(`Platform API request failed (${status}). ${code && Object.hasOwn(publicErrors, code) ? publicErrors[code] : "Check the developer key, project access and limits in the console."}`);
|
|
34
|
-
this.status = status;
|
|
35
|
-
this.code = code;
|
|
36
|
-
this.name = "PlatformApiError";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
async function responseJson(response) {
|
|
40
|
-
const reader = response.body?.getReader();
|
|
41
|
-
if (!reader)
|
|
42
|
-
throw new Error("The Platform API returned an empty response.");
|
|
43
|
-
const chunks = [];
|
|
44
|
-
let size = 0;
|
|
45
|
-
try {
|
|
46
|
-
for (;;) {
|
|
47
|
-
const { value, done } = await reader.read();
|
|
48
|
-
if (done)
|
|
49
|
-
break;
|
|
50
|
-
size += value.byteLength;
|
|
51
|
-
if (size > PLATFORM_EXECUTION_RESPONSE_MAX_BYTES) {
|
|
52
|
-
await reader.cancel();
|
|
53
|
-
throw new Error("The Platform API response is too large.");
|
|
54
|
-
}
|
|
55
|
-
chunks.push(value);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
finally {
|
|
59
|
-
reader.releaseLock();
|
|
60
|
-
}
|
|
61
|
-
const bytes = new Uint8Array(size);
|
|
62
|
-
let offset = 0;
|
|
63
|
-
for (const chunk of chunks) {
|
|
64
|
-
bytes.set(chunk, offset);
|
|
65
|
-
offset += chunk.length;
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
return JSON.parse(new TextDecoder().decode(bytes));
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
throw new Error("The Platform API returned an invalid response.");
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
export function configFromEnvironment(env, source) {
|
|
75
|
-
if (!env.NOIMOS_PLATFORM_API_URL || !env.NOIMOS_PLATFORM_API_KEY)
|
|
76
|
-
throw new Error("Set NOIMOS_PLATFORM_API_URL and NOIMOS_PLATFORM_API_KEY from the developer console.");
|
|
77
|
-
return { apiUrl: env.NOIMOS_PLATFORM_API_URL, apiKey: env.NOIMOS_PLATFORM_API_KEY, source, local: env.NOIMOS_PLATFORM_ENV === "local" };
|
|
78
|
-
}
|
|
79
|
-
export class PlatformClient {
|
|
80
|
-
config;
|
|
81
|
-
transport;
|
|
82
|
-
baseUrl;
|
|
83
|
-
constructor(config, transport = fetch) {
|
|
84
|
-
this.config = config;
|
|
85
|
-
this.transport = transport;
|
|
86
|
-
const url = new URL(config.apiUrl);
|
|
87
|
-
if (url.username || url.password || url.search || url.hash || (url.protocol !== "https:" && !(config.local && url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname))))
|
|
88
|
-
throw new Error("The Platform API URL must use HTTPS. Explicit local mode permits loopback HTTP.");
|
|
89
|
-
if (!/^nmp_[A-Za-z0-9_-]{43}$/.test(config.apiKey))
|
|
90
|
-
throw new Error("A dedicated nmp_ developer API key is required.");
|
|
91
|
-
this.baseUrl = url.href.replace(/\/$/, "");
|
|
92
|
-
}
|
|
93
|
-
async request(path, schema, options) {
|
|
94
|
-
const body = options?.body === undefined ? undefined : JSON.stringify(options.body);
|
|
95
|
-
if (body && new TextEncoder().encode(body).byteLength > (path === "/tools/upload" ? PLATFORM_UPLOAD_MAX_BYTES : 65536))
|
|
96
|
-
throw new Error(path === "/tools/upload" ? "The upload exceeds 12 MiB." : "The tool request exceeds 64 KiB.");
|
|
97
|
-
let response;
|
|
98
|
-
try {
|
|
99
|
-
options?.signal?.throwIfAborted();
|
|
100
|
-
const timeout = AbortSignal.timeout(20000);
|
|
101
|
-
response = await this.transport(`${this.baseUrl}${path}`, { method: options?.method ?? "GET", redirect: "error", signal: options?.signal ? AbortSignal.any([timeout, options.signal]) : timeout,
|
|
102
|
-
headers: { authorization: `Bearer ${this.config.apiKey}`, "x-noimos-client": this.config.source, "idempotency-key": options?.idempotencyKey ?? crypto.randomUUID(),
|
|
103
|
-
...(body === undefined ? {} : { "content-type": "application/json" }) }, ...(body === undefined ? {} : { body }) });
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
options?.signal?.throwIfAborted();
|
|
107
|
-
throw new Error("The Platform API could not be reached. If submitting an execution, retry with the same body and idempotency key.");
|
|
108
|
-
}
|
|
109
|
-
const payload = await responseJson(response).catch(() => {
|
|
110
|
-
options?.signal?.throwIfAborted();
|
|
111
|
-
if (!response.ok)
|
|
112
|
-
throw new PlatformApiError(response.status, null);
|
|
113
|
-
throw new Error("The Platform API returned an invalid response. If submitting an execution, retain the same body and idempotency key.");
|
|
114
|
-
});
|
|
115
|
-
if (!response.ok) {
|
|
116
|
-
const parsed = z.object({ error: z.object({ message: z.string() }) }).safeParse(payload);
|
|
117
|
-
const code = parsed.success && Object.hasOwn(publicErrors, parsed.data.error.message) ? parsed.data.error.message : null;
|
|
118
|
-
throw new PlatformApiError(response.status, code);
|
|
119
|
-
}
|
|
120
|
-
const parsed = schema.safeParse(payload);
|
|
121
|
-
if (!parsed.success)
|
|
122
|
-
throw new Error("The Platform API returned an unexpected response. If submitting an execution, retain the same body and idempotency key.");
|
|
123
|
-
return parsed.data;
|
|
124
|
-
}
|
|
125
|
-
project() { return this.request("/tools/project", projectResultSchema); }
|
|
126
|
-
usage() { return this.request("/tools/usage", usageResultSchema); }
|
|
127
|
-
tools() { return this.request("/tools", catalogResultSchema); }
|
|
128
|
-
uploadInput(input) {
|
|
129
|
-
return this.request("/tools/upload", z.object({ data: platformInputFileSchema }), { method: "POST", body: platformInputUploadSchema.parse(input) });
|
|
130
|
-
}
|
|
131
|
-
async discoverTools(options = {}) {
|
|
132
|
-
const { query, category, availableOnly, summary } = platformToolDiscoverySchema.parse(options);
|
|
133
|
-
const { data } = await this.tools();
|
|
134
|
-
const search = query?.toLowerCase();
|
|
135
|
-
const tools = data.tools.filter((tool) => (!search || tool.name.toLowerCase().includes(search) || tool.description.toLowerCase().includes(search)) &&
|
|
136
|
-
(!category || tool.category === category) && (!availableOnly || tool.available));
|
|
137
|
-
return { data: { ...data, tools: summary ? tools.map(({ inputSchema, example, ...tool }) => tool) : tools } };
|
|
138
|
-
}
|
|
139
|
-
async tool(name) {
|
|
140
|
-
platformToolNameSchema.parse(name);
|
|
141
|
-
const { data } = await this.tools();
|
|
142
|
-
const tool = data.tools.find((entry) => entry.name === name);
|
|
143
|
-
if (!tool)
|
|
144
|
-
throw new Error("Tool is unavailable in the standalone Platform catalog. Run tools to discover supported operations.");
|
|
145
|
-
return { data: { ...tool, priceVersion: data.priceVersion, currency: data.currency, executionEnabled: data.executionEnabled } };
|
|
146
|
-
}
|
|
147
|
-
executions(input = {}) {
|
|
148
|
-
const parsed = platformExecutionListQuerySchema.parse(input);
|
|
149
|
-
const query = new URLSearchParams({ limit: String(parsed.limit) });
|
|
150
|
-
if (parsed.before)
|
|
151
|
-
query.set("before", parsed.before);
|
|
152
|
-
if (parsed.status)
|
|
153
|
-
query.set("status", parsed.status);
|
|
154
|
-
if (parsed.tool)
|
|
155
|
-
query.set("tool", parsed.tool);
|
|
156
|
-
return this.request(`/tools/executions?${query}`, z.object({ data: platformExecutionListSchema }));
|
|
157
|
-
}
|
|
158
|
-
quote(input) {
|
|
159
|
-
const parsed = platformToolInputSchema.safeParse(input);
|
|
160
|
-
if (!parsed.success)
|
|
161
|
-
throw new Error("Provide a tool name and a JSON object of arguments.");
|
|
162
|
-
return this.request("/tools/quote", quoteResultSchema, { method: "POST", body: parsed.data });
|
|
163
|
-
}
|
|
164
|
-
createExecution(input, idempotencyKey) {
|
|
165
|
-
const parsed = platformCreateExecutionSchema.safeParse(input);
|
|
166
|
-
if (!parsed.success)
|
|
167
|
-
throw new Error("Provide a tool, arguments, the quote's priceVersion and maximumChargeMicros.");
|
|
168
|
-
if (!idempotencySchema.safeParse(idempotencyKey).success)
|
|
169
|
-
throw new Error("Keep an idempotency key of 8–128 letters, digits, periods, underscores, colons or hyphens for this execution.");
|
|
170
|
-
return this.request(`/tools/${parsed.data.tool}`, executionResultSchema, { method: "POST", body: parsed.data, idempotencyKey });
|
|
171
|
-
}
|
|
172
|
-
execution(id, signal) {
|
|
173
|
-
if (!executionIdSchema.safeParse(id).success)
|
|
174
|
-
throw new Error("Provide a valid execution ID.");
|
|
175
|
-
return this.request(`/tools/executions/${id}`, executionResultSchema, { signal });
|
|
176
|
-
}
|
|
177
|
-
async waitExecution(id, timeoutMs = 20000, signal) {
|
|
178
|
-
executionIdSchema.parse(id);
|
|
179
|
-
if (!Number.isInteger(timeoutMs) || timeoutMs < 1000 || timeoutMs > 900000)
|
|
180
|
-
throw new Error("Wait must be between 1 and 900 seconds.");
|
|
181
|
-
const controller = new AbortController();
|
|
182
|
-
const timeout = new Error("Execution wait timed out; use the same execution ID to continue reading.");
|
|
183
|
-
const timer = setTimeout(() => controller.abort(timeout), timeoutMs);
|
|
184
|
-
const abort = () => controller.abort(signal?.reason);
|
|
185
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
186
|
-
if (signal?.aborted)
|
|
187
|
-
abort();
|
|
188
|
-
let latest = null;
|
|
189
|
-
try {
|
|
190
|
-
for (;;) {
|
|
191
|
-
controller.signal.throwIfAborted();
|
|
192
|
-
latest = (await this.execution(id, controller.signal)).data;
|
|
193
|
-
if (!["queued", "running", "waiting"].includes(latest.status))
|
|
194
|
-
return { data: latest, id, timedOut: false };
|
|
195
|
-
await new Promise((resolve, reject) => {
|
|
196
|
-
const done = () => { controller.signal.removeEventListener("abort", cancelled); resolve(); };
|
|
197
|
-
const pause = setTimeout(done, 5000);
|
|
198
|
-
const cancelled = () => { clearTimeout(pause); controller.signal.removeEventListener("abort", cancelled); reject(controller.signal.reason); };
|
|
199
|
-
controller.signal.addEventListener("abort", cancelled, { once: true });
|
|
200
|
-
if (controller.signal.aborted)
|
|
201
|
-
cancelled();
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
if (error !== timeout)
|
|
207
|
-
throw error;
|
|
208
|
-
return { data: latest, id, timedOut: true };
|
|
209
|
-
}
|
|
210
|
-
finally {
|
|
211
|
-
clearTimeout(timer);
|
|
212
|
-
signal?.removeEventListener("abort", abort);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
cancelExecution(id) {
|
|
216
|
-
if (!executionIdSchema.safeParse(id).success)
|
|
217
|
-
throw new Error("Provide a valid execution ID.");
|
|
218
|
-
return this.request(`/tools/executions/${id}/cancel`, executionResultSchema, { method: "POST" });
|
|
219
|
-
}
|
|
220
|
-
artifactDownload(id, artifactId) {
|
|
221
|
-
if (!executionIdSchema.safeParse(id).success || !platformArtifactIdSchema.safeParse(artifactId).success)
|
|
222
|
-
throw new Error("Provide valid execution and artifact IDs.");
|
|
223
|
-
return this.request(`/tools/executions/${id}/artifacts/${artifactId}/download`, z.object({ data: platformArtifactDownloadSchema }));
|
|
224
|
-
}
|
|
225
|
-
}
|
|
1
|
+
export * from "./browser.js";
|
|
2
|
+
export { PLATFORM_DISTRIBUTION, PLATFORM_DISTRIBUTIONS } from "./distribution.js";
|
|
3
|
+
export { loadEvmWallet } from "./evm-wallet.js";
|
|
4
|
+
export { walletSignIn, walletTopUp, walletAutoTopUp, walletAutoTopUpFromEnvironment, WalletAccessError } from "./wallet-access.js";
|