@synapsor/client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/generated-contract.mjs +273 -0
- package/package.json +40 -0
- package/synapsor.mjs +611 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Synapsor Node.js Adapter
|
|
2
|
+
|
|
3
|
+
The Node.js adapter is a thin ESM client over `synapsor_server` HTTP/JSON APIs.
|
|
4
|
+
It keeps Synapsor semantics in C++ and exposes a compact JavaScript API.
|
|
5
|
+
|
|
6
|
+
## Local Usage
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import { Synapsor } from "@synapsor/client";
|
|
10
|
+
|
|
11
|
+
const db = await Synapsor.connect("app.db", { autoStart: true });
|
|
12
|
+
db.setSession({
|
|
13
|
+
tenant_id: "acme",
|
|
14
|
+
principal: "support_agent_17",
|
|
15
|
+
session_id: "agent_run_1",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
await db.execute("CREATE TABLE messages (id INT, body VARCHAR);");
|
|
19
|
+
await db.execute("INSERT INTO messages VALUES (1, 'hello');");
|
|
20
|
+
const rows = await db.query("SELECT * FROM messages;");
|
|
21
|
+
|
|
22
|
+
const proposal = await db.proposeMemoryFact({
|
|
23
|
+
scope: ["tenant", "acme"],
|
|
24
|
+
subject: ["preference", "answer_style"],
|
|
25
|
+
claim: "The operator prefers concise answers.",
|
|
26
|
+
source: ["turn", "msg_1"],
|
|
27
|
+
trust: "verified",
|
|
28
|
+
approval: "approved",
|
|
29
|
+
reason: "stable preference extracted from chat",
|
|
30
|
+
});
|
|
31
|
+
await db.approveMemoryProposal(proposal.proposal.proposal_id, "operator approved");
|
|
32
|
+
|
|
33
|
+
await db.close();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Hosted Usage
|
|
37
|
+
|
|
38
|
+
Install from npm after the package is published:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install @synapsor/client
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { Synapsor } from "@synapsor/client";
|
|
46
|
+
|
|
47
|
+
const db = await Synapsor.connect("https://dev-api.synapsor.ai", {
|
|
48
|
+
apiKey: process.env.SYNAPSOR_API_KEY,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
db.setSession({
|
|
52
|
+
tenant_id: "acme",
|
|
53
|
+
principal: "app_user_1",
|
|
54
|
+
session_id: "first_hosted_run",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
console.log(await db.query("SELECT 1;"));
|
|
58
|
+
const ctx = await db.invokeAgentCapability("chat.prepare_llm_context", { question: "..." });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use database-scoped API keys from the Synapsor control panel for hosted projects.
|
|
62
|
+
The cloud gateway pins those keys to the database runtime branch before forwarding
|
|
63
|
+
requests to the single-node runtime.
|
|
64
|
+
|
|
65
|
+
## API Surface
|
|
66
|
+
|
|
67
|
+
- `execute(sql)` and `query(sql)`
|
|
68
|
+
- `setSession({...})`
|
|
69
|
+
- `invokeAgentCapability(name, args)`
|
|
70
|
+
- `listCapabilities(query)`
|
|
71
|
+
- `rememberFact({...})`
|
|
72
|
+
- `proposeMemoryFact({...})`
|
|
73
|
+
- `approveMemoryProposal(id, reason)`
|
|
74
|
+
- `rejectMemoryProposal(id, reason)`
|
|
75
|
+
- `listMemoryProposals({...})`
|
|
76
|
+
- `recallMemory({...})`
|
|
77
|
+
- `retireFact(id, {...})` and `forgetFact(id, reason)`
|
|
78
|
+
- `checkFactForAction({...})`
|
|
79
|
+
- branch helpers: `createBranch`, `useBranch`, `diffBranch`, `mergeBranch`, `dropBranch`
|
|
80
|
+
- write lifecycle helpers: `previewWrite`, `approveWrite`, `commitWrite`, `rejectWrite`
|
|
81
|
+
- `readResource(uri)`
|
|
82
|
+
|
|
83
|
+
Errors from Synapsor become `SynapsorError` with `status` and `payload`.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// Generated by tools/generate_openapi_sdk_contract.py. Do not edit by hand.
|
|
2
|
+
export const PROTOCOL_VERSION = "v1";
|
|
3
|
+
export const CONTRACT_SHA256 = "f738189b0873fe48c5900cbffafb7d4eed90acbe9fc8c7cfcd4db89ad78f106a";
|
|
4
|
+
export const OPENAPI_OPERATIONS = Object.freeze({
|
|
5
|
+
"approveAgentProposal": {
|
|
6
|
+
"method": "POST",
|
|
7
|
+
"node_method": "approveWrite",
|
|
8
|
+
"path": "/v1/agent/proposals/approve",
|
|
9
|
+
"public": false,
|
|
10
|
+
"python_method": "approve_write",
|
|
11
|
+
"request_schema": "#/components/schemas/ProposalLifecycleRequest"
|
|
12
|
+
},
|
|
13
|
+
"cancelAgentProposal": {
|
|
14
|
+
"method": "POST",
|
|
15
|
+
"node_method": "cancelWrite",
|
|
16
|
+
"path": "/v1/agent/proposals/cancel",
|
|
17
|
+
"public": false,
|
|
18
|
+
"python_method": "cancel_write",
|
|
19
|
+
"request_schema": "#/components/schemas/ProposalLifecycleRequest"
|
|
20
|
+
},
|
|
21
|
+
"cancelHybridReembedJob": {
|
|
22
|
+
"method": "POST",
|
|
23
|
+
"node_method": "cancelHybridReembedJob",
|
|
24
|
+
"path": "/v1/admin/hybrid/reembed-jobs/cancel",
|
|
25
|
+
"public": false,
|
|
26
|
+
"python_method": "cancel_hybrid_reembed_job",
|
|
27
|
+
"request_schema": "#/components/schemas/HybridReembedJobActionRequest"
|
|
28
|
+
},
|
|
29
|
+
"commitAgentProposal": {
|
|
30
|
+
"method": "POST",
|
|
31
|
+
"node_method": "commitWrite",
|
|
32
|
+
"path": "/v1/agent/proposals/commit",
|
|
33
|
+
"public": false,
|
|
34
|
+
"python_method": "commit_write",
|
|
35
|
+
"request_schema": "#/components/schemas/ProposalLifecycleRequest"
|
|
36
|
+
},
|
|
37
|
+
"createAdminBackup": {
|
|
38
|
+
"method": "POST",
|
|
39
|
+
"node_method": "createAdminBackup",
|
|
40
|
+
"path": "/v1/admin/backups/create",
|
|
41
|
+
"public": false,
|
|
42
|
+
"python_method": "create_admin_backup",
|
|
43
|
+
"request_schema": null
|
|
44
|
+
},
|
|
45
|
+
"getAdminCheck": {
|
|
46
|
+
"method": "GET",
|
|
47
|
+
"node_method": "adminCheck",
|
|
48
|
+
"path": "/v1/admin/check",
|
|
49
|
+
"public": false,
|
|
50
|
+
"python_method": "admin_check",
|
|
51
|
+
"request_schema": null
|
|
52
|
+
},
|
|
53
|
+
"getAdminDiagnostics": {
|
|
54
|
+
"method": "GET",
|
|
55
|
+
"node_method": "adminDiagnostics",
|
|
56
|
+
"path": "/v1/admin/diagnostics",
|
|
57
|
+
"public": false,
|
|
58
|
+
"python_method": "admin_diagnostics",
|
|
59
|
+
"request_schema": null
|
|
60
|
+
},
|
|
61
|
+
"getAdminMetrics": {
|
|
62
|
+
"method": "GET",
|
|
63
|
+
"node_method": "adminMetrics",
|
|
64
|
+
"path": "/v1/admin/metrics",
|
|
65
|
+
"public": false,
|
|
66
|
+
"python_method": "admin_metrics",
|
|
67
|
+
"request_schema": null
|
|
68
|
+
},
|
|
69
|
+
"getAdminOtelTraces": {
|
|
70
|
+
"method": "GET",
|
|
71
|
+
"node_method": "adminOtelTraces",
|
|
72
|
+
"path": "/v1/admin/otel/traces",
|
|
73
|
+
"public": false,
|
|
74
|
+
"python_method": "admin_otel_traces",
|
|
75
|
+
"request_schema": null
|
|
76
|
+
},
|
|
77
|
+
"getAdminPrometheusMetrics": {
|
|
78
|
+
"method": "GET",
|
|
79
|
+
"node_method": "adminPrometheusMetrics",
|
|
80
|
+
"path": "/v1/admin/metrics/prometheus",
|
|
81
|
+
"public": false,
|
|
82
|
+
"python_method": "admin_prometheus_metrics",
|
|
83
|
+
"request_schema": null
|
|
84
|
+
},
|
|
85
|
+
"getHealth": {
|
|
86
|
+
"method": "GET",
|
|
87
|
+
"node_method": "health",
|
|
88
|
+
"path": "/v1/health",
|
|
89
|
+
"public": true,
|
|
90
|
+
"python_method": "health",
|
|
91
|
+
"request_schema": null
|
|
92
|
+
},
|
|
93
|
+
"getHybridReembedJobs": {
|
|
94
|
+
"method": "GET",
|
|
95
|
+
"node_method": "hybridReembedJobs",
|
|
96
|
+
"path": "/v1/admin/hybrid/reembed-jobs",
|
|
97
|
+
"public": false,
|
|
98
|
+
"python_method": "hybrid_reembed_jobs",
|
|
99
|
+
"request_schema": null
|
|
100
|
+
},
|
|
101
|
+
"getOpenApi": {
|
|
102
|
+
"method": "GET",
|
|
103
|
+
"node_method": "openapi",
|
|
104
|
+
"path": "/v1/openapi.json",
|
|
105
|
+
"public": true,
|
|
106
|
+
"python_method": "openapi",
|
|
107
|
+
"request_schema": null
|
|
108
|
+
},
|
|
109
|
+
"pauseHybridReembedJob": {
|
|
110
|
+
"method": "POST",
|
|
111
|
+
"node_method": "pauseHybridReembedJob",
|
|
112
|
+
"path": "/v1/admin/hybrid/reembed-jobs/pause",
|
|
113
|
+
"public": false,
|
|
114
|
+
"python_method": "pause_hybrid_reembed_job",
|
|
115
|
+
"request_schema": "#/components/schemas/HybridReembedJobActionRequest"
|
|
116
|
+
},
|
|
117
|
+
"postAdminShutdown": {
|
|
118
|
+
"method": "POST",
|
|
119
|
+
"node_method": "shutdown",
|
|
120
|
+
"path": "/v1/admin/shutdown",
|
|
121
|
+
"public": false,
|
|
122
|
+
"python_method": "shutdown",
|
|
123
|
+
"request_schema": null
|
|
124
|
+
},
|
|
125
|
+
"postAgentCapabilities": {
|
|
126
|
+
"method": "POST",
|
|
127
|
+
"node_method": "listCapabilities",
|
|
128
|
+
"path": "/v1/agent/capabilities",
|
|
129
|
+
"public": false,
|
|
130
|
+
"python_method": "list_capabilities",
|
|
131
|
+
"request_schema": "#/components/schemas/CapabilityDiscoveryRequest"
|
|
132
|
+
},
|
|
133
|
+
"postAgentInvoke": {
|
|
134
|
+
"method": "POST",
|
|
135
|
+
"node_method": "invokeAgentCapability",
|
|
136
|
+
"path": "/v1/agent/invoke",
|
|
137
|
+
"public": false,
|
|
138
|
+
"python_method": "invoke_agent_capability",
|
|
139
|
+
"request_schema": "#/components/schemas/AgentInvokeRequest"
|
|
140
|
+
},
|
|
141
|
+
"postAgentMemory": {
|
|
142
|
+
"method": "POST",
|
|
143
|
+
"node_method": "memoryCommand",
|
|
144
|
+
"path": "/v1/agent/memory",
|
|
145
|
+
"public": false,
|
|
146
|
+
"python_method": "memory_command",
|
|
147
|
+
"request_schema": "#/components/schemas/AgentMemoryRequest"
|
|
148
|
+
},
|
|
149
|
+
"postAgentRunReplay": {
|
|
150
|
+
"method": "POST",
|
|
151
|
+
"node_method": "replayAgentRun",
|
|
152
|
+
"path": "/v1/agent/runs/replay",
|
|
153
|
+
"public": false,
|
|
154
|
+
"python_method": "replay_agent_run",
|
|
155
|
+
"request_schema": "#/components/schemas/AgentRunReplayRequest"
|
|
156
|
+
},
|
|
157
|
+
"postQuery": {
|
|
158
|
+
"method": "POST",
|
|
159
|
+
"node_method": "query",
|
|
160
|
+
"path": "/v1/query",
|
|
161
|
+
"public": false,
|
|
162
|
+
"python_method": "query",
|
|
163
|
+
"request_schema": "#/components/schemas/SqlRequest"
|
|
164
|
+
},
|
|
165
|
+
"postResourceRead": {
|
|
166
|
+
"method": "POST",
|
|
167
|
+
"node_method": "readResource",
|
|
168
|
+
"path": "/v1/resources/read",
|
|
169
|
+
"public": false,
|
|
170
|
+
"python_method": "read_resource",
|
|
171
|
+
"request_schema": "#/components/schemas/ResourceReadRequest"
|
|
172
|
+
},
|
|
173
|
+
"postSql": {
|
|
174
|
+
"method": "POST",
|
|
175
|
+
"node_method": "execute",
|
|
176
|
+
"path": "/v1/sql",
|
|
177
|
+
"public": false,
|
|
178
|
+
"python_method": "execute",
|
|
179
|
+
"request_schema": "#/components/schemas/SqlRequest"
|
|
180
|
+
},
|
|
181
|
+
"previewAgentProposal": {
|
|
182
|
+
"method": "POST",
|
|
183
|
+
"node_method": "previewWrite",
|
|
184
|
+
"path": "/v1/agent/proposals/preview",
|
|
185
|
+
"public": false,
|
|
186
|
+
"python_method": "preview_write",
|
|
187
|
+
"request_schema": "#/components/schemas/ProposalLifecycleRequest"
|
|
188
|
+
},
|
|
189
|
+
"rejectAgentProposal": {
|
|
190
|
+
"method": "POST",
|
|
191
|
+
"node_method": "rejectWrite",
|
|
192
|
+
"path": "/v1/agent/proposals/reject",
|
|
193
|
+
"public": false,
|
|
194
|
+
"python_method": "reject_write",
|
|
195
|
+
"request_schema": "#/components/schemas/ProposalLifecycleRequest"
|
|
196
|
+
},
|
|
197
|
+
"resumeHybridReembedJob": {
|
|
198
|
+
"method": "POST",
|
|
199
|
+
"node_method": "resumeHybridReembedJob",
|
|
200
|
+
"path": "/v1/admin/hybrid/reembed-jobs/resume",
|
|
201
|
+
"public": false,
|
|
202
|
+
"python_method": "resume_hybrid_reembed_job",
|
|
203
|
+
"request_schema": "#/components/schemas/HybridReembedJobActionRequest"
|
|
204
|
+
},
|
|
205
|
+
"runHybridReembedJobs": {
|
|
206
|
+
"method": "POST",
|
|
207
|
+
"node_method": "runHybridReembedJobs",
|
|
208
|
+
"path": "/v1/admin/hybrid/reembed-jobs/run",
|
|
209
|
+
"public": false,
|
|
210
|
+
"python_method": "run_hybrid_reembed_jobs",
|
|
211
|
+
"request_schema": "#/components/schemas/HybridReembedRunRequest"
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
export const PYTHON_OPERATION_METHODS = Object.freeze({
|
|
215
|
+
"approveAgentProposal": "approve_write",
|
|
216
|
+
"cancelAgentProposal": "cancel_write",
|
|
217
|
+
"cancelHybridReembedJob": "cancel_hybrid_reembed_job",
|
|
218
|
+
"commitAgentProposal": "commit_write",
|
|
219
|
+
"createAdminBackup": "create_admin_backup",
|
|
220
|
+
"getAdminCheck": "admin_check",
|
|
221
|
+
"getAdminDiagnostics": "admin_diagnostics",
|
|
222
|
+
"getAdminMetrics": "admin_metrics",
|
|
223
|
+
"getAdminOtelTraces": "admin_otel_traces",
|
|
224
|
+
"getAdminPrometheusMetrics": "admin_prometheus_metrics",
|
|
225
|
+
"getHealth": "health",
|
|
226
|
+
"getHybridReembedJobs": "hybrid_reembed_jobs",
|
|
227
|
+
"getOpenApi": "openapi",
|
|
228
|
+
"pauseHybridReembedJob": "pause_hybrid_reembed_job",
|
|
229
|
+
"postAdminShutdown": "shutdown",
|
|
230
|
+
"postAgentCapabilities": "list_capabilities",
|
|
231
|
+
"postAgentInvoke": "invoke_agent_capability",
|
|
232
|
+
"postAgentMemory": "memory_command",
|
|
233
|
+
"postAgentRunReplay": "replay_agent_run",
|
|
234
|
+
"postQuery": "query",
|
|
235
|
+
"postResourceRead": "read_resource",
|
|
236
|
+
"postSql": "execute",
|
|
237
|
+
"previewAgentProposal": "preview_write",
|
|
238
|
+
"rejectAgentProposal": "reject_write",
|
|
239
|
+
"resumeHybridReembedJob": "resume_hybrid_reembed_job",
|
|
240
|
+
"runHybridReembedJobs": "run_hybrid_reembed_jobs"
|
|
241
|
+
});
|
|
242
|
+
export const NODE_OPERATION_METHODS = Object.freeze({
|
|
243
|
+
"approveAgentProposal": "approveWrite",
|
|
244
|
+
"cancelAgentProposal": "cancelWrite",
|
|
245
|
+
"cancelHybridReembedJob": "cancelHybridReembedJob",
|
|
246
|
+
"commitAgentProposal": "commitWrite",
|
|
247
|
+
"createAdminBackup": "createAdminBackup",
|
|
248
|
+
"getAdminCheck": "adminCheck",
|
|
249
|
+
"getAdminDiagnostics": "adminDiagnostics",
|
|
250
|
+
"getAdminMetrics": "adminMetrics",
|
|
251
|
+
"getAdminOtelTraces": "adminOtelTraces",
|
|
252
|
+
"getAdminPrometheusMetrics": "adminPrometheusMetrics",
|
|
253
|
+
"getHealth": "health",
|
|
254
|
+
"getHybridReembedJobs": "hybridReembedJobs",
|
|
255
|
+
"getOpenApi": "openapi",
|
|
256
|
+
"pauseHybridReembedJob": "pauseHybridReembedJob",
|
|
257
|
+
"postAdminShutdown": "shutdown",
|
|
258
|
+
"postAgentCapabilities": "listCapabilities",
|
|
259
|
+
"postAgentInvoke": "invokeAgentCapability",
|
|
260
|
+
"postAgentMemory": "memoryCommand",
|
|
261
|
+
"postAgentRunReplay": "replayAgentRun",
|
|
262
|
+
"postQuery": "query",
|
|
263
|
+
"postResourceRead": "readResource",
|
|
264
|
+
"postSql": "execute",
|
|
265
|
+
"previewAgentProposal": "previewWrite",
|
|
266
|
+
"rejectAgentProposal": "rejectWrite",
|
|
267
|
+
"resumeHybridReembedJob": "resumeHybridReembedJob",
|
|
268
|
+
"runHybridReembedJobs": "runHybridReembedJobs"
|
|
269
|
+
});
|
|
270
|
+
export const PUBLIC_OPERATIONS = Object.freeze([
|
|
271
|
+
"getHealth",
|
|
272
|
+
"getOpenApi"
|
|
273
|
+
]);
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@synapsor/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node.js client for Synapsor HTTP/JSON APIs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./synapsor.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./synapsor.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"synapsor.mjs",
|
|
12
|
+
"generated-contract.mjs",
|
|
13
|
+
"README.md",
|
|
14
|
+
"package.json"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "node --check synapsor.mjs && node --check generated-contract.mjs",
|
|
18
|
+
"test": "node --test test/*.test.mjs"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"synapsor",
|
|
22
|
+
"database",
|
|
23
|
+
"agents",
|
|
24
|
+
"sql",
|
|
25
|
+
"rag",
|
|
26
|
+
"audit"
|
|
27
|
+
],
|
|
28
|
+
"homepage": "https://synapsor.ai",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/sandeshtiwari/Synapsor.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://synapsor.ai/contact"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|
package/synapsor.mjs
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
CONTRACT_SHA256 as OPENAPI_CONTRACT_SHA256,
|
|
9
|
+
NODE_OPERATION_METHODS,
|
|
10
|
+
OPENAPI_OPERATIONS,
|
|
11
|
+
PROTOCOL_VERSION,
|
|
12
|
+
} from "./generated-contract.mjs";
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
NODE_OPERATION_METHODS,
|
|
16
|
+
OPENAPI_CONTRACT_SHA256,
|
|
17
|
+
OPENAPI_OPERATIONS,
|
|
18
|
+
PROTOCOL_VERSION,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class SynapsorError extends Error {
|
|
22
|
+
constructor(message, { status = undefined, payload = undefined } = {}) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "SynapsorError";
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.payload = payload;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class SynapsorBadRequestError extends SynapsorError {}
|
|
31
|
+
export class SynapsorUnauthenticatedError extends SynapsorError {}
|
|
32
|
+
export class SynapsorPermissionDeniedError extends SynapsorError {}
|
|
33
|
+
export class SynapsorNotFoundError extends SynapsorError {}
|
|
34
|
+
export class SynapsorConflictError extends SynapsorError {}
|
|
35
|
+
export class SynapsorTimeoutError extends SynapsorError {}
|
|
36
|
+
export class SynapsorResourceExhaustedError extends SynapsorError {}
|
|
37
|
+
export class SynapsorUnavailableError extends SynapsorError {}
|
|
38
|
+
export class SynapsorInternalError extends SynapsorError {}
|
|
39
|
+
|
|
40
|
+
const ERROR_BY_STATUS = new Map([
|
|
41
|
+
[400, SynapsorBadRequestError],
|
|
42
|
+
[401, SynapsorUnauthenticatedError],
|
|
43
|
+
[403, SynapsorPermissionDeniedError],
|
|
44
|
+
[404, SynapsorNotFoundError],
|
|
45
|
+
[408, SynapsorTimeoutError],
|
|
46
|
+
[409, SynapsorConflictError],
|
|
47
|
+
[413, SynapsorResourceExhaustedError],
|
|
48
|
+
[500, SynapsorInternalError],
|
|
49
|
+
[503, SynapsorUnavailableError],
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
export async function connect(target, options = {}) {
|
|
53
|
+
return Synapsor.connect(target, options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class Synapsor {
|
|
57
|
+
static openApiContractSha256 = OPENAPI_CONTRACT_SHA256;
|
|
58
|
+
static openApiOperations = OPENAPI_OPERATIONS;
|
|
59
|
+
static sdkOperationMethods = NODE_OPERATION_METHODS;
|
|
60
|
+
|
|
61
|
+
constructor({ baseUrl, session = {}, apiKey = undefined, process = undefined, timeoutMs = 5000 }) {
|
|
62
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
63
|
+
this.session = { ...session };
|
|
64
|
+
this.apiKey = apiKey;
|
|
65
|
+
this.process = process;
|
|
66
|
+
this.timeoutMs = timeoutMs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static async connect(target, options = {}) {
|
|
70
|
+
const targetText = String(target);
|
|
71
|
+
if (targetText.startsWith("http://") || targetText.startsWith("https://")) {
|
|
72
|
+
if (options.autoStart) {
|
|
73
|
+
throw new Error("autoStart cannot be used with an HTTP Synapsor URL");
|
|
74
|
+
}
|
|
75
|
+
return new Synapsor({
|
|
76
|
+
baseUrl: targetText,
|
|
77
|
+
session: options.session,
|
|
78
|
+
apiKey: options.apiKey,
|
|
79
|
+
timeoutMs: options.timeoutMs,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!options.autoStart) {
|
|
84
|
+
throw new Error("local database paths require autoStart: true for the HTTP adapter");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const host = options.host ?? "127.0.0.1";
|
|
88
|
+
const port = options.port ?? (await findFreePort(host));
|
|
89
|
+
const binary = options.serverBinary ?? defaultServerBinary();
|
|
90
|
+
if (!existsSync(binary)) {
|
|
91
|
+
throw new Error(`synapsor_server binary not found: ${binary}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const args = ["--db", targetText, "--host", host, "--port", String(port)];
|
|
95
|
+
if (Array.isArray(options.extraServerArgs)) {
|
|
96
|
+
args.push(...options.extraServerArgs.map(String));
|
|
97
|
+
}
|
|
98
|
+
const child = spawn(binary, args, {
|
|
99
|
+
cwd: repoRoot(),
|
|
100
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
101
|
+
});
|
|
102
|
+
const stdout = [];
|
|
103
|
+
const stderr = [];
|
|
104
|
+
child.stdout.on("data", (chunk) => stdout.push(String(chunk)));
|
|
105
|
+
child.stderr.on("data", (chunk) => stderr.push(String(chunk)));
|
|
106
|
+
|
|
107
|
+
const client = new Synapsor({
|
|
108
|
+
baseUrl: `http://${host}:${port}`,
|
|
109
|
+
session: options.session,
|
|
110
|
+
apiKey: options.apiKey,
|
|
111
|
+
process: child,
|
|
112
|
+
timeoutMs: options.timeoutMs,
|
|
113
|
+
});
|
|
114
|
+
await client.waitUntilReady(() => stderr.join("") || stdout.join(""));
|
|
115
|
+
return client;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async close() {
|
|
119
|
+
if (!this.process) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await this.shutdown();
|
|
124
|
+
} catch {
|
|
125
|
+
// Local close must still terminate the process if shutdown returned an error.
|
|
126
|
+
}
|
|
127
|
+
if (this.process.exitCode === null && this.process.signalCode === null) {
|
|
128
|
+
this.process.kill("SIGTERM");
|
|
129
|
+
await onceExitOrTimeout(this.process, 2000);
|
|
130
|
+
if (this.process.exitCode === null && this.process.signalCode === null) {
|
|
131
|
+
this.process.kill("SIGKILL");
|
|
132
|
+
await onceExitOrTimeout(this.process, 2000);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
this.process = undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setSession(session) {
|
|
139
|
+
this.session = { ...this.session, ...session };
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async health() {
|
|
144
|
+
return this.request("GET", "/v1/health");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async openapi() {
|
|
148
|
+
return this.request("GET", "/v1/openapi.json");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async adminMetrics() {
|
|
152
|
+
return this.request("GET", "/v1/admin/metrics");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async adminPrometheusMetrics() {
|
|
156
|
+
return this.request("GET", "/v1/admin/metrics/prometheus");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async adminDiagnostics() {
|
|
160
|
+
return this.request("GET", "/v1/admin/diagnostics");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async adminOtelTraces() {
|
|
164
|
+
return this.request("GET", "/v1/admin/otel/traces");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async adminCheck() {
|
|
168
|
+
return this.request("GET", "/v1/admin/check");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async createAdminBackup(backupDir, { overwrite = false, verify = false } = {}) {
|
|
172
|
+
return this.request("POST", "/v1/admin/backups/create", {
|
|
173
|
+
backup_dir: String(backupDir),
|
|
174
|
+
overwrite: Boolean(overwrite),
|
|
175
|
+
verify: Boolean(verify),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async shutdown() {
|
|
180
|
+
return this.request("POST", "/v1/admin/shutdown", {});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async hybridReembedJobs() {
|
|
184
|
+
return this.request("GET", "/v1/admin/hybrid/reembed-jobs");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async runHybridReembedJobs(options = {}) {
|
|
188
|
+
const payload = {};
|
|
189
|
+
if (options.index !== undefined) {
|
|
190
|
+
payload.index = options.index;
|
|
191
|
+
}
|
|
192
|
+
if (options.indexName !== undefined) {
|
|
193
|
+
payload.index_name = options.indexName;
|
|
194
|
+
}
|
|
195
|
+
if (options.maxIndexes !== undefined) {
|
|
196
|
+
payload.max_indexes = Number(options.maxIndexes);
|
|
197
|
+
}
|
|
198
|
+
if (options.max_chunks_per_index !== undefined) {
|
|
199
|
+
payload.max_chunks_per_index = Number(options.max_chunks_per_index);
|
|
200
|
+
} else if (options.maxChunksPerIndex !== undefined) {
|
|
201
|
+
payload.max_chunks_per_index = Number(options.maxChunksPerIndex);
|
|
202
|
+
}
|
|
203
|
+
return this.request("POST", "/v1/admin/hybrid/reembed-jobs/run", payload);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async pauseHybridReembedJob(index) {
|
|
207
|
+
return this.request("POST", "/v1/admin/hybrid/reembed-jobs/pause", { index });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async resumeHybridReembedJob(index) {
|
|
211
|
+
return this.request("POST", "/v1/admin/hybrid/reembed-jobs/resume", { index });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async cancelHybridReembedJob(index) {
|
|
215
|
+
return this.request("POST", "/v1/admin/hybrid/reembed-jobs/cancel", { index });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async execute(sql, { session = undefined, asOf = undefined } = {}) {
|
|
219
|
+
const payload = { sql };
|
|
220
|
+
const requestSession = session ?? this.session;
|
|
221
|
+
if (Object.keys(requestSession).length > 0) {
|
|
222
|
+
payload.session = requestSession;
|
|
223
|
+
}
|
|
224
|
+
if (asOf !== undefined) {
|
|
225
|
+
payload.as_of = asOf;
|
|
226
|
+
}
|
|
227
|
+
return this.request("POST", asOf !== undefined ? "/v1/query" : "/v1/sql", payload);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async query(sql, options = {}) {
|
|
231
|
+
const response = await this.execute(sql, options);
|
|
232
|
+
const results = response.results ?? [];
|
|
233
|
+
if (results.length === 0) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
return results.at(-1).result?.rows ?? [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async invokeAgentCapability(capability, args = {}, { session = undefined, traceId = undefined } = {}) {
|
|
240
|
+
const payload = {
|
|
241
|
+
capability,
|
|
242
|
+
arguments: args,
|
|
243
|
+
session: this.requireSession(session),
|
|
244
|
+
};
|
|
245
|
+
if (traceId !== undefined) {
|
|
246
|
+
payload.trace_id = traceId;
|
|
247
|
+
}
|
|
248
|
+
return this.request("POST", "/v1/agent/invoke", payload);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async listCapabilities(query = "", { session = undefined } = {}) {
|
|
252
|
+
return this.request("POST", "/v1/agent/capabilities", {
|
|
253
|
+
query,
|
|
254
|
+
session: this.requireSession(session),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async memoryCommand(command, { session = undefined } = {}) {
|
|
259
|
+
return this.request("POST", "/v1/agent/memory", {
|
|
260
|
+
session: this.requireSession(session),
|
|
261
|
+
command,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async rememberFact(options) {
|
|
266
|
+
return this.memoryCommand(factCommand("agent_memory.remember_fact", options), options);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async proposeMemoryFact(options) {
|
|
270
|
+
const command = factCommand("agent_memory.propose_fact", options);
|
|
271
|
+
command.replace_subject = Boolean(options.replaceSubject ?? options.replace_subject ?? false);
|
|
272
|
+
if (options.reason !== undefined) {
|
|
273
|
+
command.reason = options.reason;
|
|
274
|
+
}
|
|
275
|
+
return this.memoryCommand(command, options);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async approveMemoryProposal(proposalId, reason, options = {}) {
|
|
279
|
+
return this.memoryCommand({
|
|
280
|
+
kind: "agent_memory.approve_memory_proposal",
|
|
281
|
+
proposal_id: proposalId,
|
|
282
|
+
reason,
|
|
283
|
+
}, options);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async rejectMemoryProposal(proposalId, reason, options = {}) {
|
|
287
|
+
return this.memoryCommand({
|
|
288
|
+
kind: "agent_memory.reject_memory_proposal",
|
|
289
|
+
proposal_id: proposalId,
|
|
290
|
+
reason,
|
|
291
|
+
}, options);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async listMemoryProposals(options = {}) {
|
|
295
|
+
const command = { kind: "agent_memory.proposals" };
|
|
296
|
+
if (options.state !== undefined) {
|
|
297
|
+
command.state = options.state;
|
|
298
|
+
}
|
|
299
|
+
if (options.scope !== undefined) {
|
|
300
|
+
command.scope = typedRef(options.scope);
|
|
301
|
+
}
|
|
302
|
+
if (options.subject !== undefined) {
|
|
303
|
+
command.subject = typedRef(options.subject);
|
|
304
|
+
}
|
|
305
|
+
return this.memoryCommand(command, options);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async recallMemory(options = {}) {
|
|
309
|
+
const command = { kind: "agent_memory.recall" };
|
|
310
|
+
if (options.scope !== undefined) {
|
|
311
|
+
command.scope = typedRef(options.scope);
|
|
312
|
+
}
|
|
313
|
+
if (options.subject !== undefined) {
|
|
314
|
+
command.subject = typedRef(options.subject);
|
|
315
|
+
}
|
|
316
|
+
for (const [from, to] of [
|
|
317
|
+
["asOf", "as_of"],
|
|
318
|
+
["budgetTokens", "budget_tokens"],
|
|
319
|
+
["profile", "profile"],
|
|
320
|
+
["inlineEvidence", "inline_evidence"],
|
|
321
|
+
["preferHandles", "prefer_handles"],
|
|
322
|
+
["maxReasonCount", "max_reason_count"],
|
|
323
|
+
]) {
|
|
324
|
+
if (options[from] !== undefined) {
|
|
325
|
+
command[to] = options[from];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return this.memoryCommand(command, options);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async retireFact(memoryId, { validTo, reason, session = undefined }) {
|
|
332
|
+
return this.memoryCommand({
|
|
333
|
+
kind: "agent_memory.retire_fact",
|
|
334
|
+
memory_id: memoryId,
|
|
335
|
+
valid_to: validTo,
|
|
336
|
+
reason,
|
|
337
|
+
}, { session });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async forgetFact(memoryId, reason, options = {}) {
|
|
341
|
+
return this.memoryCommand({
|
|
342
|
+
kind: "agent_memory.forget_fact",
|
|
343
|
+
memory_id: memoryId,
|
|
344
|
+
reason,
|
|
345
|
+
}, options);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async checkFactForAction({ claim, scope = undefined, subject = undefined, asOf = undefined, action = "policy", session = undefined }) {
|
|
349
|
+
const args = [`claim => ${sqlLiteral(claim)}`, `action => ${sqlLiteral(action)}`];
|
|
350
|
+
if (scope !== undefined) {
|
|
351
|
+
args.push(`scope => ${typedSql(scope)}`);
|
|
352
|
+
}
|
|
353
|
+
if (subject !== undefined) {
|
|
354
|
+
args.push(`subject => ${typedSql(subject)}`);
|
|
355
|
+
}
|
|
356
|
+
if (asOf !== undefined) {
|
|
357
|
+
args.push(`as_of => ${Number(asOf)}`);
|
|
358
|
+
}
|
|
359
|
+
const response = await this.execute(`CALL memory.check_fact_for_action(${args.join(", ")});`, { session });
|
|
360
|
+
return response.results.at(-1).result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async createBranch(name, parent = "main") {
|
|
364
|
+
return this.execute(`CREATE BRANCH ${identifier(name)} FROM ${identifier(parent)};`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async useBranch(name) {
|
|
368
|
+
const result = await this.execute(`USE BRANCH ${identifier(name)};`);
|
|
369
|
+
this.session.branch_id = name;
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async diffBranch(source, target = "main") {
|
|
374
|
+
const response = await this.execute(`DIFF BRANCH ${identifier(source)} AGAINST ${identifier(target)};`);
|
|
375
|
+
return response.results.at(-1).result;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async mergeBranch(source, target = "main") {
|
|
379
|
+
return this.execute(`MERGE BRANCH ${identifier(source)} INTO ${identifier(target)};`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async dropBranch(name) {
|
|
383
|
+
const result = await this.execute(`DROP BRANCH ${identifier(name)};`);
|
|
384
|
+
if (this.session.branch_id === name) {
|
|
385
|
+
this.session.branch_id = "main";
|
|
386
|
+
}
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async readResource(uri, options = {}) {
|
|
391
|
+
return this.request("POST", "/v1/resources/read", {
|
|
392
|
+
uri,
|
|
393
|
+
session: this.requireSession(options.session),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async previewWrite(proposal, options = {}) {
|
|
398
|
+
return this.proposalLifecycle("preview", proposal, options);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async approveWrite(proposal, options = {}) {
|
|
402
|
+
return this.proposalLifecycle("approve", proposal, options);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async commitWrite(proposal, options = {}) {
|
|
406
|
+
return this.proposalLifecycle("commit", proposal, options);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async rejectWrite(proposal, options = {}) {
|
|
410
|
+
return this.proposalLifecycle("reject", proposal, options);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async cancelWrite(proposal, options = {}) {
|
|
414
|
+
return this.proposalLifecycle("cancel", proposal, options);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async replayAgentRun(runId, options = {}) {
|
|
418
|
+
return this.request("POST", "/v1/agent/runs/replay", {
|
|
419
|
+
run_id: Number(runId),
|
|
420
|
+
session: this.requireSession(options.session),
|
|
421
|
+
include_sensitive_memory: Boolean(options.includeSensitiveMemory ?? options.include_sensitive_memory ?? false),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async proposalLifecycle(action, proposal, { session = undefined, promoteBranch = undefined, promote_branch = undefined, targetBranch = undefined, target_branch = undefined } = {}) {
|
|
426
|
+
const payload = {
|
|
427
|
+
proposal,
|
|
428
|
+
session: this.requireSession(session),
|
|
429
|
+
};
|
|
430
|
+
const shouldPromote = promoteBranch ?? promote_branch;
|
|
431
|
+
const target = targetBranch ?? target_branch;
|
|
432
|
+
if (shouldPromote !== undefined) {
|
|
433
|
+
payload.promote_branch = Boolean(shouldPromote);
|
|
434
|
+
}
|
|
435
|
+
if (target !== undefined) {
|
|
436
|
+
payload.target_branch = target;
|
|
437
|
+
}
|
|
438
|
+
return this.request("POST", `/v1/agent/proposals/${action}`, payload);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
requireSession(session = undefined) {
|
|
442
|
+
const requestSession = session ?? this.session;
|
|
443
|
+
if (!requestSession || Object.keys(requestSession).length === 0) {
|
|
444
|
+
throw new Error("agent APIs require a session with principal and tenant_id");
|
|
445
|
+
}
|
|
446
|
+
if (requestSession.principal === undefined || requestSession.tenant_id === undefined) {
|
|
447
|
+
throw new Error("agent session requires principal and tenant_id");
|
|
448
|
+
}
|
|
449
|
+
return requestSession;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async request(method, path, payload = undefined) {
|
|
453
|
+
const headers = { accept: "application/json" };
|
|
454
|
+
const init = { method, headers };
|
|
455
|
+
if (payload !== undefined) {
|
|
456
|
+
headers["content-type"] = "application/json";
|
|
457
|
+
init.body = JSON.stringify(payload);
|
|
458
|
+
}
|
|
459
|
+
if (this.apiKey) {
|
|
460
|
+
headers.authorization = `Bearer ${this.apiKey}`;
|
|
461
|
+
}
|
|
462
|
+
const response = await fetch(this.baseUrl + path, init);
|
|
463
|
+
const text = await response.text();
|
|
464
|
+
let body = {};
|
|
465
|
+
if (text.length > 0) {
|
|
466
|
+
try {
|
|
467
|
+
body = JSON.parse(text);
|
|
468
|
+
} catch {
|
|
469
|
+
body = { error: text };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (!response.ok) {
|
|
473
|
+
const ErrorType = ERROR_BY_STATUS.get(response.status) ?? SynapsorError;
|
|
474
|
+
throw new ErrorType(body.error ?? `Synapsor HTTP ${response.status}`, {
|
|
475
|
+
status: response.status,
|
|
476
|
+
payload: body,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return body;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async waitUntilReady(errorText) {
|
|
483
|
+
const deadline = Date.now() + this.timeoutMs;
|
|
484
|
+
let lastError;
|
|
485
|
+
while (Date.now() < deadline) {
|
|
486
|
+
if (this.process && this.process.exitCode !== null) {
|
|
487
|
+
throw new SynapsorError(`synapsor_server exited early: ${errorText()}`);
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
await this.health();
|
|
491
|
+
return;
|
|
492
|
+
} catch (err) {
|
|
493
|
+
lastError = err;
|
|
494
|
+
await sleep(50);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
throw new SynapsorError(`synapsor_server did not become ready: ${lastError}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function factCommand(kind, options) {
|
|
502
|
+
const command = {
|
|
503
|
+
kind,
|
|
504
|
+
claim: options.claim,
|
|
505
|
+
source: typedRef(options.source),
|
|
506
|
+
trust: options.trust ?? "unverified",
|
|
507
|
+
approval: options.approval ?? "pending",
|
|
508
|
+
};
|
|
509
|
+
if (options.scope !== undefined) {
|
|
510
|
+
command.scope = typedRef(options.scope);
|
|
511
|
+
}
|
|
512
|
+
if (options.subject !== undefined) {
|
|
513
|
+
command.subject = typedRef(options.subject);
|
|
514
|
+
}
|
|
515
|
+
if (options.validFrom !== undefined) {
|
|
516
|
+
command.valid_from = options.validFrom;
|
|
517
|
+
}
|
|
518
|
+
if (options.valid_from !== undefined) {
|
|
519
|
+
command.valid_from = options.valid_from;
|
|
520
|
+
}
|
|
521
|
+
if (options.validTo !== undefined) {
|
|
522
|
+
command.valid_to = options.validTo;
|
|
523
|
+
}
|
|
524
|
+
if (options.valid_to !== undefined) {
|
|
525
|
+
command.valid_to = options.valid_to;
|
|
526
|
+
}
|
|
527
|
+
return command;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function typedRef(value) {
|
|
531
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
532
|
+
return { type: String(value[0]), id: String(value[1]) };
|
|
533
|
+
}
|
|
534
|
+
if (value && typeof value === "object" && typeof value.type === "string") {
|
|
535
|
+
return { ...value };
|
|
536
|
+
}
|
|
537
|
+
throw new TypeError("typed reference must be [type, id] or {type, id}");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function typedSql(value) {
|
|
541
|
+
const ref = typedRef(value);
|
|
542
|
+
return `${identifier(ref.type)}(${sqlLiteral(String(ref.id ?? ""))})`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function sqlLiteral(value) {
|
|
546
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function identifier(value) {
|
|
550
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
551
|
+
throw new Error(`unsafe Synapsor identifier: ${value}`);
|
|
552
|
+
}
|
|
553
|
+
return value;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function repoRoot() {
|
|
557
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function defaultServerBinary() {
|
|
561
|
+
if (process.env.SYNAPSOR_SERVER) {
|
|
562
|
+
return process.env.SYNAPSOR_SERVER;
|
|
563
|
+
}
|
|
564
|
+
const root = repoRoot();
|
|
565
|
+
for (const candidate of [
|
|
566
|
+
join(root, "build", "synapsor_server"),
|
|
567
|
+
join(root, "build", "debug", "synapsor_server"),
|
|
568
|
+
join(root, "build", "bin", "synapsor_server"),
|
|
569
|
+
]) {
|
|
570
|
+
if (existsSync(candidate)) {
|
|
571
|
+
return candidate;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return join(root, "build", "synapsor_server");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function findFreePort(host) {
|
|
578
|
+
return new Promise((resolvePort, reject) => {
|
|
579
|
+
const server = net.createServer();
|
|
580
|
+
server.on("error", reject);
|
|
581
|
+
server.listen(0, host, () => {
|
|
582
|
+
const address = server.address();
|
|
583
|
+
const port = typeof address === "object" && address ? address.port : undefined;
|
|
584
|
+
server.close(() => {
|
|
585
|
+
if (port === undefined) {
|
|
586
|
+
reject(new Error("could not allocate a free port"));
|
|
587
|
+
} else {
|
|
588
|
+
resolvePort(port);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function onceExitOrTimeout(child, timeoutMs) {
|
|
596
|
+
return new Promise((resolveDone) => {
|
|
597
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
598
|
+
resolveDone();
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const timer = setTimeout(resolveDone, timeoutMs);
|
|
602
|
+
child.once("exit", () => {
|
|
603
|
+
clearTimeout(timer);
|
|
604
|
+
resolveDone();
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function sleep(ms) {
|
|
610
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
611
|
+
}
|