toolroll 0.9.30 → 0.9.31
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 +14 -0
- package/dist/flow-gallery-ui.d.ts +14 -9
- package/dist/flow-gallery-ui.js +71 -29
- package/dist/flow-gallery.d.ts +26 -2
- package/dist/flow-gallery.js +182 -7
- package/dist/mcp-connect.d.ts +3 -1
- package/dist/mcp-connect.js +11 -6
- package/dist/serve.js +26 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.31 — 2026-10-04
|
|
4
|
+
|
|
5
|
+
- **Flow templates for your integrations.** Nine new templates put connected
|
|
6
|
+
tools to work: UI inspiration (Mobbin), Metrics digest and Fix the biggest
|
|
7
|
+
drop-off (PostHog), Outage to cause (Better Stack), Figma frame to pull
|
|
8
|
+
request, Worker errors to fix (Cloudflare), Failed deploy to fix (Vercel),
|
|
9
|
+
Linear issues to PRs, and Support to bug fix (Intercom). Error to fix now
|
|
10
|
+
reads stack traces through Sentry. Each card shows its tool's logo and
|
|
11
|
+
whether it's connected; one that isn't offers Connect first. Research steps
|
|
12
|
+
only read, and nothing merges.
|
|
13
|
+
- **Templates show their steps.** The unlabeled boxes on each template are
|
|
14
|
+
now a named step strip, like "Find the drop-off → You choose → Build the fix
|
|
15
|
+
→ Pull request → Sent to you", with your steps marked.
|
|
16
|
+
|
|
3
17
|
## 0.9.30 — 2026-10-04
|
|
4
18
|
|
|
5
19
|
- **Long tasks can be planned.** 0.9.28 let task goals run to 8,000
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* its promise, a small drawing of its zones, what it needs and "Use this". A template's page asks only what it
|
|
4
|
-
* needs, previews in plain words what it will do and never do, and creates it — the same answers it previewed.
|
|
5
|
-
*/
|
|
6
|
-
import { type GalleryAnswers, type GalleryPreview, type GalleryTemplate } from "./flow-gallery.js";
|
|
7
|
-
import { type FlowDefinition } from "./flows.js";
|
|
1
|
+
import { type GalleryAnswers, type GalleryPreview, type GalleryTemplate, type GalleryTool } from "./flow-gallery.js";
|
|
2
|
+
import { type FlowDefinition, type FlowStage } from "./flows.js";
|
|
8
3
|
export declare const GALLERY_CSS: string;
|
|
9
|
-
/** A
|
|
10
|
-
|
|
4
|
+
/** A flow's main path, as a person reads it: from the start, each step's first way on (a sort's first answer, a choose's
|
|
5
|
+
* first option that goes somewhere, else next; a check's failure when passing just ends), never back to a step shown. */
|
|
6
|
+
export declare function mainPath(definition: FlowDefinition): FlowStage[];
|
|
7
|
+
/** A flow's steps in order, joined by arrows; a person's decisions carry the accent. */
|
|
8
|
+
export declare function stepStrip(definition: FlowDefinition): string;
|
|
11
9
|
/** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
|
|
12
10
|
export declare function galleryHtml(input: {
|
|
13
11
|
repo: string | null;
|
|
14
12
|
canUse: boolean;
|
|
13
|
+
connections?: readonly {
|
|
14
|
+
id: string;
|
|
15
|
+
state: "connected" | "open" | "taken";
|
|
16
|
+
}[] | null;
|
|
15
17
|
}): string;
|
|
16
18
|
/** A template's page: its questions, then what it will do and never do, then Create. */
|
|
17
19
|
export declare function galleryUseHtml(input: {
|
|
@@ -27,4 +29,7 @@ export declare function galleryUseHtml(input: {
|
|
|
27
29
|
problem: string | null;
|
|
28
30
|
csrf: string;
|
|
29
31
|
diagram: FlowDefinition | null;
|
|
32
|
+
/** The template's tools in this project; `said` is how the last Connect went. */
|
|
33
|
+
tools?: readonly GalleryTool[];
|
|
34
|
+
said?: string | null;
|
|
30
35
|
}): string;
|
package/dist/flow-gallery-ui.js
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The flow gallery in the console (Flows → New and Settings → Flows): cards grouped by what they're for, each with
|
|
3
|
-
* its promise,
|
|
3
|
+
* its promise, its steps in order, what it needs and "Use this". A template's page asks only what it
|
|
4
4
|
* needs, previews in plain words what it will do and never do, and creates it — the same answers it previewed.
|
|
5
|
+
* A template's tools show their marks and whether the project has them; one it lacks offers Connect first.
|
|
5
6
|
*/
|
|
6
|
-
import {
|
|
7
|
+
import { brandMarkHtml } from "./brand-mark.js";
|
|
8
|
+
import { BLANK, GALLERY, GALLERY_GROUPS, galleryDiagram, galleryToolsOf, OUTDATED_COMMANDS, SEND_RESULT, sendsAlready } from "./flow-gallery.js";
|
|
7
9
|
import { choiceTargets } from "./flows.js";
|
|
8
10
|
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
9
11
|
export const GALLERY_CSS = `.gallery{max-width:1120px;min-width:0}.gallery h2{margin:28px 0 10px;font-size:.9375rem}.gallery h2:first-of-type{margin-top:8px}` +
|
|
10
12
|
`.gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}` +
|
|
11
13
|
`.gallery-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}` +
|
|
12
14
|
`.gallery-card h3{margin:0;font-size:.9375rem}.gallery-card p{margin:0;overflow-wrap:anywhere}.gallery-card .gallery-promise{line-height:1.5;flex:1}` +
|
|
13
|
-
|
|
14
|
-
`.gallery-
|
|
15
|
+
// A template's steps in order, joined by arrows; a person's own steps carry the attention chip.
|
|
16
|
+
`.gallery-steps{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px;list-style:none;padding:0;margin:0;font-size:12px;font-weight:500;line-height:1.5;color:var(--so-ink)}` +
|
|
17
|
+
`.gallery-steps li{display:inline-flex;align-items:center;gap:6px;min-width:0;overflow-wrap:anywhere}.gallery-steps li>span[aria-hidden]{color:var(--so-muted)}` +
|
|
18
|
+
`.gallery-steps b{font-weight:500;font-size:11.5px;line-height:18px;padding:1px 6px;border-radius:5px;background:var(--so-signal-soft);color:var(--so-signal)}` +
|
|
15
19
|
`.gallery-needs{display:flex;flex-wrap:wrap;gap:4px;list-style:none;padding:0;margin:0}.gallery-needs li{font-size:11.5px;font-weight:500;line-height:18px;padding:1px 6px;border-radius:5px;background:var(--so-neutral-soft,var(--so-raised));color:var(--so-neutral-ink,var(--so-muted))}` +
|
|
16
20
|
// An outline action in a list (the Quiet List Rule), over the workspace's ink default for .button-link.
|
|
17
21
|
`.gallery .gallery-card a.button-link{align-self:flex-start;min-height:36px;background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
|
|
18
22
|
`.gallery-blank{margin-top:20px}` +
|
|
19
|
-
|
|
23
|
+
// A template's tools: the mark Settings → Integrations shows, its name and where the project stands.
|
|
24
|
+
`.gallery-tools{display:grid;gap:6px;list-style:none;padding:0;margin:0}.gallery-tools li{display:flex;align-items:center;gap:8px;min-width:0;flex-wrap:wrap}.gallery-tools li>span:not(.brand-mark){min-width:0;overflow-wrap:anywhere}` +
|
|
25
|
+
`.gallery-tools strong{font-weight:500;overflow-wrap:anywhere}.gallery-connect{display:grid;gap:10px;padding:12px 0;border-top:1px solid var(--so-line)}.gallery-connect:last-child{border-bottom:1px solid var(--so-line);margin-bottom:4px}` +
|
|
26
|
+
`.gallery-connect form{display:grid;gap:8px;margin:0}.gallery-connect p{margin:0;overflow-wrap:anywhere}.gallery-connect .gallery-tool-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.gallery-connect button{justify-self:start;min-height:36px;white-space:nowrap;background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}` +
|
|
27
|
+
`.gallery-use{max-width:720px;min-width:0}.gallery-use .gallery-steps{margin:4px 0 12px}.gallery-use form{display:grid;gap:12px;margin:0}.gallery-use label{display:grid;gap:4px;font-weight:500}` +
|
|
20
28
|
`.gallery-use label small{font-weight:400;color:var(--so-muted)}.gallery-use label.gallery-check{display:flex;align-items:flex-start;gap:8px;font-weight:500}.gallery-use label.gallery-check input{margin-top:3px;width:16px;height:16px;min-height:0;flex:none}.gallery-use label.gallery-check small{display:block}` +
|
|
21
29
|
`.gallery-use input:not([type=hidden]),.gallery-use select{box-sizing:border-box;width:100%;max-width:100%;min-height:32px;padding:0 10px;border:1px solid var(--so-input-line);border-radius:8px;background:var(--so-paper);color:var(--so-ink);font:inherit}` +
|
|
22
30
|
`.gallery-preview{display:grid;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}.gallery-preview h2{margin:0;font-size:.9375rem}` +
|
|
@@ -24,33 +32,47 @@ export const GALLERY_CSS = `.gallery{max-width:1120px;min-width:0}.gallery h2{ma
|
|
|
24
32
|
`.gallery-preview details ol{margin:6px 0 0;padding-left:20px;white-space:pre-line;font-size:.875rem;line-height:1.5;color:var(--so-muted)}.gallery-preview details li{margin:0 0 6px;overflow-wrap:anywhere}` +
|
|
25
33
|
`.gallery-preview summary{cursor:pointer;min-height:36px;display:flex;align-items:center;font-weight:500}` +
|
|
26
34
|
`.gallery-actions{display:flex;flex-wrap:wrap;gap:8px;margin:0}.gallery-actions button{min-height:36px;white-space:nowrap}` +
|
|
27
|
-
`.gallery-use .gallery-actions button[value=create]{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}.gallery-heading{margin:36px 0 0;font-size:1.05rem}` +
|
|
28
|
-
`@media(max-width:760px){.gallery-grid{grid-template-columns:minmax(0,1fr)}.gallery .gallery-card a.button-link{min-height:44px;align-self:stretch;justify-content:center}` +
|
|
35
|
+
`.gallery-use .gallery-actions button[value=create]:not(.secondary){background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}.gallery-heading{margin:36px 0 0;font-size:1.05rem}` +
|
|
36
|
+
`@media(max-width:760px){.gallery-grid{grid-template-columns:minmax(0,1fr)}.gallery-connect button{min-height:44px;justify-self:stretch}.gallery .gallery-card a.button-link{min-height:44px;align-self:stretch;justify-content:center}` +
|
|
29
37
|
`.gallery-use input:not([type=hidden]),.gallery-use select{font-size:16px;min-height:44px}.gallery-actions button{flex:1 1 auto;min-height:44px}}`;
|
|
30
|
-
/** A
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return `<svg class="gallery-zones" viewBox="${box}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="${e(label)}">${lines}${rects}</svg>`;
|
|
38
|
+
/** A flow's main path, as a person reads it: from the start, each step's first way on (a sort's first answer, a choose's
|
|
39
|
+
* first option that goes somewhere, else next; a check's failure when passing just ends), never back to a step shown. */
|
|
40
|
+
export function mainPath(definition) {
|
|
41
|
+
const byId = new Map(definition.stages.map(one => [one.id, one]));
|
|
42
|
+
const path = [];
|
|
43
|
+
for (let at = byId.get(definition.start); at !== undefined && at.kind !== "done" && !path.includes(at);) {
|
|
44
|
+
path.push(at);
|
|
45
|
+
const ways = [...(at.sort?.answers.map(one => one.to) ?? []), ...choiceTargets(at), at.next, ...(at.routes?.map(one => one.to) ?? [])].map(id => id === null ? undefined : byId.get(id));
|
|
46
|
+
if (at.kind === "check")
|
|
47
|
+
ways.push(at.onFail === null ? undefined : byId.get(at.onFail));
|
|
48
|
+
at = at.kind === "check" ? ways.find(one => one !== undefined && one.kind !== "done") : ways.find(one => one !== undefined);
|
|
49
|
+
}
|
|
50
|
+
return path;
|
|
44
51
|
}
|
|
45
|
-
|
|
52
|
+
/** A step's short name in the strip: a person's own steps from their side. */
|
|
53
|
+
const stepName = (stage) => stage.kind === "choose" ? "You choose" : stage.kind === "approval" ? "You approve" : stage.kind === "send" ? "Sent to you" : stage.title;
|
|
54
|
+
/** A flow's steps in order, joined by arrows; a person's decisions carry the accent. */
|
|
55
|
+
export function stepStrip(definition) {
|
|
56
|
+
return `<ol class="gallery-steps" aria-label="Steps">${mainPath(definition).map((one, i) => {
|
|
57
|
+
const name = e(stepName(one));
|
|
58
|
+
return `<li>${i === 0 ? "" : `<span aria-hidden="true">→</span>`}${one.kind === "choose" || one.kind === "approval" ? `<b data-person>${name}</b>` : name}</li>`;
|
|
59
|
+
}).join("")}</ol>`;
|
|
60
|
+
}
|
|
61
|
+
const needsList = (template) => template.needs.length === 0 ? (template.tools ?? []).length > 0 ? "" : `<ul class="gallery-needs"><li>Nothing to connect</li></ul>`
|
|
46
62
|
: `<ul class="gallery-needs" aria-label="Needs">${template.needs.map(one => `<li>${e(one)}</li>`).join("")}</ul>`;
|
|
63
|
+
/** A tool's state in words, as Settings → Integrations badges it; nothing without a project. */
|
|
64
|
+
const STATE = { connected: ["connected", "Connected"], open: ["not-set-up", "Not connected"], taken: ["not-set-up", "Added another way"] };
|
|
65
|
+
const toolState = (tool) => tool.state === null ? "" : `<span class="integration-state integration-state--${STATE[tool.state][0]}"><i aria-hidden="true"></i>${STATE[tool.state][1]}</span>`;
|
|
66
|
+
const toolMark = (tool) => brandMarkHtml(tool.id.replace(/-desktop$/, ""), tool.label, tool.state === "connected");
|
|
67
|
+
const toolsList = (tools) => tools.length === 0 ? ""
|
|
68
|
+
: `<ul class="gallery-tools" aria-label="Tools">${tools.map(one => `<li data-tool="${e(one.id)}" data-state="${one.state ?? "unknown"}">${toolMark(one)}<strong>${e(one.label)}</strong>${toolState(one)}</li>`).join("")}</ul>`;
|
|
47
69
|
/** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
|
|
48
70
|
export function galleryHtml(input) {
|
|
49
71
|
const href = (id) => `/flows/new/${id}${input.repo === null ? "" : `?repo=${encodeURIComponent(input.repo)}`}`;
|
|
50
72
|
const card = (template) => {
|
|
51
73
|
const definition = galleryDiagram(template);
|
|
52
74
|
return `<article class="gallery-card" data-template="${e(template.id)}"><h3>${e(template.name)}</h3><p class="gallery-promise">${e(template.promise)}</p>` +
|
|
53
|
-
|
|
75
|
+
stepStrip(definition) + toolsList(galleryToolsOf(template, definition, input.repo === null ? null : input.connections ?? null)) + needsList(template) +
|
|
54
76
|
(input.canUse ? `<a class="button-link" href="${e(href(template.id))}" aria-label="Use ${e(template.name)}">Use this</a>` : "") + `</article>`;
|
|
55
77
|
};
|
|
56
78
|
const groups = GALLERY_GROUPS.map(group => `<h2 id="gallery-${group.id}">${e(group.label)}</h2><div class="gallery-grid" data-group="${group.id}">${GALLERY.filter(one => one.group === group.id).map(card).join("")}</div>`).join("");
|
|
@@ -59,28 +81,48 @@ export function galleryHtml(input) {
|
|
|
59
81
|
/** A template's page: its questions, then what it will do and never do, then Create. */
|
|
60
82
|
export function galleryUseHtml(input) {
|
|
61
83
|
const { template } = input;
|
|
84
|
+
const tools = input.tools ?? input.preview?.tools ?? [];
|
|
85
|
+
// A tool added another way is there: only one the project lacks is missing.
|
|
86
|
+
const missing = tools.filter(one => one.state !== "connected" && one.state !== "taken");
|
|
87
|
+
// Each tool with its state; one the project lacks offers its own Connect (the Tools page's, with the same password), and comes back here.
|
|
88
|
+
const connect = (tool) => {
|
|
89
|
+
const head = `<p class="gallery-tool-head">${toolMark(tool)} <strong>${e(tool.label)}</strong> ${toolState(tool)}</p>`;
|
|
90
|
+
// Added another way, it is there: nothing to connect.
|
|
91
|
+
if (tool.state === "connected" || tool.state === "taken" || input.csrf === "")
|
|
92
|
+
return `<div class="gallery-connect" data-tool="${e(tool.id)}" data-state="${tool.state}">${head}</div>`;
|
|
93
|
+
const how = tool.id === "figma-desktop" ? "Open the Figma desktop app and turn on its Dev Mode MCP server in Preferences." : `You sign in on ${tool.label}, then come back here.`;
|
|
94
|
+
return `<div class="gallery-connect" data-tool="${e(tool.id)}" data-state="open">${head}<form method="post" action="/settings/tools/connect">` +
|
|
95
|
+
`<input type="hidden" name="csrf" value="${e(input.csrf)}"><input type="hidden" name="repo" value="${e(input.repo)}"><input type="hidden" name="shown" value="${e(input.repo)}"><input type="hidden" name="template" value="${e(template.id)}">` +
|
|
96
|
+
`<p class="meta">${e(how)}</p><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>` +
|
|
97
|
+
`<button name="service" value="${e(tool.id)}">Connect ${e(tool.label)}</button></form></div>`;
|
|
98
|
+
};
|
|
99
|
+
const toolsBlock = tools.length === 0 ? "" : `<section class="gallery-tools-use" aria-label="Tools">${tools.map(connect).join("")}</section>`;
|
|
100
|
+
// Made without a tool, the preview names the zone that needs it.
|
|
101
|
+
const zones = (tool) => tool.zones.map(zone => `“${e(zone)}”`).join(" and ");
|
|
102
|
+
const needing = missing.filter(one => one.zones.length > 0).map(one => `<p class="meta" data-needs-tool="${e(one.id)}">${zones(one)} need${one.zones.length === 1 ? "s" : ""} ${e(one.label)}, which isn't connected.</p>`).join("") +
|
|
103
|
+
tools.filter(one => one.state === "taken" && one.zones.length > 0).map(one => `<p class="meta" data-tool-taken="${e(one.id)}">${zones(one)} use${one.zones.length === 1 ? "s" : ""} ${e(one.label)}, added another way.</p>`).join("");
|
|
62
104
|
const field = (ask) => {
|
|
63
105
|
const value = input.answers[ask.key] ?? ask.default;
|
|
64
106
|
const hint = ask.hint === undefined ? "" : ` <small>${e(ask.hint)}</small>`;
|
|
65
107
|
if (ask.key === "outdated")
|
|
66
108
|
return `<label>${e(ask.label)}<select name="outdated">${Object.entries(OUTDATED_COMMANDS).map(([id, one]) => `<option value="${id}"${id === value ? " selected" : ""}>${e(one.label)}</option>`).join("")}</select></label>`;
|
|
67
|
-
return `<label>${e(ask.label)}${hint}<input name="${ask.key}" value="${e(value)}" required maxlength="${ask.key === "command" ? 500 : 100}"${ask.key === "command" ? ' spellcheck="false" autocapitalize="off"' : ""}></label>`;
|
|
109
|
+
return `<label>${e(ask.label)}${hint}<input name="${ask.key}" value="${e(value)}" required maxlength="${ask.key === "command" ? 500 : ask.key === "team" ? 12 : 100}"${ask.key === "command" ? ' spellcheck="false" autocapitalize="off"' : ask.key === "team" ? ' spellcheck="false" autocapitalize="characters"' : ""}></label>`;
|
|
68
110
|
};
|
|
69
111
|
const project = input.projects.length > 1
|
|
70
112
|
? `<label>Project<select name="repo">${input.projects.map(one => `<option value="${e(one.path)}"${one.path === input.repo ? " selected" : ""}>${e(one.name)}</option>`).join("")}</select></label>`
|
|
71
113
|
: `<input type="hidden" name="repo" value="${e(input.repo)}">`;
|
|
72
114
|
const preview = input.preview === null ? "" : `<section class="gallery-preview" aria-labelledby="gallery-will">` +
|
|
73
115
|
`<h2 id="gallery-will">What it will do</h2><ul>${input.preview.built.does.map(one => `<li>${e(one)}</li>`).join("")}</ul>` +
|
|
74
|
-
`<p class="gallery-never">${e(input.preview.built.never)}</p
|
|
116
|
+
`<p class="gallery-never">${e(input.preview.built.never)}</p>${needing}` +
|
|
75
117
|
(input.preview.startsFrom.length === 0 ? `<p class="meta">Cards start when you add them.</p>` : input.preview.startsFrom.map(one => `<p class="meta">Starts from: ${e(one)}</p>`).join("")) +
|
|
76
118
|
`<details><summary>Every step</summary><ol>${input.preview.steps.map(one => `<li>${e(one.replace(/^\d+\.\s/, ""))}</li>`).join("")}</ol></details></section>`;
|
|
77
119
|
return `<section class="gallery-use" data-gallery-template="${e(template.id)}">` +
|
|
78
|
-
(input.problem === null ? "" : `<p class="problem" role="alert">${e(input.problem)}</p>`) +
|
|
79
|
-
`<p>${e(template.promise)}</p>${input.diagram === null ? "" :
|
|
120
|
+
(input.problem === null ? "" : `<p class="problem" role="alert">${e(input.problem)}</p>`) + (input.said ? `<p role="status">${e(input.said)}</p>` : "") +
|
|
121
|
+
`<p>${e(template.promise)}</p>${input.diagram === null ? "" : stepStrip(input.diagram)}${needsList(template)}${toolsBlock}` +
|
|
80
122
|
`<form method="post" action="/flows/new/${e(template.id)}" data-gallery-use><input type="hidden" name="csrf" value="${e(input.csrf)}">` +
|
|
81
123
|
`<input type="hidden" name="previewed" value="${e(input.preview?.digest ?? "")}">${project}${template.asks.map(field).join("")}` +
|
|
82
124
|
`<label>Name<input name="name" value="${e(input.name)}" maxlength="80"></label>` +
|
|
83
|
-
`<label class="gallery-check"><input type="checkbox" name="${SEND_RESULT.key}" value="yes"${input.answers[SEND_RESULT.key] === "yes" ? " checked" : ""} data-send-result><span>${e(SEND_RESULT.title)}<small>When a card finishes, what was done comes to you in your chat apps.</small></span></label
|
|
84
|
-
`<p class="gallery-actions">${input.preview === null ? "" : `<button type="submit" name="intent" value="create">Create flow</button>`}<button type="submit" name="intent" value="preview" class="secondary">${input.preview === null ? "Preview" : "Update preview"}</button></p>` +
|
|
125
|
+
(sendsAlready(template) ? "" : `<label class="gallery-check"><input type="checkbox" name="${SEND_RESULT.key}" value="yes"${input.answers[SEND_RESULT.key] === "yes" ? " checked" : ""} data-send-result><span>${e(SEND_RESULT.title)}<small>When a card finishes, what was done comes to you in your chat apps.</small></span></label>`) + preview +
|
|
126
|
+
`<p class="gallery-actions">${input.preview === null ? "" : `<button type="submit" name="intent" value="create"${missing.length > 0 ? ' class="secondary" data-without-tools' : ""}>Create flow</button>`}<button type="submit" name="intent" value="preview" class="secondary">${input.preview === null ? "Preview" : "Update preview"}</button></p>` +
|
|
85
127
|
`</form><p class="meta"><a href="/flows/new">All templates</a></p></section>`;
|
|
86
128
|
}
|
package/dist/flow-gallery.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare const GALLERY_GROUPS: readonly [{
|
|
|
15
15
|
}];
|
|
16
16
|
export type GalleryGroup = (typeof GALLERY_GROUPS)[number]["id"];
|
|
17
17
|
/** What a template may ask, besides the project. */
|
|
18
|
-
export type AskKey = "label" | "branch" | "schedule" | "time" | "command" | "outdated";
|
|
18
|
+
export type AskKey = "label" | "branch" | "schedule" | "time" | "command" | "outdated" | "team";
|
|
19
19
|
export type GalleryAsk = {
|
|
20
20
|
key: AskKey;
|
|
21
21
|
default: string;
|
|
@@ -36,6 +36,8 @@ export type GalleryTemplate = {
|
|
|
36
36
|
promise: string;
|
|
37
37
|
/** What it needs to work, in a word or two each. */
|
|
38
38
|
needs: string[];
|
|
39
|
+
/** The project tools it puts to work: one-click service ids (ONE_CLICK) or "figma-desktop". */
|
|
40
|
+
tools?: readonly string[];
|
|
39
41
|
/** Watches a GitHub repository, so the project must be on GitHub. */
|
|
40
42
|
github?: boolean;
|
|
41
43
|
asks: GalleryAsk[];
|
|
@@ -55,10 +57,28 @@ export type GalleryTemplate = {
|
|
|
55
57
|
does: string[];
|
|
56
58
|
never: string;
|
|
57
59
|
};
|
|
60
|
+
/** The card's Linear issue key, read from its source as the Linear trigger stored it ("Linear ENG-123"); "none" skips the comment. */
|
|
61
|
+
export declare const LINEAR_KEY = "key=\"$(sed -n 's/.*\"source\":{\"kind\":\"linear\",\"label\":\"Linear \\([A-Z][A-Z0-9]*-[0-9][0-9]*\\)\".*/\\1/p' | head -n 1)\"\nif [ -n \"$key\" ]; then echo \"$key\"; echo \"goto: Found\"; else echo none; echo \"goto: None\"; fi";
|
|
58
62
|
export declare const GALLERY: readonly GalleryTemplate[];
|
|
59
63
|
/** The blank flow, for starting from nothing. */
|
|
60
64
|
export declare const BLANK: GalleryTemplate;
|
|
61
65
|
export declare const galleryTemplateOf: (id: string) => GalleryTemplate | null;
|
|
66
|
+
/** A tool a template uses: where the chosen project stands with it (null with no project), and the zones that use it. */
|
|
67
|
+
export type GalleryTool = {
|
|
68
|
+
id: string;
|
|
69
|
+
label: string;
|
|
70
|
+
state: "connected" | "open" | "taken" | null;
|
|
71
|
+
zones: string[];
|
|
72
|
+
};
|
|
73
|
+
/** A tool's name ("Mobbin", "Figma (desktop app)"); null for an id that's neither a one-click service nor an app on this computer. */
|
|
74
|
+
export declare const galleryToolLabel: (id: string) => string | null;
|
|
75
|
+
/** The zones that use a tool: a tool step on it, or a research or build step that names it ("With Mobbin, …"). */
|
|
76
|
+
export declare function zonesUsing(definition: FlowDefinition, id: string): string[];
|
|
77
|
+
/** A template's tools, each with its state in a project's connections (connectionsOf), or none without a project. */
|
|
78
|
+
export declare function galleryToolsOf(template: GalleryTemplate, definition: FlowDefinition, connections: readonly {
|
|
79
|
+
id: string;
|
|
80
|
+
state: "connected" | "open" | "taken";
|
|
81
|
+
}[] | null): GalleryTool[];
|
|
62
82
|
export type GalleryAnswers = Record<string, string>;
|
|
63
83
|
/** Each question's default for a project: the branch pull requests go to, when that's set up. */
|
|
64
84
|
export declare function galleryDefaults(store: Store, template: GalleryTemplate, repo: string | null): GalleryAnswers;
|
|
@@ -68,7 +88,10 @@ export declare const SEND_RESULT: {
|
|
|
68
88
|
readonly title: "Send me the result";
|
|
69
89
|
readonly does: "When a card finishes, sends you what was done (its summary, links and screenshots) in your chat apps.";
|
|
70
90
|
};
|
|
71
|
-
/** A
|
|
91
|
+
/** A template that already sends its result to the person: it isn't offered "Send me the result" again. */
|
|
92
|
+
export declare const sendsAlready: (template: GalleryTemplate) => boolean;
|
|
93
|
+
/** A flow with "Send me the result" before its end: every path into its main Done zone passes through it first.
|
|
94
|
+
* A flow that already has a send zone is left as it is, so nobody gets the result twice. */
|
|
72
95
|
export declare function withSendResult(definition: FlowDefinition): FlowDefinition;
|
|
73
96
|
/** The answers, checked in plain words, with what they fill in. Throws. */
|
|
74
97
|
export declare function checkAnswers(template: GalleryTemplate, given: GalleryAnswers): GalleryAnswers;
|
|
@@ -96,6 +119,7 @@ export type GalleryPreview = {
|
|
|
96
119
|
built: GalleryBuilt;
|
|
97
120
|
startsFrom: string[];
|
|
98
121
|
steps: string[];
|
|
122
|
+
tools: GalleryTool[];
|
|
99
123
|
digest: string;
|
|
100
124
|
};
|
|
101
125
|
/** What using a template will do, in words, before anything is made: its triggers checked as the flow will have them. Throws. */
|
package/dist/flow-gallery.js
CHANGED
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
* shared flow file later. Every one is built only from existing zone and trigger kinds, and none merges:
|
|
10
10
|
* a pull request it opens waits for a person.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* A template that puts a connected tool to work names it (`tools`): a one-click service (ONE_CLICK) or the Figma
|
|
13
|
+
* desktop app. Its research steps read that tool and only read; a build gets the project's tools as any build does.
|
|
14
|
+
*
|
|
15
|
+
* Every template without its own send zone can end with "Send me the result" (a "Send to me" zone before its end),
|
|
16
|
+
* off unless asked for (`send-result: yes`): what the flow did reaches the person in their chat apps.
|
|
14
17
|
*/
|
|
15
18
|
import { createHash } from "node:crypto";
|
|
16
19
|
import { scanForSecrets } from "./evidence.js";
|
|
@@ -19,6 +22,7 @@ import { saveScript, validateScript } from "./flow-scripts.js";
|
|
|
19
22
|
import { isToolrollRepo, planeReviewSteps } from "./flow-starters.js";
|
|
20
23
|
import { addFlowTriggerTo, describeTrigger, githubRepoOf, scheduleFromWords, validateTriggerConfig } from "./flow-triggers.js";
|
|
21
24
|
import { choiceTargets, clockTime, flowFromSteps, flowTerms, FLOW_TEMPLATES, ISSUE_LABEL } from "./flows.js";
|
|
25
|
+
import { connectionsOf, localConnectOf, oneClickOf } from "./mcp-connect.js";
|
|
22
26
|
import { publishingOf } from "./pull-request-flow.js";
|
|
23
27
|
export const GALLERY_GROUPS = [
|
|
24
28
|
{ id: "ship", label: "Ship code" },
|
|
@@ -32,6 +36,9 @@ export const OUTDATED_COMMANDS = {
|
|
|
32
36
|
pip: { label: "pip (pip list --outdated)", command: "out=\"$(python3 -m pip list --outdated 2>/dev/null | tail -n +3)\"\nif [ -n \"$out\" ]; then echo \"$out\"; exit 1; fi" },
|
|
33
37
|
cargo: { label: "cargo (cargo outdated)", command: "cargo outdated --root-deps-only --exit-code 1" },
|
|
34
38
|
};
|
|
39
|
+
/** The card's Linear issue key, read from its source as the Linear trigger stored it ("Linear ENG-123"); "none" skips the comment. */
|
|
40
|
+
export const LINEAR_KEY = `key="$(sed -n 's/.*"source":{"kind":"linear","label":"Linear \\([A-Z][A-Z0-9]*-[0-9][0-9]*\\)".*/\\1/p' | head -n 1)"
|
|
41
|
+
if [ -n "$key" ]; then echo "$key"; echo "goto: Found"; else echo none; echo "goto: None"; fi`;
|
|
35
42
|
const NEVER_MERGES = "Never merges. Each pull request waits for you.";
|
|
36
43
|
const work = (text) => `${text}\n\nChanges asked for (if any): {{note}}`;
|
|
37
44
|
const research = (text) => `${text}\n\nFeedback to address (if any): {{note}}`;
|
|
@@ -40,6 +47,11 @@ const pr = { id: "pull-request", title: "Pull request", kind: "pull-request" };
|
|
|
40
47
|
const drawn = (id) => structuredClone(FLOW_TEMPLATES.find(one => one.id === id).definition);
|
|
41
48
|
const about = (id) => FLOW_TEMPLATES.find(one => one.id === id).about;
|
|
42
49
|
const SORTS = "OpenRouter (Jev sorts)";
|
|
50
|
+
const WEEKLY = { key: "schedule", default: "monday 09:00", label: "When", hint: "Like “monday 09:00”." };
|
|
51
|
+
const weekly = (title) => ({ kind: "schedule", schedule: "{{ask.schedule}}", title });
|
|
52
|
+
const send = (title) => ({ id: "send", title, kind: "send" });
|
|
53
|
+
/** A person's choice: build it, or end there. A reply goes to the build as its note. */
|
|
54
|
+
const choose = (title, yes, build) => ({ id: "choose", title, kind: "choose", options: [{ label: yes, goesTo: build }, { label: "Ignore", goesTo: "end" }], ifReplied: build });
|
|
43
55
|
export const GALLERY = [
|
|
44
56
|
// ---------------------------------------------------------------- Ship code
|
|
45
57
|
{
|
|
@@ -137,6 +149,50 @@ export const GALLERY = [
|
|
|
137
149
|
id: "effort-routing", name: "Effort routing", group: "ship", promise: "Small changes go straight to a build; big ones get a plan and a person first.", needs: [SORTS], asks: [], definition: drawn("effort-routing"), triggers: [],
|
|
138
150
|
does: [about("effort-routing")], never: "Never builds a big change before you approve its plan.",
|
|
139
151
|
},
|
|
152
|
+
{
|
|
153
|
+
id: "ui-inspiration", name: "UI inspiration", group: "ship", tools: ["mobbin"],
|
|
154
|
+
promise: "Each week, three real app examples for one part of your UI, ready to build.",
|
|
155
|
+
needs: ["GitHub"], asks: [WEEKLY],
|
|
156
|
+
steps: [
|
|
157
|
+
{ id: "find", title: "Find examples", kind: "report", instructions: research("Pick one surface of this project (a screen or a flow) that no recent commit or earlier inspiration covered.\n\nWith Mobbin, read only: find 3 real app examples for it. For each: the pattern, why it fits here, how it plugs into this code (files and components), and a Mobbin screenshot.") },
|
|
158
|
+
choose("Implement, remix or ignore?", "Implement", "build"),
|
|
159
|
+
{ id: "build", title: "Build it", kind: "task", instructions: work("Build the chosen example from the report into this project with its own components. A note below is a remix: follow it.\n\nThe report:\n{{stage.find}}") },
|
|
160
|
+
pr,
|
|
161
|
+
send("Send the result"),
|
|
162
|
+
],
|
|
163
|
+
triggers: [weekly("UI inspiration")],
|
|
164
|
+
does: ["Picks one part of your UI {{ask.schedule-words}} and finds 3 Mobbin examples for it, each with a screenshot.", "You choose Implement or Ignore, or reply with a remix note.", "What you choose is built as a task, under your usual approvals, and opens a pull request.", "The result is sent to you."],
|
|
165
|
+
never: NEVER_MERGES,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "figma-to-pr", name: "Figma frame to pull request", group: "ship", tools: ["figma-desktop"],
|
|
169
|
+
promise: "Paste a Figma frame link; get it built in your own components.",
|
|
170
|
+
needs: ["GitHub"], asks: [],
|
|
171
|
+
steps: [
|
|
172
|
+
{ id: "build", title: "Build the frame", kind: "task", instructions: work("Build this Figma frame in the project's own components and styles: {{card.title}}\n\n{{card.description}}\n\nRead the frame with the Figma desktop app's tools (its code, screenshot and variables). Reuse existing components; add one only where none fits. Take screenshots before and after, on desktop and phone.") },
|
|
173
|
+
pr,
|
|
174
|
+
send("Send before and after"),
|
|
175
|
+
],
|
|
176
|
+
triggers: [{ kind: "button", label: "Build a Figma frame", questions: ["Figma frame link", "Notes (optional)"] }],
|
|
177
|
+
does: ["Adds a “Build a Figma frame” button that asks for the frame link.", "Builds the frame in your components as a task, under your usual approvals, with desktop and phone screenshots.", "Opens a pull request and sends you before and after."],
|
|
178
|
+
never: NEVER_MERGES,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "linear-to-prs", name: "Linear issues to PRs", group: "ship", tools: ["linear"],
|
|
182
|
+
promise: "Labelled Linear issues are built, and the issue gets the pull request link.",
|
|
183
|
+
needs: ["GitHub", "A Linear API key"], asks: [{ key: "team", default: "ENG", label: "Linear team", hint: "Its short key, like ENG." }, { key: "label", default: ISSUE_LABEL, label: "Issue label" }],
|
|
184
|
+
// The issue's key comes from the card's source ("Linear ENG-123"), as the trigger stored it, never from a model.
|
|
185
|
+
scripts: [{ name: "linear-issue-key", about: "Prints the Linear issue the card came from, or none.", body: LINEAR_KEY, timeoutMinutes: 1 }],
|
|
186
|
+
steps: [
|
|
187
|
+
{ id: "build", title: "Build it", kind: "task", instructions: work("Build this issue: {{card.title}}\n\n{{card.description}}") },
|
|
188
|
+
pr,
|
|
189
|
+
{ id: "issue-key", title: "Find the issue key", kind: "check", script: "{{script.linear-issue-key}}", runIn: "folder", routes: [{ answer: "Found", goesTo: "comment" }, { answer: "None", goesTo: "Done" }] },
|
|
190
|
+
{ id: "comment", title: "Comment on the issue", kind: "tool", server: "linear", tool: "create_comment", args: { issueId: "{{stage.issue-key}}", body: "A pull request is ready: {{stage.pull-request}}" } },
|
|
191
|
+
],
|
|
192
|
+
triggers: [{ kind: "linear", team: "{{ask.team}}", label: "{{ask.label}}" }],
|
|
193
|
+
does: ["Watches {{ask.team}} issues labelled “{{ask.label}}” in Linear.", "Builds each one as a task, under your usual approvals, and opens a pull request.", "Comments the pull request link on the issue, through Linear."],
|
|
194
|
+
never: NEVER_MERGES,
|
|
195
|
+
},
|
|
140
196
|
// ---------------------------------------------------------- Keep it healthy
|
|
141
197
|
{
|
|
142
198
|
id: "fix-ci", name: "Fix failing CI", group: "health",
|
|
@@ -211,7 +267,7 @@ export const GALLERY = [
|
|
|
211
267
|
never: NEVER_MERGES,
|
|
212
268
|
},
|
|
213
269
|
{
|
|
214
|
-
id: "error-to-fix", name: "Error to fix", group: "health",
|
|
270
|
+
id: "error-to-fix", name: "Error to fix", group: "health", tools: ["sentry"],
|
|
215
271
|
promise: "New errors from Sentry or any error tracker are sorted, researched and fixed.",
|
|
216
272
|
needs: ["A webhook from Sentry or another tracker", SORTS], asks: [],
|
|
217
273
|
steps: [
|
|
@@ -220,7 +276,7 @@ export const GALLERY = [
|
|
|
220
276
|
{ answer: "Fix it", means: "It comes from our code and is new, frequent, or hurts people", goesTo: "find-cause" },
|
|
221
277
|
{ answer: "Ignore", means: "Noise: bots, browser extensions, a third party's outage, or something known and expected", goesTo: "ignored" },
|
|
222
278
|
], ifNotSure: "by-hand" },
|
|
223
|
-
{ id: "find-cause", title: "Find the cause", kind: "report", instructions: research("An error came in: {{card.title}}\n\n{{card.description}}\n\
|
|
279
|
+
{ id: "find-cause", title: "Find the cause", kind: "report", instructions: research("An error came in: {{card.title}}\n\n{{card.description}}\n\nWith Sentry, read the issue's stack trace and latest events. Read only. Find the cause in this code: cite the files and lines, say why it happens and what change fixes it.") },
|
|
224
280
|
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix this error, with a regression check where one fits: {{card.title}}\n\nThe report:\n{{stage.find-cause}}") },
|
|
225
281
|
owner("review", "Review the fix"),
|
|
226
282
|
{ ...pr, next: "Done" },
|
|
@@ -233,6 +289,56 @@ export const GALLERY = [
|
|
|
233
289
|
"Each kept error is researched, then fixed as a task, under your usual approvals.", "What you approve opens a pull request."],
|
|
234
290
|
never: NEVER_MERGES,
|
|
235
291
|
},
|
|
292
|
+
{
|
|
293
|
+
id: "outage-to-cause", name: "Outage to cause", group: "health", tools: ["betterstack"],
|
|
294
|
+
promise: "A Better Stack incident gets its likely cause, and a fix if you want one.",
|
|
295
|
+
needs: ["A webhook from Better Stack", "GitHub"], asks: [],
|
|
296
|
+
steps: [
|
|
297
|
+
{ id: "find-cause", title: "Find the cause", kind: "report", instructions: research("A Better Stack incident: {{card.title}}\n\n{{card.description}}\n\nWith Better Stack, read the incident and its monitor's recent history. Read this project's commits and deploys from before it started. Read only. Say the likely cause, with times, and what change fixes it.") },
|
|
298
|
+
choose("Fix or ignore?", "Fix", "fix"),
|
|
299
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix the cause of this outage, with a regression check where one fits.\n\nThe report:\n{{stage.find-cause}}") },
|
|
300
|
+
pr,
|
|
301
|
+
],
|
|
302
|
+
triggers: [{ kind: "webhook", title: "Incident", titleField: "data.attributes.name" }],
|
|
303
|
+
does: ["Adds a webhook. Once the flow is made, open its Triggers and choose New address, then paste that into Better Stack.", "Reads the incident, its monitor's history and your recent commits and deploys, and sends you the likely cause.", "You choose Fix or Ignore; a fix is built as a task, under your usual approvals, and opens a pull request."],
|
|
304
|
+
never: NEVER_MERGES,
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: "worker-errors", name: "Worker errors to fix", group: "health", tools: ["cloudflare"],
|
|
308
|
+
promise: "Each week, Cloudflare Workers and Pages errors worth fixing get a fix.",
|
|
309
|
+
needs: ["GitHub", SORTS], asks: [WEEKLY],
|
|
310
|
+
steps: [
|
|
311
|
+
{ id: "read", title: "Read the errors", kind: "report", instructions: research("With Cloudflare, read this week's Workers and Pages errors and analytics. Read only. List each error with its count, its Worker or Pages project, and when it started. Say “No errors” if there are none.") },
|
|
312
|
+
{ id: "worth-fixing", title: "Worth fixing?", kind: "sort", question: "Do these errors come from this project's code, often enough to fix now?",
|
|
313
|
+
answers: [
|
|
314
|
+
{ answer: "Fix it", means: "Errors from our own code that are new, frequent or hurt people", goesTo: "find-cause" },
|
|
315
|
+
{ answer: "Ignore", means: "No errors, or only noise: bots, a provider's outage, something known", goesTo: "ignored" },
|
|
316
|
+
], ifNotSure: "find-cause" },
|
|
317
|
+
{ id: "find-cause", title: "Find the cause", kind: "report", instructions: research("Errors worth fixing:\n{{stage.read}}\n\nWith Cloudflare, read the worst one's logs. Read only. Find the cause in this code, citing files and lines, and say what change fixes it.") },
|
|
318
|
+
choose("Fix or ignore?", "Fix", "fix"),
|
|
319
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix this error, with a regression check where one fits.\n\nThe report:\n{{stage.find-cause}}") },
|
|
320
|
+
{ ...pr, next: "Done" },
|
|
321
|
+
{ id: "done", title: "Done", kind: "done" },
|
|
322
|
+
{ id: "ignored", title: "Ignored", kind: "done" },
|
|
323
|
+
],
|
|
324
|
+
triggers: [weekly("Worker errors")],
|
|
325
|
+
does: ["Reads your Cloudflare Workers and Pages errors and analytics {{ask.schedule-words}}.", "Jev decides whether they're worth fixing; if so, finds the cause and sends you a report.", "You choose Fix or Ignore; a fix is built as a task, under your usual approvals, and opens a pull request."],
|
|
326
|
+
never: NEVER_MERGES,
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: "failed-deploy", name: "Failed deploy to fix", group: "health", tools: ["vercel"],
|
|
330
|
+
promise: "A failed Vercel deployment gets its cause found and a fix opened.",
|
|
331
|
+
needs: ["A webhook from Vercel", "GitHub"], asks: [],
|
|
332
|
+
steps: [
|
|
333
|
+
{ id: "find-cause", title: "Find the cause", kind: "report", instructions: research("A Vercel deployment failed: {{card.title}}\n\n{{card.description}}\n\nWith Vercel, read the deployment's build log. Read only. Find the cause in this code, citing files and lines, and say what change fixes it.") },
|
|
334
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix this failed deployment without weakening any check.\n\nThe report:\n{{stage.find-cause}}") },
|
|
335
|
+
pr,
|
|
336
|
+
send("Send the fix"),
|
|
337
|
+
],
|
|
338
|
+
triggers: [{ kind: "webhook", title: "Failed deployment", titleField: "payload.deployment.name" }],
|
|
339
|
+
does: ["Adds a webhook. Once the flow is made, open its Triggers and choose New address, then paste that into Vercel for failed deployments.", "Reads the build log and finds the cause.", "Builds a fix as a task, under your usual approvals, opens a pull request and sends it to you."],
|
|
340
|
+
never: NEVER_MERGES,
|
|
341
|
+
},
|
|
236
342
|
// ---------------------------------------------------------- Hear from users
|
|
237
343
|
{
|
|
238
344
|
id: "feedback-to-feature", name: "Feedback to feature", group: "users",
|
|
@@ -265,6 +371,48 @@ export const GALLERY = [
|
|
|
265
371
|
id: "triage", name: "Issue triage", group: "users", promise: "Jev sorts new issues into bugs, ideas and questions, and says how urgent each is.", needs: [SORTS], asks: [], definition: drawn("triage"), triggers: [],
|
|
266
372
|
does: [about("triage")], never: "Never closes an issue or replies without your approval.",
|
|
267
373
|
},
|
|
374
|
+
{
|
|
375
|
+
id: "metrics-digest", name: "Metrics digest", group: "users", tools: ["posthog"],
|
|
376
|
+
promise: "Each week, this week against last: signups, activation, retention and top events.",
|
|
377
|
+
needs: [], asks: [WEEKLY],
|
|
378
|
+
steps: [
|
|
379
|
+
{ id: "read", title: "Read the numbers", kind: "report", instructions: research("With PostHog, compare this week with last week. Read only. Give signups, activation, retention and the top 10 events, each with its change. Name the biggest change and its likely cause (a release, a campaign, a broken step), citing this week's commits where they fit.") },
|
|
380
|
+
send("Send the digest"),
|
|
381
|
+
],
|
|
382
|
+
triggers: [weekly("Metrics digest")],
|
|
383
|
+
does: ["Compares this week's PostHog numbers with last week's, {{ask.schedule-words}}.", "Names the biggest change and its likely cause.", "Sends you the digest."],
|
|
384
|
+
never: "Never changes PostHog or your code.",
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: "fix-drop-off", name: "Fix the biggest drop-off", group: "users", tools: ["posthog"],
|
|
388
|
+
promise: "Each week, the funnel step losing the most people gets a proposed fix.",
|
|
389
|
+
needs: ["GitHub"], asks: [WEEKLY],
|
|
390
|
+
steps: [
|
|
391
|
+
{ id: "find", title: "Find the drop-off", kind: "report", instructions: research("With PostHog, find the funnel step that lost the most people this week. Read only. Give the funnel, the step, how many reached it and how many left. Propose one fix in this code, citing files.") },
|
|
392
|
+
choose("Implement or ignore?", "Implement", "build"),
|
|
393
|
+
{ id: "build", title: "Build the fix", kind: "task", instructions: work("Build the proposed fix for this drop-off.\n\nThe report:\n{{stage.find}}") },
|
|
394
|
+
pr,
|
|
395
|
+
send("Send the result"),
|
|
396
|
+
],
|
|
397
|
+
triggers: [weekly("Biggest drop-off")],
|
|
398
|
+
does: ["Finds the PostHog funnel step losing the most people {{ask.schedule-words}}, with the numbers and one proposed fix.", "You choose Implement or Ignore.", "A fix is built as a task, under your usual approvals, opens a pull request and is sent to you."],
|
|
399
|
+
never: NEVER_MERGES,
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: "support-to-fix", name: "Support to bug fix", group: "users", tools: ["intercom"],
|
|
403
|
+
promise: "Each week, the bugs your customers hit most, ranked, with a fix for the one you pick.",
|
|
404
|
+
needs: ["GitHub"], asks: [WEEKLY],
|
|
405
|
+
steps: [
|
|
406
|
+
{ id: "group", title: "Rank the bugs", kind: "report", instructions: research("With Intercom, read this week's conversations. Read only. Group the bug reports by problem, rank them by how many people hit each, and number them, with a count and one example conversation each.") },
|
|
407
|
+
{ id: "choose", title: "Which to fix?", kind: "choose", options: [{ label: "Fix the top one", goesTo: "fix" }, { label: "Ignore", goesTo: "end" }], ifReplied: "fix" },
|
|
408
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix the bug chosen from this ranking: the top one, unless the note names another by number. Add a regression check where one fits.\n\nThe ranking:\n{{stage.group}}") },
|
|
409
|
+
pr,
|
|
410
|
+
send("Send the result"),
|
|
411
|
+
],
|
|
412
|
+
triggers: [weekly("Support bugs")],
|
|
413
|
+
does: ["Reads your Intercom conversations {{ask.schedule-words}} and ranks the bug reports by how many people hit each.", "You fix the top one, reply with the number of another, or ignore them.", "The fix is built as a task, under your usual approvals, opens a pull request and is sent to you."],
|
|
414
|
+
never: NEVER_MERGES,
|
|
415
|
+
},
|
|
268
416
|
// ------------------------------------------------------------------ Operations
|
|
269
417
|
...[
|
|
270
418
|
["research", "Research a question, have a person check it, then share it.", [], "Never shares an answer you haven't checked."],
|
|
@@ -281,6 +429,20 @@ export const GALLERY = [
|
|
|
281
429
|
/** The blank flow, for starting from nothing. */
|
|
282
430
|
export const BLANK = { id: "blank", name: "Blank flow", group: "ops", promise: about("blank"), needs: [], asks: [], definition: drawn("blank"), triggers: [], does: [about("blank")], never: "Does nothing until you add zones." };
|
|
283
431
|
export const galleryTemplateOf = (id) => id === BLANK.id ? BLANK : GALLERY.find(one => one.id === id) ?? null;
|
|
432
|
+
/** A tool's name ("Mobbin", "Figma (desktop app)"); null for an id that's neither a one-click service nor an app on this computer. */
|
|
433
|
+
export const galleryToolLabel = (id) => oneClickOf(id)?.label ?? localConnectOf(id)?.label ?? null;
|
|
434
|
+
/** The zones that use a tool: a tool step on it, or a research or build step that names it ("With Mobbin, …"). */
|
|
435
|
+
export function zonesUsing(definition, id) {
|
|
436
|
+
const label = galleryToolLabel(id);
|
|
437
|
+
if (label === null)
|
|
438
|
+
return [];
|
|
439
|
+
const word = new RegExp(`\\b${label.replace(/\s*\(.*\)$/, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
440
|
+
return definition.stages.filter(one => one.tool?.server === id || ((one.kind === "report" || one.kind === "task") && word.test(one.instructions ?? ""))).map(one => one.title);
|
|
441
|
+
}
|
|
442
|
+
/** A template's tools, each with its state in a project's connections (connectionsOf), or none without a project. */
|
|
443
|
+
export function galleryToolsOf(template, definition, connections) {
|
|
444
|
+
return (template.tools ?? []).map(id => ({ id, label: galleryToolLabel(id) ?? id, state: connections === null ? null : connections.find(one => one.id === id)?.state ?? "open", zones: zonesUsing(definition, id) }));
|
|
445
|
+
}
|
|
284
446
|
/** Each question's default for a project: the branch pull requests go to, when that's set up. */
|
|
285
447
|
export function galleryDefaults(store, template, repo) {
|
|
286
448
|
const publishing = repo === null ? { on: false } : publishingOf(store, repo);
|
|
@@ -296,12 +458,17 @@ const oneLine = (value, cap, what) => {
|
|
|
296
458
|
};
|
|
297
459
|
/** The optional last step every template offers, and the answer that turns it on. */
|
|
298
460
|
export const SEND_RESULT = { key: "send-result", title: "Send me the result", does: "When a card finishes, sends you what was done (its summary, links and screenshots) in your chat apps." };
|
|
299
|
-
/** A
|
|
461
|
+
/** A template that already sends its result to the person: it isn't offered "Send me the result" again. */
|
|
462
|
+
export const sendsAlready = (template) => galleryDiagram(template).stages.some(one => one.kind === "send");
|
|
463
|
+
/** A flow with "Send me the result" before its end: every path into its main Done zone passes through it first.
|
|
464
|
+
* A flow that already has a send zone is left as it is, so nobody gets the result twice. */
|
|
300
465
|
export function withSendResult(definition) {
|
|
466
|
+
if (definition.stages.some(one => one.kind === "send"))
|
|
467
|
+
return definition;
|
|
301
468
|
// Its main end: Done, else an end that isn't a filtered-out one (drawn in rose), else any end.
|
|
302
469
|
const ends = definition.stages.filter(one => one.kind === "done");
|
|
303
470
|
const end = ends.find(one => one.id === "done") ?? ends.find(one => one.zone.color !== "rose") ?? ends[0];
|
|
304
|
-
if (end === undefined
|
|
471
|
+
if (end === undefined)
|
|
305
472
|
return definition;
|
|
306
473
|
let id = "send-result";
|
|
307
474
|
for (let n = 2; definition.stages.some(one => one.id === id); n++)
|
|
@@ -352,6 +519,11 @@ export function checkAnswers(template, given) {
|
|
|
352
519
|
}
|
|
353
520
|
else if (ask.key === "command")
|
|
354
521
|
answers["command"] = oneLine(value, 500, "test command");
|
|
522
|
+
else if (ask.key === "team") {
|
|
523
|
+
if (!/^[A-Za-z0-9]{1,12}$/.test(value))
|
|
524
|
+
throw new Error("Say the Linear team as its short key, like ENG.");
|
|
525
|
+
answers["team"] = value.toUpperCase();
|
|
526
|
+
}
|
|
355
527
|
else if (ask.key === "outdated") {
|
|
356
528
|
const manager = OUTDATED_COMMANDS[value];
|
|
357
529
|
if (manager === undefined)
|
|
@@ -420,6 +592,9 @@ export function buildFromGallery(store, template, repo, given) {
|
|
|
420
592
|
const github = githubRepoOf(repo);
|
|
421
593
|
const drawn = template.definition !== undefined ? structuredClone(template.definition)
|
|
422
594
|
: flowFromSteps(fill(template.ownSteps !== undefined && isToolrollRepo(repo) ? template.ownSteps : template.steps, answers, names, github), null);
|
|
595
|
+
// One that already sends doesn't take the answer, so it reads as it was asked.
|
|
596
|
+
if (drawn.stages.some(one => one.kind === "send"))
|
|
597
|
+
delete answers[SEND_RESULT.key];
|
|
423
598
|
const sends = answers[SEND_RESULT.key] === "yes";
|
|
424
599
|
const definition = sends ? withSendResult(drawn) : drawn;
|
|
425
600
|
return { template, repo, answers, definition, triggers: fill(template.triggers, answers, names, github), scripts,
|
|
@@ -440,7 +615,7 @@ export function previewGallery(store, template, repo, given, actor, now) {
|
|
|
440
615
|
const steps = flowTerms(built.definition, null);
|
|
441
616
|
if (built.scripts.some(one => !one.existing))
|
|
442
617
|
steps.push(...built.scripts.filter(one => !one.existing).map(one => `Adds the ${one.name} script to this project: ${one.body.split("\n")[0]}${one.body.includes("\n") ? " …" : ""}`));
|
|
443
|
-
return { built, startsFrom, steps, digest: galleryDigest(template.id, repo, built.answers) };
|
|
618
|
+
return { built, startsFrom, steps, tools: galleryToolsOf(template, built.definition, connectionsOf(store, repo)), digest: galleryDigest(template.id, repo, built.answers) };
|
|
444
619
|
}
|
|
445
620
|
/** What a preview showed: the template, the project and the answers. Creating checks the person saw these. */
|
|
446
621
|
export function galleryDigest(id, repo, answers) {
|
package/dist/mcp-connect.d.ts
CHANGED
|
@@ -80,12 +80,13 @@ type Server = {
|
|
|
80
80
|
scopes: string[];
|
|
81
81
|
resource: string | null;
|
|
82
82
|
};
|
|
83
|
-
/** A sign-in on its way: what the callback needs to finish it (kept in the console's memory, 15 minutes). */
|
|
83
|
+
/** A sign-in on its way: what the callback needs to finish it (kept in the console's memory, 15 minutes). `kit` or `template` is the page it returns to. */
|
|
84
84
|
export type ConnectVisit = {
|
|
85
85
|
service: string;
|
|
86
86
|
repo: string;
|
|
87
87
|
by: string;
|
|
88
88
|
kit: string | null;
|
|
89
|
+
template: string | null;
|
|
89
90
|
verifier: string;
|
|
90
91
|
clientId: string;
|
|
91
92
|
clientSecret: string | null;
|
|
@@ -107,6 +108,7 @@ export declare function startConnect(input: {
|
|
|
107
108
|
by: string;
|
|
108
109
|
origin: string;
|
|
109
110
|
kit?: string | null;
|
|
111
|
+
template?: string | null;
|
|
110
112
|
}, fetcher?: Fetch, now?: number): Promise<{
|
|
111
113
|
ok: true;
|
|
112
114
|
go: string;
|
package/dist/mcp-connect.js
CHANGED
|
@@ -30,10 +30,13 @@ import { envValue } from "./names.js";
|
|
|
30
30
|
export const ONE_CLICK = [
|
|
31
31
|
{ id: "stripe", label: "Stripe", url: "https://mcp.stripe.com/", about: "Payments, customers, refunds and invoices." },
|
|
32
32
|
{ id: "notion", label: "Notion", url: "https://mcp.notion.com/mcp", about: "Pages and databases: search, read and write." },
|
|
33
|
-
{ id: "linear", label: "Linear", url: "https://mcp.linear.app/mcp", about: "Issues, projects and comments."
|
|
34
|
-
|
|
33
|
+
{ id: "linear", label: "Linear", url: "https://mcp.linear.app/mcp", about: "Issues, projects and comments.",
|
|
34
|
+
reads: ["issue", "project", "team", "comment", "label", "cycle", "document", "status", "statuses", "user"] },
|
|
35
|
+
{ id: "sentry", label: "Sentry", url: "https://mcp.sentry.dev/mcp", about: "Errors and issues, with their stack traces.",
|
|
36
|
+
reads: ["organization", "project", "issue", "detail", "details", "event", "trace", "release", "tag", "attachment"] },
|
|
35
37
|
{ id: "atlassian", label: "Jira and Confluence", url: "https://mcp.atlassian.com/v1/mcp", about: "Jira issues and Confluence pages." },
|
|
36
|
-
{ id: "intercom", label: "Intercom", url: "https://mcp.intercom.com/mcp", about: "Customer conversations and contacts."
|
|
38
|
+
{ id: "intercom", label: "Intercom", url: "https://mcp.intercom.com/mcp", about: "Customer conversations and contacts.",
|
|
39
|
+
reads: ["conversation", "contact"] },
|
|
37
40
|
{ id: "attio", label: "Attio", url: "https://mcp.attio.com/mcp", about: "Your CRM: people, companies, deals and notes." },
|
|
38
41
|
{ id: "zapier", label: "Zapier", url: "https://mcp.zapier.com/api/mcp/mcp", about: "Thousands of apps, through the actions you choose in Zapier." },
|
|
39
42
|
{ id: "square", label: "Square", url: "https://mcp.squareup.com/mcp", about: "Payments, orders, customers and catalog." },
|
|
@@ -42,8 +45,10 @@ export const ONE_CLICK = [
|
|
|
42
45
|
{ id: "webflow", label: "Webflow", url: "https://mcp.webflow.com/mcp", about: "Sites, pages and CMS items." },
|
|
43
46
|
{ id: "wix", label: "Wix", url: "https://mcp.wix.com/mcp", about: "Your Wix site, store and bookings." },
|
|
44
47
|
{ id: "canva", label: "Canva", url: "https://mcp.canva.com/mcp", about: "Designs: search, create and export." },
|
|
45
|
-
{ id: "vercel", label: "Vercel", url: "https://mcp.vercel.com/", about: "Projects, deployments and logs."
|
|
46
|
-
|
|
48
|
+
{ id: "vercel", label: "Vercel", url: "https://mcp.vercel.com/", about: "Projects, deployments and logs.",
|
|
49
|
+
reads: ["team", "project", "deployment", "build", "runtime", "log", "logs", "vercel", "documentation"] },
|
|
50
|
+
{ id: "cloudflare", label: "Cloudflare", url: "https://mcp.cloudflare.com/mcp", about: "Workers, DNS and your Cloudflare account.",
|
|
51
|
+
reads: ["account", "worker", "workers", "observability", "analytics", "log", "logs", "page", "pages", "project", "deployment"] },
|
|
47
52
|
// Mobbin's sign-in is its Supabase auth server, on another origin: its protected-resource metadata names it.
|
|
48
53
|
{ id: "mobbin", label: "Mobbin", url: "https://api.mobbin.com/mcp", about: "Real app screens and flows to learn from.",
|
|
49
54
|
reads: ["screen", "flow", "app", "site", "element", "pattern", "ui", "ios", "android", "web"] },
|
|
@@ -237,7 +242,7 @@ export async function startConnect(input, fetcher = fetch, now = Date.now()) {
|
|
|
237
242
|
go.searchParams.set("resource", resource);
|
|
238
243
|
if (server.scopes.length > 0)
|
|
239
244
|
go.searchParams.set("scope", server.scopes.join(" "));
|
|
240
|
-
return { ok: true, go: go.toString(), state, visit: { service: service.id, repo: input.repo, by: input.by, kit: input.kit ?? null, verifier, clientId, clientSecret, token: server.token, resource, redirect, expires: now + VISIT_MS } };
|
|
245
|
+
return { ok: true, go: go.toString(), state, visit: { service: service.id, repo: input.repo, by: input.by, kit: input.kit ?? null, template: input.template ?? null, verifier, clientId, clientSecret, token: server.token, resource, redirect, expires: now + VISIT_MS } };
|
|
241
246
|
}
|
|
242
247
|
/** The spec a connected service joins the project with: its MCP address, signed in with the token its sign-in gave. */
|
|
243
248
|
export function connectedSpec(serviceId) {
|
package/dist/serve.js
CHANGED
|
@@ -34,7 +34,7 @@ import { CONNECT_CALLBACK, connectionsOf, finishConnect, localConnectOf, oneClic
|
|
|
34
34
|
import { KITS_CSS, kitPageHtml, kitsGalleryHtml } from "./kits-ui.js";
|
|
35
35
|
import { STARTERS_CSS, startersHtml } from "./flow-starters-ui.js";
|
|
36
36
|
import { GALLERY_CSS, galleryHtml, galleryUseHtml } from "./flow-gallery-ui.js";
|
|
37
|
-
import { galleryDefaults, galleryDiagram, galleryTemplateOf, previewGallery, useGalleryTemplate } from "./flow-gallery.js";
|
|
37
|
+
import { galleryDefaults, galleryDiagram, galleryTemplateOf, galleryToolsOf, previewGallery, useGalleryTemplate } from "./flow-gallery.js";
|
|
38
38
|
import { starterForWork, starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
|
|
39
39
|
import { createTeammateFrom, labelOf, nameOf, saveSoul, setTeammateState, teammateSettings, sendTeammateSummaries } from "./teammate-admin.js";
|
|
40
40
|
import { editMemory, forgetMemory, tellTeammate } from "./teammate-memory.js";
|
|
@@ -1116,7 +1116,10 @@ export function createDecisionServer(options) {
|
|
|
1116
1116
|
return done("/settings/tools", "problem", "That sign-in expired. Connect again.");
|
|
1117
1117
|
const service = oneClickOf(visit.service);
|
|
1118
1118
|
const kit = visit.kit === null ? null : kitOf(visit.kit);
|
|
1119
|
-
|
|
1119
|
+
// Started from a gallery template's page: back to it, for the same project.
|
|
1120
|
+
const template = visit.template === null ? null : galleryTemplateOf(visit.template);
|
|
1121
|
+
const back = template !== null ? `/flows/new/${template.id}?repo=${encodeURIComponent(visit.repo)}`
|
|
1122
|
+
: kit === null ? `/settings/tools?repo=${encodeURIComponent(visit.repo)}` : `/kits/${kit.id}?repo=${encodeURIComponent(visit.repo)}`;
|
|
1120
1123
|
const to = projectName(visit.repo);
|
|
1121
1124
|
if (url.searchParams.has("error"))
|
|
1122
1125
|
return done(back, "problem", url.searchParams.get("error") === "access_denied" ? `${service.label} wasn't connected to ${to}: access was declined.` : `${service.label} wasn't connected to ${to}.`);
|
|
@@ -1748,7 +1751,7 @@ export function createDecisionServer(options) {
|
|
|
1748
1751
|
}
|
|
1749
1752
|
// ---- reads ---------------------------------------------------------------
|
|
1750
1753
|
/** A gallery template's page (Flows → New → Use this): its questions, the preview of these answers, and Create. */
|
|
1751
|
-
function sendGalleryUse(response, who, template, projects, repo, given, name, notice) {
|
|
1754
|
+
function sendGalleryUse(response, who, template, projects, repo, given, name, notice, said = null) {
|
|
1752
1755
|
const answers = given ?? galleryDefaults(store, template, repo);
|
|
1753
1756
|
let preview = null, problem = notice;
|
|
1754
1757
|
try {
|
|
@@ -1757,8 +1760,9 @@ export function createDecisionServer(options) {
|
|
|
1757
1760
|
catch (error) {
|
|
1758
1761
|
problem = error instanceof Error ? error.message : "That can't be made here.";
|
|
1759
1762
|
}
|
|
1763
|
+
const diagram = preview?.built.definition ?? galleryDiagram(template);
|
|
1760
1764
|
const html = galleryUseHtml({ template, projects: projects.map(path => ({ path, name: projectName(path) })), repo, answers, name, preview, problem,
|
|
1761
|
-
csrf: who.via === "cookie" ? who.session.csrf : "", diagram: preview?.
|
|
1765
|
+
csrf: who.via === "cookie" ? who.session.csrf : "", diagram, tools: preview?.tools ?? galleryToolsOf(template, diagram, connectionsOf(store, repo)), said });
|
|
1762
1766
|
return sendScreen(response, problem !== null && preview === null && notice !== null ? 400 : 200, screen(template.name, `<p><a href="/flows/new">New flow</a></p><h1>${escape(template.name)}</h1>${html}`, { chrome: chromeFor(repo, "flows") }));
|
|
1763
1767
|
}
|
|
1764
1768
|
async function handleGet(url, who, request, response) {
|
|
@@ -3188,8 +3192,10 @@ export function createDecisionServer(options) {
|
|
|
3188
3192
|
if (url.pathname === "/flows/new") {
|
|
3189
3193
|
const projects = [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])].filter(visible);
|
|
3190
3194
|
const canUse = who.via === "cookie" && who.role === "approver" && projects.length > 0;
|
|
3191
|
-
|
|
3192
|
-
|
|
3195
|
+
// The chosen project, else the one being looked at: its tools are what each card says is connected.
|
|
3196
|
+
const asked = url.searchParams.get("repo");
|
|
3197
|
+
const repo = asked !== null && projects.includes(asked) ? asked : project !== null && projects.includes(project) ? project : null;
|
|
3198
|
+
return sendScreen(response, 200, screen("New flow", `<p><a href="/flows">Flows</a></p><h1>New flow</h1>${galleryHtml({ repo, canUse, connections: repo === null ? null : connectionsOf(store, repo) })}`, { chrome: chromeFor(repo ?? project, "flows") }));
|
|
3193
3199
|
}
|
|
3194
3200
|
const galleryRead = /^\/flows\/new\/([a-z-]{1,40})$/.exec(url.pathname);
|
|
3195
3201
|
if (galleryRead !== null) {
|
|
@@ -3203,7 +3209,9 @@ export function createDecisionServer(options) {
|
|
|
3203
3209
|
return refuse(response, who, 409, "Add a project first.", "/projects");
|
|
3204
3210
|
const asked = url.searchParams.get("repo");
|
|
3205
3211
|
const repo = asked !== null && projects.includes(asked) ? asked : project !== null && projects.includes(project) ? project : projects[0];
|
|
3206
|
-
|
|
3212
|
+
// Back from a Connect started on this page: how it went.
|
|
3213
|
+
const problem = url.searchParams.get("problem"), said = url.searchParams.get("said");
|
|
3214
|
+
return sendGalleryUse(response, who, template, projects, repo, null, template.name, problem === null ? null : problem.slice(0, 400), said === null ? null : said.slice(0, 400));
|
|
3207
3215
|
}
|
|
3208
3216
|
if (url.pathname === "/flows") {
|
|
3209
3217
|
const projects = [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])].filter(visible);
|
|
@@ -3969,7 +3977,7 @@ export function createDecisionServer(options) {
|
|
|
3969
3977
|
said: url.searchParams.get("said"), problem: url.searchParams.get("problem")
|
|
3970
3978
|
}) +
|
|
3971
3979
|
// Every template, beside the starters: Use this opens its page for this project.
|
|
3972
|
-
`<h2 class="gallery-heading">Templates</h2>${galleryHtml({ repo: chosen, canUse: who.via === "cookie" && who.role === "approver" && !store.isDemo() })}`;
|
|
3980
|
+
`<h2 class="gallery-heading">Templates</h2>${galleryHtml({ repo: chosen, canUse: who.via === "cookie" && who.role === "approver" && !store.isDemo(), connections: connectionsOf(store, chosen) })}`;
|
|
3973
3981
|
return sendScreen(response, 200, screen("Flows", `<p><a href="/settings">Settings</a></p><h1>Flows</h1>${content}`, { chrome: chromeFor(chosen || project, "settings") }));
|
|
3974
3982
|
}
|
|
3975
3983
|
// Settings → Integrations: which integrations work. The list is the last checks; a render never waits on one.
|
|
@@ -7737,10 +7745,14 @@ export function createDecisionServer(options) {
|
|
|
7737
7745
|
const local = service === null && also === null ? localConnectOf(body.get("service") ?? "") : null;
|
|
7738
7746
|
const chosen = service ?? local;
|
|
7739
7747
|
const target = also === null ? repo : split > 0 ? also.slice(split + 1) : "";
|
|
7740
|
-
|
|
7748
|
+
// From a gallery template that uses this tool: it comes back to that template's page, for this project.
|
|
7749
|
+
const gallery = also === null && chosen !== null ? galleryTemplateOf(body.get("template") ?? "") : null;
|
|
7750
|
+
const template = gallery !== null && (gallery.tools ?? []).includes(chosen.id) ? gallery.id : null;
|
|
7751
|
+
const kit = template === null ? kitOf(body.get("kit") ?? "")?.id ?? null : null;
|
|
7741
7752
|
// A Connect that fails comes back to its own button, where the reason is said (no #fragment: one stops the alert's
|
|
7742
7753
|
// autofocus); an "Also connect to" refusal goes back to the Connect card.
|
|
7743
|
-
const back = (words, page = repo) => redirect(response, `/
|
|
7754
|
+
const back = (words, page = repo) => template !== null ? redirect(response, `/flows/new/${template}?repo=${encodeURIComponent(page)}&problem=${encodeURIComponent(words)}`)
|
|
7755
|
+
: redirect(response, `/settings/tools?repo=${encodeURIComponent(page)}${kit === null ? "" : `&kit=${kit}`}${chosen === null || also !== null ? "" : `&connect=${chosen.id}`}&problem=${encodeURIComponent(words)}${also === null ? "" : "#connect"}`);
|
|
7744
7756
|
// The page posts the project it showed; a post from a page showing another project is stale.
|
|
7745
7757
|
const shown = body.get("shown") ?? "";
|
|
7746
7758
|
if (shown !== repo)
|
|
@@ -7769,8 +7781,10 @@ export function createDecisionServer(options) {
|
|
|
7769
7781
|
return back(added.message);
|
|
7770
7782
|
}
|
|
7771
7783
|
const tested = await testToolOf(store, repo, local.id, clock(), { home: toolHome, omitEnv: ALL_CREDENTIAL_ENV });
|
|
7784
|
+
const connected = `${local.label} is connected: ${tested?.tools.length ?? 0} action${tested?.tools.length === 1 ? "" : "s"}.`;
|
|
7772
7785
|
if (tested?.ok)
|
|
7773
|
-
return redirect(response, `/
|
|
7786
|
+
return redirect(response, template !== null ? `/flows/new/${template}?repo=${encodeURIComponent(repo)}&said=${encodeURIComponent(connected)}`
|
|
7787
|
+
: `/settings/tools?repo=${encodeURIComponent(repo)}${kit === null ? "" : `&kit=${kit}`}&said=${encodeURIComponent(connected)}#tool-${local.id}`);
|
|
7774
7788
|
if (had === undefined)
|
|
7775
7789
|
removeToolFrom(store, repo, local.id, who.name, clock(), toolHome);
|
|
7776
7790
|
return back(tested?.problem ?? `${local.label} didn't answer.`);
|
|
@@ -7780,7 +7794,7 @@ export function createDecisionServer(options) {
|
|
|
7780
7794
|
const origin = consoleOrigin(request.headers.host);
|
|
7781
7795
|
if (origin === null)
|
|
7782
7796
|
return back("Connect tools from this computer (localhost) or from your https address.");
|
|
7783
|
-
const started = await startConnect({ service: service.id, repo: target, by: who.name, origin, kit: also === null ? kit : null }, options.connectFetch ?? fetch);
|
|
7797
|
+
const started = await startConnect({ service: service.id, repo: target, by: who.name, origin, kit: also === null ? kit : null, template }, options.connectFetch ?? fetch);
|
|
7784
7798
|
if (!started.ok)
|
|
7785
7799
|
return back(started.said);
|
|
7786
7800
|
for (const [key, visit] of connectVisits)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.31",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|