mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +00:00
* feat: battle-tested skill patterns from production deployment Backport production-learned brain-operations patterns: - Iron Law of Back-Linking (mandatory bidirectional linking) - Brain filing rules (file by primary subject, not format) - Enrichment protocol (7-step pipeline, 3-tier system, person/company templates) - Media ingest workflows (articles, videos, podcasts, PDFs, screenshots) - Citation requirements (mandatory [Source: ...] on every fact) - Test Before Bulk operating principle - Voice recipe: unicode crash fix, PII scrub, identity-first prompt, DIY STT+LLM+TTS - X-to-Brain recipe: image OCR, Filtered Stream, tweet rating rubric, cron stagger * chore: bump version and changelog (v0.8.1) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add _brain-filing-rules.md to CLAUDE.md key files * feat: smart file upload with TUS resumable and .redirect.yaml pointers - Supabase Storage auto-selects upload method by file size: < 100 MB standard POST, >= 100 MB TUS resumable (6 MB chunks + retry) - Signed URL generation for private bucket access (1-hour expiry) - New `upload-raw` command with size routing: small text stays in git, large/media files go to cloud with .redirect.yaml pointer - New `signed-url` command for generating access links - File resolver supports both .redirect.yaml (v0.9+) and .redirect (legacy) - Redirect format upgraded: 10 fields with full metadata - All migration commands (mirror, redirect, restore, clean) handle both formats * feat: skills reference actual gbrain file commands - Filing rules document upload-raw, signed-url, and .redirect.yaml format - Ingest skill uses gbrain files upload-raw for raw source preservation - Maintain skill adds file storage health checks - Setup skill adds storage configuration phase with migration guidance - Voice recipe uses upload-raw for call audio storage - Migration v0.9.0 with complete storage setup instructions * chore: bump version and changelog (v0.9.0) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: gbrain publish -- shareable HTML with password protection First code+skill pair: deterministic code does the work (strip private data, encrypt with AES-256-GCM, generate self-contained HTML), the skill tells the agent when and how to use it. 34 new tests. See: https://x.com/garrytan/status/2042925773300908103 * feat: backlinks check/fix, page lint, and report commands Three new deterministic tools (zero LLM calls): - gbrain backlinks check/fix -- scans brain for entity mentions without back-links, creates them. Enforces the Iron Law from the skills. - gbrain lint [--fix] -- catches LLM preambles, code fence wrapping, placeholder dates, missing frontmatter, broken citations, empty sections. --fix auto-strips fixable artifacts. - gbrain report --type <name> -- saves timestamped reports to brain/reports/{type}/YYYY-MM-DD-HHMM.md for audit trails. 33 new tests (409 total, 0 fail). * feat: v0.9.0 migration tells agents to swap scripts for built-in commands Migration file now: - Lists all 5 new deterministic commands with usage examples - Includes a script-to-command replacement table (old -> new) - Tells the agent to find custom script references in AGENTS.md, skills, and cron jobs and replace with gbrain commands - Adds recommended cron jobs for daily backlink fix + weekly lint - References the Thin Harness, Fat Skills thread * fix: CLI routing bugs found during DX review - Fixed subArgs reference error in handleCliOnly (used wrong variable name) - Renamed gbrain backlinks check/fix to gbrain check-backlinks to avoid conflict with existing backlinks operation (per-page incoming links) - Added TOOLS section to --help output showing publish, check-backlinks, lint, report - Added upload-raw and signed-url to FILES section in --help - Updated all docs/migration references to use check-backlinks * fix: security hardening from adversarial review - XSS: sanitize marked.parse() output (strip script/iframe/on* attrs) - Path traversal: validate report --type against [a-z0-9-] pattern - TUS: HEAD request before retry to get server's actual offset (TUS spec) - Pointer: upload-raw now includes pointer content in JSON output - Symlinks: use lstatSync in all walkers to prevent directory escape --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
/**
|
|
* gbrain report — Save a structured report to brain/reports/.
|
|
*
|
|
* Deterministic: zero LLM calls. Creates timestamped report pages
|
|
* for audit trails of enrichment sweeps, maintenance runs, syncs, etc.
|
|
*
|
|
* Usage:
|
|
* gbrain report --type enrichment-sweep --title "Enrichment Sweep" --content "..."
|
|
* echo "report body" | gbrain report --type meeting-sync --title "Meeting Sync"
|
|
* gbrain report --type enrichment-sweep --dir /path/to/brain
|
|
*/
|
|
|
|
import { writeFileSync, mkdirSync, readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
export async function runReport(args: string[]) {
|
|
const typeIdx = args.indexOf('--type');
|
|
const titleIdx = args.indexOf('--title');
|
|
const contentIdx = args.indexOf('--content');
|
|
const dirIdx = args.indexOf('--dir');
|
|
|
|
const reportType = typeIdx >= 0 ? args[typeIdx + 1] : null;
|
|
const brainDir = dirIdx >= 0 ? args[dirIdx + 1] : '.';
|
|
|
|
// Validate reportType to prevent path traversal
|
|
if (reportType && !/^[a-z0-9][a-z0-9-]*$/.test(reportType)) {
|
|
console.error('Report type must be lowercase alphanumeric with hyphens only (e.g., "enrichment-sweep")');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!reportType) {
|
|
console.error('Usage: gbrain report --type <name> --title "..." --content "..." [--dir <brain>]');
|
|
console.error(' Or pipe content via stdin:');
|
|
console.error(' echo "report body" | gbrain report --type meeting-sync --title "Daily Sync"');
|
|
console.error('');
|
|
console.error(' Common types: enrichment-sweep, meeting-sync, maintenance, backlink-check, lint');
|
|
console.error(' Creates: brain/reports/{type}/{YYYY-MM-DD-HHMM}.md');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Read content from --content arg or stdin
|
|
let content = contentIdx >= 0 ? args[contentIdx + 1] : null;
|
|
if (!content && !process.stdin.isTTY) {
|
|
content = readFileSync('/dev/stdin', 'utf-8');
|
|
}
|
|
|
|
if (!content?.trim()) {
|
|
console.error('No content provided. Use --content "..." or pipe via stdin.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const now = new Date();
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
const dateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
const timeStr = `${pad(now.getHours())}${pad(now.getMinutes())}`;
|
|
const timePretty = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
|
|
const title = titleIdx >= 0
|
|
? args[titleIdx + 1]
|
|
: reportType.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
|
|
const filename = `${dateStr}-${timeStr}.md`;
|
|
const reportDir = join(brainDir, 'reports', reportType);
|
|
mkdirSync(reportDir, { recursive: true });
|
|
|
|
const page = `---
|
|
title: "${title} -- ${dateStr}"
|
|
type: report
|
|
report_type: ${reportType}
|
|
date: ${dateStr}
|
|
time: "${timePretty}"
|
|
---
|
|
|
|
# ${title} -- ${dateStr} ${timePretty}
|
|
|
|
${content.trim()}
|
|
`;
|
|
|
|
const filepath = join(reportDir, filename);
|
|
writeFileSync(filepath, page);
|
|
console.log(filepath);
|
|
}
|