@join-pantheon/cli 0.29.1 → 0.31.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 +142 -40
- package/bin/pantheon.js +12 -0
- package/dist/accounts.js +151 -26
- package/dist/agents.js +224 -17
- package/dist/claude-confine.js +299 -0
- package/dist/cli.js +1116 -137
- package/dist/client.js +294 -10
- package/dist/command-state.js +102 -0
- package/dist/config.js +287 -14
- package/dist/continued.js +42 -10
- package/dist/doctor.js +191 -23
- package/dist/file-lock.js +197 -0
- package/dist/grants.js +1 -1
- package/dist/history-view.js +2 -1
- package/dist/home-guard.js +565 -0
- package/dist/hooks-install.js +2 -1
- package/dist/identity.js +101 -1
- package/dist/install.js +76 -26
- package/dist/kiro-files.js +2 -2
- package/dist/ledger.js +111 -28
- package/dist/machine.js +494 -32
- package/dist/mcp-supervisor.js +29 -7
- package/dist/mcp-windows.js +47 -25
- package/dist/mcp.js +99 -36
- package/dist/network.js +39 -8
- package/dist/nonces.js +130 -0
- package/dist/notify.js +26 -9
- package/dist/pending.js +121 -22
- package/dist/persona.js +21 -0
- package/dist/pins.js +161 -0
- package/dist/profile.js +22 -1
- package/dist/protocol.js +40 -1
- package/dist/quickstart.js +173 -24
- package/dist/reads.js +37 -0
- package/dist/refusal.js +14 -1
- package/dist/relay.js +1341 -207
- package/dist/repo-find.js +127 -0
- package/dist/repo-route.js +70 -0
- package/dist/roster-file.js +190 -25
- package/dist/run-failure.js +99 -3
- package/dist/secret-guard.js +141 -0
- package/dist/service.js +91 -35
- package/dist/setup-guide.js +91 -31
- package/dist/spend.js +6 -2
- package/dist/store.js +94 -10
- package/dist/text-file.js +74 -0
- package/dist/tray.js +24 -3
- package/dist/untrusted.js +24 -5
- package/dist/update-check.js +2 -0
- package/dist/upgrade.js +9 -5
- package/dist/verify-act.js +102 -0
- package/dist/which.js +69 -12
- package/dist/worker-fleet.js +30 -9
- package/dist/worker-policy.js +231 -7
- package/dist/worker.js +1717 -130
- package/helper/macos/menubar.swift +63 -2
- package/package.json +2 -2
package/dist/protocol.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
export const PROTOCOL_VERSION = 3;
|
|
3
4
|
export const ACTS = [
|
|
4
5
|
"JOIN",
|
|
@@ -35,6 +36,7 @@ export const LIMITS = {
|
|
|
35
36
|
activityBusyTtlMs: 30 * 60 * 1_000,
|
|
36
37
|
activityPruneMs: 7 * 24 * 60 * 60 * 1_000,
|
|
37
38
|
claimTtlMs: 300_000,
|
|
39
|
+
claimLiveTtlMs: 30 * 60_000,
|
|
38
40
|
workerFallbackMs: 20_000 + 120_000 + 60_000,
|
|
39
41
|
pendingTimeoutMs: 1_500,
|
|
40
42
|
cursorTtlMs: 7 * 24 * 60 * 60 * 1_000,
|
|
@@ -80,6 +82,37 @@ function loopback(host) {
|
|
|
80
82
|
return "0.0.0.0";
|
|
81
83
|
return null;
|
|
82
84
|
}
|
|
85
|
+
export const DelegateCertSchema = z.object({
|
|
86
|
+
pub: z.string().min(32).max(200),
|
|
87
|
+
run: z.string().min(1).max(64),
|
|
88
|
+
expires: z.number().int().positive(),
|
|
89
|
+
sig: z.string().min(32).max(200),
|
|
90
|
+
});
|
|
91
|
+
export const ActSigSchema = z.object({
|
|
92
|
+
v: z.literal(1),
|
|
93
|
+
role: z.enum(["member", "run"]),
|
|
94
|
+
nonce: z.string().min(8).max(64),
|
|
95
|
+
ts: z.number().int().positive(),
|
|
96
|
+
pub: z.string().min(32).max(200),
|
|
97
|
+
s: z.string().min(32).max(200),
|
|
98
|
+
meta: z.record(z.unknown()).optional(),
|
|
99
|
+
repo: z.string().regex(/^[rl]_[0-9a-f]{16}$/).optional(),
|
|
100
|
+
cert: DelegateCertSchema.optional(),
|
|
101
|
+
});
|
|
102
|
+
export const GoalSigSchema = z.object({
|
|
103
|
+
v: z.literal(1),
|
|
104
|
+
role: z.enum(["member", "run"]),
|
|
105
|
+
nonce: z.string().min(8).max(64),
|
|
106
|
+
ts: z.number().int().positive(),
|
|
107
|
+
pub: z.string().min(32).max(200),
|
|
108
|
+
s: z.string().min(32).max(200),
|
|
109
|
+
invite: z.array(z.string().min(1).max(48)).max(16).nullable(),
|
|
110
|
+
budget: z.number().int().nullable().optional(),
|
|
111
|
+
deadline: z.number().int().nullable().optional(),
|
|
112
|
+
kind: z.string().max(32).nullable().optional(),
|
|
113
|
+
cert: DelegateCertSchema.optional(),
|
|
114
|
+
});
|
|
115
|
+
export const RELAY_FEATURES = ["act-sig", "delegate"];
|
|
83
116
|
export const Hello = z.object({
|
|
84
117
|
t: z.literal("hello"),
|
|
85
118
|
v: z.number().int().optional(),
|
|
@@ -98,6 +131,7 @@ export const Hello = z.object({
|
|
|
98
131
|
from_seq: z.number().int().nonnegative().optional(),
|
|
99
132
|
repo: z.string().regex(/^[rl]_[0-9a-f]{16}$/).optional(),
|
|
100
133
|
repo_origin: z.string().max(200).optional(),
|
|
134
|
+
delegate: DelegateCertSchema.optional(),
|
|
101
135
|
});
|
|
102
136
|
export const OrganizationProfileSchema = z.object({
|
|
103
137
|
version: z.literal(1),
|
|
@@ -147,12 +181,14 @@ export const Admit = z.object({
|
|
|
147
181
|
member: z.string().min(1).max(48),
|
|
148
182
|
may_wake: z.boolean().default(false),
|
|
149
183
|
revoke: z.boolean().default(false),
|
|
184
|
+
wake_only: z.boolean().optional(),
|
|
150
185
|
forget: z.boolean().default(false),
|
|
151
186
|
});
|
|
152
187
|
export const SendAct = z.object({
|
|
153
188
|
t: z.literal("act"),
|
|
154
189
|
cid: z.string().max(64).optional(),
|
|
155
190
|
after_seq: z.number().int().nonnegative().optional(),
|
|
191
|
+
id: z.string().regex(/^a_[A-Za-z0-9]{6,48}$/).optional(),
|
|
156
192
|
act: z.enum(ACTS),
|
|
157
193
|
body: z.string().max(LIMITS.bodyChars),
|
|
158
194
|
summary: z.string().max(LIMITS.summaryChars).optional(),
|
|
@@ -163,6 +199,7 @@ export const SendAct = z.object({
|
|
|
163
199
|
affects: z.array(z.string().min(1).max(48)).max(16).optional(),
|
|
164
200
|
meta: z.record(z.unknown()).optional(),
|
|
165
201
|
repo: z.string().regex(/^[rl]_[0-9a-f]{16}$/).optional(),
|
|
202
|
+
sig: ActSigSchema.optional(),
|
|
166
203
|
});
|
|
167
204
|
export const BindRepo = z.object({
|
|
168
205
|
t: z.literal("bind_repo"),
|
|
@@ -174,12 +211,14 @@ export const BindRepo = z.object({
|
|
|
174
211
|
export const OpenGoal = z.object({
|
|
175
212
|
t: z.literal("open_goal"),
|
|
176
213
|
cid: z.string().max(64).optional(),
|
|
214
|
+
id: z.string().regex(/^g_[a-z0-9]{8,40}$/).optional(),
|
|
177
215
|
title: z.string().min(1).max(200),
|
|
178
216
|
acceptance: z.string().max(2000).default(""),
|
|
179
217
|
invite: z.array(z.string().min(1).max(48)).max(16).optional(),
|
|
180
218
|
budget: z.number().int().min(4).max(1000).optional(),
|
|
181
219
|
deadline: z.number().int().positive().optional(),
|
|
182
220
|
kind: z.enum(["worklist"]).optional(),
|
|
221
|
+
sig: GoalSigSchema.optional(),
|
|
183
222
|
});
|
|
184
223
|
export const Ping = z.object({ t: z.literal("ping") });
|
|
185
224
|
export const DeleteSpace = z.object({
|
|
@@ -223,6 +262,6 @@ export function isContinuationFor(act, me, ctx) {
|
|
|
223
262
|
&& ctx.goalMembers.includes(me);
|
|
224
263
|
}
|
|
225
264
|
export function newId(prefix) {
|
|
226
|
-
return `${prefix}_${Date.now().toString(36)}${
|
|
265
|
+
return `${prefix}_${Date.now().toString(36)}${randomBytes(5).readUIntBE(0, 5).toString(36).padStart(8, "0").slice(0, 8)}`;
|
|
227
266
|
}
|
|
228
267
|
//# sourceMappingURL=protocol.js.map
|
package/dist/quickstart.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { createServer } from "node:net";
|
|
4
|
-
import { basename, join as pathJoin } from "node:path";
|
|
5
|
-
import { homedir, hostname, platform } from "node:os";
|
|
4
|
+
import { basename, dirname, join as pathJoin, resolve as resolvePath } from "node:path";
|
|
5
|
+
import { homedir, hostname, platform, userInfo } from "node:os";
|
|
6
6
|
import { lookup } from "node:dns/promises";
|
|
7
7
|
import { describeNetError } from "./network.js";
|
|
8
8
|
import { PantheonClient } from "./client.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { holdsRepo } from "./repo-find.js";
|
|
10
|
+
import { load, loadAll, identity, save, toInvite, resolveFor, repoInfo, bindRepo, bindFolder, folderBinding, DEFAULT_RELAY } from "./config.js";
|
|
11
|
+
import { commandsChosen, loadProfile, wakeChosen, wakeDefault } from "./profile.js";
|
|
11
12
|
import { newKeypair } from "./identity.js";
|
|
12
13
|
import { installForUser } from "./install.js";
|
|
13
14
|
import { writeIntent, installHooks } from "./notify.js";
|
|
14
|
-
import { detectAvailable, agentById } from "./agents.js";
|
|
15
|
+
import { detectAvailable, agentById, agentsNeedingUnsandboxed } from "./agents.js";
|
|
15
16
|
import { installService, servicesUnavailable } from "./service.js";
|
|
16
17
|
import { ensureNativeNotifier } from "./notifier.js";
|
|
17
18
|
import { ensureTrayBinary, writeTrayCommands } from "./tray.js";
|
|
18
19
|
import { desktopUnavailable, ephemeralWarning, hostEnv } from "./host-env.js";
|
|
19
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
20
21
|
import { localAddresses } from "./machine.js";
|
|
21
|
-
import { grant as grantCap, loadPolicy, policyPath, savePolicy } from "./worker-policy.js";
|
|
22
|
+
import { grant as grantCap, loadPolicy, policyPath, savePolicy, unsandboxedAllowed } from "./worker-policy.js";
|
|
22
23
|
import { Relay } from "./relay.js";
|
|
23
24
|
import { which } from "./which.js";
|
|
24
25
|
const sh = (cmd) => {
|
|
@@ -29,6 +30,9 @@ const sh = (cmd) => {
|
|
|
29
30
|
return null;
|
|
30
31
|
}
|
|
31
32
|
};
|
|
33
|
+
const notLoaded = (r) => r && typeof r === "object" && r.loaded === false
|
|
34
|
+
? String(r.reason ?? "the service manager did not load it")
|
|
35
|
+
: null;
|
|
32
36
|
export function wireUp(opts, deps = {}) {
|
|
33
37
|
const installSvc = deps.installService ?? installService;
|
|
34
38
|
const ensureNotifier = deps.ensureNotifier ?? ensureNativeNotifier;
|
|
@@ -63,9 +67,14 @@ export function wireUp(opts, deps = {}) {
|
|
|
63
67
|
else if (opts.notifications) {
|
|
64
68
|
try {
|
|
65
69
|
ensureNotifier();
|
|
66
|
-
installSvc("notifier", homedir(), []);
|
|
70
|
+
const why = notLoaded(installSvc("notifier", homedir(), []));
|
|
67
71
|
writeTrayCommands();
|
|
68
|
-
|
|
72
|
+
if (why) {
|
|
73
|
+
tools.push({ tool: "notifications", status: `installed, not running: ${why}` });
|
|
74
|
+
warnings.push(`human notifications are installed but not running (${why}). Try \`pantheon service restart notifier\``);
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
tools.push({ tool: "notifications", status: "on — native alerts for decisions that require you" });
|
|
69
78
|
}
|
|
70
79
|
catch (e) {
|
|
71
80
|
tools.push({ tool: "notifications", status: `could not install: ${e.message}` });
|
|
@@ -82,8 +91,13 @@ export function wireUp(opts, deps = {}) {
|
|
|
82
91
|
try {
|
|
83
92
|
ensureTray();
|
|
84
93
|
writeTrayCommands();
|
|
85
|
-
installSvc("menubar", homedir(), []);
|
|
86
|
-
|
|
94
|
+
const why = notLoaded(installSvc("menubar", homedir(), []));
|
|
95
|
+
if (why) {
|
|
96
|
+
tools.push({ tool: "menu bar", status: `installed, not running: ${why}` });
|
|
97
|
+
warnings.push(`the menu bar item is installed but not running (${why}). Try \`pantheon service restart menubar\``);
|
|
98
|
+
}
|
|
99
|
+
else
|
|
100
|
+
tools.push({ tool: "menu bar", status: "on — status, pause/resume, and decision badge in your menu bar" });
|
|
87
101
|
}
|
|
88
102
|
catch (e) {
|
|
89
103
|
tools.push({ tool: "menu bar", status: `could not install: ${e.message}` });
|
|
@@ -108,18 +122,68 @@ export function wireUp(opts, deps = {}) {
|
|
|
108
122
|
catch {
|
|
109
123
|
return false;
|
|
110
124
|
} })();
|
|
111
|
-
|
|
125
|
+
const boundFolder = !isRepo && (() => { try {
|
|
126
|
+
const f = loadAll();
|
|
127
|
+
return !!f && !!folderBinding(f, project);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return false;
|
|
131
|
+
} })();
|
|
132
|
+
if (boundFolder) {
|
|
133
|
+
try {
|
|
134
|
+
const why = notLoaded(installSvc("worker", project, []));
|
|
135
|
+
if (why) {
|
|
136
|
+
tools.push({ tool: "worker", status: `installed, not running: ${why}` });
|
|
137
|
+
warnings.push(`the worker is installed but not running (${why}) — this machine cannot answer the team while you are away. Try \`pantheon service restart worker\``);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
tools.push({ tool: "worker", status: `on — answers peers from this folder while you are away (it reads it; anything it writes goes to a drafts folder for you)` });
|
|
141
|
+
granted.push(`${wakeChosen() === "approved" ? "A teammate you approve from this machine (pantheon wake allow <name>)" : "Your teammates"} can start an agent run that READS ${project} and answers them. It never changes that folder: drafts go to a folder of its own for you to review.`);
|
|
142
|
+
const hasClaude = (() => { try {
|
|
143
|
+
return detectAvailable().some((a) => a.backend.id === "claude-code");
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return true;
|
|
147
|
+
} })();
|
|
148
|
+
if (!hasClaude)
|
|
149
|
+
warnings.push(`answering from a plain folder while you are away needs Claude Code (claude), and it is not installed here — until it is, teammates' requests for this folder wait for your live session`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
tools.push({ tool: "worker", status: `could not install: ${e.message}` });
|
|
154
|
+
warnings.push(`the worker did not install: ${e.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (!isRepo && how === "explicit") {
|
|
112
158
|
tools.push({ tool: "worker", status: `not installed — ${project} is not a git repo` });
|
|
113
159
|
warnings.push(`--unattended needs a git repo to work in; pass --project <a clone>. Nothing was installed.`);
|
|
114
160
|
}
|
|
115
161
|
else if (!isRepo) {
|
|
116
|
-
tools.push({ tool: "worker", status: `not on —
|
|
162
|
+
tools.push({ tool: "worker", status: `not on yet — nothing here is bound to the team. Bind a repo or docs folder (pantheon repo find, or pantheon use <team> --here), then: pantheon service install worker` });
|
|
117
163
|
}
|
|
118
164
|
else {
|
|
119
165
|
try {
|
|
120
|
-
|
|
166
|
+
const commandsOn = commandsChosen() !== false;
|
|
167
|
+
const why = notLoaded(installSvc("worker", project, commandsOn ? ["--allow-bash"] : []));
|
|
168
|
+
if (why) {
|
|
169
|
+
tools.push({ tool: "worker", status: `installed, not running: ${why}` });
|
|
170
|
+
warnings.push(`the worker is installed but not running (${why}) — this machine cannot answer the team while you are away. Try \`pantheon service restart worker\``);
|
|
171
|
+
return { tools, granted, warnings };
|
|
172
|
+
}
|
|
121
173
|
tools.push({ tool: "worker", status: `on — answers peers while you are away (${project})` });
|
|
122
|
-
granted.push(
|
|
174
|
+
granted.push(wakeChosen() === "approved"
|
|
175
|
+
? `A teammate you approve here can start an agent run on this machine, in an isolated worktree under ${project}: you are asked the first time each one's request needs it (Always / Just this once / No), or approve ahead with pantheon wake allow <name>. Under wake "anyone", every teammate the team lets wake can.`
|
|
176
|
+
: `Your teammates can hand work to this machine while you're away, so nothing waits on you: it runs in its own copy of ${project} and delivers on a branch for your review. To approve each teammate first: pantheon setup --wake approved`);
|
|
177
|
+
if (commandsOn)
|
|
178
|
+
granted.push(`That work runs your tests, commits, and pushes to its own branch, so it arrives ready to review. Edits only instead: pantheon setup --commands no`);
|
|
179
|
+
const away = (() => { try {
|
|
180
|
+
return agentsNeedingUnsandboxed();
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return [];
|
|
184
|
+
} })();
|
|
185
|
+
if (away.length && unsandboxedAllowed(loadPolicy()))
|
|
186
|
+
granted.push(`${away.join(", ")} ${away.length === 1 ? "answers" : "answer"} those requests, so teammates get answers even when you're offline. To have them wait for you instead: pantheon setup --unsandboxed no`);
|
|
123
187
|
if (!existsSync(policyPath())) {
|
|
124
188
|
const pol = loadPolicy();
|
|
125
189
|
grantCap(pol, "self_upgrade", "always", "setup-default");
|
|
@@ -151,6 +215,47 @@ export function slugName(raw, fallback) {
|
|
|
151
215
|
.replace(/^[-._]+|[-._]+$/g, "");
|
|
152
216
|
return /[a-z0-9]/.test(s) ? s : fallback;
|
|
153
217
|
}
|
|
218
|
+
export function osUsername(env = process.env, info = () => userInfo().username) {
|
|
219
|
+
const tries = [info, () => env.USERNAME, () => env.USER];
|
|
220
|
+
for (const t of tries) {
|
|
221
|
+
try {
|
|
222
|
+
const v = t()?.trim();
|
|
223
|
+
if (v && v.toLowerCase() !== "root")
|
|
224
|
+
return v;
|
|
225
|
+
}
|
|
226
|
+
catch { }
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
export function defaultMemberName(env = process.env, info, host = hostname) {
|
|
231
|
+
const user = osUsername(env, info);
|
|
232
|
+
const random = `member-${randomBytes(3).toString("hex")}`;
|
|
233
|
+
if (user) {
|
|
234
|
+
const s = slugName(user, "");
|
|
235
|
+
if (s)
|
|
236
|
+
return s;
|
|
237
|
+
}
|
|
238
|
+
let h = "";
|
|
239
|
+
try {
|
|
240
|
+
h = host().split(".")[0] ?? "";
|
|
241
|
+
}
|
|
242
|
+
catch { }
|
|
243
|
+
const s = slugName(h, "");
|
|
244
|
+
return s && s !== "localhost" ? `${s}` : random;
|
|
245
|
+
}
|
|
246
|
+
export function deviceLabel(plat = platform(), env = process.env, info, host = hostname) {
|
|
247
|
+
const os = plat === "darwin" ? "Mac" : plat === "win32" ? "Windows" : plat === "linux" ? "Linux" : plat;
|
|
248
|
+
let who = osUsername(env, info);
|
|
249
|
+
if (!who) {
|
|
250
|
+
try {
|
|
251
|
+
who = host().split(".")[0] || null;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
who = null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return `${who ?? "a"}'s ${os}`;
|
|
258
|
+
}
|
|
154
259
|
export function teamNameFor(dir, repo = repoInfo(dir)) {
|
|
155
260
|
const fromOrigin = repo?.origin ? (repo.origin.replace(/\.git$/i, "").split(/[/:]/).filter(Boolean).pop() ?? "") : "";
|
|
156
261
|
return slugName(fromOrigin || basename(repo?.root ?? dir), "team");
|
|
@@ -298,11 +403,10 @@ export async function start(opts) {
|
|
|
298
403
|
const reach = chosen
|
|
299
404
|
? (place === "loopback"
|
|
300
405
|
? { url: chosen, how: "as given", shareable: false,
|
|
301
|
-
hint: `
|
|
302
|
-
` Use a tailnet address (tailscale ip -4) or a tunnel, then re-run pantheon start.` }
|
|
406
|
+
hint: `A teammate cannot reach it. Use a tailnet address (tailscale ip -4) or a tunnel, then re-run pantheon start.` }
|
|
303
407
|
: { url: chosen, how: place === "this-machine" ? "this machine's address" : place === "tunnel" ? "a tunnel to this machine" : opts.relay ? "as given" : "your signed-in relay", shareable: true })
|
|
304
408
|
: findReach(opts.port);
|
|
305
|
-
const nothing = (why) => ({ cfg: priorCfg ?? { relay: reach.url }, invite: null, claimed: false, reach, tools: [], granted: [], relay: null, warnings: [why] });
|
|
409
|
+
const nothing = (why) => ({ cfg: priorCfg ?? { relay: reach.url }, invite: null, inviteMayWake: false, claimed: false, reach, tools: [], granted: [], relay: null, warnings: [why] });
|
|
306
410
|
let relay = null;
|
|
307
411
|
if (chosen && !hostHere) {
|
|
308
412
|
const probe = await probeRelay(reach.url, opts.remoteTimeoutMs ?? 10_000, opts.fetchImpl);
|
|
@@ -345,7 +449,7 @@ export async function start(opts) {
|
|
|
345
449
|
relay: reach.url,
|
|
346
450
|
space: opts.space ?? teamNameFor(process.cwd(), repo),
|
|
347
451
|
token: randomBytes(16).toString("hex"),
|
|
348
|
-
member: opts.me ?? (keepIdentity ? existing.member :
|
|
452
|
+
member: opts.me ?? (keepIdentity ? existing.member : defaultMemberName()),
|
|
349
453
|
agent: detectAgent(),
|
|
350
454
|
...(keepIdentity ? { publicKey: existing.publicKey, privateKey: existing.privateKey } : newKeypair()),
|
|
351
455
|
...(keepIdentity && existing.account ? { account: existing.account } : {}),
|
|
@@ -359,6 +463,10 @@ export async function start(opts) {
|
|
|
359
463
|
warnings.push(`you are already "${cfg.member}" here; --me ${opts.me} ignored (use --reset to start over)`);
|
|
360
464
|
}
|
|
361
465
|
}
|
|
466
|
+
else if (keepIdentity && existing.space) {
|
|
467
|
+
opts.log(` you ${cfg.member}, as in "${existing.space}" — that team stays; this adds one`);
|
|
468
|
+
opts.log(` default "${cfg.space}" from now on for folders bound to no team — pantheon use ${existing.space} switches back`);
|
|
469
|
+
}
|
|
362
470
|
else if (keepIdentity && existing.account) {
|
|
363
471
|
opts.log(` you ${cfg.member} — signed in; creating your first team`);
|
|
364
472
|
}
|
|
@@ -366,6 +474,8 @@ export async function start(opts) {
|
|
|
366
474
|
warnings.push(`replacing your identity in "${existing.space}" — that space is no longer reachable from this machine`);
|
|
367
475
|
}
|
|
368
476
|
let invite = null;
|
|
477
|
+
let inviteMayWake = false;
|
|
478
|
+
let folderBound = null;
|
|
369
479
|
let claimed = false;
|
|
370
480
|
const claim = async (c) => {
|
|
371
481
|
const client = new PantheonClient(c);
|
|
@@ -377,15 +487,23 @@ export async function start(opts) {
|
|
|
377
487
|
if (repo && !client.repos.some((r) => r.fingerprint === repo.fingerprint)) {
|
|
378
488
|
await client.bindRepo(repo.fingerprint, repo.origin).catch((e) => warnings.push(`could not register this repo with the team: ${e.message}`));
|
|
379
489
|
}
|
|
490
|
+
if (!repo && c.spaceId && bindableFolder(process.cwd())) {
|
|
491
|
+
bindFolder(process.cwd(), c.spaceId);
|
|
492
|
+
folderBound = process.cwd();
|
|
493
|
+
}
|
|
380
494
|
if (repo && c.spaceId) {
|
|
381
495
|
const { replaced } = bindRepo(repo, c.spaceId);
|
|
382
|
-
|
|
383
|
-
|
|
496
|
+
const was = replaced ?? (here.binding && here.binding.space !== c.spaceId ? here.binding : null);
|
|
497
|
+
if (was) {
|
|
498
|
+
const name = Object.values(loadAll()?.spaces ?? {}).find((m) => m.id === was.space)?.name ?? was.space;
|
|
499
|
+
warnings.push(`this repo was bound to "${name}"; it now belongs to "${c.space}" — agents opened here speak in "${c.space}" from now on (pantheon use ${name} --here moves it back)`);
|
|
500
|
+
}
|
|
384
501
|
}
|
|
385
|
-
opts.log(` space ${c.space} ${reuse ? "— reconnected" : "— created, you own it"}${repo ? ` ${repo.origin ?? "local repo"} → bound to it` : ""}`);
|
|
502
|
+
opts.log(` space ${c.space} ${reuse ? "— reconnected" : "— created, you own it"}${repo ? ` ${repo.origin ?? "local repo"} → bound to it` : folderBound ? ` this folder → bound to it` : ""}`);
|
|
386
503
|
const mayWake = opts.wake ? opts.wake === "anyone" : wakeDefault(loadProfile(c));
|
|
387
504
|
const inv = await client.makeInvite(mayWake, opts.ttlMinutes);
|
|
388
505
|
invite = toInvite({ ...c, token: inv.url });
|
|
506
|
+
inviteMayWake = mayWake;
|
|
389
507
|
claimed = true;
|
|
390
508
|
}
|
|
391
509
|
finally {
|
|
@@ -426,7 +544,7 @@ export async function start(opts) {
|
|
|
426
544
|
...(opts.workerHow ? { workerHow: opts.workerHow } : {}),
|
|
427
545
|
});
|
|
428
546
|
warnings.push(...wired.warnings);
|
|
429
|
-
return { cfg, invite, claimed, reach, tools: wired.tools, granted: wired.granted, relay, warnings };
|
|
547
|
+
return { cfg, invite, inviteMayWake, claimed, reach, tools: wired.tools, granted: wired.granted, relay, warnings };
|
|
430
548
|
}
|
|
431
549
|
export async function join(opts) {
|
|
432
550
|
const existing = opts.reset ? null : (load() ?? identity());
|
|
@@ -441,7 +559,7 @@ export async function join(opts) {
|
|
|
441
559
|
relay: opts.invite.relay,
|
|
442
560
|
space: opts.invite.space,
|
|
443
561
|
token: "",
|
|
444
|
-
member: opts.me ?? existing?.member ??
|
|
562
|
+
member: opts.me ?? existing?.member ?? defaultMemberName(),
|
|
445
563
|
agent: detectAgent(),
|
|
446
564
|
...keys,
|
|
447
565
|
...(existing?.account ? { account: existing.account } : {}),
|
|
@@ -456,6 +574,10 @@ export async function join(opts) {
|
|
|
456
574
|
cfg.spaceId = client.spaceId;
|
|
457
575
|
save(cfg);
|
|
458
576
|
const warnings = [];
|
|
577
|
+
if (!repo && cfg.spaceId && bindableFolder(process.cwd())) {
|
|
578
|
+
bindFolder(process.cwd(), cfg.spaceId);
|
|
579
|
+
opts.log(` folder ${process.cwd()} → bound to "${cfg.space}" (agents opened here speak in it; no git needed)`);
|
|
580
|
+
}
|
|
459
581
|
if (repo && cfg.spaceId) {
|
|
460
582
|
const { replaced } = bindRepo(repo, cfg.spaceId);
|
|
461
583
|
if (replaced)
|
|
@@ -476,6 +598,33 @@ export async function join(opts) {
|
|
|
476
598
|
});
|
|
477
599
|
return { cfg, tools: wired.tools, granted: wired.granted, warnings: [...warnings, ...wired.warnings] };
|
|
478
600
|
}
|
|
601
|
+
export function bindableFolder(dir) {
|
|
602
|
+
const real = (d) => { try {
|
|
603
|
+
return realpathSync.native(d);
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
return resolvePath(d);
|
|
607
|
+
} };
|
|
608
|
+
const here = real(dir), home = real(homedir());
|
|
609
|
+
if (dirname(here) === here || here === home || home.startsWith(here.endsWith("/") ? here : `${here}/`))
|
|
610
|
+
return false;
|
|
611
|
+
if (["Downloads", "Desktop", "Documents", "Library", "tmp"].some((n) => here === pathJoin(home, n)))
|
|
612
|
+
return false;
|
|
613
|
+
try {
|
|
614
|
+
if (holdsRepo(here))
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
const f = loadAll();
|
|
622
|
+
if (Object.values(f?.bindings ?? {}).some((b) => { const p = real(b.path); return p !== here && p.startsWith(`${here}/`); }))
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
catch { }
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
479
628
|
function detectAgent() {
|
|
480
629
|
const found = detectAvailable();
|
|
481
630
|
return found[0]?.backend.id ?? "unknown";
|
package/dist/reads.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { signRead } from "./identity.js";
|
|
2
|
+
let clockSkewMs = 0;
|
|
3
|
+
export const relayClockSkew = () => clockSkewMs;
|
|
4
|
+
export function learnRelayClock(res) {
|
|
5
|
+
const there = Date.parse(res.headers.get("date") ?? "");
|
|
6
|
+
if (!Number.isFinite(there) || Math.abs(there - Date.now() - clockSkewMs) <= 60_000)
|
|
7
|
+
return false;
|
|
8
|
+
clockSkewMs = there - Date.now();
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
export const httpBase = (relay) => relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
12
|
+
export function signedReadQuery(cfg, path, params, at) {
|
|
13
|
+
const full = { space: cfg.space, token: cfg.token };
|
|
14
|
+
for (const [k, v] of Object.entries(params))
|
|
15
|
+
if (v !== undefined)
|
|
16
|
+
full[k] = String(v);
|
|
17
|
+
full.reader = cfg.member;
|
|
18
|
+
full.ts = String(at);
|
|
19
|
+
const d = cfg.delegate;
|
|
20
|
+
if (d) {
|
|
21
|
+
full.dpub = d.cert.pub;
|
|
22
|
+
full.drun = d.cert.run;
|
|
23
|
+
full.dexp = String(d.cert.expires);
|
|
24
|
+
full.dcert = d.cert.sig;
|
|
25
|
+
}
|
|
26
|
+
const sig = signRead(d ? d.privateKey : cfg.privateKey, "GET", path, full);
|
|
27
|
+
return new URLSearchParams({ ...full, sig }).toString();
|
|
28
|
+
}
|
|
29
|
+
export async function signedRead(cfg, path, params = {}, opts = {}) {
|
|
30
|
+
const signal = opts.signal ?? (opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined);
|
|
31
|
+
const go = () => fetch(`${httpBase(cfg.relay)}${path}?${signedReadQuery(cfg, path, params, Date.now() + clockSkewMs)}`, signal ? { signal } : {});
|
|
32
|
+
const res = await go();
|
|
33
|
+
if (res.status === 403 && learnRelayClock(res))
|
|
34
|
+
return go();
|
|
35
|
+
return res;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=reads.js.map
|
package/dist/refusal.js
CHANGED
|
@@ -21,9 +21,22 @@ export function connectFailure(e, relay) {
|
|
|
21
21
|
const r = refusalOf(e);
|
|
22
22
|
if (r) {
|
|
23
23
|
const fix = r.fix && !/pantheon \w/.test(r.message) ? `\n ${r.fix}` : "";
|
|
24
|
-
return `the relay refused this machine (${r.code}): ${r.message}${fix}`;
|
|
24
|
+
return `the relay refused this machine (${r.code}): ${r.message}${fix}${signInHint(r.code, relay)}`;
|
|
25
25
|
}
|
|
26
26
|
const text = typeof e === "string" ? e : String(e?.message ?? e ?? "");
|
|
27
27
|
return `cannot reach the relay at ${relay} — ${text}`;
|
|
28
28
|
}
|
|
29
|
+
export function signInHint(code, relay, hosted = process.env.PANTHEON_DEFAULT_RELAY ?? "wss://relay.joinpantheon.network") {
|
|
30
|
+
if (code !== "NOT_SIGNED_IN" || !relay)
|
|
31
|
+
return "";
|
|
32
|
+
try {
|
|
33
|
+
if (new URL(relay).host === new URL(hosted).host)
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
return `\n This team is on ${relay}, so sign in there: pantheon login --email you@work.com --relay ${relay}` +
|
|
40
|
+
`\n (new here: pantheon signup --email you@work.com --username you --relay ${relay})`;
|
|
41
|
+
}
|
|
29
42
|
//# sourceMappingURL=refusal.js.map
|