@comergehq/claude-plugin 0.1.1
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/.claude-plugin/marketplace.json +23 -0
- package/.claude-plugin/plugin.json +23 -0
- package/.mcp.json +9 -0
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/agents/comerge-collab.md +38 -0
- package/dist/hook-post-collab.d.ts +2 -0
- package/dist/hook-post-collab.js +114 -0
- package/dist/hook-post-collab.js.map +1 -0
- package/dist/hook-pre-git.d.ts +2 -0
- package/dist/hook-pre-git.js +269 -0
- package/dist/hook-pre-git.js.map +1 -0
- package/dist/hook-stop-collab.d.ts +2 -0
- package/dist/hook-stop-collab.js +189 -0
- package/dist/hook-stop-collab.js.map +1 -0
- package/dist/hook-user-prompt.d.ts +2 -0
- package/dist/hook-user-prompt.js +215 -0
- package/dist/hook-user-prompt.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +32 -0
- package/dist/mcp-server.js.map +1 -0
- package/hooks/hooks.json +46 -0
- package/package.json +43 -0
- package/skills/historical-memory-routing/SKILL.md +31 -0
- package/skills/init-or-remix/SKILL.md +34 -0
- package/skills/review-merge-request/SKILL.md +28 -0
- package/skills/safe-collab-workflow/SKILL.md +53 -0
- package/skills/submit-change-step/SKILL.md +37 -0
- package/skills/sync-and-reconcile/SKILL.md +31 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hook-stop-collab.ts
|
|
4
|
+
import { collabAdd, collabRecordTurn } from "@comergehq/core/collab";
|
|
5
|
+
import { readCollabBinding } from "@comergehq/core/binding";
|
|
6
|
+
import { findGitRoot, getWorkspaceDiff } from "@comergehq/core/repo";
|
|
7
|
+
|
|
8
|
+
// src/hook-auth.ts
|
|
9
|
+
import {
|
|
10
|
+
createApiClient,
|
|
11
|
+
createLocalSessionStore,
|
|
12
|
+
createStoredSessionTokenProvider,
|
|
13
|
+
createSupabaseAuthHelpers,
|
|
14
|
+
resolveConfig
|
|
15
|
+
} from "@comergehq/core";
|
|
16
|
+
async function createHookCollabApiClient() {
|
|
17
|
+
const config = await resolveConfig();
|
|
18
|
+
const sessionStore = createLocalSessionStore();
|
|
19
|
+
const tokenProvider = createStoredSessionTokenProvider({
|
|
20
|
+
config,
|
|
21
|
+
sessionStore,
|
|
22
|
+
refreshStoredSession: async ({ config: refreshConfig, session }) => {
|
|
23
|
+
const supabase = createSupabaseAuthHelpers(refreshConfig);
|
|
24
|
+
return supabase.refreshWithStoredSession({ session });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return createApiClient(config, {
|
|
28
|
+
tokenProvider
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/hook-state.ts
|
|
33
|
+
import fs from "fs/promises";
|
|
34
|
+
import os from "os";
|
|
35
|
+
import path from "path";
|
|
36
|
+
import { randomUUID } from "crypto";
|
|
37
|
+
function stateRoot() {
|
|
38
|
+
return path.join(os.tmpdir(), "comerge-claude-plugin-hooks");
|
|
39
|
+
}
|
|
40
|
+
function statePath(sessionId) {
|
|
41
|
+
return path.join(stateRoot(), `${sessionId}.json`);
|
|
42
|
+
}
|
|
43
|
+
async function loadPendingTurnState(sessionId) {
|
|
44
|
+
const raw = await fs.readFile(statePath(sessionId), "utf8").catch(() => null);
|
|
45
|
+
if (!raw) return null;
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
49
|
+
if (typeof parsed.sessionId !== "string" || typeof parsed.turnId !== "string" || typeof parsed.prompt !== "string") {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const intent = parsed.intent;
|
|
53
|
+
return {
|
|
54
|
+
sessionId: parsed.sessionId,
|
|
55
|
+
turnId: parsed.turnId,
|
|
56
|
+
prompt: parsed.prompt,
|
|
57
|
+
cwd: typeof parsed.cwd === "string" && parsed.cwd.trim() ? parsed.cwd : null,
|
|
58
|
+
intent: intent === "memory_first" || intent === "collab_state" || intent === "git_facts" ? intent : "neutral",
|
|
59
|
+
submittedAt: typeof parsed.submittedAt === "string" ? parsed.submittedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
60
|
+
recordedByTool: Boolean(parsed.recordedByTool),
|
|
61
|
+
consultedMemory: Boolean(parsed.consultedMemory)
|
|
62
|
+
};
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function clearPendingTurnState(sessionId) {
|
|
68
|
+
await fs.rm(statePath(sessionId), { force: true }).catch(() => void 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/metadata.ts
|
|
72
|
+
import { createRequire } from "module";
|
|
73
|
+
var require2 = createRequire(import.meta.url);
|
|
74
|
+
var pkg = require2("../package.json");
|
|
75
|
+
var pluginMetadata = {
|
|
76
|
+
name: pkg.name,
|
|
77
|
+
version: pkg.version,
|
|
78
|
+
description: pkg.description,
|
|
79
|
+
pluginId: "comerge",
|
|
80
|
+
agentName: "comerge-collab"
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/hook-utils.ts
|
|
84
|
+
import fs2 from "fs/promises";
|
|
85
|
+
import path2 from "path";
|
|
86
|
+
async function readJsonStdin() {
|
|
87
|
+
const chunks = [];
|
|
88
|
+
for await (const chunk of process.stdin) {
|
|
89
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
90
|
+
}
|
|
91
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
92
|
+
if (!raw) return {};
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
95
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
96
|
+
} catch {
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function extractString(input, keys) {
|
|
101
|
+
for (const key of keys) {
|
|
102
|
+
const value = input[key];
|
|
103
|
+
if (typeof value === "string" && value.trim()) {
|
|
104
|
+
return value.trim();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
function extractBoolean(input, keys) {
|
|
110
|
+
for (const key of keys) {
|
|
111
|
+
const value = input[key];
|
|
112
|
+
if (typeof value === "boolean") {
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/hook-stop-collab.ts
|
|
120
|
+
var HOOK_ACTOR = {
|
|
121
|
+
type: "agent",
|
|
122
|
+
name: "claude-code",
|
|
123
|
+
version: pluginMetadata.version,
|
|
124
|
+
provider: "anthropic"
|
|
125
|
+
};
|
|
126
|
+
async function main() {
|
|
127
|
+
const payload = await readJsonStdin();
|
|
128
|
+
if (extractBoolean(payload, ["stop_hook_active"])) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const sessionId = extractString(payload, ["session_id"]);
|
|
132
|
+
if (!sessionId) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const state = await loadPendingTurnState(sessionId);
|
|
136
|
+
if (!state) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
if (state.recordedByTool) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const prompt = state.prompt.trim();
|
|
144
|
+
const assistantResponse = (extractString(payload, ["last_assistant_message"]) || "").trim();
|
|
145
|
+
if (!prompt || !assistantResponse) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const cwd = extractString(payload, ["cwd"]) ?? state.cwd ?? process.cwd();
|
|
149
|
+
const repoRoot = await findGitRoot(cwd).catch(() => null);
|
|
150
|
+
if (!repoRoot) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const binding = await readCollabBinding(repoRoot).catch(() => null);
|
|
154
|
+
if (!binding) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const api = await createHookCollabApiClient();
|
|
158
|
+
const workspaceDiff = await getWorkspaceDiff(repoRoot);
|
|
159
|
+
if (workspaceDiff.diff.trim()) {
|
|
160
|
+
await collabAdd({
|
|
161
|
+
api,
|
|
162
|
+
cwd: repoRoot,
|
|
163
|
+
prompt,
|
|
164
|
+
assistantResponse,
|
|
165
|
+
diffSource: "worktree",
|
|
166
|
+
idempotencyKey: state.turnId,
|
|
167
|
+
actor: HOOK_ACTOR
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await collabRecordTurn({
|
|
172
|
+
api,
|
|
173
|
+
cwd: repoRoot,
|
|
174
|
+
prompt,
|
|
175
|
+
assistantResponse,
|
|
176
|
+
idempotencyKey: state.turnId,
|
|
177
|
+
actor: HOOK_ACTOR
|
|
178
|
+
});
|
|
179
|
+
} finally {
|
|
180
|
+
await clearPendingTurnState(sessionId);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
main().catch((error) => {
|
|
184
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
185
|
+
process.stderr.write(`${message}
|
|
186
|
+
`);
|
|
187
|
+
process.exitCode = 0;
|
|
188
|
+
});
|
|
189
|
+
//# sourceMappingURL=hook-stop-collab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hook-stop-collab.ts","../src/hook-auth.ts","../src/hook-state.ts","../src/metadata.ts","../src/hook-utils.ts"],"sourcesContent":["import { collabAdd, collabRecordTurn } from \"@comergehq/core/collab\";\nimport { readCollabBinding } from \"@comergehq/core/binding\";\nimport { findGitRoot, getWorkspaceDiff } from \"@comergehq/core/repo\";\n\nimport { createHookCollabApiClient } from \"./hook-auth.js\";\nimport { clearPendingTurnState, loadPendingTurnState } from \"./hook-state.js\";\nimport { pluginMetadata } from \"./metadata.js\";\nimport { extractBoolean, extractString, readJsonStdin } from \"./hook-utils.js\";\n\nconst HOOK_ACTOR = {\n type: \"agent\",\n name: \"claude-code\",\n version: pluginMetadata.version,\n provider: \"anthropic\",\n} as const;\n\nasync function main(): Promise<void> {\n const payload = await readJsonStdin();\n if (extractBoolean(payload, [\"stop_hook_active\"])) {\n return;\n }\n\n const sessionId = extractString(payload, [\"session_id\"]);\n if (!sessionId) {\n return;\n }\n\n const state = await loadPendingTurnState(sessionId);\n if (!state) {\n return;\n }\n\n try {\n if (state.recordedByTool) {\n return;\n }\n\n const prompt = state.prompt.trim();\n const assistantResponse = (extractString(payload, [\"last_assistant_message\"]) || \"\").trim();\n if (!prompt || !assistantResponse) {\n return;\n }\n\n const cwd = extractString(payload, [\"cwd\"]) ?? state.cwd ?? process.cwd();\n const repoRoot = await findGitRoot(cwd).catch(() => null);\n if (!repoRoot) {\n return;\n }\n\n const binding = await readCollabBinding(repoRoot).catch(() => null);\n if (!binding) {\n return;\n }\n\n const api = await createHookCollabApiClient();\n const workspaceDiff = await getWorkspaceDiff(repoRoot);\n if (workspaceDiff.diff.trim()) {\n await collabAdd({\n api,\n cwd: repoRoot,\n prompt,\n assistantResponse,\n diffSource: \"worktree\",\n idempotencyKey: state.turnId,\n actor: HOOK_ACTOR,\n });\n return;\n }\n\n await collabRecordTurn({\n api,\n cwd: repoRoot,\n prompt,\n assistantResponse,\n idempotencyKey: state.turnId,\n actor: HOOK_ACTOR,\n });\n } finally {\n await clearPendingTurnState(sessionId);\n }\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exitCode = 0;\n});\n","import {\n createApiClient,\n createLocalSessionStore,\n createStoredSessionTokenProvider,\n createSupabaseAuthHelpers,\n resolveConfig,\n} from \"@comergehq/core\";\nimport type { CollabApiClient } from \"@comergehq/core/collab\";\n\nexport async function createHookCollabApiClient(): Promise<CollabApiClient> {\n const config = await resolveConfig();\n const sessionStore = createLocalSessionStore();\n const tokenProvider = createStoredSessionTokenProvider({\n config,\n sessionStore,\n refreshStoredSession: async ({ config: refreshConfig, session }) => {\n const supabase = createSupabaseAuthHelpers(refreshConfig);\n return supabase.refreshWithStoredSession({ session });\n },\n });\n\n return createApiClient(config, {\n tokenProvider,\n }) as unknown as CollabApiClient;\n}\n","import fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nimport type { TurnIntent } from \"./history-routing.js\";\n\nexport type PendingTurnState = {\n sessionId: string;\n turnId: string;\n prompt: string;\n cwd: string | null;\n intent: TurnIntent;\n submittedAt: string;\n recordedByTool: boolean;\n consultedMemory: boolean;\n};\n\nfunction stateRoot(): string {\n return path.join(os.tmpdir(), \"comerge-claude-plugin-hooks\");\n}\n\nfunction statePath(sessionId: string): string {\n return path.join(stateRoot(), `${sessionId}.json`);\n}\n\nasync function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;\n await fs.writeFile(tmpPath, JSON.stringify(value, null, 2) + \"\\n\", \"utf8\");\n await fs.rename(tmpPath, filePath);\n}\n\nexport async function loadPendingTurnState(sessionId: string): Promise<PendingTurnState | null> {\n const raw = await fs.readFile(statePath(sessionId), \"utf8\").catch(() => null);\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<PendingTurnState>;\n if (!parsed || typeof parsed !== \"object\") return null;\n if (typeof parsed.sessionId !== \"string\" || typeof parsed.turnId !== \"string\" || typeof parsed.prompt !== \"string\") {\n return null;\n }\n\n const intent = parsed.intent;\n return {\n sessionId: parsed.sessionId,\n turnId: parsed.turnId,\n prompt: parsed.prompt,\n cwd: typeof parsed.cwd === \"string\" && parsed.cwd.trim() ? parsed.cwd : null,\n intent: intent === \"memory_first\" || intent === \"collab_state\" || intent === \"git_facts\" ? intent : \"neutral\",\n submittedAt: typeof parsed.submittedAt === \"string\" ? parsed.submittedAt : new Date().toISOString(),\n recordedByTool: Boolean(parsed.recordedByTool),\n consultedMemory: Boolean(parsed.consultedMemory),\n };\n } catch {\n return null;\n }\n}\n\nexport async function savePendingTurnState(state: PendingTurnState): Promise<void> {\n await writeJsonAtomic(statePath(state.sessionId), state);\n}\n\nexport async function createPendingTurnState(params: {\n sessionId: string;\n prompt: string;\n cwd?: string | null;\n intent: TurnIntent;\n}): Promise<PendingTurnState> {\n const state: PendingTurnState = {\n sessionId: params.sessionId,\n turnId: randomUUID(),\n prompt: params.prompt,\n cwd: params.cwd?.trim() || null,\n intent: params.intent,\n submittedAt: new Date().toISOString(),\n recordedByTool: false,\n consultedMemory: false,\n };\n await savePendingTurnState(state);\n return state;\n}\n\nexport async function markPendingTurnRecordedByTool(sessionId: string): Promise<void> {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing) return;\n existing.recordedByTool = true;\n await savePendingTurnState(existing);\n}\n\nexport async function markPendingTurnConsultedMemory(sessionId: string): Promise<void> {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing || existing.consultedMemory) return;\n existing.consultedMemory = true;\n await savePendingTurnState(existing);\n}\n\nexport async function clearPendingTurnState(sessionId: string): Promise<void> {\n await fs.rm(statePath(sessionId), { force: true }).catch(() => undefined);\n}\n","import { createRequire } from \"node:module\";\n\nconst require = createRequire(import.meta.url);\nconst pkg = require(\"../package.json\") as { name: string; version: string; description: string };\n\nexport const pluginMetadata = {\n name: pkg.name,\n version: pkg.version,\n description: pkg.description,\n pluginId: \"comerge\",\n agentName: \"comerge-collab\",\n};\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport async function readJsonStdin(): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));\n }\n const raw = Buffer.concat(chunks).toString(\"utf8\").trim();\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw);\n return parsed && typeof parsed === \"object\" ? (parsed as Record<string, unknown>) : {};\n } catch {\n return {};\n }\n}\n\nfunction getNestedRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === \"object\" ? (value as Record<string, unknown>) : null;\n}\n\nexport function extractToolInput(payload: Record<string, unknown>): Record<string, unknown> {\n return getNestedRecord(payload.tool_input) ?? getNestedRecord(payload.toolInput) ?? payload;\n}\n\nexport function extractString(input: Record<string, unknown>, keys: string[]): string | null {\n for (const key of keys) {\n const value = input[key];\n if (typeof value === \"string\" && value.trim()) {\n return value.trim();\n }\n }\n return null;\n}\n\nexport function extractBoolean(input: Record<string, unknown>, keys: string[]): boolean | null {\n for (const key of keys) {\n const value = input[key];\n if (typeof value === \"boolean\") {\n return value;\n }\n }\n return null;\n}\n\nexport async function findBoundRepo(startPath: string | null): Promise<string | null> {\n if (!startPath) return null;\n let current = path.resolve(startPath);\n let stats = await fs.stat(current).catch(() => null);\n if (stats?.isFile()) {\n current = path.dirname(current);\n }\n\n while (true) {\n const bindingPath = path.join(current, \".comerge\", \"config.json\");\n const bindingStats = await fs.stat(bindingPath).catch(() => null);\n if (bindingStats?.isFile()) return current;\n const parent = path.dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n}\n"],"mappings":"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","names":["require","fs","path"]}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/history-routing.ts
|
|
4
|
+
var STRONG_MEMORY_FIRST_PATTERNS = [
|
|
5
|
+
/\bwhy\b/i,
|
|
6
|
+
/\breason(?:ing)?\b/i,
|
|
7
|
+
/\brationale\b/i,
|
|
8
|
+
/\bintent\b/i,
|
|
9
|
+
/\bdecision(?: trail)?\b/i,
|
|
10
|
+
/\bhidden assumptions?\b/i,
|
|
11
|
+
/\bwhat led to\b/i,
|
|
12
|
+
/\btrying to solve\b/i,
|
|
13
|
+
/\bearlier prompts?\b/i,
|
|
14
|
+
/\brequirements?\b/i,
|
|
15
|
+
/\btemporary patch\b/i,
|
|
16
|
+
/\bworkaround\b/i,
|
|
17
|
+
/\blong[-\s]?term design\b/i,
|
|
18
|
+
/\bfailed attempts?\b/i,
|
|
19
|
+
/\btried before\b/i,
|
|
20
|
+
/\bprevious attempts?\b/i,
|
|
21
|
+
/\babandon(?:ed)?\b/i,
|
|
22
|
+
/\broll(?:ed)? back\b/i,
|
|
23
|
+
/\bregressions?\b/i,
|
|
24
|
+
/\berrors?\b.*\bkept happening\b/i,
|
|
25
|
+
/\bbefore i (?:touch|change|modify|refactor)\b/i,
|
|
26
|
+
/\bmerge request discussions?\b/i,
|
|
27
|
+
/\brecovery\b/i,
|
|
28
|
+
/\bdrift\b/i,
|
|
29
|
+
/\bcontext did the agent have\b/i,
|
|
30
|
+
/\buser (?:ask|request|approval)\b/i
|
|
31
|
+
];
|
|
32
|
+
var MEMORY_FIRST_PATTERNS = [
|
|
33
|
+
/\brecent changes?\b/i,
|
|
34
|
+
/\bwhat led to\b/i,
|
|
35
|
+
/\bproblem\b/i,
|
|
36
|
+
/\bchange step\b/i,
|
|
37
|
+
/\bhistorical\b/i,
|
|
38
|
+
/\bhistory\b/i,
|
|
39
|
+
...STRONG_MEMORY_FIRST_PATTERNS
|
|
40
|
+
];
|
|
41
|
+
var COLLAB_STATE_PATTERNS = [
|
|
42
|
+
/\bcollab status\b/i,
|
|
43
|
+
/\bsync\b/i,
|
|
44
|
+
/\breconcile\b/i,
|
|
45
|
+
/\bmerge request\b/i,
|
|
46
|
+
/\brequest merge\b/i,
|
|
47
|
+
/\breview\b/i,
|
|
48
|
+
/\bbind(?:ing)?\b/i,
|
|
49
|
+
/\bremix\b/i,
|
|
50
|
+
/\bupstream\b/i
|
|
51
|
+
];
|
|
52
|
+
var GIT_FACT_PATTERNS = [
|
|
53
|
+
/\bgit (?:log|show|diff|blame|rev-list|whatchanged)\b/i,
|
|
54
|
+
/\bcommit hash(?:es)?\b/i,
|
|
55
|
+
/\bexact commits?\b/i,
|
|
56
|
+
/\braw git\b/i,
|
|
57
|
+
/\bgit history\b/i,
|
|
58
|
+
/\bblame this\b/i,
|
|
59
|
+
/\bwho changed (?:this line|this file|that line)\b/i,
|
|
60
|
+
/\bbranch ancestr(?:y|ies)\b/i,
|
|
61
|
+
/\bpatch[-\s]?level\b/i
|
|
62
|
+
];
|
|
63
|
+
function hasMatch(prompt, patterns) {
|
|
64
|
+
return patterns.some((pattern) => pattern.test(prompt));
|
|
65
|
+
}
|
|
66
|
+
function classifyTurnIntent(prompt) {
|
|
67
|
+
const normalizedPrompt = prompt.trim();
|
|
68
|
+
if (!normalizedPrompt) {
|
|
69
|
+
return "neutral";
|
|
70
|
+
}
|
|
71
|
+
const hasStrongMemorySignals = hasMatch(normalizedPrompt, STRONG_MEMORY_FIRST_PATTERNS);
|
|
72
|
+
const hasMemorySignals = hasMatch(normalizedPrompt, MEMORY_FIRST_PATTERNS);
|
|
73
|
+
const hasGitFactSignals = hasMatch(normalizedPrompt, GIT_FACT_PATTERNS);
|
|
74
|
+
if (hasGitFactSignals && !hasStrongMemorySignals) {
|
|
75
|
+
return "git_facts";
|
|
76
|
+
}
|
|
77
|
+
if (hasMemorySignals) {
|
|
78
|
+
return "memory_first";
|
|
79
|
+
}
|
|
80
|
+
if (hasMatch(normalizedPrompt, COLLAB_STATE_PATTERNS)) {
|
|
81
|
+
return "collab_state";
|
|
82
|
+
}
|
|
83
|
+
if (hasMatch(normalizedPrompt, GIT_FACT_PATTERNS)) {
|
|
84
|
+
return "git_facts";
|
|
85
|
+
}
|
|
86
|
+
return "neutral";
|
|
87
|
+
}
|
|
88
|
+
function buildPromptRoutingAdvisory(intent) {
|
|
89
|
+
if (intent === "memory_first") {
|
|
90
|
+
return [
|
|
91
|
+
"Comerge advisory:",
|
|
92
|
+
"This prompt looks like a historical reasoning request in a repo bound to Comerge.",
|
|
93
|
+
"Start with `comerge_collab_memory_summary`, `comerge_collab_memory_search`, or `comerge_collab_memory_timeline` before raw git history. Only fetch `comerge_collab_memory_change_step_diff` after identifying a relevant `changeStepId`."
|
|
94
|
+
].join("\n");
|
|
95
|
+
}
|
|
96
|
+
if (intent === "collab_state") {
|
|
97
|
+
return [
|
|
98
|
+
"Comerge advisory:",
|
|
99
|
+
"This prompt looks like a repo collaboration-state request in a repo bound to Comerge.",
|
|
100
|
+
"Start with `comerge_collab_status`, then follow the recommended sync, reconcile, merge-request, or memory reads from there."
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/hook-state.ts
|
|
107
|
+
import fs from "fs/promises";
|
|
108
|
+
import os from "os";
|
|
109
|
+
import path from "path";
|
|
110
|
+
import { randomUUID } from "crypto";
|
|
111
|
+
function stateRoot() {
|
|
112
|
+
return path.join(os.tmpdir(), "comerge-claude-plugin-hooks");
|
|
113
|
+
}
|
|
114
|
+
function statePath(sessionId) {
|
|
115
|
+
return path.join(stateRoot(), `${sessionId}.json`);
|
|
116
|
+
}
|
|
117
|
+
async function writeJsonAtomic(filePath, value) {
|
|
118
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
119
|
+
const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
120
|
+
await fs.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
121
|
+
await fs.rename(tmpPath, filePath);
|
|
122
|
+
}
|
|
123
|
+
async function savePendingTurnState(state) {
|
|
124
|
+
await writeJsonAtomic(statePath(state.sessionId), state);
|
|
125
|
+
}
|
|
126
|
+
async function createPendingTurnState(params) {
|
|
127
|
+
const state = {
|
|
128
|
+
sessionId: params.sessionId,
|
|
129
|
+
turnId: randomUUID(),
|
|
130
|
+
prompt: params.prompt,
|
|
131
|
+
cwd: params.cwd?.trim() || null,
|
|
132
|
+
intent: params.intent,
|
|
133
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
134
|
+
recordedByTool: false,
|
|
135
|
+
consultedMemory: false
|
|
136
|
+
};
|
|
137
|
+
await savePendingTurnState(state);
|
|
138
|
+
return state;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/hook-utils.ts
|
|
142
|
+
import fs2 from "fs/promises";
|
|
143
|
+
import path2 from "path";
|
|
144
|
+
async function readJsonStdin() {
|
|
145
|
+
const chunks = [];
|
|
146
|
+
for await (const chunk of process.stdin) {
|
|
147
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
148
|
+
}
|
|
149
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
150
|
+
if (!raw) return {};
|
|
151
|
+
try {
|
|
152
|
+
const parsed = JSON.parse(raw);
|
|
153
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
154
|
+
} catch {
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function extractString(input, keys) {
|
|
159
|
+
for (const key of keys) {
|
|
160
|
+
const value = input[key];
|
|
161
|
+
if (typeof value === "string" && value.trim()) {
|
|
162
|
+
return value.trim();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
async function findBoundRepo(startPath) {
|
|
168
|
+
if (!startPath) return null;
|
|
169
|
+
let current = path2.resolve(startPath);
|
|
170
|
+
let stats = await fs2.stat(current).catch(() => null);
|
|
171
|
+
if (stats?.isFile()) {
|
|
172
|
+
current = path2.dirname(current);
|
|
173
|
+
}
|
|
174
|
+
while (true) {
|
|
175
|
+
const bindingPath = path2.join(current, ".comerge", "config.json");
|
|
176
|
+
const bindingStats = await fs2.stat(bindingPath).catch(() => null);
|
|
177
|
+
if (bindingStats?.isFile()) return current;
|
|
178
|
+
const parent = path2.dirname(current);
|
|
179
|
+
if (parent === current) return null;
|
|
180
|
+
current = parent;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/hook-user-prompt.ts
|
|
185
|
+
async function main() {
|
|
186
|
+
const payload = await readJsonStdin();
|
|
187
|
+
const sessionId = extractString(payload, ["session_id"]);
|
|
188
|
+
const prompt = extractString(payload, ["prompt"]);
|
|
189
|
+
if (!sessionId || !prompt) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const cwd = extractString(payload, ["cwd"]);
|
|
193
|
+
const intent = classifyTurnIntent(prompt);
|
|
194
|
+
await createPendingTurnState({
|
|
195
|
+
sessionId,
|
|
196
|
+
prompt,
|
|
197
|
+
cwd,
|
|
198
|
+
intent
|
|
199
|
+
});
|
|
200
|
+
const boundRepo = await findBoundRepo(cwd);
|
|
201
|
+
if (!boundRepo) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const advisory = buildPromptRoutingAdvisory(intent);
|
|
205
|
+
if (advisory) {
|
|
206
|
+
process.stdout.write(advisory);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
main().catch((error) => {
|
|
210
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
211
|
+
process.stderr.write(`${message}
|
|
212
|
+
`);
|
|
213
|
+
process.exitCode = 0;
|
|
214
|
+
});
|
|
215
|
+
//# sourceMappingURL=hook-user-prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/history-routing.ts","../src/hook-state.ts","../src/hook-utils.ts","../src/hook-user-prompt.ts"],"sourcesContent":["export type TurnIntent = \"memory_first\" | \"collab_state\" | \"git_facts\" | \"neutral\";\n\nconst STRONG_MEMORY_FIRST_PATTERNS: RegExp[] = [\n /\\bwhy\\b/i,\n /\\breason(?:ing)?\\b/i,\n /\\brationale\\b/i,\n /\\bintent\\b/i,\n /\\bdecision(?: trail)?\\b/i,\n /\\bhidden assumptions?\\b/i,\n /\\bwhat led to\\b/i,\n /\\btrying to solve\\b/i,\n /\\bearlier prompts?\\b/i,\n /\\brequirements?\\b/i,\n /\\btemporary patch\\b/i,\n /\\bworkaround\\b/i,\n /\\blong[-\\s]?term design\\b/i,\n /\\bfailed attempts?\\b/i,\n /\\btried before\\b/i,\n /\\bprevious attempts?\\b/i,\n /\\babandon(?:ed)?\\b/i,\n /\\broll(?:ed)? back\\b/i,\n /\\bregressions?\\b/i,\n /\\berrors?\\b.*\\bkept happening\\b/i,\n /\\bbefore i (?:touch|change|modify|refactor)\\b/i,\n /\\bmerge request discussions?\\b/i,\n /\\brecovery\\b/i,\n /\\bdrift\\b/i,\n /\\bcontext did the agent have\\b/i,\n /\\buser (?:ask|request|approval)\\b/i,\n];\n\nconst MEMORY_FIRST_PATTERNS: RegExp[] = [\n /\\brecent changes?\\b/i,\n /\\bwhat led to\\b/i,\n /\\bproblem\\b/i,\n /\\bchange step\\b/i,\n /\\bhistorical\\b/i,\n /\\bhistory\\b/i,\n ...STRONG_MEMORY_FIRST_PATTERNS,\n];\n\nconst COLLAB_STATE_PATTERNS: RegExp[] = [\n /\\bcollab status\\b/i,\n /\\bsync\\b/i,\n /\\breconcile\\b/i,\n /\\bmerge request\\b/i,\n /\\brequest merge\\b/i,\n /\\breview\\b/i,\n /\\bbind(?:ing)?\\b/i,\n /\\bremix\\b/i,\n /\\bupstream\\b/i,\n];\n\nconst GIT_FACT_PATTERNS: RegExp[] = [\n /\\bgit (?:log|show|diff|blame|rev-list|whatchanged)\\b/i,\n /\\bcommit hash(?:es)?\\b/i,\n /\\bexact commits?\\b/i,\n /\\braw git\\b/i,\n /\\bgit history\\b/i,\n /\\bblame this\\b/i,\n /\\bwho changed (?:this line|this file|that line)\\b/i,\n /\\bbranch ancestr(?:y|ies)\\b/i,\n /\\bpatch[-\\s]?level\\b/i,\n];\n\nfunction hasMatch(prompt: string, patterns: RegExp[]): boolean {\n return patterns.some((pattern) => pattern.test(prompt));\n}\n\nexport function classifyTurnIntent(prompt: string): TurnIntent {\n const normalizedPrompt = prompt.trim();\n if (!normalizedPrompt) {\n return \"neutral\";\n }\n\n const hasStrongMemorySignals = hasMatch(normalizedPrompt, STRONG_MEMORY_FIRST_PATTERNS);\n const hasMemorySignals = hasMatch(normalizedPrompt, MEMORY_FIRST_PATTERNS);\n const hasGitFactSignals = hasMatch(normalizedPrompt, GIT_FACT_PATTERNS);\n\n if (hasGitFactSignals && !hasStrongMemorySignals) {\n return \"git_facts\";\n }\n\n if (hasMemorySignals) {\n return \"memory_first\";\n }\n\n if (hasMatch(normalizedPrompt, COLLAB_STATE_PATTERNS)) {\n return \"collab_state\";\n }\n\n if (hasMatch(normalizedPrompt, GIT_FACT_PATTERNS)) {\n return \"git_facts\";\n }\n\n return \"neutral\";\n}\n\nexport function shouldPreferComergeMemory(intent: TurnIntent): boolean {\n return intent === \"memory_first\";\n}\n\nexport function buildPromptRoutingAdvisory(intent: TurnIntent): string | null {\n if (intent === \"memory_first\") {\n return [\n \"Comerge advisory:\",\n \"This prompt looks like a historical reasoning request in a repo bound to Comerge.\",\n \"Start with `comerge_collab_memory_summary`, `comerge_collab_memory_search`, or `comerge_collab_memory_timeline` before raw git history. Only fetch `comerge_collab_memory_change_step_diff` after identifying a relevant `changeStepId`.\",\n ].join(\"\\n\");\n }\n\n if (intent === \"collab_state\") {\n return [\n \"Comerge advisory:\",\n \"This prompt looks like a repo collaboration-state request in a repo bound to Comerge.\",\n \"Start with `comerge_collab_status`, then follow the recommended sync, reconcile, merge-request, or memory reads from there.\",\n ].join(\"\\n\");\n }\n\n return null;\n}\n","import fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nimport type { TurnIntent } from \"./history-routing.js\";\n\nexport type PendingTurnState = {\n sessionId: string;\n turnId: string;\n prompt: string;\n cwd: string | null;\n intent: TurnIntent;\n submittedAt: string;\n recordedByTool: boolean;\n consultedMemory: boolean;\n};\n\nfunction stateRoot(): string {\n return path.join(os.tmpdir(), \"comerge-claude-plugin-hooks\");\n}\n\nfunction statePath(sessionId: string): string {\n return path.join(stateRoot(), `${sessionId}.json`);\n}\n\nasync function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;\n await fs.writeFile(tmpPath, JSON.stringify(value, null, 2) + \"\\n\", \"utf8\");\n await fs.rename(tmpPath, filePath);\n}\n\nexport async function loadPendingTurnState(sessionId: string): Promise<PendingTurnState | null> {\n const raw = await fs.readFile(statePath(sessionId), \"utf8\").catch(() => null);\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<PendingTurnState>;\n if (!parsed || typeof parsed !== \"object\") return null;\n if (typeof parsed.sessionId !== \"string\" || typeof parsed.turnId !== \"string\" || typeof parsed.prompt !== \"string\") {\n return null;\n }\n\n const intent = parsed.intent;\n return {\n sessionId: parsed.sessionId,\n turnId: parsed.turnId,\n prompt: parsed.prompt,\n cwd: typeof parsed.cwd === \"string\" && parsed.cwd.trim() ? parsed.cwd : null,\n intent: intent === \"memory_first\" || intent === \"collab_state\" || intent === \"git_facts\" ? intent : \"neutral\",\n submittedAt: typeof parsed.submittedAt === \"string\" ? parsed.submittedAt : new Date().toISOString(),\n recordedByTool: Boolean(parsed.recordedByTool),\n consultedMemory: Boolean(parsed.consultedMemory),\n };\n } catch {\n return null;\n }\n}\n\nexport async function savePendingTurnState(state: PendingTurnState): Promise<void> {\n await writeJsonAtomic(statePath(state.sessionId), state);\n}\n\nexport async function createPendingTurnState(params: {\n sessionId: string;\n prompt: string;\n cwd?: string | null;\n intent: TurnIntent;\n}): Promise<PendingTurnState> {\n const state: PendingTurnState = {\n sessionId: params.sessionId,\n turnId: randomUUID(),\n prompt: params.prompt,\n cwd: params.cwd?.trim() || null,\n intent: params.intent,\n submittedAt: new Date().toISOString(),\n recordedByTool: false,\n consultedMemory: false,\n };\n await savePendingTurnState(state);\n return state;\n}\n\nexport async function markPendingTurnRecordedByTool(sessionId: string): Promise<void> {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing) return;\n existing.recordedByTool = true;\n await savePendingTurnState(existing);\n}\n\nexport async function markPendingTurnConsultedMemory(sessionId: string): Promise<void> {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing || existing.consultedMemory) return;\n existing.consultedMemory = true;\n await savePendingTurnState(existing);\n}\n\nexport async function clearPendingTurnState(sessionId: string): Promise<void> {\n await fs.rm(statePath(sessionId), { force: true }).catch(() => undefined);\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport async function readJsonStdin(): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));\n }\n const raw = Buffer.concat(chunks).toString(\"utf8\").trim();\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw);\n return parsed && typeof parsed === \"object\" ? (parsed as Record<string, unknown>) : {};\n } catch {\n return {};\n }\n}\n\nfunction getNestedRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === \"object\" ? (value as Record<string, unknown>) : null;\n}\n\nexport function extractToolInput(payload: Record<string, unknown>): Record<string, unknown> {\n return getNestedRecord(payload.tool_input) ?? getNestedRecord(payload.toolInput) ?? payload;\n}\n\nexport function extractString(input: Record<string, unknown>, keys: string[]): string | null {\n for (const key of keys) {\n const value = input[key];\n if (typeof value === \"string\" && value.trim()) {\n return value.trim();\n }\n }\n return null;\n}\n\nexport function extractBoolean(input: Record<string, unknown>, keys: string[]): boolean | null {\n for (const key of keys) {\n const value = input[key];\n if (typeof value === \"boolean\") {\n return value;\n }\n }\n return null;\n}\n\nexport async function findBoundRepo(startPath: string | null): Promise<string | null> {\n if (!startPath) return null;\n let current = path.resolve(startPath);\n let stats = await fs.stat(current).catch(() => null);\n if (stats?.isFile()) {\n current = path.dirname(current);\n }\n\n while (true) {\n const bindingPath = path.join(current, \".comerge\", \"config.json\");\n const bindingStats = await fs.stat(bindingPath).catch(() => null);\n if (bindingStats?.isFile()) return current;\n const parent = path.dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n}\n","import { buildPromptRoutingAdvisory, classifyTurnIntent } from \"./history-routing.js\";\nimport { createPendingTurnState } from \"./hook-state.js\";\nimport { extractString, findBoundRepo, readJsonStdin } from \"./hook-utils.js\";\n\nasync function main(): Promise<void> {\n const payload = await readJsonStdin();\n const sessionId = extractString(payload, [\"session_id\"]);\n const prompt = extractString(payload, [\"prompt\"]);\n if (!sessionId || !prompt) {\n return;\n }\n\n const cwd = extractString(payload, [\"cwd\"]);\n const intent = classifyTurnIntent(prompt);\n await createPendingTurnState({\n sessionId,\n prompt,\n cwd,\n intent,\n });\n\n const boundRepo = await findBoundRepo(cwd);\n if (!boundRepo) {\n return;\n }\n\n const advisory = buildPromptRoutingAdvisory(intent);\n if (advisory) {\n process.stdout.write(advisory);\n }\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exitCode = 0;\n});\n"],"mappings":"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","names":["fs","path","path","fs"]}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/metadata.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
var require2 = createRequire(import.meta.url);
|
|
6
|
+
var pkg = require2("../package.json");
|
|
7
|
+
var pluginMetadata = {
|
|
8
|
+
name: pkg.name,
|
|
9
|
+
version: pkg.version,
|
|
10
|
+
description: pkg.description,
|
|
11
|
+
pluginId: "comerge",
|
|
12
|
+
agentName: "comerge-collab"
|
|
13
|
+
};
|
|
14
|
+
export {
|
|
15
|
+
pluginMetadata
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/metadata.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\n\nconst require = createRequire(import.meta.url);\nconst pkg = require(\"../package.json\") as { name: string; version: string; description: string };\n\nexport const pluginMetadata = {\n name: pkg.name,\n version: pkg.version,\n description: pkg.description,\n pluginId: \"comerge\",\n agentName: \"comerge-collab\",\n};\n"],"mappings":";;;AAAA,SAAS,qBAAqB;AAE9B,IAAMA,WAAU,cAAc,YAAY,GAAG;AAC7C,IAAM,MAAMA,SAAQ,iBAAiB;AAE9B,IAAM,iBAAiB;AAAA,EAC5B,MAAM,IAAI;AAAA,EACV,SAAS,IAAI;AAAA,EACb,aAAa,IAAI;AAAA,EACjB,UAAU;AAAA,EACV,WAAW;AACb;","names":["require"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/mcp-server.ts
|
|
4
|
+
import { startStdioServer } from "@comergehq/mcp";
|
|
5
|
+
|
|
6
|
+
// src/metadata.ts
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
var require2 = createRequire(import.meta.url);
|
|
9
|
+
var pkg = require2("../package.json");
|
|
10
|
+
var pluginMetadata = {
|
|
11
|
+
name: pkg.name,
|
|
12
|
+
version: pkg.version,
|
|
13
|
+
description: pkg.description,
|
|
14
|
+
pluginId: "comerge",
|
|
15
|
+
agentName: "comerge-collab"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/mcp-server.ts
|
|
19
|
+
async function main() {
|
|
20
|
+
process.env.COMERGE_AGENT_TYPE ||= "agent";
|
|
21
|
+
process.env.COMERGE_AGENT_NAME ||= "claude-code";
|
|
22
|
+
process.env.COMERGE_AGENT_VERSION ||= pluginMetadata.version;
|
|
23
|
+
process.env.COMERGE_AGENT_PROVIDER ||= "anthropic";
|
|
24
|
+
await startStdioServer({ version: pluginMetadata.version });
|
|
25
|
+
}
|
|
26
|
+
main().catch((error) => {
|
|
27
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
+
process.stderr.write(`${message}
|
|
29
|
+
`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=mcp-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mcp-server.ts","../src/metadata.ts"],"sourcesContent":["import { startStdioServer } from \"@comergehq/mcp\";\n\nimport { pluginMetadata } from \"./metadata.js\";\n\nasync function main(): Promise<void> {\n process.env.COMERGE_AGENT_TYPE ||= \"agent\";\n process.env.COMERGE_AGENT_NAME ||= \"claude-code\";\n process.env.COMERGE_AGENT_VERSION ||= pluginMetadata.version;\n process.env.COMERGE_AGENT_PROVIDER ||= \"anthropic\";\n\n await startStdioServer({ version: pluginMetadata.version });\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exitCode = 1;\n});\n","import { createRequire } from \"node:module\";\n\nconst require = createRequire(import.meta.url);\nconst pkg = require(\"../package.json\") as { name: string; version: string; description: string };\n\nexport const pluginMetadata = {\n name: pkg.name,\n version: pkg.version,\n description: pkg.description,\n pluginId: \"comerge\",\n agentName: \"comerge-collab\",\n};\n"],"mappings":";;;AAAA,SAAS,wBAAwB;;;ACAjC,SAAS,qBAAqB;AAE9B,IAAMA,WAAU,cAAc,YAAY,GAAG;AAC7C,IAAM,MAAMA,SAAQ,iBAAiB;AAE9B,IAAM,iBAAiB;AAAA,EAC5B,MAAM,IAAI;AAAA,EACV,SAAS,IAAI;AAAA,EACb,aAAa,IAAI;AAAA,EACjB,UAAU;AAAA,EACV,WAAW;AACb;;;ADPA,eAAe,OAAsB;AACnC,UAAQ,IAAI,uBAAuB;AACnC,UAAQ,IAAI,uBAAuB;AACnC,UAAQ,IAAI,0BAA0B,eAAe;AACrD,UAAQ,IAAI,2BAA2B;AAEvC,QAAM,iBAAiB,EAAE,SAAS,eAAe,QAAQ,CAAC;AAC5D;AAEA,KAAK,EAAE,MAAM,CAAC,UAAU;AACtB,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,UAAQ,OAAO,MAAM,GAAG,OAAO;AAAA,CAAI;AACnC,UAAQ,WAAW;AACrB,CAAC;","names":["require"]}
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/hook-user-prompt.js\""
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"PreToolUse": [
|
|
14
|
+
{
|
|
15
|
+
"matcher": "Bash",
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/hook-pre-git.js\""
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"PostToolUse": [
|
|
25
|
+
{
|
|
26
|
+
"matcher": "mcp__.*|comerge_collab_.*",
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/hook-post-collab.js\""
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"Stop": [
|
|
36
|
+
{
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/hook-stop-collab.js\""
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@comergehq/claude-plugin",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Claude Code plugin for Comerge collaboration workflows",
|
|
5
|
+
"homepage": "https://github.com/AbdelrahmanRizq97/comerge-claude-plugin",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/AbdelrahmanRizq97/comerge-claude-plugin.git"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
".claude-plugin",
|
|
21
|
+
".mcp.json",
|
|
22
|
+
"skills",
|
|
23
|
+
"hooks",
|
|
24
|
+
"agents"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"postbuild": "node -e \"const fs=require('node:fs'); for (const p of ['dist/mcp-server.js','dist/hook-pre-git.js','dist/hook-user-prompt.js','dist/hook-post-collab.js','dist/hook-stop-collab.js']) fs.chmodSync(p, 0o755);\"",
|
|
29
|
+
"dev": "tsx src/mcp-server.ts",
|
|
30
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
31
|
+
"prepack": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@comergehq/core": "^0.1.1",
|
|
35
|
+
"@comergehq/mcp": "^0.1.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^25.4.0",
|
|
39
|
+
"tsup": "^8.5.1",
|
|
40
|
+
"tsx": "^4.21.0",
|
|
41
|
+
"typescript": "^5.9.3"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: historical-memory-routing
|
|
3
|
+
description: Use when the user asks why something changed, what happened before, which attempts failed, or what historical context matters before modifying code in a Comerge-bound repo.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Historical Memory Routing
|
|
7
|
+
|
|
8
|
+
In a Comerge-bound repo, treat Comerge memory as the first stop for historical reasoning.
|
|
9
|
+
|
|
10
|
+
Use this skill when the user is asking about:
|
|
11
|
+
|
|
12
|
+
- why something changed
|
|
13
|
+
- which prompt, requirement, or discussion led to an implementation
|
|
14
|
+
- failed attempts, abandoned ideas, regressions, or rollbacks
|
|
15
|
+
- hidden assumptions, tradeoffs, or architectural reasoning
|
|
16
|
+
- merge request, reconcile, drift, or recovery backstory
|
|
17
|
+
- what historical context matters before changing a file or subsystem
|
|
18
|
+
|
|
19
|
+
Default order:
|
|
20
|
+
|
|
21
|
+
1. `comerge_collab_memory_summary` for current state and recent context
|
|
22
|
+
2. `comerge_collab_memory_search` for targeted historical lookup
|
|
23
|
+
3. `comerge_collab_memory_timeline` when chronology matters
|
|
24
|
+
4. `comerge_collab_memory_change_step_diff` only after identifying a relevant `changeStepId`
|
|
25
|
+
5. Raw git history only as a fallback for exact commit, blame, ancestry, or patch details
|
|
26
|
+
|
|
27
|
+
Important rules:
|
|
28
|
+
|
|
29
|
+
- Do not start these questions with `git log`, `git show`, `git blame`, or `git diff`.
|
|
30
|
+
- Use bounded memory reads first, then expand to the full stored diff only when the relevant change step is known.
|
|
31
|
+
- If the user explicitly asks for raw git facts, raw git can be used, but Comerge memory is still the better source for intent and reasoning.
|