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

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.
@@ -1,6 +1,6 @@
1
1
  // src/plugin.ts
2
2
  import { NodeServices } from "@effect/platform-node";
3
- import { Cause as Cause3, Clock as Clock3, Config as Config3, Console as Console2, Effect as Effect6, Layer as Layer3, Logger, ManagedRuntime, Schema as Schema5 } from "effect";
3
+ import { Cause as Cause3, Clock as Clock3, Console as Console2, Effect as Effect7, Formatter as Formatter2, Layer as Layer3, Logger, ManagedRuntime, Schema as Schema5 } from "effect";
4
4
 
5
5
  // src/inspection.ts
6
6
  import { Effect, Option, Path as PlatformPath, Schema, SchemaParser } from "effect";
@@ -40,6 +40,13 @@ var ProcessIdentity = Schema.Struct({
40
40
  ownership: Schema.Literals(["owned", "shared", "unknown"])
41
41
  });
42
42
  var NativeWorkState = Schema.Struct({ id: Id, state: Schema.Literals(["pending", "active", "completed", "cancelled", "unknown"]) });
43
+ var RexText = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(4096), Schema.makeFilter(
44
+ (value) => /[\u0000-\u001f\u007f-\u009f]/u.test(value) ? "Control characters are forbidden" : void 0
45
+ ));
46
+ var ProgramStatus = Schema.Union([
47
+ Schema.Struct({ state: Schema.Literals(["idle", "working", "done", "error"]) }),
48
+ Schema.Struct({ state: Schema.Literal("blocked"), kind: Schema.Literals(["permission", "question", "auth"]) })
49
+ ]);
43
50
  var SessionPresence = Schema.Struct({
44
51
  ref: SessionRef,
45
52
  observedAt: Timestamp,
@@ -60,7 +67,8 @@ var SessionPresence = Schema.Struct({
60
67
  agent: Schema.optional(Id),
61
68
  cwd: Schema.optional(Path),
62
69
  repository: Schema.optional(Path),
63
- rex: Schema.optional(Schema.Struct({ serverId: Schema.optional(Id), blockId: Schema.optional(Id), sessionId: Schema.optional(Id), windowId: Schema.optional(Id), paneId: Schema.optional(Id), terminalId: Schema.optional(Id) })),
70
+ rex: Schema.optional(Schema.Struct({ serverAddress: Schema.optional(RexText), blockId: Schema.optional(RexText), sessionId: Schema.optional(RexText) })),
71
+ programStatus: Schema.optional(ProgramStatus),
64
72
  processes: Schema.optional(bounded(ProcessIdentity, 128)),
65
73
  activity: Schema.optional(Schema.Struct({
66
74
  state: Schema.Literals(["idle", "working", "waiting", "stopped", "unknown"]),
@@ -561,7 +569,7 @@ var Mcp = class extends Context.Service()("@samuelho-dev/coordinator/Mcp", {
561
569
  };
562
570
 
563
571
  // src/native.ts
564
- import { Cause as Cause2, Clock as Clock2, Config as Config2, Console, Crypto, Data as Data2, Duration, Effect as Effect5, FileSystem as FileSystem3, Option as Option4, Path as Path2, PlatformError as PlatformError2, Schedule, Schema as Schema4, Scope } from "effect";
572
+ import { Cause as Cause2, Clock as Clock2, Config as Config3, Console, Crypto, Data as Data2, Duration, Effect as Effect6, FileSystem as FileSystem3, Option as Option4, Path as Path2, PlatformError as PlatformError2, Schedule, Schema as Schema4, Scope } from "effect";
565
573
  import { Hex } from "effect/encoding";
566
574
 
567
575
  // src/platform.ts
@@ -879,16 +887,51 @@ var observeResources = (roots) => Effect4.gen(function* () {
879
887
  };
880
888
  });
881
889
 
890
+ // src/rex.ts
891
+ import { randomUUID } from "node:crypto";
892
+ import { Config as Config2, Effect as Effect5 } from "effect";
893
+ var readRexContext = Effect5.gen(function* () {
894
+ const serverAddress = yield* Config2.String("REX_SERVER").pipe(Config2.withDefault(""));
895
+ const sessionId = yield* Config2.String("REX_SESSION").pipe(Config2.withDefault(""));
896
+ const blockId = yield* Config2.String("REX_BLOCK").pipe(Config2.withDefault(""));
897
+ return serverAddress || sessionId || blockId ? {
898
+ ...serverAddress ? { serverAddress } : {},
899
+ ...sessionId ? { sessionId } : {},
900
+ ...blockId ? { blockId } : {}
901
+ } : void 0;
902
+ });
903
+ var rexSequence = (id, status) => {
904
+ if (id.length > 128 || id.split("/").length > 8 || !/^[A-Za-z0-9_.+-]{1,32}(?:\/[A-Za-z0-9_.+-]{1,32})*$/u.test(id)) return "";
905
+ return `\x1B]7501;id=${id}:state=${status?.state ?? "clear"}:app=coordinator:title=Q29vcmRpbmF0b3I=${status?.state === "blocked" ? `:kind=${status.kind}` : ""}\x1B\\`;
906
+ };
907
+ var createRexReporter = (write) => {
908
+ const id = `coordinator/${randomUUID().replaceAll("-", "")}`;
909
+ let previous;
910
+ return {
911
+ update: (status, terminal) => {
912
+ if (!terminal) return;
913
+ const sequence = rexSequence(id, status);
914
+ if (previous === sequence || previous === void 0 && status === void 0) return;
915
+ write(sequence);
916
+ previous = sequence;
917
+ },
918
+ close: (terminal) => {
919
+ if (terminal && previous !== void 0 && !/:state=(?:done|error):/u.test(previous)) write(rexSequence(id, void 0));
920
+ previous = void 0;
921
+ }
922
+ };
923
+ };
924
+
882
925
  // src/native.ts
883
926
  var NativeError = class extends Data2.TaggedError("NativeError") {
884
927
  };
885
- var json = (text) => Effect5.try({ try: () => JSON.parse(text), catch: () => new NativeError({ operation: "Invalid native JSON" }) });
928
+ var json = (text) => Effect6.try({ try: () => JSON.parse(text), catch: () => new NativeError({ operation: "Invalid native JSON" }) });
886
929
  var maximumBytes = 65536;
887
930
  var platformFailure = (operation) => () => new NativeError({ operation });
888
931
  var sanitizeFileFinalizers = (operation) => (effect) => effect.pipe(
889
- Effect5.catchCause((cause) => Effect5.failCause(Cause2.fromReasons(cause.reasons.map((reason) => reason._tag === "Die" && PlatformError2.isPlatformError(reason.defect) && reason.defect.reason.module === "FileSystem" ? Cause2.makeFailReason(new NativeError({ operation })) : reason))))
932
+ Effect6.catchCause((cause) => Effect6.failCause(Cause2.fromReasons(cause.reasons.map((reason) => reason._tag === "Die" && PlatformError2.isPlatformError(reason.defect) && reason.defect.reason.module === "FileSystem" ? Cause2.makeFailReason(new NativeError({ operation })) : reason))))
890
933
  );
891
- var validateDirectory = (directory, exact = false) => Effect5.gen(function* () {
934
+ var validateDirectory = (directory, exact = false) => Effect6.gen(function* () {
892
935
  const fs = yield* FileSystem3.FileSystem;
893
936
  const host = yield* Host;
894
937
  const info = yield* fs.stat(directory);
@@ -896,7 +939,7 @@ var validateDirectory = (directory, exact = false) => Effect5.gen(function* () {
896
939
  return yield* new NativeError({ operation: "Unsafe private directory" });
897
940
  }
898
941
  });
899
- var readPrivateText = (file, exact = false) => Effect5.scoped(Effect5.gen(function* () {
942
+ var readPrivateText = (file, exact = false) => Effect6.scoped(Effect6.gen(function* () {
900
943
  const fs = yield* FileSystem3.FileSystem;
901
944
  const path = yield* Path2.Path;
902
945
  const host = yield* Host;
@@ -915,73 +958,73 @@ var readPrivateText = (file, exact = false) => Effect5.scoped(Effect5.gen(functi
915
958
  if (length > maximumBytes) return yield* new NativeError({ operation: "Private file exceeds bound" });
916
959
  return new TextDecoder().decode(bytes.subarray(0, length));
917
960
  })).pipe(sanitizeFileFinalizers("Read private file"));
918
- var readPrivateFile = (file) => readPrivateText(file).pipe(Effect5.mapError(platformFailure("Read private file")));
919
- var loadConnectorConfig = (file) => Effect5.gen(function* () {
961
+ var readPrivateFile = (file) => readPrivateText(file).pipe(Effect6.mapError(platformFailure("Read private file")));
962
+ var loadConnectorConfig = (file) => Effect6.gen(function* () {
920
963
  const fs = yield* FileSystem3.FileSystem;
921
964
  const path = yield* Path2.Path;
922
965
  const host = yield* Host;
923
- const selected = yield* Config2.String("COORDINATOR_CONNECTOR_CONFIG").pipe(Config2.option);
924
- const xdg = yield* Config2.String("XDG_CONFIG_HOME").pipe(Config2.option);
966
+ const selected = yield* Config3.String("COORDINATOR_CONNECTOR_CONFIG").pipe(Config3.option);
967
+ const xdg = yield* Config3.String("XDG_CONFIG_HOME").pipe(Config3.option);
925
968
  const explicit = file ?? Option4.getOrUndefined(selected);
926
969
  const target = explicit ?? path.join(Option4.getOrUndefined(xdg) ?? path.join(host.homeDirectory, ".config"), "coordinator", "inspector.json");
927
970
  const exists = explicit !== void 0 || (yield* fs.readDirectory(path.dirname(target)).pipe(
928
- Effect5.map((entries) => entries.includes(path.basename(target))),
929
- Effect5.catch((error) => error.reason._tag === "NotFound" ? Effect5.succeed(false) : Effect5.fail(error)),
930
- Effect5.mapError(platformFailure("Locate connector configuration"))
971
+ Effect6.map((entries) => entries.includes(path.basename(target))),
972
+ Effect6.catch((error) => error.reason._tag === "NotFound" ? Effect6.succeed(false) : Effect6.fail(error)),
973
+ Effect6.mapError(platformFailure("Locate connector configuration"))
931
974
  ));
932
975
  const fields = yield* Schema4.decodeUnknownEffect(Schema4.Struct({
933
976
  inspectorUrl: Schema4.optional(Schema4.Unknown),
934
977
  apiToken: Schema4.optional(Schema4.Unknown),
935
978
  tokenFile: Schema4.optional(Schema4.Unknown)
936
979
  }), { onExcessProperty: "error" })(exists ? yield* json(yield* readPrivateFile(target)) : {}).pipe(
937
- Effect5.mapError(platformFailure("Invalid connector configuration"))
980
+ Effect6.mapError(platformFailure("Invalid connector configuration"))
938
981
  );
939
- const url = yield* Config2.String("COORDINATOR_API_URL").pipe(Config2.option);
940
- const token = yield* Config2.String("COORDINATOR_API_TOKEN").pipe(Config2.option);
982
+ const url = yield* Config3.String("COORDINATOR_API_URL").pipe(Config3.option);
983
+ const token = yield* Config3.String("COORDINATOR_API_TOKEN").pipe(Config3.option);
941
984
  const inspectorUrl = Option4.getOrUndefined(url) ?? fields.inspectorUrl;
942
985
  if (!exists && inspectorUrl === void 0) return void 0;
943
986
  return yield* Schema4.decodeUnknownEffect(ConnectorConfig, { onExcessProperty: "error" })({
944
987
  ...fields,
945
988
  ...inspectorUrl === void 0 ? {} : { inspectorUrl },
946
989
  ...Option4.isNone(token) ? {} : { apiToken: token.value }
947
- }).pipe(Effect5.mapError(platformFailure("Invalid connector configuration")));
990
+ }).pipe(Effect6.mapError(platformFailure("Invalid connector configuration")));
948
991
  });
949
- var readConnectorToken = (config) => Effect5.gen(function* () {
992
+ var readConnectorToken = (config) => Effect6.gen(function* () {
950
993
  if (config.apiToken !== void 0) return config.apiToken;
951
994
  if (config.tokenFile === void 0) return yield* new NativeError({ operation: "Connector credential required" });
952
995
  const token = (yield* readPrivateFile(config.tokenFile)).trim();
953
996
  if (token.length < 16 || token.length > 4096 || !/^[A-Za-z0-9._~+\/-]+=*$/.test(token)) return yield* new NativeError({ operation: "Invalid connector credential" });
954
997
  return token;
955
998
  });
956
- var resolveNativeConfig = (config) => Effect5.gen(function* () {
999
+ var resolveNativeConfig = (config) => Effect6.gen(function* () {
957
1000
  const fs = yield* FileSystem3.FileSystem;
958
1001
  const path = yield* Path2.Path;
959
1002
  const crypto = yield* Crypto.Crypto;
960
1003
  const host = yield* Host;
961
- const xdg = yield* Config2.String("XDG_STATE_HOME").pipe(Config2.option);
1004
+ const xdg = yield* Config3.String("XDG_STATE_HOME").pipe(Config3.option);
962
1005
  const base = Option4.getOrUndefined(xdg) ?? (host.platform === "darwin" ? path.join(host.homeDirectory, "Library", "Application Support") : path.join(host.homeDirectory, ".local", "state"));
963
1006
  if (!path.isAbsolute(base) || path.resolve(base) !== base || /[\0\r\n]/.test(base)) return yield* new NativeError({ operation: "Unsafe state path" });
964
1007
  const stateDirectory = path.join(base, "coordinator");
965
1008
  yield* fs.makeDirectory(stateDirectory, { recursive: true, mode: 448 });
966
1009
  yield* validateDirectory(stateDirectory, true);
967
1010
  const target = path.join(stateDirectory, "machine-id");
968
- const readIdentity = readPrivateText(target, true).pipe(Effect5.flatMap((text) => /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\n$/.test(text) ? Effect5.succeed(text.slice(0, -1)) : Effect5.fail(new NativeError({ operation: "Invalid installation identity" }))));
969
- const existing = yield* readIdentity.pipe(Effect5.map(Option4.some), Effect5.catch((error) => error._tag === "PlatformError" && error.reason._tag === "NotFound" ? Effect5.succeed(Option4.none()) : Effect5.fail(error)));
1011
+ const readIdentity = readPrivateText(target, true).pipe(Effect6.flatMap((text) => /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\n$/.test(text) ? Effect6.succeed(text.slice(0, -1)) : Effect6.fail(new NativeError({ operation: "Invalid installation identity" }))));
1012
+ const existing = yield* readIdentity.pipe(Effect6.map(Option4.some), Effect6.catch((error) => error._tag === "PlatformError" && error.reason._tag === "NotFound" ? Effect6.succeed(Option4.none()) : Effect6.fail(error)));
970
1013
  if (Option4.isSome(existing)) return { ...config, machineId: existing.value, stateDirectory };
971
1014
  const temporary = path.join(stateDirectory, `.machine-id-${yield* crypto.randomUUIDv4()}`);
972
1015
  const machineId = yield* crypto.randomUUIDv4();
973
- return yield* Effect5.scoped(Effect5.gen(function* () {
974
- const cleanupScope = yield* Effect5.scope;
975
- yield* Effect5.scoped(Effect5.uninterruptibleMask((restore) => Effect5.gen(function* () {
1016
+ return yield* Effect6.scoped(Effect6.gen(function* () {
1017
+ const cleanupScope = yield* Effect6.scope;
1018
+ yield* Effect6.scoped(Effect6.uninterruptibleMask((restore) => Effect6.gen(function* () {
976
1019
  const handle = yield* fs.open(temporary, { flag: "wx", mode: 384 });
977
- yield* Scope.addFinalizer(cleanupScope, fs.remove(temporary).pipe(Effect5.orDie));
1020
+ yield* Scope.addFinalizer(cleanupScope, fs.remove(temporary).pipe(Effect6.orDie));
978
1021
  yield* restore(handle.writeAll(new TextEncoder().encode(machineId + "\n")));
979
1022
  })));
980
- yield* fs.link(temporary, target).pipe(Effect5.catch((error) => error.reason._tag === "AlreadyExists" ? Effect5.void : Effect5.fail(error)));
1023
+ yield* fs.link(temporary, target).pipe(Effect6.catch((error) => error.reason._tag === "AlreadyExists" ? Effect6.void : Effect6.fail(error)));
981
1024
  return yield* readIdentity;
982
- })).pipe(Effect5.map((identity) => ({ ...config, machineId: identity, stateDirectory })));
983
- }).pipe(sanitizeFileFinalizers("Resolve private native installation identity"), Effect5.mapError(platformFailure("Resolve private native installation identity")));
984
- var withProcessResources = (inspection) => Effect5.gen(function* () {
1025
+ })).pipe(Effect6.map((identity) => ({ ...config, machineId: identity, stateDirectory })));
1026
+ }).pipe(sanitizeFileFinalizers("Resolve private native installation identity"), Effect6.mapError(platformFailure("Resolve private native installation identity")));
1027
+ var withProcessResources = (inspection) => Effect6.gen(function* () {
985
1028
  const roots = inspection.presence.processes ?? [];
986
1029
  if (roots.length === 0) {
987
1030
  if (new TextEncoder().encode(JSON.stringify(inspection)).length > INSPECTION_LIMIT - 2 * 1024) return yield* new NativeError({ operation: "Native inspection exceeds connector budget" });
@@ -1038,14 +1081,14 @@ var withProcessResources = (inspection) => Effect5.gen(function* () {
1038
1081
  if (new TextEncoder().encode(JSON.stringify(result)).length > INSPECTION_LIMIT - 2 * 1024) return yield* new NativeError({ operation: "Native inspection exceeds connector budget" });
1039
1082
  return result;
1040
1083
  });
1041
- var connectNative = (config, callbacks) => Effect5.gen(function* () {
1084
+ var connectNative = (config, callbacks) => Effect6.gen(function* () {
1042
1085
  const mcp = yield* Mcp;
1043
1086
  const clock = yield* Clock2.Clock;
1044
1087
  const now = () => Number(clock.monotonicTimeNanosUnsafe() / 1000000n);
1045
1088
  let token = yield* readConnectorToken(config);
1046
1089
  const crypto = yield* Crypto.Crypto;
1047
1090
  const identity = { connectorId: yield* crypto.randomUUIDv4(), incarnation: yield* crypto.randomUUIDv4() };
1048
- const report = callbacks.report ?? ((notice) => Effect5.logDebug({ message: "Native inspector", state: notice.state, operation: notice.operation, code: notice.code, status: notice.status }));
1091
+ const report = callbacks.report ?? ((notice) => Effect6.logDebug({ message: "Native inspector", state: notice.state, operation: notice.operation, code: notice.code, status: notice.status }));
1049
1092
  let isUnavailable = false;
1050
1093
  let hasReportedHealthy = false;
1051
1094
  let failedAt;
@@ -1056,14 +1099,14 @@ var connectNative = (config, callbacks) => Effect5.gen(function* () {
1056
1099
  code: error._tag,
1057
1100
  status: error.status
1058
1101
  });
1059
- const unavailable2 = (operation, error) => Effect5.gen(function* () {
1102
+ const unavailable2 = (operation, error) => Effect6.gen(function* () {
1060
1103
  healthyAt = void 0;
1061
1104
  failedAt ??= now();
1062
1105
  if (isUnavailable || now() - failedAt < 5e3) return;
1063
1106
  isUnavailable = true;
1064
1107
  yield* report(failure("unavailable", operation, error));
1065
1108
  });
1066
- const recovered = Effect5.gen(function* () {
1109
+ const recovered = Effect6.gen(function* () {
1067
1110
  healthyAt ??= now();
1068
1111
  if (now() - healthyAt < 1e4) return false;
1069
1112
  const resetBackoff = failedAt !== void 0;
@@ -1075,37 +1118,37 @@ var connectNative = (config, callbacks) => Effect5.gen(function* () {
1075
1118
  if (shouldReport) yield* report({ state: "recovered", operation: "connection" });
1076
1119
  return resetBackoff;
1077
1120
  });
1078
- const post = (route, input, output, value) => mcp.post(config.inspectorUrl, token, route, input, output, value).pipe(Effect5.tapError((error) => unavailable2(route, error)));
1079
- yield* Effect5.addFinalizer(() => mcp.post(config.inspectorUrl, token, "/connector/disconnect", ConnectorDisconnect, ConnectorAccepted, identity, 500).pipe(
1080
- Effect5.catch((error) => Effect5.logDebug({ operation: "/connector/disconnect", code: error._tag, status: error._tag === "HttpError" ? error.status : void 0 }))
1121
+ const post = (route, input, output, value) => mcp.post(config.inspectorUrl, token, route, input, output, value).pipe(Effect6.tapError((error) => unavailable2(route, error)));
1122
+ yield* Effect6.addFinalizer(() => mcp.post(config.inspectorUrl, token, "/connector/disconnect", ConnectorDisconnect, ConnectorAccepted, identity, 500).pipe(
1123
+ Effect6.catch((error) => Effect6.logDebug({ operation: "/connector/disconnect", code: error._tag, status: error._tag === "HttpError" ? error.status : void 0 }))
1081
1124
  ));
1082
- const announce = Effect5.gen(function* () {
1125
+ const announce = Effect6.gen(function* () {
1083
1126
  const sessions = yield* callbacks.presence();
1084
1127
  if (sessions.length > 256) return yield* new NativeError({ operation: "Native presence exceeds bound" });
1085
1128
  yield* post("/connector/announce", ConnectorAnnounce, ConnectorAccepted, { ...identity, machineId: config.machineId, sessions });
1086
- }).pipe(Effect5.tapError((error) => unavailable2("native presence", error)));
1087
- const observationFailure = (error) => Effect5.logDebug({ operation: "native detail", code: error._tag, status: error.status }).pipe(
1088
- Effect5.as({
1129
+ }).pipe(Effect6.tapError((error) => unavailable2("native presence", error)));
1130
+ const observationFailure = (error) => Effect6.logDebug({ operation: "native detail", code: error._tag, status: error.status }).pipe(
1131
+ Effect6.as({
1089
1132
  code: error._tag === "TimeoutError" ? "timeout" : error._tag === "HttpError" && error.status === 404 ? "not-found" : "unavailable",
1090
1133
  message: "Native session observation unavailable"
1091
1134
  })
1092
1135
  );
1093
- const cycle = Effect5.gen(function* () {
1136
+ const cycle = Effect6.gen(function* () {
1094
1137
  const pending = yield* post("/connector/poll", ConnectorPoll, ConnectorPollResult, identity);
1095
1138
  for (const request of pending.requests) {
1096
- const observation = yield* callbacks.inspect(request.ref).pipe(Effect5.timeout("8 seconds"), Effect5.catch(observationFailure));
1139
+ const observation = yield* callbacks.inspect(request.ref).pipe(Effect6.timeout("8 seconds"), Effect6.catch(observationFailure));
1097
1140
  yield* post("/connector/result", ConnectorResult, ConnectorAccepted, { ...identity, requestId: request.requestId, observation });
1098
1141
  }
1099
1142
  if (yield* recovered) return true;
1100
- yield* Effect5.sleep("1 second");
1143
+ yield* Effect6.sleep("1 second");
1101
1144
  return false;
1102
1145
  });
1103
- const retryable = (error) => Effect5.gen(function* () {
1146
+ const retryable = (error) => Effect6.gen(function* () {
1104
1147
  if (error._tag === "TimeoutError") return true;
1105
1148
  if (error._tag !== "HttpError") return false;
1106
1149
  if (error.status === 401 || error.status === 403) {
1107
1150
  const next = yield* readConnectorToken(config).pipe(
1108
- Effect5.catch((failure2) => unavailable2("credential reload", failure2).pipe(Effect5.as(void 0)))
1151
+ Effect6.catch((failure2) => unavailable2("credential reload", failure2).pipe(Effect6.as(void 0)))
1109
1152
  );
1110
1153
  if (next === void 0 || next === token) return false;
1111
1154
  token = next;
@@ -1116,15 +1159,15 @@ var connectNative = (config, callbacks) => Effect5.gen(function* () {
1116
1159
  const maximumDelay = Duration.seconds(30);
1117
1160
  const backoff = Schedule.min([Schedule.exponential("2 seconds"), Schedule.spaced(maximumDelay)]).pipe(
1118
1161
  Schedule.jittered,
1119
- Schedule.modifyDelay(({ duration }) => Effect5.succeed(Duration.min(duration, maximumDelay)))
1162
+ Schedule.modifyDelay(({ duration }) => Effect6.succeed(Duration.min(duration, maximumDelay)))
1120
1163
  );
1121
- return yield* Effect5.forever(Effect5.gen(function* () {
1164
+ return yield* Effect6.forever(Effect6.gen(function* () {
1122
1165
  yield* announce;
1123
- yield* Effect5.raceFirst(
1124
- Effect5.forever(Effect5.sleep("10 seconds").pipe(Effect5.andThen(announce))),
1125
- cycle.pipe(Effect5.repeat({ until: (resetBackoff) => resetBackoff }))
1166
+ yield* Effect6.raceFirst(
1167
+ Effect6.forever(Effect6.sleep("10 seconds").pipe(Effect6.andThen(announce))),
1168
+ cycle.pipe(Effect6.repeat({ until: (resetBackoff) => resetBackoff }))
1126
1169
  );
1127
- }).pipe(Effect5.retry({ while: retryable, schedule: backoff })));
1170
+ }).pipe(Effect6.retry({ while: retryable, schedule: backoff })));
1128
1171
  });
1129
1172
  var Id2 = Schema4.String.check(Schema4.isMinLength(1), Schema4.isMaxLength(256));
1130
1173
  var N = Schema4.Finite.check(Schema4.isGreaterThanOrEqualTo(0));
@@ -1148,6 +1191,8 @@ var Cache = Schema4.Struct({ version: Schema4.Literal(2), kind: Schema4.Literal(
1148
1191
  var ClaudeRoster = Schema4.Array(Schema4.Struct({ sessionId: Schema4.optional(Id2), id: Schema4.optional(Id2), pid: Schema4.optional(Schema4.Int), cwd: Schema4.optional(Schema4.String), kind: Schema4.optional(Id2), status: Schema4.optional(Schema4.Literals(["busy", "waiting", "idle"])), waitingFor: Schema4.optional(Schema4.String) })).check(Schema4.isMaxLength(256));
1149
1192
 
1150
1193
  // src/plugin.ts
1194
+ import { highlightCode } from "@earendil-works/pi-coding-agent";
1195
+ import { Text } from "@earendil-works/pi-tui";
1151
1196
  var provenance = (source, scope, scopeDescription, observedAt, nativeUpdatedAt = null) => ({
1152
1197
  source,
1153
1198
  scope,
@@ -1160,11 +1205,13 @@ var provenance = (source, scope, scopeDescription, observedAt, nativeUpdatedAt =
1160
1205
  partial: false,
1161
1206
  stale: false
1162
1207
  });
1163
- var createNativeObserver = (harness, machineId, processes = []) => Effect6.gen(function* () {
1208
+ var createNativeObserver = (harness, machineId, processes = []) => Effect7.gen(function* () {
1164
1209
  const host = yield* Host;
1165
1210
  const clock = yield* Clock3.Clock;
1166
- const rexSession = yield* Config3.String("REX_SESSION").pipe(Config3.withDefault(""));
1167
- const rexBlock = yield* Config3.String("REX_BLOCK").pipe(Config3.withDefault(""));
1211
+ const rex = yield* readRexContext;
1212
+ let programStatus;
1213
+ let userPrompt;
1214
+ let stopReason;
1168
1215
  let current;
1169
1216
  let identity;
1170
1217
  let lastActivityAt;
@@ -1176,6 +1223,9 @@ var createNativeObserver = (harness, machineId, processes = []) => Effect6.gen(f
1176
1223
  const key = JSON.stringify([ctx.sessionManager.getSessionId(), harness === "omp" ? ctx.agent?.id : void 0]);
1177
1224
  if (identity !== key) {
1178
1225
  latest = void 0;
1226
+ programStatus = void 0;
1227
+ userPrompt = void 0;
1228
+ stopReason = void 0;
1179
1229
  activeTools.clear();
1180
1230
  identity = key;
1181
1231
  }
@@ -1184,6 +1234,24 @@ var createNativeObserver = (harness, machineId, processes = []) => Effect6.gen(f
1184
1234
  if (event?.type === "tool_execution_start" && event.toolCallId && event.toolName) activeTools.set(event.toolCallId, event.toolName);
1185
1235
  if (event?.type === "tool_execution_end" && event.toolCallId) activeTools.delete(event.toolCallId);
1186
1236
  if (event?.type === "agent_end") activeTools.clear();
1237
+ if (event?.type === "agent_start") {
1238
+ programStatus = { state: "working" };
1239
+ stopReason = void 0;
1240
+ }
1241
+ if (event?.type === "turn_start") {
1242
+ programStatus = { state: "working" };
1243
+ stopReason = void 0;
1244
+ }
1245
+ if (event?.type === "tool_execution_start") programStatus = { state: "working" };
1246
+ if (event?.message?.role === "assistant" && event.message.stopReason) stopReason = event.message.stopReason;
1247
+ if (event?.type === "agent_end") stopReason = event.messages?.findLast((message2) => message2.role === "assistant")?.stopReason ?? stopReason;
1248
+ if (harness === "pi" && event?.type === "agent_before_settle") stopReason = event.outcome === "completed" ? "stop" : event.outcome;
1249
+ if (event?.type === "agent_settled" || harness !== "pi" && event?.type === "agent_end") {
1250
+ programStatus = event.willContinue ? { state: "working" } : stopReason === "aborted" ? { state: "idle" } : stopReason === "error" ? { state: "error" } : stopReason === "stop" || stopReason === "length" ? { state: "done" } : void 0;
1251
+ } else if (event?.type === "agent_end") programStatus = { state: "working" };
1252
+ if (harness === "pi" && event?.type === "ui_prompt_start") userPrompt = { state: "blocked", kind: "question" };
1253
+ if (harness === "pi" && event?.type === "ui_prompt_end") userPrompt = void 0;
1254
+ if ((event === void 0 || event.type === "session_start" || event.type === "session_switch") && programStatus === void 0) programStatus = { state: ctx.isIdle() ? "idle" : "working" };
1187
1255
  const message = event?.message;
1188
1256
  if (message?.role === "assistant" && message.usage) {
1189
1257
  const u = message.usage;
@@ -1203,6 +1271,7 @@ var createNativeObserver = (harness, machineId, processes = []) => Effect6.gen(f
1203
1271
  const presence = (now = clock.currentTimeMillisUnsafe()) => {
1204
1272
  if (!current) return void 0;
1205
1273
  const currentToolName = activeTools.values().next().value;
1274
+ const status = userPrompt ?? programStatus;
1206
1275
  return {
1207
1276
  ref: { machineId, harness, nativeSessionId: current.sessionManager.getSessionId(), ...harness === "omp" && current.agent?.id ? { nativeAgentId: current.agent.id } : {} },
1208
1277
  observedAt: now,
@@ -1211,13 +1280,15 @@ var createNativeObserver = (harness, machineId, processes = []) => Effect6.gen(f
1211
1280
  processes,
1212
1281
  machineName,
1213
1282
  ...os ? { os } : {},
1214
- ...rexSession || rexBlock ? { rex: { ...rexSession ? { sessionId: rexSession } : {}, ...rexBlock ? { blockId: rexBlock } : {} } } : {},
1283
+ ...rex ? { rex } : {},
1284
+ ...status ? { programStatus: status } : {},
1215
1285
  ...current.model ? { provider: current.model.provider, model: current.model.id } : {},
1216
1286
  ...current.agent ? { kind: current.agent.kind, ...current.agent.name ? { agent: current.agent.name } : {} } : {},
1217
1287
  activity: {
1218
- state: current.isIdle() ? "idle" : "working",
1288
+ state: userPrompt ? "waiting" : current.isIdle() ? "idle" : "working",
1219
1289
  streaming: !current.isIdle(),
1220
- ...currentToolName === void 0 ? {} : { currentToolName, wait: "tool" }
1290
+ ...currentToolName === void 0 ? {} : { currentToolName },
1291
+ ...userPrompt ? { wait: "user" } : currentToolName === void 0 ? {} : { wait: "tool" }
1221
1292
  }
1222
1293
  };
1223
1294
  };
@@ -1321,6 +1392,9 @@ var createNativeObserver = (harness, machineId, processes = []) => Effect6.gen(f
1321
1392
  current = void 0;
1322
1393
  identity = void 0;
1323
1394
  latest = void 0;
1395
+ programStatus = void 0;
1396
+ userPrompt = void 0;
1397
+ stopReason = void 0;
1324
1398
  lastActivityAt = void 0;
1325
1399
  activeTools.clear();
1326
1400
  } };
@@ -1337,9 +1411,9 @@ Use the available session.inspect MCP tool for on-demand details relevant to thi
1337
1411
  </session-inspection>`;
1338
1412
  return harness === "pi" ? { action: "transform", text, ...input.images ? { images: input.images } : {} } : { text, ...input.images ? { images: input.images } : {} };
1339
1413
  };
1340
- var registerCoordinateKeyword = Effect6.gen(function* () {
1341
- const { MAGIC_KEYWORDS } = yield* Effect6.tryPromise({ try: () => import("@oh-my-pi/pi-coding-agent/modes/magic-keywords"), catch: () => new NativeError({ operation: "Load native keyword definitions" }) });
1342
- const { isMagicKeyword, setMagicKeywords } = yield* Effect6.tryPromise({ try: () => import("@oh-my-pi/pi-tui/prompt/magic-keywords"), catch: () => new NativeError({ operation: "Load native keyword highlighter" }) });
1414
+ var registerCoordinateKeyword = Effect7.gen(function* () {
1415
+ const { MAGIC_KEYWORDS } = yield* Effect7.tryPromise({ try: () => import("@oh-my-pi/pi-coding-agent/modes/magic-keywords"), catch: () => new NativeError({ operation: "Load native keyword definitions" }) });
1416
+ const { isMagicKeyword, setMagicKeywords } = yield* Effect7.tryPromise({ try: () => import("@oh-my-pi/pi-tui/prompt/magic-keywords"), catch: () => new NativeError({ operation: "Load native keyword highlighter" }) });
1343
1417
  setMagicKeywords([...MAGIC_KEYWORDS.filter(({ word }) => isMagicKeyword(word)), { word: "coordinate", hue: [190, 260] }]);
1344
1418
  });
1345
1419
  function plugin(host) {
@@ -1349,6 +1423,53 @@ function plugin(host) {
1349
1423
  let observer;
1350
1424
  let starting;
1351
1425
  let stopped = false;
1426
+ const rexReporter = harness === "pi" ? createRexReporter((sequence) => process.stdout.write(sequence)) : void 0;
1427
+ let terminal = false;
1428
+ host.registerMessageRenderer("coordinator-list", (message) => {
1429
+ if (typeof message.content !== "string") return void 0;
1430
+ return new Text(message.details === void 0 ? message.content : highlightCode(message.content, "json").join("\n"), 0, 0);
1431
+ });
1432
+ host.registerCommand("coordinator", {
1433
+ description: "List visible sessions, their machines, and native metadata",
1434
+ handler: async (args) => {
1435
+ if (args.trim() !== "list") {
1436
+ host.sendMessage({ customType: "coordinator-list", content: "Usage: /coordinator list", display: true }, { triggerTurn: false });
1437
+ return;
1438
+ }
1439
+ const exit = await runtime.runPromiseExit(Effect7.gen(function* () {
1440
+ const activeConfig = config ?? (yield* loadConnectorConfig());
1441
+ if (!activeConfig) return yield* new NativeError({ operation: "Connector configuration required" });
1442
+ const token = yield* readConnectorToken(activeConfig);
1443
+ const mcp = yield* Mcp;
1444
+ const sessions = [];
1445
+ let cursor;
1446
+ do {
1447
+ const page = yield* mcp.call(activeConfig.inspectorUrl, token, InspectionTools.list, { limit: 100, ...cursor === void 0 ? {} : { cursor } });
1448
+ for (const session of page.sessions) sessions.push(session);
1449
+ cursor = page.nextCursor ?? void 0;
1450
+ } while (cursor !== void 0);
1451
+ return sessions;
1452
+ }));
1453
+ if (stopped) return;
1454
+ if (exit._tag === "Failure") {
1455
+ const error = Cause3.findErrorOption(exit.cause);
1456
+ const reason = error._tag === "Some" ? error.value._tag === "HttpError" ? `HTTP ${error.value.status}` : error.value._tag : "unexpected failure";
1457
+ host.sendMessage({
1458
+ customType: "coordinator-list",
1459
+ content: `Coordinator list unavailable (${reason}). Check inspectorUrl, your access token, and API reachability.`,
1460
+ display: true
1461
+ }, { triggerTurn: false });
1462
+ return;
1463
+ }
1464
+ const result = { sessions: exit.value };
1465
+ host.sendMessage({
1466
+ customType: "coordinator-list",
1467
+ content: Formatter2.formatJson(result, { space: 2 }),
1468
+ display: true,
1469
+ details: result
1470
+ }, { triggerTurn: false });
1471
+ }
1472
+ });
1352
1473
  if (harness === "pi") {
1353
1474
  const register = (descriptor) => {
1354
1475
  const document = Schema5.toJsonSchemaDocument(Schema5.toCodecJson(descriptor.input), { onExcessProperty: "error" });
@@ -1357,7 +1478,7 @@ function plugin(host) {
1357
1478
  label: `Coordinator ${descriptor.name}`,
1358
1479
  description: descriptor.description,
1359
1480
  parameters: { ...document.schema, type: "object", $defs: document.definitions },
1360
- execute: (_id, params, signal) => runtime.runPromise(Effect6.gen(function* () {
1481
+ execute: (_id, params, signal) => runtime.runPromise(Effect7.gen(function* () {
1361
1482
  const activeConfig = config ?? (yield* loadConnectorConfig());
1362
1483
  if (!activeConfig) return yield* new NativeError({ operation: "Connector configuration required" });
1363
1484
  const input = yield* Schema5.decodeUnknownEffect(Schema5.toCodecJson(descriptor.input), { onExcessProperty: "error" })(params);
@@ -1372,21 +1493,21 @@ function plugin(host) {
1372
1493
  register(InspectionTools.inspect);
1373
1494
  }
1374
1495
  let lastError;
1375
- const report = (ctx) => (notice) => Effect6.gen(function* () {
1496
+ const report = (ctx) => (notice) => Effect7.gen(function* () {
1376
1497
  if (stopped) return;
1377
1498
  if (notice.state !== "error") {
1378
1499
  if (notice.state === "recovered") lastError = void 0;
1379
- yield* Effect6.logDebug("Native inspector connection", { state: notice.state, operation: notice.operation, code: notice.code, status: notice.status });
1500
+ yield* Effect7.logDebug("Native inspector connection", { state: notice.state, operation: notice.operation, code: notice.code, status: notice.status });
1380
1501
  return;
1381
1502
  }
1382
1503
  if (lastError?.operation === notice.operation && lastError.code === notice.code && lastError.status === notice.status) return;
1383
1504
  lastError = notice;
1384
1505
  const message = `Native inspector unavailable (operation=${notice.operation}${notice.code === void 0 ? "" : `, code=${notice.code}`}${notice.status === void 0 ? "" : `, status=${notice.status}`})`;
1385
- if (ctx.hasUI) yield* Effect6.sync(() => ctx.ui.notify(message, "warning"));
1506
+ if (ctx.hasUI) yield* Effect7.sync(() => ctx.ui.notify(message, "warning"));
1386
1507
  else yield* Console2.error(message);
1387
1508
  });
1388
1509
  const reportCause = (ctx, operation) => (cause) => {
1389
- if (Cause3.hasInterruptsOnly(cause)) return Effect6.void;
1510
+ if (Cause3.hasInterruptsOnly(cause)) return Effect7.void;
1390
1511
  const error = Cause3.findErrorOption(cause);
1391
1512
  return report(ctx)({
1392
1513
  state: "error",
@@ -1395,7 +1516,7 @@ function plugin(host) {
1395
1516
  status: error._tag === "Some" ? error.value.status : void 0
1396
1517
  });
1397
1518
  };
1398
- const start = (ctx) => starting ??= runtime.runPromiseExit(Effect6.gen(function* () {
1519
+ const start = (ctx) => starting ??= runtime.runPromiseExit(Effect7.gen(function* () {
1399
1520
  config = yield* loadConnectorConfig();
1400
1521
  if (!config || stopped) return false;
1401
1522
  const nativeConfig = yield* resolveNativeConfig(config);
@@ -1403,13 +1524,15 @@ function plugin(host) {
1403
1524
  const processIdentity = yield* identifyProcess(host2.pid);
1404
1525
  observer = yield* createNativeObserver(harness, nativeConfig.machineId, processIdentity.status === "available" ? [{ pid: host2.pid, startIdentity: processIdentity.startTime, ownership: harness === "omp" ? "shared" : "owned" }] : []);
1405
1526
  observer.update(ctx);
1527
+ terminal = ctx.mode === "tui" && ctx.hasUI && process.stdout.isTTY === true;
1528
+ rexReporter?.update(observer.presence()?.programStatus, terminal);
1406
1529
  if (harness === "omp" && ctx.hasUI && ctx.mode === "tui") yield* registerCoordinateKeyword;
1407
1530
  runtime.runFork(connectNative(nativeConfig, {
1408
- presence: () => Effect6.sync(() => {
1531
+ presence: () => Effect7.sync(() => {
1409
1532
  const p = observer?.presence();
1410
1533
  return p ? [p] : [];
1411
1534
  }),
1412
- inspect: (ref) => Effect6.gen(function* () {
1535
+ inspect: (ref) => Effect7.gen(function* () {
1413
1536
  const observation = observer?.inspect();
1414
1537
  if (!observation || sessionRefKey(ref) !== sessionRefKey(observation.presence.ref)) {
1415
1538
  return yield* new HttpError({ status: 404, message: "Native session unavailable" });
@@ -1418,14 +1541,14 @@ function plugin(host) {
1418
1541
  }),
1419
1542
  report: report(ctx)
1420
1543
  }).pipe(
1421
- Effect6.scoped,
1422
- Effect6.tapCause(reportCause(ctx, "connector")),
1423
- Effect6.onExit(() => Effect6.sync(() => {
1544
+ Effect7.scoped,
1545
+ Effect7.tapCause(reportCause(ctx, "connector")),
1546
+ Effect7.onExit(() => Effect7.sync(() => {
1424
1547
  starting = void 0;
1425
1548
  }))
1426
1549
  ));
1427
1550
  return true;
1428
- }).pipe(Effect6.tapCause(reportCause(ctx, "startup")))).then((exit) => {
1551
+ }).pipe(Effect7.tapCause(reportCause(ctx, "startup")))).then((exit) => {
1429
1552
  if (exit._tag === "Failure" || !exit.value) {
1430
1553
  starting = void 0;
1431
1554
  observer?.clear();
@@ -1434,16 +1557,23 @@ function plugin(host) {
1434
1557
  }
1435
1558
  });
1436
1559
  const events = ["session_start", "session_switch", "agent_start", "agent_end", "turn_start", "turn_end", "message_end", "tool_execution_start", "tool_execution_end"];
1560
+ if (harness === "pi") events.push("agent_before_settle", "agent_settled", "ui_prompt_start", "ui_prompt_end");
1437
1561
  for (const name of events) {
1438
1562
  host.on(name, async (event, ctx) => {
1439
1563
  if (stopped) return;
1440
1564
  await start(ctx);
1441
1565
  if (stopped || !observer) return;
1442
- await runtime.runPromiseExit(Effect6.sync(() => observer?.update(ctx, event)).pipe(Effect6.tapCause(reportCause(ctx, "native update"))));
1566
+ await runtime.runPromiseExit(Effect7.sync(() => {
1567
+ if (name === "session_switch") rexReporter?.update(void 0, terminal);
1568
+ observer?.update(ctx, event);
1569
+ terminal = ctx.mode === "tui" && ctx.hasUI && process.stdout.isTTY === true;
1570
+ rexReporter?.update(observer?.presence()?.programStatus, terminal);
1571
+ }).pipe(Effect7.tapCause(reportCause(ctx, "native update"))));
1443
1572
  });
1444
1573
  }
1445
1574
  host.on("session_shutdown", async () => {
1446
1575
  stopped = true;
1576
+ rexReporter?.close(terminal);
1447
1577
  try {
1448
1578
  await runtime.dispose();
1449
1579
  } finally {
@@ -1454,14 +1584,14 @@ function plugin(host) {
1454
1584
  });
1455
1585
  host.on("input", async (event, ctx) => {
1456
1586
  if (harness !== "omp" || !isCoordinateInput(event.text) || stopped) return;
1457
- const exit = await runtime.runPromiseExit(Effect6.gen(function* () {
1587
+ const exit = await runtime.runPromiseExit(Effect7.gen(function* () {
1458
1588
  const activeConfig = config ?? (yield* loadConnectorConfig());
1459
1589
  if (!activeConfig) return;
1460
1590
  const token = yield* readConnectorToken(activeConfig);
1461
1591
  const mcp = yield* Mcp;
1462
1592
  const snapshot = yield* mcp.call(activeConfig.inspectorUrl, token, InspectionTools.list, { limit: 100 });
1463
1593
  return transformCoordinateInput(harness, event, snapshot);
1464
- }).pipe(Effect6.tapCause(reportCause(ctx, "coordinate"))));
1594
+ }).pipe(Effect7.tapCause(reportCause(ctx, "coordinate"))));
1465
1595
  return exit._tag === "Success" ? exit.value : void 0;
1466
1596
  });
1467
1597
  }