feat: add SQLite-backed activity event logging layer (#1528)
* feat: add SQLite-backed activity event logging layer Implements a structured diagnostic event trail for the orchestrator. When unexpected behavior occurs (stuck sessions, silent CI failures, missed PR transitions), operators can now reconstruct timelines with `ao events` rather than guessing from logs. Key design decisions driven by Codex review: - FTS5 external-content table uses INSERT/DELETE triggers so search actually works without manual rebuild - ts_epoch (epoch ms) used for all time comparisons to avoid text vs SQLite datetime() ambiguity near cutoff - PRAGMA busy_timeout=3000 handles WAL lock contention across CLI/lifecycle/web processes - user_version schema versioning for future migrations - EventType renamed to ActivityEventKind to avoid collision with existing types.ts export - Event names match existing vocabulary (ci.failing, review.pending) - ActivityStateCache (Map) tracks previous activity state so lifecycle-manager can emit activity.transition diffs - session.spawn_failed captures failed spawns via wrapper try/catch - better-sqlite3 in optionalDependencies: AO keeps working if native build fails; getDb() returns null and writes become no-ops New files: - packages/core/src/events-db.ts — lazy DB init, WAL, schema+triggers - packages/core/src/activity-events.ts — write API, sanitizer - packages/core/src/query-activity-events.ts — query + FTS + stats - packages/cli/src/commands/events.ts — `ao events list/search/stats` Wired into: - lifecycle-manager.ts: lifecycle.transition + activity.transition - session-manager.ts: session.spawned, session.spawn_failed, session.killed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(activity-events): address PR review comments - redactValue: remove value-string regex check that silently erased error messages mentioning auth terms (key-based redaction is sufficient) - sanitizeData: reject payloads >16KB instead of slicing (sliced JSON is malformed and corrupts JSON output) - lifecycle-manager: prune activityStateCache alongside states in the per-poll stale-entry cleanup loop (prevents unbounded growth) - events CLI: warn on unrecognised --since duration format instead of silently applying no time filter - searchActivityEvents: accept optional projectId and push it into SQL WHERE clause instead of post-LIMIT in-memory filter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(activity-events): review pass fixes - formatRow: pad level string before chalk-wrapping so ANSI codes don't corrupt column alignment in ao events output - events-db: add PRAGMA synchronous=NORMAL (WAL+NORMAL is standard recommendation; avoids per-write fsync in the poll hot path) - events-db: emit console.warn when _dbFailed is set so operators know events are being dropped instead of failing silently forever - activity-events: remove dead redactValue wrapper (was a no-op after the previous fix; call site now directly assigns the value) - activity-events: remove unused session.cleanup from ActivityEventKind (no call site emitted it; dead API surface) - query-activity-events: add optional limit param to searchActivityEvents (default 100, max 1000, parameterized); add --limit flag to ao events search Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(activity-events): widen source/kind to allow plugin-defined values ActivityEventInput.source and .kind accept ActivityEventSource|string and ActivityEventKind|string respectively, so new event sources (e.g. scm-github, notifier-slack) don't require editing core types. The named union members are preserved for IDE autocomplete on known values. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(events): level column alignment, filter kind widening, negative limit guard - events.ts: padEnd(5) → padEnd(9) so level column aligns with the 9-char LEVEL header - query-activity-events.ts: ActivityEventFilter.kind widened to ActivityEventKind|string for consistency with ActivityEventInput.kind (plugin-defined kinds can now be queried) - query-activity-events.ts: negative/NaN limit values sanitized with Number.isFinite + Math.max(1,...) to prevent SQLite LIMIT -N returning the full table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(events): FTS alias, credential URL sanitization, periodic retention - query-activity-events.ts: use full table name in MATCH (activity_events_fts MATCH ?) instead of alias to avoid 'no such column: fts' in some FTS5 builds - activity-events.ts: redact https://token@host URL credentials in string values; add hourly retention sweep so long-lived processes don't grow DB indefinitely - events-fts-integration.test.ts: real SQLite integration test for FTS5 search, projectId filter, and epoch-based time filter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(lint): remove inline import() type annotations in integration test ESLint @typescript-eslint/consistent-type-imports forbids import() in type positions — replaced with any to keep the test working without the type imports. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(events): parse data to structured JSON in --json output; add test proving value sanitizer correctness - ao events list/search --json now parses ActivityEvent.data from JSON string back to a structured object, making it jq-friendly without requiring double-parsing by callers (P1 finding from PR review) - Add test "preserves error messages that mention sensitive words in values" to refute Greptile's false-positive P1 finding: SENSITIVE_KEY_RE only matches key names, not string values; "token expired" and "authorization header missing" values are preserved correctly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(events): BM25 relevance ordering for FTS search; add versioned JSON envelope Search now orders by FTS5 rank (BM25) instead of ts_epoch, returning most relevant events first. --json output now wraps events in { version, query, meta, events } for stable CLI contracts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(events): quote FTS tokens to prevent false negatives on operator-like terms Searching for words like "OR", "AND", or "NOT" would produce empty results because SQLite FTS5 treated them as operators after token joining. Wrapping each token in double quotes forces literal phrase matching. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(tests): update FTS assertion for quoted tokens; raise timeout on slow audit test query-activity-events test expected the old unquoted join format; update to match the quoted form added in the previous commit. The agent-report audit trail test occasionally exceeds the 5 s default on CI runners; raise to 15 s. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(qa): ISSUE-001 - type events stats entries * test(events): cover session and lifecycle event call sites * test(core): wait for lifecycle branch adoption * chore: add activity events changeset * fix activity event review feedback * batch prune old activity events * record activity event when spawn starts * Fix activity event FTS rebuild and sanitization guard --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9ca3c1fcd7
commit
2306078761
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@aoagents/ao-core": patch
|
||||
"@aoagents/ao-cli": patch
|
||||
---
|
||||
|
||||
Add SQLite-backed activity event logging for session and lifecycle diagnostics, plus `ao events` commands for listing, searching, and inspecting event log stats.
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
import chalk from "chalk";
|
||||
import type { Command } from "commander";
|
||||
import {
|
||||
queryActivityEvents,
|
||||
searchActivityEvents,
|
||||
getActivityEventStats,
|
||||
droppedEventCount,
|
||||
isActivityEventsFtsEnabled,
|
||||
type ActivityEvent,
|
||||
type ActivityEventLevel,
|
||||
type ActivityEventKind,
|
||||
} from "@aoagents/ao-core";
|
||||
|
||||
interface JsonEnvelope {
|
||||
version: number;
|
||||
query: Record<string, unknown>;
|
||||
meta: {
|
||||
resultCount: number;
|
||||
droppedEventCount: number;
|
||||
ftsEnabled: boolean;
|
||||
fallbackUsed: boolean;
|
||||
ts: string;
|
||||
};
|
||||
events: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
function toJsonOutput(ev: ActivityEvent): Record<string, unknown> {
|
||||
let data: unknown = ev.data;
|
||||
if (typeof ev.data === "string") {
|
||||
try {
|
||||
data = JSON.parse(ev.data);
|
||||
} catch {
|
||||
// leave as raw string if not valid JSON
|
||||
}
|
||||
}
|
||||
return { ...ev, data };
|
||||
}
|
||||
|
||||
function parseSinceDuration(raw: string): Date | undefined {
|
||||
const match = raw.match(/^(\d+)(m|h|d)$/);
|
||||
if (!match) return undefined;
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2];
|
||||
const ms = unit === "m" ? value * 60_000 : unit === "h" ? value * 3_600_000 : value * 86_400_000;
|
||||
return new Date(Date.now() - ms);
|
||||
}
|
||||
|
||||
function formatRow(ev: ActivityEvent): string {
|
||||
const ts = new Date(ev.tsEpoch).toLocaleTimeString();
|
||||
const session = ev.sessionId ? ev.sessionId.slice(0, 12) : "—";
|
||||
const kind = chalk.cyan(ev.kind.padEnd(22));
|
||||
// Pad raw string before chalk-wrapping: chalk adds ANSI codes that inflate .length
|
||||
const levelLabel = ev.level.padEnd(9);
|
||||
const level =
|
||||
ev.level === "error"
|
||||
? chalk.red(levelLabel)
|
||||
: ev.level === "warn"
|
||||
? chalk.yellow(levelLabel)
|
||||
: chalk.gray(levelLabel);
|
||||
return `${chalk.dim(ts)} ${kind} ${level} ${chalk.dim(session)} ${ev.summary}`;
|
||||
}
|
||||
|
||||
function jsonMeta(resultCount: number, fallbackUsed = false): JsonEnvelope["meta"] {
|
||||
return {
|
||||
resultCount,
|
||||
droppedEventCount: droppedEventCount(),
|
||||
ftsEnabled: isActivityEventsFtsEnabled(),
|
||||
fallbackUsed,
|
||||
ts: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function registerEvents(program: Command): void {
|
||||
const events = program
|
||||
.command("events")
|
||||
.description("Query activity event log (session spawns, transitions, CI failures)");
|
||||
|
||||
events
|
||||
.command("list")
|
||||
.description("List recent activity events")
|
||||
.option("-p, --project <id>", "Filter by project ID")
|
||||
.option("-s, --session <id>", "Filter by session ID")
|
||||
.option("-t, --type <kind>", "Filter by event kind (e.g. session.spawned, lifecycle.transition)")
|
||||
.option("--log-level <level>", "Filter by log level (debug, info, warn, error)")
|
||||
.option("--since <duration>", "Show events from last N minutes/hours/days (e.g. 30m, 2h, 1d)")
|
||||
.option("-n, --limit <n>", "Max results", "50")
|
||||
.option("--json", "Output as JSON")
|
||||
.action(async (opts: Record<string, string | undefined>) => {
|
||||
const sinceRaw = opts["since"];
|
||||
let since: Date | undefined;
|
||||
if (sinceRaw) {
|
||||
since = parseSinceDuration(sinceRaw);
|
||||
if (!since) {
|
||||
console.error(chalk.yellow(`Warning: unrecognised --since format "${sinceRaw}" (use e.g. 30m, 2h, 1d). No time filter applied.`));
|
||||
}
|
||||
}
|
||||
const limit = parseInt(opts["limit"] ?? "50", 10);
|
||||
|
||||
const results = queryActivityEvents({
|
||||
projectId: opts["project"],
|
||||
sessionId: opts["session"],
|
||||
kind: opts["type"] as ActivityEventKind,
|
||||
level: opts["logLevel"] as ActivityEventLevel,
|
||||
since,
|
||||
limit,
|
||||
});
|
||||
|
||||
if (opts["json"]) {
|
||||
const envelope: JsonEnvelope = {
|
||||
version: 1,
|
||||
query: {
|
||||
projectId: opts["project"] ?? null,
|
||||
sessionId: opts["session"] ?? null,
|
||||
kind: opts["type"] ?? null,
|
||||
level: opts["logLevel"] ?? null,
|
||||
since: sinceRaw ?? null,
|
||||
limit,
|
||||
},
|
||||
meta: jsonMeta(results.length),
|
||||
events: results.map(toJsonOutput),
|
||||
};
|
||||
console.log(JSON.stringify(envelope, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
console.log(chalk.dim("No events found."));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.dim(
|
||||
`${"TIME".padEnd(10)} ${"KIND".padEnd(22)} ${"LEVEL".padEnd(9)} ${"SESSION".padEnd(12)} SUMMARY`,
|
||||
),
|
||||
);
|
||||
for (const ev of results) {
|
||||
console.log(formatRow(ev));
|
||||
}
|
||||
console.log(chalk.dim(`\n${results.length} event(s)`));
|
||||
});
|
||||
|
||||
events
|
||||
.command("search <query>")
|
||||
.description("Full-text search across event summaries and data")
|
||||
.option("-p, --project <id>", "Filter by project ID")
|
||||
.option("-n, --limit <n>", "Max results", "100")
|
||||
.option("--json", "Output as JSON")
|
||||
.action(async (query: string, opts: Record<string, string | undefined>) => {
|
||||
const limit = parseInt(opts["limit"] ?? "100", 10);
|
||||
const results = searchActivityEvents(query, opts["project"], limit);
|
||||
const fallbackUsed = !isActivityEventsFtsEnabled();
|
||||
|
||||
if (opts["json"]) {
|
||||
const envelope: JsonEnvelope = {
|
||||
version: 1,
|
||||
query: { q: query, projectId: opts["project"] ?? null, limit },
|
||||
meta: jsonMeta(results.length, fallbackUsed),
|
||||
events: results.map(toJsonOutput),
|
||||
};
|
||||
console.log(JSON.stringify(envelope, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
console.log(chalk.dim("No events found."));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.dim(
|
||||
`${"TIME".padEnd(10)} ${"KIND".padEnd(22)} ${"LEVEL".padEnd(9)} ${"SESSION".padEnd(12)} SUMMARY`,
|
||||
),
|
||||
);
|
||||
for (const ev of results) {
|
||||
console.log(formatRow(ev));
|
||||
}
|
||||
console.log(chalk.dim(`\n${results.length} event(s)`));
|
||||
});
|
||||
|
||||
events
|
||||
.command("stats")
|
||||
.description("Show event log statistics")
|
||||
.action(async () => {
|
||||
const stats = getActivityEventStats();
|
||||
if (!stats) {
|
||||
console.log(chalk.yellow("Event log unavailable (better-sqlite3 not loaded)."));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold("Event Log Stats"));
|
||||
console.log(` Total events: ${stats.total}`);
|
||||
console.log(` Dropped (process): ${stats.droppedThisProcess}`);
|
||||
if (stats.oldestTs) console.log(` Oldest event: ${stats.oldestTs}`);
|
||||
if (stats.newestTs) console.log(` Newest event: ${stats.newestTs}`);
|
||||
|
||||
if (Object.keys(stats.byKind).length > 0) {
|
||||
console.log(chalk.bold("\nBy kind:"));
|
||||
const byKind = Object.entries(stats.byKind) as [string, number][];
|
||||
for (const [kind, count] of byKind.sort((a, b) => b[1] - a[1])) {
|
||||
console.log(` ${kind.padEnd(30)} ${count}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(stats.bySource).length > 0) {
|
||||
console.log(chalk.bold("\nBy source:"));
|
||||
const bySource = Object.entries(stats.bySource) as [string, number][];
|
||||
for (const [source, count] of bySource.sort((a, b) => b[1] - a[1])) {
|
||||
console.log(` ${source.padEnd(30)} ${count}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import { registerPlugin } from "./commands/plugin.js";
|
|||
import { registerProjectCommand } from "./commands/project.js";
|
||||
import { registerMigrateStorage } from "./commands/migrate-storage.js";
|
||||
import { registerCompletion } from "./commands/completion.js";
|
||||
import { registerEvents } from "./commands/events.js";
|
||||
import { getConfigInstruction } from "./lib/config-instruction.js";
|
||||
import { getCliVersion } from "./options/version.js";
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ export function createProgram(): Command {
|
|||
registerProjectCommand(program);
|
||||
registerMigrateStorage(program);
|
||||
registerCompletion(program);
|
||||
registerEvents(program);
|
||||
|
||||
program
|
||||
.command("config-help")
|
||||
|
|
|
|||
|
|
@ -90,8 +90,12 @@
|
|||
"yaml": "^2.7.0",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"better-sqlite3": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "^12.3.0",
|
||||
"@types/better-sqlite3": "^7.6.0",
|
||||
"@types/node": "^25.2.3",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"rollup": "^4.60.1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
||||
|
||||
// Mock events-db before importing activity-events so getDb is controllable
|
||||
vi.mock("../events-db.js", () => {
|
||||
const rows: unknown[] = [];
|
||||
const mockDb = {
|
||||
prepare: (sql: string) => ({
|
||||
run: (..._args: unknown[]) => {
|
||||
if (sql.includes("INSERT INTO activity_events")) {
|
||||
rows.push(_args);
|
||||
}
|
||||
},
|
||||
all: () => [],
|
||||
}),
|
||||
};
|
||||
return {
|
||||
getDb: vi.fn(() => mockDb),
|
||||
__rows: rows,
|
||||
};
|
||||
});
|
||||
|
||||
import { recordActivityEvent, droppedEventCount } from "../activity-events.js";
|
||||
import * as eventsDb from "../events-db.js";
|
||||
|
||||
describe("recordActivityEvent", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("inserts an event when DB is available", () => {
|
||||
recordActivityEvent({
|
||||
projectId: "proj-1",
|
||||
sessionId: "sess-1",
|
||||
source: "lifecycle",
|
||||
kind: "lifecycle.transition",
|
||||
summary: "working → pr_open",
|
||||
data: { from: "working", to: "pr_open" },
|
||||
});
|
||||
// getDb was called
|
||||
expect(eventsDb.getDb).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("increments droppedEventCount when DB returns null", () => {
|
||||
const before = droppedEventCount();
|
||||
vi.mocked(eventsDb.getDb).mockReturnValueOnce(null);
|
||||
recordActivityEvent({
|
||||
source: "lifecycle",
|
||||
kind: "session.spawned",
|
||||
summary: "spawned: sess-x",
|
||||
});
|
||||
expect(droppedEventCount()).toBe(before + 1);
|
||||
});
|
||||
|
||||
it("never throws even if prepare throws", () => {
|
||||
const badDb = {
|
||||
prepare: () => {
|
||||
throw new Error("disk full");
|
||||
},
|
||||
};
|
||||
vi.mocked(eventsDb.getDb).mockReturnValueOnce(badDb as any);
|
||||
expect(() =>
|
||||
recordActivityEvent({
|
||||
source: "session-manager",
|
||||
kind: "session.killed",
|
||||
summary: "killed: sess-1",
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("never throws even if data sanitization throws", () => {
|
||||
const data = {};
|
||||
Object.defineProperty(data, "bad", {
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
throw new Error("getter failed");
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
recordActivityEvent({
|
||||
source: "session-manager",
|
||||
kind: "session.spawned",
|
||||
summary: "spawned",
|
||||
data,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("sanitizes sensitive data keys", () => {
|
||||
let capturedData: unknown;
|
||||
const captureDb = {
|
||||
prepare: (_sql: string) => ({
|
||||
run: (...args: unknown[]) => {
|
||||
capturedData = args[8]; // data is 9th param (index 8)
|
||||
},
|
||||
all: () => [],
|
||||
}),
|
||||
};
|
||||
vi.mocked(eventsDb.getDb).mockReturnValueOnce(captureDb as any);
|
||||
recordActivityEvent({
|
||||
source: "lifecycle",
|
||||
kind: "session.spawned",
|
||||
summary: "spawned",
|
||||
data: { token: "secret123", agent: "claude-code" },
|
||||
});
|
||||
const parsed = JSON.parse(capturedData as string);
|
||||
expect(parsed["token"]).toBe("[redacted]");
|
||||
expect(parsed["agent"]).toBe("claude-code");
|
||||
});
|
||||
|
||||
it("sanitizes nested sensitive data keys and credential URLs", () => {
|
||||
let capturedData: unknown;
|
||||
const captureDb = {
|
||||
prepare: (_sql: string) => ({
|
||||
run: (...args: unknown[]) => {
|
||||
capturedData = args[8];
|
||||
},
|
||||
all: () => [],
|
||||
}),
|
||||
};
|
||||
vi.mocked(eventsDb.getDb).mockReturnValueOnce(captureDb as any);
|
||||
recordActivityEvent({
|
||||
source: "lifecycle",
|
||||
kind: "session.spawned",
|
||||
summary: "spawned",
|
||||
data: {
|
||||
request: {
|
||||
headers: {
|
||||
authorization: "Bearer ghp_secret",
|
||||
url: "HTTPS://token@example.com/path",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const parsed = JSON.parse(capturedData as string);
|
||||
expect(parsed["request"]["headers"]["authorization"]).toBe("[redacted]");
|
||||
expect(parsed["request"]["headers"]["url"]).toBe("https://[redacted]@example.com/path");
|
||||
});
|
||||
|
||||
it("preserves error messages that mention sensitive words in values", () => {
|
||||
// Greptile flagged this as a bug: values like "token expired" or
|
||||
// "authorization header missing" would be redacted. They are not —
|
||||
// SENSITIVE_KEY_RE only matches key names, not string values.
|
||||
let capturedData: unknown;
|
||||
const captureDb = {
|
||||
prepare: (_sql: string) => ({
|
||||
run: (...args: unknown[]) => {
|
||||
capturedData = args[8];
|
||||
},
|
||||
all: () => [],
|
||||
}),
|
||||
};
|
||||
vi.mocked(eventsDb.getDb).mockReturnValueOnce(captureDb as any);
|
||||
recordActivityEvent({
|
||||
source: "session-manager",
|
||||
kind: "session.spawn_failed",
|
||||
summary: "spawn failed",
|
||||
data: {
|
||||
reason: "token expired",
|
||||
message: "authorization header missing",
|
||||
agent: "claude-code",
|
||||
},
|
||||
});
|
||||
const parsed = JSON.parse(capturedData as string);
|
||||
expect(parsed["reason"]).toBe("token expired");
|
||||
expect(parsed["message"]).toBe("authorization header missing");
|
||||
expect(parsed["agent"]).toBe("claude-code");
|
||||
});
|
||||
|
||||
it("handles BigInt in data without throwing", () => {
|
||||
let capturedData: unknown;
|
||||
const captureDb = {
|
||||
prepare: (_sql: string) => ({
|
||||
run: (...args: unknown[]) => {
|
||||
capturedData = args[8];
|
||||
},
|
||||
all: () => [],
|
||||
}),
|
||||
};
|
||||
vi.mocked(eventsDb.getDb).mockReturnValueOnce(captureDb as any);
|
||||
expect(() =>
|
||||
recordActivityEvent({
|
||||
source: "lifecycle",
|
||||
kind: "session.spawned",
|
||||
summary: "spawned",
|
||||
data: { big: BigInt(9007199254740991) as any },
|
||||
}),
|
||||
).not.toThrow();
|
||||
expect(typeof capturedData).toBe("string");
|
||||
});
|
||||
|
||||
it("truncates summary to 500 chars", () => {
|
||||
let capturedSummary: unknown;
|
||||
const captureDb = {
|
||||
prepare: (_sql: string) => ({
|
||||
run: (...args: unknown[]) => {
|
||||
capturedSummary = args[7]; // summary is 8th param (index 7)
|
||||
},
|
||||
all: () => [],
|
||||
}),
|
||||
};
|
||||
vi.mocked(eventsDb.getDb).mockReturnValueOnce(captureDb as any);
|
||||
const longSummary = "x".repeat(600);
|
||||
recordActivityEvent({
|
||||
source: "lifecycle",
|
||||
kind: "lifecycle.transition",
|
||||
summary: longSummary,
|
||||
});
|
||||
expect((capturedSummary as string).length).toBe(500);
|
||||
expect(capturedSummary).toMatch(/\.\.\.$/);
|
||||
});
|
||||
});
|
||||
|
|
@ -444,7 +444,7 @@ describe("applyAgentReport", () => {
|
|||
).toThrow(/not found/);
|
||||
});
|
||||
|
||||
it("bounds the on-disk audit trail to recent entries", () => {
|
||||
it("bounds the on-disk audit trail to recent entries", { timeout: 15000 }, () => {
|
||||
for (let index = 0; index < 260; index += 1) {
|
||||
applyAgentReport(dataDir, sessionId, {
|
||||
state: "working",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
/**
|
||||
* Integration test for FTS5 search using a real in-memory SQLite database.
|
||||
* Verifies that the FTS virtual table, triggers, and MATCH query actually work.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
// We need a real better-sqlite3 instance for this test.
|
||||
// If it's not available, skip.
|
||||
let Database: (new (path: string) => any) | null = null;
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
Database = require("better-sqlite3") as new (path: string) => any;
|
||||
} catch {
|
||||
// better-sqlite3 unavailable — integration tests will be skipped
|
||||
}
|
||||
|
||||
// Mock events-db to inject our real in-memory DB
|
||||
vi.mock("../events-db.js", () => ({
|
||||
getDb: vi.fn(),
|
||||
isActivityEventsFtsEnabled: vi.fn(() => true),
|
||||
}));
|
||||
|
||||
import * as eventsDb from "../events-db.js";
|
||||
import { recordActivityEvent } from "../activity-events.js";
|
||||
import { searchActivityEvents, queryActivityEvents } from "../query-activity-events.js";
|
||||
|
||||
function openMemoryDb(): any {
|
||||
if (!Database) throw new Error("better-sqlite3 not available");
|
||||
const db = new (Database as any)(":memory:");
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS activity_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts_epoch INTEGER NOT NULL,
|
||||
ts TEXT NOT NULL,
|
||||
project_id TEXT,
|
||||
session_id TEXT,
|
||||
source TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
log_level TEXT NOT NULL DEFAULT 'info',
|
||||
summary TEXT NOT NULL,
|
||||
data TEXT
|
||||
);
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS activity_events_fts USING fts5(
|
||||
summary, data,
|
||||
content='activity_events',
|
||||
content_rowid='id'
|
||||
);
|
||||
CREATE TRIGGER IF NOT EXISTS activity_events_ai
|
||||
AFTER INSERT ON activity_events
|
||||
BEGIN
|
||||
INSERT INTO activity_events_fts(rowid, summary, data)
|
||||
VALUES (new.id, new.summary, new.data);
|
||||
END;
|
||||
CREATE TRIGGER IF NOT EXISTS activity_events_ad
|
||||
AFTER DELETE ON activity_events
|
||||
BEGIN
|
||||
INSERT INTO activity_events_fts(activity_events_fts, rowid, summary, data)
|
||||
VALUES ('delete', old.id, old.summary, old.data);
|
||||
END;
|
||||
CREATE TRIGGER IF NOT EXISTS activity_events_au
|
||||
AFTER UPDATE ON activity_events
|
||||
BEGIN
|
||||
INSERT INTO activity_events_fts(activity_events_fts, rowid, summary, data)
|
||||
VALUES ('delete', old.id, old.summary, old.data);
|
||||
INSERT INTO activity_events_fts(rowid, summary, data)
|
||||
VALUES (new.id, new.summary, new.data);
|
||||
END;
|
||||
CREATE INDEX IF NOT EXISTS idx_ae_ts ON activity_events(ts_epoch);
|
||||
CREATE INDEX IF NOT EXISTS idx_ae_session ON activity_events(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ae_project ON activity_events(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ae_type ON activity_events(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_ae_source ON activity_events(source);
|
||||
PRAGMA user_version = 1;
|
||||
`);
|
||||
return db;
|
||||
}
|
||||
|
||||
const itIfAvailable = Database ? it : it.skip;
|
||||
|
||||
describe("FTS5 integration (real SQLite)", () => {
|
||||
let db: ReturnType<typeof openMemoryDb>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
if (!Database) return;
|
||||
db = openMemoryDb();
|
||||
vi.mocked(eventsDb.getDb).mockReturnValue(db as any);
|
||||
});
|
||||
|
||||
itIfAvailable("inserts and FTS-searches events end-to-end", () => {
|
||||
recordActivityEvent({
|
||||
source: "session-manager",
|
||||
kind: "session.spawned",
|
||||
summary: "spawned agent for feature-branch",
|
||||
data: { agent: "claude-code", branch: "feat/my-feature" },
|
||||
});
|
||||
recordActivityEvent({
|
||||
source: "lifecycle",
|
||||
kind: "lifecycle.transition",
|
||||
summary: "working → pr_open",
|
||||
data: { from: "working", to: "pr_open" },
|
||||
});
|
||||
|
||||
const results = searchActivityEvents("spawned");
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0]!.kind).toBe("session.spawned");
|
||||
expect(results[0]!.summary).toContain("spawned");
|
||||
});
|
||||
|
||||
itIfAvailable("FTS search finds terms in data field", () => {
|
||||
recordActivityEvent({
|
||||
source: "session-manager",
|
||||
kind: "session.spawned",
|
||||
summary: "spawned",
|
||||
data: { agent: "claude-code", branch: "feat/my-feature" },
|
||||
});
|
||||
|
||||
const results = searchActivityEvents("claude");
|
||||
expect(results).toHaveLength(1);
|
||||
});
|
||||
|
||||
itIfAvailable("FTS search returns [] for non-matching term", () => {
|
||||
recordActivityEvent({
|
||||
source: "lifecycle",
|
||||
kind: "lifecycle.transition",
|
||||
summary: "working → pr_open",
|
||||
});
|
||||
|
||||
const results = searchActivityEvents("nonexistentterm12345");
|
||||
expect(results).toHaveLength(0);
|
||||
});
|
||||
|
||||
itIfAvailable("FTS search filters by projectId in SQL (before LIMIT)", () => {
|
||||
recordActivityEvent({
|
||||
projectId: "proj-a",
|
||||
source: "lifecycle",
|
||||
kind: "session.spawned",
|
||||
summary: "spawned in proj-a",
|
||||
});
|
||||
recordActivityEvent({
|
||||
projectId: "proj-b",
|
||||
source: "lifecycle",
|
||||
kind: "session.spawned",
|
||||
summary: "spawned in proj-b",
|
||||
});
|
||||
|
||||
const results = searchActivityEvents("spawned", "proj-a");
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0]!.projectId).toBe("proj-a");
|
||||
});
|
||||
|
||||
itIfAvailable("queryActivityEvents returns events with epoch filter", () => {
|
||||
const past = Date.now() - 60_000;
|
||||
db.prepare(
|
||||
`INSERT INTO activity_events (ts_epoch, ts, source, type, log_level, summary)
|
||||
VALUES (?, ?, 'lifecycle', 'session.spawned', 'info', 'old event')`,
|
||||
).run(past, new Date(past).toISOString());
|
||||
|
||||
recordActivityEvent({
|
||||
source: "lifecycle",
|
||||
kind: "session.spawned",
|
||||
summary: "new event",
|
||||
});
|
||||
|
||||
const recent = queryActivityEvents({ since: new Date(Date.now() - 1000) });
|
||||
expect(recent.length).toBeGreaterThanOrEqual(1);
|
||||
expect(recent.some((e) => e.summary === "new event")).toBe(true);
|
||||
expect(recent.every((e) => e.summary !== "old event")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { createLifecycleManager } from "../lifecycle-manager.js";
|
||||
import { recordActivityEvent } from "../activity-events.js";
|
||||
import { DEFAULT_BUGBOT_COMMENTS_MESSAGE } from "../config.js";
|
||||
import {
|
||||
resolvePREnrichmentDecision,
|
||||
|
|
@ -34,6 +35,10 @@ import {
|
|||
type MockPlugins,
|
||||
} from "./test-utils.js";
|
||||
|
||||
vi.mock("../activity-events.js", () => ({
|
||||
recordActivityEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
let env: TestEnvironment;
|
||||
let plugins: MockPlugins;
|
||||
let mockRegistry: PluginRegistry;
|
||||
|
|
@ -46,6 +51,7 @@ beforeEach(() => {
|
|||
mockRegistry = createMockRegistry({ runtime: plugins.runtime, agent: plugins.agent });
|
||||
mockSessionManager = createMockSessionManager();
|
||||
config = env.config;
|
||||
vi.mocked(recordActivityEvent).mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -278,6 +284,44 @@ describe("check (single session)", () => {
|
|||
expect(meta!["status"]).toBe("working");
|
||||
});
|
||||
|
||||
it("records lifecycle.transition when status changes", async () => {
|
||||
const lm = setupCheck("app-1", {
|
||||
session: makeSession({ status: "spawning" }),
|
||||
});
|
||||
|
||||
await lm.check("app-1");
|
||||
|
||||
expect(recordActivityEvent).toHaveBeenCalledWith({
|
||||
projectId: "my-app",
|
||||
sessionId: "app-1",
|
||||
source: "lifecycle",
|
||||
kind: "lifecycle.transition",
|
||||
level: "info",
|
||||
summary: "spawning → working",
|
||||
data: { from: "spawning", to: "working" },
|
||||
});
|
||||
});
|
||||
|
||||
it("records activity.transition after observed activity changes", async () => {
|
||||
const session = makeSession({ id: "app-activity", status: "working" });
|
||||
const lm = setupCheck("app-activity", { session });
|
||||
|
||||
await lm.check("app-activity");
|
||||
vi.mocked(recordActivityEvent).mockClear();
|
||||
vi.mocked(plugins.agent.getActivityState).mockResolvedValue({ state: "idle" });
|
||||
|
||||
await lm.check("app-activity");
|
||||
|
||||
expect(recordActivityEvent).toHaveBeenCalledWith({
|
||||
projectId: "my-app",
|
||||
sessionId: "app-activity",
|
||||
source: "lifecycle",
|
||||
kind: "activity.transition",
|
||||
summary: "active → idle",
|
||||
data: { from: "active", to: "idle" },
|
||||
});
|
||||
});
|
||||
|
||||
it("records split lifecycle observability for transitions", async () => {
|
||||
const lm = setupCheck("app-1", {
|
||||
session: makeSession({ status: "spawning" }),
|
||||
|
|
@ -1236,12 +1280,12 @@ describe("check (single session)", () => {
|
|||
|
||||
try {
|
||||
lm.start(60_000);
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
|
||||
const adoptedCount = [sessionA.branch, sessionB.branch].filter(
|
||||
(branch) => branch === "shared-branch",
|
||||
).length;
|
||||
expect(adoptedCount).toBe(1);
|
||||
await vi.waitFor(() => {
|
||||
const adoptedCount = [sessionA.branch, sessionB.branch].filter(
|
||||
(branch) => branch === "shared-branch",
|
||||
).length;
|
||||
expect(adoptedCount).toBe(1);
|
||||
});
|
||||
expect(mockSessionManager.list).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
lm.stop();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("../events-db.js", () => ({
|
||||
getDb: vi.fn(),
|
||||
isActivityEventsFtsEnabled: vi.fn(() => true),
|
||||
}));
|
||||
vi.mock("../activity-events.js", async (importOriginal) => {
|
||||
const mod = await (importOriginal as () => Promise<any>)();
|
||||
return { ...mod, droppedEventCount: () => 0 };
|
||||
});
|
||||
|
||||
import { queryActivityEvents, searchActivityEvents, getActivityEventStats } from "../query-activity-events.js";
|
||||
import * as eventsDb from "../events-db.js";
|
||||
|
||||
const sampleRow = {
|
||||
id: 1,
|
||||
ts_epoch: Date.now(),
|
||||
ts: new Date().toISOString(),
|
||||
project_id: "proj-1",
|
||||
session_id: "sess-1",
|
||||
source: "lifecycle",
|
||||
type: "lifecycle.transition",
|
||||
log_level: "info",
|
||||
summary: "working → pr_open",
|
||||
data: JSON.stringify({ from: "working", to: "pr_open" }),
|
||||
};
|
||||
|
||||
function makeDb(rows: unknown[] = [sampleRow]) {
|
||||
return {
|
||||
prepare: () => ({
|
||||
all: (..._args: unknown[]) => rows,
|
||||
run: () => {},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("queryActivityEvents", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it("returns [] when DB is null", () => {
|
||||
vi.mocked(eventsDb.getDb).mockReturnValue(null);
|
||||
expect(queryActivityEvents()).toEqual([]);
|
||||
});
|
||||
|
||||
it("maps rows to ActivityEvent shape", () => {
|
||||
vi.mocked(eventsDb.getDb).mockReturnValue(makeDb() as any);
|
||||
const results = queryActivityEvents();
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0]!.kind).toBe("lifecycle.transition");
|
||||
expect(results[0]!.projectId).toBe("proj-1");
|
||||
});
|
||||
|
||||
it("clamps limit to max 1000", () => {
|
||||
let capturedSql = "";
|
||||
const captureDb = {
|
||||
prepare: (sql: string) => {
|
||||
capturedSql = sql;
|
||||
return { all: () => [] };
|
||||
},
|
||||
};
|
||||
vi.mocked(eventsDb.getDb).mockReturnValue(captureDb as any);
|
||||
queryActivityEvents({ limit: 9999 });
|
||||
expect(capturedSql).toContain("LIMIT ?");
|
||||
});
|
||||
|
||||
it("returns [] on DB error", () => {
|
||||
const badDb = {
|
||||
prepare: () => ({
|
||||
all: () => {
|
||||
throw new Error("disk error");
|
||||
},
|
||||
}),
|
||||
};
|
||||
vi.mocked(eventsDb.getDb).mockReturnValue(badDb as any);
|
||||
expect(queryActivityEvents()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("searchActivityEvents", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it("returns [] for empty query", () => {
|
||||
vi.mocked(eventsDb.getDb).mockReturnValue(makeDb() as any);
|
||||
expect(searchActivityEvents("")).toEqual([]);
|
||||
expect(searchActivityEvents(" ")).toEqual([]);
|
||||
});
|
||||
|
||||
it("sanitizes query — only word characters reach FTS", () => {
|
||||
let capturedFtsQuery = "";
|
||||
const captureDb = {
|
||||
prepare: () => ({
|
||||
all: (q: string) => {
|
||||
capturedFtsQuery = q;
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
};
|
||||
vi.mocked(eventsDb.getDb).mockReturnValue(captureDb as any);
|
||||
searchActivityEvents("spawn; DROP TABLE activity_events--");
|
||||
// SQL injection stripped; only word tokens joined by AND
|
||||
expect(capturedFtsQuery).toBe('"spawn" AND "DROP" AND "TABLE" AND "activity_events"');
|
||||
});
|
||||
|
||||
it("returns [] when DB is null", () => {
|
||||
vi.mocked(eventsDb.getDb).mockReturnValue(null);
|
||||
expect(searchActivityEvents("spawned")).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses a bounded LIKE fallback when FTS is unavailable", () => {
|
||||
let capturedSql = "";
|
||||
let capturedArgs: unknown[] = [];
|
||||
const captureDb = {
|
||||
prepare: (sql: string) => {
|
||||
capturedSql = sql;
|
||||
return {
|
||||
all: (...args: unknown[]) => {
|
||||
capturedArgs = args;
|
||||
return [sampleRow];
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
vi.mocked(eventsDb.getDb).mockReturnValue(captureDb as any);
|
||||
vi.mocked(eventsDb.isActivityEventsFtsEnabled).mockReturnValueOnce(false);
|
||||
|
||||
const results = searchActivityEvents("spawn failed", "proj-1", 10);
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
expect(capturedSql).toContain("LIKE");
|
||||
expect(capturedSql).toContain("ae.project_id = ?");
|
||||
expect(capturedArgs).toEqual(["%spawn failed%", "%spawn failed%", "proj-1", 10]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getActivityEventStats", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it("returns null when DB is null", () => {
|
||||
vi.mocked(eventsDb.getDb).mockReturnValue(null);
|
||||
expect(getActivityEventStats()).toBeNull();
|
||||
});
|
||||
|
||||
it("aggregates byKind and bySource", () => {
|
||||
let callCount = 0;
|
||||
const stubDb = {
|
||||
prepare: () => ({
|
||||
all: () => {
|
||||
callCount++;
|
||||
if (callCount === 1) return [{ cnt: 5 }]; // total
|
||||
if (callCount === 2)
|
||||
return [
|
||||
{ type: "lifecycle.transition", cnt: 3 },
|
||||
{ type: "session.spawned", cnt: 2 },
|
||||
];
|
||||
if (callCount === 3)
|
||||
return [
|
||||
{ source: "lifecycle", cnt: 4 },
|
||||
{ source: "session-manager", cnt: 1 },
|
||||
];
|
||||
return [{ oldest: "2026-01-01T00:00:00Z", newest: "2026-04-27T00:00:00Z" }];
|
||||
},
|
||||
}),
|
||||
};
|
||||
vi.mocked(eventsDb.getDb).mockReturnValue(stubDb as any);
|
||||
const stats = getActivityEventStats();
|
||||
expect(stats).not.toBeNull();
|
||||
expect(stats!.total).toBe(5);
|
||||
expect(stats!.byKind["lifecycle.transition"]).toBe(3);
|
||||
expect(stats!.bySource["lifecycle"]).toBe(4);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,9 +1,14 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { createSessionManager } from "../session-manager.js";
|
||||
import { writeMetadata } from "../metadata.js";
|
||||
import { recordActivityEvent } from "../activity-events.js";
|
||||
import type { OrchestratorConfig, PluginRegistry, Agent } from "../types.js";
|
||||
import { setupTestContext, teardownTestContext, makeHandle, type TestContext } from "./test-utils.js";
|
||||
|
||||
vi.mock("../activity-events.js", () => ({
|
||||
recordActivityEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock child_process module with custom promisify
|
||||
vi.mock("node:child_process", () => {
|
||||
const execFileMock = vi.fn() as any;
|
||||
|
|
@ -32,6 +37,7 @@ let config: OrchestratorConfig;
|
|||
beforeEach(() => {
|
||||
ctx = setupTestContext();
|
||||
({ sessionsDir, mockRegistry, config } = ctx);
|
||||
vi.mocked(recordActivityEvent).mockClear();
|
||||
|
||||
// Create an opencode agent mock
|
||||
const opencodeAgent: Agent = {
|
||||
|
|
@ -64,6 +70,88 @@ afterEach(() => {
|
|||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("activity event logging", () => {
|
||||
it("records session.spawned after a successful spawn", async () => {
|
||||
const { execFile } = await import("node:child_process");
|
||||
vi.mocked(execFile).mockImplementation(((_file: string, _args: string[], options: any, callback?: any) => {
|
||||
const cb = typeof options === "function" ? options : callback;
|
||||
if (cb) cb(null, "", "");
|
||||
return null as any;
|
||||
}) as any);
|
||||
|
||||
vi.useFakeTimers();
|
||||
config.projects["my-app"]!.agent = "mock-agent";
|
||||
const sm = createSessionManager({ config, registry: mockRegistry });
|
||||
|
||||
const spawnPromise = sm.spawn({ projectId: "my-app" });
|
||||
await vi.runAllTimersAsync();
|
||||
const session = await spawnPromise;
|
||||
|
||||
expect(recordActivityEvent).toHaveBeenCalledWith({
|
||||
projectId: "my-app",
|
||||
source: "session-manager",
|
||||
kind: "session.spawn_started",
|
||||
summary: "spawn started",
|
||||
data: { agent: undefined },
|
||||
});
|
||||
expect(recordActivityEvent).toHaveBeenCalledWith({
|
||||
projectId: "my-app",
|
||||
sessionId: session.id,
|
||||
source: "session-manager",
|
||||
kind: "session.spawned",
|
||||
summary: `spawned: ${session.id}`,
|
||||
data: { agent: "mock-agent", branch: session.branch },
|
||||
});
|
||||
});
|
||||
|
||||
it("records session.spawn_failed when spawn fails", async () => {
|
||||
const sm = createSessionManager({ config, registry: mockRegistry });
|
||||
|
||||
await expect(sm.spawn({ projectId: "missing-project", prompt: "nope" })).rejects.toThrow(
|
||||
"Unknown project: missing-project",
|
||||
);
|
||||
|
||||
expect(recordActivityEvent).toHaveBeenCalledWith({
|
||||
projectId: "missing-project",
|
||||
source: "session-manager",
|
||||
kind: "session.spawn_started",
|
||||
summary: "spawn started",
|
||||
data: { agent: undefined },
|
||||
});
|
||||
expect(recordActivityEvent).toHaveBeenCalledWith({
|
||||
projectId: "missing-project",
|
||||
source: "session-manager",
|
||||
kind: "session.spawn_failed",
|
||||
level: "error",
|
||||
summary: "spawn failed",
|
||||
data: { reason: "Unknown project: missing-project" },
|
||||
});
|
||||
});
|
||||
|
||||
it("records session.killed after successful kill cleanup", async () => {
|
||||
writeMetadata(sessionsDir, "app-kill", {
|
||||
worktree: "/tmp/ws",
|
||||
branch: "main",
|
||||
status: "working",
|
||||
project: "my-app",
|
||||
agent: "opencode",
|
||||
runtimeHandle: makeHandle("rt-kill"),
|
||||
});
|
||||
|
||||
const sm = createSessionManager({ config, registry: mockRegistry });
|
||||
await sm.kill("app-kill");
|
||||
|
||||
expect(recordActivityEvent).toHaveBeenCalledWith({
|
||||
projectId: "my-app",
|
||||
sessionId: "app-kill",
|
||||
source: "session-manager",
|
||||
kind: "session.killed",
|
||||
summary: "killed: app-kill",
|
||||
data: { reason: "manually_killed" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteSession retry loop", () => {
|
||||
it("verifies retry count - calls execFileAsync 3 times when all attempts fail", async () => {
|
||||
const { execFile } = await import("node:child_process");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* Activity event logging — write API.
|
||||
*
|
||||
* recordActivityEvent() is synchronous and best-effort: it never throws.
|
||||
* If the DB is unavailable or a write fails, the event is dropped and
|
||||
* droppedEventCount is incremented.
|
||||
*
|
||||
* droppedEventCount is process-local. Events dropped in other processes
|
||||
* (web server, lifecycle manager) are not reflected here.
|
||||
*/
|
||||
|
||||
import { getDb } from "./events-db.js";
|
||||
|
||||
// Distinct names to avoid collision with types.ts EventType / EventSource.
|
||||
export type ActivityEventSource = "lifecycle" | "session-manager" | "api" | "ui";
|
||||
|
||||
export type ActivityEventKind =
|
||||
| "session.spawn_started"
|
||||
| "session.spawned"
|
||||
| "session.spawn_failed"
|
||||
| "session.killed"
|
||||
| "activity.transition"
|
||||
| "lifecycle.transition"
|
||||
| "ci.failing"
|
||||
| "review.pending";
|
||||
|
||||
export type ActivityEventLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
export interface ActivityEventInput {
|
||||
projectId?: string;
|
||||
sessionId?: string;
|
||||
source: ActivityEventSource | string;
|
||||
kind: ActivityEventKind | string;
|
||||
level?: ActivityEventLevel;
|
||||
summary: string;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ActivityEvent {
|
||||
id: number;
|
||||
tsEpoch: number;
|
||||
ts: string;
|
||||
projectId: string | null;
|
||||
sessionId: string | null;
|
||||
source: string;
|
||||
kind: string;
|
||||
level: string;
|
||||
summary: string;
|
||||
data: string | null;
|
||||
rank?: number;
|
||||
}
|
||||
|
||||
let _droppedEventCount = 0;
|
||||
let _lastPruneMs = 0;
|
||||
const PRUNE_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
const PRUNE_BATCH_SIZE = 1000;
|
||||
|
||||
/** Number of events dropped due to DB errors in this process. */
|
||||
export function droppedEventCount(): number {
|
||||
return _droppedEventCount;
|
||||
}
|
||||
|
||||
function pruneOldEvents(db: ReturnType<typeof getDb>, cutoff: number): void {
|
||||
db
|
||||
?.prepare(
|
||||
`DELETE FROM activity_events
|
||||
WHERE rowid IN (
|
||||
SELECT rowid FROM activity_events WHERE ts_epoch < ? LIMIT ?
|
||||
)`,
|
||||
)
|
||||
.run(cutoff, PRUNE_BATCH_SIZE);
|
||||
}
|
||||
|
||||
// Patterns that indicate sensitive field names
|
||||
const SENSITIVE_KEY_RE = /token|password|secret|authorization|cookie|api[-_]?key/i;
|
||||
// URL credentials: https://token@host or http://user:pass@host
|
||||
const CREDENTIAL_URL_RE = /https?:\/\/[^@\s]+@/gi;
|
||||
|
||||
function sanitizeValue(value: unknown, seen: WeakSet<object>): unknown {
|
||||
if (typeof value === "bigint") return value.toString();
|
||||
if (typeof value === "string") return value.replace(CREDENTIAL_URL_RE, "https://[redacted]@");
|
||||
if (value === null || typeof value !== "object") return value;
|
||||
|
||||
if (seen.has(value)) return "[circular]";
|
||||
seen.add(value);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => sanitizeValue(item, seen));
|
||||
}
|
||||
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||
cleaned[k] = SENSITIVE_KEY_RE.test(k) ? "[redacted]" : sanitizeValue(v, seen);
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
function sanitizeData(data: Record<string, unknown>): string | undefined {
|
||||
const cleaned = sanitizeValue(data, new WeakSet<object>());
|
||||
|
||||
let json: string;
|
||||
try {
|
||||
json = JSON.stringify(cleaned);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Reject if over 16 KB after sanitization (slicing would produce malformed JSON)
|
||||
if (json.length > 16 * 1024) {
|
||||
return undefined;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
function sanitizeSummary(summary: string): string {
|
||||
if (summary.length <= 500) return summary;
|
||||
return `${summary.slice(0, 497)}...`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an activity event. Synchronous, best-effort — never throws.
|
||||
*/
|
||||
export function recordActivityEvent(event: ActivityEventInput): void {
|
||||
try {
|
||||
const db = getDb();
|
||||
if (!db) {
|
||||
_droppedEventCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const ts = new Date(now).toISOString();
|
||||
const summary = sanitizeSummary(event.summary);
|
||||
const data = event.data ? sanitizeData(event.data) : undefined;
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO activity_events
|
||||
(ts_epoch, ts, project_id, session_id, source, type, log_level, summary, data)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
now,
|
||||
ts,
|
||||
event.projectId ?? null,
|
||||
event.sessionId ?? null,
|
||||
event.source,
|
||||
event.kind,
|
||||
event.level ?? "info",
|
||||
summary,
|
||||
data ?? null,
|
||||
);
|
||||
// Periodically purge old events so long-lived processes don't grow the DB indefinitely
|
||||
if (now - _lastPruneMs >= PRUNE_INTERVAL_MS) {
|
||||
_lastPruneMs = now;
|
||||
pruneOldEvents(db, now - RETENTION_MS);
|
||||
}
|
||||
} catch {
|
||||
_droppedEventCount++;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* SQLite database layer for activity event logging.
|
||||
*
|
||||
* Lazy-initialized singleton. Opens on first call to getDb(), never on import.
|
||||
* Returns null if better-sqlite3 is unavailable (native build failure, optional dep).
|
||||
* WAL mode + busy_timeout for multi-process concurrent access.
|
||||
*/
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { getAoBaseDir } from "./paths.js";
|
||||
|
||||
// Use createRequire so we can try/catch on native module load without top-level await.
|
||||
const _require = createRequire(import.meta.url);
|
||||
|
||||
type BetterSqlite3Database = {
|
||||
pragma(source: string, options?: { simple?: boolean }): unknown;
|
||||
exec(source: string): void;
|
||||
prepare(source: string): { run(...args: unknown[]): unknown; all(...args: unknown[]): unknown[] };
|
||||
close(): void;
|
||||
};
|
||||
|
||||
let _db: BetterSqlite3Database | null = null;
|
||||
let _dbFailed = false;
|
||||
let _ftsEnabled = false;
|
||||
const PRUNE_BATCH_SIZE = 1000;
|
||||
|
||||
function getEventsDbPath(): string {
|
||||
return join(getAoBaseDir(), "activity-events.db");
|
||||
}
|
||||
|
||||
function initSchema(db: BetterSqlite3Database): void {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS activity_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts_epoch INTEGER NOT NULL,
|
||||
ts TEXT NOT NULL,
|
||||
project_id TEXT,
|
||||
session_id TEXT,
|
||||
source TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
log_level TEXT NOT NULL DEFAULT 'info',
|
||||
summary TEXT NOT NULL,
|
||||
data TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ae_ts ON activity_events(ts_epoch);
|
||||
CREATE INDEX IF NOT EXISTS idx_ae_session ON activity_events(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ae_project ON activity_events(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ae_type ON activity_events(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_ae_source ON activity_events(source);
|
||||
`);
|
||||
}
|
||||
|
||||
function initFts(db: BetterSqlite3Database): void {
|
||||
db.exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS activity_events_fts USING fts5(
|
||||
summary, data,
|
||||
content='activity_events',
|
||||
content_rowid='id'
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS activity_events_ai
|
||||
AFTER INSERT ON activity_events
|
||||
BEGIN
|
||||
INSERT INTO activity_events_fts(rowid, summary, data)
|
||||
VALUES (new.id, new.summary, new.data);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS activity_events_ad
|
||||
AFTER DELETE ON activity_events
|
||||
BEGIN
|
||||
INSERT INTO activity_events_fts(activity_events_fts, rowid, summary, data)
|
||||
VALUES ('delete', old.id, old.summary, old.data);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS activity_events_au
|
||||
AFTER UPDATE ON activity_events
|
||||
BEGIN
|
||||
INSERT INTO activity_events_fts(activity_events_fts, rowid, summary, data)
|
||||
VALUES ('delete', old.id, old.summary, old.data);
|
||||
INSERT INTO activity_events_fts(rowid, summary, data)
|
||||
VALUES (new.id, new.summary, new.data);
|
||||
END;
|
||||
`);
|
||||
|
||||
// Backfill rows written before FTS was available; triggers only cover future writes.
|
||||
db.exec("INSERT INTO activity_events_fts(activity_events_fts) VALUES('rebuild')");
|
||||
}
|
||||
|
||||
function pruneOldEvents(db: BetterSqlite3Database, cutoff: number): void {
|
||||
db
|
||||
.prepare(
|
||||
`DELETE FROM activity_events
|
||||
WHERE rowid IN (
|
||||
SELECT rowid FROM activity_events WHERE ts_epoch < ? LIMIT ?
|
||||
)`,
|
||||
)
|
||||
.run(cutoff, PRUNE_BATCH_SIZE);
|
||||
}
|
||||
|
||||
function openDb(): BetterSqlite3Database {
|
||||
const Database = _require("better-sqlite3") as new (path: string) => BetterSqlite3Database;
|
||||
mkdirSync(getAoBaseDir(), { recursive: true });
|
||||
const db = new Database(getEventsDbPath());
|
||||
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.pragma("busy_timeout = 3000");
|
||||
// WAL + NORMAL gives one checkpoint window of exposure (acceptable for a diagnostic log)
|
||||
db.pragma("synchronous = NORMAL");
|
||||
|
||||
const version = db.pragma("user_version", { simple: true }) as number;
|
||||
initSchema(db);
|
||||
if (version < 1) {
|
||||
db.pragma("user_version = 1");
|
||||
}
|
||||
|
||||
try {
|
||||
initFts(db);
|
||||
_ftsEnabled = true;
|
||||
} catch (err) {
|
||||
_ftsEnabled = false;
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"[ao] activity-events FTS unavailable — writes will continue and search will use a bounded LIKE fallback:",
|
||||
err instanceof Error ? err.message : String(err),
|
||||
);
|
||||
}
|
||||
|
||||
// 7-day retention using epoch comparison (no text/datetime ambiguity)
|
||||
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
||||
pruneOldEvents(db, cutoff);
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
/** Whether the current process has an initialized FTS5 search table. */
|
||||
export function isActivityEventsFtsEnabled(): boolean {
|
||||
return _ftsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lazily-initialized DB connection.
|
||||
* Returns null if better-sqlite3 failed to load or init — callers should treat null as no-op.
|
||||
*/
|
||||
export function getDb(): BetterSqlite3Database | null {
|
||||
if (_dbFailed) return null;
|
||||
if (_db) return _db;
|
||||
try {
|
||||
_db = openDb();
|
||||
return _db;
|
||||
} catch (err) {
|
||||
_dbFailed = true;
|
||||
// Log once so operators know events are being dropped; subsequent calls return null silently.
|
||||
console.warn("[ao] activity-events DB unavailable — events will be dropped:", err instanceof Error ? err.message : String(err));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -368,3 +368,20 @@ export type {
|
|||
} from "./migration/storage-v2.js";
|
||||
|
||||
export { atomicWriteFileSync } from "./atomic-write.js";
|
||||
|
||||
// Activity event logging — structured diagnostic event trail
|
||||
export { recordActivityEvent, droppedEventCount } from "./activity-events.js";
|
||||
export { isActivityEventsFtsEnabled } from "./events-db.js";
|
||||
export type {
|
||||
ActivityEventInput,
|
||||
ActivityEventKind,
|
||||
ActivityEventSource,
|
||||
ActivityEventLevel,
|
||||
ActivityEvent,
|
||||
} from "./activity-events.js";
|
||||
export {
|
||||
queryActivityEvents,
|
||||
searchActivityEvents,
|
||||
getActivityEventStats,
|
||||
} from "./query-activity-events.js";
|
||||
export type { ActivityEventFilter, ActivityEventStats } from "./query-activity-events.js";
|
||||
|
|
|
|||
|
|
@ -13,10 +13,12 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { recordActivityEvent } from "./activity-events.js";
|
||||
import {
|
||||
ACTIVITY_STATE,
|
||||
SESSION_STATUS,
|
||||
TERMINAL_STATUSES,
|
||||
type ActivityState,
|
||||
type LifecycleManager,
|
||||
type OpenCodeSessionManager,
|
||||
type SessionId,
|
||||
|
|
@ -474,6 +476,7 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan
|
|||
const observer = createProjectObserver(config, "lifecycle-manager");
|
||||
|
||||
const states = new Map<SessionId, SessionStatus>();
|
||||
const activityStateCache = new Map<string, ActivityState>(); // sessionId → last observed activity
|
||||
const reactionTrackers = new Map<string, ReactionTracker>(); // "sessionId:reactionKey"
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let polling = false; // re-entrancy guard
|
||||
|
|
@ -945,6 +948,18 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan
|
|||
activitySignal = classifyActivitySignal(detectedActivity, "native");
|
||||
activityEvidence = formatActivitySignalEvidence(activitySignal);
|
||||
lifecycle.runtime.lastObservedAt = nowIso;
|
||||
const prevActivity = activityStateCache.get(session.id);
|
||||
activityStateCache.set(session.id, detectedActivity.state);
|
||||
if (prevActivity !== undefined && prevActivity !== detectedActivity.state) {
|
||||
recordActivityEvent({
|
||||
projectId: session.projectId,
|
||||
sessionId: session.id,
|
||||
source: "lifecycle",
|
||||
kind: "activity.transition",
|
||||
summary: `${prevActivity} → ${detectedActivity.state}`,
|
||||
data: { from: prevActivity, to: detectedActivity.state },
|
||||
});
|
||||
}
|
||||
if (lifecycle.runtime.state !== "missing" && lifecycle.runtime.state !== "probe_failed") {
|
||||
lifecycle.runtime.state = "alive";
|
||||
lifecycle.runtime.reason = "process_running";
|
||||
|
|
@ -2098,6 +2113,15 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan
|
|||
// State transition detected
|
||||
states.set(session.id, newStatus);
|
||||
updateSessionMetadata(session, { status: newStatus });
|
||||
recordActivityEvent({
|
||||
projectId: session.projectId,
|
||||
sessionId: session.id,
|
||||
source: "lifecycle",
|
||||
kind: "lifecycle.transition",
|
||||
level: newStatus === "ci_failed" ? "warn" : "info",
|
||||
summary: `${oldStatus} → ${newStatus}`,
|
||||
data: { from: oldStatus, to: newStatus },
|
||||
});
|
||||
observer.recordOperation({
|
||||
metric: "lifecycle_poll",
|
||||
operation: "lifecycle.transition",
|
||||
|
|
@ -2413,6 +2437,11 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan
|
|||
states.delete(trackedId);
|
||||
}
|
||||
}
|
||||
for (const trackedId of activityStateCache.keys()) {
|
||||
if (!currentSessionIds.has(trackedId)) {
|
||||
activityStateCache.delete(trackedId);
|
||||
}
|
||||
}
|
||||
for (const trackerKey of reactionTrackers.keys()) {
|
||||
const sessionId = trackerKey.split(":")[0];
|
||||
if (sessionId && !currentSessionIds.has(sessionId)) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
/**
|
||||
* Activity event logging — read API.
|
||||
*
|
||||
* queryActivityEvents: structured filter-based retrieval.
|
||||
* searchActivityEvents: FTS5 natural-language search.
|
||||
* getActivityEventStats: aggregate counts for `ao events stats`.
|
||||
*/
|
||||
|
||||
import { getDb, isActivityEventsFtsEnabled } from "./events-db.js";
|
||||
import {
|
||||
droppedEventCount,
|
||||
type ActivityEvent,
|
||||
type ActivityEventKind,
|
||||
type ActivityEventSource,
|
||||
type ActivityEventLevel,
|
||||
} from "./activity-events.js";
|
||||
|
||||
export interface ActivityEventFilter {
|
||||
projectId?: string;
|
||||
sessionId?: string;
|
||||
kind?: ActivityEventKind | string;
|
||||
source?: ActivityEventSource;
|
||||
level?: ActivityEventLevel;
|
||||
since?: Date;
|
||||
until?: Date;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface ActivityEventStats {
|
||||
total: number;
|
||||
byKind: Record<string, number>;
|
||||
bySource: Record<string, number>;
|
||||
droppedThisProcess: number;
|
||||
oldestTs?: string;
|
||||
newestTs?: string;
|
||||
}
|
||||
|
||||
function rowToEvent(row: Record<string, unknown>): ActivityEvent {
|
||||
return {
|
||||
id: row["id"] as number,
|
||||
tsEpoch: row["ts_epoch"] as number,
|
||||
ts: row["ts"] as string,
|
||||
projectId: (row["project_id"] as string | null) ?? null,
|
||||
sessionId: (row["session_id"] as string | null) ?? null,
|
||||
source: row["source"] as string,
|
||||
kind: row["type"] as string,
|
||||
level: row["log_level"] as string,
|
||||
summary: row["summary"] as string,
|
||||
data: (row["data"] as string | null) ?? null,
|
||||
rank: typeof row["rank"] === "number" ? (row["rank"] as number) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function escapeLike(raw: string): string {
|
||||
return raw.replace(/[\\%_]/g, "\\$&");
|
||||
}
|
||||
|
||||
/**
|
||||
* Query events with structured filters. Returns [] if DB is unavailable.
|
||||
*/
|
||||
export function queryActivityEvents(filter: ActivityEventFilter = {}): ActivityEvent[] {
|
||||
const db = getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (filter.projectId) {
|
||||
conditions.push("project_id = ?");
|
||||
params.push(filter.projectId);
|
||||
}
|
||||
if (filter.sessionId) {
|
||||
conditions.push("session_id = ?");
|
||||
params.push(filter.sessionId);
|
||||
}
|
||||
if (filter.kind) {
|
||||
conditions.push("type = ?");
|
||||
params.push(filter.kind);
|
||||
}
|
||||
if (filter.source) {
|
||||
conditions.push("source = ?");
|
||||
params.push(filter.source);
|
||||
}
|
||||
if (filter.level) {
|
||||
conditions.push("log_level = ?");
|
||||
params.push(filter.level);
|
||||
}
|
||||
if (filter.since) {
|
||||
conditions.push("ts_epoch >= ?");
|
||||
params.push(filter.since.getTime());
|
||||
}
|
||||
if (filter.until) {
|
||||
conditions.push("ts_epoch <= ?");
|
||||
params.push(filter.until.getTime());
|
||||
}
|
||||
|
||||
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
const rawLimit = filter.limit ?? 100;
|
||||
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 1000)) : 100;
|
||||
|
||||
try {
|
||||
const rows = db
|
||||
.prepare(`SELECT * FROM activity_events ${where} ORDER BY ts_epoch DESC LIMIT ?`)
|
||||
.all(...params, limit) as Record<string, unknown>[];
|
||||
return rows.map(rowToEvent);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FTS5 natural-language search. Sanitizes the query to prevent injection.
|
||||
* Returns [] if DB is unavailable or search fails.
|
||||
* projectId filter is pushed into SQL so it applies before the LIMIT.
|
||||
*/
|
||||
export function searchActivityEvents(rawQuery: string, projectId?: string, limit = 100): ActivityEvent[] {
|
||||
const db = getDb();
|
||||
if (!db) return [];
|
||||
|
||||
// Only allow word characters; join with AND to prevent FTS syntax injection
|
||||
const tokens = rawQuery.match(/\w+/g);
|
||||
if (!tokens || tokens.length === 0) return [];
|
||||
const ftsQuery = tokens.map((t) => `"${t}"`).join(" AND ");
|
||||
|
||||
const projectFilter = projectId ? "AND ae.project_id = ?" : "";
|
||||
const clampedLimit = Number.isFinite(limit) ? Math.max(1, Math.min(limit, 1000)) : 100;
|
||||
const params: unknown[] = [];
|
||||
if (projectId) params.push(projectId);
|
||||
params.push(clampedLimit);
|
||||
|
||||
try {
|
||||
if (!isActivityEventsFtsEnabled()) {
|
||||
const likePattern = `%${escapeLike(tokens.join(" "))}%`;
|
||||
const fallbackParams: unknown[] = [likePattern, likePattern];
|
||||
if (projectId) fallbackParams.push(projectId);
|
||||
fallbackParams.push(clampedLimit);
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT ae.*, NULL AS rank FROM activity_events ae
|
||||
WHERE (ae.summary LIKE ? ESCAPE '\\' OR ae.data LIKE ? ESCAPE '\\') ${projectFilter}
|
||||
ORDER BY ae.ts_epoch DESC
|
||||
LIMIT ?`,
|
||||
)
|
||||
.all(...fallbackParams) as Record<string, unknown>[];
|
||||
return rows.map(rowToEvent);
|
||||
}
|
||||
|
||||
params.unshift(ftsQuery);
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT ae.*, activity_events_fts.rank AS rank FROM activity_events ae
|
||||
JOIN activity_events_fts ON activity_events_fts.rowid = ae.id
|
||||
WHERE activity_events_fts MATCH ? ${projectFilter}
|
||||
ORDER BY activity_events_fts.rank
|
||||
LIMIT ?`,
|
||||
)
|
||||
.all(...params) as Record<string, unknown>[];
|
||||
return rows.map(rowToEvent);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate stats for `ao events stats`. Returns null if DB is unavailable.
|
||||
*/
|
||||
export function getActivityEventStats(): ActivityEventStats | null {
|
||||
const db = getDb();
|
||||
if (!db) return null;
|
||||
|
||||
try {
|
||||
const totalRow = db.prepare("SELECT COUNT(*) as cnt FROM activity_events").all() as Record<
|
||||
string,
|
||||
unknown
|
||||
>[];
|
||||
const total = (totalRow[0]?.["cnt"] as number) ?? 0;
|
||||
|
||||
const byKindRows = db
|
||||
.prepare("SELECT type, COUNT(*) as cnt FROM activity_events GROUP BY type")
|
||||
.all() as Record<string, unknown>[];
|
||||
const byKind: Record<string, number> = {};
|
||||
for (const row of byKindRows) {
|
||||
byKind[row["type"] as string] = row["cnt"] as number;
|
||||
}
|
||||
|
||||
const bySourceRows = db
|
||||
.prepare("SELECT source, COUNT(*) as cnt FROM activity_events GROUP BY source")
|
||||
.all() as Record<string, unknown>[];
|
||||
const bySource: Record<string, number> = {};
|
||||
for (const row of bySourceRows) {
|
||||
bySource[row["source"] as string] = row["cnt"] as number;
|
||||
}
|
||||
|
||||
const rangeRow = db
|
||||
.prepare("SELECT MIN(ts) as oldest, MAX(ts) as newest FROM activity_events")
|
||||
.all() as Record<string, unknown>[];
|
||||
|
||||
return {
|
||||
total,
|
||||
byKind,
|
||||
bySource,
|
||||
droppedThisProcess: droppedEventCount(),
|
||||
oldestTs: (rangeRow[0]?.["oldest"] as string | null) ?? undefined,
|
||||
newestTs: (rangeRow[0]?.["newest"] as string | null) ?? undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export type { ActivityEvent, ActivityEventKind, ActivityEventSource, ActivityEventLevel };
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
*/
|
||||
|
||||
import { statSync, existsSync, writeFileSync, mkdirSync, utimesSync, unlinkSync } from "node:fs";
|
||||
import { recordActivityEvent } from "./activity-events.js";
|
||||
import { execFile } from "node:child_process";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
|
|
@ -1070,6 +1071,30 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM
|
|||
|
||||
// Define methods as local functions so `this` is not needed
|
||||
async function spawn(spawnConfig: SessionSpawnConfig): Promise<Session> {
|
||||
recordActivityEvent({
|
||||
projectId: spawnConfig.projectId,
|
||||
source: "session-manager",
|
||||
kind: "session.spawn_started",
|
||||
summary: "spawn started",
|
||||
data: { agent: spawnConfig.agent ?? undefined },
|
||||
});
|
||||
|
||||
try {
|
||||
return await _spawnInner(spawnConfig);
|
||||
} catch (err) {
|
||||
recordActivityEvent({
|
||||
projectId: spawnConfig.projectId,
|
||||
source: "session-manager",
|
||||
kind: "session.spawn_failed",
|
||||
level: "error",
|
||||
summary: `spawn failed`,
|
||||
data: { reason: err instanceof Error ? err.message : String(err) },
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function _spawnInner(spawnConfig: SessionSpawnConfig): Promise<Session> {
|
||||
const project = config.projects[spawnConfig.projectId];
|
||||
if (!project) {
|
||||
throw new Error(`Unknown project: ${spawnConfig.projectId}`);
|
||||
|
|
@ -1469,6 +1494,15 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM
|
|||
invalidateCache();
|
||||
}
|
||||
|
||||
recordActivityEvent({
|
||||
projectId: spawnConfig.projectId,
|
||||
sessionId,
|
||||
source: "session-manager",
|
||||
kind: "session.spawned",
|
||||
summary: `spawned: ${sessionId}`,
|
||||
data: { agent: plugins.agent.name, branch: session.branch ?? undefined },
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
|
|
@ -2130,6 +2164,14 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM
|
|||
});
|
||||
|
||||
invalidateCache();
|
||||
recordActivityEvent({
|
||||
projectId,
|
||||
sessionId,
|
||||
source: "session-manager",
|
||||
kind: "session.killed",
|
||||
summary: `killed: ${sessionId}`,
|
||||
data: { reason: killReason },
|
||||
});
|
||||
return { cleaned: true, alreadyTerminated: false };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1150,6 +1150,7 @@ export type EventPriority = "urgent" | "action" | "warning" | "info";
|
|||
/** All orchestrator event types */
|
||||
export type EventType =
|
||||
// Session lifecycle
|
||||
| "session.spawn_started"
|
||||
| "session.spawned"
|
||||
| "session.working"
|
||||
| "session.exited"
|
||||
|
|
|
|||
300
pnpm-lock.yaml
300
pnpm-lock.yaml
|
|
@ -161,10 +161,17 @@ importers:
|
|||
zod:
|
||||
specifier: ^3.24.0
|
||||
version: 3.25.76
|
||||
optionalDependencies:
|
||||
better-sqlite3:
|
||||
specifier: ^11.0.0
|
||||
version: 11.10.0
|
||||
devDependencies:
|
||||
'@rollup/plugin-typescript':
|
||||
specifier: ^12.3.0
|
||||
version: 12.3.0(rollup@4.60.1)(tslib@2.8.1)(typescript@5.9.3)
|
||||
'@types/better-sqlite3':
|
||||
specifier: ^7.6.0
|
||||
version: 7.6.13
|
||||
'@types/node':
|
||||
specifier: ^25.2.3
|
||||
version: 25.6.0
|
||||
|
|
@ -2030,6 +2037,9 @@ packages:
|
|||
'@types/babel__traverse@7.28.0':
|
||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||
|
||||
'@types/better-sqlite3@7.6.13':
|
||||
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||
|
||||
|
|
@ -2336,6 +2346,15 @@ packages:
|
|||
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
better-sqlite3@11.10.0:
|
||||
resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==}
|
||||
|
||||
bindings@1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
|
||||
bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
||||
brace-expansion@2.0.3:
|
||||
resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==}
|
||||
|
||||
|
|
@ -2352,6 +2371,9 @@ packages:
|
|||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
buffer@5.7.1:
|
||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||
|
||||
cac@6.7.14:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -2397,6 +2419,9 @@ packages:
|
|||
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
|
||||
chownr@3.0.0:
|
||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -2505,10 +2530,18 @@ packages:
|
|||
decimal.js@10.6.0:
|
||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||
|
||||
decompress-response@6.0.0:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
deep-eql@5.0.2:
|
||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
deep-extend@0.6.0:
|
||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
|
|
@ -2564,6 +2597,9 @@ packages:
|
|||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||
|
||||
enhanced-resolve@5.20.1:
|
||||
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
|
@ -2689,6 +2725,10 @@ packages:
|
|||
resolution: {integrity: sha512-zwxwiQqexizSXFZV13zMiEtW1E3lv7RlUv+1f5FBiR4x7wFhEjm3aFTyYkZQWzyN08WnPdox015GoRH5D/E5YA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
expand-template@2.0.3:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
expect-type@1.3.0:
|
||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
|
@ -2736,6 +2776,9 @@ packages:
|
|||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -2772,6 +2815,9 @@ packages:
|
|||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
fs-extra@7.0.1:
|
||||
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
|
||||
engines: {node: '>=6 <7 || >=8'}
|
||||
|
|
@ -2820,6 +2866,9 @@ packages:
|
|||
get-tsconfig@4.13.7:
|
||||
resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==}
|
||||
|
||||
github-from-package@0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
|
|
@ -2907,6 +2956,9 @@ packages:
|
|||
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
|
@ -2923,6 +2975,12 @@ packages:
|
|||
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ini@1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
|
||||
inquirer@10.2.2:
|
||||
resolution: {integrity: sha512-tyao/4Vo36XnUItZ7DnUXX4f1jVao2mSrleV/5IPtW/XAEA26hRVsbc68nuTEKWcr5vMP/1mVoT2O7u8H4v1Vg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3251,6 +3309,10 @@ packages:
|
|||
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
min-indent@1.0.1:
|
||||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -3263,6 +3325,9 @@ packages:
|
|||
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
minipass-collect@2.0.1:
|
||||
resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
|
@ -3295,6 +3360,9 @@ packages:
|
|||
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
mkdirp-classic@0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
|
||||
mri@1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -3319,6 +3387,9 @@ packages:
|
|||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
napi-build-utils@2.0.0:
|
||||
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
||||
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
|
|
@ -3353,6 +3424,10 @@ packages:
|
|||
sass:
|
||||
optional: true
|
||||
|
||||
node-abi@3.89.0:
|
||||
resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
|
|
@ -3378,6 +3453,9 @@ packages:
|
|||
obug@2.1.1:
|
||||
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
onetime@7.0.0:
|
||||
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3549,6 +3627,12 @@ packages:
|
|||
resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
|
||||
hasBin: true
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
|
@ -3575,6 +3659,9 @@ packages:
|
|||
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
pump@3.0.4:
|
||||
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -3588,6 +3675,10 @@ packages:
|
|||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
rc@1.2.8:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
|
||||
react-dom@19.2.5:
|
||||
resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
|
||||
peerDependencies:
|
||||
|
|
@ -3608,6 +3699,10 @@ packages:
|
|||
resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
redent@3.0.0:
|
||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -3666,6 +3761,9 @@ packages:
|
|||
rxjs@7.8.2:
|
||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
|
|
@ -3711,6 +3809,12 @@ packages:
|
|||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
simple-concat@1.0.1:
|
||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||
|
||||
simple-get@4.0.1:
|
||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||
|
||||
sirv@2.0.4:
|
||||
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
|
@ -3773,6 +3877,9 @@ packages:
|
|||
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -3789,6 +3896,10 @@ packages:
|
|||
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-json-comments@2.0.1:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
strip-literal@3.1.0:
|
||||
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
|
||||
|
||||
|
|
@ -3827,6 +3938,13 @@ packages:
|
|||
resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tar-fs@2.1.4:
|
||||
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
|
||||
|
||||
tar-stream@2.2.0:
|
||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tar@7.5.13:
|
||||
resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3914,6 +4032,9 @@ packages:
|
|||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
|
||||
tweetnacl@1.0.3:
|
||||
resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==}
|
||||
|
||||
|
|
@ -3956,6 +4077,9 @@ packages:
|
|||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
hasBin: true
|
||||
|
|
@ -4179,6 +4303,9 @@ packages:
|
|||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
ws@7.5.10:
|
||||
resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
|
||||
engines: {node: '>=8.3.0'}
|
||||
|
|
@ -5415,6 +5542,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@types/better-sqlite3@7.6.13':
|
||||
dependencies:
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
|
|
@ -5780,6 +5911,24 @@ snapshots:
|
|||
dependencies:
|
||||
is-windows: 1.0.2
|
||||
|
||||
better-sqlite3@11.10.0:
|
||||
dependencies:
|
||||
bindings: 1.5.0
|
||||
prebuild-install: 7.1.3
|
||||
optional: true
|
||||
|
||||
bindings@1.5.0:
|
||||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
optional: true
|
||||
|
||||
bl@4.1.0:
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
optional: true
|
||||
|
||||
brace-expansion@2.0.3:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
|
@ -5800,6 +5949,12 @@ snapshots:
|
|||
node-releases: 2.0.37
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
||||
|
||||
buffer@5.7.1:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
optional: true
|
||||
|
||||
cac@6.7.14: {}
|
||||
|
||||
cacache@20.0.4:
|
||||
|
|
@ -5847,6 +6002,9 @@ snapshots:
|
|||
|
||||
check-error@2.1.3: {}
|
||||
|
||||
chownr@1.1.4:
|
||||
optional: true
|
||||
|
||||
chownr@3.0.0: {}
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
|
|
@ -5951,8 +6109,16 @@ snapshots:
|
|||
|
||||
decimal.js@10.6.0: {}
|
||||
|
||||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
optional: true
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
deep-extend@0.6.0:
|
||||
optional: true
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
define-lazy-prop@2.0.0: {}
|
||||
|
|
@ -5991,6 +6157,11 @@ snapshots:
|
|||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
optional: true
|
||||
|
||||
enhanced-resolve@5.20.1:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
|
@ -6170,6 +6341,9 @@ snapshots:
|
|||
|
||||
eventsource-parser@3.0.7: {}
|
||||
|
||||
expand-template@2.0.3:
|
||||
optional: true
|
||||
|
||||
expect-type@1.3.0: {}
|
||||
|
||||
exponential-backoff@3.1.3: {}
|
||||
|
|
@ -6216,6 +6390,9 @@ snapshots:
|
|||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
optional: true
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
|
@ -6252,6 +6429,9 @@ snapshots:
|
|||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
fs-constants@1.0.0:
|
||||
optional: true
|
||||
|
||||
fs-extra@7.0.1:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
|
@ -6304,6 +6484,9 @@ snapshots:
|
|||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
github-from-package@0.0.0:
|
||||
optional: true
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
|
|
@ -6394,6 +6577,9 @@ snapshots:
|
|||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
ieee754@1.2.1:
|
||||
optional: true
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
ignore@7.0.5: {}
|
||||
|
|
@ -6402,6 +6588,12 @@ snapshots:
|
|||
|
||||
indent-string@4.0.0: {}
|
||||
|
||||
inherits@2.0.4:
|
||||
optional: true
|
||||
|
||||
ini@1.3.8:
|
||||
optional: true
|
||||
|
||||
inquirer@10.2.2:
|
||||
dependencies:
|
||||
'@inquirer/core': 9.2.1
|
||||
|
|
@ -6717,6 +6909,9 @@ snapshots:
|
|||
|
||||
mimic-function@5.0.1: {}
|
||||
|
||||
mimic-response@3.1.0:
|
||||
optional: true
|
||||
|
||||
min-indent@1.0.1: {}
|
||||
|
||||
minimatch@10.2.5:
|
||||
|
|
@ -6727,6 +6922,9 @@ snapshots:
|
|||
dependencies:
|
||||
brace-expansion: 2.0.3
|
||||
|
||||
minimist@1.2.8:
|
||||
optional: true
|
||||
|
||||
minipass-collect@2.0.1:
|
||||
dependencies:
|
||||
minipass: 7.1.3
|
||||
|
|
@ -6761,6 +6959,9 @@ snapshots:
|
|||
dependencies:
|
||||
minipass: 7.1.3
|
||||
|
||||
mkdirp-classic@0.5.3:
|
||||
optional: true
|
||||
|
||||
mri@1.2.0: {}
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
|
|
@ -6773,6 +6974,9 @@ snapshots:
|
|||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
napi-build-utils@2.0.0:
|
||||
optional: true
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
|
@ -6806,6 +7010,11 @@ snapshots:
|
|||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
node-abi@3.89.0:
|
||||
dependencies:
|
||||
semver: 7.7.4
|
||||
optional: true
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
optional: true
|
||||
|
||||
|
|
@ -6839,6 +7048,11 @@ snapshots:
|
|||
|
||||
obug@2.1.1: {}
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
optional: true
|
||||
|
||||
onetime@7.0.0:
|
||||
dependencies:
|
||||
mimic-function: 5.0.1
|
||||
|
|
@ -6993,6 +7207,22 @@ snapshots:
|
|||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
expand-template: 2.0.3
|
||||
github-from-package: 0.0.0
|
||||
minimist: 1.2.8
|
||||
mkdirp-classic: 0.5.3
|
||||
napi-build-utils: 2.0.0
|
||||
node-abi: 3.89.0
|
||||
pump: 3.0.4
|
||||
rc: 1.2.8
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.4
|
||||
tunnel-agent: 0.6.0
|
||||
optional: true
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@2.8.8: {}
|
||||
|
|
@ -7009,6 +7239,12 @@ snapshots:
|
|||
|
||||
proxy-from-env@2.1.0: {}
|
||||
|
||||
pump@3.0.4:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.5
|
||||
once: 1.4.0
|
||||
optional: true
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
pusher-js@8.4.0-rc2:
|
||||
|
|
@ -7019,6 +7255,14 @@ snapshots:
|
|||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
rc@1.2.8:
|
||||
dependencies:
|
||||
deep-extend: 0.6.0
|
||||
ini: 1.3.8
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
optional: true
|
||||
|
||||
react-dom@19.2.5(react@19.2.5):
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
|
@ -7037,6 +7281,13 @@ snapshots:
|
|||
pify: 4.0.1
|
||||
strip-bom: 3.0.0
|
||||
|
||||
readable-stream@3.6.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
optional: true
|
||||
|
||||
redent@3.0.0:
|
||||
dependencies:
|
||||
indent-string: 4.0.0
|
||||
|
|
@ -7116,6 +7367,9 @@ snapshots:
|
|||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
optional: true
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
saxes@6.0.0:
|
||||
|
|
@ -7174,6 +7428,16 @@ snapshots:
|
|||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
simple-concat@1.0.1:
|
||||
optional: true
|
||||
|
||||
simple-get@4.0.1:
|
||||
dependencies:
|
||||
decompress-response: 6.0.0
|
||||
once: 1.4.0
|
||||
simple-concat: 1.0.1
|
||||
optional: true
|
||||
|
||||
sirv@2.0.4:
|
||||
dependencies:
|
||||
'@polka/url': 1.0.0-next.29
|
||||
|
|
@ -7238,6 +7502,11 @@ snapshots:
|
|||
get-east-asian-width: 1.5.0
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
string_decoder@1.3.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
optional: true
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
|
@ -7252,6 +7521,9 @@ snapshots:
|
|||
dependencies:
|
||||
min-indent: 1.0.1
|
||||
|
||||
strip-json-comments@2.0.1:
|
||||
optional: true
|
||||
|
||||
strip-literal@3.1.0:
|
||||
dependencies:
|
||||
js-tokens: 9.0.1
|
||||
|
|
@ -7279,6 +7551,23 @@ snapshots:
|
|||
|
||||
tapable@2.3.2: {}
|
||||
|
||||
tar-fs@2.1.4:
|
||||
dependencies:
|
||||
chownr: 1.1.4
|
||||
mkdirp-classic: 0.5.3
|
||||
pump: 3.0.4
|
||||
tar-stream: 2.2.0
|
||||
optional: true
|
||||
|
||||
tar-stream@2.2.0:
|
||||
dependencies:
|
||||
bl: 4.1.0
|
||||
end-of-stream: 1.4.5
|
||||
fs-constants: 1.0.0
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
optional: true
|
||||
|
||||
tar@7.5.13:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
|
|
@ -7351,6 +7640,11 @@ snapshots:
|
|||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
optional: true
|
||||
|
||||
tweetnacl@1.0.3: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
|
|
@ -7388,6 +7682,9 @@ snapshots:
|
|||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
optional: true
|
||||
|
||||
uuid@10.0.0: {}
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
|
@ -7588,6 +7885,9 @@ snapshots:
|
|||
string-width: 5.1.2
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
wrappy@1.0.2:
|
||||
optional: true
|
||||
|
||||
ws@7.5.10: {}
|
||||
|
||||
ws@8.20.0: {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue