sessionpipe 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +298 -18
- package/dist/cli.js.map +4 -4
- package/dist/control/cli.d.ts +1 -1
- package/dist/control/cli.d.ts.map +1 -1
- package/dist/control/daemon.d.ts +18 -0
- package/dist/control/daemon.d.ts.map +1 -1
- package/dist/control/reply.d.ts +2 -0
- package/dist/control/reply.d.ts.map +1 -1
- package/dist/control/sdk.d.ts.map +1 -1
- package/dist/control/stream.d.ts +26 -0
- package/dist/control/stream.d.ts.map +1 -0
- package/dist/hook.js +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +2 -2
- package/dist/worker.js +2 -2
- package/dist/worker.js.map +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// sessionpipe 0.
|
|
2
|
+
// sessionpipe 0.4.0 · Apache-2.0 · https://sessionpipe.org
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __export = (target, all) => {
|
|
5
5
|
for (var name in all)
|
|
@@ -723,7 +723,7 @@ var OPTS = {
|
|
|
723
723
|
timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
|
|
724
724
|
};
|
|
725
725
|
var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
|
|
726
|
-
function claudeDirs(env = process.env, home = HOME) {
|
|
726
|
+
function claudeDirs(env = process.env, home = env.HOME || HOME) {
|
|
727
727
|
const out2 = [];
|
|
728
728
|
const add = (d) => {
|
|
729
729
|
if (!d) return;
|
|
@@ -1946,7 +1946,8 @@ function parseCaps(help2) {
|
|
|
1946
1946
|
hasMode: !!pm,
|
|
1947
1947
|
modes: pm ? [...pm.matchAll(/"([A-Za-z]+)"/g)].map((m) => m[1]) : [],
|
|
1948
1948
|
promptsNone: !!pp && /"none"/.test(pp),
|
|
1949
|
-
fork: !!optionBlock(help2, "--fork-session")
|
|
1949
|
+
fork: !!optionBlock(help2, "--fork-session"),
|
|
1950
|
+
stream: !!optionBlock(help2, "--include-partial-messages")
|
|
1950
1951
|
};
|
|
1951
1952
|
}
|
|
1952
1953
|
function modeFlags(mode, caps) {
|
|
@@ -2148,7 +2149,26 @@ function runClaude(bin, args2, o) {
|
|
|
2148
2149
|
}
|
|
2149
2150
|
}
|
|
2150
2151
|
};
|
|
2152
|
+
let partial = "";
|
|
2151
2153
|
child.stdout?.on("data", (b) => {
|
|
2154
|
+
if (o.onLine) {
|
|
2155
|
+
const chunk = partial + b.toString("utf8");
|
|
2156
|
+
const lines = chunk.split("\n");
|
|
2157
|
+
partial = lines.pop() ?? "";
|
|
2158
|
+
for (const line of lines) {
|
|
2159
|
+
if (!line.trim()) continue;
|
|
2160
|
+
try {
|
|
2161
|
+
o.onLine(line);
|
|
2162
|
+
} catch {
|
|
2163
|
+
}
|
|
2164
|
+
if (line.includes('"type":"result"')) {
|
|
2165
|
+
out2 = [Buffer.from(line)];
|
|
2166
|
+
outLen = line.length;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
if (partial.length > OUT_CAP) partial = "";
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2152
2172
|
if (tooBig) return;
|
|
2153
2173
|
if (outLen + b.length > OUT_CAP) {
|
|
2154
2174
|
tooBig = true;
|
|
@@ -2182,7 +2202,20 @@ function runClaude(bin, args2, o) {
|
|
|
2182
2202
|
};
|
|
2183
2203
|
child.on("error", (e) => finish({ error: e.message }));
|
|
2184
2204
|
child.on("exit", (code, signal) => setTimeout(() => finish(result(code, signal)), 2e3).unref());
|
|
2185
|
-
child.on("close", (code, signal) =>
|
|
2205
|
+
child.on("close", (code, signal) => {
|
|
2206
|
+
if (o.onLine && partial.trim()) {
|
|
2207
|
+
try {
|
|
2208
|
+
o.onLine(partial);
|
|
2209
|
+
} catch {
|
|
2210
|
+
}
|
|
2211
|
+
if (partial.includes('"type":"result"')) {
|
|
2212
|
+
out2 = [Buffer.from(partial)];
|
|
2213
|
+
outLen = partial.length;
|
|
2214
|
+
}
|
|
2215
|
+
partial = "";
|
|
2216
|
+
}
|
|
2217
|
+
finish(result(code, signal));
|
|
2218
|
+
});
|
|
2186
2219
|
});
|
|
2187
2220
|
}
|
|
2188
2221
|
function parseResult(stdout) {
|
|
@@ -2850,13 +2883,14 @@ var SKEW_MS = 5 * 6e4;
|
|
|
2850
2883
|
var MACHINE_MAX_AGE_MS = 24 * 36e5;
|
|
2851
2884
|
var RECEIVER_MAX_AGE_MS = 10 * 6e4;
|
|
2852
2885
|
var NONCE_TTL_MS = 25 * 36e5;
|
|
2853
|
-
var CMD_MAX =
|
|
2854
|
-
var TEXT_MAX =
|
|
2886
|
+
var CMD_MAX = 48e3;
|
|
2887
|
+
var TEXT_MAX = 2e4;
|
|
2855
2888
|
var NOTE_MAX = 2e3;
|
|
2856
2889
|
var GRANT_STR_MAX = 2e3;
|
|
2857
2890
|
var COMMAND_TYPE = "sessionpipe.control";
|
|
2858
2891
|
var GRANT_TYPE = "sessionpipe.grant";
|
|
2859
|
-
var CONTROL_KINDS = ["prompt", "permission.answer", "cancel", "start"];
|
|
2892
|
+
var CONTROL_KINDS = ["prompt", "permission.answer", "cancel", "start", "key.add"];
|
|
2893
|
+
var KEY_STORE_SESSION = "sessionpipe:keys";
|
|
2860
2894
|
var ES256 = -7;
|
|
2861
2895
|
var RS256 = -257;
|
|
2862
2896
|
var NONCE_RE = /^[A-Za-z0-9_-]{22,64}$/;
|
|
@@ -3066,7 +3100,20 @@ function parseCommand(str) {
|
|
|
3066
3100
|
if (has2("text") || has2("for") || has2("decision") || has2("cwd"))
|
|
3067
3101
|
return bad("bad_fields", "a cancel carries nothing");
|
|
3068
3102
|
break;
|
|
3103
|
+
case "key.add": {
|
|
3104
|
+
if (c.session !== KEY_STORE_SESSION) return bad("bad_session", `a key.add names ${KEY_STORE_SESSION}`);
|
|
3105
|
+
const k = c.key;
|
|
3106
|
+
const b64 = (v, min, max) => typeof v === "string" && v.length >= min && v.length <= max && /^[A-Za-z0-9_-]+$/.test(v);
|
|
3107
|
+
if (!k || typeof k !== "object" || !b64(k.id, 16, 1400) || k.alg !== -7 && k.alg !== -257 || !b64(k.spki, 40, 2e3))
|
|
3108
|
+
return bad("bad_fields", "a key.add carries the key: id, alg (-7 or -257) and spki");
|
|
3109
|
+
if (has2("name") && (typeof c.name !== "string" || !c.name.trim() || c.name.length > 80))
|
|
3110
|
+
return bad("bad_fields", "a device name is text, at most 80 characters");
|
|
3111
|
+
if (has2("text") || has2("for") || has2("decision") || has2("cwd"))
|
|
3112
|
+
return bad("bad_fields", "a key.add carries the key and a name");
|
|
3113
|
+
break;
|
|
3114
|
+
}
|
|
3069
3115
|
}
|
|
3116
|
+
if (c.kind !== "key.add" && (has2("key") || has2("name"))) return bad("bad_fields", "only a key.add carries a key");
|
|
3070
3117
|
if (has2("note") && (typeof c.note !== "string" || c.note.length > NOTE_MAX))
|
|
3071
3118
|
return bad("bad_fields", `a note is text, at most ${NOTE_MAX} characters`);
|
|
3072
3119
|
return { ok: true, cmd: c };
|
|
@@ -3217,6 +3264,7 @@ async function awaitAnswer(file, from, o) {
|
|
|
3217
3264
|
}
|
|
3218
3265
|
const a = answerIn(lines);
|
|
3219
3266
|
if (a.done) return a.text || null;
|
|
3267
|
+
if (a.text) o.onText?.(a.text);
|
|
3220
3268
|
}
|
|
3221
3269
|
await sleep2(o.pollMs ?? 500);
|
|
3222
3270
|
}
|
|
@@ -3392,6 +3440,83 @@ function readState(file) {
|
|
|
3392
3440
|
}
|
|
3393
3441
|
var writeState = (file, s) => writePrivate(file, s);
|
|
3394
3442
|
|
|
3443
|
+
// src/control/stream.ts
|
|
3444
|
+
function streamJsonPiece(line) {
|
|
3445
|
+
let j;
|
|
3446
|
+
try {
|
|
3447
|
+
j = JSON.parse(line);
|
|
3448
|
+
} catch {
|
|
3449
|
+
return null;
|
|
3450
|
+
}
|
|
3451
|
+
if (j.type !== "stream_event" || !j.event) return null;
|
|
3452
|
+
if (j.parent_tool_use_id) return null;
|
|
3453
|
+
const e = j.event;
|
|
3454
|
+
if (e.type === "content_block_delta" && e.delta?.type === "text_delta" && typeof e.delta.text === "string")
|
|
3455
|
+
return { text: e.delta.text };
|
|
3456
|
+
if (e.type === "message_start") return { boundary: true };
|
|
3457
|
+
return null;
|
|
3458
|
+
}
|
|
3459
|
+
var AnswerStream = class {
|
|
3460
|
+
constructor(send, everyMs = 400) {
|
|
3461
|
+
this.send = send;
|
|
3462
|
+
this.everyMs = everyMs;
|
|
3463
|
+
}
|
|
3464
|
+
send;
|
|
3465
|
+
everyMs;
|
|
3466
|
+
text = "";
|
|
3467
|
+
sent = "";
|
|
3468
|
+
seq = 0;
|
|
3469
|
+
timer = null;
|
|
3470
|
+
inFlight = false;
|
|
3471
|
+
closed = false;
|
|
3472
|
+
/** A stream-json line from a headless run. */
|
|
3473
|
+
feed(line) {
|
|
3474
|
+
const p = streamJsonPiece(line);
|
|
3475
|
+
if (!p) return;
|
|
3476
|
+
if (p.boundary) {
|
|
3477
|
+
if (this.text && !this.text.endsWith("\n\n")) this.text += "\n\n";
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3480
|
+
if (p.text) {
|
|
3481
|
+
this.text += p.text;
|
|
3482
|
+
this.schedule();
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
/** The whole answer so far (a live session's transcript). */
|
|
3486
|
+
set(text) {
|
|
3487
|
+
if (text === this.text) return;
|
|
3488
|
+
this.text = text;
|
|
3489
|
+
this.schedule();
|
|
3490
|
+
}
|
|
3491
|
+
/** No more progress: the final ack follows. */
|
|
3492
|
+
close() {
|
|
3493
|
+
this.closed = true;
|
|
3494
|
+
if (this.timer) clearTimeout(this.timer);
|
|
3495
|
+
this.timer = null;
|
|
3496
|
+
}
|
|
3497
|
+
schedule() {
|
|
3498
|
+
if (this.closed || this.timer || this.inFlight) return;
|
|
3499
|
+
this.timer = setTimeout(() => {
|
|
3500
|
+
this.timer = null;
|
|
3501
|
+
void this.flush();
|
|
3502
|
+
}, this.everyMs);
|
|
3503
|
+
this.timer.unref?.();
|
|
3504
|
+
}
|
|
3505
|
+
async flush() {
|
|
3506
|
+
const t = this.text.trim();
|
|
3507
|
+
if (this.closed || !t || t === this.sent) return;
|
|
3508
|
+
this.inFlight = true;
|
|
3509
|
+
this.sent = t;
|
|
3510
|
+
try {
|
|
3511
|
+
await this.send(t, ++this.seq);
|
|
3512
|
+
} catch {
|
|
3513
|
+
} finally {
|
|
3514
|
+
this.inFlight = false;
|
|
3515
|
+
}
|
|
3516
|
+
if (this.text.trim() !== this.sent) this.schedule();
|
|
3517
|
+
}
|
|
3518
|
+
};
|
|
3519
|
+
|
|
3395
3520
|
// src/control/daemon.ts
|
|
3396
3521
|
var {
|
|
3397
3522
|
claudeEnv: claudeEnv2,
|
|
@@ -3404,7 +3529,7 @@ var {
|
|
|
3404
3529
|
recentlyWritten: recentlyWritten2,
|
|
3405
3530
|
sessionFolder: sessionFolder2
|
|
3406
3531
|
} = claude_code_control_exports;
|
|
3407
|
-
var VERSION = true ? "0.
|
|
3532
|
+
var VERSION = true ? "0.4.0" : "0.0.0";
|
|
3408
3533
|
var MAX_REPLY = 2e4;
|
|
3409
3534
|
var HELLO_EVERY_MS = 10 * 6e4;
|
|
3410
3535
|
var TURN_FALLBACK_MS = 30 * 6e4;
|
|
@@ -3418,6 +3543,12 @@ var CLOSES_ATTENTION = /* @__PURE__ */ new Set([
|
|
|
3418
3543
|
"UserPromptSubmit",
|
|
3419
3544
|
"SessionEnd"
|
|
3420
3545
|
]);
|
|
3546
|
+
function sessionToolsFrom(v) {
|
|
3547
|
+
if (!Array.isArray(v)) return [];
|
|
3548
|
+
return [
|
|
3549
|
+
...new Set(v.filter((t) => typeof t === "string" && /^mcp__[A-Za-z0-9_-]{1,64}$/.test(t)))
|
|
3550
|
+
].slice(0, 8);
|
|
3551
|
+
}
|
|
3421
3552
|
var LIMITS = { startConcurrent: 2, startPerHour: 10, headless: 3 };
|
|
3422
3553
|
var ControlDaemon = class {
|
|
3423
3554
|
constructor(cfg, dir, sock, deps) {
|
|
@@ -3449,6 +3580,8 @@ var ControlDaemon = class {
|
|
|
3449
3580
|
helloTimer = null;
|
|
3450
3581
|
helloSoon = null;
|
|
3451
3582
|
aborts = /* @__PURE__ */ new Set();
|
|
3583
|
+
/** Per receiver: the MCP servers its sessions may use unattended (its well-known). */
|
|
3584
|
+
sessionTools = /* @__PURE__ */ new Map();
|
|
3452
3585
|
startsRunning = 0;
|
|
3453
3586
|
startTimes = [];
|
|
3454
3587
|
procs = 0;
|
|
@@ -3729,7 +3862,31 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
3729
3862
|
}, 1500);
|
|
3730
3863
|
this.helloSoon.unref();
|
|
3731
3864
|
}
|
|
3865
|
+
/** Read each receiver's well-known for its session tools (at start, then with every hello). */
|
|
3866
|
+
async refreshSessionTools() {
|
|
3867
|
+
for (const r of this.cfg.receivers) {
|
|
3868
|
+
try {
|
|
3869
|
+
const res = await this.deps.fetch(`${r.url.replace(/\/$/, "")}/.well-known/sessionpipe`, {
|
|
3870
|
+
signal: AbortSignal.timeout(8e3)
|
|
3871
|
+
});
|
|
3872
|
+
if (!res.ok) continue;
|
|
3873
|
+
const wk = await res.json();
|
|
3874
|
+
const tools = sessionToolsFrom(wk.control?.session_tools);
|
|
3875
|
+
const key = r.control + r.machine;
|
|
3876
|
+
if (JSON.stringify(tools) !== JSON.stringify(this.sessionTools.get(key) ?? []))
|
|
3877
|
+
this.deps.log(
|
|
3878
|
+
`${r.url}: sessions it messages may use ${tools.length ? tools.join(", ") : "nothing extra"} without asking`
|
|
3879
|
+
);
|
|
3880
|
+
this.sessionTools.set(key, tools);
|
|
3881
|
+
} catch {
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
allowedFor(r) {
|
|
3886
|
+
return this.sessionTools.get(r.control + r.machine) ?? [];
|
|
3887
|
+
}
|
|
3732
3888
|
async helloAll() {
|
|
3889
|
+
void this.refreshSessionTools();
|
|
3733
3890
|
const waiting = [...this.live].filter(([, l]) => l.waiter).map(([k]) => k);
|
|
3734
3891
|
const modes = ["waiter", "turn", "resume", "fork", ...this.deps.sdk ? ["sdk"] : []];
|
|
3735
3892
|
for (const r of this.cfg.receivers) {
|
|
@@ -3825,6 +3982,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
3825
3982
|
return this.finish(r, m.id, { outcome: "refused", code: v.code, detail: v.why });
|
|
3826
3983
|
}
|
|
3827
3984
|
const cmd2 = v.cmd;
|
|
3985
|
+
if (cmd2.kind === "key.add") return this.addKey(r, m.id, cmd2, sent);
|
|
3828
3986
|
const ref = parseSessionRef(cmd2.session);
|
|
3829
3987
|
const kp = kindPath(cmd2.kind, ref.harness);
|
|
3830
3988
|
if (typeof kp === "object") return this.finish(r, m.id, { outcome: "unsupported", detail: kp.unsupported });
|
|
@@ -4015,15 +4173,26 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4015
4173
|
await this.serial(ref.id, async () => {
|
|
4016
4174
|
const target = this.state.copies[ref.id] ?? ref.id;
|
|
4017
4175
|
const resume = mode === "fork" ? ["--resume", target, "--fork-session"] : ["--resume", target];
|
|
4018
|
-
const
|
|
4176
|
+
const allow = this.allowedFor(r);
|
|
4177
|
+
const args2 = [
|
|
4178
|
+
"-p",
|
|
4179
|
+
...this.outputArgs(claude.caps),
|
|
4180
|
+
...mf.args,
|
|
4181
|
+
...allow.length ? [`--allowedTools=${allow.join(",")}`] : [],
|
|
4182
|
+
...resume,
|
|
4183
|
+
frame(cmd2.text, r.url)
|
|
4184
|
+
];
|
|
4019
4185
|
this.deps.log(`${id.slice(-8)}: claude -p ${[...mf.args, ...resume].join(" ")} in ${place.folder}`);
|
|
4186
|
+
const stream = this.answerStream(r, id);
|
|
4020
4187
|
const res = await this.slot(
|
|
4021
4188
|
() => this.deps.run(claude.bin, args2, {
|
|
4022
4189
|
cwd: place.folder,
|
|
4023
4190
|
env: claudeEnv2(place.transcript?.configDir, this.deps.env),
|
|
4024
|
-
timeoutMs: JOB_TIMEOUT_MS2
|
|
4191
|
+
timeoutMs: JOB_TIMEOUT_MS2,
|
|
4192
|
+
...claude.caps.stream ? { onLine: (l) => stream.feed(l) } : {}
|
|
4025
4193
|
})
|
|
4026
4194
|
);
|
|
4195
|
+
stream.close();
|
|
4027
4196
|
this.lastEnd.set(target, this.deps.now());
|
|
4028
4197
|
const out2 = this.outcome(res);
|
|
4029
4198
|
if ("failed" in out2) return this.finish(r, id, { outcome: "failed", detail: out2.failed });
|
|
@@ -4061,6 +4230,33 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4061
4230
|
const text = typeof j.result === "string" && j.result.trim() ? j.result : "(Claude Code finished without a written reply.)";
|
|
4062
4231
|
return { reply: text + tail2, ...typeof j.session_id === "string" ? { session: j.session_id } : {} };
|
|
4063
4232
|
}
|
|
4233
|
+
addKey(r, id, cmd2, sent) {
|
|
4234
|
+
const k = cmd2.key;
|
|
4235
|
+
const name = typeof cmd2.name === "string" ? cmd2.name.trim().slice(0, 80) : void 0;
|
|
4236
|
+
const rec = this.cfg.receivers.find((x) => x.control === r.control && x.machine === r.machine) ?? r;
|
|
4237
|
+
if (rec.keys.some((x) => x.id === k.id))
|
|
4238
|
+
return this.finish(r, id, { outcome: "delivered", detail: "that device was already trusted" }, sent);
|
|
4239
|
+
rec.keys.push({
|
|
4240
|
+
id: k.id,
|
|
4241
|
+
alg: k.alg,
|
|
4242
|
+
spki: k.spki,
|
|
4243
|
+
...name ? { name } : {},
|
|
4244
|
+
added_at: new Date(this.deps.now()).toISOString()
|
|
4245
|
+
});
|
|
4246
|
+
try {
|
|
4247
|
+
writeControl(this.cfg, this.deps.env);
|
|
4248
|
+
} catch (e) {
|
|
4249
|
+
rec.keys = rec.keys.filter((x) => x.id !== k.id);
|
|
4250
|
+
return this.finish(r, id, {
|
|
4251
|
+
outcome: "failed",
|
|
4252
|
+
detail: `couldn't save the key: ${String(e?.message || e)}`
|
|
4253
|
+
});
|
|
4254
|
+
}
|
|
4255
|
+
this.deps.log(
|
|
4256
|
+
`trusted a new device${name ? ` (${name})` : ""} on ${r.url}: ${k.id.slice(0, 12)}\u2026, vouched for by a key it already trusted`
|
|
4257
|
+
);
|
|
4258
|
+
this.finish(r, id, { outcome: "delivered", detail: `trusted ${name || "a new device"}` }, sent);
|
|
4259
|
+
}
|
|
4064
4260
|
/** Where the session's transcript ends right now: its answer starts after this. */
|
|
4065
4261
|
answerFrom(session) {
|
|
4066
4262
|
const ref = parseSessionRef(session);
|
|
@@ -4075,14 +4271,38 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4075
4271
|
followAnswer(r, id, mode, session, from, sent) {
|
|
4076
4272
|
this.deps.log(`${id.slice(-8)}: in ${session.slice(0, 24)} via ${mode} ${this.deps.now() - sent} ms after send`);
|
|
4077
4273
|
this.finish(r, id, { outcome: "taken" });
|
|
4274
|
+
const stream = this.answerStream(r, id);
|
|
4078
4275
|
void (async () => {
|
|
4079
4276
|
const answer = from ? await awaitAnswer(from.file, from.offset, {
|
|
4080
4277
|
timeoutMs: this.deps.answerWaitMs ?? ANSWER_WAIT_MS,
|
|
4081
|
-
now: this.deps.now
|
|
4278
|
+
now: this.deps.now,
|
|
4279
|
+
onText: (t) => stream.set(t)
|
|
4082
4280
|
}) : null;
|
|
4281
|
+
stream.close();
|
|
4083
4282
|
this.finish(r, id, { outcome: "delivered", mode, ...answer ? { reply: answer } : {} }, sent);
|
|
4084
4283
|
})().catch((e) => this.finish(r, id, { outcome: "delivered", mode, detail: String(e?.message || e) }));
|
|
4085
4284
|
}
|
|
4285
|
+
/** The answer so far, sent as a `progress` ack: whole, with a rising seq, at most
|
|
4286
|
+
* one in flight. Fire-and-forget — the final ack carries everything. */
|
|
4287
|
+
answerStream(r, id) {
|
|
4288
|
+
return new AnswerStream(async (text, seq) => {
|
|
4289
|
+
await this.post(r, "/ack", {
|
|
4290
|
+
acks: [
|
|
4291
|
+
{
|
|
4292
|
+
id,
|
|
4293
|
+
outcome: "progress",
|
|
4294
|
+
at: new Date(this.deps.now()).toISOString(),
|
|
4295
|
+
seq,
|
|
4296
|
+
reply: redactSecrets(text).slice(0, MAX_REPLY)
|
|
4297
|
+
}
|
|
4298
|
+
]
|
|
4299
|
+
});
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
/** How a headless run prints: streamed where this Claude Code can, else one JSON at the end. */
|
|
4303
|
+
outputArgs(caps) {
|
|
4304
|
+
return caps.stream ? ["--output-format", "stream-json", "--verbose", "--include-partial-messages"] : ["--output-format", "json"];
|
|
4305
|
+
}
|
|
4086
4306
|
limits() {
|
|
4087
4307
|
const l = this.cfg.limits ?? {};
|
|
4088
4308
|
const n = (v, d) => typeof v === "number" && Number.isInteger(v) && v >= 0 ? v : d;
|
|
@@ -4139,7 +4359,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4139
4359
|
if (this.deps.sdk) {
|
|
4140
4360
|
const sdk = this.deps.sdk;
|
|
4141
4361
|
const out3 = await this.slot(async () => {
|
|
4142
|
-
const s = sdk.start({ session: ref.id, cwd, mode: this.cfg.mode });
|
|
4362
|
+
const s = sdk.start({ session: ref.id, cwd, mode: this.cfg.mode, allowedTools: this.allowedFor(r) });
|
|
4143
4363
|
this.sdkSessions.set(ref.id, s);
|
|
4144
4364
|
return s.send(frame(cmd2.text, r.url));
|
|
4145
4365
|
});
|
|
@@ -4155,13 +4375,28 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
|
|
|
4155
4375
|
return this.finish(r, id, { outcome: "failed", detail: "Claude Code isn't installed on this machine" });
|
|
4156
4376
|
const mf = modeFlags2(this.cfg.mode, claude.caps);
|
|
4157
4377
|
if ("error" in mf) return this.finish(r, id, { outcome: "refused", code: "mode", detail: mf.error });
|
|
4378
|
+
const stream = this.answerStream(r, id);
|
|
4158
4379
|
const res = await this.slot(
|
|
4159
4380
|
() => this.deps.run(
|
|
4160
4381
|
claude.bin,
|
|
4161
|
-
[
|
|
4162
|
-
|
|
4382
|
+
[
|
|
4383
|
+
"-p",
|
|
4384
|
+
...this.outputArgs(claude.caps),
|
|
4385
|
+
...mf.args,
|
|
4386
|
+
...this.allowedFor(r).length ? [`--allowedTools=${this.allowedFor(r).join(",")}`] : [],
|
|
4387
|
+
"--session-id",
|
|
4388
|
+
ref.id,
|
|
4389
|
+
frame(cmd2.text, r.url)
|
|
4390
|
+
],
|
|
4391
|
+
{
|
|
4392
|
+
cwd,
|
|
4393
|
+
env: claudeEnv2(void 0, this.deps.env),
|
|
4394
|
+
timeoutMs: JOB_TIMEOUT_MS2,
|
|
4395
|
+
...claude.caps.stream ? { onLine: (l) => stream.feed(l) } : {}
|
|
4396
|
+
}
|
|
4163
4397
|
)
|
|
4164
4398
|
);
|
|
4399
|
+
stream.close();
|
|
4165
4400
|
this.lastEnd.set(ref.id, this.deps.now());
|
|
4166
4401
|
const out2 = this.outcome(res);
|
|
4167
4402
|
if ("failed" in out2) return this.finish(r, id, { outcome: "failed", detail: out2.failed });
|
|
@@ -4226,7 +4461,7 @@ import { randomBytes as randomBytes2 } from "node:crypto";
|
|
|
4226
4461
|
import { existsSync as existsSync10, mkdirSync as mkdirSync9, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "node:fs";
|
|
4227
4462
|
import os4 from "node:os";
|
|
4228
4463
|
import path15 from "node:path";
|
|
4229
|
-
var VERSION2 = true ? "0.
|
|
4464
|
+
var VERSION2 = true ? "0.4.0" : "0.0.0";
|
|
4230
4465
|
async function json(r) {
|
|
4231
4466
|
try {
|
|
4232
4467
|
return await r.json();
|
|
@@ -4462,7 +4697,8 @@ async function loadSdk(o) {
|
|
|
4462
4697
|
start({
|
|
4463
4698
|
session,
|
|
4464
4699
|
cwd,
|
|
4465
|
-
mode
|
|
4700
|
+
mode,
|
|
4701
|
+
allowedTools
|
|
4466
4702
|
}) {
|
|
4467
4703
|
const queue = [];
|
|
4468
4704
|
let wake = null;
|
|
@@ -4481,6 +4717,7 @@ async function loadSdk(o) {
|
|
|
4481
4717
|
sessionId: session,
|
|
4482
4718
|
cwd,
|
|
4483
4719
|
permissionMode: mode === "auto" ? "auto" : "dontAsk",
|
|
4720
|
+
...allowedTools?.length ? { allowedTools } : {},
|
|
4484
4721
|
...o.claudeBin ? { pathToClaudeCodeExecutable: o.claudeBin } : {},
|
|
4485
4722
|
env: { ...o.env ?? process.env, SESSIONPIPE_CONTROL_JOB: "1" }
|
|
4486
4723
|
}
|
|
@@ -4556,8 +4793,34 @@ ${RE_ARM}`;
|
|
|
4556
4793
|
// src/control/cli.ts
|
|
4557
4794
|
var { detectCaps: detectCaps2, findClaude: findClaude2, runClaude: runClaude2 } = claude_code_control_exports;
|
|
4558
4795
|
var CONTROL_HELP = ` sessionpipe control pair <receiver> [--folder DIR]\u2026 [--mode safe|auto] [--token T] [--name N] [--no-service]
|
|
4796
|
+
sessionpipe control mode auto|safe (how sessions run when you message them)
|
|
4559
4797
|
sessionpipe control status | keys [remove <id>] | off [<receiver>] | run
|
|
4560
4798
|
sessionpipe wait [--session <harness>:<id>] (a session runs this in the background)`;
|
|
4799
|
+
var MODE_TEXT = ` auto Claude Code's auto mode: the session reads, edits and runs commands, and Claude Code's
|
|
4800
|
+
own safety check stops what looks dangerous. Pick this to get work done from your phone.
|
|
4801
|
+
safe Only what each folder's Claude Code settings already allow: with no allow rules, a
|
|
4802
|
+
session can't even read a file. Pick this if this machine holds things no agent
|
|
4803
|
+
should touch unattended.`;
|
|
4804
|
+
async function askMode(out2) {
|
|
4805
|
+
if (!process.stdin.isTTY) {
|
|
4806
|
+
out2(" Mode: safe for now. Run `sessionpipe control mode auto` to let sessions you message work unattended.");
|
|
4807
|
+
return "safe";
|
|
4808
|
+
}
|
|
4809
|
+
out2("");
|
|
4810
|
+
out2(" When you message a session on this machine from your phone, how may it run?");
|
|
4811
|
+
out2(MODE_TEXT);
|
|
4812
|
+
const { createInterface } = await import("node:readline/promises");
|
|
4813
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
4814
|
+
try {
|
|
4815
|
+
for (; ; ) {
|
|
4816
|
+
const a = (await rl.question(" auto or safe? [auto] ")).trim().toLowerCase();
|
|
4817
|
+
if (a === "" || a === "a" || a === "auto") return "auto";
|
|
4818
|
+
if (a === "s" || a === "safe") return "safe";
|
|
4819
|
+
}
|
|
4820
|
+
} finally {
|
|
4821
|
+
rl.close();
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4561
4824
|
async function controlMain(argv, out2, distDir2) {
|
|
4562
4825
|
const flag2 = (n) => {
|
|
4563
4826
|
const i = argv.indexOf(n);
|
|
@@ -4581,6 +4844,7 @@ ${CONTROL_HELP}`);
|
|
|
4581
4844
|
const folders = flags("--folder");
|
|
4582
4845
|
const existing = readControl();
|
|
4583
4846
|
if (!folders.length && !existing?.folders.length) folders.push(process.cwd());
|
|
4847
|
+
const mode = flag2("--mode") === "auto" || flag2("--mode") === "safe" ? flag2("--mode") : existing?.mode && existing.receivers.length ? existing.mode : await askMode(out2);
|
|
4584
4848
|
await pair({
|
|
4585
4849
|
url,
|
|
4586
4850
|
token,
|
|
@@ -4596,7 +4860,7 @@ ${CONTROL_HELP}`);
|
|
|
4596
4860
|
);
|
|
4597
4861
|
},
|
|
4598
4862
|
folders,
|
|
4599
|
-
mode
|
|
4863
|
+
mode,
|
|
4600
4864
|
name: flag2("--name") ?? existing?.name ?? cfg.machine ?? os5.hostname(),
|
|
4601
4865
|
out: out2
|
|
4602
4866
|
});
|
|
@@ -4615,6 +4879,22 @@ ${CONTROL_HELP}`);
|
|
|
4615
4879
|
else out2(` Daemon: ${installService({ node: process.execPath, cli })}`);
|
|
4616
4880
|
return;
|
|
4617
4881
|
}
|
|
4882
|
+
case "mode": {
|
|
4883
|
+
const want = argv[2];
|
|
4884
|
+
if (want !== "auto" && want !== "safe") {
|
|
4885
|
+
const c2 = readControl();
|
|
4886
|
+
return out2(` mode: ${c2?.mode ?? "not set up"}
|
|
4887
|
+
usage: sessionpipe control mode auto|safe
|
|
4888
|
+
${MODE_TEXT}`);
|
|
4889
|
+
}
|
|
4890
|
+
const c = readControl();
|
|
4891
|
+
if (!c) return out2(" control isn't set up on this machine: run `sessionpipe control pair <receiver>` first");
|
|
4892
|
+
c.mode = want;
|
|
4893
|
+
writeControl(c);
|
|
4894
|
+
return out2(
|
|
4895
|
+
` \u2713 Sessions you message now run in ${want} mode${want === "auto" ? " (Claude Code's auto mode decides what's safe)" : " (only what each folder's settings already allow)"}.`
|
|
4896
|
+
);
|
|
4897
|
+
}
|
|
4618
4898
|
case "keys": {
|
|
4619
4899
|
const c = readControl();
|
|
4620
4900
|
if (argv[2] === "remove" && argv[3]) {
|
|
@@ -4813,7 +5093,7 @@ async function tellDaemon(state2, job, session, transcript) {
|
|
|
4813
5093
|
300
|
|
4814
5094
|
);
|
|
4815
5095
|
}
|
|
4816
|
-
var VERSION3 = true ? "0.
|
|
5096
|
+
var VERSION3 = true ? "0.4.0" : "0.0.0";
|
|
4817
5097
|
function enqueueJob(state2, job) {
|
|
4818
5098
|
const jobs = jobsDir(state2);
|
|
4819
5099
|
mkdirSync10(jobs, { recursive: true, mode: 448 });
|