happy-coder 0.6.3 → 0.7.1-beta.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/dist/index.cjs +1623 -774
- package/dist/index.mjs +1631 -782
- package/dist/lib.cjs +3 -11
- package/dist/lib.d.cts +162 -14
- package/dist/lib.d.mts +162 -14
- package/dist/lib.mjs +1 -1
- package/dist/{types-Dz5kZrVh.mjs → types-BZC9-exR.mjs} +413 -43
- package/dist/{types-BDtHM1DY.cjs → types-CzvFvJwf.cjs} +458 -89
- package/package.json +16 -8
|
@@ -14,40 +14,32 @@ import { Expo } from 'expo-server-sdk';
|
|
|
14
14
|
|
|
15
15
|
class Configuration {
|
|
16
16
|
serverUrl;
|
|
17
|
-
installationLocation;
|
|
18
17
|
isDaemonProcess;
|
|
19
18
|
// Directories and paths (from persistence)
|
|
20
|
-
|
|
19
|
+
happyHomeDir;
|
|
21
20
|
logsDir;
|
|
22
21
|
daemonLogsDir;
|
|
23
22
|
settingsFile;
|
|
24
23
|
privateKeyFile;
|
|
25
|
-
|
|
26
|
-
constructor(
|
|
27
|
-
this.serverUrl = process.env.
|
|
24
|
+
daemonStateFile;
|
|
25
|
+
constructor() {
|
|
26
|
+
this.serverUrl = process.env.HAPPY_SERVER_URL || "https://handy-api.korshakov.org";
|
|
28
27
|
const args = process.argv.slice(2);
|
|
29
|
-
this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" &&
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
this.
|
|
33
|
-
} else if (location === "global") {
|
|
34
|
-
this.happyDir = join(homedir(), ".happy");
|
|
35
|
-
this.installationLocation = "global";
|
|
28
|
+
this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
|
|
29
|
+
if (process.env.HAPPY_HOME_DIR) {
|
|
30
|
+
const expandedPath = process.env.HAPPY_HOME_DIR.replace(/^~/, homedir());
|
|
31
|
+
this.happyHomeDir = expandedPath;
|
|
36
32
|
} else {
|
|
37
|
-
this.
|
|
38
|
-
this.installationLocation = "global";
|
|
33
|
+
this.happyHomeDir = join(homedir(), ".happy");
|
|
39
34
|
}
|
|
40
|
-
this.logsDir = join(this.
|
|
41
|
-
this.daemonLogsDir = join(this.
|
|
42
|
-
this.settingsFile = join(this.
|
|
43
|
-
this.privateKeyFile = join(this.
|
|
44
|
-
this.
|
|
35
|
+
this.logsDir = join(this.happyHomeDir, "logs");
|
|
36
|
+
this.daemonLogsDir = join(this.happyHomeDir, "logs-daemon");
|
|
37
|
+
this.settingsFile = join(this.happyHomeDir, "settings.json");
|
|
38
|
+
this.privateKeyFile = join(this.happyHomeDir, "access.key");
|
|
39
|
+
this.daemonStateFile = join(this.happyHomeDir, "daemon.state.json");
|
|
45
40
|
}
|
|
46
41
|
}
|
|
47
|
-
|
|
48
|
-
function initializeConfiguration(location) {
|
|
49
|
-
configuration = new Configuration(location);
|
|
50
|
-
}
|
|
42
|
+
const configuration = new Configuration();
|
|
51
43
|
|
|
52
44
|
function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
|
|
53
45
|
return date.toLocaleString("sv-SE", {
|
|
@@ -81,7 +73,12 @@ async function getSessionLogPath() {
|
|
|
81
73
|
class Logger {
|
|
82
74
|
constructor(logFilePathPromise = getSessionLogPath()) {
|
|
83
75
|
this.logFilePathPromise = logFilePathPromise;
|
|
76
|
+
if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING && process.env.HAPPY_SERVER_URL) {
|
|
77
|
+
this.dangerouslyUnencryptedServerLoggingUrl = process.env.HAPPY_SERVER_URL;
|
|
78
|
+
console.log(chalk.yellow("[REMOTE LOGGING] Sending logs to server for AI debugging"));
|
|
79
|
+
}
|
|
84
80
|
}
|
|
81
|
+
dangerouslyUnencryptedServerLoggingUrl;
|
|
85
82
|
// Use local timezone for simplicity of locating the logs,
|
|
86
83
|
// in practice you will not need absolute timestamps
|
|
87
84
|
localTimezoneTimestamp() {
|
|
@@ -156,11 +153,38 @@ class Logger {
|
|
|
156
153
|
}
|
|
157
154
|
}
|
|
158
155
|
}
|
|
156
|
+
async sendToRemoteServer(level, message, ...args) {
|
|
157
|
+
if (!this.dangerouslyUnencryptedServerLoggingUrl) return;
|
|
158
|
+
try {
|
|
159
|
+
await fetch(this.dangerouslyUnencryptedServerLoggingUrl + "/logs-combined-from-cli-and-mobile-for-simple-ai-debugging", {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: { "Content-Type": "application/json" },
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
164
|
+
level,
|
|
165
|
+
message: `${message} ${args.map(
|
|
166
|
+
(a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)
|
|
167
|
+
).join(" ")}`,
|
|
168
|
+
source: "cli",
|
|
169
|
+
platform: process.platform
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
}
|
|
174
|
+
}
|
|
159
175
|
logToFile(prefix, message, ...args) {
|
|
160
176
|
const logLine = `${prefix} ${message} ${args.map(
|
|
161
177
|
(arg) => typeof arg === "string" ? arg : JSON.stringify(arg)
|
|
162
178
|
).join(" ")}
|
|
163
179
|
`;
|
|
180
|
+
if (this.dangerouslyUnencryptedServerLoggingUrl) {
|
|
181
|
+
let level = "info";
|
|
182
|
+
if (prefix.includes(this.localTimezoneTimestamp())) {
|
|
183
|
+
level = "debug";
|
|
184
|
+
}
|
|
185
|
+
this.sendToRemoteServer(level, message, ...args).catch(() => {
|
|
186
|
+
});
|
|
187
|
+
}
|
|
164
188
|
this.logFilePathPromise.then((logFilePath) => {
|
|
165
189
|
try {
|
|
166
190
|
appendFileSync(logFilePath, logLine);
|
|
@@ -179,16 +203,7 @@ class Logger {
|
|
|
179
203
|
});
|
|
180
204
|
}
|
|
181
205
|
}
|
|
182
|
-
let logger;
|
|
183
|
-
function initLoggerWithGlobalConfiguration() {
|
|
184
|
-
logger = new Logger();
|
|
185
|
-
if (process.env.DEBUG) {
|
|
186
|
-
logger.logFilePathPromise.then((logPath) => {
|
|
187
|
-
logger.info(chalk.yellow("[DEBUG MODE] Debug logging enabled"));
|
|
188
|
-
logger.info(chalk.gray(`Log file: ${logPath}`));
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
206
|
+
let logger = new Logger();
|
|
192
207
|
|
|
193
208
|
const SessionMessageContentSchema = z.object({
|
|
194
209
|
c: z.string(),
|
|
@@ -217,10 +232,26 @@ const UpdateSessionBodySchema = z.object({
|
|
|
217
232
|
value: z.string()
|
|
218
233
|
}).nullish()
|
|
219
234
|
});
|
|
235
|
+
const UpdateMachineBodySchema = z.object({
|
|
236
|
+
t: z.literal("update-machine"),
|
|
237
|
+
machineId: z.string(),
|
|
238
|
+
metadata: z.object({
|
|
239
|
+
version: z.number(),
|
|
240
|
+
value: z.string()
|
|
241
|
+
}).nullish(),
|
|
242
|
+
daemonState: z.object({
|
|
243
|
+
version: z.number(),
|
|
244
|
+
value: z.string()
|
|
245
|
+
}).nullish()
|
|
246
|
+
});
|
|
220
247
|
z.object({
|
|
221
248
|
id: z.string(),
|
|
222
249
|
seq: z.number(),
|
|
223
|
-
body: z.union([
|
|
250
|
+
body: z.union([
|
|
251
|
+
UpdateBodySchema,
|
|
252
|
+
UpdateSessionBodySchema,
|
|
253
|
+
UpdateMachineBodySchema
|
|
254
|
+
]),
|
|
224
255
|
createdAt: z.number()
|
|
225
256
|
});
|
|
226
257
|
z.object({
|
|
@@ -233,6 +264,44 @@ z.object({
|
|
|
233
264
|
agentState: z.any().nullable(),
|
|
234
265
|
agentStateVersion: z.number()
|
|
235
266
|
});
|
|
267
|
+
z.object({
|
|
268
|
+
host: z.string(),
|
|
269
|
+
platform: z.string(),
|
|
270
|
+
happyCliVersion: z.string(),
|
|
271
|
+
homeDir: z.string(),
|
|
272
|
+
happyHomeDir: z.string()
|
|
273
|
+
});
|
|
274
|
+
z.object({
|
|
275
|
+
status: z.union([
|
|
276
|
+
z.enum(["running", "shutting-down"]),
|
|
277
|
+
z.string()
|
|
278
|
+
// Forward compatibility
|
|
279
|
+
]),
|
|
280
|
+
pid: z.number().optional(),
|
|
281
|
+
httpPort: z.number().optional(),
|
|
282
|
+
startedAt: z.number().optional(),
|
|
283
|
+
shutdownRequestedAt: z.number().optional(),
|
|
284
|
+
shutdownSource: z.union([
|
|
285
|
+
z.enum(["mobile-app", "cli", "os-signal", "unknown"]),
|
|
286
|
+
z.string()
|
|
287
|
+
// Forward compatibility
|
|
288
|
+
]).optional()
|
|
289
|
+
});
|
|
290
|
+
z.object({
|
|
291
|
+
id: z.string(),
|
|
292
|
+
metadata: z.any(),
|
|
293
|
+
// Decrypted MachineMetadata
|
|
294
|
+
metadataVersion: z.number(),
|
|
295
|
+
daemonState: z.any().nullable(),
|
|
296
|
+
// Decrypted DaemonState
|
|
297
|
+
daemonStateVersion: z.number(),
|
|
298
|
+
// We don't really care about these on the CLI for now
|
|
299
|
+
// ApiMachineClient will not sync these
|
|
300
|
+
active: z.boolean(),
|
|
301
|
+
activeAt: z.number(),
|
|
302
|
+
createdAt: z.number(),
|
|
303
|
+
updatedAt: z.number()
|
|
304
|
+
});
|
|
236
305
|
z.object({
|
|
237
306
|
content: SessionMessageContentSchema,
|
|
238
307
|
createdAt: z.number(),
|
|
@@ -323,6 +392,7 @@ function decrypt(data, secret) {
|
|
|
323
392
|
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
324
393
|
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
325
394
|
if (!decrypted) {
|
|
395
|
+
logger.debug("[ERROR] Decryption failed");
|
|
326
396
|
return null;
|
|
327
397
|
}
|
|
328
398
|
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
@@ -489,6 +559,8 @@ class ApiSessionClient extends EventEmitter {
|
|
|
489
559
|
this.agentState = data.body.agentState.value ? decrypt(decodeBase64(data.body.agentState.value), this.secret) : null;
|
|
490
560
|
this.agentStateVersion = data.body.agentState.version;
|
|
491
561
|
}
|
|
562
|
+
} else if (data.body.t === "update-machine") {
|
|
563
|
+
logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
|
|
492
564
|
} else {
|
|
493
565
|
this.emit("message", data.body);
|
|
494
566
|
}
|
|
@@ -708,6 +780,243 @@ class ApiSessionClient extends EventEmitter {
|
|
|
708
780
|
}
|
|
709
781
|
}
|
|
710
782
|
|
|
783
|
+
class ApiMachineClient {
|
|
784
|
+
constructor(token, secret, machine) {
|
|
785
|
+
this.token = token;
|
|
786
|
+
this.secret = secret;
|
|
787
|
+
this.machine = machine;
|
|
788
|
+
}
|
|
789
|
+
socket;
|
|
790
|
+
keepAliveInterval = null;
|
|
791
|
+
// RPC handlers
|
|
792
|
+
spawnSession;
|
|
793
|
+
stopSession;
|
|
794
|
+
requestShutdown;
|
|
795
|
+
setRPCHandlers({
|
|
796
|
+
spawnSession,
|
|
797
|
+
stopSession,
|
|
798
|
+
requestShutdown
|
|
799
|
+
}) {
|
|
800
|
+
this.spawnSession = spawnSession;
|
|
801
|
+
this.stopSession = stopSession;
|
|
802
|
+
this.requestShutdown = requestShutdown;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Update machine metadata
|
|
806
|
+
* Currently unused, changes from the mobile client are more likely
|
|
807
|
+
* for example to set a custom name.
|
|
808
|
+
*/
|
|
809
|
+
async updateMachineMetadata(handler) {
|
|
810
|
+
await backoff(async () => {
|
|
811
|
+
const updated = handler(this.machine.metadata);
|
|
812
|
+
const answer = await this.socket.emitWithAck("machine-update-metadata", {
|
|
813
|
+
machineId: this.machine.id,
|
|
814
|
+
metadata: encodeBase64(encrypt(updated, this.secret)),
|
|
815
|
+
expectedVersion: this.machine.metadataVersion
|
|
816
|
+
});
|
|
817
|
+
if (answer.result === "success") {
|
|
818
|
+
this.machine.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
|
|
819
|
+
this.machine.metadataVersion = answer.version;
|
|
820
|
+
logger.debug("[API MACHINE] Metadata updated successfully");
|
|
821
|
+
} else if (answer.result === "version-mismatch") {
|
|
822
|
+
if (answer.version > this.machine.metadataVersion) {
|
|
823
|
+
this.machine.metadataVersion = answer.version;
|
|
824
|
+
this.machine.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
|
|
825
|
+
}
|
|
826
|
+
throw new Error("Metadata version mismatch");
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Update daemon state (runtime info) - similar to session updateAgentState
|
|
832
|
+
* Simplified without lock - relies on backoff for retry
|
|
833
|
+
*/
|
|
834
|
+
async updateDaemonState(handler) {
|
|
835
|
+
await backoff(async () => {
|
|
836
|
+
const updated = handler(this.machine.daemonState);
|
|
837
|
+
const answer = await this.socket.emitWithAck("machine-update-state", {
|
|
838
|
+
machineId: this.machine.id,
|
|
839
|
+
daemonState: encodeBase64(encrypt(updated, this.secret)),
|
|
840
|
+
expectedVersion: this.machine.daemonStateVersion
|
|
841
|
+
});
|
|
842
|
+
if (answer.result === "success") {
|
|
843
|
+
this.machine.daemonState = decrypt(decodeBase64(answer.daemonState), this.secret);
|
|
844
|
+
this.machine.daemonStateVersion = answer.version;
|
|
845
|
+
logger.debug("[API MACHINE] Daemon state updated successfully");
|
|
846
|
+
} else if (answer.result === "version-mismatch") {
|
|
847
|
+
if (answer.version > this.machine.daemonStateVersion) {
|
|
848
|
+
this.machine.daemonStateVersion = answer.version;
|
|
849
|
+
this.machine.daemonState = decrypt(decodeBase64(answer.daemonState), this.secret);
|
|
850
|
+
}
|
|
851
|
+
throw new Error("Daemon state version mismatch");
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
connect() {
|
|
856
|
+
const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
|
|
857
|
+
logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
|
|
858
|
+
this.socket = io(serverUrl, {
|
|
859
|
+
transports: ["websocket"],
|
|
860
|
+
auth: {
|
|
861
|
+
token: this.token,
|
|
862
|
+
clientType: "machine-scoped",
|
|
863
|
+
machineId: this.machine.id
|
|
864
|
+
},
|
|
865
|
+
path: "/v1/updates",
|
|
866
|
+
reconnection: true,
|
|
867
|
+
reconnectionDelay: 1e3,
|
|
868
|
+
reconnectionDelayMax: 5e3
|
|
869
|
+
});
|
|
870
|
+
const spawnMethod = `${this.machine.id}:spawn-happy-session`;
|
|
871
|
+
const stopMethod = `${this.machine.id}:stop-session`;
|
|
872
|
+
const stopDaemonMethod = `${this.machine.id}:stop-daemon`;
|
|
873
|
+
this.socket.on("connect", () => {
|
|
874
|
+
logger.debug("[API MACHINE] Connected to server");
|
|
875
|
+
this.updateDaemonState((state) => ({
|
|
876
|
+
...state,
|
|
877
|
+
status: "running",
|
|
878
|
+
pid: process.pid,
|
|
879
|
+
httpPort: this.machine.daemonState?.httpPort,
|
|
880
|
+
startedAt: Date.now()
|
|
881
|
+
}));
|
|
882
|
+
this.socket.emit("rpc-register", { method: spawnMethod });
|
|
883
|
+
this.socket.emit("rpc-register", { method: stopMethod });
|
|
884
|
+
this.socket.emit("rpc-register", { method: stopDaemonMethod });
|
|
885
|
+
logger.debug(`[API MACHINE] Registered RPC methods: ${spawnMethod}, ${stopMethod}, ${stopDaemonMethod}`);
|
|
886
|
+
this.startKeepAlive();
|
|
887
|
+
});
|
|
888
|
+
this.socket.on("rpc-request", async (data, callback) => {
|
|
889
|
+
logger.debugLargeJson(`[API MACHINE] Received RPC request:`, data);
|
|
890
|
+
try {
|
|
891
|
+
const spawnMethod2 = `${this.machine.id}:spawn-happy-session`;
|
|
892
|
+
const stopMethod2 = `${this.machine.id}:stop-session`;
|
|
893
|
+
const stopDaemonMethod2 = `${this.machine.id}:stop-daemon`;
|
|
894
|
+
if (data.method === spawnMethod2) {
|
|
895
|
+
if (!this.spawnSession) {
|
|
896
|
+
throw new Error("Spawn session handler not set");
|
|
897
|
+
}
|
|
898
|
+
const { directory, sessionId } = decrypt(decodeBase64(data.params), this.secret) || {};
|
|
899
|
+
if (!directory) {
|
|
900
|
+
throw new Error("Directory is required");
|
|
901
|
+
}
|
|
902
|
+
const session = await this.spawnSession(directory, sessionId);
|
|
903
|
+
if (!session) {
|
|
904
|
+
throw new Error("Failed to spawn session");
|
|
905
|
+
}
|
|
906
|
+
logger.debug(`[API MACHINE] Spawned session ${session.happySessionId || "pending"} with PID ${session.pid}`);
|
|
907
|
+
if (!session.happySessionId) {
|
|
908
|
+
throw new Error(`Session spawned (PID ${session.pid}) but no sessionId received from webhook. The session process may still be initializing.`);
|
|
909
|
+
}
|
|
910
|
+
const response = { sessionId: session.happySessionId };
|
|
911
|
+
logger.debug(`[API MACHINE] Sending RPC response:`, response);
|
|
912
|
+
callback(encodeBase64(encrypt(response, this.secret)));
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (data.method === stopMethod2) {
|
|
916
|
+
logger.debug("[API MACHINE] Received stop-session RPC request");
|
|
917
|
+
const decryptedParams = decrypt(decodeBase64(data.params), this.secret);
|
|
918
|
+
const { sessionId } = decryptedParams || {};
|
|
919
|
+
if (!this.stopSession) {
|
|
920
|
+
throw new Error("Stop session handler not set");
|
|
921
|
+
}
|
|
922
|
+
if (!sessionId) {
|
|
923
|
+
throw new Error("Session ID is required");
|
|
924
|
+
}
|
|
925
|
+
const success = this.stopSession(sessionId);
|
|
926
|
+
if (!success) {
|
|
927
|
+
throw new Error("Session not found or failed to stop");
|
|
928
|
+
}
|
|
929
|
+
logger.debug(`[API MACHINE] Stopped session ${sessionId}`);
|
|
930
|
+
const response = { message: "Session stopped" };
|
|
931
|
+
const encryptedResponse = encodeBase64(encrypt(response, this.secret));
|
|
932
|
+
callback(encryptedResponse);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (data.method === stopDaemonMethod2) {
|
|
936
|
+
logger.debug("[API MACHINE] Received stop-daemon RPC request");
|
|
937
|
+
callback(encodeBase64(encrypt({
|
|
938
|
+
message: "Daemon stop request acknowledged, starting shutdown sequence..."
|
|
939
|
+
}, this.secret)));
|
|
940
|
+
setTimeout(() => {
|
|
941
|
+
logger.debug("[API MACHINE] Initiating daemon shutdown from RPC");
|
|
942
|
+
if (this.requestShutdown) {
|
|
943
|
+
this.requestShutdown();
|
|
944
|
+
}
|
|
945
|
+
}, 100);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
throw new Error(`Unknown RPC method: ${data.method}`);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
logger.debug(`[API MACHINE] RPC handler failed:`, error.message || error);
|
|
951
|
+
logger.debug(`[API MACHINE] Error stack:`, error.stack);
|
|
952
|
+
callback(encodeBase64(encrypt({ error: error.message || String(error) }, this.secret)));
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
this.socket.on("update", (data) => {
|
|
956
|
+
if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
|
|
957
|
+
const update = data.body;
|
|
958
|
+
if (update.metadata) {
|
|
959
|
+
logger.debug("[API MACHINE] Received external metadata update");
|
|
960
|
+
this.machine.metadata = decrypt(decodeBase64(update.metadata.value), this.secret);
|
|
961
|
+
this.machine.metadataVersion = update.metadata.version;
|
|
962
|
+
}
|
|
963
|
+
if (update.daemonState) {
|
|
964
|
+
logger.debug("[API MACHINE] Received external daemon state update");
|
|
965
|
+
this.machine.daemonState = decrypt(decodeBase64(update.daemonState.value), this.secret);
|
|
966
|
+
this.machine.daemonStateVersion = update.daemonState.version;
|
|
967
|
+
}
|
|
968
|
+
} else {
|
|
969
|
+
logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
this.socket.on("disconnect", () => {
|
|
973
|
+
logger.debug("[API MACHINE] Disconnected from server");
|
|
974
|
+
this.stopKeepAlive();
|
|
975
|
+
});
|
|
976
|
+
this.socket.io.on("reconnect", () => {
|
|
977
|
+
logger.debug("[API MACHINE] Reconnected to server");
|
|
978
|
+
this.socket.emit("rpc-register", { method: spawnMethod });
|
|
979
|
+
this.socket.emit("rpc-register", { method: stopMethod });
|
|
980
|
+
this.socket.emit("rpc-register", { method: stopDaemonMethod });
|
|
981
|
+
});
|
|
982
|
+
this.socket.on("connect_error", (error) => {
|
|
983
|
+
logger.debug(`[API MACHINE] Connection error: ${error.message}`);
|
|
984
|
+
});
|
|
985
|
+
this.socket.io.on("error", (error) => {
|
|
986
|
+
logger.debug("[API MACHINE] Socket error:", error);
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
startKeepAlive() {
|
|
990
|
+
this.stopKeepAlive();
|
|
991
|
+
this.keepAliveInterval = setInterval(() => {
|
|
992
|
+
const payload = {
|
|
993
|
+
machineId: this.machine.id,
|
|
994
|
+
time: Date.now()
|
|
995
|
+
};
|
|
996
|
+
if (process.env.VERBOSE) {
|
|
997
|
+
logger.debugLargeJson(`[API MACHINE] Emitting machine-alive`, payload);
|
|
998
|
+
}
|
|
999
|
+
this.socket.emit("machine-alive", payload);
|
|
1000
|
+
}, 2e4);
|
|
1001
|
+
logger.debug("[API MACHINE] Keep-alive started (20s interval)");
|
|
1002
|
+
}
|
|
1003
|
+
stopKeepAlive() {
|
|
1004
|
+
if (this.keepAliveInterval) {
|
|
1005
|
+
clearInterval(this.keepAliveInterval);
|
|
1006
|
+
this.keepAliveInterval = null;
|
|
1007
|
+
logger.debug("[API MACHINE] Keep-alive stopped");
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
shutdown() {
|
|
1011
|
+
logger.debug("[API MACHINE] Shutting down");
|
|
1012
|
+
this.stopKeepAlive();
|
|
1013
|
+
if (this.socket) {
|
|
1014
|
+
this.socket.close();
|
|
1015
|
+
logger.debug("[API MACHINE] Socket closed");
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
711
1020
|
class PushNotificationClient {
|
|
712
1021
|
token;
|
|
713
1022
|
baseUrl;
|
|
@@ -859,7 +1168,9 @@ class ApiClient {
|
|
|
859
1168
|
headers: {
|
|
860
1169
|
"Authorization": `Bearer ${this.token}`,
|
|
861
1170
|
"Content-Type": "application/json"
|
|
862
|
-
}
|
|
1171
|
+
},
|
|
1172
|
+
timeout: 5e3
|
|
1173
|
+
// 5 second timeout
|
|
863
1174
|
}
|
|
864
1175
|
);
|
|
865
1176
|
logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
|
|
@@ -881,17 +1192,76 @@ class ApiClient {
|
|
|
881
1192
|
}
|
|
882
1193
|
}
|
|
883
1194
|
/**
|
|
884
|
-
*
|
|
885
|
-
*
|
|
886
|
-
* @returns Session client
|
|
1195
|
+
* Get machine by ID from the server
|
|
1196
|
+
* Returns the current machine state from the server with decrypted metadata and daemonState
|
|
887
1197
|
*/
|
|
888
|
-
|
|
889
|
-
|
|
1198
|
+
async getMachine(machineId) {
|
|
1199
|
+
const response = await axios.get(`${configuration.serverUrl}/v1/machines/${machineId}`, {
|
|
1200
|
+
headers: {
|
|
1201
|
+
"Authorization": `Bearer ${this.token}`,
|
|
1202
|
+
"Content-Type": "application/json"
|
|
1203
|
+
},
|
|
1204
|
+
timeout: 2e3
|
|
1205
|
+
});
|
|
1206
|
+
const raw = response.data.machine;
|
|
1207
|
+
if (!raw) {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
logger.debug(`[API] Machine ${machineId} fetched from server`);
|
|
1211
|
+
const machine = {
|
|
1212
|
+
id: raw.id,
|
|
1213
|
+
metadata: raw.metadata ? decrypt(decodeBase64(raw.metadata), this.secret) : null,
|
|
1214
|
+
metadataVersion: raw.metadataVersion || 0,
|
|
1215
|
+
daemonState: raw.daemonState ? decrypt(decodeBase64(raw.daemonState), this.secret) : null,
|
|
1216
|
+
daemonStateVersion: raw.daemonStateVersion || 0,
|
|
1217
|
+
active: raw.active,
|
|
1218
|
+
activeAt: raw.activeAt,
|
|
1219
|
+
createdAt: raw.createdAt,
|
|
1220
|
+
updatedAt: raw.updatedAt
|
|
1221
|
+
};
|
|
1222
|
+
return machine;
|
|
890
1223
|
}
|
|
891
1224
|
/**
|
|
892
|
-
*
|
|
893
|
-
*
|
|
1225
|
+
* Register or update machine with the server
|
|
1226
|
+
* Returns the current machine state from the server with decrypted metadata and daemonState
|
|
894
1227
|
*/
|
|
1228
|
+
async createOrReturnExistingAsIs(opts) {
|
|
1229
|
+
const response = await axios.post(
|
|
1230
|
+
`${configuration.serverUrl}/v1/machines`,
|
|
1231
|
+
{
|
|
1232
|
+
id: opts.machineId,
|
|
1233
|
+
metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
|
|
1234
|
+
daemonState: opts.daemonState ? encodeBase64(encrypt(opts.daemonState, this.secret)) : void 0
|
|
1235
|
+
},
|
|
1236
|
+
{
|
|
1237
|
+
headers: {
|
|
1238
|
+
"Authorization": `Bearer ${this.token}`,
|
|
1239
|
+
"Content-Type": "application/json"
|
|
1240
|
+
},
|
|
1241
|
+
timeout: 5e3
|
|
1242
|
+
}
|
|
1243
|
+
);
|
|
1244
|
+
const raw = response.data.machine;
|
|
1245
|
+
logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
|
|
1246
|
+
const machine = {
|
|
1247
|
+
id: raw.id,
|
|
1248
|
+
metadata: raw.metadata ? decrypt(decodeBase64(raw.metadata), this.secret) : null,
|
|
1249
|
+
metadataVersion: raw.metadataVersion || 0,
|
|
1250
|
+
daemonState: raw.daemonState ? decrypt(decodeBase64(raw.daemonState), this.secret) : null,
|
|
1251
|
+
daemonStateVersion: raw.daemonStateVersion || 0,
|
|
1252
|
+
active: raw.active,
|
|
1253
|
+
activeAt: raw.activeAt,
|
|
1254
|
+
createdAt: raw.createdAt,
|
|
1255
|
+
updatedAt: raw.updatedAt
|
|
1256
|
+
};
|
|
1257
|
+
return machine;
|
|
1258
|
+
}
|
|
1259
|
+
sessionSyncClient(session) {
|
|
1260
|
+
return new ApiSessionClient(this.token, this.secret, session);
|
|
1261
|
+
}
|
|
1262
|
+
machineSyncClient(machine) {
|
|
1263
|
+
return new ApiMachineClient(this.token, this.secret, machine);
|
|
1264
|
+
}
|
|
895
1265
|
push() {
|
|
896
1266
|
return this.pushClient;
|
|
897
1267
|
}
|
|
@@ -945,4 +1315,4 @@ const RawJSONLinesSchema = z.discriminatedUnion("type", [
|
|
|
945
1315
|
}).passthrough()
|
|
946
1316
|
]);
|
|
947
1317
|
|
|
948
|
-
export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a,
|
|
1318
|
+
export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a, backoff as b, configuration as c, delay as d, encodeBase64 as e, encodeBase64Url as f, decodeBase64 as g, logger as l };
|