feat(KB-086): complete Step 2 — add activity log storage methods
This commit is contained in:
@@ -3,7 +3,7 @@ import { execSync } from "node:child_process";
|
|||||||
import { appendFile, mkdir, readFile, writeFile, readdir, rename, unlink } from "node:fs/promises";
|
import { appendFile, mkdir, readFile, writeFile, readdir, rename, unlink } from "node:fs/promises";
|
||||||
import { join, sep } from "node:path";
|
import { join, sep } from "node:path";
|
||||||
import { existsSync, watch, type FSWatcher, readFileSync } from "node:fs";
|
import { existsSync, watch, type FSWatcher, readFileSync } from "node:fs";
|
||||||
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, MergeResult, Settings } from "./types.js";
|
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, MergeResult, Settings, ActivityLogEntry, ActivityEventType } from "./types.js";
|
||||||
import { VALID_TRANSITIONS, DEFAULT_SETTINGS } from "./types.js";
|
import { VALID_TRANSITIONS, DEFAULT_SETTINGS } from "./types.js";
|
||||||
|
|
||||||
export interface TaskStoreEvents {
|
export interface TaskStoreEvents {
|
||||||
@@ -21,6 +21,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
private tasksDir: string;
|
private tasksDir: string;
|
||||||
private configPath: string;
|
private configPath: string;
|
||||||
private archiveLogPath: string;
|
private archiveLogPath: string;
|
||||||
|
private activityLogPath: string;
|
||||||
|
|
||||||
/** File-system watcher instance */
|
/** File-system watcher instance */
|
||||||
private watcher: FSWatcher | null = null;
|
private watcher: FSWatcher | null = null;
|
||||||
@@ -44,6 +45,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
this.tasksDir = join(this.kbDir, "tasks");
|
this.tasksDir = join(this.kbDir, "tasks");
|
||||||
this.configPath = join(this.kbDir, "config.json");
|
this.configPath = join(this.kbDir, "config.json");
|
||||||
this.archiveLogPath = join(this.kbDir, "archive.jsonl");
|
this.archiveLogPath = join(this.kbDir, "archive.jsonl");
|
||||||
|
this.activityLogPath = join(this.kbDir, "activity-log.jsonl");
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
@@ -1820,6 +1822,89 @@ ${notificationsSection}`;
|
|||||||
return DEFAULT_SETTINGS;
|
return DEFAULT_SETTINGS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Activity Log Methods ─────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record an activity log entry to the global activity log.
|
||||||
|
* Appends to .kb/activity-log.jsonl (JSON Lines format).
|
||||||
|
* Auto-generates ID and timestamp.
|
||||||
|
*/
|
||||||
|
async recordActivity(entry: Omit<ActivityLogEntry, "id" | "timestamp">): Promise<ActivityLogEntry> {
|
||||||
|
const fullEntry: ActivityLogEntry = {
|
||||||
|
...entry,
|
||||||
|
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await appendFile(this.activityLogPath, JSON.stringify(fullEntry) + "\n");
|
||||||
|
} catch (err) {
|
||||||
|
// Best-effort: log errors but don't break operations
|
||||||
|
console.error("Failed to record activity:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get activity log entries from the JSONL file.
|
||||||
|
* Returns entries sorted newest first.
|
||||||
|
* Supports filtering by limit, since timestamp, and event type.
|
||||||
|
*/
|
||||||
|
async getActivityLog(options?: { limit?: number; since?: string; type?: ActivityEventType }): Promise<ActivityLogEntry[]> {
|
||||||
|
if (!existsSync(this.activityLogPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await readFile(this.activityLogPath, "utf-8");
|
||||||
|
const entries: ActivityLogEntry[] = [];
|
||||||
|
|
||||||
|
for (const line of content.split("\n")) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
try {
|
||||||
|
entries.push(JSON.parse(line) as ActivityLogEntry);
|
||||||
|
} catch {
|
||||||
|
// Skip malformed lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by since timestamp if provided
|
||||||
|
let filtered = entries;
|
||||||
|
if (options?.since) {
|
||||||
|
filtered = filtered.filter((e) => e.timestamp > options.since!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by type if provided
|
||||||
|
if (options?.type) {
|
||||||
|
filtered = filtered.filter((e) => e.type === options.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort newest first (by timestamp descending)
|
||||||
|
filtered.sort((a, b) => b.timestamp.localeCompare(b.timestamp));
|
||||||
|
|
||||||
|
// Apply limit if provided
|
||||||
|
if (options?.limit && options.limit > 0) {
|
||||||
|
filtered = filtered.slice(0, options.limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all activity log entries by truncating the log file.
|
||||||
|
* Use with caution - this permanently deletes activity history.
|
||||||
|
*/
|
||||||
|
async clearActivityLog(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (existsSync(this.activityLogPath)) {
|
||||||
|
await writeFile(this.activityLogPath, "", "utf-8");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to clear activity log:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user