@samuelho-dev/coordinator 0.3.0-beta.2 → 0.3.0-beta.3

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
@@ -6,14 +6,14 @@ Read-only inspection of existing Pi, OMP, Claude Code, Codex CLI and Hermes sess
6
6
 
7
7
  There are two parts: **one inspector service**, plus **native integrations on each host**. Configuration/Helm values describe desired settings; activation and service startup are explicit commands. Building and artifact generation do not deploy, activate or hot-reload anything. CI/release workflows produce artifacts, not host installations.
8
8
 
9
- The API and standalone CLI use Node >=22.19; no additional runtime is required. Build a stable, reviewed package checkout using pnpm 11.9.0:
9
+ The API and setup CLI require Node >=22.19. Install the version-pinned release:
10
10
 
11
11
  ```sh
12
- pnpm install --frozen-lockfile
13
- pnpm build
14
- export PACKAGE_ROOT="$PWD"
12
+ npm install --global @samuelho-dev/coordinator@0.3.0-beta.3
15
13
  ```
16
14
 
15
+ For source development only, use a reviewed checkout with `pnpm install --frozen-lockfile && pnpm build` (pnpm 11.9.0). `coordinator-install --source` deliberately activates that exact local package; never source-link a native package manager's own `node_modules` directory.
16
+
17
17
  The [esbuild build](scripts/build.ts) keeps Pi dependencies external and bundles OMP dependencies to avoid compatibility-loader traversal. Native host APIs stay external; OMP uses [Bun's native CommonJS support](https://bun.com/docs/runtime/module-resolution#module-systems), not an injected shim. Rebuild and restart OMP after source updates.
18
18
 
19
19
  For Kubernetes, follow the [chart operator guide](deploy/helm/coordinator/README.md): provide an existing Secret, verified image digest and matching source SHA. The chart creates an inspector Deployment/internal Service, not native plugins, TLS routing, a database or runners. Operators own rollout and supervision. Existing broker state requires the guide's preservation/migration procedure; the plugin does not migrate or delete it.
@@ -24,51 +24,36 @@ For Kubernetes, follow the [chart operator guide](deploy/helm/coordinator/README
24
24
 
25
25
  Provision owner-scoped bearer keys. The server stores SHA-256 hashes, not raw tokens; every principal contains only `id`, `owner` and `tokenHash`. A key authorizes reads and publishing across that owner's fleet, not one physical device. Same-owner keys can announce multiple installations; installation IDs are labels, not device authentication. Anyone holding an owner key can publish under any of that owner's machine labels. Revoking a shared key revokes every holder; there is no separate read-only or health-token role.
26
26
 
27
- Generate a key in a **new** private directory from the installed package checkout; this prints only its hash:
27
+ Bootstrap a new API with one owner key:
28
28
 
29
29
  ```sh
30
- export PRIVATE="$HOME/.config/coordinator-inspector-private"
31
- (
32
- set -eu
33
- cd "$PACKAGE_ROOT"
34
- node --input-type=module <<'JS'
35
- import { Config, Console, Crypto, Effect, FileSystem } from 'effect';
36
- import { Hex } from 'effect/encoding';
37
- import { NodeRuntime, NodeServices } from '@effect/platform-node';
38
- NodeRuntime.runMain(Effect.gen(function* () {
39
- const fs = yield* FileSystem.FileSystem;
40
- const crypto = yield* Crypto.Crypto;
41
- const directory = yield* Config.String('PRIVATE');
42
- yield* fs.makeDirectory(directory, { mode: 0o700 });
43
- yield* fs.chmod(directory, 0o700);
44
- const token = Hex.encode(yield* crypto.randomBytes(32));
45
- const file = `${directory}/owner.token`;
46
- yield* fs.writeFileString(file, token, { mode: 0o600, flag: 'wx' });
47
- yield* fs.chmod(file, 0o600);
48
- yield* Console.log(Hex.encode(yield* crypto.digest('SHA-256', new TextEncoder().encode(token))));
49
- }).pipe(Effect.provide(NodeServices.layer)));
50
- JS
51
- )
30
+ PRIVATE="${XDG_CONFIG_HOME:-$HOME/.config}/coordinator/server"
31
+ coordinator init --directory "$PRIVATE" --owner operator
32
+ COORDINATOR_CONFIG="$PRIVATE/server.json" coordinator inspector
52
33
  ```
53
34
 
54
- Create `server.json` as **0600** in that **0700** directory, substituting the hash:
35
+ `init` generates a random 32-byte bearer in `owner.token` and its SHA-256 principal in `server.json`, with directory0700/files0600. It prints paths and the start command, never the bearer. Existing files, unsafe ownership/permissions and symlinks are rejected; partial writes are removed. Defaults are loopback `127.0.0.1:8787`; `--listen-host` and `--port` select another bind address. No database is required.
55
36
 
56
- ```json
57
- {
58
- "host": "127.0.0.1", "port": 8787,
59
- "principals": [{ "id": "operator-key", "owner": "operator", "tokenHash": "REPLACE_WITH_TOKEN_SHA256" }]
60
- }
37
+ Run the API under your normal process supervisor. Remote clients require operator-provided HTTPS with ordinary certificate verification; only loopback permits HTTP. For Kubernetes, bootstrap with `--listen-host 0.0.0.0`, provision the existing Secret from `server.json` and `owner.token` as described in the chart guide, and deploy the verified image/chart. Do not publish the token or expose an unauthenticated HTTP service.
38
+
39
+ `COORDINATOR_CONFIG` selects server configuration, not client credentials. Initialization does not start a service, provision TLS or replace an existing server configuration.
40
+
41
+ ### Native-owner/client setup
42
+
43
+ Configure a native client interactively:
44
+
45
+ ```sh
46
+ coordinator connect
61
47
  ```
62
48
 
63
- Start a local inspector in the foreground:
49
+ This prompts for the endpoint and a masked owner credential, performs an authenticated API read, and only then saves the private client configuration. It does not create machine identity or change native settings. For private-file provisioning:
64
50
 
65
51
  ```sh
66
- COORDINATOR_CONFIG="$PRIVATE/server.json" node dist/src/main.js inspector
52
+ coordinator connect --endpoint https://inspector.example.net --token-file /absolute/private/owner.token
67
53
  ```
68
54
 
69
- `COORDINATOR_CONFIG` selects server configuration, not client credentials. Authentication is mandatory: no anonymous fallback. HTTPS with ordinary certificate verification is required except on loopback.
55
+ `--config` chooses a new client file; otherwise the shared default location below is used. Existing files are never overwritten. `COORDINATOR_API_TOKEN` may supply the credential instead of the prompt, but credentials are never accepted as argv. Environment URL/token overrides still take precedence after setup.
70
56
 
71
- ### Native-owner/client setup
72
57
 
73
58
  Clients need only `inspectorUrl` and `apiToken`, supplied through private JSON or `COORDINATOR_API_URL` / `COORDINATOR_API_TOKEN`. The environment overrides corresponding file fields. Have the native owner's secret manager inject the token into the environment of the native host, reader bridge and separately supervised connector; never put raw keys in Git, argv, generated native MCP configuration or logs:
74
59
 
@@ -97,8 +82,9 @@ Run as the nonroot native user with the endpoint and credentials provisioned as
97
82
 
98
83
  ```sh
99
84
  TARGET=omp
100
- pnpm install:host --target "$TARGET" --destination "$PRIVATE/$TARGET-host" --dry-run
101
- pnpm install:host --target "$TARGET" --destination "$PRIVATE/$TARGET-host"
85
+ PRIVATE="${XDG_CONFIG_HOME:-$HOME/.config}/coordinator"
86
+ coordinator-install --target "$TARGET" --destination "$PRIVATE/$TARGET-host" --dry-run
87
+ coordinator-install --target "$TARGET" --destination "$PRIVATE/$TARGET-host"
102
88
  ```
103
89
 
104
90
  `--config "$PRIVATE/inspector.json"` is optional. An explicit flag or `COORDINATOR_CONNECTOR_CONFIG` selection is retained as a literal private-file pointer, never the raw token. Without either, clients look up `$XDG_CONFIG_HOME/coordinator/inspector.json` or `~/.config/coordinator/inspector.json` at runtime; the installer does not freeze that file's current endpoint into launchers or hooks. File-backed endpoint/credential changes take effect after ordinary process restart. Keep the same configuration environment available to the native host, hooks and separately supervised connector:
@@ -114,13 +100,13 @@ The installer validates prerequisites and creates launchers/config fragments. `-
114
100
 
115
101
  | Target | Activation | Presence publisher |
116
102
  | --- | --- | --- |
117
- | OMP | `omp plugin link "$PACKAGE_ROOT"`; merge only the generated `mcp.json` coordinator entry into native MCP settings. | In-session extension. |
118
- | Pi | `pi install "$PACKAGE_ROOT"`; directly registers `coordinator_session_list` / `coordinator_session_inspect`, without an MCP bridge. | In-session extension. |
103
+ | OMP | `omp plugin install @samuelho-dev/coordinator@0.3.0-beta.3`; merge only the generated `mcp.json` coordinator entry into native MCP settings. `/coordinator list` needs no MCP registration. | In-session extension. |
104
+ | Pi | `pi install npm:@samuelho-dev/coordinator@0.3.0-beta.3`; directly registers `coordinator_session_list` / `coordinator_session_inspect`, without an MCP bridge. | In-session extension. |
119
105
  | Claude Code | `claude --plugin-dir "$PRIVATE/claude-host"`; approve generated hooks/MCP tools; preserve existing status-line configuration. | Separate `connector.sh`. |
120
106
  | Codex CLI | Merge generated `hooks/hooks.json` and coordinator `mcp.toml`; trust only Coordinator's handlers in native `/hooks`. | Separate `connector.sh`. |
121
107
  | Hermes | Merge generated hook entries and `mcp_servers.coordinator` from `mcp.yaml` into the selected profile; no Python plugin installation. | Separate `connector.sh`. |
122
108
 
123
- Generated directories contain configuration, not a copy of the package. Pi/OMP load `dist/plugins/{pi,omp}.js`; MCP/connector launchers reference the build's absolute paths. Keep that package available. Updates require a reviewed build/relink, deliberate native reload/new session, and restart of affected reader bridges/external connectors—not merely editing configuration. Environment or config-token changes require process reload; private token files are reread for rotation/recovery. Staged server Secret changes require a controlled inspector restart.
109
+ Generated directories contain configuration, not a package copy. Launchers reference the installed package's absolute paths, so keep that package available. Updates use explicit native package installs, followed by deliberate native reload/new sessions and restart of affected reader bridges/external connectors. Already-open sessions retain loaded code. Updating configuration alone does not update the plugin. Private token files are reread for rotation/recovery; environment or config-token changes require process reload. Staged server Secret changes require a controlled inspector restart.
124
110
 
125
111
  `mcp.sh` is the stdio reader bridge; `connector.sh` publishes external hook metadata. The latter's equivalent foreground command is:
126
112
 
@@ -130,6 +116,16 @@ COORDINATOR_CONNECTOR_CONFIG="$PRIVATE/inspector.json" node dist/src/main.js con
130
116
 
131
117
  Hook/status-line/notification capture is local and can initialize identity/cache on first use while offline, without a token; it is not an offline delivery queue. Network publishing still needs credentials and a running publisher. Metadata cache is bounded to 1024 observations and 64 KiB per file, normally expires after 120 s, and may retain Hermes observations while the attributed host remains live. No transcripts are collected.
132
118
 
119
+ ### Native session commands and Rex
120
+
121
+ Pi/OMP register `/coordinator list`. It follows every API page visible to the owner key and displays full native session/agent IDs, machine names/installation IDs, harness and all reported metadata: model/provider, workspace, activity, connection liveness, process identity and Rex association. Missing metadata is omitted. It reads the API directly, with no model turn, per-session resource sampling or separate MCP registration.
122
+
123
+ Session JSON is Coordinator's fleet model, not an OSC wire payload. `rex` preserves `REX_SERVER` as `serverAddress` and the actual full `REX_SESSION`/`REX_BLOCK` IDs; these are separate from native agent IDs. Optional `programStatus` uses [Rex Program Status Protocol](https://www.superlogical.com/rex/docs/build/program-status) semantics: idle/working/done/error, or blocked with permission/question/auth kind. Agent `kind` remains separate. Tool/API waits are working; unknown outcomes are omitted, not inferred from disconnects or generic stops. API connection leases do not expire terminal status records.
124
+
125
+ Stock OMP 18.12.6 already owns OSC7501 through `terminal.programStatus`; Coordinator does not duplicate, override or patch that reporter. Pi's terminal-only adapter uses a private scoped record, full replacements and change-only reporting; no controls are written to RPC/JSON/print output. Pi user prompts are blocked/question; completion waits for native final settlement, not an intermediate loop end. Claude hooks supply explicit permission/question/completion/failure states; unavailable native UI-only reasons are not guessed. Prompt/error text is never copied into status reports.
126
+
127
+ Deploy the matching API **before** upgrading clients. Strict schemas reject new fields on older APIs, and older readers cannot decode new presence fields. Coordinate native reloads; preserve existing credentials and installation identity.
128
+
133
129
  ## Coordinated migration
134
130
 
135
131
  This is a clean schema cutover, with no legacy aliases or compatibility shims. Before a separately approved rollout, coordinate the server and all native owners: provision fresh owner keys privately, replace server principals with `{ id, owner, tokenHash }`, update the chart's `health.token` to match an owner key, and replace client files with endpoint/credential fields only. Remove manual `machineId`/`stateDirectory` settings, regenerate affected installer artifacts, then deliberately restart the inspector and affected native hosts/bridges/connectors. Preserve the automatically created local identity on subsequent upgrades. Old server/client configuration is rejected; do not mix releases expecting the old machine-bound schema. These instructions do not perform a live rollout or rotate live keys.
@@ -137,17 +133,21 @@ This is a clean schema cutover, with no legacy aliases or compatibility shims. B
137
133
 
138
134
  ## Plugin publication
139
135
 
140
- The npm candidate is `@samuelho-dev/coordinator@0.3.0-beta.2`; publication requires the exact-source CI-accepted tarball and owner authorization. The [npm workflow](.github/workflows/npm-release.yaml) promotes that accepted artifact; [first-publication authentication and trust setup](#first-npm-publication) remain operator prerequisites. The existing [release workflow](.github/workflows/release.yaml) publishes server/chart/GitHub artifacts, not native marketplace listings. Generated per-machine configuration is not a portable marketplace package.
136
+ The version-pinned beta is `@samuelho-dev/coordinator@0.3.0-beta.3`. The [npm workflow](.github/workflows/npm-release.yaml) promotes the exact-source CI-accepted tarball with owner authorization; [first-publication authentication and trust setup](#first-npm-publication) are prerequisites for a new publisher. The existing [release workflow](.github/workflows/release.yaml) publishes server/chart/GitHub artifacts, not native marketplace listings. Generated per-machine configuration is not a portable marketplace package.
141
137
 
142
- Once published, use your native package manager, then follow [configuration](#configuration) and [native host setup](#native-host-setup):
138
+ For a version-pinned install, use your native package manager, then follow [configuration](#configuration) and [native host setup](#native-host-setup):
143
139
 
144
140
  ```sh
145
- omp plugin install @samuelho-dev/coordinator@0.3.0-beta.2
146
- pi install npm:@samuelho-dev/coordinator@0.3.0-beta.2
141
+ omp plugin install @samuelho-dev/coordinator@0.3.0-beta.3
142
+ pi install npm:@samuelho-dev/coordinator@0.3.0-beta.3
147
143
  ```
148
144
 
145
+ Stock OMP 18.12.6 does not automatically update npm plugins. Until upstream fixes its channel handling and dry-run behavior, use explicit versioned installs for updates; avoid `omp plugin upgrade` and `upgrade --dry-run` for beta releases.
146
+
149
147
  `0.3.0-beta.2` fixes clean-install startup failures by pinning `effect`, `@effect/platform-node` and `@effect/platform-node-shared` together at 4.0.3 in the published dependencies. Checkout-only pnpm overrides do not constrain npm/OMP consumers; leaving the shared package floating can select an incompatible release (`effect/Path` missing `make`). Keep these three pins aligned when upgrading.
150
148
 
149
+ `0.3.0-beta.3` adds native `/coordinator list`, secure self-host bootstrap/authenticated client setup, and Rex references/status normalization without an OMP fork. Runtime dependency pins are unchanged.
150
+
151
151
  | Host | Publication route |
152
152
  | --- | --- |
153
153
  | [OMP](https://github.com/can1357/oh-my-pi/blob/main/docs/marketplace.md) | Publish a Git-backed `.omp-plugin/marketplace.json` catalog and runnable plugin tree with `package.json` `omp.extensions`. Claude-compatible `.claude-plugin/marketplace.json` is a fallback. npm catalog sources are not supported; ordinary `omp plugin install PACKAGE` is a separate npm route. |
@@ -178,7 +178,7 @@ Release gaps and constraints:
178
178
  - **Startup:** generated integrations do not await the inspector during activation. Pi/OMP publish in the background; Claude/Codex/Hermes hooks capture locally; the local stdio bridge initializes and lists tools without a remote handshake. Explicit inspection/`coordinate` requests may await a bounded remote reply. Local configuration, filesystem and native loading still run during initialization.
179
179
  - **State/lifetime:** the owner-isolated registry is in memory; restart loses discovery until reannouncement, never native sessions. A Pi/OMP plugin owns one Effect managed runtime; presence/poll failures cancel their peer and shutdown disposes its fibers without killing the host. Closing the stdio reader's stdin ends its scope and cancels pending HTTP reads. Failed startup/terminated connectors can restart on a later native event.
180
180
  - **Effect composition:** `NodeServices.layer` provides filesystem/path/crypto/process/stdio services; `Host.layer` provides host facts. `Mcp.layer` privately supplies its HTTP client. The CLI scope owns the inspector/server; one Pi/OMP `ManagedRuntime` owns plugin fibers/layers through shutdown. CLI-only stdin closes on completion; plugin stdin remains host-owned. Layer construction, provisioning and sharing follow the [v4 layer contract](https://effect.website/docs/v4/api/effect/Layer).
181
- - **Node bindings:** [`platform.ts`](src/platform.ts) confines native host facts and CLI-owned stdin alongside the server factory required by [`NodeHttpServer.layerServer`](https://effect.website/docs/v4/api/platform-node/NodeHttpServer#layerServer) and constant-time comparison absent from [`Crypto`](https://effect.website/docs/v4/api/effect/Crypto#Crypto-interface). Filesystem checks use `FileSystem`; execute permission uses Effect's scoped process service running `/bin/test -x`, never the selected runtime. Native SDK callbacks remain foreign interop boundaries.
181
+ - **Node bindings:** [`platform.ts`](src/platform.ts) confines host facts, CLI-owned stdin, server composition and constant-time digest comparison. Runtime private readers use Effect `FileSystem`; setup's exclusive private-file transaction uses Node opened descriptors and no-follow flags. Native SDK callbacks remain foreign interop boundaries.
182
182
  - **OMP shortcut:** a leading `coordinate` enriches the current input with fleet presence and an inspection cue. It does not contact other sessions with prompts.
183
183
 
184
184
  | Timing/bound | Value |
@@ -208,6 +208,8 @@ CPU/RSS sampling runs only on inspection, over approximately 200 ms. PID plus OS
208
208
  | [mcp.ts](src/mcp.ts) | `Mcp` service: bounded HTTP/MCP transport and stdio reader. |
209
209
  | [native.ts](src/native.ts) | Private files/cache, native projections and connector policy. |
210
210
  | [plugin.ts](src/plugin.ts) | Pi/OMP hooks, observers, tools and runtime ownership. |
211
+ | [rex.ts](src/rex.ts) | Native Rex environment association and Pi-only scoped OSC reporting. |
212
+ | [setup.ts](src/setup.ts) | Private self-host initialization, masked client prompts and authenticated configuration creation. |
211
213
  | [resources.ts](src/resources.ts) | Linux/macOS process identity and on-demand sampling. |
212
214
  | [platform.ts](src/platform.ts) | Effect Node platform composition, scoped CLI stdin, and host/crypto/executable bindings. |
213
215
  | [main.ts](src/main.ts) | CLI entrypoints and layer composition. |
@@ -17,10 +17,12 @@ The implemented inspector cutover changes the shipped runtime, not the completen
17
17
 
18
18
  The current Dockerfile does **not** download/copy standalone OMP or collect OMP notices into `/usr/local/share/coordinator/third-party/`; it adds no PostgreSQL client/backup tooling or application database. `tsx`, `undici` and `ws` are no longer direct runtime dependencies. Their absence from the direct manifest is not proof of absence from every transitive package or base-image byte. No Pi/OMP/native binary closure is asserted to be shipped by the current inspector image.
19
19
 
20
- ## npm plugin bundle notices — candidate 0.3.0-beta.2
20
+ ## npm plugin bundle notices — candidate 0.3.0-beta.3
21
21
 
22
22
  The OMP extension embeds runtime dependencies to support its native compatibility loader; Pi keeps those dependencies external. Native Pi/OMP SDK implementations are not part of this inventory. The entries below were traced in the existing emitted OMP artifact and matching package distributions. Reconcile them against the newly built, accepted tarball before publication; this source review is not a completed image, history, or legal certification.
23
23
 
24
+ `0.3.0-beta.3` keeps the `0.3.0-beta.2` runtime dependency pins; bootstrap uses Node built-ins and the already-declared Effect CLI. No native host binary or SDK implementation is added to the published package.
25
+
24
26
  | Embedded component | Version | Matching notice source |
25
27
  | --- | --- | --- |
26
28
  | `effect`, `@effect/platform-node`, `@effect/platform-node-shared` | 4.0.3 | Matching installed `LICENSE` files; [Effect package](https://registry.npmjs.org/effect/4.0.3) |
@@ -39214,6 +39214,13 @@ var ProcessIdentity = Schema_exports.Struct({
39214
39214
  ownership: Schema_exports.Literals(["owned", "shared", "unknown"])
39215
39215
  });
39216
39216
  var NativeWorkState = Schema_exports.Struct({ id: Id, state: Schema_exports.Literals(["pending", "active", "completed", "cancelled", "unknown"]) });
39217
+ var RexText = Schema_exports.String.check(Schema_exports.isMinLength(1), Schema_exports.isMaxLength(4096), Schema_exports.makeFilter(
39218
+ (value5) => /[\u0000-\u001f\u007f-\u009f]/u.test(value5) ? "Control characters are forbidden" : void 0
39219
+ ));
39220
+ var ProgramStatus = Schema_exports.Union([
39221
+ Schema_exports.Struct({ state: Schema_exports.Literals(["idle", "working", "done", "error"]) }),
39222
+ Schema_exports.Struct({ state: Schema_exports.Literal("blocked"), kind: Schema_exports.Literals(["permission", "question", "auth"]) })
39223
+ ]);
39217
39224
  var SessionPresence = Schema_exports.Struct({
39218
39225
  ref: SessionRef,
39219
39226
  observedAt: Timestamp,
@@ -39234,7 +39241,8 @@ var SessionPresence = Schema_exports.Struct({
39234
39241
  agent: Schema_exports.optional(Id),
39235
39242
  cwd: Schema_exports.optional(Path3),
39236
39243
  repository: Schema_exports.optional(Path3),
39237
- rex: Schema_exports.optional(Schema_exports.Struct({ serverId: Schema_exports.optional(Id), blockId: Schema_exports.optional(Id), sessionId: Schema_exports.optional(Id), windowId: Schema_exports.optional(Id), paneId: Schema_exports.optional(Id), terminalId: Schema_exports.optional(Id) })),
39244
+ rex: Schema_exports.optional(Schema_exports.Struct({ serverAddress: Schema_exports.optional(RexText), blockId: Schema_exports.optional(RexText), sessionId: Schema_exports.optional(RexText) })),
39245
+ programStatus: Schema_exports.optional(ProgramStatus),
39238
39246
  processes: Schema_exports.optional(bounded3(ProcessIdentity, 128)),
39239
39247
  activity: Schema_exports.optional(Schema_exports.Struct({
39240
39248
  state: Schema_exports.Literals(["idle", "working", "waiting", "stopped", "unknown"]),
@@ -72016,6 +72024,40 @@ var observeResources = (roots) => Effect_exports.gen(function* () {
72016
72024
  };
72017
72025
  });
72018
72026
 
72027
+ // src/rex.ts
72028
+ import { randomUUID } from "node:crypto";
72029
+ var readRexContext = Effect_exports.gen(function* () {
72030
+ const serverAddress = yield* Config_exports.String("REX_SERVER").pipe(Config_exports.withDefault(""));
72031
+ const sessionId = yield* Config_exports.String("REX_SESSION").pipe(Config_exports.withDefault(""));
72032
+ const blockId = yield* Config_exports.String("REX_BLOCK").pipe(Config_exports.withDefault(""));
72033
+ return serverAddress || sessionId || blockId ? {
72034
+ ...serverAddress ? { serverAddress } : {},
72035
+ ...sessionId ? { sessionId } : {},
72036
+ ...blockId ? { blockId } : {}
72037
+ } : void 0;
72038
+ });
72039
+ var rexSequence = (id2, status) => {
72040
+ if (id2.length > 128 || id2.split("/").length > 8 || !/^[A-Za-z0-9_.+-]{1,32}(?:\/[A-Za-z0-9_.+-]{1,32})*$/u.test(id2)) return "";
72041
+ return `\x1B]7501;id=${id2}:state=${status?.state ?? "clear"}:app=coordinator:title=Q29vcmRpbmF0b3I=${status?.state === "blocked" ? `:kind=${status.kind}` : ""}\x1B\\`;
72042
+ };
72043
+ var createRexReporter = (write2) => {
72044
+ const id2 = `coordinator/${randomUUID().replaceAll("-", "")}`;
72045
+ let previous;
72046
+ return {
72047
+ update: (status, terminal) => {
72048
+ if (!terminal) return;
72049
+ const sequence = rexSequence(id2, status);
72050
+ if (previous === sequence || previous === void 0 && status === void 0) return;
72051
+ write2(sequence);
72052
+ previous = sequence;
72053
+ },
72054
+ close: (terminal) => {
72055
+ if (terminal && previous !== void 0 && !/:state=(?:done|error):/u.test(previous)) write2(rexSequence(id2, void 0));
72056
+ previous = void 0;
72057
+ }
72058
+ };
72059
+ };
72060
+
72019
72061
  // src/native.ts
72020
72062
  var NativeError = class extends Data_exports.TaggedError("NativeError") {
72021
72063
  };
@@ -72300,8 +72342,10 @@ var provenance = (source, scope3, scopeDescription, observedAt, nativeUpdatedAt
72300
72342
  var createNativeObserver = (harness, machineId, processes = []) => Effect_exports.gen(function* () {
72301
72343
  const host = yield* Host2;
72302
72344
  const clock = yield* Clock_exports.Clock;
72303
- const rexSession = yield* Config_exports.String("REX_SESSION").pipe(Config_exports.withDefault(""));
72304
- const rexBlock = yield* Config_exports.String("REX_BLOCK").pipe(Config_exports.withDefault(""));
72345
+ const rex = yield* readRexContext;
72346
+ let programStatus;
72347
+ let userPrompt;
72348
+ let stopReason;
72305
72349
  let current;
72306
72350
  let identity2;
72307
72351
  let lastActivityAt;
@@ -72313,6 +72357,9 @@ var createNativeObserver = (harness, machineId, processes = []) => Effect_export
72313
72357
  const key = JSON.stringify([ctx.sessionManager.getSessionId(), harness === "omp" ? ctx.agent?.id : void 0]);
72314
72358
  if (identity2 !== key) {
72315
72359
  latest = void 0;
72360
+ programStatus = void 0;
72361
+ userPrompt = void 0;
72362
+ stopReason = void 0;
72316
72363
  activeTools.clear();
72317
72364
  identity2 = key;
72318
72365
  }
@@ -72321,6 +72368,24 @@ var createNativeObserver = (harness, machineId, processes = []) => Effect_export
72321
72368
  if (event?.type === "tool_execution_start" && event.toolCallId && event.toolName) activeTools.set(event.toolCallId, event.toolName);
72322
72369
  if (event?.type === "tool_execution_end" && event.toolCallId) activeTools.delete(event.toolCallId);
72323
72370
  if (event?.type === "agent_end") activeTools.clear();
72371
+ if (event?.type === "agent_start") {
72372
+ programStatus = { state: "working" };
72373
+ stopReason = void 0;
72374
+ }
72375
+ if (event?.type === "turn_start") {
72376
+ programStatus = { state: "working" };
72377
+ stopReason = void 0;
72378
+ }
72379
+ if (event?.type === "tool_execution_start") programStatus = { state: "working" };
72380
+ if (event?.message?.role === "assistant" && event.message.stopReason) stopReason = event.message.stopReason;
72381
+ if (event?.type === "agent_end") stopReason = event.messages?.findLast((message2) => message2.role === "assistant")?.stopReason ?? stopReason;
72382
+ if (harness === "pi" && event?.type === "agent_before_settle") stopReason = event.outcome === "completed" ? "stop" : event.outcome;
72383
+ if (event?.type === "agent_settled" || harness !== "pi" && event?.type === "agent_end") {
72384
+ programStatus = event.willContinue ? { state: "working" } : stopReason === "aborted" ? { state: "idle" } : stopReason === "error" ? { state: "error" } : stopReason === "stop" || stopReason === "length" ? { state: "done" } : void 0;
72385
+ } else if (event?.type === "agent_end") programStatus = { state: "working" };
72386
+ if (harness === "pi" && event?.type === "ui_prompt_start") userPrompt = { state: "blocked", kind: "question" };
72387
+ if (harness === "pi" && event?.type === "ui_prompt_end") userPrompt = void 0;
72388
+ if ((event === void 0 || event.type === "session_start" || event.type === "session_switch") && programStatus === void 0) programStatus = { state: ctx.isIdle() ? "idle" : "working" };
72324
72389
  const message = event?.message;
72325
72390
  if (message?.role === "assistant" && message.usage) {
72326
72391
  const u = message.usage;
@@ -72340,6 +72405,7 @@ var createNativeObserver = (harness, machineId, processes = []) => Effect_export
72340
72405
  const presence = (now2 = clock.currentTimeMillisUnsafe()) => {
72341
72406
  if (!current) return void 0;
72342
72407
  const currentToolName = activeTools.values().next().value;
72408
+ const status = userPrompt ?? programStatus;
72343
72409
  return {
72344
72410
  ref: { machineId, harness, nativeSessionId: current.sessionManager.getSessionId(), ...harness === "omp" && current.agent?.id ? { nativeAgentId: current.agent.id } : {} },
72345
72411
  observedAt: now2,
@@ -72348,13 +72414,15 @@ var createNativeObserver = (harness, machineId, processes = []) => Effect_export
72348
72414
  processes,
72349
72415
  machineName,
72350
72416
  ...os ? { os } : {},
72351
- ...rexSession || rexBlock ? { rex: { ...rexSession ? { sessionId: rexSession } : {}, ...rexBlock ? { blockId: rexBlock } : {} } } : {},
72417
+ ...rex ? { rex } : {},
72418
+ ...status ? { programStatus: status } : {},
72352
72419
  ...current.model ? { provider: current.model.provider, model: current.model.id } : {},
72353
72420
  ...current.agent ? { kind: current.agent.kind, ...current.agent.name ? { agent: current.agent.name } : {} } : {},
72354
72421
  activity: {
72355
- state: current.isIdle() ? "idle" : "working",
72422
+ state: userPrompt ? "waiting" : current.isIdle() ? "idle" : "working",
72356
72423
  streaming: !current.isIdle(),
72357
- ...currentToolName === void 0 ? {} : { currentToolName, wait: "tool" }
72424
+ ...currentToolName === void 0 ? {} : { currentToolName },
72425
+ ...userPrompt ? { wait: "user" } : currentToolName === void 0 ? {} : { wait: "tool" }
72358
72426
  }
72359
72427
  };
72360
72428
  };
@@ -72458,6 +72526,9 @@ var createNativeObserver = (harness, machineId, processes = []) => Effect_export
72458
72526
  current = void 0;
72459
72527
  identity2 = void 0;
72460
72528
  latest = void 0;
72529
+ programStatus = void 0;
72530
+ userPrompt = void 0;
72531
+ stopReason = void 0;
72461
72532
  lastActivityAt = void 0;
72462
72533
  activeTools.clear();
72463
72534
  } };
@@ -72479,6 +72550,23 @@ var registerCoordinateKeyword = Effect_exports.gen(function* () {
72479
72550
  const { isMagicKeyword, setMagicKeywords } = yield* Effect_exports.tryPromise({ try: () => import("@oh-my-pi/pi-tui/prompt/magic-keywords"), catch: () => new NativeError({ operation: "Load native keyword highlighter" }) });
72480
72551
  setMagicKeywords([...MAGIC_KEYWORDS.filter(({ word }) => isMagicKeyword(word)), { word: "coordinate", hue: [190, 260] }]);
72481
72552
  });
72553
+ var formatSessionList = (sessions) => {
72554
+ if (sessions.length === 0) return "No sessions are currently visible to this API key.";
72555
+ const lines = [`Coordinator: ${sessions.length} sessions visible to this API key.`, "", "~~~text"];
72556
+ for (const session of sessions) {
72557
+ lines.push(
72558
+ `Machine: ${JSON.stringify(session.machineName ?? "unavailable")} (${JSON.stringify(session.ref.machineId)})`,
72559
+ `Session: ${session.ref.harness} ${JSON.stringify(session.ref.nativeSessionId)}`
72560
+ );
72561
+ if (session.ref.nativeAgentId !== void 0) lines.push(`Agent ID: ${JSON.stringify(session.ref.nativeAgentId)}`);
72562
+ for (const [field, value5] of Object.entries(session)) {
72563
+ if (field !== "ref" && field !== "machineName" && value5 !== void 0) lines.push(`${field}: ${JSON.stringify(value5)}`);
72564
+ }
72565
+ lines.push("");
72566
+ }
72567
+ lines.push("~~~");
72568
+ return lines.join("\n");
72569
+ };
72482
72570
  function plugin(host) {
72483
72571
  const harness = "omp";
72484
72572
  const runtime2 = ManagedRuntime_exports.make(Layer_exports.mergeAll(NodeServices_exports.layer, Host2.layer, Mcp.layer, Layer_exports.succeed(Logger_exports.LogToStderr, true)));
@@ -72486,6 +72574,48 @@ function plugin(host) {
72486
72574
  let observer;
72487
72575
  let starting;
72488
72576
  let stopped = false;
72577
+ const rexReporter = harness === "pi" ? createRexReporter((sequence) => process.stdout.write(sequence)) : void 0;
72578
+ let terminal = false;
72579
+ host.registerCommand("coordinator", {
72580
+ description: "List visible sessions, their machines, and native metadata",
72581
+ handler: async (args2) => {
72582
+ if (args2.trim() !== "list") {
72583
+ host.sendMessage({ customType: "coordinator-list", content: "Usage: /coordinator list", display: true }, { triggerTurn: false });
72584
+ return;
72585
+ }
72586
+ const exit3 = await runtime2.runPromiseExit(Effect_exports.gen(function* () {
72587
+ const activeConfig = config2 ?? (yield* loadConnectorConfig());
72588
+ if (!activeConfig) return yield* new NativeError({ operation: "Connector configuration required" });
72589
+ const token = yield* readConnectorToken(activeConfig);
72590
+ const mcp = yield* Mcp;
72591
+ const sessions = [];
72592
+ let cursor;
72593
+ do {
72594
+ const page = yield* mcp.call(activeConfig.inspectorUrl, token, InspectionTools.list, { limit: 100, ...cursor === void 0 ? {} : { cursor } });
72595
+ for (const session of page.sessions) sessions.push(session);
72596
+ cursor = page.nextCursor ?? void 0;
72597
+ } while (cursor !== void 0);
72598
+ return sessions;
72599
+ }));
72600
+ if (stopped) return;
72601
+ if (exit3._tag === "Failure") {
72602
+ const error3 = Cause_exports.findErrorOption(exit3.cause);
72603
+ const reason = error3._tag === "Some" ? error3.value._tag === "HttpError" ? `HTTP ${error3.value.status}` : error3.value._tag : "unexpected failure";
72604
+ host.sendMessage({
72605
+ customType: "coordinator-list",
72606
+ content: `Coordinator list unavailable (${reason}). Check inspectorUrl, your access token, and API reachability.`,
72607
+ display: true
72608
+ }, { triggerTurn: false });
72609
+ return;
72610
+ }
72611
+ host.sendMessage({
72612
+ customType: "coordinator-list",
72613
+ content: formatSessionList(exit3.value),
72614
+ display: true,
72615
+ details: { sessions: exit3.value }
72616
+ }, { triggerTurn: false });
72617
+ }
72618
+ });
72489
72619
  if (harness === "pi") {
72490
72620
  const register2 = (descriptor) => {
72491
72621
  const document = Schema_exports.toJsonSchemaDocument(Schema_exports.toCodecJson(descriptor.input), { onExcessProperty: "error" });
@@ -72540,6 +72670,8 @@ function plugin(host) {
72540
72670
  const processIdentity = yield* identifyProcess(host2.pid);
72541
72671
  observer = yield* createNativeObserver(harness, nativeConfig.machineId, processIdentity.status === "available" ? [{ pid: host2.pid, startIdentity: processIdentity.startTime, ownership: harness === "omp" ? "shared" : "owned" }] : []);
72542
72672
  observer.update(ctx);
72673
+ terminal = ctx.mode === "tui" && ctx.hasUI && process.stdout.isTTY === true;
72674
+ rexReporter?.update(observer.presence()?.programStatus, terminal);
72543
72675
  if (harness === "omp" && ctx.hasUI && ctx.mode === "tui") yield* registerCoordinateKeyword;
72544
72676
  runtime2.runFork(connectNative(nativeConfig, {
72545
72677
  presence: () => Effect_exports.sync(() => {
@@ -72571,16 +72703,23 @@ function plugin(host) {
72571
72703
  }
72572
72704
  });
72573
72705
  const events = ["session_start", "session_switch", "agent_start", "agent_end", "turn_start", "turn_end", "message_end", "tool_execution_start", "tool_execution_end"];
72706
+ if (harness === "pi") events.push("agent_before_settle", "agent_settled", "ui_prompt_start", "ui_prompt_end");
72574
72707
  for (const name of events) {
72575
72708
  host.on(name, async (event, ctx) => {
72576
72709
  if (stopped) return;
72577
72710
  await start(ctx);
72578
72711
  if (stopped || !observer) return;
72579
- await runtime2.runPromiseExit(Effect_exports.sync(() => observer?.update(ctx, event)).pipe(Effect_exports.tapCause(reportCause(ctx, "native update"))));
72712
+ await runtime2.runPromiseExit(Effect_exports.sync(() => {
72713
+ if (name === "session_switch") rexReporter?.update(void 0, terminal);
72714
+ observer?.update(ctx, event);
72715
+ terminal = ctx.mode === "tui" && ctx.hasUI && process.stdout.isTTY === true;
72716
+ rexReporter?.update(observer?.presence()?.programStatus, terminal);
72717
+ }).pipe(Effect_exports.tapCause(reportCause(ctx, "native update"))));
72580
72718
  });
72581
72719
  }
72582
72720
  host.on("session_shutdown", async () => {
72583
72721
  stopped = true;
72722
+ rexReporter?.close(terminal);
72584
72723
  try {
72585
72724
  await runtime2.dispose();
72586
72725
  } finally {