taskchef 8.7.0 → 9.0.0
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/.codex-plugin/plugin.json +7 -10
- package/.mcp.json +3 -2
- package/README.md +31 -158
- package/docs/mcp-app.md +21 -4
- package/docs/spec.md +38 -746
- package/docs/taskchef-next-storage.md +2 -2
- package/index.js +2 -99
- package/package.json +12 -49
- package/skills/delegate/SKILL.md +44 -0
- package/skills/delegate/agents/openai.yaml +4 -0
- package/src/codex-app.js +3 -152
- package/src/codex-session-scanner.js +2 -2
- package/src/file-lock.js +25 -0
- package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
- package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
- package/src/mcp-app/react/main.test.tsx +3 -3
- package/src/mcp-app/react/main.tsx +7 -7
- package/src/mcp-app.js +4 -4
- package/src/mcp.js +4 -542
- package/src/next-cache.js +2 -2
- package/src/next-github.js +3 -3
- package/src/next-notifications.js +2 -2
- package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
- package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
- package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
- package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
- package/src/{dashboard/react → ui}/presentation.ts +1 -1
- package/src/version.js +2 -1
- package/BACKLOG.md +0 -48
- package/assets/e2e-benchmark-example.json +0 -50
- package/assets/taskchef-dispatcher-instructions.md +0 -44
- package/bin/taskchef.js +0 -20
- package/docs/advanced-guide.md +0 -497
- package/docs/dashboard-lifecycle.md +0 -149
- package/docs/dashboard-ui.md +0 -40
- package/docs/firstmate-taskchef-comparison.md +0 -283
- package/docs/images/ccusage-token-consumption.png +0 -0
- package/docs/images/dashboard-identity.jpg +0 -0
- package/docs/images/dashboard-token-pending.jpg +0 -0
- package/docs/images/interrupted-turn-recovery.jpg +0 -0
- package/docs/images/notification-event-snapshots.jpg +0 -0
- package/docs/images/readme-board.png +0 -0
- package/docs/images/readme-completed.png +0 -0
- package/docs/images/readme-list.png +0 -0
- package/docs/images/readme-needs-input.png +0 -0
- package/docs/images/readme-roles.png +0 -0
- package/docs/images/reported-work-duration-desktop.jpg +0 -0
- package/docs/images/reported-work-duration-mobile.jpg +0 -0
- package/docs/images/result-history-dashboard.jpg +0 -0
- package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
- package/docs/images/taskchef-unified-status-popup.jpg +0 -0
- package/docs/workflows.md +0 -476
- package/mcp/dashboard-session.js +0 -51
- package/scripts/benchmark-dispatch-prepare.js +0 -90
- package/scripts/e2e-benchmark.js +0 -206
- package/scripts/roles/resolve_roles.py +0 -293
- package/skills/taskchef-bootstrap/SKILL.md +0 -96
- package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
- package/skills/taskchef-bootstrap/references/project-index.md +0 -154
- package/skills/taskchef-copilot/SKILL.md +0 -75
- package/skills/taskchef-copilot/agents/openai.yaml +0 -4
- package/skills/taskchef-dashboard/SKILL.md +0 -24
- package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/SKILL.md +0 -102
- package/skills/taskchef-delegate/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/references/model-roles.md +0 -29
- package/skills/taskchef-executor/SKILL.md +0 -154
- package/skills/taskchef-executor/agents/openai.yaml +0 -4
- package/skills/taskchef-executor/references/compatibility.md +0 -22
- package/skills/taskchef-executor/references/ending-actions.md +0 -26
- package/src/backups.js +0 -359
- package/src/cli.js +0 -733
- package/src/config-audit.js +0 -99
- package/src/config-mutations.js +0 -106
- package/src/dashboard/actions.js +0 -136
- package/src/dashboard/app.js +0 -1172
- package/src/dashboard/dist/app.js +0 -269
- package/src/dashboard/dist/app.js.map +0 -1
- package/src/dashboard/dist/index.html +0 -16
- package/src/dashboard/dist/styles.css +0 -1
- package/src/dashboard/index.html +0 -148
- package/src/dashboard/react/App.test.tsx +0 -346
- package/src/dashboard/react/App.tsx +0 -515
- package/src/dashboard/react/api.test.ts +0 -40
- package/src/dashboard/react/api.ts +0 -103
- package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
- package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
- package/src/dashboard/react/components/ModelSettings.tsx +0 -106
- package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
- package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
- package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
- package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
- package/src/dashboard/react/components/TaskDetail.tsx +0 -271
- package/src/dashboard/react/components/UsagePanel.tsx +0 -96
- package/src/dashboard/react/index.html +0 -15
- package/src/dashboard/react/main.tsx +0 -12
- package/src/dashboard/react/overlay-state.test.ts +0 -110
- package/src/dashboard/react/overlay-state.ts +0 -60
- package/src/dashboard/react/presentation.test.tsx +0 -571
- package/src/dashboard/styles.css +0 -261
- package/src/dashboard-control.js +0 -34
- package/src/dashboard-manager.js +0 -339
- package/src/dashboard-session-process.js +0 -88
- package/src/dashboard-session.js +0 -86
- package/src/dashboard.js +0 -1303
- package/src/delegation.js +0 -277
- package/src/github.js +0 -107
- package/src/model-roles.js +0 -70
- package/src/recorded-usage-tracker.js +0 -92
- package/src/usage-tracker.js +0 -650
- package/src/usage.js +0 -367
- package/src/workspace-path.js +0 -48
- package/src/workspace.js +0 -3629
- /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
- /package/src/{dashboard → ui}/github-links.js +0 -0
- /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
- /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
- /package/src/{dashboard → ui}/state.js +0 -0
- /package/src/{dashboard/react → ui}/styles.css +0 -0
- /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
- /package/src/{dashboard → ui}/time.js +0 -0
- /package/src/{dashboard/react → ui}/types.ts +0 -0
package/src/usage.js
DELETED
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
import { lstat, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
|
|
5
|
-
const USAGE_FILE_NAME = ".taskchef-usage.json";
|
|
6
|
-
const USAGE_SCHEMA_VERSION = 2;
|
|
7
|
-
const UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
|
|
8
|
-
const TOKEN_FIELDS = [
|
|
9
|
-
"inputTokens",
|
|
10
|
-
"cachedInputTokens",
|
|
11
|
-
"outputTokens",
|
|
12
|
-
"reasoningOutputTokens",
|
|
13
|
-
"totalTokens",
|
|
14
|
-
];
|
|
15
|
-
const USAGE_STATUSES = new Set(["calculating", "available", "unavailable"]);
|
|
16
|
-
const MAX_USAGE_FILE_BYTES = 16 * 1024 * 1024;
|
|
17
|
-
const USAGE_WRITE_BUDGET_BYTES = 8 * 1024 * 1024;
|
|
18
|
-
const MAX_PERSISTED_TASKS = 1_000;
|
|
19
|
-
const MAX_PERSISTED_TURNS = 250;
|
|
20
|
-
|
|
21
|
-
function nonNegativeNumber(value, name) {
|
|
22
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
23
|
-
throw new Error(`${name} must be a non-negative number`);
|
|
24
|
-
}
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function matchingSession(session, threadId) {
|
|
29
|
-
const identity = `${session.sessionId ?? ""} ${session.sessionFile ?? ""}`;
|
|
30
|
-
const [primaryThreadId] = identity.match(UUID_PATTERN) ?? [];
|
|
31
|
-
return primaryThreadId?.toLowerCase() === threadId;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function normalizeModelUsage(models, name) {
|
|
35
|
-
if (!models || typeof models !== "object" || Array.isArray(models)) {
|
|
36
|
-
throw new Error(`${name}.models must be an object`);
|
|
37
|
-
}
|
|
38
|
-
return Object.fromEntries(Object.entries(models).map(([model, usage]) => {
|
|
39
|
-
if (model.length === 0 || model.length > 256) throw new Error(`${name}.models has an invalid model name`);
|
|
40
|
-
return [model, {
|
|
41
|
-
inputTokens: nonNegativeNumber(usage.inputTokens, `${name}.models.${model}.inputTokens`),
|
|
42
|
-
cachedInputTokens: nonNegativeNumber(usage.cacheReadTokens, `${name}.models.${model}.cacheReadTokens`),
|
|
43
|
-
outputTokens: nonNegativeNumber(usage.outputTokens, `${name}.models.${model}.outputTokens`),
|
|
44
|
-
reasoningOutputTokens: nonNegativeNumber(
|
|
45
|
-
usage.reasoningOutputTokens,
|
|
46
|
-
`${name}.models.${model}.reasoningOutputTokens`,
|
|
47
|
-
),
|
|
48
|
-
totalTokens: nonNegativeNumber(usage.totalTokens, `${name}.models.${model}.totalTokens`),
|
|
49
|
-
}];
|
|
50
|
-
}));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function addModelUsage(target, source) {
|
|
54
|
-
for (const [model, usage] of Object.entries(source)) {
|
|
55
|
-
if (!Object.hasOwn(target, model)) {
|
|
56
|
-
target[model] = Object.fromEntries(TOKEN_FIELDS.map((field) => [field, 0]));
|
|
57
|
-
}
|
|
58
|
-
for (const field of TOKEN_FIELDS) target[model][field] += usage[field];
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function timestampOrNull(value, name) {
|
|
63
|
-
if (value === null) return null;
|
|
64
|
-
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
|
|
65
|
-
throw new Error(`${name} must be an ISO timestamp or null`);
|
|
66
|
-
}
|
|
67
|
-
return value;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function normalizeProvenance(value, name, { includeSessionCount = false } = {}) {
|
|
71
|
-
if (!value || !["taskchef", "ccusage"].includes(value.provider)) {
|
|
72
|
-
throw new Error(`${name}.provenance is invalid`);
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
provider: value.provider,
|
|
76
|
-
version: typeof value.version === "string" ? value.version.slice(0, 64) : null,
|
|
77
|
-
pricingMode: ["online", "offline", "standard"].includes(value.pricingMode)
|
|
78
|
-
? value.pricingMode
|
|
79
|
-
: null,
|
|
80
|
-
...(includeSessionCount ? {
|
|
81
|
-
sessionCount: nonNegativeNumber(value.sessionCount, `${name}.provenance.sessionCount`),
|
|
82
|
-
} : {}),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function normalizeStoredSnapshot(value, name) {
|
|
87
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
88
|
-
throw new Error(`${name} must be an object`);
|
|
89
|
-
}
|
|
90
|
-
const snapshot = Object.fromEntries(TOKEN_FIELDS.map((field) => [
|
|
91
|
-
field,
|
|
92
|
-
nonNegativeNumber(value[field], `${name}.${field}`),
|
|
93
|
-
]));
|
|
94
|
-
if (value.estimatedCostUsd !== null) {
|
|
95
|
-
snapshot.estimatedCostUsd = nonNegativeNumber(value.estimatedCostUsd, `${name}.estimatedCostUsd`);
|
|
96
|
-
} else {
|
|
97
|
-
snapshot.estimatedCostUsd = null;
|
|
98
|
-
}
|
|
99
|
-
snapshot.costStatus = value.costStatus === "estimated" ? "estimated" : "unavailable";
|
|
100
|
-
const models = normalizeModelUsage(Object.fromEntries(Object.entries(value.models ?? {}).map(
|
|
101
|
-
([model, usage]) => [model, {
|
|
102
|
-
...usage,
|
|
103
|
-
cacheReadTokens: usage.cachedInputTokens,
|
|
104
|
-
}],
|
|
105
|
-
)), name);
|
|
106
|
-
if (Object.keys(models).length > 64 || Object.keys(models).some((model) => model.length > 256)) {
|
|
107
|
-
throw new Error(`${name}.models exceeds the usage cache limit`);
|
|
108
|
-
}
|
|
109
|
-
snapshot.models = models;
|
|
110
|
-
if (value.partial === true) snapshot.partial = true;
|
|
111
|
-
if (value.costPartial === true) snapshot.costPartial = true;
|
|
112
|
-
snapshot.provenance = normalizeProvenance(value.provenance, name, { includeSessionCount: true });
|
|
113
|
-
snapshot.sampledAt = timestampOrNull(value.sampledAt, `${name}.sampledAt`);
|
|
114
|
-
snapshot.sourceUpdatedAt = timestampOrNull(value.sourceUpdatedAt, `${name}.sourceUpdatedAt`);
|
|
115
|
-
return snapshot;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function normalizeStoredTurn(value, name) {
|
|
119
|
-
if (!value || typeof value !== "object" || !USAGE_STATUSES.has(value.status)) {
|
|
120
|
-
throw new Error(`${name} has an invalid status`);
|
|
121
|
-
}
|
|
122
|
-
const updatedAt = timestampOrNull(value.updatedAt, `${name}.updatedAt`);
|
|
123
|
-
if (value.status === "available") {
|
|
124
|
-
const normalized = Object.fromEntries(TOKEN_FIELDS.map((field) => [
|
|
125
|
-
field,
|
|
126
|
-
nonNegativeNumber(value[field], `${name}.${field}`),
|
|
127
|
-
]));
|
|
128
|
-
normalized.estimatedCostUsd = value.estimatedCostUsd === null
|
|
129
|
-
? null
|
|
130
|
-
: nonNegativeNumber(value.estimatedCostUsd, `${name}.estimatedCostUsd`);
|
|
131
|
-
const provenance = normalizeProvenance(value.provenance, name);
|
|
132
|
-
if (provenance.pricingMode === null) {
|
|
133
|
-
normalized.estimatedCostUsd = null;
|
|
134
|
-
}
|
|
135
|
-
return {
|
|
136
|
-
status: "available",
|
|
137
|
-
...(value.partial === true ? { partial: true } : {}),
|
|
138
|
-
...(value.costPartial === true ? { costPartial: true } : {}),
|
|
139
|
-
...normalized,
|
|
140
|
-
costStatus: normalized.estimatedCostUsd === null ? "unavailable" : "estimated",
|
|
141
|
-
provenance,
|
|
142
|
-
sampledAt: timestampOrNull(value.sampledAt, `${name}.sampledAt`),
|
|
143
|
-
sourceUpdatedAt: timestampOrNull(value.sourceUpdatedAt, `${name}.sourceUpdatedAt`),
|
|
144
|
-
updatedAt,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
return {
|
|
148
|
-
status: value.status,
|
|
149
|
-
...(value.status === "unavailable" ? {
|
|
150
|
-
reason: typeof value.reason === "string" ? value.reason.slice(0, 256) : "Usage is unavailable.",
|
|
151
|
-
} : {}),
|
|
152
|
-
updatedAt,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function normalizeStoredRecord(value, name) {
|
|
157
|
-
if (!value || typeof value !== "object" || !USAGE_STATUSES.has(value.status)) {
|
|
158
|
-
throw new Error(`${name} is invalid`);
|
|
159
|
-
}
|
|
160
|
-
const threadId = String(value.threadId ?? "").toLowerCase();
|
|
161
|
-
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(threadId)) {
|
|
162
|
-
throw new Error(`${name}.threadId is invalid`);
|
|
163
|
-
}
|
|
164
|
-
const generationTurnRef = value.generationTurnRef ?? null;
|
|
165
|
-
if (generationTurnRef !== null
|
|
166
|
-
&& (typeof generationTurnRef !== "string" || generationTurnRef.length === 0 || generationTurnRef.length > 256)) {
|
|
167
|
-
throw new Error(`${name}.generationTurnRef is invalid`);
|
|
168
|
-
}
|
|
169
|
-
const zeroBaselineTurnRef = value.zeroBaselineTurnRef ?? null;
|
|
170
|
-
if (zeroBaselineTurnRef !== null
|
|
171
|
-
&& (typeof zeroBaselineTurnRef !== "string" || zeroBaselineTurnRef.length === 0 || zeroBaselineTurnRef.length > 256)) {
|
|
172
|
-
throw new Error(`${name}.zeroBaselineTurnRef is invalid`);
|
|
173
|
-
}
|
|
174
|
-
const generationTurnCount = nonNegativeNumber(
|
|
175
|
-
value.generationTurnCount ?? 0,
|
|
176
|
-
`${name}.generationTurnCount`,
|
|
177
|
-
);
|
|
178
|
-
const generationTerminal = value.generationTerminal === true;
|
|
179
|
-
const normalizeMap = (entries, normalizer, mapName) => {
|
|
180
|
-
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
|
181
|
-
throw new Error(`${mapName} must be an object`);
|
|
182
|
-
}
|
|
183
|
-
const pairs = Object.entries(entries);
|
|
184
|
-
if (pairs.length > 10_000) throw new Error(`${mapName} exceeds the usage cache limit`);
|
|
185
|
-
return Object.fromEntries(pairs.map(([key, item]) => [key, normalizer(item, `${mapName}.${key}`)]));
|
|
186
|
-
};
|
|
187
|
-
return {
|
|
188
|
-
threadId,
|
|
189
|
-
generationTurnRef,
|
|
190
|
-
generationTurnCount,
|
|
191
|
-
generationTerminal,
|
|
192
|
-
zeroBaselineTurnRef,
|
|
193
|
-
status: value.status,
|
|
194
|
-
updatedAt: timestampOrNull(value.updatedAt, `${name}.updatedAt`),
|
|
195
|
-
retryAfter: timestampOrNull(value.retryAfter ?? null, `${name}.retryAfter`),
|
|
196
|
-
task: value.task === null ? null : normalizeStoredSnapshot(value.task, `${name}.task`),
|
|
197
|
-
turns: normalizeMap(value.turns, normalizeStoredTurn, `${name}.turns`),
|
|
198
|
-
boundaries: normalizeMap(value.boundaries, normalizeStoredSnapshot, `${name}.boundaries`),
|
|
199
|
-
...(value.status === "unavailable" ? {
|
|
200
|
-
reason: typeof value.reason === "string" ? value.reason.slice(0, 256) : "Usage is unavailable.",
|
|
201
|
-
} : {}),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function aggregateCcusageSessions(payload, threadId, {
|
|
206
|
-
sampledAt = new Date().toISOString(),
|
|
207
|
-
version = null,
|
|
208
|
-
pricingMode = null,
|
|
209
|
-
} = {}) {
|
|
210
|
-
if (!payload || typeof payload !== "object" || !Array.isArray(payload.sessions)) {
|
|
211
|
-
throw new Error("ccusage output must contain a sessions array");
|
|
212
|
-
}
|
|
213
|
-
const normalizedThreadId = String(threadId).toLowerCase();
|
|
214
|
-
const sessions = payload.sessions.filter((session) => matchingSession(session, normalizedThreadId));
|
|
215
|
-
if (sessions.length === 0) throw new Error("ccusage could not resolve this Codex thread");
|
|
216
|
-
|
|
217
|
-
const usage = Object.fromEntries(TOKEN_FIELDS.map((field) => [field, 0]));
|
|
218
|
-
const models = Object.create(null);
|
|
219
|
-
let estimatedCostUsd = 0;
|
|
220
|
-
let hasUnpricedUsage = false;
|
|
221
|
-
let sourceUpdatedAt = null;
|
|
222
|
-
for (const [index, session] of sessions.entries()) {
|
|
223
|
-
const name = `ccusage.sessions[${index}]`;
|
|
224
|
-
usage.inputTokens += nonNegativeNumber(session.inputTokens, `${name}.inputTokens`);
|
|
225
|
-
usage.cachedInputTokens += nonNegativeNumber(session.cacheReadTokens, `${name}.cacheReadTokens`);
|
|
226
|
-
usage.outputTokens += nonNegativeNumber(session.outputTokens, `${name}.outputTokens`);
|
|
227
|
-
usage.reasoningOutputTokens += nonNegativeNumber(
|
|
228
|
-
session.reasoningOutputTokens,
|
|
229
|
-
`${name}.reasoningOutputTokens`,
|
|
230
|
-
);
|
|
231
|
-
const sessionTotalTokens = nonNegativeNumber(session.totalTokens, `${name}.totalTokens`);
|
|
232
|
-
usage.totalTokens += sessionTotalTokens;
|
|
233
|
-
const sessionCost = session.costUSD === undefined || session.costUSD === null
|
|
234
|
-
? null
|
|
235
|
-
: nonNegativeNumber(session.costUSD, `${name}.costUSD`);
|
|
236
|
-
if (sessionCost !== null) estimatedCostUsd += sessionCost;
|
|
237
|
-
if (sessionTotalTokens > 0 && (sessionCost === null || sessionCost === 0)) hasUnpricedUsage = true;
|
|
238
|
-
addModelUsage(models, normalizeModelUsage(session.models, name));
|
|
239
|
-
if (typeof session.lastActivity === "string" && !Number.isNaN(Date.parse(session.lastActivity))) {
|
|
240
|
-
if (sourceUpdatedAt === null || Date.parse(session.lastActivity) > Date.parse(sourceUpdatedAt)) {
|
|
241
|
-
sourceUpdatedAt = session.lastActivity;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
...usage,
|
|
248
|
-
estimatedCostUsd: hasUnpricedUsage ? null : estimatedCostUsd,
|
|
249
|
-
costStatus: hasUnpricedUsage ? "unavailable" : "estimated",
|
|
250
|
-
models,
|
|
251
|
-
provenance: {
|
|
252
|
-
provider: "ccusage",
|
|
253
|
-
version,
|
|
254
|
-
pricingMode,
|
|
255
|
-
sessionCount: sessions.length,
|
|
256
|
-
},
|
|
257
|
-
sampledAt,
|
|
258
|
-
sourceUpdatedAt,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export function usageStorePath(workspace) {
|
|
263
|
-
return path.join(workspace, USAGE_FILE_NAME);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export async function readUsageStore(workspace) {
|
|
267
|
-
const filePath = usageStorePath(workspace);
|
|
268
|
-
const details = await lstat(filePath).catch((error) => {
|
|
269
|
-
if (error.code === "ENOENT") return null;
|
|
270
|
-
throw error;
|
|
271
|
-
});
|
|
272
|
-
if (details === null) return { schemaVersion: USAGE_SCHEMA_VERSION, tasks: {} };
|
|
273
|
-
if (details.isSymbolicLink() || !details.isFile()) {
|
|
274
|
-
throw new Error("TaskChef usage cache must be a regular file");
|
|
275
|
-
}
|
|
276
|
-
if (details.size > MAX_USAGE_FILE_BYTES) {
|
|
277
|
-
return { schemaVersion: USAGE_SCHEMA_VERSION, tasks: {} };
|
|
278
|
-
}
|
|
279
|
-
const value = JSON.parse(await readFile(filePath, "utf8"));
|
|
280
|
-
if (![1, USAGE_SCHEMA_VERSION].includes(value?.schemaVersion)
|
|
281
|
-
|| !value.tasks
|
|
282
|
-
|| typeof value.tasks !== "object") {
|
|
283
|
-
throw new Error("TaskChef usage cache has an unsupported schema");
|
|
284
|
-
}
|
|
285
|
-
const tasks = Object.entries(value.tasks);
|
|
286
|
-
if (tasks.length > 2_000) return { schemaVersion: USAGE_SCHEMA_VERSION, tasks: {} };
|
|
287
|
-
return {
|
|
288
|
-
schemaVersion: USAGE_SCHEMA_VERSION,
|
|
289
|
-
tasks: Object.fromEntries(tasks.map(([taskId, record]) => [
|
|
290
|
-
taskId,
|
|
291
|
-
normalizeStoredRecord(record, `usage task ${taskId}`),
|
|
292
|
-
])),
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function compactUsageRecord(record) {
|
|
297
|
-
const turnEntries = Object.entries(record.turns ?? {}).slice(-MAX_PERSISTED_TURNS);
|
|
298
|
-
const boundaryEntries = Object.entries(record.boundaries ?? {}).slice(-1);
|
|
299
|
-
return {
|
|
300
|
-
...record,
|
|
301
|
-
turns: Object.fromEntries(turnEntries),
|
|
302
|
-
boundaries: Object.fromEntries(boundaryEntries),
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export function compactUsageStore(store) {
|
|
307
|
-
const entries = Object.entries(store.tasks ?? {})
|
|
308
|
-
.sort(([, left], [, right]) => Date.parse(right.updatedAt ?? 0) - Date.parse(left.updatedAt ?? 0))
|
|
309
|
-
.slice(0, MAX_PERSISTED_TASKS);
|
|
310
|
-
const tasks = {};
|
|
311
|
-
let approximateBytes = 64;
|
|
312
|
-
for (const [taskId, record] of entries) {
|
|
313
|
-
const compacted = compactUsageRecord(record);
|
|
314
|
-
const entryBytes = Buffer.byteLength(JSON.stringify([taskId, compacted]), "utf8");
|
|
315
|
-
if (Object.keys(tasks).length > 0 && approximateBytes + entryBytes > USAGE_WRITE_BUDGET_BYTES) break;
|
|
316
|
-
tasks[taskId] = compacted;
|
|
317
|
-
approximateBytes += entryBytes;
|
|
318
|
-
}
|
|
319
|
-
return { schemaVersion: USAGE_SCHEMA_VERSION, tasks };
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
export async function writeUsageStore(workspace, store) {
|
|
323
|
-
await mkdir(workspace, { recursive: true });
|
|
324
|
-
const filePath = usageStorePath(workspace);
|
|
325
|
-
const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
326
|
-
const serialized = `${JSON.stringify(compactUsageStore(store), null, 2)}\n`;
|
|
327
|
-
if (Buffer.byteLength(serialized, "utf8") > MAX_USAGE_FILE_BYTES) {
|
|
328
|
-
throw new Error("TaskChef usage cache cannot be compacted below the size limit");
|
|
329
|
-
}
|
|
330
|
-
await writeFile(temporaryPath, serialized, {
|
|
331
|
-
encoding: "utf8",
|
|
332
|
-
flag: "wx",
|
|
333
|
-
mode: 0o600,
|
|
334
|
-
});
|
|
335
|
-
try {
|
|
336
|
-
await rename(temporaryPath, filePath);
|
|
337
|
-
} catch (error) {
|
|
338
|
-
await unlink(temporaryPath).catch(() => {});
|
|
339
|
-
throw error;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
export function usageDelta(current, previous = null) {
|
|
344
|
-
const baseline = previous ?? Object.fromEntries(TOKEN_FIELDS.map((field) => [field, 0]));
|
|
345
|
-
const delta = {};
|
|
346
|
-
for (const field of TOKEN_FIELDS) {
|
|
347
|
-
const value = current[field] - baseline[field];
|
|
348
|
-
if (!Number.isFinite(value) || value < 0) return null;
|
|
349
|
-
delta[field] = value;
|
|
350
|
-
}
|
|
351
|
-
const samePricingSource = previous === null || ["version", "pricingMode"]
|
|
352
|
-
.every((field) => current.provenance?.[field] === previous.provenance?.[field]);
|
|
353
|
-
let estimatedCostUsd = !samePricingSource
|
|
354
|
-
|| current.estimatedCostUsd === null
|
|
355
|
-
|| (previous !== null && previous.estimatedCostUsd === null)
|
|
356
|
-
? null
|
|
357
|
-
: current.estimatedCostUsd - (previous?.estimatedCostUsd ?? 0);
|
|
358
|
-
if (estimatedCostUsd !== null && (!Number.isFinite(estimatedCostUsd) || estimatedCostUsd < 0)) {
|
|
359
|
-
estimatedCostUsd = null;
|
|
360
|
-
}
|
|
361
|
-
if (estimatedCostUsd === 0 && delta.totalTokens > 0) estimatedCostUsd = null;
|
|
362
|
-
return {
|
|
363
|
-
...delta,
|
|
364
|
-
estimatedCostUsd,
|
|
365
|
-
costStatus: estimatedCostUsd === null ? "unavailable" : "estimated",
|
|
366
|
-
};
|
|
367
|
-
}
|
package/src/workspace-path.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
export const TASKCHEF_WORKSPACE_ENV = "TASKCHEF_WORKSPACE";
|
|
5
|
-
|
|
6
|
-
export function defaultWorkspacePath({ homedir = os.homedir() } = {}) {
|
|
7
|
-
return path.join(homedir, ".agents", "taskchef");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function expandHome(value, homedir) {
|
|
11
|
-
if (value === "~") return homedir;
|
|
12
|
-
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
13
|
-
return path.join(homedir, value.slice(2));
|
|
14
|
-
}
|
|
15
|
-
return value;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function resolveWorkspacePath({
|
|
19
|
-
explicit = null,
|
|
20
|
-
env = process.env,
|
|
21
|
-
homedir = os.homedir(),
|
|
22
|
-
cwd = process.cwd(),
|
|
23
|
-
} = {}) {
|
|
24
|
-
let source = "default";
|
|
25
|
-
let value = defaultWorkspacePath({ homedir });
|
|
26
|
-
if (env[TASKCHEF_WORKSPACE_ENV] !== undefined) {
|
|
27
|
-
if (typeof env[TASKCHEF_WORKSPACE_ENV] !== "string" || env[TASKCHEF_WORKSPACE_ENV].trim() === "") {
|
|
28
|
-
throw new Error(`${TASKCHEF_WORKSPACE_ENV} must be a non-empty path`);
|
|
29
|
-
}
|
|
30
|
-
source = "environment";
|
|
31
|
-
value = env[TASKCHEF_WORKSPACE_ENV];
|
|
32
|
-
}
|
|
33
|
-
if (explicit !== null) {
|
|
34
|
-
if (typeof explicit !== "string" || explicit.trim() === "") {
|
|
35
|
-
throw new Error("--workspace must be a non-empty path");
|
|
36
|
-
}
|
|
37
|
-
source = "explicit";
|
|
38
|
-
value = explicit;
|
|
39
|
-
}
|
|
40
|
-
const expanded = expandHome(value, homedir);
|
|
41
|
-
if (source === "environment" && !path.isAbsolute(expanded)) {
|
|
42
|
-
throw new Error(`${TASKCHEF_WORKSPACE_ENV} must be an absolute path or start with ~/`);
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
workspace: path.resolve(cwd, expanded),
|
|
46
|
-
source,
|
|
47
|
-
};
|
|
48
|
-
}
|