@super-harness/server 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 +21 -0
- package/README.md +176 -0
- package/dist/index.cjs +422 -0
- package/dist/index.d.cts +74 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +389 -0
- package/package.json +93 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mert
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# @super-harness/server
|
|
2
|
+
|
|
3
|
+
The super-line binding for a `@super-harness/core` Harness, shipped as a
|
|
4
|
+
super-line **plugin**. Two exports, two adoption modes:
|
|
5
|
+
|
|
6
|
+
- **`harness(engine)`** — a `SuperLinePlugin` a host adds to its own
|
|
7
|
+
`createSuperLineServer` `plugins:` array. The primary story: one server, one
|
|
8
|
+
socket, one auth, one collections backend for the host AND the harness.
|
|
9
|
+
- **`serve(engine, config)`** — the standalone host, built from the same
|
|
10
|
+
pieces: it owns the collections backend, a default query-param
|
|
11
|
+
`authenticate`, `identify`, and mounts `plugins: [harness(engine), ...]`.
|
|
12
|
+
|
|
13
|
+
The tree rides **collections**, not the request surface: structural state is
|
|
14
|
+
`harness.threads`/`nodes`/`tools`/`membership` rows (declared by
|
|
15
|
+
`harnessContract()` in `@super-harness/shared`), and the token stream rides
|
|
16
|
+
ephemeral per-thread room events that are never persisted per-token. Clients
|
|
17
|
+
assemble the live tree with `subscribeTree` from `@super-harness/shared` (or
|
|
18
|
+
`@super-harness/react`).
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add @super-harness/server @super-harness/core @super-harness/shared \
|
|
24
|
+
@mastra/core @super-line/core @super-line/server \
|
|
25
|
+
@super-line/collections-memory @super-line/collections-sqlite
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
All `@super-line/*` packages are **peers** — your app owns one core instance
|
|
29
|
+
shared by host and library. What you need:
|
|
30
|
+
|
|
31
|
+
| Peer | Version | When |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `@super-line/core` | `^0.11.0` | always |
|
|
34
|
+
| `@super-line/server` | `^0.11.0` | always |
|
|
35
|
+
| `@super-line/collections-memory` | `^0.1.0` | always (serve's `memory` backend; cheap) |
|
|
36
|
+
| `@super-line/collections-sqlite` | `^0.1.0` | always (serve's default backend) |
|
|
37
|
+
| `@super-line/collections-pglite` | `^0.1.0` | **optional** — only for `storage: { type: 'pglite' }` |
|
|
38
|
+
| `@mastra/core` | `^1.49.0-alpha.2` | always (the engine's peer) |
|
|
39
|
+
|
|
40
|
+
A transport is the host's choice — e.g. `@super-line/transport-websocket`
|
|
41
|
+
(`^0.6.0`). Composing hosts that bring their own backend only need the
|
|
42
|
+
collections package they actually use.
|
|
43
|
+
|
|
44
|
+
## Composition — the harness as a plugin in your server
|
|
45
|
+
|
|
46
|
+
If the app runs (or should run) its own super-line server, add the harness
|
|
47
|
+
beside its surface. Four obligations (see `examples/composed-host` for the
|
|
48
|
+
runnable version, `composition.test.ts` for the e2e):
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { z } from 'zod'
|
|
52
|
+
import { defineContract, defineSurface } from '@super-line/core'
|
|
53
|
+
import { createSuperLineServer } from '@super-line/server'
|
|
54
|
+
import { memoryCollections } from '@super-line/collections-memory'
|
|
55
|
+
import { webSocketServerTransport } from '@super-line/transport-websocket'
|
|
56
|
+
import { harnessContract } from '@super-harness/shared'
|
|
57
|
+
import { harness } from '@super-harness/server'
|
|
58
|
+
|
|
59
|
+
// 1. Merge the contract fragment: harnessContract() contributes the harness
|
|
60
|
+
// surface (on `shared`) + the four harness.* collections.
|
|
61
|
+
const hostContract = defineContract({
|
|
62
|
+
plugins: [harnessContract()],
|
|
63
|
+
shared: defineSurface({
|
|
64
|
+
clientToServer: {
|
|
65
|
+
'demo.echo': { input: z.object({ text: z.string() }), output: z.object({ echoed: z.string() }) },
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
roles: { user: {} },
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const srv = createSuperLineServer(hostContract, {
|
|
72
|
+
transports: [webSocketServerTransport({ server: httpServer, path: '/ws' })],
|
|
73
|
+
// 2. ONE collections backend, host-owned — serves the harness collections
|
|
74
|
+
// beside any of the host's own.
|
|
75
|
+
collections: memoryCollections(),
|
|
76
|
+
// 3. authenticate ctx carries userId (+resourceId); identify returns it.
|
|
77
|
+
authenticate: (h) => ({ role: 'user' as const, ctx: { userId: userIdFrom(h) } }),
|
|
78
|
+
identify: (conn) => (conn.ctx as { userId: string }).userId,
|
|
79
|
+
// 4. Add the plugin. harness.* handler keys are subtracted from implement().
|
|
80
|
+
plugins: [harness(engine)],
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// The host implements only its own surface — harness.* is owned by the plugin.
|
|
84
|
+
srv.implement({ shared: { 'demo.echo': async ({ text }) => ({ echoed: text.toUpperCase() }) }, user: {} })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**`identify` is load-bearing.** The harness collections' RLS keys on
|
|
88
|
+
`ctx.userId`, and super-line's principal is `identify(conn) ?? conn.id` — a
|
|
89
|
+
random connection id. A host that skips `identify` gets a working request
|
|
90
|
+
surface and a **silently empty tree**: every membership-gated read is denied.
|
|
91
|
+
With `@super-line/plugin-auth`, `identify` comes from the session and the
|
|
92
|
+
identity becomes the principal for free (see `examples/auth`).
|
|
93
|
+
|
|
94
|
+
Client side: one `createSuperLineClient(hostContract)` handed to
|
|
95
|
+
`createHarnessClient({ client })` from `@super-harness/react` in borrowed mode — its
|
|
96
|
+
`close()` detaches listeners, never the host's socket.
|
|
97
|
+
|
|
98
|
+
## Standalone — `serve()`
|
|
99
|
+
|
|
100
|
+
The harness-only contract on its own server, same pieces pre-wired:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { createServer } from 'node:http'
|
|
104
|
+
import { webSocketServerTransport } from '@super-line/transport-websocket'
|
|
105
|
+
import { serve } from '@super-harness/server'
|
|
106
|
+
|
|
107
|
+
const httpServer = createServer()
|
|
108
|
+
const { server, close } = await serve(engine, {
|
|
109
|
+
storage: { type: 'sqlite', file: './harness.db' }, // default; 'memory' for tests
|
|
110
|
+
transports: [webSocketServerTransport({ server: httpServer, path: '/super-line' })],
|
|
111
|
+
// authenticate?: (handshake) => ({ role: 'user', ctx: { userId, resourceId? } })
|
|
112
|
+
// plugin?: { roleFor, defaultRole } — membership role policy (see below)
|
|
113
|
+
// plugins?: [inspector(), auth()] — extra super-line plugins beside harness()
|
|
114
|
+
})
|
|
115
|
+
httpServer.listen(4111)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The storage union picks the collections backend:
|
|
119
|
+
|
|
120
|
+
- `{ type: 'sqlite', file? }` — default; owns its own file. Needs
|
|
121
|
+
`better-sqlite3`'s native build.
|
|
122
|
+
- `{ type: 'memory' }` — tests/dev.
|
|
123
|
+
- `{ type: 'pglite', pgUrl, electricUrl? }` — multi-node: central Postgres for
|
|
124
|
+
writes + Electric-synced per-node replicas for fan-out. Loads
|
|
125
|
+
`@super-line/collections-pglite` (the optional peer) on demand. Viable even
|
|
126
|
+
though writes round-trip PG → Electric → replica, because tokens are
|
|
127
|
+
ephemeral — only low-frequency structural rows hit the backend.
|
|
128
|
+
|
|
129
|
+
The default `authenticate` trusts `userId`/`resourceId` query params — replace
|
|
130
|
+
it (or compose `@super-line/plugin-auth` via `plugins:`) for anything
|
|
131
|
+
non-local. `close()` tears the server down and disposes the plugin's bus
|
|
132
|
+
subscription; the transports/http server are the caller's to close.
|
|
133
|
+
|
|
134
|
+
## What the plugin contributes
|
|
135
|
+
|
|
136
|
+
`harness(engine, opts)` returns `{ policies, handlers, setup }`:
|
|
137
|
+
|
|
138
|
+
- **Policies** — membership-based RLS over the four collections.
|
|
139
|
+
`harness.nodes`/`tools` read `isIn('threadId', joinedThreads(principal))`;
|
|
140
|
+
`harness.threads` reads `eq('resourceId', ctx.resourceId)` (the sidebar
|
|
141
|
+
list), falling back to membership when the connection carries no
|
|
142
|
+
`resourceId`; `harness.membership` reads `eq('userId', principal)`. Client
|
|
143
|
+
writes are all **deny** — the plugin co-writes server-authoritatively,
|
|
144
|
+
bypassing policy.
|
|
145
|
+
- **Handlers** — the `harness.*` requests (send/resume/abort/approve/
|
|
146
|
+
switchMode/listModes/thread CRUD/join), subtracted from the host's
|
|
147
|
+
`implement()`. `harness.join` adds the connection to the thread room and
|
|
148
|
+
inserts a membership row.
|
|
149
|
+
- **`setup(ctx)`** — subscribes the engine bus. Token deltas
|
|
150
|
+
(`reasoning`/`text`/`argsText`) broadcast to the per-thread room
|
|
151
|
+
(`harness:thread:{id}`) — ephemeral, never persisted — and fold into a
|
|
152
|
+
per-thread Projector; structural events fold through the Projector into the
|
|
153
|
+
collections writer (`collectionsTreeSink`), which lands the final strings on
|
|
154
|
+
the row at `node_end`/`tool_end`. Session signals (`suspended`,
|
|
155
|
+
`approvalRequired`, `modeChanged`, `followUpQueued`) broadcast; a
|
|
156
|
+
suspension's `{resumeSchema, request}` also parks on the node row
|
|
157
|
+
(`pendingResume`) so a mid-turn reload rebuilds the prompt. Thread metadata
|
|
158
|
+
persists as row updates (thread-list reactivity is `harness.threads` row
|
|
159
|
+
deltas, not events); `thread_deleted` cascades node/tool/membership rows.
|
|
160
|
+
|
|
161
|
+
### Roles: viewer vs operator
|
|
162
|
+
|
|
163
|
+
The viewer/operator split is a **`role` column on `harness.membership`** —
|
|
164
|
+
per-user, per-thread — NOT a connection role. Control ops (send/resume/abort/
|
|
165
|
+
approve/switchMode/rename/delete) reject a `viewer` membership with
|
|
166
|
+
`FORBIDDEN`; read ops don't, so a viewer still sees the full live tree. The
|
|
167
|
+
default join role is `operator` (every joiner can drive — preserves
|
|
168
|
+
single-user behavior); pass `roleFor(ctx)` or `defaultRole` in
|
|
169
|
+
`HarnessPluginOptions` to hand out `viewer` memberships.
|
|
170
|
+
|
|
171
|
+
## Auth
|
|
172
|
+
|
|
173
|
+
The plugin is auth-agnostic: it reads `ctx.userId` however the host's
|
|
174
|
+
`authenticate` supplies it — query-param dev auth, `@super-line/plugin-auth`,
|
|
175
|
+
or a custom scheme. `resourceId` (optional) scopes the thread list. See
|
|
176
|
+
`examples/auth` for the plugin-auth pairing.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
collectionsTreeSink: () => collectionsTreeSink,
|
|
34
|
+
harness: () => harness,
|
|
35
|
+
harnessContract: () => import_shared3.harnessContract,
|
|
36
|
+
serve: () => serve
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/serve.ts
|
|
41
|
+
var import_server = require("@super-line/server");
|
|
42
|
+
var import_collections_memory = require("@super-line/collections-memory");
|
|
43
|
+
var import_collections_sqlite = require("@super-line/collections-sqlite");
|
|
44
|
+
var import_shared2 = require("@super-harness/shared");
|
|
45
|
+
|
|
46
|
+
// src/plugin.ts
|
|
47
|
+
var import_core = require("@super-line/core");
|
|
48
|
+
var import_core2 = require("@super-harness/core");
|
|
49
|
+
var import_shared = require("@super-harness/shared");
|
|
50
|
+
|
|
51
|
+
// src/sink.ts
|
|
52
|
+
var isCode = (e, code) => e?.code === code;
|
|
53
|
+
function collectionsTreeSink(opts) {
|
|
54
|
+
const { collections, threadId } = opts;
|
|
55
|
+
const nodesCol = () => collections(opts.nodes);
|
|
56
|
+
const toolsCol = () => collections(opts.tools);
|
|
57
|
+
const threadsCol = () => collections(opts.threads);
|
|
58
|
+
const known = /* @__PURE__ */ new Set();
|
|
59
|
+
const lastWritten = /* @__PURE__ */ new Map();
|
|
60
|
+
const chains = /* @__PURE__ */ new Map();
|
|
61
|
+
const run = (key, fn) => {
|
|
62
|
+
const prev = chains.get(key) ?? Promise.resolve();
|
|
63
|
+
chains.set(
|
|
64
|
+
key,
|
|
65
|
+
prev.then(fn).catch((e) => console.error("[harness] collection write failed", key, e))
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
const upsert = (handle, ns, id, row) => {
|
|
69
|
+
const key = `${ns}\0${id}`;
|
|
70
|
+
const serialized = JSON.stringify(row);
|
|
71
|
+
if (lastWritten.get(key) === serialized) return;
|
|
72
|
+
lastWritten.set(key, serialized);
|
|
73
|
+
run(key, async () => {
|
|
74
|
+
if (known.has(key)) return handle.update(row);
|
|
75
|
+
try {
|
|
76
|
+
await handle.insert(row);
|
|
77
|
+
known.add(key);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
if (!isCode(e, "CONFLICT")) throw e;
|
|
80
|
+
known.add(key);
|
|
81
|
+
await handle.update(row);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
let threadRow = { id: threadId, turns: [], todos: [], createdAt: 0, updatedAt: 0 };
|
|
86
|
+
const flushThread = () => upsert(threadsCol(), opts.threads, threadId, threadRow);
|
|
87
|
+
const pending = /* @__PURE__ */ new Map();
|
|
88
|
+
const pendingPayload = /* @__PURE__ */ new Map();
|
|
89
|
+
const nodeRowOf = (node) => {
|
|
90
|
+
const terminal = node.status !== "running";
|
|
91
|
+
const parkedTool = pending.get(node.nodeId);
|
|
92
|
+
const stillPending = parkedTool !== void 0 && !isSettled(node.tools[parkedTool]?.status);
|
|
93
|
+
if (parkedTool !== void 0 && !stillPending) pending.delete(node.nodeId);
|
|
94
|
+
return {
|
|
95
|
+
id: node.nodeId,
|
|
96
|
+
threadId,
|
|
97
|
+
parentNodeId: node.parentNodeId,
|
|
98
|
+
depth: node.depth,
|
|
99
|
+
agentType: node.agentType,
|
|
100
|
+
task: node.task,
|
|
101
|
+
status: node.status,
|
|
102
|
+
reasoning: terminal ? node.reasoning : "",
|
|
103
|
+
text: terminal ? node.text : "",
|
|
104
|
+
toolOrder: node.toolOrder,
|
|
105
|
+
childOrder: node.childOrder,
|
|
106
|
+
usage: node.usage,
|
|
107
|
+
durationMs: node.durationMs,
|
|
108
|
+
error: node.error,
|
|
109
|
+
textOffset: node.textOffset,
|
|
110
|
+
pendingResume: stillPending ? pendingPayload.get(node.nodeId) : void 0
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
const toolRowOf = (node, toolCallId) => {
|
|
114
|
+
const t = node.tools[toolCallId];
|
|
115
|
+
const streaming = t.status === "input-streaming";
|
|
116
|
+
return {
|
|
117
|
+
id: t.toolCallId,
|
|
118
|
+
threadId,
|
|
119
|
+
nodeId: node.nodeId,
|
|
120
|
+
toolName: t.toolName,
|
|
121
|
+
status: t.status,
|
|
122
|
+
argsText: streaming ? "" : t.argsText,
|
|
123
|
+
args: t.args,
|
|
124
|
+
result: t.result,
|
|
125
|
+
isError: t.isError,
|
|
126
|
+
textOffset: t.textOffset
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
writeNode: (node) => {
|
|
131
|
+
upsert(nodesCol(), opts.nodes, node.nodeId, nodeRowOf(node));
|
|
132
|
+
for (const tid of node.toolOrder) upsert(toolsCol(), opts.tools, tid, toolRowOf(node, tid));
|
|
133
|
+
},
|
|
134
|
+
writeThread: (doc) => {
|
|
135
|
+
threadRow = { ...threadRow, turns: doc.turns, todos: doc.todos ?? [] };
|
|
136
|
+
flushThread();
|
|
137
|
+
},
|
|
138
|
+
setThreadMeta: (meta) => {
|
|
139
|
+
threadRow = {
|
|
140
|
+
...threadRow,
|
|
141
|
+
resourceId: meta.resourceId ?? threadRow.resourceId,
|
|
142
|
+
title: meta.title ?? threadRow.title,
|
|
143
|
+
createdAt: threadRow.createdAt || meta.createdAt || 0,
|
|
144
|
+
updatedAt: meta.updatedAt ?? threadRow.updatedAt
|
|
145
|
+
};
|
|
146
|
+
flushThread();
|
|
147
|
+
},
|
|
148
|
+
setPending: (nodeId, toolCallId, payload) => {
|
|
149
|
+
pending.set(nodeId, toolCallId);
|
|
150
|
+
pendingPayload.set(nodeId, payload);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function isSettled(status) {
|
|
155
|
+
return status === "output-available" || status === "error";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/plugin.ts
|
|
159
|
+
function harness(engine, opts = {}) {
|
|
160
|
+
let pctx;
|
|
161
|
+
const P = () => {
|
|
162
|
+
if (!pctx) throw new Error("harness plugin: setup() has not run yet");
|
|
163
|
+
return pctx;
|
|
164
|
+
};
|
|
165
|
+
const col = (name) => P().collection(name);
|
|
166
|
+
const room = (threadId) => P().room((0, import_shared.harnessThreadRoom)(threadId));
|
|
167
|
+
const sinks = /* @__PURE__ */ new Map();
|
|
168
|
+
const projectors = /* @__PURE__ */ new Map();
|
|
169
|
+
const sinkFor = (threadId) => {
|
|
170
|
+
let s = sinks.get(threadId);
|
|
171
|
+
if (!s) {
|
|
172
|
+
s = collectionsTreeSink({
|
|
173
|
+
collections: (n) => col(n),
|
|
174
|
+
threadId,
|
|
175
|
+
nodes: import_shared.HARNESS_NODES,
|
|
176
|
+
tools: import_shared.HARNESS_TOOLS,
|
|
177
|
+
threads: import_shared.HARNESS_THREADS
|
|
178
|
+
});
|
|
179
|
+
sinks.set(threadId, s);
|
|
180
|
+
}
|
|
181
|
+
return s;
|
|
182
|
+
};
|
|
183
|
+
const projectorFor = (threadId) => {
|
|
184
|
+
let p = projectors.get(threadId);
|
|
185
|
+
if (!p) {
|
|
186
|
+
p = new import_core2.Projector(sinkFor(threadId));
|
|
187
|
+
projectors.set(threadId, p);
|
|
188
|
+
}
|
|
189
|
+
return p;
|
|
190
|
+
};
|
|
191
|
+
const joined = async (principal) => {
|
|
192
|
+
const rows = await col(import_shared.HARNESS_MEMBERSHIP).snapshot({ filter: (0, import_core.eq)("userId", principal) });
|
|
193
|
+
return rows.map((r) => r.threadId);
|
|
194
|
+
};
|
|
195
|
+
const roleOf = async (threadId, userId) => {
|
|
196
|
+
const row = await col(import_shared.HARNESS_MEMBERSHIP).read((0, import_shared.membershipId)(threadId, userId));
|
|
197
|
+
return row?.role;
|
|
198
|
+
};
|
|
199
|
+
const requireDriver = async (threadId, ctx) => {
|
|
200
|
+
if (await roleOf(threadId, ctx.userId) === "viewer") {
|
|
201
|
+
throw new import_core.SuperLineError("FORBIDDEN", "viewer role cannot drive the thread");
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const ok = { ok: true };
|
|
205
|
+
const attempt = async (fn) => {
|
|
206
|
+
try {
|
|
207
|
+
await fn();
|
|
208
|
+
return ok;
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.error("[harness] request failed", e);
|
|
211
|
+
return { ok: false };
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const purgeThread = async (threadId) => {
|
|
215
|
+
projectors.delete(threadId);
|
|
216
|
+
sinks.delete(threadId);
|
|
217
|
+
for (const c of [import_shared.HARNESS_NODES, import_shared.HARNESS_TOOLS, import_shared.HARNESS_MEMBERSHIP]) {
|
|
218
|
+
const rows = await col(c).snapshot({ filter: (0, import_core.eq)("threadId", threadId) }).catch(() => []);
|
|
219
|
+
for (const r of rows) await col(c).delete(r.id).catch(() => {
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
await col(import_shared.HARNESS_THREADS).delete(threadId).catch(() => {
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
const onBus = (threadId, e) => {
|
|
226
|
+
switch (e.type) {
|
|
227
|
+
case "reasoning_delta":
|
|
228
|
+
room(threadId).broadcast("harness.reasoningDelta", { threadId, nodeId: e.nodeId, text: e.text });
|
|
229
|
+
projectorFor(threadId).emit(e);
|
|
230
|
+
return;
|
|
231
|
+
case "text_delta":
|
|
232
|
+
room(threadId).broadcast("harness.textDelta", { threadId, nodeId: e.nodeId, text: e.text });
|
|
233
|
+
projectorFor(threadId).emit(e);
|
|
234
|
+
return;
|
|
235
|
+
case "tool_input_delta":
|
|
236
|
+
room(threadId).broadcast("harness.toolInputDelta", {
|
|
237
|
+
threadId,
|
|
238
|
+
nodeId: e.nodeId,
|
|
239
|
+
toolCallId: e.toolCallId,
|
|
240
|
+
argsTextDelta: e.argsTextDelta
|
|
241
|
+
});
|
|
242
|
+
projectorFor(threadId).emit(e);
|
|
243
|
+
return;
|
|
244
|
+
case "suspended":
|
|
245
|
+
room(threadId).broadcast("harness.suspended", {
|
|
246
|
+
threadId,
|
|
247
|
+
nodeId: e.nodeId,
|
|
248
|
+
toolCallId: e.toolCallId,
|
|
249
|
+
toolName: e.toolName,
|
|
250
|
+
request: e.request,
|
|
251
|
+
resumeSchema: e.resumeSchema
|
|
252
|
+
});
|
|
253
|
+
if (e.nodeId && e.toolCallId) sinkFor(threadId).setPending(e.nodeId, e.toolCallId, { resumeSchema: e.resumeSchema, request: e.request });
|
|
254
|
+
return;
|
|
255
|
+
case "approval_required":
|
|
256
|
+
room(threadId).broadcast("harness.approvalRequired", {
|
|
257
|
+
threadId,
|
|
258
|
+
nodeId: e.nodeId,
|
|
259
|
+
toolCallId: e.toolCallId,
|
|
260
|
+
toolName: e.toolName,
|
|
261
|
+
args: e.args
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
264
|
+
case "mode_changed":
|
|
265
|
+
room(threadId).broadcast("harness.modeChanged", { threadId, modeId: e.modeId, previousModeId: e.previousModeId });
|
|
266
|
+
return;
|
|
267
|
+
case "follow_up_queued":
|
|
268
|
+
room(threadId).broadcast("harness.followUpQueued", { threadId, count: e.count });
|
|
269
|
+
return;
|
|
270
|
+
case "thread_created":
|
|
271
|
+
sinkFor(threadId).setThreadMeta({ resourceId: e.resourceId, title: e.title, createdAt: now(), updatedAt: now() });
|
|
272
|
+
return;
|
|
273
|
+
case "thread_renamed":
|
|
274
|
+
sinkFor(threadId).setThreadMeta({ title: e.title, updatedAt: now() });
|
|
275
|
+
return;
|
|
276
|
+
case "thread_deleted":
|
|
277
|
+
void purgeThread(threadId);
|
|
278
|
+
return;
|
|
279
|
+
case "tree_changed":
|
|
280
|
+
return;
|
|
281
|
+
default:
|
|
282
|
+
projectorFor(threadId).emit(e);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
const buildHandlers = (ctx) => {
|
|
286
|
+
pctx = ctx;
|
|
287
|
+
return {
|
|
288
|
+
"harness.join": async ({ threadId }, connCtx, conn) => {
|
|
289
|
+
const c = connCtx;
|
|
290
|
+
room(threadId).add(conn);
|
|
291
|
+
const role = opts.roleFor?.(c) ?? opts.defaultRole ?? "operator";
|
|
292
|
+
const row = { id: (0, import_shared.membershipId)(threadId, c.userId), threadId, userId: c.userId, role, joinedAt: now() };
|
|
293
|
+
await col(import_shared.HARNESS_MEMBERSHIP).insert(row).catch((e) => {
|
|
294
|
+
if (e?.code !== "CONFLICT") console.error("[harness] membership insert failed", e);
|
|
295
|
+
});
|
|
296
|
+
return ok;
|
|
297
|
+
},
|
|
298
|
+
"harness.sendMessage": async ({ threadId, message, files }, connCtx) => {
|
|
299
|
+
await requireDriver(threadId, connCtx);
|
|
300
|
+
void engine.sendMessage({ threadId, content: message, files }).catch((e) => console.error("[harness] run failed", e));
|
|
301
|
+
return ok;
|
|
302
|
+
},
|
|
303
|
+
"harness.resumeMessage": async ({ threadId, toolCallId, resumeData }, connCtx) => {
|
|
304
|
+
await requireDriver(threadId, connCtx);
|
|
305
|
+
try {
|
|
306
|
+
void engine.resume({ threadId, toolCallId, resumeData }).catch((e) => console.error("[harness] resume failed", e));
|
|
307
|
+
return ok;
|
|
308
|
+
} catch (e) {
|
|
309
|
+
console.error("[harness] resume rejected", e);
|
|
310
|
+
return { ok: false };
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
"harness.abort": async ({ threadId }, connCtx) => {
|
|
314
|
+
await requireDriver(threadId, connCtx);
|
|
315
|
+
engine.abort(threadId);
|
|
316
|
+
return ok;
|
|
317
|
+
},
|
|
318
|
+
"harness.respondToApproval": async (input, connCtx) => {
|
|
319
|
+
await requireDriver(input.threadId, connCtx);
|
|
320
|
+
return attempt(() => engine.respondToApproval(input));
|
|
321
|
+
},
|
|
322
|
+
"harness.switchMode": async ({ threadId, modeId }, connCtx) => {
|
|
323
|
+
await requireDriver(threadId, connCtx);
|
|
324
|
+
return attempt(() => engine.switchMode(threadId, modeId));
|
|
325
|
+
},
|
|
326
|
+
"harness.listModes": async () => ({
|
|
327
|
+
modes: engine.listModes().map((m) => ({ id: m.id, name: m.name, description: m.description })),
|
|
328
|
+
defaultModeId: engine.defaultModeId
|
|
329
|
+
}),
|
|
330
|
+
// A connection-pinned resourceId is AUTHORITATIVE (the host's authenticate
|
|
331
|
+
// validated it — honoring the request's would let any client enumerate or
|
|
332
|
+
// write another tenant's scope). Hosts that leave ctx.resourceId unset opt
|
|
333
|
+
// into request-level scoping instead: clients switch resources per call,
|
|
334
|
+
// no reconnect. Note this covers the REQUEST surface only — a
|
|
335
|
+
// harness.threads collection subscription's read filter is frozen per
|
|
336
|
+
// connection from ctx, so live sidebar reactivity follows ctx, not requests.
|
|
337
|
+
"harness.listThreads": async (input, connCtx) => ({
|
|
338
|
+
threads: await engine.threads.list(connCtx.resourceId ?? input.resourceId)
|
|
339
|
+
}),
|
|
340
|
+
"harness.createThread": async (input, connCtx) => ({
|
|
341
|
+
threadId: (await engine.threads.create({ ...input, resourceId: connCtx.resourceId ?? input.resourceId })).id
|
|
342
|
+
}),
|
|
343
|
+
"harness.renameThread": async ({ threadId, title }, connCtx) => {
|
|
344
|
+
await requireDriver(threadId, connCtx);
|
|
345
|
+
return attempt(() => engine.threads.rename(threadId, title));
|
|
346
|
+
},
|
|
347
|
+
"harness.deleteThread": async ({ threadId }, connCtx) => {
|
|
348
|
+
await requireDriver(threadId, connCtx);
|
|
349
|
+
return attempt(() => engine.threads.delete(threadId));
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
return {
|
|
354
|
+
name: "harness",
|
|
355
|
+
policies: {
|
|
356
|
+
[import_shared.HARNESS_MEMBERSHIP]: { read: (principal) => (0, import_core.eq)("userId", principal) },
|
|
357
|
+
[import_shared.HARNESS_NODES]: { read: async (principal) => (0, import_core.isIn)("threadId", await joined(principal)) },
|
|
358
|
+
[import_shared.HARNESS_TOOLS]: { read: async (principal) => (0, import_core.isIn)("threadId", await joined(principal)) },
|
|
359
|
+
// Thread LIST = your resource's threads (sidebar) UNIONED with threads you
|
|
360
|
+
// joined. Membership must never be masked by resourceId: a client-minted
|
|
361
|
+
// thread (join + send, no createThread) has no resourceId on its row, and
|
|
362
|
+
// an eq-only filter would hide the caller's OWN thread — empty turns.
|
|
363
|
+
[import_shared.HARNESS_THREADS]: {
|
|
364
|
+
read: async (principal, ctx) => {
|
|
365
|
+
const rid = ctx?.resourceId;
|
|
366
|
+
const mine = (0, import_core.isIn)("id", await joined(principal));
|
|
367
|
+
return rid ? (0, import_core.or)((0, import_core.eq)("resourceId", rid), mine) : mine;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
handlers: buildHandlers,
|
|
372
|
+
setup: (ctx) => {
|
|
373
|
+
pctx = ctx;
|
|
374
|
+
const unsub = engine.subscribe(onBus);
|
|
375
|
+
return () => unsub();
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function now() {
|
|
380
|
+
return Date.now();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/serve.ts
|
|
384
|
+
async function collectionsFor(storage) {
|
|
385
|
+
switch (storage.type) {
|
|
386
|
+
case "memory":
|
|
387
|
+
return (0, import_collections_memory.memoryCollections)();
|
|
388
|
+
case "pglite": {
|
|
389
|
+
const { pgliteCollections } = await import("@super-line/collections-pglite");
|
|
390
|
+
return pgliteCollections({ pgUrl: storage.pgUrl, electricUrl: storage.electricUrl });
|
|
391
|
+
}
|
|
392
|
+
default:
|
|
393
|
+
return (0, import_collections_sqlite.sqliteCollections)({ file: storage.file ?? "./harness.db" });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
var defaultAuthenticate = (h) => {
|
|
397
|
+
const q = h?.query ?? {};
|
|
398
|
+
return { role: "user", ctx: { userId: q.userId ?? q.resourceId ?? "local", resourceId: q.resourceId } };
|
|
399
|
+
};
|
|
400
|
+
async function serve(engine, config = {}) {
|
|
401
|
+
const collections = await collectionsFor(config.storage ?? { type: "sqlite" });
|
|
402
|
+
const server = (0, import_server.createSuperLineServer)(import_shared2.contract, {
|
|
403
|
+
transports: config.transports ?? [],
|
|
404
|
+
authenticate: config.authenticate ?? defaultAuthenticate,
|
|
405
|
+
identify: (conn) => conn.ctx.userId,
|
|
406
|
+
collections,
|
|
407
|
+
plugins: [harness(engine, config.plugin), ...config.plugins ?? []],
|
|
408
|
+
adapter: config.adapter
|
|
409
|
+
});
|
|
410
|
+
server.implement({});
|
|
411
|
+
return { server, close: () => void server.close() };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/index.ts
|
|
415
|
+
var import_shared3 = require("@super-harness/shared");
|
|
416
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
417
|
+
0 && (module.exports = {
|
|
418
|
+
collectionsTreeSink,
|
|
419
|
+
harness,
|
|
420
|
+
harnessContract,
|
|
421
|
+
serve
|
|
422
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ServerTransport, Adapter } from '@super-line/core';
|
|
2
|
+
import { SuperLinePlugin, createSuperLineServer } from '@super-line/server';
|
|
3
|
+
import { Harness, TreeSink } from '@super-harness/core';
|
|
4
|
+
import { MemberRole, HarnessSurface } from '@super-harness/shared';
|
|
5
|
+
export { harnessContract } from '@super-harness/shared';
|
|
6
|
+
|
|
7
|
+
interface HarnessCtx {
|
|
8
|
+
userId: string;
|
|
9
|
+
resourceId?: string;
|
|
10
|
+
roles?: string[];
|
|
11
|
+
}
|
|
12
|
+
interface HarnessPluginOptions {
|
|
13
|
+
roleFor?: (ctx: HarnessCtx) => MemberRole;
|
|
14
|
+
defaultRole?: MemberRole;
|
|
15
|
+
}
|
|
16
|
+
declare function harness(engine: Harness, opts?: HarnessPluginOptions): SuperLinePlugin<HarnessSurface>;
|
|
17
|
+
|
|
18
|
+
type HarnessStorage = {
|
|
19
|
+
type: 'memory';
|
|
20
|
+
} | {
|
|
21
|
+
type: 'sqlite';
|
|
22
|
+
file?: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'pglite';
|
|
25
|
+
pgUrl: string;
|
|
26
|
+
electricUrl?: string;
|
|
27
|
+
};
|
|
28
|
+
interface ServeConfig {
|
|
29
|
+
storage?: HarnessStorage;
|
|
30
|
+
transports?: ServerTransport[];
|
|
31
|
+
adapter?: Adapter;
|
|
32
|
+
authenticate?: (handshake: unknown) => {
|
|
33
|
+
role: 'user';
|
|
34
|
+
ctx: HarnessCtx;
|
|
35
|
+
};
|
|
36
|
+
plugin?: HarnessPluginOptions;
|
|
37
|
+
plugins?: SuperLinePlugin[];
|
|
38
|
+
}
|
|
39
|
+
interface HarnessServer {
|
|
40
|
+
server: ReturnType<typeof createSuperLineServer>;
|
|
41
|
+
close(): void;
|
|
42
|
+
}
|
|
43
|
+
declare function serve(engine: Harness, config?: ServeConfig): Promise<HarnessServer>;
|
|
44
|
+
|
|
45
|
+
interface CollectionHandleLike {
|
|
46
|
+
insert(row: unknown): Promise<void>;
|
|
47
|
+
update(row: unknown): Promise<void>;
|
|
48
|
+
delete(id: string): Promise<void>;
|
|
49
|
+
snapshot(query?: unknown): Promise<unknown[]>;
|
|
50
|
+
}
|
|
51
|
+
interface Collections {
|
|
52
|
+
(name: string): CollectionHandleLike;
|
|
53
|
+
}
|
|
54
|
+
interface CollectionsTreeSink extends TreeSink {
|
|
55
|
+
setThreadMeta(meta: {
|
|
56
|
+
resourceId?: string;
|
|
57
|
+
title?: string;
|
|
58
|
+
createdAt?: number;
|
|
59
|
+
updatedAt?: number;
|
|
60
|
+
}): void;
|
|
61
|
+
setPending(nodeId: string, toolCallId: string, payload: {
|
|
62
|
+
resumeSchema?: string;
|
|
63
|
+
request?: unknown;
|
|
64
|
+
}): void;
|
|
65
|
+
}
|
|
66
|
+
declare function collectionsTreeSink(opts: {
|
|
67
|
+
collections: Collections;
|
|
68
|
+
threadId: string;
|
|
69
|
+
nodes: string;
|
|
70
|
+
tools: string;
|
|
71
|
+
threads: string;
|
|
72
|
+
}): CollectionsTreeSink;
|
|
73
|
+
|
|
74
|
+
export { type CollectionHandleLike, type Collections, type CollectionsTreeSink, type HarnessCtx, type HarnessPluginOptions, type HarnessServer, type HarnessStorage, type ServeConfig, collectionsTreeSink, harness, serve };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ServerTransport, Adapter } from '@super-line/core';
|
|
2
|
+
import { SuperLinePlugin, createSuperLineServer } from '@super-line/server';
|
|
3
|
+
import { Harness, TreeSink } from '@super-harness/core';
|
|
4
|
+
import { MemberRole, HarnessSurface } from '@super-harness/shared';
|
|
5
|
+
export { harnessContract } from '@super-harness/shared';
|
|
6
|
+
|
|
7
|
+
interface HarnessCtx {
|
|
8
|
+
userId: string;
|
|
9
|
+
resourceId?: string;
|
|
10
|
+
roles?: string[];
|
|
11
|
+
}
|
|
12
|
+
interface HarnessPluginOptions {
|
|
13
|
+
roleFor?: (ctx: HarnessCtx) => MemberRole;
|
|
14
|
+
defaultRole?: MemberRole;
|
|
15
|
+
}
|
|
16
|
+
declare function harness(engine: Harness, opts?: HarnessPluginOptions): SuperLinePlugin<HarnessSurface>;
|
|
17
|
+
|
|
18
|
+
type HarnessStorage = {
|
|
19
|
+
type: 'memory';
|
|
20
|
+
} | {
|
|
21
|
+
type: 'sqlite';
|
|
22
|
+
file?: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'pglite';
|
|
25
|
+
pgUrl: string;
|
|
26
|
+
electricUrl?: string;
|
|
27
|
+
};
|
|
28
|
+
interface ServeConfig {
|
|
29
|
+
storage?: HarnessStorage;
|
|
30
|
+
transports?: ServerTransport[];
|
|
31
|
+
adapter?: Adapter;
|
|
32
|
+
authenticate?: (handshake: unknown) => {
|
|
33
|
+
role: 'user';
|
|
34
|
+
ctx: HarnessCtx;
|
|
35
|
+
};
|
|
36
|
+
plugin?: HarnessPluginOptions;
|
|
37
|
+
plugins?: SuperLinePlugin[];
|
|
38
|
+
}
|
|
39
|
+
interface HarnessServer {
|
|
40
|
+
server: ReturnType<typeof createSuperLineServer>;
|
|
41
|
+
close(): void;
|
|
42
|
+
}
|
|
43
|
+
declare function serve(engine: Harness, config?: ServeConfig): Promise<HarnessServer>;
|
|
44
|
+
|
|
45
|
+
interface CollectionHandleLike {
|
|
46
|
+
insert(row: unknown): Promise<void>;
|
|
47
|
+
update(row: unknown): Promise<void>;
|
|
48
|
+
delete(id: string): Promise<void>;
|
|
49
|
+
snapshot(query?: unknown): Promise<unknown[]>;
|
|
50
|
+
}
|
|
51
|
+
interface Collections {
|
|
52
|
+
(name: string): CollectionHandleLike;
|
|
53
|
+
}
|
|
54
|
+
interface CollectionsTreeSink extends TreeSink {
|
|
55
|
+
setThreadMeta(meta: {
|
|
56
|
+
resourceId?: string;
|
|
57
|
+
title?: string;
|
|
58
|
+
createdAt?: number;
|
|
59
|
+
updatedAt?: number;
|
|
60
|
+
}): void;
|
|
61
|
+
setPending(nodeId: string, toolCallId: string, payload: {
|
|
62
|
+
resumeSchema?: string;
|
|
63
|
+
request?: unknown;
|
|
64
|
+
}): void;
|
|
65
|
+
}
|
|
66
|
+
declare function collectionsTreeSink(opts: {
|
|
67
|
+
collections: Collections;
|
|
68
|
+
threadId: string;
|
|
69
|
+
nodes: string;
|
|
70
|
+
tools: string;
|
|
71
|
+
threads: string;
|
|
72
|
+
}): CollectionsTreeSink;
|
|
73
|
+
|
|
74
|
+
export { type CollectionHandleLike, type Collections, type CollectionsTreeSink, type HarnessCtx, type HarnessPluginOptions, type HarnessServer, type HarnessStorage, type ServeConfig, collectionsTreeSink, harness, serve };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// src/serve.ts
|
|
2
|
+
import { createSuperLineServer } from "@super-line/server";
|
|
3
|
+
import { memoryCollections } from "@super-line/collections-memory";
|
|
4
|
+
import { sqliteCollections } from "@super-line/collections-sqlite";
|
|
5
|
+
import { contract } from "@super-harness/shared";
|
|
6
|
+
|
|
7
|
+
// src/plugin.ts
|
|
8
|
+
import { SuperLineError, eq, isIn, or } from "@super-line/core";
|
|
9
|
+
import { Projector } from "@super-harness/core";
|
|
10
|
+
import {
|
|
11
|
+
HARNESS_THREADS,
|
|
12
|
+
HARNESS_NODES,
|
|
13
|
+
HARNESS_TOOLS,
|
|
14
|
+
HARNESS_MEMBERSHIP,
|
|
15
|
+
membershipId,
|
|
16
|
+
harnessThreadRoom
|
|
17
|
+
} from "@super-harness/shared";
|
|
18
|
+
|
|
19
|
+
// src/sink.ts
|
|
20
|
+
var isCode = (e, code) => e?.code === code;
|
|
21
|
+
function collectionsTreeSink(opts) {
|
|
22
|
+
const { collections, threadId } = opts;
|
|
23
|
+
const nodesCol = () => collections(opts.nodes);
|
|
24
|
+
const toolsCol = () => collections(opts.tools);
|
|
25
|
+
const threadsCol = () => collections(opts.threads);
|
|
26
|
+
const known = /* @__PURE__ */ new Set();
|
|
27
|
+
const lastWritten = /* @__PURE__ */ new Map();
|
|
28
|
+
const chains = /* @__PURE__ */ new Map();
|
|
29
|
+
const run = (key, fn) => {
|
|
30
|
+
const prev = chains.get(key) ?? Promise.resolve();
|
|
31
|
+
chains.set(
|
|
32
|
+
key,
|
|
33
|
+
prev.then(fn).catch((e) => console.error("[harness] collection write failed", key, e))
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
const upsert = (handle, ns, id, row) => {
|
|
37
|
+
const key = `${ns}\0${id}`;
|
|
38
|
+
const serialized = JSON.stringify(row);
|
|
39
|
+
if (lastWritten.get(key) === serialized) return;
|
|
40
|
+
lastWritten.set(key, serialized);
|
|
41
|
+
run(key, async () => {
|
|
42
|
+
if (known.has(key)) return handle.update(row);
|
|
43
|
+
try {
|
|
44
|
+
await handle.insert(row);
|
|
45
|
+
known.add(key);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
if (!isCode(e, "CONFLICT")) throw e;
|
|
48
|
+
known.add(key);
|
|
49
|
+
await handle.update(row);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
let threadRow = { id: threadId, turns: [], todos: [], createdAt: 0, updatedAt: 0 };
|
|
54
|
+
const flushThread = () => upsert(threadsCol(), opts.threads, threadId, threadRow);
|
|
55
|
+
const pending = /* @__PURE__ */ new Map();
|
|
56
|
+
const pendingPayload = /* @__PURE__ */ new Map();
|
|
57
|
+
const nodeRowOf = (node) => {
|
|
58
|
+
const terminal = node.status !== "running";
|
|
59
|
+
const parkedTool = pending.get(node.nodeId);
|
|
60
|
+
const stillPending = parkedTool !== void 0 && !isSettled(node.tools[parkedTool]?.status);
|
|
61
|
+
if (parkedTool !== void 0 && !stillPending) pending.delete(node.nodeId);
|
|
62
|
+
return {
|
|
63
|
+
id: node.nodeId,
|
|
64
|
+
threadId,
|
|
65
|
+
parentNodeId: node.parentNodeId,
|
|
66
|
+
depth: node.depth,
|
|
67
|
+
agentType: node.agentType,
|
|
68
|
+
task: node.task,
|
|
69
|
+
status: node.status,
|
|
70
|
+
reasoning: terminal ? node.reasoning : "",
|
|
71
|
+
text: terminal ? node.text : "",
|
|
72
|
+
toolOrder: node.toolOrder,
|
|
73
|
+
childOrder: node.childOrder,
|
|
74
|
+
usage: node.usage,
|
|
75
|
+
durationMs: node.durationMs,
|
|
76
|
+
error: node.error,
|
|
77
|
+
textOffset: node.textOffset,
|
|
78
|
+
pendingResume: stillPending ? pendingPayload.get(node.nodeId) : void 0
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
const toolRowOf = (node, toolCallId) => {
|
|
82
|
+
const t = node.tools[toolCallId];
|
|
83
|
+
const streaming = t.status === "input-streaming";
|
|
84
|
+
return {
|
|
85
|
+
id: t.toolCallId,
|
|
86
|
+
threadId,
|
|
87
|
+
nodeId: node.nodeId,
|
|
88
|
+
toolName: t.toolName,
|
|
89
|
+
status: t.status,
|
|
90
|
+
argsText: streaming ? "" : t.argsText,
|
|
91
|
+
args: t.args,
|
|
92
|
+
result: t.result,
|
|
93
|
+
isError: t.isError,
|
|
94
|
+
textOffset: t.textOffset
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
writeNode: (node) => {
|
|
99
|
+
upsert(nodesCol(), opts.nodes, node.nodeId, nodeRowOf(node));
|
|
100
|
+
for (const tid of node.toolOrder) upsert(toolsCol(), opts.tools, tid, toolRowOf(node, tid));
|
|
101
|
+
},
|
|
102
|
+
writeThread: (doc) => {
|
|
103
|
+
threadRow = { ...threadRow, turns: doc.turns, todos: doc.todos ?? [] };
|
|
104
|
+
flushThread();
|
|
105
|
+
},
|
|
106
|
+
setThreadMeta: (meta) => {
|
|
107
|
+
threadRow = {
|
|
108
|
+
...threadRow,
|
|
109
|
+
resourceId: meta.resourceId ?? threadRow.resourceId,
|
|
110
|
+
title: meta.title ?? threadRow.title,
|
|
111
|
+
createdAt: threadRow.createdAt || meta.createdAt || 0,
|
|
112
|
+
updatedAt: meta.updatedAt ?? threadRow.updatedAt
|
|
113
|
+
};
|
|
114
|
+
flushThread();
|
|
115
|
+
},
|
|
116
|
+
setPending: (nodeId, toolCallId, payload) => {
|
|
117
|
+
pending.set(nodeId, toolCallId);
|
|
118
|
+
pendingPayload.set(nodeId, payload);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function isSettled(status) {
|
|
123
|
+
return status === "output-available" || status === "error";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/plugin.ts
|
|
127
|
+
function harness(engine, opts = {}) {
|
|
128
|
+
let pctx;
|
|
129
|
+
const P = () => {
|
|
130
|
+
if (!pctx) throw new Error("harness plugin: setup() has not run yet");
|
|
131
|
+
return pctx;
|
|
132
|
+
};
|
|
133
|
+
const col = (name) => P().collection(name);
|
|
134
|
+
const room = (threadId) => P().room(harnessThreadRoom(threadId));
|
|
135
|
+
const sinks = /* @__PURE__ */ new Map();
|
|
136
|
+
const projectors = /* @__PURE__ */ new Map();
|
|
137
|
+
const sinkFor = (threadId) => {
|
|
138
|
+
let s = sinks.get(threadId);
|
|
139
|
+
if (!s) {
|
|
140
|
+
s = collectionsTreeSink({
|
|
141
|
+
collections: (n) => col(n),
|
|
142
|
+
threadId,
|
|
143
|
+
nodes: HARNESS_NODES,
|
|
144
|
+
tools: HARNESS_TOOLS,
|
|
145
|
+
threads: HARNESS_THREADS
|
|
146
|
+
});
|
|
147
|
+
sinks.set(threadId, s);
|
|
148
|
+
}
|
|
149
|
+
return s;
|
|
150
|
+
};
|
|
151
|
+
const projectorFor = (threadId) => {
|
|
152
|
+
let p = projectors.get(threadId);
|
|
153
|
+
if (!p) {
|
|
154
|
+
p = new Projector(sinkFor(threadId));
|
|
155
|
+
projectors.set(threadId, p);
|
|
156
|
+
}
|
|
157
|
+
return p;
|
|
158
|
+
};
|
|
159
|
+
const joined = async (principal) => {
|
|
160
|
+
const rows = await col(HARNESS_MEMBERSHIP).snapshot({ filter: eq("userId", principal) });
|
|
161
|
+
return rows.map((r) => r.threadId);
|
|
162
|
+
};
|
|
163
|
+
const roleOf = async (threadId, userId) => {
|
|
164
|
+
const row = await col(HARNESS_MEMBERSHIP).read(membershipId(threadId, userId));
|
|
165
|
+
return row?.role;
|
|
166
|
+
};
|
|
167
|
+
const requireDriver = async (threadId, ctx) => {
|
|
168
|
+
if (await roleOf(threadId, ctx.userId) === "viewer") {
|
|
169
|
+
throw new SuperLineError("FORBIDDEN", "viewer role cannot drive the thread");
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const ok = { ok: true };
|
|
173
|
+
const attempt = async (fn) => {
|
|
174
|
+
try {
|
|
175
|
+
await fn();
|
|
176
|
+
return ok;
|
|
177
|
+
} catch (e) {
|
|
178
|
+
console.error("[harness] request failed", e);
|
|
179
|
+
return { ok: false };
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const purgeThread = async (threadId) => {
|
|
183
|
+
projectors.delete(threadId);
|
|
184
|
+
sinks.delete(threadId);
|
|
185
|
+
for (const c of [HARNESS_NODES, HARNESS_TOOLS, HARNESS_MEMBERSHIP]) {
|
|
186
|
+
const rows = await col(c).snapshot({ filter: eq("threadId", threadId) }).catch(() => []);
|
|
187
|
+
for (const r of rows) await col(c).delete(r.id).catch(() => {
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
await col(HARNESS_THREADS).delete(threadId).catch(() => {
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
const onBus = (threadId, e) => {
|
|
194
|
+
switch (e.type) {
|
|
195
|
+
case "reasoning_delta":
|
|
196
|
+
room(threadId).broadcast("harness.reasoningDelta", { threadId, nodeId: e.nodeId, text: e.text });
|
|
197
|
+
projectorFor(threadId).emit(e);
|
|
198
|
+
return;
|
|
199
|
+
case "text_delta":
|
|
200
|
+
room(threadId).broadcast("harness.textDelta", { threadId, nodeId: e.nodeId, text: e.text });
|
|
201
|
+
projectorFor(threadId).emit(e);
|
|
202
|
+
return;
|
|
203
|
+
case "tool_input_delta":
|
|
204
|
+
room(threadId).broadcast("harness.toolInputDelta", {
|
|
205
|
+
threadId,
|
|
206
|
+
nodeId: e.nodeId,
|
|
207
|
+
toolCallId: e.toolCallId,
|
|
208
|
+
argsTextDelta: e.argsTextDelta
|
|
209
|
+
});
|
|
210
|
+
projectorFor(threadId).emit(e);
|
|
211
|
+
return;
|
|
212
|
+
case "suspended":
|
|
213
|
+
room(threadId).broadcast("harness.suspended", {
|
|
214
|
+
threadId,
|
|
215
|
+
nodeId: e.nodeId,
|
|
216
|
+
toolCallId: e.toolCallId,
|
|
217
|
+
toolName: e.toolName,
|
|
218
|
+
request: e.request,
|
|
219
|
+
resumeSchema: e.resumeSchema
|
|
220
|
+
});
|
|
221
|
+
if (e.nodeId && e.toolCallId) sinkFor(threadId).setPending(e.nodeId, e.toolCallId, { resumeSchema: e.resumeSchema, request: e.request });
|
|
222
|
+
return;
|
|
223
|
+
case "approval_required":
|
|
224
|
+
room(threadId).broadcast("harness.approvalRequired", {
|
|
225
|
+
threadId,
|
|
226
|
+
nodeId: e.nodeId,
|
|
227
|
+
toolCallId: e.toolCallId,
|
|
228
|
+
toolName: e.toolName,
|
|
229
|
+
args: e.args
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
case "mode_changed":
|
|
233
|
+
room(threadId).broadcast("harness.modeChanged", { threadId, modeId: e.modeId, previousModeId: e.previousModeId });
|
|
234
|
+
return;
|
|
235
|
+
case "follow_up_queued":
|
|
236
|
+
room(threadId).broadcast("harness.followUpQueued", { threadId, count: e.count });
|
|
237
|
+
return;
|
|
238
|
+
case "thread_created":
|
|
239
|
+
sinkFor(threadId).setThreadMeta({ resourceId: e.resourceId, title: e.title, createdAt: now(), updatedAt: now() });
|
|
240
|
+
return;
|
|
241
|
+
case "thread_renamed":
|
|
242
|
+
sinkFor(threadId).setThreadMeta({ title: e.title, updatedAt: now() });
|
|
243
|
+
return;
|
|
244
|
+
case "thread_deleted":
|
|
245
|
+
void purgeThread(threadId);
|
|
246
|
+
return;
|
|
247
|
+
case "tree_changed":
|
|
248
|
+
return;
|
|
249
|
+
default:
|
|
250
|
+
projectorFor(threadId).emit(e);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const buildHandlers = (ctx) => {
|
|
254
|
+
pctx = ctx;
|
|
255
|
+
return {
|
|
256
|
+
"harness.join": async ({ threadId }, connCtx, conn) => {
|
|
257
|
+
const c = connCtx;
|
|
258
|
+
room(threadId).add(conn);
|
|
259
|
+
const role = opts.roleFor?.(c) ?? opts.defaultRole ?? "operator";
|
|
260
|
+
const row = { id: membershipId(threadId, c.userId), threadId, userId: c.userId, role, joinedAt: now() };
|
|
261
|
+
await col(HARNESS_MEMBERSHIP).insert(row).catch((e) => {
|
|
262
|
+
if (e?.code !== "CONFLICT") console.error("[harness] membership insert failed", e);
|
|
263
|
+
});
|
|
264
|
+
return ok;
|
|
265
|
+
},
|
|
266
|
+
"harness.sendMessage": async ({ threadId, message, files }, connCtx) => {
|
|
267
|
+
await requireDriver(threadId, connCtx);
|
|
268
|
+
void engine.sendMessage({ threadId, content: message, files }).catch((e) => console.error("[harness] run failed", e));
|
|
269
|
+
return ok;
|
|
270
|
+
},
|
|
271
|
+
"harness.resumeMessage": async ({ threadId, toolCallId, resumeData }, connCtx) => {
|
|
272
|
+
await requireDriver(threadId, connCtx);
|
|
273
|
+
try {
|
|
274
|
+
void engine.resume({ threadId, toolCallId, resumeData }).catch((e) => console.error("[harness] resume failed", e));
|
|
275
|
+
return ok;
|
|
276
|
+
} catch (e) {
|
|
277
|
+
console.error("[harness] resume rejected", e);
|
|
278
|
+
return { ok: false };
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
"harness.abort": async ({ threadId }, connCtx) => {
|
|
282
|
+
await requireDriver(threadId, connCtx);
|
|
283
|
+
engine.abort(threadId);
|
|
284
|
+
return ok;
|
|
285
|
+
},
|
|
286
|
+
"harness.respondToApproval": async (input, connCtx) => {
|
|
287
|
+
await requireDriver(input.threadId, connCtx);
|
|
288
|
+
return attempt(() => engine.respondToApproval(input));
|
|
289
|
+
},
|
|
290
|
+
"harness.switchMode": async ({ threadId, modeId }, connCtx) => {
|
|
291
|
+
await requireDriver(threadId, connCtx);
|
|
292
|
+
return attempt(() => engine.switchMode(threadId, modeId));
|
|
293
|
+
},
|
|
294
|
+
"harness.listModes": async () => ({
|
|
295
|
+
modes: engine.listModes().map((m) => ({ id: m.id, name: m.name, description: m.description })),
|
|
296
|
+
defaultModeId: engine.defaultModeId
|
|
297
|
+
}),
|
|
298
|
+
// A connection-pinned resourceId is AUTHORITATIVE (the host's authenticate
|
|
299
|
+
// validated it — honoring the request's would let any client enumerate or
|
|
300
|
+
// write another tenant's scope). Hosts that leave ctx.resourceId unset opt
|
|
301
|
+
// into request-level scoping instead: clients switch resources per call,
|
|
302
|
+
// no reconnect. Note this covers the REQUEST surface only — a
|
|
303
|
+
// harness.threads collection subscription's read filter is frozen per
|
|
304
|
+
// connection from ctx, so live sidebar reactivity follows ctx, not requests.
|
|
305
|
+
"harness.listThreads": async (input, connCtx) => ({
|
|
306
|
+
threads: await engine.threads.list(connCtx.resourceId ?? input.resourceId)
|
|
307
|
+
}),
|
|
308
|
+
"harness.createThread": async (input, connCtx) => ({
|
|
309
|
+
threadId: (await engine.threads.create({ ...input, resourceId: connCtx.resourceId ?? input.resourceId })).id
|
|
310
|
+
}),
|
|
311
|
+
"harness.renameThread": async ({ threadId, title }, connCtx) => {
|
|
312
|
+
await requireDriver(threadId, connCtx);
|
|
313
|
+
return attempt(() => engine.threads.rename(threadId, title));
|
|
314
|
+
},
|
|
315
|
+
"harness.deleteThread": async ({ threadId }, connCtx) => {
|
|
316
|
+
await requireDriver(threadId, connCtx);
|
|
317
|
+
return attempt(() => engine.threads.delete(threadId));
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
return {
|
|
322
|
+
name: "harness",
|
|
323
|
+
policies: {
|
|
324
|
+
[HARNESS_MEMBERSHIP]: { read: (principal) => eq("userId", principal) },
|
|
325
|
+
[HARNESS_NODES]: { read: async (principal) => isIn("threadId", await joined(principal)) },
|
|
326
|
+
[HARNESS_TOOLS]: { read: async (principal) => isIn("threadId", await joined(principal)) },
|
|
327
|
+
// Thread LIST = your resource's threads (sidebar) UNIONED with threads you
|
|
328
|
+
// joined. Membership must never be masked by resourceId: a client-minted
|
|
329
|
+
// thread (join + send, no createThread) has no resourceId on its row, and
|
|
330
|
+
// an eq-only filter would hide the caller's OWN thread — empty turns.
|
|
331
|
+
[HARNESS_THREADS]: {
|
|
332
|
+
read: async (principal, ctx) => {
|
|
333
|
+
const rid = ctx?.resourceId;
|
|
334
|
+
const mine = isIn("id", await joined(principal));
|
|
335
|
+
return rid ? or(eq("resourceId", rid), mine) : mine;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
handlers: buildHandlers,
|
|
340
|
+
setup: (ctx) => {
|
|
341
|
+
pctx = ctx;
|
|
342
|
+
const unsub = engine.subscribe(onBus);
|
|
343
|
+
return () => unsub();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function now() {
|
|
348
|
+
return Date.now();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/serve.ts
|
|
352
|
+
async function collectionsFor(storage) {
|
|
353
|
+
switch (storage.type) {
|
|
354
|
+
case "memory":
|
|
355
|
+
return memoryCollections();
|
|
356
|
+
case "pglite": {
|
|
357
|
+
const { pgliteCollections } = await import("@super-line/collections-pglite");
|
|
358
|
+
return pgliteCollections({ pgUrl: storage.pgUrl, electricUrl: storage.electricUrl });
|
|
359
|
+
}
|
|
360
|
+
default:
|
|
361
|
+
return sqliteCollections({ file: storage.file ?? "./harness.db" });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
var defaultAuthenticate = (h) => {
|
|
365
|
+
const q = h?.query ?? {};
|
|
366
|
+
return { role: "user", ctx: { userId: q.userId ?? q.resourceId ?? "local", resourceId: q.resourceId } };
|
|
367
|
+
};
|
|
368
|
+
async function serve(engine, config = {}) {
|
|
369
|
+
const collections = await collectionsFor(config.storage ?? { type: "sqlite" });
|
|
370
|
+
const server = createSuperLineServer(contract, {
|
|
371
|
+
transports: config.transports ?? [],
|
|
372
|
+
authenticate: config.authenticate ?? defaultAuthenticate,
|
|
373
|
+
identify: (conn) => conn.ctx.userId,
|
|
374
|
+
collections,
|
|
375
|
+
plugins: [harness(engine, config.plugin), ...config.plugins ?? []],
|
|
376
|
+
adapter: config.adapter
|
|
377
|
+
});
|
|
378
|
+
server.implement({});
|
|
379
|
+
return { server, close: () => void server.close() };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/index.ts
|
|
383
|
+
import { harnessContract } from "@super-harness/shared";
|
|
384
|
+
export {
|
|
385
|
+
collectionsTreeSink,
|
|
386
|
+
harness,
|
|
387
|
+
harnessContract,
|
|
388
|
+
serve
|
|
389
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@super-harness/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Batteries-included super-line binding for Super Harness: the harness() plugin and standalone serve() expose a core Harness over a super-line server with durable harness.* collections, the wire contract, and HITL suspension/approval broadcast.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"agents",
|
|
8
|
+
"multi-agent",
|
|
9
|
+
"ai",
|
|
10
|
+
"llm",
|
|
11
|
+
"language-model",
|
|
12
|
+
"tool-calling",
|
|
13
|
+
"orchestration",
|
|
14
|
+
"supervisor",
|
|
15
|
+
"subagent",
|
|
16
|
+
"streaming",
|
|
17
|
+
"harness",
|
|
18
|
+
"mastra",
|
|
19
|
+
"super-line",
|
|
20
|
+
"typescript"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://mertdogar.github.io/super-harness/",
|
|
23
|
+
"bugs": "https://github.com/mertdogar/super-harness/issues",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "Mert Dogar",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/mertdogar/super-harness.git",
|
|
29
|
+
"directory": "packages/server"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"CHANGELOG.md"
|
|
34
|
+
],
|
|
35
|
+
"type": "module",
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"main": "./dist/index.js",
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"import": {
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"default": "./dist/index.js"
|
|
44
|
+
},
|
|
45
|
+
"require": {
|
|
46
|
+
"types": "./dist/index.d.cts",
|
|
47
|
+
"default": "./dist/index.cjs"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@super-harness/shared": "^0.1.0",
|
|
56
|
+
"@super-harness/core": "^0.1.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@mastra/core": "^1.49.0-alpha.2",
|
|
60
|
+
"@super-line/collections-memory": "^0.1.0",
|
|
61
|
+
"@super-line/collections-pglite": "^0.1.0",
|
|
62
|
+
"@super-line/collections-sqlite": "^0.1.0",
|
|
63
|
+
"@super-line/core": "^0.11.0",
|
|
64
|
+
"@super-line/server": "^0.11.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"@super-line/collections-pglite": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@electric-sql/pglite": "^0.5.4",
|
|
73
|
+
"@libsql/client": "^0.17.4",
|
|
74
|
+
"@mastra/core": "1.49.0-alpha.2",
|
|
75
|
+
"@super-line/client": "^0.9.0",
|
|
76
|
+
"@super-line/collections-memory": "^0.1.0",
|
|
77
|
+
"@super-line/collections-pglite": "^0.1.0",
|
|
78
|
+
"@super-line/collections-sqlite": "^0.1.0",
|
|
79
|
+
"@super-line/core": "^0.11.0",
|
|
80
|
+
"@super-line/server": "^0.11.0",
|
|
81
|
+
"@super-line/transport-websocket": "^0.6.0",
|
|
82
|
+
"vitest": "^4.1.2",
|
|
83
|
+
"zod": "^4.4.3"
|
|
84
|
+
},
|
|
85
|
+
"engines": {
|
|
86
|
+
"node": ">=18"
|
|
87
|
+
},
|
|
88
|
+
"scripts": {
|
|
89
|
+
"build": "tsup",
|
|
90
|
+
"test": "vitest run",
|
|
91
|
+
"test:watch": "vitest"
|
|
92
|
+
}
|
|
93
|
+
}
|