toolroll 0.9.54 → 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 +9 -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-polish.d.ts +5 -4
- package/dist/chat-polish.js +9 -10
- 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/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/health.d.ts +1 -3
- 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-settings.d.ts +3 -2
- package/dist/discord-settings.js +17 -42
- 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/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/mcp-oauth.js +35 -36
- package/dist/models-ui.d.ts +2 -1
- package/dist/models-ui.js +25 -35
- package/dist/monitoring-ui.d.ts +4 -3
- package/dist/monitoring-ui.js +13 -26
- package/dist/openrouter-models.d.ts +2 -1
- package/dist/openrouter-models.js +5 -9
- package/dist/operate.js +11 -7
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +13 -13
- 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/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/retention-ui.d.ts +7 -1
- package/dist/retention-ui.js +7 -11
- 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 +2181 -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-settings.d.ts +3 -2
- package/dist/slack-settings.js +17 -42
- 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/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/teammates-ui.d.ts +5 -4
- package/dist/teammates-ui.js +61 -90
- package/dist/teams-settings.d.ts +3 -2
- package/dist/teams-settings.js +20 -31
- package/dist/telegram-settings.d.ts +3 -2
- package/dist/telegram-settings.js +14 -25
- package/dist/toolroll-update-ui.d.ts +3 -2
- package/dist/toolroll-update-ui.js +35 -39
- 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-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/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/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
|
}
|
|
@@ -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;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Live discovery for the UI. Prices are USD, separate from rounded budget pins. */
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { html } from "./html.js";
|
|
3
4
|
import { readCappedBody } from "./converse.js";
|
|
4
5
|
import { validModelId } from "./provider.js";
|
|
5
6
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -76,7 +77,6 @@ export function openRouterModelsCache(fetcher = fetch) {
|
|
|
76
77
|
return value;
|
|
77
78
|
};
|
|
78
79
|
}
|
|
79
|
-
const escape = (s) => s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
80
80
|
export function modelDollars(amount) {
|
|
81
81
|
if (amount === null)
|
|
82
82
|
return "Not reported";
|
|
@@ -96,14 +96,10 @@ function details(model) {
|
|
|
96
96
|
export function openRouterPicker(catalog, selected, refreshUrl) {
|
|
97
97
|
const rows = catalog.ok ? catalog.models : [];
|
|
98
98
|
const current = rows.find(row => row.id === selected);
|
|
99
|
-
const manual = selected && !current ? `<option selected value="${
|
|
100
|
-
return `<div data-openrouter-picker><p class="meta">${catalog.ok
|
|
101
|
-
? `${rows.length} text models · ${catalog.source === "account" ? "Filtered for your OpenRouter account" : "Public catalog · Connect a key to check your account"} · Updated ${
|
|
102
|
-
:
|
|
103
|
-
`<label data-model-search hidden>Search models<input type="search" placeholder="Search by name or provider" autocomplete="off"></label>` +
|
|
104
|
-
`<label>Model<select name="model" aria-describedby="openrouter-price"><option value="__custom__"${selected === null ? " selected" : ""}>Choose a model</option>${manual}${rows.map(row => `<option value="${escape(row.id)}" data-detail="${escape(details(row))}"${row.id === selected ? " selected" : ""}>${escape(row.name)} · ${modelDollars(row.input)} in / ${modelDollars(row.output)} out per 1M</option>`).join("")}</select></label>` +
|
|
105
|
-
`<p id="openrouter-price" class="meta" aria-live="polite">${current ? escape(details(current)) : selected ? "This saved model is not in the loaded catalog. Availability and pricing are unverified." : "Choose a model to see its token prices and context size."}</p>` +
|
|
106
|
-
`<p data-model-count class="meta" aria-live="polite"></p><p class="meta"><a href="https://openrouter.ai/models" target="_blank" rel="noreferrer">Compare models on OpenRouter ↗</a> · Prices in USD; catalog browsing uses no model tokens.</p></div>`;
|
|
99
|
+
const manual = selected && !current ? html `<option selected value="${selected}" data-detail="This saved model is not in the loaded catalog. Availability and pricing are unverified.">${selected} · Saved choice</option>` : "";
|
|
100
|
+
return html `<div data-openrouter-picker><p class="meta">${catalog.ok
|
|
101
|
+
? `${rows.length} text models · ${catalog.source === "account" ? "Filtered for your OpenRouter account" : "Public catalog · Connect a key to check your account"} · Updated ${catalog.checkedAt.slice(11, 16)} UTC`
|
|
102
|
+
: catalog.problem} <a href="${refreshUrl}">Refresh models</a></p><label data-model-search hidden>Search models<input type="search" placeholder="Search by name or provider" autocomplete="off"></label><label>Model<select name="model" aria-describedby="openrouter-price"><option value="__custom__"${selected === null ? html ` selected` : ""}>Choose a model</option>${manual}${rows.map(row => html `<option value="${row.id}" data-detail="${details(row)}"${row.id === selected ? html ` selected` : ""}>${row.name} · ${modelDollars(row.input)} in / ${modelDollars(row.output)} out per 1M</option>`)}</select></label><p id="openrouter-price" class="meta" aria-live="polite">${current ? details(current) : selected ? "This saved model is not in the loaded catalog. Availability and pricing are unverified." : "Choose a model to see its token prices and context size."}</p><p data-model-count class="meta" aria-live="polite"></p><p class="meta"><a href="https://openrouter.ai/models" target="_blank" rel="noreferrer">Compare models on OpenRouter ↗</a> · Prices in USD; catalog browsing uses no model tokens.</p></div>`;
|
|
107
103
|
}
|
|
108
104
|
/** Only filters already-rendered options; credentials never enter this script. */
|
|
109
105
|
export function openRouterPickerScript() {
|
package/dist/operate.js
CHANGED
|
@@ -370,7 +370,7 @@ External trackers — build what a tracker nominates, under local approvals
|
|
|
370
370
|
|
|
371
371
|
toolroll claim <id> --runner <name> [--ttl <seconds>]
|
|
372
372
|
toolroll heartbeat <lease> still working; extends the lease
|
|
373
|
-
toolroll release <lease>
|
|
373
|
+
toolroll worker release <lease> done with it; fenced if superseded
|
|
374
374
|
toolroll reap release every lease that ran out
|
|
375
375
|
|
|
376
376
|
toolroll tick --runner <name> --token <t> --repo <path>
|
|
@@ -1143,8 +1143,10 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1143
1143
|
return claimCommand(positional, flags, context);
|
|
1144
1144
|
case "heartbeat":
|
|
1145
1145
|
return leaseCommand("heartbeat", positional, flags, context);
|
|
1146
|
-
case "
|
|
1147
|
-
|
|
1146
|
+
case "worker":
|
|
1147
|
+
if (positional[0] === "release")
|
|
1148
|
+
return leaseCommand("release", positional.slice(1), flags, context);
|
|
1149
|
+
return fail(context.write, context.json, "worker", "usage", "`toolroll worker release <lease>` returns a lease.", EXIT.usage);
|
|
1148
1150
|
case "reap":
|
|
1149
1151
|
return reapCommand(context);
|
|
1150
1152
|
case "runner":
|
|
@@ -1476,18 +1478,20 @@ function claimCommand(positional, flags, context) {
|
|
|
1476
1478
|
*/
|
|
1477
1479
|
function leaseCommand(command, positional, flags, context) {
|
|
1478
1480
|
const { store, write, json, now } = context;
|
|
1481
|
+
// `release` answers as the worker verb it now is (the bare `toolroll release <lease>` form is deprecated).
|
|
1482
|
+
const name = command === "release" ? "worker release" : command;
|
|
1479
1483
|
const lease = positional[0];
|
|
1480
1484
|
if (lease === undefined)
|
|
1481
|
-
return fail(write, json,
|
|
1485
|
+
return fail(write, json, name, "usage", `which lease? \`toolroll ${name} <lease>\``, EXIT.usage);
|
|
1482
1486
|
const ttl = readTtl(flags);
|
|
1483
1487
|
if (ttl === null)
|
|
1484
|
-
return fail(write, json,
|
|
1488
|
+
return fail(write, json, name, "usage", "--ttl takes whole seconds", EXIT.usage);
|
|
1485
1489
|
const result = command === "heartbeat" ? heartbeat(store, lease, now, ttl) : release(store, lease, now);
|
|
1486
1490
|
if (!result.ok) {
|
|
1487
1491
|
const message = result.reason === "fenced"
|
|
1488
1492
|
? "superseded — another runner holds this task now; stop rather than retry"
|
|
1489
1493
|
: "no such lease";
|
|
1490
|
-
return fail(write, json,
|
|
1494
|
+
return fail(write, json, name, result.reason, message, EXIT.refused);
|
|
1491
1495
|
}
|
|
1492
1496
|
if (command === "release") {
|
|
1493
1497
|
const task = store.getTask(String(refExternalId(store, result.claim.taskRef)));
|
|
@@ -1496,7 +1500,7 @@ function leaseCommand(command, positional, flags, context) {
|
|
|
1496
1500
|
if (task !== null && task.state === "running")
|
|
1497
1501
|
store.setTaskState(task.id, "queued", now);
|
|
1498
1502
|
}
|
|
1499
|
-
return succeed(write, json,
|
|
1503
|
+
return succeed(write, json, name, { lease: result.claim }, () => [
|
|
1500
1504
|
command === "heartbeat"
|
|
1501
1505
|
? `Still yours until ${result.claim.expiresAt}.`
|
|
1502
1506
|
: `Released ${result.claim.leaseId}.`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AuditAction } from "./remote-audit.js";
|
|
2
2
|
import type { ApiTokenRow } from "./store.js";
|
|
3
|
+
import { type Html } from "./html.js";
|
|
3
4
|
export declare const PEOPLE_AUDIT_CSS: string;
|
|
4
5
|
export type PersonAuditView = {
|
|
5
6
|
name: string;
|
|
@@ -11,4 +12,4 @@ export type PersonAuditView = {
|
|
|
11
12
|
now: Date;
|
|
12
13
|
};
|
|
13
14
|
export declare const personHref: (name: string, more?: Record<string, string>) => string;
|
|
14
|
-
export declare function personAuditHtml(view: PersonAuditView):
|
|
15
|
+
export declare function personAuditHtml(view: PersonAuditView): Html;
|
package/dist/people-audit-ui.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { basename } from "node:path";
|
|
7
7
|
import { tokenStatus } from "./remote-audit.js";
|
|
8
|
-
|
|
8
|
+
import { html } from "./html.js";
|
|
9
9
|
const stamp = (value) => `${value.slice(0, 16).replace("T", " ")} UTC`;
|
|
10
10
|
export const PEOPLE_AUDIT_CSS = `.person-audit{max-width:820px;min-width:0;overflow-wrap:anywhere}.person-audit h2{font-size:1.05rem;margin:1.25rem 0 8px}` +
|
|
11
11
|
`.person-audit .rows{display:grid;border:1px solid var(--so-line);border-radius:10px;overflow:hidden}.person-audit .row-item{padding:10px 14px;border-bottom:1px solid var(--so-line);min-width:0}` +
|
|
@@ -17,22 +17,22 @@ export const PEOPLE_AUDIT_CSS = `.person-audit{max-width:820px;min-width:0;overf
|
|
|
17
17
|
`@media(max-width:600px){.person-audit .row-item a{min-height:44px}}`;
|
|
18
18
|
export const personHref = (name, more = {}) => `/people?${new URLSearchParams({ person: name, ...more }).toString()}`;
|
|
19
19
|
export function personAuditHtml(view) {
|
|
20
|
-
const tokens = view.tokens.length === 0 ? `<p class="meta">No API tokens.</p>` : `<div class="rows">${view.tokens.map(one => {
|
|
20
|
+
const tokens = view.tokens.length === 0 ? html `<p class="meta">No API tokens.</p>` : html `<div class="rows">${view.tokens.map(one => {
|
|
21
21
|
const status = tokenStatus(one, view.now);
|
|
22
|
-
return `<div class="row-item" data-token-name="${
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
})
|
|
26
|
-
const filtered = view.token === null ? "" : `<p class="meta">With ${
|
|
27
|
-
const actions = view.actions.length === 0 ? `<p class="meta">No remote actions${view.token === null ? "" : " with this token"}.</p>` : `<div class="rows">${view.actions.map(actionHtml)
|
|
28
|
-
const older = view.nextCursor === null ? "" : `<p><a href="${
|
|
29
|
-
return `<p><a href="/people">People</a></p><h1>${
|
|
30
|
-
|
|
22
|
+
return html `<div class="row-item" data-token-name="${one.name}"><p><a href="${personHref(view.name, { "token-name": one.name })}">${one.name}</a></p>\
|
|
23
|
+
<p class="meta">${status === "active" ? "Active" : status === "expired" ? "Expired" : "Revoked"} · ${one.access === "act" ? "Can act" : "Read only"} · \
|
|
24
|
+
last used ${one.lastUsedAt === null ? "never" : stamp(one.lastUsedAt)}</p></div>`;
|
|
25
|
+
})}</div>`;
|
|
26
|
+
const filtered = view.token === null ? "" : html `<p class="meta">With ${view.token}</p><p><a href="${personHref(view.name)}">All tokens</a></p>`;
|
|
27
|
+
const actions = view.actions.length === 0 ? html `<p class="meta">No remote actions${view.token === null ? "" : " with this token"}.</p>` : html `<div class="rows">${view.actions.map(actionHtml)}</div>`;
|
|
28
|
+
const older = view.nextCursor === null ? "" : html `<p><a href="${personHref(view.name, { ...(view.token === null ? {} : { "token-name": view.token }), before: view.nextCursor })}">Older actions</a></p>`;
|
|
29
|
+
return html `<p><a href="/people">People</a></p><h1>${view.name}</h1><section class="person-audit"><h2>API tokens</h2>${tokens}\
|
|
30
|
+
<h2>Remote activity</h2>${filtered}${actions}${older}</section>`;
|
|
31
31
|
}
|
|
32
32
|
function actionHtml(action) {
|
|
33
33
|
const label = action.outcome === "ok" ? "OK" : action.outcome === "refused" ? "Refused" : "Error";
|
|
34
34
|
const detail = [stamp(action.at), action.token ?? "unknown token", action.source === "mcp" ? "Agent (MCP)" : "CLI",
|
|
35
35
|
...(action.repo === null ? [] : [basename(action.repo)]), ...(action.reason === null ? [] : [action.reason])];
|
|
36
|
-
return `<div class="row-item" data-action="${action.id}"><p><span class="outcome outcome-${action.outcome}">${label}</span>${
|
|
37
|
-
|
|
36
|
+
return html `<div class="row-item" data-action="${action.id}"><p><span class="outcome outcome-${action.outcome}">${label}</span>${action.name}\
|
|
37
|
+
${action.taskId === null ? "" : html ` · <a href="/t/${encodeURIComponent(action.taskId)}">${action.taskId}</a>`}</p><p class="meta">${detail.join(" · ")}</p></div>`;
|
|
38
38
|
}
|
package/dist/policy-ui.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* action ledger. Anyone signed in reads it; an instance operator changes it
|
|
5
5
|
* with their password.
|
|
6
6
|
*/
|
|
7
|
+
import { type Html } from "./html.js";
|
|
7
8
|
import type { LedgerEntry } from "./action-ledger.js";
|
|
8
9
|
import { type SavedPolicy } from "./policy.js";
|
|
9
10
|
export declare const POLICY_CSS: string;
|
|
@@ -14,7 +15,7 @@ export type PolicyView = {
|
|
|
14
15
|
/** The project tools here, as a hint for the tools list. */
|
|
15
16
|
toolNames: string[];
|
|
16
17
|
};
|
|
17
|
-
export declare function policyHtml(view: PolicyView,
|
|
18
|
+
export declare function policyHtml(view: PolicyView, notice: {
|
|
18
19
|
said?: string | null;
|
|
19
20
|
problem?: string | null;
|
|
20
|
-
}):
|
|
21
|
+
}): Html;
|