Add /ultraresearch-local for structured research combining local codebase analysis with external knowledge via parallel agent swarms. Produces research briefs with triangulation, confidence ratings, and source quality assessment. New command: /ultraresearch-local with modes --quick, --local, --external, --fg. New agents: research-orchestrator (opus), docs-researcher, community-researcher, security-researcher, contrarian-researcher, gemini-bridge (all sonnet). New template: research-brief-template.md. Integration: --research flag in /ultraplan-local accepts pre-built research briefs (up to 3), enriches the interview and exploration phases. Planning orchestrator cross-references brief findings during synthesis. Design principle: Context Engineering — right information to right agent at right time. Research briefs are structured artifacts in the pipeline: ultraresearch → brief → ultraplan --research → plan → ultraexecute. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
290 lines
7.9 KiB
TypeScript
290 lines
7.9 KiB
TypeScript
import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from "node:fs";
|
|
import { join, resolve, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import type { AnalyticsBatch, WeeklyReport, MonthlyReport, PostAnalytics } from "../models/types.js";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
/**
|
|
* Get the analytics root directory from environment or default location
|
|
* Default is assets/analytics relative to the plugin root
|
|
*/
|
|
export function getAnalyticsRoot(): string {
|
|
if (process.env.ANALYTICS_ROOT) {
|
|
return resolve(process.env.ANALYTICS_ROOT);
|
|
}
|
|
|
|
// Build output is at: scripts/analytics/build/utils/storage.js
|
|
// Plugin root is 4 levels up: ../../../../
|
|
// Then assets/analytics from there
|
|
const pluginRoot = resolve(__dirname, "../../../../");
|
|
return join(pluginRoot, "assets", "analytics");
|
|
}
|
|
|
|
/**
|
|
* Ensure required subdirectories exist under analytics root
|
|
*/
|
|
export function ensureDirectories(root: string): void {
|
|
const directories = ["exports", "posts", "weekly-reports", "monthly-reports"];
|
|
|
|
if (!existsSync(root)) {
|
|
mkdirSync(root, { recursive: true });
|
|
}
|
|
|
|
for (const dir of directories) {
|
|
const path = join(root, dir);
|
|
if (!existsSync(path)) {
|
|
mkdirSync(path, { recursive: true });
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all CSV export files in the exports directory
|
|
*/
|
|
export function listExports(root: string): string[] {
|
|
const exportsDir = join(root, "exports");
|
|
|
|
if (!existsSync(exportsDir)) {
|
|
return [];
|
|
}
|
|
|
|
return readdirSync(exportsDir)
|
|
.filter(file => file.endsWith(".csv"))
|
|
.sort();
|
|
}
|
|
|
|
/**
|
|
* Sanitize date string to only allow YYYY-MM-DD format
|
|
*/
|
|
function sanitizeDate(date: string): string {
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
throw new Error(`Invalid date format: ${date}. Expected YYYY-MM-DD`);
|
|
}
|
|
return date;
|
|
}
|
|
|
|
/**
|
|
* Sanitize ID string to only allow alphanumeric and hyphens
|
|
*/
|
|
function sanitizeId(id: string): string {
|
|
if (!/^[a-zA-Z0-9-]+$/.test(id)) {
|
|
throw new Error(`Invalid ID format: ${id}. Only alphanumeric and hyphens allowed`);
|
|
}
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Verify that the resolved path is within the expected directory
|
|
*/
|
|
function verifyPathWithinDirectory(filepath: string, expectedDir: string): void {
|
|
const resolvedPath = resolve(filepath);
|
|
const resolvedDir = resolve(expectedDir);
|
|
|
|
if (!resolvedPath.startsWith(resolvedDir + "/") && resolvedPath !== resolvedDir) {
|
|
throw new Error(`Path traversal detected: ${filepath} is not within ${expectedDir}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save an analytics batch to disk
|
|
* Returns the filename that was created
|
|
*/
|
|
export function saveBatch(root: string, batch: AnalyticsBatch): string {
|
|
ensureDirectories(root);
|
|
|
|
const postsDir = join(root, "posts");
|
|
|
|
// Sanitize inputs to prevent path traversal
|
|
const date = sanitizeDate(batch.dateRange.from);
|
|
const shortId = sanitizeId(batch.batchId.substring(0, 8));
|
|
const filename = `${date}-${shortId}.json`;
|
|
const filepath = join(postsDir, filename);
|
|
|
|
// Verify the resolved filepath is within postsDir
|
|
verifyPathWithinDirectory(filepath, postsDir);
|
|
|
|
writeFileSync(filepath, JSON.stringify(batch, null, 2), "utf-8");
|
|
|
|
return filename;
|
|
}
|
|
|
|
/**
|
|
* Load all analytics batches from disk
|
|
* Returns batches sorted by importedAt timestamp
|
|
*/
|
|
export function loadAllBatches(root: string): AnalyticsBatch[] {
|
|
const postsDir = join(root, "posts");
|
|
|
|
if (!existsSync(postsDir)) {
|
|
return [];
|
|
}
|
|
|
|
const batches: AnalyticsBatch[] = [];
|
|
|
|
for (const file of readdirSync(postsDir)) {
|
|
if (!file.endsWith(".json")) {
|
|
continue;
|
|
}
|
|
|
|
const filepath = join(postsDir, file);
|
|
const content = readFileSync(filepath, "utf-8");
|
|
try {
|
|
const batch = JSON.parse(content) as AnalyticsBatch;
|
|
batches.push(batch);
|
|
} catch {
|
|
// Skip corrupt batch file
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return batches.sort((a, b) =>
|
|
a.importedAt.localeCompare(b.importedAt)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Load all posts from all batches, deduplicated by post ID
|
|
* Latest import wins. Sorted by publishedDate descending.
|
|
*/
|
|
export function loadAllPosts(root: string): PostAnalytics[] {
|
|
const batches = loadAllBatches(root);
|
|
|
|
// Use Map to deduplicate - key is post ID, value is { post, importedAt }
|
|
const postMap = new Map<string, { post: PostAnalytics; importedAt: string }>();
|
|
|
|
for (const batch of batches) {
|
|
for (const post of batch.posts) {
|
|
const existing = postMap.get(post.id);
|
|
|
|
// Keep post with latest importedAt timestamp
|
|
if (!existing || batch.importedAt > existing.importedAt) {
|
|
postMap.set(post.id, {
|
|
post,
|
|
importedAt: batch.importedAt
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract posts and sort by publishedDate descending
|
|
const posts = Array.from(postMap.values()).map(({ post }) => post);
|
|
|
|
return posts.sort((a, b) =>
|
|
b.publishedDate.localeCompare(a.publishedDate)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sanitize week string to only allow ISO week format (YYYY-WXX)
|
|
*/
|
|
function sanitizeWeek(week: string): string {
|
|
if (!/^\d{4}-W\d{2}$/.test(week)) {
|
|
throw new Error(`Invalid week format: ${week}. Expected YYYY-WXX`);
|
|
}
|
|
return week;
|
|
}
|
|
|
|
/**
|
|
* Save a weekly report to disk
|
|
* Returns the filename that was created
|
|
*/
|
|
export function saveWeeklyReport(root: string, report: WeeklyReport): string {
|
|
ensureDirectories(root);
|
|
|
|
const reportsDir = join(root, "weekly-reports");
|
|
|
|
// Sanitize week to prevent path traversal
|
|
const week = sanitizeWeek(report.week);
|
|
const filename = `${week}.json`;
|
|
const filepath = join(reportsDir, filename);
|
|
|
|
// Verify the resolved filepath is within reportsDir
|
|
verifyPathWithinDirectory(filepath, reportsDir);
|
|
|
|
writeFileSync(filepath, JSON.stringify(report, null, 2), "utf-8");
|
|
|
|
return filename;
|
|
}
|
|
|
|
/**
|
|
* Load a specific weekly report by week identifier
|
|
* Returns null if not found
|
|
*/
|
|
export function loadWeeklyReport(root: string, week: string): WeeklyReport | null {
|
|
week = sanitizeWeek(week);
|
|
const reportsDir = join(root, "weekly-reports");
|
|
const filepath = join(reportsDir, `${week}.json`);
|
|
|
|
if (!existsSync(filepath)) {
|
|
return null;
|
|
}
|
|
|
|
const content = readFileSync(filepath, "utf-8");
|
|
return JSON.parse(content) as WeeklyReport;
|
|
}
|
|
|
|
/**
|
|
* Load all weekly reports from disk
|
|
* Returns reports sorted by week descending (newest first)
|
|
*/
|
|
export function loadAllWeeklyReports(root: string): WeeklyReport[] {
|
|
const reportsDir = join(root, "weekly-reports");
|
|
|
|
if (!existsSync(reportsDir)) {
|
|
return [];
|
|
}
|
|
|
|
const reports: WeeklyReport[] = [];
|
|
|
|
for (const file of readdirSync(reportsDir)) {
|
|
if (!file.endsWith(".json")) {
|
|
continue;
|
|
}
|
|
|
|
const filepath = join(reportsDir, file);
|
|
const content = readFileSync(filepath, "utf-8");
|
|
const report = JSON.parse(content) as WeeklyReport;
|
|
reports.push(report);
|
|
}
|
|
|
|
return reports.sort((a, b) =>
|
|
b.week.localeCompare(a.week)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sanitize month string to only allow YYYY-MM format
|
|
*/
|
|
function sanitizeMonth(month: string): string {
|
|
if (!/^\d{4}-\d{2}$/.test(month)) {
|
|
throw new Error(`Invalid month format: ${month}. Expected YYYY-MM`);
|
|
}
|
|
return month;
|
|
}
|
|
|
|
/**
|
|
* Save a monthly report to disk
|
|
*/
|
|
export function saveMonthlyReport(root: string, report: MonthlyReport): string {
|
|
ensureDirectories(root);
|
|
const reportsDir = join(root, "monthly-reports");
|
|
const month = sanitizeMonth(report.month);
|
|
const filename = `${month}.json`;
|
|
const filepath = join(reportsDir, filename);
|
|
verifyPathWithinDirectory(filepath, reportsDir);
|
|
writeFileSync(filepath, JSON.stringify(report, null, 2), "utf-8");
|
|
return filename;
|
|
}
|
|
|
|
/**
|
|
* Load a specific monthly report by month identifier
|
|
*/
|
|
export function loadMonthlyReport(root: string, month: string): MonthlyReport | null {
|
|
month = sanitizeMonth(month);
|
|
const reportsDir = join(root, "monthly-reports");
|
|
const filepath = join(reportsDir, `${month}.json`);
|
|
if (!existsSync(filepath)) return null;
|
|
const content = readFileSync(filepath, "utf-8");
|
|
return JSON.parse(content) as MonthlyReport;
|
|
}
|