activity-frames 0.1.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.
- activity_frames-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +21 -0
- activity_frames-0.1.0/.github/ISSUE_TEMPLATE/parser_request.md +20 -0
- activity_frames-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +7 -0
- activity_frames-0.1.0/.github/workflows/test.yml +22 -0
- activity_frames-0.1.0/.gitignore +11 -0
- activity_frames-0.1.0/ACKNOWLEDGMENTS.md +8 -0
- activity_frames-0.1.0/CHANGELOG.md +39 -0
- activity_frames-0.1.0/CONTRIBUTING.md +55 -0
- activity_frames-0.1.0/LICENSE +21 -0
- activity_frames-0.1.0/PKG-INFO +157 -0
- activity_frames-0.1.0/README.md +126 -0
- activity_frames-0.1.0/SPEC.md +142 -0
- activity_frames-0.1.0/examples/README.md +40 -0
- activity_frames-0.1.0/examples/agent_context.py +38 -0
- activity_frames-0.1.0/examples/daily_standup.py +57 -0
- activity_frames-0.1.0/pyproject.toml +49 -0
- activity_frames-0.1.0/src/activity_frames/__init__.py +129 -0
- activity_frames-0.1.0/src/activity_frames/_time.py +86 -0
- activity_frames-0.1.0/src/activity_frames/capture.py +251 -0
- activity_frames-0.1.0/src/activity_frames/cli.py +155 -0
- activity_frames-0.1.0/src/activity_frames/db.py +86 -0
- activity_frames-0.1.0/src/activity_frames/emit.py +118 -0
- activity_frames-0.1.0/src/activity_frames/enrich.py +342 -0
- activity_frames-0.1.0/src/activity_frames/entities.py +416 -0
- activity_frames-0.1.0/src/activity_frames/frames.py +358 -0
- activity_frames-0.1.0/src/activity_frames/mcp_server.py +262 -0
- activity_frames-0.1.0/src/activity_frames/patterns.py +253 -0
- activity_frames-0.1.0/src/activity_frames/sessionize.py +356 -0
- activity_frames-0.1.0/tests/conftest.py +166 -0
- activity_frames-0.1.0/tests/test_enrich.py +83 -0
- activity_frames-0.1.0/tests/test_entities.py +144 -0
- activity_frames-0.1.0/tests/test_frames_emit.py +82 -0
- activity_frames-0.1.0/tests/test_mcp.py +70 -0
- activity_frames-0.1.0/tests/test_multidevice.py +118 -0
- activity_frames-0.1.0/tests/test_robustness.py +136 -0
- activity_frames-0.1.0/tests/test_sessionize.py +56 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Something compiled wrong or crashed
|
|
4
|
+
title: ""
|
|
5
|
+
labels: bug
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
**What happened:**
|
|
9
|
+
|
|
10
|
+
**What you expected:**
|
|
11
|
+
|
|
12
|
+
**Reproduce:**
|
|
13
|
+
- Command or API call:
|
|
14
|
+
- `aframes --version`:
|
|
15
|
+
- OS and Python version:
|
|
16
|
+
|
|
17
|
+
**Output** (redact anything sensitive - and note that `--include-text` is off by
|
|
18
|
+
default, so paste only what you are comfortable sharing):
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Site parser request
|
|
3
|
+
about: Ask for (or propose) typing support for a website
|
|
4
|
+
title: "Parser: <site>"
|
|
5
|
+
labels: parser
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
**Site**: <e.g. news.ycombinator.com>
|
|
9
|
+
|
|
10
|
+
**Example URLs and what they should become:**
|
|
11
|
+
|
|
12
|
+
| URL | kind | entity |
|
|
13
|
+
|-----|------|--------|
|
|
14
|
+
| https://news.ycombinator.com/item?id=123 | post | 123 |
|
|
15
|
+
| https://news.ycombinator.com/user?id=pg | profile | pg |
|
|
16
|
+
|
|
17
|
+
**Notes** (path structure, query params that matter, anything tricky):
|
|
18
|
+
|
|
19
|
+
Parsers are pure functions of the URL and take about 15 minutes to add - see
|
|
20
|
+
[CONTRIBUTING.md](../../CONTRIBUTING.md). PRs very welcome.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## What this changes
|
|
2
|
+
|
|
3
|
+
## Checklist
|
|
4
|
+
- [ ] Output stays deterministic (no clock, network, randomness, or model calls)
|
|
5
|
+
- [ ] New behavior has a test; `pytest -q` passes
|
|
6
|
+
- [ ] New schema fields, if any, are measured - or live in the tier-2 inferred block with a confidence tag
|
|
7
|
+
- [ ] No typed text or page content is emitted by default
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
os: [ubuntu-latest, macos-latest]
|
|
15
|
+
python-version: ["3.9", "3.11", "3.13"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- run: pip install -e ".[dev]"
|
|
22
|
+
- run: pytest -q
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Acknowledgments
|
|
2
|
+
|
|
3
|
+
The default capture engine provisioned by `aframes record` is a pinned
|
|
4
|
+
build of screenpipe v0.3.324 (Mediar AI), released under the MIT
|
|
5
|
+
license. It is downloaded on demand from the public npm registry and is
|
|
6
|
+
not bundled with this package. The compiler itself is engine-agnostic:
|
|
7
|
+
any capture system writing a compatible SQLite schema works via
|
|
8
|
+
`$AFRAMES_DB`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and versions follow semantic
|
|
5
|
+
versioning. The document schema version is tracked separately in [SPEC.md](SPEC.md).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-07-04
|
|
8
|
+
|
|
9
|
+
Initial release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Schema v1** ([SPEC.md](SPEC.md)): two-tier document format separating measured
|
|
13
|
+
fields from an optional confidence-tagged inferred tier, with coverage gaps,
|
|
14
|
+
blind spots, and evidence pointers as first-class elements.
|
|
15
|
+
- **Deterministic compiler**: dwell-capped sessionization (per capture device,
|
|
16
|
+
so multi-monitor streams never fragment each other), session-gap detection,
|
|
17
|
+
flicker merging with interruption records, and single-assignment input
|
|
18
|
+
accounting across overlapping monitor segments.
|
|
19
|
+
- **Enrichment library API** (`activity_frames.enrich`): nearest-frame app
|
|
20
|
+
attribution, coordinate-based click resolution against the recorded element
|
|
21
|
+
tree (with neighbor-frame rescue), and optional keyboard-layout decoding.
|
|
22
|
+
- **Entity typing**: deterministic URL parsers for LinkedIn, GitHub, Google
|
|
23
|
+
(Search/Docs/Gmail/Maps/Meet/Calendar), YouTube, X, Instagram, Reddit, Luma,
|
|
24
|
+
Partiful, Product Hunt, Vercel, Supabase, Stripe, Discord, Notion, Figma,
|
|
25
|
+
Stack Overflow, Calendly, and AI-chat sites, plus a subdomain/path heuristic
|
|
26
|
+
layer (sign-in, dashboard, email, calendar, meeting) and a total generic
|
|
27
|
+
fallback.
|
|
28
|
+
- **Built-in capture**: `aframes record` provisions and runs a pinned,
|
|
29
|
+
MIT-licensed screenpipe build, sha512-verified before first run (audio off
|
|
30
|
+
by default; `--status` checks that frames are actually flowing and points
|
|
31
|
+
at macOS permissions when they are not).
|
|
32
|
+
- **MCP server**: zero-dependency stdio JSON-RPC server exposing `get_context`,
|
|
33
|
+
`get_activity`, `get_day_summary`, and `get_patterns`.
|
|
34
|
+
- **CLI** (`aframes`): `record`, `today`, `day`, `context`, `apps`, `patterns`,
|
|
35
|
+
`mcp`, with JSON / YAML / Markdown / context-block output.
|
|
36
|
+
- **Python API** (`ActivityLog`) and workflow-pattern detection.
|
|
37
|
+
- Test suite (58 tests) and CI on macOS and Linux (Python 3.9, 3.11, 3.13).
|
|
38
|
+
|
|
39
|
+
[0.1.0]: https://github.com/nossa-y/activity-frames/releases/tag/v0.1.0
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for helping build a shared, honest representation of computer activity for agents.
|
|
4
|
+
|
|
5
|
+
## The easiest, highest-value contribution: a site parser
|
|
6
|
+
|
|
7
|
+
44% of a real browser history is niche sites. Every site parser you add makes activity frames sharper for everyone. A parser is a pure function of the URL - no network, no state, reviewable in one sitting.
|
|
8
|
+
|
|
9
|
+
1. Open `src/activity_frames/entities.py`.
|
|
10
|
+
2. Add a function that takes `(domain, parts, q)` and returns a `PageRef` (or `None` to fall through). `parts` is the path split on `/`; `q` is the parsed query dict.
|
|
11
|
+
3. Register it in `_SITE_PARSERS` by host.
|
|
12
|
+
4. Add a test in `tests/test_entities.py`.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
def _hackernews(domain, parts, q):
|
|
16
|
+
if parts and parts[0] == "item":
|
|
17
|
+
return PageRef(kind="post", domain=domain, entity=q.get("id", [None])[0])
|
|
18
|
+
if parts and parts[0] == "user":
|
|
19
|
+
return PageRef(kind="profile", domain=domain, entity=q.get("id", [None])[0])
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
# in _SITE_PARSERS:
|
|
23
|
+
"news.ycombinator.com": _hackernews,
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Rules for parsers
|
|
27
|
+
|
|
28
|
+
- **Deterministic.** Output must be a pure function of the URL. No clocks, no randomness, no fetching.
|
|
29
|
+
- **Total.** Return `None` for paths you do not handle so the fallback layers can type them; never raise.
|
|
30
|
+
- **Honest kinds.** A `kind` describes what a page *is* (`profile`, `repo`, `event`), never what the user was *doing* there (`prospecting`, `researching`). Intent is the agent's job, not the parser's. See [SPEC.md](SPEC.md) section 7.
|
|
31
|
+
- **No content.** Parse the URL only. Do not pull identifiers from page bodies.
|
|
32
|
+
|
|
33
|
+
## Other contributions
|
|
34
|
+
|
|
35
|
+
- **Capture sources.** The compiler is engine-agnostic. A new source needs an adapter that presents `frames`, `ui_events`, and `elements` tables (see `db.py` and [SPEC.md](SPEC.md) section 9).
|
|
36
|
+
- **Bug fixes and robustness.** Edge cases in timezones, multi-monitor coordinates, and malformed capture data are always welcome.
|
|
37
|
+
|
|
38
|
+
## Development
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/nossa-y/activity-frames
|
|
42
|
+
cd activity-frames
|
|
43
|
+
pip install -e ".[dev]"
|
|
44
|
+
pytest -q
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Everything is standard-library Python (PyYAML is the only optional extra). Keep it that way where you can: zero required dependencies is a feature.
|
|
48
|
+
|
|
49
|
+
## Ground rules
|
|
50
|
+
|
|
51
|
+
- Determinism is the core promise. A change that makes output depend on wall-clock time, network, or a model will be declined.
|
|
52
|
+
- Keep the measured tier measured. New fields must be derivable by code; anything inferred belongs in the tier-2 extension with a confidence tag.
|
|
53
|
+
- Add a test with every behavior change.
|
|
54
|
+
|
|
55
|
+
By contributing you agree your work is licensed under the project's MIT license.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nossa Iyamu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: activity-frames
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Episodic memory for AI agents: compile raw screen capture into structured, deterministic activity frames.
|
|
5
|
+
Project-URL: Homepage, https://github.com/nossa-y/activity-frames
|
|
6
|
+
Project-URL: Repository, https://github.com/nossa-y/activity-frames
|
|
7
|
+
Project-URL: Issues, https://github.com/nossa-y/activity-frames/issues
|
|
8
|
+
Author: Nossa Iyamu
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent-memory,ai-agents,context,episodic-memory,llm,mcp,screen-activity,screenpipe
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
27
|
+
Requires-Dist: pyyaml>=6.0; extra == 'dev'
|
|
28
|
+
Provides-Extra: yaml
|
|
29
|
+
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# activity-frames
|
|
33
|
+
|
|
34
|
+
[](https://github.com/nossa-y/activity-frames/actions/workflows/test.yml)
|
|
35
|
+
[](https://pypi.org/project/activity-frames/)
|
|
36
|
+
[](https://pypi.org/project/activity-frames/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
[](https://modelcontextprotocol.io)
|
|
39
|
+
|
|
40
|
+
**Episodic memory for AI agents.**
|
|
41
|
+
|
|
42
|
+
Your agent can read your code, search the web, and call APIs - but it has no idea what you have been doing for the last 8 hours. It starts every conversation blind.
|
|
43
|
+
|
|
44
|
+
activity-frames gives your agent eyes. It records your screen locally, compiles what it sees into structured **activity frames** (bounded episodes of what you actually did), and serves them to any agent over MCP. No cloud, no LLM in the pipeline, no guessing.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install activity-frames
|
|
48
|
+
aframes record # start capturing (local, audio off by default)
|
|
49
|
+
aframes context # your last 2 hours, agent-ready
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What your agent sees
|
|
53
|
+
|
|
54
|
+
Capture stores instants: thousands of snapshot rows a day, each one saying "at 22:53:05, Chrome showed linkedin.com/in/...". Useless to reason over.
|
|
55
|
+
|
|
56
|
+
activity-frames compiles those instants into episodes:
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
- id: f-0007
|
|
60
|
+
app: Google Chrome
|
|
61
|
+
site: linkedin.com
|
|
62
|
+
start: "20:24:04"
|
|
63
|
+
end: "20:42:11"
|
|
64
|
+
duration_min: 18.0
|
|
65
|
+
pages:
|
|
66
|
+
- {kind: people_search, entity: "cto paris", count: 2}
|
|
67
|
+
- {kind: profile, entity: najmuzzaman}
|
|
68
|
+
- {kind: company, entity: nexdotai}
|
|
69
|
+
input: {keys: 214, clicks: 31}
|
|
70
|
+
evidence: {frame_ids: "99871..100147"}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
And into a compact context block for any system prompt:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
USER ACTIVITY (2026-07-04, local time; measured from screen capture, no interpretation):
|
|
77
|
+
coverage: 09:12-18:47, 342 active min, 11 apps
|
|
78
|
+
away: 12:30-13:15 (45m)
|
|
79
|
+
- 09:12-09:58 Cursor (46.2m): main.py - api
|
|
80
|
+
- 10:01-10:44 Google Chrome/github.com (41.3m): pull_request:acme/api#412; code:acme/api
|
|
81
|
+
- 20:24-20:42 Google Chrome/linkedin.com (18.0m): people_search:cto paris x2; profile:najmuzzaman; company:nexdotai
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Drop that into a prompt and your agent knows your day. A full day compiles in under a second and costs zero tokens.
|
|
85
|
+
|
|
86
|
+
## Episodic memory, done honestly
|
|
87
|
+
|
|
88
|
+
Agent memory today means conversation memory: what you told the model. Episodic memory is what you actually *did* - and the hard part is representing it without lying.
|
|
89
|
+
|
|
90
|
+
activity-frames enforces a two-tier contract ([SPEC.md](SPEC.md)):
|
|
91
|
+
|
|
92
|
+
- **Tier 1, measured (this package):** everything is derivable by deterministic code from capture data. Sessions, durations, typed page entities, input volume, coverage gaps. Same input, same output, every time. There are no intent labels - code cannot know that 2 profile views + a people search was "prospecting". That is your agent's job; it is an LLM.
|
|
93
|
+
- **Tier 2, inferred (optional extension):** tools that add interpretation must namespace it, tag confidence (`high | medium | speculative`), and link evidence. Facts and guesses can never silently mix.
|
|
94
|
+
|
|
95
|
+
Every frame carries evidence pointers back to raw capture rows. Every document declares its blind spots. What the system did not see, it says it did not see.
|
|
96
|
+
|
|
97
|
+
## Use it from an agent (MCP)
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Claude Code
|
|
101
|
+
claude mcp add activity-frames -- aframes mcp
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Any MCP client works: command `aframes`, args `["mcp"]`. Four tools: `get_context`, `get_activity`, `get_day_summary`, `get_patterns` (repetitive-workflow detection: repeated clicks, URL loops, daily habits).
|
|
105
|
+
|
|
106
|
+
## Use it from Python
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from activity_frames import ActivityLog
|
|
110
|
+
|
|
111
|
+
log = ActivityLog()
|
|
112
|
+
doc = log.day() # today, structured
|
|
113
|
+
doc = log.recent(hours=2) # last 2 hours
|
|
114
|
+
print(log.context(hours=2)) # paste-ready context block
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Privacy model
|
|
118
|
+
|
|
119
|
+
- **Local only.** Capture, storage, and compilation all happen on your machine. Nothing is uploaded anywhere, ever.
|
|
120
|
+
- **Read-only compilation.** The compiler opens the capture database read-only.
|
|
121
|
+
- **Content opt-in at the output.** Compiled documents carry input *counts* by default; typed-text content appears only if you explicitly pass `--include-text` (this also gates the repeated-text pattern detector). Be clear about the boundary: the capture database itself does store what the recorder sees, locally, so protect it like any sensitive file (FileVault, permissions).
|
|
122
|
+
- **Audio off by default.** `aframes record --audio` to opt in.
|
|
123
|
+
- **No LLM in the compile path.** Compilation is plain code, so no language model, local or remote, is involved in producing memory. The capture engine does run on-device OCR to read what is on screen; that stays on your machine.
|
|
124
|
+
- **You choose what leaves**, when you paste a context block into an agent. Note that window titles and page entities originate from your screen and can contain third-party text; agents should treat them as data, not instructions.
|
|
125
|
+
|
|
126
|
+
## Architecture
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
capture engine compiler (this package) your agent
|
|
130
|
+
------------------ --------------------------- -----------------
|
|
131
|
+
screen snapshots --> sessionize (dwell, gaps, --> MCP tools /
|
|
132
|
+
accessibility tree flicker merge) context blocks /
|
|
133
|
+
input events entity typing (20+ sites) JSON, YAML, md
|
|
134
|
+
(local SQLite) enrichment, patterns
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The default capture engine is [screenpipe](https://github.com/mediar-ai/screenpipe): `aframes record` provisions a pinned, MIT-licensed build (v0.3.324), verifies its published sha512 before first run, and manages it for you (see [ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md)). Already running your own recorder? Point `$AFRAMES_DB` at any capture database with compatible `frames` / `ui_events` / `elements` tables and skip `aframes record` entirely.
|
|
138
|
+
|
|
139
|
+
## CLI
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
aframes record # start capture (--stop / --status / --audio)
|
|
143
|
+
aframes today # today's frames (YAML*)
|
|
144
|
+
aframes day 2026-07-03 -f json # any day, JSON
|
|
145
|
+
aframes context --hours 3 # agent context block
|
|
146
|
+
aframes apps # per-app time ledger
|
|
147
|
+
aframes patterns --days 7 # repetitive workflow detection
|
|
148
|
+
aframes mcp # MCP stdio server
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
*YAML output uses PyYAML (`pip install "activity-frames[yaml]"`); without it the CLI falls back to JSON.
|
|
152
|
+
|
|
153
|
+
## Status
|
|
154
|
+
|
|
155
|
+
v0.1. Developed and tested on macOS (Apple Silicon); Intel macOS and Linux x64 engine builds exist but are less exercised - reports welcome. Entity parsers cover LinkedIn, GitHub, Google (Search/Docs/Gmail/Maps/Meet/Calendar), YouTube, X, Instagram, Reddit, Luma, Partiful, Product Hunt, Vercel, Supabase, Stripe, Discord, Notion, Figma, Stack Overflow, Calendly, ChatGPT/Claude, localhost; unknown sites fall back to a generic page reference - always total, never lossy. Issues and parser PRs welcome.
|
|
156
|
+
|
|
157
|
+
Built by [Nossa Iyamu](https://github.com/nossa-y), maker of [Nocta](https://usenocta.com). MIT.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# activity-frames
|
|
2
|
+
|
|
3
|
+
[](https://github.com/nossa-y/activity-frames/actions/workflows/test.yml)
|
|
4
|
+
[](https://pypi.org/project/activity-frames/)
|
|
5
|
+
[](https://pypi.org/project/activity-frames/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://modelcontextprotocol.io)
|
|
8
|
+
|
|
9
|
+
**Episodic memory for AI agents.**
|
|
10
|
+
|
|
11
|
+
Your agent can read your code, search the web, and call APIs - but it has no idea what you have been doing for the last 8 hours. It starts every conversation blind.
|
|
12
|
+
|
|
13
|
+
activity-frames gives your agent eyes. It records your screen locally, compiles what it sees into structured **activity frames** (bounded episodes of what you actually did), and serves them to any agent over MCP. No cloud, no LLM in the pipeline, no guessing.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install activity-frames
|
|
17
|
+
aframes record # start capturing (local, audio off by default)
|
|
18
|
+
aframes context # your last 2 hours, agent-ready
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What your agent sees
|
|
22
|
+
|
|
23
|
+
Capture stores instants: thousands of snapshot rows a day, each one saying "at 22:53:05, Chrome showed linkedin.com/in/...". Useless to reason over.
|
|
24
|
+
|
|
25
|
+
activity-frames compiles those instants into episodes:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
- id: f-0007
|
|
29
|
+
app: Google Chrome
|
|
30
|
+
site: linkedin.com
|
|
31
|
+
start: "20:24:04"
|
|
32
|
+
end: "20:42:11"
|
|
33
|
+
duration_min: 18.0
|
|
34
|
+
pages:
|
|
35
|
+
- {kind: people_search, entity: "cto paris", count: 2}
|
|
36
|
+
- {kind: profile, entity: najmuzzaman}
|
|
37
|
+
- {kind: company, entity: nexdotai}
|
|
38
|
+
input: {keys: 214, clicks: 31}
|
|
39
|
+
evidence: {frame_ids: "99871..100147"}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
And into a compact context block for any system prompt:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
USER ACTIVITY (2026-07-04, local time; measured from screen capture, no interpretation):
|
|
46
|
+
coverage: 09:12-18:47, 342 active min, 11 apps
|
|
47
|
+
away: 12:30-13:15 (45m)
|
|
48
|
+
- 09:12-09:58 Cursor (46.2m): main.py - api
|
|
49
|
+
- 10:01-10:44 Google Chrome/github.com (41.3m): pull_request:acme/api#412; code:acme/api
|
|
50
|
+
- 20:24-20:42 Google Chrome/linkedin.com (18.0m): people_search:cto paris x2; profile:najmuzzaman; company:nexdotai
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Drop that into a prompt and your agent knows your day. A full day compiles in under a second and costs zero tokens.
|
|
54
|
+
|
|
55
|
+
## Episodic memory, done honestly
|
|
56
|
+
|
|
57
|
+
Agent memory today means conversation memory: what you told the model. Episodic memory is what you actually *did* - and the hard part is representing it without lying.
|
|
58
|
+
|
|
59
|
+
activity-frames enforces a two-tier contract ([SPEC.md](SPEC.md)):
|
|
60
|
+
|
|
61
|
+
- **Tier 1, measured (this package):** everything is derivable by deterministic code from capture data. Sessions, durations, typed page entities, input volume, coverage gaps. Same input, same output, every time. There are no intent labels - code cannot know that 2 profile views + a people search was "prospecting". That is your agent's job; it is an LLM.
|
|
62
|
+
- **Tier 2, inferred (optional extension):** tools that add interpretation must namespace it, tag confidence (`high | medium | speculative`), and link evidence. Facts and guesses can never silently mix.
|
|
63
|
+
|
|
64
|
+
Every frame carries evidence pointers back to raw capture rows. Every document declares its blind spots. What the system did not see, it says it did not see.
|
|
65
|
+
|
|
66
|
+
## Use it from an agent (MCP)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Claude Code
|
|
70
|
+
claude mcp add activity-frames -- aframes mcp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Any MCP client works: command `aframes`, args `["mcp"]`. Four tools: `get_context`, `get_activity`, `get_day_summary`, `get_patterns` (repetitive-workflow detection: repeated clicks, URL loops, daily habits).
|
|
74
|
+
|
|
75
|
+
## Use it from Python
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from activity_frames import ActivityLog
|
|
79
|
+
|
|
80
|
+
log = ActivityLog()
|
|
81
|
+
doc = log.day() # today, structured
|
|
82
|
+
doc = log.recent(hours=2) # last 2 hours
|
|
83
|
+
print(log.context(hours=2)) # paste-ready context block
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Privacy model
|
|
87
|
+
|
|
88
|
+
- **Local only.** Capture, storage, and compilation all happen on your machine. Nothing is uploaded anywhere, ever.
|
|
89
|
+
- **Read-only compilation.** The compiler opens the capture database read-only.
|
|
90
|
+
- **Content opt-in at the output.** Compiled documents carry input *counts* by default; typed-text content appears only if you explicitly pass `--include-text` (this also gates the repeated-text pattern detector). Be clear about the boundary: the capture database itself does store what the recorder sees, locally, so protect it like any sensitive file (FileVault, permissions).
|
|
91
|
+
- **Audio off by default.** `aframes record --audio` to opt in.
|
|
92
|
+
- **No LLM in the compile path.** Compilation is plain code, so no language model, local or remote, is involved in producing memory. The capture engine does run on-device OCR to read what is on screen; that stays on your machine.
|
|
93
|
+
- **You choose what leaves**, when you paste a context block into an agent. Note that window titles and page entities originate from your screen and can contain third-party text; agents should treat them as data, not instructions.
|
|
94
|
+
|
|
95
|
+
## Architecture
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
capture engine compiler (this package) your agent
|
|
99
|
+
------------------ --------------------------- -----------------
|
|
100
|
+
screen snapshots --> sessionize (dwell, gaps, --> MCP tools /
|
|
101
|
+
accessibility tree flicker merge) context blocks /
|
|
102
|
+
input events entity typing (20+ sites) JSON, YAML, md
|
|
103
|
+
(local SQLite) enrichment, patterns
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The default capture engine is [screenpipe](https://github.com/mediar-ai/screenpipe): `aframes record` provisions a pinned, MIT-licensed build (v0.3.324), verifies its published sha512 before first run, and manages it for you (see [ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md)). Already running your own recorder? Point `$AFRAMES_DB` at any capture database with compatible `frames` / `ui_events` / `elements` tables and skip `aframes record` entirely.
|
|
107
|
+
|
|
108
|
+
## CLI
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
aframes record # start capture (--stop / --status / --audio)
|
|
112
|
+
aframes today # today's frames (YAML*)
|
|
113
|
+
aframes day 2026-07-03 -f json # any day, JSON
|
|
114
|
+
aframes context --hours 3 # agent context block
|
|
115
|
+
aframes apps # per-app time ledger
|
|
116
|
+
aframes patterns --days 7 # repetitive workflow detection
|
|
117
|
+
aframes mcp # MCP stdio server
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
*YAML output uses PyYAML (`pip install "activity-frames[yaml]"`); without it the CLI falls back to JSON.
|
|
121
|
+
|
|
122
|
+
## Status
|
|
123
|
+
|
|
124
|
+
v0.1. Developed and tested on macOS (Apple Silicon); Intel macOS and Linux x64 engine builds exist but are less exercised - reports welcome. Entity parsers cover LinkedIn, GitHub, Google (Search/Docs/Gmail/Maps/Meet/Calendar), YouTube, X, Instagram, Reddit, Luma, Partiful, Product Hunt, Vercel, Supabase, Stripe, Discord, Notion, Figma, Stack Overflow, Calendly, ChatGPT/Claude, localhost; unknown sites fall back to a generic page reference - always total, never lossy. Issues and parser PRs welcome.
|
|
125
|
+
|
|
126
|
+
Built by [Nossa Iyamu](https://github.com/nossa-y), maker of [Nocta](https://usenocta.com). MIT.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Activity Frames Specification
|
|
2
|
+
|
|
3
|
+
**Version:** 1 (`schema_version: 1`)
|
|
4
|
+
**Status:** stable
|
|
5
|
+
**Purpose:** a standard, deterministic representation of human computer activity for consumption by AI agents.
|
|
6
|
+
|
|
7
|
+
## 1. Motivation
|
|
8
|
+
|
|
9
|
+
Screen recorders capture instants: thousands of snapshot rows per day, each one saying "at time T, app A showed window W at URL U". Agents cannot reason over that efficiently. They need bounded episodes: "the user was in app A on site S from T1 to T2, looked at these pages, typed this much."
|
|
10
|
+
|
|
11
|
+
This spec defines that episode format, and the rules that keep it trustworthy:
|
|
12
|
+
|
|
13
|
+
1. **Measured, not guessed.** Every field at this tier is derivable by deterministic code from recorder data. No intent labels, no summaries, no model output.
|
|
14
|
+
2. **Reproducible.** The same input database and window must always produce the same document.
|
|
15
|
+
3. **Evidenced.** Every frame points back to the raw rows it was compiled from.
|
|
16
|
+
4. **Honest about absence.** Coverage gaps and known blind spots are part of the document, not an implementation detail.
|
|
17
|
+
|
|
18
|
+
## 2. Document structure
|
|
19
|
+
|
|
20
|
+
A document is a single JSON/YAML object:
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
schema_version: 1
|
|
24
|
+
generated_at: "2026-07-04T21:14:03Z" # UTC, ISO-8601
|
|
25
|
+
source:
|
|
26
|
+
recorder: screenpipe # schema family of the capture DB
|
|
27
|
+
window:
|
|
28
|
+
start_utc: "2026-07-04T07:00:00"
|
|
29
|
+
end_utc: "2026-07-05T07:00:00"
|
|
30
|
+
day: "2026-07-04" # present when the window is a local day
|
|
31
|
+
coverage: { ... } # section 3
|
|
32
|
+
frames: [ ... ] # section 4
|
|
33
|
+
blind_spots: [ ... ] # section 6
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
All times inside `coverage` and `frames` are **local** wall-clock times (`HH:MM` or `HH:MM:SS`), because they describe a human's day. The `window` is UTC, because it describes a database query.
|
|
37
|
+
|
|
38
|
+
## 3. Coverage
|
|
39
|
+
|
|
40
|
+
What the recorder actually saw. Consumers must treat anything outside covered time as unknown, not as inactivity.
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
coverage:
|
|
44
|
+
first_activity: "09:12"
|
|
45
|
+
last_activity: "18:47"
|
|
46
|
+
active_minutes: 342 # distinct minutes with at least one frame
|
|
47
|
+
span_minutes: 575 # last - first
|
|
48
|
+
coverage_pct: 59 # active / span, capped at 100
|
|
49
|
+
frames_analyzed: 4211 # raw snapshot count in the window
|
|
50
|
+
distinct_apps: 11
|
|
51
|
+
gaps: # periods >= 5 min with no capture
|
|
52
|
+
- {start: "12:30", end: "13:15", minutes: 45}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 4. Frames
|
|
56
|
+
|
|
57
|
+
A frame is one bounded stretch of attention in a single context. The context key is `(app, site)`: the site is the URL host for browser activity, absent otherwise.
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
- id: f-0007 # stable within the document
|
|
61
|
+
app: "Google Chrome"
|
|
62
|
+
site: "linkedin.com" # omitted for non-browser apps
|
|
63
|
+
start: "20:24:04" # local
|
|
64
|
+
end: "20:42:11"
|
|
65
|
+
duration_min: 18.0 # ACTIVE minutes (dwell-based, see 5.1)
|
|
66
|
+
wall_min: 21.5 # end - start; emitted when it differs by > 1 min
|
|
67
|
+
windows: # up to 3 most-seen window titles
|
|
68
|
+
- "Search | LinkedIn"
|
|
69
|
+
pages: # typed page references, browser frames only
|
|
70
|
+
- {kind: people_search, entity: "cto paris", count: 2}
|
|
71
|
+
- {kind: profile, entity: "najmuzzaman"}
|
|
72
|
+
- {kind: company, entity: "nexdotai"}
|
|
73
|
+
input: # omitted when empty
|
|
74
|
+
keys: 214 # keystrokes + typed characters
|
|
75
|
+
clicks: 31
|
|
76
|
+
copies: 2
|
|
77
|
+
interruptions: # brief context switches folded in (see 5.3)
|
|
78
|
+
- {app: "Slack", seconds: 12} # site key present for browser flickers
|
|
79
|
+
evidence:
|
|
80
|
+
frame_ids: "99871..100147" # raw row id range in the source DB
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 4.1 Page references
|
|
84
|
+
|
|
85
|
+
A page reference types a URL into something an agent can read: `kind` (what sort of page), optional `entity` (the human-relevant identifier), optional `count` (views within the frame, emitted when > 1).
|
|
86
|
+
|
|
87
|
+
Standard kinds include: `profile`, `company`, `feed`, `messaging`, `people_search`, `search`, `repo`, `pull_request`, `issue`, `code`, `doc`, `sheet`, `email`, `video`, `channel`, `post`, `question`, `booking`, `design`, `ai_chat`, `local_dev`, `notifications`, and the fallback `page`. Kinds are open: producers may add site-specific kinds, but must keep them deterministic functions of the URL.
|
|
88
|
+
|
|
89
|
+
### 4.2 Input privacy rule
|
|
90
|
+
|
|
91
|
+
Input **volume** (counts) is part of the standard document. Input **content** (typed text) must be excluded by default and only included on an explicit opt-in from the operator of the producing tool.
|
|
92
|
+
|
|
93
|
+
## 5. Determinism rules
|
|
94
|
+
|
|
95
|
+
Implementations must produce identical documents for identical inputs. The reference constants:
|
|
96
|
+
|
|
97
|
+
### 5.1 Dwell
|
|
98
|
+
Capture is event-driven: a frame is stored when the screen changes. A frame contributes `min(gap_to_next_frame, 90s)` of active time. The cap prevents a static screen (or an absent user) from earning unbounded credit. `duration_min` is the sum of dwells inside the frame.
|
|
99
|
+
|
|
100
|
+
### 5.2 Session gap
|
|
101
|
+
A gap larger than `300s` between consecutive raw frames closes the current activity frame and is a candidate coverage gap (reported when >= 5 min).
|
|
102
|
+
|
|
103
|
+
### 5.3 Flicker merge
|
|
104
|
+
The sequence A -> B -> A, where B lasts at most `20s` of wall time and no session gap intervenes on either side of B, collapses into a single A frame. B is recorded in `interruptions` with its measured seconds. B's time is **not** added to A's `duration_min`. Nothing is silently dropped.
|
|
105
|
+
|
|
106
|
+
### 5.4 Minimum duration
|
|
107
|
+
Producers may offer a `min_minutes` filter for consumer convenience. Filtered-out frames are omitted with the count disclosed in the document (`omitted: {below_min_minutes: N, min_minutes: X}`); they must never be silently merged into neighbors.
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
### 5.5 Capture devices
|
|
111
|
+
|
|
112
|
+
Each monitor records its own frame stream (`device_name`). Segmentation runs per device, so simultaneous monitors do not fragment each other's frames; the document lists all devices' frames sorted by start time, which means frames MAY overlap in time. Input events are assigned to exactly one containing segment (ties across devices resolved by the nearest captured frame), so input volume is never double-counted. A consequence to disclose: the same app visible on two monitors at once earns active time on both.
|
|
113
|
+
|
|
114
|
+
## 6. Blind spots
|
|
115
|
+
|
|
116
|
+
Every document carries a `blind_spots` list: plain-language statements of what the capture pipeline systematically cannot see (e.g. "browser URLs are only captured for browser apps"). Producers must not remove entries to make output look more complete.
|
|
117
|
+
|
|
118
|
+
## 7. Tier 2: inferred fields (extension, not part of this package)
|
|
119
|
+
|
|
120
|
+
Tools MAY extend documents with interpretation: task labels, project clusters, meeting summaries. Such fields are **inference**, and the spec requires them to be:
|
|
121
|
+
|
|
122
|
+
- namespaced under `inferred` (at document or frame level),
|
|
123
|
+
- tagged with `confidence: high | medium | speculative`,
|
|
124
|
+
- tagged with `evidence`: which measured fields or raw rows support the inference.
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
inferred:
|
|
128
|
+
- label: "LinkedIn prospecting"
|
|
129
|
+
frames: [f-0007, f-0009]
|
|
130
|
+
confidence: medium
|
|
131
|
+
evidence: "people_search + 2 profile views + 1 company page in 18 min"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A consumer must always be able to strip everything under `inferred` and be left with a purely measured document. This package emits tier 1 only.
|
|
135
|
+
|
|
136
|
+
## 8. Versioning
|
|
137
|
+
|
|
138
|
+
`schema_version` increments only on breaking changes to tier-1 field semantics. Additive fields do not bump the version. Consumers should ignore unknown fields.
|
|
139
|
+
|
|
140
|
+
## 9. Sources
|
|
141
|
+
|
|
142
|
+
The reference producer reads the local SQLite database written by the built-in capture engine (screenpipe, provisioned by `aframes record`). Required surface: a `frames` table (`id`, `timestamp` UTC ISO, `app_name`, `window_name`, `browser_url`, `focused`; `device_name` optional, treated as one stream when absent), and optionally `ui_events` (input volume) and `elements` (click resolution) - the compiler degrades gracefully when the optional tables are missing. Any capture system can be supported by a producer that satisfies sections 2 through 6; `source.recorder` identifies the schema family.
|