@hasna/todos 0.11.55 → 0.11.57
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/README.md +34 -0
- package/dist/cli/commands/dispatch.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/helpers.d.ts +0 -2
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +2689 -2030
- package/dist/contracts.d.ts +2 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +1222 -93
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1622 -291
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/dispatch.d.ts +3 -0
- package/dist/lib/dispatch.d.ts.map +1 -1
- package/dist/lib/event-hooks.d.ts +1 -1
- package/dist/lib/event-hooks.d.ts.map +1 -1
- package/dist/lib/json-schemas.d.ts +1 -1
- package/dist/lib/json-schemas.d.ts.map +1 -1
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/shared-events.d.ts +14 -0
- package/dist/lib/shared-events.d.ts.map +1 -0
- package/dist/lib/tester-issue-reports.d.ts +105 -0
- package/dist/lib/tester-issue-reports.d.ts.map +1 -0
- package/dist/lib/tmux.d.ts +19 -1
- package/dist/lib/tmux.d.ts.map +1 -1
- package/dist/mcp/index.js +865 -172
- package/dist/mcp/tools/dispatch.d.ts.map +1 -1
- package/dist/registry.js +1214 -93
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +861 -168
- package/dist/storage.js +661 -65
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -3989,6 +3989,7 @@ var init_event_hooks = __esm(() => {
|
|
|
3989
3989
|
init_runner_sandbox();
|
|
3990
3990
|
init_config();
|
|
3991
3991
|
LOCAL_EVENT_TYPES = [
|
|
3992
|
+
"task.created",
|
|
3992
3993
|
"task.assigned",
|
|
3993
3994
|
"task.blocked",
|
|
3994
3995
|
"task.started",
|
|
@@ -4013,6 +4014,572 @@ var init_event_hooks = __esm(() => {
|
|
|
4013
4014
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
4014
4015
|
});
|
|
4015
4016
|
|
|
4017
|
+
// node_modules/.bun/@hasna+events@0.1.7/node_modules/@hasna/events/dist/index.js
|
|
4018
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
4019
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4020
|
+
import { homedir } from "os";
|
|
4021
|
+
import { join as join5 } from "path";
|
|
4022
|
+
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4023
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
4024
|
+
import { spawn } from "child_process";
|
|
4025
|
+
import { randomUUID as randomUUID22 } from "crypto";
|
|
4026
|
+
function getPathValue(input, path) {
|
|
4027
|
+
return path.split(".").reduce((value, part) => {
|
|
4028
|
+
if (value && typeof value === "object" && part in value) {
|
|
4029
|
+
return value[part];
|
|
4030
|
+
}
|
|
4031
|
+
return;
|
|
4032
|
+
}, input);
|
|
4033
|
+
}
|
|
4034
|
+
function wildcardToRegExp(pattern) {
|
|
4035
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
4036
|
+
return new RegExp(`^${escaped}$`);
|
|
4037
|
+
}
|
|
4038
|
+
function matchString(value, matcher) {
|
|
4039
|
+
if (matcher === undefined)
|
|
4040
|
+
return true;
|
|
4041
|
+
if (value === undefined)
|
|
4042
|
+
return false;
|
|
4043
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
4044
|
+
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
4045
|
+
}
|
|
4046
|
+
function matchRecord(input, matcher) {
|
|
4047
|
+
if (!matcher)
|
|
4048
|
+
return true;
|
|
4049
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
4050
|
+
const actual = getPathValue(input, path);
|
|
4051
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
4052
|
+
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
4053
|
+
}
|
|
4054
|
+
return actual === expected;
|
|
4055
|
+
});
|
|
4056
|
+
}
|
|
4057
|
+
function eventMatchesFilter(event, filter) {
|
|
4058
|
+
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
4059
|
+
}
|
|
4060
|
+
function channelMatchesEvent(channel, event) {
|
|
4061
|
+
if (!channel.enabled)
|
|
4062
|
+
return false;
|
|
4063
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
4064
|
+
return true;
|
|
4065
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
4066
|
+
}
|
|
4067
|
+
function getEventsDataDir(override) {
|
|
4068
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir(), ".hasna", "events");
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
class JsonEventsStore {
|
|
4072
|
+
dataDir;
|
|
4073
|
+
channelsPath;
|
|
4074
|
+
eventsPath;
|
|
4075
|
+
deliveriesPath;
|
|
4076
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
4077
|
+
this.dataDir = dataDir;
|
|
4078
|
+
this.channelsPath = join5(dataDir, "channels.json");
|
|
4079
|
+
this.eventsPath = join5(dataDir, "events.json");
|
|
4080
|
+
this.deliveriesPath = join5(dataDir, "deliveries.json");
|
|
4081
|
+
}
|
|
4082
|
+
async init() {
|
|
4083
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
4084
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
4085
|
+
return;
|
|
4086
|
+
});
|
|
4087
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
4088
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
4089
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
4090
|
+
}
|
|
4091
|
+
async addChannel(channel) {
|
|
4092
|
+
await this.init();
|
|
4093
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
4094
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
4095
|
+
if (index >= 0) {
|
|
4096
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
4097
|
+
} else {
|
|
4098
|
+
channels.push(channel);
|
|
4099
|
+
}
|
|
4100
|
+
await this.writeJson(this.channelsPath, channels);
|
|
4101
|
+
return index >= 0 ? channels[index] : channel;
|
|
4102
|
+
}
|
|
4103
|
+
async listChannels() {
|
|
4104
|
+
await this.init();
|
|
4105
|
+
return this.readJson(this.channelsPath, []);
|
|
4106
|
+
}
|
|
4107
|
+
async getChannel(id) {
|
|
4108
|
+
const channels = await this.listChannels();
|
|
4109
|
+
return channels.find((channel) => channel.id === id);
|
|
4110
|
+
}
|
|
4111
|
+
async removeChannel(id) {
|
|
4112
|
+
await this.init();
|
|
4113
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
4114
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
4115
|
+
await this.writeJson(this.channelsPath, next);
|
|
4116
|
+
return next.length !== channels.length;
|
|
4117
|
+
}
|
|
4118
|
+
async appendEvent(event) {
|
|
4119
|
+
await this.init();
|
|
4120
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
4121
|
+
events.push(event);
|
|
4122
|
+
await this.writeJson(this.eventsPath, events);
|
|
4123
|
+
return event;
|
|
4124
|
+
}
|
|
4125
|
+
async listEvents() {
|
|
4126
|
+
await this.init();
|
|
4127
|
+
return this.readJson(this.eventsPath, []);
|
|
4128
|
+
}
|
|
4129
|
+
async findEventByIdentity(identity) {
|
|
4130
|
+
const events = await this.listEvents();
|
|
4131
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
4132
|
+
}
|
|
4133
|
+
async appendDelivery(result) {
|
|
4134
|
+
await this.init();
|
|
4135
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
4136
|
+
deliveries.push(result);
|
|
4137
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
4138
|
+
return result;
|
|
4139
|
+
}
|
|
4140
|
+
async listDeliveries() {
|
|
4141
|
+
await this.init();
|
|
4142
|
+
return this.readJson(this.deliveriesPath, []);
|
|
4143
|
+
}
|
|
4144
|
+
async exportData() {
|
|
4145
|
+
return {
|
|
4146
|
+
channels: await this.listChannels(),
|
|
4147
|
+
events: await this.listEvents(),
|
|
4148
|
+
deliveries: await this.listDeliveries()
|
|
4149
|
+
};
|
|
4150
|
+
}
|
|
4151
|
+
async ensureArrayFile(path) {
|
|
4152
|
+
if (!existsSync6(path)) {
|
|
4153
|
+
await writeFile(path, `[]
|
|
4154
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
4155
|
+
}
|
|
4156
|
+
await chmod(path, 384).catch(() => {
|
|
4157
|
+
return;
|
|
4158
|
+
});
|
|
4159
|
+
}
|
|
4160
|
+
async readJson(path, fallback) {
|
|
4161
|
+
try {
|
|
4162
|
+
const raw = await readFile(path, "utf-8");
|
|
4163
|
+
if (!raw.trim())
|
|
4164
|
+
return fallback;
|
|
4165
|
+
return JSON.parse(raw);
|
|
4166
|
+
} catch (error) {
|
|
4167
|
+
if (error.code === "ENOENT")
|
|
4168
|
+
return fallback;
|
|
4169
|
+
throw error;
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
async writeJson(path, value) {
|
|
4173
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
4174
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
4175
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
4176
|
+
await rename(tempPath, path);
|
|
4177
|
+
await chmod(path, 384).catch(() => {
|
|
4178
|
+
return;
|
|
4179
|
+
});
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
function buildSignatureBase(timestamp, body) {
|
|
4183
|
+
return `${timestamp}.${body}`;
|
|
4184
|
+
}
|
|
4185
|
+
function signPayload(secret, timestamp, body) {
|
|
4186
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
4187
|
+
return `sha256=${digest}`;
|
|
4188
|
+
}
|
|
4189
|
+
function now2() {
|
|
4190
|
+
return new Date().toISOString();
|
|
4191
|
+
}
|
|
4192
|
+
function truncate(value, max = 4096) {
|
|
4193
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
4194
|
+
}
|
|
4195
|
+
function buildWebhookRequest(event, channel) {
|
|
4196
|
+
if (!channel.webhook)
|
|
4197
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
4198
|
+
const body = JSON.stringify(event);
|
|
4199
|
+
const timestamp = event.time;
|
|
4200
|
+
const headers = {
|
|
4201
|
+
"Content-Type": "application/json",
|
|
4202
|
+
"User-Agent": "@hasna/events",
|
|
4203
|
+
"X-Hasna-Event-Id": event.id,
|
|
4204
|
+
"X-Hasna-Event-Type": event.type,
|
|
4205
|
+
"X-Hasna-Timestamp": timestamp,
|
|
4206
|
+
...channel.webhook.headers
|
|
4207
|
+
};
|
|
4208
|
+
if (channel.webhook.secret) {
|
|
4209
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
4210
|
+
}
|
|
4211
|
+
return { body, headers };
|
|
4212
|
+
}
|
|
4213
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
4214
|
+
if (!channel.webhook)
|
|
4215
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
4216
|
+
const startedAt = now2();
|
|
4217
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
4218
|
+
const controller = new AbortController;
|
|
4219
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
4220
|
+
try {
|
|
4221
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
4222
|
+
method: "POST",
|
|
4223
|
+
headers,
|
|
4224
|
+
body,
|
|
4225
|
+
signal: controller.signal
|
|
4226
|
+
});
|
|
4227
|
+
const responseBody = truncate(await response.text());
|
|
4228
|
+
return {
|
|
4229
|
+
attempt: 1,
|
|
4230
|
+
status: response.ok ? "success" : "failed",
|
|
4231
|
+
startedAt,
|
|
4232
|
+
completedAt: now2(),
|
|
4233
|
+
responseStatus: response.status,
|
|
4234
|
+
responseBody,
|
|
4235
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
4236
|
+
};
|
|
4237
|
+
} catch (error) {
|
|
4238
|
+
return {
|
|
4239
|
+
attempt: 1,
|
|
4240
|
+
status: "failed",
|
|
4241
|
+
startedAt,
|
|
4242
|
+
completedAt: now2(),
|
|
4243
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4244
|
+
};
|
|
4245
|
+
} finally {
|
|
4246
|
+
clearTimeout(timeout);
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
async function dispatchCommand(event, channel) {
|
|
4250
|
+
if (!channel.command)
|
|
4251
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
4252
|
+
const startedAt = now2();
|
|
4253
|
+
const eventJson = JSON.stringify(event);
|
|
4254
|
+
const env = {
|
|
4255
|
+
...process.env,
|
|
4256
|
+
...channel.command.env,
|
|
4257
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
4258
|
+
HASNA_EVENT_ID: event.id,
|
|
4259
|
+
HASNA_EVENT_TYPE: event.type,
|
|
4260
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
4261
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
4262
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
4263
|
+
HASNA_EVENT_TIME: event.time,
|
|
4264
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
4265
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
4266
|
+
HASNA_EVENT_JSON: eventJson
|
|
4267
|
+
};
|
|
4268
|
+
return new Promise((resolve6) => {
|
|
4269
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
4270
|
+
cwd: channel.command.cwd,
|
|
4271
|
+
env,
|
|
4272
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4273
|
+
});
|
|
4274
|
+
let stdout = "";
|
|
4275
|
+
let stderr = "";
|
|
4276
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
4277
|
+
child.stdin.end(eventJson);
|
|
4278
|
+
child.stdout.on("data", (chunk) => {
|
|
4279
|
+
stdout += chunk.toString();
|
|
4280
|
+
});
|
|
4281
|
+
child.stderr.on("data", (chunk) => {
|
|
4282
|
+
stderr += chunk.toString();
|
|
4283
|
+
});
|
|
4284
|
+
child.on("error", (error) => {
|
|
4285
|
+
clearTimeout(timeout);
|
|
4286
|
+
resolve6({
|
|
4287
|
+
attempt: 1,
|
|
4288
|
+
status: "failed",
|
|
4289
|
+
startedAt,
|
|
4290
|
+
completedAt: now2(),
|
|
4291
|
+
stdout: truncate(stdout),
|
|
4292
|
+
stderr: truncate(stderr),
|
|
4293
|
+
error: error.message
|
|
4294
|
+
});
|
|
4295
|
+
});
|
|
4296
|
+
child.on("close", (code, signal) => {
|
|
4297
|
+
clearTimeout(timeout);
|
|
4298
|
+
const success = code === 0;
|
|
4299
|
+
resolve6({
|
|
4300
|
+
attempt: 1,
|
|
4301
|
+
status: success ? "success" : "failed",
|
|
4302
|
+
startedAt,
|
|
4303
|
+
completedAt: now2(),
|
|
4304
|
+
stdout: truncate(stdout),
|
|
4305
|
+
stderr: truncate(stderr),
|
|
4306
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
4307
|
+
});
|
|
4308
|
+
});
|
|
4309
|
+
});
|
|
4310
|
+
}
|
|
4311
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
4312
|
+
if (channel.transport === "webhook")
|
|
4313
|
+
return dispatchWebhook(event, channel, options);
|
|
4314
|
+
if (channel.transport === "command")
|
|
4315
|
+
return dispatchCommand(event, channel);
|
|
4316
|
+
return {
|
|
4317
|
+
attempt: 1,
|
|
4318
|
+
status: "skipped",
|
|
4319
|
+
startedAt: now2(),
|
|
4320
|
+
completedAt: now2(),
|
|
4321
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
4322
|
+
};
|
|
4323
|
+
}
|
|
4324
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
4325
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
4326
|
+
return {
|
|
4327
|
+
id: randomUUID2(),
|
|
4328
|
+
eventId: event.id,
|
|
4329
|
+
channelId: channel.id,
|
|
4330
|
+
transport: channel.transport,
|
|
4331
|
+
status,
|
|
4332
|
+
attempts,
|
|
4333
|
+
createdAt: attempts[0]?.startedAt ?? now2(),
|
|
4334
|
+
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
4335
|
+
};
|
|
4336
|
+
}
|
|
4337
|
+
function createEvent(input) {
|
|
4338
|
+
return {
|
|
4339
|
+
id: input.id ?? randomUUID22(),
|
|
4340
|
+
source: input.source,
|
|
4341
|
+
type: input.type,
|
|
4342
|
+
time: normalizeTime(input.time),
|
|
4343
|
+
subject: input.subject,
|
|
4344
|
+
severity: input.severity ?? "info",
|
|
4345
|
+
data: input.data ?? {},
|
|
4346
|
+
message: input.message,
|
|
4347
|
+
dedupeKey: input.dedupeKey,
|
|
4348
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
4349
|
+
metadata: input.metadata ?? {}
|
|
4350
|
+
};
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
class EventsClient {
|
|
4354
|
+
store;
|
|
4355
|
+
redactors;
|
|
4356
|
+
transportOptions;
|
|
4357
|
+
constructor(options = {}) {
|
|
4358
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
4359
|
+
this.redactors = options.redactors ?? [];
|
|
4360
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
4361
|
+
}
|
|
4362
|
+
async addChannel(input) {
|
|
4363
|
+
const timestamp = new Date().toISOString();
|
|
4364
|
+
return this.store.addChannel({
|
|
4365
|
+
...input,
|
|
4366
|
+
createdAt: input.createdAt ?? timestamp,
|
|
4367
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
4368
|
+
});
|
|
4369
|
+
}
|
|
4370
|
+
async listChannels() {
|
|
4371
|
+
return this.store.listChannels();
|
|
4372
|
+
}
|
|
4373
|
+
async removeChannel(id) {
|
|
4374
|
+
return this.store.removeChannel(id);
|
|
4375
|
+
}
|
|
4376
|
+
async emit(input, options = {}) {
|
|
4377
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
4378
|
+
if (options.dedupe !== false) {
|
|
4379
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
4380
|
+
if (existing) {
|
|
4381
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
await this.store.appendEvent(event);
|
|
4385
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
4386
|
+
return { event, deliveries, deduped: false };
|
|
4387
|
+
}
|
|
4388
|
+
async listEvents() {
|
|
4389
|
+
return this.store.listEvents();
|
|
4390
|
+
}
|
|
4391
|
+
async listDeliveries() {
|
|
4392
|
+
return this.store.listDeliveries();
|
|
4393
|
+
}
|
|
4394
|
+
async deliver(event) {
|
|
4395
|
+
const channels = await this.store.listChannels();
|
|
4396
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
4397
|
+
const deliveries = [];
|
|
4398
|
+
for (const channel of selected) {
|
|
4399
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
4400
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
4401
|
+
await this.store.appendDelivery(result);
|
|
4402
|
+
deliveries.push(result);
|
|
4403
|
+
}
|
|
4404
|
+
return deliveries;
|
|
4405
|
+
}
|
|
4406
|
+
async testChannel(id, input = {}) {
|
|
4407
|
+
const channel = await this.store.getChannel(id);
|
|
4408
|
+
if (!channel)
|
|
4409
|
+
throw new Error(`Channel not found: ${id}`);
|
|
4410
|
+
const event = createEvent({
|
|
4411
|
+
source: input.source ?? "hasna.events",
|
|
4412
|
+
type: input.type ?? "events.test",
|
|
4413
|
+
subject: input.subject ?? id,
|
|
4414
|
+
severity: input.severity ?? "info",
|
|
4415
|
+
data: input.data ?? { test: true },
|
|
4416
|
+
message: input.message ?? "Hasna events test delivery",
|
|
4417
|
+
dedupeKey: input.dedupeKey,
|
|
4418
|
+
schemaVersion: input.schemaVersion,
|
|
4419
|
+
metadata: input.metadata,
|
|
4420
|
+
time: input.time,
|
|
4421
|
+
id: input.id
|
|
4422
|
+
});
|
|
4423
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
4424
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
4425
|
+
await this.store.appendDelivery(result);
|
|
4426
|
+
return result;
|
|
4427
|
+
}
|
|
4428
|
+
async replay(options = {}) {
|
|
4429
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
4430
|
+
if (options.eventId && event.id !== options.eventId)
|
|
4431
|
+
return false;
|
|
4432
|
+
if (options.source && event.source !== options.source)
|
|
4433
|
+
return false;
|
|
4434
|
+
if (options.type && event.type !== options.type)
|
|
4435
|
+
return false;
|
|
4436
|
+
return true;
|
|
4437
|
+
});
|
|
4438
|
+
if (options.dryRun)
|
|
4439
|
+
return { events, deliveries: [] };
|
|
4440
|
+
const deliveries = [];
|
|
4441
|
+
for (const event of events) {
|
|
4442
|
+
deliveries.push(...await this.deliver(event));
|
|
4443
|
+
}
|
|
4444
|
+
return { events, deliveries };
|
|
4445
|
+
}
|
|
4446
|
+
async applyRedaction(event, channel) {
|
|
4447
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
4448
|
+
for (const redactor of this.redactors) {
|
|
4449
|
+
next = await redactor(next, channel);
|
|
4450
|
+
}
|
|
4451
|
+
return next;
|
|
4452
|
+
}
|
|
4453
|
+
async deliverWithRetry(event, channel) {
|
|
4454
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
4455
|
+
const attempts = [];
|
|
4456
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
4457
|
+
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
4458
|
+
attempt.attempt = index + 1;
|
|
4459
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
4460
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
4461
|
+
}
|
|
4462
|
+
attempts.push(attempt);
|
|
4463
|
+
if (attempt.status !== "failed")
|
|
4464
|
+
break;
|
|
4465
|
+
if (attempt.nextBackoffMs)
|
|
4466
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
4467
|
+
}
|
|
4468
|
+
return createDeliveryResult(event, channel, attempts);
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
4472
|
+
if (paths.length === 0)
|
|
4473
|
+
return event;
|
|
4474
|
+
const copy = structuredClone(event);
|
|
4475
|
+
for (const path of paths) {
|
|
4476
|
+
setPath(copy, path, replacement);
|
|
4477
|
+
}
|
|
4478
|
+
return copy;
|
|
4479
|
+
}
|
|
4480
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
4481
|
+
return redactValue2(event, replacement);
|
|
4482
|
+
}
|
|
4483
|
+
function shouldRedactKey(key) {
|
|
4484
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
4485
|
+
}
|
|
4486
|
+
function redactValue2(value, replacement) {
|
|
4487
|
+
if (Array.isArray(value))
|
|
4488
|
+
return value.map((item) => redactValue2(item, replacement));
|
|
4489
|
+
if (!value || typeof value !== "object")
|
|
4490
|
+
return value;
|
|
4491
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
4492
|
+
key,
|
|
4493
|
+
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
4494
|
+
]));
|
|
4495
|
+
}
|
|
4496
|
+
function setPath(input, path, replacement) {
|
|
4497
|
+
const parts = path.split(".");
|
|
4498
|
+
let cursor = input;
|
|
4499
|
+
for (const part of parts.slice(0, -1)) {
|
|
4500
|
+
const next = cursor[part];
|
|
4501
|
+
if (!next || typeof next !== "object")
|
|
4502
|
+
return;
|
|
4503
|
+
cursor = next;
|
|
4504
|
+
}
|
|
4505
|
+
const last = parts.at(-1);
|
|
4506
|
+
if (last && last in cursor)
|
|
4507
|
+
cursor[last] = replacement;
|
|
4508
|
+
}
|
|
4509
|
+
function normalizeTime(value) {
|
|
4510
|
+
if (!value)
|
|
4511
|
+
return new Date().toISOString();
|
|
4512
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
4513
|
+
}
|
|
4514
|
+
function normalizeRetryPolicy(policy) {
|
|
4515
|
+
return {
|
|
4516
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
4517
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
4518
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
4519
|
+
};
|
|
4520
|
+
}
|
|
4521
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
|
|
4522
|
+
var init_dist = __esm(() => {
|
|
4523
|
+
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
4524
|
+
});
|
|
4525
|
+
|
|
4526
|
+
// src/lib/shared-events.ts
|
|
4527
|
+
function taskEventData(task, extra = {}) {
|
|
4528
|
+
return {
|
|
4529
|
+
id: task.id,
|
|
4530
|
+
task_id: task.id,
|
|
4531
|
+
short_id: task.short_id,
|
|
4532
|
+
title: task.title,
|
|
4533
|
+
description: task.description,
|
|
4534
|
+
status: task.status,
|
|
4535
|
+
priority: task.priority,
|
|
4536
|
+
project_id: task.project_id,
|
|
4537
|
+
parent_id: task.parent_id,
|
|
4538
|
+
plan_id: task.plan_id,
|
|
4539
|
+
task_list_id: task.task_list_id,
|
|
4540
|
+
agent_id: task.agent_id,
|
|
4541
|
+
assigned_to: task.assigned_to,
|
|
4542
|
+
session_id: task.session_id,
|
|
4543
|
+
working_dir: task.working_dir,
|
|
4544
|
+
tags: task.tags,
|
|
4545
|
+
metadata: task.metadata,
|
|
4546
|
+
version: task.version,
|
|
4547
|
+
created_at: task.created_at,
|
|
4548
|
+
updated_at: task.updated_at,
|
|
4549
|
+
started_at: task.started_at,
|
|
4550
|
+
completed_at: task.completed_at,
|
|
4551
|
+
due_at: task.due_at,
|
|
4552
|
+
...extra
|
|
4553
|
+
};
|
|
4554
|
+
}
|
|
4555
|
+
async function emitSharedTaskEvent(input) {
|
|
4556
|
+
const data = taskEventData(input.task, input.data);
|
|
4557
|
+
await new EventsClient().emit({
|
|
4558
|
+
source: SOURCE,
|
|
4559
|
+
type: input.type,
|
|
4560
|
+
subject: input.task.id,
|
|
4561
|
+
severity: input.severity ?? "info",
|
|
4562
|
+
message: input.message ?? `${input.type}: ${input.task.title}`,
|
|
4563
|
+
data,
|
|
4564
|
+
dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
|
|
4565
|
+
metadata: {
|
|
4566
|
+
package: "@hasna/todos",
|
|
4567
|
+
task_id: input.task.id,
|
|
4568
|
+
project_id: input.task.project_id,
|
|
4569
|
+
task_list_id: input.task.task_list_id
|
|
4570
|
+
}
|
|
4571
|
+
}, { deliver: true, dedupe: true });
|
|
4572
|
+
}
|
|
4573
|
+
function emitSharedTaskEventQuiet(input) {
|
|
4574
|
+
emitSharedTaskEvent(input).catch(() => {
|
|
4575
|
+
return;
|
|
4576
|
+
});
|
|
4577
|
+
}
|
|
4578
|
+
var SOURCE = "todos";
|
|
4579
|
+
var init_shared_events = __esm(() => {
|
|
4580
|
+
init_dist();
|
|
4581
|
+
});
|
|
4582
|
+
|
|
4016
4583
|
// src/lib/secret-redaction.ts
|
|
4017
4584
|
function isAllowlisted(text, match, allowlist) {
|
|
4018
4585
|
const context = text.slice(Math.max(0, text.indexOf(match) - 20), text.indexOf(match) + match.length + 20);
|
|
@@ -4480,7 +5047,7 @@ async function deliverWebhook(wh, event, body, attempt, db) {
|
|
|
4480
5047
|
activeDeliveries--;
|
|
4481
5048
|
}
|
|
4482
5049
|
}
|
|
4483
|
-
async function
|
|
5050
|
+
async function dispatchWebhook2(event, payload, db) {
|
|
4484
5051
|
const d = db || getDatabase();
|
|
4485
5052
|
const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
|
|
4486
5053
|
const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
|
|
@@ -4597,7 +5164,10 @@ function createTask(input, db) {
|
|
|
4597
5164
|
insertTaskTags(id, tags, d);
|
|
4598
5165
|
}
|
|
4599
5166
|
const task = getTask(id, d);
|
|
4600
|
-
|
|
5167
|
+
const payload = taskEventData(task);
|
|
5168
|
+
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
5169
|
+
emitLocalEventHooksQuiet({ type: "task.created", payload });
|
|
5170
|
+
emitSharedTaskEventQuiet({ type: "task.created", task });
|
|
4601
5171
|
return task;
|
|
4602
5172
|
}
|
|
4603
5173
|
function getTask(id, db) {
|
|
@@ -4941,18 +5511,7 @@ function updateTask(id, input, db) {
|
|
|
4941
5511
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
4942
5512
|
if (input.approved_by !== undefined)
|
|
4943
5513
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
4944
|
-
|
|
4945
|
-
dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
|
|
4946
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id, assigned_to: input.assigned_to, title: task.title } });
|
|
4947
|
-
}
|
|
4948
|
-
if (input.status !== undefined && input.status !== task.status) {
|
|
4949
|
-
dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
|
|
4950
|
-
emitLocalEventHooksQuiet({ type: "task.status_changed", payload: { id, old_status: task.status, new_status: input.status, title: task.title } });
|
|
4951
|
-
}
|
|
4952
|
-
if (input.approved_by !== undefined) {
|
|
4953
|
-
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
4954
|
-
}
|
|
4955
|
-
return {
|
|
5514
|
+
const updatedTask = {
|
|
4956
5515
|
...task,
|
|
4957
5516
|
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
4958
5517
|
tags: input.tags ?? task.tags,
|
|
@@ -4970,6 +5529,22 @@ function updateTask(id, input, db) {
|
|
|
4970
5529
|
approved_by: input.approved_by ?? task.approved_by,
|
|
4971
5530
|
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
4972
5531
|
};
|
|
5532
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
5533
|
+
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
5534
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
5535
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
5536
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
|
|
5537
|
+
}
|
|
5538
|
+
if (input.status !== undefined && input.status !== task.status) {
|
|
5539
|
+
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
5540
|
+
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
5541
|
+
emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
|
|
5542
|
+
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
|
|
5543
|
+
}
|
|
5544
|
+
if (input.approved_by !== undefined) {
|
|
5545
|
+
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
5546
|
+
}
|
|
5547
|
+
return updatedTask;
|
|
4973
5548
|
}
|
|
4974
5549
|
function deleteTask(id, db) {
|
|
4975
5550
|
const d = db || getDatabase();
|
|
@@ -4981,6 +5556,7 @@ var init_task_crud = __esm(() => {
|
|
|
4981
5556
|
init_database();
|
|
4982
5557
|
init_completion_guard();
|
|
4983
5558
|
init_event_hooks();
|
|
5559
|
+
init_shared_events();
|
|
4984
5560
|
init_audit();
|
|
4985
5561
|
init_webhooks();
|
|
4986
5562
|
init_checklists();
|
|
@@ -5736,9 +6312,12 @@ function startTask(id, agentId, db) {
|
|
|
5736
6312
|
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
5737
6313
|
}
|
|
5738
6314
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
6315
|
+
const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
|
|
6316
|
+
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
6317
|
+
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
6318
|
+
emitLocalEventHooksQuiet({ type: "task.started", payload });
|
|
6319
|
+
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
|
|
6320
|
+
return startedTask;
|
|
5742
6321
|
}
|
|
5743
6322
|
function completeTask(id, agentId, db, options) {
|
|
5744
6323
|
const d = db || getDatabase();
|
|
@@ -5774,8 +6353,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
5774
6353
|
});
|
|
5775
6354
|
tx();
|
|
5776
6355
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
5777
|
-
|
|
5778
|
-
|
|
6356
|
+
const completedTaskForEvent = {
|
|
6357
|
+
...task,
|
|
6358
|
+
status: "completed",
|
|
6359
|
+
locked_by: null,
|
|
6360
|
+
locked_at: null,
|
|
6361
|
+
completed_at: timestamp,
|
|
6362
|
+
confidence,
|
|
6363
|
+
version: task.version + 1,
|
|
6364
|
+
updated_at: timestamp,
|
|
6365
|
+
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
6366
|
+
};
|
|
6367
|
+
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
6368
|
+
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
6369
|
+
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
|
|
6370
|
+
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
|
|
5779
6371
|
let spawnedTask = null;
|
|
5780
6372
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
5781
6373
|
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
@@ -5816,8 +6408,12 @@ function completeTask(id, agentId, db, options) {
|
|
|
5816
6408
|
if (unblockedDeps.length > 0) {
|
|
5817
6409
|
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
5818
6410
|
for (const dep of unblockedDeps) {
|
|
5819
|
-
|
|
5820
|
-
|
|
6411
|
+
const depTask = getTask(dep.id, d);
|
|
6412
|
+
const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
|
|
6413
|
+
dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
|
|
6414
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
|
|
6415
|
+
if (depTask)
|
|
6416
|
+
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
|
|
5821
6417
|
}
|
|
5822
6418
|
}
|
|
5823
6419
|
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
@@ -6003,9 +6599,6 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
6003
6599
|
const timestamp = now();
|
|
6004
6600
|
d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
6005
6601
|
WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
|
|
6006
|
-
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
6007
|
-
dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
6008
|
-
emitLocalEventHooksQuiet({ type: "task.failed", payload: { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title } });
|
|
6009
6602
|
const failedTask = {
|
|
6010
6603
|
...task,
|
|
6011
6604
|
status: "failed",
|
|
@@ -6015,6 +6608,11 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
6015
6608
|
version: task.version + 1,
|
|
6016
6609
|
updated_at: timestamp
|
|
6017
6610
|
};
|
|
6611
|
+
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
6612
|
+
const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
|
|
6613
|
+
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
6614
|
+
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
|
|
6615
|
+
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
|
|
6018
6616
|
let retryTask;
|
|
6019
6617
|
if (options?.retry) {
|
|
6020
6618
|
const retryCount = (task.retry_count || 0) + 1;
|
|
@@ -6089,9 +6687,12 @@ function stealTask(agentId, opts, db) {
|
|
|
6089
6687
|
return null;
|
|
6090
6688
|
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
6091
6689
|
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6690
|
+
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
6691
|
+
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
6692
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
6693
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
6694
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
|
|
6695
|
+
return stolenTask;
|
|
6095
6696
|
}
|
|
6096
6697
|
function claimOrSteal(agentId, filters, db) {
|
|
6097
6698
|
const d = db || getDatabase();
|
|
@@ -6139,6 +6740,7 @@ var init_task_lifecycle = __esm(() => {
|
|
|
6139
6740
|
init_database();
|
|
6140
6741
|
init_completion_guard();
|
|
6141
6742
|
init_event_hooks();
|
|
6743
|
+
init_shared_events();
|
|
6142
6744
|
init_audit();
|
|
6143
6745
|
init_recurrence();
|
|
6144
6746
|
init_webhooks();
|
|
@@ -6826,7 +7428,7 @@ function getTaskWatchers(taskId, db) {
|
|
|
6826
7428
|
}
|
|
6827
7429
|
function notifyWatchers(taskId, event, data, db) {
|
|
6828
7430
|
const watchers = getTaskWatchers(taskId, db);
|
|
6829
|
-
|
|
7431
|
+
dispatchWebhook2(`task.watcher.${event}`, { task_id: taskId, watchers: watchers.map((w) => w.agent_id), ...data }, db).catch(() => {});
|
|
6830
7432
|
}
|
|
6831
7433
|
function logCost(taskId, tokens, usd, db) {
|
|
6832
7434
|
const d = db || getDatabase();
|
|
@@ -7320,8 +7922,8 @@ var init_boards = __esm(() => {
|
|
|
7320
7922
|
|
|
7321
7923
|
// src/lib/artifact-store.ts
|
|
7322
7924
|
import { createHash as createHash3 } from "crypto";
|
|
7323
|
-
import { existsSync as
|
|
7324
|
-
import { basename, dirname as dirname5, join as
|
|
7925
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
7926
|
+
import { basename, dirname as dirname5, join as join6, resolve as resolve6 } from "path";
|
|
7325
7927
|
import { tmpdir } from "os";
|
|
7326
7928
|
function isInMemoryDb2(path) {
|
|
7327
7929
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
@@ -7333,15 +7935,15 @@ function artifactStoreRoot() {
|
|
|
7333
7935
|
return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
7334
7936
|
const dbPath = getDatabasePath();
|
|
7335
7937
|
if (isInMemoryDb2(dbPath))
|
|
7336
|
-
return
|
|
7337
|
-
return
|
|
7938
|
+
return join6(tmpdir(), "hasna-todos-artifacts");
|
|
7939
|
+
return join6(dirname5(resolve6(dbPath)), "artifacts");
|
|
7338
7940
|
}
|
|
7339
7941
|
function artifactStorePath(relativePath) {
|
|
7340
7942
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
7341
7943
|
if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
|
|
7342
7944
|
throw new Error("Invalid artifact store path");
|
|
7343
7945
|
}
|
|
7344
|
-
return
|
|
7946
|
+
return join6(artifactStoreRoot(), normalized);
|
|
7345
7947
|
}
|
|
7346
7948
|
function sha256(buffer) {
|
|
7347
7949
|
return createHash3("sha256").update(buffer).digest("hex");
|
|
@@ -7382,7 +7984,7 @@ function mediaTypeFor(path, textLike) {
|
|
|
7382
7984
|
}
|
|
7383
7985
|
function storeArtifactContent(input) {
|
|
7384
7986
|
const sourcePath = resolve6(input.path);
|
|
7385
|
-
if (!
|
|
7987
|
+
if (!existsSync7(sourcePath))
|
|
7386
7988
|
return null;
|
|
7387
7989
|
const sourceStat = statSync2(sourcePath);
|
|
7388
7990
|
if (!sourceStat.isFile())
|
|
@@ -7399,9 +8001,9 @@ function storeArtifactContent(input) {
|
|
|
7399
8001
|
redactionStatus = "redacted";
|
|
7400
8002
|
}
|
|
7401
8003
|
const storedSha = sha256(storedBuffer);
|
|
7402
|
-
const relativePath =
|
|
8004
|
+
const relativePath = join6("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
|
|
7403
8005
|
const destination = artifactStorePath(relativePath);
|
|
7404
|
-
if (!
|
|
8006
|
+
if (!existsSync7(destination)) {
|
|
7405
8007
|
mkdirSync4(dirname5(destination), { recursive: true });
|
|
7406
8008
|
writeFileSync2(destination, storedBuffer);
|
|
7407
8009
|
}
|
|
@@ -7461,7 +8063,7 @@ function verifyStoredArtifact(input) {
|
|
|
7461
8063
|
};
|
|
7462
8064
|
}
|
|
7463
8065
|
const storedPath = artifactStorePath(store.relative_path);
|
|
7464
|
-
if (!
|
|
8066
|
+
if (!existsSync7(storedPath)) {
|
|
7465
8067
|
return {
|
|
7466
8068
|
id: input.id,
|
|
7467
8069
|
path: input.path,
|
|
@@ -9488,8 +10090,8 @@ var exports_doctor = {};
|
|
|
9488
10090
|
__export(exports_doctor, {
|
|
9489
10091
|
runTodosDoctor: () => runTodosDoctor
|
|
9490
10092
|
});
|
|
9491
|
-
import { chmodSync, copyFileSync, existsSync as
|
|
9492
|
-
import { basename as basename2, dirname as dirname6, join as
|
|
10093
|
+
import { chmodSync, copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync5, statSync as statSync3 } from "fs";
|
|
10094
|
+
import { basename as basename2, dirname as dirname6, join as join7 } from "path";
|
|
9493
10095
|
function tableExists(db, table) {
|
|
9494
10096
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
9495
10097
|
}
|
|
@@ -9583,7 +10185,7 @@ function findMissingProjectRoots(db) {
|
|
|
9583
10185
|
continue;
|
|
9584
10186
|
if (!row.path.startsWith("/"))
|
|
9585
10187
|
continue;
|
|
9586
|
-
if (!
|
|
10188
|
+
if (!existsSync8(row.path))
|
|
9587
10189
|
missing++;
|
|
9588
10190
|
}
|
|
9589
10191
|
return missing;
|
|
@@ -9643,16 +10245,16 @@ function databasePermissionsAreUnsafe(dbPath) {
|
|
|
9643
10245
|
function createBackup(dbPath) {
|
|
9644
10246
|
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
9645
10247
|
return;
|
|
9646
|
-
if (!
|
|
10248
|
+
if (!existsSync8(dbPath))
|
|
9647
10249
|
return;
|
|
9648
10250
|
const stamp = now().replace(/[:.]/g, "-");
|
|
9649
|
-
const backupDir =
|
|
10251
|
+
const backupDir = join7(dirname6(dbPath), `${basename2(dbPath)}.backup-${stamp}`);
|
|
9650
10252
|
const files = [];
|
|
9651
10253
|
mkdirSync5(backupDir, { recursive: true });
|
|
9652
10254
|
for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
9653
|
-
if (!
|
|
10255
|
+
if (!existsSync8(source))
|
|
9654
10256
|
continue;
|
|
9655
|
-
const target =
|
|
10257
|
+
const target = join7(backupDir, basename2(source));
|
|
9656
10258
|
copyFileSync(source, target);
|
|
9657
10259
|
files.push(target);
|
|
9658
10260
|
}
|
|
@@ -9878,7 +10480,7 @@ var init_doctor = __esm(() => {
|
|
|
9878
10480
|
});
|
|
9879
10481
|
|
|
9880
10482
|
// src/server/routes.ts
|
|
9881
|
-
import { join as
|
|
10483
|
+
import { join as join8, resolve as resolve7, sep } from "path";
|
|
9882
10484
|
function parseFieldsParam(url) {
|
|
9883
10485
|
const fieldsParam = url.searchParams.get("fields");
|
|
9884
10486
|
return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
@@ -10582,7 +11184,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
10582
11184
|
if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
|
|
10583
11185
|
return null;
|
|
10584
11186
|
if (path !== "/") {
|
|
10585
|
-
const filePath =
|
|
11187
|
+
const filePath = join8(ctx.dashboardDir, path);
|
|
10586
11188
|
const resolvedFile = resolve7(filePath);
|
|
10587
11189
|
const resolvedBase = resolve7(ctx.dashboardDir);
|
|
10588
11190
|
if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
|
|
@@ -10592,7 +11194,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
10592
11194
|
if (res2)
|
|
10593
11195
|
return res2;
|
|
10594
11196
|
}
|
|
10595
|
-
const indexPath =
|
|
11197
|
+
const indexPath = join8(ctx.dashboardDir, "index.html");
|
|
10596
11198
|
const res = serveStaticFile2(indexPath);
|
|
10597
11199
|
if (res)
|
|
10598
11200
|
return res;
|
|
@@ -13984,7 +14586,7 @@ class JSONSchemaGenerator {
|
|
|
13984
14586
|
if (val === undefined) {
|
|
13985
14587
|
if (this.unrepresentable === "throw") {
|
|
13986
14588
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
13987
|
-
}
|
|
14589
|
+
} else {}
|
|
13988
14590
|
} else if (typeof val === "bigint") {
|
|
13989
14591
|
if (this.unrepresentable === "throw") {
|
|
13990
14592
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -31037,17 +31639,73 @@ var init_dispatch_formatter = __esm(() => {
|
|
|
31037
31639
|
});
|
|
31038
31640
|
|
|
31039
31641
|
// src/lib/tmux.ts
|
|
31642
|
+
async function inspectTmuxPane(target) {
|
|
31643
|
+
const proc = Bun.spawn([
|
|
31644
|
+
"tmux",
|
|
31645
|
+
"display-message",
|
|
31646
|
+
"-p",
|
|
31647
|
+
"-t",
|
|
31648
|
+
target,
|
|
31649
|
+
"#{pane_id}\t#{pane_current_command}\t#{pane_dead}\t#{pane_input_off}\t#{pane_in_mode}"
|
|
31650
|
+
], {
|
|
31651
|
+
stdout: "pipe",
|
|
31652
|
+
stderr: "pipe"
|
|
31653
|
+
});
|
|
31654
|
+
const exitCode = await proc.exited;
|
|
31655
|
+
if (exitCode !== 0) {
|
|
31656
|
+
const stderr = await new Response(proc.stderr).text();
|
|
31657
|
+
throw new Error(`tmux target "${target}" not found: ${stderr.trim() || "unknown error"}`);
|
|
31658
|
+
}
|
|
31659
|
+
const stdout = (await new Response(proc.stdout).text()).trim();
|
|
31660
|
+
const [paneId, currentCommand = "", paneDead = "0", inputOff = "0", inMode = "0"] = stdout.split("\t");
|
|
31661
|
+
if (!paneId) {
|
|
31662
|
+
throw new Error(`tmux target "${target}" did not resolve to a pane`);
|
|
31663
|
+
}
|
|
31664
|
+
return {
|
|
31665
|
+
target,
|
|
31666
|
+
paneId,
|
|
31667
|
+
currentCommand,
|
|
31668
|
+
paneDead: paneDead === "1",
|
|
31669
|
+
inputOff: inputOff === "1",
|
|
31670
|
+
inMode: inMode === "1"
|
|
31671
|
+
};
|
|
31672
|
+
}
|
|
31673
|
+
function tmuxPaneBusyStatus(pane) {
|
|
31674
|
+
if (pane.paneDead) {
|
|
31675
|
+
return { busy: true, reason: "pane is dead" };
|
|
31676
|
+
}
|
|
31677
|
+
if (pane.inputOff) {
|
|
31678
|
+
return { busy: true, reason: "pane input is disabled" };
|
|
31679
|
+
}
|
|
31680
|
+
if (pane.inMode) {
|
|
31681
|
+
return { busy: true, reason: "pane is in copy or alternate mode" };
|
|
31682
|
+
}
|
|
31683
|
+
const currentCommand = pane.currentCommand.trim();
|
|
31684
|
+
if (currentCommand && !IDLE_TMUX_COMMANDS.has(currentCommand)) {
|
|
31685
|
+
return { busy: true, reason: `pane is running ${currentCommand}` };
|
|
31686
|
+
}
|
|
31687
|
+
return { busy: false, reason: null };
|
|
31688
|
+
}
|
|
31040
31689
|
function calculateDelay(message) {
|
|
31041
31690
|
const len = message.length;
|
|
31042
31691
|
const extra = Math.floor(len / 100 * 40);
|
|
31043
31692
|
return Math.min(DELAY_MIN + extra, DELAY_MAX);
|
|
31044
31693
|
}
|
|
31045
|
-
async function sendToTmux(target, message, delayMs,
|
|
31694
|
+
async function sendToTmux(target, message, delayMs, options = false) {
|
|
31695
|
+
const opts = typeof options === "boolean" ? { dryRun: options } : options;
|
|
31696
|
+
const dryRun = opts.dryRun ?? false;
|
|
31046
31697
|
if (dryRun) {
|
|
31047
31698
|
console.log(`[dry-run] sendToTmux target=${target} delay=${delayMs}ms`);
|
|
31048
31699
|
console.log(`[dry-run] message: ${message.slice(0, 200)}`);
|
|
31049
31700
|
return;
|
|
31050
31701
|
}
|
|
31702
|
+
if (!opts.confirmBusy) {
|
|
31703
|
+
const pane = await inspectTmuxPane(target);
|
|
31704
|
+
const status = tmuxPaneBusyStatus(pane);
|
|
31705
|
+
if (status.busy) {
|
|
31706
|
+
throw new Error(`tmux target "${target}" appears busy (${status.reason}). Re-run with --confirm-busy to send anyway.`);
|
|
31707
|
+
}
|
|
31708
|
+
}
|
|
31051
31709
|
const sendProc = Bun.spawn(["tmux", "send-keys", "-t", target, message, ""], {
|
|
31052
31710
|
stdout: "pipe",
|
|
31053
31711
|
stderr: "pipe"
|
|
@@ -31068,7 +31726,21 @@ async function sendToTmux(target, message, delayMs, dryRun = false) {
|
|
|
31068
31726
|
throw new Error(`tmux send-keys Enter failed for target "${target}": ${stderr.trim()}`);
|
|
31069
31727
|
}
|
|
31070
31728
|
}
|
|
31071
|
-
var DELAY_MIN = 3000, DELAY_MAX = 5000;
|
|
31729
|
+
var DELAY_MIN = 3000, DELAY_MAX = 5000, IDLE_TMUX_COMMANDS;
|
|
31730
|
+
var init_tmux = __esm(() => {
|
|
31731
|
+
IDLE_TMUX_COMMANDS = new Set([
|
|
31732
|
+
"bash",
|
|
31733
|
+
"dash",
|
|
31734
|
+
"elvish",
|
|
31735
|
+
"fish",
|
|
31736
|
+
"ksh",
|
|
31737
|
+
"nu",
|
|
31738
|
+
"pwsh",
|
|
31739
|
+
"sh",
|
|
31740
|
+
"tmux",
|
|
31741
|
+
"zsh"
|
|
31742
|
+
]);
|
|
31743
|
+
});
|
|
31072
31744
|
|
|
31073
31745
|
// src/lib/dispatch.ts
|
|
31074
31746
|
var exports_dispatch = {};
|
|
@@ -31086,7 +31758,10 @@ async function executeDispatch(dispatch, opts = {}, db) {
|
|
|
31086
31758
|
}
|
|
31087
31759
|
const delayMs = dispatch.delay_ms ?? calculateDelay(message);
|
|
31088
31760
|
try {
|
|
31089
|
-
await sendToTmux(dispatch.target_window, message, delayMs,
|
|
31761
|
+
await sendToTmux(dispatch.target_window, message, delayMs, {
|
|
31762
|
+
dryRun: opts.dryRun ?? false,
|
|
31763
|
+
confirmBusy: opts.confirmBusy ?? false
|
|
31764
|
+
});
|
|
31090
31765
|
createDispatchLog({
|
|
31091
31766
|
dispatch_id: dispatch.id,
|
|
31092
31767
|
target_window: dispatch.target_window,
|
|
@@ -31145,6 +31820,7 @@ var init_dispatch = __esm(() => {
|
|
|
31145
31820
|
init_tasks();
|
|
31146
31821
|
init_database();
|
|
31147
31822
|
init_dispatch_formatter();
|
|
31823
|
+
init_tmux();
|
|
31148
31824
|
});
|
|
31149
31825
|
|
|
31150
31826
|
// src/db/task-lists.ts
|
|
@@ -31239,8 +31915,9 @@ function registerDispatchTools(server, { shouldRegisterTool, resolveId, formatEr
|
|
|
31239
31915
|
target: exports_external.string().describe("tmux target \u2014 window name, session:window, or session:window.pane"),
|
|
31240
31916
|
delay_ms: exports_external.number().optional().describe("Delay in ms between sending the message and hitting Enter. Auto-calculated from message length (3-5s) if omitted."),
|
|
31241
31917
|
scheduled_at: exports_external.string().optional().describe("ISO datetime to schedule the dispatch for. Fires immediately if omitted."),
|
|
31918
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if the target tmux pane appears busy. Default: false."),
|
|
31242
31919
|
dry_run: exports_external.boolean().optional().describe("Preview the formatted message without sending. Default: false.")
|
|
31243
|
-
}, async ({ task_ids, target, delay_ms, scheduled_at, dry_run }) => {
|
|
31920
|
+
}, async ({ task_ids, target, delay_ms, scheduled_at, confirm_busy, dry_run }) => {
|
|
31244
31921
|
try {
|
|
31245
31922
|
const db = getDatabase();
|
|
31246
31923
|
const resolvedIds = task_ids.map((id) => resolveId(id));
|
|
@@ -31256,7 +31933,7 @@ ${message}` }]
|
|
|
31256
31933
|
}
|
|
31257
31934
|
const dispatch = createDispatch({ task_ids: resolvedIds, target_window: target, message, delay_ms: effectiveDelay, scheduled_at }, db);
|
|
31258
31935
|
if (!scheduled_at)
|
|
31259
|
-
await executeDispatch(dispatch, {}, db);
|
|
31936
|
+
await executeDispatch(dispatch, { confirmBusy: confirm_busy ?? false }, db);
|
|
31260
31937
|
return {
|
|
31261
31938
|
content: [{
|
|
31262
31939
|
type: "text",
|
|
@@ -31282,8 +31959,9 @@ ${message}`
|
|
|
31282
31959
|
filter_status: exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"])).optional().describe("Only include tasks with these statuses. Default: pending."),
|
|
31283
31960
|
delay_ms: exports_external.number().optional().describe("Delay in ms between sending and Enter. Auto-calculated if omitted."),
|
|
31284
31961
|
scheduled_at: exports_external.string().optional().describe("ISO datetime to schedule. Fires immediately if omitted."),
|
|
31962
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if the target tmux pane appears busy. Default: false."),
|
|
31285
31963
|
dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false.")
|
|
31286
|
-
}, async ({ task_list_id, target, filter_status, delay_ms, scheduled_at, dry_run }) => {
|
|
31964
|
+
}, async ({ task_list_id, target, filter_status, delay_ms, scheduled_at, confirm_busy, dry_run }) => {
|
|
31287
31965
|
try {
|
|
31288
31966
|
const db = getDatabase();
|
|
31289
31967
|
const resolvedListId = resolveId(task_list_id, "task_lists");
|
|
@@ -31303,7 +31981,7 @@ ${message}` }]
|
|
|
31303
31981
|
}
|
|
31304
31982
|
const dispatch = createDispatch({ title: `Task list: ${taskList.name}`, task_list_id: resolvedListId, target_window: target, message, delay_ms: effectiveDelay, scheduled_at }, db);
|
|
31305
31983
|
if (!scheduled_at)
|
|
31306
|
-
await executeDispatch(dispatch, {}, db);
|
|
31984
|
+
await executeDispatch(dispatch, { confirmBusy: confirm_busy ?? false }, db);
|
|
31307
31985
|
return {
|
|
31308
31986
|
content: [{
|
|
31309
31987
|
type: "text",
|
|
@@ -31330,15 +32008,16 @@ ${message}`
|
|
|
31330
32008
|
task_list_id: exports_external.string().optional().describe("Task list ID to dispatch (use this or task_ids)"),
|
|
31331
32009
|
stagger_ms: exports_external.number().optional().describe("Delay between each window dispatch. Default: 500ms."),
|
|
31332
32010
|
delay_ms: exports_external.number().optional().describe("Delay between message send and Enter. Auto-calculated if omitted."),
|
|
32011
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
|
|
31333
32012
|
dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false.")
|
|
31334
|
-
}, async ({ targets, task_ids, task_list_id, stagger_ms, delay_ms, dry_run }) => {
|
|
32013
|
+
}, async ({ targets, task_ids, task_list_id, stagger_ms, delay_ms, confirm_busy, dry_run }) => {
|
|
31335
32014
|
try {
|
|
31336
32015
|
if (!task_ids && !task_list_id)
|
|
31337
32016
|
throw new Error("Either task_ids or task_list_id is required");
|
|
31338
32017
|
const db = getDatabase();
|
|
31339
32018
|
const resolvedTaskIds = task_ids ? task_ids.map((id) => resolveId(id)) : undefined;
|
|
31340
32019
|
const resolvedListId = task_list_id ? resolveId(task_list_id, "task_lists") : undefined;
|
|
31341
|
-
const dispatches = await dispatchToMultiple({ targets, task_ids: resolvedTaskIds, task_list_id: resolvedListId, delay_ms, stagger_ms }, { dryRun: dry_run }, db);
|
|
32020
|
+
const dispatches = await dispatchToMultiple({ targets, task_ids: resolvedTaskIds, task_list_id: resolvedListId, delay_ms, stagger_ms }, { dryRun: dry_run, confirmBusy: confirm_busy ?? false }, db);
|
|
31342
32021
|
const lines = dispatches.map((d) => `${d.target_window}: ${d.id} [${d.status}]`);
|
|
31343
32022
|
return { content: [{ type: "text", text: `Dispatched to ${dispatches.length} target(s):
|
|
31344
32023
|
${lines.join(`
|
|
@@ -31383,11 +32062,12 @@ ${lines.join(`
|
|
|
31383
32062
|
if (shouldRegisterTool("run_due_dispatches")) {
|
|
31384
32063
|
server.tool("run_due_dispatches", "Manually trigger all pending dispatches that are due (scheduled_at <= now). Returns the count fired.", {
|
|
31385
32064
|
dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false."),
|
|
32065
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
|
|
31386
32066
|
all: exports_external.boolean().optional().describe("Ignore scheduled_at and fire all pending dispatches immediately.")
|
|
31387
|
-
}, async ({ dry_run }) => {
|
|
32067
|
+
}, async ({ dry_run, confirm_busy }) => {
|
|
31388
32068
|
try {
|
|
31389
32069
|
const { runDueDispatches: runDueDispatches2 } = await Promise.resolve().then(() => (init_dispatch(), exports_dispatch));
|
|
31390
|
-
const count = await runDueDispatches2({ dryRun: dry_run });
|
|
32070
|
+
const count = await runDueDispatches2({ dryRun: dry_run, confirmBusy: confirm_busy ?? false });
|
|
31391
32071
|
return { content: [{ type: "text", text: `Fired ${count} dispatch(es).` }] };
|
|
31392
32072
|
} catch (e) {
|
|
31393
32073
|
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
@@ -31400,6 +32080,7 @@ var init_dispatch2 = __esm(() => {
|
|
|
31400
32080
|
init_dispatches();
|
|
31401
32081
|
init_dispatch();
|
|
31402
32082
|
init_dispatch_formatter();
|
|
32083
|
+
init_tmux();
|
|
31403
32084
|
init_tasks();
|
|
31404
32085
|
init_task_lists();
|
|
31405
32086
|
init_database();
|
|
@@ -32207,7 +32888,7 @@ var init_task_crud2 = __esm(() => {
|
|
|
32207
32888
|
});
|
|
32208
32889
|
|
|
32209
32890
|
// src/lib/project-bootstrap.ts
|
|
32210
|
-
import { existsSync as
|
|
32891
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
32211
32892
|
import { basename as basename3, dirname as dirname7, resolve as resolve8 } from "path";
|
|
32212
32893
|
function safeStat(path) {
|
|
32213
32894
|
try {
|
|
@@ -32226,7 +32907,7 @@ function canonicalPath(input) {
|
|
|
32226
32907
|
function findUp(start, marker) {
|
|
32227
32908
|
let current = canonicalPath(start);
|
|
32228
32909
|
while (true) {
|
|
32229
|
-
if (
|
|
32910
|
+
if (existsSync9(resolve8(current, marker)))
|
|
32230
32911
|
return current;
|
|
32231
32912
|
const parent = dirname7(current);
|
|
32232
32913
|
if (parent === current)
|
|
@@ -32238,7 +32919,7 @@ function readPackageJson(path) {
|
|
|
32238
32919
|
if (!path)
|
|
32239
32920
|
return null;
|
|
32240
32921
|
const file = resolve8(path, "package.json");
|
|
32241
|
-
if (!
|
|
32922
|
+
if (!existsSync9(file))
|
|
32242
32923
|
return null;
|
|
32243
32924
|
try {
|
|
32244
32925
|
const parsed = JSON.parse(readFileSync4(file, "utf-8"));
|
|
@@ -32260,7 +32941,7 @@ function workspaceMarker(root, rootPackage) {
|
|
|
32260
32941
|
if (rootPackage?.workspaces)
|
|
32261
32942
|
markers.push("package.json#workspaces");
|
|
32262
32943
|
for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
|
|
32263
|
-
if (
|
|
32944
|
+
if (existsSync9(resolve8(root, marker)))
|
|
32264
32945
|
markers.push(marker);
|
|
32265
32946
|
}
|
|
32266
32947
|
const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
|
|
@@ -32561,7 +33242,7 @@ var init_tags = __esm(() => {
|
|
|
32561
33242
|
});
|
|
32562
33243
|
|
|
32563
33244
|
// src/lib/retention-cleanup.ts
|
|
32564
|
-
import { existsSync as
|
|
33245
|
+
import { existsSync as existsSync10, unlinkSync } from "fs";
|
|
32565
33246
|
function normalizeScopes(scopes) {
|
|
32566
33247
|
if (!scopes || scopes.length === 0)
|
|
32567
33248
|
return [...ALL_SCOPES];
|
|
@@ -32764,7 +33445,7 @@ function applyRetentionCleanup(input, db) {
|
|
|
32764
33445
|
for (const artifact of report.candidates.artifact_files) {
|
|
32765
33446
|
try {
|
|
32766
33447
|
const path = artifactStorePath(artifact.relative_path);
|
|
32767
|
-
if (!
|
|
33448
|
+
if (!existsSync10(path)) {
|
|
32768
33449
|
report.warnings.push(`stored artifact already missing: ${artifact.relative_path}`);
|
|
32769
33450
|
continue;
|
|
32770
33451
|
}
|
|
@@ -32791,8 +33472,8 @@ var init_retention_cleanup = __esm(() => {
|
|
|
32791
33472
|
});
|
|
32792
33473
|
|
|
32793
33474
|
// src/lib/mention-resolver.ts
|
|
32794
|
-
import { existsSync as
|
|
32795
|
-
import { basename as basename4, isAbsolute, join as
|
|
33475
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync5 } from "fs";
|
|
33476
|
+
import { basename as basename4, isAbsolute, join as join9, relative as relative3, resolve as resolve9, sep as sep2 } from "path";
|
|
32796
33477
|
function blankResolution(parsed) {
|
|
32797
33478
|
return {
|
|
32798
33479
|
input: parsed.input,
|
|
@@ -32890,7 +33571,7 @@ function resolveFile(parsed, workspace) {
|
|
|
32890
33571
|
return resolution;
|
|
32891
33572
|
}
|
|
32892
33573
|
resolution.path = relPath;
|
|
32893
|
-
if (!
|
|
33574
|
+
if (!existsSync11(absolutePath)) {
|
|
32894
33575
|
resolution.warnings.push("file does not exist in the local workspace");
|
|
32895
33576
|
return resolution;
|
|
32896
33577
|
}
|
|
@@ -32923,7 +33604,7 @@ function walkSourceFiles(root, current = root, files = []) {
|
|
|
32923
33604
|
if (SKIP_DIRS.has(entry.name))
|
|
32924
33605
|
continue;
|
|
32925
33606
|
}
|
|
32926
|
-
const absolutePath =
|
|
33607
|
+
const absolutePath = join9(current, entry.name);
|
|
32927
33608
|
if (entry.isDirectory()) {
|
|
32928
33609
|
if (!SKIP_DIRS.has(entry.name))
|
|
32929
33610
|
walkSourceFiles(root, absolutePath, files);
|
|
@@ -34190,8 +34871,8 @@ var init_local_notifications = __esm(() => {
|
|
|
34190
34871
|
});
|
|
34191
34872
|
|
|
34192
34873
|
// src/lib/local-encryption.ts
|
|
34193
|
-
import { createCipheriv, createDecipheriv, createHash as createHash4, randomBytes as randomBytes2, scryptSync, timingSafeEqual as
|
|
34194
|
-
function
|
|
34874
|
+
import { createCipheriv, createDecipheriv, createHash as createHash4, randomBytes as randomBytes2, scryptSync, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
34875
|
+
function now3() {
|
|
34195
34876
|
return new Date().toISOString();
|
|
34196
34877
|
}
|
|
34197
34878
|
function sha2562(value) {
|
|
@@ -34228,7 +34909,7 @@ function upsertEncryptionProfile(input) {
|
|
|
34228
34909
|
const name = normalizeProfileName(input.name);
|
|
34229
34910
|
const config2 = loadConfig();
|
|
34230
34911
|
const existing = config2.encryption_profiles?.[name];
|
|
34231
|
-
const timestamp =
|
|
34912
|
+
const timestamp = now3();
|
|
34232
34913
|
const profile = {
|
|
34233
34914
|
name,
|
|
34234
34915
|
algorithm: "aes-256-gcm",
|
|
@@ -34285,7 +34966,7 @@ function encryptString(plaintext, options = {}) {
|
|
|
34285
34966
|
return {
|
|
34286
34967
|
schemaVersion: TODOS_ENCRYPTION_SCHEMA_VERSION,
|
|
34287
34968
|
kind: TODOS_ENCRYPTED_VALUE_KIND,
|
|
34288
|
-
encryptedAt: options.encryptedAt ??
|
|
34969
|
+
encryptedAt: options.encryptedAt ?? now3(),
|
|
34289
34970
|
profile: profile.name,
|
|
34290
34971
|
key_env: profile.key_env,
|
|
34291
34972
|
algorithm: "aes-256-gcm",
|
|
@@ -34320,7 +35001,7 @@ function decryptString(envelope, env = process.env) {
|
|
|
34320
35001
|
]).toString("utf8");
|
|
34321
35002
|
const expected = Buffer.from(envelope.plaintext_sha256, "hex");
|
|
34322
35003
|
const actual = Buffer.from(sha2562(plaintext), "hex");
|
|
34323
|
-
if (expected.length !== actual.length || !
|
|
35004
|
+
if (expected.length !== actual.length || !timingSafeEqual3(expected, actual)) {
|
|
34324
35005
|
throw new EncryptedPayloadError("decrypted payload checksum mismatch");
|
|
34325
35006
|
}
|
|
34326
35007
|
return plaintext;
|
|
@@ -35843,7 +36524,7 @@ function createRoadmap(input) {
|
|
|
35843
36524
|
if (!name)
|
|
35844
36525
|
throw new Error("Roadmap name is required");
|
|
35845
36526
|
const store = readStore();
|
|
35846
|
-
const
|
|
36527
|
+
const now4 = timestamp();
|
|
35847
36528
|
const roadmap = {
|
|
35848
36529
|
id: newId("roadmap"),
|
|
35849
36530
|
name,
|
|
@@ -35854,8 +36535,8 @@ function createRoadmap(input) {
|
|
|
35854
36535
|
agent_id: cleanString(input.agent_id),
|
|
35855
36536
|
release: cleanString(input.release),
|
|
35856
36537
|
milestone_ids: [],
|
|
35857
|
-
created_at:
|
|
35858
|
-
updated_at:
|
|
36538
|
+
created_at: now4,
|
|
36539
|
+
updated_at: now4
|
|
35859
36540
|
};
|
|
35860
36541
|
store.roadmaps[roadmap.id] = roadmap;
|
|
35861
36542
|
writeStore(store);
|
|
@@ -35907,7 +36588,7 @@ function createMilestone(input) {
|
|
|
35907
36588
|
const title = input.title.trim();
|
|
35908
36589
|
if (!title)
|
|
35909
36590
|
throw new Error("Milestone title is required");
|
|
35910
|
-
const
|
|
36591
|
+
const now4 = timestamp();
|
|
35911
36592
|
const milestone = {
|
|
35912
36593
|
id: newId("milestone"),
|
|
35913
36594
|
roadmap_id: roadmapId,
|
|
@@ -35922,11 +36603,11 @@ function createMilestone(input) {
|
|
|
35922
36603
|
run_ids: cleanList(input.run_ids),
|
|
35923
36604
|
release: cleanString(input.release ?? roadmap.release ?? undefined),
|
|
35924
36605
|
tags: cleanList(input.tags),
|
|
35925
|
-
created_at:
|
|
35926
|
-
updated_at:
|
|
36606
|
+
created_at: now4,
|
|
36607
|
+
updated_at: now4
|
|
35927
36608
|
};
|
|
35928
36609
|
store.milestones[milestone.id] = milestone;
|
|
35929
|
-
store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList([...roadmap.milestone_ids, milestone.id]), updated_at:
|
|
36610
|
+
store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList([...roadmap.milestone_ids, milestone.id]), updated_at: now4 };
|
|
35930
36611
|
writeStore(store);
|
|
35931
36612
|
return milestone;
|
|
35932
36613
|
}
|
|
@@ -35981,7 +36662,7 @@ function upsertReleaseGroup(input) {
|
|
|
35981
36662
|
throw new Error("Release group name is required");
|
|
35982
36663
|
const key = releaseKey(roadmapId, name);
|
|
35983
36664
|
const existing = store.releases[key];
|
|
35984
|
-
const
|
|
36665
|
+
const now4 = timestamp();
|
|
35985
36666
|
const release = {
|
|
35986
36667
|
name,
|
|
35987
36668
|
version: input.version === undefined ? existing?.version ?? null : cleanString(input.version),
|
|
@@ -35992,8 +36673,8 @@ function upsertReleaseGroup(input) {
|
|
|
35992
36673
|
plan_ids: input.plan_ids === undefined ? existing?.plan_ids ?? [] : cleanList(input.plan_ids),
|
|
35993
36674
|
run_ids: input.run_ids === undefined ? existing?.run_ids ?? [] : cleanList(input.run_ids),
|
|
35994
36675
|
notes: input.notes === undefined ? existing?.notes ?? null : cleanString(input.notes),
|
|
35995
|
-
created_at: existing?.created_at ??
|
|
35996
|
-
updated_at:
|
|
36676
|
+
created_at: existing?.created_at ?? now4,
|
|
36677
|
+
updated_at: now4
|
|
35997
36678
|
};
|
|
35998
36679
|
store.releases[key] = release;
|
|
35999
36680
|
writeStore(store);
|
|
@@ -36192,7 +36873,7 @@ function upsertCapacityProfile(input) {
|
|
|
36192
36873
|
const store = readStore2();
|
|
36193
36874
|
const key = profileKey(agentId, projectId);
|
|
36194
36875
|
const existing = store.profiles[key];
|
|
36195
|
-
const
|
|
36876
|
+
const now4 = timestamp2();
|
|
36196
36877
|
const profile = {
|
|
36197
36878
|
id: existing?.id ?? key,
|
|
36198
36879
|
agent_id: agentId,
|
|
@@ -36200,8 +36881,8 @@ function upsertCapacityProfile(input) {
|
|
|
36200
36881
|
minutes_per_day: assertMinutes(input.minutes_per_day),
|
|
36201
36882
|
working_days: normalizeWorkingDays(input.working_days),
|
|
36202
36883
|
effective_from: cleanString2(input.effective_from),
|
|
36203
|
-
created_at: existing?.created_at ??
|
|
36204
|
-
updated_at:
|
|
36884
|
+
created_at: existing?.created_at ?? now4,
|
|
36885
|
+
updated_at: now4
|
|
36205
36886
|
};
|
|
36206
36887
|
store.profiles[key] = profile;
|
|
36207
36888
|
writeStore2(store);
|
|
@@ -36610,7 +37291,7 @@ var init_audit_ledger = __esm(() => {
|
|
|
36610
37291
|
|
|
36611
37292
|
// src/lib/release-compatibility.ts
|
|
36612
37293
|
import { readFileSync as readFileSync6 } from "fs";
|
|
36613
|
-
import { join as
|
|
37294
|
+
import { join as join10, resolve as resolve11 } from "path";
|
|
36614
37295
|
import { Database as Database2 } from "bun:sqlite";
|
|
36615
37296
|
function pass(id, message, details) {
|
|
36616
37297
|
return { id, status: "passed", message, details };
|
|
@@ -36622,7 +37303,7 @@ function warn(id, message, details) {
|
|
|
36622
37303
|
return { id, status: "warning", message, details };
|
|
36623
37304
|
}
|
|
36624
37305
|
function readPackageJson2(root) {
|
|
36625
|
-
return JSON.parse(readFileSync6(
|
|
37306
|
+
return JSON.parse(readFileSync6(join10(root, "package.json"), "utf8"));
|
|
36626
37307
|
}
|
|
36627
37308
|
function sortedKeys(value) {
|
|
36628
37309
|
return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
|
|
@@ -36891,6 +37572,9 @@ function hasFts(db) {
|
|
|
36891
37572
|
function escapeFtsQuery(q) {
|
|
36892
37573
|
return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
|
|
36893
37574
|
}
|
|
37575
|
+
function shouldUseFts(q) {
|
|
37576
|
+
return /^[\p{L}\p{N}_\-\s]+$/u.test(q);
|
|
37577
|
+
}
|
|
36894
37578
|
function searchTasks(options, projectId, taskListId, db) {
|
|
36895
37579
|
const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
|
|
36896
37580
|
const d = db || getDatabase();
|
|
@@ -36899,7 +37583,8 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
36899
37583
|
let sql;
|
|
36900
37584
|
const raw = opts.query?.trim() ?? "";
|
|
36901
37585
|
const q = raw === "*" ? "" : raw;
|
|
36902
|
-
|
|
37586
|
+
const useFts = hasFts(d) && q && shouldUseFts(q);
|
|
37587
|
+
if (useFts) {
|
|
36903
37588
|
const ftsQuery = escapeFtsQuery(q);
|
|
36904
37589
|
sql = `SELECT t.* FROM tasks t
|
|
36905
37590
|
INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
|
|
@@ -36907,8 +37592,16 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
36907
37592
|
params.push(ftsQuery);
|
|
36908
37593
|
} else if (q) {
|
|
36909
37594
|
const pattern = `%${q}%`;
|
|
36910
|
-
sql = `SELECT * FROM tasks t WHERE (
|
|
36911
|
-
|
|
37595
|
+
sql = `SELECT * FROM tasks t WHERE (
|
|
37596
|
+
t.id LIKE ?
|
|
37597
|
+
OR t.short_id LIKE ?
|
|
37598
|
+
OR t.title LIKE ?
|
|
37599
|
+
OR t.description LIKE ?
|
|
37600
|
+
OR t.working_dir LIKE ?
|
|
37601
|
+
OR t.metadata LIKE ?
|
|
37602
|
+
OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?)
|
|
37603
|
+
)`;
|
|
37604
|
+
params.push(pattern, pattern, pattern, pattern, pattern, pattern, pattern);
|
|
36912
37605
|
} else {
|
|
36913
37606
|
sql = `SELECT * FROM tasks t WHERE 1=1`;
|
|
36914
37607
|
}
|
|
@@ -36964,7 +37657,7 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
36964
37657
|
} else if (opts.is_blocked === false) {
|
|
36965
37658
|
sql += " AND t.id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
|
|
36966
37659
|
}
|
|
36967
|
-
if (
|
|
37660
|
+
if (useFts) {
|
|
36968
37661
|
sql += ` ORDER BY bm25(tasks_fts),
|
|
36969
37662
|
CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
36970
37663
|
t.created_at DESC`;
|
|
@@ -40059,7 +40752,7 @@ function registerTaskWorkflowTools(server, ctx) {
|
|
|
40059
40752
|
const globalRole = n.agent.role ? ` [${n.agent.role}]` : "";
|
|
40060
40753
|
const lead = n.is_lead ? " \u2605" : "";
|
|
40061
40754
|
const lastSeen = new Date(n.agent.last_seen_at).getTime();
|
|
40062
|
-
const active =
|
|
40755
|
+
const active = now4 - lastSeen < ACTIVE_MS ? " \u25CF" : " \u25CB";
|
|
40063
40756
|
const line = `${prefix}${active} ${n.agent.name}${title}${globalRole}${lead}`;
|
|
40064
40757
|
const children = n.reports.length > 0 ? `
|
|
40065
40758
|
` + render(n.reports, indent + 1) : "";
|
|
@@ -40072,7 +40765,7 @@ function registerTaskWorkflowTools(server, ctx) {
|
|
|
40072
40765
|
if (format === "json") {
|
|
40073
40766
|
return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) }] };
|
|
40074
40767
|
}
|
|
40075
|
-
const
|
|
40768
|
+
const now4 = Date.now();
|
|
40076
40769
|
const ACTIVE_MS = 30 * 60 * 1000;
|
|
40077
40770
|
const text = tree.length > 0 ? render(tree) : "No agents in org chart.";
|
|
40078
40771
|
return { content: [{ type: "text", text }] };
|
|
@@ -40268,14 +40961,14 @@ function registerTaskAutoTools(server, ctx) {
|
|
|
40268
40961
|
return { content: [{ type: "text", text: "No agent_id provided and no agent focus is active." }], isError: true };
|
|
40269
40962
|
}
|
|
40270
40963
|
const assigned = listTasks3({ assigned_to: effectiveAgentId, limit: 500 }, undefined);
|
|
40271
|
-
const
|
|
40272
|
-
const dueSoonCutoff =
|
|
40964
|
+
const now4 = Date.now();
|
|
40965
|
+
const dueSoonCutoff = now4 + 24 * 60 * 60 * 1000;
|
|
40273
40966
|
const blocked = getBlockedTasks2().filter((t) => t.assigned_to === effectiveAgentId);
|
|
40274
40967
|
const workload = {
|
|
40275
40968
|
in_progress: assigned.filter((t) => t.status === "in_progress").length,
|
|
40276
40969
|
pending: assigned.filter((t) => t.status === "pending").length,
|
|
40277
|
-
completed_recent: assigned.filter((t) => t.status === "completed" && t.completed_at &&
|
|
40278
|
-
due_soon: assigned.filter((t) => t.due_at && new Date(t.due_at).getTime() <= dueSoonCutoff && new Date(t.due_at).getTime() >=
|
|
40970
|
+
completed_recent: assigned.filter((t) => t.status === "completed" && t.completed_at && now4 - new Date(t.completed_at).getTime() <= 7 * 24 * 60 * 60 * 1000).length,
|
|
40971
|
+
due_soon: assigned.filter((t) => t.due_at && new Date(t.due_at).getTime() <= dueSoonCutoff && new Date(t.due_at).getTime() >= now4 && !["completed", "cancelled", "failed"].includes(t.status)).length,
|
|
40279
40972
|
blocked: blocked.length
|
|
40280
40973
|
};
|
|
40281
40974
|
const lines = [
|
|
@@ -40502,7 +41195,7 @@ function limits(input) {
|
|
|
40502
41195
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
40503
41196
|
};
|
|
40504
41197
|
}
|
|
40505
|
-
function
|
|
41198
|
+
function truncate2(value, max) {
|
|
40506
41199
|
if (!value)
|
|
40507
41200
|
return value ?? null;
|
|
40508
41201
|
const redacted = redactEvidenceText(value);
|
|
@@ -40523,9 +41216,9 @@ function acceptanceCriteria(task, maxText) {
|
|
|
40523
41216
|
const metadata = task.metadata || {};
|
|
40524
41217
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
40525
41218
|
if (Array.isArray(raw))
|
|
40526
|
-
return raw.map((item) =>
|
|
41219
|
+
return raw.map((item) => truncate2(String(item), maxText)).filter((item) => Boolean(item));
|
|
40527
41220
|
if (typeof raw === "string") {
|
|
40528
|
-
return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) =>
|
|
41221
|
+
return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) => truncate2(line, maxText)).filter((item) => Boolean(item));
|
|
40529
41222
|
}
|
|
40530
41223
|
return [];
|
|
40531
41224
|
}
|
|
@@ -40548,7 +41241,7 @@ function addFile(files, path, source, base) {
|
|
|
40548
41241
|
path,
|
|
40549
41242
|
status: base?.status || "active",
|
|
40550
41243
|
agent_id: base?.agent_id ?? null,
|
|
40551
|
-
note:
|
|
41244
|
+
note: truncate2(base?.note, 240),
|
|
40552
41245
|
updated_at: base?.updated_at || "",
|
|
40553
41246
|
sources: [source]
|
|
40554
41247
|
});
|
|
@@ -40606,7 +41299,7 @@ function estimateTokens(value) {
|
|
|
40606
41299
|
return Math.max(1, Math.ceil((text || "").length / 4));
|
|
40607
41300
|
}
|
|
40608
41301
|
function summarizeStrings(values, maxChars) {
|
|
40609
|
-
return
|
|
41302
|
+
return truncate2(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
|
|
40610
41303
|
}
|
|
40611
41304
|
function summarizeSection(pack, section, maxChars) {
|
|
40612
41305
|
if (section === "project")
|
|
@@ -40797,8 +41490,8 @@ function createAgentContextPack(input, db) {
|
|
|
40797
41490
|
...taskFiles.map((file) => file.updated_at)
|
|
40798
41491
|
], task.updated_at);
|
|
40799
41492
|
const warnings = [];
|
|
40800
|
-
const
|
|
40801
|
-
if (Date.parse(task.updated_at) <
|
|
41493
|
+
const now4 = input.now ? new Date(input.now) : new Date;
|
|
41494
|
+
if (Date.parse(task.updated_at) < now4.getTime() - limit.stale_after_hours * 60 * 60 * 1000) {
|
|
40802
41495
|
warnings.push(`task state is older than ${limit.stale_after_hours} hours`);
|
|
40803
41496
|
}
|
|
40804
41497
|
if (comments.length > recentComments.length)
|
|
@@ -40813,7 +41506,7 @@ function createAgentContextPack(input, db) {
|
|
|
40813
41506
|
id: task.id,
|
|
40814
41507
|
short_id: task.short_id,
|
|
40815
41508
|
title: redactEvidenceText(task.title),
|
|
40816
|
-
description:
|
|
41509
|
+
description: truncate2(task.description, limit.max_text_chars),
|
|
40817
41510
|
status: task.status,
|
|
40818
41511
|
priority: task.priority,
|
|
40819
41512
|
assigned_to: task.assigned_to,
|
|
@@ -40837,7 +41530,7 @@ function createAgentContextPack(input, db) {
|
|
|
40837
41530
|
plan: plan ? {
|
|
40838
41531
|
id: plan.id,
|
|
40839
41532
|
name: plan.name,
|
|
40840
|
-
description:
|
|
41533
|
+
description: truncate2(plan.description, limit.max_text_chars),
|
|
40841
41534
|
status: plan.status,
|
|
40842
41535
|
agent_id: plan.agent_id,
|
|
40843
41536
|
tasks: planTasks.slice(0, limit.plan_task_limit).map(taskSummary).filter((item) => Boolean(item)),
|
|
@@ -40856,7 +41549,7 @@ function createAgentContextPack(input, db) {
|
|
|
40856
41549
|
type: comment.type,
|
|
40857
41550
|
progress_pct: comment.progress_pct,
|
|
40858
41551
|
created_at: comment.created_at,
|
|
40859
|
-
content:
|
|
41552
|
+
content: truncate2(comment.content, limit.max_text_chars) || ""
|
|
40860
41553
|
})),
|
|
40861
41554
|
omitted: Math.max(0, comments.length - recentComments.length)
|
|
40862
41555
|
},
|
|
@@ -40864,7 +41557,7 @@ function createAgentContextPack(input, db) {
|
|
|
40864
41557
|
traceability: {
|
|
40865
41558
|
commits: traceability.commits.map((commit) => ({
|
|
40866
41559
|
sha: commit.sha,
|
|
40867
|
-
message:
|
|
41560
|
+
message: truncate2(commit.message, 240),
|
|
40868
41561
|
files_changed: commit.files_changed,
|
|
40869
41562
|
committed_at: commit.committed_at
|
|
40870
41563
|
})),
|
|
@@ -40872,7 +41565,7 @@ function createAgentContextPack(input, db) {
|
|
|
40872
41565
|
verifications: verifications.map((verification) => ({
|
|
40873
41566
|
command: verification.command,
|
|
40874
41567
|
status: verification.status,
|
|
40875
|
-
output_summary:
|
|
41568
|
+
output_summary: truncate2(verification.output_summary, limit.max_text_chars),
|
|
40876
41569
|
artifact_path: verification.artifact_path,
|
|
40877
41570
|
run_at: verification.run_at
|
|
40878
41571
|
})),
|
|
@@ -40883,14 +41576,14 @@ function createAgentContextPack(input, db) {
|
|
|
40883
41576
|
id: ledger.run.id,
|
|
40884
41577
|
title: ledger.run.title,
|
|
40885
41578
|
status: ledger.run.status,
|
|
40886
|
-
summary:
|
|
41579
|
+
summary: truncate2(ledger.run.summary, limit.max_text_chars),
|
|
40887
41580
|
agent_id: ledger.run.agent_id,
|
|
40888
41581
|
started_at: ledger.run.started_at,
|
|
40889
41582
|
completed_at: ledger.run.completed_at,
|
|
40890
|
-
events: ledger.events.map((event) => ({ event_type: event.event_type, message:
|
|
40891
|
-
commands: ledger.commands.map((command) => ({ command: command.command, status: command.status, output_summary:
|
|
40892
|
-
files: ledger.files.map((file) => ({ path: file.path, status: file.status, note:
|
|
40893
|
-
artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description:
|
|
41583
|
+
events: ledger.events.map((event) => ({ event_type: event.event_type, message: truncate2(event.message, 500), created_at: event.created_at })),
|
|
41584
|
+
commands: ledger.commands.map((command) => ({ command: command.command, status: command.status, output_summary: truncate2(command.output_summary, limit.max_text_chars), artifact_path: command.artifact_path })),
|
|
41585
|
+
files: ledger.files.map((file) => ({ path: file.path, status: file.status, note: truncate2(file.note, 240) })),
|
|
41586
|
+
artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description: truncate2(artifact.description, 240), sha256: artifact.sha256 }))
|
|
40894
41587
|
})),
|
|
40895
41588
|
omitted: Math.max(0, runs.length - selectedRuns.length)
|
|
40896
41589
|
},
|
|
@@ -40995,7 +41688,7 @@ function renderAgentContextPackCompactMarkdown(pack) {
|
|
|
40995
41688
|
const lines = [
|
|
40996
41689
|
`# Context: ${pack.task.title}`,
|
|
40997
41690
|
`${pack.task.status} | ${pack.task.priority} | ${pack.task.short_id || pack.task.id.slice(0, 8)}`,
|
|
40998
|
-
pack.task.description ?
|
|
41691
|
+
pack.task.description ? truncate2(pack.task.description, Math.min(pack.limits.summary_char_limit, 700)) : null,
|
|
40999
41692
|
"",
|
|
41000
41693
|
"## Must Know",
|
|
41001
41694
|
bullet([
|
|
@@ -56983,7 +57676,7 @@ var require_to_json_schema = __commonJS((exports) => {
|
|
|
56983
57676
|
if (val === undefined) {
|
|
56984
57677
|
if (this.unrepresentable === "throw") {
|
|
56985
57678
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
56986
|
-
}
|
|
57679
|
+
} else {}
|
|
56987
57680
|
} else if (typeof val === "bigint") {
|
|
56988
57681
|
if (this.unrepresentable === "throw") {
|
|
56989
57682
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -66019,7 +66712,7 @@ var init_agent_run_dispatcher = __esm(() => {
|
|
|
66019
66712
|
});
|
|
66020
66713
|
|
|
66021
66714
|
// src/lib/verification-providers.ts
|
|
66022
|
-
import { existsSync as
|
|
66715
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
|
|
66023
66716
|
function normalizeName5(name) {
|
|
66024
66717
|
const normalized = name.trim().toLowerCase();
|
|
66025
66718
|
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
|
|
@@ -66171,7 +66864,7 @@ Timed out after ${provider.timeout_ms}ms`);
|
|
|
66171
66864
|
};
|
|
66172
66865
|
}
|
|
66173
66866
|
function runCiLogProvider(input) {
|
|
66174
|
-
const text = input.log_text ?? (input.log_path &&
|
|
66867
|
+
const text = input.log_text ?? (input.log_path && existsSync12(input.log_path) ? readFileSync7(input.log_path, "utf-8") : "");
|
|
66175
66868
|
return {
|
|
66176
66869
|
status: classifyLog(text),
|
|
66177
66870
|
attempts: 1,
|
|
@@ -66183,7 +66876,7 @@ function runBrowserProvider(input) {
|
|
|
66183
66876
|
if (!input.artifact_path) {
|
|
66184
66877
|
return { status: "unknown", attempts: 1, exit_code: null, output_summary: "browser provider needs a screenshot or artifact path" };
|
|
66185
66878
|
}
|
|
66186
|
-
if (!
|
|
66879
|
+
if (!existsSync12(input.artifact_path)) {
|
|
66187
66880
|
return { status: "failed", attempts: 1, exit_code: null, output_summary: `artifact not found: ${input.artifact_path}` };
|
|
66188
66881
|
}
|
|
66189
66882
|
return {
|
|
@@ -67973,12 +68666,12 @@ function summarizeTask(task) {
|
|
|
67973
68666
|
};
|
|
67974
68667
|
}
|
|
67975
68668
|
function overdueTasks(tasks, nowIso) {
|
|
67976
|
-
const
|
|
68669
|
+
const now4 = Date.parse(nowIso);
|
|
67977
68670
|
return tasks.filter((task) => {
|
|
67978
68671
|
if (isTerminal2(task) || !task.due_at)
|
|
67979
68672
|
return false;
|
|
67980
68673
|
const due = Date.parse(task.due_at);
|
|
67981
|
-
return Number.isFinite(due) && due <
|
|
68674
|
+
return Number.isFinite(due) && due < now4;
|
|
67982
68675
|
});
|
|
67983
68676
|
}
|
|
67984
68677
|
function isReady(task, db) {
|
|
@@ -70403,8 +71096,8 @@ __export(exports_local_extensions, {
|
|
|
70403
71096
|
discoverLocalExtensions: () => discoverLocalExtensions
|
|
70404
71097
|
});
|
|
70405
71098
|
import { createHash as createHash9, createVerify } from "crypto";
|
|
70406
|
-
import { existsSync as
|
|
70407
|
-
import { basename as basename5, join as
|
|
71099
|
+
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
|
|
71100
|
+
import { basename as basename5, join as join11, resolve as resolve13 } from "path";
|
|
70408
71101
|
function isObject3(value) {
|
|
70409
71102
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
70410
71103
|
}
|
|
@@ -70663,10 +71356,10 @@ function verifyExtensionSignature(input) {
|
|
|
70663
71356
|
}
|
|
70664
71357
|
function inspectExtensionSource(source3) {
|
|
70665
71358
|
const resolved = resolve13(source3);
|
|
70666
|
-
if (!
|
|
71359
|
+
if (!existsSync13(resolved))
|
|
70667
71360
|
throw new Error(`extension source not found: ${source3}`);
|
|
70668
71361
|
const stat = statSync6(resolved);
|
|
70669
|
-
const manifestPath = stat.isDirectory() ? [
|
|
71362
|
+
const manifestPath = stat.isDirectory() ? [join11(resolved, "todos.extension.json"), join11(resolved, "extension.json")].find(existsSync13) : resolved;
|
|
70670
71363
|
if (!manifestPath)
|
|
70671
71364
|
throw new Error(`extension directory ${source3} is missing todos.extension.json`);
|
|
70672
71365
|
const raw = readFileSync9(manifestPath);
|
|
@@ -70762,20 +71455,20 @@ function projectExtensionSources(projectPath) {
|
|
|
70762
71455
|
return [];
|
|
70763
71456
|
const root = resolve13(projectPath);
|
|
70764
71457
|
const candidates = [
|
|
70765
|
-
|
|
70766
|
-
|
|
71458
|
+
join11(root, "todos.extension.json"),
|
|
71459
|
+
join11(root, ".todos", "todos.extension.json")
|
|
70767
71460
|
];
|
|
70768
|
-
const extensionDir =
|
|
70769
|
-
if (
|
|
71461
|
+
const extensionDir = join11(root, ".todos", "extensions");
|
|
71462
|
+
if (existsSync13(extensionDir)) {
|
|
70770
71463
|
for (const entry of readdirSync3(extensionDir)) {
|
|
70771
71464
|
if (entry.startsWith("."))
|
|
70772
71465
|
continue;
|
|
70773
|
-
const full =
|
|
71466
|
+
const full = join11(extensionDir, entry);
|
|
70774
71467
|
if (statSync6(full).isDirectory() || entry.endsWith(".json"))
|
|
70775
71468
|
candidates.push(full);
|
|
70776
71469
|
}
|
|
70777
71470
|
}
|
|
70778
|
-
return candidates.filter(
|
|
71471
|
+
return candidates.filter(existsSync13);
|
|
70779
71472
|
}
|
|
70780
71473
|
function discoverLocalExtensions(options = {}) {
|
|
70781
71474
|
const config2 = loadConfig();
|
|
@@ -74297,7 +74990,7 @@ ${lines.join(`
|
|
|
74297
74990
|
const projectRoles = n.project_roles.length > 0 ? ` <${n.project_roles.join(", ")}>` : "";
|
|
74298
74991
|
const lead = n.is_project_lead ? " \u2605" : "";
|
|
74299
74992
|
const lastSeen = new Date(n.agent.last_seen_at).getTime();
|
|
74300
|
-
const active =
|
|
74993
|
+
const active = now4 - lastSeen < ACTIVE_MS ? " \u25CF" : " \u25CB";
|
|
74301
74994
|
const line = `${prefix}${active} ${n.agent.name}${title}${globalRole}${projectRoles}${lead}`;
|
|
74302
74995
|
const children = n.reports.length > 0 ? `
|
|
74303
74996
|
` + render(n.reports, indent + 1) : "";
|
|
@@ -74311,7 +75004,7 @@ ${lines.join(`
|
|
|
74311
75004
|
if (format === "json") {
|
|
74312
75005
|
return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) }] };
|
|
74313
75006
|
}
|
|
74314
|
-
const
|
|
75007
|
+
const now4 = Date.now();
|
|
74315
75008
|
const ACTIVE_MS = 30 * 60 * 1000;
|
|
74316
75009
|
const text2 = tree.length > 0 ? render(tree) : "No agents in this project's org chart.";
|
|
74317
75010
|
return { content: [{ type: "text", text: text2 }] };
|
|
@@ -74861,10 +75554,10 @@ ${lines.join(`
|
|
|
74861
75554
|
});
|
|
74862
75555
|
}
|
|
74863
75556
|
if (shouldRegisterTool("get_idle_focus_prompts")) {
|
|
74864
|
-
server.tool("get_idle_focus_prompts", "Return local idle prompts for active focus sessions.", { agent_id: exports_external.string().optional(), now: exports_external.string().optional() }, async ({ agent_id, now:
|
|
75557
|
+
server.tool("get_idle_focus_prompts", "Return local idle prompts for active focus sessions.", { agent_id: exports_external.string().optional(), now: exports_external.string().optional() }, async ({ agent_id, now: now4 }) => {
|
|
74865
75558
|
try {
|
|
74866
75559
|
const { getIdleFocusSessionPrompts: getIdleFocusSessionPrompts2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
74867
|
-
return { content: [{ type: "text", text: JSON.stringify(getIdleFocusSessionPrompts2({ agent_id, now:
|
|
75560
|
+
return { content: [{ type: "text", text: JSON.stringify(getIdleFocusSessionPrompts2({ agent_id, now: now4 }), null, 2) }] };
|
|
74868
75561
|
} catch (e) {
|
|
74869
75562
|
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74870
75563
|
}
|
|
@@ -75059,9 +75752,9 @@ __export(exports_extract, {
|
|
|
75059
75752
|
buildCodebaseIndex: () => buildCodebaseIndex,
|
|
75060
75753
|
EXTRACT_TAGS: () => EXTRACT_TAGS
|
|
75061
75754
|
});
|
|
75062
|
-
import { existsSync as
|
|
75755
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
|
|
75063
75756
|
import { createHash as createHash11 } from "crypto";
|
|
75064
|
-
import { relative as relative5, resolve as resolve14, join as
|
|
75757
|
+
import { relative as relative5, resolve as resolve14, join as join12 } from "path";
|
|
75065
75758
|
function stableHash(value) {
|
|
75066
75759
|
return createHash11("sha256").update(value).digest("hex");
|
|
75067
75760
|
}
|
|
@@ -75070,8 +75763,8 @@ function normalizePathForMatch(value) {
|
|
|
75070
75763
|
}
|
|
75071
75764
|
function readGitignorePatterns(basePath) {
|
|
75072
75765
|
const root = statSync7(basePath).isFile() ? resolve14(basePath, "..") : basePath;
|
|
75073
|
-
const gitignorePath =
|
|
75074
|
-
if (!
|
|
75766
|
+
const gitignorePath = join12(root, ".gitignore");
|
|
75767
|
+
if (!existsSync14(gitignorePath))
|
|
75075
75768
|
return [];
|
|
75076
75769
|
try {
|
|
75077
75770
|
return readFileSync10(gitignorePath, "utf-8").split(`
|
|
@@ -75213,7 +75906,7 @@ function buildCodebaseIndex(options) {
|
|
|
75213
75906
|
const files = collectFiles(basePath, extensions, excludes, respectGitignore);
|
|
75214
75907
|
const indexed = [];
|
|
75215
75908
|
for (const file of files) {
|
|
75216
|
-
const fullPath = statSync7(basePath).isFile() ? basePath :
|
|
75909
|
+
const fullPath = statSync7(basePath).isFile() ? basePath : join12(basePath, file);
|
|
75217
75910
|
try {
|
|
75218
75911
|
const source3 = readFileSync10(fullPath, "utf-8");
|
|
75219
75912
|
const relPath = statSync7(basePath).isFile() ? relative5(resolve14(basePath, ".."), fullPath) : file;
|
|
@@ -75244,7 +75937,7 @@ function extractTodos(options, db) {
|
|
|
75244
75937
|
const files = collectFiles(basePath, extensions, excludes, respectGitignore);
|
|
75245
75938
|
const allComments = [];
|
|
75246
75939
|
for (const file of files) {
|
|
75247
|
-
const fullPath = statSync7(basePath).isFile() ? basePath :
|
|
75940
|
+
const fullPath = statSync7(basePath).isFile() ? basePath : join12(basePath, file);
|
|
75248
75941
|
try {
|
|
75249
75942
|
const source3 = readFileSync10(fullPath, "utf-8");
|
|
75250
75943
|
const relPath = statSync7(basePath).isFile() ? relative5(resolve14(basePath, ".."), fullPath) : file;
|
|
@@ -76137,7 +76830,7 @@ __export(exports_builtin_templates, {
|
|
|
76137
76830
|
BUILTIN_TEMPLATES: () => BUILTIN_TEMPLATES
|
|
76138
76831
|
});
|
|
76139
76832
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
76140
|
-
import { join as
|
|
76833
|
+
import { join as join13 } from "path";
|
|
76141
76834
|
function templateMetadata(template) {
|
|
76142
76835
|
return {
|
|
76143
76836
|
source: BUILTIN_TEMPLATE_LIBRARY_SOURCE,
|
|
@@ -76196,7 +76889,7 @@ function writeBuiltinTemplateFiles(directory) {
|
|
|
76196
76889
|
mkdirSync7(directory, { recursive: true });
|
|
76197
76890
|
const files = [];
|
|
76198
76891
|
for (const entry of exportBuiltinTemplateFiles()) {
|
|
76199
|
-
const path =
|
|
76892
|
+
const path = join13(directory, entry.filename);
|
|
76200
76893
|
writeFileSync4(path, `${JSON.stringify(entry.template, null, 2)}
|
|
76201
76894
|
`, "utf-8");
|
|
76202
76895
|
files.push(path);
|
|
@@ -76722,16 +77415,16 @@ __export(exports_environment_snapshots, {
|
|
|
76722
77415
|
captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
|
|
76723
77416
|
});
|
|
76724
77417
|
import { createHash as createHash12 } from "crypto";
|
|
76725
|
-
import { existsSync as
|
|
77418
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
|
|
76726
77419
|
import { hostname as hostname3, platform, arch } from "os";
|
|
76727
|
-
import { dirname as dirname9, join as
|
|
77420
|
+
import { dirname as dirname9, join as join14, resolve as resolve15 } from "path";
|
|
76728
77421
|
import { tmpdir as tmpdir2 } from "os";
|
|
76729
77422
|
function sha2566(value) {
|
|
76730
77423
|
return createHash12("sha256").update(value).digest("hex");
|
|
76731
77424
|
}
|
|
76732
77425
|
function fileRecord(root, relativePath) {
|
|
76733
|
-
const path =
|
|
76734
|
-
if (!
|
|
77426
|
+
const path = join14(root, relativePath);
|
|
77427
|
+
if (!existsSync15(path))
|
|
76735
77428
|
return null;
|
|
76736
77429
|
const stat = statSync8(path);
|
|
76737
77430
|
if (!stat.isFile())
|
|
@@ -76743,7 +77436,7 @@ function manifestRecord(root, relativePath) {
|
|
|
76743
77436
|
const base = fileRecord(root, relativePath);
|
|
76744
77437
|
if (!base)
|
|
76745
77438
|
return null;
|
|
76746
|
-
const parsed = readJsonFile(
|
|
77439
|
+
const parsed = readJsonFile(join14(root, relativePath));
|
|
76747
77440
|
if (!parsed)
|
|
76748
77441
|
return { ...base, redacted: {} };
|
|
76749
77442
|
const redacted = redactValue({
|
|
@@ -76838,8 +77531,8 @@ function commandEnv(env, includeValues) {
|
|
|
76838
77531
|
function defaultSnapshotDir() {
|
|
76839
77532
|
const dbPath = getDatabasePath();
|
|
76840
77533
|
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
76841
|
-
return
|
|
76842
|
-
return
|
|
77534
|
+
return join14(tmpdir2(), "hasna-todos", "environment-snapshots");
|
|
77535
|
+
return join14(dirname9(resolve15(dbPath)), "environment-snapshots");
|
|
76843
77536
|
}
|
|
76844
77537
|
function snapshotWithId(snapshot) {
|
|
76845
77538
|
const digest = sha2566(JSON.stringify(snapshot)).slice(0, 24);
|
|
@@ -76886,7 +77579,7 @@ function captureEnvironmentSnapshot(input = {}) {
|
|
|
76886
77579
|
});
|
|
76887
77580
|
}
|
|
76888
77581
|
function writeEnvironmentSnapshot(snapshot, outputPath) {
|
|
76889
|
-
const path = outputPath ? resolve15(outputPath) :
|
|
77582
|
+
const path = outputPath ? resolve15(outputPath) : join14(defaultSnapshotDir(), `${snapshot.id}.json`);
|
|
76890
77583
|
ensureDir2(dirname9(path));
|
|
76891
77584
|
writeJsonFile(path, snapshot);
|
|
76892
77585
|
return path;
|
|
@@ -77532,27 +78225,27 @@ __export(exports_serve, {
|
|
|
77532
78225
|
SECURITY_HEADERS: () => SECURITY_HEADERS,
|
|
77533
78226
|
MIME_TYPES: () => MIME_TYPES
|
|
77534
78227
|
});
|
|
77535
|
-
import { existsSync as
|
|
77536
|
-
import { join as
|
|
78228
|
+
import { existsSync as existsSync16 } from "fs";
|
|
78229
|
+
import { join as join15, dirname as dirname10, extname } from "path";
|
|
77537
78230
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
77538
78231
|
function resolveDashboardDir() {
|
|
77539
78232
|
const candidates = [];
|
|
77540
78233
|
try {
|
|
77541
78234
|
const scriptDir = dirname10(fileURLToPath2(import.meta.url));
|
|
77542
|
-
candidates.push(
|
|
77543
|
-
candidates.push(
|
|
78235
|
+
candidates.push(join15(scriptDir, "..", "dashboard", "dist"));
|
|
78236
|
+
candidates.push(join15(scriptDir, "..", "..", "dashboard", "dist"));
|
|
77544
78237
|
} catch {}
|
|
77545
78238
|
if (process.argv[1]) {
|
|
77546
78239
|
const mainDir = dirname10(process.argv[1]);
|
|
77547
|
-
candidates.push(
|
|
77548
|
-
candidates.push(
|
|
78240
|
+
candidates.push(join15(mainDir, "..", "dashboard", "dist"));
|
|
78241
|
+
candidates.push(join15(mainDir, "..", "..", "dashboard", "dist"));
|
|
77549
78242
|
}
|
|
77550
|
-
candidates.push(
|
|
78243
|
+
candidates.push(join15(process.cwd(), "dashboard", "dist"));
|
|
77551
78244
|
for (const candidate of candidates) {
|
|
77552
|
-
if (
|
|
78245
|
+
if (existsSync16(candidate))
|
|
77553
78246
|
return candidate;
|
|
77554
78247
|
}
|
|
77555
|
-
return
|
|
78248
|
+
return join15(process.cwd(), "dashboard", "dist");
|
|
77556
78249
|
}
|
|
77557
78250
|
function getProvidedApiKey(req) {
|
|
77558
78251
|
const headerKey = req.headers.get("x-api-key");
|
|
@@ -77579,15 +78272,15 @@ function checkAuth(req, apiKey) {
|
|
|
77579
78272
|
return null;
|
|
77580
78273
|
}
|
|
77581
78274
|
function checkRateLimit(ip) {
|
|
77582
|
-
const
|
|
78275
|
+
const now4 = Date.now();
|
|
77583
78276
|
const entry = rateLimitMap.get(ip);
|
|
77584
|
-
if (!entry ||
|
|
77585
|
-
rateLimitMap.set(ip, { count: 1, resetAt:
|
|
78277
|
+
if (!entry || now4 > entry.resetAt) {
|
|
78278
|
+
rateLimitMap.set(ip, { count: 1, resetAt: now4 + RATE_LIMIT_WINDOW_MS });
|
|
77586
78279
|
return { allowed: true };
|
|
77587
78280
|
}
|
|
77588
78281
|
entry.count++;
|
|
77589
78282
|
if (entry.count > RATE_LIMIT_MAX) {
|
|
77590
|
-
return { allowed: false, retryAfter: Math.ceil((entry.resetAt -
|
|
78283
|
+
return { allowed: false, retryAfter: Math.ceil((entry.resetAt - now4) / 1000) };
|
|
77591
78284
|
}
|
|
77592
78285
|
return { allowed: true };
|
|
77593
78286
|
}
|
|
@@ -77602,7 +78295,7 @@ function json(data, status = 200, headers) {
|
|
|
77602
78295
|
});
|
|
77603
78296
|
}
|
|
77604
78297
|
function serveStaticFile(filePath) {
|
|
77605
|
-
if (!
|
|
78298
|
+
if (!existsSync16(filePath))
|
|
77606
78299
|
return null;
|
|
77607
78300
|
const ext = extname(filePath);
|
|
77608
78301
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
@@ -77681,7 +78374,7 @@ data: ${data}
|
|
|
77681
78374
|
filteredSseClients.delete(client);
|
|
77682
78375
|
}
|
|
77683
78376
|
const dashboardDir = resolveDashboardDir();
|
|
77684
|
-
const dashboardExists =
|
|
78377
|
+
const dashboardExists = existsSync16(dashboardDir);
|
|
77685
78378
|
if (!dashboardExists) {
|
|
77686
78379
|
console.error(`
|
|
77687
78380
|
Dashboard not found at: ${dashboardDir}`);
|