@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.
- package/README.md +51 -47
- package/THIRD_PARTY_NOTICES.md +3 -1
- package/dist/plugins/omp.js +136 -7
- package/dist/plugins/pi.js +207 -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 +107 -8
- package/dist/src/rex.js +36 -0
- package/dist/src/setup.js +134 -0
- package/package.json +2 -1
package/dist/plugins/pi.js
CHANGED
|
@@ -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,
|
|
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({
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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(
|
|
919
|
-
var loadConnectorConfig = (file) =>
|
|
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*
|
|
924
|
-
const xdg = yield*
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
-
|
|
980
|
+
Effect6.mapError(platformFailure("Invalid connector configuration"))
|
|
938
981
|
);
|
|
939
|
-
const url = yield*
|
|
940
|
-
const token = yield*
|
|
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(
|
|
990
|
+
}).pipe(Effect6.mapError(platformFailure("Invalid connector configuration")));
|
|
948
991
|
});
|
|
949
|
-
var readConnectorToken = (config) =>
|
|
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) =>
|
|
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*
|
|
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(
|
|
969
|
-
const existing = yield* readIdentity.pipe(
|
|
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*
|
|
974
|
-
const cleanupScope = yield*
|
|
975
|
-
yield*
|
|
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(
|
|
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(
|
|
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(
|
|
983
|
-
}).pipe(sanitizeFileFinalizers("Resolve private native installation identity"),
|
|
984
|
-
var withProcessResources = (inspection) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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 =
|
|
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(
|
|
1079
|
-
yield*
|
|
1080
|
-
|
|
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 =
|
|
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(
|
|
1087
|
-
const observationFailure = (error) =>
|
|
1088
|
-
|
|
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 =
|
|
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(
|
|
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*
|
|
1143
|
+
yield* Effect6.sleep("1 second");
|
|
1101
1144
|
return false;
|
|
1102
1145
|
});
|
|
1103
|
-
const retryable = (error) =>
|
|
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
|
-
|
|
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 }) =>
|
|
1162
|
+
Schedule.modifyDelay(({ duration }) => Effect6.succeed(Duration.min(duration, maximumDelay)))
|
|
1120
1163
|
);
|
|
1121
|
-
return yield*
|
|
1164
|
+
return yield* Effect6.forever(Effect6.gen(function* () {
|
|
1122
1165
|
yield* announce;
|
|
1123
|
-
yield*
|
|
1124
|
-
|
|
1125
|
-
cycle.pipe(
|
|
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(
|
|
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 = []) =>
|
|
1208
|
+
var createNativeObserver = (harness, machineId, processes = []) => Effect7.gen(function* () {
|
|
1164
1209
|
const host = yield* Host;
|
|
1165
1210
|
const clock = yield* Clock3.Clock;
|
|
1166
|
-
const
|
|
1167
|
-
|
|
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
|
-
...
|
|
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
|
|
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 =
|
|
1341
|
-
const { MAGIC_KEYWORDS } = yield*
|
|
1342
|
-
const { isMagicKeyword, setMagicKeywords } = yield*
|
|
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(
|
|
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) =>
|
|
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*
|
|
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*
|
|
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
|
|
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(
|
|
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: () =>
|
|
1531
|
+
presence: () => Effect7.sync(() => {
|
|
1409
1532
|
const p = observer?.presence();
|
|
1410
1533
|
return p ? [p] : [];
|
|
1411
1534
|
}),
|
|
1412
|
-
inspect: (ref) =>
|
|
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
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1594
|
+
}).pipe(Effect7.tapCause(reportCause(ctx, "coordinate"))));
|
|
1465
1595
|
return exit._tag === "Success" ? exit.value : void 0;
|
|
1466
1596
|
});
|
|
1467
1597
|
}
|