shraga 0.1.2 → 0.1.4

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.
@@ -13,7 +13,7 @@
13
13
  <link rel="preconnect" href="https://fonts.googleapis.com" />
14
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
15
15
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
16
- <script type="module" crossorigin src="/assets/index-BoHttkMt.js"></script>
16
+ <script type="module" crossorigin src="/assets/index-BnArwb7g.js"></script>
17
17
  <link rel="stylesheet" crossorigin href="/assets/index-DdibEb2O.css">
18
18
  </head>
19
19
  <body>
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
5
5
  "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./package.json": "./package.json"
11
+ },
6
12
  "license": "MIT",
7
13
  "author": "Elya Livshitz",
8
14
  "homepage": "https://livx.cc/shraga",
@@ -63,7 +69,7 @@
63
69
  "express": "^4.21.1",
64
70
  "firebase": "^11.0.0",
65
71
  "lucide-react": "^0.468.0",
66
- "mcp-slack-use": "github:Livshitz/mcp-slack-use#0c877d3397f7",
72
+ "mcp-slack-use": "github:Livshitz/mcp-slack-use#1dde291",
67
73
  "react": "^19.0.0",
68
74
  "react-dom": "^19.0.0",
69
75
  "react-markdown": "^9.0.0",
@@ -88,8 +94,5 @@
88
94
  "tailwindcss-animate": "^1.0.7",
89
95
  "typescript": "^5.7.0",
90
96
  "vite": "^6.0.7"
91
- },
92
- "optionalDependencies": {
93
- "puppeteer": "^25.0.2"
94
97
  }
95
98
  }
package/src/cli.ts CHANGED
@@ -84,4 +84,6 @@ function cleanup() {
84
84
  process.on('SIGINT', cleanup);
85
85
  process.on('SIGTERM', cleanup);
86
86
 
87
- await import('./server/index.ts');
87
+ // Dogfood the public library surface — the CLI's server-run path IS createShraga(...).start().
88
+ const { createShraga, fromEnv } = await import('./index.ts');
89
+ await createShraga(fromEnv()).start();
@@ -17,6 +17,7 @@ interface Props {
17
17
  export function ArtifactPanel({ artifacts, selectedId, sessionId, getToken, onSelect, onClose }: Props) {
18
18
  const [expanded, setExpanded] = useState(false);
19
19
  const [exporting, setExporting] = useState(false);
20
+ const [pngExport, setPngExport] = useState(false); // PNG export is an EE overlay capability; hidden in CE
20
21
  const [scale, setScale] = useState(1);
21
22
  const containerRef = useRef<HTMLDivElement>(null);
22
23
  const selected = artifacts.find(a => a.id === selectedId);
@@ -31,6 +32,16 @@ export function ArtifactPanel({ artifacts, selectedId, sessionId, getToken, onSe
31
32
  setScale(Math.min(availW / w, availH / h, 1));
32
33
  }, [selected]);
33
34
 
35
+ useEffect(() => {
36
+ let alive = true;
37
+ getToken()
38
+ .then(t => fetch('/api/features', { headers: t ? { Authorization: `Bearer ${t}` } : {} }))
39
+ .then(r => r.json())
40
+ .then((f: { artifactPngExport?: boolean }) => { if (alive) setPngExport(!!f.artifactPngExport); })
41
+ .catch(err => console.error('[artifact] features fetch failed:', err));
42
+ return () => { alive = false; };
43
+ }, [getToken]);
44
+
34
45
  useEffect(() => {
35
46
  recalcScale();
36
47
  const el = containerRef.current;
@@ -106,9 +117,11 @@ export function ArtifactPanel({ artifacts, selectedId, sessionId, getToken, onSe
106
117
  </span>
107
118
  )}
108
119
  <div className="flex-1" />
109
- <Button variant="ghost" size="icon" className="h-6 w-6" onClick={handleExport} disabled={!selected || exporting} title="Export PNG">
110
- {exporting ? <span className="w-3 h-3 border-2 border-current border-t-transparent rounded-full animate-spin" /> : <Image className="w-3.5 h-3.5" />}
111
- </Button>
120
+ {pngExport && (
121
+ <Button variant="ghost" size="icon" className="h-6 w-6" onClick={handleExport} disabled={!selected || exporting} title="Export PNG">
122
+ {exporting ? <span className="w-3 h-3 border-2 border-current border-t-transparent rounded-full animate-spin" /> : <Image className="w-3.5 h-3.5" />}
123
+ </Button>
124
+ )}
112
125
  <Button variant="ghost" size="icon" className="h-6 w-6 hidden sm:inline-flex" onClick={() => setExpanded(!expanded)} title={expanded ? 'Minimize' : 'Maximize'}>
113
126
  {expanded ? <Minimize2 className="w-3.5 h-3.5" /> : <Maximize2 className="w-3.5 h-3.5" />}
114
127
  </Button>
package/src/index.ts ADDED
@@ -0,0 +1,172 @@
1
+ // Shraga — public library surface.
2
+ //
3
+ // The FIRST-CLASS way to consume Shraga: `import { createShraga } from 'shraga'`, configure with
4
+ // typed options, register/extend programmatically through the same seams the built-ins use
5
+ // (features, engines, the event bus, verified webhooks, data-side extensions), then `start()`.
6
+ //
7
+ // import { createShraga } from 'shraga';
8
+ // const app = createShraga({ port: 3032, dataDir: './data', authProvider: 'local' });
9
+ // app.registerFeature(myFeature);
10
+ // app.on('gmail', (payload) => { … });
11
+ // await app.start();
12
+ // // … app.stop() to shut down.
13
+ //
14
+ // The run-from-source entry (`src/server/index.ts`, used by prod `bun run src/server/index.ts`) and
15
+ // the `shraga` CLI both dogfood this exact surface — createShraga(fromEnv()).start(). File-based
16
+ // `data/extensions/*.ext.ts` drop-ins and `SHRAGA_OVERLAY` keep working: they reach the SAME
17
+ // programmatic registries, just via a different door.
18
+
19
+ // Type-only imports below: importing this module must NOT eagerly load the server graph (which
20
+ // resolves DATA_DIR at import time). The runtime boot is a dynamic import inside start(), AFTER
21
+ // options have been mapped onto the environment.
22
+ import type { BootRegistrations, ServerHandle } from './server/boot.ts';
23
+ import type { ServerFeature, FeatureContext } from './server/features.ts';
24
+ import type { AgentEngine, EngineModel, EngineStreamOpts } from './server/engine/types.ts';
25
+ import type { ExtRegisterFn, ExtensionContext } from './server/extensions.ts';
26
+ import type { WebhookOptions } from './server/events/webhook.ts';
27
+ import type { ShragaEvent, ShragaEventMap, PayloadOf } from './server/events/types.ts';
28
+
29
+ export type {
30
+ ServerHandle,
31
+ ServerFeature,
32
+ FeatureContext,
33
+ AgentEngine,
34
+ EngineModel,
35
+ EngineStreamOpts,
36
+ ExtRegisterFn,
37
+ ExtensionContext,
38
+ WebhookOptions,
39
+ ShragaEvent,
40
+ ShragaEventMap,
41
+ PayloadOf,
42
+ };
43
+
44
+ /** Typed configuration for a Shraga instance. Follows the repo's ModuleOptions convention:
45
+ * defaults live here and are merged with the caller's partial. Anything not modelled is still
46
+ * reachable via `env` (Shraga is heavily env-driven) — those are set before the server boots. */
47
+ export class ShragaOptions {
48
+ /** HTTP/WS port. Default 3032 (or PORT env). */
49
+ port?: number;
50
+ /** Data directory (flat-file storage root). Default ./data (or DATA_DIR env). */
51
+ dataDir?: string;
52
+ /** Auth backend. 'local' (default) = username/password; 'firebase' needs the firebase add-on. */
53
+ authProvider?: 'local' | 'firebase';
54
+ /** Passive mode — HTTP serving only, no schedulers/consumers/background writers (standby twins). */
55
+ passive?: boolean;
56
+ /** Install process SIGTERM/SIGINT handlers (default true — the standalone server/CLI wants them).
57
+ * A library embedder owning its own lifecycle sets false and uses stop(). */
58
+ installSignalHandlers?: boolean = true;
59
+ /** Arbitrary extra environment to apply before boot (e.g. ANTHROPIC_API_KEY, SHRAGA_FEAT_*). */
60
+ env?: Record<string, string>;
61
+ }
62
+
63
+ export interface ShragaInstance {
64
+ /** Register a server feature (routes/WS/consumers) — the same seam Slack uses. */
65
+ registerFeature(feature: ServerFeature): this;
66
+ /** Register a pluggable agent engine (runtime). */
67
+ registerEngine(engine: AgentEngine): this;
68
+ /** Register a programmatic extension — the same shape as a data/extensions/*.ext.ts default export. */
69
+ registerExtension(fn: ExtRegisterFn): this;
70
+ /** Declare a verified vendor webhook (public POST /api/webhooks/<source> → typed event). */
71
+ registerWebhook<K extends string>(opts: WebhookOptions<K>): this;
72
+ /** Subscribe to a typed event source on the in-process bus. */
73
+ on<K extends string>(source: K, handler: (payload: PayloadOf<K>, evt: ShragaEvent<K>) => void): this;
74
+ /** Publish an event onto the bus (after start()). */
75
+ emit<K extends string>(source: K, payload: PayloadOf<K>): this;
76
+ /** Boot HTTP + WS. Resolves once listening. Idempotent — repeated calls return the same handle. */
77
+ start(): Promise<ServerHandle>;
78
+ /** Drain and shut down without exiting the process. */
79
+ stop(): Promise<void>;
80
+ /** The live Express app (advanced use) — available after start(). */
81
+ readonly app: ServerHandle['app'] | undefined;
82
+ /** Publish onto the bus directly (advanced use) — available after start(). */
83
+ readonly emitEvent: ServerHandle['emitEvent'] | undefined;
84
+ }
85
+
86
+ class Shraga implements ShragaInstance {
87
+ public options: ShragaOptions;
88
+ private reg: Required<BootRegistrations> = { features: [], engines: [], extensions: [], eventSubs: [] };
89
+ private handle: ServerHandle | null = null;
90
+ private starting: Promise<ServerHandle> | null = null;
91
+
92
+ constructor(options?: Partial<ShragaOptions>) {
93
+ this.options = { ...new ShragaOptions(), ...options };
94
+ // Map options onto the environment NOW, before any server module (which resolves DATA_DIR /
95
+ // AUTH_PROVIDER at import time) can load. start()'s dynamic import happens strictly after this.
96
+ this.applyEnv();
97
+ }
98
+
99
+ private applyEnv(): void {
100
+ const o = this.options;
101
+ if (o.port != null) process.env.PORT = String(o.port);
102
+ if (o.dataDir != null) process.env.DATA_DIR = o.dataDir;
103
+ if (o.authProvider != null) process.env.AUTH_PROVIDER = o.authProvider;
104
+ if (o.passive != null) process.env.SHRAGA_PASSIVE = o.passive ? '1' : '0';
105
+ if (o.installSignalHandlers === false) process.env.SHRAGA_INSTALL_SIGNALS = '0';
106
+ for (const [k, v] of Object.entries(o.env ?? {})) process.env[k] = v;
107
+ }
108
+
109
+ private assertNotStarted(what: string): void {
110
+ if (this.handle || this.starting) throw new Error(`[shraga] ${what} must be called before start()`);
111
+ }
112
+
113
+ registerFeature(feature: ServerFeature): this { this.assertNotStarted('registerFeature'); this.reg.features.push(feature); return this; }
114
+ registerEngine(engine: AgentEngine): this { this.assertNotStarted('registerEngine'); this.reg.engines.push(engine); return this; }
115
+ registerExtension(fn: ExtRegisterFn): this { this.assertNotStarted('registerExtension'); this.reg.extensions.push(fn); return this; }
116
+
117
+ registerWebhook<K extends string>(opts: WebhookOptions<K>): this {
118
+ this.assertNotStarted('registerWebhook');
119
+ // A webhook is just an extension that mounts a verified route on the extension router (before the
120
+ // SPA catch-all) — identical to a file-based webhook. Reuse the seam; don't invent a parallel one.
121
+ this.reg.extensions.push((_router, ctx: ExtensionContext) => { ctx.registerWebhook(opts); });
122
+ return this;
123
+ }
124
+
125
+ on<K extends string>(source: K, handler: (payload: PayloadOf<K>, evt: ShragaEvent<K>) => void): this {
126
+ this.assertNotStarted('on');
127
+ this.reg.eventSubs.push({ source, handler: handler as (payload: unknown, evt: unknown) => void });
128
+ return this;
129
+ }
130
+
131
+ emit<K extends string>(source: K, payload: PayloadOf<K>): this {
132
+ if (!this.handle) throw new Error('[shraga] emit() requires start() first');
133
+ this.handle.emitEvent(source, payload);
134
+ return this;
135
+ }
136
+
137
+ async start(): Promise<ServerHandle> {
138
+ if (this.handle) return this.handle;
139
+ if (this.starting) return this.starting;
140
+ this.starting = (async () => {
141
+ const { bootServer } = await import('./server/boot.ts');
142
+ this.handle = await bootServer(this.reg);
143
+ return this.handle;
144
+ })();
145
+ return this.starting;
146
+ }
147
+
148
+ async stop(): Promise<void> {
149
+ if (this.handle) { await this.handle.stop(); this.handle = null; }
150
+ this.starting = null;
151
+ }
152
+
153
+ get app() { return this.handle?.app; }
154
+ get emitEvent() { return this.handle?.emitEvent; }
155
+ }
156
+
157
+ /** Create a Shraga instance. Configure it, register/extend, then `await instance.start()`. */
158
+ export function createShraga(options?: Partial<ShragaOptions>): ShragaInstance {
159
+ return new Shraga(options);
160
+ }
161
+
162
+ /** Derive options from the environment (PORT / DATA_DIR / AUTH_PROVIDER / SHRAGA_PASSIVE). The
163
+ * run-from-source entry and the CLI pass this straight into createShraga. */
164
+ export function fromEnv(): Partial<ShragaOptions> {
165
+ const passive = process.env.SHRAGA_PASSIVE ?? process.env.UNCLAW_PASSIVE;
166
+ return {
167
+ port: process.env.PORT ? Number(process.env.PORT) : undefined,
168
+ dataDir: process.env.DATA_DIR || undefined,
169
+ authProvider: (process.env.AUTH_PROVIDER as 'local' | 'firebase' | undefined) || undefined,
170
+ passive: passive === '1' || passive === 'true' ? true : undefined,
171
+ };
172
+ }
@@ -1,9 +1,6 @@
1
1
  import { Router, type Request } from 'express';
2
2
  import { requireAuth } from '../auth.ts';
3
3
  import { getArtifact, getArtifactHtml, listArtifacts } from './artifacts.service.ts';
4
- import { exportToPng } from './artifacts.export.ts';
5
-
6
- const PREFIX = '[artifacts:http]';
7
4
 
8
5
  export const artifactsRouter = Router();
9
6
 
@@ -25,19 +22,5 @@ artifactsRouter.get('/api/artifacts/:sid/:id/meta', requireAuth, (req: Request<{
25
22
  res.json(artifact.meta);
26
23
  });
27
24
 
28
- artifactsRouter.post('/api/artifacts/:sid/:id/export', requireAuth, async (req: Request<{ sid: string; id: string }>, res) => {
29
- const { sid, id } = req.params;
30
- const artifact = getArtifact(sid, id);
31
- if (!artifact) return res.sendStatus(404);
32
-
33
- const dimensions: [number, number] = req.body?.dimensions ?? artifact.meta.dimensions;
34
-
35
- try {
36
- const png = await exportToPng(artifact.html, dimensions);
37
- console.log(PREFIX, `exported ${id} → ${png.length} bytes`);
38
- res.type('png').send(png);
39
- } catch (err: any) {
40
- console.error(PREFIX, 'export failed:', err.message);
41
- res.status(500).json({ error: err.message });
42
- }
43
- });
25
+ // PNG export (Puppeteer) is not part of CE. An EE overlay adds the
26
+ // `POST /api/artifacts/:sid/:id/export` route and declares the `artifactPngExport` capability flag.