ai-hist-mcp 0.3.2 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,24 @@ Thin `npx` wrapper for the [`ai-hist`](https://www.npmjs.com/package/ai-hist) st
6
6
  npx -y ai-hist-mcp
7
7
  ```
8
8
 
9
+ ## Pair one-command setup
10
+
11
+ Install the project MCP config and advisory Pair hooks for Claude Code / Codex:
12
+
13
+ ```bash
14
+ npx -y ai-hist-mcp setup
15
+ ```
16
+
17
+ The setup is idempotent and secret-free. It writes `.mcp.json`, `.claude/settings.json`,
18
+ and `.codex/hooks.json` entries that call the package wrappers; it never embeds internal
19
+ auth keys or relayhistory tokens.
20
+
21
+ For the hook command alone:
22
+
23
+ ```bash
24
+ npx -y ai-hist-mcp hook
25
+ ```
26
+
9
27
  The wrapper depends on `ai-hist` and launches its `ai-hist/mcp-server` export. It preserves the same environment contract:
10
28
 
11
29
  - `AI_HIST_DB` points to the ai-hist SQLite database.
@@ -1,2 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import 'ai-hist/mcp-server';
2
+ const subcommand = process.argv[2];
3
+
4
+ if (subcommand === "setup" || subcommand === "pair-setup" || subcommand === "install") {
5
+ process.argv.splice(2, 1);
6
+ await import("./pair-setup.js");
7
+ } else if (subcommand === "hook" || subcommand === "pair-hook") {
8
+ process.argv.splice(2, 1);
9
+ await import("./pair-hook.js");
10
+ } else {
11
+ await import("ai-hist/mcp-server");
12
+ }
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
3
+ import { isAbsolute, relative } from "node:path";
4
+ import { promisify } from "node:util";
5
+
6
+ const MAX_TASK_CHARS = 800;
7
+ const DEFAULT_LIMIT = 3;
8
+ const TIMEOUT_MS = Number(process.env.PAIR_CHECK_TIMEOUT_MS || 8000);
9
+ const execFileAsync = promisify(execFile);
10
+ const SECRET_PATTERNS = [
11
+ /\brth_[a-z]+_[A-Za-z0-9._-]+/g,
12
+ /\bsk-[A-Za-z0-9._-]+/g,
13
+ /\bgh[pousr]_[A-Za-z0-9_]{20,}/g,
14
+ /\bAKIA[0-9A-Z]{16}\b/g,
15
+ /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
16
+ ];
17
+
18
+ async function readStdin() {
19
+ const chunks = [];
20
+ for await (const chunk of process.stdin) chunks.push(chunk);
21
+ return Buffer.concat(chunks).toString("utf8");
22
+ }
23
+
24
+ function compact(value, limit = MAX_TASK_CHARS) {
25
+ if (typeof value !== "string") return undefined;
26
+ let text = value.replace(/\s+/g, " ").trim();
27
+ for (const pattern of SECRET_PATTERNS) text = text.replace(pattern, "[REDACTED]");
28
+ if (!text) return undefined;
29
+ return text.length > limit ? `${text.slice(0, limit)}...` : text;
30
+ }
31
+
32
+ function normalizeFile(cwd, file) {
33
+ if (typeof file !== "string" || !file.trim()) return undefined;
34
+ if (!cwd || !isAbsolute(file)) return file;
35
+ return relative(cwd, file) || file;
36
+ }
37
+
38
+ function collectFiles(cwd, input) {
39
+ const files = new Set();
40
+ if (!input || typeof input !== "object") return [];
41
+ for (const key of ["file_path", "filePath", "path", "target"]) {
42
+ const normalized = normalizeFile(cwd, input[key]);
43
+ if (normalized) files.add(normalized);
44
+ }
45
+ return [...files].slice(0, 20);
46
+ }
47
+
48
+ function buildContext(event) {
49
+ const toolInput = event.tool_input && typeof event.tool_input === "object" ? event.tool_input : {};
50
+ const cwd = typeof event.cwd === "string" ? event.cwd : process.cwd();
51
+ const files = collectFiles(cwd, toolInput);
52
+ const tool = event.tool_name || event.tool || undefined;
53
+ const target = files[0] || compact(toolInput.command, 300) || compact(toolInput.pattern, 300);
54
+ const prompt = compact(event.prompt);
55
+ const description = compact(toolInput.description, 300);
56
+ const command = compact(toolInput.command, 500);
57
+ const action = event.hook_event_name === "UserPromptSubmit" ? "prompt" : "tool";
58
+
59
+ return {
60
+ cwd,
61
+ repoPath: cwd,
62
+ task: prompt || description || command,
63
+ files,
64
+ tool,
65
+ target,
66
+ action,
67
+ recentPrompt: prompt,
68
+ };
69
+ }
70
+
71
+ function pushArg(args, name, value) {
72
+ if (typeof value === "string" && value.trim()) args.push(name, value.trim());
73
+ }
74
+
75
+ async function pairCheck(context) {
76
+ const bin = process.env.AI_HIST_PAIR_CHECK_BIN || "ai-hist";
77
+ const args = ["pair", "check", "--json"];
78
+ pushArg(args, "--task", context.task);
79
+ pushArg(args, "--tool", context.tool);
80
+ pushArg(args, "--target", context.target);
81
+ pushArg(args, "--recent-prompt", context.recentPrompt);
82
+ for (const file of context.files ?? []) pushArg(args, "--file", file);
83
+ args.push("--limit", String(process.env.PAIR_CHECK_LIMIT || DEFAULT_LIMIT));
84
+
85
+ const { stdout } = await execFileAsync(bin, args, {
86
+ timeout: TIMEOUT_MS,
87
+ maxBuffer: 1024 * 1024,
88
+ env: process.env,
89
+ });
90
+ const parsed = stdout.trim() ? JSON.parse(stdout) : {};
91
+ return Array.isArray(parsed.warnings) ? { ...parsed, warnings: parsed.warnings } : { ...parsed, warnings: [] };
92
+ }
93
+
94
+ function formatWarnings(result) {
95
+ if (!result.warnings?.length) return "";
96
+ const lines = ["Pair advisory warnings from prior convergence events:"];
97
+ for (const warning of result.warnings) {
98
+ const label = [warning.kind, warning.lens].filter(Boolean).join("/");
99
+ const score = typeof warning.score === "number" ? ` score=${warning.score.toFixed(2)}` : "";
100
+ lines.push(`- ${label ? `[${label}${score}] ` : ""}${warning.text}`);
101
+ for (const ev of warning.evidence ?? []) {
102
+ const id = [ev.machineId, ev.source, ev.sessionId, ev.eventId].filter(Boolean).join(":");
103
+ const when = ev.ts ? ` ${ev.ts}` : "";
104
+ const snippet = ev.snippet ? ` - ${ev.snippet}` : "";
105
+ lines.push(` evidence: ${id || ev.eventId || "(unknown)"}${when}${snippet}`);
106
+ }
107
+ }
108
+ if (result.correlationId) lines.push(`correlationId: ${result.correlationId}`);
109
+ return lines.join("\n");
110
+ }
111
+
112
+ function outputFor(eventName, additionalContext) {
113
+ return {
114
+ hookSpecificOutput: {
115
+ hookEventName: eventName,
116
+ additionalContext,
117
+ },
118
+ };
119
+ }
120
+
121
+ try {
122
+ const input = await readStdin();
123
+ const event = input.trim() ? JSON.parse(input) : {};
124
+ const eventName = event.hook_event_name || "UserPromptSubmit";
125
+ const result = await pairCheck(buildContext(event));
126
+ const warningText = formatWarnings(result);
127
+ if (warningText) {
128
+ process.stdout.write(`${JSON.stringify(outputFor(eventName, warningText))}\n`);
129
+ }
130
+ } catch (err) {
131
+ if (process.env.PAIR_CHECK_DEBUG) {
132
+ process.stderr.write(`pair hook skipped: ${err instanceof Error ? err.message : String(err)}\n`);
133
+ }
134
+ process.exit(0);
135
+ }
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+
5
+ const DEFAULT_AGENTS = "all";
6
+ const MCP_SERVER_NAME = "ai-hist";
7
+
8
+ function usage() {
9
+ return `Usage: npx -y ai-hist-mcp setup [options]
10
+
11
+ Install Pair MCP + advisory hooks for the current project.
12
+
13
+ Options:
14
+ --agents <all|claude|codex> Which agent configs to update (default: all)
15
+ --project <path> Project path passed to ai-hist-mcp (default: cwd)
16
+ --ai-hist-bin <path> ai-hist binary used by the Pair hook (default: ai-hist)
17
+ --mcp-only Only write .mcp.json, skip hooks
18
+ --hooks-only Only write hook configs, skip .mcp.json
19
+ --dry-run Print planned writes without changing files
20
+ -h, --help Show this help
21
+ `;
22
+ }
23
+
24
+ function parseArgs(argv) {
25
+ const out = {
26
+ agents: DEFAULT_AGENTS,
27
+ project: process.cwd(),
28
+ aiHistBin: "ai-hist",
29
+ hooks: true,
30
+ mcp: true,
31
+ dryRun: false,
32
+ };
33
+
34
+ for (let i = 0; i < argv.length; i++) {
35
+ const arg = argv[i];
36
+ if (arg === "-h" || arg === "--help") {
37
+ process.stdout.write(usage());
38
+ process.exit(0);
39
+ }
40
+ if (arg === "--agents") out.agents = requireValue(argv, ++i, arg);
41
+ else if (arg === "--project") out.project = requireValue(argv, ++i, arg);
42
+ else if (arg === "--ai-hist-bin") out.aiHistBin = requireValue(argv, ++i, arg);
43
+ else if (arg === "--mcp-only") out.hooks = false;
44
+ else if (arg === "--hooks-only") out.mcp = false;
45
+ else if (arg === "--dry-run") out.dryRun = true;
46
+ else throw new Error(`unknown option: ${arg}`);
47
+ }
48
+
49
+ if (!["all", "claude", "codex"].includes(out.agents)) {
50
+ throw new Error("--agents must be one of: all, claude, codex");
51
+ }
52
+ return out;
53
+ }
54
+
55
+ function requireValue(argv, index, flag) {
56
+ const value = argv[index];
57
+ if (!value || value.startsWith("-")) throw new Error(`${flag} requires a value`);
58
+ return value;
59
+ }
60
+
61
+ async function readJson(path, fallback) {
62
+ try {
63
+ return JSON.parse(await readFile(path, "utf8"));
64
+ } catch (err) {
65
+ if (err?.code === "ENOENT") return fallback;
66
+ throw err;
67
+ }
68
+ }
69
+
70
+ async function writeJson(path, value, dryRun) {
71
+ const body = `${JSON.stringify(value, null, 2)}\n`;
72
+ if (dryRun) {
73
+ process.stdout.write(`[dry-run] would write ${path}\n`);
74
+ return;
75
+ }
76
+ await mkdir(dirname(path), { recursive: true });
77
+ await writeFile(path, body, { mode: 0o600 });
78
+ process.stdout.write(`wrote ${path}\n`);
79
+ }
80
+
81
+ function shellQuote(value) {
82
+ if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) return value;
83
+ return `'${value.replace(/'/g, "'\\''")}'`;
84
+ }
85
+
86
+ function hookCommand(aiHistBin) {
87
+ const command = "npx -y ai-hist-mcp hook";
88
+ if (!aiHistBin || aiHistBin === "ai-hist") return command;
89
+ return `AI_HIST_PAIR_CHECK_BIN=${shellQuote(aiHistBin)} ${command}`;
90
+ }
91
+
92
+ function hook(command, statusMessage) {
93
+ return {
94
+ type: "command",
95
+ command,
96
+ timeout: 10,
97
+ ...(statusMessage ? { statusMessage } : {}),
98
+ };
99
+ }
100
+
101
+ function hasHook(hooks, command) {
102
+ return Array.isArray(hooks) && hooks.some((entry) =>
103
+ Array.isArray(entry?.hooks) && entry.hooks.some((candidate) => candidate?.command === command),
104
+ );
105
+ }
106
+
107
+ function ensureHook(settings, eventName, entry) {
108
+ settings.hooks ??= {};
109
+ settings.hooks[eventName] ??= [];
110
+ const command = entry.hooks?.[0]?.command;
111
+ if (!command || hasHook(settings.hooks[eventName], command)) return;
112
+ settings.hooks[eventName].push(entry);
113
+ }
114
+
115
+ async function installMcp(project, dryRun) {
116
+ const path = resolve(process.cwd(), ".mcp.json");
117
+ const config = await readJson(path, {});
118
+ config.mcpServers ??= {};
119
+ config.mcpServers[MCP_SERVER_NAME] = {
120
+ command: "npx",
121
+ args: ["-y", "ai-hist-mcp", "--project", project],
122
+ };
123
+ await writeJson(path, config, dryRun);
124
+ }
125
+
126
+ async function installClaudeHooks(command, dryRun) {
127
+ const path = resolve(process.cwd(), ".claude", "settings.json");
128
+ const settings = await readJson(path, {});
129
+ ensureHook(settings, "UserPromptSubmit", {
130
+ hooks: [hook(command)],
131
+ });
132
+ ensureHook(settings, "PreToolUse", {
133
+ matcher: "Edit|Write|Bash",
134
+ hooks: [hook(command)],
135
+ });
136
+ await writeJson(path, settings, dryRun);
137
+ }
138
+
139
+ async function installCodexHooks(command, dryRun) {
140
+ const path = resolve(process.cwd(), ".codex", "hooks.json");
141
+ const settings = await readJson(path, {});
142
+ ensureHook(settings, "UserPromptSubmit", {
143
+ hooks: [hook(command, "Checking Pair warnings")],
144
+ });
145
+ ensureHook(settings, "PreToolUse", {
146
+ matcher: "Edit|Write|apply_patch|Bash",
147
+ hooks: [hook(command, "Checking Pair warnings")],
148
+ });
149
+ await writeJson(path, settings, dryRun);
150
+ }
151
+
152
+ try {
153
+ const args = parseArgs(process.argv.slice(2));
154
+ const project = resolve(args.project);
155
+ const command = hookCommand(args.aiHistBin);
156
+
157
+ if (args.mcp) await installMcp(project, args.dryRun);
158
+ if (args.hooks && (args.agents === "all" || args.agents === "claude")) {
159
+ await installClaudeHooks(command, args.dryRun);
160
+ }
161
+ if (args.hooks && (args.agents === "all" || args.agents === "codex")) {
162
+ await installCodexHooks(command, args.dryRun);
163
+ }
164
+
165
+ process.stdout.write("Pair setup complete. Restart your agent session so it reloads MCP/hooks.\n");
166
+ } catch (err) {
167
+ process.stderr.write(`ai-hist pair setup failed: ${err instanceof Error ? err.message : String(err)}\n`);
168
+ process.exit(1);
169
+ }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "ai-hist-mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.5",
4
4
  "description": "Thin npx wrapper for the ai-hist stdio MCP server.",
5
5
  "license": "MIT",
6
6
  "private": false,
7
7
  "type": "module",
8
8
  "bin": {
9
- "ai-hist-mcp": "bin/ai-hist-mcp.js"
9
+ "ai-hist-mcp": "bin/ai-hist-mcp.js",
10
+ "ai-hist-pair-setup": "bin/pair-setup.js",
11
+ "ai-hist-pair-hook": "bin/pair-hook.js"
10
12
  },
11
13
  "files": [
12
14
  "bin",
@@ -15,14 +17,14 @@
15
17
  ],
16
18
  "repository": {
17
19
  "type": "git",
18
- "url": "git+https://github.com/AgentWorkforce/ai-hist.git",
20
+ "url": "https://github.com/AgentWorkforce/relayhistory",
19
21
  "directory": "mcp-package"
20
22
  },
21
23
  "publishConfig": {
22
24
  "access": "public"
23
25
  },
24
26
  "dependencies": {
25
- "ai-hist": "0.3.2"
27
+ "ai-hist": "0.3.5"
26
28
  },
27
29
  "engines": {
28
30
  "node": ">=18"