offhands 0.1.5 → 0.1.7
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/approval-mcp.mjs +50 -10
- package/dist/daemon.mjs +921 -123
- package/package.json +1 -1
- package/README.internal.md +0 -49
package/dist/daemon.mjs
CHANGED
|
@@ -79,9 +79,9 @@ var require_buffer_util = __commonJS({
|
|
|
79
79
|
}
|
|
80
80
|
return target;
|
|
81
81
|
}
|
|
82
|
-
function _mask(source, mask,
|
|
82
|
+
function _mask(source, mask, output2, offset, length) {
|
|
83
83
|
for (let i2 = 0; i2 < length; i2++) {
|
|
84
|
-
|
|
84
|
+
output2[offset + i2] = source[i2] ^ mask[i2 & 3];
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
function _unmask(buffer, mask) {
|
|
@@ -119,9 +119,9 @@ var require_buffer_util = __commonJS({
|
|
|
119
119
|
if (!process.env.WS_NO_BUFFER_UTIL) {
|
|
120
120
|
try {
|
|
121
121
|
const bufferUtil = __require("bufferutil");
|
|
122
|
-
module2.exports.mask = function(source, mask,
|
|
123
|
-
if (length < 48) _mask(source, mask,
|
|
124
|
-
else bufferUtil.mask(source, mask,
|
|
122
|
+
module2.exports.mask = function(source, mask, output2, offset, length) {
|
|
123
|
+
if (length < 48) _mask(source, mask, output2, offset, length);
|
|
124
|
+
else bufferUtil.mask(source, mask, output2, offset, length);
|
|
125
125
|
};
|
|
126
126
|
module2.exports.unmask = function(buffer, mask) {
|
|
127
127
|
if (buffer.length < 32) _unmask(buffer, mask);
|
|
@@ -4719,7 +4719,7 @@ var require_main = __commonJS({
|
|
|
4719
4719
|
var qrcode = new QRCode(-1, this.error);
|
|
4720
4720
|
qrcode.addData(input);
|
|
4721
4721
|
qrcode.make();
|
|
4722
|
-
var
|
|
4722
|
+
var output2 = "";
|
|
4723
4723
|
if (opts && opts.small) {
|
|
4724
4724
|
var BLACK = true, WHITE = false;
|
|
4725
4725
|
var moduleCount = qrcode.getModuleCount();
|
|
@@ -4736,37 +4736,37 @@ var require_main = __commonJS({
|
|
|
4736
4736
|
};
|
|
4737
4737
|
var borderTop = repeat(platte.BLACK_WHITE).times(moduleCount + 3);
|
|
4738
4738
|
var borderBottom = repeat(platte.WHITE_BLACK).times(moduleCount + 3);
|
|
4739
|
-
|
|
4739
|
+
output2 += borderTop + "\n";
|
|
4740
4740
|
for (var row = 0; row < moduleCount; row += 2) {
|
|
4741
|
-
|
|
4741
|
+
output2 += platte.WHITE_ALL;
|
|
4742
4742
|
for (var col = 0; col < moduleCount; col++) {
|
|
4743
4743
|
if (moduleData[row][col] === WHITE && moduleData[row + 1][col] === WHITE) {
|
|
4744
|
-
|
|
4744
|
+
output2 += platte.WHITE_ALL;
|
|
4745
4745
|
} else if (moduleData[row][col] === WHITE && moduleData[row + 1][col] === BLACK) {
|
|
4746
|
-
|
|
4746
|
+
output2 += platte.WHITE_BLACK;
|
|
4747
4747
|
} else if (moduleData[row][col] === BLACK && moduleData[row + 1][col] === WHITE) {
|
|
4748
|
-
|
|
4748
|
+
output2 += platte.BLACK_WHITE;
|
|
4749
4749
|
} else {
|
|
4750
|
-
|
|
4750
|
+
output2 += platte.BLACK_ALL;
|
|
4751
4751
|
}
|
|
4752
4752
|
}
|
|
4753
|
-
|
|
4753
|
+
output2 += platte.WHITE_ALL + "\n";
|
|
4754
4754
|
}
|
|
4755
4755
|
if (!oddRow) {
|
|
4756
|
-
|
|
4756
|
+
output2 += borderBottom;
|
|
4757
4757
|
}
|
|
4758
4758
|
} else {
|
|
4759
4759
|
var border = repeat(white).times(qrcode.getModuleCount() + 3);
|
|
4760
|
-
|
|
4760
|
+
output2 += border + "\n";
|
|
4761
4761
|
qrcode.modules.forEach(function(row2) {
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4762
|
+
output2 += white;
|
|
4763
|
+
output2 += row2.map(toCell).join("");
|
|
4764
|
+
output2 += white + "\n";
|
|
4765
4765
|
});
|
|
4766
|
-
|
|
4766
|
+
output2 += border;
|
|
4767
4767
|
}
|
|
4768
|
-
if (cb) cb(
|
|
4769
|
-
else console.log(
|
|
4768
|
+
if (cb) cb(output2);
|
|
4769
|
+
else console.log(output2);
|
|
4770
4770
|
},
|
|
4771
4771
|
setErrorLevel: function(error) {
|
|
4772
4772
|
this.error = QRErrorCorrectLevel[error] || this.error;
|
|
@@ -4777,7 +4777,7 @@ var require_main = __commonJS({
|
|
|
4777
4777
|
|
|
4778
4778
|
// ../daemon/src/index.ts
|
|
4779
4779
|
import { resolve as resolve5 } from "node:path";
|
|
4780
|
-
import { existsSync as
|
|
4780
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
4781
4781
|
|
|
4782
4782
|
// ../daemon/src/runners/claude-code.ts
|
|
4783
4783
|
import { spawn } from "node:child_process";
|
|
@@ -4862,10 +4862,12 @@ function mapClaudeEvent(value) {
|
|
|
4862
4862
|
for (const block of content) {
|
|
4863
4863
|
if (block?.type === "tool_use" && typeof block.name === "string") {
|
|
4864
4864
|
if (block.name.startsWith("mcp__offhand__")) continue;
|
|
4865
|
+
const todos = block.name === "TodoWrite" ? extractTodos(block.input) : void 0;
|
|
4865
4866
|
events.push({
|
|
4866
4867
|
type: "tool",
|
|
4867
4868
|
name: block.name,
|
|
4868
|
-
summary: summariseToolInput(block.name, block.input)
|
|
4869
|
+
summary: todos ? summariseTodos(todos) : summariseToolInput(block.name, block.input),
|
|
4870
|
+
...todos ? { todos } : {}
|
|
4869
4871
|
});
|
|
4870
4872
|
}
|
|
4871
4873
|
}
|
|
@@ -4894,6 +4896,29 @@ function contextUsage(usage, costUsd) {
|
|
|
4894
4896
|
if (contextTokens <= 0) return null;
|
|
4895
4897
|
return { type: "usage", contextTokens, contextWindow: 2e5, ...costUsd !== void 0 ? { costUsd } : {} };
|
|
4896
4898
|
}
|
|
4899
|
+
function extractTodos(input) {
|
|
4900
|
+
if (typeof input !== "object" || input === null) return void 0;
|
|
4901
|
+
const list = input.todos;
|
|
4902
|
+
if (!Array.isArray(list)) return void 0;
|
|
4903
|
+
const todos = [];
|
|
4904
|
+
for (const item of list) {
|
|
4905
|
+
if (typeof item !== "object" || item === null) return void 0;
|
|
4906
|
+
const t2 = item;
|
|
4907
|
+
if (typeof t2.content !== "string") return void 0;
|
|
4908
|
+
if (t2.status !== "pending" && t2.status !== "in_progress" && t2.status !== "completed") return void 0;
|
|
4909
|
+
todos.push({
|
|
4910
|
+
content: t2.content,
|
|
4911
|
+
status: t2.status,
|
|
4912
|
+
...typeof t2.activeForm === "string" ? { activeForm: t2.activeForm } : {}
|
|
4913
|
+
});
|
|
4914
|
+
}
|
|
4915
|
+
return todos;
|
|
4916
|
+
}
|
|
4917
|
+
function summariseTodos(todos) {
|
|
4918
|
+
const active = todos.find((t2) => t2.status === "in_progress");
|
|
4919
|
+
const done = todos.filter((t2) => t2.status === "completed").length;
|
|
4920
|
+
return active ? `TodoWrite: ${active.activeForm ?? active.content} (${done}/${todos.length} done)` : `TodoWrite: ${done}/${todos.length} done`;
|
|
4921
|
+
}
|
|
4897
4922
|
function summariseToolInput(name, input) {
|
|
4898
4923
|
if (typeof input !== "object" || input === null) return name;
|
|
4899
4924
|
const i2 = input;
|
|
@@ -4941,8 +4966,16 @@ var ClaudeCodeRunner = class {
|
|
|
4941
4966
|
}
|
|
4942
4967
|
id = "claude-code";
|
|
4943
4968
|
name = "Claude Code";
|
|
4944
|
-
|
|
4969
|
+
// No enumeration command exists (checked `claude --help` in full against
|
|
4970
|
+
// CLI v2.1.263 — no `models` subcommand, no --list-models flag). These are
|
|
4971
|
+
// the aliases `--model`'s own help text names as examples ('fable', 'opus',
|
|
4972
|
+
// 'sonnet'), plus 'haiku' (long-standing, not superseded by anything found
|
|
4973
|
+
// here) — hardcoded because there is genuinely nothing to query, not out
|
|
4974
|
+
// of laziness. Re-verify against `claude --help` when bumping this list.
|
|
4975
|
+
models = ["sonnet", "opus", "haiku", "fable"];
|
|
4945
4976
|
supportsApprovals = true;
|
|
4977
|
+
// Real: every tier maps to a MAX_THINKING_TOKENS value below (see start()).
|
|
4978
|
+
effortTiers = ["low", "medium", "high", "max"];
|
|
4946
4979
|
loggedIn() {
|
|
4947
4980
|
return existsSync(join(homedir(), ".claude", ".credentials.json"));
|
|
4948
4981
|
}
|
|
@@ -4988,7 +5021,15 @@ var ClaudeCodeRunner = class {
|
|
|
4988
5021
|
offhand: {
|
|
4989
5022
|
command: process.execPath,
|
|
4990
5023
|
args: [APPROVAL_MCP_PATH],
|
|
4991
|
-
|
|
5024
|
+
// OFFHAND_STATUS_URL is derived from approvalUrl rather than a
|
|
5025
|
+
// second constructor param — same daemon, same port, one fewer
|
|
5026
|
+
// thing to keep in sync as this.approvalUrl already carries the
|
|
5027
|
+
// host:port everything else here needs.
|
|
5028
|
+
env: {
|
|
5029
|
+
OFFHAND_APPROVAL_URL: this.approvalUrl,
|
|
5030
|
+
OFFHAND_STATUS_URL: this.approvalUrl.replace(/\/approval$/, "/status"),
|
|
5031
|
+
OFFHAND_SESSION_ID: run.sessionId
|
|
5032
|
+
}
|
|
4992
5033
|
}
|
|
4993
5034
|
}
|
|
4994
5035
|
};
|
|
@@ -5174,6 +5215,10 @@ var CopilotCliRunner = class {
|
|
|
5174
5215
|
models = [];
|
|
5175
5216
|
// no headless enumeration in 1.0.31; CLI default
|
|
5176
5217
|
supportsApprovals = false;
|
|
5218
|
+
// No effortTiers: start() never reads run.effort — there's no thinking-
|
|
5219
|
+
// budget flag in Copilot CLI 1.0.31's non-interactive mode. Leaving this
|
|
5220
|
+
// undefined (not an empty array) is what tells the phone to hide the
|
|
5221
|
+
// effort control here rather than show one that silently does nothing.
|
|
5177
5222
|
/** [command, ...prefixArgs] resolved once. */
|
|
5178
5223
|
resolved = null;
|
|
5179
5224
|
resolveCommand() {
|
|
@@ -5394,6 +5439,10 @@ var OpenCodeRunner = class {
|
|
|
5394
5439
|
supportsApprovals = true;
|
|
5395
5440
|
models = [];
|
|
5396
5441
|
// populated dynamically via `opencode models`
|
|
5442
|
+
// Real: every tier maps to a --variant value below (see startViaCli()) —
|
|
5443
|
+
// low/medium collide onto minimal/high internally, but all four inputs
|
|
5444
|
+
// are genuinely honored, just not 1:1 named.
|
|
5445
|
+
effortTiers = ["low", "medium", "high", "max"];
|
|
5397
5446
|
/** [command, ...prefixArgs] resolved once. */
|
|
5398
5447
|
resolved = null;
|
|
5399
5448
|
modelsCache = [];
|
|
@@ -5462,15 +5511,15 @@ var OpenCodeRunner = class {
|
|
|
5462
5511
|
stdio: ["ignore", "pipe", "ignore"],
|
|
5463
5512
|
shell: process.platform === "win32"
|
|
5464
5513
|
});
|
|
5465
|
-
let
|
|
5514
|
+
let output2 = "";
|
|
5466
5515
|
p2.stdout.setEncoding("utf8");
|
|
5467
5516
|
p2.stdout.on("data", (chunk) => {
|
|
5468
|
-
|
|
5517
|
+
output2 += chunk;
|
|
5469
5518
|
});
|
|
5470
5519
|
p2.on("close", (code) => {
|
|
5471
5520
|
if (code === 0) {
|
|
5472
5521
|
try {
|
|
5473
|
-
const lines =
|
|
5522
|
+
const lines = output2.trim().split("\n");
|
|
5474
5523
|
const models = lines.map((l2) => l2.trim()).filter((l2) => l2.length > 0);
|
|
5475
5524
|
this.modelsCache = models;
|
|
5476
5525
|
Object.defineProperty(this, "models", {
|
|
@@ -5652,6 +5701,14 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5652
5701
|
sessionId = session.id;
|
|
5653
5702
|
}
|
|
5654
5703
|
callbacks?.onConversationId?.(sessionId);
|
|
5704
|
+
const baselineMessageIds = /* @__PURE__ */ new Set();
|
|
5705
|
+
if (run.resumeConversationId) {
|
|
5706
|
+
try {
|
|
5707
|
+
const existing = await httpJson(`${base}/session/${sessionId}/message?directory=${encDir}`, "GET");
|
|
5708
|
+
for (const row of existing) baselineMessageIds.add(row.info.id);
|
|
5709
|
+
} catch {
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5655
5712
|
const parts = [{ type: "text", text: run.prompt }];
|
|
5656
5713
|
for (const file of run.attachments ?? []) {
|
|
5657
5714
|
parts.push({ type: "file", mime: "application/octet-stream", url: fileUrl(file) });
|
|
@@ -5660,7 +5717,7 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5660
5717
|
const modelRef = parseModelRef(run.model);
|
|
5661
5718
|
if (modelRef) promptBody.model = { providerID: modelRef.providerId, modelID: modelRef.modelId };
|
|
5662
5719
|
await httpJson(`${base}/session/${sessionId}/prompt_async?directory=${encDir}`, "POST", promptBody, true);
|
|
5663
|
-
await this.pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, () => cancelled);
|
|
5720
|
+
await this.pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, baselineMessageIds, () => cancelled);
|
|
5664
5721
|
} catch (e) {
|
|
5665
5722
|
if (!cancelled) emit([{ type: "error", message: `opencode HTTP API error: ${String(e)}` }]);
|
|
5666
5723
|
} finally {
|
|
@@ -5689,7 +5746,7 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5689
5746
|
};
|
|
5690
5747
|
}
|
|
5691
5748
|
/** Poll session messages + pending permissions/questions until the turn finishes. */
|
|
5692
|
-
async pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, isCancelled) {
|
|
5749
|
+
async pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, baselineMessageIds, isCancelled) {
|
|
5693
5750
|
const seenPermissions = /* @__PURE__ */ new Set();
|
|
5694
5751
|
const seenQuestions = /* @__PURE__ */ new Set();
|
|
5695
5752
|
const emittedParts = /* @__PURE__ */ new Set();
|
|
@@ -5721,21 +5778,9 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5721
5778
|
} catch {
|
|
5722
5779
|
continue;
|
|
5723
5780
|
}
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
if (!part.id || emittedParts.has(part.id)) continue;
|
|
5728
|
-
if ((part.type === "text" || part.type === "reasoning") && !part.time?.end) continue;
|
|
5729
|
-
emittedParts.add(part.id);
|
|
5730
|
-
const events = mapOpenCodeEvent({ part });
|
|
5731
|
-
emit(events);
|
|
5732
|
-
if (events.some((e) => e.type === "done" || e.type === "error")) sawTerminal = true;
|
|
5733
|
-
}
|
|
5734
|
-
if (!sawTerminal && row.info.error) {
|
|
5735
|
-
sawTerminal = true;
|
|
5736
|
-
emit([{ type: "error", message: row.info.error.data?.message ?? "run failed" }]);
|
|
5737
|
-
}
|
|
5738
|
-
}
|
|
5781
|
+
const result = processMessages(messages, baselineMessageIds, emittedParts);
|
|
5782
|
+
emit(result.events);
|
|
5783
|
+
if (result.sawTerminal) sawTerminal = true;
|
|
5739
5784
|
}
|
|
5740
5785
|
}
|
|
5741
5786
|
/** Route one pending permission through the shared broker, then reply. */
|
|
@@ -5816,6 +5861,26 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5816
5861
|
}
|
|
5817
5862
|
}
|
|
5818
5863
|
};
|
|
5864
|
+
function processMessages(messages, baselineMessageIds, emittedParts) {
|
|
5865
|
+
const events = [];
|
|
5866
|
+
let sawTerminal = false;
|
|
5867
|
+
for (const row of messages) {
|
|
5868
|
+
if (row.info.role !== "assistant" || baselineMessageIds.has(row.info.id)) continue;
|
|
5869
|
+
for (const part of row.parts) {
|
|
5870
|
+
if (!part.id || emittedParts.has(part.id)) continue;
|
|
5871
|
+
if ((part.type === "text" || part.type === "reasoning") && !part.time?.end) continue;
|
|
5872
|
+
emittedParts.add(part.id);
|
|
5873
|
+
const partEvents = mapOpenCodeEvent({ part });
|
|
5874
|
+
events.push(...partEvents);
|
|
5875
|
+
if (partEvents.some((e) => e.type === "done" || e.type === "error")) sawTerminal = true;
|
|
5876
|
+
}
|
|
5877
|
+
if (!sawTerminal && row.info.error) {
|
|
5878
|
+
sawTerminal = true;
|
|
5879
|
+
events.push({ type: "error", message: row.info.error.data?.message ?? "run failed" });
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
return { events, sawTerminal };
|
|
5883
|
+
}
|
|
5819
5884
|
function buildPermissionRuleset(mode) {
|
|
5820
5885
|
const base = [{ permission: "*", pattern: "*", action: "allow" }];
|
|
5821
5886
|
if (mode === "acceptEdits") {
|
|
@@ -5886,8 +5951,8 @@ function sleep(ms) {
|
|
|
5886
5951
|
|
|
5887
5952
|
// ../daemon/src/session-manager.ts
|
|
5888
5953
|
import { randomUUID } from "node:crypto";
|
|
5889
|
-
import { hostname, platform, tmpdir } from "node:os";
|
|
5890
|
-
import { existsSync as
|
|
5954
|
+
import { hostname, platform as platform2, tmpdir } from "node:os";
|
|
5955
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5891
5956
|
import { dirname as dirname2, join as join6, basename as basename3, parse as parse2, resolve as resolve3 } from "node:path";
|
|
5892
5957
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5893
5958
|
|
|
@@ -6234,6 +6299,267 @@ function mimeFromName(path) {
|
|
|
6234
6299
|
}[ext] ?? "application/octet-stream";
|
|
6235
6300
|
}
|
|
6236
6301
|
|
|
6302
|
+
// ../daemon/src/handover.ts
|
|
6303
|
+
var MAX_DIFF_BYTES2 = 48 * 1024;
|
|
6304
|
+
var MAX_TOOL_SUMMARIES = 30;
|
|
6305
|
+
var MAX_RECENT_MESSAGES = 10;
|
|
6306
|
+
async function liveDiff(workspace) {
|
|
6307
|
+
const diff = await git(workspace, "diff", "HEAD");
|
|
6308
|
+
if (!diff) return "";
|
|
6309
|
+
return diff.length > MAX_DIFF_BYTES2 ? diff.slice(0, MAX_DIFF_BYTES2) + "\n\u2026 diff truncated \u2026" : diff;
|
|
6310
|
+
}
|
|
6311
|
+
function assembleHandoverPacket(items, diff) {
|
|
6312
|
+
const toolSummaries = [];
|
|
6313
|
+
let todos;
|
|
6314
|
+
const recentMessages = [];
|
|
6315
|
+
const resolvedApprovalIds = /* @__PURE__ */ new Set();
|
|
6316
|
+
const approvalsById = /* @__PURE__ */ new Map();
|
|
6317
|
+
for (const msg of items) {
|
|
6318
|
+
if (msg.type === "run-started") {
|
|
6319
|
+
recentMessages.push({ role: "user", text: msg.prompt });
|
|
6320
|
+
continue;
|
|
6321
|
+
}
|
|
6322
|
+
if (msg.type !== "run-event") continue;
|
|
6323
|
+
const ev = msg.event;
|
|
6324
|
+
if (ev.type === "tool") {
|
|
6325
|
+
toolSummaries.push(ev.summary);
|
|
6326
|
+
if (ev.todos) todos = ev.todos;
|
|
6327
|
+
} else if (ev.type === "text") {
|
|
6328
|
+
const last = recentMessages[recentMessages.length - 1];
|
|
6329
|
+
if (last?.role === "assistant") last.text += ev.chunk;
|
|
6330
|
+
else recentMessages.push({ role: "assistant", text: ev.chunk });
|
|
6331
|
+
} else if (ev.type === "approval") {
|
|
6332
|
+
approvalsById.set(ev.id, { action: ev.action, detail: ev.detail, risk: ev.risk });
|
|
6333
|
+
} else if (ev.type === "approval-result") {
|
|
6334
|
+
resolvedApprovalIds.add(ev.id);
|
|
6335
|
+
}
|
|
6336
|
+
}
|
|
6337
|
+
const pendingApprovals = [...approvalsById.entries()].filter(([id]) => !resolvedApprovalIds.has(id)).map(([, a2]) => a2);
|
|
6338
|
+
return {
|
|
6339
|
+
diff,
|
|
6340
|
+
toolSummaries: toolSummaries.slice(-MAX_TOOL_SUMMARIES),
|
|
6341
|
+
...todos ? { todos } : {},
|
|
6342
|
+
pendingApprovals,
|
|
6343
|
+
recentMessages: recentMessages.slice(-MAX_RECENT_MESSAGES)
|
|
6344
|
+
};
|
|
6345
|
+
}
|
|
6346
|
+
function translatePacketToPrompt(packet, fromRunnerName) {
|
|
6347
|
+
const sections = [
|
|
6348
|
+
`You're continuing a task handed off from ${fromRunnerName}, which hit its usage limit mid-task. Here is the context so far.`
|
|
6349
|
+
];
|
|
6350
|
+
if (packet.recentMessages.length > 0) {
|
|
6351
|
+
sections.push(
|
|
6352
|
+
"## Recent conversation\n" + packet.recentMessages.map((m3) => `**${m3.role}:** ${m3.text}`).join("\n\n")
|
|
6353
|
+
);
|
|
6354
|
+
}
|
|
6355
|
+
if (packet.toolSummaries.length > 0) {
|
|
6356
|
+
sections.push("## Actions already taken\n" + packet.toolSummaries.map((t2) => `- ${t2}`).join("\n"));
|
|
6357
|
+
}
|
|
6358
|
+
if (packet.todos && packet.todos.length > 0) {
|
|
6359
|
+
sections.push(
|
|
6360
|
+
"## Remaining plan\n" + packet.todos.map((t2) => `- [${t2.status === "completed" ? "x" : " "}] ${t2.content}`).join("\n")
|
|
6361
|
+
);
|
|
6362
|
+
}
|
|
6363
|
+
if (packet.pendingApprovals.length > 0) {
|
|
6364
|
+
sections.push(
|
|
6365
|
+
"## Not yet approved (ask again if still needed)\n" + packet.pendingApprovals.map((a2) => `- ${a2.action}: ${a2.detail} (${a2.risk} risk)`).join("\n")
|
|
6366
|
+
);
|
|
6367
|
+
}
|
|
6368
|
+
if (packet.diff.trim()) {
|
|
6369
|
+
sections.push("## Uncommitted changes so far (git diff)\n```diff\n" + packet.diff + "\n```");
|
|
6370
|
+
}
|
|
6371
|
+
if (packet.agentSummary) {
|
|
6372
|
+
sections.push("## Outgoing agent's own summary\n" + packet.agentSummary);
|
|
6373
|
+
}
|
|
6374
|
+
sections.push("Please continue the task from here.");
|
|
6375
|
+
return sections.join("\n\n");
|
|
6376
|
+
}
|
|
6377
|
+
|
|
6378
|
+
// ../daemon/src/wol.ts
|
|
6379
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
6380
|
+
import { promisify as promisify2 } from "node:util";
|
|
6381
|
+
import { networkInterfaces, platform } from "node:os";
|
|
6382
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
6383
|
+
var exec2 = promisify2(execFile2);
|
|
6384
|
+
function pickPrimaryInterface(ifaces) {
|
|
6385
|
+
for (const [name, infos] of Object.entries(ifaces)) {
|
|
6386
|
+
if (!infos) continue;
|
|
6387
|
+
const ipv4 = infos.find((i2) => i2.family === "IPv4" && !i2.internal);
|
|
6388
|
+
if (ipv4?.mac && ipv4.mac !== "00:00:00:00:00:00") {
|
|
6389
|
+
return { name, mac: ipv4.mac, address: ipv4.address, netmask: ipv4.netmask };
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
return null;
|
|
6393
|
+
}
|
|
6394
|
+
function pickInterfaceByName(ifaces, name) {
|
|
6395
|
+
const infos = ifaces[name];
|
|
6396
|
+
if (!infos) return null;
|
|
6397
|
+
const ipv4 = infos.find((i2) => i2.family === "IPv4" && !i2.internal);
|
|
6398
|
+
if (!ipv4?.mac || ipv4.mac === "00:00:00:00:00:00") return null;
|
|
6399
|
+
return { name, mac: ipv4.mac, address: ipv4.address, netmask: ipv4.netmask };
|
|
6400
|
+
}
|
|
6401
|
+
function computeBroadcastAddress(address, netmask) {
|
|
6402
|
+
const a2 = address.split(".").map(Number);
|
|
6403
|
+
const m3 = netmask.split(".").map(Number);
|
|
6404
|
+
if (a2.length !== 4 || m3.length !== 4 || a2.some(Number.isNaN) || m3.some(Number.isNaN)) return null;
|
|
6405
|
+
return a2.map((octet, i2) => (octet | ~m3[i2] & 255) & 255).join(".");
|
|
6406
|
+
}
|
|
6407
|
+
function normalizeMac(mac) {
|
|
6408
|
+
return (mac ?? "").toLowerCase().replace(/[:-]/g, "");
|
|
6409
|
+
}
|
|
6410
|
+
function classifyWindowsMediaType(mediaType) {
|
|
6411
|
+
const t2 = (mediaType ?? "").toLowerCase();
|
|
6412
|
+
if (t2.includes("802.11") || t2.includes("wireless") || t2.includes("native 802.11")) return "wifi";
|
|
6413
|
+
if (t2.includes("802.3") || t2.includes("ethernet")) return "ethernet";
|
|
6414
|
+
return "unknown";
|
|
6415
|
+
}
|
|
6416
|
+
function parseWindowsAdapterJson(json) {
|
|
6417
|
+
let parsed;
|
|
6418
|
+
try {
|
|
6419
|
+
parsed = JSON.parse(json);
|
|
6420
|
+
} catch {
|
|
6421
|
+
return [];
|
|
6422
|
+
}
|
|
6423
|
+
const arr = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
|
|
6424
|
+
return arr.filter((x2) => !!x2 && typeof x2 === "object").map((x2) => ({
|
|
6425
|
+
name: String(x2.Name ?? ""),
|
|
6426
|
+
macAddress: x2.MacAddress ? String(x2.MacAddress) : null,
|
|
6427
|
+
networkType: classifyWindowsMediaType(x2.MediaType),
|
|
6428
|
+
wakeOnMagicPacket: x2.WakeOnMagicPacket === "Enabled" || x2.WakeOnMagicPacket === "Disabled" || x2.WakeOnMagicPacket === "NotSupported" ? x2.WakeOnMagicPacket : null
|
|
6429
|
+
}));
|
|
6430
|
+
}
|
|
6431
|
+
function parseMacPmset(output2) {
|
|
6432
|
+
const m3 = output2.match(/^\s*womp\s+(\d)/m);
|
|
6433
|
+
return m3 ? m3[1] === "1" : false;
|
|
6434
|
+
}
|
|
6435
|
+
function classifyMacHardwarePort(listing, deviceName) {
|
|
6436
|
+
const blocks = listing.split(/\n\n+/);
|
|
6437
|
+
for (const block of blocks) {
|
|
6438
|
+
if (!new RegExp(`Device:\\s*${deviceName}\\b`).test(block)) continue;
|
|
6439
|
+
if (/Hardware Port:\s*Wi-Fi/i.test(block)) return "wifi";
|
|
6440
|
+
if (/Hardware Port:\s*(Ethernet|Thunderbolt Ethernet)/i.test(block)) return "ethernet";
|
|
6441
|
+
}
|
|
6442
|
+
return "unknown";
|
|
6443
|
+
}
|
|
6444
|
+
function parseLinuxEthtool(output2) {
|
|
6445
|
+
let supports = "";
|
|
6446
|
+
let current = "";
|
|
6447
|
+
for (const line of output2.split(/\r?\n/)) {
|
|
6448
|
+
const supportsMatch = line.match(/^\s*Supports Wake-on:\s*([a-zA-Z]*)/);
|
|
6449
|
+
if (supportsMatch) {
|
|
6450
|
+
supports = supportsMatch[1];
|
|
6451
|
+
continue;
|
|
6452
|
+
}
|
|
6453
|
+
const currentMatch = line.match(/^\s*Wake-on:\s*([a-zA-Z]*)/);
|
|
6454
|
+
if (currentMatch) current = currentMatch[1];
|
|
6455
|
+
}
|
|
6456
|
+
return { supportsWakeOnG: supports.toLowerCase().includes("g"), wakeOnGEnabled: current.toLowerCase().includes("g") };
|
|
6457
|
+
}
|
|
6458
|
+
function computeWolCapability(networkType, wolWorks) {
|
|
6459
|
+
if (!wolWorks) {
|
|
6460
|
+
return {
|
|
6461
|
+
capability: "unsupported",
|
|
6462
|
+
reason: networkType === "wifi" ? "Wake-on-Wireless-LAN isn't enabled on this network adapter." : "Wake-on-LAN isn't enabled (or isn't supported) on this network adapter."
|
|
6463
|
+
};
|
|
6464
|
+
}
|
|
6465
|
+
if (networkType === "wifi") {
|
|
6466
|
+
return {
|
|
6467
|
+
capability: "wifi-limited",
|
|
6468
|
+
reason: "Wake-on-Wireless-LAN is enabled, but WiFi wake support depends on the router too \u2014 less reliable than a wired connection."
|
|
6469
|
+
};
|
|
6470
|
+
}
|
|
6471
|
+
if (networkType === "ethernet") {
|
|
6472
|
+
return { capability: "supported", reason: "Wake-on-LAN is enabled on this wired connection." };
|
|
6473
|
+
}
|
|
6474
|
+
return { capability: "unsupported", reason: "Couldn't determine this machine's network connection type." };
|
|
6475
|
+
}
|
|
6476
|
+
var WINDOWS_ADAPTER_SCRIPT = "Get-NetAdapter -Physical | Where-Object Status -eq 'Up' | ForEach-Object { $pm = Get-NetAdapterPowerManagement -Name $_.Name -ErrorAction SilentlyContinue; [PSCustomObject]@{ Name = $_.Name; MediaType = [string]$_.PhysicalMediaType; MacAddress = $_.MacAddress; WakeOnMagicPacket = [string]$pm.WakeOnMagicPacket } } | ConvertTo-Json";
|
|
6477
|
+
async function detectWindows(primary) {
|
|
6478
|
+
const { stdout } = await exec2("powershell", ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_ADAPTER_SCRIPT], {
|
|
6479
|
+
timeout: 8e3
|
|
6480
|
+
});
|
|
6481
|
+
const adapters = parseWindowsAdapterJson(stdout);
|
|
6482
|
+
const match = adapters.find((a2) => normalizeMac(a2.macAddress) === normalizeMac(primary.mac)) ?? adapters[0];
|
|
6483
|
+
if (!match) throw new Error("no active physical adapter reported by Get-NetAdapter");
|
|
6484
|
+
return { networkType: match.networkType, wolWorks: match.wakeOnMagicPacket === "Enabled" };
|
|
6485
|
+
}
|
|
6486
|
+
async function detectMac(primary) {
|
|
6487
|
+
const { stdout: hardwarePorts } = await exec2("networksetup", ["-listallhardwareports"], { timeout: 8e3 });
|
|
6488
|
+
const networkType = classifyMacHardwarePort(hardwarePorts, primary.name);
|
|
6489
|
+
const { stdout: pmsetOut } = await exec2("pmset", ["-g"], { timeout: 8e3 });
|
|
6490
|
+
return { networkType, wolWorks: parseMacPmset(pmsetOut) };
|
|
6491
|
+
}
|
|
6492
|
+
async function detectLinux(primary) {
|
|
6493
|
+
const networkType = existsSync6(`/sys/class/net/${primary.name}/wireless`) ? "wifi" : "ethernet";
|
|
6494
|
+
const { stdout } = await exec2("ethtool", [primary.name], { timeout: 8e3 });
|
|
6495
|
+
const { supportsWakeOnG, wakeOnGEnabled } = parseLinuxEthtool(stdout);
|
|
6496
|
+
return { networkType, wolWorks: supportsWakeOnG && wakeOnGEnabled };
|
|
6497
|
+
}
|
|
6498
|
+
async function getDefaultRouteInterfaceName(plat) {
|
|
6499
|
+
try {
|
|
6500
|
+
if (plat === "win32") {
|
|
6501
|
+
const { stdout } = await exec2(
|
|
6502
|
+
"powershell",
|
|
6503
|
+
[
|
|
6504
|
+
"-NoProfile",
|
|
6505
|
+
"-NonInteractive",
|
|
6506
|
+
"-Command",
|
|
6507
|
+
"Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue | Sort-Object RouteMetric | Select-Object -First 1 -ExpandProperty InterfaceAlias"
|
|
6508
|
+
],
|
|
6509
|
+
{ timeout: 8e3 }
|
|
6510
|
+
);
|
|
6511
|
+
return stdout.trim() || null;
|
|
6512
|
+
}
|
|
6513
|
+
if (plat === "darwin") {
|
|
6514
|
+
const { stdout } = await exec2("route", ["-n", "get", "default"], { timeout: 8e3 });
|
|
6515
|
+
const m3 = stdout.match(/interface:\s*(\S+)/);
|
|
6516
|
+
return m3 ? m3[1] : null;
|
|
6517
|
+
}
|
|
6518
|
+
if (plat === "linux") {
|
|
6519
|
+
const { stdout } = await exec2("ip", ["route", "show", "default"], { timeout: 8e3 });
|
|
6520
|
+
const m3 = stdout.match(/\bdev\s+(\S+)/);
|
|
6521
|
+
return m3 ? m3[1] : null;
|
|
6522
|
+
}
|
|
6523
|
+
return null;
|
|
6524
|
+
} catch {
|
|
6525
|
+
return null;
|
|
6526
|
+
}
|
|
6527
|
+
}
|
|
6528
|
+
async function probeWolCapability() {
|
|
6529
|
+
const checkedAtMs = Date.now();
|
|
6530
|
+
const plat = platform();
|
|
6531
|
+
const ifaces = networkInterfaces();
|
|
6532
|
+
const routeIfaceName = await getDefaultRouteInterfaceName(plat);
|
|
6533
|
+
const primary = routeIfaceName && pickInterfaceByName(ifaces, routeIfaceName) || pickPrimaryInterface(ifaces);
|
|
6534
|
+
if (!primary) {
|
|
6535
|
+
return {
|
|
6536
|
+
capability: "unsupported",
|
|
6537
|
+
reason: "Couldn't find an active network interface to check.",
|
|
6538
|
+
networkType: "unknown",
|
|
6539
|
+
mac: null,
|
|
6540
|
+
broadcast: null,
|
|
6541
|
+
checkedAtMs
|
|
6542
|
+
};
|
|
6543
|
+
}
|
|
6544
|
+
const broadcast = computeBroadcastAddress(primary.address, primary.netmask);
|
|
6545
|
+
try {
|
|
6546
|
+
const detected = plat === "win32" ? await detectWindows(primary) : plat === "darwin" ? await detectMac(primary) : plat === "linux" ? await detectLinux(primary) : (() => {
|
|
6547
|
+
throw new Error(`unsupported OS: ${plat}`);
|
|
6548
|
+
})();
|
|
6549
|
+
const { capability, reason } = computeWolCapability(detected.networkType, detected.wolWorks);
|
|
6550
|
+
return { capability, reason, networkType: detected.networkType, mac: primary.mac, broadcast, checkedAtMs };
|
|
6551
|
+
} catch (e) {
|
|
6552
|
+
return {
|
|
6553
|
+
capability: "unsupported",
|
|
6554
|
+
reason: `Couldn't check Wake-on-LAN support: ${e instanceof Error ? e.message : String(e)}`,
|
|
6555
|
+
networkType: "unknown",
|
|
6556
|
+
mac: primary.mac,
|
|
6557
|
+
broadcast,
|
|
6558
|
+
checkedAtMs
|
|
6559
|
+
};
|
|
6560
|
+
}
|
|
6561
|
+
}
|
|
6562
|
+
|
|
6237
6563
|
// ../daemon/src/session-manager.ts
|
|
6238
6564
|
function resolveDaemonVersion() {
|
|
6239
6565
|
try {
|
|
@@ -6245,6 +6571,9 @@ function resolveDaemonVersion() {
|
|
|
6245
6571
|
}
|
|
6246
6572
|
}
|
|
6247
6573
|
var DAEMON_VERSION = resolveDaemonVersion();
|
|
6574
|
+
var SYSTEM_LOG_SESSION_ID = "system";
|
|
6575
|
+
var MOBILE_SESSION_PREAMBLE_BASE = "You're being driven remotely from a phone via offhand \u2014 the person who sent this prompt isn't at this keyboard and can't see this terminal. Lean on visual proof (screenshots, diffs, clear file paths) over long text explanations where you can, since they're reading this on a phone screen, not a terminal.";
|
|
6576
|
+
var MOBILE_SESSION_PREAMBLE_STATUS_TOOL = " You have an offhand_status_update tool available \u2014 call it with a short plain-English note at meaningful checkpoints (starting a long step, hitting a snag, finishing a phase) so they see progress without waiting for the whole run to end.";
|
|
6248
6577
|
var SessionManager = class {
|
|
6249
6578
|
constructor(store2, runners2) {
|
|
6250
6579
|
this.store = store2;
|
|
@@ -6259,6 +6588,14 @@ var SessionManager = class {
|
|
|
6259
6588
|
// sessionId → queued prompts
|
|
6260
6589
|
runners = /* @__PURE__ */ new Map();
|
|
6261
6590
|
runnerAvailability = /* @__PURE__ */ new Map();
|
|
6591
|
+
/** handoverId → what was proposed, so `handover-response` (which only
|
|
6592
|
+
* carries the id back) knows what to act on. */
|
|
6593
|
+
pendingHandovers = /* @__PURE__ */ new Map();
|
|
6594
|
+
/** sessionId → the most recent offhand_status_update note (Execution Plan
|
|
6595
|
+
* 5) — becomes a Handover Packet's optional agentSummary when a handover
|
|
6596
|
+
* is proposed afterward. Still zero hosted AI: the agent chose to send
|
|
6597
|
+
* this, on its own tokens, no different from any other tool call. */
|
|
6598
|
+
latestStatusUpdate = /* @__PURE__ */ new Map();
|
|
6262
6599
|
/** Optional artifact plumbing (set by index when capture is possible). */
|
|
6263
6600
|
uploader = null;
|
|
6264
6601
|
capture = null;
|
|
@@ -6276,6 +6613,21 @@ var SessionManager = class {
|
|
|
6276
6613
|
for (const [id, r2] of this.runners) {
|
|
6277
6614
|
this.runnerAvailability.set(id, await r2.detect());
|
|
6278
6615
|
}
|
|
6616
|
+
void this.recheckWolCapability();
|
|
6617
|
+
}
|
|
6618
|
+
/** Runs the real Wake-on-LAN/WiFi probe and persists the result — called
|
|
6619
|
+
* once at startup and again on-demand via the phone's "check again"
|
|
6620
|
+
* action (Execution Plan 7), since drivers/BIOS/network type can change. */
|
|
6621
|
+
async recheckWolCapability() {
|
|
6622
|
+
const result = await probeWolCapability();
|
|
6623
|
+
this.store.setWolCapability({
|
|
6624
|
+
capability: result.capability,
|
|
6625
|
+
reason: result.reason,
|
|
6626
|
+
networkType: result.networkType,
|
|
6627
|
+
mac: result.mac,
|
|
6628
|
+
broadcast: result.broadcast,
|
|
6629
|
+
checkedAtMs: result.checkedAtMs
|
|
6630
|
+
});
|
|
6279
6631
|
}
|
|
6280
6632
|
// ---- transport attachment --------------------------------------------------
|
|
6281
6633
|
attach(sink) {
|
|
@@ -6283,10 +6635,16 @@ var SessionManager = class {
|
|
|
6283
6635
|
return () => this.sinks.delete(sink);
|
|
6284
6636
|
}
|
|
6285
6637
|
hello() {
|
|
6638
|
+
const wol = this.store.getWolCapability();
|
|
6286
6639
|
return {
|
|
6287
6640
|
type: "hello",
|
|
6288
6641
|
protocol: 2,
|
|
6289
|
-
host: {
|
|
6642
|
+
host: {
|
|
6643
|
+
hostname: hostname(),
|
|
6644
|
+
os: platform2(),
|
|
6645
|
+
daemonVersion: DAEMON_VERSION,
|
|
6646
|
+
...wol ? { wolCapability: wol.capability, wolReason: wol.reason, wolCheckedAtMs: wol.checkedAtMs } : {}
|
|
6647
|
+
},
|
|
6290
6648
|
lastSeq: this.store.lastSeq()
|
|
6291
6649
|
};
|
|
6292
6650
|
}
|
|
@@ -6297,7 +6655,8 @@ var SessionManager = class {
|
|
|
6297
6655
|
available: this.runnerAvailability.get(r2.id) ?? false,
|
|
6298
6656
|
...r2.loggedIn ? { loggedIn: r2.loggedIn() } : {},
|
|
6299
6657
|
models: r2.models,
|
|
6300
|
-
supportsApprovals: r2.supportsApprovals
|
|
6658
|
+
supportsApprovals: r2.supportsApprovals,
|
|
6659
|
+
...r2.effortTiers ? { effortTiers: [...r2.effortTiers] } : {}
|
|
6301
6660
|
}));
|
|
6302
6661
|
const workspaces = await Promise.all(
|
|
6303
6662
|
this.store.listWorkspaces().map((w2) => workspaceInfo(w2))
|
|
@@ -6392,6 +6751,41 @@ var SessionManager = class {
|
|
|
6392
6751
|
case "resume":
|
|
6393
6752
|
for (const m3 of this.store.replayAfter(msg.afterSeq)) reply(m3);
|
|
6394
6753
|
return;
|
|
6754
|
+
case "handover-response": {
|
|
6755
|
+
const pending = this.pendingHandovers.get(msg.handoverId);
|
|
6756
|
+
if (!pending || pending.sessionId !== msg.sessionId) return;
|
|
6757
|
+
this.pendingHandovers.delete(msg.handoverId);
|
|
6758
|
+
if (!msg.accept) {
|
|
6759
|
+
this.record(msg.sessionId, (seq) => ({
|
|
6760
|
+
type: "run-event",
|
|
6761
|
+
sessionId: msg.sessionId,
|
|
6762
|
+
runId: "",
|
|
6763
|
+
seq,
|
|
6764
|
+
event: { type: "handover-declined", id: msg.handoverId }
|
|
6765
|
+
}));
|
|
6766
|
+
return;
|
|
6767
|
+
}
|
|
6768
|
+
const session = this.store.getSession(msg.sessionId);
|
|
6769
|
+
const toRunner = this.runners.get(pending.toRunnerId);
|
|
6770
|
+
if (!session || !toRunner) return;
|
|
6771
|
+
const fromRunnerId = session.runnerId;
|
|
6772
|
+
const fromRunner = this.runners.get(fromRunnerId);
|
|
6773
|
+
const prompt = translatePacketToPrompt(pending.packet, fromRunner?.name ?? fromRunnerId);
|
|
6774
|
+
this.store.updateSession(session.id, { runnerId: pending.toRunnerId, conversationId: null });
|
|
6775
|
+
this.record(session.id, (seq) => ({
|
|
6776
|
+
type: "run-event",
|
|
6777
|
+
sessionId: session.id,
|
|
6778
|
+
runId: "",
|
|
6779
|
+
seq,
|
|
6780
|
+
event: { type: "handover-accepted", id: msg.handoverId, fromRunnerId, toRunnerId: pending.toRunnerId }
|
|
6781
|
+
}));
|
|
6782
|
+
const q2 = this.queues.get(session.id) ?? [];
|
|
6783
|
+
q2.push(prompt);
|
|
6784
|
+
this.queues.set(session.id, q2);
|
|
6785
|
+
this.pump(session.id);
|
|
6786
|
+
await this.broadcastManifest();
|
|
6787
|
+
return;
|
|
6788
|
+
}
|
|
6395
6789
|
case "session-create": {
|
|
6396
6790
|
const row = this.store.createSession(msg.workspace, msg.runnerId, msg.model, msg.label);
|
|
6397
6791
|
this.store.upsertWorkspace(msg.workspace);
|
|
@@ -6415,7 +6809,9 @@ var SessionManager = class {
|
|
|
6415
6809
|
...msg.model !== void 0 ? { model: msg.model || null } : {},
|
|
6416
6810
|
...msg.archived !== void 0 ? { archived: msg.archived } : {},
|
|
6417
6811
|
...msg.permissionMode !== void 0 ? { permissionMode: msg.permissionMode } : {},
|
|
6418
|
-
...msg.effort !== void 0 ? { effort: msg.effort } : {}
|
|
6812
|
+
...msg.effort !== void 0 ? { effort: msg.effort } : {},
|
|
6813
|
+
...msg.budgetCostUsdCap !== void 0 ? { budgetCostUsdCap: msg.budgetCostUsdCap } : {},
|
|
6814
|
+
...msg.budgetMinutesCap !== void 0 ? { budgetMinutesCap: msg.budgetMinutesCap } : {}
|
|
6419
6815
|
});
|
|
6420
6816
|
await this.broadcastManifest();
|
|
6421
6817
|
return;
|
|
@@ -6448,8 +6844,56 @@ var SessionManager = class {
|
|
|
6448
6844
|
case "fs-list":
|
|
6449
6845
|
reply({ type: "fs-response", rpcId: msg.rpcId, ...listFolders(msg.path) });
|
|
6450
6846
|
return;
|
|
6847
|
+
case "fs-create-folder": {
|
|
6848
|
+
try {
|
|
6849
|
+
const safeName = safeDropName(msg.name);
|
|
6850
|
+
mkdirSync2(join6(resolve3(msg.path), safeName), { recursive: false });
|
|
6851
|
+
} catch (e) {
|
|
6852
|
+
reply({
|
|
6853
|
+
type: "fs-response",
|
|
6854
|
+
rpcId: msg.rpcId,
|
|
6855
|
+
...listFolders(msg.path),
|
|
6856
|
+
error: `couldn't create folder: ${e instanceof Error ? e.message : String(e)}`
|
|
6857
|
+
});
|
|
6858
|
+
return;
|
|
6859
|
+
}
|
|
6860
|
+
reply({ type: "fs-response", rpcId: msg.rpcId, ...listFolders(msg.path) });
|
|
6861
|
+
return;
|
|
6862
|
+
}
|
|
6863
|
+
case "wol-recheck":
|
|
6864
|
+
await this.recheckWolCapability();
|
|
6865
|
+
reply(this.hello());
|
|
6866
|
+
return;
|
|
6451
6867
|
}
|
|
6452
6868
|
}
|
|
6869
|
+
/** The daemon's own relay connection dropped — logged durably so a phone
|
|
6870
|
+
* that wasn't listening live can still see it in the "left off" digest
|
|
6871
|
+
* (Execution Plan 6), unlike the relay's ephemeral `presence` frame. */
|
|
6872
|
+
recordDeviceOffline() {
|
|
6873
|
+
this.record(SYSTEM_LOG_SESSION_ID, (seq) => ({ type: "device-offline", seq, atMs: Date.now() }));
|
|
6874
|
+
}
|
|
6875
|
+
/** A running agent proactively pushed a status note (Execution Plan 5's
|
|
6876
|
+
* offhand_status_update MCP tool). Recorded into the normal session log
|
|
6877
|
+
* (so it shows in the transcript and the run timeline) and cached as the
|
|
6878
|
+
* latest note for this session, ready to enrich a Handover Packet later. */
|
|
6879
|
+
recordStatusUpdate(sessionId, text) {
|
|
6880
|
+
this.latestStatusUpdate.set(sessionId, text);
|
|
6881
|
+
this.record(sessionId, (seq) => ({
|
|
6882
|
+
type: "run-event",
|
|
6883
|
+
sessionId,
|
|
6884
|
+
runId: "",
|
|
6885
|
+
seq,
|
|
6886
|
+
event: { type: "status-update", text }
|
|
6887
|
+
}));
|
|
6888
|
+
}
|
|
6889
|
+
recordDeviceReconnected(offlineForMs) {
|
|
6890
|
+
this.record(SYSTEM_LOG_SESSION_ID, (seq) => ({
|
|
6891
|
+
type: "device-reconnected",
|
|
6892
|
+
seq,
|
|
6893
|
+
atMs: Date.now(),
|
|
6894
|
+
offlineForMs
|
|
6895
|
+
}));
|
|
6896
|
+
}
|
|
6453
6897
|
async sendDropToPhone(path) {
|
|
6454
6898
|
if (!this.uploader) throw new Error("drop upload unavailable: start daemon with --relay");
|
|
6455
6899
|
const file = readOutgoingDrop(path);
|
|
@@ -6492,10 +6936,17 @@ var SessionManager = class {
|
|
|
6492
6936
|
const touchedFiles = [];
|
|
6493
6937
|
let toolCount = 0;
|
|
6494
6938
|
let succeeded = false;
|
|
6939
|
+
let limitHitEmitted = false;
|
|
6940
|
+
let budgetWarned = false;
|
|
6941
|
+
const hasStatusTool = runner.id === "claude-code" && session.permissionMode !== "bypass";
|
|
6942
|
+
const sentPrompt = session.conversationId ? prompt : `${MOBILE_SESSION_PREAMBLE_BASE}${hasStatusTool ? MOBILE_SESSION_PREAMBLE_STATUS_TOOL : ""}
|
|
6943
|
+
|
|
6944
|
+
${prompt}`;
|
|
6495
6945
|
const handle = runner.start(
|
|
6496
6946
|
{
|
|
6497
6947
|
runId,
|
|
6498
|
-
|
|
6948
|
+
sessionId: session.id,
|
|
6949
|
+
prompt: sentPrompt,
|
|
6499
6950
|
workspace: session.workspace,
|
|
6500
6951
|
...session.model ? { model: session.model } : {},
|
|
6501
6952
|
...session.conversationId ? { resumeConversationId: session.conversationId } : {},
|
|
@@ -6528,6 +6979,17 @@ var SessionManager = class {
|
|
|
6528
6979
|
seq,
|
|
6529
6980
|
event
|
|
6530
6981
|
}));
|
|
6982
|
+
const limitHit = checkLimitHit(event, limitHitEmitted);
|
|
6983
|
+
if (limitHit) {
|
|
6984
|
+
limitHitEmitted = true;
|
|
6985
|
+
this.record(session.id, (seq) => ({ type: "run-event", sessionId: session.id, runId, seq, event: limitHit }));
|
|
6986
|
+
void this.proposeHandover(session, runId, limitHit.reason);
|
|
6987
|
+
}
|
|
6988
|
+
const budgetHit = checkBudgetWarning(event, session, budgetWarned) ?? checkTimeBudget(session, Date.now() - startedAt, budgetWarned);
|
|
6989
|
+
if (budgetHit) {
|
|
6990
|
+
budgetWarned = true;
|
|
6991
|
+
this.record(session.id, (seq) => ({ type: "run-event", sessionId: session.id, runId, seq, event: budgetHit }));
|
|
6992
|
+
}
|
|
6531
6993
|
}
|
|
6532
6994
|
} finally {
|
|
6533
6995
|
this.active.delete(session.id);
|
|
@@ -6559,7 +7021,65 @@ var SessionManager = class {
|
|
|
6559
7021
|
}));
|
|
6560
7022
|
}
|
|
6561
7023
|
}
|
|
7024
|
+
/** Builds and offers a Handover Packet once a limit-hit fires. Entirely
|
|
7025
|
+
* deterministic (live git diff + this session's own log) — no cooperation
|
|
7026
|
+
* needed from the agent that just hit its limit, which is the point:
|
|
7027
|
+
* this must still work when that agent is already dead or blocked, not
|
|
7028
|
+
* only the graceful case. Silent (no card at all) when there's no other
|
|
7029
|
+
* detected, available runner to hand off to — never a dead-end proposal. */
|
|
7030
|
+
async proposeHandover(session, runId, reason) {
|
|
7031
|
+
const target = this.pickHandoverTarget(session.runnerId);
|
|
7032
|
+
if (!target) return;
|
|
7033
|
+
const diff = await liveDiff(session.workspace);
|
|
7034
|
+
const { items } = this.store.historyPage(session.id, 0, 500);
|
|
7035
|
+
const packet = assembleHandoverPacket(items, diff);
|
|
7036
|
+
const agentSummary = this.latestStatusUpdate.get(session.id);
|
|
7037
|
+
if (agentSummary) packet.agentSummary = agentSummary;
|
|
7038
|
+
const id = randomUUID();
|
|
7039
|
+
this.pendingHandovers.set(id, { sessionId: session.id, toRunnerId: target.id, packet });
|
|
7040
|
+
this.record(session.id, (seq) => ({
|
|
7041
|
+
type: "run-event",
|
|
7042
|
+
sessionId: session.id,
|
|
7043
|
+
runId,
|
|
7044
|
+
seq,
|
|
7045
|
+
event: { type: "handover-proposed", id, fromRunnerId: session.runnerId, toRunnerId: target.id, reason, packet }
|
|
7046
|
+
}));
|
|
7047
|
+
}
|
|
7048
|
+
/** First other detected+available runner — good enough for v1 (single
|
|
7049
|
+
* candidate, not a picker); a real choice among several becomes relevant
|
|
7050
|
+
* once more than one alternative is commonly available at once. */
|
|
7051
|
+
pickHandoverTarget(currentRunnerId) {
|
|
7052
|
+
for (const [id, runner] of this.runners) {
|
|
7053
|
+
if (id === currentRunnerId) continue;
|
|
7054
|
+
if (!(this.runnerAvailability.get(id) ?? false)) continue;
|
|
7055
|
+
return runner;
|
|
7056
|
+
}
|
|
7057
|
+
return null;
|
|
7058
|
+
}
|
|
6562
7059
|
};
|
|
7060
|
+
var LIMIT_HIT_THRESHOLD = 0.9;
|
|
7061
|
+
function checkLimitHit(event, alreadyEmitted) {
|
|
7062
|
+
if (alreadyEmitted || event.type !== "rate-limit") return null;
|
|
7063
|
+
const utilization = Math.max(event.fiveHourUtilization, event.sevenDayUtilization);
|
|
7064
|
+
if (utilization < LIMIT_HIT_THRESHOLD) return null;
|
|
7065
|
+
return { type: "limit-hit", reason: "rate_limit", utilization };
|
|
7066
|
+
}
|
|
7067
|
+
function checkBudgetWarning(event, session, alreadyWarned) {
|
|
7068
|
+
if (alreadyWarned || event.type !== "usage" || !session.budgetCostUsdCap) return null;
|
|
7069
|
+
if (event.costUsd === void 0 || event.costUsd < session.budgetCostUsdCap) return null;
|
|
7070
|
+
return { type: "budget-warning", kind: "cost", capUsd: session.budgetCostUsdCap, actualUsd: event.costUsd };
|
|
7071
|
+
}
|
|
7072
|
+
function checkTimeBudget(session, elapsedMs, alreadyWarned) {
|
|
7073
|
+
if (alreadyWarned || !session.budgetMinutesCap) return null;
|
|
7074
|
+
const minutes = elapsedMs / 6e4;
|
|
7075
|
+
if (minutes < session.budgetMinutesCap) return null;
|
|
7076
|
+
return {
|
|
7077
|
+
type: "budget-warning",
|
|
7078
|
+
kind: "time",
|
|
7079
|
+
capMinutes: session.budgetMinutesCap,
|
|
7080
|
+
actualMinutes: Math.round(minutes)
|
|
7081
|
+
};
|
|
7082
|
+
}
|
|
6563
7083
|
function trackTouches(event, touched) {
|
|
6564
7084
|
if (event.type !== "tool") return;
|
|
6565
7085
|
if (!/^(Edit|Write|MultiEdit|NotebookEdit)/.test(event.name)) return;
|
|
@@ -6576,16 +7096,16 @@ function listFolders(path) {
|
|
|
6576
7096
|
const current = resolve3(path);
|
|
6577
7097
|
const dirs = safeReadDir2(current).filter((entry) => entry.isDirectory() && !skipDir(entry.name)).map((entry) => {
|
|
6578
7098
|
const full = join6(current, entry.name);
|
|
6579
|
-
return { name: entry.name, path: full, isGit:
|
|
7099
|
+
return { name: entry.name, path: full, isGit: existsSync7(join6(full, ".git")) };
|
|
6580
7100
|
}).sort((a2, b2) => Number(b2.isGit) - Number(a2.isGit) || a2.name.localeCompare(b2.name)).slice(0, 200);
|
|
6581
7101
|
return { path: current, parent: isRoot(current) ? null : dirname2(current), dirs };
|
|
6582
7102
|
}
|
|
6583
7103
|
function driveRoots() {
|
|
6584
|
-
if (process.platform !== "win32") return [{ name: "/", path: "/", isGit:
|
|
7104
|
+
if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync7("/.git") }];
|
|
6585
7105
|
const roots = [];
|
|
6586
7106
|
for (let code = 67; code <= 90; code++) {
|
|
6587
7107
|
const name = `${String.fromCharCode(code)}:\\`;
|
|
6588
|
-
if (
|
|
7108
|
+
if (existsSync7(name)) roots.push({ name, path: name, isGit: existsSync7(join6(name, ".git")) });
|
|
6589
7109
|
}
|
|
6590
7110
|
return roots.sort((a2, b2) => a2.name.localeCompare(b2.name)).slice(0, 200);
|
|
6591
7111
|
}
|
|
@@ -6650,6 +7170,15 @@ var Store = class {
|
|
|
6650
7170
|
dev_url TEXT
|
|
6651
7171
|
);
|
|
6652
7172
|
CREATE VIRTUAL TABLE IF NOT EXISTS log_fts USING fts5(text, session_id UNINDEXED, seq UNINDEXED);
|
|
7173
|
+
CREATE TABLE IF NOT EXISTS device (
|
|
7174
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
7175
|
+
wol_capability TEXT,
|
|
7176
|
+
wol_reason TEXT,
|
|
7177
|
+
wol_network_type TEXT,
|
|
7178
|
+
wol_mac TEXT,
|
|
7179
|
+
wol_broadcast TEXT,
|
|
7180
|
+
wol_checked_at_ms INTEGER
|
|
7181
|
+
);
|
|
6653
7182
|
`);
|
|
6654
7183
|
try {
|
|
6655
7184
|
this.db.exec(`ALTER TABLE workspaces ADD COLUMN policy TEXT NOT NULL DEFAULT 'balanced'`);
|
|
@@ -6663,6 +7192,14 @@ var Store = class {
|
|
|
6663
7192
|
this.db.exec(`ALTER TABLE sessions ADD COLUMN effort TEXT`);
|
|
6664
7193
|
} catch {
|
|
6665
7194
|
}
|
|
7195
|
+
try {
|
|
7196
|
+
this.db.exec(`ALTER TABLE sessions ADD COLUMN budget_cost_usd_cap REAL`);
|
|
7197
|
+
} catch {
|
|
7198
|
+
}
|
|
7199
|
+
try {
|
|
7200
|
+
this.db.exec(`ALTER TABLE sessions ADD COLUMN budget_minutes_cap INTEGER`);
|
|
7201
|
+
} catch {
|
|
7202
|
+
}
|
|
6666
7203
|
}
|
|
6667
7204
|
// ---- sessions -------------------------------------------------------------
|
|
6668
7205
|
createSession(workspace, runnerId, model, label) {
|
|
@@ -6676,7 +7213,9 @@ var Store = class {
|
|
|
6676
7213
|
archived: false,
|
|
6677
7214
|
conversationId: null,
|
|
6678
7215
|
permissionMode: "guarded",
|
|
6679
|
-
effort: null
|
|
7216
|
+
effort: null,
|
|
7217
|
+
budgetCostUsdCap: null,
|
|
7218
|
+
budgetMinutesCap: null
|
|
6680
7219
|
};
|
|
6681
7220
|
this.db.prepare(
|
|
6682
7221
|
`INSERT INTO sessions (id, workspace, runner_id, model, label, created_at_ms, archived, conversation_id)
|
|
@@ -6696,7 +7235,9 @@ var Store = class {
|
|
|
6696
7235
|
archived: Boolean(r2.archived),
|
|
6697
7236
|
conversationId: r2.conversation_id ?? null,
|
|
6698
7237
|
permissionMode: r2.permission_mode ?? "guarded",
|
|
6699
|
-
effort: r2.effort ?? null
|
|
7238
|
+
effort: r2.effort ?? null,
|
|
7239
|
+
budgetCostUsdCap: r2.budget_cost_usd_cap ?? null,
|
|
7240
|
+
budgetMinutesCap: r2.budget_minutes_cap ?? null
|
|
6700
7241
|
}));
|
|
6701
7242
|
}
|
|
6702
7243
|
getSession(id) {
|
|
@@ -6707,8 +7248,20 @@ var Store = class {
|
|
|
6707
7248
|
if (!current) return;
|
|
6708
7249
|
const next = { ...current, ...patch };
|
|
6709
7250
|
this.db.prepare(
|
|
6710
|
-
`UPDATE sessions SET label = ?, model = ?, archived = ?, conversation_id = ?, permission_mode = ?, effort =
|
|
6711
|
-
|
|
7251
|
+
`UPDATE sessions SET label = ?, model = ?, archived = ?, conversation_id = ?, permission_mode = ?, effort = ?,
|
|
7252
|
+
budget_cost_usd_cap = ?, budget_minutes_cap = ?, runner_id = ? WHERE id = ?`
|
|
7253
|
+
).run(
|
|
7254
|
+
next.label,
|
|
7255
|
+
next.model,
|
|
7256
|
+
next.archived ? 1 : 0,
|
|
7257
|
+
next.conversationId,
|
|
7258
|
+
next.permissionMode,
|
|
7259
|
+
next.effort,
|
|
7260
|
+
next.budgetCostUsdCap,
|
|
7261
|
+
next.budgetMinutesCap,
|
|
7262
|
+
next.runnerId,
|
|
7263
|
+
id
|
|
7264
|
+
);
|
|
6712
7265
|
}
|
|
6713
7266
|
toSessionInfo(row, busy, queued) {
|
|
6714
7267
|
return {
|
|
@@ -6722,7 +7275,9 @@ var Store = class {
|
|
|
6722
7275
|
permissionMode: row.permissionMode,
|
|
6723
7276
|
effort: row.effort ?? void 0,
|
|
6724
7277
|
busy,
|
|
6725
|
-
queuedPrompts: queued
|
|
7278
|
+
queuedPrompts: queued,
|
|
7279
|
+
budgetCostUsdCap: row.budgetCostUsdCap ?? void 0,
|
|
7280
|
+
budgetMinutesCap: row.budgetMinutesCap ?? void 0
|
|
6726
7281
|
};
|
|
6727
7282
|
}
|
|
6728
7283
|
// ---- transcript log ---------------------------------------------------------
|
|
@@ -6793,6 +7348,32 @@ var Store = class {
|
|
|
6793
7348
|
setWorkspacePolicy(path, policy) {
|
|
6794
7349
|
this.db.prepare(`UPDATE workspaces SET policy = ? WHERE path = ?`).run(policy, path);
|
|
6795
7350
|
}
|
|
7351
|
+
// ---- device capability (Execution Plan 7) ----------------------------------
|
|
7352
|
+
getWolCapability() {
|
|
7353
|
+
const row = this.db.prepare(`SELECT * FROM device WHERE id = 1`).get();
|
|
7354
|
+
if (!row || row.wol_capability == null) return null;
|
|
7355
|
+
return {
|
|
7356
|
+
capability: row.wol_capability,
|
|
7357
|
+
reason: row.wol_reason,
|
|
7358
|
+
networkType: row.wol_network_type,
|
|
7359
|
+
mac: row.wol_mac ?? null,
|
|
7360
|
+
broadcast: row.wol_broadcast ?? null,
|
|
7361
|
+
checkedAtMs: row.wol_checked_at_ms
|
|
7362
|
+
};
|
|
7363
|
+
}
|
|
7364
|
+
setWolCapability(v2) {
|
|
7365
|
+
this.db.prepare(
|
|
7366
|
+
`INSERT INTO device (id, wol_capability, wol_reason, wol_network_type, wol_mac, wol_broadcast, wol_checked_at_ms)
|
|
7367
|
+
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
7368
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
7369
|
+
wol_capability = excluded.wol_capability,
|
|
7370
|
+
wol_reason = excluded.wol_reason,
|
|
7371
|
+
wol_network_type = excluded.wol_network_type,
|
|
7372
|
+
wol_mac = excluded.wol_mac,
|
|
7373
|
+
wol_broadcast = excluded.wol_broadcast,
|
|
7374
|
+
wol_checked_at_ms = excluded.wol_checked_at_ms`
|
|
7375
|
+
).run(v2.capability, v2.reason, v2.networkType, v2.mac, v2.broadcast, v2.checkedAtMs);
|
|
7376
|
+
}
|
|
6796
7377
|
};
|
|
6797
7378
|
function searchableText(msg) {
|
|
6798
7379
|
switch (msg.type) {
|
|
@@ -6803,6 +7384,7 @@ function searchableText(msg) {
|
|
|
6803
7384
|
if (msg.event.type === "tool") return msg.event.summary;
|
|
6804
7385
|
if (msg.event.type === "done") return msg.event.summary;
|
|
6805
7386
|
if (msg.event.type === "error") return msg.event.message;
|
|
7387
|
+
if (msg.event.type === "status-update") return msg.event.text;
|
|
6806
7388
|
return "";
|
|
6807
7389
|
case "drop":
|
|
6808
7390
|
return msg.name;
|
|
@@ -10872,11 +11454,42 @@ var ApprovalQuestionSchema = external_exports.object({
|
|
|
10872
11454
|
options: external_exports.array(external_exports.object({ label: external_exports.string(), description: external_exports.string().optional() })),
|
|
10873
11455
|
multiSelect: external_exports.boolean().optional()
|
|
10874
11456
|
});
|
|
11457
|
+
var TodoItemSchema = external_exports.object({
|
|
11458
|
+
content: external_exports.string(),
|
|
11459
|
+
status: external_exports.enum(["pending", "in_progress", "completed"]),
|
|
11460
|
+
activeForm: external_exports.string().optional()
|
|
11461
|
+
});
|
|
11462
|
+
var HandoverPacketSchema = external_exports.object({
|
|
11463
|
+
/** Live `git diff HEAD` against the workspace, captured at handover time. */
|
|
11464
|
+
diff: external_exports.string(),
|
|
11465
|
+
/** Tool-call summaries this session made, oldest first, capped to the most recent. */
|
|
11466
|
+
toolSummaries: external_exports.array(external_exports.string()),
|
|
11467
|
+
/** The last TodoWrite call's task list, verbatim — never summarized. */
|
|
11468
|
+
todos: external_exports.array(TodoItemSchema).optional(),
|
|
11469
|
+
/** Approvals raised but never resolved (still pending or timed out) at handover time. */
|
|
11470
|
+
pendingApprovals: external_exports.array(
|
|
11471
|
+
external_exports.object({ action: external_exports.string(), detail: external_exports.string(), risk: external_exports.enum(["low", "high"]) })
|
|
11472
|
+
),
|
|
11473
|
+
/** Recent prompt/response turns, oldest first, capped — for tone/context continuity. */
|
|
11474
|
+
recentMessages: external_exports.array(external_exports.object({ role: external_exports.enum(["user", "assistant"]), text: external_exports.string() })),
|
|
11475
|
+
/** Optional free-text self-summary from the outgoing agent — enrichment
|
|
11476
|
+
* only, attempted when there was runway before a hard cutoff. Its absence
|
|
11477
|
+
* changes nothing; the packet above is already sufficient on its own. */
|
|
11478
|
+
agentSummary: external_exports.string().optional()
|
|
11479
|
+
});
|
|
10875
11480
|
var RunEventSchema = external_exports.discriminatedUnion("type", [
|
|
10876
11481
|
external_exports.object({ type: external_exports.literal("text"), chunk: external_exports.string() }),
|
|
10877
11482
|
/** Extended-thinking content, streamed the same way as text (medium+ effort tiers). */
|
|
10878
11483
|
external_exports.object({ type: external_exports.literal("thinking"), chunk: external_exports.string() }),
|
|
10879
|
-
external_exports.object({
|
|
11484
|
+
external_exports.object({
|
|
11485
|
+
type: external_exports.literal("tool"),
|
|
11486
|
+
name: external_exports.string(),
|
|
11487
|
+
summary: external_exports.string(),
|
|
11488
|
+
/** Structured payload for calls that matter to continuity (TodoWrite's
|
|
11489
|
+
* actual task list). Optional: most tool calls have none, and omitting
|
|
11490
|
+
* it is always safe — `summary` alone still renders the transcript. */
|
|
11491
|
+
todos: external_exports.array(TodoItemSchema).optional()
|
|
11492
|
+
}),
|
|
10880
11493
|
external_exports.object({
|
|
10881
11494
|
type: external_exports.literal("approval"),
|
|
10882
11495
|
id: external_exports.string(),
|
|
@@ -10921,12 +11534,67 @@ var RunEventSchema = external_exports.discriminatedUnion("type", [
|
|
|
10921
11534
|
fiveHourResetsAtMs: external_exports.number(),
|
|
10922
11535
|
sevenDayUtilization: external_exports.number(),
|
|
10923
11536
|
sevenDayResetsAtMs: external_exports.number()
|
|
11537
|
+
}),
|
|
11538
|
+
/** Emitted once, when the daemon's own threshold check crosses before the
|
|
11539
|
+
* CLI itself refuses — the phone's cue to propose a handover (Execution
|
|
11540
|
+
* Plan 5) while there's still runway to do it gracefully. */
|
|
11541
|
+
external_exports.object({
|
|
11542
|
+
type: external_exports.literal("limit-hit"),
|
|
11543
|
+
reason: external_exports.enum(["rate_limit", "context_window", "cost_cap"]),
|
|
11544
|
+
/** 0-1 utilization that crossed the threshold, when known. */
|
|
11545
|
+
utilization: external_exports.number().optional()
|
|
11546
|
+
}),
|
|
11547
|
+
/** Session cost or wall-clock time crossed the cap set on it (Execution
|
|
11548
|
+
* Plan 1's budget feature) — informational, the run is not cancelled. */
|
|
11549
|
+
external_exports.object({
|
|
11550
|
+
type: external_exports.literal("budget-warning"),
|
|
11551
|
+
kind: external_exports.enum(["cost", "time"]),
|
|
11552
|
+
capUsd: external_exports.number().optional(),
|
|
11553
|
+
capMinutes: external_exports.number().optional(),
|
|
11554
|
+
actualUsd: external_exports.number().optional(),
|
|
11555
|
+
actualMinutes: external_exports.number().optional()
|
|
11556
|
+
}),
|
|
11557
|
+
/** The daemon is offering to move this session to another runner
|
|
11558
|
+
* (Execution Plan 4) — built from the log/diff alone, never from asking
|
|
11559
|
+
* this agent to cooperate (it may already be unable to). */
|
|
11560
|
+
external_exports.object({
|
|
11561
|
+
type: external_exports.literal("handover-proposed"),
|
|
11562
|
+
id: external_exports.string(),
|
|
11563
|
+
fromRunnerId: external_exports.string(),
|
|
11564
|
+
toRunnerId: external_exports.string(),
|
|
11565
|
+
reason: external_exports.enum(["rate_limit", "context_window", "cost_cap", "crash", "manual"]),
|
|
11566
|
+
packet: HandoverPacketSchema
|
|
11567
|
+
}),
|
|
11568
|
+
external_exports.object({
|
|
11569
|
+
type: external_exports.literal("handover-declined"),
|
|
11570
|
+
id: external_exports.string()
|
|
11571
|
+
}),
|
|
11572
|
+
external_exports.object({
|
|
11573
|
+
type: external_exports.literal("handover-accepted"),
|
|
11574
|
+
id: external_exports.string(),
|
|
11575
|
+
fromRunnerId: external_exports.string(),
|
|
11576
|
+
toRunnerId: external_exports.string()
|
|
11577
|
+
}),
|
|
11578
|
+
/** A proactive note the agent chose to push mid-run (Execution Plan 5's
|
|
11579
|
+
* offhand_status_update MCP tool) — outside the normal streamed
|
|
11580
|
+
* text/thinking, and never fabricated: only exists when the agent itself
|
|
11581
|
+
* called the tool. The most recent one on a session also becomes the
|
|
11582
|
+
* Handover Packet's optional `agentSummary` (Execution Plan 4) when a
|
|
11583
|
+
* handover is proposed afterward — still zero hosted AI, since it's the
|
|
11584
|
+
* same agent, on its own tokens, choosing to leave a note. */
|
|
11585
|
+
external_exports.object({
|
|
11586
|
+
type: external_exports.literal("status-update"),
|
|
11587
|
+
text: external_exports.string()
|
|
10924
11588
|
})
|
|
10925
11589
|
]);
|
|
10926
11590
|
var PermissionModeSchema = external_exports.enum(["guarded", "plan", "acceptEdits", "bypass"]);
|
|
10927
11591
|
var EffortSchema = external_exports.enum(["low", "medium", "high", "max"]);
|
|
10928
11592
|
var RunSpecSchema = external_exports.object({
|
|
10929
11593
|
runId: external_exports.string(),
|
|
11594
|
+
/** The offhand session this run belongs to — lets an MCP bridge tool
|
|
11595
|
+
* (Execution Plan 5) tag a proactive call (status update, dropped file)
|
|
11596
|
+
* with the right session without a global single-listener broker. */
|
|
11597
|
+
sessionId: external_exports.string(),
|
|
10930
11598
|
prompt: external_exports.string().min(1),
|
|
10931
11599
|
/** Absolute path of the workspace the agent runs in. */
|
|
10932
11600
|
workspace: external_exports.string(),
|
|
@@ -10951,7 +11619,11 @@ var RunnerInfoSchema = external_exports.object({
|
|
|
10951
11619
|
version: external_exports.string().optional(),
|
|
10952
11620
|
/** Model ids the phone may offer; empty = CLI default only. */
|
|
10953
11621
|
models: external_exports.array(external_exports.string()),
|
|
10954
|
-
supportsApprovals: external_exports.boolean()
|
|
11622
|
+
supportsApprovals: external_exports.boolean(),
|
|
11623
|
+
/** Thinking-budget tiers this runner actually honors (Execution Plan 3) —
|
|
11624
|
+
* undefined means the phone should hide the effort control entirely for
|
|
11625
|
+
* this runner, not show one that silently does nothing. */
|
|
11626
|
+
effortTiers: external_exports.array(external_exports.enum(["low", "medium", "high", "max"])).optional()
|
|
10955
11627
|
});
|
|
10956
11628
|
var ApprovalPolicySchema = external_exports.enum(["paranoid", "balanced", "trusting"]);
|
|
10957
11629
|
var WorkspaceInfoSchema = external_exports.object({
|
|
@@ -10978,12 +11650,23 @@ var SessionInfoSchema = external_exports.object({
|
|
|
10978
11650
|
effort: external_exports.enum(["low", "medium", "high", "max"]).optional(),
|
|
10979
11651
|
/** Whether a run is active or prompts are queued right now. */
|
|
10980
11652
|
busy: external_exports.boolean(),
|
|
10981
|
-
queuedPrompts: external_exports.number().int()
|
|
11653
|
+
queuedPrompts: external_exports.number().int(),
|
|
11654
|
+
/** Per-session budget caps (Execution Plan 1) — undefined means no cap set. */
|
|
11655
|
+
budgetCostUsdCap: external_exports.number().optional(),
|
|
11656
|
+
budgetMinutesCap: external_exports.number().int().optional()
|
|
10982
11657
|
});
|
|
10983
11658
|
var HostInfoSchema = external_exports.object({
|
|
10984
11659
|
hostname: external_exports.string(),
|
|
10985
11660
|
os: external_exports.string(),
|
|
10986
|
-
daemonVersion: external_exports.string()
|
|
11661
|
+
daemonVersion: external_exports.string(),
|
|
11662
|
+
/** Wake-on-LAN/WiFi capability, detected per device (Execution Plan 7) —
|
|
11663
|
+
* never assumed from the OS alone. Absent only if no probe has completed
|
|
11664
|
+
* yet (e.g. mid-startup). */
|
|
11665
|
+
wolCapability: external_exports.enum(["supported", "wifi-limited", "unsupported"]).optional(),
|
|
11666
|
+
/** Human-readable reason behind wolCapability — always present alongside
|
|
11667
|
+
* it, since a bare enum can't explain itself. */
|
|
11668
|
+
wolReason: external_exports.string().optional(),
|
|
11669
|
+
wolCheckedAtMs: external_exports.number().int().optional()
|
|
10987
11670
|
});
|
|
10988
11671
|
|
|
10989
11672
|
// ../shared/src/protocol-v2.ts
|
|
@@ -11017,6 +11700,12 @@ var ClientMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
11017
11700
|
answer: external_exports.string().optional()
|
|
11018
11701
|
}),
|
|
11019
11702
|
external_exports.object({ type: external_exports.literal("resume"), afterSeq: external_exports.number().int().nonnegative() }),
|
|
11703
|
+
external_exports.object({
|
|
11704
|
+
type: external_exports.literal("handover-response"),
|
|
11705
|
+
sessionId: external_exports.string(),
|
|
11706
|
+
handoverId: external_exports.string(),
|
|
11707
|
+
accept: external_exports.boolean()
|
|
11708
|
+
}),
|
|
11020
11709
|
external_exports.object({
|
|
11021
11710
|
type: external_exports.literal("drop-send"),
|
|
11022
11711
|
blobId: external_exports.string(),
|
|
@@ -11047,7 +11736,10 @@ var ClientMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
11047
11736
|
model: external_exports.string().optional(),
|
|
11048
11737
|
archived: external_exports.boolean().optional(),
|
|
11049
11738
|
permissionMode: external_exports.enum(["guarded", "plan", "acceptEdits", "bypass"]).optional(),
|
|
11050
|
-
effort: external_exports.enum(["low", "medium", "high", "max"]).optional()
|
|
11739
|
+
effort: external_exports.enum(["low", "medium", "high", "max"]).optional(),
|
|
11740
|
+
/** Per-session budget caps (Execution Plan 1). null clears an existing cap. */
|
|
11741
|
+
budgetCostUsdCap: external_exports.number().positive().nullable().optional(),
|
|
11742
|
+
budgetMinutesCap: external_exports.number().int().positive().nullable().optional()
|
|
11051
11743
|
}),
|
|
11052
11744
|
/** Start a fresh agent conversation inside the session. */
|
|
11053
11745
|
external_exports.object({ type: external_exports.literal("session-reset"), sessionId: external_exports.string() }),
|
|
@@ -11081,7 +11773,19 @@ var ClientMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
11081
11773
|
type: external_exports.literal("fs-list"),
|
|
11082
11774
|
rpcId: external_exports.string(),
|
|
11083
11775
|
path: external_exports.string().optional()
|
|
11084
|
-
})
|
|
11776
|
+
}),
|
|
11777
|
+
/** Mobile-first session bootstrap (Execution Plan 5): create a new project
|
|
11778
|
+
* folder from the phone's workspace browser, no laptop needed first. */
|
|
11779
|
+
external_exports.object({
|
|
11780
|
+
type: external_exports.literal("fs-create-folder"),
|
|
11781
|
+
rpcId: external_exports.string(),
|
|
11782
|
+
path: external_exports.string(),
|
|
11783
|
+
name: external_exports.string().min(1)
|
|
11784
|
+
}),
|
|
11785
|
+
/** Re-run the Wake-on-LAN/WiFi capability probe (Execution Plan 7) — an
|
|
11786
|
+
* explicit "check again" action, since drivers/BIOS settings/network type
|
|
11787
|
+
* can change after first setup. */
|
|
11788
|
+
external_exports.object({ type: external_exports.literal("wol-recheck") })
|
|
11085
11789
|
]);
|
|
11086
11790
|
var ReceiptSchema = external_exports.object({
|
|
11087
11791
|
runId: external_exports.string(),
|
|
@@ -11139,6 +11843,17 @@ var ServerMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
11139
11843
|
size: external_exports.number().int().nonnegative(),
|
|
11140
11844
|
direction: external_exports.enum(["to-phone", "to-pc"])
|
|
11141
11845
|
}),
|
|
11846
|
+
/** The daemon's own relay connection dropped/came back — logged durably
|
|
11847
|
+
* (unlike the relay's live `presence` frame) so a phone that wasn't
|
|
11848
|
+
* listening at the time can still reconstruct "laptop asleep since 11pm"
|
|
11849
|
+
* from history (Execution Plan 6's digest). Not tied to any run. */
|
|
11850
|
+
external_exports.object({ type: external_exports.literal("device-offline"), seq: external_exports.number().int(), atMs: external_exports.number().int() }),
|
|
11851
|
+
external_exports.object({
|
|
11852
|
+
type: external_exports.literal("device-reconnected"),
|
|
11853
|
+
seq: external_exports.number().int(),
|
|
11854
|
+
atMs: external_exports.number().int(),
|
|
11855
|
+
offlineForMs: external_exports.number().int().nonnegative()
|
|
11856
|
+
}),
|
|
11142
11857
|
// RPC responses (not seq-logged)
|
|
11143
11858
|
external_exports.object({
|
|
11144
11859
|
type: external_exports.literal("history-response"),
|
|
@@ -11164,7 +11879,12 @@ var ServerMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
11164
11879
|
rpcId: external_exports.string(),
|
|
11165
11880
|
path: external_exports.string(),
|
|
11166
11881
|
parent: external_exports.string().nullable(),
|
|
11167
|
-
dirs: external_exports.array(FsDirSchema)
|
|
11882
|
+
dirs: external_exports.array(FsDirSchema),
|
|
11883
|
+
/** Set only by fs-create-folder on failure — the generic top-level
|
|
11884
|
+
* `error` message carries no rpcId, so it can never resolve a specific
|
|
11885
|
+
* pending RPC (see client.ts's rpc()); this keeps the failure correctly
|
|
11886
|
+
* routed back to whoever asked, with the listing unchanged either way. */
|
|
11887
|
+
error: external_exports.string().optional()
|
|
11168
11888
|
}),
|
|
11169
11889
|
external_exports.object({ type: external_exports.literal("error"), message: external_exports.string() })
|
|
11170
11890
|
]);
|
|
@@ -11188,7 +11908,8 @@ var RelayFrameSchema = external_exports.discriminatedUnion("kind", [
|
|
|
11188
11908
|
*/
|
|
11189
11909
|
external_exports.object({
|
|
11190
11910
|
kind: external_exports.literal("notify"),
|
|
11191
|
-
notice: external_exports.enum(["approval", "drop", "done", "error", "progress"]),
|
|
11911
|
+
notice: external_exports.enum(["approval", "drop", "done", "error", "progress", "handover", "limit-hit"]),
|
|
11912
|
+
/** 'approval'/'handover' only — the approval/handover id being acted on. */
|
|
11192
11913
|
id: external_exports.string().optional(),
|
|
11193
11914
|
sessionId: external_exports.string().optional(),
|
|
11194
11915
|
/** 'progress' only — a bare action count, never the action itself (no content in push). */
|
|
@@ -11196,6 +11917,16 @@ var RelayFrameSchema = external_exports.discriminatedUnion("kind", [
|
|
|
11196
11917
|
}),
|
|
11197
11918
|
/** relay → daemon: verdict delivered via push-notification action button. */
|
|
11198
11919
|
external_exports.object({ kind: external_exports.literal("verdict"), approvalId: external_exports.string(), approve: external_exports.boolean() }),
|
|
11920
|
+
/** relay → daemon: handover accept/decline from a push-notification action
|
|
11921
|
+
* button (Execution Plan 6) — same "opaque ids only" shape as `verdict`,
|
|
11922
|
+
* with `chatSessionId` since handover-response needs it to look up the
|
|
11923
|
+
* pending proposal. */
|
|
11924
|
+
external_exports.object({
|
|
11925
|
+
kind: external_exports.literal("handover-verdict"),
|
|
11926
|
+
handoverId: external_exports.string(),
|
|
11927
|
+
chatSessionId: external_exports.string(),
|
|
11928
|
+
accept: external_exports.boolean()
|
|
11929
|
+
}),
|
|
11199
11930
|
external_exports.object({ kind: external_exports.literal("ping") }),
|
|
11200
11931
|
external_exports.object({ kind: external_exports.literal("pong") })
|
|
11201
11932
|
]);
|
|
@@ -41803,6 +42534,22 @@ var LocalSessionServer = class {
|
|
|
41803
42534
|
}
|
|
41804
42535
|
return;
|
|
41805
42536
|
}
|
|
42537
|
+
if (req.method === "POST" && req.url === "/status") {
|
|
42538
|
+
try {
|
|
42539
|
+
const chunks = [];
|
|
42540
|
+
for await (const c2 of req) chunks.push(c2);
|
|
42541
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
42542
|
+
if (typeof body.sessionId === "string" && typeof body.text === "string" && body.text.trim()) {
|
|
42543
|
+
this.manager.recordStatusUpdate(body.sessionId, body.text.trim());
|
|
42544
|
+
}
|
|
42545
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
42546
|
+
res.end(JSON.stringify({ ok: true }));
|
|
42547
|
+
} catch (e) {
|
|
42548
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
42549
|
+
res.end(JSON.stringify({ ok: false, message: `bad status request: ${String(e)}` }));
|
|
42550
|
+
}
|
|
42551
|
+
return;
|
|
42552
|
+
}
|
|
41806
42553
|
res.writeHead(404);
|
|
41807
42554
|
res.end();
|
|
41808
42555
|
}
|
|
@@ -41838,8 +42585,19 @@ var RelayClient = class {
|
|
|
41838
42585
|
heartbeat = null;
|
|
41839
42586
|
detach = null;
|
|
41840
42587
|
stopped = false;
|
|
42588
|
+
/** Set when a heartbeat ping goes out; cleared by ANY inbound frame. If
|
|
42589
|
+
* still set on the next tick, nothing has answered in a full interval —
|
|
42590
|
+
* the connection is a zombie (readyState still reports OPEN even though
|
|
42591
|
+
* the underlying TCP connection died silently, e.g. after laptop sleep or
|
|
42592
|
+
* a NAT/firewall idle timeout — no close/error event fires on its own). */
|
|
42593
|
+
awaitingPongSince = null;
|
|
41841
42594
|
/** sessionId → tool-call count for the in-flight run (opaque count only, never the action). */
|
|
41842
42595
|
toolCounts = /* @__PURE__ */ new Map();
|
|
42596
|
+
/** True once we've been online at least once — a failed first connection
|
|
42597
|
+
* attempt isn't "going offline" (there was nothing to lose), so the very
|
|
42598
|
+
* first successful open never logs a reconnect. */
|
|
42599
|
+
everConnected = false;
|
|
42600
|
+
offlineSinceMs = null;
|
|
41843
42601
|
start() {
|
|
41844
42602
|
this.connect();
|
|
41845
42603
|
}
|
|
@@ -41854,8 +42612,13 @@ var RelayClient = class {
|
|
|
41854
42612
|
}
|
|
41855
42613
|
connect() {
|
|
41856
42614
|
if (this.stopped) return;
|
|
42615
|
+
console.log(`relay: connecting (${this.relayUrl})`);
|
|
41857
42616
|
const ws = new wrapper_default(this.url());
|
|
41858
42617
|
this.ws = ws;
|
|
42618
|
+
const slowStartTimer = setTimeout(() => {
|
|
42619
|
+
console.log("relay: still connecting \u2014 the hosted relay may be waking up from a cold start (can take up to a minute)");
|
|
42620
|
+
}, 8e3);
|
|
42621
|
+
const clearSlowStartTimer = () => clearTimeout(slowStartTimer);
|
|
41859
42622
|
const send = (msg) => {
|
|
41860
42623
|
if (ws.readyState === wrapper_default.OPEN) {
|
|
41861
42624
|
const envelope = seal(msg, this.keys.tx);
|
|
@@ -41895,24 +42658,52 @@ var RelayClient = class {
|
|
|
41895
42658
|
JSON.stringify({ kind: "notify", notice: "error", sessionId: msg.sessionId })
|
|
41896
42659
|
);
|
|
41897
42660
|
}
|
|
42661
|
+
if (msg.type === "run-event" && msg.event.type === "handover-proposed") {
|
|
42662
|
+
ws.send(
|
|
42663
|
+
JSON.stringify({
|
|
42664
|
+
kind: "notify",
|
|
42665
|
+
notice: "handover",
|
|
42666
|
+
id: msg.event.id,
|
|
42667
|
+
sessionId: msg.sessionId
|
|
42668
|
+
})
|
|
42669
|
+
);
|
|
42670
|
+
}
|
|
42671
|
+
if (msg.type === "run-event" && msg.event.type === "limit-hit") {
|
|
42672
|
+
ws.send(
|
|
42673
|
+
JSON.stringify({ kind: "notify", notice: "limit-hit", sessionId: msg.sessionId })
|
|
42674
|
+
);
|
|
42675
|
+
}
|
|
41898
42676
|
if (msg.type === "drop" && msg.direction === "to-phone") {
|
|
41899
42677
|
ws.send(JSON.stringify({ kind: "notify", notice: "drop" }));
|
|
41900
42678
|
}
|
|
41901
42679
|
}
|
|
41902
42680
|
};
|
|
41903
42681
|
ws.on("open", () => {
|
|
42682
|
+
clearSlowStartTimer();
|
|
41904
42683
|
this.backoffMs = 500;
|
|
42684
|
+
this.awaitingPongSince = null;
|
|
41905
42685
|
console.log(`relay: connected (${this.relayUrl})`);
|
|
42686
|
+
if (this.everConnected && this.offlineSinceMs !== null) {
|
|
42687
|
+
this.manager.recordDeviceReconnected(Date.now() - this.offlineSinceMs);
|
|
42688
|
+
}
|
|
42689
|
+
this.everConnected = true;
|
|
42690
|
+
this.offlineSinceMs = null;
|
|
41906
42691
|
this.detach = this.manager.attach(send);
|
|
41907
42692
|
send(this.manager.hello());
|
|
41908
42693
|
void this.manager.manifest().then(send);
|
|
41909
42694
|
this.heartbeat = setInterval(() => {
|
|
41910
|
-
if (ws.readyState
|
|
41911
|
-
|
|
42695
|
+
if (ws.readyState !== wrapper_default.OPEN) return;
|
|
42696
|
+
if (this.awaitingPongSince !== null) {
|
|
42697
|
+
console.log("relay: no response since last heartbeat \u2014 reconnecting");
|
|
42698
|
+
ws.terminate();
|
|
42699
|
+
return;
|
|
41912
42700
|
}
|
|
42701
|
+
this.awaitingPongSince = Date.now();
|
|
42702
|
+
ws.send(JSON.stringify({ kind: "ping" }));
|
|
41913
42703
|
}, HEARTBEAT_MS);
|
|
41914
42704
|
});
|
|
41915
42705
|
ws.on("message", (raw) => {
|
|
42706
|
+
this.awaitingPongSince = null;
|
|
41916
42707
|
let frame;
|
|
41917
42708
|
try {
|
|
41918
42709
|
frame = parseRelayFrame(raw.toString());
|
|
@@ -41926,6 +42717,18 @@ var RelayClient = class {
|
|
|
41926
42717
|
);
|
|
41927
42718
|
return;
|
|
41928
42719
|
}
|
|
42720
|
+
if (frame.kind === "handover-verdict") {
|
|
42721
|
+
this.manager.handle(
|
|
42722
|
+
{
|
|
42723
|
+
type: "handover-response",
|
|
42724
|
+
sessionId: frame.chatSessionId,
|
|
42725
|
+
handoverId: frame.handoverId,
|
|
42726
|
+
accept: frame.accept
|
|
42727
|
+
},
|
|
42728
|
+
send
|
|
42729
|
+
);
|
|
42730
|
+
return;
|
|
42731
|
+
}
|
|
41929
42732
|
if (frame.kind !== "peer") return;
|
|
41930
42733
|
try {
|
|
41931
42734
|
const envelope = EnvelopeSchema.parse(frame.payload);
|
|
@@ -41938,7 +42741,12 @@ var RelayClient = class {
|
|
|
41938
42741
|
console.error(`relay: ${err.message}`);
|
|
41939
42742
|
});
|
|
41940
42743
|
ws.on("close", () => {
|
|
42744
|
+
clearSlowStartTimer();
|
|
41941
42745
|
this.cleanup();
|
|
42746
|
+
if (this.everConnected && this.offlineSinceMs === null) {
|
|
42747
|
+
this.offlineSinceMs = Date.now();
|
|
42748
|
+
this.manager.recordDeviceOffline();
|
|
42749
|
+
}
|
|
41942
42750
|
if (this.stopped) return;
|
|
41943
42751
|
console.log(`relay: disconnected \u2014 retrying in ${this.backoffMs}ms`);
|
|
41944
42752
|
setTimeout(() => this.connect(), this.backoffMs);
|
|
@@ -41954,7 +42762,7 @@ var RelayClient = class {
|
|
|
41954
42762
|
};
|
|
41955
42763
|
|
|
41956
42764
|
// ../daemon/src/pairing.ts
|
|
41957
|
-
import { existsSync as
|
|
42765
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
|
|
41958
42766
|
import { homedir as homedir7 } from "node:os";
|
|
41959
42767
|
import { join as join8 } from "node:path";
|
|
41960
42768
|
import { randomInt } from "node:crypto";
|
|
@@ -41964,7 +42772,7 @@ var POLL_MS = 2e3;
|
|
|
41964
42772
|
var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
41965
42773
|
async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://offhand-web.onrender.com") {
|
|
41966
42774
|
await ready;
|
|
41967
|
-
if (!forceNew &&
|
|
42775
|
+
if (!forceNew && existsSync8(PAIRING_PATH)) {
|
|
41968
42776
|
const f2 = JSON.parse(readFileSync4(PAIRING_PATH, "utf8"));
|
|
41969
42777
|
const kp2 = { publicKey: fromB64u(f2.daemonPublicKey), secretKey: fromB64u(f2.daemonSecretKey) };
|
|
41970
42778
|
const phonePk = fromB64u(f2.phonePublicKey);
|
|
@@ -42171,9 +42979,9 @@ function buildPreview(toolName, input) {
|
|
|
42171
42979
|
return void 0;
|
|
42172
42980
|
}
|
|
42173
42981
|
function formatUnifiedDiff(diff) {
|
|
42174
|
-
|
|
42175
|
-
|
|
42176
|
-
|
|
42982
|
+
return diff.split("\n").filter(
|
|
42983
|
+
(l2) => !l2.startsWith("index ") && !l2.startsWith("Index:") && !/^={3,}$/.test(l2) && !/^(new|deleted) file mode /.test(l2) && !/^(old|new) mode /.test(l2)
|
|
42984
|
+
).join("\n").trimEnd();
|
|
42177
42985
|
}
|
|
42178
42986
|
function truncate6(s2, max) {
|
|
42179
42987
|
return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
|
|
@@ -42214,7 +43022,7 @@ async function downloadArtifact(relayUrl2, sessionId, blobId, keys) {
|
|
|
42214
43022
|
// ../daemon/src/autostart.ts
|
|
42215
43023
|
import { homedir as homedir8 } from "node:os";
|
|
42216
43024
|
import { join as join10, resolve as resolve4, dirname as dirname3 } from "node:path";
|
|
42217
|
-
import { existsSync as
|
|
43025
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync } from "node:fs";
|
|
42218
43026
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
42219
43027
|
import { spawnSync } from "node:child_process";
|
|
42220
43028
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
@@ -42235,27 +43043,23 @@ function getWrapperPath() {
|
|
|
42235
43043
|
function getShortcutPath() {
|
|
42236
43044
|
return join10(getStartupDir(), SHORTCUT_NAME);
|
|
42237
43045
|
}
|
|
42238
|
-
function buildWrapperScript(
|
|
42239
|
-
const
|
|
42240
|
-
const
|
|
42241
|
-
const
|
|
42242
|
-
const
|
|
42243
|
-
const
|
|
42244
|
-
const
|
|
42245
|
-
const
|
|
42246
|
-
|
|
42247
|
-
|
|
42248
|
-
|
|
42249
|
-
|
|
42250
|
-
|
|
42251
|
-
|
|
42252
|
-
|
|
42253
|
-
|
|
42254
|
-
|
|
42255
|
-
const cmd = parts.join(" ");
|
|
42256
|
-
return `@echo off
|
|
42257
|
-
cd /d "${projectRoot}"
|
|
42258
|
-
${cmd}
|
|
43046
|
+
function buildWrapperScript(config2, entry = process.argv[1] ?? "", execPath = process.execPath) {
|
|
43047
|
+
const wsArgs2 = config2.workspaces.map((w2) => `--workspace "${w2}"`).join(" ");
|
|
43048
|
+
const relayArg = config2.relayUrl ? `--relay "${config2.relayUrl}"` : "";
|
|
43049
|
+
const devUrlArg = config2.devUrl ? `--dev-url "${config2.devUrl}"` : "";
|
|
43050
|
+
const portArg = `--port ${config2.port}`;
|
|
43051
|
+
const approvalArg = `--approval-timeout ${config2.approvalTimeout ?? 300}`;
|
|
43052
|
+
const webUrlArg = `--web-url "${config2.webUrl ?? "https://offhand-web.onrender.com"}"`;
|
|
43053
|
+
const args2 = [wsArgs2, relayArg, devUrlArg, portArg, approvalArg, webUrlArg].filter(Boolean).join(" ");
|
|
43054
|
+
if (entry.endsWith(".ts")) {
|
|
43055
|
+
const projectRoot = resolve4(__dirname2, "..", "..");
|
|
43056
|
+
return `@echo off\r
|
|
43057
|
+
cd /d "${projectRoot}"\r
|
|
43058
|
+
pnpm --filter @offhand/daemon dev -- ${args2}\r
|
|
43059
|
+
`;
|
|
43060
|
+
}
|
|
43061
|
+
return `@echo off\r
|
|
43062
|
+
"${execPath}" "${entry}" ${args2}\r
|
|
42259
43063
|
`;
|
|
42260
43064
|
}
|
|
42261
43065
|
function createShortcut(targetPath, shortcutPath) {
|
|
@@ -42276,11 +43080,11 @@ $Shortcut.Save()`;
|
|
|
42276
43080
|
return { success: result.status === 0, output: result.stdout + result.stderr };
|
|
42277
43081
|
}
|
|
42278
43082
|
function checkShortcutExists(shortcutPath) {
|
|
42279
|
-
return
|
|
43083
|
+
return existsSync9(shortcutPath);
|
|
42280
43084
|
}
|
|
42281
|
-
function installAutostart(
|
|
43085
|
+
function installAutostart(config2) {
|
|
42282
43086
|
const wrapperPath = getWrapperPath();
|
|
42283
|
-
const script = buildWrapperScript(
|
|
43087
|
+
const script = buildWrapperScript(config2);
|
|
42284
43088
|
writeFileSync4(wrapperPath, script);
|
|
42285
43089
|
const shortcutPath = getShortcutPath();
|
|
42286
43090
|
const result = createShortcut(wrapperPath, shortcutPath);
|
|
@@ -42296,7 +43100,7 @@ function getAutostartStatus() {
|
|
|
42296
43100
|
const shortcutPath = getShortcutPath();
|
|
42297
43101
|
const installed = checkShortcutExists(shortcutPath);
|
|
42298
43102
|
const wrapperPath = getWrapperPath();
|
|
42299
|
-
const wrapperExists =
|
|
43103
|
+
const wrapperExists = existsSync9(wrapperPath);
|
|
42300
43104
|
let details = `Shortcut: ${shortcutPath} (${installed ? "EXISTS" : "MISSING"})`;
|
|
42301
43105
|
if (wrapperExists) {
|
|
42302
43106
|
details += `
|
|
@@ -42304,14 +43108,13 @@ Wrapper: ${wrapperPath} (EXISTS)`;
|
|
|
42304
43108
|
}
|
|
42305
43109
|
return { installed, details };
|
|
42306
43110
|
}
|
|
42307
|
-
function saveConfig(
|
|
43111
|
+
function saveConfig(config2) {
|
|
42308
43112
|
const configPath = join10(OFFHAND_HOME, "autostart.json");
|
|
42309
|
-
writeFileSync4(configPath, JSON.stringify(
|
|
43113
|
+
writeFileSync4(configPath, JSON.stringify(config2, null, 2));
|
|
42310
43114
|
}
|
|
42311
43115
|
function getConfigFromStore() {
|
|
42312
|
-
const workspaces = ["C:\\Users\\udbha\\dev\\offhand"];
|
|
42313
43116
|
return {
|
|
42314
|
-
workspaces,
|
|
43117
|
+
workspaces: [],
|
|
42315
43118
|
port: 4317,
|
|
42316
43119
|
relayUrl: void 0,
|
|
42317
43120
|
devUrl: void 0,
|
|
@@ -42354,7 +43157,7 @@ var devUrl = argValue("--dev-url");
|
|
|
42354
43157
|
var store = new Store();
|
|
42355
43158
|
var wsArgs = argValues("--workspace").map((w2) => resolve5(w2));
|
|
42356
43159
|
for (const w2 of wsArgs) {
|
|
42357
|
-
if (!
|
|
43160
|
+
if (!existsSync10(w2)) {
|
|
42358
43161
|
console.error(`workspace does not exist: ${w2}`);
|
|
42359
43162
|
process.exit(1);
|
|
42360
43163
|
}
|
|
@@ -42387,25 +43190,20 @@ if (m2.type === "manifest") {
|
|
|
42387
43190
|
new LocalSessionServer(manager, port, broker);
|
|
42388
43191
|
console.log(` local : ws://127.0.0.1:${port}`);
|
|
42389
43192
|
console.log(` approvals : timeout ${approvalTimeoutMs / 1e3}s then auto-deny`);
|
|
42390
|
-
var
|
|
42391
|
-
|
|
42392
|
-
|
|
42393
|
-
|
|
42394
|
-
|
|
42395
|
-
|
|
42396
|
-
|
|
42397
|
-
|
|
42398
|
-
|
|
42399
|
-
|
|
42400
|
-
|
|
42401
|
-
|
|
42402
|
-
if (success) {
|
|
42403
|
-
console.log(` autostart : installed (runs on login)`);
|
|
42404
|
-
} else {
|
|
42405
|
-
console.warn(` autostart : failed to install: ${output}`);
|
|
42406
|
-
}
|
|
43193
|
+
var wasInstalled = getAutostartStatus().installed;
|
|
43194
|
+
var config = getConfigFromStore();
|
|
43195
|
+
config.workspaces = store.listWorkspaces().map((w2) => w2.path);
|
|
43196
|
+
config.port = port;
|
|
43197
|
+
config.relayUrl = relayUrl;
|
|
43198
|
+
config.devUrl = devUrl;
|
|
43199
|
+
config.approvalTimeout = approvalTimeoutMs / 1e3;
|
|
43200
|
+
config.webUrl = webUrl;
|
|
43201
|
+
saveConfig(config);
|
|
43202
|
+
var { success, output } = installAutostart(config);
|
|
43203
|
+
if (success) {
|
|
43204
|
+
console.log(` autostart : ${wasInstalled ? "refreshed" : "installed"} (runs on login)`);
|
|
42407
43205
|
} else {
|
|
42408
|
-
console.
|
|
43206
|
+
console.warn(` autostart : failed to ${wasInstalled ? "refresh" : "install"}: ${output}`);
|
|
42409
43207
|
}
|
|
42410
43208
|
if (relayUrl) {
|
|
42411
43209
|
const pairing = await ensurePairing(relayUrl, forceRepair, webUrl);
|