offhands 0.1.5 → 0.1.9
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 +189 -10
- package/dist/daemon.mjs +1567 -181
- 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,21 @@ 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"];
|
|
4979
|
+
// The vendor's own supported install path (code.claude.com/docs/en/setup
|
|
4980
|
+
// lists this as the "advanced installation option" alongside their native
|
|
4981
|
+
// curl/PowerShell installer — npm works fine and is what the daemon can
|
|
4982
|
+
// run unattended). Verified Sept 2026; re-check if this ever 404s.
|
|
4983
|
+
installCommand = { command: "npm", args: ["install", "-g", "@anthropic-ai/claude-code"] };
|
|
4946
4984
|
loggedIn() {
|
|
4947
4985
|
return existsSync(join(homedir(), ".claude", ".credentials.json"));
|
|
4948
4986
|
}
|
|
@@ -4988,7 +5026,15 @@ var ClaudeCodeRunner = class {
|
|
|
4988
5026
|
offhand: {
|
|
4989
5027
|
command: process.execPath,
|
|
4990
5028
|
args: [APPROVAL_MCP_PATH],
|
|
4991
|
-
|
|
5029
|
+
// OFFHAND_STATUS_URL is derived from approvalUrl rather than a
|
|
5030
|
+
// second constructor param — same daemon, same port, one fewer
|
|
5031
|
+
// thing to keep in sync as this.approvalUrl already carries the
|
|
5032
|
+
// host:port everything else here needs.
|
|
5033
|
+
env: {
|
|
5034
|
+
OFFHAND_APPROVAL_URL: this.approvalUrl,
|
|
5035
|
+
OFFHAND_STATUS_URL: this.approvalUrl.replace(/\/approval$/, "/status"),
|
|
5036
|
+
OFFHAND_SESSION_ID: run.sessionId
|
|
5037
|
+
}
|
|
4992
5038
|
}
|
|
4993
5039
|
}
|
|
4994
5040
|
};
|
|
@@ -5102,6 +5148,7 @@ var CodexCliRunner = class {
|
|
|
5102
5148
|
name = "OpenAI Codex CLI";
|
|
5103
5149
|
models = [];
|
|
5104
5150
|
supportsApprovals = false;
|
|
5151
|
+
installCommand = { command: "npm", args: ["install", "-g", "@openai/codex"] };
|
|
5105
5152
|
detect = () => commandExists("codex");
|
|
5106
5153
|
start = (_run) => notImplemented(this.id);
|
|
5107
5154
|
};
|
|
@@ -5110,7 +5157,18 @@ var CursorAgentRunner = class {
|
|
|
5110
5157
|
name = "Cursor";
|
|
5111
5158
|
models = [];
|
|
5112
5159
|
supportsApprovals = false;
|
|
5113
|
-
|
|
5160
|
+
// No npm package exists — the vendor's real installer is a piped shell
|
|
5161
|
+
// script (`curl https://cursor.com/install | bash` / an iwr|iex on
|
|
5162
|
+
// Windows). Never auto-execute a remote script on the user's behalf;
|
|
5163
|
+
// link out instead of offering a one-tap install here.
|
|
5164
|
+
installUrl = "https://cursor.com/docs/cli/installation";
|
|
5165
|
+
// The installed binary is actually named `agent`, not `cursor-agent`
|
|
5166
|
+
// (confirmed against the vendor's current docs, Sept 2026) — detect()
|
|
5167
|
+
// was checking the wrong command name and would never find a real
|
|
5168
|
+
// install. Fixed alongside the install-hint work since the two are
|
|
5169
|
+
// directly related: there's no point offering install guidance for a
|
|
5170
|
+
// runner whose own detection can never succeed.
|
|
5171
|
+
detect = () => commandExists("agent");
|
|
5114
5172
|
start = (_run) => notImplemented(this.id);
|
|
5115
5173
|
};
|
|
5116
5174
|
var GeminiCliRunner = class {
|
|
@@ -5118,6 +5176,7 @@ var GeminiCliRunner = class {
|
|
|
5118
5176
|
name = "Gemini CLI";
|
|
5119
5177
|
models = [];
|
|
5120
5178
|
supportsApprovals = false;
|
|
5179
|
+
installCommand = { command: "npm", args: ["install", "-g", "@google/gemini-cli"] };
|
|
5121
5180
|
detect = () => commandExists("gemini");
|
|
5122
5181
|
start = (_run) => notImplemented(this.id);
|
|
5123
5182
|
};
|
|
@@ -5174,6 +5233,12 @@ var CopilotCliRunner = class {
|
|
|
5174
5233
|
models = [];
|
|
5175
5234
|
// no headless enumeration in 1.0.31; CLI default
|
|
5176
5235
|
supportsApprovals = false;
|
|
5236
|
+
// No effortTiers: start() never reads run.effort — there's no thinking-
|
|
5237
|
+
// budget flag in Copilot CLI 1.0.31's non-interactive mode. Leaving this
|
|
5238
|
+
// undefined (not an empty array) is what tells the phone to hide the
|
|
5239
|
+
// effort control here rather than show one that silently does nothing.
|
|
5240
|
+
// Vendor's own docs (docs.github.com/copilot/how-tos/copilot-cli/install-copilot-cli).
|
|
5241
|
+
installCommand = { command: "npm", args: ["install", "-g", "@github/copilot"] };
|
|
5177
5242
|
/** [command, ...prefixArgs] resolved once. */
|
|
5178
5243
|
resolved = null;
|
|
5179
5244
|
resolveCommand() {
|
|
@@ -5302,7 +5367,8 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
5302
5367
|
import { createServer } from "node:net";
|
|
5303
5368
|
import { existsSync as existsSync3 } from "node:fs";
|
|
5304
5369
|
import { homedir as homedir3 } from "node:os";
|
|
5305
|
-
import { delimiter as delimiter2, join as join3 } from "node:path";
|
|
5370
|
+
import { delimiter as delimiter2, join as join3, dirname as dirname2 } from "node:path";
|
|
5371
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5306
5372
|
|
|
5307
5373
|
// ../daemon/src/runners/opencode-map.ts
|
|
5308
5374
|
function mapOpenCodeEvent(value) {
|
|
@@ -5385,22 +5451,59 @@ function truncate3(s2, max) {
|
|
|
5385
5451
|
|
|
5386
5452
|
// ../daemon/src/runners/opencode-cli.ts
|
|
5387
5453
|
var POLL_INTERVAL_MS = 500;
|
|
5454
|
+
var APPROVAL_MCP_PATH2 = join3(dirname2(fileURLToPath2(import.meta.url)), "..", "approval-mcp.mjs");
|
|
5455
|
+
function statusOnlyMcpConfigContent(statusUrl2, sessionId) {
|
|
5456
|
+
return JSON.stringify({
|
|
5457
|
+
mcp: {
|
|
5458
|
+
"offhand-status": {
|
|
5459
|
+
type: "local",
|
|
5460
|
+
command: [process.execPath, APPROVAL_MCP_PATH2],
|
|
5461
|
+
environment: {
|
|
5462
|
+
OFFHAND_STATUS_URL: statusUrl2 ?? "",
|
|
5463
|
+
OFFHAND_SESSION_ID: sessionId
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
5467
|
+
});
|
|
5468
|
+
}
|
|
5388
5469
|
var OpenCodeRunner = class {
|
|
5389
|
-
constructor(broker2) {
|
|
5470
|
+
constructor(broker2, statusUrl2) {
|
|
5390
5471
|
this.broker = broker2;
|
|
5472
|
+
this.statusUrl = statusUrl2;
|
|
5391
5473
|
}
|
|
5392
5474
|
id = "opencode";
|
|
5393
5475
|
name = "OpenCode";
|
|
5394
5476
|
supportsApprovals = true;
|
|
5395
5477
|
models = [];
|
|
5396
5478
|
// populated dynamically via `opencode models`
|
|
5479
|
+
// Real: every tier maps to a --variant value below (see startViaCli()) —
|
|
5480
|
+
// low/medium collide onto minimal/high internally, but all four inputs
|
|
5481
|
+
// are genuinely honored, just not 1:1 named.
|
|
5482
|
+
effortTiers = ["low", "medium", "high", "max"];
|
|
5483
|
+
// Package name is `opencode-ai` (opencode.ai/docs), not `opencode` or
|
|
5484
|
+
// `@opencode/cli` — verified against the vendor's own install docs.
|
|
5485
|
+
installCommand = { command: "npm", args: ["install", "-g", "opencode-ai"] };
|
|
5397
5486
|
/** [command, ...prefixArgs] resolved once. */
|
|
5398
5487
|
resolved = null;
|
|
5399
5488
|
modelsCache = [];
|
|
5400
|
-
// Lazily-started
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5489
|
+
// Lazily-started `opencode serve` instances backing startViaHttpApi — ONE
|
|
5490
|
+
// PER OFFHAND SESSION, not a single shared process. A shared server was
|
|
5491
|
+
// the original design (simpler, one cold-start instead of many), but its
|
|
5492
|
+
// MCP config is fixed at spawn time via an env var, and the server itself
|
|
5493
|
+
// is genuinely multi-tenant (concurrent offhand sessions each get their
|
|
5494
|
+
// own opencode-internal session id on the SAME shared HTTP server — see
|
|
5495
|
+
// the `POST /session` call in startViaHttpApi). A single shared server
|
|
5496
|
+
// therefore cannot carry a correct OFFHAND_SESSION_ID for
|
|
5497
|
+
// offhand_status_update: two sessions running at once would have the
|
|
5498
|
+
// second one's status updates silently attributed to the first. Keying
|
|
5499
|
+
// by session id costs an extra `opencode serve` cold-start per session
|
|
5500
|
+
// (kept alive for that session's lifetime, matching how
|
|
5501
|
+
// `resumeConversationId` already expects the SAME server instance across
|
|
5502
|
+
// that session's later turns) in exchange for actually-correct
|
|
5503
|
+
// attribution — worth it; a silently wrong session id is a Rule #2
|
|
5504
|
+
// violation, a slower first prompt is not.
|
|
5505
|
+
servers = /* @__PURE__ */ new Map();
|
|
5506
|
+
serverStarting = /* @__PURE__ */ new Map();
|
|
5404
5507
|
resolveCommand() {
|
|
5405
5508
|
if (this.resolved) return this.resolved;
|
|
5406
5509
|
if (process.platform === "win32") {
|
|
@@ -5462,15 +5565,15 @@ var OpenCodeRunner = class {
|
|
|
5462
5565
|
stdio: ["ignore", "pipe", "ignore"],
|
|
5463
5566
|
shell: process.platform === "win32"
|
|
5464
5567
|
});
|
|
5465
|
-
let
|
|
5568
|
+
let output2 = "";
|
|
5466
5569
|
p2.stdout.setEncoding("utf8");
|
|
5467
5570
|
p2.stdout.on("data", (chunk) => {
|
|
5468
|
-
|
|
5571
|
+
output2 += chunk;
|
|
5469
5572
|
});
|
|
5470
5573
|
p2.on("close", (code) => {
|
|
5471
5574
|
if (code === 0) {
|
|
5472
5575
|
try {
|
|
5473
|
-
const lines =
|
|
5576
|
+
const lines = output2.trim().split("\n");
|
|
5474
5577
|
const models = lines.map((l2) => l2.trim()).filter((l2) => l2.length > 0);
|
|
5475
5578
|
this.modelsCache = models;
|
|
5476
5579
|
Object.defineProperty(this, "models", {
|
|
@@ -5557,7 +5660,10 @@ var OpenCodeRunner = class {
|
|
|
5557
5660
|
child = spawn4(cmd[0], args2, {
|
|
5558
5661
|
cwd: run.workspace,
|
|
5559
5662
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5560
|
-
shell: process.platform === "win32"
|
|
5663
|
+
shell: process.platform === "win32",
|
|
5664
|
+
// One-shot process per prompt — no shared-server cross-session risk
|
|
5665
|
+
// here, so the session id can go straight on the env unconditionally.
|
|
5666
|
+
env: { ...process.env, OPENCODE_CONFIG_CONTENT: statusOnlyMcpConfigContent(this.statusUrl, run.sessionId) }
|
|
5561
5667
|
});
|
|
5562
5668
|
} catch (e) {
|
|
5563
5669
|
queue.push({ type: "error", message: `failed to spawn opencode: ${String(e)}` });
|
|
@@ -5631,7 +5737,7 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5631
5737
|
};
|
|
5632
5738
|
void (async () => {
|
|
5633
5739
|
try {
|
|
5634
|
-
port2 = await this.ensureServer();
|
|
5740
|
+
port2 = await this.ensureServer(run.sessionId);
|
|
5635
5741
|
} catch (e) {
|
|
5636
5742
|
emit([{ type: "error", message: `failed to start opencode server: ${String(e)}` }]);
|
|
5637
5743
|
cleanup();
|
|
@@ -5652,6 +5758,14 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5652
5758
|
sessionId = session.id;
|
|
5653
5759
|
}
|
|
5654
5760
|
callbacks?.onConversationId?.(sessionId);
|
|
5761
|
+
const baselineMessageIds = /* @__PURE__ */ new Set();
|
|
5762
|
+
if (run.resumeConversationId) {
|
|
5763
|
+
try {
|
|
5764
|
+
const existing = await httpJson(`${base}/session/${sessionId}/message?directory=${encDir}`, "GET");
|
|
5765
|
+
for (const row of existing) baselineMessageIds.add(row.info.id);
|
|
5766
|
+
} catch {
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5655
5769
|
const parts = [{ type: "text", text: run.prompt }];
|
|
5656
5770
|
for (const file of run.attachments ?? []) {
|
|
5657
5771
|
parts.push({ type: "file", mime: "application/octet-stream", url: fileUrl(file) });
|
|
@@ -5660,7 +5774,7 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5660
5774
|
const modelRef = parseModelRef(run.model);
|
|
5661
5775
|
if (modelRef) promptBody.model = { providerID: modelRef.providerId, modelID: modelRef.modelId };
|
|
5662
5776
|
await httpJson(`${base}/session/${sessionId}/prompt_async?directory=${encDir}`, "POST", promptBody, true);
|
|
5663
|
-
await this.pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, () => cancelled);
|
|
5777
|
+
await this.pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, baselineMessageIds, () => cancelled);
|
|
5664
5778
|
} catch (e) {
|
|
5665
5779
|
if (!cancelled) emit([{ type: "error", message: `opencode HTTP API error: ${String(e)}` }]);
|
|
5666
5780
|
} finally {
|
|
@@ -5689,7 +5803,7 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5689
5803
|
};
|
|
5690
5804
|
}
|
|
5691
5805
|
/** Poll session messages + pending permissions/questions until the turn finishes. */
|
|
5692
|
-
async pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, isCancelled) {
|
|
5806
|
+
async pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, baselineMessageIds, isCancelled) {
|
|
5693
5807
|
const seenPermissions = /* @__PURE__ */ new Set();
|
|
5694
5808
|
const seenQuestions = /* @__PURE__ */ new Set();
|
|
5695
5809
|
const emittedParts = /* @__PURE__ */ new Set();
|
|
@@ -5721,21 +5835,9 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5721
5835
|
} catch {
|
|
5722
5836
|
continue;
|
|
5723
5837
|
}
|
|
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
|
-
}
|
|
5838
|
+
const result = processMessages(messages, baselineMessageIds, emittedParts);
|
|
5839
|
+
emit(result.events);
|
|
5840
|
+
if (result.sawTerminal) sawTerminal = true;
|
|
5739
5841
|
}
|
|
5740
5842
|
}
|
|
5741
5843
|
/** Route one pending permission through the shared broker, then reply. */
|
|
@@ -5785,37 +5887,61 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5785
5887
|
} catch {
|
|
5786
5888
|
}
|
|
5787
5889
|
}
|
|
5788
|
-
/** Lazily start (once) the
|
|
5789
|
-
* broker-driven runs, and wait for it to accept
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5890
|
+
/** Lazily start (once per offhand session) the `opencode serve` instance
|
|
5891
|
+
* backing that session's broker-driven runs, and wait for it to accept
|
|
5892
|
+
* connections. Reused across that session's later turns (so
|
|
5893
|
+
* `resumeConversationId` keeps working); a different session never
|
|
5894
|
+
* shares this instance — see the field comment above for why. */
|
|
5895
|
+
async ensureServer(sessionId) {
|
|
5896
|
+
const existing = this.servers.get(sessionId);
|
|
5897
|
+
if (existing) return existing.port;
|
|
5898
|
+
const inFlight = this.serverStarting.get(sessionId);
|
|
5899
|
+
if (inFlight) return inFlight;
|
|
5900
|
+
const starting = (async () => {
|
|
5794
5901
|
const cmd = this.resolveCommand();
|
|
5795
5902
|
if (!cmd) throw new Error("opencode CLI not found");
|
|
5796
5903
|
const port2 = await findFreePort();
|
|
5797
5904
|
const proc = spawn4(cmd[0], [...cmd.slice(1), "serve", "--port", String(port2), "--hostname", "127.0.0.1"], {
|
|
5798
5905
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5799
|
-
shell: process.platform === "win32"
|
|
5906
|
+
shell: process.platform === "win32",
|
|
5907
|
+
env: { ...process.env, OPENCODE_CONFIG_CONTENT: statusOnlyMcpConfigContent(this.statusUrl, sessionId) }
|
|
5800
5908
|
});
|
|
5801
5909
|
proc.on("exit", () => {
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
this.serverPort = null;
|
|
5805
|
-
}
|
|
5910
|
+
const cur = this.servers.get(sessionId);
|
|
5911
|
+
if (cur?.process === proc) this.servers.delete(sessionId);
|
|
5806
5912
|
});
|
|
5807
5913
|
await waitForServerReady(port2);
|
|
5808
|
-
this.
|
|
5809
|
-
this.serverPort = port2;
|
|
5914
|
+
this.servers.set(sessionId, { port: port2, process: proc });
|
|
5810
5915
|
return port2;
|
|
5811
5916
|
})();
|
|
5917
|
+
this.serverStarting.set(sessionId, starting);
|
|
5812
5918
|
try {
|
|
5813
|
-
return await
|
|
5919
|
+
return await starting;
|
|
5814
5920
|
} finally {
|
|
5815
|
-
this.serverStarting
|
|
5921
|
+
this.serverStarting.delete(sessionId);
|
|
5816
5922
|
}
|
|
5817
5923
|
}
|
|
5818
5924
|
};
|
|
5925
|
+
function processMessages(messages, baselineMessageIds, emittedParts) {
|
|
5926
|
+
const events = [];
|
|
5927
|
+
let sawTerminal = false;
|
|
5928
|
+
for (const row of messages) {
|
|
5929
|
+
if (row.info.role !== "assistant" || baselineMessageIds.has(row.info.id)) continue;
|
|
5930
|
+
for (const part of row.parts) {
|
|
5931
|
+
if (!part.id || emittedParts.has(part.id)) continue;
|
|
5932
|
+
if ((part.type === "text" || part.type === "reasoning") && !part.time?.end) continue;
|
|
5933
|
+
emittedParts.add(part.id);
|
|
5934
|
+
const partEvents = mapOpenCodeEvent({ part });
|
|
5935
|
+
events.push(...partEvents);
|
|
5936
|
+
if (partEvents.some((e) => e.type === "done" || e.type === "error")) sawTerminal = true;
|
|
5937
|
+
}
|
|
5938
|
+
if (!sawTerminal && row.info.error) {
|
|
5939
|
+
sawTerminal = true;
|
|
5940
|
+
events.push({ type: "error", message: row.info.error.data?.message ?? "run failed" });
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
return { events, sawTerminal };
|
|
5944
|
+
}
|
|
5819
5945
|
function buildPermissionRuleset(mode) {
|
|
5820
5946
|
const base = [{ permission: "*", pattern: "*", action: "allow" }];
|
|
5821
5947
|
if (mode === "acceptEdits") {
|
|
@@ -5886,10 +6012,12 @@ function sleep(ms) {
|
|
|
5886
6012
|
|
|
5887
6013
|
// ../daemon/src/session-manager.ts
|
|
5888
6014
|
import { randomUUID } from "node:crypto";
|
|
5889
|
-
import { hostname, platform, tmpdir } from "node:os";
|
|
5890
|
-
import { existsSync as
|
|
5891
|
-
import { dirname as
|
|
5892
|
-
import { fileURLToPath as
|
|
6015
|
+
import { hostname, platform as platform2, tmpdir } from "node:os";
|
|
6016
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6017
|
+
import { dirname as dirname3, join as join6, basename as basename3, parse as parse2, resolve as resolve3 } from "node:path";
|
|
6018
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
6019
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
6020
|
+
import { promisify as promisify4 } from "node:util";
|
|
5893
6021
|
|
|
5894
6022
|
// ../daemon/src/receipts.ts
|
|
5895
6023
|
import { execFile } from "node:child_process";
|
|
@@ -5911,6 +6039,7 @@ async function workspaceInfo(row) {
|
|
|
5911
6039
|
path: row.path,
|
|
5912
6040
|
label: row.label,
|
|
5913
6041
|
policy: row.policy,
|
|
6042
|
+
intelligenceMode: row.intelligenceMode,
|
|
5914
6043
|
...branch ? { gitBranch: branch } : {},
|
|
5915
6044
|
...status !== null ? { dirty: status.trim() !== "" } : {},
|
|
5916
6045
|
...row.devUrl ? { devUrl: row.devUrl } : {}
|
|
@@ -6234,10 +6363,508 @@ function mimeFromName(path) {
|
|
|
6234
6363
|
}[ext] ?? "application/octet-stream";
|
|
6235
6364
|
}
|
|
6236
6365
|
|
|
6366
|
+
// ../daemon/src/handover.ts
|
|
6367
|
+
var MAX_DIFF_BYTES2 = 48 * 1024;
|
|
6368
|
+
var MAX_TOOL_SUMMARIES = 30;
|
|
6369
|
+
var MAX_RECENT_MESSAGES = 10;
|
|
6370
|
+
async function liveDiff(workspace) {
|
|
6371
|
+
const diff = await git(workspace, "diff", "HEAD");
|
|
6372
|
+
if (!diff) return "";
|
|
6373
|
+
return diff.length > MAX_DIFF_BYTES2 ? diff.slice(0, MAX_DIFF_BYTES2) + "\n\u2026 diff truncated \u2026" : diff;
|
|
6374
|
+
}
|
|
6375
|
+
function assembleHandoverPacket(items, diff) {
|
|
6376
|
+
const toolSummaries = [];
|
|
6377
|
+
let todos;
|
|
6378
|
+
const recentMessages = [];
|
|
6379
|
+
const resolvedApprovalIds = /* @__PURE__ */ new Set();
|
|
6380
|
+
const approvalsById = /* @__PURE__ */ new Map();
|
|
6381
|
+
for (const msg of items) {
|
|
6382
|
+
if (msg.type === "run-started") {
|
|
6383
|
+
recentMessages.push({ role: "user", text: msg.prompt });
|
|
6384
|
+
continue;
|
|
6385
|
+
}
|
|
6386
|
+
if (msg.type !== "run-event") continue;
|
|
6387
|
+
const ev = msg.event;
|
|
6388
|
+
if (ev.type === "tool") {
|
|
6389
|
+
toolSummaries.push(ev.summary);
|
|
6390
|
+
if (ev.todos) todos = ev.todos;
|
|
6391
|
+
} else if (ev.type === "text") {
|
|
6392
|
+
const last = recentMessages[recentMessages.length - 1];
|
|
6393
|
+
if (last?.role === "assistant") last.text += ev.chunk;
|
|
6394
|
+
else recentMessages.push({ role: "assistant", text: ev.chunk });
|
|
6395
|
+
} else if (ev.type === "approval") {
|
|
6396
|
+
approvalsById.set(ev.id, { action: ev.action, detail: ev.detail, risk: ev.risk });
|
|
6397
|
+
} else if (ev.type === "approval-result") {
|
|
6398
|
+
resolvedApprovalIds.add(ev.id);
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
const pendingApprovals = [...approvalsById.entries()].filter(([id]) => !resolvedApprovalIds.has(id)).map(([, a2]) => a2);
|
|
6402
|
+
return {
|
|
6403
|
+
diff,
|
|
6404
|
+
toolSummaries: toolSummaries.slice(-MAX_TOOL_SUMMARIES),
|
|
6405
|
+
...todos ? { todos } : {},
|
|
6406
|
+
pendingApprovals,
|
|
6407
|
+
recentMessages: recentMessages.slice(-MAX_RECENT_MESSAGES)
|
|
6408
|
+
};
|
|
6409
|
+
}
|
|
6410
|
+
function translatePacketToPrompt(packet, fromRunnerName) {
|
|
6411
|
+
const sections = [
|
|
6412
|
+
`You're continuing a task handed off from ${fromRunnerName}, which hit its usage limit mid-task. Here is the context so far.`
|
|
6413
|
+
];
|
|
6414
|
+
if (packet.recentMessages.length > 0) {
|
|
6415
|
+
sections.push(
|
|
6416
|
+
"## Recent conversation\n" + packet.recentMessages.map((m3) => `**${m3.role}:** ${m3.text}`).join("\n\n")
|
|
6417
|
+
);
|
|
6418
|
+
}
|
|
6419
|
+
if (packet.toolSummaries.length > 0) {
|
|
6420
|
+
sections.push("## Actions already taken\n" + packet.toolSummaries.map((t2) => `- ${t2}`).join("\n"));
|
|
6421
|
+
}
|
|
6422
|
+
if (packet.todos && packet.todos.length > 0) {
|
|
6423
|
+
sections.push(
|
|
6424
|
+
"## Remaining plan\n" + packet.todos.map((t2) => `- [${t2.status === "completed" ? "x" : " "}] ${t2.content}`).join("\n")
|
|
6425
|
+
);
|
|
6426
|
+
}
|
|
6427
|
+
if (packet.pendingApprovals.length > 0) {
|
|
6428
|
+
sections.push(
|
|
6429
|
+
"## Not yet approved (ask again if still needed)\n" + packet.pendingApprovals.map((a2) => `- ${a2.action}: ${a2.detail} (${a2.risk} risk)`).join("\n")
|
|
6430
|
+
);
|
|
6431
|
+
}
|
|
6432
|
+
if (packet.diff.trim()) {
|
|
6433
|
+
sections.push("## Uncommitted changes so far (git diff)\n```diff\n" + packet.diff + "\n```");
|
|
6434
|
+
}
|
|
6435
|
+
if (packet.agentSummary) {
|
|
6436
|
+
sections.push("## Outgoing agent's own summary\n" + packet.agentSummary);
|
|
6437
|
+
}
|
|
6438
|
+
sections.push("Please continue the task from here.");
|
|
6439
|
+
return sections.join("\n\n");
|
|
6440
|
+
}
|
|
6441
|
+
|
|
6442
|
+
// ../daemon/src/wol.ts
|
|
6443
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
6444
|
+
import { promisify as promisify2 } from "node:util";
|
|
6445
|
+
import { networkInterfaces, platform } from "node:os";
|
|
6446
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
6447
|
+
var exec2 = promisify2(execFile2);
|
|
6448
|
+
function pickPrimaryInterface(ifaces) {
|
|
6449
|
+
for (const [name, infos] of Object.entries(ifaces)) {
|
|
6450
|
+
if (!infos) continue;
|
|
6451
|
+
const ipv4 = infos.find((i2) => i2.family === "IPv4" && !i2.internal);
|
|
6452
|
+
if (ipv4?.mac && ipv4.mac !== "00:00:00:00:00:00") {
|
|
6453
|
+
return { name, mac: ipv4.mac, address: ipv4.address, netmask: ipv4.netmask };
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
return null;
|
|
6457
|
+
}
|
|
6458
|
+
function pickInterfaceByName(ifaces, name) {
|
|
6459
|
+
const infos = ifaces[name];
|
|
6460
|
+
if (!infos) return null;
|
|
6461
|
+
const ipv4 = infos.find((i2) => i2.family === "IPv4" && !i2.internal);
|
|
6462
|
+
if (!ipv4?.mac || ipv4.mac === "00:00:00:00:00:00") return null;
|
|
6463
|
+
return { name, mac: ipv4.mac, address: ipv4.address, netmask: ipv4.netmask };
|
|
6464
|
+
}
|
|
6465
|
+
function computeBroadcastAddress(address, netmask) {
|
|
6466
|
+
const a2 = address.split(".").map(Number);
|
|
6467
|
+
const m3 = netmask.split(".").map(Number);
|
|
6468
|
+
if (a2.length !== 4 || m3.length !== 4 || a2.some(Number.isNaN) || m3.some(Number.isNaN)) return null;
|
|
6469
|
+
return a2.map((octet, i2) => (octet | ~m3[i2] & 255) & 255).join(".");
|
|
6470
|
+
}
|
|
6471
|
+
function normalizeMac(mac) {
|
|
6472
|
+
return (mac ?? "").toLowerCase().replace(/[:-]/g, "");
|
|
6473
|
+
}
|
|
6474
|
+
function classifyWindowsMediaType(mediaType) {
|
|
6475
|
+
const t2 = (mediaType ?? "").toLowerCase();
|
|
6476
|
+
if (t2.includes("802.11") || t2.includes("wireless") || t2.includes("native 802.11")) return "wifi";
|
|
6477
|
+
if (t2.includes("802.3") || t2.includes("ethernet")) return "ethernet";
|
|
6478
|
+
return "unknown";
|
|
6479
|
+
}
|
|
6480
|
+
function parseWindowsAdapterJson(json) {
|
|
6481
|
+
let parsed;
|
|
6482
|
+
try {
|
|
6483
|
+
parsed = JSON.parse(json);
|
|
6484
|
+
} catch {
|
|
6485
|
+
return [];
|
|
6486
|
+
}
|
|
6487
|
+
const arr = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
|
|
6488
|
+
return arr.filter((x2) => !!x2 && typeof x2 === "object").map((x2) => ({
|
|
6489
|
+
name: String(x2.Name ?? ""),
|
|
6490
|
+
macAddress: x2.MacAddress ? String(x2.MacAddress) : null,
|
|
6491
|
+
networkType: classifyWindowsMediaType(x2.MediaType),
|
|
6492
|
+
wakeOnMagicPacket: x2.WakeOnMagicPacket === "Enabled" || x2.WakeOnMagicPacket === "Disabled" || x2.WakeOnMagicPacket === "NotSupported" ? x2.WakeOnMagicPacket : null
|
|
6493
|
+
}));
|
|
6494
|
+
}
|
|
6495
|
+
function parseMacPmset(output2) {
|
|
6496
|
+
const m3 = output2.match(/^\s*womp\s+(\d)/m);
|
|
6497
|
+
return m3 ? m3[1] === "1" : false;
|
|
6498
|
+
}
|
|
6499
|
+
function classifyMacHardwarePort(listing, deviceName) {
|
|
6500
|
+
const blocks = listing.split(/\n\n+/);
|
|
6501
|
+
for (const block of blocks) {
|
|
6502
|
+
if (!new RegExp(`Device:\\s*${deviceName}\\b`).test(block)) continue;
|
|
6503
|
+
if (/Hardware Port:\s*Wi-Fi/i.test(block)) return "wifi";
|
|
6504
|
+
if (/Hardware Port:\s*(Ethernet|Thunderbolt Ethernet)/i.test(block)) return "ethernet";
|
|
6505
|
+
}
|
|
6506
|
+
return "unknown";
|
|
6507
|
+
}
|
|
6508
|
+
function parseLinuxEthtool(output2) {
|
|
6509
|
+
let supports = "";
|
|
6510
|
+
let current = "";
|
|
6511
|
+
for (const line of output2.split(/\r?\n/)) {
|
|
6512
|
+
const supportsMatch = line.match(/^\s*Supports Wake-on:\s*([a-zA-Z]*)/);
|
|
6513
|
+
if (supportsMatch) {
|
|
6514
|
+
supports = supportsMatch[1];
|
|
6515
|
+
continue;
|
|
6516
|
+
}
|
|
6517
|
+
const currentMatch = line.match(/^\s*Wake-on:\s*([a-zA-Z]*)/);
|
|
6518
|
+
if (currentMatch) current = currentMatch[1];
|
|
6519
|
+
}
|
|
6520
|
+
return { supportsWakeOnG: supports.toLowerCase().includes("g"), wakeOnGEnabled: current.toLowerCase().includes("g") };
|
|
6521
|
+
}
|
|
6522
|
+
function computeWolCapability(networkType, wolWorks) {
|
|
6523
|
+
if (!wolWorks) {
|
|
6524
|
+
return {
|
|
6525
|
+
capability: "unsupported",
|
|
6526
|
+
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."
|
|
6527
|
+
};
|
|
6528
|
+
}
|
|
6529
|
+
if (networkType === "wifi") {
|
|
6530
|
+
return {
|
|
6531
|
+
capability: "wifi-limited",
|
|
6532
|
+
reason: "Wake-on-Wireless-LAN is enabled, but WiFi wake support depends on the router too \u2014 less reliable than a wired connection."
|
|
6533
|
+
};
|
|
6534
|
+
}
|
|
6535
|
+
if (networkType === "ethernet") {
|
|
6536
|
+
return { capability: "supported", reason: "Wake-on-LAN is enabled on this wired connection." };
|
|
6537
|
+
}
|
|
6538
|
+
return { capability: "unsupported", reason: "Couldn't determine this machine's network connection type." };
|
|
6539
|
+
}
|
|
6540
|
+
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";
|
|
6541
|
+
async function detectWindows(primary) {
|
|
6542
|
+
const { stdout } = await exec2("powershell", ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_ADAPTER_SCRIPT], {
|
|
6543
|
+
timeout: 8e3
|
|
6544
|
+
});
|
|
6545
|
+
const adapters = parseWindowsAdapterJson(stdout);
|
|
6546
|
+
const match = adapters.find((a2) => normalizeMac(a2.macAddress) === normalizeMac(primary.mac)) ?? adapters[0];
|
|
6547
|
+
if (!match) throw new Error("no active physical adapter reported by Get-NetAdapter");
|
|
6548
|
+
return { networkType: match.networkType, wolWorks: match.wakeOnMagicPacket === "Enabled" };
|
|
6549
|
+
}
|
|
6550
|
+
async function detectMac(primary) {
|
|
6551
|
+
const { stdout: hardwarePorts } = await exec2("networksetup", ["-listallhardwareports"], { timeout: 8e3 });
|
|
6552
|
+
const networkType = classifyMacHardwarePort(hardwarePorts, primary.name);
|
|
6553
|
+
const { stdout: pmsetOut } = await exec2("pmset", ["-g"], { timeout: 8e3 });
|
|
6554
|
+
return { networkType, wolWorks: parseMacPmset(pmsetOut) };
|
|
6555
|
+
}
|
|
6556
|
+
async function detectLinux(primary) {
|
|
6557
|
+
const networkType = existsSync6(`/sys/class/net/${primary.name}/wireless`) ? "wifi" : "ethernet";
|
|
6558
|
+
const { stdout } = await exec2("ethtool", [primary.name], { timeout: 8e3 });
|
|
6559
|
+
const { supportsWakeOnG, wakeOnGEnabled } = parseLinuxEthtool(stdout);
|
|
6560
|
+
return { networkType, wolWorks: supportsWakeOnG && wakeOnGEnabled };
|
|
6561
|
+
}
|
|
6562
|
+
async function getDefaultRouteInterfaceName(plat) {
|
|
6563
|
+
try {
|
|
6564
|
+
if (plat === "win32") {
|
|
6565
|
+
const { stdout } = await exec2(
|
|
6566
|
+
"powershell",
|
|
6567
|
+
[
|
|
6568
|
+
"-NoProfile",
|
|
6569
|
+
"-NonInteractive",
|
|
6570
|
+
"-Command",
|
|
6571
|
+
"Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue | Sort-Object RouteMetric | Select-Object -First 1 -ExpandProperty InterfaceAlias"
|
|
6572
|
+
],
|
|
6573
|
+
{ timeout: 8e3 }
|
|
6574
|
+
);
|
|
6575
|
+
return stdout.trim() || null;
|
|
6576
|
+
}
|
|
6577
|
+
if (plat === "darwin") {
|
|
6578
|
+
const { stdout } = await exec2("route", ["-n", "get", "default"], { timeout: 8e3 });
|
|
6579
|
+
const m3 = stdout.match(/interface:\s*(\S+)/);
|
|
6580
|
+
return m3 ? m3[1] : null;
|
|
6581
|
+
}
|
|
6582
|
+
if (plat === "linux") {
|
|
6583
|
+
const { stdout } = await exec2("ip", ["route", "show", "default"], { timeout: 8e3 });
|
|
6584
|
+
const m3 = stdout.match(/\bdev\s+(\S+)/);
|
|
6585
|
+
return m3 ? m3[1] : null;
|
|
6586
|
+
}
|
|
6587
|
+
return null;
|
|
6588
|
+
} catch {
|
|
6589
|
+
return null;
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
6592
|
+
async function probeWolCapability() {
|
|
6593
|
+
const checkedAtMs = Date.now();
|
|
6594
|
+
const plat = platform();
|
|
6595
|
+
const ifaces = networkInterfaces();
|
|
6596
|
+
const routeIfaceName = await getDefaultRouteInterfaceName(plat);
|
|
6597
|
+
const primary = routeIfaceName && pickInterfaceByName(ifaces, routeIfaceName) || pickPrimaryInterface(ifaces);
|
|
6598
|
+
if (!primary) {
|
|
6599
|
+
return {
|
|
6600
|
+
capability: "unsupported",
|
|
6601
|
+
reason: "Couldn't find an active network interface to check.",
|
|
6602
|
+
networkType: "unknown",
|
|
6603
|
+
mac: null,
|
|
6604
|
+
broadcast: null,
|
|
6605
|
+
checkedAtMs
|
|
6606
|
+
};
|
|
6607
|
+
}
|
|
6608
|
+
const broadcast = computeBroadcastAddress(primary.address, primary.netmask);
|
|
6609
|
+
try {
|
|
6610
|
+
const detected = plat === "win32" ? await detectWindows(primary) : plat === "darwin" ? await detectMac(primary) : plat === "linux" ? await detectLinux(primary) : (() => {
|
|
6611
|
+
throw new Error(`unsupported OS: ${plat}`);
|
|
6612
|
+
})();
|
|
6613
|
+
const { capability, reason } = computeWolCapability(detected.networkType, detected.wolWorks);
|
|
6614
|
+
return { capability, reason, networkType: detected.networkType, mac: primary.mac, broadcast, checkedAtMs };
|
|
6615
|
+
} catch (e) {
|
|
6616
|
+
return {
|
|
6617
|
+
capability: "unsupported",
|
|
6618
|
+
reason: `Couldn't check Wake-on-LAN support: ${e instanceof Error ? e.message : String(e)}`,
|
|
6619
|
+
networkType: "unknown",
|
|
6620
|
+
mac: primary.mac,
|
|
6621
|
+
broadcast,
|
|
6622
|
+
checkedAtMs
|
|
6623
|
+
};
|
|
6624
|
+
}
|
|
6625
|
+
}
|
|
6626
|
+
|
|
6627
|
+
// ../daemon/src/intelligence/ripgrep.ts
|
|
6628
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
6629
|
+
import { promisify as promisify3 } from "node:util";
|
|
6630
|
+
var exec3 = promisify3(execFile3);
|
|
6631
|
+
var cachedAvailable = null;
|
|
6632
|
+
async function detectRipgrep() {
|
|
6633
|
+
if (cachedAvailable !== null) return cachedAvailable;
|
|
6634
|
+
try {
|
|
6635
|
+
await exec3("rg", ["--version"], { timeout: 5e3 });
|
|
6636
|
+
cachedAvailable = true;
|
|
6637
|
+
} catch {
|
|
6638
|
+
cachedAvailable = false;
|
|
6639
|
+
}
|
|
6640
|
+
return cachedAvailable;
|
|
6641
|
+
}
|
|
6642
|
+
async function searchTerms(workspace, terms, opts = {}) {
|
|
6643
|
+
if (terms.length === 0) return [];
|
|
6644
|
+
const timeoutMs = opts.timeoutMs ?? 3e3;
|
|
6645
|
+
const maxMatches = opts.maxMatches ?? 40;
|
|
6646
|
+
const args2 = [
|
|
6647
|
+
"--fixed-strings",
|
|
6648
|
+
"--ignore-case",
|
|
6649
|
+
"--line-number",
|
|
6650
|
+
"--no-heading",
|
|
6651
|
+
"--max-count",
|
|
6652
|
+
"3",
|
|
6653
|
+
"--max-filesize",
|
|
6654
|
+
"1M",
|
|
6655
|
+
"--glob",
|
|
6656
|
+
"!.git",
|
|
6657
|
+
"--glob",
|
|
6658
|
+
"!node_modules",
|
|
6659
|
+
...terms.flatMap((t2) => ["-e", t2]),
|
|
6660
|
+
"."
|
|
6661
|
+
];
|
|
6662
|
+
try {
|
|
6663
|
+
const { stdout } = await exec3("rg", args2, { cwd: workspace, timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 });
|
|
6664
|
+
return parseRipgrepOutput(stdout).slice(0, maxMatches);
|
|
6665
|
+
} catch (e) {
|
|
6666
|
+
const err = e;
|
|
6667
|
+
return err.stdout ? parseRipgrepOutput(err.stdout).slice(0, maxMatches) : [];
|
|
6668
|
+
}
|
|
6669
|
+
}
|
|
6670
|
+
function parseRipgrepOutput(stdout) {
|
|
6671
|
+
const hits = [];
|
|
6672
|
+
for (const line of stdout.split("\n")) {
|
|
6673
|
+
if (!line) continue;
|
|
6674
|
+
const m3 = /^(.+?):(\d+):(.*)$/.exec(line);
|
|
6675
|
+
if (!m3) continue;
|
|
6676
|
+
hits.push({ file: m3[1], line: Number(m3[2]), text: m3[3].trim().slice(0, 200) });
|
|
6677
|
+
}
|
|
6678
|
+
return hits;
|
|
6679
|
+
}
|
|
6680
|
+
|
|
6681
|
+
// ../daemon/src/intelligence/context-inject.ts
|
|
6682
|
+
var DEFAULT_MAX_CHARS = 8e3;
|
|
6683
|
+
var OVERALL_TIMEOUT_MS = 4e3;
|
|
6684
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
6685
|
+
"the",
|
|
6686
|
+
"a",
|
|
6687
|
+
"an",
|
|
6688
|
+
"to",
|
|
6689
|
+
"for",
|
|
6690
|
+
"in",
|
|
6691
|
+
"on",
|
|
6692
|
+
"of",
|
|
6693
|
+
"and",
|
|
6694
|
+
"or",
|
|
6695
|
+
"is",
|
|
6696
|
+
"are",
|
|
6697
|
+
"be",
|
|
6698
|
+
"been",
|
|
6699
|
+
"add",
|
|
6700
|
+
"please",
|
|
6701
|
+
"can",
|
|
6702
|
+
"you",
|
|
6703
|
+
"we",
|
|
6704
|
+
"this",
|
|
6705
|
+
"that",
|
|
6706
|
+
"it",
|
|
6707
|
+
"with",
|
|
6708
|
+
"from",
|
|
6709
|
+
"when",
|
|
6710
|
+
"where",
|
|
6711
|
+
"how",
|
|
6712
|
+
"what",
|
|
6713
|
+
"i",
|
|
6714
|
+
"my",
|
|
6715
|
+
"our",
|
|
6716
|
+
"some",
|
|
6717
|
+
"make",
|
|
6718
|
+
"sure",
|
|
6719
|
+
"need",
|
|
6720
|
+
"needs",
|
|
6721
|
+
"want",
|
|
6722
|
+
"wants",
|
|
6723
|
+
"like",
|
|
6724
|
+
"using",
|
|
6725
|
+
"use",
|
|
6726
|
+
"so",
|
|
6727
|
+
"up",
|
|
6728
|
+
"as",
|
|
6729
|
+
"at",
|
|
6730
|
+
"by",
|
|
6731
|
+
"not",
|
|
6732
|
+
"do",
|
|
6733
|
+
"does",
|
|
6734
|
+
"did",
|
|
6735
|
+
"have",
|
|
6736
|
+
"has",
|
|
6737
|
+
"had",
|
|
6738
|
+
"will",
|
|
6739
|
+
"would",
|
|
6740
|
+
"should",
|
|
6741
|
+
"could",
|
|
6742
|
+
"also",
|
|
6743
|
+
"into",
|
|
6744
|
+
"if",
|
|
6745
|
+
"then",
|
|
6746
|
+
"than"
|
|
6747
|
+
]);
|
|
6748
|
+
function extractTerms(prompt, maxTerms = 8) {
|
|
6749
|
+
const raw = prompt.toLowerCase().split(/[^a-z0-9._-]+/).map((t2) => t2.replace(/^[._-]+|[._-]+$/g, "")).filter((t2) => t2.length >= 3 && !STOPWORDS.has(t2) && !/^\d+$/.test(t2));
|
|
6750
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6751
|
+
const deduped = [];
|
|
6752
|
+
for (const t2 of raw) {
|
|
6753
|
+
if (seen.has(t2)) continue;
|
|
6754
|
+
seen.add(t2);
|
|
6755
|
+
deduped.push(t2);
|
|
6756
|
+
}
|
|
6757
|
+
return deduped.slice().sort((a2, b2) => b2.length - a2.length).slice(0, maxTerms);
|
|
6758
|
+
}
|
|
6759
|
+
async function buildContext(workspace, prompt, opts = {}) {
|
|
6760
|
+
const startedAt = Date.now();
|
|
6761
|
+
const maxChars = opts.maxChars ?? (Number(process.env.OFFHAND_INTEL_MAX_CHARS) || DEFAULT_MAX_CHARS);
|
|
6762
|
+
const work = gatherFindings(workspace, prompt);
|
|
6763
|
+
const timeout = new Promise((resolve6) => setTimeout(() => resolve6("timeout"), OVERALL_TIMEOUT_MS));
|
|
6764
|
+
const result = await Promise.race([work, timeout]);
|
|
6765
|
+
const durationMs = Date.now() - startedAt;
|
|
6766
|
+
if (result === "timeout") {
|
|
6767
|
+
return {
|
|
6768
|
+
block: "",
|
|
6769
|
+
meta: {
|
|
6770
|
+
promptChars: prompt.length,
|
|
6771
|
+
contextChars: 0,
|
|
6772
|
+
filesConsidered: 0,
|
|
6773
|
+
filesSelected: 0,
|
|
6774
|
+
retrievalOpsRun: 0,
|
|
6775
|
+
durationMs,
|
|
6776
|
+
ripgrepAvailable: false,
|
|
6777
|
+
timedOut: true
|
|
6778
|
+
}
|
|
6779
|
+
};
|
|
6780
|
+
}
|
|
6781
|
+
const { findings, retrievalOpsRun, ripgrepAvailable, gitState } = result;
|
|
6782
|
+
if (findings.length === 0) return null;
|
|
6783
|
+
findings.sort((a2, b2) => b2.priority - a2.priority);
|
|
6784
|
+
const { text: block, selectedCount } = formatBlock(prompt, findings, gitState, maxChars);
|
|
6785
|
+
if (selectedCount === 0) return null;
|
|
6786
|
+
return {
|
|
6787
|
+
block,
|
|
6788
|
+
meta: {
|
|
6789
|
+
promptChars: prompt.length,
|
|
6790
|
+
contextChars: block.length,
|
|
6791
|
+
filesConsidered: findings.length,
|
|
6792
|
+
filesSelected: selectedCount,
|
|
6793
|
+
retrievalOpsRun,
|
|
6794
|
+
durationMs: Date.now() - startedAt,
|
|
6795
|
+
ripgrepAvailable,
|
|
6796
|
+
timedOut: false
|
|
6797
|
+
}
|
|
6798
|
+
};
|
|
6799
|
+
}
|
|
6800
|
+
async function gatherFindings(workspace, prompt) {
|
|
6801
|
+
let retrievalOpsRun = 0;
|
|
6802
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
6803
|
+
const add = (path, reason, priority) => {
|
|
6804
|
+
const existing = byPath.get(path);
|
|
6805
|
+
if (existing) {
|
|
6806
|
+
if (!existing.reasons.includes(reason)) existing.reasons.push(reason);
|
|
6807
|
+
existing.priority = Math.max(existing.priority, priority);
|
|
6808
|
+
} else {
|
|
6809
|
+
byPath.set(path, { path, reasons: [reason], priority });
|
|
6810
|
+
}
|
|
6811
|
+
};
|
|
6812
|
+
const statusOut = await git(workspace, "status", "--porcelain");
|
|
6813
|
+
retrievalOpsRun++;
|
|
6814
|
+
let gitState = null;
|
|
6815
|
+
if (statusOut !== null) {
|
|
6816
|
+
const dirtyFiles = statusOut.split("\n").filter((l2) => l2.length > 3).map((l2) => l2.slice(3).trim()).filter(Boolean);
|
|
6817
|
+
for (const f2 of dirtyFiles.slice(0, 15)) add(f2, "currently dirty", 3);
|
|
6818
|
+
gitState = dirtyFiles.length > 0 ? `${dirtyFiles.length} file(s) with uncommitted changes` : "clean working tree";
|
|
6819
|
+
}
|
|
6820
|
+
const ripgrepAvailable = await detectRipgrep();
|
|
6821
|
+
retrievalOpsRun++;
|
|
6822
|
+
const terms = extractTerms(prompt);
|
|
6823
|
+
if (ripgrepAvailable && terms.length > 0) {
|
|
6824
|
+
const hits = await searchTerms(workspace, terms);
|
|
6825
|
+
retrievalOpsRun++;
|
|
6826
|
+
for (const hit of hits) {
|
|
6827
|
+
const term = terms.find((t2) => hit.text.toLowerCase().includes(t2));
|
|
6828
|
+
add(hit.file, term ? `matched "${term}"` : "matched search terms", 2);
|
|
6829
|
+
}
|
|
6830
|
+
}
|
|
6831
|
+
const logOut = await git(workspace, "log", "-n", "15", "--name-only", "--pretty=format:");
|
|
6832
|
+
retrievalOpsRun++;
|
|
6833
|
+
if (logOut !== null) {
|
|
6834
|
+
const recentFiles = [...new Set(logOut.split("\n").map((l2) => l2.trim()).filter(Boolean))];
|
|
6835
|
+
for (const f2 of recentFiles.slice(0, 10)) add(f2, "recently changed", 1);
|
|
6836
|
+
}
|
|
6837
|
+
return { findings: [...byPath.values()], retrievalOpsRun, ripgrepAvailable, gitState };
|
|
6838
|
+
}
|
|
6839
|
+
function formatBlock(prompt, rankedFindings, gitState, maxChars) {
|
|
6840
|
+
const header = `TASK
|
|
6841
|
+
${prompt}
|
|
6842
|
+
|
|
6843
|
+
OFFHAND REPOSITORY CONTEXT
|
|
6844
|
+
`;
|
|
6845
|
+
const gitLine = gitState ? `- Current git state: ${gitState}
|
|
6846
|
+
` : "";
|
|
6847
|
+
const footer = "\nIMPORTANT: This context was generated from local repository analysis and may be incomplete or wrong. Verify against the workspace and search further wherever it looks warranted \u2014 do not treat this as the complete picture.";
|
|
6848
|
+
const budget = maxChars - header.length - gitLine.length - footer.length;
|
|
6849
|
+
const lines = [];
|
|
6850
|
+
let used = 0;
|
|
6851
|
+
let selectedCount = 0;
|
|
6852
|
+
for (const f2 of rankedFindings) {
|
|
6853
|
+
const line = `- ${f2.path} \u2014 ${f2.reasons.join(", ")}
|
|
6854
|
+
`;
|
|
6855
|
+
if (used + line.length > budget) break;
|
|
6856
|
+
lines.push(line);
|
|
6857
|
+
used += line.length;
|
|
6858
|
+
selectedCount++;
|
|
6859
|
+
}
|
|
6860
|
+
const text = header + gitLine + lines.join("") + footer;
|
|
6861
|
+
return { text, selectedCount };
|
|
6862
|
+
}
|
|
6863
|
+
|
|
6237
6864
|
// ../daemon/src/session-manager.ts
|
|
6238
6865
|
function resolveDaemonVersion() {
|
|
6239
6866
|
try {
|
|
6240
|
-
const pkgPath = join6(
|
|
6867
|
+
const pkgPath = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
6241
6868
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
6242
6869
|
return pkg.version ?? "0.0.0";
|
|
6243
6870
|
} catch {
|
|
@@ -6245,6 +6872,10 @@ function resolveDaemonVersion() {
|
|
|
6245
6872
|
}
|
|
6246
6873
|
}
|
|
6247
6874
|
var DAEMON_VERSION = resolveDaemonVersion();
|
|
6875
|
+
var execFileAsync = promisify4(execFile4);
|
|
6876
|
+
var SYSTEM_LOG_SESSION_ID = "system";
|
|
6877
|
+
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.";
|
|
6878
|
+
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
6879
|
var SessionManager = class {
|
|
6249
6880
|
constructor(store2, runners2) {
|
|
6250
6881
|
this.store = store2;
|
|
@@ -6257,8 +6888,19 @@ var SessionManager = class {
|
|
|
6257
6888
|
// sessionId → workspace
|
|
6258
6889
|
queues = /* @__PURE__ */ new Map();
|
|
6259
6890
|
// sessionId → queued prompts
|
|
6891
|
+
/** reviewId → an Offhand Intelligence proposal (Execution Plan 8) awaiting
|
|
6892
|
+
* `intelligence-response` — not tied to any run, since none exists yet. */
|
|
6893
|
+
pendingIntelligence = /* @__PURE__ */ new Map();
|
|
6260
6894
|
runners = /* @__PURE__ */ new Map();
|
|
6261
6895
|
runnerAvailability = /* @__PURE__ */ new Map();
|
|
6896
|
+
/** handoverId → what was proposed, so `handover-response` (which only
|
|
6897
|
+
* carries the id back) knows what to act on. */
|
|
6898
|
+
pendingHandovers = /* @__PURE__ */ new Map();
|
|
6899
|
+
/** sessionId → the most recent offhand_status_update note (Execution Plan
|
|
6900
|
+
* 5) — becomes a Handover Packet's optional agentSummary when a handover
|
|
6901
|
+
* is proposed afterward. Still zero hosted AI: the agent chose to send
|
|
6902
|
+
* this, on its own tokens, no different from any other tool call. */
|
|
6903
|
+
latestStatusUpdate = /* @__PURE__ */ new Map();
|
|
6262
6904
|
/** Optional artifact plumbing (set by index when capture is possible). */
|
|
6263
6905
|
uploader = null;
|
|
6264
6906
|
capture = null;
|
|
@@ -6276,6 +6918,21 @@ var SessionManager = class {
|
|
|
6276
6918
|
for (const [id, r2] of this.runners) {
|
|
6277
6919
|
this.runnerAvailability.set(id, await r2.detect());
|
|
6278
6920
|
}
|
|
6921
|
+
void this.recheckWolCapability();
|
|
6922
|
+
}
|
|
6923
|
+
/** Runs the real Wake-on-LAN/WiFi probe and persists the result — called
|
|
6924
|
+
* once at startup and again on-demand via the phone's "check again"
|
|
6925
|
+
* action (Execution Plan 7), since drivers/BIOS/network type can change. */
|
|
6926
|
+
async recheckWolCapability() {
|
|
6927
|
+
const result = await probeWolCapability();
|
|
6928
|
+
this.store.setWolCapability({
|
|
6929
|
+
capability: result.capability,
|
|
6930
|
+
reason: result.reason,
|
|
6931
|
+
networkType: result.networkType,
|
|
6932
|
+
mac: result.mac,
|
|
6933
|
+
broadcast: result.broadcast,
|
|
6934
|
+
checkedAtMs: result.checkedAtMs
|
|
6935
|
+
});
|
|
6279
6936
|
}
|
|
6280
6937
|
// ---- transport attachment --------------------------------------------------
|
|
6281
6938
|
attach(sink) {
|
|
@@ -6283,10 +6940,16 @@ var SessionManager = class {
|
|
|
6283
6940
|
return () => this.sinks.delete(sink);
|
|
6284
6941
|
}
|
|
6285
6942
|
hello() {
|
|
6943
|
+
const wol = this.store.getWolCapability();
|
|
6286
6944
|
return {
|
|
6287
6945
|
type: "hello",
|
|
6288
6946
|
protocol: 2,
|
|
6289
|
-
host: {
|
|
6947
|
+
host: {
|
|
6948
|
+
hostname: hostname(),
|
|
6949
|
+
os: platform2(),
|
|
6950
|
+
daemonVersion: DAEMON_VERSION,
|
|
6951
|
+
...wol ? { wolCapability: wol.capability, wolReason: wol.reason, wolCheckedAtMs: wol.checkedAtMs } : {}
|
|
6952
|
+
},
|
|
6290
6953
|
lastSeq: this.store.lastSeq()
|
|
6291
6954
|
};
|
|
6292
6955
|
}
|
|
@@ -6297,7 +6960,10 @@ var SessionManager = class {
|
|
|
6297
6960
|
available: this.runnerAvailability.get(r2.id) ?? false,
|
|
6298
6961
|
...r2.loggedIn ? { loggedIn: r2.loggedIn() } : {},
|
|
6299
6962
|
models: r2.models,
|
|
6300
|
-
supportsApprovals: r2.supportsApprovals
|
|
6963
|
+
supportsApprovals: r2.supportsApprovals,
|
|
6964
|
+
...r2.effortTiers ? { effortTiers: [...r2.effortTiers] } : {},
|
|
6965
|
+
...r2.installCommand ? { installCommand: `${r2.installCommand.command} ${r2.installCommand.args.join(" ")}` } : {},
|
|
6966
|
+
...r2.installUrl ? { installUrl: r2.installUrl } : {}
|
|
6301
6967
|
}));
|
|
6302
6968
|
const workspaces = await Promise.all(
|
|
6303
6969
|
this.store.listWorkspaces().map((w2) => workspaceInfo(w2))
|
|
@@ -6329,28 +6995,86 @@ var SessionManager = class {
|
|
|
6329
6995
|
reply({ type: "error", message: `unknown session ${msg.sessionId}` });
|
|
6330
6996
|
return;
|
|
6331
6997
|
}
|
|
6332
|
-
let prompt
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6998
|
+
let prompt;
|
|
6999
|
+
try {
|
|
7000
|
+
prompt = await this.stageAttachments(msg.prompt, msg.attachments);
|
|
7001
|
+
} catch (e) {
|
|
7002
|
+
reply({ type: "error", message: `attachment staging failed: ${String(e)}` });
|
|
7003
|
+
return;
|
|
7004
|
+
}
|
|
7005
|
+
this.enqueuePrompt(session, prompt);
|
|
7006
|
+
await this.broadcastManifest();
|
|
7007
|
+
return;
|
|
7008
|
+
}
|
|
7009
|
+
case "intelligence-set": {
|
|
7010
|
+
this.store.setWorkspaceIntelligenceMode(msg.workspace, msg.mode);
|
|
7011
|
+
await this.broadcastManifest();
|
|
7012
|
+
return;
|
|
7013
|
+
}
|
|
7014
|
+
case "intelligence-prepare": {
|
|
7015
|
+
const session = this.store.getSession(msg.sessionId);
|
|
7016
|
+
if (!session) {
|
|
7017
|
+
reply({ type: "error", message: `unknown session ${msg.sessionId}` });
|
|
7018
|
+
return;
|
|
7019
|
+
}
|
|
7020
|
+
let prompt;
|
|
7021
|
+
try {
|
|
7022
|
+
prompt = await this.stageAttachments(msg.prompt, msg.attachments);
|
|
7023
|
+
} catch (e) {
|
|
7024
|
+
reply({ type: "error", message: `attachment staging failed: ${String(e)}` });
|
|
7025
|
+
return;
|
|
7026
|
+
}
|
|
7027
|
+
const built = await buildContext(session.workspace, prompt);
|
|
7028
|
+
this.record(session.id, (seq) => ({
|
|
7029
|
+
type: "run-event",
|
|
7030
|
+
sessionId: session.id,
|
|
7031
|
+
runId: "",
|
|
7032
|
+
seq,
|
|
7033
|
+
event: {
|
|
7034
|
+
type: "intelligence-built",
|
|
7035
|
+
promptChars: prompt.length,
|
|
7036
|
+
contextChars: built?.meta.contextChars ?? 0,
|
|
7037
|
+
filesConsidered: built?.meta.filesConsidered ?? 0,
|
|
7038
|
+
filesSelected: built?.meta.filesSelected ?? 0,
|
|
7039
|
+
retrievalOpsRun: built?.meta.retrievalOpsRun ?? 0,
|
|
7040
|
+
durationMs: built?.meta.durationMs ?? 0,
|
|
7041
|
+
ripgrepAvailable: built?.meta.ripgrepAvailable ?? false,
|
|
7042
|
+
timedOut: built?.meta.timedOut ?? false,
|
|
7043
|
+
attached: built !== null && built.block !== ""
|
|
6348
7044
|
}
|
|
7045
|
+
}));
|
|
7046
|
+
if (!built || built.block === "") {
|
|
7047
|
+
this.enqueuePrompt(session, prompt);
|
|
7048
|
+
await this.broadcastManifest();
|
|
7049
|
+
return;
|
|
6349
7050
|
}
|
|
6350
|
-
const
|
|
6351
|
-
|
|
6352
|
-
this.
|
|
6353
|
-
|
|
7051
|
+
const reviewId = randomUUID();
|
|
7052
|
+
this.pendingIntelligence.set(reviewId, { sessionId: session.id, prompt, block: built.block });
|
|
7053
|
+
this.record(session.id, (seq) => ({
|
|
7054
|
+
type: "intelligence-review",
|
|
7055
|
+
sessionId: session.id,
|
|
7056
|
+
seq,
|
|
7057
|
+
reviewId,
|
|
7058
|
+
originalPrompt: prompt,
|
|
7059
|
+
block: built.block,
|
|
7060
|
+
filesSelected: built.meta.filesSelected
|
|
7061
|
+
}));
|
|
7062
|
+
return;
|
|
7063
|
+
}
|
|
7064
|
+
case "intelligence-response": {
|
|
7065
|
+
const pending = this.pendingIntelligence.get(msg.reviewId);
|
|
7066
|
+
if (!pending || pending.sessionId !== msg.sessionId) return;
|
|
7067
|
+
this.pendingIntelligence.delete(msg.reviewId);
|
|
7068
|
+
const session = this.store.getSession(msg.sessionId);
|
|
7069
|
+
if (!session) return;
|
|
7070
|
+
this.record(session.id, (seq) => ({
|
|
7071
|
+
type: "intelligence-resolved",
|
|
7072
|
+
sessionId: session.id,
|
|
7073
|
+
seq,
|
|
7074
|
+
reviewId: msg.reviewId,
|
|
7075
|
+
useContext: msg.useContext
|
|
7076
|
+
}));
|
|
7077
|
+
this.enqueuePrompt(session, pending.prompt, msg.useContext ? pending.block : void 0);
|
|
6354
7078
|
await this.broadcastManifest();
|
|
6355
7079
|
return;
|
|
6356
7080
|
}
|
|
@@ -6392,6 +7116,38 @@ var SessionManager = class {
|
|
|
6392
7116
|
case "resume":
|
|
6393
7117
|
for (const m3 of this.store.replayAfter(msg.afterSeq)) reply(m3);
|
|
6394
7118
|
return;
|
|
7119
|
+
case "handover-response": {
|
|
7120
|
+
const pending = this.pendingHandovers.get(msg.handoverId);
|
|
7121
|
+
if (!pending || pending.sessionId !== msg.sessionId) return;
|
|
7122
|
+
this.pendingHandovers.delete(msg.handoverId);
|
|
7123
|
+
if (!msg.accept) {
|
|
7124
|
+
this.record(msg.sessionId, (seq) => ({
|
|
7125
|
+
type: "run-event",
|
|
7126
|
+
sessionId: msg.sessionId,
|
|
7127
|
+
runId: "",
|
|
7128
|
+
seq,
|
|
7129
|
+
event: { type: "handover-declined", id: msg.handoverId }
|
|
7130
|
+
}));
|
|
7131
|
+
return;
|
|
7132
|
+
}
|
|
7133
|
+
const session = this.store.getSession(msg.sessionId);
|
|
7134
|
+
const toRunner = this.runners.get(pending.toRunnerId);
|
|
7135
|
+
if (!session || !toRunner) return;
|
|
7136
|
+
const fromRunnerId = session.runnerId;
|
|
7137
|
+
const fromRunner = this.runners.get(fromRunnerId);
|
|
7138
|
+
const prompt = translatePacketToPrompt(pending.packet, fromRunner?.name ?? fromRunnerId);
|
|
7139
|
+
this.store.updateSession(session.id, { runnerId: pending.toRunnerId, conversationId: null });
|
|
7140
|
+
this.record(session.id, (seq) => ({
|
|
7141
|
+
type: "run-event",
|
|
7142
|
+
sessionId: session.id,
|
|
7143
|
+
runId: "",
|
|
7144
|
+
seq,
|
|
7145
|
+
event: { type: "handover-accepted", id: msg.handoverId, fromRunnerId, toRunnerId: pending.toRunnerId }
|
|
7146
|
+
}));
|
|
7147
|
+
this.enqueuePrompt(session, prompt);
|
|
7148
|
+
await this.broadcastManifest();
|
|
7149
|
+
return;
|
|
7150
|
+
}
|
|
6395
7151
|
case "session-create": {
|
|
6396
7152
|
const row = this.store.createSession(msg.workspace, msg.runnerId, msg.model, msg.label);
|
|
6397
7153
|
this.store.upsertWorkspace(msg.workspace);
|
|
@@ -6415,7 +7171,9 @@ var SessionManager = class {
|
|
|
6415
7171
|
...msg.model !== void 0 ? { model: msg.model || null } : {},
|
|
6416
7172
|
...msg.archived !== void 0 ? { archived: msg.archived } : {},
|
|
6417
7173
|
...msg.permissionMode !== void 0 ? { permissionMode: msg.permissionMode } : {},
|
|
6418
|
-
...msg.effort !== void 0 ? { effort: msg.effort } : {}
|
|
7174
|
+
...msg.effort !== void 0 ? { effort: msg.effort } : {},
|
|
7175
|
+
...msg.budgetCostUsdCap !== void 0 ? { budgetCostUsdCap: msg.budgetCostUsdCap } : {},
|
|
7176
|
+
...msg.budgetMinutesCap !== void 0 ? { budgetMinutesCap: msg.budgetMinutesCap } : {}
|
|
6419
7177
|
});
|
|
6420
7178
|
await this.broadcastManifest();
|
|
6421
7179
|
return;
|
|
@@ -6448,8 +7206,117 @@ var SessionManager = class {
|
|
|
6448
7206
|
case "fs-list":
|
|
6449
7207
|
reply({ type: "fs-response", rpcId: msg.rpcId, ...listFolders(msg.path) });
|
|
6450
7208
|
return;
|
|
7209
|
+
case "fs-create-folder": {
|
|
7210
|
+
try {
|
|
7211
|
+
const safeName = safeDropName(msg.name);
|
|
7212
|
+
mkdirSync2(join6(resolve3(msg.path), safeName), { recursive: false });
|
|
7213
|
+
} catch (e) {
|
|
7214
|
+
reply({
|
|
7215
|
+
type: "fs-response",
|
|
7216
|
+
rpcId: msg.rpcId,
|
|
7217
|
+
...listFolders(msg.path),
|
|
7218
|
+
error: `couldn't create folder: ${e instanceof Error ? e.message : String(e)}`
|
|
7219
|
+
});
|
|
7220
|
+
return;
|
|
7221
|
+
}
|
|
7222
|
+
reply({ type: "fs-response", rpcId: msg.rpcId, ...listFolders(msg.path) });
|
|
7223
|
+
return;
|
|
7224
|
+
}
|
|
7225
|
+
case "wol-recheck":
|
|
7226
|
+
await this.recheckWolCapability();
|
|
7227
|
+
reply(this.hello());
|
|
7228
|
+
return;
|
|
7229
|
+
case "runner-install": {
|
|
7230
|
+
const runner = this.runners.get(msg.runnerId);
|
|
7231
|
+
if (!runner?.installCommand) {
|
|
7232
|
+
reply({
|
|
7233
|
+
type: "runner-install-response",
|
|
7234
|
+
rpcId: msg.rpcId,
|
|
7235
|
+
ok: false,
|
|
7236
|
+
message: `No one-tap install available for "${msg.runnerId}".`
|
|
7237
|
+
});
|
|
7238
|
+
return;
|
|
7239
|
+
}
|
|
7240
|
+
try {
|
|
7241
|
+
const { stderr } = await execFileAsync(runner.installCommand.command, runner.installCommand.args, {
|
|
7242
|
+
timeout: 18e4
|
|
7243
|
+
});
|
|
7244
|
+
this.runnerAvailability.set(runner.id, await runner.detect());
|
|
7245
|
+
reply({
|
|
7246
|
+
type: "runner-install-response",
|
|
7247
|
+
rpcId: msg.rpcId,
|
|
7248
|
+
ok: true,
|
|
7249
|
+
message: stderr.trim() || "Installed."
|
|
7250
|
+
});
|
|
7251
|
+
await this.broadcastManifest();
|
|
7252
|
+
} catch (e) {
|
|
7253
|
+
const err = e;
|
|
7254
|
+
const detail = (err.stderr?.trim() || err.message || String(e)).slice(0, 500);
|
|
7255
|
+
reply({ type: "runner-install-response", rpcId: msg.rpcId, ok: false, message: detail });
|
|
7256
|
+
}
|
|
7257
|
+
return;
|
|
7258
|
+
}
|
|
6451
7259
|
}
|
|
6452
7260
|
}
|
|
7261
|
+
/** The daemon's own relay connection dropped — logged durably so a phone
|
|
7262
|
+
* that wasn't listening live can still see it in the "left off" digest
|
|
7263
|
+
* (Execution Plan 6), unlike the relay's ephemeral `presence` frame. */
|
|
7264
|
+
recordDeviceOffline() {
|
|
7265
|
+
this.record(SYSTEM_LOG_SESSION_ID, (seq) => ({ type: "device-offline", seq, atMs: Date.now() }));
|
|
7266
|
+
}
|
|
7267
|
+
/** A running agent proactively pushed a status note (Execution Plan 5's
|
|
7268
|
+
* offhand_status_update MCP tool). Recorded into the normal session log
|
|
7269
|
+
* (so it shows in the transcript and the run timeline) and cached as the
|
|
7270
|
+
* latest note for this session, ready to enrich a Handover Packet later. */
|
|
7271
|
+
recordStatusUpdate(sessionId, text) {
|
|
7272
|
+
this.latestStatusUpdate.set(sessionId, text);
|
|
7273
|
+
this.record(sessionId, (seq) => ({
|
|
7274
|
+
type: "run-event",
|
|
7275
|
+
sessionId,
|
|
7276
|
+
runId: "",
|
|
7277
|
+
seq,
|
|
7278
|
+
event: { type: "status-update", text }
|
|
7279
|
+
}));
|
|
7280
|
+
}
|
|
7281
|
+
/** Offhand Intelligence Phase 5 (Execution Plan 8) — the agent itself
|
|
7282
|
+
* calls this via the offhand_context MCP tool, mid-run, instead of a
|
|
7283
|
+
* human reviewing on the phone first. Same buildContext() mechanism and
|
|
7284
|
+
* telemetry as the mobile review path (Phase 2); the tool's own response
|
|
7285
|
+
* carries the same "may be incomplete, keep searching" framing so the
|
|
7286
|
+
* agent's discovery is supplemented, never restricted. */
|
|
7287
|
+
async buildContextFor(sessionId, query) {
|
|
7288
|
+
const session = this.store.getSession(sessionId);
|
|
7289
|
+
if (!session) return null;
|
|
7290
|
+
const built = await buildContext(session.workspace, query);
|
|
7291
|
+
this.record(sessionId, (seq) => ({
|
|
7292
|
+
type: "run-event",
|
|
7293
|
+
sessionId,
|
|
7294
|
+
runId: "",
|
|
7295
|
+
seq,
|
|
7296
|
+
event: {
|
|
7297
|
+
type: "intelligence-built",
|
|
7298
|
+
promptChars: query.length,
|
|
7299
|
+
contextChars: built?.meta.contextChars ?? 0,
|
|
7300
|
+
filesConsidered: built?.meta.filesConsidered ?? 0,
|
|
7301
|
+
filesSelected: built?.meta.filesSelected ?? 0,
|
|
7302
|
+
retrievalOpsRun: built?.meta.retrievalOpsRun ?? 0,
|
|
7303
|
+
durationMs: built?.meta.durationMs ?? 0,
|
|
7304
|
+
ripgrepAvailable: built?.meta.ripgrepAvailable ?? false,
|
|
7305
|
+
timedOut: built?.meta.timedOut ?? false,
|
|
7306
|
+
attached: built !== null && built.block !== ""
|
|
7307
|
+
}
|
|
7308
|
+
}));
|
|
7309
|
+
if (!built || built.block === "") return null;
|
|
7310
|
+
return { block: built.block, filesSelected: built.meta.filesSelected };
|
|
7311
|
+
}
|
|
7312
|
+
recordDeviceReconnected(offlineForMs) {
|
|
7313
|
+
this.record(SYSTEM_LOG_SESSION_ID, (seq) => ({
|
|
7314
|
+
type: "device-reconnected",
|
|
7315
|
+
seq,
|
|
7316
|
+
atMs: Date.now(),
|
|
7317
|
+
offlineForMs
|
|
7318
|
+
}));
|
|
7319
|
+
}
|
|
6453
7320
|
async sendDropToPhone(path) {
|
|
6454
7321
|
if (!this.uploader) throw new Error("drop upload unavailable: start daemon with --relay");
|
|
6455
7322
|
const file = readOutgoingDrop(path);
|
|
@@ -6469,13 +7336,40 @@ var SessionManager = class {
|
|
|
6469
7336
|
pump(sessionId) {
|
|
6470
7337
|
if (this.active.has(sessionId)) return;
|
|
6471
7338
|
const q2 = this.queues.get(sessionId);
|
|
6472
|
-
const
|
|
6473
|
-
if (!
|
|
7339
|
+
const item = q2?.shift();
|
|
7340
|
+
if (!item) return;
|
|
6474
7341
|
const session = this.store.getSession(sessionId);
|
|
6475
7342
|
if (!session) return;
|
|
6476
|
-
void this.runOne(session, prompt);
|
|
7343
|
+
void this.runOne(session, item.prompt, item.block);
|
|
7344
|
+
}
|
|
7345
|
+
/** Appends staged-attachment file paths to a prompt — shared by `prompt`
|
|
7346
|
+
* and `intelligence-prepare` so the two paths can't drift. Throws on
|
|
7347
|
+
* fetch/write failure; callers turn that into an `error` reply. */
|
|
7348
|
+
async stageAttachments(prompt, attachments) {
|
|
7349
|
+
if (!attachments?.length || !this.attachmentFetcher) return prompt;
|
|
7350
|
+
let out = prompt + "\n\nAttached files (read them from disk):";
|
|
7351
|
+
for (const a2 of attachments) {
|
|
7352
|
+
const bytes = await this.attachmentFetcher(a2.blobId);
|
|
7353
|
+
const dir = join6(tmpdir(), "offhand-attachments");
|
|
7354
|
+
mkdirSync2(dir, { recursive: true });
|
|
7355
|
+
const path = join6(dir, `${a2.blobId.slice(0, 8)}-${basename3(a2.name)}`);
|
|
7356
|
+
writeFileSync2(path, bytes);
|
|
7357
|
+
out += `
|
|
7358
|
+
- ${path} (${a2.mime})`;
|
|
7359
|
+
}
|
|
7360
|
+
return out;
|
|
7361
|
+
}
|
|
7362
|
+
/** Pushes a prompt onto a session's queue and pumps it — the one place
|
|
7363
|
+
* every prompt (plain, post-review, or a translated handover packet)
|
|
7364
|
+
* enters execution, so none of those callers duplicate queue/pump/
|
|
7365
|
+
* broadcast bookkeeping. */
|
|
7366
|
+
enqueuePrompt(session, prompt, block) {
|
|
7367
|
+
const q2 = this.queues.get(session.id) ?? [];
|
|
7368
|
+
q2.push(block !== void 0 ? { prompt, block } : { prompt });
|
|
7369
|
+
this.queues.set(session.id, q2);
|
|
7370
|
+
this.pump(session.id);
|
|
6477
7371
|
}
|
|
6478
|
-
async runOne(session, prompt) {
|
|
7372
|
+
async runOne(session, prompt, precomputedBlock) {
|
|
6479
7373
|
const runner = this.runners.get(session.runnerId);
|
|
6480
7374
|
if (!runner || !(this.runnerAvailability.get(session.runnerId) ?? false)) {
|
|
6481
7375
|
this.record(session.id, (seq) => ({
|
|
@@ -6492,10 +7386,44 @@ var SessionManager = class {
|
|
|
6492
7386
|
const touchedFiles = [];
|
|
6493
7387
|
let toolCount = 0;
|
|
6494
7388
|
let succeeded = false;
|
|
7389
|
+
let limitHitEmitted = false;
|
|
7390
|
+
let budgetWarned = false;
|
|
7391
|
+
const hasStatusTool = runner.id === "claude-code" && session.permissionMode !== "bypass";
|
|
7392
|
+
const preambled = session.conversationId ? prompt : `${MOBILE_SESSION_PREAMBLE_BASE}${hasStatusTool ? MOBILE_SESSION_PREAMBLE_STATUS_TOOL : ""}
|
|
7393
|
+
|
|
7394
|
+
${prompt}`;
|
|
7395
|
+
let sentPrompt = preambled;
|
|
7396
|
+
if (precomputedBlock !== void 0) {
|
|
7397
|
+
sentPrompt = preambled.replace(prompt, precomputedBlock);
|
|
7398
|
+
} else if (process.env.OFFHAND_INTEL_DEBUG === "1") {
|
|
7399
|
+
const built = await buildContext(session.workspace, prompt);
|
|
7400
|
+
this.record(session.id, (seq) => ({
|
|
7401
|
+
type: "run-event",
|
|
7402
|
+
sessionId: session.id,
|
|
7403
|
+
runId,
|
|
7404
|
+
seq,
|
|
7405
|
+
event: {
|
|
7406
|
+
type: "intelligence-built",
|
|
7407
|
+
promptChars: prompt.length,
|
|
7408
|
+
contextChars: built?.meta.contextChars ?? 0,
|
|
7409
|
+
filesConsidered: built?.meta.filesConsidered ?? 0,
|
|
7410
|
+
filesSelected: built?.meta.filesSelected ?? 0,
|
|
7411
|
+
retrievalOpsRun: built?.meta.retrievalOpsRun ?? 0,
|
|
7412
|
+
durationMs: built?.meta.durationMs ?? 0,
|
|
7413
|
+
ripgrepAvailable: built?.meta.ripgrepAvailable ?? false,
|
|
7414
|
+
timedOut: built?.meta.timedOut ?? false,
|
|
7415
|
+
attached: built !== null && built.block !== ""
|
|
7416
|
+
}
|
|
7417
|
+
}));
|
|
7418
|
+
if (built && built.block !== "") {
|
|
7419
|
+
sentPrompt = preambled.replace(prompt, built.block);
|
|
7420
|
+
}
|
|
7421
|
+
}
|
|
6495
7422
|
const handle = runner.start(
|
|
6496
7423
|
{
|
|
6497
7424
|
runId,
|
|
6498
|
-
|
|
7425
|
+
sessionId: session.id,
|
|
7426
|
+
prompt: sentPrompt,
|
|
6499
7427
|
workspace: session.workspace,
|
|
6500
7428
|
...session.model ? { model: session.model } : {},
|
|
6501
7429
|
...session.conversationId ? { resumeConversationId: session.conversationId } : {},
|
|
@@ -6528,6 +7456,17 @@ var SessionManager = class {
|
|
|
6528
7456
|
seq,
|
|
6529
7457
|
event
|
|
6530
7458
|
}));
|
|
7459
|
+
const limitHit = checkLimitHit(event, limitHitEmitted);
|
|
7460
|
+
if (limitHit) {
|
|
7461
|
+
limitHitEmitted = true;
|
|
7462
|
+
this.record(session.id, (seq) => ({ type: "run-event", sessionId: session.id, runId, seq, event: limitHit }));
|
|
7463
|
+
void this.proposeHandover(session, runId, limitHit.reason);
|
|
7464
|
+
}
|
|
7465
|
+
const budgetHit = checkBudgetWarning(event, session, budgetWarned) ?? checkTimeBudget(session, Date.now() - startedAt, budgetWarned);
|
|
7466
|
+
if (budgetHit) {
|
|
7467
|
+
budgetWarned = true;
|
|
7468
|
+
this.record(session.id, (seq) => ({ type: "run-event", sessionId: session.id, runId, seq, event: budgetHit }));
|
|
7469
|
+
}
|
|
6531
7470
|
}
|
|
6532
7471
|
} finally {
|
|
6533
7472
|
this.active.delete(session.id);
|
|
@@ -6559,7 +7498,65 @@ var SessionManager = class {
|
|
|
6559
7498
|
}));
|
|
6560
7499
|
}
|
|
6561
7500
|
}
|
|
7501
|
+
/** Builds and offers a Handover Packet once a limit-hit fires. Entirely
|
|
7502
|
+
* deterministic (live git diff + this session's own log) — no cooperation
|
|
7503
|
+
* needed from the agent that just hit its limit, which is the point:
|
|
7504
|
+
* this must still work when that agent is already dead or blocked, not
|
|
7505
|
+
* only the graceful case. Silent (no card at all) when there's no other
|
|
7506
|
+
* detected, available runner to hand off to — never a dead-end proposal. */
|
|
7507
|
+
async proposeHandover(session, runId, reason) {
|
|
7508
|
+
const target = this.pickHandoverTarget(session.runnerId);
|
|
7509
|
+
if (!target) return;
|
|
7510
|
+
const diff = await liveDiff(session.workspace);
|
|
7511
|
+
const { items } = this.store.historyPage(session.id, 0, 500);
|
|
7512
|
+
const packet = assembleHandoverPacket(items, diff);
|
|
7513
|
+
const agentSummary = this.latestStatusUpdate.get(session.id);
|
|
7514
|
+
if (agentSummary) packet.agentSummary = agentSummary;
|
|
7515
|
+
const id = randomUUID();
|
|
7516
|
+
this.pendingHandovers.set(id, { sessionId: session.id, toRunnerId: target.id, packet });
|
|
7517
|
+
this.record(session.id, (seq) => ({
|
|
7518
|
+
type: "run-event",
|
|
7519
|
+
sessionId: session.id,
|
|
7520
|
+
runId,
|
|
7521
|
+
seq,
|
|
7522
|
+
event: { type: "handover-proposed", id, fromRunnerId: session.runnerId, toRunnerId: target.id, reason, packet }
|
|
7523
|
+
}));
|
|
7524
|
+
}
|
|
7525
|
+
/** First other detected+available runner — good enough for v1 (single
|
|
7526
|
+
* candidate, not a picker); a real choice among several becomes relevant
|
|
7527
|
+
* once more than one alternative is commonly available at once. */
|
|
7528
|
+
pickHandoverTarget(currentRunnerId) {
|
|
7529
|
+
for (const [id, runner] of this.runners) {
|
|
7530
|
+
if (id === currentRunnerId) continue;
|
|
7531
|
+
if (!(this.runnerAvailability.get(id) ?? false)) continue;
|
|
7532
|
+
return runner;
|
|
7533
|
+
}
|
|
7534
|
+
return null;
|
|
7535
|
+
}
|
|
6562
7536
|
};
|
|
7537
|
+
var LIMIT_HIT_THRESHOLD = 0.9;
|
|
7538
|
+
function checkLimitHit(event, alreadyEmitted) {
|
|
7539
|
+
if (alreadyEmitted || event.type !== "rate-limit") return null;
|
|
7540
|
+
const utilization = Math.max(event.fiveHourUtilization, event.sevenDayUtilization);
|
|
7541
|
+
if (utilization < LIMIT_HIT_THRESHOLD) return null;
|
|
7542
|
+
return { type: "limit-hit", reason: "rate_limit", utilization };
|
|
7543
|
+
}
|
|
7544
|
+
function checkBudgetWarning(event, session, alreadyWarned) {
|
|
7545
|
+
if (alreadyWarned || event.type !== "usage" || !session.budgetCostUsdCap) return null;
|
|
7546
|
+
if (event.costUsd === void 0 || event.costUsd < session.budgetCostUsdCap) return null;
|
|
7547
|
+
return { type: "budget-warning", kind: "cost", capUsd: session.budgetCostUsdCap, actualUsd: event.costUsd };
|
|
7548
|
+
}
|
|
7549
|
+
function checkTimeBudget(session, elapsedMs, alreadyWarned) {
|
|
7550
|
+
if (alreadyWarned || !session.budgetMinutesCap) return null;
|
|
7551
|
+
const minutes = elapsedMs / 6e4;
|
|
7552
|
+
if (minutes < session.budgetMinutesCap) return null;
|
|
7553
|
+
return {
|
|
7554
|
+
type: "budget-warning",
|
|
7555
|
+
kind: "time",
|
|
7556
|
+
capMinutes: session.budgetMinutesCap,
|
|
7557
|
+
actualMinutes: Math.round(minutes)
|
|
7558
|
+
};
|
|
7559
|
+
}
|
|
6563
7560
|
function trackTouches(event, touched) {
|
|
6564
7561
|
if (event.type !== "tool") return;
|
|
6565
7562
|
if (!/^(Edit|Write|MultiEdit|NotebookEdit)/.test(event.name)) return;
|
|
@@ -6576,16 +7573,16 @@ function listFolders(path) {
|
|
|
6576
7573
|
const current = resolve3(path);
|
|
6577
7574
|
const dirs = safeReadDir2(current).filter((entry) => entry.isDirectory() && !skipDir(entry.name)).map((entry) => {
|
|
6578
7575
|
const full = join6(current, entry.name);
|
|
6579
|
-
return { name: entry.name, path: full, isGit:
|
|
7576
|
+
return { name: entry.name, path: full, isGit: existsSync7(join6(full, ".git")) };
|
|
6580
7577
|
}).sort((a2, b2) => Number(b2.isGit) - Number(a2.isGit) || a2.name.localeCompare(b2.name)).slice(0, 200);
|
|
6581
|
-
return { path: current, parent: isRoot(current) ? null :
|
|
7578
|
+
return { path: current, parent: isRoot(current) ? null : dirname3(current), dirs };
|
|
6582
7579
|
}
|
|
6583
7580
|
function driveRoots() {
|
|
6584
|
-
if (process.platform !== "win32") return [{ name: "/", path: "/", isGit:
|
|
7581
|
+
if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync7("/.git") }];
|
|
6585
7582
|
const roots = [];
|
|
6586
7583
|
for (let code = 67; code <= 90; code++) {
|
|
6587
7584
|
const name = `${String.fromCharCode(code)}:\\`;
|
|
6588
|
-
if (
|
|
7585
|
+
if (existsSync7(name)) roots.push({ name, path: name, isGit: existsSync7(join6(name, ".git")) });
|
|
6589
7586
|
}
|
|
6590
7587
|
return roots.sort((a2, b2) => a2.name.localeCompare(b2.name)).slice(0, 200);
|
|
6591
7588
|
}
|
|
@@ -6650,6 +7647,15 @@ var Store = class {
|
|
|
6650
7647
|
dev_url TEXT
|
|
6651
7648
|
);
|
|
6652
7649
|
CREATE VIRTUAL TABLE IF NOT EXISTS log_fts USING fts5(text, session_id UNINDEXED, seq UNINDEXED);
|
|
7650
|
+
CREATE TABLE IF NOT EXISTS device (
|
|
7651
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
7652
|
+
wol_capability TEXT,
|
|
7653
|
+
wol_reason TEXT,
|
|
7654
|
+
wol_network_type TEXT,
|
|
7655
|
+
wol_mac TEXT,
|
|
7656
|
+
wol_broadcast TEXT,
|
|
7657
|
+
wol_checked_at_ms INTEGER
|
|
7658
|
+
);
|
|
6653
7659
|
`);
|
|
6654
7660
|
try {
|
|
6655
7661
|
this.db.exec(`ALTER TABLE workspaces ADD COLUMN policy TEXT NOT NULL DEFAULT 'balanced'`);
|
|
@@ -6663,6 +7669,18 @@ var Store = class {
|
|
|
6663
7669
|
this.db.exec(`ALTER TABLE sessions ADD COLUMN effort TEXT`);
|
|
6664
7670
|
} catch {
|
|
6665
7671
|
}
|
|
7672
|
+
try {
|
|
7673
|
+
this.db.exec(`ALTER TABLE sessions ADD COLUMN budget_cost_usd_cap REAL`);
|
|
7674
|
+
} catch {
|
|
7675
|
+
}
|
|
7676
|
+
try {
|
|
7677
|
+
this.db.exec(`ALTER TABLE sessions ADD COLUMN budget_minutes_cap INTEGER`);
|
|
7678
|
+
} catch {
|
|
7679
|
+
}
|
|
7680
|
+
try {
|
|
7681
|
+
this.db.exec(`ALTER TABLE workspaces ADD COLUMN intelligence_mode TEXT NOT NULL DEFAULT 'off'`);
|
|
7682
|
+
} catch {
|
|
7683
|
+
}
|
|
6666
7684
|
}
|
|
6667
7685
|
// ---- sessions -------------------------------------------------------------
|
|
6668
7686
|
createSession(workspace, runnerId, model, label) {
|
|
@@ -6676,7 +7694,9 @@ var Store = class {
|
|
|
6676
7694
|
archived: false,
|
|
6677
7695
|
conversationId: null,
|
|
6678
7696
|
permissionMode: "guarded",
|
|
6679
|
-
effort: null
|
|
7697
|
+
effort: null,
|
|
7698
|
+
budgetCostUsdCap: null,
|
|
7699
|
+
budgetMinutesCap: null
|
|
6680
7700
|
};
|
|
6681
7701
|
this.db.prepare(
|
|
6682
7702
|
`INSERT INTO sessions (id, workspace, runner_id, model, label, created_at_ms, archived, conversation_id)
|
|
@@ -6696,7 +7716,9 @@ var Store = class {
|
|
|
6696
7716
|
archived: Boolean(r2.archived),
|
|
6697
7717
|
conversationId: r2.conversation_id ?? null,
|
|
6698
7718
|
permissionMode: r2.permission_mode ?? "guarded",
|
|
6699
|
-
effort: r2.effort ?? null
|
|
7719
|
+
effort: r2.effort ?? null,
|
|
7720
|
+
budgetCostUsdCap: r2.budget_cost_usd_cap ?? null,
|
|
7721
|
+
budgetMinutesCap: r2.budget_minutes_cap ?? null
|
|
6700
7722
|
}));
|
|
6701
7723
|
}
|
|
6702
7724
|
getSession(id) {
|
|
@@ -6707,8 +7729,20 @@ var Store = class {
|
|
|
6707
7729
|
if (!current) return;
|
|
6708
7730
|
const next = { ...current, ...patch };
|
|
6709
7731
|
this.db.prepare(
|
|
6710
|
-
`UPDATE sessions SET label = ?, model = ?, archived = ?, conversation_id = ?, permission_mode = ?, effort =
|
|
6711
|
-
|
|
7732
|
+
`UPDATE sessions SET label = ?, model = ?, archived = ?, conversation_id = ?, permission_mode = ?, effort = ?,
|
|
7733
|
+
budget_cost_usd_cap = ?, budget_minutes_cap = ?, runner_id = ? WHERE id = ?`
|
|
7734
|
+
).run(
|
|
7735
|
+
next.label,
|
|
7736
|
+
next.model,
|
|
7737
|
+
next.archived ? 1 : 0,
|
|
7738
|
+
next.conversationId,
|
|
7739
|
+
next.permissionMode,
|
|
7740
|
+
next.effort,
|
|
7741
|
+
next.budgetCostUsdCap,
|
|
7742
|
+
next.budgetMinutesCap,
|
|
7743
|
+
next.runnerId,
|
|
7744
|
+
id
|
|
7745
|
+
);
|
|
6712
7746
|
}
|
|
6713
7747
|
toSessionInfo(row, busy, queued) {
|
|
6714
7748
|
return {
|
|
@@ -6722,7 +7756,9 @@ var Store = class {
|
|
|
6722
7756
|
permissionMode: row.permissionMode,
|
|
6723
7757
|
effort: row.effort ?? void 0,
|
|
6724
7758
|
busy,
|
|
6725
|
-
queuedPrompts: queued
|
|
7759
|
+
queuedPrompts: queued,
|
|
7760
|
+
budgetCostUsdCap: row.budgetCostUsdCap ?? void 0,
|
|
7761
|
+
budgetMinutesCap: row.budgetMinutesCap ?? void 0
|
|
6726
7762
|
};
|
|
6727
7763
|
}
|
|
6728
7764
|
// ---- transcript log ---------------------------------------------------------
|
|
@@ -6787,12 +7823,42 @@ var Store = class {
|
|
|
6787
7823
|
path: r2.path,
|
|
6788
7824
|
label: r2.label,
|
|
6789
7825
|
devUrl: r2.dev_url ?? null,
|
|
6790
|
-
policy: r2.policy ?? "balanced"
|
|
7826
|
+
policy: r2.policy ?? "balanced",
|
|
7827
|
+
intelligenceMode: r2.intelligence_mode ?? "off"
|
|
6791
7828
|
}));
|
|
6792
7829
|
}
|
|
6793
7830
|
setWorkspacePolicy(path, policy) {
|
|
6794
7831
|
this.db.prepare(`UPDATE workspaces SET policy = ? WHERE path = ?`).run(policy, path);
|
|
6795
7832
|
}
|
|
7833
|
+
setWorkspaceIntelligenceMode(path, mode) {
|
|
7834
|
+
this.db.prepare(`UPDATE workspaces SET intelligence_mode = ? WHERE path = ?`).run(mode, path);
|
|
7835
|
+
}
|
|
7836
|
+
// ---- device capability (Execution Plan 7) ----------------------------------
|
|
7837
|
+
getWolCapability() {
|
|
7838
|
+
const row = this.db.prepare(`SELECT * FROM device WHERE id = 1`).get();
|
|
7839
|
+
if (!row || row.wol_capability == null) return null;
|
|
7840
|
+
return {
|
|
7841
|
+
capability: row.wol_capability,
|
|
7842
|
+
reason: row.wol_reason,
|
|
7843
|
+
networkType: row.wol_network_type,
|
|
7844
|
+
mac: row.wol_mac ?? null,
|
|
7845
|
+
broadcast: row.wol_broadcast ?? null,
|
|
7846
|
+
checkedAtMs: row.wol_checked_at_ms
|
|
7847
|
+
};
|
|
7848
|
+
}
|
|
7849
|
+
setWolCapability(v2) {
|
|
7850
|
+
this.db.prepare(
|
|
7851
|
+
`INSERT INTO device (id, wol_capability, wol_reason, wol_network_type, wol_mac, wol_broadcast, wol_checked_at_ms)
|
|
7852
|
+
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
7853
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
7854
|
+
wol_capability = excluded.wol_capability,
|
|
7855
|
+
wol_reason = excluded.wol_reason,
|
|
7856
|
+
wol_network_type = excluded.wol_network_type,
|
|
7857
|
+
wol_mac = excluded.wol_mac,
|
|
7858
|
+
wol_broadcast = excluded.wol_broadcast,
|
|
7859
|
+
wol_checked_at_ms = excluded.wol_checked_at_ms`
|
|
7860
|
+
).run(v2.capability, v2.reason, v2.networkType, v2.mac, v2.broadcast, v2.checkedAtMs);
|
|
7861
|
+
}
|
|
6796
7862
|
};
|
|
6797
7863
|
function searchableText(msg) {
|
|
6798
7864
|
switch (msg.type) {
|
|
@@ -6803,6 +7869,7 @@ function searchableText(msg) {
|
|
|
6803
7869
|
if (msg.event.type === "tool") return msg.event.summary;
|
|
6804
7870
|
if (msg.event.type === "done") return msg.event.summary;
|
|
6805
7871
|
if (msg.event.type === "error") return msg.event.message;
|
|
7872
|
+
if (msg.event.type === "status-update") return msg.event.text;
|
|
6806
7873
|
return "";
|
|
6807
7874
|
case "drop":
|
|
6808
7875
|
return msg.name;
|
|
@@ -10872,11 +11939,42 @@ var ApprovalQuestionSchema = external_exports.object({
|
|
|
10872
11939
|
options: external_exports.array(external_exports.object({ label: external_exports.string(), description: external_exports.string().optional() })),
|
|
10873
11940
|
multiSelect: external_exports.boolean().optional()
|
|
10874
11941
|
});
|
|
11942
|
+
var TodoItemSchema = external_exports.object({
|
|
11943
|
+
content: external_exports.string(),
|
|
11944
|
+
status: external_exports.enum(["pending", "in_progress", "completed"]),
|
|
11945
|
+
activeForm: external_exports.string().optional()
|
|
11946
|
+
});
|
|
11947
|
+
var HandoverPacketSchema = external_exports.object({
|
|
11948
|
+
/** Live `git diff HEAD` against the workspace, captured at handover time. */
|
|
11949
|
+
diff: external_exports.string(),
|
|
11950
|
+
/** Tool-call summaries this session made, oldest first, capped to the most recent. */
|
|
11951
|
+
toolSummaries: external_exports.array(external_exports.string()),
|
|
11952
|
+
/** The last TodoWrite call's task list, verbatim — never summarized. */
|
|
11953
|
+
todos: external_exports.array(TodoItemSchema).optional(),
|
|
11954
|
+
/** Approvals raised but never resolved (still pending or timed out) at handover time. */
|
|
11955
|
+
pendingApprovals: external_exports.array(
|
|
11956
|
+
external_exports.object({ action: external_exports.string(), detail: external_exports.string(), risk: external_exports.enum(["low", "high"]) })
|
|
11957
|
+
),
|
|
11958
|
+
/** Recent prompt/response turns, oldest first, capped — for tone/context continuity. */
|
|
11959
|
+
recentMessages: external_exports.array(external_exports.object({ role: external_exports.enum(["user", "assistant"]), text: external_exports.string() })),
|
|
11960
|
+
/** Optional free-text self-summary from the outgoing agent — enrichment
|
|
11961
|
+
* only, attempted when there was runway before a hard cutoff. Its absence
|
|
11962
|
+
* changes nothing; the packet above is already sufficient on its own. */
|
|
11963
|
+
agentSummary: external_exports.string().optional()
|
|
11964
|
+
});
|
|
10875
11965
|
var RunEventSchema = external_exports.discriminatedUnion("type", [
|
|
10876
11966
|
external_exports.object({ type: external_exports.literal("text"), chunk: external_exports.string() }),
|
|
10877
11967
|
/** Extended-thinking content, streamed the same way as text (medium+ effort tiers). */
|
|
10878
11968
|
external_exports.object({ type: external_exports.literal("thinking"), chunk: external_exports.string() }),
|
|
10879
|
-
external_exports.object({
|
|
11969
|
+
external_exports.object({
|
|
11970
|
+
type: external_exports.literal("tool"),
|
|
11971
|
+
name: external_exports.string(),
|
|
11972
|
+
summary: external_exports.string(),
|
|
11973
|
+
/** Structured payload for calls that matter to continuity (TodoWrite's
|
|
11974
|
+
* actual task list). Optional: most tool calls have none, and omitting
|
|
11975
|
+
* it is always safe — `summary` alone still renders the transcript. */
|
|
11976
|
+
todos: external_exports.array(TodoItemSchema).optional()
|
|
11977
|
+
}),
|
|
10880
11978
|
external_exports.object({
|
|
10881
11979
|
type: external_exports.literal("approval"),
|
|
10882
11980
|
id: external_exports.string(),
|
|
@@ -10921,12 +12019,87 @@ var RunEventSchema = external_exports.discriminatedUnion("type", [
|
|
|
10921
12019
|
fiveHourResetsAtMs: external_exports.number(),
|
|
10922
12020
|
sevenDayUtilization: external_exports.number(),
|
|
10923
12021
|
sevenDayResetsAtMs: external_exports.number()
|
|
12022
|
+
}),
|
|
12023
|
+
/** Emitted once, when the daemon's own threshold check crosses before the
|
|
12024
|
+
* CLI itself refuses — the phone's cue to propose a handover (Execution
|
|
12025
|
+
* Plan 5) while there's still runway to do it gracefully. */
|
|
12026
|
+
external_exports.object({
|
|
12027
|
+
type: external_exports.literal("limit-hit"),
|
|
12028
|
+
reason: external_exports.enum(["rate_limit", "context_window", "cost_cap"]),
|
|
12029
|
+
/** 0-1 utilization that crossed the threshold, when known. */
|
|
12030
|
+
utilization: external_exports.number().optional()
|
|
12031
|
+
}),
|
|
12032
|
+
/** Session cost or wall-clock time crossed the cap set on it (Execution
|
|
12033
|
+
* Plan 1's budget feature) — informational, the run is not cancelled. */
|
|
12034
|
+
external_exports.object({
|
|
12035
|
+
type: external_exports.literal("budget-warning"),
|
|
12036
|
+
kind: external_exports.enum(["cost", "time"]),
|
|
12037
|
+
capUsd: external_exports.number().optional(),
|
|
12038
|
+
capMinutes: external_exports.number().optional(),
|
|
12039
|
+
actualUsd: external_exports.number().optional(),
|
|
12040
|
+
actualMinutes: external_exports.number().optional()
|
|
12041
|
+
}),
|
|
12042
|
+
/** The daemon is offering to move this session to another runner
|
|
12043
|
+
* (Execution Plan 4) — built from the log/diff alone, never from asking
|
|
12044
|
+
* this agent to cooperate (it may already be unable to). */
|
|
12045
|
+
external_exports.object({
|
|
12046
|
+
type: external_exports.literal("handover-proposed"),
|
|
12047
|
+
id: external_exports.string(),
|
|
12048
|
+
fromRunnerId: external_exports.string(),
|
|
12049
|
+
toRunnerId: external_exports.string(),
|
|
12050
|
+
reason: external_exports.enum(["rate_limit", "context_window", "cost_cap", "crash", "manual"]),
|
|
12051
|
+
packet: HandoverPacketSchema
|
|
12052
|
+
}),
|
|
12053
|
+
external_exports.object({
|
|
12054
|
+
type: external_exports.literal("handover-declined"),
|
|
12055
|
+
id: external_exports.string()
|
|
12056
|
+
}),
|
|
12057
|
+
external_exports.object({
|
|
12058
|
+
type: external_exports.literal("handover-accepted"),
|
|
12059
|
+
id: external_exports.string(),
|
|
12060
|
+
fromRunnerId: external_exports.string(),
|
|
12061
|
+
toRunnerId: external_exports.string()
|
|
12062
|
+
}),
|
|
12063
|
+
/** A proactive note the agent chose to push mid-run (Execution Plan 5's
|
|
12064
|
+
* offhand_status_update MCP tool) — outside the normal streamed
|
|
12065
|
+
* text/thinking, and never fabricated: only exists when the agent itself
|
|
12066
|
+
* called the tool. The most recent one on a session also becomes the
|
|
12067
|
+
* Handover Packet's optional `agentSummary` (Execution Plan 4) when a
|
|
12068
|
+
* handover is proposed afterward — still zero hosted AI, since it's the
|
|
12069
|
+
* same agent, on its own tokens, choosing to leave a note. */
|
|
12070
|
+
external_exports.object({
|
|
12071
|
+
type: external_exports.literal("status-update"),
|
|
12072
|
+
text: external_exports.string()
|
|
12073
|
+
}),
|
|
12074
|
+
/** Telemetry for Offhand Intelligence's Phase 1 (Execution Plan 8) — one
|
|
12075
|
+
* record per run where the deterministic context-injection step actually
|
|
12076
|
+
* ran, so Intelligence-ON vs Intelligence-OFF is eventually a real
|
|
12077
|
+
* measurement, not a guess. Never carries the context block's own text —
|
|
12078
|
+
* just the shape of what happened — reusing the existing event-log spine
|
|
12079
|
+
* rather than a parallel telemetry system. */
|
|
12080
|
+
external_exports.object({
|
|
12081
|
+
type: external_exports.literal("intelligence-built"),
|
|
12082
|
+
promptChars: external_exports.number().int(),
|
|
12083
|
+
contextChars: external_exports.number().int(),
|
|
12084
|
+
filesConsidered: external_exports.number().int(),
|
|
12085
|
+
filesSelected: external_exports.number().int(),
|
|
12086
|
+
retrievalOpsRun: external_exports.number().int(),
|
|
12087
|
+
durationMs: external_exports.number().int(),
|
|
12088
|
+
ripgrepAvailable: external_exports.boolean(),
|
|
12089
|
+
timedOut: external_exports.boolean(),
|
|
12090
|
+
/** Whether the block was actually attached to the run (false when
|
|
12091
|
+
* buildContext found nothing worth attaching, or timed out). */
|
|
12092
|
+
attached: external_exports.boolean()
|
|
10924
12093
|
})
|
|
10925
12094
|
]);
|
|
10926
12095
|
var PermissionModeSchema = external_exports.enum(["guarded", "plan", "acceptEdits", "bypass"]);
|
|
10927
12096
|
var EffortSchema = external_exports.enum(["low", "medium", "high", "max"]);
|
|
10928
12097
|
var RunSpecSchema = external_exports.object({
|
|
10929
12098
|
runId: external_exports.string(),
|
|
12099
|
+
/** The offhand session this run belongs to — lets an MCP bridge tool
|
|
12100
|
+
* (Execution Plan 5) tag a proactive call (status update, dropped file)
|
|
12101
|
+
* with the right session without a global single-listener broker. */
|
|
12102
|
+
sessionId: external_exports.string(),
|
|
10930
12103
|
prompt: external_exports.string().min(1),
|
|
10931
12104
|
/** Absolute path of the workspace the agent runs in. */
|
|
10932
12105
|
workspace: external_exports.string(),
|
|
@@ -10951,9 +12124,22 @@ var RunnerInfoSchema = external_exports.object({
|
|
|
10951
12124
|
version: external_exports.string().optional(),
|
|
10952
12125
|
/** Model ids the phone may offer; empty = CLI default only. */
|
|
10953
12126
|
models: external_exports.array(external_exports.string()),
|
|
10954
|
-
supportsApprovals: external_exports.boolean()
|
|
12127
|
+
supportsApprovals: external_exports.boolean(),
|
|
12128
|
+
/** Thinking-budget tiers this runner actually honors (Execution Plan 3) —
|
|
12129
|
+
* undefined means the phone should hide the effort control entirely for
|
|
12130
|
+
* this runner, not show one that silently does nothing. */
|
|
12131
|
+
effortTiers: external_exports.array(external_exports.enum(["low", "medium", "high", "max"])).optional(),
|
|
12132
|
+
/** Display string for the vendor's own install command (e.g. "npm install
|
|
12133
|
+
* -g @anthropic-ai/claude-code") — present only when the daemon can run
|
|
12134
|
+
* it unattended via `runner-install`. */
|
|
12135
|
+
installCommand: external_exports.string().optional(),
|
|
12136
|
+
/** Where to send the user instead, when there's no safe one-tap install
|
|
12137
|
+
* (a piped shell-script installer, a GUI app, etc.) — never set alongside
|
|
12138
|
+
* installCommand. */
|
|
12139
|
+
installUrl: external_exports.string().optional()
|
|
10955
12140
|
});
|
|
10956
12141
|
var ApprovalPolicySchema = external_exports.enum(["paranoid", "balanced", "trusting"]);
|
|
12142
|
+
var IntelligenceModeSchema = external_exports.enum(["off", "automatic", "manual"]);
|
|
10957
12143
|
var WorkspaceInfoSchema = external_exports.object({
|
|
10958
12144
|
path: external_exports.string(),
|
|
10959
12145
|
label: external_exports.string(),
|
|
@@ -10962,7 +12148,8 @@ var WorkspaceInfoSchema = external_exports.object({
|
|
|
10962
12148
|
devUrl: external_exports.string().optional(),
|
|
10963
12149
|
/** Approval policy: paranoid = ask everything the CLI would ask; balanced =
|
|
10964
12150
|
* same (default); trusting = auto-approve low-risk, ask only high-risk. */
|
|
10965
|
-
policy: ApprovalPolicySchema
|
|
12151
|
+
policy: ApprovalPolicySchema,
|
|
12152
|
+
intelligenceMode: IntelligenceModeSchema
|
|
10966
12153
|
});
|
|
10967
12154
|
var SessionInfoSchema = external_exports.object({
|
|
10968
12155
|
id: external_exports.string(),
|
|
@@ -10978,12 +12165,23 @@ var SessionInfoSchema = external_exports.object({
|
|
|
10978
12165
|
effort: external_exports.enum(["low", "medium", "high", "max"]).optional(),
|
|
10979
12166
|
/** Whether a run is active or prompts are queued right now. */
|
|
10980
12167
|
busy: external_exports.boolean(),
|
|
10981
|
-
queuedPrompts: external_exports.number().int()
|
|
12168
|
+
queuedPrompts: external_exports.number().int(),
|
|
12169
|
+
/** Per-session budget caps (Execution Plan 1) — undefined means no cap set. */
|
|
12170
|
+
budgetCostUsdCap: external_exports.number().optional(),
|
|
12171
|
+
budgetMinutesCap: external_exports.number().int().optional()
|
|
10982
12172
|
});
|
|
10983
12173
|
var HostInfoSchema = external_exports.object({
|
|
10984
12174
|
hostname: external_exports.string(),
|
|
10985
12175
|
os: external_exports.string(),
|
|
10986
|
-
daemonVersion: external_exports.string()
|
|
12176
|
+
daemonVersion: external_exports.string(),
|
|
12177
|
+
/** Wake-on-LAN/WiFi capability, detected per device (Execution Plan 7) —
|
|
12178
|
+
* never assumed from the OS alone. Absent only if no probe has completed
|
|
12179
|
+
* yet (e.g. mid-startup). */
|
|
12180
|
+
wolCapability: external_exports.enum(["supported", "wifi-limited", "unsupported"]).optional(),
|
|
12181
|
+
/** Human-readable reason behind wolCapability — always present alongside
|
|
12182
|
+
* it, since a bare enum can't explain itself. */
|
|
12183
|
+
wolReason: external_exports.string().optional(),
|
|
12184
|
+
wolCheckedAtMs: external_exports.number().int().optional()
|
|
10987
12185
|
});
|
|
10988
12186
|
|
|
10989
12187
|
// ../shared/src/protocol-v2.ts
|
|
@@ -11017,6 +12215,12 @@ var ClientMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
11017
12215
|
answer: external_exports.string().optional()
|
|
11018
12216
|
}),
|
|
11019
12217
|
external_exports.object({ type: external_exports.literal("resume"), afterSeq: external_exports.number().int().nonnegative() }),
|
|
12218
|
+
external_exports.object({
|
|
12219
|
+
type: external_exports.literal("handover-response"),
|
|
12220
|
+
sessionId: external_exports.string(),
|
|
12221
|
+
handoverId: external_exports.string(),
|
|
12222
|
+
accept: external_exports.boolean()
|
|
12223
|
+
}),
|
|
11020
12224
|
external_exports.object({
|
|
11021
12225
|
type: external_exports.literal("drop-send"),
|
|
11022
12226
|
blobId: external_exports.string(),
|
|
@@ -11047,7 +12251,10 @@ var ClientMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
11047
12251
|
model: external_exports.string().optional(),
|
|
11048
12252
|
archived: external_exports.boolean().optional(),
|
|
11049
12253
|
permissionMode: external_exports.enum(["guarded", "plan", "acceptEdits", "bypass"]).optional(),
|
|
11050
|
-
effort: external_exports.enum(["low", "medium", "high", "max"]).optional()
|
|
12254
|
+
effort: external_exports.enum(["low", "medium", "high", "max"]).optional(),
|
|
12255
|
+
/** Per-session budget caps (Execution Plan 1). null clears an existing cap. */
|
|
12256
|
+
budgetCostUsdCap: external_exports.number().positive().nullable().optional(),
|
|
12257
|
+
budgetMinutesCap: external_exports.number().int().positive().nullable().optional()
|
|
11051
12258
|
}),
|
|
11052
12259
|
/** Start a fresh agent conversation inside the session. */
|
|
11053
12260
|
external_exports.object({ type: external_exports.literal("session-reset"), sessionId: external_exports.string() }),
|
|
@@ -11057,6 +12264,29 @@ var ClientMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
11057
12264
|
workspace: external_exports.string(),
|
|
11058
12265
|
policy: ApprovalPolicySchema
|
|
11059
12266
|
}),
|
|
12267
|
+
/** Set a workspace's Offhand Intelligence mode (Execution Plan 8). */
|
|
12268
|
+
external_exports.object({
|
|
12269
|
+
type: external_exports.literal("intelligence-set"),
|
|
12270
|
+
workspace: external_exports.string(),
|
|
12271
|
+
mode: IntelligenceModeSchema
|
|
12272
|
+
}),
|
|
12273
|
+
/** Like `prompt`, but routes through Offhand Intelligence first — the
|
|
12274
|
+
* daemon replies with `intelligence-review` instead of queueing
|
|
12275
|
+
* immediately, and queueing happens only after `intelligence-response`. */
|
|
12276
|
+
external_exports.object({
|
|
12277
|
+
type: external_exports.literal("intelligence-prepare"),
|
|
12278
|
+
sessionId: external_exports.string(),
|
|
12279
|
+
prompt: external_exports.string().min(1),
|
|
12280
|
+
attachments: external_exports.array(external_exports.object({ blobId: external_exports.string(), name: external_exports.string(), mime: external_exports.string() })).optional()
|
|
12281
|
+
}),
|
|
12282
|
+
/** Resolves a pending `intelligence-review` — useContext=false sends the
|
|
12283
|
+
* plain original prompt, never a dead end that discards it. */
|
|
12284
|
+
external_exports.object({
|
|
12285
|
+
type: external_exports.literal("intelligence-response"),
|
|
12286
|
+
sessionId: external_exports.string(),
|
|
12287
|
+
reviewId: external_exports.string(),
|
|
12288
|
+
useContext: external_exports.boolean()
|
|
12289
|
+
}),
|
|
11060
12290
|
// History RPC (request/response by rpcId; responses are not in the seq log)
|
|
11061
12291
|
external_exports.object({
|
|
11062
12292
|
type: external_exports.literal("history-request"),
|
|
@@ -11081,7 +12311,24 @@ var ClientMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
11081
12311
|
type: external_exports.literal("fs-list"),
|
|
11082
12312
|
rpcId: external_exports.string(),
|
|
11083
12313
|
path: external_exports.string().optional()
|
|
11084
|
-
})
|
|
12314
|
+
}),
|
|
12315
|
+
/** Mobile-first session bootstrap (Execution Plan 5): create a new project
|
|
12316
|
+
* folder from the phone's workspace browser, no laptop needed first. */
|
|
12317
|
+
external_exports.object({
|
|
12318
|
+
type: external_exports.literal("fs-create-folder"),
|
|
12319
|
+
rpcId: external_exports.string(),
|
|
12320
|
+
path: external_exports.string(),
|
|
12321
|
+
name: external_exports.string().min(1)
|
|
12322
|
+
}),
|
|
12323
|
+
/** Re-run the Wake-on-LAN/WiFi capability probe (Execution Plan 7) — an
|
|
12324
|
+
* explicit "check again" action, since drivers/BIOS settings/network type
|
|
12325
|
+
* can change after first setup. */
|
|
12326
|
+
external_exports.object({ type: external_exports.literal("wol-recheck") }),
|
|
12327
|
+
/** One-tap install: run a runner's own vendor install command on the
|
|
12328
|
+
* daemon's machine. Only valid for a runner whose manifest entry carries
|
|
12329
|
+
* `installCommand` — the daemon refuses (with a reply, not a hang) for
|
|
12330
|
+
* any other runnerId. */
|
|
12331
|
+
external_exports.object({ type: external_exports.literal("runner-install"), rpcId: external_exports.string(), runnerId: external_exports.string() })
|
|
11085
12332
|
]);
|
|
11086
12333
|
var ReceiptSchema = external_exports.object({
|
|
11087
12334
|
runId: external_exports.string(),
|
|
@@ -11139,6 +12386,37 @@ var ServerMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
11139
12386
|
size: external_exports.number().int().nonnegative(),
|
|
11140
12387
|
direction: external_exports.enum(["to-phone", "to-pc"])
|
|
11141
12388
|
}),
|
|
12389
|
+
/** The daemon's own relay connection dropped/came back — logged durably
|
|
12390
|
+
* (unlike the relay's live `presence` frame) so a phone that wasn't
|
|
12391
|
+
* listening at the time can still reconstruct "laptop asleep since 11pm"
|
|
12392
|
+
* from history (Execution Plan 6's digest). Not tied to any run. */
|
|
12393
|
+
external_exports.object({ type: external_exports.literal("device-offline"), seq: external_exports.number().int(), atMs: external_exports.number().int() }),
|
|
12394
|
+
external_exports.object({
|
|
12395
|
+
type: external_exports.literal("device-reconnected"),
|
|
12396
|
+
seq: external_exports.number().int(),
|
|
12397
|
+
atMs: external_exports.number().int(),
|
|
12398
|
+
offlineForMs: external_exports.number().int().nonnegative()
|
|
12399
|
+
}),
|
|
12400
|
+
/** Offhand Intelligence (Execution Plan 8) proposed a context block for a
|
|
12401
|
+
* prompt not yet queued to any runner — not a RunEvent, since no run
|
|
12402
|
+
* exists at this point. `filesSelected` is a quick summary for the sheet
|
|
12403
|
+
* header; the full reasoning lives in `block` itself. */
|
|
12404
|
+
external_exports.object({
|
|
12405
|
+
type: external_exports.literal("intelligence-review"),
|
|
12406
|
+
sessionId: external_exports.string(),
|
|
12407
|
+
seq: external_exports.number().int(),
|
|
12408
|
+
reviewId: external_exports.string(),
|
|
12409
|
+
originalPrompt: external_exports.string(),
|
|
12410
|
+
block: external_exports.string(),
|
|
12411
|
+
filesSelected: external_exports.number().int()
|
|
12412
|
+
}),
|
|
12413
|
+
external_exports.object({
|
|
12414
|
+
type: external_exports.literal("intelligence-resolved"),
|
|
12415
|
+
sessionId: external_exports.string(),
|
|
12416
|
+
seq: external_exports.number().int(),
|
|
12417
|
+
reviewId: external_exports.string(),
|
|
12418
|
+
useContext: external_exports.boolean()
|
|
12419
|
+
}),
|
|
11142
12420
|
// RPC responses (not seq-logged)
|
|
11143
12421
|
external_exports.object({
|
|
11144
12422
|
type: external_exports.literal("history-response"),
|
|
@@ -11164,7 +12442,21 @@ var ServerMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
11164
12442
|
rpcId: external_exports.string(),
|
|
11165
12443
|
path: external_exports.string(),
|
|
11166
12444
|
parent: external_exports.string().nullable(),
|
|
11167
|
-
dirs: external_exports.array(FsDirSchema)
|
|
12445
|
+
dirs: external_exports.array(FsDirSchema),
|
|
12446
|
+
/** Set only by fs-create-folder on failure — the generic top-level
|
|
12447
|
+
* `error` message carries no rpcId, so it can never resolve a specific
|
|
12448
|
+
* pending RPC (see client.ts's rpc()); this keeps the failure correctly
|
|
12449
|
+
* routed back to whoever asked, with the listing unchanged either way. */
|
|
12450
|
+
error: external_exports.string().optional()
|
|
12451
|
+
}),
|
|
12452
|
+
/** Reply to `runner-install` — always sent, success or failure, never a
|
|
12453
|
+
* silent hang. On failure `message` carries the real error (e.g. npm's
|
|
12454
|
+
* own stderr tail), not a generic "install failed". */
|
|
12455
|
+
external_exports.object({
|
|
12456
|
+
type: external_exports.literal("runner-install-response"),
|
|
12457
|
+
rpcId: external_exports.string(),
|
|
12458
|
+
ok: external_exports.boolean(),
|
|
12459
|
+
message: external_exports.string()
|
|
11168
12460
|
}),
|
|
11169
12461
|
external_exports.object({ type: external_exports.literal("error"), message: external_exports.string() })
|
|
11170
12462
|
]);
|
|
@@ -11188,7 +12480,8 @@ var RelayFrameSchema = external_exports.discriminatedUnion("kind", [
|
|
|
11188
12480
|
*/
|
|
11189
12481
|
external_exports.object({
|
|
11190
12482
|
kind: external_exports.literal("notify"),
|
|
11191
|
-
notice: external_exports.enum(["approval", "drop", "done", "error", "progress"]),
|
|
12483
|
+
notice: external_exports.enum(["approval", "drop", "done", "error", "progress", "handover", "limit-hit"]),
|
|
12484
|
+
/** 'approval'/'handover' only — the approval/handover id being acted on. */
|
|
11192
12485
|
id: external_exports.string().optional(),
|
|
11193
12486
|
sessionId: external_exports.string().optional(),
|
|
11194
12487
|
/** 'progress' only — a bare action count, never the action itself (no content in push). */
|
|
@@ -11196,6 +12489,16 @@ var RelayFrameSchema = external_exports.discriminatedUnion("kind", [
|
|
|
11196
12489
|
}),
|
|
11197
12490
|
/** relay → daemon: verdict delivered via push-notification action button. */
|
|
11198
12491
|
external_exports.object({ kind: external_exports.literal("verdict"), approvalId: external_exports.string(), approve: external_exports.boolean() }),
|
|
12492
|
+
/** relay → daemon: handover accept/decline from a push-notification action
|
|
12493
|
+
* button (Execution Plan 6) — same "opaque ids only" shape as `verdict`,
|
|
12494
|
+
* with `chatSessionId` since handover-response needs it to look up the
|
|
12495
|
+
* pending proposal. */
|
|
12496
|
+
external_exports.object({
|
|
12497
|
+
kind: external_exports.literal("handover-verdict"),
|
|
12498
|
+
handoverId: external_exports.string(),
|
|
12499
|
+
chatSessionId: external_exports.string(),
|
|
12500
|
+
accept: external_exports.boolean()
|
|
12501
|
+
}),
|
|
11199
12502
|
external_exports.object({ kind: external_exports.literal("ping") }),
|
|
11200
12503
|
external_exports.object({ kind: external_exports.literal("pong") })
|
|
11201
12504
|
]);
|
|
@@ -41803,6 +43106,36 @@ var LocalSessionServer = class {
|
|
|
41803
43106
|
}
|
|
41804
43107
|
return;
|
|
41805
43108
|
}
|
|
43109
|
+
if (req.method === "POST" && req.url === "/status") {
|
|
43110
|
+
try {
|
|
43111
|
+
const chunks = [];
|
|
43112
|
+
for await (const c2 of req) chunks.push(c2);
|
|
43113
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
43114
|
+
if (typeof body.sessionId === "string" && typeof body.text === "string" && body.text.trim()) {
|
|
43115
|
+
this.manager.recordStatusUpdate(body.sessionId, body.text.trim());
|
|
43116
|
+
}
|
|
43117
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
43118
|
+
res.end(JSON.stringify({ ok: true }));
|
|
43119
|
+
} catch (e) {
|
|
43120
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
43121
|
+
res.end(JSON.stringify({ ok: false, message: `bad status request: ${String(e)}` }));
|
|
43122
|
+
}
|
|
43123
|
+
return;
|
|
43124
|
+
}
|
|
43125
|
+
if (req.method === "POST" && req.url === "/context") {
|
|
43126
|
+
try {
|
|
43127
|
+
const chunks = [];
|
|
43128
|
+
for await (const c2 of req) chunks.push(c2);
|
|
43129
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
43130
|
+
const result = typeof body.sessionId === "string" && typeof body.query === "string" && body.query.trim() ? await this.manager.buildContextFor(body.sessionId, body.query.trim()) : null;
|
|
43131
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
43132
|
+
res.end(JSON.stringify(result ?? { block: null, filesSelected: 0 }));
|
|
43133
|
+
} catch (e) {
|
|
43134
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
43135
|
+
res.end(JSON.stringify({ block: null, filesSelected: 0, message: `bad context request: ${String(e)}` }));
|
|
43136
|
+
}
|
|
43137
|
+
return;
|
|
43138
|
+
}
|
|
41806
43139
|
res.writeHead(404);
|
|
41807
43140
|
res.end();
|
|
41808
43141
|
}
|
|
@@ -41838,8 +43171,19 @@ var RelayClient = class {
|
|
|
41838
43171
|
heartbeat = null;
|
|
41839
43172
|
detach = null;
|
|
41840
43173
|
stopped = false;
|
|
43174
|
+
/** Set when a heartbeat ping goes out; cleared by ANY inbound frame. If
|
|
43175
|
+
* still set on the next tick, nothing has answered in a full interval —
|
|
43176
|
+
* the connection is a zombie (readyState still reports OPEN even though
|
|
43177
|
+
* the underlying TCP connection died silently, e.g. after laptop sleep or
|
|
43178
|
+
* a NAT/firewall idle timeout — no close/error event fires on its own). */
|
|
43179
|
+
awaitingPongSince = null;
|
|
41841
43180
|
/** sessionId → tool-call count for the in-flight run (opaque count only, never the action). */
|
|
41842
43181
|
toolCounts = /* @__PURE__ */ new Map();
|
|
43182
|
+
/** True once we've been online at least once — a failed first connection
|
|
43183
|
+
* attempt isn't "going offline" (there was nothing to lose), so the very
|
|
43184
|
+
* first successful open never logs a reconnect. */
|
|
43185
|
+
everConnected = false;
|
|
43186
|
+
offlineSinceMs = null;
|
|
41843
43187
|
start() {
|
|
41844
43188
|
this.connect();
|
|
41845
43189
|
}
|
|
@@ -41854,8 +43198,13 @@ var RelayClient = class {
|
|
|
41854
43198
|
}
|
|
41855
43199
|
connect() {
|
|
41856
43200
|
if (this.stopped) return;
|
|
43201
|
+
console.log(`relay: connecting (${this.relayUrl})`);
|
|
41857
43202
|
const ws = new wrapper_default(this.url());
|
|
41858
43203
|
this.ws = ws;
|
|
43204
|
+
const slowStartTimer = setTimeout(() => {
|
|
43205
|
+
console.log("relay: still connecting \u2014 the hosted relay may be waking up from a cold start (can take up to a minute)");
|
|
43206
|
+
}, 8e3);
|
|
43207
|
+
const clearSlowStartTimer = () => clearTimeout(slowStartTimer);
|
|
41859
43208
|
const send = (msg) => {
|
|
41860
43209
|
if (ws.readyState === wrapper_default.OPEN) {
|
|
41861
43210
|
const envelope = seal(msg, this.keys.tx);
|
|
@@ -41895,24 +43244,52 @@ var RelayClient = class {
|
|
|
41895
43244
|
JSON.stringify({ kind: "notify", notice: "error", sessionId: msg.sessionId })
|
|
41896
43245
|
);
|
|
41897
43246
|
}
|
|
43247
|
+
if (msg.type === "run-event" && msg.event.type === "handover-proposed") {
|
|
43248
|
+
ws.send(
|
|
43249
|
+
JSON.stringify({
|
|
43250
|
+
kind: "notify",
|
|
43251
|
+
notice: "handover",
|
|
43252
|
+
id: msg.event.id,
|
|
43253
|
+
sessionId: msg.sessionId
|
|
43254
|
+
})
|
|
43255
|
+
);
|
|
43256
|
+
}
|
|
43257
|
+
if (msg.type === "run-event" && msg.event.type === "limit-hit") {
|
|
43258
|
+
ws.send(
|
|
43259
|
+
JSON.stringify({ kind: "notify", notice: "limit-hit", sessionId: msg.sessionId })
|
|
43260
|
+
);
|
|
43261
|
+
}
|
|
41898
43262
|
if (msg.type === "drop" && msg.direction === "to-phone") {
|
|
41899
43263
|
ws.send(JSON.stringify({ kind: "notify", notice: "drop" }));
|
|
41900
43264
|
}
|
|
41901
43265
|
}
|
|
41902
43266
|
};
|
|
41903
43267
|
ws.on("open", () => {
|
|
43268
|
+
clearSlowStartTimer();
|
|
41904
43269
|
this.backoffMs = 500;
|
|
43270
|
+
this.awaitingPongSince = null;
|
|
41905
43271
|
console.log(`relay: connected (${this.relayUrl})`);
|
|
43272
|
+
if (this.everConnected && this.offlineSinceMs !== null) {
|
|
43273
|
+
this.manager.recordDeviceReconnected(Date.now() - this.offlineSinceMs);
|
|
43274
|
+
}
|
|
43275
|
+
this.everConnected = true;
|
|
43276
|
+
this.offlineSinceMs = null;
|
|
41906
43277
|
this.detach = this.manager.attach(send);
|
|
41907
43278
|
send(this.manager.hello());
|
|
41908
43279
|
void this.manager.manifest().then(send);
|
|
41909
43280
|
this.heartbeat = setInterval(() => {
|
|
41910
|
-
if (ws.readyState
|
|
41911
|
-
|
|
43281
|
+
if (ws.readyState !== wrapper_default.OPEN) return;
|
|
43282
|
+
if (this.awaitingPongSince !== null) {
|
|
43283
|
+
console.log("relay: no response since last heartbeat \u2014 reconnecting");
|
|
43284
|
+
ws.terminate();
|
|
43285
|
+
return;
|
|
41912
43286
|
}
|
|
43287
|
+
this.awaitingPongSince = Date.now();
|
|
43288
|
+
ws.send(JSON.stringify({ kind: "ping" }));
|
|
41913
43289
|
}, HEARTBEAT_MS);
|
|
41914
43290
|
});
|
|
41915
43291
|
ws.on("message", (raw) => {
|
|
43292
|
+
this.awaitingPongSince = null;
|
|
41916
43293
|
let frame;
|
|
41917
43294
|
try {
|
|
41918
43295
|
frame = parseRelayFrame(raw.toString());
|
|
@@ -41926,6 +43303,18 @@ var RelayClient = class {
|
|
|
41926
43303
|
);
|
|
41927
43304
|
return;
|
|
41928
43305
|
}
|
|
43306
|
+
if (frame.kind === "handover-verdict") {
|
|
43307
|
+
this.manager.handle(
|
|
43308
|
+
{
|
|
43309
|
+
type: "handover-response",
|
|
43310
|
+
sessionId: frame.chatSessionId,
|
|
43311
|
+
handoverId: frame.handoverId,
|
|
43312
|
+
accept: frame.accept
|
|
43313
|
+
},
|
|
43314
|
+
send
|
|
43315
|
+
);
|
|
43316
|
+
return;
|
|
43317
|
+
}
|
|
41929
43318
|
if (frame.kind !== "peer") return;
|
|
41930
43319
|
try {
|
|
41931
43320
|
const envelope = EnvelopeSchema.parse(frame.payload);
|
|
@@ -41938,7 +43327,12 @@ var RelayClient = class {
|
|
|
41938
43327
|
console.error(`relay: ${err.message}`);
|
|
41939
43328
|
});
|
|
41940
43329
|
ws.on("close", () => {
|
|
43330
|
+
clearSlowStartTimer();
|
|
41941
43331
|
this.cleanup();
|
|
43332
|
+
if (this.everConnected && this.offlineSinceMs === null) {
|
|
43333
|
+
this.offlineSinceMs = Date.now();
|
|
43334
|
+
this.manager.recordDeviceOffline();
|
|
43335
|
+
}
|
|
41942
43336
|
if (this.stopped) return;
|
|
41943
43337
|
console.log(`relay: disconnected \u2014 retrying in ${this.backoffMs}ms`);
|
|
41944
43338
|
setTimeout(() => this.connect(), this.backoffMs);
|
|
@@ -41954,7 +43348,7 @@ var RelayClient = class {
|
|
|
41954
43348
|
};
|
|
41955
43349
|
|
|
41956
43350
|
// ../daemon/src/pairing.ts
|
|
41957
|
-
import { existsSync as
|
|
43351
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
|
|
41958
43352
|
import { homedir as homedir7 } from "node:os";
|
|
41959
43353
|
import { join as join8 } from "node:path";
|
|
41960
43354
|
import { randomInt } from "node:crypto";
|
|
@@ -41964,7 +43358,7 @@ var POLL_MS = 2e3;
|
|
|
41964
43358
|
var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
41965
43359
|
async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://offhand-web.onrender.com") {
|
|
41966
43360
|
await ready;
|
|
41967
|
-
if (!forceNew &&
|
|
43361
|
+
if (!forceNew && existsSync8(PAIRING_PATH)) {
|
|
41968
43362
|
const f2 = JSON.parse(readFileSync4(PAIRING_PATH, "utf8"));
|
|
41969
43363
|
const kp2 = { publicKey: fromB64u(f2.daemonPublicKey), secretKey: fromB64u(f2.daemonSecretKey) };
|
|
41970
43364
|
const phonePk = fromB64u(f2.phonePublicKey);
|
|
@@ -42118,6 +43512,7 @@ var ApprovalBroker = class {
|
|
|
42118
43512
|
}
|
|
42119
43513
|
};
|
|
42120
43514
|
function classifyRisk(toolName, input) {
|
|
43515
|
+
if (toolName === "offhand_send_file") return "high";
|
|
42121
43516
|
const i2 = input ?? {};
|
|
42122
43517
|
const text = [toolName, i2.command, i2.file_path, i2.path, i2.filepath].filter((x2) => typeof x2 === "string").join(" ");
|
|
42123
43518
|
return /\b(rm|del|rmdir|rd|format|mkfs|shutdown|reboot|kill|drop\s+table|truncate|git\s+push\s+--force|--hard)\b/i.test(
|
|
@@ -42171,9 +43566,9 @@ function buildPreview(toolName, input) {
|
|
|
42171
43566
|
return void 0;
|
|
42172
43567
|
}
|
|
42173
43568
|
function formatUnifiedDiff(diff) {
|
|
42174
|
-
|
|
42175
|
-
|
|
42176
|
-
|
|
43569
|
+
return diff.split("\n").filter(
|
|
43570
|
+
(l2) => !l2.startsWith("index ") && !l2.startsWith("Index:") && !/^={3,}$/.test(l2) && !/^(new|deleted) file mode /.test(l2) && !/^(old|new) mode /.test(l2)
|
|
43571
|
+
).join("\n").trimEnd();
|
|
42177
43572
|
}
|
|
42178
43573
|
function truncate6(s2, max) {
|
|
42179
43574
|
return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
|
|
@@ -42213,12 +43608,12 @@ async function downloadArtifact(relayUrl2, sessionId, blobId, keys) {
|
|
|
42213
43608
|
|
|
42214
43609
|
// ../daemon/src/autostart.ts
|
|
42215
43610
|
import { homedir as homedir8 } from "node:os";
|
|
42216
|
-
import { join as join10, resolve as resolve4, dirname as
|
|
42217
|
-
import { existsSync as
|
|
42218
|
-
import { fileURLToPath as
|
|
43611
|
+
import { join as join10, resolve as resolve4, dirname as dirname4 } from "node:path";
|
|
43612
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync } from "node:fs";
|
|
43613
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
42219
43614
|
import { spawnSync } from "node:child_process";
|
|
42220
43615
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
42221
|
-
var __dirname2 =
|
|
43616
|
+
var __dirname2 = dirname4(fileURLToPath4(import.meta.url));
|
|
42222
43617
|
var SHORTCUT_NAME = "OffhandDaemon.lnk";
|
|
42223
43618
|
var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join10(homedir8(), ".offhand");
|
|
42224
43619
|
function getStartupDir() {
|
|
@@ -42235,27 +43630,23 @@ function getWrapperPath() {
|
|
|
42235
43630
|
function getShortcutPath() {
|
|
42236
43631
|
return join10(getStartupDir(), SHORTCUT_NAME);
|
|
42237
43632
|
}
|
|
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}
|
|
43633
|
+
function buildWrapperScript(config2, entry = process.argv[1] ?? "", execPath = process.execPath) {
|
|
43634
|
+
const wsArgs2 = config2.workspaces.map((w2) => `--workspace "${w2}"`).join(" ");
|
|
43635
|
+
const relayArg = config2.relayUrl ? `--relay "${config2.relayUrl}"` : "";
|
|
43636
|
+
const devUrlArg = config2.devUrl ? `--dev-url "${config2.devUrl}"` : "";
|
|
43637
|
+
const portArg = `--port ${config2.port}`;
|
|
43638
|
+
const approvalArg = `--approval-timeout ${config2.approvalTimeout ?? 300}`;
|
|
43639
|
+
const webUrlArg = `--web-url "${config2.webUrl ?? "https://offhand-web.onrender.com"}"`;
|
|
43640
|
+
const args2 = [wsArgs2, relayArg, devUrlArg, portArg, approvalArg, webUrlArg].filter(Boolean).join(" ");
|
|
43641
|
+
if (entry.endsWith(".ts")) {
|
|
43642
|
+
const projectRoot = resolve4(__dirname2, "..", "..");
|
|
43643
|
+
return `@echo off\r
|
|
43644
|
+
cd /d "${projectRoot}"\r
|
|
43645
|
+
pnpm --filter @offhand/daemon dev -- ${args2}\r
|
|
43646
|
+
`;
|
|
43647
|
+
}
|
|
43648
|
+
return `@echo off\r
|
|
43649
|
+
"${execPath}" "${entry}" ${args2}\r
|
|
42259
43650
|
`;
|
|
42260
43651
|
}
|
|
42261
43652
|
function createShortcut(targetPath, shortcutPath) {
|
|
@@ -42276,11 +43667,11 @@ $Shortcut.Save()`;
|
|
|
42276
43667
|
return { success: result.status === 0, output: result.stdout + result.stderr };
|
|
42277
43668
|
}
|
|
42278
43669
|
function checkShortcutExists(shortcutPath) {
|
|
42279
|
-
return
|
|
43670
|
+
return existsSync9(shortcutPath);
|
|
42280
43671
|
}
|
|
42281
|
-
function installAutostart(
|
|
43672
|
+
function installAutostart(config2) {
|
|
42282
43673
|
const wrapperPath = getWrapperPath();
|
|
42283
|
-
const script = buildWrapperScript(
|
|
43674
|
+
const script = buildWrapperScript(config2);
|
|
42284
43675
|
writeFileSync4(wrapperPath, script);
|
|
42285
43676
|
const shortcutPath = getShortcutPath();
|
|
42286
43677
|
const result = createShortcut(wrapperPath, shortcutPath);
|
|
@@ -42296,7 +43687,7 @@ function getAutostartStatus() {
|
|
|
42296
43687
|
const shortcutPath = getShortcutPath();
|
|
42297
43688
|
const installed = checkShortcutExists(shortcutPath);
|
|
42298
43689
|
const wrapperPath = getWrapperPath();
|
|
42299
|
-
const wrapperExists =
|
|
43690
|
+
const wrapperExists = existsSync9(wrapperPath);
|
|
42300
43691
|
let details = `Shortcut: ${shortcutPath} (${installed ? "EXISTS" : "MISSING"})`;
|
|
42301
43692
|
if (wrapperExists) {
|
|
42302
43693
|
details += `
|
|
@@ -42304,14 +43695,13 @@ Wrapper: ${wrapperPath} (EXISTS)`;
|
|
|
42304
43695
|
}
|
|
42305
43696
|
return { installed, details };
|
|
42306
43697
|
}
|
|
42307
|
-
function saveConfig(
|
|
43698
|
+
function saveConfig(config2) {
|
|
42308
43699
|
const configPath = join10(OFFHAND_HOME, "autostart.json");
|
|
42309
|
-
writeFileSync4(configPath, JSON.stringify(
|
|
43700
|
+
writeFileSync4(configPath, JSON.stringify(config2, null, 2));
|
|
42310
43701
|
}
|
|
42311
43702
|
function getConfigFromStore() {
|
|
42312
|
-
const workspaces = ["C:\\Users\\udbha\\dev\\offhand"];
|
|
42313
43703
|
return {
|
|
42314
|
-
workspaces,
|
|
43704
|
+
workspaces: [],
|
|
42315
43705
|
port: 4317,
|
|
42316
43706
|
relayUrl: void 0,
|
|
42317
43707
|
devUrl: void 0,
|
|
@@ -42354,7 +43744,7 @@ var devUrl = argValue("--dev-url");
|
|
|
42354
43744
|
var store = new Store();
|
|
42355
43745
|
var wsArgs = argValues("--workspace").map((w2) => resolve5(w2));
|
|
42356
43746
|
for (const w2 of wsArgs) {
|
|
42357
|
-
if (!
|
|
43747
|
+
if (!existsSync10(w2)) {
|
|
42358
43748
|
console.error(`workspace does not exist: ${w2}`);
|
|
42359
43749
|
process.exit(1);
|
|
42360
43750
|
}
|
|
@@ -42363,10 +43753,11 @@ for (const w2 of wsArgs) {
|
|
|
42363
43753
|
if (store.listWorkspaces().length === 0) store.upsertWorkspace(process.cwd(), devUrl);
|
|
42364
43754
|
var broker = new ApprovalBroker(approvalTimeoutMs);
|
|
42365
43755
|
var approvalUrl = `http://127.0.0.1:${port}/approval`;
|
|
43756
|
+
var statusUrl = approvalUrl.replace(/\/approval$/, "/status");
|
|
42366
43757
|
var runners = [
|
|
42367
43758
|
new ClaudeCodeRunner(broker, approvalUrl),
|
|
42368
43759
|
new CopilotCliRunner(),
|
|
42369
|
-
new OpenCodeRunner(broker),
|
|
43760
|
+
new OpenCodeRunner(broker, statusUrl),
|
|
42370
43761
|
new CodexCliRunner(),
|
|
42371
43762
|
new CursorAgentRunner(),
|
|
42372
43763
|
new GeminiCliRunner()
|
|
@@ -42387,25 +43778,20 @@ if (m2.type === "manifest") {
|
|
|
42387
43778
|
new LocalSessionServer(manager, port, broker);
|
|
42388
43779
|
console.log(` local : ws://127.0.0.1:${port}`);
|
|
42389
43780
|
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
|
-
}
|
|
43781
|
+
var wasInstalled = getAutostartStatus().installed;
|
|
43782
|
+
var config = getConfigFromStore();
|
|
43783
|
+
config.workspaces = store.listWorkspaces().map((w2) => w2.path);
|
|
43784
|
+
config.port = port;
|
|
43785
|
+
config.relayUrl = relayUrl;
|
|
43786
|
+
config.devUrl = devUrl;
|
|
43787
|
+
config.approvalTimeout = approvalTimeoutMs / 1e3;
|
|
43788
|
+
config.webUrl = webUrl;
|
|
43789
|
+
saveConfig(config);
|
|
43790
|
+
var { success, output } = installAutostart(config);
|
|
43791
|
+
if (success) {
|
|
43792
|
+
console.log(` autostart : ${wasInstalled ? "refreshed" : "installed"} (runs on login)`);
|
|
42407
43793
|
} else {
|
|
42408
|
-
console.
|
|
43794
|
+
console.warn(` autostart : failed to ${wasInstalled ? "refresh" : "install"}: ${output}`);
|
|
42409
43795
|
}
|
|
42410
43796
|
if (relayUrl) {
|
|
42411
43797
|
const pairing = await ensurePairing(relayUrl, forceRepair, webUrl);
|