failproofai 1.0.12-beta.2 → 1.0.12

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 (196) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  30. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +3 -3
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  37. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  41. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  51. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  54. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
  57. package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pwj2h2efe5f-._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00l2u6j86cb8y._.js → [root-of-the-server]__167mipqmjprq5._.js} +2 -2
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/ssr/_0bseae9u7g98r._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/_0mouhaflye1ro._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/{_1fw0_nzkae5ao._.js → _1d98fyfrcaqhy._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/_20hn4gn87nsjc._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr0vt64h._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1fv03uigni8s5.js → node_modules_html-to-image_es_index_0tqc05n832v7a.js} +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0janj0i6dqko7._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
  87. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  88. package/.next/standalone/.next/server/pages/404.html +1 -1
  89. package/.next/standalone/.next/server/pages/500.html +1 -1
  90. package/.next/standalone/.next/server/preview-props.json +3 -3
  91. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  93. package/.next/standalone/.next/static/chunks/{1c_jf6g5wnslk.js → 0el9n3h9_zp5m.js} +1 -1
  94. package/.next/standalone/.next/static/chunks/0k7g_8x3o6gd-.js +1 -0
  95. package/.next/standalone/.next/static/chunks/{2o6d37j425uci.js → 0umzct31duk5g.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{42xptz88mtq3x.js → 0wobqgosirs8p.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/12rug3jilwqzs.js +1 -0
  98. package/.next/standalone/.next/static/chunks/{0ay__-d2h1xb5.js → 1kduj74-715jh.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{40oworn_-x12p.js → 2701amhrbsbot.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/{0tbm3vjm62h-2.js → 27cavoc8paelg.js} +1 -1
  101. package/.next/standalone/.next/static/chunks/{1my3-5p47t7r-.js → 2ve23a7keupjw.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/{21hckpmsc4g70.js → 3bv6xaoitjpnh.js} +2 -2
  103. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +19 -0
  104. package/.next/standalone/fp-cloud-cli/README.md +120 -0
  105. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +15 -1
  106. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +198 -7
  107. package/.next/standalone/fp-cloud-cli/fp_cli/cloud_output.py +523 -0
  108. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_cloud.py +45 -0
  109. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +3 -1
  110. package/.next/standalone/fp-cloud-cli/fp_cli/commands/account_cmds.py +171 -0
  111. package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +80 -7
  112. package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +23 -0
  113. package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +2 -0
  114. package/.next/standalone/fp-cloud-cli/fp_cli/commands/contracts_cmds.py +67 -0
  115. package/.next/standalone/fp-cloud-cli/fp_cli/commands/dashboards_cmds.py +159 -0
  116. package/.next/standalone/fp-cloud-cli/fp_cli/commands/evaluators_cmds.py +380 -0
  117. package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +14 -1
  118. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +82 -6
  119. package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +4 -1
  120. package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +17 -0
  121. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +143 -1
  122. package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +70 -0
  123. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sentiment_cmds.py +49 -0
  124. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +95 -1
  125. package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +2 -0
  126. package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +21 -1
  127. package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +14 -0
  128. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +3 -0
  129. package/.next/standalone/fp-cloud-cli/fp_cli/evaluator_spec.py +266 -0
  130. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +187 -81
  131. package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +21 -24
  132. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +124 -23
  133. package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +2 -1
  134. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +168 -16
  135. package/.next/standalone/fp-cloud-cli/tests/fake_cloud_smoke.py +312 -0
  136. package/.next/standalone/fp-cloud-cli/tests/test_cloud_account.py +125 -0
  137. package/.next/standalone/fp-cloud-cli/tests/test_cloud_agent.py +253 -0
  138. package/.next/standalone/fp-cloud-cli/tests/test_cloud_human_views.py +287 -0
  139. package/.next/standalone/fp-cloud-cli/tests/test_cloud_observability.py +116 -0
  140. package/.next/standalone/fp-cloud-cli/tests/test_cloud_transport.py +141 -0
  141. package/.next/standalone/fp-cloud-cli/tests/test_commands.py +3 -3
  142. package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +3 -3
  143. package/.next/standalone/fp-cloud-cli/tests/test_evaluator_authoring.py +449 -0
  144. package/.next/standalone/fp-cloud-cli/tests/test_facets.py +1 -1
  145. package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +15 -0
  146. package/.next/standalone/fp-cloud-cli/tests/test_operator.py +1 -1
  147. package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +1 -1
  148. package/.next/standalone/fp-cloud-cli/tests/test_output.py +37 -37
  149. package/.next/standalone/fp-cloud-cli/tests/test_round4_behaviour.py +144 -0
  150. package/.next/standalone/fp-cloud-cli/tests/test_screen_language.py +80 -0
  151. package/.next/standalone/fp-cloud-cli/tests/test_session_reevaluate.py +33 -0
  152. package/.next/standalone/fp-cloud-cli/tests/test_staging_parity.py +198 -0
  153. package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +1 -1
  154. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  155. package/.next/standalone/package.json +9 -9
  156. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +68 -176
  157. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +2 -2
  158. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -4
  159. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +1 -1
  160. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +7 -7
  161. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +1 -1
  162. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +7 -7
  163. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +1 -1
  164. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +98 -115
  165. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +3 -3
  166. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +4 -4
  167. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +1 -1
  168. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -4
  169. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +1 -1
  170. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +149 -171
  171. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +2 -2
  172. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +4 -4
  173. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +1 -1
  174. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +25 -25
  175. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +1 -1
  176. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +7 -7
  177. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +1 -1
  178. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +10 -10
  179. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +1 -1
  180. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +7 -7
  181. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +1 -1
  182. package/.next/standalone/server.js +1 -1
  183. package/dist/cli.mjs +85 -37
  184. package/dist/worker.mjs +73 -28
  185. package/hermes-plugin/plugin.yaml +1 -1
  186. package/package.json +9 -9
  187. package/src/hooks/agent-skill.ts +3 -0
  188. package/src/hooks/fp-home.ts +1 -1
  189. package/src/hooks/handler.ts +22 -3
  190. package/src/hooks/semantic/intent.ts +127 -24
  191. package/src/hooks/semantic/jev-review.ts +21 -2
  192. package/.next/standalone/.next/static/chunks/20py8k1gd_i1m.js +0 -1
  193. package/.next/standalone/.next/static/chunks/2hmex93ppj1ke.js +0 -1
  194. /package/.next/standalone/.next/static/{LHCtFxVG0mdvd4lsXAOFC → D7LY1YSrw96ZxKSpxxrFw}/_buildManifest.js +0 -0
  195. /package/.next/standalone/.next/static/{LHCtFxVG0mdvd4lsXAOFC → D7LY1YSrw96ZxKSpxxrFw}/_clientMiddlewareManifest.js +0 -0
  196. /package/.next/standalone/.next/static/{LHCtFxVG0mdvd4lsXAOFC → D7LY1YSrw96ZxKSpxxrFw}/_ssgManifest.js +0 -0
@@ -22,7 +22,7 @@ from .. import _click_compat as click # the Click Typer is running; see _click_
22
22
  from ..enforcement import RefError, RefUsageError, parse_semantic, read_source, resolve_kind
23
23
  from ..policy_check import SyntaxResult, check_syntax, run_policy
24
24
  from ..errors import ApiError, NotFoundError
25
- from . import _write
25
+ from . import _cloud, _write
26
26
 
27
27
  #: Every command here is session-only. These endpoints are ROOT-ONLY on the
28
28
  #: server — deliberately absent from `/v1`, because `/v1` is internet-facing and
@@ -565,4 +565,146 @@ def register(app: typer.Typer) -> None:
565
565
  policies_app.command("delete", epilog=GLOBALS_EPILOG)(policies_delete)
566
566
  policies_app.command("test", epilog=GLOBALS_EPILOG)(policies_test)
567
567
  policies_app.command("compose", epilog=GLOBALS_EPILOG)(policies_compose)
568
+ policies_app.command("author", epilog=GLOBALS_EPILOG)(policies_author)
569
+ policies_app.command("backtest", epilog=GLOBALS_EPILOG)(policies_backtest)
570
+ policies_app.command("candidacy", epilog=GLOBALS_EPILOG)(policies_candidacy)
568
571
  app.add_typer(policies_app, name="policies")
572
+
573
+
574
+ def policies_author(
575
+ ctx: typer.Context,
576
+ intent: str = typer.Argument(...),
577
+ kind: Optional[str] = typer.Option(
578
+ None, "--kind",
579
+ help="regex | jev | both. Default: the assistant writes a regex (JavaScript) policy.",
580
+ ),
581
+ ) -> None:
582
+ """Author a vetted policy from an intent (session; does not publish).
583
+
584
+ `--kind jev` asks for Jev checks only, `--kind both` for JavaScript plus the
585
+ Jev checks that may clear it. Publish the result with `fp policies publish`.
586
+ """
587
+ _cloud.session_only(ctx.obj, "policies author")
588
+ if kind is not None and kind not in ("regex", "jev", "both"):
589
+ raise click.UsageError("--kind must be regex, jev or both")
590
+ body = {"issue": {"title": intent, "source": "manual"}}
591
+ if kind in ("jev", "both"):
592
+ # The dashboard proxy treats anything else as regex, exactly as before kinds.
593
+ body["kind"] = kind
594
+ events = list(api.cloud_stream(require_auth(ctx.obj), "/api/agent/policy-author", body))
595
+ for event in events:
596
+ if event.get("type") == "error":
597
+ raise ApiError(str(event.get("message") or event.get("error") or event.get("reason") or "Policy author failed"))
598
+ result = next((e.get("result", e) for e in reversed(events) if e.get("type") in ("result", "done")), None)
599
+ if result is None:
600
+ raise ApiError("Policy author returned no result; nothing was published")
601
+ _cloud.emit(ctx.obj, result, "Policy draft")
602
+
603
+
604
+ def policies_backtest(
605
+ ctx: typer.Context,
606
+ file_or_id: Optional[str] = typer.Argument(None, metavar="FILE_OR_ID"),
607
+ kind: Optional[str] = typer.Option(
608
+ None, "--kind",
609
+ help="regex | jev | both. Default: a published policy's own kind; for a file, "
610
+ "regex, or jev/both when --semantic is given.",
611
+ ),
612
+ semantic_path: Optional[str] = typer.Option(
613
+ None, "--semantic", help="Jev checks JSON (the same file `policies publish --semantic` takes).",
614
+ ),
615
+ hours: Optional[float] = typer.Option(None, "--hours", help="How far back to replay (default 168 for jev/both)."),
616
+ agent_id: Optional[str] = typer.Option(None, "--agent-id", help="Replay only this agent's calls."),
617
+ env: Optional[str] = typer.Option(None, "--env", help="Replay only this environment's calls."),
618
+ run: bool = typer.Option(
619
+ False, "--run",
620
+ help="jev/both: actually ask Jev (spends the org's Jev allowance). Default is a free estimate.",
621
+ ),
622
+ sample: Optional[int] = typer.Option(None, "--sample", help="jev/both with --run: calls to ask Jev about (max 50)."),
623
+ ) -> None:
624
+ """Replay local or published policy source against stored events (session).
625
+
626
+ A regex policy (a .mjs file or a published regex id) is replayed module-only,
627
+ as before. A jev/both policy — a published one, or `--semantic checks.json`
628
+ with or without a source file — runs the every-kind backtest: an estimate
629
+ by default, `--run` to spend Jev allowance on up to `--sample` calls.
630
+ Needs policies:write and events:read.
631
+ """
632
+ from pathlib import Path
633
+ _cloud.session_only(ctx.obj, "policies backtest")
634
+ if kind is not None and kind not in ("regex", "jev", "both"):
635
+ raise click.UsageError("--kind must be regex, jev or both")
636
+ if file_or_id is None and semantic_path is None:
637
+ raise click.UsageError("give a policy file or published id, or --semantic checks.json")
638
+ if hours is not None and not hours > 0:
639
+ raise click.UsageError("--hours must be a positive number")
640
+ if sample is not None and not 1 <= sample <= 50:
641
+ raise click.UsageError("--sample must be between 1 and 50")
642
+ if file_or_id == "-" and semantic_path == "-":
643
+ raise click.UsageError("only one of the source and --semantic can come from stdin")
644
+ cctx = require_auth(ctx.obj)
645
+ source: Optional[str] = None
646
+ semantic = None
647
+ published_kind = None
648
+ if file_or_id is not None:
649
+ if file_or_id == "-" or file_or_id.startswith("@") or Path(file_or_id).is_file():
650
+ source = _write.read_text_arg(file_or_id.removeprefix("@"))
651
+ else:
652
+ data = api.cloud_request(cctx, "GET", "/api/enforcement/policies")
653
+ versions = [p for p in _cloud.rows(data, "policies") if p.get("id") == file_or_id]
654
+ if not versions:
655
+ raise NotFoundError(f"no policy named {file_or_id}")
656
+ newest = max(versions, key=lambda p: p.get("version", 0))
657
+ published_kind = newest.get("kind") or "regex"
658
+ source = newest.get("source") or None
659
+ if semantic_path is None and isinstance(newest.get("semantic"), list):
660
+ semantic = newest["semantic"]
661
+ if semantic_path is not None:
662
+ try:
663
+ semantic = parse_semantic(read_source(semantic_path, isatty=False))
664
+ except RefUsageError as exc:
665
+ raise click.UsageError(str(exc))
666
+ except RefError as exc:
667
+ raise ApiError(str(exc))
668
+ resolved = kind or published_kind or ("regex" if not semantic else ("both" if source else "jev"))
669
+ if resolved == "regex":
670
+ if semantic_path is not None:
671
+ raise click.UsageError("--semantic needs --kind jev or both")
672
+ if not isinstance(source, str) or not source.strip():
673
+ raise click.UsageError("Policy source is empty")
674
+ body = {"source": source}
675
+ if agent_id:
676
+ body["agentId"] = agent_id
677
+ if env:
678
+ body["environment"] = env
679
+ if hours is not None:
680
+ body["hours"] = hours
681
+ _cloud.emit(ctx.obj, api.cloud_request(cctx, "POST", "/api/agent/policy-backtest",
682
+ body=body), "Policy backtest")
683
+ return
684
+ if resolved == "both" and not (source or "").strip():
685
+ raise click.UsageError("a both backtest needs the JavaScript source too")
686
+ if not semantic:
687
+ raise click.UsageError(f"a {resolved} backtest needs Jev checks: pass --semantic checks.json")
688
+ body = {"kind": resolved, "semantic": semantic, "hours": hours if hours is not None else 168,
689
+ "mode": "run" if run else "estimate"}
690
+ if resolved == "both":
691
+ body["source"] = source
692
+ if agent_id:
693
+ body["agentId"] = agent_id
694
+ if env:
695
+ body["environment"] = env
696
+ if sample is not None:
697
+ body["sample"] = sample
698
+ if run and not ctx.obj.json:
699
+ output.warn("--run asks Jev about stored calls and spends the org's Jev allowance.")
700
+ _cloud.emit(ctx.obj, api.cloud_request(cctx, "POST", "/api/agent/policy-backtest-all",
701
+ body=body), "Policy backtest")
702
+
703
+
704
+ def policies_candidacy(ctx: typer.Context, issue: str = typer.Option(..., "--issue")) -> None:
705
+ """Ask whether a policy could address an issue (session; read-only)."""
706
+ _cloud.session_only(ctx.obj, "policies candidacy")
707
+ cctx = require_auth(ctx.obj)
708
+ row = api.cloud_request(cctx, "GET", f"/api/issues/{issue}")
709
+ _cloud.emit(ctx.obj, api.cloud_request(cctx, "POST", "/api/agent/policy-candidacy",
710
+ body={"issue": row}), "Policy candidacy")
@@ -17,6 +17,7 @@ from .._context import GLOBALS_EPILOG, AppState, require_auth, resolve_fields, v
17
17
  from ..errors import NotFoundError
18
18
  from ..models import SavedQuery
19
19
  from . import _write
20
+ from . import _cloud
20
21
 
21
22
 
22
23
  def _humanize_list(items: List[str]) -> str:
@@ -375,4 +376,73 @@ def register(app: typer.Typer) -> None:
375
376
  query_app.command("delete", epilog=GLOBALS_EPILOG)(query_delete)
376
377
  query_app.command("run", epilog=GLOBALS_EPILOG)(query_run)
377
378
  query_app.command("schema", epilog=GLOBALS_EPILOG)(query_schema)
379
+ query_app.command("compose", epilog=GLOBALS_EPILOG)(query_compose)
380
+ query_app.command("export", epilog=GLOBALS_EPILOG)(query_export)
378
381
  app.add_typer(query_app, name="query")
382
+
383
+
384
+ def query_compose(ctx: typer.Context, question: str = typer.Argument(...),
385
+ run: bool = typer.Option(False, "--run")) -> None:
386
+ """AI-compose SQL (session only); optionally run it."""
387
+ _cloud.session_only(ctx.obj, "query compose")
388
+ cctx = require_auth(ctx.obj)
389
+ draft = None
390
+ for event in api.cloud_stream(cctx, "/api/agent/compose-sql", {"intent": question}):
391
+ if event.get("type") == "error":
392
+ from ..errors import ApiError
393
+ raise ApiError(str(event.get("error") or event.get("reason") or "SQL composer failed"))
394
+ if event.get("type") in ("result", "done"):
395
+ draft = event.get("result", event)
396
+ if not isinstance(draft, dict):
397
+ raise click.UsageError("Cloud did not return composed SQL")
398
+ sql = draft.get("sql") or draft.get("sql_text")
399
+ if not isinstance(sql, str) or not sql.strip():
400
+ raise click.UsageError("Cloud did not return composed SQL")
401
+ if run:
402
+ data = api.cloud_request(cctx, "POST", "/api/queries/run", body={"sql": sql, "params": []})
403
+ _cloud.emit(ctx.obj, {"sql": sql, "result": data}, "Composed query")
404
+ elif ctx.obj.json:
405
+ output.emit_json(draft)
406
+ else:
407
+ _cloud.emit(ctx.obj, {"sql": sql}, "Composed query")
408
+
409
+
410
+ def query_export(ctx: typer.Context, id: Optional[str] = typer.Argument(None),
411
+ sql: Optional[str] = typer.Option(None, "--sql"),
412
+ format: str = typer.Option("csv", "--format"),
413
+ out: Optional[str] = typer.Option(None, "--out")) -> None:
414
+ """Export a saved or ad-hoc query as CSV/JSON (all returned rows).
415
+
416
+ --json wraps CSV in {format,content} unless --out writes the file.
417
+ """
418
+ import csv
419
+ import io
420
+ import json
421
+ if (id is None) == (sql is None):
422
+ raise click.UsageError("Pass a saved query id or --sql, exactly one")
423
+ if format not in ("csv", "json"):
424
+ raise click.UsageError("--format must be csv or json")
425
+ data = api.cloud_request(require_auth(ctx.obj), "POST", "/api/queries/run",
426
+ body={"query_id": id, "sql": _read_sql(sql) if sql is not None else None, "params": []})
427
+ if format == "json":
428
+ content = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
429
+ else:
430
+ buf = io.StringIO()
431
+ writer = csv.writer(buf)
432
+ writer.writerow([c.get("name", "") if isinstance(c, dict) else c for c in data.get("columns", [])])
433
+ # Spreadsheet-safe export of untrusted event cells.
434
+ writer.writerows([
435
+ ["'" + v if isinstance(v, str) and v.startswith(("=", "+", "-", "@", "\t", "\r")) else v for v in row]
436
+ for row in data.get("rows", [])
437
+ ])
438
+ content = buf.getvalue()
439
+ if out:
440
+ _cloud.write_file(out, content)
441
+ _cloud.emit(ctx.obj, {"out": out, "format": format}, "Query exported")
442
+ elif ctx.obj.json:
443
+ output.emit_json(data if format == "json" else {"format": format, "content": content})
444
+ elif format == "json":
445
+ from .. import cloud_output
446
+ cloud_output.query_result(data, "Query export")
447
+ else:
448
+ print(content, end="")
@@ -0,0 +1,49 @@
1
+ """Conversation sentiment summary, messages and sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from .. import client as api
10
+ from .._context import GLOBALS_EPILOG, require_auth, resolve_dates, validate_limit
11
+ from . import _cloud
12
+
13
+
14
+ def read(ctx, suffix, since=None, limit=None, session_id=None):
15
+ validate_limit(limit)
16
+ frm, to = resolve_dates(since, None, None)
17
+ data = api.cloud_request(require_auth(ctx.obj), "GET", f"/api/events/sentiment{suffix}",
18
+ params={"ts_from": frm, "ts_to": to, "limit": limit, "session_id": session_id})
19
+ _cloud.emit(ctx.obj, data, "Sentiment", view=suffix.strip("/") or "summary")
20
+
21
+
22
+ def summary(ctx: typer.Context, since: Optional[str] = typer.Option(None, "--since")) -> None:
23
+ """Summarize conversation sentiment."""
24
+ read(ctx, "", since)
25
+
26
+
27
+ def messages(ctx: typer.Context, since: Optional[str] = typer.Option(None, "--since"),
28
+ limit: int = typer.Option(50, "--limit"),
29
+ session_id: Optional[str] = typer.Option(None, "--session-id")) -> None:
30
+ """List sentiment-scored messages."""
31
+ read(ctx, "/messages", since, limit, session_id)
32
+
33
+
34
+ def sessions(ctx: typer.Context, since: Optional[str] = typer.Option(None, "--since"),
35
+ limit: int = typer.Option(50, "--limit")) -> None:
36
+ """List sentiment-scored sessions."""
37
+ read(ctx, "/sessions", since, limit)
38
+
39
+
40
+ def session(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
41
+ """Show one session's sentiment."""
42
+ read(ctx, f"/sessions/{id}")
43
+
44
+
45
+ def register(app: typer.Typer) -> None:
46
+ group = typer.Typer(no_args_is_help=True, help="Inspect conversation sentiment.", context_settings={"help_option_names": ["-h", "--help"]})
47
+ for name, fn in (("summary", summary), ("messages", messages), ("sessions", sessions), ("session", session)):
48
+ group.command(name, epilog=GLOBALS_EPILOG)(fn)
49
+ app.add_typer(group, name="sentiment")
@@ -18,7 +18,9 @@ from .._context import (
18
18
  validate_choice,
19
19
  validate_limit,
20
20
  )
21
+ from .. import _click_compat as click
21
22
  from ..models import Session
23
+ from . import _cloud, _write
22
24
 
23
25
 
24
26
  def sessions(
@@ -70,6 +72,8 @@ def sessions(
70
72
  * `fp sessions --agents` — expand each multi-agent session to its full agent roster
71
73
  * `fp --json sessions --all --fields session_id,agents | jq '.sessions'` — every session's full agent roster as JSON
72
74
  """
75
+ if ctx.invoked_subcommand:
76
+ return
73
77
  state: AppState = ctx.obj
74
78
  cctx = require_auth(state)
75
79
  validate_limit(limit)
@@ -147,5 +151,95 @@ def sessions(
147
151
  output.recheck_filters_hint(active_filters)
148
152
 
149
153
 
154
+ def re_evaluate(ctx: typer.Context, session_id: str = typer.Argument(...)) -> None:
155
+ """Re-run evaluators on one session. Needs evaluations:trigger."""
156
+ _write.confirm(ctx.obj, f"Re-evaluate session {session_id}", assume_yes=False)
157
+ _cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "POST",
158
+ f"/api/sessions/{session_id}/re-evaluate"), "Session re-evaluation", body=session_id)
159
+
160
+
161
+ def import_sessions(
162
+ ctx: typer.Context,
163
+ files: List[str] = typer.Argument(..., metavar="FILE...", help="Event JSONL, Claude Code transcripts or Codex rollouts (.gz ok)."),
164
+ format: str = typer.Option("auto", "--format", help="auto | failproofai (FailproofAI event JSONL) | claude | codex."),
165
+ env: Optional[str] = typer.Option(None, "--env", help="Environment for events that name none (server default: upload)."),
166
+ dry_run: bool = typer.Option(False, "--dry-run", help="Convert and report; store nothing."),
167
+ ) -> None:
168
+ """Import sessions from files, as the dashboard's Import sessions dialog does.
169
+
170
+ Each file is gzipped and sent on its own. Sessions already in the org are skipped
171
+ whole; secrets are redacted the way the collector redacts them live. Claude Code
172
+ and Codex files take the session id from the file name, so pass the original files.
173
+ Needs events:add. With `--json`: `{"files": [{file, result}]}`.
174
+ """
175
+ import gzip
176
+ import time
177
+ from pathlib import Path
178
+ validate_choice(format, ("auto", "failproofai", "agenteye", "claude", "codex"), flag="--format")
179
+ # The server names FailproofAI's own event JSONL `agenteye`; `failproofai` is its public name.
180
+ wire_format = "agenteye" if format == "failproofai" else format
181
+ payloads = []
182
+ for name in files:
183
+ path = Path(name)
184
+ try:
185
+ raw = path.read_bytes()
186
+ except OSError as exc:
187
+ raise click.BadParameter(f"cannot read '{name}': {exc.strerror or exc}", param_hint="FILE")
188
+ rel = "/".join(path.resolve().parts[-2:])
189
+ payloads.append((name, rel, raw if raw[:2] == b"\x1f\x8b" else gzip.compress(raw)))
190
+ if not dry_run:
191
+ _write.confirm(ctx.obj, f"Import {len(files)} file{'s' if len(files) != 1 else ''}", assume_yes=False)
192
+ cctx = require_auth(ctx.obj)
193
+ # One cutoff for the whole run, so a later file of a session this run created
194
+ # is not skipped as "already present".
195
+ before = int(time.time() * 1000)
196
+ results = []
197
+ for name, rel, data in payloads:
198
+ result = api.import_events_file(cctx, data, {
199
+ "format": wire_format, "path": rel, "environment": env,
200
+ "dry_run": "true" if dry_run else None, "existing_before": before,
201
+ })
202
+ results.append({"file": name, "result": result})
203
+ if not dry_run:
204
+ _write.record_action("sessions_imported", resource="events", success=True)
205
+ _cloud.emit(ctx.obj, {"files": results}, "Session import")
206
+
207
+
208
+ def show(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
209
+ """Show a session overview and its event timeline."""
210
+ cctx = require_auth(ctx.obj)
211
+ overview = api.cloud_request(cctx, "GET", "/api/sessions", params={"session_id": id, "limit": 1})
212
+ items = _cloud.rows(overview, "sessions")
213
+ if not items:
214
+ from ..errors import NotFoundError
215
+ raise NotFoundError(f"no session {id}")
216
+ timeline = api.cloud_request(cctx, "GET", "/api/events/summary", params={"session_id": id, "limit": 200, "order": "asc"})
217
+ _cloud.emit(ctx.obj, {"overview": items[0], "timeline": timeline}, "Session")
218
+
219
+
220
+ def export(ctx: typer.Context, id: str = typer.Argument(...),
221
+ out: Optional[str] = typer.Option(None, "--out")) -> None:
222
+ """Export a full session as JSON."""
223
+ import json
224
+ data = api.cloud_request(require_auth(ctx.obj), "GET", f"/api/sessions/{id}/export")
225
+ if out:
226
+ _cloud.write_file(out, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
227
+ _cloud.emit(ctx.obj, {"out": out}, "Session exported")
228
+ else:
229
+ _cloud.emit(ctx.obj, data, "Session")
230
+
231
+
232
+ def history(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
233
+ """Show a session's evaluation history."""
234
+ _cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "GET", f"/api/sessions/{id}/evaluation-history"), "Evaluation history")
235
+
236
+
150
237
  def register(app: typer.Typer) -> None:
151
- app.command("sessions", epilog=GLOBALS_EPILOG)(sessions)
238
+ group = typer.Typer(invoke_without_command=True, rich_markup_mode="markdown", context_settings={"help_option_names": ["-h", "--help"]})
239
+ group.callback(epilog=GLOBALS_EPILOG)(sessions)
240
+ group.command("re-evaluate", epilog=GLOBALS_EPILOG)(re_evaluate)
241
+ group.command("show", epilog=GLOBALS_EPILOG)(show)
242
+ group.command("export", epilog=GLOBALS_EPILOG)(export)
243
+ group.command("history", epilog=GLOBALS_EPILOG)(history)
244
+ group.command("import", epilog=GLOBALS_EPILOG)(import_sessions)
245
+ app.add_typer(group, name="sessions")
@@ -147,4 +147,6 @@ def register(app: typer.Typer) -> None:
147
147
  settings_app.command("list", epilog=GLOBALS_EPILOG)(settings_list)
148
148
  settings_app.command("schema", epilog=GLOBALS_EPILOG)(settings_schema)
149
149
  settings_app.command("set", epilog=GLOBALS_EPILOG)(settings_set)
150
+ from .account_cmds import register_context_windows
151
+ register_context_windows(settings_app)
150
152
  app.add_typer(settings_app, name="settings")
@@ -23,6 +23,8 @@ def usage(ctx: typer.Context) -> None:
23
23
  * `fp usage`
24
24
  * `fp --json usage | jq '.usage.events_ingested'`
25
25
  """
26
+ if ctx.invoked_subcommand:
27
+ return
26
28
  state: AppState = ctx.obj
27
29
  data = api.get_usage(require_auth(state))
28
30
  if state.json:
@@ -32,4 +34,22 @@ def usage(ctx: typer.Context) -> None:
32
34
 
33
35
 
34
36
  def register(app: typer.Typer) -> None:
35
- app.command("usage", epilog=GLOBALS_EPILOG)(usage)
37
+ group = typer.Typer(invoke_without_command=True, rich_markup_mode="markdown", context_settings={"help_option_names": ["-h", "--help"]})
38
+ group.callback(epilog=GLOBALS_EPILOG)(usage)
39
+ group.command("windows", epilog=GLOBALS_EPILOG)(usage_windows)
40
+ app.add_typer(group, name="usage")
41
+
42
+
43
+ def usage_windows(ctx: typer.Context) -> None:
44
+ """List usage metering windows. API key only (the dashboard has no session route)."""
45
+ from .account_cmds import request
46
+ from ..client import AuthMode
47
+ from ..errors import KeyModeUnsupportedError
48
+ if ctx.obj.auth_mode is not AuthMode.API_KEY:
49
+ # GET /usage/windows is served on the versioned API only; a signed-in
50
+ # session would reach a dashboard 404 and print a misleading message.
51
+ raise KeyModeUnsupportedError(
52
+ "`fp usage windows` needs an API key — the dashboard has no session route for it.",
53
+ hint="pass --api-key (or FP_API_KEY) with a usage:read key",
54
+ )
55
+ request(ctx, "GET", "/api/usage/windows")
@@ -401,4 +401,18 @@ def register(app: typer.Typer) -> None:
401
401
  users_app.command("update", epilog=GLOBALS_EPILOG)(users_update)
402
402
  users_app.command("disable", epilog=GLOBALS_EPILOG)(users_disable)
403
403
  users_app.command("enable", epilog=GLOBALS_EPILOG)(users_enable)
404
+ users_app.command("delete", epilog=GLOBALS_EPILOG)(users_delete)
405
+ users_app.command("defaults", epilog=GLOBALS_EPILOG)(users_defaults)
404
406
  app.add_typer(users_app, name="users")
407
+
408
+
409
+ def users_delete(ctx: typer.Context, id: str = typer.Argument(...)) -> None:
410
+ """Delete a member by id (not email)."""
411
+ from .account_cmds import request
412
+ request(ctx, "DELETE", f"/api/users/{id}")
413
+
414
+
415
+ def users_defaults(ctx: typer.Context) -> None:
416
+ """Show defaults for new members."""
417
+ from .account_cmds import request
418
+ request(ctx, "GET", "/api/users/defaults")
@@ -85,6 +85,9 @@ class NotFoundError(FpCliError):
85
85
  """The requested resource does not exist."""
86
86
 
87
87
  exit_code = 6
88
+ # The HTTP response may explicitly distinguish an absent endpoint from a
89
+ # missing record. None means callers must fall back to the request shape.
90
+ route_unknown: Optional[bool] = None
88
91
 
89
92
 
90
93
  class ApiError(FpCliError):