@maximem/synap-js-sdk 0.1.1
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/.env.example +47 -0
- package/README.md +266 -0
- package/bin/synap-js-sdk.js +124 -0
- package/bridge/synap_bridge.py +493 -0
- package/package.json +38 -0
- package/src/bridge-manager.js +262 -0
- package/src/index.js +11 -0
- package/src/runtime.js +161 -0
- package/src/setup-typescript.js +141 -0
- package/src/synap-client.js +83 -0
- package/types/index.d.ts +170 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
const { spawn, spawnSync } = require('child_process');
|
|
2
|
+
const readline = require('readline');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const {
|
|
5
|
+
resolveBridgeScriptPath,
|
|
6
|
+
resolvePythonBin,
|
|
7
|
+
resolveInstanceId,
|
|
8
|
+
setupPythonRuntime,
|
|
9
|
+
} = require('./runtime');
|
|
10
|
+
|
|
11
|
+
class BridgeManager {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = {
|
|
14
|
+
requestTimeoutMs: 30_000,
|
|
15
|
+
initTimeoutMs: 45_000,
|
|
16
|
+
ingestTimeoutMs: 120_000,
|
|
17
|
+
autoSetup: false,
|
|
18
|
+
...options,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
this.child = null;
|
|
22
|
+
this.stdoutReader = null;
|
|
23
|
+
this.stderrReader = null;
|
|
24
|
+
this.pending = new Map();
|
|
25
|
+
this.nextId = 1;
|
|
26
|
+
this.initPromise = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async ensureStarted() {
|
|
30
|
+
if (this.initPromise) return this.initPromise;
|
|
31
|
+
|
|
32
|
+
this.initPromise = (async () => {
|
|
33
|
+
const bridgeScript = resolveBridgeScriptPath(this.options.bridgeScriptPath);
|
|
34
|
+
const pythonBin = await this.#resolvePythonBinary();
|
|
35
|
+
this.#preflightPythonImport(pythonBin);
|
|
36
|
+
|
|
37
|
+
if (!fs.existsSync(bridgeScript)) {
|
|
38
|
+
throw new Error(`Bridge script not found at ${bridgeScript}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const bridgeEnv = {
|
|
42
|
+
...process.env,
|
|
43
|
+
PYTHONUNBUFFERED: '1',
|
|
44
|
+
...(this.options.pythonEnv || {}),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this.child = spawn(pythonBin, [bridgeScript], {
|
|
48
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
49
|
+
env: bridgeEnv,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
this.#attachReaders();
|
|
53
|
+
this.#attachLifecycle();
|
|
54
|
+
|
|
55
|
+
const instanceId = resolveInstanceId(this.options.instanceId);
|
|
56
|
+
|
|
57
|
+
await this.sendCommand(
|
|
58
|
+
'init',
|
|
59
|
+
{
|
|
60
|
+
instance_id: instanceId,
|
|
61
|
+
bootstrap_token: this.options.bootstrapToken
|
|
62
|
+
|| process.env.SYNAP_BOOTSTRAP_TOKEN
|
|
63
|
+
|| process.env.SYNAP_BOOTSTRAP_KEY,
|
|
64
|
+
base_url: this.options.baseUrl || process.env.SYNAP_BASE_URL,
|
|
65
|
+
grpc_host: this.options.grpcHost || process.env.SYNAP_GRPC_HOST,
|
|
66
|
+
grpc_port: Number(this.options.grpcPort || process.env.SYNAP_GRPC_PORT || 50051),
|
|
67
|
+
grpc_use_tls: this.options.grpcUseTls ?? (process.env.SYNAP_GRPC_TLS === 'true'),
|
|
68
|
+
},
|
|
69
|
+
this.options.initTimeoutMs
|
|
70
|
+
);
|
|
71
|
+
})()
|
|
72
|
+
.catch((error) => {
|
|
73
|
+
this.initPromise = null;
|
|
74
|
+
throw error;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return this.initPromise;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async sendCommand(method, params = {}, timeoutMs = this.options.requestTimeoutMs) {
|
|
81
|
+
if (!this.child || !this.child.stdin || !this.child.stdin.writable) {
|
|
82
|
+
throw new Error('Synap bridge process is not running');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const id = this.nextId++;
|
|
86
|
+
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
this.pending.delete(id);
|
|
90
|
+
reject(new Error(`Bridge command '${method}' timed out after ${timeoutMs}ms`));
|
|
91
|
+
}, timeoutMs);
|
|
92
|
+
|
|
93
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
this.child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
this.pending.delete(id);
|
|
100
|
+
reject(error);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async call(method, params = {}, timeoutMs) {
|
|
106
|
+
await this.ensureStarted();
|
|
107
|
+
return this.sendCommand(method, params, timeoutMs);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async shutdown() {
|
|
111
|
+
if (!this.child) return;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await this.sendCommand('shutdown', {}, 10_000);
|
|
115
|
+
} catch (_) {
|
|
116
|
+
// Ignore shutdown RPC errors and continue with forced process stop.
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const proc = this.child;
|
|
120
|
+
this.child = null;
|
|
121
|
+
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
if (proc && !proc.killed) proc.kill();
|
|
124
|
+
}, 1_500);
|
|
125
|
+
|
|
126
|
+
this.#resetState();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#attachReaders() {
|
|
130
|
+
this.stdoutReader = readline.createInterface({ input: this.child.stdout });
|
|
131
|
+
this.stderrReader = readline.createInterface({ input: this.child.stderr });
|
|
132
|
+
|
|
133
|
+
this.stdoutReader.on('line', (line) => {
|
|
134
|
+
let payload;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
payload = JSON.parse(line);
|
|
138
|
+
} catch (_) {
|
|
139
|
+
if (this.options.onLog) this.options.onLog('error', `Bad bridge JSON: ${line}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const pending = this.pending.get(payload.id);
|
|
144
|
+
if (!pending) return;
|
|
145
|
+
|
|
146
|
+
clearTimeout(pending.timer);
|
|
147
|
+
this.pending.delete(payload.id);
|
|
148
|
+
|
|
149
|
+
if (payload.error) {
|
|
150
|
+
pending.reject(new Error(payload.error));
|
|
151
|
+
} else {
|
|
152
|
+
pending.resolve(payload.result);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
this.stderrReader.on('line', (line) => {
|
|
157
|
+
if (this.options.onLog) this.options.onLog('debug', line);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#attachLifecycle() {
|
|
162
|
+
this.child.on('exit', (code) => {
|
|
163
|
+
const err = new Error(`Synap bridge exited with code ${code}`);
|
|
164
|
+
for (const [, pending] of this.pending) {
|
|
165
|
+
clearTimeout(pending.timer);
|
|
166
|
+
pending.reject(err);
|
|
167
|
+
}
|
|
168
|
+
this.#resetState();
|
|
169
|
+
if (this.options.onLog) this.options.onLog('error', err.message);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
this.child.on('error', (error) => {
|
|
173
|
+
for (const [, pending] of this.pending) {
|
|
174
|
+
clearTimeout(pending.timer);
|
|
175
|
+
pending.reject(error);
|
|
176
|
+
}
|
|
177
|
+
this.#resetState();
|
|
178
|
+
if (this.options.onLog) this.options.onLog('error', error.message);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#resetState() {
|
|
183
|
+
this.pending.clear();
|
|
184
|
+
this.initPromise = null;
|
|
185
|
+
|
|
186
|
+
if (this.stdoutReader) {
|
|
187
|
+
this.stdoutReader.close();
|
|
188
|
+
this.stdoutReader = null;
|
|
189
|
+
}
|
|
190
|
+
if (this.stderrReader) {
|
|
191
|
+
this.stderrReader.close();
|
|
192
|
+
this.stderrReader = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async #resolvePythonBinary() {
|
|
197
|
+
const candidates = resolvePythonBin(this.options);
|
|
198
|
+
|
|
199
|
+
for (const candidate of candidates) {
|
|
200
|
+
if (!candidate) continue;
|
|
201
|
+
if (candidate.includes('/') || candidate.includes('\\')) {
|
|
202
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Command-style candidate (python3/python).
|
|
207
|
+
return candidate;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (this.options.autoSetup) {
|
|
211
|
+
const setupResult = await setupPythonRuntime({
|
|
212
|
+
sdkHome: this.options.sdkHome,
|
|
213
|
+
venvPath: this.options.venvPath,
|
|
214
|
+
pythonBootstrap: this.options.pythonBootstrap,
|
|
215
|
+
pythonPackage: this.options.pythonPackage,
|
|
216
|
+
pythonSdkVersion: this.options.pythonSdkVersion,
|
|
217
|
+
noDeps: this.options.noDeps,
|
|
218
|
+
noBuildIsolation: this.options.noBuildIsolation,
|
|
219
|
+
upgrade: this.options.upgrade,
|
|
220
|
+
forceRecreateVenv: this.options.forceRecreateVenv,
|
|
221
|
+
});
|
|
222
|
+
return setupResult.pythonBin;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
throw new Error(
|
|
226
|
+
'No usable Python runtime found. Run `npx synap-js-sdk setup` or pass `pythonBin` in SynapClient options.'
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#preflightPythonImport(pythonBin) {
|
|
231
|
+
const envBase = {
|
|
232
|
+
...process.env,
|
|
233
|
+
...(this.options.pythonEnv || {}),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const checkImport = (envExtra = {}) => {
|
|
237
|
+
const env = { ...envBase, ...envExtra };
|
|
238
|
+
return spawnSync(
|
|
239
|
+
pythonBin,
|
|
240
|
+
['-c', 'import maximem_synap,sys; print(maximem_synap.__file__)'],
|
|
241
|
+
{ env, encoding: 'utf8' }
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const noPath = checkImport();
|
|
246
|
+
if (noPath.status !== 0) {
|
|
247
|
+
const details = [
|
|
248
|
+
`Python SDK import failed.`,
|
|
249
|
+
`pythonBin=${pythonBin}`,
|
|
250
|
+
`error=${(noPath.stderr || noPath.stdout || '').trim()}`,
|
|
251
|
+
`Hint: run 'synap-js-sdk setup --sdk-version <version>' to install maximem-synap from PyPI.`,
|
|
252
|
+
].join('\n');
|
|
253
|
+
throw new Error(details);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (this.options.onLog) {
|
|
257
|
+
this.options.onLog('debug', 'Python SDK import OK from installed package');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = { BridgeManager };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const { SynapClient } = require('./synap-client');
|
|
2
|
+
const { setupPythonRuntime, resolveInstanceId } = require('./runtime');
|
|
3
|
+
const { setupTypeScriptExtension } = require('./setup-typescript');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
SynapClient,
|
|
7
|
+
createClient: (options = {}) => new SynapClient(options),
|
|
8
|
+
setupPythonRuntime,
|
|
9
|
+
setupTypeScriptExtension,
|
|
10
|
+
resolveInstanceId,
|
|
11
|
+
};
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
|
|
6
|
+
function isWindows() {
|
|
7
|
+
return process.platform === 'win32';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getSdkHome(customHome) {
|
|
11
|
+
if (customHome) return customHome;
|
|
12
|
+
if (process.env.SYNAP_JS_SDK_HOME) return process.env.SYNAP_JS_SDK_HOME;
|
|
13
|
+
return path.join(os.homedir(), '.synap-js-sdk');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getVenvPythonPath(venvPath) {
|
|
17
|
+
if (isWindows()) return path.join(venvPath, 'Scripts', 'python.exe');
|
|
18
|
+
return path.join(venvPath, 'bin', 'python');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveBridgeScriptPath(customBridgeScriptPath) {
|
|
22
|
+
if (customBridgeScriptPath) return customBridgeScriptPath;
|
|
23
|
+
return path.join(__dirname, '..', 'bridge', 'synap_bridge.py');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolvePythonBin(options = {}) {
|
|
27
|
+
const candidates = [];
|
|
28
|
+
|
|
29
|
+
if (options.pythonBin) candidates.push(options.pythonBin);
|
|
30
|
+
if (process.env.SYNAP_PYTHON_BIN) candidates.push(process.env.SYNAP_PYTHON_BIN);
|
|
31
|
+
|
|
32
|
+
const sdkHome = getSdkHome(options.sdkHome);
|
|
33
|
+
const venvPath = options.venvPath || path.join(sdkHome, '.venv');
|
|
34
|
+
candidates.push(getVenvPythonPath(venvPath));
|
|
35
|
+
|
|
36
|
+
if (isWindows()) {
|
|
37
|
+
candidates.push('python.exe');
|
|
38
|
+
candidates.push('python');
|
|
39
|
+
} else {
|
|
40
|
+
candidates.push('python3');
|
|
41
|
+
candidates.push('python');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return candidates;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveInstanceId(explicitInstanceId) {
|
|
48
|
+
if (explicitInstanceId) return explicitInstanceId;
|
|
49
|
+
if (process.env.SYNAP_INSTANCE_ID) return process.env.SYNAP_INSTANCE_ID;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const instancesDir = path.join(os.homedir(), '.synap', 'instances');
|
|
53
|
+
if (!fs.existsSync(instancesDir)) return '';
|
|
54
|
+
|
|
55
|
+
const now = new Date();
|
|
56
|
+
let best = null;
|
|
57
|
+
|
|
58
|
+
for (const entry of fs.readdirSync(instancesDir)) {
|
|
59
|
+
const metadataPath = path.join(instancesDir, entry, 'metadata.json');
|
|
60
|
+
if (!fs.existsSync(metadataPath)) continue;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
64
|
+
const expiresAt = new Date(metadata.expires_at);
|
|
65
|
+
const issuedAt = new Date(metadata.issued_at);
|
|
66
|
+
|
|
67
|
+
if (Number.isNaN(expiresAt.getTime()) || Number.isNaN(issuedAt.getTime())) continue;
|
|
68
|
+
if (expiresAt <= now) continue;
|
|
69
|
+
|
|
70
|
+
if (!best || issuedAt > new Date(best.issued_at)) best = metadata;
|
|
71
|
+
} catch (_) {
|
|
72
|
+
// Ignore malformed metadata and continue scanning.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return best ? best.instance_id || '' : '';
|
|
77
|
+
} catch (_) {
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function runCommand(command, args, opts = {}) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const child = spawn(command, args, {
|
|
85
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
86
|
+
...opts,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let stdout = '';
|
|
90
|
+
let stderr = '';
|
|
91
|
+
|
|
92
|
+
child.stdout.on('data', (chunk) => {
|
|
93
|
+
stdout += chunk.toString();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
child.stderr.on('data', (chunk) => {
|
|
97
|
+
stderr += chunk.toString();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
child.on('error', (error) => {
|
|
101
|
+
reject(error);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
child.on('exit', (code) => {
|
|
105
|
+
if (code === 0) {
|
|
106
|
+
resolve({ code, stdout, stderr });
|
|
107
|
+
} else {
|
|
108
|
+
const err = new Error(`Command failed: ${command} ${args.join(' ')} (exit ${code})\n${stderr || stdout}`);
|
|
109
|
+
err.code = code;
|
|
110
|
+
err.stdout = stdout;
|
|
111
|
+
err.stderr = stderr;
|
|
112
|
+
reject(err);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function setupPythonRuntime(options = {}) {
|
|
119
|
+
const sdkHome = getSdkHome(options.sdkHome);
|
|
120
|
+
const venvPath = options.venvPath || path.join(sdkHome, '.venv');
|
|
121
|
+
const pythonBootstrap = options.pythonBootstrap || process.env.SYNAP_PYTHON_BOOTSTRAP || (isWindows() ? 'python' : 'python3');
|
|
122
|
+
const pythonPackage = options.pythonPackage || process.env.SYNAP_PY_SDK_PACKAGE || 'maximem-synap';
|
|
123
|
+
const pythonSdkVersion = options.pythonSdkVersion || process.env.SYNAP_PY_SDK_VERSION || '';
|
|
124
|
+
const packageSpec = pythonSdkVersion ? `${pythonPackage}==${pythonSdkVersion}` : pythonPackage;
|
|
125
|
+
const pythonBin = getVenvPythonPath(venvPath);
|
|
126
|
+
|
|
127
|
+
fs.mkdirSync(sdkHome, { recursive: true });
|
|
128
|
+
|
|
129
|
+
if (!fs.existsSync(pythonBin) || options.forceRecreateVenv) {
|
|
130
|
+
await runCommand(pythonBootstrap, ['-m', 'venv', venvPath], { env: process.env });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await runCommand(pythonBin, ['-m', 'pip', 'install', '--upgrade', 'pip'], { env: process.env });
|
|
134
|
+
|
|
135
|
+
const installArgs = ['-m', 'pip', 'install'];
|
|
136
|
+
if (options.upgrade) installArgs.push('--upgrade');
|
|
137
|
+
if (options.noDeps) installArgs.push('--no-deps');
|
|
138
|
+
if (options.noBuildIsolation) installArgs.push('--no-build-isolation');
|
|
139
|
+
installArgs.push(packageSpec);
|
|
140
|
+
|
|
141
|
+
await runCommand(pythonBin, installArgs, { env: process.env });
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
sdkHome,
|
|
145
|
+
venvPath,
|
|
146
|
+
pythonBin,
|
|
147
|
+
pythonPackage,
|
|
148
|
+
pythonSdkVersion: pythonSdkVersion || null,
|
|
149
|
+
installTarget: packageSpec,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
getSdkHome,
|
|
155
|
+
getVenvPythonPath,
|
|
156
|
+
resolveBridgeScriptPath,
|
|
157
|
+
resolvePythonBin,
|
|
158
|
+
resolveInstanceId,
|
|
159
|
+
setupPythonRuntime,
|
|
160
|
+
runCommand,
|
|
161
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { runCommand } = require('./runtime');
|
|
4
|
+
|
|
5
|
+
function detectPackageManager(projectDir, explicitPackageManager) {
|
|
6
|
+
if (explicitPackageManager) return explicitPackageManager;
|
|
7
|
+
|
|
8
|
+
if (fs.existsSync(path.join(projectDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
9
|
+
if (fs.existsSync(path.join(projectDir, 'yarn.lock'))) return 'yarn';
|
|
10
|
+
if (fs.existsSync(path.join(projectDir, 'bun.lockb')) || fs.existsSync(path.join(projectDir, 'bun.lock'))) {
|
|
11
|
+
return 'bun';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return 'npm';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getInstallArgs(packageManager) {
|
|
18
|
+
switch (packageManager) {
|
|
19
|
+
case 'pnpm':
|
|
20
|
+
return { command: 'pnpm', args: ['add', '-D', 'typescript', '@types/node'] };
|
|
21
|
+
case 'yarn':
|
|
22
|
+
return { command: 'yarn', args: ['add', '-D', 'typescript', '@types/node'] };
|
|
23
|
+
case 'bun':
|
|
24
|
+
return { command: 'bun', args: ['add', '-d', 'typescript', '@types/node'] };
|
|
25
|
+
case 'npm':
|
|
26
|
+
return { command: 'npm', args: ['install', '-D', 'typescript', '@types/node'] };
|
|
27
|
+
default:
|
|
28
|
+
throw new Error(`Unsupported package manager '${packageManager}'. Use npm, pnpm, yarn, or bun.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeFileIfNeeded(filePath, contents, force) {
|
|
33
|
+
if (fs.existsSync(filePath) && !force) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
38
|
+
fs.writeFileSync(filePath, contents, 'utf8');
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getTsconfigTemplate() {
|
|
43
|
+
return `{
|
|
44
|
+
"compilerOptions": {
|
|
45
|
+
"target": "ES2020",
|
|
46
|
+
"module": "CommonJS",
|
|
47
|
+
"moduleResolution": "Node",
|
|
48
|
+
"strict": true,
|
|
49
|
+
"esModuleInterop": true,
|
|
50
|
+
"forceConsistentCasingInFileNames": true,
|
|
51
|
+
"skipLibCheck": true,
|
|
52
|
+
"outDir": "dist"
|
|
53
|
+
},
|
|
54
|
+
"include": ["src/**/*.ts", "types/**/*.d.ts"]
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getWrapperTemplate() {
|
|
60
|
+
return `import {
|
|
61
|
+
createClient,
|
|
62
|
+
type SynapClient,
|
|
63
|
+
type SynapClientOptions,
|
|
64
|
+
type AddMemoryInput,
|
|
65
|
+
type SearchMemoryInput,
|
|
66
|
+
type GetMemoriesInput,
|
|
67
|
+
type DeleteMemoryInput,
|
|
68
|
+
} from '@maximem/synap-js-sdk';
|
|
69
|
+
|
|
70
|
+
export class SynapTsClient {
|
|
71
|
+
private readonly client: SynapClient;
|
|
72
|
+
|
|
73
|
+
constructor(options: SynapClientOptions = {}) {
|
|
74
|
+
this.client = createClient(options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
init() {
|
|
78
|
+
return this.client.init();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
addMemory(input: AddMemoryInput) {
|
|
82
|
+
return this.client.addMemory(input);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
searchMemory(input: SearchMemoryInput) {
|
|
86
|
+
return this.client.searchMemory(input);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getMemories(input: GetMemoriesInput) {
|
|
90
|
+
return this.client.getMemories(input);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
deleteMemory(input: DeleteMemoryInput) {
|
|
94
|
+
return this.client.deleteMemory(input);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
shutdown() {
|
|
98
|
+
return this.client.shutdown();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const createTsClient = (options: SynapClientOptions = {}) => new SynapTsClient(options);
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function setupTypeScriptExtension(options = {}) {
|
|
107
|
+
const projectDir = path.resolve(options.projectDir || process.cwd());
|
|
108
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
109
|
+
|
|
110
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
111
|
+
throw new Error(`No package.json found in ${projectDir}. Run setup-ts inside a Node project.`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const packageManager = detectPackageManager(projectDir, options.packageManager);
|
|
115
|
+
const tsconfigPath = path.resolve(projectDir, options.tsconfigPath || 'tsconfig.json');
|
|
116
|
+
const wrapperPath = path.resolve(projectDir, options.wrapperPath || path.join('src', 'synap.ts'));
|
|
117
|
+
|
|
118
|
+
if (!options.skipInstall) {
|
|
119
|
+
const install = getInstallArgs(packageManager);
|
|
120
|
+
await runCommand(install.command, install.args, { env: process.env, cwd: projectDir });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const tsconfigCreated = writeFileIfNeeded(tsconfigPath, getTsconfigTemplate(), options.force);
|
|
124
|
+
const wrapperCreated = options.noWrapper
|
|
125
|
+
? false
|
|
126
|
+
: writeFileIfNeeded(wrapperPath, getWrapperTemplate(), options.force);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
projectDir,
|
|
130
|
+
packageManager,
|
|
131
|
+
installedDevDependencies: !options.skipInstall,
|
|
132
|
+
tsconfigPath,
|
|
133
|
+
tsconfigCreated,
|
|
134
|
+
wrapperPath: options.noWrapper ? null : wrapperPath,
|
|
135
|
+
wrapperCreated,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
setupTypeScriptExtension,
|
|
141
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const { BridgeManager } = require('./bridge-manager');
|
|
2
|
+
|
|
3
|
+
class SynapClient {
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.bridge = new BridgeManager(options);
|
|
6
|
+
this.options = {
|
|
7
|
+
ingestTimeoutMs: options.ingestTimeoutMs || 120_000,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
this.#registerShutdownHooks();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async init() {
|
|
14
|
+
await this.bridge.ensureStarted();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async addMemory({ userId, messages }) {
|
|
18
|
+
this.#assert(userId, 'userId is required');
|
|
19
|
+
this.#assert(Array.isArray(messages), 'messages must be an array');
|
|
20
|
+
|
|
21
|
+
return this.bridge.call(
|
|
22
|
+
'add_memory',
|
|
23
|
+
{ user_id: userId, messages },
|
|
24
|
+
this.options.ingestTimeoutMs
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async searchMemory({ userId, query, maxResults = 10 }) {
|
|
29
|
+
this.#assert(userId, 'userId is required');
|
|
30
|
+
this.#assert(query, 'query is required');
|
|
31
|
+
|
|
32
|
+
return this.bridge.call('search_memory', {
|
|
33
|
+
user_id: userId,
|
|
34
|
+
query,
|
|
35
|
+
max_results: maxResults,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getMemories({ userId }) {
|
|
40
|
+
this.#assert(userId, 'userId is required');
|
|
41
|
+
|
|
42
|
+
return this.bridge.call('get_memories', { user_id: userId });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async deleteMemory({ userId, memoryId = null }) {
|
|
46
|
+
this.#assert(userId, 'userId is required');
|
|
47
|
+
|
|
48
|
+
return this.bridge.call('delete_memory', {
|
|
49
|
+
user_id: userId,
|
|
50
|
+
memory_id: memoryId,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async shutdown() {
|
|
55
|
+
await this.bridge.shutdown();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#assert(value, message) {
|
|
59
|
+
if (!value) throw new Error(message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#registerShutdownHooks() {
|
|
63
|
+
const close = async () => {
|
|
64
|
+
try {
|
|
65
|
+
await this.shutdown();
|
|
66
|
+
} catch (_) {
|
|
67
|
+
// Best-effort shutdown.
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
process.once('beforeExit', close);
|
|
72
|
+
process.once('SIGINT', async () => {
|
|
73
|
+
await close();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
});
|
|
76
|
+
process.once('SIGTERM', async () => {
|
|
77
|
+
await close();
|
|
78
|
+
process.exit(0);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { SynapClient };
|