@paigy/harness 0.1.1 → 0.1.5

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
@@ -193,6 +193,22 @@ the window.
193
193
  Pairing is one-time and lives in `~/.paigy` — the app reads it, never mints it. If
194
194
  you've never paired: `npx -y -p @paigy/mcp paigy-mcp-onboard`.
195
195
 
196
+ ## Uninstall
197
+
198
+ `curl -fsSL https://paigy.ai/uninstall.sh | sh` removes the harness surface (service,
199
+ global package, workspace list, the machine's Desktop slot — other agents' slots
200
+ survive). Then revoke the machine on the phone's Agents screen: server-side tokens
201
+ outlive local files.
202
+
203
+ The FULL Paigy strip (tested 2026-08-03) additionally removes, in order: every
204
+ `~/.paigy` slot (unpairs ALL local agents — re-pairing resets voices), the ACP
205
+ adapters (`npm rm -g @agentclientprotocol/{claude-agent-acp,codex-acp}`), Electron
206
+ userData, the statusline block + `paigy@paigy` plugin enablement + marketplace +
207
+ `mcp__paigy__*` permission entries in `~/.claude/settings.json`, the project
208
+ `.mcp.json` paigy server, the plugin cache/registry entries, and `~/.npm/_npx` (so
209
+ `@paigy/mcp@latest` can't serve stale). Hooks can recreate `~/.paigy` — delete it
210
+ LAST, after the hooks are gone.
211
+
196
212
  ## Known gaps (#804)
197
213
 
198
214
  - **Protocol churn is the standing risk.** Neither `stream-json` nor ACP adapter
package/dist/cli.js CHANGED
@@ -30857,7 +30857,7 @@ async function setTaskState(notificationId, state, opts = {}) {
30857
30857
  // src/cli.ts
30858
30858
  var import_qrcode = __toESM(require_lib(), 1);
30859
30859
  import { hostname } from "os";
30860
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
30860
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
30861
30861
  import { execSync } from "child_process";
30862
30862
  import { homedir as homedir5 } from "os";
30863
30863
  import { join as join4 } from "path";
@@ -31729,9 +31729,10 @@ async function drainInput(state, deps = {}) {
31729
31729
  return [];
31730
31730
  }
31731
31731
  const ours = (parentId) => state.exclusive || parentId === state.parentId;
31732
+ const claimable = (notificationId) => !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
31732
31733
  const mine = [
31733
- ...work.replies.filter((r) => ours(r.parentId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: r.answer })),
31734
- ...work.requests.filter((r) => ours(r.parentId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: { kind: "text", text: r.text } }))
31734
+ ...work.replies.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: r.answer })),
31735
+ ...work.requests.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: { kind: "text", text: r.text } }))
31735
31736
  ];
31736
31737
  const claimed = [];
31737
31738
  for (const item of mine) {
@@ -31881,6 +31882,7 @@ async function mirror(event, state, deps = {}) {
31881
31882
  try {
31882
31883
  const req = NotifyRequestSchema2.parse(entry);
31883
31884
  const { notificationId, parentId } = await (deps.submit ?? submitNotification)(req);
31885
+ (state.mine ??= /* @__PURE__ */ new Set()).add(notificationId);
31884
31886
  return { parentId, notificationId };
31885
31887
  } catch {
31886
31888
  return {};
@@ -31895,6 +31897,7 @@ async function askPermission(event, state, deps = {}) {
31895
31897
  if (!entry) return { decision: { allow: false, reason: "Could not ask" } };
31896
31898
  try {
31897
31899
  const sent = await (deps.submit ?? submitNotification)(NotifyRequestSchema2.parse(entry));
31900
+ (state.mine ??= /* @__PURE__ */ new Set()).add(sent.notificationId);
31898
31901
  const answer = await awaitAnswer(state, sent.notificationId);
31899
31902
  return { decision: decisionFrom(answer), parentId: sent.parentId };
31900
31903
  } catch (e) {
@@ -31913,6 +31916,7 @@ async function askQuestion(question, state, deps = {}) {
31913
31916
  };
31914
31917
  try {
31915
31918
  const sent = await (deps.submit ?? submitNotification)(NotifyRequestSchema2.parse(entry));
31919
+ (state.mine ??= /* @__PURE__ */ new Set()).add(sent.notificationId);
31916
31920
  return spokenText(await awaitAnswer(state, sent.notificationId));
31917
31921
  } catch {
31918
31922
  return null;
@@ -31965,15 +31969,17 @@ function optionFor(options, decision) {
31965
31969
  return options.find((o) => o.kind === want)?.optionId ?? null;
31966
31970
  }
31967
31971
  function createAcpDriver(opts) {
31968
- return new AcpDriver(opts.cwd, opts.mode);
31972
+ return new AcpDriver(opts.cwd, opts.mode, opts.mcp ?? []);
31969
31973
  }
31970
31974
  var AcpDriver = class {
31971
- constructor(cwd, mode) {
31975
+ constructor(cwd, mode, mcp = []) {
31972
31976
  this.cwd = cwd;
31973
31977
  this.mode = mode;
31978
+ this.mcp = mcp;
31974
31979
  }
31975
31980
  cwd;
31976
31981
  mode;
31982
+ mcp;
31977
31983
  nextId = 1;
31978
31984
  initId;
31979
31985
  sessionNewId;
@@ -32049,7 +32055,7 @@ var AcpDriver = class {
32049
32055
  this.sessionNewId = this.nextId++;
32050
32056
  return {
32051
32057
  events: [],
32052
- writes: [frame({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
32058
+ writes: [frame({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: this.mcp } })]
32053
32059
  };
32054
32060
  }
32055
32061
  if (msg.id === this.sessionNewId) {
@@ -32198,7 +32204,7 @@ function startSession(opts) {
32198
32204
  opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
32199
32205
  });
32200
32206
  child.on("error", (e) => opts.onEvent({ kind: "error", message: e.message }));
32201
- const driver = createAcpDriver({ cwd, mode: opts.mode });
32207
+ const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.mcp ? { mcp: opts.mcp } : {} });
32202
32208
  let buffer = "";
32203
32209
  child.stdout?.on("data", (chunk) => {
32204
32210
  const { lines, rest } = splitLines(buffer, String(chunk));
@@ -32273,10 +32279,13 @@ function runHarness(opts) {
32273
32279
  }
32274
32280
  }
32275
32281
  }
32282
+ const paigyMcp = opts.exclusive && opts.token ? [{ name: "paigy", command: "npx", args: ["-y", "@paigy/mcp@latest"], env: { PAIGY_TOKEN: opts.token } }] : void 0;
32283
+ if (paigyMcp) state.sharedWithAgent = true;
32276
32284
  session = startSession({
32277
32285
  harness: opts.harness,
32278
32286
  cwd: opts.cwd,
32279
32287
  mode: opts.mode,
32288
+ ...paigyMcp ? { mcp: paigyMcp } : {},
32280
32289
  ...opts.bin ? { bin: opts.bin } : {},
32281
32290
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
32282
32291
  });
@@ -32377,7 +32386,7 @@ function startHost(opts) {
32377
32386
 
32378
32387
  // src/cli.ts
32379
32388
  function parseArgs(argv) {
32380
- const args = { harness: "claude", mode: "bypass", cwd: process.cwd(), doctor: false, host: false, pair: false, service: false, grant: [], hatch: null, identity: null, graceSeconds: 90, prompt: "" };
32389
+ const args = { harness: "claude", mode: "bypass", cwd: process.cwd(), doctor: false, host: false, pair: false, service: false, setup: false, grant: [], hatch: null, voice: null, slot: null, identity: null, graceSeconds: 90, prompt: "" };
32381
32390
  const words = [];
32382
32391
  for (let i = 0; i < argv.length; i++) {
32383
32392
  const arg = argv[i];
@@ -32404,6 +32413,8 @@ function parseArgs(argv) {
32404
32413
  args.host = true;
32405
32414
  } else if (arg === "pair") {
32406
32415
  args.pair = true;
32416
+ } else if (arg === "setup") {
32417
+ args.setup = true;
32407
32418
  } else if (arg === "service") {
32408
32419
  args.service = true;
32409
32420
  } else if (arg === "--grant") {
@@ -32414,6 +32425,10 @@ function parseArgs(argv) {
32414
32425
  const v = next();
32415
32426
  if (!v) return { error: 'hatch needs a name: paigy-harness hatch "Voice bug hunter"' };
32416
32427
  args.hatch = v;
32428
+ } else if (arg === "--voice") {
32429
+ args.voice = next() ?? null;
32430
+ } else if (arg === "--slot") {
32431
+ args.slot = next() ?? null;
32417
32432
  } else if (arg === "--identity") {
32418
32433
  const v = next();
32419
32434
  if (!v) return { error: "--identity needs a hatched agent's name" };
@@ -32425,7 +32440,7 @@ function parseArgs(argv) {
32425
32440
  }
32426
32441
  }
32427
32442
  args.prompt = words.join(" ");
32428
- if (!args.doctor && !args.hatch && !args.host && !args.pair && !args.service && !args.prompt) return { error: "a prompt is required (or --doctor / pair / host / service / hatch NAME)" };
32443
+ if (!args.doctor && !args.hatch && !args.host && !args.pair && !args.service && !args.setup && !args.prompt) return { error: "a prompt is required (or setup / --doctor / pair / host / service / hatch NAME)" };
32429
32444
  return args;
32430
32445
  }
32431
32446
  var STATUS_MARK = { ready: "\u2713", login: "\u25D0", "adapter-missing": "\u25D0", missing: "\u2717" };
@@ -32433,7 +32448,7 @@ async function main() {
32433
32448
  const parsed = parseArgs(process.argv.slice(2));
32434
32449
  if ("error" in parsed) {
32435
32450
  console.error(`paigy-harness: ${parsed.error}`);
32436
- console.error("usage: paigy-harness pair | host [--grant DIR]\u2026 | service | hatch NAME | [--harness claude|codex] [--mode bypass|ask] [--cwd DIR] [--identity NAME] [--grace SECONDS] [--doctor] PROMPT\u2026");
32451
+ console.error("usage: paigy-harness setup | pair | host [--grant DIR]\u2026 | service | hatch NAME [--voice KEY] [--slot SLOT] | [--harness claude|codex] [--mode bypass|ask] [--cwd DIR] [--identity NAME] [--grace SECONDS] [--doctor] PROMPT\u2026");
32437
32452
  process.exit(2);
32438
32453
  }
32439
32454
  if (parsed.doctor) {
@@ -32443,6 +32458,91 @@ async function main() {
32443
32458
  }
32444
32459
  return;
32445
32460
  }
32461
+ if (parsed.setup) {
32462
+ if (!readToken("Desktop")) {
32463
+ const code = await requestCode(hostname().replace(/\.local$/, ""));
32464
+ console.log(await import_qrcode.default.toString(code.verification_uri_complete, { type: "terminal", small: true }));
32465
+ console.log(`Scan with your phone's camera, or enter code ${code.user_code} at ${code.verification_uri}`);
32466
+ const deadline = Date.now() + code.expires_in * 1e3;
32467
+ let paired = false;
32468
+ while (!paired && Date.now() < deadline) {
32469
+ const token = await pollToken(code.device_code).catch(() => null);
32470
+ if (token) {
32471
+ saveToken(token, "Desktop");
32472
+ console.log(`\u2713 paired as "${token.name}"`);
32473
+ paired = true;
32474
+ } else await sleep(code.interval * 1e3);
32475
+ }
32476
+ if (!paired) {
32477
+ console.error("Pairing code expired \u2014 run `paigy-harness setup` again.");
32478
+ process.exit(1);
32479
+ }
32480
+ } else console.log("\u2713 already paired as this machine");
32481
+ for (const a of detectAll()) {
32482
+ if (a.status === "adapter-missing" && a.install) {
32483
+ console.log(`\u2192 installing the ${a.label} bridge\u2026`);
32484
+ try {
32485
+ execSync(a.install, { stdio: "inherit", shell: "/bin/sh" });
32486
+ } catch {
32487
+ console.log(` couldn't install \u2014 later: ${a.install}`);
32488
+ }
32489
+ } else if (a.status === "missing") console.log(`\u2717 ${a.label} CLI not installed \u2014 ${a.hint ?? ""}`);
32490
+ else if (a.status === "login") console.log(`\u25D0 ${a.label}: ${a.hint ?? "sign in"}`);
32491
+ else console.log(`\u2713 ${a.label} ready`);
32492
+ }
32493
+ const wsDeps = { file: workspacesFile() };
32494
+ for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
32495
+ if (!listWorkspaces(wsDeps).length) {
32496
+ const guess = join4(homedir5(), "projects");
32497
+ if (existsSync5(guess)) {
32498
+ addWorkspace(guess, wsDeps);
32499
+ console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
32500
+ } else console.log("no workspace granted yet \u2014 grant one with: paigy-harness host --grant DIR");
32501
+ }
32502
+ if (process.platform === "darwin") {
32503
+ try {
32504
+ execSync(`${JSON.stringify(process.execPath)} ${JSON.stringify(process.argv[1])} service`, { stdio: "inherit", shell: "/bin/sh" });
32505
+ } catch {
32506
+ console.log("service install failed \u2014 run `paigy-harness host` to host in the foreground");
32507
+ }
32508
+ } else console.log("start hosting with: paigy-harness host (keep it running under your init system)");
32509
+ const TERMINAL_AGENTS = [
32510
+ { cli: "claude", name: "Claude Code", slot: "mcp-agent", wire: "claude mcp add --scope user paigy -- npx -y @paigy/mcp@latest" },
32511
+ { cli: "codex", name: "Codex", slot: "codex", wire: "codex mcp add paigy -- npx -y @paigy/mcp@latest" },
32512
+ { cli: "antigravity", name: "Antigravity", slot: "antigravity" }
32513
+ ];
32514
+ for (const t of TERMINAL_AGENTS) {
32515
+ try {
32516
+ if (!which(t.cli) || readToken(t.slot)) continue;
32517
+ const minted = await hatch(t.name, null);
32518
+ saveToken({ access_token: minted.token, name: minted.name, device: null }, t.slot);
32519
+ console.log(`\u2713 hatched "${minted.name}" for ${t.cli} sessions (name/voice it on the phone)`);
32520
+ if (t.wire) {
32521
+ try {
32522
+ execSync(t.wire, { stdio: "ignore", shell: "/bin/sh" });
32523
+ console.log(`\u2713 paigy MCP registered for ${t.cli} (restart open sessions)`);
32524
+ } catch {
32525
+ }
32526
+ }
32527
+ } catch {
32528
+ }
32529
+ }
32530
+ try {
32531
+ const settings = join4(homedir5(), ".claude", "settings.json");
32532
+ if (which("claude") && existsSync5(settings)) {
32533
+ const cfg = JSON.parse(readFileSync3(settings, "utf8"));
32534
+ if (!cfg["statusLine"]) {
32535
+ cfg["statusLine"] = { type: "command", command: "npx -y -p @paigy/mcp@latest paigy-statusline" };
32536
+ writeFileSync3(settings, JSON.stringify(cfg, null, 2));
32537
+ console.log("\u2713 statusline wired");
32538
+ }
32539
+ }
32540
+ } catch {
32541
+ }
32542
+ console.log("\n\u2705 Done \u2014 look at your phone: Agents shows this machine with a green dot,");
32543
+ console.log(" and \u201C+ New session\u201D launches agents on it.");
32544
+ return;
32545
+ }
32446
32546
  if (parsed.pair) {
32447
32547
  if (readToken("Desktop")) {
32448
32548
  console.log("\u2713 already paired as this machine");
@@ -32499,8 +32599,8 @@ async function main() {
32499
32599
  process.exit(1);
32500
32600
  }
32501
32601
  if (parsed.hatch) {
32502
- const minted = await hatch(parsed.hatch);
32503
- saveToken({ access_token: minted.token, name: minted.name, device: null }, minted.name);
32602
+ const minted = await hatch(parsed.hatch, parsed.voice ?? null);
32603
+ saveToken({ access_token: minted.token, name: minted.name, device: null }, parsed.slot ?? minted.name);
32504
32604
  console.log(`\u2713 hatched "${minted.name}" \u2014 it's on your phone's Agents screen now.`);
32505
32605
  console.log(` speak as it: PAIGY_AGENT="${minted.name}" <any paigy tool>`);
32506
32606
  console.log(` run with it: paigy-harness --identity "${minted.name}" --harness codex "\u2026"`);
package/dist/main.js CHANGED
@@ -12386,15 +12386,17 @@ function optionFor(options, decision) {
12386
12386
  return options.find((o) => o.kind === want)?.optionId ?? null;
12387
12387
  }
12388
12388
  function createAcpDriver(opts) {
12389
- return new AcpDriver(opts.cwd, opts.mode);
12389
+ return new AcpDriver(opts.cwd, opts.mode, opts.mcp ?? []);
12390
12390
  }
12391
12391
  var AcpDriver = class {
12392
- constructor(cwd, mode) {
12392
+ constructor(cwd, mode, mcp = []) {
12393
12393
  this.cwd = cwd;
12394
12394
  this.mode = mode;
12395
+ this.mcp = mcp;
12395
12396
  }
12396
12397
  cwd;
12397
12398
  mode;
12399
+ mcp;
12398
12400
  nextId = 1;
12399
12401
  initId;
12400
12402
  sessionNewId;
@@ -12470,7 +12472,7 @@ var AcpDriver = class {
12470
12472
  this.sessionNewId = this.nextId++;
12471
12473
  return {
12472
12474
  events: [],
12473
- writes: [frame({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
12475
+ writes: [frame({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: this.mcp } })]
12474
12476
  };
12475
12477
  }
12476
12478
  if (msg.id === this.sessionNewId) {
@@ -12619,7 +12621,7 @@ function startSession(opts) {
12619
12621
  opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
12620
12622
  });
12621
12623
  child.on("error", (e) => opts.onEvent({ kind: "error", message: e.message }));
12622
- const driver = createAcpDriver({ cwd, mode: opts.mode });
12624
+ const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.mcp ? { mcp: opts.mcp } : {} });
12623
12625
  let buffer = "";
12624
12626
  child.stdout?.on("data", (chunk) => {
12625
12627
  const { lines, rest } = splitLines(buffer, String(chunk));
@@ -13512,9 +13514,10 @@ async function drainInput(state, deps = {}) {
13512
13514
  return [];
13513
13515
  }
13514
13516
  const ours = (parentId) => state.exclusive || parentId === state.parentId;
13517
+ const claimable = (notificationId) => !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
13515
13518
  const mine = [
13516
- ...work.replies.filter((r) => ours(r.parentId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: r.answer })),
13517
- ...work.requests.filter((r) => ours(r.parentId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: { kind: "text", text: r.text } }))
13519
+ ...work.replies.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: r.answer })),
13520
+ ...work.requests.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: { kind: "text", text: r.text } }))
13518
13521
  ];
13519
13522
  const claimed = [];
13520
13523
  for (const item of mine) {
@@ -13664,6 +13667,7 @@ async function mirror(event, state, deps = {}) {
13664
13667
  try {
13665
13668
  const req = NotifyRequestSchema2.parse(entry);
13666
13669
  const { notificationId, parentId } = await (deps.submit ?? submitNotification)(req);
13670
+ (state.mine ??= /* @__PURE__ */ new Set()).add(notificationId);
13667
13671
  return { parentId, notificationId };
13668
13672
  } catch {
13669
13673
  return {};
@@ -13678,6 +13682,7 @@ async function askPermission(event, state, deps = {}) {
13678
13682
  if (!entry) return { decision: { allow: false, reason: "Could not ask" } };
13679
13683
  try {
13680
13684
  const sent = await (deps.submit ?? submitNotification)(NotifyRequestSchema2.parse(entry));
13685
+ (state.mine ??= /* @__PURE__ */ new Set()).add(sent.notificationId);
13681
13686
  const answer = await awaitAnswer(state, sent.notificationId);
13682
13687
  return { decision: decisionFrom(answer), parentId: sent.parentId };
13683
13688
  } catch (e) {
@@ -13696,6 +13701,7 @@ async function askQuestion(question, state, deps = {}) {
13696
13701
  };
13697
13702
  try {
13698
13703
  const sent = await (deps.submit ?? submitNotification)(NotifyRequestSchema2.parse(entry));
13704
+ (state.mine ??= /* @__PURE__ */ new Set()).add(sent.notificationId);
13699
13705
  return spokenText(await awaitAnswer(state, sent.notificationId));
13700
13706
  } catch {
13701
13707
  return null;
@@ -13773,10 +13779,13 @@ function runHarness(opts) {
13773
13779
  }
13774
13780
  }
13775
13781
  }
13782
+ const paigyMcp = opts.exclusive && opts.token ? [{ name: "paigy", command: "npx", args: ["-y", "@paigy/mcp@latest"], env: { PAIGY_TOKEN: opts.token } }] : void 0;
13783
+ if (paigyMcp) state.sharedWithAgent = true;
13776
13784
  session = startSession({
13777
13785
  harness: opts.harness,
13778
13786
  cwd: opts.cwd,
13779
13787
  mode: opts.mode,
13788
+ ...paigyMcp ? { mcp: paigyMcp } : {},
13780
13789
  ...opts.bin ? { bin: opts.bin } : {},
13781
13790
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
13782
13791
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/harness",
3
- "version": "0.1.1",
3
+ "version": "0.1.5",
4
4
  "description": "Run Claude Code / Codex on this machine, bridged to Paigy — launchable from your phone.",
5
5
  "license": "MIT",
6
6
  "type": "module",