@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/mcp/index.js
CHANGED
|
@@ -8628,6 +8628,7 @@ var init_event_hooks = __esm(() => {
|
|
|
8628
8628
|
init_runner_sandbox();
|
|
8629
8629
|
init_config();
|
|
8630
8630
|
LOCAL_EVENT_TYPES = [
|
|
8631
|
+
"task.created",
|
|
8631
8632
|
"task.assigned",
|
|
8632
8633
|
"task.blocked",
|
|
8633
8634
|
"task.started",
|
|
@@ -8652,6 +8653,572 @@ var init_event_hooks = __esm(() => {
|
|
|
8652
8653
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
8653
8654
|
});
|
|
8654
8655
|
|
|
8656
|
+
// node_modules/.bun/@hasna+events@0.1.7/node_modules/@hasna/events/dist/index.js
|
|
8657
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
8658
|
+
import { existsSync as existsSync5 } from "fs";
|
|
8659
|
+
import { homedir } from "os";
|
|
8660
|
+
import { join as join4 } from "path";
|
|
8661
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
8662
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
8663
|
+
import { spawn } from "child_process";
|
|
8664
|
+
import { randomUUID as randomUUID22 } from "crypto";
|
|
8665
|
+
function getPathValue(input, path) {
|
|
8666
|
+
return path.split(".").reduce((value, part) => {
|
|
8667
|
+
if (value && typeof value === "object" && part in value) {
|
|
8668
|
+
return value[part];
|
|
8669
|
+
}
|
|
8670
|
+
return;
|
|
8671
|
+
}, input);
|
|
8672
|
+
}
|
|
8673
|
+
function wildcardToRegExp(pattern) {
|
|
8674
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
8675
|
+
return new RegExp(`^${escaped}$`);
|
|
8676
|
+
}
|
|
8677
|
+
function matchString(value, matcher) {
|
|
8678
|
+
if (matcher === undefined)
|
|
8679
|
+
return true;
|
|
8680
|
+
if (value === undefined)
|
|
8681
|
+
return false;
|
|
8682
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
8683
|
+
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
8684
|
+
}
|
|
8685
|
+
function matchRecord(input, matcher) {
|
|
8686
|
+
if (!matcher)
|
|
8687
|
+
return true;
|
|
8688
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
8689
|
+
const actual = getPathValue(input, path);
|
|
8690
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
8691
|
+
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
8692
|
+
}
|
|
8693
|
+
return actual === expected;
|
|
8694
|
+
});
|
|
8695
|
+
}
|
|
8696
|
+
function eventMatchesFilter(event, filter) {
|
|
8697
|
+
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);
|
|
8698
|
+
}
|
|
8699
|
+
function channelMatchesEvent(channel, event) {
|
|
8700
|
+
if (!channel.enabled)
|
|
8701
|
+
return false;
|
|
8702
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
8703
|
+
return true;
|
|
8704
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
8705
|
+
}
|
|
8706
|
+
function getEventsDataDir(override) {
|
|
8707
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir(), ".hasna", "events");
|
|
8708
|
+
}
|
|
8709
|
+
|
|
8710
|
+
class JsonEventsStore {
|
|
8711
|
+
dataDir;
|
|
8712
|
+
channelsPath;
|
|
8713
|
+
eventsPath;
|
|
8714
|
+
deliveriesPath;
|
|
8715
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
8716
|
+
this.dataDir = dataDir;
|
|
8717
|
+
this.channelsPath = join4(dataDir, "channels.json");
|
|
8718
|
+
this.eventsPath = join4(dataDir, "events.json");
|
|
8719
|
+
this.deliveriesPath = join4(dataDir, "deliveries.json");
|
|
8720
|
+
}
|
|
8721
|
+
async init() {
|
|
8722
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
8723
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
8724
|
+
return;
|
|
8725
|
+
});
|
|
8726
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
8727
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
8728
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
8729
|
+
}
|
|
8730
|
+
async addChannel(channel) {
|
|
8731
|
+
await this.init();
|
|
8732
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
8733
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
8734
|
+
if (index >= 0) {
|
|
8735
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
8736
|
+
} else {
|
|
8737
|
+
channels.push(channel);
|
|
8738
|
+
}
|
|
8739
|
+
await this.writeJson(this.channelsPath, channels);
|
|
8740
|
+
return index >= 0 ? channels[index] : channel;
|
|
8741
|
+
}
|
|
8742
|
+
async listChannels() {
|
|
8743
|
+
await this.init();
|
|
8744
|
+
return this.readJson(this.channelsPath, []);
|
|
8745
|
+
}
|
|
8746
|
+
async getChannel(id) {
|
|
8747
|
+
const channels = await this.listChannels();
|
|
8748
|
+
return channels.find((channel) => channel.id === id);
|
|
8749
|
+
}
|
|
8750
|
+
async removeChannel(id) {
|
|
8751
|
+
await this.init();
|
|
8752
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
8753
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
8754
|
+
await this.writeJson(this.channelsPath, next);
|
|
8755
|
+
return next.length !== channels.length;
|
|
8756
|
+
}
|
|
8757
|
+
async appendEvent(event) {
|
|
8758
|
+
await this.init();
|
|
8759
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
8760
|
+
events.push(event);
|
|
8761
|
+
await this.writeJson(this.eventsPath, events);
|
|
8762
|
+
return event;
|
|
8763
|
+
}
|
|
8764
|
+
async listEvents() {
|
|
8765
|
+
await this.init();
|
|
8766
|
+
return this.readJson(this.eventsPath, []);
|
|
8767
|
+
}
|
|
8768
|
+
async findEventByIdentity(identity) {
|
|
8769
|
+
const events = await this.listEvents();
|
|
8770
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
8771
|
+
}
|
|
8772
|
+
async appendDelivery(result) {
|
|
8773
|
+
await this.init();
|
|
8774
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
8775
|
+
deliveries.push(result);
|
|
8776
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
8777
|
+
return result;
|
|
8778
|
+
}
|
|
8779
|
+
async listDeliveries() {
|
|
8780
|
+
await this.init();
|
|
8781
|
+
return this.readJson(this.deliveriesPath, []);
|
|
8782
|
+
}
|
|
8783
|
+
async exportData() {
|
|
8784
|
+
return {
|
|
8785
|
+
channels: await this.listChannels(),
|
|
8786
|
+
events: await this.listEvents(),
|
|
8787
|
+
deliveries: await this.listDeliveries()
|
|
8788
|
+
};
|
|
8789
|
+
}
|
|
8790
|
+
async ensureArrayFile(path) {
|
|
8791
|
+
if (!existsSync5(path)) {
|
|
8792
|
+
await writeFile(path, `[]
|
|
8793
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
8794
|
+
}
|
|
8795
|
+
await chmod(path, 384).catch(() => {
|
|
8796
|
+
return;
|
|
8797
|
+
});
|
|
8798
|
+
}
|
|
8799
|
+
async readJson(path, fallback) {
|
|
8800
|
+
try {
|
|
8801
|
+
const raw = await readFile(path, "utf-8");
|
|
8802
|
+
if (!raw.trim())
|
|
8803
|
+
return fallback;
|
|
8804
|
+
return JSON.parse(raw);
|
|
8805
|
+
} catch (error) {
|
|
8806
|
+
if (error.code === "ENOENT")
|
|
8807
|
+
return fallback;
|
|
8808
|
+
throw error;
|
|
8809
|
+
}
|
|
8810
|
+
}
|
|
8811
|
+
async writeJson(path, value) {
|
|
8812
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
8813
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
8814
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
8815
|
+
await rename(tempPath, path);
|
|
8816
|
+
await chmod(path, 384).catch(() => {
|
|
8817
|
+
return;
|
|
8818
|
+
});
|
|
8819
|
+
}
|
|
8820
|
+
}
|
|
8821
|
+
function buildSignatureBase(timestamp, body) {
|
|
8822
|
+
return `${timestamp}.${body}`;
|
|
8823
|
+
}
|
|
8824
|
+
function signPayload(secret, timestamp, body) {
|
|
8825
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
8826
|
+
return `sha256=${digest}`;
|
|
8827
|
+
}
|
|
8828
|
+
function now2() {
|
|
8829
|
+
return new Date().toISOString();
|
|
8830
|
+
}
|
|
8831
|
+
function truncate(value, max = 4096) {
|
|
8832
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
8833
|
+
}
|
|
8834
|
+
function buildWebhookRequest(event, channel) {
|
|
8835
|
+
if (!channel.webhook)
|
|
8836
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
8837
|
+
const body = JSON.stringify(event);
|
|
8838
|
+
const timestamp = event.time;
|
|
8839
|
+
const headers = {
|
|
8840
|
+
"Content-Type": "application/json",
|
|
8841
|
+
"User-Agent": "@hasna/events",
|
|
8842
|
+
"X-Hasna-Event-Id": event.id,
|
|
8843
|
+
"X-Hasna-Event-Type": event.type,
|
|
8844
|
+
"X-Hasna-Timestamp": timestamp,
|
|
8845
|
+
...channel.webhook.headers
|
|
8846
|
+
};
|
|
8847
|
+
if (channel.webhook.secret) {
|
|
8848
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
8849
|
+
}
|
|
8850
|
+
return { body, headers };
|
|
8851
|
+
}
|
|
8852
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
8853
|
+
if (!channel.webhook)
|
|
8854
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
8855
|
+
const startedAt = now2();
|
|
8856
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
8857
|
+
const controller = new AbortController;
|
|
8858
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
8859
|
+
try {
|
|
8860
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
8861
|
+
method: "POST",
|
|
8862
|
+
headers,
|
|
8863
|
+
body,
|
|
8864
|
+
signal: controller.signal
|
|
8865
|
+
});
|
|
8866
|
+
const responseBody = truncate(await response.text());
|
|
8867
|
+
return {
|
|
8868
|
+
attempt: 1,
|
|
8869
|
+
status: response.ok ? "success" : "failed",
|
|
8870
|
+
startedAt,
|
|
8871
|
+
completedAt: now2(),
|
|
8872
|
+
responseStatus: response.status,
|
|
8873
|
+
responseBody,
|
|
8874
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
8875
|
+
};
|
|
8876
|
+
} catch (error) {
|
|
8877
|
+
return {
|
|
8878
|
+
attempt: 1,
|
|
8879
|
+
status: "failed",
|
|
8880
|
+
startedAt,
|
|
8881
|
+
completedAt: now2(),
|
|
8882
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8883
|
+
};
|
|
8884
|
+
} finally {
|
|
8885
|
+
clearTimeout(timeout);
|
|
8886
|
+
}
|
|
8887
|
+
}
|
|
8888
|
+
async function dispatchCommand(event, channel) {
|
|
8889
|
+
if (!channel.command)
|
|
8890
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
8891
|
+
const startedAt = now2();
|
|
8892
|
+
const eventJson = JSON.stringify(event);
|
|
8893
|
+
const env = {
|
|
8894
|
+
...process.env,
|
|
8895
|
+
...channel.command.env,
|
|
8896
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
8897
|
+
HASNA_EVENT_ID: event.id,
|
|
8898
|
+
HASNA_EVENT_TYPE: event.type,
|
|
8899
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
8900
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
8901
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
8902
|
+
HASNA_EVENT_TIME: event.time,
|
|
8903
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
8904
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
8905
|
+
HASNA_EVENT_JSON: eventJson
|
|
8906
|
+
};
|
|
8907
|
+
return new Promise((resolve6) => {
|
|
8908
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
8909
|
+
cwd: channel.command.cwd,
|
|
8910
|
+
env,
|
|
8911
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
8912
|
+
});
|
|
8913
|
+
let stdout = "";
|
|
8914
|
+
let stderr = "";
|
|
8915
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
8916
|
+
child.stdin.end(eventJson);
|
|
8917
|
+
child.stdout.on("data", (chunk) => {
|
|
8918
|
+
stdout += chunk.toString();
|
|
8919
|
+
});
|
|
8920
|
+
child.stderr.on("data", (chunk) => {
|
|
8921
|
+
stderr += chunk.toString();
|
|
8922
|
+
});
|
|
8923
|
+
child.on("error", (error) => {
|
|
8924
|
+
clearTimeout(timeout);
|
|
8925
|
+
resolve6({
|
|
8926
|
+
attempt: 1,
|
|
8927
|
+
status: "failed",
|
|
8928
|
+
startedAt,
|
|
8929
|
+
completedAt: now2(),
|
|
8930
|
+
stdout: truncate(stdout),
|
|
8931
|
+
stderr: truncate(stderr),
|
|
8932
|
+
error: error.message
|
|
8933
|
+
});
|
|
8934
|
+
});
|
|
8935
|
+
child.on("close", (code, signal) => {
|
|
8936
|
+
clearTimeout(timeout);
|
|
8937
|
+
const success = code === 0;
|
|
8938
|
+
resolve6({
|
|
8939
|
+
attempt: 1,
|
|
8940
|
+
status: success ? "success" : "failed",
|
|
8941
|
+
startedAt,
|
|
8942
|
+
completedAt: now2(),
|
|
8943
|
+
stdout: truncate(stdout),
|
|
8944
|
+
stderr: truncate(stderr),
|
|
8945
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
8946
|
+
});
|
|
8947
|
+
});
|
|
8948
|
+
});
|
|
8949
|
+
}
|
|
8950
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
8951
|
+
if (channel.transport === "webhook")
|
|
8952
|
+
return dispatchWebhook(event, channel, options);
|
|
8953
|
+
if (channel.transport === "command")
|
|
8954
|
+
return dispatchCommand(event, channel);
|
|
8955
|
+
return {
|
|
8956
|
+
attempt: 1,
|
|
8957
|
+
status: "skipped",
|
|
8958
|
+
startedAt: now2(),
|
|
8959
|
+
completedAt: now2(),
|
|
8960
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
8961
|
+
};
|
|
8962
|
+
}
|
|
8963
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
8964
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
8965
|
+
return {
|
|
8966
|
+
id: randomUUID2(),
|
|
8967
|
+
eventId: event.id,
|
|
8968
|
+
channelId: channel.id,
|
|
8969
|
+
transport: channel.transport,
|
|
8970
|
+
status,
|
|
8971
|
+
attempts,
|
|
8972
|
+
createdAt: attempts[0]?.startedAt ?? now2(),
|
|
8973
|
+
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
8974
|
+
};
|
|
8975
|
+
}
|
|
8976
|
+
function createEvent(input) {
|
|
8977
|
+
return {
|
|
8978
|
+
id: input.id ?? randomUUID22(),
|
|
8979
|
+
source: input.source,
|
|
8980
|
+
type: input.type,
|
|
8981
|
+
time: normalizeTime(input.time),
|
|
8982
|
+
subject: input.subject,
|
|
8983
|
+
severity: input.severity ?? "info",
|
|
8984
|
+
data: input.data ?? {},
|
|
8985
|
+
message: input.message,
|
|
8986
|
+
dedupeKey: input.dedupeKey,
|
|
8987
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
8988
|
+
metadata: input.metadata ?? {}
|
|
8989
|
+
};
|
|
8990
|
+
}
|
|
8991
|
+
|
|
8992
|
+
class EventsClient {
|
|
8993
|
+
store;
|
|
8994
|
+
redactors;
|
|
8995
|
+
transportOptions;
|
|
8996
|
+
constructor(options = {}) {
|
|
8997
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
8998
|
+
this.redactors = options.redactors ?? [];
|
|
8999
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
9000
|
+
}
|
|
9001
|
+
async addChannel(input) {
|
|
9002
|
+
const timestamp = new Date().toISOString();
|
|
9003
|
+
return this.store.addChannel({
|
|
9004
|
+
...input,
|
|
9005
|
+
createdAt: input.createdAt ?? timestamp,
|
|
9006
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
9007
|
+
});
|
|
9008
|
+
}
|
|
9009
|
+
async listChannels() {
|
|
9010
|
+
return this.store.listChannels();
|
|
9011
|
+
}
|
|
9012
|
+
async removeChannel(id) {
|
|
9013
|
+
return this.store.removeChannel(id);
|
|
9014
|
+
}
|
|
9015
|
+
async emit(input, options = {}) {
|
|
9016
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
9017
|
+
if (options.dedupe !== false) {
|
|
9018
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
9019
|
+
if (existing) {
|
|
9020
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
9021
|
+
}
|
|
9022
|
+
}
|
|
9023
|
+
await this.store.appendEvent(event);
|
|
9024
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
9025
|
+
return { event, deliveries, deduped: false };
|
|
9026
|
+
}
|
|
9027
|
+
async listEvents() {
|
|
9028
|
+
return this.store.listEvents();
|
|
9029
|
+
}
|
|
9030
|
+
async listDeliveries() {
|
|
9031
|
+
return this.store.listDeliveries();
|
|
9032
|
+
}
|
|
9033
|
+
async deliver(event) {
|
|
9034
|
+
const channels = await this.store.listChannels();
|
|
9035
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
9036
|
+
const deliveries = [];
|
|
9037
|
+
for (const channel of selected) {
|
|
9038
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
9039
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
9040
|
+
await this.store.appendDelivery(result);
|
|
9041
|
+
deliveries.push(result);
|
|
9042
|
+
}
|
|
9043
|
+
return deliveries;
|
|
9044
|
+
}
|
|
9045
|
+
async testChannel(id, input = {}) {
|
|
9046
|
+
const channel = await this.store.getChannel(id);
|
|
9047
|
+
if (!channel)
|
|
9048
|
+
throw new Error(`Channel not found: ${id}`);
|
|
9049
|
+
const event = createEvent({
|
|
9050
|
+
source: input.source ?? "hasna.events",
|
|
9051
|
+
type: input.type ?? "events.test",
|
|
9052
|
+
subject: input.subject ?? id,
|
|
9053
|
+
severity: input.severity ?? "info",
|
|
9054
|
+
data: input.data ?? { test: true },
|
|
9055
|
+
message: input.message ?? "Hasna events test delivery",
|
|
9056
|
+
dedupeKey: input.dedupeKey,
|
|
9057
|
+
schemaVersion: input.schemaVersion,
|
|
9058
|
+
metadata: input.metadata,
|
|
9059
|
+
time: input.time,
|
|
9060
|
+
id: input.id
|
|
9061
|
+
});
|
|
9062
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
9063
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
9064
|
+
await this.store.appendDelivery(result);
|
|
9065
|
+
return result;
|
|
9066
|
+
}
|
|
9067
|
+
async replay(options = {}) {
|
|
9068
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
9069
|
+
if (options.eventId && event.id !== options.eventId)
|
|
9070
|
+
return false;
|
|
9071
|
+
if (options.source && event.source !== options.source)
|
|
9072
|
+
return false;
|
|
9073
|
+
if (options.type && event.type !== options.type)
|
|
9074
|
+
return false;
|
|
9075
|
+
return true;
|
|
9076
|
+
});
|
|
9077
|
+
if (options.dryRun)
|
|
9078
|
+
return { events, deliveries: [] };
|
|
9079
|
+
const deliveries = [];
|
|
9080
|
+
for (const event of events) {
|
|
9081
|
+
deliveries.push(...await this.deliver(event));
|
|
9082
|
+
}
|
|
9083
|
+
return { events, deliveries };
|
|
9084
|
+
}
|
|
9085
|
+
async applyRedaction(event, channel) {
|
|
9086
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
9087
|
+
for (const redactor of this.redactors) {
|
|
9088
|
+
next = await redactor(next, channel);
|
|
9089
|
+
}
|
|
9090
|
+
return next;
|
|
9091
|
+
}
|
|
9092
|
+
async deliverWithRetry(event, channel) {
|
|
9093
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
9094
|
+
const attempts = [];
|
|
9095
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
9096
|
+
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
9097
|
+
attempt.attempt = index + 1;
|
|
9098
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
9099
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
9100
|
+
}
|
|
9101
|
+
attempts.push(attempt);
|
|
9102
|
+
if (attempt.status !== "failed")
|
|
9103
|
+
break;
|
|
9104
|
+
if (attempt.nextBackoffMs)
|
|
9105
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
9106
|
+
}
|
|
9107
|
+
return createDeliveryResult(event, channel, attempts);
|
|
9108
|
+
}
|
|
9109
|
+
}
|
|
9110
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
9111
|
+
if (paths.length === 0)
|
|
9112
|
+
return event;
|
|
9113
|
+
const copy = structuredClone(event);
|
|
9114
|
+
for (const path of paths) {
|
|
9115
|
+
setPath(copy, path, replacement);
|
|
9116
|
+
}
|
|
9117
|
+
return copy;
|
|
9118
|
+
}
|
|
9119
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
9120
|
+
return redactValue2(event, replacement);
|
|
9121
|
+
}
|
|
9122
|
+
function shouldRedactKey(key) {
|
|
9123
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
9124
|
+
}
|
|
9125
|
+
function redactValue2(value, replacement) {
|
|
9126
|
+
if (Array.isArray(value))
|
|
9127
|
+
return value.map((item) => redactValue2(item, replacement));
|
|
9128
|
+
if (!value || typeof value !== "object")
|
|
9129
|
+
return value;
|
|
9130
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
9131
|
+
key,
|
|
9132
|
+
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
9133
|
+
]));
|
|
9134
|
+
}
|
|
9135
|
+
function setPath(input, path, replacement) {
|
|
9136
|
+
const parts = path.split(".");
|
|
9137
|
+
let cursor = input;
|
|
9138
|
+
for (const part of parts.slice(0, -1)) {
|
|
9139
|
+
const next = cursor[part];
|
|
9140
|
+
if (!next || typeof next !== "object")
|
|
9141
|
+
return;
|
|
9142
|
+
cursor = next;
|
|
9143
|
+
}
|
|
9144
|
+
const last = parts.at(-1);
|
|
9145
|
+
if (last && last in cursor)
|
|
9146
|
+
cursor[last] = replacement;
|
|
9147
|
+
}
|
|
9148
|
+
function normalizeTime(value) {
|
|
9149
|
+
if (!value)
|
|
9150
|
+
return new Date().toISOString();
|
|
9151
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
9152
|
+
}
|
|
9153
|
+
function normalizeRetryPolicy(policy) {
|
|
9154
|
+
return {
|
|
9155
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
9156
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
9157
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
9158
|
+
};
|
|
9159
|
+
}
|
|
9160
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
|
|
9161
|
+
var init_dist = __esm(() => {
|
|
9162
|
+
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
9163
|
+
});
|
|
9164
|
+
|
|
9165
|
+
// src/lib/shared-events.ts
|
|
9166
|
+
function taskEventData(task, extra = {}) {
|
|
9167
|
+
return {
|
|
9168
|
+
id: task.id,
|
|
9169
|
+
task_id: task.id,
|
|
9170
|
+
short_id: task.short_id,
|
|
9171
|
+
title: task.title,
|
|
9172
|
+
description: task.description,
|
|
9173
|
+
status: task.status,
|
|
9174
|
+
priority: task.priority,
|
|
9175
|
+
project_id: task.project_id,
|
|
9176
|
+
parent_id: task.parent_id,
|
|
9177
|
+
plan_id: task.plan_id,
|
|
9178
|
+
task_list_id: task.task_list_id,
|
|
9179
|
+
agent_id: task.agent_id,
|
|
9180
|
+
assigned_to: task.assigned_to,
|
|
9181
|
+
session_id: task.session_id,
|
|
9182
|
+
working_dir: task.working_dir,
|
|
9183
|
+
tags: task.tags,
|
|
9184
|
+
metadata: task.metadata,
|
|
9185
|
+
version: task.version,
|
|
9186
|
+
created_at: task.created_at,
|
|
9187
|
+
updated_at: task.updated_at,
|
|
9188
|
+
started_at: task.started_at,
|
|
9189
|
+
completed_at: task.completed_at,
|
|
9190
|
+
due_at: task.due_at,
|
|
9191
|
+
...extra
|
|
9192
|
+
};
|
|
9193
|
+
}
|
|
9194
|
+
async function emitSharedTaskEvent(input) {
|
|
9195
|
+
const data = taskEventData(input.task, input.data);
|
|
9196
|
+
await new EventsClient().emit({
|
|
9197
|
+
source: SOURCE,
|
|
9198
|
+
type: input.type,
|
|
9199
|
+
subject: input.task.id,
|
|
9200
|
+
severity: input.severity ?? "info",
|
|
9201
|
+
message: input.message ?? `${input.type}: ${input.task.title}`,
|
|
9202
|
+
data,
|
|
9203
|
+
dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
|
|
9204
|
+
metadata: {
|
|
9205
|
+
package: "@hasna/todos",
|
|
9206
|
+
task_id: input.task.id,
|
|
9207
|
+
project_id: input.task.project_id,
|
|
9208
|
+
task_list_id: input.task.task_list_id
|
|
9209
|
+
}
|
|
9210
|
+
}, { deliver: true, dedupe: true });
|
|
9211
|
+
}
|
|
9212
|
+
function emitSharedTaskEventQuiet(input) {
|
|
9213
|
+
emitSharedTaskEvent(input).catch(() => {
|
|
9214
|
+
return;
|
|
9215
|
+
});
|
|
9216
|
+
}
|
|
9217
|
+
var SOURCE = "todos";
|
|
9218
|
+
var init_shared_events = __esm(() => {
|
|
9219
|
+
init_dist();
|
|
9220
|
+
});
|
|
9221
|
+
|
|
8655
9222
|
// src/lib/secret-redaction.ts
|
|
8656
9223
|
function isAllowlisted(text, match, allowlist) {
|
|
8657
9224
|
const context = text.slice(Math.max(0, text.indexOf(match) - 20), text.indexOf(match) + match.length + 20);
|
|
@@ -9119,7 +9686,7 @@ async function deliverWebhook(wh, event, body, attempt, db) {
|
|
|
9119
9686
|
activeDeliveries--;
|
|
9120
9687
|
}
|
|
9121
9688
|
}
|
|
9122
|
-
async function
|
|
9689
|
+
async function dispatchWebhook2(event, payload, db) {
|
|
9123
9690
|
const d = db || getDatabase();
|
|
9124
9691
|
const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
|
|
9125
9692
|
const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
|
|
@@ -9236,7 +9803,10 @@ function createTask(input, db) {
|
|
|
9236
9803
|
insertTaskTags(id, tags, d);
|
|
9237
9804
|
}
|
|
9238
9805
|
const task = getTask(id, d);
|
|
9239
|
-
|
|
9806
|
+
const payload = taskEventData(task);
|
|
9807
|
+
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
9808
|
+
emitLocalEventHooksQuiet({ type: "task.created", payload });
|
|
9809
|
+
emitSharedTaskEventQuiet({ type: "task.created", task });
|
|
9240
9810
|
return task;
|
|
9241
9811
|
}
|
|
9242
9812
|
function getTask(id, db) {
|
|
@@ -9580,18 +10150,7 @@ function updateTask(id, input, db) {
|
|
|
9580
10150
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
9581
10151
|
if (input.approved_by !== undefined)
|
|
9582
10152
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
9583
|
-
|
|
9584
|
-
dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
|
|
9585
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id, assigned_to: input.assigned_to, title: task.title } });
|
|
9586
|
-
}
|
|
9587
|
-
if (input.status !== undefined && input.status !== task.status) {
|
|
9588
|
-
dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
|
|
9589
|
-
emitLocalEventHooksQuiet({ type: "task.status_changed", payload: { id, old_status: task.status, new_status: input.status, title: task.title } });
|
|
9590
|
-
}
|
|
9591
|
-
if (input.approved_by !== undefined) {
|
|
9592
|
-
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
9593
|
-
}
|
|
9594
|
-
return {
|
|
10153
|
+
const updatedTask = {
|
|
9595
10154
|
...task,
|
|
9596
10155
|
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
9597
10156
|
tags: input.tags ?? task.tags,
|
|
@@ -9609,6 +10168,22 @@ function updateTask(id, input, db) {
|
|
|
9609
10168
|
approved_by: input.approved_by ?? task.approved_by,
|
|
9610
10169
|
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
9611
10170
|
};
|
|
10171
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
10172
|
+
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
10173
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
10174
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
10175
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
|
|
10176
|
+
}
|
|
10177
|
+
if (input.status !== undefined && input.status !== task.status) {
|
|
10178
|
+
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
10179
|
+
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
10180
|
+
emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
|
|
10181
|
+
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
|
|
10182
|
+
}
|
|
10183
|
+
if (input.approved_by !== undefined) {
|
|
10184
|
+
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
10185
|
+
}
|
|
10186
|
+
return updatedTask;
|
|
9612
10187
|
}
|
|
9613
10188
|
function deleteTask(id, db) {
|
|
9614
10189
|
const d = db || getDatabase();
|
|
@@ -9620,6 +10195,7 @@ var init_task_crud = __esm(() => {
|
|
|
9620
10195
|
init_database();
|
|
9621
10196
|
init_completion_guard();
|
|
9622
10197
|
init_event_hooks();
|
|
10198
|
+
init_shared_events();
|
|
9623
10199
|
init_audit();
|
|
9624
10200
|
init_webhooks();
|
|
9625
10201
|
init_checklists();
|
|
@@ -10375,9 +10951,12 @@ function startTask(id, agentId, db) {
|
|
|
10375
10951
|
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
10376
10952
|
}
|
|
10377
10953
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10954
|
+
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 };
|
|
10955
|
+
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
10956
|
+
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
10957
|
+
emitLocalEventHooksQuiet({ type: "task.started", payload });
|
|
10958
|
+
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
|
|
10959
|
+
return startedTask;
|
|
10381
10960
|
}
|
|
10382
10961
|
function completeTask(id, agentId, db, options) {
|
|
10383
10962
|
const d = db || getDatabase();
|
|
@@ -10413,8 +10992,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
10413
10992
|
});
|
|
10414
10993
|
tx();
|
|
10415
10994
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
10416
|
-
|
|
10417
|
-
|
|
10995
|
+
const completedTaskForEvent = {
|
|
10996
|
+
...task,
|
|
10997
|
+
status: "completed",
|
|
10998
|
+
locked_by: null,
|
|
10999
|
+
locked_at: null,
|
|
11000
|
+
completed_at: timestamp,
|
|
11001
|
+
confidence,
|
|
11002
|
+
version: task.version + 1,
|
|
11003
|
+
updated_at: timestamp,
|
|
11004
|
+
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
11005
|
+
};
|
|
11006
|
+
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
11007
|
+
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
11008
|
+
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
|
|
11009
|
+
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
|
|
10418
11010
|
let spawnedTask = null;
|
|
10419
11011
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
10420
11012
|
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
@@ -10455,8 +11047,12 @@ function completeTask(id, agentId, db, options) {
|
|
|
10455
11047
|
if (unblockedDeps.length > 0) {
|
|
10456
11048
|
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
10457
11049
|
for (const dep of unblockedDeps) {
|
|
10458
|
-
|
|
10459
|
-
|
|
11050
|
+
const depTask = getTask(dep.id, d);
|
|
11051
|
+
const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
|
|
11052
|
+
dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
|
|
11053
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
|
|
11054
|
+
if (depTask)
|
|
11055
|
+
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
|
|
10460
11056
|
}
|
|
10461
11057
|
}
|
|
10462
11058
|
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
@@ -10642,9 +11238,6 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
10642
11238
|
const timestamp = now();
|
|
10643
11239
|
d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
10644
11240
|
WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
|
|
10645
|
-
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
10646
|
-
dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
10647
|
-
emitLocalEventHooksQuiet({ type: "task.failed", payload: { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title } });
|
|
10648
11241
|
const failedTask = {
|
|
10649
11242
|
...task,
|
|
10650
11243
|
status: "failed",
|
|
@@ -10654,6 +11247,11 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
10654
11247
|
version: task.version + 1,
|
|
10655
11248
|
updated_at: timestamp
|
|
10656
11249
|
};
|
|
11250
|
+
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
11251
|
+
const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
|
|
11252
|
+
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
11253
|
+
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
|
|
11254
|
+
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
|
|
10657
11255
|
let retryTask;
|
|
10658
11256
|
if (options?.retry) {
|
|
10659
11257
|
const retryCount = (task.retry_count || 0) + 1;
|
|
@@ -10728,9 +11326,12 @@ function stealTask(agentId, opts, db) {
|
|
|
10728
11326
|
return null;
|
|
10729
11327
|
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
10730
11328
|
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
11329
|
+
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
11330
|
+
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
11331
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
11332
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
11333
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
|
|
11334
|
+
return stolenTask;
|
|
10734
11335
|
}
|
|
10735
11336
|
function claimOrSteal(agentId, filters, db) {
|
|
10736
11337
|
const d = db || getDatabase();
|
|
@@ -10778,6 +11379,7 @@ var init_task_lifecycle = __esm(() => {
|
|
|
10778
11379
|
init_database();
|
|
10779
11380
|
init_completion_guard();
|
|
10780
11381
|
init_event_hooks();
|
|
11382
|
+
init_shared_events();
|
|
10781
11383
|
init_audit();
|
|
10782
11384
|
init_recurrence();
|
|
10783
11385
|
init_webhooks();
|
|
@@ -11465,7 +12067,7 @@ function getTaskWatchers(taskId, db) {
|
|
|
11465
12067
|
}
|
|
11466
12068
|
function notifyWatchers(taskId, event, data, db) {
|
|
11467
12069
|
const watchers = getTaskWatchers(taskId, db);
|
|
11468
|
-
|
|
12070
|
+
dispatchWebhook2(`task.watcher.${event}`, { task_id: taskId, watchers: watchers.map((w) => w.agent_id), ...data }, db).catch(() => {});
|
|
11469
12071
|
}
|
|
11470
12072
|
function logCost(taskId, tokens, usd, db) {
|
|
11471
12073
|
const d = db || getDatabase();
|
|
@@ -11959,8 +12561,8 @@ var init_boards = __esm(() => {
|
|
|
11959
12561
|
|
|
11960
12562
|
// src/lib/artifact-store.ts
|
|
11961
12563
|
import { createHash as createHash2 } from "crypto";
|
|
11962
|
-
import { existsSync as
|
|
11963
|
-
import { basename, dirname as dirname4, join as
|
|
12564
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
12565
|
+
import { basename, dirname as dirname4, join as join5, resolve as resolve6 } from "path";
|
|
11964
12566
|
import { tmpdir } from "os";
|
|
11965
12567
|
function isInMemoryDb2(path) {
|
|
11966
12568
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
@@ -11972,15 +12574,15 @@ function artifactStoreRoot() {
|
|
|
11972
12574
|
return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
11973
12575
|
const dbPath = getDatabasePath();
|
|
11974
12576
|
if (isInMemoryDb2(dbPath))
|
|
11975
|
-
return
|
|
11976
|
-
return
|
|
12577
|
+
return join5(tmpdir(), "hasna-todos-artifacts");
|
|
12578
|
+
return join5(dirname4(resolve6(dbPath)), "artifacts");
|
|
11977
12579
|
}
|
|
11978
12580
|
function artifactStorePath(relativePath) {
|
|
11979
12581
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
11980
12582
|
if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
|
|
11981
12583
|
throw new Error("Invalid artifact store path");
|
|
11982
12584
|
}
|
|
11983
|
-
return
|
|
12585
|
+
return join5(artifactStoreRoot(), normalized);
|
|
11984
12586
|
}
|
|
11985
12587
|
function sha256(buffer) {
|
|
11986
12588
|
return createHash2("sha256").update(buffer).digest("hex");
|
|
@@ -12021,7 +12623,7 @@ function mediaTypeFor(path, textLike) {
|
|
|
12021
12623
|
}
|
|
12022
12624
|
function storeArtifactContent(input) {
|
|
12023
12625
|
const sourcePath = resolve6(input.path);
|
|
12024
|
-
if (!
|
|
12626
|
+
if (!existsSync6(sourcePath))
|
|
12025
12627
|
return null;
|
|
12026
12628
|
const sourceStat = statSync2(sourcePath);
|
|
12027
12629
|
if (!sourceStat.isFile())
|
|
@@ -12038,9 +12640,9 @@ function storeArtifactContent(input) {
|
|
|
12038
12640
|
redactionStatus = "redacted";
|
|
12039
12641
|
}
|
|
12040
12642
|
const storedSha = sha256(storedBuffer);
|
|
12041
|
-
const relativePath =
|
|
12643
|
+
const relativePath = join5("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
|
|
12042
12644
|
const destination = artifactStorePath(relativePath);
|
|
12043
|
-
if (!
|
|
12645
|
+
if (!existsSync6(destination)) {
|
|
12044
12646
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
12045
12647
|
writeFileSync2(destination, storedBuffer);
|
|
12046
12648
|
}
|
|
@@ -12100,7 +12702,7 @@ function verifyStoredArtifact(input) {
|
|
|
12100
12702
|
};
|
|
12101
12703
|
}
|
|
12102
12704
|
const storedPath = artifactStorePath(store.relative_path);
|
|
12103
|
-
if (!
|
|
12705
|
+
if (!existsSync6(storedPath)) {
|
|
12104
12706
|
return {
|
|
12105
12707
|
id: input.id,
|
|
12106
12708
|
path: input.path,
|
|
@@ -13385,17 +13987,73 @@ var init_dispatch_formatter = __esm(() => {
|
|
|
13385
13987
|
});
|
|
13386
13988
|
|
|
13387
13989
|
// src/lib/tmux.ts
|
|
13990
|
+
async function inspectTmuxPane(target) {
|
|
13991
|
+
const proc = Bun.spawn([
|
|
13992
|
+
"tmux",
|
|
13993
|
+
"display-message",
|
|
13994
|
+
"-p",
|
|
13995
|
+
"-t",
|
|
13996
|
+
target,
|
|
13997
|
+
"#{pane_id}\t#{pane_current_command}\t#{pane_dead}\t#{pane_input_off}\t#{pane_in_mode}"
|
|
13998
|
+
], {
|
|
13999
|
+
stdout: "pipe",
|
|
14000
|
+
stderr: "pipe"
|
|
14001
|
+
});
|
|
14002
|
+
const exitCode = await proc.exited;
|
|
14003
|
+
if (exitCode !== 0) {
|
|
14004
|
+
const stderr = await new Response(proc.stderr).text();
|
|
14005
|
+
throw new Error(`tmux target "${target}" not found: ${stderr.trim() || "unknown error"}`);
|
|
14006
|
+
}
|
|
14007
|
+
const stdout = (await new Response(proc.stdout).text()).trim();
|
|
14008
|
+
const [paneId, currentCommand = "", paneDead = "0", inputOff = "0", inMode = "0"] = stdout.split("\t");
|
|
14009
|
+
if (!paneId) {
|
|
14010
|
+
throw new Error(`tmux target "${target}" did not resolve to a pane`);
|
|
14011
|
+
}
|
|
14012
|
+
return {
|
|
14013
|
+
target,
|
|
14014
|
+
paneId,
|
|
14015
|
+
currentCommand,
|
|
14016
|
+
paneDead: paneDead === "1",
|
|
14017
|
+
inputOff: inputOff === "1",
|
|
14018
|
+
inMode: inMode === "1"
|
|
14019
|
+
};
|
|
14020
|
+
}
|
|
14021
|
+
function tmuxPaneBusyStatus(pane) {
|
|
14022
|
+
if (pane.paneDead) {
|
|
14023
|
+
return { busy: true, reason: "pane is dead" };
|
|
14024
|
+
}
|
|
14025
|
+
if (pane.inputOff) {
|
|
14026
|
+
return { busy: true, reason: "pane input is disabled" };
|
|
14027
|
+
}
|
|
14028
|
+
if (pane.inMode) {
|
|
14029
|
+
return { busy: true, reason: "pane is in copy or alternate mode" };
|
|
14030
|
+
}
|
|
14031
|
+
const currentCommand = pane.currentCommand.trim();
|
|
14032
|
+
if (currentCommand && !IDLE_TMUX_COMMANDS.has(currentCommand)) {
|
|
14033
|
+
return { busy: true, reason: `pane is running ${currentCommand}` };
|
|
14034
|
+
}
|
|
14035
|
+
return { busy: false, reason: null };
|
|
14036
|
+
}
|
|
13388
14037
|
function calculateDelay(message) {
|
|
13389
14038
|
const len = message.length;
|
|
13390
14039
|
const extra = Math.floor(len / 100 * 40);
|
|
13391
14040
|
return Math.min(DELAY_MIN + extra, DELAY_MAX);
|
|
13392
14041
|
}
|
|
13393
|
-
async function sendToTmux(target, message, delayMs,
|
|
14042
|
+
async function sendToTmux(target, message, delayMs, options = false) {
|
|
14043
|
+
const opts = typeof options === "boolean" ? { dryRun: options } : options;
|
|
14044
|
+
const dryRun = opts.dryRun ?? false;
|
|
13394
14045
|
if (dryRun) {
|
|
13395
14046
|
console.log(`[dry-run] sendToTmux target=${target} delay=${delayMs}ms`);
|
|
13396
14047
|
console.log(`[dry-run] message: ${message.slice(0, 200)}`);
|
|
13397
14048
|
return;
|
|
13398
14049
|
}
|
|
14050
|
+
if (!opts.confirmBusy) {
|
|
14051
|
+
const pane = await inspectTmuxPane(target);
|
|
14052
|
+
const status = tmuxPaneBusyStatus(pane);
|
|
14053
|
+
if (status.busy) {
|
|
14054
|
+
throw new Error(`tmux target "${target}" appears busy (${status.reason}). Re-run with --confirm-busy to send anyway.`);
|
|
14055
|
+
}
|
|
14056
|
+
}
|
|
13399
14057
|
const sendProc = Bun.spawn(["tmux", "send-keys", "-t", target, message, ""], {
|
|
13400
14058
|
stdout: "pipe",
|
|
13401
14059
|
stderr: "pipe"
|
|
@@ -13416,7 +14074,21 @@ async function sendToTmux(target, message, delayMs, dryRun = false) {
|
|
|
13416
14074
|
throw new Error(`tmux send-keys Enter failed for target "${target}": ${stderr.trim()}`);
|
|
13417
14075
|
}
|
|
13418
14076
|
}
|
|
13419
|
-
var DELAY_MIN = 3000, DELAY_MAX = 5000;
|
|
14077
|
+
var DELAY_MIN = 3000, DELAY_MAX = 5000, IDLE_TMUX_COMMANDS;
|
|
14078
|
+
var init_tmux = __esm(() => {
|
|
14079
|
+
IDLE_TMUX_COMMANDS = new Set([
|
|
14080
|
+
"bash",
|
|
14081
|
+
"dash",
|
|
14082
|
+
"elvish",
|
|
14083
|
+
"fish",
|
|
14084
|
+
"ksh",
|
|
14085
|
+
"nu",
|
|
14086
|
+
"pwsh",
|
|
14087
|
+
"sh",
|
|
14088
|
+
"tmux",
|
|
14089
|
+
"zsh"
|
|
14090
|
+
]);
|
|
14091
|
+
});
|
|
13420
14092
|
|
|
13421
14093
|
// src/lib/dispatch.ts
|
|
13422
14094
|
var exports_dispatch = {};
|
|
@@ -13434,7 +14106,10 @@ async function executeDispatch(dispatch, opts = {}, db) {
|
|
|
13434
14106
|
}
|
|
13435
14107
|
const delayMs = dispatch.delay_ms ?? calculateDelay(message);
|
|
13436
14108
|
try {
|
|
13437
|
-
await sendToTmux(dispatch.target_window, message, delayMs,
|
|
14109
|
+
await sendToTmux(dispatch.target_window, message, delayMs, {
|
|
14110
|
+
dryRun: opts.dryRun ?? false,
|
|
14111
|
+
confirmBusy: opts.confirmBusy ?? false
|
|
14112
|
+
});
|
|
13438
14113
|
createDispatchLog({
|
|
13439
14114
|
dispatch_id: dispatch.id,
|
|
13440
14115
|
target_window: dispatch.target_window,
|
|
@@ -13493,6 +14168,7 @@ var init_dispatch = __esm(() => {
|
|
|
13493
14168
|
init_tasks();
|
|
13494
14169
|
init_database();
|
|
13495
14170
|
init_dispatch_formatter();
|
|
14171
|
+
init_tmux();
|
|
13496
14172
|
});
|
|
13497
14173
|
|
|
13498
14174
|
// src/db/task-lists.ts
|
|
@@ -13587,8 +14263,9 @@ function registerDispatchTools(server, { shouldRegisterTool, resolveId, formatEr
|
|
|
13587
14263
|
target: exports_external.string().describe("tmux target \u2014 window name, session:window, or session:window.pane"),
|
|
13588
14264
|
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."),
|
|
13589
14265
|
scheduled_at: exports_external.string().optional().describe("ISO datetime to schedule the dispatch for. Fires immediately if omitted."),
|
|
14266
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if the target tmux pane appears busy. Default: false."),
|
|
13590
14267
|
dry_run: exports_external.boolean().optional().describe("Preview the formatted message without sending. Default: false.")
|
|
13591
|
-
}, async ({ task_ids, target, delay_ms, scheduled_at, dry_run }) => {
|
|
14268
|
+
}, async ({ task_ids, target, delay_ms, scheduled_at, confirm_busy, dry_run }) => {
|
|
13592
14269
|
try {
|
|
13593
14270
|
const db = getDatabase();
|
|
13594
14271
|
const resolvedIds = task_ids.map((id) => resolveId(id));
|
|
@@ -13604,7 +14281,7 @@ ${message}` }]
|
|
|
13604
14281
|
}
|
|
13605
14282
|
const dispatch = createDispatch({ task_ids: resolvedIds, target_window: target, message, delay_ms: effectiveDelay, scheduled_at }, db);
|
|
13606
14283
|
if (!scheduled_at)
|
|
13607
|
-
await executeDispatch(dispatch, {}, db);
|
|
14284
|
+
await executeDispatch(dispatch, { confirmBusy: confirm_busy ?? false }, db);
|
|
13608
14285
|
return {
|
|
13609
14286
|
content: [{
|
|
13610
14287
|
type: "text",
|
|
@@ -13630,8 +14307,9 @@ ${message}`
|
|
|
13630
14307
|
filter_status: exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"])).optional().describe("Only include tasks with these statuses. Default: pending."),
|
|
13631
14308
|
delay_ms: exports_external.number().optional().describe("Delay in ms between sending and Enter. Auto-calculated if omitted."),
|
|
13632
14309
|
scheduled_at: exports_external.string().optional().describe("ISO datetime to schedule. Fires immediately if omitted."),
|
|
14310
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if the target tmux pane appears busy. Default: false."),
|
|
13633
14311
|
dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false.")
|
|
13634
|
-
}, async ({ task_list_id, target, filter_status, delay_ms, scheduled_at, dry_run }) => {
|
|
14312
|
+
}, async ({ task_list_id, target, filter_status, delay_ms, scheduled_at, confirm_busy, dry_run }) => {
|
|
13635
14313
|
try {
|
|
13636
14314
|
const db = getDatabase();
|
|
13637
14315
|
const resolvedListId = resolveId(task_list_id, "task_lists");
|
|
@@ -13651,7 +14329,7 @@ ${message}` }]
|
|
|
13651
14329
|
}
|
|
13652
14330
|
const dispatch = createDispatch({ title: `Task list: ${taskList.name}`, task_list_id: resolvedListId, target_window: target, message, delay_ms: effectiveDelay, scheduled_at }, db);
|
|
13653
14331
|
if (!scheduled_at)
|
|
13654
|
-
await executeDispatch(dispatch, {}, db);
|
|
14332
|
+
await executeDispatch(dispatch, { confirmBusy: confirm_busy ?? false }, db);
|
|
13655
14333
|
return {
|
|
13656
14334
|
content: [{
|
|
13657
14335
|
type: "text",
|
|
@@ -13678,15 +14356,16 @@ ${message}`
|
|
|
13678
14356
|
task_list_id: exports_external.string().optional().describe("Task list ID to dispatch (use this or task_ids)"),
|
|
13679
14357
|
stagger_ms: exports_external.number().optional().describe("Delay between each window dispatch. Default: 500ms."),
|
|
13680
14358
|
delay_ms: exports_external.number().optional().describe("Delay between message send and Enter. Auto-calculated if omitted."),
|
|
14359
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
|
|
13681
14360
|
dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false.")
|
|
13682
|
-
}, async ({ targets, task_ids, task_list_id, stagger_ms, delay_ms, dry_run }) => {
|
|
14361
|
+
}, async ({ targets, task_ids, task_list_id, stagger_ms, delay_ms, confirm_busy, dry_run }) => {
|
|
13683
14362
|
try {
|
|
13684
14363
|
if (!task_ids && !task_list_id)
|
|
13685
14364
|
throw new Error("Either task_ids or task_list_id is required");
|
|
13686
14365
|
const db = getDatabase();
|
|
13687
14366
|
const resolvedTaskIds = task_ids ? task_ids.map((id) => resolveId(id)) : undefined;
|
|
13688
14367
|
const resolvedListId = task_list_id ? resolveId(task_list_id, "task_lists") : undefined;
|
|
13689
|
-
const dispatches = await dispatchToMultiple({ targets, task_ids: resolvedTaskIds, task_list_id: resolvedListId, delay_ms, stagger_ms }, { dryRun: dry_run }, db);
|
|
14368
|
+
const dispatches = await dispatchToMultiple({ targets, task_ids: resolvedTaskIds, task_list_id: resolvedListId, delay_ms, stagger_ms }, { dryRun: dry_run, confirmBusy: confirm_busy ?? false }, db);
|
|
13690
14369
|
const lines = dispatches.map((d) => `${d.target_window}: ${d.id} [${d.status}]`);
|
|
13691
14370
|
return { content: [{ type: "text", text: `Dispatched to ${dispatches.length} target(s):
|
|
13692
14371
|
${lines.join(`
|
|
@@ -13731,11 +14410,12 @@ ${lines.join(`
|
|
|
13731
14410
|
if (shouldRegisterTool("run_due_dispatches")) {
|
|
13732
14411
|
server.tool("run_due_dispatches", "Manually trigger all pending dispatches that are due (scheduled_at <= now). Returns the count fired.", {
|
|
13733
14412
|
dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false."),
|
|
14413
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
|
|
13734
14414
|
all: exports_external.boolean().optional().describe("Ignore scheduled_at and fire all pending dispatches immediately.")
|
|
13735
|
-
}, async ({ dry_run }) => {
|
|
14415
|
+
}, async ({ dry_run, confirm_busy }) => {
|
|
13736
14416
|
try {
|
|
13737
14417
|
const { runDueDispatches: runDueDispatches2 } = await Promise.resolve().then(() => (init_dispatch(), exports_dispatch));
|
|
13738
|
-
const count = await runDueDispatches2({ dryRun: dry_run });
|
|
14418
|
+
const count = await runDueDispatches2({ dryRun: dry_run, confirmBusy: confirm_busy ?? false });
|
|
13739
14419
|
return { content: [{ type: "text", text: `Fired ${count} dispatch(es).` }] };
|
|
13740
14420
|
} catch (e) {
|
|
13741
14421
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
@@ -13748,6 +14428,7 @@ var init_dispatch2 = __esm(() => {
|
|
|
13748
14428
|
init_dispatches();
|
|
13749
14429
|
init_dispatch();
|
|
13750
14430
|
init_dispatch_formatter();
|
|
14431
|
+
init_tmux();
|
|
13751
14432
|
init_tasks();
|
|
13752
14433
|
init_task_lists();
|
|
13753
14434
|
init_database();
|
|
@@ -14555,7 +15236,7 @@ var init_task_crud2 = __esm(() => {
|
|
|
14555
15236
|
});
|
|
14556
15237
|
|
|
14557
15238
|
// src/lib/project-bootstrap.ts
|
|
14558
|
-
import { existsSync as
|
|
15239
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
14559
15240
|
import { basename as basename2, dirname as dirname5, resolve as resolve7 } from "path";
|
|
14560
15241
|
function safeStat(path) {
|
|
14561
15242
|
try {
|
|
@@ -14574,7 +15255,7 @@ function canonicalPath(input) {
|
|
|
14574
15255
|
function findUp(start, marker) {
|
|
14575
15256
|
let current = canonicalPath(start);
|
|
14576
15257
|
while (true) {
|
|
14577
|
-
if (
|
|
15258
|
+
if (existsSync7(resolve7(current, marker)))
|
|
14578
15259
|
return current;
|
|
14579
15260
|
const parent = dirname5(current);
|
|
14580
15261
|
if (parent === current)
|
|
@@ -14586,7 +15267,7 @@ function readPackageJson(path) {
|
|
|
14586
15267
|
if (!path)
|
|
14587
15268
|
return null;
|
|
14588
15269
|
const file = resolve7(path, "package.json");
|
|
14589
|
-
if (!
|
|
15270
|
+
if (!existsSync7(file))
|
|
14590
15271
|
return null;
|
|
14591
15272
|
try {
|
|
14592
15273
|
const parsed = JSON.parse(readFileSync3(file, "utf-8"));
|
|
@@ -14608,7 +15289,7 @@ function workspaceMarker(root, rootPackage) {
|
|
|
14608
15289
|
if (rootPackage?.workspaces)
|
|
14609
15290
|
markers.push("package.json#workspaces");
|
|
14610
15291
|
for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
|
|
14611
|
-
if (
|
|
15292
|
+
if (existsSync7(resolve7(root, marker)))
|
|
14612
15293
|
markers.push(marker);
|
|
14613
15294
|
}
|
|
14614
15295
|
const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
|
|
@@ -14909,7 +15590,7 @@ var init_tags = __esm(() => {
|
|
|
14909
15590
|
});
|
|
14910
15591
|
|
|
14911
15592
|
// src/lib/retention-cleanup.ts
|
|
14912
|
-
import { existsSync as
|
|
15593
|
+
import { existsSync as existsSync8, unlinkSync } from "fs";
|
|
14913
15594
|
function normalizeScopes(scopes) {
|
|
14914
15595
|
if (!scopes || scopes.length === 0)
|
|
14915
15596
|
return [...ALL_SCOPES];
|
|
@@ -15112,7 +15793,7 @@ function applyRetentionCleanup(input, db) {
|
|
|
15112
15793
|
for (const artifact of report.candidates.artifact_files) {
|
|
15113
15794
|
try {
|
|
15114
15795
|
const path = artifactStorePath(artifact.relative_path);
|
|
15115
|
-
if (!
|
|
15796
|
+
if (!existsSync8(path)) {
|
|
15116
15797
|
report.warnings.push(`stored artifact already missing: ${artifact.relative_path}`);
|
|
15117
15798
|
continue;
|
|
15118
15799
|
}
|
|
@@ -15139,8 +15820,8 @@ var init_retention_cleanup = __esm(() => {
|
|
|
15139
15820
|
});
|
|
15140
15821
|
|
|
15141
15822
|
// src/lib/mention-resolver.ts
|
|
15142
|
-
import { existsSync as
|
|
15143
|
-
import { basename as basename3, isAbsolute, join as
|
|
15823
|
+
import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
15824
|
+
import { basename as basename3, isAbsolute, join as join6, relative as relative3, resolve as resolve8, sep } from "path";
|
|
15144
15825
|
function blankResolution(parsed) {
|
|
15145
15826
|
return {
|
|
15146
15827
|
input: parsed.input,
|
|
@@ -15238,7 +15919,7 @@ function resolveFile(parsed, workspace) {
|
|
|
15238
15919
|
return resolution;
|
|
15239
15920
|
}
|
|
15240
15921
|
resolution.path = relPath;
|
|
15241
|
-
if (!
|
|
15922
|
+
if (!existsSync9(absolutePath)) {
|
|
15242
15923
|
resolution.warnings.push("file does not exist in the local workspace");
|
|
15243
15924
|
return resolution;
|
|
15244
15925
|
}
|
|
@@ -15271,7 +15952,7 @@ function walkSourceFiles(root, current = root, files = []) {
|
|
|
15271
15952
|
if (SKIP_DIRS.has(entry.name))
|
|
15272
15953
|
continue;
|
|
15273
15954
|
}
|
|
15274
|
-
const absolutePath =
|
|
15955
|
+
const absolutePath = join6(current, entry.name);
|
|
15275
15956
|
if (entry.isDirectory()) {
|
|
15276
15957
|
if (!SKIP_DIRS.has(entry.name))
|
|
15277
15958
|
walkSourceFiles(root, absolutePath, files);
|
|
@@ -16538,8 +17219,8 @@ var init_local_notifications = __esm(() => {
|
|
|
16538
17219
|
});
|
|
16539
17220
|
|
|
16540
17221
|
// src/lib/local-encryption.ts
|
|
16541
|
-
import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes, scryptSync, timingSafeEqual } from "crypto";
|
|
16542
|
-
function
|
|
17222
|
+
import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
17223
|
+
function now3() {
|
|
16543
17224
|
return new Date().toISOString();
|
|
16544
17225
|
}
|
|
16545
17226
|
function sha2562(value) {
|
|
@@ -16576,7 +17257,7 @@ function upsertEncryptionProfile(input) {
|
|
|
16576
17257
|
const name = normalizeProfileName(input.name);
|
|
16577
17258
|
const config = loadConfig();
|
|
16578
17259
|
const existing = config.encryption_profiles?.[name];
|
|
16579
|
-
const timestamp =
|
|
17260
|
+
const timestamp = now3();
|
|
16580
17261
|
const profile = {
|
|
16581
17262
|
name,
|
|
16582
17263
|
algorithm: "aes-256-gcm",
|
|
@@ -16633,7 +17314,7 @@ function encryptString(plaintext, options = {}) {
|
|
|
16633
17314
|
return {
|
|
16634
17315
|
schemaVersion: TODOS_ENCRYPTION_SCHEMA_VERSION,
|
|
16635
17316
|
kind: TODOS_ENCRYPTED_VALUE_KIND,
|
|
16636
|
-
encryptedAt: options.encryptedAt ??
|
|
17317
|
+
encryptedAt: options.encryptedAt ?? now3(),
|
|
16637
17318
|
profile: profile.name,
|
|
16638
17319
|
key_env: profile.key_env,
|
|
16639
17320
|
algorithm: "aes-256-gcm",
|
|
@@ -16668,7 +17349,7 @@ function decryptString(envelope, env = process.env) {
|
|
|
16668
17349
|
]).toString("utf8");
|
|
16669
17350
|
const expected = Buffer.from(envelope.plaintext_sha256, "hex");
|
|
16670
17351
|
const actual = Buffer.from(sha2562(plaintext), "hex");
|
|
16671
|
-
if (expected.length !== actual.length || !
|
|
17352
|
+
if (expected.length !== actual.length || !timingSafeEqual2(expected, actual)) {
|
|
16672
17353
|
throw new EncryptedPayloadError("decrypted payload checksum mismatch");
|
|
16673
17354
|
}
|
|
16674
17355
|
return plaintext;
|
|
@@ -18191,7 +18872,7 @@ function createRoadmap(input) {
|
|
|
18191
18872
|
if (!name)
|
|
18192
18873
|
throw new Error("Roadmap name is required");
|
|
18193
18874
|
const store = readStore();
|
|
18194
|
-
const
|
|
18875
|
+
const now4 = timestamp();
|
|
18195
18876
|
const roadmap = {
|
|
18196
18877
|
id: newId("roadmap"),
|
|
18197
18878
|
name,
|
|
@@ -18202,8 +18883,8 @@ function createRoadmap(input) {
|
|
|
18202
18883
|
agent_id: cleanString(input.agent_id),
|
|
18203
18884
|
release: cleanString(input.release),
|
|
18204
18885
|
milestone_ids: [],
|
|
18205
|
-
created_at:
|
|
18206
|
-
updated_at:
|
|
18886
|
+
created_at: now4,
|
|
18887
|
+
updated_at: now4
|
|
18207
18888
|
};
|
|
18208
18889
|
store.roadmaps[roadmap.id] = roadmap;
|
|
18209
18890
|
writeStore(store);
|
|
@@ -18255,7 +18936,7 @@ function createMilestone(input) {
|
|
|
18255
18936
|
const title = input.title.trim();
|
|
18256
18937
|
if (!title)
|
|
18257
18938
|
throw new Error("Milestone title is required");
|
|
18258
|
-
const
|
|
18939
|
+
const now4 = timestamp();
|
|
18259
18940
|
const milestone = {
|
|
18260
18941
|
id: newId("milestone"),
|
|
18261
18942
|
roadmap_id: roadmapId,
|
|
@@ -18270,11 +18951,11 @@ function createMilestone(input) {
|
|
|
18270
18951
|
run_ids: cleanList(input.run_ids),
|
|
18271
18952
|
release: cleanString(input.release ?? roadmap.release ?? undefined),
|
|
18272
18953
|
tags: cleanList(input.tags),
|
|
18273
|
-
created_at:
|
|
18274
|
-
updated_at:
|
|
18954
|
+
created_at: now4,
|
|
18955
|
+
updated_at: now4
|
|
18275
18956
|
};
|
|
18276
18957
|
store.milestones[milestone.id] = milestone;
|
|
18277
|
-
store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList([...roadmap.milestone_ids, milestone.id]), updated_at:
|
|
18958
|
+
store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList([...roadmap.milestone_ids, milestone.id]), updated_at: now4 };
|
|
18278
18959
|
writeStore(store);
|
|
18279
18960
|
return milestone;
|
|
18280
18961
|
}
|
|
@@ -18329,7 +19010,7 @@ function upsertReleaseGroup(input) {
|
|
|
18329
19010
|
throw new Error("Release group name is required");
|
|
18330
19011
|
const key = releaseKey(roadmapId, name);
|
|
18331
19012
|
const existing = store.releases[key];
|
|
18332
|
-
const
|
|
19013
|
+
const now4 = timestamp();
|
|
18333
19014
|
const release = {
|
|
18334
19015
|
name,
|
|
18335
19016
|
version: input.version === undefined ? existing?.version ?? null : cleanString(input.version),
|
|
@@ -18340,8 +19021,8 @@ function upsertReleaseGroup(input) {
|
|
|
18340
19021
|
plan_ids: input.plan_ids === undefined ? existing?.plan_ids ?? [] : cleanList(input.plan_ids),
|
|
18341
19022
|
run_ids: input.run_ids === undefined ? existing?.run_ids ?? [] : cleanList(input.run_ids),
|
|
18342
19023
|
notes: input.notes === undefined ? existing?.notes ?? null : cleanString(input.notes),
|
|
18343
|
-
created_at: existing?.created_at ??
|
|
18344
|
-
updated_at:
|
|
19024
|
+
created_at: existing?.created_at ?? now4,
|
|
19025
|
+
updated_at: now4
|
|
18345
19026
|
};
|
|
18346
19027
|
store.releases[key] = release;
|
|
18347
19028
|
writeStore(store);
|
|
@@ -18540,7 +19221,7 @@ function upsertCapacityProfile(input) {
|
|
|
18540
19221
|
const store = readStore2();
|
|
18541
19222
|
const key = profileKey(agentId, projectId);
|
|
18542
19223
|
const existing = store.profiles[key];
|
|
18543
|
-
const
|
|
19224
|
+
const now4 = timestamp2();
|
|
18544
19225
|
const profile = {
|
|
18545
19226
|
id: existing?.id ?? key,
|
|
18546
19227
|
agent_id: agentId,
|
|
@@ -18548,8 +19229,8 @@ function upsertCapacityProfile(input) {
|
|
|
18548
19229
|
minutes_per_day: assertMinutes(input.minutes_per_day),
|
|
18549
19230
|
working_days: normalizeWorkingDays(input.working_days),
|
|
18550
19231
|
effective_from: cleanString2(input.effective_from),
|
|
18551
|
-
created_at: existing?.created_at ??
|
|
18552
|
-
updated_at:
|
|
19232
|
+
created_at: existing?.created_at ?? now4,
|
|
19233
|
+
updated_at: now4
|
|
18553
19234
|
};
|
|
18554
19235
|
store.profiles[key] = profile;
|
|
18555
19236
|
writeStore2(store);
|
|
@@ -18958,7 +19639,7 @@ var init_audit_ledger = __esm(() => {
|
|
|
18958
19639
|
|
|
18959
19640
|
// src/lib/release-compatibility.ts
|
|
18960
19641
|
import { readFileSync as readFileSync5 } from "fs";
|
|
18961
|
-
import { join as
|
|
19642
|
+
import { join as join7, resolve as resolve10 } from "path";
|
|
18962
19643
|
import { Database as Database2 } from "bun:sqlite";
|
|
18963
19644
|
function pass(id, message, details) {
|
|
18964
19645
|
return { id, status: "passed", message, details };
|
|
@@ -18970,7 +19651,7 @@ function warn(id, message, details) {
|
|
|
18970
19651
|
return { id, status: "warning", message, details };
|
|
18971
19652
|
}
|
|
18972
19653
|
function readPackageJson2(root) {
|
|
18973
|
-
return JSON.parse(readFileSync5(
|
|
19654
|
+
return JSON.parse(readFileSync5(join7(root, "package.json"), "utf8"));
|
|
18974
19655
|
}
|
|
18975
19656
|
function sortedKeys(value) {
|
|
18976
19657
|
return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
|
|
@@ -19239,6 +19920,9 @@ function hasFts(db) {
|
|
|
19239
19920
|
function escapeFtsQuery(q) {
|
|
19240
19921
|
return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
|
|
19241
19922
|
}
|
|
19923
|
+
function shouldUseFts(q) {
|
|
19924
|
+
return /^[\p{L}\p{N}_\-\s]+$/u.test(q);
|
|
19925
|
+
}
|
|
19242
19926
|
function searchTasks(options, projectId, taskListId, db) {
|
|
19243
19927
|
const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
|
|
19244
19928
|
const d = db || getDatabase();
|
|
@@ -19247,7 +19931,8 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
19247
19931
|
let sql;
|
|
19248
19932
|
const raw = opts.query?.trim() ?? "";
|
|
19249
19933
|
const q = raw === "*" ? "" : raw;
|
|
19250
|
-
|
|
19934
|
+
const useFts = hasFts(d) && q && shouldUseFts(q);
|
|
19935
|
+
if (useFts) {
|
|
19251
19936
|
const ftsQuery = escapeFtsQuery(q);
|
|
19252
19937
|
sql = `SELECT t.* FROM tasks t
|
|
19253
19938
|
INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
|
|
@@ -19255,8 +19940,16 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
19255
19940
|
params.push(ftsQuery);
|
|
19256
19941
|
} else if (q) {
|
|
19257
19942
|
const pattern = `%${q}%`;
|
|
19258
|
-
sql = `SELECT * FROM tasks t WHERE (
|
|
19259
|
-
|
|
19943
|
+
sql = `SELECT * FROM tasks t WHERE (
|
|
19944
|
+
t.id LIKE ?
|
|
19945
|
+
OR t.short_id LIKE ?
|
|
19946
|
+
OR t.title LIKE ?
|
|
19947
|
+
OR t.description LIKE ?
|
|
19948
|
+
OR t.working_dir LIKE ?
|
|
19949
|
+
OR t.metadata LIKE ?
|
|
19950
|
+
OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?)
|
|
19951
|
+
)`;
|
|
19952
|
+
params.push(pattern, pattern, pattern, pattern, pattern, pattern, pattern);
|
|
19260
19953
|
} else {
|
|
19261
19954
|
sql = `SELECT * FROM tasks t WHERE 1=1`;
|
|
19262
19955
|
}
|
|
@@ -19312,7 +20005,7 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
19312
20005
|
} else if (opts.is_blocked === false) {
|
|
19313
20006
|
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')";
|
|
19314
20007
|
}
|
|
19315
|
-
if (
|
|
20008
|
+
if (useFts) {
|
|
19316
20009
|
sql += ` ORDER BY bm25(tasks_fts),
|
|
19317
20010
|
CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
19318
20011
|
t.created_at DESC`;
|
|
@@ -22407,7 +23100,7 @@ function registerTaskWorkflowTools(server, ctx) {
|
|
|
22407
23100
|
const globalRole = n.agent.role ? ` [${n.agent.role}]` : "";
|
|
22408
23101
|
const lead = n.is_lead ? " \u2605" : "";
|
|
22409
23102
|
const lastSeen = new Date(n.agent.last_seen_at).getTime();
|
|
22410
|
-
const active =
|
|
23103
|
+
const active = now4 - lastSeen < ACTIVE_MS ? " \u25CF" : " \u25CB";
|
|
22411
23104
|
const line = `${prefix}${active} ${n.agent.name}${title}${globalRole}${lead}`;
|
|
22412
23105
|
const children = n.reports.length > 0 ? `
|
|
22413
23106
|
` + render(n.reports, indent + 1) : "";
|
|
@@ -22420,7 +23113,7 @@ function registerTaskWorkflowTools(server, ctx) {
|
|
|
22420
23113
|
if (format === "json") {
|
|
22421
23114
|
return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) }] };
|
|
22422
23115
|
}
|
|
22423
|
-
const
|
|
23116
|
+
const now4 = Date.now();
|
|
22424
23117
|
const ACTIVE_MS = 30 * 60 * 1000;
|
|
22425
23118
|
const text = tree.length > 0 ? render(tree) : "No agents in org chart.";
|
|
22426
23119
|
return { content: [{ type: "text", text }] };
|
|
@@ -22542,8 +23235,8 @@ var exports_doctor = {};
|
|
|
22542
23235
|
__export(exports_doctor, {
|
|
22543
23236
|
runTodosDoctor: () => runTodosDoctor
|
|
22544
23237
|
});
|
|
22545
|
-
import { chmodSync, copyFileSync, existsSync as
|
|
22546
|
-
import { basename as basename4, dirname as dirname6, join as
|
|
23238
|
+
import { chmodSync, copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync5, statSync as statSync5 } from "fs";
|
|
23239
|
+
import { basename as basename4, dirname as dirname6, join as join8 } from "path";
|
|
22547
23240
|
function tableExists(db, table) {
|
|
22548
23241
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
22549
23242
|
}
|
|
@@ -22637,7 +23330,7 @@ function findMissingProjectRoots(db) {
|
|
|
22637
23330
|
continue;
|
|
22638
23331
|
if (!row.path.startsWith("/"))
|
|
22639
23332
|
continue;
|
|
22640
|
-
if (!
|
|
23333
|
+
if (!existsSync10(row.path))
|
|
22641
23334
|
missing++;
|
|
22642
23335
|
}
|
|
22643
23336
|
return missing;
|
|
@@ -22697,16 +23390,16 @@ function databasePermissionsAreUnsafe(dbPath) {
|
|
|
22697
23390
|
function createBackup(dbPath) {
|
|
22698
23391
|
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
22699
23392
|
return;
|
|
22700
|
-
if (!
|
|
23393
|
+
if (!existsSync10(dbPath))
|
|
22701
23394
|
return;
|
|
22702
23395
|
const stamp = now().replace(/[:.]/g, "-");
|
|
22703
|
-
const backupDir =
|
|
23396
|
+
const backupDir = join8(dirname6(dbPath), `${basename4(dbPath)}.backup-${stamp}`);
|
|
22704
23397
|
const files = [];
|
|
22705
23398
|
mkdirSync5(backupDir, { recursive: true });
|
|
22706
23399
|
for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
22707
|
-
if (!
|
|
23400
|
+
if (!existsSync10(source))
|
|
22708
23401
|
continue;
|
|
22709
|
-
const target =
|
|
23402
|
+
const target = join8(backupDir, basename4(source));
|
|
22710
23403
|
copyFileSync(source, target);
|
|
22711
23404
|
files.push(target);
|
|
22712
23405
|
}
|
|
@@ -23010,14 +23703,14 @@ function registerTaskAutoTools(server, ctx) {
|
|
|
23010
23703
|
return { content: [{ type: "text", text: "No agent_id provided and no agent focus is active." }], isError: true };
|
|
23011
23704
|
}
|
|
23012
23705
|
const assigned = listTasks3({ assigned_to: effectiveAgentId, limit: 500 }, undefined);
|
|
23013
|
-
const
|
|
23014
|
-
const dueSoonCutoff =
|
|
23706
|
+
const now4 = Date.now();
|
|
23707
|
+
const dueSoonCutoff = now4 + 24 * 60 * 60 * 1000;
|
|
23015
23708
|
const blocked = getBlockedTasks2().filter((t) => t.assigned_to === effectiveAgentId);
|
|
23016
23709
|
const workload = {
|
|
23017
23710
|
in_progress: assigned.filter((t) => t.status === "in_progress").length,
|
|
23018
23711
|
pending: assigned.filter((t) => t.status === "pending").length,
|
|
23019
|
-
completed_recent: assigned.filter((t) => t.status === "completed" && t.completed_at &&
|
|
23020
|
-
due_soon: assigned.filter((t) => t.due_at && new Date(t.due_at).getTime() <= dueSoonCutoff && new Date(t.due_at).getTime() >=
|
|
23712
|
+
completed_recent: assigned.filter((t) => t.status === "completed" && t.completed_at && now4 - new Date(t.completed_at).getTime() <= 7 * 24 * 60 * 60 * 1000).length,
|
|
23713
|
+
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,
|
|
23021
23714
|
blocked: blocked.length
|
|
23022
23715
|
};
|
|
23023
23716
|
const lines = [
|
|
@@ -23244,7 +23937,7 @@ function limits(input) {
|
|
|
23244
23937
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
23245
23938
|
};
|
|
23246
23939
|
}
|
|
23247
|
-
function
|
|
23940
|
+
function truncate2(value, max) {
|
|
23248
23941
|
if (!value)
|
|
23249
23942
|
return value ?? null;
|
|
23250
23943
|
const redacted = redactEvidenceText(value);
|
|
@@ -23265,9 +23958,9 @@ function acceptanceCriteria(task, maxText) {
|
|
|
23265
23958
|
const metadata = task.metadata || {};
|
|
23266
23959
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
23267
23960
|
if (Array.isArray(raw))
|
|
23268
|
-
return raw.map((item) =>
|
|
23961
|
+
return raw.map((item) => truncate2(String(item), maxText)).filter((item) => Boolean(item));
|
|
23269
23962
|
if (typeof raw === "string") {
|
|
23270
|
-
return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) =>
|
|
23963
|
+
return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) => truncate2(line, maxText)).filter((item) => Boolean(item));
|
|
23271
23964
|
}
|
|
23272
23965
|
return [];
|
|
23273
23966
|
}
|
|
@@ -23290,7 +23983,7 @@ function addFile(files, path, source, base) {
|
|
|
23290
23983
|
path,
|
|
23291
23984
|
status: base?.status || "active",
|
|
23292
23985
|
agent_id: base?.agent_id ?? null,
|
|
23293
|
-
note:
|
|
23986
|
+
note: truncate2(base?.note, 240),
|
|
23294
23987
|
updated_at: base?.updated_at || "",
|
|
23295
23988
|
sources: [source]
|
|
23296
23989
|
});
|
|
@@ -23348,7 +24041,7 @@ function estimateTokens(value) {
|
|
|
23348
24041
|
return Math.max(1, Math.ceil((text || "").length / 4));
|
|
23349
24042
|
}
|
|
23350
24043
|
function summarizeStrings(values, maxChars) {
|
|
23351
|
-
return
|
|
24044
|
+
return truncate2(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
|
|
23352
24045
|
}
|
|
23353
24046
|
function summarizeSection(pack, section, maxChars) {
|
|
23354
24047
|
if (section === "project")
|
|
@@ -23539,8 +24232,8 @@ function createAgentContextPack(input, db) {
|
|
|
23539
24232
|
...taskFiles.map((file) => file.updated_at)
|
|
23540
24233
|
], task.updated_at);
|
|
23541
24234
|
const warnings = [];
|
|
23542
|
-
const
|
|
23543
|
-
if (Date.parse(task.updated_at) <
|
|
24235
|
+
const now4 = input.now ? new Date(input.now) : new Date;
|
|
24236
|
+
if (Date.parse(task.updated_at) < now4.getTime() - limit.stale_after_hours * 60 * 60 * 1000) {
|
|
23544
24237
|
warnings.push(`task state is older than ${limit.stale_after_hours} hours`);
|
|
23545
24238
|
}
|
|
23546
24239
|
if (comments.length > recentComments.length)
|
|
@@ -23555,7 +24248,7 @@ function createAgentContextPack(input, db) {
|
|
|
23555
24248
|
id: task.id,
|
|
23556
24249
|
short_id: task.short_id,
|
|
23557
24250
|
title: redactEvidenceText(task.title),
|
|
23558
|
-
description:
|
|
24251
|
+
description: truncate2(task.description, limit.max_text_chars),
|
|
23559
24252
|
status: task.status,
|
|
23560
24253
|
priority: task.priority,
|
|
23561
24254
|
assigned_to: task.assigned_to,
|
|
@@ -23579,7 +24272,7 @@ function createAgentContextPack(input, db) {
|
|
|
23579
24272
|
plan: plan ? {
|
|
23580
24273
|
id: plan.id,
|
|
23581
24274
|
name: plan.name,
|
|
23582
|
-
description:
|
|
24275
|
+
description: truncate2(plan.description, limit.max_text_chars),
|
|
23583
24276
|
status: plan.status,
|
|
23584
24277
|
agent_id: plan.agent_id,
|
|
23585
24278
|
tasks: planTasks.slice(0, limit.plan_task_limit).map(taskSummary).filter((item) => Boolean(item)),
|
|
@@ -23598,7 +24291,7 @@ function createAgentContextPack(input, db) {
|
|
|
23598
24291
|
type: comment.type,
|
|
23599
24292
|
progress_pct: comment.progress_pct,
|
|
23600
24293
|
created_at: comment.created_at,
|
|
23601
|
-
content:
|
|
24294
|
+
content: truncate2(comment.content, limit.max_text_chars) || ""
|
|
23602
24295
|
})),
|
|
23603
24296
|
omitted: Math.max(0, comments.length - recentComments.length)
|
|
23604
24297
|
},
|
|
@@ -23606,7 +24299,7 @@ function createAgentContextPack(input, db) {
|
|
|
23606
24299
|
traceability: {
|
|
23607
24300
|
commits: traceability.commits.map((commit) => ({
|
|
23608
24301
|
sha: commit.sha,
|
|
23609
|
-
message:
|
|
24302
|
+
message: truncate2(commit.message, 240),
|
|
23610
24303
|
files_changed: commit.files_changed,
|
|
23611
24304
|
committed_at: commit.committed_at
|
|
23612
24305
|
})),
|
|
@@ -23614,7 +24307,7 @@ function createAgentContextPack(input, db) {
|
|
|
23614
24307
|
verifications: verifications.map((verification) => ({
|
|
23615
24308
|
command: verification.command,
|
|
23616
24309
|
status: verification.status,
|
|
23617
|
-
output_summary:
|
|
24310
|
+
output_summary: truncate2(verification.output_summary, limit.max_text_chars),
|
|
23618
24311
|
artifact_path: verification.artifact_path,
|
|
23619
24312
|
run_at: verification.run_at
|
|
23620
24313
|
})),
|
|
@@ -23625,14 +24318,14 @@ function createAgentContextPack(input, db) {
|
|
|
23625
24318
|
id: ledger.run.id,
|
|
23626
24319
|
title: ledger.run.title,
|
|
23627
24320
|
status: ledger.run.status,
|
|
23628
|
-
summary:
|
|
24321
|
+
summary: truncate2(ledger.run.summary, limit.max_text_chars),
|
|
23629
24322
|
agent_id: ledger.run.agent_id,
|
|
23630
24323
|
started_at: ledger.run.started_at,
|
|
23631
24324
|
completed_at: ledger.run.completed_at,
|
|
23632
|
-
events: ledger.events.map((event) => ({ event_type: event.event_type, message:
|
|
23633
|
-
commands: ledger.commands.map((command) => ({ command: command.command, status: command.status, output_summary:
|
|
23634
|
-
files: ledger.files.map((file) => ({ path: file.path, status: file.status, note:
|
|
23635
|
-
artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description:
|
|
24325
|
+
events: ledger.events.map((event) => ({ event_type: event.event_type, message: truncate2(event.message, 500), created_at: event.created_at })),
|
|
24326
|
+
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 })),
|
|
24327
|
+
files: ledger.files.map((file) => ({ path: file.path, status: file.status, note: truncate2(file.note, 240) })),
|
|
24328
|
+
artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description: truncate2(artifact.description, 240), sha256: artifact.sha256 }))
|
|
23636
24329
|
})),
|
|
23637
24330
|
omitted: Math.max(0, runs.length - selectedRuns.length)
|
|
23638
24331
|
},
|
|
@@ -23737,7 +24430,7 @@ function renderAgentContextPackCompactMarkdown(pack) {
|
|
|
23737
24430
|
const lines = [
|
|
23738
24431
|
`# Context: ${pack.task.title}`,
|
|
23739
24432
|
`${pack.task.status} | ${pack.task.priority} | ${pack.task.short_id || pack.task.id.slice(0, 8)}`,
|
|
23740
|
-
pack.task.description ?
|
|
24433
|
+
pack.task.description ? truncate2(pack.task.description, Math.min(pack.limits.summary_char_limit, 700)) : null,
|
|
23741
24434
|
"",
|
|
23742
24435
|
"## Must Know",
|
|
23743
24436
|
bullet([
|
|
@@ -24987,7 +25680,7 @@ var init_agent_run_dispatcher = __esm(() => {
|
|
|
24987
25680
|
});
|
|
24988
25681
|
|
|
24989
25682
|
// src/lib/verification-providers.ts
|
|
24990
|
-
import { existsSync as
|
|
25683
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
|
|
24991
25684
|
function normalizeName5(name) {
|
|
24992
25685
|
const normalized = name.trim().toLowerCase();
|
|
24993
25686
|
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
|
|
@@ -25139,7 +25832,7 @@ Timed out after ${provider.timeout_ms}ms`);
|
|
|
25139
25832
|
};
|
|
25140
25833
|
}
|
|
25141
25834
|
function runCiLogProvider(input) {
|
|
25142
|
-
const text = input.log_text ?? (input.log_path &&
|
|
25835
|
+
const text = input.log_text ?? (input.log_path && existsSync11(input.log_path) ? readFileSync6(input.log_path, "utf-8") : "");
|
|
25143
25836
|
return {
|
|
25144
25837
|
status: classifyLog(text),
|
|
25145
25838
|
attempts: 1,
|
|
@@ -25151,7 +25844,7 @@ function runBrowserProvider(input) {
|
|
|
25151
25844
|
if (!input.artifact_path) {
|
|
25152
25845
|
return { status: "unknown", attempts: 1, exit_code: null, output_summary: "browser provider needs a screenshot or artifact path" };
|
|
25153
25846
|
}
|
|
25154
|
-
if (!
|
|
25847
|
+
if (!existsSync11(input.artifact_path)) {
|
|
25155
25848
|
return { status: "failed", attempts: 1, exit_code: null, output_summary: `artifact not found: ${input.artifact_path}` };
|
|
25156
25849
|
}
|
|
25157
25850
|
return {
|
|
@@ -26941,12 +27634,12 @@ function summarizeTask(task) {
|
|
|
26941
27634
|
};
|
|
26942
27635
|
}
|
|
26943
27636
|
function overdueTasks(tasks, nowIso) {
|
|
26944
|
-
const
|
|
27637
|
+
const now4 = Date.parse(nowIso);
|
|
26945
27638
|
return tasks.filter((task) => {
|
|
26946
27639
|
if (isTerminal(task) || !task.due_at)
|
|
26947
27640
|
return false;
|
|
26948
27641
|
const due = Date.parse(task.due_at);
|
|
26949
|
-
return Number.isFinite(due) && due <
|
|
27642
|
+
return Number.isFinite(due) && due < now4;
|
|
26950
27643
|
});
|
|
26951
27644
|
}
|
|
26952
27645
|
function isReady(task, db) {
|
|
@@ -27296,15 +27989,15 @@ var init_local_reports = __esm(() => {
|
|
|
27296
27989
|
});
|
|
27297
27990
|
|
|
27298
27991
|
// src/lib/package-version.ts
|
|
27299
|
-
import { existsSync as
|
|
27300
|
-
import { dirname as dirname7, join as
|
|
27992
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
|
|
27993
|
+
import { dirname as dirname7, join as join9 } from "path";
|
|
27301
27994
|
import { fileURLToPath } from "url";
|
|
27302
27995
|
function getPackageVersion(fromUrl = import.meta.url) {
|
|
27303
27996
|
try {
|
|
27304
27997
|
let dir = dirname7(fileURLToPath(fromUrl));
|
|
27305
27998
|
for (let i = 0;i < 5; i++) {
|
|
27306
|
-
const pkgPath =
|
|
27307
|
-
if (
|
|
27999
|
+
const pkgPath = join9(dir, "package.json");
|
|
28000
|
+
if (existsSync12(pkgPath)) {
|
|
27308
28001
|
return JSON.parse(readFileSync7(pkgPath, "utf-8")).version || "0.0.0";
|
|
27309
28002
|
}
|
|
27310
28003
|
const parent = dirname7(dir);
|
|
@@ -29395,8 +30088,8 @@ __export(exports_local_extensions, {
|
|
|
29395
30088
|
discoverLocalExtensions: () => discoverLocalExtensions
|
|
29396
30089
|
});
|
|
29397
30090
|
import { createHash as createHash8, createVerify } from "crypto";
|
|
29398
|
-
import { existsSync as
|
|
29399
|
-
import { basename as basename5, join as
|
|
30091
|
+
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
|
|
30092
|
+
import { basename as basename5, join as join10, resolve as resolve12 } from "path";
|
|
29400
30093
|
function isObject2(value) {
|
|
29401
30094
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
29402
30095
|
}
|
|
@@ -29655,10 +30348,10 @@ function verifyExtensionSignature(input) {
|
|
|
29655
30348
|
}
|
|
29656
30349
|
function inspectExtensionSource(source3) {
|
|
29657
30350
|
const resolved = resolve12(source3);
|
|
29658
|
-
if (!
|
|
30351
|
+
if (!existsSync13(resolved))
|
|
29659
30352
|
throw new Error(`extension source not found: ${source3}`);
|
|
29660
30353
|
const stat = statSync6(resolved);
|
|
29661
|
-
const manifestPath = stat.isDirectory() ? [
|
|
30354
|
+
const manifestPath = stat.isDirectory() ? [join10(resolved, "todos.extension.json"), join10(resolved, "extension.json")].find(existsSync13) : resolved;
|
|
29662
30355
|
if (!manifestPath)
|
|
29663
30356
|
throw new Error(`extension directory ${source3} is missing todos.extension.json`);
|
|
29664
30357
|
const raw = readFileSync9(manifestPath);
|
|
@@ -29754,20 +30447,20 @@ function projectExtensionSources(projectPath) {
|
|
|
29754
30447
|
return [];
|
|
29755
30448
|
const root = resolve12(projectPath);
|
|
29756
30449
|
const candidates = [
|
|
29757
|
-
|
|
29758
|
-
|
|
30450
|
+
join10(root, "todos.extension.json"),
|
|
30451
|
+
join10(root, ".todos", "todos.extension.json")
|
|
29759
30452
|
];
|
|
29760
|
-
const extensionDir =
|
|
29761
|
-
if (
|
|
30453
|
+
const extensionDir = join10(root, ".todos", "extensions");
|
|
30454
|
+
if (existsSync13(extensionDir)) {
|
|
29762
30455
|
for (const entry of readdirSync3(extensionDir)) {
|
|
29763
30456
|
if (entry.startsWith("."))
|
|
29764
30457
|
continue;
|
|
29765
|
-
const full =
|
|
30458
|
+
const full = join10(extensionDir, entry);
|
|
29766
30459
|
if (statSync6(full).isDirectory() || entry.endsWith(".json"))
|
|
29767
30460
|
candidates.push(full);
|
|
29768
30461
|
}
|
|
29769
30462
|
}
|
|
29770
|
-
return candidates.filter(
|
|
30463
|
+
return candidates.filter(existsSync13);
|
|
29771
30464
|
}
|
|
29772
30465
|
function discoverLocalExtensions(options = {}) {
|
|
29773
30466
|
const config = loadConfig();
|
|
@@ -33289,7 +33982,7 @@ ${lines.join(`
|
|
|
33289
33982
|
const projectRoles = n.project_roles.length > 0 ? ` <${n.project_roles.join(", ")}>` : "";
|
|
33290
33983
|
const lead = n.is_project_lead ? " \u2605" : "";
|
|
33291
33984
|
const lastSeen = new Date(n.agent.last_seen_at).getTime();
|
|
33292
|
-
const active =
|
|
33985
|
+
const active = now4 - lastSeen < ACTIVE_MS ? " \u25CF" : " \u25CB";
|
|
33293
33986
|
const line = `${prefix}${active} ${n.agent.name}${title}${globalRole}${projectRoles}${lead}`;
|
|
33294
33987
|
const children = n.reports.length > 0 ? `
|
|
33295
33988
|
` + render(n.reports, indent + 1) : "";
|
|
@@ -33303,7 +33996,7 @@ ${lines.join(`
|
|
|
33303
33996
|
if (format === "json") {
|
|
33304
33997
|
return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) }] };
|
|
33305
33998
|
}
|
|
33306
|
-
const
|
|
33999
|
+
const now4 = Date.now();
|
|
33307
34000
|
const ACTIVE_MS = 30 * 60 * 1000;
|
|
33308
34001
|
const text2 = tree.length > 0 ? render(tree) : "No agents in this project's org chart.";
|
|
33309
34002
|
return { content: [{ type: "text", text: text2 }] };
|
|
@@ -33853,10 +34546,10 @@ ${lines.join(`
|
|
|
33853
34546
|
});
|
|
33854
34547
|
}
|
|
33855
34548
|
if (shouldRegisterTool("get_idle_focus_prompts")) {
|
|
33856
|
-
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:
|
|
34549
|
+
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 }) => {
|
|
33857
34550
|
try {
|
|
33858
34551
|
const { getIdleFocusSessionPrompts: getIdleFocusSessionPrompts2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
33859
|
-
return { content: [{ type: "text", text: JSON.stringify(getIdleFocusSessionPrompts2({ agent_id, now:
|
|
34552
|
+
return { content: [{ type: "text", text: JSON.stringify(getIdleFocusSessionPrompts2({ agent_id, now: now4 }), null, 2) }] };
|
|
33860
34553
|
} catch (e) {
|
|
33861
34554
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33862
34555
|
}
|
|
@@ -34051,9 +34744,9 @@ __export(exports_extract, {
|
|
|
34051
34744
|
buildCodebaseIndex: () => buildCodebaseIndex,
|
|
34052
34745
|
EXTRACT_TAGS: () => EXTRACT_TAGS
|
|
34053
34746
|
});
|
|
34054
|
-
import { existsSync as
|
|
34747
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
|
|
34055
34748
|
import { createHash as createHash10 } from "crypto";
|
|
34056
|
-
import { relative as relative5, resolve as resolve13, join as
|
|
34749
|
+
import { relative as relative5, resolve as resolve13, join as join11 } from "path";
|
|
34057
34750
|
function stableHash(value) {
|
|
34058
34751
|
return createHash10("sha256").update(value).digest("hex");
|
|
34059
34752
|
}
|
|
@@ -34062,8 +34755,8 @@ function normalizePathForMatch(value) {
|
|
|
34062
34755
|
}
|
|
34063
34756
|
function readGitignorePatterns(basePath) {
|
|
34064
34757
|
const root = statSync7(basePath).isFile() ? resolve13(basePath, "..") : basePath;
|
|
34065
|
-
const gitignorePath =
|
|
34066
|
-
if (!
|
|
34758
|
+
const gitignorePath = join11(root, ".gitignore");
|
|
34759
|
+
if (!existsSync14(gitignorePath))
|
|
34067
34760
|
return [];
|
|
34068
34761
|
try {
|
|
34069
34762
|
return readFileSync10(gitignorePath, "utf-8").split(`
|
|
@@ -34205,7 +34898,7 @@ function buildCodebaseIndex(options) {
|
|
|
34205
34898
|
const files = collectFiles(basePath, extensions, excludes, respectGitignore);
|
|
34206
34899
|
const indexed = [];
|
|
34207
34900
|
for (const file of files) {
|
|
34208
|
-
const fullPath = statSync7(basePath).isFile() ? basePath :
|
|
34901
|
+
const fullPath = statSync7(basePath).isFile() ? basePath : join11(basePath, file);
|
|
34209
34902
|
try {
|
|
34210
34903
|
const source3 = readFileSync10(fullPath, "utf-8");
|
|
34211
34904
|
const relPath = statSync7(basePath).isFile() ? relative5(resolve13(basePath, ".."), fullPath) : file;
|
|
@@ -34236,7 +34929,7 @@ function extractTodos(options, db) {
|
|
|
34236
34929
|
const files = collectFiles(basePath, extensions, excludes, respectGitignore);
|
|
34237
34930
|
const allComments = [];
|
|
34238
34931
|
for (const file of files) {
|
|
34239
|
-
const fullPath = statSync7(basePath).isFile() ? basePath :
|
|
34932
|
+
const fullPath = statSync7(basePath).isFile() ? basePath : join11(basePath, file);
|
|
34240
34933
|
try {
|
|
34241
34934
|
const source3 = readFileSync10(fullPath, "utf-8");
|
|
34242
34935
|
const relPath = statSync7(basePath).isFile() ? relative5(resolve13(basePath, ".."), fullPath) : file;
|
|
@@ -35129,7 +35822,7 @@ __export(exports_builtin_templates, {
|
|
|
35129
35822
|
BUILTIN_TEMPLATES: () => BUILTIN_TEMPLATES
|
|
35130
35823
|
});
|
|
35131
35824
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
35132
|
-
import { join as
|
|
35825
|
+
import { join as join12 } from "path";
|
|
35133
35826
|
function templateMetadata(template) {
|
|
35134
35827
|
return {
|
|
35135
35828
|
source: BUILTIN_TEMPLATE_LIBRARY_SOURCE,
|
|
@@ -35188,7 +35881,7 @@ function writeBuiltinTemplateFiles(directory) {
|
|
|
35188
35881
|
mkdirSync7(directory, { recursive: true });
|
|
35189
35882
|
const files = [];
|
|
35190
35883
|
for (const entry of exportBuiltinTemplateFiles()) {
|
|
35191
|
-
const path =
|
|
35884
|
+
const path = join12(directory, entry.filename);
|
|
35192
35885
|
writeFileSync4(path, `${JSON.stringify(entry.template, null, 2)}
|
|
35193
35886
|
`, "utf-8");
|
|
35194
35887
|
files.push(path);
|
|
@@ -35714,16 +36407,16 @@ __export(exports_environment_snapshots, {
|
|
|
35714
36407
|
captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
|
|
35715
36408
|
});
|
|
35716
36409
|
import { createHash as createHash11 } from "crypto";
|
|
35717
|
-
import { existsSync as
|
|
36410
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
|
|
35718
36411
|
import { hostname as hostname2, platform, arch } from "os";
|
|
35719
|
-
import { dirname as dirname9, join as
|
|
36412
|
+
import { dirname as dirname9, join as join13, resolve as resolve14 } from "path";
|
|
35720
36413
|
import { tmpdir as tmpdir2 } from "os";
|
|
35721
36414
|
function sha2566(value) {
|
|
35722
36415
|
return createHash11("sha256").update(value).digest("hex");
|
|
35723
36416
|
}
|
|
35724
36417
|
function fileRecord(root, relativePath) {
|
|
35725
|
-
const path =
|
|
35726
|
-
if (!
|
|
36418
|
+
const path = join13(root, relativePath);
|
|
36419
|
+
if (!existsSync15(path))
|
|
35727
36420
|
return null;
|
|
35728
36421
|
const stat = statSync8(path);
|
|
35729
36422
|
if (!stat.isFile())
|
|
@@ -35735,7 +36428,7 @@ function manifestRecord(root, relativePath) {
|
|
|
35735
36428
|
const base = fileRecord(root, relativePath);
|
|
35736
36429
|
if (!base)
|
|
35737
36430
|
return null;
|
|
35738
|
-
const parsed = readJsonFile(
|
|
36431
|
+
const parsed = readJsonFile(join13(root, relativePath));
|
|
35739
36432
|
if (!parsed)
|
|
35740
36433
|
return { ...base, redacted: {} };
|
|
35741
36434
|
const redacted = redactValue({
|
|
@@ -35830,8 +36523,8 @@ function commandEnv(env, includeValues) {
|
|
|
35830
36523
|
function defaultSnapshotDir() {
|
|
35831
36524
|
const dbPath = getDatabasePath();
|
|
35832
36525
|
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
35833
|
-
return
|
|
35834
|
-
return
|
|
36526
|
+
return join13(tmpdir2(), "hasna-todos", "environment-snapshots");
|
|
36527
|
+
return join13(dirname9(resolve14(dbPath)), "environment-snapshots");
|
|
35835
36528
|
}
|
|
35836
36529
|
function snapshotWithId(snapshot) {
|
|
35837
36530
|
const digest = sha2566(JSON.stringify(snapshot)).slice(0, 24);
|
|
@@ -35878,7 +36571,7 @@ function captureEnvironmentSnapshot(input = {}) {
|
|
|
35878
36571
|
});
|
|
35879
36572
|
}
|
|
35880
36573
|
function writeEnvironmentSnapshot(snapshot, outputPath) {
|
|
35881
|
-
const path = outputPath ? resolve14(outputPath) :
|
|
36574
|
+
const path = outputPath ? resolve14(outputPath) : join13(defaultSnapshotDir(), `${snapshot.id}.json`);
|
|
35882
36575
|
ensureDir2(dirname9(path));
|
|
35883
36576
|
writeJsonFile(path, snapshot);
|
|
35884
36577
|
return path;
|
|
@@ -36250,7 +36943,7 @@ var init_http = __esm(() => {
|
|
|
36250
36943
|
});
|
|
36251
36944
|
|
|
36252
36945
|
// src/db/api-keys.ts
|
|
36253
|
-
import { createHash as createHash12, randomBytes as randomBytes2, timingSafeEqual as
|
|
36946
|
+
import { createHash as createHash12, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
36254
36947
|
function rowToRecord(row) {
|
|
36255
36948
|
return {
|
|
36256
36949
|
id: row.id,
|
|
@@ -36269,7 +36962,7 @@ function hashApiKey(key) {
|
|
|
36269
36962
|
function safeEqualHex(a, b) {
|
|
36270
36963
|
if (a.length !== b.length)
|
|
36271
36964
|
return false;
|
|
36272
|
-
return
|
|
36965
|
+
return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
36273
36966
|
}
|
|
36274
36967
|
function hasActiveApiKeys(db) {
|
|
36275
36968
|
const d = db || getDatabase();
|
|
@@ -36430,7 +37123,7 @@ var init_headless_boundaries = __esm(() => {
|
|
|
36430
37123
|
});
|
|
36431
37124
|
|
|
36432
37125
|
// src/server/routes.ts
|
|
36433
|
-
import { join as
|
|
37126
|
+
import { join as join14, resolve as resolve15, sep as sep2 } from "path";
|
|
36434
37127
|
function parseFieldsParam(url) {
|
|
36435
37128
|
const fieldsParam = url.searchParams.get("fields");
|
|
36436
37129
|
return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
@@ -37134,7 +37827,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
37134
37827
|
if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
|
|
37135
37828
|
return null;
|
|
37136
37829
|
if (path !== "/") {
|
|
37137
|
-
const filePath =
|
|
37830
|
+
const filePath = join14(ctx.dashboardDir, path);
|
|
37138
37831
|
const resolvedFile = resolve15(filePath);
|
|
37139
37832
|
const resolvedBase = resolve15(ctx.dashboardDir);
|
|
37140
37833
|
if (!resolvedFile.startsWith(resolvedBase + sep2) && resolvedFile !== resolvedBase) {
|
|
@@ -37144,7 +37837,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
37144
37837
|
if (res2)
|
|
37145
37838
|
return res2;
|
|
37146
37839
|
}
|
|
37147
|
-
const indexPath =
|
|
37840
|
+
const indexPath = join14(ctx.dashboardDir, "index.html");
|
|
37148
37841
|
const res = serveStaticFile2(indexPath);
|
|
37149
37842
|
if (res)
|
|
37150
37843
|
return res;
|
|
@@ -37173,27 +37866,27 @@ __export(exports_serve, {
|
|
|
37173
37866
|
SECURITY_HEADERS: () => SECURITY_HEADERS,
|
|
37174
37867
|
MIME_TYPES: () => MIME_TYPES
|
|
37175
37868
|
});
|
|
37176
|
-
import { existsSync as
|
|
37177
|
-
import { join as
|
|
37869
|
+
import { existsSync as existsSync16 } from "fs";
|
|
37870
|
+
import { join as join15, dirname as dirname10, extname } from "path";
|
|
37178
37871
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
37179
37872
|
function resolveDashboardDir() {
|
|
37180
37873
|
const candidates = [];
|
|
37181
37874
|
try {
|
|
37182
37875
|
const scriptDir = dirname10(fileURLToPath2(import.meta.url));
|
|
37183
|
-
candidates.push(
|
|
37184
|
-
candidates.push(
|
|
37876
|
+
candidates.push(join15(scriptDir, "..", "dashboard", "dist"));
|
|
37877
|
+
candidates.push(join15(scriptDir, "..", "..", "dashboard", "dist"));
|
|
37185
37878
|
} catch {}
|
|
37186
37879
|
if (process.argv[1]) {
|
|
37187
37880
|
const mainDir = dirname10(process.argv[1]);
|
|
37188
|
-
candidates.push(
|
|
37189
|
-
candidates.push(
|
|
37881
|
+
candidates.push(join15(mainDir, "..", "dashboard", "dist"));
|
|
37882
|
+
candidates.push(join15(mainDir, "..", "..", "dashboard", "dist"));
|
|
37190
37883
|
}
|
|
37191
|
-
candidates.push(
|
|
37884
|
+
candidates.push(join15(process.cwd(), "dashboard", "dist"));
|
|
37192
37885
|
for (const candidate of candidates) {
|
|
37193
|
-
if (
|
|
37886
|
+
if (existsSync16(candidate))
|
|
37194
37887
|
return candidate;
|
|
37195
37888
|
}
|
|
37196
|
-
return
|
|
37889
|
+
return join15(process.cwd(), "dashboard", "dist");
|
|
37197
37890
|
}
|
|
37198
37891
|
function getProvidedApiKey(req) {
|
|
37199
37892
|
const headerKey = req.headers.get("x-api-key");
|
|
@@ -37220,15 +37913,15 @@ function checkAuth(req, apiKey) {
|
|
|
37220
37913
|
return null;
|
|
37221
37914
|
}
|
|
37222
37915
|
function checkRateLimit(ip) {
|
|
37223
|
-
const
|
|
37916
|
+
const now4 = Date.now();
|
|
37224
37917
|
const entry = rateLimitMap.get(ip);
|
|
37225
|
-
if (!entry ||
|
|
37226
|
-
rateLimitMap.set(ip, { count: 1, resetAt:
|
|
37918
|
+
if (!entry || now4 > entry.resetAt) {
|
|
37919
|
+
rateLimitMap.set(ip, { count: 1, resetAt: now4 + RATE_LIMIT_WINDOW_MS });
|
|
37227
37920
|
return { allowed: true };
|
|
37228
37921
|
}
|
|
37229
37922
|
entry.count++;
|
|
37230
37923
|
if (entry.count > RATE_LIMIT_MAX) {
|
|
37231
|
-
return { allowed: false, retryAfter: Math.ceil((entry.resetAt -
|
|
37924
|
+
return { allowed: false, retryAfter: Math.ceil((entry.resetAt - now4) / 1000) };
|
|
37232
37925
|
}
|
|
37233
37926
|
return { allowed: true };
|
|
37234
37927
|
}
|
|
@@ -37243,7 +37936,7 @@ function json(data, status = 200, headers) {
|
|
|
37243
37936
|
});
|
|
37244
37937
|
}
|
|
37245
37938
|
function serveStaticFile(filePath) {
|
|
37246
|
-
if (!
|
|
37939
|
+
if (!existsSync16(filePath))
|
|
37247
37940
|
return null;
|
|
37248
37941
|
const ext = extname(filePath);
|
|
37249
37942
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
@@ -37322,7 +38015,7 @@ data: ${data}
|
|
|
37322
38015
|
filteredSseClients.delete(client);
|
|
37323
38016
|
}
|
|
37324
38017
|
const dashboardDir = resolveDashboardDir();
|
|
37325
|
-
const dashboardExists =
|
|
38018
|
+
const dashboardExists = existsSync16(dashboardDir);
|
|
37326
38019
|
if (!dashboardExists) {
|
|
37327
38020
|
console.error(`
|
|
37328
38021
|
Dashboard not found at: ${dashboardDir}`);
|