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
|
@@ -2,9 +2,10 @@ import type { BudgetLimit } from './request-budget.js';
|
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
3
|
export declare const TELEMETRY_WINDOW_MS = 300000;
|
|
4
4
|
export declare const LATENCY_BUCKETS: number[];
|
|
5
|
-
export declare const ROUTE_FAMILIES: readonly ['cli', 'mcp', 'team', '
|
|
5
|
+
export declare const ROUTE_FAMILIES: readonly ['cli', 'mcp', 'team', 'live', 'chat', 'flows', 'tasks', 'results', 'decisions', 'sessions', 'settings', 'projects', 'assets', 'auth', 'metrics', 'health', 'console', 'other'];
|
|
6
6
|
export type RouteFamily = typeof ROUTE_FAMILIES[number];
|
|
7
|
-
|
|
7
|
+
/** Every page's live updates are one stream (GET /live). */
|
|
8
|
+
export type StreamFamily = 'live' | 'other';
|
|
8
9
|
export declare const STREAM_FAMILIES: readonly StreamFamily[];
|
|
9
10
|
export declare const QUEUE_BUCKETS: number[];
|
|
10
11
|
/** Only literals from the allowlist can become labels, including for unknown or malformed paths. */
|
|
@@ -54,7 +55,7 @@ export type HealthSnapshot = {
|
|
|
54
55
|
export declare class ServerTelemetry {
|
|
55
56
|
readonly clock: () => number;
|
|
56
57
|
readonly requests: TimingHistogram;
|
|
57
|
-
readonly routes: Map<"assets" | "auth" | "chat" | "
|
|
58
|
+
readonly routes: Map<"assets" | "auth" | "chat" | "cli" | "console" | "decisions" | "flows" | "health" | "live" | "mcp" | "metrics" | "other" | "projects" | "results" | "sessions" | "settings" | "tasks" | "team", TimingHistogram>;
|
|
58
59
|
readonly eventLoop: TimingHistogram;
|
|
59
60
|
readonly writeWait: TimingHistogram;
|
|
60
61
|
readonly writeHold: TimingHistogram;
|
package/dist/server-telemetry.js
CHANGED
|
@@ -5,8 +5,8 @@ const SLICE_MS = 10_000;
|
|
|
5
5
|
/** monitorEventLoopDelay samples by sleeping this long, so every raw sample includes it. */
|
|
6
6
|
const EVENT_LOOP_RESOLUTION_MS = 20;
|
|
7
7
|
export const LATENCY_BUCKETS = [0.0001, 0.00025, 0.0005, 0.001, 0.0025, 0.005, 0.01, 0.015, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 30, 60];
|
|
8
|
-
export const ROUTE_FAMILIES = ['cli', 'mcp', 'team', '
|
|
9
|
-
export const STREAM_FAMILIES = ['
|
|
8
|
+
export const ROUTE_FAMILIES = ['cli', 'mcp', 'team', 'live', 'chat', 'flows', 'tasks', 'results', 'decisions', 'sessions', 'settings', 'projects', 'assets', 'auth', 'metrics', 'health', 'console', 'other'];
|
|
9
|
+
export const STREAM_FAMILIES = ['live', 'other'];
|
|
10
10
|
export const QUEUE_BUCKETS = [0, 1024, 4096, 16384, 65536, 262144, 1048576];
|
|
11
11
|
/** Only literals from the allowlist can become labels, including for unknown or malformed paths. */
|
|
12
12
|
export function routeFamily(raw) {
|
|
@@ -21,14 +21,10 @@ export function routeFamily(raw) {
|
|
|
21
21
|
return 'cli';
|
|
22
22
|
if (path === '/mcp' || path.startsWith('/mcp/'))
|
|
23
23
|
return 'mcp';
|
|
24
|
-
if (path === '/
|
|
25
|
-
return '
|
|
24
|
+
if (path === '/live')
|
|
25
|
+
return 'live';
|
|
26
26
|
if (path === '/api/team')
|
|
27
27
|
return 'team';
|
|
28
|
-
if (path === '/chat/stream')
|
|
29
|
-
return 'chat-stream';
|
|
30
|
-
if (/^\/flows\/[^/]+\/live$/.test(path))
|
|
31
|
-
return 'flow-stream';
|
|
32
28
|
if (path === '/metrics')
|
|
33
29
|
return 'metrics';
|
|
34
30
|
if (path === '/health')
|
package/dist/skills-ui.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Html } from "./html.js";
|
|
1
2
|
import type { SkillsSnapshot, SkillsView, skillTestResult } from "./project-skills.js";
|
|
2
3
|
export declare const SKILLS_CSS = ".skills{max-width:780px;min-width:0;overflow-wrap:anywhere}.skills h2{font-size:1.1rem}.skills .card{padding:18px;margin:14px 0;min-width:0}.skills form,.skills label{display:grid;gap:10px}.skills input,.skills textarea,.skills select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.skills textarea{resize:vertical;min-height:130px}.skills button{justify-self:start;white-space:nowrap;min-height:44px}.skills summary{cursor:pointer;min-height:44px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}.skills pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:360px;overflow:auto;font-size:.9rem}.skills .skill-heading{display:flex;align-items:baseline;justify-content:space-between;gap:12px}.skills .skill-heading h2{margin:0}.skills .skill-state{font-size:.85rem;flex-shrink:0}.skills .skill-source{font-size:.85rem}.skills .skill-controls{display:flex;gap:12px;flex-wrap:wrap}.skills .skill-controls form{margin:0}.skills details details{margin-top:10px}.skills [hidden]{display:none!important}.skills .skill-files{max-height:200px;overflow:auto}.skills .problem{padding:12px}@media(max-width:600px){.skills input,.skills textarea,.skills select{font-size:16px}.skills .card{padding:14px}.skills .skill-heading{align-items:flex-start}}";
|
|
3
4
|
export declare function skillsHtml(view: SkillsView, csrf: string, canManage: boolean, options?: {
|
|
@@ -5,7 +6,7 @@ export declare function skillsHtml(view: SkillsView, csrf: string, canManage: bo
|
|
|
5
6
|
error?: string;
|
|
6
7
|
draft?: Record<string, string>;
|
|
7
8
|
agent?: string;
|
|
8
|
-
}):
|
|
9
|
-
export declare function skillsSnapshotHtml(snapshot: SkillsSnapshot | null):
|
|
9
|
+
}): Html;
|
|
10
|
+
export declare function skillsSnapshotHtml(snapshot: SkillsSnapshot | null): Html | "";
|
|
10
11
|
export declare function skillsScript(): string;
|
|
11
|
-
export declare function skillTestFeedbackHtml(test: NonNullable<ReturnType<typeof skillTestResult>>, csrf: string, canManage: boolean, error?: string, draft?: string):
|
|
12
|
+
export declare function skillTestFeedbackHtml(test: NonNullable<ReturnType<typeof skillTestResult>>, csrf: string, canManage: boolean, error?: string, draft?: string): Html;
|
package/dist/skills-ui.js
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { whenUtc } from "./when-html.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
.join("");
|
|
3
|
+
import { html, joinHtml, postForm } from "./html.js";
|
|
4
|
+
// Hidden fields as they always were: a missing value is an empty field.
|
|
5
|
+
const fields = (data) => Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v ?? "")]));
|
|
7
6
|
export const SKILLS_CSS = `.skills{max-width:780px;min-width:0;overflow-wrap:anywhere}.skills h2{font-size:1.1rem}.skills .card{padding:18px;margin:14px 0;min-width:0}.skills form,.skills label{display:grid;gap:10px}.skills input,.skills textarea,.skills select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.skills textarea{resize:vertical;min-height:130px}.skills button{justify-self:start;white-space:nowrap;min-height:44px}.skills summary{cursor:pointer;min-height:44px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}.skills pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:360px;overflow:auto;font-size:.9rem}.skills .skill-heading{display:flex;align-items:baseline;justify-content:space-between;gap:12px}.skills .skill-heading h2{margin:0}.skills .skill-state{font-size:.85rem;flex-shrink:0}.skills .skill-source{font-size:.85rem}.skills .skill-controls{display:flex;gap:12px;flex-wrap:wrap}.skills .skill-controls form{margin:0}.skills details details{margin-top:10px}.skills [hidden]{display:none!important}.skills .skill-files{max-height:200px;overflow:auto}.skills .problem{padding:12px}@media(max-width:600px){.skills input,.skills textarea,.skills select{font-size:16px}.skills .card{padding:14px}.skills .skill-heading{align-items:flex-start}}`;
|
|
8
7
|
function source(skill) {
|
|
9
|
-
return `<p class="skill-source">${
|
|
8
|
+
return html `<p class="skill-source">${skill.source} · ${skill.sha.slice(0, 10)}</p><p>${skill.files.length} files · ${Math.ceil(skill.files.reduce((n, f) => n + Buffer.from(f.base64, "base64").length, 0) / 1024)} KB</p><pre>${Buffer.from(skill.files.find((f) => f.path === "SKILL.md").base64, "base64").toString("utf8")}</pre><details><summary>Package files</summary><ul class="skill-files">${skill.files.map((f) => html `<li>${f.path}</li>`)}</ul></details>`;
|
|
10
9
|
}
|
|
11
10
|
export function skillsHtml(view, csrf, canManage, options = {}) {
|
|
12
11
|
const manage = canManage && !!csrf, base = {
|
|
13
|
-
csrf,
|
|
14
12
|
repo: view.repo,
|
|
15
13
|
identity: view.identity,
|
|
16
14
|
revision: view.revision,
|
|
17
15
|
};
|
|
18
|
-
const form = (action, contents, extra = {}) =>
|
|
16
|
+
const form = (action, contents, extra = {}) => postForm("/settings/skills/change", contents, { hidden: fields({ ...base, action, ...extra }) });
|
|
19
17
|
const active = Object.values(view.selection).filter((c) => c.enabled).length;
|
|
20
18
|
const renderSkill = (skill) => {
|
|
21
19
|
const enabled = view.selection[skill.name]?.sha === skill.sha &&
|
|
22
20
|
view.selection[skill.name]?.enabled === true;
|
|
23
21
|
const replaced = view.selection[skill.name]?.enabled &&
|
|
24
22
|
view.selection[skill.name]?.sha !== skill.sha;
|
|
25
|
-
return `<article class="card" id="skill-${skill.sha}"><div class="skill-heading"><h2>${
|
|
23
|
+
return html `<article class="card" id="skill-${skill.sha}"><div class="skill-heading"><h2>${skill.name}</h2><span class="skill-state">${enabled ? "Enabled" : "In library"}</span></div><p>${skill.description}</p>${skill.requirements ? html `<p><strong>Requires:</strong> ${skill.requirements} <span class="meta">Availability is checked when the agent tests or uses it.</span></p>` : ""}${skill.warnings.map((w) => html `<p class="meta">${w}</p>`)}<details${options.focus === skill.sha ? html ` open` : ""}><summary>Review skill</summary>${source(skill)}${manage ? form(enabled ? "disable" : "enable", html `${replaced ? html `<p>Replaces the enabled version of this skill for new tasks.</p>` : ""}<button>${enabled ? "Disable skill" : "Enable skill"}</button>`, { sha: skill.sha }) : ""}</details>${manage ? html `<details${options.draft?.["sha"] === skill.sha ? html ` open` : ""}><summary>Test skill</summary>${form("test", html `<p>${options.agent ?? "Uses this project’s configured report agent"}. Runs a read-only sample through the normal task and approval flow.</p><label>Sample request<textarea name="sample" required maxlength="800" placeholder="Review this project’s home page and suggest three clearer button labels.">${options.draft?.["sha"] === skill.sha ? (options.draft?.["sample"] ?? "") : ""}</textarea></label><p class="meta">The report shows what worked and what needs tools or permission. Enabled status alone does not prove use.</p><button>Create test</button>`, { sha: skill.sha, nonce: randomUUID() })}</details>` : ""}</article>`;
|
|
26
24
|
};
|
|
27
25
|
const names = [...new Set(view.library.map((s) => s.name))];
|
|
28
26
|
const cards = names
|
|
@@ -31,29 +29,28 @@ export function skillsHtml(view, csrf, canManage, options = {}) {
|
|
|
31
29
|
versions.find((s) => s.sha === view.selection[name]?.sha) ??
|
|
32
30
|
versions.at(-1);
|
|
33
31
|
const older = versions.filter((s) => s !== primary);
|
|
34
|
-
return `<div data-skill-group data-skill-query="${
|
|
35
|
-
})
|
|
36
|
-
.join("");
|
|
32
|
+
return html `<div data-skill-group data-skill-query="${name + " " + primary.description}">${renderSkill(primary)}${older.length ? html `<details><summary>Other versions (${older.length})</summary>${older.map(renderSkill)}</details>` : ""}</div>`;
|
|
33
|
+
});
|
|
37
34
|
const search = names.length > 5
|
|
38
|
-
?
|
|
35
|
+
? html `<label>Search library<input type="search" data-skill-search placeholder="Name or description"></label><p data-skill-no-matches hidden>No matching skills.</p>`
|
|
39
36
|
: "";
|
|
40
37
|
const d = options.draft ?? {};
|
|
41
38
|
const add = manage
|
|
42
|
-
? `<details class="card"${options.error && options.draft?.["method"] ?
|
|
39
|
+
? html `<details class="card"${options.error && options.draft?.["method"] ? html ` open` : ""}><summary>Add skill</summary>${postForm("/settings/skills/import", html `<label>Add from<select name="method" data-skill-method><option value="paste"${d["method"] === "paste" ? html ` selected` : ""}>Paste SKILL.md</option><option value="github"${d["method"] === "github" ? html ` selected` : ""}>GitHub folder</option><option value="folder"${d["method"] === "folder" ? html ` selected` : ""}>Local folder</option></select></label><div data-skill-input="paste"><label>SKILL.md<textarea name="content" rows="8" maxlength="24576" placeholder="--- name: design-review description: Review interface copy and layout. --- Write your instructions here.">${d["content"]}</textarea></label></div><div data-skill-input="github"><label>Public GitHub folder<input name="url" type="url" value="${d["url"]}" placeholder="https://github.com/owner/repo/tree/main/skills/review"></label></div><div data-skill-input="folder"><label>Skill folder<input type="file" webkitdirectory multiple data-skill-folder></label><input type="hidden" name="files" value=""><p class="meta">Choose the folder containing SKILL.md. Up to 64 files, 1 MB total.</p><noscript>Folder uploads require JavaScript. You can paste SKILL.md or use a GitHub folder.</noscript></div><p class="meta">Added to your library first. Review its source and requirements before enabling it.</p><p data-skill-error role="alert" hidden></p><button>Add to library</button>`, { attrs: { "data-skill-import": true }, hidden: fields(base) })}</details>`
|
|
43
40
|
: "";
|
|
44
41
|
const history = view.history.length
|
|
45
|
-
? `<details><summary>Change history</summary>${view.history.map((h) => `<div class="card"><p>Selection ${h.revision} · ${
|
|
42
|
+
? html `<details><summary>Change history</summary>${view.history.map((h) => html `<div class="card"><p>Selection ${h.revision} · ${h.actor} · ${whenUtc(h.at)}</p><ul>${h.enabled.length ? h.enabled.map((s) => html `<li>${s.name} · ${s.version}</li>`) : html `<li>No skills enabled</li>`}</ul>${manage && h.revision !== view.revision ? form("restore", html `<button>Restore selection</button>`, { restore: h.revision }) : html `<span class="meta">Current selection</span>`}</div>`)}</details>`
|
|
46
43
|
: "";
|
|
47
|
-
return `<section class="skills">${options.error ? `<p class="problem" role="alert">${
|
|
44
|
+
return html `<section class="skills">${options.error ? html `<p class="problem" role="alert">${options.error}</p>` : ""}<p>${active ? `${active} skill${active === 1 ? "" : "s"} enabled for new tasks.` : "No skills enabled for this project."} Active tasks keep their saved versions.</p>${add}${search}${cards.length ? cards : html `<p>Your library is empty. Add a skill to review and enable it here.</p>`}${history}</section>`;
|
|
48
45
|
}
|
|
49
46
|
export function skillsSnapshotHtml(snapshot) {
|
|
50
47
|
if (!snapshot || !snapshot.packages.length)
|
|
51
48
|
return "";
|
|
52
|
-
return `<details class="skills"><summary>Skills supplied (${snapshot.packages.length})</summary><p class="meta">Exact versions supplied to this run. This does not confirm the agent read or applied them.</p>${snapshot.packages.map((s) => `<details><summary>${
|
|
49
|
+
return html `<details class="skills"><summary>Skills supplied (${snapshot.packages.length})</summary><p class="meta">Exact versions supplied to this run. This does not confirm the agent read or applied them.</p>${snapshot.packages.map((s) => html `<details><summary>${s.name} · ${s.sha.slice(0, 10)}</summary>${source(s)}</details>`)}</details>`;
|
|
53
50
|
}
|
|
54
51
|
export function skillsScript() {
|
|
55
52
|
return `(()=>{const search=document.querySelector('[data-skill-search]');if(search)search.addEventListener('input',()=>{let found=0;document.querySelectorAll('[data-skill-group]').forEach(el=>{el.hidden=!el.dataset.skillQuery.toLowerCase().includes(search.value.trim().toLowerCase());if(!el.hidden)found++;});document.querySelector('[data-skill-no-matches]').hidden=found>0;});const form=document.querySelector('[data-skill-import]');if(!form)return;const method=form.querySelector('[data-skill-method]'),error=form.querySelector('[data-skill-error]');function sync(){form.querySelectorAll('[data-skill-input]').forEach(el=>{el.hidden=el.dataset.skillInput!==method.value;el.querySelectorAll('input,textarea').forEach(i=>i.disabled=el.hidden);});}method.addEventListener('change',sync);sync();form.addEventListener('submit',async event=>{if(method.value!=='folder')return;if(form.dataset.prepared==='yes'){delete form.dataset.prepared;return;}event.preventDefault();error.hidden=true;const button=form.querySelector('button');button.disabled=true;try{const files=Array.from(form.querySelector('[data-skill-folder]').files);if(!files.length||files.length>64||files.reduce((n,f)=>n+f.size,0)>1048576||files.some(f=>f.size>262144))throw Error('Choose one skill folder with up to 64 files and 1 MB total.');const packed=[];for(const file of files){const parts=file.webkitRelativePath.split('/');parts.shift();const data=new Uint8Array(await file.arrayBuffer());let binary='';for(const byte of data)binary+=String.fromCharCode(byte);packed.push({path:parts.join('/'),base64:btoa(binary)});}form.querySelector('[name=files]').value=JSON.stringify(packed);form.dataset.prepared='yes';button.disabled=false;form.requestSubmit();}catch(e){error.textContent=e.message||'The folder could not be read. Select it again.';error.hidden=false;button.disabled=false;}});})();`;
|
|
56
53
|
}
|
|
57
54
|
export function skillTestFeedbackHtml(test, csrf, canManage, error = "", draft = "") {
|
|
58
|
-
return `<section class="skills" id="skill-test-feedback"><h2>Improve this test</h2>${test.sourceRun ? `<p>Revises test run #${test.sourceRun}.</p>` : ""}${test.feedback ? `<details><summary>Feedback for this test</summary><p>${
|
|
55
|
+
return html `<section class="skills" id="skill-test-feedback"><h2>Improve this test</h2>${test.sourceRun ? html `<p>Revises test run #${test.sourceRun}.</p>` : ""}${test.feedback ? html `<details><summary>Feedback for this test</summary><p>${test.feedback}</p></details>` : ""}${test.revisions.length ? html `<p>${joinHtml(test.revisions.map((id, i) => html `<a href="/t/${encodeURIComponent(id)}">Test revision ${i + 1}</a>`), " · ")}</p>` : ""}${error ? html `<p role="alert">${error}</p>` : ""}${canManage && csrf ? postForm("/settings/skills/revise", html `<label>What should change?<textarea name="feedback" required maxlength="500" rows="3" placeholder="Try a clearer action label and explain why it fits the user’s goal.">${draft}</textarea></label><p class="meta">Creates another report task with this skill version and your feedback. Review its scope before it runs.</p><button>Create revision</button>`, { hidden: fields({ repo: test.repo, run: test.run, nonce: randomUUID() }) }) : ""}</section>`;
|
|
59
56
|
}
|
package/dist/skills.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export declare function applyInstall(repo: string, writeContext: boolean): Insta
|
|
|
67
67
|
* project setup flow.
|
|
68
68
|
*/
|
|
69
69
|
export declare const CLAUDE_CODE_MANAGED_MARK = "<!-- standing-orders:claude-code-skill:v1 -->";
|
|
70
|
-
export declare const CLAUDE_CODE_GUIDES: readonly ["console", "operating", "runner"];
|
|
70
|
+
export declare const CLAUDE_CODE_GUIDES: readonly ["console", "operating", "runner", "release"];
|
|
71
71
|
/** The thin user-level skill `toolroll onboard` writes (agent-onboard.ts). */
|
|
72
72
|
export declare const OPERATOR_SKILL_MARK = "<!-- toolroll:operator-skill:v1";
|
|
73
73
|
export type ClaudeCodeSkillFile = {
|
package/dist/skills.js
CHANGED
|
@@ -161,7 +161,7 @@ export function applyInstall(repo, writeContext) {
|
|
|
161
161
|
* project setup flow.
|
|
162
162
|
*/
|
|
163
163
|
export const CLAUDE_CODE_MANAGED_MARK = "<!-- standing-orders:claude-code-skill:v1 -->";
|
|
164
|
-
export const CLAUDE_CODE_GUIDES = ["console", "operating", "runner"];
|
|
164
|
+
export const CLAUDE_CODE_GUIDES = ["console", "operating", "runner", "release"];
|
|
165
165
|
/** The thin user-level skill `toolroll onboard` writes (agent-onboard.ts). */
|
|
166
166
|
export const OPERATOR_SKILL_MARK = "<!-- toolroll:operator-skill:v1";
|
|
167
167
|
export function defaultClaudeCodeSkillDir(home = homedir()) {
|
|
@@ -213,6 +213,7 @@ Use the installed \`toolroll\` command. Its live help is authoritative.
|
|
|
213
213
|
- [Console guide](console.md) — where a person can review and act.
|
|
214
214
|
- [Operating guide](operating.md) — the command contract and safe workflow.
|
|
215
215
|
- [Runner guide](runner.md) — claims, leases, heartbeats, and fencing.
|
|
216
|
+
- [Release guide](release.md) — releasing Toolroll itself with \`toolroll release <branch>\`.
|
|
216
217
|
`;
|
|
217
218
|
}
|
|
218
219
|
/** One linked file, generated from the guide embedded in this binary. */
|
package/dist/slack-api.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ChatDeliveryError } from "./chat-delivery-state.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type ChatIdentity } from "./chat-delivery-state.js";
|
|
3
|
+
/** A Slack app installation: the chat identity, with the workspace (team) it belongs to. */
|
|
4
|
+
export type SlackIdentity = ChatIdentity & {
|
|
5
|
+
team: string;
|
|
6
|
+
};
|
|
3
7
|
export type SlackCredentials = SlackIdentity & {
|
|
4
8
|
appToken: string;
|
|
5
9
|
botToken: string;
|
package/dist/slack-api.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ChatDeliveryError } from "./chat-delivery-state.js";
|
|
|
2
2
|
import { chmodSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
|
-
import {
|
|
5
|
+
import { chatHash } from "./chat-delivery-state.js";
|
|
6
6
|
export class SlackError extends ChatDeliveryError {
|
|
7
7
|
constructor(code, retryMs = 5_000, uncertain = false) {
|
|
8
8
|
super(code, retryMs, uncertain);
|
|
@@ -128,7 +128,7 @@ export async function checkSlackCredentials(appToken, botToken, fetcher = fetch)
|
|
|
128
128
|
app: String(detail.app_id),
|
|
129
129
|
bot: String(auth.user_id),
|
|
130
130
|
workspace: typeof auth.team === "string" ? auth.team : "Slack",
|
|
131
|
-
installation:
|
|
131
|
+
installation: chatHash(`${auth.team_id}:${detail.app_id}:${auth.user_id}`),
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
export function object(value) {
|
package/dist/slack-chat.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { uploadSlackBytes, type SlackApi } from "./slack-api.js";
|
|
1
|
+
import { ChatState, type ChatBinding, type ChatContent, type ChatEvent } from "./chat-delivery-state.js";
|
|
2
|
+
import { uploadSlackBytes, type SlackApi, type SlackIdentity } from "./slack-api.js";
|
|
3
3
|
import { type Store } from "./store.js";
|
|
4
4
|
import type { SubscriptionMateRunner } from "./subscription-chat.js";
|
|
5
5
|
export type SlackChatOptions = {
|
|
@@ -17,9 +17,14 @@ export type SlackChatOptions = {
|
|
|
17
17
|
upload?: typeof uploadSlackBytes;
|
|
18
18
|
};
|
|
19
19
|
/** Synchronous receipt before ACK. Never stores response_url, raw tokens, or a pairing code. */
|
|
20
|
-
export declare function receiveSlack(state:
|
|
20
|
+
export declare function receiveSlack(state: ChatState, identity: SlackIdentity, type: string, raw: unknown, now: Date): boolean;
|
|
21
21
|
export declare const processSlackEvent: (options: SlackChatOptions) => Promise<boolean>;
|
|
22
|
-
export declare const applySlackAction: (options: SlackChatOptions, event:
|
|
22
|
+
export declare const applySlackAction: (options: SlackChatOptions, event: ChatEvent, binding: ChatBinding, repos: readonly string[]) => {
|
|
23
|
+
merge: import("./chat-decide.js").ChatMerge;
|
|
24
|
+
part: number;
|
|
25
|
+
shown: string;
|
|
26
|
+
content: ChatContent;
|
|
27
|
+
} | null;
|
|
23
28
|
export declare const planSlackNotifications: (options: SlackChatOptions) => Promise<void>;
|
|
24
29
|
export declare const planSlackRooms: (options: SlackChatOptions) => Promise<void>;
|
|
25
30
|
export declare function slackBlocks(text: string, buttons?: Record<string, unknown>[]): Record<string, unknown>[];
|
package/dist/slack-chat.js
CHANGED
|
@@ -9,8 +9,7 @@ import { processChatEvent, applyChatAction, planChatNotifications, channelAccess
|
|
|
9
9
|
import { proposalPreview, proposalLink, proposalOutcomeText, armedCardText, armedYesLabel, } from "./chat-channel.js";
|
|
10
10
|
import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
|
|
11
11
|
import { renderReply } from "./reply-shape.js";
|
|
12
|
-
import {
|
|
13
|
-
import { ChatDeliveryError, partContent } from "./chat-delivery-state.js";
|
|
12
|
+
import { ChatDeliveryError, ChatState, chatHash, partContent } from "./chat-delivery-state.js";
|
|
14
13
|
import { readChatPart } from "./contracts/chat-content.js";
|
|
15
14
|
import { readSlackBlockActions, readSlackButton, SLACK_LINK_ACTION } from "./contracts/slack-callback.js";
|
|
16
15
|
import { object, slackId, slackTs, slackMember, SlackError, uploadSlackBytes, } from "./slack-api.js";
|
|
@@ -64,12 +63,12 @@ export function receiveSlack(state, identity, type, raw, now) {
|
|
|
64
63
|
const match = /^pair ([a-f0-9]{32})$/.exec(event.text.trim());
|
|
65
64
|
kind = match ? "pair" : "message";
|
|
66
65
|
payload = match
|
|
67
|
-
? { hash:
|
|
66
|
+
? { hash: chatHash(match[1]) }
|
|
68
67
|
: {
|
|
69
68
|
text: event.text.slice(0, MATE_MESSAGE_MAX_CHARS + 1),
|
|
70
69
|
originalLength: event.text.length,
|
|
71
70
|
};
|
|
72
|
-
id =
|
|
71
|
+
id = chatHash(`${identity.installation}:event:${body.event_id}`);
|
|
73
72
|
}
|
|
74
73
|
else if (type === "interactive" && body.type === "block_actions") {
|
|
75
74
|
const interaction = readSlackBlockActions(body);
|
|
@@ -88,7 +87,7 @@ export function receiveSlack(state, identity, type, raw, now) {
|
|
|
88
87
|
const button = readSlackButton(interaction.value);
|
|
89
88
|
payload = button.ok ? { token: button.value.value } : { problem: button.issues.map(issue => issue.line).join("; ") };
|
|
90
89
|
const tapped = actions[0];
|
|
91
|
-
id =
|
|
90
|
+
id = chatHash(`${identity.installation}:action:${member}:${ts}:${tapped?.value ?? ""}:${tapped?.action_ts ?? ""}`);
|
|
92
91
|
}
|
|
93
92
|
else
|
|
94
93
|
return false;
|
|
@@ -131,7 +130,7 @@ export function receiveSlack(state, identity, type, raw, now) {
|
|
|
131
130
|
}
|
|
132
131
|
const delivery = (options) => ({
|
|
133
132
|
...options,
|
|
134
|
-
state: new
|
|
133
|
+
state: new ChatState(options.store, "slack"),
|
|
135
134
|
label: "Slack",
|
|
136
135
|
member: (member, channel) => slackMember(options.api, options.identity, member, channel),
|
|
137
136
|
// A 👍 on the owner's message where the app has reactions:write; Slack shows bots no typing indicator.
|
|
@@ -201,26 +200,20 @@ function linkButton(origin, link) {
|
|
|
201
200
|
}
|
|
202
201
|
}
|
|
203
202
|
export async function deliverSlackPart(options) {
|
|
204
|
-
const state = new
|
|
203
|
+
const state = new ChatState(options.store, "slack"), { store, identity } = options, now = nowOf(options);
|
|
205
204
|
if (!state.owns(identity.installation, options.owner, now) ||
|
|
206
205
|
!options.current())
|
|
207
206
|
return false;
|
|
208
|
-
const row = state.
|
|
209
|
-
.prepare("SELECT p.* FROM slack_part p JOIN slack_event e ON e.id=p.event WHERE e.installation=? AND p.state='pending' AND (e.kind!='notice' OR ?=1) AND (p.next_at IS NULL OR p.next_at<=?) ORDER BY p.id LIMIT 1")
|
|
210
|
-
.get(identity.installation, options.canNotify?.() === false ? 0 : 1, now.toISOString());
|
|
207
|
+
const row = state.nextPart(identity.installation, options.canNotify?.() !== false, now);
|
|
211
208
|
if (!row)
|
|
212
209
|
return false;
|
|
213
210
|
const event = state.event(row.event), binding = event.binding === null ? null : state.bindingById(event.binding);
|
|
214
211
|
if (!binding) {
|
|
215
|
-
state.
|
|
216
|
-
.prepare("UPDATE slack_part SET state='dropped',problem='Chat access changed' WHERE id=?")
|
|
217
|
-
.run(row.id);
|
|
212
|
+
state.dropPart(row.id, "Chat access changed");
|
|
218
213
|
return true;
|
|
219
214
|
}
|
|
220
215
|
if (new Date(row.created).getTime() + 86_400_000 < now.getTime()) {
|
|
221
|
-
state.
|
|
222
|
-
.prepare("UPDATE slack_part SET state='dropped',problem='Delivery expired; open the saved chat to recover it' WHERE id=?")
|
|
223
|
-
.run(row.id);
|
|
216
|
+
state.dropPart(row.id, "Delivery expired; open the saved chat to recover it");
|
|
224
217
|
return true;
|
|
225
218
|
}
|
|
226
219
|
try {
|
|
@@ -235,15 +228,15 @@ export async function deliverSlackPart(options) {
|
|
|
235
228
|
!repos.includes(store.lookupRef(content.task)?.repo ?? ""))
|
|
236
229
|
throw new SlackError("Connected projects changed");
|
|
237
230
|
// A screenshot sent with a result goes in that result's thread once its message is placed, and waits until then.
|
|
238
|
-
const result = content.shot?.follows == null ?
|
|
239
|
-
if (result !==
|
|
240
|
-
state.
|
|
231
|
+
const result = content.shot?.follows == null ? null : state.part(content.shot.follows);
|
|
232
|
+
if (result !== null && !slackTs(result.message) && result.state === "pending") {
|
|
233
|
+
state.holdPart(row.id, shotWaitsUntil(result.next_at, now));
|
|
241
234
|
return true;
|
|
242
235
|
}
|
|
243
|
-
const follows = result?.
|
|
236
|
+
const follows = result?.message;
|
|
244
237
|
const thread = slackTs(follows) ? String(follows) : event.thread;
|
|
245
238
|
if (content.image && content.shot && resultShotsPruned(options.evidenceRoot, content.image.run)) {
|
|
246
|
-
state.
|
|
239
|
+
state.dropPart(row.id, "Removed by retention");
|
|
247
240
|
return true;
|
|
248
241
|
}
|
|
249
242
|
if (content.image) {
|
|
@@ -262,17 +255,13 @@ export async function deliverSlackPart(options) {
|
|
|
262
255
|
typeof allocated.upload_url !== "string")
|
|
263
256
|
throw new SlackError("Slack did not identify the upload");
|
|
264
257
|
file = allocated.file_id;
|
|
265
|
-
state.
|
|
266
|
-
.prepare("UPDATE slack_part SET file=? WHERE id=?")
|
|
267
|
-
.run(file, row.id);
|
|
258
|
+
state.setPartFile(row.id, file);
|
|
268
259
|
await access(options, binding, session?.ceilingDigest);
|
|
269
260
|
const fresh = verifyResultImage(store, options.evidenceRoot, repos, content.image);
|
|
270
261
|
if (!fresh.ok)
|
|
271
262
|
throw new SlackError("Screenshot evidence changed before upload");
|
|
272
263
|
await (options.upload ?? uploadSlackBytes)(allocated.upload_url, fresh.bytes);
|
|
273
|
-
state.
|
|
274
|
-
.prepare("UPDATE slack_part SET uploaded=1 WHERE id=?")
|
|
275
|
-
.run(row.id);
|
|
264
|
+
state.setPartFile(row.id, null, true);
|
|
276
265
|
}
|
|
277
266
|
await access(options, binding, session?.ceilingDigest);
|
|
278
267
|
if (!verifyResultImage(store, options.evidenceRoot, repos, content.image)
|
|
@@ -288,9 +277,7 @@ export async function deliverSlackPart(options) {
|
|
|
288
277
|
.map(object)
|
|
289
278
|
.find((receipt) => (receipt.thread_ts ?? "") === thread && slackTs(receipt.ts));
|
|
290
279
|
if (found) {
|
|
291
|
-
state.
|
|
292
|
-
.prepare("UPDATE slack_part SET state='sent',message=?,problem=NULL WHERE id=?")
|
|
293
|
-
.run(String(found.ts), row.id);
|
|
280
|
+
state.partSent(row.id, String(found.ts), nowOf(options), false);
|
|
294
281
|
return true;
|
|
295
282
|
}
|
|
296
283
|
}
|
|
@@ -308,9 +295,7 @@ export async function deliverSlackPart(options) {
|
|
|
308
295
|
if (!Array.isArray(completed.files) ||
|
|
309
296
|
!completed.files.some((value) => object(value).id === file))
|
|
310
297
|
throw new SlackError("Slack did not confirm the file identity", 15_000, true);
|
|
311
|
-
state.
|
|
312
|
-
.prepare("UPDATE slack_part SET state='sent',problem=NULL WHERE id=?")
|
|
313
|
-
.run(row.id);
|
|
298
|
+
state.partSent(row.id, null, nowOf(options), false);
|
|
314
299
|
return true;
|
|
315
300
|
}
|
|
316
301
|
}
|
|
@@ -327,9 +312,7 @@ export async function deliverSlackPart(options) {
|
|
|
327
312
|
? armedCardText(proposal, preview.text)
|
|
328
313
|
: preview.text.replace("\n\nConfirm or Dismiss below. Nothing changes until you confirm.", "");
|
|
329
314
|
if (preview.buttons && preview.text.length <= 10_000) {
|
|
330
|
-
const tokens = state.
|
|
331
|
-
.prepare("SELECT token,phase FROM slack_action WHERE part=? AND consumed IS NULL AND expires>? ORDER BY rowid")
|
|
332
|
-
.all(row.id, now.toISOString());
|
|
315
|
+
const tokens = state.partTokens(row.id, now);
|
|
333
316
|
buttons = tokens.map((action) => ({
|
|
334
317
|
type: "button",
|
|
335
318
|
text: {
|
|
@@ -398,6 +381,10 @@ export async function deliverSlackPart(options) {
|
|
|
398
381
|
value: one.token,
|
|
399
382
|
}))
|
|
400
383
|
: []),
|
|
384
|
+
// A result's, plan's, failure's or pull request's own buttons (chat-decide.ts): its acts, then its links.
|
|
385
|
+
...(content.decide?.rows.flat() ?? []).flatMap((one, index) => "token" in one
|
|
386
|
+
? [{ type: "button", text: { type: "plain_text", text: one.label }, action_id: `toolroll_decide_${index}`, value: one.token, ...(index === 0 ? { style: "primary" } : {}) }]
|
|
387
|
+
: linkButton(options.origin(), one.link).map(button => ({ ...button, action_id: `toolroll_link_${index + 10}` }))),
|
|
401
388
|
...linkButton(options.origin(), content.link),
|
|
402
389
|
// [Look first] beside [Merge] or [Accept and finish]: each button's own action id.
|
|
403
390
|
...(content.also ?? []).flatMap((one, index) => linkButton(options.origin(), one).map(button => ({ ...button, action_id: `toolroll_link_${index + 2}` }))),
|
|
@@ -427,17 +414,13 @@ export async function deliverSlackPart(options) {
|
|
|
427
414
|
});
|
|
428
415
|
if (!slackTs(answer.ts) || (target && answer.ts !== target))
|
|
429
416
|
throw new SlackError("Slack did not confirm the message identity", 15_000, true);
|
|
430
|
-
state.
|
|
431
|
-
|
|
432
|
-
.run(answer.ts, row.id);
|
|
433
|
-
state.db
|
|
434
|
-
.prepare("UPDATE slack_runtime SET problem=NULL WHERE installation=? AND owner=?")
|
|
435
|
-
.run(identity.installation, options.owner);
|
|
417
|
+
state.partSent(row.id, String(answer.ts), nowOf(options));
|
|
418
|
+
state.setProblem(identity.installation, options.owner, null);
|
|
436
419
|
return true;
|
|
437
420
|
}
|
|
438
421
|
catch (error) {
|
|
439
422
|
if (error instanceof ChatDeliveryError && error.permanent) {
|
|
440
|
-
state.
|
|
423
|
+
state.dropPart(row.id, error.message);
|
|
441
424
|
return true;
|
|
442
425
|
}
|
|
443
426
|
// No permission to upload files here: one plain line instead of the result's screenshots.
|
|
@@ -449,18 +432,12 @@ export async function deliverSlackPart(options) {
|
|
|
449
432
|
const problem = error instanceof SlackError
|
|
450
433
|
? error
|
|
451
434
|
: new SlackError("Slack delivery is waiting to retry", 15_000, true);
|
|
452
|
-
state.
|
|
453
|
-
.prepare("UPDATE slack_runtime SET problem=? WHERE installation=? AND owner=?")
|
|
454
|
-
.run(problem.message, identity.installation, options.owner);
|
|
435
|
+
state.setProblem(identity.installation, options.owner, problem.message);
|
|
455
436
|
const until = new Date(nowOf(options).getTime() +
|
|
456
437
|
Math.max(problem.retryMs, [5000, 15000, 60000, 300000][Math.min(row.attempts, 3)])).toISOString();
|
|
457
|
-
state.
|
|
458
|
-
.prepare("UPDATE slack_part SET attempts=attempts+1,uncertain=uncertain+?,next_at=?,problem=? WHERE id=?")
|
|
459
|
-
.run(problem.uncertain ? 1 : 0, until, problem.message, row.id);
|
|
438
|
+
state.partFailed(row.id, { problem: problem.message, until, uncertain: problem.uncertain });
|
|
460
439
|
if (problem.code === "ratelimited")
|
|
461
|
-
state.
|
|
462
|
-
.prepare("UPDATE slack_runtime SET retry_at=? WHERE installation=?")
|
|
463
|
-
.run(until, identity.installation);
|
|
440
|
+
state.deferUntil(identity.installation, until);
|
|
464
441
|
return true;
|
|
465
442
|
}
|
|
466
443
|
}
|
package/dist/slack-settings.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Store } from "./store.js";
|
|
2
|
-
|
|
2
|
+
import { type Html } from "./html.js";
|
|
3
|
+
export declare function slackSettingsHtml(store: Store, dir: string, options?: {
|
|
3
4
|
code?: string;
|
|
4
5
|
problem?: string;
|
|
5
6
|
now?: Date;
|
|
6
7
|
who?: string;
|
|
7
|
-
}):
|
|
8
|
+
}): Html;
|
package/dist/slack-settings.js
CHANGED
|
@@ -1,75 +1,41 @@
|
|
|
1
1
|
import { loadPrimary } from "./webhooks.js";
|
|
2
|
+
import { html, postForm } from "./html.js";
|
|
2
3
|
import { loadSlackCredentials } from "./slack-api.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
.
|
|
7
|
-
.replaceAll(">", ">")
|
|
8
|
-
.replaceAll('"', """)
|
|
9
|
-
.replaceAll("'", "'");
|
|
10
|
-
export function slackSettingsHtml(store, dir, csrf, options = {}) {
|
|
11
|
-
const credentials = loadSlackCredentials(dir), state = new SlackState(store), bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [], binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null, others = bindings.length - (binding === null ? 0 : 1);
|
|
12
|
-
const runtime = credentials
|
|
13
|
-
? state.db
|
|
14
|
-
.prepare("SELECT connected,problem,lease_until FROM slack_runtime WHERE installation=?")
|
|
15
|
-
.get(credentials.installation)
|
|
16
|
-
: null;
|
|
4
|
+
import { ChatState } from "./chat-delivery-state.js";
|
|
5
|
+
export function slackSettingsHtml(store, dir, options = {}) {
|
|
6
|
+
const credentials = loadSlackCredentials(dir), state = new ChatState(store, "slack"), bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [], binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null, others = bindings.length - (binding === null ? 0 : 1);
|
|
7
|
+
const runtime = credentials ? state.runtime(credentials.installation) : null;
|
|
17
8
|
const now = options.now ?? new Date(), live = runtime &&
|
|
18
|
-
|
|
19
|
-
runtime.
|
|
9
|
+
runtime.leaseUntil !== null &&
|
|
10
|
+
runtime.leaseUntil > now.toISOString() &&
|
|
20
11
|
runtime.connected;
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
let content = "";
|
|
12
|
+
const password = html `<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>`;
|
|
13
|
+
const post = (action, content) => postForm(`/settings/slack/${action}`, content, { attrs: { class: "card" } });
|
|
14
|
+
const parts = [];
|
|
25
15
|
if (!credentials) {
|
|
26
|
-
|
|
27
|
-
"<p>Manage projects and review results in a private Slack conversation.</p>" +
|
|
28
|
-
'<details open><summary>Create your Slack app</summary><ol><li>Download the <a href="/settings/slack/manifest">app manifest</a>, then <a href="https://api.slack.com/apps" target="_blank" rel="noopener noreferrer">create an app</a> in your workspace using that manifest.</li><li>Under Basic Information, create an app-level token with <code>connections:write</code>.</li><li>Under OAuth & Permissions, install the app and copy its bot token.</li></ol></details>' +
|
|
29
|
-
post("connect", '<label>App token<input style="min-height:44px" type="password" name="app-token" autocomplete="off" placeholder="xapp-…" required></label><label>Bot token<input style="min-height:44px" type="password" name="bot-token" autocomplete="off" placeholder="xoxb-…" required></label>' +
|
|
30
|
-
password +
|
|
31
|
-
'<p class="meta">Tokens stay on this installation. Next, pair your own Slack account.</p><button type="submit">Connect Slack</button>');
|
|
16
|
+
parts.push(html `<p>Manage projects and review results in a private Slack conversation.</p>`, html `<details open><summary>Create your Slack app</summary><ol><li>Download the <a href="/settings/slack/manifest">app manifest</a>, then <a href="https://api.slack.com/apps" target="_blank" rel="noopener noreferrer">create an app</a> in your workspace using that manifest.</li><li>Under Basic Information, create an app-level token with <code>connections:write</code>.</li><li>Under OAuth & Permissions, install the app and copy its bot token.</li></ol></details>`, post("connect", html `<label>App token<input style="min-height:44px" type="password" name="app-token" autocomplete="off" placeholder="xapp-…" required></label><label>Bot token<input style="min-height:44px" type="password" name="bot-token" autocomplete="off" placeholder="xoxb-…" required></label>${password}<p class="meta">Tokens stay on this installation. Next, pair your own Slack account.</p><button type="submit">Connect Slack</button>`));
|
|
32
17
|
}
|
|
33
18
|
else {
|
|
34
|
-
|
|
19
|
+
parts.push(html `<p><strong>${credentials.workspace}</strong> · ${live ? "Connected" : "Waiting for connection"}</p>`);
|
|
35
20
|
if (runtime?.problem)
|
|
36
|
-
|
|
21
|
+
parts.push(html `<p role="status">${String(runtime.problem)}</p>`);
|
|
37
22
|
if (!live && !runtime?.problem)
|
|
38
|
-
|
|
39
|
-
"<p>Keep the Toolroll worker running to receive Slack messages.</p>";
|
|
23
|
+
parts.push(html `<p>Keep the Toolroll worker running to receive Slack messages.</p>`);
|
|
40
24
|
if (options.code) {
|
|
41
|
-
|
|
42
|
-
"<h2>Pair your account</h2><p>Send this in a direct message to your Toolroll Slack app. It expires in 10 minutes.</p>" +
|
|
43
|
-
`<label>Pairing message<input type="text" readonly value="pair ${escape(options.code)}" autocomplete="off" style="width:100%;max-width:100%;font-size:14px;min-height:44px"></label>` +
|
|
44
|
-
'<a class="button-link" style="min-height:44px;white-space:nowrap" href="/settings/slack">Check connection</a>';
|
|
25
|
+
parts.push(html `<h2>Pair your account</h2><p>Send this in a direct message to your Toolroll Slack app. It expires in 10 minutes.</p>`, html `<label>Pairing message<input type="text" readonly value="pair ${options.code}" autocomplete="off" style="width:100%;max-width:100%;font-size:14px;min-height:44px"></label>`, html `<a class="button-link" style="min-height:44px;white-space:nowrap" href="/settings/slack">Check connection</a>`);
|
|
45
26
|
}
|
|
46
27
|
else if (binding && state.live(binding)) {
|
|
47
|
-
const pending =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.prepare("SELECT count(*) n FROM slack_part p JOIN slack_event e ON e.id=p.event WHERE e.binding=? AND p.state='dropped'")
|
|
52
|
-
.get(binding.id)?.n ?? 0);
|
|
53
|
-
content +=
|
|
54
|
-
`<p>Your Slack account is paired.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} paired too.` : ""}${pending ? ` ${pending} replies waiting to send.` : ""}${failed ? ` ${failed} replies could not be delivered. Open the saved chat to recover them.` : ""}</p>` +
|
|
55
|
-
"<p>Try “What needs my attention?” or “Show the evidence for the latest result.”</p>" +
|
|
56
|
-
'<p><a href="/chat">Open saved chat</a></p>' +
|
|
57
|
-
(loadPrimary(process.env, dir) === "slack"
|
|
58
|
-
? "<p>Task updates are sent here.</p>"
|
|
59
|
-
: post("alerts", '<button type="submit">Send task updates here</button>'));
|
|
28
|
+
const { pending, dropped: failed } = state.partCounts(binding.id);
|
|
29
|
+
parts.push(html `<p>Your Slack account is paired.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} paired too.` : ""}${pending ? ` ${pending} replies waiting to send.` : ""}${failed ? ` ${failed} replies could not be delivered. Open the saved chat to recover them.` : ""}</p>`, html `<p>Try “What needs my attention?” or “Show the evidence for the latest result.”</p>`, html `<p><a href="/chat">Open saved chat</a></p>`, loadPrimary(process.env, dir) === "slack"
|
|
30
|
+
? html `<p>Task updates are sent here.</p>`
|
|
31
|
+
: post("alerts", html `<button type="submit">Send task updates here</button>`));
|
|
60
32
|
}
|
|
61
33
|
else {
|
|
62
|
-
|
|
63
|
-
(others ? ` ${others} teammate${others === 1 ? " is" : "s are"} already paired.` : "") + "</p>" +
|
|
64
|
-
password +
|
|
65
|
-
'<button type="submit">Create pairing code</button>');
|
|
34
|
+
parts.push(post("pair", html `<h2>Pair your account</h2><p>Your paired Slack account can read your connected projects and confirm proposed changes. Password approvals still open in Toolroll.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} already paired.` : ""}</p>${password}<button type="submit">Create pairing code</button>`));
|
|
66
35
|
}
|
|
67
36
|
if (binding && state.live(binding) && !options.code)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"<details><summary>Connection settings</summary>" +
|
|
71
|
-
post("disconnect", password + '<button type="submit">Disconnect Slack</button>') +
|
|
72
|
-
"</details>";
|
|
37
|
+
parts.push(post("unpair", html `${password}<p class="meta">Unpairing ends every open button in your chat. Teammates are unaffected.</p><button type="submit">Unpair my account</button>`));
|
|
38
|
+
parts.push(html `<details><summary>Connection settings</summary>${post("disconnect", html `${password}<button type="submit">Disconnect Slack</button>`)}</details>`);
|
|
73
39
|
}
|
|
74
|
-
return `<section style="max-width:42rem;overflow-wrap:anywhere"><p><a href="/settings">Settings</a></p><h1>Slack</h1>${options.problem ? `<p class="problem" role="alert">${
|
|
40
|
+
return html `<section style="max-width:42rem;overflow-wrap:anywhere"><p><a href="/settings">Settings</a></p><h1>Slack</h1>${options.problem ? html `<p class="problem" role="alert">${options.problem}</p>` : ""}${parts}<details><summary>Access and setup</summary><p>Private messages and threads use your Toolroll permissions. Shared channels are not supported. Secure review links use this installation’s configured HTTPS console address.</p><p>Socket Mode connects out to Slack. It does not need a public webhook or a Tailscale account.</p></details></section>`;
|
|
75
41
|
}
|