@online5880/opensession 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/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @online5880/opensession
2
+
3
+ MVP CLI for session continuity with Supabase.
4
+
5
+ ## Commands
6
+
7
+ - `init --url --anon-key [--project-key] [--actor]`
8
+ - `login --actor`
9
+ - `start --project-key [--project-name] [--actor]`
10
+ - `resume --session-id [--actor]`
11
+ - `status [--project-key]`
12
+ - `log [--session-id] [--limit]`
13
+
14
+ ## Quick start
15
+
16
+ ```bash
17
+ npm install
18
+ node src/cli.js init --url "$SUPABASE_URL" --anon-key "$SUPABASE_ANON_KEY" --project-key demo --actor mane
19
+ node src/cli.js start --project-key demo
20
+ node src/cli.js status
21
+ node src/cli.js log
22
+ ```
23
+
24
+ Apply `sql/schema.sql` in Supabase SQL editor before using the CLI.
25
+
26
+ ## npx 실행
27
+
28
+ ```bash
29
+ npx @online5880/opensession init
30
+ ```
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@online5880/opensession",
3
+ "version": "0.1.0",
4
+ "description": "Session continuity bridge CLI with Supabase backend",
5
+ "type": "module",
6
+ "bin": {
7
+ "opensession": "src/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/cli.js",
11
+ "lint": "sh -c 'for f in src/*.js; do node --check \"$f\"; done'"
12
+ },
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "dependencies": {
17
+ "@supabase/supabase-js": "^2.52.1",
18
+ "commander": "^14.0.1"
19
+ }
20
+ }
package/sql/schema.sql ADDED
@@ -0,0 +1,27 @@
1
+ create table if not exists projects (
2
+ id uuid primary key default gen_random_uuid(),
3
+ project_key text unique not null,
4
+ name text not null,
5
+ created_at timestamptz not null default now()
6
+ );
7
+
8
+ create table if not exists sessions (
9
+ id uuid primary key default gen_random_uuid(),
10
+ project_id uuid not null references projects(id) on delete cascade,
11
+ actor text not null,
12
+ status text not null default 'active',
13
+ started_at timestamptz not null default now(),
14
+ ended_at timestamptz
15
+ );
16
+
17
+ create index if not exists sessions_project_status_idx on sessions(project_id, status);
18
+
19
+ create table if not exists session_events (
20
+ id uuid primary key default gen_random_uuid(),
21
+ session_id uuid not null references sessions(id) on delete cascade,
22
+ type text not null,
23
+ payload jsonb not null default '{}'::jsonb,
24
+ created_at timestamptz not null default now()
25
+ );
26
+
27
+ create index if not exists session_events_session_created_idx on session_events(session_id, created_at);
package/src/cli.js ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import {
5
+ appendEvent,
6
+ ensureProject,
7
+ getClient,
8
+ getSession,
9
+ getSessionEvents,
10
+ listActiveSessions,
11
+ startSession
12
+ } from './supabase.js';
13
+ import { getConfigPath, mergeConfig, readConfig, writeConfig } from './config.js';
14
+
15
+ const program = new Command();
16
+
17
+ program
18
+ .name('opensession')
19
+ .description('Session continuity bridge CLI for Supabase')
20
+ .version('0.1.0');
21
+
22
+ program
23
+ .command('init')
24
+ .description('Initialize CLI config')
25
+ .requiredOption('--url <url>', 'Supabase project URL')
26
+ .requiredOption('--anon-key <anonKey>', 'Supabase anon key')
27
+ .option('--project-key <projectKey>', 'Default project key')
28
+ .option('--actor <actor>', 'Default actor/username')
29
+ .action(async (options) => {
30
+ const current = await readConfig();
31
+ const next = mergeConfig(current, {
32
+ supabaseUrl: options.url,
33
+ supabaseAnonKey: options.anonKey,
34
+ defaultProjectKey: options.projectKey ?? current.defaultProjectKey,
35
+ actor: options.actor ?? current.actor
36
+ });
37
+
38
+ const path = await writeConfig(next);
39
+ console.log(`Config saved: ${path}`);
40
+ });
41
+
42
+ program
43
+ .command('login')
44
+ .description('Save actor identity used in session events')
45
+ .requiredOption('--actor <actor>', 'Actor/username')
46
+ .action(async (options) => {
47
+ const current = await readConfig();
48
+ const next = mergeConfig(current, { actor: options.actor });
49
+ await writeConfig(next);
50
+ console.log(`Logged in as ${options.actor}`);
51
+ });
52
+
53
+ program
54
+ .command('start')
55
+ .description('Start a new session and emit a start event')
56
+ .requiredOption('--project-key <projectKey>', 'Project key')
57
+ .option('--project-name <projectName>', 'Project display name')
58
+ .option('--actor <actor>', 'Actor override')
59
+ .action(async (options) => {
60
+ const config = await readConfig();
61
+ const client = getClient(config);
62
+ const actor = options.actor ?? config.actor ?? 'anonymous';
63
+
64
+ const project = await ensureProject(client, options.projectKey, options.projectName);
65
+ const session = await startSession(client, project.id, actor);
66
+
67
+ await writeConfig(
68
+ mergeConfig(config, {
69
+ defaultProjectKey: options.projectKey,
70
+ lastSessionId: session.id
71
+ })
72
+ );
73
+
74
+ console.log(`Session started: ${session.id}`);
75
+ console.log(`Project: ${project.project_key}`);
76
+ console.log(`Actor: ${session.actor}`);
77
+ });
78
+
79
+ program
80
+ .command('resume')
81
+ .description('Resume an existing session by emitting a resumed event')
82
+ .requiredOption('--session-id <sessionId>', 'Session id to resume')
83
+ .option('--actor <actor>', 'Actor override')
84
+ .action(async (options) => {
85
+ const config = await readConfig();
86
+ const client = getClient(config);
87
+ const actor = options.actor ?? config.actor ?? 'anonymous';
88
+ const session = await getSession(client, options.sessionId);
89
+
90
+ if (!session) {
91
+ throw new Error(`Session not found: ${options.sessionId}`);
92
+ }
93
+
94
+ const event = await appendEvent(client, session.id, 'resumed', { actor });
95
+ await writeConfig(mergeConfig(config, { lastSessionId: session.id }));
96
+
97
+ console.log(`Session resumed: ${session.id}`);
98
+ console.log(`Event: ${event.id}`);
99
+ });
100
+
101
+ program
102
+ .command('status')
103
+ .description('Show active sessions for a project')
104
+ .option('--project-key <projectKey>', 'Project key (defaults to configured project key)')
105
+ .action(async (options) => {
106
+ const config = await readConfig();
107
+ const client = getClient(config);
108
+ const projectKey = options.projectKey ?? config.defaultProjectKey;
109
+
110
+ if (!projectKey) {
111
+ throw new Error('Missing project key. Pass --project-key or run start first.');
112
+ }
113
+
114
+ const project = await ensureProject(client, projectKey, projectKey);
115
+ const active = await listActiveSessions(client, project.id);
116
+
117
+ console.log(`Project: ${project.project_key}`);
118
+
119
+ if (active.length === 0) {
120
+ console.log('No active sessions');
121
+ return;
122
+ }
123
+
124
+ for (const session of active) {
125
+ console.log(`- ${session.id} | actor=${session.actor} | started=${session.started_at}`);
126
+ }
127
+ });
128
+
129
+ program
130
+ .command('log')
131
+ .description('Show session event log')
132
+ .option('--session-id <sessionId>', 'Session id (defaults to last session)')
133
+ .option('--limit <limit>', 'Number of events', '50')
134
+ .action(async (options) => {
135
+ const config = await readConfig();
136
+ const client = getClient(config);
137
+ const sessionId = options.sessionId ?? config.lastSessionId;
138
+
139
+ if (!sessionId) {
140
+ throw new Error('Missing session id. Pass --session-id or run start/resume first.');
141
+ }
142
+
143
+ const events = await getSessionEvents(client, sessionId, Number.parseInt(options.limit, 10));
144
+
145
+ if (events.length === 0) {
146
+ console.log(`No events for session ${sessionId}`);
147
+ return;
148
+ }
149
+
150
+ for (const event of events) {
151
+ console.log(`${event.created_at} | ${event.type} | ${JSON.stringify(event.payload)}`);
152
+ }
153
+ });
154
+
155
+ program
156
+ .command('config-path')
157
+ .description('Print local config path')
158
+ .action(() => {
159
+ console.log(getConfigPath());
160
+ });
161
+
162
+ async function main() {
163
+ try {
164
+ await program.parseAsync(process.argv);
165
+ } catch (error) {
166
+ const message = error instanceof Error ? error.message : String(error);
167
+ console.error(message);
168
+ process.exitCode = 1;
169
+ }
170
+ }
171
+
172
+ await main();
package/src/config.js ADDED
@@ -0,0 +1,32 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.opensession');
6
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ export async function readConfig() {
9
+ try {
10
+ const raw = await fs.readFile(CONFIG_PATH, 'utf8');
11
+ return JSON.parse(raw);
12
+ } catch (error) {
13
+ if (error.code === 'ENOENT') {
14
+ return {};
15
+ }
16
+ throw error;
17
+ }
18
+ }
19
+
20
+ export async function writeConfig(config) {
21
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
22
+ await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
23
+ return CONFIG_PATH;
24
+ }
25
+
26
+ export function getConfigPath() {
27
+ return CONFIG_PATH;
28
+ }
29
+
30
+ export function mergeConfig(base, patch) {
31
+ return { ...base, ...patch };
32
+ }
@@ -0,0 +1,119 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+
3
+ export function getClient(config) {
4
+ if (!config.supabaseUrl || !config.supabaseAnonKey) {
5
+ throw new Error('Supabase is not configured. Run `opensession init --url <url> --anon-key <key>`.');
6
+ }
7
+
8
+ return createClient(config.supabaseUrl, config.supabaseAnonKey, {
9
+ auth: { persistSession: false }
10
+ });
11
+ }
12
+
13
+ export async function ensureProject(client, projectKey, projectName) {
14
+ const { data: existing, error: lookupError } = await client
15
+ .from('projects')
16
+ .select('id,project_key,name')
17
+ .eq('project_key', projectKey)
18
+ .maybeSingle();
19
+
20
+ if (lookupError) {
21
+ throw lookupError;
22
+ }
23
+
24
+ if (existing) {
25
+ return existing;
26
+ }
27
+
28
+ const { data: created, error: insertError } = await client
29
+ .from('projects')
30
+ .insert({ project_key: projectKey, name: projectName ?? projectKey })
31
+ .select('id,project_key,name')
32
+ .single();
33
+
34
+ if (insertError) {
35
+ throw insertError;
36
+ }
37
+
38
+ return created;
39
+ }
40
+
41
+ export async function startSession(client, projectId, actor) {
42
+ const { data: session, error: sessionError } = await client
43
+ .from('sessions')
44
+ .insert({ project_id: projectId, actor, status: 'active', started_at: new Date().toISOString() })
45
+ .select('id,project_id,actor,status,started_at')
46
+ .single();
47
+
48
+ if (sessionError) {
49
+ throw sessionError;
50
+ }
51
+
52
+ const { error: eventError } = await client
53
+ .from('session_events')
54
+ .insert({ session_id: session.id, type: 'started', payload: { actor } });
55
+
56
+ if (eventError) {
57
+ throw eventError;
58
+ }
59
+
60
+ return session;
61
+ }
62
+
63
+ export async function appendEvent(client, sessionId, type, payload = {}) {
64
+ const { data, error } = await client
65
+ .from('session_events')
66
+ .insert({ session_id: sessionId, type, payload })
67
+ .select('id,session_id,type,payload,created_at')
68
+ .single();
69
+
70
+ if (error) {
71
+ throw error;
72
+ }
73
+
74
+ return data;
75
+ }
76
+
77
+ export async function getSession(client, sessionId) {
78
+ const { data, error } = await client
79
+ .from('sessions')
80
+ .select('id,project_id,actor,status,started_at,ended_at')
81
+ .eq('id', sessionId)
82
+ .maybeSingle();
83
+
84
+ if (error) {
85
+ throw error;
86
+ }
87
+
88
+ return data;
89
+ }
90
+
91
+ export async function listActiveSessions(client, projectId) {
92
+ const { data, error } = await client
93
+ .from('sessions')
94
+ .select('id,project_id,actor,status,started_at,ended_at')
95
+ .eq('project_id', projectId)
96
+ .eq('status', 'active')
97
+ .order('started_at', { ascending: false });
98
+
99
+ if (error) {
100
+ throw error;
101
+ }
102
+
103
+ return data;
104
+ }
105
+
106
+ export async function getSessionEvents(client, sessionId, limit = 50) {
107
+ const { data, error } = await client
108
+ .from('session_events')
109
+ .select('id,session_id,type,payload,created_at')
110
+ .eq('session_id', sessionId)
111
+ .order('created_at', { ascending: true })
112
+ .limit(limit);
113
+
114
+ if (error) {
115
+ throw error;
116
+ }
117
+
118
+ return data;
119
+ }