@ref0/cli 0.8.0 → 0.10.0
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/main.mjs +2714 -801
- package/package.json +3 -2
package/dist/main.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
3
|
-
import { Cause, Clock, Config, ConfigProvider, Console, Context, Cron, Crypto, Data, DateTime, Deferred, Duration, Effect, Exit, FiberSet, FileSystem, Layer, Match, Option, Path, Predicate, Queue, Random, Redacted, Ref, Result, Runtime, Schedule, Schema, SchemaIssue, SchemaTransformation, Scope, Semaphore, Stdio, Stream, Struct, Terminal, Tuple } from "effect";
|
|
3
|
+
import { Cause, Clock, Config, ConfigProvider, Console, Context, Cron, Crypto, Data, DateTime, Deferred, Duration, Effect, Exit, Fiber, FiberSet, FileSystem, Layer, Match, Option, Path, Predicate, Queue, Random, Redacted, Ref, Result, Runtime, Schedule, Schema, SchemaIssue, SchemaTransformation, Scope, Semaphore, Stdio, Stream, Struct, Terminal, Tuple } from "effect";
|
|
4
4
|
import { Argument, CliOutput, Command, Flag, Prompt } from "effect/cli";
|
|
5
5
|
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
|
|
6
6
|
import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity } from "effect/http-api";
|
|
@@ -11,7 +11,7 @@ import { access, constants } from "node:fs/promises";
|
|
|
11
11
|
import { hostname } from "node:os";
|
|
12
12
|
import { connect, createServer as createServer$1 } from "node:net";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import { McpProtocol, McpServer, Tool, Toolkit } from "effect/ai";
|
|
14
|
+
import { McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
|
|
15
15
|
//#region ../contracts/src/typeid.ts
|
|
16
16
|
/**
|
|
17
17
|
* TypeIDs: a prefix, an underscore, then a UUID in 26 characters of Crockford's base32,
|
|
@@ -18337,11 +18337,29 @@ const CodexProfile = Schema.Struct({
|
|
|
18337
18337
|
])))),
|
|
18338
18338
|
network: Schema.optionalKey(Schema.NullOr(Schema.Struct({ enabled: Schema.optionalKey(Schema.NullOr(Schema.Boolean)) })))
|
|
18339
18339
|
});
|
|
18340
|
-
/**
|
|
18341
|
-
const
|
|
18342
|
-
|
|
18343
|
-
|
|
18344
|
-
|
|
18340
|
+
/** Codex applies excludes before `set`, then applies either form of allowlist after it. */
|
|
18341
|
+
const CodexShellEnvironment = Schema.Struct({
|
|
18342
|
+
set: Schema.optionalKey(Schema.NullOr(Schema.Record(Schema.String, Schema.String))),
|
|
18343
|
+
exclude: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))),
|
|
18344
|
+
include_only: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))),
|
|
18345
|
+
filters: Schema.optionalKey(Schema.NullOr(Schema.Record(Schema.String, Schema.Literals(["include", "exclude"]))))
|
|
18346
|
+
});
|
|
18347
|
+
/** The permission and environment policies, with user-layer versions and raw policy for rollback. */
|
|
18348
|
+
const CodexConfigRead = Schema.Struct({
|
|
18349
|
+
config: Schema.Struct({
|
|
18350
|
+
default_permissions: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
18351
|
+
permissions: Schema.optionalKey(Schema.NullOr(Schema.Record(Schema.String, CodexProfile))),
|
|
18352
|
+
shell_environment_policy: Schema.optionalKey(Schema.NullOr(CodexShellEnvironment))
|
|
18353
|
+
}),
|
|
18354
|
+
layers: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.Struct({
|
|
18355
|
+
name: Schema.Struct({
|
|
18356
|
+
type: Schema.String,
|
|
18357
|
+
file: Schema.optionalKey(Schema.String)
|
|
18358
|
+
}),
|
|
18359
|
+
version: Schema.String,
|
|
18360
|
+
config: Schema.optionalKey(Schema.Struct({ shell_environment_policy: Schema.optionalKey(Schema.Record(Schema.String, Schema.Json)) }))
|
|
18361
|
+
}))))
|
|
18362
|
+
});
|
|
18345
18363
|
/**
|
|
18346
18364
|
* `config/batchWrite`'s result, as much of it as ref0 holds Codex to: that it wrote, and whether
|
|
18347
18365
|
* a layer above the person's own config overrides what it wrote.
|
|
@@ -18446,7 +18464,7 @@ const HooksFile = Schema.Struct({ hooks: Schema.Record(Schema.String, Schema.Arr
|
|
|
18446
18464
|
const McpServersFile = Schema.Struct({ mcpServers: Schema.Record(Schema.String, Schema.Struct({
|
|
18447
18465
|
command: Schema.String,
|
|
18448
18466
|
args: Schema.Array(Schema.String),
|
|
18449
|
-
/** The variables Codex passes the server beside
|
|
18467
|
+
/** The variables Codex passes the server beside the few it always passes, such as `HOME`. */
|
|
18450
18468
|
env_vars: Schema.optionalKey(Schema.Array(Schema.String))
|
|
18451
18469
|
})) });
|
|
18452
18470
|
const PluginManifest = Schema.Struct({
|
|
@@ -18597,12 +18615,15 @@ const CommandHookInput = Schema.Struct({
|
|
|
18597
18615
|
});
|
|
18598
18616
|
/**
|
|
18599
18617
|
* `SessionStart`'s input, as far as ref0 reads it: the session, where it started, and why
|
|
18600
|
-
* (`startup`, `resume`, `clear` or `compact`, which both harnesses send)
|
|
18618
|
+
* (`startup`, `resume`, `clear` or `compact`, which both harnesses send); and, for the trailer
|
|
18619
|
+
* on the agent's commits, its transcript and the model it started with, which Claude Code sends.
|
|
18601
18620
|
*/
|
|
18602
18621
|
const SessionStartHookInput = Schema.Struct({
|
|
18603
18622
|
session_id: HarnessSessionId,
|
|
18604
18623
|
cwd: Schema.optionalKey(Schema.String.check(Schema.isBetweenLength(1, 4096))),
|
|
18605
|
-
source: Schema.optionalKey(Schema.String.check(Schema.isBetweenLength(1, 32)))
|
|
18624
|
+
source: Schema.optionalKey(Schema.String.check(Schema.isBetweenLength(1, 32))),
|
|
18625
|
+
transcript_path: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
18626
|
+
model: Schema.optionalKey(Schema.NullOr(Schema.String))
|
|
18606
18627
|
});
|
|
18607
18628
|
/**
|
|
18608
18629
|
* A frame on Claude Code's own inbox socket, as `ref0 mcp` writes one, a line each: the token
|
|
@@ -18648,6 +18669,7 @@ const StatusCheck = Schema.Struct({
|
|
|
18648
18669
|
"accepted",
|
|
18649
18670
|
"plugin",
|
|
18650
18671
|
"trusted",
|
|
18672
|
+
"server",
|
|
18651
18673
|
"session"
|
|
18652
18674
|
]),
|
|
18653
18675
|
answer: StatusAnswer,
|
|
@@ -18802,6 +18824,12 @@ const freshLinkSecret = Effect.suspend(() => hashed(Buffer.from(crypto.getRandom
|
|
|
18802
18824
|
//#endregion
|
|
18803
18825
|
//#region src/session.ts
|
|
18804
18826
|
/**
|
|
18827
|
+
* An agent's command in a harness session that has no ref0 session, though an agent is connected
|
|
18828
|
+
* for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
|
|
18829
|
+
* person instead.
|
|
18830
|
+
*/
|
|
18831
|
+
var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
|
|
18832
|
+
/**
|
|
18805
18833
|
* The token of the session this command runs in, if it runs in one: an agent's, which its
|
|
18806
18834
|
* harness bound when the session started. Where it's found is the harness's to say. Outside a
|
|
18807
18835
|
* session, there's none, and every command is the person's, asked by approval.
|
|
@@ -19079,7 +19107,7 @@ const minute = (at) => `${DateTime.formatIso(at).slice(0, 16).replace("T", " ")}
|
|
|
19079
19107
|
const styled = (text, code, terminal) => terminal ? `\u001b[${code}m${printable(text)}\u001b[0m` : printable(text);
|
|
19080
19108
|
const renderLine = (line, terminal) => {
|
|
19081
19109
|
if (Predicate.isString(line)) return printable(line);
|
|
19082
|
-
if ("label" in line) return styled(line.label, 1, terminal) + (line.text ? ` ${printable(line.text)}` : "");
|
|
19110
|
+
if ("label" in line) return (line.before ? `${printable(line.before)} ` : "") + styled(line.label, 1, terminal) + (line.text ? ` ${printable(line.text)}` : "");
|
|
19083
19111
|
if ("command" in line) return printable(line.command).split("\n").map((part) => ` ${part}`).join("\n");
|
|
19084
19112
|
if (line.status === "unknown") return `? unknown ${printable(line.text)}`;
|
|
19085
19113
|
return `${line.status === "yes" ? styled("✓", 32, terminal) : styled("✗", 31, terminal)} ${line.status} ${printable(line.text)}`;
|
|
@@ -19172,11 +19200,13 @@ const accountCommands = [Command.make("whoami", { json }, ({ json: asJson }) =>
|
|
|
19172
19200
|
* stays the committer, with their own signature. Git keeps every commit for good, and anyone
|
|
19173
19201
|
* who reads the repo reads its author, so these name the agent and nothing about its sessions.
|
|
19174
19202
|
*/
|
|
19203
|
+
/** The domain of every agent's commit email, which tells an agent's commits from a person's. */
|
|
19204
|
+
const agentCommitDomain = "ref0.work";
|
|
19175
19205
|
/**
|
|
19176
19206
|
* An agent's commit email: built from its agent ID alone, so it never changes, and never passes
|
|
19177
19207
|
* to another agent. Nobody reads mail there.
|
|
19178
19208
|
*/
|
|
19179
|
-
const agentCommitEmail = (agent) => `${agent}
|
|
19209
|
+
const agentCommitEmail = (agent) => `${agent}@${agentCommitDomain}`;
|
|
19180
19210
|
/**
|
|
19181
19211
|
* An agent's name as its commits' author: its owner, then its handle, as commands write it. A
|
|
19182
19212
|
* handle renamed later stays as it was in the commits made before.
|
|
@@ -19471,6 +19501,67 @@ function freeTextProblem(text, maxCharacters) {
|
|
|
19471
19501
|
*/
|
|
19472
19502
|
const forkPath = /^\/([^/]+)(?:\/([^/]+))?\/threads\/([1-9]\d{0,9})(?:\/sessions\/([1-9]\d{0,9})|\/forks\/(fork_[a-z0-9]+))?\/([^/]+)\.git\/(info\/refs|git-upload-pack|git-receive-pack)$/;
|
|
19473
19503
|
//#endregion
|
|
19504
|
+
//#region ../model/src/path-glob.ts
|
|
19505
|
+
/** Charges `cells` to the budget: whether it had them. */
|
|
19506
|
+
const spend = (budget, cells) => {
|
|
19507
|
+
budget.left -= cells;
|
|
19508
|
+
return budget.left >= 0;
|
|
19509
|
+
};
|
|
19510
|
+
/**
|
|
19511
|
+
* Whether one segment matches one segment pattern, `*` and `?` within it; none once the budget
|
|
19512
|
+
* runs out.
|
|
19513
|
+
*/
|
|
19514
|
+
function segmentMatches(pattern, segment, budget) {
|
|
19515
|
+
const characters = Array.from(segment);
|
|
19516
|
+
const width = characters.length + 1;
|
|
19517
|
+
let reach = new Uint8Array(width);
|
|
19518
|
+
let next = new Uint8Array(width);
|
|
19519
|
+
reach[0] = 1;
|
|
19520
|
+
for (const symbol of pattern) {
|
|
19521
|
+
if (!spend(budget, width)) return void 0;
|
|
19522
|
+
next.fill(0);
|
|
19523
|
+
if (symbol === "*") {
|
|
19524
|
+
let any = 0;
|
|
19525
|
+
for (let j = 0; j < width; j += 1) {
|
|
19526
|
+
any |= reach[j] ?? 0;
|
|
19527
|
+
next[j] = any;
|
|
19528
|
+
}
|
|
19529
|
+
} else for (let j = 1; j < width; j += 1) next[j] = (reach[j - 1] ?? 0) & (symbol === "?" || characters[j - 1] === symbol ? 1 : 0);
|
|
19530
|
+
[reach, next] = [next, reach];
|
|
19531
|
+
}
|
|
19532
|
+
return reach[width - 1] === 1;
|
|
19533
|
+
}
|
|
19534
|
+
/**
|
|
19535
|
+
* Whether `path`, a repo path like `src/a.ts`, matches `pattern`, within `budget`: none once it
|
|
19536
|
+
* runs out.
|
|
19537
|
+
*/
|
|
19538
|
+
function pathMatchesWithin(pattern, path, budget) {
|
|
19539
|
+
const parts = pattern.split("/");
|
|
19540
|
+
const segments = path.split("/");
|
|
19541
|
+
const width = segments.length + 1;
|
|
19542
|
+
let reach = new Uint8Array(width);
|
|
19543
|
+
let next = new Uint8Array(width);
|
|
19544
|
+
reach[0] = 1;
|
|
19545
|
+
for (const part of parts) {
|
|
19546
|
+
if (!spend(budget, width)) return void 0;
|
|
19547
|
+
next.fill(0);
|
|
19548
|
+
if (part === "**") {
|
|
19549
|
+
let any = 0;
|
|
19550
|
+
for (let j = 0; j < width; j += 1) {
|
|
19551
|
+
any |= reach[j] ?? 0;
|
|
19552
|
+
next[j] = any;
|
|
19553
|
+
}
|
|
19554
|
+
} else for (let j = 1; j < width; j += 1) {
|
|
19555
|
+
if (reach[j - 1] !== 1) continue;
|
|
19556
|
+
const matched = segmentMatches(part, segments[j - 1] ?? "", budget);
|
|
19557
|
+
if (matched === void 0) return void 0;
|
|
19558
|
+
next[j] = matched ? 1 : 0;
|
|
19559
|
+
}
|
|
19560
|
+
[reach, next] = [next, reach];
|
|
19561
|
+
}
|
|
19562
|
+
return reach[width - 1] === 1;
|
|
19563
|
+
}
|
|
19564
|
+
//#endregion
|
|
19474
19565
|
//#region ../model/src/thread-workflows.ts
|
|
19475
19566
|
const minuteMillis = 6e4;
|
|
19476
19567
|
const hourMillis = 60 * minuteMillis;
|
|
@@ -20370,6 +20461,18 @@ const harnessFlagNames = { protocol: "protocol" };
|
|
|
20370
20461
|
* release, which no hook's declaration carries.
|
|
20371
20462
|
*/
|
|
20372
20463
|
const pluginVersionVariable = "REF0_PLUGIN_VERSION";
|
|
20464
|
+
/**
|
|
20465
|
+
* What the launcher puts in `ref0 mcp`'s environment in a checkout of another origin: that
|
|
20466
|
+
* origin. The server runs all the same, answering every hook with nothing, since a harness
|
|
20467
|
+
* takes a server that exits before `initialize` as broken.
|
|
20468
|
+
*/
|
|
20469
|
+
const otherOriginVariable = "REF0_OTHER_ORIGIN";
|
|
20470
|
+
/**
|
|
20471
|
+
* What `ref0 status` puts in the environment of a folder's ref0 server, which it starts to check
|
|
20472
|
+
* it starts: the harness it plays. No harness sets it, so a harness's server finds its harness up
|
|
20473
|
+
* its parent chain.
|
|
20474
|
+
*/
|
|
20475
|
+
const serverCheckVariable = "REF0_SERVER_CHECK";
|
|
20373
20476
|
/** A flag as a command line writes it. */
|
|
20374
20477
|
const flagWord = (name) => `--${name}`;
|
|
20375
20478
|
/**
|
|
@@ -20383,11 +20486,15 @@ const watchVariables = {
|
|
|
20383
20486
|
};
|
|
20384
20487
|
/**
|
|
20385
20488
|
* What ref0 puts in the environment of an agent's commands for its commits, beside Git's own
|
|
20386
|
-
* author variables: the
|
|
20387
|
-
*
|
|
20489
|
+
* author variables: for the trailer, the harness, the session's transcript, whose last model it
|
|
20490
|
+
* names, and the model the session started with, for a transcript that names none; and what a
|
|
20491
|
+
* forwarder in ref0's hooks folder sets for the repo's own hook it runs, so a forwarder that hook
|
|
20492
|
+
* reaches forwards nothing.
|
|
20388
20493
|
*/
|
|
20389
20494
|
const gitHookVariables = {
|
|
20390
20495
|
harness: "REF0_HARNESS",
|
|
20496
|
+
transcript: "REF0_TRANSCRIPT",
|
|
20497
|
+
model: "REF0_MODEL",
|
|
20391
20498
|
forwarded: "REF0_GIT_HOOK"
|
|
20392
20499
|
};
|
|
20393
20500
|
//#endregion
|
|
@@ -20473,10 +20580,19 @@ const sessionStartHook = ref0Hook(harnessCommandNames.sessionStart);
|
|
|
20473
20580
|
/** Ends the session, as a command too. */
|
|
20474
20581
|
const sessionEndHook = ref0Hook(harnessCommandNames.sessionEnd);
|
|
20475
20582
|
/**
|
|
20583
|
+
* What Linux's keychain needs to be reached: `secret-tool` finds the session bus by its address,
|
|
20584
|
+
* or else in the runtime folder, and with neither it can't read a grant. Codex 0.164.0 passes a
|
|
20585
|
+
* server neither unless asked. macOS's keychain needs neither, and Codex passes an unset one as
|
|
20586
|
+
* unset.
|
|
20587
|
+
*/
|
|
20588
|
+
const keychainVariables = ["DBUS_SESSION_BUS_ADDRESS", "XDG_RUNTIME_DIR"];
|
|
20589
|
+
/**
|
|
20476
20590
|
* The plugin's server, `ref0 mcp`, started by M0's inline launcher, the lookup in one line: a
|
|
20477
20591
|
* script path in a server's arguments stays literal in Codex. It has no `cwd`, so the server
|
|
20478
|
-
* keeps the project's folder in both harnesses. Codex starts its servers with only
|
|
20479
|
-
* and `SHELL
|
|
20592
|
+
* keeps the project's folder in both harnesses. Codex starts its servers with only a few of its
|
|
20593
|
+
* variables, such as `HOME`, `PATH` and `SHELL` (`server-check.ts` lists them), so it's asked for
|
|
20594
|
+
* `CODEX_HOME` and the keychain's, each passed through as Codex has it and never written down: a
|
|
20595
|
+
* bus address changes with each login. Claude Code passes its whole environment.
|
|
20480
20596
|
*/
|
|
20481
20597
|
const mcpFile = (version, api, server = "ref0") => ({
|
|
20482
20598
|
path: "ref0/.mcp.json",
|
|
@@ -20491,133 +20607,14 @@ const mcpFile = (version, api, server = "ref0") => ({
|
|
|
20491
20607
|
env_vars: [
|
|
20492
20608
|
"CODEX_HOME",
|
|
20493
20609
|
"CLAUDE_CONFIG_DIR",
|
|
20494
|
-
"REF0_API"
|
|
20610
|
+
"REF0_API",
|
|
20611
|
+
...keychainVariables
|
|
20495
20612
|
]
|
|
20496
20613
|
} } })
|
|
20497
20614
|
});
|
|
20498
20615
|
/** Each event a harness's hooks send `ref0 mcp`, with the fields it reads, as an `mcp_tool` hook. */
|
|
20499
20616
|
const mcpToolHooks = (server, hookFields) => Object.fromEntries(Object.entries(hookFields).map(([event, fields]) => [event, [mcpToolHook(server, event, fields)]]));
|
|
20500
20617
|
//#endregion
|
|
20501
|
-
//#region src/keychain.ts
|
|
20502
|
-
/**
|
|
20503
|
-
* The machine's keychain, where the CLI keeps what lets it act: the host grant `ref0 connect`
|
|
20504
|
-
* makes, and a session's token. Never a file. On macOS through `security`, on Linux through
|
|
20505
|
-
* `secret-tool`, under an origin-specific `ref0/<origin>` service, by account. A secret goes in through stdin, never as
|
|
20506
|
-
* an argument, since any process on the machine can read another's arguments.
|
|
20507
|
-
*/
|
|
20508
|
-
var KeychainError = class extends Schema.TaggedError()("KeychainError", {
|
|
20509
|
-
nextStep: Schema.String,
|
|
20510
|
-
/** The keychain answered, with an item ref0 can't read, rather than not answering. */
|
|
20511
|
-
unreadable: Schema.optionalKey(Schema.Literal(true))
|
|
20512
|
-
}) {};
|
|
20513
|
-
/**
|
|
20514
|
-
* The keychain under one service. Each API origin has its own, `ref0/<origin>`; 0.5.0 kept its
|
|
20515
|
-
* grants under `ref0`, which only `ref0 connect` reads, to replace and delete them.
|
|
20516
|
-
*/
|
|
20517
|
-
const keychainIn = Effect.fnUntraced(function* (service) {
|
|
20518
|
-
const crypto = yield* Crypto.Crypto;
|
|
20519
|
-
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
20520
|
-
const macOS = (yield* spawner.string(ChildProcess.make("uname", ["-s"])).pipe(Effect.orElseSucceed(() => ""))).trim() === "Darwin";
|
|
20521
|
-
const unavailable = new KeychainError({ nextStep: macOS ? "The keychain didn’t answer. Unlock it, and try again." : "The keychain didn’t answer. Unlock the keychain or start a secret service, and try again." });
|
|
20522
|
-
const missing = new KeychainError({ nextStep: "The keychain didn’t answer: ref0 needs `secret-tool`, from libsecret. Install `secret-tool` with `sudo apt install libsecret-tools` on Debian or Ubuntu, `sudo dnf install libsecret` on Fedora or `sudo pacman -S libsecret` on Arch." });
|
|
20523
|
-
const run = (command, args, stdin) => Effect.scoped(Effect.gen(function* () {
|
|
20524
|
-
const handle = yield* spawner.spawn(ChildProcess.make(command, args, { stdin: Stream.make(new TextEncoder().encode(stdin)) })).pipe(Effect.mapError((error) => !macOS && Predicate.isTagged(error.reason, "NotFound") ? missing : unavailable));
|
|
20525
|
-
const [output, complaint] = yield* Effect.all([handle.stdout.pipe(Stream.decodeText(), Stream.mkString), handle.stderr.pipe(Stream.decodeText(), Stream.mkString)], { concurrency: "unbounded" });
|
|
20526
|
-
const code = yield* handle.exitCode;
|
|
20527
|
-
return {
|
|
20528
|
-
output: output.trim(),
|
|
20529
|
-
complaint: complaint.trim(),
|
|
20530
|
-
code,
|
|
20531
|
-
succeeded: code === ChildProcessSpawner.ExitCode(0)
|
|
20532
|
-
};
|
|
20533
|
-
})).pipe(Effect.catchTag("PlatformError", () => Effect.fail(unavailable)));
|
|
20534
|
-
const get = Effect.fn("Keychain.get")(function* (account) {
|
|
20535
|
-
const found = macOS ? yield* run("security", [
|
|
20536
|
-
"find-generic-password",
|
|
20537
|
-
"-s",
|
|
20538
|
-
service,
|
|
20539
|
-
"-a",
|
|
20540
|
-
account,
|
|
20541
|
-
"-w"
|
|
20542
|
-
], "") : yield* run("secret-tool", [
|
|
20543
|
-
"lookup",
|
|
20544
|
-
"service",
|
|
20545
|
-
service,
|
|
20546
|
-
"account",
|
|
20547
|
-
account
|
|
20548
|
-
], "");
|
|
20549
|
-
if (found.succeeded) return found.output.length > 0 ? Option.some(Redacted.make(found.output)) : Option.none();
|
|
20550
|
-
if (macOS) {
|
|
20551
|
-
if (found.code === ChildProcessSpawner.ExitCode(44)) return Option.none();
|
|
20552
|
-
return yield* unavailable;
|
|
20553
|
-
}
|
|
20554
|
-
if (found.code !== ChildProcessSpawner.ExitCode(1) || found.complaint.length > 0) return yield* unavailable;
|
|
20555
|
-
const listed = yield* run("secret-tool", [
|
|
20556
|
-
"search",
|
|
20557
|
-
"--all",
|
|
20558
|
-
"service",
|
|
20559
|
-
service,
|
|
20560
|
-
"account",
|
|
20561
|
-
account
|
|
20562
|
-
], "");
|
|
20563
|
-
if (listed.output.length > 0 || listed.complaint.length > 0) return yield* unavailable;
|
|
20564
|
-
return Option.none();
|
|
20565
|
-
});
|
|
20566
|
-
const set = Effect.fn("Keychain.set")(function* (account, secret) {
|
|
20567
|
-
const quote = (text) => `"${text.replace(/[\\"]/g, "\\$&")}"`;
|
|
20568
|
-
const value = Redacted.value(secret);
|
|
20569
|
-
if (macOS && /[\r\n\0]/.test(account + value)) return yield* unavailable;
|
|
20570
|
-
const input = macOS ? `add-generic-password -U -s ${quote(service)} -a ${quote(account)} -w ${quote(value)}\n` : value;
|
|
20571
|
-
if (macOS && new TextEncoder().encode(input).length >= 4096) return yield* unavailable;
|
|
20572
|
-
if (!(macOS ? yield* run("security", ["-i"], input) : yield* run("secret-tool", [
|
|
20573
|
-
"store",
|
|
20574
|
-
"--label",
|
|
20575
|
-
`ref0 ${account}`,
|
|
20576
|
-
"service",
|
|
20577
|
-
service,
|
|
20578
|
-
"account",
|
|
20579
|
-
account
|
|
20580
|
-
], Redacted.value(secret))).succeeded) return yield* unavailable;
|
|
20581
|
-
});
|
|
20582
|
-
const remove = Effect.fn("Keychain.remove")(function* (account) {
|
|
20583
|
-
if (!(yield* macOS ? run("security", [
|
|
20584
|
-
"delete-generic-password",
|
|
20585
|
-
"-s",
|
|
20586
|
-
service,
|
|
20587
|
-
"-a",
|
|
20588
|
-
account
|
|
20589
|
-
], "") : run("secret-tool", [
|
|
20590
|
-
"clear",
|
|
20591
|
-
"service",
|
|
20592
|
-
service,
|
|
20593
|
-
"account",
|
|
20594
|
-
account
|
|
20595
|
-
], "")).succeeded) return yield* unavailable;
|
|
20596
|
-
});
|
|
20597
|
-
const random = crypto.randomUUIDv4.pipe(Effect.mapError(() => unavailable));
|
|
20598
|
-
return {
|
|
20599
|
-
probe: Effect.gen(function* () {
|
|
20600
|
-
const account = `connect-probe/${yield* random}`;
|
|
20601
|
-
const value = Redacted.make(yield* random);
|
|
20602
|
-
yield* Effect.gen(function* () {
|
|
20603
|
-
yield* set(account, value);
|
|
20604
|
-
const kept = yield* get(account);
|
|
20605
|
-
if (Option.isNone(kept) || Redacted.value(kept.value) !== Redacted.value(value)) return yield* unavailable;
|
|
20606
|
-
yield* remove(account);
|
|
20607
|
-
}).pipe(Effect.catch((error) => remove(account).pipe(Effect.ignore, Effect.andThen(Effect.fail(error)))));
|
|
20608
|
-
}).pipe(Effect.timeoutOrElse({
|
|
20609
|
-
duration: Duration.minutes(1),
|
|
20610
|
-
orElse: () => Effect.fail(unavailable)
|
|
20611
|
-
})),
|
|
20612
|
-
get,
|
|
20613
|
-
set,
|
|
20614
|
-
remove
|
|
20615
|
-
};
|
|
20616
|
-
});
|
|
20617
|
-
var Keychain = class Keychain extends Context.Service()("ref0/cli/Keychain") {
|
|
20618
|
-
static layer = Layer.effect(Keychain, Effect.flatMap(Ref0Config, (config) => Effect.map(keychainIn(`ref0/${originKey(config.api)}`), Keychain.of)));
|
|
20619
|
-
};
|
|
20620
|
-
//#endregion
|
|
20621
20618
|
//#region src/harness/binding.ts
|
|
20622
20619
|
/**
|
|
20623
20620
|
* Where a session's token is kept: the machine's keychain, under the API origin’s keychain service, by an
|
|
@@ -20655,9 +20652,8 @@ function bindingFromEnvironment(environment, found, hookSession) {
|
|
|
20655
20652
|
//#endregion
|
|
20656
20653
|
//#region src/harness/session-token.ts
|
|
20657
20654
|
/**
|
|
20658
|
-
* The
|
|
20659
|
-
*
|
|
20660
|
-
* there's none, and a command is its person's, asked by approval.
|
|
20655
|
+
* The harness session a command runs in: the harness process up its parent chain, and the
|
|
20656
|
+
* binding its session's token is kept under, which `command-token.ts` reads.
|
|
20661
20657
|
*/
|
|
20662
20658
|
const optional$1 = (name) => Config.String(name).pipe(Config.option, Config.map(Option.getOrUndefined));
|
|
20663
20659
|
/** What the harness put in this process's environment, as far as a binding needs. */
|
|
@@ -20665,7 +20661,6 @@ const harnessEnvironment = Config.all({
|
|
|
20665
20661
|
CLAUDE_CODE_SESSION_ID: optional$1("CLAUDE_CODE_SESSION_ID"),
|
|
20666
20662
|
CLAUDE_PID: optional$1("CLAUDE_PID"),
|
|
20667
20663
|
CODEX_SESSION_ID: optional$1("CODEX_SESSION_ID"),
|
|
20668
|
-
CODEX_HOME: optional$1("CODEX_HOME"),
|
|
20669
20664
|
CODEX_SANDBOX: optional$1("CODEX_SANDBOX")
|
|
20670
20665
|
});
|
|
20671
20666
|
/**
|
|
@@ -20740,17 +20735,6 @@ const bindingForServer = Effect.fnUntraced(function* (harnessSession) {
|
|
|
20740
20735
|
process: found.value.process
|
|
20741
20736
|
});
|
|
20742
20737
|
});
|
|
20743
|
-
/** `SessionToken` from the keychain, by the binding of the harness session this runs in. */
|
|
20744
|
-
const harnessSessionToken = Layer.effect(SessionToken, Effect.gen(function* () {
|
|
20745
|
-
const keychain = yield* Keychain;
|
|
20746
|
-
const harness = yield* HarnessProcess;
|
|
20747
|
-
const current = Effect.gen(function* () {
|
|
20748
|
-
const binding = yield* bindingHere().pipe(Effect.provideService(HarnessProcess, harness), Effect.orElseSucceed(() => Option.none()));
|
|
20749
|
-
if (Option.isNone(binding)) return Option.none();
|
|
20750
|
-
return yield* keychain.get(sessionAccount(binding.value));
|
|
20751
|
-
});
|
|
20752
|
-
return SessionToken.of({ current });
|
|
20753
|
-
}));
|
|
20754
20738
|
//#endregion
|
|
20755
20739
|
//#region src/harness/config-folders.ts
|
|
20756
20740
|
const Folders = Schema.Array(Schema.String);
|
|
@@ -20863,101 +20847,227 @@ const connectLineFor = Effect.fnUntraced(function* (harness, configFolder) {
|
|
|
20863
20847
|
return `${configFolders[harness].variable}=${shellWord(configFolder)} ref0 connect`;
|
|
20864
20848
|
});
|
|
20865
20849
|
//#endregion
|
|
20866
|
-
//#region src/
|
|
20867
|
-
/**
|
|
20868
|
-
* All lasting authority for one config folder, stored as one keychain value: the agent the folder
|
|
20869
|
-
* acts as, by its owner, handle and agent ID, and its token. The ID is kept in the same write as
|
|
20870
|
-
* the token, so a folder with a grant always knows which agent its commits name.
|
|
20871
|
-
*/
|
|
20872
|
-
const KeptHostGrant = Schema.Struct({
|
|
20873
|
-
harness: ConnectHarness,
|
|
20874
|
-
owner: OwnerName,
|
|
20875
|
-
agent: AgentHandle,
|
|
20876
|
-
agentId: AgentId,
|
|
20877
|
-
token: Schema.RedactedFromValue(Schema.String)
|
|
20878
|
-
});
|
|
20879
|
-
const decodeGrant = Schema.decodeUnknownOption(Schema.fromJsonString(KeptHostGrant));
|
|
20880
|
-
const encodeGrant = Schema.encodeSync(Schema.fromJsonString(KeptHostGrant));
|
|
20881
|
-
/** Grants in the origin’s keychain service, account host-grant/<resolved config folder>. */
|
|
20882
|
-
var HostGrant = class HostGrant extends Context.Service()("ref0/cli/HostGrant") {
|
|
20883
|
-
static layer = Layer.effect(HostGrant, Effect.gen(function* () {
|
|
20884
|
-
const keychain = yield* Keychain;
|
|
20885
|
-
const folders = yield* ConfigFolders;
|
|
20886
|
-
const account = (folder) => folders.resolve(folder).pipe(Effect.map((real) => `host-grant/${real}`));
|
|
20887
|
-
return HostGrant.of({
|
|
20888
|
-
get: (folder) => account(folder).pipe(Effect.flatMap(keychain.get), Effect.flatMap((kept) => {
|
|
20889
|
-
if (Option.isNone(kept)) return Effect.succeedNone;
|
|
20890
|
-
const grant = decodeGrant(Redacted.value(kept.value));
|
|
20891
|
-
return Option.isSome(grant) ? Effect.succeed(grant) : Effect.fail(new KeychainError({
|
|
20892
|
-
nextStep: "This config folder’s host grant is unreadable. Run ref0 connect again.",
|
|
20893
|
-
unreadable: true
|
|
20894
|
-
}));
|
|
20895
|
-
})),
|
|
20896
|
-
keep: (folder, grant) => Effect.gen(function* () {
|
|
20897
|
-
yield* keychain.set(yield* account(folder), Redacted.make(encodeGrant(grant)));
|
|
20898
|
-
yield* folders.keep(folder).pipe(Effect.mapError(() => new KeychainError({ nextStep: "Couldn’t record this connected config folder in ~/.ref0. Check that it’s writable, then run ref0 connect." })));
|
|
20899
|
-
}),
|
|
20900
|
-
remove: (folder) => account(folder).pipe(Effect.flatMap(keychain.remove))
|
|
20901
|
-
});
|
|
20902
|
-
}));
|
|
20903
|
-
};
|
|
20904
|
-
/** An explicitly named agent's grant: the current folder first, then other connected folders. */
|
|
20905
|
-
const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
|
|
20906
|
-
const grants = yield* HostGrant;
|
|
20907
|
-
const folders = yield* ConfigFolders;
|
|
20908
|
-
const candidates = [yield* currentConfigFolder, ...yield* folders.all];
|
|
20909
|
-
for (const folder of new Set(candidates)) {
|
|
20910
|
-
const grant = yield* grants.get(folder);
|
|
20911
|
-
if (Option.isSome(grant) && grant.value.owner === owner && grant.value.agent === agent) return grant;
|
|
20912
|
-
}
|
|
20913
|
-
return Option.none();
|
|
20914
|
-
});
|
|
20915
|
-
//#endregion
|
|
20916
|
-
//#region src/version.ts
|
|
20917
|
-
/**
|
|
20918
|
-
* The CLI's version, from its `package.json` as the build reads it: what `ref0 --version`
|
|
20919
|
-
* prints, what its MCP server and Codex client say they are, and what the plugin carries, so
|
|
20920
|
-
* an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
|
|
20921
|
-
* and only as a default export.
|
|
20922
|
-
*/
|
|
20923
|
-
const cliVersion = "0.8.0";
|
|
20924
|
-
//#endregion
|
|
20925
|
-
//#region src/harness/hook-protocol.ts
|
|
20850
|
+
//#region src/keychain.ts
|
|
20926
20851
|
/**
|
|
20927
|
-
*
|
|
20928
|
-
*
|
|
20929
|
-
*
|
|
20930
|
-
*
|
|
20852
|
+
* The machine's keychain, where the CLI keeps what lets it act: the host grant `ref0 connect`
|
|
20853
|
+
* makes, and a session's token. Never a file. On macOS through `security`, on Linux through
|
|
20854
|
+
* `secret-tool`, under an origin-specific `ref0/<origin>` service, by account. A secret goes in through stdin, never as
|
|
20855
|
+
* an argument, since any process on the machine can read another's arguments.
|
|
20931
20856
|
*/
|
|
20932
|
-
|
|
20933
|
-
|
|
20934
|
-
/** The
|
|
20935
|
-
|
|
20936
|
-
|
|
20937
|
-
const protocolsOf = (file) => Object.values(file.hooks).flatMap((groups) => groups.flatMap((group) => group.hooks)).map((hook) => hook.type === "mcp_tool" ? hook.input["protocol"] : protocolFlagged(hook.command)).filter(Predicate.isString);
|
|
20938
|
-
/** `--protocol`, which each command hook passes: the hook protocol its declaration speaks. */
|
|
20939
|
-
const protocolFlag = Flag.String(harnessFlagNames.protocol).pipe(Flag.withDescription("The hook protocol the plugin speaks"));
|
|
20940
|
-
/** The command that installs the plugin again into a connected folder. */
|
|
20941
|
-
const installLine = (folder) => `ref0 install ${shellWord(folder)}`;
|
|
20857
|
+
var KeychainError = class extends Schema.TaggedError()("KeychainError", {
|
|
20858
|
+
nextStep: Schema.String,
|
|
20859
|
+
/** The keychain answered, with an item ref0 can't read, rather than not answering. */
|
|
20860
|
+
unreadable: Schema.optionalKey(Schema.Literal(true))
|
|
20861
|
+
}) {};
|
|
20942
20862
|
/**
|
|
20943
|
-
*
|
|
20944
|
-
*
|
|
20863
|
+
* The keychain under one service. Each API origin has its own, `ref0/<origin>`; 0.5.0 kept its
|
|
20864
|
+
* grants under `ref0`, which only `ref0 connect` reads, to replace and delete them.
|
|
20945
20865
|
*/
|
|
20946
|
-
const
|
|
20947
|
-
|
|
20948
|
-
const
|
|
20949
|
-
|
|
20950
|
-
|
|
20951
|
-
});
|
|
20952
|
-
const
|
|
20953
|
-
|
|
20954
|
-
|
|
20955
|
-
|
|
20956
|
-
|
|
20957
|
-
|
|
20958
|
-
|
|
20959
|
-
|
|
20960
|
-
|
|
20866
|
+
const keychainIn = Effect.fnUntraced(function* (service) {
|
|
20867
|
+
const crypto = yield* Crypto.Crypto;
|
|
20868
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
20869
|
+
const macOS = (yield* spawner.string(ChildProcess.make("uname", ["-s"])).pipe(Effect.orElseSucceed(() => ""))).trim() === "Darwin";
|
|
20870
|
+
const unavailable = new KeychainError({ nextStep: macOS ? "The keychain didn’t answer. Unlock it, and try again." : "The keychain didn’t answer. Unlock the keychain or start a secret service, and try again." });
|
|
20871
|
+
const missing = new KeychainError({ nextStep: "The keychain didn’t answer: ref0 needs `secret-tool`, from libsecret. Install `secret-tool` with `sudo apt install libsecret-tools` on Debian or Ubuntu, `sudo dnf install libsecret` on Fedora or `sudo pacman -S libsecret` on Arch." });
|
|
20872
|
+
const run = (command, args, stdin) => Effect.scoped(Effect.gen(function* () {
|
|
20873
|
+
const handle = yield* spawner.spawn(ChildProcess.make(command, args, { stdin: Stream.make(new TextEncoder().encode(stdin)) })).pipe(Effect.mapError((error) => !macOS && Predicate.isTagged(error.reason, "NotFound") ? missing : unavailable));
|
|
20874
|
+
const [output, complaint] = yield* Effect.all([handle.stdout.pipe(Stream.decodeText(), Stream.mkString), handle.stderr.pipe(Stream.decodeText(), Stream.mkString)], { concurrency: "unbounded" });
|
|
20875
|
+
const code = yield* handle.exitCode;
|
|
20876
|
+
return {
|
|
20877
|
+
output: output.trim(),
|
|
20878
|
+
complaint: complaint.trim(),
|
|
20879
|
+
code,
|
|
20880
|
+
succeeded: code === ChildProcessSpawner.ExitCode(0)
|
|
20881
|
+
};
|
|
20882
|
+
})).pipe(Effect.catchTag("PlatformError", () => Effect.fail(unavailable)));
|
|
20883
|
+
const get = Effect.fn("Keychain.get")(function* (account) {
|
|
20884
|
+
const found = macOS ? yield* run("security", [
|
|
20885
|
+
"find-generic-password",
|
|
20886
|
+
"-s",
|
|
20887
|
+
service,
|
|
20888
|
+
"-a",
|
|
20889
|
+
account,
|
|
20890
|
+
"-w"
|
|
20891
|
+
], "") : yield* run("secret-tool", [
|
|
20892
|
+
"lookup",
|
|
20893
|
+
"service",
|
|
20894
|
+
service,
|
|
20895
|
+
"account",
|
|
20896
|
+
account
|
|
20897
|
+
], "");
|
|
20898
|
+
if (found.succeeded) return found.output.length > 0 ? Option.some(Redacted.make(found.output)) : Option.none();
|
|
20899
|
+
if (macOS) {
|
|
20900
|
+
if (found.code === ChildProcessSpawner.ExitCode(44)) return Option.none();
|
|
20901
|
+
return yield* unavailable;
|
|
20902
|
+
}
|
|
20903
|
+
if (found.code !== ChildProcessSpawner.ExitCode(1) || found.complaint.length > 0) return yield* unavailable;
|
|
20904
|
+
const listed = yield* run("secret-tool", [
|
|
20905
|
+
"search",
|
|
20906
|
+
"--all",
|
|
20907
|
+
"service",
|
|
20908
|
+
service,
|
|
20909
|
+
"account",
|
|
20910
|
+
account
|
|
20911
|
+
], "");
|
|
20912
|
+
if (listed.output.length > 0 || listed.complaint.length > 0) return yield* unavailable;
|
|
20913
|
+
return Option.none();
|
|
20914
|
+
});
|
|
20915
|
+
const set = Effect.fn("Keychain.set")(function* (account, secret) {
|
|
20916
|
+
const quote = (text) => `"${text.replace(/[\\"]/g, "\\$&")}"`;
|
|
20917
|
+
const value = Redacted.value(secret);
|
|
20918
|
+
if (macOS && /[\r\n\0]/.test(account + value)) return yield* unavailable;
|
|
20919
|
+
const input = macOS ? `add-generic-password -U -s ${quote(service)} -a ${quote(account)} -w ${quote(value)}\n` : value;
|
|
20920
|
+
if (macOS && new TextEncoder().encode(input).length >= 4096) return yield* unavailable;
|
|
20921
|
+
if (!(macOS ? yield* run("security", ["-i"], input) : yield* run("secret-tool", [
|
|
20922
|
+
"store",
|
|
20923
|
+
"--label",
|
|
20924
|
+
`ref0 ${account}`,
|
|
20925
|
+
"service",
|
|
20926
|
+
service,
|
|
20927
|
+
"account",
|
|
20928
|
+
account
|
|
20929
|
+
], Redacted.value(secret))).succeeded) return yield* unavailable;
|
|
20930
|
+
});
|
|
20931
|
+
const remove = Effect.fn("Keychain.remove")(function* (account) {
|
|
20932
|
+
if (!(yield* macOS ? run("security", [
|
|
20933
|
+
"delete-generic-password",
|
|
20934
|
+
"-s",
|
|
20935
|
+
service,
|
|
20936
|
+
"-a",
|
|
20937
|
+
account
|
|
20938
|
+
], "") : run("secret-tool", [
|
|
20939
|
+
"clear",
|
|
20940
|
+
"service",
|
|
20941
|
+
service,
|
|
20942
|
+
"account",
|
|
20943
|
+
account
|
|
20944
|
+
], "")).succeeded) return yield* unavailable;
|
|
20945
|
+
});
|
|
20946
|
+
const random = crypto.randomUUIDv4.pipe(Effect.mapError(() => unavailable));
|
|
20947
|
+
return {
|
|
20948
|
+
probe: Effect.gen(function* () {
|
|
20949
|
+
const account = `connect-probe/${yield* random}`;
|
|
20950
|
+
const value = Redacted.make(yield* random);
|
|
20951
|
+
yield* Effect.gen(function* () {
|
|
20952
|
+
yield* set(account, value);
|
|
20953
|
+
const kept = yield* get(account);
|
|
20954
|
+
if (Option.isNone(kept) || Redacted.value(kept.value) !== Redacted.value(value)) return yield* unavailable;
|
|
20955
|
+
yield* remove(account);
|
|
20956
|
+
}).pipe(Effect.catch((error) => remove(account).pipe(Effect.ignore, Effect.andThen(Effect.fail(error)))));
|
|
20957
|
+
}).pipe(Effect.timeoutOrElse({
|
|
20958
|
+
duration: Duration.minutes(1),
|
|
20959
|
+
orElse: () => Effect.fail(unavailable)
|
|
20960
|
+
})),
|
|
20961
|
+
get,
|
|
20962
|
+
set,
|
|
20963
|
+
remove
|
|
20964
|
+
};
|
|
20965
|
+
});
|
|
20966
|
+
var Keychain = class Keychain extends Context.Service()("ref0/cli/Keychain") {
|
|
20967
|
+
static layer = Layer.effect(Keychain, Effect.flatMap(Ref0Config, (config) => Effect.map(keychainIn(`ref0/${originKey(config.api)}`), Keychain.of)));
|
|
20968
|
+
};
|
|
20969
|
+
//#endregion
|
|
20970
|
+
//#region src/host-grant.ts
|
|
20971
|
+
/**
|
|
20972
|
+
* All lasting authority for one config folder, stored as one keychain value: the agent the folder
|
|
20973
|
+
* acts as, by its owner, handle and agent ID, and its token. The ID is kept in the same write as
|
|
20974
|
+
* the token, so a folder with a grant always knows which agent its commits name.
|
|
20975
|
+
*/
|
|
20976
|
+
const KeptHostGrant = Schema.Struct({
|
|
20977
|
+
harness: ConnectHarness,
|
|
20978
|
+
owner: OwnerName,
|
|
20979
|
+
agent: AgentHandle,
|
|
20980
|
+
agentId: AgentId,
|
|
20981
|
+
token: Schema.RedactedFromValue(Schema.String)
|
|
20982
|
+
});
|
|
20983
|
+
const decodeGrant = Schema.decodeUnknownOption(Schema.fromJsonString(KeptHostGrant));
|
|
20984
|
+
const encodeGrant = Schema.encodeSync(Schema.fromJsonString(KeptHostGrant));
|
|
20985
|
+
/**
|
|
20986
|
+
* The token alone, from any record that's JSON with a string `token`, so a record this ref0 can't
|
|
20987
|
+
* read as a grant, such as one an earlier ref0 kept, still names the grant it held.
|
|
20988
|
+
*/
|
|
20989
|
+
const decodeHeldToken = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Struct({ token: KeptHostGrant.fields.token })));
|
|
20990
|
+
/** Grants in the origin’s keychain service, account host-grant/<resolved config folder>. */
|
|
20991
|
+
var HostGrant = class HostGrant extends Context.Service()("ref0/cli/HostGrant") {
|
|
20992
|
+
static layer = Layer.effect(HostGrant, Effect.gen(function* () {
|
|
20993
|
+
const keychain = yield* Keychain;
|
|
20994
|
+
const folders = yield* ConfigFolders;
|
|
20995
|
+
const account = (folder) => folders.resolve(folder).pipe(Effect.map((real) => `host-grant/${real}`));
|
|
20996
|
+
return HostGrant.of({
|
|
20997
|
+
get: (folder) => account(folder).pipe(Effect.flatMap(keychain.get), Effect.flatMap((kept) => {
|
|
20998
|
+
if (Option.isNone(kept)) return Effect.succeedNone;
|
|
20999
|
+
const grant = decodeGrant(Redacted.value(kept.value));
|
|
21000
|
+
return Option.isSome(grant) ? Effect.succeed(grant) : Effect.fail(new KeychainError({
|
|
21001
|
+
nextStep: "This config folder’s host grant is unreadable. Run ref0 connect again.",
|
|
21002
|
+
unreadable: true
|
|
21003
|
+
}));
|
|
21004
|
+
})),
|
|
21005
|
+
heldToken: (folder) => account(folder).pipe(Effect.flatMap(keychain.get), Effect.map(Option.flatMap((kept) => Option.map(decodeHeldToken(Redacted.value(kept)), (held) => held.token)))),
|
|
21006
|
+
keep: (folder, grant) => Effect.gen(function* () {
|
|
21007
|
+
yield* keychain.set(yield* account(folder), Redacted.make(encodeGrant(grant)));
|
|
21008
|
+
yield* folders.keep(folder).pipe(Effect.mapError(() => new KeychainError({ nextStep: "Couldn’t record this connected config folder in ~/.ref0. Check that it’s writable, then run ref0 connect." })));
|
|
21009
|
+
}),
|
|
21010
|
+
remove: (folder) => account(folder).pipe(Effect.flatMap(keychain.remove))
|
|
21011
|
+
});
|
|
21012
|
+
}));
|
|
21013
|
+
};
|
|
21014
|
+
/** An explicitly named agent's grant: the current folder first, then other connected folders. */
|
|
21015
|
+
const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
|
|
21016
|
+
const grants = yield* HostGrant;
|
|
21017
|
+
const folders = yield* ConfigFolders;
|
|
21018
|
+
const candidates = [yield* currentConfigFolder, ...yield* folders.all];
|
|
21019
|
+
for (const folder of new Set(candidates)) {
|
|
21020
|
+
const grant = yield* grants.get(folder);
|
|
21021
|
+
if (Option.isSome(grant) && grant.value.owner === owner && grant.value.agent === agent) return grant;
|
|
21022
|
+
}
|
|
21023
|
+
return Option.none();
|
|
21024
|
+
});
|
|
21025
|
+
//#endregion
|
|
21026
|
+
//#region src/version.ts
|
|
21027
|
+
/**
|
|
21028
|
+
* The CLI's version, from its `package.json` as the build reads it: what `ref0 --version`
|
|
21029
|
+
* prints, what its MCP server and Codex client say they are, and what the plugin carries, so
|
|
21030
|
+
* an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
|
|
21031
|
+
* and only as a default export.
|
|
21032
|
+
*/
|
|
21033
|
+
const cliVersion = "0.10.0";
|
|
21034
|
+
//#endregion
|
|
21035
|
+
//#region src/harness/hook-protocol.ts
|
|
21036
|
+
/**
|
|
21037
|
+
* Whether this `ref0` speaks the hook protocol of the plugin that ran it, and what to say when
|
|
21038
|
+
* it doesn't. The plugin and the CLI update apart, so each hook's declaration carries the
|
|
21039
|
+
* protocol it speaks, and a `ref0` that speaks another answers with the update line: the session
|
|
21040
|
+
* start says it to the agent, and `ref0 mcp` refuses each hook with it.
|
|
21041
|
+
*/
|
|
21042
|
+
/** The plugin's release, from its lookup script or launcher; none if something else ran `ref0`. */
|
|
21043
|
+
const pluginVersion = Config.String(pluginVersionVariable).pipe(Config.option);
|
|
21044
|
+
/** The protocol passed by a command hook, without running any of its code. */
|
|
21045
|
+
const protocolFlagged = (command) => /--protocol (\S+)/.exec(command)?.[1];
|
|
21046
|
+
/** Every readable protocol in a fetched or installed hooks file. */
|
|
21047
|
+
const protocolsOf = (file) => Object.values(file.hooks).flatMap((groups) => groups.flatMap((group) => group.hooks)).map((hook) => hook.type === "mcp_tool" ? hook.input["protocol"] : protocolFlagged(hook.command)).filter(Predicate.isString);
|
|
21048
|
+
/** `--protocol`, which each command hook passes: the hook protocol its declaration speaks. */
|
|
21049
|
+
const protocolFlag = Flag.String(harnessFlagNames.protocol).pipe(Flag.withDescription("The hook protocol the plugin speaks"));
|
|
21050
|
+
/** The command that installs the plugin again into a connected folder. */
|
|
21051
|
+
const installLine = (folder) => `ref0 install ${shellWord(folder)}`;
|
|
21052
|
+
/**
|
|
21053
|
+
* Why this `ref0` doesn't speak the plugin's hook protocol, and what to run: a newer plugin needs
|
|
21054
|
+
* a newer `ref0`, and an older one installing again. None when `ref0` speaks it.
|
|
21055
|
+
*/
|
|
21056
|
+
const protocolMismatch = (plugin, reinstall) => {
|
|
21057
|
+
if (plugin.protocol === "1") return Option.none();
|
|
21058
|
+
const which = Option.match(plugin.version, {
|
|
21059
|
+
onNone: () => "ref0’s plugin",
|
|
21060
|
+
onSome: (version) => `ref0’s plugin ${version}`
|
|
21061
|
+
});
|
|
21062
|
+
const newer = Number(plugin.protocol) > 1;
|
|
21063
|
+
return Option.some(newer ? {
|
|
21064
|
+
says: `${which} needs a newer ref0 than ${cliVersion}`,
|
|
21065
|
+
run: "npm i -g @ref0/cli"
|
|
21066
|
+
} : {
|
|
21067
|
+
says: `${which} is older than ref0 ${cliVersion} speaks`,
|
|
21068
|
+
run: reinstall
|
|
21069
|
+
});
|
|
21070
|
+
};
|
|
20961
21071
|
/** The line that says what to run when the plugin speaks a protocol this `ref0` doesn't. */
|
|
20962
21072
|
const updateLine = (plugin, reinstall) => Option.map(protocolMismatch(plugin, reinstall), ({ says, run }) => `ref0: ${says}. Run: ${run}`);
|
|
20963
21073
|
//#endregion
|
|
@@ -21076,45 +21186,129 @@ var OldHostGrants = class OldHostGrants extends Context.Service()("ref0/cli/OldH
|
|
|
21076
21186
|
}));
|
|
21077
21187
|
};
|
|
21078
21188
|
//#endregion
|
|
21079
|
-
//#region src/
|
|
21080
|
-
/**
|
|
21081
|
-
* The machine prints its link and Find code, then collects with its own secret and the answer
|
|
21082
|
-
* from the approving browser. It keeps and confirms each folder's grant before installing.
|
|
21083
|
-
*/
|
|
21084
|
-
const isAnswerCode = Schema.is(TypedApprovalCode);
|
|
21085
|
-
/** A key's escape code, such as an arrow's, as a terminal sends it. */
|
|
21086
|
-
const escapeCode = /\u001b\[[\d;]*[A-Za-z~]/gu;
|
|
21087
|
-
const decodeTokenId$1 = Schema.decodeUnknownOption(TokenId);
|
|
21088
|
-
const decodeSecretHash$1 = Schema.decodeUnknownEffect(SecretHash);
|
|
21089
|
-
/** How often the machine asks whether its person has answered. */
|
|
21090
|
-
const pollMillis = 2e3;
|
|
21091
|
-
/** How long one request may take before the machine gives up on it, and asks again. */
|
|
21092
|
-
const requestTimeout = Duration.seconds(15);
|
|
21093
|
-
/**
|
|
21094
|
-
* How long past the time to answer the machine still asks while ref0 says it waits: a yes
|
|
21095
|
-
* admitted in time waits until ref0 publishes what it connected.
|
|
21096
|
-
*/
|
|
21097
|
-
const giveUpAfterMillis = 12e4;
|
|
21098
|
-
/**
|
|
21099
|
-
* How long one try to keep the host grant may wait on the keychain, such as on a prompt to unlock
|
|
21100
|
-
* it, before the machine stops it. Ctrl-C can't: the grant is kept even after an interrupt.
|
|
21101
|
-
*/
|
|
21102
|
-
const keepTimeout = Duration.minutes(1);
|
|
21189
|
+
//#region src/yes-or-no.ts
|
|
21103
21190
|
/**
|
|
21104
|
-
* How long
|
|
21105
|
-
*
|
|
21191
|
+
* How long what a terminal held while nobody read it takes to arrive once a question reads: one
|
|
21192
|
+
* read, well within this.
|
|
21106
21193
|
*/
|
|
21107
|
-
const
|
|
21108
|
-
|
|
21109
|
-
|
|
21110
|
-
const
|
|
21194
|
+
const settle = Duration.millis(50);
|
|
21195
|
+
const again$2 = "Type y or yes, or n or no. Enter alone is no.";
|
|
21196
|
+
/** A line's answer, or none when it's neither yes nor no. */
|
|
21197
|
+
const answerOf$1 = (line) => {
|
|
21198
|
+
const word = line.trim().toLowerCase();
|
|
21199
|
+
if (word === "y" || word === "yes") return Option.some(true);
|
|
21200
|
+
if (word === "" || word === "n" || word === "no") return Option.some(false);
|
|
21201
|
+
return Option.none();
|
|
21202
|
+
};
|
|
21203
|
+
/** What Backspace takes back: one character as the person sees it, an emoji's parts included. */
|
|
21204
|
+
const characters = new Intl.Segmenter();
|
|
21205
|
+
/** `typed` with Backspace pressed once. */
|
|
21206
|
+
const takeBack = (typed) => typed.slice(0, [...characters.segment(typed)].at(-1)?.index);
|
|
21207
|
+
/** Whether a key types text: none of its characters moves the cursor or controls the terminal. */
|
|
21208
|
+
const typesText = (text) => text !== "" && !/\p{Cc}/u.test(text);
|
|
21209
|
+
/** Ctrl-C or Ctrl-D, which a terminal in raw mode hands ref0 as keys, never as a signal. */
|
|
21210
|
+
const quits = (key) => key.ctrl && (key.name === "c" || key.name === "d");
|
|
21211
|
+
/** The explanation goes to stderr, the question to stdout and its answer comes from stdin. */
|
|
21212
|
+
const allTerminals = () => process.stdin.isTTY === true && process.stdout.isTTY === true && process.stderr.isTTY === true;
|
|
21111
21213
|
/**
|
|
21112
|
-
*
|
|
21113
|
-
*
|
|
21114
|
-
* when that
|
|
21214
|
+
* Asks yes-or-no questions in this terminal, one after another: each shows `explained` on stderr,
|
|
21215
|
+
* then the question, and waits for a line. What was typed before a question showed doesn't answer
|
|
21216
|
+
* it, unless another question was asked when it arrived: a line typed after the one that answered
|
|
21217
|
+
* a question answers the next. The answer is no, unasked, unless stdin, stdout and stderr are each
|
|
21218
|
+
* a terminal, and on Ctrl-C or Ctrl-D.
|
|
21115
21219
|
*/
|
|
21116
|
-
const
|
|
21117
|
-
|
|
21220
|
+
const yesOrNoQuestions = Effect.gen(function* () {
|
|
21221
|
+
const terminal = yield* Terminal.Terminal;
|
|
21222
|
+
const ahead = yield* Ref.make([]);
|
|
21223
|
+
const ask = (explained, question) => Effect.gen(function* () {
|
|
21224
|
+
if (!allTerminals()) return false;
|
|
21225
|
+
const keys = yield* terminal.readInput;
|
|
21226
|
+
yield* Effect.sleep(settle);
|
|
21227
|
+
yield* Queue.clear(keys);
|
|
21228
|
+
const pending = [...yield* Ref.getAndSet(ahead, [])];
|
|
21229
|
+
const next = Effect.suspend(() => {
|
|
21230
|
+
const key = pending.shift();
|
|
21231
|
+
return key === void 0 ? Queue.take(keys) : Effect.succeed(key);
|
|
21232
|
+
});
|
|
21233
|
+
yield* printBlocks([explained]);
|
|
21234
|
+
yield* terminal.display(questionLine(question));
|
|
21235
|
+
let typed = "";
|
|
21236
|
+
let previous = "";
|
|
21237
|
+
while (true) {
|
|
21238
|
+
const pressed = yield* Effect.option(next);
|
|
21239
|
+
if (Option.isNone(pressed) || quits(pressed.value.key)) {
|
|
21240
|
+
yield* terminal.display("\n");
|
|
21241
|
+
return false;
|
|
21242
|
+
}
|
|
21243
|
+
const { input, key } = pressed.value;
|
|
21244
|
+
const ended = key.name === "enter" && previous === "return";
|
|
21245
|
+
previous = key.name;
|
|
21246
|
+
if (ended) continue;
|
|
21247
|
+
if (key.name === "backspace") {
|
|
21248
|
+
if (typed === "") continue;
|
|
21249
|
+
typed = takeBack(typed);
|
|
21250
|
+
yield* terminal.display("\b \b");
|
|
21251
|
+
continue;
|
|
21252
|
+
}
|
|
21253
|
+
if (key.name !== "return" && key.name !== "enter") {
|
|
21254
|
+
const text = Option.getOrElse(input, () => "");
|
|
21255
|
+
if (!typesText(text)) continue;
|
|
21256
|
+
typed += text;
|
|
21257
|
+
yield* terminal.display(text);
|
|
21258
|
+
continue;
|
|
21259
|
+
}
|
|
21260
|
+
yield* terminal.display("\n");
|
|
21261
|
+
const answer = answerOf$1(typed);
|
|
21262
|
+
if (Option.isSome(answer)) {
|
|
21263
|
+
const rest = [...pending, ...yield* Queue.clear(keys)];
|
|
21264
|
+
const ended = key.name === "return" && rest[0]?.key.name === "enter";
|
|
21265
|
+
yield* Ref.set(ahead, ended ? rest.slice(1) : rest);
|
|
21266
|
+
return answer.value;
|
|
21267
|
+
}
|
|
21268
|
+
typed = "";
|
|
21269
|
+
yield* terminal.display(`${again$2}\n${questionLine(question)}`);
|
|
21270
|
+
}
|
|
21271
|
+
}).pipe(Effect.scoped, Effect.orElseSucceed(() => false));
|
|
21272
|
+
return ask;
|
|
21273
|
+
});
|
|
21274
|
+
//#endregion
|
|
21275
|
+
//#region src/attachment-request.ts
|
|
21276
|
+
/**
|
|
21277
|
+
* The machine prints its link and Find code, then collects with its own secret and the answer
|
|
21278
|
+
* from the approving browser. It keeps and confirms each folder's grant before installing.
|
|
21279
|
+
*/
|
|
21280
|
+
const isAnswerCode = Schema.is(TypedApprovalCode);
|
|
21281
|
+
const decodeTokenId$1 = Schema.decodeUnknownOption(TokenId);
|
|
21282
|
+
const decodeSecretHash$1 = Schema.decodeUnknownEffect(SecretHash);
|
|
21283
|
+
/** How often the machine asks whether its person has answered. */
|
|
21284
|
+
const pollMillis = 2e3;
|
|
21285
|
+
/** How long one request may take before the machine gives up on it, and asks again. */
|
|
21286
|
+
const requestTimeout = Duration.seconds(15);
|
|
21287
|
+
/**
|
|
21288
|
+
* How long past the time to answer the machine still asks while ref0 says it waits: a yes
|
|
21289
|
+
* admitted in time waits until ref0 publishes what it connected.
|
|
21290
|
+
*/
|
|
21291
|
+
const giveUpAfterMillis = 12e4;
|
|
21292
|
+
/**
|
|
21293
|
+
* How long one try to keep the host grant may wait on the keychain, such as on a prompt to unlock
|
|
21294
|
+
* it, before the machine stops it. Ctrl-C can't: the grant is kept even after an interrupt.
|
|
21295
|
+
*/
|
|
21296
|
+
const keepTimeout = Duration.minutes(1);
|
|
21297
|
+
/**
|
|
21298
|
+
* How long keeping every folder's grant may take, so the machine confirms what it kept well
|
|
21299
|
+
* before ref0 revokes what it hasn't, 10 minutes after it connected.
|
|
21300
|
+
*/
|
|
21301
|
+
const keepBudget = Duration.minutes(5);
|
|
21302
|
+
/** A request that couldn't finish: no answer in time, no connection, or ref0 briefly unable. */
|
|
21303
|
+
var Unanswered = class extends Schema.TaggedError()("Unanswered", {}) {};
|
|
21304
|
+
const unanswered = () => Effect.fail(new Unanswered());
|
|
21305
|
+
/**
|
|
21306
|
+
* The name the machine sends for itself: its host name without any character that could reorder
|
|
21307
|
+
* or hide what's around it, cut to 64 characters without splitting one in two, or `this machine`
|
|
21308
|
+
* when that leaves none.
|
|
21309
|
+
*/
|
|
21310
|
+
const machineNameOf = (hostName) => {
|
|
21311
|
+
let name = "";
|
|
21118
21312
|
for (const character of withoutHiddenCharacters(hostName).trim()) {
|
|
21119
21313
|
if (name.length + character.length > 64) break;
|
|
21120
21314
|
name += character;
|
|
@@ -21125,14 +21319,14 @@ const machineNameOf = (hostName) => {
|
|
|
21125
21319
|
const said = (refusal) => sayFailed(refusal.nextStep);
|
|
21126
21320
|
const isEnded = (state) => state.state !== "waiting";
|
|
21127
21321
|
/** The line that asks again. */
|
|
21128
|
-
const again$
|
|
21322
|
+
const again$1 = "ref0 connect";
|
|
21129
21323
|
/** What each way a request can end without connecting says, with the line that asks again. */
|
|
21130
21324
|
const endedWords$1 = (state) => {
|
|
21131
21325
|
switch (state.state) {
|
|
21132
|
-
case "refused": return `It was refused, and nothing was connected. To ask again, run: ${again$
|
|
21133
|
-
case "aborted": return `It was aborted, and nothing was connected. To ask again, run: ${again$
|
|
21134
|
-
case "expired": return `Nobody approved it in time, and nothing was connected. For a new code, run: ${again$
|
|
21135
|
-
case "failed": return `${state.nextStep} To connect this machine, run: ${again$
|
|
21326
|
+
case "refused": return `It was refused, and nothing was connected. To ask again, run: ${again$1}`;
|
|
21327
|
+
case "aborted": return `It was aborted, and nothing was connected. To ask again, run: ${again$1}`;
|
|
21328
|
+
case "expired": return `Nobody approved it in time, and nothing was connected. For a new code, run: ${again$1}`;
|
|
21329
|
+
case "failed": return `${state.nextStep} To connect this machine, run: ${again$1}`;
|
|
21136
21330
|
}
|
|
21137
21331
|
};
|
|
21138
21332
|
/** The grant a kept record holds, by its ID. */
|
|
@@ -21148,6 +21342,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21148
21342
|
const hostGrant = yield* HostGrant;
|
|
21149
21343
|
const oldGrants = yield* OldHostGrants;
|
|
21150
21344
|
const http = yield* HttpClient.HttpClient;
|
|
21345
|
+
const terminal = yield* Terminal.Terminal;
|
|
21151
21346
|
const home = yield* Config.String("HOME").pipe(Config.withDefault("."));
|
|
21152
21347
|
const collecting = yield* freshSecret;
|
|
21153
21348
|
const link = yield* freshLinkSecret;
|
|
@@ -21159,12 +21354,12 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21159
21354
|
const unreachable = () => sayFailed(`Couldn’t reach ref0 at ${config.api}. Try again.`);
|
|
21160
21355
|
const requestId = yield* newRequestId;
|
|
21161
21356
|
const asking = yield* Effect.forEach(asked, Effect.fnUntraced(function* (folder) {
|
|
21162
|
-
const
|
|
21357
|
+
const held = yield* hostGrant.heldToken(folder.configFolder).pipe(Effect.catchTag("KeychainError", said));
|
|
21163
21358
|
return {
|
|
21164
21359
|
...folder,
|
|
21165
21360
|
shown: shownConfigFolder(home, folder.configFolder),
|
|
21166
21361
|
grant: yield* freshTokenSecret,
|
|
21167
|
-
replaces: Option.isSome(
|
|
21362
|
+
replaces: Option.isSome(held) ? grantOf(held.value) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said))
|
|
21168
21363
|
};
|
|
21169
21364
|
}));
|
|
21170
21365
|
const folders = yield* Effect.forEach(asking, Effect.fnUntraced(function* (folder) {
|
|
@@ -21203,12 +21398,20 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21203
21398
|
while: Predicate.isTagged("Unanswered"),
|
|
21204
21399
|
times: 2
|
|
21205
21400
|
}), Effect.catchTag("Unanswered", unreachable));
|
|
21206
|
-
yield*
|
|
21401
|
+
yield* printBlocks([[
|
|
21207
21402
|
`Open ${config.api}/connect#${link.secret}`,
|
|
21208
|
-
|
|
21403
|
+
{
|
|
21404
|
+
before: "Or type",
|
|
21405
|
+
label: started.code,
|
|
21406
|
+
text: `in Find under Waiting on you, by ${minute(started.expiresAt)}.`
|
|
21407
|
+
},
|
|
21209
21408
|
"Approve it only if you ran this ref0 connect yourself, just now: a yes gives this machine a lasting credential for each agent it connects.",
|
|
21210
21409
|
"Waiting for the answer. Ctrl-C stops it."
|
|
21211
|
-
]
|
|
21410
|
+
]]);
|
|
21411
|
+
let typed = "";
|
|
21412
|
+
const show = (text) => Effect.ignore(terminal.display(text));
|
|
21413
|
+
const endWait = yield* Effect.cached(Effect.suspend(() => typed === "" ? Effect.void : show("\n")).pipe(Effect.andThen(Console.error(""))));
|
|
21414
|
+
const saidAfterWait = (refusal) => Effect.andThen(endWait, said(refusal));
|
|
21212
21415
|
if (opener !== void 0) yield* openBrowser(opener, `${config.api}/connect#${link.secret}`);
|
|
21213
21416
|
const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, collecting.secret)));
|
|
21214
21417
|
const collector = yield* ref0Client(config.api).pipe(Effect.provide(Layer.merge(anonymousCredential, withSecret)), Effect.provideService(HttpClient.HttpClient, http));
|
|
@@ -21220,8 +21423,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21220
21423
|
duration: requestTimeout,
|
|
21221
21424
|
orElse: () => Effect.succeedNone
|
|
21222
21425
|
}), Effect.catchTags({
|
|
21223
|
-
AttachmentNotFound:
|
|
21224
|
-
Unauthenticated:
|
|
21426
|
+
AttachmentNotFound: saidAfterWait,
|
|
21427
|
+
Unauthenticated: saidAfterWait,
|
|
21225
21428
|
AuthorityUnavailable: () => Effect.succeedNone,
|
|
21226
21429
|
HttpClientError: () => Effect.succeedNone,
|
|
21227
21430
|
SchemaError: () => Effect.succeedNone
|
|
@@ -21231,7 +21434,10 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21231
21434
|
while (true) {
|
|
21232
21435
|
const state = yield* collect;
|
|
21233
21436
|
if (Option.isSome(state) && isEnded(state.value)) return state.value;
|
|
21234
|
-
if ((yield* Clock.currentTimeMillis) > giveUpAt)
|
|
21437
|
+
if ((yield* Clock.currentTimeMillis) > giveUpAt) {
|
|
21438
|
+
yield* endWait;
|
|
21439
|
+
return yield* sayFailed(`ref0 hasn’t said how it ended, so this machine kept nothing. If it was approved, ref0 revokes its grants 10 minutes after it connected. To connect this machine, run: ${again$1}`);
|
|
21440
|
+
}
|
|
21235
21441
|
yield* Effect.sleep(pollMillis);
|
|
21236
21442
|
}
|
|
21237
21443
|
});
|
|
@@ -21295,12 +21501,12 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21295
21501
|
"It connected, but this machine couldn’t keep what it needs to act:",
|
|
21296
21502
|
...notKept.map((each) => `${shownConfigFolder(home, each.configFolder)}: ${each.nextStep}`),
|
|
21297
21503
|
"This machine never confirmed the grants, so ref0 revokes them 10 minutes after they connected, even if the keychain kept them after all. To connect this machine, run:",
|
|
21298
|
-
{ command: again$
|
|
21504
|
+
{ command: again$1 }
|
|
21299
21505
|
]]);
|
|
21300
|
-
const unconfirmed = () => sayFailed(`It connected, and this machine kept its grants, but ref0 didn’t answer when it said so. If ref0 never heard, it revokes them 10 minutes after they connected: then run ${again$
|
|
21506
|
+
const unconfirmed = () => sayFailed(`It connected, and this machine kept its grants, but ref0 didn’t answer when it said so. If ref0 never heard, it revokes them 10 minutes after they connected: then run ${again$1}`);
|
|
21301
21507
|
const confirmId = yield* newRequestId;
|
|
21302
21508
|
const answerCode = yield* Ref.get(pasted);
|
|
21303
|
-
if (answerCode === void 0) return yield* sayFailed(`The answer code is missing. Run ${again$
|
|
21509
|
+
if (answerCode === void 0) return yield* sayFailed(`The answer code is missing. Run ${again$1} again.`);
|
|
21304
21510
|
if (!(yield* collector.machineAttachments.confirm({
|
|
21305
21511
|
params,
|
|
21306
21512
|
headers: { "idempotency-key": confirmId },
|
|
@@ -21323,7 +21529,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21323
21529
|
schedule: Schedule.spaced("1 second")
|
|
21324
21530
|
}), Effect.catchTag("Unanswered", unconfirmed))).confirmed) {
|
|
21325
21531
|
for (const folder of kept) yield* hostGrant.remove(folder.configFolder).pipe(Effect.ignore);
|
|
21326
|
-
return yield* sayFailed(`ref0 revoked the grants, since this machine didn’t confirm in time that it kept them. They’re deleted here, and nothing is connected. To connect this machine, run: ${again$
|
|
21532
|
+
return yield* sayFailed(`ref0 revoked the grants, since this machine didn’t confirm in time that it kept them. They’re deleted here, and nothing is connected. To connect this machine, run: ${again$1}`);
|
|
21327
21533
|
}
|
|
21328
21534
|
return {
|
|
21329
21535
|
owner: state.owner,
|
|
@@ -21361,23 +21567,42 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21361
21567
|
const connected = yield* finish(isEnded(state) ? state : yield* answered);
|
|
21362
21568
|
yield* printBlocks([[{ label: `Connected as ${connected.owner}’s agents` }, ...connected.kept.map((folder) => `${shownConfigFolder(home, folder.configFolder)} ${folder.handle}`)], ["Stopped, so nothing was installed. To install ref0’s plugin into each, run:", ...connected.kept.map((folder) => ({ command: installLine(folder.configFolder) }))]]);
|
|
21363
21569
|
});
|
|
21364
|
-
const
|
|
21365
|
-
|
|
21366
|
-
yield* Console.error(`Paste the code from your own page on ${config.api}.`);
|
|
21570
|
+
const typedCode = Effect.gen(function* () {
|
|
21571
|
+
const keys = yield* terminal.readInput;
|
|
21367
21572
|
while (true) {
|
|
21368
|
-
const
|
|
21369
|
-
if (
|
|
21370
|
-
|
|
21371
|
-
|
|
21573
|
+
const pressed = yield* Effect.option(Queue.take(keys));
|
|
21574
|
+
if (Option.isNone(pressed) || quits(pressed.value.key)) return yield* Effect.interrupt;
|
|
21575
|
+
const { input, key } = pressed.value;
|
|
21576
|
+
if (key.name === "backspace") {
|
|
21577
|
+
if (typed === "") continue;
|
|
21578
|
+
typed = takeBack(typed);
|
|
21579
|
+
yield* show("\b \b");
|
|
21580
|
+
continue;
|
|
21581
|
+
}
|
|
21582
|
+
if (key.name !== "return" && key.name !== "enter") {
|
|
21583
|
+
const text = Option.getOrElse(input, () => "");
|
|
21584
|
+
if (!typesText(text)) continue;
|
|
21585
|
+
typed += text;
|
|
21586
|
+
yield* show(text);
|
|
21372
21587
|
continue;
|
|
21373
21588
|
}
|
|
21374
|
-
|
|
21375
|
-
if (
|
|
21376
|
-
|
|
21589
|
+
const line = typed.trim();
|
|
21590
|
+
if (typed !== "") yield* show("\n");
|
|
21591
|
+
typed = "";
|
|
21592
|
+
if (line === "") continue;
|
|
21593
|
+
if (isAnswerCode(line)) return line;
|
|
21594
|
+
yield* printBlocks([[`Paste the 8-character code from your own page on ${config.api}.`]]);
|
|
21377
21595
|
}
|
|
21378
|
-
}).pipe(Effect.
|
|
21596
|
+
}).pipe(Effect.scoped);
|
|
21597
|
+
const readAnswer = Effect.gen(function* () {
|
|
21598
|
+
yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
|
|
21599
|
+
const code = yield* typedCode;
|
|
21600
|
+
yield* Ref.update(pasted, (first) => first ?? code);
|
|
21601
|
+
if (listener !== void 0) yield* listener.stop;
|
|
21602
|
+
return yield* Effect.never;
|
|
21603
|
+
});
|
|
21379
21604
|
const receiveAnswer = listener === void 0 ? readAnswer : Effect.raceFirst(readAnswer, listener.answer.pipe(Effect.flatMap((code) => Ref.update(pasted, (first) => first ?? code)), Effect.andThen(Effect.never)));
|
|
21380
|
-
return yield* Effect.uninterruptibleMask((restore) => restore(Effect.raceFirst(answered, receiveAnswer)).pipe(Effect.ensuring(listener === void 0 ? Effect.void : listener.stop), Effect.onInterrupt(() => Effect.ignore(stopped)), Effect.flatMap(finish)));
|
|
21605
|
+
return yield* Effect.uninterruptibleMask((restore) => restore(Effect.raceFirst(answered, receiveAnswer)).pipe(Effect.ensuring(listener === void 0 ? Effect.void : listener.stop), Effect.ensuring(endWait), Effect.onInterrupt(() => Effect.ignore(stopped)), Effect.flatMap(finish)));
|
|
21381
21606
|
}, Effect.scoped);
|
|
21382
21607
|
//#endregion
|
|
21383
21608
|
//#region src/connect-lock.ts
|
|
@@ -21512,108 +21737,1411 @@ var ConnectLock = class ConnectLock extends Context.Service()("ref0/cli/ConnectL
|
|
|
21512
21737
|
}));
|
|
21513
21738
|
};
|
|
21514
21739
|
//#endregion
|
|
21515
|
-
//#region
|
|
21516
|
-
|
|
21517
|
-
*
|
|
21518
|
-
*
|
|
21519
|
-
|
|
21520
|
-
|
|
21521
|
-
|
|
21522
|
-
|
|
21523
|
-
|
|
21524
|
-
|
|
21525
|
-
|
|
21526
|
-
|
|
21527
|
-
|
|
21528
|
-
|
|
21529
|
-
|
|
21530
|
-
|
|
21531
|
-
|
|
21532
|
-
|
|
21533
|
-
|
|
21534
|
-
|
|
21535
|
-
|
|
21536
|
-
|
|
21537
|
-
*
|
|
21538
|
-
*
|
|
21539
|
-
*
|
|
21540
|
-
*
|
|
21541
|
-
*
|
|
21542
|
-
*/
|
|
21543
|
-
|
|
21544
|
-
|
|
21545
|
-
|
|
21546
|
-
|
|
21547
|
-
|
|
21548
|
-
|
|
21549
|
-
|
|
21550
|
-
|
|
21551
|
-
|
|
21552
|
-
|
|
21553
|
-
|
|
21554
|
-
|
|
21740
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/error.js
|
|
21741
|
+
/*!
|
|
21742
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
21743
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
21744
|
+
*
|
|
21745
|
+
* Redistribution and use in source and binary forms, with or without
|
|
21746
|
+
* modification, are permitted provided that the following conditions are met:
|
|
21747
|
+
*
|
|
21748
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
21749
|
+
* list of conditions and the following disclaimer.
|
|
21750
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
21751
|
+
* this list of conditions and the following disclaimer in the
|
|
21752
|
+
* documentation and/or other materials provided with the distribution.
|
|
21753
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
21754
|
+
* may be used to endorse or promote products derived from this software without
|
|
21755
|
+
* specific prior written permission.
|
|
21756
|
+
*
|
|
21757
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
21758
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
21759
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21760
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
21761
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21762
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21763
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
21764
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
21765
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
21766
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
21767
|
+
*/
|
|
21768
|
+
function getLineColFromPtr(string, ptr) {
|
|
21769
|
+
let lines = string.slice(0, ptr).split(/\r?\n/);
|
|
21770
|
+
return [lines.length, lines.pop().length + 1];
|
|
21771
|
+
}
|
|
21772
|
+
function makeCodeBlock(string, line, column) {
|
|
21773
|
+
let lines = string.split(/\r?\n/);
|
|
21774
|
+
let codeblock = "";
|
|
21775
|
+
let numberLen = (Math.log10(line + 1) | 0) + 1;
|
|
21776
|
+
for (let i = line - 1; i <= line + 1; i++) {
|
|
21777
|
+
let l = lines[i - 1];
|
|
21778
|
+
if (!l) continue;
|
|
21779
|
+
codeblock += i.toString().padEnd(numberLen, " ");
|
|
21780
|
+
codeblock += ": ";
|
|
21781
|
+
codeblock += l;
|
|
21782
|
+
codeblock += "\n";
|
|
21783
|
+
if (i === line) {
|
|
21784
|
+
codeblock += " ".repeat(numberLen + column + 2);
|
|
21785
|
+
codeblock += "^\n";
|
|
21786
|
+
}
|
|
21787
|
+
}
|
|
21788
|
+
return codeblock;
|
|
21789
|
+
}
|
|
21790
|
+
var TomlError = class TomlError extends Error {
|
|
21791
|
+
line;
|
|
21792
|
+
column;
|
|
21793
|
+
codeblock;
|
|
21794
|
+
constructor(message, options) {
|
|
21795
|
+
const [line, column] = getLineColFromPtr(options.toml, options.ptr);
|
|
21796
|
+
const codeblock = makeCodeBlock(options.toml, line, column);
|
|
21797
|
+
super(`Invalid TOML document: ${message}\n\n${codeblock}`, options);
|
|
21798
|
+
this.line = line;
|
|
21799
|
+
this.column = column;
|
|
21800
|
+
this.codeblock = codeblock;
|
|
21801
|
+
}
|
|
21802
|
+
/** @internal */
|
|
21803
|
+
static x(message, ctx, ptr) {
|
|
21804
|
+
throw new TomlError(message, {
|
|
21805
|
+
toml: ctx.s,
|
|
21806
|
+
ptr: ptr ?? ctx.p
|
|
21555
21807
|
});
|
|
21556
|
-
|
|
21557
|
-
|
|
21558
|
-
|
|
21559
|
-
|
|
21560
|
-
|
|
21561
|
-
|
|
21562
|
-
|
|
21563
|
-
|
|
21564
|
-
|
|
21808
|
+
}
|
|
21809
|
+
};
|
|
21810
|
+
//#endregion
|
|
21811
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/primitive.js
|
|
21812
|
+
/*!
|
|
21813
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
21814
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
21815
|
+
*
|
|
21816
|
+
* Redistribution and use in source and binary forms, with or without
|
|
21817
|
+
* modification, are permitted provided that the following conditions are met:
|
|
21818
|
+
*
|
|
21819
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
21820
|
+
* list of conditions and the following disclaimer.
|
|
21821
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
21822
|
+
* this list of conditions and the following disclaimer in the
|
|
21823
|
+
* documentation and/or other materials provided with the distribution.
|
|
21824
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
21825
|
+
* may be used to endorse or promote products derived from this software without
|
|
21826
|
+
* specific prior written permission.
|
|
21827
|
+
*
|
|
21828
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
21829
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
21830
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21831
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
21832
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21833
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21834
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
21835
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
21836
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
21837
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
21838
|
+
*/
|
|
21839
|
+
/** @internal */
|
|
21840
|
+
function parseString(ctx) {
|
|
21841
|
+
let startPtr = ctx.p;
|
|
21842
|
+
let c = ctx.s.charCodeAt(ctx.p++);
|
|
21843
|
+
let first = c;
|
|
21844
|
+
let isLiteral = c === 39;
|
|
21845
|
+
let isMultiline = c === ctx.s.charCodeAt(ctx.p) && c === ctx.s.charCodeAt(ctx.p + 1);
|
|
21846
|
+
if (isMultiline) {
|
|
21847
|
+
if ((c = ctx.s.charCodeAt(ctx.p += 2)) === 10) ctx.p++;
|
|
21848
|
+
else if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10) ctx.p += 2;
|
|
21849
|
+
}
|
|
21850
|
+
let parsed = "";
|
|
21851
|
+
let sliceStart = ctx.p;
|
|
21852
|
+
let state = 0;
|
|
21853
|
+
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
21854
|
+
c = ctx.s.charCodeAt(ctx.p);
|
|
21855
|
+
if (isMultiline && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)) state = state && 3;
|
|
21856
|
+
else if (c < 32 && c !== 9 || c === 127) TomlError.x("control characters are not allowed in strings", ctx);
|
|
21857
|
+
else if ((!state || state === 3) && c === first && (!isMultiline || ctx.s.charCodeAt(ctx.p + 1) === first && ctx.s.charCodeAt(ctx.p + 2) === first)) {
|
|
21858
|
+
if (isMultiline) {
|
|
21859
|
+
if (ctx.s.charCodeAt(ctx.p + 3) === first) ctx.p++;
|
|
21860
|
+
if (ctx.s.charCodeAt(ctx.p + 3) === first) ctx.p++;
|
|
21565
21861
|
}
|
|
21566
|
-
|
|
21567
|
-
|
|
21568
|
-
|
|
21569
|
-
if (ended) continue;
|
|
21570
|
-
if (key.name === "backspace") {
|
|
21571
|
-
if (typed === "") continue;
|
|
21572
|
-
typed = typed.slice(0, [...characters.segment(typed)].at(-1)?.index);
|
|
21573
|
-
yield* terminal.display("\b \b");
|
|
21574
|
-
continue;
|
|
21862
|
+
if (!state) {
|
|
21863
|
+
let s = ctx.s.slice(sliceStart, ctx.p);
|
|
21864
|
+
parsed = parsed ? parsed + s : s;
|
|
21575
21865
|
}
|
|
21576
|
-
|
|
21577
|
-
|
|
21578
|
-
|
|
21579
|
-
|
|
21580
|
-
|
|
21581
|
-
|
|
21866
|
+
ctx.p += isMultiline ? 3 : 1;
|
|
21867
|
+
return parsed;
|
|
21868
|
+
} else if (!state) {
|
|
21869
|
+
if (!isLiteral && c === 92) {
|
|
21870
|
+
parsed += ctx.s.slice(sliceStart, sliceStart = ctx.p);
|
|
21871
|
+
state = 1;
|
|
21582
21872
|
}
|
|
21583
|
-
|
|
21584
|
-
|
|
21585
|
-
|
|
21586
|
-
|
|
21587
|
-
|
|
21588
|
-
|
|
21589
|
-
|
|
21873
|
+
} else if (state === 1) {
|
|
21874
|
+
if (c === 120 || c === 117 || c === 85) {
|
|
21875
|
+
let errPtr = ctx.p++ - 1;
|
|
21876
|
+
let value = 0;
|
|
21877
|
+
let len = c === 120 ? 2 : c === 117 ? 4 : 8;
|
|
21878
|
+
for (let j = 0; j < len; j++, ctx.p++) {
|
|
21879
|
+
let hex = ctx.s.charCodeAt(ctx.p);
|
|
21880
|
+
let digit = hex >= 48 && hex <= 57 ? hex - 48 : hex >= 65 && hex <= 70 ? hex - 65 + 10 : hex >= 97 && hex <= 102 ? hex - 97 + 10 : -1;
|
|
21881
|
+
if (digit < 0) TomlError.x("invalid non-hex character in unicode escape", ctx);
|
|
21882
|
+
value = value << 4 | digit;
|
|
21883
|
+
}
|
|
21884
|
+
if (value < 0 || value > 1114111 || value >= 55296 && value <= 57343) TomlError.x("invalid unicode escape", ctx, errPtr);
|
|
21885
|
+
parsed += String.fromCodePoint(value);
|
|
21886
|
+
sliceStart = ctx.p--;
|
|
21887
|
+
state = 0;
|
|
21888
|
+
} else if (isMultiline && (c === 32 || c === 9)) state = 2;
|
|
21889
|
+
else {
|
|
21890
|
+
if (c === 98) parsed += "\b";
|
|
21891
|
+
else if (c === 116) parsed += " ";
|
|
21892
|
+
else if (c === 110) parsed += "\n";
|
|
21893
|
+
else if (c === 102) parsed += "\f";
|
|
21894
|
+
else if (c === 114) parsed += "\r";
|
|
21895
|
+
else if (c === 101) parsed += "\x1B";
|
|
21896
|
+
else if (c === 34) parsed += "\"";
|
|
21897
|
+
else if (c === 92) parsed += "\\";
|
|
21898
|
+
else TomlError.x("unrecognised escape sequence", ctx);
|
|
21899
|
+
sliceStart = ctx.p + 1;
|
|
21900
|
+
state = 0;
|
|
21590
21901
|
}
|
|
21591
|
-
|
|
21592
|
-
|
|
21902
|
+
} else if (c !== 32 && c !== 9) {
|
|
21903
|
+
if (state === 2) TomlError.x("invalid escape: only line-ending whitespace may be escaped", ctx, sliceStart);
|
|
21904
|
+
state = !isLiteral && c === 92 ? 1 : 0;
|
|
21905
|
+
sliceStart = ctx.p;
|
|
21906
|
+
}
|
|
21907
|
+
}
|
|
21908
|
+
TomlError.x("unfinished string", ctx, startPtr);
|
|
21909
|
+
}
|
|
21910
|
+
//#endregion
|
|
21911
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/date.js
|
|
21912
|
+
/*!
|
|
21913
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
21914
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
21915
|
+
*
|
|
21916
|
+
* Redistribution and use in source and binary forms, with or without
|
|
21917
|
+
* modification, are permitted provided that the following conditions are met:
|
|
21918
|
+
*
|
|
21919
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
21920
|
+
* list of conditions and the following disclaimer.
|
|
21921
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
21922
|
+
* this list of conditions and the following disclaimer in the
|
|
21923
|
+
* documentation and/or other materials provided with the distribution.
|
|
21924
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
21925
|
+
* may be used to endorse or promote products derived from this software without
|
|
21926
|
+
* specific prior written permission.
|
|
21927
|
+
*
|
|
21928
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
21929
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
21930
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21931
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
21932
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21933
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21934
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
21935
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
21936
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
21937
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
21938
|
+
*/
|
|
21939
|
+
let DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[Tt ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|z|[-+]\d{2}:\d{2})?$/i;
|
|
21940
|
+
var TomlDate = class TomlDate extends Date {
|
|
21941
|
+
#hasDate = false;
|
|
21942
|
+
#hasTime = false;
|
|
21943
|
+
#offset = null;
|
|
21944
|
+
constructor(date, fasttype, unsafeDelim) {
|
|
21945
|
+
let hasDate = true;
|
|
21946
|
+
let hasTime = true;
|
|
21947
|
+
let offset = "Z";
|
|
21948
|
+
let c;
|
|
21949
|
+
if (typeof date === "string") {
|
|
21950
|
+
if (fasttype) prep: {
|
|
21951
|
+
if (fasttype < 3) {
|
|
21952
|
+
if (+date.slice(11, 13) > 23) {
|
|
21953
|
+
date = "";
|
|
21954
|
+
break prep;
|
|
21955
|
+
}
|
|
21956
|
+
if (fasttype === 2) {
|
|
21957
|
+
offset = null;
|
|
21958
|
+
date += "Z";
|
|
21959
|
+
} else if ((c = date.charCodeAt(date.length - 1)) !== 90 && c !== 122) offset = date.slice(date.length - 6);
|
|
21960
|
+
if (unsafeDelim) date = date.slice(0, 10) + "T" + date.slice(11);
|
|
21961
|
+
} else if (fasttype === 4) date = +date.slice(0, 2) > 23 ? "" : `0000-01-01T${date}Z`;
|
|
21962
|
+
hasDate = fasttype !== 4;
|
|
21963
|
+
hasTime = fasttype !== 3;
|
|
21964
|
+
}
|
|
21965
|
+
else {
|
|
21966
|
+
let match = date.match(DATE_TIME_RE);
|
|
21967
|
+
if (match) {
|
|
21968
|
+
if (!match[1]) {
|
|
21969
|
+
hasDate = false;
|
|
21970
|
+
date = `0000-01-01T${date}`;
|
|
21971
|
+
}
|
|
21972
|
+
hasTime = !!match[2];
|
|
21973
|
+
hasTime && date[10] === " " && (date = date.replace(" ", "T"));
|
|
21974
|
+
if (match[2] && +match[2] > 23) date = "";
|
|
21975
|
+
else {
|
|
21976
|
+
offset = match[3] || null;
|
|
21977
|
+
if (!offset && hasTime) date += "Z";
|
|
21978
|
+
}
|
|
21979
|
+
} else date = "";
|
|
21980
|
+
}
|
|
21981
|
+
}
|
|
21982
|
+
super(date);
|
|
21983
|
+
if (!isNaN(this.getTime())) {
|
|
21984
|
+
this.#hasDate = hasDate;
|
|
21985
|
+
this.#hasTime = hasTime;
|
|
21986
|
+
this.#offset = offset;
|
|
21987
|
+
}
|
|
21988
|
+
}
|
|
21989
|
+
isDateTime() {
|
|
21990
|
+
return this.#hasDate && this.#hasTime;
|
|
21991
|
+
}
|
|
21992
|
+
isLocal() {
|
|
21993
|
+
return !this.#hasDate || !this.#hasTime || !this.#offset;
|
|
21994
|
+
}
|
|
21995
|
+
isDate() {
|
|
21996
|
+
return this.#hasDate && !this.#hasTime;
|
|
21997
|
+
}
|
|
21998
|
+
isTime() {
|
|
21999
|
+
return this.#hasTime && !this.#hasDate;
|
|
22000
|
+
}
|
|
22001
|
+
isValid() {
|
|
22002
|
+
return this.#hasDate || this.#hasTime;
|
|
22003
|
+
}
|
|
22004
|
+
toISOString() {
|
|
22005
|
+
let iso = super.toISOString();
|
|
22006
|
+
if (this.isDate()) return iso.slice(0, 10);
|
|
22007
|
+
if (this.isTime()) return iso.slice(11, 23);
|
|
22008
|
+
if (this.#offset === null) return iso.slice(0, -1);
|
|
22009
|
+
if (this.#offset === "Z" || this.#offset === "z") return iso;
|
|
22010
|
+
let offset = +this.#offset.slice(1, 3) * 60 + +this.#offset.slice(4, 6);
|
|
22011
|
+
offset = this.#offset[0] === "-" ? offset : -offset;
|
|
22012
|
+
return (/* @__PURE__ */ new Date(this.getTime() - offset * 6e4)).toISOString().slice(0, -1) + this.#offset;
|
|
22013
|
+
}
|
|
22014
|
+
static wrapAsOffsetDateTime(jsDate, offset = "Z") {
|
|
22015
|
+
let date = new TomlDate(jsDate);
|
|
22016
|
+
date.#offset = offset;
|
|
22017
|
+
return date;
|
|
22018
|
+
}
|
|
22019
|
+
static wrapAsLocalDateTime(jsDate) {
|
|
22020
|
+
let date = new TomlDate(jsDate);
|
|
22021
|
+
date.#offset = null;
|
|
22022
|
+
return date;
|
|
22023
|
+
}
|
|
22024
|
+
static wrapAsLocalDate(jsDate) {
|
|
22025
|
+
let date = new TomlDate(jsDate);
|
|
22026
|
+
date.#hasTime = false;
|
|
22027
|
+
date.#offset = null;
|
|
22028
|
+
return date;
|
|
22029
|
+
}
|
|
22030
|
+
static wrapAsLocalTime(jsDate) {
|
|
22031
|
+
let date = new TomlDate(jsDate);
|
|
22032
|
+
date.#hasDate = false;
|
|
22033
|
+
date.#offset = null;
|
|
22034
|
+
return date;
|
|
22035
|
+
}
|
|
22036
|
+
};
|
|
22037
|
+
//#endregion
|
|
22038
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/extract.js
|
|
22039
|
+
/*!
|
|
22040
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
22041
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
22042
|
+
*
|
|
22043
|
+
* Redistribution and use in source and binary forms, with or without
|
|
22044
|
+
* modification, are permitted provided that the following conditions are met:
|
|
22045
|
+
*
|
|
22046
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
22047
|
+
* list of conditions and the following disclaimer.
|
|
22048
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22049
|
+
* this list of conditions and the following disclaimer in the
|
|
22050
|
+
* documentation and/or other materials provided with the distribution.
|
|
22051
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
22052
|
+
* may be used to endorse or promote products derived from this software without
|
|
22053
|
+
* specific prior written permission.
|
|
22054
|
+
*
|
|
22055
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22056
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22057
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22058
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22059
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22060
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22061
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22062
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22063
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22064
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22065
|
+
*/
|
|
22066
|
+
function isDigit(char, base = 10) {
|
|
22067
|
+
return base === 16 ? char > 47 && char < 58 || char > 64 && char < 71 || char > 96 && char < 103 : char > 47 && char < 48 + base;
|
|
22068
|
+
}
|
|
22069
|
+
function isEndOfValue(char, delim) {
|
|
22070
|
+
return char === 32 || char === 9 || char === 10 || char === 13 || delim && (char === delim || char === 44) || char === 35;
|
|
22071
|
+
}
|
|
22072
|
+
/** @internal */
|
|
22073
|
+
function extractValue(ctx, end) {
|
|
22074
|
+
let errPtr = ctx.p;
|
|
22075
|
+
let c = ctx.s.charCodeAt(ctx.p);
|
|
22076
|
+
if (c === 91 || c === 123) {
|
|
22077
|
+
ctx.d-- || TomlError.x("document contains excessively nested structures. aborting.", ctx);
|
|
22078
|
+
let value = c === 91 ? parseArray(ctx) : parseInlineTable(ctx);
|
|
22079
|
+
ctx.d++;
|
|
22080
|
+
return value;
|
|
22081
|
+
}
|
|
22082
|
+
if (c === 34 || c === 39) return parseString(ctx);
|
|
22083
|
+
if (c === 116) {
|
|
22084
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 114 || ctx.s.charCodeAt(++ctx.p) !== 117 || ctx.s.charCodeAt(++ctx.p) !== 101) TomlError.x("invalid value", ctx, errPtr);
|
|
22085
|
+
return ctx.p++, true;
|
|
22086
|
+
}
|
|
22087
|
+
if (c === 102) {
|
|
22088
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 97 || ctx.s.charCodeAt(++ctx.p) !== 108 || ctx.s.charCodeAt(++ctx.p) !== 115 || ctx.s.charCodeAt(++ctx.p) !== 101) TomlError.x("invalid value", ctx, errPtr);
|
|
22089
|
+
return ctx.p++, false;
|
|
22090
|
+
}
|
|
22091
|
+
if (c === 43 || c === 45) return parseNumber(ctx, ctx.p, ctx.s.charCodeAt(++ctx.p), 44 - c, end);
|
|
22092
|
+
if (ctx.s.charCodeAt(ctx.p + 4) === 45 && ctx.s.charCodeAt(ctx.p + 7) === 45) return parseDate(ctx, c, end);
|
|
22093
|
+
if (ctx.s.charCodeAt(ctx.p + 2) === 58) return parseTime(ctx, c, end);
|
|
22094
|
+
return parseNumber(ctx, ctx.p, c, 0, end);
|
|
22095
|
+
}
|
|
22096
|
+
function parseNumber(ctx, startPtr, startChr, sign, endChr) {
|
|
22097
|
+
let c = startChr;
|
|
22098
|
+
let state = 0;
|
|
22099
|
+
let hasUnderscores = false;
|
|
22100
|
+
if (c === 105) {
|
|
22101
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 110 || ctx.s.charCodeAt(++ctx.p) !== 102) TomlError.x("invalid value", ctx, startPtr);
|
|
22102
|
+
return ctx.p++, (sign || 1) / 0;
|
|
22103
|
+
}
|
|
22104
|
+
if (c === 110) {
|
|
22105
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 97 || ctx.s.charCodeAt(++ctx.p) !== 110) TomlError.x("invalid value", ctx, startPtr);
|
|
22106
|
+
return ctx.p++, NaN;
|
|
22107
|
+
}
|
|
22108
|
+
if (c === 48) {
|
|
22109
|
+
if (++ctx.p >= ctx.s.length || isEndOfValue(c = ctx.s.charCodeAt(ctx.p), endChr)) return ctx.bi === true ? 0n : 0;
|
|
22110
|
+
if (!sign) {
|
|
22111
|
+
if (c === 120) return parseIntegerBaseN(ctx, startPtr, 16, endChr);
|
|
22112
|
+
else if (c === 98) return parseIntegerBaseN(ctx, startPtr, 2, endChr);
|
|
22113
|
+
else if (c === 111) return parseIntegerBaseN(ctx, startPtr, 8, endChr);
|
|
22114
|
+
}
|
|
22115
|
+
if (c === 46) state = 2;
|
|
22116
|
+
else if (c === 101 || c === 69) state = 4;
|
|
22117
|
+
else TomlError.x("illegal leading zero", ctx, startPtr);
|
|
22118
|
+
} else if (!isDigit(c)) TomlError.x("invalid value", ctx, startPtr);
|
|
22119
|
+
while (++ctx.p < ctx.s.length && (c = ctx.s.charCodeAt(ctx.p), !isEndOfValue(c, endChr))) {
|
|
22120
|
+
if (!state) state = 1;
|
|
22121
|
+
if (c === 95) {
|
|
22122
|
+
if (!(state & 1)) TomlError.x("illegal underscore", ctx);
|
|
22123
|
+
state += 11;
|
|
22124
|
+
hasUnderscores = true;
|
|
22125
|
+
} else if (state === 1 && c === 46) state = 2;
|
|
22126
|
+
else if ((state === 1 || state === 3) && (c === 101 || c === 69)) state = 4;
|
|
22127
|
+
else if (state === 4 && (c === 43 || c === 45)) state = 6;
|
|
22128
|
+
else if (!isDigit(c)) TomlError.x(`illegal character in numeric literal`, ctx);
|
|
22129
|
+
else if (state > 9) state -= 11;
|
|
22130
|
+
else if (!(state & 1)) state++;
|
|
22131
|
+
}
|
|
22132
|
+
if (!state) {
|
|
22133
|
+
let val = (startChr - 48) * (sign || 1);
|
|
22134
|
+
return ctx.bi === true ? BigInt(val) : val;
|
|
22135
|
+
}
|
|
22136
|
+
if (!(state & 1)) TomlError.x("unfinished numeric value", ctx, startPtr);
|
|
22137
|
+
let str = ctx.s.slice(startPtr, ctx.p);
|
|
22138
|
+
if (hasUnderscores) str = str.replaceAll("_", "");
|
|
22139
|
+
return state > 1 ? parseFloat(str) : parseInteger(ctx, str, 10, startPtr);
|
|
22140
|
+
}
|
|
22141
|
+
function parseIntegerBaseN(ctx, startPtr, base, endChr) {
|
|
22142
|
+
let c, underscore = 1;
|
|
22143
|
+
while (++ctx.p < ctx.s.length && (c = ctx.s.charCodeAt(ctx.p), !isEndOfValue(c, endChr))) if (c === 95) {
|
|
22144
|
+
if (underscore & 1) TomlError.x("illegal underscore", ctx);
|
|
22145
|
+
underscore = 3;
|
|
22146
|
+
} else if (!isDigit(c, base)) TomlError.x(`illegal character in numeric literal`, ctx);
|
|
22147
|
+
else if (underscore & 1) underscore--;
|
|
22148
|
+
if (underscore & 1) TomlError.x("unfinished numeric value", ctx);
|
|
22149
|
+
let str = ctx.s.slice(startPtr + 2, ctx.p);
|
|
22150
|
+
if (underscore) str = str.replaceAll("_", "");
|
|
22151
|
+
return parseInteger(ctx, str, base, startPtr);
|
|
22152
|
+
}
|
|
22153
|
+
function parseInteger(ctx, str, base, startPtr) {
|
|
22154
|
+
if (ctx.bi !== true) int: {
|
|
22155
|
+
let val = parseInt(str, base);
|
|
22156
|
+
if (!Number.isSafeInteger(val)) {
|
|
22157
|
+
if (ctx.bi) break int;
|
|
22158
|
+
TomlError.x("integer value cannot be represented losslessly", ctx, startPtr);
|
|
22159
|
+
}
|
|
22160
|
+
return val;
|
|
22161
|
+
}
|
|
22162
|
+
return base === 10 ? BigInt(str) : BigInt((base === 2 ? "0b" : base === 8 ? "0o" : "0x") + str);
|
|
22163
|
+
}
|
|
22164
|
+
function parseDate(ctx, c, endChr) {
|
|
22165
|
+
let startPtr = ctx.p++, unsafeSeparator;
|
|
22166
|
+
if (!isDigit(c) || !isDigit(ctx.s.charCodeAt(ctx.p++)) || !isDigit(ctx.s.charCodeAt(ctx.p++)) || !isDigit(ctx.s.charCodeAt(ctx.p++))) return parseNumber(ctx, ctx.p = startPtr, c, 0, endChr);
|
|
22167
|
+
ctx.p += 5;
|
|
22168
|
+
if (!isDigit(ctx.s.charCodeAt(ctx.p++))) TomlError.x("invalid date-time: date part is malformed", ctx, startPtr);
|
|
22169
|
+
if (ctx.p >= ctx.s.length || ((c = ctx.s.charCodeAt(ctx.p)) !== 32 || (unsafeSeparator = true, !isDigit(ctx.s.charCodeAt(ctx.p + 1)))) && c !== 84 && c !== 116) return readDate(ctx, ctx.s.slice(startPtr, ctx.p), 3, false, startPtr);
|
|
22170
|
+
if (ctx.s.charCodeAt(ctx.p += 3) !== 58) TomlError.x("invalid date-time: time part is malformed", ctx, startPtr);
|
|
22171
|
+
if (ctx.s.charCodeAt(ctx.p += 3) === 58) ctx.p += 3;
|
|
22172
|
+
if (ctx.s.charCodeAt(ctx.p) === 46) while (isDigit(ctx.s.charCodeAt(++ctx.p)));
|
|
22173
|
+
if (c = ctx.s.charCodeAt(ctx.p)) {
|
|
22174
|
+
if (c === 90 || c === 122) return readDate(ctx, ctx.s.slice(startPtr, ++ctx.p), 1, unsafeSeparator, startPtr, "[+00:00]");
|
|
22175
|
+
if (c === 43 || c === 45) return readDate(ctx, ctx.s.slice(startPtr, ctx.p += 6), 1, unsafeSeparator, startPtr, !ctx.ld && "[" + ctx.s.slice(ctx.p - 6, ctx.p) + "]");
|
|
22176
|
+
}
|
|
22177
|
+
return readDate(ctx, ctx.s.slice(startPtr, ctx.p), 2, unsafeSeparator, startPtr);
|
|
22178
|
+
}
|
|
22179
|
+
function parseTime(ctx, c, endChr) {
|
|
22180
|
+
let start = ctx.p;
|
|
22181
|
+
if (!isDigit(c) || !isDigit(ctx.s.charCodeAt(++ctx.p))) return parseNumber(ctx, --ctx.p, c, 0, endChr);
|
|
22182
|
+
if (ctx.s.charCodeAt(ctx.p += 4) === 58) ctx.p += 3;
|
|
22183
|
+
if (ctx.s.charCodeAt(ctx.p) === 46) while (isDigit(ctx.s.charCodeAt(++ctx.p)));
|
|
22184
|
+
return readDate(ctx, ctx.s.slice(start, ctx.p), 4, false, start);
|
|
22185
|
+
}
|
|
22186
|
+
function readDate(ctx, str, type, unsafeDelim, errPtr, temporalSuffix) {
|
|
22187
|
+
if (ctx.ld) {
|
|
22188
|
+
let date = new TomlDate(str, type, unsafeDelim);
|
|
22189
|
+
if (!date.isValid()) TomlError.x("invalid date", ctx, errPtr);
|
|
22190
|
+
return date;
|
|
22191
|
+
}
|
|
22192
|
+
try {
|
|
22193
|
+
if (temporalSuffix) str += temporalSuffix;
|
|
22194
|
+
switch (type) {
|
|
22195
|
+
case 1: return Temporal.ZonedDateTime.from(str);
|
|
22196
|
+
case 2: return Temporal.PlainDateTime.from(str);
|
|
22197
|
+
case 3: return Temporal.PlainDate.from(str);
|
|
22198
|
+
case 4: return Temporal.PlainTime.from(str);
|
|
22199
|
+
}
|
|
22200
|
+
} catch (e) {
|
|
22201
|
+
TomlError.x(e instanceof Error ? e.message : "" + e, ctx, errPtr);
|
|
22202
|
+
}
|
|
22203
|
+
}
|
|
22204
|
+
//#endregion
|
|
22205
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/util.js
|
|
22206
|
+
/*!
|
|
22207
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
22208
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
22209
|
+
*
|
|
22210
|
+
* Redistribution and use in source and binary forms, with or without
|
|
22211
|
+
* modification, are permitted provided that the following conditions are met:
|
|
22212
|
+
*
|
|
22213
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
22214
|
+
* list of conditions and the following disclaimer.
|
|
22215
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22216
|
+
* this list of conditions and the following disclaimer in the
|
|
22217
|
+
* documentation and/or other materials provided with the distribution.
|
|
22218
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
22219
|
+
* may be used to endorse or promote products derived from this software without
|
|
22220
|
+
* specific prior written permission.
|
|
22221
|
+
*
|
|
22222
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22223
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22224
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22225
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22226
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22227
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22228
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22229
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22230
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22231
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22232
|
+
*/
|
|
22233
|
+
/** @internal */
|
|
22234
|
+
function skipComment(ctx) {
|
|
22235
|
+
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
22236
|
+
let c = ctx.s.charCodeAt(ctx.p);
|
|
22237
|
+
if (c === 10) break;
|
|
22238
|
+
if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10) {
|
|
22239
|
+
ctx.p++;
|
|
22240
|
+
break;
|
|
22241
|
+
}
|
|
22242
|
+
if (c < 32 && c !== 9 || c === 127) TomlError.x("control characters are not allowed in comments", ctx);
|
|
22243
|
+
}
|
|
22244
|
+
}
|
|
22245
|
+
/** @internal */
|
|
22246
|
+
function skipVoid(ctx, banNewLines, banComments) {
|
|
22247
|
+
let c;
|
|
22248
|
+
while (ctx.p < ctx.s.length) {
|
|
22249
|
+
while (ctx.p < ctx.s.length && ((c = ctx.s.charCodeAt(ctx.p)) === 32 || c === 9 || !banNewLines && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10))) ctx.p++;
|
|
22250
|
+
if (banComments || c !== 35) break;
|
|
22251
|
+
skipComment(ctx);
|
|
22252
|
+
}
|
|
22253
|
+
}
|
|
22254
|
+
//#endregion
|
|
22255
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/struct.js
|
|
22256
|
+
/*!
|
|
22257
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
22258
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
22259
|
+
*
|
|
22260
|
+
* Redistribution and use in source and binary forms, with or without
|
|
22261
|
+
* modification, are permitted provided that the following conditions are met:
|
|
22262
|
+
*
|
|
22263
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
22264
|
+
* list of conditions and the following disclaimer.
|
|
22265
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22266
|
+
* this list of conditions and the following disclaimer in the
|
|
22267
|
+
* documentation and/or other materials provided with the distribution.
|
|
22268
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
22269
|
+
* may be used to endorse or promote products derived from this software without
|
|
22270
|
+
* specific prior written permission.
|
|
22271
|
+
*
|
|
22272
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22273
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22274
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22275
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22276
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22277
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22278
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22279
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22280
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22281
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22282
|
+
*/
|
|
22283
|
+
/** @internal */
|
|
22284
|
+
function parseKey(ctx, end = 61) {
|
|
22285
|
+
let startPtr;
|
|
22286
|
+
let state = 0;
|
|
22287
|
+
let parsed = [];
|
|
22288
|
+
let sliceStart;
|
|
22289
|
+
let c = ctx.s.charCodeAt(startPtr = ctx.p);
|
|
22290
|
+
do
|
|
22291
|
+
if (c === end) {
|
|
22292
|
+
if (!state) TomlError.x("unexpected end of key", ctx);
|
|
22293
|
+
if (state === 1) parsed.push(ctx.s.slice(sliceStart, ctx.p));
|
|
22294
|
+
return ctx.p++, parsed;
|
|
22295
|
+
} else if (c === 46) {
|
|
22296
|
+
if (!state) TomlError.x("illegal empty bare key", ctx);
|
|
22297
|
+
if (state === 1) parsed.push(ctx.s.slice(sliceStart, ctx.p));
|
|
22298
|
+
state = 0;
|
|
22299
|
+
} else if (!state && (c === 34 || c === 39)) {
|
|
22300
|
+
if (c === ctx.s.charCodeAt(ctx.p + 1) && c === ctx.s.charCodeAt(ctx.p + 2)) TomlError.x("illegal quoted key: multiline strings are not allowed", ctx);
|
|
22301
|
+
parsed.push(parseString(ctx));
|
|
22302
|
+
state = 2;
|
|
22303
|
+
ctx.p--;
|
|
22304
|
+
} else if (c === 32 || c === 9) {
|
|
22305
|
+
if (state === 1) {
|
|
22306
|
+
parsed.push(ctx.s.slice(sliceStart, ctx.p));
|
|
22307
|
+
state = 2;
|
|
22308
|
+
}
|
|
22309
|
+
} else if (state === 2 || c < 48 && c !== 45 || c > 57 && c < 65 || c > 90 && c < 97 && c !== 95 || c > 122) TomlError.x("illegal character in key", ctx);
|
|
22310
|
+
else if (!state) {
|
|
22311
|
+
state = 1;
|
|
22312
|
+
sliceStart = ctx.p;
|
|
22313
|
+
}
|
|
22314
|
+
while (c = ctx.s.charCodeAt(++ctx.p));
|
|
22315
|
+
TomlError.x("incomplete key-value: cannot find end of key", ctx, startPtr);
|
|
22316
|
+
}
|
|
22317
|
+
/** @internal */
|
|
22318
|
+
function parseInlineTable(ctx) {
|
|
22319
|
+
let startPtr = ctx.p++;
|
|
22320
|
+
let res = Object.create(null);
|
|
22321
|
+
let seen = /* @__PURE__ */ new Set();
|
|
22322
|
+
let c;
|
|
22323
|
+
while (ctx.p < ctx.s.length) {
|
|
22324
|
+
skipVoid(ctx);
|
|
22325
|
+
if ((c = ctx.s.charCodeAt(ctx.p)) === 125) {
|
|
22326
|
+
ctx.p++;
|
|
22327
|
+
return res;
|
|
22328
|
+
}
|
|
22329
|
+
let k;
|
|
22330
|
+
let t = res;
|
|
22331
|
+
let hasOwn = false;
|
|
22332
|
+
let errPtr = ctx.p;
|
|
22333
|
+
let key = parseKey(ctx);
|
|
22334
|
+
for (let i = 0; i < key.length; i++) {
|
|
22335
|
+
if (i) t = hasOwn ? t[k] : t[k] = Object.create(null);
|
|
22336
|
+
k = key[i];
|
|
22337
|
+
if ((hasOwn = Object.hasOwn(t, k)) && (typeof t[k] !== "object" || seen.has(t[k]))) TomlError.x("trying to redefine an already defined value", ctx, errPtr);
|
|
22338
|
+
let unsafe = k === "__proto__";
|
|
22339
|
+
if (ctx.uk && (unsafe || k === "constructor")) {
|
|
22340
|
+
t = ctx.uk !== 1 && TomlError.x("document contains an unsafe property", ctx, errPtr);
|
|
22341
|
+
break;
|
|
22342
|
+
}
|
|
22343
|
+
if (!hasOwn && unsafe) Object.defineProperty(t, k, {
|
|
22344
|
+
enumerable: true,
|
|
22345
|
+
configurable: true,
|
|
22346
|
+
writable: true
|
|
22347
|
+
});
|
|
22348
|
+
}
|
|
22349
|
+
if (hasOwn) TomlError.x("trying to redefine an already defined value", ctx, errPtr);
|
|
22350
|
+
skipVoid(ctx, true, true);
|
|
22351
|
+
let value = extractValue(ctx, 125);
|
|
22352
|
+
if (t && typeof (t[k] = value) === "object") seen.add(value);
|
|
22353
|
+
skipVoid(ctx);
|
|
22354
|
+
if ((c = ctx.s.charCodeAt(ctx.p++)) === 125) return res;
|
|
22355
|
+
if (c !== 44) TomlError.x("expected comma or end of structure", ctx, ctx.p - 1);
|
|
22356
|
+
}
|
|
22357
|
+
TomlError.x("unfinished table", ctx, startPtr);
|
|
22358
|
+
}
|
|
22359
|
+
/** @internal */
|
|
22360
|
+
function parseArray(ctx) {
|
|
22361
|
+
let startPtr = ctx.p++;
|
|
22362
|
+
let res = [];
|
|
22363
|
+
let c;
|
|
22364
|
+
while (ctx.p < ctx.s.length) {
|
|
22365
|
+
skipVoid(ctx);
|
|
22366
|
+
if ((c = ctx.s.charCodeAt(ctx.p)) === 93) {
|
|
22367
|
+
ctx.p++;
|
|
22368
|
+
return res;
|
|
22369
|
+
}
|
|
22370
|
+
res.push(extractValue(ctx, 93));
|
|
22371
|
+
skipVoid(ctx);
|
|
22372
|
+
if ((c = ctx.s.charCodeAt(ctx.p++)) === 93) return res;
|
|
22373
|
+
if (c !== 44) TomlError.x("expected comma or end of structure", ctx, ctx.p - 1);
|
|
22374
|
+
}
|
|
22375
|
+
TomlError.x("unfinished array", ctx, startPtr);
|
|
22376
|
+
}
|
|
22377
|
+
//#endregion
|
|
22378
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/parse.js
|
|
22379
|
+
/*!
|
|
22380
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
22381
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
22382
|
+
*
|
|
22383
|
+
* Redistribution and use in source and binary forms, with or without
|
|
22384
|
+
* modification, are permitted provided that the following conditions are met:
|
|
22385
|
+
*
|
|
22386
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
22387
|
+
* list of conditions and the following disclaimer.
|
|
22388
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22389
|
+
* this list of conditions and the following disclaimer in the
|
|
22390
|
+
* documentation and/or other materials provided with the distribution.
|
|
22391
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
22392
|
+
* may be used to endorse or promote products derived from this software without
|
|
22393
|
+
* specific prior written permission.
|
|
22394
|
+
*
|
|
22395
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22396
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22397
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22398
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22399
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22400
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22401
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22402
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22403
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22404
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22405
|
+
*/
|
|
22406
|
+
function peekTable(ctx, key, table, meta, type) {
|
|
22407
|
+
let t = table;
|
|
22408
|
+
let m = meta;
|
|
22409
|
+
let k;
|
|
22410
|
+
let hasOwn = false;
|
|
22411
|
+
let state;
|
|
22412
|
+
for (let i = 0; i < key.length; i++) {
|
|
22413
|
+
if (i) {
|
|
22414
|
+
t = hasOwn ? t[k] : t[k] = Object.create(null);
|
|
22415
|
+
m = (state = m[k]).c;
|
|
22416
|
+
if (type === 0 && (state.t === 1 || state.t === 2)) return null;
|
|
22417
|
+
if (state.t === 2) {
|
|
22418
|
+
let l = t.length - 1;
|
|
22419
|
+
t = t[l];
|
|
22420
|
+
m = m[l].c;
|
|
22421
|
+
}
|
|
22422
|
+
}
|
|
22423
|
+
k = key[i];
|
|
22424
|
+
if ((hasOwn = Object.hasOwn(t, k)) && m[k]?.t === 0 && m[k]?.d) return null;
|
|
22425
|
+
if (!hasOwn) {
|
|
22426
|
+
let unsafe = k === "__proto__";
|
|
22427
|
+
if (ctx.uk && (unsafe || k === "constructor")) return false;
|
|
22428
|
+
if (unsafe) {
|
|
22429
|
+
Object.defineProperty(t, k, {
|
|
22430
|
+
enumerable: true,
|
|
22431
|
+
configurable: true,
|
|
22432
|
+
writable: true
|
|
22433
|
+
});
|
|
22434
|
+
Object.defineProperty(m, k, {
|
|
22435
|
+
enumerable: true,
|
|
22436
|
+
configurable: true,
|
|
22437
|
+
writable: true
|
|
22438
|
+
});
|
|
22439
|
+
}
|
|
22440
|
+
m[k] = {
|
|
22441
|
+
t: i < key.length - 1 && type === 2 ? 3 : type,
|
|
22442
|
+
d: false,
|
|
22443
|
+
i: 0,
|
|
22444
|
+
c: Object.create(null)
|
|
22445
|
+
};
|
|
22446
|
+
}
|
|
22447
|
+
}
|
|
22448
|
+
state = m[k];
|
|
22449
|
+
if (state.t !== type && !(type === 1 && state.t === 3)) return null;
|
|
22450
|
+
if (type === 2) {
|
|
22451
|
+
if (!state.d) {
|
|
22452
|
+
state.d = true;
|
|
22453
|
+
t[k] = [];
|
|
22454
|
+
}
|
|
22455
|
+
t[k].push(t = Object.create(null));
|
|
22456
|
+
state.c[state.i++] = state = {
|
|
22457
|
+
t: 1,
|
|
22458
|
+
d: false,
|
|
22459
|
+
i: 0,
|
|
22460
|
+
c: Object.create(null)
|
|
22461
|
+
};
|
|
22462
|
+
}
|
|
22463
|
+
if (state.d) return null;
|
|
22464
|
+
state.d = true;
|
|
22465
|
+
if (type === 1) t = hasOwn ? t[k] : t[k] = Object.create(null);
|
|
22466
|
+
else if (type === 0 && hasOwn) return null;
|
|
22467
|
+
return [
|
|
22468
|
+
k,
|
|
22469
|
+
t,
|
|
22470
|
+
state.c
|
|
22471
|
+
];
|
|
22472
|
+
}
|
|
22473
|
+
function validateTablePeek(ctx, peek, ptr) {
|
|
22474
|
+
if (peek === null || ctx.uk === 2) TomlError.x(peek === null ? "trying to redefine an already defined table or value" : "document contains an unsafe property", ctx, ptr);
|
|
22475
|
+
}
|
|
22476
|
+
function parse(toml, options = {}) {
|
|
22477
|
+
let ctx = {
|
|
22478
|
+
s: toml,
|
|
22479
|
+
p: 0,
|
|
22480
|
+
d: options.maxDepth ?? 1e3,
|
|
22481
|
+
bi: options.integersAsBigInt ?? false,
|
|
22482
|
+
ld: options.useLegacyDate ?? true,
|
|
22483
|
+
uk: options.unsafeKeyBehaviour === "throw" ? 2 : options.unsafeKeyBehaviour === "drop" ? 1 : 0
|
|
22484
|
+
};
|
|
22485
|
+
let res = Object.create(null);
|
|
22486
|
+
let meta = Object.create(null);
|
|
22487
|
+
let tmp;
|
|
22488
|
+
let skipping = false;
|
|
22489
|
+
let tbl = res;
|
|
22490
|
+
let m = meta;
|
|
22491
|
+
if (toml.charCodeAt(0) === 65279) ctx.p++;
|
|
22492
|
+
skipVoid(ctx);
|
|
22493
|
+
while (ctx.p < toml.length) {
|
|
22494
|
+
if (toml.charCodeAt(ctx.p) === 91) {
|
|
22495
|
+
let isTableArray = toml.charCodeAt(++ctx.p) === 91;
|
|
22496
|
+
tmp = ctx.p += +isTableArray;
|
|
22497
|
+
skipping = false;
|
|
22498
|
+
let k = parseKey(ctx, 93);
|
|
22499
|
+
if (isTableArray) {
|
|
22500
|
+
if (toml.charCodeAt(ctx.p) !== 93) TomlError.x("expected end of table array declaration", ctx);
|
|
22501
|
+
ctx.p++;
|
|
22502
|
+
}
|
|
22503
|
+
let p = peekTable(ctx, k, res, meta, isTableArray ? 2 : 1);
|
|
22504
|
+
if (!p) {
|
|
22505
|
+
validateTablePeek(ctx, p, tmp);
|
|
22506
|
+
skipping = true;
|
|
22507
|
+
} else {
|
|
22508
|
+
m = p[2];
|
|
22509
|
+
tbl = p[1];
|
|
22510
|
+
}
|
|
22511
|
+
} else {
|
|
22512
|
+
tmp = ctx.p;
|
|
22513
|
+
let p = peekTable(ctx, parseKey(ctx), tbl, m, 0);
|
|
22514
|
+
if (!p && !skipping) validateTablePeek(ctx, p, tmp);
|
|
22515
|
+
skipVoid(ctx, true, true);
|
|
22516
|
+
let v = extractValue(ctx, void 0);
|
|
22517
|
+
if (p && !skipping) p[1][p[0]] = v;
|
|
22518
|
+
}
|
|
22519
|
+
skipVoid(ctx, true);
|
|
22520
|
+
if (ctx.p < toml.length && (tmp = toml.charCodeAt(ctx.p)) !== 10 && (tmp !== 13 || toml.charCodeAt(ctx.p + 1) !== 10)) TomlError.x("each key-value declaration must be followed by an end-of-line", ctx);
|
|
22521
|
+
skipVoid(ctx);
|
|
22522
|
+
}
|
|
22523
|
+
return res;
|
|
22524
|
+
}
|
|
22525
|
+
/*!
|
|
22526
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
22527
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
22528
|
+
*
|
|
22529
|
+
* Redistribution and use in source and binary forms, with or without
|
|
22530
|
+
* modification, are permitted provided that the following conditions are met:
|
|
22531
|
+
*
|
|
22532
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
22533
|
+
* list of conditions and the following disclaimer.
|
|
22534
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22535
|
+
* this list of conditions and the following disclaimer in the
|
|
22536
|
+
* documentation and/or other materials provided with the distribution.
|
|
22537
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
22538
|
+
* may be used to endorse or promote products derived from this software without
|
|
22539
|
+
* specific prior written permission.
|
|
22540
|
+
*
|
|
22541
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22542
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22543
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22544
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22545
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22546
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22547
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22548
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22549
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22550
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22551
|
+
*/
|
|
22552
|
+
"".isWellFormed;
|
|
22553
|
+
//#endregion
|
|
22554
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/index.js
|
|
22555
|
+
/*!
|
|
22556
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
22557
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
22558
|
+
*
|
|
22559
|
+
* Redistribution and use in source and binary forms, with or without
|
|
22560
|
+
* modification, are permitted provided that the following conditions are met:
|
|
22561
|
+
*
|
|
22562
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
22563
|
+
* list of conditions and the following disclaimer.
|
|
22564
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22565
|
+
* this list of conditions and the following disclaimer in the
|
|
22566
|
+
* documentation and/or other materials provided with the distribution.
|
|
22567
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
22568
|
+
* may be used to endorse or promote products derived from this software without
|
|
22569
|
+
* specific prior written permission.
|
|
22570
|
+
*
|
|
22571
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22572
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22573
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22574
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22575
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22576
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22577
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22578
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22579
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22580
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22581
|
+
*/
|
|
22582
|
+
//#endregion
|
|
22583
|
+
//#region src/harness/codex-checkouts.ts
|
|
22584
|
+
const codexGitFolder = (configFolder, pluginId) => {
|
|
22585
|
+
const at = pluginId.indexOf("@");
|
|
22586
|
+
return `${configFolder}/plugins/cache/${pluginId.slice(at + 1)}/${pluginId.slice(0, at)}/@git`;
|
|
22587
|
+
};
|
|
22588
|
+
const codexCheckoutsFolder = (gitFolder) => `${gitFolder}/checkouts`;
|
|
22589
|
+
/** Parse Git's quoted parameters as data, never shell code, and keep every other `-c`. */
|
|
22590
|
+
const withoutCodexInclude = String.raw`
|
|
22591
|
+
use Text::ParseWords qw(shellwords);
|
|
22592
|
+
my @parameters = shellwords($ENV{GIT_CONFIG_PARAMETERS} // '');
|
|
22593
|
+
my $root = '';
|
|
22594
|
+
@parameters = grep { if (m{^include\.path=(.*/plugins/cache/[^/]+/[^/]+/\@git)/agents/[^/]+\.gitconfig$}) { $root = $1; 0 } else { 1 } } @parameters;
|
|
22595
|
+
if (($ARGV[0] // '') eq 'root') { print $root; exit; }
|
|
22596
|
+
for (@parameters) { s/'/'\\''/g; $_ = "'$_'"; }
|
|
22597
|
+
print join(' ', @parameters);
|
|
22598
|
+
`;
|
|
22599
|
+
/**
|
|
22600
|
+
* Both session start and the sign-off refusal use this writer. Perl's flock works on Linux
|
|
22601
|
+
* and macOS and is released even after a crash. One lock covers reading the shared includes,
|
|
22602
|
+
* writing a checkout's overrides, replacing the includes, and restoring the agent's file.
|
|
22603
|
+
* Git writes the include keys so unusual checkout names can't become config syntax. Its
|
|
22604
|
+
* gitdir conditions escape wildcard characters and cover the main checkout and worktrees.
|
|
22605
|
+
*/
|
|
22606
|
+
const codexCheckoutWriter = String.raw`
|
|
22607
|
+
use strict;
|
|
22608
|
+
use warnings;
|
|
22609
|
+
use Fcntl qw(LOCK_EX);
|
|
22610
|
+
use File::Path qw(make_path);
|
|
22611
|
+
use File::Temp qw(tempfile);
|
|
22612
|
+
use Digest::SHA qw(sha256_hex);
|
|
22613
|
+
umask 0077;
|
|
22614
|
+
my ($directory, $common, $hooks, $signs_off, $author_file, $author_text, $clear_author, $activate_author) = @ARGV;
|
|
22615
|
+
make_path($directory);
|
|
22616
|
+
open my $lock, '>>', "$directory/lock" or die "open checkout lock: $!\n";
|
|
22617
|
+
$SIG{ALRM} = sub { die "checkout lock timed out\n" };
|
|
22618
|
+
alarm 5;
|
|
22619
|
+
flock($lock, LOCK_EX) or die "lock checkouts: $!\n";
|
|
22620
|
+
alarm 0;
|
|
22621
|
+
sub quoted {
|
|
22622
|
+
my $value = shift;
|
|
22623
|
+
$value =~ s/\\/\\\\/g;
|
|
22624
|
+
$value =~ s/"/\\"/g;
|
|
22625
|
+
$value =~ s/\n/\\n/g;
|
|
22626
|
+
$value =~ s/\t/\\t/g;
|
|
22627
|
+
$value =~ s/\x08/\\b/g;
|
|
22628
|
+
return qq{"$value"};
|
|
22629
|
+
}
|
|
22630
|
+
sub replace_file {
|
|
22631
|
+
my ($file, $text) = @_;
|
|
22632
|
+
my ($out, $next) = tempfile('.ref0-XXXXXX', DIR => $directory, UNLINK => 1);
|
|
22633
|
+
print {$out} $text or die "write config: $!\n";
|
|
22634
|
+
close $out or die "close config: $!\n";
|
|
22635
|
+
rename $next, $file or die "replace config: $!\n";
|
|
22636
|
+
}
|
|
22637
|
+
my $disabled = "# ref0: author disabled until commit setup succeeds\n";
|
|
22638
|
+
if (defined $clear_author && length $clear_author) {
|
|
22639
|
+
my $text = $author_text // '';
|
|
22640
|
+
if (-e $clear_author) {
|
|
22641
|
+
open my $in, '<', $clear_author or die "read author: $!\n";
|
|
22642
|
+
local $/;
|
|
22643
|
+
$text = <$in>;
|
|
22644
|
+
close $in or die "close author: $!\n";
|
|
22645
|
+
}
|
|
22646
|
+
$text =~ s/^\[author\]\n.*?(?=^\[|\z)//msg;
|
|
22647
|
+
$text = $disabled . $text unless index($text, $disabled) == 0;
|
|
22648
|
+
replace_file($clear_author, $text);
|
|
22649
|
+
exit;
|
|
22650
|
+
}
|
|
22651
|
+
if (length $common) {
|
|
22652
|
+
my $checkout = "$directory/" . sha256_hex($common) . '.gitconfig';
|
|
22653
|
+
my $text = "[core]\n\thooksPath = " . quoted($hooks) . "\n";
|
|
22654
|
+
$text .= "[author]\n\tname = \"\"\n\temail = \"\"\n" if $signs_off;
|
|
22655
|
+
my $includes = "$directory/checkouts.gitconfig";
|
|
22656
|
+
my $before = '';
|
|
22657
|
+
if (-e $includes) {
|
|
22658
|
+
open my $in, '<', $includes or die "read checkouts: $!\n";
|
|
22659
|
+
local $/;
|
|
22660
|
+
$before = <$in>;
|
|
22661
|
+
close $in or die "close checkouts: $!\n";
|
|
22662
|
+
}
|
|
22663
|
+
my ($out, $next) = tempfile('.ref0-XXXXXX', DIR => $directory, UNLINK => 1);
|
|
22664
|
+
print {$out} $before or die "write checkouts: $!\n";
|
|
22665
|
+
close $out or die "close checkouts: $!\n";
|
|
22666
|
+
(my $pattern = $common) =~ s/([\\*?\[])/\\$1/g;
|
|
22667
|
+
for my $gitdir ($pattern, "$pattern/worktrees/") {
|
|
22668
|
+
system('git', 'config', '--file', $next, '--replace-all', "includeIf.gitdir:$gitdir.path", $checkout) == 0
|
|
22669
|
+
or die "write checkout include\n";
|
|
22670
|
+
}
|
|
22671
|
+
replace_file($checkout, $text);
|
|
22672
|
+
rename $next, $includes or die "replace checkouts: $!\n";
|
|
22673
|
+
}
|
|
22674
|
+
if (defined $author_file && length $author_file) {
|
|
22675
|
+
# An in-flight old session cannot restore an author cleared after a failed switch.
|
|
22676
|
+
my $may_write = 1;
|
|
22677
|
+
if (!$activate_author && -e $author_file) {
|
|
22678
|
+
open my $in, '<', $author_file or die "read author: $!\n";
|
|
22679
|
+
my $first = <$in> // '';
|
|
22680
|
+
close $in or die "close author: $!\n";
|
|
22681
|
+
$may_write = 0 if $first eq $disabled;
|
|
22682
|
+
}
|
|
22683
|
+
replace_file($author_file, $author_text) if $may_write;
|
|
22684
|
+
}
|
|
22685
|
+
`;
|
|
22686
|
+
/** The same writer inside a hook, where the next plain commit must use the person as author. */
|
|
22687
|
+
const codexSignOff = [
|
|
22688
|
+
"ref0_common=$(git rev-parse --path-format=absolute --git-common-dir) || exit 1",
|
|
22689
|
+
"ref0_common=$(cd -P \"$ref0_common\" && pwd) || exit 1",
|
|
22690
|
+
"[ -n \"${ref0_codex-}\" ] || exit 1",
|
|
22691
|
+
"ref0_codex_hooks=$ref0_hooks/most",
|
|
22692
|
+
"for ref0_frequent in post-index-change reference-transaction; do",
|
|
22693
|
+
" ref0_own=$(git rev-parse --git-path \"hooks/$ref0_frequent\") || exit 1",
|
|
22694
|
+
" [ ! -x \"$ref0_own\" ] || ref0_codex_hooks=$ref0_hooks/all",
|
|
22695
|
+
"done",
|
|
22696
|
+
`perl -e ${shellWord(codexCheckoutWriter)} "$ref0_codex/checkouts" "$ref0_common" "$ref0_codex_hooks" 1 || exit 1`
|
|
22697
|
+
];
|
|
22698
|
+
//#endregion
|
|
22699
|
+
//#region src/harness/empty-folder.ts
|
|
22700
|
+
/**
|
|
22701
|
+
* The folder one of a harness's own commands runs in: made empty for it in the system's temporary
|
|
22702
|
+
* folder, and removed once the command's scope ends. ref0 never runs one in the folder the person
|
|
22703
|
+
* runs ref0 in, and names the config folder only by `CLAUDE_CONFIG_DIR` or `CODEX_HOME`.
|
|
22704
|
+
*
|
|
22705
|
+
* Measured 24 Oct 2026. Claude Code 2.1.310's `plugin marketplace remove` also clears the name
|
|
22706
|
+
* from `.claude/settings.json` and `.claude/settings.local.json` in the folder it runs in, and
|
|
22707
|
+
* `plugin list` reads them. In the home folder, the first is `~/.claude`'s own settings: removing
|
|
22708
|
+
* ref0's marketplace from `~/.claude-work` there turned ref0's plugin off in `~/.claude`. Codex
|
|
22709
|
+
* 0.164.0 reads a trusted project's `.codex/` in each folder from its own up to the Git root: from
|
|
22710
|
+
* a folder under a home kept in Git, such as `~/.ref0`, it reads `~/.codex` as that project's too.
|
|
22711
|
+
*/
|
|
22712
|
+
const emptyFolder = (fs) => fs.makeTempDirectoryScoped({ prefix: "ref0-harness-" });
|
|
22713
|
+
//#endregion
|
|
22714
|
+
//#region src/harness/codex-app-server.ts
|
|
22715
|
+
/**
|
|
22716
|
+
* Codex's app server, spoken as its TUI speaks it: `codex app-server` started for one exchange,
|
|
22717
|
+
* from an empty folder of its own, the requests written a line each, the answers read until each
|
|
22718
|
+
* request has one, then stopped. Each answer is matched to its request by ID, and read by that
|
|
22719
|
+
* request's method. Anything else it writes, such as notifications, or an answer to an ID ref0
|
|
22720
|
+
* didn't send, is passed over.
|
|
22721
|
+
*/
|
|
22722
|
+
/** Codex's app server didn't answer a request within 15 seconds, or didn't start. */
|
|
22723
|
+
var CodexUnanswered = class extends Schema.TaggedError()("CodexUnanswered", { method: Schema.String }) {};
|
|
22724
|
+
/** Codex's app server answered a request with an error. */
|
|
22725
|
+
var CodexRefused = class extends Schema.TaggedError()("CodexRefused", {
|
|
22726
|
+
method: Schema.String,
|
|
22727
|
+
error: CodexError
|
|
22728
|
+
}) {};
|
|
22729
|
+
/** Codex's app server answered a request with a result its method's schema doesn't read. */
|
|
22730
|
+
var CodexAnswerMismatch = class extends Schema.TaggedError()("CodexAnswerMismatch", {
|
|
22731
|
+
method: Schema.String,
|
|
22732
|
+
issue: Schema.String
|
|
22733
|
+
}) {};
|
|
22734
|
+
const encodeRequest = Schema.encodeUnknownEffect(Schema.fromJsonString(CodexRequest));
|
|
22735
|
+
const decodeAnswer$2 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
|
|
22736
|
+
/** The IDs of the exchange's two requests: the opening's, and the one asked. */
|
|
22737
|
+
const initializeId = "ref0-initialize";
|
|
22738
|
+
const askedId = "ref0-asked";
|
|
22739
|
+
/** The exchange's opening: who's asking, then that it's ready. */
|
|
22740
|
+
const opening = [{
|
|
22741
|
+
jsonrpc: "2.0",
|
|
22742
|
+
id: initializeId,
|
|
22743
|
+
method: codexMethods.initialize.method,
|
|
22744
|
+
params: { clientInfo: {
|
|
22745
|
+
name: "ref0",
|
|
22746
|
+
title: null,
|
|
22747
|
+
version: cliVersion
|
|
22748
|
+
} }
|
|
22749
|
+
}, {
|
|
22750
|
+
jsonrpc: "2.0",
|
|
22751
|
+
method: codexMethods.initialized.method
|
|
22752
|
+
}];
|
|
22753
|
+
/**
|
|
22754
|
+
* Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
|
|
22755
|
+
* answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
|
|
22756
|
+
*/
|
|
22757
|
+
const exchange = (codex, lines, ids, env) => Effect.scoped(Effect.gen(function* () {
|
|
22758
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
22759
|
+
const fs = yield* FileSystem.FileSystem;
|
|
22760
|
+
const answered = yield* (yield* spawner.spawn(ChildProcess.make(codex, ["app-server"], {
|
|
22761
|
+
cwd: yield* emptyFolder(fs),
|
|
22762
|
+
env,
|
|
22763
|
+
extendEnv: true,
|
|
22764
|
+
forceKillAfter: "2 seconds",
|
|
22765
|
+
stdin: {
|
|
22766
|
+
stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
|
|
22767
|
+
endOnDone: false
|
|
22768
|
+
}
|
|
22769
|
+
}))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.map((line) => decodeAnswer$2(line)), Stream.filter(Option.isSome), Stream.scan(() => /* @__PURE__ */ new Map(), (answers, { value: answer }) => ids.includes(answer.id) && !answers.has(answer.id) ? new Map(answers).set(answer.id, answer) : answers), Stream.takeUntil((answers) => answers.size === ids.length), Stream.runLast);
|
|
22770
|
+
return Option.filter(answered, (answers) => answers.size === ids.length);
|
|
22771
|
+
})).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
|
|
22772
|
+
/** `answer`'s result, read by `method`'s own schema, or why it can't be. */
|
|
22773
|
+
const resultOf = (method, answer) => {
|
|
22774
|
+
if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
|
|
22775
|
+
if ("error" in answer) return Effect.fail(new CodexRefused({
|
|
22776
|
+
method: method.method,
|
|
22777
|
+
error: answer.error
|
|
22778
|
+
}));
|
|
22779
|
+
const { result } = method;
|
|
22780
|
+
return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
|
|
22781
|
+
method: method.method,
|
|
22782
|
+
issue: error.message
|
|
22783
|
+
})));
|
|
22784
|
+
};
|
|
22785
|
+
/**
|
|
22786
|
+
* One exchange with Codex's app server under the person's Codex home: the opening, then `method`
|
|
22787
|
+
* asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
|
|
22788
|
+
* its request's ID, and its result is read by the method's result schema.
|
|
22789
|
+
*/
|
|
22790
|
+
const askCodex = (codex, method, params, env = {}) => Effect.gen(function* () {
|
|
22791
|
+
const asked = {
|
|
22792
|
+
jsonrpc: "2.0",
|
|
22793
|
+
id: askedId,
|
|
22794
|
+
method: method.method,
|
|
22795
|
+
params
|
|
22796
|
+
};
|
|
22797
|
+
const lines = yield* Effect.forEach([...opening, asked], (request) => encodeRequest(request).pipe(Effect.orDie));
|
|
22798
|
+
const answers = yield* exchange(codex, lines, [initializeId, askedId], env);
|
|
22799
|
+
if (Option.isNone(answers)) return yield* new CodexUnanswered({ method: method.method });
|
|
22800
|
+
yield* resultOf(codexMethods.initialize, answers.value.get(initializeId));
|
|
22801
|
+
return yield* resultOf(method, answers.value.get(askedId));
|
|
22802
|
+
});
|
|
22803
|
+
//#endregion
|
|
22804
|
+
//#region src/harness/commit-hooks.ts
|
|
22805
|
+
/**
|
|
22806
|
+
* ref0's part of Git's `prepare-commit-msg` and `commit-msg` hooks, which their forwarders in
|
|
22807
|
+
* ref0's hooks folder run once the repo's own hook succeeds, and only in an agent's commands,
|
|
22808
|
+
* where `REF0_HARNESS` is set. A commit anywhere else is unchanged.
|
|
22809
|
+
*
|
|
22810
|
+
* The trailer, `Assisted-by: <harness>:<model>`, goes on once with `git interpret-trailers`:
|
|
22811
|
+
* `prepare-commit-msg` adds it to a message the command or Git wrote, and `commit-msg` to one
|
|
22812
|
+
* written in an editor, or again if an editor removed it, since Git runs `prepare-commit-msg`
|
|
22813
|
+
* before the editor and `commit-msg` after. Neither adds it to a message Git refuses as empty, or
|
|
22814
|
+
* to `commit.template` left as it was, which Git would otherwise commit. Two commits made in an
|
|
22815
|
+
* editor are beyond them: one with `--no-verify`, which skips `commit-msg`, gets no trailer, and
|
|
22816
|
+
* a template from `-t`, which `commit-msg` can't see, gets it even when left as it was. The model
|
|
22817
|
+
* is the last one the session's transcript names, read from its end in `sh`, with no Node
|
|
22818
|
+
* process, or else the one the session started with. Both are as the harness reports them, never
|
|
22819
|
+
* proven.
|
|
22820
|
+
*
|
|
22821
|
+
* The refusal: a commit whose author is an agent's commit email and whose message has a
|
|
22822
|
+
* `Signed-off-by`. A sign-off must match its commit's author, and an agent never signs off.
|
|
22823
|
+
* `prepare-commit-msg` catches `-s` and `-m`, which `--no-verify` doesn't skip, and `commit-msg`
|
|
22824
|
+
* a sign-off written in an editor, unless `--no-verify` skips it.
|
|
22825
|
+
*
|
|
22826
|
+
* Only the refusal stops a commit: when Git can't say what ref0's part needs, such as a Git older
|
|
22827
|
+
* than 2.20 without `--no-divider`, the commit goes on without the trailer.
|
|
22828
|
+
*
|
|
22829
|
+
* Neither part runs while Git replays commits, in a rebase or a cherry-pick: Git makes those
|
|
22830
|
+
* commits itself with their own authors, and these hooks see the command's author instead (Git
|
|
22831
|
+
* 2.53, 23 Oct 2026). Nor does the trailer go on an amend that keeps another's commit: `git
|
|
22832
|
+
* commit` gives its hooks the author it commits with, and an amend keeps HEAD's, to the second.
|
|
22833
|
+
* So a new commit by HEAD's author in HEAD's second gets none either.
|
|
22834
|
+
*/
|
|
22835
|
+
/** The hooks ref0 adds its part to, after the repo's own. */
|
|
22836
|
+
const commitHooks = ["prepare-commit-msg", "commit-msg"];
|
|
22837
|
+
/** Whether a hook is one ref0 adds its part to. */
|
|
22838
|
+
const isCommitHook = (name) => commitHooks.some((hook) => hook === name);
|
|
22839
|
+
/** The trailer's key, as the Linux kernel writes it. */
|
|
22840
|
+
const assistedBy = "Assisted-by";
|
|
22841
|
+
/** How much of the transcript's end the hook reads for its last model: well past its last turn. */
|
|
22842
|
+
const transcriptTail = 1048576;
|
|
22843
|
+
/**
|
|
22844
|
+
* Git's trailer tool, reading and writing `:` alone whatever the repo's `trailer.separators`
|
|
22845
|
+
* says, since Git writes a sign-off with `:`.
|
|
22846
|
+
*/
|
|
22847
|
+
const trailers = "git -c trailer.separators=: interpret-trailers --no-divider";
|
|
22848
|
+
/**
|
|
22849
|
+
* Whether its input says anything, as Git decides whether a message is empty: with a line that's
|
|
22850
|
+
* neither blank nor a sign-off.
|
|
22851
|
+
*/
|
|
22852
|
+
const saysSomething = "grep -qv -e '^[[:space:]]*$' -e '^Signed-off-by: '";
|
|
22853
|
+
/** How the agent commits again as the person, which the refusal prints. */
|
|
22854
|
+
const asThePerson = "env -u GIT_AUTHOR_NAME -u GIT_AUTHOR_EMAIL git commit -s …";
|
|
22855
|
+
/** What the refusal prints, for a commit by `author` that the person `person` should author. */
|
|
22856
|
+
const signOffRefusal = (author, person) => `ref0: this commit has a Signed-off-by, and its author is ${author}, an agent. A sign-off must match its commit's author, and an agent never signs off, so commit again with ${person} as the author, adding --reset-author to an amend:\n ${asThePerson}\n`;
|
|
22857
|
+
/** ref0's part of `hook`, run once the repo's own hook has succeeded. */
|
|
22858
|
+
const commitPart = (hook) => {
|
|
22859
|
+
const { harness, transcript, model } = gitHookVariables;
|
|
22860
|
+
return [
|
|
22861
|
+
"ref0_nl='",
|
|
22862
|
+
"'",
|
|
22863
|
+
`[ -n "\${${harness}-}" ] || exit 0`,
|
|
22864
|
+
`case $${harness} in *[!a-z0-9-]*) exit 0 ;; esac`,
|
|
22865
|
+
"ref0_git=$(git rev-parse --git-dir) || exit 0",
|
|
22866
|
+
"[ -d \"$ref0_git/rebase-merge\" ] || [ -d \"$ref0_git/rebase-apply\" ] || [ -f \"$ref0_git/CHERRY_PICK_HEAD\" ] && exit 0",
|
|
22867
|
+
`ref0_trailers=$(${trailers} --parse "$1") || exit 0`,
|
|
22868
|
+
"case ${GIT_AUTHOR_EMAIL-} in",
|
|
22869
|
+
`*@${agentCommitDomain})`,
|
|
22870
|
+
" if printf '%s\\n' \"$ref0_trailers\" | grep -qi '^signed-off-by:'; then",
|
|
22871
|
+
" ref0_person=$(git var GIT_COMMITTER_IDENT)",
|
|
22872
|
+
` if [ "$${harness}" = codex ]; then`,
|
|
22873
|
+
...codexSignOff.map((line) => ` ${line}`),
|
|
22874
|
+
" printf '%s\\n' 'ref0: this checkout now uses the person as author. Run the same git commit -s again; add --reset-author to an amend.' >&2",
|
|
22875
|
+
" else",
|
|
22876
|
+
` printf ${shellWord(signOffRefusal("%s", "%s"))} "\${GIT_AUTHOR_NAME-} <$GIT_AUTHOR_EMAIL>" "\${ref0_person% * *}" >&2`,
|
|
22877
|
+
" fi",
|
|
22878
|
+
" exit 1",
|
|
22879
|
+
" fi",
|
|
22880
|
+
" ;;",
|
|
22881
|
+
"*)",
|
|
22882
|
+
" if [ -n \"${GIT_AUTHOR_DATE-}\" ]; then",
|
|
22883
|
+
` ref0_head=$(git log -1 --no-show-signature --date=raw --format='%(trailers:key=${assistedBy},valueonly)%n%an <%ae> %ad' HEAD 2>/dev/null)`,
|
|
22884
|
+
" [ \"${ref0_head##*\"$ref0_nl\"}\" = \"${GIT_AUTHOR_NAME-} <${GIT_AUTHOR_EMAIL-}> ${GIT_AUTHOR_DATE#@}\" ] && [ -z \"${ref0_head%\"$ref0_nl\"*}\" ] && exit 0",
|
|
22885
|
+
" fi",
|
|
22886
|
+
" ;;",
|
|
22887
|
+
"esac",
|
|
22888
|
+
...hook === "prepare-commit-msg" ? ["case ${2-} in ''|template) exit 0 ;; esac"] : [],
|
|
22889
|
+
`case "$ref0_nl$ref0_trailers$ref0_nl" in *"\${ref0_nl}${assistedBy}: $${harness}:"*|*"\${ref0_nl}${assistedBy}: $${harness}$ref0_nl"*) exit 0 ;; esac`,
|
|
22890
|
+
...hook === "prepare-commit-msg" ? [`${saysSomething} "$1" || exit 0`] : [
|
|
22891
|
+
"ref0_message=$(git stripspace --strip-comments < \"$1\")",
|
|
22892
|
+
`printf '%s\\n' "$ref0_message" | ${saysSomething} || exit 0`,
|
|
22893
|
+
`ref0_template=$(git config --path commit.template) && ref0_template=$(git stripspace --strip-comments < "$ref0_template" 2>/dev/null) && case $ref0_message in "$ref0_template"*) printf '%s\\n' "\${ref0_message#"$ref0_template"}" | ${saysSomething} || exit 0 ;; esac`
|
|
22894
|
+
],
|
|
22895
|
+
"ref0_model=",
|
|
22896
|
+
`if [ "$${harness}" = codex ]; then`,
|
|
22897
|
+
" case ${CODEX_THREAD_ID-} in \"\"|*[!a-zA-Z0-9-]*) ;; *)",
|
|
22898
|
+
" ref0_rollout=$(find \"${CODEX_HOME:-\"$HOME/.codex\"}/sessions\" -type f -name \"rollout-*-${CODEX_THREAD_ID}.jsonl\" 2>/dev/null | head -n 1)",
|
|
22899
|
+
` [ -f "$ref0_rollout" ] && ref0_model=$(tail -c ${transcriptTail} "$ref0_rollout" | LC_ALL=C grep '"type"[[:space:]]*:[[:space:]]*"turn_context"' | grep -o '"model"[[:space:]]*:[[:space:]]*"[A-Za-z0-9][A-Za-z0-9._:@/-]*"' | tail -n 1 | sed 's/^[^:]*:[[:space:]]*"//; s/"$//')`,
|
|
22900
|
+
" ;; esac",
|
|
22901
|
+
"else",
|
|
22902
|
+
`[ -f "\${${transcript}-}" ] && ref0_model=$(tail -c ${transcriptTail} "$${transcript}" | LC_ALL=C grep -o '"message":{"model":"[A-Za-z0-9][A-Za-z0-9._:@/-]*"')`,
|
|
22903
|
+
"ref0_model=${ref0_model##*\\\"model\\\":\\\"}",
|
|
22904
|
+
"ref0_model=${ref0_model%\\\"}",
|
|
22905
|
+
`[ -n "$ref0_model" ] || ref0_model=\${${model}-}`,
|
|
22906
|
+
"fi",
|
|
22907
|
+
"case $ref0_model in *[!A-Za-z0-9._:@/-]*) ref0_model= ;; esac",
|
|
22908
|
+
`${trailers} --in-place --if-exists addIfDifferent --trailer "${assistedBy}: $${harness}\${ref0_model:+:$ref0_model}" "$1"`,
|
|
22909
|
+
"exit 0"
|
|
22910
|
+
];
|
|
22911
|
+
};
|
|
22912
|
+
//#endregion
|
|
22913
|
+
//#region src/harness/git-hooks.ts
|
|
22914
|
+
/**
|
|
22915
|
+
* ref0's Git hooks: folders of forwarders in `~/.ref0/origins/<origin>/git-hooks`, which ref0 names as
|
|
22916
|
+
* `core.hooksPath` in its agents' commands alone, never in a repo, its config or its hooks.
|
|
22917
|
+
* `core.hooksPath` replaces a repo's whole hooks folder, so each forwarder runs the repo's own
|
|
22918
|
+
* hook of its name, with the same arguments, stdin and exit status, wherever the repo keeps it:
|
|
22919
|
+
* husky's `.husky/_`, `.git/hooks/` or another `core.hooksPath`. It finds and runs that hook
|
|
22920
|
+
* with ref0's entry left out of its environment, so a hook that looks up another, as
|
|
22921
|
+
* `pre-applypatch.sample` runs `pre-commit`, finds the repo's own. A forwarder runs nothing
|
|
22922
|
+
* when the repo's hooks path is one of ref0's folders, or when a forwarder started it, so none
|
|
22923
|
+
* calls itself: a first version that left ref0's entry in started about 1,000 processes. Only
|
|
22924
|
+
* `prepare-commit-msg` and `commit-msg` do more: once the repo's own hook succeeds, they add
|
|
22925
|
+
* ref0's part, in `commit-hooks.ts`.
|
|
22926
|
+
*
|
|
22927
|
+
* Git runs a hook that exists even where the repo has none, so the forwarders cost each hook one
|
|
22928
|
+
* `sh` and one `git rev-parse`, about 1.5 ms. Two hooks run on every index write and ref update,
|
|
22929
|
+
* hundreds of times in a long rebase, so only `all` forwards them, for a checkout that has its
|
|
22930
|
+
* own; `most` forwards every other (measured 23 Oct 2026, in the piece's pull request).
|
|
22931
|
+
*
|
|
22932
|
+
* The folders are the CLI's: a session's start writes them again when a forwarder differs, as
|
|
22933
|
+
* after an update. Each is read-only, so a tool that installs hooks wherever `core.hooksPath`
|
|
22934
|
+
* points, run in an agent's command, fails there rather than replacing a forwarder for every
|
|
22935
|
+
* repo.
|
|
22936
|
+
*/
|
|
22937
|
+
/** The hooks Git runs on every index write and ref update. */
|
|
22938
|
+
const frequentHooks = ["post-index-change", "reference-transaction"];
|
|
22939
|
+
/**
|
|
22940
|
+
* Every other hook Git looks for in its hooks path. Not `push-to-checkout`, which replaces Git's
|
|
22941
|
+
* own update of a checkout pushed to merely by existing, nor `fsmonitor-watchman`, which Git runs
|
|
22942
|
+
* from `core.fsmonitor`, never from the hooks path.
|
|
22943
|
+
*/
|
|
22944
|
+
const forwardedHooks = [
|
|
22945
|
+
"applypatch-msg",
|
|
22946
|
+
"pre-applypatch",
|
|
22947
|
+
"post-applypatch",
|
|
22948
|
+
"pre-commit",
|
|
22949
|
+
"pre-merge-commit",
|
|
22950
|
+
"prepare-commit-msg",
|
|
22951
|
+
"commit-msg",
|
|
22952
|
+
"post-commit",
|
|
22953
|
+
"pre-rebase",
|
|
22954
|
+
"post-checkout",
|
|
22955
|
+
"post-merge",
|
|
22956
|
+
"pre-push",
|
|
22957
|
+
"pre-receive",
|
|
22958
|
+
"update",
|
|
22959
|
+
"proc-receive",
|
|
22960
|
+
"post-receive",
|
|
22961
|
+
"post-update",
|
|
22962
|
+
"pre-auto-gc",
|
|
22963
|
+
"post-rewrite",
|
|
22964
|
+
"sendemail-validate",
|
|
22965
|
+
"p4-changelist",
|
|
22966
|
+
"p4-prepare-changelist",
|
|
22967
|
+
"p4-post-changelist",
|
|
22968
|
+
"p4-pre-submit"
|
|
22969
|
+
];
|
|
22970
|
+
/**
|
|
22971
|
+
* What finds and runs the repo's own hook `name`, with ref0's entry left out: an entry naming one
|
|
22972
|
+
* of ref0's folders, in `$ref0_hooks`, is ref0's, and the rest keep their order, renumbered.
|
|
22973
|
+
* `done` ends it with nothing to run, `failed` when Git can't say where the hook is, and `run`
|
|
22974
|
+
* runs the hook it found.
|
|
22975
|
+
*/
|
|
22976
|
+
const forwarding = (name, end) => [
|
|
22977
|
+
`[ -n "\${${gitHookVariables.forwarded}-}" ] && ${end.done}`,
|
|
22978
|
+
"case ${GIT_CONFIG_PARAMETERS-} in *'/@git/agents/'*)",
|
|
22979
|
+
` ref0_codex=$(perl -e ${shellWord(withoutCodexInclude)} root) || ${end.failed}`,
|
|
22980
|
+
` GIT_CONFIG_PARAMETERS=$(perl -e ${shellWord(withoutCodexInclude)}) || ${end.failed}`,
|
|
22981
|
+
" if [ -z \"$GIT_CONFIG_PARAMETERS\" ]; then unset GIT_CONFIG_PARAMETERS; else export GIT_CONFIG_PARAMETERS; fi",
|
|
22982
|
+
";; esac",
|
|
22983
|
+
"ref0_n=${GIT_CONFIG_COUNT:-0}",
|
|
22984
|
+
"ref0_i=0",
|
|
22985
|
+
"ref0_j=0",
|
|
22986
|
+
"while [ \"$ref0_i\" -lt \"$ref0_n\" ]; do",
|
|
22987
|
+
" eval \"ref0_key=\\${GIT_CONFIG_KEY_$ref0_i-} ref0_value=\\${GIT_CONFIG_VALUE_$ref0_i-}\"",
|
|
22988
|
+
" unset \"GIT_CONFIG_KEY_$ref0_i\" \"GIT_CONFIG_VALUE_$ref0_i\"",
|
|
22989
|
+
" if [ \"$ref0_key\" != core.hooksPath ] || [ \"${ref0_value%/*}\" != \"$ref0_hooks\" ]; then",
|
|
22990
|
+
" export \"GIT_CONFIG_KEY_$ref0_j=$ref0_key\" \"GIT_CONFIG_VALUE_$ref0_j=$ref0_value\"",
|
|
22991
|
+
" ref0_j=$((ref0_j + 1))",
|
|
22992
|
+
" fi",
|
|
22993
|
+
" ref0_i=$((ref0_i + 1))",
|
|
22994
|
+
"done",
|
|
22995
|
+
"if [ \"$ref0_j\" -eq 0 ]; then unset GIT_CONFIG_COUNT; else export GIT_CONFIG_COUNT=\"$ref0_j\"; fi",
|
|
22996
|
+
`ref0_hook=$(git rev-parse --git-path hooks/${name}) || ${end.failed}`,
|
|
22997
|
+
`[ -f "$ref0_hook" ] && [ -x "$ref0_hook" ] || ${end.done}`,
|
|
22998
|
+
`ref0_dir=$(cd -P "\${ref0_hook%/*}" && pwd) || ${end.failed}`,
|
|
22999
|
+
`[ "\${ref0_dir%/*}" = "$ref0_hooks" ] && ${end.done}`,
|
|
23000
|
+
...end.run
|
|
23001
|
+
];
|
|
23002
|
+
/**
|
|
23003
|
+
* The forwarder for one hook, in ref0's folders in `hooks`. Each `exec`s the repo's own hook,
|
|
23004
|
+
* but `prepare-commit-msg` and `commit-msg`, which run it and add ref0's part once it succeeds,
|
|
23005
|
+
* even when a forwarder started them and they forward nothing.
|
|
23006
|
+
*/
|
|
23007
|
+
const forwarder = (name, hooks) => [
|
|
23008
|
+
"#!/bin/sh",
|
|
23009
|
+
`# ref0 ${cliVersion}'s forwarder for Git's ${name} hook. ref0 sets core.hooksPath to this`,
|
|
23010
|
+
"# folder only in its agents' commands. This runs the repo's own hook, found and run with",
|
|
23011
|
+
"# ref0's entry left out, with the same arguments, stdin and exit status.",
|
|
23012
|
+
`ref0_hooks=${shellWord(hooks)}`,
|
|
23013
|
+
...isCommitHook(name) ? [
|
|
23014
|
+
"# Then, in an agent's commands, it adds the Assisted-by trailer and refuses a sign-off on an",
|
|
23015
|
+
"# agent's commit.",
|
|
23016
|
+
"ref0_forward() {",
|
|
23017
|
+
...forwarding(name, {
|
|
23018
|
+
done: "return 0",
|
|
23019
|
+
failed: "return",
|
|
23020
|
+
run: [`${gitHookVariables.forwarded}=${name} "$ref0_hook" "$@"`]
|
|
23021
|
+
}).map((line) => ` ${line}`),
|
|
23022
|
+
"}",
|
|
23023
|
+
"ref0_forward \"$@\" || exit",
|
|
23024
|
+
...commitPart(name)
|
|
23025
|
+
] : forwarding(name, {
|
|
23026
|
+
done: "exit 0",
|
|
23027
|
+
failed: "exit",
|
|
23028
|
+
run: [`export ${gitHookVariables.forwarded}=${name}`, "exec \"$ref0_hook\" \"$@\""]
|
|
23029
|
+
}),
|
|
23030
|
+
""
|
|
23031
|
+
].join("\n");
|
|
23032
|
+
/** The hooks each folder forwards. */
|
|
23033
|
+
const hooksIn = (folder) => folder === "all" ? [...forwardedHooks, ...frequentHooks] : forwardedHooks;
|
|
23034
|
+
/** The folder holding ref0's hooks folders, made if it isn't yet: its path, through any links. */
|
|
23035
|
+
const gitHooksHome = Effect.fnUntraced(function* (home, api) {
|
|
23036
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23037
|
+
const given = (yield* Path.Path).join(home, ".ref0", "origins", originKey(api), "git-hooks");
|
|
23038
|
+
yield* fs.makeDirectory(given, {
|
|
23039
|
+
recursive: true,
|
|
23040
|
+
mode: 448
|
|
23041
|
+
});
|
|
23042
|
+
return yield* fs.realPath(given);
|
|
23043
|
+
});
|
|
23044
|
+
/**
|
|
23045
|
+
* One of ref0's hooks folders, in `hooks`, written first unless every forwarder in it is already
|
|
23046
|
+
* as this CLI writes it: its path, as the agent's `core.hooksPath` names it.
|
|
23047
|
+
*/
|
|
23048
|
+
const hooksFolder = Effect.fnUntraced(function* (hooks, folder) {
|
|
23049
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23050
|
+
const path = yield* Path.Path;
|
|
23051
|
+
const crypto = yield* Crypto.Crypto;
|
|
23052
|
+
const written = path.join(hooks, folder);
|
|
23053
|
+
const names = hooksIn(folder);
|
|
23054
|
+
const current = Effect.forEach(names, (name) => fs.readFileString(path.join(written, name)).pipe(Effect.map((text) => text === forwarder(name, hooks)), Effect.orElseSucceed(() => false))).pipe(Effect.map((each) => each.every(Boolean)));
|
|
23055
|
+
if (yield* current) return written;
|
|
23056
|
+
yield* Effect.gen(function* () {
|
|
23057
|
+
yield* fs.makeDirectory(written, { recursive: true });
|
|
23058
|
+
yield* fs.chmod(written, 493);
|
|
23059
|
+
for (const name of names) {
|
|
23060
|
+
const next = path.join(written, `${name}.${yield* crypto.randomUUIDv4}.next`);
|
|
23061
|
+
yield* fs.writeFileString(next, forwarder(name, hooks), { mode: 365 });
|
|
23062
|
+
yield* fs.rename(next, path.join(written, name));
|
|
21593
23063
|
}
|
|
21594
|
-
|
|
21595
|
-
|
|
23064
|
+
yield* fs.chmod(written, 365);
|
|
23065
|
+
}).pipe(Effect.catch((error) => Effect.flatMap(current, (done) => done ? Effect.void : Effect.fail(error))));
|
|
23066
|
+
return written;
|
|
21596
23067
|
});
|
|
21597
23068
|
//#endregion
|
|
21598
|
-
//#region src/harness/
|
|
21599
|
-
/**
|
|
21600
|
-
|
|
21601
|
-
|
|
21602
|
-
|
|
21603
|
-
|
|
21604
|
-
|
|
21605
|
-
|
|
21606
|
-
|
|
21607
|
-
|
|
21608
|
-
|
|
21609
|
-
|
|
21610
|
-
|
|
21611
|
-
"
|
|
21612
|
-
|
|
21613
|
-
|
|
21614
|
-
|
|
21615
|
-
|
|
21616
|
-
|
|
23069
|
+
//#region src/harness/codex-commits.ts
|
|
23070
|
+
/**
|
|
23071
|
+
* `@git` isn't a valid Codex plugin version. Version selection and upgrade skip it, while
|
|
23072
|
+
* `plugin remove` deletes it with the plugin's cache root (Codex 0.160–0.164). A same-version
|
|
23073
|
+
* reinstall replaces the root; session start restores these files before the first command.
|
|
23074
|
+
* Only `checkouts` is writable in ref0's permission profile, never the agents' author files.
|
|
23075
|
+
*/
|
|
23076
|
+
/** A Git config value, including paths with quotes, backslashes or line breaks. */
|
|
23077
|
+
const gitValue = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\n", "\\n").replaceAll(" ", "\\t").replaceAll("\b", "\\b")}"`;
|
|
23078
|
+
var CodexCommitFailed = class extends Schema.TaggedError()("CodexCommitFailed", { detail: Schema.String }) {};
|
|
23079
|
+
/** Writes a checkout's overrides and, at session start, the agent's include under the same lock. */
|
|
23080
|
+
const writeCodexCheckout = Effect.fnUntraced(function* (input) {
|
|
23081
|
+
if ((yield* (yield* ChildProcessSpawner.ChildProcessSpawner).exitCode(ChildProcess.make("perl", [
|
|
23082
|
+
"-e",
|
|
23083
|
+
codexCheckoutWriter,
|
|
23084
|
+
codexCheckoutsFolder(input.gitFolder),
|
|
23085
|
+
input.common,
|
|
23086
|
+
input.folder,
|
|
23087
|
+
input.signsOff ? "1" : "0",
|
|
23088
|
+
input.author?.file ?? "",
|
|
23089
|
+
input.author?.text ?? "",
|
|
23090
|
+
input.clearAuthor ?? "",
|
|
23091
|
+
input.activateAuthor ? "1" : ""
|
|
23092
|
+
]))) !== ChildProcessSpawner.ExitCode(0)) return yield* new CodexCommitFailed({ detail: "Could not write Codex’s Git config files." });
|
|
23093
|
+
});
|
|
23094
|
+
/** The include installed into `set`, and restored by each session's start. */
|
|
23095
|
+
const codexAgentConfig = Effect.fnUntraced(function* (agent, root, home) {
|
|
23096
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23097
|
+
const directory = `${root}/agents`;
|
|
23098
|
+
yield* fs.makeDirectory(directory, {
|
|
23099
|
+
recursive: true,
|
|
23100
|
+
mode: 448
|
|
23101
|
+
});
|
|
23102
|
+
const folder = yield* hooksFolder(yield* gitHooksHome(home, (yield* Ref0Config).api), "most");
|
|
23103
|
+
yield* hooksFolder(yield* gitHooksHome(home, (yield* Ref0Config).api), "all");
|
|
23104
|
+
const author = `[author]\n\tname = ${gitValue(agentCommitName(agent.owner, agent.agent))}\n\temail = ${gitValue(agentCommitEmail(agent.agentId))}\n`;
|
|
23105
|
+
const file = `${directory}/${agent.agentId}.gitconfig`;
|
|
23106
|
+
return {
|
|
23107
|
+
file,
|
|
23108
|
+
text: `${author}[core]\n\thooksPath = ${gitValue(folder)}\n[include]\n\tpath = ${gitValue(`${codexCheckoutsFolder(root)}/checkouts.gitconfig`)}\n`,
|
|
23109
|
+
folder,
|
|
23110
|
+
parameters: shellWord(`include.path=${file}`)
|
|
23111
|
+
};
|
|
23112
|
+
});
|
|
23113
|
+
/**
|
|
23114
|
+
* Codex has no environment file: its install selects a file; session start fills its checkout,
|
|
23115
|
+
* for the agent of the config folder `CODEX_HOME` names, or the default `~/.codex`.
|
|
23116
|
+
*/
|
|
23117
|
+
const setCodexCommitEnvironment = Effect.fnUntraced(function* (cwd, checkout) {
|
|
23118
|
+
const folders = yield* ConfigFolders;
|
|
23119
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23120
|
+
const path = yield* Path.Path;
|
|
23121
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
23122
|
+
const agent = yield* (yield* HostGrant).get(yield* folders.folder("codex")).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
|
|
23123
|
+
if (Option.isNone(agent)) return false;
|
|
23124
|
+
const pluginRoot = yield* Config.String("CLAUDE_PLUGIN_ROOT").pipe(Config.withDefault(""));
|
|
23125
|
+
if (pluginRoot === "") return yield* new CodexCommitFailed({ detail: "Codex gave the hook no plugin root." });
|
|
23126
|
+
const gitFolder = path.join(path.dirname(pluginRoot), "@git");
|
|
23127
|
+
const home = yield* Config.String("HOME");
|
|
23128
|
+
const author = yield* codexAgentConfig(agent.value, gitFolder, home);
|
|
23129
|
+
const common = (yield* spawner.string(ChildProcess.make("git", [
|
|
23130
|
+
"-C",
|
|
23131
|
+
cwd,
|
|
23132
|
+
"rev-parse",
|
|
23133
|
+
"--path-format=absolute",
|
|
23134
|
+
"--git-common-dir"
|
|
23135
|
+
])).pipe(Effect.orElseSucceed(() => ""))).trim();
|
|
23136
|
+
const folder = yield* hooksFolder(yield* gitHooksHome(yield* Config.String("HOME"), (yield* Ref0Config).api), checkout.folder);
|
|
23137
|
+
yield* writeCodexCheckout({
|
|
23138
|
+
gitFolder,
|
|
23139
|
+
common: common === "" ? "" : yield* fs.realPath(path.resolve(common)),
|
|
23140
|
+
folder,
|
|
23141
|
+
signsOff: checkout.signsOff,
|
|
23142
|
+
author
|
|
23143
|
+
});
|
|
23144
|
+
return true;
|
|
21617
23145
|
});
|
|
21618
23146
|
//#endregion
|
|
21619
23147
|
//#region src/harness/hook-fields.ts
|
|
@@ -21828,6 +23356,7 @@ const marketplaceFor = Effect.fnUntraced(function* (harness, options) {
|
|
|
21828
23356
|
const failed = (reason, detail = "") => into === void 0 ? unavailable : new InstallFailed({ says: `${name} ${reason}.${detail === "" ? "" : ` ${detail}`}` });
|
|
21829
23357
|
return Effect.scoped(Effect.gen(function* () {
|
|
21830
23358
|
const handle = yield* spawner.spawn(ChildProcess.make(options.command, args, {
|
|
23359
|
+
cwd: yield* emptyFolder(fs),
|
|
21831
23360
|
env: into === void 0 ? fetchEnv : envFor(into),
|
|
21832
23361
|
extendEnv: true
|
|
21833
23362
|
}));
|
|
@@ -21948,90 +23477,212 @@ const marketplaceFor = Effect.fnUntraced(function* (harness, options) {
|
|
|
21948
23477
|
};
|
|
21949
23478
|
});
|
|
21950
23479
|
//#endregion
|
|
21951
|
-
//#region src/harness/codex-
|
|
21952
|
-
|
|
21953
|
-
|
|
21954
|
-
|
|
21955
|
-
|
|
21956
|
-
|
|
21957
|
-
|
|
21958
|
-
|
|
21959
|
-
|
|
21960
|
-
/**
|
|
21961
|
-
|
|
21962
|
-
|
|
21963
|
-
|
|
21964
|
-
|
|
21965
|
-
|
|
21966
|
-
|
|
21967
|
-
|
|
21968
|
-
|
|
21969
|
-
|
|
21970
|
-
const
|
|
21971
|
-
|
|
21972
|
-
|
|
21973
|
-
const
|
|
21974
|
-
|
|
21975
|
-
|
|
21976
|
-
|
|
21977
|
-
|
|
21978
|
-
|
|
21979
|
-
|
|
21980
|
-
|
|
21981
|
-
|
|
21982
|
-
|
|
21983
|
-
|
|
21984
|
-
|
|
21985
|
-
|
|
21986
|
-
|
|
21987
|
-
|
|
21988
|
-
|
|
21989
|
-
/**
|
|
21990
|
-
* Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
|
|
21991
|
-
* answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
|
|
21992
|
-
*/
|
|
21993
|
-
const exchange = (codex, lines, ids, configFolder) => Effect.scoped(Effect.gen(function* () {
|
|
21994
|
-
const answered = yield* (yield* (yield* ChildProcessSpawner.ChildProcessSpawner).spawn(ChildProcess.make(codex, ["app-server"], {
|
|
21995
|
-
env: configFolder === void 0 ? void 0 : { CODEX_HOME: configFolder },
|
|
21996
|
-
extendEnv: true,
|
|
21997
|
-
forceKillAfter: "2 seconds",
|
|
21998
|
-
stdin: {
|
|
21999
|
-
stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
|
|
22000
|
-
endOnDone: false
|
|
22001
|
-
}
|
|
22002
|
-
}))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.map((line) => decodeAnswer$1(line)), Stream.filter(Option.isSome), Stream.scan(() => /* @__PURE__ */ new Map(), (answers, { value: answer }) => ids.includes(answer.id) && !answers.has(answer.id) ? new Map(answers).set(answer.id, answer) : answers), Stream.takeUntil((answers) => answers.size === ids.length), Stream.runLast);
|
|
22003
|
-
return Option.filter(answered, (answers) => answers.size === ids.length);
|
|
22004
|
-
})).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
|
|
22005
|
-
/** `answer`'s result, read by `method`'s own schema, or why it can't be. */
|
|
22006
|
-
const resultOf = (method, answer) => {
|
|
22007
|
-
if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
|
|
22008
|
-
if ("error" in answer) return Effect.fail(new CodexRefused({
|
|
22009
|
-
method: method.method,
|
|
22010
|
-
error: answer.error
|
|
22011
|
-
}));
|
|
22012
|
-
const { result } = method;
|
|
22013
|
-
return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
|
|
22014
|
-
method: method.method,
|
|
22015
|
-
issue: error.message
|
|
22016
|
-
})));
|
|
22017
|
-
};
|
|
22018
|
-
/**
|
|
22019
|
-
* One exchange with Codex's app server under the person's Codex home: the opening, then `method`
|
|
22020
|
-
* asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
|
|
22021
|
-
* its request's ID, and its result is read by the method's result schema.
|
|
22022
|
-
*/
|
|
22023
|
-
const askCodex = (codex, method, params, configFolder) => Effect.gen(function* () {
|
|
22024
|
-
const asked = {
|
|
22025
|
-
jsonrpc: "2.0",
|
|
22026
|
-
id: askedId,
|
|
22027
|
-
method: method.method,
|
|
22028
|
-
params
|
|
23480
|
+
//#region src/harness/codex-install-commits.ts
|
|
23481
|
+
const decodeProfile = Schema.decodeUnknownEffect(CodexConfigRead);
|
|
23482
|
+
const sameJson = Schema.toEquivalence(Schema.Json);
|
|
23483
|
+
/** Only the single ref0 include installed by this CLI belongs to ref0. */
|
|
23484
|
+
const selectedInclude = (set) => {
|
|
23485
|
+
const parameters = set["GIT_CONFIG_PARAMETERS"] ?? "";
|
|
23486
|
+
const included = parameters.slice(1, -1).replaceAll("'\\''", "'");
|
|
23487
|
+
return set["REF0_HARNESS"] === "codex" && shellWord(included) === parameters && /^include\.path=[\s\S]*\/plugins\/cache\/[^/]+\/[^/]+\/@git\/agents\/[a-z0-9_-]+\.gitconfig$/.test(included) ? included.slice(13) : void 0;
|
|
23488
|
+
};
|
|
23489
|
+
/** A changed grant must never leave the previous agent as author after a failed install. */
|
|
23490
|
+
const clearOtherCodexAuthor = Effect.fnUntraced(function* (agent, options) {
|
|
23491
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23492
|
+
const configFile = `${options.configFolder}/config.toml`;
|
|
23493
|
+
if (!(yield* fs.exists(configFile))) return;
|
|
23494
|
+
const text = yield* fs.readFileString(configFile);
|
|
23495
|
+
const document = yield* Effect.try(() => parse(text, { integersAsBigInt: "asNeeded" }));
|
|
23496
|
+
const read = yield* decodeProfile({ config: document });
|
|
23497
|
+
const file = selectedInclude(read.config.shell_environment_policy?.set ?? {});
|
|
23498
|
+
if (file === void 0) return;
|
|
23499
|
+
const name = yield* originPluginName((yield* Ref0Config).api);
|
|
23500
|
+
if (file === `${codexGitFolder(options.configFolder, `${name}@${name}`)}/agents/${agent.agentId}.gitconfig`) return;
|
|
23501
|
+
const oldRoot = file.slice(0, file.lastIndexOf("/agents/"));
|
|
23502
|
+
const fallback = yield* codexAgentConfig(agent, oldRoot, options.home ?? (yield* Config.String("HOME").pipe(Config.withDefault("."))));
|
|
23503
|
+
yield* writeCodexCheckout({
|
|
23504
|
+
gitFolder: oldRoot,
|
|
23505
|
+
common: "",
|
|
23506
|
+
folder: "",
|
|
23507
|
+
signsOff: false,
|
|
23508
|
+
clearAuthor: file,
|
|
23509
|
+
author: fallback
|
|
23510
|
+
});
|
|
23511
|
+
yield* Console.error(`ref0: Commits now name you instead of the previously connected agent, including its running sessions; ref0’s Git hooks still run. Fix the shell_environment_policy conflict in Codex’s config or profile named above: remove conflicting set values or allowlists, and keep GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL excluded. Then run: ${installLine(options.configFolder)}`);
|
|
23512
|
+
});
|
|
23513
|
+
/** Install the connected agent's include and shell policy. */
|
|
23514
|
+
const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
23515
|
+
const env = {
|
|
23516
|
+
HOME: options.home,
|
|
23517
|
+
CODEX_HOME: options.configFolder
|
|
22029
23518
|
};
|
|
22030
|
-
const
|
|
22031
|
-
|
|
22032
|
-
|
|
22033
|
-
|
|
22034
|
-
|
|
23519
|
+
const failed = (what) => new InstallFailed({ says: `Couldn’t ${what}. Check that Codex is installed.` });
|
|
23520
|
+
return yield* Effect.gen(function* () {
|
|
23521
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23522
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
23523
|
+
yield* fs.makeDirectory(`${options.home}/.ref0`, {
|
|
23524
|
+
recursive: true,
|
|
23525
|
+
mode: 448
|
|
23526
|
+
});
|
|
23527
|
+
if ((yield* (yield* spawner.spawn(ChildProcess.make("perl", [
|
|
23528
|
+
"-e",
|
|
23529
|
+
"use Fcntl qw(LOCK_EX); open my $lock, \">>\", $ARGV[0] or die $!; alarm 5; flock($lock, LOCK_EX) or die $!; alarm 0; $|=1; print \"locked\\n\"; <STDIN>;",
|
|
23530
|
+
`${options.home}/.ref0/codex-install.lock`
|
|
23531
|
+
], { stdin: "pipe" }))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.mkString)) !== "locked") return yield* failed("lock Codex’s commit setup");
|
|
23532
|
+
const read = yield* askCodex(options.codex, codexMethods.configRead, {
|
|
23533
|
+
includeLayers: true,
|
|
23534
|
+
cwd: null
|
|
23535
|
+
}, env);
|
|
23536
|
+
const policy = read.config.shell_environment_policy;
|
|
23537
|
+
const set = policy?.set ?? {};
|
|
23538
|
+
const own = selectedInclude(set) !== void 0;
|
|
23539
|
+
const collision = [
|
|
23540
|
+
"GIT_CONFIG_PARAMETERS",
|
|
23541
|
+
"REF0_HARNESS",
|
|
23542
|
+
"GIT_AUTHOR_NAME",
|
|
23543
|
+
"GIT_AUTHOR_EMAIL"
|
|
23544
|
+
].find((key) => Object.hasOwn(set, key) && !(own && (key === "GIT_CONFIG_PARAMETERS" || key === "REF0_HARNESS")));
|
|
23545
|
+
const allowlist = policy?.include_only != null || Object.values(policy?.filters ?? {}).includes("include");
|
|
23546
|
+
if (collision !== void 0 || allowlist) {
|
|
23547
|
+
yield* Console.error(`ref0: Codex’s shell_environment_policy ${collision === void 0 ? "has an allowlist" : `already sets ${collision}`}, so ref0 left its commit environment as it was.`);
|
|
23548
|
+
if (own) return yield* failed("update the existing commit environment; fix the conflicting Codex config or profile, then reinstall");
|
|
23549
|
+
return;
|
|
23550
|
+
}
|
|
23551
|
+
const profiles = (yield* fs.readDirectory(options.configFolder)).filter((file) => file.endsWith(".config.toml"));
|
|
23552
|
+
for (const file of profiles) {
|
|
23553
|
+
const text = yield* fs.readFileString(`${options.configFolder}/${file}`);
|
|
23554
|
+
const document = yield* Effect.try({
|
|
23555
|
+
try: () => parse(text, { integersAsBigInt: "asNeeded" }),
|
|
23556
|
+
catch: () => failed(`read Codex’s profile ${printable(file)}`)
|
|
23557
|
+
});
|
|
23558
|
+
const profile = yield* decodeProfile({ config: document });
|
|
23559
|
+
const overrides = profile.config.shell_environment_policy?.set ?? {};
|
|
23560
|
+
const key = [
|
|
23561
|
+
"GIT_CONFIG_PARAMETERS",
|
|
23562
|
+
"REF0_HARNESS",
|
|
23563
|
+
"GIT_AUTHOR_NAME",
|
|
23564
|
+
"GIT_AUTHOR_EMAIL"
|
|
23565
|
+
].find((key) => Object.hasOwn(overrides, key));
|
|
23566
|
+
const profilePolicy = profile.config.shell_environment_policy;
|
|
23567
|
+
const filters = profilePolicy?.filters;
|
|
23568
|
+
const replaced = filters != null && policy?.filters == null;
|
|
23569
|
+
const excluded = filters != null ? [...replaced ? [] : ["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL"], ...Object.keys(filters)] : profilePolicy?.exclude;
|
|
23570
|
+
const losesAuthors = excluded !== void 0 && excluded !== null && ["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL"].some((name) => !excluded.some((pattern) => pathMatchesWithin(pattern.toLowerCase(), name.toLowerCase(), { left: 1e4 })));
|
|
23571
|
+
const allowlist = profilePolicy?.include_only != null || Object.values(filters ?? {}).includes("include");
|
|
23572
|
+
if (key === void 0 && !losesAuthors && !allowlist) continue;
|
|
23573
|
+
yield* Console.error(`ref0: Codex’s profile ${printable(file)} ${key === void 0 ? "overrides the author exclusions or has an allowlist" : `already sets ${key}`}, so ref0 left its commit environment as it was.`);
|
|
23574
|
+
if (own) return yield* failed("update the existing commit environment; fix the conflicting Codex config or profile, then reinstall");
|
|
23575
|
+
return;
|
|
23576
|
+
}
|
|
23577
|
+
if (!own && (options.approveCommits === void 0 || !(yield* options.approveCommits()))) return;
|
|
23578
|
+
const crypto = yield* Crypto.Crypto;
|
|
23579
|
+
const author = yield* codexAgentConfig(agent, options.gitFolder, options.home);
|
|
23580
|
+
const configFile = `${options.configFolder}/config.toml`;
|
|
23581
|
+
const before = (file) => Effect.flatMap(fs.exists(file), (exists) => exists ? Effect.map(fs.readFileString(file), Option.some) : Effect.succeed(Option.none()));
|
|
23582
|
+
const previousAuthor = yield* before(author.file);
|
|
23583
|
+
const restore = (file, before) => Effect.gen(function* () {
|
|
23584
|
+
if (Option.isNone(before)) return yield* fs.remove(file, { force: true });
|
|
23585
|
+
const next = `${file}.${yield* crypto.randomUUIDv4}.next`;
|
|
23586
|
+
yield* fs.writeFileString(next, before.value, { mode: 384 });
|
|
23587
|
+
yield* fs.rename(next, file);
|
|
23588
|
+
});
|
|
23589
|
+
const user = read.layers?.find((layer) => layer.name.type === "user" && layer.name.file === configFile);
|
|
23590
|
+
const previousPolicy = user?.config === void 0 ? policy ?? null : user.config.shell_environment_policy ?? null;
|
|
23591
|
+
const edits = [{
|
|
23592
|
+
keyPath: "shell_environment_policy.set",
|
|
23593
|
+
value: {
|
|
23594
|
+
...set,
|
|
23595
|
+
GIT_CONFIG_PARAMETERS: author.parameters,
|
|
23596
|
+
REF0_HARNESS: "codex"
|
|
23597
|
+
},
|
|
23598
|
+
mergeStrategy: "replace"
|
|
23599
|
+
}, ...policy?.filters == null ? [{
|
|
23600
|
+
keyPath: "shell_environment_policy.exclude",
|
|
23601
|
+
value: [.../* @__PURE__ */ new Set([
|
|
23602
|
+
...policy?.exclude ?? [],
|
|
23603
|
+
"GIT_AUTHOR_NAME",
|
|
23604
|
+
"GIT_AUTHOR_EMAIL"
|
|
23605
|
+
])],
|
|
23606
|
+
mergeStrategy: "replace"
|
|
23607
|
+
}] : [{
|
|
23608
|
+
keyPath: "shell_environment_policy.filters",
|
|
23609
|
+
value: {
|
|
23610
|
+
...policy.filters,
|
|
23611
|
+
...Object.fromEntries(["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL"].flatMap((key) => Object.keys(policy.filters ?? {}).some((pattern) => pattern.toUpperCase() === key) ? [] : [[key, "exclude"]]))
|
|
23612
|
+
},
|
|
23613
|
+
mergeStrategy: "replace"
|
|
23614
|
+
}]];
|
|
23615
|
+
const installedPolicy = {
|
|
23616
|
+
...previousPolicy,
|
|
23617
|
+
...Object.fromEntries(edits.map((edit) => [edit.keyPath.slice(25), edit.value]))
|
|
23618
|
+
};
|
|
23619
|
+
const readCurrent = askCodex(options.codex, codexMethods.configRead, {
|
|
23620
|
+
includeLayers: true,
|
|
23621
|
+
cwd: null
|
|
23622
|
+
}, env);
|
|
23623
|
+
const recover = Effect.gen(function* () {
|
|
23624
|
+
const layer = (yield* readCurrent).layers?.find((layer) => layer.name.type === "user" && layer.name.file === configFile);
|
|
23625
|
+
if (layer?.config === void 0) return false;
|
|
23626
|
+
const currentPolicy = layer.config.shell_environment_policy ?? null;
|
|
23627
|
+
if (sameJson(currentPolicy, previousPolicy)) return true;
|
|
23628
|
+
if (!sameJson(currentPolicy, installedPolicy)) return false;
|
|
23629
|
+
const restored = yield* askCodex(options.codex, codexMethods.configBatchWrite, {
|
|
23630
|
+
edits: [{
|
|
23631
|
+
keyPath: "shell_environment_policy",
|
|
23632
|
+
value: previousPolicy,
|
|
23633
|
+
mergeStrategy: "replace"
|
|
23634
|
+
}],
|
|
23635
|
+
filePath: configFile,
|
|
23636
|
+
expectedVersion: layer.version,
|
|
23637
|
+
reloadUserConfig: false
|
|
23638
|
+
}, env).pipe(Effect.option);
|
|
23639
|
+
if (Option.isSome(restored)) return true;
|
|
23640
|
+
const checkedLayer = (yield* readCurrent).layers?.find((layer) => layer.name.type === "user" && layer.name.file === configFile);
|
|
23641
|
+
return checkedLayer?.config !== void 0 && sameJson(checkedLayer.config.shell_environment_policy ?? null, previousPolicy);
|
|
23642
|
+
}).pipe(Effect.orElseSucceed(() => false));
|
|
23643
|
+
let attemptedConfig = false;
|
|
23644
|
+
yield* Effect.gen(function* () {
|
|
23645
|
+
yield* writeCodexCheckout({
|
|
23646
|
+
gitFolder: options.gitFolder,
|
|
23647
|
+
common: "",
|
|
23648
|
+
folder: author.folder,
|
|
23649
|
+
signsOff: false,
|
|
23650
|
+
author,
|
|
23651
|
+
activateAuthor: true
|
|
23652
|
+
});
|
|
23653
|
+
attemptedConfig = true;
|
|
23654
|
+
if ((yield* askCodex(options.codex, codexMethods.configBatchWrite, {
|
|
23655
|
+
edits,
|
|
23656
|
+
filePath: configFile,
|
|
23657
|
+
expectedVersion: user?.version ?? null,
|
|
23658
|
+
reloadUserConfig: false
|
|
23659
|
+
}, env)).status !== "ok") return yield* failed("set Codex’s commit environment because another config layer overrides it");
|
|
23660
|
+
}).pipe(Effect.onError(() => Effect.gen(function* () {
|
|
23661
|
+
if (!attemptedConfig || (yield* recover)) yield* restore(author.file, previousAuthor);
|
|
23662
|
+
else yield* Console.error("ref0: Could not safely restore Codex’s commit policy after the failed setup. Its author file was kept so the include remains valid. Check your Codex config, then run ref0 install again.");
|
|
23663
|
+
}).pipe(Effect.orDie)));
|
|
23664
|
+
}).pipe(Effect.scoped, Effect.mapError(() => failed("set Codex’s commit environment")));
|
|
23665
|
+
}, Effect.scoped);
|
|
23666
|
+
//#endregion
|
|
23667
|
+
//#region src/harness/install-claude-code.ts
|
|
23668
|
+
/** Install into one config folder, from the release Claude Code fetched and ref0 checked. */
|
|
23669
|
+
const installClaudeCode = Effect.fnUntraced(function* (release, configFolder) {
|
|
23670
|
+
const run = yield* release.into(configFolder);
|
|
23671
|
+
yield* run([
|
|
23672
|
+
"plugin",
|
|
23673
|
+
"install",
|
|
23674
|
+
release.id,
|
|
23675
|
+
"--scope",
|
|
23676
|
+
"user"
|
|
23677
|
+
]);
|
|
23678
|
+
yield* run([
|
|
23679
|
+
"plugin",
|
|
23680
|
+
"update",
|
|
23681
|
+
release.id,
|
|
23682
|
+
"--scope",
|
|
23683
|
+
"user"
|
|
23684
|
+
]);
|
|
23685
|
+
return release.folder;
|
|
22035
23686
|
});
|
|
22036
23687
|
//#endregion
|
|
22037
23688
|
//#region src/harness/install-codex.ts
|
|
@@ -22076,95 +23727,120 @@ const trustRef0Hooks = (reinstall, id) => `In a terminal, run: ${reinstall}, and
|
|
|
22076
23727
|
* `CODEX_HOME`: neither half alone lets a commit and HTTPS through. A linked worktree's own git
|
|
22077
23728
|
* directory isn't covered; there Codex needs full access, which whatever launches it sets.
|
|
22078
23729
|
*/
|
|
22079
|
-
const ref0Profile = {
|
|
23730
|
+
const ref0Profile = (checkouts) => ({
|
|
22080
23731
|
extends: ":workspace",
|
|
22081
|
-
filesystem: {
|
|
23732
|
+
filesystem: {
|
|
23733
|
+
":workspace_roots": { ".git": "write" },
|
|
23734
|
+
[checkouts]: "write"
|
|
23735
|
+
},
|
|
22082
23736
|
network: { enabled: true }
|
|
22083
|
-
};
|
|
23737
|
+
});
|
|
22084
23738
|
const isPathModes = Schema.is(Schema.Record(Schema.String, Schema.String));
|
|
22085
23739
|
/** A default that already lets a command write `.git` and reach the network, so needs nothing. */
|
|
22086
23740
|
const enough = (read) => read.config.default_permissions === ":danger-full-access";
|
|
22087
23741
|
/** Whether a setup's config has ref0's profile, as its default. */
|
|
22088
|
-
const profileSet = (read) => {
|
|
23742
|
+
const profileSet = (read, checkouts) => {
|
|
22089
23743
|
const profile = read.config.permissions?.["ref0"];
|
|
22090
23744
|
const roots = profile?.filesystem?.[":workspace_roots"];
|
|
22091
|
-
return read.config.default_permissions === "ref0" && profile?.extends === ref0Profile.extends && isPathModes(roots) && roots[".git"] === "write" && profile.network?.enabled === true;
|
|
23745
|
+
return read.config.default_permissions === "ref0" && profile?.extends === ref0Profile(checkouts).extends && isPathModes(roots) && roots[".git"] === "write" && profile.filesystem?.[checkouts] === "write" && profile.network?.enabled === true;
|
|
22092
23746
|
};
|
|
22093
|
-
/**
|
|
22094
|
-
|
|
22095
|
-
|
|
22096
|
-
|
|
22097
|
-
|
|
22098
|
-
|
|
22099
|
-
|
|
22100
|
-
id
|
|
22101
|
-
|
|
22102
|
-
|
|
22103
|
-
|
|
22104
|
-
|
|
22105
|
-
|
|
22106
|
-
|
|
22107
|
-
|
|
22108
|
-
|
|
22109
|
-
|
|
22110
|
-
|
|
22111
|
-
|
|
22112
|
-
|
|
22113
|
-
|
|
22114
|
-
|
|
22115
|
-
|
|
22116
|
-
|
|
22117
|
-
|
|
22118
|
-
|
|
22119
|
-
|
|
22120
|
-
|
|
22121
|
-
|
|
22122
|
-
|
|
22123
|
-
|
|
22124
|
-
|
|
22125
|
-
|
|
22126
|
-
|
|
22127
|
-
|
|
22128
|
-
ownRef0: false
|
|
22129
|
-
}),
|
|
22130
|
-
onSome: (read) => ({
|
|
22131
|
-
default: read.config.default_permissions ?? void 0,
|
|
22132
|
-
ownRef0: read.config.permissions?.["ref0"] !== void 0
|
|
22133
|
-
})
|
|
23747
|
+
/**
|
|
23748
|
+
* Install into one config folder, from the release Codex fetched and ref0 checked, for the agent
|
|
23749
|
+
* the folder's host grant names.
|
|
23750
|
+
*/
|
|
23751
|
+
const installCodex = Effect.fnUntraced(function* (release, agent, options) {
|
|
23752
|
+
return yield* Effect.gen(function* () {
|
|
23753
|
+
const failed = (what) => new InstallFailed({ says: `Couldn’t ${what}. Check that Codex is installed.` });
|
|
23754
|
+
const { folder, id, name } = release;
|
|
23755
|
+
yield* (yield* release.into(options.configFolder))([
|
|
23756
|
+
"plugin",
|
|
23757
|
+
"add",
|
|
23758
|
+
id
|
|
23759
|
+
]);
|
|
23760
|
+
const gitFolder = codexGitFolder(options.configFolder, id);
|
|
23761
|
+
const checkouts = codexCheckoutsFolder(gitFolder);
|
|
23762
|
+
const env = {
|
|
23763
|
+
HOME: options.home,
|
|
23764
|
+
CODEX_HOME: options.configFolder
|
|
23765
|
+
};
|
|
23766
|
+
const hooks = (yield* askCodex(options.codex, codexMethods.hooksList, { cwds: [] }, env).pipe(Effect.mapError(() => failed("read the plugin’s hooks from Codex")))).data.flatMap((entry) => entry.hooks).filter((hook) => pluginHook(hook, id));
|
|
23767
|
+
if (hooks.length === 0) return yield* failed("read the plugin’s hooks from Codex");
|
|
23768
|
+
if (!hooks.every((hook) => ownHook(hook, name))) return yield* new InstallFailed({ says: `Codex’s ref0 plugin holds hooks ref0 didn’t write, so none was trusted. Remove it with: codex plugin remove ${id}.` });
|
|
23769
|
+
const trust = Effect.fnUntraced(function* (waiting) {
|
|
23770
|
+
if (!(yield* options.approve(waiting))) return false;
|
|
23771
|
+
yield* askCodex(options.codex, codexMethods.configBatchWrite, {
|
|
23772
|
+
edits: [{
|
|
23773
|
+
keyPath: "hooks.state",
|
|
23774
|
+
value: Object.fromEntries(waiting.map((hook) => [hook.key, { trusted_hash: hook.currentHash }])),
|
|
23775
|
+
mergeStrategy: "upsert"
|
|
23776
|
+
}],
|
|
23777
|
+
filePath: null,
|
|
23778
|
+
expectedVersion: null,
|
|
23779
|
+
reloadUserConfig: false
|
|
23780
|
+
}, env).pipe(Effect.mapError(() => failed("record the hooks as trusted in Codex")));
|
|
23781
|
+
return true;
|
|
22134
23782
|
});
|
|
22135
|
-
|
|
22136
|
-
|
|
22137
|
-
|
|
22138
|
-
|
|
22139
|
-
|
|
22140
|
-
|
|
22141
|
-
|
|
22142
|
-
|
|
22143
|
-
|
|
22144
|
-
|
|
22145
|
-
|
|
22146
|
-
|
|
22147
|
-
|
|
22148
|
-
|
|
22149
|
-
|
|
22150
|
-
|
|
22151
|
-
|
|
22152
|
-
|
|
22153
|
-
|
|
22154
|
-
|
|
22155
|
-
|
|
22156
|
-
|
|
22157
|
-
|
|
23783
|
+
const offerProfile = Effect.fnUntraced(function* () {
|
|
23784
|
+
const config = yield* askCodex(options.codex, codexMethods.configRead, {
|
|
23785
|
+
includeLayers: false,
|
|
23786
|
+
cwd: null
|
|
23787
|
+
}, env).pipe(Effect.option);
|
|
23788
|
+
if (Option.isSome(config) && (profileSet(config.value, checkouts) || enough(config.value))) return true;
|
|
23789
|
+
const replacing = Option.match(config, {
|
|
23790
|
+
onNone: () => ({
|
|
23791
|
+
default: void 0,
|
|
23792
|
+
ownRef0: false
|
|
23793
|
+
}),
|
|
23794
|
+
onSome: (read) => ({
|
|
23795
|
+
default: read.config.default_permissions ?? void 0,
|
|
23796
|
+
ownRef0: read.config.permissions?.["ref0"] !== void 0
|
|
23797
|
+
})
|
|
23798
|
+
});
|
|
23799
|
+
if (!(yield* options.approveProfile(replacing, checkouts))) return false;
|
|
23800
|
+
yield* askCodex(options.codex, codexMethods.configBatchWrite, {
|
|
23801
|
+
edits: [{
|
|
23802
|
+
keyPath: "permissions.ref0",
|
|
23803
|
+
value: ref0Profile(checkouts),
|
|
23804
|
+
mergeStrategy: "replace"
|
|
23805
|
+
}, {
|
|
23806
|
+
keyPath: "default_permissions",
|
|
23807
|
+
value: "ref0",
|
|
23808
|
+
mergeStrategy: "replace"
|
|
23809
|
+
}],
|
|
23810
|
+
filePath: null,
|
|
23811
|
+
expectedVersion: null,
|
|
23812
|
+
reloadUserConfig: false
|
|
23813
|
+
}, env).pipe(Effect.mapError(() => failed("set ref0’s profile in Codex")));
|
|
23814
|
+
return true;
|
|
23815
|
+
});
|
|
23816
|
+
const waiting = hooks.filter(waitingForTrust);
|
|
23817
|
+
const trusted = waiting.length === 0 || (yield* trust(waiting));
|
|
23818
|
+
const profile = yield* offerProfile();
|
|
23819
|
+
yield* installCodexCommits(agent, {
|
|
23820
|
+
...options,
|
|
23821
|
+
gitFolder
|
|
23822
|
+
});
|
|
23823
|
+
return {
|
|
23824
|
+
folder,
|
|
23825
|
+
trusted,
|
|
23826
|
+
profile
|
|
23827
|
+
};
|
|
23828
|
+
}).pipe(Effect.onError(() => clearOtherCodexAuthor(agent, options).pipe(Effect.orDie)));
|
|
22158
23829
|
});
|
|
22159
23830
|
//#endregion
|
|
22160
23831
|
//#region src/harness/install.ts
|
|
22161
|
-
/**
|
|
23832
|
+
/**
|
|
23833
|
+
* A command on this machine's `PATH`, by absolute path: a folder `PATH` names relative to this
|
|
23834
|
+
* one is read from here, since a harness's commands run from an empty folder of their own. Each
|
|
23835
|
+
* folder is kept as written, `..` and all, so the system follows a symlink before the `..` after it.
|
|
23836
|
+
*/
|
|
22162
23837
|
const onPath = Effect.fnUntraced(function* (name) {
|
|
22163
23838
|
const fs = yield* FileSystem.FileSystem;
|
|
23839
|
+
const paths = yield* Path.Path;
|
|
22164
23840
|
const path = yield* Config.String("PATH").pipe(Config.withDefault(""));
|
|
22165
23841
|
for (const folder of path.split(":")) {
|
|
22166
23842
|
if (folder === "") continue;
|
|
22167
|
-
const candidate = `${folder}/${name}`;
|
|
23843
|
+
const candidate = `${paths.isAbsolute(folder) ? folder : `${process.cwd()}/${folder}`}/${name}`;
|
|
22168
23844
|
if (yield* fs.exists(candidate).pipe(Effect.orElseSucceed(() => false))) return Option.some(candidate);
|
|
22169
23845
|
}
|
|
22170
23846
|
return Option.none();
|
|
@@ -22182,15 +23858,18 @@ const askToTrust = (hooks, shown, yesOrNo) => yesOrNo([`Codex runs a plugin's ho
|
|
|
22182
23858
|
* Codex's default. A change to their Codex's security, so it's shown in full and asked the first
|
|
22183
23859
|
* time (decided 9 Oct, 5).
|
|
22184
23860
|
*/
|
|
22185
|
-
const askForProfile = (replacing, shown, yesOrNo) => yesOrNo([
|
|
23861
|
+
const askForProfile = (replacing, checkouts, shown, yesOrNo) => yesOrNo([
|
|
22186
23862
|
"Codex's sandbox keeps commands from writing .git or reaching the network, so a Codex agent can't commit or push to ref0.",
|
|
22187
|
-
"ref0's profile, \"ref0\", is Codex's workspace profile with
|
|
23863
|
+
"ref0's profile, \"ref0\", is Codex's workspace profile with these changes:",
|
|
22188
23864
|
"- each workspace root's .git is writable, so an agent can commit",
|
|
23865
|
+
`- ${printable(checkouts)} is writable, so a signed-off checkout can keep you as author; the agent’s author files stay outside it`,
|
|
22189
23866
|
"- the network is on, so an agent can push; any command can then reach the internet, services and sockets on this machine, and your login Keychain",
|
|
22190
23867
|
"In a linked worktree (git worktree add), Codex still needs full access there.",
|
|
22191
23868
|
...replacing.default === void 0 ? [] : [`It replaces your default now, ${printable(replacing.default)}.`],
|
|
22192
23869
|
...replacing.ownRef0 ? ["It replaces the profile you have named \"ref0\", and every rule in it."] : []
|
|
22193
23870
|
], `Make "ref0" the default profile for ${printable(shown)}?`);
|
|
23871
|
+
/** Shows the person what naming the agent as author changes in Codex, and asks whether it may. */
|
|
23872
|
+
const askForCommits = (shown, yesOrNo) => yesOrNo(["ref0 will set GIT_CONFIG_PARAMETERS and REF0_HARNESS in this Codex setup’s shell_environment_policy, and exclude inherited GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL. Its agents will author their commits, with you as committer. Signed-off checkouts keep you as author. Your own terminal is unchanged."], `Name the agent as author of its Codex commits, for ${printable(shown)}?`);
|
|
22194
23873
|
/** What a Codex folder without ref0's profile, or full access, can't do. */
|
|
22195
23874
|
const sandboxWords = "Codex’s sandbox is as it was, so its agents commit and push to ref0 only where it has full access; to change that, run it again in a terminal.";
|
|
22196
23875
|
/**
|
|
@@ -22213,6 +23892,7 @@ const writeRef0Launcher = Effect.fnUntraced(function* (home, git) {
|
|
|
22213
23892
|
const gitPath = yield* fs.realPath(git);
|
|
22214
23893
|
yield* fs.writeFileString(next, [
|
|
22215
23894
|
"#!/bin/sh",
|
|
23895
|
+
`unset ${otherOriginVariable}`,
|
|
22216
23896
|
"if [ \"$REF0_SELECT_ORIGIN\" = 1 ]; then",
|
|
22217
23897
|
` checkout=$(${shellWord(gitPath)} config --get ref0.api 2>/dev/null)`,
|
|
22218
23898
|
" result=$?",
|
|
@@ -22222,7 +23902,11 @@ const writeRef0Launcher = Effect.fnUntraced(function* (home, git) {
|
|
|
22222
23902
|
` *) printf '%s\\n' ${shellWord("ref0: could not read the checkout origin with Git. Restore Git, then run ref0 install again.")} >&2; exit 1 ;;`,
|
|
22223
23903
|
" esac",
|
|
22224
23904
|
" export REF0_SELECTED_API",
|
|
22225
|
-
` ${shellWord(node)} -e ${shellWord("try { process.exit(new URL(process.env.REF0_SELECTED_API).origin === process.argv[1] ? 0 : 1) } catch { process.exit(1) }")} ${shellWord(config.api)}
|
|
23905
|
+
` if ! ${shellWord(node)} -e ${shellWord("try { process.exit(new URL(process.env.REF0_SELECTED_API).origin === process.argv[1] ? 0 : 1) } catch { process.exit(1) }")} ${shellWord(config.api)}; then`,
|
|
23906
|
+
` [ "$1" = ${harnessCommandNames.mcp} ] || exit 0`,
|
|
23907
|
+
` ${otherOriginVariable}=$REF0_SELECTED_API`,
|
|
23908
|
+
` export ${otherOriginVariable}`,
|
|
23909
|
+
" fi",
|
|
22226
23910
|
"fi",
|
|
22227
23911
|
"unset REF0_SELECT_ORIGIN REF0_SELECTED_API",
|
|
22228
23912
|
`export REF0_API=${shellWord(config.api)}`,
|
|
@@ -22246,38 +23930,47 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
|
|
|
22246
23930
|
const failAll = (into, failed) => {
|
|
22247
23931
|
for (const folder of into) answers.set(folder, Result.fail(failed));
|
|
22248
23932
|
};
|
|
23933
|
+
const started = /* @__PURE__ */ new Set();
|
|
23934
|
+
const clearUnstarted = (into) => Effect.forEach(into.filter((folder) => folder.harness === "codex" && !started.has(folder)), (folder) => codexAgentOf(folder.configFolder).pipe(Effect.flatMap((agent) => clearOtherCodexAuthor(agent, {
|
|
23935
|
+
configFolder: folder.configFolder,
|
|
23936
|
+
home
|
|
23937
|
+
}).pipe(Effect.orDie)), Effect.catchTag("InstallFailed", () => Effect.void)), { discard: true });
|
|
22249
23938
|
const launcher = yield* Effect.cached(Effect.gen(function* () {
|
|
22250
23939
|
const git = yield* onPath("git");
|
|
22251
23940
|
if (Option.isNone(git)) return yield* new InstallFailed({ says: "Git isn’t on this machine’s PATH. Install it." });
|
|
22252
23941
|
yield* writeRef0Launcher(home, git.value).pipe(Effect.mapError(() => new InstallFailed({ says: "Couldn’t write ~/.ref0. Check that it’s writable." })));
|
|
22253
23942
|
}));
|
|
22254
|
-
|
|
22255
|
-
const
|
|
22256
|
-
|
|
22257
|
-
|
|
22258
|
-
|
|
22259
|
-
|
|
22260
|
-
|
|
22261
|
-
|
|
22262
|
-
|
|
22263
|
-
yield* launcher;
|
|
22264
|
-
const release = yield* marketplaceFor(harness, {
|
|
22265
|
-
command: command.value,
|
|
22266
|
-
home,
|
|
22267
|
-
configFolder: into[0]?.configFolder ?? home
|
|
22268
|
-
});
|
|
22269
|
-
for (const folder of into) {
|
|
22270
|
-
const installed = yield* (harness === "claude-code" ? installedInClaudeCode(release, folder.configFolder) : installedInCodex(release, {
|
|
22271
|
-
codex: command.value,
|
|
22272
|
-
home,
|
|
22273
|
-
configFolder: folder.configFolder,
|
|
22274
|
-
name,
|
|
22275
|
-
yesOrNo
|
|
22276
|
-
})).pipe(Effect.result);
|
|
22277
|
-
answers.set(folder, installed);
|
|
23943
|
+
yield* Effect.gen(function* () {
|
|
23944
|
+
for (const harness of ConnectHarness.literals) {
|
|
23945
|
+
const into = folders.filter((folder) => folder.harness === harness);
|
|
23946
|
+
if (into.length === 0) continue;
|
|
23947
|
+
const command = yield* onPath(harness === "codex" ? "codex" : "claude");
|
|
23948
|
+
if (Option.isNone(command)) {
|
|
23949
|
+
failAll(into, new InstallFailed({ says: `${configFolders[harness].name} isn’t on this machine’s PATH. Install it.` }));
|
|
23950
|
+
yield* clearUnstarted(into);
|
|
23951
|
+
continue;
|
|
22278
23952
|
}
|
|
22279
|
-
|
|
22280
|
-
|
|
23953
|
+
yield* Effect.scoped(Effect.gen(function* () {
|
|
23954
|
+
yield* launcher;
|
|
23955
|
+
const release = yield* marketplaceFor(harness, {
|
|
23956
|
+
command: command.value,
|
|
23957
|
+
home,
|
|
23958
|
+
configFolder: into[0]?.configFolder ?? home
|
|
23959
|
+
});
|
|
23960
|
+
for (const folder of into) {
|
|
23961
|
+
started.add(folder);
|
|
23962
|
+
const installed = yield* (harness === "claude-code" ? installedInClaudeCode(release, folder.configFolder) : installedInCodex(release, {
|
|
23963
|
+
codex: command.value,
|
|
23964
|
+
home,
|
|
23965
|
+
configFolder: folder.configFolder,
|
|
23966
|
+
name,
|
|
23967
|
+
yesOrNo
|
|
23968
|
+
})).pipe(Effect.result);
|
|
23969
|
+
answers.set(folder, installed);
|
|
23970
|
+
}
|
|
23971
|
+
}).pipe(Effect.catchTag("InstallFailed", (failed) => Effect.sync(() => failAll(into, failed)).pipe(Effect.andThen(clearUnstarted(into))))));
|
|
23972
|
+
}
|
|
23973
|
+
}).pipe(Effect.onInterrupt(() => clearUnstarted(folders)));
|
|
22281
23974
|
return folders.map((folder) => ({
|
|
22282
23975
|
...folder,
|
|
22283
23976
|
installed: answers.get(folder) ?? Result.fail(new InstallFailed({ says: "ref0 didn’t install into this folder." }))
|
|
@@ -22292,14 +23985,27 @@ const installedInClaudeCode = Effect.fnUntraced(function* (release, configFolder
|
|
|
22292
23985
|
words: `Installed in Claude Code, from ${folder}.`
|
|
22293
23986
|
};
|
|
22294
23987
|
});
|
|
23988
|
+
/** The agent a Codex folder's host grant names, whose ID its commits' author file is named by. */
|
|
23989
|
+
const codexAgentOf = (configFolder) => HostGrant.use((grants) => grants.get(configFolder)).pipe(Effect.catchTag("KeychainError", (failed) => Effect.fail(failed.unreadable === true ? new InstallFailed({
|
|
23990
|
+
says: "Its host grant can’t be read.",
|
|
23991
|
+
run: "ref0 connect"
|
|
23992
|
+
}) : new InstallFailed({ says: failed.nextStep }))), Effect.flatMap(Option.match({
|
|
23993
|
+
onNone: () => Effect.fail(new InstallFailed({
|
|
23994
|
+
says: "It isn’t connected.",
|
|
23995
|
+
run: "ref0 connect"
|
|
23996
|
+
})),
|
|
23997
|
+
onSome: (grant) => Effect.succeed(grant)
|
|
23998
|
+
})));
|
|
22295
23999
|
const installedInCodex = Effect.fnUntraced(function* (release, options) {
|
|
22296
24000
|
const shown = shownConfigFolder(options.home, options.configFolder);
|
|
22297
|
-
const
|
|
24001
|
+
const agent = yield* codexAgentOf(options.configFolder);
|
|
24002
|
+
const done = yield* installCodex(release, agent, {
|
|
22298
24003
|
codex: options.codex,
|
|
22299
24004
|
home: options.home,
|
|
22300
24005
|
configFolder: options.configFolder,
|
|
22301
24006
|
approve: (hooks) => askToTrust(hooks, shown, options.yesOrNo),
|
|
22302
|
-
approveProfile: (replacing) => askForProfile(replacing, shown, options.yesOrNo)
|
|
24007
|
+
approveProfile: (replacing, checkouts) => askForProfile(replacing, checkouts, shown, options.yesOrNo),
|
|
24008
|
+
approveCommits: () => askForCommits(shown, options.yesOrNo)
|
|
22303
24009
|
});
|
|
22304
24010
|
const sandbox = done.profile ? "" : ` ${sandboxWords}`;
|
|
22305
24011
|
const trust = trustRef0Hooks(installLine(options.configFolder), `${options.name}@${options.name}`);
|
|
@@ -22482,7 +24188,7 @@ const answerOf = (connected, installed, others) => ({
|
|
|
22482
24188
|
const connectCommands = [Command.make("connect", { json }, ({ json: asJson }) => Effect.gen(function* () {
|
|
22483
24189
|
const session = yield* SessionToken;
|
|
22484
24190
|
const tty = yield* Tty;
|
|
22485
|
-
if (
|
|
24191
|
+
if (yield* session.current.pipe(Effect.map(Option.isSome), Effect.catchTag("NoSession", () => Effect.succeed(true)))) return yield* sayFailed("ref0 connect runs only in a person’s own terminal, never in an agent’s session: it shows a code, and a yes to that code connects this machine to an agent. If this machine needs connecting, tell your person to run ref0 connect in their own terminal. Never ask them to approve a code for you.");
|
|
22486
24192
|
if (!tty.attached) return yield* sayFailed("ref0 connect runs only in a terminal you type into, and this isn’t one. Run it yourself, in your own terminal: not through a script, a pipe or an agent.");
|
|
22487
24193
|
yield* (yield* ConnectLock).hold;
|
|
22488
24194
|
yield* (yield* Keychain).probe;
|
|
@@ -23350,7 +25056,7 @@ const isPlainJson = (value, depth = 0) => {
|
|
|
23350
25056
|
const proto = Object.getPrototypeOf(value);
|
|
23351
25057
|
return (proto === Object.prototype || proto === null) && Object.values(value).every(inner);
|
|
23352
25058
|
};
|
|
23353
|
-
const decodeAnswer = Schema.decodeUnknownOption(CheckRunAnswer);
|
|
25059
|
+
const decodeAnswer$1 = Schema.decodeUnknownOption(CheckRunAnswer);
|
|
23354
25060
|
const encodeJson$1 = Schema.encodeSync(Schema.fromJsonString(Schema.Json));
|
|
23355
25061
|
/**
|
|
23356
25062
|
* A check's bundle, written for `import()` under a name of its code's and this process's, in the
|
|
@@ -23491,7 +25197,7 @@ const runPart = Effect.fnUntraced(function* (own, part) {
|
|
|
23491
25197
|
error: "timeout",
|
|
23492
25198
|
why: `It ran past its timeout, ${part.timeoutSeconds} seconds`
|
|
23493
25199
|
}),
|
|
23494
|
-
onSome: (answer) => isPlainJson(answer, 0) ? Option.getOrElse(decodeAnswer(answer), () => ({
|
|
25200
|
+
onSome: (answer) => isPlainJson(answer, 0) ? Option.getOrElse(decodeAnswer$1(answer), () => ({
|
|
23495
25201
|
error: "invalid output",
|
|
23496
25202
|
why: "Its answer isn't one a check gives"
|
|
23497
25203
|
})) : {
|
|
@@ -23688,7 +25394,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
|
|
|
23688
25394
|
if (Option.isNone(thread)) return yield* sayFailed("Name the thread with --thread owner/project#number.");
|
|
23689
25395
|
const root = yield* gitIn(folder, ["rev-parse", "--show-toplevel"]);
|
|
23690
25396
|
if (Option.isNone(root) || root.value === "") return yield* sayFailed("Git couldn’t find this checkout’s root.");
|
|
23691
|
-
const session = yield* (yield* SessionToken).current.pipe(Effect.catchTag("KeychainError", (failed) => sayFailed(failed.nextStep)));
|
|
25397
|
+
const session = yield* (yield* SessionToken).current.pipe(Effect.catchTag(["KeychainError", "NoSession"], (failed) => sayFailed(failed.nextStep)));
|
|
23692
25398
|
if (Option.isSome(session) && Option.isSome(named.agent)) return yield* sayFailed("A session’s local run is signed by the session: leave out --agent.");
|
|
23693
25399
|
if (Option.isNone(session) && Option.isNone(named.agent)) return yield* sayFailed("Outside a session, name the agent this machine is attached as, with --agent owner/handle: a local run is signed by the attachment it ran on.");
|
|
23694
25400
|
const attachment = Option.isSome(named.agent) ? { attachment: yield* namedAttachment(named.agent.value, Option.none(), true) } : {};
|
|
@@ -24467,6 +26173,44 @@ const answerGit = Effect.fnUntraced(function* (operation, input) {
|
|
|
24467
26173
|
return Option.map(token, credentialLines);
|
|
24468
26174
|
});
|
|
24469
26175
|
//#endregion
|
|
26176
|
+
//#region src/own-session.ts
|
|
26177
|
+
/**
|
|
26178
|
+
* A command only an agent's session runs, on a repo of the session's project. Its endpoints take
|
|
26179
|
+
* the repo's name alone and find it in that project, so the command takes the name alone too: an
|
|
26180
|
+
* owner it couldn't check could name another project's repo, and the session's of that name
|
|
26181
|
+
* would be changed in its place.
|
|
26182
|
+
*/
|
|
26183
|
+
const decodeRepoName$1 = Schema.decodeUnknownOption(RepoName);
|
|
26184
|
+
/**
|
|
26185
|
+
* The token of the agent's session a command runs in. Outside one it says `outside`, what the
|
|
26186
|
+
* command is for; in a harness session whose ref0 session never started, why and what to do.
|
|
26187
|
+
* Either way it fails before anything's sent.
|
|
26188
|
+
*/
|
|
26189
|
+
const ownSession = Effect.fnUntraced(function* (outside) {
|
|
26190
|
+
const token = yield* (yield* SessionToken).current.pipe(Effect.catchTag("NoSession", (none) => sayFailed(none.nextStep)));
|
|
26191
|
+
if (Option.isNone(token)) return yield* sayFailed(outside);
|
|
26192
|
+
return token.value;
|
|
26193
|
+
});
|
|
26194
|
+
/** The repo of a session's command, by its name alone. */
|
|
26195
|
+
const sessionRepoFlag = Flag.String("repo").pipe(Flag.withDescription("A repo of your session’s project, by its name alone, as shop; if not named, the one this checkout is of"), Flag.optional);
|
|
26196
|
+
/**
|
|
26197
|
+
* The session a command runs as, and the name of the repo it acts on: the one named, or this
|
|
26198
|
+
* checkout's. Outside a session it says `outside`, what the command is for, and fails before
|
|
26199
|
+
* anything's sent.
|
|
26200
|
+
*/
|
|
26201
|
+
const sessionRepo = Effect.fnUntraced(function* (named, outside) {
|
|
26202
|
+
yield* ownSession(outside);
|
|
26203
|
+
const checkout = yield* Checkout;
|
|
26204
|
+
const repo = Option.isSome(named) ? decodeRepoName$1(named.value) : yield* checkout.repoAt(".");
|
|
26205
|
+
if (Option.isNone(repo)) return yield* sayFailed(Option.isSome(named) ? "Name a repo of your session’s project by its name alone, as --repo shop." : "Name the repo with --repo and its name, as --repo shop, or run this in its checkout.");
|
|
26206
|
+
const session = (yield* callEndpoint({ endpoint: "me.get" })).value.session?.session;
|
|
26207
|
+
if (session === void 0) return yield* sayFailed("This token isn’t a session’s.");
|
|
26208
|
+
return {
|
|
26209
|
+
session,
|
|
26210
|
+
repo: repo.value
|
|
26211
|
+
};
|
|
26212
|
+
});
|
|
26213
|
+
//#endregion
|
|
24470
26214
|
//#region src/sync-history.ts
|
|
24471
26215
|
const decodePlace = Schema.decodeUnknownEffect(Schema.Struct({
|
|
24472
26216
|
owner: OwnerRef,
|
|
@@ -24522,7 +26266,7 @@ const trimShallow = Effect.fnUntraced(function* (gitDir) {
|
|
|
24522
26266
|
*/
|
|
24523
26267
|
const loadSyncHistory = Effect.fnUntraced(function* (gitDir, repo) {
|
|
24524
26268
|
const config = yield* Ref0Config;
|
|
24525
|
-
const token = yield* (yield* SessionToken).current;
|
|
26269
|
+
const token = yield* (yield* SessionToken).current.pipe(Effect.catchTag("NoSession", () => Effect.succeedNone));
|
|
24526
26270
|
if (Option.isNone(token)) return yield* new HistoryNotLoaded({ why: "this terminal has no session to load it as" });
|
|
24527
26271
|
const fs = yield* FileSystem.FileSystem;
|
|
24528
26272
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -24695,9 +26439,8 @@ const gitCommands = [
|
|
|
24695
26439
|
json
|
|
24696
26440
|
}, ({ repo: written, directory, json: asJson }) => Effect.gen(function* () {
|
|
24697
26441
|
const { owner, repo: name } = repoParts(written);
|
|
24698
|
-
const session = yield* SessionToken;
|
|
24699
26442
|
const config = yield* Ref0Config;
|
|
24700
|
-
|
|
26443
|
+
yield* ownSession("ref0 clone runs only in an agent’s ref0 session, which pushes to a fork of its own, and none runs here. In Claude Code or Codex, one starts at each prompt once ref0 connect has connected its config folder.");
|
|
24701
26444
|
const ours = (yield* callEndpoint({ endpoint: "me.get" })).value.session?.session;
|
|
24702
26445
|
if (ours === void 0) return yield* sayFailed("This token isn’t a session’s.");
|
|
24703
26446
|
const fork = yield* callEndpoint({
|
|
@@ -24729,8 +26472,7 @@ const gitCommands = [
|
|
|
24729
26472
|
yield* printAnswer(fork.json, asJson, `Cloned ${owner}/${name} into ${into}. origin is read-only to agents: git push goes to ${forkRemote(fork.value)}, the fork of whichever session on the thread pushes.`);
|
|
24730
26473
|
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Clone a repo of the session’s project, to push to its fork"), Command.withShortDescription("Clone a repo to work in your session’s fork")),
|
|
24731
26474
|
Command.make("sync", { json }, ({ json: asJson }) => Effect.gen(function* () {
|
|
24732
|
-
|
|
24733
|
-
if (Option.isNone(yield* session.current)) return yield* sayFailed("ref0 sync runs in an agent’s session, in a clone ref0 clone made.");
|
|
26475
|
+
yield* ownSession("ref0 sync runs in an agent’s session, in a clone ref0 clone made.");
|
|
24734
26476
|
const synced = yield* syncBase(".");
|
|
24735
26477
|
if (synced.kind === "refused") return yield* sayFailed(synced.why);
|
|
24736
26478
|
const { on, commit } = synced.base;
|
|
@@ -26014,35 +27756,6 @@ const planCommands = [
|
|
|
26014
27756
|
}).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("Answer a decision that waits on you"))
|
|
26015
27757
|
];
|
|
26016
27758
|
//#endregion
|
|
26017
|
-
//#region src/own-session.ts
|
|
26018
|
-
/**
|
|
26019
|
-
* A command only an agent's session runs, on a repo of the session's project. Its endpoints take
|
|
26020
|
-
* the repo's name alone and find it in that project, so the command takes the name alone too: an
|
|
26021
|
-
* owner it couldn't check could name another project's repo, and the session's of that name
|
|
26022
|
-
* would be changed in its place.
|
|
26023
|
-
*/
|
|
26024
|
-
const decodeRepoName$1 = Schema.decodeUnknownOption(RepoName);
|
|
26025
|
-
/** The repo of a session's command, by its name alone. */
|
|
26026
|
-
const sessionRepoFlag = Flag.String("repo").pipe(Flag.withDescription("A repo of your session’s project, by its name alone, as shop; if not named, the one this checkout is of"), Flag.optional);
|
|
26027
|
-
/**
|
|
26028
|
-
* The session a command runs as, and the name of the repo it acts on: the one named, or this
|
|
26029
|
-
* checkout's. Outside a session it says `outside`, what the command is for, and fails before
|
|
26030
|
-
* anything's sent.
|
|
26031
|
-
*/
|
|
26032
|
-
const sessionRepo = Effect.fnUntraced(function* (named, outside) {
|
|
26033
|
-
const token = yield* SessionToken;
|
|
26034
|
-
if (Option.isNone(yield* token.current)) return yield* sayFailed(outside);
|
|
26035
|
-
const checkout = yield* Checkout;
|
|
26036
|
-
const repo = Option.isSome(named) ? decodeRepoName$1(named.value) : yield* checkout.repoAt(".");
|
|
26037
|
-
if (Option.isNone(repo)) return yield* sayFailed(Option.isSome(named) ? "Name a repo of your session’s project by its name alone, as --repo shop." : "Name the repo with --repo and its name, as --repo shop, or run this in its checkout.");
|
|
26038
|
-
const session = (yield* callEndpoint({ endpoint: "me.get" })).value.session?.session;
|
|
26039
|
-
if (session === void 0) return yield* sayFailed("This token isn’t a session’s.");
|
|
26040
|
-
return {
|
|
26041
|
-
session,
|
|
26042
|
-
repo: repo.value
|
|
26043
|
-
};
|
|
26044
|
-
});
|
|
26045
|
-
//#endregion
|
|
26046
27759
|
//#region src/pull-request-lines.ts
|
|
26047
27760
|
/**
|
|
26048
27761
|
* Pull requests as `ref0 pr` says them: each one's number, thread, state and current version,
|
|
@@ -26503,8 +28216,7 @@ const taskCommands = [Command.make("tasks", {
|
|
|
26503
28216
|
file: Flag.FileText("file").pipe(Flag.withDescription("The whole list, as JSON: each task’s text, up to 200 characters, and its state, \"to do\", \"in progress\" or \"done\"; at most 50")),
|
|
26504
28217
|
json
|
|
26505
28218
|
}, ({ file, json: asJson }) => Effect.gen(function* () {
|
|
26506
|
-
|
|
26507
|
-
if (Option.isNone(yield* session.current)) return yield* sayFailed("ref0 tasks runs in an agent’s session, which sets its own list.");
|
|
28219
|
+
yield* ownSession("ref0 tasks runs in an agent’s session, which sets its own list.");
|
|
26508
28220
|
const parsed = yield* Effect.result(decodeJsonText(file));
|
|
26509
28221
|
if (Result.isFailure(parsed)) return yield* sayFailed("That list isn’t JSON.");
|
|
26510
28222
|
const checked = yield* Effect.result(checkTasks(parsed.success));
|
|
@@ -27107,9 +28819,8 @@ const waitCommands = [Command.make("wait", {
|
|
|
27107
28819
|
blockers: Flag.Boolean("blockers").pipe(Flag.withDescription("Wait until nothing blocks your session’s thread from merging"), Flag.withDefault(false)),
|
|
27108
28820
|
json
|
|
27109
28821
|
}, ({ blockers, json: asJson }) => Effect.gen(function* () {
|
|
27110
|
-
const session = yield* SessionToken;
|
|
27111
28822
|
if (!blockers) return yield* sayFailed("Say what to wait for: ref0 wait --blockers waits until nothing blocks your thread.");
|
|
27112
|
-
|
|
28823
|
+
yield* ownSession("ref0 wait --blockers runs in an agent’s session: it waits on its own thread.");
|
|
27113
28824
|
yield* waitOnBlockers(asJson);
|
|
27114
28825
|
}).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription(`Wait until nothing blocks your session’s thread from merging, for 9 minutes at most`), Command.withShortDescription("Wait for your session’s thread to be ready to merge"))];
|
|
27115
28826
|
//#endregion
|
|
@@ -30727,6 +32438,7 @@ var Waker = class Waker extends Context.Service()("ref0/cli/Waker") {
|
|
|
30727
32438
|
const socket = yield* Config.String("CLAUDE_CODE_MESSAGING_SOCKET").pipe(Config.option);
|
|
30728
32439
|
const token = yield* Config.Redacted("CLAUDE_CODE_MESSAGING_TOKEN").pipe(Config.option);
|
|
30729
32440
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
32441
|
+
const fs = yield* FileSystem.FileSystem;
|
|
30730
32442
|
const post = Option.zipWith(socket, token, (path, secret) => Effect.callback((resume) => {
|
|
30731
32443
|
const connection = connect(path);
|
|
30732
32444
|
connection.on("error", () => resume(Effect.succeed(false)));
|
|
@@ -30753,6 +32465,7 @@ var Waker = class Waker extends Context.Service()("ref0/cli/Waker") {
|
|
|
30753
32465
|
"--message",
|
|
30754
32466
|
wakeLine
|
|
30755
32467
|
], {
|
|
32468
|
+
cwd: yield* emptyFolder(fs),
|
|
30756
32469
|
env: { CODEX_HOME: setup.home },
|
|
30757
32470
|
extendEnv: true
|
|
30758
32471
|
}));
|
|
@@ -30867,6 +32580,8 @@ function readHook(args) {
|
|
|
30867
32580
|
*/
|
|
30868
32581
|
/** The keychain account a binding's thread is kept under, beside its session token's. */
|
|
30869
32582
|
const threadAccount = (binding) => `${sessionAccount(binding)}/thread`;
|
|
32583
|
+
/** The keychain account a start not yet landed is kept under, beside its session token's. */
|
|
32584
|
+
const pendingAccount = (binding) => `${sessionAccount(binding)}/pending`;
|
|
30870
32585
|
const decodeRequest = Schema.decodeUnknownEffect(StartSessionRequest);
|
|
30871
32586
|
const decodeRequestId = Schema.decodeUnknownOption(RequestId);
|
|
30872
32587
|
/**
|
|
@@ -30906,14 +32621,14 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
|
|
|
30906
32621
|
const folder = yield* (yield* ConfigFolders).folder(harness);
|
|
30907
32622
|
const grant = yield* hostGrant.get(folder);
|
|
30908
32623
|
if (Option.isNone(grant) || grant.value.harness !== harness) return Option.none();
|
|
30909
|
-
const
|
|
32624
|
+
const pending = pendingAccount(input.binding);
|
|
30910
32625
|
const held = Option.flatMap(yield* keychain.get(sessionAccount(input.binding)), (kept) => parseToken(Redacted.value(kept)));
|
|
30911
|
-
const
|
|
30912
|
-
const { requestId, secret } = Option.isSome(
|
|
32626
|
+
const resent = Option.filter(Option.flatMap(yield* keychain.get(pending), (kept) => decodePending(Redacted.value(kept))), (one) => !Option.exists(held, (parts) => parts.secret === one.secret));
|
|
32627
|
+
const { requestId, secret } = Option.isSome(resent) ? resent.value : {
|
|
30913
32628
|
requestId: yield* newRequestId,
|
|
30914
32629
|
secret: encodeSecret(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)))
|
|
30915
32630
|
};
|
|
30916
|
-
if (Option.isNone(
|
|
32631
|
+
if (Option.isNone(resent)) yield* keychain.set(pending, Redacted.make(`${requestId} ${secret}`));
|
|
30917
32632
|
const hash = yield* hashOf(secret);
|
|
30918
32633
|
const where = input.on.kind === "thread" ? threadParts(input.on.written) : {
|
|
30919
32634
|
owner: grant.value.owner,
|
|
@@ -30943,7 +32658,7 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
|
|
|
30943
32658
|
const started = yield* (yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(grant.value.token)))).sessions.start({
|
|
30944
32659
|
headers: { "idempotency-key": requestId },
|
|
30945
32660
|
payload
|
|
30946
|
-
}).pipe(Effect.tapError((error) => Match.value(error).pipe(Match.tag("ProjectNotFound", "ProjectNotReady", "ThreadNotFound", "ThreadIsClosed", "TextRefused", "NotPermitted", "DeliveryOutsideWindow", () => keychain.remove(
|
|
32661
|
+
}).pipe(Effect.tapError((error) => Match.value(error).pipe(Match.tag("ProjectNotFound", "ProjectNotReady", "ThreadNotFound", "ThreadIsClosed", "TextRefused", "NotPermitted", "DeliveryOutsideWindow", () => keychain.remove(pending)), Match.orElse(() => Effect.void))));
|
|
30947
32662
|
const thread = input.on.kind === "thread" ? input.on.written : writtenThread(grant.value.owner, {
|
|
30948
32663
|
kind: "project",
|
|
30949
32664
|
name: newThreadProject
|
|
@@ -30951,7 +32666,7 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
|
|
|
30951
32666
|
yield* keychain.remove(gitTokenAccount(input.binding)).pipe(Effect.ignore);
|
|
30952
32667
|
yield* keychain.set(sessionAccount(input.binding), Redacted.make(formatToken(started.credential.person, started.credential.id, secret)));
|
|
30953
32668
|
yield* keychain.set(threadAccount(input.binding), Redacted.make(thread)).pipe(Effect.ignore);
|
|
30954
|
-
yield* keychain.remove(
|
|
32669
|
+
yield* keychain.remove(pending).pipe(Effect.ignore);
|
|
30955
32670
|
return Option.some(started);
|
|
30956
32671
|
});
|
|
30957
32672
|
//#endregion
|
|
@@ -31013,6 +32728,11 @@ const ownerOf = (bound) => ({
|
|
|
31013
32728
|
const decodeWrittenThread = Schema.decodeUnknownOption(WrittenThread);
|
|
31014
32729
|
/** How long a harness session with no token is left before the keychain is asked again. */
|
|
31015
32730
|
const unboundMillis = 3e4;
|
|
32731
|
+
/**
|
|
32732
|
+
* How long a prompt's start may run on past its hook's deadline before it's given up, and a later
|
|
32733
|
+
* prompt sends it again, as itself: well past the slowest start seen on staging, 4.5 seconds.
|
|
32734
|
+
*/
|
|
32735
|
+
const startBound = "20 seconds";
|
|
31016
32736
|
/** How often `ref0 mcp` renews its session's 30-minute lease while its harness process lives. */
|
|
31017
32737
|
const renewEvery = "10 minutes";
|
|
31018
32738
|
/** How soon a renewal that failed is tried again, well inside the lease. */
|
|
@@ -31028,6 +32748,7 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
|
|
|
31028
32748
|
const hints = yield* LiveHints;
|
|
31029
32749
|
const context = yield* Effect.context();
|
|
31030
32750
|
const owned = yield* Ref.make(Option.none());
|
|
32751
|
+
const onTheWay = yield* Ref.make(Option.none());
|
|
31031
32752
|
const listen = Effect.fnUntraced(function* (bound) {
|
|
31032
32753
|
const client = yield* clientOf(bound.token);
|
|
31033
32754
|
let pause = 1;
|
|
@@ -31123,17 +32844,31 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
|
|
|
31123
32844
|
if (Option.isNone(started)) return Option.none();
|
|
31124
32845
|
return yield* keychain.get(sessionAccount(binding)).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
31125
32846
|
});
|
|
32847
|
+
const startedFor = Effect.fnUntraced(function* (binding, hook, starting) {
|
|
32848
|
+
const sent = yield* Ref.get(onTheWay);
|
|
32849
|
+
if (Option.isSome(sent) && sent.value.harnessSession === binding.harnessSession) return yield* Deferred.await(sent.value.done);
|
|
32850
|
+
if (!starting) return Option.none();
|
|
32851
|
+
const done = yield* Deferred.make();
|
|
32852
|
+
const mine = Option.some({
|
|
32853
|
+
harnessSession: binding.harnessSession,
|
|
32854
|
+
done
|
|
32855
|
+
});
|
|
32856
|
+
yield* Ref.set(onTheWay, mine);
|
|
32857
|
+
yield* Effect.forkIn(startAtPrompt(binding, hook).pipe(Effect.timeoutOption(startBound), Effect.map(Option.flatten), Effect.orElseSucceed(() => Option.none()), Effect.ensuring(Ref.update(onTheWay, (now) => now === mine ? Option.none() : now)), Effect.onExit((exit) => Deferred.done(done, exit))), scope);
|
|
32858
|
+
return yield* Deferred.await(done);
|
|
32859
|
+
});
|
|
31126
32860
|
const bind = Effect.fnUntraced(function* (hook) {
|
|
31127
32861
|
const now = yield* Clock.currentTimeMillis;
|
|
31128
32862
|
const current = yield* Ref.get(state);
|
|
31129
32863
|
if (current.kind === "bound" && current.bound.binding.harnessSession === hook.harnessSession) return Option.some(current.bound);
|
|
31130
32864
|
const starting = hook.event === "UserPromptSubmit" && Option.isSome(agent);
|
|
32865
|
+
const sent = Option.exists(yield* Ref.get(onTheWay), (one) => one.harnessSession === hook.harnessSession);
|
|
31131
32866
|
const over = current.kind === "ended" && current.harnessSession === hook.harnessSession;
|
|
31132
32867
|
const waiting = current.kind === "unbound" && now < current.until && !starting;
|
|
31133
|
-
if (over && !starting || waiting) return Option.none();
|
|
32868
|
+
if ((over && !starting || waiting) && !sent) return Option.none();
|
|
31134
32869
|
const binding = yield* bindingForServer(hook.harnessSession);
|
|
31135
32870
|
const held = Option.isNone(binding) ? Option.none() : yield* heldFor(binding.value, over);
|
|
31136
|
-
const token = Option.isSome(held) || Option.isNone(binding)
|
|
32871
|
+
const token = Option.isSome(held) || Option.isNone(binding) ? held : yield* startedFor(binding.value, hook, starting);
|
|
31137
32872
|
if (Option.isNone(binding) || Option.isNone(token)) {
|
|
31138
32873
|
if (!over) yield* Ref.set(state, {
|
|
31139
32874
|
kind: "unbound",
|
|
@@ -31423,7 +33158,7 @@ const toRead = (hook, ownHook) => {
|
|
|
31423
33158
|
const sessionTurns = Effect.fnUntraced(function* (edges) {
|
|
31424
33159
|
const transcripts = yield* Transcripts;
|
|
31425
33160
|
const turn = yield* Ref.make(Option.none());
|
|
31426
|
-
const trackTurn = Effect.fnUntraced(function* (client, bound, hook, deadline) {
|
|
33161
|
+
const trackTurn = Effect.fnUntraced(function* (client, bound, hook, deadline, late = false) {
|
|
31427
33162
|
const checkout = yield* Checkout;
|
|
31428
33163
|
const session = bound.session;
|
|
31429
33164
|
const owner = ownerOf(bound);
|
|
@@ -31437,6 +33172,10 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
|
|
|
31437
33172
|
const prompted = isNew ? hook.transcript : void 0;
|
|
31438
33173
|
if (prompted !== void 0) yield* transcripts.begin(owner, harness, prompted, harnessTurn);
|
|
31439
33174
|
const ending = prompted === void 0 ? void 0 : yield* transcripts.read(owner, harness, [prompted], { until: harnessTurn });
|
|
33175
|
+
const fresh = late && harness === "claude-code" ? {
|
|
33176
|
+
...nothingSeen,
|
|
33177
|
+
tokens: readFailed(nothingSeen.tokens)
|
|
33178
|
+
} : nothingSeen;
|
|
31440
33179
|
const tracked = isNew ? Option.some({
|
|
31441
33180
|
kind: "starting",
|
|
31442
33181
|
harnessTurn,
|
|
@@ -31447,7 +33186,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
|
|
|
31447
33186
|
tokens: addRead(now.seen.tokens, ending, true)
|
|
31448
33187
|
}
|
|
31449
33188
|
}) : now.previous),
|
|
31450
|
-
seen:
|
|
33189
|
+
seen: fresh
|
|
31451
33190
|
}) : before;
|
|
31452
33191
|
if (Option.isNone(tracked)) return;
|
|
31453
33192
|
const ownHook = hook.harnessTurn === tracked.value.harnessTurn;
|
|
@@ -31523,6 +33262,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
|
|
|
31523
33262
|
return {
|
|
31524
33263
|
now: Ref.get(turn),
|
|
31525
33264
|
track: trackTurn,
|
|
33265
|
+
trackLate: (client, bound, prompt, deadline) => trackTurn(client, bound, prompt, deadline, true),
|
|
31526
33266
|
fresh: Ref.set(turn, Option.none())
|
|
31527
33267
|
};
|
|
31528
33268
|
});
|
|
@@ -31659,6 +33399,7 @@ var HarnessSession = class HarnessSession extends Context.Service()("ref0/cli/Ha
|
|
|
31659
33399
|
const scope = yield* Scope.Scope;
|
|
31660
33400
|
const tasks = yield* sessionTasks;
|
|
31661
33401
|
const turns = yield* sessionTurns({ tasksLanded: tasks.landed });
|
|
33402
|
+
const prompted = yield* Ref.make(Option.none());
|
|
31662
33403
|
const edits = yield* sessionEdits({ turn: turns.now });
|
|
31663
33404
|
const delivery = yield* sessionDelivery;
|
|
31664
33405
|
const wake = yield* sessionWake({
|
|
@@ -31716,9 +33457,13 @@ var HarnessSession = class HarnessSession extends Context.Service()("ref0/cli/Ha
|
|
|
31716
33457
|
lastLook
|
|
31717
33458
|
});
|
|
31718
33459
|
const deliver = Effect.fnUntraced(function* (hook, deadline) {
|
|
33460
|
+
if (hook.event === "UserPromptSubmit") yield* Ref.set(prompted, Option.some(hook));
|
|
31719
33461
|
const bound = yield* binding.bind(hook);
|
|
31720
33462
|
if (Option.isNone(bound)) return Option.none();
|
|
31721
33463
|
const client = yield* clientOf(bound.value.token);
|
|
33464
|
+
const prompt = Option.filter(yield* Ref.get(prompted), (one) => one.harnessSession === hook.harnessSession && one.harnessTurn === hook.harnessTurn);
|
|
33465
|
+
if (Option.isSome(prompt)) yield* Ref.set(prompted, Option.none());
|
|
33466
|
+
if (Option.isSome(prompt) && prompt.value !== hook) yield* turns.trackLate(client, bound.value, prompt.value, deadline);
|
|
31722
33467
|
const unattributed = yield* edits.noteEdits(bound.value, hook);
|
|
31723
33468
|
const within = yield* binding.within;
|
|
31724
33469
|
if (yield* tasks.note(bound.value, hook)) yield* Effect.forkIn(tasks.send(client, bound.value).pipe(Effect.ignore), within);
|
|
@@ -31834,6 +33579,18 @@ const mcpServer = (version, started) => McpServer.toolkit(HarnessTools).pipe(Lay
|
|
|
31834
33579
|
protocols
|
|
31835
33580
|
})));
|
|
31836
33581
|
/**
|
|
33582
|
+
* `ref0 mcp` in a checkout of another origin, which the launcher names: the plugin's server all
|
|
33583
|
+
* the same, answering every hook with nothing, since that origin's own plugin serves the
|
|
33584
|
+
* checkout. A harness takes a server that closes before `initialize` as broken, and Claude Code
|
|
33585
|
+
* then skips it for 15 minutes, in every folder. It reads nothing of this machine's, so it
|
|
33586
|
+
* answers at once.
|
|
33587
|
+
*/
|
|
33588
|
+
const otherOriginServer = (version) => McpServer.toolkit(HarnessTools).pipe(Layer.provide(HarnessTools.toLayer({ hook: () => Effect.succeed({}) })), Layer.provide(McpServer.layerStdio({
|
|
33589
|
+
name: "ref0",
|
|
33590
|
+
version,
|
|
33591
|
+
protocols
|
|
33592
|
+
})));
|
|
33593
|
+
/**
|
|
31837
33594
|
* The `codex` that started this server, by absolute path: its executable, which Linux names, or
|
|
31838
33595
|
* its command's first word when that's a path. Codex isn't sure to be on `PATH` (M0).
|
|
31839
33596
|
*/
|
|
@@ -31864,11 +33621,18 @@ const codexSetupHere = Effect.fnUntraced(function* (process) {
|
|
|
31864
33621
|
});
|
|
31865
33622
|
});
|
|
31866
33623
|
const mcp = Command.make(harnessCommandNames.mcp, {}, () => Effect.gen(function* () {
|
|
33624
|
+
const checked = yield* Config.schema(ConnectHarness, serverCheckVariable).pipe(Config.option);
|
|
33625
|
+
const other = yield* Config.option(Config.String(otherOriginVariable));
|
|
33626
|
+
if (Option.isSome(other) && Option.isNone(checked)) {
|
|
33627
|
+
yield* printBlocks([[`ref0: this folder is for ${other.value}, so ref0’s plugin for ${(yield* Ref0Config).api} does nothing in it.`]]);
|
|
33628
|
+
return yield* Layer.launch(otherOriginServer(cliVersion));
|
|
33629
|
+
}
|
|
31867
33630
|
const agents = yield* ConfigFolders;
|
|
31868
|
-
const found = yield* (yield* HarnessProcess).innermost;
|
|
31869
|
-
const
|
|
33631
|
+
const found = Option.isSome(checked) ? Option.none() : yield* (yield* HarnessProcess).innermost;
|
|
33632
|
+
const harness = Option.orElse(checked, () => Option.map(found, (each) => each.harness));
|
|
33633
|
+
const agent = Option.isSome(harness) ? yield* (yield* HostGrant).get(yield* agents.folder(harness.value)).pipe(Effect.catchTag("KeychainError", (failed) => failed.unreadable === true ? Effect.succeedNone : sayFailed(failed.nextStep))) : Option.none();
|
|
31870
33634
|
const codex = Option.isSome(found) && found.value.harness === "codex" ? yield* codexSetupHere(found.value.process) : Option.none();
|
|
31871
|
-
const reinstall = Option.isSome(
|
|
33635
|
+
const reinstall = Option.isSome(harness) && Option.isSome(agent) ? installLine(yield* agents.folder(harness.value)) : "ref0 connect";
|
|
31872
33636
|
return yield* Layer.launch(mcpServer(cliVersion, {
|
|
31873
33637
|
plugin: {
|
|
31874
33638
|
version: yield* pluginVersion,
|
|
@@ -31962,139 +33726,17 @@ const endHarnessSession = Effect.gen(function* () {
|
|
|
31962
33726
|
}).pipe(Effect.ignore);
|
|
31963
33727
|
const sessionEnd = Command.make(harnessCommandNames.sessionEnd, { protocol: protocolFlag }, (flags) => flags.protocol === "1" ? endHarnessSession : Effect.void).pipe(Command.withDescription("Ends the ref0 session of a harness session that's closing"), Command.withShortDescription("End the harness’s ref0 session"));
|
|
31964
33728
|
//#endregion
|
|
31965
|
-
//#region src/harness/git-hooks.ts
|
|
31966
|
-
/**
|
|
31967
|
-
* ref0's Git hooks: folders of forwarders in `~/.ref0/origins/<origin>/git-hooks`, which ref0 names as
|
|
31968
|
-
* `core.hooksPath` in its agents' commands alone, never in a repo, its config or its hooks.
|
|
31969
|
-
* `core.hooksPath` replaces a repo's whole hooks folder, so each forwarder runs the repo's own
|
|
31970
|
-
* hook of its name, with the same arguments, stdin and exit status, wherever the repo keeps it:
|
|
31971
|
-
* husky's `.husky/_`, `.git/hooks/` or another `core.hooksPath`. It finds and runs that hook
|
|
31972
|
-
* with ref0's entry left out of its environment, so a hook that looks up another, as
|
|
31973
|
-
* `pre-applypatch.sample` runs `pre-commit`, finds the repo's own. A forwarder runs nothing
|
|
31974
|
-
* when the repo's hooks path is one of ref0's folders, or when a forwarder started it, so none
|
|
31975
|
-
* calls itself: a first version that left ref0's entry in started about 1,000 processes.
|
|
31976
|
-
*
|
|
31977
|
-
* Git runs a hook that exists even where the repo has none, so the forwarders cost each hook one
|
|
31978
|
-
* `sh` and one `git rev-parse`, about 1.5 ms. Two hooks run on every index write and ref update,
|
|
31979
|
-
* hundreds of times in a long rebase, so only `all` forwards them, for a checkout that has its
|
|
31980
|
-
* own; `most` forwards every other (measured 23 Oct 2026, in the piece's pull request).
|
|
31981
|
-
*
|
|
31982
|
-
* The folders are the CLI's: a session's start writes them again when a forwarder differs, as
|
|
31983
|
-
* after an update. Each is read-only, so a tool that installs hooks wherever `core.hooksPath`
|
|
31984
|
-
* points, run in an agent's command, fails there rather than replacing a forwarder for every
|
|
31985
|
-
* repo.
|
|
31986
|
-
*/
|
|
31987
|
-
/** The hooks Git runs on every index write and ref update. */
|
|
31988
|
-
const frequentHooks = ["post-index-change", "reference-transaction"];
|
|
31989
|
-
/**
|
|
31990
|
-
* Every other hook Git looks for in its hooks path. Not `push-to-checkout`, which replaces Git's
|
|
31991
|
-
* own update of a checkout pushed to merely by existing, nor `fsmonitor-watchman`, which Git runs
|
|
31992
|
-
* from `core.fsmonitor`, never from the hooks path.
|
|
31993
|
-
*/
|
|
31994
|
-
const forwardedHooks = [
|
|
31995
|
-
"applypatch-msg",
|
|
31996
|
-
"pre-applypatch",
|
|
31997
|
-
"post-applypatch",
|
|
31998
|
-
"pre-commit",
|
|
31999
|
-
"pre-merge-commit",
|
|
32000
|
-
"prepare-commit-msg",
|
|
32001
|
-
"commit-msg",
|
|
32002
|
-
"post-commit",
|
|
32003
|
-
"pre-rebase",
|
|
32004
|
-
"post-checkout",
|
|
32005
|
-
"post-merge",
|
|
32006
|
-
"pre-push",
|
|
32007
|
-
"pre-receive",
|
|
32008
|
-
"update",
|
|
32009
|
-
"proc-receive",
|
|
32010
|
-
"post-receive",
|
|
32011
|
-
"post-update",
|
|
32012
|
-
"pre-auto-gc",
|
|
32013
|
-
"post-rewrite",
|
|
32014
|
-
"sendemail-validate",
|
|
32015
|
-
"p4-changelist",
|
|
32016
|
-
"p4-prepare-changelist",
|
|
32017
|
-
"p4-post-changelist",
|
|
32018
|
-
"p4-pre-submit"
|
|
32019
|
-
];
|
|
32020
|
-
/**
|
|
32021
|
-
* The forwarder for one hook. `hooks` is the folder holding ref0's folders: an entry naming one
|
|
32022
|
-
* of them is ref0's, and is left out. The rest keep their order, renumbered.
|
|
32023
|
-
*/
|
|
32024
|
-
const forwarder = (name, hooks) => [
|
|
32025
|
-
"#!/bin/sh",
|
|
32026
|
-
`# ref0 ${cliVersion}'s forwarder for Git's ${name} hook. ref0 sets core.hooksPath to this`,
|
|
32027
|
-
"# folder only in its agents' commands. This runs the repo's own hook, found and run with",
|
|
32028
|
-
"# ref0's entry left out, with the same arguments, stdin and exit status.",
|
|
32029
|
-
`[ -n "\${${gitHookVariables.forwarded}-}" ] && exit 0`,
|
|
32030
|
-
`ref0_hooks=${shellWord(hooks)}`,
|
|
32031
|
-
"ref0_n=${GIT_CONFIG_COUNT:-0}",
|
|
32032
|
-
"ref0_i=0",
|
|
32033
|
-
"ref0_j=0",
|
|
32034
|
-
"while [ \"$ref0_i\" -lt \"$ref0_n\" ]; do",
|
|
32035
|
-
" eval \"ref0_key=\\${GIT_CONFIG_KEY_$ref0_i-} ref0_value=\\${GIT_CONFIG_VALUE_$ref0_i-}\"",
|
|
32036
|
-
" unset \"GIT_CONFIG_KEY_$ref0_i\" \"GIT_CONFIG_VALUE_$ref0_i\"",
|
|
32037
|
-
" if [ \"$ref0_key\" != core.hooksPath ] || [ \"${ref0_value%/*}\" != \"$ref0_hooks\" ]; then",
|
|
32038
|
-
" export \"GIT_CONFIG_KEY_$ref0_j=$ref0_key\" \"GIT_CONFIG_VALUE_$ref0_j=$ref0_value\"",
|
|
32039
|
-
" ref0_j=$((ref0_j + 1))",
|
|
32040
|
-
" fi",
|
|
32041
|
-
" ref0_i=$((ref0_i + 1))",
|
|
32042
|
-
"done",
|
|
32043
|
-
"if [ \"$ref0_j\" -eq 0 ]; then unset GIT_CONFIG_COUNT; else export GIT_CONFIG_COUNT=\"$ref0_j\"; fi",
|
|
32044
|
-
`ref0_hook=$(git rev-parse --git-path hooks/${name}) || exit`,
|
|
32045
|
-
"[ -f \"$ref0_hook\" ] && [ -x \"$ref0_hook\" ] || exit 0",
|
|
32046
|
-
"ref0_dir=$(cd -P \"${ref0_hook%/*}\" && pwd) || exit",
|
|
32047
|
-
"[ \"${ref0_dir%/*}\" = \"$ref0_hooks\" ] && exit 0",
|
|
32048
|
-
`export ${gitHookVariables.forwarded}=${name}`,
|
|
32049
|
-
"exec \"$ref0_hook\" \"$@\"",
|
|
32050
|
-
""
|
|
32051
|
-
].join("\n");
|
|
32052
|
-
/** The hooks each folder forwards. */
|
|
32053
|
-
const hooksIn = (folder) => folder === "all" ? [...forwardedHooks, ...frequentHooks] : forwardedHooks;
|
|
32054
|
-
/** The folder holding ref0's hooks folders, made if it isn't yet: its path, through any links. */
|
|
32055
|
-
const gitHooksHome = Effect.fnUntraced(function* (home, api) {
|
|
32056
|
-
const fs = yield* FileSystem.FileSystem;
|
|
32057
|
-
const given = (yield* Path.Path).join(home, ".ref0", "origins", originKey(api), "git-hooks");
|
|
32058
|
-
yield* fs.makeDirectory(given, {
|
|
32059
|
-
recursive: true,
|
|
32060
|
-
mode: 448
|
|
32061
|
-
});
|
|
32062
|
-
return yield* fs.realPath(given);
|
|
32063
|
-
});
|
|
32064
|
-
/**
|
|
32065
|
-
* One of ref0's hooks folders, in `hooks`, written first unless every forwarder in it is already
|
|
32066
|
-
* as this CLI writes it: its path, as the agent's `core.hooksPath` names it.
|
|
32067
|
-
*/
|
|
32068
|
-
const hooksFolder = Effect.fnUntraced(function* (hooks, folder) {
|
|
32069
|
-
const fs = yield* FileSystem.FileSystem;
|
|
32070
|
-
const path = yield* Path.Path;
|
|
32071
|
-
const crypto = yield* Crypto.Crypto;
|
|
32072
|
-
const written = path.join(hooks, folder);
|
|
32073
|
-
const names = hooksIn(folder);
|
|
32074
|
-
const current = Effect.forEach(names, (name) => fs.readFileString(path.join(written, name)).pipe(Effect.map((text) => text === forwarder(name, hooks)), Effect.orElseSucceed(() => false))).pipe(Effect.map((each) => each.every(Boolean)));
|
|
32075
|
-
if (yield* current) return written;
|
|
32076
|
-
yield* Effect.gen(function* () {
|
|
32077
|
-
yield* fs.makeDirectory(written, { recursive: true });
|
|
32078
|
-
yield* fs.chmod(written, 493);
|
|
32079
|
-
for (const name of names) {
|
|
32080
|
-
const next = path.join(written, `${name}.${yield* crypto.randomUUIDv4}.next`);
|
|
32081
|
-
yield* fs.writeFileString(next, forwarder(name, hooks), { mode: 365 });
|
|
32082
|
-
yield* fs.rename(next, path.join(written, name));
|
|
32083
|
-
}
|
|
32084
|
-
yield* fs.chmod(written, 365);
|
|
32085
|
-
}).pipe(Effect.catch((error) => Effect.flatMap(current, (done) => done ? Effect.void : Effect.fail(error))));
|
|
32086
|
-
return written;
|
|
32087
|
-
});
|
|
32088
|
-
//#endregion
|
|
32089
33729
|
//#region src/harness/commit-environment.ts
|
|
32090
33730
|
/**
|
|
32091
33731
|
* The agent as the author of its commits, set at a Claude Code session's start in the
|
|
32092
33732
|
* environment of the agent's own commands, and nowhere else: the file `CLAUDE_ENV_FILE` names,
|
|
32093
33733
|
* which Claude Code runs ahead of each of them. It sets Git's author to the agent, by its owner
|
|
32094
33734
|
* and handle and its commit email; one `GIT_CONFIG_*` entry naming ref0's hooks folder as
|
|
32095
|
-
* `core.hooksPath`, after any the command already has; and
|
|
32096
|
-
*
|
|
32097
|
-
*
|
|
33735
|
+
* `core.hooksPath`, after any the command already has; and, for the trailer, the harness, the
|
|
33736
|
+
* session's transcript and the model it started with, both from Claude Code's `SessionStart`.
|
|
33737
|
+
* Its other hooks carry no model, so the trailer reads the transcript's last. Git takes the
|
|
33738
|
+
* committer from the person's own config, with their signature, and a commit the person makes in
|
|
33739
|
+
* their own terminal has none of it.
|
|
32098
33740
|
*
|
|
32099
33741
|
* The agent is the one the session's config folder acts as, from that folder's host grant, which
|
|
32100
33742
|
* `ref0 connect` keeps with the agent's ID. It sets no author when the grant can't be read, or
|
|
@@ -32104,6 +33746,18 @@ const hooksFolder = Effect.fnUntraced(function* (hooks, folder) {
|
|
|
32104
33746
|
*/
|
|
32105
33747
|
/** How many commits on the default branch a session's start reads for a sign-off. */
|
|
32106
33748
|
const signedOffWithin = 20;
|
|
33749
|
+
/** A model as the trailer names it, which the hook also checks: one word, without a newline. */
|
|
33750
|
+
const modelName = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
|
|
33751
|
+
/**
|
|
33752
|
+
* The model a session started with, as the trailer names it: none for a value it can't name.
|
|
33753
|
+
* Claude Code may follow the model with its context window, as `[1m]`, which isn't the model.
|
|
33754
|
+
*/
|
|
33755
|
+
const startingModel = (model) => Option.filter(Option.fromNullishOr(model?.replace(/\[[^\]]*\]$/, "")), (name) => modelName.test(name));
|
|
33756
|
+
/** A variable the file exports, or unsets when the session has no value for it. */
|
|
33757
|
+
const exported = (name, value) => Option.match(value, {
|
|
33758
|
+
onNone: () => `unset ${name}`,
|
|
33759
|
+
onSome: (word) => `export ${name}=${shellWord(word)}`
|
|
33760
|
+
});
|
|
32107
33761
|
/**
|
|
32108
33762
|
* What a session's start adds to the file Claude Code runs ahead of the agent's commands. The
|
|
32109
33763
|
* hooks path goes after the entries a command already has, once however often the file runs.
|
|
@@ -32113,10 +33767,12 @@ const environmentScript = (set) => [
|
|
|
32113
33767
|
"",
|
|
32114
33768
|
"# ref0: the agent as the author of its commits, and ref0's Git hooks, in its commands",
|
|
32115
33769
|
...Option.match(set.author, {
|
|
32116
|
-
onNone: () => [
|
|
33770
|
+
onNone: () => [`case \${GIT_AUTHOR_EMAIL-} in *@${agentCommitDomain}) unset GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL ;; esac`],
|
|
32117
33771
|
onSome: (author) => [`export GIT_AUTHOR_NAME=${shellWord(author.name)}`, `export GIT_AUTHOR_EMAIL=${shellWord(author.email)}`]
|
|
32118
33772
|
}),
|
|
32119
33773
|
`export ${gitHookVariables.harness}=${set.harness}`,
|
|
33774
|
+
exported(gitHookVariables.transcript, set.transcript),
|
|
33775
|
+
exported(gitHookVariables.model, set.model),
|
|
32120
33776
|
"if [ -z \"${ref0_hooks_set-}\" ]; then",
|
|
32121
33777
|
" ref0_n=${GIT_CONFIG_COUNT:-0}",
|
|
32122
33778
|
` export "GIT_CONFIG_KEY_$ref0_n=core.hooksPath" "GIT_CONFIG_VALUE_$ref0_n="${shellWord(set.folder)} "GIT_CONFIG_COUNT=$((ref0_n + 1))"`,
|
|
@@ -32125,12 +33781,16 @@ const environmentScript = (set) => [
|
|
|
32125
33781
|
"fi",
|
|
32126
33782
|
""
|
|
32127
33783
|
].join("\n");
|
|
33784
|
+
/** How the agent runs a tool that installs hooks where `core.hooksPath` points. */
|
|
33785
|
+
const hookInstallerPrefix = "env -u GIT_CONFIG_COUNT";
|
|
32128
33786
|
/**
|
|
32129
33787
|
* What the agent reads at its session's start once its commands name ref0's hooks folder: an
|
|
32130
33788
|
* installer run there fails on that read-only folder, and without ref0's entry it installs into
|
|
32131
33789
|
* the repo's own.
|
|
32132
33790
|
*/
|
|
32133
|
-
const hookInstallerWords = `ref0: Git in your commands runs hooks through ref0's read-only folder, so run a hook installer, such as git lfs install, pre-commit install, lefthook install or simple-git-hooks, as
|
|
33791
|
+
const hookInstallerWords = `ref0: Git in your commands runs hooks through ref0's read-only folder, so run a hook installer, such as git lfs install, pre-commit install, lefthook install or simple-git-hooks, as ${hookInstallerPrefix} <command>, to install into the repo's own hooks folder.`;
|
|
33792
|
+
/** Codex uses an include in `GIT_CONFIG_PARAMETERS`, leaving a launcher's count alone. */
|
|
33793
|
+
const codexHookInstallerWords = hookInstallerWords.replace(hookInstallerPrefix, "env -u GIT_CONFIG_PARAMETERS");
|
|
32134
33794
|
/**
|
|
32135
33795
|
* What the session's checkout says: which of ref0's hooks folders it needs, `all` when it has
|
|
32136
33796
|
* its own frequent hook, and whether it signs off its commits. Outside a checkout, neither.
|
|
@@ -32176,29 +33836,59 @@ const checkoutAt = Effect.fnUntraced(function* (cwd) {
|
|
|
32176
33836
|
};
|
|
32177
33837
|
});
|
|
32178
33838
|
/**
|
|
32179
|
-
*
|
|
32180
|
-
*
|
|
32181
|
-
*
|
|
33839
|
+
* The harness whose `SessionStart` runs this: Claude Code, which alone gives its hook the file it
|
|
33840
|
+
* runs ahead of the agent's commands, and never its agent's commands; else Codex, when the
|
|
33841
|
+
* innermost harness process above it is Codex's, as the session's binding reads it. Codex's hook
|
|
33842
|
+
* input says nothing Claude Code's doesn't, and Codex sets no `CODEX_HOME` for the default
|
|
33843
|
+
* `~/.codex`. Neither otherwise.
|
|
33844
|
+
*/
|
|
33845
|
+
const startingHarness = Effect.gen(function* () {
|
|
33846
|
+
const file = yield* Config.String("CLAUDE_ENV_FILE").pipe(Config.withDefault(""));
|
|
33847
|
+
if (file !== "") return Option.some({
|
|
33848
|
+
harness: "claude-code",
|
|
33849
|
+
file
|
|
33850
|
+
});
|
|
33851
|
+
const found = yield* (yield* HarnessProcess).innermost;
|
|
33852
|
+
return Option.isSome(found) && found.value.harness === "codex" ? Option.some({ harness: "codex" }) : Option.none();
|
|
33853
|
+
});
|
|
33854
|
+
/**
|
|
33855
|
+
* What the agent reads when its commits couldn't be set up at its session's start, in one line,
|
|
33856
|
+
* whatever failed: the session still starts. Its commits may then name the person, skip ref0's
|
|
33857
|
+
* hooks, or meet a sign-off's refusal once.
|
|
33858
|
+
*/
|
|
33859
|
+
const commitSetupFailedWords = (error) => {
|
|
33860
|
+
return `ref0: ref0 couldn't set up Git for your commands, so your commits may not name you as their author or run ref0's hooks: ${("nextStep" in error ? error.nextStep : "detail" in error ? error.detail : error.message).replaceAll(/\s+/g, " ").trim()}`;
|
|
33861
|
+
};
|
|
33862
|
+
/**
|
|
33863
|
+
* Adds the agent's commit environment for the session a `SessionStart` opens, in Claude Code to
|
|
33864
|
+
* the file it names and in Codex to its checkout's section, only for a setup with an agent. ref0's
|
|
33865
|
+
* hooks folder is written first, so nothing names a folder that isn't there. The harness whose
|
|
33866
|
+
* hooks path it set, if it set one.
|
|
32182
33867
|
*/
|
|
32183
|
-
const setCommitEnvironment = Effect.fnUntraced(function* (
|
|
33868
|
+
const setCommitEnvironment = Effect.fnUntraced(function* (session) {
|
|
32184
33869
|
const fs = yield* FileSystem.FileSystem;
|
|
32185
33870
|
const folders = yield* ConfigFolders;
|
|
32186
|
-
const
|
|
32187
|
-
if (
|
|
33871
|
+
const starting = yield* startingHarness;
|
|
33872
|
+
if (Option.isNone(starting)) return Option.none();
|
|
33873
|
+
const hook = starting.value;
|
|
33874
|
+
if (hook.harness === "codex") return (yield* setCodexCommitEnvironment(session.cwd ?? ".", yield* checkoutAt(session.cwd ?? "."))) ? Option.some("codex") : Option.none();
|
|
32188
33875
|
const connected = yield* (yield* HostGrant).get(yield* folders.folder("claude-code")).pipe(Effect.map(Option.map(Option.some)), Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedSome(Option.none())));
|
|
32189
|
-
if (Option.isNone(connected)) return
|
|
32190
|
-
const checkout = yield* checkoutAt(cwd);
|
|
33876
|
+
if (Option.isNone(connected)) return Option.none();
|
|
33877
|
+
const checkout = yield* checkoutAt(session.cwd ?? ".");
|
|
32191
33878
|
const folder = yield* hooksFolder(yield* gitHooksHome(yield* Config.String("HOME"), (yield* Ref0Config).api), checkout.folder);
|
|
32192
33879
|
const author = checkout.signsOff ? Option.none() : Option.map(connected.value, (grant) => ({
|
|
32193
33880
|
name: agentCommitName(grant.owner, grant.agent),
|
|
32194
33881
|
email: agentCommitEmail(grant.agentId)
|
|
32195
33882
|
}));
|
|
32196
|
-
|
|
33883
|
+
const script = environmentScript({
|
|
32197
33884
|
author,
|
|
32198
33885
|
folder,
|
|
32199
|
-
harness: "claude-code"
|
|
32200
|
-
|
|
32201
|
-
|
|
33886
|
+
harness: "claude-code",
|
|
33887
|
+
transcript: Option.filter(Option.fromNullishOr(session.transcript_path), (path) => path !== ""),
|
|
33888
|
+
model: startingModel(session.model)
|
|
33889
|
+
});
|
|
33890
|
+
yield* fs.writeFileString(hook.file, script, { flag: "a" });
|
|
33891
|
+
return Option.some("claude-code");
|
|
32202
33892
|
});
|
|
32203
33893
|
//#endregion
|
|
32204
33894
|
//#region src/harness/session-start.ts
|
|
@@ -32250,7 +33940,8 @@ const startOnHook = (protocol) => Effect.gen(function* () {
|
|
|
32250
33940
|
const input = yield* stdio.stdin.pipe(Stream.decodeText(), Stream.mkString);
|
|
32251
33941
|
const hook = decodeHook(input);
|
|
32252
33942
|
if (Option.isNone(hook)) return;
|
|
32253
|
-
|
|
33943
|
+
const hooksSet = yield* setCommitEnvironment(hook.value).pipe(Effect.catch((error) => Console.log(commitSetupFailedWords(error)).pipe(Effect.as(Option.none()))));
|
|
33944
|
+
if (Option.isSome(hooksSet)) yield* Console.log(hooksSet.value === "codex" ? codexHookInstallerWords : hookInstallerWords);
|
|
32254
33945
|
const binding = yield* bindingHere(hook.value.session_id);
|
|
32255
33946
|
if (Option.isNone(binding)) return;
|
|
32256
33947
|
const harnessName = binding.value.harness === "codex" ? "codex" : "claude-code";
|
|
@@ -32304,6 +33995,203 @@ const sessionStart = Command.make(harnessCommandNames.sessionStart, { protocol:
|
|
|
32304
33995
|
/** The commands a harness runs: `ref0 mcp`, and its command hooks. */
|
|
32305
33996
|
const harnessCommands = [mcp, Command.make(harnessCommandNames.hook).pipe(Command.withDescription("The harness's hooks that run as commands"), Command.withShortDescription("Run the harness’s session hooks"), Command.withSubcommands([sessionStart, sessionEnd]))];
|
|
32306
33997
|
//#endregion
|
|
33998
|
+
//#region src/harness/command-token.ts
|
|
33999
|
+
/**
|
|
34000
|
+
* The session an agent's command acts as: its harness session's, by the token kept under its
|
|
34001
|
+
* binding. Outside a harness there's none, and a command is its person's, asked by approval; so
|
|
34002
|
+
* too in a harness whose config folder has no agent connected. Where one is connected, a harness
|
|
34003
|
+
* session with no token has lost its start, and the command says why, as far as ref0 can tell,
|
|
34004
|
+
* and what to do, rather than run as its person.
|
|
34005
|
+
*/
|
|
34006
|
+
/**
|
|
34007
|
+
* Whether ref0's server, `ref0 mcp`, runs right under a harness process, as the harness starts
|
|
34008
|
+
* it: none when the process table can't be read.
|
|
34009
|
+
*/
|
|
34010
|
+
var HarnessServers = class HarnessServers extends Context.Service()("ref0/cli/HarnessServers") {
|
|
34011
|
+
static layer = Layer.effect(HarnessServers, Effect.gen(function* () {
|
|
34012
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
34013
|
+
const runsUnder = (process) => spawner.string(ChildProcess.make("ps", [
|
|
34014
|
+
"-A",
|
|
34015
|
+
"-o",
|
|
34016
|
+
"ppid=,args="
|
|
34017
|
+
])).pipe(Effect.map((table) => table.trim() === "" ? Option.none() : Option.some(table.split("\n").some((line) => {
|
|
34018
|
+
const [parent = "", ...words] = line.trim().split(/\s+/);
|
|
34019
|
+
return Number(parent) === process && words.at(-1) === harnessCommandNames.mcp && words.some((word) => word.includes("ref0"));
|
|
34020
|
+
}))), Effect.orElseSucceed(() => Option.none()));
|
|
34021
|
+
return HarnessServers.of({ runsUnder });
|
|
34022
|
+
}));
|
|
34023
|
+
};
|
|
34024
|
+
/**
|
|
34025
|
+
* What an agent's command in a harness session with no ref0 session says to do: run again, while
|
|
34026
|
+
* the prompt's start is on its way; reconnect or restart, when the server isn't running; else
|
|
34027
|
+
* send the prompt again, which starts one.
|
|
34028
|
+
*/
|
|
34029
|
+
const noSessionStep = (harness, server, seen) => {
|
|
34030
|
+
const name = configFolders[harness].name;
|
|
34031
|
+
const restart = harness === "codex" ? "restart Codex" : `run /mcp, choose ${server} and Reconnect, or restart Claude Code`;
|
|
34032
|
+
if (Option.contains(seen.serverRuns, false)) return `This ${name} session has no ref0 session: ref0’s server isn’t running in it, so none started at the last prompt. Tell your person to ${restart}, then send the prompt again.`;
|
|
34033
|
+
if (seen.pending) return `This ${name} session has no ref0 session yet: its start at the last prompt hasn’t landed. Run this again in a few seconds. If it says this again, tell your person to send the prompt again.`;
|
|
34034
|
+
return `This ${name} session has no ref0 session: none started at the last prompt. Tell your person to send the prompt again. If it says this again, tell them to ${restart}.`;
|
|
34035
|
+
};
|
|
34036
|
+
/** `SessionToken` from the keychain, by the binding of the harness session this runs in. */
|
|
34037
|
+
const harnessSessionToken = Layer.effect(SessionToken, Effect.gen(function* () {
|
|
34038
|
+
const keychain = yield* Keychain;
|
|
34039
|
+
const harness = yield* HarnessProcess;
|
|
34040
|
+
const servers = yield* HarnessServers;
|
|
34041
|
+
const hostGrant = yield* HostGrant;
|
|
34042
|
+
const folders = yield* ConfigFolders;
|
|
34043
|
+
const config = yield* Ref0Config;
|
|
34044
|
+
const plugin = yield* originPluginName(config.api);
|
|
34045
|
+
const current = Effect.gen(function* () {
|
|
34046
|
+
const binding = yield* bindingHere().pipe(Effect.provideService(HarnessProcess, harness), Effect.orElseSucceed(() => Option.none()));
|
|
34047
|
+
if (Option.isNone(binding)) return Option.none();
|
|
34048
|
+
const kept = yield* keychain.get(sessionAccount(binding.value));
|
|
34049
|
+
if (Option.isSome(kept)) return kept;
|
|
34050
|
+
const name = binding.value.harness === "codex" ? "codex" : "claude-code";
|
|
34051
|
+
const grant = yield* hostGrant.get(yield* folders.folder(name)).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
|
|
34052
|
+
if (Option.isNone(grant) || grant.value.harness !== name) return Option.none();
|
|
34053
|
+
const pending = yield* keychain.get(pendingAccount(binding.value));
|
|
34054
|
+
const serverRuns = yield* servers.runsUnder(binding.value.process);
|
|
34055
|
+
return yield* new NoSession({ nextStep: noSessionStep(name, `plugin:${plugin}:${plugin}`, {
|
|
34056
|
+
serverRuns,
|
|
34057
|
+
pending: Option.isSome(pending)
|
|
34058
|
+
}) });
|
|
34059
|
+
});
|
|
34060
|
+
return SessionToken.of({ current });
|
|
34061
|
+
}));
|
|
34062
|
+
//#endregion
|
|
34063
|
+
//#region src/harness/server-check.ts
|
|
34064
|
+
/**
|
|
34065
|
+
* Whether a config folder's ref0 server starts, asked as its harness asks: the command its
|
|
34066
|
+
* installed plugin's `.mcp.json` names, started from an empty folder of ref0's own with the
|
|
34067
|
+
* environment that harness would give it from this terminal's, and sent `initialize`. Told by
|
|
34068
|
+
* `serverCheckVariable` which harness it's started as, the server reads that folder's grant from
|
|
34069
|
+
* the keychain as it does at a session's start, and starts no session. Its whole process group is
|
|
34070
|
+
* stopped once it answers, stops, or the limit passes, so nothing it started outlasts the check.
|
|
34071
|
+
*/
|
|
34072
|
+
/**
|
|
34073
|
+
* The variables Codex 0.164.0 passes a stdio server of its own, beside those its entry's
|
|
34074
|
+
* `env_vars` names that are set. Nothing else reaches the server (measured on Linux, 24 Oct 2026).
|
|
34075
|
+
*/
|
|
34076
|
+
const codexPasses = [
|
|
34077
|
+
"HOME",
|
|
34078
|
+
"LOGNAME",
|
|
34079
|
+
"PATH",
|
|
34080
|
+
"SHELL",
|
|
34081
|
+
"USER",
|
|
34082
|
+
"LANG",
|
|
34083
|
+
"LC_ALL",
|
|
34084
|
+
"TERM",
|
|
34085
|
+
"TMPDIR",
|
|
34086
|
+
"TZ",
|
|
34087
|
+
"__CF_USER_TEXT_ENCODING",
|
|
34088
|
+
"SSL_CERT_FILE",
|
|
34089
|
+
"NODE_EXTRA_CA_CERTS"
|
|
34090
|
+
];
|
|
34091
|
+
/**
|
|
34092
|
+
* How long a server has to answer `initialize`: ten times its slowest start measured on Linux,
|
|
34093
|
+
* 0.5 s (24 Oct 2026), so status stays quick. A harness may wait longer (Codex 0.164.0 waited 14 s
|
|
34094
|
+
* for a slow one), so a server that doesn't answer in it is unknown, not one that can't start.
|
|
34095
|
+
*/
|
|
34096
|
+
const serverLimit = Duration.seconds(5);
|
|
34097
|
+
const checkId = "ref0-status";
|
|
34098
|
+
/**
|
|
34099
|
+
* `initialize` and its answer, as JSON-RPC carries them, in MCP's own schemas: those `ref0 mcp`
|
|
34100
|
+
* serves through Effect's MCP server.
|
|
34101
|
+
*/
|
|
34102
|
+
const InitializeRequest = Schema.Struct({
|
|
34103
|
+
jsonrpc: Schema.Literal("2.0"),
|
|
34104
|
+
id: Schema.Literal(checkId),
|
|
34105
|
+
method: Schema.Literal(McpSchema.Initialize._tag),
|
|
34106
|
+
params: McpSchema.Initialize.payloadSchema
|
|
34107
|
+
});
|
|
34108
|
+
const InitializeAnswer = Schema.Struct({
|
|
34109
|
+
id: Schema.Literal(checkId),
|
|
34110
|
+
result: McpSchema.InitializeResult
|
|
34111
|
+
});
|
|
34112
|
+
/** `initialize`, at a version every ref0 server speaks, as a harness's first line. */
|
|
34113
|
+
const initialize = `${Schema.encodeSync(Schema.fromJsonString(InitializeRequest))({
|
|
34114
|
+
jsonrpc: "2.0",
|
|
34115
|
+
id: checkId,
|
|
34116
|
+
method: McpSchema.Initialize._tag,
|
|
34117
|
+
params: {
|
|
34118
|
+
protocolVersion: "2025-06-18",
|
|
34119
|
+
capabilities: new McpSchema.ClientCapabilities({}),
|
|
34120
|
+
clientInfo: {
|
|
34121
|
+
name: "ref0 status",
|
|
34122
|
+
version: cliVersion
|
|
34123
|
+
}
|
|
34124
|
+
}
|
|
34125
|
+
})}\n`;
|
|
34126
|
+
const decodeServers = Schema.decodeUnknownEffect(Schema.fromJsonString(McpServersFile));
|
|
34127
|
+
const decodeAnswer = Schema.decodeUnknownOption(Schema.fromJsonString(InitializeAnswer));
|
|
34128
|
+
/**
|
|
34129
|
+
* `names` as this terminal sets them, which a harness started here would pass on. A relative
|
|
34130
|
+
* `PATH` folder is read from here, as `onPath` reads it, since the server starts elsewhere.
|
|
34131
|
+
*/
|
|
34132
|
+
const fromHere = Effect.fnUntraced(function* (names) {
|
|
34133
|
+
const paths = yield* Path.Path;
|
|
34134
|
+
const set = [];
|
|
34135
|
+
for (const name of new Set(names)) {
|
|
34136
|
+
const value = yield* Config.String(name).pipe(Config.option);
|
|
34137
|
+
if (Option.isNone(value)) continue;
|
|
34138
|
+
set.push([name, name === "PATH" ? value.value.split(":").map((folder) => folder === "" || paths.isAbsolute(folder) ? folder : `${process.cwd()}/${folder}`).join(":") : value.value]);
|
|
34139
|
+
}
|
|
34140
|
+
return Object.fromEntries(set);
|
|
34141
|
+
});
|
|
34142
|
+
/** The first line a stopped server said, made printable. */
|
|
34143
|
+
const firstLine = (said) => printable(said.split("\n").map((line) => line.trim()).find((line) => line !== "") ?? "");
|
|
34144
|
+
/**
|
|
34145
|
+
* Starts the server named `name` in the plugin installed at `plugin`, as `harness` would for
|
|
34146
|
+
* `configFolder`, and answers how its start went.
|
|
34147
|
+
*/
|
|
34148
|
+
const startServer = Effect.fnUntraced(function* (harness, configFolder, plugin, name) {
|
|
34149
|
+
const fs = yield* FileSystem.FileSystem;
|
|
34150
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
34151
|
+
const server = yield* fs.readFileString(`${plugin}/.mcp.json`).pipe(Effect.flatMap(decodeServers), Effect.map((file) => Option.fromNullishOr(file.mcpServers[name])), Effect.orElseSucceed(Option.none));
|
|
34152
|
+
if (Option.isNone(server)) return { kind: "unnamed" };
|
|
34153
|
+
const asked = server.value.env_vars ?? [];
|
|
34154
|
+
const checked = {
|
|
34155
|
+
REF0_API: (yield* Ref0Config).api,
|
|
34156
|
+
[serverCheckVariable]: harness
|
|
34157
|
+
};
|
|
34158
|
+
const env = harness === "codex" ? {
|
|
34159
|
+
...yield* fromHere([...codexPasses, ...asked]),
|
|
34160
|
+
CODEX_HOME: configFolder,
|
|
34161
|
+
...checked
|
|
34162
|
+
} : {
|
|
34163
|
+
...yield* fromHere([
|
|
34164
|
+
...codexPasses,
|
|
34165
|
+
...keychainVariables,
|
|
34166
|
+
...asked
|
|
34167
|
+
]),
|
|
34168
|
+
CLAUDE_CONFIG_DIR: configFolder,
|
|
34169
|
+
...checked
|
|
34170
|
+
};
|
|
34171
|
+
return yield* Effect.scoped(Effect.gen(function* () {
|
|
34172
|
+
const handle = yield* spawner.spawn(ChildProcess.make(server.value.command, server.value.args, {
|
|
34173
|
+
cwd: yield* emptyFolder(fs),
|
|
34174
|
+
env,
|
|
34175
|
+
extendEnv: harness === "claude-code",
|
|
34176
|
+
forceKillAfter: "1 second",
|
|
34177
|
+
stdin: {
|
|
34178
|
+
stream: Stream.make(initialize).pipe(Stream.encodeText),
|
|
34179
|
+
endOnDone: false
|
|
34180
|
+
}
|
|
34181
|
+
}));
|
|
34182
|
+
const said = yield* handle.stderr.pipe(Stream.decodeText(), Stream.mkString, Effect.forkScoped);
|
|
34183
|
+
const lines = yield* handle.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.takeUntil((line) => Option.isSome(decodeAnswer(line))), Stream.runCollect);
|
|
34184
|
+
if (lines.some((line) => Option.isSome(decodeAnswer(line)))) return { kind: "answered" };
|
|
34185
|
+
return {
|
|
34186
|
+
kind: "stopped",
|
|
34187
|
+
said: firstLine(yield* Fiber.join(said)) || firstLine(lines.join("\n"))
|
|
34188
|
+
};
|
|
34189
|
+
})).pipe(Effect.catchTag("PlatformError", (error) => Effect.succeed({
|
|
34190
|
+
kind: "stopped",
|
|
34191
|
+
said: firstLine(error.message)
|
|
34192
|
+
})), Effect.timeoutOption(serverLimit), Effect.map(Option.getOrElse(() => ({ kind: "unanswered" }))));
|
|
34193
|
+
});
|
|
34194
|
+
//#endregion
|
|
32307
34195
|
//#region src/harness/status.ts
|
|
32308
34196
|
/** Each connected or initialized folder has its own report; secrets are used only to ask ref0. */
|
|
32309
34197
|
const encodeReports = Schema.encodeEffect(Schema.Array(StatusReport));
|
|
@@ -32359,6 +34247,15 @@ const speaksCheck = (protocols, version, reinstall) => {
|
|
|
32359
34247
|
if (Option.isSome(mismatch)) return checkOf("plugin", "no", mismatch.value.says, mismatch.value.run);
|
|
32360
34248
|
return checkOf("plugin", "yes", "installed and speaks this ref0");
|
|
32361
34249
|
};
|
|
34250
|
+
/** A server's start, as its line says it, with this folder's reinstall for one that can't start. */
|
|
34251
|
+
const serverCheckOf = (started, configFolder) => {
|
|
34252
|
+
switch (started.kind) {
|
|
34253
|
+
case "answered": return checkOf("server", "yes", "starts");
|
|
34254
|
+
case "stopped": return checkOf("server", "no", started.said === "" ? "can’t start, and said nothing" : `can’t start: ${started.said}`, installLine(configFolder));
|
|
34255
|
+
case "unnamed": return checkOf("server", "no", "the plugin names no ref0 server", installLine(configFolder));
|
|
34256
|
+
case "unanswered": return checkOf("server", "unknown", `didn’t answer in ${Duration.toSeconds(serverLimit)} s`, "ref0 status");
|
|
34257
|
+
}
|
|
34258
|
+
};
|
|
32362
34259
|
const decodePlugins = Schema.decodeUnknownEffect(Schema.fromJsonString(ClaudePluginsListed));
|
|
32363
34260
|
const decodeHooks = Schema.decodeUnknownEffect(Schema.fromJsonString(HooksFile));
|
|
32364
34261
|
/** Ask Claude Code about this folder, even when another folder is current in the terminal. */
|
|
@@ -32367,30 +34264,32 @@ const claudeCodeChecks = Effect.fnUntraced(function* (configFolder) {
|
|
|
32367
34264
|
const claude = yield* onPath("claude");
|
|
32368
34265
|
if (Option.isNone(claude)) return [checkOf("plugin", "unknown", "Claude Code isn’t on PATH", reinstall)];
|
|
32369
34266
|
const home = yield* Config.String("HOME").pipe(Config.withDefault("."));
|
|
32370
|
-
const
|
|
34267
|
+
const fs = yield* FileSystem.FileSystem;
|
|
34268
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
34269
|
+
const listed = yield* Effect.scoped(Effect.flatMap(emptyFolder(fs), (cwd) => spawner.string(ChildProcess.make(claude.value, [
|
|
32371
34270
|
"plugin",
|
|
32372
34271
|
"list",
|
|
32373
34272
|
"--json"
|
|
32374
34273
|
], {
|
|
34274
|
+
cwd,
|
|
32375
34275
|
env: {
|
|
32376
34276
|
HOME: home,
|
|
32377
34277
|
CLAUDE_CONFIG_DIR: configFolder
|
|
32378
34278
|
},
|
|
32379
34279
|
extendEnv: true
|
|
32380
|
-
})).pipe(Effect.flatMap(decodePlugins), Effect.timeout("15 seconds"), Effect.option);
|
|
34280
|
+
})))).pipe(Effect.flatMap(decodePlugins), Effect.timeout("15 seconds"), Effect.option);
|
|
32381
34281
|
if (Option.isNone(listed)) return [checkOf("plugin", "unknown", "Claude Code didn’t list its plugins", reinstall)];
|
|
32382
34282
|
const name = yield* originPluginName((yield* Ref0Config).api);
|
|
32383
34283
|
const id = `${name}@${name}`;
|
|
32384
34284
|
const plugin = listed.value.find((each) => each.id === id);
|
|
32385
34285
|
if (plugin === void 0) return [checkOf("plugin", "no", "not installed in this folder", reinstall)];
|
|
32386
34286
|
if (!plugin.enabled) return [checkOf("plugin", "no", "turned off in this folder", `CLAUDE_CONFIG_DIR=${shellWord(configFolder)} claude plugin enable ${id}`)];
|
|
32387
|
-
const fs = yield* FileSystem.FileSystem;
|
|
32388
34287
|
const folder = plugin.readFromFolder ?? plugin.installPath;
|
|
32389
34288
|
const hooks = folder === void 0 ? Option.none() : yield* fs.readFileString(`${folder}/hooks/hooks.json`).pipe(Effect.flatMap(decodeHooks), Effect.option);
|
|
32390
34289
|
return [speaksCheck(Option.match(hooks, {
|
|
32391
34290
|
onNone: () => [],
|
|
32392
34291
|
onSome: protocolsOf
|
|
32393
|
-
}), Option.fromUndefinedOr(plugin.version), reinstall)];
|
|
34292
|
+
}), Option.fromUndefinedOr(plugin.version), reinstall), folder === void 0 ? checkOf("server", "unknown", "Claude Code didn’t say where ref0’s plugin is", reinstall) : serverCheckOf(yield* startServer("claude-code", configFolder, folder, name), configFolder)];
|
|
32394
34293
|
});
|
|
32395
34294
|
/** Trust only ref0's hooks; removal of an unexpected plugin targets this folder too. */
|
|
32396
34295
|
const trustCheck = (hooks, configFolder, name) => {
|
|
@@ -32404,14 +34303,19 @@ const codexChecks = Effect.fnUntraced(function* (configFolder) {
|
|
|
32404
34303
|
const reinstall = installLine(configFolder);
|
|
32405
34304
|
const codex = yield* onPath("codex");
|
|
32406
34305
|
if (Option.isNone(codex)) return [checkOf("plugin", "unknown", "Codex isn’t on PATH", reinstall)];
|
|
32407
|
-
const
|
|
32408
|
-
const listed = yield* askCodex(codex.value, codexMethods.hooksList, { cwds: [home] }, configFolder).pipe(Effect.option);
|
|
34306
|
+
const listed = yield* askCodex(codex.value, codexMethods.hooksList, { cwds: [] }, { CODEX_HOME: configFolder }).pipe(Effect.option);
|
|
32409
34307
|
if (Option.isNone(listed)) return [checkOf("plugin", "unknown", "Codex didn’t list its hooks", reinstall)];
|
|
32410
34308
|
const name = yield* originPluginName((yield* Ref0Config).api);
|
|
32411
34309
|
const hooks = listed.value.data.flatMap((entry) => entry.hooks).filter((hook) => pluginHook(hook, `${name}@${name}`));
|
|
32412
34310
|
if (hooks.length === 0) return [checkOf("plugin", "no", "not installed or turned off in this folder", reinstall)];
|
|
32413
34311
|
const protocols = hooks.map((hook) => protocolFlagged(hook.command ?? "")).filter(Predicate.isString);
|
|
32414
|
-
|
|
34312
|
+
const path = yield* Path.Path;
|
|
34313
|
+
const source = hooks.find((hook) => Predicate.isString(hook.sourcePath))?.sourcePath;
|
|
34314
|
+
return [
|
|
34315
|
+
speaksCheck(protocols, Option.none(), reinstall),
|
|
34316
|
+
trustCheck(hooks, configFolder, name),
|
|
34317
|
+
Predicate.isString(source) ? serverCheckOf(yield* startServer("codex", configFolder, path.dirname(path.dirname(source)), name), configFolder) : checkOf("server", "unknown", "Codex didn’t say where ref0’s plugin is", reinstall)
|
|
34318
|
+
];
|
|
32415
34319
|
});
|
|
32416
34320
|
/** A session belongs only to the current folder, even when several folders share its agent. */
|
|
32417
34321
|
const sessionCheck = Effect.fnUntraced(function* (harness, configFolder) {
|
|
@@ -32440,8 +34344,7 @@ const statusCommands = [Command.make(harnessCommandNames.status, { json }, ({ js
|
|
|
32440
34344
|
}
|
|
32441
34345
|
for (const { configFolder, harness } of found) candidates.set(configFolder, harness);
|
|
32442
34346
|
const paths = [...candidates.keys()].sort((a, b) => a === current ? -1 : b === current ? 1 : 0);
|
|
32443
|
-
const
|
|
32444
|
-
for (const configFolder of paths) {
|
|
34347
|
+
const reportOf = Effect.fnUntraced(function* (configFolder) {
|
|
32445
34348
|
const kept = yield* Effect.result(grants.get(configFolder));
|
|
32446
34349
|
const unreadable = Result.isFailure(kept) && kept.failure.unreadable === true;
|
|
32447
34350
|
const grant = Result.isSuccess(kept) ? kept.success : Option.none();
|
|
@@ -32451,15 +34354,19 @@ const statusCommands = [Command.make(harnessCommandNames.status, { json }, ({ js
|
|
|
32451
34354
|
configFolder,
|
|
32452
34355
|
checks: [Result.isFailure(kept) && !unreadable ? checkOf("connected", "unknown", "The keychain didn’t say whether this config folder holds a host grant.", kept.failure.nextStep) : checkOf("connected", "no", unreadable ? "its host grant can’t be read" : "not connected", harness === void 0 ? "ref0 connect" : yield* connectLineFor(harness, configFolder))]
|
|
32453
34356
|
};
|
|
32454
|
-
|
|
34357
|
+
return harness === void 0 ? report : {
|
|
32455
34358
|
...report,
|
|
32456
34359
|
harness
|
|
32457
|
-
}
|
|
32458
|
-
|
|
34360
|
+
};
|
|
34361
|
+
}
|
|
34362
|
+
const [granted, installed] = yield* Effect.all([grantChecks(grant.value, configFolder), grant.value.harness === "codex" ? codexChecks(configFolder) : claudeCodeChecks(configFolder)], { concurrency: "unbounded" });
|
|
34363
|
+
const checks = [...granted, ...installed];
|
|
34364
|
+
if (configFolder === current) {
|
|
34365
|
+
const session = yield* sessionCheck(grant.value.harness, configFolder);
|
|
34366
|
+
const server = checks.find((check) => check.check === "server");
|
|
34367
|
+
checks.push(server !== void 0 && server.answer !== "yes" && session.answer === "no" ? checkOf("session", "no", `none, since ref0’s server ${server.answer === "no" ? "can’t start" : "didn’t answer"}`, server.next) : session);
|
|
32459
34368
|
}
|
|
32460
|
-
|
|
32461
|
-
if (configFolder === current) checks.push(yield* sessionCheck(grant.value.harness, configFolder));
|
|
32462
|
-
reports.push({
|
|
34369
|
+
return {
|
|
32463
34370
|
harness: grant.value.harness,
|
|
32464
34371
|
configFolder,
|
|
32465
34372
|
agent: {
|
|
@@ -32467,8 +34374,9 @@ const statusCommands = [Command.make(harnessCommandNames.status, { json }, ({ js
|
|
|
32467
34374
|
agent: grant.value.agent
|
|
32468
34375
|
},
|
|
32469
34376
|
checks
|
|
32470
|
-
}
|
|
32471
|
-
}
|
|
34377
|
+
};
|
|
34378
|
+
});
|
|
34379
|
+
const reports = yield* Effect.forEach(paths, reportOf, { concurrency: "unbounded" });
|
|
32472
34380
|
const home = yield* Config.String("HOME").pipe(Config.withDefault("."));
|
|
32473
34381
|
yield* printAnswer(yield* Effect.orDie(encodeReports(reports)), asJson, reports.length === 0 ? [["No initialized harness folders found.", { command: "ref0 connect" }]] : reports.map((report) => wordsOf(report, home)));
|
|
32474
34382
|
if (reports.some((report) => report.checks.some((check) => check.answer === "no"))) return yield* new Said();
|
|
@@ -32513,7 +34421,12 @@ const ref0 = Command.make("ref0").pipe(Command.withDescription("ref0 from the te
|
|
|
32513
34421
|
]));
|
|
32514
34422
|
const keychain = Keychain.layer.pipe(Layer.provide([NodeServices.layer, Ref0Config.layer]));
|
|
32515
34423
|
const harnessProcess = HarnessProcess.layer.pipe(Layer.provide(NodeServices.layer));
|
|
32516
|
-
const sessionToken = harnessSessionToken.pipe(Layer.provide([
|
|
34424
|
+
const sessionToken = harnessSessionToken.pipe(Layer.provide([
|
|
34425
|
+
keychain,
|
|
34426
|
+
harnessProcess,
|
|
34427
|
+
HarnessServers.layer.pipe(Layer.provide(NodeServices.layer)),
|
|
34428
|
+
HostGrant.layer.pipe(Layer.provide(keychain))
|
|
34429
|
+
]));
|
|
32517
34430
|
/** Every service `ref0`'s commands use, on this machine. */
|
|
32518
34431
|
const services = Layer.mergeAll(cliOutput, Ref0.layer.pipe(Layer.provide([
|
|
32519
34432
|
Ref0Config.layer,
|