toolroll 0.9.53 → 0.9.55
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 +17 -0
- package/README.md +5 -4
- package/dist/agent-onboard.js +4 -1
- package/dist/approval-rules-ui.d.ts +3 -2
- package/dist/approval-rules-ui.js +15 -20
- package/dist/assignment-ui.d.ts +3 -2
- package/dist/assignment-ui.js +4 -11
- package/dist/backup-ui.d.ts +2 -1
- package/dist/backup-ui.js +16 -27
- package/dist/brand-mark.d.ts +3 -2
- package/dist/brand-mark.js +4 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-shell.d.ts +2 -1
- package/dist/browser-shell.js +9 -9
- package/dist/browser-workspace.d.ts +2 -3
- package/dist/browser-workspace.js +0 -3
- package/dist/chat-ask.js +8 -8
- package/dist/chat-core.d.ts +171 -0
- package/dist/chat-core.js +288 -0
- package/dist/chat-decide.d.ts +10 -8
- package/dist/chat-decide.js +26 -21
- package/dist/chat-delivery-state.d.ts +51 -75
- package/dist/chat-delivery-state.js +140 -170
- package/dist/chat-delivery.d.ts +21 -2
- package/dist/chat-delivery.js +313 -194
- package/dist/chat-flow.d.ts +29 -1
- package/dist/chat-flow.js +24 -50
- package/dist/chat-messages.d.ts +319 -0
- package/dist/chat-messages.js +841 -0
- package/dist/chat-migration.d.ts +30 -0
- package/dist/chat-migration.js +802 -0
- package/dist/chat-polish.d.ts +5 -4
- package/dist/chat-polish.js +9 -10
- package/dist/chat-quiet.d.ts +3 -0
- package/dist/chat-quiet.js +5 -0
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +26 -53
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +9 -3
- package/dist/coding-shipping-ui.d.ts +2 -1
- package/dist/coding-shipping-ui.js +6 -6
- package/dist/coding-ui.d.ts +3 -1
- package/dist/coding-ui.js +30 -29
- package/dist/contracts/chat-content.d.ts +71 -2
- package/dist/contracts/chat-content.js +22 -3
- package/dist/contracts/chat-tables.d.ts +11 -4
- package/dist/contracts/chat-tables.js +165 -87
- package/dist/contracts/cli.d.ts +1 -3
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +6 -5
- package/dist/contracts/discord-callback.js +2 -1
- package/dist/contracts/health.d.ts +1 -3
- package/dist/contracts/slack-callback.js +3 -2
- package/dist/contracts/store-json.d.ts +1 -5
- package/dist/contracts/store-json.js +1 -4
- package/dist/contracts/teams-callback.js +2 -1
- package/dist/control-ui.d.ts +5 -4
- package/dist/control-ui.js +9 -34
- package/dist/credentials-ui.d.ts +5 -4
- package/dist/credentials-ui.js +23 -24
- package/dist/demo-chat.d.ts +3 -3
- package/dist/demo-chat.js +29 -76
- package/dist/discord-chat.js +19 -38
- package/dist/discord-settings.d.ts +3 -2
- package/dist/discord-settings.js +21 -55
- package/dist/discord.js +4 -14
- package/dist/envelope.js +10 -0
- package/dist/evidence-pack.d.ts +2 -1
- package/dist/evidence-pack.js +45 -49
- package/dist/export-ui.d.ts +7 -2
- package/dist/export-ui.js +4 -10
- package/dist/export.js +4 -4
- package/dist/flow-gallery-ui.d.ts +4 -3
- package/dist/flow-gallery-ui.js +28 -43
- package/dist/flow-starters-ui.d.ts +2 -1
- package/dist/flow-starters-ui.js +12 -15
- package/dist/flow-triggers.d.ts +1 -0
- package/dist/flow-triggers.js +16 -14
- package/dist/flows-ui.d.ts +4 -3
- package/dist/flows-ui.js +19 -30
- package/dist/flows.js +2 -1
- package/dist/guides.js +108 -3
- package/dist/html.d.ts +76 -0
- package/dist/html.js +173 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +19 -23
- package/dist/kits-ui.d.ts +5 -4
- package/dist/kits-ui.js +18 -20
- package/dist/knowledge-ui.d.ts +6 -5
- package/dist/knowledge-ui.js +26 -25
- package/dist/lead-context.d.ts +11 -1
- package/dist/lead-context.js +6 -8
- package/dist/ledger-view.d.ts +2 -1
- package/dist/ledger-view.js +27 -29
- package/dist/limits-ui.d.ts +2 -1
- package/dist/limits-ui.js +7 -7
- package/dist/live-bus.d.ts +50 -8
- package/dist/live-bus.js +133 -50
- package/dist/mate-tools.js +3 -2
- package/dist/mcp-oauth.js +35 -36
- package/dist/models-ui.d.ts +2 -1
- package/dist/models-ui.js +25 -35
- package/dist/modes.d.ts +13 -1
- package/dist/modes.js +17 -1
- package/dist/monitoring-ui.d.ts +4 -3
- package/dist/monitoring-ui.js +13 -26
- package/dist/notification-rows.d.ts +30 -0
- package/dist/notification-rows.js +102 -0
- package/dist/openrouter-models.d.ts +2 -1
- package/dist/openrouter-models.js +5 -9
- package/dist/operate.js +16 -9
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +13 -13
- package/dist/plane-review.js +5 -11
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +17 -23
- package/dist/project-delete-ui.d.ts +4 -3
- package/dist/project-delete-ui.js +15 -27
- package/dist/project-delete.js +21 -26
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +17 -32
- package/dist/recipe-ui.d.ts +8 -7
- package/dist/recipe-ui.js +39 -32
- package/dist/release-adapters.d.ts +60 -0
- package/dist/release-adapters.js +435 -0
- package/dist/release-cli.d.ts +24 -0
- package/dist/release-cli.js +181 -0
- package/dist/release-gate.d.ts +80 -0
- package/dist/release-gate.js +150 -0
- package/dist/release-links.d.ts +29 -0
- package/dist/release-links.js +102 -0
- package/dist/release.d.ts +331 -0
- package/dist/release.js +550 -0
- package/dist/reply-shape.d.ts +2 -1
- package/dist/reply-shape.js +5 -5
- package/dist/repository-context-ui.d.ts +2 -1
- package/dist/repository-context-ui.js +2 -7
- package/dist/request-budget-ui.d.ts +2 -1
- package/dist/request-budget-ui.js +16 -16
- package/dist/result-review.d.ts +2 -3
- package/dist/result-review.js +3 -9
- package/dist/result-shots.js +2 -2
- package/dist/retention-ui.d.ts +7 -1
- package/dist/retention-ui.js +7 -11
- package/dist/retention.js +1 -1
- package/dist/serve.d.ts +11 -2
- package/dist/serve.js +69 -58
- package/dist/server/chat.d.ts +3 -6
- package/dist/server/chat.js +969 -1006
- package/dist/server/chrome.d.ts +291 -0
- package/dist/server/chrome.js +747 -0
- package/dist/server/console.css +2819 -0
- package/dist/server/flows.d.ts +2 -6
- package/dist/server/flows.js +840 -766
- package/dist/server/guards.d.ts +1 -1
- package/dist/server/guards.js +5 -2
- package/dist/server/handler-context.d.ts +1 -1
- package/dist/server/handler-context.js +1 -1
- package/dist/server/handler-registry.d.ts +8 -6
- package/dist/server/handler-registry.js +14 -2
- package/dist/server/http.d.ts +274 -0
- package/dist/server/http.js +239 -0
- package/dist/server/live.d.ts +34 -0
- package/dist/server/live.js +309 -0
- package/dist/server/pages.d.ts +2 -6
- package/dist/server/pages.js +808 -789
- package/dist/server/people-tokens.d.ts +1 -6
- package/dist/server/people-tokens.js +580 -572
- package/dist/server/remote.d.ts +4 -7
- package/dist/server/remote.js +37 -70
- package/dist/server/render-chat.d.ts +402 -0
- package/dist/server/render-chat.js +927 -0
- package/dist/server/render-pages.d.ts +535 -0
- package/dist/server/render-pages.js +1277 -0
- package/dist/server/render-people.d.ts +32 -0
- package/dist/server/render-people.js +121 -0
- package/dist/server/render-results.d.ts +744 -0
- package/dist/server/render-results.js +2237 -0
- package/dist/server/render-settings.d.ts +147 -0
- package/dist/server/render-settings.js +577 -0
- package/dist/server/render-tasks.d.ts +632 -0
- package/dist/server/render-tasks.js +2345 -0
- package/dist/server/request-context.d.ts +46 -0
- package/dist/server/request-context.js +17 -0
- package/dist/server/route-table.d.ts +25 -3
- package/dist/server/route-table.js +163 -75
- package/dist/server/runtime.d.ts +15 -7
- package/dist/server/runtime.js +7 -1
- package/dist/server/session.d.ts +138 -0
- package/dist/server/session.js +94 -0
- package/dist/server/settings.d.ts +1 -5
- package/dist/server/settings.js +2182 -2117
- package/dist/server/task-views.d.ts +11 -4
- package/dist/server/task-views.js +26 -22
- package/dist/server/tasks.d.ts +9 -9
- package/dist/server/tasks.js +1486 -1418
- package/dist/server-telemetry.d.ts +4 -3
- package/dist/server-telemetry.js +4 -8
- package/dist/skills-ui.d.ts +4 -3
- package/dist/skills-ui.js +14 -17
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +2 -1
- package/dist/slack-api.d.ts +5 -1
- package/dist/slack-api.js +2 -2
- package/dist/slack-chat.d.ts +9 -4
- package/dist/slack-chat.js +29 -52
- package/dist/slack-settings.d.ts +3 -2
- package/dist/slack-settings.js +23 -57
- package/dist/slack.js +6 -16
- package/dist/spend-ui.d.ts +2 -1
- package/dist/spend-ui.js +9 -32
- package/dist/sso-ui.d.ts +3 -2
- package/dist/sso-ui.js +26 -27
- package/dist/storage-ui.d.ts +2 -1
- package/dist/storage-ui.js +14 -33
- package/dist/store.d.ts +97 -364
- package/dist/store.js +207 -1531
- package/dist/surface.js +14 -3
- package/dist/task-composer.d.ts +2 -1
- package/dist/task-composer.js +7 -7
- package/dist/task-status.d.ts +4 -3
- package/dist/task-status.js +15 -16
- package/dist/team-http.d.ts +5 -9
- package/dist/team-http.js +5 -63
- package/dist/team-ui.d.ts +2 -1
- package/dist/team-ui.js +2 -5
- package/dist/teammate-question.js +12 -12
- package/dist/teammates-ui.d.ts +5 -4
- package/dist/teammates-ui.js +61 -90
- package/dist/teams-chat.js +12 -9
- package/dist/teams-settings.d.ts +3 -2
- package/dist/teams-settings.js +22 -33
- package/dist/teams.js +4 -5
- package/dist/telegram-flow.js +5 -5
- package/dist/telegram-settings.d.ts +3 -2
- package/dist/telegram-settings.js +14 -25
- package/dist/telegram.js +4 -4
- package/dist/toolroll-update-ui.d.ts +3 -2
- package/dist/toolroll-update-ui.js +35 -39
- package/dist/toolroll-update.d.ts +10 -0
- package/dist/toolroll-update.js +50 -3
- package/dist/tools-ui.d.ts +5 -2
- package/dist/tools-ui.js +31 -31
- package/dist/when-html.d.ts +11 -3
- package/dist/when-html.js +5 -6
- package/dist/workspace-revision.js +3 -1
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +10 -10
- package/package.json +1 -1
- package/dist/server/shared.d.ts +0 -3151
- package/dist/server/shared.js +0 -12001
- package/dist/slack-state.d.ts +0 -13
- package/dist/slack-state.js +0 -10
package/dist/mcp-oauth.js
CHANGED
|
@@ -4,6 +4,8 @@ import { hashSecret, mintApiToken } from "./api-tokens.js";
|
|
|
4
4
|
import { limitWords } from "./request-budget.js";
|
|
5
5
|
import { sourceKey } from "./source-key.js";
|
|
6
6
|
import { PKCE_CHALLENGE, readOAuthRegistration, readOAuthTokenRequest } from "./contracts/mcp-oauth.js";
|
|
7
|
+
import { html, htmlString, joinHtml, postForm, styleElement } from "./html.js";
|
|
8
|
+
import { withFormToken } from "./server/request-context.js";
|
|
7
9
|
/** How long each piece lives. A code is spent at once; a grant ends after 30 days and the person consents again. */
|
|
8
10
|
export const OAUTH_TIMES = { requestMs: 10 * 60_000, codeMs: 60_000, accessSeconds: 3600, grantDays: 30 };
|
|
9
11
|
/** The most sign-ins waiting at once, and registered clients kept. */
|
|
@@ -33,7 +35,6 @@ export function oauthTokenAllowed(store, tokenId, path, now) {
|
|
|
33
35
|
export const oauthProjects = (store, tokenId) => store.oauthGrant(tokenId)?.projects ?? null;
|
|
34
36
|
const sha256 = (text) => createHash("sha256").update(text, "utf8").digest("hex");
|
|
35
37
|
const same = (a, b) => { const x = Buffer.from(a), y = Buffer.from(b); return x.length === y.length && timingSafeEqual(x, y); };
|
|
36
|
-
const escape = (text) => text.replace(/[&<>"']/g, one => `&#${one.charCodeAt(0)};`);
|
|
37
38
|
const clean = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2066-\u2069]+/g, " ").replace(/\s+/g, " ").trim();
|
|
38
39
|
/**
|
|
39
40
|
* A redirect a client may register: https anywhere, or http on 127.0.0.1 / [::1] (RFC 8252 §7.3), with no
|
|
@@ -80,10 +81,10 @@ export function createOAuthHttp(options) {
|
|
|
80
81
|
};
|
|
81
82
|
const oauthError = (response, status, error, description) => json(response, status, { error, error_description: description });
|
|
82
83
|
/** A page of this sign-in: script-free, never framed, posting only to this console. */
|
|
83
|
-
const page = (response, status,
|
|
84
|
+
const page = (response, status, document) => {
|
|
84
85
|
response.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "referrer-policy": "no-referrer", "x-content-type-options": "nosniff", "x-frame-options": "DENY",
|
|
85
86
|
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'" });
|
|
86
|
-
response.end(
|
|
87
|
+
response.end(htmlString(document));
|
|
87
88
|
};
|
|
88
89
|
/**
|
|
89
90
|
* On to an address by a page that moves on by itself, never a redirect: a redirect answering a form is held to
|
|
@@ -93,9 +94,9 @@ export function createOAuthHttp(options) {
|
|
|
93
94
|
const moveOn = (response, to, words) => {
|
|
94
95
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "referrer-policy": "no-referrer", "x-content-type-options": "nosniff", "x-frame-options": "DENY",
|
|
95
96
|
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'" });
|
|
96
|
-
response.end(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="refresh" content="0;url=${
|
|
97
|
+
response.end(htmlString(html `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="refresh" content="0;url=${to}"><title>Toolroll</title>${STYLE}<main><p>${words} <a href="${to}">Continue</a></p></main></html>`));
|
|
97
98
|
};
|
|
98
|
-
const problemPage = (response, status, title, said) => page(response, status, shell(title, `<h1>${
|
|
99
|
+
const problemPage = (response, status, title, said) => page(response, status, shell(title, html `<h1>${title}</h1><p>${said}</p>`));
|
|
99
100
|
/** Back to the client with an answer: the code, or an error, and its state; `iss` names this console (RFC 9207). */
|
|
100
101
|
const backToClient = (response, origin, redirectUri, state, answer, words) => {
|
|
101
102
|
const to = new URL(redirectUri);
|
|
@@ -234,34 +235,34 @@ export function createOAuthHttp(options) {
|
|
|
234
235
|
const title = "Connect to Toolroll";
|
|
235
236
|
const projectList = projects.length === 0 ? "" : projects.map(path => {
|
|
236
237
|
const name = path.split("/").filter(part => part !== "").pop() ?? path;
|
|
237
|
-
return `<label class="pick"><input type="checkbox" name="project" value="${
|
|
238
|
-
})
|
|
238
|
+
return html `<label class="pick"><input type="checkbox" name="project" value="${path}"${checked.has(path) ? " checked" : ""}><span><b>${name}</b><small>${path}</small></span></label>`;
|
|
239
|
+
});
|
|
239
240
|
const step = session.sso?.fresh === true
|
|
240
|
-
? `<p class="meta">Confirmed with ${
|
|
241
|
-
: `<label class="field">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>${session.sso === null ? "" : `<p class="meta"><a href="/login/sso?reauth=1&return=${
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
241
|
+
? html `<p class="meta">Confirmed with ${session.sso.label}.</p><input type="hidden" name="password" value="">`
|
|
242
|
+
: html `<label class="field">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>${session.sso === null ? "" : html `<p class="meta"><a href="/login/sso?reauth=1&return=${encodeURIComponent(`/oauth/authorize?request=${id}`)}">Confirm with ${session.sso.label}</a> instead.</p>`}`;
|
|
243
|
+
// The consent form carries the console session's own CSRF token (checked against session.csrf on the post).
|
|
244
|
+
const consentForm = (body) => withFormToken(session.csrf, () => postForm("/oauth/authorize", body, { hidden: { request: id } }));
|
|
245
|
+
const body = joinHtml([
|
|
246
|
+
html `<h1>${title}</h1>`,
|
|
247
|
+
html `<p class="client"><b>${client.name}</b> <span class="tag">unverified</span> <span class="meta">returns to ${host}</span></p>`,
|
|
248
|
+
problem === null ? "" : html `<div class="problem" role="alert">${problem}</div>`,
|
|
246
249
|
projects.length === 0
|
|
247
|
-
? `<p>You don't have access to any projects. Ask your Toolroll administrator to add you to one.</p>`
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
`<details><summary>Client details</summary><p class="meta">Client ID <code>${escape(client.id)}</code><br>Returns to <code>${escape(one.redirectUri)}</code><br>Toolroll doesn't know who made this client; its name is what it says about itself.</p></details>`,
|
|
264
|
-
].join("");
|
|
250
|
+
? html `<p>You don't have access to any projects. Ask your Toolroll administrator to add you to one.</p>${consentForm(html `<button name="decision" value="deny" class="secondary">Cancel</button>`)}`
|
|
251
|
+
: joinHtml([
|
|
252
|
+
html `<p>Connects as ${session.name} in your selected projects. It can't approve work or change people or policy.</p>`,
|
|
253
|
+
consentForm(joinHtml([
|
|
254
|
+
html `<fieldset><legend>Access</legend>`,
|
|
255
|
+
html `<label class="pick"><input type="radio" name="access" value="read"${access === "read" ? " checked" : ""}><span><b>Read</b><small>See tasks, results and status.</small></span></label>`,
|
|
256
|
+
mayAct ? html `<label class="pick"><input type="radio" name="access" value="act"${access === "act" ? " checked" : ""}><span><b>Act</b><small>Also file tasks under your name.</small></span></label>` : "",
|
|
257
|
+
html `</fieldset>`,
|
|
258
|
+
html `<fieldset><legend>Projects</legend>${projectList}</fieldset>`,
|
|
259
|
+
html `<p class="meta">Access lasts up to 30 days. Revoke it anytime in Settings → Sessions & tokens.</p>`,
|
|
260
|
+
step,
|
|
261
|
+
html `<div class="actions"><button name="decision" value="allow">Allow access</button><button name="decision" value="deny" class="secondary" formnovalidate>Cancel</button></div>`,
|
|
262
|
+
])),
|
|
263
|
+
]),
|
|
264
|
+
html `<details><summary>Client details</summary><p class="meta">Client ID <code>${client.id}</code><br>Returns to <code>${one.redirectUri}</code><br>Toolroll doesn't know who made this client; its name is what it says about itself.</p></details>`,
|
|
265
|
+
]);
|
|
265
266
|
return shell(title, body);
|
|
266
267
|
};
|
|
267
268
|
async function authorizeConsent(request, response, url, origin) {
|
|
@@ -449,8 +450,7 @@ export function createOAuthHttp(options) {
|
|
|
449
450
|
};
|
|
450
451
|
}
|
|
451
452
|
/** The sign-in's pages stand alone: no stylesheet, script or font loads here, so the palette rides inline. */
|
|
452
|
-
const STYLE = [
|
|
453
|
-
`<style>`,
|
|
453
|
+
const STYLE = styleElement([
|
|
454
454
|
`:root{color-scheme:light dark;--ground:#efefef;--paper:#fff;--ink:#171717;--muted:#666;--line:#e6e6e6;--soft:#f2f2f2;--accent:#171717;--on-accent:#fff;--problem:#b42318}`,
|
|
455
455
|
`@media (prefers-color-scheme:dark){:root{--ground:#0b0b0b;--paper:#161616;--ink:#ededed;--muted:#a1a1a1;--line:#262626;--soft:#1f1f1f;--accent:#ededed;--on-accent:#0b0b0b;--problem:#ff8a80}}`,
|
|
456
456
|
`*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px 16px;background:var(--ground);color:var(--ink);font:15px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}`,
|
|
@@ -467,6 +467,5 @@ const STYLE = [
|
|
|
467
467
|
`.problem{border:1px solid var(--problem);color:var(--problem);border-radius:8px;padding:10px 12px;margin-bottom:14px}a{color:inherit;text-underline-offset:3px}`,
|
|
468
468
|
`details{border-top:1px solid var(--line)}summary{cursor:pointer;color:var(--muted);font-size:13px;min-height:44px;padding:12px 0}code{font:12.5px/1.5 ui-monospace,"SF Mono",Menlo,monospace;background:var(--soft);border-radius:5px;padding:1px 5px;overflow-wrap:anywhere}`,
|
|
469
469
|
`@media (max-width:480px){main{padding:20px}.field input{font-size:16px}.actions button{flex:1 1 auto}}`,
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const shell = (title, body) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><title>${escape(title)}</title>${STYLE}</head><body><main>${body}</main></body></html>`;
|
|
470
|
+
].join(""));
|
|
471
|
+
const shell = (title, body) => html `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><title>${title}</title>${STYLE}</head><body><main>${body}</main></body></html>`;
|
package/dist/models-ui.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Settings → Models: the CLIs on this computer, the default agent for each
|
|
2
2
|
* role picked from live lists, and the models that just arrived. */
|
|
3
|
+
import { type Html } from "./html.js";
|
|
3
4
|
import type { ModelOption, RuntimeState, SeenModel, WatchState } from "./model-catalog.js";
|
|
4
5
|
export declare const MODELS_CSS = ".models{max-width:780px;min-width:0;overflow-wrap:anywhere}.models .card{padding:16px 20px;margin:12px 0;min-width:0}.models h2{margin:24px 0 8px}.models form{margin:0}.models label{display:grid;gap:6px}.models select,.models input[type=search]{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.models button,.models summary,.models .button-link{min-height:44px}.models button{white-space:nowrap}.models summary{padding:12px 0;cursor:pointer}.models .tool{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}.models .role{display:grid;grid-template-columns:minmax(0,1fr);gap:8px}.models .role-row{display:flex;gap:12px;align-items:end;flex-wrap:wrap}.models .role-row select{flex:1 1 240px;width:auto;min-width:0}.models .role-name{font-weight:600}.models .new{font-weight:600}.models ul{padding-left:20px}.models .meta{font-size:.86rem}@media(max-width:600px){.models .card{padding:14px}}";
|
|
5
6
|
export type RoleView = {
|
|
@@ -29,6 +30,6 @@ export type ModelsView = {
|
|
|
29
30
|
said: string | null;
|
|
30
31
|
problem: string | null;
|
|
31
32
|
};
|
|
32
|
-
export declare function modelsHtml(view: ModelsView):
|
|
33
|
+
export declare function modelsHtml(view: ModelsView): Html;
|
|
33
34
|
/** Filters one long picker as you type; the saved choice always stays listed. */
|
|
34
35
|
export declare function modelsScript(): string;
|
package/dist/models-ui.js
CHANGED
|
@@ -1,60 +1,50 @@
|
|
|
1
|
+
/** Settings → Models: the CLIs on this computer, the default agent for each
|
|
2
|
+
* role picked from live lists, and the models that just arrived. */
|
|
3
|
+
import { html, postForm } from "./html.js";
|
|
1
4
|
import { priceWords } from "./model-catalog.js";
|
|
2
5
|
import { whenUtc } from "./when-html.js";
|
|
3
|
-
const e = (s) => String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
4
|
-
const hidden = (data) => Object.entries(data).map(([k, v]) => `<input type="hidden" name="${e(k)}" value="${e(v)}">`).join("");
|
|
5
6
|
const when = (iso) => iso === null ? "never" : whenUtc(iso);
|
|
6
7
|
export const MODELS_CSS = `.models{max-width:780px;min-width:0;overflow-wrap:anywhere}.models .card{padding:16px 20px;margin:12px 0;min-width:0}.models h2{margin:24px 0 8px}.models form{margin:0}.models label{display:grid;gap:6px}.models select,.models input[type=search]{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.models button,.models summary,.models .button-link{min-height:44px}.models button{white-space:nowrap}.models summary{padding:12px 0;cursor:pointer}.models .tool{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}.models .role{display:grid;grid-template-columns:minmax(0,1fr);gap:8px}.models .role-row{display:flex;gap:12px;align-items:end;flex-wrap:wrap}.models .role-row select{flex:1 1 240px;width:auto;min-width:0}.models .role-name{font-weight:600}.models .new{font-weight:600}.models ul{padding-left:20px}.models .meta{font-size:.86rem}@media(max-width:600px){.models .card{padding:14px}}`;
|
|
7
|
-
function toolRow(one, manage
|
|
8
|
+
function toolRow(one, manage) {
|
|
8
9
|
const state = one.installed === null ? "Version unknown"
|
|
9
|
-
: one.behind ? `${
|
|
10
|
-
: `${
|
|
10
|
+
: one.behind ? html `${one.installed} · <strong>${one.latest} available</strong>`
|
|
11
|
+
: `${one.installed} · Up to date`;
|
|
11
12
|
const action = manage && one.behind && one.updateCommand !== null
|
|
12
|
-
?
|
|
13
|
-
: one.behind ? `<span class="meta">Update it the way you installed it</span>` : "";
|
|
14
|
-
return `<div class="card tool"><span><strong>${
|
|
13
|
+
? postForm("/settings/models/update", html `<button type="submit">Update ${one.name}</button>`, { hidden: { tool: one.tool } })
|
|
14
|
+
: one.behind ? html `<span class="meta">Update it the way you installed it</span>` : "";
|
|
15
|
+
return html `<div class="card tool"><span><strong>${one.name}</strong> ${state}${one.problem === null ? "" : html `<br><span class="meta">${one.problem}</span>`}</span>${action}</div>`;
|
|
15
16
|
}
|
|
16
|
-
function roleForm(role, manage
|
|
17
|
+
function roleForm(role, manage) {
|
|
17
18
|
if (!manage)
|
|
18
|
-
return `<div class="card role"><strong>${
|
|
19
|
+
return html `<div class="card role"><strong>${role.label}</strong><span>${role.words}</span></div>`;
|
|
19
20
|
const known = role.groups.some(group => group.options.some(option => `${group.provider}|${option.value}` === role.current));
|
|
20
|
-
const saved = !known && role.current !== "inherit" && role.current !== "" ? `<option value="${
|
|
21
|
-
const groups = role.groups.map(group => `<optgroup label="${
|
|
21
|
+
const saved = !known && role.current !== "inherit" && role.current !== "" ? html `<option value="${role.current}" selected>${role.words} · current</option>` : "";
|
|
22
|
+
const groups = role.groups.map(group => html `<optgroup label="${group.label}">${group.options.map(option => {
|
|
22
23
|
const value = `${group.provider}|${option.value}`;
|
|
23
|
-
return `<option value="${
|
|
24
|
-
})
|
|
25
|
-
const inherit = role.inherit ? `<option value="inherit"${role.current === "inherit" ?
|
|
24
|
+
return html `<option value="${value}"${value === role.current ? html ` selected` : ""}>${option.label}</option>`;
|
|
25
|
+
})}</optgroup>`);
|
|
26
|
+
const inherit = role.inherit ? html `<option value="inherit"${role.current === "inherit" ? html ` selected` : ""}>Same as the builder</option>` : "";
|
|
26
27
|
const long = role.groups.reduce((sum, group) => sum + group.options.length, 0) > 40;
|
|
27
28
|
const count = role.groups.reduce((sum, group) => sum + group.options.length, 0);
|
|
28
29
|
const id = `role-${role.phase}`;
|
|
29
|
-
return `<
|
|
30
|
-
`<label class="role-name" for="${id}">${e(role.label)}</label>` +
|
|
31
|
-
(long ? `<input type="search" data-model-filter hidden aria-label="Search ${e(role.label.toLowerCase())} models" placeholder="Search ${count} models" autocomplete="off">` : "") +
|
|
32
|
-
`<div class="role-row"><select id="${id}" name="agent">${inherit}${saved}${groups}</select><button type="submit" class="secondary">Save</button></div></form>`;
|
|
30
|
+
return postForm("/settings/models/agent", html `<label class="role-name" for="${id}">${role.label}</label>${long ? html `<input type="search" data-model-filter hidden aria-label="Search ${role.label.toLowerCase()} models" placeholder="Search ${count} models" autocomplete="off">` : ""}<div class="role-row"><select id="${id}" name="agent">${inherit}${saved}${groups}</select><button type="submit" class="secondary">Save</button></div>`, { attrs: { class: "card role" }, hidden: { phase: role.phase } });
|
|
33
31
|
}
|
|
34
32
|
export function modelsHtml(view) {
|
|
35
33
|
const manage = view.canManage && view.csrf !== "";
|
|
36
|
-
const status = view.said === null ? "" : `<p role="status">${
|
|
37
|
-
const problem = view.problem === null ? "" : `<p class="problem" role="alert">${
|
|
34
|
+
const status = view.said === null ? "" : html `<p role="status">${view.said}</p>`;
|
|
35
|
+
const problem = view.problem === null ? "" : html `<p class="problem" role="alert">${view.problem}</p>`;
|
|
38
36
|
const tools = view.runtimes.length === 0
|
|
39
|
-
? `<p class="meta">No Claude, Codex or Gemini CLI found on this computer yet.</p>`
|
|
40
|
-
: view.runtimes.map(one => toolRow(one, manage
|
|
37
|
+
? html `<p class="meta">No Claude, Codex or Gemini CLI found on this computer yet.</p>`
|
|
38
|
+
: view.runtimes.map(one => toolRow(one, manage));
|
|
41
39
|
const check = manage
|
|
42
|
-
?
|
|
40
|
+
? postForm("/settings/models/check", html `<button type="submit" class="secondary">Check now</button>`, { attrs: { class: "inline" } })
|
|
43
41
|
: "";
|
|
44
42
|
const fresh = view.fresh.length === 0 ? "" :
|
|
45
|
-
`<h2>New models</h2><ul>${view.fresh.map(model => `<li><span class="new">${
|
|
43
|
+
html `<h2>New models</h2><ul>${view.fresh.map(model => html `<li><span class="new">${model.name}</span> <span class="meta">${priceWords(model)}${model.releasedAt === null ? "" : ` · released ${model.releasedAt.slice(0, 10)}`}</span></li>`)}</ul>`;
|
|
46
44
|
const watch = manage
|
|
47
|
-
? `<details><summary>Automatic checks · ${view.watch.enabled ? "On" : "Off"}</summary><
|
|
48
|
-
`<p>Check every 6 hours and send a message when a new model or CLI update appears. It uses public lists and sends nothing about you or your projects.</p>` +
|
|
49
|
-
`<button type="submit" class="secondary">${view.watch.enabled ? "Turn off" : "Turn on"}</button></form></details>`
|
|
45
|
+
? html `<details><summary>Automatic checks · ${view.watch.enabled ? "On" : "Off"}</summary>${postForm("/settings/models/watch", html `<p>Check every 6 hours and send a message when a new model or CLI update appears. It uses public lists and sends nothing about you or your projects.</p><button type="submit" class="secondary">${view.watch.enabled ? "Turn off" : "Turn on"}</button>`, { hidden: { enabled: view.watch.enabled ? "0" : "1" } })}</details>`
|
|
50
46
|
: "";
|
|
51
|
-
return `<section class="models">${status}${problem}
|
|
52
|
-
`<h2>AI tools</h2>${tools}<p class="meta">Checked ${when(view.watch.checkedAt)} ${check}</p>` +
|
|
53
|
-
fresh +
|
|
54
|
-
`<h2>Default agents</h2><p class="meta">New tasks use these. Approved tasks keep the agents they were approved with.</p>` +
|
|
55
|
-
view.roles.map(role => roleForm(role, manage, view.csrf)).join("") +
|
|
56
|
-
(view.chat === null ? "" : `<h2>Chat</h2><p>${e(view.chat.words)} · <a href="/settings/lead">Change</a></p>`) +
|
|
57
|
-
watch + `</section>`;
|
|
47
|
+
return html `<section class="models">${status}${problem}<h2>AI tools</h2>${tools}<p class="meta">Checked ${when(view.watch.checkedAt)} ${check}</p>${fresh}<h2>Default agents</h2><p class="meta">New tasks use these. Approved tasks keep the agents they were approved with.</p>${view.roles.map(role => roleForm(role, manage))}${view.chat === null ? "" : html `<h2>Chat</h2><p>${view.chat.words} · <a href="/settings/lead">Change</a></p>`}${watch}</section>`;
|
|
58
48
|
}
|
|
59
49
|
/** Filters one long picker as you type; the saved choice always stays listed. */
|
|
60
50
|
export function modelsScript() {
|
package/dist/modes.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* owns the transactions; the tick owns the rails; serve owns the
|
|
14
14
|
* ceremony screens.
|
|
15
15
|
*/
|
|
16
|
+
import { type ChatProvider } from "./contracts/chat-tables.js";
|
|
16
17
|
export type ModeName = "standard" | "hands-off";
|
|
17
18
|
export type ModeTerms = {
|
|
18
19
|
name: ModeName;
|
|
@@ -64,12 +65,16 @@ export type ModeTerms = {
|
|
|
64
65
|
* `repairAuto` is also true — a mode may sign a cap without signing the
|
|
65
66
|
* authority, but never the reverse. */
|
|
66
67
|
repairMaxAttempts: number;
|
|
67
|
-
/** Explicit opt-in: the signer's paired
|
|
68
|
+
/** Explicit opt-in: the signer's paired chat may approve this
|
|
68
69
|
* repository's plans and merge its ready pull requests with two taps,
|
|
69
70
|
* for the mode's lifetime. A plan that widens permissions, exceeds the
|
|
70
71
|
* per-attempt budget or touches protected paths still opens Toolroll.
|
|
71
72
|
* Legacy modes read FALSE — never inherited, only freshly signed. */
|
|
72
73
|
chatApprove: boolean;
|
|
74
|
+
/** The chat apps `chatApprove` names, signed with it. Absent on a mode signed before chat apps other than Telegram
|
|
75
|
+
* could approve: its words said "your paired Telegram chat", so it stays Telegram only (chatApproveChatsOf). A wider
|
|
76
|
+
* grant is only ever a fresh signature naming the apps. */
|
|
77
|
+
chatApproveChats?: readonly ChatProvider[];
|
|
73
78
|
absoluteExpiry: string;
|
|
74
79
|
};
|
|
75
80
|
export declare const MODE_MAX_DAYS = 7;
|
|
@@ -82,6 +87,13 @@ export declare function modeTermsJson(terms: ModeTerms): string;
|
|
|
82
87
|
export declare function modeDigestOf(terms: ModeTerms): string;
|
|
83
88
|
/** Strict rehydration — anything unexpected is null, never a guess. */
|
|
84
89
|
export declare function modeTermsFromJson(json: string | null): ModeTerms | null;
|
|
90
|
+
/** The chat apps a mode lets its signer approve from: none without chatApprove; Telegram alone for a grant signed
|
|
91
|
+
* before it named its apps (that is what its words said); otherwise exactly the apps it names. */
|
|
92
|
+
export declare function chatApproveChatsOf(terms: Pick<ModeTerms, "chatApprove" | "chatApproveChats">): readonly ChatProvider[];
|
|
93
|
+
/** What a new chatApprove signature names: every chat app, said in its words. */
|
|
94
|
+
export declare const CHAT_APPROVE_ALL: readonly ChatProvider[];
|
|
95
|
+
/** "Telegram", "Telegram or Slack", "Telegram, Slack, Discord or Teams". */
|
|
96
|
+
export declare const chatNames: (chats: readonly ChatProvider[]) => string;
|
|
85
97
|
/** Every term in words — what the ceremony renders and the password
|
|
86
98
|
* signs. The reversal sentence is verbatim from the chain (C1). */
|
|
87
99
|
export declare function modeWords(terms: ModeTerms): string[];
|
package/dist/modes.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* ceremony screens.
|
|
15
15
|
*/
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
|
+
import { CHAT_PROVIDERS, isChatProvider } from "./contracts/chat-tables.js";
|
|
17
18
|
export const MODE_MAX_DAYS = 7;
|
|
18
19
|
/** The presets are STARTING POINTS the ceremony renders in full — the
|
|
19
20
|
* signature always covers the resolved terms, never a label. */
|
|
@@ -120,6 +121,10 @@ export function modeTermsFromJson(json) {
|
|
|
120
121
|
(t["repairMaxAttempts"] === undefined ||
|
|
121
122
|
(typeof t["repairMaxAttempts"] === "number" && Number.isInteger(t["repairMaxAttempts"]) && t["repairMaxAttempts"] >= 0 && t["repairMaxAttempts"] <= 3)) &&
|
|
122
123
|
(t["chatApprove"] === undefined || typeof t["chatApprove"] === "boolean") &&
|
|
124
|
+
// chatApproveChats: absent (a Telegram-only grant, or none), or the apps a chatApprove term names, each once.
|
|
125
|
+
(t["chatApproveChats"] === undefined ||
|
|
126
|
+
(t["chatApprove"] === true && Array.isArray(t["chatApproveChats"]) && t["chatApproveChats"].length > 0 &&
|
|
127
|
+
t["chatApproveChats"].every(isChatProvider) && new Set(t["chatApproveChats"]).size === t["chatApproveChats"].length)) &&
|
|
123
128
|
typeof t["absoluteExpiry"] === "string" &&
|
|
124
129
|
!Number.isNaN(Date.parse(t["absoluteExpiry"]))) {
|
|
125
130
|
return {
|
|
@@ -138,11 +143,22 @@ export function modeTermsFromJson(json) {
|
|
|
138
143
|
repairAuto: t["repairAuto"] === true,
|
|
139
144
|
repairMaxAttempts: typeof t["repairMaxAttempts"] === "number" ? t["repairMaxAttempts"] : 0,
|
|
140
145
|
chatApprove: t["chatApprove"] === true,
|
|
146
|
+
...(Array.isArray(t["chatApproveChats"]) ? { chatApproveChats: [...t["chatApproveChats"]] } : {}),
|
|
141
147
|
absoluteExpiry: t["absoluteExpiry"],
|
|
142
148
|
};
|
|
143
149
|
}
|
|
144
150
|
return null;
|
|
145
151
|
}
|
|
152
|
+
/** The chat apps a mode lets its signer approve from: none without chatApprove; Telegram alone for a grant signed
|
|
153
|
+
* before it named its apps (that is what its words said); otherwise exactly the apps it names. */
|
|
154
|
+
export function chatApproveChatsOf(terms) {
|
|
155
|
+
return !terms.chatApprove ? [] : terms.chatApproveChats ?? ["telegram"];
|
|
156
|
+
}
|
|
157
|
+
/** What a new chatApprove signature names: every chat app, said in its words. */
|
|
158
|
+
export const CHAT_APPROVE_ALL = CHAT_PROVIDERS;
|
|
159
|
+
const CHAT_NAMES = { telegram: "Telegram", slack: "Slack", discord: "Discord", teams: "Teams" };
|
|
160
|
+
/** "Telegram", "Telegram or Slack", "Telegram, Slack, Discord or Teams". */
|
|
161
|
+
export const chatNames = (chats) => chats.length <= 1 ? chats.map(one => CHAT_NAMES[one]).join("") : `${chats.slice(0, -1).map(one => CHAT_NAMES[one]).join(", ")} or ${CHAT_NAMES[chats[chats.length - 1]]}`;
|
|
146
162
|
/** Every term in words — what the ceremony renders and the password
|
|
147
163
|
* signs. The reversal sentence is verbatim from the chain (C1). */
|
|
148
164
|
export function modeWords(terms) {
|
|
@@ -178,7 +194,7 @@ export function modeWords(terms) {
|
|
|
178
194
|
...(terms.repairAuto
|
|
179
195
|
? ["historical automatic repair grants are retained on record but no longer schedule work"] : []),
|
|
180
196
|
...(terms.chatApprove
|
|
181
|
-
? [
|
|
197
|
+
? [`your paired ${chatNames(chatApproveChatsOf(terms))} chat may approve this repository's plans and merge its ready pull requests, two taps each, without your password; a plan that widens permissions, exceeds the per-attempt cap above or touches protected paths still opens Toolroll`] : []),
|
|
182
198
|
`everything above ends at ${terms.absoluteExpiry.slice(0, 16).replace("T", " ")} — revoking it earlier is one click, and every act it covered falls back to its own ceremony`,
|
|
183
199
|
];
|
|
184
200
|
}
|
package/dist/monitoring-ui.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* the company's tools. One form, the status of each destination beside it,
|
|
4
4
|
* and how Prometheus reads `/metrics`. The signing secret is shown once.
|
|
5
5
|
*/
|
|
6
|
+
import { type Html } from "./html.js";
|
|
6
7
|
import type { MonitoringStatus } from "./store.js";
|
|
7
8
|
import type { MonitoringSettings } from "./monitoring-settings.js";
|
|
8
9
|
export declare const MONITORING_CSS: string;
|
|
@@ -12,9 +13,9 @@ export type MonitoringView = {
|
|
|
12
13
|
head: number;
|
|
13
14
|
origin: string | null;
|
|
14
15
|
};
|
|
15
|
-
export declare function monitoringHtml(view: MonitoringView,
|
|
16
|
+
export declare function monitoringHtml(view: MonitoringView, notice: {
|
|
16
17
|
said?: string | null;
|
|
17
18
|
problem?: string | null;
|
|
18
|
-
}):
|
|
19
|
+
}): Html;
|
|
19
20
|
/** The webhook's signing secret, once: a focused page with no script. */
|
|
20
|
-
export declare function signingSecretHtml(url: string, secret: string):
|
|
21
|
+
export declare function signingSecretHtml(url: string, secret: string): Html;
|
package/dist/monitoring-ui.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings → Monitoring (v104): an instance operator points Toolroll at
|
|
3
|
+
* the company's tools. One form, the status of each destination beside it,
|
|
4
|
+
* and how Prometheus reads `/metrics`. The signing secret is shown once.
|
|
5
|
+
*/
|
|
6
|
+
import { html, postForm } from "./html.js";
|
|
1
7
|
import { targetOf } from "./monitoring.js";
|
|
2
8
|
import { whenUtc } from "./when-html.js";
|
|
3
|
-
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
4
9
|
const when = (at) => whenUtc(at);
|
|
5
10
|
export const MONITORING_CSS = `.monitoring{max-width:720px;min-width:0}.monitoring fieldset{border:0;padding:0;margin:20px 0 0;min-width:0}` +
|
|
6
11
|
`.monitoring legend{font-weight:600;font-size:.9375rem;margin-bottom:6px}.monitoring label{display:grid;gap:4px;margin:10px 0 0;font-size:.875rem}` +
|
|
@@ -15,38 +20,20 @@ function statusLine(view, sink, address) {
|
|
|
15
20
|
// A status for another address (it just changed) says nothing about this one yet.
|
|
16
21
|
const one = view.status.find(row => row.sink === sink && row.target === targetOf(address));
|
|
17
22
|
if (one === undefined || (one.lastOkAt === null && one.lastError === null))
|
|
18
|
-
return `<p class="status meta" data-monitoring="${sink}" data-state="starting">Starting: the first delivery goes out within a few seconds.</p>`;
|
|
23
|
+
return html `<p class="status meta" data-monitoring="${sink}" data-state="starting">Starting: the first delivery goes out within a few seconds.</p>`;
|
|
19
24
|
if (one.failures > 0 && one.lastError !== null) {
|
|
20
|
-
return `<p class="status problem" role="alert" data-monitoring="${sink}" data-state="failing">Failing: ${
|
|
25
|
+
return html `<p class="status problem" role="alert" data-monitoring="${sink}" data-state="failing">Failing: ${one.lastError}${one.nextTryAt === null ? "" : html ` · trying again ${when(one.nextTryAt)}`}</p>`;
|
|
21
26
|
}
|
|
22
27
|
const behind = Math.max(0, view.head - one.through);
|
|
23
|
-
return `<p class="status meta" data-monitoring="${sink}" data-state="ok">${sink === "traces" ? "Sending" : `Sent through entry #${one.through}`}${one.lastOkAt === null ? "" : ` · last ${when(one.lastOkAt)}`}${behind > 0 && sink !== "traces" ? ` · ${behind} to go` : ""}</p>`;
|
|
28
|
+
return html `<p class="status meta" data-monitoring="${sink}" data-state="ok">${sink === "traces" ? "Sending" : `Sent through entry #${one.through}`}${one.lastOkAt === null ? "" : html ` · last ${when(one.lastOkAt)}`}${behind > 0 && sink !== "traces" ? ` · ${behind} to go` : ""}</p>`;
|
|
24
29
|
}
|
|
25
|
-
export function monitoringHtml(view,
|
|
30
|
+
export function monitoringHtml(view, notice) {
|
|
26
31
|
const s = view.settings;
|
|
27
|
-
const note = notice.problem ? `<p class="problem" role="alert">${
|
|
32
|
+
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
28
33
|
const metrics = view.origin === null ? "/metrics" : `${view.origin}/metrics`;
|
|
29
|
-
return `<section class="monitoring">${note}
|
|
30
|
-
`<form method="post" action="/settings/monitoring"><input type="hidden" name="csrf" value="${e(csrf)}">` +
|
|
31
|
-
`<fieldset><legend>Audit stream</legend><p class="meta">Every ledger entry with its seal, as it happens, to your log system.</p>` +
|
|
32
|
-
`<label>Webhook<input type="url" name="webhook" value="${e(s.webhook?.url ?? "")}" placeholder="https://logs.example.com/standing-orders" spellcheck="false"></label>` +
|
|
33
|
-
statusLine(view, "webhook", s.webhook?.url ?? null) +
|
|
34
|
-
(s.webhook === null ? "" : `<label class="choice"><input type="checkbox" name="rotate" value="1"> Make a new signing secret</label>`) +
|
|
35
|
-
`<label>Folder (JSON Lines, a file per day)<input type="text" name="folder" value="${e(s.folder?.path ?? "")}" placeholder="/var/log/standing-orders" spellcheck="false"></label>` +
|
|
36
|
-
statusLine(view, "folder", s.folder?.path ?? null) + `</fieldset>` +
|
|
37
|
-
`<fieldset><legend>Traces</legend><p class="meta">Each run as an OpenTelemetry span: timings, model, tokens and cost. Never a prompt or code.</p>` +
|
|
38
|
-
`<label>Collector (OTLP over HTTP)<input type="url" name="traces" value="${e(s.traces?.endpoint ?? "")}" placeholder="https://otel.example.com:4318" spellcheck="false"></label>` +
|
|
39
|
-
statusLine(view, "traces", s.traces?.endpoint ?? null) +
|
|
40
|
-
`<label>Header name (optional)<input type="text" name="header-name" value="${e(s.traces?.header?.name ?? "")}" placeholder="x-honeycomb-team" spellcheck="false"></label>` +
|
|
41
|
-
`<label>Header value<input type="password" name="header-value" autocomplete="off" placeholder="${s.traces?.header ? "Saved; leave blank to keep" : "API key"}"></label></fieldset>` +
|
|
42
|
-
`<div class="step-up"><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label></div>` +
|
|
43
|
-
`<button type="submit">Save</button></form>` +
|
|
44
|
-
`<fieldset><legend>Metrics</legend><p class="meta">Prometheus reads <code>${e(metrics)}</code> with an instance operator's API token (<a href="/settings/sessions">Sessions & tokens</a>).</p>` +
|
|
45
|
-
`<details><summary>Example</summary><pre>curl -H "Authorization: Bearer so_…" ${e(metrics)}</pre></details></fieldset></section>`;
|
|
34
|
+
return html `<section class="monitoring">${note}${postForm("/settings/monitoring", html `<fieldset><legend>Audit stream</legend><p class="meta">Every ledger entry with its seal, as it happens, to your log system.</p><label>Webhook<input type="url" name="webhook" value="${s.webhook?.url ?? ""}" placeholder="https://logs.example.com/standing-orders" spellcheck="false"></label>${statusLine(view, "webhook", s.webhook?.url ?? null)}${s.webhook === null ? "" : html `<label class="choice"><input type="checkbox" name="rotate" value="1"> Make a new signing secret</label>`}<label>Folder (JSON Lines, a file per day)<input type="text" name="folder" value="${s.folder?.path ?? ""}" placeholder="/var/log/standing-orders" spellcheck="false"></label>${statusLine(view, "folder", s.folder?.path ?? null)}</fieldset><fieldset><legend>Traces</legend><p class="meta">Each run as an OpenTelemetry span: timings, model, tokens and cost. Never a prompt or code.</p><label>Collector (OTLP over HTTP)<input type="url" name="traces" value="${s.traces?.endpoint ?? ""}" placeholder="https://otel.example.com:4318" spellcheck="false"></label>${statusLine(view, "traces", s.traces?.endpoint ?? null)}<label>Header name (optional)<input type="text" name="header-name" value="${s.traces?.header?.name ?? ""}" placeholder="x-honeycomb-team" spellcheck="false"></label><label>Header value<input type="password" name="header-value" autocomplete="off" placeholder="${s.traces?.header ? "Saved; leave blank to keep" : "API key"}"></label></fieldset><div class="step-up"><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label></div><button type="submit">Save</button>`)}<fieldset><legend>Metrics</legend><p class="meta">Prometheus reads <code>${metrics}</code> with an instance operator's API token (<a href="/settings/sessions">Sessions & tokens</a>).</p><details><summary>Example</summary><pre>curl -H "Authorization: Bearer so_…" ${metrics}</pre></details></fieldset></section>`;
|
|
46
35
|
}
|
|
47
36
|
/** The webhook's signing secret, once: a focused page with no script. */
|
|
48
37
|
export function signingSecretHtml(url, secret) {
|
|
49
|
-
return `<h1>Signing secret</h1><div class="card"><p>Give this to the receiver at <span class="mono">${
|
|
50
|
-
`<p class="meta">Each request carries <code>x-standing-orders-signature: t=<time>,v1=<HMAC-SHA256 of "time.body"></code>. Check it, and that the time is recent.</p></div>` +
|
|
51
|
-
`<p class="meta"><a href="/settings/monitoring">Back to Monitoring</a></p>`;
|
|
38
|
+
return html `<h1>Signing secret</h1><div class="card"><p>Give this to the receiver at <span class="mono">${url}</span>. It's shown once.</p><p class="mono secret-value">${secret}</p><p class="meta">Each request carries <code>x-standing-orders-signature: t=<time>,v1=<HMAC-SHA256 of "time.body"></code>. Check it, and that the time is recent.</p></div><p class="meta"><a href="/settings/monitoring">Back to Monitoring</a></p>`;
|
|
52
39
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Saved notifications, read and told apart without a store: the row readers, the lifecycle key and the predicates
|
|
2
|
+
* the store and the chat core (chat-messages.ts) share. A leaf: it imports types only. */
|
|
3
|
+
import type { MateProposal, Notification, NotificationReceipt } from "./store.js";
|
|
4
|
+
/** The one task a card is about: a task its confirmation created (a new
|
|
5
|
+
* task, a revision), else the task it names (and, for feedback on a result,
|
|
6
|
+
* that result). Null for a card about no single task. A pointer for
|
|
7
|
+
* replies, never an authority. */
|
|
8
|
+
export declare function proposalTaskOf(proposal: MateProposal | null): {
|
|
9
|
+
task: string;
|
|
10
|
+
run: number | null;
|
|
11
|
+
} | null;
|
|
12
|
+
/** Every lifecycle dedupe key starts here: `life:<kind>:<identity>:<ordinal>`. */
|
|
13
|
+
export declare const LIFECYCLE_KEY_PREFIX = "life:";
|
|
14
|
+
/**
|
|
15
|
+
* Task lifecycle updates (Telegram task updates, 2026-09-16): the CLOSED
|
|
16
|
+
* vocabulary of routine progress facts the shared mutations record beside
|
|
17
|
+
* the change that made them true. None carries a push class — they are
|
|
18
|
+
* digest-eligible progress, never a page — and every one is project-bound
|
|
19
|
+
* through the task it names. A decision, incident, gap, stall, publication
|
|
20
|
+
* or merge page keeps its own producer; nothing here repeats one.
|
|
21
|
+
*/
|
|
22
|
+
export declare const LIFECYCLE_KINDS: readonly ["task-filed", "scope-approved", "approval-withdrawn", "task-held", "task-released", "task-queued", "task-requeued", "task-cancelled", "run-started", "run-phase", "check-progress", "run-finished", "run-stopping", "run-stopped", "run-resumed", "review-requested", "review-finished", "acceptance-evidence", "acceptance-ready"];
|
|
23
|
+
export type LifecycleKind = (typeof LIFECYCLE_KINDS)[number];
|
|
24
|
+
/** A progress fact, told apart by its durable key — never by display text. */
|
|
25
|
+
export declare function isLifecycleNotification(row: Pick<Notification, "dedupeKey">): boolean;
|
|
26
|
+
/** Only known progress producers can repaint a card. Decisions and urgent
|
|
27
|
+
* incidents keep their own alerts; a routine retry updates its saved attempt. */
|
|
28
|
+
export declare function isTelegramProgressNotification(row: Pick<Notification, "dedupeKey" | "kind" | "pushClass" | "run">): boolean;
|
|
29
|
+
export declare function readNotification(row: Record<string, unknown>): Notification;
|
|
30
|
+
export declare function readNotificationReceipt(row: Record<string, unknown>): NotificationReceipt;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** The one task a card is about: a task its confirmation created (a new
|
|
2
|
+
* task, a revision), else the task it names (and, for feedback on a result,
|
|
3
|
+
* that result). Null for a card about no single task. A pointer for
|
|
4
|
+
* replies, never an authority. */
|
|
5
|
+
export function proposalTaskOf(proposal) {
|
|
6
|
+
if (proposal === null)
|
|
7
|
+
return null;
|
|
8
|
+
const named = typeof proposal.payload["task"] === "string" && proposal.payload["task"] !== "" ? proposal.payload["task"] : null;
|
|
9
|
+
const creates = proposal.kind === "task" || (proposal.kind === "review" && proposal.payload["operation"] === "revise");
|
|
10
|
+
const made = creates && proposal.state === "confirmed" ? proposal.outcome?.["taskId"] : undefined;
|
|
11
|
+
if (typeof made === "string" && made !== "" && made !== named)
|
|
12
|
+
return { task: made, run: null };
|
|
13
|
+
if (named === null)
|
|
14
|
+
return null;
|
|
15
|
+
const run = proposal.kind === "review" ? proposal.payload["run"] : null;
|
|
16
|
+
return { task: named, run: typeof run === "number" && Number.isSafeInteger(run) ? run : null };
|
|
17
|
+
}
|
|
18
|
+
/** Every lifecycle dedupe key starts here: `life:<kind>:<identity>:<ordinal>`. */
|
|
19
|
+
export const LIFECYCLE_KEY_PREFIX = "life:";
|
|
20
|
+
/**
|
|
21
|
+
* Task lifecycle updates (Telegram task updates, 2026-09-16): the CLOSED
|
|
22
|
+
* vocabulary of routine progress facts the shared mutations record beside
|
|
23
|
+
* the change that made them true. None carries a push class — they are
|
|
24
|
+
* digest-eligible progress, never a page — and every one is project-bound
|
|
25
|
+
* through the task it names. A decision, incident, gap, stall, publication
|
|
26
|
+
* or merge page keeps its own producer; nothing here repeats one.
|
|
27
|
+
*/
|
|
28
|
+
export const LIFECYCLE_KINDS = [
|
|
29
|
+
"task-filed",
|
|
30
|
+
"scope-approved",
|
|
31
|
+
"approval-withdrawn",
|
|
32
|
+
"task-held",
|
|
33
|
+
"task-released",
|
|
34
|
+
"task-queued",
|
|
35
|
+
"task-requeued",
|
|
36
|
+
"task-cancelled",
|
|
37
|
+
"run-started",
|
|
38
|
+
"run-phase",
|
|
39
|
+
"check-progress",
|
|
40
|
+
"run-finished",
|
|
41
|
+
"run-stopping",
|
|
42
|
+
"run-stopped",
|
|
43
|
+
"run-resumed",
|
|
44
|
+
"review-requested",
|
|
45
|
+
"review-finished",
|
|
46
|
+
"acceptance-evidence",
|
|
47
|
+
"acceptance-ready",
|
|
48
|
+
];
|
|
49
|
+
/** A progress fact, told apart by its durable key — never by display text. */
|
|
50
|
+
export function isLifecycleNotification(row) {
|
|
51
|
+
return row.dedupeKey.startsWith(LIFECYCLE_KEY_PREFIX);
|
|
52
|
+
}
|
|
53
|
+
/** Only known progress producers can repaint a card. Decisions and urgent
|
|
54
|
+
* incidents keep their own alerts; a routine retry updates its saved attempt. */
|
|
55
|
+
export function isTelegramProgressNotification(row) {
|
|
56
|
+
if (row.kind === "check-progress" && row.pushClass === "progress" && row.run !== null)
|
|
57
|
+
return isLifecycleNotification(row);
|
|
58
|
+
if (row.pushClass !== null)
|
|
59
|
+
return false;
|
|
60
|
+
if (row.kind === "build-failed" && row.run !== null && row.dedupeKey === `run:${row.run}:failed`)
|
|
61
|
+
return true;
|
|
62
|
+
// The owner's lead took the attempt on, or let it lapse (lead-voice.ts): its card repaints in place.
|
|
63
|
+
if ((row.kind === "lead-on-it" || row.kind === "lead-lapsed") && row.run !== null)
|
|
64
|
+
return true;
|
|
65
|
+
return isLifecycleNotification(row) &&
|
|
66
|
+
["run-started", "run-phase", "run-finished", "review-requested", "review-finished", "run-stopping", "run-stopped", "run-resumed", "task-held", "task-released"].includes(row.kind);
|
|
67
|
+
}
|
|
68
|
+
export function readNotification(row) {
|
|
69
|
+
return {
|
|
70
|
+
id: Number(row["id"]),
|
|
71
|
+
recipient: row["recipient"] == null ? null : String(row["recipient"]),
|
|
72
|
+
dedupeKey: String(row["dedupe_key"]),
|
|
73
|
+
scope: row["provenance_scope"],
|
|
74
|
+
project: row["project"] == null ? null : String(row["project"]),
|
|
75
|
+
taskRef: row["task_ref"] == null ? null : Number(row["task_ref"]),
|
|
76
|
+
taskId: row["task_id"] == null ? null : String(row["task_id"]),
|
|
77
|
+
run: row["source_run"] == null ? null : Number(row["source_run"]),
|
|
78
|
+
kind: String(row["kind"]),
|
|
79
|
+
subject: String(row["subject"]),
|
|
80
|
+
body: String(row["body"]),
|
|
81
|
+
createdAt: String(row["created_at"]),
|
|
82
|
+
resolvedAt: row["resolved_at"] === null || row["resolved_at"] === undefined
|
|
83
|
+
? null
|
|
84
|
+
: String(row["resolved_at"]),
|
|
85
|
+
pushClass: (row["push_class"] === null || row["push_class"] === undefined) && String(row["kind"]) === "check-progress"
|
|
86
|
+
? "progress"
|
|
87
|
+
: row["push_class"] === null || row["push_class"] === undefined
|
|
88
|
+
? null
|
|
89
|
+
: String(row["push_class"]),
|
|
90
|
+
link: row["link"] === null || row["link"] === undefined ? null : String(row["link"]),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function readNotificationReceipt(row) {
|
|
94
|
+
return {
|
|
95
|
+
...readNotification(row),
|
|
96
|
+
attempts: Number(row["attempts"]),
|
|
97
|
+
lastAttemptAt: row["last_attempt_at"] == null ? null : String(row["last_attempt_at"]),
|
|
98
|
+
lastError: row["last_error"] == null ? null : String(row["last_error"]),
|
|
99
|
+
deliveredAt: row["delivered_at"] == null ? null : String(row["delivered_at"]),
|
|
100
|
+
receipt: row["receipt"] == null ? null : String(row["receipt"]),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Html } from "./html.js";
|
|
1
2
|
export type OpenRouterModel = {
|
|
2
3
|
id: string;
|
|
3
4
|
name: string;
|
|
@@ -25,6 +26,6 @@ export declare function fetchOpenRouterModels(key: string | null, fetcher?: type
|
|
|
25
26
|
/** One bounded cache per console. A changed/removed key cannot reuse another account's list. */
|
|
26
27
|
export declare function openRouterModelsCache(fetcher?: typeof fetch): (key: string | null, refresh?: boolean) => Promise<OpenRouterModels>;
|
|
27
28
|
export declare function modelDollars(amount: number | null): string;
|
|
28
|
-
export declare function openRouterPicker(catalog: OpenRouterModels, selected: string | null, refreshUrl: string):
|
|
29
|
+
export declare function openRouterPicker(catalog: OpenRouterModels, selected: string | null, refreshUrl: string): Html;
|
|
29
30
|
/** Only filters already-rendered options; credentials never enter this script. */
|
|
30
31
|
export declare function openRouterPickerScript(): string;
|