thumbgate 1.30.0 → 1.34.0

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 (92) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/mcp/server-card.json +1 -1
  3. package/README.md +54 -16
  4. package/adapters/claude/.mcp.json +2 -2
  5. package/adapters/forge/forge.yaml +3 -3
  6. package/adapters/mcp/server-stdio.js +105 -10
  7. package/adapters/opencode/opencode.json +1 -1
  8. package/bench/observability-eval-suite.json +2 -2
  9. package/bin/cli.js +168 -31
  10. package/config/evals/generation-quality-golden.json +95 -0
  11. package/config/evals/rag-answer-quality-golden.json +91 -0
  12. package/config/evals/retrieval-hybrid-ablation.json +66 -0
  13. package/config/evals/retrieval-ranking-golden.json +522 -0
  14. package/config/gates/claim-verifiers.example.json +42 -0
  15. package/config/gates/claim-verifiers.json +25 -0
  16. package/config/gates/default.json +217 -50
  17. package/config/mcp-allowlists.json +233 -206
  18. package/config/model-tiers.json +7 -2
  19. package/glama.json +6 -0
  20. package/hooks/hooks.json +1 -1
  21. package/package.json +69 -12
  22. package/public/assets/diagrams/before-after.svg +17 -16
  23. package/public/assets/diagrams/hero-thumbs.svg +68 -0
  24. package/public/assets/diagrams/loop.svg +19 -13
  25. package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
  26. package/public/compare.html +1 -0
  27. package/public/dashboard.html +126 -28
  28. package/public/evaluations.html +1 -1
  29. package/public/index.html +142 -13
  30. package/public/numbers.html +3 -2
  31. package/public/pricing.html +143 -30
  32. package/scripts/a-plus-evidence-scorecard.js +303 -0
  33. package/scripts/agent-readiness.js +110 -0
  34. package/scripts/async-eval-observability.js +36 -11
  35. package/scripts/audit-trail.js +37 -1
  36. package/scripts/auto-promote-gates.js +149 -34
  37. package/scripts/auto-wire-hooks.js +20 -8
  38. package/scripts/cli-schema.js +14 -0
  39. package/scripts/colbert-style-maxsim.js +236 -0
  40. package/scripts/cross-encoder-reranker.js +356 -126
  41. package/scripts/dashboard-chat.js +350 -17
  42. package/scripts/document-intake.js +283 -7
  43. package/scripts/eval-quality-suite.js +204 -0
  44. package/scripts/feedback-loop.js +115 -7
  45. package/scripts/feedback-paths.js +32 -13
  46. package/scripts/feedback-quality.js +53 -0
  47. package/scripts/feedback-schema.js +3 -0
  48. package/scripts/file-ledger-lock.js +130 -0
  49. package/scripts/filesystem-search.js +17 -7
  50. package/scripts/financial-control-plane.js +1514 -0
  51. package/scripts/gates-engine.js +202 -7
  52. package/scripts/gemini-embedding-policy.js +1 -0
  53. package/scripts/harness-tool-names.js +70 -0
  54. package/scripts/hook-runtime.js +15 -3
  55. package/scripts/hook-stop-anti-claim.js +63 -3
  56. package/scripts/human-escalation.js +353 -41
  57. package/scripts/lesson-db.js +16 -5
  58. package/scripts/lesson-embedding-index.js +67 -20
  59. package/scripts/lesson-embedding-maintenance.js +177 -0
  60. package/scripts/lesson-reranker.js +55 -9
  61. package/scripts/lesson-retrieval.js +305 -29
  62. package/scripts/lesson-search.js +22 -8
  63. package/scripts/llm-client.js +304 -15
  64. package/scripts/model-tier-router.js +593 -0
  65. package/scripts/pragmatic-hybrid-search.js +379 -0
  66. package/scripts/provider-action-normalizer.js +11 -4
  67. package/scripts/rag-document-pipeline.js +461 -0
  68. package/scripts/rag-structured-output.js +441 -0
  69. package/scripts/ragas-style-metrics.js +351 -0
  70. package/scripts/request-envelope.js +178 -0
  71. package/scripts/rerank-pipeline.js +370 -0
  72. package/scripts/rerank-quality-eval.js +155 -0
  73. package/scripts/retrieval-hybrid-ablation.js +120 -0
  74. package/scripts/retrieval-quality-tier.js +118 -0
  75. package/scripts/secret-scanner.js +395 -4
  76. package/scripts/self-distill-agent.js +7 -1
  77. package/scripts/self-healing-check.js +25 -0
  78. package/scripts/skill-packs.js +183 -0
  79. package/scripts/slow-loop.js +72 -0
  80. package/scripts/statusline-links.js +1 -1
  81. package/scripts/statusline.sh +8 -1
  82. package/scripts/telemetry-analytics.js +13 -1
  83. package/scripts/thumbgate-search.js +98 -6
  84. package/scripts/tier-budget-guard.js +186 -0
  85. package/scripts/tool-registry.js +141 -5
  86. package/scripts/universal-claim-evaluator.js +767 -0
  87. package/scripts/vector-store.js +154 -17
  88. package/scripts/verify-marketing-pages-deployed.js +85 -3
  89. package/scripts/workflow-sentinel.js +77 -11
  90. package/server.json +44 -0
  91. package/smithery.yaml +17 -0
  92. package/src/api/server.js +196 -13
package/server.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.IgorGanapolsky/thumbgate",
4
+ "description": "ThumbGate pre-action gates for AI coding agents. Captures thumbs-up/down feedback as lessons, promotes repeated failures into prevention rules, and evaluates tool calls over stdio MCP before they run.",
5
+ "title": "ThumbGate",
6
+ "websiteUrl": "https://thumbgate.ai",
7
+ "repository": {
8
+ "source": "github",
9
+ "url": "https://github.com/IgorGanapolsky/ThumbGate"
10
+ },
11
+ "version": "1.34.0",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "registryBaseUrl": "https://registry.npmjs.org",
16
+ "identifier": "thumbgate",
17
+ "version": "1.34.0",
18
+ "runtimeHint": "npx",
19
+ "runtimeArguments": [
20
+ {
21
+ "type": "positional",
22
+ "value": "-y"
23
+ }
24
+ ],
25
+ "packageArguments": [
26
+ {
27
+ "type": "positional",
28
+ "value": "serve"
29
+ }
30
+ ],
31
+ "transport": {
32
+ "type": "stdio"
33
+ },
34
+ "environmentVariables": [
35
+ {
36
+ "name": "THUMBGATE_MCP_PROFILE",
37
+ "description": "Optional MCP tool profile override (essential | default | readonly | locked). Omit to keep the factory least-privilege essential profile; only set when expanded tools are intentional.",
38
+ "isRequired": false,
39
+ "format": "string"
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ }
package/smithery.yaml ADDED
@@ -0,0 +1,17 @@
1
+ startCommand:
2
+ type: "stdio"
3
+ configSchema:
4
+ type: "object"
5
+ properties:
6
+ mcpProfile:
7
+ type: "string"
8
+ description: "MCP profile to use (default, readonly, locked)"
9
+ default: "default"
10
+ required: []
11
+ commandFunction:
12
+ command: "npx"
13
+ args:
14
+ - "-y"
15
+ - "thumbgate"
16
+ - "serve"
17
+ description: "ThumbGate — pre-action gates for AI coding agents. Turns thumbs-down feedback into prevention rules and evaluates tool calls over stdio MCP (npx -y thumbgate serve) before they run. Not the HTTP API (npm start). Works with Claude Code, Cursor, Codex, Gemini CLI, Amp, OpenCode, and any MCP-compatible agent."
package/src/api/server.js CHANGED
@@ -210,7 +210,7 @@ const {
210
210
  getSettingsStatus,
211
211
  } = require('../../scripts/settings-hierarchy');
212
212
  const {
213
- searchThumbgate,
213
+ searchThumbgateAsync,
214
214
  } = require('../../scripts/thumbgate-search');
215
215
  const {
216
216
  appendTelemetryPing,
@@ -480,6 +480,25 @@ const TRACKED_LINK_TARGETS = Object.freeze({
480
480
  },
481
481
  allowCustomerEmail: true,
482
482
  },
483
+ // Human skip from Pro interstitial: same-origin hop so crawlers never see buy.stripe.com
484
+ // in the interstitial HTML (bot-guard contract). Bots are deflected to /checkout/pro.
485
+ 'pro-direct': {
486
+ fallbackHref: PRO_CHECKOUT_URL,
487
+ external: true,
488
+ botDeflectPath: '/checkout/pro',
489
+ ctaId: 'pro_checkout_direct_stripe',
490
+ ctaPlacement: 'checkout_interstitial',
491
+ eventType: 'pro_checkout_direct_stripe',
492
+ defaults: {
493
+ utm_source: 'website',
494
+ utm_medium: 'checkout_interstitial',
495
+ utm_campaign: 'pro_upgrade',
496
+ plan_id: 'pro',
497
+ billing_cycle: 'monthly',
498
+ },
499
+ allowCustomerEmail: true,
500
+ prefillStripeEmail: true,
501
+ },
483
502
  // 2026-06-02: Teams/Aiventyx deprecated. Redirect legacy links to Pro.
484
503
  teams: {
485
504
  path: '/go/pro?utm_source=legacy_teams&utm_medium=redirect',
@@ -837,6 +856,67 @@ function getRequestFeedbackPaths(req, parsed) {
837
856
  });
838
857
  }
839
858
 
859
+ const DATA_IDENTITY_CACHE = new Map();
860
+ const DATA_IDENTITY_CACHE_LIMIT = 2048;
861
+
862
+ function hashDataIdentity(prefix, value) {
863
+ const normalized = String(value || '');
864
+ const cacheKey = `${prefix}\0${normalized}`;
865
+ const cached = DATA_IDENTITY_CACHE.get(cacheKey);
866
+ if (cached) return cached;
867
+
868
+ // Billing identifiers can be email-like and API keys must never be made
869
+ // guessable through a fast digest. A memory-hard KDF gives us a stable,
870
+ // path-safe pseudonym without persisting the source identifier.
871
+ const derived = `${prefix}_${crypto.scryptSync(
872
+ normalized,
873
+ `thumbgate-${prefix}-data-identity-v1`,
874
+ 16,
875
+ { N: 16384, r: 8, p: 1 }
876
+ ).toString('hex').slice(0, 24)}`;
877
+
878
+ if (DATA_IDENTITY_CACHE.size >= DATA_IDENTITY_CACHE_LIMIT) {
879
+ DATA_IDENTITY_CACHE.clear();
880
+ }
881
+ DATA_IDENTITY_CACHE.set(cacheKey, derived);
882
+ return derived;
883
+ }
884
+
885
+ function buildHostedTenantIdentity(validation = {}) {
886
+ if (!validation || validation.valid !== true || !validation.customerId) return null;
887
+ const tenantSeed = validation.customerId;
888
+ const principalSeed = validation.installId || tenantSeed;
889
+ const tenantId = hashDataIdentity('tenant', tenantSeed);
890
+ return {
891
+ partition: tenantId,
892
+ accessContext: {
893
+ tenantId,
894
+ principalId: hashDataIdentity('principal', principalSeed),
895
+ isAdmin: false,
896
+ },
897
+ };
898
+ }
899
+
900
+ function resolveRequestDataIdentity(req, expectedApiKey) {
901
+ const token = extractApiKey(req);
902
+ if (!token || (expectedApiKey && safeKeyEqual(token, expectedApiKey))) {
903
+ return { partition: null, accessContext: null };
904
+ }
905
+ const validation = validateApiKey(token);
906
+ const identity = buildHostedTenantIdentity(validation);
907
+ if (validation.valid === true && !identity) {
908
+ return { partition: null, accessContext: null, invalidHostedIdentity: true };
909
+ }
910
+ return identity || { partition: null, accessContext: null };
911
+ }
912
+
913
+ function partitionFeedbackPaths(paths, dataIdentity) {
914
+ if (!dataIdentity?.partition) return paths;
915
+ return getFeedbackPaths({
916
+ feedbackDir: path.join(paths.FEEDBACK_DIR, 'tenants', dataIdentity.partition),
917
+ });
918
+ }
919
+
840
920
  function getSafeDataDir(req, parsed) {
841
921
  const { FEEDBACK_LOG_PATH } = getRequestFeedbackPaths(req, parsed);
842
922
  return path.resolve(path.dirname(FEEDBACK_LOG_PATH));
@@ -2501,8 +2581,9 @@ ${hiddenInputs}
2501
2581
  <p class="email-note">Required so the checkout has attributable buyer intent and your receipt can be recovered.</p>
2502
2582
  <button type="submit" class="primary">Pay $19/mo with Stripe →</button>
2503
2583
  </form>
2584
+ <a class="secondary" data-i="pro_checkout_direct_stripe" rel="nofollow" href="/go/pro-direct?utm_source=website&amp;utm_medium=checkout_interstitial&amp;utm_campaign=pro_upgrade&amp;utm_content=skip_form&amp;cta_id=pro_checkout_direct_stripe&amp;cta_placement=checkout_interstitial">Skip form — open Stripe checkout directly →</a>
2504
2585
  <a class="secondary" data-i="workflow_sprint_intake" href="/#workflow-sprint-intake">Not sure yet? Send the workflow first</a>
2505
- <p class="choice-note">Cancel anytime. 7-day refund, no questions. Diagnostics and sprints have their own pages.</p>
2586
+ <p class="choice-note">Cancel anytime. 7-day refund, no questions. Diagnostics and sprints have their own pages. Email path keeps receipts attributable; skip uses a same-origin hop that bot-deflects crawlers.</p>
2506
2587
  <div class="objection-box" aria-label="Checkout feedback">
2507
2588
  <strong>Not buying today?</strong>
2508
2589
  <p class="objection-note">Tap one reason so we know what to fix. This does not sign you up.</p>
@@ -2825,6 +2906,37 @@ function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadReques
2825
2906
 
2826
2907
  const { FEEDBACK_DIR } = getFeedbackPaths();
2827
2908
  const journeyState = resolveJourneyState(req, parsed);
2909
+ const botClassification = classifyRequester(req.headers);
2910
+ if (target.botDeflectPath && botClassification.isBot) {
2911
+ const deflectUrl = new URL(target.botDeflectPath, hostedConfig.appOrigin);
2912
+ appendTrackedLinkQueryParams(deflectUrl, parsed, target);
2913
+ if (!isHeadRequest) {
2914
+ appendBestEffortTelemetry(
2915
+ FEEDBACK_DIR,
2916
+ {
2917
+ eventType: 'checkout_bot_deflected',
2918
+ clientType: 'web',
2919
+ page: `/go/${target.slug}`,
2920
+ reason: botClassification.reason || 'bot',
2921
+ destinationSlug: target.slug,
2922
+ acquisitionId: journeyState.acquisitionId,
2923
+ visitorId: journeyState.visitorId,
2924
+ sessionId: journeyState.sessionId,
2925
+ },
2926
+ req.headers,
2927
+ `tracked_link_bot_deflect:${target.slug}`
2928
+ );
2929
+ }
2930
+ res.writeHead(302, {
2931
+ ...(journeyState.setCookieHeaders.length ? { 'Set-Cookie': journeyState.setCookieHeaders } : {}),
2932
+ 'Cache-Control': 'no-store',
2933
+ 'X-Robots-Tag': 'noindex,nofollow',
2934
+ 'X-ThumbGate-Link-Slug': target.slug,
2935
+ Location: deflectUrl.toString(),
2936
+ });
2937
+ res.end();
2938
+ return;
2939
+ }
2828
2940
  const destinationUrl = buildTrackedLinkDestination(target, hostedConfig, parsed);
2829
2941
  const attribution = buildTrackedLinkAttribution(target, parsed, req, journeyState, destinationUrl);
2830
2942
  if (target.external) {
@@ -5224,9 +5336,10 @@ function createApiServer() {
5224
5336
  sendJson(res, 403, { error: 'project selection is only available on localhost requests' });
5225
5337
  return;
5226
5338
  }
5227
- const requestFeedbackPaths = getRequestFeedbackPaths(req, parsed);
5228
- const requestFeedbackDir = requestFeedbackPaths.FEEDBACK_DIR;
5229
- const requestSafeDataDir = getSafeDataDir(req, parsed);
5339
+ let requestFeedbackPaths = getRequestFeedbackPaths(req, parsed);
5340
+ let requestFeedbackDir = requestFeedbackPaths.FEEDBACK_DIR;
5341
+ let requestSafeDataDir = getSafeDataDir(req, parsed);
5342
+ let requestDocumentAccessContext = null;
5230
5343
 
5231
5344
  // PostHog reverse proxy -- bypasses ad blockers.
5232
5345
  // Only allow known PostHog API paths to prevent SSRF (CodeQL js/request-forgery).
@@ -8645,6 +8758,25 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
8645
8758
  return;
8646
8759
  }
8647
8760
 
8761
+ // Provisioned hosted keys are authenticated identities, not merely valid
8762
+ // booleans. Bind every private data path to a hash of the billing customer
8763
+ // before any endpoint reads or writes feedback, documents, traces, or jobs.
8764
+ // Static admin/local operation retains the existing single-tenant path.
8765
+ const requestDataIdentity = resolveRequestDataIdentity(req, expectedApiKey);
8766
+ if (requestDataIdentity.invalidHostedIdentity) {
8767
+ sendProblem(res, {
8768
+ type: PROBLEM_TYPES.UNAUTHORIZED,
8769
+ title: 'Unauthorized',
8770
+ status: 403,
8771
+ detail: 'The hosted API key is not bound to a customer identity.',
8772
+ });
8773
+ return;
8774
+ }
8775
+ requestFeedbackPaths = partitionFeedbackPaths(requestFeedbackPaths, requestDataIdentity);
8776
+ requestFeedbackDir = requestFeedbackPaths.FEEDBACK_DIR;
8777
+ requestSafeDataDir = path.resolve(requestFeedbackDir);
8778
+ requestDocumentAccessContext = requestDataIdentity.accessContext;
8779
+
8648
8780
  // Usage metering — record request for billing keys (not static THUMBGATE_API_KEY)
8649
8781
  const _token = extractBearerToken(req);
8650
8782
  if (_token && _token !== expectedApiKey) {
@@ -8665,6 +8797,33 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
8665
8797
  stats.tier = 'Pro';
8666
8798
  }
8667
8799
 
8800
+ // Active gates for the dashboard hero cards. Connect only fetches
8801
+ // /v1/feedback/stats first; without these fields Active Gates stays
8802
+ // stuck on the HTML placeholder "—" until a later /v1/dashboard call
8803
+ // succeeds (and never updates if that call fails).
8804
+ try {
8805
+ const { computeGateStats } = require('../../scripts/dashboard');
8806
+ const gateStats = computeGateStats();
8807
+ stats.activeGateCount = Number(gateStats.totalGates) || 0;
8808
+ stats.gateStats = {
8809
+ totalGates: Number(gateStats.totalGates) || 0,
8810
+ manualCount: Number(gateStats.manualCount) || 0,
8811
+ autoCount: Number(gateStats.autoCount) || 0,
8812
+ blocked: Number(gateStats.blocked) || 0,
8813
+ warned: Number(gateStats.warned) || 0,
8814
+ };
8815
+ } catch (error) {
8816
+ debugApiFallback('gate stats unavailable for feedback/stats', error);
8817
+ stats.activeGateCount = 0;
8818
+ stats.gateStats = {
8819
+ totalGates: 0,
8820
+ manualCount: 0,
8821
+ autoCount: 0,
8822
+ blocked: 0,
8823
+ warned: 0,
8824
+ };
8825
+ }
8826
+
8668
8827
  const projectChatSettings = readProjectChatSettings(req, parsed);
8669
8828
 
8670
8829
  stats.geminiConfigured = Boolean(
@@ -9294,6 +9453,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9294
9453
  escalationId,
9295
9454
  }, {
9296
9455
  authenticatedActor,
9456
+ approvalSigningKey: humanReviewerConfig.key,
9297
9457
  feedbackDir: requestFeedbackDir,
9298
9458
  }));
9299
9459
  } catch (error) {
@@ -9364,13 +9524,19 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9364
9524
  const signal = parsed.searchParams.get('signal') || null;
9365
9525
  let results;
9366
9526
  try {
9367
- const requestFeedbackPaths = getRequestFeedbackPaths(req, parsed);
9368
- results = searchThumbgate({
9527
+ results = await searchThumbgateAsync({
9369
9528
  query,
9370
9529
  limit: Number.isFinite(limit) ? limit : 10,
9371
9530
  source,
9372
9531
  signal,
9373
- feedbackDir: requestFeedbackPaths.FEEDBACK_DIR,
9532
+ feedbackDir: requestFeedbackDir,
9533
+ metadataFilters: {
9534
+ tags: (parsed.searchParams.get('tags') || '').split(',').map((tag) => tag.trim()).filter(Boolean),
9535
+ sourceFormat: parsed.searchParams.get('sourceFormat') || undefined,
9536
+ sourceType: parsed.searchParams.get('sourceType') || undefined,
9537
+ },
9538
+ queryRewrite: parsed.searchParams.get('queryRewrite') !== 'false',
9539
+ accessContext: requestDocumentAccessContext,
9374
9540
  });
9375
9541
  } catch (err) {
9376
9542
  throw createHttpError(400, err.message || 'Invalid ThumbGate search request');
@@ -9388,13 +9554,15 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9388
9554
  const body = await parseJsonBody(req);
9389
9555
  let results;
9390
9556
  try {
9391
- const requestFeedbackPaths = getRequestFeedbackPaths(req, parsed);
9392
- results = searchThumbgate({
9557
+ results = await searchThumbgateAsync({
9393
9558
  query: body.query || body.q || '',
9394
9559
  limit: body.limit,
9395
9560
  source: body.source,
9396
9561
  signal: body.signal,
9397
- feedbackDir: requestFeedbackPaths.FEEDBACK_DIR,
9562
+ feedbackDir: requestFeedbackDir,
9563
+ metadataFilters: body.filters,
9564
+ queryRewrite: body.queryRewrite !== false,
9565
+ accessContext: requestDocumentAccessContext,
9398
9566
  });
9399
9567
  } catch (err) {
9400
9568
  throw createHttpError(400, err.message || 'Invalid ThumbGate search request');
@@ -9412,6 +9580,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9412
9580
  limit: Number.isFinite(limit) ? limit : 20,
9413
9581
  query,
9414
9582
  tag,
9583
+ accessContext: requestDocumentAccessContext,
9415
9584
  });
9416
9585
  sendJson(res, 200, results);
9417
9586
  return;
@@ -9425,6 +9594,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9425
9594
  }
9426
9595
  const document = readImportedDocument(documentId, {
9427
9596
  feedbackDir: requestFeedbackDir,
9597
+ accessContext: requestDocumentAccessContext,
9428
9598
  });
9429
9599
  if (!document) {
9430
9600
  throw createHttpError(404, `Imported document not found: ${escapeHtml(documentId)}`);
@@ -9557,6 +9727,11 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9557
9727
  tags: extractTags(body.tags),
9558
9728
  proposeGates: body.proposeGates !== false,
9559
9729
  feedbackDir: requestFeedbackDir,
9730
+ accessContext: requestDocumentAccessContext,
9731
+ visibility: body.visibility === 'private' ? 'private' : 'tenant',
9732
+ allowedPrincipals: Array.isArray(body.allowedPrincipals)
9733
+ ? body.allowedPrincipals.map((value) => String(value || '').trim()).filter(Boolean)
9734
+ : [],
9560
9735
  });
9561
9736
  sendJson(res, 201, {
9562
9737
  ok: true,
@@ -9607,14 +9782,19 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9607
9782
  fs.writeFileSync(paths.outputPath, result.jsonl);
9608
9783
  }
9609
9784
 
9610
- sendJson(res, 200, {
9785
+ const response = {
9611
9786
  pairs: result.pairs.length,
9787
+ pairCount: result.pairs.length,
9612
9788
  errors: result.errors.length,
9613
9789
  learnings: result.learnings.length,
9614
9790
  unpairedErrors: result.unpairedErrors.length,
9615
9791
  unpairedLearnings: result.unpairedLearnings.length,
9616
9792
  outputPath: paths.outputPath,
9617
- });
9793
+ };
9794
+ if (body.includePairs === true) {
9795
+ response.records = result.pairs;
9796
+ }
9797
+ sendJson(res, 200, response);
9618
9798
  return;
9619
9799
  }
9620
9800
 
@@ -10398,6 +10578,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
10398
10578
  params: body.params,
10399
10579
  mcp: body.mcp,
10400
10580
  mcpToolCall: body.mcpToolCall,
10581
+ financialControl: body.financialControl || body.financial_control,
10401
10582
  budget: body.budget,
10402
10583
  usage: body.usage,
10403
10584
  };
@@ -10640,6 +10821,8 @@ module.exports = {
10640
10821
  buildLossAnalyticsResponse,
10641
10822
  buildIntakeAlertRateLimitKey,
10642
10823
  sanitizeHtmlUnsafeJsonValue,
10824
+ buildHostedTenantIdentity,
10825
+ partitionFeedbackPaths,
10643
10826
  },
10644
10827
  };
10645
10828