@dataparade/cli 0.0.6 → 0.0.7

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 (55) hide show
  1. package/README.md +3 -2
  2. package/dist/package.json +1 -1
  3. package/dist/src/cli.js +65 -36
  4. package/dist/src/config/env.js +7 -2
  5. package/dist/src/config/load-cli-env.d.ts +2 -0
  6. package/dist/src/config/load-cli-env.js +11 -2
  7. package/dist/src/config/redact.js +3 -0
  8. package/dist/src/config/validate-scan-ai.js +13 -7
  9. package/dist/src/core/pipeline/ai-orchestrator-options.js +8 -4
  10. package/dist/src/core/pipeline/orchestrator.js +1 -1
  11. package/dist/src/core/schema/scan-config.schema.d.ts +1 -0
  12. package/dist/src/core/schema/scan-config.schema.js +1 -0
  13. package/dist/src/core/types/config.d.ts +5 -0
  14. package/dist/src/platform-api/anonymous-ai-session-client.d.ts +14 -0
  15. package/dist/src/platform-api/anonymous-ai-session-client.js +40 -0
  16. package/dist/src/platform-api/upload-client.d.ts +10 -3
  17. package/dist/src/platform-api/upload-client.js +35 -16
  18. package/dist/src/platform-api/upload.types.d.ts +10 -5
  19. package/dist/src/upload/build-preview-url.d.ts +1 -0
  20. package/dist/src/upload/build-preview-url.js +6 -0
  21. package/dist/src/upload/run-upload.js +20 -4
  22. package/dist/src/upload/types.d.ts +2 -2
  23. package/dist/tests/cli/cli.spec.js +17 -3
  24. package/dist/tests/unit/cli/scan-command-ai-flags.spec.js +19 -0
  25. package/dist/tests/unit/cli/scan-command-e2e-monorepo-app-plus-terraform.spec.js +12 -8
  26. package/dist/tests/unit/cli/scan-command-e2e-monorepo-front-back-sections.spec.js +10 -1
  27. package/dist/tests/unit/cli/scan-command-e2e-python-basic.spec.js +10 -1
  28. package/dist/tests/unit/cli/scan-command-e2e-terraform-basic.spec.js +12 -8
  29. package/dist/tests/unit/cli/scan-command-e2e-ts-sample.spec.js +10 -1
  30. package/dist/tests/unit/cli/scan-command-e2e-ts-sentry-route-env.spec.js +1 -1
  31. package/dist/tests/unit/cli/scan-command-exit-code.spec.js +21 -0
  32. package/dist/tests/unit/cli/scan-quota-flow.spec.js +1 -1
  33. package/dist/tests/unit/config/load-cli-env.spec.d.ts +1 -0
  34. package/dist/tests/unit/config/load-cli-env.spec.js +52 -0
  35. package/dist/tests/unit/config/redact.spec.js +1 -1
  36. package/dist/tests/unit/config/resolve-config.spec.js +1 -1
  37. package/dist/tests/unit/config/validate-scan-ai.spec.js +25 -0
  38. package/dist/tests/unit/core/ai-orchestrator-options-anon.spec.d.ts +1 -0
  39. package/dist/tests/unit/core/ai-orchestrator-options-anon.spec.js +29 -0
  40. package/dist/tests/unit/core/dependency-manifest-performance-budgets.spec.js +1 -1
  41. package/dist/tests/unit/core/graph-mapping.spec.js +1 -1
  42. package/dist/tests/unit/core/invariant-tests-dedupe-determinism.spec.js +1 -1
  43. package/dist/tests/unit/core/no-source-code-leakage.spec.js +1 -1
  44. package/dist/tests/unit/core/orchestrator.spec.js +12 -12
  45. package/dist/tests/unit/core/output-json.spec.js +2 -2
  46. package/dist/tests/unit/core/parsing-error-handling-nonfatal.spec.js +1 -1
  47. package/dist/tests/unit/core/scan-result-language-stats.spec.js +1 -1
  48. package/dist/tests/unit/core/structural-scan.spec.js +1 -1
  49. package/dist/tests/unit/core/terraform-json-overlay.spec.js +1 -1
  50. package/dist/tests/unit/platform-api/anonymous-ai-session-client.spec.d.ts +1 -0
  51. package/dist/tests/unit/platform-api/anonymous-ai-session-client.spec.js +35 -0
  52. package/dist/tests/unit/upload/build-preview-url.spec.js +16 -0
  53. package/dist/tests/unit/upload/run-upload.spec.d.ts +1 -0
  54. package/dist/tests/unit/upload/run-upload.spec.js +43 -0
  55. package/package.json +1 -1
package/README.md CHANGED
@@ -38,10 +38,11 @@ List commands and options: `npx @dataparade/cli --help` and `npx @dataparade/cli
38
38
 
39
39
  ## Upload to dashboard
40
40
 
41
- After a scan, the CLI **auto-uploads** `dataflow.json` when `DATAPARADE_WORKSPACE_API_KEY` is set (from **Workspace Access keys**). This creates an **import preview draft** in the web app — not a finished assessment.
41
+ After a scan, the CLI **auto-uploads** `dataflow.json` by default (unless you opt out). This creates an **import preview draft** in the web app — not a finished assessment.
42
42
 
43
+ - **With a workspace API key** (`DATAPARADE_WORKSPACE_API_KEY` from **Workspace → Access keys**): prints a dashboard link (`?importDraft=`) for that workspace.
44
+ - **Without a workspace key**: prints a sign-up link (`/preview/cli/<token>`). Create an account to open **Preview & Edit** on your dashboard.
43
45
  - Opt out: `--skip-auto-upload` or `DATAPARADE_SKIP_AUTO_UPLOAD=true`
44
- - Without a workspace key, the scan still succeeds; run `upload` later
45
46
  - Upload alone does **not** consume scan quota
46
47
 
47
48
  ```bash
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dataparade/cli",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "dataPARADE CLI — scan codebases for privacy and security data flows",
5
5
  "publishConfig": {
6
6
  "access": "public",
package/dist/src/cli.js CHANGED
@@ -130,7 +130,8 @@ function createProgram() {
130
130
  .option("--no-terraform-stack-section-auto", "Do not infer terraformStackSectionPathDepth from .tf layout when depth is unset")
131
131
  .option("--monorepo-package-section-path-depth <n>", "Workspace package section depth (max 3): primary packages and rollup use N POSIX path segments (e.g. 3 for packages/twenty-apps/hello)")
132
132
  .option("--no-monorepo-package-section-auto", "Do not infer monorepoPackageSectionPathDepth from package.json layout when depth is unset")
133
- .option("--ai-inference", "Enable post-scan AI inference pipeline")
133
+ .option("--ai-inference", "Enable post-scan AI inference (default: on; SCAN_AI_INFERENCE=false also disables)")
134
+ .option("--no-ai-inference", "Disable post-scan AI inference")
134
135
  .option("--ai-provider <provider>", `AI provider: ${types_1.AI_PROVIDER_IDS.join("|")}`)
135
136
  .option("--ai-model <model>", "Model name for AI inference")
136
137
  .option("--ai-endpoint <url>", "Override provider endpoint URL")
@@ -265,14 +266,53 @@ function createProgram() {
265
266
  config.cliQuotaJobId = preflight.jobId;
266
267
  if (config.enableAiInference &&
267
268
  preflight.suggestedAiBudgetTokens > 0) {
268
- config.aiBudgetTokens = preflight.suggestedAiBudgetTokens;
269
+ // suggestedAiBudgetTokens is a server ceiling (remaining quota /
270
+ // per-job cap), not a spend target. Never raise the CLI working
271
+ // budget above the configured default (or user override).
272
+ config.aiBudgetTokens = Math.min(config.aiBudgetTokens ?? 12_000, preflight.suggestedAiBudgetTokens);
269
273
  }
270
274
  if (preflight.aiDelivery === "platform_proxy") {
271
275
  config.aiMode = "platform";
272
276
  }
273
277
  if (isInteractive) {
274
278
  // eslint-disable-next-line no-console
275
- console.log(`[scan] quota: scans_remaining=${preflight.scansRemaining} ai_tokens_remaining=${preflight.aiTokensRemaining} job_id=${preflight.jobId}`);
279
+ console.log(`[scan] quota: scans_remaining=${preflight.scansRemaining} ai_tokens_remaining=${preflight.aiTokensRemaining} job_id=${preflight.jobId} working_budget=${config.aiBudgetTokens ?? 0}`);
280
+ }
281
+ }
282
+ else if (!workspaceApiKey &&
283
+ !usesByok &&
284
+ config.enableAiInference) {
285
+ try {
286
+ const { cliAnonymousAiSession } = await Promise.resolve().then(() => __importStar(require("./platform-api/anonymous-ai-session-client")));
287
+ const session = await cliAnonymousAiSession({
288
+ projectName: config.projectName,
289
+ });
290
+ cliQuotaJobId = session.jobId;
291
+ config.cliQuotaJobId = session.jobId;
292
+ config.anonSessionToken = session.sessionToken;
293
+ if (session.suggestedAiBudgetTokens > 0) {
294
+ // Anon suggested value is the hard per-job ceiling (default 100k).
295
+ // Applying it as the working budget caused scans to burn ~100k tokens.
296
+ config.aiBudgetTokens = Math.min(config.aiBudgetTokens ?? 12_000, session.suggestedAiBudgetTokens);
297
+ }
298
+ if (session.aiDelivery === "platform_proxy") {
299
+ config.aiMode = "platform";
300
+ }
301
+ if (isInteractive) {
302
+ // eslint-disable-next-line no-console
303
+ console.log(`[scan] anonymous platform AI: job_id=${session.jobId} working_budget=${config.aiBudgetTokens ?? 0} server_cap=${session.suggestedAiBudgetTokens} (claim debits actual tokens used)`);
304
+ }
305
+ }
306
+ catch (anonSessionError) {
307
+ const { CliAnonymousIpLimitError } = await Promise.resolve().then(() => __importStar(require("./platform-api/anonymous-ai-session-client")));
308
+ const message = anonSessionError instanceof Error
309
+ ? anonSessionError.message
310
+ : "Anonymous AI session failed.";
311
+ // eslint-disable-next-line no-console
312
+ console.error(`[scan] ${message}`);
313
+ process.exitCode =
314
+ anonSessionError instanceof CliAnonymousIpLimitError ? 2 : 1;
315
+ return;
276
316
  }
277
317
  }
278
318
  const configValidation = (0, scan_config_schema_1.validateScanConfiguration)(config);
@@ -310,10 +350,12 @@ function createProgram() {
310
350
  const s = scanResult.aiInferenceSummary;
311
351
  // eslint-disable-next-line no-console
312
352
  console.log(`[scan] llm-inference summary: candidates=${s.candidatesConsidered} proposals=${s.proposalsGenerated} (provider=${s.proposalsGeneratedProvider}) applied=${s.proposalsApplied} (provider=${s.proposalsAppliedProvider}) rejected=${s.proposalsRejected} (${s.aiProvider}/${s.aiModel})`);
313
- if (options.aiVerbose) {
314
- // eslint-disable-next-line no-console
315
- console.log(`[scan] ai-usage totals: provider_calls=${s.providerCalls} tokens_in=${s.inputTokens} tokens_out=${s.outputTokens} tokens_total=${s.totalTokens} estimated_cost_usd=${formatUsd(s.estimatedCostUsd)}`);
316
- }
353
+ // Always show real provider token usage (budget_cap is only a ceiling, not cost).
354
+ // eslint-disable-next-line no-console
355
+ console.log(`[scan] ai-usage: provider_calls=${s.providerCalls} tokens_in=${s.inputTokens} tokens_out=${s.outputTokens} tokens_total=${s.totalTokens}` +
356
+ (typeof s.estimatedCostUsd === "number"
357
+ ? ` estimated_cost_usd=${formatUsd(s.estimatedCostUsd)}`
358
+ : ""));
317
359
  if (options.aiVerbose && scanResult.aiInferenceProposalDetails) {
318
360
  printAiInferenceVerbose(scanResult.aiInferenceProposalDetails);
319
361
  }
@@ -387,30 +429,23 @@ function createProgram() {
387
429
  const skipAutoUpload = Boolean(options.skipAutoUpload) ||
388
430
  (0, upload_env_1.resolveSkipAutoUpload)(process.env);
389
431
  if (!skipAutoUpload) {
390
- const uploadApiKey = workspaceApiKey;
391
- if (!uploadApiKey) {
392
- // eslint-disable-next-line no-console
393
- console.log(`[scan] Auto-upload skipped (no workspace API key). Run: dataparade upload ${dataflowOutputPath}`);
432
+ try {
433
+ const { runDataflowUpload } = await Promise.resolve().then(() => __importStar(require("./upload/run-upload")));
434
+ const dataflowWrapper = (0, json_1.buildDataflowWrapper)(scanResult, diagramGraph, { projectName: resolvedProjectName });
435
+ await runDataflowUpload({
436
+ apiKey: workspaceApiKey,
437
+ dataflow: dataflowWrapper,
438
+ projectName: resolvedProjectName,
439
+ scanJobId: cliQuotaJobId,
440
+ logPrefix: "[scan]",
441
+ });
394
442
  }
395
- else {
396
- try {
397
- const { runDataflowUpload } = await Promise.resolve().then(() => __importStar(require("./upload/run-upload")));
398
- const dataflowWrapper = (0, json_1.buildDataflowWrapper)(scanResult, diagramGraph, { projectName: resolvedProjectName });
399
- await runDataflowUpload({
400
- apiKey: uploadApiKey,
401
- dataflow: dataflowWrapper,
402
- projectName: resolvedProjectName,
403
- scanJobId: cliQuotaJobId,
404
- logPrefix: "[scan]",
405
- });
406
- }
407
- catch (uploadError) {
408
- const uploadMessage = uploadError instanceof Error
409
- ? uploadError.message
410
- : "Unknown upload error.";
411
- // eslint-disable-next-line no-console
412
- console.error(`[scan] Auto-upload failed: ${uploadMessage}`);
413
- }
443
+ catch (uploadError) {
444
+ const uploadMessage = uploadError instanceof Error
445
+ ? uploadError.message
446
+ : "Unknown upload error.";
447
+ // eslint-disable-next-line no-console
448
+ console.error(`[scan] Auto-upload failed: ${uploadMessage}`);
414
449
  }
415
450
  }
416
451
  }
@@ -498,12 +533,6 @@ function createProgram() {
498
533
  const apiKey = options.workspaceApiKey?.trim() ||
499
534
  options.apiKey?.trim() ||
500
535
  (0, scan_env_1.resolveWorkspaceApiKey)(process.env);
501
- if (!apiKey) {
502
- // eslint-disable-next-line no-console
503
- console.error("[upload] error: workspace API key is required (DATAPARADE_WORKSPACE_API_KEY or --workspace-api-key)");
504
- process.exitCode = 1;
505
- return;
506
- }
507
536
  const filePath = path_1.default.resolve(process.cwd(), file || "dataflow.json");
508
537
  let dataflow;
509
538
  try {
@@ -22,10 +22,15 @@ function parseBoolean(value) {
22
22
  if (value == null)
23
23
  return undefined;
24
24
  const normalized = value.trim().toLowerCase();
25
- if (normalized === "true")
25
+ if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
26
26
  return true;
27
- if (normalized === "false")
27
+ }
28
+ if (normalized === "false" ||
29
+ normalized === "0" ||
30
+ normalized === "no" ||
31
+ normalized === "off") {
28
32
  return false;
33
+ }
29
34
  return undefined;
30
35
  }
31
36
  function loadCliConfigEnv(env) {
@@ -1 +1,3 @@
1
+ /** @internal Exported for unit tests. */
2
+ export declare function findCliPackageRoot(startDir: string): string | undefined;
1
3
  export declare function loadCliDotenv(): void;
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.findCliPackageRoot = findCliPackageRoot;
6
7
  exports.loadCliDotenv = loadCliDotenv;
7
8
  /**
8
9
  * Loads `cli/.env` when not running under Jest.
@@ -16,9 +17,12 @@ const node_path_1 = __importDefault(require("node:path"));
16
17
  const dotenv_1 = require("dotenv");
17
18
  const CLI_PACKAGE_NAME = "@dataparade/cli";
18
19
  const tried = { current: false };
20
+ /** @internal Exported for unit tests. */
19
21
  function findCliPackageRoot(startDir) {
20
22
  let dir = node_path_1.default.resolve(startDir);
21
23
  const { root } = node_path_1.default.parse(dir);
24
+ /** Fallback when tsc emits package.json under dist/ but no outer root is found. */
25
+ let distPackageRoot;
22
26
  while (dir !== root) {
23
27
  const pkgPath = node_path_1.default.join(dir, "package.json");
24
28
  if (node_fs_1.default.existsSync(pkgPath)) {
@@ -26,7 +30,12 @@ function findCliPackageRoot(startDir) {
26
30
  const raw = node_fs_1.default.readFileSync(pkgPath, "utf8");
27
31
  const pkg = JSON.parse(raw);
28
32
  if (pkg.name === CLI_PACKAGE_NAME) {
29
- return dir;
33
+ if (node_path_1.default.basename(dir) === "dist") {
34
+ distPackageRoot = dir;
35
+ }
36
+ else {
37
+ return dir;
38
+ }
30
39
  }
31
40
  }
32
41
  catch {
@@ -35,7 +44,7 @@ function findCliPackageRoot(startDir) {
35
44
  }
36
45
  dir = node_path_1.default.dirname(dir);
37
46
  }
38
- return undefined;
47
+ return distPackageRoot;
39
48
  }
40
49
  function loadCliDotenv() {
41
50
  if (tried.current)
@@ -10,5 +10,8 @@ function redactScanConfigurationForDisplay(config) {
10
10
  if (config.workspaceApiKey?.trim()) {
11
11
  redacted.workspaceApiKey = "<redacted>";
12
12
  }
13
+ if (config.anonSessionToken?.trim()) {
14
+ redacted.anonSessionToken = "<redacted>";
15
+ }
13
16
  return redacted;
14
17
  }
@@ -13,15 +13,20 @@ function validateAiInferenceCredentials(config) {
13
13
  Boolean(config.aiModel?.trim()) &&
14
14
  Boolean(config.aiApiKey?.trim());
15
15
  const hasWorkspace = Boolean(config.workspaceApiKey?.trim());
16
- if (hasByok && hasWorkspace) {
17
- errors.push("ai-inference: set either SCAN_BYOK_* (your LLM provider) or DATAPARADE_WORKSPACE_API_KEY (platform AI), not both");
16
+ const hasAnonSession = Boolean(config.anonSessionToken?.trim());
17
+ if (hasByok && (hasWorkspace || hasAnonSession)) {
18
+ errors.push("ai-inference: set either SCAN_BYOK_* (your LLM provider) or platform AI (DATAPARADE_WORKSPACE_API_KEY or anonymous session), not both");
18
19
  return errors;
19
20
  }
20
- if (!hasByok && !hasWorkspace) {
21
- errors.push("ai-inference: LLM inference requires SCAN_BYOK_PROVIDER, SCAN_BYOK_MODEL, and SCAN_BYOK_API_KEY, or DATAPARADE_WORKSPACE_API_KEY for platform AI");
21
+ if (hasWorkspace && hasAnonSession) {
22
+ errors.push("ai-inference: set either DATAPARADE_WORKSPACE_API_KEY or an anonymous AI session, not both");
23
+ return errors;
24
+ }
25
+ if (!hasByok && !hasWorkspace && !hasAnonSession) {
26
+ errors.push("ai-inference: LLM inference requires SCAN_BYOK_PROVIDER, SCAN_BYOK_MODEL, and SCAN_BYOK_API_KEY, DATAPARADE_WORKSPACE_API_KEY for platform AI, or an anonymous platform AI session");
22
27
  }
23
- if (hasWorkspace && !config.cliQuotaJobId) {
24
- errors.push("ai-inference: platform AI requires a quota preflight job id (pass DATAPARADE_WORKSPACE_API_KEY and ensure preflight succeeded)");
28
+ if ((hasWorkspace || hasAnonSession) && !config.cliQuotaJobId) {
29
+ errors.push("ai-inference: platform AI requires a quota/session job id (preflight or anonymous-session must succeed)");
25
30
  }
26
31
  return errors;
27
32
  }
@@ -32,7 +37,8 @@ function resolveAiMode(config) {
32
37
  return "hosted_worker";
33
38
  if (config.aiApiKey?.trim())
34
39
  return "byok";
35
- if (config.workspaceApiKey?.trim())
40
+ if (config.workspaceApiKey?.trim() || config.anonSessionToken?.trim()) {
36
41
  return "platform";
42
+ }
37
43
  return "none";
38
44
  }
@@ -8,7 +8,9 @@ function buildAgentOrchestratorOptions(config, opts) {
8
8
  const platformProvider = process.env.SCAN_WORKER_LLM_PROVIDER?.trim() ||
9
9
  "openai";
10
10
  const platformModel = process.env.SCAN_WORKER_LLM_MODEL?.trim() ||
11
- config.aiModel?.trim() ||
11
+ (config.aiModel?.trim() && config.aiModel.trim() !== "heuristic"
12
+ ? config.aiModel.trim()
13
+ : undefined) ||
12
14
  "gpt-4o-mini";
13
15
  const base = {
14
16
  provider: aiMode === "platform"
@@ -29,11 +31,13 @@ function buildAgentOrchestratorOptions(config, opts) {
29
31
  skipStructuralHeuristics: opts.skipStructuralHeuristics,
30
32
  };
31
33
  if (aiMode === "platform" &&
32
- config.workspaceApiKey?.trim() &&
33
- config.cliQuotaJobId?.trim()) {
34
+ config.cliQuotaJobId?.trim() &&
35
+ (config.workspaceApiKey?.trim() || config.anonSessionToken?.trim())) {
34
36
  base.platformProxy = {
35
37
  apiBaseUrl: config.platformApiBaseUrl ?? (0, dataparade_api_base_url_1.getDataparadeApiBaseUrl)(),
36
- workspaceApiKey: config.workspaceApiKey.trim(),
38
+ workspaceApiKey: (config.workspaceApiKey?.trim() ||
39
+ config.anonSessionToken?.trim() ||
40
+ ""),
37
41
  jobId: config.cliQuotaJobId.trim(),
38
42
  };
39
43
  base.apiKey = undefined;
@@ -27,7 +27,7 @@ function createDefaultScanConfiguration(overrides = {}) {
27
27
  languages: undefined,
28
28
  minimumConfidence: 0.5,
29
29
  deepAnalysis: false,
30
- enableAiInference: false,
30
+ enableAiInference: true,
31
31
  aiProvider: "mock",
32
32
  aiModel: "heuristic",
33
33
  aiEndpoint: undefined,
@@ -48,6 +48,7 @@ export declare const scanConfigurationSchema: z.ZodObject<{
48
48
  }>>;
49
49
  aiVerbose: z.ZodOptional<z.ZodBoolean>;
50
50
  workspaceApiKey: z.ZodOptional<z.ZodString>;
51
+ anonSessionToken: z.ZodOptional<z.ZodString>;
51
52
  aiMode: z.ZodOptional<z.ZodEnum<{
52
53
  byok: "byok";
53
54
  platform: "platform";
@@ -45,6 +45,7 @@ exports.scanConfigurationSchema = zod_1.z.object({
45
45
  aiInferenceScope: zod_1.z.enum(["default", "third_party_only"]).optional(),
46
46
  aiVerbose: zod_1.z.boolean().optional(),
47
47
  workspaceApiKey: zod_1.z.string().min(1).optional(),
48
+ anonSessionToken: zod_1.z.string().min(1).optional(),
48
49
  aiMode: zod_1.z.enum(["byok", "platform", "hosted_worker", "none"]).optional(),
49
50
  platformApiBaseUrl: zod_1.z.string().min(1).optional(),
50
51
  cliQuotaJobId: zod_1.z.string().min(1).optional(),
@@ -48,6 +48,11 @@ export interface ScanConfiguration {
48
48
  aiThirdPartyDataFlowEnabled?: boolean;
49
49
  /** DataParade workspace API key (quota + platform LLM proxy). */
50
50
  workspaceApiKey?: string;
51
+ /**
52
+ * Anonymous platform AI session token (`dp_anon_…`) from
53
+ * POST /api/scans/cli/ai/anonymous-session. Mutually exclusive with workspaceApiKey.
54
+ */
55
+ anonSessionToken?: string;
51
56
  /** How LLM inference is billed: byok (direct provider) or platform (API proxy). */
52
57
  aiMode?: "byok" | "platform" | "hosted_worker" | "none";
53
58
  /** Base URL for platform API (preflight / infer / complete). */
@@ -0,0 +1,14 @@
1
+ export type AnonymousAiSessionResponse = {
2
+ sessionToken: string;
3
+ jobId: string;
4
+ suggestedAiBudgetTokens: number;
5
+ expiresAt: string;
6
+ aiDelivery: "platform_proxy" | "none";
7
+ };
8
+ export declare class CliAnonymousIpLimitError extends Error {
9
+ readonly status = 429;
10
+ constructor(message: string);
11
+ }
12
+ export declare function cliAnonymousAiSession(input?: {
13
+ projectName?: string;
14
+ }): Promise<AnonymousAiSessionResponse>;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CliAnonymousIpLimitError = void 0;
4
+ exports.cliAnonymousAiSession = cliAnonymousAiSession;
5
+ const dataparade_api_base_url_1 = require("./dataparade-api-base-url");
6
+ class CliAnonymousIpLimitError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.status = 429;
10
+ this.name = "CliAnonymousIpLimitError";
11
+ }
12
+ }
13
+ exports.CliAnonymousIpLimitError = CliAnonymousIpLimitError;
14
+ async function cliAnonymousAiSession(input) {
15
+ const res = await fetch(`${(0, dataparade_api_base_url_1.getDataparadeApiBaseUrl)()}/api/scans/cli/ai/anonymous-session`, {
16
+ method: "POST",
17
+ headers: { "Content-Type": "application/json" },
18
+ body: JSON.stringify({
19
+ projectName: input?.projectName,
20
+ }),
21
+ });
22
+ if (!res.ok) {
23
+ let message = `Anonymous AI session failed (${res.status})`;
24
+ try {
25
+ const body = (await res.json());
26
+ if (typeof body.message === "string")
27
+ message = body.message;
28
+ else if (Array.isArray(body.message))
29
+ message = body.message.join(", ");
30
+ }
31
+ catch {
32
+ // ignore
33
+ }
34
+ if (res.status === 429) {
35
+ throw new CliAnonymousIpLimitError(message);
36
+ }
37
+ throw new Error(message);
38
+ }
39
+ return (await res.json());
40
+ }
@@ -1,3 +1,10 @@
1
- import type { CliUploadPreviewInput, CliUploadPreviewResponse } from "./upload.types";
2
- export type { CliUploadPreviewInput, CliUploadPreviewResponse } from "./upload.types";
3
- export declare function cliUploadPreview(input: CliUploadPreviewInput): Promise<CliUploadPreviewResponse>;
1
+ import type { CliAnonymousUploadPreviewResponse, CliUploadPreviewInput, CliUploadPreviewResponse } from "./upload.types";
2
+ export type { CliAnonymousUploadPreviewResponse, CliUploadPreviewInput, CliUploadPreviewResponse, } from "./upload.types";
3
+ export declare function cliUploadAnonymousPreview(input: {
4
+ dataflow: unknown;
5
+ projectName?: string;
6
+ scanJobId?: string;
7
+ }): Promise<CliAnonymousUploadPreviewResponse>;
8
+ export declare function cliUploadPreview(input: CliUploadPreviewInput & {
9
+ apiKey: string;
10
+ }): Promise<CliUploadPreviewResponse>;
@@ -1,17 +1,49 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cliUploadAnonymousPreview = cliUploadAnonymousPreview;
3
4
  exports.cliUploadPreview = cliUploadPreview;
4
5
  const dataparade_api_base_url_1 = require("./dataparade-api-base-url");
5
- function buildHeaders(apiKey) {
6
+ function buildAuthHeaders(apiKey) {
6
7
  return {
7
8
  Authorization: `Bearer ${apiKey}`,
8
9
  "Content-Type": "application/json",
9
10
  };
10
11
  }
12
+ async function parseUploadError(res) {
13
+ let message = `Upload failed (${res.status})`;
14
+ try {
15
+ const body = (await res.json());
16
+ if (typeof body.message === "string")
17
+ message = body.message;
18
+ else if (Array.isArray(body.message))
19
+ message = body.message.join(", ");
20
+ else if (typeof body.error === "string")
21
+ message = body.error;
22
+ }
23
+ catch {
24
+ // ignore
25
+ }
26
+ throw new Error(message);
27
+ }
28
+ async function cliUploadAnonymousPreview(input) {
29
+ const res = await fetch(`${(0, dataparade_api_base_url_1.getDataparadeApiBaseUrl)()}/api/scans/cli/preview`, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({
33
+ dataflow: input.dataflow,
34
+ projectName: input.projectName,
35
+ scanJobId: input.scanJobId,
36
+ }),
37
+ });
38
+ if (!res.ok) {
39
+ return await parseUploadError(res);
40
+ }
41
+ return (await res.json());
42
+ }
11
43
  async function cliUploadPreview(input) {
12
44
  const res = await fetch(`${(0, dataparade_api_base_url_1.getDataparadeApiBaseUrl)()}/api/scans/cli/upload`, {
13
45
  method: "POST",
14
- headers: buildHeaders(input.apiKey),
46
+ headers: buildAuthHeaders(input.apiKey),
15
47
  body: JSON.stringify({
16
48
  dataflow: input.dataflow,
17
49
  projectName: input.projectName,
@@ -19,20 +51,7 @@ async function cliUploadPreview(input) {
19
51
  }),
20
52
  });
21
53
  if (!res.ok) {
22
- let message = `Upload failed (${res.status})`;
23
- try {
24
- const body = (await res.json());
25
- if (typeof body.message === "string")
26
- message = body.message;
27
- else if (Array.isArray(body.message))
28
- message = body.message.join(", ");
29
- else if (typeof body.error === "string")
30
- message = body.error;
31
- }
32
- catch {
33
- // ignore
34
- }
35
- throw new Error(message);
54
+ return await parseUploadError(res);
36
55
  }
37
56
  return (await res.json());
38
57
  }
@@ -1,10 +1,15 @@
1
+ export type CliUploadPreviewResponse = {
2
+ draftId: string;
3
+ projectName?: string | null;
4
+ };
5
+ export type CliAnonymousUploadPreviewResponse = {
6
+ claimToken: string;
7
+ expiresAt: string;
8
+ projectName: string;
9
+ };
1
10
  export type CliUploadPreviewInput = {
2
- apiKey: string;
11
+ apiKey?: string;
3
12
  dataflow: unknown;
4
13
  projectName?: string;
5
14
  scanJobId?: string;
6
15
  };
7
- export type CliUploadPreviewResponse = {
8
- draftId: string;
9
- projectName?: string | null;
10
- };
@@ -1 +1,2 @@
1
1
  export declare function buildImportPreviewUrl(draftId: string): string;
2
+ export declare function buildAnonymousCliPreviewUrl(claimToken: string): string;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildImportPreviewUrl = buildImportPreviewUrl;
4
+ exports.buildAnonymousCliPreviewUrl = buildAnonymousCliPreviewUrl;
4
5
  const dataparade_app_base_url_1 = require("../platform-api/dataparade-app-base-url");
5
6
  function buildImportPreviewUrl(draftId) {
6
7
  const base = (0, dataparade_app_base_url_1.getDataparadeAppBaseUrl)();
@@ -8,3 +9,8 @@ function buildImportPreviewUrl(draftId) {
8
9
  url.searchParams.set("importDraft", draftId);
9
10
  return url.toString();
10
11
  }
12
+ function buildAnonymousCliPreviewUrl(claimToken) {
13
+ const base = (0, dataparade_app_base_url_1.getDataparadeAppBaseUrl)();
14
+ const encoded = encodeURIComponent(claimToken);
15
+ return `${base}/preview/cli/${encoded}`;
16
+ }
@@ -5,14 +5,30 @@ const upload_client_1 = require("../platform-api/upload-client");
5
5
  const build_preview_url_1 = require("./build-preview-url");
6
6
  async function runDataflowUpload(input) {
7
7
  const prefix = input.logPrefix ?? "[upload]";
8
- const result = await (0, upload_client_1.cliUploadPreview)({
9
- apiKey: input.apiKey,
8
+ if (input.apiKey?.trim()) {
9
+ const result = await (0, upload_client_1.cliUploadPreview)({
10
+ apiKey: input.apiKey.trim(),
11
+ dataflow: input.dataflow,
12
+ projectName: input.projectName,
13
+ scanJobId: input.scanJobId,
14
+ });
15
+ const previewUrl = (0, build_preview_url_1.buildImportPreviewUrl)(result.draftId);
16
+ // eslint-disable-next-line no-console
17
+ console.log(`${prefix} Preview ready: ${previewUrl}`);
18
+ return { draftId: result.draftId, previewUrl };
19
+ }
20
+ const result = await (0, upload_client_1.cliUploadAnonymousPreview)({
10
21
  dataflow: input.dataflow,
11
22
  projectName: input.projectName,
12
23
  scanJobId: input.scanJobId,
13
24
  });
14
- const previewUrl = (0, build_preview_url_1.buildImportPreviewUrl)(result.draftId);
25
+ const previewUrl = (0, build_preview_url_1.buildAnonymousCliPreviewUrl)(result.claimToken);
15
26
  // eslint-disable-next-line no-console
16
27
  console.log(`${prefix} Preview ready: ${previewUrl}`);
17
- return { draftId: result.draftId, previewUrl };
28
+ // eslint-disable-next-line no-console
29
+ console.log(`${prefix} Claiming this preview after signup uses 1 scan slot` +
30
+ (input.scanJobId
31
+ ? " and any platform AI tokens used during the scan."
32
+ : "."));
33
+ return { previewUrl };
18
34
  }
@@ -1,11 +1,11 @@
1
1
  export type RunDataflowUploadInput = {
2
- apiKey: string;
2
+ apiKey?: string;
3
3
  dataflow: unknown;
4
4
  projectName?: string;
5
5
  scanJobId?: string;
6
6
  logPrefix?: string;
7
7
  };
8
8
  export type RunDataflowUploadResult = {
9
- draftId: string;
9
+ draftId?: string;
10
10
  previewUrl: string;
11
11
  };
@@ -14,7 +14,7 @@ describe("CLI", () => {
14
14
  }
15
15
  });
16
16
  it("scan <path> writes a dataflow.json wrapper to the current directory and exits 0", () => {
17
- const result = (0, child_process_1.spawnSync)("node", [cliDistPath, "scan", "."], {
17
+ const result = (0, child_process_1.spawnSync)("node", [cliDistPath, "scan", ".", "--no-ai-inference", "--skip-auto-upload"], {
18
18
  encoding: "utf-8",
19
19
  cwd: path_1.default.join(__dirname, "../../"),
20
20
  });
@@ -33,7 +33,13 @@ describe("CLI", () => {
33
33
  it("scan ../frontend produces a dataflow.json wrapper with at least one node", () => {
34
34
  const cliCwd = path_1.default.join(__dirname, "../../");
35
35
  const frontendPath = "../frontend";
36
- const result = (0, child_process_1.spawnSync)("node", [cliDistPath, "scan", frontendPath], {
36
+ const result = (0, child_process_1.spawnSync)("node", [
37
+ cliDistPath,
38
+ "scan",
39
+ frontendPath,
40
+ "--no-ai-inference",
41
+ "--skip-auto-upload",
42
+ ], {
37
43
  encoding: "utf-8",
38
44
  cwd: cliCwd,
39
45
  });
@@ -48,7 +54,15 @@ describe("CLI", () => {
48
54
  const cliCwd = path_1.default.join(__dirname, "../../");
49
55
  const frontendPath = "../frontend";
50
56
  const outputFile = "scan-frontend-summary.json";
51
- const result = (0, child_process_1.spawnSync)("node", [cliDistPath, "scan", frontendPath, "--output", outputFile], {
57
+ const result = (0, child_process_1.spawnSync)("node", [
58
+ cliDistPath,
59
+ "scan",
60
+ frontendPath,
61
+ "--output",
62
+ outputFile,
63
+ "--no-ai-inference",
64
+ "--skip-auto-upload",
65
+ ], {
52
66
  encoding: "utf-8",
53
67
  cwd: cliCwd,
54
68
  });