openplexer 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
@@ -0,0 +1,13 @@
1
+ import { ClientSideConnection, type SessionInfo } from '@agentclientprotocol/sdk';
2
+ export type AcpConnection = {
3
+ connection: ClientSideConnection;
4
+ client: 'opencode' | 'claude';
5
+ kill: () => void;
6
+ };
7
+ export declare function connectAcp({ client, }: {
8
+ client: 'opencode' | 'claude';
9
+ }): Promise<AcpConnection>;
10
+ export declare function listAllSessions({ connection, }: {
11
+ connection: ClientSideConnection;
12
+ }): Promise<SessionInfo[]>;
13
+ //# sourceMappingURL=acp-client.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +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.
3
+ import { spawn } from 'node:child_process';
4
+ import { ClientSideConnection, ndJsonStream, } from '@agentclientprotocol/sdk';
5
+ function nodeToWebWritable(nodeStream) {
6
+ return new WritableStream({
7
+ write(chunk) {
8
+ return new Promise((resolve, reject) => {
9
+ nodeStream.write(Buffer.from(chunk), (err) => {
10
+ if (err) {
11
+ reject(err);
12
+ }
13
+ else {
14
+ resolve();
15
+ }
16
+ });
17
+ });
18
+ },
19
+ });
20
+ }
21
+ function nodeToWebReadable(nodeStream) {
22
+ return new ReadableStream({
23
+ start(controller) {
24
+ nodeStream.on('data', (chunk) => {
25
+ controller.enqueue(new Uint8Array(chunk));
26
+ });
27
+ nodeStream.on('end', () => {
28
+ controller.close();
29
+ });
30
+ nodeStream.on('error', (err) => {
31
+ controller.error(err);
32
+ });
33
+ },
34
+ });
35
+ }
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
+ class MinimalClient {
40
+ async requestPermission() {
41
+ return { outcome: { outcome: 'cancelled' } };
42
+ }
43
+ async sessionUpdate() { }
44
+ async readTextFile() {
45
+ return { content: '' };
46
+ }
47
+ async writeTextFile() {
48
+ return {};
49
+ }
50
+ }
51
+ export async function connectAcp({ client, }) {
52
+ const cmd = client === 'opencode' ? 'opencode' : 'claude';
53
+ const args = ['acp'];
54
+ const child = spawn(cmd, args, {
55
+ stdio: ['pipe', 'pipe', 'inherit'],
56
+ });
57
+ const stream = ndJsonStream(nodeToWebWritable(child.stdin), nodeToWebReadable(child.stdout));
58
+ const connection = new ClientSideConnection((_agent) => {
59
+ return new MinimalClient();
60
+ }, stream);
61
+ await connection.initialize({
62
+ protocolVersion: 1,
63
+ clientCapabilities: {},
64
+ });
65
+ return {
66
+ connection,
67
+ client,
68
+ kill: () => {
69
+ child.kill();
70
+ },
71
+ };
72
+ }
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;
86
+ }
87
+ return sessions;
88
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+ // openplexer CLI entrypoint.
3
+ // Syncs ACP sessions (from OpenCode or Claude Code) to Notion board databases.
4
+ // Uses goke for CLI parsing and clack for interactive prompts.
5
+ import { goke } from 'goke';
6
+ import { intro, outro, note, cancel, isCancel, confirm, log, multiselect, select, spinner, } from '@clack/prompts';
7
+ import crypto from 'node:crypto';
8
+ import path from 'node:path';
9
+ import { exec } from 'node:child_process';
10
+ import { readConfig, writeConfig } from "./config.js";
11
+ import { connectAcp, listAllSessions } from "./acp-client.js";
12
+ import { getRepoInfo } from "./git.js";
13
+ import { createNotionClient, createBoardDatabase, getRootPages } from "./notion.js";
14
+ import { evictExistingInstance, getLockPort, startLockServer } from "./lock.js";
15
+ import { startSyncLoop } from "./sync.js";
16
+ import { enableStartupService, disableStartupService, isStartupServiceEnabled, getServiceLocationDescription, } from "./startup-service.js";
17
+ const OPENPLEXER_URL = 'https://openplexer.com';
18
+ process.title = 'openplexer';
19
+ const cli = goke('openplexer');
20
+ // Default command: start sync if boards exist, otherwise run connect wizard
21
+ cli
22
+ .command('', 'Sync coding sessions to Notion boards')
23
+ .action(async () => {
24
+ const config = readConfig();
25
+ if (!config || config.boards.length === 0) {
26
+ await connectFlow();
27
+ return;
28
+ }
29
+ await startDaemon(config);
30
+ });
31
+ // Connect command: add a new board
32
+ cli.command('connect', 'Connect a new Notion board').action(async () => {
33
+ await connectFlow();
34
+ });
35
+ // Status command: show current sync state
36
+ cli.command('status', 'Show sync state').action(async () => {
37
+ const config = readConfig();
38
+ if (!config || config.boards.length === 0) {
39
+ console.log('No boards configured. Run `openplexer connect` to add one.');
40
+ return;
41
+ }
42
+ console.log(`Clients: ${config.clients.join(', ')}`);
43
+ console.log(`Boards: ${config.boards.length}`);
44
+ config.boards.forEach((board, i) => {
45
+ console.log(` ${i + 1}. ${board.notionWorkspaceName} — ${board.trackedRepos.length} repos, ${Object.keys(board.syncedSessions).length} synced sessions`);
46
+ });
47
+ });
48
+ // Stop command: kill running daemon via lock port
49
+ cli.command('stop', 'Stop the running openplexer daemon').action(async () => {
50
+ const port = getLockPort();
51
+ const probe = await fetch(`http://127.0.0.1:${port}/health`, {
52
+ signal: AbortSignal.timeout(1000),
53
+ }).catch(() => {
54
+ return undefined;
55
+ });
56
+ if (!probe) {
57
+ console.log('No running daemon found.');
58
+ return;
59
+ }
60
+ const body = (await probe.json().catch(() => ({})));
61
+ if (body.pid) {
62
+ process.kill(body.pid, 'SIGTERM');
63
+ console.log(`Stopped daemon (PID ${body.pid})`);
64
+ }
65
+ });
66
+ // Boards command: list boards
67
+ cli.command('boards', 'List configured boards').action(async () => {
68
+ const config = readConfig();
69
+ if (!config || config.boards.length === 0) {
70
+ console.log('No boards configured.');
71
+ return;
72
+ }
73
+ config.boards.forEach((board, i) => {
74
+ console.log(`${i + 1}. ${board.notionWorkspaceName}`);
75
+ console.log(` Page: https://notion.so/${board.notionPageId.replace(/-/g, '')}`);
76
+ console.log(` Repos: ${board.trackedRepos.join(', ') || '(all)'}`);
77
+ console.log(` Synced: ${Object.keys(board.syncedSessions).length} sessions`);
78
+ });
79
+ });
80
+ // Startup command: manage startup registration
81
+ cli.command('startup', 'Show startup registration status').action(async () => {
82
+ const enabled = await isStartupServiceEnabled();
83
+ if (enabled) {
84
+ console.log(`Registered: ${getServiceLocationDescription()}`);
85
+ }
86
+ else {
87
+ console.log('Not registered to run on login.');
88
+ }
89
+ });
90
+ cli
91
+ .command('startup enable', 'Register openplexer to run on login')
92
+ .action(async () => {
93
+ const openplexerBin = path.resolve(process.argv[1]);
94
+ await enableStartupService({ command: process.execPath, args: [openplexerBin] });
95
+ console.log(`Registered at ${getServiceLocationDescription()}`);
96
+ });
97
+ cli
98
+ .command('startup disable', 'Unregister openplexer from login')
99
+ .action(async () => {
100
+ await disableStartupService();
101
+ console.log('Unregistered from login startup.');
102
+ });
103
+ cli.parse();
104
+ // --- Connect wizard ---
105
+ async function connectFlow() {
106
+ intro('openplexer — connect a Notion board');
107
+ const config = readConfig() || { clients: [], boards: [] };
108
+ // Step 1: Choose ACP clients (only on first run)
109
+ if (config.clients.length === 0) {
110
+ const clientChoice = await multiselect({
111
+ message: 'Which coding agents do you use?',
112
+ options: [
113
+ { value: 'opencode', label: 'OpenCode' },
114
+ { value: 'claude', label: 'Claude Code' },
115
+ ],
116
+ required: true,
117
+ });
118
+ if (isCancel(clientChoice)) {
119
+ cancel('Setup cancelled');
120
+ process.exit(0);
121
+ }
122
+ config.clients = clientChoice;
123
+ }
124
+ // Step 2: Spawn ACP for each client and discover projects
125
+ const s = spinner();
126
+ const clientLabel = config.clients.join(' + ');
127
+ s.start(`Connecting to ${clientLabel}...`);
128
+ let repoSlugs = [];
129
+ const connectedClients = [];
130
+ for (const client of config.clients) {
131
+ try {
132
+ const acp = await connectAcp({ client });
133
+ const sessions = await listAllSessions({ connection: acp.connection });
134
+ // Extract unique repos from session cwds
135
+ const cwds = [...new Set(sessions.map((sess) => sess.cwd).filter(Boolean))];
136
+ const repoInfos = await Promise.all(cwds.map((cwd) => getRepoInfo({ cwd })));
137
+ repoSlugs.push(...repoInfos.filter(Boolean).map((r) => r.slug));
138
+ acp.kill();
139
+ connectedClients.push(client);
140
+ log.info(`${client}: ${sessions.length} sessions`);
141
+ }
142
+ catch {
143
+ log.warn(`Could not connect to ${client}. Make sure "${client}" is installed and in PATH.`);
144
+ }
145
+ }
146
+ repoSlugs = [...new Set(repoSlugs)];
147
+ s.stop(`Found ${repoSlugs.length} repos from ${connectedClients.join(' + ')}`);
148
+ if (connectedClients.length === 0) {
149
+ log.error('Could not connect to any ACP agent.');
150
+ process.exit(1);
151
+ }
152
+ // Step 3: Select repos to track
153
+ let trackedRepos = [];
154
+ if (repoSlugs.length > 0) {
155
+ note('Select specific repos if you plan to collaborate.\nThis avoids showing personal projects on the shared board.', 'Repo selection');
156
+ const repoChoice = await multiselect({
157
+ message: 'Which repos to track?',
158
+ options: [
159
+ { value: '*', label: '* All repos', hint: 'sync every repo with a git remote' },
160
+ ...repoSlugs.map((slug) => ({
161
+ value: slug,
162
+ label: slug,
163
+ })),
164
+ ],
165
+ required: false,
166
+ });
167
+ if (isCancel(repoChoice)) {
168
+ cancel('Setup cancelled');
169
+ process.exit(0);
170
+ }
171
+ trackedRepos = repoChoice.includes('*') ? [] : repoChoice;
172
+ }
173
+ else {
174
+ log.warn('No git repos found in sessions. All future sessions will be tracked.');
175
+ }
176
+ // Step 4: Notion OAuth
177
+ const state = crypto.randomBytes(16).toString('hex');
178
+ const authUrl = `${OPENPLEXER_URL}/auth/notion?state=${state}`;
179
+ note(`Opening browser to connect Notion.\nAuthorize the integration and select a page to share.\n\n${authUrl}`, 'Notion');
180
+ // Open browser
181
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
182
+ exec(`${openCmd} "${authUrl}"`);
183
+ s.start('Waiting for Notion authorization...');
184
+ let authResult;
185
+ const maxAttempts = 150; // 5 minutes at 2s intervals
186
+ for (let i = 0; i < maxAttempts; i++) {
187
+ await new Promise((resolve) => {
188
+ setTimeout(resolve, 2000);
189
+ });
190
+ const resp = await fetch(`${OPENPLEXER_URL}/auth/status?state=${state}`, {
191
+ signal: AbortSignal.timeout(3000),
192
+ }).catch(() => {
193
+ return undefined;
194
+ });
195
+ if (resp?.ok) {
196
+ authResult = (await resp.json());
197
+ break;
198
+ }
199
+ }
200
+ if (!authResult) {
201
+ s.stop('Timed out waiting for Notion authorization');
202
+ process.exit(1);
203
+ }
204
+ s.stop(`Connected to ${authResult.workspaceName}`);
205
+ // Step 5: Select Notion page from root pages
206
+ const notion = createNotionClient({ token: authResult.accessToken });
207
+ s.start('Fetching Notion pages...');
208
+ const rootPages = await getRootPages({ notion });
209
+ s.stop(`Found ${rootPages.length} root pages`);
210
+ if (rootPages.length === 0) {
211
+ log.error('No root pages found in your Notion workspace. Create a page first.');
212
+ process.exit(1);
213
+ }
214
+ // Filter out pages already used by other boards
215
+ const usedPageIds = new Set(config.boards.map((b) => b.notionPageId));
216
+ const availablePages = rootPages.filter((p) => !usedPageIds.has(p.id));
217
+ if (availablePages.length === 0) {
218
+ log.error('All root pages are already connected to boards.');
219
+ process.exit(1);
220
+ }
221
+ const pageId = await (async () => {
222
+ if (availablePages.length === 1) {
223
+ log.info(`Auto-selected page: ${availablePages[0].icon} ${availablePages[0].title}`);
224
+ return availablePages[0].id;
225
+ }
226
+ const pageChoice = await select({
227
+ message: 'Which Notion page should hold the board?',
228
+ options: availablePages.map((p) => ({
229
+ value: p.id,
230
+ label: `${p.icon} ${p.title}`.trim(),
231
+ hint: usedPageIds.has(p.id) ? 'already used' : undefined,
232
+ })),
233
+ });
234
+ if (isCancel(pageChoice)) {
235
+ cancel('Setup cancelled');
236
+ process.exit(0);
237
+ }
238
+ return pageChoice;
239
+ })();
240
+ // Step 6: Create database
241
+ s.start('Creating board database...');
242
+ const { databaseId } = await createBoardDatabase({ notion, pageId });
243
+ s.stop('Board database created');
244
+ log.success('Open the database in Notion and click "+ Add a view" → Board, grouped by Status.');
245
+ // Step 7: Save to config
246
+ const board = {
247
+ notionToken: authResult.accessToken,
248
+ notionUserId: authResult.notionUserId || '',
249
+ notionUserName: authResult.notionUserName || '',
250
+ notionWorkspaceId: authResult.workspaceId,
251
+ notionWorkspaceName: authResult.workspaceName,
252
+ notionPageId: pageId,
253
+ notionDatabaseId: databaseId,
254
+ trackedRepos,
255
+ syncedSessions: {},
256
+ connectedAt: new Date().toISOString(),
257
+ };
258
+ config.boards.push(board);
259
+ writeConfig(config);
260
+ // Resolve absolute path to the CLI script so startup service and
261
+ // detached spawn work regardless of cwd at login/invocation time.
262
+ const openplexerBin = path.resolve(process.argv[1]);
263
+ // Step 8: Offer startup registration
264
+ const alreadyEnabled = await isStartupServiceEnabled();
265
+ if (!alreadyEnabled) {
266
+ const registerStartup = await confirm({
267
+ message: 'Register openplexer to run on login?',
268
+ });
269
+ if (!isCancel(registerStartup) && registerStartup) {
270
+ await enableStartupService({ command: process.execPath, args: [openplexerBin] });
271
+ log.success(`Registered at ${getServiceLocationDescription()}`);
272
+ }
273
+ }
274
+ else {
275
+ log.info(`Already registered at ${getServiceLocationDescription()}`);
276
+ }
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.');
285
+ }
286
+ // --- Daemon ---
287
+ async function startDaemon(config) {
288
+ const port = getLockPort();
289
+ await evictExistingInstance({ port });
290
+ startLockServer({ port });
291
+ console.log(`openplexer daemon started (PID ${process.pid}, port ${port})`);
292
+ const connections = [];
293
+ for (const client of config.clients) {
294
+ try {
295
+ const acp = await connectAcp({ client });
296
+ connections.push(acp);
297
+ console.log(`Connected to ${client} via ACP`);
298
+ }
299
+ catch {
300
+ console.error(`Failed to connect to ${client}, skipping`);
301
+ }
302
+ }
303
+ if (connections.length === 0) {
304
+ console.error('Could not connect to any ACP agent.');
305
+ process.exit(1);
306
+ }
307
+ await startSyncLoop({ config, acpConnections: connections });
308
+ }
@@ -0,0 +1,34 @@
1
+ export type OpenplexerBoard = {
2
+ /** Notion OAuth access token */
3
+ notionToken: string;
4
+ /** Notion user ID of this machine's user */
5
+ notionUserId: string;
6
+ /** Notion user name */
7
+ notionUserName: string;
8
+ /** Notion workspace ID */
9
+ notionWorkspaceId: string;
10
+ /** Notion workspace name */
11
+ notionWorkspaceName: string;
12
+ /** Notion page ID where database was created */
13
+ notionPageId: string;
14
+ /** Notion database ID (created by CLI) */
15
+ notionDatabaseId: string;
16
+ /** Git repo URLs to track (e.g. ["owner/repo1", "owner/repo2"]) */
17
+ trackedRepos: string[];
18
+ /** Map of ACP session ID → Notion page ID (already synced) */
19
+ syncedSessions: Record<string, string>;
20
+ /** ISO timestamp of when this board was connected. Only sessions
21
+ * created or last updated after this time are synced. */
22
+ connectedAt: string;
23
+ };
24
+ export type AcpClient = 'opencode' | 'claude';
25
+ export type OpenplexerConfig = {
26
+ /** ACP clients to connect to (user may use both opencode and claude) */
27
+ clients: AcpClient[];
28
+ /** Multiple boards this CLI syncs to */
29
+ boards: OpenplexerBoard[];
30
+ };
31
+ export declare function getConfigDir(): string;
32
+ export declare function readConfig(): OpenplexerConfig | undefined;
33
+ export declare function writeConfig(config: OpenplexerConfig): void;
34
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +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"}
package/dist/config.js ADDED
@@ -0,0 +1,26 @@
1
+ // Typed config for openplexer, stored at ~/.openplexer/config.json.
2
+ // Supports multiple boards — each board is a separate Notion database
3
+ // that this CLI syncs ACP sessions to.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+ const CONFIG_DIR = path.join(os.homedir(), '.openplexer');
8
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+ export function getConfigDir() {
10
+ return CONFIG_DIR;
11
+ }
12
+ export function readConfig() {
13
+ try {
14
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
15
+ return JSON.parse(raw);
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ export function writeConfig(config) {
22
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
23
+ const tmpFile = CONFIG_FILE + '.tmp';
24
+ fs.writeFileSync(tmpFile, JSON.stringify(config, null, 2));
25
+ fs.renameSync(tmpFile, CONFIG_FILE);
26
+ }
package/dist/env.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type Env = {
2
+ OPENPLEXER_KV: KVNamespace;
3
+ NOTION_CLIENT_ID: string;
4
+ NOTION_CLIENT_SECRET: string;
5
+ };
6
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,GAAG,GAAG;IAChB,aAAa,EAAE,WAAW,CAAA;IAC1B,gBAAgB,EAAE,MAAM,CAAA;IACxB,oBAAoB,EAAE,MAAM,CAAA;CAC7B,CAAA"}
package/dist/env.js ADDED
@@ -0,0 +1,4 @@
1
+ // Typed environment variables for the Cloudflare Worker.
2
+ // NOTION_CLIENT_ID and NOTION_CLIENT_SECRET are the openplexer Notion
3
+ // integration's OAuth2 credentials, used to exchange auth codes for tokens.
4
+ export {};
package/dist/git.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export type RepoInfo = {
2
+ owner: string;
3
+ repo: string;
4
+ /** e.g. "owner/repo" */
5
+ slug: string;
6
+ /** Full GitHub URL */
7
+ url: string;
8
+ /** Current branch name */
9
+ branch: string;
10
+ };
11
+ export declare function getRepoInfo({ cwd }: {
12
+ cwd: string;
13
+ }): Promise<RepoInfo | undefined>;
14
+ //# sourceMappingURL=git.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,sBAAsB;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,wBAAsB,WAAW,CAAC,EAAE,GAAG,EAAE,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAyBzF"}
package/dist/git.js ADDED
@@ -0,0 +1,60 @@
1
+ // Extract git repo info from session cwd paths.
2
+ // Parses the remote origin URL to get owner/repo.
3
+ import { execFile } from 'node:child_process';
4
+ export async function getRepoInfo({ cwd }) {
5
+ const remoteUrl = await execAsync('git', ['-C', cwd, 'remote', 'get-url', 'origin']).catch(() => {
6
+ return undefined;
7
+ });
8
+ if (!remoteUrl) {
9
+ return undefined;
10
+ }
11
+ const parsed = parseGitRemoteUrl(remoteUrl.trim());
12
+ if (!parsed) {
13
+ return undefined;
14
+ }
15
+ const branch = await execAsync('git', ['-C', cwd, 'rev-parse', '--abbrev-ref', 'HEAD']).catch(() => {
16
+ return 'main';
17
+ });
18
+ return {
19
+ ...parsed,
20
+ branch: branch.trim(),
21
+ };
22
+ }
23
+ function parseGitRemoteUrl(url) {
24
+ // SSH: git@github.com:owner/repo.git
25
+ const sshMatch = url.match(/git@github\.com:([^/]+)\/([^/.]+)/);
26
+ if (sshMatch) {
27
+ const owner = sshMatch[1];
28
+ const repo = sshMatch[2];
29
+ return {
30
+ owner,
31
+ repo,
32
+ slug: `${owner}/${repo}`,
33
+ url: `https://github.com/${owner}/${repo}`,
34
+ };
35
+ }
36
+ // HTTPS: https://github.com/owner/repo.git
37
+ const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+)/);
38
+ if (httpsMatch) {
39
+ const owner = httpsMatch[1];
40
+ const repo = httpsMatch[2];
41
+ return {
42
+ owner,
43
+ repo,
44
+ slug: `${owner}/${repo}`,
45
+ url: `https://github.com/${owner}/${repo}`,
46
+ };
47
+ }
48
+ return undefined;
49
+ }
50
+ function execAsync(cmd, args) {
51
+ return new Promise((resolve, reject) => {
52
+ execFile(cmd, args, { timeout: 5000 }, (error, stdout) => {
53
+ if (error) {
54
+ reject(error);
55
+ return;
56
+ }
57
+ resolve(stdout);
58
+ });
59
+ });
60
+ }
package/dist/lock.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import http from 'node:http';
2
+ export declare function getLockPort(): number;
3
+ export declare function evictExistingInstance({ port }: {
4
+ port: number;
5
+ }): Promise<void>;
6
+ export declare function startLockServer({ port }: {
7
+ port: number;
8
+ }): http.Server;
9
+ //# sourceMappingURL=lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"AAGA,OAAO,IAAI,MAAM,WAAW,CAAA;AAI5B,wBAAgB,WAAW,IAAI,MAAM,CASpC;AAED,wBAAsB,qBAAqB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BrF;AAED,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,MAAM,CAavE"}