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 +195 -0
- package/dist/acp-client.d.ts +13 -8
- package/dist/acp-client.d.ts.map +1 -1
- package/dist/acp-client.js +127 -23
- package/dist/acp-client.test.d.ts +2 -0
- package/dist/acp-client.test.d.ts.map +1 -0
- package/dist/acp-client.test.js +91 -0
- package/dist/cli.js +14 -15
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/notion.d.ts +4 -6
- package/dist/notion.d.ts.map +1 -1
- package/dist/notion.js +149 -7
- package/dist/sync.d.ts +2 -2
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +51 -33
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +68 -11
- package/package.json +11 -7
- package/src/acp-client.test.ts +95 -0
- package/src/acp-client.ts +158 -35
- package/src/cli.ts +16 -16
- package/src/config.ts +1 -1
- package/src/notion.ts +160 -7
- package/src/sync.ts +52 -35
- package/src/worker.ts +71 -11
- package/LICENSE +0 -21
package/src/acp-client.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
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
|
|
|
4
|
-
import { spawn } from 'node:child_process'
|
|
11
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
12
|
+
import { createRequire } from 'node:module'
|
|
13
|
+
import path from 'node:path'
|
|
5
14
|
import { Writable, Readable } from 'node:stream'
|
|
6
15
|
import {
|
|
7
16
|
ClientSideConnection,
|
|
@@ -10,6 +19,102 @@ import {
|
|
|
10
19
|
type Client,
|
|
11
20
|
type SessionInfo,
|
|
12
21
|
} from '@agentclientprotocol/sdk'
|
|
22
|
+
import { createOpencodeClient } from '@opencode-ai/sdk/v2'
|
|
23
|
+
import type { AcpClient } from './config.ts'
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Shared types
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export type AgentConnection = {
|
|
30
|
+
client: AcpClient
|
|
31
|
+
listSessions: () => Promise<SessionInfo[]>
|
|
32
|
+
kill: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Keep the old type as an alias for backwards compat in sync.ts
|
|
36
|
+
export type AcpConnection = AgentConnection
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// opencode — HTTP server with /experimental/session (global, all projects)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
async function connectOpencode(): Promise<AgentConnection> {
|
|
43
|
+
const PORT = 18_923
|
|
44
|
+
const baseUrl = `http://127.0.0.1:${PORT}`
|
|
45
|
+
|
|
46
|
+
// Spawn `opencode serve` on a known port. cwd doesn't matter since
|
|
47
|
+
// we use the global endpoint.
|
|
48
|
+
const child = spawn('opencode', ['serve', '--port', String(PORT)], {
|
|
49
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
50
|
+
cwd: '/',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const sdk = createOpencodeClient({ baseUrl })
|
|
54
|
+
|
|
55
|
+
// Wait for the server to be ready (poll until it responds)
|
|
56
|
+
const deadline = Date.now() + 15_000
|
|
57
|
+
while (Date.now() < deadline) {
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(`${baseUrl}/session?limit=1`)
|
|
60
|
+
if (res.ok) break
|
|
61
|
+
} catch {
|
|
62
|
+
// server not ready yet
|
|
63
|
+
}
|
|
64
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Verify it's actually up
|
|
68
|
+
const check = await fetch(`${baseUrl}/session?limit=1`).catch(() => null)
|
|
69
|
+
if (!check?.ok) {
|
|
70
|
+
child.kill()
|
|
71
|
+
throw new Error('opencode serve failed to start')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
client: 'opencode',
|
|
76
|
+
listSessions: async () => {
|
|
77
|
+
const sessions: SessionInfo[] = []
|
|
78
|
+
let cursor: number | undefined
|
|
79
|
+
|
|
80
|
+
// Paginate through /experimental/session which uses Session.listGlobal()
|
|
81
|
+
// (returns sessions across ALL projects, not scoped to one)
|
|
82
|
+
while (true) {
|
|
83
|
+
const result = await sdk.experimental.session.list({
|
|
84
|
+
roots: true,
|
|
85
|
+
...(cursor !== undefined && { cursor }),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (result.error || !result.data) {
|
|
89
|
+
throw new Error(`opencode API error: ${result.error}`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const s of result.data) {
|
|
93
|
+
sessions.push({
|
|
94
|
+
sessionId: s.id,
|
|
95
|
+
cwd: s.directory,
|
|
96
|
+
title: s.title,
|
|
97
|
+
updatedAt: new Date(s.time.updated).toISOString(),
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Pagination cursor is in the x-next-cursor response header
|
|
102
|
+
const nextCursor = result.response.headers.get('x-next-cursor')
|
|
103
|
+
if (!nextCursor) break
|
|
104
|
+
cursor = Number(nextCursor)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return sessions
|
|
108
|
+
},
|
|
109
|
+
kill: () => {
|
|
110
|
+
child.kill()
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// claude / codex — ACP over stdio
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
13
118
|
|
|
14
119
|
function nodeToWebWritable(nodeStream: Writable): WritableStream<Uint8Array> {
|
|
15
120
|
return new WritableStream<Uint8Array>({
|
|
@@ -43,9 +148,6 @@ function nodeToWebReadable(nodeStream: Readable): ReadableStream<Uint8Array> {
|
|
|
43
148
|
})
|
|
44
149
|
}
|
|
45
150
|
|
|
46
|
-
// Minimal Client implementation — we only need session listing,
|
|
47
|
-
// not file ops or permissions. requestPermission and sessionUpdate
|
|
48
|
-
// are required by the Client interface.
|
|
49
151
|
class MinimalClient implements Client {
|
|
50
152
|
async requestPermission() {
|
|
51
153
|
return { outcome: { outcome: 'cancelled' as const } }
|
|
@@ -59,22 +161,28 @@ class MinimalClient implements Client {
|
|
|
59
161
|
}
|
|
60
162
|
}
|
|
61
163
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
client
|
|
65
|
-
|
|
164
|
+
function resolveAcpBinary(client: 'claude' | 'codex'): { cmd: string; args: string[] } {
|
|
165
|
+
const require = createRequire(import.meta.url)
|
|
166
|
+
const packageName = client === 'claude'
|
|
167
|
+
? '@zed-industries/claude-agent-acp'
|
|
168
|
+
: '@zed-industries/codex-acp'
|
|
169
|
+
const binName = client === 'claude' ? 'claude-agent-acp' : 'codex-acp'
|
|
170
|
+
|
|
171
|
+
const pkgJsonPath = require.resolve(`${packageName}/package.json`)
|
|
172
|
+
const pkgDir = path.dirname(pkgJsonPath)
|
|
173
|
+
const pkg = require(pkgJsonPath) as { bin: string | Record<string, string> }
|
|
174
|
+
const binRelative = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin[binName]
|
|
175
|
+
const binPath = path.resolve(pkgDir, binRelative)
|
|
176
|
+
|
|
177
|
+
return { cmd: process.execPath, args: [binPath] }
|
|
66
178
|
}
|
|
67
179
|
|
|
68
|
-
|
|
69
|
-
client
|
|
70
|
-
}: {
|
|
71
|
-
client: 'opencode' | 'claude'
|
|
72
|
-
}): Promise<AcpConnection> {
|
|
73
|
-
const cmd = client === 'opencode' ? 'opencode' : 'claude'
|
|
74
|
-
const args = ['acp']
|
|
180
|
+
async function connectAcpAgent(client: 'claude' | 'codex'): Promise<AgentConnection> {
|
|
181
|
+
const { cmd, args } = resolveAcpBinary(client)
|
|
75
182
|
|
|
76
183
|
const child = spawn(cmd, args, {
|
|
77
184
|
stdio: ['pipe', 'pipe', 'inherit'],
|
|
185
|
+
cwd: '/',
|
|
78
186
|
})
|
|
79
187
|
|
|
80
188
|
const stream = ndJsonStream(
|
|
@@ -92,33 +200,48 @@ export async function connectAcp({
|
|
|
92
200
|
})
|
|
93
201
|
|
|
94
202
|
return {
|
|
95
|
-
connection,
|
|
96
203
|
client,
|
|
204
|
+
listSessions: async () => {
|
|
205
|
+
const sessions: SessionInfo[] = []
|
|
206
|
+
let cursor: string | undefined
|
|
207
|
+
|
|
208
|
+
while (true) {
|
|
209
|
+
const response = await connection.listSessions({
|
|
210
|
+
...(cursor ? { cursor } : {}),
|
|
211
|
+
})
|
|
212
|
+
sessions.push(...response.sessions)
|
|
213
|
+
if (!response.nextCursor) break
|
|
214
|
+
cursor = response.nextCursor
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return sessions
|
|
218
|
+
},
|
|
97
219
|
kill: () => {
|
|
98
220
|
child.kill()
|
|
99
221
|
},
|
|
100
222
|
}
|
|
101
223
|
}
|
|
102
224
|
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Public API
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
export async function connectAgent({ client }: { client: AcpClient }): Promise<AgentConnection> {
|
|
230
|
+
if (client === 'opencode') {
|
|
231
|
+
return connectOpencode()
|
|
232
|
+
}
|
|
233
|
+
return connectAcpAgent(client)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Legacy exports for backwards compat
|
|
237
|
+
export async function connectAcp({ client }: { client: AcpClient }): Promise<AgentConnection> {
|
|
238
|
+
return connectAgent({ client })
|
|
239
|
+
}
|
|
240
|
+
|
|
103
241
|
export async function listAllSessions({
|
|
104
242
|
connection,
|
|
105
243
|
}: {
|
|
106
|
-
connection:
|
|
244
|
+
connection: AgentConnection
|
|
107
245
|
}): Promise<SessionInfo[]> {
|
|
108
|
-
|
|
109
|
-
let cursor: string | undefined
|
|
110
|
-
|
|
111
|
-
// Paginate through all sessions
|
|
112
|
-
while (true) {
|
|
113
|
-
const response = await connection.listSessions({
|
|
114
|
-
...(cursor ? { cursor } : {}),
|
|
115
|
-
})
|
|
116
|
-
sessions.push(...response.sessions)
|
|
117
|
-
if (!response.nextCursor) {
|
|
118
|
-
break
|
|
119
|
-
}
|
|
120
|
-
cursor = response.nextCursor
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return sessions
|
|
246
|
+
return connection.listSessions()
|
|
124
247
|
}
|
package/src/cli.ts
CHANGED
|
@@ -21,9 +21,9 @@ import crypto from 'node:crypto'
|
|
|
21
21
|
import path from 'node:path'
|
|
22
22
|
import { exec } from 'node:child_process'
|
|
23
23
|
import { readConfig, writeConfig, type OpenplexerConfig, type OpenplexerBoard, type AcpClient } from './config.ts'
|
|
24
|
-
import {
|
|
24
|
+
import { connectAgent, type AgentConnection } from './acp-client.ts'
|
|
25
25
|
import { getRepoInfo } from './git.ts'
|
|
26
|
-
import { createNotionClient, createBoardDatabase, getRootPages } from './notion.ts'
|
|
26
|
+
import { createNotionClient, createBoardDatabase, createExamplePage, getRootPages } from './notion.ts'
|
|
27
27
|
import { evictExistingInstance, getLockPort, startLockServer } from './lock.ts'
|
|
28
28
|
import { startSyncLoop } from './sync.ts'
|
|
29
29
|
import {
|
|
@@ -149,6 +149,7 @@ async function connectFlow(): Promise<void> {
|
|
|
149
149
|
options: [
|
|
150
150
|
{ value: 'opencode' as const, label: 'OpenCode' },
|
|
151
151
|
{ value: 'claude' as const, label: 'Claude Code' },
|
|
152
|
+
{ value: 'codex' as const, label: 'Codex' },
|
|
152
153
|
],
|
|
153
154
|
required: true,
|
|
154
155
|
})
|
|
@@ -169,8 +170,8 @@ async function connectFlow(): Promise<void> {
|
|
|
169
170
|
|
|
170
171
|
for (const client of config.clients) {
|
|
171
172
|
try {
|
|
172
|
-
const acp = await
|
|
173
|
-
const sessions = await
|
|
173
|
+
const acp = await connectAgent({ client })
|
|
174
|
+
const sessions = await acp.listSessions()
|
|
174
175
|
|
|
175
176
|
// Extract unique repos from session cwds
|
|
176
177
|
const cwds = [...new Set(sessions.map((sess) => sess.cwd).filter(Boolean))] as string[]
|
|
@@ -240,6 +241,7 @@ async function connectFlow(): Promise<void> {
|
|
|
240
241
|
workspaceName: string
|
|
241
242
|
notionUserId?: string
|
|
242
243
|
notionUserName?: string
|
|
244
|
+
duplicatedTemplateId?: string | null
|
|
243
245
|
}
|
|
244
246
|
let authResult: AuthResult | undefined
|
|
245
247
|
const maxAttempts = 150 // 5 minutes at 2s intervals
|
|
@@ -305,12 +307,15 @@ async function connectFlow(): Promise<void> {
|
|
|
305
307
|
return pageChoice
|
|
306
308
|
})()
|
|
307
309
|
|
|
308
|
-
// Step 6: Create database
|
|
310
|
+
// Step 6: Create database and seed with an example page
|
|
309
311
|
s.start('Creating board database...')
|
|
310
312
|
const { databaseId } = await createBoardDatabase({ notion, pageId })
|
|
313
|
+
await createExamplePage({ notion, databaseId })
|
|
311
314
|
s.stop('Board database created')
|
|
312
315
|
|
|
313
|
-
|
|
316
|
+
// Show link to the Notion page so user can open it directly
|
|
317
|
+
const notionPageUrl = `https://notion.so/${pageId.replace(/-/g, '')}`
|
|
318
|
+
log.success(`Board created: ${notionPageUrl}`)
|
|
314
319
|
|
|
315
320
|
// Step 7: Save to config
|
|
316
321
|
const board: OpenplexerBoard = {
|
|
@@ -347,15 +352,10 @@ async function connectFlow(): Promise<void> {
|
|
|
347
352
|
log.info(`Already registered at ${getServiceLocationDescription()}`)
|
|
348
353
|
}
|
|
349
354
|
|
|
350
|
-
|
|
351
|
-
const { spawn: spawnProcess } = await import('node:child_process')
|
|
352
|
-
const child = spawnProcess(process.execPath, [openplexerBin], {
|
|
353
|
-
detached: true,
|
|
354
|
-
stdio: 'ignore',
|
|
355
|
-
})
|
|
356
|
-
child.unref()
|
|
355
|
+
outro('Board connected! Starting sync, keep this process running.')
|
|
357
356
|
|
|
358
|
-
|
|
357
|
+
// Transition directly into the sync daemon instead of spawning a child
|
|
358
|
+
await startDaemon(config)
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
// --- Daemon ---
|
|
@@ -367,10 +367,10 @@ async function startDaemon(config: OpenplexerConfig): Promise<void> {
|
|
|
367
367
|
|
|
368
368
|
console.log(`openplexer daemon started (PID ${process.pid}, port ${port})`)
|
|
369
369
|
|
|
370
|
-
const connections:
|
|
370
|
+
const connections: AgentConnection[] = []
|
|
371
371
|
for (const client of config.clients) {
|
|
372
372
|
try {
|
|
373
|
-
const acp = await
|
|
373
|
+
const acp = await connectAgent({ client })
|
|
374
374
|
connections.push(acp)
|
|
375
375
|
console.log(`Connected to ${client} via ACP`)
|
|
376
376
|
} catch {
|
package/src/config.ts
CHANGED
|
@@ -30,7 +30,7 @@ export type OpenplexerBoard = {
|
|
|
30
30
|
connectedAt: string
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export type AcpClient = 'opencode' | 'claude'
|
|
33
|
+
export type AcpClient = 'opencode' | 'claude' | 'codex'
|
|
34
34
|
|
|
35
35
|
export type OpenplexerConfig = {
|
|
36
36
|
/** ACP clients to connect to (user may use both opencode and claude) */
|
package/src/notion.ts
CHANGED
|
@@ -7,8 +7,6 @@ export const STATUS_OPTIONS = [
|
|
|
7
7
|
{ name: 'Not Started', color: 'default' as const },
|
|
8
8
|
{ name: 'In Progress', color: 'blue' as const },
|
|
9
9
|
{ name: 'Done', color: 'green' as const },
|
|
10
|
-
{ name: 'Needs Attention', color: 'red' as const },
|
|
11
|
-
{ name: 'Ignored', color: 'gray' as const },
|
|
12
10
|
]
|
|
13
11
|
|
|
14
12
|
export type CreateDatabaseResult = {
|
|
@@ -26,10 +24,10 @@ export type RootPage = {
|
|
|
26
24
|
icon: string
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
// Get
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
27
|
+
// Get root-level pages (parent.type === 'workspace') using notion.search.
|
|
28
|
+
// Only pages are returned (not databases). With OAuth integrations,
|
|
29
|
+
// only pages the user explicitly shared during consent are searchable,
|
|
30
|
+
// so users must share root-level pages for them to appear here.
|
|
33
31
|
export async function getRootPages({ notion }: { notion: Client }): Promise<RootPage[]> {
|
|
34
32
|
const pages: RootPage[] = []
|
|
35
33
|
let startCursor: string | undefined
|
|
@@ -46,6 +44,10 @@ export async function getRootPages({ notion }: { notion: Client }): Promise<Root
|
|
|
46
44
|
if (!('parent' in result) || !('properties' in result)) {
|
|
47
45
|
continue
|
|
48
46
|
}
|
|
47
|
+
// Only show root pages (direct children of workspace), skip databases
|
|
48
|
+
if (result.object !== 'page' || result.parent.type !== 'workspace') {
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
49
51
|
|
|
50
52
|
const titleProp = Object.values(result.properties).find((p) => p.type === 'title')
|
|
51
53
|
const title = (() => {
|
|
@@ -86,6 +88,7 @@ export async function createBoardDatabase({
|
|
|
86
88
|
}): Promise<CreateDatabaseResult> {
|
|
87
89
|
const database = await notion.databases.create({
|
|
88
90
|
parent: { type: 'page_id', page_id: pageId },
|
|
91
|
+
is_inline: true,
|
|
89
92
|
title: [{ text: { content: 'openplexer - Coding Sessions' } }],
|
|
90
93
|
initial_data_source: {
|
|
91
94
|
properties: {
|
|
@@ -108,22 +111,172 @@ export async function createBoardDatabase({
|
|
|
108
111
|
})
|
|
109
112
|
|
|
110
113
|
// Database is created with a default Table view. Create a Board view
|
|
111
|
-
// grouped by Status so sessions show as a kanban board
|
|
114
|
+
// grouped by Status so sessions show as a kanban board, then delete the
|
|
115
|
+
// default Table view so Board becomes the default.
|
|
112
116
|
const dataSourceId = 'data_sources' in database
|
|
113
117
|
? database.data_sources?.[0]?.id
|
|
114
118
|
: undefined
|
|
115
119
|
if (dataSourceId) {
|
|
120
|
+
// Retrieve the data source to get the Status property ID for group_by
|
|
121
|
+
const dataSource = await notion.dataSources.retrieve({ data_source_id: dataSourceId })
|
|
122
|
+
const statusPropertyId = 'properties' in dataSource
|
|
123
|
+
? Object.entries(dataSource.properties as Record<string, { id: string; type: string }>)
|
|
124
|
+
.find(([name]) => name === 'Status')?.[1]?.id
|
|
125
|
+
: undefined
|
|
126
|
+
|
|
127
|
+
// List existing views (should contain the auto-created Table view)
|
|
128
|
+
const existingViews = await notion.views.list({ database_id: database.id })
|
|
129
|
+
const tableViewIds = existingViews.results.map((v) => v.id)
|
|
130
|
+
|
|
131
|
+
// Create the Board view, grouped by Status
|
|
116
132
|
await notion.views.create({
|
|
117
133
|
database_id: database.id,
|
|
118
134
|
data_source_id: dataSourceId,
|
|
119
135
|
name: 'Board',
|
|
120
136
|
type: 'board',
|
|
137
|
+
...(statusPropertyId && {
|
|
138
|
+
configuration: {
|
|
139
|
+
type: 'board' as const,
|
|
140
|
+
group_by: {
|
|
141
|
+
type: 'select' as const,
|
|
142
|
+
property_id: statusPropertyId,
|
|
143
|
+
sort: { type: 'manual' as const },
|
|
144
|
+
hide_empty_groups: false,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
121
148
|
})
|
|
149
|
+
|
|
150
|
+
// Delete the default Table view(s) so Board is the only (and default) view
|
|
151
|
+
for (const viewId of tableViewIds) {
|
|
152
|
+
await notion.views.delete({ view_id: viewId }).catch(() => {
|
|
153
|
+
// Ignore errors — can't delete the last view, but we just created Board
|
|
154
|
+
})
|
|
155
|
+
}
|
|
122
156
|
}
|
|
123
157
|
|
|
124
158
|
return { databaseId: database.id }
|
|
125
159
|
}
|
|
126
160
|
|
|
161
|
+
// Create an example page in the database explaining how sessions appear.
|
|
162
|
+
export async function createExamplePage({
|
|
163
|
+
notion,
|
|
164
|
+
databaseId,
|
|
165
|
+
}: {
|
|
166
|
+
notion: Client
|
|
167
|
+
databaseId: string
|
|
168
|
+
}): Promise<string> {
|
|
169
|
+
const page = await notion.pages.create({
|
|
170
|
+
parent: { database_id: databaseId },
|
|
171
|
+
properties: {
|
|
172
|
+
Name: { title: [{ text: { content: 'Sessions will appear here automatically' } }] },
|
|
173
|
+
Status: { select: { name: 'In Progress' } },
|
|
174
|
+
'Session ID': { rich_text: [{ text: { content: 'example' } }] },
|
|
175
|
+
Repo: { select: { name: 'owner/repo' } },
|
|
176
|
+
Resume: { rich_text: [{ text: { content: 'opencode --session <id>' } }] },
|
|
177
|
+
Folder: { rich_text: [{ text: { content: '/path/to/project' } }] },
|
|
178
|
+
} as Parameters<Client['pages']['create']>[0]['properties'],
|
|
179
|
+
children: [
|
|
180
|
+
{
|
|
181
|
+
type: 'paragraph',
|
|
182
|
+
paragraph: {
|
|
183
|
+
rich_text: [
|
|
184
|
+
{
|
|
185
|
+
type: 'text',
|
|
186
|
+
text: { content: 'Each card on this board represents a coding session from OpenCode, Claude Code, or Codex. openplexer syncs them automatically every few seconds.' },
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{ type: 'divider', divider: {} },
|
|
192
|
+
{
|
|
193
|
+
type: 'heading_3',
|
|
194
|
+
heading_3: {
|
|
195
|
+
rich_text: [{ type: 'text', text: { content: 'What each field means' } }],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: 'bulleted_list_item',
|
|
200
|
+
bulleted_list_item: {
|
|
201
|
+
rich_text: [
|
|
202
|
+
{ type: 'text', text: { content: 'Status' }, annotations: { bold: true } },
|
|
203
|
+
{ type: 'text', text: { content: ' — In Progress while the session is active, Done when finished. You can set Needs Attention or Ignored manually.' } },
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
type: 'bulleted_list_item',
|
|
209
|
+
bulleted_list_item: {
|
|
210
|
+
rich_text: [
|
|
211
|
+
{ type: 'text', text: { content: 'Repo' }, annotations: { bold: true } },
|
|
212
|
+
{ type: 'text', text: { content: ' — The GitHub repository the session is working in (owner/repo).' } },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
type: 'bulleted_list_item',
|
|
218
|
+
bulleted_list_item: {
|
|
219
|
+
rich_text: [
|
|
220
|
+
{ type: 'text', text: { content: 'Branch' }, annotations: { bold: true } },
|
|
221
|
+
{ type: 'text', text: { content: ' — Link to the git branch on GitHub.' } },
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
type: 'bulleted_list_item',
|
|
227
|
+
bulleted_list_item: {
|
|
228
|
+
rich_text: [
|
|
229
|
+
{ type: 'text', text: { content: 'Resume' }, annotations: { bold: true } },
|
|
230
|
+
{ type: 'text', text: { content: ' — Command to resume the session in your terminal.' } },
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: 'bulleted_list_item',
|
|
236
|
+
bulleted_list_item: {
|
|
237
|
+
rich_text: [
|
|
238
|
+
{ type: 'text', text: { content: 'Share URL' }, annotations: { bold: true } },
|
|
239
|
+
{ type: 'text', text: { content: ' — Public share link for the session (if available).' } },
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
type: 'bulleted_list_item',
|
|
245
|
+
bulleted_list_item: {
|
|
246
|
+
rich_text: [
|
|
247
|
+
{ type: 'text', text: { content: 'Discord' }, annotations: { bold: true } },
|
|
248
|
+
{ type: 'text', text: { content: ' — Link to the Discord thread (if using kimaki).' } },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
type: 'bulleted_list_item',
|
|
254
|
+
bulleted_list_item: {
|
|
255
|
+
rich_text: [
|
|
256
|
+
{ type: 'text', text: { content: 'Assignee' }, annotations: { bold: true } },
|
|
257
|
+
{ type: 'text', text: { content: ' — The Notion user who authorized the integration.' } },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{ type: 'divider', divider: {} },
|
|
262
|
+
{
|
|
263
|
+
type: 'paragraph',
|
|
264
|
+
paragraph: {
|
|
265
|
+
rich_text: [
|
|
266
|
+
{
|
|
267
|
+
type: 'text',
|
|
268
|
+
text: { content: 'You can archive this card once real sessions start appearing.' },
|
|
269
|
+
annotations: { italic: true, color: 'gray' },
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
] as Parameters<Client['blocks']['children']['append']>[0]['children'],
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
return page.id
|
|
278
|
+
}
|
|
279
|
+
|
|
127
280
|
export async function createSessionPage({
|
|
128
281
|
notion,
|
|
129
282
|
databaseId,
|