run402 1.54.2 → 1.54.4
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/cli.mjs +18 -0
- package/core-dist/allowance-auth.js +5 -0
- package/core-dist/allowance.js +49 -1
- package/core-dist/config.js +35 -2
- package/core-dist/wallet-auth.js +62 -0
- package/core-dist/wallet.js +25 -0
- package/lib/agent.mjs +29 -1
- package/lib/ai.mjs +113 -37
- package/lib/apps.mjs +34 -0
- package/lib/argparse.mjs +128 -0
- package/lib/auth.mjs +15 -2
- package/lib/billing.mjs +35 -0
- package/lib/config.mjs +20 -1
- package/lib/contracts.mjs +41 -0
- package/lib/deploy-v2.mjs +37 -0
- package/lib/deploy.mjs +125 -58
- package/lib/domains.mjs +79 -5
- package/lib/email.mjs +34 -0
- package/lib/functions.mjs +25 -1
- package/lib/image.mjs +33 -1
- package/lib/message.mjs +50 -3
- package/lib/projects.mjs +43 -33
- package/lib/sdk-errors.mjs +2 -1
- package/lib/secrets.mjs +29 -0
- package/lib/sender-domain.mjs +78 -1
- package/lib/service.mjs +30 -1
- package/lib/subdomains.mjs +49 -4
- package/lib/tier.mjs +41 -1
- package/lib/webhooks.mjs +10 -0
- package/package.json +1 -1
- package/sdk/core-dist/allowance-auth.js +5 -0
- package/sdk/core-dist/allowance.js +49 -1
- package/sdk/core-dist/config.js +35 -2
- package/sdk/core-dist/wallet-auth.js +62 -0
- package/sdk/core-dist/wallet.js +25 -0
- package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
- package/sdk/dist/node/paid-fetch.js +12 -1
- package/sdk/dist/node/paid-fetch.js.map +1 -1
package/cli.mjs
CHANGED
|
@@ -74,6 +74,23 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
|
74
74
|
process.exit(0);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
try {
|
|
78
|
+
await dispatch();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// Surface env/config errors (e.g. invalid RUN402_API_BASE) as a clean
|
|
81
|
+
// JSON envelope on stderr instead of a raw stack trace. We import the
|
|
82
|
+
// helper lazily so a broken env doesn't fail this catch handler too.
|
|
83
|
+
const { fail } = await import("./lib/sdk-errors.mjs");
|
|
84
|
+
fail({
|
|
85
|
+
code: "BAD_ENV",
|
|
86
|
+
message: err && err.message ? err.message : String(err),
|
|
87
|
+
hint: typeof err?.message === "string" && err.message.includes("RUN402_API_BASE")
|
|
88
|
+
? "Check the RUN402_API_BASE env var."
|
|
89
|
+
: undefined,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function dispatch() {
|
|
77
94
|
switch (cmd) {
|
|
78
95
|
case "init": {
|
|
79
96
|
const { run } = await import("./lib/init.mjs");
|
|
@@ -200,3 +217,4 @@ switch (cmd) {
|
|
|
200
217
|
console.log(HELP);
|
|
201
218
|
process.exit(1);
|
|
202
219
|
}
|
|
220
|
+
}
|
|
@@ -87,6 +87,11 @@ export function formatSIWEMessage(opts, address) {
|
|
|
87
87
|
* @param path - API path (e.g. "/projects/v1") used to build the SIWE uri field.
|
|
88
88
|
*/
|
|
89
89
|
export function getAllowanceAuthHeaders(path, allowancePath) {
|
|
90
|
+
// GH-194: readAllowance throws on a malformed-shape allowance file. The
|
|
91
|
+
// CLI's higher-level readAllowance wrapper surfaces this as a structured
|
|
92
|
+
// BAD_ALLOWANCE_FILE envelope; here we preserve the public contract that
|
|
93
|
+
// this helper returns SIWxAuthHeaders | null. Re-throw so callers above
|
|
94
|
+
// the CLI's wrapper (e.g. SDK paid-fetch) can decide whether to swallow it.
|
|
90
95
|
const allowance = readAllowance(allowancePath);
|
|
91
96
|
if (!allowance || !allowance.address || !allowance.privateKey)
|
|
92
97
|
return null;
|
package/core-dist/allowance.js
CHANGED
|
@@ -2,16 +2,64 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSy
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { getAllowancePath } from "./config.js";
|
|
5
|
+
// 0x-prefixed 40-hex EVM address.
|
|
6
|
+
const ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
|
|
7
|
+
// 0x-prefixed 64-hex secp256k1 private key (32 bytes).
|
|
8
|
+
const PRIVATE_KEY_RE = /^0x[a-fA-F0-9]{64}$/;
|
|
9
|
+
/**
|
|
10
|
+
* Load the agent allowance from disk.
|
|
11
|
+
*
|
|
12
|
+
* Returns `null` for the two "no allowance configured" cases:
|
|
13
|
+
* - the file does not exist
|
|
14
|
+
* - the file exists but is not parseable JSON (preserve existing UX —
|
|
15
|
+
* consumers print "no_allowance" and tell the user to run init)
|
|
16
|
+
*
|
|
17
|
+
* Throws a structured `Error` (GH-194) when the file parses as JSON but the
|
|
18
|
+
* shape is wrong (missing/wrong-type/wrong-length fields). Without this guard
|
|
19
|
+
* downstream callers crash with raw stack traces:
|
|
20
|
+
* - `cli/lib/status.mjs` reaches for `allowance.address.toLowerCase()`
|
|
21
|
+
* and crashes with `TypeError: Cannot read properties of undefined`.
|
|
22
|
+
* - `core/src/allowance-auth.ts` passes a malformed `privateKey` to
|
|
23
|
+
* `@noble/curves` which throws "expected 32 bytes, got N".
|
|
24
|
+
*
|
|
25
|
+
* The CLI's `cli/lib/config.mjs:readAllowance()` wrapper and the MCP
|
|
26
|
+
* `src/tools/{status,init}.ts` callers translate the throw into their own
|
|
27
|
+
* structured envelopes (`code: BAD_ALLOWANCE_FILE`).
|
|
28
|
+
*/
|
|
5
29
|
export function readAllowance(path) {
|
|
6
30
|
const p = path ?? getAllowancePath();
|
|
7
31
|
if (!existsSync(p))
|
|
8
32
|
return null;
|
|
33
|
+
let raw;
|
|
9
34
|
try {
|
|
10
|
-
|
|
35
|
+
raw = readFileSync(p, "utf-8");
|
|
11
36
|
}
|
|
12
37
|
catch {
|
|
13
38
|
return null;
|
|
14
39
|
}
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Preserve historical UX — completely unparseable input reads as "no
|
|
46
|
+
// allowance configured" rather than as an error. Consumers already handle
|
|
47
|
+
// null with a friendly "run 'run402 init'" message.
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
51
|
+
throw new Error(`allowance.json must contain a JSON object (got ${Array.isArray(parsed) ? "array" : parsed === null ? "null" : typeof parsed}). Back up the file and run 'run402 init' to recreate it.`);
|
|
52
|
+
}
|
|
53
|
+
const data = parsed;
|
|
54
|
+
if (typeof data.address !== "string" || !ADDRESS_RE.test(data.address)) {
|
|
55
|
+
throw new Error("allowance.json missing valid 'address' (expected 0x-prefixed 40-hex string). " +
|
|
56
|
+
"Back up the file and run 'run402 init' to recreate it.");
|
|
57
|
+
}
|
|
58
|
+
if (typeof data.privateKey !== "string" || !PRIVATE_KEY_RE.test(data.privateKey)) {
|
|
59
|
+
throw new Error("allowance.json missing valid 'privateKey' (expected 0x-prefixed 64-hex string). " +
|
|
60
|
+
"Back up the file and run 'run402 init' to recreate it.");
|
|
61
|
+
}
|
|
62
|
+
return data;
|
|
15
63
|
}
|
|
16
64
|
export function saveAllowance(data, path) {
|
|
17
65
|
const p = path ?? getAllowancePath();
|
package/core-dist/config.js
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { existsSync, renameSync, mkdirSync } from "node:fs";
|
|
4
|
+
const DEFAULT_API_BASE = "https://api.run402.com";
|
|
5
|
+
/**
|
|
6
|
+
* Validate a user-supplied API base URL. Throws a clear error message that
|
|
7
|
+
* names the env var when the URL is malformed or uses a scheme other than
|
|
8
|
+
* http(s). Empty string is treated as "set but empty" (almost always a
|
|
9
|
+
* templating mishap) and emits a stderr warning before falling back to
|
|
10
|
+
* `fallback`.
|
|
11
|
+
*
|
|
12
|
+
* Returns the validated URL string (unchanged) or `null` if the env var was
|
|
13
|
+
* unset.
|
|
14
|
+
*/
|
|
15
|
+
function validateApiBase(envVar, raw, fallback) {
|
|
16
|
+
if (raw == null)
|
|
17
|
+
return null;
|
|
18
|
+
if (raw === "") {
|
|
19
|
+
process.stderr.write(`warning: ${envVar} is set but empty - using default. Unset the env var to suppress this warning.\n`);
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
let u;
|
|
23
|
+
try {
|
|
24
|
+
u = new URL(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error(`${envVar} is not a valid URL: ${JSON.stringify(raw)}. Expected an http(s) URL like https://api.run402.com.`);
|
|
28
|
+
}
|
|
29
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
30
|
+
throw new Error(`${envVar} must use http(s):, got ${u.protocol} (full value: ${JSON.stringify(raw)}).`);
|
|
31
|
+
}
|
|
32
|
+
return raw;
|
|
33
|
+
}
|
|
4
34
|
export function getApiBase() {
|
|
5
|
-
|
|
35
|
+
const validated = validateApiBase("RUN402_API_BASE", process.env.RUN402_API_BASE, DEFAULT_API_BASE);
|
|
36
|
+
return validated ?? DEFAULT_API_BASE;
|
|
6
37
|
}
|
|
7
38
|
/**
|
|
8
39
|
* API base for the deploy-v2 routes. Defaults to the same value as
|
|
@@ -12,7 +43,9 @@ export function getApiBase() {
|
|
|
12
43
|
* should not need this override.
|
|
13
44
|
*/
|
|
14
45
|
export function getDeployApiBase() {
|
|
15
|
-
|
|
46
|
+
const fallback = getApiBase();
|
|
47
|
+
const validated = validateApiBase("RUN402_DEPLOY_API_BASE", process.env.RUN402_DEPLOY_API_BASE, fallback);
|
|
48
|
+
return validated ?? fallback;
|
|
16
49
|
}
|
|
17
50
|
export function getConfigDir() {
|
|
18
51
|
return process.env.RUN402_CONFIG_DIR || join(homedir(), ".config", "run402");
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet auth helper — generates EIP-191 signature headers for Run402 API.
|
|
3
|
+
* Uses @noble/curves (lighter than viem) for signing.
|
|
4
|
+
*/
|
|
5
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
6
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
7
|
+
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
8
|
+
import { readWallet } from "./wallet.js";
|
|
9
|
+
/**
|
|
10
|
+
* EIP-191 personal_sign: sign a message with the wallet's private key.
|
|
11
|
+
*/
|
|
12
|
+
function personalSign(privateKeyHex, address, message) {
|
|
13
|
+
const msgBytes = new TextEncoder().encode(message);
|
|
14
|
+
const prefix = new TextEncoder().encode(`\x19Ethereum Signed Message:\n${msgBytes.length}`);
|
|
15
|
+
const prefixed = new Uint8Array(prefix.length + msgBytes.length);
|
|
16
|
+
prefixed.set(prefix);
|
|
17
|
+
prefixed.set(msgBytes, prefix.length);
|
|
18
|
+
const hash = keccak_256(prefixed);
|
|
19
|
+
const pkHex = privateKeyHex.startsWith("0x")
|
|
20
|
+
? privateKeyHex.slice(2)
|
|
21
|
+
: privateKeyHex;
|
|
22
|
+
const pkBytes = Uint8Array.from(Buffer.from(pkHex, "hex"));
|
|
23
|
+
const rawSig = secp256k1.sign(hash, pkBytes);
|
|
24
|
+
const sig = secp256k1.Signature.fromBytes(rawSig);
|
|
25
|
+
// Determine recovery bit by trying both and matching the address
|
|
26
|
+
let recovery = 0;
|
|
27
|
+
for (const v of [0, 1]) {
|
|
28
|
+
try {
|
|
29
|
+
const recovered = sig.addRecoveryBit(v).recoverPublicKey(hash);
|
|
30
|
+
const pubBytes = recovered.toBytes(false).slice(1); // uncompressed, drop 04 prefix
|
|
31
|
+
const addrBytes = keccak_256(pubBytes).slice(-20);
|
|
32
|
+
if ("0x" + bytesToHex(addrBytes) === address.toLowerCase()) {
|
|
33
|
+
recovery = v;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const r = sig.r.toString(16).padStart(64, "0");
|
|
42
|
+
const s = sig.s.toString(16).padStart(64, "0");
|
|
43
|
+
const vHex = (recovery + 27).toString(16).padStart(2, "0");
|
|
44
|
+
return "0x" + r + s + vHex;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get wallet auth headers for the Run402 API.
|
|
48
|
+
* Returns null if no wallet is configured.
|
|
49
|
+
*/
|
|
50
|
+
export function getWalletAuthHeaders(walletPath) {
|
|
51
|
+
const wallet = readWallet(walletPath);
|
|
52
|
+
if (!wallet || !wallet.address || !wallet.privateKey)
|
|
53
|
+
return null;
|
|
54
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
55
|
+
const signature = personalSign(wallet.privateKey, wallet.address, `run402:${timestamp}`);
|
|
56
|
+
return {
|
|
57
|
+
"X-Run402-Wallet": wallet.address,
|
|
58
|
+
"X-Run402-Signature": signature,
|
|
59
|
+
"X-Run402-Timestamp": timestamp,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=wallet-auth.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { getWalletPath } from "./config.js";
|
|
5
|
+
export function readWallet(path) {
|
|
6
|
+
const p = path ?? getWalletPath();
|
|
7
|
+
if (!existsSync(p))
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function saveWallet(data, path) {
|
|
17
|
+
const p = path ?? getWalletPath();
|
|
18
|
+
const dir = dirname(p);
|
|
19
|
+
mkdirSync(dir, { recursive: true });
|
|
20
|
+
const tmp = join(dir, `.wallet.${randomBytes(4).toString("hex")}.tmp`);
|
|
21
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
22
|
+
renameSync(tmp, p);
|
|
23
|
+
chmodSync(p, 0o600);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=wallet.js.map
|
package/lib/agent.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { allowanceAuthHeaders } from "./config.mjs";
|
|
2
2
|
import { getSdk } from "./sdk.mjs";
|
|
3
3
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
4
|
+
import { validateWebhookUrl } from "./argparse.mjs";
|
|
4
5
|
|
|
5
6
|
const HELP = `run402 agent — Manage agent identity
|
|
6
7
|
|
|
@@ -17,6 +18,29 @@ Examples:
|
|
|
17
18
|
run402 agent contact --name my-agent --email ops@example.com --webhook https://example.com/hook
|
|
18
19
|
`;
|
|
19
20
|
|
|
21
|
+
const SUB_HELP = {
|
|
22
|
+
contact: `run402 agent contact — Register agent contact info
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
run402 agent contact --name <name> [--email <email>] [--webhook <url>]
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--name <name> Required: agent name (e.g. "my-agent")
|
|
29
|
+
--email <email> Optional: contact email address
|
|
30
|
+
--webhook <url> Optional: webhook URL Run402 can call to reach the
|
|
31
|
+
agent
|
|
32
|
+
|
|
33
|
+
Notes:
|
|
34
|
+
- Free with allowance auth (run an 'allowance create' first)
|
|
35
|
+
- Registers contact info so Run402 can reach your agent
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
run402 agent contact --name my-agent
|
|
39
|
+
run402 agent contact --name my-agent --email ops@example.com \\
|
|
40
|
+
--webhook https://example.com/hook
|
|
41
|
+
`,
|
|
42
|
+
};
|
|
43
|
+
|
|
20
44
|
async function contact(args) {
|
|
21
45
|
let name = null, email = null, webhook = null;
|
|
22
46
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -27,6 +51,10 @@ async function contact(args) {
|
|
|
27
51
|
if (!name) {
|
|
28
52
|
fail({ code: "BAD_USAGE", message: "Missing --name <name>" });
|
|
29
53
|
}
|
|
54
|
+
// GH-192: validate webhook scheme locally BEFORE the allowance check so
|
|
55
|
+
// bad URLs fail fast even without an allowance configured. No-op when
|
|
56
|
+
// --webhook is omitted (it's optional).
|
|
57
|
+
validateWebhookUrl(webhook, "--webhook");
|
|
30
58
|
// Preserve the aggressive early exit when no allowance is configured.
|
|
31
59
|
allowanceAuthHeaders("/agent/v1/contact");
|
|
32
60
|
|
|
@@ -45,7 +73,7 @@ async function contact(args) {
|
|
|
45
73
|
export async function run(sub, args) {
|
|
46
74
|
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
47
75
|
if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
|
|
48
|
-
console.log(HELP);
|
|
76
|
+
console.log(SUB_HELP[sub] || HELP);
|
|
49
77
|
process.exit(0);
|
|
50
78
|
}
|
|
51
79
|
if (sub !== "contact") {
|
package/lib/ai.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveProjectId } from "./config.mjs";
|
|
2
2
|
import { getSdk } from "./sdk.mjs";
|
|
3
3
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
4
|
+
import { resolvePositionalProject } from "./argparse.mjs";
|
|
4
5
|
|
|
5
6
|
const HELP = `run402 ai — AI translation and moderation tools
|
|
6
7
|
|
|
@@ -8,17 +9,23 @@ Usage:
|
|
|
8
9
|
run402 ai <subcommand> [args...]
|
|
9
10
|
|
|
10
11
|
Subcommands:
|
|
11
|
-
translate
|
|
12
|
-
moderate
|
|
13
|
-
usage
|
|
12
|
+
translate [project_id] <text> --to <lang> [--from <lang>] [--context <hint>]
|
|
13
|
+
moderate [project_id] <text>
|
|
14
|
+
usage [project_id]
|
|
14
15
|
|
|
15
16
|
Examples:
|
|
17
|
+
run402 ai translate "Hello world" --to es # uses active project
|
|
16
18
|
run402 ai translate prj_abc123 "Hello world" --to es
|
|
17
19
|
run402 ai translate prj_abc123 "Hello" --to ja --from en --context "formal business email"
|
|
20
|
+
run402 ai moderate "content to check" # uses active project
|
|
18
21
|
run402 ai moderate prj_abc123 "content to check"
|
|
22
|
+
run402 ai usage # uses active project
|
|
19
23
|
run402 ai usage prj_abc123
|
|
20
24
|
|
|
21
25
|
Notes:
|
|
26
|
+
- [project_id] defaults to the active project when omitted (set with
|
|
27
|
+
'run402 projects use <id>'). Project IDs start with 'prj_'; any first
|
|
28
|
+
positional that doesn't is treated as the next argument instead.
|
|
22
29
|
- translate requires the AI Translation add-on on the project
|
|
23
30
|
- moderate is free for all projects
|
|
24
31
|
- usage shows translation word quota for the current billing period
|
|
@@ -28,25 +35,71 @@ const SUB_HELP = {
|
|
|
28
35
|
translate: `run402 ai translate — Translate text to another language
|
|
29
36
|
|
|
30
37
|
Usage:
|
|
31
|
-
run402 ai translate
|
|
38
|
+
run402 ai translate [project_id] <text> --to <lang> [--from <lang>] [--context <hint>]
|
|
32
39
|
|
|
33
40
|
Arguments:
|
|
34
|
-
|
|
41
|
+
[project_id] Project ID (defaults to the active project if omitted).
|
|
42
|
+
Project IDs start with 'prj_'; any first positional that
|
|
43
|
+
doesn't is treated as the <text> argument instead.
|
|
35
44
|
<text> Text to translate (quote it to preserve spaces)
|
|
36
45
|
|
|
37
46
|
Options:
|
|
38
47
|
--to <lang> Target language code (required, e.g. es, ja, fr)
|
|
39
48
|
--from <lang> Source language code (optional; auto-detected if omitted)
|
|
40
49
|
--context <hint> Optional translation hint (e.g. "formal business email")
|
|
50
|
+
--project <id> Project ID (alternative to the positional argument)
|
|
41
51
|
|
|
42
52
|
Notes:
|
|
43
53
|
- Requires the AI Translation add-on on the project
|
|
44
54
|
- Counts against the project's translation word quota
|
|
45
55
|
|
|
46
56
|
Examples:
|
|
57
|
+
run402 ai translate "Hello world" --to es # uses active project
|
|
47
58
|
run402 ai translate prj_abc123 "Hello world" --to es
|
|
48
59
|
run402 ai translate prj_abc123 "Hello" --to ja --from en \\
|
|
49
60
|
--context "formal business email"
|
|
61
|
+
`,
|
|
62
|
+
moderate: `run402 ai moderate — Run content moderation on text
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
run402 ai moderate [project_id] <text>
|
|
66
|
+
|
|
67
|
+
Arguments:
|
|
68
|
+
[project_id] Project ID (defaults to the active project if omitted).
|
|
69
|
+
Project IDs start with 'prj_'; any first positional that
|
|
70
|
+
doesn't is treated as the <text> argument instead.
|
|
71
|
+
<text> Text to check (quote it to preserve spaces)
|
|
72
|
+
|
|
73
|
+
Options:
|
|
74
|
+
--project <id> Project ID (alternative to the positional argument)
|
|
75
|
+
|
|
76
|
+
Notes:
|
|
77
|
+
- Free for all projects; uses the project's service key
|
|
78
|
+
- Returns a JSON object with 'flagged' (boolean), 'categories' and 'category_scores'
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
run402 ai moderate "content to check" # uses active project
|
|
82
|
+
run402 ai moderate prj_abc123 "content to check"
|
|
83
|
+
`,
|
|
84
|
+
usage: `run402 ai usage — Show AI translation word usage for the current billing cycle
|
|
85
|
+
|
|
86
|
+
Usage:
|
|
87
|
+
run402 ai usage [project_id]
|
|
88
|
+
|
|
89
|
+
Arguments:
|
|
90
|
+
[project_id] Project ID (defaults to the active project if omitted).
|
|
91
|
+
Must start with 'prj_'; any other first positional is an error.
|
|
92
|
+
|
|
93
|
+
Options:
|
|
94
|
+
--project <id> Project ID (alternative to the positional argument)
|
|
95
|
+
|
|
96
|
+
Notes:
|
|
97
|
+
- Reports translation word quota and usage; only meaningful with the
|
|
98
|
+
AI Translation add-on enabled on the project.
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
run402 ai usage # uses active project
|
|
102
|
+
run402 ai usage prj_abc123
|
|
50
103
|
`,
|
|
51
104
|
};
|
|
52
105
|
|
|
@@ -57,20 +110,34 @@ function parseFlag(args, flag) {
|
|
|
57
110
|
return null;
|
|
58
111
|
}
|
|
59
112
|
|
|
113
|
+
// translate has value-bearing flags (--to, --from, --context, --project) that
|
|
114
|
+
// must not be mistaken for positional bare args when prefix-matching.
|
|
115
|
+
const TRANSLATE_VALUE_FLAGS = ["--to", "--from", "--context", "--project"];
|
|
116
|
+
|
|
60
117
|
async function translate(args) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
let
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
118
|
+
// --project <id> wins over positional, mirroring previous behavior.
|
|
119
|
+
const projectOpt = parseFlag(args, "--project");
|
|
120
|
+
let projectId;
|
|
121
|
+
let rest;
|
|
122
|
+
if (projectOpt) {
|
|
123
|
+
projectId = resolveProjectId(projectOpt);
|
|
124
|
+
rest = args;
|
|
125
|
+
} else {
|
|
126
|
+
({ projectId, rest } = resolvePositionalProject(args, {
|
|
127
|
+
valueFlags: TRANSLATE_VALUE_FLAGS,
|
|
128
|
+
}));
|
|
70
129
|
}
|
|
71
130
|
|
|
72
|
-
|
|
73
|
-
|
|
131
|
+
// Walk `rest` as the post-project argv, collecting bare positionals while
|
|
132
|
+
// skipping value-flag pairs. The first bare positional becomes <text>.
|
|
133
|
+
let text = null;
|
|
134
|
+
for (let i = 0; i < rest.length; i++) {
|
|
135
|
+
const arg = rest[i];
|
|
136
|
+
if (TRANSLATE_VALUE_FLAGS.includes(arg)) { i++; continue; }
|
|
137
|
+
if (typeof arg === "string" && arg.startsWith("--")) continue;
|
|
138
|
+
text = arg;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
74
141
|
|
|
75
142
|
const to = parseFlag(args, "--to");
|
|
76
143
|
const from = parseFlag(args, "--from");
|
|
@@ -80,7 +147,7 @@ async function translate(args) {
|
|
|
80
147
|
fail({
|
|
81
148
|
code: "BAD_USAGE",
|
|
82
149
|
message: "Text required.",
|
|
83
|
-
hint: "run402 ai translate
|
|
150
|
+
hint: "run402 ai translate [project_id] <text> --to <lang>",
|
|
84
151
|
});
|
|
85
152
|
}
|
|
86
153
|
if (!to) {
|
|
@@ -95,25 +162,35 @@ async function translate(args) {
|
|
|
95
162
|
}
|
|
96
163
|
}
|
|
97
164
|
|
|
165
|
+
const MODERATE_VALUE_FLAGS = ["--project"];
|
|
166
|
+
|
|
98
167
|
async function moderate(args) {
|
|
99
|
-
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
168
|
+
const projectOpt = parseFlag(args, "--project");
|
|
169
|
+
let projectId;
|
|
170
|
+
let rest;
|
|
171
|
+
if (projectOpt) {
|
|
172
|
+
projectId = resolveProjectId(projectOpt);
|
|
173
|
+
rest = args;
|
|
174
|
+
} else {
|
|
175
|
+
({ projectId, rest } = resolvePositionalProject(args, {
|
|
176
|
+
valueFlags: MODERATE_VALUE_FLAGS,
|
|
177
|
+
}));
|
|
107
178
|
}
|
|
108
179
|
|
|
109
|
-
|
|
110
|
-
|
|
180
|
+
let text = null;
|
|
181
|
+
for (let i = 0; i < rest.length; i++) {
|
|
182
|
+
const arg = rest[i];
|
|
183
|
+
if (MODERATE_VALUE_FLAGS.includes(arg)) { i++; continue; }
|
|
184
|
+
if (typeof arg === "string" && arg.startsWith("--")) continue;
|
|
185
|
+
text = arg;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
111
188
|
|
|
112
189
|
if (!text) {
|
|
113
190
|
fail({
|
|
114
191
|
code: "BAD_USAGE",
|
|
115
192
|
message: "Text required.",
|
|
116
|
-
hint: "run402 ai moderate
|
|
193
|
+
hint: "run402 ai moderate [project_id] <text>",
|
|
117
194
|
});
|
|
118
195
|
}
|
|
119
196
|
|
|
@@ -126,17 +203,16 @@ async function moderate(args) {
|
|
|
126
203
|
}
|
|
127
204
|
|
|
128
205
|
async function usage(args) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
206
|
+
const projectOpt = parseFlag(args, "--project");
|
|
207
|
+
let projectId;
|
|
208
|
+
if (projectOpt) {
|
|
209
|
+
projectId = resolveProjectId(projectOpt);
|
|
210
|
+
} else {
|
|
211
|
+
// No bare-text positional is meaningful here, so reject any non-prj first
|
|
212
|
+
// positional with a clear error.
|
|
213
|
+
({ projectId } = resolvePositionalProject(args, { rejectBareFirst: true }));
|
|
136
214
|
}
|
|
137
215
|
|
|
138
|
-
const projectId = resolveProjectId(projectOpt || positional[0]);
|
|
139
|
-
|
|
140
216
|
try {
|
|
141
217
|
const data = await getSdk().ai.usage(projectId);
|
|
142
218
|
console.log(JSON.stringify({ status: "ok", ...data }));
|
package/lib/apps.mjs
CHANGED
|
@@ -99,6 +99,40 @@ Examples:
|
|
|
99
99
|
run402 apps update prj_abc123 ver_abc123 --description "Updated"
|
|
100
100
|
run402 apps update prj_abc123 ver_abc123 --tags todo,auth --fork-allowed
|
|
101
101
|
run402 apps update prj_abc123 ver_abc123 --no-fork
|
|
102
|
+
`,
|
|
103
|
+
inspect: `run402 apps inspect — Inspect a published app version
|
|
104
|
+
|
|
105
|
+
Usage:
|
|
106
|
+
run402 apps inspect <version_id>
|
|
107
|
+
|
|
108
|
+
Arguments:
|
|
109
|
+
<version_id> Published version ID (e.g. ver_abc123)
|
|
110
|
+
|
|
111
|
+
Examples:
|
|
112
|
+
run402 apps inspect ver_abc123
|
|
113
|
+
`,
|
|
114
|
+
versions: `run402 apps versions — List published versions of a project
|
|
115
|
+
|
|
116
|
+
Usage:
|
|
117
|
+
run402 apps versions <id>
|
|
118
|
+
|
|
119
|
+
Arguments:
|
|
120
|
+
<id> Project ID (e.g. prj_abc123)
|
|
121
|
+
|
|
122
|
+
Examples:
|
|
123
|
+
run402 apps versions prj_abc123
|
|
124
|
+
`,
|
|
125
|
+
delete: `run402 apps delete — Delete a published version
|
|
126
|
+
|
|
127
|
+
Usage:
|
|
128
|
+
run402 apps delete <project_id> <version_id>
|
|
129
|
+
|
|
130
|
+
Arguments:
|
|
131
|
+
<project_id> Project ID that owns the version
|
|
132
|
+
<version_id> Published version ID to delete
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
run402 apps delete prj_abc123 ver_abc123
|
|
102
136
|
`,
|
|
103
137
|
};
|
|
104
138
|
|