@pushary/agent-hooks 0.62.6 → 0.63.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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.63.0
4
+
5
+ ### Machines
6
+
7
+ `pushary daemon` now reports itself to your workspace once a minute, and
8
+ `pushary status --devices` lists what is there:
9
+
10
+ ```
11
+ Machines
12
+ ● macbook-pro 0.63.0 just now
13
+ ○ build-server 0.62.6 4h ago
14
+ ```
15
+
16
+ `online` is worked out from the last heartbeat rather than stored, so a laptop
17
+ that closes mid-session reads offline a few minutes later without anything
18
+ having to notice it left.
19
+
20
+ This is what lets a phone start a session on a machine you name, instead of on
21
+ whichever one happens to answer first.
22
+
23
+ Presence is best effort throughout. A heartbeat that fails never stops the daemon
24
+ draining its queue, and `status` still reports your key and channels if the
25
+ machine list cannot be read. Against a server that predates the endpoint,
26
+ `--devices` says so rather than reporting an empty list.
27
+
28
+ **Needs a database migration** (`0066`). Until it is applied, `--devices` reports
29
+ that it could not read the list, and nothing else is affected.
30
+
31
+
3
32
  ## 0.62.6
4
33
 
5
34
  ### `clean` rewrote your project's Cursor config and left your own alone
@@ -34,8 +34,8 @@ import {
34
34
  } from "../chunk-MUEW424A.js";
35
35
  import {
36
36
  exitOnHelpFlag
37
- } from "../chunk-674M3JQF.js";
38
- import "../chunk-KVUBLB6K.js";
37
+ } from "../chunk-VYD27DKZ.js";
38
+ import "../chunk-PPTC2QDB.js";
39
39
  import "../chunk-DQAN3JQP.js";
40
40
  import "../chunk-KZERVKTD.js";
41
41
 
@@ -24,8 +24,8 @@ import {
24
24
  import "../chunk-BC3VCZ3E.js";
25
25
  import {
26
26
  exitOnHelpFlag
27
- } from "../chunk-674M3JQF.js";
28
- import "../chunk-KVUBLB6K.js";
27
+ } from "../chunk-VYD27DKZ.js";
28
+ import "../chunk-PPTC2QDB.js";
29
29
  import "../chunk-BSZYIAZL.js";
30
30
  import "../chunk-SAF6HGAA.js";
31
31
  import {
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ HEARTBEAT_INTERVAL_MS,
4
+ sendHeartbeat
5
+ } from "../chunk-R22IAMSS.js";
2
6
  import {
3
7
  spawnClaude
4
8
  } from "../chunk-XHKBHWLX.js";
@@ -7,8 +11,8 @@ import {
7
11
  } from "../chunk-MUEW424A.js";
8
12
  import {
9
13
  exitOnHelpFlag
10
- } from "../chunk-674M3JQF.js";
11
- import "../chunk-KVUBLB6K.js";
14
+ } from "../chunk-VYD27DKZ.js";
15
+ import "../chunk-PPTC2QDB.js";
12
16
  import {
13
17
  getMachineId
14
18
  } from "../chunk-RN3NOEJF.js";
@@ -87,6 +91,7 @@ var runSpawnDaemon = async () => {
87
91
  let fatalCode = null;
88
92
  let finish = () => {
89
93
  };
94
+ let heartbeat;
90
95
  const launch = (req) => {
91
96
  spawnTimestamps.push(Date.now());
92
97
  const cwd = req.cwd && existsSync(req.cwd) ? req.cwd : process.cwd();
@@ -166,8 +171,20 @@ var runSpawnDaemon = async () => {
166
171
  if (stopped) return;
167
172
  stopped = true;
168
173
  if (timer) clearTimeout(timer);
174
+ if (heartbeat) clearInterval(heartbeat);
169
175
  inflight?.abort();
170
176
  };
177
+ const beat = async () => {
178
+ if (stopped) return;
179
+ const result = await sendHeartbeat(apiKey, { repoKey: basename(process.cwd()) });
180
+ if (result.kind === "unauthorized" && !stopped) {
181
+ fatalCode = EXIT.UNAUTHENTICATED;
182
+ stderr("[pushary] daemon stopping: this machine's key was rejected. Run `pushary login` to sign in again.\n");
183
+ finish();
184
+ }
185
+ };
186
+ void beat();
187
+ heartbeat = setInterval(() => void beat(), HEARTBEAT_INTERVAL_MS);
171
188
  stderr(
172
189
  `[pushary] daemon online \u2014 this machine (${machineId}) can now be sent a new session from your phone. Ctrl-C to stop.
173
190
  `
@@ -57,10 +57,10 @@ import {
57
57
  } from "../chunk-MUEW424A.js";
58
58
  import {
59
59
  exitOnHelpFlag
60
- } from "../chunk-674M3JQF.js";
60
+ } from "../chunk-VYD27DKZ.js";
61
61
  import {
62
62
  getPackageVersion
63
- } from "../chunk-KVUBLB6K.js";
63
+ } from "../chunk-PPTC2QDB.js";
64
64
  import {
65
65
  readLedgerSummary
66
66
  } from "../chunk-WLGEY4UX.js";
@@ -5,8 +5,8 @@ import {
5
5
  import "../chunk-CAJZAFVS.js";
6
6
  import {
7
7
  exitOnHelpFlag
8
- } from "../chunk-674M3JQF.js";
9
- import "../chunk-KVUBLB6K.js";
8
+ } from "../chunk-VYD27DKZ.js";
9
+ import "../chunk-PPTC2QDB.js";
10
10
  import "../chunk-YHG74UFF.js";
11
11
  import "../chunk-5ZDYDU2Q.js";
12
12
  import "../chunk-WLGEY4UX.js";
@@ -21,10 +21,10 @@ import {
21
21
  import "../chunk-BC3VCZ3E.js";
22
22
  import {
23
23
  exitOnHelpFlag
24
- } from "../chunk-674M3JQF.js";
24
+ } from "../chunk-VYD27DKZ.js";
25
25
  import {
26
26
  getPackageVersion
27
- } from "../chunk-KVUBLB6K.js";
27
+ } from "../chunk-PPTC2QDB.js";
28
28
  import "../chunk-SAF6HGAA.js";
29
29
  import {
30
30
  UsageError
@@ -19,8 +19,8 @@ import {
19
19
  import "../chunk-BC3VCZ3E.js";
20
20
  import {
21
21
  exitOnHelpFlag
22
- } from "../chunk-674M3JQF.js";
23
- import "../chunk-KVUBLB6K.js";
22
+ } from "../chunk-VYD27DKZ.js";
23
+ import "../chunk-PPTC2QDB.js";
24
24
  import {
25
25
  UsageError
26
26
  } from "../chunk-7QLSKOSU.js";
@@ -11,8 +11,8 @@ import {
11
11
  } from "../chunk-GMXKITVA.js";
12
12
  import {
13
13
  exitOnHelpFlag
14
- } from "../chunk-674M3JQF.js";
15
- import "../chunk-KVUBLB6K.js";
14
+ } from "../chunk-VYD27DKZ.js";
15
+ import "../chunk-PPTC2QDB.js";
16
16
  import "../chunk-SAF6HGAA.js";
17
17
  import {
18
18
  UsageError
@@ -82,7 +82,7 @@ import {
82
82
  getPackageVersion,
83
83
  isNewerVersion,
84
84
  renderCommandHelp
85
- } from "../chunk-KVUBLB6K.js";
85
+ } from "../chunk-PPTC2QDB.js";
86
86
  import {
87
87
  reportEvent
88
88
  } from "../chunk-5ZDYDU2Q.js";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  exitOnHelpFlag
4
- } from "../chunk-674M3JQF.js";
5
- import "../chunk-KVUBLB6K.js";
4
+ } from "../chunk-VYD27DKZ.js";
5
+ import "../chunk-PPTC2QDB.js";
6
6
  import {
7
7
  readLedgerSummary
8
8
  } from "../chunk-WLGEY4UX.js";
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ describeLastSeen,
4
+ listMachines
5
+ } from "../chunk-R22IAMSS.js";
2
6
  import {
3
7
  describeReach,
4
8
  fetchChannels,
@@ -22,14 +26,15 @@ import {
22
26
  import "../chunk-BC3VCZ3E.js";
23
27
  import {
24
28
  exitOnHelpFlag
25
- } from "../chunk-674M3JQF.js";
26
- import "../chunk-KVUBLB6K.js";
29
+ } from "../chunk-VYD27DKZ.js";
30
+ import "../chunk-PPTC2QDB.js";
27
31
  import "../chunk-BSZYIAZL.js";
28
32
  import "../chunk-SAF6HGAA.js";
29
33
  import {
30
34
  UsageError
31
35
  } from "../chunk-7QLSKOSU.js";
32
36
  import "../chunk-DQAN3JQP.js";
37
+ import "../chunk-RN3NOEJF.js";
33
38
  import "../chunk-2UMNXADU.js";
34
39
  import {
35
40
  EXIT
@@ -57,10 +62,10 @@ var reachLine = (channels) => {
57
62
  var siteLine = (channels) => {
58
63
  const site = channels.site;
59
64
  if (site === null || site.otherMembers === 0) return null;
60
- const devices = site.appDevices + site.webActive;
65
+ const devices2 = site.appDevices + site.webActive;
61
66
  return {
62
67
  label: "Team",
63
- value: `${devices} device${devices === 1 ? "" : "s"} across ${site.otherMembers + 1} members`,
68
+ value: `${devices2} device${devices2 === 1 ? "" : "s"} across ${site.otherMembers + 1} members`,
64
69
  ok: null
65
70
  };
66
71
  };
@@ -178,8 +183,9 @@ var statusJson = (facts, report) => ({
178
183
  exitOnHelpFlag("status");
179
184
  var { bold, dim, green, red, yellow } = createIo();
180
185
  var args = process.argv.slice(2);
181
- rejectUnknownFlags(args.filter((a) => a !== "status"), ["--json"]);
186
+ rejectUnknownFlags(args.filter((a) => a !== "status"), ["--json", "--devices"]);
182
187
  var json = args.includes("--json");
188
+ var devices = args.includes("--devices");
183
189
  var main = async () => {
184
190
  const source = readKeySource();
185
191
  let keyCheck = null;
@@ -188,10 +194,16 @@ var main = async () => {
188
194
  keyCheck = await checkApiKey(source.key);
189
195
  if (keyCheck.kind === "ok") channels = await fetchChannels(source.key);
190
196
  }
197
+ const machines = devices && keyCheck?.kind === "ok" ? await listMachines(source.key) : null;
191
198
  const facts = { source, keyCheck, channels };
192
199
  const report = buildStatusReport(facts);
193
200
  if (json) {
194
- process.stdout.write(`${JSON.stringify(statusJson(facts, report), null, 2)}
201
+ const payload = statusJson(facts, report);
202
+ if (machines) {
203
+ payload.machines = machines.kind === "ok" ? machines.machines : [];
204
+ if (machines.kind !== "ok") payload.machinesUnavailable = machines.kind;
205
+ }
206
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
195
207
  `);
196
208
  process.exit(report.exitCode);
197
209
  }
@@ -203,6 +215,25 @@ var main = async () => {
203
215
  const icon = line.ok === true ? green("\u2713") : line.ok === false ? red("\u2717") : dim("\xB7");
204
216
  console.log(` ${icon} ${line.label.padEnd(width)}${line.value}`);
205
217
  }
218
+ if (machines) {
219
+ console.log();
220
+ console.log(` ${bold("Machines")}`);
221
+ if (machines.kind === "unsupported") {
222
+ console.log(` ${dim("This server does not report machines yet.")}`);
223
+ } else if (machines.kind === "failed") {
224
+ console.log(` ${dim(`Could not read the machine list (${machines.detail}).`)}`);
225
+ } else if (machines.machines.length === 0) {
226
+ console.log(` ${dim("None registered. Run")} pushary daemon ${dim("to keep a machine reachable.")}`);
227
+ } else {
228
+ const hostWidth = Math.max(...machines.machines.map((m) => m.hostname.length)) + 2;
229
+ for (const machine of machines.machines) {
230
+ const mark = machine.online ? green("\u25CF") : dim("\u25CB");
231
+ console.log(
232
+ ` ${mark} ${machine.hostname.padEnd(hostWidth)}${dim(`${machine.cliVersion} ${describeLastSeen(machine.lastSeenAt)}`)}`
233
+ );
234
+ }
235
+ }
236
+ }
206
237
  console.log();
207
238
  console.log(` ${report.exitCode === 0 ? green(bold(report.headline)) : yellow(bold(report.headline))}`);
208
239
  for (const next of report.next) console.log(` ${dim(next)}`);
@@ -7,8 +7,8 @@ import {
7
7
  } from "../chunk-GMXKITVA.js";
8
8
  import {
9
9
  exitOnHelpFlag
10
- } from "../chunk-674M3JQF.js";
11
- import "../chunk-KVUBLB6K.js";
10
+ } from "../chunk-VYD27DKZ.js";
11
+ import "../chunk-PPTC2QDB.js";
12
12
  import {
13
13
  UsageError
14
14
  } from "../chunk-7QLSKOSU.js";
@@ -34,10 +34,10 @@ import {
34
34
  } from "../chunk-MUEW424A.js";
35
35
  import {
36
36
  exitOnHelpFlag
37
- } from "../chunk-674M3JQF.js";
37
+ } from "../chunk-VYD27DKZ.js";
38
38
  import {
39
39
  isNewerVersion
40
- } from "../chunk-KVUBLB6K.js";
40
+ } from "../chunk-PPTC2QDB.js";
41
41
  import "../chunk-DQAN3JQP.js";
42
42
  import {
43
43
  getApiKey
@@ -14,8 +14,8 @@ import {
14
14
  } from "../chunk-GMXKITVA.js";
15
15
  import {
16
16
  exitOnHelpFlag
17
- } from "../chunk-674M3JQF.js";
18
- import "../chunk-KVUBLB6K.js";
17
+ } from "../chunk-VYD27DKZ.js";
18
+ import "../chunk-PPTC2QDB.js";
19
19
  import "../chunk-SAF6HGAA.js";
20
20
  import {
21
21
  UsageError
@@ -4,7 +4,7 @@ import {
4
4
  getPackageVersion,
5
5
  isCommandName,
6
6
  renderHelp
7
- } from "../chunk-KVUBLB6K.js";
7
+ } from "../chunk-PPTC2QDB.js";
8
8
  import {
9
9
  installProxyDispatcher
10
10
  } from "../chunk-SAF6HGAA.js";
@@ -80,7 +80,10 @@ var COMMAND_OPTIONS = {
80
80
  ["status", "Show the whole ladder"],
81
81
  ["clear", "Reset the default window"]
82
82
  ],
83
- status: [["--json", "Emit the whole report as one object instead of the screen"]],
83
+ status: [
84
+ ["--json", "Emit the whole report as one object instead of the screen"],
85
+ ["--devices", "Also list the machines registered to this site"]
86
+ ],
84
87
  logout: [
85
88
  ["--revoke", "Also kill the key server-side, so it stops working everywhere"],
86
89
  ["--json", "Emit the result as one object instead of the screen"]
@@ -0,0 +1,86 @@
1
+ import {
2
+ getPackageVersion
3
+ } from "./chunk-PPTC2QDB.js";
4
+ import {
5
+ getMachineId
6
+ } from "./chunk-RN3NOEJF.js";
7
+ import {
8
+ getBaseUrl
9
+ } from "./chunk-2UMNXADU.js";
10
+
11
+ // src/machines.ts
12
+ import { hostname } from "os";
13
+ var MACHINES_PATH = "/api/v1/server/machines";
14
+ var HEARTBEAT_INTERVAL_MS = 6e4;
15
+ var sendHeartbeat = async (apiKey, input = {}, deps = {}) => {
16
+ const fetchImpl = deps.fetchImpl ?? fetch;
17
+ const baseUrl = deps.baseUrl ?? getBaseUrl();
18
+ try {
19
+ const res = await fetchImpl(`${baseUrl}${MACHINES_PATH}`, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
22
+ body: JSON.stringify({
23
+ machineId: getMachineId(),
24
+ hostname: hostname(),
25
+ cliVersion: getPackageVersion(),
26
+ repoKey: input.repoKey ?? null,
27
+ capabilities: input.capabilities ?? []
28
+ }),
29
+ signal: AbortSignal.timeout(1e4)
30
+ });
31
+ if (res.status === 401 || res.status === 403) return { kind: "unauthorized" };
32
+ if (!res.ok) return { kind: "failed", detail: `HTTP ${res.status}` };
33
+ return { kind: "ok" };
34
+ } catch (err) {
35
+ return { kind: "failed", detail: err instanceof Error ? err.message : "network error" };
36
+ }
37
+ };
38
+ var parseMachine = (value) => {
39
+ if (typeof value !== "object" || value === null) return null;
40
+ const raw = value;
41
+ if (typeof raw.machineId !== "string" || typeof raw.hostname !== "string") return null;
42
+ return {
43
+ machineId: raw.machineId,
44
+ hostname: raw.hostname,
45
+ cliVersion: typeof raw.cliVersion === "string" ? raw.cliVersion : "unknown",
46
+ repoKey: typeof raw.repoKey === "string" ? raw.repoKey : null,
47
+ capabilities: Array.isArray(raw.capabilities) ? raw.capabilities.filter((c) => typeof c === "string") : [],
48
+ lastSeenAt: typeof raw.lastSeenAt === "string" ? raw.lastSeenAt : "",
49
+ online: raw.online === true
50
+ };
51
+ };
52
+ var listMachines = async (apiKey, deps = {}) => {
53
+ const fetchImpl = deps.fetchImpl ?? fetch;
54
+ const baseUrl = deps.baseUrl ?? getBaseUrl();
55
+ try {
56
+ const res = await fetchImpl(`${baseUrl}${MACHINES_PATH}`, {
57
+ headers: { Authorization: `Bearer ${apiKey}` },
58
+ signal: AbortSignal.timeout(1e4)
59
+ });
60
+ if (res.status === 404) return { kind: "unsupported" };
61
+ if (!res.ok) return { kind: "failed", detail: `HTTP ${res.status}` };
62
+ const body = await res.json().catch(() => null);
63
+ const list = body && Array.isArray(body.machines) ? body.machines : [];
64
+ return { kind: "ok", machines: list.map(parseMachine).filter((m) => m !== null) };
65
+ } catch (err) {
66
+ return { kind: "failed", detail: err instanceof Error ? err.message : "network error" };
67
+ }
68
+ };
69
+ var describeLastSeen = (iso, now = Date.now()) => {
70
+ const seen = Date.parse(iso);
71
+ if (!Number.isFinite(seen)) return "unknown";
72
+ const seconds = Math.max(0, Math.round((now - seen) / 1e3));
73
+ if (seconds < 60) return "just now";
74
+ const minutes = Math.round(seconds / 60);
75
+ if (minutes < 60) return `${minutes}m ago`;
76
+ const hours = Math.round(minutes / 60);
77
+ if (hours < 48) return `${hours}h ago`;
78
+ return `${Math.round(hours / 24)}d ago`;
79
+ };
80
+
81
+ export {
82
+ HEARTBEAT_INTERVAL_MS,
83
+ sendHeartbeat,
84
+ listMachines,
85
+ describeLastSeen
86
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getPackageVersion,
3
3
  renderCommandHelp
4
- } from "./chunk-KVUBLB6K.js";
4
+ } from "./chunk-PPTC2QDB.js";
5
5
  import {
6
6
  EXIT
7
7
  } from "./chunk-KZERVKTD.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.62.6",
3
+ "version": "0.63.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",