openplexer 0.1.0 → 0.2.0

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.
package/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # openplexer
2
+
3
+ Track every coding session across your team in a Notion board. Automatically.
4
+
5
+ ```
6
+ npm install -g openplexer
7
+ ```
8
+
9
+ ## The problem
10
+
11
+ AI coding agents are everywhere now. OpenCode, Claude Code, Codex — you run them in worktrees, in different repos, on different branches. You start a session to fix a bug, another to refactor auth, another to explore an idea. Some finish, some don't. Some need your attention, some are fine.
12
+
13
+ After a week you have 40+ sessions scattered across your machine with no way to tell which ones matter.
14
+
15
+ **For solo developers**, it's hard to keep track. You forget about sessions. You don't know which worktree has unfinished work. You resume the wrong one. You lose context.
16
+
17
+ **For teams**, it's worse. You have no idea what your teammates are working on right now. You can't see if someone already started a session on the bug you're about to fix. There's no shared view of who's doing what, on which branch, in which repo. No way to flag a session as "needs review" or "blocked" or "done — merge it."
18
+
19
+ ## The solution
20
+
21
+ openplexer runs as a background daemon on your machine. It connects to your coding agents via ACP (Agent Client Protocol), discovers all your sessions, and syncs them to a Notion kanban board — automatically, every 5 seconds.
22
+
23
+ ```
24
+ ┌──────────────┐ ┌──────────────────┐
25
+ │ OpenCode │◄── ACP (stdio) ────►│ │
26
+ │ Claude Code │◄── ACP (stdio) ────►│ openplexer │
27
+ │ Codex │◄── ACP (stdio) ────►│ (background) │
28
+ └──────────────┘ │ │
29
+ │ syncs every 5s │
30
+ └────────┬─────────┘
31
+
32
+ │ Notion API
33
+
34
+ ┌──────────────────┐
35
+ │ Notion Board │
36
+ │ (shared kanban) │
37
+ └──────────────────┘
38
+ ```
39
+
40
+ Each session becomes a card on the board. You can see at a glance:
41
+
42
+ - **What's in progress** — sessions that are still running or were never finished
43
+ - **What's done** — completed sessions you can archive or review
44
+ - **What needs attention** — sessions you manually flag for follow-up
45
+ - **Who's working on what** — every card is assigned to the person who started it
46
+ - **Which repo and branch** — direct links to the GitHub branch
47
+ - **How to resume** — a ready-to-paste CLI command to pick up where you left off
48
+
49
+ ## Collaborative by default
50
+
51
+ The board is a shared Notion page. Multiple team members can connect their machines to the same board. Each person's sessions show up automatically, assigned to them.
52
+
53
+ This means your team gets a single view of all active coding work:
54
+
55
+ - Alice is refactoring the auth module on `feature/auth-v2` in `acme/backend` — **In Progress**
56
+ - Bob finished the migration script on `fix/db-migrate` in `acme/infra` — **Done**
57
+ - Charlie's session on `acme/frontend` needs review — **Needs Attention**
58
+
59
+ No standups needed to know what's happening. No Slack messages asking "are you still working on that?" The board is always current because every machine syncs continuously.
60
+
61
+ Each user controls which repos they sync to the shared board. If you're also hacking on personal side projects, those sessions stay off the shared board — only repos you explicitly select are tracked. This keeps the shared view clean and focused on team work.
62
+
63
+ ## Getting started
64
+
65
+ Run `openplexer` for the first time and the setup wizard walks you through everything:
66
+
67
+ **1. Pick your agents**
68
+
69
+ ```
70
+ ◆ Which coding agents do you use?
71
+ │ ◼ OpenCode
72
+ │ ◼ Claude Code
73
+ │ ◻ Codex
74
+ ```
75
+
76
+ Select one or more. openplexer connects to each via ACP and merges all sessions into a single board.
77
+
78
+ **2. Auto-discover repos**
79
+
80
+ openplexer spawns the ACP server, lists all your sessions, and extracts the git repos from their working directories. No manual configuration needed.
81
+
82
+ **3. Select repos to track**
83
+
84
+ ```
85
+ ◆ Which repos to track?
86
+ │ ◼ * All repos
87
+ │ ◻ acme/backend
88
+ │ ◻ acme/frontend
89
+ │ ◻ acme/infra
90
+ ```
91
+
92
+ Pick specific repos for shared boards (recommended — keeps personal projects off the team board). Or select all if you want everything tracked.
93
+
94
+ **4. Connect Notion**
95
+
96
+ Your browser opens to authorize the Notion integration. Select the page where the board should live. No manual API tokens, no copying secrets — just click authorize and you're done.
97
+
98
+ **5. Board created**
99
+
100
+ openplexer creates a database inside your selected Notion page with a kanban Board view grouped by status. An example card explains what each field means. Real sessions start appearing within seconds.
101
+
102
+ **6. Run on login (optional)**
103
+
104
+ ```
105
+ ◆ Register openplexer to run on login?
106
+ │ Yes
107
+ ```
108
+
109
+ openplexer registers itself as a startup service so it runs in the background every time you log in. The board stays current without you having to think about it.
110
+
111
+ ## Board properties
112
+
113
+ Every session card in Notion has these fields:
114
+
115
+ | Property | Type | Description |
116
+ |---|---|---|
117
+ | **Name** | Title | Session title from the agent |
118
+ | **Status** | Select | `In Progress`, `Done`, `Needs Attention`, `Ignored`, `Not Started` |
119
+ | **Repo** | Select | GitHub repo as `owner/repo` |
120
+ | **Branch** | URL | Link to the branch on GitHub |
121
+ | **Share URL** | URL | Public share link (OpenCode `/share`) |
122
+ | **Resume** | Text | CLI command to resume the session |
123
+ | **Assignee** | People | Notion user who authorized the integration |
124
+ | **Folder** | Text | Local filesystem path |
125
+ | **Discord** | URL | Discord thread link (if using kimaki) |
126
+ | **Updated** | Date | Last update timestamp from the agent |
127
+ | **Session ID** | Text | Internal ACP session identifier |
128
+
129
+ **Status** is the only field you manage manually. Everything else is synced automatically. Move cards between columns as you triage — mark sessions as done when you're finished, flag ones that need attention, ignore ones you don't care about.
130
+
131
+ **Resume** gives you the exact command to pick up a session:
132
+
133
+ ```bash
134
+ # OpenCode sessions
135
+ opencode --session ses_abc123
136
+
137
+ # Claude Code sessions
138
+ claude --resume ses_abc123
139
+
140
+ # Codex sessions
141
+ codex resume ses_abc123
142
+ ```
143
+
144
+ ## CLI commands
145
+
146
+ ```bash
147
+ openplexer # Start daemon (first run triggers setup wizard)
148
+ openplexer connect # Add another board
149
+ openplexer status # Show sync state and session counts
150
+ openplexer boards # List all configured boards with URLs
151
+ openplexer stop # Kill the running daemon
152
+ openplexer startup # Show startup registration status
153
+ openplexer startup enable # Register to run on login
154
+ openplexer startup disable # Unregister from login
155
+ ```
156
+
157
+ ## Multiple boards
158
+
159
+ You can connect as many boards as you want. Each board is a separate Notion database with its own repo filter and assignee.
160
+
161
+ ```bash
162
+ openplexer connect
163
+ ```
164
+
165
+ Use cases:
166
+ - **Team board** — shared page, filtered to company repos, everyone connects to it
167
+ - **Personal board** — private page, all repos, just for you
168
+ - **Project board** — scoped to a single repo for focused tracking
169
+
170
+ ## How it works
171
+
172
+ **ACP protocol** — openplexer spawns each agent's ACP server as a child process and communicates over stdio using the Agent Client Protocol. It lists all sessions with pagination and extracts git repo info from each session's working directory.
173
+
174
+ **Sync loop** — every 5 seconds, openplexer polls all connected agents for sessions. New sessions get a Notion page created. Existing sessions get their title and timestamp updated. Only sessions created or updated after the board was connected are synced (no backfilling old sessions).
175
+
176
+ **Single instance** — a lock port (default `29990`) ensures only one daemon runs at a time. Starting a new instance cleanly terminates the old one via SIGTERM, then SIGKILL if needed.
177
+
178
+ **Notion OAuth** — authentication goes through `openplexer.com` (a Cloudflare Worker). The CLI opens your browser, you authorize, the worker exchanges the code for tokens and stores them in KV with a 5-minute TTL. The CLI polls for the result. No secrets to manage.
179
+
180
+ **Startup service** — cross-platform registration so openplexer starts automatically:
181
+ - **macOS**: launchd plist at `~/Library/LaunchAgents/com.openplexer.plist`
182
+ - **Linux**: XDG autostart at `~/.config/autostart/openplexer.desktop`
183
+ - **Windows**: registry key at `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`
184
+
185
+ **Config** — stored at `~/.openplexer/config.json`. Contains the list of agents, board configurations (Notion tokens, database IDs, tracked repos), and a map of synced session IDs to Notion page IDs.
186
+
187
+ **Rate limiting** — Notion API calls are throttled to ~3/second to stay within rate limits.
188
+
189
+ ## Discord integration
190
+
191
+ If you use [kimaki](https://kimaki.xyz) to run coding sessions from Discord, openplexer automatically detects the kimaki CLI and adds a Discord thread URL to each session card. This links the Notion board directly to the Discord conversation where the work is happening.
192
+
193
+ ## License
194
+
195
+ MIT
@@ -1,13 +1,18 @@
1
- import { ClientSideConnection, type SessionInfo } from '@agentclientprotocol/sdk';
2
- export type AcpConnection = {
3
- connection: ClientSideConnection;
4
- client: 'opencode' | 'claude';
1
+ import { type SessionInfo } from '@agentclientprotocol/sdk';
2
+ import type { AcpClient } from './config.ts';
3
+ export type AgentConnection = {
4
+ client: AcpClient;
5
+ listSessions: () => Promise<SessionInfo[]>;
5
6
  kill: () => void;
6
7
  };
7
- export declare function connectAcp({ client, }: {
8
- client: 'opencode' | 'claude';
9
- }): Promise<AcpConnection>;
8
+ export type AcpConnection = AgentConnection;
9
+ export declare function connectAgent({ client }: {
10
+ client: AcpClient;
11
+ }): Promise<AgentConnection>;
12
+ export declare function connectAcp({ client }: {
13
+ client: AcpClient;
14
+ }): Promise<AgentConnection>;
10
15
  export declare function listAllSessions({ connection, }: {
11
- connection: ClientSideConnection;
16
+ connection: AgentConnection;
12
17
  }): Promise<SessionInfo[]>;
13
18
  //# sourceMappingURL=acp-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"acp-client.d.ts","sourceRoot":"","sources":["../src/acp-client.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,oBAAoB,EAIpB,KAAK,WAAW,EACjB,MAAM,0BAA0B,CAAA;AAkDjC,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,oBAAoB,CAAA;IAChC,MAAM,EAAE,UAAU,GAAG,QAAQ,CAAA;IAC7B,IAAI,EAAE,MAAM,IAAI,CAAA;CACjB,CAAA;AAED,wBAAsB,UAAU,CAAC,EAC/B,MAAM,GACP,EAAE;IACD,MAAM,EAAE,UAAU,GAAG,QAAQ,CAAA;CAC9B,GAAG,OAAO,CAAC,aAAa,CAAC,CA6BzB;AAED,wBAAsB,eAAe,CAAC,EACpC,UAAU,GACX,EAAE;IACD,UAAU,EAAE,oBAAoB,CAAA;CACjC,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAiBzB"}
1
+ {"version":3,"file":"acp-client.d.ts","sourceRoot":"","sources":["../src/acp-client.ts"],"names":[],"mappings":"AAcA,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,0BAA0B,CAAA;AAEjC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAM5C,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,SAAS,CAAA;IACjB,YAAY,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAA;IAC1C,IAAI,EAAE,MAAM,IAAI,CAAA;CACjB,CAAA;AAGD,MAAM,MAAM,aAAa,GAAG,eAAe,CAAA;AAiM3C,wBAAsB,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GAAG,OAAO,CAAC,eAAe,CAAC,CAK9F;AAGD,wBAAsB,UAAU,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GAAG,OAAO,CAAC,eAAe,CAAC,CAE5F;AAED,wBAAsB,eAAe,CAAC,EACpC,UAAU,GACX,EAAE;IACD,UAAU,EAAE,eAAe,CAAA;CAC5B,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAEzB"}
@@ -1,7 +1,88 @@
1
- // Spawn an ACP agent (opencode or claude) as a child process and connect
2
- // as a client via stdio. Uses @agentclientprotocol/sdk for the protocol.
1
+ // Connect to coding agents and list their sessions.
2
+ //
3
+ // opencode: spawns `opencode serve` and uses the HTTP API's
4
+ // /experimental/session endpoint which returns sessions across ALL
5
+ // projects (Session.listGlobal). The ACP protocol's listSessions
6
+ // calls Session.list which is scoped to a single project — that's
7
+ // why we bypass ACP for opencode.
8
+ //
9
+ // claude / codex: uses ACP over stdio (unchanged).
3
10
  import { spawn } from 'node:child_process';
11
+ import { createRequire } from 'node:module';
12
+ import path from 'node:path';
4
13
  import { ClientSideConnection, ndJsonStream, } from '@agentclientprotocol/sdk';
14
+ import { createOpencodeClient } from '@opencode-ai/sdk/v2';
15
+ // ---------------------------------------------------------------------------
16
+ // opencode — HTTP server with /experimental/session (global, all projects)
17
+ // ---------------------------------------------------------------------------
18
+ async function connectOpencode() {
19
+ const PORT = 18_923;
20
+ const baseUrl = `http://127.0.0.1:${PORT}`;
21
+ // Spawn `opencode serve` on a known port. cwd doesn't matter since
22
+ // we use the global endpoint.
23
+ const child = spawn('opencode', ['serve', '--port', String(PORT)], {
24
+ stdio: ['pipe', 'pipe', 'pipe'],
25
+ cwd: '/',
26
+ });
27
+ const sdk = createOpencodeClient({ baseUrl });
28
+ // Wait for the server to be ready (poll until it responds)
29
+ const deadline = Date.now() + 15_000;
30
+ while (Date.now() < deadline) {
31
+ try {
32
+ const res = await fetch(`${baseUrl}/session?limit=1`);
33
+ if (res.ok)
34
+ break;
35
+ }
36
+ catch {
37
+ // server not ready yet
38
+ }
39
+ await new Promise((r) => setTimeout(r, 200));
40
+ }
41
+ // Verify it's actually up
42
+ const check = await fetch(`${baseUrl}/session?limit=1`).catch(() => null);
43
+ if (!check?.ok) {
44
+ child.kill();
45
+ throw new Error('opencode serve failed to start');
46
+ }
47
+ return {
48
+ client: 'opencode',
49
+ listSessions: async () => {
50
+ const sessions = [];
51
+ let cursor;
52
+ // Paginate through /experimental/session which uses Session.listGlobal()
53
+ // (returns sessions across ALL projects, not scoped to one)
54
+ while (true) {
55
+ const result = await sdk.experimental.session.list({
56
+ roots: true,
57
+ ...(cursor !== undefined && { cursor }),
58
+ });
59
+ if (result.error || !result.data) {
60
+ throw new Error(`opencode API error: ${result.error}`);
61
+ }
62
+ for (const s of result.data) {
63
+ sessions.push({
64
+ sessionId: s.id,
65
+ cwd: s.directory,
66
+ title: s.title,
67
+ updatedAt: new Date(s.time.updated).toISOString(),
68
+ });
69
+ }
70
+ // Pagination cursor is in the x-next-cursor response header
71
+ const nextCursor = result.response.headers.get('x-next-cursor');
72
+ if (!nextCursor)
73
+ break;
74
+ cursor = Number(nextCursor);
75
+ }
76
+ return sessions;
77
+ },
78
+ kill: () => {
79
+ child.kill();
80
+ },
81
+ };
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // claude / codex — ACP over stdio
85
+ // ---------------------------------------------------------------------------
5
86
  function nodeToWebWritable(nodeStream) {
6
87
  return new WritableStream({
7
88
  write(chunk) {
@@ -33,9 +114,6 @@ function nodeToWebReadable(nodeStream) {
33
114
  },
34
115
  });
35
116
  }
36
- // Minimal Client implementation — we only need session listing,
37
- // not file ops or permissions. requestPermission and sessionUpdate
38
- // are required by the Client interface.
39
117
  class MinimalClient {
40
118
  async requestPermission() {
41
119
  return { outcome: { outcome: 'cancelled' } };
@@ -48,11 +126,24 @@ class MinimalClient {
48
126
  return {};
49
127
  }
50
128
  }
51
- export async function connectAcp({ client, }) {
52
- const cmd = client === 'opencode' ? 'opencode' : 'claude';
53
- const args = ['acp'];
129
+ function resolveAcpBinary(client) {
130
+ const require = createRequire(import.meta.url);
131
+ const packageName = client === 'claude'
132
+ ? '@zed-industries/claude-agent-acp'
133
+ : '@zed-industries/codex-acp';
134
+ const binName = client === 'claude' ? 'claude-agent-acp' : 'codex-acp';
135
+ const pkgJsonPath = require.resolve(`${packageName}/package.json`);
136
+ const pkgDir = path.dirname(pkgJsonPath);
137
+ const pkg = require(pkgJsonPath);
138
+ const binRelative = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin[binName];
139
+ const binPath = path.resolve(pkgDir, binRelative);
140
+ return { cmd: process.execPath, args: [binPath] };
141
+ }
142
+ async function connectAcpAgent(client) {
143
+ const { cmd, args } = resolveAcpBinary(client);
54
144
  const child = spawn(cmd, args, {
55
145
  stdio: ['pipe', 'pipe', 'inherit'],
146
+ cwd: '/',
56
147
  });
57
148
  const stream = ndJsonStream(nodeToWebWritable(child.stdin), nodeToWebReadable(child.stdout));
58
149
  const connection = new ClientSideConnection((_agent) => {
@@ -63,26 +154,39 @@ export async function connectAcp({ client, }) {
63
154
  clientCapabilities: {},
64
155
  });
65
156
  return {
66
- connection,
67
157
  client,
158
+ listSessions: async () => {
159
+ const sessions = [];
160
+ let cursor;
161
+ while (true) {
162
+ const response = await connection.listSessions({
163
+ ...(cursor ? { cursor } : {}),
164
+ });
165
+ sessions.push(...response.sessions);
166
+ if (!response.nextCursor)
167
+ break;
168
+ cursor = response.nextCursor;
169
+ }
170
+ return sessions;
171
+ },
68
172
  kill: () => {
69
173
  child.kill();
70
174
  },
71
175
  };
72
176
  }
73
- export async function listAllSessions({ connection, }) {
74
- const sessions = [];
75
- let cursor;
76
- // Paginate through all sessions
77
- while (true) {
78
- const response = await connection.listSessions({
79
- ...(cursor ? { cursor } : {}),
80
- });
81
- sessions.push(...response.sessions);
82
- if (!response.nextCursor) {
83
- break;
84
- }
85
- cursor = response.nextCursor;
177
+ // ---------------------------------------------------------------------------
178
+ // Public API
179
+ // ---------------------------------------------------------------------------
180
+ export async function connectAgent({ client }) {
181
+ if (client === 'opencode') {
182
+ return connectOpencode();
86
183
  }
87
- return sessions;
184
+ return connectAcpAgent(client);
185
+ }
186
+ // Legacy exports for backwards compat
187
+ export async function connectAcp({ client }) {
188
+ return connectAgent({ client });
189
+ }
190
+ export async function listAllSessions({ connection, }) {
191
+ return connection.listSessions();
88
192
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=acp-client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acp-client.test.d.ts","sourceRoot":"","sources":["../src/acp-client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,91 @@
1
+ // Integration test: verifies that opencode returns sessions from
2
+ // multiple different project directories, not just one.
3
+ // This test only runs on machines with opencode installed and real sessions.
4
+ import { describe, it, expect } from 'vitest';
5
+ import { connectAgent } from "./acp-client.js";
6
+ import { getRepoInfo } from "./git.js";
7
+ describe('agent-client', () => {
8
+ it('listSessions returns sessions from at least 2 different projects', async () => {
9
+ const agent = await connectAgent({ client: 'opencode' });
10
+ try {
11
+ const sessions = await agent.listSessions();
12
+ // Collect unique cwd values
13
+ const cwds = [...new Set(sessions.map((s) => s.cwd).filter(Boolean))];
14
+ console.log(`Found ${sessions.length} sessions across ${cwds.length} directories:`);
15
+ for (const cwd of cwds) {
16
+ const count = sessions.filter((s) => s.cwd === cwd).length;
17
+ console.log(` ${cwd}: ${count} sessions`);
18
+ }
19
+ expect(sessions.length).toBeGreaterThan(0);
20
+ expect(cwds.length).toBeGreaterThanOrEqual(2);
21
+ }
22
+ finally {
23
+ agent.kill();
24
+ }
25
+ }, 30_000);
26
+ it('debug: show which sessions would pass sync filters', async () => {
27
+ const connectedAt = '2026-03-21T23:03:40.127Z';
28
+ const connectedAtMs = new Date(connectedAt).getTime();
29
+ console.log(`connectedAt: ${connectedAt} (${connectedAtMs})`);
30
+ console.log(`now: ${new Date().toISOString()} (${Date.now()})`);
31
+ console.log();
32
+ const agent = await connectAgent({ client: 'opencode' });
33
+ try {
34
+ const sessions = await agent.listSessions();
35
+ // Sort by updatedAt descending (most recent first)
36
+ const sorted = [...sessions].sort((a, b) => {
37
+ const aMs = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
38
+ const bMs = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
39
+ return bMs - aMs;
40
+ });
41
+ // Show the 10 most recent sessions with all their timestamps and filter results
42
+ console.log('=== 10 most recent sessions ===');
43
+ for (const session of sorted.slice(0, 10)) {
44
+ const updatedAt = session.updatedAt || '(none)';
45
+ const updatedAtMs = session.updatedAt ? new Date(session.updatedAt).getTime() : 0;
46
+ const passesTimeFilter = updatedAtMs >= connectedAtMs;
47
+ const hasCwd = !!session.cwd;
48
+ const repo = hasCwd ? await getRepoInfo({ cwd: session.cwd }) : undefined;
49
+ console.log(` session: ${session.sessionId.slice(0, 12)}`);
50
+ console.log(` title: ${(session.title || '(none)').slice(0, 80)}`);
51
+ console.log(` cwd: ${session.cwd || '(none)'}`);
52
+ console.log(` updatedAt: ${updatedAt}`);
53
+ console.log(` passTime: ${passesTimeFilter}`);
54
+ console.log(` repo: ${repo ? repo.slug : '(no repo)'}`);
55
+ console.log();
56
+ }
57
+ // Summary: how many pass each filter
58
+ let noCwd = 0;
59
+ let tooOld = 0;
60
+ let noRepo = 0;
61
+ let wouldSync = 0;
62
+ for (const session of sessions) {
63
+ if (!session.cwd) {
64
+ noCwd++;
65
+ continue;
66
+ }
67
+ const updatedAtMs = session.updatedAt ? new Date(session.updatedAt).getTime() : 0;
68
+ if (updatedAtMs < connectedAtMs) {
69
+ tooOld++;
70
+ continue;
71
+ }
72
+ const repo = await getRepoInfo({ cwd: session.cwd });
73
+ if (!repo) {
74
+ noRepo++;
75
+ continue;
76
+ }
77
+ wouldSync++;
78
+ }
79
+ console.log('=== Summary ===');
80
+ console.log(` Total sessions: ${sessions.length}`);
81
+ console.log(` No cwd: ${noCwd}`);
82
+ console.log(` Too old: ${tooOld}`);
83
+ console.log(` No git repo: ${noRepo}`);
84
+ console.log(` Would sync: ${wouldSync}`);
85
+ expect(sessions.length).toBeGreaterThan(0);
86
+ }
87
+ finally {
88
+ agent.kill();
89
+ }
90
+ }, 120_000);
91
+ });
package/dist/cli.js CHANGED
@@ -8,9 +8,9 @@ import crypto from 'node:crypto';
8
8
  import path from 'node:path';
9
9
  import { exec } from 'node:child_process';
10
10
  import { readConfig, writeConfig } from "./config.js";
11
- import { connectAcp, listAllSessions } from "./acp-client.js";
11
+ import { connectAgent } from "./acp-client.js";
12
12
  import { getRepoInfo } from "./git.js";
13
- import { createNotionClient, createBoardDatabase, getRootPages } from "./notion.js";
13
+ import { createNotionClient, createBoardDatabase, createExamplePage, getRootPages } from "./notion.js";
14
14
  import { evictExistingInstance, getLockPort, startLockServer } from "./lock.js";
15
15
  import { startSyncLoop } from "./sync.js";
16
16
  import { enableStartupService, disableStartupService, isStartupServiceEnabled, getServiceLocationDescription, } from "./startup-service.js";
@@ -112,6 +112,7 @@ async function connectFlow() {
112
112
  options: [
113
113
  { value: 'opencode', label: 'OpenCode' },
114
114
  { value: 'claude', label: 'Claude Code' },
115
+ { value: 'codex', label: 'Codex' },
115
116
  ],
116
117
  required: true,
117
118
  });
@@ -129,8 +130,8 @@ async function connectFlow() {
129
130
  const connectedClients = [];
130
131
  for (const client of config.clients) {
131
132
  try {
132
- const acp = await connectAcp({ client });
133
- const sessions = await listAllSessions({ connection: acp.connection });
133
+ const acp = await connectAgent({ client });
134
+ const sessions = await acp.listSessions();
134
135
  // Extract unique repos from session cwds
135
136
  const cwds = [...new Set(sessions.map((sess) => sess.cwd).filter(Boolean))];
136
137
  const repoInfos = await Promise.all(cwds.map((cwd) => getRepoInfo({ cwd })));
@@ -237,11 +238,14 @@ async function connectFlow() {
237
238
  }
238
239
  return pageChoice;
239
240
  })();
240
- // Step 6: Create database
241
+ // Step 6: Create database and seed with an example page
241
242
  s.start('Creating board database...');
242
243
  const { databaseId } = await createBoardDatabase({ notion, pageId });
244
+ await createExamplePage({ notion, databaseId });
243
245
  s.stop('Board database created');
244
- log.success('Open the database in Notion and click "+ Add a view" → Board, grouped by Status.');
246
+ // Show link to the Notion page so user can open it directly
247
+ const notionPageUrl = `https://notion.so/${pageId.replace(/-/g, '')}`;
248
+ log.success(`Board created: ${notionPageUrl}`);
245
249
  // Step 7: Save to config
246
250
  const board = {
247
251
  notionToken: authResult.accessToken,
@@ -274,14 +278,9 @@ async function connectFlow() {
274
278
  else {
275
279
  log.info(`Already registered at ${getServiceLocationDescription()}`);
276
280
  }
277
- // Step 9: Spawn daemon in background so syncing starts immediately
278
- const { spawn: spawnProcess } = await import('node:child_process');
279
- const child = spawnProcess(process.execPath, [openplexerBin], {
280
- detached: true,
281
- stdio: 'ignore',
282
- });
283
- child.unref();
284
- outro('Board connected! Sync daemon started in background.');
281
+ outro('Board connected! Starting sync, keep this process running.');
282
+ // Transition directly into the sync daemon instead of spawning a child
283
+ await startDaemon(config);
285
284
  }
286
285
  // --- Daemon ---
287
286
  async function startDaemon(config) {
@@ -292,7 +291,7 @@ async function startDaemon(config) {
292
291
  const connections = [];
293
292
  for (const client of config.clients) {
294
293
  try {
295
- const acp = await connectAcp({ client });
294
+ const acp = await connectAgent({ client });
296
295
  connections.push(acp);
297
296
  console.log(`Connected to ${client} via ACP`);
298
297
  }
package/dist/config.d.ts CHANGED
@@ -21,7 +21,7 @@ export type OpenplexerBoard = {
21
21
  * created or last updated after this time are synced. */
22
22
  connectedAt: string;
23
23
  };
24
- export type AcpClient = 'opencode' | 'claude';
24
+ export type AcpClient = 'opencode' | 'claude' | 'codex';
25
25
  export type OpenplexerConfig = {
26
26
  /** ACP clients to connect to (user may use both opencode and claude) */
27
27
  clients: AcpClient[];
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,eAAe,GAAG;IAC5B,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,4CAA4C;IAC5C,YAAY,EAAE,MAAM,CAAA;IACpB,uBAAuB;IACvB,cAAc,EAAE,MAAM,CAAA;IACtB,0BAA0B;IAC1B,iBAAiB,EAAE,MAAM,CAAA;IACzB,4BAA4B;IAC5B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAA;IACpB,0CAA0C;IAC1C,gBAAgB,EAAE,MAAM,CAAA;IACxB,mEAAmE;IACnE,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,8DAA8D;IAC9D,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC;8DAC0D;IAC1D,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAA;AAE7C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wEAAwE;IACxE,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,wCAAwC;IACxC,MAAM,EAAE,eAAe,EAAE,CAAA;CAC1B,CAAA;AAKD,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,UAAU,IAAI,gBAAgB,GAAG,SAAS,CAOzD;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAK1D"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,eAAe,GAAG;IAC5B,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,4CAA4C;IAC5C,YAAY,EAAE,MAAM,CAAA;IACpB,uBAAuB;IACvB,cAAc,EAAE,MAAM,CAAA;IACtB,0BAA0B;IAC1B,iBAAiB,EAAE,MAAM,CAAA;IACzB,4BAA4B;IAC5B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAA;IACpB,0CAA0C;IAC1C,gBAAgB,EAAE,MAAM,CAAA;IACxB,mEAAmE;IACnE,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,8DAA8D;IAC9D,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC;8DAC0D;IAC1D,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEvD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wEAAwE;IACxE,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,wCAAwC;IACxC,MAAM,EAAE,eAAe,EAAE,CAAA;CAC1B,CAAA;AAKD,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,UAAU,IAAI,gBAAgB,GAAG,SAAS,CAOzD;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAK1D"}
package/dist/notion.d.ts CHANGED
@@ -8,12 +8,6 @@ export declare const STATUS_OPTIONS: ({
8
8
  } | {
9
9
  name: string;
10
10
  color: "green";
11
- } | {
12
- name: string;
13
- color: "red";
14
- } | {
15
- name: string;
16
- color: "gray";
17
11
  })[];
18
12
  export type CreateDatabaseResult = {
19
13
  databaseId: string;
@@ -34,6 +28,10 @@ export declare function createBoardDatabase({ notion, pageId, }: {
34
28
  notion: Client;
35
29
  pageId: string;
36
30
  }): Promise<CreateDatabaseResult>;
31
+ export declare function createExamplePage({ notion, databaseId, }: {
32
+ notion: Client;
33
+ databaseId: string;
34
+ }): Promise<string>;
37
35
  export declare function createSessionPage({ notion, databaseId, title, sessionId, status, repoSlug, branchUrl, shareUrl, resumeCommand, assigneeId, folder, discordUrl, updatedAt, }: {
38
36
  notion: Client;
39
37
  databaseId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"notion.d.ts","sourceRoot":"","sources":["../src/notion.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAEzC,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;IAM1B,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAEvE;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAMD,wBAAsB,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CA6CtF;AAED,wBAAsB,mBAAmB,CAAC,EACxC,MAAM,EACN,MAAM,GACP,EAAE;IACD,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAuChC;AAED,wBAAsB,iBAAiB,CAAC,EACtC,MAAM,EACN,UAAU,EACV,KAAK,EACL,SAAS,EACT,MAAM,EACN,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,aAAa,EACb,UAAU,EACV,MAAM,EACN,UAAU,EACV,SAAS,GACV,EAAE;IACD,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgClB;AAED,wBAAsB,iBAAiB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,KAAK,EACL,SAAS,GACV,EAAE;IACD,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBhB;AAMD,wBAAsB,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAUzE"}
1
+ {"version":3,"file":"notion.d.ts","sourceRoot":"","sources":["../src/notion.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAEzC,eAAO,MAAM,cAAc;;;;;;;;;IAI1B,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAEvE;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAMD,wBAAsB,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAiDtF;AAED,wBAAsB,mBAAmB,CAAC,EACxC,MAAM,EACN,MAAM,GACP,EAAE;IACD,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAuEhC;AAGD,wBAAsB,iBAAiB,CAAC,EACtC,MAAM,EACN,UAAU,GACX,EAAE;IACD,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;CACnB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8GlB;AAED,wBAAsB,iBAAiB,CAAC,EACtC,MAAM,EACN,UAAU,EACV,KAAK,EACL,SAAS,EACT,MAAM,EACN,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,aAAa,EACb,UAAU,EACV,MAAM,EACN,UAAU,EACV,SAAS,GACV,EAAE;IACD,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgClB;AAED,wBAAsB,iBAAiB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,KAAK,EACL,SAAS,GACV,EAAE;IACD,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBhB;AAMD,wBAAsB,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAUzE"}