flowforge-map 0.4.0__tar.gz → 0.4.2__tar.gz
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.
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/PKG-INFO +37 -17
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/README.md +36 -16
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/agent/server.py +2 -1
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/keys.py +12 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/checks/probes.py +20 -5
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/cli_ui.py +5 -1
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/example_data.py +9 -9
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flow/__init__.py +3 -1
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flow/curly.py +3 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flow/index.py +1 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flow/order.py +76 -11
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flow/rewrite.py +22 -3
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scan/files.py +20 -12
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scan/scanner.py +16 -4
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/security/finders.py +92 -1
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/security/findings.py +20 -5
- flowforge_map-0.4.0/frontend/dist/assets/AddConnector-Bi8J4U4d.js → flowforge_map-0.4.2/frontend/dist/assets/AddConnector-D3dJ-1pw.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/Catalog-zd0ADYry.js → flowforge_map-0.4.2/frontend/dist/assets/Catalog-_1sdJlO0.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/ChecksPage-B77bP82v.js → flowforge_map-0.4.2/frontend/dist/assets/ChecksPage-B4VDQZj7.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/Connector-D4bc9fm5.js → flowforge_map-0.4.2/frontend/dist/assets/Connector-D3hHmyyL.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/ExportPage-X83lvpEi.js → flowforge_map-0.4.2/frontend/dist/assets/ExportPage-DW0JW4us.js +2 -2
- flowforge_map-0.4.0/frontend/dist/assets/FlowMapPage-BrnQhApb.js → flowforge_map-0.4.2/frontend/dist/assets/FlowMapPage-DUuVP3sL.js +1 -1
- flowforge_map-0.4.2/frontend/dist/assets/Health-5Ea16UNQ.js +1 -0
- flowforge_map-0.4.0/frontend/dist/assets/HowDrawn-CazyemwU.js → flowforge_map-0.4.2/frontend/dist/assets/HowDrawn-W7o-RELm.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/IssuesPage-DI9W3uIc.js → flowforge_map-0.4.2/frontend/dist/assets/IssuesPage-D1R8nhFJ.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/KeysPage-B2vcuxeq.js → flowforge_map-0.4.2/frontend/dist/assets/KeysPage-BW2TiEJ4.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/Later-89_57i-0.js → flowforge_map-0.4.2/frontend/dist/assets/Later-BKVN1K5u.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/LiveRun-DgaIy_Hy.js → flowforge_map-0.4.2/frontend/dist/assets/LiveRun-BPspDohV.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/Recheck-IaxmWKc4.js → flowforge_map-0.4.2/frontend/dist/assets/Recheck-Cfl9oCf6.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/RunHistory-DeusCIFP.js → flowforge_map-0.4.2/frontend/dist/assets/RunHistory-CpOpVa9H.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/SettingsPage-CF0lFy0X.js → flowforge_map-0.4.2/frontend/dist/assets/SettingsPage-D4KMC8BT.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/flowmap-DhjGiCkH.js → flowforge_map-0.4.2/frontend/dist/assets/flowmap-DCEYMfCm.js +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/index-Bl5rbBs-.js → flowforge_map-0.4.2/frontend/dist/assets/index-X7o1rC4D.js +5 -4
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/index.html +1 -1
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/pyproject.toml +1 -1
- flowforge_map-0.4.0/frontend/dist/assets/Health-DROS3rFg.js +0 -1
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/.gitignore +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/LICENSE +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/__init__.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/agent/__init__.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/__init__.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/agent.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/apps.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/checks.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/connectors.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/flowmap.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/issues.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/lock.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/projects.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/runs.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/scan.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/security.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/settings.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/api/stats.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/apps.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/checks/__init__.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/checks/models.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/cli.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/connectors/__init__.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/connectors/catalog.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/connectors/models.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/connectors/presets.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/connectors/registry.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/connectors/runtime.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/connectors/secrets.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flow/advice.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flow/reach.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flowmap/__init__.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flowmap/diff.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flowmap/fixture.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flowmap/models.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flowmap/planner.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flowmap/service.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/flowmap/validate.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/home.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/main.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/nodes/__init__.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/nodes/base.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/nodes/check_node.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/nodes/http_node.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/nodes/llm_node.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/nodes/local_node.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/nodes/mcp_node.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/nodes/mock_node.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/project_states.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/projects.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scan/__init__.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scan/facts.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scan/manifests.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scan/mcp_config.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scan/models.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scan/patterns.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scan/python_ast.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scan/services.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scan/urls.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scheduler/__init__.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scheduler/cache.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scheduler/critical_path.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scheduler/durations.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scheduler/executor.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scheduler/graph.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/scheduler/rate_limit.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/schema.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/security/__init__.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/security/fixes.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/security/keystore.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/security/local_only.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/security/lock.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/security/logfilter.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/security/masks.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/security/wire.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/settings.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/spa.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/state.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/storage.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/templating.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/flowforge/terminal.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/workflows/diamond_mock.json +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/backend/workflows/stripe_to_razorpay.json +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/.env.example +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/.gitignore +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/.mcp.json +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/README.md +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/config.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/mcp_servers/currency.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/planner.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/requirements.txt +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/services/__init__.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/services/geo.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/services/llm.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/services/mcp_tools.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/services/weather.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/tools/packing_list.py +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/examples/trip-planner/trips/.gitkeep +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/classic.html +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/Catalog-CA9b5nAc.css +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/ExportPage-e2bor7CC.css +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/Health-FLn--j7D.css +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/LiveRun-NyTlzZhk.css +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/SettingsPage-DrMN0pie.css +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/client-8TP7XnGi.js +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/flowmap-qRSp4v09.css +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/how-Bunfi3hG.css +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/icons-B5HLu6t-.js +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/index-BygBw1rS.css +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/links-BKyTYkee.js +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/mapId-CAn3Vwt1.js +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/sse-Bbzu7IqU.js +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/status-BtyVLBUd.js +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/assets/useChecks-Dgca8Xe7.js +0 -0
- {flowforge_map-0.4.0 → flowforge_map-0.4.2}/frontend/dist/favicon.svg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flowforge-map
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Map what your app depends on (LLMs, MCP servers, APIs), check their health and security, and see what could run faster. Local and free; the scan needs no AI.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Vishwa-Thangapandiyan/flowforge-agent-orchestrator
|
|
6
6
|
Project-URL: Issues, https://github.com/Vishwa-Thangapandiyan/flowforge-agent-orchestrator/issues
|
|
@@ -37,24 +37,30 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
|
|
38
38
|
# FlowForge
|
|
39
39
|
|
|
40
|
-
**
|
|
40
|
+
**See everything your AI app depends on: whether it's reachable, whether it's safe, and what's slowing it down.**
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
```bash
|
|
43
|
+
npx flowforge-map --example # a sample app's map in your browser: no keys, nothing sent
|
|
44
|
+
```
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+

|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
| | |
|
|
49
|
+
|---|---|
|
|
50
|
+
| 🗺 **Map** | Every LLM, MCP server, HTTP API and local program your code calls, each with the `file:line` it came from |
|
|
51
|
+
| 💚 **Check now** | Green, amber or red for each one, with its latency. When something is down, it tells you why |
|
|
52
|
+
| ⚡ **Faster** | Reads the order your code makes its calls and shows which ones could run together, with the time you'd save |
|
|
53
|
+
| 🛡 **Issues** | Keys written in code, plain `http://`, MCP servers that aren't pinned. Fix one on a new branch, or send it to your agent |
|
|
54
|
+
| 🔑 **Keys** | Paste a key once. **Add to code** writes it to a git-ignored file your app reads; FlowForge only ever hands your agent a mask |
|
|
55
|
+
| 🤖 **Your agent** | One MCP entry gives Claude Code, Codex or Antigravity the map, the advice and the issues |
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
- finds the bottleneck chain with a **longest-path DP** (critical path), predicted before the run and measured after it
|
|
50
|
-
- runs independent steps concurrently under limited resources (`k` slots + per-provider rate limits) using **critical-path list scheduling** (HLFET), which is within `(2 − 1/k)` of optimal (Graham's bound)
|
|
51
|
-
- makes duplicate calls only once through **memoization** (single-flight in-run cache + a safe persistent cache)
|
|
57
|
+

|
|
52
58
|
|
|
53
|
-
**Free
|
|
59
|
+
**Free and local.** MIT, no account, no paid service. FlowForge keeps its data on your machine, and the scan is plain code that sends nothing to any LLM.
|
|
54
60
|
|
|
55
|
-
**
|
|
61
|
+
**Under the hood:** a DAG scheduler runs the checks concurrently under rate limits. It uses a topological sort, a critical-path DP and HLFET list scheduling (within `(2 − 1/k)` of optimal), and it caches repeat calls. See [Scheduling policies and benchmarks](#scheduling-policies-and-benchmarks).
|
|
56
62
|
|
|
57
|
-
> FlowForge started as a Design and Analysis of Algorithms course project. That version is
|
|
63
|
+
> FlowForge started as a Design and Analysis of Algorithms course project. That version is commit [`233a681`](../../tree/233a681c3e17fa4081c883193f8456a53a4c049c) in `main`'s history.
|
|
58
64
|
|
|
59
65
|
## Quick start
|
|
60
66
|
|
|
@@ -65,7 +71,7 @@ npx flowforge-map --example # try it: a sample app's maps in your browser,
|
|
|
65
71
|
npx flowforge-map --repo . # map the folder you're in
|
|
66
72
|
```
|
|
67
73
|
|
|
68
|
-
That's all: no Python, no virtual env. The first run downloads FlowForge's ready-made program for your computer (about 60 MB, checked against the release's checksums) into `~/.flowforge/bin`; after that it starts at once, even offline. Works on Windows, macOS (Apple
|
|
74
|
+
That's all: no Python, no virtual env. The first run downloads FlowForge's ready-made program for your computer (about 60 MB, checked against the release's checksums) into `~/.flowforge/bin`; after that it starts at once, even offline. Works on Windows, macOS (Apple chips) and Linux x64 with Node 18+. Your data lives in `~/.flowforge`.
|
|
69
75
|
|
|
70
76
|
| You have | Run | |
|
|
71
77
|
|---|---|---|
|
|
@@ -74,7 +80,7 @@ That's all: no Python, no virtual env. The first run downloads FlowForge's ready
|
|
|
74
80
|
| Python people | `uvx flowforge-map` or `pipx install flowforge-map` | Python 3.12+; no virtual env to manage |
|
|
75
81
|
| No terminal | the desktop app (Releases) | download and open; **Settings → Terminal** adds the `flowforge` command too (Linux .rpm/.deb have it already) |
|
|
76
82
|
|
|
77
|
-
Every way gives the same `flowforge` command: `npx flowforge-map status`, `npx flowforge-map key add STRIPE_KEY`, and so on. Linux ARM and Windows ARM have no ready-made build yet, so `npx` uses `uvx` there.
|
|
83
|
+
Every way gives the same `flowforge` command: `npx flowforge-map status`, `npx flowforge-map key add STRIPE_KEY`, and so on. Intel Macs, Linux ARM and Windows ARM have no ready-made build yet, so `npx` uses `uvx` there.
|
|
78
84
|
|
|
79
85
|
The first time, a short welcome walks you through three steps, each one skippable: your app's folder (or the example), pairing your coding agent once, and a lock for your keys. **⚙ Settings** at the bottom of the rail keeps what applies to every project: your agent, the theme and map style, the Vault's lock, and your data (each project's size, the MCP servers it may start, its check history, and the version, with **Check for updates**, which asks pypi.org only when you press it).
|
|
80
86
|
|
|
@@ -99,9 +105,11 @@ The same dashboard in its own window, with Python inside: nothing else to instal
|
|
|
99
105
|
| Your computer | Download |
|
|
100
106
|
|---|---|
|
|
101
107
|
| Windows | `FlowForge_x.y.z_x64-setup.exe` or `.msi` |
|
|
102
|
-
| Mac (Apple
|
|
108
|
+
| Mac (Apple chip) | `FlowForge_x.y.z_aarch64.dmg` |
|
|
103
109
|
| Linux | `.AppImage`, `.deb` or `.rpm` |
|
|
104
110
|
|
|
111
|
+
Intel Macs have no desktop build yet: use `npx flowforge-map` (it runs FlowForge through `uv` there) or `uvx flowforge-map`.
|
|
112
|
+
|
|
105
113
|
The builds aren't signed yet (signing costs money every year), so the first launch asks once:
|
|
106
114
|
- **Mac:** right-click FlowForge in Applications → **Open** → **Open**.
|
|
107
115
|
- **Windows:** on "Windows protected your PC", click **More info** → **Run anyway**.
|
|
@@ -147,7 +155,6 @@ Switch the map to **Flow**. FlowForge reads your Python (by its syntax tree), Da
|
|
|
147
155
|
- **Faster** compares "as written" (the slowest action, one call after another) with "could be", and lists the calls that don't need each other's results. **Show the change** writes out the `asyncio.gather`, Dart's `(a, b).wait` or `Promise.all` (now → could be, ready to copy) when it's safe; **Copy prompt for your coding agent** hands the rest to Claude Code, Codex or any agent. FlowForge never edits your code.
|
|
148
156
|
- **Watch it run** plays the could-be schedule; pointing at a block lights up its path. Five map styles: Studio, Boxed, Blueprint, Metro, Soft.
|
|
149
157
|
|
|
150
|
-
|
|
151
158
|
### Many projects
|
|
152
159
|
|
|
153
160
|
The home page lists every app you've mapped, with its dependencies, what needs fixing and missing keys. **+ New project** takes your app's folder, scans it and shows its map. The project tile at the top of the rail switches between the projects you have open, and each one comes back where you left it. **×** closes a project from that list without deleting it. Each project keeps its own map, checks, issues and keys; `flowforge --repo PATH` opens (or adds) that folder's project.
|
|
@@ -160,10 +167,21 @@ FlowForge is also an **MCP server**, so Claude Code, Codex or Antigravity can as
|
|
|
160
167
|
claude mcp add flowforge -- uvx flowforge-map mcp
|
|
161
168
|
```
|
|
162
169
|
|
|
163
|
-
Pair your agent once and it sees every project: `flowforge mcp` with no folder answers about the project your agent is working in, or any other by name (`list_projects` shows them all). Your agent gets
|
|
170
|
+
Pair your agent once and it sees every project: `flowforge mcp` with no folder answers about the project your agent is working in, or any other by name (`list_projects` shows them all). Your agent gets fourteen tools:
|
|
171
|
+
|
|
172
|
+
| Kind | Tools |
|
|
173
|
+
|---|---|
|
|
174
|
+
| Read only | `list_projects`, `get_map`, `get_flow`, `get_advice`, `get_security_findings`, `get_keys`, `wait_for_keys`, `app_output`, `get_fix_requests`, `what_changed` |
|
|
175
|
+
| Checks | `check_health`: makes requests, and starts an MCP server only after you said yes to it |
|
|
176
|
+
| Asks you | `request_key`: asks for a key by name |
|
|
177
|
+
| Runs your app | `run_app`, `stop_app`: start and stop your app with its keys filled in, only after you allow that exact command once (D43) |
|
|
178
|
+
|
|
179
|
+
Every answer is masked (key values never appear), and the agent can't make FlowForge change your code. FlowForge needs no AI of its own for any of this.
|
|
164
180
|
|
|
165
181
|
### Fix what's unsafe, with one click
|
|
166
182
|
|
|
183
|
+

|
|
184
|
+
|
|
167
185
|
The **Issues** page lists what the scan found unsafe, and every issue has one button:
|
|
168
186
|
|
|
169
187
|
| Issue | Button | What happens |
|
|
@@ -175,6 +193,8 @@ Fix needs a git repo and the file committed. FlowForge works out the change itse
|
|
|
175
193
|
|
|
176
194
|
### Keys your coding agent never sees
|
|
177
195
|
|
|
196
|
+

|
|
197
|
+
|
|
178
198
|
**Add to code.** The **Keys** page lists each place your code needs a key, with its `file:line`. Paste the key and press **Add to code**:
|
|
179
199
|
|
|
180
200
|
```
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
# FlowForge
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**See everything your AI app depends on: whether it's reachable, whether it's safe, and what's slowing it down.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```bash
|
|
6
|
+
npx flowforge-map --example # a sample app's map in your browser: no keys, nothing sent
|
|
7
|
+
```
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+

|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
| | |
|
|
12
|
+
|---|---|
|
|
13
|
+
| 🗺 **Map** | Every LLM, MCP server, HTTP API and local program your code calls, each with the `file:line` it came from |
|
|
14
|
+
| 💚 **Check now** | Green, amber or red for each one, with its latency. When something is down, it tells you why |
|
|
15
|
+
| ⚡ **Faster** | Reads the order your code makes its calls and shows which ones could run together, with the time you'd save |
|
|
16
|
+
| 🛡 **Issues** | Keys written in code, plain `http://`, MCP servers that aren't pinned. Fix one on a new branch, or send it to your agent |
|
|
17
|
+
| 🔑 **Keys** | Paste a key once. **Add to code** writes it to a git-ignored file your app reads; FlowForge only ever hands your agent a mask |
|
|
18
|
+
| 🤖 **Your agent** | One MCP entry gives Claude Code, Codex or Antigravity the map, the advice and the issues |
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
- finds the bottleneck chain with a **longest-path DP** (critical path), predicted before the run and measured after it
|
|
13
|
-
- runs independent steps concurrently under limited resources (`k` slots + per-provider rate limits) using **critical-path list scheduling** (HLFET), which is within `(2 − 1/k)` of optimal (Graham's bound)
|
|
14
|
-
- makes duplicate calls only once through **memoization** (single-flight in-run cache + a safe persistent cache)
|
|
20
|
+

|
|
15
21
|
|
|
16
|
-
**Free
|
|
22
|
+
**Free and local.** MIT, no account, no paid service. FlowForge keeps its data on your machine, and the scan is plain code that sends nothing to any LLM.
|
|
17
23
|
|
|
18
|
-
**
|
|
24
|
+
**Under the hood:** a DAG scheduler runs the checks concurrently under rate limits. It uses a topological sort, a critical-path DP and HLFET list scheduling (within `(2 − 1/k)` of optimal), and it caches repeat calls. See [Scheduling policies and benchmarks](#scheduling-policies-and-benchmarks).
|
|
19
25
|
|
|
20
|
-
> FlowForge started as a Design and Analysis of Algorithms course project. That version is
|
|
26
|
+
> FlowForge started as a Design and Analysis of Algorithms course project. That version is commit [`233a681`](../../tree/233a681c3e17fa4081c883193f8456a53a4c049c) in `main`'s history.
|
|
21
27
|
|
|
22
28
|
## Quick start
|
|
23
29
|
|
|
@@ -28,7 +34,7 @@ npx flowforge-map --example # try it: a sample app's maps in your browser,
|
|
|
28
34
|
npx flowforge-map --repo . # map the folder you're in
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
That's all: no Python, no virtual env. The first run downloads FlowForge's ready-made program for your computer (about 60 MB, checked against the release's checksums) into `~/.flowforge/bin`; after that it starts at once, even offline. Works on Windows, macOS (Apple
|
|
37
|
+
That's all: no Python, no virtual env. The first run downloads FlowForge's ready-made program for your computer (about 60 MB, checked against the release's checksums) into `~/.flowforge/bin`; after that it starts at once, even offline. Works on Windows, macOS (Apple chips) and Linux x64 with Node 18+. Your data lives in `~/.flowforge`.
|
|
32
38
|
|
|
33
39
|
| You have | Run | |
|
|
34
40
|
|---|---|---|
|
|
@@ -37,7 +43,7 @@ That's all: no Python, no virtual env. The first run downloads FlowForge's ready
|
|
|
37
43
|
| Python people | `uvx flowforge-map` or `pipx install flowforge-map` | Python 3.12+; no virtual env to manage |
|
|
38
44
|
| No terminal | the desktop app (Releases) | download and open; **Settings → Terminal** adds the `flowforge` command too (Linux .rpm/.deb have it already) |
|
|
39
45
|
|
|
40
|
-
Every way gives the same `flowforge` command: `npx flowforge-map status`, `npx flowforge-map key add STRIPE_KEY`, and so on. Linux ARM and Windows ARM have no ready-made build yet, so `npx` uses `uvx` there.
|
|
46
|
+
Every way gives the same `flowforge` command: `npx flowforge-map status`, `npx flowforge-map key add STRIPE_KEY`, and so on. Intel Macs, Linux ARM and Windows ARM have no ready-made build yet, so `npx` uses `uvx` there.
|
|
41
47
|
|
|
42
48
|
The first time, a short welcome walks you through three steps, each one skippable: your app's folder (or the example), pairing your coding agent once, and a lock for your keys. **⚙ Settings** at the bottom of the rail keeps what applies to every project: your agent, the theme and map style, the Vault's lock, and your data (each project's size, the MCP servers it may start, its check history, and the version, with **Check for updates**, which asks pypi.org only when you press it).
|
|
43
49
|
|
|
@@ -62,9 +68,11 @@ The same dashboard in its own window, with Python inside: nothing else to instal
|
|
|
62
68
|
| Your computer | Download |
|
|
63
69
|
|---|---|
|
|
64
70
|
| Windows | `FlowForge_x.y.z_x64-setup.exe` or `.msi` |
|
|
65
|
-
| Mac (Apple
|
|
71
|
+
| Mac (Apple chip) | `FlowForge_x.y.z_aarch64.dmg` |
|
|
66
72
|
| Linux | `.AppImage`, `.deb` or `.rpm` |
|
|
67
73
|
|
|
74
|
+
Intel Macs have no desktop build yet: use `npx flowforge-map` (it runs FlowForge through `uv` there) or `uvx flowforge-map`.
|
|
75
|
+
|
|
68
76
|
The builds aren't signed yet (signing costs money every year), so the first launch asks once:
|
|
69
77
|
- **Mac:** right-click FlowForge in Applications → **Open** → **Open**.
|
|
70
78
|
- **Windows:** on "Windows protected your PC", click **More info** → **Run anyway**.
|
|
@@ -110,7 +118,6 @@ Switch the map to **Flow**. FlowForge reads your Python (by its syntax tree), Da
|
|
|
110
118
|
- **Faster** compares "as written" (the slowest action, one call after another) with "could be", and lists the calls that don't need each other's results. **Show the change** writes out the `asyncio.gather`, Dart's `(a, b).wait` or `Promise.all` (now → could be, ready to copy) when it's safe; **Copy prompt for your coding agent** hands the rest to Claude Code, Codex or any agent. FlowForge never edits your code.
|
|
111
119
|
- **Watch it run** plays the could-be schedule; pointing at a block lights up its path. Five map styles: Studio, Boxed, Blueprint, Metro, Soft.
|
|
112
120
|
|
|
113
|
-
|
|
114
121
|
### Many projects
|
|
115
122
|
|
|
116
123
|
The home page lists every app you've mapped, with its dependencies, what needs fixing and missing keys. **+ New project** takes your app's folder, scans it and shows its map. The project tile at the top of the rail switches between the projects you have open, and each one comes back where you left it. **×** closes a project from that list without deleting it. Each project keeps its own map, checks, issues and keys; `flowforge --repo PATH` opens (or adds) that folder's project.
|
|
@@ -123,10 +130,21 @@ FlowForge is also an **MCP server**, so Claude Code, Codex or Antigravity can as
|
|
|
123
130
|
claude mcp add flowforge -- uvx flowforge-map mcp
|
|
124
131
|
```
|
|
125
132
|
|
|
126
|
-
Pair your agent once and it sees every project: `flowforge mcp` with no folder answers about the project your agent is working in, or any other by name (`list_projects` shows them all). Your agent gets
|
|
133
|
+
Pair your agent once and it sees every project: `flowforge mcp` with no folder answers about the project your agent is working in, or any other by name (`list_projects` shows them all). Your agent gets fourteen tools:
|
|
134
|
+
|
|
135
|
+
| Kind | Tools |
|
|
136
|
+
|---|---|
|
|
137
|
+
| Read only | `list_projects`, `get_map`, `get_flow`, `get_advice`, `get_security_findings`, `get_keys`, `wait_for_keys`, `app_output`, `get_fix_requests`, `what_changed` |
|
|
138
|
+
| Checks | `check_health`: makes requests, and starts an MCP server only after you said yes to it |
|
|
139
|
+
| Asks you | `request_key`: asks for a key by name |
|
|
140
|
+
| Runs your app | `run_app`, `stop_app`: start and stop your app with its keys filled in, only after you allow that exact command once (D43) |
|
|
141
|
+
|
|
142
|
+
Every answer is masked (key values never appear), and the agent can't make FlowForge change your code. FlowForge needs no AI of its own for any of this.
|
|
127
143
|
|
|
128
144
|
### Fix what's unsafe, with one click
|
|
129
145
|
|
|
146
|
+

|
|
147
|
+
|
|
130
148
|
The **Issues** page lists what the scan found unsafe, and every issue has one button:
|
|
131
149
|
|
|
132
150
|
| Issue | Button | What happens |
|
|
@@ -138,6 +156,8 @@ Fix needs a git repo and the file committed. FlowForge works out the change itse
|
|
|
138
156
|
|
|
139
157
|
### Keys your coding agent never sees
|
|
140
158
|
|
|
159
|
+

|
|
160
|
+
|
|
141
161
|
**Add to code.** The **Keys** page lists each place your code needs a key, with its `file:line`. Paste the key and press **Add to code**:
|
|
142
162
|
|
|
143
163
|
```
|
|
@@ -157,7 +157,8 @@ def headline(tool: str, data: dict[str, Any]) -> str | None:
|
|
|
157
157
|
return _plural(len(data["dependencies"]), "dependency", "dependencies")
|
|
158
158
|
if tool == "check_health":
|
|
159
159
|
counts = data["summary"]
|
|
160
|
-
|
|
160
|
+
words = {"up": "reachable", "warn": "need a look", "down": "down", "skipped": "not checked"} # D52
|
|
161
|
+
parts = [f"{counts[s]} {w}" for s, w in words.items() if counts.get(s)]
|
|
161
162
|
return " · ".join(parts) or "Nothing to check"
|
|
162
163
|
if tool in ("get_flow", "get_advice") and not data["available"]:
|
|
163
164
|
return "No flow: no Python calls found"
|
|
@@ -185,6 +185,18 @@ def _writable(s: AppState) -> Path:
|
|
|
185
185
|
return repo
|
|
186
186
|
|
|
187
187
|
|
|
188
|
+
@router.get("/keys/asked")
|
|
189
|
+
def keys_asked() -> dict[str, list[str]]:
|
|
190
|
+
"""The names your agent asked for and you haven't saved yet (D38): cheap enough for the rail's badge to poll,
|
|
191
|
+
since it never reads the code. Names only."""
|
|
192
|
+
s = app_state()
|
|
193
|
+
repo = repo_path(s)
|
|
194
|
+
if repo is None or not repo.is_dir():
|
|
195
|
+
return {"names": []}
|
|
196
|
+
saved = set(KeyStore(repo).names())
|
|
197
|
+
return {"names": [r["name"] for r in key_requests(s.storage, repo) if r["name"] not in saved]}
|
|
198
|
+
|
|
199
|
+
|
|
188
200
|
@router.get("/keys")
|
|
189
201
|
def keys() -> dict[str, Any]:
|
|
190
202
|
s = app_state()
|
|
@@ -205,6 +205,13 @@ def _warnings(url: str, answer: _Answer) -> list[str]:
|
|
|
205
205
|
return out
|
|
206
206
|
|
|
207
207
|
|
|
208
|
+
def _reachable(code: int) -> str:
|
|
209
|
+
"""What one answer proves (D52): the server is there. Not that a key works, nor that your call does."""
|
|
210
|
+
if code in (401, 403):
|
|
211
|
+
return f"Reachable, needs a key ({code})"
|
|
212
|
+
return "Reachable" if code < 300 else f"Reachable, answered {code}" # a redirect isn't followed: say so
|
|
213
|
+
|
|
214
|
+
|
|
208
215
|
async def api(t: CheckTarget, http: httpx.AsyncClient) -> CheckResult:
|
|
209
216
|
if not t.target:
|
|
210
217
|
return _result(t, "skipped", "Not checked: no address in the code")
|
|
@@ -215,7 +222,11 @@ async def api(t: CheckTarget, http: httpx.AsyncClient) -> CheckResult:
|
|
|
215
222
|
detail = {"http_status": answer.code}
|
|
216
223
|
if answer.code >= 500:
|
|
217
224
|
return _result(t, "down", f"Answered {answer.code}", latency_ms=answer.ms, detail=detail)
|
|
218
|
-
|
|
225
|
+
if answer.code == 429:
|
|
226
|
+
return _result(t, "warn", "Reachable, but rate-limiting this computer (429)", latency_ms=answer.ms,
|
|
227
|
+
detail=detail, fix="Check again later. If it keeps happening, your app may be hitting the "
|
|
228
|
+
"same limit.")
|
|
229
|
+
up = _reachable(answer.code)
|
|
219
230
|
if warnings := _warnings(url, answer):
|
|
220
231
|
return _result(t, "warn", f"{up}, but {'; '.join(warnings)}", latency_ms=answer.ms, detail=detail)
|
|
221
232
|
return _result(t, "up", up, latency_ms=answer.ms, detail=detail)
|
|
@@ -252,8 +263,12 @@ async def llm(t: CheckTarget, http: httpx.AsyncClient) -> CheckResult:
|
|
|
252
263
|
detail = {"http_status": answer.code}
|
|
253
264
|
if answer.code >= 500:
|
|
254
265
|
return _result(t, "down", f"Answered {answer.code}", latency_ms=answer.ms, detail=detail)
|
|
266
|
+
if answer.code == 429:
|
|
267
|
+
return _result(t, "warn", "Reachable, but rate-limiting this computer (429)", latency_ms=answer.ms,
|
|
268
|
+
detail=detail, fix="Check again later. If it keeps happening, your app may be hitting the "
|
|
269
|
+
"same limit.")
|
|
255
270
|
if answer.code in (401, 403):
|
|
256
|
-
return _result(t, "up", f"
|
|
271
|
+
return _result(t, "up", f"Reachable, needs a key ({answer.code}) to list its models", latency_ms=answer.ms,
|
|
257
272
|
detail=detail)
|
|
258
273
|
if t.model and answer.body is not None:
|
|
259
274
|
listed = t.model.removesuffix(":latest") in _model_names(answer.body)
|
|
@@ -261,8 +276,8 @@ async def llm(t: CheckTarget, http: httpx.AsyncClient) -> CheckResult:
|
|
|
261
276
|
if not listed:
|
|
262
277
|
fix = f"Run `ollama pull {t.model}`." if t.catalog == "ollama" else None
|
|
263
278
|
what = "isn't pulled" if t.catalog == "ollama" else "isn't in its model list"
|
|
264
|
-
return _result(t, "warn", f"
|
|
265
|
-
up = f"
|
|
279
|
+
return _result(t, "warn", f"Reachable, but {t.model} {what}", latency_ms=answer.ms, detail=detail, fix=fix)
|
|
280
|
+
up = f"Reachable, {t.model} is ready" if t.model and detail.get("model_listed") else _reachable(answer.code)
|
|
266
281
|
if warnings := _warnings(url, answer):
|
|
267
282
|
return _result(t, "warn", f"{up}, but {'; '.join(warnings)}", latency_ms=answer.ms, detail=detail)
|
|
268
283
|
return _result(t, "up", up, latency_ms=answer.ms, detail=detail)
|
|
@@ -329,7 +344,7 @@ async def mcp(t: CheckTarget, root: Path, http: httpx.AsyncClient) -> CheckResul
|
|
|
329
344
|
reason += f"\nIts config sets {', '.join(t.env_names)}."
|
|
330
345
|
return _result(t, "down", "Stopped while starting", reason=reason, fix=_env_fix(t))
|
|
331
346
|
ms = (time.perf_counter() - start) * 1000
|
|
332
|
-
up = f"
|
|
347
|
+
up = f"Reachable: {len(tools)} tool{'s' if len(tools) != 1 else ''}"
|
|
333
348
|
detail = {"tools": tools}
|
|
334
349
|
if ms > MCP_SLOW_MS:
|
|
335
350
|
return _result(t, "warn", f"{up}, but slow to start: {took(ms)}", latency_ms=ms, detail=detail)
|
|
@@ -73,9 +73,13 @@ def level(value: str) -> Text:
|
|
|
73
73
|
return Text(value, style=STYLES.get(value, ""))
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
HEALTH_WORDS = {"up": "reachable", "warn": "needs a look"} # what a check proves (D52)
|
|
77
|
+
|
|
78
|
+
|
|
76
79
|
def dot(out: Console, status: str) -> Text:
|
|
77
80
|
"""Health keeps its label even without colour (D41)."""
|
|
78
|
-
|
|
81
|
+
word = HEALTH_WORDS.get(status, status)
|
|
82
|
+
return Text(f"{'●' if utf8(out) else '*'} {word}", style=STYLES.get(status, "bright_black"))
|
|
79
83
|
|
|
80
84
|
|
|
81
85
|
def table(out: Console, title: str, columns: list[str], rows: list[list[Any]], *, border: bool = True) -> None:
|
|
@@ -176,21 +176,21 @@ def _recorded(id: str, status: Status, latency_ms: float | None, summary: str, *
|
|
|
176
176
|
|
|
177
177
|
# Recorded check results for the trip planner (D24): what a real check run gave, so example mode stays offline.
|
|
178
178
|
CHECK_RESULTS: dict[str, CheckResult] = {r.id: r for r in [
|
|
179
|
-
_recorded("mcp-time", "up", 1900, "
|
|
180
|
-
_recorded("mcp-files", "up", 2400, "
|
|
181
|
-
_recorded("mcp-fetch", "warn", 6800, "
|
|
179
|
+
_recorded("mcp-time", "up", 1900, "Reachable: 2 tools"),
|
|
180
|
+
_recorded("mcp-files", "up", 2400, "Reachable: 14 tools"),
|
|
181
|
+
_recorded("mcp-fetch", "warn", 6800, "Reachable: 1 tool, but slow to start: 6.8 s"),
|
|
182
182
|
_recorded("mcp-currency", "down", None, "Stopped while starting",
|
|
183
183
|
reason="currency server: CURRENCY_API_URL is not set",
|
|
184
184
|
fix="Its code reads CURRENCY_API_URL (mcp_servers/currency.py:9), but its MCP config gives it no env."),
|
|
185
185
|
_recorded("local-packing-list", "up", None, "Found, not run"),
|
|
186
186
|
_recorded("llm-ollama", "down", None, "Nothing answers at localhost:11434",
|
|
187
187
|
reason="[Errno 111] Connection refused", fix="Start Ollama (`ollama serve`)."),
|
|
188
|
-
_recorded("api-osrm", "up", 640, "
|
|
189
|
-
_recorded("api-open-meteo", "up", 180, "
|
|
190
|
-
_recorded("api-nominatim", "up", 420, "
|
|
191
|
-
_recorded("api-ip-api-com", "warn", 210, "
|
|
192
|
-
_recorded("api-google-maps", "up", 230, "
|
|
193
|
-
_recorded("api-wikipedia", "up", 310, "
|
|
188
|
+
_recorded("api-osrm", "up", 640, "Reachable"),
|
|
189
|
+
_recorded("api-open-meteo", "up", 180, "Reachable"),
|
|
190
|
+
_recorded("api-nominatim", "up", 420, "Reachable"),
|
|
191
|
+
_recorded("api-ip-api-com", "warn", 210, "Reachable, but plain http://: not encrypted"),
|
|
192
|
+
_recorded("api-google-maps", "up", 230, "Reachable, needs a key (403)"),
|
|
193
|
+
_recorded("api-wikipedia", "up", 310, "Reachable"),
|
|
194
194
|
]}
|
|
195
195
|
|
|
196
196
|
|
|
@@ -19,8 +19,10 @@ from flowforge.flow.order import Body, Stmt, flatten, label, preds, reduce
|
|
|
19
19
|
from flowforge.flow.reach import Hit, Reach, writes
|
|
20
20
|
from flowforge.flowmap.models import Evidence, Flow, FlowEdge, FlowNode, Op
|
|
21
21
|
from flowforge.flowmap.validate import DEFAULT_MS, actions_of
|
|
22
|
+
from flowforge.scan.files import language
|
|
22
23
|
from flowforge.scan.models import Dependency, ScanResult
|
|
23
24
|
from flowforge.scan.scanner import Places, scan_with_places
|
|
25
|
+
from flowforge.security.finders import masked_lines
|
|
24
26
|
from flowforge.security.logfilter import Redactor, redact_code
|
|
25
27
|
|
|
26
28
|
NO_CODE = "Flow order reads Python, Dart, JavaScript and TypeScript."
|
|
@@ -130,7 +132,7 @@ class _Builder:
|
|
|
130
132
|
|
|
131
133
|
def entry(self, func: Any, body: Body, lang: Any) -> None:
|
|
132
134
|
self.lang = lang
|
|
133
|
-
lines = self.sources[func.path].
|
|
135
|
+
lines = masked_lines(self.sources[func.path], language(func.path), _redactor) # every key masked first
|
|
134
136
|
start = f"start-{func.name.replace('.', '-')}"[:64]
|
|
135
137
|
self.nodes.append(FlowNode(id=start, kind="start", title=f"{func.name}()"[:80],
|
|
136
138
|
what=f"An entry point in {func.path}.",
|
|
@@ -16,6 +16,7 @@ from flowforge.flow.rewrite import Call
|
|
|
16
16
|
from flowforge.flowmap.models import Change
|
|
17
17
|
from flowforge.scan import services
|
|
18
18
|
from flowforge.scan.scanner import Places
|
|
19
|
+
from flowforge.security.finders import split_key_in
|
|
19
20
|
from flowforge.security.logfilter import redact_code
|
|
20
21
|
|
|
21
22
|
LANGUAGES = frozenset({"dart", "javascript", "typescript"})
|
|
@@ -852,6 +853,8 @@ def change(calls: list[Call], lang: Lang) -> Change | None:
|
|
|
852
853
|
first_line = plain[0][0].line
|
|
853
854
|
last_line = lang.end_line(func, plain[-1][0].stmt)
|
|
854
855
|
lines = src.text.splitlines()
|
|
856
|
+
if split_key_in(src.text, func.language, first_line, last_line):
|
|
857
|
+
return None # a key split over lines sits in the block: the prompt only, never its pieces
|
|
855
858
|
indent = re.match(r"\s*", lines[first_line - 1]).group(0)
|
|
856
859
|
semi = ";" if func.language == "dart" or all(p[4] for _, p in plain) else ""
|
|
857
860
|
if func.language == "dart":
|
|
@@ -58,6 +58,7 @@ class Index:
|
|
|
58
58
|
def __init__(self, sources: dict[str, str]):
|
|
59
59
|
self.modules: dict[str, Module] = {}
|
|
60
60
|
self.funcs: dict[str, Func] = {}
|
|
61
|
+
self.changed: dict[str, set[str]] = {} # func key -> parameters it changes in place (D51)
|
|
61
62
|
for path, text in sources.items():
|
|
62
63
|
try:
|
|
63
64
|
tree = ast.parse(text)
|
|
@@ -11,7 +11,7 @@ import ast
|
|
|
11
11
|
import re
|
|
12
12
|
from dataclasses import dataclass, field
|
|
13
13
|
|
|
14
|
-
from flowforge.flow.index import Func, Index
|
|
14
|
+
from flowforge.flow.index import Func, Index, Module
|
|
15
15
|
from flowforge.flow.reach import Hit, Reach, gathers
|
|
16
16
|
|
|
17
17
|
MUTATORS = frozenset({"append", "extend", "update", "add", "insert", "pop", "remove", "clear", "setdefault", "write",
|
|
@@ -41,17 +41,82 @@ def _reads(node: ast.AST) -> set[str]:
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def _writes(node: ast.AST) -> set[str]:
|
|
44
|
-
|
|
44
|
+
bound = {n.id for n in ast.walk(node) if isinstance(n, ast.Name) and isinstance(n.ctx, (ast.Store, ast.Del))}
|
|
45
|
+
return bound | changes(node)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _base(node: ast.AST) -> str | None:
|
|
49
|
+
while isinstance(node, (ast.Subscript, ast.Attribute)):
|
|
50
|
+
node = node.value
|
|
51
|
+
return node.id if isinstance(node, ast.Name) else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def changes(node: ast.AST) -> set[str]:
|
|
55
|
+
"""Names whose object the code changes in place: `x[k] = …`, `x.a = …`, `x.append(…)` (rebinding `x` isn't)."""
|
|
56
|
+
out: set[str] = set()
|
|
45
57
|
for n in ast.walk(node):
|
|
46
|
-
if isinstance(n, (ast.Subscript, ast.Attribute)) and isinstance(n.ctx, ast.Store)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
base = base.value
|
|
50
|
-
if isinstance(base, ast.Name):
|
|
51
|
-
out.add(base.id)
|
|
58
|
+
if isinstance(n, (ast.Subscript, ast.Attribute)) and isinstance(n.ctx, (ast.Store, ast.Del)) \
|
|
59
|
+
and (base := _base(n.value)):
|
|
60
|
+
out.add(base)
|
|
52
61
|
if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute) and n.func.attr in MUTATORS \
|
|
53
|
-
and
|
|
54
|
-
out.add(
|
|
62
|
+
and (base := _base(n.func.value)):
|
|
63
|
+
out.add(base) # facts.append(x) writes facts
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _params(func: Func) -> list[str]:
|
|
68
|
+
a = func.node.args
|
|
69
|
+
return [p.arg for p in (*a.posonlyargs, *a.args, *a.kwonlyargs)]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_method(func: Func) -> bool:
|
|
73
|
+
static = any(isinstance(d, ast.Name) and d.id == "staticmethod" for d in func.node.decorator_list)
|
|
74
|
+
return "." in func.name and not static
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _passed(call: ast.Call, target: Func, changed: set[str]) -> set[str]:
|
|
78
|
+
"""The caller's names that land on `changed` parameters of `target`: by position, by keyword, and the receiver
|
|
79
|
+
of a method (`cart.add(x)` passes `cart` as `self`)."""
|
|
80
|
+
params = _params(target)
|
|
81
|
+
out: set[str] = set()
|
|
82
|
+
if _is_method(target) and isinstance(call.func, ast.Attribute):
|
|
83
|
+
if params and params[0] in changed and (base := _base(call.func.value)):
|
|
84
|
+
out.add(base)
|
|
85
|
+
params = params[1:]
|
|
86
|
+
for i, arg in enumerate(call.args):
|
|
87
|
+
if i < len(params) and params[i] in changed and (base := _base(arg)):
|
|
88
|
+
out.add(base)
|
|
89
|
+
for kw in call.keywords:
|
|
90
|
+
if kw.arg in changed and (base := _base(kw.value)):
|
|
91
|
+
out.add(base)
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def changed_params(index: Index, key: str, _stack: frozenset[str] = frozenset()) -> set[str]:
|
|
96
|
+
"""The parameters a function in the repo changes in place, itself or through the repo's functions it calls (D51).
|
|
97
|
+
Library calls are taken not to change their arguments."""
|
|
98
|
+
if key in index.changed:
|
|
99
|
+
return index.changed[key]
|
|
100
|
+
func = index.funcs[key]
|
|
101
|
+
params = set(_params(func))
|
|
102
|
+
found = changes(func.node) & params
|
|
103
|
+
module = index.modules[func.module]
|
|
104
|
+
cls = func.name.split(".")[0] if "." in func.name else None
|
|
105
|
+
for n in ast.walk(func.node):
|
|
106
|
+
if isinstance(n, ast.Call) and (sub := index.resolve(n, module, cls)) and sub not in _stack | {key}:
|
|
107
|
+
found |= _passed(n, index.funcs[sub], changed_params(index, sub, _stack | {key})) & params
|
|
108
|
+
if not _stack:
|
|
109
|
+
index.changed[key] = found # a result found inside a cycle may be partial: kept only from the top
|
|
110
|
+
return found
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def call_writes(node: ast.AST, index: Index, module: Module, cls: str | None) -> set[str]:
|
|
114
|
+
"""Names a statement changes through the repo's own functions it calls: `await load(ctx)` writes `ctx` when `load`
|
|
115
|
+
fills it (D51)."""
|
|
116
|
+
out: set[str] = set()
|
|
117
|
+
for n in ast.walk(node):
|
|
118
|
+
if isinstance(n, ast.Call) and (key := index.resolve(n, module, cls)):
|
|
119
|
+
out |= _passed(n, index.funcs[key], changed_params(index, key))
|
|
55
120
|
return out
|
|
56
121
|
|
|
57
122
|
|
|
@@ -71,7 +136,7 @@ def flatten(func: Func, index: Index, reach: Reach) -> Body:
|
|
|
71
136
|
def add(node: ast.AST, line: int, ifs: list[tuple[int, str]], loop: bool, *, reads: set[str] | None = None,
|
|
72
137
|
writes: set[str] | None = None, hit_node: ast.AST | None = None, is_if: bool = False) -> Stmt:
|
|
73
138
|
s = Stmt(idx=len(body.stmts), node=node, line=line, reads=_reads(node) if reads is None else reads,
|
|
74
|
-
writes=_writes(node) if writes is None else writes,
|
|
139
|
+
writes=_writes(node) | call_writes(node, index, module, cls) if writes is None else writes,
|
|
75
140
|
hits=reach.statement(hit_node if hit_node is not None else node, module, cls) if hit_node is not False
|
|
76
141
|
else [], ifs=list(ifs), loop=loop, returns=isinstance(node, (ast.Return, ast.Raise)),
|
|
77
142
|
gathered=gathers(node), is_if=is_if)
|
|
@@ -12,7 +12,9 @@ import ast
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
+
from flowforge.flow.order import changes
|
|
15
16
|
from flowforge.flowmap.models import Change
|
|
17
|
+
from flowforge.security.finders import split_key_in
|
|
16
18
|
from flowforge.security.logfilter import redact_code
|
|
17
19
|
|
|
18
20
|
|
|
@@ -75,6 +77,20 @@ def _names(node: ast.AST, ctx: type) -> set[str]:
|
|
|
75
77
|
return {n.id for n in ast.walk(node) if isinstance(n, ast.Name) and isinstance(n.ctx, ctx)}
|
|
76
78
|
|
|
77
79
|
|
|
80
|
+
def _handed(node: ast.AST) -> set[str]:
|
|
81
|
+
"""Names handed to any call: a function we can't see into might change them, so a written change won't move a
|
|
82
|
+
call past it (D51)."""
|
|
83
|
+
out: set[str] = set()
|
|
84
|
+
for n in ast.walk(node):
|
|
85
|
+
if isinstance(n, ast.Call):
|
|
86
|
+
for arg in [*n.args, *(k.value for k in n.keywords)]:
|
|
87
|
+
while isinstance(arg, (ast.Subscript, ast.Attribute, ast.Starred)):
|
|
88
|
+
arg = arg.value
|
|
89
|
+
if isinstance(arg, ast.Name):
|
|
90
|
+
out.add(arg.id)
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
|
|
78
94
|
def _quoted(c: Call) -> str:
|
|
79
95
|
return f'"{c.words}" ({c.ref})'
|
|
80
96
|
|
|
@@ -102,16 +118,19 @@ def change(calls: list[Call], source: str, module: ast.Module) -> Change | None:
|
|
|
102
118
|
targets = set().union(*(_names(c.stmt, ast.Store) for c in plain))
|
|
103
119
|
reads = set().union(*(_names(_awaited(c.stmt)[1], ast.Load) for c in plain))
|
|
104
120
|
for st in body[first: last + 1]:
|
|
105
|
-
if id(st) not in members and (_names(st, ast.Load) & targets or _names(st, ast.Store) & reads
|
|
106
|
-
|
|
121
|
+
if id(st) not in members and (_names(st, ast.Load) & targets or _names(st, ast.Store) & reads
|
|
122
|
+
or (changes(st) | _handed(st)) & reads):
|
|
123
|
+
return None # code between them uses a result, or changes (or may change) what a call reads (D51)
|
|
107
124
|
|
|
108
125
|
lines = source.splitlines()
|
|
126
|
+
end = plain[-1].stmt.end_lineno or plain[-1].stmt.lineno
|
|
127
|
+
if split_key_in(source, "python", plain[0].stmt.lineno, end):
|
|
128
|
+
return None # a key split over lines sits in the block: the prompt only, never its pieces
|
|
109
129
|
indent = lines[plain[0].stmt.lineno - 1][: plain[0].stmt.col_offset]
|
|
110
130
|
names = [_awaited(c.stmt)[0] for c in plain]
|
|
111
131
|
lhs = "" if all(n == "_" for n in names) else f"{', '.join(names)} = "
|
|
112
132
|
gather = [f"{indent}{lhs}await asyncio.gather(",
|
|
113
133
|
*(f"{indent} {ast.unparse(_awaited(c.stmt)[1])}," for c in plain), f"{indent})"]
|
|
114
|
-
end = plain[-1].stmt.end_lineno or plain[-1].stmt.lineno
|
|
115
134
|
spans = [(c.stmt.lineno, c.stmt.end_lineno or c.stmt.lineno) for c in plain]
|
|
116
135
|
after: list[str] = []
|
|
117
136
|
for n in range(plain[0].stmt.lineno, end + 1):
|