arcane-cli 1.17.2 → 1.18.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/README.md +71 -0
- package/dist/assets/.arcane/governance/threat-model.md +43 -0
- package/dist/assets/.arcane/spells/_fragments/presence-report.md +16 -0
- package/dist/assets/.arcane/spells/spell-close-session.md +19 -0
- package/dist/assets/.arcane/spells/spell-commit-work.md +19 -0
- package/dist/assets/.arcane/spells/spell-implement.md +19 -0
- package/dist/assets/.arcane/spells/spell-open-session.md +19 -0
- package/dist/assets/schemas/presence.schema.json +586 -0
- package/dist/index.js +1699 -281
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,9 +4,13 @@ const require = __createRequire(import.meta.url);
|
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
-
import { createRequire as createRequire3 } from "module";
|
|
8
7
|
import { fileURLToPath } from "url";
|
|
9
|
-
import { dirname as
|
|
8
|
+
import { dirname as dirname15, join as join31 } from "path";
|
|
9
|
+
|
|
10
|
+
// src/package-info.ts
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
var require2 = createRequire(import.meta.url);
|
|
13
|
+
var packageJson = require2("../package.json");
|
|
10
14
|
|
|
11
15
|
// src/commands/init.ts
|
|
12
16
|
import { select as select3, confirm as confirm2, input as input3 } from "@inquirer/prompts";
|
|
@@ -148,21 +152,21 @@ function runGit(cwd, args, options = {}) {
|
|
|
148
152
|
windowsHide: true,
|
|
149
153
|
encoding: "utf8"
|
|
150
154
|
},
|
|
151
|
-
(
|
|
152
|
-
if (
|
|
153
|
-
if (
|
|
155
|
+
(error2, stdout, stderr) => {
|
|
156
|
+
if (error2) {
|
|
157
|
+
if (error2.killed) {
|
|
154
158
|
reject(
|
|
155
159
|
new GitTimeoutError(
|
|
156
160
|
// NOTE: embeds the raw args, including any secrets a future
|
|
157
161
|
// network-class caller might pass (e.g. a credentialed
|
|
158
162
|
// clone URL). No current caller does; redact before one does.
|
|
159
163
|
`git ${args.join(" ")} timed out after ${timeoutMs}ms (class: ${commandClass})`,
|
|
160
|
-
{ cause:
|
|
164
|
+
{ cause: error2 }
|
|
161
165
|
)
|
|
162
166
|
);
|
|
163
167
|
return;
|
|
164
168
|
}
|
|
165
|
-
reject(
|
|
169
|
+
reject(error2);
|
|
166
170
|
return;
|
|
167
171
|
}
|
|
168
172
|
resolve10({ stdout, stderr });
|
|
@@ -223,8 +227,8 @@ async function correctUnbornMasterDefault(cwd) {
|
|
|
223
227
|
try {
|
|
224
228
|
await runGit(cwd, ["symbolic-ref", "-q", "refs/heads/main"]);
|
|
225
229
|
return { corrected: false, to: "main", blockedReason: "target-unreadable" };
|
|
226
|
-
} catch (
|
|
227
|
-
if (
|
|
230
|
+
} catch (error2) {
|
|
231
|
+
if (error2.code !== 1) {
|
|
228
232
|
return { corrected: false, to: "main", blockedReason: "target-unreadable" };
|
|
229
233
|
}
|
|
230
234
|
}
|
|
@@ -659,8 +663,8 @@ function isValidTodoDoneBook(value) {
|
|
|
659
663
|
if (value !== value.trim() || value.includes(":")) return false;
|
|
660
664
|
const normalized = path2.posix.normalize(value.replace(/\\/g, "/"));
|
|
661
665
|
const segments = normalized.split("/");
|
|
662
|
-
const
|
|
663
|
-
return /^[^/]+\.md$/i.test(
|
|
666
|
+
const basename6 = segments[segments.length - 1] ?? "";
|
|
667
|
+
return /^[^/]+\.md$/i.test(basename6) && !/^\.md$/i.test(basename6) && !segments.slice(0, -1).some((segment) => segment.startsWith(".") && segment !== ".") && !TODO_DONE_BOOK_RESERVED.has(basename6.toLowerCase());
|
|
664
668
|
}
|
|
665
669
|
function manifestPath(targetDir) {
|
|
666
670
|
return path2.join(targetDir, MANIFEST_FILE);
|
|
@@ -1483,13 +1487,13 @@ function isClientShimPath(path5) {
|
|
|
1483
1487
|
}
|
|
1484
1488
|
var FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
1485
1489
|
function parsePromptFrontmatter(content) {
|
|
1486
|
-
const
|
|
1487
|
-
if (!
|
|
1490
|
+
const match2 = content.match(FRONTMATTER_PATTERN);
|
|
1491
|
+
if (!match2) {
|
|
1488
1492
|
throw new MissingFrontmatterError(
|
|
1489
1493
|
"Prompt file has no YAML frontmatter block (expected a leading --- ... --- block)."
|
|
1490
1494
|
);
|
|
1491
1495
|
}
|
|
1492
|
-
const raw = parseYaml(
|
|
1496
|
+
const raw = parseYaml(match2[1]);
|
|
1493
1497
|
const name = raw?.["name"];
|
|
1494
1498
|
const description = raw?.["description"];
|
|
1495
1499
|
if (typeof name !== "string" || name.trim() === "") {
|
|
@@ -1902,7 +1906,7 @@ function scale(hex, f) {
|
|
|
1902
1906
|
}
|
|
1903
1907
|
function lockupLines(f = 1) {
|
|
1904
1908
|
const g = f >= 1 ? arcaneGradient : gradient(STOPS.map((c) => scale(c, f)));
|
|
1905
|
-
return WORDMARK.map((
|
|
1909
|
+
return WORDMARK.map((line2) => INDENT + g(line2));
|
|
1906
1910
|
}
|
|
1907
1911
|
function printFooter(version) {
|
|
1908
1912
|
const taglineText = ` ${TAGLINE} \xB7 agent-native SDLC \xB7 v${version}`;
|
|
@@ -1937,7 +1941,7 @@ async function printWelcome(version, cwd) {
|
|
|
1937
1941
|
if (isAnimatable()) {
|
|
1938
1942
|
const draw = (lines, redraw) => {
|
|
1939
1943
|
if (redraw) process.stdout.write(`\x1B[${lines.length}A`);
|
|
1940
|
-
for (const
|
|
1944
|
+
for (const line2 of lines) process.stdout.write(`\r\x1B[2K${line2}
|
|
1941
1945
|
`);
|
|
1942
1946
|
};
|
|
1943
1947
|
const fade = [0.1, 0.22, 0.36, 0.52, 0.68, 0.84, 1];
|
|
@@ -1951,7 +1955,7 @@ async function printWelcome(version, cwd) {
|
|
|
1951
1955
|
await sleep(140);
|
|
1952
1956
|
}
|
|
1953
1957
|
} else {
|
|
1954
|
-
for (const
|
|
1958
|
+
for (const line2 of lockupLines()) console.log(line2);
|
|
1955
1959
|
}
|
|
1956
1960
|
printFooter(version);
|
|
1957
1961
|
console.log();
|
|
@@ -3000,8 +3004,8 @@ async function performAgentSync(targetDir, assetsDir, roster, options) {
|
|
|
3000
3004
|
dryRun: options.dryRun,
|
|
3001
3005
|
fallbackDir: assetsDir
|
|
3002
3006
|
});
|
|
3003
|
-
for (const
|
|
3004
|
-
console.log(` ${
|
|
3007
|
+
for (const line2 of describeFanoutOutcomes(fanout.outcomes, options.dryRun)) {
|
|
3008
|
+
console.log(` ${line2}`);
|
|
3005
3009
|
}
|
|
3006
3010
|
if (!options.dryRun) {
|
|
3007
3011
|
await writeManifest(targetDir, { ...manifest, fanout: fanout.record }, { followLinks: true });
|
|
@@ -3312,7 +3316,7 @@ async function detectAdoContext(targetDir) {
|
|
|
3312
3316
|
} catch {
|
|
3313
3317
|
return false;
|
|
3314
3318
|
}
|
|
3315
|
-
return stdout.split("\n").map((
|
|
3319
|
+
return stdout.split("\n").map((line2) => line2.slice(line2.indexOf(" ") + 1)).some((url) => url.trim() !== "" && isAdoRemoteUrl(url));
|
|
3316
3320
|
}
|
|
3317
3321
|
|
|
3318
3322
|
// src/modules/hub.ts
|
|
@@ -3546,19 +3550,19 @@ function resolveManifestFlags(flags, current, manifestPath2) {
|
|
|
3546
3550
|
if (flags.contentSensitivity !== void 0) requested.content_sensitivity = flags.contentSensitivity;
|
|
3547
3551
|
if (flags.subjectRoot !== void 0) requested.subject_root = flags.subjectRoot;
|
|
3548
3552
|
if (flags.pushPolicy !== void 0) requested.push_policy = flags.pushPolicy;
|
|
3549
|
-
const invalid = (
|
|
3550
|
-
const allowed =
|
|
3553
|
+
const invalid = (error2) => {
|
|
3554
|
+
const allowed = error2.field === "role" ? ["hub", "consumer"] : MANIFEST_ENUM_VALUES[error2.field];
|
|
3551
3555
|
const hint = allowed ? ` Valid values: ${allowed.join(", ")}.` : "";
|
|
3552
|
-
return new ManifestFlagError(`Invalid ${FLAG_NAMES[
|
|
3556
|
+
return new ManifestFlagError(`Invalid ${FLAG_NAMES[error2.field] ?? error2.field}: ${error2.message}${hint}`);
|
|
3553
3557
|
};
|
|
3554
3558
|
if (requested.role !== void 0 && requested.role !== "hub" && requested.role !== "consumer") {
|
|
3555
3559
|
throw invalid(new ManifestInvalidFieldError(manifestPath2, "role", requested.role));
|
|
3556
3560
|
}
|
|
3557
3561
|
try {
|
|
3558
3562
|
validateManifestFields(requested, manifestPath2);
|
|
3559
|
-
} catch (
|
|
3560
|
-
if (
|
|
3561
|
-
throw
|
|
3563
|
+
} catch (error2) {
|
|
3564
|
+
if (error2 instanceof ManifestInvalidFieldError) throw invalid(error2);
|
|
3565
|
+
throw error2;
|
|
3562
3566
|
}
|
|
3563
3567
|
if (requested.external_provider !== void 0 && requested.tracking_mode !== "external") {
|
|
3564
3568
|
throw new ManifestFlagError("--external-provider applies only together with --tracking-mode external.");
|
|
@@ -3659,8 +3663,8 @@ async function readHooksPath(cwd) {
|
|
|
3659
3663
|
try {
|
|
3660
3664
|
const { stdout } = await runGit(cwd, ["config", "--show-scope", "--get", "core.hooksPath"]);
|
|
3661
3665
|
raw = stdout.trim();
|
|
3662
|
-
} catch (
|
|
3663
|
-
if (
|
|
3666
|
+
} catch (error2) {
|
|
3667
|
+
if (error2.code !== 1) {
|
|
3664
3668
|
return { value: "", scope: "unknown", unreadable: true };
|
|
3665
3669
|
}
|
|
3666
3670
|
return void 0;
|
|
@@ -3886,7 +3890,7 @@ async function pushTargets(cwd) {
|
|
|
3886
3890
|
const { stdout } = await runGit(cwd, ["remote", "get-url", "--push", "--all", remote]);
|
|
3887
3891
|
targets.push({
|
|
3888
3892
|
remote,
|
|
3889
|
-
urls: stdout.split("\n").map((
|
|
3893
|
+
urls: stdout.split("\n").map((line2) => line2.trim()).filter(Boolean)
|
|
3890
3894
|
});
|
|
3891
3895
|
} catch {
|
|
3892
3896
|
targets.push({ remote, urls: [] });
|
|
@@ -3900,7 +3904,7 @@ function originalPushUrlKey(remote) {
|
|
|
3900
3904
|
async function readLocalAll(cwd, key) {
|
|
3901
3905
|
try {
|
|
3902
3906
|
const { stdout } = await runGit(cwd, ["config", "--local", "--get-all", key]);
|
|
3903
|
-
return stdout.split("\n").map((
|
|
3907
|
+
return stdout.split("\n").map((line2) => line2.trim()).filter(Boolean);
|
|
3904
3908
|
} catch {
|
|
3905
3909
|
return [];
|
|
3906
3910
|
}
|
|
@@ -3912,10 +3916,10 @@ async function readScopedAll(cwd, key) {
|
|
|
3912
3916
|
} catch {
|
|
3913
3917
|
return [];
|
|
3914
3918
|
}
|
|
3915
|
-
return stdout.split("\n").filter((
|
|
3916
|
-
const separator =
|
|
3917
|
-
if (separator === -1) return { scope: "unknown", value:
|
|
3918
|
-
return { scope:
|
|
3919
|
+
return stdout.split("\n").filter((line2) => line2 !== "").map((line2) => {
|
|
3920
|
+
const separator = line2.indexOf(" ");
|
|
3921
|
+
if (separator === -1) return { scope: "unknown", value: line2.trim() };
|
|
3922
|
+
return { scope: line2.slice(0, separator).trim(), value: line2.slice(separator + 1).trim() };
|
|
3919
3923
|
});
|
|
3920
3924
|
}
|
|
3921
3925
|
async function effectivePushUrls(cwd, remote) {
|
|
@@ -3927,8 +3931,8 @@ async function disablePushUrls(cwd) {
|
|
|
3927
3931
|
for (const remote of await listRemotes(cwd)) {
|
|
3928
3932
|
try {
|
|
3929
3933
|
results.push(await disableOneRemote(cwd, remote));
|
|
3930
|
-
} catch (
|
|
3931
|
-
results.push({ remote, status: "failed", reason: describeError(
|
|
3934
|
+
} catch (error2) {
|
|
3935
|
+
results.push({ remote, status: "failed", reason: describeError(error2) });
|
|
3932
3936
|
}
|
|
3933
3937
|
}
|
|
3934
3938
|
return results;
|
|
@@ -4003,8 +4007,8 @@ async function restorePushUrls(cwd) {
|
|
|
4003
4007
|
for (const remote of await listRemotes(cwd)) {
|
|
4004
4008
|
try {
|
|
4005
4009
|
results.push(await restoreOneRemote(cwd, remote));
|
|
4006
|
-
} catch (
|
|
4007
|
-
results.push({ remote, status: "failed", reason: describeError(
|
|
4010
|
+
} catch (error2) {
|
|
4011
|
+
results.push({ remote, status: "failed", reason: describeError(error2) });
|
|
4008
4012
|
}
|
|
4009
4013
|
}
|
|
4010
4014
|
return results;
|
|
@@ -4044,8 +4048,8 @@ async function restoreOneRemote(cwd, remote) {
|
|
|
4044
4048
|
async function unsetIgnoringMissing(cwd, key) {
|
|
4045
4049
|
try {
|
|
4046
4050
|
await runGit(cwd, ["config", "--local", "--unset-all", key]);
|
|
4047
|
-
} catch (
|
|
4048
|
-
if (
|
|
4051
|
+
} catch (error2) {
|
|
4052
|
+
if (error2.code !== 5) throw error2;
|
|
4049
4053
|
}
|
|
4050
4054
|
}
|
|
4051
4055
|
async function hadNoPushUrl(cwd, remote) {
|
|
@@ -4073,8 +4077,8 @@ async function readLegacyRecord(cwd, remote) {
|
|
|
4073
4077
|
}
|
|
4074
4078
|
return { status: "usable", values };
|
|
4075
4079
|
}
|
|
4076
|
-
function describeError(
|
|
4077
|
-
const message =
|
|
4080
|
+
function describeError(error2) {
|
|
4081
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
4078
4082
|
return message.split("\n")[0].trim();
|
|
4079
4083
|
}
|
|
4080
4084
|
function hookInForce(outcome) {
|
|
@@ -4082,8 +4086,8 @@ function hookInForce(outcome) {
|
|
|
4082
4086
|
}
|
|
4083
4087
|
async function applyBlockedPushControls(cwd, options = {}) {
|
|
4084
4088
|
const messages = [];
|
|
4085
|
-
const say = (level,
|
|
4086
|
-
const message = { level, text };
|
|
4089
|
+
const say = (level, text2) => {
|
|
4090
|
+
const message = { level, text: text2 };
|
|
4087
4091
|
messages.push(message);
|
|
4088
4092
|
options.emit?.(message);
|
|
4089
4093
|
};
|
|
@@ -4373,7 +4377,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
4373
4377
|
}
|
|
4374
4378
|
const escapes = findLinkedEscapes(targetDir, plannedFiles);
|
|
4375
4379
|
if (escapes.length > 0) {
|
|
4376
|
-
for (const
|
|
4380
|
+
for (const line2 of describeLinkedEscapes(targetDir, escapes)) console.error(line2);
|
|
4377
4381
|
console.error(
|
|
4378
4382
|
" \u2717 Init refused: nothing was installed. Arcane writes managed files only inside its target directory: replace each link named above with a directory, then run `spell init` again."
|
|
4379
4383
|
);
|
|
@@ -4562,7 +4566,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
4562
4566
|
const flagLine = options.profile ? manifestFlagLine(manifest) : void 0;
|
|
4563
4567
|
if (flagLine) {
|
|
4564
4568
|
printInfo("Some setup questions were left unanswered. Answer them without a prompt with:");
|
|
4565
|
-
for (const
|
|
4569
|
+
for (const line2 of flagLine) printInfo(` ${line2}`);
|
|
4566
4570
|
}
|
|
4567
4571
|
if (push_policy === "blocked" || push_policy === "guarded") {
|
|
4568
4572
|
try {
|
|
@@ -4754,9 +4758,9 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
4754
4758
|
};
|
|
4755
4759
|
await writeManifest(storeRoot, manifest, { followLinks: true });
|
|
4756
4760
|
printStep(`\u2728 ${fileCount} Spells installed to ${storeRoot}/spells/`);
|
|
4757
|
-
for (const
|
|
4758
|
-
if (
|
|
4759
|
-
else printStep(`\u{1F517} ${
|
|
4761
|
+
for (const line2 of describeFanoutOutcomes(fanout.outcomes)) {
|
|
4762
|
+
if (line2.startsWith("!")) printWarning(line2.slice(2));
|
|
4763
|
+
else printStep(`\u{1F517} ${line2}`);
|
|
4760
4764
|
}
|
|
4761
4765
|
printSuccess(
|
|
4762
4766
|
`Initialized the user tier \u2014 ${fileCount} spells, ${Object.keys(fanout.record).length} client files recorded`
|
|
@@ -4764,7 +4768,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
4764
4768
|
console.log();
|
|
4765
4769
|
printSection("\u{1F9ED} Clients");
|
|
4766
4770
|
console.log();
|
|
4767
|
-
for (const
|
|
4771
|
+
for (const line2 of VSCODE_USER_TIER_NOTE) printInfo(line2);
|
|
4768
4772
|
printInfo(CLAUDE_PRECEDENCE_NOTE);
|
|
4769
4773
|
console.log();
|
|
4770
4774
|
printSection("\u{1F680} Next steps");
|
|
@@ -5045,11 +5049,11 @@ function computeLcsDiff(base, other) {
|
|
|
5045
5049
|
}
|
|
5046
5050
|
function arraysEqual(a, b) {
|
|
5047
5051
|
if (a.length !== b.length) return false;
|
|
5048
|
-
return a.every((
|
|
5052
|
+
return a.every((line2, idx) => line2 === b[idx]);
|
|
5049
5053
|
}
|
|
5050
5054
|
var MAX_LINES_FOR_LCS = 4e3;
|
|
5051
|
-
function splitLines(
|
|
5052
|
-
return
|
|
5055
|
+
function splitLines(text2) {
|
|
5056
|
+
return text2.split("\n");
|
|
5053
5057
|
}
|
|
5054
5058
|
function merge3(base, ours, theirs) {
|
|
5055
5059
|
const baseLines = splitLines(base);
|
|
@@ -5344,9 +5348,9 @@ async function missingTierWarning(scope, spellScope, homeDir) {
|
|
|
5344
5348
|
function reportFlagPatch(flagPatch, before, after, dryRun) {
|
|
5345
5349
|
console.log(`
|
|
5346
5350
|
${dryRun ? "[dry-run] Would record" : "Recorded"} from flags:`);
|
|
5347
|
-
for (const
|
|
5351
|
+
for (const line2 of describeFlagPatch(flagPatch)) console.log(` ${line2}`);
|
|
5348
5352
|
if (!dryRun && after.push_policy !== before.push_policy) {
|
|
5349
|
-
for (const
|
|
5353
|
+
for (const line2 of pushPolicyNotice(after.push_policy)) console.log(line2);
|
|
5350
5354
|
}
|
|
5351
5355
|
}
|
|
5352
5356
|
function defaultScopePatch(options, manifest) {
|
|
@@ -5619,7 +5623,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5619
5623
|
const homeDir = dirname7(targetDir);
|
|
5620
5624
|
const tierGate = await missingTierWarning(scope, spellScope);
|
|
5621
5625
|
if (tierGate.length > 0) {
|
|
5622
|
-
for (const
|
|
5626
|
+
for (const line2 of tierGate) console.error(line2);
|
|
5623
5627
|
console.error(
|
|
5624
5628
|
" \u2717 Update refused: nothing was written. Install the tier or change spell_scope, then run `spell update` again."
|
|
5625
5629
|
);
|
|
@@ -5662,7 +5666,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5662
5666
|
const linkedEscapes = followDirectoryLinks || manifest.components.length === 0 ? [] : findLinkedEscapes(targetDir, await plannedUpdateWrites(manifest.components, scope, spellScope, assetsDir));
|
|
5663
5667
|
const refusedForLinks = () => {
|
|
5664
5668
|
if (linkedEscapes.length === 0) return false;
|
|
5665
|
-
for (const
|
|
5669
|
+
for (const line2 of describeLinkedEscapes(targetDir, linkedEscapes)) console.error(line2);
|
|
5666
5670
|
console.error(
|
|
5667
5671
|
" \u2717 Update refused: nothing was written. Arcane writes and deletes managed files only inside its target directory: replace each link named above with a directory, then run `spell update` again."
|
|
5668
5672
|
);
|
|
@@ -5698,7 +5702,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5698
5702
|
registryChanges.missingRequires = missingRequires.filter((m) => !installing.has(m.name)).map((m) => ({ ...m, leftAloneReason: installPlan.leftAlone.find((l) => l.name === m.name)?.reason }));
|
|
5699
5703
|
}
|
|
5700
5704
|
const printRegistryChanges = () => {
|
|
5701
|
-
for (const
|
|
5705
|
+
for (const line2 of formatRegistryChanges(registryChanges)) console.log(line2);
|
|
5702
5706
|
};
|
|
5703
5707
|
const applyInstalls = async () => {
|
|
5704
5708
|
const declined = installPlan.leftAlone.filter((l) => l.kind === "new");
|
|
@@ -5706,14 +5710,14 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5706
5710
|
console.log(options.dryRun ? "\n[dry-run] Components this update would install (ARC-052):" : "\nComponents installed by this update (ARC-052):");
|
|
5707
5711
|
for (const l of declined) console.log(` ! Not installed: ${l.name} \u2014 ${l.reason}`);
|
|
5708
5712
|
if (options.dryRun) {
|
|
5709
|
-
for (const
|
|
5713
|
+
for (const line2 of describeInstallPlan(installPlan)) console.log(line2);
|
|
5710
5714
|
return [];
|
|
5711
5715
|
}
|
|
5712
5716
|
const { installed, lines } = await executeUpdateInstalls(installPlan, targetDir, packageVersion, {
|
|
5713
5717
|
linkTracked,
|
|
5714
5718
|
conversions
|
|
5715
5719
|
});
|
|
5716
|
-
for (const
|
|
5720
|
+
for (const line2 of lines) console.log(line2);
|
|
5717
5721
|
return installed;
|
|
5718
5722
|
};
|
|
5719
5723
|
let sameVersionRestore = false;
|
|
@@ -5740,7 +5744,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5740
5744
|
dryRun: options.dryRun,
|
|
5741
5745
|
fallbackDir: assetsDir
|
|
5742
5746
|
});
|
|
5743
|
-
for (const
|
|
5747
|
+
for (const line2 of describeFanoutOutcomes(fanout.outcomes, options.dryRun)) console.log(` ${line2}`);
|
|
5744
5748
|
const scopePatch = defaultScopePatch(options, manifest);
|
|
5745
5749
|
if (!options.dryRun && (JSON.stringify(fanout.record) !== JSON.stringify(manifest.fanout ?? {}) || scopePatch !== void 0)) {
|
|
5746
5750
|
await writeManifest(targetDir, { ...manifest, ...scopePatch, fanout: fanout.record }, { followLinks: scope === "user" });
|
|
@@ -6044,7 +6048,7 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
6044
6048
|
});
|
|
6045
6049
|
const lines = describeFanoutOutcomes(fanout.outcomes, options.dryRun);
|
|
6046
6050
|
if (lines.length > 0) console.log();
|
|
6047
|
-
for (const
|
|
6051
|
+
for (const line2 of lines) console.log(` ${line2}`);
|
|
6048
6052
|
fanoutRecord = fanout.record;
|
|
6049
6053
|
}
|
|
6050
6054
|
if (options.dryRun) {
|
|
@@ -6069,7 +6073,7 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
6069
6073
|
console.log(
|
|
6070
6074
|
` ! Skipped ${pending.length} manifest question${pending.length === 1 ? "" : "s"} (${pending.map((r) => r.field).join(", ")}) \u2014 no interactive terminal. Run \`spell update\` from a terminal, or answer by flags:`
|
|
6071
6075
|
);
|
|
6072
|
-
for (const
|
|
6076
|
+
for (const line2 of manifestFlagLine(answered) ?? []) console.log(` ${line2}`);
|
|
6073
6077
|
}
|
|
6074
6078
|
}
|
|
6075
6079
|
const updated = {
|
|
@@ -6089,7 +6093,7 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
6089
6093
|
if (Object.keys(flagPatch).length > 0) {
|
|
6090
6094
|
reportFlagPatch(flagPatch, manifest, updated, false);
|
|
6091
6095
|
} else if (updated.push_policy !== manifest.push_policy) {
|
|
6092
|
-
for (const
|
|
6096
|
+
for (const line2 of pushPolicyNotice(updated.push_policy)) console.log(line2);
|
|
6093
6097
|
}
|
|
6094
6098
|
if (conflictedFiles.length > 0) {
|
|
6095
6099
|
console.log(
|
|
@@ -6117,14 +6121,11 @@ import { dirname as dirname9 } from "path";
|
|
|
6117
6121
|
import { readFile as readFile10, writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
|
|
6118
6122
|
import { dirname as dirname8, join as join12 } from "path";
|
|
6119
6123
|
import { homedir as homedir3 } from "os";
|
|
6120
|
-
import { createRequire } from "module";
|
|
6121
|
-
var require2 = createRequire(import.meta.url);
|
|
6122
6124
|
function getFeedUrl() {
|
|
6123
|
-
const
|
|
6124
|
-
const url = pkg2.publishConfig?.registry;
|
|
6125
|
+
const url = packageJson.publishConfig?.registry;
|
|
6125
6126
|
if (!url) {
|
|
6126
6127
|
throw new Error(
|
|
6127
|
-
"publishConfig.registry is not set in package.json
|
|
6128
|
+
"publishConfig.registry is not set in package.json, so the npm feed URL cannot be resolved"
|
|
6128
6129
|
);
|
|
6129
6130
|
}
|
|
6130
6131
|
return url;
|
|
@@ -6219,14 +6220,15 @@ async function collectVersions(manifest, packageVersion, options) {
|
|
|
6219
6220
|
let lookup = null;
|
|
6220
6221
|
if (options.online) {
|
|
6221
6222
|
let feedUrl = options.feedUrl ?? "";
|
|
6223
|
+
let feedError = "";
|
|
6222
6224
|
if (feedUrl === "") {
|
|
6223
6225
|
try {
|
|
6224
6226
|
feedUrl = getFeedUrl();
|
|
6225
|
-
} catch {
|
|
6226
|
-
|
|
6227
|
+
} catch (error2) {
|
|
6228
|
+
feedError = `Feed URL unavailable: ${error2 instanceof Error ? error2.message : String(error2)}`;
|
|
6227
6229
|
}
|
|
6228
6230
|
}
|
|
6229
|
-
lookup = feedUrl ? await checkForUpdate(packageVersion, feedUrl, options.cachePath) : { current: packageVersion, latest: null, updateAvailable: false, error:
|
|
6231
|
+
lookup = feedUrl ? await checkForUpdate(packageVersion, feedUrl, options.cachePath) : { current: packageVersion, latest: null, updateAvailable: false, error: feedError };
|
|
6230
6232
|
if (lookup.latest === null) {
|
|
6231
6233
|
warnings.push(
|
|
6232
6234
|
`versions.latest: the npm lookup gave no answer${lookup.error ? ` (${lookup.error})` : ""}; latest and drift.cli_behind_latest are null`
|
|
@@ -6358,7 +6360,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
6358
6360
|
}
|
|
6359
6361
|
}
|
|
6360
6362
|
console.log("");
|
|
6361
|
-
const latestStr =
|
|
6363
|
+
const latestStr = versions.latest ?? "unable to check";
|
|
6362
6364
|
let footer = ` Installed: ${versions.cli} Latest: ${latestStr}`;
|
|
6363
6365
|
if (versionResult.updateAvailable) {
|
|
6364
6366
|
footer += " (spell update available)";
|
|
@@ -6386,7 +6388,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
6386
6388
|
}
|
|
6387
6389
|
if (scope === "user") {
|
|
6388
6390
|
console.log("");
|
|
6389
|
-
for (const
|
|
6391
|
+
for (const line2 of VSCODE_USER_TIER_NOTE) console.log(` ${line2}`);
|
|
6390
6392
|
console.log(` ${CLAUDE_PRECEDENCE_NOTE}`);
|
|
6391
6393
|
}
|
|
6392
6394
|
}
|
|
@@ -6406,9 +6408,9 @@ async function collectAgents(targetDir, options = {}) {
|
|
|
6406
6408
|
let resolved;
|
|
6407
6409
|
try {
|
|
6408
6410
|
resolved = await resolveRoster(targetDir, "repo", options.homeDir);
|
|
6409
|
-
} catch (
|
|
6410
|
-
if (
|
|
6411
|
-
throw
|
|
6411
|
+
} catch (error2) {
|
|
6412
|
+
if (error2 instanceof AgentRosterNotFoundError) return { agents: null, warnings: [] };
|
|
6413
|
+
throw error2;
|
|
6412
6414
|
}
|
|
6413
6415
|
const origin = resolved.fromUserTier ? "user" : "repo";
|
|
6414
6416
|
const warnings = [];
|
|
@@ -6455,18 +6457,18 @@ function titleAfterId(rest) {
|
|
|
6455
6457
|
return rest.replace(/^\s*[—–:-]+\s*/u, "").trim();
|
|
6456
6458
|
}
|
|
6457
6459
|
function parseDecisions(content) {
|
|
6458
|
-
const
|
|
6460
|
+
const text2 = content.replace(/\r\n?/g, "\n");
|
|
6459
6461
|
const positions = [];
|
|
6460
|
-
let
|
|
6462
|
+
let match2;
|
|
6461
6463
|
HEADING.lastIndex = 0;
|
|
6462
|
-
while ((
|
|
6463
|
-
positions.push({ id: `${
|
|
6464
|
+
while ((match2 = HEADING.exec(text2)) !== null) {
|
|
6465
|
+
positions.push({ id: `${match2[1]}-${match2[2]}`, heading: `${match2[1]}-${match2[2]}${match2[3]}`, index: match2.index });
|
|
6464
6466
|
}
|
|
6465
6467
|
const entries = [];
|
|
6466
6468
|
const warnings = [];
|
|
6467
6469
|
positions.forEach(({ id, heading, index }, i) => {
|
|
6468
|
-
const end = i + 1 < positions.length ? positions[i + 1].index :
|
|
6469
|
-
const section =
|
|
6470
|
+
const end = i + 1 < positions.length ? positions[i + 1].index : text2.length;
|
|
6471
|
+
const section = text2.slice(index, end);
|
|
6470
6472
|
const dateMatch = /^\*\*Date:\*\*\s*(\d{4}-\d{2}-\d{2})/m.exec(section);
|
|
6471
6473
|
const statusMatch = /^\*\*Status:\*\*\s*(\S+)/m.exec(section);
|
|
6472
6474
|
if (!dateMatch && !statusMatch) {
|
|
@@ -6495,11 +6497,11 @@ async function collectDecisions(root) {
|
|
|
6495
6497
|
let content;
|
|
6496
6498
|
try {
|
|
6497
6499
|
content = await readFile12(join14(root, "DECISIONS.md"), "utf8");
|
|
6498
|
-
} catch (
|
|
6499
|
-
if (
|
|
6500
|
+
} catch (error2) {
|
|
6501
|
+
if (error2.code === "ENOENT") return { decisions: null, warnings: [] };
|
|
6500
6502
|
return {
|
|
6501
6503
|
decisions: null,
|
|
6502
|
-
warnings: [`indexes.decisions: DECISIONS.md could not be read (${
|
|
6504
|
+
warnings: [`indexes.decisions: DECISIONS.md could not be read (${error2 instanceof Error ? error2.message : String(error2)})`]
|
|
6503
6505
|
};
|
|
6504
6506
|
}
|
|
6505
6507
|
const { entries, warnings } = parseDecisions(content);
|
|
@@ -6513,12 +6515,12 @@ async function collectDecisions(root) {
|
|
|
6513
6515
|
var ITEM = /^\s*- \[( |x|X)\] (.*)$/;
|
|
6514
6516
|
var HEADING2 = /^#{1,6}\s+(.+?)\s*#*\s*$/;
|
|
6515
6517
|
var TEXT_LIMIT = 200;
|
|
6516
|
-
function normalizeItemText(
|
|
6517
|
-
return
|
|
6518
|
+
function normalizeItemText(text2) {
|
|
6519
|
+
return text2.replace(/\s+/g, " ").trim();
|
|
6518
6520
|
}
|
|
6519
|
-
function itemLocator(section,
|
|
6521
|
+
function itemLocator(section, text2) {
|
|
6520
6522
|
const slug = section === null ? "" : headingAnchor(section);
|
|
6521
|
-
const hash = createHash3("sha256").update(normalizeItemText(
|
|
6523
|
+
const hash = createHash3("sha256").update(normalizeItemText(text2), "utf8").digest("hex").slice(0, 12);
|
|
6522
6524
|
return `${slug === "" ? "root" : slug}/${hash}`;
|
|
6523
6525
|
}
|
|
6524
6526
|
function parseBook(content) {
|
|
@@ -6526,22 +6528,22 @@ function parseBook(content) {
|
|
|
6526
6528
|
let section = null;
|
|
6527
6529
|
let open = 0;
|
|
6528
6530
|
let done = 0;
|
|
6529
|
-
for (const
|
|
6530
|
-
const heading = HEADING2.exec(
|
|
6531
|
+
for (const line2 of content.split(/\r?\n/)) {
|
|
6532
|
+
const heading = HEADING2.exec(line2);
|
|
6531
6533
|
if (heading) {
|
|
6532
6534
|
section = heading[1];
|
|
6533
6535
|
continue;
|
|
6534
6536
|
}
|
|
6535
|
-
const item = ITEM.exec(
|
|
6537
|
+
const item = ITEM.exec(line2);
|
|
6536
6538
|
if (!item) continue;
|
|
6537
6539
|
const checked = item[1] !== " ";
|
|
6538
|
-
const
|
|
6540
|
+
const text2 = item[2].trim();
|
|
6539
6541
|
if (checked) done++;
|
|
6540
6542
|
else open++;
|
|
6541
6543
|
items.push({
|
|
6542
|
-
text:
|
|
6544
|
+
text: text2.length > TEXT_LIMIT ? text2.slice(0, TEXT_LIMIT) : text2,
|
|
6543
6545
|
section,
|
|
6544
|
-
locator: itemLocator(section,
|
|
6546
|
+
locator: itemLocator(section, text2),
|
|
6545
6547
|
checked
|
|
6546
6548
|
});
|
|
6547
6549
|
}
|
|
@@ -6551,11 +6553,11 @@ async function collectBook(root, fileName) {
|
|
|
6551
6553
|
let content;
|
|
6552
6554
|
try {
|
|
6553
6555
|
content = await readFile13(join15(root, fileName), "utf8");
|
|
6554
|
-
} catch (
|
|
6555
|
-
if (
|
|
6556
|
+
} catch (error2) {
|
|
6557
|
+
if (error2.code === "ENOENT") return { book: null, warnings: [] };
|
|
6556
6558
|
return {
|
|
6557
6559
|
book: null,
|
|
6558
|
-
warnings: [`indexes: ${fileName} could not be read (${
|
|
6560
|
+
warnings: [`indexes: ${fileName} could not be read (${error2 instanceof Error ? error2.message : String(error2)})`]
|
|
6559
6561
|
};
|
|
6560
6562
|
}
|
|
6561
6563
|
return { book: parseBook(content), warnings: [] };
|
|
@@ -6606,16 +6608,16 @@ function featureTracking(frontmatter) {
|
|
|
6606
6608
|
};
|
|
6607
6609
|
}
|
|
6608
6610
|
function parsePrd(content, displayPath) {
|
|
6609
|
-
const
|
|
6610
|
-
const heading = H1.exec(
|
|
6611
|
+
const text2 = content.replace(/\r\n?/g, "\n");
|
|
6612
|
+
const heading = H1.exec(text2);
|
|
6611
6613
|
const empty = { title: heading ? heading[1] : null, status: null, implementation_status: null, tracking: null };
|
|
6612
|
-
const
|
|
6613
|
-
if (!
|
|
6614
|
+
const match2 = FRONTMATTER.exec(text2);
|
|
6615
|
+
if (!match2) return { fields: empty, warning: `features: ${displayPath} has no frontmatter` };
|
|
6614
6616
|
let parsed;
|
|
6615
6617
|
try {
|
|
6616
|
-
parsed = parse2(
|
|
6617
|
-
} catch (
|
|
6618
|
-
const reason =
|
|
6618
|
+
parsed = parse2(match2[1]);
|
|
6619
|
+
} catch (error2) {
|
|
6620
|
+
const reason = error2 instanceof Error ? error2.message.split("\n")[0] : String(error2);
|
|
6619
6621
|
return { fields: empty, warning: `features: ${displayPath} has malformed frontmatter (${reason})` };
|
|
6620
6622
|
}
|
|
6621
6623
|
if (!isRecord3(parsed)) return { fields: empty, warning: `features: ${displayPath} has frontmatter that is not a map` };
|
|
@@ -6635,11 +6637,11 @@ async function collectFeatures(root) {
|
|
|
6635
6637
|
try {
|
|
6636
6638
|
const entries = await readdir7(dir, { withFileTypes: true });
|
|
6637
6639
|
slugs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
6638
|
-
} catch (
|
|
6639
|
-
if (
|
|
6640
|
+
} catch (error2) {
|
|
6641
|
+
if (error2.code === "ENOENT") return { features: null, warnings: [] };
|
|
6640
6642
|
return {
|
|
6641
6643
|
features: null,
|
|
6642
|
-
warnings: [`features: ${FEATURES_DIR}/ could not be read (${
|
|
6644
|
+
warnings: [`features: ${FEATURES_DIR}/ could not be read (${error2 instanceof Error ? error2.message : String(error2)})`]
|
|
6643
6645
|
};
|
|
6644
6646
|
}
|
|
6645
6647
|
const warnings = [];
|
|
@@ -6649,9 +6651,9 @@ async function collectFeatures(root) {
|
|
|
6649
6651
|
let content;
|
|
6650
6652
|
try {
|
|
6651
6653
|
content = await readFile14(join16(dir, slug, PRD_FILE), "utf8");
|
|
6652
|
-
} catch (
|
|
6653
|
-
if (
|
|
6654
|
-
warnings.push(`features: ${path5} could not be read (${
|
|
6654
|
+
} catch (error2) {
|
|
6655
|
+
if (error2.code === "ENOENT") continue;
|
|
6656
|
+
warnings.push(`features: ${path5} could not be read (${error2 instanceof Error ? error2.message : String(error2)})`);
|
|
6655
6657
|
continue;
|
|
6656
6658
|
}
|
|
6657
6659
|
const { fields, warning } = parsePrd(content, path5);
|
|
@@ -6815,10 +6817,10 @@ function serializeState(doc) {
|
|
|
6815
6817
|
`;
|
|
6816
6818
|
}
|
|
6817
6819
|
function toPosixRelative(root, target) {
|
|
6818
|
-
const
|
|
6819
|
-
if (
|
|
6820
|
-
if (path4.isAbsolute(
|
|
6821
|
-
return
|
|
6820
|
+
const relative5 = path4.relative(root, target);
|
|
6821
|
+
if (relative5 === "") return ".";
|
|
6822
|
+
if (path4.isAbsolute(relative5)) return relative5.replace(/\\/g, "/");
|
|
6823
|
+
return relative5.split(path4.sep).join("/");
|
|
6822
6824
|
}
|
|
6823
6825
|
|
|
6824
6826
|
// src/modules/state/git.ts
|
|
@@ -6840,10 +6842,10 @@ function stripUserinfo(url) {
|
|
|
6840
6842
|
function parseWorktreeList(porcelain, root) {
|
|
6841
6843
|
const worktrees = [];
|
|
6842
6844
|
for (const block of porcelain.replace(/\r\n/g, "\n").split(/\n\n+/)) {
|
|
6843
|
-
const lines = block.split("\n").filter((
|
|
6844
|
-
const pathLine = lines.find((
|
|
6845
|
+
const lines = block.split("\n").filter((line2) => line2.length > 0);
|
|
6846
|
+
const pathLine = lines.find((line2) => line2.startsWith("worktree "));
|
|
6845
6847
|
if (!pathLine) continue;
|
|
6846
|
-
const branchLine = lines.find((
|
|
6848
|
+
const branchLine = lines.find((line2) => line2.startsWith("branch "));
|
|
6847
6849
|
const branch = branchLine ? branchLine.slice("branch ".length).replace(/^refs\/heads\//, "") : null;
|
|
6848
6850
|
const absolute = resolveRealPath(pathLine.slice("worktree ".length));
|
|
6849
6851
|
worktrees.push({ path: toPosixRelative(root, absolute), branch });
|
|
@@ -6853,12 +6855,12 @@ function parseWorktreeList(porcelain, root) {
|
|
|
6853
6855
|
async function probe(read) {
|
|
6854
6856
|
try {
|
|
6855
6857
|
return { ok: true, value: await read() };
|
|
6856
|
-
} catch (
|
|
6857
|
-
return { ok: false, error };
|
|
6858
|
+
} catch (error2) {
|
|
6859
|
+
return { ok: false, error: error2 };
|
|
6858
6860
|
}
|
|
6859
6861
|
}
|
|
6860
|
-
function describe(
|
|
6861
|
-
return
|
|
6862
|
+
function describe(error2) {
|
|
6863
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
6862
6864
|
}
|
|
6863
6865
|
async function collectGit(cwd, root, options = {}) {
|
|
6864
6866
|
const git = options.runGit ?? runGit;
|
|
@@ -6949,8 +6951,8 @@ async function collectGit(cwd, root, options = {}) {
|
|
|
6949
6951
|
warnings
|
|
6950
6952
|
};
|
|
6951
6953
|
}
|
|
6952
|
-
function isTimeout(
|
|
6953
|
-
return
|
|
6954
|
+
function isTimeout(error2) {
|
|
6955
|
+
return error2 instanceof Error && error2.name === "GitTimeoutError";
|
|
6954
6956
|
}
|
|
6955
6957
|
|
|
6956
6958
|
// src/modules/state/journal.ts
|
|
@@ -6959,13 +6961,13 @@ import { join as join17 } from "path";
|
|
|
6959
6961
|
var JOURNAL_DIR = "journal";
|
|
6960
6962
|
var ENTRY_NAME = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
|
6961
6963
|
function journalTitle(content) {
|
|
6962
|
-
const
|
|
6963
|
-
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(
|
|
6964
|
+
const text2 = content.replace(/\r\n?/g, "\n");
|
|
6965
|
+
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(text2);
|
|
6964
6966
|
if (frontmatter) {
|
|
6965
6967
|
const title = /^title:\s*(.+)$/m.exec(frontmatter[1]);
|
|
6966
6968
|
if (title) return title[1].trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
6967
6969
|
}
|
|
6968
|
-
const heading = /^# (.+?)\s*$/m.exec(
|
|
6970
|
+
const heading = /^# (.+?)\s*$/m.exec(text2);
|
|
6969
6971
|
return heading ? heading[1].trim() : null;
|
|
6970
6972
|
}
|
|
6971
6973
|
async function collectJournal(root) {
|
|
@@ -6974,29 +6976,29 @@ async function collectJournal(root) {
|
|
|
6974
6976
|
try {
|
|
6975
6977
|
const entries = await readdir8(dir, { withFileTypes: true });
|
|
6976
6978
|
names = entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
6977
|
-
} catch (
|
|
6978
|
-
if (
|
|
6979
|
+
} catch (error2) {
|
|
6980
|
+
if (error2.code === "ENOENT") return { journal: null, warnings: [] };
|
|
6979
6981
|
return {
|
|
6980
6982
|
journal: null,
|
|
6981
|
-
warnings: [`indexes.journal: ${JOURNAL_DIR}/ could not be read (${
|
|
6983
|
+
warnings: [`indexes.journal: ${JOURNAL_DIR}/ could not be read (${error2 instanceof Error ? error2.message : String(error2)})`]
|
|
6982
6984
|
};
|
|
6983
6985
|
}
|
|
6984
6986
|
const warnings = [];
|
|
6985
6987
|
const journal = [];
|
|
6986
6988
|
for (const name of names) {
|
|
6987
|
-
const
|
|
6988
|
-
if (!
|
|
6989
|
+
const match2 = ENTRY_NAME.exec(name);
|
|
6990
|
+
if (!match2) {
|
|
6989
6991
|
warnings.push(`indexes.journal: ${JOURNAL_DIR}/${name} does not match YYYY-MM-DD-<slug>.md and was skipped`);
|
|
6990
6992
|
continue;
|
|
6991
6993
|
}
|
|
6992
6994
|
let title = null;
|
|
6993
6995
|
try {
|
|
6994
6996
|
title = journalTitle(await readFile15(join17(dir, name), "utf8"));
|
|
6995
|
-
} catch (
|
|
6996
|
-
warnings.push(`indexes.journal: ${JOURNAL_DIR}/${name} could not be read (${
|
|
6997
|
+
} catch (error2) {
|
|
6998
|
+
warnings.push(`indexes.journal: ${JOURNAL_DIR}/${name} could not be read (${error2 instanceof Error ? error2.message : String(error2)})`);
|
|
6997
6999
|
}
|
|
6998
7000
|
if (title === null) warnings.push(`indexes.journal: ${JOURNAL_DIR}/${name} has no title: frontmatter or # heading`);
|
|
6999
|
-
journal.push({ date:
|
|
7001
|
+
journal.push({ date: match2[1], slug: match2[2], title, path: `${JOURNAL_DIR}/${name}` });
|
|
7000
7002
|
}
|
|
7001
7003
|
journal.sort((a, b) => a.date < b.date ? -1 : a.date > b.date ? 1 : a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
|
|
7002
7004
|
return { journal, warnings };
|
|
@@ -7019,9 +7021,9 @@ function execFileWithTimeout(file, args, timeoutMs, options = {}) {
|
|
|
7019
7021
|
encoding: "utf8",
|
|
7020
7022
|
...options.env ? { env: { ...process.env, ...options.env } } : {}
|
|
7021
7023
|
},
|
|
7022
|
-
(
|
|
7023
|
-
if (
|
|
7024
|
-
reject(
|
|
7024
|
+
(error2, stdout, stderr) => {
|
|
7025
|
+
if (error2) {
|
|
7026
|
+
reject(error2);
|
|
7025
7027
|
return;
|
|
7026
7028
|
}
|
|
7027
7029
|
resolve10({ stdout, stderr });
|
|
@@ -7071,14 +7073,14 @@ function parseAdoRemote(url) {
|
|
|
7071
7073
|
if (ssh) return { org: ssh[1], project: decodeURIComponent(ssh[2]), repo: decodeURIComponent(ssh[3]) };
|
|
7072
7074
|
return null;
|
|
7073
7075
|
}
|
|
7074
|
-
function describe2(
|
|
7075
|
-
if (
|
|
7076
|
-
const code =
|
|
7076
|
+
function describe2(error2) {
|
|
7077
|
+
if (error2 instanceof Error) {
|
|
7078
|
+
const code = error2.code;
|
|
7077
7079
|
if (code === "ENOENT") return "the CLI is not installed";
|
|
7078
|
-
if (
|
|
7079
|
-
return
|
|
7080
|
+
if (error2.killed) return `timed out after ${EXTERNAL_CLI_TIMEOUT_MS}ms`;
|
|
7081
|
+
return error2.message.split("\n")[0];
|
|
7080
7082
|
}
|
|
7081
|
-
return String(
|
|
7083
|
+
return String(error2);
|
|
7082
7084
|
}
|
|
7083
7085
|
function isRecord5(value) {
|
|
7084
7086
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -7087,8 +7089,8 @@ async function lookupGitHub(branch, exec) {
|
|
|
7087
7089
|
let stdout;
|
|
7088
7090
|
try {
|
|
7089
7091
|
({ stdout } = await exec("gh", ["pr", "list", "--head", branch, "--state", "open", "--json", "number,url,state"], EXTERNAL_CLI_TIMEOUT_MS));
|
|
7090
|
-
} catch (
|
|
7091
|
-
return { pull_request: null, warnings: [`git.pull_request: gh pr list failed (${describe2(
|
|
7092
|
+
} catch (error2) {
|
|
7093
|
+
return { pull_request: null, warnings: [`git.pull_request: gh pr list failed (${describe2(error2)})`] };
|
|
7092
7094
|
}
|
|
7093
7095
|
let parsed;
|
|
7094
7096
|
try {
|
|
@@ -7135,8 +7137,8 @@ async function lookupAdo(branch, remoteUrl, exec, launch) {
|
|
|
7135
7137
|
EXTERNAL_CLI_TIMEOUT_MS,
|
|
7136
7138
|
launch.env ? { env: launch.env } : {}
|
|
7137
7139
|
));
|
|
7138
|
-
} catch (
|
|
7139
|
-
return { pull_request: null, warnings: [`git.pull_request: az repos pr list failed (${describe2(
|
|
7140
|
+
} catch (error2) {
|
|
7141
|
+
return { pull_request: null, warnings: [`git.pull_request: az repos pr list failed (${describe2(error2)})`] };
|
|
7140
7142
|
}
|
|
7141
7143
|
let parsed;
|
|
7142
7144
|
try {
|
|
@@ -7243,17 +7245,17 @@ async function collectRegistry(root, manifest, options) {
|
|
|
7243
7245
|
let content;
|
|
7244
7246
|
try {
|
|
7245
7247
|
content = await readFile16(join18(root, businessRoot, "registry.json"), "utf8");
|
|
7246
|
-
} catch (
|
|
7247
|
-
const reason =
|
|
7248
|
+
} catch (error2) {
|
|
7249
|
+
const reason = error2.code === "ENOENT" ? "is missing" : `could not be read (${error2 instanceof Error ? error2.message : String(error2)})`;
|
|
7248
7250
|
return { registry: null, warnings: [`registry: this repository is a hub but ${relativePath} ${reason}`] };
|
|
7249
7251
|
}
|
|
7250
7252
|
let raw;
|
|
7251
7253
|
try {
|
|
7252
7254
|
raw = JSON.parse(content);
|
|
7253
|
-
} catch (
|
|
7255
|
+
} catch (error2) {
|
|
7254
7256
|
return {
|
|
7255
7257
|
registry: null,
|
|
7256
|
-
warnings: [`registry: ${relativePath} is not valid JSON (${
|
|
7258
|
+
warnings: [`registry: ${relativePath} is not valid JSON (${error2 instanceof Error ? error2.message : String(error2)})`]
|
|
7257
7259
|
};
|
|
7258
7260
|
}
|
|
7259
7261
|
return { registry: parseRegistry(raw, options), warnings: [] };
|
|
@@ -7268,9 +7270,9 @@ async function guarded(section, warnings, run, fallback) {
|
|
|
7268
7270
|
const result = await run();
|
|
7269
7271
|
warnings.push(...result.warnings);
|
|
7270
7272
|
return result.value;
|
|
7271
|
-
} catch (
|
|
7272
|
-
if (
|
|
7273
|
-
warnings.push(`${section}: collector failed (${
|
|
7273
|
+
} catch (error2) {
|
|
7274
|
+
if (error2 instanceof AgentConfigValidationError) throw error2;
|
|
7275
|
+
warnings.push(`${section}: collector failed (${error2 instanceof Error ? error2.message : String(error2)}); the section is null`);
|
|
7274
7276
|
return fallback;
|
|
7275
7277
|
}
|
|
7276
7278
|
}
|
|
@@ -7403,9 +7405,9 @@ async function collectState(cwd, packageVersion, options) {
|
|
|
7403
7405
|
|
|
7404
7406
|
// src/commands/state.ts
|
|
7405
7407
|
var USAGE_POINTER = "spell state prints one JSON document to stdout: run `spell state --json` (spell status is the human view).";
|
|
7406
|
-
function writeStdout(
|
|
7408
|
+
function writeStdout(text2) {
|
|
7407
7409
|
return new Promise((resolve10, reject) => {
|
|
7408
|
-
process.stdout.write(
|
|
7410
|
+
process.stdout.write(text2, (error2) => error2 ? reject(error2) : resolve10());
|
|
7409
7411
|
});
|
|
7410
7412
|
}
|
|
7411
7413
|
async function runState(targetDir, packageVersion, options = {}, deps = {}) {
|
|
@@ -7415,28 +7417,28 @@ async function runState(targetDir, packageVersion, options = {}, deps = {}) {
|
|
|
7415
7417
|
return;
|
|
7416
7418
|
}
|
|
7417
7419
|
const collect = deps.collect ?? collectState;
|
|
7418
|
-
let
|
|
7420
|
+
let text2;
|
|
7419
7421
|
try {
|
|
7420
7422
|
const doc = await collect(targetDir, packageVersion, {
|
|
7421
7423
|
online: options.online === true,
|
|
7422
7424
|
includeLocalPaths: options.includeLocalPaths === true
|
|
7423
7425
|
});
|
|
7424
|
-
|
|
7425
|
-
} catch (
|
|
7426
|
-
if (
|
|
7426
|
+
text2 = serializeState(doc);
|
|
7427
|
+
} catch (error2) {
|
|
7428
|
+
if (error2 instanceof ManifestNotFoundError) {
|
|
7427
7429
|
console.error('Not initialized. Run "spell init" first.');
|
|
7428
|
-
} else if (
|
|
7429
|
-
console.error(
|
|
7430
|
+
} else if (error2 instanceof AgentConfigValidationError) {
|
|
7431
|
+
console.error(error2.message);
|
|
7430
7432
|
} else {
|
|
7431
|
-
console.error(`spell state: ${
|
|
7433
|
+
console.error(`spell state: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
7432
7434
|
}
|
|
7433
7435
|
process.exitCode = 1;
|
|
7434
7436
|
return;
|
|
7435
7437
|
}
|
|
7436
7438
|
try {
|
|
7437
|
-
await (deps.write ?? writeStdout)(
|
|
7438
|
-
} catch (
|
|
7439
|
-
console.error(`spell state: stdout is not writable (${
|
|
7439
|
+
await (deps.write ?? writeStdout)(text2);
|
|
7440
|
+
} catch (error2) {
|
|
7441
|
+
console.error(`spell state: stdout is not writable (${error2 instanceof Error ? error2.message : String(error2)})`);
|
|
7440
7442
|
process.exitCode = 1;
|
|
7441
7443
|
}
|
|
7442
7444
|
}
|
|
@@ -7588,7 +7590,7 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
7588
7590
|
previous: manifest.fanout,
|
|
7589
7591
|
dryRun: true
|
|
7590
7592
|
});
|
|
7591
|
-
for (const
|
|
7593
|
+
for (const line2 of describeFanoutOutcomes(preview.outcomes, true)) console.log(line2);
|
|
7592
7594
|
console.log(`[dry-run] Would remove: ${storeRoot}/.arcane.json`);
|
|
7593
7595
|
console.log(
|
|
7594
7596
|
`
|
|
@@ -7626,7 +7628,7 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
7626
7628
|
await rm5(join19(storeRoot, "agents.yaml"), { force: true });
|
|
7627
7629
|
await removeDirectoryIfEmpty(join19(storeRoot, USER_TIER_SPELLS_DIR));
|
|
7628
7630
|
await removeDirectoryIfEmpty(storeRoot);
|
|
7629
|
-
for (const
|
|
7631
|
+
for (const line2 of describeFanoutOutcomes(fanout.outcomes)) console.log(` ${line2}`);
|
|
7630
7632
|
const pruned = fanout.outcomes.filter((o) => o.status === "pruned").length;
|
|
7631
7633
|
const kept = fanout.outcomes.filter((o) => o.status === "kept-edited").length;
|
|
7632
7634
|
console.log(
|
|
@@ -7929,9 +7931,9 @@ async function fetchAdoMergeTypePolicies(org, project, repoName, branch, launch
|
|
|
7929
7931
|
}
|
|
7930
7932
|
}
|
|
7931
7933
|
function parseGitHubRemote(url) {
|
|
7932
|
-
const
|
|
7933
|
-
if (!
|
|
7934
|
-
const [, owner, repo] =
|
|
7934
|
+
const match2 = /github\.com[/:]([^/]+)\/([^/.]+?)(?:\.git)?\/?$/.exec(url);
|
|
7935
|
+
if (!match2) return null;
|
|
7936
|
+
const [, owner, repo] = match2;
|
|
7935
7937
|
if (!owner || !repo) return null;
|
|
7936
7938
|
return { owner, repo };
|
|
7937
7939
|
}
|
|
@@ -7946,9 +7948,9 @@ function decodeAdoSegment(raw) {
|
|
|
7946
7948
|
return decoded;
|
|
7947
7949
|
}
|
|
7948
7950
|
function parseAdoRemote2(url) {
|
|
7949
|
-
const
|
|
7950
|
-
if (!
|
|
7951
|
-
const [, rawOrg, rawProject, rawRepo] =
|
|
7951
|
+
const match2 = /dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/?]+)/.exec(url) ?? /([^./]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/?]+)/.exec(url);
|
|
7952
|
+
if (!match2) return null;
|
|
7953
|
+
const [, rawOrg, rawProject, rawRepo] = match2;
|
|
7952
7954
|
if (!rawOrg || !rawProject || !rawRepo) return null;
|
|
7953
7955
|
const org = decodeAdoSegment(rawOrg);
|
|
7954
7956
|
const project = decodeAdoSegment(rawProject);
|
|
@@ -7975,28 +7977,28 @@ function stripFrontmatter(content) {
|
|
|
7975
7977
|
const block = FRONTMATTER_PATTERN2.exec(content);
|
|
7976
7978
|
return block ? content.slice(block[0].length) : content;
|
|
7977
7979
|
}
|
|
7978
|
-
function stripFencedCode(
|
|
7980
|
+
function stripFencedCode(text2) {
|
|
7979
7981
|
const kept = [];
|
|
7980
7982
|
let fence = null;
|
|
7981
|
-
for (const
|
|
7983
|
+
for (const line2 of text2.split(/\r?\n/)) {
|
|
7982
7984
|
if (fence) {
|
|
7983
|
-
const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(
|
|
7985
|
+
const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line2);
|
|
7984
7986
|
if (close && close[1][0] === fence.char && close[1].length >= fence.length) fence = null;
|
|
7985
7987
|
kept.push("");
|
|
7986
7988
|
continue;
|
|
7987
7989
|
}
|
|
7988
|
-
const open = /^ {0,3}(`{3,}|~{3,})/.exec(
|
|
7989
|
-
if (open && !(open[1][0] === "`" &&
|
|
7990
|
+
const open = /^ {0,3}(`{3,}|~{3,})/.exec(line2);
|
|
7991
|
+
if (open && !(open[1][0] === "`" && line2.slice(open[0].length).includes("`"))) {
|
|
7990
7992
|
fence = { char: open[1][0], length: open[1].length };
|
|
7991
7993
|
kept.push("");
|
|
7992
7994
|
continue;
|
|
7993
7995
|
}
|
|
7994
|
-
kept.push(
|
|
7996
|
+
kept.push(line2);
|
|
7995
7997
|
}
|
|
7996
7998
|
return kept.join("\n");
|
|
7997
7999
|
}
|
|
7998
|
-
function stripInlineCode(
|
|
7999
|
-
return
|
|
8000
|
+
function stripInlineCode(text2) {
|
|
8001
|
+
return text2.split(/\n[ \t]*\n/).map((paragraph) => {
|
|
8000
8002
|
let out = "";
|
|
8001
8003
|
let i = 0;
|
|
8002
8004
|
while (i < paragraph.length) {
|
|
@@ -8037,7 +8039,7 @@ function stripInlineCode(text) {
|
|
|
8037
8039
|
function findPlaceholderTokens(content) {
|
|
8038
8040
|
const prose = stripInlineCode(stripFencedCode(stripFrontmatter(content)));
|
|
8039
8041
|
const seen = /* @__PURE__ */ new Set();
|
|
8040
|
-
for (const
|
|
8042
|
+
for (const match2 of prose.matchAll(PLACEHOLDER_PATTERN)) seen.add(`{${match2[1]}}`);
|
|
8041
8043
|
return [...seen];
|
|
8042
8044
|
}
|
|
8043
8045
|
function parseRuntimePlaceholders(standardContent) {
|
|
@@ -8046,8 +8048,8 @@ function parseRuntimePlaceholders(standardContent) {
|
|
|
8046
8048
|
if (start === -1 || end === -1 || end < start) return null;
|
|
8047
8049
|
const region = standardContent.slice(start + RUNTIME_PLACEHOLDERS_START.length, end);
|
|
8048
8050
|
const tokens = /* @__PURE__ */ new Set();
|
|
8049
|
-
for (const
|
|
8050
|
-
tokens.add(
|
|
8051
|
+
for (const match2 of region.matchAll(/^[ \t]*[-*][ \t]+`(\{[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*\})`/gm)) {
|
|
8052
|
+
tokens.add(match2[1]);
|
|
8051
8053
|
}
|
|
8052
8054
|
return tokens;
|
|
8053
8055
|
}
|
|
@@ -8984,9 +8986,9 @@ function parseTermsFileContent(raw) {
|
|
|
8984
8986
|
async function readTermsFile(filePath) {
|
|
8985
8987
|
try {
|
|
8986
8988
|
return parseTermsFileContent(await readFile21(filePath, "utf8"));
|
|
8987
|
-
} catch (
|
|
8989
|
+
} catch (error2) {
|
|
8988
8990
|
console.warn(
|
|
8989
|
-
`\u26A0 arcane: could not read --terms-file "${filePath}": ${
|
|
8991
|
+
`\u26A0 arcane: could not read --terms-file "${filePath}": ${error2.message}`
|
|
8990
8992
|
);
|
|
8991
8993
|
return [];
|
|
8992
8994
|
}
|
|
@@ -9249,8 +9251,8 @@ var ChunkedSecretStore = class {
|
|
|
9249
9251
|
guard(operation, fn) {
|
|
9250
9252
|
try {
|
|
9251
9253
|
return fn();
|
|
9252
|
-
} catch (
|
|
9253
|
-
throw new KeychainUnavailableError(operation,
|
|
9254
|
+
} catch (error2) {
|
|
9255
|
+
throw new KeychainUnavailableError(operation, error2);
|
|
9254
9256
|
}
|
|
9255
9257
|
}
|
|
9256
9258
|
/** Reads the stored secret. Never throws for bad data; throws only when the keychain itself fails. */
|
|
@@ -9360,9 +9362,9 @@ function resultPagePath(kind) {
|
|
|
9360
9362
|
return join24("sign-in", `sign-in-callback-${kind}.html`);
|
|
9361
9363
|
}
|
|
9362
9364
|
function styleHashes(html) {
|
|
9363
|
-
return [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)].map((
|
|
9364
|
-
const
|
|
9365
|
-
return `'sha256-${createHash5("sha256").update(
|
|
9365
|
+
return [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)].map((match2) => {
|
|
9366
|
+
const text2 = match2[1].replace(/\r\n?/g, "\n");
|
|
9367
|
+
return `'sha256-${createHash5("sha256").update(text2, "utf8").digest("base64")}'`;
|
|
9366
9368
|
});
|
|
9367
9369
|
}
|
|
9368
9370
|
function resultPageHeaders(html) {
|
|
@@ -9446,8 +9448,8 @@ async function loadIdentityConfiguration(environment, options = {}) {
|
|
|
9446
9448
|
const response = await fetchImpl(environment.discoveryUrl, { redirect: "manual" });
|
|
9447
9449
|
if (!response.ok) throw new Error(`discovery returned HTTP ${response.status}`);
|
|
9448
9450
|
metadata = await response.json();
|
|
9449
|
-
} catch (
|
|
9450
|
-
throw new IdentityUnreachableError(environment.signInHost,
|
|
9451
|
+
} catch (error2) {
|
|
9452
|
+
throw new IdentityUnreachableError(environment.signInHost, error2);
|
|
9451
9453
|
}
|
|
9452
9454
|
if (typeof metadata.issuer !== "string" || !metadata.token_endpoint || !metadata.authorization_endpoint) {
|
|
9453
9455
|
throw new IdentityUnreachableError(environment.signInHost, new Error("discovery document incomplete"));
|
|
@@ -9572,8 +9574,8 @@ async function browserLogin(config, options) {
|
|
|
9572
9574
|
const result = toTokenResult(response);
|
|
9573
9575
|
await options.onValidated?.(result);
|
|
9574
9576
|
redirectToResult({ kind: "success", result });
|
|
9575
|
-
}).catch((
|
|
9576
|
-
redirectToResult({ kind: "failure", error });
|
|
9577
|
+
}).catch((error2) => {
|
|
9578
|
+
redirectToResult({ kind: "failure", error: error2 });
|
|
9577
9579
|
});
|
|
9578
9580
|
});
|
|
9579
9581
|
timer = setTimeout(() => reject(new LoginTimeoutError()), timeoutMs);
|
|
@@ -9605,7 +9607,7 @@ function openInDefaultBrowser(url) {
|
|
|
9605
9607
|
const [command, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
9606
9608
|
return new Promise((resolve10) => {
|
|
9607
9609
|
try {
|
|
9608
|
-
const child = execFile3(command, args, { windowsHide: true }, (
|
|
9610
|
+
const child = execFile3(command, args, { windowsHide: true }, (error2) => resolve10(!error2));
|
|
9609
9611
|
child.on("error", () => resolve10(false));
|
|
9610
9612
|
} catch {
|
|
9611
9613
|
resolve10(false);
|
|
@@ -9711,11 +9713,11 @@ var FileSecretBackend = class {
|
|
|
9711
9713
|
writeFileSync(tmp, JSON.stringify(entries), { encoding: "utf8", mode: 384 });
|
|
9712
9714
|
try {
|
|
9713
9715
|
this.restrict(tmp);
|
|
9714
|
-
} catch (
|
|
9716
|
+
} catch (error2) {
|
|
9715
9717
|
rmSync(tmp, { force: true });
|
|
9716
9718
|
throw new Error(
|
|
9717
9719
|
"Could not restrict the session file to your account, so it was not written.",
|
|
9718
|
-
{ cause:
|
|
9720
|
+
{ cause: error2 }
|
|
9719
9721
|
);
|
|
9720
9722
|
}
|
|
9721
9723
|
renameSync(tmp, this.filePath);
|
|
@@ -9770,9 +9772,9 @@ function createSessionStorage(options = {}) {
|
|
|
9770
9772
|
try {
|
|
9771
9773
|
const fromKeychain = interpret(keychain.load(), "keychain");
|
|
9772
9774
|
if (fromKeychain) return fromKeychain;
|
|
9773
|
-
} catch (
|
|
9774
|
-
if (!(
|
|
9775
|
-
keychainError =
|
|
9775
|
+
} catch (error2) {
|
|
9776
|
+
if (!(error2 instanceof KeychainUnavailableError)) throw error2;
|
|
9777
|
+
keychainError = error2;
|
|
9776
9778
|
}
|
|
9777
9779
|
if (fileBackend.exists()) {
|
|
9778
9780
|
const fromFile = interpret(file.load(), "file");
|
|
@@ -9800,8 +9802,8 @@ function createSessionStorage(options = {}) {
|
|
|
9800
9802
|
const hadKeychainSession = keychain.load().state !== "signed-out";
|
|
9801
9803
|
keychain.clear();
|
|
9802
9804
|
if (hadKeychainSession) removed.push("keychain");
|
|
9803
|
-
} catch (
|
|
9804
|
-
keychainError =
|
|
9805
|
+
} catch (error2) {
|
|
9806
|
+
keychainError = error2;
|
|
9805
9807
|
}
|
|
9806
9808
|
if (fileBackend.exists()) {
|
|
9807
9809
|
fileBackend.remove();
|
|
@@ -9831,8 +9833,8 @@ function defaultLoginDeps(assetsDir) {
|
|
|
9831
9833
|
}
|
|
9832
9834
|
var INSECURE_STORAGE_WARNING = "Insecure storage: the session is kept in a file protected only by your account's file permissions, not in the OS keychain. Run `spell logout` to remove it.";
|
|
9833
9835
|
var MISSING_RESULT_PAGES_WARNING = "This arcane-cli install is missing its sign-in result pages (assets/sign-in/), so the browser will show a plain page. Reinstall the package to restore them.";
|
|
9834
|
-
function safeErrorText(
|
|
9835
|
-
const message =
|
|
9836
|
+
function safeErrorText(error2) {
|
|
9837
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
9836
9838
|
return message.split("\n")[0].replace(/([?&](?:code|state|id_token|access_token|refresh_token)=)[^&\s]+/gi, "$1[redacted]");
|
|
9837
9839
|
}
|
|
9838
9840
|
function toSessionRecord(result, environment) {
|
|
@@ -9854,13 +9856,13 @@ async function runLogin(options, deps) {
|
|
|
9854
9856
|
let environment;
|
|
9855
9857
|
try {
|
|
9856
9858
|
environment = deps.environment();
|
|
9857
|
-
} catch (
|
|
9858
|
-
if (
|
|
9859
|
-
console.error(
|
|
9859
|
+
} catch (error2) {
|
|
9860
|
+
if (error2 instanceof UnknownEnvironmentError) {
|
|
9861
|
+
console.error(error2.message);
|
|
9860
9862
|
process.exit(1);
|
|
9861
9863
|
return;
|
|
9862
9864
|
}
|
|
9863
|
-
throw
|
|
9865
|
+
throw error2;
|
|
9864
9866
|
}
|
|
9865
9867
|
if (!options.deviceCode && !deps.isInteractive()) {
|
|
9866
9868
|
console.error(
|
|
@@ -9872,9 +9874,9 @@ async function runLogin(options, deps) {
|
|
|
9872
9874
|
let config;
|
|
9873
9875
|
try {
|
|
9874
9876
|
config = await deps.loadConfiguration(environment);
|
|
9875
|
-
} catch (
|
|
9877
|
+
} catch (error2) {
|
|
9876
9878
|
console.error(
|
|
9877
|
-
|
|
9879
|
+
error2 instanceof IdentityUnreachableError ? `${error2.message} Check your network connection and try again.` : `Could not prepare the sign-in: ${safeErrorText(error2)}`
|
|
9878
9880
|
);
|
|
9879
9881
|
process.exit(1);
|
|
9880
9882
|
return;
|
|
@@ -9910,15 +9912,15 @@ async function runLogin(options, deps) {
|
|
|
9910
9912
|
});
|
|
9911
9913
|
if (!record) throw new Error("The browser sign-in ended without storing a session.");
|
|
9912
9914
|
}
|
|
9913
|
-
} catch (
|
|
9914
|
-
if (
|
|
9915
|
+
} catch (error2) {
|
|
9916
|
+
if (error2 instanceof KeychainUnavailableError) {
|
|
9915
9917
|
console.error(
|
|
9916
|
-
`${
|
|
9918
|
+
`${error2.message} Nothing was stored. If this machine has no keychain, run \`spell login --insecure-storage\` to keep the session in a file instead (protected only by your account's file permissions).`
|
|
9917
9919
|
);
|
|
9918
|
-
} else if (
|
|
9919
|
-
console.error(`${
|
|
9920
|
+
} else if (error2 instanceof LoginTimeoutError || error2 instanceof LoginDeniedError) {
|
|
9921
|
+
console.error(`${error2.message} Nothing was changed.`);
|
|
9920
9922
|
} else {
|
|
9921
|
-
console.error(`The sign-in did not complete: ${safeErrorText(
|
|
9923
|
+
console.error(`The sign-in did not complete: ${safeErrorText(error2)}. Nothing was changed.`);
|
|
9922
9924
|
}
|
|
9923
9925
|
process.exit(1);
|
|
9924
9926
|
return;
|
|
@@ -9933,21 +9935,21 @@ function runLogout(storage = createSessionStorage()) {
|
|
|
9933
9935
|
let removed;
|
|
9934
9936
|
try {
|
|
9935
9937
|
removed = storage.clear().removed;
|
|
9936
|
-
} catch (
|
|
9937
|
-
if (
|
|
9938
|
-
const fileResult =
|
|
9938
|
+
} catch (error2) {
|
|
9939
|
+
if (error2 instanceof SessionClearError) {
|
|
9940
|
+
const fileResult = error2.removed.includes("file") ? "The insecure session file was removed. " : "";
|
|
9939
9941
|
console.error(
|
|
9940
|
-
`${fileResult}${
|
|
9942
|
+
`${fileResult}${error2.keychainError.message} Any session in the OS keychain could not be verified or removed.`
|
|
9941
9943
|
);
|
|
9942
9944
|
process.exit(1);
|
|
9943
9945
|
return;
|
|
9944
9946
|
}
|
|
9945
|
-
if (
|
|
9946
|
-
console.error(`${
|
|
9947
|
+
if (error2 instanceof KeychainUnavailableError) {
|
|
9948
|
+
console.error(`${error2.message} The session could not be removed from it.`);
|
|
9947
9949
|
process.exit(1);
|
|
9948
9950
|
return;
|
|
9949
9951
|
}
|
|
9950
|
-
throw
|
|
9952
|
+
throw error2;
|
|
9951
9953
|
}
|
|
9952
9954
|
if (removed.length === 0) {
|
|
9953
9955
|
printSuccess("Nothing was stored; you were already signed out.");
|
|
@@ -9971,13 +9973,13 @@ async function runWhoami(options, deps = defaultWhoamiDeps()) {
|
|
|
9971
9973
|
let loaded;
|
|
9972
9974
|
try {
|
|
9973
9975
|
loaded = deps.storage.load();
|
|
9974
|
-
} catch (
|
|
9975
|
-
if (
|
|
9976
|
-
console.error(`${
|
|
9976
|
+
} catch (error2) {
|
|
9977
|
+
if (error2 instanceof KeychainUnavailableError) {
|
|
9978
|
+
console.error(`${error2.message} Cannot tell whether you are signed in.`);
|
|
9977
9979
|
process.exit(1);
|
|
9978
9980
|
return;
|
|
9979
9981
|
}
|
|
9980
|
-
throw
|
|
9982
|
+
throw error2;
|
|
9981
9983
|
}
|
|
9982
9984
|
if (loaded.state === "signed-out") {
|
|
9983
9985
|
console.error("Not signed in. Run `spell login`.");
|
|
@@ -10009,9 +10011,9 @@ async function runWhoami(options, deps = defaultWhoamiDeps()) {
|
|
|
10009
10011
|
const fresh = await deps.refreshSession(config, record.refreshToken, SIGN_IN_SCOPES);
|
|
10010
10012
|
deps.storage.save(toSessionRecord(fresh, environment), { insecure: source === "file" });
|
|
10011
10013
|
printSuccess(`Verified with the identity service at ${environment.signInHost}; the session is active.`);
|
|
10012
|
-
} catch (
|
|
10014
|
+
} catch (error2) {
|
|
10013
10015
|
console.error(
|
|
10014
|
-
`The identity service did not accept the session (${safeErrorText(
|
|
10016
|
+
`The identity service did not accept the session (${safeErrorText(error2)}). Run \`spell login\` again.`
|
|
10015
10017
|
);
|
|
10016
10018
|
process.exit(1);
|
|
10017
10019
|
}
|
|
@@ -10031,11 +10033,11 @@ import { join as join26 } from "path";
|
|
|
10031
10033
|
|
|
10032
10034
|
// src/modules/show-report/plan-parser.ts
|
|
10033
10035
|
function parseFrontmatter(content) {
|
|
10034
|
-
const
|
|
10035
|
-
if (!
|
|
10036
|
+
const match2 = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
10037
|
+
if (!match2) return {};
|
|
10036
10038
|
const fields = {};
|
|
10037
|
-
for (const
|
|
10038
|
-
const kv = /^([a-zA-Z][\w-]*):\s*(.*)$/.exec(
|
|
10039
|
+
for (const line2 of match2[1].split(/\r?\n/)) {
|
|
10040
|
+
const kv = /^([a-zA-Z][\w-]*):\s*(.*)$/.exec(line2);
|
|
10039
10041
|
if (kv) fields[kv[1]] = kv[2].trim();
|
|
10040
10042
|
}
|
|
10041
10043
|
return fields;
|
|
@@ -10103,7 +10105,7 @@ function parseEpics(content) {
|
|
|
10103
10105
|
}
|
|
10104
10106
|
function extractSection(content, headingPrefix) {
|
|
10105
10107
|
const lines = content.split(/\r?\n/);
|
|
10106
|
-
const startIndex = lines.findIndex((
|
|
10108
|
+
const startIndex = lines.findIndex((line2) => line2.startsWith(headingPrefix));
|
|
10107
10109
|
if (startIndex === -1) return null;
|
|
10108
10110
|
let endIndex = lines.length;
|
|
10109
10111
|
for (let i = startIndex + 1; i < lines.length; i += 1) {
|
|
@@ -10125,11 +10127,11 @@ function parseParkedSection(content) {
|
|
|
10125
10127
|
}));
|
|
10126
10128
|
}
|
|
10127
10129
|
return unwrapBullets(body).flatMap((bullet) => {
|
|
10128
|
-
const
|
|
10129
|
-
if (!
|
|
10130
|
-
const tail =
|
|
10131
|
-
const title = tail ? `${
|
|
10132
|
-
return [{ title: stripMarkdown(title), reason: stripMarkdown(
|
|
10130
|
+
const match2 = /^\*\*(.+?)\*\*(.*?)\s*—\s*(.+)$/.exec(bullet);
|
|
10131
|
+
if (!match2) return [];
|
|
10132
|
+
const tail = match2[2].replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
|
|
10133
|
+
const title = tail ? `${match2[1].trim()} ${tail}` : match2[1].trim();
|
|
10134
|
+
return [{ title: stripMarkdown(title), reason: stripMarkdown(match2[3].trim()) }];
|
|
10133
10135
|
});
|
|
10134
10136
|
}
|
|
10135
10137
|
function unwrapBullets(body) {
|
|
@@ -10139,12 +10141,12 @@ function unwrapBullets(body) {
|
|
|
10139
10141
|
if (current) bullets.push(current.join(" ").replace(/\s+/g, " ").trim());
|
|
10140
10142
|
current = null;
|
|
10141
10143
|
};
|
|
10142
|
-
for (const
|
|
10143
|
-
if (/^- /.test(
|
|
10144
|
+
for (const line2 of body.split(/\r?\n/)) {
|
|
10145
|
+
if (/^- /.test(line2)) {
|
|
10144
10146
|
flush();
|
|
10145
|
-
current = [
|
|
10147
|
+
current = [line2.slice(2).trim()];
|
|
10146
10148
|
} else if (current !== null) {
|
|
10147
|
-
if (/^\s+\S/.test(
|
|
10149
|
+
if (/^\s+\S/.test(line2)) current.push(line2.trim());
|
|
10148
10150
|
else flush();
|
|
10149
10151
|
}
|
|
10150
10152
|
}
|
|
@@ -10162,8 +10164,8 @@ function computeCoverageOutcome(content) {
|
|
|
10162
10164
|
const dispositioned = total - parkedCount;
|
|
10163
10165
|
return `${total} of ${total} items dispositioned \u2014 ${dispositioned} routed to an epic, ${parkedCount} parked`;
|
|
10164
10166
|
}
|
|
10165
|
-
function stripMarkdown(
|
|
10166
|
-
return
|
|
10167
|
+
function stripMarkdown(text2) {
|
|
10168
|
+
return text2.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*/g, "").replace(/`/g, "").trim();
|
|
10167
10169
|
}
|
|
10168
10170
|
|
|
10169
10171
|
// src/modules/show-report/queue-parser.ts
|
|
@@ -10209,8 +10211,8 @@ function parseVerificationLedgerSection(content, programNameWords) {
|
|
|
10209
10211
|
}
|
|
10210
10212
|
return rows;
|
|
10211
10213
|
}
|
|
10212
|
-
function stripMarkdown2(
|
|
10213
|
-
return
|
|
10214
|
+
function stripMarkdown2(text2) {
|
|
10215
|
+
return text2.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*/g, "").replace(/`/g, "").trim();
|
|
10214
10216
|
}
|
|
10215
10217
|
|
|
10216
10218
|
// src/modules/show-report/sources.ts
|
|
@@ -10248,8 +10250,8 @@ async function getCloseCommit(cwd, completedDate, generatedOutputs = []) {
|
|
|
10248
10250
|
}
|
|
10249
10251
|
const log = await runGitTextOrNull(cwd, ["log", "--format=%H%x09%cs", "HEAD", ...excludeOutputs(generatedOutputs)]);
|
|
10250
10252
|
if (log === null || log === "") return null;
|
|
10251
|
-
for (const
|
|
10252
|
-
const [sha, landed] =
|
|
10253
|
+
for (const line2 of log.split("\n")) {
|
|
10254
|
+
const [sha, landed] = line2.split(" ");
|
|
10253
10255
|
if (sha && landed && landed <= completedDate) return sha;
|
|
10254
10256
|
}
|
|
10255
10257
|
return null;
|
|
@@ -10346,9 +10348,9 @@ function buildNeedsYou(queueEntries) {
|
|
|
10346
10348
|
return entry;
|
|
10347
10349
|
});
|
|
10348
10350
|
}
|
|
10349
|
-
function truncateAtWord(
|
|
10350
|
-
if (
|
|
10351
|
-
const window =
|
|
10351
|
+
function truncateAtWord(text2, limit) {
|
|
10352
|
+
if (text2.length <= limit) return text2;
|
|
10353
|
+
const window = text2.slice(0, limit - 1);
|
|
10352
10354
|
const lastSpace = window.lastIndexOf(" ");
|
|
10353
10355
|
const head = lastSpace > 0 ? window.slice(0, lastSpace) : window;
|
|
10354
10356
|
return `${head.replace(/[\s,;:.—-]+$/, "")}\u2026`;
|
|
@@ -10474,11 +10476,11 @@ var CATEGORY_ORDER = [
|
|
|
10474
10476
|
"docs",
|
|
10475
10477
|
"platform"
|
|
10476
10478
|
];
|
|
10477
|
-
function escapeHtml(
|
|
10478
|
-
return
|
|
10479
|
+
function escapeHtml(text2) {
|
|
10480
|
+
return text2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
10479
10481
|
}
|
|
10480
|
-
function inlineMarkupToHtml(
|
|
10481
|
-
const escaped = escapeHtml(
|
|
10482
|
+
function inlineMarkupToHtml(text2) {
|
|
10483
|
+
const escaped = escapeHtml(text2);
|
|
10482
10484
|
const withLinks = escaped.replace(
|
|
10483
10485
|
/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g,
|
|
10484
10486
|
'<a href="$2">$1</a>'
|
|
@@ -10550,12 +10552,12 @@ function applyReportTheme(html, theme) {
|
|
|
10550
10552
|
if (!radioTagPattern(theme).test(html)) return { html, applied: false };
|
|
10551
10553
|
let result = html;
|
|
10552
10554
|
for (const id of REPORT_THEMES) {
|
|
10553
|
-
const
|
|
10554
|
-
if (!
|
|
10555
|
-
const original =
|
|
10555
|
+
const match2 = radioTagPattern(id).exec(result);
|
|
10556
|
+
if (!match2) continue;
|
|
10557
|
+
const original = match2[0];
|
|
10556
10558
|
const replaced = id === theme ? withChecked(original) : withoutChecked(original);
|
|
10557
10559
|
if (replaced !== original) {
|
|
10558
|
-
result = result.slice(0,
|
|
10560
|
+
result = result.slice(0, match2.index) + replaced + result.slice(match2.index + original.length);
|
|
10559
10561
|
}
|
|
10560
10562
|
}
|
|
10561
10563
|
return { html: result, applied: true };
|
|
@@ -10598,9 +10600,9 @@ async function discoverPrograms(rootDir) {
|
|
|
10598
10600
|
let entries;
|
|
10599
10601
|
try {
|
|
10600
10602
|
entries = (await readdir13(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
|
|
10601
|
-
} catch (
|
|
10602
|
-
if (
|
|
10603
|
-
throw
|
|
10603
|
+
} catch (error2) {
|
|
10604
|
+
if (error2.code === "ENOENT") return { programs: [], skipped: [] };
|
|
10605
|
+
throw error2;
|
|
10604
10606
|
}
|
|
10605
10607
|
const programs = [];
|
|
10606
10608
|
const skipped = [];
|
|
@@ -10622,9 +10624,9 @@ function normalizeLineEndings2(content) {
|
|
|
10622
10624
|
async function readOptionalFile(path5) {
|
|
10623
10625
|
try {
|
|
10624
10626
|
return await readFile24(path5, "utf8");
|
|
10625
|
-
} catch (
|
|
10626
|
-
if (
|
|
10627
|
-
throw
|
|
10627
|
+
} catch (error2) {
|
|
10628
|
+
if (error2.code === "ENOENT") return null;
|
|
10629
|
+
throw error2;
|
|
10628
10630
|
}
|
|
10629
10631
|
}
|
|
10630
10632
|
async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
|
|
@@ -10755,11 +10757,1400 @@ async function runReport(targetDir, options = {}, assetsDir) {
|
|
|
10755
10757
|
for (const path5 of result.repaired) console.log(` ${path5}`);
|
|
10756
10758
|
}
|
|
10757
10759
|
|
|
10760
|
+
// src/modules/agent-ref.ts
|
|
10761
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
10762
|
+
var REF_TYPES = ["agent", "template", "session"];
|
|
10763
|
+
var UUID_V7 = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
10764
|
+
var ANY_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
10765
|
+
function createUuidV7Generator(options = {}) {
|
|
10766
|
+
const now = options.now ?? Date.now;
|
|
10767
|
+
const random = options.random ?? ((n) => randomBytes2(n));
|
|
10768
|
+
let lastMs = -1;
|
|
10769
|
+
let counter = 0;
|
|
10770
|
+
return () => {
|
|
10771
|
+
let ms = Math.floor(now());
|
|
10772
|
+
const bytes = random(10);
|
|
10773
|
+
if (ms > lastMs) {
|
|
10774
|
+
counter = (bytes[0] << 8 | bytes[1]) & 2047;
|
|
10775
|
+
} else {
|
|
10776
|
+
ms = lastMs;
|
|
10777
|
+
counter += 1;
|
|
10778
|
+
if (counter > 4095) {
|
|
10779
|
+
ms += 1;
|
|
10780
|
+
counter = (bytes[0] << 8 | bytes[1]) & 2047;
|
|
10781
|
+
}
|
|
10782
|
+
}
|
|
10783
|
+
lastMs = ms;
|
|
10784
|
+
const out = new Uint8Array(16);
|
|
10785
|
+
let t = ms;
|
|
10786
|
+
for (let i = 5; i >= 0; i -= 1) {
|
|
10787
|
+
out[i] = t % 256;
|
|
10788
|
+
t = Math.floor(t / 256);
|
|
10789
|
+
}
|
|
10790
|
+
out[6] = 112 | counter >> 8 & 15;
|
|
10791
|
+
out[7] = counter & 255;
|
|
10792
|
+
out[8] = 128 | bytes[2] & 63;
|
|
10793
|
+
for (let i = 9; i < 16; i += 1) out[i] = bytes[i - 6];
|
|
10794
|
+
const hex = Array.from(out, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
10795
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
10796
|
+
};
|
|
10797
|
+
}
|
|
10798
|
+
var defaultUuidV7 = createUuidV7Generator();
|
|
10799
|
+
function uuidv7() {
|
|
10800
|
+
return defaultUuidV7();
|
|
10801
|
+
}
|
|
10802
|
+
function mintRef(type, generate = uuidv7) {
|
|
10803
|
+
if (!REF_TYPES.includes(type)) {
|
|
10804
|
+
throw new Error(`"${String(type)}" is not a registered reference type`);
|
|
10805
|
+
}
|
|
10806
|
+
return `arcane:${type}:${generate()}`;
|
|
10807
|
+
}
|
|
10808
|
+
function mintSessionRef() {
|
|
10809
|
+
return mintRef("session");
|
|
10810
|
+
}
|
|
10811
|
+
function parseRef(text2, expected) {
|
|
10812
|
+
if (typeof text2 !== "string") return { ok: false, reason: "a reference must be a string" };
|
|
10813
|
+
if (text2.length === 0) return { ok: false, reason: "a reference must not be empty" };
|
|
10814
|
+
if (text2 !== text2.trim()) return { ok: false, reason: "a reference must not carry surrounding whitespace" };
|
|
10815
|
+
if (text2 !== text2.toLowerCase()) return { ok: false, reason: "a reference must be lowercase" };
|
|
10816
|
+
const parts = text2.split(":");
|
|
10817
|
+
if (parts.length !== 3 || parts[0] !== "arcane") {
|
|
10818
|
+
return { ok: false, reason: 'a reference must have the form "arcane:<type>:<uuid>"' };
|
|
10819
|
+
}
|
|
10820
|
+
const type = parts[1];
|
|
10821
|
+
const uuid = parts[2];
|
|
10822
|
+
if (!REF_TYPES.includes(type)) {
|
|
10823
|
+
return { ok: false, reason: `"${type}" is not a registered reference type` };
|
|
10824
|
+
}
|
|
10825
|
+
if (uuid.startsWith("{") || uuid.endsWith("}")) {
|
|
10826
|
+
return { ok: false, reason: "a reference's uuid must not be braced" };
|
|
10827
|
+
}
|
|
10828
|
+
if (!ANY_UUID.test(uuid)) return { ok: false, reason: "a reference's uuid must be in canonical hex form" };
|
|
10829
|
+
if (!UUID_V7.test(uuid)) return { ok: false, reason: "a reference's uuid must be a version 7 UUID" };
|
|
10830
|
+
if (expected !== void 0 && type !== expected) {
|
|
10831
|
+
return { ok: false, reason: `expected a ${expected} reference, got one of type ${type}` };
|
|
10832
|
+
}
|
|
10833
|
+
return { ok: true, type, uuid };
|
|
10834
|
+
}
|
|
10835
|
+
|
|
10836
|
+
// src/modules/presence/client.ts
|
|
10837
|
+
import { request } from "http";
|
|
10838
|
+
|
|
10839
|
+
// src/modules/presence/store.ts
|
|
10840
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
10841
|
+
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
10842
|
+
import { join as join29, relative as relative4 } from "path";
|
|
10843
|
+
|
|
10844
|
+
// src/modules/presence/types.ts
|
|
10845
|
+
var PRESENCE_CONTRACT = "v0";
|
|
10846
|
+
var DEFAULT_PORT = 27226;
|
|
10847
|
+
var PRESENCE_ENV = { port: "ARCANE_PRESENCE_PORT", token: "ARCANE_PRESENCE_TOKEN" };
|
|
10848
|
+
var LOOPBACK_HOST = "127.0.0.1";
|
|
10849
|
+
var MAX_BODY_BYTES = 65536;
|
|
10850
|
+
var MAX_LINE_CHARS = 256;
|
|
10851
|
+
var MAX_NEEDS_YOU = 20;
|
|
10852
|
+
var MAX_SUBSCRIBERS = 32;
|
|
10853
|
+
var MAX_SUBSCRIBER_BUFFER_BYTES = 1048576;
|
|
10854
|
+
var MAX_CONNECTIONS = 128;
|
|
10855
|
+
var MAX_SESSIONS = 256;
|
|
10856
|
+
var MAX_ALIASES = 32;
|
|
10857
|
+
var CONNECTIONS_CHECK_MS = 1e3;
|
|
10858
|
+
var HEADERS_TIMEOUT_MS = 5e3;
|
|
10859
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
10860
|
+
var STALE_AFTER_MS = 9e4;
|
|
10861
|
+
var EXPIRE_AFTER_MS = 864e5;
|
|
10862
|
+
var KEEPALIVE_MS = 15e3;
|
|
10863
|
+
var SWEEP_MS = 5e3;
|
|
10864
|
+
var HARNESSES = ["claude-code", "copilot", "codex", "openclaw", "cron", "other"];
|
|
10865
|
+
var REPORT_STATUSES = ["working", "waiting", "blocked", "idle"];
|
|
10866
|
+
var ALIAS_CONTEXTS = ["openclaw", "slug"];
|
|
10867
|
+
var HELLO_EVENT = "server.hello";
|
|
10868
|
+
function presenceCapabilities() {
|
|
10869
|
+
return { observe: true, actions: [] };
|
|
10870
|
+
}
|
|
10871
|
+
function nullAgentBlock() {
|
|
10872
|
+
return { ref: null, slug: null, name: null, origin: null, definition_revision: null, aliases: [] };
|
|
10873
|
+
}
|
|
10874
|
+
|
|
10875
|
+
// src/modules/presence/store.ts
|
|
10876
|
+
var PRESENCE_DIR_NAME = "presence";
|
|
10877
|
+
var STORE_FILES = { token: "token", serverInfo: "server.json" };
|
|
10878
|
+
var PresenceStoreError = class extends Error {
|
|
10879
|
+
};
|
|
10880
|
+
function home(options) {
|
|
10881
|
+
return options.home ?? resolveHomeDir();
|
|
10882
|
+
}
|
|
10883
|
+
function presenceDir(options = {}) {
|
|
10884
|
+
return join29(userTierRoot(home(options)), PRESENCE_DIR_NAME);
|
|
10885
|
+
}
|
|
10886
|
+
function tokenPath(options = {}) {
|
|
10887
|
+
return join29(presenceDir(options), STORE_FILES.token);
|
|
10888
|
+
}
|
|
10889
|
+
function serverInfoPath(options = {}) {
|
|
10890
|
+
return join29(presenceDir(options), STORE_FILES.serverInfo);
|
|
10891
|
+
}
|
|
10892
|
+
function ensureStore(options = {}) {
|
|
10893
|
+
const root = userTierRoot(home(options));
|
|
10894
|
+
const dir = presenceDir(options);
|
|
10895
|
+
mkdirSync2(root, { recursive: true });
|
|
10896
|
+
const realRoot = resolveRealPath(root);
|
|
10897
|
+
if (relative4(realRoot, resolveRealPath(dir)) !== PRESENCE_DIR_NAME) {
|
|
10898
|
+
throw new PresenceStoreError(`Refusing to use ${dir}: it resolves outside ${root}.`);
|
|
10899
|
+
}
|
|
10900
|
+
if (relative4(realRoot, resolveRealPath(tokenPath(options))) !== join29(PRESENCE_DIR_NAME, STORE_FILES.token)) {
|
|
10901
|
+
throw new PresenceStoreError(`Refusing to use ${tokenPath(options)}: it resolves outside ${dir}.`);
|
|
10902
|
+
}
|
|
10903
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
10904
|
+
chmodSync(dir, 448);
|
|
10905
|
+
return dir;
|
|
10906
|
+
}
|
|
10907
|
+
function writeRestricted(target, content, options) {
|
|
10908
|
+
const restrict = options.restrict ?? restrictToCurrentUser;
|
|
10909
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
10910
|
+
writeFileSync2(tmp, content, { encoding: "utf8", mode: 384 });
|
|
10911
|
+
try {
|
|
10912
|
+
restrict(tmp);
|
|
10913
|
+
} catch (error2) {
|
|
10914
|
+
rmSync2(tmp, { force: true });
|
|
10915
|
+
throw new PresenceStoreError(`Could not restrict ${target} to your account, so it was not written.`, { cause: error2 });
|
|
10916
|
+
}
|
|
10917
|
+
renameSync2(tmp, target);
|
|
10918
|
+
chmodSync(target, 384);
|
|
10919
|
+
}
|
|
10920
|
+
function generateToken() {
|
|
10921
|
+
return randomBytes3(32).toString("base64url");
|
|
10922
|
+
}
|
|
10923
|
+
function readTokenFile(options) {
|
|
10924
|
+
let raw;
|
|
10925
|
+
try {
|
|
10926
|
+
raw = readFileSync3(tokenPath(options), "utf8");
|
|
10927
|
+
} catch {
|
|
10928
|
+
return null;
|
|
10929
|
+
}
|
|
10930
|
+
const token = raw.replace(/[\r\n]+$/, "").trim();
|
|
10931
|
+
return token.length > 0 ? token : null;
|
|
10932
|
+
}
|
|
10933
|
+
function ensureToken(options = {}) {
|
|
10934
|
+
ensureStore(options);
|
|
10935
|
+
const existing = options.rotate === true ? null : readTokenFile(options);
|
|
10936
|
+
if (existing !== null) {
|
|
10937
|
+
chmodSync(tokenPath(options), 384);
|
|
10938
|
+
(options.restrict ?? restrictToCurrentUser)(tokenPath(options));
|
|
10939
|
+
return existing;
|
|
10940
|
+
}
|
|
10941
|
+
const token = generateToken();
|
|
10942
|
+
writeRestricted(tokenPath(options), `${token}
|
|
10943
|
+
`, options);
|
|
10944
|
+
return token;
|
|
10945
|
+
}
|
|
10946
|
+
function readToken(options = {}) {
|
|
10947
|
+
const fromEnv = (options.env ?? process.env)[PRESENCE_ENV.token];
|
|
10948
|
+
if (fromEnv !== void 0 && fromEnv.replace(/[\r\n]+$/, "") !== "") return fromEnv.replace(/[\r\n]+$/, "");
|
|
10949
|
+
return readTokenFile(options);
|
|
10950
|
+
}
|
|
10951
|
+
function writeServerInfo(info, options = {}) {
|
|
10952
|
+
ensureStore(options);
|
|
10953
|
+
writeRestricted(serverInfoPath(options), `${JSON.stringify(info, null, 2)}
|
|
10954
|
+
`, options);
|
|
10955
|
+
}
|
|
10956
|
+
function readServerInfo(options = {}) {
|
|
10957
|
+
let parsed;
|
|
10958
|
+
try {
|
|
10959
|
+
parsed = JSON.parse(readFileSync3(serverInfoPath(options), "utf8"));
|
|
10960
|
+
} catch {
|
|
10961
|
+
return null;
|
|
10962
|
+
}
|
|
10963
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
10964
|
+
const o = parsed;
|
|
10965
|
+
const generator = o["generator"];
|
|
10966
|
+
const valid = typeof o["url"] === "string" && Number.isInteger(o["pid"]) && o["pid"] > 0 && Number.isInteger(o["port"]) && o["port"] >= 1 && o["port"] <= 65535 && typeof o["started_at"] === "string" && typeof generator === "object" && generator !== null && generator["name"] === "arcane-cli" && typeof generator["version"] === "string";
|
|
10967
|
+
if (!valid) return null;
|
|
10968
|
+
return {
|
|
10969
|
+
url: o["url"],
|
|
10970
|
+
pid: o["pid"],
|
|
10971
|
+
port: o["port"],
|
|
10972
|
+
started_at: o["started_at"],
|
|
10973
|
+
generator: { name: "arcane-cli", version: generator["version"] }
|
|
10974
|
+
};
|
|
10975
|
+
}
|
|
10976
|
+
function removeServerInfo(options = {}) {
|
|
10977
|
+
rmSync2(serverInfoPath(options), { force: true });
|
|
10978
|
+
}
|
|
10979
|
+
|
|
10980
|
+
// src/modules/presence/client.ts
|
|
10981
|
+
var CONNECT_TIMEOUT_MS = 1e3;
|
|
10982
|
+
var RESPONSE_TIMEOUT_MS = 1500;
|
|
10983
|
+
function pidAlive(pid) {
|
|
10984
|
+
try {
|
|
10985
|
+
process.kill(pid, 0);
|
|
10986
|
+
return true;
|
|
10987
|
+
} catch {
|
|
10988
|
+
return false;
|
|
10989
|
+
}
|
|
10990
|
+
}
|
|
10991
|
+
function resolveEndpoint(options = {}) {
|
|
10992
|
+
const info = readServerInfo(options);
|
|
10993
|
+
if (info === null || !pidAlive(info.pid)) return null;
|
|
10994
|
+
const token = readToken({ ...options, env: options.env ?? process.env });
|
|
10995
|
+
if (token === null) return null;
|
|
10996
|
+
return { info, token };
|
|
10997
|
+
}
|
|
10998
|
+
function errorFields(body) {
|
|
10999
|
+
const e = body?.error;
|
|
11000
|
+
return { code: typeof e?.code === "string" ? e.code : null, field: typeof e?.field === "string" ? e.field : null };
|
|
11001
|
+
}
|
|
11002
|
+
function send(port, token, method, path5, body, options = {}) {
|
|
11003
|
+
const connectTimeout = options.connectTimeoutMs ?? CONNECT_TIMEOUT_MS;
|
|
11004
|
+
const responseTimeout = options.responseTimeoutMs ?? RESPONSE_TIMEOUT_MS;
|
|
11005
|
+
const payload = body === void 0 ? void 0 : JSON.stringify(body);
|
|
11006
|
+
return new Promise((resolve10) => {
|
|
11007
|
+
let settled = false;
|
|
11008
|
+
const done = (result) => {
|
|
11009
|
+
if (settled) return;
|
|
11010
|
+
settled = true;
|
|
11011
|
+
clearTimeout(connectTimer);
|
|
11012
|
+
clearTimeout(responseTimer);
|
|
11013
|
+
resolve10(result);
|
|
11014
|
+
};
|
|
11015
|
+
const headers = { Authorization: `Bearer ${token}`, Accept: "application/json" };
|
|
11016
|
+
if (payload !== void 0) {
|
|
11017
|
+
headers["Content-Type"] = "application/json";
|
|
11018
|
+
headers["Content-Length"] = Buffer.byteLength(payload);
|
|
11019
|
+
}
|
|
11020
|
+
const req = request({ host: LOOPBACK_HOST, port, method, path: path5, headers, agent: false }, (res) => {
|
|
11021
|
+
const chunks = [];
|
|
11022
|
+
res.on("data", (c) => chunks.push(c));
|
|
11023
|
+
res.on("error", () => done({ kind: "unreachable" }));
|
|
11024
|
+
res.on("end", () => {
|
|
11025
|
+
const status = res.statusCode ?? 0;
|
|
11026
|
+
const text2 = Buffer.concat(chunks).toString("utf8");
|
|
11027
|
+
let parsed = null;
|
|
11028
|
+
try {
|
|
11029
|
+
parsed = text2.length > 0 ? JSON.parse(text2) : null;
|
|
11030
|
+
} catch {
|
|
11031
|
+
parsed = null;
|
|
11032
|
+
}
|
|
11033
|
+
if (status >= 200 && status < 300 && parsed !== null) done({ kind: "ok", status, body: parsed });
|
|
11034
|
+
else if (status === 401 || status === 400 || status === 404 || status === 413) done({ kind: "refused", status, ...errorFields(parsed) });
|
|
11035
|
+
else done({ kind: "unreachable" });
|
|
11036
|
+
});
|
|
11037
|
+
});
|
|
11038
|
+
const connectTimer = setTimeout(() => {
|
|
11039
|
+
req.destroy();
|
|
11040
|
+
done({ kind: "unreachable" });
|
|
11041
|
+
}, connectTimeout);
|
|
11042
|
+
const responseTimer = setTimeout(() => {
|
|
11043
|
+
req.destroy();
|
|
11044
|
+
done({ kind: "unreachable" });
|
|
11045
|
+
}, connectTimeout + responseTimeout);
|
|
11046
|
+
req.on("socket", (socket) => {
|
|
11047
|
+
socket.once("connect", () => clearTimeout(connectTimer));
|
|
11048
|
+
});
|
|
11049
|
+
req.on("error", () => done({ kind: "unreachable" }));
|
|
11050
|
+
if (payload !== void 0) req.write(payload);
|
|
11051
|
+
req.end();
|
|
11052
|
+
});
|
|
11053
|
+
}
|
|
11054
|
+
async function callPresence(method, path5, body, options = {}) {
|
|
11055
|
+
const endpoint = resolveEndpoint(options);
|
|
11056
|
+
if (endpoint === null) return { kind: "not-configured" };
|
|
11057
|
+
return send(endpoint.info.port, endpoint.token, method, path5, body, options);
|
|
11058
|
+
}
|
|
11059
|
+
|
|
11060
|
+
// src/modules/presence/mcp.ts
|
|
11061
|
+
import { createInterface } from "readline";
|
|
11062
|
+
|
|
11063
|
+
// src/modules/presence/reporter.ts
|
|
11064
|
+
import { basename as basename5, dirname as dirname14 } from "path";
|
|
11065
|
+
|
|
11066
|
+
// src/modules/presence/agent.ts
|
|
11067
|
+
async function v2Profiles(targetDir, homeDir) {
|
|
11068
|
+
try {
|
|
11069
|
+
const { agents } = await collectAgents(targetDir, homeDir === void 0 ? {} : { homeDir });
|
|
11070
|
+
if (agents === null) return [];
|
|
11071
|
+
return agents.entries.map((e) => ({
|
|
11072
|
+
ref: null,
|
|
11073
|
+
slug: e.id,
|
|
11074
|
+
name: e.name,
|
|
11075
|
+
origin: e.origin,
|
|
11076
|
+
definition_revision: e.definition_revision,
|
|
11077
|
+
aliases: [],
|
|
11078
|
+
definition: e.definition
|
|
11079
|
+
}));
|
|
11080
|
+
} catch {
|
|
11081
|
+
return [];
|
|
11082
|
+
}
|
|
11083
|
+
}
|
|
11084
|
+
function matchAgent(profiles, value) {
|
|
11085
|
+
const bySlug = profiles.filter((p) => p.slug === value);
|
|
11086
|
+
if (bySlug.length === 1) return bySlug[0] ?? null;
|
|
11087
|
+
if (bySlug.length > 1) return null;
|
|
11088
|
+
const byAlias = profiles.filter((p) => p.aliases.some((a) => a.value === value));
|
|
11089
|
+
return byAlias.length === 1 ? byAlias[0] ?? null : null;
|
|
11090
|
+
}
|
|
11091
|
+
function defaultAgent(profiles) {
|
|
11092
|
+
const orchestrators = profiles.filter((p) => p.definition === "orchestrator");
|
|
11093
|
+
return orchestrators.length === 1 ? orchestrators[0] ?? null : null;
|
|
11094
|
+
}
|
|
11095
|
+
function toAgentBlock(profile) {
|
|
11096
|
+
if (profile === null) return nullAgentBlock();
|
|
11097
|
+
return {
|
|
11098
|
+
ref: profile.ref,
|
|
11099
|
+
slug: profile.slug,
|
|
11100
|
+
name: profile.name,
|
|
11101
|
+
origin: profile.origin,
|
|
11102
|
+
definition_revision: profile.definition_revision,
|
|
11103
|
+
aliases: profile.aliases.map((a) => ({ value: a.value, context: a.context }))
|
|
11104
|
+
};
|
|
11105
|
+
}
|
|
11106
|
+
async function resolveSessionAgent(targetDir, agentFlag, deps = {}) {
|
|
11107
|
+
const provider = deps.profiles ?? ((dir) => v2Profiles(dir, deps.homeDir));
|
|
11108
|
+
let profiles;
|
|
11109
|
+
try {
|
|
11110
|
+
profiles = await provider(targetDir);
|
|
11111
|
+
} catch {
|
|
11112
|
+
return nullAgentBlock();
|
|
11113
|
+
}
|
|
11114
|
+
return toAgentBlock(agentFlag === void 0 ? defaultAgent(profiles) : matchAgent(profiles, agentFlag));
|
|
11115
|
+
}
|
|
11116
|
+
|
|
11117
|
+
// src/modules/presence/harness.ts
|
|
11118
|
+
var HARNESS_TABLE = [
|
|
11119
|
+
// Observed 2026-11-05 in Claude Code 2.1.81 (desktop app 4.16336.0) on macOS; progress.txt, PS-10.
|
|
11120
|
+
{ harness: "claude-code", variable: "CLAUDECODE", value: "1" },
|
|
11121
|
+
// Observed 2026-11-05 in Codex 0.166.0-alpha.5.1, both the ChatGPT desktop app and `codex exec`; progress.txt.
|
|
11122
|
+
{ harness: "codex", variable: "CODEX_CI", value: "1" },
|
|
11123
|
+
// Observed 2026-11-05 in Copilot agent mode in VS Code 1.143.1; progress.txt.
|
|
11124
|
+
{ harness: "copilot", variable: "COPILOT_AGENT", value: "1" },
|
|
11125
|
+
// Observed 2026-11-05 in the GitHub Copilot desktop app 1.1.34; progress.txt.
|
|
11126
|
+
{ harness: "copilot", variable: "COPILOT_CLI", value: "1" }
|
|
11127
|
+
];
|
|
11128
|
+
function detectHarness(env) {
|
|
11129
|
+
for (const row of HARNESS_TABLE) if (env[row.variable] === row.value) return row.harness;
|
|
11130
|
+
return "other";
|
|
11131
|
+
}
|
|
11132
|
+
|
|
11133
|
+
// src/modules/presence/reporter.ts
|
|
11134
|
+
var NEEDS_YOU_SEPARATOR = " \u2014 ";
|
|
11135
|
+
function parseNeedsYou(line2) {
|
|
11136
|
+
const at = line2.indexOf(NEEDS_YOU_SEPARATOR);
|
|
11137
|
+
if (at < 0) return { action: line2, reason: null };
|
|
11138
|
+
return { action: line2.slice(0, at), reason: line2.slice(at + NEEDS_YOU_SEPARATOR.length) };
|
|
11139
|
+
}
|
|
11140
|
+
async function quiet(fn) {
|
|
11141
|
+
try {
|
|
11142
|
+
return await fn();
|
|
11143
|
+
} catch {
|
|
11144
|
+
return null;
|
|
11145
|
+
}
|
|
11146
|
+
}
|
|
11147
|
+
async function locateCheckout(cwd, deps = {}) {
|
|
11148
|
+
const git = deps.runGit ?? runGit;
|
|
11149
|
+
const out = await quiet(() => git(cwd, ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-dir", "--git-common-dir"]));
|
|
11150
|
+
const lines = out?.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean) ?? [];
|
|
11151
|
+
if (lines.length !== 3) return null;
|
|
11152
|
+
const [top, gitDir, commonDir] = lines;
|
|
11153
|
+
return { checkout: resolveRealPath(top), gitDir: resolveRealPath(gitDir), commonDir: resolveRealPath(commonDir) };
|
|
11154
|
+
}
|
|
11155
|
+
async function locate(cwd, deps) {
|
|
11156
|
+
const found = await locateCheckout(cwd, deps);
|
|
11157
|
+
if (found === null) return null;
|
|
11158
|
+
const git = deps.runGit ?? runGit;
|
|
11159
|
+
const readManifest2 = deps.readManifest ?? readManifest;
|
|
11160
|
+
const [manifest, symbolic, remote] = await Promise.all([
|
|
11161
|
+
quiet(() => readManifest2(found.checkout)),
|
|
11162
|
+
quiet(() => git(found.checkout, ["symbolic-ref", "--short", "-q", "HEAD"])),
|
|
11163
|
+
quiet(() => git(found.checkout, ["remote", "get-url", "origin"]))
|
|
11164
|
+
]);
|
|
11165
|
+
if (manifest === null) return null;
|
|
11166
|
+
let branch = symbolic?.stdout.trim() || null;
|
|
11167
|
+
if (branch === null) branch = (await quiet(() => git(found.checkout, ["rev-parse", "--short", "HEAD"])))?.stdout.trim() || null;
|
|
11168
|
+
const root = basename5(found.commonDir) === ".git" ? dirname14(found.commonDir) : found.checkout;
|
|
11169
|
+
const url = remote?.stdout.trim() ?? "";
|
|
11170
|
+
return {
|
|
11171
|
+
repository: { root, name: basename5(root), remote: url === "" ? null : { provider: remoteProvider(url), url: stripUserinfo(url) } },
|
|
11172
|
+
checkout: { path: found.checkout, branch, linked_worktree: found.gitDir !== found.commonDir }
|
|
11173
|
+
};
|
|
11174
|
+
}
|
|
11175
|
+
function refusedLine(verb, result) {
|
|
11176
|
+
const detail = [result.code, result.field].filter((v) => v !== null).join(" ");
|
|
11177
|
+
return `spell presence ${verb}: the presence service refused it (HTTP ${result.status}${detail === "" ? "" : `, ${detail}`}).`;
|
|
11178
|
+
}
|
|
11179
|
+
async function sendReport(cwd, flags, deps = {}) {
|
|
11180
|
+
const endpoint = resolveEndpoint(deps);
|
|
11181
|
+
if (endpoint === null) return { kind: "silent" };
|
|
11182
|
+
const located = await locate(cwd, deps);
|
|
11183
|
+
if (located === null) return { kind: "silent" };
|
|
11184
|
+
const env = deps.env ?? process.env;
|
|
11185
|
+
const resolveAgent = deps.resolveAgent ?? ((dir, flag) => resolveSessionAgent(dir, flag));
|
|
11186
|
+
const agent2 = await quiet(() => resolveAgent(located.checkout.path, flags.agent)) ?? nullAgentBlock();
|
|
11187
|
+
const harness = flags.harness ?? (deps.detectHarness ?? detectHarness)(env);
|
|
11188
|
+
const body = {
|
|
11189
|
+
status: flags.status,
|
|
11190
|
+
harness,
|
|
11191
|
+
agent: agent2,
|
|
11192
|
+
repository: located.repository,
|
|
11193
|
+
checkout: located.checkout,
|
|
11194
|
+
needs_you: (flags.needsYou ?? []).map(parseNeedsYou)
|
|
11195
|
+
};
|
|
11196
|
+
if (flags.spell !== void 0) body["spell"] = flags.spell;
|
|
11197
|
+
if (flags.task !== void 0) body["task"] = flags.task;
|
|
11198
|
+
if (flags.message !== void 0) body["message"] = flags.message === "" ? null : flags.message;
|
|
11199
|
+
const result = await send(endpoint.info.port, endpoint.token, "POST", "/v0/sessions/report", body, deps);
|
|
11200
|
+
if (result.kind === "ok") return { kind: "sent", record: result.body };
|
|
11201
|
+
if (result.kind === "refused") return { kind: "refused", line: refusedLine("report", result) };
|
|
11202
|
+
return { kind: "silent" };
|
|
11203
|
+
}
|
|
11204
|
+
async function sendEnd(cwd, deps = {}) {
|
|
11205
|
+
const endpoint = resolveEndpoint(deps);
|
|
11206
|
+
if (endpoint === null) return { kind: "silent" };
|
|
11207
|
+
const found = await locateCheckout(cwd, deps);
|
|
11208
|
+
if (found === null) return { kind: "silent" };
|
|
11209
|
+
const result = await send(endpoint.info.port, endpoint.token, "POST", "/v0/sessions/end", { checkout: { path: found.checkout } }, deps);
|
|
11210
|
+
if (result.kind === "ok") return { kind: "sent", record: result.body };
|
|
11211
|
+
if (result.kind === "refused" && result.status !== 404) return { kind: "refused", line: refusedLine("end", result) };
|
|
11212
|
+
return { kind: "silent" };
|
|
11213
|
+
}
|
|
11214
|
+
|
|
11215
|
+
// src/modules/presence/mcp.ts
|
|
11216
|
+
var MODERN_VERSIONS = ["2026-07-28"];
|
|
11217
|
+
var LEGACY_VERSIONS = ["2025-11-25", "2025-06-18", "2024-11-05"];
|
|
11218
|
+
var SUPPORTED_VERSIONS = [...MODERN_VERSIONS, ...LEGACY_VERSIONS];
|
|
11219
|
+
var SERVER_NAME = "arcane-presence";
|
|
11220
|
+
var NOT_CONFIGURED_TEXT = "no presence channel is configured on this machine";
|
|
11221
|
+
var VERSION_KEY = "io.modelcontextprotocol/protocolVersion";
|
|
11222
|
+
var SERVER_INFO_KEY = "io.modelcontextprotocol/serverInfo";
|
|
11223
|
+
var SESSIONS_URI = "arcane://presence/sessions";
|
|
11224
|
+
var CAPABILITIES_URI = "arcane://presence/capabilities";
|
|
11225
|
+
var SERVER_CAPABILITIES = { tools: {}, resources: {} };
|
|
11226
|
+
var LINE = { type: "string", maxLength: 256 };
|
|
11227
|
+
var PRESENCE_TOOLS = [
|
|
11228
|
+
{
|
|
11229
|
+
name: "presence_report",
|
|
11230
|
+
title: "Report presence",
|
|
11231
|
+
description: "Report this session to the local presence service (the same as `spell presence report`). Observation only: it changes what a dashboard shows, nothing else. Answers that no channel is configured when no service runs.",
|
|
11232
|
+
inputSchema: {
|
|
11233
|
+
type: "object",
|
|
11234
|
+
additionalProperties: false,
|
|
11235
|
+
required: ["status"],
|
|
11236
|
+
properties: {
|
|
11237
|
+
status: { type: "string", enum: [...REPORT_STATUSES] },
|
|
11238
|
+
task: { ...LINE, description: "What the session is doing, in one line." },
|
|
11239
|
+
spell: { ...LINE, description: "The spell running, e.g. spell-implement." },
|
|
11240
|
+
message: { ...LINE, description: "A free one-line note; an empty string clears it." },
|
|
11241
|
+
needs_you: { type: "array", maxItems: 20, items: { type: "string" }, description: "Lines of a Needs you block, '<action> \u2014 <reason>'." },
|
|
11242
|
+
agent: { type: "string", description: "The roster agent (a slug or an alias); default: the orchestrator." },
|
|
11243
|
+
harness: { type: "string", enum: [...HARNESSES] },
|
|
11244
|
+
cwd: { type: "string", description: "The checkout to report for; default: the server's working directory." }
|
|
11245
|
+
}
|
|
11246
|
+
}
|
|
11247
|
+
},
|
|
11248
|
+
{
|
|
11249
|
+
name: "presence_end",
|
|
11250
|
+
title: "End presence",
|
|
11251
|
+
description: "End this checkout's session on the local presence service (the same as `spell presence end`).",
|
|
11252
|
+
inputSchema: {
|
|
11253
|
+
type: "object",
|
|
11254
|
+
additionalProperties: false,
|
|
11255
|
+
properties: { cwd: { type: "string", description: "The checkout; default: the server's working directory." } }
|
|
11256
|
+
}
|
|
11257
|
+
}
|
|
11258
|
+
];
|
|
11259
|
+
var PRESENCE_RESOURCES = [
|
|
11260
|
+
{ uri: SESSIONS_URI, name: "presence-sessions", title: "Presence sessions", description: "The GET /v0/sessions document: every active and stale session.", mimeType: "application/json" },
|
|
11261
|
+
{ uri: CAPABILITIES_URI, name: "presence-capabilities", title: "Presence capabilities", description: "The capability descriptor: observation only, no actions.", mimeType: "application/json" }
|
|
11262
|
+
];
|
|
11263
|
+
var RpcError = class extends Error {
|
|
11264
|
+
constructor(code, message, data) {
|
|
11265
|
+
super(message);
|
|
11266
|
+
this.code = code;
|
|
11267
|
+
this.data = data;
|
|
11268
|
+
}
|
|
11269
|
+
code;
|
|
11270
|
+
data;
|
|
11271
|
+
};
|
|
11272
|
+
function isObject(value) {
|
|
11273
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11274
|
+
}
|
|
11275
|
+
function text(value, isError = false) {
|
|
11276
|
+
return { content: [{ type: "text", text: value }], isError };
|
|
11277
|
+
}
|
|
11278
|
+
function stringArg(args, key) {
|
|
11279
|
+
const v = args[key];
|
|
11280
|
+
if (v === void 0) return void 0;
|
|
11281
|
+
if (typeof v !== "string") throw new RpcError(-32602, `${key} must be a string`);
|
|
11282
|
+
return v;
|
|
11283
|
+
}
|
|
11284
|
+
function serveMcp(options) {
|
|
11285
|
+
const deps = options.deps ?? {};
|
|
11286
|
+
const cwd = options.cwd ?? process.cwd();
|
|
11287
|
+
const log = options.log ?? ((line2) => process.stderr.write(`${line2}
|
|
11288
|
+
`));
|
|
11289
|
+
const write = (message) => {
|
|
11290
|
+
options.output.write(`${JSON.stringify(message)}
|
|
11291
|
+
`);
|
|
11292
|
+
};
|
|
11293
|
+
const reply = (id, result) => write({ jsonrpc: "2.0", id, result });
|
|
11294
|
+
const fail = (id, error2) => write({ jsonrpc: "2.0", id, error: error2 });
|
|
11295
|
+
const report = async (args) => {
|
|
11296
|
+
const status = stringArg(args, "status");
|
|
11297
|
+
if (status === void 0) return text("status is required: one of working, waiting, blocked, idle.", true);
|
|
11298
|
+
const needs = args["needs_you"];
|
|
11299
|
+
if (needs !== void 0 && (!Array.isArray(needs) || needs.some((n) => typeof n !== "string"))) {
|
|
11300
|
+
return text("needs_you must be a list of strings.", true);
|
|
11301
|
+
}
|
|
11302
|
+
const flags = { status };
|
|
11303
|
+
for (const key of ["task", "spell", "message", "agent", "harness"]) {
|
|
11304
|
+
const v = stringArg(args, key);
|
|
11305
|
+
if (v !== void 0) flags[key] = v;
|
|
11306
|
+
}
|
|
11307
|
+
if (Array.isArray(needs)) flags.needsYou = needs;
|
|
11308
|
+
if (resolveEndpoint(deps) === null) return text(NOT_CONFIGURED_TEXT);
|
|
11309
|
+
const where = stringArg(args, "cwd") ?? cwd;
|
|
11310
|
+
const outcome = await sendReport(where, flags, deps);
|
|
11311
|
+
if (outcome.kind === "refused") return text(outcome.line, true);
|
|
11312
|
+
if (outcome.kind === "silent") return text(`Nothing was reported: ${where} is not inside an Arcane repository, or the service did not answer.`);
|
|
11313
|
+
const record = outcome.record;
|
|
11314
|
+
return { ...text(`Reported ${record.status} for ${record.session}.`), structuredContent: record };
|
|
11315
|
+
};
|
|
11316
|
+
const end = async (args) => {
|
|
11317
|
+
if (resolveEndpoint(deps) === null) return text(NOT_CONFIGURED_TEXT);
|
|
11318
|
+
const where = stringArg(args, "cwd") ?? cwd;
|
|
11319
|
+
const outcome = await sendEnd(where, deps);
|
|
11320
|
+
if (outcome.kind === "refused") return text(outcome.line, true);
|
|
11321
|
+
if (outcome.kind === "silent") return text(`No session was open for ${where}.`);
|
|
11322
|
+
return { ...text(`Ended ${outcome.record.session}.`), structuredContent: outcome.record };
|
|
11323
|
+
};
|
|
11324
|
+
const read = async (uri) => {
|
|
11325
|
+
const resource = PRESENCE_RESOURCES.find((r) => r.uri === uri);
|
|
11326
|
+
if (resource === void 0) throw new RpcError(-32602, "Resource not found", { uri });
|
|
11327
|
+
const result = await callPresence("GET", resource.uri === SESSIONS_URI ? "/v0/sessions" : "/v0/health", void 0, deps);
|
|
11328
|
+
if (result.kind !== "ok") return { contents: [{ uri: resource.uri, mimeType: "text/plain", text: NOT_CONFIGURED_TEXT }] };
|
|
11329
|
+
const body = resource.uri === SESSIONS_URI ? result.body : result.body.capabilities;
|
|
11330
|
+
return { contents: [{ uri: resource.uri, mimeType: "application/json", text: JSON.stringify(body) }] };
|
|
11331
|
+
};
|
|
11332
|
+
const handle = async (method, params) => {
|
|
11333
|
+
switch (method) {
|
|
11334
|
+
case "ping":
|
|
11335
|
+
return {};
|
|
11336
|
+
case "tools/list":
|
|
11337
|
+
return { tools: PRESENCE_TOOLS };
|
|
11338
|
+
case "tools/call": {
|
|
11339
|
+
const name = params["name"];
|
|
11340
|
+
const args = isObject(params["arguments"]) ? params["arguments"] : {};
|
|
11341
|
+
try {
|
|
11342
|
+
if (name === "presence_report") return await report(args);
|
|
11343
|
+
if (name === "presence_end") return await end(args);
|
|
11344
|
+
} catch (error2) {
|
|
11345
|
+
if (error2 instanceof RpcError && error2.code === -32602) return text(error2.message, true);
|
|
11346
|
+
throw error2;
|
|
11347
|
+
}
|
|
11348
|
+
throw new RpcError(-32602, `Unknown tool: ${String(name)}`);
|
|
11349
|
+
}
|
|
11350
|
+
case "resources/list":
|
|
11351
|
+
return { resources: PRESENCE_RESOURCES };
|
|
11352
|
+
case "resources/read":
|
|
11353
|
+
return read(params["uri"]);
|
|
11354
|
+
default:
|
|
11355
|
+
throw new RpcError(-32601, `Method not found: ${method}`);
|
|
11356
|
+
}
|
|
11357
|
+
};
|
|
11358
|
+
const dispatch = async (raw) => {
|
|
11359
|
+
let message;
|
|
11360
|
+
try {
|
|
11361
|
+
message = JSON.parse(raw);
|
|
11362
|
+
} catch {
|
|
11363
|
+
fail(null, { code: -32700, message: "Parse error" });
|
|
11364
|
+
return;
|
|
11365
|
+
}
|
|
11366
|
+
if (!isObject(message) || message["jsonrpc"] !== "2.0" || typeof message["method"] !== "string") {
|
|
11367
|
+
const id2 = isObject(message) && (typeof message["id"] === "string" || typeof message["id"] === "number") ? message["id"] : null;
|
|
11368
|
+
fail(id2, { code: -32600, message: "Invalid Request" });
|
|
11369
|
+
return;
|
|
11370
|
+
}
|
|
11371
|
+
const method = message["method"];
|
|
11372
|
+
const hasId = "id" in message;
|
|
11373
|
+
const rawId = message["id"];
|
|
11374
|
+
if (hasId && rawId !== null && typeof rawId !== "string" && typeof rawId !== "number") {
|
|
11375
|
+
fail(null, { code: -32600, message: "Invalid Request" });
|
|
11376
|
+
return;
|
|
11377
|
+
}
|
|
11378
|
+
const id = rawId ?? null;
|
|
11379
|
+
const params = isObject(message["params"]) ? message["params"] : {};
|
|
11380
|
+
if (!hasId) return;
|
|
11381
|
+
try {
|
|
11382
|
+
if (method === "initialize") {
|
|
11383
|
+
const requested2 = params["protocolVersion"];
|
|
11384
|
+
const protocolVersion = typeof requested2 === "string" && LEGACY_VERSIONS.includes(requested2) ? requested2 : LEGACY_VERSIONS[0];
|
|
11385
|
+
reply(id, { protocolVersion, capabilities: SERVER_CAPABILITIES, serverInfo: { name: SERVER_NAME, version: options.version } });
|
|
11386
|
+
return;
|
|
11387
|
+
}
|
|
11388
|
+
const meta = isObject(params["_meta"]) ? params["_meta"] : {};
|
|
11389
|
+
const requested = meta[VERSION_KEY];
|
|
11390
|
+
const modern = typeof requested === "string" && MODERN_VERSIONS.includes(requested);
|
|
11391
|
+
if (requested !== void 0 && (typeof requested !== "string" || !SUPPORTED_VERSIONS.includes(requested))) {
|
|
11392
|
+
fail(id, { code: -32022, message: "Unsupported protocol version", data: { supported: SUPPORTED_VERSIONS, requested } });
|
|
11393
|
+
return;
|
|
11394
|
+
}
|
|
11395
|
+
if (method === "server/discover") {
|
|
11396
|
+
reply(id, { resultType: "complete", supportedVersions: SUPPORTED_VERSIONS, capabilities: SERVER_CAPABILITIES, _meta: { [SERVER_INFO_KEY]: { name: SERVER_NAME, version: options.version } } });
|
|
11397
|
+
return;
|
|
11398
|
+
}
|
|
11399
|
+
const result = await handle(method, params);
|
|
11400
|
+
reply(id, modern ? { resultType: "complete", ...result } : result);
|
|
11401
|
+
} catch (error2) {
|
|
11402
|
+
if (error2 instanceof RpcError) {
|
|
11403
|
+
fail(id, error2.data === void 0 ? { code: error2.code, message: error2.message } : { code: error2.code, message: error2.message, data: error2.data });
|
|
11404
|
+
} else {
|
|
11405
|
+
log(`spell presence mcp: internal error on ${method}`);
|
|
11406
|
+
fail(id, { code: -32603, message: "Internal error" });
|
|
11407
|
+
}
|
|
11408
|
+
}
|
|
11409
|
+
};
|
|
11410
|
+
return new Promise((resolve10) => {
|
|
11411
|
+
const lines = createInterface({ input: options.input, crlfDelay: Infinity });
|
|
11412
|
+
let chain = Promise.resolve();
|
|
11413
|
+
lines.on("line", (line2) => {
|
|
11414
|
+
if (line2.trim() === "") return;
|
|
11415
|
+
chain = chain.then(() => dispatch(line2));
|
|
11416
|
+
});
|
|
11417
|
+
lines.on("close", () => {
|
|
11418
|
+
void chain.then(() => resolve10());
|
|
11419
|
+
});
|
|
11420
|
+
});
|
|
11421
|
+
}
|
|
11422
|
+
|
|
11423
|
+
// src/modules/presence/log.ts
|
|
11424
|
+
var EVENT = /^[a-z]+(\.[a-z_]+)*$/;
|
|
11425
|
+
var SESSION = /^arcane:session:[0-9a-f-]{36}$/;
|
|
11426
|
+
var STATUS = /^[a-z]+$/;
|
|
11427
|
+
var sink = (line2) => {
|
|
11428
|
+
process.stderr.write(`${line2}
|
|
11429
|
+
`);
|
|
11430
|
+
};
|
|
11431
|
+
function logEvent(event, session, status) {
|
|
11432
|
+
const e = EVENT.test(event) ? event : "-";
|
|
11433
|
+
const s = session !== null && SESSION.test(session) ? session : "-";
|
|
11434
|
+
const st = status !== null && STATUS.test(status) ? status : "-";
|
|
11435
|
+
sink(`${(/* @__PURE__ */ new Date()).toISOString()} presence ${e} ${s} ${st}`);
|
|
11436
|
+
}
|
|
11437
|
+
|
|
11438
|
+
// src/modules/presence/server.ts
|
|
11439
|
+
import { createServer as createServer2 } from "http";
|
|
11440
|
+
|
|
11441
|
+
// src/modules/presence/auth.ts
|
|
11442
|
+
import { createHash as createHash6, timingSafeEqual } from "crypto";
|
|
11443
|
+
var BEARER = /^Bearer ([^\s]+)$/;
|
|
11444
|
+
function digest(value) {
|
|
11445
|
+
return createHash6("sha256").update(value, "utf8").digest();
|
|
11446
|
+
}
|
|
11447
|
+
function checkBearer(header, token) {
|
|
11448
|
+
if (typeof header !== "string" || token.length === 0) return false;
|
|
11449
|
+
const match2 = BEARER.exec(header);
|
|
11450
|
+
if (match2 === null) return false;
|
|
11451
|
+
return timingSafeEqual(digest(match2[1]), digest(token));
|
|
11452
|
+
}
|
|
11453
|
+
|
|
11454
|
+
// src/modules/presence/validate.ts
|
|
11455
|
+
var Refusal = class extends Error {
|
|
11456
|
+
constructor(code, field, message) {
|
|
11457
|
+
super(message);
|
|
11458
|
+
this.code = code;
|
|
11459
|
+
this.field = field;
|
|
11460
|
+
}
|
|
11461
|
+
code;
|
|
11462
|
+
field;
|
|
11463
|
+
};
|
|
11464
|
+
var AGENT_REF = /^arcane:agent:[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
11465
|
+
var REVISION = /^sha256:[0-9a-f]{64}$/;
|
|
11466
|
+
var CONTROL = new RegExp("[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]", "g");
|
|
11467
|
+
function oneLine(value) {
|
|
11468
|
+
const flat = value.replace(CONTROL, " ");
|
|
11469
|
+
const points = Array.from(flat);
|
|
11470
|
+
return points.length > MAX_LINE_CHARS ? points.slice(0, MAX_LINE_CHARS).join("") : flat;
|
|
11471
|
+
}
|
|
11472
|
+
function isObject2(value) {
|
|
11473
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11474
|
+
}
|
|
11475
|
+
function closed(value, field, required, optional = []) {
|
|
11476
|
+
if (!isObject2(value)) throw new Refusal("invalid_field", field, `${field ?? "the body"} must be an object`);
|
|
11477
|
+
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
11478
|
+
for (const key of Object.keys(value)) {
|
|
11479
|
+
if (!allowed.has(key)) throw new Refusal("unknown_field", join30(field, key), `${join30(field, key)} is not a field of this contract`);
|
|
11480
|
+
}
|
|
11481
|
+
for (const key of required) {
|
|
11482
|
+
if (!(key in value)) throw new Refusal("missing_field", join30(field, key), `${join30(field, key)} is required`);
|
|
11483
|
+
}
|
|
11484
|
+
return value;
|
|
11485
|
+
}
|
|
11486
|
+
function join30(parent, key) {
|
|
11487
|
+
return parent === void 0 ? key : `${parent}.${key}`;
|
|
11488
|
+
}
|
|
11489
|
+
function nonEmpty(value, field) {
|
|
11490
|
+
if (typeof value !== "string" || value.length === 0) throw new Refusal("invalid_field", field, `${field} must be a non-empty string`);
|
|
11491
|
+
return value;
|
|
11492
|
+
}
|
|
11493
|
+
function line(value, field, nullable) {
|
|
11494
|
+
if (value === null && nullable) return null;
|
|
11495
|
+
if (typeof value !== "string") throw new Refusal("invalid_field", field, `${field} must be a string${nullable ? " or null" : ""}`);
|
|
11496
|
+
return oneLine(value);
|
|
11497
|
+
}
|
|
11498
|
+
function nonEmptyLine(value, field) {
|
|
11499
|
+
const out = line(value, field, false);
|
|
11500
|
+
if (out.trim().length === 0) throw new Refusal("invalid_field", field, `${field} must not be empty`);
|
|
11501
|
+
return out;
|
|
11502
|
+
}
|
|
11503
|
+
function oneOf(value, allowed, field) {
|
|
11504
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
11505
|
+
throw new Refusal("invalid_field", field, `${field} must be one of ${allowed.join(", ")}`);
|
|
11506
|
+
}
|
|
11507
|
+
return value;
|
|
11508
|
+
}
|
|
11509
|
+
function agent(value) {
|
|
11510
|
+
const a = closed(value, "agent", ["ref", "slug", "name", "origin", "definition_revision", "aliases"]);
|
|
11511
|
+
if (a["ref"] !== null && (typeof a["ref"] !== "string" || !AGENT_REF.test(a["ref"]))) {
|
|
11512
|
+
throw new Refusal("invalid_field", "agent.ref", "agent.ref must be an arcane:agent: reference or null");
|
|
11513
|
+
}
|
|
11514
|
+
if (a["origin"] !== null && a["origin"] !== "repo" && a["origin"] !== "user") {
|
|
11515
|
+
throw new Refusal("invalid_field", "agent.origin", "agent.origin must be repo, user or null");
|
|
11516
|
+
}
|
|
11517
|
+
if (a["definition_revision"] !== null && (typeof a["definition_revision"] !== "string" || !REVISION.test(a["definition_revision"]))) {
|
|
11518
|
+
throw new Refusal("invalid_field", "agent.definition_revision", "agent.definition_revision must be sha256:<64 hex> or null");
|
|
11519
|
+
}
|
|
11520
|
+
if (!Array.isArray(a["aliases"])) throw new Refusal("invalid_field", "agent.aliases", "agent.aliases must be a list");
|
|
11521
|
+
if (a["aliases"].length > MAX_ALIASES) throw new Refusal("invalid_field", "agent.aliases", `agent.aliases carries at most ${MAX_ALIASES} entries`);
|
|
11522
|
+
const aliases = a["aliases"].map((item, i) => {
|
|
11523
|
+
const field = `agent.aliases.${i}`;
|
|
11524
|
+
const alias = closed(item, field, ["value", "context"]);
|
|
11525
|
+
return { value: nonEmptyLine(alias["value"], `${field}.value`), context: oneOf(alias["context"], ALIAS_CONTEXTS, `${field}.context`) };
|
|
11526
|
+
});
|
|
11527
|
+
return {
|
|
11528
|
+
ref: a["ref"],
|
|
11529
|
+
slug: line(a["slug"], "agent.slug", true),
|
|
11530
|
+
name: line(a["name"], "agent.name", true),
|
|
11531
|
+
origin: a["origin"],
|
|
11532
|
+
definition_revision: a["definition_revision"],
|
|
11533
|
+
aliases
|
|
11534
|
+
};
|
|
11535
|
+
}
|
|
11536
|
+
function repository(value) {
|
|
11537
|
+
const r = closed(value, "repository", ["root", "name", "remote"]);
|
|
11538
|
+
let remote = null;
|
|
11539
|
+
if (r["remote"] !== null) {
|
|
11540
|
+
const rem = closed(r["remote"], "repository.remote", ["provider", "url"]);
|
|
11541
|
+
if (typeof rem["url"] !== "string") throw new Refusal("invalid_field", "repository.remote.url", "repository.remote.url must be a string");
|
|
11542
|
+
remote = { provider: oneOf(rem["provider"], ["github", "ado", "other"], "repository.remote.provider"), url: rem["url"] };
|
|
11543
|
+
}
|
|
11544
|
+
return { root: nonEmpty(r["root"], "repository.root"), name: nonEmptyLine(r["name"], "repository.name"), remote };
|
|
11545
|
+
}
|
|
11546
|
+
function checkout(value) {
|
|
11547
|
+
const c = closed(value, "checkout", ["path", "branch", "linked_worktree"]);
|
|
11548
|
+
if (typeof c["linked_worktree"] !== "boolean") {
|
|
11549
|
+
throw new Refusal("invalid_field", "checkout.linked_worktree", "checkout.linked_worktree must be true or false");
|
|
11550
|
+
}
|
|
11551
|
+
return { path: nonEmpty(c["path"], "checkout.path"), branch: line(c["branch"], "checkout.branch", true), linked_worktree: c["linked_worktree"] };
|
|
11552
|
+
}
|
|
11553
|
+
function needsYou(value) {
|
|
11554
|
+
if (!Array.isArray(value)) throw new Refusal("invalid_field", "needs_you", "needs_you must be a list");
|
|
11555
|
+
if (value.length > MAX_NEEDS_YOU) {
|
|
11556
|
+
throw new Refusal("too_many_lines", "needs_you", `needs_you carries at most ${MAX_NEEDS_YOU} lines; ${value.length} were sent`);
|
|
11557
|
+
}
|
|
11558
|
+
return value.map((item, i) => {
|
|
11559
|
+
const field = `needs_you.${i}`;
|
|
11560
|
+
const n = closed(item, field, ["action", "reason"]);
|
|
11561
|
+
return { action: nonEmptyLine(n["action"], `${field}.action`), reason: line(n["reason"], `${field}.reason`, true) };
|
|
11562
|
+
});
|
|
11563
|
+
}
|
|
11564
|
+
function refuse(fn) {
|
|
11565
|
+
try {
|
|
11566
|
+
return { ok: true, value: fn() };
|
|
11567
|
+
} catch (error2) {
|
|
11568
|
+
if (error2 instanceof Refusal) {
|
|
11569
|
+
return error2.field === void 0 ? { ok: false, code: error2.code, message: error2.message } : { ok: false, code: error2.code, field: error2.field, message: error2.message };
|
|
11570
|
+
}
|
|
11571
|
+
throw error2;
|
|
11572
|
+
}
|
|
11573
|
+
}
|
|
11574
|
+
function validateReport(value) {
|
|
11575
|
+
return refuse(() => {
|
|
11576
|
+
const b = closed(value, void 0, ["status", "harness", "agent", "repository", "checkout"], ["spell", "task", "message", "needs_you"]);
|
|
11577
|
+
if (b["status"] === "done") {
|
|
11578
|
+
throw new Refusal("invalid_field", "status", "status done is set by ending a session: use POST /v0/sessions/end (spell presence end)");
|
|
11579
|
+
}
|
|
11580
|
+
const out = {
|
|
11581
|
+
status: oneOf(b["status"], REPORT_STATUSES, "status"),
|
|
11582
|
+
harness: oneOf(b["harness"], HARNESSES, "harness"),
|
|
11583
|
+
agent: agent(b["agent"]),
|
|
11584
|
+
repository: repository(b["repository"]),
|
|
11585
|
+
checkout: checkout(b["checkout"])
|
|
11586
|
+
};
|
|
11587
|
+
for (const key of ["spell", "task", "message"]) {
|
|
11588
|
+
if (key in b) out[key] = line(b[key], key, true);
|
|
11589
|
+
}
|
|
11590
|
+
if ("needs_you" in b) out.needs_you = needsYou(b["needs_you"]);
|
|
11591
|
+
return out;
|
|
11592
|
+
});
|
|
11593
|
+
}
|
|
11594
|
+
function validateEnd(value) {
|
|
11595
|
+
return refuse(() => {
|
|
11596
|
+
const b = closed(value, void 0, ["checkout"]);
|
|
11597
|
+
const c = closed(b["checkout"], "checkout", ["path"]);
|
|
11598
|
+
return { checkout: { path: nonEmpty(c["path"], "checkout.path") } };
|
|
11599
|
+
});
|
|
11600
|
+
}
|
|
11601
|
+
|
|
11602
|
+
// src/modules/presence/server.ts
|
|
11603
|
+
var PRESENCE_ROUTES = [
|
|
11604
|
+
["GET", "/v0/health"],
|
|
11605
|
+
["POST", "/v0/sessions/report"],
|
|
11606
|
+
["POST", "/v0/sessions/end"],
|
|
11607
|
+
["GET", "/v0/sessions"],
|
|
11608
|
+
["GET", "/v0/sessions/{session}"],
|
|
11609
|
+
["GET", "/v0/events"]
|
|
11610
|
+
];
|
|
11611
|
+
var JSON_HEADERS = {
|
|
11612
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
11613
|
+
"Cache-Control": "no-store",
|
|
11614
|
+
"X-Content-Type-Options": "nosniff"
|
|
11615
|
+
};
|
|
11616
|
+
function send2(res, status, body, extra = {}) {
|
|
11617
|
+
const text2 = `${JSON.stringify(body)}
|
|
11618
|
+
`;
|
|
11619
|
+
res.writeHead(status, { ...JSON_HEADERS, "Content-Length": Buffer.byteLength(text2), ...extra });
|
|
11620
|
+
res.end(text2);
|
|
11621
|
+
}
|
|
11622
|
+
function error(res, status, code, message, field, extra = {}) {
|
|
11623
|
+
const body = { error: field === void 0 ? { code, message } : { code, field, message } };
|
|
11624
|
+
send2(res, status, body, extra);
|
|
11625
|
+
}
|
|
11626
|
+
function unauthorized(res) {
|
|
11627
|
+
res.writeHead(401, { "Content-Length": 0, "WWW-Authenticate": "Bearer", "Cache-Control": "no-store", Connection: "close" });
|
|
11628
|
+
res.end();
|
|
11629
|
+
}
|
|
11630
|
+
function readBody(req, res) {
|
|
11631
|
+
return new Promise((resolve10) => {
|
|
11632
|
+
const tooLarge = () => {
|
|
11633
|
+
error(res, 413, "too_large", `bodies are limited to ${MAX_BODY_BYTES} bytes`, void 0, { Connection: "close" });
|
|
11634
|
+
req.resume();
|
|
11635
|
+
resolve10(null);
|
|
11636
|
+
};
|
|
11637
|
+
const declared = Number(req.headers["content-length"] ?? "0");
|
|
11638
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
11639
|
+
tooLarge();
|
|
11640
|
+
return;
|
|
11641
|
+
}
|
|
11642
|
+
const chunks = [];
|
|
11643
|
+
let size = 0;
|
|
11644
|
+
let done = false;
|
|
11645
|
+
req.on("data", (chunk) => {
|
|
11646
|
+
if (done) return;
|
|
11647
|
+
size += chunk.length;
|
|
11648
|
+
if (size > MAX_BODY_BYTES) {
|
|
11649
|
+
done = true;
|
|
11650
|
+
tooLarge();
|
|
11651
|
+
return;
|
|
11652
|
+
}
|
|
11653
|
+
chunks.push(chunk);
|
|
11654
|
+
});
|
|
11655
|
+
req.on("end", () => {
|
|
11656
|
+
if (done) return;
|
|
11657
|
+
done = true;
|
|
11658
|
+
resolve10(Buffer.concat(chunks).toString("utf8"));
|
|
11659
|
+
});
|
|
11660
|
+
req.on("error", () => {
|
|
11661
|
+
if (done) return;
|
|
11662
|
+
done = true;
|
|
11663
|
+
resolve10(null);
|
|
11664
|
+
});
|
|
11665
|
+
});
|
|
11666
|
+
}
|
|
11667
|
+
function parseJson(text2) {
|
|
11668
|
+
try {
|
|
11669
|
+
return { ok: true, value: JSON.parse(text2) };
|
|
11670
|
+
} catch {
|
|
11671
|
+
return { ok: false };
|
|
11672
|
+
}
|
|
11673
|
+
}
|
|
11674
|
+
function match(method, pathname) {
|
|
11675
|
+
const allow = [];
|
|
11676
|
+
for (const route of PRESENCE_ROUTES) {
|
|
11677
|
+
const [m, pattern] = route;
|
|
11678
|
+
let param;
|
|
11679
|
+
let hit = pattern === pathname;
|
|
11680
|
+
if (!hit && pattern.endsWith("/{session}")) {
|
|
11681
|
+
const prefix = pattern.slice(0, -"{session}".length);
|
|
11682
|
+
const rest = pathname.startsWith(prefix) ? pathname.slice(prefix.length) : "";
|
|
11683
|
+
if (rest.length > 0 && !rest.includes("/") && !PRESENCE_ROUTES.some(([, p]) => p === pathname)) {
|
|
11684
|
+
hit = true;
|
|
11685
|
+
try {
|
|
11686
|
+
param = decodeURIComponent(rest);
|
|
11687
|
+
} catch {
|
|
11688
|
+
param = "";
|
|
11689
|
+
}
|
|
11690
|
+
}
|
|
11691
|
+
}
|
|
11692
|
+
if (!hit) continue;
|
|
11693
|
+
if (m === method) return param === void 0 ? { route } : { route, param };
|
|
11694
|
+
allow.push(m);
|
|
11695
|
+
}
|
|
11696
|
+
return allow.length > 0 ? { allow } : null;
|
|
11697
|
+
}
|
|
11698
|
+
function createPresenceServer(options) {
|
|
11699
|
+
const now = options.now ?? Date.now;
|
|
11700
|
+
const keepaliveMs = options.keepaliveMs ?? KEEPALIVE_MS;
|
|
11701
|
+
const maxSubscribers = options.maxSubscribers ?? MAX_SUBSCRIBERS;
|
|
11702
|
+
const { table } = options;
|
|
11703
|
+
const token = typeof options.token === "function" ? options.token : () => options.token;
|
|
11704
|
+
const feeds = /* @__PURE__ */ new Set();
|
|
11705
|
+
table.listen((e) => logEvent(e.event, e.data.session, e.data.status));
|
|
11706
|
+
const health = () => ({
|
|
11707
|
+
ok: true,
|
|
11708
|
+
generator: options.generator,
|
|
11709
|
+
contract: PRESENCE_CONTRACT,
|
|
11710
|
+
capabilities: presenceCapabilities(),
|
|
11711
|
+
sessions: table.count(),
|
|
11712
|
+
started_at: options.startedAt
|
|
11713
|
+
});
|
|
11714
|
+
const snapshot = () => ({
|
|
11715
|
+
capabilities: presenceCapabilities(),
|
|
11716
|
+
generated_at: new Date(now()).toISOString(),
|
|
11717
|
+
sessions: table.snapshot()
|
|
11718
|
+
});
|
|
11719
|
+
const writeEvent = (res, name, data) => {
|
|
11720
|
+
if (res.writableLength > MAX_SUBSCRIBER_BUFFER_BYTES) {
|
|
11721
|
+
res.destroy();
|
|
11722
|
+
feeds.delete(res);
|
|
11723
|
+
return;
|
|
11724
|
+
}
|
|
11725
|
+
res.write(`event: ${name}
|
|
11726
|
+
data: ${JSON.stringify(data)}
|
|
11727
|
+
|
|
11728
|
+
`);
|
|
11729
|
+
};
|
|
11730
|
+
table.listen((e) => {
|
|
11731
|
+
for (const feed of feeds) writeEvent(feed, e.event, e.data);
|
|
11732
|
+
});
|
|
11733
|
+
const events = (req, res) => {
|
|
11734
|
+
if (feeds.size >= maxSubscribers) {
|
|
11735
|
+
error(res, 503, "too_many_subscribers", `at most ${maxSubscribers} event feeds are served at once`, void 0, { "Retry-After": "5" });
|
|
11736
|
+
return;
|
|
11737
|
+
}
|
|
11738
|
+
res.writeHead(200, {
|
|
11739
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
11740
|
+
"Cache-Control": "no-store",
|
|
11741
|
+
"X-Content-Type-Options": "nosniff",
|
|
11742
|
+
Connection: "keep-alive"
|
|
11743
|
+
});
|
|
11744
|
+
feeds.add(res);
|
|
11745
|
+
const hello = { contract: PRESENCE_CONTRACT, capabilities: presenceCapabilities(), sessions: table.snapshot() };
|
|
11746
|
+
res.write(`event: ${HELLO_EVENT}
|
|
11747
|
+
data: ${JSON.stringify(hello)}
|
|
11748
|
+
|
|
11749
|
+
`);
|
|
11750
|
+
const keepalive = setInterval(() => res.write(": keepalive\n\n"), keepaliveMs);
|
|
11751
|
+
keepalive.unref();
|
|
11752
|
+
const drop = () => {
|
|
11753
|
+
clearInterval(keepalive);
|
|
11754
|
+
feeds.delete(res);
|
|
11755
|
+
};
|
|
11756
|
+
req.on("close", drop);
|
|
11757
|
+
res.on("close", drop);
|
|
11758
|
+
};
|
|
11759
|
+
const handle = async (req, res) => {
|
|
11760
|
+
if (!checkBearer(req.headers.authorization, token())) {
|
|
11761
|
+
unauthorized(res);
|
|
11762
|
+
req.resume();
|
|
11763
|
+
return;
|
|
11764
|
+
}
|
|
11765
|
+
const pathname = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
11766
|
+
const found = match(req.method ?? "GET", pathname);
|
|
11767
|
+
if (found === null) {
|
|
11768
|
+
error(res, 404, "not_found", "no such route");
|
|
11769
|
+
req.resume();
|
|
11770
|
+
return;
|
|
11771
|
+
}
|
|
11772
|
+
if ("allow" in found) {
|
|
11773
|
+
error(res, 405, "method_not_allowed", `use ${found.allow.join(", ")}`, void 0, { Allow: found.allow.join(", ") });
|
|
11774
|
+
req.resume();
|
|
11775
|
+
return;
|
|
11776
|
+
}
|
|
11777
|
+
const path5 = found.route[1];
|
|
11778
|
+
if (path5 === "/v0/health") return send2(res, 200, health());
|
|
11779
|
+
if (path5 === "/v0/sessions") return send2(res, 200, snapshot());
|
|
11780
|
+
if (path5 === "/v0/events") return events(req, res);
|
|
11781
|
+
if (path5 === "/v0/sessions/{session}") {
|
|
11782
|
+
const parsed = parseRef(found.param, "session");
|
|
11783
|
+
const record = parsed.ok ? table.get(found.param ?? "") : null;
|
|
11784
|
+
if (record === null) return error(res, 404, "not_found", "no such session");
|
|
11785
|
+
return send2(res, 200, record);
|
|
11786
|
+
}
|
|
11787
|
+
const text2 = await readBody(req, res);
|
|
11788
|
+
if (text2 === null) return;
|
|
11789
|
+
const json = parseJson(text2);
|
|
11790
|
+
if (!json.ok) return error(res, 400, "invalid_json", "the body is not valid JSON");
|
|
11791
|
+
if (path5 === "/v0/sessions/report") {
|
|
11792
|
+
const result2 = validateReport(json.value);
|
|
11793
|
+
if (!result2.ok) return error(res, 400, result2.code, result2.message, result2.field);
|
|
11794
|
+
const isNew = table.snapshot().every((r) => r.checkout.path !== result2.value.checkout.path);
|
|
11795
|
+
if (isNew && table.count() >= MAX_SESSIONS) {
|
|
11796
|
+
return error(res, 503, "too_many_sessions", `at most ${MAX_SESSIONS} sessions are held at once`, void 0, { "Retry-After": "60" });
|
|
11797
|
+
}
|
|
11798
|
+
const { record, created } = table.report(result2.value);
|
|
11799
|
+
return send2(res, created ? 201 : 200, record);
|
|
11800
|
+
}
|
|
11801
|
+
const result = validateEnd(json.value);
|
|
11802
|
+
if (!result.ok) return error(res, 400, result.code, result.message, result.field);
|
|
11803
|
+
const ended = table.end(result.value.checkout.path);
|
|
11804
|
+
if (ended === null) return error(res, 404, "not_found", "no session reports from that checkout", "checkout.path");
|
|
11805
|
+
return send2(res, 200, ended);
|
|
11806
|
+
};
|
|
11807
|
+
const server = createServer2({ connectionsCheckingInterval: CONNECTIONS_CHECK_MS }, (req, res) => {
|
|
11808
|
+
handle(req, res).catch(() => {
|
|
11809
|
+
if (!res.headersSent) error(res, 500, "internal", "the service could not answer");
|
|
11810
|
+
else res.destroy();
|
|
11811
|
+
});
|
|
11812
|
+
});
|
|
11813
|
+
server.maxConnections = MAX_CONNECTIONS;
|
|
11814
|
+
server.headersTimeout = HEADERS_TIMEOUT_MS;
|
|
11815
|
+
server.requestTimeout = REQUEST_TIMEOUT_MS;
|
|
11816
|
+
return {
|
|
11817
|
+
server,
|
|
11818
|
+
closeFeeds() {
|
|
11819
|
+
for (const feed of feeds) feed.end();
|
|
11820
|
+
feeds.clear();
|
|
11821
|
+
},
|
|
11822
|
+
subscriberCount() {
|
|
11823
|
+
return feeds.size;
|
|
11824
|
+
}
|
|
11825
|
+
};
|
|
11826
|
+
}
|
|
11827
|
+
|
|
11828
|
+
// src/modules/presence/sessions.ts
|
|
11829
|
+
function copy(record) {
|
|
11830
|
+
return structuredClone(record);
|
|
11831
|
+
}
|
|
11832
|
+
function sameNeedsYou(a, b) {
|
|
11833
|
+
if (a.length !== b.length) return false;
|
|
11834
|
+
return a.every((item, i) => item.action === b[i]?.action && item.reason === b[i]?.reason);
|
|
11835
|
+
}
|
|
11836
|
+
function byStart(a, b) {
|
|
11837
|
+
if (a.started_at !== b.started_at) return a.started_at < b.started_at ? -1 : 1;
|
|
11838
|
+
if (a.session === b.session) return 0;
|
|
11839
|
+
return a.session < b.session ? -1 : 1;
|
|
11840
|
+
}
|
|
11841
|
+
function createSessionTable(options) {
|
|
11842
|
+
const now = options.now ?? Date.now;
|
|
11843
|
+
const staleAfterMs = options.staleAfterMs ?? STALE_AFTER_MS;
|
|
11844
|
+
const expireAfterMs = options.expireAfterMs ?? EXPIRE_AFTER_MS;
|
|
11845
|
+
const entries = /* @__PURE__ */ new Map();
|
|
11846
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
11847
|
+
const emit = (name, record) => {
|
|
11848
|
+
const event = { event: name, data: copy(record) };
|
|
11849
|
+
for (const listener of listeners) listener({ event: name, data: copy(record) });
|
|
11850
|
+
return event;
|
|
11851
|
+
};
|
|
11852
|
+
return {
|
|
11853
|
+
report(body) {
|
|
11854
|
+
const ms = now();
|
|
11855
|
+
const at = new Date(ms).toISOString();
|
|
11856
|
+
const key = body.checkout.path;
|
|
11857
|
+
const needsYou2 = (body.needs_you ?? []).map((n) => ({ action: n.action, reason: n.reason }));
|
|
11858
|
+
const existing = entries.get(key);
|
|
11859
|
+
if (existing === void 0) {
|
|
11860
|
+
const record2 = {
|
|
11861
|
+
session: options.mint(),
|
|
11862
|
+
agent: structuredClone(body.agent),
|
|
11863
|
+
harness: body.harness,
|
|
11864
|
+
repository: structuredClone(body.repository),
|
|
11865
|
+
checkout: structuredClone(body.checkout),
|
|
11866
|
+
spell: body.spell ?? null,
|
|
11867
|
+
task: body.task ?? null,
|
|
11868
|
+
message: body.message ?? null,
|
|
11869
|
+
status: body.status,
|
|
11870
|
+
needs_you: needsYou2,
|
|
11871
|
+
started_at: at,
|
|
11872
|
+
reported_at: at,
|
|
11873
|
+
ended_at: null,
|
|
11874
|
+
stale: false
|
|
11875
|
+
};
|
|
11876
|
+
entries.set(key, { record: record2, reportedMs: ms });
|
|
11877
|
+
emit("session.started", record2);
|
|
11878
|
+
return { record: copy(record2), created: true, event: "session.started" };
|
|
11879
|
+
}
|
|
11880
|
+
const record = existing.record;
|
|
11881
|
+
const wasStale = record.stale;
|
|
11882
|
+
const needsChanged = !sameNeedsYou(record.needs_you, needsYou2);
|
|
11883
|
+
record.agent = structuredClone(body.agent);
|
|
11884
|
+
record.harness = body.harness;
|
|
11885
|
+
record.repository = structuredClone(body.repository);
|
|
11886
|
+
record.checkout = structuredClone(body.checkout);
|
|
11887
|
+
if (body.spell !== void 0) record.spell = body.spell;
|
|
11888
|
+
if (body.task !== void 0) record.task = body.task;
|
|
11889
|
+
if (body.message !== void 0) record.message = body.message;
|
|
11890
|
+
record.status = body.status;
|
|
11891
|
+
record.needs_you = needsYou2;
|
|
11892
|
+
record.reported_at = at;
|
|
11893
|
+
record.stale = false;
|
|
11894
|
+
existing.reportedMs = ms;
|
|
11895
|
+
const event = wasStale ? "session.resumed" : needsChanged ? "session.needs_you" : "session.updated";
|
|
11896
|
+
emit(event, record);
|
|
11897
|
+
return { record: copy(record), created: false, event };
|
|
11898
|
+
},
|
|
11899
|
+
end(checkoutPath) {
|
|
11900
|
+
const existing = entries.get(checkoutPath);
|
|
11901
|
+
if (existing === void 0) return null;
|
|
11902
|
+
entries.delete(checkoutPath);
|
|
11903
|
+
const record = existing.record;
|
|
11904
|
+
record.status = "done";
|
|
11905
|
+
record.ended_at = new Date(now()).toISOString();
|
|
11906
|
+
emit("session.ended", record);
|
|
11907
|
+
return copy(record);
|
|
11908
|
+
},
|
|
11909
|
+
get(session) {
|
|
11910
|
+
for (const { record } of entries.values()) if (record.session === session) return copy(record);
|
|
11911
|
+
return null;
|
|
11912
|
+
},
|
|
11913
|
+
snapshot() {
|
|
11914
|
+
return [...entries.values()].map((e) => copy(e.record)).sort(byStart);
|
|
11915
|
+
},
|
|
11916
|
+
count() {
|
|
11917
|
+
return entries.size;
|
|
11918
|
+
},
|
|
11919
|
+
sweep() {
|
|
11920
|
+
const ms = now();
|
|
11921
|
+
const emitted = [];
|
|
11922
|
+
for (const [key, entry] of [...entries.entries()]) {
|
|
11923
|
+
const silent = ms - entry.reportedMs;
|
|
11924
|
+
if (silent >= expireAfterMs) {
|
|
11925
|
+
entries.delete(key);
|
|
11926
|
+
entry.record.stale = true;
|
|
11927
|
+
emitted.push(emit("session.expired", entry.record));
|
|
11928
|
+
} else if (silent >= staleAfterMs && !entry.record.stale) {
|
|
11929
|
+
entry.record.stale = true;
|
|
11930
|
+
emitted.push(emit("session.stale", entry.record));
|
|
11931
|
+
}
|
|
11932
|
+
}
|
|
11933
|
+
return emitted;
|
|
11934
|
+
},
|
|
11935
|
+
listen(listener) {
|
|
11936
|
+
listeners.add(listener);
|
|
11937
|
+
return () => {
|
|
11938
|
+
listeners.delete(listener);
|
|
11939
|
+
};
|
|
11940
|
+
}
|
|
11941
|
+
};
|
|
11942
|
+
}
|
|
11943
|
+
|
|
11944
|
+
// src/commands/presence.ts
|
|
11945
|
+
var NOT_CONFIGURED = "No presence channel is configured on this machine.";
|
|
11946
|
+
var consoleIo = {
|
|
11947
|
+
out: (line2) => console.log(line2),
|
|
11948
|
+
err: (line2) => console.error(line2)
|
|
11949
|
+
};
|
|
11950
|
+
function resolvePort(flag, env) {
|
|
11951
|
+
const raw = flag ?? env[PRESENCE_ENV.port] ?? String(DEFAULT_PORT);
|
|
11952
|
+
if (!/^\d+$/.test(raw.trim())) return null;
|
|
11953
|
+
const port = Number(raw.trim());
|
|
11954
|
+
return port >= 0 && port <= 65535 ? port : null;
|
|
11955
|
+
}
|
|
11956
|
+
var ServeError = class extends Error {
|
|
11957
|
+
};
|
|
11958
|
+
async function startPresenceService(options, context = {}) {
|
|
11959
|
+
const env = context.env ?? process.env;
|
|
11960
|
+
const store = context.store ?? {};
|
|
11961
|
+
const port = resolvePort(options.port, env);
|
|
11962
|
+
if (port === null) throw new ServeError(`The port must be a whole number from 0 to 65535 (--port or ${PRESENCE_ENV.port}).`);
|
|
11963
|
+
let token = "";
|
|
11964
|
+
const table = createSessionTable({ mint: mintSessionRef });
|
|
11965
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11966
|
+
const presence = createPresenceServer({
|
|
11967
|
+
token: () => token,
|
|
11968
|
+
table,
|
|
11969
|
+
generator: { name: "arcane-cli", version: context.version ?? packageJson.version },
|
|
11970
|
+
startedAt
|
|
11971
|
+
});
|
|
11972
|
+
const { server } = presence;
|
|
11973
|
+
await new Promise((resolve10, reject) => {
|
|
11974
|
+
const onError = (error2) => {
|
|
11975
|
+
server.off("listening", onListening);
|
|
11976
|
+
reject(
|
|
11977
|
+
error2.code === "EADDRINUSE" ? new ServeError(`Port ${port} is already in use. Is a presence service already running? (spell presence status)`) : new ServeError(`Could not listen on ${LOOPBACK_HOST}:${port}: ${error2.message}`)
|
|
11978
|
+
);
|
|
11979
|
+
};
|
|
11980
|
+
const onListening = () => {
|
|
11981
|
+
server.off("error", onError);
|
|
11982
|
+
resolve10();
|
|
11983
|
+
};
|
|
11984
|
+
server.once("error", onError);
|
|
11985
|
+
server.once("listening", onListening);
|
|
11986
|
+
server.listen(port, LOOPBACK_HOST);
|
|
11987
|
+
});
|
|
11988
|
+
const closeServer = () => new Promise((resolve10) => {
|
|
11989
|
+
presence.closeFeeds();
|
|
11990
|
+
server.closeAllConnections();
|
|
11991
|
+
server.close(() => resolve10());
|
|
11992
|
+
});
|
|
11993
|
+
const actualPort = server.address().port;
|
|
11994
|
+
const url = `http://${LOOPBACK_HOST}:${actualPort}`;
|
|
11995
|
+
const recorded = readServerInfo(store);
|
|
11996
|
+
if (recorded !== null && recorded.pid !== process.pid && pidAlive(recorded.pid)) {
|
|
11997
|
+
const health = await callPresence("GET", "/v0/health", void 0, { ...store, env });
|
|
11998
|
+
if (health.kind === "ok" || health.kind === "refused" && health.status === 401) {
|
|
11999
|
+
await closeServer();
|
|
12000
|
+
throw new ServeError(`A presence service is already running at ${recorded.url} (pid ${recorded.pid}). Stop it first: spell presence stop`);
|
|
12001
|
+
}
|
|
12002
|
+
}
|
|
12003
|
+
try {
|
|
12004
|
+
token = ensureToken({ ...store, rotate: options.rotate === true });
|
|
12005
|
+
writeServerInfo({ url, pid: process.pid, port: actualPort, started_at: startedAt, generator: { name: "arcane-cli", version: context.version ?? packageJson.version } }, store);
|
|
12006
|
+
} catch (error2) {
|
|
12007
|
+
await closeServer();
|
|
12008
|
+
throw new ServeError(error2 instanceof PresenceStoreError ? error2.message : `Could not write the presence store: ${String(error2)}`);
|
|
12009
|
+
}
|
|
12010
|
+
server.on("error", () => logEvent("server.error", null, null));
|
|
12011
|
+
const sweep = setInterval(() => table.sweep(), SWEEP_MS);
|
|
12012
|
+
sweep.unref();
|
|
12013
|
+
let stopped = false;
|
|
12014
|
+
return {
|
|
12015
|
+
url,
|
|
12016
|
+
port: actualPort,
|
|
12017
|
+
async stop() {
|
|
12018
|
+
if (stopped) return;
|
|
12019
|
+
stopped = true;
|
|
12020
|
+
clearInterval(sweep);
|
|
12021
|
+
await closeServer();
|
|
12022
|
+
const recorded2 = readServerInfo(store);
|
|
12023
|
+
if (recorded2 !== null && recorded2.pid === process.pid && recorded2.started_at === startedAt) removeServerInfo(store);
|
|
12024
|
+
}
|
|
12025
|
+
};
|
|
12026
|
+
}
|
|
12027
|
+
async function runPresenceServe(options, io = consoleIo) {
|
|
12028
|
+
let signalled = false;
|
|
12029
|
+
let onSignal = () => {
|
|
12030
|
+
signalled = true;
|
|
12031
|
+
};
|
|
12032
|
+
const handler = () => onSignal();
|
|
12033
|
+
process.on("SIGINT", handler);
|
|
12034
|
+
process.on("SIGTERM", handler);
|
|
12035
|
+
const detach = () => {
|
|
12036
|
+
process.off("SIGINT", handler);
|
|
12037
|
+
process.off("SIGTERM", handler);
|
|
12038
|
+
};
|
|
12039
|
+
let service;
|
|
12040
|
+
try {
|
|
12041
|
+
service = await startPresenceService(options);
|
|
12042
|
+
} catch (error2) {
|
|
12043
|
+
detach();
|
|
12044
|
+
io.err(error2 instanceof Error ? error2.message : String(error2));
|
|
12045
|
+
process.exitCode = 1;
|
|
12046
|
+
return;
|
|
12047
|
+
}
|
|
12048
|
+
io.out(`Presence service listening on ${service.url} (loopback only). Stop it with Ctrl+C or \`spell presence stop\`.`);
|
|
12049
|
+
await new Promise((resolve10) => {
|
|
12050
|
+
onSignal = () => {
|
|
12051
|
+
detach();
|
|
12052
|
+
void service.stop().then(() => {
|
|
12053
|
+
io.out("Presence service stopped.");
|
|
12054
|
+
resolve10();
|
|
12055
|
+
});
|
|
12056
|
+
};
|
|
12057
|
+
if (signalled) onSignal();
|
|
12058
|
+
});
|
|
12059
|
+
process.exitCode = 0;
|
|
12060
|
+
}
|
|
12061
|
+
function isHealth(body) {
|
|
12062
|
+
const b = body;
|
|
12063
|
+
return b !== null && typeof b === "object" && b.ok === true && b.contract === "v0" && typeof b.started_at === "string";
|
|
12064
|
+
}
|
|
12065
|
+
async function runPresenceStatus(io = consoleIo, store = {}) {
|
|
12066
|
+
const info = readServerInfo(store);
|
|
12067
|
+
if (info === null) {
|
|
12068
|
+
io.out(NOT_CONFIGURED);
|
|
12069
|
+
return 1;
|
|
12070
|
+
}
|
|
12071
|
+
const result = await callPresence("GET", "/v0/health", void 0, store);
|
|
12072
|
+
if (result.kind === "ok" && isHealth(result.body)) {
|
|
12073
|
+
io.out(`Presence service: ${info.url}`);
|
|
12074
|
+
io.out(` pid: ${info.pid}`);
|
|
12075
|
+
io.out(` version: ${result.body.generator.version}`);
|
|
12076
|
+
io.out(` sessions: ${result.body.sessions}`);
|
|
12077
|
+
return 0;
|
|
12078
|
+
}
|
|
12079
|
+
if (result.kind === "refused" && result.status === 401) {
|
|
12080
|
+
io.out(`The presence service at ${info.url} refused this machine's token (rotated, or ${PRESENCE_ENV.token} is set to another value).`);
|
|
12081
|
+
return 1;
|
|
12082
|
+
}
|
|
12083
|
+
io.out(`The presence service recorded at ${info.url} is not reachable.`);
|
|
12084
|
+
return 1;
|
|
12085
|
+
}
|
|
12086
|
+
async function waitForExit(pid, ms) {
|
|
12087
|
+
const until = Date.now() + ms;
|
|
12088
|
+
while (Date.now() < until) {
|
|
12089
|
+
if (!pidAlive(pid)) return true;
|
|
12090
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
12091
|
+
}
|
|
12092
|
+
return !pidAlive(pid);
|
|
12093
|
+
}
|
|
12094
|
+
async function runPresenceStop(io = consoleIo, store = {}) {
|
|
12095
|
+
const info = readServerInfo(store);
|
|
12096
|
+
if (info === null) {
|
|
12097
|
+
io.out("No presence service is running.");
|
|
12098
|
+
return 0;
|
|
12099
|
+
}
|
|
12100
|
+
const result = await callPresence("GET", "/v0/health", void 0, store);
|
|
12101
|
+
if (result.kind === "ok" && isHealth(result.body) && result.body.started_at === info.started_at) {
|
|
12102
|
+
try {
|
|
12103
|
+
process.kill(info.pid, "SIGTERM");
|
|
12104
|
+
} catch {
|
|
12105
|
+
}
|
|
12106
|
+
if (!await waitForExit(info.pid, 3e3)) {
|
|
12107
|
+
io.out(`Asked the presence service (pid ${info.pid}) to stop; it is still running after 3 s.`);
|
|
12108
|
+
return 0;
|
|
12109
|
+
}
|
|
12110
|
+
const after = readServerInfo(store);
|
|
12111
|
+
if (after !== null && after.started_at === info.started_at) removeServerInfo(store);
|
|
12112
|
+
io.out(`Stopped the presence service (pid ${info.pid}).`);
|
|
12113
|
+
return 0;
|
|
12114
|
+
}
|
|
12115
|
+
if (result.kind === "refused" && result.status === 401) {
|
|
12116
|
+
io.out(`The presence service at ${info.url} refused this machine's token, so it was not stopped.`);
|
|
12117
|
+
return 0;
|
|
12118
|
+
}
|
|
12119
|
+
if (result.kind === "not-configured" && pidAlive(info.pid)) {
|
|
12120
|
+
io.out(`The presence service recorded at ${info.url} (pid ${info.pid}) is running, but there is no token here to confirm it, so it was not stopped.`);
|
|
12121
|
+
return 0;
|
|
12122
|
+
}
|
|
12123
|
+
removeServerInfo(store);
|
|
12124
|
+
io.out("No presence service was answering; removed its stale record.");
|
|
12125
|
+
return 0;
|
|
12126
|
+
}
|
|
12127
|
+
var REPORT_DEADLINE_MS = 2e3;
|
|
12128
|
+
async function withDeadline(work, ms) {
|
|
12129
|
+
let timer;
|
|
12130
|
+
const deadline = new Promise((resolve10) => {
|
|
12131
|
+
timer = setTimeout(() => resolve10({ outcome: null, timedOut: true }), ms);
|
|
12132
|
+
});
|
|
12133
|
+
const run = await Promise.race([work.then((outcome) => ({ outcome, timedOut: false })).catch(() => ({ outcome: null, timedOut: false })), deadline]);
|
|
12134
|
+
clearTimeout(timer);
|
|
12135
|
+
return run;
|
|
12136
|
+
}
|
|
12137
|
+
function finish(run, io) {
|
|
12138
|
+
if (run.outcome?.kind === "refused") io.err(run.outcome.line);
|
|
12139
|
+
return run;
|
|
12140
|
+
}
|
|
12141
|
+
async function runPresenceReport(cwd, flags, io = consoleIo, deps = {}, deadlineMs = REPORT_DEADLINE_MS) {
|
|
12142
|
+
return finish(await withDeadline(sendReport(cwd, flags, deps), deadlineMs), io);
|
|
12143
|
+
}
|
|
12144
|
+
async function runPresenceEnd(cwd, io = consoleIo, deps = {}, deadlineMs = REPORT_DEADLINE_MS) {
|
|
12145
|
+
return finish(await withDeadline(sendEnd(cwd, deps), deadlineMs), io);
|
|
12146
|
+
}
|
|
12147
|
+
async function runPresenceMcp() {
|
|
12148
|
+
await serveMcp({ input: process.stdin, output: process.stdout, version: packageJson.version });
|
|
12149
|
+
}
|
|
12150
|
+
|
|
10758
12151
|
// src/index.ts
|
|
10759
12152
|
import { realpathSync as realpathSync2 } from "fs";
|
|
10760
|
-
var
|
|
10761
|
-
var pkg = require3("../package.json");
|
|
10762
|
-
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join29(dirname14(fileURLToPath(import.meta.url)), "assets");
|
|
12153
|
+
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join31(dirname15(fileURLToPath(import.meta.url)), "assets");
|
|
10763
12154
|
var program = new Command();
|
|
10764
12155
|
var USER_FLAG_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 spells shared by every repository on this machine, fanned out to ~/.agents/skills (Codex, Copilot) and ~/.claude/commands (Claude Code)";
|
|
10765
12156
|
var USER_TIER_AGENTS_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 one agent roster shared by every repository on this machine, rendered to ~/.copilot/agents (VS Code agent modes)";
|
|
@@ -10782,7 +12173,7 @@ function manifestFlagsFrom(opts) {
|
|
|
10782
12173
|
subjectRoot: opts.subjectRoot
|
|
10783
12174
|
};
|
|
10784
12175
|
}
|
|
10785
|
-
program.name("spell").description("Arcane framework CLI \u2014 scaffold and manage governance files").version(
|
|
12176
|
+
program.name("spell").description("Arcane framework CLI \u2014 scaffold and manage governance files").version(packageJson.version).action(async () => {
|
|
10786
12177
|
if (program.args.length > 0) {
|
|
10787
12178
|
const [attempted] = program.args;
|
|
10788
12179
|
console.error(`spell: unrecognized command '${attempted}'`);
|
|
@@ -10798,7 +12189,7 @@ program.name("spell").description("Arcane framework CLI \u2014 scaffold and mana
|
|
|
10798
12189
|
process.exitCode = 1;
|
|
10799
12190
|
return;
|
|
10800
12191
|
}
|
|
10801
|
-
await printWelcome(
|
|
12192
|
+
await printWelcome(packageJson.version, process.cwd());
|
|
10802
12193
|
program.outputHelp();
|
|
10803
12194
|
});
|
|
10804
12195
|
withManifestFlags(
|
|
@@ -10815,7 +12206,7 @@ withManifestFlags(
|
|
|
10815
12206
|
},
|
|
10816
12207
|
targetDirFor(opts),
|
|
10817
12208
|
ASSETS_DIR,
|
|
10818
|
-
|
|
12209
|
+
packageJson.version
|
|
10819
12210
|
);
|
|
10820
12211
|
}
|
|
10821
12212
|
);
|
|
@@ -10826,7 +12217,7 @@ program.command("add <components...>").description("Add one or more components t
|
|
|
10826
12217
|
{ force: opts.force, dryRun: opts.dryRun },
|
|
10827
12218
|
process.cwd(),
|
|
10828
12219
|
ASSETS_DIR,
|
|
10829
|
-
|
|
12220
|
+
packageJson.version
|
|
10830
12221
|
);
|
|
10831
12222
|
}
|
|
10832
12223
|
);
|
|
@@ -10867,11 +12258,11 @@ withManifestFlags(
|
|
|
10867
12258
|
},
|
|
10868
12259
|
targetDirFor(opts),
|
|
10869
12260
|
ASSETS_DIR,
|
|
10870
|
-
|
|
12261
|
+
packageJson.version
|
|
10871
12262
|
);
|
|
10872
12263
|
});
|
|
10873
12264
|
program.command("status").description("Show installed components and check for updates").option("--user", USER_FLAG_DESCRIPTION).action(async (opts) => {
|
|
10874
|
-
await runStatus(targetDirFor(opts),
|
|
12265
|
+
await runStatus(targetDirFor(opts), packageJson.version, { user: opts.user });
|
|
10875
12266
|
});
|
|
10876
12267
|
program.command("state").description("Export this repository's Arcane state as one JSON document (schema https://arcaneai.net/schemas/state/v1)").option("--json", "Print the document to stdout (required; `spell status` is the human view)").option(
|
|
10877
12268
|
"--online",
|
|
@@ -10880,7 +12271,7 @@ program.command("state").description("Export this repository's Arcane state as o
|
|
|
10880
12271
|
"--include-local-paths",
|
|
10881
12272
|
"Hub only: add each registered repository's clone path on this machine. Other machines' paths are never exported"
|
|
10882
12273
|
).action(async (opts) => {
|
|
10883
|
-
await runState(process.cwd(),
|
|
12274
|
+
await runState(process.cwd(), packageJson.version, opts);
|
|
10884
12275
|
});
|
|
10885
12276
|
program.command("uninstall").description("Remove all installed Arcane framework files").option("--yes", "Skip confirmation prompt").option("--dry-run", "Preview what would be removed without deleting files").option("--user", USER_FLAG_DESCRIPTION).action(async (opts) => {
|
|
10886
12277
|
await runUninstall({ yes: opts.yes, dryRun: opts.dryRun, user: opts.user }, targetDirFor(opts));
|
|
@@ -10906,7 +12297,7 @@ program.command("whoami").description("Show who is signed in; exits 1 when signe
|
|
|
10906
12297
|
program.command("doctor").description("Check your environment for Arcane prerequisites").option("--fix", "Automatically create missing session continuity files").option("--leaks", "Run only the on-demand secrets/credential scan (ARC-037), skipping the default checks").action(async (opts) => {
|
|
10907
12298
|
await runDoctor(
|
|
10908
12299
|
process.cwd(),
|
|
10909
|
-
{ fix: opts.fix, leaks: opts.leaks, packageVersion:
|
|
12300
|
+
{ fix: opts.fix, leaks: opts.leaks, packageVersion: packageJson.version },
|
|
10910
12301
|
ASSETS_DIR
|
|
10911
12302
|
);
|
|
10912
12303
|
});
|
|
@@ -10978,6 +12369,33 @@ agentsCommand.command("list").description("Show the current agent roster with as
|
|
|
10978
12369
|
const scope = opts.user ? "user" : "repo";
|
|
10979
12370
|
await runAgentsList(agentsTargetDir(scope, process.cwd()), scope);
|
|
10980
12371
|
});
|
|
12372
|
+
var presenceCommand = program.command("presence").description("The local presence channel: which session works on what, right now (ARC-060). Loopback only; nothing runs until `serve`");
|
|
12373
|
+
presenceCommand.command("serve").description("Run the presence service in the foreground on 127.0.0.1 until Ctrl+C or `spell presence stop`").option("--port <port>", "Port on 127.0.0.1 (default 27226, or ARCANE_PRESENCE_PORT); 0 picks a free one").option("--rotate", "Replace the token in ~/.arcane/presence/token; every subscriber must re-read it").action(async (opts) => {
|
|
12374
|
+
await runPresenceServe(opts);
|
|
12375
|
+
});
|
|
12376
|
+
presenceCommand.command("status").description("Say whether a presence service is running and answering this machine's token (exit 0) or not (exit 1)").action(async () => {
|
|
12377
|
+
process.exitCode = await runPresenceStatus();
|
|
12378
|
+
});
|
|
12379
|
+
presenceCommand.command("stop").description("Stop the presence service this machine recorded; exits 0 whether or not one was running").action(async () => {
|
|
12380
|
+
process.exitCode = await runPresenceStop();
|
|
12381
|
+
});
|
|
12382
|
+
function collectLine(value, previous) {
|
|
12383
|
+
return [...previous, value];
|
|
12384
|
+
}
|
|
12385
|
+
var exitZero = () => process.exit(0);
|
|
12386
|
+
presenceCommand.command("report").description("Report this checkout's session to the presence service. Silent, exit 0, when no channel is configured; never blocks").requiredOption("--status <status>", "working | waiting | blocked | idle").option("--task <line>", "What the session is doing, in one line").option("--spell <id>", "The spell running, e.g. spell-implement").option("--message <line>", "A free one-line note; an empty value clears it").option("--needs-you <line>", "One line of a Needs you block, '<action> \u2014 <reason>'; repeat per line", collectLine, []).option("--agent <slug>", "The roster agent this session reports as (a slug or an alias); default: the orchestrator").option("--harness <id>", "claude-code | copilot | codex | openclaw | cron | other; default: detected, else other").exitOverride(exitZero).action(async (opts) => {
|
|
12387
|
+
const run = await runPresenceReport(process.cwd(), opts);
|
|
12388
|
+
process.exitCode = 0;
|
|
12389
|
+
if (run.timedOut) process.exit(0);
|
|
12390
|
+
});
|
|
12391
|
+
presenceCommand.command("end").description("End this checkout's session on the presence service. Silent, exit 0, when there is none").exitOverride(exitZero).action(async () => {
|
|
12392
|
+
const run = await runPresenceEnd(process.cwd());
|
|
12393
|
+
process.exitCode = 0;
|
|
12394
|
+
if (run.timedOut) process.exit(0);
|
|
12395
|
+
});
|
|
12396
|
+
presenceCommand.command("mcp").description("Serve the presence channel as an MCP server over stdio (two tools, two resources). Register it in your own client; Arcane writes no client configuration").action(async () => {
|
|
12397
|
+
await runPresenceMcp();
|
|
12398
|
+
});
|
|
10981
12399
|
if (realpathSync2(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
10982
12400
|
await program.parseAsync(process.argv);
|
|
10983
12401
|
}
|