run402 1.54.1 → 1.54.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 (71) hide show
  1. package/cli.mjs +18 -0
  2. package/core-dist/allowance-auth.js +5 -0
  3. package/core-dist/allowance.js +49 -1
  4. package/core-dist/config.js +35 -2
  5. package/core-dist/wallet-auth.js +62 -0
  6. package/core-dist/wallet.js +25 -0
  7. package/lib/agent.mjs +29 -1
  8. package/lib/ai.mjs +119 -43
  9. package/lib/apps.mjs +43 -9
  10. package/lib/argparse.mjs +234 -0
  11. package/lib/auth.mjs +17 -4
  12. package/lib/billing.mjs +35 -0
  13. package/lib/blob.mjs +55 -22
  14. package/lib/config.mjs +20 -1
  15. package/lib/contracts.mjs +41 -0
  16. package/lib/deploy.mjs +125 -58
  17. package/lib/domains.mjs +79 -5
  18. package/lib/email.mjs +34 -0
  19. package/lib/functions.mjs +104 -30
  20. package/lib/image.mjs +33 -1
  21. package/lib/message.mjs +50 -3
  22. package/lib/projects.mjs +75 -36
  23. package/lib/sdk-errors.mjs +2 -1
  24. package/lib/secrets.mjs +33 -6
  25. package/lib/sender-domain.mjs +78 -1
  26. package/lib/service.mjs +30 -1
  27. package/lib/status.mjs +4 -1
  28. package/lib/subdomains.mjs +31 -2
  29. package/lib/tier.mjs +41 -1
  30. package/lib/webhooks.mjs +10 -0
  31. package/package.json +1 -1
  32. package/sdk/core-dist/allowance-auth.js +5 -0
  33. package/sdk/core-dist/allowance.js +49 -1
  34. package/sdk/core-dist/config.js +35 -2
  35. package/sdk/core-dist/wallet-auth.js +62 -0
  36. package/sdk/core-dist/wallet.js +25 -0
  37. package/sdk/dist/index.d.ts +1 -0
  38. package/sdk/dist/index.d.ts.map +1 -1
  39. package/sdk/dist/index.js +5 -0
  40. package/sdk/dist/index.js.map +1 -1
  41. package/sdk/dist/namespaces/auth.d.ts +7 -0
  42. package/sdk/dist/namespaces/auth.d.ts.map +1 -1
  43. package/sdk/dist/namespaces/auth.js +24 -0
  44. package/sdk/dist/namespaces/auth.js.map +1 -1
  45. package/sdk/dist/namespaces/billing.d.ts +3 -0
  46. package/sdk/dist/namespaces/billing.d.ts.map +1 -1
  47. package/sdk/dist/namespaces/billing.js +6 -0
  48. package/sdk/dist/namespaces/billing.js.map +1 -1
  49. package/sdk/dist/namespaces/contracts.d.ts +3 -0
  50. package/sdk/dist/namespaces/contracts.d.ts.map +1 -1
  51. package/sdk/dist/namespaces/contracts.js +6 -0
  52. package/sdk/dist/namespaces/contracts.js.map +1 -1
  53. package/sdk/dist/namespaces/email.d.ts +4 -0
  54. package/sdk/dist/namespaces/email.d.ts.map +1 -1
  55. package/sdk/dist/namespaces/email.js +8 -0
  56. package/sdk/dist/namespaces/email.js.map +1 -1
  57. package/sdk/dist/namespaces/projects.d.ts +14 -0
  58. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  59. package/sdk/dist/namespaces/projects.js +72 -0
  60. package/sdk/dist/namespaces/projects.js.map +1 -1
  61. package/sdk/dist/namespaces/sender-domain.d.ts +2 -0
  62. package/sdk/dist/namespaces/sender-domain.d.ts.map +1 -1
  63. package/sdk/dist/namespaces/sender-domain.js +4 -0
  64. package/sdk/dist/namespaces/sender-domain.js.map +1 -1
  65. package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
  66. package/sdk/dist/node/paid-fetch.js +12 -1
  67. package/sdk/dist/node/paid-fetch.js.map +1 -1
  68. package/sdk/dist/scoped.d.ts +8 -1
  69. package/sdk/dist/scoped.d.ts.map +1 -1
  70. package/sdk/dist/scoped.js +21 -0
  71. package/sdk/dist/scoped.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;
@@ -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
- return JSON.parse(readFileSync(p, "utf-8"));
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();
@@ -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
- return process.env.RUN402_API_BASE || "https://api.run402.com";
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
- return process.env.RUN402_DEPLOY_API_BASE || getApiBase();
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 <project_id> <text> --to <lang> [--from <lang>] [--context <hint>]
12
- moderate <project_id> <text>
13
- usage <project_id>
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:
16
- run402 ai translate proj-001 "Hello world" --to es
17
- run402 ai translate proj-001 "Hello" --to ja --from en --context "formal business email"
18
- run402 ai moderate proj-001 "content to check"
19
- run402 ai usage proj-001
17
+ run402 ai translate "Hello world" --to es # uses active project
18
+ run402 ai translate prj_abc123 "Hello world" --to es
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
21
+ run402 ai moderate prj_abc123 "content to check"
22
+ run402 ai usage # uses active project
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 <project_id> <text> --to <lang> [--from <lang>] [--context <hint>]
38
+ run402 ai translate [project_id] <text> --to <lang> [--from <lang>] [--context <hint>]
32
39
 
33
40
  Arguments:
34
- <project_id> Project ID (defaults to the active project if omitted)
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:
47
- run402 ai translate proj-001 "Hello world" --to es
48
- run402 ai translate proj-001 "Hello" --to ja --from en \\
57
+ run402 ai translate "Hello world" --to es # uses active project
58
+ run402 ai translate prj_abc123 "Hello world" --to es
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
- let projectOpt = null;
62
- let text = null;
63
- const positional = [];
64
- let i = 0;
65
- while (i < args.length) {
66
- if (args[i] === "--project" && args[i + 1]) { projectOpt = args[++i]; }
67
- else if (args[i] === "--to" || args[i] === "--from" || args[i] === "--context") { i++; }
68
- else if (!args[i].startsWith("--")) { positional.push(args[i]); }
69
- i++;
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
- const projectId = resolveProjectId(projectOpt || positional[0]);
73
- text = positional[1] || null;
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 <project_id> <text> --to <lang>",
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
- let projectOpt = null;
100
- let text = null;
101
- const positional = [];
102
- let i = 0;
103
- while (i < args.length) {
104
- if (args[i] === "--project" && args[i + 1]) { projectOpt = args[++i]; }
105
- else if (!args[i].startsWith("--")) { positional.push(args[i]); }
106
- i++;
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
- const projectId = resolveProjectId(projectOpt || positional[0]);
110
- text = positional[1] || null;
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 <project_id> <text>",
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
- let projectOpt = null;
130
- const positional = [];
131
- let i = 0;
132
- while (i < args.length) {
133
- if (args[i] === "--project" && args[i + 1]) { projectOpt = args[++i]; }
134
- else if (!args[i].startsWith("--")) { positional.push(args[i]); }
135
- i++;
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
@@ -23,11 +23,11 @@ Examples:
23
23
  run402 apps browse
24
24
  run402 apps browse --tag auth
25
25
  run402 apps fork ver_abc123 my-todo --tier prototype
26
- run402 apps publish proj123 --description "Todo app" --tags todo,auth --visibility public --fork-allowed
27
- run402 apps versions proj123
26
+ run402 apps publish prj_abc123 --description "Todo app" --tags todo,auth --visibility public --fork-allowed
27
+ run402 apps versions prj_abc123
28
28
  run402 apps inspect ver_abc123
29
- run402 apps update proj123 ver_abc123 --description "Updated" --tags todo
30
- run402 apps delete proj123 ver_abc123
29
+ run402 apps update prj_abc123 ver_abc123 --description "Updated" --tags todo
30
+ run402 apps delete prj_abc123 ver_abc123
31
31
  `;
32
32
 
33
33
  const SUB_HELP = {
@@ -76,8 +76,8 @@ Options:
76
76
  --fork-allowed Allow other users to fork this app
77
77
 
78
78
  Examples:
79
- run402 apps publish proj123 --description "Todo app" --tags todo,auth
80
- run402 apps publish proj123 --visibility public --fork-allowed
79
+ run402 apps publish prj_abc123 --description "Todo app" --tags todo,auth
80
+ run402 apps publish prj_abc123 --visibility public --fork-allowed
81
81
  `,
82
82
  update: `run402 apps update — Update a published version's metadata
83
83
 
@@ -96,9 +96,43 @@ Options:
96
96
  --no-fork Disable forking for this version
97
97
 
98
98
  Examples:
99
- run402 apps update proj123 ver_abc123 --description "Updated"
100
- run402 apps update proj123 ver_abc123 --tags todo,auth --fork-allowed
101
- run402 apps update proj123 ver_abc123 --no-fork
99
+ run402 apps update prj_abc123 ver_abc123 --description "Updated"
100
+ run402 apps update prj_abc123 ver_abc123 --tags todo,auth --fork-allowed
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