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 +18 -0
- package/bin/ai-hist-mcp.js +11 -1
- package/bin/pair-hook.js +135 -0
- package/bin/pair-setup.js +169 -0
- package/package.json +6 -4
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.
|
package/bin/ai-hist-mcp.js
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
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
|
+
}
|
package/bin/pair-hook.js
ADDED
|
@@ -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.
|
|
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": "
|
|
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.
|
|
27
|
+
"ai-hist": "0.3.5"
|
|
26
28
|
},
|
|
27
29
|
"engines": {
|
|
28
30
|
"node": ">=18"
|