toolroll 0.9.86 → 0.9.87
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 +59 -0
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -70
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +329 -183
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/design/kit.css +685 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +19 -1
- package/dist/discord-settings.js +8 -4
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.js +7 -3
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +141 -30
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +38 -4
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +21 -0
- package/dist/release.js +40 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +3 -0
- package/dist/scope.js +6 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +16 -16
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +2 -2
- package/dist/server/flows.js +64 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +69 -0
- package/dist/server/kit.d.ts +405 -0
- package/dist/server/kit.js +361 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +22 -190
- package/dist/server/render-pages.js +123 -575
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +35 -5
- package/dist/server/render-tasks.js +114 -85
- package/dist/server/route-table.js +20 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +85 -2
- package/dist/server/tasks.d.ts +8 -0
- package/dist/server/tasks.js +95 -40
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +112 -3
- package/dist/store.js +292 -20
- package/dist/surface.js +5 -1
- package/dist/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.js +11 -7
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +7 -4
- package/package.json +1 -1
package/dist/settings-kit.d.ts
CHANGED
|
@@ -86,6 +86,7 @@ export declare const typedOnly: (row: ProofRow) => boolean;
|
|
|
86
86
|
* `data-so-typed`, so the identity-provider swap leaves it a real password field (keepTypedFields). Never a value. */
|
|
87
87
|
export declare function proofField(row: ProofRow, options?: {
|
|
88
88
|
label?: string;
|
|
89
|
+
optional?: boolean;
|
|
89
90
|
}): Html;
|
|
90
91
|
/**
|
|
91
92
|
* The identity-provider swap (render-people.ts ssoStepUps) with typed-only proof fields kept: those handlers take only
|
|
@@ -116,12 +117,6 @@ export type StepUpForm = {
|
|
|
116
117
|
/** What the dialog says the person is agreeing to (the action's own words by default). */
|
|
117
118
|
confirm?: string;
|
|
118
119
|
};
|
|
119
|
-
/**
|
|
120
|
-
* A section's form, posted to its existing route with its existing fields, and its sticky Save. When the row needs a
|
|
121
|
-
* proof, the proof field sits in the form's own HTML — inline (as today, which is how it works without JavaScript) and
|
|
122
|
-
* moved into a `<dialog>` inside the same form by settings-kit-script.ts, which opens it on submit, keeps the pressed
|
|
123
|
-
* button, and posts nothing on Cancel. The server still proves everything.
|
|
124
|
-
*/
|
|
125
120
|
export declare function stepUpForm(form: StepUpForm): Html;
|
|
126
121
|
/** A section: a heading, a one-line state, a problem the handler sent back (role=alert), then its form. */
|
|
127
122
|
export declare function kitSection(section: {
|
|
@@ -135,3 +130,44 @@ export declare function kitSection(section: {
|
|
|
135
130
|
/** The kit's look: one 760px column, sections with no nested wells, the sticky Save, the dialog. The needs-you colour
|
|
136
131
|
* falls back to the warning token until S5 lands `--so-needs-you`. */
|
|
137
132
|
export declare const SETTINGS_KIT_CSS: string;
|
|
133
|
+
export type SectionForm = {
|
|
134
|
+
/** The proof inventory row this form posts under. */
|
|
135
|
+
proof: string;
|
|
136
|
+
/** The exact route (one of the row's). */
|
|
137
|
+
action: string;
|
|
138
|
+
/** Unique on the page; the refusal slot is `<id>-err`. */
|
|
139
|
+
id: string;
|
|
140
|
+
/** The section's own fields. */
|
|
141
|
+
body: HtmlValue;
|
|
142
|
+
/** Hidden terms posted back unchanged (the shown project, a nonce, a digest…). */
|
|
143
|
+
hidden?: Readonly<Record<string, string | number | null | undefined>>;
|
|
144
|
+
/** The button: "Save" by default; a name=value it posts. */
|
|
145
|
+
save?: {
|
|
146
|
+
label?: string;
|
|
147
|
+
name?: string;
|
|
148
|
+
value?: string;
|
|
149
|
+
variant?: "primary" | "outline" | "danger";
|
|
150
|
+
};
|
|
151
|
+
/** A settings section: Save shows only once something in it changed, and saves only what changed with the rest. An
|
|
152
|
+
* act (Invite, Remove) leaves this off: its button always shows. */
|
|
153
|
+
gate?: boolean;
|
|
154
|
+
/** The proof is needed only when the form posts this value (the row's `when`): [field, value]. */
|
|
155
|
+
proofWhen?: readonly [field: string, value: string];
|
|
156
|
+
/** Post as a page navigation: the answer is a page of its own (a one-time secret, a review). */
|
|
157
|
+
native?: boolean;
|
|
158
|
+
/** The answer is a file the browser saves (implies native): the page stays, so the button never stays busy. */
|
|
159
|
+
download?: boolean;
|
|
160
|
+
/** What saying yes does, for the password popover (the row's own words by default). */
|
|
161
|
+
confirm?: string;
|
|
162
|
+
/** What the handler sent back for this section, in place: done, or refused (role=alert). */
|
|
163
|
+
said?: string | null;
|
|
164
|
+
problem?: string | null;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* A settings section's form in the revamp kit's look, posted to its existing route with its existing fields and
|
|
168
|
+
* proof. Without the kit's script it is a plain form: the proof field (when the row asks for one) shows in place and
|
|
169
|
+
* Save posts. With it (src/browser/kit/behavior.ts, section forms): Save appears once something changed, the proof is
|
|
170
|
+
* asked in a password popover only when the rules need it, one validated snapshot is posted, a second press does
|
|
171
|
+
* nothing while one is pending, and a refusal shows next to Save with the edits kept. The server proves everything.
|
|
172
|
+
*/
|
|
173
|
+
export declare function kitSectionForm(form: SectionForm): Html;
|
package/dist/settings-kit.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { attributes, html, htmlString, joinHtml, postForm, reauth, replaceMarkup } from "./html.js";
|
|
17
17
|
import { isSettingsFrame } from "./settings-kit-script.js";
|
|
18
|
+
import { requestContext } from "./server/request-context.js";
|
|
18
19
|
import { contractReads, PROOF_INVENTORY, proofRow } from "./settings-proofs.js";
|
|
19
20
|
export { isSettingsFrame };
|
|
20
21
|
const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', "#39": "'", "#x27": "'", nbsp: " " };
|
|
@@ -30,6 +31,17 @@ export function headingText(page) {
|
|
|
30
31
|
export function settingsCrumbs(pathname, title, page) {
|
|
31
32
|
if (!isSettingsFrame(pathname) || pathname === "/settings")
|
|
32
33
|
return undefined;
|
|
34
|
+
// A page in the revamp kit's Settings frame (server/kit.ts settingsShell) names its own trail: Settings, its group, itself.
|
|
35
|
+
const kit = /<nav class="k-tb-ctx k-crumbs" aria-label="Breadcrumb">([\s\S]*?)<\/nav>/.exec(page);
|
|
36
|
+
if (kit !== null) {
|
|
37
|
+
const trail = [{ label: "Settings", href: "/settings" }];
|
|
38
|
+
for (const step of kit[1].matchAll(/<a href="([^"]*)">([\s\S]*?)<\/a>|<b aria-current="page">([\s\S]*?)<\/b>/g)) {
|
|
39
|
+
const label = (headingText(`<h1>${step[2] ?? step[3] ?? ""}</h1>`) ?? "").slice(0, 80);
|
|
40
|
+
if (label !== "")
|
|
41
|
+
trail.push({ label, href: step[1] ?? null });
|
|
42
|
+
}
|
|
43
|
+
return trail;
|
|
44
|
+
}
|
|
33
45
|
const name = headingText(page) ?? (title.charAt(0).toUpperCase() + title.slice(1));
|
|
34
46
|
return [{ label: "Settings", href: "/settings" }, { label: name, href: null }];
|
|
35
47
|
}
|
|
@@ -96,7 +108,10 @@ export function proofField(row, options = {}) {
|
|
|
96
108
|
const label = options.label ?? "Your Toolroll password";
|
|
97
109
|
if (typedOnly(row))
|
|
98
110
|
return html `<label>${label}<input type="password" name="${row.field}" data-so-typed autocomplete="current-password" required></label>`;
|
|
99
|
-
|
|
111
|
+
// A row that needs its proof only for some posts (its `when`) leaves the field optional: the handler decides.
|
|
112
|
+
const field = options.optional === true && row.when !== undefined
|
|
113
|
+
? html `<label>${label}<input type="password" name="${row.field}" autocomplete="current-password"></label>`
|
|
114
|
+
: html `<label>${label}<input type="password" name="${row.field}" autocomplete="current-password" required></label>`;
|
|
100
115
|
return row.proof === "step-up" ? reauth(field) : field;
|
|
101
116
|
}
|
|
102
117
|
const TYPED_FIELD = /<input type="password" name="([a-z-]+)" data-so-typed autocomplete="current-password" required>/g;
|
|
@@ -128,11 +143,11 @@ export function proofNote(row) {
|
|
|
128
143
|
* moved into a `<dialog>` inside the same form by settings-kit-script.ts, which opens it on submit, keeps the pressed
|
|
129
144
|
* button, and posts nothing on Cancel. The server still proves everything.
|
|
130
145
|
*/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
146
|
+
/** The row for a form posting to `action` with these hidden terms; throws on a form the row doesn't describe. */
|
|
147
|
+
function rowForForm(proof, action, hidden) {
|
|
148
|
+
const row = proofRow(proof);
|
|
149
|
+
if (!row.routes.includes(action))
|
|
150
|
+
throw new Error(`${row.id} doesn't post to ${action}`);
|
|
136
151
|
for (const name of Object.keys(hidden))
|
|
137
152
|
if (row.field === name || row.secrets.includes(name))
|
|
138
153
|
throw new Error(`${row.id}: ${name} is never a hidden value`);
|
|
@@ -141,7 +156,12 @@ export function stepUpForm(form) {
|
|
|
141
156
|
throw new Error(`${row.id} posts ${row.select.field}=${row.select.value}`);
|
|
142
157
|
for (const other of variantsOf(row))
|
|
143
158
|
if (other.select !== undefined && String(hidden[other.select.field] ?? "") === other.select.value)
|
|
144
|
-
throw new Error(`${
|
|
159
|
+
throw new Error(`${action} with ${other.select.field}=${other.select.value} is ${other.id}, not ${row.id}`);
|
|
160
|
+
return row;
|
|
161
|
+
}
|
|
162
|
+
export function stepUpForm(form) {
|
|
163
|
+
const hidden = form.hidden ?? {};
|
|
164
|
+
const row = rowForForm(form.proof, form.action, hidden);
|
|
145
165
|
const proof = proofField(row);
|
|
146
166
|
const asks = htmlString(proof) !== "";
|
|
147
167
|
const note = proofNote(row);
|
|
@@ -184,3 +204,45 @@ export const SETTINGS_KIT_CSS = ":root{--so-settings-width:760px}" +
|
|
|
184
204
|
".kit-dialog::backdrop{background:rgb(0 0 0 / .35)}.kit-dialog h2{margin:0 0 6px;font-size:17px}.kit-dialog-slot .kit-proof{display:block}" +
|
|
185
205
|
".kit-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin:16px 0 0}" +
|
|
186
206
|
"@media (max-width:900px){.kit-save-bar button,.kit-dialog-actions button,.kit-crumbs a{min-height:44px}.kit-crumbs a{display:inline-flex;align-items:center}}";
|
|
207
|
+
/**
|
|
208
|
+
* A settings section's form in the revamp kit's look, posted to its existing route with its existing fields and
|
|
209
|
+
* proof. Without the kit's script it is a plain form: the proof field (when the row asks for one) shows in place and
|
|
210
|
+
* Save posts. With it (src/browser/kit/behavior.ts, section forms): Save appears once something changed, the proof is
|
|
211
|
+
* asked in a password popover only when the rules need it, one validated snapshot is posted, a second press does
|
|
212
|
+
* nothing while one is pending, and a refusal shows next to Save with the edits kept. The server proves everything.
|
|
213
|
+
*/
|
|
214
|
+
export function kitSectionForm(form) {
|
|
215
|
+
const hidden = form.hidden ?? {};
|
|
216
|
+
const row = rowForForm(form.proof, form.action, hidden);
|
|
217
|
+
if (form.proofWhen !== undefined && row.when === undefined)
|
|
218
|
+
throw new Error(`${row.id} always needs its proof`);
|
|
219
|
+
const proof = proofField(row, { label: "Your password", optional: form.proofWhen !== undefined });
|
|
220
|
+
const asks = htmlString(proof) !== "";
|
|
221
|
+
const save = form.save ?? {};
|
|
222
|
+
const button = html `<button${attributes({ type: "submit", class: `k-btn k-btn-${save.variant ?? "primary"} k-btn-sm`, name: save.name ?? null, value: save.value ?? null, "data-k-save": true })}><span class="k-face">${save.label ?? "Save"}</span></button>`;
|
|
223
|
+
const note = proofNote(row);
|
|
224
|
+
return postForm(form.action, joinHtml([
|
|
225
|
+
form.body,
|
|
226
|
+
asks ? html `<div class="k-sproof" data-k-proof>${proof}</div>` : "",
|
|
227
|
+
html `<div class="k-ssave"><span class="k-ssave-said" aria-live="polite">${form.said ?? ""}</span>${button}</div>`,
|
|
228
|
+
html `<div class="k-ssave-err" id="${form.id}-err">${form.problem ? html `<p class="k-notice" role="alert"><span>${form.problem}</span></p>`
|
|
229
|
+
: unconfirmedHere(form.id) ? html `<p class="k-notice info" role="status">Couldn’t confirm whether this saved. This page shows what is stored now; check it before saving again.</p>` : ""}</div>`,
|
|
230
|
+
]), { hidden, attrs: {
|
|
231
|
+
id: form.id, class: "k-sform", "data-k-sform": row.id, "data-k-gate": form.gate === true ? true : null, "data-k-native": form.native === true || form.download === true ? true : null, "data-k-download": form.download === true ? true : null,
|
|
232
|
+
"data-k-proof-if": form.proofWhen === undefined ? null : `${form.proofWhen[0]}=${form.proofWhen[1]}`,
|
|
233
|
+
"data-k-confirm": form.confirm ?? row.action, "data-k-note": note,
|
|
234
|
+
} });
|
|
235
|
+
}
|
|
236
|
+
/** Whether this request is the page again after a save of form `id` couldn't be confirmed (section-forms.ts asks for it
|
|
237
|
+
* with ?unconfirmed=<id>). It only picks which of this page's own forms shows the fixed note; the value is never drawn. */
|
|
238
|
+
function unconfirmedHere(id) {
|
|
239
|
+
const at = requestContext.getStore()?.returnTo;
|
|
240
|
+
if (at === undefined)
|
|
241
|
+
return false;
|
|
242
|
+
try {
|
|
243
|
+
return new URL(at, "http://toolroll.local").searchParams.get("unconfirmed") === id;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** Browser installation may write only this known file inside its project. */
|
|
17
17
|
export declare function previewProjectInstructions(repo: string): {
|
|
18
|
+
content?: never;
|
|
18
19
|
ok: false;
|
|
19
20
|
message: string;
|
|
20
21
|
plan?: never;
|
|
21
|
-
content?: never;
|
|
22
22
|
fingerprint?: never;
|
|
23
23
|
installed?: never;
|
|
24
24
|
} | {
|
|
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
30
30
|
installed: boolean;
|
|
31
31
|
};
|
|
32
32
|
export declare function addProjectInstructions(repo: string, fingerprint: string): {
|
|
33
|
+
content?: never;
|
|
33
34
|
ok: false;
|
|
34
35
|
message: string;
|
|
35
36
|
plan?: never;
|
|
36
|
-
content?: never;
|
|
37
37
|
fingerprint?: never;
|
|
38
38
|
installed?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|
package/dist/skills-ui.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { whenUtc } from "./when-html.js";
|
|
3
3
|
import { html, joinHtml, postForm } from "./html.js";
|
|
4
|
+
import { acts, btn, chip, disclose, facts, formRow, input, note, notice, select, settingsRecord, settingsSection, textarea } from "./server/kit.js";
|
|
4
5
|
// Hidden fields as they always were: a missing value is an empty field.
|
|
5
6
|
const fields = (data) => Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v ?? "")]));
|
|
6
7
|
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}}`;
|
|
7
8
|
function source(skill) {
|
|
8
9
|
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>`;
|
|
9
10
|
}
|
|
11
|
+
/** A skill's package as the settings page shows it: where it came from, its size, its SKILL.md, its files. */
|
|
12
|
+
function sourceKit(skill) {
|
|
13
|
+
const kb = Math.ceil(skill.files.reduce((n, f) => n + Buffer.from(f.base64, "base64").length, 0) / 1024);
|
|
14
|
+
return joinHtml([
|
|
15
|
+
facts([["Source", html `${skill.source} · <span class="k-mono">${skill.sha.slice(0, 10)}</span>`], ["Size", `${skill.files.length} files · ${kb} KB`]]),
|
|
16
|
+
html `<pre class="k-scode">${Buffer.from(skill.files.find((f) => f.path === "SKILL.md").base64, "base64").toString("utf8")}</pre>`,
|
|
17
|
+
disclose("Package files", html `<ul class="k-note">${skill.files.map((f) => html `<li class="k-mono">${f.path}</li>`)}</ul>`),
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
10
20
|
export function skillsHtml(view, csrf, canManage, options = {}) {
|
|
11
21
|
const manage = canManage && !!csrf, base = {
|
|
12
22
|
repo: view.repo,
|
|
@@ -20,7 +30,27 @@ export function skillsHtml(view, csrf, canManage, options = {}) {
|
|
|
20
30
|
view.selection[skill.name]?.enabled === true;
|
|
21
31
|
const replaced = view.selection[skill.name]?.enabled &&
|
|
22
32
|
view.selection[skill.name]?.sha !== skill.sha;
|
|
23
|
-
|
|
33
|
+
const toggle = manage ? form(enabled ? "disable" : "enable", joinHtml([
|
|
34
|
+
replaced ? note("Replaces the enabled version of this skill for new tasks.") : "",
|
|
35
|
+
acts(btn(enabled ? "Disable skill" : "Enable skill", { sm: true, type: "submit" })),
|
|
36
|
+
]), { sha: skill.sha }) : "";
|
|
37
|
+
const testing = manage ? disclose("Test skill", form("test", joinHtml([
|
|
38
|
+
note(`${options.agent ?? "Uses this project’s configured report agent"}. Runs a read-only sample through the normal task and approval flow.`),
|
|
39
|
+
formRow("Sample request", "The report shows what worked and what needs tools or permission. Enabled status alone does not prove use.", textarea({ id: `skill-sample-${skill.sha.slice(0, 12)}`, name: "sample", rows: 3, value: options.draft?.["sha"] === skill.sha ? (options.draft?.["sample"] ?? "") : "",
|
|
40
|
+
attrs: { required: true, maxlength: 800, placeholder: "Review this project’s home page and suggest three clearer button labels.", "aria-label": "Sample request" } }), "top"),
|
|
41
|
+
acts(btn("Create test", { sm: true, type: "submit" })),
|
|
42
|
+
]), { sha: skill.sha, nonce: randomUUID() }), options.draft?.["sha"] === skill.sha) : "";
|
|
43
|
+
return settingsRecord({
|
|
44
|
+
title: html `<span>${skill.name}</span>${chip(enabled ? "Enabled" : "In library", enabled ? "done" : "")}`,
|
|
45
|
+
sub: skill.description,
|
|
46
|
+
attrs: { id: `skill-${skill.sha}` },
|
|
47
|
+
more: joinHtml([
|
|
48
|
+
skill.requirements ? note(`Requires: ${skill.requirements}. Availability is checked when the agent tests or uses it.`) : "",
|
|
49
|
+
...skill.warnings.map((w) => note(w)),
|
|
50
|
+
disclose("Review skill", joinHtml([sourceKit(skill), toggle]), options.focus === skill.sha),
|
|
51
|
+
testing,
|
|
52
|
+
]),
|
|
53
|
+
});
|
|
24
54
|
};
|
|
25
55
|
const names = [...new Set(view.library.map((s) => s.name))];
|
|
26
56
|
const cards = names
|
|
@@ -29,19 +59,41 @@ export function skillsHtml(view, csrf, canManage, options = {}) {
|
|
|
29
59
|
versions.find((s) => s.sha === view.selection[name]?.sha) ??
|
|
30
60
|
versions.at(-1);
|
|
31
61
|
const older = versions.filter((s) => s !== primary);
|
|
32
|
-
return html `<div data-skill-group data-skill-query="${name + " " + primary.description}">${renderSkill(primary)}${older.length ?
|
|
62
|
+
return html `<div data-skill-group data-skill-query="${name + " " + primary.description}">${renderSkill(primary)}${older.length ? disclose(`Other versions (${older.length})`, joinHtml(older.map(renderSkill))) : ""}</div>`;
|
|
33
63
|
});
|
|
34
64
|
const search = names.length > 5
|
|
35
|
-
? html
|
|
65
|
+
? html `${formRow("Search library", null, input({ id: "skill-search", name: "skill-search", type: "search", attrs: { "data-skill-search": true, placeholder: "Name or description", "aria-label": "Search library" } }), "wide")}<p class="k-note" data-skill-no-matches hidden>No matching skills.</p>`
|
|
36
66
|
: "";
|
|
37
67
|
const d = options.draft ?? {};
|
|
38
68
|
const add = manage
|
|
39
|
-
?
|
|
69
|
+
? disclose("Add a skill", postForm("/settings/skills/import", joinHtml([
|
|
70
|
+
formRow("Add from", null, select({ id: "skill-method", name: "method", label: "Add from", current: d["method"] ?? "paste", attrs: { "data-skill-method": true },
|
|
71
|
+
items: [["paste", "Paste SKILL.md"], ["github", "GitHub folder"], ["folder", "Local folder"]] })),
|
|
72
|
+
html `<div data-skill-input="paste">${formRow("SKILL.md", null, textarea({ id: "skill-content", name: "content", rows: 8, value: d["content"] ?? "",
|
|
73
|
+
attrs: { maxlength: 24576, placeholder: "---\nname: design-review\ndescription: Review interface copy and layout.\n---\nWrite your instructions here.", "aria-label": "SKILL.md" } }), "top")}</div>`,
|
|
74
|
+
html `<div data-skill-input="github">${formRow("Public GitHub folder", null, input({ id: "skill-url", name: "url", type: "url", value: d["url"] ?? "",
|
|
75
|
+
attrs: { placeholder: "https://github.com/owner/repo/tree/main/skills/review", "aria-label": "Public GitHub folder" } }), "wide")}</div>`,
|
|
76
|
+
html `<div data-skill-input="folder">${formRow("Skill folder", "Choose the folder containing SKILL.md. Up to 64 files, 1 MB total.", html `<input class="k-input" id="skill-folder" type="file" webkitdirectory multiple data-skill-folder aria-label="Skill folder">`)}<input type="hidden" name="files" value="">\
|
|
77
|
+
<noscript>${note("Folder uploads need JavaScript. You can paste SKILL.md or use a GitHub folder.")}</noscript></div>`,
|
|
78
|
+
note("Added to your library first. Review its source and requirements before enabling it."),
|
|
79
|
+
html `<p class="k-err" data-skill-error role="alert" hidden></p>`,
|
|
80
|
+
acts(btn("Add to library", { variant: "primary", type: "submit" })),
|
|
81
|
+
]), { attrs: { "data-skill-import": true }, hidden: fields(base) }), Boolean(options.error && options.draft?.["method"]))
|
|
40
82
|
: "";
|
|
41
83
|
const history = view.history.length
|
|
42
|
-
?
|
|
84
|
+
? disclose("Change history", joinHtml(view.history.map((h) => settingsRecord({
|
|
85
|
+
title: `Selection ${h.revision}`,
|
|
86
|
+
sub: h.enabled.length ? h.enabled.map((s) => `${s.name} · ${s.version}`).join(", ") : "No skills enabled",
|
|
87
|
+
when: html `${h.actor} · ${whenUtc(h.at)}`,
|
|
88
|
+
action: manage && h.revision !== view.revision ? form("restore", btn("Restore selection", { sm: true, type: "submit" }), { restore: h.revision }) : note("Current selection"),
|
|
89
|
+
}))))
|
|
43
90
|
: "";
|
|
44
|
-
|
|
91
|
+
const library = settingsSection({
|
|
92
|
+
id: "library", title: "Library",
|
|
93
|
+
lead: `${active ? `${active} skill${active === 1 ? "" : "s"} enabled for new tasks.` : "No skills enabled for this project."} Active tasks keep their saved versions.`,
|
|
94
|
+
body: joinHtml([add, search, cards.length ? joinHtml(cards) : note("Your library is empty. Add a skill to review and enable it here."), history]),
|
|
95
|
+
});
|
|
96
|
+
return joinHtml([options.error ? notice(options.error) : "", library]);
|
|
45
97
|
}
|
|
46
98
|
export function skillsSnapshotHtml(snapshot) {
|
|
47
99
|
if (!snapshot || !snapshot.packages.length)
|
|
@@ -49,7 +101,7 @@ export function skillsSnapshotHtml(snapshot) {
|
|
|
49
101
|
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>`;
|
|
50
102
|
}
|
|
51
103
|
export function skillsScript() {
|
|
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;}});})();`;
|
|
104
|
+
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;error.textContent='';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;}});})();`;
|
|
53
105
|
}
|
|
54
106
|
export function skillTestFeedbackHtml(test, csrf, canManage, error = "", draft = "") {
|
|
55
107
|
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>`;
|
package/dist/slack-settings.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import { html } from "./html.js";
|
|
1
|
+
import { html, joinHtml } from "./html.js";
|
|
2
2
|
import { loadSlackCredentials } from "./slack-api.js";
|
|
3
|
-
import { chatSettingsHtml } from "./chat-settings-page.js";
|
|
3
|
+
import { chatSecretRow, chatSettingsHtml } from "./chat-settings-page.js";
|
|
4
4
|
export function slackSettingsHtml(store, dir, options = {}) {
|
|
5
5
|
return chatSettingsHtml(store, dir, options, {
|
|
6
6
|
provider: "slack",
|
|
7
7
|
label: "Slack",
|
|
8
8
|
credentials: loadSlackCredentials(dir),
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
intro: "Manage projects and review results in a private Slack conversation.",
|
|
10
|
+
steps: html `<ol class="k-note"><li>Download the <a class="k-link" href="/settings/slack/manifest">app manifest</a>, then <a class="k-link" 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 class="k-mono">connections:write</code>.</li><li>Under OAuth & Permissions, install the app and copy its bot token.</li></ol>`,
|
|
11
|
+
fields: joinHtml([
|
|
12
|
+
chatSecretRow({ id: "slack-app-token", name: "app-token", label: "App token", placeholder: "xapp-…" }),
|
|
13
|
+
chatSecretRow({ id: "slack-bot-token", name: "bot-token", label: "Bot token", placeholder: "xoxb-…" }),
|
|
14
|
+
]),
|
|
15
|
+
fieldsNote: "Tokens stay on this installation. Next, pair your own Slack account.",
|
|
16
|
+
pairWhere: "in a direct message to your Toolroll Slack app",
|
|
17
|
+
tryLine: html `Try “What needs my attention?” or “Show the evidence for the latest result.”`,
|
|
18
|
+
access: html `<p class="k-note">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 class="k-note">Socket Mode connects out to Slack. It does not need a public webhook or a Tailscale account.</p>`,
|
|
12
19
|
});
|
|
13
20
|
}
|
package/dist/spend-ui.d.ts
CHANGED
|
@@ -1,50 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Spend page (docs/plans/spend-dashboard.md, slice 1)
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* and
|
|
2
|
+
* The Spend page (docs/plans/spend-dashboard.md, slice 1), in the kit's Settings frame (group "spend"): the monthly
|
|
3
|
+
* limits first (the one that needs you on top), then what agent work cost in a month, with API dollars, plan usage
|
|
4
|
+
* and unknown cost kept apart and never added together, broken down by project and model (and, on demand, role,
|
|
5
|
+
* person and the costliest runs), the plan windows, and what isn't recorded here. An instance operator's page.
|
|
6
|
+
* A limit changes in place: Edit limit opens its form under the row (`?edit=<target>`), saved with a step-up.
|
|
6
7
|
*/
|
|
7
8
|
import { type Html } from "./html.js";
|
|
9
|
+
import { type ApprovalFacts } from "./design/kit.js";
|
|
8
10
|
import { type BudgetState, type SpendItem } from "./spend.js";
|
|
9
11
|
import type { SpendReport } from "./spend-report.js";
|
|
12
|
+
import type { BrowserLimits } from "./browser-workspace.js";
|
|
13
|
+
/** The page's own layout: the limit rows and meters, the summary figures and the quiet bars. Colour from tokens only. */
|
|
10
14
|
export declare const SPEND_CSS: string;
|
|
11
15
|
/** What this page doesn't record, in plain words (docs/plans/spend-dashboard.md §2.2). */
|
|
12
16
|
export declare const NOT_COUNTED: readonly string[];
|
|
17
|
+
/** A limit's row: a saved limit, or a target with none yet (Everything, a project) and what it spent so far. */
|
|
18
|
+
export type LimitRowView = {
|
|
19
|
+
target: string;
|
|
20
|
+
label: string;
|
|
21
|
+
budget: BudgetState | null;
|
|
22
|
+
spentMicrousd: number;
|
|
23
|
+
};
|
|
24
|
+
/** The open limit form: whose, the draft the person typed, and a refusal next to the field or under the form. */
|
|
25
|
+
export type LimitEdit = {
|
|
26
|
+
target: string;
|
|
27
|
+
usd?: string | null;
|
|
28
|
+
stop?: boolean | null;
|
|
29
|
+
fieldError?: string | null;
|
|
30
|
+
problem?: string | null;
|
|
31
|
+
};
|
|
13
32
|
export type SpendView = {
|
|
14
33
|
month: string;
|
|
15
34
|
previous: string;
|
|
16
35
|
next: string | null;
|
|
17
36
|
report: SpendReport;
|
|
18
|
-
/** This month's limits
|
|
37
|
+
/** This month's saved limits (what the budget gate counts today), whichever month is shown. */
|
|
19
38
|
budgets: BudgetState[] | null;
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/** Dollars for the page: a positive amount under a cent reads "<$0.01", never a false $0. */
|
|
25
|
-
export declare const money: (microusd: number) => string;
|
|
26
|
-
export declare function spendHtml(view: SpendView, notice: {
|
|
27
|
-
said?: string | null;
|
|
28
|
-
problem?: string | null;
|
|
29
|
-
}): Html;
|
|
30
|
-
export type SpendSettingsView = {
|
|
31
|
-
budgets: BudgetState[];
|
|
32
|
-
targets: {
|
|
39
|
+
/** Everything and each project with no limit yet, with what it spent this month. */
|
|
40
|
+
unlimited?: LimitRowView[];
|
|
41
|
+
/** People and subagents a limit can be added for (Add a limit). */
|
|
42
|
+
targets?: {
|
|
33
43
|
value: string;
|
|
34
44
|
label: string;
|
|
35
|
-
group: "
|
|
45
|
+
group: "People" | "Subagents";
|
|
36
46
|
}[];
|
|
47
|
+
/** The plan windows as their providers last reported them (limits-ui.ts limitsView), or nothing. */
|
|
48
|
+
windows: BrowserLimits | null;
|
|
37
49
|
subagentNames: Map<number, string>;
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
/** The limit form that is open, if any. */
|
|
51
|
+
edit?: LimitEdit | null;
|
|
52
|
+
/** What this request's approval shows (approvalFactsNow): a step-up asks for the password unless Passwordless is on. */
|
|
53
|
+
facts?: ApprovalFacts;
|
|
54
|
+
/** This month's name (YYYY-MM): the limits are this month's. */
|
|
55
|
+
current?: string;
|
|
43
56
|
};
|
|
44
|
-
/**
|
|
45
|
-
export declare
|
|
57
|
+
/** Dollars for the page: a positive amount under a cent reads "<$0.01", never a false $0. */
|
|
58
|
+
export declare const money: (microusd: number) => string;
|
|
59
|
+
/** A limit Settings flags as Needs you: one that pauses work and is at or near its limit. */
|
|
60
|
+
export declare const limitNeedsYou: (budget: BudgetState) => boolean;
|
|
61
|
+
export declare function spendHtml(view: SpendView, said: {
|
|
46
62
|
said?: string | null;
|
|
47
63
|
problem?: string | null;
|
|
48
64
|
}): Html;
|
|
65
|
+
/** Checks the amount next to its field before any password is asked or the form leaves the page; the server checks
|
|
66
|
+
* it again. Remove skips it. */
|
|
67
|
+
export declare function spendScript(): string;
|
|
49
68
|
/** One row per piece of spend, for a spreadsheet (the command line's `spend --csv`). */
|
|
50
69
|
export declare function spendCsv(items: readonly SpendItem[], subagentNames: Map<number, string>): string;
|