@standardagents/code 0.13.0 → 0.13.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/README.md +4 -3
- package/dist/index.js +334 -144
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -632,6 +632,17 @@ var ApiClient = class {
|
|
|
632
632
|
}
|
|
633
633
|
/** Write one value to the signed-in user's account-wide KV (null deletes). */
|
|
634
634
|
async userKvSet(key, value) {
|
|
635
|
+
if (key.startsWith("standardcode.machine.")) {
|
|
636
|
+
try {
|
|
637
|
+
await this.json(`/api/users/me/standardcode/machine_value`, {
|
|
638
|
+
method: "POST",
|
|
639
|
+
body: JSON.stringify({ key, value })
|
|
640
|
+
});
|
|
641
|
+
return;
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (!(error instanceof ApiHttpError) || error.status !== 404) throw error;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
635
646
|
await this.json(`/api/users/me/kv`, {
|
|
636
647
|
method: "POST",
|
|
637
648
|
body: JSON.stringify({ key, value })
|
|
@@ -3095,6 +3106,147 @@ var MessageStream = class {
|
|
|
3095
3106
|
}
|
|
3096
3107
|
}
|
|
3097
3108
|
};
|
|
3109
|
+
var AccountUserStream = class {
|
|
3110
|
+
constructor(api, clientId, options) {
|
|
3111
|
+
this.api = api;
|
|
3112
|
+
this.clientId = clientId;
|
|
3113
|
+
this.options = options;
|
|
3114
|
+
}
|
|
3115
|
+
api;
|
|
3116
|
+
clientId;
|
|
3117
|
+
options;
|
|
3118
|
+
ws = null;
|
|
3119
|
+
closed = false;
|
|
3120
|
+
started = false;
|
|
3121
|
+
heartbeat = null;
|
|
3122
|
+
reconnectAttempt = 0;
|
|
3123
|
+
reconnectTimer = null;
|
|
3124
|
+
eventListeners = /* @__PURE__ */ new Set();
|
|
3125
|
+
connectionListeners = /* @__PURE__ */ new Set();
|
|
3126
|
+
connected = false;
|
|
3127
|
+
start() {
|
|
3128
|
+
if (this.started || this.closed) return;
|
|
3129
|
+
this.started = true;
|
|
3130
|
+
this.openSocket();
|
|
3131
|
+
}
|
|
3132
|
+
onEvent(listener) {
|
|
3133
|
+
this.eventListeners.add(listener);
|
|
3134
|
+
return () => this.eventListeners.delete(listener);
|
|
3135
|
+
}
|
|
3136
|
+
onConnection(listener) {
|
|
3137
|
+
this.connectionListeners.add(listener);
|
|
3138
|
+
return () => this.connectionListeners.delete(listener);
|
|
3139
|
+
}
|
|
3140
|
+
/** Wait for a live socket. The timeout is local and performs no network work. */
|
|
3141
|
+
waitUntilConnected(timeoutMs = 15e3) {
|
|
3142
|
+
if (this.connected) return Promise.resolve();
|
|
3143
|
+
this.start();
|
|
3144
|
+
return new Promise((resolve, reject) => {
|
|
3145
|
+
let timer;
|
|
3146
|
+
const remove = this.onConnection((state) => {
|
|
3147
|
+
if (state !== "connected") return;
|
|
3148
|
+
clearTimeout(timer);
|
|
3149
|
+
remove();
|
|
3150
|
+
resolve();
|
|
3151
|
+
});
|
|
3152
|
+
timer = setTimeout(() => {
|
|
3153
|
+
remove();
|
|
3154
|
+
reject(new Error("The account stream did not connect in time."));
|
|
3155
|
+
}, timeoutMs);
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
openSocket() {
|
|
3159
|
+
if (this.closed) return;
|
|
3160
|
+
const clientId = this.options.uniqueConnectionId ? `${this.clientId}:${crypto.randomBytes(8).toString("hex")}` : this.clientId;
|
|
3161
|
+
let url = `${this.api.wsEndpoint}/api/users/me/stream?token=${encodeURIComponent(this.api.bearer)}&client_id=${encodeURIComponent(clientId)}&client_kind=${encodeURIComponent(this.options.clientKind)}`;
|
|
3162
|
+
if (this.options.clientName) url += `&client_name=${encodeURIComponent(this.options.clientName)}`;
|
|
3163
|
+
let ws;
|
|
3164
|
+
try {
|
|
3165
|
+
ws = new WebSocket(url);
|
|
3166
|
+
} catch {
|
|
3167
|
+
this.scheduleReconnect();
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
this.ws = ws;
|
|
3171
|
+
ws.addEventListener("open", () => {
|
|
3172
|
+
if (this.ws !== ws) return;
|
|
3173
|
+
this.connected = true;
|
|
3174
|
+
this.reconnectAttempt = 0;
|
|
3175
|
+
this.startHeartbeat(ws);
|
|
3176
|
+
this.emitConnection("connected", 0);
|
|
3177
|
+
});
|
|
3178
|
+
ws.addEventListener("message", (event) => {
|
|
3179
|
+
if (this.ws === ws) this.heartbeat?.markAlive();
|
|
3180
|
+
this.onMessage(String(event.data));
|
|
3181
|
+
});
|
|
3182
|
+
ws.addEventListener("error", () => this.handleDrop(ws));
|
|
3183
|
+
ws.addEventListener("close", () => this.handleDrop(ws));
|
|
3184
|
+
}
|
|
3185
|
+
onMessage(raw) {
|
|
3186
|
+
let message;
|
|
3187
|
+
try {
|
|
3188
|
+
message = JSON.parse(raw);
|
|
3189
|
+
} catch {
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
if (!message || typeof message !== "object") return;
|
|
3193
|
+
const frame = message;
|
|
3194
|
+
if (typeof frame.event === "string") {
|
|
3195
|
+
this.emitEvent(frame.event, frame.data);
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
if (frame.type === "wake" && typeof frame.threadId === "string") {
|
|
3199
|
+
this.emitEvent("standardagents.wake", { thread_id: frame.threadId });
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
emitEvent(event, data) {
|
|
3203
|
+
for (const listener of this.eventListeners) listener(event, data);
|
|
3204
|
+
}
|
|
3205
|
+
emitConnection(state, attempt) {
|
|
3206
|
+
for (const listener of this.connectionListeners) listener(state, attempt);
|
|
3207
|
+
}
|
|
3208
|
+
handleDrop(ws) {
|
|
3209
|
+
if (this.ws !== ws) return;
|
|
3210
|
+
this.ws = null;
|
|
3211
|
+
this.connected = false;
|
|
3212
|
+
this.stopHeartbeat();
|
|
3213
|
+
this.scheduleReconnect();
|
|
3214
|
+
}
|
|
3215
|
+
scheduleReconnect() {
|
|
3216
|
+
if (this.closed || this.reconnectTimer) return;
|
|
3217
|
+
this.reconnectAttempt++;
|
|
3218
|
+
this.emitConnection("reconnecting", this.reconnectAttempt);
|
|
3219
|
+
const base = Math.min(500 * 2 ** (this.reconnectAttempt - 1), 15e3);
|
|
3220
|
+
const delay = base + Math.floor(Math.random() * 400);
|
|
3221
|
+
this.reconnectTimer = setTimeout(() => {
|
|
3222
|
+
this.reconnectTimer = null;
|
|
3223
|
+
this.openSocket();
|
|
3224
|
+
}, delay);
|
|
3225
|
+
}
|
|
3226
|
+
startHeartbeat(ws) {
|
|
3227
|
+
this.stopHeartbeat();
|
|
3228
|
+
this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
|
|
3229
|
+
this.heartbeat.start();
|
|
3230
|
+
}
|
|
3231
|
+
stopHeartbeat() {
|
|
3232
|
+
this.heartbeat?.stop();
|
|
3233
|
+
this.heartbeat = null;
|
|
3234
|
+
}
|
|
3235
|
+
close() {
|
|
3236
|
+
this.closed = true;
|
|
3237
|
+
this.connected = false;
|
|
3238
|
+
this.stopHeartbeat();
|
|
3239
|
+
if (this.reconnectTimer) {
|
|
3240
|
+
clearTimeout(this.reconnectTimer);
|
|
3241
|
+
this.reconnectTimer = null;
|
|
3242
|
+
}
|
|
3243
|
+
const ws = this.ws;
|
|
3244
|
+
this.ws = null;
|
|
3245
|
+
ws?.close();
|
|
3246
|
+
this.eventListeners.clear();
|
|
3247
|
+
this.connectionListeners.clear();
|
|
3248
|
+
}
|
|
3249
|
+
};
|
|
3098
3250
|
|
|
3099
3251
|
// src/events-stream.ts
|
|
3100
3252
|
var SystemEvents = class {
|
|
@@ -6492,7 +6644,7 @@ function readVersion() {
|
|
|
6492
6644
|
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
6493
6645
|
} catch {
|
|
6494
6646
|
}
|
|
6495
|
-
return "0.13.
|
|
6647
|
+
return "0.13.1" ;
|
|
6496
6648
|
}
|
|
6497
6649
|
function isLocalHost(host) {
|
|
6498
6650
|
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local") || host.endsWith(".localhost") || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
|
|
@@ -6607,6 +6759,68 @@ async function readFsResponse(api, machineId) {
|
|
|
6607
6759
|
async function readFsRequest(api, machineId) {
|
|
6608
6760
|
return parseFsRequest(await api.userKvGet(fsRequestKey(machineId)));
|
|
6609
6761
|
}
|
|
6762
|
+
async function requestFsBrowse(api, accountStream, machineId, req, timeoutMs = 15e3) {
|
|
6763
|
+
await accountStream.waitUntilConnected(timeoutMs);
|
|
6764
|
+
const responseKey = fsResponseKey(machineId);
|
|
6765
|
+
return new Promise((resolve, reject) => {
|
|
6766
|
+
let settled = false;
|
|
6767
|
+
let reading = false;
|
|
6768
|
+
let readAgain = false;
|
|
6769
|
+
let timer;
|
|
6770
|
+
let removeEvent = () => {
|
|
6771
|
+
};
|
|
6772
|
+
let removeConnection = () => {
|
|
6773
|
+
};
|
|
6774
|
+
const cleanup = () => {
|
|
6775
|
+
clearTimeout(timer);
|
|
6776
|
+
removeEvent();
|
|
6777
|
+
removeConnection();
|
|
6778
|
+
};
|
|
6779
|
+
const fail = (error) => {
|
|
6780
|
+
if (settled) return;
|
|
6781
|
+
settled = true;
|
|
6782
|
+
cleanup();
|
|
6783
|
+
reject(error);
|
|
6784
|
+
};
|
|
6785
|
+
const read = async () => {
|
|
6786
|
+
if (settled) return;
|
|
6787
|
+
if (reading) {
|
|
6788
|
+
readAgain = true;
|
|
6789
|
+
return;
|
|
6790
|
+
}
|
|
6791
|
+
reading = true;
|
|
6792
|
+
try {
|
|
6793
|
+
do {
|
|
6794
|
+
readAgain = false;
|
|
6795
|
+
const response = await readFsResponse(api, machineId);
|
|
6796
|
+
if (response?.nonce === req.nonce) {
|
|
6797
|
+
settled = true;
|
|
6798
|
+
cleanup();
|
|
6799
|
+
resolve(response);
|
|
6800
|
+
return;
|
|
6801
|
+
}
|
|
6802
|
+
} while (readAgain && !settled);
|
|
6803
|
+
} catch (error) {
|
|
6804
|
+
fail(error);
|
|
6805
|
+
} finally {
|
|
6806
|
+
reading = false;
|
|
6807
|
+
}
|
|
6808
|
+
};
|
|
6809
|
+
removeEvent = accountStream.onEvent((event, data) => {
|
|
6810
|
+
if (event !== "standardcode.machine_changed") return;
|
|
6811
|
+
const key = data && typeof data === "object" ? data.key : null;
|
|
6812
|
+
if (key === responseKey) void read();
|
|
6813
|
+
});
|
|
6814
|
+
removeConnection = accountStream.onConnection((state) => {
|
|
6815
|
+
if (state === "connected") void read();
|
|
6816
|
+
});
|
|
6817
|
+
timer = setTimeout(
|
|
6818
|
+
() => fail(new Error("The remote machine did not answer in time.")),
|
|
6819
|
+
timeoutMs
|
|
6820
|
+
);
|
|
6821
|
+
void writeFsRequest(api, machineId, req).catch(fail);
|
|
6822
|
+
});
|
|
6823
|
+
}
|
|
6610
6824
|
async function writeFsResponse(api, machineId, res) {
|
|
6611
6825
|
await api.userKvSet(fsResponseKey(machineId), { ...res, responded_at: Date.now() });
|
|
6612
6826
|
}
|
|
@@ -6756,8 +6970,11 @@ async function loadMachine(api, machineId) {
|
|
|
6756
6970
|
overlayProjectNames(rec, projNames);
|
|
6757
6971
|
return rec;
|
|
6758
6972
|
}
|
|
6973
|
+
var CONNECTED_LAST_SEEN_AT = Number.MAX_SAFE_INTEGER;
|
|
6759
6974
|
function daemonOnline(record2, now = Date.now()) {
|
|
6760
|
-
|
|
6975
|
+
if (!record2.daemon) return false;
|
|
6976
|
+
if (typeof record2.daemon.connected === "boolean") return record2.daemon.connected;
|
|
6977
|
+
return now - record2.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
|
|
6761
6978
|
}
|
|
6762
6979
|
function newRecord(identity) {
|
|
6763
6980
|
const now = Date.now();
|
|
@@ -6786,6 +7003,18 @@ async function updateOwnMachineRecord(api, identity, mutate) {
|
|
|
6786
7003
|
await api.userKvSet(machineKey(identity.machine_id), record2);
|
|
6787
7004
|
return record2;
|
|
6788
7005
|
}
|
|
7006
|
+
async function setDaemonPresence(api, identity, version, connected, pid = process.pid) {
|
|
7007
|
+
await updateOwnMachineRecord(api, identity, (record2) => {
|
|
7008
|
+
const now = Date.now();
|
|
7009
|
+
record2.daemon = {
|
|
7010
|
+
version,
|
|
7011
|
+
installed_at: record2.daemon?.installed_at ?? now,
|
|
7012
|
+
last_seen_at: connected ? CONNECTED_LAST_SEEN_AT : 0,
|
|
7013
|
+
connected,
|
|
7014
|
+
pid
|
|
7015
|
+
};
|
|
7016
|
+
});
|
|
7017
|
+
}
|
|
6789
7018
|
function projectRepository(projectDir) {
|
|
6790
7019
|
try {
|
|
6791
7020
|
const url = execFileSync("git", ["-C", projectDir, "remote", "get-url", "origin"], {
|
|
@@ -7028,22 +7257,20 @@ function parseBrowseResult(value) {
|
|
|
7028
7257
|
error: typeof r.error === "string" ? r.error : void 0
|
|
7029
7258
|
};
|
|
7030
7259
|
}
|
|
7031
|
-
function remoteBrowseBackend(api, machineId, label) {
|
|
7260
|
+
function remoteBrowseBackend(api, accountStream, machineId, label) {
|
|
7032
7261
|
const rpc = async (req) => {
|
|
7033
7262
|
const nonce = crypto.randomBytes(8).toString("hex");
|
|
7034
|
-
await
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
if (res && res.nonce === nonce) {
|
|
7040
|
-
if (!res.ok) throw new Error(res.error || "Browse failed on the remote machine.");
|
|
7041
|
-
const parsed = parseBrowseResult(res.result);
|
|
7042
|
-
if (!parsed) throw new Error("The remote machine sent an unreadable listing.");
|
|
7043
|
-
return parsed;
|
|
7263
|
+
const res = await requestFsBrowse(api, accountStream, machineId, { nonce, ...req }).catch(
|
|
7264
|
+
(error) => {
|
|
7265
|
+
throw new Error(
|
|
7266
|
+
error instanceof Error && error.message === "The remote machine did not answer in time." ? `${label} didn't answer \u2014 is its daemon online?` : error instanceof Error ? error.message : String(error)
|
|
7267
|
+
);
|
|
7044
7268
|
}
|
|
7045
|
-
|
|
7046
|
-
throw new Error(
|
|
7269
|
+
);
|
|
7270
|
+
if (!res.ok) throw new Error(res.error || "Browse failed on the remote machine.");
|
|
7271
|
+
const parsed = parseBrowseResult(res.result);
|
|
7272
|
+
if (!parsed) throw new Error("The remote machine sent an unreadable listing.");
|
|
7273
|
+
return parsed;
|
|
7047
7274
|
};
|
|
7048
7275
|
return {
|
|
7049
7276
|
label,
|
|
@@ -7225,106 +7452,31 @@ async function awaitApprovalViaRelay(api, threadId, request, options = {}) {
|
|
|
7225
7452
|
});
|
|
7226
7453
|
}
|
|
7227
7454
|
}
|
|
7455
|
+
|
|
7456
|
+
// src/daemon-user-stream.ts
|
|
7228
7457
|
var DaemonUserStream = class {
|
|
7458
|
+
stream;
|
|
7229
7459
|
constructor(api, clientId, hooks, clientName) {
|
|
7230
|
-
this.
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
}
|
|
7235
|
-
api;
|
|
7236
|
-
clientId;
|
|
7237
|
-
hooks;
|
|
7238
|
-
clientName;
|
|
7239
|
-
ws = null;
|
|
7240
|
-
closed = false;
|
|
7241
|
-
heartbeat = null;
|
|
7242
|
-
reconnectAttempt = 0;
|
|
7243
|
-
reconnectTimer = null;
|
|
7244
|
-
start() {
|
|
7245
|
-
this.openSocket();
|
|
7246
|
-
}
|
|
7247
|
-
openSocket() {
|
|
7248
|
-
if (this.closed) return;
|
|
7249
|
-
const connectionClientId = `${this.clientId}:${crypto.randomBytes(8).toString("hex")}`;
|
|
7250
|
-
let url = `${this.api.wsEndpoint}/api/users/me/stream?token=${encodeURIComponent(this.api.bearer)}&client_id=${encodeURIComponent(connectionClientId)}&client_kind=daemon`;
|
|
7251
|
-
if (this.clientName) url += `&client_name=${encodeURIComponent(this.clientName)}`;
|
|
7252
|
-
let ws;
|
|
7253
|
-
try {
|
|
7254
|
-
ws = new WebSocket(url);
|
|
7255
|
-
} catch {
|
|
7256
|
-
this.scheduleReconnect();
|
|
7257
|
-
return;
|
|
7258
|
-
}
|
|
7259
|
-
this.ws = ws;
|
|
7260
|
-
ws.addEventListener("open", () => {
|
|
7261
|
-
this.reconnectAttempt = 0;
|
|
7262
|
-
this.startHeartbeat(ws);
|
|
7263
|
-
this.hooks.onConnection?.("connected", 0);
|
|
7460
|
+
this.stream = new AccountUserStream(api, clientId, {
|
|
7461
|
+
clientKind: "daemon",
|
|
7462
|
+
clientName,
|
|
7463
|
+
uniqueConnectionId: true
|
|
7264
7464
|
});
|
|
7265
|
-
|
|
7266
|
-
if (
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
ws.addEventListener("close", () => this.handleDrop(ws));
|
|
7271
|
-
}
|
|
7272
|
-
onMessage(raw) {
|
|
7273
|
-
let message;
|
|
7274
|
-
try {
|
|
7275
|
-
message = JSON.parse(raw);
|
|
7276
|
-
} catch {
|
|
7277
|
-
return;
|
|
7278
|
-
}
|
|
7279
|
-
if (!message || typeof message !== "object") return;
|
|
7280
|
-
const frame = message;
|
|
7281
|
-
if (typeof frame.event === "string") {
|
|
7282
|
-
if (frame.event === "standardagents.wake") {
|
|
7283
|
-
const threadId = frame.data?.thread_id;
|
|
7284
|
-
if (typeof threadId === "string") this.hooks.onWake(threadId);
|
|
7285
|
-
} else {
|
|
7286
|
-
this.hooks.onEvent(frame.event, frame.data);
|
|
7465
|
+
this.stream.onEvent((event, data) => {
|
|
7466
|
+
if (event === "standardagents.wake") {
|
|
7467
|
+
const threadId = data?.thread_id;
|
|
7468
|
+
if (typeof threadId === "string") hooks.onWake(threadId);
|
|
7469
|
+
return;
|
|
7287
7470
|
}
|
|
7288
|
-
|
|
7289
|
-
}
|
|
7290
|
-
if (
|
|
7291
|
-
this.hooks.onWake(frame.threadId);
|
|
7292
|
-
}
|
|
7293
|
-
}
|
|
7294
|
-
handleDrop(ws) {
|
|
7295
|
-
if (this.ws !== ws) return;
|
|
7296
|
-
this.ws = null;
|
|
7297
|
-
this.stopHeartbeat();
|
|
7298
|
-
this.scheduleReconnect();
|
|
7299
|
-
}
|
|
7300
|
-
scheduleReconnect() {
|
|
7301
|
-
if (this.closed || this.reconnectTimer) return;
|
|
7302
|
-
this.reconnectAttempt++;
|
|
7303
|
-
this.hooks.onConnection?.("reconnecting", this.reconnectAttempt);
|
|
7304
|
-
const base = Math.min(500 * 2 ** (this.reconnectAttempt - 1), 15e3);
|
|
7305
|
-
const delay = base + Math.floor(Math.random() * 400);
|
|
7306
|
-
this.reconnectTimer = setTimeout(() => {
|
|
7307
|
-
this.reconnectTimer = null;
|
|
7308
|
-
this.openSocket();
|
|
7309
|
-
}, delay);
|
|
7310
|
-
}
|
|
7311
|
-
startHeartbeat(ws) {
|
|
7312
|
-
this.stopHeartbeat();
|
|
7313
|
-
this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
|
|
7314
|
-
this.heartbeat.start();
|
|
7471
|
+
hooks.onEvent(event, data);
|
|
7472
|
+
});
|
|
7473
|
+
if (hooks.onConnection) this.stream.onConnection(hooks.onConnection);
|
|
7315
7474
|
}
|
|
7316
|
-
|
|
7317
|
-
this.
|
|
7318
|
-
this.heartbeat = null;
|
|
7475
|
+
start() {
|
|
7476
|
+
this.stream.start();
|
|
7319
7477
|
}
|
|
7320
7478
|
close() {
|
|
7321
|
-
this.
|
|
7322
|
-
this.stopHeartbeat();
|
|
7323
|
-
if (this.reconnectTimer) {
|
|
7324
|
-
clearTimeout(this.reconnectTimer);
|
|
7325
|
-
this.reconnectTimer = null;
|
|
7326
|
-
}
|
|
7327
|
-
this.ws?.close();
|
|
7479
|
+
this.stream.close();
|
|
7328
7480
|
}
|
|
7329
7481
|
};
|
|
7330
7482
|
var PKG_NAME = "@standardagents/code";
|
|
@@ -7697,15 +7849,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
7697
7849
|
process.on("unhandledRejection", (err) => {
|
|
7698
7850
|
daemonLog(`unhandledRejection: ${err instanceof Error ? err.stack : String(err)}`);
|
|
7699
7851
|
});
|
|
7700
|
-
await
|
|
7701
|
-
const now = Date.now();
|
|
7702
|
-
record2.daemon = {
|
|
7703
|
-
version,
|
|
7704
|
-
installed_at: record2.daemon?.installed_at ?? now,
|
|
7705
|
-
last_seen_at: 0,
|
|
7706
|
-
pid: process.pid
|
|
7707
|
-
};
|
|
7708
|
-
}).catch(
|
|
7852
|
+
await setDaemonPresence(api, identity, version, false).catch(
|
|
7709
7853
|
(e) => daemonLog(`machine registration failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
7710
7854
|
);
|
|
7711
7855
|
const workers = /* @__PURE__ */ new Map();
|
|
@@ -7717,13 +7861,26 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
7717
7861
|
let forcedUpdatePending = false;
|
|
7718
7862
|
let restartInProgress = false;
|
|
7719
7863
|
let shuttingDown = false;
|
|
7720
|
-
|
|
7864
|
+
let presenceWrite = Promise.resolve();
|
|
7865
|
+
function publishPresence(connected) {
|
|
7866
|
+
presenceWrite = presenceWrite.then(() => setDaemonPresence(api, identity, version, connected)).catch((error) => {
|
|
7867
|
+
daemonLog(
|
|
7868
|
+
`presence ${connected ? "publish" : "clear"} failed: ${error instanceof Error ? error.message : String(error)}`
|
|
7869
|
+
);
|
|
7870
|
+
});
|
|
7871
|
+
return presenceWrite;
|
|
7872
|
+
}
|
|
7873
|
+
async function shutdown(code) {
|
|
7721
7874
|
if (shuttingDown) return;
|
|
7722
7875
|
shuttingDown = true;
|
|
7723
7876
|
if (updateTimer) clearInterval(updateTimer);
|
|
7724
7877
|
for (const worker of workers.values()) worker.stop();
|
|
7725
7878
|
workers.clear();
|
|
7726
7879
|
hub?.close();
|
|
7880
|
+
await Promise.race([
|
|
7881
|
+
publishPresence(false),
|
|
7882
|
+
new Promise((resolve) => setTimeout(resolve, 2e3))
|
|
7883
|
+
]);
|
|
7727
7884
|
daemonLog(`daemon exiting (code ${code})`);
|
|
7728
7885
|
process.exit(code);
|
|
7729
7886
|
}
|
|
@@ -7742,14 +7899,14 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
7742
7899
|
daemonLog(`forced update ${ok ? "succeeded" : "failed"}: ${output4.trim().split("\n").slice(-2).join(" | ")}`);
|
|
7743
7900
|
if (ok) {
|
|
7744
7901
|
daemonLog("restarting to apply the forced update");
|
|
7745
|
-
shutdown(0);
|
|
7902
|
+
await shutdown(0);
|
|
7746
7903
|
return;
|
|
7747
7904
|
}
|
|
7748
7905
|
restartInProgress = false;
|
|
7749
7906
|
}
|
|
7750
7907
|
if (updateReady) {
|
|
7751
7908
|
daemonLog("restarting to apply the installed update");
|
|
7752
|
-
shutdown(0);
|
|
7909
|
+
await shutdown(0);
|
|
7753
7910
|
}
|
|
7754
7911
|
}
|
|
7755
7912
|
const threadIsActive = (threadId) => lineActiveThreads.has(threadId) || samaActiveThreads.has(threadId);
|
|
@@ -7886,9 +8043,16 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
7886
8043
|
}
|
|
7887
8044
|
fsBusy = true;
|
|
7888
8045
|
try {
|
|
7889
|
-
|
|
8046
|
+
let req;
|
|
8047
|
+
try {
|
|
8048
|
+
req = await readFsRequest(api, identity.machine_id);
|
|
8049
|
+
} catch (error2) {
|
|
8050
|
+
daemonLog(`fs-browse request read failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
8051
|
+
return;
|
|
8052
|
+
}
|
|
7890
8053
|
if (!req || req.nonce === lastFsNonce) return;
|
|
7891
8054
|
lastFsNonce = req.nonce;
|
|
8055
|
+
daemonLog(`fs-browse ${req.op} received (${req.nonce})`);
|
|
7892
8056
|
let ok = true;
|
|
7893
8057
|
let result;
|
|
7894
8058
|
let error;
|
|
@@ -7898,8 +8062,12 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
7898
8062
|
ok = false;
|
|
7899
8063
|
error = err instanceof Error ? err.message : "browse failed";
|
|
7900
8064
|
}
|
|
7901
|
-
|
|
7902
|
-
|
|
8065
|
+
try {
|
|
8066
|
+
await writeFsResponse(api, identity.machine_id, { nonce: req.nonce, ok, result, error });
|
|
8067
|
+
daemonLog(`fs-browse ${req.op} ${ok ? "answered" : "failed"} (${req.nonce})${error ? `: ${error}` : ""}`);
|
|
8068
|
+
} catch (writeError) {
|
|
8069
|
+
daemonLog(`fs-browse response write failed (${req.nonce}): ${writeError instanceof Error ? writeError.message : String(writeError)}`);
|
|
8070
|
+
}
|
|
7903
8071
|
} finally {
|
|
7904
8072
|
fsBusy = false;
|
|
7905
8073
|
if (fsAgain) {
|
|
@@ -8001,9 +8169,11 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
8001
8169
|
}
|
|
8002
8170
|
if (event !== "standardcode.machine_changed") return;
|
|
8003
8171
|
const key = data && typeof data === "object" ? data.key : null;
|
|
8172
|
+
daemonLog(`machine event: ${typeof key === "string" ? key : "missing key"}`);
|
|
8004
8173
|
if (key === `${machinePrefix}.cmd`) void drainCommands();
|
|
8005
|
-
else if (key === `${machinePrefix}.fsreq`)
|
|
8006
|
-
|
|
8174
|
+
else if (key === `${machinePrefix}.fsreq`) {
|
|
8175
|
+
void drainFsRequests();
|
|
8176
|
+
} else if (key === `${machinePrefix}.name`) {
|
|
8007
8177
|
void getMachineName(api, identity.machine_id).then((name) => {
|
|
8008
8178
|
displayName = name || os8.hostname();
|
|
8009
8179
|
}).catch(() => {
|
|
@@ -8012,11 +8182,14 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
8012
8182
|
},
|
|
8013
8183
|
onConnection: (state, attempt) => {
|
|
8014
8184
|
if (state === "reconnecting") {
|
|
8015
|
-
if (attempt === 1)
|
|
8185
|
+
if (attempt === 1) {
|
|
8186
|
+
daemonLog("user stream: reconnecting");
|
|
8187
|
+
void publishPresence(false);
|
|
8188
|
+
}
|
|
8016
8189
|
return;
|
|
8017
8190
|
}
|
|
8018
8191
|
daemonLog("user stream: connected");
|
|
8019
|
-
void recoverOnConnect();
|
|
8192
|
+
void publishPresence(true).then(() => recoverOnConnect());
|
|
8020
8193
|
}
|
|
8021
8194
|
},
|
|
8022
8195
|
displayName
|
|
@@ -8024,8 +8197,8 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
8024
8197
|
hub.start();
|
|
8025
8198
|
void checkUpdates();
|
|
8026
8199
|
updateTimer = setInterval(() => void checkUpdates(), UPDATE_CHECK_MS);
|
|
8027
|
-
process.on("SIGTERM", () => shutdown(0));
|
|
8028
|
-
process.on("SIGINT", () => shutdown(0));
|
|
8200
|
+
process.on("SIGTERM", () => void shutdown(0));
|
|
8201
|
+
process.on("SIGINT", () => void shutdown(0));
|
|
8029
8202
|
daemonLog(`daemon ready \u2014 waiting for work tagged ${runnerTag}`);
|
|
8030
8203
|
await new Promise(() => {
|
|
8031
8204
|
});
|
|
@@ -8372,8 +8545,8 @@ async function statusCommand() {
|
|
|
8372
8545
|
return;
|
|
8373
8546
|
}
|
|
8374
8547
|
const online = daemonOnline(record2);
|
|
8375
|
-
const streamConnected = !!record2.daemon && record2.daemon.last_seen_at > Date.now();
|
|
8376
|
-
const seen = streamConnected && record2.daemon ? `user stream connected (v${record2.daemon.version})` : record2.daemon ? `last legacy heartbeat ${Math.round((Date.now() - record2.daemon.last_seen_at) / 1e3)}s ago (v${record2.daemon.version})` : "never";
|
|
8548
|
+
const streamConnected = !!record2.daemon && (record2.daemon.connected ?? record2.daemon.last_seen_at > Date.now());
|
|
8549
|
+
const seen = streamConnected && record2.daemon ? `user stream connected (v${record2.daemon.version})` : record2.daemon?.connected === false ? `user stream disconnected (v${record2.daemon.version})` : record2.daemon ? `last legacy heartbeat ${Math.round((Date.now() - record2.daemon.last_seen_at) / 1e3)}s ago (v${record2.daemon.version})` : "never";
|
|
8377
8550
|
stdout.write(`${c3.bold}Registry:${c3.reset} ${online ? `${c3.green}online${c3.reset}` : `${c3.yellow}offline${c3.reset}`} \xB7 ${seen}
|
|
8378
8551
|
`);
|
|
8379
8552
|
const projects = Object.keys(record2.projects);
|
|
@@ -9028,6 +9201,12 @@ ${c5.dim}Press Control-C again to exit${c5.reset}
|
|
|
9028
9201
|
handoffClosing = true;
|
|
9029
9202
|
reader.rl?.close();
|
|
9030
9203
|
const identity = loadMachineIdentity();
|
|
9204
|
+
const accountStream = new AccountUserStream(api, interactiveClientId(identity), {
|
|
9205
|
+
clientKind: "cli",
|
|
9206
|
+
clientName: `${machine} (terminal)`
|
|
9207
|
+
});
|
|
9208
|
+
accountStream.start();
|
|
9209
|
+
process.once("exit", () => accountStream.close());
|
|
9031
9210
|
void registerProject(api, identity, projectDir).catch(() => {
|
|
9032
9211
|
});
|
|
9033
9212
|
const tui = new Tui(1);
|
|
@@ -9155,7 +9334,7 @@ ${c5.bold}This machine has no always-on daemon${c5.reset} ${c5.dim}\u2014 sessio
|
|
|
9155
9334
|
session.remotePath = void 0;
|
|
9156
9335
|
projectDir = launchDir;
|
|
9157
9336
|
if (where) {
|
|
9158
|
-
const remotePath = await pickRemoteProject(tui, api, where);
|
|
9337
|
+
const remotePath = await pickRemoteProject(tui, api, accountStream, where);
|
|
9159
9338
|
if (!remotePath) {
|
|
9160
9339
|
step = "machine";
|
|
9161
9340
|
continue;
|
|
@@ -9275,7 +9454,18 @@ ${c5.bold}This machine has no always-on daemon${c5.reset} ${c5.dim}\u2014 sessio
|
|
|
9275
9454
|
}
|
|
9276
9455
|
for (; ; ) {
|
|
9277
9456
|
const agentTitle = AGENT_CHOICES.find((a) => a.id === selectedAgent)?.title ?? selectedAgent;
|
|
9278
|
-
await runInteractive(
|
|
9457
|
+
await runInteractive(
|
|
9458
|
+
tui,
|
|
9459
|
+
api,
|
|
9460
|
+
accountStream,
|
|
9461
|
+
threadId,
|
|
9462
|
+
projectDir,
|
|
9463
|
+
machine,
|
|
9464
|
+
resumed,
|
|
9465
|
+
session,
|
|
9466
|
+
agentTitle,
|
|
9467
|
+
historySeed
|
|
9468
|
+
);
|
|
9279
9469
|
historySeed = threadId;
|
|
9280
9470
|
threadId = await createSessionThread();
|
|
9281
9471
|
await api.kvSet(threadId, "lease_supersedes", historySeed);
|
|
@@ -9379,7 +9569,7 @@ async function pickLocalProject(tui, api, self, cwd, machineName) {
|
|
|
9379
9569
|
if (picked !== NEW) return picked;
|
|
9380
9570
|
return await pickDirectory(tui, localBrowseBackend(label), { startPath: cwd, loader: startLoader });
|
|
9381
9571
|
}
|
|
9382
|
-
async function pickRemoteProject(tui, api, runner) {
|
|
9572
|
+
async function pickRemoteProject(tui, api, accountStream, runner) {
|
|
9383
9573
|
const ENTER_PATH = "__enter_path__";
|
|
9384
9574
|
const projects = Object.entries(runner.projects).sort(
|
|
9385
9575
|
(a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
|
|
@@ -9401,7 +9591,7 @@ async function pickRemoteProject(tui, api, runner) {
|
|
|
9401
9591
|
);
|
|
9402
9592
|
if (!picked) return null;
|
|
9403
9593
|
if (picked !== ENTER_PATH) return picked;
|
|
9404
|
-
return await pickDirectory(tui, remoteBrowseBackend(api, runner.id, runner.name), {
|
|
9594
|
+
return await pickDirectory(tui, remoteBrowseBackend(api, accountStream, runner.id, runner.name), {
|
|
9405
9595
|
loader: startLoader
|
|
9406
9596
|
});
|
|
9407
9597
|
}
|
|
@@ -9469,7 +9659,7 @@ async function printHistory(api, threadId, tui) {
|
|
|
9469
9659
|
else printAssistant(tui, text);
|
|
9470
9660
|
}
|
|
9471
9661
|
}
|
|
9472
|
-
async function runInteractive(tui, api, threadId, projectDir, machine, resumed, session, agentTitle, historySeedThreadId) {
|
|
9662
|
+
async function runInteractive(tui, api, accountStream, threadId, projectDir, machine, resumed, session, agentTitle, historySeedThreadId) {
|
|
9473
9663
|
const remote = session.mode === "remote";
|
|
9474
9664
|
const runnerName = session.runner?.name ?? "the remote machine";
|
|
9475
9665
|
let currentOwner = null;
|
|
@@ -10129,7 +10319,7 @@ ${c5.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
10129
10319
|
name: "machines",
|
|
10130
10320
|
label: "Your machines",
|
|
10131
10321
|
hint: "list, rename, update, manage projects",
|
|
10132
|
-
run: () => runMachinesMenu(tui, api, session.identity)
|
|
10322
|
+
run: () => runMachinesMenu(tui, api, accountStream, session.identity)
|
|
10133
10323
|
},
|
|
10134
10324
|
{
|
|
10135
10325
|
name: "daemon",
|
|
@@ -10606,7 +10796,7 @@ async function runLevelMenu(tui, perm) {
|
|
|
10606
10796
|
perm.level = picked;
|
|
10607
10797
|
}
|
|
10608
10798
|
}
|
|
10609
|
-
async function runMachinesMenu(tui, api, self) {
|
|
10799
|
+
async function runMachinesMenu(tui, api, accountStream, self) {
|
|
10610
10800
|
let machines;
|
|
10611
10801
|
try {
|
|
10612
10802
|
machines = await loadMachines(api);
|
|
@@ -10635,9 +10825,9 @@ async function runMachinesMenu(tui, api, self) {
|
|
|
10635
10825
|
);
|
|
10636
10826
|
if (!picked) return;
|
|
10637
10827
|
const machine = machines.find((m) => m.id === picked);
|
|
10638
|
-
await manageMachine(tui, api, self, machine);
|
|
10828
|
+
await manageMachine(tui, api, accountStream, self, machine);
|
|
10639
10829
|
}
|
|
10640
|
-
async function manageMachine(tui, api, self, machine) {
|
|
10830
|
+
async function manageMachine(tui, api, accountStream, self, machine) {
|
|
10641
10831
|
const isSelf = machine.id === self.machine_id;
|
|
10642
10832
|
const online = daemonOnline(machine);
|
|
10643
10833
|
const canRunCommands = isSelf || !!machine.daemon;
|
|
@@ -10675,7 +10865,7 @@ async function manageMachine(tui, api, self, machine) {
|
|
|
10675
10865
|
machine.icon = trimmed || void 0;
|
|
10676
10866
|
tui.print(`${c5.green}\u2713${c5.reset} icon ${trimmed ? `set to ${trimmed}` : "reset"} for ${machine.name}`);
|
|
10677
10867
|
}
|
|
10678
|
-
return manageMachine(tui, api, self, machine);
|
|
10868
|
+
return manageMachine(tui, api, accountStream, self, machine);
|
|
10679
10869
|
}
|
|
10680
10870
|
const dispatch = async (kind, args) => {
|
|
10681
10871
|
if (isSelf) {
|
|
@@ -10703,10 +10893,10 @@ async function manageMachine(tui, api, self, machine) {
|
|
|
10703
10893
|
tui.print(`${c5.green}\u2713${c5.reset} Update ${c5.gray}${applyNote} (its daemon updates and restarts on the new version).${c5.reset}`);
|
|
10704
10894
|
}
|
|
10705
10895
|
} else if (action === "projects") {
|
|
10706
|
-
await manageMachineProjects(tui, api, self, machine, dispatch, applyNote);
|
|
10896
|
+
await manageMachineProjects(tui, api, accountStream, self, machine, dispatch, applyNote);
|
|
10707
10897
|
}
|
|
10708
10898
|
}
|
|
10709
|
-
async function manageMachineProjects(tui, api, self, machine, dispatch, applyNote) {
|
|
10899
|
+
async function manageMachineProjects(tui, api, accountStream, self, machine, dispatch, applyNote) {
|
|
10710
10900
|
const ADD = "__add__";
|
|
10711
10901
|
const paths = Object.keys(machine.projects).sort();
|
|
10712
10902
|
const projLabels = projectDisplayLabels(machine.projects);
|
|
@@ -10725,7 +10915,7 @@ async function manageMachineProjects(tui, api, self, machine, dispatch, applyNot
|
|
|
10725
10915
|
if (!picked) return;
|
|
10726
10916
|
if (picked === ADD) {
|
|
10727
10917
|
const isSelf = machine.id === self.machine_id;
|
|
10728
|
-
const backend = isSelf ? localBrowseBackend(machine.name) : remoteBrowseBackend(api, machine.id, machine.name);
|
|
10918
|
+
const backend = isSelf ? localBrowseBackend(machine.name) : remoteBrowseBackend(api, accountStream, machine.id, machine.name);
|
|
10729
10919
|
const chosen = await pickDirectory(tui, backend, {
|
|
10730
10920
|
startPath: isSelf ? process.cwd() : null,
|
|
10731
10921
|
loader: startLoader
|