osborn 0.9.122 → 0.9.123

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.
Files changed (2) hide show
  1. package/dist/index.js +54 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -815,6 +815,60 @@ function startApiServer(workingDir, port) {
815
815
  res.destroy(); });
816
816
  return;
817
817
  }
818
+ // GET /sessions/export-one?sessionId=X — tar.gz of a SINGLE session's files
819
+ // (the .jsonl, its sidecar dir, and its osb/ workspace), for SESSION SHARING
820
+ // (0.9.123): the frontend fetches this from the owner's machine, uploads it
821
+ // to Supabase Storage, and the recipient's machine imports it via the
822
+ // existing POST /sessions/import (slug-remapped into the recipient's
823
+ // workspace). Note: gated only by knowing the machine URL + the session UUID
824
+ // for the MVP — a per-share access token is a follow-up (tracked in
825
+ // shared_sessions). sessionId is regex-validated to block path traversal.
826
+ if (req.method === 'GET' && url.pathname === '/sessions/export-one') {
827
+ const sessionId = (url.searchParams.get('sessionId') || '').trim();
828
+ if (!sessionId || !/^[a-zA-Z0-9._-]+$/.test(sessionId)) {
829
+ res.writeHead(400, { 'Content-Type': 'application/json' });
830
+ res.end(JSON.stringify({ error: 'bad or missing sessionId' }));
831
+ return;
832
+ }
833
+ const claudeDir = join(homedir(), '.claude');
834
+ const projectsDir = join(claudeDir, 'projects');
835
+ if (!existsSync(projectsDir)) {
836
+ res.writeHead(404, { 'Content-Type': 'application/json' });
837
+ res.end(JSON.stringify({ error: 'no projects dir' }));
838
+ return;
839
+ }
840
+ // Find the project slug whose folder contains {sessionId}.jsonl.
841
+ let foundSlug = null;
842
+ for (const slug of readdirSync(projectsDir)) {
843
+ if (existsSync(join(projectsDir, slug, `${sessionId}.jsonl`))) {
844
+ foundSlug = slug;
845
+ break;
846
+ }
847
+ }
848
+ if (!foundSlug) {
849
+ res.writeHead(404, { 'Content-Type': 'application/json' });
850
+ res.end(JSON.stringify({ error: 'session not found' }));
851
+ return;
852
+ }
853
+ // Only tar the paths that exist (tar errors on a missing member).
854
+ const members = [join('projects', foundSlug, `${sessionId}.jsonl`)];
855
+ if (existsSync(join(projectsDir, foundSlug, sessionId)))
856
+ members.push(join('projects', foundSlug, sessionId));
857
+ if (existsSync(join(projectsDir, foundSlug, 'osb', sessionId)))
858
+ members.push(join('projects', foundSlug, 'osb', sessionId));
859
+ console.log(`📤 export-one session ${sessionId} (slug ${foundSlug}, ${members.length} member(s))`);
860
+ res.writeHead(200, {
861
+ 'Content-Type': 'application/gzip',
862
+ 'Content-Disposition': `attachment; filename="session-${sessionId}.tar.gz"`,
863
+ 'Access-Control-Allow-Origin': '*',
864
+ });
865
+ const tar = spawn('tar', ['-czf', '-', '-C', claudeDir, ...members]);
866
+ tar.stdout.pipe(res);
867
+ tar.stderr.on('data', (d) => console.error('[export-one]', d.toString()));
868
+ tar.on('close', (code) => { if (code !== 0)
869
+ res.destroy(); });
870
+ return;
871
+ }
818
872
  // GET /sessions/manifest — return mtime+size for all .jsonl files per slug (public, no auth)
819
873
  // Helper: merge an extracted tar directory into ~/.claude/{projects,skills}/.
820
874
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.122",
3
+ "version": "0.9.123",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {