arcane-cli 1.17.3 → 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 +1681 -263
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ const require = __createRequire(import.meta.url);
|
|
|
5
5
|
// src/index.ts
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
|
-
import { dirname as
|
|
8
|
+
import { dirname as dirname15, join as join31 } from "path";
|
|
9
9
|
|
|
10
10
|
// src/package-info.ts
|
|
11
11
|
import { createRequire } from "module";
|
|
@@ -152,21 +152,21 @@ function runGit(cwd, args, options = {}) {
|
|
|
152
152
|
windowsHide: true,
|
|
153
153
|
encoding: "utf8"
|
|
154
154
|
},
|
|
155
|
-
(
|
|
156
|
-
if (
|
|
157
|
-
if (
|
|
155
|
+
(error2, stdout, stderr) => {
|
|
156
|
+
if (error2) {
|
|
157
|
+
if (error2.killed) {
|
|
158
158
|
reject(
|
|
159
159
|
new GitTimeoutError(
|
|
160
160
|
// NOTE: embeds the raw args, including any secrets a future
|
|
161
161
|
// network-class caller might pass (e.g. a credentialed
|
|
162
162
|
// clone URL). No current caller does; redact before one does.
|
|
163
163
|
`git ${args.join(" ")} timed out after ${timeoutMs}ms (class: ${commandClass})`,
|
|
164
|
-
{ cause:
|
|
164
|
+
{ cause: error2 }
|
|
165
165
|
)
|
|
166
166
|
);
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
|
-
reject(
|
|
169
|
+
reject(error2);
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
172
172
|
resolve10({ stdout, stderr });
|
|
@@ -227,8 +227,8 @@ async function correctUnbornMasterDefault(cwd) {
|
|
|
227
227
|
try {
|
|
228
228
|
await runGit(cwd, ["symbolic-ref", "-q", "refs/heads/main"]);
|
|
229
229
|
return { corrected: false, to: "main", blockedReason: "target-unreadable" };
|
|
230
|
-
} catch (
|
|
231
|
-
if (
|
|
230
|
+
} catch (error2) {
|
|
231
|
+
if (error2.code !== 1) {
|
|
232
232
|
return { corrected: false, to: "main", blockedReason: "target-unreadable" };
|
|
233
233
|
}
|
|
234
234
|
}
|
|
@@ -663,8 +663,8 @@ function isValidTodoDoneBook(value) {
|
|
|
663
663
|
if (value !== value.trim() || value.includes(":")) return false;
|
|
664
664
|
const normalized = path2.posix.normalize(value.replace(/\\/g, "/"));
|
|
665
665
|
const segments = normalized.split("/");
|
|
666
|
-
const
|
|
667
|
-
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());
|
|
668
668
|
}
|
|
669
669
|
function manifestPath(targetDir) {
|
|
670
670
|
return path2.join(targetDir, MANIFEST_FILE);
|
|
@@ -1487,13 +1487,13 @@ function isClientShimPath(path5) {
|
|
|
1487
1487
|
}
|
|
1488
1488
|
var FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
1489
1489
|
function parsePromptFrontmatter(content) {
|
|
1490
|
-
const
|
|
1491
|
-
if (!
|
|
1490
|
+
const match2 = content.match(FRONTMATTER_PATTERN);
|
|
1491
|
+
if (!match2) {
|
|
1492
1492
|
throw new MissingFrontmatterError(
|
|
1493
1493
|
"Prompt file has no YAML frontmatter block (expected a leading --- ... --- block)."
|
|
1494
1494
|
);
|
|
1495
1495
|
}
|
|
1496
|
-
const raw = parseYaml(
|
|
1496
|
+
const raw = parseYaml(match2[1]);
|
|
1497
1497
|
const name = raw?.["name"];
|
|
1498
1498
|
const description = raw?.["description"];
|
|
1499
1499
|
if (typeof name !== "string" || name.trim() === "") {
|
|
@@ -1906,7 +1906,7 @@ function scale(hex, f) {
|
|
|
1906
1906
|
}
|
|
1907
1907
|
function lockupLines(f = 1) {
|
|
1908
1908
|
const g = f >= 1 ? arcaneGradient : gradient(STOPS.map((c) => scale(c, f)));
|
|
1909
|
-
return WORDMARK.map((
|
|
1909
|
+
return WORDMARK.map((line2) => INDENT + g(line2));
|
|
1910
1910
|
}
|
|
1911
1911
|
function printFooter(version) {
|
|
1912
1912
|
const taglineText = ` ${TAGLINE} \xB7 agent-native SDLC \xB7 v${version}`;
|
|
@@ -1941,7 +1941,7 @@ async function printWelcome(version, cwd) {
|
|
|
1941
1941
|
if (isAnimatable()) {
|
|
1942
1942
|
const draw = (lines, redraw) => {
|
|
1943
1943
|
if (redraw) process.stdout.write(`\x1B[${lines.length}A`);
|
|
1944
|
-
for (const
|
|
1944
|
+
for (const line2 of lines) process.stdout.write(`\r\x1B[2K${line2}
|
|
1945
1945
|
`);
|
|
1946
1946
|
};
|
|
1947
1947
|
const fade = [0.1, 0.22, 0.36, 0.52, 0.68, 0.84, 1];
|
|
@@ -1955,7 +1955,7 @@ async function printWelcome(version, cwd) {
|
|
|
1955
1955
|
await sleep(140);
|
|
1956
1956
|
}
|
|
1957
1957
|
} else {
|
|
1958
|
-
for (const
|
|
1958
|
+
for (const line2 of lockupLines()) console.log(line2);
|
|
1959
1959
|
}
|
|
1960
1960
|
printFooter(version);
|
|
1961
1961
|
console.log();
|
|
@@ -3004,8 +3004,8 @@ async function performAgentSync(targetDir, assetsDir, roster, options) {
|
|
|
3004
3004
|
dryRun: options.dryRun,
|
|
3005
3005
|
fallbackDir: assetsDir
|
|
3006
3006
|
});
|
|
3007
|
-
for (const
|
|
3008
|
-
console.log(` ${
|
|
3007
|
+
for (const line2 of describeFanoutOutcomes(fanout.outcomes, options.dryRun)) {
|
|
3008
|
+
console.log(` ${line2}`);
|
|
3009
3009
|
}
|
|
3010
3010
|
if (!options.dryRun) {
|
|
3011
3011
|
await writeManifest(targetDir, { ...manifest, fanout: fanout.record }, { followLinks: true });
|
|
@@ -3316,7 +3316,7 @@ async function detectAdoContext(targetDir) {
|
|
|
3316
3316
|
} catch {
|
|
3317
3317
|
return false;
|
|
3318
3318
|
}
|
|
3319
|
-
return stdout.split("\n").map((
|
|
3319
|
+
return stdout.split("\n").map((line2) => line2.slice(line2.indexOf(" ") + 1)).some((url) => url.trim() !== "" && isAdoRemoteUrl(url));
|
|
3320
3320
|
}
|
|
3321
3321
|
|
|
3322
3322
|
// src/modules/hub.ts
|
|
@@ -3550,19 +3550,19 @@ function resolveManifestFlags(flags, current, manifestPath2) {
|
|
|
3550
3550
|
if (flags.contentSensitivity !== void 0) requested.content_sensitivity = flags.contentSensitivity;
|
|
3551
3551
|
if (flags.subjectRoot !== void 0) requested.subject_root = flags.subjectRoot;
|
|
3552
3552
|
if (flags.pushPolicy !== void 0) requested.push_policy = flags.pushPolicy;
|
|
3553
|
-
const invalid = (
|
|
3554
|
-
const allowed =
|
|
3553
|
+
const invalid = (error2) => {
|
|
3554
|
+
const allowed = error2.field === "role" ? ["hub", "consumer"] : MANIFEST_ENUM_VALUES[error2.field];
|
|
3555
3555
|
const hint = allowed ? ` Valid values: ${allowed.join(", ")}.` : "";
|
|
3556
|
-
return new ManifestFlagError(`Invalid ${FLAG_NAMES[
|
|
3556
|
+
return new ManifestFlagError(`Invalid ${FLAG_NAMES[error2.field] ?? error2.field}: ${error2.message}${hint}`);
|
|
3557
3557
|
};
|
|
3558
3558
|
if (requested.role !== void 0 && requested.role !== "hub" && requested.role !== "consumer") {
|
|
3559
3559
|
throw invalid(new ManifestInvalidFieldError(manifestPath2, "role", requested.role));
|
|
3560
3560
|
}
|
|
3561
3561
|
try {
|
|
3562
3562
|
validateManifestFields(requested, manifestPath2);
|
|
3563
|
-
} catch (
|
|
3564
|
-
if (
|
|
3565
|
-
throw
|
|
3563
|
+
} catch (error2) {
|
|
3564
|
+
if (error2 instanceof ManifestInvalidFieldError) throw invalid(error2);
|
|
3565
|
+
throw error2;
|
|
3566
3566
|
}
|
|
3567
3567
|
if (requested.external_provider !== void 0 && requested.tracking_mode !== "external") {
|
|
3568
3568
|
throw new ManifestFlagError("--external-provider applies only together with --tracking-mode external.");
|
|
@@ -3663,8 +3663,8 @@ async function readHooksPath(cwd) {
|
|
|
3663
3663
|
try {
|
|
3664
3664
|
const { stdout } = await runGit(cwd, ["config", "--show-scope", "--get", "core.hooksPath"]);
|
|
3665
3665
|
raw = stdout.trim();
|
|
3666
|
-
} catch (
|
|
3667
|
-
if (
|
|
3666
|
+
} catch (error2) {
|
|
3667
|
+
if (error2.code !== 1) {
|
|
3668
3668
|
return { value: "", scope: "unknown", unreadable: true };
|
|
3669
3669
|
}
|
|
3670
3670
|
return void 0;
|
|
@@ -3890,7 +3890,7 @@ async function pushTargets(cwd) {
|
|
|
3890
3890
|
const { stdout } = await runGit(cwd, ["remote", "get-url", "--push", "--all", remote]);
|
|
3891
3891
|
targets.push({
|
|
3892
3892
|
remote,
|
|
3893
|
-
urls: stdout.split("\n").map((
|
|
3893
|
+
urls: stdout.split("\n").map((line2) => line2.trim()).filter(Boolean)
|
|
3894
3894
|
});
|
|
3895
3895
|
} catch {
|
|
3896
3896
|
targets.push({ remote, urls: [] });
|
|
@@ -3904,7 +3904,7 @@ function originalPushUrlKey(remote) {
|
|
|
3904
3904
|
async function readLocalAll(cwd, key) {
|
|
3905
3905
|
try {
|
|
3906
3906
|
const { stdout } = await runGit(cwd, ["config", "--local", "--get-all", key]);
|
|
3907
|
-
return stdout.split("\n").map((
|
|
3907
|
+
return stdout.split("\n").map((line2) => line2.trim()).filter(Boolean);
|
|
3908
3908
|
} catch {
|
|
3909
3909
|
return [];
|
|
3910
3910
|
}
|
|
@@ -3916,10 +3916,10 @@ async function readScopedAll(cwd, key) {
|
|
|
3916
3916
|
} catch {
|
|
3917
3917
|
return [];
|
|
3918
3918
|
}
|
|
3919
|
-
return stdout.split("\n").filter((
|
|
3920
|
-
const separator =
|
|
3921
|
-
if (separator === -1) return { scope: "unknown", value:
|
|
3922
|
-
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() };
|
|
3923
3923
|
});
|
|
3924
3924
|
}
|
|
3925
3925
|
async function effectivePushUrls(cwd, remote) {
|
|
@@ -3931,8 +3931,8 @@ async function disablePushUrls(cwd) {
|
|
|
3931
3931
|
for (const remote of await listRemotes(cwd)) {
|
|
3932
3932
|
try {
|
|
3933
3933
|
results.push(await disableOneRemote(cwd, remote));
|
|
3934
|
-
} catch (
|
|
3935
|
-
results.push({ remote, status: "failed", reason: describeError(
|
|
3934
|
+
} catch (error2) {
|
|
3935
|
+
results.push({ remote, status: "failed", reason: describeError(error2) });
|
|
3936
3936
|
}
|
|
3937
3937
|
}
|
|
3938
3938
|
return results;
|
|
@@ -4007,8 +4007,8 @@ async function restorePushUrls(cwd) {
|
|
|
4007
4007
|
for (const remote of await listRemotes(cwd)) {
|
|
4008
4008
|
try {
|
|
4009
4009
|
results.push(await restoreOneRemote(cwd, remote));
|
|
4010
|
-
} catch (
|
|
4011
|
-
results.push({ remote, status: "failed", reason: describeError(
|
|
4010
|
+
} catch (error2) {
|
|
4011
|
+
results.push({ remote, status: "failed", reason: describeError(error2) });
|
|
4012
4012
|
}
|
|
4013
4013
|
}
|
|
4014
4014
|
return results;
|
|
@@ -4048,8 +4048,8 @@ async function restoreOneRemote(cwd, remote) {
|
|
|
4048
4048
|
async function unsetIgnoringMissing(cwd, key) {
|
|
4049
4049
|
try {
|
|
4050
4050
|
await runGit(cwd, ["config", "--local", "--unset-all", key]);
|
|
4051
|
-
} catch (
|
|
4052
|
-
if (
|
|
4051
|
+
} catch (error2) {
|
|
4052
|
+
if (error2.code !== 5) throw error2;
|
|
4053
4053
|
}
|
|
4054
4054
|
}
|
|
4055
4055
|
async function hadNoPushUrl(cwd, remote) {
|
|
@@ -4077,8 +4077,8 @@ async function readLegacyRecord(cwd, remote) {
|
|
|
4077
4077
|
}
|
|
4078
4078
|
return { status: "usable", values };
|
|
4079
4079
|
}
|
|
4080
|
-
function describeError(
|
|
4081
|
-
const message =
|
|
4080
|
+
function describeError(error2) {
|
|
4081
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
4082
4082
|
return message.split("\n")[0].trim();
|
|
4083
4083
|
}
|
|
4084
4084
|
function hookInForce(outcome) {
|
|
@@ -4086,8 +4086,8 @@ function hookInForce(outcome) {
|
|
|
4086
4086
|
}
|
|
4087
4087
|
async function applyBlockedPushControls(cwd, options = {}) {
|
|
4088
4088
|
const messages = [];
|
|
4089
|
-
const say = (level,
|
|
4090
|
-
const message = { level, text };
|
|
4089
|
+
const say = (level, text2) => {
|
|
4090
|
+
const message = { level, text: text2 };
|
|
4091
4091
|
messages.push(message);
|
|
4092
4092
|
options.emit?.(message);
|
|
4093
4093
|
};
|
|
@@ -4377,7 +4377,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
4377
4377
|
}
|
|
4378
4378
|
const escapes = findLinkedEscapes(targetDir, plannedFiles);
|
|
4379
4379
|
if (escapes.length > 0) {
|
|
4380
|
-
for (const
|
|
4380
|
+
for (const line2 of describeLinkedEscapes(targetDir, escapes)) console.error(line2);
|
|
4381
4381
|
console.error(
|
|
4382
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."
|
|
4383
4383
|
);
|
|
@@ -4566,7 +4566,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
4566
4566
|
const flagLine = options.profile ? manifestFlagLine(manifest) : void 0;
|
|
4567
4567
|
if (flagLine) {
|
|
4568
4568
|
printInfo("Some setup questions were left unanswered. Answer them without a prompt with:");
|
|
4569
|
-
for (const
|
|
4569
|
+
for (const line2 of flagLine) printInfo(` ${line2}`);
|
|
4570
4570
|
}
|
|
4571
4571
|
if (push_policy === "blocked" || push_policy === "guarded") {
|
|
4572
4572
|
try {
|
|
@@ -4758,9 +4758,9 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
4758
4758
|
};
|
|
4759
4759
|
await writeManifest(storeRoot, manifest, { followLinks: true });
|
|
4760
4760
|
printStep(`\u2728 ${fileCount} Spells installed to ${storeRoot}/spells/`);
|
|
4761
|
-
for (const
|
|
4762
|
-
if (
|
|
4763
|
-
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}`);
|
|
4764
4764
|
}
|
|
4765
4765
|
printSuccess(
|
|
4766
4766
|
`Initialized the user tier \u2014 ${fileCount} spells, ${Object.keys(fanout.record).length} client files recorded`
|
|
@@ -4768,7 +4768,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
4768
4768
|
console.log();
|
|
4769
4769
|
printSection("\u{1F9ED} Clients");
|
|
4770
4770
|
console.log();
|
|
4771
|
-
for (const
|
|
4771
|
+
for (const line2 of VSCODE_USER_TIER_NOTE) printInfo(line2);
|
|
4772
4772
|
printInfo(CLAUDE_PRECEDENCE_NOTE);
|
|
4773
4773
|
console.log();
|
|
4774
4774
|
printSection("\u{1F680} Next steps");
|
|
@@ -5049,11 +5049,11 @@ function computeLcsDiff(base, other) {
|
|
|
5049
5049
|
}
|
|
5050
5050
|
function arraysEqual(a, b) {
|
|
5051
5051
|
if (a.length !== b.length) return false;
|
|
5052
|
-
return a.every((
|
|
5052
|
+
return a.every((line2, idx) => line2 === b[idx]);
|
|
5053
5053
|
}
|
|
5054
5054
|
var MAX_LINES_FOR_LCS = 4e3;
|
|
5055
|
-
function splitLines(
|
|
5056
|
-
return
|
|
5055
|
+
function splitLines(text2) {
|
|
5056
|
+
return text2.split("\n");
|
|
5057
5057
|
}
|
|
5058
5058
|
function merge3(base, ours, theirs) {
|
|
5059
5059
|
const baseLines = splitLines(base);
|
|
@@ -5348,9 +5348,9 @@ async function missingTierWarning(scope, spellScope, homeDir) {
|
|
|
5348
5348
|
function reportFlagPatch(flagPatch, before, after, dryRun) {
|
|
5349
5349
|
console.log(`
|
|
5350
5350
|
${dryRun ? "[dry-run] Would record" : "Recorded"} from flags:`);
|
|
5351
|
-
for (const
|
|
5351
|
+
for (const line2 of describeFlagPatch(flagPatch)) console.log(` ${line2}`);
|
|
5352
5352
|
if (!dryRun && after.push_policy !== before.push_policy) {
|
|
5353
|
-
for (const
|
|
5353
|
+
for (const line2 of pushPolicyNotice(after.push_policy)) console.log(line2);
|
|
5354
5354
|
}
|
|
5355
5355
|
}
|
|
5356
5356
|
function defaultScopePatch(options, manifest) {
|
|
@@ -5623,7 +5623,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5623
5623
|
const homeDir = dirname7(targetDir);
|
|
5624
5624
|
const tierGate = await missingTierWarning(scope, spellScope);
|
|
5625
5625
|
if (tierGate.length > 0) {
|
|
5626
|
-
for (const
|
|
5626
|
+
for (const line2 of tierGate) console.error(line2);
|
|
5627
5627
|
console.error(
|
|
5628
5628
|
" \u2717 Update refused: nothing was written. Install the tier or change spell_scope, then run `spell update` again."
|
|
5629
5629
|
);
|
|
@@ -5666,7 +5666,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5666
5666
|
const linkedEscapes = followDirectoryLinks || manifest.components.length === 0 ? [] : findLinkedEscapes(targetDir, await plannedUpdateWrites(manifest.components, scope, spellScope, assetsDir));
|
|
5667
5667
|
const refusedForLinks = () => {
|
|
5668
5668
|
if (linkedEscapes.length === 0) return false;
|
|
5669
|
-
for (const
|
|
5669
|
+
for (const line2 of describeLinkedEscapes(targetDir, linkedEscapes)) console.error(line2);
|
|
5670
5670
|
console.error(
|
|
5671
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."
|
|
5672
5672
|
);
|
|
@@ -5702,7 +5702,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5702
5702
|
registryChanges.missingRequires = missingRequires.filter((m) => !installing.has(m.name)).map((m) => ({ ...m, leftAloneReason: installPlan.leftAlone.find((l) => l.name === m.name)?.reason }));
|
|
5703
5703
|
}
|
|
5704
5704
|
const printRegistryChanges = () => {
|
|
5705
|
-
for (const
|
|
5705
|
+
for (const line2 of formatRegistryChanges(registryChanges)) console.log(line2);
|
|
5706
5706
|
};
|
|
5707
5707
|
const applyInstalls = async () => {
|
|
5708
5708
|
const declined = installPlan.leftAlone.filter((l) => l.kind === "new");
|
|
@@ -5710,14 +5710,14 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5710
5710
|
console.log(options.dryRun ? "\n[dry-run] Components this update would install (ARC-052):" : "\nComponents installed by this update (ARC-052):");
|
|
5711
5711
|
for (const l of declined) console.log(` ! Not installed: ${l.name} \u2014 ${l.reason}`);
|
|
5712
5712
|
if (options.dryRun) {
|
|
5713
|
-
for (const
|
|
5713
|
+
for (const line2 of describeInstallPlan(installPlan)) console.log(line2);
|
|
5714
5714
|
return [];
|
|
5715
5715
|
}
|
|
5716
5716
|
const { installed, lines } = await executeUpdateInstalls(installPlan, targetDir, packageVersion, {
|
|
5717
5717
|
linkTracked,
|
|
5718
5718
|
conversions
|
|
5719
5719
|
});
|
|
5720
|
-
for (const
|
|
5720
|
+
for (const line2 of lines) console.log(line2);
|
|
5721
5721
|
return installed;
|
|
5722
5722
|
};
|
|
5723
5723
|
let sameVersionRestore = false;
|
|
@@ -5744,7 +5744,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5744
5744
|
dryRun: options.dryRun,
|
|
5745
5745
|
fallbackDir: assetsDir
|
|
5746
5746
|
});
|
|
5747
|
-
for (const
|
|
5747
|
+
for (const line2 of describeFanoutOutcomes(fanout.outcomes, options.dryRun)) console.log(` ${line2}`);
|
|
5748
5748
|
const scopePatch = defaultScopePatch(options, manifest);
|
|
5749
5749
|
if (!options.dryRun && (JSON.stringify(fanout.record) !== JSON.stringify(manifest.fanout ?? {}) || scopePatch !== void 0)) {
|
|
5750
5750
|
await writeManifest(targetDir, { ...manifest, ...scopePatch, fanout: fanout.record }, { followLinks: scope === "user" });
|
|
@@ -6048,7 +6048,7 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
6048
6048
|
});
|
|
6049
6049
|
const lines = describeFanoutOutcomes(fanout.outcomes, options.dryRun);
|
|
6050
6050
|
if (lines.length > 0) console.log();
|
|
6051
|
-
for (const
|
|
6051
|
+
for (const line2 of lines) console.log(` ${line2}`);
|
|
6052
6052
|
fanoutRecord = fanout.record;
|
|
6053
6053
|
}
|
|
6054
6054
|
if (options.dryRun) {
|
|
@@ -6073,7 +6073,7 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
6073
6073
|
console.log(
|
|
6074
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:`
|
|
6075
6075
|
);
|
|
6076
|
-
for (const
|
|
6076
|
+
for (const line2 of manifestFlagLine(answered) ?? []) console.log(` ${line2}`);
|
|
6077
6077
|
}
|
|
6078
6078
|
}
|
|
6079
6079
|
const updated = {
|
|
@@ -6093,7 +6093,7 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
6093
6093
|
if (Object.keys(flagPatch).length > 0) {
|
|
6094
6094
|
reportFlagPatch(flagPatch, manifest, updated, false);
|
|
6095
6095
|
} else if (updated.push_policy !== manifest.push_policy) {
|
|
6096
|
-
for (const
|
|
6096
|
+
for (const line2 of pushPolicyNotice(updated.push_policy)) console.log(line2);
|
|
6097
6097
|
}
|
|
6098
6098
|
if (conflictedFiles.length > 0) {
|
|
6099
6099
|
console.log(
|
|
@@ -6224,8 +6224,8 @@ async function collectVersions(manifest, packageVersion, options) {
|
|
|
6224
6224
|
if (feedUrl === "") {
|
|
6225
6225
|
try {
|
|
6226
6226
|
feedUrl = getFeedUrl();
|
|
6227
|
-
} catch (
|
|
6228
|
-
feedError = `Feed URL unavailable: ${
|
|
6227
|
+
} catch (error2) {
|
|
6228
|
+
feedError = `Feed URL unavailable: ${error2 instanceof Error ? error2.message : String(error2)}`;
|
|
6229
6229
|
}
|
|
6230
6230
|
}
|
|
6231
6231
|
lookup = feedUrl ? await checkForUpdate(packageVersion, feedUrl, options.cachePath) : { current: packageVersion, latest: null, updateAvailable: false, error: feedError };
|
|
@@ -6388,7 +6388,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
6388
6388
|
}
|
|
6389
6389
|
if (scope === "user") {
|
|
6390
6390
|
console.log("");
|
|
6391
|
-
for (const
|
|
6391
|
+
for (const line2 of VSCODE_USER_TIER_NOTE) console.log(` ${line2}`);
|
|
6392
6392
|
console.log(` ${CLAUDE_PRECEDENCE_NOTE}`);
|
|
6393
6393
|
}
|
|
6394
6394
|
}
|
|
@@ -6408,9 +6408,9 @@ async function collectAgents(targetDir, options = {}) {
|
|
|
6408
6408
|
let resolved;
|
|
6409
6409
|
try {
|
|
6410
6410
|
resolved = await resolveRoster(targetDir, "repo", options.homeDir);
|
|
6411
|
-
} catch (
|
|
6412
|
-
if (
|
|
6413
|
-
throw
|
|
6411
|
+
} catch (error2) {
|
|
6412
|
+
if (error2 instanceof AgentRosterNotFoundError) return { agents: null, warnings: [] };
|
|
6413
|
+
throw error2;
|
|
6414
6414
|
}
|
|
6415
6415
|
const origin = resolved.fromUserTier ? "user" : "repo";
|
|
6416
6416
|
const warnings = [];
|
|
@@ -6457,18 +6457,18 @@ function titleAfterId(rest) {
|
|
|
6457
6457
|
return rest.replace(/^\s*[—–:-]+\s*/u, "").trim();
|
|
6458
6458
|
}
|
|
6459
6459
|
function parseDecisions(content) {
|
|
6460
|
-
const
|
|
6460
|
+
const text2 = content.replace(/\r\n?/g, "\n");
|
|
6461
6461
|
const positions = [];
|
|
6462
|
-
let
|
|
6462
|
+
let match2;
|
|
6463
6463
|
HEADING.lastIndex = 0;
|
|
6464
|
-
while ((
|
|
6465
|
-
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 });
|
|
6466
6466
|
}
|
|
6467
6467
|
const entries = [];
|
|
6468
6468
|
const warnings = [];
|
|
6469
6469
|
positions.forEach(({ id, heading, index }, i) => {
|
|
6470
|
-
const end = i + 1 < positions.length ? positions[i + 1].index :
|
|
6471
|
-
const section =
|
|
6470
|
+
const end = i + 1 < positions.length ? positions[i + 1].index : text2.length;
|
|
6471
|
+
const section = text2.slice(index, end);
|
|
6472
6472
|
const dateMatch = /^\*\*Date:\*\*\s*(\d{4}-\d{2}-\d{2})/m.exec(section);
|
|
6473
6473
|
const statusMatch = /^\*\*Status:\*\*\s*(\S+)/m.exec(section);
|
|
6474
6474
|
if (!dateMatch && !statusMatch) {
|
|
@@ -6497,11 +6497,11 @@ async function collectDecisions(root) {
|
|
|
6497
6497
|
let content;
|
|
6498
6498
|
try {
|
|
6499
6499
|
content = await readFile12(join14(root, "DECISIONS.md"), "utf8");
|
|
6500
|
-
} catch (
|
|
6501
|
-
if (
|
|
6500
|
+
} catch (error2) {
|
|
6501
|
+
if (error2.code === "ENOENT") return { decisions: null, warnings: [] };
|
|
6502
6502
|
return {
|
|
6503
6503
|
decisions: null,
|
|
6504
|
-
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)})`]
|
|
6505
6505
|
};
|
|
6506
6506
|
}
|
|
6507
6507
|
const { entries, warnings } = parseDecisions(content);
|
|
@@ -6515,12 +6515,12 @@ async function collectDecisions(root) {
|
|
|
6515
6515
|
var ITEM = /^\s*- \[( |x|X)\] (.*)$/;
|
|
6516
6516
|
var HEADING2 = /^#{1,6}\s+(.+?)\s*#*\s*$/;
|
|
6517
6517
|
var TEXT_LIMIT = 200;
|
|
6518
|
-
function normalizeItemText(
|
|
6519
|
-
return
|
|
6518
|
+
function normalizeItemText(text2) {
|
|
6519
|
+
return text2.replace(/\s+/g, " ").trim();
|
|
6520
6520
|
}
|
|
6521
|
-
function itemLocator(section,
|
|
6521
|
+
function itemLocator(section, text2) {
|
|
6522
6522
|
const slug = section === null ? "" : headingAnchor(section);
|
|
6523
|
-
const hash = createHash3("sha256").update(normalizeItemText(
|
|
6523
|
+
const hash = createHash3("sha256").update(normalizeItemText(text2), "utf8").digest("hex").slice(0, 12);
|
|
6524
6524
|
return `${slug === "" ? "root" : slug}/${hash}`;
|
|
6525
6525
|
}
|
|
6526
6526
|
function parseBook(content) {
|
|
@@ -6528,22 +6528,22 @@ function parseBook(content) {
|
|
|
6528
6528
|
let section = null;
|
|
6529
6529
|
let open = 0;
|
|
6530
6530
|
let done = 0;
|
|
6531
|
-
for (const
|
|
6532
|
-
const heading = HEADING2.exec(
|
|
6531
|
+
for (const line2 of content.split(/\r?\n/)) {
|
|
6532
|
+
const heading = HEADING2.exec(line2);
|
|
6533
6533
|
if (heading) {
|
|
6534
6534
|
section = heading[1];
|
|
6535
6535
|
continue;
|
|
6536
6536
|
}
|
|
6537
|
-
const item = ITEM.exec(
|
|
6537
|
+
const item = ITEM.exec(line2);
|
|
6538
6538
|
if (!item) continue;
|
|
6539
6539
|
const checked = item[1] !== " ";
|
|
6540
|
-
const
|
|
6540
|
+
const text2 = item[2].trim();
|
|
6541
6541
|
if (checked) done++;
|
|
6542
6542
|
else open++;
|
|
6543
6543
|
items.push({
|
|
6544
|
-
text:
|
|
6544
|
+
text: text2.length > TEXT_LIMIT ? text2.slice(0, TEXT_LIMIT) : text2,
|
|
6545
6545
|
section,
|
|
6546
|
-
locator: itemLocator(section,
|
|
6546
|
+
locator: itemLocator(section, text2),
|
|
6547
6547
|
checked
|
|
6548
6548
|
});
|
|
6549
6549
|
}
|
|
@@ -6553,11 +6553,11 @@ async function collectBook(root, fileName) {
|
|
|
6553
6553
|
let content;
|
|
6554
6554
|
try {
|
|
6555
6555
|
content = await readFile13(join15(root, fileName), "utf8");
|
|
6556
|
-
} catch (
|
|
6557
|
-
if (
|
|
6556
|
+
} catch (error2) {
|
|
6557
|
+
if (error2.code === "ENOENT") return { book: null, warnings: [] };
|
|
6558
6558
|
return {
|
|
6559
6559
|
book: null,
|
|
6560
|
-
warnings: [`indexes: ${fileName} could not be read (${
|
|
6560
|
+
warnings: [`indexes: ${fileName} could not be read (${error2 instanceof Error ? error2.message : String(error2)})`]
|
|
6561
6561
|
};
|
|
6562
6562
|
}
|
|
6563
6563
|
return { book: parseBook(content), warnings: [] };
|
|
@@ -6608,16 +6608,16 @@ function featureTracking(frontmatter) {
|
|
|
6608
6608
|
};
|
|
6609
6609
|
}
|
|
6610
6610
|
function parsePrd(content, displayPath) {
|
|
6611
|
-
const
|
|
6612
|
-
const heading = H1.exec(
|
|
6611
|
+
const text2 = content.replace(/\r\n?/g, "\n");
|
|
6612
|
+
const heading = H1.exec(text2);
|
|
6613
6613
|
const empty = { title: heading ? heading[1] : null, status: null, implementation_status: null, tracking: null };
|
|
6614
|
-
const
|
|
6615
|
-
if (!
|
|
6614
|
+
const match2 = FRONTMATTER.exec(text2);
|
|
6615
|
+
if (!match2) return { fields: empty, warning: `features: ${displayPath} has no frontmatter` };
|
|
6616
6616
|
let parsed;
|
|
6617
6617
|
try {
|
|
6618
|
-
parsed = parse2(
|
|
6619
|
-
} catch (
|
|
6620
|
-
const reason =
|
|
6618
|
+
parsed = parse2(match2[1]);
|
|
6619
|
+
} catch (error2) {
|
|
6620
|
+
const reason = error2 instanceof Error ? error2.message.split("\n")[0] : String(error2);
|
|
6621
6621
|
return { fields: empty, warning: `features: ${displayPath} has malformed frontmatter (${reason})` };
|
|
6622
6622
|
}
|
|
6623
6623
|
if (!isRecord3(parsed)) return { fields: empty, warning: `features: ${displayPath} has frontmatter that is not a map` };
|
|
@@ -6637,11 +6637,11 @@ async function collectFeatures(root) {
|
|
|
6637
6637
|
try {
|
|
6638
6638
|
const entries = await readdir7(dir, { withFileTypes: true });
|
|
6639
6639
|
slugs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
6640
|
-
} catch (
|
|
6641
|
-
if (
|
|
6640
|
+
} catch (error2) {
|
|
6641
|
+
if (error2.code === "ENOENT") return { features: null, warnings: [] };
|
|
6642
6642
|
return {
|
|
6643
6643
|
features: null,
|
|
6644
|
-
warnings: [`features: ${FEATURES_DIR}/ could not be read (${
|
|
6644
|
+
warnings: [`features: ${FEATURES_DIR}/ could not be read (${error2 instanceof Error ? error2.message : String(error2)})`]
|
|
6645
6645
|
};
|
|
6646
6646
|
}
|
|
6647
6647
|
const warnings = [];
|
|
@@ -6651,9 +6651,9 @@ async function collectFeatures(root) {
|
|
|
6651
6651
|
let content;
|
|
6652
6652
|
try {
|
|
6653
6653
|
content = await readFile14(join16(dir, slug, PRD_FILE), "utf8");
|
|
6654
|
-
} catch (
|
|
6655
|
-
if (
|
|
6656
|
-
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)})`);
|
|
6657
6657
|
continue;
|
|
6658
6658
|
}
|
|
6659
6659
|
const { fields, warning } = parsePrd(content, path5);
|
|
@@ -6817,10 +6817,10 @@ function serializeState(doc) {
|
|
|
6817
6817
|
`;
|
|
6818
6818
|
}
|
|
6819
6819
|
function toPosixRelative(root, target) {
|
|
6820
|
-
const
|
|
6821
|
-
if (
|
|
6822
|
-
if (path4.isAbsolute(
|
|
6823
|
-
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("/");
|
|
6824
6824
|
}
|
|
6825
6825
|
|
|
6826
6826
|
// src/modules/state/git.ts
|
|
@@ -6842,10 +6842,10 @@ function stripUserinfo(url) {
|
|
|
6842
6842
|
function parseWorktreeList(porcelain, root) {
|
|
6843
6843
|
const worktrees = [];
|
|
6844
6844
|
for (const block of porcelain.replace(/\r\n/g, "\n").split(/\n\n+/)) {
|
|
6845
|
-
const lines = block.split("\n").filter((
|
|
6846
|
-
const pathLine = lines.find((
|
|
6845
|
+
const lines = block.split("\n").filter((line2) => line2.length > 0);
|
|
6846
|
+
const pathLine = lines.find((line2) => line2.startsWith("worktree "));
|
|
6847
6847
|
if (!pathLine) continue;
|
|
6848
|
-
const branchLine = lines.find((
|
|
6848
|
+
const branchLine = lines.find((line2) => line2.startsWith("branch "));
|
|
6849
6849
|
const branch = branchLine ? branchLine.slice("branch ".length).replace(/^refs\/heads\//, "") : null;
|
|
6850
6850
|
const absolute = resolveRealPath(pathLine.slice("worktree ".length));
|
|
6851
6851
|
worktrees.push({ path: toPosixRelative(root, absolute), branch });
|
|
@@ -6855,12 +6855,12 @@ function parseWorktreeList(porcelain, root) {
|
|
|
6855
6855
|
async function probe(read) {
|
|
6856
6856
|
try {
|
|
6857
6857
|
return { ok: true, value: await read() };
|
|
6858
|
-
} catch (
|
|
6859
|
-
return { ok: false, error };
|
|
6858
|
+
} catch (error2) {
|
|
6859
|
+
return { ok: false, error: error2 };
|
|
6860
6860
|
}
|
|
6861
6861
|
}
|
|
6862
|
-
function describe(
|
|
6863
|
-
return
|
|
6862
|
+
function describe(error2) {
|
|
6863
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
6864
6864
|
}
|
|
6865
6865
|
async function collectGit(cwd, root, options = {}) {
|
|
6866
6866
|
const git = options.runGit ?? runGit;
|
|
@@ -6951,8 +6951,8 @@ async function collectGit(cwd, root, options = {}) {
|
|
|
6951
6951
|
warnings
|
|
6952
6952
|
};
|
|
6953
6953
|
}
|
|
6954
|
-
function isTimeout(
|
|
6955
|
-
return
|
|
6954
|
+
function isTimeout(error2) {
|
|
6955
|
+
return error2 instanceof Error && error2.name === "GitTimeoutError";
|
|
6956
6956
|
}
|
|
6957
6957
|
|
|
6958
6958
|
// src/modules/state/journal.ts
|
|
@@ -6961,13 +6961,13 @@ import { join as join17 } from "path";
|
|
|
6961
6961
|
var JOURNAL_DIR = "journal";
|
|
6962
6962
|
var ENTRY_NAME = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
|
6963
6963
|
function journalTitle(content) {
|
|
6964
|
-
const
|
|
6965
|
-
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);
|
|
6966
6966
|
if (frontmatter) {
|
|
6967
6967
|
const title = /^title:\s*(.+)$/m.exec(frontmatter[1]);
|
|
6968
6968
|
if (title) return title[1].trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
6969
6969
|
}
|
|
6970
|
-
const heading = /^# (.+?)\s*$/m.exec(
|
|
6970
|
+
const heading = /^# (.+?)\s*$/m.exec(text2);
|
|
6971
6971
|
return heading ? heading[1].trim() : null;
|
|
6972
6972
|
}
|
|
6973
6973
|
async function collectJournal(root) {
|
|
@@ -6976,29 +6976,29 @@ async function collectJournal(root) {
|
|
|
6976
6976
|
try {
|
|
6977
6977
|
const entries = await readdir8(dir, { withFileTypes: true });
|
|
6978
6978
|
names = entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
6979
|
-
} catch (
|
|
6980
|
-
if (
|
|
6979
|
+
} catch (error2) {
|
|
6980
|
+
if (error2.code === "ENOENT") return { journal: null, warnings: [] };
|
|
6981
6981
|
return {
|
|
6982
6982
|
journal: null,
|
|
6983
|
-
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)})`]
|
|
6984
6984
|
};
|
|
6985
6985
|
}
|
|
6986
6986
|
const warnings = [];
|
|
6987
6987
|
const journal = [];
|
|
6988
6988
|
for (const name of names) {
|
|
6989
|
-
const
|
|
6990
|
-
if (!
|
|
6989
|
+
const match2 = ENTRY_NAME.exec(name);
|
|
6990
|
+
if (!match2) {
|
|
6991
6991
|
warnings.push(`indexes.journal: ${JOURNAL_DIR}/${name} does not match YYYY-MM-DD-<slug>.md and was skipped`);
|
|
6992
6992
|
continue;
|
|
6993
6993
|
}
|
|
6994
6994
|
let title = null;
|
|
6995
6995
|
try {
|
|
6996
6996
|
title = journalTitle(await readFile15(join17(dir, name), "utf8"));
|
|
6997
|
-
} catch (
|
|
6998
|
-
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)})`);
|
|
6999
6999
|
}
|
|
7000
7000
|
if (title === null) warnings.push(`indexes.journal: ${JOURNAL_DIR}/${name} has no title: frontmatter or # heading`);
|
|
7001
|
-
journal.push({ date:
|
|
7001
|
+
journal.push({ date: match2[1], slug: match2[2], title, path: `${JOURNAL_DIR}/${name}` });
|
|
7002
7002
|
}
|
|
7003
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);
|
|
7004
7004
|
return { journal, warnings };
|
|
@@ -7021,9 +7021,9 @@ function execFileWithTimeout(file, args, timeoutMs, options = {}) {
|
|
|
7021
7021
|
encoding: "utf8",
|
|
7022
7022
|
...options.env ? { env: { ...process.env, ...options.env } } : {}
|
|
7023
7023
|
},
|
|
7024
|
-
(
|
|
7025
|
-
if (
|
|
7026
|
-
reject(
|
|
7024
|
+
(error2, stdout, stderr) => {
|
|
7025
|
+
if (error2) {
|
|
7026
|
+
reject(error2);
|
|
7027
7027
|
return;
|
|
7028
7028
|
}
|
|
7029
7029
|
resolve10({ stdout, stderr });
|
|
@@ -7073,14 +7073,14 @@ function parseAdoRemote(url) {
|
|
|
7073
7073
|
if (ssh) return { org: ssh[1], project: decodeURIComponent(ssh[2]), repo: decodeURIComponent(ssh[3]) };
|
|
7074
7074
|
return null;
|
|
7075
7075
|
}
|
|
7076
|
-
function describe2(
|
|
7077
|
-
if (
|
|
7078
|
-
const code =
|
|
7076
|
+
function describe2(error2) {
|
|
7077
|
+
if (error2 instanceof Error) {
|
|
7078
|
+
const code = error2.code;
|
|
7079
7079
|
if (code === "ENOENT") return "the CLI is not installed";
|
|
7080
|
-
if (
|
|
7081
|
-
return
|
|
7080
|
+
if (error2.killed) return `timed out after ${EXTERNAL_CLI_TIMEOUT_MS}ms`;
|
|
7081
|
+
return error2.message.split("\n")[0];
|
|
7082
7082
|
}
|
|
7083
|
-
return String(
|
|
7083
|
+
return String(error2);
|
|
7084
7084
|
}
|
|
7085
7085
|
function isRecord5(value) {
|
|
7086
7086
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -7089,8 +7089,8 @@ async function lookupGitHub(branch, exec) {
|
|
|
7089
7089
|
let stdout;
|
|
7090
7090
|
try {
|
|
7091
7091
|
({ stdout } = await exec("gh", ["pr", "list", "--head", branch, "--state", "open", "--json", "number,url,state"], EXTERNAL_CLI_TIMEOUT_MS));
|
|
7092
|
-
} catch (
|
|
7093
|
-
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)})`] };
|
|
7094
7094
|
}
|
|
7095
7095
|
let parsed;
|
|
7096
7096
|
try {
|
|
@@ -7137,8 +7137,8 @@ async function lookupAdo(branch, remoteUrl, exec, launch) {
|
|
|
7137
7137
|
EXTERNAL_CLI_TIMEOUT_MS,
|
|
7138
7138
|
launch.env ? { env: launch.env } : {}
|
|
7139
7139
|
));
|
|
7140
|
-
} catch (
|
|
7141
|
-
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)})`] };
|
|
7142
7142
|
}
|
|
7143
7143
|
let parsed;
|
|
7144
7144
|
try {
|
|
@@ -7245,17 +7245,17 @@ async function collectRegistry(root, manifest, options) {
|
|
|
7245
7245
|
let content;
|
|
7246
7246
|
try {
|
|
7247
7247
|
content = await readFile16(join18(root, businessRoot, "registry.json"), "utf8");
|
|
7248
|
-
} catch (
|
|
7249
|
-
const reason =
|
|
7248
|
+
} catch (error2) {
|
|
7249
|
+
const reason = error2.code === "ENOENT" ? "is missing" : `could not be read (${error2 instanceof Error ? error2.message : String(error2)})`;
|
|
7250
7250
|
return { registry: null, warnings: [`registry: this repository is a hub but ${relativePath} ${reason}`] };
|
|
7251
7251
|
}
|
|
7252
7252
|
let raw;
|
|
7253
7253
|
try {
|
|
7254
7254
|
raw = JSON.parse(content);
|
|
7255
|
-
} catch (
|
|
7255
|
+
} catch (error2) {
|
|
7256
7256
|
return {
|
|
7257
7257
|
registry: null,
|
|
7258
|
-
warnings: [`registry: ${relativePath} is not valid JSON (${
|
|
7258
|
+
warnings: [`registry: ${relativePath} is not valid JSON (${error2 instanceof Error ? error2.message : String(error2)})`]
|
|
7259
7259
|
};
|
|
7260
7260
|
}
|
|
7261
7261
|
return { registry: parseRegistry(raw, options), warnings: [] };
|
|
@@ -7270,9 +7270,9 @@ async function guarded(section, warnings, run, fallback) {
|
|
|
7270
7270
|
const result = await run();
|
|
7271
7271
|
warnings.push(...result.warnings);
|
|
7272
7272
|
return result.value;
|
|
7273
|
-
} catch (
|
|
7274
|
-
if (
|
|
7275
|
-
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`);
|
|
7276
7276
|
return fallback;
|
|
7277
7277
|
}
|
|
7278
7278
|
}
|
|
@@ -7405,9 +7405,9 @@ async function collectState(cwd, packageVersion, options) {
|
|
|
7405
7405
|
|
|
7406
7406
|
// src/commands/state.ts
|
|
7407
7407
|
var USAGE_POINTER = "spell state prints one JSON document to stdout: run `spell state --json` (spell status is the human view).";
|
|
7408
|
-
function writeStdout(
|
|
7408
|
+
function writeStdout(text2) {
|
|
7409
7409
|
return new Promise((resolve10, reject) => {
|
|
7410
|
-
process.stdout.write(
|
|
7410
|
+
process.stdout.write(text2, (error2) => error2 ? reject(error2) : resolve10());
|
|
7411
7411
|
});
|
|
7412
7412
|
}
|
|
7413
7413
|
async function runState(targetDir, packageVersion, options = {}, deps = {}) {
|
|
@@ -7417,28 +7417,28 @@ async function runState(targetDir, packageVersion, options = {}, deps = {}) {
|
|
|
7417
7417
|
return;
|
|
7418
7418
|
}
|
|
7419
7419
|
const collect = deps.collect ?? collectState;
|
|
7420
|
-
let
|
|
7420
|
+
let text2;
|
|
7421
7421
|
try {
|
|
7422
7422
|
const doc = await collect(targetDir, packageVersion, {
|
|
7423
7423
|
online: options.online === true,
|
|
7424
7424
|
includeLocalPaths: options.includeLocalPaths === true
|
|
7425
7425
|
});
|
|
7426
|
-
|
|
7427
|
-
} catch (
|
|
7428
|
-
if (
|
|
7426
|
+
text2 = serializeState(doc);
|
|
7427
|
+
} catch (error2) {
|
|
7428
|
+
if (error2 instanceof ManifestNotFoundError) {
|
|
7429
7429
|
console.error('Not initialized. Run "spell init" first.');
|
|
7430
|
-
} else if (
|
|
7431
|
-
console.error(
|
|
7430
|
+
} else if (error2 instanceof AgentConfigValidationError) {
|
|
7431
|
+
console.error(error2.message);
|
|
7432
7432
|
} else {
|
|
7433
|
-
console.error(`spell state: ${
|
|
7433
|
+
console.error(`spell state: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
7434
7434
|
}
|
|
7435
7435
|
process.exitCode = 1;
|
|
7436
7436
|
return;
|
|
7437
7437
|
}
|
|
7438
7438
|
try {
|
|
7439
|
-
await (deps.write ?? writeStdout)(
|
|
7440
|
-
} catch (
|
|
7441
|
-
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)})`);
|
|
7442
7442
|
process.exitCode = 1;
|
|
7443
7443
|
}
|
|
7444
7444
|
}
|
|
@@ -7590,7 +7590,7 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
7590
7590
|
previous: manifest.fanout,
|
|
7591
7591
|
dryRun: true
|
|
7592
7592
|
});
|
|
7593
|
-
for (const
|
|
7593
|
+
for (const line2 of describeFanoutOutcomes(preview.outcomes, true)) console.log(line2);
|
|
7594
7594
|
console.log(`[dry-run] Would remove: ${storeRoot}/.arcane.json`);
|
|
7595
7595
|
console.log(
|
|
7596
7596
|
`
|
|
@@ -7628,7 +7628,7 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
7628
7628
|
await rm5(join19(storeRoot, "agents.yaml"), { force: true });
|
|
7629
7629
|
await removeDirectoryIfEmpty(join19(storeRoot, USER_TIER_SPELLS_DIR));
|
|
7630
7630
|
await removeDirectoryIfEmpty(storeRoot);
|
|
7631
|
-
for (const
|
|
7631
|
+
for (const line2 of describeFanoutOutcomes(fanout.outcomes)) console.log(` ${line2}`);
|
|
7632
7632
|
const pruned = fanout.outcomes.filter((o) => o.status === "pruned").length;
|
|
7633
7633
|
const kept = fanout.outcomes.filter((o) => o.status === "kept-edited").length;
|
|
7634
7634
|
console.log(
|
|
@@ -7931,9 +7931,9 @@ async function fetchAdoMergeTypePolicies(org, project, repoName, branch, launch
|
|
|
7931
7931
|
}
|
|
7932
7932
|
}
|
|
7933
7933
|
function parseGitHubRemote(url) {
|
|
7934
|
-
const
|
|
7935
|
-
if (!
|
|
7936
|
-
const [, owner, repo] =
|
|
7934
|
+
const match2 = /github\.com[/:]([^/]+)\/([^/.]+?)(?:\.git)?\/?$/.exec(url);
|
|
7935
|
+
if (!match2) return null;
|
|
7936
|
+
const [, owner, repo] = match2;
|
|
7937
7937
|
if (!owner || !repo) return null;
|
|
7938
7938
|
return { owner, repo };
|
|
7939
7939
|
}
|
|
@@ -7948,9 +7948,9 @@ function decodeAdoSegment(raw) {
|
|
|
7948
7948
|
return decoded;
|
|
7949
7949
|
}
|
|
7950
7950
|
function parseAdoRemote2(url) {
|
|
7951
|
-
const
|
|
7952
|
-
if (!
|
|
7953
|
-
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;
|
|
7954
7954
|
if (!rawOrg || !rawProject || !rawRepo) return null;
|
|
7955
7955
|
const org = decodeAdoSegment(rawOrg);
|
|
7956
7956
|
const project = decodeAdoSegment(rawProject);
|
|
@@ -7977,28 +7977,28 @@ function stripFrontmatter(content) {
|
|
|
7977
7977
|
const block = FRONTMATTER_PATTERN2.exec(content);
|
|
7978
7978
|
return block ? content.slice(block[0].length) : content;
|
|
7979
7979
|
}
|
|
7980
|
-
function stripFencedCode(
|
|
7980
|
+
function stripFencedCode(text2) {
|
|
7981
7981
|
const kept = [];
|
|
7982
7982
|
let fence = null;
|
|
7983
|
-
for (const
|
|
7983
|
+
for (const line2 of text2.split(/\r?\n/)) {
|
|
7984
7984
|
if (fence) {
|
|
7985
|
-
const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(
|
|
7985
|
+
const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line2);
|
|
7986
7986
|
if (close && close[1][0] === fence.char && close[1].length >= fence.length) fence = null;
|
|
7987
7987
|
kept.push("");
|
|
7988
7988
|
continue;
|
|
7989
7989
|
}
|
|
7990
|
-
const open = /^ {0,3}(`{3,}|~{3,})/.exec(
|
|
7991
|
-
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("`"))) {
|
|
7992
7992
|
fence = { char: open[1][0], length: open[1].length };
|
|
7993
7993
|
kept.push("");
|
|
7994
7994
|
continue;
|
|
7995
7995
|
}
|
|
7996
|
-
kept.push(
|
|
7996
|
+
kept.push(line2);
|
|
7997
7997
|
}
|
|
7998
7998
|
return kept.join("\n");
|
|
7999
7999
|
}
|
|
8000
|
-
function stripInlineCode(
|
|
8001
|
-
return
|
|
8000
|
+
function stripInlineCode(text2) {
|
|
8001
|
+
return text2.split(/\n[ \t]*\n/).map((paragraph) => {
|
|
8002
8002
|
let out = "";
|
|
8003
8003
|
let i = 0;
|
|
8004
8004
|
while (i < paragraph.length) {
|
|
@@ -8039,7 +8039,7 @@ function stripInlineCode(text) {
|
|
|
8039
8039
|
function findPlaceholderTokens(content) {
|
|
8040
8040
|
const prose = stripInlineCode(stripFencedCode(stripFrontmatter(content)));
|
|
8041
8041
|
const seen = /* @__PURE__ */ new Set();
|
|
8042
|
-
for (const
|
|
8042
|
+
for (const match2 of prose.matchAll(PLACEHOLDER_PATTERN)) seen.add(`{${match2[1]}}`);
|
|
8043
8043
|
return [...seen];
|
|
8044
8044
|
}
|
|
8045
8045
|
function parseRuntimePlaceholders(standardContent) {
|
|
@@ -8048,8 +8048,8 @@ function parseRuntimePlaceholders(standardContent) {
|
|
|
8048
8048
|
if (start === -1 || end === -1 || end < start) return null;
|
|
8049
8049
|
const region = standardContent.slice(start + RUNTIME_PLACEHOLDERS_START.length, end);
|
|
8050
8050
|
const tokens = /* @__PURE__ */ new Set();
|
|
8051
|
-
for (const
|
|
8052
|
-
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]);
|
|
8053
8053
|
}
|
|
8054
8054
|
return tokens;
|
|
8055
8055
|
}
|
|
@@ -8986,9 +8986,9 @@ function parseTermsFileContent(raw) {
|
|
|
8986
8986
|
async function readTermsFile(filePath) {
|
|
8987
8987
|
try {
|
|
8988
8988
|
return parseTermsFileContent(await readFile21(filePath, "utf8"));
|
|
8989
|
-
} catch (
|
|
8989
|
+
} catch (error2) {
|
|
8990
8990
|
console.warn(
|
|
8991
|
-
`\u26A0 arcane: could not read --terms-file "${filePath}": ${
|
|
8991
|
+
`\u26A0 arcane: could not read --terms-file "${filePath}": ${error2.message}`
|
|
8992
8992
|
);
|
|
8993
8993
|
return [];
|
|
8994
8994
|
}
|
|
@@ -9251,8 +9251,8 @@ var ChunkedSecretStore = class {
|
|
|
9251
9251
|
guard(operation, fn) {
|
|
9252
9252
|
try {
|
|
9253
9253
|
return fn();
|
|
9254
|
-
} catch (
|
|
9255
|
-
throw new KeychainUnavailableError(operation,
|
|
9254
|
+
} catch (error2) {
|
|
9255
|
+
throw new KeychainUnavailableError(operation, error2);
|
|
9256
9256
|
}
|
|
9257
9257
|
}
|
|
9258
9258
|
/** Reads the stored secret. Never throws for bad data; throws only when the keychain itself fails. */
|
|
@@ -9362,9 +9362,9 @@ function resultPagePath(kind) {
|
|
|
9362
9362
|
return join24("sign-in", `sign-in-callback-${kind}.html`);
|
|
9363
9363
|
}
|
|
9364
9364
|
function styleHashes(html) {
|
|
9365
|
-
return [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)].map((
|
|
9366
|
-
const
|
|
9367
|
-
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")}'`;
|
|
9368
9368
|
});
|
|
9369
9369
|
}
|
|
9370
9370
|
function resultPageHeaders(html) {
|
|
@@ -9448,8 +9448,8 @@ async function loadIdentityConfiguration(environment, options = {}) {
|
|
|
9448
9448
|
const response = await fetchImpl(environment.discoveryUrl, { redirect: "manual" });
|
|
9449
9449
|
if (!response.ok) throw new Error(`discovery returned HTTP ${response.status}`);
|
|
9450
9450
|
metadata = await response.json();
|
|
9451
|
-
} catch (
|
|
9452
|
-
throw new IdentityUnreachableError(environment.signInHost,
|
|
9451
|
+
} catch (error2) {
|
|
9452
|
+
throw new IdentityUnreachableError(environment.signInHost, error2);
|
|
9453
9453
|
}
|
|
9454
9454
|
if (typeof metadata.issuer !== "string" || !metadata.token_endpoint || !metadata.authorization_endpoint) {
|
|
9455
9455
|
throw new IdentityUnreachableError(environment.signInHost, new Error("discovery document incomplete"));
|
|
@@ -9574,8 +9574,8 @@ async function browserLogin(config, options) {
|
|
|
9574
9574
|
const result = toTokenResult(response);
|
|
9575
9575
|
await options.onValidated?.(result);
|
|
9576
9576
|
redirectToResult({ kind: "success", result });
|
|
9577
|
-
}).catch((
|
|
9578
|
-
redirectToResult({ kind: "failure", error });
|
|
9577
|
+
}).catch((error2) => {
|
|
9578
|
+
redirectToResult({ kind: "failure", error: error2 });
|
|
9579
9579
|
});
|
|
9580
9580
|
});
|
|
9581
9581
|
timer = setTimeout(() => reject(new LoginTimeoutError()), timeoutMs);
|
|
@@ -9607,7 +9607,7 @@ function openInDefaultBrowser(url) {
|
|
|
9607
9607
|
const [command, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
9608
9608
|
return new Promise((resolve10) => {
|
|
9609
9609
|
try {
|
|
9610
|
-
const child = execFile3(command, args, { windowsHide: true }, (
|
|
9610
|
+
const child = execFile3(command, args, { windowsHide: true }, (error2) => resolve10(!error2));
|
|
9611
9611
|
child.on("error", () => resolve10(false));
|
|
9612
9612
|
} catch {
|
|
9613
9613
|
resolve10(false);
|
|
@@ -9713,11 +9713,11 @@ var FileSecretBackend = class {
|
|
|
9713
9713
|
writeFileSync(tmp, JSON.stringify(entries), { encoding: "utf8", mode: 384 });
|
|
9714
9714
|
try {
|
|
9715
9715
|
this.restrict(tmp);
|
|
9716
|
-
} catch (
|
|
9716
|
+
} catch (error2) {
|
|
9717
9717
|
rmSync(tmp, { force: true });
|
|
9718
9718
|
throw new Error(
|
|
9719
9719
|
"Could not restrict the session file to your account, so it was not written.",
|
|
9720
|
-
{ cause:
|
|
9720
|
+
{ cause: error2 }
|
|
9721
9721
|
);
|
|
9722
9722
|
}
|
|
9723
9723
|
renameSync(tmp, this.filePath);
|
|
@@ -9772,9 +9772,9 @@ function createSessionStorage(options = {}) {
|
|
|
9772
9772
|
try {
|
|
9773
9773
|
const fromKeychain = interpret(keychain.load(), "keychain");
|
|
9774
9774
|
if (fromKeychain) return fromKeychain;
|
|
9775
|
-
} catch (
|
|
9776
|
-
if (!(
|
|
9777
|
-
keychainError =
|
|
9775
|
+
} catch (error2) {
|
|
9776
|
+
if (!(error2 instanceof KeychainUnavailableError)) throw error2;
|
|
9777
|
+
keychainError = error2;
|
|
9778
9778
|
}
|
|
9779
9779
|
if (fileBackend.exists()) {
|
|
9780
9780
|
const fromFile = interpret(file.load(), "file");
|
|
@@ -9802,8 +9802,8 @@ function createSessionStorage(options = {}) {
|
|
|
9802
9802
|
const hadKeychainSession = keychain.load().state !== "signed-out";
|
|
9803
9803
|
keychain.clear();
|
|
9804
9804
|
if (hadKeychainSession) removed.push("keychain");
|
|
9805
|
-
} catch (
|
|
9806
|
-
keychainError =
|
|
9805
|
+
} catch (error2) {
|
|
9806
|
+
keychainError = error2;
|
|
9807
9807
|
}
|
|
9808
9808
|
if (fileBackend.exists()) {
|
|
9809
9809
|
fileBackend.remove();
|
|
@@ -9833,8 +9833,8 @@ function defaultLoginDeps(assetsDir) {
|
|
|
9833
9833
|
}
|
|
9834
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.";
|
|
9835
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.";
|
|
9836
|
-
function safeErrorText(
|
|
9837
|
-
const message =
|
|
9836
|
+
function safeErrorText(error2) {
|
|
9837
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
9838
9838
|
return message.split("\n")[0].replace(/([?&](?:code|state|id_token|access_token|refresh_token)=)[^&\s]+/gi, "$1[redacted]");
|
|
9839
9839
|
}
|
|
9840
9840
|
function toSessionRecord(result, environment) {
|
|
@@ -9856,13 +9856,13 @@ async function runLogin(options, deps) {
|
|
|
9856
9856
|
let environment;
|
|
9857
9857
|
try {
|
|
9858
9858
|
environment = deps.environment();
|
|
9859
|
-
} catch (
|
|
9860
|
-
if (
|
|
9861
|
-
console.error(
|
|
9859
|
+
} catch (error2) {
|
|
9860
|
+
if (error2 instanceof UnknownEnvironmentError) {
|
|
9861
|
+
console.error(error2.message);
|
|
9862
9862
|
process.exit(1);
|
|
9863
9863
|
return;
|
|
9864
9864
|
}
|
|
9865
|
-
throw
|
|
9865
|
+
throw error2;
|
|
9866
9866
|
}
|
|
9867
9867
|
if (!options.deviceCode && !deps.isInteractive()) {
|
|
9868
9868
|
console.error(
|
|
@@ -9874,9 +9874,9 @@ async function runLogin(options, deps) {
|
|
|
9874
9874
|
let config;
|
|
9875
9875
|
try {
|
|
9876
9876
|
config = await deps.loadConfiguration(environment);
|
|
9877
|
-
} catch (
|
|
9877
|
+
} catch (error2) {
|
|
9878
9878
|
console.error(
|
|
9879
|
-
|
|
9879
|
+
error2 instanceof IdentityUnreachableError ? `${error2.message} Check your network connection and try again.` : `Could not prepare the sign-in: ${safeErrorText(error2)}`
|
|
9880
9880
|
);
|
|
9881
9881
|
process.exit(1);
|
|
9882
9882
|
return;
|
|
@@ -9912,15 +9912,15 @@ async function runLogin(options, deps) {
|
|
|
9912
9912
|
});
|
|
9913
9913
|
if (!record) throw new Error("The browser sign-in ended without storing a session.");
|
|
9914
9914
|
}
|
|
9915
|
-
} catch (
|
|
9916
|
-
if (
|
|
9915
|
+
} catch (error2) {
|
|
9916
|
+
if (error2 instanceof KeychainUnavailableError) {
|
|
9917
9917
|
console.error(
|
|
9918
|
-
`${
|
|
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).`
|
|
9919
9919
|
);
|
|
9920
|
-
} else if (
|
|
9921
|
-
console.error(`${
|
|
9920
|
+
} else if (error2 instanceof LoginTimeoutError || error2 instanceof LoginDeniedError) {
|
|
9921
|
+
console.error(`${error2.message} Nothing was changed.`);
|
|
9922
9922
|
} else {
|
|
9923
|
-
console.error(`The sign-in did not complete: ${safeErrorText(
|
|
9923
|
+
console.error(`The sign-in did not complete: ${safeErrorText(error2)}. Nothing was changed.`);
|
|
9924
9924
|
}
|
|
9925
9925
|
process.exit(1);
|
|
9926
9926
|
return;
|
|
@@ -9935,21 +9935,21 @@ function runLogout(storage = createSessionStorage()) {
|
|
|
9935
9935
|
let removed;
|
|
9936
9936
|
try {
|
|
9937
9937
|
removed = storage.clear().removed;
|
|
9938
|
-
} catch (
|
|
9939
|
-
if (
|
|
9940
|
-
const fileResult =
|
|
9938
|
+
} catch (error2) {
|
|
9939
|
+
if (error2 instanceof SessionClearError) {
|
|
9940
|
+
const fileResult = error2.removed.includes("file") ? "The insecure session file was removed. " : "";
|
|
9941
9941
|
console.error(
|
|
9942
|
-
`${fileResult}${
|
|
9942
|
+
`${fileResult}${error2.keychainError.message} Any session in the OS keychain could not be verified or removed.`
|
|
9943
9943
|
);
|
|
9944
9944
|
process.exit(1);
|
|
9945
9945
|
return;
|
|
9946
9946
|
}
|
|
9947
|
-
if (
|
|
9948
|
-
console.error(`${
|
|
9947
|
+
if (error2 instanceof KeychainUnavailableError) {
|
|
9948
|
+
console.error(`${error2.message} The session could not be removed from it.`);
|
|
9949
9949
|
process.exit(1);
|
|
9950
9950
|
return;
|
|
9951
9951
|
}
|
|
9952
|
-
throw
|
|
9952
|
+
throw error2;
|
|
9953
9953
|
}
|
|
9954
9954
|
if (removed.length === 0) {
|
|
9955
9955
|
printSuccess("Nothing was stored; you were already signed out.");
|
|
@@ -9973,13 +9973,13 @@ async function runWhoami(options, deps = defaultWhoamiDeps()) {
|
|
|
9973
9973
|
let loaded;
|
|
9974
9974
|
try {
|
|
9975
9975
|
loaded = deps.storage.load();
|
|
9976
|
-
} catch (
|
|
9977
|
-
if (
|
|
9978
|
-
console.error(`${
|
|
9976
|
+
} catch (error2) {
|
|
9977
|
+
if (error2 instanceof KeychainUnavailableError) {
|
|
9978
|
+
console.error(`${error2.message} Cannot tell whether you are signed in.`);
|
|
9979
9979
|
process.exit(1);
|
|
9980
9980
|
return;
|
|
9981
9981
|
}
|
|
9982
|
-
throw
|
|
9982
|
+
throw error2;
|
|
9983
9983
|
}
|
|
9984
9984
|
if (loaded.state === "signed-out") {
|
|
9985
9985
|
console.error("Not signed in. Run `spell login`.");
|
|
@@ -10011,9 +10011,9 @@ async function runWhoami(options, deps = defaultWhoamiDeps()) {
|
|
|
10011
10011
|
const fresh = await deps.refreshSession(config, record.refreshToken, SIGN_IN_SCOPES);
|
|
10012
10012
|
deps.storage.save(toSessionRecord(fresh, environment), { insecure: source === "file" });
|
|
10013
10013
|
printSuccess(`Verified with the identity service at ${environment.signInHost}; the session is active.`);
|
|
10014
|
-
} catch (
|
|
10014
|
+
} catch (error2) {
|
|
10015
10015
|
console.error(
|
|
10016
|
-
`The identity service did not accept the session (${safeErrorText(
|
|
10016
|
+
`The identity service did not accept the session (${safeErrorText(error2)}). Run \`spell login\` again.`
|
|
10017
10017
|
);
|
|
10018
10018
|
process.exit(1);
|
|
10019
10019
|
}
|
|
@@ -10033,11 +10033,11 @@ import { join as join26 } from "path";
|
|
|
10033
10033
|
|
|
10034
10034
|
// src/modules/show-report/plan-parser.ts
|
|
10035
10035
|
function parseFrontmatter(content) {
|
|
10036
|
-
const
|
|
10037
|
-
if (!
|
|
10036
|
+
const match2 = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
10037
|
+
if (!match2) return {};
|
|
10038
10038
|
const fields = {};
|
|
10039
|
-
for (const
|
|
10040
|
-
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);
|
|
10041
10041
|
if (kv) fields[kv[1]] = kv[2].trim();
|
|
10042
10042
|
}
|
|
10043
10043
|
return fields;
|
|
@@ -10105,7 +10105,7 @@ function parseEpics(content) {
|
|
|
10105
10105
|
}
|
|
10106
10106
|
function extractSection(content, headingPrefix) {
|
|
10107
10107
|
const lines = content.split(/\r?\n/);
|
|
10108
|
-
const startIndex = lines.findIndex((
|
|
10108
|
+
const startIndex = lines.findIndex((line2) => line2.startsWith(headingPrefix));
|
|
10109
10109
|
if (startIndex === -1) return null;
|
|
10110
10110
|
let endIndex = lines.length;
|
|
10111
10111
|
for (let i = startIndex + 1; i < lines.length; i += 1) {
|
|
@@ -10127,11 +10127,11 @@ function parseParkedSection(content) {
|
|
|
10127
10127
|
}));
|
|
10128
10128
|
}
|
|
10129
10129
|
return unwrapBullets(body).flatMap((bullet) => {
|
|
10130
|
-
const
|
|
10131
|
-
if (!
|
|
10132
|
-
const tail =
|
|
10133
|
-
const title = tail ? `${
|
|
10134
|
-
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()) }];
|
|
10135
10135
|
});
|
|
10136
10136
|
}
|
|
10137
10137
|
function unwrapBullets(body) {
|
|
@@ -10141,12 +10141,12 @@ function unwrapBullets(body) {
|
|
|
10141
10141
|
if (current) bullets.push(current.join(" ").replace(/\s+/g, " ").trim());
|
|
10142
10142
|
current = null;
|
|
10143
10143
|
};
|
|
10144
|
-
for (const
|
|
10145
|
-
if (/^- /.test(
|
|
10144
|
+
for (const line2 of body.split(/\r?\n/)) {
|
|
10145
|
+
if (/^- /.test(line2)) {
|
|
10146
10146
|
flush();
|
|
10147
|
-
current = [
|
|
10147
|
+
current = [line2.slice(2).trim()];
|
|
10148
10148
|
} else if (current !== null) {
|
|
10149
|
-
if (/^\s+\S/.test(
|
|
10149
|
+
if (/^\s+\S/.test(line2)) current.push(line2.trim());
|
|
10150
10150
|
else flush();
|
|
10151
10151
|
}
|
|
10152
10152
|
}
|
|
@@ -10164,8 +10164,8 @@ function computeCoverageOutcome(content) {
|
|
|
10164
10164
|
const dispositioned = total - parkedCount;
|
|
10165
10165
|
return `${total} of ${total} items dispositioned \u2014 ${dispositioned} routed to an epic, ${parkedCount} parked`;
|
|
10166
10166
|
}
|
|
10167
|
-
function stripMarkdown(
|
|
10168
|
-
return
|
|
10167
|
+
function stripMarkdown(text2) {
|
|
10168
|
+
return text2.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*/g, "").replace(/`/g, "").trim();
|
|
10169
10169
|
}
|
|
10170
10170
|
|
|
10171
10171
|
// src/modules/show-report/queue-parser.ts
|
|
@@ -10211,8 +10211,8 @@ function parseVerificationLedgerSection(content, programNameWords) {
|
|
|
10211
10211
|
}
|
|
10212
10212
|
return rows;
|
|
10213
10213
|
}
|
|
10214
|
-
function stripMarkdown2(
|
|
10215
|
-
return
|
|
10214
|
+
function stripMarkdown2(text2) {
|
|
10215
|
+
return text2.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*/g, "").replace(/`/g, "").trim();
|
|
10216
10216
|
}
|
|
10217
10217
|
|
|
10218
10218
|
// src/modules/show-report/sources.ts
|
|
@@ -10250,8 +10250,8 @@ async function getCloseCommit(cwd, completedDate, generatedOutputs = []) {
|
|
|
10250
10250
|
}
|
|
10251
10251
|
const log = await runGitTextOrNull(cwd, ["log", "--format=%H%x09%cs", "HEAD", ...excludeOutputs(generatedOutputs)]);
|
|
10252
10252
|
if (log === null || log === "") return null;
|
|
10253
|
-
for (const
|
|
10254
|
-
const [sha, landed] =
|
|
10253
|
+
for (const line2 of log.split("\n")) {
|
|
10254
|
+
const [sha, landed] = line2.split(" ");
|
|
10255
10255
|
if (sha && landed && landed <= completedDate) return sha;
|
|
10256
10256
|
}
|
|
10257
10257
|
return null;
|
|
@@ -10348,9 +10348,9 @@ function buildNeedsYou(queueEntries) {
|
|
|
10348
10348
|
return entry;
|
|
10349
10349
|
});
|
|
10350
10350
|
}
|
|
10351
|
-
function truncateAtWord(
|
|
10352
|
-
if (
|
|
10353
|
-
const window =
|
|
10351
|
+
function truncateAtWord(text2, limit) {
|
|
10352
|
+
if (text2.length <= limit) return text2;
|
|
10353
|
+
const window = text2.slice(0, limit - 1);
|
|
10354
10354
|
const lastSpace = window.lastIndexOf(" ");
|
|
10355
10355
|
const head = lastSpace > 0 ? window.slice(0, lastSpace) : window;
|
|
10356
10356
|
return `${head.replace(/[\s,;:.—-]+$/, "")}\u2026`;
|
|
@@ -10476,11 +10476,11 @@ var CATEGORY_ORDER = [
|
|
|
10476
10476
|
"docs",
|
|
10477
10477
|
"platform"
|
|
10478
10478
|
];
|
|
10479
|
-
function escapeHtml(
|
|
10480
|
-
return
|
|
10479
|
+
function escapeHtml(text2) {
|
|
10480
|
+
return text2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
10481
10481
|
}
|
|
10482
|
-
function inlineMarkupToHtml(
|
|
10483
|
-
const escaped = escapeHtml(
|
|
10482
|
+
function inlineMarkupToHtml(text2) {
|
|
10483
|
+
const escaped = escapeHtml(text2);
|
|
10484
10484
|
const withLinks = escaped.replace(
|
|
10485
10485
|
/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g,
|
|
10486
10486
|
'<a href="$2">$1</a>'
|
|
@@ -10552,12 +10552,12 @@ function applyReportTheme(html, theme) {
|
|
|
10552
10552
|
if (!radioTagPattern(theme).test(html)) return { html, applied: false };
|
|
10553
10553
|
let result = html;
|
|
10554
10554
|
for (const id of REPORT_THEMES) {
|
|
10555
|
-
const
|
|
10556
|
-
if (!
|
|
10557
|
-
const original =
|
|
10555
|
+
const match2 = radioTagPattern(id).exec(result);
|
|
10556
|
+
if (!match2) continue;
|
|
10557
|
+
const original = match2[0];
|
|
10558
10558
|
const replaced = id === theme ? withChecked(original) : withoutChecked(original);
|
|
10559
10559
|
if (replaced !== original) {
|
|
10560
|
-
result = result.slice(0,
|
|
10560
|
+
result = result.slice(0, match2.index) + replaced + result.slice(match2.index + original.length);
|
|
10561
10561
|
}
|
|
10562
10562
|
}
|
|
10563
10563
|
return { html: result, applied: true };
|
|
@@ -10600,9 +10600,9 @@ async function discoverPrograms(rootDir) {
|
|
|
10600
10600
|
let entries;
|
|
10601
10601
|
try {
|
|
10602
10602
|
entries = (await readdir13(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
|
|
10603
|
-
} catch (
|
|
10604
|
-
if (
|
|
10605
|
-
throw
|
|
10603
|
+
} catch (error2) {
|
|
10604
|
+
if (error2.code === "ENOENT") return { programs: [], skipped: [] };
|
|
10605
|
+
throw error2;
|
|
10606
10606
|
}
|
|
10607
10607
|
const programs = [];
|
|
10608
10608
|
const skipped = [];
|
|
@@ -10624,9 +10624,9 @@ function normalizeLineEndings2(content) {
|
|
|
10624
10624
|
async function readOptionalFile(path5) {
|
|
10625
10625
|
try {
|
|
10626
10626
|
return await readFile24(path5, "utf8");
|
|
10627
|
-
} catch (
|
|
10628
|
-
if (
|
|
10629
|
-
throw
|
|
10627
|
+
} catch (error2) {
|
|
10628
|
+
if (error2.code === "ENOENT") return null;
|
|
10629
|
+
throw error2;
|
|
10630
10630
|
}
|
|
10631
10631
|
}
|
|
10632
10632
|
async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
|
|
@@ -10757,9 +10757,1400 @@ async function runReport(targetDir, options = {}, assetsDir) {
|
|
|
10757
10757
|
for (const path5 of result.repaired) console.log(` ${path5}`);
|
|
10758
10758
|
}
|
|
10759
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
|
+
|
|
10760
12151
|
// src/index.ts
|
|
10761
12152
|
import { realpathSync as realpathSync2 } from "fs";
|
|
10762
|
-
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ??
|
|
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)";
|
|
@@ -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
|
}
|