@hasna/todos 0.11.56 → 0.11.58
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 +1887 -1124
- package/dist/contracts.d.ts +2 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +1408 -95
- 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 +1808 -374
- 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 +1053 -256
- package/dist/mcp/tools/dispatch.d.ts.map +1 -1
- package/dist/registry.js +1400 -95
- package/dist/release-provenance.json +7 -0
- package/dist/server/index.js +1049 -252
- package/dist/storage.js +845 -146
- package/package.json +2 -2
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,760 @@ 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.9/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, options = {}) {
|
|
8674
|
+
let body = "";
|
|
8675
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
8676
|
+
const char = pattern[index];
|
|
8677
|
+
if (char === "*") {
|
|
8678
|
+
if (pattern[index + 1] === "*") {
|
|
8679
|
+
body += ".*";
|
|
8680
|
+
index += 1;
|
|
8681
|
+
} else {
|
|
8682
|
+
body += options.segmentSafe ? "[^/]*" : ".*";
|
|
8683
|
+
}
|
|
8684
|
+
} else {
|
|
8685
|
+
body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
8686
|
+
}
|
|
8687
|
+
}
|
|
8688
|
+
return new RegExp(`^${body}$`);
|
|
8689
|
+
}
|
|
8690
|
+
function matchString(value, matcher, options = {}) {
|
|
8691
|
+
if (matcher === undefined)
|
|
8692
|
+
return true;
|
|
8693
|
+
if (value === undefined)
|
|
8694
|
+
return false;
|
|
8695
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
8696
|
+
return matchers.some((item) => wildcardToRegExp(item, options).test(value));
|
|
8697
|
+
}
|
|
8698
|
+
function matchRecord(input, matcher) {
|
|
8699
|
+
if (!matcher)
|
|
8700
|
+
return true;
|
|
8701
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
8702
|
+
const actual = getPathValue(input, path);
|
|
8703
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
8704
|
+
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
8705
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
8706
|
+
});
|
|
8707
|
+
}
|
|
8708
|
+
return actual === expected;
|
|
8709
|
+
});
|
|
8710
|
+
}
|
|
8711
|
+
function eventMatchesFilter(event, filter) {
|
|
8712
|
+
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);
|
|
8713
|
+
}
|
|
8714
|
+
function channelMatchesEvent(channel, event) {
|
|
8715
|
+
if (!channel.enabled)
|
|
8716
|
+
return false;
|
|
8717
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
8718
|
+
return true;
|
|
8719
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
8720
|
+
}
|
|
8721
|
+
function getEventsDataDir(override) {
|
|
8722
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir(), ".hasna", "events");
|
|
8723
|
+
}
|
|
8724
|
+
|
|
8725
|
+
class JsonEventsStore {
|
|
8726
|
+
dataDir;
|
|
8727
|
+
channelsPath;
|
|
8728
|
+
eventsPath;
|
|
8729
|
+
deliveriesPath;
|
|
8730
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
8731
|
+
this.dataDir = dataDir;
|
|
8732
|
+
this.channelsPath = join4(dataDir, "channels.json");
|
|
8733
|
+
this.eventsPath = join4(dataDir, "events.json");
|
|
8734
|
+
this.deliveriesPath = join4(dataDir, "deliveries.json");
|
|
8735
|
+
}
|
|
8736
|
+
async init() {
|
|
8737
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
8738
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
8739
|
+
return;
|
|
8740
|
+
});
|
|
8741
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
8742
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
8743
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
8744
|
+
}
|
|
8745
|
+
async addChannel(channel) {
|
|
8746
|
+
await this.init();
|
|
8747
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
8748
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
8749
|
+
if (index >= 0) {
|
|
8750
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
8751
|
+
} else {
|
|
8752
|
+
channels.push(channel);
|
|
8753
|
+
}
|
|
8754
|
+
await this.writeJson(this.channelsPath, channels);
|
|
8755
|
+
return index >= 0 ? channels[index] : channel;
|
|
8756
|
+
}
|
|
8757
|
+
async listChannels() {
|
|
8758
|
+
await this.init();
|
|
8759
|
+
return this.readJson(this.channelsPath, []);
|
|
8760
|
+
}
|
|
8761
|
+
async getChannel(id) {
|
|
8762
|
+
const channels = await this.listChannels();
|
|
8763
|
+
return channels.find((channel) => channel.id === id);
|
|
8764
|
+
}
|
|
8765
|
+
async removeChannel(id) {
|
|
8766
|
+
await this.init();
|
|
8767
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
8768
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
8769
|
+
await this.writeJson(this.channelsPath, next);
|
|
8770
|
+
return next.length !== channels.length;
|
|
8771
|
+
}
|
|
8772
|
+
async appendEvent(event) {
|
|
8773
|
+
await this.init();
|
|
8774
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
8775
|
+
events.push(event);
|
|
8776
|
+
await this.writeJson(this.eventsPath, events);
|
|
8777
|
+
return event;
|
|
8778
|
+
}
|
|
8779
|
+
async listEvents() {
|
|
8780
|
+
await this.init();
|
|
8781
|
+
return this.readJson(this.eventsPath, []);
|
|
8782
|
+
}
|
|
8783
|
+
async findEventByIdentity(identity) {
|
|
8784
|
+
const events = await this.listEvents();
|
|
8785
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
8786
|
+
}
|
|
8787
|
+
async appendDelivery(result) {
|
|
8788
|
+
await this.init();
|
|
8789
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
8790
|
+
deliveries.push(result);
|
|
8791
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
8792
|
+
return result;
|
|
8793
|
+
}
|
|
8794
|
+
async listDeliveries() {
|
|
8795
|
+
await this.init();
|
|
8796
|
+
return this.readJson(this.deliveriesPath, []);
|
|
8797
|
+
}
|
|
8798
|
+
async exportData() {
|
|
8799
|
+
return {
|
|
8800
|
+
channels: await this.listChannels(),
|
|
8801
|
+
events: await this.listEvents(),
|
|
8802
|
+
deliveries: await this.listDeliveries()
|
|
8803
|
+
};
|
|
8804
|
+
}
|
|
8805
|
+
async ensureArrayFile(path) {
|
|
8806
|
+
if (!existsSync5(path)) {
|
|
8807
|
+
await writeFile(path, `[]
|
|
8808
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
8809
|
+
}
|
|
8810
|
+
await chmod(path, 384).catch(() => {
|
|
8811
|
+
return;
|
|
8812
|
+
});
|
|
8813
|
+
}
|
|
8814
|
+
async readJson(path, fallback) {
|
|
8815
|
+
try {
|
|
8816
|
+
const raw = await readFile(path, "utf-8");
|
|
8817
|
+
if (!raw.trim())
|
|
8818
|
+
return fallback;
|
|
8819
|
+
return JSON.parse(raw);
|
|
8820
|
+
} catch (error) {
|
|
8821
|
+
if (error.code === "ENOENT")
|
|
8822
|
+
return fallback;
|
|
8823
|
+
throw error;
|
|
8824
|
+
}
|
|
8825
|
+
}
|
|
8826
|
+
async writeJson(path, value) {
|
|
8827
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
8828
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
8829
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
8830
|
+
await rename(tempPath, path);
|
|
8831
|
+
await chmod(path, 384).catch(() => {
|
|
8832
|
+
return;
|
|
8833
|
+
});
|
|
8834
|
+
}
|
|
8835
|
+
}
|
|
8836
|
+
function buildSignatureBase(timestamp, body) {
|
|
8837
|
+
return `${timestamp}.${body}`;
|
|
8838
|
+
}
|
|
8839
|
+
function signPayload(secret, timestamp, body) {
|
|
8840
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
8841
|
+
return `sha256=${digest}`;
|
|
8842
|
+
}
|
|
8843
|
+
function now2() {
|
|
8844
|
+
return new Date().toISOString();
|
|
8845
|
+
}
|
|
8846
|
+
function truncate(value, max = 4096) {
|
|
8847
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
8848
|
+
}
|
|
8849
|
+
function buildWebhookRequest(event, channel) {
|
|
8850
|
+
if (!channel.webhook)
|
|
8851
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
8852
|
+
const body = JSON.stringify(event);
|
|
8853
|
+
const timestamp = event.time;
|
|
8854
|
+
const headers = {
|
|
8855
|
+
"Content-Type": "application/json",
|
|
8856
|
+
"User-Agent": "@hasna/events",
|
|
8857
|
+
"X-Hasna-Event-Id": event.id,
|
|
8858
|
+
"X-Hasna-Event-Type": event.type,
|
|
8859
|
+
"X-Hasna-Timestamp": timestamp,
|
|
8860
|
+
...channel.webhook.headers
|
|
8861
|
+
};
|
|
8862
|
+
if (channel.webhook.secret) {
|
|
8863
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
8864
|
+
}
|
|
8865
|
+
return { body, headers };
|
|
8866
|
+
}
|
|
8867
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
8868
|
+
if (!channel.webhook)
|
|
8869
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
8870
|
+
const startedAt = now2();
|
|
8871
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
8872
|
+
const controller = new AbortController;
|
|
8873
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
8874
|
+
try {
|
|
8875
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
8876
|
+
method: "POST",
|
|
8877
|
+
headers,
|
|
8878
|
+
body,
|
|
8879
|
+
signal: controller.signal
|
|
8880
|
+
});
|
|
8881
|
+
const responseBody = truncate(await response.text());
|
|
8882
|
+
return {
|
|
8883
|
+
attempt: 1,
|
|
8884
|
+
status: response.ok ? "success" : "failed",
|
|
8885
|
+
startedAt,
|
|
8886
|
+
completedAt: now2(),
|
|
8887
|
+
responseStatus: response.status,
|
|
8888
|
+
responseBody,
|
|
8889
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
8890
|
+
};
|
|
8891
|
+
} catch (error) {
|
|
8892
|
+
return {
|
|
8893
|
+
attempt: 1,
|
|
8894
|
+
status: "failed",
|
|
8895
|
+
startedAt,
|
|
8896
|
+
completedAt: now2(),
|
|
8897
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8898
|
+
};
|
|
8899
|
+
} finally {
|
|
8900
|
+
clearTimeout(timeout);
|
|
8901
|
+
}
|
|
8902
|
+
}
|
|
8903
|
+
async function dispatchCommand(event, channel) {
|
|
8904
|
+
if (!channel.command)
|
|
8905
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
8906
|
+
const startedAt = now2();
|
|
8907
|
+
const eventJson = JSON.stringify(event);
|
|
8908
|
+
const env = {
|
|
8909
|
+
...process.env,
|
|
8910
|
+
...channel.command.env,
|
|
8911
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
8912
|
+
HASNA_EVENT_ID: event.id,
|
|
8913
|
+
HASNA_EVENT_TYPE: event.type,
|
|
8914
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
8915
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
8916
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
8917
|
+
HASNA_EVENT_TIME: event.time,
|
|
8918
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
8919
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
8920
|
+
HASNA_EVENT_JSON: eventJson
|
|
8921
|
+
};
|
|
8922
|
+
return new Promise((resolve6) => {
|
|
8923
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
8924
|
+
cwd: channel.command.cwd,
|
|
8925
|
+
env,
|
|
8926
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
8927
|
+
});
|
|
8928
|
+
let stdout = "";
|
|
8929
|
+
let stderr = "";
|
|
8930
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
8931
|
+
child.stdin.end(eventJson);
|
|
8932
|
+
child.stdout.on("data", (chunk) => {
|
|
8933
|
+
stdout += chunk.toString();
|
|
8934
|
+
});
|
|
8935
|
+
child.stderr.on("data", (chunk) => {
|
|
8936
|
+
stderr += chunk.toString();
|
|
8937
|
+
});
|
|
8938
|
+
child.on("error", (error) => {
|
|
8939
|
+
clearTimeout(timeout);
|
|
8940
|
+
resolve6({
|
|
8941
|
+
attempt: 1,
|
|
8942
|
+
status: "failed",
|
|
8943
|
+
startedAt,
|
|
8944
|
+
completedAt: now2(),
|
|
8945
|
+
stdout: truncate(stdout),
|
|
8946
|
+
stderr: truncate(stderr),
|
|
8947
|
+
error: error.message
|
|
8948
|
+
});
|
|
8949
|
+
});
|
|
8950
|
+
child.on("close", (code, signal) => {
|
|
8951
|
+
clearTimeout(timeout);
|
|
8952
|
+
const success = code === 0;
|
|
8953
|
+
resolve6({
|
|
8954
|
+
attempt: 1,
|
|
8955
|
+
status: success ? "success" : "failed",
|
|
8956
|
+
startedAt,
|
|
8957
|
+
completedAt: now2(),
|
|
8958
|
+
stdout: truncate(stdout),
|
|
8959
|
+
stderr: truncate(stderr),
|
|
8960
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
8961
|
+
});
|
|
8962
|
+
});
|
|
8963
|
+
});
|
|
8964
|
+
}
|
|
8965
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
8966
|
+
if (channel.transport === "webhook")
|
|
8967
|
+
return dispatchWebhook(event, channel, options);
|
|
8968
|
+
if (channel.transport === "command")
|
|
8969
|
+
return dispatchCommand(event, channel);
|
|
8970
|
+
return {
|
|
8971
|
+
attempt: 1,
|
|
8972
|
+
status: "skipped",
|
|
8973
|
+
startedAt: now2(),
|
|
8974
|
+
completedAt: now2(),
|
|
8975
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
8976
|
+
};
|
|
8977
|
+
}
|
|
8978
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
8979
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
8980
|
+
return {
|
|
8981
|
+
id: randomUUID2(),
|
|
8982
|
+
eventId: event.id,
|
|
8983
|
+
channelId: channel.id,
|
|
8984
|
+
transport: channel.transport,
|
|
8985
|
+
status,
|
|
8986
|
+
attempts,
|
|
8987
|
+
createdAt: attempts[0]?.startedAt ?? now2(),
|
|
8988
|
+
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
8989
|
+
};
|
|
8990
|
+
}
|
|
8991
|
+
function createEvent(input) {
|
|
8992
|
+
return {
|
|
8993
|
+
id: input.id ?? randomUUID22(),
|
|
8994
|
+
source: input.source,
|
|
8995
|
+
type: input.type,
|
|
8996
|
+
time: normalizeTime(input.time),
|
|
8997
|
+
subject: input.subject,
|
|
8998
|
+
severity: input.severity ?? "info",
|
|
8999
|
+
data: input.data ?? {},
|
|
9000
|
+
message: input.message,
|
|
9001
|
+
dedupeKey: input.dedupeKey,
|
|
9002
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
9003
|
+
metadata: input.metadata ?? {}
|
|
9004
|
+
};
|
|
9005
|
+
}
|
|
9006
|
+
|
|
9007
|
+
class EventsClient {
|
|
9008
|
+
store;
|
|
9009
|
+
redactors;
|
|
9010
|
+
transportOptions;
|
|
9011
|
+
constructor(options = {}) {
|
|
9012
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
9013
|
+
this.redactors = options.redactors ?? [];
|
|
9014
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
9015
|
+
}
|
|
9016
|
+
async addChannel(input) {
|
|
9017
|
+
const timestamp = new Date().toISOString();
|
|
9018
|
+
return this.store.addChannel({
|
|
9019
|
+
...input,
|
|
9020
|
+
createdAt: input.createdAt ?? timestamp,
|
|
9021
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
9022
|
+
});
|
|
9023
|
+
}
|
|
9024
|
+
async listChannels() {
|
|
9025
|
+
return this.store.listChannels();
|
|
9026
|
+
}
|
|
9027
|
+
async removeChannel(id) {
|
|
9028
|
+
return this.store.removeChannel(id);
|
|
9029
|
+
}
|
|
9030
|
+
async emit(input, options = {}) {
|
|
9031
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
9032
|
+
if (options.dedupe !== false) {
|
|
9033
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
9034
|
+
if (existing) {
|
|
9035
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
9036
|
+
}
|
|
9037
|
+
}
|
|
9038
|
+
await this.store.appendEvent(event);
|
|
9039
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
9040
|
+
return { event, deliveries, deduped: false };
|
|
9041
|
+
}
|
|
9042
|
+
async listEvents() {
|
|
9043
|
+
return this.store.listEvents();
|
|
9044
|
+
}
|
|
9045
|
+
async listDeliveries() {
|
|
9046
|
+
return this.store.listDeliveries();
|
|
9047
|
+
}
|
|
9048
|
+
async deliver(event) {
|
|
9049
|
+
const channels = await this.store.listChannels();
|
|
9050
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
9051
|
+
const deliveries = [];
|
|
9052
|
+
for (const channel of selected) {
|
|
9053
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
9054
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
9055
|
+
await this.store.appendDelivery(result);
|
|
9056
|
+
deliveries.push(result);
|
|
9057
|
+
}
|
|
9058
|
+
return deliveries;
|
|
9059
|
+
}
|
|
9060
|
+
async matchChannel(id, input = {}) {
|
|
9061
|
+
const channel = await this.store.getChannel(id);
|
|
9062
|
+
if (!channel)
|
|
9063
|
+
throw new Error(`Channel not found: ${id}`);
|
|
9064
|
+
const event = createEvent({
|
|
9065
|
+
source: input.source ?? "hasna.events",
|
|
9066
|
+
type: input.type ?? "events.test",
|
|
9067
|
+
subject: input.subject ?? id,
|
|
9068
|
+
severity: input.severity ?? "info",
|
|
9069
|
+
data: input.data ?? { test: true },
|
|
9070
|
+
message: input.message ?? "Hasna events test delivery",
|
|
9071
|
+
dedupeKey: input.dedupeKey,
|
|
9072
|
+
schemaVersion: input.schemaVersion,
|
|
9073
|
+
metadata: input.metadata,
|
|
9074
|
+
time: input.time,
|
|
9075
|
+
id: input.id
|
|
9076
|
+
});
|
|
9077
|
+
const matched = channelMatchesEvent(channel, event);
|
|
9078
|
+
return {
|
|
9079
|
+
channelId: channel.id,
|
|
9080
|
+
matched,
|
|
9081
|
+
event,
|
|
9082
|
+
filters: channel.filters,
|
|
9083
|
+
reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
|
|
9084
|
+
};
|
|
9085
|
+
}
|
|
9086
|
+
async testChannel(id, input = {}, options = {}) {
|
|
9087
|
+
const channel = await this.store.getChannel(id);
|
|
9088
|
+
if (!channel)
|
|
9089
|
+
throw new Error(`Channel not found: ${id}`);
|
|
9090
|
+
const match = await this.matchChannel(id, input);
|
|
9091
|
+
const event = match.event;
|
|
9092
|
+
if (options.honorFilters && !match.matched) {
|
|
9093
|
+
const timestamp = new Date().toISOString();
|
|
9094
|
+
const result2 = createDeliveryResult(event, channel, [{
|
|
9095
|
+
attempt: 1,
|
|
9096
|
+
status: "skipped",
|
|
9097
|
+
startedAt: timestamp,
|
|
9098
|
+
completedAt: timestamp,
|
|
9099
|
+
error: match.reason
|
|
9100
|
+
}]);
|
|
9101
|
+
result2.metadata = { reason: "filter_mismatch" };
|
|
9102
|
+
await this.store.appendDelivery(result2);
|
|
9103
|
+
return result2;
|
|
9104
|
+
}
|
|
9105
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
9106
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
9107
|
+
await this.store.appendDelivery(result);
|
|
9108
|
+
return result;
|
|
9109
|
+
}
|
|
9110
|
+
async replay(options = {}) {
|
|
9111
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
9112
|
+
if (options.eventId && event.id !== options.eventId)
|
|
9113
|
+
return false;
|
|
9114
|
+
if (options.source && event.source !== options.source)
|
|
9115
|
+
return false;
|
|
9116
|
+
if (options.type && event.type !== options.type)
|
|
9117
|
+
return false;
|
|
9118
|
+
return true;
|
|
9119
|
+
});
|
|
9120
|
+
if (options.dryRun)
|
|
9121
|
+
return { events, deliveries: [] };
|
|
9122
|
+
const deliveries = [];
|
|
9123
|
+
for (const event of events) {
|
|
9124
|
+
deliveries.push(...await this.deliver(event));
|
|
9125
|
+
}
|
|
9126
|
+
return { events, deliveries };
|
|
9127
|
+
}
|
|
9128
|
+
async applyRedaction(event, channel) {
|
|
9129
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
9130
|
+
for (const redactor of this.redactors) {
|
|
9131
|
+
next = await redactor(next, channel);
|
|
9132
|
+
}
|
|
9133
|
+
return next;
|
|
9134
|
+
}
|
|
9135
|
+
async deliverWithRetry(event, channel) {
|
|
9136
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
9137
|
+
const attempts = [];
|
|
9138
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
9139
|
+
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
9140
|
+
attempt.attempt = index + 1;
|
|
9141
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
9142
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
9143
|
+
}
|
|
9144
|
+
attempts.push(attempt);
|
|
9145
|
+
if (attempt.status !== "failed")
|
|
9146
|
+
break;
|
|
9147
|
+
if (attempt.nextBackoffMs)
|
|
9148
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
9149
|
+
}
|
|
9150
|
+
return createDeliveryResult(event, channel, attempts);
|
|
9151
|
+
}
|
|
9152
|
+
}
|
|
9153
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
9154
|
+
if (paths.length === 0)
|
|
9155
|
+
return event;
|
|
9156
|
+
const copy = structuredClone(event);
|
|
9157
|
+
for (const path of paths) {
|
|
9158
|
+
setPath(copy, path, replacement);
|
|
9159
|
+
}
|
|
9160
|
+
return copy;
|
|
9161
|
+
}
|
|
9162
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
9163
|
+
return redactValue2(event, replacement);
|
|
9164
|
+
}
|
|
9165
|
+
function shouldRedactKey(key) {
|
|
9166
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
9167
|
+
}
|
|
9168
|
+
function redactValue2(value, replacement) {
|
|
9169
|
+
if (Array.isArray(value))
|
|
9170
|
+
return value.map((item) => redactValue2(item, replacement));
|
|
9171
|
+
if (!value || typeof value !== "object")
|
|
9172
|
+
return value;
|
|
9173
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
9174
|
+
key,
|
|
9175
|
+
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
9176
|
+
]));
|
|
9177
|
+
}
|
|
9178
|
+
function setPath(input, path, replacement) {
|
|
9179
|
+
const parts = path.split(".");
|
|
9180
|
+
let cursor = input;
|
|
9181
|
+
for (const part of parts.slice(0, -1)) {
|
|
9182
|
+
const next = cursor[part];
|
|
9183
|
+
if (!next || typeof next !== "object")
|
|
9184
|
+
return;
|
|
9185
|
+
cursor = next;
|
|
9186
|
+
}
|
|
9187
|
+
const last = parts.at(-1);
|
|
9188
|
+
if (last && last in cursor)
|
|
9189
|
+
cursor[last] = replacement;
|
|
9190
|
+
}
|
|
9191
|
+
function normalizeTime(value) {
|
|
9192
|
+
if (!value)
|
|
9193
|
+
return new Date().toISOString();
|
|
9194
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
9195
|
+
}
|
|
9196
|
+
function normalizeRetryPolicy(policy) {
|
|
9197
|
+
return {
|
|
9198
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
9199
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
9200
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
9201
|
+
};
|
|
9202
|
+
}
|
|
9203
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
|
|
9204
|
+
var init_dist = __esm(() => {
|
|
9205
|
+
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
9206
|
+
});
|
|
9207
|
+
|
|
9208
|
+
// src/db/task-lists.ts
|
|
9209
|
+
function rowToTaskList(row) {
|
|
9210
|
+
return {
|
|
9211
|
+
...row,
|
|
9212
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
9213
|
+
};
|
|
9214
|
+
}
|
|
9215
|
+
function createTaskList(input, db) {
|
|
9216
|
+
const d = db || getDatabase();
|
|
9217
|
+
const id = uuid();
|
|
9218
|
+
const timestamp = now();
|
|
9219
|
+
const slug = input.slug || slugify(input.name);
|
|
9220
|
+
if (!input.project_id) {
|
|
9221
|
+
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
9222
|
+
if (existing) {
|
|
9223
|
+
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
9224
|
+
}
|
|
9225
|
+
}
|
|
9226
|
+
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
9227
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
9228
|
+
return getTaskList(id, d);
|
|
9229
|
+
}
|
|
9230
|
+
function getTaskList(id, db) {
|
|
9231
|
+
const d = db || getDatabase();
|
|
9232
|
+
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
9233
|
+
return row ? rowToTaskList(row) : null;
|
|
9234
|
+
}
|
|
9235
|
+
function getTaskListBySlug(slug, projectId, db) {
|
|
9236
|
+
const d = db || getDatabase();
|
|
9237
|
+
let row;
|
|
9238
|
+
if (projectId) {
|
|
9239
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
9240
|
+
} else {
|
|
9241
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
9242
|
+
}
|
|
9243
|
+
return row ? rowToTaskList(row) : null;
|
|
9244
|
+
}
|
|
9245
|
+
function listTaskLists(projectId, db) {
|
|
9246
|
+
const d = db || getDatabase();
|
|
9247
|
+
if (projectId) {
|
|
9248
|
+
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
9249
|
+
}
|
|
9250
|
+
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
9251
|
+
}
|
|
9252
|
+
function updateTaskList(id, input, db) {
|
|
9253
|
+
const d = db || getDatabase();
|
|
9254
|
+
const existing = getTaskList(id, d);
|
|
9255
|
+
if (!existing)
|
|
9256
|
+
throw new TaskListNotFoundError(id);
|
|
9257
|
+
const sets = ["updated_at = ?"];
|
|
9258
|
+
const params = [now()];
|
|
9259
|
+
if (input.name !== undefined) {
|
|
9260
|
+
sets.push("name = ?");
|
|
9261
|
+
params.push(input.name);
|
|
9262
|
+
}
|
|
9263
|
+
if (input.description !== undefined) {
|
|
9264
|
+
sets.push("description = ?");
|
|
9265
|
+
params.push(input.description);
|
|
9266
|
+
}
|
|
9267
|
+
if (input.metadata !== undefined) {
|
|
9268
|
+
sets.push("metadata = ?");
|
|
9269
|
+
params.push(JSON.stringify(input.metadata));
|
|
9270
|
+
}
|
|
9271
|
+
params.push(id);
|
|
9272
|
+
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
9273
|
+
return getTaskList(id, d);
|
|
9274
|
+
}
|
|
9275
|
+
function deleteTaskList(id, db) {
|
|
9276
|
+
const d = db || getDatabase();
|
|
9277
|
+
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
9278
|
+
}
|
|
9279
|
+
function ensureTaskList(name, slug, projectId, db) {
|
|
9280
|
+
const d = db || getDatabase();
|
|
9281
|
+
const existing = getTaskListBySlug(slug, projectId, d);
|
|
9282
|
+
if (existing)
|
|
9283
|
+
return existing;
|
|
9284
|
+
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
9285
|
+
}
|
|
9286
|
+
var init_task_lists = __esm(() => {
|
|
9287
|
+
init_types();
|
|
9288
|
+
init_database();
|
|
9289
|
+
init_projects();
|
|
9290
|
+
});
|
|
9291
|
+
|
|
9292
|
+
// src/lib/shared-events.ts
|
|
9293
|
+
function taskEventData(task, extra = {}) {
|
|
9294
|
+
return {
|
|
9295
|
+
id: task.id,
|
|
9296
|
+
task_id: task.id,
|
|
9297
|
+
short_id: task.short_id,
|
|
9298
|
+
title: task.title,
|
|
9299
|
+
description: task.description,
|
|
9300
|
+
status: task.status,
|
|
9301
|
+
priority: task.priority,
|
|
9302
|
+
project_id: task.project_id,
|
|
9303
|
+
parent_id: task.parent_id,
|
|
9304
|
+
plan_id: task.plan_id,
|
|
9305
|
+
task_list_id: task.task_list_id,
|
|
9306
|
+
agent_id: task.agent_id,
|
|
9307
|
+
assigned_to: task.assigned_to,
|
|
9308
|
+
session_id: task.session_id,
|
|
9309
|
+
working_dir: task.working_dir,
|
|
9310
|
+
tags: task.tags,
|
|
9311
|
+
metadata: task.metadata,
|
|
9312
|
+
version: task.version,
|
|
9313
|
+
created_at: task.created_at,
|
|
9314
|
+
updated_at: task.updated_at,
|
|
9315
|
+
started_at: task.started_at,
|
|
9316
|
+
completed_at: task.completed_at,
|
|
9317
|
+
due_at: task.due_at,
|
|
9318
|
+
...extra
|
|
9319
|
+
};
|
|
9320
|
+
}
|
|
9321
|
+
function taskEventMetadata(task) {
|
|
9322
|
+
const metadata = {
|
|
9323
|
+
package: "@hasna/todos",
|
|
9324
|
+
todos_event_schema_version: 1,
|
|
9325
|
+
task_id: task.id,
|
|
9326
|
+
task_short_id: task.short_id,
|
|
9327
|
+
project_id: task.project_id,
|
|
9328
|
+
task_list_id: task.task_list_id,
|
|
9329
|
+
working_dir: task.working_dir
|
|
9330
|
+
};
|
|
9331
|
+
try {
|
|
9332
|
+
const project = task.project_id ? getProject(task.project_id) : null;
|
|
9333
|
+
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
9334
|
+
if (project) {
|
|
9335
|
+
metadata.project_id = project.id;
|
|
9336
|
+
metadata.project_name = project.name;
|
|
9337
|
+
metadata.project_path = projectPath;
|
|
9338
|
+
metadata.project_canonical_path = project.path;
|
|
9339
|
+
metadata.project_default_task_list_slug = project.task_list_id;
|
|
9340
|
+
metadata.root_project_id = inferRootProjectId(project);
|
|
9341
|
+
} else if (projectPath) {
|
|
9342
|
+
metadata.project_path = projectPath;
|
|
9343
|
+
metadata.project_canonical_path = projectPath;
|
|
9344
|
+
}
|
|
9345
|
+
if (projectPath) {
|
|
9346
|
+
metadata.project_kind = classifyProjectKind(projectPath);
|
|
9347
|
+
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
9348
|
+
if (typeof task.metadata.route_enabled === "boolean") {
|
|
9349
|
+
metadata.route_enabled = task.metadata.route_enabled;
|
|
9350
|
+
}
|
|
9351
|
+
metadata.working_dir = task.working_dir ?? projectPath;
|
|
9352
|
+
}
|
|
9353
|
+
const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
|
|
9354
|
+
if (taskList) {
|
|
9355
|
+
metadata.task_list_id = taskList.id;
|
|
9356
|
+
metadata.task_list_slug = taskList.slug;
|
|
9357
|
+
metadata.task_list_name = taskList.name;
|
|
9358
|
+
metadata.task_list_project_id = taskList.project_id;
|
|
9359
|
+
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
9360
|
+
}
|
|
9361
|
+
} catch {}
|
|
9362
|
+
return metadata;
|
|
9363
|
+
}
|
|
9364
|
+
function classifyProjectKind(path) {
|
|
9365
|
+
return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
|
|
9366
|
+
}
|
|
9367
|
+
function isWorktreePath(path) {
|
|
9368
|
+
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
9369
|
+
}
|
|
9370
|
+
function inferRootProjectId(project) {
|
|
9371
|
+
return isWorktreePath(project.path) ? null : project.id;
|
|
9372
|
+
}
|
|
9373
|
+
function readMachineLocalPath(project) {
|
|
9374
|
+
const machineId = process.env["TODOS_MACHINE_ID"];
|
|
9375
|
+
if (!machineId)
|
|
9376
|
+
return null;
|
|
9377
|
+
try {
|
|
9378
|
+
const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
|
|
9379
|
+
return row?.path ?? null;
|
|
9380
|
+
} catch {
|
|
9381
|
+
return null;
|
|
9382
|
+
}
|
|
9383
|
+
}
|
|
9384
|
+
async function emitSharedTaskEvent(input) {
|
|
9385
|
+
const data = taskEventData(input.task, input.data);
|
|
9386
|
+
await new EventsClient().emit({
|
|
9387
|
+
source: SOURCE,
|
|
9388
|
+
type: input.type,
|
|
9389
|
+
subject: input.task.id,
|
|
9390
|
+
severity: input.severity ?? "info",
|
|
9391
|
+
message: input.message ?? `${input.type}: ${input.task.title}`,
|
|
9392
|
+
data,
|
|
9393
|
+
dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
|
|
9394
|
+
metadata: taskEventMetadata(input.task)
|
|
9395
|
+
}, { deliver: true, dedupe: true });
|
|
9396
|
+
}
|
|
9397
|
+
function emitSharedTaskEventQuiet(input) {
|
|
9398
|
+
emitSharedTaskEvent(input).catch(() => {
|
|
9399
|
+
return;
|
|
9400
|
+
});
|
|
9401
|
+
}
|
|
9402
|
+
var SOURCE = "todos";
|
|
9403
|
+
var init_shared_events = __esm(() => {
|
|
9404
|
+
init_dist();
|
|
9405
|
+
init_database();
|
|
9406
|
+
init_projects();
|
|
9407
|
+
init_task_lists();
|
|
9408
|
+
});
|
|
9409
|
+
|
|
8655
9410
|
// src/lib/secret-redaction.ts
|
|
8656
9411
|
function isAllowlisted(text, match, allowlist) {
|
|
8657
9412
|
const context = text.slice(Math.max(0, text.indexOf(match) - 20), text.indexOf(match) + match.length + 20);
|
|
@@ -9119,7 +9874,7 @@ async function deliverWebhook(wh, event, body, attempt, db) {
|
|
|
9119
9874
|
activeDeliveries--;
|
|
9120
9875
|
}
|
|
9121
9876
|
}
|
|
9122
|
-
async function
|
|
9877
|
+
async function dispatchWebhook2(event, payload, db) {
|
|
9123
9878
|
const d = db || getDatabase();
|
|
9124
9879
|
const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
|
|
9125
9880
|
const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
|
|
@@ -9236,7 +9991,10 @@ function createTask(input, db) {
|
|
|
9236
9991
|
insertTaskTags(id, tags, d);
|
|
9237
9992
|
}
|
|
9238
9993
|
const task = getTask(id, d);
|
|
9239
|
-
|
|
9994
|
+
const payload = taskEventData(task);
|
|
9995
|
+
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
9996
|
+
emitLocalEventHooksQuiet({ type: "task.created", payload });
|
|
9997
|
+
emitSharedTaskEventQuiet({ type: "task.created", task });
|
|
9240
9998
|
return task;
|
|
9241
9999
|
}
|
|
9242
10000
|
function getTask(id, db) {
|
|
@@ -9580,18 +10338,7 @@ function updateTask(id, input, db) {
|
|
|
9580
10338
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
9581
10339
|
if (input.approved_by !== undefined)
|
|
9582
10340
|
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 {
|
|
10341
|
+
const updatedTask = {
|
|
9595
10342
|
...task,
|
|
9596
10343
|
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
9597
10344
|
tags: input.tags ?? task.tags,
|
|
@@ -9609,6 +10356,22 @@ function updateTask(id, input, db) {
|
|
|
9609
10356
|
approved_by: input.approved_by ?? task.approved_by,
|
|
9610
10357
|
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
9611
10358
|
};
|
|
10359
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
10360
|
+
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
10361
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
10362
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
10363
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
|
|
10364
|
+
}
|
|
10365
|
+
if (input.status !== undefined && input.status !== task.status) {
|
|
10366
|
+
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
10367
|
+
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
10368
|
+
emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
|
|
10369
|
+
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
|
|
10370
|
+
}
|
|
10371
|
+
if (input.approved_by !== undefined) {
|
|
10372
|
+
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
10373
|
+
}
|
|
10374
|
+
return updatedTask;
|
|
9612
10375
|
}
|
|
9613
10376
|
function deleteTask(id, db) {
|
|
9614
10377
|
const d = db || getDatabase();
|
|
@@ -9620,6 +10383,7 @@ var init_task_crud = __esm(() => {
|
|
|
9620
10383
|
init_database();
|
|
9621
10384
|
init_completion_guard();
|
|
9622
10385
|
init_event_hooks();
|
|
10386
|
+
init_shared_events();
|
|
9623
10387
|
init_audit();
|
|
9624
10388
|
init_webhooks();
|
|
9625
10389
|
init_checklists();
|
|
@@ -10375,9 +11139,12 @@ function startTask(id, agentId, db) {
|
|
|
10375
11139
|
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
10376
11140
|
}
|
|
10377
11141
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
11142
|
+
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 };
|
|
11143
|
+
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
11144
|
+
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
11145
|
+
emitLocalEventHooksQuiet({ type: "task.started", payload });
|
|
11146
|
+
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
|
|
11147
|
+
return startedTask;
|
|
10381
11148
|
}
|
|
10382
11149
|
function completeTask(id, agentId, db, options) {
|
|
10383
11150
|
const d = db || getDatabase();
|
|
@@ -10413,8 +11180,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
10413
11180
|
});
|
|
10414
11181
|
tx();
|
|
10415
11182
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
10416
|
-
|
|
10417
|
-
|
|
11183
|
+
const completedTaskForEvent = {
|
|
11184
|
+
...task,
|
|
11185
|
+
status: "completed",
|
|
11186
|
+
locked_by: null,
|
|
11187
|
+
locked_at: null,
|
|
11188
|
+
completed_at: timestamp,
|
|
11189
|
+
confidence,
|
|
11190
|
+
version: task.version + 1,
|
|
11191
|
+
updated_at: timestamp,
|
|
11192
|
+
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
11193
|
+
};
|
|
11194
|
+
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
11195
|
+
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
11196
|
+
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
|
|
11197
|
+
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
|
|
10418
11198
|
let spawnedTask = null;
|
|
10419
11199
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
10420
11200
|
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
@@ -10455,8 +11235,12 @@ function completeTask(id, agentId, db, options) {
|
|
|
10455
11235
|
if (unblockedDeps.length > 0) {
|
|
10456
11236
|
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
10457
11237
|
for (const dep of unblockedDeps) {
|
|
10458
|
-
|
|
10459
|
-
|
|
11238
|
+
const depTask = getTask(dep.id, d);
|
|
11239
|
+
const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
|
|
11240
|
+
dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
|
|
11241
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
|
|
11242
|
+
if (depTask)
|
|
11243
|
+
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
|
|
10460
11244
|
}
|
|
10461
11245
|
}
|
|
10462
11246
|
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 +11426,6 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
10642
11426
|
const timestamp = now();
|
|
10643
11427
|
d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
10644
11428
|
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
11429
|
const failedTask = {
|
|
10649
11430
|
...task,
|
|
10650
11431
|
status: "failed",
|
|
@@ -10654,6 +11435,11 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
10654
11435
|
version: task.version + 1,
|
|
10655
11436
|
updated_at: timestamp
|
|
10656
11437
|
};
|
|
11438
|
+
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
11439
|
+
const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
|
|
11440
|
+
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
11441
|
+
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
|
|
11442
|
+
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
|
|
10657
11443
|
let retryTask;
|
|
10658
11444
|
if (options?.retry) {
|
|
10659
11445
|
const retryCount = (task.retry_count || 0) + 1;
|
|
@@ -10728,9 +11514,12 @@ function stealTask(agentId, opts, db) {
|
|
|
10728
11514
|
return null;
|
|
10729
11515
|
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
10730
11516
|
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
11517
|
+
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
11518
|
+
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
11519
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
11520
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
11521
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
|
|
11522
|
+
return stolenTask;
|
|
10734
11523
|
}
|
|
10735
11524
|
function claimOrSteal(agentId, filters, db) {
|
|
10736
11525
|
const d = db || getDatabase();
|
|
@@ -10778,6 +11567,7 @@ var init_task_lifecycle = __esm(() => {
|
|
|
10778
11567
|
init_database();
|
|
10779
11568
|
init_completion_guard();
|
|
10780
11569
|
init_event_hooks();
|
|
11570
|
+
init_shared_events();
|
|
10781
11571
|
init_audit();
|
|
10782
11572
|
init_recurrence();
|
|
10783
11573
|
init_webhooks();
|
|
@@ -11465,7 +12255,7 @@ function getTaskWatchers(taskId, db) {
|
|
|
11465
12255
|
}
|
|
11466
12256
|
function notifyWatchers(taskId, event, data, db) {
|
|
11467
12257
|
const watchers = getTaskWatchers(taskId, db);
|
|
11468
|
-
|
|
12258
|
+
dispatchWebhook2(`task.watcher.${event}`, { task_id: taskId, watchers: watchers.map((w) => w.agent_id), ...data }, db).catch(() => {});
|
|
11469
12259
|
}
|
|
11470
12260
|
function logCost(taskId, tokens, usd, db) {
|
|
11471
12261
|
const d = db || getDatabase();
|
|
@@ -11959,8 +12749,8 @@ var init_boards = __esm(() => {
|
|
|
11959
12749
|
|
|
11960
12750
|
// src/lib/artifact-store.ts
|
|
11961
12751
|
import { createHash as createHash2 } from "crypto";
|
|
11962
|
-
import { existsSync as
|
|
11963
|
-
import { basename, dirname as dirname4, join as
|
|
12752
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
12753
|
+
import { basename, dirname as dirname4, join as join5, resolve as resolve6 } from "path";
|
|
11964
12754
|
import { tmpdir } from "os";
|
|
11965
12755
|
function isInMemoryDb2(path) {
|
|
11966
12756
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
@@ -11972,15 +12762,15 @@ function artifactStoreRoot() {
|
|
|
11972
12762
|
return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
11973
12763
|
const dbPath = getDatabasePath();
|
|
11974
12764
|
if (isInMemoryDb2(dbPath))
|
|
11975
|
-
return
|
|
11976
|
-
return
|
|
12765
|
+
return join5(tmpdir(), "hasna-todos-artifacts");
|
|
12766
|
+
return join5(dirname4(resolve6(dbPath)), "artifacts");
|
|
11977
12767
|
}
|
|
11978
12768
|
function artifactStorePath(relativePath) {
|
|
11979
12769
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
11980
12770
|
if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
|
|
11981
12771
|
throw new Error("Invalid artifact store path");
|
|
11982
12772
|
}
|
|
11983
|
-
return
|
|
12773
|
+
return join5(artifactStoreRoot(), normalized);
|
|
11984
12774
|
}
|
|
11985
12775
|
function sha256(buffer) {
|
|
11986
12776
|
return createHash2("sha256").update(buffer).digest("hex");
|
|
@@ -12021,7 +12811,7 @@ function mediaTypeFor(path, textLike) {
|
|
|
12021
12811
|
}
|
|
12022
12812
|
function storeArtifactContent(input) {
|
|
12023
12813
|
const sourcePath = resolve6(input.path);
|
|
12024
|
-
if (!
|
|
12814
|
+
if (!existsSync6(sourcePath))
|
|
12025
12815
|
return null;
|
|
12026
12816
|
const sourceStat = statSync2(sourcePath);
|
|
12027
12817
|
if (!sourceStat.isFile())
|
|
@@ -12038,9 +12828,9 @@ function storeArtifactContent(input) {
|
|
|
12038
12828
|
redactionStatus = "redacted";
|
|
12039
12829
|
}
|
|
12040
12830
|
const storedSha = sha256(storedBuffer);
|
|
12041
|
-
const relativePath =
|
|
12831
|
+
const relativePath = join5("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
|
|
12042
12832
|
const destination = artifactStorePath(relativePath);
|
|
12043
|
-
if (!
|
|
12833
|
+
if (!existsSync6(destination)) {
|
|
12044
12834
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
12045
12835
|
writeFileSync2(destination, storedBuffer);
|
|
12046
12836
|
}
|
|
@@ -12100,7 +12890,7 @@ function verifyStoredArtifact(input) {
|
|
|
12100
12890
|
};
|
|
12101
12891
|
}
|
|
12102
12892
|
const storedPath = artifactStorePath(store.relative_path);
|
|
12103
|
-
if (!
|
|
12893
|
+
if (!existsSync6(storedPath)) {
|
|
12104
12894
|
return {
|
|
12105
12895
|
id: input.id,
|
|
12106
12896
|
path: input.path,
|
|
@@ -13385,17 +14175,73 @@ var init_dispatch_formatter = __esm(() => {
|
|
|
13385
14175
|
});
|
|
13386
14176
|
|
|
13387
14177
|
// src/lib/tmux.ts
|
|
14178
|
+
async function inspectTmuxPane(target) {
|
|
14179
|
+
const proc = Bun.spawn([
|
|
14180
|
+
"tmux",
|
|
14181
|
+
"display-message",
|
|
14182
|
+
"-p",
|
|
14183
|
+
"-t",
|
|
14184
|
+
target,
|
|
14185
|
+
"#{pane_id}\t#{pane_current_command}\t#{pane_dead}\t#{pane_input_off}\t#{pane_in_mode}"
|
|
14186
|
+
], {
|
|
14187
|
+
stdout: "pipe",
|
|
14188
|
+
stderr: "pipe"
|
|
14189
|
+
});
|
|
14190
|
+
const exitCode = await proc.exited;
|
|
14191
|
+
if (exitCode !== 0) {
|
|
14192
|
+
const stderr = await new Response(proc.stderr).text();
|
|
14193
|
+
throw new Error(`tmux target "${target}" not found: ${stderr.trim() || "unknown error"}`);
|
|
14194
|
+
}
|
|
14195
|
+
const stdout = (await new Response(proc.stdout).text()).trim();
|
|
14196
|
+
const [paneId, currentCommand = "", paneDead = "0", inputOff = "0", inMode = "0"] = stdout.split("\t");
|
|
14197
|
+
if (!paneId) {
|
|
14198
|
+
throw new Error(`tmux target "${target}" did not resolve to a pane`);
|
|
14199
|
+
}
|
|
14200
|
+
return {
|
|
14201
|
+
target,
|
|
14202
|
+
paneId,
|
|
14203
|
+
currentCommand,
|
|
14204
|
+
paneDead: paneDead === "1",
|
|
14205
|
+
inputOff: inputOff === "1",
|
|
14206
|
+
inMode: inMode === "1"
|
|
14207
|
+
};
|
|
14208
|
+
}
|
|
14209
|
+
function tmuxPaneBusyStatus(pane) {
|
|
14210
|
+
if (pane.paneDead) {
|
|
14211
|
+
return { busy: true, reason: "pane is dead" };
|
|
14212
|
+
}
|
|
14213
|
+
if (pane.inputOff) {
|
|
14214
|
+
return { busy: true, reason: "pane input is disabled" };
|
|
14215
|
+
}
|
|
14216
|
+
if (pane.inMode) {
|
|
14217
|
+
return { busy: true, reason: "pane is in copy or alternate mode" };
|
|
14218
|
+
}
|
|
14219
|
+
const currentCommand = pane.currentCommand.trim();
|
|
14220
|
+
if (currentCommand && !IDLE_TMUX_COMMANDS.has(currentCommand)) {
|
|
14221
|
+
return { busy: true, reason: `pane is running ${currentCommand}` };
|
|
14222
|
+
}
|
|
14223
|
+
return { busy: false, reason: null };
|
|
14224
|
+
}
|
|
13388
14225
|
function calculateDelay(message) {
|
|
13389
14226
|
const len = message.length;
|
|
13390
14227
|
const extra = Math.floor(len / 100 * 40);
|
|
13391
14228
|
return Math.min(DELAY_MIN + extra, DELAY_MAX);
|
|
13392
14229
|
}
|
|
13393
|
-
async function sendToTmux(target, message, delayMs,
|
|
14230
|
+
async function sendToTmux(target, message, delayMs, options = false) {
|
|
14231
|
+
const opts = typeof options === "boolean" ? { dryRun: options } : options;
|
|
14232
|
+
const dryRun = opts.dryRun ?? false;
|
|
13394
14233
|
if (dryRun) {
|
|
13395
14234
|
console.log(`[dry-run] sendToTmux target=${target} delay=${delayMs}ms`);
|
|
13396
14235
|
console.log(`[dry-run] message: ${message.slice(0, 200)}`);
|
|
13397
14236
|
return;
|
|
13398
14237
|
}
|
|
14238
|
+
if (!opts.confirmBusy) {
|
|
14239
|
+
const pane = await inspectTmuxPane(target);
|
|
14240
|
+
const status = tmuxPaneBusyStatus(pane);
|
|
14241
|
+
if (status.busy) {
|
|
14242
|
+
throw new Error(`tmux target "${target}" appears busy (${status.reason}). Re-run with --confirm-busy to send anyway.`);
|
|
14243
|
+
}
|
|
14244
|
+
}
|
|
13399
14245
|
const sendProc = Bun.spawn(["tmux", "send-keys", "-t", target, message, ""], {
|
|
13400
14246
|
stdout: "pipe",
|
|
13401
14247
|
stderr: "pipe"
|
|
@@ -13416,7 +14262,21 @@ async function sendToTmux(target, message, delayMs, dryRun = false) {
|
|
|
13416
14262
|
throw new Error(`tmux send-keys Enter failed for target "${target}": ${stderr.trim()}`);
|
|
13417
14263
|
}
|
|
13418
14264
|
}
|
|
13419
|
-
var DELAY_MIN = 3000, DELAY_MAX = 5000;
|
|
14265
|
+
var DELAY_MIN = 3000, DELAY_MAX = 5000, IDLE_TMUX_COMMANDS;
|
|
14266
|
+
var init_tmux = __esm(() => {
|
|
14267
|
+
IDLE_TMUX_COMMANDS = new Set([
|
|
14268
|
+
"bash",
|
|
14269
|
+
"dash",
|
|
14270
|
+
"elvish",
|
|
14271
|
+
"fish",
|
|
14272
|
+
"ksh",
|
|
14273
|
+
"nu",
|
|
14274
|
+
"pwsh",
|
|
14275
|
+
"sh",
|
|
14276
|
+
"tmux",
|
|
14277
|
+
"zsh"
|
|
14278
|
+
]);
|
|
14279
|
+
});
|
|
13420
14280
|
|
|
13421
14281
|
// src/lib/dispatch.ts
|
|
13422
14282
|
var exports_dispatch = {};
|
|
@@ -13434,7 +14294,10 @@ async function executeDispatch(dispatch, opts = {}, db) {
|
|
|
13434
14294
|
}
|
|
13435
14295
|
const delayMs = dispatch.delay_ms ?? calculateDelay(message);
|
|
13436
14296
|
try {
|
|
13437
|
-
await sendToTmux(dispatch.target_window, message, delayMs,
|
|
14297
|
+
await sendToTmux(dispatch.target_window, message, delayMs, {
|
|
14298
|
+
dryRun: opts.dryRun ?? false,
|
|
14299
|
+
confirmBusy: opts.confirmBusy ?? false
|
|
14300
|
+
});
|
|
13438
14301
|
createDispatchLog({
|
|
13439
14302
|
dispatch_id: dispatch.id,
|
|
13440
14303
|
target_window: dispatch.target_window,
|
|
@@ -13493,90 +14356,7 @@ var init_dispatch = __esm(() => {
|
|
|
13493
14356
|
init_tasks();
|
|
13494
14357
|
init_database();
|
|
13495
14358
|
init_dispatch_formatter();
|
|
13496
|
-
|
|
13497
|
-
|
|
13498
|
-
// src/db/task-lists.ts
|
|
13499
|
-
function rowToTaskList(row) {
|
|
13500
|
-
return {
|
|
13501
|
-
...row,
|
|
13502
|
-
metadata: JSON.parse(row.metadata || "{}")
|
|
13503
|
-
};
|
|
13504
|
-
}
|
|
13505
|
-
function createTaskList(input, db) {
|
|
13506
|
-
const d = db || getDatabase();
|
|
13507
|
-
const id = uuid();
|
|
13508
|
-
const timestamp = now();
|
|
13509
|
-
const slug = input.slug || slugify(input.name);
|
|
13510
|
-
if (!input.project_id) {
|
|
13511
|
-
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
13512
|
-
if (existing) {
|
|
13513
|
-
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
13514
|
-
}
|
|
13515
|
-
}
|
|
13516
|
-
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
13517
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
13518
|
-
return getTaskList(id, d);
|
|
13519
|
-
}
|
|
13520
|
-
function getTaskList(id, db) {
|
|
13521
|
-
const d = db || getDatabase();
|
|
13522
|
-
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
13523
|
-
return row ? rowToTaskList(row) : null;
|
|
13524
|
-
}
|
|
13525
|
-
function getTaskListBySlug(slug, projectId, db) {
|
|
13526
|
-
const d = db || getDatabase();
|
|
13527
|
-
let row;
|
|
13528
|
-
if (projectId) {
|
|
13529
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
13530
|
-
} else {
|
|
13531
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
13532
|
-
}
|
|
13533
|
-
return row ? rowToTaskList(row) : null;
|
|
13534
|
-
}
|
|
13535
|
-
function listTaskLists(projectId, db) {
|
|
13536
|
-
const d = db || getDatabase();
|
|
13537
|
-
if (projectId) {
|
|
13538
|
-
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
13539
|
-
}
|
|
13540
|
-
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
13541
|
-
}
|
|
13542
|
-
function updateTaskList(id, input, db) {
|
|
13543
|
-
const d = db || getDatabase();
|
|
13544
|
-
const existing = getTaskList(id, d);
|
|
13545
|
-
if (!existing)
|
|
13546
|
-
throw new TaskListNotFoundError(id);
|
|
13547
|
-
const sets = ["updated_at = ?"];
|
|
13548
|
-
const params = [now()];
|
|
13549
|
-
if (input.name !== undefined) {
|
|
13550
|
-
sets.push("name = ?");
|
|
13551
|
-
params.push(input.name);
|
|
13552
|
-
}
|
|
13553
|
-
if (input.description !== undefined) {
|
|
13554
|
-
sets.push("description = ?");
|
|
13555
|
-
params.push(input.description);
|
|
13556
|
-
}
|
|
13557
|
-
if (input.metadata !== undefined) {
|
|
13558
|
-
sets.push("metadata = ?");
|
|
13559
|
-
params.push(JSON.stringify(input.metadata));
|
|
13560
|
-
}
|
|
13561
|
-
params.push(id);
|
|
13562
|
-
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
13563
|
-
return getTaskList(id, d);
|
|
13564
|
-
}
|
|
13565
|
-
function deleteTaskList(id, db) {
|
|
13566
|
-
const d = db || getDatabase();
|
|
13567
|
-
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
13568
|
-
}
|
|
13569
|
-
function ensureTaskList(name, slug, projectId, db) {
|
|
13570
|
-
const d = db || getDatabase();
|
|
13571
|
-
const existing = getTaskListBySlug(slug, projectId, d);
|
|
13572
|
-
if (existing)
|
|
13573
|
-
return existing;
|
|
13574
|
-
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
13575
|
-
}
|
|
13576
|
-
var init_task_lists = __esm(() => {
|
|
13577
|
-
init_types();
|
|
13578
|
-
init_database();
|
|
13579
|
-
init_projects();
|
|
14359
|
+
init_tmux();
|
|
13580
14360
|
});
|
|
13581
14361
|
|
|
13582
14362
|
// src/mcp/tools/dispatch.ts
|
|
@@ -13587,8 +14367,9 @@ function registerDispatchTools(server, { shouldRegisterTool, resolveId, formatEr
|
|
|
13587
14367
|
target: exports_external.string().describe("tmux target \u2014 window name, session:window, or session:window.pane"),
|
|
13588
14368
|
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
14369
|
scheduled_at: exports_external.string().optional().describe("ISO datetime to schedule the dispatch for. Fires immediately if omitted."),
|
|
14370
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if the target tmux pane appears busy. Default: false."),
|
|
13590
14371
|
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 }) => {
|
|
14372
|
+
}, async ({ task_ids, target, delay_ms, scheduled_at, confirm_busy, dry_run }) => {
|
|
13592
14373
|
try {
|
|
13593
14374
|
const db = getDatabase();
|
|
13594
14375
|
const resolvedIds = task_ids.map((id) => resolveId(id));
|
|
@@ -13604,7 +14385,7 @@ ${message}` }]
|
|
|
13604
14385
|
}
|
|
13605
14386
|
const dispatch = createDispatch({ task_ids: resolvedIds, target_window: target, message, delay_ms: effectiveDelay, scheduled_at }, db);
|
|
13606
14387
|
if (!scheduled_at)
|
|
13607
|
-
await executeDispatch(dispatch, {}, db);
|
|
14388
|
+
await executeDispatch(dispatch, { confirmBusy: confirm_busy ?? false }, db);
|
|
13608
14389
|
return {
|
|
13609
14390
|
content: [{
|
|
13610
14391
|
type: "text",
|
|
@@ -13630,8 +14411,9 @@ ${message}`
|
|
|
13630
14411
|
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
14412
|
delay_ms: exports_external.number().optional().describe("Delay in ms between sending and Enter. Auto-calculated if omitted."),
|
|
13632
14413
|
scheduled_at: exports_external.string().optional().describe("ISO datetime to schedule. Fires immediately if omitted."),
|
|
14414
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if the target tmux pane appears busy. Default: false."),
|
|
13633
14415
|
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 }) => {
|
|
14416
|
+
}, async ({ task_list_id, target, filter_status, delay_ms, scheduled_at, confirm_busy, dry_run }) => {
|
|
13635
14417
|
try {
|
|
13636
14418
|
const db = getDatabase();
|
|
13637
14419
|
const resolvedListId = resolveId(task_list_id, "task_lists");
|
|
@@ -13651,7 +14433,7 @@ ${message}` }]
|
|
|
13651
14433
|
}
|
|
13652
14434
|
const dispatch = createDispatch({ title: `Task list: ${taskList.name}`, task_list_id: resolvedListId, target_window: target, message, delay_ms: effectiveDelay, scheduled_at }, db);
|
|
13653
14435
|
if (!scheduled_at)
|
|
13654
|
-
await executeDispatch(dispatch, {}, db);
|
|
14436
|
+
await executeDispatch(dispatch, { confirmBusy: confirm_busy ?? false }, db);
|
|
13655
14437
|
return {
|
|
13656
14438
|
content: [{
|
|
13657
14439
|
type: "text",
|
|
@@ -13678,15 +14460,16 @@ ${message}`
|
|
|
13678
14460
|
task_list_id: exports_external.string().optional().describe("Task list ID to dispatch (use this or task_ids)"),
|
|
13679
14461
|
stagger_ms: exports_external.number().optional().describe("Delay between each window dispatch. Default: 500ms."),
|
|
13680
14462
|
delay_ms: exports_external.number().optional().describe("Delay between message send and Enter. Auto-calculated if omitted."),
|
|
14463
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
|
|
13681
14464
|
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 }) => {
|
|
14465
|
+
}, async ({ targets, task_ids, task_list_id, stagger_ms, delay_ms, confirm_busy, dry_run }) => {
|
|
13683
14466
|
try {
|
|
13684
14467
|
if (!task_ids && !task_list_id)
|
|
13685
14468
|
throw new Error("Either task_ids or task_list_id is required");
|
|
13686
14469
|
const db = getDatabase();
|
|
13687
14470
|
const resolvedTaskIds = task_ids ? task_ids.map((id) => resolveId(id)) : undefined;
|
|
13688
14471
|
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);
|
|
14472
|
+
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
14473
|
const lines = dispatches.map((d) => `${d.target_window}: ${d.id} [${d.status}]`);
|
|
13691
14474
|
return { content: [{ type: "text", text: `Dispatched to ${dispatches.length} target(s):
|
|
13692
14475
|
${lines.join(`
|
|
@@ -13731,11 +14514,12 @@ ${lines.join(`
|
|
|
13731
14514
|
if (shouldRegisterTool("run_due_dispatches")) {
|
|
13732
14515
|
server.tool("run_due_dispatches", "Manually trigger all pending dispatches that are due (scheduled_at <= now). Returns the count fired.", {
|
|
13733
14516
|
dry_run: exports_external.boolean().optional().describe("Preview without sending. Default: false."),
|
|
14517
|
+
confirm_busy: exports_external.boolean().optional().describe("Send even if target tmux panes appear busy. Default: false."),
|
|
13734
14518
|
all: exports_external.boolean().optional().describe("Ignore scheduled_at and fire all pending dispatches immediately.")
|
|
13735
|
-
}, async ({ dry_run }) => {
|
|
14519
|
+
}, async ({ dry_run, confirm_busy }) => {
|
|
13736
14520
|
try {
|
|
13737
14521
|
const { runDueDispatches: runDueDispatches2 } = await Promise.resolve().then(() => (init_dispatch(), exports_dispatch));
|
|
13738
|
-
const count = await runDueDispatches2({ dryRun: dry_run });
|
|
14522
|
+
const count = await runDueDispatches2({ dryRun: dry_run, confirmBusy: confirm_busy ?? false });
|
|
13739
14523
|
return { content: [{ type: "text", text: `Fired ${count} dispatch(es).` }] };
|
|
13740
14524
|
} catch (e) {
|
|
13741
14525
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
@@ -13748,6 +14532,7 @@ var init_dispatch2 = __esm(() => {
|
|
|
13748
14532
|
init_dispatches();
|
|
13749
14533
|
init_dispatch();
|
|
13750
14534
|
init_dispatch_formatter();
|
|
14535
|
+
init_tmux();
|
|
13751
14536
|
init_tasks();
|
|
13752
14537
|
init_task_lists();
|
|
13753
14538
|
init_database();
|
|
@@ -14555,7 +15340,7 @@ var init_task_crud2 = __esm(() => {
|
|
|
14555
15340
|
});
|
|
14556
15341
|
|
|
14557
15342
|
// src/lib/project-bootstrap.ts
|
|
14558
|
-
import { existsSync as
|
|
15343
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
14559
15344
|
import { basename as basename2, dirname as dirname5, resolve as resolve7 } from "path";
|
|
14560
15345
|
function safeStat(path) {
|
|
14561
15346
|
try {
|
|
@@ -14574,7 +15359,7 @@ function canonicalPath(input) {
|
|
|
14574
15359
|
function findUp(start, marker) {
|
|
14575
15360
|
let current = canonicalPath(start);
|
|
14576
15361
|
while (true) {
|
|
14577
|
-
if (
|
|
15362
|
+
if (existsSync7(resolve7(current, marker)))
|
|
14578
15363
|
return current;
|
|
14579
15364
|
const parent = dirname5(current);
|
|
14580
15365
|
if (parent === current)
|
|
@@ -14586,7 +15371,7 @@ function readPackageJson(path) {
|
|
|
14586
15371
|
if (!path)
|
|
14587
15372
|
return null;
|
|
14588
15373
|
const file = resolve7(path, "package.json");
|
|
14589
|
-
if (!
|
|
15374
|
+
if (!existsSync7(file))
|
|
14590
15375
|
return null;
|
|
14591
15376
|
try {
|
|
14592
15377
|
const parsed = JSON.parse(readFileSync3(file, "utf-8"));
|
|
@@ -14608,7 +15393,7 @@ function workspaceMarker(root, rootPackage) {
|
|
|
14608
15393
|
if (rootPackage?.workspaces)
|
|
14609
15394
|
markers.push("package.json#workspaces");
|
|
14610
15395
|
for (const marker of ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json", "rush.json", "bun.lock", "bun.lockb"]) {
|
|
14611
|
-
if (
|
|
15396
|
+
if (existsSync7(resolve7(root, marker)))
|
|
14612
15397
|
markers.push(marker);
|
|
14613
15398
|
}
|
|
14614
15399
|
const kind = markers.find((marker) => marker !== "bun.lock" && marker !== "bun.lockb") ?? null;
|
|
@@ -14909,7 +15694,7 @@ var init_tags = __esm(() => {
|
|
|
14909
15694
|
});
|
|
14910
15695
|
|
|
14911
15696
|
// src/lib/retention-cleanup.ts
|
|
14912
|
-
import { existsSync as
|
|
15697
|
+
import { existsSync as existsSync8, unlinkSync } from "fs";
|
|
14913
15698
|
function normalizeScopes(scopes) {
|
|
14914
15699
|
if (!scopes || scopes.length === 0)
|
|
14915
15700
|
return [...ALL_SCOPES];
|
|
@@ -15112,7 +15897,7 @@ function applyRetentionCleanup(input, db) {
|
|
|
15112
15897
|
for (const artifact of report.candidates.artifact_files) {
|
|
15113
15898
|
try {
|
|
15114
15899
|
const path = artifactStorePath(artifact.relative_path);
|
|
15115
|
-
if (!
|
|
15900
|
+
if (!existsSync8(path)) {
|
|
15116
15901
|
report.warnings.push(`stored artifact already missing: ${artifact.relative_path}`);
|
|
15117
15902
|
continue;
|
|
15118
15903
|
}
|
|
@@ -15139,8 +15924,8 @@ var init_retention_cleanup = __esm(() => {
|
|
|
15139
15924
|
});
|
|
15140
15925
|
|
|
15141
15926
|
// src/lib/mention-resolver.ts
|
|
15142
|
-
import { existsSync as
|
|
15143
|
-
import { basename as basename3, isAbsolute, join as
|
|
15927
|
+
import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
15928
|
+
import { basename as basename3, isAbsolute, join as join6, relative as relative3, resolve as resolve8, sep } from "path";
|
|
15144
15929
|
function blankResolution(parsed) {
|
|
15145
15930
|
return {
|
|
15146
15931
|
input: parsed.input,
|
|
@@ -15238,7 +16023,7 @@ function resolveFile(parsed, workspace) {
|
|
|
15238
16023
|
return resolution;
|
|
15239
16024
|
}
|
|
15240
16025
|
resolution.path = relPath;
|
|
15241
|
-
if (!
|
|
16026
|
+
if (!existsSync9(absolutePath)) {
|
|
15242
16027
|
resolution.warnings.push("file does not exist in the local workspace");
|
|
15243
16028
|
return resolution;
|
|
15244
16029
|
}
|
|
@@ -15271,7 +16056,7 @@ function walkSourceFiles(root, current = root, files = []) {
|
|
|
15271
16056
|
if (SKIP_DIRS.has(entry.name))
|
|
15272
16057
|
continue;
|
|
15273
16058
|
}
|
|
15274
|
-
const absolutePath =
|
|
16059
|
+
const absolutePath = join6(current, entry.name);
|
|
15275
16060
|
if (entry.isDirectory()) {
|
|
15276
16061
|
if (!SKIP_DIRS.has(entry.name))
|
|
15277
16062
|
walkSourceFiles(root, absolutePath, files);
|
|
@@ -16538,8 +17323,8 @@ var init_local_notifications = __esm(() => {
|
|
|
16538
17323
|
});
|
|
16539
17324
|
|
|
16540
17325
|
// src/lib/local-encryption.ts
|
|
16541
|
-
import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes, scryptSync, timingSafeEqual } from "crypto";
|
|
16542
|
-
function
|
|
17326
|
+
import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
17327
|
+
function now3() {
|
|
16543
17328
|
return new Date().toISOString();
|
|
16544
17329
|
}
|
|
16545
17330
|
function sha2562(value) {
|
|
@@ -16576,7 +17361,7 @@ function upsertEncryptionProfile(input) {
|
|
|
16576
17361
|
const name = normalizeProfileName(input.name);
|
|
16577
17362
|
const config = loadConfig();
|
|
16578
17363
|
const existing = config.encryption_profiles?.[name];
|
|
16579
|
-
const timestamp =
|
|
17364
|
+
const timestamp = now3();
|
|
16580
17365
|
const profile = {
|
|
16581
17366
|
name,
|
|
16582
17367
|
algorithm: "aes-256-gcm",
|
|
@@ -16633,7 +17418,7 @@ function encryptString(plaintext, options = {}) {
|
|
|
16633
17418
|
return {
|
|
16634
17419
|
schemaVersion: TODOS_ENCRYPTION_SCHEMA_VERSION,
|
|
16635
17420
|
kind: TODOS_ENCRYPTED_VALUE_KIND,
|
|
16636
|
-
encryptedAt: options.encryptedAt ??
|
|
17421
|
+
encryptedAt: options.encryptedAt ?? now3(),
|
|
16637
17422
|
profile: profile.name,
|
|
16638
17423
|
key_env: profile.key_env,
|
|
16639
17424
|
algorithm: "aes-256-gcm",
|
|
@@ -16668,7 +17453,7 @@ function decryptString(envelope, env = process.env) {
|
|
|
16668
17453
|
]).toString("utf8");
|
|
16669
17454
|
const expected = Buffer.from(envelope.plaintext_sha256, "hex");
|
|
16670
17455
|
const actual = Buffer.from(sha2562(plaintext), "hex");
|
|
16671
|
-
if (expected.length !== actual.length || !
|
|
17456
|
+
if (expected.length !== actual.length || !timingSafeEqual2(expected, actual)) {
|
|
16672
17457
|
throw new EncryptedPayloadError("decrypted payload checksum mismatch");
|
|
16673
17458
|
}
|
|
16674
17459
|
return plaintext;
|
|
@@ -18191,7 +18976,7 @@ function createRoadmap(input) {
|
|
|
18191
18976
|
if (!name)
|
|
18192
18977
|
throw new Error("Roadmap name is required");
|
|
18193
18978
|
const store = readStore();
|
|
18194
|
-
const
|
|
18979
|
+
const now4 = timestamp();
|
|
18195
18980
|
const roadmap = {
|
|
18196
18981
|
id: newId("roadmap"),
|
|
18197
18982
|
name,
|
|
@@ -18202,8 +18987,8 @@ function createRoadmap(input) {
|
|
|
18202
18987
|
agent_id: cleanString(input.agent_id),
|
|
18203
18988
|
release: cleanString(input.release),
|
|
18204
18989
|
milestone_ids: [],
|
|
18205
|
-
created_at:
|
|
18206
|
-
updated_at:
|
|
18990
|
+
created_at: now4,
|
|
18991
|
+
updated_at: now4
|
|
18207
18992
|
};
|
|
18208
18993
|
store.roadmaps[roadmap.id] = roadmap;
|
|
18209
18994
|
writeStore(store);
|
|
@@ -18255,7 +19040,7 @@ function createMilestone(input) {
|
|
|
18255
19040
|
const title = input.title.trim();
|
|
18256
19041
|
if (!title)
|
|
18257
19042
|
throw new Error("Milestone title is required");
|
|
18258
|
-
const
|
|
19043
|
+
const now4 = timestamp();
|
|
18259
19044
|
const milestone = {
|
|
18260
19045
|
id: newId("milestone"),
|
|
18261
19046
|
roadmap_id: roadmapId,
|
|
@@ -18270,11 +19055,11 @@ function createMilestone(input) {
|
|
|
18270
19055
|
run_ids: cleanList(input.run_ids),
|
|
18271
19056
|
release: cleanString(input.release ?? roadmap.release ?? undefined),
|
|
18272
19057
|
tags: cleanList(input.tags),
|
|
18273
|
-
created_at:
|
|
18274
|
-
updated_at:
|
|
19058
|
+
created_at: now4,
|
|
19059
|
+
updated_at: now4
|
|
18275
19060
|
};
|
|
18276
19061
|
store.milestones[milestone.id] = milestone;
|
|
18277
|
-
store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList([...roadmap.milestone_ids, milestone.id]), updated_at:
|
|
19062
|
+
store.roadmaps[roadmapId] = { ...roadmap, milestone_ids: cleanList([...roadmap.milestone_ids, milestone.id]), updated_at: now4 };
|
|
18278
19063
|
writeStore(store);
|
|
18279
19064
|
return milestone;
|
|
18280
19065
|
}
|
|
@@ -18329,7 +19114,7 @@ function upsertReleaseGroup(input) {
|
|
|
18329
19114
|
throw new Error("Release group name is required");
|
|
18330
19115
|
const key = releaseKey(roadmapId, name);
|
|
18331
19116
|
const existing = store.releases[key];
|
|
18332
|
-
const
|
|
19117
|
+
const now4 = timestamp();
|
|
18333
19118
|
const release = {
|
|
18334
19119
|
name,
|
|
18335
19120
|
version: input.version === undefined ? existing?.version ?? null : cleanString(input.version),
|
|
@@ -18340,8 +19125,8 @@ function upsertReleaseGroup(input) {
|
|
|
18340
19125
|
plan_ids: input.plan_ids === undefined ? existing?.plan_ids ?? [] : cleanList(input.plan_ids),
|
|
18341
19126
|
run_ids: input.run_ids === undefined ? existing?.run_ids ?? [] : cleanList(input.run_ids),
|
|
18342
19127
|
notes: input.notes === undefined ? existing?.notes ?? null : cleanString(input.notes),
|
|
18343
|
-
created_at: existing?.created_at ??
|
|
18344
|
-
updated_at:
|
|
19128
|
+
created_at: existing?.created_at ?? now4,
|
|
19129
|
+
updated_at: now4
|
|
18345
19130
|
};
|
|
18346
19131
|
store.releases[key] = release;
|
|
18347
19132
|
writeStore(store);
|
|
@@ -18540,7 +19325,7 @@ function upsertCapacityProfile(input) {
|
|
|
18540
19325
|
const store = readStore2();
|
|
18541
19326
|
const key = profileKey(agentId, projectId);
|
|
18542
19327
|
const existing = store.profiles[key];
|
|
18543
|
-
const
|
|
19328
|
+
const now4 = timestamp2();
|
|
18544
19329
|
const profile = {
|
|
18545
19330
|
id: existing?.id ?? key,
|
|
18546
19331
|
agent_id: agentId,
|
|
@@ -18548,8 +19333,8 @@ function upsertCapacityProfile(input) {
|
|
|
18548
19333
|
minutes_per_day: assertMinutes(input.minutes_per_day),
|
|
18549
19334
|
working_days: normalizeWorkingDays(input.working_days),
|
|
18550
19335
|
effective_from: cleanString2(input.effective_from),
|
|
18551
|
-
created_at: existing?.created_at ??
|
|
18552
|
-
updated_at:
|
|
19336
|
+
created_at: existing?.created_at ?? now4,
|
|
19337
|
+
updated_at: now4
|
|
18553
19338
|
};
|
|
18554
19339
|
store.profiles[key] = profile;
|
|
18555
19340
|
writeStore2(store);
|
|
@@ -18958,7 +19743,7 @@ var init_audit_ledger = __esm(() => {
|
|
|
18958
19743
|
|
|
18959
19744
|
// src/lib/release-compatibility.ts
|
|
18960
19745
|
import { readFileSync as readFileSync5 } from "fs";
|
|
18961
|
-
import { join as
|
|
19746
|
+
import { join as join7, resolve as resolve10 } from "path";
|
|
18962
19747
|
import { Database as Database2 } from "bun:sqlite";
|
|
18963
19748
|
function pass(id, message, details) {
|
|
18964
19749
|
return { id, status: "passed", message, details };
|
|
@@ -18970,7 +19755,7 @@ function warn(id, message, details) {
|
|
|
18970
19755
|
return { id, status: "warning", message, details };
|
|
18971
19756
|
}
|
|
18972
19757
|
function readPackageJson2(root) {
|
|
18973
|
-
return JSON.parse(readFileSync5(
|
|
19758
|
+
return JSON.parse(readFileSync5(join7(root, "package.json"), "utf8"));
|
|
18974
19759
|
}
|
|
18975
19760
|
function sortedKeys(value) {
|
|
18976
19761
|
return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
|
|
@@ -19239,6 +20024,9 @@ function hasFts(db) {
|
|
|
19239
20024
|
function escapeFtsQuery(q) {
|
|
19240
20025
|
return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
|
|
19241
20026
|
}
|
|
20027
|
+
function shouldUseFts(q) {
|
|
20028
|
+
return /^[\p{L}\p{N}_\-\s]+$/u.test(q);
|
|
20029
|
+
}
|
|
19242
20030
|
function searchTasks(options, projectId, taskListId, db) {
|
|
19243
20031
|
const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
|
|
19244
20032
|
const d = db || getDatabase();
|
|
@@ -19247,7 +20035,8 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
19247
20035
|
let sql;
|
|
19248
20036
|
const raw = opts.query?.trim() ?? "";
|
|
19249
20037
|
const q = raw === "*" ? "" : raw;
|
|
19250
|
-
|
|
20038
|
+
const useFts = hasFts(d) && q && shouldUseFts(q);
|
|
20039
|
+
if (useFts) {
|
|
19251
20040
|
const ftsQuery = escapeFtsQuery(q);
|
|
19252
20041
|
sql = `SELECT t.* FROM tasks t
|
|
19253
20042
|
INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
|
|
@@ -19255,8 +20044,16 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
19255
20044
|
params.push(ftsQuery);
|
|
19256
20045
|
} else if (q) {
|
|
19257
20046
|
const pattern = `%${q}%`;
|
|
19258
|
-
sql = `SELECT * FROM tasks t WHERE (
|
|
19259
|
-
|
|
20047
|
+
sql = `SELECT * FROM tasks t WHERE (
|
|
20048
|
+
t.id LIKE ?
|
|
20049
|
+
OR t.short_id LIKE ?
|
|
20050
|
+
OR t.title LIKE ?
|
|
20051
|
+
OR t.description LIKE ?
|
|
20052
|
+
OR t.working_dir LIKE ?
|
|
20053
|
+
OR t.metadata LIKE ?
|
|
20054
|
+
OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?)
|
|
20055
|
+
)`;
|
|
20056
|
+
params.push(pattern, pattern, pattern, pattern, pattern, pattern, pattern);
|
|
19260
20057
|
} else {
|
|
19261
20058
|
sql = `SELECT * FROM tasks t WHERE 1=1`;
|
|
19262
20059
|
}
|
|
@@ -19312,7 +20109,7 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
19312
20109
|
} else if (opts.is_blocked === false) {
|
|
19313
20110
|
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
20111
|
}
|
|
19315
|
-
if (
|
|
20112
|
+
if (useFts) {
|
|
19316
20113
|
sql += ` ORDER BY bm25(tasks_fts),
|
|
19317
20114
|
CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
19318
20115
|
t.created_at DESC`;
|
|
@@ -22407,7 +23204,7 @@ function registerTaskWorkflowTools(server, ctx) {
|
|
|
22407
23204
|
const globalRole = n.agent.role ? ` [${n.agent.role}]` : "";
|
|
22408
23205
|
const lead = n.is_lead ? " \u2605" : "";
|
|
22409
23206
|
const lastSeen = new Date(n.agent.last_seen_at).getTime();
|
|
22410
|
-
const active =
|
|
23207
|
+
const active = now4 - lastSeen < ACTIVE_MS ? " \u25CF" : " \u25CB";
|
|
22411
23208
|
const line = `${prefix}${active} ${n.agent.name}${title}${globalRole}${lead}`;
|
|
22412
23209
|
const children = n.reports.length > 0 ? `
|
|
22413
23210
|
` + render(n.reports, indent + 1) : "";
|
|
@@ -22420,7 +23217,7 @@ function registerTaskWorkflowTools(server, ctx) {
|
|
|
22420
23217
|
if (format === "json") {
|
|
22421
23218
|
return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) }] };
|
|
22422
23219
|
}
|
|
22423
|
-
const
|
|
23220
|
+
const now4 = Date.now();
|
|
22424
23221
|
const ACTIVE_MS = 30 * 60 * 1000;
|
|
22425
23222
|
const text = tree.length > 0 ? render(tree) : "No agents in org chart.";
|
|
22426
23223
|
return { content: [{ type: "text", text }] };
|
|
@@ -22542,8 +23339,8 @@ var exports_doctor = {};
|
|
|
22542
23339
|
__export(exports_doctor, {
|
|
22543
23340
|
runTodosDoctor: () => runTodosDoctor
|
|
22544
23341
|
});
|
|
22545
|
-
import { chmodSync, copyFileSync, existsSync as
|
|
22546
|
-
import { basename as basename4, dirname as dirname6, join as
|
|
23342
|
+
import { chmodSync, copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync5, statSync as statSync5 } from "fs";
|
|
23343
|
+
import { basename as basename4, dirname as dirname6, join as join8 } from "path";
|
|
22547
23344
|
function tableExists(db, table) {
|
|
22548
23345
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
22549
23346
|
}
|
|
@@ -22637,7 +23434,7 @@ function findMissingProjectRoots(db) {
|
|
|
22637
23434
|
continue;
|
|
22638
23435
|
if (!row.path.startsWith("/"))
|
|
22639
23436
|
continue;
|
|
22640
|
-
if (!
|
|
23437
|
+
if (!existsSync10(row.path))
|
|
22641
23438
|
missing++;
|
|
22642
23439
|
}
|
|
22643
23440
|
return missing;
|
|
@@ -22697,16 +23494,16 @@ function databasePermissionsAreUnsafe(dbPath) {
|
|
|
22697
23494
|
function createBackup(dbPath) {
|
|
22698
23495
|
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
22699
23496
|
return;
|
|
22700
|
-
if (!
|
|
23497
|
+
if (!existsSync10(dbPath))
|
|
22701
23498
|
return;
|
|
22702
23499
|
const stamp = now().replace(/[:.]/g, "-");
|
|
22703
|
-
const backupDir =
|
|
23500
|
+
const backupDir = join8(dirname6(dbPath), `${basename4(dbPath)}.backup-${stamp}`);
|
|
22704
23501
|
const files = [];
|
|
22705
23502
|
mkdirSync5(backupDir, { recursive: true });
|
|
22706
23503
|
for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
22707
|
-
if (!
|
|
23504
|
+
if (!existsSync10(source))
|
|
22708
23505
|
continue;
|
|
22709
|
-
const target =
|
|
23506
|
+
const target = join8(backupDir, basename4(source));
|
|
22710
23507
|
copyFileSync(source, target);
|
|
22711
23508
|
files.push(target);
|
|
22712
23509
|
}
|
|
@@ -23010,14 +23807,14 @@ function registerTaskAutoTools(server, ctx) {
|
|
|
23010
23807
|
return { content: [{ type: "text", text: "No agent_id provided and no agent focus is active." }], isError: true };
|
|
23011
23808
|
}
|
|
23012
23809
|
const assigned = listTasks3({ assigned_to: effectiveAgentId, limit: 500 }, undefined);
|
|
23013
|
-
const
|
|
23014
|
-
const dueSoonCutoff =
|
|
23810
|
+
const now4 = Date.now();
|
|
23811
|
+
const dueSoonCutoff = now4 + 24 * 60 * 60 * 1000;
|
|
23015
23812
|
const blocked = getBlockedTasks2().filter((t) => t.assigned_to === effectiveAgentId);
|
|
23016
23813
|
const workload = {
|
|
23017
23814
|
in_progress: assigned.filter((t) => t.status === "in_progress").length,
|
|
23018
23815
|
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() >=
|
|
23816
|
+
completed_recent: assigned.filter((t) => t.status === "completed" && t.completed_at && now4 - new Date(t.completed_at).getTime() <= 7 * 24 * 60 * 60 * 1000).length,
|
|
23817
|
+
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
23818
|
blocked: blocked.length
|
|
23022
23819
|
};
|
|
23023
23820
|
const lines = [
|
|
@@ -23244,7 +24041,7 @@ function limits(input) {
|
|
|
23244
24041
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
23245
24042
|
};
|
|
23246
24043
|
}
|
|
23247
|
-
function
|
|
24044
|
+
function truncate2(value, max) {
|
|
23248
24045
|
if (!value)
|
|
23249
24046
|
return value ?? null;
|
|
23250
24047
|
const redacted = redactEvidenceText(value);
|
|
@@ -23265,9 +24062,9 @@ function acceptanceCriteria(task, maxText) {
|
|
|
23265
24062
|
const metadata = task.metadata || {};
|
|
23266
24063
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
23267
24064
|
if (Array.isArray(raw))
|
|
23268
|
-
return raw.map((item) =>
|
|
24065
|
+
return raw.map((item) => truncate2(String(item), maxText)).filter((item) => Boolean(item));
|
|
23269
24066
|
if (typeof raw === "string") {
|
|
23270
|
-
return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) =>
|
|
24067
|
+
return raw.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((line) => truncate2(line, maxText)).filter((item) => Boolean(item));
|
|
23271
24068
|
}
|
|
23272
24069
|
return [];
|
|
23273
24070
|
}
|
|
@@ -23290,7 +24087,7 @@ function addFile(files, path, source, base) {
|
|
|
23290
24087
|
path,
|
|
23291
24088
|
status: base?.status || "active",
|
|
23292
24089
|
agent_id: base?.agent_id ?? null,
|
|
23293
|
-
note:
|
|
24090
|
+
note: truncate2(base?.note, 240),
|
|
23294
24091
|
updated_at: base?.updated_at || "",
|
|
23295
24092
|
sources: [source]
|
|
23296
24093
|
});
|
|
@@ -23348,7 +24145,7 @@ function estimateTokens(value) {
|
|
|
23348
24145
|
return Math.max(1, Math.ceil((text || "").length / 4));
|
|
23349
24146
|
}
|
|
23350
24147
|
function summarizeStrings(values, maxChars) {
|
|
23351
|
-
return
|
|
24148
|
+
return truncate2(values.filter(Boolean).join("; "), maxChars) || "No local details were available before this section was omitted.";
|
|
23352
24149
|
}
|
|
23353
24150
|
function summarizeSection(pack, section, maxChars) {
|
|
23354
24151
|
if (section === "project")
|
|
@@ -23539,8 +24336,8 @@ function createAgentContextPack(input, db) {
|
|
|
23539
24336
|
...taskFiles.map((file) => file.updated_at)
|
|
23540
24337
|
], task.updated_at);
|
|
23541
24338
|
const warnings = [];
|
|
23542
|
-
const
|
|
23543
|
-
if (Date.parse(task.updated_at) <
|
|
24339
|
+
const now4 = input.now ? new Date(input.now) : new Date;
|
|
24340
|
+
if (Date.parse(task.updated_at) < now4.getTime() - limit.stale_after_hours * 60 * 60 * 1000) {
|
|
23544
24341
|
warnings.push(`task state is older than ${limit.stale_after_hours} hours`);
|
|
23545
24342
|
}
|
|
23546
24343
|
if (comments.length > recentComments.length)
|
|
@@ -23555,7 +24352,7 @@ function createAgentContextPack(input, db) {
|
|
|
23555
24352
|
id: task.id,
|
|
23556
24353
|
short_id: task.short_id,
|
|
23557
24354
|
title: redactEvidenceText(task.title),
|
|
23558
|
-
description:
|
|
24355
|
+
description: truncate2(task.description, limit.max_text_chars),
|
|
23559
24356
|
status: task.status,
|
|
23560
24357
|
priority: task.priority,
|
|
23561
24358
|
assigned_to: task.assigned_to,
|
|
@@ -23579,7 +24376,7 @@ function createAgentContextPack(input, db) {
|
|
|
23579
24376
|
plan: plan ? {
|
|
23580
24377
|
id: plan.id,
|
|
23581
24378
|
name: plan.name,
|
|
23582
|
-
description:
|
|
24379
|
+
description: truncate2(plan.description, limit.max_text_chars),
|
|
23583
24380
|
status: plan.status,
|
|
23584
24381
|
agent_id: plan.agent_id,
|
|
23585
24382
|
tasks: planTasks.slice(0, limit.plan_task_limit).map(taskSummary).filter((item) => Boolean(item)),
|
|
@@ -23598,7 +24395,7 @@ function createAgentContextPack(input, db) {
|
|
|
23598
24395
|
type: comment.type,
|
|
23599
24396
|
progress_pct: comment.progress_pct,
|
|
23600
24397
|
created_at: comment.created_at,
|
|
23601
|
-
content:
|
|
24398
|
+
content: truncate2(comment.content, limit.max_text_chars) || ""
|
|
23602
24399
|
})),
|
|
23603
24400
|
omitted: Math.max(0, comments.length - recentComments.length)
|
|
23604
24401
|
},
|
|
@@ -23606,7 +24403,7 @@ function createAgentContextPack(input, db) {
|
|
|
23606
24403
|
traceability: {
|
|
23607
24404
|
commits: traceability.commits.map((commit) => ({
|
|
23608
24405
|
sha: commit.sha,
|
|
23609
|
-
message:
|
|
24406
|
+
message: truncate2(commit.message, 240),
|
|
23610
24407
|
files_changed: commit.files_changed,
|
|
23611
24408
|
committed_at: commit.committed_at
|
|
23612
24409
|
})),
|
|
@@ -23614,7 +24411,7 @@ function createAgentContextPack(input, db) {
|
|
|
23614
24411
|
verifications: verifications.map((verification) => ({
|
|
23615
24412
|
command: verification.command,
|
|
23616
24413
|
status: verification.status,
|
|
23617
|
-
output_summary:
|
|
24414
|
+
output_summary: truncate2(verification.output_summary, limit.max_text_chars),
|
|
23618
24415
|
artifact_path: verification.artifact_path,
|
|
23619
24416
|
run_at: verification.run_at
|
|
23620
24417
|
})),
|
|
@@ -23625,14 +24422,14 @@ function createAgentContextPack(input, db) {
|
|
|
23625
24422
|
id: ledger.run.id,
|
|
23626
24423
|
title: ledger.run.title,
|
|
23627
24424
|
status: ledger.run.status,
|
|
23628
|
-
summary:
|
|
24425
|
+
summary: truncate2(ledger.run.summary, limit.max_text_chars),
|
|
23629
24426
|
agent_id: ledger.run.agent_id,
|
|
23630
24427
|
started_at: ledger.run.started_at,
|
|
23631
24428
|
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:
|
|
24429
|
+
events: ledger.events.map((event) => ({ event_type: event.event_type, message: truncate2(event.message, 500), created_at: event.created_at })),
|
|
24430
|
+
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 })),
|
|
24431
|
+
files: ledger.files.map((file) => ({ path: file.path, status: file.status, note: truncate2(file.note, 240) })),
|
|
24432
|
+
artifacts: ledger.artifacts.map((artifact) => ({ path: artifact.path, artifact_type: artifact.artifact_type, description: truncate2(artifact.description, 240), sha256: artifact.sha256 }))
|
|
23636
24433
|
})),
|
|
23637
24434
|
omitted: Math.max(0, runs.length - selectedRuns.length)
|
|
23638
24435
|
},
|
|
@@ -23737,7 +24534,7 @@ function renderAgentContextPackCompactMarkdown(pack) {
|
|
|
23737
24534
|
const lines = [
|
|
23738
24535
|
`# Context: ${pack.task.title}`,
|
|
23739
24536
|
`${pack.task.status} | ${pack.task.priority} | ${pack.task.short_id || pack.task.id.slice(0, 8)}`,
|
|
23740
|
-
pack.task.description ?
|
|
24537
|
+
pack.task.description ? truncate2(pack.task.description, Math.min(pack.limits.summary_char_limit, 700)) : null,
|
|
23741
24538
|
"",
|
|
23742
24539
|
"## Must Know",
|
|
23743
24540
|
bullet([
|
|
@@ -24987,7 +25784,7 @@ var init_agent_run_dispatcher = __esm(() => {
|
|
|
24987
25784
|
});
|
|
24988
25785
|
|
|
24989
25786
|
// src/lib/verification-providers.ts
|
|
24990
|
-
import { existsSync as
|
|
25787
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
|
|
24991
25788
|
function normalizeName5(name) {
|
|
24992
25789
|
const normalized = name.trim().toLowerCase();
|
|
24993
25790
|
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
|
|
@@ -25139,7 +25936,7 @@ Timed out after ${provider.timeout_ms}ms`);
|
|
|
25139
25936
|
};
|
|
25140
25937
|
}
|
|
25141
25938
|
function runCiLogProvider(input) {
|
|
25142
|
-
const text = input.log_text ?? (input.log_path &&
|
|
25939
|
+
const text = input.log_text ?? (input.log_path && existsSync11(input.log_path) ? readFileSync6(input.log_path, "utf-8") : "");
|
|
25143
25940
|
return {
|
|
25144
25941
|
status: classifyLog(text),
|
|
25145
25942
|
attempts: 1,
|
|
@@ -25151,7 +25948,7 @@ function runBrowserProvider(input) {
|
|
|
25151
25948
|
if (!input.artifact_path) {
|
|
25152
25949
|
return { status: "unknown", attempts: 1, exit_code: null, output_summary: "browser provider needs a screenshot or artifact path" };
|
|
25153
25950
|
}
|
|
25154
|
-
if (!
|
|
25951
|
+
if (!existsSync11(input.artifact_path)) {
|
|
25155
25952
|
return { status: "failed", attempts: 1, exit_code: null, output_summary: `artifact not found: ${input.artifact_path}` };
|
|
25156
25953
|
}
|
|
25157
25954
|
return {
|
|
@@ -26941,12 +27738,12 @@ function summarizeTask(task) {
|
|
|
26941
27738
|
};
|
|
26942
27739
|
}
|
|
26943
27740
|
function overdueTasks(tasks, nowIso) {
|
|
26944
|
-
const
|
|
27741
|
+
const now4 = Date.parse(nowIso);
|
|
26945
27742
|
return tasks.filter((task) => {
|
|
26946
27743
|
if (isTerminal(task) || !task.due_at)
|
|
26947
27744
|
return false;
|
|
26948
27745
|
const due = Date.parse(task.due_at);
|
|
26949
|
-
return Number.isFinite(due) && due <
|
|
27746
|
+
return Number.isFinite(due) && due < now4;
|
|
26950
27747
|
});
|
|
26951
27748
|
}
|
|
26952
27749
|
function isReady(task, db) {
|
|
@@ -27296,15 +28093,15 @@ var init_local_reports = __esm(() => {
|
|
|
27296
28093
|
});
|
|
27297
28094
|
|
|
27298
28095
|
// src/lib/package-version.ts
|
|
27299
|
-
import { existsSync as
|
|
27300
|
-
import { dirname as dirname7, join as
|
|
28096
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
|
|
28097
|
+
import { dirname as dirname7, join as join9 } from "path";
|
|
27301
28098
|
import { fileURLToPath } from "url";
|
|
27302
28099
|
function getPackageVersion(fromUrl = import.meta.url) {
|
|
27303
28100
|
try {
|
|
27304
28101
|
let dir = dirname7(fileURLToPath(fromUrl));
|
|
27305
28102
|
for (let i = 0;i < 5; i++) {
|
|
27306
|
-
const pkgPath =
|
|
27307
|
-
if (
|
|
28103
|
+
const pkgPath = join9(dir, "package.json");
|
|
28104
|
+
if (existsSync12(pkgPath)) {
|
|
27308
28105
|
return JSON.parse(readFileSync7(pkgPath, "utf-8")).version || "0.0.0";
|
|
27309
28106
|
}
|
|
27310
28107
|
const parent = dirname7(dir);
|
|
@@ -29395,8 +30192,8 @@ __export(exports_local_extensions, {
|
|
|
29395
30192
|
discoverLocalExtensions: () => discoverLocalExtensions
|
|
29396
30193
|
});
|
|
29397
30194
|
import { createHash as createHash8, createVerify } from "crypto";
|
|
29398
|
-
import { existsSync as
|
|
29399
|
-
import { basename as basename5, join as
|
|
30195
|
+
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
|
|
30196
|
+
import { basename as basename5, join as join10, resolve as resolve12 } from "path";
|
|
29400
30197
|
function isObject2(value) {
|
|
29401
30198
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
29402
30199
|
}
|
|
@@ -29655,10 +30452,10 @@ function verifyExtensionSignature(input) {
|
|
|
29655
30452
|
}
|
|
29656
30453
|
function inspectExtensionSource(source3) {
|
|
29657
30454
|
const resolved = resolve12(source3);
|
|
29658
|
-
if (!
|
|
30455
|
+
if (!existsSync13(resolved))
|
|
29659
30456
|
throw new Error(`extension source not found: ${source3}`);
|
|
29660
30457
|
const stat = statSync6(resolved);
|
|
29661
|
-
const manifestPath = stat.isDirectory() ? [
|
|
30458
|
+
const manifestPath = stat.isDirectory() ? [join10(resolved, "todos.extension.json"), join10(resolved, "extension.json")].find(existsSync13) : resolved;
|
|
29662
30459
|
if (!manifestPath)
|
|
29663
30460
|
throw new Error(`extension directory ${source3} is missing todos.extension.json`);
|
|
29664
30461
|
const raw = readFileSync9(manifestPath);
|
|
@@ -29754,20 +30551,20 @@ function projectExtensionSources(projectPath) {
|
|
|
29754
30551
|
return [];
|
|
29755
30552
|
const root = resolve12(projectPath);
|
|
29756
30553
|
const candidates = [
|
|
29757
|
-
|
|
29758
|
-
|
|
30554
|
+
join10(root, "todos.extension.json"),
|
|
30555
|
+
join10(root, ".todos", "todos.extension.json")
|
|
29759
30556
|
];
|
|
29760
|
-
const extensionDir =
|
|
29761
|
-
if (
|
|
30557
|
+
const extensionDir = join10(root, ".todos", "extensions");
|
|
30558
|
+
if (existsSync13(extensionDir)) {
|
|
29762
30559
|
for (const entry of readdirSync3(extensionDir)) {
|
|
29763
30560
|
if (entry.startsWith("."))
|
|
29764
30561
|
continue;
|
|
29765
|
-
const full =
|
|
30562
|
+
const full = join10(extensionDir, entry);
|
|
29766
30563
|
if (statSync6(full).isDirectory() || entry.endsWith(".json"))
|
|
29767
30564
|
candidates.push(full);
|
|
29768
30565
|
}
|
|
29769
30566
|
}
|
|
29770
|
-
return candidates.filter(
|
|
30567
|
+
return candidates.filter(existsSync13);
|
|
29771
30568
|
}
|
|
29772
30569
|
function discoverLocalExtensions(options = {}) {
|
|
29773
30570
|
const config = loadConfig();
|
|
@@ -33289,7 +34086,7 @@ ${lines.join(`
|
|
|
33289
34086
|
const projectRoles = n.project_roles.length > 0 ? ` <${n.project_roles.join(", ")}>` : "";
|
|
33290
34087
|
const lead = n.is_project_lead ? " \u2605" : "";
|
|
33291
34088
|
const lastSeen = new Date(n.agent.last_seen_at).getTime();
|
|
33292
|
-
const active =
|
|
34089
|
+
const active = now4 - lastSeen < ACTIVE_MS ? " \u25CF" : " \u25CB";
|
|
33293
34090
|
const line = `${prefix}${active} ${n.agent.name}${title}${globalRole}${projectRoles}${lead}`;
|
|
33294
34091
|
const children = n.reports.length > 0 ? `
|
|
33295
34092
|
` + render(n.reports, indent + 1) : "";
|
|
@@ -33303,7 +34100,7 @@ ${lines.join(`
|
|
|
33303
34100
|
if (format === "json") {
|
|
33304
34101
|
return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) }] };
|
|
33305
34102
|
}
|
|
33306
|
-
const
|
|
34103
|
+
const now4 = Date.now();
|
|
33307
34104
|
const ACTIVE_MS = 30 * 60 * 1000;
|
|
33308
34105
|
const text2 = tree.length > 0 ? render(tree) : "No agents in this project's org chart.";
|
|
33309
34106
|
return { content: [{ type: "text", text: text2 }] };
|
|
@@ -33853,10 +34650,10 @@ ${lines.join(`
|
|
|
33853
34650
|
});
|
|
33854
34651
|
}
|
|
33855
34652
|
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:
|
|
34653
|
+
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
34654
|
try {
|
|
33858
34655
|
const { getIdleFocusSessionPrompts: getIdleFocusSessionPrompts2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
33859
|
-
return { content: [{ type: "text", text: JSON.stringify(getIdleFocusSessionPrompts2({ agent_id, now:
|
|
34656
|
+
return { content: [{ type: "text", text: JSON.stringify(getIdleFocusSessionPrompts2({ agent_id, now: now4 }), null, 2) }] };
|
|
33860
34657
|
} catch (e) {
|
|
33861
34658
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33862
34659
|
}
|
|
@@ -34051,9 +34848,9 @@ __export(exports_extract, {
|
|
|
34051
34848
|
buildCodebaseIndex: () => buildCodebaseIndex,
|
|
34052
34849
|
EXTRACT_TAGS: () => EXTRACT_TAGS
|
|
34053
34850
|
});
|
|
34054
|
-
import { existsSync as
|
|
34851
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
|
|
34055
34852
|
import { createHash as createHash10 } from "crypto";
|
|
34056
|
-
import { relative as relative5, resolve as resolve13, join as
|
|
34853
|
+
import { relative as relative5, resolve as resolve13, join as join11 } from "path";
|
|
34057
34854
|
function stableHash(value) {
|
|
34058
34855
|
return createHash10("sha256").update(value).digest("hex");
|
|
34059
34856
|
}
|
|
@@ -34062,8 +34859,8 @@ function normalizePathForMatch(value) {
|
|
|
34062
34859
|
}
|
|
34063
34860
|
function readGitignorePatterns(basePath) {
|
|
34064
34861
|
const root = statSync7(basePath).isFile() ? resolve13(basePath, "..") : basePath;
|
|
34065
|
-
const gitignorePath =
|
|
34066
|
-
if (!
|
|
34862
|
+
const gitignorePath = join11(root, ".gitignore");
|
|
34863
|
+
if (!existsSync14(gitignorePath))
|
|
34067
34864
|
return [];
|
|
34068
34865
|
try {
|
|
34069
34866
|
return readFileSync10(gitignorePath, "utf-8").split(`
|
|
@@ -34205,7 +35002,7 @@ function buildCodebaseIndex(options) {
|
|
|
34205
35002
|
const files = collectFiles(basePath, extensions, excludes, respectGitignore);
|
|
34206
35003
|
const indexed = [];
|
|
34207
35004
|
for (const file of files) {
|
|
34208
|
-
const fullPath = statSync7(basePath).isFile() ? basePath :
|
|
35005
|
+
const fullPath = statSync7(basePath).isFile() ? basePath : join11(basePath, file);
|
|
34209
35006
|
try {
|
|
34210
35007
|
const source3 = readFileSync10(fullPath, "utf-8");
|
|
34211
35008
|
const relPath = statSync7(basePath).isFile() ? relative5(resolve13(basePath, ".."), fullPath) : file;
|
|
@@ -34236,7 +35033,7 @@ function extractTodos(options, db) {
|
|
|
34236
35033
|
const files = collectFiles(basePath, extensions, excludes, respectGitignore);
|
|
34237
35034
|
const allComments = [];
|
|
34238
35035
|
for (const file of files) {
|
|
34239
|
-
const fullPath = statSync7(basePath).isFile() ? basePath :
|
|
35036
|
+
const fullPath = statSync7(basePath).isFile() ? basePath : join11(basePath, file);
|
|
34240
35037
|
try {
|
|
34241
35038
|
const source3 = readFileSync10(fullPath, "utf-8");
|
|
34242
35039
|
const relPath = statSync7(basePath).isFile() ? relative5(resolve13(basePath, ".."), fullPath) : file;
|
|
@@ -35129,7 +35926,7 @@ __export(exports_builtin_templates, {
|
|
|
35129
35926
|
BUILTIN_TEMPLATES: () => BUILTIN_TEMPLATES
|
|
35130
35927
|
});
|
|
35131
35928
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
35132
|
-
import { join as
|
|
35929
|
+
import { join as join12 } from "path";
|
|
35133
35930
|
function templateMetadata(template) {
|
|
35134
35931
|
return {
|
|
35135
35932
|
source: BUILTIN_TEMPLATE_LIBRARY_SOURCE,
|
|
@@ -35188,7 +35985,7 @@ function writeBuiltinTemplateFiles(directory) {
|
|
|
35188
35985
|
mkdirSync7(directory, { recursive: true });
|
|
35189
35986
|
const files = [];
|
|
35190
35987
|
for (const entry of exportBuiltinTemplateFiles()) {
|
|
35191
|
-
const path =
|
|
35988
|
+
const path = join12(directory, entry.filename);
|
|
35192
35989
|
writeFileSync4(path, `${JSON.stringify(entry.template, null, 2)}
|
|
35193
35990
|
`, "utf-8");
|
|
35194
35991
|
files.push(path);
|
|
@@ -35714,16 +36511,16 @@ __export(exports_environment_snapshots, {
|
|
|
35714
36511
|
captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
|
|
35715
36512
|
});
|
|
35716
36513
|
import { createHash as createHash11 } from "crypto";
|
|
35717
|
-
import { existsSync as
|
|
36514
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
|
|
35718
36515
|
import { hostname as hostname2, platform, arch } from "os";
|
|
35719
|
-
import { dirname as dirname9, join as
|
|
36516
|
+
import { dirname as dirname9, join as join13, resolve as resolve14 } from "path";
|
|
35720
36517
|
import { tmpdir as tmpdir2 } from "os";
|
|
35721
36518
|
function sha2566(value) {
|
|
35722
36519
|
return createHash11("sha256").update(value).digest("hex");
|
|
35723
36520
|
}
|
|
35724
36521
|
function fileRecord(root, relativePath) {
|
|
35725
|
-
const path =
|
|
35726
|
-
if (!
|
|
36522
|
+
const path = join13(root, relativePath);
|
|
36523
|
+
if (!existsSync15(path))
|
|
35727
36524
|
return null;
|
|
35728
36525
|
const stat = statSync8(path);
|
|
35729
36526
|
if (!stat.isFile())
|
|
@@ -35735,7 +36532,7 @@ function manifestRecord(root, relativePath) {
|
|
|
35735
36532
|
const base = fileRecord(root, relativePath);
|
|
35736
36533
|
if (!base)
|
|
35737
36534
|
return null;
|
|
35738
|
-
const parsed = readJsonFile(
|
|
36535
|
+
const parsed = readJsonFile(join13(root, relativePath));
|
|
35739
36536
|
if (!parsed)
|
|
35740
36537
|
return { ...base, redacted: {} };
|
|
35741
36538
|
const redacted = redactValue({
|
|
@@ -35830,8 +36627,8 @@ function commandEnv(env, includeValues) {
|
|
|
35830
36627
|
function defaultSnapshotDir() {
|
|
35831
36628
|
const dbPath = getDatabasePath();
|
|
35832
36629
|
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
35833
|
-
return
|
|
35834
|
-
return
|
|
36630
|
+
return join13(tmpdir2(), "hasna-todos", "environment-snapshots");
|
|
36631
|
+
return join13(dirname9(resolve14(dbPath)), "environment-snapshots");
|
|
35835
36632
|
}
|
|
35836
36633
|
function snapshotWithId(snapshot) {
|
|
35837
36634
|
const digest = sha2566(JSON.stringify(snapshot)).slice(0, 24);
|
|
@@ -35878,7 +36675,7 @@ function captureEnvironmentSnapshot(input = {}) {
|
|
|
35878
36675
|
});
|
|
35879
36676
|
}
|
|
35880
36677
|
function writeEnvironmentSnapshot(snapshot, outputPath) {
|
|
35881
|
-
const path = outputPath ? resolve14(outputPath) :
|
|
36678
|
+
const path = outputPath ? resolve14(outputPath) : join13(defaultSnapshotDir(), `${snapshot.id}.json`);
|
|
35882
36679
|
ensureDir2(dirname9(path));
|
|
35883
36680
|
writeJsonFile(path, snapshot);
|
|
35884
36681
|
return path;
|
|
@@ -36250,7 +37047,7 @@ var init_http = __esm(() => {
|
|
|
36250
37047
|
});
|
|
36251
37048
|
|
|
36252
37049
|
// src/db/api-keys.ts
|
|
36253
|
-
import { createHash as createHash12, randomBytes as randomBytes2, timingSafeEqual as
|
|
37050
|
+
import { createHash as createHash12, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
36254
37051
|
function rowToRecord(row) {
|
|
36255
37052
|
return {
|
|
36256
37053
|
id: row.id,
|
|
@@ -36269,7 +37066,7 @@ function hashApiKey(key) {
|
|
|
36269
37066
|
function safeEqualHex(a, b) {
|
|
36270
37067
|
if (a.length !== b.length)
|
|
36271
37068
|
return false;
|
|
36272
|
-
return
|
|
37069
|
+
return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
36273
37070
|
}
|
|
36274
37071
|
function hasActiveApiKeys(db) {
|
|
36275
37072
|
const d = db || getDatabase();
|
|
@@ -36430,7 +37227,7 @@ var init_headless_boundaries = __esm(() => {
|
|
|
36430
37227
|
});
|
|
36431
37228
|
|
|
36432
37229
|
// src/server/routes.ts
|
|
36433
|
-
import { join as
|
|
37230
|
+
import { join as join14, resolve as resolve15, sep as sep2 } from "path";
|
|
36434
37231
|
function parseFieldsParam(url) {
|
|
36435
37232
|
const fieldsParam = url.searchParams.get("fields");
|
|
36436
37233
|
return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
@@ -37134,7 +37931,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
37134
37931
|
if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
|
|
37135
37932
|
return null;
|
|
37136
37933
|
if (path !== "/") {
|
|
37137
|
-
const filePath =
|
|
37934
|
+
const filePath = join14(ctx.dashboardDir, path);
|
|
37138
37935
|
const resolvedFile = resolve15(filePath);
|
|
37139
37936
|
const resolvedBase = resolve15(ctx.dashboardDir);
|
|
37140
37937
|
if (!resolvedFile.startsWith(resolvedBase + sep2) && resolvedFile !== resolvedBase) {
|
|
@@ -37144,7 +37941,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
37144
37941
|
if (res2)
|
|
37145
37942
|
return res2;
|
|
37146
37943
|
}
|
|
37147
|
-
const indexPath =
|
|
37944
|
+
const indexPath = join14(ctx.dashboardDir, "index.html");
|
|
37148
37945
|
const res = serveStaticFile2(indexPath);
|
|
37149
37946
|
if (res)
|
|
37150
37947
|
return res;
|
|
@@ -37173,27 +37970,27 @@ __export(exports_serve, {
|
|
|
37173
37970
|
SECURITY_HEADERS: () => SECURITY_HEADERS,
|
|
37174
37971
|
MIME_TYPES: () => MIME_TYPES
|
|
37175
37972
|
});
|
|
37176
|
-
import { existsSync as
|
|
37177
|
-
import { join as
|
|
37973
|
+
import { existsSync as existsSync16 } from "fs";
|
|
37974
|
+
import { join as join15, dirname as dirname10, extname } from "path";
|
|
37178
37975
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
37179
37976
|
function resolveDashboardDir() {
|
|
37180
37977
|
const candidates = [];
|
|
37181
37978
|
try {
|
|
37182
37979
|
const scriptDir = dirname10(fileURLToPath2(import.meta.url));
|
|
37183
|
-
candidates.push(
|
|
37184
|
-
candidates.push(
|
|
37980
|
+
candidates.push(join15(scriptDir, "..", "dashboard", "dist"));
|
|
37981
|
+
candidates.push(join15(scriptDir, "..", "..", "dashboard", "dist"));
|
|
37185
37982
|
} catch {}
|
|
37186
37983
|
if (process.argv[1]) {
|
|
37187
37984
|
const mainDir = dirname10(process.argv[1]);
|
|
37188
|
-
candidates.push(
|
|
37189
|
-
candidates.push(
|
|
37985
|
+
candidates.push(join15(mainDir, "..", "dashboard", "dist"));
|
|
37986
|
+
candidates.push(join15(mainDir, "..", "..", "dashboard", "dist"));
|
|
37190
37987
|
}
|
|
37191
|
-
candidates.push(
|
|
37988
|
+
candidates.push(join15(process.cwd(), "dashboard", "dist"));
|
|
37192
37989
|
for (const candidate of candidates) {
|
|
37193
|
-
if (
|
|
37990
|
+
if (existsSync16(candidate))
|
|
37194
37991
|
return candidate;
|
|
37195
37992
|
}
|
|
37196
|
-
return
|
|
37993
|
+
return join15(process.cwd(), "dashboard", "dist");
|
|
37197
37994
|
}
|
|
37198
37995
|
function getProvidedApiKey(req) {
|
|
37199
37996
|
const headerKey = req.headers.get("x-api-key");
|
|
@@ -37220,15 +38017,15 @@ function checkAuth(req, apiKey) {
|
|
|
37220
38017
|
return null;
|
|
37221
38018
|
}
|
|
37222
38019
|
function checkRateLimit(ip) {
|
|
37223
|
-
const
|
|
38020
|
+
const now4 = Date.now();
|
|
37224
38021
|
const entry = rateLimitMap.get(ip);
|
|
37225
|
-
if (!entry ||
|
|
37226
|
-
rateLimitMap.set(ip, { count: 1, resetAt:
|
|
38022
|
+
if (!entry || now4 > entry.resetAt) {
|
|
38023
|
+
rateLimitMap.set(ip, { count: 1, resetAt: now4 + RATE_LIMIT_WINDOW_MS });
|
|
37227
38024
|
return { allowed: true };
|
|
37228
38025
|
}
|
|
37229
38026
|
entry.count++;
|
|
37230
38027
|
if (entry.count > RATE_LIMIT_MAX) {
|
|
37231
|
-
return { allowed: false, retryAfter: Math.ceil((entry.resetAt -
|
|
38028
|
+
return { allowed: false, retryAfter: Math.ceil((entry.resetAt - now4) / 1000) };
|
|
37232
38029
|
}
|
|
37233
38030
|
return { allowed: true };
|
|
37234
38031
|
}
|
|
@@ -37243,7 +38040,7 @@ function json(data, status = 200, headers) {
|
|
|
37243
38040
|
});
|
|
37244
38041
|
}
|
|
37245
38042
|
function serveStaticFile(filePath) {
|
|
37246
|
-
if (!
|
|
38043
|
+
if (!existsSync16(filePath))
|
|
37247
38044
|
return null;
|
|
37248
38045
|
const ext = extname(filePath);
|
|
37249
38046
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
@@ -37322,7 +38119,7 @@ data: ${data}
|
|
|
37322
38119
|
filteredSseClients.delete(client);
|
|
37323
38120
|
}
|
|
37324
38121
|
const dashboardDir = resolveDashboardDir();
|
|
37325
|
-
const dashboardExists =
|
|
38122
|
+
const dashboardExists = existsSync16(dashboardDir);
|
|
37326
38123
|
if (!dashboardExists) {
|
|
37327
38124
|
console.error(`
|
|
37328
38125
|
Dashboard not found at: ${dashboardDir}`);
|