From 66eed9eeb92089232dfa1ce44eab949ab79d0cbd Mon Sep 17 00:00:00 2001 From: Tom Brown <58399955+ToruGuy@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:56:53 +0100 Subject: [PATCH 1/4] chore: add v1.2.0 changelog entry --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0564a51..f4f4006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to megg will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0] - 2026-03-15 + +### Added +- **Stale info.md warning** - `context()` now warns when `info.md` hasn't been updated in >30 days + - Banner shown in context output: `⚠️ info.md last updated N days ago` + - Prompts to run `megg init --update` +- **`init --update` mode** - Intelligent update of existing `info.md` + - Loads current info, analyzes what may be outdated + - Asks targeted questions section by section + - Updates `updated` timestamp on save + ## [1.1.0] - 2026-01-17 ### Added From 809ead13f3f02d6ec4970e0f9c016376a306eb34 Mon Sep 17 00:00:00 2001 From: Tom Brown <58399955+ToruGuy@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:03:34 +0100 Subject: [PATCH 2/4] feat: stale info.md warning in context() and init --update (v1.2.0) --- package.json | 2 +- src/commands/context.ts | 34 +++++++++++++- src/commands/init.ts | 99 +++++++++++++++++++++++++++++++++-------- src/index.ts | 17 +++---- src/types.ts | 15 +++++++ 5 files changed, 139 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index d6d92ee..a68a3ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "megg", - "version": "1.1.0", + "version": "1.2.0", "description": "megg - Memory for AI Agents. Simplified knowledge system with auto-discovery and size-aware loading.", "main": "build/index.js", "bin": { diff --git a/src/commands/context.ts b/src/commands/context.ts index 9ef6929..00ec1fe 100644 --- a/src/commands/context.ts +++ b/src/commands/context.ts @@ -39,6 +39,30 @@ const FULL_LOAD_THRESHOLD = 8000; // Load full if under this const SUMMARY_THRESHOLD = 16000; // Show summary if under this // Above SUMMARY_THRESHOLD = blocked +const STALE_INFO_DAYS = 30; // Warn if info.md not updated in this many days + +/** + * Parse `updated` timestamp from info.md frontmatter. + * Returns null if not found or unparseable. + */ +function parseUpdatedDate(infoContent: string): Date | null { + const match = infoContent.match(/^updated:\s*(.+)$/m); + if (!match) return null; + const d = new Date(match[1].trim()); + return isNaN(d.getTime()) ? null : d; +} + +/** + * Returns stale warning string if info.md is older than STALE_INFO_DAYS, else null. + */ +function getStaleWarning(infoContent: string, domain: string): string | undefined { + const updated = parseUpdatedDate(infoContent); + if (!updated) return undefined; + const daysSince = Math.floor((Date.now() - updated.getTime()) / (1000 * 60 * 60 * 24)); + if (daysSince < STALE_INFO_DAYS) return undefined; + return `⚠️ ${domain}/info.md last updated ${daysSince} days ago — still current? Run: megg init --update`; +} + /** * Main context command - gathers all relevant context for a path. */ @@ -54,11 +78,13 @@ export async function context(targetPath?: string, topic?: string): Promise c.staleWarning).map(c => c.staleWarning!); + if (staleWarnings.length > 0) { + out += staleWarnings.join('\n') + '\n\n'; + } + // Current context (deepest level info.md) const deepest = result.chain[result.chain.length - 1]; if (deepest) { diff --git a/src/commands/init.ts b/src/commands/init.ts index 5e1a81f..7b96968 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -9,7 +9,7 @@ import path from 'path'; import fs from 'fs/promises'; -import type { InitAnalysis, InitContent, DomainInfo, ProjectStructure } from '../types.js'; +import type { InitAnalysis, InitContent, DomainInfo, ProjectStructure, UpdateAnalysis, InfoSection } from '../types.js'; import { exists, readFile, writeFile, getTimestamp, ensureDir } from '../utils/files.js'; import { findAncestorMegg, getDomainName, MEGG_DIR_NAME, INFO_FILE_NAME, KNOWLEDGE_FILE_NAME } from '../utils/paths.js'; @@ -55,28 +55,67 @@ const SKIP_DIRS = [ export async function init( projectRoot?: string, content?: InitContent -): Promise { +): Promise { const root = projectRoot || process.cwd(); const meggDir = path.join(root, MEGG_DIR_NAME); const infoPath = path.join(meggDir, INFO_FILE_NAME); - // If content provided, create the files + // If content provided, create or update the files if (content) { return createMeggFiles(root, content); } - // Check if already initialized + // If already initialized → return update analysis instead of error if (await exists(infoPath)) { - return { - status: 'already_initialized', - message: 'megg already initialized. Use context() to load.', - }; + return analyzeForUpdate(infoPath); } - // Analyze project + // Analyze project for fresh init return analyzeProject(root); } +/** + * Reads existing info.md and returns section-by-section update analysis. + */ +async function analyzeForUpdate(infoPath: string): Promise { + const current = await readFile(infoPath); + + // Parse updated date from frontmatter + const updatedMatch = current.match(/^updated:\s*(.+)$/m); + const updatedDate = updatedMatch ? new Date(updatedMatch[1].trim()) : null; + const daysSinceUpdate = updatedDate && !isNaN(updatedDate.getTime()) + ? Math.floor((Date.now() - updatedDate.getTime()) / (1000 * 60 * 60 * 24)) + : 0; + + // Parse sections (## headings) from info content (strip frontmatter first) + const bodyMatch = current.match(/^---[\s\S]*?---\n([\s\S]*)$/); + const body = bodyMatch ? bodyMatch[1] : current; + + const sections: InfoSection[] = []; + const sectionRegex = /^(#{1,3} .+)\n([\s\S]*?)(?=^#{1,3} |\s*$)/gm; + let match; + while ((match = sectionRegex.exec(body)) !== null) { + const heading = match[1].replace(/^#+\s*/, '').trim(); + const content = match[2].trim(); + if (heading && content) { + sections.push({ heading, content }); + } + } + + // Generate targeted question per section + const questions = sections.map(s => + `**${s.heading}** currently says:\n> ${s.content.split('\n').slice(0, 3).join('\n> ')}${s.content.split('\n').length > 3 ? '\n> ...' : ''}\n→ Is this still accurate? What's changed?` + ); + + return { + status: 'needs_update', + currentInfo: current, + sections, + questions, + daysSinceUpdate, + }; +} + /** * Analyzes project and returns information for agent. */ @@ -199,20 +238,29 @@ async function createMeggFiles( ): Promise<{ success: boolean; message: string }> { const meggDir = path.join(root, MEGG_DIR_NAME); const now = getTimestamp(); + const infoPath = path.join(meggDir, INFO_FILE_NAME); try { await ensureDir(meggDir); - // Create info.md + // Preserve `created` timestamp if updating existing file + let createdTimestamp = now; + if (content.update && await exists(infoPath)) { + const existing = await readFile(infoPath); + const createdMatch = existing.match(/^created:\s*(.+)$/m); + if (createdMatch) createdTimestamp = createdMatch[1].trim(); + } + + // Create/update info.md const infoContent = `--- -created: ${now} +created: ${createdTimestamp} updated: ${now} type: context --- ${content.info} `; - await writeFile(path.join(meggDir, INFO_FILE_NAME), infoContent); + await writeFile(infoPath, infoContent); // Create knowledge.md if provided if (content.knowledge) { @@ -231,7 +279,9 @@ ${content.knowledge} return { success: true, - message: `✓ megg initialized in ${meggDir}`, + message: content.update + ? `✓ megg info.md updated in ${meggDir}` + : `✓ megg initialized in ${meggDir}`, }; } catch (err) { return { @@ -247,13 +297,15 @@ ${content.knowledge} export async function initCommand( projectRoot?: string, infoContent?: string, - knowledgeContent?: string + knowledgeContent?: string, + update?: boolean ): Promise { - // If content provided, create files + // If content provided, create or update files if (infoContent) { const content: InitContent = { info: infoContent, knowledge: knowledgeContent, + update, }; const result = await init(projectRoot, content); @@ -267,11 +319,22 @@ export async function initCommand( const analysis = await init(projectRoot); if ('status' in analysis) { - if (analysis.status === 'already_initialized') { - return analysis.message || 'Already initialized.'; + // Update analysis — show sections with targeted questions + if (analysis.status === 'needs_update') { + const a = analysis as import('../types.js').UpdateAnalysis; + let output = '# megg Update Analysis\n\n'; + if (a.daysSinceUpdate > 0) { + output += `> ⚠️ info.md last updated ${a.daysSinceUpdate} days ago\n\n`; + } + output += 'Review each section and confirm what has changed:\n\n'; + for (const q of a.questions) { + output += q + '\n\n---\n\n'; + } + output += 'Call `init(path, { info: "...", update: true })` with the updated content to save.'; + return output; } - // Format analysis for display + // Format fresh analysis for display let output = '# megg Init Analysis\n\n'; if (analysis.parentChain && analysis.parentChain.length > 0) { diff --git a/src/index.ts b/src/index.ts index 8138398..60d9ca6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ import { state, formatStateForDisplay } from "./commands/state.js"; // Create server instance const server = new McpServer({ name: "megg", - version: "1.1.0", + version: "1.2.0", }); const PROJECT_ROOT = process.cwd(); @@ -93,26 +93,27 @@ server.tool( server.tool( "init", - "Initialize megg in current directory. Without content: analyzes project and returns questions to ask. With content: creates .megg/info.md and optionally knowledge.md.", + "Initialize megg in current directory. Without content: analyzes project (or returns update analysis if already initialized). With content: creates .megg/info.md and optionally knowledge.md. Use update=true to update existing info.md (preserves created timestamp).", { projectRoot: z.string().optional().describe("Root directory (defaults to cwd)"), - info: z.string().optional().describe("Content for info.md (if provided, creates the file)"), + info: z.string().optional().describe("Content for info.md (if provided, creates or updates the file)"), knowledge: z.string().optional().describe("Initial content for knowledge.md (optional)"), + update: z.boolean().optional().describe("If true, update existing info.md instead of creating new (preserves created timestamp)"), }, - async ({ projectRoot, info, knowledge }) => { + async ({ projectRoot, info, knowledge, update }) => { try { const root = projectRoot || PROJECT_ROOT; if (info) { - // Create files mode - const result = await init(root, { info, knowledge }); + // Create or update files mode + const result = await init(root, { info, knowledge, update }); if ('success' in result) { return { content: [{ type: "text", text: result.message }] }; } } - // Analysis mode - const output = await initCommand(root); + // Analysis mode (fresh init or update analysis) + const output = await initCommand(root, undefined, undefined, update); return { content: [{ type: "text", text: output }] }; } catch (err: any) { return { diff --git a/src/types.ts b/src/types.ts index 70d686b..755f7ae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -40,6 +40,7 @@ export interface DomainInfo { path: string; meggPath: string; info: string; + staleWarning?: string; // Set if info.md hasn't been updated recently } export type KnowledgeMode = 'full' | 'summary' | 'blocked'; @@ -117,6 +118,20 @@ export interface InitAnalysis { export interface InitContent { info: string; knowledge?: string; + update?: boolean; // If true, update existing info.md instead of creating new +} + +export interface InfoSection { + heading: string; + content: string; +} + +export interface UpdateAnalysis { + status: 'needs_update'; + currentInfo: string; + sections: InfoSection[]; + questions: string[]; + daysSinceUpdate: number; } // ============================================================================ From e62dd07447b677a34756a397892ccfc7388d0985 Mon Sep 17 00:00:00 2001 From: Tom Brown <58399955+ToruGuy@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:08:29 +0100 Subject: [PATCH 3/4] feat: universal entry types (rule/fact/decision/process) --- CHANGELOG.md | 10 ++++++++++ src/commands/learn.ts | 6 +++--- src/index.ts | 2 +- src/types.ts | 2 +- src/utils/knowledge.ts | 33 ++++++++++++++++++--------------- 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4f4006..8aa68e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Loads current info, analyzes what may be outdated - Asks targeted questions section by section - Updates `updated` timestamp on save +- **Universal entry types** - New taxonomy that works for any domain (not just code) + - `rule` — always/never do X (replaces `pattern` + `gotcha`) + - `fact` — this is true about X (replaces `context`) + - `decision` — we chose X because Y (unchanged) + - `process` — how to do X step by step (new) + +### Breaking Changes +- Entry types `pattern`, `gotcha`, `context` removed; replaced by `rule`, `fact`, `process` + - Existing knowledge.md files with old types will still parse (type is read as-is from file) + - New entries must use the new types ## [1.1.0] - 2026-01-17 diff --git a/src/commands/learn.ts b/src/commands/learn.ts index b635e28..dea80ad 100644 --- a/src/commands/learn.ts +++ b/src/commands/learn.ts @@ -112,7 +112,7 @@ export interface LearnPromptResult { * Validates entry type. */ export function isValidEntryType(type: string): type is EntryType { - return ['decision', 'pattern', 'gotcha', 'context'].includes(type); + return ['rule', 'fact', 'decision', 'process'].includes(type); } /** @@ -127,7 +127,7 @@ export async function learnCommand( ): Promise { // Validate type if (!isValidEntryType(type)) { - return `Error: Invalid type "${type}". Must be one of: decision, pattern, gotcha, context`; + return `Error: Invalid type "${type}". Must be one of: rule, fact, decision, process`; } // Parse topics @@ -162,7 +162,7 @@ export async function learnCommand( */ export async function quickLearn( content: string, - type: EntryType = 'context', + type: EntryType = 'fact', targetPath?: string ): Promise { // Extract title from first line or first sentence diff --git a/src/index.ts b/src/index.ts index 60d9ca6..1f7a5e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,7 +54,7 @@ server.tool( "Add a knowledge entry to the nearest .megg/knowledge.md. Entries have type (decision/pattern/gotcha/context), topics for categorization, and content.", { title: z.string().describe("Short title for the entry"), - type: z.enum(["decision", "pattern", "gotcha", "context"]).describe("Entry type: decision (architectural choice), pattern (how we do things), gotcha (trap to avoid), context (background info)"), + type: z.enum(["rule", "fact", "decision", "process"]).describe("Entry type: rule (always/never do X), fact (this is true about X), decision (we chose X because Y), process (how to do X step by step)"), topics: z.array(z.string()).describe("Tags for categorization (e.g., ['auth', 'api', 'security'])"), content: z.string().describe("The knowledge content in markdown"), path: z.string().optional().describe("Target path (defaults to cwd, finds nearest .megg)"), diff --git a/src/types.ts b/src/types.ts index 755f7ae..c249f62 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,7 +12,7 @@ // Knowledge Entry Types // ============================================================================ -export type EntryType = 'decision' | 'pattern' | 'gotcha' | 'context'; +export type EntryType = 'rule' | 'fact' | 'decision' | 'process'; export interface KnowledgeEntry { date: string; diff --git a/src/utils/knowledge.ts b/src/utils/knowledge.ts index 26ba22e..ae599ef 100644 --- a/src/utils/knowledge.ts +++ b/src/utils/knowledge.ts @@ -62,7 +62,7 @@ function parseEntry(block: string): KnowledgeEntry | null { const title = headerMatch[2].trim(); // Parse metadata lines - let type: EntryType = 'context'; + let type: EntryType = 'fact'; let topics: string[] = []; let contentStart = 1; @@ -131,25 +131,28 @@ export function generateSummary(parsed: ParsedKnowledge): string { summary += '\n'; } - // Patterns - const patterns = byType.get('pattern') || []; - if (patterns.length > 0) { - summary += `## Patterns (${patterns.length})\n`; - for (const p of patterns.slice(0, 5)) { - summary += `- ${p.title}\n`; + // Rules + const rules = byType.get('rule') || []; + if (rules.length > 0) { + summary += `## Rules (${rules.length})\n`; + for (const r of rules.slice(0, 5)) { + summary += `- ${r.title}\n`; } - if (patterns.length > 5) { - summary += `- ... and ${patterns.length - 5} more\n`; + if (rules.length > 5) { + summary += `- ... and ${rules.length - 5} more\n`; } summary += '\n'; } - // Gotchas (important for avoiding issues) - const gotchas = byType.get('gotcha') || []; - if (gotchas.length > 0) { - summary += `## Gotchas (${gotchas.length})\n`; - for (const g of gotchas) { - summary += `- ⚠️ ${g.title}\n`; + // Processes + const processes = byType.get('process') || []; + if (processes.length > 0) { + summary += `## Processes (${processes.length})\n`; + for (const p of processes.slice(0, 5)) { + summary += `- ${p.title}\n`; + } + if (processes.length > 5) { + summary += `- ... and ${processes.length - 5} more\n`; } summary += '\n'; } From 2b739bb55fc2a4f02ab71d8629bbb5b492b89470 Mon Sep 17 00:00:00 2001 From: Tom Brown <58399955+ToruGuy@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:13:26 +0100 Subject: [PATCH 4/4] docs: update README with new entry types --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ab56c0d..40c25e1 100644 --- a/README.md +++ b/README.md @@ -129,10 +129,10 @@ When you call `context("clients/acme")`, megg loads the full chain: | Type | Use For | Example | |------|---------|---------| -| `decision` | Architectural choices | "We chose PostgreSQL over MongoDB because..." | -| `pattern` | Team conventions | "API endpoints use kebab-case" | -| `gotcha` | Traps to avoid | "Don't use localStorage for auth tokens" | -| `context` | Background info | "This client requires HIPAA compliance" | +| `rule` | Always/never do X | "Don't use localStorage for auth tokens" | +| `fact` | This is true about X | "This client requires HIPAA compliance" | +| `decision` | We chose X because Y | "We chose PostgreSQL over MongoDB because..." | +| `process` | How to do X step by step | "Deploy: build → tag → push → migrate" | ### Smart Token Management @@ -164,7 +164,10 @@ npx megg context npx megg context . --topic auth # Add a decision -npx megg learn "JWT Auth" decision "auth,security" "We use JWT with refresh tokens..." +npx megg learn "JWT Auth" decision "auth,security" "We use JWT because..." + +# Add a rule +npx megg learn "No localStorage for tokens" rule "auth,security" "Use httpOnly cookies instead" # Initialize megg npx megg init @@ -300,7 +303,7 @@ Brief description of what this project is. 3. When Z, prefer A ## Memory Files -- knowledge.md: decisions, patterns, gotchas +- knowledge.md: rules, facts, decisions, processes ``` ### knowledge.md Entry Format