@paigy/harness 0.1.8 → 0.2.2

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.
Files changed (3) hide show
  1. package/dist/cli.js +64 -10
  2. package/dist/main.js +63 -9
  3. package/package.json +13 -13
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
  }
@@ -31963,7 +31967,7 @@ var clip = (s) => (s.length > 120 ? `${s.slice(0, 117)}\u2026` : s) || "(no text
31963
31967
  import { spawn } from "child_process";
31964
31968
  import { existsSync as existsSync3 } from "fs";
31965
31969
  import { homedir as homedir3 } from "os";
31966
- import { resolve, delimiter } from "path";
31970
+ import { dirname, resolve, delimiter } from "path";
31967
31971
 
31968
31972
  // src/harness/acp.ts
31969
31973
  var none = { events: [], writes: [] };
@@ -32190,9 +32194,15 @@ function startSession(opts) {
32190
32194
  }
32191
32195
  const child = (opts.spawnFn ?? spawn)(opts.bin ?? ADAPTER_BIN[opts.harness], [], {
32192
32196
  cwd,
32193
- // The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI-launched
32194
- // app's PATH won't have it — same reason detection probes these dirs.
32195
- env: { ...process.env, PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter) },
32197
+ // The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI- or
32198
+ // launchd-launched process's PATH won't have it — probe dirs plus the RUNNING
32199
+ // node's own bin dir (nvm installs the adapters next to node; launchd's bare
32200
+ // PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
32201
+ env: {
32202
+ ...process.env,
32203
+ PATH: [process.env.PATH, dirname(process.execPath), ...probeDirs()].filter(Boolean).join(delimiter),
32204
+ ...opts.token ? { PAIGY_TOKEN: opts.token } : {}
32205
+ },
32196
32206
  stdio: ["pipe", "pipe", "pipe"]
32197
32207
  });
32198
32208
  const write = (frames) => {
@@ -32205,8 +32215,12 @@ function startSession(opts) {
32205
32215
  });
32206
32216
  child.on("exit", (code) => {
32207
32217
  opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
32218
+ opts.onExit?.();
32219
+ });
32220
+ child.on("error", (e) => {
32221
+ opts.onEvent({ kind: "error", message: e.message });
32222
+ opts.onExit?.();
32208
32223
  });
32209
- child.on("error", (e) => opts.onEvent({ kind: "error", message: e.message }));
32210
32224
  const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.mcp ? { mcp: opts.mcp } : {} });
32211
32225
  let buffer = "";
32212
32226
  child.stdout?.on("data", (chunk) => {
@@ -32289,7 +32303,9 @@ function runHarness(opts) {
32289
32303
  cwd: opts.cwd,
32290
32304
  mode: opts.mode,
32291
32305
  ...paigyMcp ? { mcp: paigyMcp } : {},
32306
+ ...opts.token ? { token: opts.token } : {},
32292
32307
  ...opts.bin ? { bin: opts.bin } : {},
32308
+ ...opts.onExit ? { onExit: opts.onExit } : {},
32293
32309
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
32294
32310
  });
32295
32311
  if (opts.prompt) session.send(opts.prompt);
@@ -32313,7 +32329,7 @@ function runHarness(opts) {
32313
32329
 
32314
32330
  // src/workspaces.ts
32315
32331
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
32316
- import { dirname, join as join3, resolve as resolve2 } from "path";
32332
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
32317
32333
  import { homedir as homedir4 } from "os";
32318
32334
  function workspacesFile() {
32319
32335
  return join3(homedir4(), ".paigy", "workspaces.json");
@@ -32331,7 +32347,7 @@ function addWorkspace(dir, deps) {
32331
32347
  const all = listWorkspaces(deps);
32332
32348
  if (!all.includes(next)) {
32333
32349
  all.push(next);
32334
- mkdirSync2(dirname(deps.file), { recursive: true });
32350
+ mkdirSync2(dirname2(deps.file), { recursive: true });
32335
32351
  writeFileSync2(deps.file, JSON.stringify(all, null, 2));
32336
32352
  }
32337
32353
  return all;
@@ -32360,15 +32376,53 @@ function startHost(opts) {
32360
32376
  prompt: spec.prompt ?? "",
32361
32377
  exclusive: true,
32362
32378
  token: spec.token,
32379
+ onExit: () => {
32380
+ runs.delete(spec.sessionId);
32381
+ opts.log(`\u2716 ${label} ended`);
32382
+ },
32363
32383
  log
32364
32384
  });
32365
32385
  runs.set(spec.sessionId, { run, label });
32366
32386
  opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
32367
32387
  }
32368
32388
  }
32389
+ const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
32390
+ async function sweepSlots() {
32391
+ const workspace = listWorkspaces(opts.wsDeps)[0];
32392
+ if (!workspace) return;
32393
+ for (const slot of listSlots()) {
32394
+ const harness = SLOT_HARNESS[slot];
32395
+ const key = `slot:${slot}`;
32396
+ if (!harness || runs.has(key)) continue;
32397
+ const token = readToken(slot);
32398
+ if (!token) continue;
32399
+ const work = await checkReplies({ token }).catch(() => null);
32400
+ if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
32401
+ const log = (line) => opts.log(`[${slot}] ${line}`);
32402
+ const run = runHarness({
32403
+ harness,
32404
+ cwd: workspace,
32405
+ mode: "bypass",
32406
+ prompt: "",
32407
+ exclusive: true,
32408
+ token,
32409
+ // A dead run must not squat the slot — evict so the next wake can respawn.
32410
+ onExit: () => {
32411
+ runs.delete(key);
32412
+ opts.log(`\u2716 ${slot} ended`);
32413
+ },
32414
+ log
32415
+ });
32416
+ runs.set(key, { run, label: slot });
32417
+ opts.log(`\u25B6 woke ${slot} (${harness}) \u2014 work was waiting in ${workspace}`);
32418
+ }
32419
+ }
32369
32420
  beat();
32370
32421
  const pulse = setInterval(beat, 6e4);
32371
- const spawnPoll = setInterval(() => void claimAndSpawn(), 5e3);
32422
+ const spawnPoll = setInterval(() => {
32423
+ void claimAndSpawn();
32424
+ void sweepSlots();
32425
+ }, 5e3);
32372
32426
  const stopSessions = () => {
32373
32427
  for (const { run, label } of runs.values()) {
32374
32428
  run.stop();
@@ -32512,7 +32566,7 @@ async function main() {
32512
32566
  const TERMINAL_AGENTS = [
32513
32567
  // Wire = MCP registration + the plugin (skills, hooks, idle-escalation — the
32514
32568
  // piece that makes a LIVE session notice your requests without polling).
32515
- { 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" },
32569
+ { 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" },
32516
32570
  { cli: "codex", name: "Codex", slot: "codex", wire: "codex mcp add paigy -- npx -y @paigy/mcp@latest" },
32517
32571
  { cli: "antigravity", name: "Antigravity", slot: "antigravity" }
32518
32572
  ];
package/dist/main.js CHANGED
@@ -4577,7 +4577,7 @@ var require_lib = __commonJS({
4577
4577
  import { app, BrowserWindow, dialog, ipcMain } from "electron";
4578
4578
  import { spawn as spawn2 } from "child_process";
4579
4579
  import { fileURLToPath } from "url";
4580
- import { dirname as dirname2, join as join4 } from "path";
4580
+ import { dirname as dirname3, join as join4 } from "path";
4581
4581
 
4582
4582
  // ../../packages/sdk/dist/index.js
4583
4583
  import { createRequire as __sdkCreateRequire } from "module";
@@ -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",
@@ -12377,7 +12380,7 @@ function removeWorkspace(dir, deps) {
12377
12380
  import { spawn } from "child_process";
12378
12381
  import { existsSync as existsSync4 } from "fs";
12379
12382
  import { homedir as homedir4 } from "os";
12380
- import { resolve as resolve2, delimiter } from "path";
12383
+ import { dirname as dirname2, resolve as resolve2, delimiter } from "path";
12381
12384
 
12382
12385
  // src/harness/acp.ts
12383
12386
  var none = { events: [], writes: [] };
@@ -12604,9 +12607,15 @@ function startSession(opts) {
12604
12607
  }
12605
12608
  const child = (opts.spawnFn ?? spawn)(opts.bin ?? ADAPTER_BIN[opts.harness], [], {
12606
12609
  cwd,
12607
- // The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI-launched
12608
- // app's PATH won't have it — same reason detection probes these dirs.
12609
- env: { ...process.env, PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter) },
12610
+ // The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI- or
12611
+ // launchd-launched process's PATH won't have it — probe dirs plus the RUNNING
12612
+ // node's own bin dir (nvm installs the adapters next to node; launchd's bare
12613
+ // PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
12614
+ env: {
12615
+ ...process.env,
12616
+ PATH: [process.env.PATH, dirname2(process.execPath), ...probeDirs()].filter(Boolean).join(delimiter),
12617
+ ...opts.token ? { PAIGY_TOKEN: opts.token } : {}
12618
+ },
12610
12619
  stdio: ["pipe", "pipe", "pipe"]
12611
12620
  });
12612
12621
  const write = (frames) => {
@@ -12619,8 +12628,12 @@ function startSession(opts) {
12619
12628
  });
12620
12629
  child.on("exit", (code) => {
12621
12630
  opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
12631
+ opts.onExit?.();
12632
+ });
12633
+ child.on("error", (e) => {
12634
+ opts.onEvent({ kind: "error", message: e.message });
12635
+ opts.onExit?.();
12622
12636
  });
12623
- child.on("error", (e) => opts.onEvent({ kind: "error", message: e.message }));
12624
12637
  const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.mcp ? { mcp: opts.mcp } : {} });
12625
12638
  let buffer = "";
12626
12639
  child.stdout?.on("data", (chunk) => {
@@ -13522,7 +13535,8 @@ async function drainInput(state, deps = {}) {
13522
13535
  const claimed = [];
13523
13536
  for (const item of mine) {
13524
13537
  try {
13525
- await (deps.ack ?? setTaskState)(item.notificationId, "in_progress");
13538
+ const ack = await (deps.ack ?? setTaskState)(item.notificationId, "in_progress");
13539
+ if (ack?.won === false) continue;
13526
13540
  claimed.push(item);
13527
13541
  } catch {
13528
13542
  }
@@ -13786,7 +13800,9 @@ function runHarness(opts) {
13786
13800
  cwd: opts.cwd,
13787
13801
  mode: opts.mode,
13788
13802
  ...paigyMcp ? { mcp: paigyMcp } : {},
13803
+ ...opts.token ? { token: opts.token } : {},
13789
13804
  ...opts.bin ? { bin: opts.bin } : {},
13805
+ ...opts.onExit ? { onExit: opts.onExit } : {},
13790
13806
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
13791
13807
  });
13792
13808
  if (opts.prompt) session.send(opts.prompt);
@@ -13831,15 +13847,53 @@ function startHost(opts) {
13831
13847
  prompt: spec.prompt ?? "",
13832
13848
  exclusive: true,
13833
13849
  token: spec.token,
13850
+ onExit: () => {
13851
+ runs.delete(spec.sessionId);
13852
+ opts.log(`\u2716 ${label} ended`);
13853
+ },
13834
13854
  log: log2
13835
13855
  });
13836
13856
  runs.set(spec.sessionId, { run, label });
13837
13857
  opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
13838
13858
  }
13839
13859
  }
13860
+ const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
13861
+ async function sweepSlots() {
13862
+ const workspace = listWorkspaces(opts.wsDeps)[0];
13863
+ if (!workspace) return;
13864
+ for (const slot of listSlots()) {
13865
+ const harness = SLOT_HARNESS[slot];
13866
+ const key = `slot:${slot}`;
13867
+ if (!harness || runs.has(key)) continue;
13868
+ const token = readToken(slot);
13869
+ if (!token) continue;
13870
+ const work = await checkReplies({ token }).catch(() => null);
13871
+ if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
13872
+ const log2 = (line) => opts.log(`[${slot}] ${line}`);
13873
+ const run = runHarness({
13874
+ harness,
13875
+ cwd: workspace,
13876
+ mode: "bypass",
13877
+ prompt: "",
13878
+ exclusive: true,
13879
+ token,
13880
+ // A dead run must not squat the slot — evict so the next wake can respawn.
13881
+ onExit: () => {
13882
+ runs.delete(key);
13883
+ opts.log(`\u2716 ${slot} ended`);
13884
+ },
13885
+ log: log2
13886
+ });
13887
+ runs.set(key, { run, label: slot });
13888
+ opts.log(`\u25B6 woke ${slot} (${harness}) \u2014 work was waiting in ${workspace}`);
13889
+ }
13890
+ }
13840
13891
  beat();
13841
13892
  const pulse = setInterval(beat, 6e4);
13842
- const spawnPoll = setInterval(() => void claimAndSpawn(), 5e3);
13893
+ const spawnPoll = setInterval(() => {
13894
+ void claimAndSpawn();
13895
+ void sweepSlots();
13896
+ }, 5e3);
13843
13897
  const stopSessions = () => {
13844
13898
  for (const { run, label } of runs.values()) {
13845
13899
  run.stop();
@@ -13859,7 +13913,7 @@ function startHost(opts) {
13859
13913
  }
13860
13914
 
13861
13915
  // src/main.ts
13862
- var here = dirname2(fileURLToPath(import.meta.url));
13916
+ var here = dirname3(fileURLToPath(import.meta.url));
13863
13917
  var wsDeps = { file: workspacesFile() };
13864
13918
  var DEVICE_SLOT = "Desktop";
13865
13919
  var deviceToken = () => readToken(DEVICE_SLOT) || readToken();
package/package.json CHANGED
@@ -1,13 +1,20 @@
1
1
  {
2
2
  "name": "@paigy/harness",
3
- "version": "0.1.8",
4
- "description": "Run Claude Code / Codex on this machine, bridged to Paigy launchable from your phone.",
3
+ "version": "0.2.2",
4
+ "description": "Run Claude Code / Codex on this machine, bridged to Paigy \u2014 launchable from your phone.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "dist/main.js",
8
8
  "bin": {
9
9
  "paigy-harness": "dist/cli.js"
10
10
  },
11
+ "scripts": {
12
+ "build": "tsup",
13
+ "dev": "pnpm build && electron .",
14
+ "start": "electron .",
15
+ "typecheck": "tsc --noEmit",
16
+ "test": "vitest run"
17
+ },
11
18
  "dependencies": {},
12
19
  "devDependencies": {
13
20
  "@types/node": "^22.0.0",
@@ -16,10 +23,10 @@
16
23
  "tsup": "^8.3.5",
17
24
  "typescript": "^5.7.2",
18
25
  "vitest": "^2.1.8",
26
+ "@paigy/schema": "workspace:*",
27
+ "@paigy/sdk": "workspace:*",
19
28
  "qrcode": "^1.5.4",
20
- "zod": "^3.24.1",
21
- "@paigy/sdk": "0.1.0",
22
- "@paigy/schema": "0.0.0"
29
+ "zod": "^3.24.1"
23
30
  },
24
31
  "files": [
25
32
  "dist/cli.js",
@@ -27,12 +34,5 @@
27
34
  ],
28
35
  "publishConfig": {
29
36
  "access": "public"
30
- },
31
- "scripts": {
32
- "build": "tsup",
33
- "dev": "pnpm build && electron .",
34
- "start": "electron .",
35
- "typecheck": "tsc --noEmit",
36
- "test": "vitest run"
37
37
  }
38
- }
38
+ }