flowstatemcp 2.5.0__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.
- flowstatemcp-2.5.0/.gitignore +63 -0
- flowstatemcp-2.5.0/PKG-INFO +88 -0
- flowstatemcp-2.5.0/README.md +52 -0
- flowstatemcp-2.5.0/flowstate/__init__.py +34 -0
- flowstatemcp-2.5.0/flowstate/__main__.py +7 -0
- flowstatemcp-2.5.0/flowstate/activity.py +224 -0
- flowstatemcp-2.5.0/flowstate/agent_guide.py +251 -0
- flowstatemcp-2.5.0/flowstate/app_keys.py +141 -0
- flowstatemcp-2.5.0/flowstate/approvals.py +974 -0
- flowstatemcp-2.5.0/flowstate/approve_page.py +565 -0
- flowstatemcp-2.5.0/flowstate/ask.py +107 -0
- flowstatemcp-2.5.0/flowstate/autosession.py +480 -0
- flowstatemcp-2.5.0/flowstate/backup.py +201 -0
- flowstatemcp-2.5.0/flowstate/brief.py +420 -0
- flowstatemcp-2.5.0/flowstate/card_offer.py +208 -0
- flowstatemcp-2.5.0/flowstate/card_taps.py +165 -0
- flowstatemcp-2.5.0/flowstate/card_trust.py +127 -0
- flowstatemcp-2.5.0/flowstate/claims.py +461 -0
- flowstatemcp-2.5.0/flowstate/cleanup.py +619 -0
- flowstatemcp-2.5.0/flowstate/cli.py +728 -0
- flowstatemcp-2.5.0/flowstate/cli_data.py +434 -0
- flowstatemcp-2.5.0/flowstate/cli_more.py +302 -0
- flowstatemcp-2.5.0/flowstate/cli_review.py +459 -0
- flowstatemcp-2.5.0/flowstate/cloudflared_check.py +49 -0
- flowstatemcp-2.5.0/flowstate/conflict_card_ui.py +286 -0
- flowstatemcp-2.5.0/flowstate/conflict_cards.py +1193 -0
- flowstatemcp-2.5.0/flowstate/conflict_reopen.py +320 -0
- flowstatemcp-2.5.0/flowstate/conflicts.py +420 -0
- flowstatemcp-2.5.0/flowstate/connections.py +33 -0
- flowstatemcp-2.5.0/flowstate/database.py +2191 -0
- flowstatemcp-2.5.0/flowstate/dbconn.py +308 -0
- flowstatemcp-2.5.0/flowstate/deletes.py +860 -0
- flowstatemcp-2.5.0/flowstate/embeddings.py +354 -0
- flowstatemcp-2.5.0/flowstate/erase.py +1047 -0
- flowstatemcp-2.5.0/flowstate/extras.py +71 -0
- flowstatemcp-2.5.0/flowstate/git_capture.py +157 -0
- flowstatemcp-2.5.0/flowstate/guard.py +224 -0
- flowstatemcp-2.5.0/flowstate/handoffs.py +168 -0
- flowstatemcp-2.5.0/flowstate/history.py +755 -0
- flowstatemcp-2.5.0/flowstate/history_page.py +276 -0
- flowstatemcp-2.5.0/flowstate/http_app.py +204 -0
- flowstatemcp-2.5.0/flowstate/identity.py +387 -0
- flowstatemcp-2.5.0/flowstate/install_name.py +140 -0
- flowstatemcp-2.5.0/flowstate/ledger.py +272 -0
- flowstatemcp-2.5.0/flowstate/legacy_args.py +65 -0
- flowstatemcp-2.5.0/flowstate/links.py +609 -0
- flowstatemcp-2.5.0/flowstate/markdown_io.py +242 -0
- flowstatemcp-2.5.0/flowstate/meaning.py +398 -0
- flowstatemcp-2.5.0/flowstate/memory_import.py +92 -0
- flowstatemcp-2.5.0/flowstate/migrations/001_auto_sessions.sql +16 -0
- flowstatemcp-2.5.0/flowstate/migrations/002_review_queue.sql +33 -0
- flowstatemcp-2.5.0/flowstate/migrations/003_links.sql +47 -0
- flowstatemcp-2.5.0/flowstate/migrations/004_review_mode.sql +24 -0
- flowstatemcp-2.5.0/flowstate/migrations/005_history.sql +21 -0
- flowstatemcp-2.5.0/flowstate/migrations/006_activity.sql +26 -0
- flowstatemcp-2.5.0/flowstate/migrations/007_meaning_index.sql +16 -0
- flowstatemcp-2.5.0/flowstate/migrations/008_git_capture.sql +12 -0
- flowstatemcp-2.5.0/flowstate/migrations/009_hard_rules.sql +13 -0
- flowstatemcp-2.5.0/flowstate/migrations/010_handoffs.sql +14 -0
- flowstatemcp-2.5.0/flowstate/migrations/011_record_meta.sql +13 -0
- flowstatemcp-2.5.0/flowstate/migrations/012_accountability.sql +71 -0
- flowstatemcp-2.5.0/flowstate/migrations/013_todo_claims.sql +36 -0
- flowstatemcp-2.5.0/flowstate/migrations/014_decision_conflicts.sql +41 -0
- flowstatemcp-2.5.0/flowstate/migrations/015_session_runs.sql +17 -0
- flowstatemcp-2.5.0/flowstate/migrations/016_write_receipts.sql +35 -0
- flowstatemcp-2.5.0/flowstate/migrations/017_run_views.sql +26 -0
- flowstatemcp-2.5.0/flowstate/migrations/018_run_views_upto.sql +4 -0
- flowstatemcp-2.5.0/flowstate/migrations/019_approvals.sql +47 -0
- flowstatemcp-2.5.0/flowstate/migrations/020_source_stamps.sql +10 -0
- flowstatemcp-2.5.0/flowstate/migrations/021_review_links.sql +11 -0
- flowstatemcp-2.5.0/flowstate/migrations/022_review_start_tickets.sql +10 -0
- flowstatemcp-2.5.0/flowstate/migrations/023_conflict_cards.sql +68 -0
- flowstatemcp-2.5.0/flowstate/migrations/024_rule_confirmations.sql +7 -0
- flowstatemcp-2.5.0/flowstate/migrations/025_confirmed_hash.sql +7 -0
- flowstatemcp-2.5.0/flowstate/migrations/__init__.py +149 -0
- flowstatemcp-2.5.0/flowstate/models.py +307 -0
- flowstatemcp-2.5.0/flowstate/names.py +157 -0
- flowstatemcp-2.5.0/flowstate/offsite.py +386 -0
- flowstatemcp-2.5.0/flowstate/offsite_crypto.py +272 -0
- flowstatemcp-2.5.0/flowstate/paths.py +265 -0
- flowstatemcp-2.5.0/flowstate/popup.py +238 -0
- flowstatemcp-2.5.0/flowstate/priority.py +66 -0
- flowstatemcp-2.5.0/flowstate/privatefs.py +476 -0
- flowstatemcp-2.5.0/flowstate/project_history.py +710 -0
- flowstatemcp-2.5.0/flowstate/provenance.py +696 -0
- flowstatemcp-2.5.0/flowstate/receipts.py +377 -0
- flowstatemcp-2.5.0/flowstate/relay/__init__.py +72 -0
- flowstatemcp-2.5.0/flowstate/relay/acme.py +271 -0
- flowstatemcp-2.5.0/flowstate/relay/api.py +164 -0
- flowstatemcp-2.5.0/flowstate/relay/cli.py +344 -0
- flowstatemcp-2.5.0/flowstate/relay/crypto.py +137 -0
- flowstatemcp-2.5.0/flowstate/relay/ct.py +118 -0
- flowstatemcp-2.5.0/flowstate/relay/runner.py +177 -0
- flowstatemcp-2.5.0/flowstate/relay/store.py +191 -0
- flowstatemcp-2.5.0/flowstate/relay/tunnel.py +413 -0
- flowstatemcp-2.5.0/flowstate/release_signers +14 -0
- flowstatemcp-2.5.0/flowstate/remote_auth.py +1219 -0
- flowstatemcp-2.5.0/flowstate/review.py +320 -0
- flowstatemcp-2.5.0/flowstate/review_apply.py +140 -0
- flowstatemcp-2.5.0/flowstate/review_card_ui.py +241 -0
- flowstatemcp-2.5.0/flowstate/review_cards.py +564 -0
- flowstatemcp-2.5.0/flowstate/review_mode.py +274 -0
- flowstatemcp-2.5.0/flowstate/review_page.py +1361 -0
- flowstatemcp-2.5.0/flowstate/review_phone.py +465 -0
- flowstatemcp-2.5.0/flowstate/review_tunnel.py +338 -0
- flowstatemcp-2.5.0/flowstate/routing.py +392 -0
- flowstatemcp-2.5.0/flowstate/rules.py +418 -0
- flowstatemcp-2.5.0/flowstate/safety.py +98 -0
- flowstatemcp-2.5.0/flowstate/saves.py +83 -0
- flowstatemcp-2.5.0/flowstate/scheduler.py +160 -0
- flowstatemcp-2.5.0/flowstate/schema.sql +713 -0
- flowstatemcp-2.5.0/flowstate/search.py +355 -0
- flowstatemcp-2.5.0/flowstate/secret_store.py +271 -0
- flowstatemcp-2.5.0/flowstate/secrets_scan.py +135 -0
- flowstatemcp-2.5.0/flowstate/selfupdate.py +1020 -0
- flowstatemcp-2.5.0/flowstate/server.py +1299 -0
- flowstatemcp-2.5.0/flowstate/session_contents.py +382 -0
- flowstatemcp-2.5.0/flowstate/session_records.py +629 -0
- flowstatemcp-2.5.0/flowstate/site_style.py +46 -0
- flowstatemcp-2.5.0/flowstate/stored.py +158 -0
- flowstatemcp-2.5.0/flowstate/story.py +442 -0
- flowstatemcp-2.5.0/flowstate/surface.py +173 -0
- flowstatemcp-2.5.0/flowstate/tasks.py +231 -0
- flowstatemcp-2.5.0/flowstate/term.py +40 -0
- flowstatemcp-2.5.0/flowstate/tool_labels.py +104 -0
- flowstatemcp-2.5.0/flowstate/tooldefs.py +383 -0
- flowstatemcp-2.5.0/flowstate/tools/__init__.py +118 -0
- flowstatemcp-2.5.0/flowstate/tools/core.py +545 -0
- flowstatemcp-2.5.0/flowstate/tools/files.py +416 -0
- flowstatemcp-2.5.0/flowstate/tools/git_ops.py +1081 -0
- flowstatemcp-2.5.0/flowstate/tools/intelligence.py +376 -0
- flowstatemcp-2.5.0/flowstate/tools/learning.py +282 -0
- flowstatemcp-2.5.0/flowstate/tools/problems.py +159 -0
- flowstatemcp-2.5.0/flowstate/tools/sessions.py +266 -0
- flowstatemcp-2.5.0/flowstate/tools/story.py +48 -0
- flowstatemcp-2.5.0/flowstate/tools/todos.py +79 -0
- flowstatemcp-2.5.0/flowstate/tools/utils.py +71 -0
- flowstatemcp-2.5.0/flowstate/tools/variables.py +345 -0
- flowstatemcp-2.5.0/flowstate/updates.py +482 -0
- flowstatemcp-2.5.0/flowstate/urls.py +7 -0
- flowstatemcp-2.5.0/flowstate/waiting.py +828 -0
- flowstatemcp-2.5.0/flowstate/who.py +302 -0
- flowstatemcp-2.5.0/pyproject.toml +62 -0
- flowstatemcp-2.5.0/requirements.txt +9 -0
- flowstatemcp-2.5.0/run_server.bat +2 -0
- flowstatemcp-2.5.0/run_server.sh +8 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# OS files
|
|
2
|
+
.DS_Store
|
|
3
|
+
Thumbs.db
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
*.egg
|
|
11
|
+
.venv/
|
|
12
|
+
.venv.pre-update/
|
|
13
|
+
venv/
|
|
14
|
+
|
|
15
|
+
# IDE
|
|
16
|
+
.idea/
|
|
17
|
+
.vscode/
|
|
18
|
+
*.swp
|
|
19
|
+
*.swo
|
|
20
|
+
|
|
21
|
+
# Environment / Secrets
|
|
22
|
+
.env
|
|
23
|
+
.env.local
|
|
24
|
+
*.env
|
|
25
|
+
|
|
26
|
+
# SQLite
|
|
27
|
+
*.db
|
|
28
|
+
*.db-journal
|
|
29
|
+
*.db-wal
|
|
30
|
+
*.db-shm
|
|
31
|
+
*.sqlite
|
|
32
|
+
*.sqlite-journal
|
|
33
|
+
*.sqlite-wal
|
|
34
|
+
*.sqlite-shm
|
|
35
|
+
|
|
36
|
+
# Backups
|
|
37
|
+
*.bak
|
|
38
|
+
*.backup.json
|
|
39
|
+
|
|
40
|
+
# Local test data and scratch (never commit DB copies)
|
|
41
|
+
testdata/
|
|
42
|
+
playground/
|
|
43
|
+
.pytest_cache/
|
|
44
|
+
|
|
45
|
+
# Local FlowState runtime files (never commit)
|
|
46
|
+
*.log
|
|
47
|
+
logs/
|
|
48
|
+
http-token.txt
|
|
49
|
+
*token*.txt
|
|
50
|
+
*.pem
|
|
51
|
+
*.key
|
|
52
|
+
|
|
53
|
+
# Relay agent state and other credentials (never commit)
|
|
54
|
+
/relay/
|
|
55
|
+
relay-state/
|
|
56
|
+
device-token*
|
|
57
|
+
acme-account*
|
|
58
|
+
*.p8
|
|
59
|
+
*.p12
|
|
60
|
+
*.pfx
|
|
61
|
+
master.key
|
|
62
|
+
*recovery*code*
|
|
63
|
+
*.crt
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: flowstatemcp
|
|
3
|
+
Version: 2.5.0
|
|
4
|
+
Summary: Local-first shared project memory for AI apps (MCP server)
|
|
5
|
+
Project-URL: Homepage, https://flowstatemcp.com
|
|
6
|
+
Project-URL: Repository, https://github.com/dialectforge/FlowState
|
|
7
|
+
Project-URL: Changelog, https://github.com/dialectforge/FlowState/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Hosted, https://flowstatemcp.com/hosted
|
|
9
|
+
Author: John Martin
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: ai,flowstatemcp,local-first,mcp,memory,model-context-protocol,sqlite
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: mcp<2,>=1.30
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: httpx2; extra == 'dev'
|
|
27
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
30
|
+
Provides-Extra: embeddings
|
|
31
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == 'embeddings'
|
|
32
|
+
Requires-Dist: sqlite-vec>=0.1.0; extra == 'embeddings'
|
|
33
|
+
Provides-Extra: offsite
|
|
34
|
+
Requires-Dist: cryptography>=42; extra == 'offsite'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# flowstatemcp
|
|
38
|
+
|
|
39
|
+
Local-first shared project memory for AI apps. One SQLite file on your computer holds the
|
|
40
|
+
todos, changes, decisions, problems, and lessons your AI apps share. The import package is
|
|
41
|
+
`flowstate`. The PyPI name is `flowstatemcp`, because `flowstate` is a different project.
|
|
42
|
+
|
|
43
|
+
- Site: https://flowstatemcp.com
|
|
44
|
+
- Hosted (no install): https://flowstatemcp.com/hosted
|
|
45
|
+
- Source: https://github.com/dialectforge/FlowState
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
You need Python 3.11+ and [uv](https://docs.astral.sh/uv/getting-started/installation/) for the
|
|
50
|
+
one-command form.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uvx flowstatemcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
That starts the stdio MCP server. These do the same thing:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pipx run flowstatemcp
|
|
60
|
+
pip install flowstatemcp
|
|
61
|
+
flowstatemcp
|
|
62
|
+
flowstate
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`pip install flowstate` does not install this project.
|
|
66
|
+
|
|
67
|
+
The git clone is the fallback when you want `setup_flowstate.py` to write app configs, schedule
|
|
68
|
+
backups, and install the FlowState skill. See the repository
|
|
69
|
+
[README](https://github.com/dialectforge/FlowState/blob/main/README.md).
|
|
70
|
+
|
|
71
|
+
## Run
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
flowstatemcp # stdio (what desktop apps start)
|
|
75
|
+
flowstate # the same server
|
|
76
|
+
python -m flowstate --transport http --host 127.0.0.1 --port 8766
|
|
77
|
+
flowstate --help
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The database is `FLOWSTATE_DB` if set, otherwise the per-OS default in the repository AGENTS.md.
|
|
81
|
+
|
|
82
|
+
A plain `pip install flowstatemcp` (or of a built wheel) includes the base schema
|
|
83
|
+
(`flowstate/schema.sql`), the migrations, and `release_signers`. `flowstate update` only works
|
|
84
|
+
in a git clone set up by `setup_flowstate.py`. Like every FlowState 2.4.0 or later, it checks
|
|
85
|
+
GitHub once a day for a newer release (nothing about you or your projects is sent);
|
|
86
|
+
`flowstate update check off` or `FLOWSTATE_UPDATE_CHECK=0` turns that off.
|
|
87
|
+
|
|
88
|
+
Search by meaning is optional; see "Honest limits" in the repository README.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# flowstatemcp
|
|
2
|
+
|
|
3
|
+
Local-first shared project memory for AI apps. One SQLite file on your computer holds the
|
|
4
|
+
todos, changes, decisions, problems, and lessons your AI apps share. The import package is
|
|
5
|
+
`flowstate`. The PyPI name is `flowstatemcp`, because `flowstate` is a different project.
|
|
6
|
+
|
|
7
|
+
- Site: https://flowstatemcp.com
|
|
8
|
+
- Hosted (no install): https://flowstatemcp.com/hosted
|
|
9
|
+
- Source: https://github.com/dialectforge/FlowState
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
You need Python 3.11+ and [uv](https://docs.astral.sh/uv/getting-started/installation/) for the
|
|
14
|
+
one-command form.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uvx flowstatemcp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That starts the stdio MCP server. These do the same thing:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pipx run flowstatemcp
|
|
24
|
+
pip install flowstatemcp
|
|
25
|
+
flowstatemcp
|
|
26
|
+
flowstate
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`pip install flowstate` does not install this project.
|
|
30
|
+
|
|
31
|
+
The git clone is the fallback when you want `setup_flowstate.py` to write app configs, schedule
|
|
32
|
+
backups, and install the FlowState skill. See the repository
|
|
33
|
+
[README](https://github.com/dialectforge/FlowState/blob/main/README.md).
|
|
34
|
+
|
|
35
|
+
## Run
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
flowstatemcp # stdio (what desktop apps start)
|
|
39
|
+
flowstate # the same server
|
|
40
|
+
python -m flowstate --transport http --host 127.0.0.1 --port 8766
|
|
41
|
+
flowstate --help
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The database is `FLOWSTATE_DB` if set, otherwise the per-OS default in the repository AGENTS.md.
|
|
45
|
+
|
|
46
|
+
A plain `pip install flowstatemcp` (or of a built wheel) includes the base schema
|
|
47
|
+
(`flowstate/schema.sql`), the migrations, and `release_signers`. `flowstate update` only works
|
|
48
|
+
in a git clone set up by `setup_flowstate.py`. Like every FlowState 2.4.0 or later, it checks
|
|
49
|
+
GitHub once a day for a newer release (nothing about you or your projects is sent);
|
|
50
|
+
`flowstate update check off` or `FLOWSTATE_UPDATE_CHECK=0` turns that off.
|
|
51
|
+
|
|
52
|
+
Search by meaning is optional; see "Honest limits" in the repository README.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""FlowState - Development memory system.
|
|
3
|
+
|
|
4
|
+
A project-centric memory system with:
|
|
5
|
+
- Hierarchical organization (Project -> Component -> Changes/Problems/Solutions)
|
|
6
|
+
- Decision tree tracking (what failed, what worked, why)
|
|
7
|
+
- Conversation logging (what prompts led to what outcomes)
|
|
8
|
+
- Semantic search with embeddings
|
|
9
|
+
- Story generation for project narratives
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "2.5.0"
|
|
13
|
+
|
|
14
|
+
from .database import Database, init_db
|
|
15
|
+
from .models import (
|
|
16
|
+
Project, Component, Change, Problem, SolutionAttempt,
|
|
17
|
+
Solution, Todo, Conversation, Learning, Session, ProjectContext
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Database",
|
|
22
|
+
"init_db",
|
|
23
|
+
"Project",
|
|
24
|
+
"Component",
|
|
25
|
+
"Change",
|
|
26
|
+
"Problem",
|
|
27
|
+
"SolutionAttempt",
|
|
28
|
+
"Solution",
|
|
29
|
+
"Todo",
|
|
30
|
+
"Conversation",
|
|
31
|
+
"Learning",
|
|
32
|
+
"Session",
|
|
33
|
+
"ProjectContext",
|
|
34
|
+
]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Which app did what, and a pause switch per app (v2 Phase D, migration 006).
|
|
2
|
+
|
|
3
|
+
Every tool call adds one row: time, app, tool, action, project id, ok or the error code,
|
|
4
|
+
and (migration 012) the model, machine and identity source (identity.py) plus the type and
|
|
5
|
+
id of a record the call created. Never the arguments or any text, so nothing secret can
|
|
6
|
+
land here. Rows older than
|
|
7
|
+
FLOWSTATE_ACTIVITY_DAYS (default 90) are pruned now and then.
|
|
8
|
+
|
|
9
|
+
The pause switch belongs to the user: it is set from the command line or the review page,
|
|
10
|
+
never through an MCP tool. A paused app can still read; its writes get "paused by the user".
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import random
|
|
15
|
+
import sqlite3
|
|
16
|
+
from datetime import datetime, timedelta, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from . import dbconn
|
|
21
|
+
from . import identity
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _now() -> str:
|
|
25
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _conn(db_path: Path) -> sqlite3.Connection:
|
|
29
|
+
conn = dbconn.connect(str(db_path), timeout=5.0)
|
|
30
|
+
conn.row_factory = sqlite3.Row
|
|
31
|
+
conn.execute("PRAGMA busy_timeout = 5000")
|
|
32
|
+
return conn
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def available(conn) -> bool:
|
|
36
|
+
return conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' "
|
|
37
|
+
"AND name='activity_log'").fetchone() is not None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def keep_days() -> int:
|
|
41
|
+
try:
|
|
42
|
+
return max(1, int(os.environ.get("FLOWSTATE_ACTIVITY_DAYS", "90")))
|
|
43
|
+
except ValueError:
|
|
44
|
+
return 90
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def record(db_path: Path, app: str, tool: str, action: Optional[str], project_id: Optional[int],
|
|
48
|
+
write: bool, ok: bool, error: Optional[str] = None, ms: Optional[int] = None,
|
|
49
|
+
caller: Optional[str] = None, record: Optional[tuple] = None) -> None:
|
|
50
|
+
"""One row per call, with the caller's model / machine / identity source (migration
|
|
51
|
+
012) and, for a write that created a record, its (record_type, id)."""
|
|
52
|
+
conn = _conn(db_path)
|
|
53
|
+
try:
|
|
54
|
+
if not available(conn):
|
|
55
|
+
return
|
|
56
|
+
row = {"at": _now(), "app": (app or "unknown")[:80],
|
|
57
|
+
"caller": (caller or "")[:80] or None, "tool": tool[:60],
|
|
58
|
+
"action": (action or None) and str(action)[:40], "project_id": project_id,
|
|
59
|
+
"write": int(bool(write)), "ok": int(bool(ok)),
|
|
60
|
+
"error": (error or None) and str(error)[:60], "ms": ms}
|
|
61
|
+
if record and identity.has_column(conn, "activity_log", "record_type"):
|
|
62
|
+
row["record_type"], row["record_id"] = str(record[0])[:20], int(record[1])
|
|
63
|
+
identity.insert(conn, "activity_log", row, app)
|
|
64
|
+
if random.random() < 0.01: # prune now and then
|
|
65
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=keep_days())).strftime(
|
|
66
|
+
"%Y-%m-%d %H:%M:%S")
|
|
67
|
+
conn.execute("DELETE FROM activity_log WHERE at < ?", (cutoff,))
|
|
68
|
+
conn.commit()
|
|
69
|
+
finally:
|
|
70
|
+
conn.close()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_paused(db_path: Path, app: str) -> bool:
|
|
74
|
+
conn = _conn(db_path)
|
|
75
|
+
try:
|
|
76
|
+
if not conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' "
|
|
77
|
+
"AND name='app_controls'").fetchone():
|
|
78
|
+
return False
|
|
79
|
+
row = conn.execute("SELECT paused FROM app_controls WHERE app = ?",
|
|
80
|
+
((app or "unknown")[:80],)).fetchone()
|
|
81
|
+
return bool(row and row[0])
|
|
82
|
+
finally:
|
|
83
|
+
conn.close()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def set_paused(db_path: Path, app: str, paused: bool, by: str) -> dict:
|
|
87
|
+
app = (app or "").strip()
|
|
88
|
+
if not app:
|
|
89
|
+
raise ValueError("name the app (as shown by `flowstate apps list`)")
|
|
90
|
+
conn = _conn(db_path)
|
|
91
|
+
try:
|
|
92
|
+
conn.execute("""INSERT INTO app_controls (app, paused, changed_at, changed_by)
|
|
93
|
+
VALUES (?, ?, ?, ?) ON CONFLICT(app) DO UPDATE SET paused = excluded.paused,
|
|
94
|
+
changed_at = excluded.changed_at, changed_by = excluded.changed_by""",
|
|
95
|
+
(app[:80], int(paused), _now(), by[:40]))
|
|
96
|
+
conn.commit()
|
|
97
|
+
return {"app": app, "paused": paused}
|
|
98
|
+
finally:
|
|
99
|
+
conn.close()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def pause_names(db_path: Path, app: str, paused: bool = True) -> list:
|
|
103
|
+
"""The names Pause / Resume on `app` set: the app's own name and every key, token or
|
|
104
|
+
sign-in it came in on (apps()' "via"). An app's name is its own claim, so pausing only
|
|
105
|
+
that name would let it come back as "Grok Bot 2" on the same key; the key's name it can't
|
|
106
|
+
change. Resume (paused=False) leaves out a key that another paused app also uses
|
|
107
|
+
(still_paused_by): resuming one app must not let the other dodge its pause by renaming
|
|
108
|
+
itself (audit N-F18)."""
|
|
109
|
+
app = (app or "").strip()
|
|
110
|
+
rows = apps(db_path)
|
|
111
|
+
row = next((a for a in rows if a["app"] == app), None)
|
|
112
|
+
names = [app] + [v for v in (row or {}).get("via", []) if v.casefold() != app.casefold()]
|
|
113
|
+
if paused:
|
|
114
|
+
return names
|
|
115
|
+
held = still_paused_by(db_path, app, rows)
|
|
116
|
+
return [n for n in names if n not in held]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def still_paused_by(db_path: Path, app: str, rows: Optional[list] = None) -> dict:
|
|
120
|
+
"""{key / sign-in name: [other apps]} for the keys `app` uses that stay paused when `app`
|
|
121
|
+
is resumed, because another app on the same key is paused by its own name."""
|
|
122
|
+
app = (app or "").strip()
|
|
123
|
+
rows = rows if rows is not None else apps(db_path)
|
|
124
|
+
row = next((a for a in rows if a["app"] == app), None)
|
|
125
|
+
mine = {v for v in (row or {}).get("via", []) if v.casefold() != app.casefold()}
|
|
126
|
+
if not mine:
|
|
127
|
+
return {}
|
|
128
|
+
own = _own_pauses(db_path)
|
|
129
|
+
held: dict = {}
|
|
130
|
+
for other in rows:
|
|
131
|
+
if other["app"] == app or not own.get(other["app"]):
|
|
132
|
+
continue
|
|
133
|
+
for v in mine.intersection(other.get("via") or []):
|
|
134
|
+
if v.casefold() != other["app"].casefold():
|
|
135
|
+
held.setdefault(v, []).append(other["app"])
|
|
136
|
+
return held
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _own_pauses(db_path: Path) -> dict:
|
|
140
|
+
"""{name: paused} as the user set it, name by name (no key inheritance)."""
|
|
141
|
+
conn = _conn(db_path)
|
|
142
|
+
try:
|
|
143
|
+
if not conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' "
|
|
144
|
+
"AND name='app_controls'").fetchone():
|
|
145
|
+
return {}
|
|
146
|
+
return {r["app"]: bool(r["paused"]) for r in conn.execute(
|
|
147
|
+
"SELECT app, paused FROM app_controls")}
|
|
148
|
+
finally:
|
|
149
|
+
conn.close()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def held_text(app: str, key: str, others: list) -> str:
|
|
153
|
+
return (f"{key!r} stays paused: {', '.join(repr(o) for o in others)} also use(s) it and "
|
|
154
|
+
f"{'is' if len(others) == 1 else 'are'} paused, so {app!r}'s writes through it are "
|
|
155
|
+
f"still refused. Resume {'that app' if len(others) == 1 else 'those apps'} too, or "
|
|
156
|
+
f"give {app!r} its own key or token.")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def set_paused_all(db_path: Path, app: str, paused: bool, by: str) -> list:
|
|
160
|
+
"""set_paused for each of pause_names(app, paused); returns the names."""
|
|
161
|
+
names = pause_names(db_path, app, paused)
|
|
162
|
+
for n in names:
|
|
163
|
+
set_paused(db_path, n, paused, by)
|
|
164
|
+
return names
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def apps(db_path: Path) -> list[dict]:
|
|
168
|
+
"""Every app seen (or paused), with last use and counts."""
|
|
169
|
+
conn = _conn(db_path)
|
|
170
|
+
try:
|
|
171
|
+
if not available(conn):
|
|
172
|
+
return []
|
|
173
|
+
rows = {r["app"]: dict(r) for r in conn.execute(
|
|
174
|
+
"""SELECT app, MAX(at) last_seen, COUNT(*) calls, SUM(write) writes,
|
|
175
|
+
SUM(CASE WHEN ok = 0 THEN 1 ELSE 0 END) errors
|
|
176
|
+
FROM activity_log GROUP BY app""")}
|
|
177
|
+
controls = {}
|
|
178
|
+
for r in conn.execute("SELECT app, paused, changed_at FROM app_controls"):
|
|
179
|
+
controls[r["app"]] = bool(r["paused"])
|
|
180
|
+
rows.setdefault(r["app"], {"app": r["app"], "last_seen": None, "calls": 0,
|
|
181
|
+
"writes": 0, "errors": 0})
|
|
182
|
+
rows[r["app"]]["paused"] = bool(r["paused"])
|
|
183
|
+
rows[r["app"]]["paused_changed_at"] = r["changed_at"]
|
|
184
|
+
if identity.has_column(conn, "activity_log", "identity_source"):
|
|
185
|
+
for r in conn.execute("SELECT DISTINCT app, machine, identity_source FROM activity_log "
|
|
186
|
+
"WHERE identity_source IN ('oauth_client', 'remote_token', 'app_key')"):
|
|
187
|
+
signed = identity.via(r["machine"], r["identity_source"])
|
|
188
|
+
if signed and r["app"] in rows and signed not in rows[r["app"]].setdefault("via", []):
|
|
189
|
+
rows[r["app"]]["via"].append(signed)
|
|
190
|
+
for v in rows.values():
|
|
191
|
+
v.setdefault("paused", False)
|
|
192
|
+
v["via"] = sorted(v.get("via", []))
|
|
193
|
+
# writes are refused when the key or sign-in it uses is paused too
|
|
194
|
+
v["paused"] = v["paused"] or any(controls.get(x) for x in v["via"])
|
|
195
|
+
return sorted(rows.values(), key=lambda v: v["last_seen"] or "", reverse=True)
|
|
196
|
+
finally:
|
|
197
|
+
conn.close()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def app_label(a: dict) -> str:
|
|
201
|
+
"""An apps() row for people: 'Grok Bot (via Cursor)' when it came in on keys, tokens or
|
|
202
|
+
sign-ins of another name (pausing either name stops it), else the name."""
|
|
203
|
+
others = [v for v in a.get("via") or [] if v.casefold() != (a["app"] or "").casefold()]
|
|
204
|
+
return f"{a['app']} (via {', '.join(others)})" if others else a["app"]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def recent(db_path: Path, app: Optional[str] = None, limit: int = 50) -> list[dict]:
|
|
208
|
+
conn = _conn(db_path)
|
|
209
|
+
try:
|
|
210
|
+
if not available(conn):
|
|
211
|
+
return []
|
|
212
|
+
who = ", ".join(f"a.{c}" if identity.has_column(conn, "activity_log", c) else f"NULL AS {c}"
|
|
213
|
+
for c in ("machine", "identity_source"))
|
|
214
|
+
sql = f"""SELECT a.at, a.app, a.tool, a.action, a.project_id, p.name project, a.write, a.ok,
|
|
215
|
+
a.error, {who} FROM activity_log a LEFT JOIN projects p ON p.id = a.project_id"""
|
|
216
|
+
params: list = []
|
|
217
|
+
if app:
|
|
218
|
+
sql += " WHERE a.app = ?"
|
|
219
|
+
params.append(app)
|
|
220
|
+
sql += " ORDER BY a.id DESC LIMIT ?"
|
|
221
|
+
params.append(max(1, min(int(limit), 500)))
|
|
222
|
+
return [dict(r) for r in conn.execute(sql, params)]
|
|
223
|
+
finally:
|
|
224
|
+
conn.close()
|