Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c52f81f
feat(obkmacos): add Swift helper binary for native macOS integrations
priyanshujain Mar 16, 2026
ecb3168
feat(obkmacos): add Go bridge package for Swift helper binary
priyanshujain Mar 16, 2026
634e237
refactor(contacts): replace osascript with obkmacos for Apple Contacts
priyanshujain Mar 16, 2026
a4c4a42
refactor(applenotes): replace osascript with obkmacos for Apple Notes
priyanshujain Mar 16, 2026
f576e67
fix(setup): link applecontacts instead of contacts in Apple Contacts …
priyanshujain Mar 16, 2026
6a60236
fix(contacts): remove erroneous LinkSource("contacts") from sync command
priyanshujain Mar 16, 2026
b147f88
fix(daemon): add migration from contacts to applecontacts linking
priyanshujain Mar 16, 2026
7ff7d4a
chore(make): add build-obkmacos target for Swift helper binary
priyanshujain Mar 16, 2026
9a1d665
ci: add Swift binary build to CI and release workflows
priyanshujain Mar 16, 2026
a4bbcc7
fix(spectest): skip spec tests unless OBK_TEST_PROVIDER is set
priyanshujain Mar 16, 2026
9cdb289
test(contacts): add unit tests for Apple Contacts conversion logic
priyanshujain Mar 16, 2026
a92f5b2
test(applenotes): add unit tests for notes/folder conversion logic
priyanshujain Mar 16, 2026
1b6b988
fix(obkmacos): swap createdAt/modifiedAt in notes output
priyanshujain Mar 16, 2026
17f5cb6
fix(contacts): use FetchContacts for permission check to trigger TCC …
priyanshujain Mar 16, 2026
fc9789a
fix(release): build obkmacos for both arm64 and amd64, upload as rele…
priyanshujain Mar 16, 2026
3c4cc53
feat(setup): add obk setup applecontacts and applenotes subcommands
priyanshujain Mar 16, 2026
2a137ff
fix(obkmacos): check authorization status before requesting contacts …
priyanshujain Mar 16, 2026
6d67894
feat(install): add cross-platform install script
priyanshujain Mar 16, 2026
85ff1ac
refactor(install): rewrite installer following industry patterns
priyanshujain Mar 16, 2026
aba773f
fix(install): trigger xcode-select --install when swiftc missing
priyanshujain Mar 16, 2026
b514a31
fix(cli): guard setup applecontacts/applenotes on non-darwin
priyanshujain Mar 16, 2026
56dfc08
fix(cli): guard applenotes sync on non-darwin
priyanshujain Mar 16, 2026
61f21a6
chore(release): drop Windows from goreleaser build targets
priyanshujain Mar 16, 2026
c2793a1
docs(readme): add install script, platform support table, drop Windows
priyanshujain Mar 16, 2026
3b878f8
fix(test): skip migration tests on non-darwin platforms
priyanshujain Mar 16, 2026
888507a
ci: remove Windows from test and cross-compile matrices
priyanshujain Mar 16, 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: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
Expand All @@ -18,6 +18,12 @@ jobs:
go-version-file: go.mod
- run: go test ./...

build-obkmacos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- run: swiftc -O -o obkmacos swift/obkmacos.swift

cross-compile:
runs-on: ubuntu-latest
strategy:
Expand All @@ -33,10 +39,6 @@ jobs:
goarch: amd64
- goos: darwin
goarch: arm64
- goos: windows
goarch: amd64
- goos: windows
goarch: arm64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,25 @@ permissions:
contents: write

jobs:
build-obkmacos:
strategy:
matrix:
include:
- arch: arm64
runner: macos-latest
- arch: amd64
runner: macos-13
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- run: swiftc -O -o obkmacos-darwin-${{ matrix.arch }} swift/obkmacos.swift
- uses: actions/upload-artifact@v4
with:
name: obkmacos-darwin-${{ matrix.arch }}
path: obkmacos-darwin-${{ matrix.arch }}

release:
needs: build-obkmacos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -22,3 +40,11 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4
with:
pattern: obkmacos-*
merge-multiple: true
- run: |
gh release upload "${{ github.ref_name }}" obkmacos-darwin-arm64 obkmacos-darwin-amd64
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
9 changes: 1 addition & 8 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@ builds:
- binary: obk
env: [CGO_ENABLED=0]
ldflags: -s -w -X github.com/priyanshujain/openbotkit/internal/cli.Version={{.Version}}
goos: [linux, darwin, windows]
goos: [linux, darwin]
goarch: [amd64, arm64, riscv64]
ignore:
- goos: darwin
goarch: riscv64
- goos: windows
goarch: riscv64

archives:
- format_overrides:
- goos: windows
format: zip
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ ALIAS = obk
SKILLS_DIR = $(HOME)/.obk/skills
ASSISTANT_SKILLS = assistant/.claude/skills

.PHONY: build install uninstall update-local
.PHONY: build build-obkmacos install uninstall update-local

build:
go build -o $(BINARY) .

install:
build-obkmacos:
ifeq ($(shell uname),Darwin)
@mkdir -p $(HOME)/.obk/bin
swiftc -O -o $(HOME)/.obk/bin/obkmacos swift/obkmacos.swift
@echo "Built obkmacos -> $(HOME)/.obk/bin/obkmacos"
endif

install: build-obkmacos
go install .
ln -sf $(GOBIN)/$(BINARY) $(GOBIN)/$(ALIAS)
mkdir -p $(SKILLS_DIR)
Expand Down
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,22 @@ AI agents can now read your email, send messages, and browse the web on your beh

### 1. Install

**macOS and Linux** (Windows is not supported):

```bash
curl -fsSL https://raw.githubusercontent.com/priyanshujain/openbotkit/master/install.sh | sh
```

<details>
<summary>Alternative: build from source</summary>

Requires [Go 1.25+](https://go.dev/dl/).

```bash
# Build from source
git clone https://github.com/priyanshujain/openbotkit.git
cd openbotkit && make install
```
</details>

### 2. Set up your sources

Expand Down Expand Up @@ -154,10 +165,17 @@ All data lives under `~/.obk/` (override with `OBK_CONFIG_DIR`):
└── data.db # Audit log
```

## Platform Support

| Platform | Status |
|----------|--------|
| **macOS** (Apple Silicon & Intel) | Fully supported — all features including Apple Contacts, Notes, iMessage |
| **Linux** (amd64 & arm64) | Supported — server deployment, all features except Apple-native integrations |
| **Windows** | Not supported |

## Prerequisites

- macOS (primary target; Linux supported for server deployment)
- Go 1.25+
- macOS or Linux
- Gmail: API credentials from [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
- WhatsApp: Phone with WhatsApp to scan QR code
- Telegram: Bot token from [@BotFather](https://t.me/botfather) (for approval flow)
Expand Down
33 changes: 31 additions & 2 deletions daemon/contacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package daemon
import (
"context"
"log/slog"
"runtime"
"time"

"github.com/priyanshujain/openbotkit/config"
Expand All @@ -18,6 +19,8 @@ func runContactsSync(ctx context.Context, cfg *config.Config) <-chan error {
go func() {
defer close(errCh)

migrateContactsLinking()

if err := config.EnsureSourceDir("contacts"); err != nil {
slog.Error("contacts: failed to create dir", "error", err)
errCh <- err
Expand All @@ -38,7 +41,7 @@ func runContactsSync(ctx context.Context, cfg *config.Config) <-chan error {
sourceDBs := openSourceDBs(cfg)
defer closeSourceDBs(sourceDBs)

if len(sourceDBs) == 0 {
if len(sourceDBs) == 0 && !config.IsSourceLinked("applecontacts") {
slog.Info("contacts: no linked sources, skipping sync")
return
}
Expand All @@ -62,8 +65,21 @@ func runContactsSync(ctx context.Context, cfg *config.Config) <-chan error {
return errCh
}

func linkedSources(sourceDBs map[string]*store.DB) []string {
sources := make([]string, 0, len(sourceDBs)+1)
for name := range sourceDBs {
sources = append(sources, name)
}
if config.IsSourceLinked("applecontacts") {
sources = append(sources, "applecontacts")
}
return sources
}

func syncContacts(db *store.DB, sourceDBs map[string]*store.DB) {
result, err := contactsrc.Sync(db, sourceDBs, contactsrc.SyncOptions{})
result, err := contactsrc.Sync(db, sourceDBs, contactsrc.SyncOptions{
Sources: linkedSources(sourceDBs),
})
if err != nil {
slog.Error("contacts: sync error", "error", err)
return
Expand Down Expand Up @@ -102,3 +118,16 @@ func closeSourceDBs(dbs map[string]*store.DB) {
db.Close()
}
}

func migrateContactsLinking() {
if runtime.GOOS != "darwin" {
return
}
if config.IsSourceLinked("contacts") && !config.IsSourceLinked("applecontacts") {
if err := config.LinkSource("applecontacts"); err != nil {
slog.Warn("contacts: failed to migrate contacts->applecontacts linking", "error", err)
} else {
slog.Info("contacts: migrated contacts->applecontacts linking")
}
}
}
111 changes: 111 additions & 0 deletions daemon/contacts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package daemon

import (
"context"
"runtime"
"sort"
"testing"
"time"

"github.com/priyanshujain/openbotkit/config"
"github.com/priyanshujain/openbotkit/store"
)

func TestRunContactsSync_StartAndStop(t *testing.T) {
Expand Down Expand Up @@ -34,6 +37,114 @@ func TestRunContactsSync_StartAndStop(t *testing.T) {
}
}

func TestLinkedSources_OnlyDBSources(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("OBK_CONFIG_DIR", tmpDir)

sourceDBs := map[string]*store.DB{
"whatsapp": nil,
"gmail": nil,
}
got := linkedSources(sourceDBs)
sort.Strings(got)
want := []string{"gmail", "whatsapp"}
if len(got) != len(want) {
t.Fatalf("got %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("got %v, want %v", got, want)
}
}
}

func TestLinkedSources_IncludesAppleContactsWhenLinked(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("OBK_CONFIG_DIR", tmpDir)

if err := config.LinkSource("applecontacts"); err != nil {
t.Fatal(err)
}
sourceDBs := map[string]*store.DB{
"whatsapp": nil,
}
got := linkedSources(sourceDBs)
sort.Strings(got)
want := []string{"applecontacts", "whatsapp"}
if len(got) != len(want) {
t.Fatalf("got %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("got %v, want %v", got, want)
}
}
}

func TestLinkedSources_ExcludesAppleContactsWhenNotLinked(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("OBK_CONFIG_DIR", tmpDir)

sourceDBs := map[string]*store.DB{}
got := linkedSources(sourceDBs)
if len(got) != 0 {
t.Fatalf("expected empty sources, got %v", got)
}
}

func TestMigrateContactsLinking(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skip("migrateContactsLinking only runs on macOS")
}
tmpDir := t.TempDir()
t.Setenv("OBK_CONFIG_DIR", tmpDir)

// Link "contacts" (the old incorrect name)
if err := config.LinkSource("contacts"); err != nil {
t.Fatal(err)
}

migrateContactsLinking()

if !config.IsSourceLinked("applecontacts") {
t.Error("expected applecontacts to be linked after migration")
}
}

func TestMigrateContactsLinking_NoOp(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("OBK_CONFIG_DIR", tmpDir)

// Neither "contacts" nor "applecontacts" linked
migrateContactsLinking()

if config.IsSourceLinked("applecontacts") {
t.Error("expected applecontacts to remain unlinked")
}
}

func TestMigrateContactsLinking_AlreadyMigrated(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skip("migrateContactsLinking only runs on macOS")
}
tmpDir := t.TempDir()
t.Setenv("OBK_CONFIG_DIR", tmpDir)

// Both linked — should not error
if err := config.LinkSource("contacts"); err != nil {
t.Fatal(err)
}
if err := config.LinkSource("applecontacts"); err != nil {
t.Fatal(err)
}

migrateContactsLinking()

if !config.IsSourceLinked("applecontacts") {
t.Error("expected applecontacts to remain linked")
}
}

func TestRunContactsSync_NoLinkedSources(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("OBK_CONFIG_DIR", tmpDir)
Expand Down
Loading
Loading