toolroll 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/dist/agent-onboard.d.ts +24 -1
- package/dist/agent-onboard.js +53 -1
- package/dist/backup-ui.js +4 -2
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +33 -32
- package/dist/browser-workspace.d.ts +22 -2
- package/dist/chat-actions.d.ts +5 -0
- package/dist/chat-actions.js +43 -3
- package/dist/chat-delivery.js +51 -3
- package/dist/chat-quiet.d.ts +28 -0
- package/dist/chat-quiet.js +161 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/digest-times.d.ts +3 -0
- package/dist/digest-times.js +7 -0
- package/dist/dispose.js +2 -0
- package/dist/export.js +1 -1
- package/dist/flow-engine.js +13 -1
- package/dist/flow-pull-request.d.ts +11 -0
- package/dist/flow-pull-request.js +123 -0
- package/dist/flow-starters-ui.d.ts +20 -0
- package/dist/flow-starters-ui.js +35 -0
- package/dist/flow-starters.d.ts +55 -0
- package/dist/flow-starters.js +104 -0
- package/dist/flow-steps.js +14 -0
- package/dist/flows-ui.d.ts +1 -1
- package/dist/flows-ui.js +4 -3
- package/dist/flows.d.ts +30 -6
- package/dist/flows.js +110 -6
- package/dist/integrations-ui.d.ts +13 -0
- package/dist/integrations-ui.js +70 -0
- package/dist/integrations.d.ts +113 -0
- package/dist/integrations.js +526 -0
- package/dist/lead-context.d.ts +1 -1
- package/dist/lead-context.js +1 -1
- package/dist/mate-contract.js +1 -1
- package/dist/mate-tools.js +18 -5
- package/dist/models-ui.js +3 -2
- package/dist/monitoring-ui.js +2 -1
- package/dist/operate.d.ts +5 -2
- package/dist/operate.js +169 -4
- package/dist/policy-ui.js +2 -1
- package/dist/project-delete.js +1 -0
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +9 -2
- package/dist/pull-request-flow.d.ts +172 -0
- package/dist/pull-request-flow.js +475 -0
- package/dist/pull-request-schema.d.ts +5 -0
- package/dist/pull-request-schema.js +21 -0
- package/dist/pull-request-ui.d.ts +23 -0
- package/dist/pull-request-ui.js +55 -0
- package/dist/pulls.d.ts +5 -0
- package/dist/pulls.js +11 -0
- package/dist/serve.d.ts +6 -0
- package/dist/serve.js +371 -58
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +2 -1
- package/dist/store.d.ts +61 -0
- package/dist/store.js +144 -3
- package/dist/surface.js +3 -1
- package/dist/telegram-progress.d.ts +1 -0
- package/dist/telegram-progress.js +1 -1
- package/dist/telegram.js +95 -15
- package/dist/when-html.d.ts +8 -0
- package/dist/when-html.js +36 -0
- package/package.json +1 -1
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings → Integrations and `toolroll integrations`: one place that says
|
|
3
|
+
* which integrations work. Each is Connected, Not set up or Broken, with its
|
|
4
|
+
* last success and last error, what uses it, and one action (Set up, Send
|
|
5
|
+
* test or Fix).
|
|
6
|
+
*
|
|
7
|
+
* Two halves. `integrationsNow` is cheap and synchronous: configuration files,
|
|
8
|
+
* delivery history already in SQLite, and the last check's answer from
|
|
9
|
+
* `integration_check`. It never touches the network, so a page can call it on
|
|
10
|
+
* every render. `checkIntegrations` runs the checks themselves: a harmless
|
|
11
|
+
* read-only call per integration (never a message to anyone, never a write to
|
|
12
|
+
* a repository) with a short timeout, and records each answer. The console
|
|
13
|
+
* runs them in the background through `createIntegrationMonitor`; the CLI
|
|
14
|
+
* runs them inline. Only account and workspace names leave this module:
|
|
15
|
+
* tokens, keys and passwords are never in a row, and every problem text is
|
|
16
|
+
* scrubbed before it is stored.
|
|
17
|
+
*/
|
|
18
|
+
import { connect as tcpConnect } from "node:net";
|
|
19
|
+
import { connect as tlsConnect } from "node:tls";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { basename } from "node:path";
|
|
22
|
+
import { ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
23
|
+
import { providerName, signInCommand } from "./provider-auth.js";
|
|
24
|
+
import { loadBotToken } from "./telegram.js";
|
|
25
|
+
import { loadSlackCredentials } from "./slack-api.js";
|
|
26
|
+
import { loadDiscordCredentials } from "./discord-api.js";
|
|
27
|
+
import { loadTeamsCredentials, teamsAccessToken } from "./teams-api.js";
|
|
28
|
+
import { effectivePrimary } from "./webhooks.js";
|
|
29
|
+
import { readEmailSettings } from "./email-settings.js";
|
|
30
|
+
import { googleConnected } from "./google-mail.js";
|
|
31
|
+
import { LINEAR_URL, readHooksBase, readLinearKey } from "./flow-triggers.js";
|
|
32
|
+
import { projectToolsOf, readToolSecrets, missingSecrets, testToolOf } from "./project-tools.js";
|
|
33
|
+
import { origin, readMonitoring } from "./monitoring-settings.js";
|
|
34
|
+
import { targetOf } from "./monitoring.js";
|
|
35
|
+
export const STATE_WORDS = { connected: "Connected", "not-set-up": "Not set up", broken: "Broken" };
|
|
36
|
+
const NO_ACTIVITY = { okAt: null, error: null, errorAt: null, failing: false };
|
|
37
|
+
const PROBE_MS = 8_000;
|
|
38
|
+
const AGENTS = ["claude", "codex"];
|
|
39
|
+
// ---------------------------------------------------------------- secrets
|
|
40
|
+
/** Token shapes that must never reach a page, a log line or --json, whatever an upstream error message says. */
|
|
41
|
+
const SECRET_SHAPES = [
|
|
42
|
+
/\b\d{5,}:[A-Za-z0-9_-]{30,}\b/g, // Telegram bot token
|
|
43
|
+
/\bx(?:ox[abposr]|app)-[A-Za-z0-9-]{8,}\b/g, // Slack tokens
|
|
44
|
+
/\blin_(?:api|oauth)_[A-Za-z0-9]{12,}\b/g, // Linear keys
|
|
45
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, // GitHub tokens
|
|
46
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
47
|
+
/\bsk-[A-Za-z0-9_-]{16,}\b/g, // provider API keys
|
|
48
|
+
/\bBearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi,
|
|
49
|
+
/\b[A-Za-z0-9_-]{24,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{20,}\b/g, // Discord bot token, JWTs
|
|
50
|
+
/\/\/[^/\s:@]+:[^/\s@]+@/g, // user:password@ in an address
|
|
51
|
+
];
|
|
52
|
+
/** Remove every known secret value and anything shaped like a token; keep it to one short line. */
|
|
53
|
+
export function scrubIntegrationText(text, secrets = []) {
|
|
54
|
+
let out = text;
|
|
55
|
+
for (const secret of secrets)
|
|
56
|
+
if (secret !== null && secret !== undefined && secret.length >= 6)
|
|
57
|
+
out = out.split(secret).join("[hidden]");
|
|
58
|
+
for (const shape of SECRET_SHAPES)
|
|
59
|
+
out = out.replace(shape, match => (match.startsWith("//") ? "//[hidden]@" : "[hidden]"));
|
|
60
|
+
out = out.replace(/[\p{Cc}\s]+/gu, " ").trim();
|
|
61
|
+
return out.length > 240 ? `${out.slice(0, 239).trimEnd()}…` : out;
|
|
62
|
+
}
|
|
63
|
+
const str = (value) => (value === null || value === undefined ? null : String(value));
|
|
64
|
+
export function readIntegrationChecks(store) {
|
|
65
|
+
if (!tableExists(store, "integration_check"))
|
|
66
|
+
return new Map();
|
|
67
|
+
const rows = store.handle.prepare("SELECT * FROM integration_check").all();
|
|
68
|
+
return new Map(rows.map(row => [String(row["key"]), {
|
|
69
|
+
key: String(row["key"]), outcome: String(row["outcome"]), account: str(row["account"]), detail: str(row["detail"]), problem: str(row["problem"]),
|
|
70
|
+
checkedAt: String(row["checked_at"]), okAt: str(row["ok_at"]), error: str(row["error"]), errorAt: str(row["error_at"]),
|
|
71
|
+
}]));
|
|
72
|
+
}
|
|
73
|
+
export function recordIntegrationCheck(store, key, result, now) {
|
|
74
|
+
const at = now.toISOString();
|
|
75
|
+
const account = result.outcome === "absent" ? null : result.account ?? null;
|
|
76
|
+
const detail = result.outcome === "absent" ? null : result.detail ?? null;
|
|
77
|
+
const problem = result.outcome === "ok" ? null : scrubIntegrationText(result.problem);
|
|
78
|
+
store.handle.prepare(`INSERT INTO integration_check (key, outcome, account, detail, problem, checked_at, ok_at, error, error_at)
|
|
79
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
80
|
+
ON CONFLICT(key) DO UPDATE SET outcome = excluded.outcome, account = excluded.account, detail = excluded.detail, problem = excluded.problem,
|
|
81
|
+
checked_at = excluded.checked_at, ok_at = COALESCE(excluded.ok_at, integration_check.ok_at),
|
|
82
|
+
error = COALESCE(excluded.error, integration_check.error), error_at = COALESCE(excluded.error_at, integration_check.error_at)`)
|
|
83
|
+
.run(key, result.outcome, account, detail, problem, at, result.outcome === "ok" ? at : null, result.outcome === "failed" ? problem : null, result.outcome === "failed" ? at : null);
|
|
84
|
+
}
|
|
85
|
+
const later = (a, b) => (a === null ? b : b === null ? a : a > b ? a : b);
|
|
86
|
+
function tableExists(store, table) {
|
|
87
|
+
return store.handle.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) !== undefined;
|
|
88
|
+
}
|
|
89
|
+
function telegramActivity(store) {
|
|
90
|
+
const ok = store.handle.prepare("SELECT MAX(delivered_at) AS at FROM notification_delivery WHERE destination LIKE 'telegram:%' AND delivered_at IS NOT NULL").get();
|
|
91
|
+
const bad = store.handle.prepare("SELECT last_error, last_attempt_at FROM notification_delivery WHERE destination LIKE 'telegram:%' AND last_error IS NOT NULL AND delivered_at IS NULL ORDER BY last_attempt_at DESC LIMIT 1").get();
|
|
92
|
+
const okAt = str(ok?.["at"]), errorAt = str(bad?.["last_attempt_at"]);
|
|
93
|
+
return { okAt, error: str(bad?.["last_error"]), errorAt, failing: errorAt !== null && (okAt === null || errorAt > okAt) };
|
|
94
|
+
}
|
|
95
|
+
function chatActivity(store, channel, installation) {
|
|
96
|
+
const runtime = `${channel}_runtime`, part = `${channel}_part`, event = `${channel}_event`;
|
|
97
|
+
if (!tableExists(store, runtime))
|
|
98
|
+
return NO_ACTIVITY;
|
|
99
|
+
const row = store.handle.prepare(`SELECT connected, problem, retry_at FROM ${runtime} WHERE installation = ?`).get(installation);
|
|
100
|
+
const sent = store.handle.prepare(`SELECT MAX(p.created) AS at FROM ${part} p JOIN ${event} e ON e.id = p.event WHERE e.installation = ? AND p.state = 'sent'`).get(installation);
|
|
101
|
+
const dropped = store.handle.prepare(`SELECT p.problem, p.created FROM ${part} p JOIN ${event} e ON e.id = p.event WHERE e.installation = ? AND p.state = 'dropped' AND p.problem IS NOT NULL ORDER BY p.id DESC LIMIT 1`).get(installation);
|
|
102
|
+
const okAt = later(str(row?.["connected"]), str(sent?.["at"]));
|
|
103
|
+
const runtimeProblem = str(row?.["problem"]);
|
|
104
|
+
const error = runtimeProblem ?? str(dropped?.["problem"]);
|
|
105
|
+
const errorAt = runtimeProblem !== null ? str(row?.["retry_at"]) ?? okAt : str(dropped?.["created"]);
|
|
106
|
+
return { okAt, error, errorAt, failing: runtimeProblem !== null };
|
|
107
|
+
}
|
|
108
|
+
function triggerUses(store) {
|
|
109
|
+
return store.handle.prepare(`SELECT t.kind, t.config_json, t.last_at, t.last_outcome, t.failures, f.name FROM flow_trigger t JOIN flow f ON f.id = t.flow
|
|
110
|
+
WHERE t.state = 'active' AND f.state = 'active' ORDER BY t.id`).all().map(row => {
|
|
111
|
+
let config = {};
|
|
112
|
+
try {
|
|
113
|
+
config = JSON.parse(String(row["config_json"]));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
config = {};
|
|
117
|
+
}
|
|
118
|
+
return { kind: String(row["kind"]), app: typeof config["app"] === "string" ? config["app"] : null, flow: String(row["name"]), delivery: typeof config["delivery"] === "string" ? config["delivery"] : null,
|
|
119
|
+
lastAt: str(row["last_at"]), lastOutcome: str(row["last_outcome"]), failures: Number(row["failures"] ?? 0) };
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function triggerActivity(uses) {
|
|
123
|
+
let activity = NO_ACTIVITY;
|
|
124
|
+
for (const one of uses) {
|
|
125
|
+
if (one.lastAt === null)
|
|
126
|
+
continue;
|
|
127
|
+
if (one.failures > 0) {
|
|
128
|
+
if (activity.errorAt === null || one.lastAt > activity.errorAt)
|
|
129
|
+
activity = { ...activity, error: one.lastOutcome ?? "The last check failed", errorAt: one.lastAt, failing: true };
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
activity = { ...activity, okAt: later(activity.okAt, one.lastAt) };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return activity;
|
|
136
|
+
}
|
|
137
|
+
function merge(a, b) {
|
|
138
|
+
const aNewer = (a.errorAt ?? "") >= (b.errorAt ?? "");
|
|
139
|
+
return { okAt: later(a.okAt, b.okAt), error: aNewer ? a.error : b.error, errorAt: later(a.errorAt, b.errorAt), failing: a.failing || b.failing };
|
|
140
|
+
}
|
|
141
|
+
const plural = (count, one) => `${count} ${one}${count === 1 ? "" : "s"}`;
|
|
142
|
+
function flowWords(uses) {
|
|
143
|
+
const flows = [...new Set(uses.map(one => one.flow))];
|
|
144
|
+
return flows.length === 0 ? [] : [flows.length <= 2 ? `Flow${flows.length === 1 ? "" : "s"} ${flows.join(", ")}` : plural(flows.length, "flow")];
|
|
145
|
+
}
|
|
146
|
+
// ---------------------------------------------------------------- probes
|
|
147
|
+
const place = (href, command = null) => ({ href, command });
|
|
148
|
+
async function getJson(io, url, init = {}) {
|
|
149
|
+
const response = await (io.fetch ?? fetch)(url, { redirect: "error", signal: AbortSignal.timeout(PROBE_MS), ...init });
|
|
150
|
+
let body = {};
|
|
151
|
+
try {
|
|
152
|
+
const parsed = await response.json();
|
|
153
|
+
if (parsed !== null && typeof parsed === "object")
|
|
154
|
+
body = parsed;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
body = {};
|
|
158
|
+
}
|
|
159
|
+
return { status: response.status, body };
|
|
160
|
+
}
|
|
161
|
+
const unreachable = (service) => `Couldn't reach ${service}. Check this computer's internet connection, then send a test again.`;
|
|
162
|
+
async function probeTelegram(io, token) {
|
|
163
|
+
try {
|
|
164
|
+
const { status, body } = await getJson(io, `https://api.telegram.org/bot${token}/getMe`);
|
|
165
|
+
const result = body["result"];
|
|
166
|
+
if (body["ok"] === true && typeof result?.["username"] === "string")
|
|
167
|
+
return { outcome: "ok", account: `@${result["username"]}` };
|
|
168
|
+
if (status === 401 || status === 404)
|
|
169
|
+
return { outcome: "failed", problem: "Telegram doesn't accept the saved bot token. Paste a new one from @BotFather." };
|
|
170
|
+
return { outcome: "failed", problem: `Telegram answered with an error (${status}). Try again in a minute.` };
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return { outcome: "failed", problem: unreachable("Telegram") };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function probeSlack(io, botToken) {
|
|
177
|
+
try {
|
|
178
|
+
const { body } = await getJson(io, "https://slack.com/api/auth.test", { method: "POST", headers: { Authorization: `Bearer ${botToken}`, "Content-Type": "application/json; charset=utf-8" }, body: "{}" });
|
|
179
|
+
if (body["ok"] === true)
|
|
180
|
+
return { outcome: "ok", account: typeof body["team"] === "string" ? body["team"] : null };
|
|
181
|
+
const code = typeof body["error"] === "string" ? body["error"] : "unknown";
|
|
182
|
+
if (["invalid_auth", "not_authed", "account_inactive", "token_revoked", "token_expired"].includes(code))
|
|
183
|
+
return { outcome: "failed", problem: "Slack no longer accepts the app's bot token. Reinstall the app in Slack and connect it again." };
|
|
184
|
+
return { outcome: "failed", problem: `Slack said "${code}". Connect the app again if this keeps happening.` };
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return { outcome: "failed", problem: unreachable("Slack") };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function probeDiscord(io, botToken) {
|
|
191
|
+
try {
|
|
192
|
+
const { status, body } = await getJson(io, "https://discord.com/api/v10/users/@me", { headers: { Authorization: `Bot ${botToken}` } });
|
|
193
|
+
if (status === 200 && typeof body["id"] === "string")
|
|
194
|
+
return { outcome: "ok" };
|
|
195
|
+
if (status === 401)
|
|
196
|
+
return { outcome: "failed", problem: "Discord no longer accepts the bot token. Reset it in the Discord developer portal and connect again." };
|
|
197
|
+
return { outcome: "failed", problem: `Discord answered with an error (${status}). Try again in a minute.` };
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return { outcome: "failed", problem: unreachable("Discord") };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function probeTeams(io, credentials) {
|
|
204
|
+
try {
|
|
205
|
+
await teamsAccessToken(credentials, io.fetch ?? fetch);
|
|
206
|
+
return { outcome: "ok" };
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
const said = error instanceof Error ? error.message : "";
|
|
210
|
+
return { outcome: "failed", problem: /secret|credential|unauthori[sz]ed|invalid/i.test(said)
|
|
211
|
+
? "Microsoft doesn't accept the app's client secret. Make a new secret in Azure and connect Teams again."
|
|
212
|
+
: unreachable("Microsoft Teams") };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function probeGithub(io, needed) {
|
|
216
|
+
const gh = io.gh;
|
|
217
|
+
if (gh === undefined)
|
|
218
|
+
return { outcome: "failed", problem: "GitHub can't be checked here." };
|
|
219
|
+
const user = await gh("gh", ["api", "user", "--jq", ".login"], { timeoutMs: PROBE_MS, maxBuffer: 16 * 1024 }).catch(() => null);
|
|
220
|
+
if (user === null || user.notFound) {
|
|
221
|
+
const problem = "The GitHub CLI (gh) isn't installed on this computer.";
|
|
222
|
+
return needed ? { outcome: "failed", problem: `${problem} Install it, then run gh auth login.`, fix: { command: "gh auth login" } } : { outcome: "absent", problem, setup: { command: "gh auth login" } };
|
|
223
|
+
}
|
|
224
|
+
if (user.timedOut)
|
|
225
|
+
return { outcome: "failed", problem: unreachable("GitHub") };
|
|
226
|
+
const login = user.stdout.trim();
|
|
227
|
+
if (user.code !== 0 || !/^[A-Za-z0-9-]{1,39}$/.test(login)) {
|
|
228
|
+
const signedOut = /auth login|not logged|authenticat/i.test(`${user.stderr}\n${user.stdout}`);
|
|
229
|
+
if (signedOut && !needed)
|
|
230
|
+
return { outcome: "absent", problem: "gh isn't signed in to GitHub.", setup: { command: "gh auth login" } };
|
|
231
|
+
return { outcome: "failed", problem: signedOut ? "gh isn't signed in to GitHub. Run gh auth login." : unreachable("GitHub"), fix: { command: "gh auth login" } };
|
|
232
|
+
}
|
|
233
|
+
// Push rights per project: a read of the repository's own permission for this account, never a write.
|
|
234
|
+
const push = [], noPush = [];
|
|
235
|
+
for (const repo of io.repos) {
|
|
236
|
+
const viewed = await gh("gh", ["repo", "view", "--json", "nameWithOwner,viewerPermission"], { cwd: repo, timeoutMs: PROBE_MS, maxBuffer: 16 * 1024 }).catch(() => null);
|
|
237
|
+
if (viewed === null || viewed.code !== 0)
|
|
238
|
+
continue;
|
|
239
|
+
try {
|
|
240
|
+
const value = JSON.parse(viewed.stdout);
|
|
241
|
+
const name = typeof value.nameWithOwner === "string" ? value.nameWithOwner : basename(repo);
|
|
242
|
+
(["ADMIN", "MAINTAIN", "WRITE"].includes(String(value.viewerPermission)) ? push : noPush).push(name);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const detail = [push.length === 0 ? null : `Can push to ${push.join(", ")}`, noPush.length === 0 ? null : `can't push to ${noPush.join(", ")}`].filter(Boolean).join("; ") || null;
|
|
249
|
+
if (noPush.length > 0)
|
|
250
|
+
return { outcome: "failed", account: login, detail, problem: `${login} can't push to ${noPush.join(", ")}. Ask for write access on GitHub, or sign gh in as an account that has it.`, fix: { command: "gh auth login" } };
|
|
251
|
+
return { outcome: "ok", account: login, detail };
|
|
252
|
+
}
|
|
253
|
+
async function probeLinear(io, key) {
|
|
254
|
+
try {
|
|
255
|
+
const { status, body } = await getJson(io, LINEAR_URL, { method: "POST", headers: { Authorization: key, "Content-Type": "application/json" }, body: JSON.stringify({ query: "{ viewer { name organization { name } } }" }) });
|
|
256
|
+
const viewer = body["data"]?.viewer;
|
|
257
|
+
if (status === 200 && viewer !== undefined && viewer !== null)
|
|
258
|
+
return { outcome: "ok", account: typeof viewer.organization?.name === "string" ? viewer.organization.name : null };
|
|
259
|
+
if (status === 400 || status === 401 || status === 403)
|
|
260
|
+
return { outcome: "failed", problem: "Linear doesn't accept the saved API key. Make a new one in Linear → Settings → Security & access, and save it on a flow's Linear trigger." };
|
|
261
|
+
return { outcome: "failed", problem: `Linear answered with an error (${status}). Try again in a minute.` };
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return { outcome: "failed", problem: unreachable("Linear") };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** Whether something answers at host:port: a connection opened and closed, no login and no message. */
|
|
268
|
+
export function reachServer(host, port, secure) {
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
const socket = secure ? tlsConnect({ host, port, servername: host, timeout: PROBE_MS }) : tcpConnect({ host, port, timeout: PROBE_MS });
|
|
271
|
+
const done = (error) => { socket.removeAllListeners(); socket.destroy(); if (error === undefined)
|
|
272
|
+
resolve();
|
|
273
|
+
else
|
|
274
|
+
reject(error); };
|
|
275
|
+
socket.once(secure ? "secureConnect" : "connect", () => done());
|
|
276
|
+
socket.once("timeout", () => done(new Error("timed out")));
|
|
277
|
+
socket.once("error", error => done(error));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async function probeEmail(io, smtp, imap) {
|
|
281
|
+
const reach = io.reach ?? reachServer;
|
|
282
|
+
const failed = [];
|
|
283
|
+
if (smtp !== null)
|
|
284
|
+
await reach(smtp.host, smtp.port, smtp.secure).catch(() => failed.push(`the outgoing mail server ${smtp.host}:${smtp.port}`));
|
|
285
|
+
if (imap !== null)
|
|
286
|
+
await reach(imap.host, imap.port, imap.secure).catch(() => failed.push(`the incoming mail server ${imap.host}:${imap.port}`));
|
|
287
|
+
const detail = [smtp === null ? null : "Sending", imap === null ? null : "reading"].filter(Boolean).join(" and ") || null;
|
|
288
|
+
if (failed.length > 0)
|
|
289
|
+
return { outcome: "failed", detail, problem: `Couldn't reach ${failed.join(" or ")}. Check the server name and port in Settings → Email.` };
|
|
290
|
+
return { outcome: "ok", detail: detail === null ? null : `${detail} reachable` };
|
|
291
|
+
}
|
|
292
|
+
async function probeAgent(io, provider) {
|
|
293
|
+
if (io.checkConnection === undefined)
|
|
294
|
+
return { outcome: "failed", problem: `${providerName(provider)} can't be checked here.` };
|
|
295
|
+
let connection;
|
|
296
|
+
try {
|
|
297
|
+
connection = await io.checkConnection(provider);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return { outcome: "failed", problem: `Couldn't check ${providerName(provider)}'s sign-in.` };
|
|
301
|
+
}
|
|
302
|
+
const name = providerName(provider);
|
|
303
|
+
const account = connection.email ?? connection.method ?? null;
|
|
304
|
+
const detail = connection.plan ?? null;
|
|
305
|
+
const command = signInCommand({ provider, authMode: connection.mode });
|
|
306
|
+
switch (connection.state) {
|
|
307
|
+
case "connected":
|
|
308
|
+
case "key-works":
|
|
309
|
+
case "key-present": return { outcome: "ok", account, detail };
|
|
310
|
+
case "not-installed": return { outcome: "absent", problem: `The ${name} CLI isn't installed on this computer.` };
|
|
311
|
+
case "signed-out": return { outcome: "failed", account, problem: `${name} isn't signed in. Run ${command}.`, fix: { command } };
|
|
312
|
+
case "missing-key": return { outcome: "failed", problem: `${name} is set to use an API key, and none is saved. Run ${command}.`, fix: { command } };
|
|
313
|
+
case "key-refused": return { outcome: "failed", problem: `${name} refused the saved API key. Save a new one with ${command}.`, fix: { command } };
|
|
314
|
+
default: return { outcome: "ok", account, detail: "Signed in; the account couldn't be read" };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// ---------------------------------------------------------------- targets
|
|
318
|
+
function targets(io) {
|
|
319
|
+
const { store, dir, env } = io;
|
|
320
|
+
const uses = triggerUses(store);
|
|
321
|
+
const telegram = io.telegramTokenFile === null ? null : loadBotToken(env, io.telegramTokenFile);
|
|
322
|
+
const slack = dir === null ? null : loadSlackCredentials(dir);
|
|
323
|
+
const discord = dir === null ? null : loadDiscordCredentials(dir);
|
|
324
|
+
const teams = dir === null ? null : loadTeamsCredentials(dir);
|
|
325
|
+
const primary = dir === null ? null : effectivePrimary(env, dir, telegram !== null).channel;
|
|
326
|
+
const chatUses = (app) => [...(primary === app ? ["Alerts"] : []), ...flowWords(uses.filter(one => one.kind === "chat" && one.app === app))];
|
|
327
|
+
const list = [];
|
|
328
|
+
list.push({
|
|
329
|
+
key: "telegram", group: "chat", name: "Telegram", configured: telegram !== null, account: null,
|
|
330
|
+
setup: place("/settings#telegram-token"), fix: place("/settings#telegram-token"), usedBy: chatUses("telegram"), activity: telegramActivity(store),
|
|
331
|
+
probe: telegram === null ? null : check => probeTelegram(check, telegram.token),
|
|
332
|
+
});
|
|
333
|
+
list.push({
|
|
334
|
+
key: "slack", group: "chat", name: "Slack", configured: slack !== null, account: slack?.workspace ?? null,
|
|
335
|
+
setup: place("/settings/slack"), fix: place("/settings/slack"), usedBy: chatUses("slack"), activity: slack === null ? NO_ACTIVITY : chatActivity(store, "slack", slack.installation),
|
|
336
|
+
probe: slack === null ? null : check => probeSlack(check, slack.botToken),
|
|
337
|
+
});
|
|
338
|
+
list.push({
|
|
339
|
+
key: "discord", group: "chat", name: "Discord", configured: discord !== null, account: discord?.workspace ?? null,
|
|
340
|
+
setup: place("/settings/discord"), fix: place("/settings/discord"), usedBy: chatUses("discord"), activity: discord === null ? NO_ACTIVITY : chatActivity(store, "discord", discord.installation),
|
|
341
|
+
probe: discord === null ? null : check => probeDiscord(check, discord.botToken),
|
|
342
|
+
});
|
|
343
|
+
list.push({
|
|
344
|
+
key: "teams", group: "chat", name: "Microsoft Teams", configured: teams !== null, account: teams === null ? null : "Microsoft Teams",
|
|
345
|
+
setup: place("/settings/teams"), fix: place("/settings/teams"), usedBy: chatUses("teams"), activity: teams === null ? NO_ACTIVITY : chatActivity(store, "teams", teams.installation),
|
|
346
|
+
probe: teams === null ? null : check => probeTeams(check, teams),
|
|
347
|
+
});
|
|
348
|
+
// GitHub: always worth checking (gh is how results become pull requests); webhook deliveries count when public.
|
|
349
|
+
const githubUses = uses.filter(one => one.kind === "github");
|
|
350
|
+
const hooksPublic = readHooksBase(dir) !== null;
|
|
351
|
+
const githubActivity = triggerActivity(githubUses.filter(one => one.delivery !== "webhook" || hooksPublic));
|
|
352
|
+
list.push({
|
|
353
|
+
key: "github", group: "code", name: "GitHub", configured: true, account: null,
|
|
354
|
+
setup: place(null, "gh auth login"), fix: place(null, "gh auth login"),
|
|
355
|
+
usedBy: ["Pull requests", ...flowWords(githubUses), ...(hooksPublic && githubUses.some(one => one.delivery === "webhook") ? ["Webhooks"] : [])], activity: githubActivity,
|
|
356
|
+
probe: check => probeGithub(check, githubUses.length > 0),
|
|
357
|
+
});
|
|
358
|
+
const linearKey = readLinearKey(dir);
|
|
359
|
+
const linearUses = uses.filter(one => one.kind === "linear");
|
|
360
|
+
list.push({
|
|
361
|
+
key: "linear", group: "code", name: "Linear", configured: linearKey !== null, account: null,
|
|
362
|
+
setup: place("/flows"), fix: place("/flows"), usedBy: flowWords(linearUses), activity: triggerActivity(linearUses),
|
|
363
|
+
probe: linearKey === null ? null : check => probeLinear(check, linearKey),
|
|
364
|
+
});
|
|
365
|
+
const email = readEmailSettings(dir);
|
|
366
|
+
const google = googleConnected(dir);
|
|
367
|
+
const emailUses = uses.filter(one => one.kind === "email");
|
|
368
|
+
const mailWatch = tableExists(store, "flow_mail_watch") ? store.handle.prepare("SELECT failures, last_outcome, updated_at FROM flow_mail_watch WHERE id = 1").get() : undefined;
|
|
369
|
+
const watchActivity = mailWatch === undefined ? NO_ACTIVITY : Number(mailWatch["failures"] ?? 0) > 0
|
|
370
|
+
? { okAt: null, error: str(mailWatch["last_outcome"]), errorAt: str(mailWatch["updated_at"]), failing: true }
|
|
371
|
+
: { okAt: str(mailWatch["updated_at"]), error: null, errorAt: null, failing: false };
|
|
372
|
+
list.push({
|
|
373
|
+
key: "email", group: "mail", name: "Email", configured: email !== null || google !== null, account: email?.from ?? google?.address ?? null,
|
|
374
|
+
setup: place("/settings#email"), fix: place("/settings#email"), usedBy: [...flowWords(emailUses)], activity: merge(triggerActivity(emailUses), watchActivity),
|
|
375
|
+
probe: email === null ? null : check => probeEmail(check, { host: email.host, port: email.port, secure: email.secure }, email.imap),
|
|
376
|
+
});
|
|
377
|
+
// Each project's MCP tools: the server starts and lists its tools.
|
|
378
|
+
const toolHome = io.toolHome ?? homedir();
|
|
379
|
+
let anyTool = false;
|
|
380
|
+
for (const repo of io.repos) {
|
|
381
|
+
for (const tool of projectToolsOf(store, repo)) {
|
|
382
|
+
anyTool = true;
|
|
383
|
+
const href = `/settings/tools?repo=${encodeURIComponent(repo)}#tool-${encodeURIComponent(tool.name)}`;
|
|
384
|
+
const test = tool.lastTest;
|
|
385
|
+
list.push({
|
|
386
|
+
key: `mcp:${repo}:${tool.name}`, group: "tools", name: tool.name, configured: true, account: basename(repo),
|
|
387
|
+
setup: place(href), fix: place(href), usedBy: [`Tasks in ${basename(repo)}`],
|
|
388
|
+
activity: test === null ? NO_ACTIVITY : test.ok ? { okAt: test.at, error: null, errorAt: null, failing: false } : { okAt: null, error: test.problem, errorAt: test.at, failing: false },
|
|
389
|
+
probe: async (check) => {
|
|
390
|
+
const missing = missingSecrets(tool.spec, readToolSecrets(repo, tool.name, toolHome));
|
|
391
|
+
if (missing.length > 0)
|
|
392
|
+
return { outcome: "failed", problem: `${tool.name} needs ${missing.join(", ")}. Add ${missing.length === 1 ? "it" : "them"} in Settings → Tools.` };
|
|
393
|
+
const tested = await testToolOf(check.store, repo, tool.name, (check.clock ?? (() => new Date()))(), { home: toolHome, omitEnv: ALL_CREDENTIAL_ENV, timeoutMs: 15_000 });
|
|
394
|
+
if (tested === null)
|
|
395
|
+
return { outcome: "failed", problem: `${tool.name} is no longer on this project.` };
|
|
396
|
+
return tested.ok ? { outcome: "ok", account: basename(repo), detail: plural(tested.tools.length, "tool") } : { outcome: "failed", account: basename(repo), problem: `${tool.name} didn't start: ${scrubIntegrationText(tested.problem ?? "no answer")}` };
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!anyTool)
|
|
402
|
+
list.push({ key: "mcp", group: "tools", name: "MCP tools", configured: false, account: null, setup: place("/settings/tools"), fix: place("/settings/tools"), usedBy: [], activity: NO_ACTIVITY, probe: null });
|
|
403
|
+
// Monitoring: the delivery loop is the check; its status row says how it's going.
|
|
404
|
+
const monitoring = readMonitoring(dir);
|
|
405
|
+
const destinations = [
|
|
406
|
+
...(monitoring.webhook === null ? [] : [{ sink: "webhook", name: "Audit webhook", account: origin(monitoring.webhook.url), address: monitoring.webhook.url, uses: "Audit stream" }]),
|
|
407
|
+
...(monitoring.folder === null ? [] : [{ sink: "folder", name: "Audit folder", account: monitoring.folder.path, address: monitoring.folder.path, uses: "Audit stream" }]),
|
|
408
|
+
...(monitoring.traces === null ? [] : [{ sink: "traces", name: "Traces", account: origin(monitoring.traces.endpoint), address: monitoring.traces.endpoint, uses: "Run traces" }]),
|
|
409
|
+
];
|
|
410
|
+
for (const one of destinations) {
|
|
411
|
+
const status = store.monitoringStatus(one.sink).find(row => row.target === targetOf(one.address)) ?? null;
|
|
412
|
+
const activity = status === null ? NO_ACTIVITY : { okAt: status.lastOkAt, error: status.lastError, errorAt: status.lastErrorAt, failing: status.failures > 0 };
|
|
413
|
+
list.push({
|
|
414
|
+
key: `monitoring:${one.sink}`, group: "monitoring", name: one.name, configured: true, account: one.account,
|
|
415
|
+
setup: place("/settings/monitoring"), fix: place("/settings/monitoring"), usedBy: [one.uses], activity,
|
|
416
|
+
probe: async () => status === null || (status.lastOkAt === null && status.lastError === null)
|
|
417
|
+
? { outcome: "ok", account: one.account, detail: "Starting: the first delivery goes out within a few seconds" }
|
|
418
|
+
: status.failures > 0 ? { outcome: "failed", account: one.account, problem: `Deliveries are failing: ${status.lastError ?? "no answer"}` }
|
|
419
|
+
: { outcome: "ok", account: one.account },
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (destinations.length === 0)
|
|
423
|
+
list.push({ key: "monitoring", group: "monitoring", name: "Monitoring", configured: false, account: null, setup: place("/settings/monitoring"), fix: place("/settings/monitoring"), usedBy: [], activity: NO_ACTIVITY, probe: null });
|
|
424
|
+
for (const provider of AGENTS) {
|
|
425
|
+
list.push({
|
|
426
|
+
key: `agent:${provider}`, group: "agents", name: providerName(provider), configured: true, account: null,
|
|
427
|
+
setup: place("/settings#providers"), fix: place("/settings#providers"), usedBy: ["Tasks"], activity: NO_ACTIVITY,
|
|
428
|
+
probe: check => probeAgent(check, provider),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return list;
|
|
432
|
+
}
|
|
433
|
+
// ---------------------------------------------------------------- the list
|
|
434
|
+
function combine(target, row) {
|
|
435
|
+
const base = { key: target.key, group: target.group, name: target.name, usedBy: target.usedBy };
|
|
436
|
+
const checked = target.probe === null || row !== undefined;
|
|
437
|
+
const lastSuccessAt = later(row?.okAt ?? null, target.activity.okAt);
|
|
438
|
+
const checkError = row?.error ?? null, checkErrorAt = row?.errorAt ?? null;
|
|
439
|
+
const activityNewer = (target.activity.errorAt ?? "") > (checkErrorAt ?? "");
|
|
440
|
+
const lastError = target.activity.error !== null && (activityNewer || checkError === null) ? scrubIntegrationText(target.activity.error) : checkError;
|
|
441
|
+
const lastErrorAt = lastError === null ? null : later(checkErrorAt, target.activity.errorAt);
|
|
442
|
+
const common = { account: row?.account ?? target.account, detail: row?.detail ?? null, checked, checkedAt: row?.checkedAt ?? null, lastSuccessAt, lastError, lastErrorAt };
|
|
443
|
+
if (!target.configured || row?.outcome === "absent") {
|
|
444
|
+
return { ...base, ...common, account: null, detail: row?.outcome === "absent" ? row.problem : null, state: "not-set-up", action: { kind: "setup", label: "Set up", ...target.setup } };
|
|
445
|
+
}
|
|
446
|
+
// Broken: the last check failed, or deliveries are failing and no successful check has come since.
|
|
447
|
+
const failing = row?.outcome === "failed" ? row.problem
|
|
448
|
+
: target.activity.failing && (row === undefined || (target.activity.errorAt ?? "") >= row.checkedAt) ? target.activity.error ?? "Deliveries are failing."
|
|
449
|
+
: null;
|
|
450
|
+
if (failing !== null) {
|
|
451
|
+
return { ...base, ...common, state: "broken", action: { kind: "fix", label: "Fix", words: scrubIntegrationText(failing ?? "The last check failed."), ...target.fix } };
|
|
452
|
+
}
|
|
453
|
+
return { ...base, ...common, state: "connected", action: { kind: "test", label: "Send test" } };
|
|
454
|
+
}
|
|
455
|
+
/** Every integration as it stands now, from files and saved history only: no network, no process, never slow. */
|
|
456
|
+
export function integrationsNow(io) {
|
|
457
|
+
const rows = readIntegrationChecks(io.store);
|
|
458
|
+
return targets(io).map(target => combine(target, rows.get(target.key)));
|
|
459
|
+
}
|
|
460
|
+
/** Run the checks (all, or the named ones), record each answer, and return the list. */
|
|
461
|
+
export async function checkIntegrations(io, keys) {
|
|
462
|
+
const clock = io.clock ?? (() => new Date());
|
|
463
|
+
const wanted = targets(io).filter(one => one.probe !== null && (keys === undefined || keys.includes(one.key)));
|
|
464
|
+
await Promise.all(wanted.map(async (target) => {
|
|
465
|
+
let result;
|
|
466
|
+
try {
|
|
467
|
+
result = await target.probe(io);
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
result = { outcome: "failed", problem: "The check stopped unexpectedly. Try again." };
|
|
471
|
+
}
|
|
472
|
+
recordIntegrationCheck(io.store, target.key, result, clock());
|
|
473
|
+
}));
|
|
474
|
+
return integrationsNow(io);
|
|
475
|
+
}
|
|
476
|
+
/** The one line `status` and `onboard` add when something is Broken; null when nothing is. */
|
|
477
|
+
export function integrationsBrokenLine(list) {
|
|
478
|
+
const broken = list.filter(one => one.state === "broken").map(one => one.name);
|
|
479
|
+
if (broken.length === 0)
|
|
480
|
+
return null;
|
|
481
|
+
const names = broken.length <= 3 ? broken.join(", ") : `${broken.slice(0, 3).join(", ")} and ${broken.length - 3} more`;
|
|
482
|
+
return `Broken integration${broken.length === 1 ? "" : "s"}: ${names}. See \`toolroll integrations\` or Settings → Integrations.`;
|
|
483
|
+
}
|
|
484
|
+
const whenText = (at) => (at === null ? "never" : `${at.slice(0, 16).replace("T", " ")} UTC`);
|
|
485
|
+
/** The CLI's lines: one per integration, then the fix under anything Broken. */
|
|
486
|
+
export function renderIntegrations(list) {
|
|
487
|
+
const width = Math.min(24, Math.max(...list.map(one => one.name.length)));
|
|
488
|
+
return list.flatMap(one => {
|
|
489
|
+
const facts = [one.account, one.detail].filter(Boolean).join(" · ");
|
|
490
|
+
const lines = [`${one.name.padEnd(width)} ${STATE_WORDS[one.state].padEnd(10)}${facts === "" ? "" : ` ${facts}`}`];
|
|
491
|
+
if (one.state !== "not-set-up")
|
|
492
|
+
lines.push(`${"".padEnd(width)} last success ${whenText(one.lastSuccessAt)}${one.lastError === null ? "" : ` · last error ${whenText(one.lastErrorAt)}: ${one.lastError}`}`);
|
|
493
|
+
if (one.action.kind === "fix")
|
|
494
|
+
lines.push(`${"".padEnd(width)} Fix: ${one.action.words}${one.action.command === null || one.action.words.includes(one.action.command) ? "" : ` (run ${one.action.command})`}`);
|
|
495
|
+
if (one.action.kind === "setup")
|
|
496
|
+
lines.push(`${"".padEnd(width)} Set up: ${one.action.command !== null ? `run ${one.action.command}` : `Settings (${one.action.href ?? "/settings"})`}`);
|
|
497
|
+
return lines;
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/** Checks in the background with a short cache, as the first-run suggestions are: a render never waits on a check. */
|
|
501
|
+
export function createIntegrationMonitor(io, options = {}) {
|
|
502
|
+
const cacheMs = options.cacheMs ?? 60_000;
|
|
503
|
+
const now = options.now ?? Date.now;
|
|
504
|
+
let lastStarted = 0;
|
|
505
|
+
let running = null;
|
|
506
|
+
const start = (keys) => {
|
|
507
|
+
const current = io();
|
|
508
|
+
const run = checkIntegrations(current, keys).catch(() => []);
|
|
509
|
+
if (keys === undefined) {
|
|
510
|
+
lastStarted = now();
|
|
511
|
+
running = run.finally(() => { running = null; });
|
|
512
|
+
}
|
|
513
|
+
return run;
|
|
514
|
+
};
|
|
515
|
+
return {
|
|
516
|
+
list() {
|
|
517
|
+
const current = io();
|
|
518
|
+
const list = integrationsNow(current);
|
|
519
|
+
if (running === null && now() - lastStarted >= cacheMs)
|
|
520
|
+
void start();
|
|
521
|
+
return list;
|
|
522
|
+
},
|
|
523
|
+
check: keys => start(keys),
|
|
524
|
+
get checking() { return running !== null; },
|
|
525
|
+
};
|
|
526
|
+
}
|
package/dist/lead-context.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ import { type AssignmentCatchUp } from './assignment-brief.js';
|
|
|
5
5
|
export declare function leadContext(store: Store, repos: readonly string[], now: Date, evidenceRoot?: string): string;
|
|
6
6
|
/** The same saved brief is useful before chat spending is authorized. */
|
|
7
7
|
export declare function leadBriefHtml(brief: AssignmentCatchUp): string;
|
|
8
|
-
export declare const LEAD_CONTEXT_CSS = ".lead-brief{margin:1rem 0;min-width:0}.lead-brief h2{font-size:1.1rem;margin:0 0 .8rem}.repository-context input[name=q]{display:block;box-sizing:border-box;min-height:44px;width:100%;margin:.4rem 0 .75rem;padding:.6rem .75rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:inherit;font:inherit}.repository-context form{margin:.5rem 0 1rem}.repository-context summary{min-height:44px;padding:.75rem 0;overflow-wrap:anywhere}.lead-brief h3{font-size:.85rem;margin:1rem 0 .4rem;color:var(--muted-foreground)}.lead-brief ul{list-style:none;margin:0;padding:0}.lead-brief li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);min-width:0}.lead-brief li a{display:flex;min-height:44px;align-items:center;font-weight:500;overflow-wrap:anywhere;text-decoration:none}.lead-brief li a:hover{text-decoration:underline}.lead-brief-state{font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink);white-space:nowrap}.lead-brief-state--needs-decision,.lead-brief-state--ready-to-check{background:var(--so-attention-soft);color:var(--so-attention)}.lead-brief-state--working,.lead-brief-state--checking{background:var(--so-info-soft);color:var(--so-info)}.lead-brief-state--complete{background:var(--so-success-soft);color:var(--so-success)}.lead-brief-detail{grid-column:1/-1;font-size:.85rem;color:var(--muted-foreground);overflow-wrap:anywhere;padding-bottom:.35rem}";
|
|
8
|
+
export declare const LEAD_CONTEXT_CSS = ".lead-brief{margin:1rem 0;min-width:0}.lead-brief h2{font-size:1.1rem;margin:0 0 .8rem}.repository-context input[name=q]{display:block;box-sizing:border-box;min-height:44px;width:100%;margin:.4rem 0 .75rem;padding:.6rem .75rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:inherit;font:inherit}.repository-context form{margin:.5rem 0 1rem}.repository-context summary{min-height:44px;padding:.75rem 0;overflow-wrap:anywhere}.lead-brief h3{font-size:.85rem;margin:1rem 0 .4rem;color:var(--muted-foreground)}.lead-brief ul{list-style:none;margin:0;padding:0}.lead-brief li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);min-width:0}.lead-brief li a{display:flex;min-height:44px;align-items:center;font-weight:500;overflow-wrap:anywhere;text-decoration:none}.lead-brief li a:hover{text-decoration:underline}.lead-brief-state{font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink);white-space:nowrap}.lead-brief-state--needs-decision,.lead-brief-state--ready-to-check{background:var(--so-attention-soft);color:var(--so-attention)}.lead-brief-state--working,.lead-brief-state--checking{background:var(--so-info-soft);color:var(--so-info)}.lead-brief-state--complete{background:var(--so-success-soft);color:var(--so-success)}.lead-brief-detail{grid-column:1/-1;font-size:.85rem;color:var(--muted-foreground);overflow-wrap:anywhere;padding-bottom:.35rem}@media(max-width:760px){.lead-brief{margin:.5rem 0}.lead-brief h2{margin:0 0 .25rem}.lead-brief h3{margin:.5rem 0 0}.lead-brief li{padding:0 0 .375rem}.lead-brief-detail{margin-top:-.375rem;padding-bottom:0;line-height:1.35;pointer-events:none}}";
|
package/dist/lead-context.js
CHANGED
|
@@ -53,4 +53,4 @@ export function leadBriefHtml(brief) {
|
|
|
53
53
|
});
|
|
54
54
|
return `<section class="lead-brief" aria-label="Project catch-up"><h2>Catch up</h2>${sections.length ? sections.join('') : '<p class="meta">Nothing needs you right now.</p>'}${brief.assignments.length > 3 || brief.omissions.candidateScanLimited || brief.omissions.assignments > 0 ? '<a href="/work">See all tasks</a>' : ''}</section>`;
|
|
55
55
|
}
|
|
56
|
-
export const LEAD_CONTEXT_CSS = '.lead-brief{margin:1rem 0;min-width:0}.lead-brief h2{font-size:1.1rem;margin:0 0 .8rem}.repository-context input[name=q]{display:block;box-sizing:border-box;min-height:44px;width:100%;margin:.4rem 0 .75rem;padding:.6rem .75rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:inherit;font:inherit}.repository-context form{margin:.5rem 0 1rem}.repository-context summary{min-height:44px;padding:.75rem 0;overflow-wrap:anywhere}.lead-brief h3{font-size:.85rem;margin:1rem 0 .4rem;color:var(--muted-foreground)}.lead-brief ul{list-style:none;margin:0;padding:0}.lead-brief li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);min-width:0}.lead-brief li a{display:flex;min-height:44px;align-items:center;font-weight:500;overflow-wrap:anywhere;text-decoration:none}.lead-brief li a:hover{text-decoration:underline}.lead-brief-state{font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink);white-space:nowrap}.lead-brief-state--needs-decision,.lead-brief-state--ready-to-check{background:var(--so-attention-soft);color:var(--so-attention)}.lead-brief-state--working,.lead-brief-state--checking{background:var(--so-info-soft);color:var(--so-info)}.lead-brief-state--complete{background:var(--so-success-soft);color:var(--so-success)}.lead-brief-detail{grid-column:1/-1;font-size:.85rem;color:var(--muted-foreground);overflow-wrap:anywhere;padding-bottom:.35rem}';
|
|
56
|
+
export const LEAD_CONTEXT_CSS = '.lead-brief{margin:1rem 0;min-width:0}.lead-brief h2{font-size:1.1rem;margin:0 0 .8rem}.repository-context input[name=q]{display:block;box-sizing:border-box;min-height:44px;width:100%;margin:.4rem 0 .75rem;padding:.6rem .75rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:inherit;font:inherit}.repository-context form{margin:.5rem 0 1rem}.repository-context summary{min-height:44px;padding:.75rem 0;overflow-wrap:anywhere}.lead-brief h3{font-size:.85rem;margin:1rem 0 .4rem;color:var(--muted-foreground)}.lead-brief ul{list-style:none;margin:0;padding:0}.lead-brief li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);min-width:0}.lead-brief li a{display:flex;min-height:44px;align-items:center;font-weight:500;overflow-wrap:anywhere;text-decoration:none}.lead-brief li a:hover{text-decoration:underline}.lead-brief-state{font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink);white-space:nowrap}.lead-brief-state--needs-decision,.lead-brief-state--ready-to-check{background:var(--so-attention-soft);color:var(--so-attention)}.lead-brief-state--working,.lead-brief-state--checking{background:var(--so-info-soft);color:var(--so-info)}.lead-brief-state--complete{background:var(--so-success-soft);color:var(--so-success)}.lead-brief-detail{grid-column:1/-1;font-size:.85rem;color:var(--muted-foreground);overflow-wrap:anywhere;padding-bottom:.35rem}@media(max-width:760px){.lead-brief{margin:.5rem 0}.lead-brief h2{margin:0 0 .25rem}.lead-brief h3{margin:.5rem 0 0}.lead-brief li{padding:0 0 .375rem}.lead-brief-detail{margin-top:-.375rem;padding-bottom:0;line-height:1.35;pointer-events:none}}';
|
package/dist/mate-contract.js
CHANGED
|
@@ -19,7 +19,7 @@ export const MATE_CONTRACT = [
|
|
|
19
19
|
"For intake, a plain-language outcome is enough to draft a task. Infer title, narrow goal, safe non-goals and testable criteria; leave touches empty for discovery. Do not ask the operator for a title, paths, implementation details, acceptance wording, model, budget or safely inferable fields. Ask at most three questions with defaults for material ambiguity, conflicting goals or unresolved irreversible/public/security/data-loss/migration choices. With 'use your judgment', use reversible defaults; real blockers may still arise.",
|
|
20
20
|
"Set propose_task planning to 'required' for broad, risky or plan-first work, 'skip' for explicitly small direct builds, otherwise 'auto'. report:true investigates without a branch; follow-ups require confirmed proposals.",
|
|
21
21
|
"Tools are MCP servers a project's builds get, and builds get nothing else: read get_project_tools. A service in connectBySigningIn (Stripe, Notion, Linear, Sentry, Jira, Intercom, Attio and more) connects in one click: give the operator its connect link, where they sign in on the service's own page; never tool_add it and never ask for its key. To add another, prefer its commonTools entry (propose_action tool_add with catalog); otherwise use exactly the command or address the operator gives, never a package you are unsure exists. A tool reaches work approved after it was added. Never ask for, accept or repeat a secret's value: name it and open the Tools page with show_control tools.",
|
|
22
|
-
"Flows are a project's process drawn as zones that cards move through: Holding, Build and Research (each files an ordinary task), Person decides, Message, Done. Read get_flows. To make one, propose_flow create with a template or the steps in plain names; leave instructions out unless the operator gave them, and use decider 'me' when they decide. To change one, propose_flow edit with the whole step list, keeping existing steps by id. For cards use add_card, move_card, approve, send_back with the operator's note, or cancel_card; find the card by what it is about. Triggers start cards on their own: a button with questions, a schedule, GitHub (new issues, a label being added, new pull requests, failed checks), Linear, another flow's cards reaching a zone, or email arriving in the operator's mailbox (optionally only from some senders or with words in the subject; reading mail is set up in Settings → Email); propose_flow add_trigger. A Slack, Discord or Teams channel feeds a flow when a paired approver sends 'flow <the flow's number>' in that channel (never from here); each message there becomes a card, and an 'update' step answers in its thread. Webhook addresses and the Linear key are set on the flow's Triggers panel, never in chat. Cards have an owner, followers and a discussion: read a card's discussion with get_flows flow and card before acting on what teammates said; comment (with @name to ping someone), assign and follow through propose_flow. Two steps run without a model: 'check' runs one of the project's scripts (named scripts in Python, Node or shell that belong to the project, not a flow — save one with propose_flow save_script and the repo even before any flow exists; keep them short, or point them at a file already in the repository) with the card as its input; what it prints is passed to later steps, a last line 'goto: <answer>' picks where the card goes, it can run in an empty folder or a copy of the card's work, and it takes the failure path if it fails; a schedule trigger can run a script and make a card of each item it prints; 'update' comments on the GitHub or Linear issue the card came from and can close it, or answers in the chat thread it came from. A Build whose project checks fail takes its failure path too. A 'sort' step has Jev (a fast decision model on the operator's OpenRouter account) read the card and send it where the answer it picks leads, or to its not-sure step, noting scores or yes/no answers too; make the sort the first step, because cards wait in a holding step until a person moves them; for triage, spam, lead, effort or exception routing, start from the matching template. A 'draft' step has Claude write a reply, summary or note from the card in seconds and sends nothing itself: put a decision after it (decider 'owner' asks the flow's owner in their chat app to approve, edit or send it back), then the step that posts it. Steps can reach outside too: 'request' calls a web address, 'email' sends mail from the operator's email account (answering the sender of a card that came from email keeps the reply in that thread), 'tool' uses one of the project's tools (get_project_tools); secrets for them are saved on the canvas or Tools page, never in chat, and a decision should come before any that sends what a model wrote or an outsider sent. A 'wait' step after an 'email' step waits for the person to reply (the reply moves the card on and is kept for later steps; with none by the time it waits, the card takes its no-reply step, like a follow-up email), or just waits a set time. Any step can remind whoever it waits on after a while (remindAfter), and holding and decision steps can then move the card on (thenMoveTo): use these for follow-ups and decisions that stall. AI teammates are agents on the operator's team with a soul file (who they are, how they write, what they know, what they decide on their own, what they ask first, what they never do); read get_teammates. They work flow cards within those rules: a teammate named on an approval step decides it (and hands hard ones to that step's person), and a 'teammate' step has one pick where the card goes and write what the next steps send, asking the flow's owner when its rules say to. They never approve code tasks or merges. With propose_teammate you add one (from a template: support, sales, ops, triage), change one rule section (edit_section), pause, resume or remove it, pass it a note, or answer its question for the operator; put it to work with propose_flow. When the operator says something like 'Maya can approve refunds up to $100 now', change that section of her soul file; when it's for this week or a one-off, pass it as a note. Teammates can use the project's tools under a rule per action: do it, ask first (the operator approves each exact call), never, or do it up to a limit on a number; propose_teammate use_tool, stop_tool and tool_rule change them, and when a rule has a limit also change the soul file so its words agree. Each teammate has a memory (get_teammates): what people told it (a note adds one) and facts it kept from cards; forget or edit_memory when the operator says it has something wrong. When it suggests a rule change, that is a question for the operator; answer it only as they say. Each teammate has a desk (a flow): the operator can message it by name in their chat app ('@maya, …'), and routines (propose_teammate add_routine) put a card there on a schedule; its answer goes back to whoever asked. Its desk can send a card to a Build zone that files an ordinary task under the usual approvals. When someone new wants a support desk, bug triage, sales follow-up or requests handled, a starter kit (propose_flow kit) sets up the teammate and its flow in one card. get_teammates with a teammate gives its week (what it did, cost, what the operator overrode); a tool call can be undone (propose_teammate undo) only when its action has an undo set (tool_rule undoWith). For where flows break, read get_flow_insights, and read a failed run's log before explaining it.",
|
|
22
|
+
"Flows are a project's process drawn as zones that cards move through: Holding, Build and Research (each files an ordinary task), Person decides, Message, Done. Read get_flows. To make one, propose_flow create with a template or the steps in plain names; leave instructions out unless the operator gave them, and use decider 'me' when they decide. To change one, propose_flow edit with the whole step list, keeping existing steps by id. For cards use add_card, move_card, approve, send_back with the operator's note, or cancel_card; find the card by what it is about. Triggers start cards on their own: a button with questions, a schedule, GitHub (new issues, a label being added, new pull requests, failed checks), Linear, another flow's cards reaching a zone, or email arriving in the operator's mailbox (optionally only from some senders or with words in the subject; reading mail is set up in Settings → Email); propose_flow add_trigger. A Slack, Discord or Teams channel feeds a flow when a paired approver sends 'flow <the flow's number>' in that channel (never from here); each message there becomes a card, and an 'update' step answers in its thread. Webhook addresses and the Linear key are set on the flow's Triggers panel, never in chat. Cards have an owner, followers and a discussion: read a card's discussion with get_flows flow and card before acting on what teammates said; comment (with @name to ping someone), assign and follow through propose_flow. Two steps run without a model: 'check' runs one of the project's scripts (named scripts in Python, Node or shell that belong to the project, not a flow — save one with propose_flow save_script and the repo even before any flow exists; keep them short, or point them at a file already in the repository) with the card as its input; what it prints is passed to later steps, a last line 'goto: <answer>' picks where the card goes, it can run in an empty folder or a copy of the card's work, and it takes the failure path if it fails; a schedule trigger can run a script and make a card of each item it prints; 'update' comments on the GitHub or Linear issue the card came from and can close it, or answers in the chat thread it came from. A Build whose project checks fail takes its failure path too. A 'sort' step has Jev (a fast decision model on the operator's OpenRouter account) read the card and send it where the answer it picks leads, or to its not-sure step, noting scores or yes/no answers too; make the sort the first step, because cards wait in a holding step until a person moves them; for triage, spam, lead, effort or exception routing, start from the matching template. A 'draft' step has Claude write a reply, summary or note from the card in seconds and sends nothing itself: put a decision after it (decider 'owner' asks the flow's owner in their chat app to approve, edit or send it back), then the step that posts it. Steps can reach outside too: 'request' calls a web address, 'email' sends mail from the operator's email account (answering the sender of a card that came from email keeps the reply in that thread), 'tool' uses one of the project's tools (get_project_tools); secrets for them are saved on the canvas or Tools page, never in chat, and a decision should come before any that sends what a model wrote or an outsider sent. A 'wait' step after an 'email' step waits for the person to reply (the reply moves the card on and is kept for later steps; with none by the time it waits, the card takes its no-reply step, like a follow-up email), or just waits a set time. Any step can remind whoever it waits on after a while (remindAfter), and holding and decision steps can then move the card on (thenMoveTo): use these for follow-ups and decisions that stall. AI teammates are agents on the operator's team with a soul file (who they are, how they write, what they know, what they decide on their own, what they ask first, what they never do); read get_teammates. They work flow cards within those rules: a teammate named on an approval step decides it (and hands hard ones to that step's person), and a 'teammate' step has one pick where the card goes and write what the next steps send, asking the flow's owner when its rules say to. They never approve code tasks or merges. With propose_teammate you add one (from a template: support, sales, ops, triage), change one rule section (edit_section), pause, resume or remove it, pass it a note, or answer its question for the operator; put it to work with propose_flow. When the operator says something like 'Maya can approve refunds up to $100 now', change that section of her soul file; when it's for this week or a one-off, pass it as a note. Teammates can use the project's tools under a rule per action: do it, ask first (the operator approves each exact call), never, or do it up to a limit on a number; propose_teammate use_tool, stop_tool and tool_rule change them, and when a rule has a limit also change the soul file so its words agree. Each teammate has a memory (get_teammates): what people told it (a note adds one) and facts it kept from cards; forget or edit_memory when the operator says it has something wrong. When it suggests a rule change, that is a question for the operator; answer it only as they say. Each teammate has a desk (a flow): the operator can message it by name in their chat app ('@maya, …'), and routines (propose_teammate add_routine) put a card there on a schedule; its answer goes back to whoever asked. Its desk can send a card to a Build zone that files an ordinary task under the usual approvals. When someone new wants a support desk, bug triage, sales follow-up or requests handled, a starter kit (propose_flow kit) sets up the teammate and its flow in one card. get_teammates with a teammate gives its week (what it did, cost, what the operator overrode); a tool call can be undone (propose_teammate undo) only when its action has an undo set (tool_rule undoWith). A 'pull-request' step opens a pull request for a Build's result and follows its CI (green moves on, red goes back to the build naming the failing check); it merges only when set to, and only after a person approved the card. The Issues to PRs template runs GitHub issues labelled toolroll through build, approval, pull request and a comment on the issue. When the operator asks for something to happen every time (fix CI when it fails, turn labelled issues into tasks, queue work overnight), offer the matching starter flow with propose_flow starter; each is one yes, and none merges without a person. For where flows break, read get_flow_insights, and read a failed run's log before explaining it.",
|
|
23
23
|
"For skills read get_skills and exact instructions. Enabled means supplied, not proven used or connected. Manage/test through get_actions/propose_action with project/version; require a receipt before claiming deployment/test start.",
|
|
24
24
|
"To see what a result changed, read get_diff: the file list, then one file's changes. To see why checks failed, read get_check_log: its end, or search for the error. When the operator asks for a change, read the relevant file's diff first, then propose_review revise with the exact path and line and one precise instruction in their words.",
|
|
25
25
|
"Each task also has its own chat with the operator (its Ask panel and phone replies). When they refer to what was said or asked about a task, read get_task_conversation. Cards you draft about a task are recorded in that task's chat once confirmed.",
|