@oh-hai/cli 0.2.0 → 0.2.1
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 +51 -8
- package/dist/auth/resolve-token.d.ts +2 -4
- package/dist/auth/resolve-token.js +4 -11
- package/dist/auth/resolve-token.js.map +1 -1
- package/dist/bindings.d.ts +65 -0
- package/dist/bindings.js +242 -0
- package/dist/bindings.js.map +1 -0
- package/dist/cli.js +171 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/ask.js +21 -17
- package/dist/commands/ask.js.map +1 -1
- package/dist/commands/binary-update.d.ts +61 -0
- package/dist/commands/binary-update.js +160 -0
- package/dist/commands/binary-update.js.map +1 -0
- package/dist/commands/context.d.ts +123 -0
- package/dist/commands/doctor.js +30 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/flags.d.ts +14 -2
- package/dist/commands/flags.js +34 -6
- package/dist/commands/flags.js.map +1 -1
- package/dist/commands/handlers.js +6 -0
- package/dist/commands/handlers.js.map +1 -1
- package/dist/commands/inbox.js +129 -33
- package/dist/commands/inbox.js.map +1 -1
- package/dist/commands/messaging/identity.d.ts +9 -0
- package/dist/commands/messaging/identity.js +21 -0
- package/dist/commands/messaging/identity.js.map +1 -1
- package/dist/commands/messaging/validate.d.ts +6 -5
- package/dist/commands/messaging/validate.js +6 -5
- package/dist/commands/messaging/validate.js.map +1 -1
- package/dist/commands/registry.js +71 -8
- package/dist/commands/registry.js.map +1 -1
- package/dist/commands/setup.d.ts +17 -0
- package/dist/commands/setup.js +103 -3
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/task.js +21 -24
- package/dist/commands/task.js.map +1 -1
- package/dist/commands/teach.d.ts +13 -0
- package/dist/commands/teach.js +317 -0
- package/dist/commands/teach.js.map +1 -0
- package/dist/commands/update-check.d.ts +49 -0
- package/dist/commands/update-check.js +203 -0
- package/dist/commands/update-check.js.map +1 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +178 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/use.d.ts +2 -0
- package/dist/commands/use.js +99 -0
- package/dist/commands/use.js.map +1 -0
- package/dist/commands/whoami.js +113 -27
- package/dist/commands/whoami.js.map +1 -1
- package/dist/config-file.js +4 -20
- package/dist/config-file.js.map +1 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.js +39 -3
- package/dist/config.js.map +1 -1
- package/dist/exit-codes.d.ts +6 -0
- package/dist/exit-codes.js +6 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/help.js +4 -1
- package/dist/help.js.map +1 -1
- package/dist/os.d.ts +16 -0
- package/dist/os.js +32 -0
- package/dist/os.js.map +1 -0
- package/dist/url.d.ts +7 -0
- package/dist/url.js +14 -0
- package/dist/url.js.map +1 -1
- package/dist/version.d.ts +11 -3
- package/dist/version.js +20 -3
- package/dist/version.js.map +1 -1
- package/dist/watch-lock.d.ts +85 -0
- package/dist/watch-lock.js +358 -0
- package/dist/watch-lock.js.map +1 -0
- package/package.json +2 -2
package/dist/version.js
CHANGED
|
@@ -5,11 +5,28 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
/** The MA2H wire protocol version this CLI targets (docs/specs/cli.md §3). */
|
|
6
6
|
export const MA2H_WIRE_VERSION = "v0.3";
|
|
7
7
|
/**
|
|
8
|
-
* The CLI's own version
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The CLI's own version.
|
|
9
|
+
*
|
|
10
|
+
* A standalone binary built by `bun build --compile` (issue #344) has no package.json on the
|
|
11
|
+
* real filesystem to read, so the dist workflow inlines the version at compile time: it sets
|
|
12
|
+
* OH_HAI_BUILD_VERSION in the build env and passes `--env='OH_HAI_BUILD_*'`, which freezes this
|
|
13
|
+
* `process.env.OH_HAI_BUILD_VERSION` read into the bundle as a literal. We prefer that value **only
|
|
14
|
+
* when running as the compiled Bun executable** (`process.versions.bun` is set) — so a stray
|
|
15
|
+
* OH_HAI_BUILD_VERSION in the environment can NOT hijack `--version` on the npm/Node path, which
|
|
16
|
+
* stays exactly as before: fall back to reading the manifest relative to this module. That read
|
|
17
|
+
* works both from `dist/version.js` and `src/version.ts` via tsx — both sit one level under the
|
|
18
|
+
* package root, so `../package.json` resolves to the manifest either way.
|
|
11
19
|
*/
|
|
12
20
|
export function cliVersion() {
|
|
21
|
+
// Build-time injected version — trusted ONLY under Bun (the compiled binary), never on the
|
|
22
|
+
// Node/npm path, so an ambient OH_HAI_BUILD_VERSION can't spoof the npm CLI's version. In the
|
|
23
|
+
// compiled binary `process.env.OH_HAI_BUILD_VERSION` is the frozen `--env` literal, not a live
|
|
24
|
+
// read. Empty string is not a valid version, so an unset/blank value falls through to the manifest.
|
|
25
|
+
const runningUnderBun = typeof process.versions.bun === "string";
|
|
26
|
+
const injected = process.env.OH_HAI_BUILD_VERSION;
|
|
27
|
+
if (runningUnderBun && typeof injected === "string" && injected.length > 0) {
|
|
28
|
+
return injected;
|
|
29
|
+
}
|
|
13
30
|
try {
|
|
14
31
|
const manifestUrl = new URL("../package.json", import.meta.url);
|
|
15
32
|
const raw = readFileSync(fileURLToPath(manifestUrl), "utf8");
|
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,4FAA4F;AAE5F,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAExC
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,4FAA4F;AAE5F,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAExC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU;IACxB,2FAA2F;IAC3F,8FAA8F;IAC9F,+FAA+F;IAC/F,oGAAoG;IACpG,MAAM,eAAe,GAAG,OAAQ,OAAO,CAAC,QAA6B,CAAC,GAAG,KAAK,QAAQ,CAAC;IACvF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IAClD,IAAI,eAAe,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3E,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChE,MAAM,GAAG,GAAG,YAAY,CAAC,aAAa,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA0B,CAAC;QACxD,OAAO,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** The injected seam: all disk / clock / pid / host access. Production is `nodeWatchLockIo()`; tests
|
|
2
|
+
* pass an in-memory fake (mirrors `ConfigFileIo` in config-file.ts, `TeachFs` in context.ts). */
|
|
3
|
+
export interface WatchLockIo {
|
|
4
|
+
/** Read a lock file's contents, or undefined when absent/unreadable. */
|
|
5
|
+
readFile(path: string): string | undefined;
|
|
6
|
+
/** Atomically create a lock file ONLY if it does not already exist (O_EXCL), creating parent dirs.
|
|
7
|
+
* Returns true when this call created it, false when the file already existed. This is what makes a
|
|
8
|
+
* same-host cold-start acquire atomic — the loser of a create race sees `false` and re-evaluates
|
|
9
|
+
* rather than clobbering the winner. Throws on a real I/O failure (→ acquire degrades to unavailable). */
|
|
10
|
+
createExclusive(path: string, content: string): boolean;
|
|
11
|
+
/** Atomically (over)write a lock file, creating parent dirs (0700 dir, 0600 file). Used for the
|
|
12
|
+
* heartbeat refresh and to steal a stale/`--force` lock (last-writer-wins). */
|
|
13
|
+
writeFile(path: string, content: string): void;
|
|
14
|
+
/** Best-effort delete; must not throw when the file is already gone. */
|
|
15
|
+
removeFile(path: string): void;
|
|
16
|
+
/** Is a process with this pid alive ON THIS HOST? (meaningful only same-host — pids are host-local). */
|
|
17
|
+
pidAlive(pid: number): boolean;
|
|
18
|
+
/** Epoch milliseconds. */
|
|
19
|
+
now(): number;
|
|
20
|
+
hostname: string;
|
|
21
|
+
pid: number;
|
|
22
|
+
homedir: string;
|
|
23
|
+
xdgStateHome: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
/** A live handle over an acquired lock. */
|
|
26
|
+
export interface WatchLock {
|
|
27
|
+
/** Refresh the heartbeat (call each poll). Best-effort — swallows write errors so a transient disk
|
|
28
|
+
* failure never crashes the loop — and RETURNS whether the write succeeded so the caller can warn
|
|
29
|
+
* once a heartbeat has been failing long enough that a peer may have reclaimed the lock (a silent
|
|
30
|
+
* heartbeat death would otherwise let two watchers drain with no signal). */
|
|
31
|
+
heartbeat(): boolean;
|
|
32
|
+
/** Release on exit — deletes the lockfile IFF it is still ours (a reclaimer/`--force` steal may
|
|
33
|
+
* own it now). Best-effort. The re-read→delete is itself non-atomic vs. a concurrent `--force`
|
|
34
|
+
* steal landing between the two, so a stolen lock could in principle be deleted — a bounded,
|
|
35
|
+
* self-healing residual (the stealer just re-acquires), not fixable without a real CAS. */
|
|
36
|
+
release(): void;
|
|
37
|
+
}
|
|
38
|
+
/** Human-facing (stderr) info about a previous holder — never includes a secret. */
|
|
39
|
+
export interface HolderInfo {
|
|
40
|
+
pid: number;
|
|
41
|
+
hostname: string;
|
|
42
|
+
origin: string;
|
|
43
|
+
agentId: string;
|
|
44
|
+
startedAt: number;
|
|
45
|
+
heartbeatAt: number;
|
|
46
|
+
}
|
|
47
|
+
export type AcquireOutcome = {
|
|
48
|
+
kind: "acquired";
|
|
49
|
+
lock: WatchLock;
|
|
50
|
+
reclaimed: {
|
|
51
|
+
holder: HolderInfo;
|
|
52
|
+
wasLive: boolean;
|
|
53
|
+
} | null;
|
|
54
|
+
} | {
|
|
55
|
+
kind: "held";
|
|
56
|
+
holder: HolderInfo | undefined;
|
|
57
|
+
} | {
|
|
58
|
+
kind: "unavailable";
|
|
59
|
+
reason: string;
|
|
60
|
+
};
|
|
61
|
+
/** Resolve the lock directory: `$XDG_STATE_HOME/oh-hai/watch`, else `~/.local/state/oh-hai/watch`,
|
|
62
|
+
* else undefined (guard unavailable). Mirrors `loadUserConfig`'s absolute-dir gating — a trusted dir
|
|
63
|
+
* must be ABSOLUTE, so a relative XDG/home is ignored. `$XDG_RUNTIME_DIR` is deliberately NOT
|
|
64
|
+
* consulted: its presence varies per shell/session, which would split one identity across two dirs
|
|
65
|
+
* and defeat the guard. A single stable per-user location is the point. */
|
|
66
|
+
export declare function resolveWatchDir(io: WatchLockIo): string | undefined;
|
|
67
|
+
/** The lock filename for an identity. The Hub mailbox is scoped to the AGENT `(accountId, agentId)`,
|
|
68
|
+
* so the discriminator is `(origin, agentId)` — NOT the bearer (one agent can hold several tokens).
|
|
69
|
+
* sha256 gives a clean fixed-length, filesystem-safe name from an arbitrary agent id. NUL-separated
|
|
70
|
+
* so (`ab`, `c`) and (`a`, `bc`) can't collide. */
|
|
71
|
+
export declare function lockFilename(origin: string, agentId: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* Acquire the per-identity watch lock. Refuses when a LIVE watcher already holds this
|
|
74
|
+
* `(origin, agentId)` (unless `force`), reclaims a stale/crashed holder, and degrades to
|
|
75
|
+
* `unavailable` (never throws) when no lock directory can be resolved or the write fails — locking
|
|
76
|
+
* is a safety net, not a hard dependency.
|
|
77
|
+
*/
|
|
78
|
+
export declare function acquireWatchLock(io: WatchLockIo, params: {
|
|
79
|
+
origin: string;
|
|
80
|
+
agentId: string;
|
|
81
|
+
intervalMs: number;
|
|
82
|
+
force: boolean;
|
|
83
|
+
}): AcquireOutcome;
|
|
84
|
+
/** Production `WatchLockIo` — real filesystem, clock, process table, and environment. */
|
|
85
|
+
export declare function nodeWatchLockIo(): WatchLockIo;
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// Concurrent-watch drain-safety guard for `oh-hai inbox watch` (issue #323).
|
|
2
|
+
//
|
|
3
|
+
// `inbox watch` is a DESTRUCTIVE drain+ack of the agent's mailbox — the Hub hands each directive to
|
|
4
|
+
// whichever watcher polls first, so two watchers sharing one identity race and the losing session
|
|
5
|
+
// silently never sees the message. This module is the local, cross-process guard: a lockfile keyed
|
|
6
|
+
// to the identity, with a pid + heartbeat liveness check, so one identity has at most one live
|
|
7
|
+
// watcher — while a crashed watcher's lock is reclaimed (never a permanent block) and a genuinely
|
|
8
|
+
// different identity is never affected.
|
|
9
|
+
//
|
|
10
|
+
// STRENGTH / LIMITS (this is a best-effort courtesy guard, NOT a kernel mutex): it reliably catches
|
|
11
|
+
// the common case — a second watcher started while one is ALREADY established — and closes the
|
|
12
|
+
// same-host cold-start race with an atomic exclusive create (O_EXCL). Two RESIDUAL windows remain,
|
|
13
|
+
// both bounded and self-healing (never a permanent wedge): (a) two watchers racing to RECLAIM the
|
|
14
|
+
// *same* stale lock in the same instant can both proceed — a write-then-verify re-read shrinks but
|
|
15
|
+
// can't fully close it (a userspace lockfile can't compare-and-swap a replace); (b) watchers on
|
|
16
|
+
// DIFFERENT hosts for one identity can't see each other (no shared state) and will split the mailbox.
|
|
17
|
+
// Both are inherent to a per-user-local lockfile; the server-side race they bound is not eliminable here.
|
|
18
|
+
//
|
|
19
|
+
// IDENTITY: the Hub mailbox is scoped to the AGENT — `(accountId, agentId)` (server drainInbox/
|
|
20
|
+
// ackDirectives) — and one agent can hold several active bearers at once (token rotation, env vs
|
|
21
|
+
// keychain). Keying on the token would give two watchers for the SAME agent different locks and miss
|
|
22
|
+
// the race, so the key is the resolved AGENT ID, not the bearer — which also means the token never
|
|
23
|
+
// enters this module at all (§5.4). The lock FILENAME is a sha256 of `(origin, agentId)` for a clean
|
|
24
|
+
// fixed-length name; the BODY carries pid / host / timestamps / origin / agentId / interval — no secret.
|
|
25
|
+
// All disk / clock / pid / host access goes through an injected `WatchLockIo` so the logic is
|
|
26
|
+
// unit-tested against an in-memory map with zero disk (mirrors `ConfigFileIo` and `TeachFs`).
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
import { linkSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { homedir as osHomedir, hostname as osHostname } from "node:os";
|
|
30
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
31
|
+
import { writeSecureFile } from "./auth/secure-write.js";
|
|
32
|
+
import { safeOsValue } from "./os.js";
|
|
33
|
+
/** Freshness floor: a lock whose heartbeat is younger than this is fresh regardless of the holder's
|
|
34
|
+
* interval. Comfortably above the default 5s poll so a normal watcher between polls is never stale. */
|
|
35
|
+
const STALE_FLOOR_MS = 30_000;
|
|
36
|
+
/** A lock is stale once its heartbeat is older than `intervalMs * this` (or the floor, whichever is
|
|
37
|
+
* larger) — sized from the holder's OWN cadence (recorded in the lock) so a deliberately slow poller
|
|
38
|
+
* (`--interval 120000`) isn't falsely declared dead. Three missed heartbeats of slack. */
|
|
39
|
+
const STALE_MULTIPLIER = 3;
|
|
40
|
+
/** Window-sizing fallback for a record whose `intervalMs` is missing/absurd. Distinct from the poll
|
|
41
|
+
* default in inbox.ts (that one is the flag default handed to the loop) — this one is ONLY the stale
|
|
42
|
+
* window basis when a record's interval can't be trusted. The two are intentionally NOT coupled. */
|
|
43
|
+
const DEFAULT_INTERVAL_MS = 5000;
|
|
44
|
+
/** Ceiling on the interval used for stale-window sizing. Caps the window at `this * STALE_MULTIPLIER`
|
|
45
|
+
* (30 min) so a corrupt/hostile record with a huge `intervalMs` can't push a CROSS-host lock's
|
|
46
|
+
* freshness window out to weeks. Well above any realistic presence-poll cadence (default 5s), so a
|
|
47
|
+
* legitimate slow poller is unaffected; same-host liveness is governed by `pidAlive` regardless. */
|
|
48
|
+
const MAX_WINDOW_INTERVAL_MS = 600_000;
|
|
49
|
+
/** Bytes of the sha256 hex digest kept for the filename — 128 bits is collision-safe and one-way. */
|
|
50
|
+
const KEY_HEX_CHARS = 32;
|
|
51
|
+
/** The max epoch-ms `Date` can represent (`Date#toISOString` throws `RangeError` beyond ±this). A
|
|
52
|
+
* record whose timestamps fall outside [0, this] is treated as malformed, so a corrupt/hostile lock
|
|
53
|
+
* can't make a later `new Date(ts).toISOString()` throw. */
|
|
54
|
+
const MAX_TIMESTAMP_MS = 8_640_000_000_000_000;
|
|
55
|
+
/** The max pid `process.kill` accepts (a 32-bit signed positive int). A larger value throws
|
|
56
|
+
* `ERR_INVALID_ARG_TYPE` (NOT `ESRCH`), which `pidAlive` would otherwise read as "alive" and wedge the
|
|
57
|
+
* lock — so a record whose pid exceeds this is treated as malformed → reclaimable. */
|
|
58
|
+
const MAX_PID = 2_147_483_647;
|
|
59
|
+
/** Resolve the lock directory: `$XDG_STATE_HOME/oh-hai/watch`, else `~/.local/state/oh-hai/watch`,
|
|
60
|
+
* else undefined (guard unavailable). Mirrors `loadUserConfig`'s absolute-dir gating — a trusted dir
|
|
61
|
+
* must be ABSOLUTE, so a relative XDG/home is ignored. `$XDG_RUNTIME_DIR` is deliberately NOT
|
|
62
|
+
* consulted: its presence varies per shell/session, which would split one identity across two dirs
|
|
63
|
+
* and defeat the guard. A single stable per-user location is the point. */
|
|
64
|
+
export function resolveWatchDir(io) {
|
|
65
|
+
const xdg = io.xdgStateHome?.trim();
|
|
66
|
+
const home = io.homedir.trim();
|
|
67
|
+
const base = xdg !== undefined && xdg !== "" && isAbsolute(xdg)
|
|
68
|
+
? xdg
|
|
69
|
+
: home !== "" && isAbsolute(home)
|
|
70
|
+
? join(home, ".local", "state")
|
|
71
|
+
: undefined;
|
|
72
|
+
return base === undefined ? undefined : join(base, "oh-hai", "watch");
|
|
73
|
+
}
|
|
74
|
+
/** The lock filename for an identity. The Hub mailbox is scoped to the AGENT `(accountId, agentId)`,
|
|
75
|
+
* so the discriminator is `(origin, agentId)` — NOT the bearer (one agent can hold several tokens).
|
|
76
|
+
* sha256 gives a clean fixed-length, filesystem-safe name from an arbitrary agent id. NUL-separated
|
|
77
|
+
* so (`ab`, `c`) and (`a`, `bc`) can't collide. */
|
|
78
|
+
export function lockFilename(origin, agentId) {
|
|
79
|
+
const digest = createHash("sha256").update(`${origin}\u0000${agentId}`).digest("hex");
|
|
80
|
+
return `${digest.slice(0, KEY_HEX_CHARS)}.lock`;
|
|
81
|
+
}
|
|
82
|
+
/** Parse a lock file, tolerating anything malformed as "no valid lock" (→ reclaimable). */
|
|
83
|
+
function parseRecord(raw) {
|
|
84
|
+
if (raw === undefined)
|
|
85
|
+
return undefined;
|
|
86
|
+
let obj;
|
|
87
|
+
try {
|
|
88
|
+
obj = JSON.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
if (typeof obj !== "object" || obj === null)
|
|
94
|
+
return undefined;
|
|
95
|
+
const r = obj;
|
|
96
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
|
|
97
|
+
// Timestamps must be within the range Date can format — a corrupt `heartbeatAt: 1e20` would otherwise
|
|
98
|
+
// survive parsing and later throw RangeError from `new Date(ts).toISOString()` in a warning. Treat
|
|
99
|
+
// out-of-range as malformed → reclaimable.
|
|
100
|
+
const ts = (v) => {
|
|
101
|
+
const n = num(v);
|
|
102
|
+
return n !== undefined && n >= 0 && n <= MAX_TIMESTAMP_MS ? n : undefined;
|
|
103
|
+
};
|
|
104
|
+
// The pid must be a POSITIVE integer within `process.kill`'s valid range. A corrupt `pid: 0` / `-1`
|
|
105
|
+
// / `1.5` would reach `process.kill(pid, 0)` — where 0 targets our process group and -1 every process
|
|
106
|
+
// — and a pid above MAX_PID throws `ERR_INVALID_ARG_TYPE` that `pidAlive` reads as "alive"; all of
|
|
107
|
+
// these could wedge the lock. Reject them as malformed → reclaimable.
|
|
108
|
+
const pidVal = num(r.pid);
|
|
109
|
+
const pid = pidVal !== undefined && Number.isInteger(pidVal) && pidVal >= 1 && pidVal <= MAX_PID ? pidVal : undefined;
|
|
110
|
+
const startedAt = ts(r.startedAt);
|
|
111
|
+
const heartbeatAt = ts(r.heartbeatAt);
|
|
112
|
+
if (pid === undefined || startedAt === undefined || heartbeatAt === undefined)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (typeof r.hostname !== "string" || typeof r.origin !== "string" || typeof r.agentId !== "string")
|
|
115
|
+
return undefined;
|
|
116
|
+
const intervalMs = num(r.intervalMs);
|
|
117
|
+
return {
|
|
118
|
+
v: 1,
|
|
119
|
+
pid,
|
|
120
|
+
hostname: r.hostname,
|
|
121
|
+
startedAt,
|
|
122
|
+
heartbeatAt,
|
|
123
|
+
origin: r.origin,
|
|
124
|
+
agentId: r.agentId,
|
|
125
|
+
// A missing/absurd interval falls back to the default window sizing rather than making the
|
|
126
|
+
// holder look permanently fresh (a huge interval) or instantly stale (zero/negative).
|
|
127
|
+
intervalMs: intervalMs !== undefined && intervalMs > 0 ? intervalMs : DEFAULT_INTERVAL_MS,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Is this holder still live (→ a second watcher must refuse)?
|
|
131
|
+
*
|
|
132
|
+
* SAME-HOST: trust the pid, NOT the heartbeat. A live pid means the watcher is running, even if it's
|
|
133
|
+
* been stuck for a while in a long drain/ack/flush (`--timeout 60000`, a blocked stdout pipe) without
|
|
134
|
+
* refreshing its heartbeat — reclaiming it on heartbeat-age would let a second process drain the same
|
|
135
|
+
* mailbox concurrently, the exact double-drain the guard exists to prevent. A crashed watcher's pid is
|
|
136
|
+
* gone, so `!pidAlive` still reclaims it; our own leftover pid is never live. (The residual: a dead
|
|
137
|
+
* watcher's pid recycled by an UNRELATED live process reads as live until that process exits or a
|
|
138
|
+
* `--force`; heartbeat age is not consulted same-host, so this is bounded by `--force`, not weeks.)
|
|
139
|
+
*
|
|
140
|
+
* CROSS-HOST: pids are host-local and unverifiable, so fall back to heartbeat freshness alone. */
|
|
141
|
+
function isLive(rec, io) {
|
|
142
|
+
if (rec.hostname === io.hostname) {
|
|
143
|
+
if (rec.pid === io.pid)
|
|
144
|
+
return false; // our own stale leftover → reclaim
|
|
145
|
+
return io.pidAlive(rec.pid); // alive → live (regardless of heartbeat age); dead → reclaim
|
|
146
|
+
}
|
|
147
|
+
// Clamp the sizing interval so a corrupt/hostile record can't inflate the window to weeks (adversarial).
|
|
148
|
+
const sizingInterval = Math.min(rec.intervalMs, MAX_WINDOW_INTERVAL_MS);
|
|
149
|
+
const window = Math.max(STALE_FLOOR_MS, sizingInterval * STALE_MULTIPLIER);
|
|
150
|
+
const age = io.now() - rec.heartbeatAt;
|
|
151
|
+
// cross-host: fresh iff the heartbeat is within ±window of now. A small clock skew (either sign) is
|
|
152
|
+
// tolerated, but a heartbeat FAR in the future is as implausible as one far in the past — a corrupt or
|
|
153
|
+
// wildly-skewed-clock lock must NOT read as "live for years" and block crash self-healing (bound the
|
|
154
|
+
// future skew, don't let a negative age be unconditionally fresh).
|
|
155
|
+
return Math.abs(age) <= window;
|
|
156
|
+
}
|
|
157
|
+
function holderInfo(rec) {
|
|
158
|
+
return {
|
|
159
|
+
pid: rec.pid,
|
|
160
|
+
hostname: rec.hostname,
|
|
161
|
+
origin: rec.origin,
|
|
162
|
+
agentId: rec.agentId,
|
|
163
|
+
startedAt: rec.startedAt,
|
|
164
|
+
heartbeatAt: rec.heartbeatAt,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** Do two records identify the same acquisition? Keyed on pid+host+startedAt — startedAt is our
|
|
168
|
+
* immutable acquisition stamp, while heartbeatAt changes each poll and is deliberately excluded. */
|
|
169
|
+
function sameHolder(a, b) {
|
|
170
|
+
return a.pid === b.pid && a.hostname === b.hostname && a.startedAt === b.startedAt;
|
|
171
|
+
}
|
|
172
|
+
function makeLock(io, path, record) {
|
|
173
|
+
return {
|
|
174
|
+
heartbeat() {
|
|
175
|
+
// Verify we STILL own the lock before refreshing it. If a reclaimer or a `--force` steal took it
|
|
176
|
+
// over while we ran, an unconditional rewrite would clobber their record with ours — we'd "take
|
|
177
|
+
// the lock back", then our release() would delete it, leaving them unguarded. A displaced holder
|
|
178
|
+
// must NOT write; report false so the caller can warn (it has effectively lost the lock).
|
|
179
|
+
const current = parseRecord(io.readFile(path));
|
|
180
|
+
if (current === undefined || !sameHolder(current, record))
|
|
181
|
+
return false;
|
|
182
|
+
try {
|
|
183
|
+
io.writeFile(path, JSON.stringify({ ...record, heartbeatAt: io.now() }));
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false; // best-effort — see the interface doc; the caller warns on sustained failure.
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
release() {
|
|
191
|
+
// Delete only if the lock on disk is still OURS. A reclaimer or a `--force` steal may have
|
|
192
|
+
// overwritten it; deleting theirs would drop their guard.
|
|
193
|
+
const current = parseRecord(io.readFile(path));
|
|
194
|
+
if (current !== undefined && sameHolder(current, record)) {
|
|
195
|
+
try {
|
|
196
|
+
io.removeFile(path);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// best-effort
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Acquire the per-identity watch lock. Refuses when a LIVE watcher already holds this
|
|
207
|
+
* `(origin, agentId)` (unless `force`), reclaims a stale/crashed holder, and degrades to
|
|
208
|
+
* `unavailable` (never throws) when no lock directory can be resolved or the write fails — locking
|
|
209
|
+
* is a safety net, not a hard dependency.
|
|
210
|
+
*/
|
|
211
|
+
export function acquireWatchLock(io, params) {
|
|
212
|
+
const dir = resolveWatchDir(io);
|
|
213
|
+
if (dir === undefined) {
|
|
214
|
+
return { kind: "unavailable", reason: "no XDG_STATE_HOME or home directory to place the lock file" };
|
|
215
|
+
}
|
|
216
|
+
const path = join(dir, lockFilename(params.origin, params.agentId));
|
|
217
|
+
// Read the RAW bytes so we can tell "absent" (undefined) from "present-but-unparseable" (a corrupt
|
|
218
|
+
// record) — the two are distinct: an absent slot is claimed via O_EXCL, a corrupt one is stolen.
|
|
219
|
+
const raw = io.readFile(path);
|
|
220
|
+
const existing = parseRecord(raw);
|
|
221
|
+
if (existing !== undefined && !params.force && isLive(existing, io)) {
|
|
222
|
+
return { kind: "held", holder: holderInfo(existing) };
|
|
223
|
+
}
|
|
224
|
+
const now = io.now();
|
|
225
|
+
const record = {
|
|
226
|
+
v: 1,
|
|
227
|
+
pid: io.pid,
|
|
228
|
+
hostname: io.hostname,
|
|
229
|
+
startedAt: now,
|
|
230
|
+
heartbeatAt: now,
|
|
231
|
+
origin: params.origin,
|
|
232
|
+
agentId: params.agentId,
|
|
233
|
+
intervalMs: params.intervalMs,
|
|
234
|
+
};
|
|
235
|
+
const content = JSON.stringify(record);
|
|
236
|
+
const fail = (error) => ({ kind: "unavailable", reason: error instanceof Error ? error.message : String(error) });
|
|
237
|
+
// Steal an existing lock (stale holder, `--force` takeover, or a corrupt record): overwrite
|
|
238
|
+
// last-writer-wins, then for a NON-force steal verify we actually won — a concurrent claimer whose
|
|
239
|
+
// write landed after ours shows a mismatch, so we back off to `held` rather than double-drain. This
|
|
240
|
+
// shrinks (does not fully close) the two-reclaimers window; `--force` is an explicit takeover so it
|
|
241
|
+
// skips the verify.
|
|
242
|
+
const steal = (reclaimed) => {
|
|
243
|
+
try {
|
|
244
|
+
io.writeFile(path, content);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
return fail(error);
|
|
248
|
+
}
|
|
249
|
+
if (!params.force) {
|
|
250
|
+
const after = parseRecord(io.readFile(path));
|
|
251
|
+
if (after !== undefined && !sameHolder(after, record)) {
|
|
252
|
+
return { kind: "held", holder: holderInfo(after) };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { kind: "acquired", lock: makeLock(io, path, record), reclaimed };
|
|
256
|
+
};
|
|
257
|
+
if (raw === undefined) {
|
|
258
|
+
// TRULY absent → claim it with an ATOMIC exclusive create, closing the same-host cold-start race.
|
|
259
|
+
let created;
|
|
260
|
+
try {
|
|
261
|
+
created = io.createExclusive(path, content);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
return fail(error);
|
|
265
|
+
}
|
|
266
|
+
if (created) {
|
|
267
|
+
return { kind: "acquired", lock: makeLock(io, path, record), reclaimed: null };
|
|
268
|
+
}
|
|
269
|
+
// Lost the create race — a peer created the lock between our read and our create. Re-read the winner.
|
|
270
|
+
const winner = parseRecord(io.readFile(path));
|
|
271
|
+
if (winner === undefined) {
|
|
272
|
+
// The winner's record exists (EEXIST) but isn't readable/parseable — a transient read failure, or a
|
|
273
|
+
// pre-existing UNREADABLE/corrupt lock (bad perms, a manual repair). Back off rather than steal a
|
|
274
|
+
// possibly-live peer — UNLESS `--force`, the documented escape hatch: a rename-based write clears a
|
|
275
|
+
// lock even when the read couldn't open it, so `--force` must not dead-end on an unreadable file.
|
|
276
|
+
return params.force ? steal(null) : { kind: "held", holder: undefined };
|
|
277
|
+
}
|
|
278
|
+
if (!params.force && isLive(winner, io)) {
|
|
279
|
+
return { kind: "held", holder: holderInfo(winner) };
|
|
280
|
+
}
|
|
281
|
+
// The winner is already stale (raced then died), or we're forcing → steal it.
|
|
282
|
+
return steal({ holder: holderInfo(winner), wasLive: isLive(winner, io) });
|
|
283
|
+
}
|
|
284
|
+
// `raw` was DEFINED: a stale/force-over live lock (live+!force was handled above), or an unparseable
|
|
285
|
+
// record → steal it. When `existing === undefined` the on-disk bytes are corrupt — reclaimable, no
|
|
286
|
+
// holder info. This is safe because every writer creates the lock ATOMICALLY (createExclusive links a
|
|
287
|
+
// fully-written temp; writeFile is a temp+rename), so a fresh winner is NEVER partially visible — an
|
|
288
|
+
// unparseable present record is genuine corruption, not a mid-write winner, so reclaiming it can't
|
|
289
|
+
// clobber a live watcher. (`--force` reaches here too and skips the steal's verify.)
|
|
290
|
+
return steal(existing !== undefined ? { holder: holderInfo(existing), wasLive: isLive(existing, io) } : null);
|
|
291
|
+
}
|
|
292
|
+
// Per-process counter so two createExclusive temp files from this process never collide.
|
|
293
|
+
let tmpSeq = 0;
|
|
294
|
+
/** Production `WatchLockIo` — real filesystem, clock, process table, and environment. */
|
|
295
|
+
export function nodeWatchLockIo() {
|
|
296
|
+
return {
|
|
297
|
+
readFile: (path) => {
|
|
298
|
+
try {
|
|
299
|
+
return readFileSync(path, "utf8");
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return undefined; // absent / unreadable → "no lock here"
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
createExclusive: (path, content) => {
|
|
306
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
307
|
+
// Write the FULL content to a temp file, then hard-link it into place. `link()` is atomic and
|
|
308
|
+
// fails with EEXIST if the target exists (the O_EXCL semantics we need) — and, crucially, the
|
|
309
|
+
// target appears with its COMPLETE content in a single step. So a concurrent reader never sees an
|
|
310
|
+
// empty/partial fresh lock (the window in which it could be mistaken for a corrupt record and
|
|
311
|
+
// stolen, letting both watchers drain). A plain `wx` write does NOT give this: it creates the file
|
|
312
|
+
// empty and then fills it, so a reader can catch it mid-write.
|
|
313
|
+
const tmp = `${path}.tmp.${process.pid}.${tmpSeq++}`;
|
|
314
|
+
try {
|
|
315
|
+
writeFileSync(tmp, content, { mode: 0o600 });
|
|
316
|
+
linkSync(tmp, path);
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
if (error.code === "EEXIST")
|
|
321
|
+
return false;
|
|
322
|
+
throw error; // a real I/O error → acquire degrades to unavailable
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
try {
|
|
326
|
+
rmSync(tmp, { force: true }); // drop the temp link; the target keeps the content via its own link
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// best-effort temp cleanup
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
writeFile: (path, content) => writeSecureFile(path, content),
|
|
334
|
+
removeFile: (path) => rmSync(path, { force: true }),
|
|
335
|
+
pidAlive: (pid) => {
|
|
336
|
+
try {
|
|
337
|
+
// Signal 0 performs the existence/permission check without actually signalling.
|
|
338
|
+
process.kill(pid, 0);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
// Alive ONLY when the error proves the process exists: EPERM (it exists but is owned by another
|
|
343
|
+
// user). ESRCH (no such process) and every OTHER error — notably `ERR_INVALID_ARG_TYPE`/
|
|
344
|
+
// `RangeError` for an out-of-range pid — count as NOT alive, so a malformed pid can't wedge the
|
|
345
|
+
// lock by masquerading as live. (parseRecord already rejects out-of-range pids; this is the
|
|
346
|
+
// defense-in-depth backstop.) POSIX-oriented: on Windows the distinction is less reliable, so
|
|
347
|
+
// same-host reclaim degrades toward the safe heartbeat-freshness path — the guard still holds.
|
|
348
|
+
return error.code === "EPERM";
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
now: () => Date.now(),
|
|
352
|
+
hostname: safeOsValue(osHostname),
|
|
353
|
+
pid: process.pid,
|
|
354
|
+
homedir: safeOsValue(osHomedir),
|
|
355
|
+
xdgStateHome: process.env.XDG_STATE_HOME,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
//# sourceMappingURL=watch-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-lock.js","sourceRoot":"","sources":["../src/watch-lock.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,EAAE;AACF,oGAAoG;AACpG,kGAAkG;AAClG,mGAAmG;AACnG,+FAA+F;AAC/F,kGAAkG;AAClG,wCAAwC;AACxC,EAAE;AACF,oGAAoG;AACpG,+FAA+F;AAC/F,mGAAmG;AACnG,kGAAkG;AAClG,mGAAmG;AACnG,gGAAgG;AAChG,sGAAsG;AACtG,0GAA0G;AAC1G,EAAE;AACF,gGAAgG;AAChG,iGAAiG;AACjG,qGAAqG;AACrG,mGAAmG;AACnG,qGAAqG;AACrG,yGAAyG;AACzG,8FAA8F;AAC9F,8FAA8F;AAE9F,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,QAAQ,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AACvE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC;wGACwG;AACxG,MAAM,cAAc,GAAG,MAAM,CAAC;AAC9B;;2FAE2F;AAC3F,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAC3B;;qGAEqG;AACrG,MAAM,mBAAmB,GAAG,IAAI,CAAC;AACjC;;;qGAGqG;AACrG,MAAM,sBAAsB,GAAG,OAAO,CAAC;AACvC,qGAAqG;AACrG,MAAM,aAAa,GAAG,EAAE,CAAC;AACzB;;6DAE6D;AAC7D,MAAM,gBAAgB,GAAG,qBAAqB,CAAC;AAC/C;;uFAEuF;AACvF,MAAM,OAAO,GAAG,aAAa,CAAC;AAuE9B;;;;4EAI4E;AAC5E,MAAM,UAAU,eAAe,CAAC,EAAe;IAC7C,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC;IACpC,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IAC/B,MAAM,IAAI,GACR,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE,IAAI,UAAU,CAAC,GAAG,CAAC;QAChD,CAAC,CAAC,GAAG;QACL,CAAC,CAAC,IAAI,KAAK,EAAE,IAAI,UAAU,CAAC,IAAI,CAAC;YAC/B,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC;YAC/B,CAAC,CAAC,SAAS,CAAC;IAClB,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;AACxE,CAAC;AAED;;;oDAGoD;AACpD,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,OAAe;IAC1D,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,MAAM,SAAS,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACtF,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC;AAClD,CAAC;AAED,2FAA2F;AAC3F,SAAS,WAAW,CAAC,GAAuB;IAC1C,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACxC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC9D,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,MAAM,GAAG,GAAG,CAAC,CAAU,EAAsB,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC9G,sGAAsG;IACtG,mGAAmG;IACnG,2CAA2C;IAC3C,MAAM,EAAE,GAAG,CAAC,CAAU,EAAsB,EAAE;QAC5C,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACjB,OAAO,CAAC,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5E,CAAC,CAAC;IACF,oGAAoG;IACpG,sGAAsG;IACtG,mGAAmG;IACnG,sEAAsE;IACtE,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC1B,MAAM,GAAG,GAAG,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,IAAI,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IACtH,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAClC,MAAM,WAAW,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACtC,IAAI,GAAG,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAChG,IAAI,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACtH,MAAM,UAAU,GAAG,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IACrC,OAAO;QACL,CAAC,EAAE,CAAC;QACJ,GAAG;QACH,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,SAAS;QACT,WAAW;QACX,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,2FAA2F;QAC3F,sFAAsF;QACtF,UAAU,EAAE,UAAU,KAAK,SAAS,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,mBAAmB;KAC1F,CAAC;AACJ,CAAC;AAED;;;;;;;;;;mGAUmG;AACnG,SAAS,MAAM,CAAC,GAAe,EAAE,EAAe;IAC9C,IAAI,GAAG,CAAC,QAAQ,KAAK,EAAE,CAAC,QAAQ,EAAE,CAAC;QACjC,IAAI,GAAG,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG;YAAE,OAAO,KAAK,CAAC,CAAC,mCAAmC;QACzE,OAAO,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,6DAA6D;IAC5F,CAAC;IACD,yGAAyG;IACzG,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,sBAAsB,CAAC,CAAC;IACxE,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,cAAc,GAAG,gBAAgB,CAAC,CAAC;IAC3E,MAAM,GAAG,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,WAAW,CAAC;IACvC,oGAAoG;IACpG,uGAAuG;IACvG,qGAAqG;IACrG,mEAAmE;IACnE,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC;AACjC,CAAC;AAED,SAAS,UAAU,CAAC,GAAe;IACjC,OAAO;QACL,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,WAAW,EAAE,GAAG,CAAC,WAAW;KAC7B,CAAC;AACJ,CAAC;AAED;qGACqG;AACrG,SAAS,UAAU,CAAC,CAAa,EAAE,CAAa;IAC9C,OAAO,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,CAAC;AACrF,CAAC;AAED,SAAS,QAAQ,CAAC,EAAe,EAAE,IAAY,EAAE,MAAkB;IACjE,OAAO;QACL,SAAS;YACP,iGAAiG;YACjG,gGAAgG;YAChG,iGAAiG;YACjG,0FAA0F;YAC1F,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YAC/C,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC;gBAAE,OAAO,KAAK,CAAC;YACxE,IAAI,CAAC;gBACH,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,MAAM,EAAE,WAAW,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;gBACzE,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC,CAAC,8EAA8E;YAC9F,CAAC;QACH,CAAC;QACD,OAAO;YACL,2FAA2F;YAC3F,0DAA0D;YAC1D,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YAC/C,IAAI,OAAO,KAAK,SAAS,IAAI,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC;gBACzD,IAAI,CAAC;oBACH,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;gBACtB,CAAC;gBAAC,MAAM,CAAC;oBACP,cAAc;gBAChB,CAAC;YACH,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,EAAe,EACf,MAA+E;IAE/E,MAAM,GAAG,GAAG,eAAe,CAAC,EAAE,CAAC,CAAC;IAChC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,4DAA4D,EAAE,CAAC;IACvG,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAEpE,mGAAmG;IACnG,iGAAiG;IACjG,MAAM,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC;QACpE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;IACxD,CAAC;IAED,MAAM,GAAG,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;IACrB,MAAM,MAAM,GAAe;QACzB,CAAC,EAAE,CAAC;QACJ,GAAG,EAAE,EAAE,CAAC,GAAG;QACX,QAAQ,EAAE,EAAE,CAAC,QAAQ;QACrB,SAAS,EAAE,GAAG;QACd,WAAW,EAAE,GAAG;QAChB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU;KAC9B,CAAC;IACF,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,CAAC,KAAc,EAAkB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAE3I,4FAA4F;IAC5F,mGAAmG;IACnG,oGAAoG;IACpG,oGAAoG;IACpG,oBAAoB;IACpB,MAAM,KAAK,GAAG,CAAC,SAA0D,EAAkB,EAAE;QAC3F,IAAI,CAAC;YACH,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC9B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAClB,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YAC7C,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC;gBACtD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YACrD,CAAC;QACH,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IAC3E,CAAC,CAAC;IAEF,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,kGAAkG;QAClG,IAAI,OAAgB,CAAC;QACrB,IAAI,CAAC;YACH,OAAO,GAAG,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;QACjF,CAAC;QACD,sGAAsG;QACtG,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QAC9C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,oGAAoG;YACpG,kGAAkG;YAClG,oGAAoG;YACpG,kGAAkG;YAClG,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC1E,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;YACxC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACtD,CAAC;QACD,8EAA8E;QAC9E,OAAO,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;IAC5E,CAAC;IAED,qGAAqG;IACrG,mGAAmG;IACnG,sGAAsG;IACtG,qGAAqG;IACrG,mGAAmG;IACnG,qFAAqF;IACrF,OAAO,KAAK,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAChH,CAAC;AAED,yFAAyF;AACzF,IAAI,MAAM,GAAG,CAAC,CAAC;AAEf,yFAAyF;AACzF,MAAM,UAAU,eAAe;IAC7B,OAAO;QACL,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;YACjB,IAAI,CAAC;gBACH,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,SAAS,CAAC,CAAC,uCAAuC;YAC3D,CAAC;QACH,CAAC;QACD,eAAe,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE;YACjC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAC3D,8FAA8F;YAC9F,8FAA8F;YAC9F,kGAAkG;YAClG,8FAA8F;YAC9F,mGAAmG;YACnG,+DAA+D;YAC/D,MAAM,GAAG,GAAG,GAAG,IAAI,QAAQ,OAAO,CAAC,GAAG,IAAI,MAAM,EAAE,EAAE,CAAC;YACrD,IAAI,CAAC;gBACH,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC7C,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gBACpB,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;oBAAE,OAAO,KAAK,CAAC;gBACrE,MAAM,KAAK,CAAC,CAAC,qDAAqD;YACpE,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC;oBACH,MAAM,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,oEAAoE;gBACpG,CAAC;gBAAC,MAAM,CAAC;oBACP,2BAA2B;gBAC7B,CAAC;YACH,CAAC;QACH,CAAC;QACD,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC;QAC5D,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QACnD,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE;YAChB,IAAI,CAAC;gBACH,gFAAgF;gBAChF,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBACrB,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,gGAAgG;gBAChG,yFAAyF;gBACzF,gGAAgG;gBAChG,4FAA4F;gBAC5F,8FAA8F;gBAC9F,+FAA+F;gBAC/F,OAAQ,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAC;YAC3D,CAAC;QACH,CAAC;QACD,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;QACrB,QAAQ,EAAE,WAAW,CAAC,UAAU,CAAC;QACjC,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC;QAC/B,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;KACzC,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-hai/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "OH HAI — the `oh-hai` CLI: reach a human from your agent through an MA2H Hub (notify / ask / task). Placement + machine contract; command bodies land in #106–#108.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^26.1.0",
|
|
29
|
-
"tsx": "^4.
|
|
29
|
+
"tsx": "^4.23.0",
|
|
30
30
|
"typescript": "^6.0.3"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|