@ekkos/cli 1.3.1 → 1.3.2
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/dist/commands/dashboard.js +147 -57
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +54 -16
- package/dist/commands/run.js +163 -44
- package/dist/commands/status.d.ts +4 -1
- package/dist/commands/status.js +165 -27
- package/dist/commands/synk.d.ts +7 -0
- package/dist/commands/synk.js +339 -0
- package/dist/deploy/settings.d.ts +6 -5
- package/dist/deploy/settings.js +27 -17
- package/dist/index.js +12 -82
- package/dist/lib/usage-parser.d.ts +1 -1
- package/dist/lib/usage-parser.js +5 -3
- package/dist/local/index.d.ts +14 -0
- package/dist/local/index.js +28 -0
- package/dist/local/local-embeddings.d.ts +49 -0
- package/dist/local/local-embeddings.js +232 -0
- package/dist/local/offline-fallback.d.ts +44 -0
- package/dist/local/offline-fallback.js +159 -0
- package/dist/local/sqlite-store.d.ts +126 -0
- package/dist/local/sqlite-store.js +393 -0
- package/dist/local/sync-engine.d.ts +42 -0
- package/dist/local/sync-engine.js +223 -0
- package/dist/synk/api.d.ts +22 -0
- package/dist/synk/api.js +133 -0
- package/dist/synk/auth.d.ts +7 -0
- package/dist/synk/auth.js +30 -0
- package/dist/synk/config.d.ts +18 -0
- package/dist/synk/config.js +37 -0
- package/dist/synk/daemon/control-client.d.ts +11 -0
- package/dist/synk/daemon/control-client.js +101 -0
- package/dist/synk/daemon/control-server.d.ts +24 -0
- package/dist/synk/daemon/control-server.js +91 -0
- package/dist/synk/daemon/run.d.ts +14 -0
- package/dist/synk/daemon/run.js +338 -0
- package/dist/synk/encryption.d.ts +17 -0
- package/dist/synk/encryption.js +133 -0
- package/dist/synk/index.d.ts +13 -0
- package/dist/synk/index.js +36 -0
- package/dist/synk/machine-client.d.ts +42 -0
- package/dist/synk/machine-client.js +218 -0
- package/dist/synk/persistence.d.ts +51 -0
- package/dist/synk/persistence.js +211 -0
- package/dist/synk/qr.d.ts +5 -0
- package/dist/synk/qr.js +33 -0
- package/dist/synk/session-bridge.d.ts +58 -0
- package/dist/synk/session-bridge.js +171 -0
- package/dist/synk/session-client.d.ts +46 -0
- package/dist/synk/session-client.js +240 -0
- package/dist/synk/types.d.ts +574 -0
- package/dist/synk/types.js +74 -0
- package/dist/utils/platform.d.ts +5 -1
- package/dist/utils/platform.js +24 -4
- package/dist/utils/proxy-url.d.ts +10 -0
- package/dist/utils/proxy-url.js +19 -0
- package/dist/utils/state.d.ts +1 -1
- package/dist/utils/state.js +11 -3
- package/package.json +13 -4
- package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +0 -819
- package/templates/claude-plugins-admin/README.md +0 -446
- package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +0 -595
- package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/backend-agent/commands/backend.md +0 -798
- package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +0 -554
- package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +0 -881
- package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +0 -85
- package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +0 -569
- package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/qa-agent/commands/qa.md +0 -863
- package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +0 -732
- package/templates/hooks-node/lib/state.js +0 -187
- package/templates/hooks-node/stop.js +0 -416
- package/templates/hooks-node/user-prompt-submit.js +0 -337
- package/templates/rules/00-hooks-contract.mdc +0 -89
- package/templates/rules/30-ekkos-core.mdc +0 -188
- package/templates/rules/31-ekkos-messages.mdc +0 -78
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Synk daemon — background process for session management and remote control
|
|
4
|
+
*
|
|
5
|
+
* Lifecycle:
|
|
6
|
+
* 1. Acquire lock file (prevent multiple daemons)
|
|
7
|
+
* 2. Authenticate with synk-server
|
|
8
|
+
* 3. Start local HTTP control server
|
|
9
|
+
* 4. Register machine via REST API
|
|
10
|
+
* 5. Connect MachineClient WebSocket
|
|
11
|
+
* 6. Heartbeat loop
|
|
12
|
+
* 7. Await shutdown signal
|
|
13
|
+
* 8. Cleanup
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.startDaemon = startDaemon;
|
|
17
|
+
const node_os_1 = require("node:os");
|
|
18
|
+
const node_path_1 = require("node:path");
|
|
19
|
+
const node_fs_1 = require("node:fs");
|
|
20
|
+
const node_child_process_1 = require("node:child_process");
|
|
21
|
+
const node_crypto_1 = require("node:crypto");
|
|
22
|
+
const config_1 = require("../config");
|
|
23
|
+
const persistence_1 = require("../persistence");
|
|
24
|
+
const api_1 = require("../api");
|
|
25
|
+
const machine_client_1 = require("../machine-client");
|
|
26
|
+
const control_server_1 = require("./control-server");
|
|
27
|
+
const control_client_1 = require("./control-client");
|
|
28
|
+
const proxy_url_1 = require("../../utils/proxy-url");
|
|
29
|
+
const state_1 = require("../../utils/state");
|
|
30
|
+
const HEARTBEAT_INTERVAL = parseInt(process.env.SYNK_DAEMON_HEARTBEAT_INTERVAL || '60000');
|
|
31
|
+
let logStream = null;
|
|
32
|
+
function getCliVersion() {
|
|
33
|
+
try {
|
|
34
|
+
const pkgPath = (0, node_path_1.resolve)(__dirname, '../../../package.json');
|
|
35
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgPath, 'utf-8'));
|
|
36
|
+
return pkg.version || '0.0.0';
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return '0.0.0';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function daemonLog(message, ...args) {
|
|
43
|
+
const line = `[${new Date().toISOString()}] ${message} ${args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ')}`;
|
|
44
|
+
if (logStream)
|
|
45
|
+
logStream.write(line + '\n');
|
|
46
|
+
if (!config_1.synkConfig.isDaemonProcess)
|
|
47
|
+
console.log(line);
|
|
48
|
+
}
|
|
49
|
+
async function startDaemon() {
|
|
50
|
+
config_1.synkConfig.ensureDirectories();
|
|
51
|
+
const cliVersion = getCliVersion();
|
|
52
|
+
// Setup log file
|
|
53
|
+
const logPath = (0, node_path_1.join)(config_1.synkConfig.logsDir, `${new Date().toISOString().replace(/[:.]/g, '-')}-daemon.log`);
|
|
54
|
+
if (!(0, node_fs_1.existsSync)(config_1.synkConfig.logsDir))
|
|
55
|
+
(0, node_fs_1.mkdirSync)(config_1.synkConfig.logsDir, { recursive: true });
|
|
56
|
+
logStream = (0, node_fs_1.createWriteStream)(logPath, { flags: 'a' });
|
|
57
|
+
daemonLog(`Starting synk daemon v${cliVersion}`);
|
|
58
|
+
// Check if daemon already running
|
|
59
|
+
const existingState = await (0, persistence_1.readDaemonState)();
|
|
60
|
+
if (existingState) {
|
|
61
|
+
try {
|
|
62
|
+
process.kill(existingState.pid, 0);
|
|
63
|
+
daemonLog('Daemon already running, stopping old instance');
|
|
64
|
+
await (0, control_client_1.stopDaemon)();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
daemonLog('Stale daemon state, cleaning up');
|
|
68
|
+
await (0, persistence_1.clearDaemonState)();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Acquire lock
|
|
72
|
+
const lockHandle = await (0, persistence_1.acquireDaemonLock)();
|
|
73
|
+
if (!lockHandle) {
|
|
74
|
+
daemonLog('Failed to acquire daemon lock — another daemon may be running');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
// Authenticate
|
|
78
|
+
const credentials = await (0, persistence_1.readCredentials)();
|
|
79
|
+
if (!credentials) {
|
|
80
|
+
daemonLog('No credentials found. Run "ekkos synk auth" first.');
|
|
81
|
+
await (0, persistence_1.releaseDaemonLock)(lockHandle);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
// Get or create machine ID
|
|
85
|
+
const settings = await (0, persistence_1.readSettings)();
|
|
86
|
+
let machineId = settings.machineId;
|
|
87
|
+
if (!machineId) {
|
|
88
|
+
machineId = (0, node_crypto_1.randomUUID)();
|
|
89
|
+
await (0, persistence_1.updateSettings)(s => ({ ...s, machineId }));
|
|
90
|
+
daemonLog('Generated new machine ID:', machineId);
|
|
91
|
+
}
|
|
92
|
+
// Session tracking — keyed by sessionId (not PID) to avoid overwrite bugs
|
|
93
|
+
const trackedSessions = new Map();
|
|
94
|
+
let sessionCounter = 0;
|
|
95
|
+
// Shutdown handling
|
|
96
|
+
let shutdownRequested = false;
|
|
97
|
+
let resolveShutdown;
|
|
98
|
+
const shutdownPromise = new Promise(resolve => { resolveShutdown = resolve; });
|
|
99
|
+
const requestShutdown = () => {
|
|
100
|
+
if (shutdownRequested)
|
|
101
|
+
return;
|
|
102
|
+
shutdownRequested = true;
|
|
103
|
+
daemonLog('Shutdown requested');
|
|
104
|
+
resolveShutdown();
|
|
105
|
+
};
|
|
106
|
+
process.on('SIGINT', requestShutdown);
|
|
107
|
+
process.on('SIGTERM', requestShutdown);
|
|
108
|
+
process.on('uncaughtException', (error) => {
|
|
109
|
+
daemonLog('Uncaught exception:', error);
|
|
110
|
+
requestShutdown();
|
|
111
|
+
});
|
|
112
|
+
// --- Spawn session implementation ---
|
|
113
|
+
const spawnSession = async (options) => {
|
|
114
|
+
const { directory } = options;
|
|
115
|
+
daemonLog('Spawning session in:', directory);
|
|
116
|
+
if (!(0, node_fs_1.existsSync)(directory)) {
|
|
117
|
+
return { type: 'error', errorMessage: `Directory does not exist: ${directory}` };
|
|
118
|
+
}
|
|
119
|
+
// Generate session identity
|
|
120
|
+
const sessionId = options.sessionId || (0, node_crypto_1.randomUUID)();
|
|
121
|
+
const sessionName = (0, state_1.uuidToWords)(sessionId);
|
|
122
|
+
// Build proxy env vars (unless disabled)
|
|
123
|
+
const proxyEnv = {};
|
|
124
|
+
if (process.env.SYNK_DISABLE_PROXY !== '1') {
|
|
125
|
+
const ekkosConfig = (0, state_1.getConfig)();
|
|
126
|
+
let userId = ekkosConfig?.userId || 'anonymous';
|
|
127
|
+
if (userId === 'anonymous') {
|
|
128
|
+
const authToken = (0, state_1.getAuthToken)();
|
|
129
|
+
if (authToken?.startsWith('ekk_')) {
|
|
130
|
+
const parts = authToken.split('_');
|
|
131
|
+
if (parts.length >= 2)
|
|
132
|
+
userId = parts[1];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
proxyEnv.ANTHROPIC_BASE_URL = (0, proxy_url_1.buildProxyUrl)(userId, sessionName, directory, sessionId);
|
|
136
|
+
proxyEnv.EKKOS_PROXY_MODE = '1';
|
|
137
|
+
proxyEnv.EKKOS_ULTRA_MINIMAL = '1';
|
|
138
|
+
}
|
|
139
|
+
// Merge env vars: process.env + options.environmentVariables + proxy (highest priority)
|
|
140
|
+
const childEnv = {
|
|
141
|
+
...process.env,
|
|
142
|
+
...(options.environmentVariables || {}),
|
|
143
|
+
...proxyEnv,
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
// Create log file for spawned session
|
|
147
|
+
const sessionLogPath = (0, node_path_1.join)(config_1.synkConfig.logsDir, `${new Date().toISOString().replace(/[:.]/g, '-')}-session-${sessionName}.log`);
|
|
148
|
+
const sessionLogFd = (0, node_fs_1.createWriteStream)(sessionLogPath, { flags: 'a' });
|
|
149
|
+
const child = (0, node_child_process_1.spawn)('ekkos', ['run', '--started-by', 'daemon'], {
|
|
150
|
+
cwd: directory,
|
|
151
|
+
env: childEnv,
|
|
152
|
+
detached: true,
|
|
153
|
+
stdio: ['ignore', sessionLogFd, sessionLogFd],
|
|
154
|
+
});
|
|
155
|
+
child.unref();
|
|
156
|
+
const trackKey = `spawn-${++sessionCounter}`;
|
|
157
|
+
const tracked = {
|
|
158
|
+
pid: child.pid || 0,
|
|
159
|
+
startedBy: 'daemon',
|
|
160
|
+
startedAt: Date.now(),
|
|
161
|
+
};
|
|
162
|
+
trackedSessions.set(trackKey, tracked);
|
|
163
|
+
// Wait for session to report itself via webhook (max 15s)
|
|
164
|
+
const webhookSessionId = await new Promise((resolve) => {
|
|
165
|
+
const timeout = setTimeout(() => resolve(null), 15000);
|
|
166
|
+
const checkInterval = setInterval(() => {
|
|
167
|
+
if (tracked.synkSessionId) {
|
|
168
|
+
clearInterval(checkInterval);
|
|
169
|
+
clearTimeout(timeout);
|
|
170
|
+
resolve(tracked.synkSessionId);
|
|
171
|
+
}
|
|
172
|
+
}, 200);
|
|
173
|
+
});
|
|
174
|
+
if (webhookSessionId) {
|
|
175
|
+
// Re-key tracked session by its actual session ID
|
|
176
|
+
trackedSessions.delete(trackKey);
|
|
177
|
+
trackedSessions.set(webhookSessionId, tracked);
|
|
178
|
+
daemonLog('Session spawned and registered:', webhookSessionId);
|
|
179
|
+
return { type: 'success', sessionId: webhookSessionId };
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
daemonLog('Session spawned but webhook timeout — PID:', child.pid);
|
|
183
|
+
return { type: 'success', sessionId: `pending-${child.pid}` };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
daemonLog('Failed to spawn session:', error);
|
|
188
|
+
return { type: 'error', errorMessage: error instanceof Error ? error.message : 'Failed to spawn session' };
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
// Start HTTP control server
|
|
192
|
+
const controlServer = await (0, control_server_1.startDaemonControlServer)({
|
|
193
|
+
getChildren: () => Array.from(trackedSessions.values()),
|
|
194
|
+
stopSession: (sessionId) => {
|
|
195
|
+
for (const [key, session] of trackedSessions) {
|
|
196
|
+
if (session.synkSessionId === sessionId || key === sessionId) {
|
|
197
|
+
try {
|
|
198
|
+
process.kill(session.pid, 'SIGTERM');
|
|
199
|
+
}
|
|
200
|
+
catch { }
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
},
|
|
206
|
+
spawnSession: async (options) => {
|
|
207
|
+
const result = await spawnSession(options);
|
|
208
|
+
if (result.type === 'success')
|
|
209
|
+
return { success: true, sessionId: result.sessionId };
|
|
210
|
+
return { success: false, error: result.errorMessage };
|
|
211
|
+
},
|
|
212
|
+
requestShutdown,
|
|
213
|
+
onSessionWebhook: (sessionId, metadata) => {
|
|
214
|
+
daemonLog('Session webhook:', sessionId);
|
|
215
|
+
// Find tracked session by PID from metadata
|
|
216
|
+
const hostPid = metadata?.hostPid;
|
|
217
|
+
let matched = false;
|
|
218
|
+
if (hostPid) {
|
|
219
|
+
for (const [, session] of trackedSessions) {
|
|
220
|
+
if (session.pid === hostPid && !session.synkSessionId) {
|
|
221
|
+
session.synkSessionId = sessionId;
|
|
222
|
+
matched = true;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!matched) {
|
|
228
|
+
// Terminal-spawned session reporting itself
|
|
229
|
+
trackedSessions.set(sessionId, {
|
|
230
|
+
pid: hostPid || process.pid,
|
|
231
|
+
startedBy: 'terminal',
|
|
232
|
+
synkSessionId: sessionId,
|
|
233
|
+
startedAt: Date.now(),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
// Write daemon state
|
|
239
|
+
(0, persistence_1.writeDaemonState)({
|
|
240
|
+
pid: process.pid,
|
|
241
|
+
httpPort: controlServer.port,
|
|
242
|
+
startTime: new Date().toLocaleString(),
|
|
243
|
+
startedWithCliVersion: cliVersion,
|
|
244
|
+
daemonLogPath: logPath,
|
|
245
|
+
});
|
|
246
|
+
daemonLog(`Control server on port ${controlServer.port}`);
|
|
247
|
+
// Register machine with synk-server via REST
|
|
248
|
+
const machineMetadata = {
|
|
249
|
+
host: (0, node_os_1.hostname)(),
|
|
250
|
+
platform: (0, node_os_1.platform)(),
|
|
251
|
+
synkCliVersion: cliVersion,
|
|
252
|
+
homeDir: (0, node_os_1.homedir)(),
|
|
253
|
+
synkHomeDir: config_1.synkConfig.synkHomeDir,
|
|
254
|
+
};
|
|
255
|
+
const initialDaemonState = {
|
|
256
|
+
status: 'starting',
|
|
257
|
+
pid: process.pid,
|
|
258
|
+
httpPort: controlServer.port,
|
|
259
|
+
startedAt: Date.now(),
|
|
260
|
+
};
|
|
261
|
+
const apiClient = await api_1.ApiClient.create(credentials);
|
|
262
|
+
const machine = await apiClient.getOrCreateMachine({
|
|
263
|
+
machineId,
|
|
264
|
+
metadata: machineMetadata,
|
|
265
|
+
daemonState: initialDaemonState,
|
|
266
|
+
});
|
|
267
|
+
daemonLog('Machine registered:', machine.id);
|
|
268
|
+
// Connect WebSocket via MachineClient
|
|
269
|
+
const machineClient = new machine_client_1.MachineClient(credentials.token, machine, daemonLog);
|
|
270
|
+
machineClient.setRPCHandlers({
|
|
271
|
+
spawnSession,
|
|
272
|
+
stopSession: (sessionId) => {
|
|
273
|
+
for (const [key, session] of trackedSessions) {
|
|
274
|
+
if (session.synkSessionId === sessionId || key === sessionId) {
|
|
275
|
+
try {
|
|
276
|
+
process.kill(session.pid, 'SIGTERM');
|
|
277
|
+
}
|
|
278
|
+
catch { }
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
},
|
|
284
|
+
requestShutdown,
|
|
285
|
+
});
|
|
286
|
+
machineClient.connect();
|
|
287
|
+
// Heartbeat loop
|
|
288
|
+
const heartbeatInterval = setInterval(async () => {
|
|
289
|
+
if (shutdownRequested)
|
|
290
|
+
return;
|
|
291
|
+
// Update last heartbeat
|
|
292
|
+
try {
|
|
293
|
+
const state = await (0, persistence_1.readDaemonState)();
|
|
294
|
+
if (state) {
|
|
295
|
+
(0, persistence_1.writeDaemonState)({ ...state, lastHeartbeat: new Date().toLocaleString() });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch { }
|
|
299
|
+
// Prune dead sessions
|
|
300
|
+
for (const [key, session] of trackedSessions) {
|
|
301
|
+
if (session.pid === 0 || session.pid === process.pid)
|
|
302
|
+
continue;
|
|
303
|
+
try {
|
|
304
|
+
process.kill(session.pid, 0);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
daemonLog('Pruning dead session:', session.synkSessionId || key);
|
|
308
|
+
trackedSessions.delete(key);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}, HEARTBEAT_INTERVAL);
|
|
312
|
+
daemonLog('Daemon ready. Waiting for shutdown signal...');
|
|
313
|
+
// Wait for shutdown
|
|
314
|
+
await shutdownPromise;
|
|
315
|
+
daemonLog('Shutting down...');
|
|
316
|
+
// Cleanup
|
|
317
|
+
clearInterval(heartbeatInterval);
|
|
318
|
+
// Update daemon state on server via MachineClient
|
|
319
|
+
try {
|
|
320
|
+
await machineClient.updateDaemonState(() => ({
|
|
321
|
+
status: 'shutting-down',
|
|
322
|
+
shutdownRequestedAt: Date.now(),
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
catch { }
|
|
326
|
+
// Close WebSocket
|
|
327
|
+
machineClient.shutdown();
|
|
328
|
+
// Stop control server
|
|
329
|
+
await controlServer.stop();
|
|
330
|
+
// Clear daemon state file
|
|
331
|
+
await (0, persistence_1.clearDaemonState)();
|
|
332
|
+
// Release lock
|
|
333
|
+
await (0, persistence_1.releaseDaemonLock)(lockHandle);
|
|
334
|
+
if (logStream)
|
|
335
|
+
logStream.end();
|
|
336
|
+
daemonLog('Daemon stopped cleanly');
|
|
337
|
+
process.exit(0);
|
|
338
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function encodeBase64(buffer: Uint8Array, variant?: 'base64' | 'base64url'): string;
|
|
2
|
+
export declare function encodeBase64Url(buffer: Uint8Array): string;
|
|
3
|
+
export declare function decodeBase64(base64: string, variant?: 'base64' | 'base64url'): Uint8Array;
|
|
4
|
+
export declare function getRandomBytes(size: number): Uint8Array;
|
|
5
|
+
export declare function libsodiumPublicKeyFromSecretKey(seed: Uint8Array): Uint8Array;
|
|
6
|
+
export declare function libsodiumEncryptForPublicKey(data: Uint8Array, recipientPublicKey: Uint8Array): Uint8Array;
|
|
7
|
+
export declare function encryptLegacy(data: any, secret: Uint8Array): Uint8Array;
|
|
8
|
+
export declare function decryptLegacy(data: Uint8Array, secret: Uint8Array): any | null;
|
|
9
|
+
export declare function encryptWithDataKey(data: any, dataKey: Uint8Array): Uint8Array;
|
|
10
|
+
export declare function decryptWithDataKey(bundle: Uint8Array, dataKey: Uint8Array): any | null;
|
|
11
|
+
export declare function encrypt(key: Uint8Array, variant: 'legacy' | 'dataKey', data: any): Uint8Array;
|
|
12
|
+
export declare function decrypt(key: Uint8Array, variant: 'legacy' | 'dataKey', data: Uint8Array): any | null;
|
|
13
|
+
export declare function authChallenge(secret: Uint8Array): {
|
|
14
|
+
challenge: Uint8Array;
|
|
15
|
+
publicKey: Uint8Array;
|
|
16
|
+
signature: Uint8Array;
|
|
17
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.encodeBase64 = encodeBase64;
|
|
7
|
+
exports.encodeBase64Url = encodeBase64Url;
|
|
8
|
+
exports.decodeBase64 = decodeBase64;
|
|
9
|
+
exports.getRandomBytes = getRandomBytes;
|
|
10
|
+
exports.libsodiumPublicKeyFromSecretKey = libsodiumPublicKeyFromSecretKey;
|
|
11
|
+
exports.libsodiumEncryptForPublicKey = libsodiumEncryptForPublicKey;
|
|
12
|
+
exports.encryptLegacy = encryptLegacy;
|
|
13
|
+
exports.decryptLegacy = decryptLegacy;
|
|
14
|
+
exports.encryptWithDataKey = encryptWithDataKey;
|
|
15
|
+
exports.decryptWithDataKey = decryptWithDataKey;
|
|
16
|
+
exports.encrypt = encrypt;
|
|
17
|
+
exports.decrypt = decrypt;
|
|
18
|
+
exports.authChallenge = authChallenge;
|
|
19
|
+
const node_crypto_1 = require("node:crypto");
|
|
20
|
+
const tweetnacl_1 = __importDefault(require("tweetnacl"));
|
|
21
|
+
function encodeBase64(buffer, variant = 'base64') {
|
|
22
|
+
if (variant === 'base64url') {
|
|
23
|
+
return encodeBase64Url(buffer);
|
|
24
|
+
}
|
|
25
|
+
return Buffer.from(buffer).toString('base64');
|
|
26
|
+
}
|
|
27
|
+
function encodeBase64Url(buffer) {
|
|
28
|
+
return Buffer.from(buffer)
|
|
29
|
+
.toString('base64')
|
|
30
|
+
.replace(/\+/g, '-')
|
|
31
|
+
.replace(/\//g, '_')
|
|
32
|
+
.replace(/=/g, '');
|
|
33
|
+
}
|
|
34
|
+
function decodeBase64(base64, variant = 'base64') {
|
|
35
|
+
if (variant === 'base64url') {
|
|
36
|
+
const base64Standard = base64
|
|
37
|
+
.replace(/-/g, '+')
|
|
38
|
+
.replace(/_/g, '/')
|
|
39
|
+
+ '='.repeat((4 - base64.length % 4) % 4);
|
|
40
|
+
return new Uint8Array(Buffer.from(base64Standard, 'base64'));
|
|
41
|
+
}
|
|
42
|
+
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
43
|
+
}
|
|
44
|
+
function getRandomBytes(size) {
|
|
45
|
+
return new Uint8Array((0, node_crypto_1.randomBytes)(size));
|
|
46
|
+
}
|
|
47
|
+
function libsodiumPublicKeyFromSecretKey(seed) {
|
|
48
|
+
const hashedSeed = new Uint8Array((0, node_crypto_1.createHash)('sha512').update(seed).digest());
|
|
49
|
+
const secretKey = hashedSeed.slice(0, 32);
|
|
50
|
+
return new Uint8Array(tweetnacl_1.default.box.keyPair.fromSecretKey(secretKey).publicKey);
|
|
51
|
+
}
|
|
52
|
+
function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
|
|
53
|
+
const ephemeralKeyPair = tweetnacl_1.default.box.keyPair();
|
|
54
|
+
const nonce = getRandomBytes(tweetnacl_1.default.box.nonceLength);
|
|
55
|
+
const encrypted = tweetnacl_1.default.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
|
|
56
|
+
const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
|
|
57
|
+
result.set(ephemeralKeyPair.publicKey, 0);
|
|
58
|
+
result.set(nonce, ephemeralKeyPair.publicKey.length);
|
|
59
|
+
result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
function encryptLegacy(data, secret) {
|
|
63
|
+
const nonce = getRandomBytes(tweetnacl_1.default.secretbox.nonceLength);
|
|
64
|
+
const encrypted = tweetnacl_1.default.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
|
|
65
|
+
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
66
|
+
result.set(nonce);
|
|
67
|
+
result.set(encrypted, nonce.length);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
function decryptLegacy(data, secret) {
|
|
71
|
+
const nonce = data.slice(0, tweetnacl_1.default.secretbox.nonceLength);
|
|
72
|
+
const encrypted = data.slice(tweetnacl_1.default.secretbox.nonceLength);
|
|
73
|
+
const decrypted = tweetnacl_1.default.secretbox.open(encrypted, nonce, secret);
|
|
74
|
+
if (!decrypted) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
78
|
+
}
|
|
79
|
+
function encryptWithDataKey(data, dataKey) {
|
|
80
|
+
const nonce = getRandomBytes(12);
|
|
81
|
+
const cipher = (0, node_crypto_1.createCipheriv)('aes-256-gcm', dataKey, nonce);
|
|
82
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
83
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
84
|
+
const authTag = cipher.getAuthTag();
|
|
85
|
+
const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
|
|
86
|
+
bundle.set([0], 0);
|
|
87
|
+
bundle.set(nonce, 1);
|
|
88
|
+
bundle.set(new Uint8Array(encrypted), 13);
|
|
89
|
+
bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
|
|
90
|
+
return bundle;
|
|
91
|
+
}
|
|
92
|
+
function decryptWithDataKey(bundle, dataKey) {
|
|
93
|
+
if (bundle.length < 1)
|
|
94
|
+
return null;
|
|
95
|
+
if (bundle[0] !== 0)
|
|
96
|
+
return null;
|
|
97
|
+
if (bundle.length < 12 + 16 + 1)
|
|
98
|
+
return null;
|
|
99
|
+
const nonce = bundle.slice(1, 13);
|
|
100
|
+
const authTag = bundle.slice(bundle.length - 16);
|
|
101
|
+
const ciphertext = bundle.slice(13, bundle.length - 16);
|
|
102
|
+
try {
|
|
103
|
+
const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', dataKey, nonce);
|
|
104
|
+
decipher.setAuthTag(authTag);
|
|
105
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
106
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function encrypt(key, variant, data) {
|
|
113
|
+
if (variant === 'legacy') {
|
|
114
|
+
return encryptLegacy(data, key);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
return encryptWithDataKey(data, key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function decrypt(key, variant, data) {
|
|
121
|
+
if (variant === 'legacy') {
|
|
122
|
+
return decryptLegacy(data, key);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
return decryptWithDataKey(data, key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function authChallenge(secret) {
|
|
129
|
+
const keypair = tweetnacl_1.default.sign.keyPair.fromSeed(secret);
|
|
130
|
+
const challenge = getRandomBytes(32);
|
|
131
|
+
const signature = tweetnacl_1.default.sign.detached(challenge, keypair.secretKey);
|
|
132
|
+
return { challenge, publicKey: keypair.publicKey, signature };
|
|
133
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ekkOS_synk — Remote session sync for Claude Code
|
|
3
|
+
*/
|
|
4
|
+
export { synkConfig } from './config';
|
|
5
|
+
export * from './encryption';
|
|
6
|
+
export * from './auth';
|
|
7
|
+
export * from './persistence';
|
|
8
|
+
export * from './types';
|
|
9
|
+
export { displayQRCode } from './qr';
|
|
10
|
+
export { ApiClient } from './api';
|
|
11
|
+
export { SessionClient } from './session-client';
|
|
12
|
+
export { MachineClient } from './machine-client';
|
|
13
|
+
export { SynkSessionBridge } from './session-bridge';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ekkOS_synk — Remote session sync for Claude Code
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
17
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.SynkSessionBridge = exports.MachineClient = exports.SessionClient = exports.ApiClient = exports.displayQRCode = exports.synkConfig = void 0;
|
|
21
|
+
var config_1 = require("./config");
|
|
22
|
+
Object.defineProperty(exports, "synkConfig", { enumerable: true, get: function () { return config_1.synkConfig; } });
|
|
23
|
+
__exportStar(require("./encryption"), exports);
|
|
24
|
+
__exportStar(require("./auth"), exports);
|
|
25
|
+
__exportStar(require("./persistence"), exports);
|
|
26
|
+
__exportStar(require("./types"), exports);
|
|
27
|
+
var qr_1 = require("./qr");
|
|
28
|
+
Object.defineProperty(exports, "displayQRCode", { enumerable: true, get: function () { return qr_1.displayQRCode; } });
|
|
29
|
+
var api_1 = require("./api");
|
|
30
|
+
Object.defineProperty(exports, "ApiClient", { enumerable: true, get: function () { return api_1.ApiClient; } });
|
|
31
|
+
var session_client_1 = require("./session-client");
|
|
32
|
+
Object.defineProperty(exports, "SessionClient", { enumerable: true, get: function () { return session_client_1.SessionClient; } });
|
|
33
|
+
var machine_client_1 = require("./machine-client");
|
|
34
|
+
Object.defineProperty(exports, "MachineClient", { enumerable: true, get: function () { return machine_client_1.MachineClient; } });
|
|
35
|
+
var session_bridge_1 = require("./session-bridge");
|
|
36
|
+
Object.defineProperty(exports, "SynkSessionBridge", { enumerable: true, get: function () { return session_bridge_1.SynkSessionBridge; } });
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client for machine/daemon communication with synk-server
|
|
3
|
+
* Manages keep-alive heartbeat, daemon state updates, and RPC handler registration
|
|
4
|
+
*
|
|
5
|
+
* This is SEPARATE from SessionClient — SessionClient is per-session, MachineClient is per-daemon.
|
|
6
|
+
*/
|
|
7
|
+
import type { MachineMetadata, DaemonState, Machine } from './types';
|
|
8
|
+
export interface SpawnSessionOptions {
|
|
9
|
+
machineId?: string;
|
|
10
|
+
directory: string;
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
environmentVariables?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export type SpawnSessionResult = {
|
|
15
|
+
type: 'success';
|
|
16
|
+
sessionId: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'error';
|
|
19
|
+
errorMessage: string;
|
|
20
|
+
};
|
|
21
|
+
interface MachineRpcHandlers {
|
|
22
|
+
spawnSession: (options: SpawnSessionOptions) => Promise<SpawnSessionResult>;
|
|
23
|
+
stopSession: (sessionId: string) => boolean;
|
|
24
|
+
requestShutdown: () => void;
|
|
25
|
+
}
|
|
26
|
+
export declare class MachineClient {
|
|
27
|
+
private readonly token;
|
|
28
|
+
private readonly machine;
|
|
29
|
+
private socket;
|
|
30
|
+
private keepAliveInterval;
|
|
31
|
+
private rpcHandlerManager;
|
|
32
|
+
private log;
|
|
33
|
+
constructor(token: string, machine: Machine, logger?: (msg: string, ...args: any[]) => void);
|
|
34
|
+
setRPCHandlers({ spawnSession, stopSession, requestShutdown }: MachineRpcHandlers): void;
|
|
35
|
+
updateMachineMetadata(handler: (metadata: MachineMetadata | null) => MachineMetadata): Promise<void>;
|
|
36
|
+
updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise<void>;
|
|
37
|
+
connect(): void;
|
|
38
|
+
private startKeepAlive;
|
|
39
|
+
private stopKeepAlive;
|
|
40
|
+
shutdown(): void;
|
|
41
|
+
}
|
|
42
|
+
export {};
|