New:Socket for Asana Is Now Available.Learn more
Get Started

aidimag

Package Overview
Dependencies
Maintainers
1
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

aidimag - npm Package Compare versions

Comparing version
1.0.19
to
1.0.20
+56
dist/capture/transcript-sources.d.ts
/**
* Transcript source adapters — each AI coding tool persists chat transcripts
* somewhere different; this module knows where, and how to pull out the
* genuine human-typed messages. The harvest orchestrator (harvest.ts) treats
* every source identically: discover sessions → extract user messages →
* redact → LLM claim extraction → proposal queue.
*
* Supported:
* - claude-code ~/.claude/projects/<path-slug>/*.jsonl
* - codex ~/.codex/sessions/(**)/*.jsonl (rollout files, matched by cwd)
* - copilot-vscode VS Code workspaceStorage/<hash>/chatSessions/*.json
* - cursor Cursor workspaceStorage/<hash>/state.vscdb (SQLite)
*
* Not supported: Devin (cloud-hosted, no local transcripts).
*/
/** One harvestable chat session (a transcript file / workspace DB). */
export interface TranscriptSession {
/** Stable id used for dedupe + evidence (file basename or workspace hash). */
id: string;
mtimeMs: number;
/** Lazily read + parse the human-typed messages. May throw on unreadable input. */
messages(): string[];
}
export interface TranscriptSource {
/** Stable slug: proposals get source `harvest:<name>`, cursor meta `harvest_<name>_last_mtime`. */
name: string;
/** Human-readable tool name for CLI output and evidence text. */
label: string;
/** Where transcripts live (for CLI hints); null if the tool isn't installed. */
transcriptDir(repoRoot: string): string | null;
/** All sessions for this repo, or null when the tool/transcripts are absent. */
sessions(repoRoot: string): TranscriptSession[] | null;
}
/** Claude Code stores transcripts under a slug of the project's absolute path. */
export declare function claudeProjectDir(repoRoot: string): string | null;
/** Extract genuine human-typed messages from one Claude Code session JSONL. */
export declare function userMessagesFromTranscript(jsonl: string): string[];
/**
* Parse one Codex rollout JSONL: find the session's cwd (session_meta /
* turn_context) and the human-typed user messages. Handles both the wrapped
* (`{type:"response_item",payload:{…}}`) and older flat line formats.
*/
export declare function codexTranscript(jsonl: string): {
cwd: string | null;
messages: string[];
};
/** Extract human-typed turns from one VS Code Copilot chatSessions/*.json file. */
export declare function copilotUserMessages(json: string): string[];
/**
* Cursor keeps chat state in a per-workspace SQLite DB (state.vscdb,
* ItemTable). Schema is undocumented and drifts between versions, so every
* read is defensive — a miss just means zero messages.
*/
export declare function cursorUserMessages(dbPath: string): string[];
/** All known transcript sources, in harvest order. */
export declare const TRANSCRIPT_SOURCES: TranscriptSource[];
/**
* Transcript source adapters — each AI coding tool persists chat transcripts
* somewhere different; this module knows where, and how to pull out the
* genuine human-typed messages. The harvest orchestrator (harvest.ts) treats
* every source identically: discover sessions → extract user messages →
* redact → LLM claim extraction → proposal queue.
*
* Supported:
* - claude-code ~/.claude/projects/<path-slug>/*.jsonl
* - codex ~/.codex/sessions/(**)/*.jsonl (rollout files, matched by cwd)
* - copilot-vscode VS Code workspaceStorage/<hash>/chatSessions/*.json
* - cursor Cursor workspaceStorage/<hash>/state.vscdb (SQLite)
*
* Not supported: Devin (cloud-hosted, no local transcripts).
*/
import Database from "better-sqlite3";
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { homedir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { debugLog } from "../debug.js";
/** Ignore short/noisy user turns ("yes", "continue", slash commands…). */
const MIN_MESSAGE_CHARS = 40;
/** Shared turn filter: drop scaffolding (`<system>…`, slash commands) and trivial turns. */
function keepHumanTurn(text) {
return Boolean(text) && !text.startsWith("<") && !text.startsWith("/") && text.length >= MIN_MESSAGE_CHARS;
}
function samePath(a, b) {
return path.resolve(a) === path.resolve(b);
}
// ================================================================ Claude Code
/** Claude Code stores transcripts under a slug of the project's absolute path. */
export function claudeProjectDir(repoRoot) {
const slug = path.resolve(repoRoot).replace(/[^a-zA-Z0-9]/g, "-");
const dir = path.join(homedir(), ".claude", "projects", slug);
return existsSync(dir) ? dir : null;
}
/** Extract genuine human-typed messages from one Claude Code session JSONL. */
export function userMessagesFromTranscript(jsonl) {
const out = [];
for (const line of jsonl.split("\n")) {
if (!line.trim())
continue;
let entry;
try {
entry = JSON.parse(line);
}
catch {
continue;
}
if (entry.type !== "user" || entry.isMeta)
continue;
const message = entry.message;
if (!message || message.role !== "user")
continue;
let text = "";
if (typeof message.content === "string") {
text = message.content;
}
else if (Array.isArray(message.content)) {
// tool_result blocks are machine output, not the human — skip them
text = message.content
.filter((c) => c.type === "text" && typeof c.text === "string")
.map((c) => c.text)
.join("\n");
}
text = text.trim();
if (keepHumanTurn(text))
out.push(text);
}
return out;
}
const claudeCodeSource = {
name: "claude-code",
label: "Claude Code",
transcriptDir: (repoRoot) => claudeProjectDir(repoRoot),
sessions(repoRoot) {
const dir = claudeProjectDir(repoRoot);
if (!dir)
return null;
return readdirSync(dir)
.filter((f) => f.endsWith(".jsonl"))
.map((f) => {
const abs = path.join(dir, f);
return {
id: f.replace(/\.jsonl$/, ""),
mtimeMs: statSync(abs).mtimeMs,
messages: () => userMessagesFromTranscript(readFileSync(abs, "utf8")),
};
});
},
};
// ================================================================ Codex CLI
function codexSessionsDir() {
const dir = path.join(homedir(), ".codex", "sessions");
return existsSync(dir) ? dir : null;
}
function walkJsonl(dir, out = []) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const abs = path.join(dir, entry.name);
if (entry.isDirectory())
walkJsonl(abs, out);
else if (entry.isFile() && entry.name.endsWith(".jsonl"))
out.push(abs);
}
return out;
}
/**
* Parse one Codex rollout JSONL: find the session's cwd (session_meta /
* turn_context) and the human-typed user messages. Handles both the wrapped
* (`{type:"response_item",payload:{…}}`) and older flat line formats.
*/
export function codexTranscript(jsonl) {
let cwd = null;
const messages = [];
for (const line of jsonl.split("\n")) {
if (!line.trim())
continue;
let entry;
try {
entry = JSON.parse(line);
}
catch {
continue;
}
const payload = (entry.payload && typeof entry.payload === "object" ? entry.payload : entry);
if (!cwd && typeof payload.cwd === "string")
cwd = payload.cwd;
if (payload.type !== "message" || payload.role !== "user")
continue;
let text = "";
if (typeof payload.content === "string") {
text = payload.content;
}
else if (Array.isArray(payload.content)) {
text = payload.content
.filter((c) => (c.type === "input_text" || c.type === "text") && typeof c.text === "string")
.map((c) => c.text)
.join("\n");
}
text = text.trim();
// Codex injects <environment_context>/<user_instructions> as user turns — startsWith("<") drops them
if (keepHumanTurn(text))
messages.push(text);
}
return { cwd, messages };
}
const codexSource = {
name: "codex",
label: "Codex CLI",
transcriptDir: () => codexSessionsDir(),
sessions(repoRoot) {
const dir = codexSessionsDir();
if (!dir)
return null;
const sessions = [];
for (const abs of walkJsonl(dir)) {
sessions.push({
id: path.basename(abs, ".jsonl"),
mtimeMs: statSync(abs).mtimeMs,
messages: () => {
const { cwd, messages } = codexTranscript(readFileSync(abs, "utf8"));
// only harvest sessions that ran inside this repo
if (!cwd || !(samePath(cwd, repoRoot) || path.resolve(cwd).startsWith(path.resolve(repoRoot) + path.sep))) {
return [];
}
return messages;
},
});
}
return sessions;
},
};
// ================================================================ VS Code workspace storage (Copilot + Cursor)
/** Per-platform user-data roots for a VS Code-family app ("Code", "Cursor", …). */
function appStorageRoots(app) {
const home = homedir();
const roots = [];
if (process.platform === "darwin") {
roots.push(path.join(home, "Library", "Application Support", app));
}
else if (process.platform === "win32") {
if (process.env.APPDATA)
roots.push(path.join(process.env.APPDATA, app));
}
else {
roots.push(path.join(home, ".config", app));
}
return roots.map((r) => path.join(r, "User", "workspaceStorage")).filter((r) => existsSync(r));
}
/** Find the workspaceStorage dirs whose workspace.json points at this repo. */
function workspaceStorageDirsFor(apps, repoRoot) {
const matches = [];
for (const app of apps) {
for (const storageRoot of appStorageRoots(app)) {
for (const hash of readdirSync(storageRoot)) {
const wsFile = path.join(storageRoot, hash, "workspace.json");
if (!existsSync(wsFile))
continue;
try {
const ws = JSON.parse(readFileSync(wsFile, "utf8"));
const uri = ws.folder ?? ws.workspace;
if (!uri || !uri.startsWith("file://"))
continue;
if (samePath(fileURLToPath(uri), repoRoot))
matches.push(path.join(storageRoot, hash));
}
catch {
// malformed workspace.json — ignore this workspace
}
}
}
}
return matches;
}
// ---------------------------------------------------------------- Copilot (VS Code)
/** Extract human-typed turns from one VS Code Copilot chatSessions/*.json file. */
export function copilotUserMessages(json) {
const out = [];
let data;
try {
data = JSON.parse(json);
}
catch {
return out;
}
for (const req of data.requests ?? []) {
let text = req.message?.text ?? "";
if (!text && Array.isArray(req.message?.parts)) {
text = req.message.parts
.filter((p) => typeof p.text === "string")
.map((p) => p.text)
.join("");
}
text = text.trim();
if (keepHumanTurn(text))
out.push(text);
}
return out;
}
const copilotSource = {
name: "copilot-vscode",
label: "GitHub Copilot (VS Code)",
transcriptDir(repoRoot) {
const dirs = workspaceStorageDirsFor(["Code", "Code - Insiders", "VSCodium"], repoRoot);
for (const d of dirs) {
const chatDir = path.join(d, "chatSessions");
if (existsSync(chatDir))
return chatDir;
}
return null;
},
sessions(repoRoot) {
const dirs = workspaceStorageDirsFor(["Code", "Code - Insiders", "VSCodium"], repoRoot);
const sessions = [];
let found = false;
for (const d of dirs) {
const chatDir = path.join(d, "chatSessions");
if (!existsSync(chatDir))
continue;
found = true;
for (const f of readdirSync(chatDir).filter((f) => f.endsWith(".json"))) {
const abs = path.join(chatDir, f);
sessions.push({
id: f.replace(/\.json$/, ""),
mtimeMs: statSync(abs).mtimeMs,
messages: () => copilotUserMessages(readFileSync(abs, "utf8")),
});
}
}
return found ? sessions : null;
},
};
// ---------------------------------------------------------------- Cursor
/**
* Cursor keeps chat state in a per-workspace SQLite DB (state.vscdb,
* ItemTable). Schema is undocumented and drifts between versions, so every
* read is defensive — a miss just means zero messages.
*/
export function cursorUserMessages(dbPath) {
const out = [];
const seen = new Set();
const push = (text) => {
if (typeof text !== "string")
return;
const t = text.trim();
if (!keepHumanTurn(t) || seen.has(t))
return;
seen.add(t);
out.push(t);
};
try {
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
try {
const rows = db
.prepare(`SELECT key, value FROM ItemTable WHERE key IN (?, ?)`)
.all("aiService.prompts", "workbench.panel.aichat.view.aichat.chatdata");
for (const row of rows) {
let value;
try {
value = JSON.parse(row.value);
}
catch {
continue;
}
if (row.key === "aiService.prompts" && Array.isArray(value)) {
for (const p of value)
push(p?.text);
}
else if (row.key.endsWith("chatdata") && value && typeof value === "object") {
const tabs = value.tabs ?? [];
for (const tab of tabs) {
for (const b of tab.bubbles ?? []) {
if (b?.type === "user")
push(b.text);
}
}
}
}
}
finally {
db.close();
}
}
catch (err) {
debugLog(`cursor transcript ${dbPath} (skipped)`, err);
}
return out;
}
const cursorSource = {
name: "cursor",
label: "Cursor",
transcriptDir(repoRoot) {
const dirs = workspaceStorageDirsFor(["Cursor"], repoRoot);
return dirs.find((d) => existsSync(path.join(d, "state.vscdb"))) ?? null;
},
sessions(repoRoot) {
const dirs = workspaceStorageDirsFor(["Cursor"], repoRoot);
const sessions = [];
let found = false;
for (const d of dirs) {
const dbPath = path.join(d, "state.vscdb");
if (!existsSync(dbPath))
continue;
found = true;
sessions.push({
id: path.basename(d), // workspace hash — one rolling "session" per workspace DB
mtimeMs: statSync(dbPath).mtimeMs,
messages: () => cursorUserMessages(dbPath),
});
}
return found ? sessions : null;
},
};
// ================================================================ registry
/** All known transcript sources, in harvest order. */
export const TRANSCRIPT_SOURCES = [claudeCodeSource, codexSource, copilotSource, cursorSource];
//# sourceMappingURL=transcript-sources.js.map
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.anup-khanal/aidimag",
"description": "Persistent, verified memory for AI coding agents: recall, capture, guardrails, review queue.",
"repository": {
"url": "https://github.com/AiDimag/aidimag",
"source": "github"
},
"version": "1.0.20",
"websiteUrl": "https://aidimag.com",
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "aidimag",
"version": "1.0.20",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
},
"packageArguments": [
{
"type": "positional",
"value": "mcp",
"description": "Start the aidimag MCP server over stdio"
}
],
"environmentVariables": [
{
"name": "AIDIMAG_REPO",
"description": "Absolute path to the git repository whose memory store to serve. Optional: defaults to walking up from the current working directory.",
"isRequired": false,
"format": "string"
},
{
"name": "AIDIMAG_LLM",
"description": "LLM provider for extraction features (chat_harvest, knowledge ingestion): openai | ollama | off. Optional: auto-detected.",
"isRequired": false,
"format": "string"
},
{
"name": "OPENAI_API_KEY",
"description": "OpenAI API key, used only if the OpenAI provider is selected/detected for extraction features.",
"isRequired": false,
"isSecret": true,
"format": "string"
}
]
}
]
}
+30
-16
/**
* Transcript harvester — out-of-band capture of the context humans type into
* AI chats. Claude Code persists every session as JSONL under
* ~/.claude/projects/<path-slug>/*.jsonl; the USER messages in there are the
* highest-signal capture source aidimag has: they're the facts a human already
* decided were worth teaching an AI ("we use X because Y", "never touch Z").
* AI chats. The USER messages in those transcripts are the highest-signal
* capture source aidimag has: they're the facts a human already decided were
* worth teaching an AI ("we use X because Y", "never touch Z").
*
* `dim harvest` extracts durable, falsifiable claims from those messages with
* the configured LLM provider (OpenAI/Ollama, same fallback as knowledge
* ingestion) and queues them as proposals (source `harvest:claude-code`) —
* nothing becomes active memory without `dim review`.
* Sources (see transcript-sources.ts): Claude Code, Codex CLI, GitHub Copilot
* (VS Code) and Cursor. `dim harvest` extracts durable, falsifiable claims
* from those messages with the configured LLM provider (OpenAI/Ollama, same
* fallback as knowledge ingestion) and queues them as proposals (source
* `harvest:<tool>`) — nothing becomes active memory without `dim review`.
*

@@ -19,2 +19,12 @@ * Privacy: opt-in by invocation, local-only (transcripts never leave the

import type { MemoryStore } from "../db/store.js";
export { claudeProjectDir, userMessagesFromTranscript } from "./transcript-sources.js";
export interface SourceHarvestResult {
source: string;
label: string;
sessionsScanned: number;
messagesConsidered: number;
proposed: number;
duplicates: number;
transcriptDir: string | null;
}
export interface HarvestResult {

@@ -26,19 +36,21 @@ sessionsScanned: number;

provider: string | null;
/** First detected source's transcript dir (back-compat; prefer `sources`). */
transcriptDir: string | null;
/** Per-source breakdown — only sources whose tool/transcripts were found. */
sources: SourceHarvestResult[];
}
/** Claude Code stores transcripts under a slug of the project's absolute path. */
export declare function claudeProjectDir(repoRoot: string): string | null;
/** Very conservative redaction: drop lines that look like secrets before they reach any LLM. */
export declare function redactSecrets(text: string): string;
/** Extract genuine human-typed messages from one Claude Code session JSONL. */
export declare function userMessagesFromTranscript(jsonl: string): string[];
export declare const HARVEST_EXTRACT_INSTRUCTIONS = "You are reviewing messages a DEVELOPER typed into an AI coding assistant while working on their project. These messages often contain durable project knowledge the developer was teaching the AI: decisions, conventions, gotchas, failed approaches, architecture facts, rules.\n\nExtract that durable knowledge as FALSIFIABLE claims. Rules:\n\n1. Only durable, project-specific facts the HUMAN stated \u2014 not the task of the day, not questions, not generic programming advice.\n2. Write each claim as a checkable statement about the codebase.\n3. kinds: DECISION, CONVENTION, GOTCHA, FAILED_APPROACH, ARCHITECTURE, INVARIANT, GUARDRAIL (set guardrail_level: never|ask-first|always), SKILL, TODO_CONTEXT.\n4. Scope with paths/symbols when the messages name them; else leave empty.\n5. In \"rationale\", QUOTE the fragment of the developer's message the claim came from.\n6. Extract 0\u20138 claims. Zero is fine \u2014 most sessions contain none. Do NOT invent.\n\nRespond with ONLY a JSON object of this exact shape:\n{\"claims\":[{\"kind\":\"CONVENTION\",\"claim\":\"...\",\"paths\":[\"src/x\"],\"symbols\":[],\"guardrail_level\":null,\"rationale\":\"user said: \\\"...\\\"\"}]}";
/**
* Harvest new/updated Claude Code sessions for this repo into the proposal
* queue. Cursor-tracked by file mtime; `all` rescans everything (the proposal
* dedupe index absorbs repeats).
* Harvest new/updated AI-chat sessions for this repo — across every detected
* source — into the proposal queue. Cursor-tracked by transcript mtime per
* source; `all` rescans everything (the proposal dedupe index absorbs repeats).
*/
export declare function harvestClaudeSessions(store: MemoryStore, repoRoot: string, opts?: {
export declare function harvestSessions(store: MemoryStore, repoRoot: string, opts?: {
all?: boolean;
sources?: string[];
}): Promise<HarvestResult>;
/** Back-compat alias — harvests all detected sources, not just Claude Code. */
export declare const harvestClaudeSessions: typeof harvestSessions;
/**

@@ -48,2 +60,4 @@ * Wire `dim harvest -q` into the repo's Claude Code SessionEnd hook

* Additive: merges with existing settings, never clobbers other hooks.
* (Codex/Copilot/Cursor have no session-end hook — those sources are swept
* whenever `dim harvest` runs.)
*/

@@ -50,0 +64,0 @@ export declare function installClaudeSessionEndHook(repoRoot: string): {

/**
* Transcript harvester — out-of-band capture of the context humans type into
* AI chats. Claude Code persists every session as JSONL under
* ~/.claude/projects/<path-slug>/*.jsonl; the USER messages in there are the
* highest-signal capture source aidimag has: they're the facts a human already
* decided were worth teaching an AI ("we use X because Y", "never touch Z").
* AI chats. The USER messages in those transcripts are the highest-signal
* capture source aidimag has: they're the facts a human already decided were
* worth teaching an AI ("we use X because Y", "never touch Z").
*
* `dim harvest` extracts durable, falsifiable claims from those messages with
* the configured LLM provider (OpenAI/Ollama, same fallback as knowledge
* ingestion) and queues them as proposals (source `harvest:claude-code`) —
* nothing becomes active memory without `dim review`.
* Sources (see transcript-sources.ts): Claude Code, Codex CLI, GitHub Copilot
* (VS Code) and Cursor. `dim harvest` extracts durable, falsifiable claims
* from those messages with the configured LLM provider (OpenAI/Ollama, same
* fallback as knowledge ingestion) and queues them as proposals (source
* `harvest:<tool>`) — nothing becomes active memory without `dim review`.
*

@@ -18,4 +18,3 @@ * Privacy: opt-in by invocation, local-only (transcripts never leave the

*/
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";

@@ -25,13 +24,7 @@ import { getTextProvider } from "../knowledge/llm.js";

import { debugLog } from "../debug.js";
const CURSOR_META_KEY = "harvest_claude_last_mtime";
/** Ignore short/noisy user turns ("yes", "continue", slash commands…). */
const MIN_MESSAGE_CHARS = 40;
import { TRANSCRIPT_SOURCES } from "./transcript-sources.js";
// re-export for back-compat (tests, external callers)
export { claudeProjectDir, userMessagesFromTranscript } from "./transcript-sources.js";
/** Cap what we send to the LLM per session (chars). */
const MAX_SESSION_CHARS = 24_000;
/** Claude Code stores transcripts under a slug of the project's absolute path. */
export function claudeProjectDir(repoRoot) {
const slug = path.resolve(repoRoot).replace(/[^a-zA-Z0-9]/g, "-");
const dir = path.join(homedir(), ".claude", "projects", slug);
return existsSync(dir) ? dir : null;
}
/** Very conservative redaction: drop lines that look like secrets before they reach any LLM. */

@@ -45,41 +38,2 @@ export function redactSecrets(text) {

}
/** Extract genuine human-typed messages from one Claude Code session JSONL. */
export function userMessagesFromTranscript(jsonl) {
const out = [];
for (const line of jsonl.split("\n")) {
if (!line.trim())
continue;
let entry;
try {
entry = JSON.parse(line);
}
catch {
continue;
}
if (entry.type !== "user" || entry.isMeta)
continue;
const message = entry.message;
if (!message || message.role !== "user")
continue;
let text = "";
if (typeof message.content === "string") {
text = message.content;
}
else if (Array.isArray(message.content)) {
// tool_result blocks are machine output, not the human — skip them
text = message.content
.filter((c) => c.type === "text" && typeof c.text === "string")
.map((c) => c.text)
.join("\n");
}
text = text.trim();
// skip injected command/system scaffolding and trivial turns
if (!text || text.startsWith("<") || text.startsWith("/"))
continue;
if (text.length < MIN_MESSAGE_CHARS)
continue;
out.push(text);
}
return out;
}
export const HARVEST_EXTRACT_INSTRUCTIONS = `You are reviewing messages a DEVELOPER typed into an AI coding assistant while working on their project. These messages often contain durable project knowledge the developer was teaching the AI: decisions, conventions, gotchas, failed approaches, architecture facts, rules.

@@ -98,18 +52,14 @@

{"claims":[{"kind":"CONVENTION","claim":"...","paths":["src/x"],"symbols":[],"guardrail_level":null,"rationale":"user said: \\"...\\""}]}`;
function pendingSessions(dir, sinceMtimeMs, all) {
return readdirSync(dir)
.filter((f) => f.endsWith(".jsonl"))
.map((f) => {
const abs = path.join(dir, f);
return { file: f, abs, mtimeMs: statSync(abs).mtimeMs };
})
.filter((s) => all || s.mtimeMs > sinceMtimeMs)
.sort((a, b) => a.mtimeMs - b.mtimeMs);
function cursorMetaKey(source) {
// claude-code keeps its historical key so existing installs don't rescan
return source.name === "claude-code"
? "harvest_claude_last_mtime"
: `harvest_${source.name.replace(/-/g, "_")}_last_mtime`;
}
/**
* Harvest new/updated Claude Code sessions for this repo into the proposal
* queue. Cursor-tracked by file mtime; `all` rescans everything (the proposal
* dedupe index absorbs repeats).
* Harvest new/updated AI-chat sessions for this repo — across every detected
* source — into the proposal queue. Cursor-tracked by transcript mtime per
* source; `all` rescans everything (the proposal dedupe index absorbs repeats).
*/
export async function harvestClaudeSessions(store, repoRoot, opts = {}) {
export async function harvestSessions(store, repoRoot, opts = {}) {
const result = {

@@ -122,7 +72,25 @@ sessionsScanned: 0,

transcriptDir: null,
sources: [],
};
const dir = claudeProjectDir(repoRoot);
if (!dir)
const wanted = opts.sources?.length
? TRANSCRIPT_SOURCES.filter((s) => opts.sources.includes(s.name))
: TRANSCRIPT_SOURCES;
// detect sources first so we can report "no transcripts anywhere" without an LLM
const detected = [];
for (const source of wanted) {
let sessions;
try {
sessions = source.sessions(repoRoot);
}
catch (err) {
debugLog(`harvest source ${source.name} discovery (skipped)`, err);
continue;
}
if (sessions === null)
continue;
detected.push({ source, sessions });
}
if (!detected.length)
return result;
result.transcriptDir = dir;
result.transcriptDir = detected[0].source.transcriptDir(repoRoot);
const provider = await getTextProvider();

@@ -132,54 +100,74 @@ if (!provider)

result.provider = `${provider.name}/${provider.model}`;
const cursor = opts.all ? 0 : parseFloat(store.getMeta(CURSOR_META_KEY) ?? "0") || 0;
const sessions = pendingSessions(dir, cursor, Boolean(opts.all));
let maxMtime = cursor;
for (const s of sessions) {
result.sessionsScanned++;
maxMtime = Math.max(maxMtime, s.mtimeMs);
let messages;
try {
messages = userMessagesFromTranscript(readFileSync(s.abs, "utf8"));
for (const { source, sessions } of detected) {
const sourceResult = {
source: source.name,
label: source.label,
sessionsScanned: 0,
messagesConsidered: 0,
proposed: 0,
duplicates: 0,
transcriptDir: source.transcriptDir(repoRoot),
};
result.sources.push(sourceResult);
const metaKey = cursorMetaKey(source);
const cursor = opts.all ? 0 : parseFloat(store.getMeta(metaKey) ?? "0") || 0;
const pending = sessions
.filter((s) => opts.all || s.mtimeMs > cursor)
.sort((a, b) => a.mtimeMs - b.mtimeMs);
let maxMtime = cursor;
for (const s of pending) {
sourceResult.sessionsScanned++;
maxMtime = Math.max(maxMtime, s.mtimeMs);
let messages;
try {
messages = s.messages();
}
catch (err) {
debugLog(`harvest transcript ${source.name}/${s.id} (skipped)`, err);
continue; // unreadable/partial file — retry next --all run (cursor still advances past it)
}
if (!messages.length)
continue;
sourceResult.messagesConsidered += messages.length;
const corpus = redactSecrets(messages.join("\n\n---\n\n")).slice(0, MAX_SESSION_CHARS);
let claims;
try {
const raw = await provider.generate(HARVEST_EXTRACT_INSTRUCTIONS, `Developer messages from one coding session on this project:\n\n----- BEGIN MESSAGES -----\n${corpus}\n----- END MESSAGES -----`);
claims = parseClaims(raw);
}
catch (err) {
debugLog(`harvest llm extraction ${source.name}/${s.id} (skipped)`, err);
continue; // provider hiccup — this session retries on the next --all run
}
for (const c of claims) {
const p = store.propose({
kind: c.kind,
claim: c.claim,
paths: c.paths,
symbols: c.symbols,
guardrailLevel: c.guardrailLevel,
rationale: c.rationale ?? `Stated by the user in a ${source.label} session.`,
evidence: [
{ type: "HUMAN_ATTESTED", payload: `stated by user in ${source.label} session ${s.id.slice(0, 8)}` },
],
source: `harvest:${source.name}`,
sourceRef: s.id,
});
if (p)
sourceResult.proposed++;
else
sourceResult.duplicates++;
}
}
catch (err) {
debugLog(`harvest transcript ${s.file} (skipped)`, err);
continue; // unreadable/partial file — retry next run (cursor still advances past it)
}
if (!messages.length)
continue;
result.messagesConsidered += messages.length;
const corpus = redactSecrets(messages.join("\n\n---\n\n")).slice(0, MAX_SESSION_CHARS);
let claims;
try {
const raw = await provider.generate(HARVEST_EXTRACT_INSTRUCTIONS, `Developer messages from one coding session on this project:\n\n----- BEGIN MESSAGES -----\n${corpus}\n----- END MESSAGES -----`);
claims = parseClaims(raw);
}
catch (err) {
debugLog(`harvest llm extraction ${s.file} (skipped)`, err);
continue; // provider hiccup — this session retries on the next --all run
}
const sessionId = s.file.replace(/\.jsonl$/, "");
for (const c of claims) {
const p = store.propose({
kind: c.kind,
claim: c.claim,
paths: c.paths,
symbols: c.symbols,
guardrailLevel: c.guardrailLevel,
rationale: c.rationale ?? "Stated by the user in a Claude Code session.",
evidence: [
{ type: "HUMAN_ATTESTED", payload: `stated by user in Claude Code session ${sessionId.slice(0, 8)}` },
],
source: "harvest:claude-code",
sourceRef: sessionId,
});
if (p)
result.proposed++;
else
result.duplicates++;
}
if (maxMtime > cursor)
store.setMeta(metaKey, String(maxMtime));
result.sessionsScanned += sourceResult.sessionsScanned;
result.messagesConsidered += sourceResult.messagesConsidered;
result.proposed += sourceResult.proposed;
result.duplicates += sourceResult.duplicates;
}
if (maxMtime > cursor)
store.setMeta(CURSOR_META_KEY, String(maxMtime));
return result;
}
/** Back-compat alias — harvests all detected sources, not just Claude Code. */
export const harvestClaudeSessions = harvestSessions;
// ---------------------------------------------------------------- hook install

@@ -191,2 +179,4 @@ const HOOK_COMMAND = "dim harvest -q";

* Additive: merges with existing settings, never clobbers other hooks.
* (Codex/Copilot/Cursor have no session-end hook — those sources are swept
* whenever `dim harvest` runs.)
*/

@@ -193,0 +183,0 @@ export function installClaudeSessionEndHook(repoRoot) {

@@ -8,3 +8,3 @@ /**

*/
export declare const SESSION_END_PROMPT = "You are finishing a coding session. Before you stop, extract durable knowledge about this codebase so future sessions don't have to re-discover it.\n\nReview what you learned this session and propose memories using the `memory_propose` tool. Rules:\n\n1. Only propose things that are DURABLE (true beyond this session) and NON-OBVIOUS (not derivable from a quick file read).\n2. Write each claim as a FALSIFIABLE statement \u2014 something a checker could verify against the code. Bad: \"the auth code is tricky\". Good: \"JWT refresh in src/auth/refresh.ts must run before middleware chain; reordering breaks session renewal (see commit abc123)\".\n3. Pick the right kind:\n - DECISION: a choice made and why (alternatives rejected)\n - CONVENTION: a rule consistently followed in this repo\n - GOTCHA: surprising behavior that cost you time\n - FAILED_APPROACH: something you tried that did NOT work, and why\n - ARCHITECTURE: how components fit together\n - INVARIANT: something that must always/never hold\n - GUARDRAIL: a behavioral rule for future agents \u2014 pass guardrail_level: 'never' (refuse + explain), 'always' (do without asking), or 'ask-first' (confirm with the user first)\n - SKILL: a reusable step-by-step procedure (e.g. \"Deploy: 1) \u2026 2) \u2026 3) \u2026\") that the team runs repeatedly\n - TODO_CONTEXT: unfinished work + the context needed to resume it\n4. Scope each memory to the paths/symbols it applies to. Repo-wide only if truly global.\n5. Attach evidence whenever possible: COMMIT_REF (a sha), STATIC_CHECK (a grep/assertion command that passes iff the claim holds), TEST_RESULT (a test command), or HUMAN_ATTESTED as last resort.\n6. FAILED_APPROACH memories are especially valuable \u2014 they prevent future sessions from repeating dead ends.\n7. Propose 0\u20137 memories. Zero is fine if nothing durable was learned. Do NOT pad.\n8. Before proposing, call `memory_critique` with a short summary of what you did and the files you touched. It checks your work against the project's existing memory and guardrails \u2014 resolve any contradictions or guardrail concerns it raises first.\n\nRespect all GUARDRAIL memories you've seen this session: 'never' = refuse and explain why, 'always' = do without asking, 'ask-first' = ask the user before proceeding.\n\nYour proposals enter a human review queue (`dim review`); they do not become active memory until approved.";
export declare const SESSION_END_PROMPT = "You are finishing a coding session. Before you stop, extract durable knowledge about this codebase so future sessions don't have to re-discover it.\n\nReview what you learned this session and propose memories using the `memory_propose` tool. Rules:\n\n1. Only propose things that are DURABLE (true beyond this session) and NON-OBVIOUS (not derivable from a quick file read).\n2. Write each claim as a FALSIFIABLE statement \u2014 something a checker could verify against the code. Bad: \"the auth code is tricky\". Good: \"JWT refresh in src/auth/refresh.ts must run before middleware chain; reordering breaks session renewal (see commit abc123)\".\n3. Pick the right kind:\n - DECISION: a choice made and why (alternatives rejected)\n - CONVENTION: a rule consistently followed in this repo\n - GOTCHA: surprising behavior that cost you time\n - FAILED_APPROACH: something you tried that did NOT work, and why\n - ARCHITECTURE: how components fit together\n - INVARIANT: something that must always/never hold\n - GUARDRAIL: a behavioral rule for future agents \u2014 pass guardrail_level: 'never' (refuse + explain), 'always' (do without asking), or 'ask-first' (confirm with the user first)\n - SKILL: a reusable step-by-step procedure (e.g. \"Deploy: 1) \u2026 2) \u2026 3) \u2026\") that the team runs repeatedly\n - TODO_CONTEXT: unfinished work + the context needed to resume it\n4. Scope each memory to the paths/symbols it applies to. Repo-wide only if truly global.\n5. Attach evidence whenever possible: COMMIT_REF (a sha), STATIC_CHECK (a grep/assertion command that passes iff the claim holds), TEST_RESULT (a test command), or HUMAN_ATTESTED as last resort.\n6. FAILED_APPROACH memories are especially valuable \u2014 they prevent future sessions from repeating dead ends.\n7. Propose 0\u20137 memories. Zero is fine if nothing durable was learned. Do NOT pad.\n8. Before proposing, call `memory_critique` with a short summary of what you did and the files you touched. It checks your work against the project's existing memory and guardrails \u2014 resolve any contradictions or guardrail concerns it raises first.\n9. Separately from YOUR learnings: if the USER stated project facts during this session (\"we use X because Y\", \"never touch Z\", \u2026) that you didn't already capture with `context_note`, call `chat_harvest` once with the user's verbatim messages \u2014 it extracts and queues their statements for review in bulk.\n\nRespect all GUARDRAIL memories you've seen this session: 'never' = refuse and explain why, 'always' = do without asking, 'ask-first' = ask the user before proceeding.\n\nYour proposals enter a human review queue (`dim review`); they do not become active memory until approved.";
/**

@@ -11,0 +11,0 @@ * Ticket-aware variant (TICKETS_DESIGN T5): when the current branch carries a

@@ -29,2 +29,3 @@ /**

8. Before proposing, call \`memory_critique\` with a short summary of what you did and the files you touched. It checks your work against the project's existing memory and guardrails — resolve any contradictions or guardrail concerns it raises first.
9. Separately from YOUR learnings: if the USER stated project facts during this session ("we use X because Y", "never touch Z", …) that you didn't already capture with \`context_note\`, call \`chat_harvest\` once with the user's verbatim messages — it extracts and queues their statements for review in bulk.

@@ -47,5 +48,5 @@ Respect all GUARDRAIL memories you've seen this session: 'never' = refuse and explain why, 'always' = do without asking, 'ask-first' = ask the user before proceeding.

This session's branch is tied to ticket ${ticketId}. Additionally:
8. Call \`ticket_get\` first — the ticket usually carries the WHY (root cause, rejected alternatives, acceptance criteria) that the code alone doesn't.
9. Combine sources: claim from what you did + ticket title/description for the rationale. A bug ticket's root cause is usually a GOTCHA; what didn't fix it is a FAILED_APPROACH; acceptance criteria are INVARIANT candidates.
10. Proposals are tagged with ${ticketId} automatically (ticket_ref); you don't need to mention the id in claims.`);
10. Call \`ticket_get\` first — the ticket usually carries the WHY (root cause, rejected alternatives, acceptance criteria) that the code alone doesn't.
11. Combine sources: claim from what you did + ticket title/description for the rationale. A bug ticket's root cause is usually a GOTCHA; what didn't fix it is a FAILED_APPROACH; acceptance criteria are INVARIANT candidates.
12. Proposals are tagged with ${ticketId} automatically (ticket_ref); you don't need to mention the id in claims.`);
}

@@ -52,0 +53,0 @@ /** One-line summary of a proposal for human review UIs. */

@@ -213,4 +213,5 @@ /**

.command("harvest")
.description("Harvest durable facts YOU typed into AI chats (Claude Code transcripts) into the review queue — local-only, secrets redacted")
.description("Harvest durable facts YOU typed into AI chats (Claude Code, Codex CLI, Copilot/VS Code, Cursor) into the review queue — local-only, secrets redacted")
.option("--all", "Rescan every session (ignore cursor; dedupe absorbs repeats)")
.option("--source <names>", "Comma-separated sources to harvest: claude-code,codex,copilot-vscode,cursor (default: all detected)")
.option("--install-hook", "Wire `dim harvest -q` into this repo's Claude Code SessionEnd hook (.claude/settings.json)")

@@ -220,3 +221,3 @@ .option("-q, --quiet", "Only speak up when proposals are queued (for the SessionEnd hook)")

const root = findRepoRoot() ?? fail("not inside a git repo");
const { harvestClaudeSessions, installClaudeSessionEndHook, claudeProjectDir } = await import("../../capture/harvest.js");
const { harvestSessions, installClaudeSessionEndHook } = await import("../../capture/harvest.js");
if (opts.installHook) {

@@ -229,4 +230,7 @@ const { installed, settingsPath } = installClaudeSessionEndHook(root);

}
const sources = typeof opts.source === "string"
? opts.source.split(",").map((s) => s.trim()).filter(Boolean)
: undefined;
const store = MemoryStore.open(root, { create: true });
const res = await harvestClaudeSessions(store, root, { all: Boolean(opts.all) });
const res = await harvestSessions(store, root, { all: Boolean(opts.all), sources });
if (opts.quiet) {

@@ -239,5 +243,5 @@ if (res.proposed > 0) {

}
if (!res.transcriptDir) {
console.log(`No Claude Code transcripts found for this repo (${claudeProjectDir(root) ?? "~/.claude/projects/<repo-slug>"} missing).`);
console.log("Transcripts appear after your first Claude Code session here. Cursor/Copilot chat harvesting: planned.");
if (!res.sources.length) {
console.log("No AI-chat transcripts found for this repo (checked Claude Code, Codex CLI, Copilot/VS Code, Cursor).");
console.log("Transcripts appear after your first chat session in this repo. Devin is cloud-hosted and can't be harvested locally.");
}

@@ -248,3 +252,4 @@ else if (!res.provider) {

else if (res.sessionsScanned === 0) {
console.log("No new sessions since the last harvest. Use --all to rescan everything.");
const names = res.sources.map((s) => s.label).join(", ");
console.log(`No new sessions since the last harvest (sources: ${names}). Use --all to rescan everything.`);
}

@@ -256,6 +261,11 @@ else {

".");
for (const s of res.sources) {
if (s.sessionsScanned === 0)
continue;
console.log(` ${s.label}: ${s.sessionsScanned} session(s), ${s.proposed} proposal(s)`);
}
if (res.proposed)
console.log(`Review with \`dim review\`.`);
}
if (!opts.installHook && res.transcriptDir) {
if (res.sources.some((s) => s.source === "claude-code")) {
console.log(`(tip: \`dim harvest --install-hook\` runs this automatically when each Claude Code session ends)`);

@@ -262,0 +272,0 @@ }

@@ -7,6 +7,7 @@ #!/usr/bin/env node

* Tools: memory_search, memory_get_for_files, memory_write, memory_refute, memory_status,
* commits_mine, context_note (passive in-chat fact capture), … — searches are logged so zero-hit
* queries surface as coverage gaps (`dim gaps`).
* commits_mine, context_note (passive in-chat fact capture), chat_harvest (bulk
* on-the-fly harvest of the current session from any tool), … — searches are logged
* so zero-hit queries surface as coverage gaps (`dim gaps`).
* Resource: aidimag://digest — repo memory digest for session bootstrapping.
*/
export {};

@@ -7,4 +7,5 @@ #!/usr/bin/env node

* Tools: memory_search, memory_get_for_files, memory_write, memory_refute, memory_status,
* commits_mine, context_note (passive in-chat fact capture), … — searches are logged so zero-hit
* queries surface as coverage gaps (`dim gaps`).
* commits_mine, context_note (passive in-chat fact capture), chat_harvest (bulk
* on-the-fly harvest of the current session from any tool), … — searches are logged
* so zero-hit queries surface as coverage gaps (`dim gaps`).
* Resource: aidimag://digest — repo memory digest for session bootstrapping.

@@ -30,2 +31,4 @@ */

import { KNOWLEDGE_EXTRACT_INSTRUCTIONS, buildExtractionUser, parseClaims } from "../knowledge/extract.js";
import { redactSecrets, HARVEST_EXTRACT_INSTRUCTIONS } from "../capture/harvest.js";
import { getTextProvider } from "../knowledge/llm.js";
import { debugLog } from "../debug.js";

@@ -77,4 +80,51 @@ const PKG_VERSION = JSON.parse(readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf8")).version;

}
/**
* One help text, three discovery surfaces:
* - server `instructions` (sent to the client at initialize — most hosts feed it to the model)
* - the `help` prompt (surfaced as a slash command / prompt picker entry in MCP clients)
* - the `aidimag_help` tool (callable when the user asks "what can aidimag do?")
*/
const HELP_TEXT = `# aidimag — verified memory for this repo
aidimag gives AI agents persistent, *verified* memory about this codebase. Everything below
runs against the repo's local memory store; inferred knowledge waits in a human review queue
(\`dim review\`) before becoming active.
## Prompts (run these as slash commands / from the prompt picker)
- **session_start** — run at the START of a session: in-scope memory, guardrails, stale warnings, questions to ask
- **session_end_extraction** — run at the END: extract durable learnings into the review queue
- **knowledge_ingest** — summarize documents waiting in the knowledge inbox into reviewable memories
- **help** — show this overview
## Tools the agent can call
- **memory_search** / **memory_get_for_files** — recall before exploring or editing
- **memory_write** / **memory_propose** / **memory_refute** — record, queue, or retract knowledge
- **context_note** — capture a durable fact the user just stated, live
- **chat_harvest** — bulk-harvest the user's messages from the current session (works from any MCP client, incl. cloud tools)
- **memory_critique** — check planned work against verified memory + guardrails
- **memory_verify** / **memory_status** — re-run evidence, see counts
- **commits_mine** — mine git history for memory candidates
- **scratchpad_write / read / clear** — short-term session notes (TTL, never synced)
- **proposals_pending**, **knowledge_pending**, **knowledge_ingest_submit**, **ticket_get**
## Resources
- **aidimag://session-briefing** — the same briefing as \`dim brief\`
- **aidimag://digest** — compact repo-memory digest
- **aidimag://instructions** — passive-capture rules for agents
## Companion CLI (run in a terminal)
\`dim review\` (approve queued memories) · \`dim verify\` · \`dim harvest\` (offline chat-transcript harvest) ·
\`dim brief\` · \`dim generate-context\` · \`dim status\` — full list: \`dim --help\`
Tip for users: state facts naturally in chat ("we use X because Y", "never touch Z") — the agent
captures them via context_note/chat_harvest, and you approve them with \`dim review\`.`;
async function main() {
const server = new McpServer({ name: "aidimag", version: PKG_VERSION });
const server = new McpServer({ name: "aidimag", version: PKG_VERSION }, {
instructions: `aidimag provides verified, persistent memory for this repo. ` +
`Start sessions with the \`session_start\` prompt (or read aidimag://session-briefing); search memory before exploring (memory_search / memory_get_for_files); ` +
`capture user-stated facts live with context_note and bulk-harvest the session with chat_harvest; ` +
`end sessions with the \`session_end_extraction\` prompt. ` +
`If the user asks what aidimag can do (or types "aidimag help"), call the aidimag_help tool and relay it. ` +
`Inferred knowledge is queued for human approval via \`dim review\`.`,
});
const store = openStore();

@@ -318,2 +368,71 @@ server.tool("memory_search", "Search the repo's verified memory for decisions, conventions, gotchas, failed approaches, and invariants. Use BEFORE exploring the codebase — past sessions may already know the answer.", {

});
server.tool("chat_harvest", "Harvest the CURRENT chat session on the fly: pass the messages the USER typed this session (verbatim) and durable facts are extracted and queued for human review — the live, tool-agnostic equivalent of `dim harvest`. Works from ANY IDE/agent (Copilot, Cursor, Codex, Claude, Devin, …), including cloud tools with no local transcripts. Call at session end, or after a long exchange rich in project knowledge. Secrets are redacted server-side before any LLM sees the text. For single facts stated in passing, prefer context_note instead.", {
user_messages: z
.array(z.string())
.min(1)
.describe("The user's messages from this session, verbatim, in order. Include only what the HUMAN typed — no assistant replies, no tool output."),
agent_id: z.string().optional().describe("Your agent/tool identifier, e.g. 'copilot', 'cursor', 'devin'"),
session_id: z.string().optional().describe("A stable id for this chat session, if your host exposes one (used for dedupe/evidence)"),
}, async (args) => {
const meaningful = args.user_messages.map((m) => m.trim()).filter((m) => m.length >= 20);
if (!meaningful.length) {
return { content: [{ type: "text", text: "No substantive user messages to harvest." }] };
}
const provider = await getTextProvider();
if (!provider) {
return {
content: [
{
type: "text",
text: "No LLM provider is configured on this machine (Ollama/OPENAI_API_KEY), so server-side extraction is unavailable. " +
"Fallback: extract the durable facts YOURSELF from the user's messages and submit each one with `context_note` " +
"(kinds: DECISION, CONVENTION, GOTCHA, FAILED_APPROACH, ARCHITECTURE, INVARIANT, GUARDRAIL, SKILL, TODO_CONTEXT; " +
"quote the user verbatim in `quote`).",
},
],
};
}
const corpus = redactSecrets(meaningful.join("\n\n---\n\n")).slice(0, 24_000);
let claims;
try {
const raw = await provider.generate(HARVEST_EXTRACT_INSTRUCTIONS, `Developer messages from one coding session on this project:\n\n----- BEGIN MESSAGES -----\n${corpus}\n----- END MESSAGES -----`);
claims = parseClaims(raw);
}
catch (err) {
debugLog("chat_harvest llm extraction failed", err);
return {
content: [
{ type: "text", text: "Extraction failed (LLM provider error) — retry later, or capture key facts individually with `context_note`." },
],
};
}
const agent = args.agent_id ?? "agent";
const sessionTag = args.session_id ?? new Date().toISOString();
let proposed = 0;
let duplicates = 0;
for (const c of claims) {
const p = store.propose({
kind: c.kind,
claim: c.claim,
paths: c.paths,
symbols: c.symbols,
guardrailLevel: c.guardrailLevel,
rationale: c.rationale ?? `Stated by the user in a live ${agent} chat session.`,
evidence: [{ type: "HUMAN_ATTESTED", payload: `stated by user in live ${agent} session ${sessionTag}` }],
source: `harvest:live:${agent}`,
sourceRef: args.session_id,
});
if (p)
proposed++;
else
duplicates++;
}
const text = claims.length === 0
? `Scanned ${meaningful.length} user message(s): no durable facts found (that's normal for most sessions).`
: `Harvested ${meaningful.length} user message(s) via ${provider.name}/${provider.model}: ${proposed} proposal(s) queued for \`dim review\`` +
(duplicates ? `, ${duplicates} duplicate(s) skipped` : "") +
`. Continue the conversation — no need to mention this unless asked.`;
return { content: [{ type: "text", text }] };
});
server.tool("aidimag_help", "Show what aidimag offers: available tools, prompts (slash commands), resources, and the companion `dim` CLI commands. Call this when the user asks what aidimag can do, how to use it, or types 'aidimag help' — then relay the overview to them.", {}, async () => ({ content: [{ type: "text", text: HELP_TEXT }] }));
server.tool("memory_critique", "Review what you just did (or plan to do) against the project's VERIFIED memory and guardrails — a 'second critic' grounded in real, falsifiable beliefs rather than another model's opinion. Call BEFORE committing or proposing memories. Resolve guardrail violations and contradictions first.", {

@@ -510,2 +629,13 @@ summary: z.string().min(10).describe("What you did or plan to do, in a sentence or two"),

});
server.prompt("help", "Show everything aidimag offers in this session: tools, prompts (slash commands), resources, and companion `dim` CLI commands.", () => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Relay the following aidimag overview to the user, verbatim or lightly formatted for this chat:\n\n${HELP_TEXT}`,
},
},
],
}));
server.prompt("session_start", "Run at the START of a coding session: surfaces in-scope memory, guardrails, stale warnings, and clarifying questions to ask the user before writing any code.", async () => {

@@ -522,2 +652,6 @@ const root = process.env.AIDIMAG_REPO ?? findRepoRoot() ?? process.cwd();

}
text +=
`\n\n---\nAlso mention to the user, in one short line at the end of your briefing summary: ` +
`aidimag is active — memory is searched/captured automatically; they can run the \`help\` prompt ` +
`(or ask "what can aidimag do?") to see all commands, and \`dim review\` in a terminal to approve queued memories.`;
return {

@@ -532,2 +666,5 @@ messages: [

});
server.resource("help", "aidimag://help", async () => ({
contents: [{ uri: "aidimag://help", mimeType: "text/markdown", text: HELP_TEXT }],
}));
server.resource("instructions", "aidimag://instructions", async () => {

@@ -548,3 +685,6 @@ return {

`• Architecture descriptions → ARCHITECTURE\n\n` +
`Capture facts as they're stated, then continue naturally. Don't ask permission.\n`,
`Capture facts as they're stated, then continue naturally. Don't ask permission.\n\n` +
`At session end (or after a knowledge-rich exchange), call chat_harvest once with the user's ` +
`verbatim messages from this session — it bulk-extracts durable facts you may have missed. ` +
`This works from any IDE/agent connected over MCP.\n`,
},

@@ -551,0 +691,0 @@ ],

@@ -18,4 +18,9 @@ /**

import { resolveKnowledgeConfig } from "../config.js";
import { ingestAll } from "../knowledge/ingest.js";
import { ingestAll, knowledgeStatus } from "../knowledge/ingest.js";
import { isAllowedSyncServerUrl } from "../security/url.js";
import { checkDiff } from "../verify/check.js";
import { buildSessionBriefing, renderBriefing } from "../capture/session-briefing.js";
import { bootstrapRepo } from "../capture/bootstrap.js";
import { harvestClaudeSessions } from "../capture/harvest.js";
import { generateContext } from "../context/generate.js";
import { readTicketsConfig, writeTicketsConfig, saveTicketCredential, getTicketCredential, ticketProviderFor, DEFAULT_TICKET_PATTERN, } from "../tickets/provider.js";

@@ -133,2 +138,12 @@ import { PAGE_HTML } from "./page.js";

const tcfg = readTicketsConfig(repoRoot);
let gapCount = 0;
try {
gapCount = store.searchGaps({ sinceDays: 30, limit: 100 }).length;
}
catch { /* pre-migration DB */ }
let scratchCount = 0;
try {
scratchCount = store.scratchpadRead(undefined, 100).length;
}
catch { /* pre-migration DB */ }
json(res, 200, {

@@ -140,2 +155,4 @@ repoRoot,

summary: store.statusSummary(),
gapCount,
scratchCount,
cloud: cloud

@@ -220,2 +237,108 @@ ? { server: cloud.server, brain: cloud.brain, hasToken: !!getToken(cloud.server, repoRoot) }

}
// ---- knowledge gaps (dim gaps) ----
if (req.method === "GET" && path === "/api/gaps") {
const days = Number(url.searchParams.get("days") ?? "30") || 30;
let gaps = [];
try {
gaps = store.searchGaps({ sinceDays: days, limit: 50 });
}
catch { /* pre-migration DB */ }
json(res, 200, { gaps, days });
return;
}
if (req.method === "POST" && path === "/api/gaps/clear") {
json(res, 200, { cleared: store.clearSearchGaps() });
return;
}
// ---- scratchpad (dim scratch) ----
if (req.method === "GET" && path === "/api/scratchpad") {
json(res, 200, { notes: store.scratchpadRead(undefined, 100) });
return;
}
if (req.method === "POST" && path === "/api/scratchpad") {
const b = await readBody(req);
const content = String(b.content ?? "").trim();
if (!content) {
json(res, 400, { error: "content is required" });
return;
}
const ttlHours = Number(b.ttlHours) || 24;
json(res, 201, { note: store.scratchpadWrite(content, { ttlHours, createdBy: "human:dashboard" }) });
return;
}
if (req.method === "POST" && path === "/api/scratchpad/clear") {
json(res, 200, { cleared: store.scratchpadClear() });
return;
}
// ---- provenance audit (dim audit) ----
if (req.method === "GET" && path === "/api/audit") {
json(res, 200, { findings: store.auditMemories({ limit: 50 }) });
return;
}
// ---- session briefing (dim brief) ----
if (req.method === "GET" && path === "/api/brief") {
const b = buildSessionBriefing(store, repoRoot);
json(res, 200, { briefing: b, rendered: renderBriefing(b) });
return;
}
// ---- staged-diff contradiction check (dim check) ----
if (req.method === "POST" && path === "/api/check") {
json(res, 200, checkDiff(store, repoRoot));
return;
}
// ---- proposals gc (dim proposals gc) ----
if (req.method === "POST" && path === "/api/proposals/gc") {
const dryRun = url.searchParams.get("dryRun") === "1";
json(res, 200, { ...store.gcResolvedProposals({ dryRun }), dryRun });
return;
}
// ---- knowledge inbox (dim knowledge sync/status) ----
if (req.method === "GET" && path === "/api/knowledge/status") {
const cfg = resolveKnowledgeConfig(repoRoot);
const s = await knowledgeStatus(repoRoot, cfg);
json(res, 200, {
folder: s.folder,
pending: s.pending.map((d) => d.file),
unsupported: s.unsupported.length,
skipped: s.skippedOnDisk.length,
processed: s.processed.length,
});
return;
}
if (req.method === "POST" && path === "/api/knowledge/sync") {
const cfg = resolveKnowledgeConfig(repoRoot);
const report = await ingestAll(store, repoRoot, cfg);
json(res, 200, {
processed: report.processed.length,
duplicates: report.duplicates.length,
pendingNoSummarizer: report.pendingNoSummarizer.length,
});
return;
}
// ---- bootstrap (dim bootstrap) — long-running, needs an LLM ----
if (req.method === "POST" && path === "/api/bootstrap") {
const force = url.searchParams.get("force") === "1";
const r = await bootstrapRepo(store, repoRoot, { force });
json(res, 200, r);
return;
}
// ---- harvest AI chats (dim harvest) — needs an LLM ----
if (req.method === "POST" && path === "/api/harvest") {
const all = url.searchParams.get("all") === "1";
const r = await harvestClaudeSessions(store, repoRoot, { all });
json(res, 200, r);
return;
}
// ---- generate context files (dim generate-context) ----
if (req.method === "POST" && path === "/api/generate-context") {
const b = await readBody(req);
const format = String(b.format ?? "claude");
if (!["claude", "cursorrules", "copilot", "windsurfrules", "agents", "all"].includes(format)) {
json(res, 400, { error: "invalid format" });
return;
}
const r = generateContext(store, repoRoot, format);
json(res, 200, { files: r.files, total: r.total, pinned: r.pinned });
return;
}
// ---- team sync ----

@@ -222,0 +345,0 @@ if (req.method === "POST" && path === "/api/sync") {

{
"name": "aidimag",
"version": "1.0.19",
"version": "1.0.20",
"description": "Persistent, verified memory for AI coding agents. CLI: dim.",

@@ -16,2 +16,3 @@ "type": "module",

"homepage": "https://aidimag.com",
"mcpName": "io.github.anup-khanal/aidimag",
"bugs": {

@@ -35,2 +36,3 @@ "url": "https://github.com/AiDimag/aidimag/issues"

"!dist/**/*.map",
"server.json",
"LICENSE",

@@ -46,5 +48,8 @@ "README.md"

"mcp": "node dist/mcp/server.js",
"registry:sync": "node scripts/sync-server-json.mjs",
"registry:publish": "npm run registry:sync && npx mcp-publisher publish",
"pretest": "tsc",
"test": "node --test dist/test/*.test.js",
"prepublishOnly": "npm run build && npm test",
"version": "npm run registry:sync && git add server.json",
"prepublishOnly": "npm run build && npm test && npm run registry:sync",
"docs:dev": "vitepress dev docs",

@@ -51,0 +56,0 @@ "docs:build": "vitepress build docs",

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display