@popoverinstall/cli 0.8.1 → 0.9.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/CHANGELOG.md +159 -78
- package/LICENSE +21 -21
- package/README.md +142 -141
- package/dist/config-command.d.ts +2 -0
- package/dist/config-command.d.ts.map +1 -0
- package/dist/config-command.js +80 -0
- package/dist/config-command.js.map +1 -0
- package/dist/cursor-hooks.d.ts +18 -0
- package/dist/cursor-hooks.d.ts.map +1 -0
- package/dist/cursor-hooks.js +105 -0
- package/dist/cursor-hooks.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -25
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts +10 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +44 -0
- package/dist/keys.js.map +1 -0
- package/dist/repo-scan.d.ts +130 -0
- package/dist/repo-scan.d.ts.map +1 -0
- package/dist/repo-scan.js +281 -0
- package/dist/repo-scan.js.map +1 -0
- package/dist/repos.d.ts +180 -0
- package/dist/repos.d.ts.map +1 -0
- package/dist/repos.js +1002 -0
- package/dist/repos.js.map +1 -0
- package/dist/snapshot.d.ts +35 -0
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +16 -16
- package/dist/snapshot.js.map +1 -1
- package/dist/vaults.d.ts +175 -0
- package/dist/vaults.d.ts.map +1 -0
- package/dist/vaults.js +945 -0
- package/dist/vaults.js.map +1 -0
- package/package.json +3 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +9 -9
- package/plugin/README.md +84 -77
- package/plugin/commands/ask.md +65 -65
- package/plugin/commands/fork.md +119 -119
- package/plugin/commands/repos.md +107 -0
- package/plugin/commands/team.md +60 -60
- package/plugin/commands/tell.md +66 -66
- package/plugin/commands/vault.md +173 -0
- package/plugin/hooks/hooks.json +111 -111
- package/plugin/mcp/index.mjs +410 -0
- package/plugin/scripts/_ipc.mjs +146 -146
- package/plugin/scripts/announce-roster.mjs +141 -141
- package/plugin/scripts/deliver-messages.mjs +77 -77
- package/plugin/scripts/emit-event.mjs +44 -44
- package/plugin/scripts/ensure-daemon.mjs +156 -156
- package/plugin/scripts/roster.mjs +52 -52
- package/plugin/skills/popover/SKILL.md +42 -5
package/plugin/mcp/index.mjs
CHANGED
|
@@ -108,6 +108,156 @@ const TOOLS = [
|
|
|
108
108
|
// Not read-only: this writes into someone else's context and notifies a person.
|
|
109
109
|
annotations: { readOnlyHint: false, openWorldHint: true, title: "Tell a teammate's agent" },
|
|
110
110
|
},
|
|
111
|
+
{
|
|
112
|
+
name: "vault_search",
|
|
113
|
+
description:
|
|
114
|
+
"Search your team's vaults. A vault is a conversation somebody on your team froze " +
|
|
115
|
+
"deliberately and published: it does not expire, it is not a live session, and its " +
|
|
116
|
+
"author does not have to be online — or still at the company — for you to use it. " +
|
|
117
|
+
"This searches the index only. It is free, it runs no model, it resumes nothing, and " +
|
|
118
|
+
"it costs neither you nor anybody else a token, so use it freely. Each result carries " +
|
|
119
|
+
"the questions that vault says it can answer, when it was frozen, who froze it, and " +
|
|
120
|
+
"how much of the code it discussed has changed since — a vault whose files nobody has " +
|
|
121
|
+
"touched is current however old it is, and one whose files have churned is suspect " +
|
|
122
|
+
"even if it is recent. Unlike team_list this is not limited to agents in your repo or " +
|
|
123
|
+
"to people who are working right now: every vault on every team you belong to is " +
|
|
124
|
+
"searchable. ALWAYS call this before vault_ask, and read what comes back: asking is " +
|
|
125
|
+
"the expensive half and this is how you spend it on the right vault.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
query: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description:
|
|
132
|
+
"What you want to know, phrased as a question. Vaults are indexed by the " +
|
|
133
|
+
"questions they can answer rather than by a summary, so a question matches far " +
|
|
134
|
+
"better than keywords do. Omit it to list the most recently frozen vaults.",
|
|
135
|
+
},
|
|
136
|
+
repo: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description:
|
|
139
|
+
"Narrow to one repo label. Display-level filtering only — vaults are visible " +
|
|
140
|
+
"across every repo your team works in, so leave this off unless the user asked " +
|
|
141
|
+
"for one repo specifically.",
|
|
142
|
+
},
|
|
143
|
+
limit: {
|
|
144
|
+
type: "number",
|
|
145
|
+
description: "How many to return. Default 20, max 50.",
|
|
146
|
+
minimum: 1,
|
|
147
|
+
maximum: 50,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
additionalProperties: false,
|
|
151
|
+
},
|
|
152
|
+
annotations: { readOnlyHint: true, openWorldHint: true, title: "Search team vaults" },
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "vault_ask",
|
|
156
|
+
description:
|
|
157
|
+
"Ask one vault one question and wait for the answer. **This costs a full context " +
|
|
158
|
+
"load and real money** — the frozen conversation is resumed in its entirety on THIS " +
|
|
159
|
+
"machine, on your user's account, at roughly $0.50 and up to 90 seconds per call. " +
|
|
160
|
+
"Call vault_search first and ask the one vault that fits; never fan out across " +
|
|
161
|
+
"several to see which answers best, and never call this on a vault you have not seen " +
|
|
162
|
+
"in a search result. The vault answers from its frozen context and has no tools at " +
|
|
163
|
+
"all: it cannot read a file, run a command, or look at the repository as it stands " +
|
|
164
|
+
"today, which is deliberate — it is authoritative about what was decided and why, and " +
|
|
165
|
+
"explicitly unreliable about what the code currently looks like. Report what comes " +
|
|
166
|
+
"back as the vault's claim, attributed and dated, never as a bare fact merged into " +
|
|
167
|
+
"your own reasoning: a vault can hold a hypothesis the team later disproved. Nothing " +
|
|
168
|
+
"you do here reaches the vault's author, changes the vault, or costs them anything.",
|
|
169
|
+
inputSchema: {
|
|
170
|
+
type: "object",
|
|
171
|
+
properties: {
|
|
172
|
+
vault_id: {
|
|
173
|
+
type: "string",
|
|
174
|
+
description: "The `id` of a vault from a vault_search result. Never guess one.",
|
|
175
|
+
},
|
|
176
|
+
question: {
|
|
177
|
+
type: "string",
|
|
178
|
+
description:
|
|
179
|
+
"A self-contained question. The vault has none of your conversation and has " +
|
|
180
|
+
"been frozen for weeks or months, so expand every pronoun, name files by path, " +
|
|
181
|
+
"and ask about decisions and reasoning rather than about the current code.",
|
|
182
|
+
},
|
|
183
|
+
timeout_seconds: {
|
|
184
|
+
type: "number",
|
|
185
|
+
description: "How long to wait for an answer. Default 90, max 300.",
|
|
186
|
+
minimum: 5,
|
|
187
|
+
maximum: 300,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
required: ["vault_id", "question"],
|
|
191
|
+
additionalProperties: false,
|
|
192
|
+
},
|
|
193
|
+
// Read-only: it resumes a copy and writes nothing back. It is not free, which the
|
|
194
|
+
// description says at length, because an annotation cannot say "and it costs money".
|
|
195
|
+
annotations: { readOnlyHint: true, openWorldHint: true, title: "Ask a team vault" },
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "vault_create",
|
|
199
|
+
description:
|
|
200
|
+
"Publish this conversation to your team's vault. This writes: it uploads the whole " +
|
|
201
|
+
"conversation — every message, and the contents of every file read into it — where " +
|
|
202
|
+
"everyone on the team can search it and ask it, permanently. It does not expire, it " +
|
|
203
|
+
"cannot be edited afterwards, and it is not sent to one named person: it goes to the " +
|
|
204
|
+
"team. **A vault is readable by the popover service.** That is the one way it differs " +
|
|
205
|
+
"from /popover:fork, which is end-to-end encrypted and which the service cannot read; " +
|
|
206
|
+
"do not describe a vault as if it made the same promise, and say so plainly if you " +
|
|
207
|
+
"tell the user you have vaulted something. You may call this on your own judgement " +
|
|
208
|
+
"rather than waiting to be asked — the moment worth vaulting is usually one the user " +
|
|
209
|
+
"is too deep in the work to notice, and a conversation nobody vaults is deleted when " +
|
|
210
|
+
"the session ends. But judge it as a real disclosure and not as a cheap call: vault a " +
|
|
211
|
+
"conversation that worked something out the team will want and the repository will " +
|
|
212
|
+
"not record — a decision and the reasons behind it, an approach that was tried and " +
|
|
213
|
+
"rejected, an investigation into why something is the way it is. Do not vault routine " +
|
|
214
|
+
"work, and do not vault the same session twice.",
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {
|
|
218
|
+
title: {
|
|
219
|
+
type: "string",
|
|
220
|
+
description:
|
|
221
|
+
"A short human label, 3 to 120 characters. It is what a teammate reads on a card " +
|
|
222
|
+
"in a grid, so name the subject rather than the activity: 'Why the repo key does " +
|
|
223
|
+
"not gate vaults', not 'vault work'.",
|
|
224
|
+
},
|
|
225
|
+
answers: {
|
|
226
|
+
type: "array",
|
|
227
|
+
items: { type: "string" },
|
|
228
|
+
description:
|
|
229
|
+
"Questions this conversation can answer — NOT a summary of what happened. This " +
|
|
230
|
+
"is the retrieval surface: the query that finds this vault later will itself be " +
|
|
231
|
+
"a question, and matching a question against a summary retrieves badly where " +
|
|
232
|
+
"question against question does not. Write each one the way a teammate would " +
|
|
233
|
+
"type it months from now, in full: 'Why did we reject the repo key as a vault " +
|
|
234
|
+
"credential?', not 'discussed encryption'. Three to six is right; each is 8 to " +
|
|
235
|
+
"200 characters and there can be at most 12. This is the field that decides " +
|
|
236
|
+
"whether the vault is ever found again, and it cannot be edited afterwards.",
|
|
237
|
+
minItems: 1,
|
|
238
|
+
maxItems: 12,
|
|
239
|
+
},
|
|
240
|
+
name: {
|
|
241
|
+
type: "string",
|
|
242
|
+
description:
|
|
243
|
+
"Optional slug, lowercase letters, digits and hyphens. It appears in URLs and in " +
|
|
244
|
+
"the command teammates type. Derived from the title when omitted, which is " +
|
|
245
|
+
"usually right — pass one only when the title makes an awkward slug or when a " +
|
|
246
|
+
"name collision has been reported.",
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
required: ["title", "answers"],
|
|
250
|
+
additionalProperties: false,
|
|
251
|
+
},
|
|
252
|
+
// Not read-only, and the only vault tool that is not. This publishes a conversation to a
|
|
253
|
+
// whole team, permanently, and there is no edit and no undo — `archive` retires a vault
|
|
254
|
+
// without erasing the record that it was made.
|
|
255
|
+
annotations: {
|
|
256
|
+
readOnlyHint: false,
|
|
257
|
+
openWorldHint: true,
|
|
258
|
+
title: "Publish this conversation to the team",
|
|
259
|
+
},
|
|
260
|
+
},
|
|
111
261
|
];
|
|
112
262
|
|
|
113
263
|
// ---------------------------------------------------------------------------
|
|
@@ -225,7 +375,259 @@ async function callTeamTell(args) {
|
|
|
225
375
|
);
|
|
226
376
|
}
|
|
227
377
|
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// Vaults — the cheap half and the costly half, deliberately two tools
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
async function callVaultSearch(args) {
|
|
383
|
+
const query = typeof args?.query === "string" ? args.query.trim() : "";
|
|
384
|
+
const repo = typeof args?.repo === "string" ? args.repo.trim() : "";
|
|
385
|
+
const limit = clamp(Number(args?.limit) || 20, 1, 50);
|
|
386
|
+
|
|
387
|
+
const reply = await request(
|
|
388
|
+
{
|
|
389
|
+
t: "vaults",
|
|
390
|
+
id: rpcId(),
|
|
391
|
+
req: {
|
|
392
|
+
limit,
|
|
393
|
+
...(query ? { query } : {}),
|
|
394
|
+
...(repo ? { repo } : {}),
|
|
395
|
+
},
|
|
396
|
+
// Not a filter. The daemon uses it to diff each vault's files against the tree in front
|
|
397
|
+
// of you, which is what the staleness figure in each result is.
|
|
398
|
+
cwd: process.cwd(),
|
|
399
|
+
},
|
|
400
|
+
{ timeoutMs: 20000 },
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
if (!reply) return errorText(daemonDownMessage());
|
|
404
|
+
if (reply.t === "error") return errorText(explain(reply));
|
|
405
|
+
if (reply.t !== "vaults.ok") return errorText("The popover daemon returned an unexpected response.");
|
|
406
|
+
|
|
407
|
+
const vaults = reply.vaults ?? [];
|
|
408
|
+
if (vaults.length === 0) {
|
|
409
|
+
return text(
|
|
410
|
+
query
|
|
411
|
+
? `No vault on your teams answers anything like "${query}". Nothing was spent looking. ` +
|
|
412
|
+
"Do not call vault_ask — there is no vault to ask."
|
|
413
|
+
: "Your teams have not published any vaults yet.",
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const lines = vaults.map((vault, index) => {
|
|
418
|
+
const facts = [
|
|
419
|
+
vault.authorLabel,
|
|
420
|
+
`frozen ${friendlyDate(vault.frozenAt)}`,
|
|
421
|
+
vault.repoLabel ? `repo ${vault.repoLabel}` : null,
|
|
422
|
+
vault.noteCount > 0 ? `${vault.noteCount} note${vault.noteCount === 1 ? "" : "s"}` : null,
|
|
423
|
+
describeStaleness(vault.staleness),
|
|
424
|
+
].filter(Boolean);
|
|
425
|
+
|
|
426
|
+
const answers = (vault.answers ?? []).map((a) => ` - ${a}`).join("\n");
|
|
427
|
+
return (
|
|
428
|
+
`${index + 1}. ${vault.title} (${vault.name})\n` +
|
|
429
|
+
` id: ${vault.id}\n` +
|
|
430
|
+
` ${facts.join(" · ")}\n` +
|
|
431
|
+
(answers ? ` Says it can answer:\n${answers}\n` : "")
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
return text(
|
|
436
|
+
`${vaults.length} vault${vaults.length === 1 ? "" : "s"} on your teams:\n\n` +
|
|
437
|
+
lines.join("\n") +
|
|
438
|
+
"\nThis search cost nothing. Asking one of these costs a full context load and real " +
|
|
439
|
+
"money on your user's account, so pick the single best fit — or none, if none of them " +
|
|
440
|
+
"claims to answer the question.",
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function callVaultAsk(args) {
|
|
445
|
+
const vaultId = typeof args?.vault_id === "string" ? args.vault_id.trim() : "";
|
|
446
|
+
const question = typeof args?.question === "string" ? args.question.trim() : "";
|
|
447
|
+
|
|
448
|
+
if (!vaultId) return errorText("`vault_id` is required — call vault_search to find one.");
|
|
449
|
+
if (!question) return errorText("`question` is required.");
|
|
450
|
+
|
|
451
|
+
const timeoutSeconds = clamp(Number(args?.timeout_seconds) || 90, 5, 300);
|
|
452
|
+
|
|
453
|
+
const reply = await request(
|
|
454
|
+
{
|
|
455
|
+
t: "vaultAsk",
|
|
456
|
+
id: rpcId(),
|
|
457
|
+
req: {
|
|
458
|
+
vaultId,
|
|
459
|
+
question,
|
|
460
|
+
timeoutSeconds,
|
|
461
|
+
// Carries the per-session budget. Without it the daemon cannot tell one agent asking
|
|
462
|
+
// six vaults from six agents asking one each.
|
|
463
|
+
...(callerSessionId() ? { fromSessionId: callerSessionId() } : {}),
|
|
464
|
+
},
|
|
465
|
+
cwd: process.cwd(),
|
|
466
|
+
},
|
|
467
|
+
// Outlast the daemon's own wait so its specific error wins over a generic timeout.
|
|
468
|
+
{ timeoutMs: timeoutSeconds * 1000 + 15000 },
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
if (!reply) return errorText(daemonDownMessage());
|
|
472
|
+
if (reply.t === "error") return errorText(explain(reply));
|
|
473
|
+
if (reply.t !== "vaultAsk.ok") return errorText("The popover daemon returned an unexpected response.");
|
|
474
|
+
|
|
475
|
+
const { answer } = reply;
|
|
476
|
+
// Attribution first, and not optional. A human reading a vault's answer supplies the
|
|
477
|
+
// skepticism unprompted; a model will not unless the format forces it, and a vault can hold
|
|
478
|
+
// a hypothesis the team later disproved.
|
|
479
|
+
const at = answer.gitSha ? ` at commit ${answer.gitSha}` : "";
|
|
480
|
+
const cost =
|
|
481
|
+
answer.costUsd != null && answer.durationMs != null
|
|
482
|
+
? ` Answered in ${(answer.durationMs / 1000).toFixed(1)}s, cost $${answer.costUsd.toFixed(3)} on your own account.`
|
|
483
|
+
: "";
|
|
484
|
+
|
|
485
|
+
return text(
|
|
486
|
+
`The "${answer.title}" vault, frozen ${friendlyDate(answer.frozenAt)}${at}, says:\n\n` +
|
|
487
|
+
`${answer.answer}\n\n` +
|
|
488
|
+
`_(That is a frozen conversation, not the current code. Treat it as authoritative ` +
|
|
489
|
+
`about what was decided and why, and check anything it says about how the repository ` +
|
|
490
|
+
`looks today. Attribute it to the vault when you pass it on.${cost})_`,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Publish this conversation. The one tool here that writes something permanent.
|
|
496
|
+
*
|
|
497
|
+
* ## Why this is an IPC verb and not `popover vault create`
|
|
498
|
+
*
|
|
499
|
+
* The pipeline it drives is the CLI's, tested and unchanged — find the transcript, settle it,
|
|
500
|
+
* sanitize, collect touched paths, upload. The obvious implementation is therefore to spawn
|
|
501
|
+
* `popover vault create --json` and relay what it prints. It is not what happens, and the
|
|
502
|
+
* reason is Windows.
|
|
503
|
+
*
|
|
504
|
+
* `popover` on Windows is a `.cmd` shim, and Node refuses to spawn a `.cmd` without a shell.
|
|
505
|
+
* With `shell: true` the arguments are pasted into a `cmd.exe` command line, where `&`, `|`,
|
|
506
|
+
* `^` and `>` are operators — and every argument here is written by a model: a title, a list
|
|
507
|
+
* of questions, a slug. That is untrusted-input-into-a-shell, on the machine this is being
|
|
508
|
+
* built on, to save a wire format. The escape hatches are worse than the problem: locating the
|
|
509
|
+
* CLI's real `.js` inside the shim is guesswork about npm's layout, and hand-quoting for
|
|
510
|
+
* `cmd.exe` is a thing to get wrong quietly.
|
|
511
|
+
*
|
|
512
|
+
* So it goes over the pipe the other four tools already use. The daemon needs nothing new to
|
|
513
|
+
* be reachable, holds the device token already, and already POSTs to the vault routes; the
|
|
514
|
+
* shared decisions — `planVault`, `collectToolPaths`, `packBody` — moved into
|
|
515
|
+
* @popoverinstall/shared so the terminal and the daemon build byte-identical payloads. No
|
|
516
|
+
* child process, nothing to quote, and the same code path on every platform.
|
|
517
|
+
*/
|
|
518
|
+
async function callVaultCreate(args) {
|
|
519
|
+
const title = typeof args?.title === "string" ? args.title.trim() : "";
|
|
520
|
+
const name = typeof args?.name === "string" ? args.name.trim() : "";
|
|
521
|
+
const answers = Array.isArray(args?.answers)
|
|
522
|
+
? args.answers.filter((a) => typeof a === "string" && a.trim()).map((a) => a.trim())
|
|
523
|
+
: [];
|
|
524
|
+
|
|
525
|
+
if (!title) return errorText("`title` is required — a vault needs a short human label.");
|
|
526
|
+
if (answers.length === 0) {
|
|
527
|
+
return errorText(
|
|
528
|
+
"`answers` is required, and it holds questions this conversation can answer rather " +
|
|
529
|
+
"than a summary of it. A vault with no questions is one nobody will ever find.",
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Never from the model. The session being frozen is the one this process belongs to, and a
|
|
534
|
+
// caller that could name any session could publish a conversation it was never part of.
|
|
535
|
+
const sessionId = callerSessionId();
|
|
536
|
+
if (!sessionId) {
|
|
537
|
+
return errorText(
|
|
538
|
+
"There is no Claude Code session here to vault. This tool freezes the conversation it " +
|
|
539
|
+
"is running inside, and outside a session there is nothing to freeze.",
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const reply = await request(
|
|
544
|
+
{
|
|
545
|
+
t: "vaultCreate",
|
|
546
|
+
id: rpcId(),
|
|
547
|
+
req: { sessionId, title, answers, ...(name ? { name } : {}) },
|
|
548
|
+
// Where git runs: it decides the recorded commit and the uncommitted half of the
|
|
549
|
+
// touched-path list. Not a filter, and never a permission.
|
|
550
|
+
cwd: process.cwd(),
|
|
551
|
+
},
|
|
552
|
+
// Generous: the daemon waits for the transcript to stop moving, runs git, and then
|
|
553
|
+
// uploads a whole gzipped conversation, which is megabytes rather than bytes.
|
|
554
|
+
{ timeoutMs: 60000 },
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
// Not `daemonDownMessage()`. Silence here has a second cause the other tools do not have:
|
|
558
|
+
// a daemon older than this plugin does not know the verb, and drops a request its schema
|
|
559
|
+
// refuses without answering it. Telling somebody to start a daemon that is already running
|
|
560
|
+
// sends them after the wrong problem, so both possibilities are named and neither is
|
|
561
|
+
// asserted. Nothing was published either way — the upload is the last thing that happens.
|
|
562
|
+
if (!reply) {
|
|
563
|
+
return errorText(
|
|
564
|
+
"The popover daemon did not answer, so nothing was published. Either it is not " +
|
|
565
|
+
"running on this machine, or it is older than this plugin and does not know how to " +
|
|
566
|
+
"publish a vault. Run `popover doctor` to find out which, and `popover update` if it " +
|
|
567
|
+
"is the second.",
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (reply.t === "error") return errorText(explain(reply));
|
|
571
|
+
if (reply.t !== "vaultCreate.ok") {
|
|
572
|
+
return errorText("The popover daemon returned an unexpected response.");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const vault = reply.vault;
|
|
576
|
+
const answered = (vault.answers ?? []).map((a) => ` - ${a}`).join("\n");
|
|
577
|
+
const sha = vault.gitSha ? ` at commit ${vault.gitSha.slice(0, 7)}` : "";
|
|
578
|
+
|
|
579
|
+
return text(
|
|
580
|
+
`Published "${vault.title}" to your team as \`${vault.name}\`.\n\n` +
|
|
581
|
+
`${vault.entryCount} messages, frozen at the last completed turn ` +
|
|
582
|
+
`(${friendlyDate(vault.frozenAt)}${sha}). Nothing said from here is in it.\n\n` +
|
|
583
|
+
`It says it can answer:\n${answered}\n\n` +
|
|
584
|
+
`Anyone on the team can ask it, on their own machine and at their own cost:\n` +
|
|
585
|
+
` popover vault ask ${vault.name} "…"\n\n` +
|
|
586
|
+
// Said every time, in the same register the CLI's confirmation uses. Anyone who has
|
|
587
|
+
// internalized /popover:fork being end-to-end will assume this is too, and it is not.
|
|
588
|
+
`Tell the user you did this, and tell them plainly that a vault is readable by the ` +
|
|
589
|
+
`popover service — a fork is end-to-end and this is not. It does not expire and cannot ` +
|
|
590
|
+
`be edited; \`popover vault archive ${vault.name}\` retires it without erasing the ` +
|
|
591
|
+
`record that it was made.`,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** ISO timestamp to something a person reads, without pulling in a formatter. */
|
|
596
|
+
function friendlyDate(iso) {
|
|
597
|
+
const at = new Date(iso);
|
|
598
|
+
if (Number.isNaN(at.getTime())) return iso;
|
|
599
|
+
const months = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ");
|
|
600
|
+
return `${at.getUTCDate()} ${months[at.getUTCMonth()]} ${at.getUTCFullYear()}`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* The staleness figure in words.
|
|
605
|
+
*
|
|
606
|
+
* Absent when the daemon had no working tree to diff against, or the vault recorded no paths.
|
|
607
|
+
* Silence is correct there: "0% changed" and "we could not check" are different claims, and
|
|
608
|
+
* only one of them is evidence that the vault is still current.
|
|
609
|
+
*/
|
|
610
|
+
function describeStaleness(staleness) {
|
|
611
|
+
if (typeof staleness !== "number") return null;
|
|
612
|
+
if (staleness === 0) return "none of its files have changed since";
|
|
613
|
+
return `${Math.round(staleness * 100)}% of its files have changed since`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* The daemon's own words for a failure, plus whatever it said to do about it.
|
|
618
|
+
*
|
|
619
|
+
* `hints` arrives on refusals that have an answer — a title too short, a name already taken,
|
|
620
|
+
* a conversation over the size limit — and is relayed verbatim rather than summarised. Those
|
|
621
|
+
* sentences are the difference between a second attempt that works and one that fails
|
|
622
|
+
* identically, and they are written where the constraint is, which is not here.
|
|
623
|
+
*/
|
|
228
624
|
function explain(errorReply) {
|
|
625
|
+
const hints = Array.isArray(errorReply.hints) ? errorReply.hints.filter(Boolean) : [];
|
|
626
|
+
const said = describe(errorReply);
|
|
627
|
+
return hints.length ? `${said}\n\n${hints.join("\n")}` : said;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function describe(errorReply) {
|
|
229
631
|
switch (errorReply.code) {
|
|
230
632
|
// The daemon knows whether credentials are missing or merely unusable; relaying its
|
|
231
633
|
// message avoids telling an already signed-in user to sign in again.
|
|
@@ -236,6 +638,11 @@ function explain(errorReply) {
|
|
|
236
638
|
case "fork_failed":
|
|
237
639
|
case "bad_request":
|
|
238
640
|
return errorReply.message;
|
|
641
|
+
// A vault that is gone is gone: they are never edited and never expire, so this means the
|
|
642
|
+
// id was wrong or it has been archived. Retrying it, or trying a neighbouring id, cannot
|
|
643
|
+
// help — searching again can.
|
|
644
|
+
case "vault_not_found":
|
|
645
|
+
return `${errorReply.message} Call vault_search again rather than trying another id.`;
|
|
239
646
|
case "cloud_unreachable":
|
|
240
647
|
return `Could not reach the popover backend: ${errorReply.message}`;
|
|
241
648
|
default:
|
|
@@ -316,6 +723,9 @@ async function handle(message) {
|
|
|
316
723
|
if (name === "team_list") return reply(id, await callTeamList());
|
|
317
724
|
if (name === "team_ask") return reply(id, await callTeamAsk(params?.arguments ?? {}));
|
|
318
725
|
if (name === "team_tell") return reply(id, await callTeamTell(params?.arguments ?? {}));
|
|
726
|
+
if (name === "vault_search") return reply(id, await callVaultSearch(params?.arguments ?? {}));
|
|
727
|
+
if (name === "vault_ask") return reply(id, await callVaultAsk(params?.arguments ?? {}));
|
|
728
|
+
if (name === "vault_create") return reply(id, await callVaultCreate(params?.arguments ?? {}));
|
|
319
729
|
return replyError(id, -32602, `Unknown tool: ${name}`);
|
|
320
730
|
} catch (err) {
|
|
321
731
|
// A thrown handler must not kill the server; report it as a failed tool call.
|