@p4code/cli 0.6.13 → 0.6.15
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/dist/bin.mjs +601 -381
- package/dist/client/assets/{DiffPanel-BmpVDbgX.js → DiffPanel-B2uEx-Ip.js} +2 -2
- package/dist/client/assets/{FilePreviewPanel-DM1DhmQP.js → FilePreviewPanel-Xd7rNhEc.js} +2 -2
- package/dist/client/assets/{PreviewPanel-DWA8uuBY.js → PreviewPanel-UqzJdhZ2.js} +2 -2
- package/dist/client/assets/{PullRequestCodeTab-BUY2RwAt.js → PullRequestCodeTab-DCdVX6w6.js} +2 -2
- package/dist/client/assets/{fileCommentAnnotations-DyExLixc.js → fileCommentAnnotations-3__-nxMy.js} +2 -2
- package/dist/client/assets/{index-Be6_pUuK.js → index-UKht9nt4.js} +19 -6
- package/dist/client/assets/{previewAssetResource-DEZquWCb.js → previewAssetResource-OVkEskN2.js} +2 -2
- package/dist/client/assets/{renderFileChildren-DfZ1aiKz.js → renderFileChildren-4kqXMwk5.js} +2 -2
- package/dist/client/assets/{toggle-group-DAfIzx1j.js → toggle-group-BgpxQt7v.js} +2 -2
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import * as Effect from "effect/Effect";
|
|
|
8
8
|
import * as Layer from "effect/Layer";
|
|
9
9
|
import { Argument, Command, Flag, GlobalFlag, Prompt } from "effect/unstable/cli";
|
|
10
10
|
import * as CliError from "effect/unstable/cli/CliError";
|
|
11
|
-
import { FetchHttpClient,
|
|
11
|
+
import { FetchHttpClient, HttpBody, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, HttpEffect, HttpRouter, HttpServer, HttpServerRequest, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http";
|
|
12
12
|
import * as Context from "effect/Context";
|
|
13
13
|
import * as NodeOS from "node:os";
|
|
14
14
|
import * as NodeNet from "node:net";
|
|
@@ -63,6 +63,8 @@ import * as NodeReadline from "node:readline";
|
|
|
63
63
|
import * as NodeFSP from "node:fs/promises";
|
|
64
64
|
import { access } from "node:fs/promises";
|
|
65
65
|
import * as Clock from "effect/Clock";
|
|
66
|
+
import * as Headers$1 from "effect/unstable/http/Headers";
|
|
67
|
+
import * as HttpRouter$1 from "effect/unstable/http/HttpRouter";
|
|
66
68
|
import * as NodeStream from "@effect/platform-node/NodeStream";
|
|
67
69
|
import * as NodeHttp from "node:http";
|
|
68
70
|
import * as Deferred from "effect/Deferred";
|
|
@@ -266,7 +268,7 @@ const make$124 = () => {
|
|
|
266
268
|
const layer$124 = Layer.sync(NetService, make$124);
|
|
267
269
|
//#endregion
|
|
268
270
|
//#region package.json
|
|
269
|
-
var version$1 = "0.6.
|
|
271
|
+
var version$1 = "0.6.15";
|
|
270
272
|
//#endregion
|
|
271
273
|
//#region src/config.ts
|
|
272
274
|
/**
|
|
@@ -10163,7 +10165,8 @@ var DeviceOperationError = class extends Schema$1.TaggedErrorClass()("DeviceOper
|
|
|
10163
10165
|
"request_failed",
|
|
10164
10166
|
"invalid_payload",
|
|
10165
10167
|
"settings_failed",
|
|
10166
|
-
"hub_rejected"
|
|
10168
|
+
"hub_rejected",
|
|
10169
|
+
"agent_access_disabled"
|
|
10167
10170
|
]),
|
|
10168
10171
|
exitCode: Schema$1.optional(Schema$1.Number),
|
|
10169
10172
|
cause: Schema$1.Defect()
|
|
@@ -10174,7 +10177,8 @@ var DeviceOperationError = class extends Schema$1.TaggedErrorClass()("DeviceOper
|
|
|
10174
10177
|
request_failed: "Could not communicate with device support. Try refreshing devices.",
|
|
10175
10178
|
invalid_payload: "The device request could not be encoded.",
|
|
10176
10179
|
settings_failed: "Could not read or save device settings.",
|
|
10177
|
-
hub_rejected: "The device hub could not complete the request."
|
|
10180
|
+
hub_rejected: "The device hub could not complete the request.",
|
|
10181
|
+
agent_access_disabled: "Agent device access is off. Turn on Settings > Devices > Allow agents to control devices."
|
|
10178
10182
|
}[this.reason];
|
|
10179
10183
|
return `Device ${this.operation} failed: ${explanation}`;
|
|
10180
10184
|
}
|
|
@@ -24244,6 +24248,49 @@ function isLoopbackHost(host) {
|
|
|
24244
24248
|
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]" || normalized.startsWith("127.");
|
|
24245
24249
|
}
|
|
24246
24250
|
//#endregion
|
|
24251
|
+
//#region src/httpCompression/HttpResponseCompression.ts
|
|
24252
|
+
const GZIP_MIN_BYTES = 1024;
|
|
24253
|
+
var HttpResponseCompression = class extends Context.Service()("@p4code/cli/httpCompression/HttpResponseCompression") {};
|
|
24254
|
+
const layerNode = Layer.effect(HttpResponseCompression, Effect.gen(function* () {
|
|
24255
|
+
const NodeZlib = yield* Effect.promise(() => import("node:zlib"));
|
|
24256
|
+
return { gzip: (body, options) => {
|
|
24257
|
+
const stream = NodeZlib.createGzip();
|
|
24258
|
+
stream.end(body);
|
|
24259
|
+
return HttpServerResponse$1.raw(stream, options);
|
|
24260
|
+
} };
|
|
24261
|
+
}));
|
|
24262
|
+
const layerBun = Layer.succeed(HttpResponseCompression, { gzip: (body, options) => HttpServerResponse$1.raw(new Response(body).body.pipeThrough(new CompressionStream("gzip")), options) });
|
|
24263
|
+
function acceptsGzip(value) {
|
|
24264
|
+
if (!value) return false;
|
|
24265
|
+
const accepted = new Map(value.split(",").map((entry) => {
|
|
24266
|
+
const [coding = "", ...parameters] = entry.trim().toLowerCase().split(";");
|
|
24267
|
+
const quality = parameters.map((parameter) => parameter.trim().match(/^q=(.+)$/)?.[1]).find((parameter) => parameter !== void 0);
|
|
24268
|
+
return [coding, quality === void 0 ? 1 : Number(quality)];
|
|
24269
|
+
}));
|
|
24270
|
+
return (accepted.get("gzip") ?? accepted.get("*") ?? 0) > 0;
|
|
24271
|
+
}
|
|
24272
|
+
function varyByAcceptEncoding(value) {
|
|
24273
|
+
if (!value) return "Accept-Encoding";
|
|
24274
|
+
const values = new Set(value.split(",").map((entry) => entry.trim().toLowerCase()));
|
|
24275
|
+
return values.has("*") || values.has("accept-encoding") ? value : `${value}, Accept-Encoding`;
|
|
24276
|
+
}
|
|
24277
|
+
const compressHttpResponse = Effect.fnUntraced(function* (response, acceptEncoding) {
|
|
24278
|
+
const body = response.body;
|
|
24279
|
+
if (body._tag !== "Uint8Array" || body.contentLength < GZIP_MIN_BYTES || !body.contentType.startsWith("application/json") || response.headers["content-encoding"]) return response;
|
|
24280
|
+
const variedResponse = HttpServerResponse$1.setHeader(response, "vary", varyByAcceptEncoding(response.headers.vary));
|
|
24281
|
+
if (!acceptsGzip(acceptEncoding)) return variedResponse;
|
|
24282
|
+
const compression = yield* HttpResponseCompression;
|
|
24283
|
+
const headers = Headers$1.set(Headers$1.remove(variedResponse.headers, "content-length"), "content-encoding", "gzip");
|
|
24284
|
+
return compression.gzip(body.body, {
|
|
24285
|
+
status: response.status,
|
|
24286
|
+
statusText: response.statusText,
|
|
24287
|
+
headers,
|
|
24288
|
+
cookies: response.cookies,
|
|
24289
|
+
contentType: body.contentType
|
|
24290
|
+
});
|
|
24291
|
+
});
|
|
24292
|
+
const httpCompressionLayer = HttpRouter$1.middleware((httpEffect) => Effect.flatMap(Effect.all([httpEffect, HttpServerRequest$1.HttpServerRequest]), ([response, request]) => compressHttpResponse(response, request.headers["accept-encoding"])), { global: true });
|
|
24293
|
+
//#endregion
|
|
24247
24294
|
//#region src/persistence/Services/AgentAssets.ts
|
|
24248
24295
|
var AgentAssetRepository = class extends Context.Service()("@p4code/cli/persistence/Services/AgentAssets/AgentAssetRepository") {};
|
|
24249
24296
|
//#endregion
|
|
@@ -28795,6 +28842,321 @@ const hubTunnelClientSocketRoute = Effect.gen(function* () {
|
|
|
28795
28842
|
})));
|
|
28796
28843
|
const layer$96 = HttpRouter.add("GET", `${HUB_TUNNEL_CLIENT_SOCKET_PATH}/*`, hubTunnelClientSocketRoute);
|
|
28797
28844
|
//#endregion
|
|
28845
|
+
//#region src/sync/HubLink.ts
|
|
28846
|
+
/**
|
|
28847
|
+
* Where this server's hub is, what it authenticates with, and whether it syncs
|
|
28848
|
+
* on its own.
|
|
28849
|
+
*
|
|
28850
|
+
* Absent by default. A server with no hub configured runs exactly as it does
|
|
28851
|
+
* today — the sync is skipped rather than failing, because "no hub" is a
|
|
28852
|
+
* complete and normal configuration, not a broken one.
|
|
28853
|
+
*
|
|
28854
|
+
* The record lives in `ServerSecretStore`, which already writes with
|
|
28855
|
+
* restrictive permissions, rather than in the settings file or the database.
|
|
28856
|
+
* The sync mode rides along in that record even though it is not a secret:
|
|
28857
|
+
* connect, disconnect and pause are then one write and one read, and the
|
|
28858
|
+
* settings panel reads one thing instead of joining two. Environment variables
|
|
28859
|
+
* override the link so a container or a service unit can be configured without
|
|
28860
|
+
* a first-run interactive step — the same trade the hub process itself makes
|
|
28861
|
+
* with `P4CODE_HUB_TOKEN_HASHES` — while the mode always comes from the stored
|
|
28862
|
+
* record, so a server configured by environment can still be paused.
|
|
28863
|
+
*
|
|
28864
|
+
* Mutable, deliberately. Resolving this once at layer construction would mean
|
|
28865
|
+
* connecting to a hub did nothing until the server restarted, which is a
|
|
28866
|
+
* miserable thing to discover after typing a token into a form.
|
|
28867
|
+
*
|
|
28868
|
+
* @module sync/HubLink
|
|
28869
|
+
*/
|
|
28870
|
+
/** Secret name holding the hub record as JSON. */
|
|
28871
|
+
const HUB_LINK_SECRET_NAME = "hub-link";
|
|
28872
|
+
var HubLink = class extends Context.Service()("@p4code/cli/sync/HubLink") {};
|
|
28873
|
+
/** The submitted URL or token could not make a usable link. */
|
|
28874
|
+
var InvalidHubLink = class extends Schema$1.TaggedErrorClass()("InvalidHubLink", { detail: Schema$1.String }) {
|
|
28875
|
+
get message() {
|
|
28876
|
+
return this.detail;
|
|
28877
|
+
}
|
|
28878
|
+
};
|
|
28879
|
+
const trimTrailingSlash = (value) => value.replace(/\/+$/u, "");
|
|
28880
|
+
/**
|
|
28881
|
+
* Both halves must be present and the URL parseable. A half-configured link is
|
|
28882
|
+
* treated as no link at all, because a server that thinks it has a hub and
|
|
28883
|
+
* cannot form a request would report every sync as a failure rather than as the
|
|
28884
|
+
* "not configured" it actually is.
|
|
28885
|
+
*/
|
|
28886
|
+
const validateHubLink = (rawBaseUrl, rawToken) => {
|
|
28887
|
+
const trimmed = trimTrailingSlash(rawBaseUrl.trim());
|
|
28888
|
+
const token = rawToken.trim();
|
|
28889
|
+
if (trimmed.length === 0 || token.length === 0) return Option.none();
|
|
28890
|
+
const baseUrl = /^[a-z][a-z0-9+.-]*:\/\//iu.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
28891
|
+
if (!URL.canParse(baseUrl)) return Option.none();
|
|
28892
|
+
const parsed = new URL(baseUrl);
|
|
28893
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return Option.none();
|
|
28894
|
+
return Option.some({
|
|
28895
|
+
baseUrl,
|
|
28896
|
+
token
|
|
28897
|
+
});
|
|
28898
|
+
};
|
|
28899
|
+
const StoredHubLink = Schema$1.fromJsonString(Schema$1.Struct({
|
|
28900
|
+
baseUrl: Schema$1.String,
|
|
28901
|
+
token: Schema$1.String,
|
|
28902
|
+
syncMode: Schema$1.optional(Schema$1.Literals(["auto", "paused"])),
|
|
28903
|
+
shareMode: Schema$1.optional(Schema$1.Literals(["all", "selected"]))
|
|
28904
|
+
}));
|
|
28905
|
+
const decodeStoredHubLink = Schema$1.decodeUnknownExit(StoredHubLink);
|
|
28906
|
+
const encodeStoredHubLink = Schema$1.encodeSync(StoredHubLink);
|
|
28907
|
+
/**
|
|
28908
|
+
* Parse a stored record, tolerating anything that is not the shape we wrote.
|
|
28909
|
+
*
|
|
28910
|
+
* A corrupt secret disables the sync rather than failing the server's start.
|
|
28911
|
+
* Losing sync is recoverable by reconnecting; refusing to boot over it is not
|
|
28912
|
+
* proportionate to what has gone wrong.
|
|
28913
|
+
*/
|
|
28914
|
+
const parseHubLink = (raw) => {
|
|
28915
|
+
const decoded = decodeStoredHubLink(raw);
|
|
28916
|
+
if (decoded._tag !== "Success") return {
|
|
28917
|
+
settings: Option.none(),
|
|
28918
|
+
syncMode: "auto",
|
|
28919
|
+
shareMode: "all"
|
|
28920
|
+
};
|
|
28921
|
+
return {
|
|
28922
|
+
settings: validateHubLink(decoded.value.baseUrl, decoded.value.token),
|
|
28923
|
+
syncMode: decoded.value.syncMode ?? "auto",
|
|
28924
|
+
shareMode: decoded.value.shareMode ?? "all"
|
|
28925
|
+
};
|
|
28926
|
+
};
|
|
28927
|
+
const formatHubLink = (input) => encodeStoredHubLink({
|
|
28928
|
+
baseUrl: Option.match(input.settings, {
|
|
28929
|
+
onNone: () => "",
|
|
28930
|
+
onSome: (settings) => trimTrailingSlash(settings.baseUrl)
|
|
28931
|
+
}),
|
|
28932
|
+
token: Option.match(input.settings, {
|
|
28933
|
+
onNone: () => "",
|
|
28934
|
+
onSome: (s) => s.token
|
|
28935
|
+
}),
|
|
28936
|
+
syncMode: input.syncMode,
|
|
28937
|
+
shareMode: input.shareMode
|
|
28938
|
+
});
|
|
28939
|
+
const fromEnvironment = (environment) => validateHubLink(environment.P4CODE_HUB_URL ?? "", environment.P4CODE_HUB_TOKEN ?? "");
|
|
28940
|
+
const make$113 = Effect.fn("HubLink.make")(function* (environment) {
|
|
28941
|
+
const secrets = yield* ServerSecretStore;
|
|
28942
|
+
const env = environment ?? process.env;
|
|
28943
|
+
const fromEnv = fromEnvironment(env);
|
|
28944
|
+
const readStored = Effect.fn("HubLink.readStored")(function* () {
|
|
28945
|
+
const stored = yield* secrets.get(HUB_LINK_SECRET_NAME).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
28946
|
+
return Option.match(stored, {
|
|
28947
|
+
onNone: () => ({
|
|
28948
|
+
settings: Option.none(),
|
|
28949
|
+
syncMode: "auto",
|
|
28950
|
+
shareMode: "all"
|
|
28951
|
+
}),
|
|
28952
|
+
onSome: (bytes) => parseHubLink(new TextDecoder().decode(bytes))
|
|
28953
|
+
});
|
|
28954
|
+
});
|
|
28955
|
+
const toState = (record) => Option.isSome(fromEnv) ? {
|
|
28956
|
+
settings: fromEnv,
|
|
28957
|
+
syncMode: record.syncMode,
|
|
28958
|
+
shareMode: record.shareMode,
|
|
28959
|
+
source: "environment"
|
|
28960
|
+
} : {
|
|
28961
|
+
settings: record.settings,
|
|
28962
|
+
syncMode: record.syncMode,
|
|
28963
|
+
shareMode: record.shareMode,
|
|
28964
|
+
source: Option.isSome(record.settings) ? "stored" : "none"
|
|
28965
|
+
};
|
|
28966
|
+
const cache = yield* Ref.make(yield* readStored());
|
|
28967
|
+
const updates = yield* PubSub.sliding({
|
|
28968
|
+
capacity: 1,
|
|
28969
|
+
replay: 1
|
|
28970
|
+
});
|
|
28971
|
+
yield* PubSub.publish(updates, toState(yield* Ref.get(cache)));
|
|
28972
|
+
const persist = Effect.fn("HubLink.persist")(function* (record) {
|
|
28973
|
+
yield* secrets.set(HUB_LINK_SECRET_NAME, new TextEncoder().encode(formatHubLink(record)));
|
|
28974
|
+
yield* Ref.set(cache, record);
|
|
28975
|
+
yield* PubSub.publish(updates, toState(record));
|
|
28976
|
+
return toState(record);
|
|
28977
|
+
});
|
|
28978
|
+
return {
|
|
28979
|
+
current: Ref.get(cache).pipe(Effect.map(toState)),
|
|
28980
|
+
changes: Stream.fromPubSub(updates),
|
|
28981
|
+
connect: (settings) => Effect.gen(function* () {
|
|
28982
|
+
const validated = validateHubLink(settings.baseUrl, settings.token);
|
|
28983
|
+
if (Option.isNone(validated)) return yield* new InvalidHubLink({ detail: "A hub needs an http or https URL and a non-empty token." });
|
|
28984
|
+
const record = yield* Ref.get(cache);
|
|
28985
|
+
return yield* persist({
|
|
28986
|
+
...record,
|
|
28987
|
+
settings: validated
|
|
28988
|
+
});
|
|
28989
|
+
}),
|
|
28990
|
+
disconnect: Effect.gen(function* () {
|
|
28991
|
+
const record = yield* Ref.get(cache);
|
|
28992
|
+
return yield* persist({
|
|
28993
|
+
...record,
|
|
28994
|
+
settings: Option.none()
|
|
28995
|
+
});
|
|
28996
|
+
}),
|
|
28997
|
+
setSyncMode: (mode) => Effect.gen(function* () {
|
|
28998
|
+
const record = yield* Ref.get(cache);
|
|
28999
|
+
return yield* persist({
|
|
29000
|
+
...record,
|
|
29001
|
+
syncMode: mode
|
|
29002
|
+
});
|
|
29003
|
+
}),
|
|
29004
|
+
setShareMode: (mode) => Effect.gen(function* () {
|
|
29005
|
+
const record = yield* Ref.get(cache);
|
|
29006
|
+
return yield* persist({
|
|
29007
|
+
...record,
|
|
29008
|
+
shareMode: mode
|
|
29009
|
+
});
|
|
29010
|
+
})
|
|
29011
|
+
};
|
|
29012
|
+
});
|
|
29013
|
+
const layer$95 = Layer.effect(HubLink, make$113());
|
|
29014
|
+
//#endregion
|
|
29015
|
+
//#region src/persistence/Layers/RemoteTasks.ts
|
|
29016
|
+
/**
|
|
29017
|
+
* The hub's task API, as a client.
|
|
29018
|
+
*
|
|
29019
|
+
* This used to be a `TaskRepository` in its own right, and the board was
|
|
29020
|
+
* either this or the local database. It is now the transport underneath
|
|
29021
|
+
* `SyncedTaskRepository`: local SQLite is a replica of the hub, so nothing
|
|
29022
|
+
* reads from here directly and the board no longer goes dark when the hub is
|
|
29023
|
+
* unreachable. What changed with it is the failure story — an unreachable hub
|
|
29024
|
+
* used to mean no board, and now means a board that is behind.
|
|
29025
|
+
*
|
|
29026
|
+
* The four calls here are what a mirror needs and no more: pull what changed
|
|
29027
|
+
* on the hub, push a whole row, arbitrate a claim, and delete. There is no partial
|
|
29028
|
+
* patch, because a mirror always holds the full row it wants the hub to have
|
|
29029
|
+
* and sending a diff of it would only add a way for the two to disagree.
|
|
29030
|
+
*
|
|
29031
|
+
* @module persistence/Layers/RemoteTasks
|
|
29032
|
+
*/
|
|
29033
|
+
const HUB_REQUEST_TIMEOUT_MS$1 = 15e3;
|
|
29034
|
+
/**
|
|
29035
|
+
* How often the hub writes to an idle `/v1/task-changes` stream. The client
|
|
29036
|
+
* treats two missed beats as a dead connection. Under ten seconds because Bun
|
|
29037
|
+
* closes a connection idle for ten, and proxies in front of a hosted hub are
|
|
29038
|
+
* rarely more patient.
|
|
29039
|
+
*/
|
|
29040
|
+
const HUB_TASK_CHANGES_HEARTBEAT_MS = 8e3;
|
|
29041
|
+
/**
|
|
29042
|
+
* `cursor`, `deletedTaskIds` and `full` are absent from a hub that predates
|
|
29043
|
+
* cursors, which answers every pull with the whole board.
|
|
29044
|
+
*/
|
|
29045
|
+
const TaskListPage = Schema$1.Struct({
|
|
29046
|
+
tasks: Schema$1.Array(Task),
|
|
29047
|
+
deletedTaskIds: Schema$1.optional(Schema$1.Array(TaskId)),
|
|
29048
|
+
cursor: Schema$1.optional(Schema$1.String),
|
|
29049
|
+
full: Schema$1.optional(Schema$1.Boolean)
|
|
29050
|
+
});
|
|
29051
|
+
const ConflictBody$1 = Schema$1.Struct({ current: Task });
|
|
29052
|
+
const decodeTaskListPage = Schema$1.decodeUnknownEffect(TaskListPage);
|
|
29053
|
+
const decodeTask = Schema$1.decodeUnknownEffect(Task);
|
|
29054
|
+
const decodeConflictBody$1 = Schema$1.decodeUnknownEffect(ConflictBody$1);
|
|
29055
|
+
/**
|
|
29056
|
+
* Every failure here is reported as the persistence error type.
|
|
29057
|
+
*
|
|
29058
|
+
* The mirror catches these and carries on — a failed push leaves the row
|
|
29059
|
+
* `pending` and the next pass retries it — so what matters is that the detail
|
|
29060
|
+
* says the hub was the store that would not answer, which is what shows up in
|
|
29061
|
+
* the log when someone asks why their two machines disagree.
|
|
29062
|
+
*/
|
|
29063
|
+
const hubUnavailable = (operation, detail) => new PersistenceSqlError({
|
|
29064
|
+
operation: `HubTaskClient.${operation}`,
|
|
29065
|
+
detail: `The hub board is unavailable: ${detail}`,
|
|
29066
|
+
cause: null
|
|
29067
|
+
});
|
|
29068
|
+
var HubTaskClient = class extends Context.Service()("@p4code/cli/persistence/Layers/RemoteTasks/HubTaskClient") {};
|
|
29069
|
+
const makeHubTaskClient = Effect.gen(function* () {
|
|
29070
|
+
const http = yield* HttpClient.HttpClient;
|
|
29071
|
+
const link = yield* HubLink;
|
|
29072
|
+
const isLinked = link.current.pipe(Effect.map((state) => Option.isSome(state.settings)));
|
|
29073
|
+
const requireSettings = (operation) => Effect.gen(function* () {
|
|
29074
|
+
const state = yield* link.current;
|
|
29075
|
+
if (Option.isNone(state.settings)) return yield* hubUnavailable(operation, "no hub is configured on this server");
|
|
29076
|
+
return state.settings.value;
|
|
29077
|
+
});
|
|
29078
|
+
const send = (operation, settings, request) => http.execute(request.pipe(HttpClientRequest.setHeader("authorization", `Bearer ${settings.token}`), HttpClientRequest.setHeader("accept", "application/json"))).pipe(Effect.timeout(HUB_REQUEST_TIMEOUT_MS$1), Effect.mapError((cause) => hubUnavailable(operation, `${cause._tag ?? "request failed"}`)));
|
|
29079
|
+
const pull = (from) => Effect.gen(function* () {
|
|
29080
|
+
const settings = yield* requireSettings("pull");
|
|
29081
|
+
const resume = from?.baseUrl === settings.baseUrl ? from : void 0;
|
|
29082
|
+
const request = HttpClientRequest.get(`${settings.baseUrl}/tasks${resume === void 0 ? "" : `?since=${encodeURIComponent(resume.cursor)}`}`);
|
|
29083
|
+
const response = yield* send("pull", settings, resume?.etag === void 0 ? request : HttpClientRequest.setHeader(request, "if-none-match", resume.etag));
|
|
29084
|
+
if (response.status === 304) return Option.none();
|
|
29085
|
+
if (response.status !== 200) return yield* hubUnavailable("pull", `status ${response.status}`);
|
|
29086
|
+
const body = yield* response.json.pipe(Effect.flatMap(decodeTaskListPage), Effect.mapError(() => hubUnavailable("pull", "the response did not match the contract")));
|
|
29087
|
+
return Option.some({
|
|
29088
|
+
full: body.full ?? true,
|
|
29089
|
+
tasks: body.tasks,
|
|
29090
|
+
deletedTaskIds: body.deletedTaskIds ?? [],
|
|
29091
|
+
next: body.cursor === void 0 ? void 0 : {
|
|
29092
|
+
baseUrl: settings.baseUrl,
|
|
29093
|
+
cursor: body.cursor,
|
|
29094
|
+
etag: response.headers.etag
|
|
29095
|
+
}
|
|
29096
|
+
});
|
|
29097
|
+
});
|
|
29098
|
+
const watch = Stream.unwrap(Effect.gen(function* () {
|
|
29099
|
+
const settings = yield* requireSettings("watch");
|
|
29100
|
+
const response = yield* http.execute(HttpClientRequest.get(`${settings.baseUrl}/v1/task-changes`).pipe(HttpClientRequest.setHeader("authorization", `Bearer ${settings.token}`))).pipe(Effect.mapError((cause) => hubUnavailable("watch", `${cause._tag ?? "request failed"}`)));
|
|
29101
|
+
if (response.status !== 200) return yield* hubUnavailable("watch", `status ${response.status}`);
|
|
29102
|
+
const relinked = link.changes.pipe(Stream.filter((state) => Option.isNone(state.settings) || state.settings.value.baseUrl !== settings.baseUrl || state.settings.value.token !== settings.token), Stream.runHead);
|
|
29103
|
+
return response.stream.pipe(Stream.mapError(() => hubUnavailable("watch", "the change stream failed")), Stream.decodeText, Stream.splitLines, Stream.timeout(HUB_TASK_CHANGES_HEARTBEAT_MS * 2), Stream.filter((line) => line.includes("\"ready\"") || line.includes("\"changed\"")), Stream.map(() => void 0), Stream.interruptWhen(relinked));
|
|
29104
|
+
}));
|
|
29105
|
+
const put = (row, readableIdPrefix) => Effect.gen(function* () {
|
|
29106
|
+
const settings = yield* requireSettings("put");
|
|
29107
|
+
const response = yield* send("put", settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.put(`${settings.baseUrl}/tasks/${encodeURIComponent(row.taskId)}`), {
|
|
29108
|
+
projectId: row.projectId,
|
|
29109
|
+
repositoryKey: row.repositoryKey,
|
|
29110
|
+
threadId: row.threadId,
|
|
29111
|
+
parentTaskId: row.parentTaskId,
|
|
29112
|
+
title: row.title,
|
|
29113
|
+
body: row.body,
|
|
29114
|
+
status: row.status,
|
|
29115
|
+
priority: row.priority,
|
|
29116
|
+
assignee: row.assignee,
|
|
29117
|
+
labels: row.labels,
|
|
29118
|
+
orderKey: row.orderKey,
|
|
29119
|
+
createdAt: row.createdAt,
|
|
29120
|
+
statusChangedAt: row.statusChangedAt ?? row.updatedAt,
|
|
29121
|
+
readableIdPrefix
|
|
29122
|
+
}));
|
|
29123
|
+
if (response.status !== 200 && response.status !== 201) return yield* hubUnavailable("put", `status ${response.status}`);
|
|
29124
|
+
return yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable("put", "the response did not match the contract")));
|
|
29125
|
+
});
|
|
29126
|
+
const patchIfStatus = (input) => Effect.gen(function* () {
|
|
29127
|
+
const settings = yield* requireSettings("patchIfStatus");
|
|
29128
|
+
const { taskId, ...fields } = input.patch;
|
|
29129
|
+
const response = yield* send("patchIfStatus", settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.patch(`${settings.baseUrl}/tasks/${encodeURIComponent(taskId)}`), {
|
|
29130
|
+
...fields,
|
|
29131
|
+
expectedStatus: input.expectedStatus
|
|
29132
|
+
}));
|
|
29133
|
+
if (response.status === 404) return { outcome: "missing" };
|
|
29134
|
+
if (response.status === 409) return {
|
|
29135
|
+
outcome: "conflict",
|
|
29136
|
+
current: (yield* response.json.pipe(Effect.flatMap(decodeConflictBody$1), Effect.mapError(() => hubUnavailable("patchIfStatus", "the conflict response did not match the contract")))).current
|
|
29137
|
+
};
|
|
29138
|
+
if (response.status !== 200) return yield* hubUnavailable("patchIfStatus", `status ${response.status}`);
|
|
29139
|
+
return {
|
|
29140
|
+
outcome: "updated",
|
|
29141
|
+
task: yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable("patchIfStatus", "the response did not match the contract")))
|
|
29142
|
+
};
|
|
29143
|
+
});
|
|
29144
|
+
const remove = (taskId) => Effect.gen(function* () {
|
|
29145
|
+
const settings = yield* requireSettings("remove");
|
|
29146
|
+
const response = yield* send("remove", settings, HttpClientRequest.delete(`${settings.baseUrl}/tasks/${encodeURIComponent(taskId)}`));
|
|
29147
|
+
if (response.status !== 200 && response.status !== 204 && response.status !== 404) return yield* hubUnavailable("remove", `status ${response.status}`);
|
|
29148
|
+
});
|
|
29149
|
+
return {
|
|
29150
|
+
isLinked,
|
|
29151
|
+
pull,
|
|
29152
|
+
watch,
|
|
29153
|
+
put,
|
|
29154
|
+
patchIfStatus,
|
|
29155
|
+
remove
|
|
29156
|
+
};
|
|
29157
|
+
});
|
|
29158
|
+
Layer.effect(HubTaskClient, makeHubTaskClient);
|
|
29159
|
+
//#endregion
|
|
28798
29160
|
//#region src/hub/HubHttpServer.ts
|
|
28799
29161
|
/**
|
|
28800
29162
|
* The hub's HTTP surface.
|
|
@@ -28857,10 +29219,66 @@ const identityRoute = HttpRouter.add("GET", "/v1/identity", respondToHubFailures
|
|
|
28857
29219
|
environmentId: principal.environmentId
|
|
28858
29220
|
}, { headers: { "cache-control": "no-store" } });
|
|
28859
29221
|
})));
|
|
29222
|
+
/**
|
|
29223
|
+
* Minted per process, not per database: a database restored from a backup
|
|
29224
|
+
* carries its old sequence numbers, and a cursor past the restore point would
|
|
29225
|
+
* otherwise skip every write it lost. The cost is one full pull per server
|
|
29226
|
+
* after each hub restart.
|
|
29227
|
+
*/
|
|
29228
|
+
const TASK_CURSOR_EPOCH = NodeCrypto.randomBytes(8).toString("hex");
|
|
29229
|
+
/**
|
|
29230
|
+
* The board, or what changed on it since a cursor.
|
|
29231
|
+
*
|
|
29232
|
+
* `?since=<cursor>` returns rows changed after that cursor plus the ids
|
|
29233
|
+
* deleted since, so a server polling an idle board downloads almost nothing.
|
|
29234
|
+
* An unfiltered list also carries the cursor to start from. A cursor this
|
|
29235
|
+
* process never issued gets the whole board back with `full: true`. Cursors
|
|
29236
|
+
* are `<epoch>-<seq>`; callers treat them as opaque.
|
|
29237
|
+
*/
|
|
29238
|
+
const readTaskPage = Effect.fn("HubHttpServer.readTaskPage")(function* (filter, since) {
|
|
29239
|
+
const tasks = yield* TaskRepository;
|
|
29240
|
+
if (since === void 0 && Object.keys(filter).length > 0) return { tasks: yield* tasks.list(filter) };
|
|
29241
|
+
const sql = yield* SqlClient.SqlClient;
|
|
29242
|
+
const [head] = yield* sql`SELECT COALESCE(MAX(seq), 0) AS seq FROM task_change_log`;
|
|
29243
|
+
const seq = head?.seq ?? 0;
|
|
29244
|
+
const epoch = TASK_CURSOR_EPOCH;
|
|
29245
|
+
const cursor = `${epoch}-${seq}`;
|
|
29246
|
+
const [sinceEpoch, sinceSeqText] = since?.split("-") ?? [];
|
|
29247
|
+
const sinceSeq = Number(sinceSeqText);
|
|
29248
|
+
if (since === void 0 || sinceEpoch !== epoch || sinceSeq > seq) return {
|
|
29249
|
+
tasks: yield* tasks.list({}),
|
|
29250
|
+
deletedTaskIds: [],
|
|
29251
|
+
cursor,
|
|
29252
|
+
full: true
|
|
29253
|
+
};
|
|
29254
|
+
const changes = yield* sql`
|
|
29255
|
+
SELECT task_id AS "taskId", deleted FROM task_change_log
|
|
29256
|
+
WHERE seq > ${sinceSeq} AND seq <= ${seq}
|
|
29257
|
+
ORDER BY seq
|
|
29258
|
+
`;
|
|
29259
|
+
const changed = [];
|
|
29260
|
+
const deletedTaskIds = [];
|
|
29261
|
+
for (const change of changes) {
|
|
29262
|
+
if (change.deleted === 1) {
|
|
29263
|
+
deletedTaskIds.push(change.taskId);
|
|
29264
|
+
continue;
|
|
29265
|
+
}
|
|
29266
|
+
const task = yield* tasks.getById({ taskId: change.taskId });
|
|
29267
|
+
if (Option.isSome(task)) changed.push(task.value);
|
|
29268
|
+
}
|
|
29269
|
+
return {
|
|
29270
|
+
tasks: changed,
|
|
29271
|
+
deletedTaskIds,
|
|
29272
|
+
cursor,
|
|
29273
|
+
full: false
|
|
29274
|
+
};
|
|
29275
|
+
});
|
|
29276
|
+
/** Weak, because gzip may re-encode the same representation on the way out. */
|
|
29277
|
+
const taskPageEtag = (body) => `W/"${NodeCrypto.createHash("sha256").update(body).digest("base64url")}"`;
|
|
29278
|
+
const matchesEtag = (ifNoneMatch, etag) => ifNoneMatch !== void 0 && ifNoneMatch.split(",").map((candidate) => candidate.trim().replace(/^W\//, "")).some((candidate) => candidate === "*" || candidate === etag.replace(/^W\//, ""));
|
|
28860
29279
|
const listTasksRoute = HttpRouter.add("GET", "/tasks", respondToHubFailures(Effect.gen(function* () {
|
|
28861
29280
|
yield* authenticateHubRequest();
|
|
28862
29281
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
28863
|
-
const tasks = yield* TaskRepository;
|
|
28864
29282
|
const url = new URL(request.url, "http://hub.invalid");
|
|
28865
29283
|
const filter = yield* decodeOrInvalid(TaskListFilter, "Query string does not match TaskListFilter.")(Object.fromEntries([
|
|
28866
29284
|
"projectId",
|
|
@@ -28869,8 +29287,39 @@ const listTasksRoute = HttpRouter.add("GET", "/tasks", respondToHubFailures(Effe
|
|
|
28869
29287
|
"assignee",
|
|
28870
29288
|
"query"
|
|
28871
29289
|
].map((key) => [key, url.searchParams.get(key)]).filter(([, value]) => value !== null)));
|
|
28872
|
-
const
|
|
28873
|
-
|
|
29290
|
+
const sinceParam = url.searchParams.get("since");
|
|
29291
|
+
if (sinceParam !== null && (!/^[0-9a-f]{1,32}-\d{1,15}$/.test(sinceParam) || Object.keys(filter).length > 0)) return yield* new InvalidHubRequest({ detail: "`since` must be a cursor from an earlier page and cannot be combined with filters." });
|
|
29292
|
+
const page = yield* readTaskPage(filter, sinceParam ?? void 0);
|
|
29293
|
+
const body = JSON.stringify(page);
|
|
29294
|
+
const etag = taskPageEtag(body);
|
|
29295
|
+
if (matchesEtag(request.headers["if-none-match"], etag)) return HttpServerResponse.empty({
|
|
29296
|
+
status: 304,
|
|
29297
|
+
headers: { etag }
|
|
29298
|
+
});
|
|
29299
|
+
return HttpServerResponse.text(body, {
|
|
29300
|
+
contentType: "application/json",
|
|
29301
|
+
headers: { etag }
|
|
29302
|
+
});
|
|
29303
|
+
})));
|
|
29304
|
+
const taskChangeLine = (type) => new TextEncoder().encode(`{"type":"${type}"}\n`);
|
|
29305
|
+
/**
|
|
29306
|
+
* A nudge per board write, so linked servers pull when something changed
|
|
29307
|
+
* instead of on a timer.
|
|
29308
|
+
*
|
|
29309
|
+
* Carries no rows: the server answers a nudge with the same cursor pull it
|
|
29310
|
+
* always makes, so a nudge that is lost or duplicated costs nothing but
|
|
29311
|
+
* latency. `ready` doubles as a nudge, covering whatever changed while the
|
|
29312
|
+
* server was not listening.
|
|
29313
|
+
*/
|
|
29314
|
+
const taskChangesRoute = HttpRouter.add("GET", "/v1/task-changes", respondToHubFailures(Effect.gen(function* () {
|
|
29315
|
+
yield* authenticateHubRequest();
|
|
29316
|
+
const tasks = yield* TaskRepository;
|
|
29317
|
+
const events = Stream.merge(tasks.streamWithSnapshot.pipe(Stream.map((event) => taskChangeLine(event.type === "snapshot" ? "ready" : "changed"))), Stream.tick(HUB_TASK_CHANGES_HEARTBEAT_MS).pipe(Stream.drop(1), Stream.map(() => taskChangeLine("heartbeat"))));
|
|
29318
|
+
return HttpServerResponse.stream(events, { headers: {
|
|
29319
|
+
"content-type": "application/x-ndjson",
|
|
29320
|
+
"cache-control": "no-store",
|
|
29321
|
+
"x-accel-buffering": "no"
|
|
29322
|
+
} });
|
|
28874
29323
|
})));
|
|
28875
29324
|
const getTaskRoute = HttpRouter.add("GET", "/tasks/:taskId", respondToHubFailures(Effect.gen(function* () {
|
|
28876
29325
|
yield* authenticateHubRequest();
|
|
@@ -29292,7 +29741,7 @@ const releaseFeedLeaseRoute = HttpRouter.add("DELETE", "/feed/lease", respondToH
|
|
|
29292
29741
|
})));
|
|
29293
29742
|
/** Provide around HttpRouter.serve so request fibers share these process-owned services. */
|
|
29294
29743
|
const routingServices = Layer.mergeAll(layer$113, layer$111, layer$102, layer$101).pipe(Layer.provideMerge(layer$112));
|
|
29295
|
-
const layer$
|
|
29744
|
+
const layer$94 = Layer.mergeAll(layer$100, layer$99, layer$103, layer$97, layer$96, layer$110, layer$98, healthRoute, identityRoute, listTasksRoute, taskChangesRoute, getTaskRoute, createTaskRoute, putTaskRoute, updateTaskRoute, deleteTaskRoute, listAssetsRoute, getAssetRoute, putAssetRoute, deleteAssetRoute, listFeedSourcesRoute, upsertFeedSourceRoute, deleteFeedSourceRoute, listFeedRoute, putFeedArticleRoute, putFeedEngagementRoute, markFeedReadRoute, cleanupFeedRoute, setFeedRefreshedAtRoute, acquireFeedLeaseRoute, renewFeedLeaseRoute, releaseFeedLeaseRoute).pipe(Layer.provideMerge(layer$113), Layer.provideMerge(layer$111), Layer.provideMerge(layer$102));
|
|
29296
29745
|
//#endregion
|
|
29297
29746
|
//#region src/hub/Migrations/001_Tasks.ts
|
|
29298
29747
|
/**
|
|
@@ -29789,6 +30238,57 @@ var _022_AnalyticsFacts_default = Effect.gen(function* () {
|
|
|
29789
30238
|
ON hub_analytics_facts(account_id, kind, occurred_at DESC, event_id DESC)`;
|
|
29790
30239
|
});
|
|
29791
30240
|
//#endregion
|
|
30241
|
+
//#region src/hub/Migrations/023_TaskChangeLog.ts
|
|
30242
|
+
/**
|
|
30243
|
+
* One row per task that has ever existed, stamped with the order of its last
|
|
30244
|
+
* change, so a server can ask for "everything since cursor N" instead of the
|
|
30245
|
+
* whole board every poll.
|
|
30246
|
+
*
|
|
30247
|
+
* The sequence is the hub's, not `updated_at`: that timestamp is written by
|
|
30248
|
+
* whichever machine made the edit, and a machine with a slow clock would land
|
|
30249
|
+
* behind a cursor that already moved past it. `AUTOINCREMENT` never reuses a
|
|
30250
|
+
* value, and SQLite serializes writers, so a reader never sees N+1 before N.
|
|
30251
|
+
*
|
|
30252
|
+
* Triggers rather than repository code, because the hub shares
|
|
30253
|
+
* `TaskRepository` with every environment server and only the hub needs this.
|
|
30254
|
+
* A delete keeps its row with `deleted = 1`, which is how another machine
|
|
30255
|
+
* learns the task is gone. The triggers delete then insert rather than
|
|
30256
|
+
* `INSERT OR REPLACE`, because the repository writes with an upsert and its
|
|
30257
|
+
* conflict policy overrides the trigger's, failing the whole write.
|
|
30258
|
+
*/
|
|
30259
|
+
var _023_TaskChangeLog_default = Effect.gen(function* () {
|
|
30260
|
+
const sql = yield* SqlClient.SqlClient;
|
|
30261
|
+
yield* sql`
|
|
30262
|
+
CREATE TABLE task_change_log (
|
|
30263
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30264
|
+
task_id TEXT NOT NULL UNIQUE,
|
|
30265
|
+
deleted INTEGER NOT NULL DEFAULT 0
|
|
30266
|
+
)
|
|
30267
|
+
`;
|
|
30268
|
+
yield* sql`
|
|
30269
|
+
INSERT INTO task_change_log (task_id)
|
|
30270
|
+
SELECT task_id FROM tasks ORDER BY updated_at, task_id
|
|
30271
|
+
`;
|
|
30272
|
+
yield* sql`
|
|
30273
|
+
CREATE TRIGGER task_change_log_insert AFTER INSERT ON tasks BEGIN
|
|
30274
|
+
DELETE FROM task_change_log WHERE task_id = NEW.task_id;
|
|
30275
|
+
INSERT INTO task_change_log (task_id, deleted) VALUES (NEW.task_id, 0);
|
|
30276
|
+
END
|
|
30277
|
+
`;
|
|
30278
|
+
yield* sql`
|
|
30279
|
+
CREATE TRIGGER task_change_log_update AFTER UPDATE ON tasks BEGIN
|
|
30280
|
+
DELETE FROM task_change_log WHERE task_id = NEW.task_id;
|
|
30281
|
+
INSERT INTO task_change_log (task_id, deleted) VALUES (NEW.task_id, 0);
|
|
30282
|
+
END
|
|
30283
|
+
`;
|
|
30284
|
+
yield* sql`
|
|
30285
|
+
CREATE TRIGGER task_change_log_delete AFTER DELETE ON tasks BEGIN
|
|
30286
|
+
DELETE FROM task_change_log WHERE task_id = OLD.task_id;
|
|
30287
|
+
INSERT INTO task_change_log (task_id, deleted) VALUES (OLD.task_id, 1);
|
|
30288
|
+
END
|
|
30289
|
+
`;
|
|
30290
|
+
});
|
|
30291
|
+
//#endregion
|
|
29792
30292
|
//#region src/hub/Migrations.ts
|
|
29793
30293
|
/**
|
|
29794
30294
|
* Hub migrations.
|
|
@@ -29909,6 +30409,11 @@ const hubMigrationEntries = [
|
|
|
29909
30409
|
22,
|
|
29910
30410
|
"AnalyticsFacts",
|
|
29911
30411
|
_022_AnalyticsFacts_default
|
|
30412
|
+
],
|
|
30413
|
+
[
|
|
30414
|
+
23,
|
|
30415
|
+
"TaskChangeLog",
|
|
30416
|
+
_023_TaskChangeLog_default
|
|
29912
30417
|
]
|
|
29913
30418
|
];
|
|
29914
30419
|
const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
|
|
@@ -30000,7 +30505,7 @@ const announce = Layer.effectDiscard(Effect.gen(function* () {
|
|
|
30000
30505
|
});
|
|
30001
30506
|
if (isRemoteReachableHost(config.host)) yield* Effect.logWarning("Hub is reachable beyond loopback. The bearer token is the only control — repeated auth failures are not rate limited.", { host: config.host });
|
|
30002
30507
|
}));
|
|
30003
|
-
const makeHubLayer = HttpRouter.serve(Layer.mergeAll(layer$
|
|
30508
|
+
const makeHubLayer = HttpRouter.serve(Layer.mergeAll(layer$94, layer$107, layer$109).pipe(Layer.provide(httpCompressionLayer))).pipe(Layer.provideMerge(typeof Bun !== "undefined" ? layerBun : layerNode), Layer.provideMerge(routingServices), Layer.provideMerge(layer$105), Layer.provideMerge(layer$106), Layer.provideMerge(layer$104), Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(layer$115), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
|
|
30004
30509
|
const runHub = Layer.launch(makeHubLayer);
|
|
30005
30510
|
//#endregion
|
|
30006
30511
|
//#region src/atomicWrite.ts
|
|
@@ -30157,7 +30662,7 @@ function stripDefaultServerSettings(current, defaults) {
|
|
|
30157
30662
|
}
|
|
30158
30663
|
return Object.is(current, defaults) ? void 0 : current;
|
|
30159
30664
|
}
|
|
30160
|
-
const make$
|
|
30665
|
+
const make$112 = Effect.gen(function* () {
|
|
30161
30666
|
const { settingsPath } = yield* ServerConfig$1;
|
|
30162
30667
|
const fs = yield* FileSystem.FileSystem;
|
|
30163
30668
|
const pathService = yield* Path$1.Path;
|
|
@@ -30380,7 +30885,7 @@ const make$113 = Effect.gen(function* () {
|
|
|
30380
30885
|
}
|
|
30381
30886
|
};
|
|
30382
30887
|
});
|
|
30383
|
-
const layer$
|
|
30888
|
+
const layer$93 = Layer.effect(ServerSettingsService, make$112);
|
|
30384
30889
|
//#endregion
|
|
30385
30890
|
//#region src/provider/skillWorkspace.ts
|
|
30386
30891
|
/** Ancestors inside the nearest Git checkout, ordered from root to cwd. */
|
|
@@ -31124,7 +31629,7 @@ function entriesOf(config) {
|
|
|
31124
31629
|
return found;
|
|
31125
31630
|
}
|
|
31126
31631
|
var ClaudeMcpFiles = class extends Context.Service()("@p4code/cli/mcp/ClaudeMcpFiles") {};
|
|
31127
|
-
const make$
|
|
31632
|
+
const make$111 = Effect.gen(function* () {
|
|
31128
31633
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
31129
31634
|
const path = yield* Path$1.Path;
|
|
31130
31635
|
const services = yield* Effect.context();
|
|
@@ -31195,7 +31700,7 @@ const make$112 = Effect.gen(function* () {
|
|
|
31195
31700
|
removeProject: (projectDir, name) => removeAt(Effect.succeed(projectFile(projectDir)))(name)
|
|
31196
31701
|
};
|
|
31197
31702
|
});
|
|
31198
|
-
const layer$
|
|
31703
|
+
const layer$92 = Layer.effect(ClaudeMcpFiles, make$111);
|
|
31199
31704
|
Layer.succeed(ClaudeMcpFiles, {
|
|
31200
31705
|
readUser: Effect.succeed([]),
|
|
31201
31706
|
readUserStrict: Effect.succeed([]),
|
|
@@ -31369,7 +31874,7 @@ const decodeClientRegistration = Schema$1.decodeUnknownExit(ClientRegistrationRe
|
|
|
31369
31874
|
const decodeTokenResponse = Schema$1.decodeUnknownExit(TokenResponse);
|
|
31370
31875
|
var McpOAuth = class extends Context.Service()("@p4code/cli/mcp/McpOAuth") {};
|
|
31371
31876
|
const registryError = (detail) => new McpRegistryError({ detail });
|
|
31372
|
-
const make$
|
|
31877
|
+
const make$110 = Effect.gen(function* () {
|
|
31373
31878
|
const config = yield* ServerConfig$1;
|
|
31374
31879
|
const secrets = yield* ServerSecretStore;
|
|
31375
31880
|
const http = yield* HttpClient.HttpClient;
|
|
@@ -31774,7 +32279,7 @@ const make$111 = Effect.gen(function* () {
|
|
|
31774
32279
|
grantIdentityFor
|
|
31775
32280
|
};
|
|
31776
32281
|
});
|
|
31777
|
-
const layer$
|
|
32282
|
+
const layer$91 = Layer.effect(McpOAuth, make$110);
|
|
31778
32283
|
Layer.succeed(McpOAuth, {
|
|
31779
32284
|
statusFor: () => Effect.succeed(Option.none()),
|
|
31780
32285
|
begin: () => Effect.fail(new McpRegistryError({ detail: "OAuth sign-in is not available." })),
|
|
@@ -31829,7 +32334,7 @@ const slotsOf = (registration) => registration.secrets ?? [];
|
|
|
31829
32334
|
*/
|
|
31830
32335
|
const OMITTED_FOR_SIGN_IN = "mcp server omitted: sign-in required";
|
|
31831
32336
|
const SIGN_IN_ACTION = "Connect it in Settings > MCP servers.";
|
|
31832
|
-
const make$
|
|
32337
|
+
const make$109 = Effect.gen(function* () {
|
|
31833
32338
|
const config = yield* ServerConfig$1;
|
|
31834
32339
|
const secrets = yield* ServerSecretStore;
|
|
31835
32340
|
const oauth = yield* McpOAuth;
|
|
@@ -32109,7 +32614,7 @@ const make$110 = Effect.gen(function* () {
|
|
|
32109
32614
|
resolveForSessionAtClaudeUserConfigPath
|
|
32110
32615
|
};
|
|
32111
32616
|
});
|
|
32112
|
-
const layer$
|
|
32617
|
+
const layer$90 = Layer.effect(McpRegistry, make$109);
|
|
32113
32618
|
//#endregion
|
|
32114
32619
|
//#region src/sync/skillDirectory.ts
|
|
32115
32620
|
/**
|
|
@@ -32393,176 +32898,6 @@ const removeAgentDefinition = Effect.fn("agentDefinitionFiles.remove")(function*
|
|
|
32393
32898
|
yield* fileSystem.remove(path.join(input.agentsRoot, input.name), { force: true });
|
|
32394
32899
|
});
|
|
32395
32900
|
//#endregion
|
|
32396
|
-
//#region src/sync/HubLink.ts
|
|
32397
|
-
/**
|
|
32398
|
-
* Where this server's hub is, what it authenticates with, and whether it syncs
|
|
32399
|
-
* on its own.
|
|
32400
|
-
*
|
|
32401
|
-
* Absent by default. A server with no hub configured runs exactly as it does
|
|
32402
|
-
* today — the sync is skipped rather than failing, because "no hub" is a
|
|
32403
|
-
* complete and normal configuration, not a broken one.
|
|
32404
|
-
*
|
|
32405
|
-
* The record lives in `ServerSecretStore`, which already writes with
|
|
32406
|
-
* restrictive permissions, rather than in the settings file or the database.
|
|
32407
|
-
* The sync mode rides along in that record even though it is not a secret:
|
|
32408
|
-
* connect, disconnect and pause are then one write and one read, and the
|
|
32409
|
-
* settings panel reads one thing instead of joining two. Environment variables
|
|
32410
|
-
* override the link so a container or a service unit can be configured without
|
|
32411
|
-
* a first-run interactive step — the same trade the hub process itself makes
|
|
32412
|
-
* with `P4CODE_HUB_TOKEN_HASHES` — while the mode always comes from the stored
|
|
32413
|
-
* record, so a server configured by environment can still be paused.
|
|
32414
|
-
*
|
|
32415
|
-
* Mutable, deliberately. Resolving this once at layer construction would mean
|
|
32416
|
-
* connecting to a hub did nothing until the server restarted, which is a
|
|
32417
|
-
* miserable thing to discover after typing a token into a form.
|
|
32418
|
-
*
|
|
32419
|
-
* @module sync/HubLink
|
|
32420
|
-
*/
|
|
32421
|
-
/** Secret name holding the hub record as JSON. */
|
|
32422
|
-
const HUB_LINK_SECRET_NAME = "hub-link";
|
|
32423
|
-
var HubLink = class extends Context.Service()("@p4code/cli/sync/HubLink") {};
|
|
32424
|
-
/** The submitted URL or token could not make a usable link. */
|
|
32425
|
-
var InvalidHubLink = class extends Schema$1.TaggedErrorClass()("InvalidHubLink", { detail: Schema$1.String }) {
|
|
32426
|
-
get message() {
|
|
32427
|
-
return this.detail;
|
|
32428
|
-
}
|
|
32429
|
-
};
|
|
32430
|
-
const trimTrailingSlash = (value) => value.replace(/\/+$/u, "");
|
|
32431
|
-
/**
|
|
32432
|
-
* Both halves must be present and the URL parseable. A half-configured link is
|
|
32433
|
-
* treated as no link at all, because a server that thinks it has a hub and
|
|
32434
|
-
* cannot form a request would report every sync as a failure rather than as the
|
|
32435
|
-
* "not configured" it actually is.
|
|
32436
|
-
*/
|
|
32437
|
-
const validateHubLink = (rawBaseUrl, rawToken) => {
|
|
32438
|
-
const trimmed = trimTrailingSlash(rawBaseUrl.trim());
|
|
32439
|
-
const token = rawToken.trim();
|
|
32440
|
-
if (trimmed.length === 0 || token.length === 0) return Option.none();
|
|
32441
|
-
const baseUrl = /^[a-z][a-z0-9+.-]*:\/\//iu.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
32442
|
-
if (!URL.canParse(baseUrl)) return Option.none();
|
|
32443
|
-
const parsed = new URL(baseUrl);
|
|
32444
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return Option.none();
|
|
32445
|
-
return Option.some({
|
|
32446
|
-
baseUrl,
|
|
32447
|
-
token
|
|
32448
|
-
});
|
|
32449
|
-
};
|
|
32450
|
-
const StoredHubLink = Schema$1.fromJsonString(Schema$1.Struct({
|
|
32451
|
-
baseUrl: Schema$1.String,
|
|
32452
|
-
token: Schema$1.String,
|
|
32453
|
-
syncMode: Schema$1.optional(Schema$1.Literals(["auto", "paused"])),
|
|
32454
|
-
shareMode: Schema$1.optional(Schema$1.Literals(["all", "selected"]))
|
|
32455
|
-
}));
|
|
32456
|
-
const decodeStoredHubLink = Schema$1.decodeUnknownExit(StoredHubLink);
|
|
32457
|
-
const encodeStoredHubLink = Schema$1.encodeSync(StoredHubLink);
|
|
32458
|
-
/**
|
|
32459
|
-
* Parse a stored record, tolerating anything that is not the shape we wrote.
|
|
32460
|
-
*
|
|
32461
|
-
* A corrupt secret disables the sync rather than failing the server's start.
|
|
32462
|
-
* Losing sync is recoverable by reconnecting; refusing to boot over it is not
|
|
32463
|
-
* proportionate to what has gone wrong.
|
|
32464
|
-
*/
|
|
32465
|
-
const parseHubLink = (raw) => {
|
|
32466
|
-
const decoded = decodeStoredHubLink(raw);
|
|
32467
|
-
if (decoded._tag !== "Success") return {
|
|
32468
|
-
settings: Option.none(),
|
|
32469
|
-
syncMode: "auto",
|
|
32470
|
-
shareMode: "all"
|
|
32471
|
-
};
|
|
32472
|
-
return {
|
|
32473
|
-
settings: validateHubLink(decoded.value.baseUrl, decoded.value.token),
|
|
32474
|
-
syncMode: decoded.value.syncMode ?? "auto",
|
|
32475
|
-
shareMode: decoded.value.shareMode ?? "all"
|
|
32476
|
-
};
|
|
32477
|
-
};
|
|
32478
|
-
const formatHubLink = (input) => encodeStoredHubLink({
|
|
32479
|
-
baseUrl: Option.match(input.settings, {
|
|
32480
|
-
onNone: () => "",
|
|
32481
|
-
onSome: (settings) => trimTrailingSlash(settings.baseUrl)
|
|
32482
|
-
}),
|
|
32483
|
-
token: Option.match(input.settings, {
|
|
32484
|
-
onNone: () => "",
|
|
32485
|
-
onSome: (s) => s.token
|
|
32486
|
-
}),
|
|
32487
|
-
syncMode: input.syncMode,
|
|
32488
|
-
shareMode: input.shareMode
|
|
32489
|
-
});
|
|
32490
|
-
const fromEnvironment = (environment) => validateHubLink(environment.P4CODE_HUB_URL ?? "", environment.P4CODE_HUB_TOKEN ?? "");
|
|
32491
|
-
const make$109 = Effect.fn("HubLink.make")(function* (environment) {
|
|
32492
|
-
const secrets = yield* ServerSecretStore;
|
|
32493
|
-
const env = environment ?? process.env;
|
|
32494
|
-
const fromEnv = fromEnvironment(env);
|
|
32495
|
-
const readStored = Effect.fn("HubLink.readStored")(function* () {
|
|
32496
|
-
const stored = yield* secrets.get(HUB_LINK_SECRET_NAME).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
32497
|
-
return Option.match(stored, {
|
|
32498
|
-
onNone: () => ({
|
|
32499
|
-
settings: Option.none(),
|
|
32500
|
-
syncMode: "auto",
|
|
32501
|
-
shareMode: "all"
|
|
32502
|
-
}),
|
|
32503
|
-
onSome: (bytes) => parseHubLink(new TextDecoder().decode(bytes))
|
|
32504
|
-
});
|
|
32505
|
-
});
|
|
32506
|
-
const toState = (record) => Option.isSome(fromEnv) ? {
|
|
32507
|
-
settings: fromEnv,
|
|
32508
|
-
syncMode: record.syncMode,
|
|
32509
|
-
shareMode: record.shareMode,
|
|
32510
|
-
source: "environment"
|
|
32511
|
-
} : {
|
|
32512
|
-
settings: record.settings,
|
|
32513
|
-
syncMode: record.syncMode,
|
|
32514
|
-
shareMode: record.shareMode,
|
|
32515
|
-
source: Option.isSome(record.settings) ? "stored" : "none"
|
|
32516
|
-
};
|
|
32517
|
-
const cache = yield* Ref.make(yield* readStored());
|
|
32518
|
-
const updates = yield* PubSub.sliding({
|
|
32519
|
-
capacity: 1,
|
|
32520
|
-
replay: 1
|
|
32521
|
-
});
|
|
32522
|
-
yield* PubSub.publish(updates, toState(yield* Ref.get(cache)));
|
|
32523
|
-
const persist = Effect.fn("HubLink.persist")(function* (record) {
|
|
32524
|
-
yield* secrets.set(HUB_LINK_SECRET_NAME, new TextEncoder().encode(formatHubLink(record)));
|
|
32525
|
-
yield* Ref.set(cache, record);
|
|
32526
|
-
yield* PubSub.publish(updates, toState(record));
|
|
32527
|
-
return toState(record);
|
|
32528
|
-
});
|
|
32529
|
-
return {
|
|
32530
|
-
current: Ref.get(cache).pipe(Effect.map(toState)),
|
|
32531
|
-
changes: Stream.fromPubSub(updates),
|
|
32532
|
-
connect: (settings) => Effect.gen(function* () {
|
|
32533
|
-
const validated = validateHubLink(settings.baseUrl, settings.token);
|
|
32534
|
-
if (Option.isNone(validated)) return yield* new InvalidHubLink({ detail: "A hub needs an http or https URL and a non-empty token." });
|
|
32535
|
-
const record = yield* Ref.get(cache);
|
|
32536
|
-
return yield* persist({
|
|
32537
|
-
...record,
|
|
32538
|
-
settings: validated
|
|
32539
|
-
});
|
|
32540
|
-
}),
|
|
32541
|
-
disconnect: Effect.gen(function* () {
|
|
32542
|
-
const record = yield* Ref.get(cache);
|
|
32543
|
-
return yield* persist({
|
|
32544
|
-
...record,
|
|
32545
|
-
settings: Option.none()
|
|
32546
|
-
});
|
|
32547
|
-
}),
|
|
32548
|
-
setSyncMode: (mode) => Effect.gen(function* () {
|
|
32549
|
-
const record = yield* Ref.get(cache);
|
|
32550
|
-
return yield* persist({
|
|
32551
|
-
...record,
|
|
32552
|
-
syncMode: mode
|
|
32553
|
-
});
|
|
32554
|
-
}),
|
|
32555
|
-
setShareMode: (mode) => Effect.gen(function* () {
|
|
32556
|
-
const record = yield* Ref.get(cache);
|
|
32557
|
-
return yield* persist({
|
|
32558
|
-
...record,
|
|
32559
|
-
shareMode: mode
|
|
32560
|
-
});
|
|
32561
|
-
})
|
|
32562
|
-
};
|
|
32563
|
-
});
|
|
32564
|
-
const layer$90 = Layer.effect(HubLink, make$109());
|
|
32565
|
-
//#endregion
|
|
32566
32901
|
//#region src/sync/HubAssetClient.ts
|
|
32567
32902
|
/**
|
|
32568
32903
|
* The server's half of the hub's asset API.
|
|
@@ -32576,7 +32911,7 @@ const layer$90 = Layer.effect(HubLink, make$109());
|
|
|
32576
32911
|
*
|
|
32577
32912
|
* @module sync/HubAssetClient
|
|
32578
32913
|
*/
|
|
32579
|
-
const HUB_REQUEST_TIMEOUT_MS
|
|
32914
|
+
const HUB_REQUEST_TIMEOUT_MS = 15e3;
|
|
32580
32915
|
/**
|
|
32581
32916
|
* The hub could not be reached, or answered something unusable.
|
|
32582
32917
|
*
|
|
@@ -32589,9 +32924,9 @@ var HubAssetConflict = class extends Data.TaggedError("HubAssetConflict") {};
|
|
|
32589
32924
|
/** No hub is configured on this server, so there is nothing to talk to. */
|
|
32590
32925
|
var HubNotConfigured = class extends Data.TaggedError("HubNotConfigured") {};
|
|
32591
32926
|
const AssetListPage = Schema$1.Struct({ assets: Schema$1.Array(AgentAsset) });
|
|
32592
|
-
const ConflictBody
|
|
32927
|
+
const ConflictBody = Schema$1.Struct({ current: Schema$1.NullOr(AgentAsset) });
|
|
32593
32928
|
const decodeAssetListPage = Schema$1.decodeUnknownEffect(AssetListPage);
|
|
32594
|
-
const decodeConflictBody
|
|
32929
|
+
const decodeConflictBody = Schema$1.decodeUnknownEffect(ConflictBody);
|
|
32595
32930
|
const decodeAsset = Schema$1.decodeUnknownEffect(AgentAsset);
|
|
32596
32931
|
var HubAssetClient = class extends Context.Service()("@p4code/cli/sync/HubAssetClient") {};
|
|
32597
32932
|
const make$108 = Effect.gen(function* () {
|
|
@@ -32603,7 +32938,7 @@ const make$108 = Effect.gen(function* () {
|
|
|
32603
32938
|
return state.settings.value;
|
|
32604
32939
|
});
|
|
32605
32940
|
const send = Effect.fn("HubAssetClient.send")(function* (operation, settings, request) {
|
|
32606
|
-
return yield* http.execute(request.pipe(HttpClientRequest.setHeader("authorization", `Bearer ${settings.token}`), HttpClientRequest.setHeader("accept", "application/json"))).pipe(Effect.timeout(HUB_REQUEST_TIMEOUT_MS
|
|
32941
|
+
return yield* http.execute(request.pipe(HttpClientRequest.setHeader("authorization", `Bearer ${settings.token}`), HttpClientRequest.setHeader("accept", "application/json"))).pipe(Effect.timeout(HUB_REQUEST_TIMEOUT_MS), Effect.mapError((cause) => new HubUnavailable({
|
|
32607
32942
|
operation,
|
|
32608
32943
|
detail: `${cause._tag ?? "error"}`
|
|
32609
32944
|
})));
|
|
@@ -32623,7 +32958,7 @@ const make$108 = Effect.gen(function* () {
|
|
|
32623
32958
|
* which is what lets a caller show both sides instead of only saying no.
|
|
32624
32959
|
*/
|
|
32625
32960
|
const conflictFrom = Effect.fn("HubAssetClient.conflictFrom")(function* (name, response) {
|
|
32626
|
-
const body = yield* decodeBody("conflict", decodeConflictBody
|
|
32961
|
+
const body = yield* decodeBody("conflict", decodeConflictBody, response).pipe(Effect.orElseSucceed(() => ({ current: null })));
|
|
32627
32962
|
return new HubAssetConflict({
|
|
32628
32963
|
name,
|
|
32629
32964
|
current: body.current === null ? Option.none() : Option.some(body.current)
|
|
@@ -34849,7 +35184,7 @@ const findAgentPreset = (requested) => {
|
|
|
34849
35184
|
};
|
|
34850
35185
|
//#endregion
|
|
34851
35186
|
//#region src/cli/assets.ts
|
|
34852
|
-
const SyncServicesLive = layer$88.pipe(Layer.provideMerge(layer$89), Layer.provideMerge(layer$
|
|
35187
|
+
const SyncServicesLive = layer$88.pipe(Layer.provideMerge(layer$89), Layer.provideMerge(layer$95), Layer.provideMerge(layer$93.pipe(Layer.provide(layer$120))), Layer.provideMerge(layer$120), Layer.provideMerge(FetchHttpClient.layer));
|
|
34853
35188
|
const runWithSync = (flags, run) => Effect.gen(function* () {
|
|
34854
35189
|
const config = yield* resolveCliAuthConfig(flags, yield* GlobalFlag.LogLevel);
|
|
34855
35190
|
return yield* Effect.gen(function* () {
|
|
@@ -51250,7 +51585,7 @@ const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* (input)
|
|
|
51250
51585
|
yield* (yield* initialize).refresh;
|
|
51251
51586
|
if (access === "agent" && !state.agentAccessEnabled) return yield* new DeviceOperationError({
|
|
51252
51587
|
operation: "list",
|
|
51253
|
-
reason: "
|
|
51588
|
+
reason: "agent_access_disabled",
|
|
51254
51589
|
cause: "Agent device access is disabled."
|
|
51255
51590
|
});
|
|
51256
51591
|
const ids = enabled ? state.hosts.map((host) => host.id) : [];
|
|
@@ -57639,6 +57974,7 @@ const requireThreadSpawn = Effect.fn("mcp.requireThreadSpawn")(function* (fusion
|
|
|
57639
57974
|
//#region src/mcp/DeviceInvocation.ts
|
|
57640
57975
|
const isDeviceToolUnavailableError = Schema$1.is(DeviceToolUnavailableError);
|
|
57641
57976
|
const isDeviceActionUnavailableError = Schema$1.is(DeviceActionUnavailableError);
|
|
57977
|
+
const isDeviceOperationError = Schema$1.is(DeviceOperationError);
|
|
57642
57978
|
/** Internal bridge; tool payloads never choose thread identity or daemon credentials. */
|
|
57643
57979
|
const withDeviceInvocation = (execute, capability = "devices") => Effect.gen(function* () {
|
|
57644
57980
|
const invocation = yield* McpInvocationContext;
|
|
@@ -57646,7 +57982,7 @@ const withDeviceInvocation = (execute, capability = "devices") => Effect.gen(fun
|
|
|
57646
57982
|
const lifetime = invocation.authorizationLifetime;
|
|
57647
57983
|
const denied = () => new DeviceToolUnavailableError({ reason: "Device command unavailable or authorization changed." });
|
|
57648
57984
|
if (!invocation.capabilities.has(capability) || !lifetime?.revocation || lifetime.revoked) return yield* denied();
|
|
57649
|
-
const work = execute(service, invocation.threadId).pipe(Effect.tap(() => lifetime.revoked ? Effect.fail(denied()) : Effect.void), Effect.mapError((error) => isDeviceToolUnavailableError(error) || isDeviceActionUnavailableError(error) ? error : denied()));
|
|
57985
|
+
const work = execute(service, invocation.threadId).pipe(Effect.tap(() => lifetime.revoked ? Effect.fail(denied()) : Effect.void), Effect.mapError((error) => isDeviceToolUnavailableError(error) || isDeviceActionUnavailableError(error) ? error : isDeviceOperationError(error) && error.reason === "agent_access_disabled" ? new DeviceToolUnavailableError({ reason: error.message }) : denied()));
|
|
57650
57986
|
return yield* Effect.raceFirst(work, Deferred.await(lifetime.revocation).pipe(Effect.andThen(Effect.fail(denied()))));
|
|
57651
57987
|
});
|
|
57652
57988
|
const withDeviceSession = (target, execute) => withDeviceInvocation((service, threadId) => service.withAgentCommand({
|
|
@@ -58879,18 +59215,6 @@ const acceptBunDeviceSocket = Effect.fn("DeviceHubBunSocket.accept")(function* (
|
|
|
58879
59215
|
}))), MAX_DEVICE_INPUT_BYTES, MAX_DEVICE_OUTPUT_BYTES);
|
|
58880
59216
|
});
|
|
58881
59217
|
//#endregion
|
|
58882
|
-
//#region src/httpCompression/HttpResponseCompression.ts
|
|
58883
|
-
var HttpResponseCompression = class extends Context.Service()("@p4code/cli/httpCompression/HttpResponseCompression") {};
|
|
58884
|
-
const layerNode = Layer.effect(HttpResponseCompression, Effect.gen(function* () {
|
|
58885
|
-
const NodeZlib = yield* Effect.promise(() => import("node:zlib"));
|
|
58886
|
-
return { gzip: (body, options) => {
|
|
58887
|
-
const stream = NodeZlib.createGzip();
|
|
58888
|
-
stream.end(body);
|
|
58889
|
-
return HttpServerResponse$1.raw(stream, options);
|
|
58890
|
-
} };
|
|
58891
|
-
}));
|
|
58892
|
-
const layerBun = Layer.succeed(HttpResponseCompression, { gzip: (body, options) => HttpServerResponse$1.raw(new Response(body).body.pipeThrough(new CompressionStream("gzip")), options) });
|
|
58893
|
-
//#endregion
|
|
58894
59218
|
//#region ../../packages/shared/src/devProxy.ts
|
|
58895
59219
|
/**
|
|
58896
59220
|
* Backend paths the web dev server proxies in single-origin browser dev.
|
|
@@ -60562,7 +60886,6 @@ const LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
|
60562
60886
|
"localhost"
|
|
60563
60887
|
]);
|
|
60564
60888
|
const DESKTOP_RENDERER_ORIGINS = ["p4code://app", "p4code-dev://app"];
|
|
60565
|
-
const GZIP_MIN_BYTES = 1024;
|
|
60566
60889
|
const SVG_CONTENT_SECURITY_POLICY = "default-src 'none'; style-src 'unsafe-inline'; sandbox";
|
|
60567
60890
|
function assetResponseHeaders(filePath) {
|
|
60568
60891
|
return {
|
|
@@ -60571,36 +60894,6 @@ function assetResponseHeaders(filePath) {
|
|
|
60571
60894
|
...filePath.toLowerCase().endsWith(".svg") ? { "Content-Security-Policy": SVG_CONTENT_SECURITY_POLICY } : {}
|
|
60572
60895
|
};
|
|
60573
60896
|
}
|
|
60574
|
-
function acceptsGzip(value) {
|
|
60575
|
-
if (!value) return false;
|
|
60576
|
-
const accepted = new Map(value.split(",").map((entry) => {
|
|
60577
|
-
const [coding = "", ...parameters] = entry.trim().toLowerCase().split(";");
|
|
60578
|
-
const quality = parameters.map((parameter) => parameter.trim().match(/^q=(.+)$/)?.[1]).find((parameter) => parameter !== void 0);
|
|
60579
|
-
return [coding, quality === void 0 ? 1 : Number(quality)];
|
|
60580
|
-
}));
|
|
60581
|
-
return (accepted.get("gzip") ?? accepted.get("*") ?? 0) > 0;
|
|
60582
|
-
}
|
|
60583
|
-
function varyByAcceptEncoding(value) {
|
|
60584
|
-
if (!value) return "Accept-Encoding";
|
|
60585
|
-
const values = new Set(value.split(",").map((entry) => entry.trim().toLowerCase()));
|
|
60586
|
-
return values.has("*") || values.has("accept-encoding") ? value : `${value}, Accept-Encoding`;
|
|
60587
|
-
}
|
|
60588
|
-
const compressHttpResponse = Effect.fnUntraced(function* (response, acceptEncoding) {
|
|
60589
|
-
const body = response.body;
|
|
60590
|
-
if (body._tag !== "Uint8Array" || body.contentLength < GZIP_MIN_BYTES || !body.contentType.startsWith("application/json") || response.headers["content-encoding"]) return response;
|
|
60591
|
-
const variedResponse = HttpServerResponse.setHeader(response, "vary", varyByAcceptEncoding(response.headers.vary));
|
|
60592
|
-
if (!acceptsGzip(acceptEncoding)) return variedResponse;
|
|
60593
|
-
const compression = yield* HttpResponseCompression;
|
|
60594
|
-
const headers = Headers$1.set(Headers$1.remove(variedResponse.headers, "content-length"), "content-encoding", "gzip");
|
|
60595
|
-
return compression.gzip(body.body, {
|
|
60596
|
-
status: response.status,
|
|
60597
|
-
statusText: response.statusText,
|
|
60598
|
-
headers,
|
|
60599
|
-
cookies: response.cookies,
|
|
60600
|
-
contentType: body.contentType
|
|
60601
|
-
});
|
|
60602
|
-
});
|
|
60603
|
-
const httpCompressionLayer = HttpRouter.middleware((httpEffect) => Effect.flatMap(Effect.all([httpEffect, HttpServerRequest.HttpServerRequest]), ([response, request]) => compressHttpResponse(response, request.headers["accept-encoding"])), { global: true });
|
|
60604
60897
|
const browserApiCorsLayer = Layer.unwrap(Effect.gen(function* () {
|
|
60605
60898
|
const config = yield* ServerConfig$1;
|
|
60606
60899
|
const devOrigin = config.devUrl?.origin;
|
|
@@ -69433,6 +69726,13 @@ const NARRATE_BEFORE_TOOLS_PROMPT = "Before each turn's first tool call, state n
|
|
|
69433
69726
|
*/
|
|
69434
69727
|
const SUBAGENTS_ALLOWED_PROMPT = "You may spawn Task subagents without asking; this overrides earlier request-only rules. Use for bounded search/review whose result is far smaller than required reading. Single-file answer/edit stays inline.";
|
|
69435
69728
|
const SUBAGENTS_ON_REQUEST_PROMPT = "Never spawn Task subagents unless user asks; this overrides earlier proactive permission. Work inline; say when delegation would have been better.";
|
|
69729
|
+
/**
|
|
69730
|
+
* Appended to ordinary (non-Fusion) sessions. Fusion role prompts already tell
|
|
69731
|
+
* both roles to follow applicable instructions and load memories and skills;
|
|
69732
|
+
* without the same line an ordinary thread treats the user's memory store as
|
|
69733
|
+
* optional background it may skip.
|
|
69734
|
+
*/
|
|
69735
|
+
const INSTRUCTIONS_AND_MEMORY_PROMPT = "Follow applicable global and project instructions, and load relevant memories and skills on demand. When ~/.agent-memory exists, read its INDEX.md and the entries it points you to before acting.";
|
|
69436
69736
|
//#endregion
|
|
69437
69737
|
//#region src/provider/Layers/ClaudeAdapter.ts
|
|
69438
69738
|
/**
|
|
@@ -70108,6 +70408,22 @@ const CLAUDE_SETTING_SOURCES = [
|
|
|
70108
70408
|
"project",
|
|
70109
70409
|
"local"
|
|
70110
70410
|
];
|
|
70411
|
+
/**
|
|
70412
|
+
* Claude user-scope MCP servers this session already receives under their
|
|
70413
|
+
* `p4-user-` alias, as `deniedMcpServers` entries.
|
|
70414
|
+
*
|
|
70415
|
+
* The `user` setting source still loads the originals from `~/.claude.json`,
|
|
70416
|
+
* where Claude has no credential for a server p4code authorized. An OAuth
|
|
70417
|
+
* server then appears twice, once working and once as "needs authentication",
|
|
70418
|
+
* and the agent believes the service is unavailable. Denying each original by
|
|
70419
|
+
* name leaves the working alias as the only copy. The deny matches by name in
|
|
70420
|
+
* every Claude scope, so a same-named project server is dropped too.
|
|
70421
|
+
*/
|
|
70422
|
+
function claudeShadowedUserScopeMcpServers(sessionServerNames) {
|
|
70423
|
+
const prefix = mcpUserScopeSessionName("");
|
|
70424
|
+
const names = new Set(sessionServerNames);
|
|
70425
|
+
return sessionServerNames.filter((name) => name.startsWith(prefix)).map((name) => name.slice(prefix.length)).filter((original) => original.length > 0 && !names.has(original)).map((serverName) => ({ serverName }));
|
|
70426
|
+
}
|
|
70111
70427
|
function buildPromptText(input, boundInstanceId, customModels = [], manifest) {
|
|
70112
70428
|
const rawEffort = input.modelSelection?.instanceId === boundInstanceId ? getModelSelectionStringOptionValue(input.modelSelection, "effort") : null;
|
|
70113
70429
|
const caps = getClaudeModelCapabilities(input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection.model : void 0, customModels, manifest);
|
|
@@ -72143,14 +72459,16 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
72143
72459
|
}[input.runtimeMode];
|
|
72144
72460
|
const disabledSkills = options?.resolveDisabledSkills === void 0 ? [] : yield* options.resolveDisabledSkills;
|
|
72145
72461
|
const skillOverrides = disabledSkills.length === 0 ? void 0 : buildSkillOverrides(disabledSkills, yield* discoverClaudeSkills(claudeSettings, input.cwd, claudeEnvironment).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path$1.Path, path)));
|
|
72462
|
+
const mcpSession = readMcpProviderSession(input.threadId);
|
|
72463
|
+
const registeredMcpServers = Object.fromEntries(Object.entries(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers(input.cwd)).filter(([name]) => !providerMcpReservedNames().has(name)));
|
|
72464
|
+
const deniedMcpServers = claudeShadowedUserScopeMcpServers([...Object.keys(registeredMcpServers), ...providerMcpReservedNames()]);
|
|
72146
72465
|
const settings = {
|
|
72147
72466
|
...typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {},
|
|
72148
72467
|
...fastMode ? { fastMode: true } : {},
|
|
72149
72468
|
...ultracode ? { ultracode: true } : {},
|
|
72150
|
-
...skillOverrides ? { skillOverrides } : {}
|
|
72469
|
+
...skillOverrides ? { skillOverrides } : {},
|
|
72470
|
+
...deniedMcpServers.length > 0 ? { deniedMcpServers } : {}
|
|
72151
72471
|
};
|
|
72152
|
-
const mcpSession = readMcpProviderSession(input.threadId);
|
|
72153
|
-
const registeredMcpServers = Object.fromEntries(Object.entries(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers(input.cwd)).filter(([name]) => !providerMcpReservedNames().has(name)));
|
|
72154
72472
|
const narrateBeforeTools = options?.resolveToolCallNarration === void 0 ? DEFAULT_SERVER_SETTINGS.enableToolCallNarration : yield* options.resolveToolCallNarration;
|
|
72155
72473
|
const guardrailSettings = options?.resolveGuardrailPrompts === void 0 ? {
|
|
72156
72474
|
enableVerificationBeforeCompletion: DEFAULT_SERVER_SETTINGS.enableVerificationBeforeCompletion,
|
|
@@ -72171,7 +72489,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
72171
72489
|
...narrateBeforeTools ? [NARRATE_BEFORE_TOOLS_PROMPT] : [],
|
|
72172
72490
|
...guardrailPromptsFor(guardrailSettings),
|
|
72173
72491
|
unpromptedSubagents ? SUBAGENTS_ALLOWED_PROMPT : SUBAGENTS_ON_REQUEST_PROMPT,
|
|
72174
|
-
...input.fusionRole !== void 0 ? [fusionRoleInstructionsWith(input.fusionRole, "claudeAgent", fusionPromptSettings)] : [],
|
|
72492
|
+
...input.fusionRole !== void 0 ? [fusionRoleInstructionsWith(input.fusionRole, "claudeAgent", fusionPromptSettings)] : [INSTRUCTIONS_AND_MEMORY_PROMPT],
|
|
72175
72493
|
...fusionLeanContextApplies(fusionContextPolicy) ? [FUSION_SAFETY_KERNEL] : [],
|
|
72176
72494
|
...compressRuleset !== void 0 ? [compressRuleset] : []
|
|
72177
72495
|
].join("\n\n");
|
|
@@ -109238,7 +109556,7 @@ const COMMON_DEV_PORTS = Object.freeze([
|
|
|
109238
109556
|
8888,
|
|
109239
109557
|
9e3
|
|
109240
109558
|
]);
|
|
109241
|
-
const POLL_INTERVAL$
|
|
109559
|
+
const POLL_INTERVAL$3 = Duration.seconds(3);
|
|
109242
109560
|
const LSOF_TIMEOUT_MS = 5e3;
|
|
109243
109561
|
const WINDOWS_LISTENER_TIMEOUT_MS = 5e3;
|
|
109244
109562
|
const terminalOwnerKey = (owner) => `${owner.threadId}\u0000${owner.terminalId}`;
|
|
@@ -109413,7 +109731,7 @@ const make$61 = Effect.gen(function* PortDiscoveryMake() {
|
|
|
109413
109731
|
lastSnapshot: next
|
|
109414
109732
|
}])) yield* broadcast(next);
|
|
109415
109733
|
}, Effect.catchCause((cause) => Effect.logWarning("preview port scan failed", Cause.pretty(cause))));
|
|
109416
|
-
yield* Effect.forkScoped(pollTick().pipe(Effect.repeat(Schedule.spaced(POLL_INTERVAL$
|
|
109734
|
+
yield* Effect.forkScoped(pollTick().pipe(Effect.repeat(Schedule.spaced(POLL_INTERVAL$3))));
|
|
109417
109735
|
const acquireRetention = Effect.fn("PortDiscovery.retain")(function* () {
|
|
109418
109736
|
if (yield* Ref.modify(stateRef, (state) => [state.retainCount === 0, {
|
|
109419
109737
|
...state,
|
|
@@ -124922,7 +125240,7 @@ const LINEAR_ISSUE_FIELDS = [
|
|
|
124922
125240
|
* a Linear workspace is edited by people at human speed rather than by agents
|
|
124923
125241
|
* at machine speed.
|
|
124924
125242
|
*/
|
|
124925
|
-
const POLL_INTERVAL$
|
|
125243
|
+
const POLL_INTERVAL$2 = Duration.seconds(30);
|
|
124926
125244
|
/** Linear's cap on one page. Asking for more is an error, not a bigger page. */
|
|
124927
125245
|
const LINEAR_PAGE_LIMIT = 250;
|
|
124928
125246
|
const LINEAR_LIST_TOOL = "list_issues";
|
|
@@ -125175,7 +125493,7 @@ const makeLinearTaskRepository = Effect.gen(function* () {
|
|
|
125175
125493
|
taskId
|
|
125176
125494
|
});
|
|
125177
125495
|
}).pipe(Effect.ignoreCause({ log: true }));
|
|
125178
|
-
yield* Effect.forever(pollOnce.pipe(Effect.andThen(Effect.sleep(POLL_INTERVAL$
|
|
125496
|
+
yield* Effect.forever(pollOnce.pipe(Effect.andThen(Effect.sleep(POLL_INTERVAL$2)))).pipe(Effect.forkScoped);
|
|
125179
125497
|
return {
|
|
125180
125498
|
create,
|
|
125181
125499
|
upsert,
|
|
@@ -125199,127 +125517,6 @@ const makeLinearTaskRepository = Effect.gen(function* () {
|
|
|
125199
125517
|
};
|
|
125200
125518
|
});
|
|
125201
125519
|
//#endregion
|
|
125202
|
-
//#region src/persistence/Layers/RemoteTasks.ts
|
|
125203
|
-
/**
|
|
125204
|
-
* The hub's task API, as a client.
|
|
125205
|
-
*
|
|
125206
|
-
* This used to be a `TaskRepository` in its own right, and the board was
|
|
125207
|
-
* either this or the local database. It is now the transport underneath
|
|
125208
|
-
* `SyncedTaskRepository`: local SQLite is a replica of the hub, so nothing
|
|
125209
|
-
* reads from here directly and the board no longer goes dark when the hub is
|
|
125210
|
-
* unreachable. What changed with it is the failure story — an unreachable hub
|
|
125211
|
-
* used to mean no board, and now means a board that is behind.
|
|
125212
|
-
*
|
|
125213
|
-
* The four calls here are what a mirror needs and no more: read the hub's
|
|
125214
|
-
* rows, push a whole row, arbitrate a claim, and delete. There is no partial
|
|
125215
|
-
* patch, because a mirror always holds the full row it wants the hub to have
|
|
125216
|
-
* and sending a diff of it would only add a way for the two to disagree.
|
|
125217
|
-
*
|
|
125218
|
-
* @module persistence/Layers/RemoteTasks
|
|
125219
|
-
*/
|
|
125220
|
-
const HUB_REQUEST_TIMEOUT_MS = 15e3;
|
|
125221
|
-
const TaskListPage = Schema$1.Struct({ tasks: Schema$1.Array(Task) });
|
|
125222
|
-
const ConflictBody = Schema$1.Struct({ current: Task });
|
|
125223
|
-
const decodeTaskListPage = Schema$1.decodeUnknownEffect(TaskListPage);
|
|
125224
|
-
const decodeTask = Schema$1.decodeUnknownEffect(Task);
|
|
125225
|
-
const decodeConflictBody = Schema$1.decodeUnknownEffect(ConflictBody);
|
|
125226
|
-
/**
|
|
125227
|
-
* Every failure here is reported as the persistence error type.
|
|
125228
|
-
*
|
|
125229
|
-
* The mirror catches these and carries on — a failed push leaves the row
|
|
125230
|
-
* `pending` and the next pass retries it — so what matters is that the detail
|
|
125231
|
-
* says the hub was the store that would not answer, which is what shows up in
|
|
125232
|
-
* the log when someone asks why their two machines disagree.
|
|
125233
|
-
*/
|
|
125234
|
-
const hubUnavailable = (operation, detail) => new PersistenceSqlError({
|
|
125235
|
-
operation: `HubTaskClient.${operation}`,
|
|
125236
|
-
detail: `The hub board is unavailable: ${detail}`,
|
|
125237
|
-
cause: null
|
|
125238
|
-
});
|
|
125239
|
-
var HubTaskClient = class extends Context.Service()("@p4code/cli/persistence/Layers/RemoteTasks/HubTaskClient") {};
|
|
125240
|
-
const makeHubTaskClient = Effect.gen(function* () {
|
|
125241
|
-
const http = yield* HttpClient.HttpClient;
|
|
125242
|
-
const link = yield* HubLink;
|
|
125243
|
-
const isLinked = link.current.pipe(Effect.map((state) => Option.isSome(state.settings)));
|
|
125244
|
-
const requireSettings = (operation) => Effect.gen(function* () {
|
|
125245
|
-
const state = yield* link.current;
|
|
125246
|
-
if (Option.isNone(state.settings)) return yield* hubUnavailable(operation, "no hub is configured on this server");
|
|
125247
|
-
return state.settings.value;
|
|
125248
|
-
});
|
|
125249
|
-
const send = (operation, settings, request) => http.execute(request.pipe(HttpClientRequest.setHeader("authorization", `Bearer ${settings.token}`), HttpClientRequest.setHeader("accept", "application/json"))).pipe(Effect.timeout(HUB_REQUEST_TIMEOUT_MS), Effect.mapError((cause) => hubUnavailable(operation, `${cause._tag ?? "request failed"}`)));
|
|
125250
|
-
const list = (filter) => Effect.gen(function* () {
|
|
125251
|
-
const settings = yield* requireSettings("list");
|
|
125252
|
-
const query = new URLSearchParams();
|
|
125253
|
-
for (const key of [
|
|
125254
|
-
"projectId",
|
|
125255
|
-
"repositoryKey",
|
|
125256
|
-
"threadId",
|
|
125257
|
-
"status",
|
|
125258
|
-
"assignee",
|
|
125259
|
-
"query"
|
|
125260
|
-
]) {
|
|
125261
|
-
const value = filter[key];
|
|
125262
|
-
if (value !== void 0) query.set(key, value);
|
|
125263
|
-
}
|
|
125264
|
-
const suffix = query.size === 0 ? "" : `?${query.toString()}`;
|
|
125265
|
-
const response = yield* send("list", settings, HttpClientRequest.get(`${settings.baseUrl}/tasks${suffix}`));
|
|
125266
|
-
if (response.status !== 200) return yield* hubUnavailable("list", `status ${response.status}`);
|
|
125267
|
-
return (yield* response.json.pipe(Effect.flatMap(decodeTaskListPage), Effect.mapError(() => hubUnavailable("list", "the response did not match the contract")))).tasks;
|
|
125268
|
-
});
|
|
125269
|
-
const put = (row, readableIdPrefix) => Effect.gen(function* () {
|
|
125270
|
-
const settings = yield* requireSettings("put");
|
|
125271
|
-
const response = yield* send("put", settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.put(`${settings.baseUrl}/tasks/${encodeURIComponent(row.taskId)}`), {
|
|
125272
|
-
projectId: row.projectId,
|
|
125273
|
-
repositoryKey: row.repositoryKey,
|
|
125274
|
-
threadId: row.threadId,
|
|
125275
|
-
parentTaskId: row.parentTaskId,
|
|
125276
|
-
title: row.title,
|
|
125277
|
-
body: row.body,
|
|
125278
|
-
status: row.status,
|
|
125279
|
-
priority: row.priority,
|
|
125280
|
-
assignee: row.assignee,
|
|
125281
|
-
labels: row.labels,
|
|
125282
|
-
orderKey: row.orderKey,
|
|
125283
|
-
createdAt: row.createdAt,
|
|
125284
|
-
statusChangedAt: row.statusChangedAt ?? row.updatedAt,
|
|
125285
|
-
readableIdPrefix
|
|
125286
|
-
}));
|
|
125287
|
-
if (response.status !== 200 && response.status !== 201) return yield* hubUnavailable("put", `status ${response.status}`);
|
|
125288
|
-
return yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable("put", "the response did not match the contract")));
|
|
125289
|
-
});
|
|
125290
|
-
const patchIfStatus = (input) => Effect.gen(function* () {
|
|
125291
|
-
const settings = yield* requireSettings("patchIfStatus");
|
|
125292
|
-
const { taskId, ...fields } = input.patch;
|
|
125293
|
-
const response = yield* send("patchIfStatus", settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.patch(`${settings.baseUrl}/tasks/${encodeURIComponent(taskId)}`), {
|
|
125294
|
-
...fields,
|
|
125295
|
-
expectedStatus: input.expectedStatus
|
|
125296
|
-
}));
|
|
125297
|
-
if (response.status === 404) return { outcome: "missing" };
|
|
125298
|
-
if (response.status === 409) return {
|
|
125299
|
-
outcome: "conflict",
|
|
125300
|
-
current: (yield* response.json.pipe(Effect.flatMap(decodeConflictBody), Effect.mapError(() => hubUnavailable("patchIfStatus", "the conflict response did not match the contract")))).current
|
|
125301
|
-
};
|
|
125302
|
-
if (response.status !== 200) return yield* hubUnavailable("patchIfStatus", `status ${response.status}`);
|
|
125303
|
-
return {
|
|
125304
|
-
outcome: "updated",
|
|
125305
|
-
task: yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable("patchIfStatus", "the response did not match the contract")))
|
|
125306
|
-
};
|
|
125307
|
-
});
|
|
125308
|
-
const remove = (taskId) => Effect.gen(function* () {
|
|
125309
|
-
const settings = yield* requireSettings("remove");
|
|
125310
|
-
const response = yield* send("remove", settings, HttpClientRequest.delete(`${settings.baseUrl}/tasks/${encodeURIComponent(taskId)}`));
|
|
125311
|
-
if (response.status !== 200 && response.status !== 204 && response.status !== 404) return yield* hubUnavailable("remove", `status ${response.status}`);
|
|
125312
|
-
});
|
|
125313
|
-
return {
|
|
125314
|
-
isLinked,
|
|
125315
|
-
list,
|
|
125316
|
-
put,
|
|
125317
|
-
patchIfStatus,
|
|
125318
|
-
remove
|
|
125319
|
-
};
|
|
125320
|
-
});
|
|
125321
|
-
Layer.effect(HubTaskClient, makeHubTaskClient);
|
|
125322
|
-
//#endregion
|
|
125323
125520
|
//#region src/persistence/Layers/TaskSyncStore.ts
|
|
125324
125521
|
/**
|
|
125325
125522
|
* The bookkeeping a mirror needs and a board does not.
|
|
@@ -125516,14 +125713,17 @@ const makeTaskSyncStore = Effect.gen(function* () {
|
|
|
125516
125713
|
* @module persistence/Layers/SyncedTasks
|
|
125517
125714
|
*/
|
|
125518
125715
|
/**
|
|
125519
|
-
* How stale another machine's write may look here.
|
|
125716
|
+
* How stale another machine's write may look here when the hub cannot push.
|
|
125520
125717
|
*
|
|
125521
|
-
*
|
|
125522
|
-
*
|
|
125523
|
-
*
|
|
125524
|
-
*
|
|
125718
|
+
* With the hub's change stream connected, a write elsewhere triggers a pull
|
|
125719
|
+
* right away and the timer is only a safety net, so it can be slow. Without
|
|
125720
|
+
* it (an older hub, or the stream is reconnecting) the timer is the only
|
|
125721
|
+
* signal, and a board left open should still follow along.
|
|
125525
125722
|
*/
|
|
125526
|
-
const
|
|
125723
|
+
const POLL_INTERVAL$1 = Duration.seconds(10);
|
|
125724
|
+
const PUSHED_POLL_INTERVAL = Duration.minutes(5);
|
|
125725
|
+
/** Between change-stream attempts, so an older hub's 404 is not hammered. */
|
|
125726
|
+
const WATCH_RETRY_DELAY = Duration.seconds(30);
|
|
125527
125727
|
const makeSyncedTaskRepository = Effect.gen(function* () {
|
|
125528
125728
|
const local = yield* makeTaskRepository;
|
|
125529
125729
|
const sync = yield* makeTaskSyncStore;
|
|
@@ -125610,6 +125810,11 @@ const makeSyncedTaskRepository = Effect.gen(function* () {
|
|
|
125610
125810
|
* that was just removed; a pull run before either would see this machine's
|
|
125611
125811
|
* own stale absence as the hub's opinion and undo local work.
|
|
125612
125812
|
*/
|
|
125813
|
+
/**
|
|
125814
|
+
* Where the last applied pull left off. Held in memory only: a restart pulls
|
|
125815
|
+
* the whole board once, then follows changes again.
|
|
125816
|
+
*/
|
|
125817
|
+
let position;
|
|
125613
125818
|
const syncOnce = Effect.gen(function* () {
|
|
125614
125819
|
if (!(yield* hub.isLinked)) return;
|
|
125615
125820
|
for (const taskId of yield* sync.listTombstones) yield* hub.remove(taskId).pipe(Effect.andThen(sync.clearTombstone(taskId)), Effect.ignoreCause({ log: true }));
|
|
@@ -125617,21 +125822,36 @@ const makeSyncedTaskRepository = Effect.gen(function* () {
|
|
|
125617
125822
|
const { readableIdPrefix, ...task } = row;
|
|
125618
125823
|
yield* pushInBackground(task, readableIdPrefix ?? "T");
|
|
125619
125824
|
}
|
|
125620
|
-
const
|
|
125825
|
+
const pulled = yield* hub.pull(position);
|
|
125826
|
+
if (Option.isNone(pulled)) return;
|
|
125827
|
+
const page = pulled.value;
|
|
125621
125828
|
const stillPending = new Set((yield* sync.listPending).map((row) => row.taskId));
|
|
125622
125829
|
const tombstoned = new Set(yield* sync.listTombstones);
|
|
125623
125830
|
const syncedIds = yield* sync.listSyncedIds;
|
|
125624
125831
|
const held = new Map((yield* local.list({})).map((task) => [task.taskId, task]));
|
|
125625
125832
|
const seen = /* @__PURE__ */ new Set();
|
|
125626
|
-
for (const task of
|
|
125833
|
+
for (const task of page.tasks) {
|
|
125627
125834
|
seen.add(task.taskId);
|
|
125628
125835
|
if (stillPending.has(task.taskId) || tombstoned.has(task.taskId)) continue;
|
|
125629
125836
|
const current = held.get(task.taskId);
|
|
125630
125837
|
if (current === void 0 || current.updatedAt !== task.updatedAt) yield* adopt(task);
|
|
125631
125838
|
}
|
|
125632
|
-
|
|
125839
|
+
const gone = page.full ? syncedIds.filter((taskId) => !seen.has(taskId)) : page.deletedTaskIds.filter((taskId) => syncedIds.includes(taskId));
|
|
125840
|
+
for (const taskId of gone) yield* local.deleteById({ taskId });
|
|
125841
|
+
position = page.next;
|
|
125633
125842
|
}).pipe(Effect.ignoreCause({ log: true }));
|
|
125634
|
-
yield*
|
|
125843
|
+
const nudges = yield* Queue.sliding(1);
|
|
125844
|
+
let watching = false;
|
|
125845
|
+
yield* hub.watch.pipe(Stream.runForEach(() => Effect.sync(() => {
|
|
125846
|
+
watching = true;
|
|
125847
|
+
}).pipe(Effect.andThen(Queue.offer(nudges, void 0)))), Effect.ignoreCause({ log: "Debug" }), Effect.andThen(Effect.sync(() => {
|
|
125848
|
+
watching = false;
|
|
125849
|
+
}).pipe(Effect.andThen(Queue.offer(nudges, void 0)))), Effect.andThen(Effect.sleep(WATCH_RETRY_DELAY)), Effect.forever, Effect.forkScoped);
|
|
125850
|
+
const nextPass = Effect.gen(function* () {
|
|
125851
|
+
const owed = (yield* sync.listPending).length > 0 || (yield* sync.listTombstones).length > 0;
|
|
125852
|
+
return yield* Queue.take(nudges).pipe(Effect.timeoutOption(watching && !owed ? PUSHED_POLL_INTERVAL : POLL_INTERVAL$1));
|
|
125853
|
+
}).pipe(Effect.catch(() => Effect.sleep(POLL_INTERVAL$1)));
|
|
125854
|
+
yield* Effect.forever(syncOnce.pipe(Effect.andThen(nextPass))).pipe(Effect.forkScoped);
|
|
125635
125855
|
return {
|
|
125636
125856
|
create,
|
|
125637
125857
|
upsert,
|
|
@@ -133683,7 +133903,7 @@ const ProviderAdapterRegistryLive = Layer.effect(ProviderAdapterRegistry, makePr
|
|
|
133683
133903
|
//#endregion
|
|
133684
133904
|
//#region src/provider/ComputerUsePrompts.ts
|
|
133685
133905
|
/** Routing guidance is injected only when the session has P4's Computer Use endpoint. */
|
|
133686
|
-
const COMPUTER_USE_ROUTING_PROMPT = "P4 Computer Use is available for this session through the cua_repl MCP server. When you use it for a native desktop app, do not substitute AppleScript or shell automation unless the user explicitly requests that path. For a named app, start directly with const app = await cua.getApp(exactNameOrBundleId); read the automatically returned state/image and stop if it answers the request. Reuse the handle and batch safe actions; do not repeat selection or unchanged state. For Device Hub, simulator, emulator, or connected-device requests, use cua.listDevices() once, then cua.getDevice({hostId,deviceId,platform}) with exact discovered IDs, unless the user explicitly requested a different route such as simctl. Selection already emits initial device state. Reuse its handle; do not print the handle or repeat that state before acting. Never substitute macOS getApp('Simulator'). Device hosts are scoped to this provider environment and can differ from the desktop host. If device discovery, opening, inspection, or interaction reports missing permission or unavailable control, stop P4 device interaction and report the exact limitation. Do not silently retry through screenshots, shell simctl, Xcode tools, or another MCP server; an explicit user request for one of those routes is separate authorization to use it. Do not call getState/getApp, control the P4 desktop app, or open device settings to work around a device API failure. Missing consent, unavailable devices, or unsupported operations are P4 limitations to report, never permission to switch P4 hosts or bypass the tool.";
|
|
133906
|
+
const COMPUTER_USE_ROUTING_PROMPT = "P4 Computer Use is available for this session through the cua_repl MCP server. When you use it for a native desktop app, do not substitute AppleScript or shell automation unless the user explicitly requests that path. For a named app, start directly with const app = await cua.getApp(exactNameOrBundleId); read the automatically returned state/image and stop if it answers the request. Reuse the handle and batch safe actions; do not repeat selection or unchanged state. For Device Hub, simulator, emulator, or connected-device requests, use cua.listDevices() once, then cua.getDevice({hostId,deviceId,platform}) with exact discovered IDs, unless the user explicitly requested a different route such as simctl. Selection already emits initial device state. Reuse its handle; do not print the handle or repeat that state before acting. Never substitute macOS getApp('Simulator'). Device hosts are scoped to this provider environment and can differ from the desktop host. If device discovery, opening, inspection, or interaction reports missing permission or unavailable control, stop P4 device interaction and report the exact limitation. Do not silently retry through screenshots, shell simctl, Xcode tools, or another MCP server; an explicit user request for one of those routes, including a standing fallback authorization in the user's global or project instructions, is separate authorization to use it; report the P4 error and the fallback you used. Do not call getState/getApp, control the P4 desktop app, or open device settings to work around a device API failure. Missing consent, unavailable devices, or unsupported operations are P4 limitations to report, never on their own permission to switch P4 hosts or bypass the tool.";
|
|
133687
133907
|
/** The routing note, naming the server the way this session's provider sees it. */
|
|
133688
133908
|
function withComputerUseRouting(input, enabled, serverName = "cua_repl") {
|
|
133689
133909
|
const prompt = serverName === "cua_repl" ? COMPUTER_USE_ROUTING_PROMPT : COMPUTER_USE_ROUTING_PROMPT.replace("the cua_repl MCP server", `the ${serverName} MCP server`);
|
|
@@ -148286,8 +148506,8 @@ const WorkspaceLayerLive = Layer.mergeAll(layer$71, WorkspaceEntriesLayerLive, W
|
|
|
148286
148506
|
const ProjectFaviconResolverLayerLive = layer$66.pipe(Layer.provide(layer$71), Layer.provide(layer$67));
|
|
148287
148507
|
const AuthLayerLive = layer$116.pipe(Layer.provideMerge(PersistenceLayerLive), Layer.provide(layer$120));
|
|
148288
148508
|
const ProviderRuntimeLayerLive = Layer.mergeAll(ProviderSessionReaperLive, ProviderHealthRefresherLive).pipe(Layer.provideMerge(ProviderLayerLive), Layer.provideMerge(OrchestrationLayerLive));
|
|
148289
|
-
const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(layer$28), Layer.provideMerge(Layer.mergeAll(PullRequestServiceLive, CheckpointingLayerLive, SourceControlProviderRegistryLayerLive)), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderAuthServiceLive), Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive, RoutedTaskRepositoryLive)), Layer.provideMerge(Layer.mergeAll(PersistenceLayerLive, layer$
|
|
148290
|
-
const UsageLayerLive = layer$25.pipe(Layer.provide(layer$
|
|
148509
|
+
const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(layer$28), Layer.provideMerge(Layer.mergeAll(PullRequestServiceLive, CheckpointingLayerLive, SourceControlProviderRegistryLayerLive)), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderAuthServiceLive), Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive, RoutedTaskRepositoryLive)), Layer.provideMerge(Layer.mergeAll(PersistenceLayerLive, layer$95)), Layer.provideMerge(layer$84), Layer.provideMerge(ProviderRegistryLive), Layer.provideMerge(layer$61), Layer.provideMerge(Layer.mergeAll(layer$60, layer$73.pipe(Layer.provide(layer$86)), layer$75, layer$70)), Layer.provideMerge(layer$55), Layer.provideMerge(layer$56), Layer.provideMerge(layer$57), Layer.provideMerge(layer$58), Layer.provideMerge(layer$59)).pipe(Layer.provideMerge(ProviderInstanceRegistryHydrationLive), Layer.provideMerge(AntigravityInstallation.layer), Layer.provideMerge(ProviderEventLoggersLive), Layer.provideMerge(OpenCodeRuntimeLive), Layer.provideMerge(layer$63), Layer.provideMerge(JevWorkspaceAdvisorLive.pipe(Layer.provide(TypeSafeClientLive))), Layer.provideMerge(layer$93.pipe(Layer.provide(layer$120))), Layer.provideMerge(SupervisorSecretsLive.pipe(Layer.provide(layer$120))), Layer.provideMerge(WorkspaceLayerLive), Layer.provideMerge(ProjectFaviconResolverLayerLive), Layer.provideMerge(layer$85), Layer.provideMerge(layer$69), Layer.provideMerge(remoteComputerUseBindingsLayer.pipe(Layer.provide(layer$95))), Layer.provideMerge(layer$78), Layer.provideMerge(AuthLayerLive), Layer.provideMerge(layer$5.pipe(Layer.provideMerge(LinearClientLive), Layer.provideMerge(Layer.mergeAll(LinearMcpClientLive, TrackerSecretsLive)), Layer.provideMerge(Layer.mergeAll(layer$90, layer$21).pipe(Layer.provideMerge(layer$91), Layer.provideMerge(layer$92))))), Layer.provideMerge(layer$120));
|
|
148510
|
+
const UsageLayerLive = layer$25.pipe(Layer.provide(layer$93.pipe(Layer.provide(layer$120))));
|
|
148291
148511
|
const RuntimeDependenciesLive = layer$24.pipe(Layer.provideMerge(RuntimeCoreDependenciesLive)).pipe(Layer.provideMerge(UsageLayerLive), Layer.provideMerge(layer$12), Layer.provideMerge(layer$11), Layer.provideMerge(layer$10), Layer.provideMerge(layer$81), Layer.provideMerge(layer$83), Layer.provideMerge(layer$82), Layer.provide(layer$124));
|
|
148292
148512
|
/**
|
|
148293
148513
|
* Hub asset sync.
|
|
@@ -148296,7 +148516,7 @@ const RuntimeDependenciesLive = layer$24.pipe(Layer.provideMerge(RuntimeCoreDepe
|
|
|
148296
148516
|
* not-configured shape, and every call reports that rather than failing — so
|
|
148297
148517
|
* this costs a server with no hub nothing but a layer.
|
|
148298
148518
|
*/
|
|
148299
|
-
const AssetSyncLive = layer$88.pipe(Layer.provideMerge(layer$77), Layer.provideMerge(layer$89), Layer.provideMerge(layer$
|
|
148519
|
+
const AssetSyncLive = layer$88.pipe(Layer.provideMerge(layer$77), Layer.provideMerge(layer$89), Layer.provideMerge(layer$95));
|
|
148300
148520
|
const RuntimeServicesLive = layer$72.pipe(Layer.provideMerge(layer$1), Layer.provideMerge(AssetSyncLive), Layer.provideMerge(layer$23), Layer.provideMerge(layer$22.pipe(Layer.provide(layer$53), Layer.provide(ProviderSessionDirectoryLayerLive), Layer.provide(ProjectionProjectRepositoryLive))), Layer.provideMerge(RuntimeDependenciesLive));
|
|
148301
148521
|
const makeRoutesLayer = Layer.mergeAll(Layer.mergeAll(HttpApiBuilder.layer(EnvironmentHttpApi).pipe(Layer.provide(authHttpApiLayer), Layer.provide(orchestrationHttpApiLayer), Layer.provide(pullRequestHttpApiLayer), Layer.provide(serverEnvironmentHttpApiLayer), Layer.provide(environmentAuthenticatedAuthLayer)), otlpTracesProxyRouteLayer, assetRouteLayer, mcpOAuthCallbackRouteLayer, staticAndDevRouteLayer, websocketRpcRouteLayer), layer$4.pipe(Layer.provide(layer$6)), layer$68, layer).pipe(Layer.provide(ProjectionProjectRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(ProjectionThreadRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(layer$67), Layer.provide(layer$28), Layer.provide(PullRequestServiceLive), Layer.provide(layer$20), Layer.provide(layer$79), Layer.provide(layer$26), Layer.provide(browserApiCorsLayer), Layer.provide(httpCompressionLayer));
|
|
148302
148522
|
const makeServerLayer = Layer.unwrap(Effect.gen(function* () {
|