@join-pantheon/cli 0.25.3 → 0.26.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 +43 -5
- package/bin/pantheon.js +7 -2
- package/dist/agents.js +159 -18
- package/dist/cli.js +39 -27
- package/dist/config.js +26 -1
- package/dist/doctor.js +23 -3
- package/dist/hooks-install.js +60 -2
- package/dist/install.js +94 -30
- package/dist/machine.js +11 -0
- package/dist/measure.js +17 -3
- package/dist/notice.js +4 -1
- package/dist/notifier.js +1 -0
- package/dist/notify.js +21 -5
- package/dist/persona.js +1 -1
- package/dist/profile.js +4 -1
- package/dist/quickstart.js +2 -1
- package/dist/run-failure.js +15 -0
- package/dist/service.js +3 -0
- package/dist/setup-guide.js +1 -1
- package/dist/tray.js +1 -1
- package/dist/update-check.js +2 -0
- package/dist/which.js +73 -0
- package/dist/worker.js +17 -7
- package/package.json +6 -4
package/dist/hooks-install.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { MAX_CONSECUTIVE_BLOCKS, isOurHookCommand } from "./notify.js";
|
|
5
|
+
import { copilotHome } from "./agents.js";
|
|
5
6
|
function ours(cmd) {
|
|
6
|
-
return
|
|
7
|
+
return isOurHookCommand(cmd);
|
|
7
8
|
}
|
|
8
9
|
function readJson(file) {
|
|
9
10
|
if (!existsSync(file))
|
|
@@ -189,4 +190,61 @@ export function removeCursorHooks(projectDir = process.cwd()) {
|
|
|
189
190
|
}
|
|
190
191
|
return out;
|
|
191
192
|
}
|
|
193
|
+
export function copilotHooksPath(scope, projectDir = process.cwd()) {
|
|
194
|
+
return scope === "user"
|
|
195
|
+
? join(copilotHome(), "hooks", "pantheon.json")
|
|
196
|
+
: join(projectDir, ".github", "hooks", "pantheon.json");
|
|
197
|
+
}
|
|
198
|
+
function copilotEvents(bin) {
|
|
199
|
+
return {
|
|
200
|
+
agentStop: `${bin} hook copilot-stop`,
|
|
201
|
+
userPromptSubmitted: `${bin} hook prompt`,
|
|
202
|
+
sessionEnd: `${bin} hook end`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export function copilotHookState(scope, projectDir = process.cwd()) {
|
|
206
|
+
const file = copilotHooksPath(scope, projectDir);
|
|
207
|
+
const doc = readJson(file);
|
|
208
|
+
if (!doc?.hooks)
|
|
209
|
+
return { agent: "copilot", label: "Copilot", installed: false };
|
|
210
|
+
const events = [];
|
|
211
|
+
for (const [event, entries] of Object.entries(doc.hooks)) {
|
|
212
|
+
if ((entries ?? []).some((h) => ours(h.command) || ours(h.bash)))
|
|
213
|
+
events.push(event);
|
|
214
|
+
}
|
|
215
|
+
return events.length
|
|
216
|
+
? { agent: "copilot", label: "Copilot", installed: true, scope, file, events }
|
|
217
|
+
: { agent: "copilot", label: "Copilot", installed: false };
|
|
218
|
+
}
|
|
219
|
+
export function installCopilotHooks(bin, scope, projectDir = process.cwd()) {
|
|
220
|
+
const file = copilotHooksPath(scope, projectDir);
|
|
221
|
+
const existed = existsSync(file);
|
|
222
|
+
const events = copilotEvents(bin);
|
|
223
|
+
const doc = { version: 1, hooks: {} };
|
|
224
|
+
for (const [event, cmd] of Object.entries(events)) {
|
|
225
|
+
doc.hooks[event] = [{ type: "command", command: cmd, timeoutSec: 10, timeout: 10 }];
|
|
226
|
+
}
|
|
227
|
+
writeJson(file, doc);
|
|
228
|
+
return [
|
|
229
|
+
` ${existed ? "updated" : "installed"} ${file}`,
|
|
230
|
+
` runs ${events.agentStop}`,
|
|
231
|
+
` ${events.userPromptSubmitted}`,
|
|
232
|
+
` ${events.sessionEnd}`,
|
|
233
|
+
` ${""}(read by Copilot CLI and by Copilot in VS Code${scope === "project" ? "; repo-level hooks run once the folder is trusted" : ""})`,
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
export function removeCopilotHooks(projectDir = process.cwd()) {
|
|
237
|
+
const out = [];
|
|
238
|
+
for (const scope of ["project", "user"]) {
|
|
239
|
+
const file = copilotHooksPath(scope, projectDir);
|
|
240
|
+
if (!copilotHookState(scope, projectDir).installed)
|
|
241
|
+
continue;
|
|
242
|
+
try {
|
|
243
|
+
rmSync(file, { force: true });
|
|
244
|
+
out.push(` removed ${file}`);
|
|
245
|
+
}
|
|
246
|
+
catch { }
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
192
250
|
//# sourceMappingURL=hooks-install.js.map
|
package/dist/install.js
CHANGED
|
@@ -1,48 +1,71 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { homedir } from "node:os";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
4
4
|
import { join, dirname, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { grantToolPermissions, claudeSettingsFile, cursorCliConfigFile } from "./grants.js";
|
|
7
7
|
import { PANTHEON_TOOL_NAMES, WRITE_TOOL_NAMES } from "./tools.js";
|
|
8
8
|
import { load } from "./config.js";
|
|
9
9
|
import { loadProfile } from "./profile.js";
|
|
10
|
+
import { agentSpawn, onPathNamed, which } from "./which.js";
|
|
10
11
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
export function onPath() {
|
|
12
|
-
|
|
13
|
-
execFileSync("sh", ["-lc", "command -v pantheon"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
13
|
+
return onPathNamed("pantheon");
|
|
19
14
|
}
|
|
20
|
-
export function launcher(force) {
|
|
15
|
+
export function launcher(force, os = platform()) {
|
|
21
16
|
const portable = force === "portable" || (force !== "local" && onPath());
|
|
17
|
+
if (os === "win32")
|
|
18
|
+
return { command: process.execPath, args: [resolve(HERE, "cli.js"), "mcp"], portable: false };
|
|
22
19
|
return portable
|
|
23
20
|
? { command: "pantheon", args: ["mcp"], portable: true }
|
|
24
21
|
: { command: process.execPath, args: [resolve(HERE, "cli.js"), "mcp"], portable: false };
|
|
25
22
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
function readConfigJson(file) {
|
|
24
|
+
if (!existsSync(file))
|
|
25
|
+
return {};
|
|
26
|
+
const text = readFileSync(file, "utf8");
|
|
27
|
+
if (!text.trim())
|
|
28
|
+
return {};
|
|
29
|
+
try {
|
|
30
|
+
const doc = JSON.parse(text);
|
|
31
|
+
if (doc && typeof doc === "object" && !Array.isArray(doc))
|
|
32
|
+
return doc;
|
|
35
33
|
}
|
|
34
|
+
catch { }
|
|
35
|
+
throw new Error(`${file} is not valid JSON — fix or move it, then re-run`);
|
|
36
|
+
}
|
|
37
|
+
function mergeMcpJson(file, entry, shape = {}) {
|
|
38
|
+
const doc = readConfigJson(file);
|
|
36
39
|
const existed = existsSync(file);
|
|
37
40
|
doc.mcpServers ??= {};
|
|
38
41
|
const before = JSON.stringify(doc.mcpServers["pantheon"] ?? null);
|
|
39
|
-
doc.mcpServers["pantheon"] = { ...entry, env: {} };
|
|
42
|
+
doc.mcpServers["pantheon"] = { ...shape, ...entry, env: {} };
|
|
40
43
|
if (before === JSON.stringify(doc.mcpServers["pantheon"]))
|
|
41
44
|
return "unchanged";
|
|
42
45
|
mkdirSync(dirname(file), { recursive: true });
|
|
43
46
|
writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
|
|
44
47
|
return existed ? "updated" : "created";
|
|
45
48
|
}
|
|
49
|
+
export const COPILOT_ENTRY_SHAPE = { type: "local", tools: ["*"] };
|
|
50
|
+
export function mergeVsCodeMcpJson(file, entry) {
|
|
51
|
+
const doc = readConfigJson(file);
|
|
52
|
+
const existed = existsSync(file);
|
|
53
|
+
doc.servers ??= {};
|
|
54
|
+
const before = JSON.stringify(doc.servers["pantheon"] ?? null);
|
|
55
|
+
doc.servers["pantheon"] = { type: "stdio", command: entry.command, args: entry.args };
|
|
56
|
+
if (before === JSON.stringify(doc.servers["pantheon"]))
|
|
57
|
+
return "unchanged";
|
|
58
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
59
|
+
writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
|
|
60
|
+
return existed ? "updated" : "created";
|
|
61
|
+
}
|
|
62
|
+
export const copilotMcpConfigFile = (home = homedir(), env = process.env) => join(env.COPILOT_HOME || join(home, ".copilot"), "mcp-config.json");
|
|
63
|
+
export function vsCodeUserDir(home = homedir(), os = platform(), env = process.env) {
|
|
64
|
+
const dir = os === "darwin" ? join(home, "Library", "Application Support", "Code", "User")
|
|
65
|
+
: os === "win32" ? join(env.APPDATA || join(home, "AppData", "Roaming"), "Code", "User")
|
|
66
|
+
: join(env.XDG_CONFIG_HOME || join(home, ".config"), "Code", "User");
|
|
67
|
+
return existsSync(dir) ? dir : null;
|
|
68
|
+
}
|
|
46
69
|
const BRIEF_MARK = "<!-- pantheon:begin -->";
|
|
47
70
|
function brief(cfg) {
|
|
48
71
|
return `${BRIEF_MARK}
|
|
@@ -95,14 +118,19 @@ export function install(projectDir, cfg, force) {
|
|
|
95
118
|
const out = [];
|
|
96
119
|
const l = launcher(force);
|
|
97
120
|
const entry = { command: l.command, args: l.args };
|
|
98
|
-
out.push(` .mcp.json ${mergeMcpJson(join(projectDir, ".mcp.json"), entry)} (claude code, conductor)`);
|
|
121
|
+
out.push(` .mcp.json ${mergeMcpJson(join(projectDir, ".mcp.json"), entry)} (claude code, conductor, copilot cli)`);
|
|
99
122
|
out.push(` .cursor/mcp.json ${mergeMcpJson(join(projectDir, ".cursor", "mcp.json"), entry)} (cursor)`);
|
|
123
|
+
const vscodeDir = join(projectDir, ".vscode");
|
|
124
|
+
out.push(existsSync(vscodeDir) || vsCodeUserDir()
|
|
125
|
+
? ` .vscode/mcp.json ${mergeVsCodeMcpJson(join(vscodeDir, "mcp.json"), entry)} (copilot in vs code)`
|
|
126
|
+
: ` .vscode/mcp.json ${"skipped".padEnd(9)} no .vscode/ here and no VS Code on this machine (a VS Code user runs pantheon install --user)`);
|
|
100
127
|
for (const f of ["CLAUDE.md", "AGENTS.md"]) {
|
|
101
128
|
const p = join(projectDir, f);
|
|
102
129
|
if (existsSync(p) || f === "AGENTS.md")
|
|
103
130
|
out.push(` ${f.padEnd(25)} ${upsertBrief(p, cfg)} (the collaboration brief)`);
|
|
104
131
|
}
|
|
105
132
|
out.push("");
|
|
133
|
+
out.push(" Copilot asks once to trust this folder before it reads the repo's MCP servers and hooks.");
|
|
106
134
|
out.push(" Codex reads ~/.codex/config.toml, which `pantheon join` already wrote at user scope.");
|
|
107
135
|
out.push(" If you need it by hand, this is the block (absolute path so a GUI launch finds it):");
|
|
108
136
|
out.push("");
|
|
@@ -128,9 +156,17 @@ export function install(projectDir, cfg, force) {
|
|
|
128
156
|
function localEntry() {
|
|
129
157
|
return { command: process.execPath, args: [resolve(HERE, "cli.js"), "mcp"] };
|
|
130
158
|
}
|
|
131
|
-
function userEntry() {
|
|
159
|
+
function userEntry(os = platform()) {
|
|
160
|
+
if (os === "win32") {
|
|
161
|
+
const node = process.execPath;
|
|
162
|
+
const entry = resolve(HERE, "cli.js");
|
|
163
|
+
if (existsSync(entry))
|
|
164
|
+
return { command: node, args: [entry, "mcp"] };
|
|
165
|
+
const abs = which("pantheon");
|
|
166
|
+
return abs ? { command: abs, args: ["mcp"] } : { command: "pantheon", args: ["mcp"] };
|
|
167
|
+
}
|
|
132
168
|
try {
|
|
133
|
-
const abs =
|
|
169
|
+
const abs = which("pantheon") ?? "";
|
|
134
170
|
if (abs && !isVersionManagedPath(abs))
|
|
135
171
|
return { command: abs, args: ["mcp"] };
|
|
136
172
|
if (abs)
|
|
@@ -145,18 +181,17 @@ function isVersionManagedPath(p) {
|
|
|
145
181
|
}
|
|
146
182
|
function claudeUserScope(entry) {
|
|
147
183
|
const tool = "claude code";
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
184
|
+
const claudeBin = which("claude");
|
|
185
|
+
if (!claudeBin)
|
|
152
186
|
return { tool, where: "-", status: "skipped (claude not on PATH)" };
|
|
153
|
-
}
|
|
154
187
|
try {
|
|
188
|
+
const remove = agentSpawn(claudeBin, ["mcp", "remove", "pantheon", "-s", "user"]);
|
|
189
|
+
const add = agentSpawn(claudeBin, ["mcp", "add", "pantheon", "-s", "user", "--", entry.command, ...entry.args]);
|
|
155
190
|
try {
|
|
156
|
-
execFileSync(
|
|
191
|
+
execFileSync(remove.command, remove.args, { stdio: "ignore", ...remove.options });
|
|
157
192
|
}
|
|
158
193
|
catch { }
|
|
159
|
-
execFileSync(
|
|
194
|
+
execFileSync(add.command, add.args, { stdio: "ignore", ...add.options });
|
|
160
195
|
return { tool, where: "user scope", status: "ready in every project and every Conductor window" };
|
|
161
196
|
}
|
|
162
197
|
catch (e) {
|
|
@@ -172,11 +207,39 @@ function cursorUserScope(entry) {
|
|
|
172
207
|
return { tool: "cursor", where: file, status: `failed: ${e.message}` };
|
|
173
208
|
}
|
|
174
209
|
}
|
|
210
|
+
function copilotUserScope(entry) {
|
|
211
|
+
const tool = "copilot";
|
|
212
|
+
const file = copilotMcpConfigFile();
|
|
213
|
+
let present = existsSync(dirname(file));
|
|
214
|
+
if (!present && onPathNamed("copilot"))
|
|
215
|
+
present = true;
|
|
216
|
+
if (!present)
|
|
217
|
+
return { tool, where: "-", status: "skipped (copilot not on PATH)" };
|
|
218
|
+
try {
|
|
219
|
+
return { tool, where: file, status: mergeMcpJson(file, entry, COPILOT_ENTRY_SHAPE) };
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
return { tool, where: file, status: `failed: ${e.message}` };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function vsCodeUserScope(entry) {
|
|
226
|
+
const tool = "vs code";
|
|
227
|
+
const dir = vsCodeUserDir();
|
|
228
|
+
if (!dir)
|
|
229
|
+
return { tool, where: "-", status: "skipped (VS Code not found)" };
|
|
230
|
+
const file = join(dir, "mcp.json");
|
|
231
|
+
try {
|
|
232
|
+
return { tool, where: file, status: mergeVsCodeMcpJson(file, entry) };
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
return { tool, where: file, status: `failed: ${e.message}` };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
175
238
|
export const CODEX_APPROVAL_LINE = `default_tools_approval_mode = "approve"`;
|
|
176
239
|
export function approvalSummary(approval) {
|
|
177
240
|
return approval === "ask-before-sending"
|
|
178
241
|
? `your agent asks you before each send, goal or claim in your name; reading and waiting run without a prompt. Unattended runs keep their own per-run approvals — this governs the agent you sit in front of`
|
|
179
|
-
: `pantheon's ${PANTHEON_TOOL_NAMES.length} tools run without a per-call prompt in Claude Code, Cursor and Codex — your agent coordinates for you without stopping to ask`;
|
|
242
|
+
: `pantheon's ${PANTHEON_TOOL_NAMES.length} tools run without a per-call prompt in Claude Code, Cursor and Codex — your agent coordinates for you without stopping to ask. Copilot has no allow-list file: it asks once per tool per folder and remembers your answer`;
|
|
180
243
|
}
|
|
181
244
|
export function codexTomlBlock(entry, env = { PANTHEON_AGENT: "codex" }, approval = "approve") {
|
|
182
245
|
const envToml = Object.entries(env)
|
|
@@ -374,6 +437,7 @@ export function installForUser(approval = chosenApproval()) {
|
|
|
374
437
|
const entry = userEntry();
|
|
375
438
|
return [
|
|
376
439
|
claudeUserScope(entry), cursorUserScope(entry), codexUserScope(entry, approval),
|
|
440
|
+
copilotUserScope(entry), vsCodeUserScope(entry),
|
|
377
441
|
...grantToolPermissions(approval).map((g) => ({
|
|
378
442
|
tool: g.tool,
|
|
379
443
|
where: g.tool.startsWith("claude") ? claudeSettingsFile() : cursorCliConfigFile(),
|
package/dist/machine.js
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir, platform } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { CONFIG_DIR } from "./config.js";
|
|
6
6
|
import { plistPath } from "./service.js";
|
|
7
|
+
import { copilotMcpConfigFile, vsCodeUserDir } from "./install.js";
|
|
7
8
|
const sh = (cmd) => {
|
|
8
9
|
try {
|
|
9
10
|
return execFileSync("sh", ["-lc", cmd], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
@@ -109,6 +110,16 @@ export function wiring() {
|
|
|
109
110
|
tool: "codex",
|
|
110
111
|
state: existsSync(codex) && /\[mcp_servers\.pantheon\]/.test(readFileSync(codex, "utf8")) ? "wired" : "not wired",
|
|
111
112
|
});
|
|
113
|
+
const copilot = copilotMcpConfigFile();
|
|
114
|
+
out.push({
|
|
115
|
+
tool: "copilot",
|
|
116
|
+
state: existsSync(copilot) && /"pantheon"/.test(readFileSync(copilot, "utf8")) ? "wired" : "not wired",
|
|
117
|
+
});
|
|
118
|
+
const vscodeDir = vsCodeUserDir();
|
|
119
|
+
if (vscodeDir) {
|
|
120
|
+
const f = join(vscodeDir, "mcp.json");
|
|
121
|
+
out.push({ tool: "vs code", state: existsSync(f) && /"pantheon"/.test(readFileSync(f, "utf8")) ? "wired" : "not wired" });
|
|
122
|
+
}
|
|
112
123
|
return out;
|
|
113
124
|
}
|
|
114
125
|
export function logFileFor(kind, proc) {
|
package/dist/measure.js
CHANGED
|
@@ -3,7 +3,9 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { resolve, dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { claudeGrantedEffective, cursorGrantedEffective, claudeAllow, cursorAllow, isServerWide } from "./grants.js";
|
|
6
|
-
import { codexApprovalState } from "./install.js";
|
|
6
|
+
import { codexApprovalState, copilotMcpConfigFile } from "./install.js";
|
|
7
|
+
import { existsSync as fileExists, readFileSync } from "node:fs";
|
|
8
|
+
import { dirname as dirOf } from "node:path";
|
|
7
9
|
import { PANTHEON_TOOL_NAMES, WRITE_TOOL_NAMES } from "./tools.js";
|
|
8
10
|
const ROOT = process.env.PANTHEON_MCP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
11
|
export const HOST_STARTUP_ALLOWANCE_MS = 10_000;
|
|
@@ -157,12 +159,24 @@ export function toolsReport(registered, seen, approval, opts = {}) {
|
|
|
157
159
|
problems.push("Codex's per-tool overrides do not match your choice");
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
|
-
const
|
|
162
|
+
const copilot = seen.copilot === undefined || seen.copilot === null ? null
|
|
163
|
+
: seen.copilot ? "Copilot wired (it asks once per tool per folder and remembers)" : "Copilot has no pantheon entry";
|
|
164
|
+
const detail = `${head}; ${claude.text}, ${cursor.text}, ${codex}${copilot ? `, ${copilot}` : ""}`;
|
|
161
165
|
if (!problems.length)
|
|
162
166
|
return { level: "pass", detail };
|
|
163
167
|
return { level: "warn", detail, fix: `${problems.join("; ")} — pantheon install --user rewrites every grant in your own settings to match this build and your approval choice; a rule in this repo's .claude/settings*.json or .cursor/cli.json is yours to edit by hand` };
|
|
164
168
|
}
|
|
165
169
|
export function grantsSeen(projectDir = null) {
|
|
166
|
-
return { claude: claudeGrantedEffective(projectDir), cursor: cursorGrantedEffective(projectDir), codex: codexApprovalState() };
|
|
170
|
+
return { claude: claudeGrantedEffective(projectDir), cursor: cursorGrantedEffective(projectDir), codex: codexApprovalState(), copilot: copilotWired() };
|
|
171
|
+
}
|
|
172
|
+
export function copilotWired(file = copilotMcpConfigFile()) {
|
|
173
|
+
if (!fileExists(dirOf(file)))
|
|
174
|
+
return null;
|
|
175
|
+
try {
|
|
176
|
+
return fileExists(file) && /"pantheon"/.test(readFileSync(file, "utf8"));
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
167
181
|
}
|
|
168
182
|
//# sourceMappingURL=measure.js.map
|
package/dist/notice.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { decisionPrompt, decisionTitle } from "./decision-ui.js";
|
|
2
|
-
export const TOOL_LABELS = { "claude-code": "Claude Code", codex: "Codex", cursor: "Cursor", grok: "Grok" };
|
|
2
|
+
export const TOOL_LABELS = { "claude-code": "Claude Code", codex: "Codex", cursor: "Cursor", copilot: "Copilot", grok: "Grok" };
|
|
3
3
|
export const APP_TOOLS = [
|
|
4
4
|
{ id: "cursor-app", label: "Cursor app", kind: "app", bundle: "com.todesktop.230313mzl4w4u92", deeplink: "cursor://anysphere.cursor-deeplink/prompt?text={prompt}" },
|
|
5
5
|
{ id: "claude-app", label: "Claude app · question copied", kind: "app", bundle: "com.anthropic.claudefordesktop" },
|
|
6
6
|
{ id: "chatgpt-app", label: "ChatGPT app · question copied", kind: "app", bundle: "com.openai.chat" },
|
|
7
7
|
{ id: "codex-app", label: "Codex app · question copied", kind: "app", bundle: "com.openai.codex" },
|
|
8
|
+
{ id: "vscode-app", label: "VS Code · question copied", kind: "app", bundle: "com.microsoft.VSCode" },
|
|
8
9
|
];
|
|
9
10
|
export const BUDGET = { who: 60, heading: 40, context: 70, body: 190 };
|
|
10
11
|
const oneLine = (s) => s.replace(/\s+/g, " ").trim();
|
|
@@ -64,6 +65,8 @@ const agentLabel = (agent) => {
|
|
|
64
65
|
return "Codex";
|
|
65
66
|
if (a.includes("cursor"))
|
|
66
67
|
return "Cursor";
|
|
68
|
+
if (a.includes("copilot"))
|
|
69
|
+
return "Copilot";
|
|
67
70
|
return null;
|
|
68
71
|
};
|
|
69
72
|
function whoLine(from, agent, ts, now) {
|
package/dist/notifier.js
CHANGED
|
@@ -99,6 +99,7 @@ export function installedTools() {
|
|
|
99
99
|
"claude-app": ["/Applications/Claude.app", join(homedir(), "Applications", "Claude.app")],
|
|
100
100
|
"chatgpt-app": ["/Applications/ChatGPT.app", join(homedir(), "Applications", "ChatGPT.app")],
|
|
101
101
|
"codex-app": ["/Applications/Codex.app", join(homedir(), "Applications", "Codex.app")],
|
|
102
|
+
"vscode-app": ["/Applications/Visual Studio Code.app", join(homedir(), "Applications", "Visual Studio Code.app")],
|
|
102
103
|
};
|
|
103
104
|
for (const t of APP_TOOLS)
|
|
104
105
|
if ((APP_PATHS[t.id] ?? []).some((p) => existsSync(p)))
|
package/dist/notify.js
CHANGED
|
@@ -3,8 +3,11 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { whichPantheon } from "./service.js";
|
|
5
5
|
import { detectForHooks, agentById } from "./agents.js";
|
|
6
|
-
import { codexHookState, cursorHookState, installCodexHooks, installCursorHooks, removeCodexHooks, removeCursorHooks, } from "./hooks-install.js";
|
|
6
|
+
import { codexHookState, cursorHookState, copilotHookState, installCodexHooks, installCursorHooks, installCopilotHooks, removeCodexHooks, removeCursorHooks, removeCopilotHooks, } from "./hooks-install.js";
|
|
7
7
|
export const HOOK_MARKER = "pantheon hook";
|
|
8
|
+
export const HOOK_RE = /pantheon(?:\.cmd|\.exe|\.bat)?"?\s+hook\s/i;
|
|
9
|
+
export const isOurHookCommand = (cmd) => !!cmd && (cmd.includes(HOOK_MARKER) || HOOK_RE.test(cmd));
|
|
10
|
+
export const hookBin = (bin) => (/\s/.test(bin) ? `"${bin}"` : bin);
|
|
8
11
|
export function settingsPath(scope, projectDir = process.cwd()) {
|
|
9
12
|
return scope === "user"
|
|
10
13
|
? join(homedir(), ".claude", "settings.json")
|
|
@@ -25,7 +28,7 @@ function write(file, doc) {
|
|
|
25
28
|
writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
|
|
26
29
|
}
|
|
27
30
|
function ours(g) {
|
|
28
|
-
return g.hooks?.some((h) => h.command
|
|
31
|
+
return g.hooks?.some((h) => isOurHookCommand(h.command)) ?? false;
|
|
29
32
|
}
|
|
30
33
|
export function hookState(projectDir = process.cwd(), only) {
|
|
31
34
|
for (const scope of (only ? [only] : ["project", "user"])) {
|
|
@@ -46,7 +49,7 @@ export function hookState(projectDir = process.cwd(), only) {
|
|
|
46
49
|
if (!hit)
|
|
47
50
|
continue;
|
|
48
51
|
events.push(event);
|
|
49
|
-
command ??= hit.hooks.find((h) => h.command
|
|
52
|
+
command ??= hit.hooks.find((h) => isOurHookCommand(h.command)).command;
|
|
50
53
|
}
|
|
51
54
|
if (events.length)
|
|
52
55
|
return { installed: true, scope, file, command, events };
|
|
@@ -93,6 +96,9 @@ export function agentHookStates(projectDir = process.cwd(), only) {
|
|
|
93
96
|
const cursor = cursorHookState(scope, projectDir);
|
|
94
97
|
if (cursor.installed)
|
|
95
98
|
out.push(cursor);
|
|
99
|
+
const copilot = copilotHookState(scope, projectDir);
|
|
100
|
+
if (copilot.installed)
|
|
101
|
+
out.push(copilot);
|
|
96
102
|
}
|
|
97
103
|
const byAgent = new Map();
|
|
98
104
|
for (const s of out) {
|
|
@@ -116,6 +122,8 @@ export function repairHook(home) {
|
|
|
116
122
|
missing.push(id);
|
|
117
123
|
if (id === "cursor" && !cursorHookState(intent.scope, projectDir).installed)
|
|
118
124
|
missing.push(id);
|
|
125
|
+
if (id === "copilot" && !copilotHookState(intent.scope, projectDir).installed)
|
|
126
|
+
missing.push(id);
|
|
119
127
|
}
|
|
120
128
|
if (!missing.length)
|
|
121
129
|
return null;
|
|
@@ -132,7 +140,10 @@ export function repairHook(home) {
|
|
|
132
140
|
function installClaudeHook(scope, projectDir = process.cwd()) {
|
|
133
141
|
const file = settingsPath(scope, projectDir);
|
|
134
142
|
const doc = read(file);
|
|
135
|
-
const
|
|
143
|
+
const found = whichPantheon();
|
|
144
|
+
if (!found)
|
|
145
|
+
throw new Error("pantheon binary not found on PATH");
|
|
146
|
+
const bin = hookBin(found);
|
|
136
147
|
const events = {
|
|
137
148
|
Stop: `${bin} hook stop`,
|
|
138
149
|
UserPromptSubmit: `${bin} hook prompt`,
|
|
@@ -165,7 +176,7 @@ export function installHooks(scope, projectDir = process.cwd(), only) {
|
|
|
165
176
|
? only.map((id) => agentById(id)).filter((b) => !!b)
|
|
166
177
|
: available;
|
|
167
178
|
if (!pick.length) {
|
|
168
|
-
throw new Error("no agent found — install claude, codex,
|
|
179
|
+
throw new Error("no agent found — install claude, codex, cursor-agent, or copilot, or open a project that already uses one");
|
|
169
180
|
}
|
|
170
181
|
const lines = [];
|
|
171
182
|
const agents = [];
|
|
@@ -183,6 +194,10 @@ export function installHooks(scope, projectDir = process.cwd(), only) {
|
|
|
183
194
|
lines.push(...installCursorHooks(bin, scope, projectDir));
|
|
184
195
|
agents.push("cursor");
|
|
185
196
|
}
|
|
197
|
+
else if (backend.id === "copilot") {
|
|
198
|
+
lines.push(...installCopilotHooks(bin, scope, projectDir));
|
|
199
|
+
agents.push("copilot");
|
|
200
|
+
}
|
|
186
201
|
}
|
|
187
202
|
lines.push(` effect when your agent tries to finish, it checks whether a peer is waiting`, ` on you — and keeps going instead of leaving them hanging.`, ` it also reports when a turn starts and ends, so a worker can tell`, ` an idle editor from one you are actually using.`);
|
|
188
203
|
return { lines, agents };
|
|
@@ -195,6 +210,7 @@ export function removeHook(projectDir = process.cwd()) {
|
|
|
195
210
|
out.push(...removeClaudeHook(projectDir));
|
|
196
211
|
out.push(...removeCodexHooks(projectDir));
|
|
197
212
|
out.push(...removeCursorHooks(projectDir));
|
|
213
|
+
out.push(...removeCopilotHooks(projectDir));
|
|
198
214
|
return out.length ? out : [" nothing installed"];
|
|
199
215
|
}
|
|
200
216
|
function removeClaudeHook(projectDir = process.cwd()) {
|
package/dist/persona.js
CHANGED
|
@@ -170,7 +170,7 @@ export const PERSONA_TEMPLATE = `---
|
|
|
170
170
|
# wake_by: alice, bob # only these peers' requests are handled by this persona
|
|
171
171
|
# kinds: ANSWER, ACCEPT # ANSWER (asks), ACCEPT (verdicts), REVISE (objections), CONTINUE (replies/deliveries)
|
|
172
172
|
# goals: launch-* # only goals whose id or title matches
|
|
173
|
-
# agent: auto # claude-code | codex | cursor | auto
|
|
173
|
+
# agent: auto # claude-code | codex | cursor | copilot | auto
|
|
174
174
|
---
|
|
175
175
|
Say how this worker should behave when it is woken while you are away — what it
|
|
176
176
|
should take on, what it should refuse, and where its judgment ends and yours
|
package/dist/profile.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { CONFIG_DIR } from "./config.js";
|
|
4
|
-
export const AGENT_SURFACES = ["codex", "claude-code", "cursor"];
|
|
4
|
+
export const AGENT_SURFACES = ["codex", "claude-code", "cursor", "copilot"];
|
|
5
5
|
export const SURFACE_BUNDLE_IDS = {
|
|
6
6
|
codex: "com.openai.codex",
|
|
7
7
|
"claude-code": "com.anthropic.claudefordesktop",
|
|
8
8
|
cursor: "com.todesktop.230313mzl4w4u92",
|
|
9
|
+
copilot: "com.microsoft.VSCode",
|
|
9
10
|
};
|
|
10
11
|
export const profilePath = (home = CONFIG_DIR) => join(home, "profile.json");
|
|
11
12
|
export function isAgentSurface(value) {
|
|
@@ -14,6 +15,8 @@ export function isAgentSurface(value) {
|
|
|
14
15
|
export function surfaceFromAgent(agent) {
|
|
15
16
|
if (agent && /cursor/i.test(agent))
|
|
16
17
|
return "cursor";
|
|
18
|
+
if (agent && /copilot/i.test(agent))
|
|
19
|
+
return "copilot";
|
|
17
20
|
if (agent && /claude/i.test(agent))
|
|
18
21
|
return "claude-code";
|
|
19
22
|
return "codex";
|
package/dist/quickstart.js
CHANGED
|
@@ -16,6 +16,7 @@ import { desktopUnavailable, ephemeralWarning, hostEnv } from "./host-env.js";
|
|
|
16
16
|
import { existsSync } from "node:fs";
|
|
17
17
|
import { grant as grantCap, loadPolicy, policyPath, savePolicy } from "./worker-policy.js";
|
|
18
18
|
import { Relay } from "./relay.js";
|
|
19
|
+
import { which } from "./which.js";
|
|
19
20
|
const sh = (cmd) => {
|
|
20
21
|
try {
|
|
21
22
|
return execFileSync("sh", ["-lc", cmd], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
@@ -126,7 +127,7 @@ export function findReach(port) {
|
|
|
126
127
|
if (ts && /^\d+\.\d+\.\d+\.\d+$/.test(ts)) {
|
|
127
128
|
return { url: `ws://${ts}:${port}`, how: `tailscale (${ts})`, shareable: true };
|
|
128
129
|
}
|
|
129
|
-
if (
|
|
130
|
+
if (which("cloudflared")) {
|
|
130
131
|
return {
|
|
131
132
|
url: `ws://127.0.0.1:${port}`, how: "localhost", shareable: false,
|
|
132
133
|
hint: `cloudflared is installed. To let a teammate reach you:\n` +
|
package/dist/run-failure.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
export const AUTH_RE = /not (?:logged|signed) in|not authenticated|no (?:github )?(?:authentication|credentials?|token) (?:found|available)|login required|please (?:log|sign) in|run .*login|invalid api key|unauthori[sz]ed|authentication (?:failed|required)|\b401\b|expired (?:token|session)/i;
|
|
3
|
+
export const SIGN_IN_FIX = {
|
|
4
|
+
"claude-code": "run `claude`, then /login",
|
|
5
|
+
codex: "run `codex login`",
|
|
6
|
+
cursor: "run `cursor-agent login`",
|
|
7
|
+
copilot: "run `copilot login` (or `gh auth login` — copilot uses gh's sign-in too)",
|
|
8
|
+
};
|
|
2
9
|
const sh = (dir, ...args) => execFileSync("git", ["-C", dir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000 }).trim();
|
|
3
10
|
export const gitProbe = {
|
|
4
11
|
isRepo: (dir) => { try {
|
|
@@ -42,6 +49,14 @@ catch {
|
|
|
42
49
|
return fallback;
|
|
43
50
|
} };
|
|
44
51
|
export function diagnoseRun(f, probe = gitProbe) {
|
|
52
|
+
if (!f.exited && AUTH_RE.test(f.detail)) {
|
|
53
|
+
const fix = SIGN_IN_FIX[f.agent ?? ""] ?? "sign in to the agent CLI on this machine";
|
|
54
|
+
return {
|
|
55
|
+
cause: "not_signed_in",
|
|
56
|
+
says: `The agent could not start because nobody is signed in to it${f.agent ? ` (${f.agent})` : ""} — ${exitLine(f.detail)}.`,
|
|
57
|
+
fix: `Sign in once and the next run works: ${fix}. Until then every woken run on this machine fails the same way.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
45
60
|
if (!f.exited) {
|
|
46
61
|
return {
|
|
47
62
|
cause: "crashed",
|
package/dist/service.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { homedir, platform } from "node:os";
|
|
4
|
+
import { which } from "./which.js";
|
|
4
5
|
import { dirname, join, resolve } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { launcher } from "./install.js";
|
|
@@ -29,6 +30,8 @@ function argvFor(kind, args) {
|
|
|
29
30
|
return abs ? [abs, kind, ...args] : [process.execPath, resolve(HERE, "cli.js"), kind, ...args];
|
|
30
31
|
}
|
|
31
32
|
export function whichPantheon() {
|
|
33
|
+
if (platform() === "win32")
|
|
34
|
+
return which("pantheon");
|
|
32
35
|
try {
|
|
33
36
|
const p = execFileSync("/usr/bin/which", ["pantheon"], { encoding: "utf8" }).trim();
|
|
34
37
|
return p.startsWith("/") && existsSync(p) ? p : null;
|
package/dist/setup-guide.js
CHANGED
|
@@ -195,7 +195,7 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
195
195
|
: "only people you approve by hand (pantheon admit <name> --may-wake)"}`);
|
|
196
196
|
io.say(` • Alerts: ${shortAlert(a.notifications).toLowerCase()}`);
|
|
197
197
|
io.say(` • Approval: ${a.approval === "approve"
|
|
198
|
-
? "your agent sends to teammates in your name without asking each time (Claude Code, Cursor and Codex)"
|
|
198
|
+
? "your agent sends to teammates in your name without asking each time (Claude Code, Cursor and Codex; Copilot asks once per tool per folder)"
|
|
199
199
|
: "your agent asks you before each send, goal and claim; reading and waiting never prompt"}`);
|
|
200
200
|
io.say(` • Decisions: ${a.decides.length ? `you personally decide ${a.decides.join(", ")}` : "none reserved for you — your agent may decide; add an area when one comes up"}`);
|
|
201
201
|
if (a.wake !== "no" && ctx.project)
|
package/dist/tray.js
CHANGED
|
@@ -144,7 +144,7 @@ async function trayStatusOne(cfg) {
|
|
|
144
144
|
];
|
|
145
145
|
const profile = loadProfile(cfg);
|
|
146
146
|
const surface = profile.agent.primary_surface;
|
|
147
|
-
const surfaceName = surface === "claude-code" ? "Claude" : surface === "cursor" ? "Cursor" : "Codex";
|
|
147
|
+
const surfaceName = surface === "claude-code" ? "Claude" : surface === "cursor" ? "Cursor" : surface === "copilot" ? "Copilot" : "Codex";
|
|
148
148
|
const rawWorkerState = workerState();
|
|
149
149
|
return {
|
|
150
150
|
configured: true,
|
package/dist/update-check.js
CHANGED
|
@@ -43,6 +43,8 @@ export function updateStatus(deps) {
|
|
|
43
43
|
export function versionLine(version, behind, latest) {
|
|
44
44
|
if (behind <= 0)
|
|
45
45
|
return `pantheon ${version}`;
|
|
46
|
+
if (latest && !semverGt(latest, version))
|
|
47
|
+
return `pantheon ${version}`;
|
|
46
48
|
return latest
|
|
47
49
|
? `pantheon ${version} · ${latest} available — run \`pantheon upgrade\``
|
|
48
50
|
: `pantheon ${version} · ${behind} update${behind === 1 ? "" : "s"} behind — run \`pantheon upgrade\``;
|