@skyelight/mcp 0.4.1 → 0.5.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 +78 -0
- package/README.md +64 -48
- package/package.json +3 -11
- package/src/brand.d.ts +9 -0
- package/src/brand.js +64 -0
- package/src/cli.js +355 -82
- package/src/client.js +9 -0
- package/src/config.js +5 -7
- package/src/index.js +11 -1
- package/src/insights.d.ts +15 -0
- package/src/insights.js +688 -0
- package/src/install.js +59 -2
- package/src/server.js +11 -2
- package/src/theme.js +120 -0
- package/src/tools.d.ts +10 -0
- package/src/tools.js +236 -14
- package/src/version.js +1 -1
package/src/insights.js
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The launch tools: reading a project's history the way an agent needs it.
|
|
3
|
+
*
|
|
4
|
+
* - `project_review`: where a project landed, for a wrap-up or hand-off.
|
|
5
|
+
* - `whats_new`: what changed since you last looked.
|
|
6
|
+
* - `find_by_source`: feedback by the file and component that rendered it.
|
|
7
|
+
* - `find_similar` and `merge_items`: has this been reported, and folding
|
|
8
|
+
* duplicates into one.
|
|
9
|
+
* - `decision_log` and `save_rule`: what the team keeps deciding, and
|
|
10
|
+
* keeping it as a project rule.
|
|
11
|
+
*
|
|
12
|
+
* Same contract as `tools.js`: each result is prose with the numbers and
|
|
13
|
+
* quotes already in it, every claim carrying the id it came from, and the
|
|
14
|
+
* structured payload attached beside it. The prose is complete on its own,
|
|
15
|
+
* because many clients only show a model the text.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export function insightToolDefinitions() {
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
name: "project_review",
|
|
22
|
+
description:
|
|
23
|
+
"Everything needed to write an accurate project review or hand-off document: " +
|
|
24
|
+
"the shape of the work, every resolved thread and how it ended, what was " +
|
|
25
|
+
"deferred and why, what is still open, and the longest conversations quoted " +
|
|
26
|
+
"in full, which is where decisions were made. Every entry carries its item id, " +
|
|
27
|
+
"so each claim in your document can be traced back to its thread. Quote; do " +
|
|
28
|
+
"not invent outcomes the threads do not state.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
projectId: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Project to review. Optional if a project is bound.",
|
|
35
|
+
},
|
|
36
|
+
section: {
|
|
37
|
+
type: "string",
|
|
38
|
+
enum: ["resolved", "deferred", "open", "discussions"],
|
|
39
|
+
description:
|
|
40
|
+
"Omit for the overview (all counts, first rows of each section). Name one to read it in full, a page at a time.",
|
|
41
|
+
},
|
|
42
|
+
offset: {
|
|
43
|
+
type: "number",
|
|
44
|
+
description:
|
|
45
|
+
"With section: where to start, from the previous result's 'more' line.",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "whats_new",
|
|
52
|
+
description:
|
|
53
|
+
"What changed on a project since you last looked: new threads, new replies " +
|
|
54
|
+
"grouped by thread, what was resolved or deferred, and what was assigned to " +
|
|
55
|
+
"you or mentioned you. Call it at the start of a session. Without `since` it " +
|
|
56
|
+
"reads from your bookmark and moves it forward; with `since` it reads that " +
|
|
57
|
+
"window and leaves the bookmark alone.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
projectId: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Optional if a project is bound.",
|
|
64
|
+
},
|
|
65
|
+
since: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description:
|
|
68
|
+
'Optional. A date or time ("2026-09-01", an ISO time) to read from instead of your bookmark.',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "find_by_source",
|
|
75
|
+
description:
|
|
76
|
+
"Feedback by the code that rendered it, from the file and line the Skyelight " +
|
|
77
|
+
"build plugin stamps on each element. Pass a file (a path or just its name) or " +
|
|
78
|
+
"a component to see every thread about it before you edit it; pass neither to " +
|
|
79
|
+
"get the hot spots, every stamped file ranked by outstanding feedback.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
projectId: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Optional if a project is bound.",
|
|
86
|
+
},
|
|
87
|
+
file: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description:
|
|
90
|
+
'A source path or file name, e.g. "src/components/PricingTable.tsx" or "PricingTable.tsx".',
|
|
91
|
+
},
|
|
92
|
+
line: {
|
|
93
|
+
type: "number",
|
|
94
|
+
description: "Optional. Threads nearest this line come first.",
|
|
95
|
+
},
|
|
96
|
+
component: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: 'A component name, e.g. "PricingTable".',
|
|
99
|
+
},
|
|
100
|
+
status: { type: "string", enum: ["open", "deferred", "resolved"] },
|
|
101
|
+
limit: { type: "number" },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "find_similar",
|
|
107
|
+
description:
|
|
108
|
+
"Has this already been reported? Pass the text you are about to report (and " +
|
|
109
|
+
"the page, if you know it) before create_item, or an itemId to find what an " +
|
|
110
|
+
"existing thread duplicates. Returns likely matches with why each matched: " +
|
|
111
|
+
"shared words, same page, same element, same source file. If two are the same " +
|
|
112
|
+
"problem, merge_items folds one into the other.",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
projectId: {
|
|
117
|
+
type: "string",
|
|
118
|
+
description: "Optional if a project is bound or itemId is given.",
|
|
119
|
+
},
|
|
120
|
+
text: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "What someone is about to report.",
|
|
123
|
+
},
|
|
124
|
+
itemId: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description: "Instead of text: find duplicates of this item.",
|
|
127
|
+
},
|
|
128
|
+
page: {
|
|
129
|
+
type: "string",
|
|
130
|
+
description: 'Optional. A page path or URL, e.g. "/checkout".',
|
|
131
|
+
},
|
|
132
|
+
selector: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "Optional. CSS selector of the element.",
|
|
135
|
+
},
|
|
136
|
+
includeResolved: {
|
|
137
|
+
type: "boolean",
|
|
138
|
+
description:
|
|
139
|
+
"Default true: a resolved match may mean it came back. false to skip them.",
|
|
140
|
+
},
|
|
141
|
+
limit: { type: "number", description: "Max matches (default 5)." },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "merge_items",
|
|
147
|
+
description:
|
|
148
|
+
"Fold duplicate items into one, the way merging works in the app: the " +
|
|
149
|
+
"duplicates leave the list and count as extra reports on the item they were " +
|
|
150
|
+
"merged into. Use it only when find_similar and the threads themselves show " +
|
|
151
|
+
"they are the same problem. Owners, admins and collaborators can merge.",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
itemId: {
|
|
156
|
+
type: "string",
|
|
157
|
+
description: "The item to keep. The others fold into it.",
|
|
158
|
+
},
|
|
159
|
+
duplicateIds: {
|
|
160
|
+
type: "array",
|
|
161
|
+
items: { type: "string" },
|
|
162
|
+
description: "Items that report the same problem.",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
required: ["itemId", "duplicateIds"],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "decision_log",
|
|
170
|
+
description:
|
|
171
|
+
"The decisions a project made in its threads, quoted: every thread resolved or " +
|
|
172
|
+
"deferred after a discussion, with the conversation, plus types people " +
|
|
173
|
+
"corrected and where outcomes keep recurring. Also returns the project's " +
|
|
174
|
+
"existing rules, so you can tell what is new. Use it to write a decision log, " +
|
|
175
|
+
"or to find rules worth keeping with save_rule.",
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: {
|
|
179
|
+
projectId: {
|
|
180
|
+
type: "string",
|
|
181
|
+
description: "Optional if a project is bound.",
|
|
182
|
+
},
|
|
183
|
+
since: {
|
|
184
|
+
type: "string",
|
|
185
|
+
description:
|
|
186
|
+
'Optional. Only decisions after this date ("2026-09-01").',
|
|
187
|
+
},
|
|
188
|
+
limit: {
|
|
189
|
+
type: "number",
|
|
190
|
+
description: "Decisions per page (default 15, at most 30).",
|
|
191
|
+
},
|
|
192
|
+
offset: {
|
|
193
|
+
type: "number",
|
|
194
|
+
description:
|
|
195
|
+
"Where to start, from the previous result's 'more' line.",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "save_rule",
|
|
202
|
+
description:
|
|
203
|
+
'Keep a rule the team agreed on ("sentence case in every button", "the brand ' +
|
|
204
|
+
"gray stays, do not raise contrast bugs on it\") in the project's context, " +
|
|
205
|
+
"under Rules learned. The classifier reads that context, and every agent reads " +
|
|
206
|
+
"it through decision_log, so it shapes future threads. Cite the threads it came " +
|
|
207
|
+
"from. Workspace owners and admins only; confirm with the person before saving.",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
projectId: {
|
|
212
|
+
type: "string",
|
|
213
|
+
description: "Optional if a project is bound.",
|
|
214
|
+
},
|
|
215
|
+
rule: {
|
|
216
|
+
type: "string",
|
|
217
|
+
description: "The rule, in one plain sentence.",
|
|
218
|
+
},
|
|
219
|
+
sourceItemIds: {
|
|
220
|
+
type: "array",
|
|
221
|
+
items: { type: "string" },
|
|
222
|
+
description: "Items the rule came from.",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
required: ["rule"],
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export const INSIGHT_TOOLS = new Set(
|
|
232
|
+
insightToolDefinitions().map((t) => t.name),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
function plural(n, one, many) {
|
|
236
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function date(ms) {
|
|
240
|
+
return ms ? new Date(ms).toISOString().slice(0, 10) : "unknown date";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function flaggedLine(i) {
|
|
244
|
+
// A machine summary is not the reporter's words; say so once per row.
|
|
245
|
+
return i.lineSource === "summary" ? `${i.line} (summary)` : `"${i.line}"`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function row(i, extra = []) {
|
|
249
|
+
const bits = [i.typeLabel ?? i.type ?? "Unclassified", i.page, ...extra];
|
|
250
|
+
if (i.replyCount) bits.push(plural(i.replyCount, "reply", "replies"));
|
|
251
|
+
if (i.reportCount > 1) bits.push(`${i.reportCount} reports`);
|
|
252
|
+
if (i.source) {
|
|
253
|
+
bits.push(`${i.source.file}${i.source.line ? `:${i.source.line}` : ""}`);
|
|
254
|
+
}
|
|
255
|
+
return `- ${flaggedLine(i)}\n ${bits.join(" · ")} (id ${i.id})`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function quote(m) {
|
|
259
|
+
return ` > ${m.author}${m.via ? ` (with ${m.via})` : ""}, ${date(m.at)}: ${m.text}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function cappedNote(result) {
|
|
263
|
+
return result.capped
|
|
264
|
+
? "This project is larger than one call reads (2,000 most recent pins), so counts are a floor."
|
|
265
|
+
: null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function renderProjectReview(r) {
|
|
269
|
+
const out = [];
|
|
270
|
+
const c = r.counts;
|
|
271
|
+
const only = r.section; // one section, a page at a time; null = overview
|
|
272
|
+
const pages = r.pages;
|
|
273
|
+
const more = (name) => {
|
|
274
|
+
const p = pages[name];
|
|
275
|
+
return p.nextOffset === null
|
|
276
|
+
? null
|
|
277
|
+
: ` (${p.total - p.nextOffset} more: call project_review with section "${name}" and offset ${p.nextOffset})`;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
out.push(
|
|
281
|
+
only
|
|
282
|
+
? `# ${r.project.name}: ${only}, from ${r.offset + 1}`
|
|
283
|
+
: `# ${r.project.name}: project review data`,
|
|
284
|
+
);
|
|
285
|
+
out.push("");
|
|
286
|
+
out.push(
|
|
287
|
+
`${plural(c.threads, "thread", "threads")} over ${plural(r.span.days, "day", "days")} ` +
|
|
288
|
+
`(${date(r.span.firstAt)} to ${date(r.span.lastAt)}): ${c.resolved} resolved, ` +
|
|
289
|
+
`${c.deferred} deferred, ${c.open} open. ${plural(c.replies, "reply", "replies")} ` +
|
|
290
|
+
`from ${plural(c.people, "person", "people")}.` +
|
|
291
|
+
(c.mergedDuplicates
|
|
292
|
+
? ` ${plural(c.mergedDuplicates, "duplicate was", "duplicates were")} merged.`
|
|
293
|
+
: "") +
|
|
294
|
+
(c.retyped ? ` ${c.retyped} retyped by hand.` : ""),
|
|
295
|
+
);
|
|
296
|
+
const note = cappedNote(r);
|
|
297
|
+
if (note) out.push(note);
|
|
298
|
+
|
|
299
|
+
if (!only) {
|
|
300
|
+
if (r.byType.length) {
|
|
301
|
+
out.push("");
|
|
302
|
+
out.push("## By type");
|
|
303
|
+
for (const t of r.byType) {
|
|
304
|
+
out.push(
|
|
305
|
+
`- ${t.key}: ${t.total} (${t.resolved} resolved, ${t.deferred} deferred, ${t.open} open)`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (r.byPage.length) {
|
|
310
|
+
out.push("");
|
|
311
|
+
out.push("## Busiest pages");
|
|
312
|
+
for (const p of r.byPage.slice(0, 10)) {
|
|
313
|
+
out.push(`- ${p.key}: ${p.total} (${p.open} open)`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (r.people.length) {
|
|
317
|
+
out.push("");
|
|
318
|
+
out.push("## Who took part");
|
|
319
|
+
for (const p of r.people.slice(0, 10)) {
|
|
320
|
+
out.push(
|
|
321
|
+
`- ${p.name}: ${plural(p.threads, "thread", "threads")}, ${plural(p.replies, "reply", "replies")}`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!only || only === "resolved") {
|
|
328
|
+
out.push("");
|
|
329
|
+
out.push(`## Resolved (${c.resolved}, newest first)`);
|
|
330
|
+
if (!r.resolved.length) out.push("Nothing resolved.");
|
|
331
|
+
for (const i of r.resolved) {
|
|
332
|
+
out.push(
|
|
333
|
+
row(i, [
|
|
334
|
+
`resolved ${date(i.resolvedAt)}${i.resolvedBy ? ` by ${i.resolvedBy}` : ""}`,
|
|
335
|
+
]),
|
|
336
|
+
);
|
|
337
|
+
if (i.lastWord) out.push(quote(i.lastWord));
|
|
338
|
+
}
|
|
339
|
+
const m = more("resolved");
|
|
340
|
+
if (m) out.push(m);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!only || only === "deferred") {
|
|
344
|
+
out.push("");
|
|
345
|
+
out.push(`## Deferred (${c.deferred})`);
|
|
346
|
+
if (!r.deferred.length) out.push("Nothing deferred.");
|
|
347
|
+
for (const i of r.deferred) {
|
|
348
|
+
out.push(
|
|
349
|
+
row(i, [
|
|
350
|
+
`deferred ${date(i.deferredAt)}${i.deferredBy ? ` by ${i.deferredBy}` : ""}`,
|
|
351
|
+
]),
|
|
352
|
+
);
|
|
353
|
+
out.push(
|
|
354
|
+
i.reason ? quote(i.reason) : " (no reason written on the thread)",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const m = more("deferred");
|
|
358
|
+
if (m) out.push(m);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!only || only === "open") {
|
|
362
|
+
out.push("");
|
|
363
|
+
out.push(`## Still open (${c.open}, oldest first)`);
|
|
364
|
+
if (!r.open.length) out.push("Nothing open.");
|
|
365
|
+
for (const i of r.open) {
|
|
366
|
+
out.push(
|
|
367
|
+
row(i, [
|
|
368
|
+
`${plural(i.ageDays, "day", "days")} old`,
|
|
369
|
+
i.assignee ? `assigned to ${i.assignee}` : "unassigned",
|
|
370
|
+
]),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
const m = more("open");
|
|
374
|
+
if (m) out.push(m);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if ((!only || only === "discussions") && pages.discussions.total) {
|
|
378
|
+
out.push("");
|
|
379
|
+
out.push(
|
|
380
|
+
`## The longest conversations (${pages.discussions.total}; where decisions were made)`,
|
|
381
|
+
);
|
|
382
|
+
for (const d of r.discussions) {
|
|
383
|
+
out.push(row(d, [d.status]));
|
|
384
|
+
out.push(quote(d.opening));
|
|
385
|
+
if (d.omittedReplies) {
|
|
386
|
+
out.push(` (${d.omittedReplies} earlier replies not shown)`);
|
|
387
|
+
}
|
|
388
|
+
for (const m of d.replies) out.push(quote(m));
|
|
389
|
+
}
|
|
390
|
+
const m = more("discussions");
|
|
391
|
+
if (m) out.push(m);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
out.push("");
|
|
395
|
+
out.push(
|
|
396
|
+
"Write the review from these facts. Read the remaining pages of any section you summarize before stating totals from it. Cite item ids, quote rather than paraphrase outcomes, and say when a thread does not state why it ended.",
|
|
397
|
+
);
|
|
398
|
+
return out.join("\n");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function renderWhatsNew(r) {
|
|
402
|
+
const c = r.counts;
|
|
403
|
+
const out = [];
|
|
404
|
+
const window = `since ${new Date(r.since).toISOString().replace("T", " ").slice(0, 16)} UTC`;
|
|
405
|
+
const total =
|
|
406
|
+
c.newThreads +
|
|
407
|
+
c.newReplies +
|
|
408
|
+
c.resolved +
|
|
409
|
+
c.deferred +
|
|
410
|
+
c.assignedToYou +
|
|
411
|
+
c.mentionsYou;
|
|
412
|
+
out.push(
|
|
413
|
+
`${r.project.name}, ${window}${r.firstBriefing ? " (your first briefing, so the last 7 days)" : ""}:`,
|
|
414
|
+
);
|
|
415
|
+
if (total === 0) {
|
|
416
|
+
out.push("Nothing new.");
|
|
417
|
+
return out.join("\n");
|
|
418
|
+
}
|
|
419
|
+
out.push(
|
|
420
|
+
`${plural(c.newThreads, "new thread", "new threads")}, ${plural(c.newReplies, "new reply", "new replies")} ` +
|
|
421
|
+
`on ${plural(c.threadsWithNewReplies, "thread", "threads")}, ${c.resolved} resolved, ${c.deferred} deferred.`,
|
|
422
|
+
);
|
|
423
|
+
const note = cappedNote(r);
|
|
424
|
+
if (note) out.push(note);
|
|
425
|
+
|
|
426
|
+
if (r.assignedToYou.length) {
|
|
427
|
+
out.push("");
|
|
428
|
+
out.push("## Assigned to you");
|
|
429
|
+
for (const i of r.assignedToYou) {
|
|
430
|
+
out.push(
|
|
431
|
+
row(i, [i.assignedBy ? `by ${i.assignedBy}` : ""].filter(Boolean)),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (r.mentionsYou.length) {
|
|
436
|
+
out.push("");
|
|
437
|
+
out.push("## Mentioned you");
|
|
438
|
+
for (const m of r.mentionsYou) {
|
|
439
|
+
out.push(
|
|
440
|
+
`- on ${m.thread ? flaggedLine(m.thread) : "a thread"} (id ${m.threadId})`,
|
|
441
|
+
);
|
|
442
|
+
out.push(quote(m.mention));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (r.newThreads.length) {
|
|
446
|
+
out.push("");
|
|
447
|
+
out.push("## New threads");
|
|
448
|
+
for (const i of r.newThreads) out.push(row(i, [i.status]));
|
|
449
|
+
}
|
|
450
|
+
if (r.newReplies.length) {
|
|
451
|
+
out.push("");
|
|
452
|
+
out.push("## New replies");
|
|
453
|
+
for (const i of r.newReplies) {
|
|
454
|
+
out.push(row(i, [i.status]));
|
|
455
|
+
if (i.omittedReplies) out.push(` (${i.omittedReplies} more not shown)`);
|
|
456
|
+
for (const m of i.replies) out.push(quote(m));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (r.resolved.length) {
|
|
460
|
+
out.push("");
|
|
461
|
+
out.push("## Resolved");
|
|
462
|
+
for (const i of r.resolved) {
|
|
463
|
+
out.push(row(i, i.resolvedBy ? [`by ${i.resolvedBy}`] : []));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (r.deferred.length) {
|
|
467
|
+
out.push("");
|
|
468
|
+
out.push("## Deferred");
|
|
469
|
+
for (const i of r.deferred) {
|
|
470
|
+
out.push(row(i, i.deferredBy ? [`by ${i.deferredBy}`] : []));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return out.join("\n");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function renderBySource(r) {
|
|
477
|
+
const out = [];
|
|
478
|
+
const cov = r.coverage;
|
|
479
|
+
if (cov.stamped === 0) {
|
|
480
|
+
return (
|
|
481
|
+
`${r.project.name}: none of its ${plural(cov.threads, "thread", "threads")} carry a source stamp. ` +
|
|
482
|
+
"Stamps come from the Skyelight build plugin (@skyelight/build) on preview builds; " +
|
|
483
|
+
"threads pinned on a build without it have no file to match."
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (r.mode === "hotspots") {
|
|
487
|
+
out.push(
|
|
488
|
+
`${r.project.name}: ${cov.stamped} of ${plural(cov.threads, "thread", "threads")} carry a source stamp, ` +
|
|
489
|
+
`across ${plural(r.totalFiles, "file", "files")}. Most outstanding feedback first:`,
|
|
490
|
+
);
|
|
491
|
+
for (const f of r.files) {
|
|
492
|
+
out.push(
|
|
493
|
+
`- ${f.file}: ${f.open} open, ${f.deferred} deferred, ${f.resolved} resolved` +
|
|
494
|
+
(f.components.length ? ` (${f.components.join(", ")})` : ""),
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
out.push("");
|
|
498
|
+
out.push("Pass file to see the threads about one of them.");
|
|
499
|
+
return out.join("\n");
|
|
500
|
+
}
|
|
501
|
+
const what = [r.query.file, r.query.component].filter(Boolean).join(", ");
|
|
502
|
+
if (r.matched === 0) {
|
|
503
|
+
return `No threads on ${what} in ${r.project.name}. ${cov.stamped} of ${cov.threads} threads carry a source stamp.`;
|
|
504
|
+
}
|
|
505
|
+
out.push(
|
|
506
|
+
`${plural(r.matched, "thread", "threads")} on ${what}: ${r.summary.open} open, ` +
|
|
507
|
+
`${r.summary.deferred} deferred, ${r.summary.resolved} resolved.`,
|
|
508
|
+
);
|
|
509
|
+
for (const i of r.items) out.push(row(i, [i.status]));
|
|
510
|
+
out.push("");
|
|
511
|
+
out.push("Use get_item with an id for the full thread and the element.");
|
|
512
|
+
return out.join("\n");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function renderSimilar(r) {
|
|
516
|
+
const out = [];
|
|
517
|
+
const about = r.subject ? `item ${r.subject.id}` : "that report";
|
|
518
|
+
if (!r.candidates.length) {
|
|
519
|
+
return `Nothing in ${r.project.name} looks like ${about} (compared against ${plural(r.compared, "thread", "threads")}). Likely new.`;
|
|
520
|
+
}
|
|
521
|
+
out.push(
|
|
522
|
+
`Possible matches for ${about} in ${r.project.name}, strongest first:`,
|
|
523
|
+
);
|
|
524
|
+
for (const c of r.candidates) {
|
|
525
|
+
out.push(row(c, [c.status]));
|
|
526
|
+
out.push(` score ${c.score}: ${c.reasons.join(", ")}`);
|
|
527
|
+
}
|
|
528
|
+
out.push("");
|
|
529
|
+
out.push(
|
|
530
|
+
"Read the threads before deciding. If one is the same problem, reply on it instead of creating a new item, or fold duplicates with merge_items.",
|
|
531
|
+
);
|
|
532
|
+
return out.join("\n");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function renderMerged(r) {
|
|
536
|
+
return r.merged
|
|
537
|
+
? `Merged ${plural(r.merged, "item", "items")} into ${r.canonicalId}. They count as extra reports on it now.`
|
|
538
|
+
: `Nothing merged: those items were already merged into ${r.canonicalId}, or were the same item.`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function renderDecisionLog(r) {
|
|
542
|
+
const out = [];
|
|
543
|
+
out.push(`# ${r.project.name}: decisions`);
|
|
544
|
+
const note = cappedNote(r);
|
|
545
|
+
if (note) out.push(note);
|
|
546
|
+
out.push("");
|
|
547
|
+
out.push("## Rules the project already has");
|
|
548
|
+
out.push(r.rules.projectContext || "(no project context yet)");
|
|
549
|
+
if (r.rules.workspaceInstructions) {
|
|
550
|
+
out.push("");
|
|
551
|
+
out.push("Workspace instructions:");
|
|
552
|
+
out.push(r.rules.workspaceInstructions);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
out.push("");
|
|
556
|
+
out.push(
|
|
557
|
+
`## Decided in threads (${r.decided}${r.decisions.length < r.decided ? `, ${r.offset + 1} to ${r.offset + r.decisions.length} shown` : ""})`,
|
|
558
|
+
);
|
|
559
|
+
if (!r.decisions.length)
|
|
560
|
+
out.push("No thread was resolved or deferred after a discussion.");
|
|
561
|
+
for (const d of r.decisions) {
|
|
562
|
+
out.push(
|
|
563
|
+
row(d, [
|
|
564
|
+
`${d.outcome} ${date(d.decidedAt)}${d.decidedBy ? ` by ${d.decidedBy}` : ""}`,
|
|
565
|
+
]),
|
|
566
|
+
);
|
|
567
|
+
out.push(quote(d.question));
|
|
568
|
+
if (d.omittedReplies)
|
|
569
|
+
out.push(` (${d.omittedReplies} earlier replies not shown)`);
|
|
570
|
+
for (const m of d.conversation) out.push(quote(m));
|
|
571
|
+
}
|
|
572
|
+
if (r.nextOffset !== null && r.nextOffset !== undefined) {
|
|
573
|
+
out.push(
|
|
574
|
+
` (${r.decided - r.nextOffset} more: call decision_log with offset ${r.nextOffset})`,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (r.corrections.length) {
|
|
579
|
+
out.push("");
|
|
580
|
+
out.push("## Types people corrected");
|
|
581
|
+
for (const c of r.corrections) {
|
|
582
|
+
out.push(`- ${c.from} to ${c.to}: "${c.line}" (id ${c.id})`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (r.recurring.length) {
|
|
586
|
+
out.push("");
|
|
587
|
+
out.push("## Where outcomes recur");
|
|
588
|
+
for (const x of r.recurring)
|
|
589
|
+
out.push(`- ${x.where}: ${x.resolved} resolved`);
|
|
590
|
+
}
|
|
591
|
+
out.push("");
|
|
592
|
+
out.push(
|
|
593
|
+
"State each decision with its id and quote. To keep one as a rule for future threads, confirm with the person, then save_rule.",
|
|
594
|
+
);
|
|
595
|
+
return out.join("\n");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function renderRuleSaved(r) {
|
|
599
|
+
if (r.duplicate)
|
|
600
|
+
return `That rule is already in the project context. Nothing saved.`;
|
|
601
|
+
return (
|
|
602
|
+
`Saved to the project's Rules learned: "${r.rule}"` +
|
|
603
|
+
(r.cited?.length ? ` (citing ${r.cited.join(", ")})` : "") +
|
|
604
|
+
". The classifier and every agent will read it from now on."
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Text for a launch tool's result, or null for a tool that is not one. */
|
|
609
|
+
export function renderInsight(name, data) {
|
|
610
|
+
switch (name) {
|
|
611
|
+
case "project_review":
|
|
612
|
+
return renderProjectReview(data);
|
|
613
|
+
case "whats_new":
|
|
614
|
+
return renderWhatsNew(data);
|
|
615
|
+
case "find_by_source":
|
|
616
|
+
return renderBySource(data);
|
|
617
|
+
case "find_similar":
|
|
618
|
+
return renderSimilar(data);
|
|
619
|
+
case "merge_items":
|
|
620
|
+
return renderMerged(data);
|
|
621
|
+
case "decision_log":
|
|
622
|
+
return renderDecisionLog(data);
|
|
623
|
+
case "save_rule":
|
|
624
|
+
return renderRuleSaved(data);
|
|
625
|
+
default:
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** The stdio side: each launch tool as its REST call. */
|
|
631
|
+
export async function callInsightTool(name, args, { client, projectId }) {
|
|
632
|
+
switch (name) {
|
|
633
|
+
case "project_review":
|
|
634
|
+
return client.projectReview({
|
|
635
|
+
projectId,
|
|
636
|
+
section: args.section,
|
|
637
|
+
offset: args.offset,
|
|
638
|
+
});
|
|
639
|
+
case "whats_new":
|
|
640
|
+
return client.whatsNew({ projectId, since: args.since });
|
|
641
|
+
case "find_by_source":
|
|
642
|
+
return client.findBySource({
|
|
643
|
+
projectId,
|
|
644
|
+
file: args.file,
|
|
645
|
+
line: args.line,
|
|
646
|
+
component: args.component,
|
|
647
|
+
status: args.status,
|
|
648
|
+
limit: args.limit,
|
|
649
|
+
});
|
|
650
|
+
case "find_similar":
|
|
651
|
+
if (!args.itemId && !args.text?.trim()) {
|
|
652
|
+
throw new Error(
|
|
653
|
+
"find_similar needs the text you are about to report, or an itemId",
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
return client.findSimilar({
|
|
657
|
+
projectId,
|
|
658
|
+
text: args.text,
|
|
659
|
+
itemId: args.itemId,
|
|
660
|
+
page: args.page,
|
|
661
|
+
selector: args.selector,
|
|
662
|
+
includeResolved: args.includeResolved,
|
|
663
|
+
limit: args.limit,
|
|
664
|
+
});
|
|
665
|
+
case "merge_items":
|
|
666
|
+
if (!args.itemId) throw new Error("merge_items needs an itemId");
|
|
667
|
+
return client.mergeItems({
|
|
668
|
+
itemId: args.itemId,
|
|
669
|
+
duplicateIds: args.duplicateIds ?? [],
|
|
670
|
+
});
|
|
671
|
+
case "decision_log":
|
|
672
|
+
return client.decisionLog({
|
|
673
|
+
projectId,
|
|
674
|
+
since: args.since,
|
|
675
|
+
limit: args.limit,
|
|
676
|
+
offset: args.offset,
|
|
677
|
+
});
|
|
678
|
+
case "save_rule":
|
|
679
|
+
if (!args.rule?.trim()) throw new Error("save_rule needs the rule text");
|
|
680
|
+
return client.saveRule({
|
|
681
|
+
projectId,
|
|
682
|
+
rule: args.rule,
|
|
683
|
+
sourceItemIds: args.sourceItemIds,
|
|
684
|
+
});
|
|
685
|
+
default:
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
688
|
+
}
|