@paigy/mcp 0.35.0 → 0.36.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/dist/{chunk-U6SLIORK.js → chunk-PRC6GKRN.js} +1937 -691
- package/dist/chunk-UG2FARVV.js +54 -0
- package/dist/chunk-VWVZLPZD.js +143 -0
- package/dist/{chunk-YOGAG4YY.js → chunk-WNE6STMA.js} +633 -489
- package/dist/chunk-XIPO3U6I.js +309 -0
- package/dist/enable.js +3 -2
- package/dist/index.js +115 -354
- package/dist/listen.js +31 -34
- package/dist/onboard.js +62 -45
- package/dist/statusline.js +1 -1
- package/package.json +7 -7
- package/dist/chunk-KNMI3CJ3.js +0 -177
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHECK_REPLIES_DESCRIPTION,
|
|
3
|
+
CONTACT_DESCRIPTION,
|
|
4
|
+
CONTACT_SCHEMA,
|
|
5
|
+
CREATE_GOAL_DESCRIPTION,
|
|
6
|
+
CreateGoalSchema,
|
|
7
|
+
HandoffSchema,
|
|
8
|
+
SCHEDULE_CALLBACK_DESCRIPTION,
|
|
9
|
+
SEARCH_THREADS_DESCRIPTION,
|
|
10
|
+
SET_WORK_STATE_DESCRIPTION,
|
|
11
|
+
ScheduleCallbackSchema,
|
|
12
|
+
SetTaskStateSchema,
|
|
13
|
+
SetWorkStateSchema,
|
|
14
|
+
mcpInputSchema,
|
|
15
|
+
serverInstructions
|
|
16
|
+
} from "./chunk-WNE6STMA.js";
|
|
17
|
+
import {
|
|
18
|
+
AGENT_NAME
|
|
19
|
+
} from "./chunk-PRC6GKRN.js";
|
|
20
|
+
|
|
21
|
+
// src/toolset.ts
|
|
22
|
+
var ONBOARD_DESCRIPTION = "Get this agent talking to Paigy \u2014 call it FIRST, before contact/await_reply, and any time you're unsure who you are. One call, and it does whatever the situation needs: NOT SET UP \u2192 hatches an identity instantly if this machine holds a device credential (the user ran the Paigy desktop app or harness), otherwise starts the code ceremony; ALREADY SET UP \u2192 returns your current identity and offers the two things left to decide, renaming it or unpairing; TOKEN NO LONGER VALID \u2192 says so, then re-pairs. Pass { name, voice } to choose who you are when hatching, or to RENAME yourself when already set up (voices: rachel, george, jessica, brian, lily). Safe to call any time: idempotent, and it never writes settings \u2014 the tool-allowlist state it reports is read-only. If it returns a `user_code`, print it to the user immediately and call onboard again with the `device_code`. If it returns `enable_command`, PRINT that command for the user to run \u2014 you cannot apply it yourself (it writes your own permission allowlist, which hosts block as privilege escalation), so print it, don't wait for it, and carry on.";
|
|
23
|
+
var SERVER_INSTRUCTIONS = serverInstructions({ waits: true });
|
|
24
|
+
|
|
25
|
+
// src/tools.ts
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
var AwaitReplySchema = z.object({
|
|
28
|
+
notificationId: z.string().describe("The notificationId returned by contact \u2014 waits for the user's reply to THIS notification only.")
|
|
29
|
+
});
|
|
30
|
+
var OnboardSchema = z.object({
|
|
31
|
+
name: z.string().max(60).optional(),
|
|
32
|
+
voice: z.string().max(40).optional(),
|
|
33
|
+
/** Continue a code ceremony already in flight — same meaning as `pair`'s. */
|
|
34
|
+
device_code: z.string().optional(),
|
|
35
|
+
/** Which allowlist to REPORT on (never written by `start`). */
|
|
36
|
+
scope: z.enum(["user", "project"]).optional()
|
|
37
|
+
});
|
|
38
|
+
var PairSchema = z.object({
|
|
39
|
+
device_code: z.string().optional().describe("Omit to start pairing (returns an approval link to show the user). Pass the device_code from that first call to finish, once the user has approved."),
|
|
40
|
+
name: z.string().min(1).max(60).optional().describe("Hatch path only: the name you choose for this identity. Pick your own \u2014 ONE or TWO words, the way you'd introduce yourself on a call (it is spoken aloud and shown in lists). 'Piper', 'Blue Heron' \u2014 never a sentence or a task description."),
|
|
41
|
+
voice: z.string().optional().describe("Hatch path only: your voice on calls \u2014 one of rachel, george, jessica, brian, lily.")
|
|
42
|
+
});
|
|
43
|
+
var GetThreadSchema = z.object({
|
|
44
|
+
parentId: z.string().describe("The thread to read \u2014 from a reply, request, or past notification.")
|
|
45
|
+
});
|
|
46
|
+
var SearchThreadsSchema = z.object({
|
|
47
|
+
q: z.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
|
|
48
|
+
});
|
|
49
|
+
var SetTaskStateToolSchema = z.object({
|
|
50
|
+
notificationId: z.string(),
|
|
51
|
+
state: SetTaskStateSchema.shape.state
|
|
52
|
+
});
|
|
53
|
+
var AnswerCallerQuestionSchema = z.object({
|
|
54
|
+
notificationId: z.string().describe("The notification whose call carried the caller's question \u2014 from the partial turn or the settled reply."),
|
|
55
|
+
answer: z.string().min(1).max(1500).describe("The answer, as one or two short SPOKEN sentences \u2014 it may be read aloud on the live call.")
|
|
56
|
+
});
|
|
57
|
+
var TOOLS = [
|
|
58
|
+
{
|
|
59
|
+
// #575: THE attention verb — the one model-facing surface, every agent.
|
|
60
|
+
// The retired names (notify/notify_user) stay callable as hidden aliases
|
|
61
|
+
// for stale prompts and cached servers; see toolset.ts for the design.
|
|
62
|
+
name: "contact",
|
|
63
|
+
description: CONTACT_DESCRIPTION,
|
|
64
|
+
inputSchema: CONTACT_SCHEMA
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
// ONE rail for starting a session (#875). `pair`, the `paigy-mcp-onboard` CLI and
|
|
68
|
+
// `enable_tools` were three doors into one flow, and "onboard" was the word people
|
|
69
|
+
// reached for attached to the tool that did the least. It's the door now.
|
|
70
|
+
name: "onboard",
|
|
71
|
+
description: ONBOARD_DESCRIPTION,
|
|
72
|
+
inputSchema: mcpInputSchema(OnboardSchema)
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "pair",
|
|
76
|
+
description: "Pair this agent with the user's Paigy account (one-time) \u2014 required before contact/await_reply work. FAST PATH: if this machine already holds a device credential (the user ran the Paigy desktop harness or app), calling pair hatches a fresh identity INSTANTLY \u2014 no code, no approval. Pass { name, voice } to choose who you are (pick your own; voices: rachel, george, jessica, brian, lily). Only when no device credential exists does the code ceremony below run. It does NOT open a browser; the user enters the code in the Paigy app (or scans `qr`). Step 1: call with NO args \u2014 returns { user_code, device_code, qr, user_message } AND starts polling for approval in the background. REQUIRED: You MUST immediately print the `user_message` (the bare code) as a text message to the user, AND in that same turn call step 2 (pair with the device_code). This ensures the user sees the code in chat while the tool blocks/polls in the background for approval. Step 2: call with that device_code to collect the result. Because approval is already being polled in the background, this returns the moment the user approves; on { status:'pending' } just call again to keep waiting; on { status:'awaiting_confirmation' } (E2EE) show the bare `user_message` verify code and call again to finish. The leading text block of every result states the code plainly, so it shows even if you emit no prose. On { status:'paired' } ALWAYS follow the `enable_prompt` \u2014 PRINT the returned `enable_command` so the user can allowlist Paigy's tools and notify/await stop prompting each time. Printing is the whole job: that command writes your own permission allowlist, so you must not run it and a host will block you if you try.",
|
|
77
|
+
inputSchema: mcpInputSchema(PairSchema)
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "unpair",
|
|
81
|
+
description: "Log out / unpair this agent from the user's Paigy account: revokes the token server-side (it stops working everywhere) and deletes the local ~/.paigy/token.json. Takes no arguments. After this, contact/await_reply won't work until the user pairs again with the pair tool.",
|
|
82
|
+
inputSchema: mcpInputSchema(z.object({}))
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "await_reply",
|
|
86
|
+
description: "Wait for the user's reply to a live call or an answer contact says already exists (pass its notificationId). A quiet inbox delivery is asynchronous: keep working and use check_replies later instead. On a live call, keep calling await_reply for your answer in-context. Polls ~45s per call \u2014 deliberately under the 60s cap most hosts put on a single tool call, so it ALWAYS returns you something. Returns { type:'reply', answer, workId?, decisionId? } when they respond, { type:'remind', remindInSeconds } on snooze (ScheduleWakeup then await_reply again), or { type:'idle' } (this window ended, no answer yet). While your contact is being handled on a LIVE call, you may receive { type:'partial', inFlight:true, turn } results: what the user said to each turn, as they say it. Use partials to PREPARE \u2014 fetch the data, draft the thing, warm the build \u2014 never to act irreversibly: the user can still revise any of them until the final reply arrives. Partial = intelligence, settled = authorization. If a partial's acts carry a question aimed at you and you know the answer, call contact on the SAME parentId right away \u2014 the caller hears your answer on the same call instead of waiting for a callback. Keep calling await_reply until you get the final reply \u2014 THAT one is the decision. On idle, if this is genuinely still blocking you and you have nothing else useful to do meanwhile, just call await_reply again immediately \u2014 keep looping. This is how you actually deliver on the point of calling: the user steps away for a while and comes back to find you'd already continued the moment they answered, not idle waiting to be checked on. Don't give up after one window. Only stop looping to do other work (and check back later), or after an unreasonably long stretch (tens of minutes to hours) worth telling the user about instead. Scoped to that one notification \u2014 it NEVER returns replies meant for other notifications, so concurrent contact calls don't cross. A CALL answer can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a blocking question'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's parentId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry `intents` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' (\"call me after lunch\") \u2192 register it NOW with schedule_callback \u2014 when the intent carries `dueInSeconds` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' (\"you pick\") \u2192 make the call yourself and tell them what you chose; 'channel' (\"text me next time\") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped \"should I call you back?\" because the follow-up is implied. `transcript` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions (\"yes, IF tests pass\") before acting. If your ask declared `points`, the reply carries `covered` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same parentId) or proceed knowingly partial; never treat a partial answer as complete.",
|
|
87
|
+
inputSchema: mcpInputSchema(AwaitReplySchema)
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "check_replies",
|
|
91
|
+
description: CHECK_REPLIES_DESCRIPTION,
|
|
92
|
+
inputSchema: mcpInputSchema(z.object({}))
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "get_thread",
|
|
96
|
+
description: "The chronological transcript of one Paigy conversation thread \u2014 every past ask, answer, and user request on it. Call this to REHYDRATE when you're resuming or being seeded: a check_replies request whose parentId you don't recognize means the user is continuing an old conversation with you, and one carrying a contextParentId means they want a past conversation (possibly with a DIFFERENT agent) as your starting context \u2014 in both cases call get_thread FIRST and read the turns as prior conversation you were part of, not as new input. Turns: { role:'agent', title, description[], answer }, { role:'user', text }, and context turns { role:'handoff'|'recap', title, description[] } \u2014 a handoff is a predecessor's brief for you; a recap SUMMARIZES everything before it (the transcript starts at the latest recap, so treat it as the base and the turns after it as what happened since). Oldest first, capped at the most recent 30.",
|
|
97
|
+
inputSchema: mcpInputSchema(GetThreadSchema)
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "search_threads",
|
|
101
|
+
description: SEARCH_THREADS_DESCRIPTION,
|
|
102
|
+
inputSchema: mcpInputSchema(SearchThreadsSchema)
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "create_goal",
|
|
106
|
+
description: CREATE_GOAL_DESCRIPTION,
|
|
107
|
+
inputSchema: mcpInputSchema(CreateGoalSchema)
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "set_work_state",
|
|
111
|
+
description: SET_WORK_STATE_DESCRIPTION,
|
|
112
|
+
inputSchema: mcpInputSchema(SetWorkStateSchema)
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "answer_caller_question",
|
|
116
|
+
description: "Answer a question the user asked DURING a live call, while they're still on it. When a partial turn or a settled reply carries a `question` intent aimed at you, answer it here immediately: if their call is still live, your answer is spoken to them on that same call (returns live: true). If the call already ended (live: false), send the answer as a threaded contact instead \u2014 never drop it. Short spoken sentences only; this may be read aloud.",
|
|
117
|
+
inputSchema: mcpInputSchema(AnswerCallerQuestionSchema)
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "schedule_callback",
|
|
121
|
+
description: SCHEDULE_CALLBACK_DESCRIPTION,
|
|
122
|
+
inputSchema: mcpInputSchema(ScheduleCallbackSchema)
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "handoff",
|
|
126
|
+
description: "Deposit your working context for a SUCCESSOR agent \u2014 what you did, what's left, links, gotchas \u2014 as one note on a thread ({ title, notes[] }). This does NOT ring the user or enter their inbox: it's context, not a question. The successor reads it back with get_thread. Pass `target` (a sibling connection's token id or agent name, SAME account only) to hand off DIRECTLY to that agent \u2014 the note is dispatched to it as a request it picks up. Pass `workId` to move that existing outcome and its pending replies to the successor without reminting it; a target is required in that form. Omit `target` to leave the thread for the user to hand off to an agent themselves in the app. Pass `parentId` to land the handoff on an existing conversation; omit it to mint a fresh thread. Returns { parentId }. Pass recap:true when the note SUMMARIZES the thread so far (for a successor OR for your own later session): a recap resets the rehydration window \u2014 get_thread returns the latest recap + only the turns after it. Write one whenever a thread has grown long and you're pausing, handing off, or nearing your context limit.",
|
|
127
|
+
inputSchema: mcpInputSchema(HandoffSchema)
|
|
128
|
+
}
|
|
129
|
+
];
|
|
130
|
+
var TOOL_NAMES = TOOLS.map((t) => t.name);
|
|
131
|
+
var HIDDEN_ALIASES = ["notify", "notify_user", "set_task_state"];
|
|
132
|
+
|
|
133
|
+
// src/clients.ts
|
|
134
|
+
import { execFile } from "child_process";
|
|
135
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
136
|
+
import { homedir, platform } from "os";
|
|
137
|
+
import { join, dirname } from "path";
|
|
138
|
+
function openBrowser(url) {
|
|
139
|
+
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
140
|
+
const args = platform() === "win32" ? ["/c", "start", url] : [url];
|
|
141
|
+
execFile(cmd, args, () => {
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function withPaigyMcpJson(existing, agent) {
|
|
145
|
+
let config = {};
|
|
146
|
+
if (existing) {
|
|
147
|
+
try {
|
|
148
|
+
config = JSON.parse(existing);
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
config.mcpServers = {
|
|
154
|
+
...config.mcpServers ?? {},
|
|
155
|
+
paigy: { command: "npx", args: ["-y", "@paigy/mcp@latest"], env: { PAIGY_AGENT: agent } }
|
|
156
|
+
};
|
|
157
|
+
return JSON.stringify(config, null, 2);
|
|
158
|
+
}
|
|
159
|
+
function withPaigyMcpToml(existing, agent) {
|
|
160
|
+
if (existing.includes("[mcp_servers.paigy]")) return null;
|
|
161
|
+
const entry = [
|
|
162
|
+
"",
|
|
163
|
+
"[mcp_servers.paigy]",
|
|
164
|
+
'command = "npx"',
|
|
165
|
+
'args = ["-y", "@paigy/mcp@latest"]',
|
|
166
|
+
`env = { PAIGY_AGENT = "${agent}" }`,
|
|
167
|
+
""
|
|
168
|
+
].join("\n");
|
|
169
|
+
return existing + entry;
|
|
170
|
+
}
|
|
171
|
+
function autoConfigureClients(skip = AGENT_NAME) {
|
|
172
|
+
const home = homedir();
|
|
173
|
+
const registered = [];
|
|
174
|
+
if (skip !== "antigravity") {
|
|
175
|
+
const antigravityConfigs = [
|
|
176
|
+
join(home, ".gemini", "config", "mcp_config.json"),
|
|
177
|
+
join(home, ".gemini", "antigravity-cli", "mcp_config.json")
|
|
178
|
+
];
|
|
179
|
+
for (const path of antigravityConfigs) {
|
|
180
|
+
try {
|
|
181
|
+
if (!existsSync(dirname(path))) continue;
|
|
182
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
183
|
+
const updated = withPaigyMcpJson(existing, "antigravity");
|
|
184
|
+
if (updated === null) continue;
|
|
185
|
+
writeFileSync(path, updated);
|
|
186
|
+
registered.push(`Google Antigravity (${path})`);
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (skip !== "codex") {
|
|
192
|
+
const codexConfig = join(home, ".codex", "config.toml");
|
|
193
|
+
try {
|
|
194
|
+
if (existsSync(dirname(codexConfig))) {
|
|
195
|
+
const existing = existsSync(codexConfig) ? readFileSync(codexConfig, "utf8") : "";
|
|
196
|
+
const updated = withPaigyMcpToml(existing, "codex");
|
|
197
|
+
if (updated !== null) {
|
|
198
|
+
writeFileSync(codexConfig, updated);
|
|
199
|
+
registered.push(`OpenAI Codex (${codexConfig})`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return registered;
|
|
206
|
+
}
|
|
207
|
+
function claudeInstallHint(skip = AGENT_NAME) {
|
|
208
|
+
if (skip.startsWith("claude")) return null;
|
|
209
|
+
const claudeDir = join(homedir(), ".claude");
|
|
210
|
+
if (!existsSync(claudeDir)) return null;
|
|
211
|
+
try {
|
|
212
|
+
const registry = join(claudeDir, "plugins", "installed_plugins.json");
|
|
213
|
+
if (existsSync(registry) && readFileSync(registry, "utf8").includes('"paigy@')) return null;
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
return "Claude Code detected \u2014 to add Paigy there, run /plugin marketplace add paigy-ai/mcp then /plugin install paigy (pairing carries over; no need to pair again). Note: If you run this inside a live session, type /reload-plugins afterward so the agent connects to the new tools.";
|
|
217
|
+
}
|
|
218
|
+
var ENABLE_COMMAND = "npx -y -p @paigy/mcp@latest paigy-enable-tools";
|
|
219
|
+
var PROMPTS_ON_PURPOSE = /* @__PURE__ */ new Set(["pair", "unpair"]);
|
|
220
|
+
var PAIGY_TOOL_IDS = [
|
|
221
|
+
...TOOL_NAMES.filter((n) => !PROMPTS_ON_PURPOSE.has(n)),
|
|
222
|
+
...HIDDEN_ALIASES
|
|
223
|
+
].map((n) => `mcp__paigy__${n}`);
|
|
224
|
+
function withPaigyAllowlist(existing, tools) {
|
|
225
|
+
let config = {};
|
|
226
|
+
if (existing) {
|
|
227
|
+
try {
|
|
228
|
+
config = JSON.parse(existing);
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const permissions = config.permissions && typeof config.permissions === "object" ? config.permissions : {};
|
|
234
|
+
const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
|
|
235
|
+
for (const t of tools) if (!allow.includes(t)) allow.push(t);
|
|
236
|
+
config.permissions = { ...permissions, allow };
|
|
237
|
+
return JSON.stringify(config, null, 2);
|
|
238
|
+
}
|
|
239
|
+
function settingsPathFor(scope, cwd) {
|
|
240
|
+
return scope === "project" ? join(cwd, ".claude", "settings.json") : join(homedir(), ".claude", "settings.json");
|
|
241
|
+
}
|
|
242
|
+
function paigyToolsAllowlisted(scope = "user", cwd = process.cwd()) {
|
|
243
|
+
try {
|
|
244
|
+
const path = settingsPathFor(scope, cwd);
|
|
245
|
+
if (!path || !existsSync(path)) return false;
|
|
246
|
+
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
247
|
+
const allow = Array.isArray(config.permissions?.allow) ? config.permissions.allow : [];
|
|
248
|
+
return PAIGY_TOOL_IDS.every((t) => allow.includes(t));
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function enablePaigyTools(scope = "user", cwd = process.cwd()) {
|
|
254
|
+
if (scope === "user" && !existsSync(join(homedir(), ".claude"))) {
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
reason: `No ~/.claude on this machine \u2014 the hosting tool uses its own permission allowlist. Add these ids there by hand: ${PAIGY_TOOL_IDS.join(", ")}.`
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const path = settingsPathFor(scope, cwd);
|
|
261
|
+
let existing = null;
|
|
262
|
+
try {
|
|
263
|
+
existing = existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
264
|
+
} catch {
|
|
265
|
+
existing = null;
|
|
266
|
+
}
|
|
267
|
+
let prevAllow = [];
|
|
268
|
+
if (existing) {
|
|
269
|
+
try {
|
|
270
|
+
const parsed = JSON.parse(existing);
|
|
271
|
+
if (Array.isArray(parsed.permissions?.allow)) prevAllow = parsed.permissions.allow;
|
|
272
|
+
} catch {
|
|
273
|
+
return { ok: false, reason: `${path} didn't parse as JSON \u2014 not overwriting it. Add the ids by hand.` };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const updated = withPaigyAllowlist(existing, PAIGY_TOOL_IDS);
|
|
277
|
+
if (updated === null) return { ok: false, reason: `${path} didn't parse as JSON \u2014 not overwriting it. Add the ids by hand.` };
|
|
278
|
+
try {
|
|
279
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
280
|
+
writeFileSync(path, updated + "\n");
|
|
281
|
+
} catch (e) {
|
|
282
|
+
return { ok: false, reason: `Couldn't write ${path}: ${e.message}` };
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
path,
|
|
287
|
+
added: PAIGY_TOOL_IDS.filter((t) => !prevAllow.includes(t)),
|
|
288
|
+
already: PAIGY_TOOL_IDS.filter((t) => prevAllow.includes(t))
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export {
|
|
293
|
+
SERVER_INSTRUCTIONS,
|
|
294
|
+
AwaitReplySchema,
|
|
295
|
+
OnboardSchema,
|
|
296
|
+
PairSchema,
|
|
297
|
+
GetThreadSchema,
|
|
298
|
+
SearchThreadsSchema,
|
|
299
|
+
SetTaskStateToolSchema,
|
|
300
|
+
AnswerCallerQuestionSchema,
|
|
301
|
+
TOOLS,
|
|
302
|
+
openBrowser,
|
|
303
|
+
autoConfigureClients,
|
|
304
|
+
claudeInstallHint,
|
|
305
|
+
ENABLE_COMMAND,
|
|
306
|
+
PAIGY_TOOL_IDS,
|
|
307
|
+
paigyToolsAllowlisted,
|
|
308
|
+
enablePaigyTools
|
|
309
|
+
};
|
package/dist/enable.js
CHANGED