@paigy/harness 0.1.7 → 0.2.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
@@ -157,12 +157,13 @@ re-voiceable, revocable.
157
157
  ## Install anywhere (the curl path)
158
158
 
159
159
  ```sh
160
- curl -fsSL https://paigy.ai/install.sh | sh
160
+ curl -fsSL https://paigy.ai/install | sh
161
161
  ```
162
162
 
163
- Installs the npm CLI (one self-contained file — the workspace libs are bundled),
164
- runs `paigy-harness pair` (device flow with a terminal QR, landing in the Desktop
165
- slot), and prints the host/service next steps. `paigy-harness service` installs a
163
+ Installs the npm CLI (one self-contained file — the workspace libs are bundled) and
164
+ runs `paigy-harness setup`: QR-pairs the machine, hatches each detected agent's
165
+ identity (no codes), registers the MCP + statusline, grants a workspace, installs
166
+ the host service. `paigy-harness service` installs a
166
167
  macOS launchd agent so hosting survives reboots — the host loop IS the service,
167
168
  launchd just keeps it alive (the old "no daemon" note was about the WAKE channel,
168
169
  which still belongs to paigy-listen).
@@ -191,7 +192,7 @@ import Electron — a machine with only the CLI is exactly as launchable as one
191
192
  the window.
192
193
 
193
194
  Pairing is one-time and lives in `~/.paigy` — the app reads it, never mints it. If
194
- you've never paired: `npx -y -p @paigy/mcp paigy-mcp-onboard`.
195
+ you've never paired: `paigy-harness setup`.
195
196
 
196
197
  ## Uninstall
197
198
 
@@ -224,7 +225,7 @@ LAST, after the hooks are gone.
224
225
  token — per-run identity is threaded through every network touch (`run.ts` binds
225
226
  submit/check/ack once per session), so parallel sessions never share a
226
227
  conversation. "Stop sessions" stops them all; per-session stop is UI away.
227
- - **Deliberately no auto-start daemon.** A promptless harness has no thread to pump,
228
- and `paigy-listen` + `PAIGY_ON_WAKE` (#486) already own the standing wake channel
229
- a launchd `paigy-harness` would be a second daemon racing the first. The composition
230
- is: the listener wakes on Paigy work, and what it runs can be `paigy-harness`.
228
+ - **The host service IS the daemon** (`paigy-harness service`, launchd, KeepAlive)
229
+ it claims phone launches and runs sessions. The WAKE channel for terminal-agent
230
+ replies still belongs to `paigy-listen` + `PAIGY_ON_WAKE` (#486); spawn-on-wake
231
+ needs the slot-lease design in companion.md before it auto-wires.
package/dist/cli.js CHANGED
@@ -30665,6 +30665,9 @@ function readToken(agent2 = AGENT_NAME) {
30665
30665
  if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
30666
30666
  return readTokenFile()[agent2]?.access_token ?? "";
30667
30667
  }
30668
+ function listSlots() {
30669
+ return Object.keys(readTokenFile());
30670
+ }
30668
30671
  async function requestCode(suggestedName = AGENT_NAME, e2ee) {
30669
30672
  const res = await reach(`${BACKEND_URL}/api/device/code`, {
30670
30673
  method: "POST",
@@ -31740,7 +31743,8 @@ async function drainInput(state, deps = {}) {
31740
31743
  const claimed = [];
31741
31744
  for (const item of mine) {
31742
31745
  try {
31743
- await (deps.ack ?? setTaskState)(item.notificationId, "in_progress");
31746
+ const ack = await (deps.ack ?? setTaskState)(item.notificationId, "in_progress");
31747
+ if (ack?.won === false) continue;
31744
31748
  claimed.push(item);
31745
31749
  } catch {
31746
31750
  }
@@ -32366,9 +32370,30 @@ function startHost(opts) {
32366
32370
  opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
32367
32371
  }
32368
32372
  }
32373
+ const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
32374
+ async function sweepSlots() {
32375
+ const workspace = listWorkspaces(opts.wsDeps)[0];
32376
+ if (!workspace) return;
32377
+ for (const slot of listSlots()) {
32378
+ const harness = SLOT_HARNESS[slot];
32379
+ const key = `slot:${slot}`;
32380
+ if (!harness || runs.has(key)) continue;
32381
+ const token = readToken(slot);
32382
+ if (!token) continue;
32383
+ const work = await checkReplies({ token }).catch(() => null);
32384
+ if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
32385
+ const log = (line) => opts.log(`[${slot}] ${line}`);
32386
+ const run = runHarness({ harness, cwd: workspace, mode: "bypass", prompt: "", exclusive: true, token, log });
32387
+ runs.set(key, { run, label: slot });
32388
+ opts.log(`\u25B6 woke ${slot} (${harness}) \u2014 work was waiting in ${workspace}`);
32389
+ }
32390
+ }
32369
32391
  beat();
32370
32392
  const pulse = setInterval(beat, 6e4);
32371
- const spawnPoll = setInterval(() => void claimAndSpawn(), 5e3);
32393
+ const spawnPoll = setInterval(() => {
32394
+ void claimAndSpawn();
32395
+ void sweepSlots();
32396
+ }, 5e3);
32372
32397
  const stopSessions = () => {
32373
32398
  for (const { run, label } of runs.values()) {
32374
32399
  run.stop();
@@ -32510,7 +32535,9 @@ async function main() {
32510
32535
  }
32511
32536
  } else console.log("start hosting with: paigy-harness host (keep it running under your init system)");
32512
32537
  const TERMINAL_AGENTS = [
32513
- { cli: "claude", name: "Claude Code", slot: "mcp-agent", wire: "claude mcp add --scope user paigy -- npx -y @paigy/mcp@latest" },
32538
+ // Wire = MCP registration + the plugin (skills, hooks, idle-escalation the
32539
+ // piece that makes a LIVE session notice your requests without polling).
32540
+ { cli: "claude", name: "Claude Code", slot: "mcp-agent", wire: "claude mcp add --scope user paigy -- npx -y @paigy/mcp@latest && claude plugin marketplace add paigy-ai/mcp; claude plugin install paigy@paigy" },
32514
32541
  { cli: "codex", name: "Codex", slot: "codex", wire: "codex mcp add paigy -- npx -y @paigy/mcp@latest" },
32515
32542
  { cli: "antigravity", name: "Antigravity", slot: "antigravity" }
32516
32543
  ];
package/dist/main.js CHANGED
@@ -12087,6 +12087,9 @@ function readToken(agent2 = AGENT_NAME) {
12087
12087
  if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
12088
12088
  return readTokenFile()[agent2]?.access_token ?? "";
12089
12089
  }
12090
+ function listSlots() {
12091
+ return Object.keys(readTokenFile());
12092
+ }
12090
12093
  async function requestCode(suggestedName = AGENT_NAME, e2ee) {
12091
12094
  const res = await reach(`${BACKEND_URL}/api/device/code`, {
12092
12095
  method: "POST",
@@ -13522,7 +13525,8 @@ async function drainInput(state, deps = {}) {
13522
13525
  const claimed = [];
13523
13526
  for (const item of mine) {
13524
13527
  try {
13525
- await (deps.ack ?? setTaskState)(item.notificationId, "in_progress");
13528
+ const ack = await (deps.ack ?? setTaskState)(item.notificationId, "in_progress");
13529
+ if (ack?.won === false) continue;
13526
13530
  claimed.push(item);
13527
13531
  } catch {
13528
13532
  }
@@ -13837,9 +13841,30 @@ function startHost(opts) {
13837
13841
  opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
13838
13842
  }
13839
13843
  }
13844
+ const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
13845
+ async function sweepSlots() {
13846
+ const workspace = listWorkspaces(opts.wsDeps)[0];
13847
+ if (!workspace) return;
13848
+ for (const slot of listSlots()) {
13849
+ const harness = SLOT_HARNESS[slot];
13850
+ const key = `slot:${slot}`;
13851
+ if (!harness || runs.has(key)) continue;
13852
+ const token = readToken(slot);
13853
+ if (!token) continue;
13854
+ const work = await checkReplies({ token }).catch(() => null);
13855
+ if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
13856
+ const log2 = (line) => opts.log(`[${slot}] ${line}`);
13857
+ const run = runHarness({ harness, cwd: workspace, mode: "bypass", prompt: "", exclusive: true, token, log: log2 });
13858
+ runs.set(key, { run, label: slot });
13859
+ opts.log(`\u25B6 woke ${slot} (${harness}) \u2014 work was waiting in ${workspace}`);
13860
+ }
13861
+ }
13840
13862
  beat();
13841
13863
  const pulse = setInterval(beat, 6e4);
13842
- const spawnPoll = setInterval(() => void claimAndSpawn(), 5e3);
13864
+ const spawnPoll = setInterval(() => {
13865
+ void claimAndSpawn();
13866
+ void sweepSlots();
13867
+ }, 5e3);
13843
13868
  const stopSessions = () => {
13844
13869
  for (const { run, label } of runs.values()) {
13845
13870
  run.stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/harness",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
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",
@@ -18,8 +18,8 @@
18
18
  "vitest": "^2.1.8",
19
19
  "qrcode": "^1.5.4",
20
20
  "zod": "^3.24.1",
21
- "@paigy/schema": "0.0.0",
22
- "@paigy/sdk": "0.1.0"
21
+ "@paigy/sdk": "0.1.0",
22
+ "@paigy/schema": "0.0.0"
23
23
  },
24
24
  "files": [
25
25
  "dist/cli.js",