sessionpipe 0.2.0 → 0.3.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/README.md CHANGED
@@ -33,7 +33,7 @@ enroll at this terminal; the receiver can queue a message but can never forge on
33
33
  ([spec](https://sessionpipe.org/protocol/control/)).
34
34
 
35
35
  ```sh
36
- sessionpipe control pair https://receiver.example --folder ~/code # confirm with your passkey; installs the daemon
36
+ sessionpipe control pair spacesheep.dev # one link: approve with your passkey; installs the daemon (this folder by default)
37
37
  sessionpipe control status # receivers, parked waiters, sessions mid-turn, open permission prompts
38
38
  sessionpipe control keys [remove <id>]
39
39
  sessionpipe control off # tell the receiver, forget its token and keys
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe 0.2.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe 0.3.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)
@@ -3404,7 +3404,7 @@ var {
3404
3404
  recentlyWritten: recentlyWritten2,
3405
3405
  sessionFolder: sessionFolder2
3406
3406
  } = claude_code_control_exports;
3407
- var VERSION = true ? "0.2.0" : "0.0.0";
3407
+ var VERSION = true ? "0.3.0" : "0.0.0";
3408
3408
  var MAX_REPLY = 2e4;
3409
3409
  var HELLO_EVERY_MS = 10 * 6e4;
3410
3410
  var TURN_FALLBACK_MS = 30 * 6e4;
@@ -4222,10 +4222,11 @@ function ask(sock, req, timeoutMs) {
4222
4222
 
4223
4223
  // src/control/pair.ts
4224
4224
  import { execFileSync as execFileSync5 } from "node:child_process";
4225
+ import { randomBytes as randomBytes2 } from "node:crypto";
4225
4226
  import { existsSync as existsSync10, mkdirSync as mkdirSync9, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "node:fs";
4226
4227
  import os4 from "node:os";
4227
4228
  import path15 from "node:path";
4228
- var VERSION2 = true ? "0.2.0" : "0.0.0";
4229
+ var VERSION2 = true ? "0.3.0" : "0.0.0";
4229
4230
  async function json(r) {
4230
4231
  try {
4231
4232
  return await r.json();
@@ -4240,23 +4241,34 @@ async function discover(url, f) {
4240
4241
  const wk = await r.json();
4241
4242
  if (!wk.capabilities?.includes("control") || !wk.endpoints?.control || !wk.control?.signing?.rp_id)
4242
4243
  throw new Error(`${base} doesn't serve control yet (its well-known file lists no control.signing)`);
4243
- const ep = wk.endpoints.control;
4244
- return { control: /^https?:\/\//.test(ep) ? ep : base + ep, rpId: wk.control.signing.rp_id };
4244
+ const abs = (ep) => /^https?:\/\//.test(ep) ? ep : base + ep;
4245
+ return {
4246
+ control: abs(wk.endpoints.control),
4247
+ rpId: wk.control.signing.rp_id,
4248
+ open: wk.control.open_pairing === true,
4249
+ events: wk.endpoints.events ? abs(wk.endpoints.events) : null
4250
+ };
4245
4251
  }
4246
4252
  async function pair(o) {
4247
4253
  const f = o.fetch ?? fetch;
4248
4254
  const sleep2 = o.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4249
- const { control, rpId } = await discover(o.url, f);
4255
+ const { control, rpId, open } = await discover(o.url, f);
4256
+ if (!o.token && !open)
4257
+ throw new Error(
4258
+ `${o.url} needs a key to pair: add it as a sink first (\`sessionpipe sink add ${o.url} --token \u2026\`) or pass --token.`
4259
+ );
4260
+ const pollKey = o.token ? null : randomBytes2(32).toString("base64url");
4250
4261
  const cfg = readControl(o.env) ?? { name: o.name, folders: [], mode: o.mode, receivers: [] };
4251
4262
  cfg.name = o.name || cfg.name;
4252
4263
  cfg.mode = o.mode;
4253
4264
  cfg.folders = [.../* @__PURE__ */ new Set([...cfg.folders, ...o.folders.map((d) => path15.resolve(d))])];
4254
4265
  const existing = cfg.receivers.find((r) => r.control === control);
4255
- const auth = { authorization: `Bearer ${o.token}`, "content-type": "application/json" };
4266
+ const auth = { authorization: `Bearer ${o.token ?? pollKey}`, "content-type": "application/json" };
4256
4267
  const started = await f(`${control}/pair`, {
4257
4268
  method: "POST",
4258
- headers: auth,
4269
+ headers: o.token ? auth : { "content-type": "application/json" },
4259
4270
  body: JSON.stringify({
4271
+ ...pollKey ? { poll_key: pollKey } : {},
4260
4272
  ...existing ? { machine: existing.machine } : {},
4261
4273
  name: cfg.name,
4262
4274
  harnesses: ["claude-code"],
@@ -4274,9 +4286,16 @@ async function pair(o) {
4274
4286
  const machine = String(s.machine);
4275
4287
  const code = String(s.code);
4276
4288
  o.out("");
4277
- o.out(` Open this link and confirm with your passkey (Touch ID, Face ID, a security key):`);
4278
- o.out(` ${String(s.url)}`);
4279
- o.out(` The page shows these digits too: ${String(s.check)}`);
4289
+ const digits = String(s.check);
4290
+ const spaced = /^\d{6}$/.test(digits) ? `${digits.slice(0, 3)} ${digits.slice(3)}` : digits;
4291
+ if (pollKey) {
4292
+ o.out(` Open ${String(s.url)} and enter: ${spaced}`);
4293
+ o.out(" then approve with your passkey (Touch ID, Face ID, a security key).");
4294
+ } else {
4295
+ o.out(" Open this link and confirm with your passkey (Touch ID, Face ID, a security key):");
4296
+ o.out(` ${String(s.url)}`);
4297
+ o.out(` The page shows these digits too: ${digits}`);
4298
+ }
4280
4299
  o.out("");
4281
4300
  const until = Date.parse(String(s.expires_at)) || Date.now() + 10 * 6e4;
4282
4301
  for (; ; ) {
@@ -4307,6 +4326,9 @@ async function pair(o) {
4307
4326
  if (!existing) cfg.receivers.push(rec);
4308
4327
  writeControl(cfg, o.env);
4309
4328
  o.out(` \u2713 Paired with ${rec.url} as ${machine} \xB7 ${rec.keys.length} key(s) trusted on this machine`);
4329
+ if (typeof p.account === "string")
4330
+ o.out(` Approved by ${p.account}. If that isn't you, run \`sessionpipe control off\` now.`);
4331
+ if (typeof p.sink_token === "string" && o.addSink) o.addSink(p.sink_token);
4310
4332
  return cfg;
4311
4333
  }
4312
4334
  }
@@ -4545,15 +4567,16 @@ async function controlMain(argv, out2, distDir2) {
4545
4567
  const sub = argv[1];
4546
4568
  switch (sub) {
4547
4569
  case "pair": {
4548
- const url = argv[2];
4549
- if (!url || !/^https?:\/\//.test(url)) return out2(`usage:
4570
+ const raw = argv[2];
4571
+ if (!raw || raw.startsWith("-")) return out2(`usage:
4550
4572
  ${CONTROL_HELP}`);
4573
+ const url = (/^https?:\/\//.test(raw) ? raw : `https://${raw}`).replace(/\/$/, "");
4551
4574
  const cfg = readConfig();
4552
- const sink2 = cfg.sinks.find((s) => s.url.replace(/\/$/, "") === url.replace(/\/$/, ""));
4553
- const token = flag2("--token") ?? sink2?.token;
4554
- if (!token)
4575
+ const sink2 = cfg.sinks.find((s) => s.url.replace(/\/$/, "") === url);
4576
+ const token = flag2("--token") ?? sink2?.token ?? null;
4577
+ if (token !== null && !/^[\x21-\x7e]+$/.test(token))
4555
4578
  return out2(
4556
- ` No token for ${url}: add it as a sink first (\`sessionpipe sink add ${url} --token \u2026\`) or pass --token.`
4579
+ " That token has a character a key never has (a placeholder like ss_\u2026 pasted as is?). Leave --token out to pair with a link instead."
4557
4580
  );
4558
4581
  const folders = flags("--folder");
4559
4582
  const existing = readControl();
@@ -4561,6 +4584,17 @@ ${CONTROL_HELP}`);
4561
4584
  await pair({
4562
4585
  url,
4563
4586
  token,
4587
+ // The key the receiver handed over at pairing becomes this machine's sink for it,
4588
+ // state only (tier 0), unless one is configured already.
4589
+ addSink: (t) => {
4590
+ const now = readConfig();
4591
+ if (now.sinks.some((x) => x.url.replace(/\/$/, "") === url)) return;
4592
+ now.sinks.push({ name: new URL(url).host, url, tier: 0, pii: false, control: false, token: t });
4593
+ writeConfig(now);
4594
+ out2(
4595
+ ` \u2713 Sink ${new URL(url).host} added (tier 0: session state, no conversation text). \`sessionpipe sink add ${url} --tier 2\` sends transcripts too.`
4596
+ );
4597
+ },
4564
4598
  folders,
4565
4599
  mode: flag2("--mode") === "auto" ? "auto" : existing?.mode ?? "safe",
4566
4600
  name: flag2("--name") ?? existing?.name ?? cfg.machine ?? os5.hostname(),
@@ -4779,7 +4813,7 @@ async function tellDaemon(state2, job, session, transcript) {
4779
4813
  300
4780
4814
  );
4781
4815
  }
4782
- var VERSION3 = true ? "0.2.0" : "0.0.0";
4816
+ var VERSION3 = true ? "0.3.0" : "0.0.0";
4783
4817
  function enqueueJob(state2, job) {
4784
4818
  const jobs = jobsDir(state2);
4785
4819
  mkdirSync10(jobs, { recursive: true, mode: 448 });