toolroll 0.9.55 → 0.9.56
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 +10 -0
- package/README.md +24 -8
- package/dist/action-ledger.d.ts +2 -4
- package/dist/action-ledger.js +2 -10
- package/dist/approval-policy.d.ts +5 -5
- package/dist/approval-policy.js +4 -4
- package/dist/approval-rules-ui.js +2 -2
- package/dist/browser/workspace.js +5 -5
- package/dist/browser-workspace.d.ts +12 -12
- package/dist/budget-alerts.js +3 -3
- package/dist/chat-actions.d.ts +23 -18
- package/dist/chat-actions.js +110 -88
- package/dist/chat-ask.d.ts +2 -2
- package/dist/chat-ask.js +5 -5
- package/dist/chat-channel.d.ts +15 -15
- package/dist/chat-channel.js +20 -20
- package/dist/chat-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- package/dist/chat-core.js +1 -1
- package/dist/chat-delivery-state.js +2 -2
- package/dist/chat-delivery.d.ts +2 -2
- package/dist/chat-delivery.js +27 -34
- package/dist/chat-flow.js +3 -3
- package/dist/chat-messages.d.ts +1 -1
- package/dist/chat-messages.js +4 -4
- package/dist/chat-migration.d.ts +1 -1
- package/dist/chat-migration.js +11 -11
- package/dist/chat-rooms.js +3 -3
- package/dist/chat-warmth.d.ts +2 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -7
- package/dist/contracts/chat-actions.d.ts +32 -27
- package/dist/contracts/chat-actions.js +29 -11
- package/dist/contracts/chat-content.d.ts +1 -1
- package/dist/contracts/chat-content.js +1 -1
- package/dist/contracts/chat-tables.js +4 -4
- package/dist/contracts/console-api.d.ts +6 -6
- package/dist/contracts/console-api.js +7 -7
- package/dist/contracts/flow-propose.d.ts +3 -3
- package/dist/contracts/flow.d.ts +132 -125
- package/dist/contracts/flow.js +27 -12
- package/dist/contracts/lead-context.d.ts +1 -1
- package/dist/contracts/lead-context.js +2 -2
- package/dist/contracts/lead-tools.d.ts +20 -18
- package/dist/contracts/lead-tools.js +15 -15
- package/dist/contracts/stage-output.d.ts +1 -1
- package/dist/contracts/stage-output.js +1 -1
- package/dist/contracts/store-json.d.ts +23 -22
- package/dist/contracts/store-json.js +17 -17
- package/dist/contracts/{teammate-turn.d.ts → subagent-turn.d.ts} +5 -5
- package/dist/contracts/{teammate-turn.js → subagent-turn.js} +7 -7
- package/dist/converse.d.ts +23 -23
- package/dist/converse.js +28 -28
- package/dist/coordinator-proposals.d.ts +2 -2
- package/dist/coordinator-proposals.js +1 -1
- package/dist/coordinator.js +1 -1
- package/dist/deprecations.d.ts +12 -0
- package/dist/deprecations.js +20 -0
- package/dist/desktop-update-gate.js +8 -4
- package/dist/desktop-update.js +6 -1
- package/dist/discord-chat.js +6 -6
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/envelope.js +0 -1
- package/dist/export-ui.js +1 -1
- package/dist/export.js +7 -7
- package/dist/flow-engine.d.ts +2 -2
- package/dist/flow-engine.js +21 -21
- package/dist/flow-insights.d.ts +1 -1
- package/dist/flow-share.js +13 -8
- package/dist/flow-steps.d.ts +5 -5
- package/dist/flow-steps.js +19 -19
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/flows-cli.js +1 -1
- package/dist/flows-ui.js +20 -20
- package/dist/flows.js +19 -19
- package/dist/guides.js +4 -3
- package/dist/invoke.js +1 -1
- package/dist/kits-ui.js +5 -5
- package/dist/kits.d.ts +12 -12
- package/dist/kits.js +31 -31
- package/dist/{mate-cli.d.ts → lead-cli.d.ts} +11 -11
- package/dist/{mate-cli.js → lead-cli.js} +38 -38
- package/dist/lead-commitments.js +4 -4
- package/dist/lead-context.d.ts +2 -2
- package/dist/lead-context.js +9 -9
- package/dist/{mate-contract.d.ts → lead-contract.d.ts} +3 -3
- package/dist/{mate-contract.js → lead-contract.js} +7 -6
- package/dist/{mate-doors.d.ts → lead-doors.d.ts} +6 -6
- package/dist/{mate-doors.js → lead-doors.js} +24 -24
- package/dist/lead-follow.d.ts +5 -5
- package/dist/lead-follow.js +9 -9
- package/dist/lead-people.d.ts +5 -5
- package/dist/lead-people.js +14 -14
- package/dist/{mate-progress.d.ts → lead-progress.d.ts} +9 -9
- package/dist/{mate-progress.js → lead-progress.js} +3 -3
- package/dist/{mate-tools.d.ts → lead-tools.d.ts} +19 -19
- package/dist/{mate-tools.js → lead-tools.js} +109 -101
- package/dist/{mate.d.ts → lead.d.ts} +27 -27
- package/dist/{mate.js → lead.js} +102 -102
- package/dist/limits-ui.d.ts +1 -1
- package/dist/limits-ui.js +1 -1
- package/dist/mcp-connect.d.ts +1 -1
- package/dist/mcp-connect.js +3 -3
- package/dist/mcp-core.js +1 -1
- package/dist/memory-pass.d.ts +2 -2
- package/dist/memory-pass.js +7 -7
- package/dist/notification-rows.d.ts +2 -2
- package/dist/operate.d.ts +3 -3
- package/dist/operate.js +35 -34
- package/dist/policy.d.ts +1 -1
- package/dist/principal.d.ts +1 -1
- package/dist/principal.js +2 -2
- package/dist/project-delete-ui.js +1 -1
- package/dist/project-delete.d.ts +1 -1
- package/dist/project-delete.js +26 -26
- package/dist/project-memory.js +1 -1
- package/dist/project-tools.d.ts +1 -1
- package/dist/project-tools.js +1 -1
- package/dist/recipe-ui.js +1 -1
- package/dist/retention.js +4 -4
- package/dist/scope.d.ts +2 -2
- package/dist/scout-report.d.ts +1 -1
- package/dist/scout-report.js +1 -1
- package/dist/serve.js +40 -45
- package/dist/server/chat.d.ts +4 -4
- package/dist/server/chat.js +97 -97
- package/dist/server/chrome.js +3 -3
- package/dist/server/flows.js +69 -62
- package/dist/server/guards.js +3 -3
- package/dist/server/http.d.ts +3 -3
- package/dist/server/http.js +1 -1
- package/dist/server/live.js +6 -6
- package/dist/server/people-tokens.js +1 -1
- package/dist/server/remote.js +12 -10
- package/dist/server/render-chat.d.ts +30 -30
- package/dist/server/render-chat.js +33 -33
- package/dist/server/render-pages.js +2 -2
- package/dist/server/render-settings.d.ts +7 -0
- package/dist/server/render-settings.js +3 -0
- package/dist/server/route-table.js +9 -7
- package/dist/server/runtime.d.ts +13 -13
- package/dist/server/settings.js +14 -13
- package/dist/server/tasks.js +6 -5
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.d.ts +2 -2
- package/dist/slack-chat.js +6 -6
- package/dist/spend-ui.d.ts +3 -3
- package/dist/spend-ui.js +9 -9
- package/dist/spend.d.ts +14 -14
- package/dist/spend.js +31 -31
- package/dist/store-baseline.d.ts +19 -0
- package/dist/store-baseline.js +2873 -0
- package/dist/store.d.ts +197 -249
- package/dist/store.js +747 -3084
- package/dist/subagent-admin.d.ts +53 -0
- package/dist/{teammate-admin.js → subagent-admin.js} +37 -37
- package/dist/{teammate-desk.d.ts → subagent-desk.d.ts} +22 -23
- package/dist/{teammate-desk.js → subagent-desk.js} +26 -35
- package/dist/{teammate-memory.d.ts → subagent-memory.d.ts} +17 -17
- package/dist/{teammate-memory.js → subagent-memory.js} +41 -41
- package/dist/{teammate-question.d.ts → subagent-question.d.ts} +6 -6
- package/dist/{teammate-question.js → subagent-question.js} +17 -17
- package/dist/{teammate-tools.d.ts → subagent-tools.d.ts} +18 -18
- package/dist/{teammate-tools.js → subagent-tools.js} +36 -36
- package/dist/{teammate-week.d.ts → subagent-week.d.ts} +10 -10
- package/dist/{teammate-week.js → subagent-week.js} +23 -23
- package/dist/{teammate-work.d.ts → subagent-work.d.ts} +21 -21
- package/dist/{teammate-work.js → subagent-work.js} +50 -50
- package/dist/subagents-ui.d.ts +22 -0
- package/dist/{teammates-ui.js → subagents-ui.js} +45 -45
- package/dist/{teammates.d.ts → subagents.d.ts} +11 -11
- package/dist/{teammates.js → subagents.js} +21 -21
- package/dist/subscription-chat.d.ts +13 -13
- package/dist/subscription-chat.js +19 -19
- package/dist/surface.d.ts +19 -2
- package/dist/surface.js +7 -31
- package/dist/team-cli.d.ts +1 -0
- package/dist/team-cli.js +14 -2
- package/dist/team-leads.js +13 -13
- package/dist/team-runtime.d.ts +2 -2
- package/dist/team-runtime.js +8 -8
- package/dist/teams-chat.js +6 -6
- package/dist/telegram-flow.d.ts +3 -3
- package/dist/telegram-flow.js +3 -3
- package/dist/{telegram-mate.d.ts → telegram-lead.d.ts} +5 -5
- package/dist/{telegram-mate.js → telegram-lead.js} +27 -27
- package/dist/telegram-status.js +1 -1
- package/dist/telegram-team.d.ts +1 -1
- package/dist/telegram-team.js +5 -5
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +9 -30
- package/dist/text-limits.d.ts +18 -18
- package/dist/text-limits.js +18 -18
- package/dist/toolroll-update.d.ts +8 -0
- package/dist/toolroll-update.js +45 -8
- package/package.json +1 -2
- package/dist/teammate-admin.d.ts +0 -53
- package/dist/teammates-ui.d.ts +0 -21
package/dist/slack-chat.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { chatFlowButtons } from "./chat-flow.js";
|
|
2
2
|
import { refuseResultShots, resultShotsPruned, shotWaitsUntil } from "./result-shots.js";
|
|
3
|
-
import { chatQuestionButtons } from "./
|
|
3
|
+
import { chatQuestionButtons } from "./subagent-question.js";
|
|
4
4
|
import { chatAskButtons } from "./chat-ask.js";
|
|
5
5
|
import { channelInbox } from "./chat-inbox.js";
|
|
6
6
|
import { roomCommand } from "./chat-rooms.js";
|
|
7
7
|
import { processChatEvent, applyChatAction, planChatNotifications, channelAccess, planRoomMessages, } from "./chat-delivery.js";
|
|
8
8
|
/** Slack is a transport for the same saved assistant and confirmation doors. */
|
|
9
9
|
import { proposalPreview, proposalLink, proposalOutcomeText, armedCardText, armedYesLabel, } from "./chat-channel.js";
|
|
10
|
-
import {
|
|
10
|
+
import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
|
|
11
11
|
import { renderReply } from "./reply-shape.js";
|
|
12
12
|
import { ChatDeliveryError, ChatState, chatHash, partContent } from "./chat-delivery-state.js";
|
|
13
13
|
import { readChatPart } from "./contracts/chat-content.js";
|
|
@@ -65,7 +65,7 @@ export function receiveSlack(state, identity, type, raw, now) {
|
|
|
65
65
|
payload = match
|
|
66
66
|
? { hash: chatHash(match[1]) }
|
|
67
67
|
: {
|
|
68
|
-
text: event.text.slice(0,
|
|
68
|
+
text: event.text.slice(0, LEAD_MESSAGE_MAX_CHARS + 1),
|
|
69
69
|
originalLength: event.text.length,
|
|
70
70
|
};
|
|
71
71
|
id = chatHash(`${identity.installation}:event:${body.event_id}`);
|
|
@@ -217,7 +217,7 @@ export async function deliverSlackPart(options) {
|
|
|
217
217
|
return true;
|
|
218
218
|
}
|
|
219
219
|
try {
|
|
220
|
-
const session = event.session === null ? null : store.
|
|
220
|
+
const session = event.session === null ? null : store.getLeadSession(event.session);
|
|
221
221
|
const repos = await access(options, binding, session?.ceilingDigest);
|
|
222
222
|
if (event.kind === "notice" && options.canNotify?.() === false)
|
|
223
223
|
return false;
|
|
@@ -300,7 +300,7 @@ export async function deliverSlackPart(options) {
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
if (content.proposal) {
|
|
303
|
-
const proposal = store.
|
|
303
|
+
const proposal = store.getLeadProposal(content.proposal);
|
|
304
304
|
if (!proposal)
|
|
305
305
|
text = "This proposal is unavailable.";
|
|
306
306
|
else if (proposal.state !== "pending")
|
|
@@ -363,7 +363,7 @@ export async function deliverSlackPart(options) {
|
|
|
363
363
|
...(one.action === "approve" ? { style: "primary" } : {}),
|
|
364
364
|
}))
|
|
365
365
|
: []),
|
|
366
|
-
// A
|
|
366
|
+
// A subagent's question (v93): its options, then "Answer in words".
|
|
367
367
|
...(content.question
|
|
368
368
|
? chatQuestionButtons(state, row.id, now).map((one) => ({
|
|
369
369
|
type: "button",
|
package/dist/spend-ui.d.ts
CHANGED
|
@@ -16,9 +16,9 @@ export type SpendView = {
|
|
|
16
16
|
targets: {
|
|
17
17
|
value: string;
|
|
18
18
|
label: string;
|
|
19
|
-
group: "Everything" | "Projects" | "People" | "
|
|
19
|
+
group: "Everything" | "Projects" | "People" | "Subagents";
|
|
20
20
|
}[];
|
|
21
|
-
|
|
21
|
+
subagentNames: Map<number, string>;
|
|
22
22
|
csrf: string;
|
|
23
23
|
};
|
|
24
24
|
export declare function spendHtml(view: SpendView, notice: {
|
|
@@ -26,4 +26,4 @@ export declare function spendHtml(view: SpendView, notice: {
|
|
|
26
26
|
problem?: string | null;
|
|
27
27
|
}): Html;
|
|
28
28
|
/** One row per piece of spend, for a spreadsheet. */
|
|
29
|
-
export declare function spendCsv(items: readonly SpendItem[],
|
|
29
|
+
export declare function spendCsv(items: readonly SpendItem[], subagentNames: Map<number, string>): string;
|
package/dist/spend-ui.js
CHANGED
|
@@ -41,32 +41,32 @@ export function spendHtml(view, notice) {
|
|
|
41
41
|
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
42
42
|
const total = view.items.reduce((sum, item) => sum + (item.microusd ?? 0), 0);
|
|
43
43
|
const unpriced = view.items.filter(item => item.microusd === null && (item.tokensIn !== null || item.kind !== "run")).length;
|
|
44
|
-
const counts = { run: view.items.filter(item => item.kind === "run").length,
|
|
45
|
-
const parts = [`${counts.run} ${counts.run === 1 ? "run" : "runs"}`, ...(counts.
|
|
46
|
-
const
|
|
44
|
+
const counts = { run: view.items.filter(item => item.kind === "run").length, subagent: view.items.filter(item => item.kind === "subagent").length, chat: view.items.filter(item => item.kind === "chat").length, sort: view.items.filter(item => item.kind === "sort").length };
|
|
45
|
+
const parts = [`${counts.run} ${counts.run === 1 ? "run" : "runs"}`, ...(counts.subagent > 0 ? [`${counts.subagent} subagent turns`] : []), ...(counts.chat > 0 ? [`${counts.chat} chat turns`] : []), ...(counts.sort > 0 ? [`${counts.sort} ${counts.sort === 1 ? "sort" : "sorts"}`] : [])];
|
|
46
|
+
const nameOfSubagent = (key) => view.subagentNames.get(Number(key)) ?? `Subagent ${key}`;
|
|
47
47
|
const budgets = view.budgets.map(budget => {
|
|
48
48
|
const width = Math.min(100, Math.max(0, budget.percent));
|
|
49
49
|
const over = budget.spentMicrousd >= budget.limitMicrousd;
|
|
50
|
-
const label = budgetLabel(budget, budget.scope === "
|
|
50
|
+
const label = budgetLabel(budget, budget.scope === "subagent" ? nameOfSubagent(budget.key) : undefined).replace(/'s$/, "");
|
|
51
51
|
return html `<div class="budget${over ? " over" : ""}" data-budget="${budget.id}"><span class="name">${label}</span><span class="figures">${usd(budget.spentMicrousd)} of ${usd(budget.limitMicrousd)} · ${budget.percent}%</span><span class="bar" aria-hidden="true"><span style="width:${width}%"></span></span><span class="meta">${over && budget.hardStop ? "Used up: new API work waits until next month or a higher budget." : budget.hardStop ? "Stops API work at 100%." : "Alerts only."}${budget.unpriced > 0 ? ` ${budget.unpriced} unpriced` : ""}</span><details><summary>Change</summary>${postForm("/spend/budget", html `<label>Monthly limit (US dollars)<input type="number" name="usd" min="1" step="1" value="${Math.round(budget.limitMicrousd / 1_000_000)}" required></label><label class="choice"><input type="checkbox" name="stop" value="1"${budget.hardStop ? html ` checked` : ""}> Stop new work at 100%</label><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label><button type="submit" name="action" value="save">Save</button> <button type="submit" name="action" value="remove">Remove</button>`, { attrs: { class: "budget-form" }, hidden: { target: `${budget.scope}:${budget.key}` } })}</details></div>`;
|
|
52
52
|
});
|
|
53
|
-
const groups = ["Everything", "Projects", "People", "
|
|
53
|
+
const groups = ["Everything", "Projects", "People", "Subagents"];
|
|
54
54
|
const options = groups.map(group => {
|
|
55
55
|
const members = view.targets.filter(one => one.group === group);
|
|
56
56
|
return members.length === 0 ? "" : html `<optgroup label="${group}">${members.map(one => html `<option value="${one.value}">${one.label}</option>`)}</optgroup>`;
|
|
57
57
|
});
|
|
58
58
|
const add = html `<details${view.budgets.length === 0 ? html ` open` : ""}><summary>Add a budget</summary>${postForm("/spend/budget", html `<label>For<select name="target" required>${options}</select></label><label>Monthly limit (US dollars)<input type="number" name="usd" min="1" step="1" required></label><label class="choice"><input type="checkbox" name="stop" value="1" checked> Stop new work at 100%</label><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label><button type="submit" name="action" value="save">Add budget</button>`, { attrs: { class: "budget-form" } })}</details>`;
|
|
59
|
-
return html `<article class="spend"><div class="spend-head"><h1>Spend</h1><p class="spend-month"><a href="/spend?month=${view.previous}">← ${view.previous}</a><strong>${view.month}</strong>${view.next === null ? "" : html `<a href="/spend?month=${view.next}">${view.next} →</a>`}<a href="/spend?month=${view.month}&format=csv" download>CSV</a></p></div>${note}<p class="spend-total" data-spend-total="${total}">${usd(total)}</p><p class="meta">${parts.join(" · ")}${unpriced > 0 ? ` · ${unpriced} unpriced (no reported cost or catalogue price)` : ""} · UTC month</p><section><h2>Budgets</h2>${budgets.length === 0 ? html `<p class="meta">No budgets yet.</p>` : budgets}${add}</section><div class="spend-grid">${table("By project", breakdown(view.items, item => item.project), projectName)}${table("By person", breakdown(view.items, item => item.person), key => key)}${table("By
|
|
59
|
+
return html `<article class="spend"><div class="spend-head"><h1>Spend</h1><p class="spend-month"><a href="/spend?month=${view.previous}">← ${view.previous}</a><strong>${view.month}</strong>${view.next === null ? "" : html `<a href="/spend?month=${view.next}">${view.next} →</a>`}<a href="/spend?month=${view.month}&format=csv" download>CSV</a></p></div>${note}<p class="spend-total" data-spend-total="${total}">${usd(total)}</p><p class="meta">${parts.join(" · ")}${unpriced > 0 ? ` · ${unpriced} unpriced (no reported cost or catalogue price)` : ""} · UTC month</p><section><h2>Budgets</h2>${budgets.length === 0 ? html `<p class="meta">No budgets yet.</p>` : budgets}${add}</section><div class="spend-grid">${table("By project", breakdown(view.items, item => item.project), projectName)}${table("By person", breakdown(view.items, item => item.person), key => key)}${table("By subagent", breakdown(view.items, item => item.subagent === null ? null : String(item.subagent)), nameOfSubagent)}${table("By model", breakdown(view.items, item => `${item.provider}${item.model === null ? "" : ` · ${item.model}`}`), key => key)}</div><p class="meta">Subscription work is $0; its limits are on Tasks. API work is what the provider reported, or its tokens at the prices in Settings → Models.</p></article>`;
|
|
60
60
|
}
|
|
61
61
|
/** One row per piece of spend, for a spreadsheet. */
|
|
62
|
-
export function spendCsv(items,
|
|
62
|
+
export function spendCsv(items, subagentNames) {
|
|
63
63
|
const cell = (value) => {
|
|
64
64
|
const text = value === null || value === undefined ? "" : String(value);
|
|
65
65
|
const safe = /^[=+\-@\t\r]/.test(text) ? `'${text}` : text;
|
|
66
66
|
return /[",\n\r]/.test(safe) ? `"${safe.replace(/"/g, '""')}"` : safe;
|
|
67
67
|
};
|
|
68
|
-
const head = ["time_utc", "kind", "project", "person", "
|
|
69
|
-
const rows = items.map(item => [item.at, item.kind, item.project, item.person, item.
|
|
68
|
+
const head = ["time_utc", "kind", "project", "person", "subagent", "task", "run", "provider", "model", "tokens_in", "tokens_out", "cost_usd", "priced_by", "billing"];
|
|
69
|
+
const rows = items.map(item => [item.at, item.kind, item.project, item.person, item.subagent === null ? null : subagentNames.get(item.subagent) ?? item.subagent,
|
|
70
70
|
item.taskId, item.runId, item.provider, item.model, item.tokensIn, item.tokensOut, item.microusd === null ? null : (item.microusd / 1_000_000).toFixed(6), item.source, item.authMode].map(cell).join(","));
|
|
71
71
|
return `${head.join(",")}\r\n${rows.map(row => `${row}\r\n`).join("")}`;
|
|
72
72
|
}
|
package/dist/spend.d.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* Models), or at the provider's highest listed price when the model isn't
|
|
8
8
|
* listed; work with no tokens and no cost is "unpriced" and said so.
|
|
9
9
|
*
|
|
10
|
-
* Spend counts toward a project (a run's task, a
|
|
11
|
-
* a person (who filed the task, or had the chat), a
|
|
10
|
+
* Spend counts toward a project (a run's task, a subagent's, a project chat),
|
|
11
|
+
* a person (who filed the task, or had the chat), a subagent (its turns, and
|
|
12
12
|
* tasks it filed), and the whole installation. Budgets are per calendar
|
|
13
13
|
* month (UTC).
|
|
14
14
|
*/
|
|
@@ -30,7 +30,7 @@ export declare function claudeBillingFrom(seen: {
|
|
|
30
30
|
model: string | null;
|
|
31
31
|
planWindows: boolean;
|
|
32
32
|
}): Billing | null;
|
|
33
|
-
/** How Claude bills on this computer when Toolroll gives it no key (
|
|
33
|
+
/** How Claude bills on this computer when Toolroll gives it no key (subagent turns, drafts): as its keyless runs
|
|
34
34
|
* were last seen, and its plan until one has been. */
|
|
35
35
|
export declare function claudeMachineBilling(db: Database): Billing;
|
|
36
36
|
/** Whether API work on this provider can be priced: it reports its cost, or the catalogue has its prices. */
|
|
@@ -60,8 +60,8 @@ export declare function priceWork(db: Database, work: {
|
|
|
60
60
|
source: PriceSource;
|
|
61
61
|
price: Price | null;
|
|
62
62
|
};
|
|
63
|
-
export declare const SPEND_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_spend (\n run INTEGER PRIMARY KEY,\n microusd INTEGER,\n source TEXT NOT NULL CHECK (source IN ('reported', 'estimated', 'unpriced', 'subscription')),\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n -- 1 once the run's own evidence set it (a key it was given, what its CLI said); a later turn can only make it a key.\n billing_fixed INTEGER NOT NULL DEFAULT 0,\n price_model TEXT,\n input_usd REAL,\n output_usd REAL,\n priced_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS budget (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', '
|
|
64
|
-
export type BudgetScope = "installation" | "project" | "person" | "
|
|
63
|
+
export declare const SPEND_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_spend (\n run INTEGER PRIMARY KEY,\n microusd INTEGER,\n source TEXT NOT NULL CHECK (source IN ('reported', 'estimated', 'unpriced', 'subscription')),\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n -- 1 once the run's own evidence set it (a key it was given, what its CLI said); a later turn can only make it a key.\n billing_fixed INTEGER NOT NULL DEFAULT 0,\n price_model TEXT,\n input_usd REAL,\n output_usd REAL,\n priced_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS budget (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', 'subagent')),\n scope_key TEXT NOT NULL,\n limit_microusd INTEGER NOT NULL CHECK (limit_microusd > 0),\n hard_stop INTEGER NOT NULL DEFAULT 1,\n created_by TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n removed_by TEXT,\n removed_at TEXT\n);\nCREATE UNIQUE INDEX IF NOT EXISTS budget_live ON budget (scope_kind, scope_key) WHERE removed_at IS NULL;\nCREATE TABLE IF NOT EXISTS provider_limit (\n provider TEXT NOT NULL,\n window TEXT NOT NULL,\n used_percent REAL NOT NULL,\n window_minutes INTEGER,\n resets_at TEXT,\n reached INTEGER NOT NULL DEFAULT 0,\n plan TEXT,\n observed_at TEXT NOT NULL,\n PRIMARY KEY (provider, window)\n);\n-- Spend that isn't a run, a subagent turn or a chat: a flow's Claude draft.\nCREATE TABLE IF NOT EXISTS side_spend (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n kind TEXT NOT NULL CHECK (kind IN ('draft')),\n repo TEXT,\n provider TEXT NOT NULL,\n model TEXT,\n cost_usd REAL,\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS side_spend_at ON side_spend (at);\nCREATE TABLE IF NOT EXISTS provider_account (\n provider TEXT PRIMARY KEY,\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n observed_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS chat_turn_created ON chat_turn (created_at);\nCREATE INDEX IF NOT EXISTS mate_turn_created ON mate_turn (created_at);\nCREATE INDEX IF NOT EXISTS subagent_turn_at ON subagent_turn (at);\n";
|
|
64
|
+
export type BudgetScope = "installation" | "project" | "person" | "subagent";
|
|
65
65
|
export type Budget = {
|
|
66
66
|
id: number;
|
|
67
67
|
scope: BudgetScope;
|
|
@@ -74,12 +74,12 @@ export type Budget = {
|
|
|
74
74
|
/** One piece of spend, whatever made it, with what it counts toward. */
|
|
75
75
|
export type SpendItem = {
|
|
76
76
|
at: string;
|
|
77
|
-
kind: "run" | "
|
|
77
|
+
kind: "run" | "subagent" | "chat" | "sort" | "draft";
|
|
78
78
|
microusd: number | null;
|
|
79
79
|
source: PriceSource;
|
|
80
80
|
project: string | null;
|
|
81
81
|
person: string | null;
|
|
82
|
-
|
|
82
|
+
subagent: number | null;
|
|
83
83
|
provider: string;
|
|
84
84
|
model: string | null;
|
|
85
85
|
tokensIn: number | null;
|
|
@@ -100,9 +100,9 @@ export declare function monthNamed(name: string | null): {
|
|
|
100
100
|
to: string;
|
|
101
101
|
name: string;
|
|
102
102
|
} | null;
|
|
103
|
-
/** Every
|
|
104
|
-
export declare function
|
|
105
|
-
export declare function
|
|
103
|
+
/** Every subagent's name by id (its handle when its soul file names none). */
|
|
104
|
+
export declare function subagentNames(db: Database): Map<number, string>;
|
|
105
|
+
export declare function subagentFilers(db: Database): (repo: string | null, filedBy: string | null, kind: string | null) => number | null;
|
|
106
106
|
/** Who a task's work counts toward: its own filer, or, for a revision filed by automation (an automatic repair), the
|
|
107
107
|
* filer of the task it revises, up its ancestry (bounded). */
|
|
108
108
|
export declare function filersOf(db: Database): (taskRef: number) => {
|
|
@@ -110,7 +110,7 @@ export declare function filersOf(db: Database): (taskRef: number) => {
|
|
|
110
110
|
filedBy: string | null;
|
|
111
111
|
kind: string | null;
|
|
112
112
|
};
|
|
113
|
-
/** Every piece of spend in [from, to): runs (by when they started),
|
|
113
|
+
/** Every piece of spend in [from, to): runs (by when they started), subagent turns and chat turns. `modeOf` says how
|
|
114
114
|
* a provider bills when a record doesn't (older rows): its current login or key. */
|
|
115
115
|
export declare function spendItems(db: Database, from: string, to: string, modeOf?: (provider: string) => Billing): SpendItem[];
|
|
116
116
|
/** Whether a piece of spend counts toward a budget. */
|
|
@@ -134,9 +134,9 @@ export type BudgetHold = {
|
|
|
134
134
|
remainingMicrousd: number | null;
|
|
135
135
|
};
|
|
136
136
|
/** Why work waits on a budget, in words. */
|
|
137
|
-
export declare function budgetHoldWords(hold: BudgetHold, month: string,
|
|
137
|
+
export declare function budgetHoldWords(hold: BudgetHold, month: string, subagentName?: string): string;
|
|
138
138
|
/** Each budget's month so far: what counted toward it, and how many pieces of work had no price. */
|
|
139
139
|
export declare function budgetStates(budgets: readonly Budget[], items: readonly SpendItem[]): BudgetState[];
|
|
140
|
-
/** A budget in words: "The shop project's", "alex's", "Maya's (
|
|
141
|
-
export declare function budgetLabel(budget: Pick<Budget, "scope" | "key">,
|
|
140
|
+
/** A budget in words: "The shop project's", "alex's", "Maya's (subagent)", "The whole installation's". */
|
|
141
|
+
export declare function budgetLabel(budget: Pick<Budget, "scope" | "key">, subagentName?: string): string;
|
|
142
142
|
export declare const usd: (microusd: number | null) => string;
|
package/dist/spend.js
CHANGED
|
@@ -30,7 +30,7 @@ export function claudeBillingFrom(seen) {
|
|
|
30
30
|
return "subscription";
|
|
31
31
|
return seen.keySource === "none" ? "api-key" : null;
|
|
32
32
|
}
|
|
33
|
-
/** How Claude bills on this computer when Toolroll gives it no key (
|
|
33
|
+
/** How Claude bills on this computer when Toolroll gives it no key (subagent turns, drafts): as its keyless runs
|
|
34
34
|
* were last seen, and its plan until one has been. */
|
|
35
35
|
export function claudeMachineBilling(db) {
|
|
36
36
|
return seenBilling(db, "claude") ?? "subscription";
|
|
@@ -100,7 +100,7 @@ CREATE TABLE IF NOT EXISTS run_spend (
|
|
|
100
100
|
);
|
|
101
101
|
CREATE TABLE IF NOT EXISTS budget (
|
|
102
102
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
|
-
scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', '
|
|
103
|
+
scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', 'subagent')),
|
|
104
104
|
scope_key TEXT NOT NULL,
|
|
105
105
|
limit_microusd INTEGER NOT NULL CHECK (limit_microusd > 0),
|
|
106
106
|
hard_stop INTEGER NOT NULL DEFAULT 1,
|
|
@@ -123,7 +123,7 @@ CREATE TABLE IF NOT EXISTS provider_limit (
|
|
|
123
123
|
observed_at TEXT NOT NULL,
|
|
124
124
|
PRIMARY KEY (provider, window)
|
|
125
125
|
);
|
|
126
|
-
-- Spend that isn't a run, a
|
|
126
|
+
-- Spend that isn't a run, a subagent turn or a chat: a flow's Claude draft.
|
|
127
127
|
CREATE TABLE IF NOT EXISTS side_spend (
|
|
128
128
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
129
129
|
kind TEXT NOT NULL CHECK (kind IN ('draft')),
|
|
@@ -142,7 +142,7 @@ CREATE TABLE IF NOT EXISTS provider_account (
|
|
|
142
142
|
);
|
|
143
143
|
CREATE INDEX IF NOT EXISTS chat_turn_created ON chat_turn (created_at);
|
|
144
144
|
CREATE INDEX IF NOT EXISTS mate_turn_created ON mate_turn (created_at);
|
|
145
|
-
CREATE INDEX IF NOT EXISTS
|
|
145
|
+
CREATE INDEX IF NOT EXISTS subagent_turn_at ON subagent_turn (at);
|
|
146
146
|
`;
|
|
147
147
|
/** The calendar month (UTC) `at` falls in: [start, end) as ISO strings, and its name (YYYY-MM). */
|
|
148
148
|
export function monthOf(at) {
|
|
@@ -156,25 +156,25 @@ export function monthNamed(name) {
|
|
|
156
156
|
return null;
|
|
157
157
|
return monthOf(new Date(`${name}-01T00:00:00.000Z`));
|
|
158
158
|
}
|
|
159
|
-
/** The
|
|
160
|
-
* its project. A later
|
|
161
|
-
/** A
|
|
159
|
+
/** The subagent behind a filing: tasks a subagent files are filed as its name ("Maya (AI)", from its soul file), in
|
|
160
|
+
* its project. A later subagent with the same name and project wins, as it would read on the card. */
|
|
161
|
+
/** A subagent's name as its soul file gives it ("Maya"), or null. */
|
|
162
162
|
const soulName = (soul) => {
|
|
163
163
|
const front = /^---\s*\n([\s\S]*?)\n---/.exec(String(soul ?? ""));
|
|
164
164
|
return front === null ? null : /^name:\s*(.+?)\s*$/m.exec(front[1])?.[1] ?? null;
|
|
165
165
|
};
|
|
166
|
-
/** Every
|
|
167
|
-
export function
|
|
168
|
-
return new Map(db.prepare("SELECT id, handle, soul FROM
|
|
166
|
+
/** Every subagent's name by id (its handle when its soul file names none). */
|
|
167
|
+
export function subagentNames(db) {
|
|
168
|
+
return new Map(db.prepare("SELECT id, handle, soul FROM subagent").all().map(row => [Number(row["id"]), soulName(row["soul"]) ?? String(row["handle"])]));
|
|
169
169
|
}
|
|
170
|
-
export function
|
|
170
|
+
export function subagentFilers(db) {
|
|
171
171
|
const byName = new Map();
|
|
172
|
-
for (const row of db.prepare("SELECT id, repo, handle, soul FROM
|
|
172
|
+
for (const row of db.prepare("SELECT id, repo, handle, soul FROM subagent ORDER BY id").all()) {
|
|
173
173
|
const name = soulName(row["soul"]);
|
|
174
174
|
for (const one of [String(row["handle"]), ...(name === null ? [] : [name])])
|
|
175
175
|
byName.set(`${String(row["repo"])}\n${one} (AI)`, Number(row["id"]));
|
|
176
176
|
}
|
|
177
|
-
return (repo, filedBy, kind) => kind !== "
|
|
177
|
+
return (repo, filedBy, kind) => kind !== "subagent" || repo === null || filedBy === null ? null : byName.get(`${repo}\n${filedBy}`) ?? null;
|
|
178
178
|
}
|
|
179
179
|
/** Who a task's work counts toward: its own filer, or, for a revision filed by automation (an automatic repair), the
|
|
180
180
|
* filer of the task it revises, up its ancestry (bounded). */
|
|
@@ -198,10 +198,10 @@ export function filersOf(db) {
|
|
|
198
198
|
return found;
|
|
199
199
|
};
|
|
200
200
|
}
|
|
201
|
-
/** Every piece of spend in [from, to): runs (by when they started),
|
|
201
|
+
/** Every piece of spend in [from, to): runs (by when they started), subagent turns and chat turns. `modeOf` says how
|
|
202
202
|
* a provider bills when a record doesn't (older rows): its current login or key. */
|
|
203
203
|
export function spendItems(db, from, to, modeOf = provider => billingOf(provider, db)) {
|
|
204
|
-
const
|
|
204
|
+
const subagentOf = subagentFilers(db);
|
|
205
205
|
const filerOf = filersOf(db);
|
|
206
206
|
const text = (value) => value == null ? null : String(value);
|
|
207
207
|
const count = (value) => value == null ? null : Number(value);
|
|
@@ -224,19 +224,19 @@ export function spendItems(db, from, to, modeOf = provider => billingOf(provider
|
|
|
224
224
|
source: settled ? String(row["source"]) : priced.source,
|
|
225
225
|
project: text(row["repo"]),
|
|
226
226
|
person: kind === "person" || kind === "coordinator" ? filer.filedBy : null,
|
|
227
|
-
|
|
227
|
+
subagent: subagentOf(filer.repo, filer.filedBy, kind), provider, model: text(row["model"]),
|
|
228
228
|
tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]),
|
|
229
229
|
taskId: String(row["task"]), runId: Number(row["id"]), authMode: billing,
|
|
230
230
|
};
|
|
231
231
|
});
|
|
232
|
-
// A
|
|
232
|
+
// A subagent's own turns get no key from Toolroll (subagents.ts claudeTurnRunner): they bill as this computer's
|
|
233
233
|
// Claude sign-in was seen billing when the turn ran (a plan, or a Console login, key helper or gateway).
|
|
234
|
-
for (const row of db.prepare(`SELECT t.at, t.
|
|
234
|
+
for (const row of db.prepare(`SELECT t.at, t.subagent, t.model, t.cost_usd, t.tokens_in, t.tokens_out, t.billing, m.repo FROM subagent_turn t JOIN subagent m ON m.id = t.subagent
|
|
235
235
|
WHERE t.at >= ? AND t.at < ? ORDER BY t.id`).all(from, to)) {
|
|
236
236
|
const billing = row["billing"] === "api-key" ? "api-key" : "subscription";
|
|
237
237
|
const priced = priceWork(db, { provider: "claude", model: text(row["model"]), costUsd: count(row["cost_usd"]), tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), billing });
|
|
238
|
-
items.push({ at: String(row["at"]), kind: "
|
|
239
|
-
|
|
238
|
+
items.push({ at: String(row["at"]), kind: "subagent", microusd: priced.microusd, source: priced.source, project: String(row["repo"]), person: null,
|
|
239
|
+
subagent: Number(row["subagent"]), provider: "claude", model: text(row["model"]),
|
|
240
240
|
tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), taskId: null, runId: null, authMode: billing });
|
|
241
241
|
}
|
|
242
242
|
// A chat turn that mirrors a project chat's turn is that turn: counted once, as the project chat's. A task's chat
|
|
@@ -246,19 +246,19 @@ export function spendItems(db, from, to, modeOf = provider => billingOf(provider
|
|
|
246
246
|
UNION ALL SELECT t.created_at, t.approver, COALESCE((SELECT c.provider FROM chat_turn c WHERE c.mate_turn = t.id ORDER BY c.id LIMIT 1), 'lead'),
|
|
247
247
|
(SELECT c.model FROM chat_turn c WHERE c.mate_turn = t.id ORDER BY c.id LIMIT 1), t.settled_microusd, t.tokens_in, t.tokens_out,
|
|
248
248
|
CASE th.scope_kind WHEN 'project' THEN th.scope_key WHEN 'task' THEN (SELECT r.repo FROM task_ref r WHERE r.external_id = th.scope_key ORDER BY r.id DESC LIMIT 1) END
|
|
249
|
-
FROM mate_turn t LEFT JOIN
|
|
249
|
+
FROM mate_turn t LEFT JOIN lead_thread th ON th.id = t.thread WHERE t.created_at >= ? AND t.created_at < ?`).all(from, to, from, to)) {
|
|
250
250
|
const provider = String(row["provider"]);
|
|
251
251
|
const subscription = provider.endsWith("-subscription") && modeOf(provider) === "subscription";
|
|
252
252
|
const settled = count(row["settled_microusd"]);
|
|
253
253
|
items.push({ at: String(row["at"]), kind: "chat", microusd: subscription ? 0 : settled, source: subscription ? "subscription" : settled === null ? "unpriced" : "reported",
|
|
254
|
-
project: text(row["project"]), person: text(row["approver"]),
|
|
254
|
+
project: text(row["project"]), person: text(row["approver"]), subagent: null, provider, model: text(row["model"]),
|
|
255
255
|
tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), taskId: null, runId: null, authMode: subscription ? "subscription" : "api-key" });
|
|
256
256
|
}
|
|
257
|
-
// A flow's Claude drafts: like a
|
|
257
|
+
// A flow's Claude drafts: like a subagent's turns, billed as the sign-in was.
|
|
258
258
|
for (const row of db.prepare("SELECT at, kind, repo, provider, model, cost_usd, billing FROM side_spend WHERE at >= ? AND at < ? ORDER BY id").all(from, to)) {
|
|
259
259
|
const billing = row["billing"] === "api-key" ? "api-key" : "subscription";
|
|
260
260
|
const priced = priceWork(db, { provider: String(row["provider"]), model: text(row["model"]), costUsd: count(row["cost_usd"]), tokensIn: null, tokensOut: null, billing });
|
|
261
|
-
items.push({ at: String(row["at"]), kind: "draft", microusd: priced.microusd, source: priced.source, project: text(row["repo"]), person: null,
|
|
261
|
+
items.push({ at: String(row["at"]), kind: "draft", microusd: priced.microusd, source: priced.source, project: text(row["repo"]), person: null, subagent: null,
|
|
262
262
|
provider: String(row["provider"]), model: text(row["model"]), tokensIn: null, tokensOut: null, taskId: null, runId: null, authMode: billing });
|
|
263
263
|
}
|
|
264
264
|
// A flow's Sort zone asks Jev on OpenRouter (the key's credit): what it cost is kept with its answer.
|
|
@@ -267,7 +267,7 @@ export function spendItems(db, from, to, modeOf = provider => billingOf(provider
|
|
|
267
267
|
WHERE fs.kind = 'sort' AND fs.decision_json IS NOT NULL AND fs.started_at >= ? AND fs.started_at < ? ORDER BY fs.started_at`).all(from, to)) {
|
|
268
268
|
const cost = typeof row["cost"] === "number" && Number.isFinite(row["cost"]) && row["cost"] >= 0 ? row["cost"] : null;
|
|
269
269
|
items.push({ at: String(row["at"]), kind: "sort", microusd: cost === null ? null : Math.round(cost * 1_000_000), source: cost === null ? "unpriced" : "reported",
|
|
270
|
-
project: text(row["repo"]), person: null,
|
|
270
|
+
project: text(row["repo"]), person: null, subagent: null, provider: "openrouter", model: text(row["model"]), tokensIn: null, tokensOut: null, taskId: null, runId: null, authMode: "api-key" });
|
|
271
271
|
}
|
|
272
272
|
return items;
|
|
273
273
|
}
|
|
@@ -279,14 +279,14 @@ export function countsToward(item, budget) {
|
|
|
279
279
|
return item.project === budget.key;
|
|
280
280
|
if (budget.scope === "person")
|
|
281
281
|
return item.person === budget.key;
|
|
282
|
-
return item.
|
|
282
|
+
return item.subagent !== null && String(item.subagent) === budget.key;
|
|
283
283
|
}
|
|
284
284
|
const PROVIDER_WORDS = { claude: "Claude", codex: "Codex", gemini: "Gemini", openrouter: "OpenRouter" };
|
|
285
285
|
/** Why work waits on a budget, in words. */
|
|
286
|
-
export function budgetHoldWords(hold, month,
|
|
286
|
+
export function budgetHoldWords(hold, month, subagentName) {
|
|
287
287
|
if (hold.over === null)
|
|
288
288
|
return "";
|
|
289
|
-
const label = budgetLabel(hold.over,
|
|
289
|
+
const label = budgetLabel(hold.over, subagentName);
|
|
290
290
|
return hold.why === "unpriced"
|
|
291
291
|
? `${label} budget can't price ${PROVIDER_WORDS[hold.unpricedProvider ?? ""] ?? hold.unpricedProvider} work on a key yet. Open Settings → Models to load prices`
|
|
292
292
|
: `${label} budget is used up for ${month}`;
|
|
@@ -300,14 +300,14 @@ export function budgetStates(budgets, items) {
|
|
|
300
300
|
percent: Math.floor((spent / budget.limitMicrousd) * 100) };
|
|
301
301
|
});
|
|
302
302
|
}
|
|
303
|
-
/** A budget in words: "The shop project's", "alex's", "Maya's (
|
|
304
|
-
export function budgetLabel(budget,
|
|
303
|
+
/** A budget in words: "The shop project's", "alex's", "Maya's (subagent)", "The whole installation's". */
|
|
304
|
+
export function budgetLabel(budget, subagentName) {
|
|
305
305
|
if (budget.scope === "installation")
|
|
306
306
|
return "The whole installation's";
|
|
307
307
|
if (budget.scope === "project")
|
|
308
308
|
return `The ${budget.key.split("/").filter(Boolean).pop() ?? budget.key} project's`;
|
|
309
309
|
if (budget.scope === "person")
|
|
310
310
|
return `${budget.key}'s`;
|
|
311
|
-
return `${
|
|
311
|
+
return `${subagentName ?? `Subagent ${budget.key}`}'s`;
|
|
312
312
|
}
|
|
313
313
|
export const usd = (microusd) => microusd === null ? "unpriced" : `$${(microusd / 1_000_000).toFixed(microusd !== 0 && Math.abs(microusd) < 10_000_000 ? 2 : 0)}`;
|