@nuvin/session 0.1.0-rc.6 → 0.1.0-rc.7

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.
@@ -0,0 +1,53 @@
1
+ import type { DaemonDirectoryRow, HistoryPage, SessionSnapshot, WorkspaceSummary } from "../protocol/index.ts";
2
+ import type { TransportCredential } from "./endpoint.ts";
3
+ /** Discovery payload: the daemon directory (account-scoped cloud, machine-scoped local). */
4
+ export type DaemonDirectory = {
5
+ daemons: DaemonDirectoryRow[];
6
+ defaultSessionId?: string;
7
+ };
8
+ /**
9
+ * Live-attach endpoint for one daemon: ws URL + credential for the session
10
+ * socket. `credential` is exactly the transport's `TransportCredential`
11
+ * ({token} local / {grant} cloud), so a directory `getEndpoint` is a trivial
12
+ * wrapper over `acquireEndpoint`.
13
+ */
14
+ export type DaemonEndpoint = {
15
+ url: string;
16
+ credential: TransportCredential;
17
+ };
18
+ /**
19
+ * Everything the web UI needs to READ and MANAGE daemon data, independent of
20
+ * how it travels (spec §4.2). No HTTP status codes, URL shapes, or fetch types
21
+ * appear here. Terminal conditions throw `TerminalEndpointError` with a code
22
+ * (`transcript-not-found`, `session-not-found`, `daemon-revoked`); auth expiry
23
+ * surfaces via the implementation's `onUnauthorized` hook, never through this
24
+ * interface; `createWorkspace` failures carry the server's message; everything
25
+ * else is a plain `Error`.
26
+ */
27
+ export interface DataClient {
28
+ listDaemons(): Promise<DaemonDirectory>;
29
+ acquireEndpoint(daemonId: string): Promise<DaemonEndpoint>;
30
+ fetchTranscript(daemonId: string, historyId: string, opts?: {
31
+ workspace?: string;
32
+ profile?: string;
33
+ }): Promise<SessionSnapshot>;
34
+ fetchHistory(daemonId: string, opts?: {
35
+ profile?: string;
36
+ cwd?: string;
37
+ limit?: number;
38
+ cursor?: string;
39
+ }): Promise<HistoryPage>;
40
+ listWorkspaces(daemonId: string, opts?: {
41
+ profile?: string;
42
+ }): Promise<WorkspaceSummary[]>;
43
+ createWorkspace(daemonId: string, opts: {
44
+ name: string;
45
+ root?: string;
46
+ profile?: string;
47
+ }): Promise<WorkspaceSummary>;
48
+ deleteSession(daemonId: string, historyId: string, opts?: {
49
+ workspace?: string;
50
+ profile?: string;
51
+ }): Promise<void>;
52
+ }
53
+ //# sourceMappingURL=data-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-client.d.ts","sourceRoot":"","sources":["../../src/client/data-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,gBAAgB,EACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,4FAA4F;AAC5F,MAAM,MAAM,eAAe,GAAG;IAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3F;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,mBAAmB,CAAA;CAAE,CAAC;AAE9E;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,WAAW,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC;IACxC,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAC3D,eAAe,CACb,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAC9C,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5B,YAAY,CACV,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GACzE,OAAO,CAAC,WAAW,CAAC,CAAC;IACxB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAC3F,eAAe,CACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GACtD,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC7B,aAAa,CACX,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAC9C,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB"}
@@ -0,0 +1,19 @@
1
+ import type { DataClient } from "./data-client.ts";
2
+ export type HttpDataClientOptions = {
3
+ /** "" / absent ⇒ same-origin (local). Absolute relay origin ⇒ cloud. */
4
+ baseUrl?: string;
5
+ /** Cloud: fresh Clerk JWT per request. Absent ⇒ no bearer on discovery. */
6
+ getToken?: () => Promise<string | null>;
7
+ /** Cloud: 401 anywhere ⇒ route to /sign-in with return-to. */
8
+ onUnauthorized?: () => void;
9
+ fetchImpl?: typeof fetch;
10
+ };
11
+ /**
12
+ * The single HTTP DataClient for BOTH web modes (spec §4.3). Local: same-origin
13
+ * discovery, per-row daemon base + static bearer for data methods, static
14
+ * endpoints. Cloud: relay base, Clerk bearer, POST /connect for endpoints.
15
+ * Mode is entirely captured in `options` + the row shapes returned by
16
+ * GET /api/daemons; there is one code path.
17
+ */
18
+ export declare function createHttpDataClient(options?: HttpDataClientOptions): DataClient;
19
+ //# sourceMappingURL=http-data-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-data-client.d.ts","sourceRoot":"","sources":["../../src/client/http-data-client.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAmC,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGpF,MAAM,MAAM,qBAAqB,GAAG;IAClC,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,8DAA8D;IAC9D,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B,CAAC;AAkBF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,UAAU,CA+KpF"}
@@ -1,5 +1,7 @@
1
+ export * from "./data-client.ts";
1
2
  export * from "./directory.ts";
2
3
  export * from "./endpoint.ts";
4
+ export * from "./http-data-client.ts";
3
5
  export * from "./session-client.ts";
4
6
  export * from "./socket.ts";
5
7
  export * from "./transport.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC"}
@@ -611,6 +611,178 @@ var TerminalEndpointError = class extends Error {
611
611
  }
612
612
  };
613
613
 
614
+ // src/client/http-data-client.ts
615
+ function httpBaseFromWsUrl(wsUrl) {
616
+ const u = new URL(wsUrl);
617
+ const scheme = u.protocol === "wss:" ? "https:" : "http:";
618
+ return `${scheme}//${u.host}`;
619
+ }
620
+ function queryString(params) {
621
+ const q = new URLSearchParams();
622
+ for (const [key, value] of Object.entries(params)) {
623
+ if (value !== void 0) q.set(key, String(value));
624
+ }
625
+ const s = q.toString();
626
+ return s ? `?${s}` : "";
627
+ }
628
+ function createHttpDataClient(options = {}) {
629
+ const fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
630
+ const baseUrl = (options.baseUrl ?? "").replace(/\/+$/, "");
631
+ const rows = /* @__PURE__ */ new Map();
632
+ const resolveToken = async (id) => {
633
+ const endpoint = rows.get(id)?.endpoint;
634
+ if (endpoint) return endpoint.token;
635
+ return options.getToken ? await options.getToken() : null;
636
+ };
637
+ const resolveBase = (id) => {
638
+ const endpoint = rows.get(id)?.endpoint;
639
+ return endpoint ? httpBaseFromWsUrl(endpoint.wsUrl) : baseUrl;
640
+ };
641
+ const daemonFetch = async (id, suffix, init = {}) => {
642
+ const token = await resolveToken(id);
643
+ const response = await fetchImpl(
644
+ `${resolveBase(id)}/api/daemons/${encodeURIComponent(id)}${suffix}`,
645
+ {
646
+ ...init,
647
+ cache: "no-store",
648
+ headers: {
649
+ ...init.headers ?? {},
650
+ ...token !== null ? { Authorization: `Bearer ${token}` } : {}
651
+ }
652
+ }
653
+ );
654
+ if (response.status === 401) {
655
+ options.onUnauthorized?.();
656
+ throw new Error("Signed out \u2014 authentication required.");
657
+ }
658
+ return response;
659
+ };
660
+ return {
661
+ async listDaemons() {
662
+ const token = options.getToken ? await options.getToken() : null;
663
+ const response = await fetchImpl(`${baseUrl}/api/daemons`, {
664
+ cache: "no-store",
665
+ headers: token !== null ? { Authorization: `Bearer ${token}` } : {}
666
+ });
667
+ if (response.status === 401) {
668
+ options.onUnauthorized?.();
669
+ throw new Error("Signed out \u2014 authentication required.");
670
+ }
671
+ if (!response.ok) {
672
+ throw new Error(`Failed to fetch the daemon list (HTTP ${response.status}).`);
673
+ }
674
+ const body = await response.json();
675
+ rows.clear();
676
+ for (const row of body.daemons) rows.set(row.id, row);
677
+ return {
678
+ daemons: body.daemons,
679
+ ...body.defaultSessionId !== void 0 ? { defaultSessionId: body.defaultSessionId } : {}
680
+ };
681
+ },
682
+ async acquireEndpoint(daemonId) {
683
+ const endpoint = rows.get(daemonId)?.endpoint;
684
+ if (endpoint) {
685
+ return { url: endpoint.wsUrl, credential: { token: endpoint.token } };
686
+ }
687
+ const token = options.getToken ? await options.getToken() : null;
688
+ const response = await fetchImpl(
689
+ `${baseUrl}/api/daemons/${encodeURIComponent(daemonId)}/connect`,
690
+ {
691
+ method: "POST",
692
+ cache: "no-store",
693
+ headers: token !== null ? { Authorization: `Bearer ${token}` } : {}
694
+ }
695
+ );
696
+ if (response.status === 401) {
697
+ options.onUnauthorized?.();
698
+ throw new Error("Signed out \u2014 authentication required.");
699
+ }
700
+ if (response.status === 404) {
701
+ throw new TerminalEndpointError(
702
+ "daemon-revoked",
703
+ `Daemon "${daemonId}" is no longer registered.`
704
+ );
705
+ }
706
+ if (!response.ok) {
707
+ throw new Error(
708
+ response.status === 409 ? `Daemon "${daemonId}" is offline.` : `Connect failed for "${daemonId}" (HTTP ${response.status}).`
709
+ );
710
+ }
711
+ const body = await response.json();
712
+ return { url: body.wsUrl, credential: { grant: body.grant } };
713
+ },
714
+ async fetchTranscript(daemonId, historyId, opts) {
715
+ const suffix = `/transcript/${encodeURIComponent(historyId)}${queryString({
716
+ workspace: opts?.workspace,
717
+ profile: opts?.profile
718
+ })}`;
719
+ const response = await daemonFetch(daemonId, suffix);
720
+ if (response.status === 404) {
721
+ throw new TerminalEndpointError(
722
+ "transcript-not-found",
723
+ "This session's transcript is no longer available."
724
+ );
725
+ }
726
+ if (!response.ok) {
727
+ throw new Error(`Failed to load the transcript (HTTP ${response.status}).`);
728
+ }
729
+ return await response.json();
730
+ },
731
+ async fetchHistory(daemonId, opts) {
732
+ const suffix = `/history${queryString({
733
+ profile: opts?.profile,
734
+ cwd: opts?.cwd,
735
+ limit: opts?.limit,
736
+ cursor: opts?.cursor
737
+ })}`;
738
+ const response = await daemonFetch(daemonId, suffix);
739
+ if (!response.ok) {
740
+ throw new Error(`Failed to load history (HTTP ${response.status}).`);
741
+ }
742
+ return await response.json();
743
+ },
744
+ async listWorkspaces(daemonId, opts) {
745
+ const suffix = `/workspaces${queryString({ profile: opts?.profile })}`;
746
+ const response = await daemonFetch(daemonId, suffix);
747
+ if (!response.ok) {
748
+ throw new Error(`Failed to load workspaces (HTTP ${response.status}).`);
749
+ }
750
+ return await response.json();
751
+ },
752
+ async createWorkspace(daemonId, opts) {
753
+ const response = await daemonFetch(daemonId, "/workspaces", {
754
+ method: "POST",
755
+ headers: { "Content-Type": "application/json" },
756
+ body: JSON.stringify({
757
+ name: opts.name,
758
+ ...opts.root !== void 0 ? { root: opts.root } : {},
759
+ ...opts.profile !== void 0 ? { profile: opts.profile } : {}
760
+ })
761
+ });
762
+ if (!response.ok) {
763
+ const body = await response.json().catch(() => null);
764
+ throw new Error(
765
+ body?.message ?? `Failed to create the workspace (HTTP ${response.status}).`
766
+ );
767
+ }
768
+ return await response.json();
769
+ },
770
+ async deleteSession(daemonId, historyId, opts) {
771
+ const suffix = `/sessions/${encodeURIComponent(historyId)}${queryString({
772
+ workspace: opts?.workspace,
773
+ profile: opts?.profile
774
+ })}`;
775
+ const response = await daemonFetch(daemonId, suffix, { method: "DELETE" });
776
+ if (response.status === 404) {
777
+ throw new TerminalEndpointError("session-not-found", "This session no longer exists.");
778
+ }
779
+ if (!response.ok) {
780
+ throw new Error(`Failed to delete the session (HTTP ${response.status}).`);
781
+ }
782
+ }
783
+ };
784
+ }
785
+
614
786
  // src/client/session-client.ts
615
787
  var EMPTY_MODEL_COMPLETION = {
616
788
  providers: [],
@@ -1051,6 +1223,7 @@ function createWebSocketTransport(options) {
1051
1223
  export {
1052
1224
  TerminalEndpointError,
1053
1225
  adaptWhatwgSocket,
1226
+ createHttpDataClient,
1054
1227
  createServerDirectory,
1055
1228
  createSessionClient,
1056
1229
  createWebSocketTransport,
@@ -0,0 +1,30 @@
1
+ import type { SessionDescriptor } from "./types.ts";
2
+ /** GET /api/daemons — served by BOTH the CLI local web server and the relay (spec §4.1). */
3
+ export type DaemonDirectoryResponse = {
4
+ daemons: DaemonDirectoryRow[];
5
+ /** Pre-provisioned first-paint session — local mode only; relay omits. */
6
+ defaultSessionId?: string;
7
+ };
8
+ export type DaemonDirectoryRow = {
9
+ id: string;
10
+ /** Display label (local: entry name; cloud: registered name). */
11
+ name: string;
12
+ /** Local: always true (unreachable entries are dropped). Cloud: live presence. */
13
+ online: boolean;
14
+ lastSeenAt: string | null;
15
+ sessions?: SessionDescriptor[];
16
+ /**
17
+ * Static credentials (local mode): the browser dials wsUrl directly with the
18
+ * bearer token. Absent (cloud): acquire via POST /api/daemons/:id/connect.
19
+ */
20
+ endpoint?: {
21
+ wsUrl: string;
22
+ token: string;
23
+ };
24
+ };
25
+ /** POST /api/daemons/:id/connect — relay only (for now, spec §10). */
26
+ export type DaemonConnectResponse = {
27
+ wsUrl: string;
28
+ grant: string;
29
+ };
30
+ //# sourceMappingURL=daemon-directory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-directory.d.ts","sourceRoot":"","sources":["../../src/protocol/daemon-directory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,4FAA4F;AAC5F,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,qBAAqB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC"}
@@ -1,3 +1,4 @@
1
+ export * from "./daemon-directory.ts";
1
2
  export * from "./types.ts";
2
3
  export * from "./version.ts";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/protocol/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/protocol/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC"}
@@ -1,4 +1,4 @@
1
- import "../chunk-DGC7JSCG.js";
1
+ import "../chunk-AXY5EQPY.js";
2
2
  import {
3
3
  PROTOCOL_VERSION
4
4
  } from "../chunk-7G3R25NS.js";
@@ -1,4 +1,4 @@
1
- import "../chunk-DGC7JSCG.js";
1
+ import "../chunk-AXY5EQPY.js";
2
2
  import {
3
3
  PROTOCOL_VERSION
4
4
  } from "../chunk-7G3R25NS.js";
@@ -137,7 +137,7 @@ async function startFakeRelay(options = {}) {
137
137
  lastSeenAt: null,
138
138
  sessions: []
139
139
  }));
140
- sendJson(res, 200, rows);
140
+ sendJson(res, 200, { daemons: rows });
141
141
  return;
142
142
  }
143
143
  const connectMatch = /^POST \/api\/daemons\/([A-Za-z0-9_-]+)\/connect$/.exec(route);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuvin/session",
3
- "version": "0.1.0-rc.6",
3
+ "version": "0.1.0-rc.7",
4
4
  "private": false,
5
5
  "description": "Headless session layer for Nuvin: state reducers, wire protocol, session controller",
6
6
  "license": "Apache-2.0",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "ws": "^8.20.0",
53
- "@nuvin/agent-core": "^0.1.0-rc.5"
53
+ "@nuvin/agent-core": "^0.1.0-rc.7"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/node": "^22.0.0",
File without changes