taskchef 7.19.1 → 7.21.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 +1 -1
- package/README.md +45 -11
- package/docs/images/ccusage-token-consumption.png +0 -0
- package/docs/spec.md +100 -15
- package/docs/workflows.md +13 -9
- package/index.js +1 -0
- package/package.json +2 -1
- package/src/dashboard/actions.js +81 -0
- package/src/dashboard/app.js +317 -29
- package/src/dashboard/index.html +17 -4
- package/src/dashboard/state.js +60 -3
- package/src/dashboard/styles.css +24 -4
- package/src/dashboard.js +129 -1
- package/src/mcp.js +23 -1
- package/src/usage-tracker.js +480 -0
- package/src/usage.js +388 -0
- package/src/workspace.js +368 -20
package/src/dashboard/styles.css
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
--danger: #8b3a35;
|
|
12
12
|
--warning: #8a5c17;
|
|
13
13
|
--shadow: 0 12px 30px rgb(24 33 29 / 10%);
|
|
14
|
+
--control-height: 38px;
|
|
15
|
+
--control-padding-inline: 11px;
|
|
16
|
+
--control-radius: 7px;
|
|
17
|
+
--control-font-size: 0.86rem;
|
|
14
18
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
15
19
|
}
|
|
16
20
|
|
|
@@ -112,7 +116,7 @@ time, .timestamp-missing { font-size: 0.75rem; }
|
|
|
112
116
|
.timestamp-toggle:hover time { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
|
|
113
117
|
.task-footer { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
|
|
114
118
|
.task-open { flex: none; }
|
|
115
|
-
.primary-button.task-action, .secondary-button.task-action { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height:
|
|
119
|
+
.primary-button.task-action, .secondary-button.task-action { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: var(--control-height); padding: 0 var(--control-padding-inline); font-size: var(--control-font-size); line-height: 1; white-space: nowrap; }
|
|
116
120
|
.codex-icon { display: block; flex: none; width: 18px; height: 18px; }
|
|
117
121
|
.codex-icon img { display: block; width: 100%; height: 100%; object-fit: contain; }
|
|
118
122
|
|
|
@@ -148,8 +152,15 @@ dialog { width: min(760px, calc(100vw - 32px)); max-height: min(82vh, 900px); pa
|
|
|
148
152
|
dialog::backdrop { background: rgb(18 23 21 / 50%); backdrop-filter: blur(2px); }
|
|
149
153
|
.dialog-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
|
|
150
154
|
.dialog-header h2 { margin-bottom: 12px; font-size: 1.6rem; }
|
|
151
|
-
.dialog-actions { display: flex; gap: 8px; margin-bottom: 26px; }
|
|
152
|
-
.
|
|
155
|
+
.dialog-actions { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 8px; margin-bottom: 26px; }
|
|
156
|
+
.more-task-actions { width: var(--control-height); min-width: var(--control-height); padding-inline: 0; font-size: 1.2rem; line-height: 1; }
|
|
157
|
+
.manual-transition-panel, .manual-transition-controls { display: contents; }
|
|
158
|
+
.manual-transition-panel[hidden] { display: none; }
|
|
159
|
+
.manual-transition-error, .manual-transition-pending { flex-basis: 100%; margin: 0; }
|
|
160
|
+
.manual-transition-controls > button[hidden] { display: none; }
|
|
161
|
+
.manual-transition-error { color: var(--danger); font-size: 0.86rem; }
|
|
162
|
+
.manual-transition-pending { color: var(--muted); font-size: 0.86rem; }
|
|
163
|
+
.primary-button, .secondary-button, .danger-button { display: inline-flex; align-items: center; justify-content: center; min-height: var(--control-height); padding: 0 var(--control-padding-inline); border-radius: var(--control-radius); font-size: var(--control-font-size); font-weight: 700; line-height: 1; cursor: pointer; }
|
|
153
164
|
.primary-button { border: 1px solid var(--accent); background: var(--accent); color: white; }
|
|
154
165
|
.secondary-button { border: 1px solid var(--border); background: var(--surface); }
|
|
155
166
|
.danger-button { border: 1px solid var(--danger); background: var(--surface); color: var(--danger); }
|
|
@@ -160,11 +171,20 @@ dialog section + section { margin-top: 24px; }
|
|
|
160
171
|
.result-history-item { padding: 13px 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-muted); }
|
|
161
172
|
.result-history-latest { border-color: var(--accent); background: var(--accent-soft); box-shadow: inset 3px 0 var(--accent); }
|
|
162
173
|
.result-history-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 9px; }
|
|
174
|
+
.result-history-header .timestamp-toggle { margin-left: auto; }
|
|
175
|
+
.result-history-source { color: var(--muted); font-size: 0.72rem; font-weight: 700; }
|
|
163
176
|
.result-history-item p { margin-bottom: 7px; }
|
|
164
177
|
.result-history-item p:last-child { margin-bottom: 0; }
|
|
165
178
|
.result-history-item h4 { margin: 10px 0 3px; color: var(--muted); font-size: 0.72rem; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
166
179
|
.result-history-turn, .result-history-empty { color: var(--muted); font-size: 0.78rem; overflow-wrap: anywhere; }
|
|
167
180
|
.result-history-empty { margin: 0; }
|
|
181
|
+
.usage-panel { margin-top: 8px; }
|
|
182
|
+
.usage-summary { display: grid; gap: 3px; margin: 10px 0 4px; color: var(--muted); font-size: 0.8rem; }
|
|
183
|
+
.usage-summary strong { color: var(--text); font-size: 0.9rem; }
|
|
184
|
+
.usage-calculating { display: flex; align-items: center; gap: 7px; }
|
|
185
|
+
.usage-spinner { width: 12px; height: 12px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: usage-spin 0.8s linear infinite; }
|
|
186
|
+
@keyframes usage-spin { to { transform: rotate(360deg); } }
|
|
187
|
+
@media (prefers-reduced-motion: reduce) { .usage-spinner { animation: none; border-top-color: var(--border); background: var(--accent); } }
|
|
168
188
|
pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius: 7px; background: var(--surface-muted); white-space: pre-wrap; overflow-wrap: anywhere; font: 0.86rem/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
169
189
|
.metadata { display: grid; grid-template-columns: minmax(100px, 150px) 1fr; margin: 0; font-size: 0.88rem; }
|
|
170
190
|
.metadata dt, .metadata dd { padding: 7px 0; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }
|
|
@@ -175,6 +195,7 @@ pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius
|
|
|
175
195
|
:focus-visible { outline: 3px solid rgb(33 95 74 / 35%); outline-offset: 3px; }
|
|
176
196
|
|
|
177
197
|
@media (max-width: 650px) {
|
|
198
|
+
:root { --control-height: 40px; }
|
|
178
199
|
.site-header { align-items: flex-start; gap: 14px; padding-top: 28px; }
|
|
179
200
|
.title-row { align-items: flex-start; }
|
|
180
201
|
.toolbar-primary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
@@ -189,7 +210,6 @@ pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius
|
|
|
189
210
|
.task-open { align-self: flex-start; }
|
|
190
211
|
select { min-height: 40px; }
|
|
191
212
|
.status-filter-option span { min-height: 38px; }
|
|
192
|
-
.primary-button.task-action, .secondary-button.task-action { min-height: 40px; }
|
|
193
213
|
.dialog-actions { align-items: center; flex-flow: row wrap; }
|
|
194
214
|
.metadata { grid-template-columns: 1fr; }
|
|
195
215
|
.metadata dt { padding-bottom: 0; border-bottom: 0; }
|
package/src/dashboard.js
CHANGED
|
@@ -16,12 +16,14 @@ import {
|
|
|
16
16
|
acquireWorkspaceLock,
|
|
17
17
|
canonicalDirectory,
|
|
18
18
|
canonicalGitRoot,
|
|
19
|
+
manuallyTransitionTask,
|
|
19
20
|
parseTaskLogContent,
|
|
20
21
|
readConfig,
|
|
21
22
|
readTask,
|
|
22
23
|
} from "./workspace.js";
|
|
23
24
|
import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
|
|
24
25
|
import { taskGitHubProjection } from "./dashboard/github-links.js";
|
|
26
|
+
import { createUsageTracker } from "./usage-tracker.js";
|
|
25
27
|
|
|
26
28
|
const TASKS_FILE_NAME = "tasks.jsonl";
|
|
27
29
|
const STATIC_ROOT = fileURLToPath(new URL("./dashboard/", import.meta.url));
|
|
@@ -30,6 +32,7 @@ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1"]);
|
|
|
30
32
|
const DEFAULT_MAX_FILE_BYTES = 16 * 1024 * 1024;
|
|
31
33
|
const DEFAULT_MAX_TASKS = 2_000;
|
|
32
34
|
const DEFAULT_MAX_EVENT_CLIENTS = 16;
|
|
35
|
+
const MAX_MANUAL_TRANSITION_BODY_BYTES = 4 * 1024;
|
|
33
36
|
export const DASHBOARD_HEALTH_PATH = "/api/health";
|
|
34
37
|
export const DASHBOARD_HEALTH_MAX_BYTES = 8 * 1024;
|
|
35
38
|
const CONTENT_SECURITY_POLICY = [
|
|
@@ -159,6 +162,11 @@ function assertDashboardTaskBounds(tasks, maximumTasks) {
|
|
|
159
162
|
boundedText(turn.turnRef, 512, `${name} turn ${turnIndex + 1} turn ref`);
|
|
160
163
|
boundedText(turn.turnId, 512, `${name} turn ${turnIndex + 1} turn ID`);
|
|
161
164
|
boundedText(turn.result?.summary, 2_000, `${name} turn ${turnIndex + 1} result summary`);
|
|
165
|
+
boundedText(
|
|
166
|
+
turn.provenance?.actionId,
|
|
167
|
+
512,
|
|
168
|
+
`${name} turn ${turnIndex + 1} manual action ID`,
|
|
169
|
+
);
|
|
162
170
|
}
|
|
163
171
|
const results = task.results ?? [];
|
|
164
172
|
if (results.length > 10_000) {
|
|
@@ -168,6 +176,11 @@ function assertDashboardTaskBounds(tasks, maximumTasks) {
|
|
|
168
176
|
boundedText(result.summary, 2_000, `${name} result ${resultIndex + 1} summary`);
|
|
169
177
|
boundedText(result.turnRef, 512, `${name} result ${resultIndex + 1} turn ref`);
|
|
170
178
|
boundedText(result.turnId, 512, `${name} result ${resultIndex + 1} turn ID`);
|
|
179
|
+
boundedText(
|
|
180
|
+
result.provenance?.actionId,
|
|
181
|
+
512,
|
|
182
|
+
`${name} result ${resultIndex + 1} manual action ID`,
|
|
183
|
+
);
|
|
171
184
|
}
|
|
172
185
|
boundedText(task.project.name, 1_000, `${name} project name`);
|
|
173
186
|
boundedText(task.project.path, 8_192, `${name} project path`);
|
|
@@ -382,6 +395,79 @@ function sendJson(response, status, value) {
|
|
|
382
395
|
response.end(`${JSON.stringify(value)}\n`);
|
|
383
396
|
}
|
|
384
397
|
|
|
398
|
+
async function readBoundedJsonBody(request, maximumBytes = MAX_MANUAL_TRANSITION_BODY_BYTES) {
|
|
399
|
+
const contentType = request.headers["content-type"] ?? "";
|
|
400
|
+
if (!/^application\/json(?:\s*;|$)/i.test(contentType)) {
|
|
401
|
+
const error = new Error("Request content type must be application/json.");
|
|
402
|
+
error.code = "unsupported_media_type";
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
const chunks = [];
|
|
406
|
+
let total = 0;
|
|
407
|
+
for await (const chunk of request) {
|
|
408
|
+
total += chunk.length;
|
|
409
|
+
if (total > maximumBytes) {
|
|
410
|
+
const error = new Error("Request body is too large.");
|
|
411
|
+
error.code = "body_too_large";
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
chunks.push(chunk);
|
|
415
|
+
}
|
|
416
|
+
let value;
|
|
417
|
+
try {
|
|
418
|
+
value = JSON.parse(Buffer.concat(chunks, total).toString("utf8"));
|
|
419
|
+
} catch {
|
|
420
|
+
const error = new Error("Request body must be valid JSON.");
|
|
421
|
+
error.code = "malformed_json";
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
425
|
+
const error = new Error("Request body must be a JSON object.");
|
|
426
|
+
error.code = "invalid_request";
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
return value;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function manualTransitionInput(body) {
|
|
433
|
+
const fields = new Set(["schemaVersion", "actionId", "expected", "targetStatus"]);
|
|
434
|
+
const unexpected = Object.keys(body).find((key) => !fields.has(key));
|
|
435
|
+
const missing = [...fields].find((key) => !(key in body));
|
|
436
|
+
if (unexpected || missing || body.schemaVersion !== 1) {
|
|
437
|
+
const error = new Error("Manual transition request has an invalid shape.");
|
|
438
|
+
error.code = "invalid_request";
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
const { schemaVersion: _schemaVersion, ...input } = body;
|
|
442
|
+
return input;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function manualTransitionErrorResponse(error) {
|
|
446
|
+
const definitions = new Map([
|
|
447
|
+
["malformed_json", [400, "Request body must be valid JSON."]],
|
|
448
|
+
["invalid_request", [400, "Manual transition request is invalid."]],
|
|
449
|
+
["task_not_found", [404, "Task not found."]],
|
|
450
|
+
["stale_task", [409, "This task changed. Review its current state and try again."]],
|
|
451
|
+
["invalid_transition", [409, "This task can no longer be changed manually."]],
|
|
452
|
+
["idempotency_conflict", [409, "This manual action ID was already used."]],
|
|
453
|
+
["body_too_large", [413, "Request body is too large."]],
|
|
454
|
+
["unsupported_media_type", [415, "Request content type must be application/json."]],
|
|
455
|
+
["ELOCKED", [503, "TaskChef is busy updating the workspace. Try again."]],
|
|
456
|
+
]);
|
|
457
|
+
const definition = definitions.get(error?.code);
|
|
458
|
+
const [status, message] = definition ?? [500, "Dashboard request failed."];
|
|
459
|
+
return {
|
|
460
|
+
status,
|
|
461
|
+
body: {
|
|
462
|
+
code: error?.code === "ELOCKED"
|
|
463
|
+
? "workspace_busy"
|
|
464
|
+
: (definition ? error.code : "dashboard_error"),
|
|
465
|
+
message,
|
|
466
|
+
...(error?.task ? { task: taskDetailProjection(error.task) } : {}),
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
385
471
|
function ssePayload(event, value) {
|
|
386
472
|
return `event: ${event}\ndata: ${JSON.stringify(value)}\n\n`;
|
|
387
473
|
}
|
|
@@ -455,6 +541,7 @@ export async function createDashboardServer({
|
|
|
455
541
|
launcher = "standalone",
|
|
456
542
|
taskchefVersion = TASKCHEF_VERSION,
|
|
457
543
|
serverVersion = DASHBOARD_SERVER_VERSION,
|
|
544
|
+
usageTracker = null,
|
|
458
545
|
} = {}) {
|
|
459
546
|
if (!LOOPBACK_HOSTS.has(host)) {
|
|
460
547
|
throw new Error("dashboard host must be a loopback address");
|
|
@@ -470,6 +557,7 @@ export async function createDashboardServer({
|
|
|
470
557
|
}
|
|
471
558
|
const monitor = new DashboardMonitor(workspace, monitorOptions);
|
|
472
559
|
await monitor.start();
|
|
560
|
+
const taskUsageTracker = usageTracker ?? createUsageTracker({ workspace: monitor.workspace });
|
|
473
561
|
const identity = Object.freeze({
|
|
474
562
|
schemaVersion: 1,
|
|
475
563
|
service: "taskchef-dashboard",
|
|
@@ -574,7 +662,47 @@ export async function createDashboardServer({
|
|
|
574
662
|
response.writeHead(200, securityHeaders("application/json; charset=utf-8"));
|
|
575
663
|
response.end();
|
|
576
664
|
} else {
|
|
577
|
-
|
|
665
|
+
const usage = await taskUsageTracker.get(task).catch(() => ({
|
|
666
|
+
status: "unavailable",
|
|
667
|
+
reason: "Task usage is temporarily unavailable.",
|
|
668
|
+
task: null,
|
|
669
|
+
turns: {},
|
|
670
|
+
}));
|
|
671
|
+
sendJson(response, 200, {
|
|
672
|
+
schemaVersion: 1,
|
|
673
|
+
task: { ...taskDetailProjection(task), usage },
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const transitionMatch = url.pathname.match(
|
|
680
|
+
/^\/api\/tasks\/([a-zA-Z0-9._-]+)\/manual-transition$/,
|
|
681
|
+
);
|
|
682
|
+
if (transitionMatch && method === "POST") {
|
|
683
|
+
if (request.headers.origin !== allowedOrigin) {
|
|
684
|
+
sendJson(response, 403, {
|
|
685
|
+
code: "invalid_origin",
|
|
686
|
+
message: "Dashboard origin validation failed.",
|
|
687
|
+
});
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const body = await readBoundedJsonBody(request);
|
|
692
|
+
const result = await manuallyTransitionTask(
|
|
693
|
+
monitor.workspace,
|
|
694
|
+
transitionMatch[1],
|
|
695
|
+
manualTransitionInput(body),
|
|
696
|
+
);
|
|
697
|
+
await monitor.refresh({ force: true }).catch(() => {});
|
|
698
|
+
sendJson(response, 200, {
|
|
699
|
+
schemaVersion: 1,
|
|
700
|
+
task: taskDetailProjection(result.task),
|
|
701
|
+
idempotent: result.idempotent,
|
|
702
|
+
});
|
|
703
|
+
} catch (error) {
|
|
704
|
+
const failure = manualTransitionErrorResponse(error);
|
|
705
|
+
sendJson(response, failure.status, failure.body);
|
|
578
706
|
}
|
|
579
707
|
return;
|
|
580
708
|
}
|
package/src/mcp.js
CHANGED
|
@@ -13,6 +13,7 @@ import { parseTaskChefMarker } from "./delegation.js";
|
|
|
13
13
|
import { createDashboardManager } from "./dashboard-manager.js";
|
|
14
14
|
import { resolveWorkspacePath } from "./workspace-path.js";
|
|
15
15
|
import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
|
|
16
|
+
import { createUsageTracker } from "./usage-tracker.js";
|
|
16
17
|
|
|
17
18
|
const projectSchema = z.object({
|
|
18
19
|
name: z.string(),
|
|
@@ -22,9 +23,23 @@ const projectSchema = z.object({
|
|
|
22
23
|
description: z.string().optional(),
|
|
23
24
|
});
|
|
24
25
|
|
|
26
|
+
const turnProvenanceSchema = z.union([
|
|
27
|
+
z.object({ kind: z.enum(["legacy", "mcp"]) }),
|
|
28
|
+
z.object({
|
|
29
|
+
kind: z.literal("dashboard_manual"),
|
|
30
|
+
actionId: z.string(),
|
|
31
|
+
fromStatus: z.enum(["working", "needs_input"]),
|
|
32
|
+
toStatus: z.enum(["completed", "failed"]),
|
|
33
|
+
expectedTurnRef: z.string().nullable(),
|
|
34
|
+
expectedThreadId: z.string().nullable(),
|
|
35
|
+
expectedUpdatedAt: z.string(),
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
|
|
25
39
|
const taskSchema = z.object({
|
|
26
40
|
schemaVersion: z.union([
|
|
27
41
|
z.literal(4), z.literal(5), z.literal(6), z.literal(7), z.literal(8), z.literal(9),
|
|
42
|
+
z.literal(10),
|
|
28
43
|
]),
|
|
29
44
|
id: z.string(),
|
|
30
45
|
project: projectSchema,
|
|
@@ -37,7 +52,7 @@ const taskSchema = z.object({
|
|
|
37
52
|
turnRef: z.string().nullable(),
|
|
38
53
|
turnId: z.string().nullable(),
|
|
39
54
|
updatedAt: z.string(),
|
|
40
|
-
updatedBy: z.enum(["dispatcher", "mcp"]),
|
|
55
|
+
updatedBy: z.enum(["dispatcher", "mcp", "dashboard"]),
|
|
41
56
|
turns: z.array(z.object({
|
|
42
57
|
turnRef: z.string().nullable(),
|
|
43
58
|
turnId: z.string().nullable(),
|
|
@@ -48,6 +63,7 @@ const taskSchema = z.object({
|
|
|
48
63
|
summary: z.string(),
|
|
49
64
|
updatedAt: z.string(),
|
|
50
65
|
}).nullable(),
|
|
66
|
+
provenance: turnProvenanceSchema.nullable(),
|
|
51
67
|
})),
|
|
52
68
|
latestTurn: z.object({
|
|
53
69
|
turnRef: z.string().nullable(),
|
|
@@ -59,6 +75,7 @@ const taskSchema = z.object({
|
|
|
59
75
|
summary: z.string(),
|
|
60
76
|
updatedAt: z.string(),
|
|
61
77
|
}).nullable(),
|
|
78
|
+
provenance: turnProvenanceSchema.nullable(),
|
|
62
79
|
}).nullable(),
|
|
63
80
|
results: z.array(z.object({
|
|
64
81
|
status: z.enum(["needs_input", "completed", "failed"]),
|
|
@@ -66,6 +83,7 @@ const taskSchema = z.object({
|
|
|
66
83
|
turnRef: z.string().nullable(),
|
|
67
84
|
turnId: z.string().nullable(),
|
|
68
85
|
updatedAt: z.string(),
|
|
86
|
+
provenance: turnProvenanceSchema.optional(),
|
|
69
87
|
})),
|
|
70
88
|
lastResult: z.object({
|
|
71
89
|
status: z.enum(["needs_input", "completed", "failed"]),
|
|
@@ -73,6 +91,7 @@ const taskSchema = z.object({
|
|
|
73
91
|
turnRef: z.string().nullable(),
|
|
74
92
|
turnId: z.string().nullable(),
|
|
75
93
|
updatedAt: z.string(),
|
|
94
|
+
provenance: turnProvenanceSchema.optional(),
|
|
76
95
|
}).nullable(),
|
|
77
96
|
});
|
|
78
97
|
|
|
@@ -145,6 +164,7 @@ export function createTaskChefMcpServer({
|
|
|
145
164
|
dashboardManager = createDashboardManager({ workspace }),
|
|
146
165
|
readConfiguration = readConfig,
|
|
147
166
|
logDashboardDiagnostic,
|
|
167
|
+
usageTracker = createUsageTracker({ workspace }),
|
|
148
168
|
} = {}) {
|
|
149
169
|
const server = new McpServer(
|
|
150
170
|
{ name: "taskchef", version: TASKCHEF_VERSION },
|
|
@@ -287,6 +307,7 @@ export function createTaskChefMcpServer({
|
|
|
287
307
|
},
|
|
288
308
|
async (input) => {
|
|
289
309
|
const task = await reportState(workspace, input);
|
|
310
|
+
void usageTracker.observe(task).catch(() => {});
|
|
290
311
|
return toolResult("task", task, `Recorded ${task.status} state for TaskChef task ${task.id}.`);
|
|
291
312
|
},
|
|
292
313
|
);
|
|
@@ -314,6 +335,7 @@ export function createTaskChefMcpServer({
|
|
|
314
335
|
},
|
|
315
336
|
async (input) => {
|
|
316
337
|
const task = await reportResult(workspace, input);
|
|
338
|
+
void usageTracker.observe(task).catch(() => {});
|
|
317
339
|
return toolResult("task", task, `Recorded ${task.status} result for TaskChef task ${task.id}.`);
|
|
318
340
|
},
|
|
319
341
|
);
|