@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.
Files changed (82) hide show
  1. package/dist/commands/dashboard.js +147 -57
  2. package/dist/commands/init.d.ts +1 -0
  3. package/dist/commands/init.js +54 -16
  4. package/dist/commands/run.js +163 -44
  5. package/dist/commands/status.d.ts +4 -1
  6. package/dist/commands/status.js +165 -27
  7. package/dist/commands/synk.d.ts +7 -0
  8. package/dist/commands/synk.js +339 -0
  9. package/dist/deploy/settings.d.ts +6 -5
  10. package/dist/deploy/settings.js +27 -17
  11. package/dist/index.js +12 -82
  12. package/dist/lib/usage-parser.d.ts +1 -1
  13. package/dist/lib/usage-parser.js +5 -3
  14. package/dist/local/index.d.ts +14 -0
  15. package/dist/local/index.js +28 -0
  16. package/dist/local/local-embeddings.d.ts +49 -0
  17. package/dist/local/local-embeddings.js +232 -0
  18. package/dist/local/offline-fallback.d.ts +44 -0
  19. package/dist/local/offline-fallback.js +159 -0
  20. package/dist/local/sqlite-store.d.ts +126 -0
  21. package/dist/local/sqlite-store.js +393 -0
  22. package/dist/local/sync-engine.d.ts +42 -0
  23. package/dist/local/sync-engine.js +223 -0
  24. package/dist/synk/api.d.ts +22 -0
  25. package/dist/synk/api.js +133 -0
  26. package/dist/synk/auth.d.ts +7 -0
  27. package/dist/synk/auth.js +30 -0
  28. package/dist/synk/config.d.ts +18 -0
  29. package/dist/synk/config.js +37 -0
  30. package/dist/synk/daemon/control-client.d.ts +11 -0
  31. package/dist/synk/daemon/control-client.js +101 -0
  32. package/dist/synk/daemon/control-server.d.ts +24 -0
  33. package/dist/synk/daemon/control-server.js +91 -0
  34. package/dist/synk/daemon/run.d.ts +14 -0
  35. package/dist/synk/daemon/run.js +338 -0
  36. package/dist/synk/encryption.d.ts +17 -0
  37. package/dist/synk/encryption.js +133 -0
  38. package/dist/synk/index.d.ts +13 -0
  39. package/dist/synk/index.js +36 -0
  40. package/dist/synk/machine-client.d.ts +42 -0
  41. package/dist/synk/machine-client.js +218 -0
  42. package/dist/synk/persistence.d.ts +51 -0
  43. package/dist/synk/persistence.js +211 -0
  44. package/dist/synk/qr.d.ts +5 -0
  45. package/dist/synk/qr.js +33 -0
  46. package/dist/synk/session-bridge.d.ts +58 -0
  47. package/dist/synk/session-bridge.js +171 -0
  48. package/dist/synk/session-client.d.ts +46 -0
  49. package/dist/synk/session-client.js +240 -0
  50. package/dist/synk/types.d.ts +574 -0
  51. package/dist/synk/types.js +74 -0
  52. package/dist/utils/platform.d.ts +5 -1
  53. package/dist/utils/platform.js +24 -4
  54. package/dist/utils/proxy-url.d.ts +10 -0
  55. package/dist/utils/proxy-url.js +19 -0
  56. package/dist/utils/state.d.ts +1 -1
  57. package/dist/utils/state.js +11 -3
  58. package/package.json +13 -4
  59. package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +0 -819
  60. package/templates/claude-plugins-admin/README.md +0 -446
  61. package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +0 -8
  62. package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +0 -595
  63. package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +0 -8
  64. package/templates/claude-plugins-admin/backend-agent/commands/backend.md +0 -798
  65. package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +0 -8
  66. package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +0 -554
  67. package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +0 -8
  68. package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +0 -881
  69. package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +0 -8
  70. package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +0 -85
  71. package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +0 -8
  72. package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +0 -569
  73. package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +0 -8
  74. package/templates/claude-plugins-admin/qa-agent/commands/qa.md +0 -863
  75. package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +0 -8
  76. package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +0 -732
  77. package/templates/hooks-node/lib/state.js +0 -187
  78. package/templates/hooks-node/stop.js +0 -416
  79. package/templates/hooks-node/user-prompt-submit.js +0 -337
  80. package/templates/rules/00-hooks-contract.mdc +0 -89
  81. package/templates/rules/30-ekkos-core.mdc +0 -188
  82. package/templates/rules/31-ekkos-messages.mdc +0 -78
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ /**
3
+ * WebSocket client for machine/daemon communication with synk-server
4
+ * Manages keep-alive heartbeat, daemon state updates, and RPC handler registration
5
+ *
6
+ * This is SEPARATE from SessionClient — SessionClient is per-session, MachineClient is per-daemon.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.MachineClient = void 0;
10
+ const socket_io_client_1 = require("socket.io-client");
11
+ const config_1 = require("./config");
12
+ const encryption_1 = require("./encryption");
13
+ class RpcHandlerManager {
14
+ constructor(scopePrefix, encryptionKey, encryptionVariant, log) {
15
+ this.scopePrefix = scopePrefix;
16
+ this.encryptionKey = encryptionKey;
17
+ this.encryptionVariant = encryptionVariant;
18
+ this.log = log;
19
+ this.handlers = new Map();
20
+ this.socket = null;
21
+ }
22
+ registerHandler(method, handler) {
23
+ const prefixed = `${this.scopePrefix}:${method}`;
24
+ this.handlers.set(prefixed, handler);
25
+ if (this.socket) {
26
+ this.socket.emit('rpc-register', { method: prefixed });
27
+ }
28
+ }
29
+ async handleRequest(request) {
30
+ try {
31
+ const handler = this.handlers.get(request.method);
32
+ if (!handler) {
33
+ this.log('[RPC] Method not found:', request.method);
34
+ return (0, encryption_1.encodeBase64)((0, encryption_1.encrypt)(this.encryptionKey, this.encryptionVariant, { error: 'Method not found' }));
35
+ }
36
+ const params = (0, encryption_1.decrypt)(this.encryptionKey, this.encryptionVariant, (0, encryption_1.decodeBase64)(request.params));
37
+ this.log('[RPC] Calling handler:', request.method);
38
+ const result = await handler(params);
39
+ return (0, encryption_1.encodeBase64)((0, encryption_1.encrypt)(this.encryptionKey, this.encryptionVariant, result));
40
+ }
41
+ catch (error) {
42
+ this.log('[RPC] Error:', error);
43
+ return (0, encryption_1.encodeBase64)((0, encryption_1.encrypt)(this.encryptionKey, this.encryptionVariant, {
44
+ error: error instanceof Error ? error.message : 'Unknown error',
45
+ }));
46
+ }
47
+ }
48
+ onSocketConnect(socket) {
49
+ this.socket = socket;
50
+ for (const [method] of this.handlers) {
51
+ socket.emit('rpc-register', { method });
52
+ }
53
+ }
54
+ onSocketDisconnect() {
55
+ this.socket = null;
56
+ }
57
+ }
58
+ // --- MachineClient ---
59
+ class MachineClient {
60
+ constructor(token, machine, logger) {
61
+ this.token = token;
62
+ this.machine = machine;
63
+ this.keepAliveInterval = null;
64
+ this.log = logger || ((...args) => console.log('[MACHINE]', ...args));
65
+ this.rpcHandlerManager = new RpcHandlerManager(this.machine.id, this.machine.encryptionKey, this.machine.encryptionVariant, this.log);
66
+ }
67
+ setRPCHandlers({ spawnSession, stopSession, requestShutdown }) {
68
+ this.rpcHandlerManager.registerHandler('spawn-synk-session', async (params) => {
69
+ const { directory, sessionId, machineId, environmentVariables } = params || {};
70
+ this.log('Spawning session:', JSON.stringify(params));
71
+ if (!directory) {
72
+ throw new Error('Directory is required');
73
+ }
74
+ const result = await spawnSession({ directory, sessionId, machineId, environmentVariables });
75
+ switch (result.type) {
76
+ case 'success':
77
+ this.log('Spawned session:', result.sessionId);
78
+ return { type: 'success', sessionId: result.sessionId };
79
+ case 'error':
80
+ throw new Error(result.errorMessage);
81
+ }
82
+ });
83
+ this.rpcHandlerManager.registerHandler('stop-session', (params) => {
84
+ const { sessionId } = params || {};
85
+ if (!sessionId)
86
+ throw new Error('Session ID is required');
87
+ const success = stopSession(sessionId);
88
+ if (!success)
89
+ throw new Error('Session not found or failed to stop');
90
+ this.log('Stopped session:', sessionId);
91
+ return { message: 'Session stopped' };
92
+ });
93
+ this.rpcHandlerManager.registerHandler('stop-daemon', () => {
94
+ this.log('Received stop-daemon RPC');
95
+ setTimeout(() => requestShutdown(), 100);
96
+ return { message: 'Daemon shutdown initiated' };
97
+ });
98
+ }
99
+ async updateMachineMetadata(handler) {
100
+ const maxRetries = 3;
101
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
102
+ const updated = handler(this.machine.metadata);
103
+ const answer = await this.socket.emitWithAck('machine-update-metadata', {
104
+ machineId: this.machine.id,
105
+ metadata: (0, encryption_1.encodeBase64)((0, encryption_1.encrypt)(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
106
+ expectedVersion: this.machine.metadataVersion,
107
+ });
108
+ if (answer.result === 'success') {
109
+ this.machine.metadata = (0, encryption_1.decrypt)(this.machine.encryptionKey, this.machine.encryptionVariant, (0, encryption_1.decodeBase64)(answer.metadata));
110
+ this.machine.metadataVersion = answer.version;
111
+ return;
112
+ }
113
+ if (answer.result === 'version-mismatch' && answer.version > this.machine.metadataVersion) {
114
+ this.machine.metadataVersion = answer.version;
115
+ this.machine.metadata = (0, encryption_1.decrypt)(this.machine.encryptionKey, this.machine.encryptionVariant, (0, encryption_1.decodeBase64)(answer.metadata));
116
+ }
117
+ if (attempt < maxRetries - 1) {
118
+ await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
119
+ }
120
+ }
121
+ }
122
+ async updateDaemonState(handler) {
123
+ const maxRetries = 3;
124
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
125
+ const updated = handler(this.machine.daemonState);
126
+ const answer = await this.socket.emitWithAck('machine-update-state', {
127
+ machineId: this.machine.id,
128
+ daemonState: (0, encryption_1.encodeBase64)((0, encryption_1.encrypt)(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
129
+ expectedVersion: this.machine.daemonStateVersion,
130
+ });
131
+ if (answer.result === 'success') {
132
+ this.machine.daemonState = (0, encryption_1.decrypt)(this.machine.encryptionKey, this.machine.encryptionVariant, (0, encryption_1.decodeBase64)(answer.daemonState));
133
+ this.machine.daemonStateVersion = answer.version;
134
+ return;
135
+ }
136
+ if (answer.result === 'version-mismatch' && answer.version > this.machine.daemonStateVersion) {
137
+ this.machine.daemonStateVersion = answer.version;
138
+ this.machine.daemonState = (0, encryption_1.decrypt)(this.machine.encryptionKey, this.machine.encryptionVariant, (0, encryption_1.decodeBase64)(answer.daemonState));
139
+ }
140
+ if (attempt < maxRetries - 1) {
141
+ await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
142
+ }
143
+ }
144
+ }
145
+ connect() {
146
+ this.log('Connecting to', config_1.synkConfig.serverUrl);
147
+ this.socket = (0, socket_io_client_1.io)(config_1.synkConfig.serverUrl, {
148
+ transports: ['websocket'],
149
+ auth: {
150
+ token: this.token,
151
+ clientType: 'machine-scoped',
152
+ machineId: this.machine.id,
153
+ },
154
+ path: '/v1/updates',
155
+ reconnection: true,
156
+ reconnectionDelay: 1000,
157
+ reconnectionDelayMax: 5000,
158
+ });
159
+ this.socket.on('connect', () => {
160
+ this.log('Connected to synk-server');
161
+ this.updateDaemonState((state) => ({
162
+ ...state,
163
+ status: 'running',
164
+ pid: process.pid,
165
+ httpPort: this.machine.daemonState?.httpPort,
166
+ startedAt: Date.now(),
167
+ })).catch(err => this.log('Failed to update daemon state:', err));
168
+ this.rpcHandlerManager.onSocketConnect(this.socket);
169
+ this.startKeepAlive();
170
+ });
171
+ this.socket.on('disconnect', () => {
172
+ this.log('Disconnected from synk-server');
173
+ this.rpcHandlerManager.onSocketDisconnect();
174
+ this.stopKeepAlive();
175
+ });
176
+ this.socket.on('rpc-request', async (data, callback) => {
177
+ callback(await this.rpcHandlerManager.handleRequest(data));
178
+ });
179
+ this.socket.on('update', (data) => {
180
+ if (data.body?.t === 'update-machine' && data.body.machineId === this.machine.id) {
181
+ if (data.body.metadata) {
182
+ this.machine.metadata = (0, encryption_1.decrypt)(this.machine.encryptionKey, this.machine.encryptionVariant, (0, encryption_1.decodeBase64)(data.body.metadata.value));
183
+ this.machine.metadataVersion = data.body.metadata.version;
184
+ }
185
+ if (data.body.daemonState) {
186
+ this.machine.daemonState = (0, encryption_1.decrypt)(this.machine.encryptionKey, this.machine.encryptionVariant, (0, encryption_1.decodeBase64)(data.body.daemonState.value));
187
+ this.machine.daemonStateVersion = data.body.daemonState.version;
188
+ }
189
+ }
190
+ });
191
+ this.socket.on('connect_error', (error) => {
192
+ this.log('Connection error:', error.message);
193
+ });
194
+ }
195
+ startKeepAlive() {
196
+ this.stopKeepAlive();
197
+ this.keepAliveInterval = setInterval(() => {
198
+ this.socket.emit('machine-alive', {
199
+ machineId: this.machine.id,
200
+ time: Date.now(),
201
+ });
202
+ }, 20000);
203
+ }
204
+ stopKeepAlive() {
205
+ if (this.keepAliveInterval) {
206
+ clearInterval(this.keepAliveInterval);
207
+ this.keepAliveInterval = null;
208
+ }
209
+ }
210
+ shutdown() {
211
+ this.log('Shutting down');
212
+ this.stopKeepAlive();
213
+ if (this.socket) {
214
+ this.socket.close();
215
+ }
216
+ }
217
+ }
218
+ exports.MachineClient = MachineClient;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Persistence for ekkOS_synk — credentials, settings, daemon state
3
+ */
4
+ import { FileHandle } from 'node:fs/promises';
5
+ /** Daemon state persisted locally */
6
+ export interface DaemonLocallyPersistedState {
7
+ pid: number;
8
+ httpPort: number;
9
+ startTime: string;
10
+ startedWithCliVersion: string;
11
+ lastHeartbeat?: string;
12
+ daemonLogPath?: string;
13
+ }
14
+ interface Settings {
15
+ schemaVersion: number;
16
+ onboardingCompleted: boolean;
17
+ machineId?: string;
18
+ machineIdConfirmedByServer?: boolean;
19
+ daemonAutoStart?: boolean;
20
+ }
21
+ export type Credentials = {
22
+ token: string;
23
+ encryption: {
24
+ type: 'legacy';
25
+ secret: Uint8Array;
26
+ } | {
27
+ type: 'dataKey';
28
+ publicKey: Uint8Array;
29
+ machineKey: Uint8Array;
30
+ };
31
+ };
32
+ export declare function readCredentials(): Promise<Credentials | null>;
33
+ export declare function writeCredentialsLegacy(credentials: {
34
+ secret: Uint8Array;
35
+ token: string;
36
+ }): Promise<void>;
37
+ export declare function writeCredentialsDataKey(credentials: {
38
+ publicKey: Uint8Array;
39
+ machineKey: Uint8Array;
40
+ token: string;
41
+ }): Promise<void>;
42
+ export declare function clearCredentials(): Promise<void>;
43
+ export declare function readSettings(): Promise<Settings>;
44
+ export declare function writeSettings(settings: Settings): Promise<void>;
45
+ export declare function updateSettings(updater: (current: Settings) => Settings | Promise<Settings>): Promise<Settings>;
46
+ export declare function readDaemonState(): Promise<DaemonLocallyPersistedState | null>;
47
+ export declare function writeDaemonState(state: DaemonLocallyPersistedState): void;
48
+ export declare function clearDaemonState(): Promise<void>;
49
+ export declare function acquireDaemonLock(maxAttempts?: number, delayIncrementMs?: number): Promise<FileHandle | null>;
50
+ export declare function releaseDaemonLock(lockHandle: FileHandle): Promise<void>;
51
+ export {};
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ /**
3
+ * Persistence for ekkOS_synk — credentials, settings, daemon state
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readCredentials = readCredentials;
7
+ exports.writeCredentialsLegacy = writeCredentialsLegacy;
8
+ exports.writeCredentialsDataKey = writeCredentialsDataKey;
9
+ exports.clearCredentials = clearCredentials;
10
+ exports.readSettings = readSettings;
11
+ exports.writeSettings = writeSettings;
12
+ exports.updateSettings = updateSettings;
13
+ exports.readDaemonState = readDaemonState;
14
+ exports.writeDaemonState = writeDaemonState;
15
+ exports.clearDaemonState = clearDaemonState;
16
+ exports.acquireDaemonLock = acquireDaemonLock;
17
+ exports.releaseDaemonLock = releaseDaemonLock;
18
+ const promises_1 = require("node:fs/promises");
19
+ const node_fs_1 = require("node:fs");
20
+ const node_fs_2 = require("node:fs");
21
+ const zod_1 = require("zod");
22
+ const config_1 = require("./config");
23
+ const encryption_1 = require("./encryption");
24
+ const defaultSettings = {
25
+ schemaVersion: 1,
26
+ onboardingCompleted: false,
27
+ };
28
+ // Credentials
29
+ const credentialsSchema = zod_1.z.object({
30
+ token: zod_1.z.string(),
31
+ secret: zod_1.z.string().base64().nullish(),
32
+ encryption: zod_1.z.object({
33
+ publicKey: zod_1.z.string().base64(),
34
+ machineKey: zod_1.z.string().base64(),
35
+ }).nullish(),
36
+ });
37
+ async function readCredentials() {
38
+ if (!(0, node_fs_1.existsSync)(config_1.synkConfig.credentialsFile))
39
+ return null;
40
+ try {
41
+ const raw = JSON.parse(await (0, promises_1.readFile)(config_1.synkConfig.credentialsFile, 'utf8'));
42
+ const credentials = credentialsSchema.parse(raw);
43
+ if (credentials.secret) {
44
+ return {
45
+ token: credentials.token,
46
+ encryption: { type: 'legacy', secret: new Uint8Array(Buffer.from(credentials.secret, 'base64')) },
47
+ };
48
+ }
49
+ else if (credentials.encryption) {
50
+ return {
51
+ token: credentials.token,
52
+ encryption: {
53
+ type: 'dataKey',
54
+ publicKey: new Uint8Array(Buffer.from(credentials.encryption.publicKey, 'base64')),
55
+ machineKey: new Uint8Array(Buffer.from(credentials.encryption.machineKey, 'base64')),
56
+ },
57
+ };
58
+ }
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ return null;
64
+ }
65
+ async function writeCredentialsLegacy(credentials) {
66
+ config_1.synkConfig.ensureDirectories();
67
+ await (0, promises_1.writeFile)(config_1.synkConfig.credentialsFile, JSON.stringify({
68
+ secret: (0, encryption_1.encodeBase64)(credentials.secret),
69
+ token: credentials.token,
70
+ }, null, 2));
71
+ }
72
+ async function writeCredentialsDataKey(credentials) {
73
+ config_1.synkConfig.ensureDirectories();
74
+ await (0, promises_1.writeFile)(config_1.synkConfig.credentialsFile, JSON.stringify({
75
+ encryption: {
76
+ publicKey: (0, encryption_1.encodeBase64)(credentials.publicKey),
77
+ machineKey: (0, encryption_1.encodeBase64)(credentials.machineKey),
78
+ },
79
+ token: credentials.token,
80
+ }, null, 2));
81
+ }
82
+ async function clearCredentials() {
83
+ if ((0, node_fs_1.existsSync)(config_1.synkConfig.credentialsFile)) {
84
+ await (0, promises_1.unlink)(config_1.synkConfig.credentialsFile);
85
+ }
86
+ }
87
+ // Settings
88
+ async function readSettings() {
89
+ if (!(0, node_fs_1.existsSync)(config_1.synkConfig.settingsFile))
90
+ return { ...defaultSettings };
91
+ try {
92
+ const raw = JSON.parse(await (0, promises_1.readFile)(config_1.synkConfig.settingsFile, 'utf8'));
93
+ return { ...defaultSettings, ...raw };
94
+ }
95
+ catch {
96
+ return { ...defaultSettings };
97
+ }
98
+ }
99
+ async function writeSettings(settings) {
100
+ config_1.synkConfig.ensureDirectories();
101
+ await (0, promises_1.writeFile)(config_1.synkConfig.settingsFile, JSON.stringify(settings, null, 2));
102
+ }
103
+ async function updateSettings(updater) {
104
+ const lockFile = config_1.synkConfig.settingsFile + '.lock';
105
+ const tmpFile = config_1.synkConfig.settingsFile + '.tmp';
106
+ let fileHandle;
107
+ let attempts = 0;
108
+ while (attempts < 50) {
109
+ try {
110
+ fileHandle = await (0, promises_1.open)(lockFile, node_fs_2.constants.O_CREAT | node_fs_2.constants.O_EXCL | node_fs_2.constants.O_WRONLY);
111
+ break;
112
+ }
113
+ catch (err) {
114
+ if (err.code === 'EEXIST') {
115
+ attempts++;
116
+ await new Promise(resolve => setTimeout(resolve, 100));
117
+ try {
118
+ const stats = await (0, promises_1.stat)(lockFile);
119
+ if (Date.now() - stats.mtimeMs > 10000) {
120
+ await (0, promises_1.unlink)(lockFile).catch(() => { });
121
+ }
122
+ }
123
+ catch { }
124
+ }
125
+ else {
126
+ throw err;
127
+ }
128
+ }
129
+ }
130
+ if (!fileHandle) {
131
+ throw new Error('Failed to acquire settings lock after 5 seconds');
132
+ }
133
+ try {
134
+ const current = await readSettings();
135
+ const updated = await updater(current);
136
+ config_1.synkConfig.ensureDirectories();
137
+ await (0, promises_1.writeFile)(tmpFile, JSON.stringify(updated, null, 2));
138
+ await (0, promises_1.rename)(tmpFile, config_1.synkConfig.settingsFile);
139
+ return updated;
140
+ }
141
+ finally {
142
+ await fileHandle.close();
143
+ await (0, promises_1.unlink)(lockFile).catch(() => { });
144
+ }
145
+ }
146
+ // Daemon state
147
+ async function readDaemonState() {
148
+ try {
149
+ if (!(0, node_fs_1.existsSync)(config_1.synkConfig.daemonStateFile))
150
+ return null;
151
+ return JSON.parse(await (0, promises_1.readFile)(config_1.synkConfig.daemonStateFile, 'utf-8'));
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ function writeDaemonState(state) {
158
+ (0, node_fs_1.writeFileSync)(config_1.synkConfig.daemonStateFile, JSON.stringify(state, null, 2), 'utf-8');
159
+ }
160
+ async function clearDaemonState() {
161
+ if ((0, node_fs_1.existsSync)(config_1.synkConfig.daemonStateFile)) {
162
+ await (0, promises_1.unlink)(config_1.synkConfig.daemonStateFile);
163
+ }
164
+ if ((0, node_fs_1.existsSync)(config_1.synkConfig.daemonLockFile)) {
165
+ try {
166
+ await (0, promises_1.unlink)(config_1.synkConfig.daemonLockFile);
167
+ }
168
+ catch { }
169
+ }
170
+ }
171
+ async function acquireDaemonLock(maxAttempts = 5, delayIncrementMs = 200) {
172
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
173
+ try {
174
+ const fileHandle = await (0, promises_1.open)(config_1.synkConfig.daemonLockFile, node_fs_2.constants.O_CREAT | node_fs_2.constants.O_EXCL | node_fs_2.constants.O_WRONLY);
175
+ await fileHandle.writeFile(String(process.pid));
176
+ return fileHandle;
177
+ }
178
+ catch (error) {
179
+ if (error.code === 'EEXIST') {
180
+ try {
181
+ const lockPid = (0, node_fs_1.readFileSync)(config_1.synkConfig.daemonLockFile, 'utf-8').trim();
182
+ if (lockPid && !isNaN(Number(lockPid))) {
183
+ try {
184
+ process.kill(Number(lockPid), 0);
185
+ }
186
+ catch {
187
+ (0, node_fs_1.unlinkSync)(config_1.synkConfig.daemonLockFile);
188
+ continue;
189
+ }
190
+ }
191
+ }
192
+ catch { }
193
+ }
194
+ if (attempt === maxAttempts)
195
+ return null;
196
+ await new Promise(resolve => setTimeout(resolve, attempt * delayIncrementMs));
197
+ }
198
+ }
199
+ return null;
200
+ }
201
+ async function releaseDaemonLock(lockHandle) {
202
+ try {
203
+ await lockHandle.close();
204
+ }
205
+ catch { }
206
+ try {
207
+ if ((0, node_fs_1.existsSync)(config_1.synkConfig.daemonLockFile))
208
+ (0, node_fs_1.unlinkSync)(config_1.synkConfig.daemonLockFile);
209
+ }
210
+ catch { }
211
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * QR code display for ekkOS_synk mobile pairing
3
+ */
4
+ /** Display a QR code in the terminal for mobile device pairing */
5
+ export declare function displayQRCode(url: string): void;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * QR code display for ekkOS_synk mobile pairing
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.displayQRCode = displayQRCode;
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ /** Display a QR code in the terminal for mobile device pairing */
12
+ function displayQRCode(url) {
13
+ let qrcode;
14
+ try {
15
+ qrcode = require('qrcode-terminal');
16
+ }
17
+ catch {
18
+ console.log(chalk_1.default.yellow(' qrcode-terminal not installed. Install it for QR code display.'));
19
+ console.log(chalk_1.default.gray(` Pairing URL: ${url}`));
20
+ return;
21
+ }
22
+ console.log('');
23
+ console.log(chalk_1.default.cyan('═'.repeat(60)));
24
+ console.log(chalk_1.default.cyan.bold(' ekkOS_synk') + chalk_1.default.gray(' — Scan to pair your mobile device'));
25
+ console.log(chalk_1.default.cyan('═'.repeat(60)));
26
+ qrcode.generate(url, { small: true }, (qr) => {
27
+ for (const line of qr.split('\n')) {
28
+ console.log(' '.repeat(8) + line);
29
+ }
30
+ });
31
+ console.log(chalk_1.default.cyan('═'.repeat(60)));
32
+ console.log('');
33
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * SynkSessionBridge — links `ekkos run` to synk real-time sync
3
+ *
4
+ * Lifecycle:
5
+ * create() → reads creds, returns null if synk disabled
6
+ * onSessionEstablished → creates synk session + WS client
7
+ * onAgentOutput → batched forwarding of PTY output
8
+ * onIdleChange → keepalive / thinking state
9
+ * onTurnEnd → flush output buffer
10
+ * shutdown → graceful close
11
+ */
12
+ import { EventEmitter } from 'node:events';
13
+ export interface SynkBridgeOptions {
14
+ cwd: string;
15
+ verbose?: boolean;
16
+ }
17
+ export declare class SynkSessionBridge extends EventEmitter {
18
+ private readonly credential;
19
+ private readonly apiClient;
20
+ private readonly cwd;
21
+ private readonly verbose;
22
+ private sessionClient;
23
+ private synkSession;
24
+ private outputBuffer;
25
+ private flushTimer;
26
+ private established;
27
+ private closed;
28
+ private constructor();
29
+ /**
30
+ * Factory — returns null when synk is not configured (zero-cost path).
31
+ */
32
+ static create(opts: SynkBridgeOptions): Promise<SynkSessionBridge | null>;
33
+ /**
34
+ * Called once after Claude Code's session name is detected from the status line.
35
+ * Creates the synk session via REST, then opens a SessionClient WebSocket.
36
+ */
37
+ onSessionEstablished(sessionName: string, sessionId: string, extra?: {
38
+ hostPid?: number;
39
+ }): Promise<void>;
40
+ /**
41
+ * Called on every PTY data chunk. Buffers and flushes every 200ms.
42
+ */
43
+ onAgentOutput(data: string): void;
44
+ /**
45
+ * Called when Claude transitions between idle <-> active.
46
+ */
47
+ onIdleChange(idle: boolean): void;
48
+ /**
49
+ * Called at turn boundaries — flush any pending output immediately.
50
+ */
51
+ onTurnEnd(): void;
52
+ /**
53
+ * Graceful shutdown — send death event, flush, close socket.
54
+ */
55
+ shutdown(): Promise<void>;
56
+ private flushOutputBuffer;
57
+ private log;
58
+ }