Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
8b2a880
docs(spec): phase 6 deeper macOS integration design
mariomeyer May 25, 2026
1a99f0f
docs(plan): phase 6b implementation plan
mariomeyer May 25, 2026
d11242d
chore(swift): SPM scaffold + spike findings
mariomeyer May 25, 2026
58a7ed1
feat(core): FFI envelope + panic safety scaffolding for Swift bridge
mariomeyer May 25, 2026
3acde18
feat(core): extern "C" verb wrappers for Swift bridge
mariomeyer May 25, 2026
2464134
feat(core): FFI for settings, log forwarder, focus id, indexer notify
mariomeyer May 25, 2026
5527501
feat(core): wire notify_indexer into verbs + pull_worker + C header
mariomeyer May 25, 2026
a29c186
feat(core): focus-default fallback in verbs::start
mariomeyer May 25, 2026
2f1e95c
feat(core): stint:// URL routes for projects and tasks
mariomeyer May 25, 2026
e80af7d
feat(swift): StintIntents framework — 10 intents + 5 App Shortcuts + …
mariomeyer May 25, 2026
041df36
feat(app): Tauri build pipeline + setup hook for StintIntents framework
mariomeyer May 25, 2026
636a827
test(swift): unit tests for envelope, entities, patch encoding, Spotl…
mariomeyer May 25, 2026
b4cae85
ci(swift): swift test step in CI + framework verify+sign in release
mariomeyer May 25, 2026
e89f14c
fix(app): link, export, and re-sign so StintIntents framework actuall…
mariomeyer May 26, 2026
88211cf
fix(swift): siri phrase set + bundle stencil at app level
mariomeyer May 26, 2026
88691b3
docs: phase 6b ship status — foundation-only with deferred Siri/Spotl…
mariomeyer May 26, 2026
9833f6e
docs(site): document new stint://project/<id> and stint://task/<id> r…
mariomeyer May 26, 2026
4bbdc1c
chore(swift): switch SpotlightIndexer scheduling to GCD + direct exte…
mariomeyer May 26, 2026
6ca1068
fix(swift): revert to dynamic framework — static link clashed with We…
mariomeyer May 26, 2026
d384db7
feat(app+ui): route stint://entry/<uuid> through to a highlighted Tod…
mariomeyer May 26, 2026
6a3ddd5
docs: phase 6b status — Spotlight is actually live; only Siri/Shortcu…
mariomeyer May 26, 2026
a905c02
docs(spec): phase 6c power-user surfaces design
mariomeyer May 27, 2026
7307799
feat(app): expose list_tasks and set_entry_task Tauri commands
mariomeyer May 27, 2026
3b6bd4f
test(app): lock HTTP task_id round-trip through start + patch
mariomeyer May 27, 2026
373b971
feat(ui): add api.listTasks + api.setEntryTask wrappers
mariomeyer May 27, 2026
3c1dfa2
feat(ui): TaskPicker combobox component
mariomeyer May 27, 2026
b4c79a5
docs(plan): phase 6c implementation plan
mariomeyer May 27, 2026
6726a55
feat(ui): TaskPicker in TimerCard for both start form and live entry
mariomeyer May 27, 2026
e4210b2
feat(ui): TaskPicker in EditEntryDialog Save flow
mariomeyer May 27, 2026
e5753e9
feat(ui): surface task name pill on EntryRow
mariomeyer May 27, 2026
bbada69
docs(skill): show --task / --clear-task CLI usage
mariomeyer May 27, 2026
712f286
feat(app): write api.port discovery file on HTTP bind
mariomeyer May 27, 2026
c0f8e78
fix(app): isolate api.port test env mutations + feature-gate test hel…
mariomeyer May 27, 2026
ee99053
chore: propagate timer.start taskId param and wrap Today test in Router
mariomeyer May 27, 2026
b67af22
feat(cli): stint projects list-tasks <id> subcommand
mariomeyer May 27, 2026
c72f409
feat(app): idle detector state machine
mariomeyer May 27, 2026
633fa02
feat(app): idle detector polling task + setup wiring
mariomeyer May 28, 2026
edfffa0
feat(app): idle_keep/discard/split Tauri commands
mariomeyer May 28, 2026
96391c3
feat(ui): IdleBanner — listen for idle:detected + render 3 actions
mariomeyer May 28, 2026
6c1975c
feat(ui): idle detection settings — toggle + threshold dropdown
mariomeyer May 28, 2026
165f7f7
feat(raycast): scaffold raycast-stint extension package
mariomeyer May 28, 2026
7cd786e
feat(raycast): Start Timer command (form with project + task)
mariomeyer May 28, 2026
8dd2ae8
feat(raycast): Stop Timer command (no-view)
mariomeyer May 28, 2026
eb17993
feat(raycast): Current Timer command (detail view, polls every 5s)
mariomeyer May 28, 2026
689c92a
feat(raycast): Recent Entries — browse + restart + copy + open in Stint
mariomeyer May 28, 2026
5dc55d6
feat(raycast): Switch Project — stop + start on new project preservin…
mariomeyer May 28, 2026
b541045
feat(alfred): scaffold alfred-stint workflow
mariomeyer May 28, 2026
012bff1
feat(widget): scaffold StintWidget Swift package
mariomeyer May 28, 2026
7faa0e9
feat(widget): PortDiscovery + DTO coding + tests
mariomeyer May 28, 2026
e377b66
feat(widget): TimelineProvider + HTTP fetch via PortDiscovery
mariomeyer May 28, 2026
b8d5de1
feat(widget): SwiftUI views for 3 widget kinds × 2 sizes
mariomeyer May 28, 2026
1b5673b
feat(widget): WidgetConfigurationIntent + Widget declaration + @main …
mariomeyer May 28, 2026
88e7b3a
chore(build): stint-app build.rs produces StintWidget.appex
mariomeyer May 28, 2026
2bfdc7c
chore(app): bundle StintWidget.appex into Stint.app/Contents/PlugIns/
mariomeyer May 28, 2026
3940c46
chore(build): widget bundle wrapper — relocate .appex into Contents/P…
mariomeyer May 28, 2026
336fc70
feat(app): auto-enable api.enabled when stint widgets are configured
mariomeyer May 28, 2026
87feb72
docs: phase 6c surfaces — Raycast + Alfred + WidgetKit + idle live
mariomeyer May 28, 2026
059ce24
ci(widget): swift test step + .appex relocation + codesign in release…
mariomeyer May 28, 2026
80433e4
style(build): rustfmt long create_dir_all line
mariomeyer May 28, 2026
c061a07
test(6c): full verification — workspace tests + Swift suites + UI green
mariomeyer May 28, 2026
7385b50
fix(widget): build as executable target, not dynamic framework
mariomeyer May 28, 2026
52bb6a4
fix(widget): app-sandbox entitlement + Info.plist platform keys
mariomeyer May 29, 2026
ddb2c62
ci(widget): pass StintWidget.entitlements to .appex codesign
mariomeyer May 29, 2026
ff60690
docs: phase 6d spec — Xcode-based extensions migration
mariomeyer May 29, 2026
7e643ab
docs(6d): implementation plan — Xcode-based extensions migration
mariomeyer May 29, 2026
6cd106d
chore: gitignore *.profraw + untrack tracked artifact
mariomeyer May 29, 2026
fe8445a
fix(ci): tolerate missing Metadata.appintents stencil
mariomeyer May 29, 2026
5e0c982
fix(build): add absolute rpath for StintIntents framework so tests load
mariomeyer May 29, 2026
c3960ab
ci: bump runner to macos-15 for Xcode 16+ (Swift Testing framework)
mariomeyer May 29, 2026
3c218a0
test(coverage): exclude idle_detector + updater_endpoint from coverag…
mariomeyer May 29, 2026
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
12 changes: 10 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ concurrency:
jobs:
build:
name: build
runs-on: macos-14
runs-on: macos-15
timeout-minutes: 30
env:
# See crates/stint-core/tests/config.rs — gates the one Keychain test.
Expand Down Expand Up @@ -54,6 +54,14 @@ jobs:
- name: cargo test
run: cargo test --workspace -- --test-threads=1

- name: Swift test (StintIntents framework)
working-directory: crates/stint-app/swift/StintIntents
run: xcodebuild -scheme StintIntents -destination 'platform=macOS' -derivedDataPath ./build/derived test

- name: Swift test (StintWidget)
working-directory: crates/stint-app/swift/StintWidget
run: xcodebuild -scheme StintWidget -destination 'platform=macOS' -derivedDataPath ./build/derived test

- name: pnpm install (root workspace)
run: pnpm install --frozen-lockfile

Expand All @@ -68,7 +76,7 @@ jobs:

coverage:
name: coverage
runs-on: macos-14
runs-on: macos-15
timeout-minutes: 25
env:
STINT_SKIP_KEYCHAIN_TESTS: "1"
Expand Down
42 changes: 40 additions & 2 deletions .github/workflows/release-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:

jobs:
build:
runs-on: macos-14
runs-on: macos-15
env:
MACOSX_DEPLOYMENT_TARGET: "13.0"
steps:
Expand Down Expand Up @@ -160,6 +160,21 @@ jobs:
cp target/universal-apple-darwin/release/stint "$APP/Contents/MacOS/stint"
echo "APP_PATH=$APP" >> "$GITHUB_ENV"

- name: Relocate StintWidget.appex into Contents/PlugIns/
run: |
# build.rs produces crates/stint-app/PlugIns/StintWidget.appex.
# Tauri's bundle.resources puts files under Contents/Resources/
# but macOS WidgetKit only discovers widget extensions at
# Contents/PlugIns/. Move it into place + strip the duplicated
# Resources/PlugIns copy left behind by Tauri.
SRC="crates/stint-app/PlugIns/StintWidget.appex"
DEST="$APP_PATH/Contents/PlugIns/StintWidget.appex"
if [ ! -d "$SRC" ]; then echo "::error::appex source missing at $SRC"; exit 1; fi
mkdir -p "$(dirname "$DEST")"
rm -rf "$DEST"
cp -R "$SRC" "$DEST"
rm -rf "$APP_PATH/Contents/Resources/PlugIns"

- name: Smoke-test embedded version
env:
VERSION: ${{ inputs.version }}
Expand Down Expand Up @@ -193,6 +208,14 @@ jobs:
--entitlements crates/stint-app/entitlements.plist \
"$APP_PATH/Contents/MacOS/stint"

- name: Verify StintIntents framework present
run: |
# The framework is bundled but its App Intents stencil isn't
# consulted by Apple's indexer on the framework path anyway
# (the 6b deferral). Phase 6d replaces this with a real .appex.
FRAMEWORK="$APP_PATH/Contents/Frameworks/StintIntents.framework"
if [ ! -d "$FRAMEWORK" ]; then echo "::error::framework missing"; exit 1; fi

- name: Sign GUI binary + .app bundle (hardened runtime + entitlements)
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
Expand All @@ -201,6 +224,19 @@ jobs:
# (which seals CodeResources including the CLI's hash). Avoid
# --deep so the per-binary signatures we did individually
# aren't overwritten.
# Sign embedded framework first (inner) so the wrapper seals it.
codesign --force --options runtime \
--sign "$APPLE_SIGNING_IDENTITY" \
"$APP_PATH/Contents/Frameworks/StintIntents.framework"
# Sign the widget appex so the bundle wrapper can seal it. The
# appex's binary already carries an ad-hoc signature from build.rs;
# this overwrites with the production identity. The sandbox
# entitlement is required — pluginkit refuses to register
# extensions without com.apple.security.app-sandbox = true.
codesign --force --options runtime \
--sign "$APPLE_SIGNING_IDENTITY" \
--entitlements crates/stint-app/swift/StintWidget/StintWidget.entitlements \
"$APP_PATH/Contents/PlugIns/StintWidget.appex"
codesign --force --options runtime \
--sign "$APPLE_SIGNING_IDENTITY" \
--entitlements crates/stint-app/entitlements.plist \
Expand All @@ -209,8 +245,10 @@ jobs:
--sign "$APPLE_SIGNING_IDENTITY" \
--entitlements crates/stint-app/entitlements.plist \
"$APP_PATH"
codesign --verify --strict --verbose=2 "$APP_PATH"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
codesign --verify --strict --verbose=2 "$APP_PATH/Contents/MacOS/stint"
codesign --verify --strict --verbose=2 "$APP_PATH/Contents/Frameworks/StintIntents.framework"
codesign --verify --strict --verbose=2 "$APP_PATH/Contents/PlugIns/StintWidget.appex"

- name: Notarize .app
env:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
/target/
**/*.rs.bk
*.pdb
# llvm-cov / cargo-llvm-cov coverage instrumentation artifacts
*.profraw

# Claude Code session-local files
.claude/
Expand Down Expand Up @@ -61,3 +63,4 @@ node_modules/

# Image-generation scratch output (nano-banana skill)
nanobanana-output/
Frameworks/
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,11 @@ git checkout -b phase-2.5
| 3c | Solidtime down-sync | ✅ shipped (`phase-3c-complete`) |
| 3.5 | Test coverage uplift across core / CLI / app / UI | ✅ shipped (`phase-3.5-complete`) |
| 3d | Post-3b UX polish + sync resilience + in-app error surfacing (picker / calendar defaults / editable times / backdate / restart-from-entry / calendar undo / 4xx-abandon / adopt-on-overlap / SyncErrorBanner + coverage CI) | ✅ shipped (`phase-3d-complete`) |
| 4 | Distribution (Homebrew cask + signing + release CD) | planned |
| 5 | Documentation site (GitHub Pages) | planned |
| 4 | Distribution (Homebrew cask + signing + release CD) | ✅ shipped (`phase-4-complete`) |
| 5 | Documentation site (GitHub Pages) | ✅ shipped (`phase-5-complete`) |
| 6a | verbs façade + MCP + HTTP API + URL scheme + man page + skill installer | ✅ shipped (`phase-6a-complete`) |
| 6b | Spotlight + App Intents + Focus filter | ⚠️ **partial** (`phase-6b-complete`) — Spotlight indexing + tap-to-focus-entry shipped and working; Siri/Shortcuts.app/Focus-filter discovery deferred to a follow-up using an App Intents Extension. See `docs/superpowers/specs/2026-05-25-stint-phase-6-deeper-integration-design.md` §1.5 |
| 6c | Raycast + Alfred + WidgetKit + idle detection | ✅ shipped (`phase-6c-complete`) |

## Gotchas / dev-environment notes

Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,11 @@ branch.
| 3c | Solidtime down-sync | ✅ shipped |
| 3.5 | Test coverage uplift | ✅ shipped |
| 3d | UX polish + sync resilience | ✅ shipped |
| 4 | Distribution (Homebrew cask + signing + release CD) | 🔜 planned |
| 5 | Documentation site (GitHub Pages) | 🔜 planned |
| 4 | Distribution (Homebrew cask + signing + release CD) | ✅ shipped |
| 5 | Documentation site (GitHub Pages) | ✅ shipped |
| 6a | verbs façade + MCP + HTTP API + URL scheme + skill installer | ✅ shipped |
| 6b | Spotlight + App Intents + Focus filter | ⚠️ partial — Spotlight tap-to-focus-entry works; Siri/Shortcuts.app discovery deferred |
| 6c | Raycast + Alfred + WidgetKit + idle detection | ✅ shipped |

---

Expand Down
41 changes: 41 additions & 0 deletions alfred-stint/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Stint for Alfred

Four keyword shortcuts for [stint](https://github.com/reyemtech/stint):

| Keyword | What it does |
|---|---|
| `s <description>` | Start a timer with that description |
| `sstop` | Stop the running timer |
| `scur` | Show the running timer |
| `srec` | List recent entries; ⏎ restarts, ⌥⏎ opens in Stint |

## Install

1. Double-click `Stint.alfredworkflow` from the GitHub Releases page.
2. Alfred prompts to import.
3. Make sure the `stint` CLI is in PATH (or set the Workflow Environment
Variable `STINT_BIN`).

## First-time setup after import

This directory ships a minimal `info.plist` skeleton — Alfred needs the
four keywords wired to the corresponding scripts. After importing:

1. Open Alfred Preferences → Workflows → Stint.
2. Add four objects:
- Keyword `s` (argument required) → Run Script (`bash`) →
`./start.sh "{query}"`.
- Keyword `sstop` → Run Script (`bash`) → `./stop.sh`.
- Script Filter, keyword `scur` → `bash` → `./current.sh`. Open URL on
selection.
- Script Filter, keyword `srec` → `bash` → `./recent.sh`. ⏎ runs
`./start.sh "$(./describe.sh {query})"`, ⌥⏎ opens the URL.
3. Export the workflow over this directory to lock in the wiring.

## Build from source

This directory IS the workflow source. Bundle:

```bash
zip -r Stint.alfredworkflow . -x ".*"
```
26 changes: 26 additions & 0 deletions alfred-stint/current.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/lib.sh"

BIN="$(resolve_bin)" || {
cat <<EOF
{"items":[{"title":"Stint binary not found","subtitle":"Set STINT_BIN in workflow env","valid":false}]}
EOF
exit 0
}

JSON="$("$BIN" --json current 2>/dev/null || echo "null")"
if [[ "$JSON" == "null" ]] || [[ -z "$JSON" ]]; then
echo '{"items":[{"title":"No active timer","valid":false}]}'
exit 0
fi
python3 - <<PY
import json, sys
e = json.loads('''$JSON''')
print(json.dumps({"items":[{
"uid": e["local_uuid"],
"title": e.get("description","(no description)"),
"subtitle": "Open in Stint",
"arg": f"stint://entry/{e['local_uuid']}",
}]}))
PY
Binary file added alfred-stint/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions alfred-stint/info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>bundleid</key>
<string>tech.reyem.stint.alfred</string>
<key>name</key>
<string>Stint</string>
<key>description</key>
<string>Start, stop, and inspect Stint time entries from Alfred.</string>
<key>version</key>
<string>0.1.0</string>
<key>createdby</key>
<string>Reyem Technologies</string>
<key>readme</key>
<string>See README.md</string>
<key>objects</key>
<array/>
<key>connections</key>
<dict/>
<key>uidata</key>
<dict/>
</dict>
</plist>
17 changes: 17 additions & 0 deletions alfred-stint/lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Shared helpers for Stint Alfred workflow scripts.

resolve_bin() {
if [[ -n "$STINT_BIN" ]] && [[ -x "$STINT_BIN" ]]; then
echo "$STINT_BIN"
return
fi
if command -v stint >/dev/null 2>&1; then
command -v stint
return
fi
for candidate in "$HOME/.cargo/bin/stint" "/Applications/Stint.app/Contents/MacOS/stint"; do
[[ -x "$candidate" ]] && { echo "$candidate"; return; }
done
return 1
}
25 changes: 25 additions & 0 deletions alfred-stint/recent.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/lib.sh"

BIN="$(resolve_bin)" || {
echo '{"items":[{"title":"Stint binary not found","valid":false}]}'
exit 0
}

JSON="$("$BIN" --json list --limit 20 2>/dev/null || echo "[]")"
python3 - <<PY
import json
items = []
for e in json.loads('''$JSON'''):
items.append({
"uid": e["local_uuid"],
"title": e.get("description","(no description)"),
"subtitle": e.get("start_at",""),
"arg": e["local_uuid"],
"mods": {
"alt": {"arg": f"stint://entry/{e['local_uuid']}", "subtitle": "Open in Stint"},
}
})
print(json.dumps({"items": items}))
PY
8 changes: 8 additions & 0 deletions alfred-stint/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/lib.sh"

DESC="${1:?usage: start.sh <description>}"
BIN="$(resolve_bin)" || { echo "Stint binary not found"; exit 1; }

"$BIN" --json start --description "$DESC" | head -1
9 changes: 9 additions & 0 deletions alfred-stint/stop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/lib.sh"

BIN="$(resolve_bin)" || { echo "Stint binary not found"; exit 1; }

ENTRY="$("$BIN" --json stop)"
DESC="$(echo "$ENTRY" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("description",""))')"
echo "Stopped: $DESC"
4 changes: 4 additions & 0 deletions crates/stint-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Frameworks/
PlugIns/
build-deps/
Metadata.appintents/
3 changes: 3 additions & 0 deletions crates/stint-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ path = "src/main.rs"
[features]
default = ["updater"]
updater = ["dep:tauri-plugin-updater", "dep:semver"]
test-utils = []

[dependencies]
stint-core = { path = "../stint-core" }
Expand All @@ -29,6 +30,7 @@ dirs.workspace = true
chrono.workspace = true

tauri = { version = "2.1", features = ["macos-private-api", "tray-icon", "image-png"] }
libc = "0.2"
tauri-plugin-opener = "2.2"
tauri-plugin-positioner = { version = "2.3", features = ["tray-icon"] }
tauri-plugin-deep-link = "2"
Expand All @@ -45,3 +47,4 @@ wiremock.workspace = true
serde_json.workspace = true
tauri = { version = "2.1", features = ["test"] }
tower = "0.5"
stint-app = { path = ".", features = ["test-utils"] }
Loading
Loading