@join-pantheon/cli 0.30.0 → 0.31.1
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 +114 -35
- package/dist/agents.js +41 -17
- package/dist/claude-confine.js +37 -21
- package/dist/cli.js +414 -38
- package/dist/client.js +258 -9
- package/dist/command-state.js +25 -2
- package/dist/config.js +79 -7
- package/dist/continued.js +42 -10
- package/dist/doctor.js +93 -15
- package/dist/file-lock.js +197 -0
- package/dist/home-guard.js +372 -17
- package/dist/identity.js +77 -1
- package/dist/mcp-supervisor.js +5 -2
- package/dist/mcp.js +78 -30
- package/dist/nonces.js +130 -0
- package/dist/notifier.js +35 -0
- package/dist/notify.js +9 -3
- package/dist/pending.js +113 -8
- package/dist/pins.js +161 -0
- package/dist/profile.js +19 -1
- package/dist/protocol.js +40 -1
- package/dist/quickstart.js +94 -13
- package/dist/reads.js +8 -1
- package/dist/relay.js +208 -35
- package/dist/repo-find.js +127 -0
- package/dist/repo-route.js +70 -0
- package/dist/roster-file.js +21 -2
- package/dist/run-failure.js +16 -0
- package/dist/service.js +7 -1
- package/dist/setup-guide.js +70 -29
- package/dist/tray.js +55 -4
- package/dist/untrusted.js +24 -5
- package/dist/upgrade.js +9 -5
- package/dist/verify-act.js +102 -0
- package/dist/worker-fleet.js +30 -9
- package/dist/worker-policy.js +96 -6
- package/dist/worker.js +1059 -93
- package/helper/macos/menubar.swift +245 -105
- package/package.json +1 -1
package/dist/relay.js
CHANGED
|
@@ -4,10 +4,10 @@ import { rmSync, readdirSync, readFileSync, statSync, existsSync, writeFileSync,
|
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
|
-
import { badRemote, Inbound, isContinuationFor, LIMITS, newId, PROTOCOL_VERSION, SENDABLE, shouldDeferToHuman, } from "./protocol.js";
|
|
7
|
+
import { badRemote, Inbound, isContinuationFor, LIMITS, newId, PROTOCOL_VERSION, RELAY_FEATURES, SENDABLE, shouldDeferToHuman, } from "./protocol.js";
|
|
8
8
|
import { VERSION } from "./version.js";
|
|
9
9
|
import { Ledger, SpaceFull } from "./ledger.js";
|
|
10
|
-
import { fingerprint, newNonce, verifyChallenge, verifyActivity, verifyAccount, verifyRead } from "./identity.js";
|
|
10
|
+
import { fingerprint, newNonce, verifyChallenge, verifyActivity, verifyAccount, verifyRead, actPayload, goalPayload, verifyBytes, verifyDelegate, canonicalJson } from "./identity.js";
|
|
11
11
|
import { Accounts, normalizeUsername, validEmail, validUsername } from "./accounts.js";
|
|
12
12
|
import { emailSenderFromEnv } from "./email.js";
|
|
13
13
|
import { Allowlist, RosterUnreadable } from "./roster-file.js";
|
|
@@ -208,6 +208,12 @@ function firstLedgerName(file) {
|
|
|
208
208
|
}
|
|
209
209
|
const GOAL_OPENED = /^goal opened: ([^\n]*)\nacceptance: ([\s\S]*?)(?:\ndeadline: (\S+))?(?:\nkind: (\w+)[\s\S]*)?$/;
|
|
210
210
|
const LIMITS_GOAL_READ = 1000;
|
|
211
|
+
function repoLabelSafe(v) {
|
|
212
|
+
if (!v)
|
|
213
|
+
return null;
|
|
214
|
+
const s = v.normalize("NFKC").replace(/[^A-Za-z0-9._@:/+~-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
|
|
215
|
+
return s || null;
|
|
216
|
+
}
|
|
211
217
|
const LIMITS_SPACE = { metaBytes: 8192, openGoals: 500, repos: 100, openInvites: 50 };
|
|
212
218
|
const SPACE_MAX_BYTES_DEFAULT = 50 * 1024 * 1024;
|
|
213
219
|
const SPACE_HARD_SLACK = 1024 * 1024;
|
|
@@ -287,6 +293,76 @@ export function netOf(ip) {
|
|
|
287
293
|
}
|
|
288
294
|
return h;
|
|
289
295
|
}
|
|
296
|
+
function idIsFresh(id) {
|
|
297
|
+
const body = /^a_([a-z0-9]+)[0-9a-f]{18}$/.exec(id)?.[1];
|
|
298
|
+
const t = body ? parseInt(body, 36) : NaN;
|
|
299
|
+
return Number.isFinite(t) && Math.abs(Date.now() - t) < DAY;
|
|
300
|
+
}
|
|
301
|
+
function actSigProblem(m, member, memberPub, teams, delegated) {
|
|
302
|
+
const sig = m.sig;
|
|
303
|
+
if (!sig)
|
|
304
|
+
return "a woken run's session must sign every act";
|
|
305
|
+
if (canonicalJson(sig.meta ?? {}) !== canonicalJson(m.meta ?? {}))
|
|
306
|
+
return "the signed meta is not the meta sent";
|
|
307
|
+
if ((sig.repo ?? null) !== (m.repo ?? null))
|
|
308
|
+
return "the signed repo is not the repo sent";
|
|
309
|
+
const keyOk = sigKeyProblem(sig, member, memberPub, teams, delegated);
|
|
310
|
+
if (keyOk)
|
|
311
|
+
return keyOk;
|
|
312
|
+
if (!m.id)
|
|
313
|
+
return "a signed act names its own id";
|
|
314
|
+
if (Math.abs(Date.now() - sig.ts) > DAY)
|
|
315
|
+
return "signed at a time more than a day from now — check this machine's clock";
|
|
316
|
+
const ok = teams.filter((t) => !!t).some((team) => verifyBytes(sig.pub, actPayload({
|
|
317
|
+
id: m.id, team, member, keyRole: sig.role, nonce: sig.nonce, ts: sig.ts,
|
|
318
|
+
act: m.act, goal: m.goal ?? null, ref: m.ref ?? null, to: m.to ?? null,
|
|
319
|
+
defers_to: m.defers_to ?? null, affects: m.affects ?? null, repo: m.repo ?? null,
|
|
320
|
+
summary: m.summary ?? null, body: m.body, meta: m.meta ?? {},
|
|
321
|
+
}), sig.s));
|
|
322
|
+
return ok ? null : "the signature does not match this act";
|
|
323
|
+
}
|
|
324
|
+
function goalSigProblem(m, member, memberPub, teams, delegated) {
|
|
325
|
+
const sig = m.sig;
|
|
326
|
+
if (!sig)
|
|
327
|
+
return "a woken run's session must sign the goals it opens";
|
|
328
|
+
const inviteSent = m.invite ? [...new Set(m.invite)].sort().join(",") : null;
|
|
329
|
+
const inviteSigned = sig.invite === null ? null : [...new Set(sig.invite)].sort().join(",");
|
|
330
|
+
if (inviteSent !== inviteSigned)
|
|
331
|
+
return "the signed invitations are not the ones sent";
|
|
332
|
+
if ((sig.budget ?? null) !== (m.budget ?? null) || (sig.deadline ?? null) !== (m.deadline ?? null) || (sig.kind ?? null) !== (m.kind ?? null))
|
|
333
|
+
return "the signed budget, deadline or kind is not what was sent";
|
|
334
|
+
const keyOk = sigKeyProblem(sig, member, memberPub, teams, delegated);
|
|
335
|
+
if (keyOk)
|
|
336
|
+
return keyOk;
|
|
337
|
+
if (!m.id)
|
|
338
|
+
return "a signed goal names its own id";
|
|
339
|
+
if (Math.abs(Date.now() - sig.ts) > DAY)
|
|
340
|
+
return "signed at a time more than a day from now — check this machine's clock";
|
|
341
|
+
const ok = teams.filter((t) => !!t).some((team) => verifyBytes(sig.pub, goalPayload({
|
|
342
|
+
id: m.id, team, member, keyRole: sig.role, nonce: sig.nonce, ts: sig.ts,
|
|
343
|
+
title: m.title, acceptance: m.acceptance ?? "", invite: sig.invite,
|
|
344
|
+
budget: sig.budget ?? null, deadline: sig.deadline ?? null, kind: sig.kind ?? null,
|
|
345
|
+
}), sig.s));
|
|
346
|
+
return ok ? null : "the signature does not match this goal";
|
|
347
|
+
}
|
|
348
|
+
function sigKeyProblem(sig, member, memberPub, teams, delegated) {
|
|
349
|
+
if (delegated) {
|
|
350
|
+
if (sig.role !== "run" || sig.pub !== delegated.pub || sig.cert?.pub !== delegated.pub)
|
|
351
|
+
return "a woken run signs with the key its session was opened with";
|
|
352
|
+
}
|
|
353
|
+
if (sig.role === "member")
|
|
354
|
+
return sig.pub === memberPub ? null : `signed with a key that is not ${member}'s`;
|
|
355
|
+
if (!sig.cert || sig.cert.pub !== sig.pub)
|
|
356
|
+
return "a run's signature needs its certificate";
|
|
357
|
+
const c = sig.cert;
|
|
358
|
+
return teams.filter((t) => !!t).some((t) => verifyDelegate(memberPub, t, member, c)) ? null : `that run key is not vouched for by ${member}, or it has expired`;
|
|
359
|
+
}
|
|
360
|
+
function withoutFromRepo(meta) {
|
|
361
|
+
if (!meta || typeof meta !== "object")
|
|
362
|
+
return {};
|
|
363
|
+
const { from_repo: _f, ...rest } = meta;
|
|
364
|
+
return rest;
|
|
365
|
+
}
|
|
290
366
|
export class Relay {
|
|
291
367
|
opts;
|
|
292
368
|
spaces = new Map();
|
|
@@ -357,6 +433,7 @@ export class Relay {
|
|
|
357
433
|
authEpoch = 0;
|
|
358
434
|
rotateOnRemove;
|
|
359
435
|
backupTimer;
|
|
436
|
+
get features() { return this.opts.features ?? RELAY_FEATURES; }
|
|
360
437
|
constructor(opts = {}) {
|
|
361
438
|
this.opts = opts;
|
|
362
439
|
this.dataDir = opts.dataDir ?? "./pantheon-data";
|
|
@@ -566,12 +643,12 @@ export class Relay {
|
|
|
566
643
|
const now = Date.now();
|
|
567
644
|
const owed = [...space.obligations.values()]
|
|
568
645
|
.filter((o) => (member ? o.owner === member : true))
|
|
569
|
-
.map((o) => ({ ...o, claimed: !!o.claimed_by &&
|
|
646
|
+
.map((o) => ({ ...o, claimed: !!o.claimed_by && this.claimHolds(space, o.claimed_by, now) }));
|
|
570
647
|
const closed = [...space.goals.values()].filter((g) => g.state === "CLOSED").map((g) => g.id);
|
|
571
648
|
const worker_online = member
|
|
572
649
|
? [...(space.members.get(member)?.sessions.values() ?? [])].some((s) => s.role === "worker")
|
|
573
650
|
: false;
|
|
574
|
-
return this.json(res, 200, { obligations: owed, closed_goals: closed, seq: space.ledger.head, worker_online });
|
|
651
|
+
return this.json(res, 200, { obligations: owed, closed_goals: closed, seq: space.ledger.head, worker_online, space_id: space.allow.spaceId(), members: this.members(space) });
|
|
575
652
|
}
|
|
576
653
|
if (url.pathname === "/activity") {
|
|
577
654
|
if (req.method !== "POST")
|
|
@@ -613,7 +690,10 @@ export class Relay {
|
|
|
613
690
|
if (oldest)
|
|
614
691
|
member.activity.delete(oldest[0]);
|
|
615
692
|
}
|
|
616
|
-
member.activity.
|
|
693
|
+
const prev = member.activity.get(session);
|
|
694
|
+
if (prev?.ts !== undefined && ts < prev.ts)
|
|
695
|
+
return this.json(res, 409, { error: "an activity report older than the last one from this session — ignored" });
|
|
696
|
+
member.activity.set(session, { busy: state === "busy", at: Date.now(), ts });
|
|
617
697
|
const seen = Number(url.searchParams.get("seen"));
|
|
618
698
|
if (Number.isFinite(seen) && seen > (member.seen_seq ?? 0))
|
|
619
699
|
member.seen_seq = seen;
|
|
@@ -1621,7 +1701,14 @@ button:hover{background:#1d4ed8}.err{color:var(--bad);margin-top:12px}
|
|
|
1621
1701
|
if (!pub || !Number.isFinite(ts) || Math.abs(Date.now() - ts) >= LIMITS.activityFreshMs)
|
|
1622
1702
|
return refused;
|
|
1623
1703
|
const { sig, ...signed } = params;
|
|
1624
|
-
|
|
1704
|
+
let signer = pub;
|
|
1705
|
+
if (signed.dpub) {
|
|
1706
|
+
const cert = { pub: signed.dpub, run: signed.drun ?? "", expires: Number(signed.dexp), sig: signed.dcert ?? "" };
|
|
1707
|
+
if (![target.allow.spaceId(), name].some((t) => t && verifyDelegate(pub, t, reader, cert)))
|
|
1708
|
+
return refused;
|
|
1709
|
+
signer = signed.dpub;
|
|
1710
|
+
}
|
|
1711
|
+
if (!verifyRead(signer, req.method ?? "GET", url.pathname, signed, sig))
|
|
1625
1712
|
return refused;
|
|
1626
1713
|
if (this.requireAccounts && this.accounts && !this.localTrusted(ip)) {
|
|
1627
1714
|
const auth = this.accounts.authorize(pub, reader);
|
|
@@ -2235,6 +2322,7 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2235
2322
|
let sessionId = "";
|
|
2236
2323
|
let role = "interactive";
|
|
2237
2324
|
let myPubkey = "";
|
|
2325
|
+
let delegated = null;
|
|
2238
2326
|
let authEpoch = -1, authOk = true;
|
|
2239
2327
|
let lastMsg = "";
|
|
2240
2328
|
let alive = true;
|
|
@@ -2300,7 +2388,7 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2300
2388
|
helloTimer = null;
|
|
2301
2389
|
} };
|
|
2302
2390
|
ws.on("close", clearHelloTimer);
|
|
2303
|
-
this.send(ws, { t: "challenge", nonce, v: PROTOCOL_VERSION });
|
|
2391
|
+
this.send(ws, { t: "challenge", nonce, v: PROTOCOL_VERSION, features: [...this.features] });
|
|
2304
2392
|
ws.on("message", (raw) => {
|
|
2305
2393
|
if (refusedOut)
|
|
2306
2394
|
return;
|
|
@@ -2333,7 +2421,11 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2333
2421
|
return reject("BAD_NONCE", "challenge mismatch — reconnect");
|
|
2334
2422
|
if (!m.token && !m.enroll)
|
|
2335
2423
|
return reject("NO_CREDENTIAL", "present either a space token or an invite");
|
|
2336
|
-
if (
|
|
2424
|
+
if (m.delegate && !this.features.includes("delegate"))
|
|
2425
|
+
return reject("BAD_SIGNATURE", `signature does not match the presented key for "${m.member}"`);
|
|
2426
|
+
if (m.delegate && m.enroll)
|
|
2427
|
+
return reject("BAD_DELEGATE", "a woken run cannot redeem an invite");
|
|
2428
|
+
if (!verifyChallenge(m.delegate ? m.delegate.pub : m.pubkey, m.space, m.member, m.nonce, m.sig)) {
|
|
2337
2429
|
return reject("BAD_SIGNATURE", `signature does not match the presented key for "${m.member}"`);
|
|
2338
2430
|
}
|
|
2339
2431
|
const gated = this.requireAccounts && !!this.accounts && !this.localTrusted(ip);
|
|
@@ -2481,17 +2573,24 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2481
2573
|
newRepo = m.repo;
|
|
2482
2574
|
else {
|
|
2483
2575
|
const known = sp.allow.repoList().map((r) => r.origin ?? r.fingerprint).join(", ");
|
|
2484
|
-
this.log(`[${sp.name}] ! REFUSED ${m.member}: repo ${m.repo_origin ?? m.repo} not bound to this space`);
|
|
2576
|
+
this.log(`[${sp.name}] ! REFUSED ${m.member}: repo ${repoLabelSafe(m.repo_origin) ?? m.repo} not bound to this space`);
|
|
2485
2577
|
return reject("REPO_MISMATCH", `this checkout (${m.repo_origin ?? "no origin"}) is not one of the repos "${sp.name}" works from (${known || "none yet"}). ` +
|
|
2486
2578
|
`Open your agent in the team's repo, or if this repo really belongs to the team: pantheon repo add`);
|
|
2487
2579
|
}
|
|
2488
2580
|
}
|
|
2581
|
+
if (m.delegate) {
|
|
2582
|
+
const teamIds = [sp.allow.spaceId(), sp.name];
|
|
2583
|
+
if (!teamIds.some((t) => verifyDelegate(m.pubkey, t, m.member, m.delegate))) {
|
|
2584
|
+
return reject("BAD_DELEGATE", `that run key is not vouched for by ${m.member}'s key for "${sp.name}", or its certificate has expired`);
|
|
2585
|
+
}
|
|
2586
|
+
delegated = { pub: m.delegate.pub, run: m.delegate.run, expires: m.delegate.expires };
|
|
2587
|
+
}
|
|
2489
2588
|
space = sp;
|
|
2490
2589
|
myPubkey = m.pubkey;
|
|
2491
2590
|
clearHelloTimer();
|
|
2492
2591
|
sessionId = acctSession ?? this.mintSession();
|
|
2493
|
-
role = m.role;
|
|
2494
|
-
me = this.attach(sp, m.member, m.display ?? m.member, m.agent ?? "unknown", sessionId,
|
|
2592
|
+
role = delegated ? "worker" : m.role;
|
|
2593
|
+
me = this.attach(sp, m.member, m.display ?? m.member, m.agent ?? "unknown", sessionId, role, ws, m.pkg);
|
|
2495
2594
|
const firstSession = me.sessions.size === 1;
|
|
2496
2595
|
const prior = typeof m.session === "string" ? m.session : "";
|
|
2497
2596
|
const stale = prior && prior !== sessionId ? me.sessions.get(prior) : undefined;
|
|
@@ -2519,16 +2618,17 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2519
2618
|
this.pushObligations(sp);
|
|
2520
2619
|
}
|
|
2521
2620
|
sp.lastTouch = Date.now();
|
|
2522
|
-
if (newRepo && sp.allow.addRepo(newRepo, m.repo_origin
|
|
2523
|
-
|
|
2524
|
-
this.
|
|
2621
|
+
if (newRepo && sp.allow.addRepo(newRepo, repoLabelSafe(m.repo_origin), me.id)) {
|
|
2622
|
+
const label = repoLabelSafe(m.repo_origin);
|
|
2623
|
+
this.log(`[${sp.name}] + repo ${label ?? newRepo} registered by ${me.id}`);
|
|
2624
|
+
this.commit(sp, { from: me.id, act: "NOTE", to: null, goal: null, ref: null, body: `repo registered with this space: ${label ?? newRepo}`, meta: { repo: newRepo, repo_origin: label ?? null } });
|
|
2525
2625
|
this.broadcast(sp, { t: "repos", repos: sp.allow.repoList() });
|
|
2526
2626
|
}
|
|
2527
2627
|
this.send(ws, {
|
|
2528
2628
|
t: "welcome", member: me.id, session: sessionId, space: sp.name, seq: sp.ledger.head,
|
|
2529
2629
|
roster: this.roster(sp), goals: [...sp.goals.values()],
|
|
2530
2630
|
obligations: [...sp.obligations.values()], members: this.members(sp), profiles: [...sp.profiles.values()],
|
|
2531
|
-
space_id: sp.allow.spaceId(), repos: sp.allow.repoList(),
|
|
2631
|
+
space_id: sp.allow.spaceId(), repos: sp.allow.repoList(), features: [...this.features],
|
|
2532
2632
|
...(acct ? { plan: this.planView(acct, sp) } : {}),
|
|
2533
2633
|
...(enrolled ? { token: enrolled } : staleToken && this.knownToken(sp) ? { token: sp.token } : {}),
|
|
2534
2634
|
});
|
|
@@ -2565,6 +2665,14 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2565
2665
|
this.hangUp(ws, "REVOKED");
|
|
2566
2666
|
return;
|
|
2567
2667
|
}
|
|
2668
|
+
if (delegated && Date.now() > delegated.expires + 10 * 60_000) {
|
|
2669
|
+
if (ws.readyState === 1)
|
|
2670
|
+
this.hangUp(ws, "REVOKED");
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
if (delegated && m.t !== "act" && m.t !== "open_goal" && m.t !== "claim" && m.t !== "worker_state") {
|
|
2674
|
+
return fail("NOT_FOR_A_RUN", `an unattended run cannot ${m.t.replace(/_/g, " ")} — its member does that from their own session`, m.cid);
|
|
2675
|
+
}
|
|
2568
2676
|
if (m.t === "publish_profile") {
|
|
2569
2677
|
if (m.profile.member !== me.id) {
|
|
2570
2678
|
return fail("PROFILE_NOT_YOURS", `you may publish only the profile for ${me.id}`, m.cid);
|
|
@@ -2597,6 +2705,13 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2597
2705
|
const target = space.allow.get(m.member);
|
|
2598
2706
|
if (!target)
|
|
2599
2707
|
return fail("NO_SUCH_MEMBER", `"${m.member}" has never connected, so there is no key to admit`, m.cid);
|
|
2708
|
+
if (!m.revoke && !m.forget && !m.wake_only && !target.admitted && space.allow.removedWhen(m.member) !== null) {
|
|
2709
|
+
const founder = space.allow.founder();
|
|
2710
|
+
if (founder && me.id !== founder)
|
|
2711
|
+
return fail("FOUNDER_ONLY", `${m.member} was removed from "${space.name}"; only ${founder}, who founded it, can bring them back`, m.cid);
|
|
2712
|
+
}
|
|
2713
|
+
if (m.wake_only && (m.revoke || m.forget || !target.admitted))
|
|
2714
|
+
return fail("NOT_ADMITTED", `"${m.member}" is not in "${space.name}" now, so there is no wake permission to change`, m.cid);
|
|
2600
2715
|
if (!m.revoke && !m.forget) {
|
|
2601
2716
|
const plan = this.spacePlan(space.name);
|
|
2602
2717
|
if (plan && m.may_wake && !plan.limits.wake)
|
|
@@ -2727,14 +2842,14 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2727
2842
|
if (existing && m.base && existing.added_by !== me.id && space.allow.founder() !== me.id) {
|
|
2728
2843
|
return fail("NOT_YOURS", `only ${existing.added_by} (who registered this repo) or the team's founder can change which branch its runs start from`, m.cid);
|
|
2729
2844
|
}
|
|
2730
|
-
const added = space.allow.addRepo(m.repo, m.origin
|
|
2845
|
+
const added = space.allow.addRepo(m.repo, repoLabelSafe(m.origin), me.id, m.base ?? null);
|
|
2731
2846
|
if (!added && m.base && space.allow.setRepoBase(m.repo, m.base)) {
|
|
2732
|
-
this.log(`[${space.name}] ~ repo ${m.origin ?? m.repo} base branch → ${m.base} (by ${me.id})`);
|
|
2847
|
+
this.log(`[${space.name}] ~ repo ${repoLabelSafe(m.origin) ?? m.repo} base branch → ${m.base} (by ${me.id})`);
|
|
2733
2848
|
this.broadcast(space, { t: "repos", repos: space.allow.repoList() });
|
|
2734
2849
|
}
|
|
2735
2850
|
if (added) {
|
|
2736
|
-
this.log(`[${space.name}] + repo ${m.origin ?? m.repo} registered by ${me.id}${m.base ? ` (base ${m.base})` : ""}`);
|
|
2737
|
-
this.commit(space, { from: me.id, session: sessionId, role, act: "NOTE", to: null, goal: null, ref: null, body: `repo registered with this space: ${m.origin ?? m.repo}`, meta: { repo: m.repo, repo_origin: m.origin ?? null } });
|
|
2851
|
+
this.log(`[${space.name}] + repo ${repoLabelSafe(m.origin) ?? m.repo} registered by ${me.id}${m.base ? ` (base ${m.base})` : ""}`);
|
|
2852
|
+
this.commit(space, { from: me.id, session: sessionId, role, act: "NOTE", to: null, goal: null, ref: null, body: `repo registered with this space: ${repoLabelSafe(m.origin) ?? m.repo}`, meta: { repo: m.repo, repo_origin: repoLabelSafe(m.origin) ?? null } });
|
|
2738
2853
|
this.broadcast(space, { t: "repos", repos: space.allow.repoList() });
|
|
2739
2854
|
}
|
|
2740
2855
|
this.send(ws, { t: "ack", cid: m.cid, id: m.repo, seq: space.ledger.head });
|
|
@@ -2770,11 +2885,11 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2770
2885
|
}
|
|
2771
2886
|
}
|
|
2772
2887
|
const held = space.contLocks.get(m.obligation);
|
|
2773
|
-
if (held && held.session !== sessionId &&
|
|
2888
|
+
if (held && held.session !== sessionId && this.claimHolds(space, held)) {
|
|
2774
2889
|
return fail("CLAIMED", `another of your sessions (${held.session}) is handling this`, m.cid);
|
|
2775
2890
|
}
|
|
2776
2891
|
for (const [k, v] of space.contLocks) {
|
|
2777
|
-
if (
|
|
2892
|
+
if (!this.claimHolds(space, v))
|
|
2778
2893
|
space.contLocks.delete(k);
|
|
2779
2894
|
}
|
|
2780
2895
|
space.contLocks.set(m.obligation, { session: sessionId, member: me.id, at: Date.now() });
|
|
@@ -2787,7 +2902,7 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2787
2902
|
if (o.owner !== me.id)
|
|
2788
2903
|
return fail("NOT_YOURS", `${o.owner} owes that, not you`, m.cid);
|
|
2789
2904
|
if (o.claimed_by && o.claimed_by.session !== sessionId) {
|
|
2790
|
-
if (
|
|
2905
|
+
if (this.claimHolds(space, o.claimed_by)) {
|
|
2791
2906
|
const holder = o.claimed_by;
|
|
2792
2907
|
const live = [...me.sessions.keys()].includes(holder.session);
|
|
2793
2908
|
const left = Math.ceil((LIMITS.claimTtlMs - (Date.now() - holder.at)) / 60_000);
|
|
@@ -2826,12 +2941,19 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2826
2941
|
if (!space.members.has(id) && !space.allow.get(id)?.admitted)
|
|
2827
2942
|
return fail("NO_SUCH_MEMBER", `nobody here is called "${id}"`, m.cid);
|
|
2828
2943
|
}
|
|
2944
|
+
if (!this.features.includes("act-sig"))
|
|
2945
|
+
delete m.sig;
|
|
2946
|
+
if (m.sig || delegated) {
|
|
2947
|
+
const why = goalSigProblem(m, me.id, myPubkey, [space.allow.spaceId(), space.name], delegated);
|
|
2948
|
+
if (why)
|
|
2949
|
+
return fail("BAD_GOAL_SIGNATURE", why, m.cid);
|
|
2950
|
+
}
|
|
2829
2951
|
const invited = (m.invite ?? [...space.members.keys()]).filter((x) => x !== me.id);
|
|
2830
2952
|
if (m.deadline !== undefined && m.deadline <= Date.now()) {
|
|
2831
2953
|
return fail("DEADLINE_PAST", `deadline ${new Date(m.deadline).toISOString()} is already in the past`, m.cid);
|
|
2832
2954
|
}
|
|
2833
2955
|
const goal = {
|
|
2834
|
-
id: newId("g"),
|
|
2956
|
+
id: m.id && !space.goals.has(m.id) ? m.id : newId("g"),
|
|
2835
2957
|
title: m.title,
|
|
2836
2958
|
acceptance: m.acceptance,
|
|
2837
2959
|
members: [me.id, ...new Set(invited)],
|
|
@@ -2842,6 +2964,7 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2842
2964
|
opened_at: Date.now(),
|
|
2843
2965
|
...(m.deadline !== undefined ? { deadline: m.deadline } : {}),
|
|
2844
2966
|
...(m.kind !== undefined ? { kind: m.kind } : {}),
|
|
2967
|
+
...(m.sig ? { sig: m.sig } : {}),
|
|
2845
2968
|
};
|
|
2846
2969
|
space.goals.set(goal.id, goal);
|
|
2847
2970
|
space.store.flush(space.goals, space.obligations, space.profiles);
|
|
@@ -2851,13 +2974,27 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2851
2974
|
body: `goal opened: ${goal.title}\nacceptance: ${goal.acceptance || "(none stated)"}` +
|
|
2852
2975
|
(goal.deadline ? `\ndeadline: ${new Date(goal.deadline).toISOString()}` : "") +
|
|
2853
2976
|
(goal.kind === "worklist" ? `\nkind: worklist — ${me.id}'s own list; peers may read it, nobody owes anything in it` : ""),
|
|
2854
|
-
meta: { goal_members: goal.members, goal_budget: goal.budget },
|
|
2977
|
+
meta: { goal_members: goal.members, goal_budget: goal.budget, ...(m.sig ? { goal_sig: m.sig, goal_text: { title: goal.title, acceptance: goal.acceptance } } : {}) },
|
|
2855
2978
|
});
|
|
2856
2979
|
this.send(ws, { t: "ack", ...(m.cid ? { cid: m.cid } : {}), id: goal.id, seq: rec.seq });
|
|
2857
2980
|
return;
|
|
2858
2981
|
}
|
|
2859
|
-
|
|
2860
|
-
|
|
2982
|
+
if (!this.features.includes("act-sig"))
|
|
2983
|
+
delete m.sig;
|
|
2984
|
+
if (m.sig || delegated) {
|
|
2985
|
+
const why = actSigProblem(m, me.id, myPubkey, [space.allow.spaceId(), space.name], delegated);
|
|
2986
|
+
if (why)
|
|
2987
|
+
return fail("BAD_ACT_SIGNATURE", why, m.cid);
|
|
2988
|
+
}
|
|
2989
|
+
if (m.act === "NOTE" && m.meta && ("goal_members" in m.meta || "organization_profile" in m.meta || "notice" in m.meta))
|
|
2990
|
+
return fail("BAD_META", "goal_members, organization_profile and notice are written by the relay, not sent", m.cid);
|
|
2991
|
+
if (m.act === "NOTE" && m.body.startsWith("repo registered with this space: "))
|
|
2992
|
+
return fail("BAD_META", "that line is written by the relay, not sent", m.cid);
|
|
2993
|
+
const fromRepo = m.repo && space.allow.hasRepo(m.repo) ? m.repo : null;
|
|
2994
|
+
const { from_repo: _forged, ...senderMeta } = (m.meta ?? {});
|
|
2995
|
+
const hasMeta = !!m.meta && (Object.keys(senderMeta).length > 0 || _forged === undefined);
|
|
2996
|
+
const storedMeta = hasMeta || m.defers_to || m.affects || fromRepo
|
|
2997
|
+
? { ...senderMeta, ...(m.defers_to ? { defers_to: m.defers_to } : {}), ...(m.affects ? { affects: m.affects } : {}), ...(fromRepo ? { from_repo: fromRepo } : {}) }
|
|
2861
2998
|
: undefined;
|
|
2862
2999
|
const fp = actFingerprint(me.id, m.act, m.body, m.goal ?? null, m.ref ?? null, m.to ?? null, storedMeta);
|
|
2863
3000
|
const prior = space.recent.get(fp);
|
|
@@ -2865,6 +3002,10 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2865
3002
|
this.send(ws, { t: "ack", ...(m.cid ? { cid: m.cid } : {}), id: prior.id, seq: prior.seq });
|
|
2866
3003
|
return;
|
|
2867
3004
|
}
|
|
3005
|
+
if (m.sig && m.id && (space.ledger.find(m.id) || space.droppedIds?.has(m.id)))
|
|
3006
|
+
return fail("ID_TAKEN", `an act with id ${m.id} already exists`, m.cid);
|
|
3007
|
+
if (m.sig && m.id && !idIsFresh(m.id))
|
|
3008
|
+
return fail("BAD_ACT_ID", `act id ${m.id} was not made in the last day — check this machine's clock`, m.cid);
|
|
2868
3009
|
if (m.after_seq !== undefined) {
|
|
2869
3010
|
const mine = me.id;
|
|
2870
3011
|
const missed = space.ledger.since(m.after_seq)
|
|
@@ -2894,13 +3035,16 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2894
3035
|
if (wasFull && !this.freesRoom(space, me.id, m.act, m.ref ?? null))
|
|
2895
3036
|
return fail("SPACE_FULL", this.spaceFullMessage(space), m.cid);
|
|
2896
3037
|
const rec = this.commit(space, {
|
|
3038
|
+
...(m.sig && m.id ? { id: m.id } : {}),
|
|
2897
3039
|
from: me.id, session: sessionId, role, act: m.act, body: m.body,
|
|
2898
3040
|
...(m.summary ? { summary: m.summary } : {}),
|
|
3041
|
+
...(m.sig ? { sig: m.sig } : {}),
|
|
2899
3042
|
to: m.to ?? null, goal: m.goal ?? null, ref: m.ref ?? null,
|
|
2900
|
-
...(m.meta || m.defers_to || m.affects
|
|
2901
|
-
? { meta: { ...(m.meta
|
|
3043
|
+
...((m.meta && (Object.keys(withoutFromRepo(m.meta)).length > 0 || !("from_repo" in m.meta))) || m.defers_to || m.affects || fromRepo
|
|
3044
|
+
? { meta: { ...withoutFromRepo(m.meta),
|
|
2902
3045
|
...(m.defers_to ? { defers_to: m.defers_to } : {}),
|
|
2903
|
-
...(m.affects ? { affects: m.affects } : {})
|
|
3046
|
+
...(m.affects ? { affects: m.affects } : {}),
|
|
3047
|
+
...(fromRepo ? { from_repo: fromRepo } : {}) } }
|
|
2904
3048
|
: {}),
|
|
2905
3049
|
});
|
|
2906
3050
|
if (wasFull && m.act === "CLOSE") {
|
|
@@ -3022,6 +3166,10 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
3022
3166
|
this.noteStorageFailure("evicting idle spaces", e);
|
|
3023
3167
|
}
|
|
3024
3168
|
}
|
|
3169
|
+
claimHolds(sp, holder, now = Date.now()) {
|
|
3170
|
+
const live = [...sp.members.values()].some((m) => m.sessions.has(holder.session));
|
|
3171
|
+
return now - holder.at < (live ? LIMITS.claimLiveTtlMs : LIMITS.claimTtlMs);
|
|
3172
|
+
}
|
|
3025
3173
|
lapseClaims(sp, now) {
|
|
3026
3174
|
const live = new Set();
|
|
3027
3175
|
for (const m of sp.members.values())
|
|
@@ -3029,14 +3177,15 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
3029
3177
|
live.add(id);
|
|
3030
3178
|
let lapsed = 0;
|
|
3031
3179
|
for (const o of sp.obligations.values()) {
|
|
3032
|
-
if (o.claimed_by && !
|
|
3180
|
+
if (o.claimed_by && !this.claimHolds(sp, o.claimed_by, now)) {
|
|
3033
3181
|
delete o.claimed_by;
|
|
3034
3182
|
lapsed++;
|
|
3035
3183
|
}
|
|
3036
3184
|
}
|
|
3037
3185
|
for (const [k, v] of sp.contLocks)
|
|
3038
|
-
if (!
|
|
3186
|
+
if (!this.claimHolds(sp, v, now))
|
|
3039
3187
|
sp.contLocks.delete(k);
|
|
3188
|
+
void live;
|
|
3040
3189
|
if (lapsed)
|
|
3041
3190
|
this.pushObligations(sp);
|
|
3042
3191
|
}
|
|
@@ -3071,6 +3220,10 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
3071
3220
|
return { code: "ESCALATE_IS_LOCAL",
|
|
3072
3221
|
message: "ESCALATE is for your own human. Use HUMAN_REQUIRED to notify one named peer's human." };
|
|
3073
3222
|
}
|
|
3223
|
+
if (meta && ("authority" in meta || "unverified_domains" in meta)) {
|
|
3224
|
+
delete meta["authority"];
|
|
3225
|
+
delete meta["unverified_domains"];
|
|
3226
|
+
}
|
|
3074
3227
|
if (act === "HUMAN_REQUIRED" && (!to || to.length !== 1)) {
|
|
3075
3228
|
return { code: "ONE_HUMAN_REQUIRED",
|
|
3076
3229
|
message: "HUMAN_REQUIRED must name exactly one human owner. Send separate requests when separate people must decide." };
|
|
@@ -3079,6 +3232,9 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
3079
3232
|
const domains = Array.isArray(meta?.decision_domains)
|
|
3080
3233
|
? [...new Set(meta.decision_domains.filter((x) => typeof x === "string" && x.trim().length > 0))]
|
|
3081
3234
|
: [];
|
|
3235
|
+
if (domains.some((d) => d.length > 64 || /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/.test(d))) {
|
|
3236
|
+
return { code: "BAD_DECISION_DOMAIN", message: "decision_domains are single-line names of up to 64 characters, as profiles publish them" };
|
|
3237
|
+
}
|
|
3082
3238
|
if (!domains.length) {
|
|
3083
3239
|
return { code: "DECISION_DOMAINS_REQUIRED",
|
|
3084
3240
|
message: "HUMAN_REQUIRED must name decision_domains so the relay can verify the human owner. Call pantheon_route_decision first." };
|
|
@@ -3253,7 +3409,10 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
3253
3409
|
if ((partial.act === "JOIN" || partial.act === "LEAVE") && this.spaceFull(sp)) {
|
|
3254
3410
|
return { ...partial, id: newId("a"), seq: sp.ledger.head, ts: Date.now(), space: sp.name };
|
|
3255
3411
|
}
|
|
3256
|
-
|
|
3412
|
+
let id = partial.id ?? newId("a");
|
|
3413
|
+
while (!partial.id && sp.ledger.find(id))
|
|
3414
|
+
id = newId("a");
|
|
3415
|
+
const rec = sp.ledger.append({ ...partial, id, ts: Date.now(), space: sp.name }, this.maxLedgerBytes + SPACE_HARD_SLACK);
|
|
3257
3416
|
sp.lastTouch = rec.ts;
|
|
3258
3417
|
const m = sp.members.get(rec.from);
|
|
3259
3418
|
if (m) {
|
|
@@ -3365,6 +3524,7 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
3365
3524
|
break;
|
|
3366
3525
|
case "ESCALATE":
|
|
3367
3526
|
this.oblige(sp, { kind: "HUMAN", owner: `human:${rec.from}`, ref: rec.id, goal: rec.goal,
|
|
3527
|
+
...(rec.ref ? { about: rec.ref } : {}),
|
|
3368
3528
|
body: `${rec.from} escalated: ${first(rec.body)}` });
|
|
3369
3529
|
if (goal && goal.state === "OPEN") {
|
|
3370
3530
|
goal.state = "BLOCKED";
|
|
@@ -3400,6 +3560,7 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
3400
3560
|
const alreadyLive = asked && [...sp.obligations.values()].some((o) => o.kind === "HUMAN" && o.ref === asked.id);
|
|
3401
3561
|
if (asked && decider === rec.from && !stillAnswered && !alreadyLive && !retracted.has(asked.id)) {
|
|
3402
3562
|
this.oblige(sp, { kind: "HUMAN", owner: `human:${decider}`, ref: asked.id, goal: asked.goal,
|
|
3563
|
+
...(asked.act === "ESCALATE" && asked.ref ? { about: asked.ref } : {}),
|
|
3403
3564
|
body: asked.act === "ESCALATE" ? `${asked.from} escalated: ${first(asked.body)}` : `${asked.from} needs a decision: ${first(asked.body)}` });
|
|
3404
3565
|
const g = asked.goal ? sp.goals.get(asked.goal) : undefined;
|
|
3405
3566
|
if (g && g.state === "OPEN") {
|
|
@@ -3762,8 +3923,9 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
3762
3923
|
const budget = typeof meta?.goal_budget === "number" && Number.isInteger(meta.goal_budget) && meta.goal_budget > 0 ? meta.goal_budget : LIMITS.goalBudget;
|
|
3763
3924
|
return {
|
|
3764
3925
|
id: r.goal,
|
|
3765
|
-
title: mm[1],
|
|
3766
|
-
acceptance: mm[2] !== "(none stated)" ? mm[2] : "",
|
|
3926
|
+
title: typeof meta?.goal_text?.title === "string" ? meta.goal_text.title : mm[1],
|
|
3927
|
+
acceptance: typeof meta?.goal_text?.acceptance === "string" ? meta.goal_text.acceptance : (mm[2] !== "(none stated)" ? mm[2] : ""),
|
|
3928
|
+
...(meta?.goal_sig && typeof meta.goal_sig === "object" ? { sig: meta.goal_sig } : {}),
|
|
3767
3929
|
members,
|
|
3768
3930
|
state: "OPEN",
|
|
3769
3931
|
acts: 0,
|
|
@@ -3785,7 +3947,18 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
3785
3947
|
return !g || g.state !== "CLOSED";
|
|
3786
3948
|
};
|
|
3787
3949
|
const referenced = new Set([...sp.obligations.values()].map((o) => o.ref));
|
|
3950
|
+
const before = sp.ledger.all().map((r) => r.id);
|
|
3788
3951
|
const { dropped, kept } = sp.ledger.compact({ keepRecent: this.opts.ledgerKeepRecent ?? LIMITS.ledgerKeepRecent, isLive, keep: (id) => referenced.has(id) });
|
|
3952
|
+
if (dropped) {
|
|
3953
|
+
const now = Date.now();
|
|
3954
|
+
const d = (sp.droppedIds ??= new Map());
|
|
3955
|
+
for (const [id, at] of d)
|
|
3956
|
+
if (now - at > DAY)
|
|
3957
|
+
d.delete(id);
|
|
3958
|
+
for (const id of before)
|
|
3959
|
+
if (!sp.ledger.find(id))
|
|
3960
|
+
d.set(id, now);
|
|
3961
|
+
}
|
|
3789
3962
|
if (dropped)
|
|
3790
3963
|
this.log(`[${sp.name}] compacted ledger — dropped ${dropped} old act(s) from closed goals, ${kept} kept`);
|
|
3791
3964
|
}
|
|
@@ -3821,7 +3994,7 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
3821
3994
|
members(sp) {
|
|
3822
3995
|
return sp.allow.all().map((e) => ({
|
|
3823
3996
|
member: e.member, admitted: e.admitted, may_wake: e.may_wake,
|
|
3824
|
-
fingerprint: fingerprint(e.publicKey),
|
|
3997
|
+
fingerprint: fingerprint(e.publicKey), pubkey: e.publicKey,
|
|
3825
3998
|
...(e.admitted_by ? { admitted_by: e.admitted_by } : {}),
|
|
3826
3999
|
...(e.revoked_at ? { revoked_at: e.revoked_at, ...(e.revoked_by ? { revoked_by: e.revoked_by } : {}) } : {}),
|
|
3827
4000
|
...(sp.allow.founder() === e.member ? { founder: true } : {}),
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir, platform } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { repoInfo } from "./config.js";
|
|
5
|
+
const SKIP = new Set(["node_modules", "vendor", "dist", "build", "target", "out", ".next", "Pods", "DerivedData", "venv", ".venv", "__pycache__", "Library", "Applications"]);
|
|
6
|
+
export function reposUnder(dir, depth = 3, budget = { left: 4000 }) {
|
|
7
|
+
const out = [];
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
const walk = (d, level) => {
|
|
10
|
+
if (budget.left-- <= 0)
|
|
11
|
+
return;
|
|
12
|
+
if (existsSync(join(d, ".git"))) {
|
|
13
|
+
const r = repoInfo(d);
|
|
14
|
+
if (r && !seen.has(r.root)) {
|
|
15
|
+
seen.add(r.root);
|
|
16
|
+
out.push(r);
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (level >= depth)
|
|
21
|
+
return;
|
|
22
|
+
let entries = [];
|
|
23
|
+
try {
|
|
24
|
+
entries = readdirSync(d);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
for (const e of entries) {
|
|
30
|
+
if (e.startsWith(".") || SKIP.has(e))
|
|
31
|
+
continue;
|
|
32
|
+
const p = join(d, e);
|
|
33
|
+
try {
|
|
34
|
+
if (!statSync(p).isDirectory())
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
walk(p, level + 1);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
walk(resolve(dir), 0);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
export function holdsRepo(dir, budget = 20_000) {
|
|
47
|
+
let left = budget;
|
|
48
|
+
const stack = [resolve(dir)];
|
|
49
|
+
while (stack.length) {
|
|
50
|
+
const d = stack.pop();
|
|
51
|
+
if (left-- <= 0)
|
|
52
|
+
return true;
|
|
53
|
+
let entries = [];
|
|
54
|
+
try {
|
|
55
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
for (const e of entries) {
|
|
61
|
+
if (e.name === ".git")
|
|
62
|
+
return true;
|
|
63
|
+
if (e.isDirectory())
|
|
64
|
+
stack.push(join(d, e.name));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
export function commonCodeRoots(home = homedir()) {
|
|
70
|
+
return ["code", "Code", "src", "dev", "Developer", "projects", "Projects", "workspace", "work", "repos", "git", "github", "GitHub", "Documents/GitHub", "Documents/code", "Sites"]
|
|
71
|
+
.map((d) => join(home, d)).filter((d) => { try {
|
|
72
|
+
return statSync(d).isDirectory();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
} });
|
|
77
|
+
}
|
|
78
|
+
export function openedFolders(home = homedir(), os = platform()) {
|
|
79
|
+
const out = new Set();
|
|
80
|
+
const add = (p) => { if (typeof p === "string" && (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)))
|
|
81
|
+
out.add(p); };
|
|
82
|
+
try {
|
|
83
|
+
for (const k of Object.keys(JSON.parse(readFileSync(join(home, ".claude.json"), "utf8")).projects ?? {}))
|
|
84
|
+
add(k);
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
const appData = os === "darwin" ? join(home, "Library", "Application Support") : os === "win32" ? (process.env["APPDATA"] ?? join(home, "AppData", "Roaming")) : join(home, ".config");
|
|
88
|
+
for (const app of ["Code", "Cursor", "Code - Insiders", "Windsurf"]) {
|
|
89
|
+
try {
|
|
90
|
+
const text = readFileSync(join(appData, app, "User", "globalStorage", "storage.json"), "utf8");
|
|
91
|
+
for (const m of text.matchAll(/"file:\/\/([^"]+)"/g)) {
|
|
92
|
+
try {
|
|
93
|
+
add(decodeURIComponent(m[1]).replace(/^\/([A-Za-z]:)/, "$1"));
|
|
94
|
+
}
|
|
95
|
+
catch { }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
}
|
|
100
|
+
return [...out];
|
|
101
|
+
}
|
|
102
|
+
export function findTeamClones(teamFps, opts = {}) {
|
|
103
|
+
const want = new Set(teamFps);
|
|
104
|
+
const found = new Map();
|
|
105
|
+
const consider = (r, how) => { if (r && want.has(r.fingerprint) && !found.has(r.fingerprint))
|
|
106
|
+
found.set(r.fingerprint, { repo: r, how }); };
|
|
107
|
+
const home = opts.home ?? homedir();
|
|
108
|
+
if (opts.recents)
|
|
109
|
+
for (const d of openedFolders(home, opts.os)) {
|
|
110
|
+
if (found.size === want.size)
|
|
111
|
+
break;
|
|
112
|
+
try {
|
|
113
|
+
if (existsSync(d))
|
|
114
|
+
consider(repoInfo(d), "opened in your editor or agent");
|
|
115
|
+
}
|
|
116
|
+
catch { }
|
|
117
|
+
}
|
|
118
|
+
const budget = { left: 6000 };
|
|
119
|
+
for (const root of [...(opts.extra ?? []), ...commonCodeRoots(home)]) {
|
|
120
|
+
if (found.size === want.size)
|
|
121
|
+
break;
|
|
122
|
+
for (const r of reposUnder(root, 3, budget))
|
|
123
|
+
consider(r, `found in ${root.startsWith(home) ? `~${root.slice(home.length)}` : root}`);
|
|
124
|
+
}
|
|
125
|
+
return [...found.values()];
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=repo-find.js.map
|