@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 +49 -47
- package/THIRD_PARTY_NOTICES.md +3 -1
- package/dist/plugins/omp.js +146 -7
- package/dist/plugins/pi.js +217 -77
- package/dist/scripts/install.js +9 -5
- package/dist/src/inspection.js +7 -1
- package/dist/src/main.js +4 -1
- package/dist/src/native.js +10 -3
- package/dist/src/plugin.js +116 -8
- package/dist/src/rex.js +36 -0
- package/dist/src/setup.js +134 -0
- package/package.json +1 -1
package/dist/scripts/install.js
CHANGED
|
@@ -9,18 +9,19 @@ class InstallError extends Schema.TaggedError()("InstallError", { message: Schem
|
|
|
9
9
|
const installationFailure = () => new InstallError({ message: "Installation failed; check owner, private paths, installed package and destination permissions. Existing native configuration was not changed." });
|
|
10
10
|
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
11
11
|
const packageRoot = new URL("../../", import.meta.url);
|
|
12
|
-
const help = `Usage: coordinator-install --target pi|omp|claude|codex|hermes --destination /absolute/new-directory [--config /absolute/private/inspector.json] [--runtime /absolute/node] [--dry-run]
|
|
12
|
+
const help = `Usage: coordinator-install --target pi|omp|claude|codex|hermes --destination /absolute/new-directory [--config /absolute/private/inspector.json] [--runtime /absolute/node] [--source] [--dry-run]
|
|
13
13
|
|
|
14
14
|
Run as the native session owner. --destination must be a NEW directory under an existing private owner directory; no existing native files are overwritten.
|
|
15
15
|
--config selects an existing private connector config; otherwise use COORDINATOR_CONNECTOR_CONFIG, the default private file, or COORDINATOR_API_URL/COORDINATOR_API_TOKEN. --runtime defaults to this Node executable.
|
|
16
16
|
--dry-run validates prerequisites and prints activation instructions without writing.
|
|
17
|
-
Printed native install
|
|
17
|
+
Printed native install commands pin the published package version. --source instead activates this exact local package (for intentional checkout/tarball development, never a managed native node_modules directory). Generated include artifacts require separate explicit activation; supervise the external coordinator connector separately. API tokens must be inherited from the native owner's environment/secret manager or read from the private config; they are never embedded in artifacts.
|
|
18
18
|
No settings/profile/statusLine replacement, exporter activation, remote setup, service management or historical data changes. Claude status-line metrics require optional manual composition; the installer does not configure a status line.`;
|
|
19
19
|
const install = (version) => Command.make("coordinator-install", {
|
|
20
20
|
target: Flag.String("target").pipe(Flag.optional),
|
|
21
21
|
destination: Flag.String("destination").pipe(Flag.optional),
|
|
22
22
|
config: Flag.String("config").pipe(Flag.optional),
|
|
23
23
|
runtime: Flag.String("runtime").pipe(Flag.optional),
|
|
24
|
+
source: Flag.Boolean("source").pipe(Flag.withDefault(false)),
|
|
24
25
|
dryRun: Flag.Boolean("dry-run").pipe(Flag.withDefault(false)),
|
|
25
26
|
}, (flags) => Effect.gen(function* () {
|
|
26
27
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -87,13 +88,16 @@ const install = (version) => Command.make("coordinator-install", {
|
|
|
87
88
|
if (target === "pi" || target === "omp") {
|
|
88
89
|
const bundle = join(root, `dist/plugins/${target}.js`);
|
|
89
90
|
yield* fs.access(bundle, { readable: true }).pipe(Effect.mapError(installationFailure));
|
|
90
|
-
//
|
|
91
|
+
// Published activation must not self-link a native package manager's node_modules tree.
|
|
91
92
|
if (target === "omp")
|
|
92
93
|
json("mcp.json", { mcpServers: { coordinator: mcp } });
|
|
93
|
-
|
|
94
|
+
const activationCommand = flags.source
|
|
95
|
+
? `${target === "pi" ? "pi install" : "omp plugin link"} ${quote(root)}`
|
|
96
|
+
: `${target === "pi" ? "pi install" : "omp plugin install"} ${quote(`${target === "pi" ? "npm:" : ""}@samuelho-dev/coordinator@${version}`)}`;
|
|
97
|
+
activation = `${activationCommand}\n${target === "pi" ? "The extension provides native coordinator_session_list and coordinator_session_inspect tools; no Pi MCP configuration or adapter is required." : `Merge ONLY the coordinator entry from ${join(destination, "mcp.json")} into ~/.omp/agent/mcp.json; preserve all existing entries.`}\nLaunch the native harness with the same configuration environment${environment ? ` and ${environment}` : ""}.`;
|
|
94
98
|
}
|
|
95
99
|
else if (target === "claude" || target === "codex") {
|
|
96
|
-
const events = ["SessionStart", "SessionEnd", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PermissionRequest", "Stop", ...(target === "claude" ? ["PostToolUseFailure", "PostModelSwitch", "PreCompact"] : ["Interrupt"])];
|
|
100
|
+
const events = ["SessionStart", "SessionEnd", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PermissionRequest", "Stop", ...(target === "claude" ? ["PostToolUseFailure", "PostModelSwitch", "PreCompact", "StopFailure", "Elicitation", "ElicitationResult"] : ["Interrupt"])];
|
|
97
101
|
const hooks = Object.fromEntries(events.map((event) => [event, [{ hooks: [{ type: "command", command: command(`hook ${target}`), timeout: 3 }] }]]));
|
|
98
102
|
json("hooks/hooks.json", { hooks });
|
|
99
103
|
if (target === "claude") {
|
package/dist/src/inspection.js
CHANGED
|
@@ -28,6 +28,11 @@ export const ProcessIdentity = Schema.Struct({
|
|
|
28
28
|
ownership: Schema.Literals(["owned", "shared", "unknown"]),
|
|
29
29
|
});
|
|
30
30
|
const NativeWorkState = Schema.Struct({ id: Id, state: Schema.Literals(["pending", "active", "completed", "cancelled", "unknown"]) });
|
|
31
|
+
const RexText = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(4096), Schema.makeFilter((value) => /[\u0000-\u001f\u007f-\u009f]/u.test(value) ? "Control characters are forbidden" : undefined));
|
|
32
|
+
export const ProgramStatus = Schema.Union([
|
|
33
|
+
Schema.Struct({ state: Schema.Literals(["idle", "working", "done", "error"]) }),
|
|
34
|
+
Schema.Struct({ state: Schema.Literal("blocked"), kind: Schema.Literals(["permission", "question", "auth"]) }),
|
|
35
|
+
]);
|
|
31
36
|
export const SessionPresence = Schema.Struct({
|
|
32
37
|
ref: SessionRef,
|
|
33
38
|
observedAt: Timestamp,
|
|
@@ -42,7 +47,8 @@ export const SessionPresence = Schema.Struct({
|
|
|
42
47
|
label: Schema.optional(Label), kind: Schema.optional(Id),
|
|
43
48
|
provider: Schema.optional(Id), model: Schema.optional(Id), profile: Schema.optional(Id), agent: Schema.optional(Id),
|
|
44
49
|
cwd: Schema.optional(Path), repository: Schema.optional(Path),
|
|
45
|
-
rex: Schema.optional(Schema.Struct({
|
|
50
|
+
rex: Schema.optional(Schema.Struct({ serverAddress: Schema.optional(RexText), blockId: Schema.optional(RexText), sessionId: Schema.optional(RexText) })),
|
|
51
|
+
programStatus: Schema.optional(ProgramStatus),
|
|
46
52
|
processes: Schema.optional(bounded(ProcessIdentity, 128)),
|
|
47
53
|
activity: Schema.optional(Schema.Struct({
|
|
48
54
|
state: Schema.Literals(["idle", "working", "waiting", "stopped", "unknown"]),
|
package/dist/src/main.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Mcp } from "./mcp.js";
|
|
|
7
7
|
import { Host, cliStdioLayer, serverLayer } from "./platform.js";
|
|
8
8
|
import { InspectorConfig, InspectionTools, SessionInspectInput } from "./inspection.js";
|
|
9
9
|
import { NativeError, connectExternal, loadConnectorConfig, resolveNativeConfig, readConnectorToken, readPrivateFile, projectHook, projectClaudeStatus, projectCodexNotification, writeNativeCache, writeHermesHook } from "./native.js";
|
|
10
|
+
import { runSetup } from "./setup.js";
|
|
10
11
|
const readInput = Effect.gen(function* () {
|
|
11
12
|
const stdio = yield* Stdio.Stdio;
|
|
12
13
|
const bytes = new Uint8Array(65536);
|
|
@@ -31,8 +32,10 @@ export const cli = (args) => Effect.scoped(Effect.gen(function* () {
|
|
|
31
32
|
return yield* Console.log(manifest.version);
|
|
32
33
|
}
|
|
33
34
|
if (mode === "help" || mode === "--help") {
|
|
34
|
-
return yield* Console.log(JSON.stringify({ commands: ["inspector", "connector", "mcp", "hook claude|codex|hermes", "claude-statusline", "codex-notification", "list", "inspect"], inspectorConfiguration: "COORDINATOR_CONFIG: private inspector server configuration", configuration: "COORDINATOR_API_URL / COORDINATOR_API_TOKEN override COORDINATOR_CONNECTOR_CONFIG or XDG_CONFIG_HOME/coordinator/inspector.json; credentials may also use tokenFile", statusline: "Optional manual composition only; consumes stdin without producing display output", codexNotification: "Version 2 envelope, source attached-app-server, thread/tokenUsage/updated only; does not observe other TUIs" }));
|
|
35
|
+
return yield* Console.log(JSON.stringify({ commands: ["init [--directory PATH] [--owner ID] [--listen-host IP] [--port 8787]", "connect [--endpoint URL] [--token-file PATH] [--config PATH]", "inspector", "connector", "mcp", "hook claude|codex|hermes", "claude-statusline", "codex-notification", "list", "inspect"], inspectorConfiguration: "COORDINATOR_CONFIG: private inspector server configuration", configuration: "COORDINATOR_API_URL / COORDINATOR_API_TOKEN override COORDINATOR_CONNECTOR_CONFIG or XDG_CONFIG_HOME/coordinator/inspector.json; credentials may also use tokenFile", setup: "Run coordinator init --help or coordinator connect --help. Credentials are accepted only by masked prompt, private file, or environment.", statusline: "Optional manual composition only; consumes stdin without producing display output", codexNotification: "Version 2 envelope, source attached-app-server, thread/tokenUsage/updated only; does not observe other TUIs" }));
|
|
35
36
|
}
|
|
37
|
+
if (mode === "init" || mode === "connect")
|
|
38
|
+
return yield* runSetup(args);
|
|
36
39
|
if (mode === "inspector") {
|
|
37
40
|
const file = yield* Config.String("COORDINATOR_CONFIG").pipe(Config.withDefault(""));
|
|
38
41
|
if (!file)
|
package/dist/src/native.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Stream } from "effect";
|
|
|
6
6
|
import { Mcp } from "./mcp.js";
|
|
7
7
|
import { INSPECTION_LIMIT, ConnectorConfig, ConnectorAnnounce, ConnectorAccepted, ConnectorPoll, ConnectorPollResult, ConnectorResult, ConnectorDisconnect, SessionInspection, createInspection, metric, unavailableMetric, sessionRefKey } from "./inspection.js";
|
|
8
8
|
import { identifyProcess, observeResources } from "./resources.js";
|
|
9
|
+
import { readRexContext } from "./rex.js";
|
|
9
10
|
export class NativeError extends Data.TaggedError("NativeError") {
|
|
10
11
|
}
|
|
11
12
|
const json = (text) => Effect.try({ try: () => JSON.parse(text), catch: () => new NativeError({ operation: "Invalid native JSON" }) });
|
|
@@ -266,7 +267,8 @@ export const projectHook = (harness, machineId, value, at) => Effect.gen(functio
|
|
|
266
267
|
const name = event.hook_event_name;
|
|
267
268
|
const activity = name === "PreCompact" || event.source === "compact" ? { state: "working", compacting: true } : name === "SessionEnd" ? { state: "stopped" } : name === "PermissionRequest" ? { state: "waiting", wait: "approval", approval: "pending", ...(event.tool_name ? { currentToolName: event.tool_name } : {}) } : name === "PreToolUse" ? { state: "working", wait: "tool", ...(event.tool_name ? { currentToolName: event.tool_name } : {}) } : name === "Stop" || name === "Interrupt" ? { state: "idle" } : name === "UserPromptSubmit" ? { state: "working", wait: "api" } : { state: "unknown" };
|
|
268
269
|
const model = event.to_model ?? event.model;
|
|
269
|
-
const
|
|
270
|
+
const programStatus = name === "PermissionRequest" ? { state: "blocked", kind: "permission" } : harness === "claude" && name === "Elicitation" ? { state: "blocked", kind: "question" } : name === "Interrupt" ? { state: "idle" } : harness === "claude" && name === "StopFailure" ? { state: "error" } : harness === "claude" && name === "Stop" ? { state: "done" } : activity.state === "working" || (harness === "claude" && (name === "PostToolUse" || name === "PostToolUseFailure" || name === "ElicitationResult")) ? { state: "working" } : undefined;
|
|
271
|
+
const presence = { ref: { machineId, harness, nativeSessionId: event.session_id }, observedAt: at, nativeUpdatedAt: at, lastActivityAt: at, activity, ...(programStatus ? { programStatus } : {}), ...(event.cwd ? { cwd: event.cwd } : {}), ...(model ? { model } : {}) };
|
|
270
272
|
return createInspection(presence, at);
|
|
271
273
|
});
|
|
272
274
|
export const projectClaudeStatus = (machineId, value, at) => Effect.gen(function* () {
|
|
@@ -355,6 +357,9 @@ const cacheRecords = (config, hermesIdentities = new Map()) => Effect.gen(functi
|
|
|
355
357
|
return records;
|
|
356
358
|
}).pipe(sanitizeFileFinalizers("Read private metadata cache"), Effect.catchTag("PlatformError", () => Effect.fail(new NativeError({ operation: "Read private metadata cache" }))));
|
|
357
359
|
export const writeNativeCache = (config, incoming) => Effect.gen(function* () {
|
|
360
|
+
const rex = yield* readRexContext;
|
|
361
|
+
if (rex)
|
|
362
|
+
incoming = { ...incoming, presence: { ...incoming.presence, rex } };
|
|
358
363
|
const records = yield* cacheRecords(config);
|
|
359
364
|
const target = yield* cacheFile(config, incoming.presence.ref);
|
|
360
365
|
if (records.size >= 1024 && !records.has(target))
|
|
@@ -364,7 +369,8 @@ export const writeNativeCache = (config, incoming) => Effect.gen(function* () {
|
|
|
364
369
|
if (previous !== undefined) {
|
|
365
370
|
const old = previous.inspection;
|
|
366
371
|
const metadataOnly = incoming.presence.nativeUpdatedAt !== undefined;
|
|
367
|
-
|
|
372
|
+
const { programStatus: previousStatus, ...oldPresence } = old.presence;
|
|
373
|
+
inspection = { ...(metadataOnly ? old : incoming), presence: { ...oldPresence, ...(!incoming.presence.activity && previousStatus ? { programStatus: previousStatus } : {}), ...incoming.presence }, queriedAt: incoming.queriedAt, context: metadataOnly && !incoming.presence.activity?.compacting ? old.context : incoming.context };
|
|
368
374
|
}
|
|
369
375
|
const fs = yield* FileSystem.FileSystem;
|
|
370
376
|
const path = yield* Path.Path;
|
|
@@ -458,7 +464,8 @@ export const claudeRoster = (config) => Effect.scoped(Effect.gen(function* () {
|
|
|
458
464
|
const identity = yield* identifyProcess(row.pid);
|
|
459
465
|
if (identity.status !== "available")
|
|
460
466
|
continue;
|
|
461
|
-
|
|
467
|
+
const programStatus = row.waitingFor === "permission prompt" ? { state: "blocked", kind: "permission" } : row.status === "busy" ? { state: "working" } : row.status === "idle" ? { state: "idle" } : undefined;
|
|
468
|
+
sessions.push({ ref: { machineId: config.machineId, harness: "claude", nativeSessionId: id }, observedAt: yield* Clock.currentTimeMillis, ...(row.cwd ? { cwd: row.cwd } : {}), ...(row.kind ? { kind: row.kind } : {}), ...(programStatus ? { programStatus } : {}), processes: [{ pid: row.pid, startIdentity: identity.startTime, ownership: "owned" }], activity: { state: row.status === "busy" ? "working" : row.status === "waiting" ? "waiting" : row.status === "idle" ? "idle" : "unknown", ...(row.waitingFor === "permission prompt" ? { wait: "approval", approval: "pending" } : {}) } });
|
|
462
469
|
}
|
|
463
470
|
return sessions;
|
|
464
471
|
})).pipe(Effect.timeout("5 seconds"));
|
package/dist/src/plugin.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { NodeServices } from "@effect/platform-node";
|
|
2
|
-
import { Cause, Clock,
|
|
2
|
+
import { Cause, Clock, Console, Effect, Layer, Logger, ManagedRuntime, Schema } from "effect";
|
|
3
3
|
import { createInspection, HttpError, InspectionTools, metric, sessionRefKey, unavailableMetric } from "./inspection.js";
|
|
4
4
|
import { Mcp } from "./mcp.js";
|
|
5
5
|
import { connectNative, loadConnectorConfig, NativeError, readConnectorToken, resolveNativeConfig, withProcessResources } from "./native.js";
|
|
6
6
|
import { identifyProcess } from "./resources.js";
|
|
7
7
|
import { Host as PlatformHost } from "./platform.js";
|
|
8
|
+
import { createRexReporter, readRexContext } from "./rex.js";
|
|
8
9
|
const provenance = (source, scope, scopeDescription, observedAt, nativeUpdatedAt = null) => ({
|
|
9
10
|
source, scope, scopeDescription, observedAt, nativeUpdatedAt, estimated: false, derived: false, shared: false, partial: false, stale: false,
|
|
10
11
|
});
|
|
@@ -12,8 +13,10 @@ const provenance = (source, scope, scopeDescription, observedAt, nativeUpdatedAt
|
|
|
12
13
|
export const createNativeObserver = (harness, machineId, processes = []) => Effect.gen(function* () {
|
|
13
14
|
const host = yield* PlatformHost;
|
|
14
15
|
const clock = yield* Clock.Clock;
|
|
15
|
-
const
|
|
16
|
-
|
|
16
|
+
const rex = yield* readRexContext;
|
|
17
|
+
let programStatus;
|
|
18
|
+
let userPrompt;
|
|
19
|
+
let stopReason;
|
|
17
20
|
let current;
|
|
18
21
|
let identity;
|
|
19
22
|
let lastActivityAt;
|
|
@@ -25,6 +28,9 @@ export const createNativeObserver = (harness, machineId, processes = []) => Effe
|
|
|
25
28
|
const key = JSON.stringify([ctx.sessionManager.getSessionId(), harness === "omp" ? ctx.agent?.id : undefined]);
|
|
26
29
|
if (identity !== key) {
|
|
27
30
|
latest = undefined;
|
|
31
|
+
programStatus = undefined;
|
|
32
|
+
userPrompt = undefined;
|
|
33
|
+
stopReason = undefined;
|
|
28
34
|
activeTools.clear();
|
|
29
35
|
identity = key;
|
|
30
36
|
}
|
|
@@ -36,6 +42,33 @@ export const createNativeObserver = (harness, machineId, processes = []) => Effe
|
|
|
36
42
|
activeTools.delete(event.toolCallId);
|
|
37
43
|
if (event?.type === "agent_end")
|
|
38
44
|
activeTools.clear();
|
|
45
|
+
if (event?.type === "agent_start") {
|
|
46
|
+
programStatus = { state: "working" };
|
|
47
|
+
stopReason = undefined;
|
|
48
|
+
}
|
|
49
|
+
if (event?.type === "turn_start") {
|
|
50
|
+
programStatus = { state: "working" };
|
|
51
|
+
stopReason = undefined;
|
|
52
|
+
}
|
|
53
|
+
if (event?.type === "tool_execution_start")
|
|
54
|
+
programStatus = { state: "working" };
|
|
55
|
+
if (event?.message?.role === "assistant" && event.message.stopReason)
|
|
56
|
+
stopReason = event.message.stopReason;
|
|
57
|
+
if (event?.type === "agent_end")
|
|
58
|
+
stopReason = event.messages?.findLast((message) => message.role === "assistant")?.stopReason ?? stopReason;
|
|
59
|
+
if (harness === "pi" && event?.type === "agent_before_settle")
|
|
60
|
+
stopReason = event.outcome === "completed" ? "stop" : event.outcome;
|
|
61
|
+
if (event?.type === "agent_settled" || (harness !== "pi" && event?.type === "agent_end")) {
|
|
62
|
+
programStatus = event.willContinue ? { state: "working" } : stopReason === "aborted" ? { state: "idle" } : stopReason === "error" ? { state: "error" } : stopReason === "stop" || stopReason === "length" ? { state: "done" } : undefined;
|
|
63
|
+
}
|
|
64
|
+
else if (event?.type === "agent_end")
|
|
65
|
+
programStatus = { state: "working" };
|
|
66
|
+
if (harness === "pi" && event?.type === "ui_prompt_start")
|
|
67
|
+
userPrompt = { state: "blocked", kind: "question" };
|
|
68
|
+
if (harness === "pi" && event?.type === "ui_prompt_end")
|
|
69
|
+
userPrompt = undefined;
|
|
70
|
+
if ((event === undefined || event.type === "session_start" || event.type === "session_switch") && programStatus === undefined)
|
|
71
|
+
programStatus = { state: ctx.isIdle() ? "idle" : "working" };
|
|
39
72
|
const message = event?.message;
|
|
40
73
|
if (message?.role === "assistant" && message.usage) {
|
|
41
74
|
const u = message.usage;
|
|
@@ -51,15 +84,18 @@ export const createNativeObserver = (harness, machineId, processes = []) => Effe
|
|
|
51
84
|
if (!current)
|
|
52
85
|
return undefined;
|
|
53
86
|
const currentToolName = activeTools.values().next().value;
|
|
87
|
+
const status = userPrompt ?? programStatus;
|
|
54
88
|
return {
|
|
55
89
|
ref: { machineId, harness, nativeSessionId: current.sessionManager.getSessionId(), ...(harness === "omp" && current.agent?.id ? { nativeAgentId: current.agent.id } : {}) },
|
|
56
90
|
observedAt: now, ...(lastActivityAt === undefined ? {} : { lastActivityAt }), cwd: current.cwd, processes,
|
|
57
91
|
machineName, ...(os ? { os } : {}),
|
|
58
|
-
...(
|
|
92
|
+
...(rex ? { rex } : {}),
|
|
93
|
+
...(status ? { programStatus: status } : {}),
|
|
59
94
|
...(current.model ? { provider: current.model.provider, model: current.model.id } : {}),
|
|
60
95
|
...(current.agent ? { kind: current.agent.kind, ...(current.agent.name ? { agent: current.agent.name } : {}) } : {}),
|
|
61
|
-
activity: { state: current.isIdle() ? "idle" : "working", streaming: !current.isIdle(),
|
|
62
|
-
...(currentToolName === undefined ? {} : { currentToolName
|
|
96
|
+
activity: { state: userPrompt ? "waiting" : current.isIdle() ? "idle" : "working", streaming: !current.isIdle(),
|
|
97
|
+
...(currentToolName === undefined ? {} : { currentToolName }),
|
|
98
|
+
...(userPrompt ? { wait: "user" } : currentToolName === undefined ? {} : { wait: "tool" }),
|
|
63
99
|
},
|
|
64
100
|
};
|
|
65
101
|
};
|
|
@@ -146,7 +182,7 @@ export const createNativeObserver = (harness, machineId, processes = []) => Effe
|
|
|
146
182
|
},
|
|
147
183
|
};
|
|
148
184
|
};
|
|
149
|
-
return { update, presence, inspect, clear: () => { current = undefined; identity = undefined; latest = undefined; lastActivityAt = undefined; activeTools.clear(); } };
|
|
185
|
+
return { update, presence, inspect, clear: () => { current = undefined; identity = undefined; latest = undefined; programStatus = undefined; userPrompt = undefined; stopReason = undefined; lastActivityAt = undefined; activeTools.clear(); } };
|
|
150
186
|
});
|
|
151
187
|
export const isCoordinateInput = (text) => /^coordinate(?:\s|$)/u.test(text);
|
|
152
188
|
export const transformCoordinateInput = (harness, input, snapshot) => {
|
|
@@ -161,6 +197,23 @@ export const registerCoordinateKeyword = Effect.gen(function* () {
|
|
|
161
197
|
// ponytail: native exposes no registry getter/append; only known core keywords can be preserved.
|
|
162
198
|
setMagicKeywords([...MAGIC_KEYWORDS.filter(({ word }) => isMagicKeyword(word)), { word: "coordinate", hue: [190, 260] }]);
|
|
163
199
|
});
|
|
200
|
+
const formatSessionList = (sessions) => {
|
|
201
|
+
if (sessions.length === 0)
|
|
202
|
+
return "No sessions are currently visible to this API key.";
|
|
203
|
+
const lines = [`Coordinator: ${sessions.length} sessions visible to this API key.`, "", "~~~text"];
|
|
204
|
+
for (const session of sessions) {
|
|
205
|
+
lines.push(`Machine: ${JSON.stringify(session.machineName ?? "unavailable")} (${JSON.stringify(session.ref.machineId)})`, `Session: ${session.ref.harness} ${JSON.stringify(session.ref.nativeSessionId)}`);
|
|
206
|
+
if (session.ref.nativeAgentId !== undefined)
|
|
207
|
+
lines.push(`Agent ID: ${JSON.stringify(session.ref.nativeAgentId)}`);
|
|
208
|
+
for (const [field, value] of Object.entries(session)) {
|
|
209
|
+
if (field !== "ref" && field !== "machineName" && value !== undefined)
|
|
210
|
+
lines.push(`${field}: ${JSON.stringify(value)}`);
|
|
211
|
+
}
|
|
212
|
+
lines.push("");
|
|
213
|
+
}
|
|
214
|
+
lines.push("~~~");
|
|
215
|
+
return lines.join("\n");
|
|
216
|
+
};
|
|
164
217
|
export default function plugin(host) {
|
|
165
218
|
const harness = COORDINATOR_HARNESS;
|
|
166
219
|
const runtime = ManagedRuntime.make(Layer.mergeAll(NodeServices.layer, PlatformHost.layer, Mcp.layer, Layer.succeed(Logger.LogToStderr, true)));
|
|
@@ -168,6 +221,50 @@ export default function plugin(host) {
|
|
|
168
221
|
let observer;
|
|
169
222
|
let starting;
|
|
170
223
|
let stopped = false;
|
|
224
|
+
// Stock OMP owns OSC 7501 (terminal.programStatus); never duplicate or override it.
|
|
225
|
+
const rexReporter = harness === "pi" ? createRexReporter((sequence) => process.stdout.write(sequence)) : undefined;
|
|
226
|
+
let terminal = false;
|
|
227
|
+
host.registerCommand("coordinator", {
|
|
228
|
+
description: "List visible sessions, their machines, and native metadata",
|
|
229
|
+
handler: async (args) => {
|
|
230
|
+
if (args.trim() !== "list") {
|
|
231
|
+
host.sendMessage({ customType: "coordinator-list", content: "Usage: /coordinator list", display: true }, { triggerTurn: false });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const exit = await runtime.runPromiseExit(Effect.gen(function* () {
|
|
235
|
+
const activeConfig = config ?? (yield* loadConnectorConfig());
|
|
236
|
+
if (!activeConfig)
|
|
237
|
+
return yield* new NativeError({ operation: "Connector configuration required" });
|
|
238
|
+
const token = yield* readConnectorToken(activeConfig);
|
|
239
|
+
const mcp = yield* Mcp;
|
|
240
|
+
const sessions = [];
|
|
241
|
+
let cursor;
|
|
242
|
+
do {
|
|
243
|
+
const page = yield* mcp.call(activeConfig.inspectorUrl, token, InspectionTools.list, { limit: 100, ...(cursor === undefined ? {} : { cursor }) });
|
|
244
|
+
for (const session of page.sessions)
|
|
245
|
+
sessions.push(session);
|
|
246
|
+
cursor = page.nextCursor ?? undefined;
|
|
247
|
+
} while (cursor !== undefined);
|
|
248
|
+
return sessions;
|
|
249
|
+
}));
|
|
250
|
+
if (stopped)
|
|
251
|
+
return;
|
|
252
|
+
if (exit._tag === "Failure") {
|
|
253
|
+
const error = Cause.findErrorOption(exit.cause);
|
|
254
|
+
const reason = error._tag === "Some" ? error.value._tag === "HttpError" ? `HTTP ${error.value.status}` : error.value._tag : "unexpected failure";
|
|
255
|
+
host.sendMessage({
|
|
256
|
+
customType: "coordinator-list",
|
|
257
|
+
content: `Coordinator list unavailable (${reason}). Check inspectorUrl, your access token, and API reachability.`,
|
|
258
|
+
display: true,
|
|
259
|
+
}, { triggerTurn: false });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
host.sendMessage({
|
|
263
|
+
customType: "coordinator-list", content: formatSessionList(exit.value), display: true,
|
|
264
|
+
details: { sessions: exit.value },
|
|
265
|
+
}, { triggerTurn: false });
|
|
266
|
+
},
|
|
267
|
+
});
|
|
171
268
|
if (harness === "pi") {
|
|
172
269
|
const register = (descriptor) => {
|
|
173
270
|
const document = Schema.toJsonSchemaDocument(Schema.toCodecJson(descriptor.input), { onExcessProperty: "error" });
|
|
@@ -229,6 +326,8 @@ export default function plugin(host) {
|
|
|
229
326
|
const processIdentity = yield* identifyProcess(host.pid);
|
|
230
327
|
observer = yield* createNativeObserver(harness, nativeConfig.machineId, processIdentity.status === "available" ? [{ pid: host.pid, startIdentity: processIdentity.startTime, ownership: harness === "omp" ? "shared" : "owned" }] : []);
|
|
231
328
|
observer.update(ctx);
|
|
329
|
+
terminal = ctx.mode === "tui" && ctx.hasUI && process.stdout.isTTY === true;
|
|
330
|
+
rexReporter?.update(observer.presence()?.programStatus, terminal);
|
|
232
331
|
// Native TUI constructors register core keywords before session_start hooks.
|
|
233
332
|
if (harness === "omp" && ctx.hasUI && ctx.mode === "tui")
|
|
234
333
|
yield* registerCoordinateKeyword;
|
|
@@ -253,6 +352,8 @@ export default function plugin(host) {
|
|
|
253
352
|
}
|
|
254
353
|
});
|
|
255
354
|
const events = ["session_start", "session_switch", "agent_start", "agent_end", "turn_start", "turn_end", "message_end", "tool_execution_start", "tool_execution_end"];
|
|
355
|
+
if (harness === "pi")
|
|
356
|
+
events.push("agent_before_settle", "agent_settled", "ui_prompt_start", "ui_prompt_end");
|
|
256
357
|
for (const name of events) {
|
|
257
358
|
host.on(name, async (event, ctx) => {
|
|
258
359
|
if (stopped)
|
|
@@ -260,11 +361,18 @@ export default function plugin(host) {
|
|
|
260
361
|
await start(ctx);
|
|
261
362
|
if (stopped || !observer)
|
|
262
363
|
return;
|
|
263
|
-
await runtime.runPromiseExit(Effect.sync(() =>
|
|
364
|
+
await runtime.runPromiseExit(Effect.sync(() => {
|
|
365
|
+
if (name === "session_switch")
|
|
366
|
+
rexReporter?.update(undefined, terminal);
|
|
367
|
+
observer?.update(ctx, event);
|
|
368
|
+
terminal = ctx.mode === "tui" && ctx.hasUI && process.stdout.isTTY === true;
|
|
369
|
+
rexReporter?.update(observer?.presence()?.programStatus, terminal);
|
|
370
|
+
}).pipe(Effect.tapCause(reportCause(ctx, "native update"))));
|
|
264
371
|
});
|
|
265
372
|
}
|
|
266
373
|
host.on("session_shutdown", async () => {
|
|
267
374
|
stopped = true;
|
|
375
|
+
rexReporter?.close(terminal);
|
|
268
376
|
try {
|
|
269
377
|
await runtime.dispose();
|
|
270
378
|
}
|
package/dist/src/rex.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Config, Effect } from "effect";
|
|
3
|
+
export const readRexContext = Effect.gen(function* () {
|
|
4
|
+
const serverAddress = yield* Config.String("REX_SERVER").pipe(Config.withDefault(""));
|
|
5
|
+
const sessionId = yield* Config.String("REX_SESSION").pipe(Config.withDefault(""));
|
|
6
|
+
const blockId = yield* Config.String("REX_BLOCK").pipe(Config.withDefault(""));
|
|
7
|
+
return serverAddress || sessionId || blockId ? {
|
|
8
|
+
...(serverAddress ? { serverAddress } : {}), ...(sessionId ? { sessionId } : {}), ...(blockId ? { blockId } : {}),
|
|
9
|
+
} : undefined;
|
|
10
|
+
});
|
|
11
|
+
// ponytail: fixed labels avoid leaking native content and stay below all OSC text limits.
|
|
12
|
+
export const rexSequence = (id, status) => {
|
|
13
|
+
if (id.length > 128 || id.split("/").length > 8 || !/^[A-Za-z0-9_.+-]{1,32}(?:\/[A-Za-z0-9_.+-]{1,32})*$/u.test(id))
|
|
14
|
+
return "";
|
|
15
|
+
return `\u001b]7501;id=${id}:state=${status?.state ?? "clear"}:app=coordinator:title=Q29vcmRpbmF0b3I=${status?.state === "blocked" ? `:kind=${status.kind}` : ""}\u001b\\`;
|
|
16
|
+
};
|
|
17
|
+
export const createRexReporter = (write) => {
|
|
18
|
+
const id = `coordinator/${randomUUID().replaceAll("-", "")}`;
|
|
19
|
+
let previous;
|
|
20
|
+
return {
|
|
21
|
+
update: (status, terminal) => {
|
|
22
|
+
if (!terminal)
|
|
23
|
+
return;
|
|
24
|
+
const sequence = rexSequence(id, status);
|
|
25
|
+
if (previous === sequence || (previous === undefined && status === undefined))
|
|
26
|
+
return;
|
|
27
|
+
write(sequence);
|
|
28
|
+
previous = sequence;
|
|
29
|
+
},
|
|
30
|
+
close: (terminal) => {
|
|
31
|
+
if (terminal && previous !== undefined && !/:state=(?:done|error):/u.test(previous))
|
|
32
|
+
write(rexSequence(id, undefined));
|
|
33
|
+
previous = undefined;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { chmod, lstat, mkdir, open, realpath, rmdir, unlink } from "node:fs/promises";
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import { isIP } from "node:net";
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { Config, Console, Effect, Option, Redacted, Schema } from "effect";
|
|
7
|
+
import { Command, Flag, Prompt } from "effect/cli";
|
|
8
|
+
import { ConnectorConfig, InspectionTools, InspectorConfig } from "./inspection.js";
|
|
9
|
+
import { Mcp } from "./mcp.js";
|
|
10
|
+
import { NativeError, readConnectorToken } from "./native.js";
|
|
11
|
+
import { Host } from "./platform.js";
|
|
12
|
+
const failure = (operation) => () => new NativeError({ operation });
|
|
13
|
+
const privateFailure = failure("Setup could not write private files; use a canonical owner-controlled path, mode-0700 directory and new filenames. Existing files are never overwritten");
|
|
14
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
15
|
+
const canonical = (file) => {
|
|
16
|
+
if (!isAbsolute(file) || resolve(file) !== file || /[\x00-\x1f\x7f-\x9f]/u.test(file))
|
|
17
|
+
throw new Error("Unsafe path");
|
|
18
|
+
};
|
|
19
|
+
// Exclusive creation is the overwrite fence; remove only files this transaction created.
|
|
20
|
+
const writePrivate = (directory, files, uid) => Effect.tryPromise({
|
|
21
|
+
try: async () => {
|
|
22
|
+
canonical(directory);
|
|
23
|
+
if (uid === undefined)
|
|
24
|
+
throw new Error("Owner unavailable");
|
|
25
|
+
const createdDirectories = [], createdFiles = [];
|
|
26
|
+
const ensure = async (target, privateDirectory) => {
|
|
27
|
+
try {
|
|
28
|
+
const info = await lstat(target);
|
|
29
|
+
if (!info.isDirectory() || info.isSymbolicLink() || await realpath(target) !== target || (privateDirectory && (info.uid !== uid || (info.mode & 0o777) !== 0o700)))
|
|
30
|
+
throw new Error("Unsafe directory");
|
|
31
|
+
// Ancestors must not be replaceable by another owner; sticky /tmp is allowed.
|
|
32
|
+
if (!privateDirectory && (info.uid !== uid && info.uid !== 0 || (info.mode & 0o022) !== 0 && (info.mode & 0o1000) === 0))
|
|
33
|
+
throw new Error("Unsafe ancestor");
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "ENOENT")
|
|
37
|
+
throw error;
|
|
38
|
+
await ensure(dirname(target), false);
|
|
39
|
+
await mkdir(target, { mode: 0o700 });
|
|
40
|
+
createdDirectories.push(target);
|
|
41
|
+
await chmod(target, 0o700);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
await ensure(directory, true);
|
|
46
|
+
// realpath rejects symlinks anywhere in existing ancestry; validate its writable ancestors too.
|
|
47
|
+
for (let parent = dirname(directory);; parent = dirname(parent)) {
|
|
48
|
+
await ensure(parent, false);
|
|
49
|
+
if (parent === dirname(parent))
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
for (const [file, content] of files) {
|
|
53
|
+
canonical(file);
|
|
54
|
+
if (dirname(file) !== directory)
|
|
55
|
+
throw new Error("Unsafe destination");
|
|
56
|
+
const handle = await open(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
57
|
+
createdFiles.push(file);
|
|
58
|
+
try {
|
|
59
|
+
await handle.chmod(0o600);
|
|
60
|
+
await handle.writeFile(content, "utf8");
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await handle.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
for (const file of createdFiles.reverse())
|
|
69
|
+
await unlink(file).catch(() => undefined);
|
|
70
|
+
for (const dir of createdDirectories.reverse())
|
|
71
|
+
await rmdir(dir).catch(() => undefined);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
catch: privateFailure,
|
|
76
|
+
});
|
|
77
|
+
const configHome = Effect.gen(function* () {
|
|
78
|
+
const host = yield* Host;
|
|
79
|
+
const xdg = yield* Config.String("XDG_CONFIG_HOME").pipe(Config.option);
|
|
80
|
+
return join(Option.getOrUndefined(xdg) ?? join(host.homeDirectory, ".config"), "coordinator");
|
|
81
|
+
});
|
|
82
|
+
export const initializeServer = (options) => Effect.gen(function* () {
|
|
83
|
+
if ((!isIP(options.host) && options.host !== "localhost") || !Number.isInteger(options.port) || options.port < 1 || options.port > 65535 || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u.test(options.owner)) {
|
|
84
|
+
return yield* new NativeError({ operation: "Init requires an IP address or localhost, port 1–65535, and an owner identifier containing only letters, digits, dot, underscore or hyphen" });
|
|
85
|
+
}
|
|
86
|
+
const token = yield* Effect.try({ try: () => randomBytes(32).toString("base64url"), catch: failure("Secure credential generation unavailable") });
|
|
87
|
+
const config = yield* Schema.decodeEffect(InspectorConfig)({ host: options.host, port: options.port, principals: [{ id: options.owner, owner: options.owner, tokenHash: createHash("sha256").update(token).digest("hex") }] }).pipe(Effect.mapError(failure("Invalid server configuration")));
|
|
88
|
+
const configFile = join(options.directory, "server.json"), tokenFile = join(options.directory, "owner.token");
|
|
89
|
+
yield* writePrivate(options.directory, [[tokenFile, `${token}\n`], [configFile, `${JSON.stringify(config, null, 2)}\n`]], (yield* Host).uid);
|
|
90
|
+
return { configFile, tokenFile };
|
|
91
|
+
});
|
|
92
|
+
export const connectClient = (options) => Effect.gen(function* () {
|
|
93
|
+
const targetToken = options.tokenFile ?? join(dirname(options.configFile), "inspector.token");
|
|
94
|
+
const config = yield* Schema.decodeEffect(ConnectorConfig)({ inspectorUrl: options.endpoint, apiToken: options.token, tokenFile: targetToken }).pipe(Effect.mapError(failure("Invalid endpoint or credential; use HTTPS except for loopback, without URL credentials, query or fragment")));
|
|
95
|
+
if (options.tokenFile !== undefined) {
|
|
96
|
+
const stored = yield* readConnectorToken({ inspectorUrl: options.endpoint, tokenFile: options.tokenFile });
|
|
97
|
+
if (stored !== options.token)
|
|
98
|
+
return yield* new NativeError({ operation: "Credential file changed; retry connection setup" });
|
|
99
|
+
}
|
|
100
|
+
// This is an authenticated read only: it never resolves/creates a native machine identity.
|
|
101
|
+
yield* (yield* Mcp).call(config.inspectorUrl, options.token, InspectionTools.list, { limit: 1 }).pipe(Effect.mapError(failure("Connection check failed; verify the endpoint, TLS certificate and owner credential. No client configuration was written")));
|
|
102
|
+
const files = [];
|
|
103
|
+
if (options.tokenFile === undefined)
|
|
104
|
+
files.push([targetToken, `${options.token}\n`]);
|
|
105
|
+
files.push([options.configFile, `${JSON.stringify({ inspectorUrl: config.inspectorUrl, tokenFile: targetToken }, null, 2)}\n`]);
|
|
106
|
+
yield* writePrivate(dirname(options.configFile), files, (yield* Host).uid);
|
|
107
|
+
return { configFile: options.configFile, tokenFile: targetToken };
|
|
108
|
+
});
|
|
109
|
+
const init = Command.make("init", {
|
|
110
|
+
directory: Flag.String("directory").pipe(Flag.optional),
|
|
111
|
+
owner: Flag.String("owner").pipe(Flag.withDefault("owner")),
|
|
112
|
+
host: Flag.String("listen-host").pipe(Flag.withDefault("127.0.0.1")),
|
|
113
|
+
port: Flag.Int("port").pipe(Flag.withDefault(8787)),
|
|
114
|
+
}, (flags) => Effect.gen(function* () {
|
|
115
|
+
const directory = Option.getOrUndefined(flags.directory) ?? join(yield* configHome, "server");
|
|
116
|
+
const result = yield* initializeServer({ ...flags, directory });
|
|
117
|
+
yield* Console.log(`Created private server configuration: ${result.configFile}\nOwner credential file: ${result.tokenFile}\nStart server:\nCOORDINATOR_CONFIG=${quote(result.configFile)} coordinator inspector\nKeep the credential private. For remote clients, expose this server through HTTPS.`);
|
|
118
|
+
})).pipe(Command.withDescription("Bootstrap a self-hosted server without printing its owner credential. Never overwrites existing files."));
|
|
119
|
+
const connect = Command.make("connect", {
|
|
120
|
+
endpoint: Flag.String("endpoint").pipe(Flag.optional),
|
|
121
|
+
tokenFile: Flag.String("token-file").pipe(Flag.optional),
|
|
122
|
+
config: Flag.String("config").pipe(Flag.optional),
|
|
123
|
+
}, (flags) => Effect.gen(function* () {
|
|
124
|
+
const selected = yield* Config.String("COORDINATOR_CONNECTOR_CONFIG").pipe(Config.option);
|
|
125
|
+
const configFile = Option.getOrUndefined(flags.config) ?? Option.getOrUndefined(selected) ?? join(yield* configHome, "inspector.json");
|
|
126
|
+
const endpoint = Option.getOrUndefined(flags.endpoint) ?? (yield* Prompt.String({ message: "Coordinator endpoint (HTTPS, or loopback HTTP)" }));
|
|
127
|
+
const tokenFile = Option.getOrUndefined(flags.tokenFile);
|
|
128
|
+
const environment = yield* Config.String("COORDINATOR_API_TOKEN").pipe(Config.option);
|
|
129
|
+
const token = tokenFile !== undefined ? yield* readConnectorToken({ inspectorUrl: endpoint, tokenFile })
|
|
130
|
+
: Option.isSome(environment) ? environment.value : Redacted.value(yield* Prompt.Password({ message: "Owner credential" }));
|
|
131
|
+
const result = yield* connectClient({ endpoint, token, configFile, ...(tokenFile === undefined ? {} : { tokenFile }) });
|
|
132
|
+
yield* Console.log(`Authenticated connection saved: ${result.configFile}\nCredential file: ${result.tokenFile}\nClient configuration contains only the endpoint and credential-file pointer. Existing native settings are unchanged.\nCOORDINATOR_API_URL / COORDINATOR_API_TOKEN environment overrides still take precedence at runtime.`);
|
|
133
|
+
})).pipe(Command.withDescription("Authenticate and save a private client configuration. Credentials: --token-file, COORDINATOR_API_TOKEN, or masked prompt; never argv. Existing files are not overwritten."));
|
|
134
|
+
export const runSetup = (args) => Command.runWith(Command.make("coordinator").pipe(Command.withSubcommands([init, connect])), { version: "", renderErrors: false })(args).pipe(Effect.catchTag("ShowHelp", () => Effect.fail(new NativeError({ operation: "Invalid setup flags. Run coordinator init --help or coordinator connect --help. Credentials are never accepted as arguments" }))));
|