Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ Thumbs.db
ehthumbs.db
.DS_Store
desktop.ini

# Serena agent artifacts
.serena/
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,31 @@

## Unreleased

## 2.1.0

**Meetings, on your own time.** Flowkey can now read your local Quill meetings and answer questions about them on the local model — and an after-hours scheduler pre-computes each meeting's digest (summary / goals / action items) during your idle window, so daytime reads are instant.

### Added

- **Meetings (Quill integration) + after-hours digest processing.** Flowkey can connect to the local [Quill](https://quillapp.com) note-taking app over MCP to search your meetings and answer questions about them — entirely on the local model. Because asking a model about a full transcript costs real prefill time (~15–17 s of time-to-first-token for a ~7k-token transcript on the NPU), a background **scheduler** pre-computes a digest (summary / goals / action items) for each meeting during a configurable idle window (default 17:00–21:00, only when the machine has been idle), caching it in `data/meeting_digests.jsonl` so daytime reads are instant. New **Meetings** dashboard tab (search → read cached digest, "Process now", or "Ask about this meeting") and a Config card for all the settings (enable, Quill MCP URL, content source, schedule window, idle gating, max-per-run) with a "Run batch now" button. Off by default (opt-in). New modules `ffp_quill` (stdlib MCP-over-HTTP client) and `ffp_meetings` (digest store, batch worker, scheduler logic, idle detection); new config block `meetings`; new daemon actions `quill_status` / `quill_search_meetings` / `meeting_digest_get` / `meeting_digests_list` / `meeting_process` / `meeting_batch_run` / `meeting_batch_status` / `meeting_ask`. The Quill MCP URL is validated loopback-only; the meeting actions write a separate cache file under their own lock, so a long after-hours batch never blocks config saves or notifications.
- **Action-item review board.** A weekly/monthly board on the Meetings tab aggregates the action items parsed from your meeting digests, each markable **accepted / rejected / pending** (status persisted in `data/meeting_action_status.jsonl`). New daemon actions `meeting_actions_list` / `meeting_action_set_status`.
- **Weekly review.** A one-click roll-up of the week's processed meetings (highlights / themes / open items) generated on the local model from the cached digests — pick the week and Generate. New daemon action `meeting_week_summary`.
- **Meeting hours on the Overview tab** — today / this-week meeting counts and hours, pulled from Quill (new daemon action `meeting_overview`).
- **Digest quality flags + strict re-digest.** Each meeting digest is checked for low-substance / social-filler / too-short / trivial-meeting signals and flagged in the Meetings tab; a "Re-digest (strict)" button re-runs the summary with a stricter prompt. New daemon action `meeting_redigest`.

### Fixed

- Benchmark history no longer shows a blank row for an interrupted/empty run.
- "Run batch now" persists the current Meetings settings (including the Enable toggle) before running, so it reflects the form rather than the last-saved config.
- The Meetings results list is now scrollable and shows a meeting counter.
- **Packaging now ships all runtime modules.** `ffp_meetings`, `ffp_notifications`, and `ffp_quill` were missing from `pyproject.toml` `py-modules` and the PyInstaller spec `hiddenimports`, so a wheel / frozen installer could omit them and crash on import even though source-tree tests passed. All three are now declared, and a new test (`test_packaging_modules`) asserts `py-modules` and the spec stay in sync with `scripts/*.py`.
- The Telemetry time-of-day chart now renders only **active hours** — zero-activity hours are dropped instead of drawn as empty bars (and an empty history shows "No activity yet").

### Internal

- Removed the unused `ffp_tools.py` tool-calling prototype (no runtime caller).
- Unified the config seed templates: the dev example (`config/`) and the shipped first-run seed (`setup/defaults/`) had drifted (the seed was missing the `llm` block) — they're now identical, enforced by `test_config_seeds`.
- Docs refreshed for the current UI/build: README dashboard tabs (Chat + Meetings), the `open_chat` hotkey (`Ctrl+Alt+C`), a new "Supported surfaces" section (web chat only, installer-vs-source install, per-user autostart), and the installer README (3 exes, dropped retired `ffp-chat.exe` references).

## 2.0.0

Expand Down
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ Flowkey is a Windows desktop assistant that adds local-LLM hotkeys for grammar f

Everything runs locally through [FastFlowLM](https://fastflowlm.com) (AMD Ryzen AI NPU) or, on machines without the NPU, through [Ollama](https://ollama.com) (CPU/GPU) as a secondary provider. No cloud service, analytics, or telemetry is used by the app.

Current version: `2.0.0`
Current version: `2.1.0`

## What's new in 2.1

- **Meetings, on your own time.** Connect the local [Quill](https://quillapp.com) note app to search your meetings and ask about them on the local model. Because asking about a full transcript costs real prefill time on the NPU, an **after-hours scheduler** pre-computes each meeting's digest (summary / goals / action items) during a configurable idle window (default 17:00–21:00) so daytime reads are instant. New **Meetings** tab + Config card; off by default.

## What's new in 2.0

Expand Down Expand Up @@ -98,7 +102,7 @@ Launch the app with AutoHotkey v2:
| `explain:` + `Ctrl+Shift+G` | Explain code, regex, SQL, or technical text |
| `tone:` + `Ctrl+Shift+G` | Rewrite in the selected tone preset |
| `<custom>:` + `Ctrl+Shift+G` | Any mode you define in Dashboard → Config → Custom modes (e.g. `translate:`) |
| `Ctrl+Shift+T` | Open chat (each tab has a "My notes" toggle that grounds replies in your notes vault) |
| `Ctrl+Alt+C` | Open chat (the Chat tab has a "My notes" toggle that grounds replies in your notes vault) |
| `Ctrl+Shift+A` | Ask in chat with selected text |
| `Ctrl+Alt+N` | Capture a note |

Expand All @@ -108,12 +112,20 @@ Prefix tip: put the keyword on the first line of your selection — `prompt: rou

The dashboard is a web page served by the local daemon — open it from the tray menu ("Dashboard") or browse to `http://127.0.0.1:52650/`. It is loopback-only and works in any browser.

- **Tabs:** Overview, History, Telemetry, Notes, Benchmark, Config.
- **Tabs:** Overview, Chat, Telemetry, History, Notes, Meetings, Config.
- **Theme:** auto-follows your OS day/night setting; the topbar button cycles auto → light → dark.
- **Custom modes:** Config → Custom modes lets you add your own `prefix:` commands (id + system prompt). Changes apply to the running app within a second.
- **Models:** pull models with live progress — pick a suggestion or type any name (on Ollama, anything from the [library](https://ollama.com/library) works); set active, remove. Suggestions are hardware-aware: detected RAM/VRAM caps the model size (e.g. 32 GB RAM → ~4B on the NPU; 8 GB VRAM → ~9B on the GPU), oversized models are hidden, and free-typing one asks before pulling.
- **Benchmark:** works on both providers — `flm bench` on FastFlowLM (~10–20 min, NPU), timed generations with native metrics on Ollama (~1–3 min, server keeps running).
- **Notes:** browse or search your vault; History shows recent runs (text is stored only if history storage is enabled).
- **Meetings:** connect the local [Quill](https://quillapp.com) app to search meetings, read AI digests (pre-computed after-hours), review action items (accept / reject), and generate a weekly review. Off by default — enable in Config → Meetings.
- **Notifications:** per-event toggles, dedupe window, Do-Not-Disturb, and quiet hours; every toast (shown or muted) is logged to the Telemetry feed.

## Supported surfaces

- **Chat is web-only.** Chat lives in the dashboard's **Chat** tab (`Ctrl+Alt+C` or tray → "Open Chat"). The old standalone modal chat popup is retired — there is no separate chat window.
- **Two install paths.** The signed **Inno Setup installer** (`Flowkey-Setup-<version>.exe`, per-machine, admin) *or* a **source install** (`INSTALL.cmd`, runs from an unzipped folder — no build, no signing). Both are supported; pick one.
- **Autostart is per-user.** "Launch Flowkey when I sign in" is a single per-user `HKCU\…\Run` entry the daemon manages from Dashboard → Config — that toggle is the source of truth. The installer doesn't add machine-wide autostart.

## Project Layout

Expand Down
106 changes: 106 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Flowkey SPEC

Caveman-encoded (compression, not amputation). Paths / ids / action names / numbers / endpoints verbatim.

## §G goals

- G1: local LLM hotkey assistant, Windows. ⊥ cloud, ⊥ analytics, ⊥ telemetry off-machine.
- G2: run on AMD NPU (FastFlowLM) | any CPU/GPU (Ollama). provider ? → auto-fallback to other.
- G3: web dashboard = single home for chat, notes, meetings, config, benchmark, notifications.
- G4: heavy LLM cost (prefill) → pre-compute after-hours, read cached.

## §C context / stack

- front: AHK v2 — `scripts/grammarFix.ahk` + `scripts/lib/*` + `scripts/ui/*`
- daemon: Python stdlib — `scripts/ffp_daemon.py` @ `http://127.0.0.1:52650` (single-instance = bound port)
- LLM: FastFlowLM NPU @ `:52625` | Ollama @ `:11434`, OpenAI-compat `POST /v1/chat/completions`
- dashboard: daemon-served `scripts/ui/web/{index.html,app.js,styles.css}`, CSP `default-src 'self'`
- paths: `scripts/paths.py` → USER_ROOT/{config,data,logs}; `_version.py` = version src of truth
- version: current `2.1.0` (held `release/v2.1.0`); public `2.0.0`; repo `agr77one/Fastflow`
- run tree = `flowkey-pub2` (worktree, branch `live`=origin/main). old `FastFlowPrompt_Local_Setup`=1.5.0 stale.

## §I interfaces

- cfg blocks: `enabled`, `llm`, `providers.{fastflowlm,ollama}`, `server`, `routing`, `notes`, `chat`, `modes`, `dictionary`, `notifications`, `meetings`, `hotkeys`
- api: `POST /action/<name>` ! header `X-FFP-API: 1` → 200 `{ok,result,error,elapsed_ms}`
- api: `GET /` → dashboard; `GET /healthz` → `{ok,version,api,actions}`
- action: `config_snapshot` → full cfg; `apply_config_patch {patch}` → merge (whitelist `filter_config_patch`)
- action: `notify_gate {title,message}` → `{show,reason,category}` (logs); `notifications_log {limit}` → rows
- action: `quill_status` → `{reachable,enabled,server,server_version}`
- action: `quill_search_meetings {query,limit}` → `{meetings:[{id,title,date,duration,participants,url}]}`
- action: `meeting_overview` → `{enabled,reachable,today:{count,minutes},week:{count,minutes}}`
- action: `meeting_process {meeting_id,title,date,url}` → digest rec (writes `meeting_digests.jsonl`)
- action: `meeting_batch_run {max_per_run?}` → `{ok,processed,queued,errors}`; `meeting_batch_status` → status
- action: `meeting_digest_get {meeting_id}` → `{found,digest_md,...}`; `meeting_digests_list` → `{digests,count}`
- action: `meeting_ask {meeting_id,question}` → `{ok,answer,source,seconds}`
- action: `meeting_actions_list {range:week|month}` → `{range,items:[{id,text,owner,status,...}],counts}`
- action: `meeting_action_set_status {id,status:pending|accepted|rejected}` → `{ok}`
- action: `meeting_week_summary {week_offset}` → `{ok,week_label,meeting_count,summary}`
- mcp: Quill @ `http://127.0.0.1:19532/mcp` — Streamable-HTTP, SSE `data:`, `Mcp-Session-Id` header; init→notifications/initialized→tools/call
- cmd: `flm serve <model> --pmode turbo --host 127.0.0.1 --port 52625`
- data: `data/{meeting_digests,meeting_action_status,notifications,chat_threads}.jsonl`
- autostart: HKCU Run `FastFlowPrompt` → bundled `AutoHotkey64.exe` + `grammarFix.ahk`; `FlowkeyGitSync` → `sync.ps1`
- sched: Windows task `FlowkeyGitSync` daily 12:00 → `sync.ps1` (ff-only pull, guarded)
- ACTIONS count = 73

## §V invariants

- V1: ∀ `POST /action` → header `X-FFP-API` = API_VERSION `1` | 403
- V2: ∀ req Host ∉ {`127.0.0.1`,`localhost`} → 403 (DNS-rebind defense)
- V3: config patch → only keys ∈ `filter_config_patch` whitelist; rest dropped
- V4: flm/llm/`meetings.mcp_url` ! loopback http/https | reject (SSRF guard)
- V5: dashboard DOM → createElement/textContent only; ⊥ innerHTML; ⊥ native alert/confirm/prompt → use `confirmDialog`
- V6: history text redacted by default (`history_store_text` false)
- V7: notes ops ! contained via `_vault_subpath`; `../` → reject
- V8: notify → daemon `notify_gate` decides+logs; AHK `Notify_Impl` fail-OPEN if daemon unreachable
- V9: notify category `errors` (critical) → bypass DND & quiet_hours; still logged; still honors per-cat disable
- V10: notify master `enabled`=false → mute all incl errors
- V11: scheduler run ⟺ `should_run_batch` = meetings.enabled & in-window & (idle ? idle≥threshold)
- V12: after-hours batch idempotent → skip meeting ∃ cached digest
- V13: `meeting_*` actions ∉ `_WRITE_ACTIONS` → self-lock (`_io_lock`/`_batch_lock`), separate files ∴ long batch ⊥ block config/notify writes
- V14: NPU prefill ∝ context (~17s @ ~7k tok) ∴ pre-compute digests after-hours; ask grounds on cached digest
- V15: action-item id = `sha1(meeting_id|norm(text))[:16]` → stable across re-list ∴ status persists
- V16: week = Monday 00:00 local; month = 1st 00:00 local
- V17: builtin mode prompts locked from patching (only `tone.preset` patchable)
- V18: version ∀ ∈ {`_version.py`,`pyproject.toml`,`installer/installer.iss`,`README.md`} equal; CI smoke fails on drift
- V19: `main` branch-protected (ruleset 17344133) → land via PR + ruleset toggle; ⊥ direct push
- V20: change gates ! pass: `ruff check scripts tests`, `pytest`, `node --check scripts/ui/web/app.js`, AHK parse-check (PowerShell `/ErrorStdOut`)
- V21: local data (config/data/logs/vendor/certs) ∈ `.gitignore` ∴ pull moves code only, never user data
- V22: `sync.ps1` ∃ uncommitted tracked changes → skip pull (⊥ clobber un-pushed WIP)

## §T tasks

```
id|status|task|cites
T1|x|web dashboard = home (chat/notes/config/bench/notifications/meetings)|V5
T2|x|Ollama provider + auto-fallback + hw-aware model sizing|G2
T3|x|notifications gate+log+panel (`ffp_notifications`)|V8,V9,V10
T4|x|Quill meetings + after-hours digest scheduler (`ffp_quill`,`ffp_meetings`)|V11,V12,V14
T5|x|overview meeting hours + action-item board + weekly review|V15,V16
T6|x|git autosync: `sync.ps1` + daily task + autostart→flowkey-pub2|V21,V22
T7|~|2.1.0 release held on `release/v2.1.0` → land after user test|V18,V19
T8|.|installer clean-VM smoke test|—
T9|.|[AUDIT] dead-code: unused daemon helper + 2 AHK wrappers + stale chat-popup config key + obsolete settings ref in test fixture + deprecated install shims|—
T10|.|[AUDIT-P1] autostart: unify 3 divergent Run keys (daemon/src-installer/pkg-installer) → single HKCU entry; fix UI autostart status reporting|V20
T11|.|[AUDIT] old open_chat default `^+t` still appears in first-run + web config fallback → replace with `^!c`|B5
T12|.|[AUDIT] first-run seed thinner than DEFAULT_CONFIG schema → add seed-vs-schema drift guard (compare keys on first-run copy)|—
T13|.|[AUDIT] installer bootstrap wrapper hardcodes old installer filename → derive from `_version.py`|V18
T14|.|[AUDIT] quality-gate gaps: installer policy drift, autostart reg-name drift, bootstrap output name, README/dashboard tab count|V20
T15|.|[DOCS] dashboard docs: 7 tabs listed, live = 8 (add Benchmark)|—
T16|.|[DOCS] autostart docs conflict: main says no machine-wide entry; installer docs+impl still describe it → align on HKCU-only|—
T17|.|[DOCS] installer layout: build script says flattened, installer.md still shows nested layout|—
T18|.|[DOCS] provider roadmap marks selector/status UX incomplete → update to reflect it exists|—
T19|.|[DOCS] first-run wizard text: "chat popup" + retired hotkey → update to current|B5
T20|.|[DOCS] daemon log location stale in docs → update to current path|—
```

## §B bugs

```
id|date|cause|fix
B1|2026-06|"Run batch now" ran vs last-saved cfg not form → "disabled"|autosave meetings patch before `meeting_batch_run`
B2|2026-06|bench history blank row from 0-point result file|skip `rows==[]` in `ffp_benchmark.history`
B3|2026-06|autostart → stale tree / empty `flowkey-public`|repoint HKCU Run → flowkey-pub2 + bundled AHK
B4|2026-06|install launch: AHK called `.py`, shipped only `.exe`|flatten bundle to {app} + AHK→exe bridge (PR #19)
B5|2026-06|`Ctrl+Shift+T` open_chat collided w/ browser reopen-tab|default → `^!c`; tray label = configured hotkey
```
9 changes: 4 additions & 5 deletions installer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Everything needed to turn this project into a signed Windows installer that non-
```
installer/
├── build.ps1 ← end-to-end orchestrator (run this)
├── fastflowprompt.spec ← PyInstaller spec (4 exes, onedir, MERGE-dedup)
├── fastflowprompt.spec ← PyInstaller spec (3 exes, onedir, MERGE-dedup)
├── installer.iss ← Inno Setup 6.x script (per-machine, admin)
├── sign.ps1 ← cert generation + signtool wrapper
├── certs/ ← .pfx and .cer (gitignored)
Expand Down Expand Up @@ -66,12 +66,12 @@ SmartScreen on first launch.

Steps the script runs:

1. Read `scripts\_version.py` → derive version (e.g. `2.0.0`)
1. Read `scripts\_version.py` → derive version (e.g. `2.1.0`)
2. Generate `file_version_info.txt` for the Win32 VERSIONINFO resource
3. Download `vendor\ahk\AutoHotkey64.exe` if missing (`-BundleAhk`)
4. Download `vendor\flm\flm-setup.exe` if missing (`-BundleFlm`)
5. Run `pyinstaller --clean --noconfirm fastflowprompt.spec` → `dist\FastFlowPrompt\`
6. Run `iscc installer.iss` → `out\Flowkey-Setup-2.0.0.exe`
6. Run `iscc installer.iss` → `out\Flowkey-Setup-2.1.0.exe`
7. Run `sign.ps1` against the output (`-Sign`)

Debug flags:
Expand All @@ -93,7 +93,7 @@ Debug flags:

## What the uninstaller does

1. Kills `ffp-daemon.exe`, `ffp-chat.exe`, and the AHK process running
1. Kills `ffp-daemon.exe` and the AHK process running
`grammarFix.ahk` so file removal doesn't fail on in-use binaries.
2. Chain-uninstalls FastFlowLM via its `QuietUninstallString` — but only if
we set the `.flm_installed_by_us` marker. Users who already had FLM keep
Expand Down Expand Up @@ -128,7 +128,6 @@ C:\Program Files\FastFlowPrompt\ (read-only, admin-installed)
├── Flowkey\ PyInstaller bundle
│ ├── ffp-daemon.exe
│ ├── ffp-grammar-fix.exe
│ ├── ffp-chat.exe
│ ├── ffp-first-run.exe
│ ├── _internal\
│ └── setup\defaults\
Expand Down
8 changes: 5 additions & 3 deletions installer/fastflowprompt.spec
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- mode: python ; coding: utf-8 -*-
"""PyInstaller spec for Flowkey v1.5.0.
"""PyInstaller spec for Flowkey (version resolved from scripts/_version.py).

Build:

Expand Down Expand Up @@ -53,20 +53,22 @@ HIDDEN_IMPORTS = [
"ffp_benchmark",
"ffp_chat",
"ffp_config",
"ffp_daemon",
"ffp_flm_server",
"ffp_hardware",
"ffp_llm_client",
"ffp_meetings",
"ffp_notifications",
"ffp_notify",
"ffp_provider_runtime",
"ffp_provider_status",
"ffp_pull",
"ffp_quill",
"ffp_telemetry",
"ffp_tools",
"ffp_updater",
"loopback_http",
"paths",
"grammar_fix",
"ffp_daemon",
"first_run",
"install",
"notes",
Expand Down
2 changes: 1 addition & 1 deletion installer/installer.iss
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
#define AppURL "https://github.com/agr77one/Fastflow"
#define AppExeName "Flowkey.exe" ; symbolic — actual launchers below
; Keep in lockstep with scripts\_version.py.
#define AppVersion "2.0.0"
#define AppVersion "2.1.0"

[Setup]
AppId={{8A4F1E6C-9B3D-4E62-9F7A-FASTFLOW140}}
Expand Down
2 changes: 1 addition & 1 deletion installer/sign.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
.EXAMPLE
# Sign the installer
$env:FFP_SIGN_PFX_PASSWORD = "ChangeMe!"
.\sign.ps1 -FilePath ..\out\Flowkey-Setup-2.0.0.exe
.\sign.ps1 -FilePath ..\out\Flowkey-Setup-2.1.0.exe
#>

[CmdletBinding(DefaultParameterSetName = "Sign")]
Expand Down
Loading