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/src/sync.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import type { SessionInfo } from '@agentclientprotocol/sdk'
6
6
  import type { OpenplexerBoard, OpenplexerConfig, AcpClient } from './config.ts'
7
7
  import { writeConfig } from './config.ts'
8
- import { listAllSessions, type AcpConnection } from './acp-client.ts'
8
+ import { type AgentConnection } from './acp-client.ts'
9
9
  import { getRepoInfo } from './git.ts'
10
10
  import {
11
11
  createNotionClient,
@@ -24,7 +24,7 @@ export async function startSyncLoop({
24
24
  acpConnections,
25
25
  }: {
26
26
  config: OpenplexerConfig
27
- acpConnections: AcpConnection[]
27
+ acpConnections: AgentConnection[]
28
28
  }): Promise<void> {
29
29
  console.log(`Syncing ${config.boards.length} board(s) every ${SYNC_INTERVAL_MS / 1000}s`)
30
30
 
@@ -48,19 +48,23 @@ async function syncOnce({
48
48
  acpConnections,
49
49
  }: {
50
50
  config: OpenplexerConfig
51
- acpConnections: AcpConnection[]
51
+ acpConnections: AgentConnection[]
52
52
  }): Promise<void> {
53
- // Collect sessions from all ACP connections, tagged with their source
53
+ // Collect sessions from all agent connections, tagged with their source
54
54
  const sessions: TaggedSession[] = []
55
55
  const seenIds = new Set<string>()
56
56
 
57
- for (const acp of acpConnections) {
58
- const clientSessions = await listAllSessions({ connection: acp.connection })
59
- for (const session of clientSessions) {
60
- if (!seenIds.has(session.sessionId)) {
61
- seenIds.add(session.sessionId)
62
- sessions.push({ ...session, source: acp.client })
57
+ for (const agent of acpConnections) {
58
+ try {
59
+ const clientSessions = await agent.listSessions()
60
+ for (const session of clientSessions) {
61
+ if (!seenIds.has(session.sessionId)) {
62
+ seenIds.add(session.sessionId)
63
+ sessions.push({ ...session, source: agent.client })
64
+ }
63
65
  }
66
+ } catch (err) {
67
+ console.error(`Error listing sessions from ${agent.client}:`, err instanceof Error ? err.message : err)
64
68
  }
65
69
  }
66
70
 
@@ -122,49 +126,62 @@ async function syncBoard({
122
126
  for (const { session, repoSlug, repoUrl, branch } of filteredSessions) {
123
127
  const existingPageId = board.syncedSessions[session.sessionId]
124
128
 
129
+ const title = session.title || `Session ${session.sessionId.slice(0, 8)}`
130
+
125
131
  if (existingPageId) {
126
132
  // Update existing page
127
- await rateLimitedCall(() => {
128
- return updateSessionPage({
129
- notion,
130
- pageId: existingPageId,
131
- title: session.title || undefined,
132
- updatedAt: session.updatedAt || undefined,
133
+ try {
134
+ await rateLimitedCall(() => {
135
+ return updateSessionPage({
136
+ notion,
137
+ pageId: existingPageId,
138
+ title: session.title || undefined,
139
+ updatedAt: session.updatedAt || undefined,
140
+ })
133
141
  })
134
- })
142
+ } catch (err) {
143
+ console.error(`Error updating "${title}" (${repoSlug}):`, err instanceof Error ? err.message : err)
144
+ }
135
145
  } else {
136
146
  // Create new page
137
- const title = session.title || `Session ${session.sessionId.slice(0, 8)}`
138
147
  const branchUrl = `${repoUrl}/tree/${branch}`
139
148
  const resumeCommand = (() => {
140
149
  if (session.source === 'opencode') {
141
150
  return `opencode --session ${session.sessionId}`
142
151
  }
152
+ if (session.source === 'codex') {
153
+ return `codex resume ${session.sessionId}`
154
+ }
143
155
  return `claude --resume ${session.sessionId}`
144
156
  })()
145
157
 
146
158
  // Try to get Discord URL if kimaki is available
147
159
  const discordUrl = await getKimakiDiscordUrl(session.sessionId)
148
160
 
149
- const pageId = await rateLimitedCall(() => {
150
- return createSessionPage({
151
- notion,
152
- databaseId: board.notionDatabaseId,
153
- title,
154
- sessionId: session.sessionId,
155
- status: 'In Progress',
156
- repoSlug,
157
- branchUrl,
158
- resumeCommand,
159
- assigneeId: board.notionUserId,
160
- folder: session.cwd || '',
161
- discordUrl: discordUrl || undefined,
162
- updatedAt: session.updatedAt || undefined,
161
+ try {
162
+ const pageId = await rateLimitedCall(() => {
163
+ return createSessionPage({
164
+ notion,
165
+ databaseId: board.notionDatabaseId,
166
+ title,
167
+ sessionId: session.sessionId,
168
+ status: 'In Progress',
169
+ repoSlug,
170
+ branchUrl,
171
+ resumeCommand,
172
+ assigneeId: board.notionUserId,
173
+ folder: session.cwd || '',
174
+ discordUrl: discordUrl || undefined,
175
+ updatedAt: session.updatedAt || undefined,
176
+ })
163
177
  })
164
- })
165
178
 
166
- board.syncedSessions[session.sessionId] = pageId
167
- console.log(` + ${title} (${repoSlug})`)
179
+ board.syncedSessions[session.sessionId] = pageId
180
+ const notionUrl = `https://notion.so/${pageId.replace(/-/g, '')}`
181
+ console.log(`+ Added "${title}" (${repoSlug}) → ${notionUrl}`)
182
+ } catch (err) {
183
+ console.error(`Error adding "${title}" (${repoSlug}):`, err instanceof Error ? err.message : err)
184
+ }
168
185
  }
169
186
  }
170
187
  }
package/src/worker.ts CHANGED
@@ -23,7 +23,7 @@ const app = new Spiceflow()
23
23
  handler() {
24
24
  return new Response(null, {
25
25
  status: 302,
26
- headers: { Location: 'https://github.com/remorses/kimaki/tree/main/openplexer' },
26
+ headers: { Location: 'https://github.com/remorses/openplexer' },
27
27
  })
28
28
  },
29
29
  })
@@ -112,8 +112,15 @@ const app = new Spiceflow()
112
112
  workspace_id: string
113
113
  workspace_name: string
114
114
  owner: { type: string; user?: { id: string; name: string } }
115
+ duplicated_template_id?: string | null
115
116
  }
116
117
 
118
+ // Build Notion page URL from duplicated template ID (if present)
119
+ const duplicatedTemplateId = tokenData.duplicated_template_id ?? null
120
+ const notionPageUrl = duplicatedTemplateId
121
+ ? `https://notion.so/${duplicatedTemplateId.replace(/-/g, '')}`
122
+ : null
123
+
117
124
  // Store tokens in KV with 5 minute TTL
118
125
  const kvPayload = {
119
126
  accessToken: tokenData.access_token,
@@ -122,30 +129,83 @@ const app = new Spiceflow()
122
129
  workspaceName: tokenData.workspace_name,
123
130
  notionUserId: tokenData.owner?.user?.id,
124
131
  notionUserName: tokenData.owner?.user?.name,
132
+ duplicatedTemplateId,
125
133
  }
126
134
 
127
135
  await env.OPENPLEXER_KV.put(`auth:${stateParam}`, JSON.stringify(kvPayload), {
128
136
  expirationTtl: 300,
129
137
  })
130
138
 
131
- // Show success page
139
+ // Show success page with link to the created Notion page (if template was used)
140
+ const pageLink = notionPageUrl
141
+ ? `<a class="button" href="${notionPageUrl}" target="_blank" rel="noopener">Open in Notion <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 3h7v7M13 3L5 11"/></svg></a>`
142
+ : ''
143
+ const subtitle = notionPageUrl
144
+ ? 'Your board page has been created. You can close this tab and return to the CLI.'
145
+ : 'You can close this tab and return to the CLI.'
146
+
132
147
  return new Response(
133
148
  `<!DOCTYPE html>
134
149
  <html>
135
- <head><title>openplexer - Connected</title>
150
+ <head>
151
+ <title>openplexer - Connected</title>
152
+ <meta name="viewport" content="width=device-width, initial-scale=1">
153
+ <meta name="color-scheme" content="light dark">
136
154
  <style>
137
- body { font-family: system-ui, sans-serif; display: flex; justify-content: center;
138
- align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
139
- .card { text-align: center; padding: 48px; border-radius: 12px;
140
- background: #16213e; max-width: 400px; }
141
- h1 { margin: 0 0 16px; font-size: 24px; }
142
- p { color: #999; margin: 0; }
155
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
156
+ :root {
157
+ --bg: #fff;
158
+ --fg: #000;
159
+ --muted: #666;
160
+ --border: #eaeaea;
161
+ --link: #000;
162
+ --link-hover: #666;
163
+ --checkmark-bg: #000;
164
+ --checkmark-fg: #fff;
165
+ }
166
+ @media (prefers-color-scheme: dark) {
167
+ :root {
168
+ --bg: #000;
169
+ --fg: #fff;
170
+ --muted: #888;
171
+ --border: #333;
172
+ --link: #fff;
173
+ --link-hover: #999;
174
+ --checkmark-bg: #fff;
175
+ --checkmark-fg: #000;
176
+ }
177
+ }
178
+ body {
179
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
180
+ display: flex; justify-content: center; align-items: center;
181
+ min-height: 100vh; background: var(--bg); color: var(--fg);
182
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
183
+ }
184
+ .container { text-align: center; padding: 32px; max-width: 380px; }
185
+ .checkmark {
186
+ width: 48px; height: 48px; border-radius: 50%;
187
+ background: var(--checkmark-bg); color: var(--checkmark-fg);
188
+ display: inline-flex; align-items: center; justify-content: center;
189
+ margin-bottom: 24px; font-size: 20px;
190
+ }
191
+ h1 { font-size: 20px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 8px; }
192
+ p { font-size: 14px; color: var(--muted); line-height: 1.5; margin-bottom: 24px; }
193
+ a.button {
194
+ display: inline-flex; align-items: center; gap: 6px;
195
+ padding: 8px 16px; border-radius: 6px; font-size: 14px; font-weight: 500;
196
+ color: var(--link); text-decoration: none;
197
+ border: 1px solid var(--border); transition: color 0.15s;
198
+ }
199
+ a.button:hover { color: var(--link-hover); }
200
+ a.button svg { width: 16px; height: 16px; }
143
201
  </style>
144
202
  </head>
145
203
  <body>
146
- <div class="card">
204
+ <div class="container">
205
+ <div class="checkmark">&#10003;</div>
147
206
  <h1>Connected to Notion</h1>
148
- <p>You can close this tab and return to the CLI.</p>
207
+ <p>${subtitle}</p>
208
+ ${pageLink}
149
209
  </div>
150
210
  </body>
151
211
  </html>`,
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Kimaki
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.