@remixhq/claude-plugin 0.1.15 → 0.1.16

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.
@@ -296,7 +296,7 @@ async function createPendingTurnState(params) {
296
296
  // package.json
297
297
  var package_default = {
298
298
  name: "@remixhq/claude-plugin",
299
- version: "0.1.15",
299
+ version: "0.1.16",
300
300
  description: "Claude Code plugin for Remix collaboration workflows",
301
301
  homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
302
302
  license: "MIT",
@@ -327,8 +327,8 @@ var package_default = {
327
327
  prepack: "npm run build"
328
328
  },
329
329
  dependencies: {
330
- "@remixhq/core": "^0.1.10",
331
- "@remixhq/mcp": "^0.1.10"
330
+ "@remixhq/core": "^0.1.11",
331
+ "@remixhq/mcp": "^0.1.11"
332
332
  },
333
333
  devDependencies: {
334
334
  "@types/node": "^25.4.0",
@@ -484,6 +484,7 @@ function buildBoundRepoFinalRecordingReminder() {
484
484
  "Remix bound-repo reminder:",
485
485
  "This turn is in a Remix-bound repo. You must record exactly one final Remix turn before your final user-facing response.",
486
486
  "In this repo, Remix MCP tools are the required workflow layer for ordinary collaboration work. Use `remix_collab_finalize_turn`. It chooses changed-turn vs no-diff automatically from the live worktree, and it can accept an explicit external diff when needed.",
487
+ "Branch reminder: switching git branches can change the valid Remix lane binding for this checkout. Run `remix_collab_status` after branch switches and before mutation commands. If the branch is unbound or mismatched, resolve that first.",
487
488
  "Do not finish the response before recording."
488
489
  ].join("\n");
489
490
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hook-user-prompt.ts","../src/history-routing.ts","../src/hook-diagnostics.ts","../src/hook-state.ts","../package.json","../src/metadata.ts","../src/hook-utils.ts"],"sourcesContent":["import { buildPromptRoutingAdvisory, classifyTurnIntent } from \"./history-routing.js\";\nimport { appendHookDiagnosticsEvent, summarizeText } from \"./hook-diagnostics.js\";\nimport { createPendingTurnState } from \"./hook-state.js\";\nimport { extractString, findBoundRepo, readJsonStdin } from \"./hook-utils.js\";\n\nfunction buildGlobalFinalRecordingReminder(): string {\n return [\n \"Remix recording rule:\",\n \"If you work in any Remix-bound repo during this turn, you must record exactly one final Remix turn before your final user-facing response.\",\n \"Use `remix_collab_finalize_turn`. Include the exact user prompt and your final assistant response. If no Remix-bound repo is involved in this turn, ignore this reminder.\",\n ].join(\"\\n\");\n}\n\nfunction buildBoundRepoFinalRecordingReminder(): string {\n return [\n \"Remix bound-repo reminder:\",\n \"This turn is in a Remix-bound repo. You must record exactly one final Remix turn before your final user-facing response.\",\n \"In this repo, Remix MCP tools are the required workflow layer for ordinary collaboration work. Use `remix_collab_finalize_turn`. It chooses changed-turn vs no-diff automatically from the live worktree, and it can accept an explicit external diff when needed.\",\n \"Do not finish the response before recording.\",\n ].join(\"\\n\");\n}\n\nfunction combineAdvisories(...messages: Array<string | null>): string | null {\n const parts = messages.map((message) => message?.trim()).filter((message): message is string => Boolean(message));\n if (parts.length === 0) return null;\n return parts.join(\"\\n\\n\");\n}\n\nexport async function runHookUserPrompt(payload: Record<string, unknown>): Promise<void> {\n const sessionId = extractString(payload, [\"session_id\"]);\n const prompt = extractString(payload, [\"prompt\"]);\n const cwd = extractString(payload, [\"cwd\"]);\n\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n stage: \"payload_received\",\n result: \"start\",\n fields: {\n hasSessionId: Boolean(sessionId),\n hasPrompt: Boolean(prompt),\n hasCwd: Boolean(cwd),\n },\n });\n\n if (!sessionId) {\n const reminder = buildGlobalFinalRecordingReminder();\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n stage: \"payload_validation\",\n result: \"info\",\n reason: \"missing_session_id\",\n fields: {\n hasPrompt: Boolean(prompt),\n advisoryLength: reminder.length,\n },\n });\n process.stdout.write(reminder);\n return;\n }\n\n if (!prompt) {\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n stage: \"payload_validation\",\n result: \"skip\",\n reason: \"missing_prompt\",\n fields: {\n hasCwd: Boolean(cwd),\n },\n });\n return;\n }\n\n const promptSummary = summarizeText(prompt);\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n stage: \"payload_parsed\",\n result: \"info\",\n fields: {\n hasCwd: Boolean(cwd),\n promptLength: promptSummary.length,\n promptHash: promptSummary.sha256Prefix,\n },\n });\n\n const intent = classifyTurnIntent(prompt);\n const state = await createPendingTurnState({\n sessionId,\n prompt,\n initialCwd: cwd,\n intent,\n });\n\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n turnId: state.turnId,\n stage: \"state_created\",\n result: \"success\",\n fields: {\n intent,\n hasInitialCwd: Boolean(cwd),\n },\n });\n\n const boundRepo = await findBoundRepo(cwd);\n if (!boundRepo) {\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n turnId: state.turnId,\n stage: \"bound_repo_lookup\",\n result: \"skip\",\n reason: \"repo_not_bound\",\n fields: {\n hasCwd: Boolean(cwd),\n },\n });\n return;\n }\n\n const advisory = boundRepo ? buildPromptRoutingAdvisory(intent) : null;\n const combinedAdvisory = combineAdvisories(\n buildGlobalFinalRecordingReminder(),\n advisory,\n boundRepo ? buildBoundRepoFinalRecordingReminder() : null,\n );\n if (combinedAdvisory) {\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n turnId: state.turnId,\n stage: \"advisory_emitted\",\n result: \"info\",\n repoRoot: boundRepo,\n fields: {\n advisoryLength: combinedAdvisory.length,\n intent,\n },\n });\n process.stdout.write(combinedAdvisory);\n return;\n }\n\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n turnId: state.turnId,\n stage: \"completed\",\n result: \"success\",\n repoRoot: boundRepo,\n fields: {\n intent,\n },\n });\n}\n\nasync function main(): Promise<void> {\n const payload = await readJsonStdin();\n await runHookUserPrompt(payload);\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n void appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n stage: \"unhandled_error\",\n result: \"error\",\n reason: \"exception\",\n message,\n });\n process.stderr.write(`${message}\\n`);\n process.exitCode = 0;\n});\n","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 shouldPreferRemixMemory(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 \"Remix advisory:\",\n \"This prompt looks like a historical reasoning request in a repo bound to Remix.\",\n \"Start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline` before raw git history. Only fetch `remix_collab_memory_change_step_diff` after identifying a relevant `changeStepId`.\",\n ].join(\"\\n\");\n }\n\n if (intent === \"collab_state\") {\n return [\n \"Remix advisory:\",\n \"This prompt looks like a repo collaboration-state request in a repo bound to Remix.\",\n \"Start with `remix_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 { createHash } from \"node:crypto\";\nimport fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nimport { getPendingTurnStateRootPath, listPendingTurnStateSummaries, type PendingTurnStateSummary } from \"./hook-state.js\";\nimport { pluginMetadata } from \"./metadata.js\";\n\ntype HookDiagnosticsResult = \"start\" | \"info\" | \"skip\" | \"success\" | \"error\";\ntype HookDiagnosticsFieldValue = string | number | boolean | null;\n\nexport type HookDiagnosticsEvent = {\n ts: string;\n hook: string;\n pluginVersion: string;\n pid: number;\n sessionId: string | null;\n turnId: string | null;\n stage: string;\n result: HookDiagnosticsResult;\n reason: string | null;\n toolName: string | null;\n repoRoot: string | null;\n message: string | null;\n fields: Record<string, HookDiagnosticsFieldValue>;\n};\n\nexport type HookDiagnosticsReport = {\n logPath: string;\n stateRoot: string;\n recentEvents: HookDiagnosticsEvent[];\n pendingStates: PendingTurnStateSummary[];\n};\n\nconst DEFAULT_EVENT_LIMIT = 50;\nconst MAX_EVENT_LIMIT = 200;\nconst MAX_LOG_BYTES = 512 * 1024;\n\nfunction resolveClaudeRoot(): string {\n const configured = process.env.CLAUDE_CONFIG_DIR?.trim();\n return configured || path.join(os.homedir(), \".claude\");\n}\n\nfunction resolvePluginDataDirName(): string {\n return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;\n}\n\nexport function getHookDiagnosticsDirPath(): string {\n const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();\n return configured || path.join(resolveClaudeRoot(), \"plugins\", \"data\", resolvePluginDataDirName());\n}\n\nexport function getHookDiagnosticsLogPath(): string {\n return path.join(getHookDiagnosticsDirPath(), \"hooks.ndjson\");\n}\n\nfunction clampEventLimit(limit?: number): number {\n if (typeof limit !== \"number\" || !Number.isFinite(limit)) return DEFAULT_EVENT_LIMIT;\n return Math.max(1, Math.min(MAX_EVENT_LIMIT, Math.trunc(limit)));\n}\n\nfunction toFieldValue(value: unknown): HookDiagnosticsFieldValue | undefined {\n if (value === null) return null;\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"boolean\") return value;\n return undefined;\n}\n\nfunction normalizeFields(fields?: Record<string, unknown>): Record<string, HookDiagnosticsFieldValue> {\n if (!fields) return {};\n const normalizedEntries = Object.entries(fields)\n .map(([key, value]) => {\n const normalized = toFieldValue(value);\n return normalized === undefined ? null : ([key, normalized] as const);\n })\n .filter((entry): entry is readonly [string, HookDiagnosticsFieldValue] => entry !== null);\n return Object.fromEntries(normalizedEntries);\n}\n\nasync function rotateLogIfNeeded(logPath: string): Promise<void> {\n const stat = await fs.stat(logPath).catch(() => null);\n if (!stat || stat.size < MAX_LOG_BYTES) {\n return;\n }\n\n const rotatedPath = `${logPath}.1`;\n await fs.rm(rotatedPath, { force: true }).catch(() => undefined);\n await fs.rename(logPath, rotatedPath).catch(() => undefined);\n}\n\nexport function summarizeText(value: string | null | undefined): {\n present: boolean;\n length: number;\n sha256Prefix: string | null;\n} {\n if (typeof value !== \"string\" || !value.trim()) {\n return {\n present: false,\n length: 0,\n sha256Prefix: null,\n };\n }\n\n const trimmed = value.trim();\n return {\n present: true,\n length: trimmed.length,\n sha256Prefix: createHash(\"sha256\").update(trimmed).digest(\"hex\").slice(0, 12),\n };\n}\n\nexport async function appendHookDiagnosticsEvent(params: {\n hook: string;\n sessionId?: string | null;\n turnId?: string | null;\n stage: string;\n result: HookDiagnosticsResult;\n reason?: string | null;\n toolName?: string | null;\n repoRoot?: string | null;\n message?: string | null;\n fields?: Record<string, unknown>;\n}): Promise<void> {\n try {\n const logPath = getHookDiagnosticsLogPath();\n await fs.mkdir(path.dirname(logPath), { recursive: true });\n await rotateLogIfNeeded(logPath);\n const event: HookDiagnosticsEvent = {\n ts: new Date().toISOString(),\n hook: params.hook,\n pluginVersion: pluginMetadata.version,\n pid: process.pid,\n sessionId: params.sessionId?.trim() || null,\n turnId: params.turnId?.trim() || null,\n stage: params.stage.trim(),\n result: params.result,\n reason: params.reason?.trim() || null,\n toolName: params.toolName?.trim() || null,\n repoRoot: params.repoRoot?.trim() || null,\n message: params.message?.trim() || null,\n fields: normalizeFields(params.fields),\n };\n await fs.appendFile(logPath, `${JSON.stringify(event)}\\n`, \"utf8\");\n } catch {\n // Diagnostics are best-effort and must never break hook execution.\n }\n}\n\nasync function readEventsFromFile(filePath: string): Promise<HookDiagnosticsEvent[]> {\n const raw = await fs.readFile(filePath, \"utf8\").catch(() => null);\n if (!raw) return [];\n return raw\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter(Boolean)\n .flatMap((line) => {\n try {\n const parsed = JSON.parse(line) as HookDiagnosticsEvent;\n return parsed && typeof parsed === \"object\" ? [parsed] : [];\n } catch {\n return [];\n }\n });\n}\n\nexport async function readRecentHookDiagnosticsEvents(limit?: number): Promise<HookDiagnosticsEvent[]> {\n const eventLimit = clampEventLimit(limit);\n const logPath = getHookDiagnosticsLogPath();\n const [rotated, current] = await Promise.all([readEventsFromFile(`${logPath}.1`), readEventsFromFile(logPath)]);\n return [...rotated, ...current].slice(-eventLimit);\n}\n\nexport async function readHookDiagnosticsReport(limit?: number): Promise<HookDiagnosticsReport> {\n const [recentEvents, pendingStates] = await Promise.all([\n readRecentHookDiagnosticsEvents(limit),\n listPendingTurnStateSummaries(),\n ]);\n\n return {\n logPath: getHookDiagnosticsLogPath(),\n stateRoot: getPendingTurnStateRootPath(),\n recentEvents,\n pendingStates,\n };\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 RepoRecordMode = \"changed_turn\" | \"no_diff_turn\";\nexport type ManualRecordingScope = \"change_step\" | \"full_turn\";\n\nexport type TouchedRepoState = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n firstTouchedAt: string;\n lastTouchedAt: string;\n lastObservedWriteAt: string | null;\n touchedBy: string[];\n hasObservedWrite: boolean;\n manuallyRecorded: boolean;\n manuallyRecordedAt: string | null;\n manuallyRecordedByTool: string | null;\n manualRecordingScope: ManualRecordingScope | null;\n manualRemoteChangeRecordedAt: string | null;\n stopAttempted: boolean;\n stopRecorded: boolean;\n stopRecordedAt: string | null;\n stopRecordedMode: RepoRecordMode | null;\n recordingFailureMessage: string | null;\n recordingFailureHint: string | null;\n recordingFailedAt: string | null;\n};\n\nexport type PendingTurnState = {\n sessionId: string;\n turnId: string;\n prompt: string;\n initialCwd: string | null;\n intent: TurnIntent;\n submittedAt: string;\n consultedMemory: boolean;\n touchedRepos: Record<string, TouchedRepoState>;\n turnFailureMessage: string | null;\n turnFailureHint: string | null;\n turnFailedAt: string | null;\n};\n\nexport type PendingTouchedRepoSummary = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n lastTouchedAt: string;\n lastObservedWriteAt: string | null;\n touchedBy: string[];\n hasObservedWrite: boolean;\n manuallyRecorded: boolean;\n manualRecordingScope: ManualRecordingScope | null;\n stopAttempted: boolean;\n stopRecorded: boolean;\n stopRecordedMode: RepoRecordMode | null;\n recordingFailureMessage: string | null;\n recordingFailureHint: string | null;\n recordingFailedAt: string | null;\n};\n\nexport type PendingTurnStateSummary = {\n sessionId: string;\n turnId: string;\n initialCwd: string | null;\n submittedAt: string;\n consultedMemory: boolean;\n promptLength: number;\n touchedRepoCount: number;\n turnFailureMessage: string | null;\n turnFailureHint: string | null;\n turnFailedAt: string | null;\n touchedRepos: PendingTouchedRepoSummary[];\n};\n\nfunction stateRoot(): string {\n const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();\n return configured || path.join(os.tmpdir(), \"remix-claude-plugin-hooks\");\n}\n\nfunction statePath(sessionId: string): string {\n return path.join(stateRoot(), `${sessionId}.json`);\n}\n\nfunction stateLockPath(sessionId: string): string {\n return path.join(stateRoot(), `${sessionId}.lock`);\n}\n\nfunction stateLockMetaPath(sessionId: string): string {\n return path.join(stateLockPath(sessionId), \"owner.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\nconst STATE_LOCK_WAIT_MS = 2_000;\nconst STATE_LOCK_POLL_MS = 25;\nconst STATE_LOCK_STALE_MS = 30_000;\nconst STATE_LOCK_HEARTBEAT_MS = 5_000;\n\ntype StateLockMetadata = {\n ownerId: string;\n pid: number;\n createdAt: string;\n heartbeatAt: string;\n};\n\nasync function sleep(ms: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function readStateLockMetadata(sessionId: string): Promise<StateLockMetadata | null> {\n const raw = await fs.readFile(stateLockMetaPath(sessionId), \"utf8\").catch(() => null);\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<StateLockMetadata>;\n if (\n typeof parsed.ownerId !== \"string\" ||\n typeof parsed.pid !== \"number\" ||\n typeof parsed.createdAt !== \"string\" ||\n typeof parsed.heartbeatAt !== \"string\"\n ) {\n return null;\n }\n return {\n ownerId: parsed.ownerId,\n pid: parsed.pid,\n createdAt: parsed.createdAt,\n heartbeatAt: parsed.heartbeatAt,\n };\n } catch {\n return null;\n }\n}\n\nasync function writeStateLockMetadata(sessionId: string, metadata: StateLockMetadata): Promise<void> {\n await writeJsonAtomic(stateLockMetaPath(sessionId), metadata);\n}\n\nasync function tryRemoveStaleStateLock(sessionId: string): Promise<boolean> {\n const lockPath = stateLockPath(sessionId);\n const metadata = await readStateLockMetadata(sessionId);\n const staleByHeartbeat =\n metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;\n if (staleByHeartbeat) {\n await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n return true;\n }\n\n if (!metadata) {\n const lockStat = await fs.stat(lockPath).catch(() => null);\n if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {\n await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n return true;\n }\n }\n\n return false;\n}\n\nasync function acquireStateLock(sessionId: string): Promise<() => Promise<void>> {\n const lockPath = stateLockPath(sessionId);\n const deadline = Date.now() + STATE_LOCK_WAIT_MS;\n await fs.mkdir(stateRoot(), { recursive: true });\n\n while (true) {\n try {\n await fs.mkdir(lockPath);\n const ownerId = randomUUID();\n const createdAt = new Date().toISOString();\n const metadata: StateLockMetadata = {\n ownerId,\n pid: process.pid,\n createdAt,\n heartbeatAt: createdAt,\n };\n await writeStateLockMetadata(sessionId, metadata);\n let released = false;\n const heartbeat = setInterval(() => {\n if (released) return;\n void writeStateLockMetadata(sessionId, {\n ...metadata,\n heartbeatAt: new Date().toISOString(),\n }).catch(() => undefined);\n }, STATE_LOCK_HEARTBEAT_MS);\n heartbeat.unref?.();\n\n return async () => {\n if (released) return;\n released = true;\n clearInterval(heartbeat);\n const currentMetadata = await readStateLockMetadata(sessionId);\n if (currentMetadata?.ownerId === ownerId) {\n await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n }\n };\n } catch (error) {\n const code = error && typeof error === \"object\" && \"code\" in error ? (error as { code?: unknown }).code : null;\n if (code !== \"EEXIST\") {\n throw error;\n }\n\n if (await tryRemoveStaleStateLock(sessionId)) {\n continue;\n }\n\n if (Date.now() >= deadline) {\n throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);\n }\n await sleep(STATE_LOCK_POLL_MS);\n }\n }\n}\n\nasync function withStateLock<T>(sessionId: string, fn: () => Promise<T>): Promise<T> {\n const release = await acquireStateLock(sessionId);\n try {\n return await fn();\n } finally {\n await release();\n }\n}\n\nfunction normalizeIntent(value: unknown): TurnIntent {\n return value === \"memory_first\" || value === \"collab_state\" || value === \"git_facts\" ? value : \"neutral\";\n}\n\nfunction normalizeString(value: unknown): string | null {\n return typeof value === \"string\" && value.trim() ? value.trim() : null;\n}\n\nfunction normalizeStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return Array.from(\n new Set(\n value\n .filter((entry): entry is string => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => entry.trim()),\n ),\n );\n}\n\nfunction normalizeTouchedRepo(value: unknown, repoRoot: string): TouchedRepoState | null {\n if (!value || typeof value !== \"object\") return null;\n const parsed = value as Partial<TouchedRepoState>;\n const normalizedRepoRoot = normalizeString(parsed.repoRoot) ?? repoRoot.trim();\n if (!normalizedRepoRoot) return null;\n\n return {\n repoRoot: normalizedRepoRoot,\n projectId: normalizeString(parsed.projectId),\n currentAppId: normalizeString(parsed.currentAppId),\n upstreamAppId: normalizeString(parsed.upstreamAppId),\n firstTouchedAt: normalizeString(parsed.firstTouchedAt) ?? new Date().toISOString(),\n lastTouchedAt: normalizeString(parsed.lastTouchedAt) ?? new Date().toISOString(),\n lastObservedWriteAt: normalizeString(parsed.lastObservedWriteAt),\n touchedBy: normalizeStringArray(parsed.touchedBy),\n hasObservedWrite: Boolean(parsed.hasObservedWrite),\n manuallyRecorded: Boolean(parsed.manuallyRecorded),\n manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),\n manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),\n manualRecordingScope:\n parsed.manualRecordingScope === \"change_step\" || parsed.manualRecordingScope === \"full_turn\"\n ? parsed.manualRecordingScope\n : null,\n manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),\n stopAttempted: Boolean(parsed.stopAttempted),\n stopRecorded: Boolean(parsed.stopRecorded),\n stopRecordedAt: normalizeString(parsed.stopRecordedAt),\n stopRecordedMode: parsed.stopRecordedMode === \"changed_turn\" || parsed.stopRecordedMode === \"no_diff_turn\" ? parsed.stopRecordedMode : null,\n recordingFailureMessage: normalizeString(parsed.recordingFailureMessage),\n recordingFailureHint: normalizeString(parsed.recordingFailureHint),\n recordingFailedAt: normalizeString(parsed.recordingFailedAt),\n };\n}\n\nfunction normalizeTouchedRepos(value: unknown): Record<string, TouchedRepoState> {\n if (!value || typeof value !== \"object\") return {};\n const entries = Object.entries(value as Record<string, unknown>)\n .map(([repoRoot, repo]) => normalizeTouchedRepo(repo, repoRoot))\n .filter((repo): repo is TouchedRepoState => repo !== null)\n .sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));\n return Object.fromEntries(entries.map((repo) => [repo.repoRoot, repo]));\n}\n\nfunction createTouchedRepo(params: {\n repoRoot: string;\n projectId?: string | null;\n currentAppId?: string | null;\n upstreamAppId?: string | null;\n touchedBy?: string | null;\n hasObservedWrite?: boolean;\n}): TouchedRepoState {\n const now = new Date().toISOString();\n const touchedBy = params.touchedBy?.trim() ? [params.touchedBy.trim()] : [];\n return {\n repoRoot: params.repoRoot,\n projectId: normalizeString(params.projectId),\n currentAppId: normalizeString(params.currentAppId),\n upstreamAppId: normalizeString(params.upstreamAppId),\n firstTouchedAt: now,\n lastTouchedAt: now,\n lastObservedWriteAt: params.hasObservedWrite ? now : null,\n touchedBy,\n hasObservedWrite: Boolean(params.hasObservedWrite),\n manuallyRecorded: false,\n manuallyRecordedAt: null,\n manuallyRecordedByTool: null,\n manualRecordingScope: null,\n manualRemoteChangeRecordedAt: null,\n stopAttempted: false,\n stopRecorded: false,\n stopRecordedAt: null,\n stopRecordedMode: null,\n recordingFailureMessage: null,\n recordingFailureHint: null,\n recordingFailedAt: null,\n };\n}\n\nasync function updatePendingTurnState(\n sessionId: string,\n updater: (state: PendingTurnState) => void | boolean,\n): Promise<PendingTurnState | null> {\n return withStateLock(sessionId, async () => {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing) return null;\n const result = updater(existing);\n if (result === false) return existing;\n await savePendingTurnState(existing);\n return existing;\n });\n}\n\nfunction summarizeTouchedRepo(repo: TouchedRepoState): PendingTouchedRepoSummary {\n return {\n repoRoot: repo.repoRoot,\n projectId: repo.projectId,\n currentAppId: repo.currentAppId,\n upstreamAppId: repo.upstreamAppId,\n lastTouchedAt: repo.lastTouchedAt,\n lastObservedWriteAt: repo.lastObservedWriteAt,\n touchedBy: [...repo.touchedBy],\n hasObservedWrite: repo.hasObservedWrite,\n manuallyRecorded: repo.manuallyRecorded,\n manualRecordingScope: repo.manualRecordingScope,\n stopAttempted: repo.stopAttempted,\n stopRecorded: repo.stopRecorded,\n stopRecordedMode: repo.stopRecordedMode,\n recordingFailureMessage: repo.recordingFailureMessage,\n recordingFailureHint: repo.recordingFailureHint,\n recordingFailedAt: repo.recordingFailedAt,\n };\n}\n\nfunction summarizePendingTurnState(state: PendingTurnState): PendingTurnStateSummary {\n const touchedRepos = Object.values(state.touchedRepos)\n .sort((a, b) => a.repoRoot.localeCompare(b.repoRoot))\n .map((repo) => summarizeTouchedRepo(repo));\n return {\n sessionId: state.sessionId,\n turnId: state.turnId,\n initialCwd: state.initialCwd,\n submittedAt: state.submittedAt,\n consultedMemory: state.consultedMemory,\n promptLength: state.prompt.length,\n touchedRepoCount: touchedRepos.length,\n turnFailureMessage: state.turnFailureMessage,\n turnFailureHint: state.turnFailureHint,\n turnFailedAt: state.turnFailedAt,\n touchedRepos,\n };\n}\n\nexport function getPendingTurnStateRootPath(): string {\n return stateRoot();\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 return {\n sessionId: parsed.sessionId,\n turnId: parsed.turnId,\n prompt: parsed.prompt,\n initialCwd: normalizeString(parsed.initialCwd),\n intent: normalizeIntent(parsed.intent),\n submittedAt: typeof parsed.submittedAt === \"string\" ? parsed.submittedAt : new Date().toISOString(),\n consultedMemory: Boolean(parsed.consultedMemory),\n touchedRepos: normalizeTouchedRepos(parsed.touchedRepos),\n turnFailureMessage: normalizeString(parsed.turnFailureMessage),\n turnFailureHint: normalizeString(parsed.turnFailureHint),\n turnFailedAt: normalizeString(parsed.turnFailedAt),\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 initialCwd?: string | null;\n intent: TurnIntent;\n}): Promise<PendingTurnState> {\n return withStateLock(params.sessionId, async () => {\n const state: PendingTurnState = {\n sessionId: params.sessionId,\n turnId: randomUUID(),\n prompt: params.prompt,\n initialCwd: params.initialCwd?.trim() || null,\n intent: params.intent,\n submittedAt: new Date().toISOString(),\n consultedMemory: false,\n touchedRepos: {},\n turnFailureMessage: null,\n turnFailureHint: null,\n turnFailedAt: null,\n };\n await savePendingTurnState(state);\n return state;\n });\n}\n\nexport async function upsertTouchedRepo(\n sessionId: string,\n params: {\n repoRoot: string;\n projectId?: string | null;\n currentAppId?: string | null;\n upstreamAppId?: string | null;\n touchedBy?: string | null;\n hasObservedWrite?: boolean;\n },\n): Promise<TouchedRepoState | null> {\n const normalizedRepoRoot = params.repoRoot.trim();\n if (!normalizedRepoRoot) return null;\n const state = await updatePendingTurnState(sessionId, (existing) => {\n const current =\n existing.touchedRepos[normalizedRepoRoot] ??\n createTouchedRepo({\n repoRoot: normalizedRepoRoot,\n projectId: params.projectId,\n currentAppId: params.currentAppId,\n upstreamAppId: params.upstreamAppId,\n touchedBy: params.touchedBy,\n hasObservedWrite: params.hasObservedWrite,\n });\n\n current.projectId = normalizeString(params.projectId) ?? current.projectId;\n current.currentAppId = normalizeString(params.currentAppId) ?? current.currentAppId;\n current.upstreamAppId = normalizeString(params.upstreamAppId) ?? current.upstreamAppId;\n current.lastTouchedAt = new Date().toISOString();\n if (params.touchedBy?.trim() && !current.touchedBy.includes(params.touchedBy.trim())) {\n current.touchedBy = [...current.touchedBy, params.touchedBy.trim()].sort((a, b) => a.localeCompare(b));\n }\n if (params.hasObservedWrite) {\n current.hasObservedWrite = true;\n current.lastObservedWriteAt = new Date().toISOString();\n }\n existing.touchedRepos[normalizedRepoRoot] = current;\n });\n return state?.touchedRepos[normalizedRepoRoot] ?? null;\n}\n\nexport async function markTouchedRepoObservedWrite(\n sessionId: string,\n repoRoot: string,\n params?: { toolName?: string | null },\n): Promise<void> {\n await upsertTouchedRepo(sessionId, {\n repoRoot,\n touchedBy: params?.toolName ?? null,\n hasObservedWrite: true,\n });\n}\n\nexport async function markTouchedRepoManuallyRecorded(\n sessionId: string,\n repoRoot: string,\n params?: { toolName?: string | null; scope?: ManualRecordingScope | null; remoteChangeRecorded?: boolean },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const normalizedRepoRoot = repoRoot.trim();\n if (!normalizedRepoRoot) return false;\n const current =\n existing.touchedRepos[normalizedRepoRoot] ??\n createTouchedRepo({\n repoRoot: normalizedRepoRoot,\n touchedBy: params?.toolName ?? null,\n hasObservedWrite: false,\n });\n current.lastTouchedAt = new Date().toISOString();\n current.manuallyRecorded = true;\n current.manuallyRecordedAt = new Date().toISOString();\n current.manuallyRecordedByTool = normalizeString(params?.toolName) ?? current.manuallyRecordedByTool;\n current.manualRecordingScope = params?.scope ?? current.manualRecordingScope;\n if (params?.remoteChangeRecorded) {\n current.manualRemoteChangeRecordedAt = current.manuallyRecordedAt;\n }\n current.recordingFailureMessage = null;\n current.recordingFailureHint = null;\n current.recordingFailedAt = null;\n if (params?.toolName?.trim() && !current.touchedBy.includes(params.toolName.trim())) {\n current.touchedBy = [...current.touchedBy, params.toolName.trim()].sort((a, b) => a.localeCompare(b));\n }\n existing.touchedRepos[normalizedRepoRoot] = current;\n });\n}\n\nexport async function markPendingTurnConsultedMemory(sessionId: string): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n if (existing.consultedMemory) return false;\n existing.consultedMemory = true;\n });\n}\n\nexport async function markTouchedRepoStopAttempted(sessionId: string, repoRoot: string): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const current = existing.touchedRepos[repoRoot];\n if (!current) return false;\n current.stopAttempted = true;\n current.lastTouchedAt = new Date().toISOString();\n });\n}\n\nexport async function markTouchedRepoStopRecorded(\n sessionId: string,\n repoRoot: string,\n params: {\n mode: RepoRecordMode;\n },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const current = existing.touchedRepos[repoRoot];\n if (!current) return false;\n current.stopAttempted = true;\n current.stopRecorded = true;\n current.stopRecordedAt = new Date().toISOString();\n current.stopRecordedMode = params.mode;\n current.recordingFailureMessage = null;\n current.recordingFailureHint = null;\n current.recordingFailedAt = null;\n current.lastTouchedAt = new Date().toISOString();\n });\n}\n\nexport async function markTouchedRepoRecordingFailure(\n sessionId: string,\n repoRoot: string,\n params: {\n message: string;\n hint?: string | null;\n },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const current = existing.touchedRepos[repoRoot];\n if (!current) return false;\n current.stopAttempted = true;\n current.recordingFailureMessage = params.message.trim();\n current.recordingFailureHint = params.hint?.trim() || null;\n current.recordingFailedAt = new Date().toISOString();\n current.lastTouchedAt = new Date().toISOString();\n });\n}\n\nexport async function markPendingTurnFailure(\n sessionId: string,\n params: {\n message: string;\n hint?: string | null;\n },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n existing.turnFailureMessage = params.message.trim();\n existing.turnFailureHint = params.hint?.trim() || null;\n existing.turnFailedAt = new Date().toISOString();\n });\n}\n\nexport async function listTouchedRepos(sessionId: string): Promise<TouchedRepoState[]> {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing) return [];\n return Object.values(existing.touchedRepos).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));\n}\n\nexport async function listPendingTurnStateSummaries(): Promise<PendingTurnStateSummary[]> {\n const root = stateRoot();\n const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);\n const sessionIds = entries\n .filter((entry) => entry.isFile() && entry.name.endsWith(\".json\"))\n .map((entry) => entry.name.replace(/\\.json$/, \"\"))\n .sort((a, b) => a.localeCompare(b));\n const states = await Promise.all(sessionIds.map((sessionId) => loadPendingTurnState(sessionId)));\n return states\n .filter((state): state is PendingTurnState => state !== null)\n .sort((a, b) => b.submittedAt.localeCompare(a.submittedAt))\n .map((state) => summarizePendingTurnState(state));\n}\n\nexport async function clearPendingTurnState(sessionId: string): Promise<void> {\n await withStateLock(sessionId, async () => {\n await fs.rm(statePath(sessionId), { force: true }).catch(() => undefined);\n });\n}\n","{\n \"name\": \"@remixhq/claude-plugin\",\n \"version\": \"0.1.15\",\n \"description\": \"Claude Code plugin for Remix collaboration workflows\",\n \"homepage\": \"https://github.com/RemixDotOne/remix-claude-plugin\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/RemixDotOne/remix-claude-plugin.git\"\n },\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"files\": [\n \"dist\",\n \".claude-plugin/plugin.json\",\n \".mcp.json\",\n \"skills\",\n \"hooks\",\n \"agents\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"postbuild\": \"node -e \\\"const fs=require('node:fs'); for (const p of ['dist/mcp-server.cjs','dist/hook-pre-git.cjs','dist/hook-user-prompt.cjs','dist/hook-post-collab.cjs','dist/hook-stop-collab.cjs']) fs.chmodSync(p, 0o755);\\\"\",\n \"dev\": \"tsx src/mcp-server.ts\",\n \"typecheck\": \"tsc -p tsconfig.json --noEmit\",\n \"prepack\": \"npm run build\"\n },\n \"dependencies\": {\n \"@remixhq/core\": \"^0.1.10\",\n \"@remixhq/mcp\": \"^0.1.10\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^25.4.0\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.9.3\"\n }\n}\n","import pkg from \"../package.json\";\n\nexport const pluginMetadata = {\n name: pkg.name,\n version: pkg.version,\n description: pkg.description,\n pluginId: \"remix\",\n agentName: \"remix-collab\",\n};\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { readCollabBinding } from \"@remixhq/core/binding\";\n\ntype BindingSummary = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n};\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 extractToolResponse(payload: Record<string, unknown>): Record<string, unknown> | null {\n return getNestedRecord(payload.tool_response) ?? getNestedRecord(payload.toolResponse);\n}\n\nfunction extractToolStructuredData(payload: Record<string, unknown>): Record<string, unknown> | null {\n const toolResponse = extractToolResponse(payload);\n const structuredContent = getNestedRecord(toolResponse?.structuredContent) ?? getNestedRecord(payload.structuredContent);\n return (\n getNestedRecord(toolResponse?.data) ??\n getNestedRecord(structuredContent?.data) ??\n structuredContent\n );\n}\n\nexport function extractToolName(payload: Record<string, unknown>): string | null {\n return extractString(payload, [\"tool_name\", \"toolName\"]);\n}\n\nexport function normalizeHookToolName(toolName: string | null): string | null {\n if (!toolName) return null;\n const trimmed = toolName.trim();\n if (!trimmed) return null;\n\n const remixToolIndex = trimmed.toLowerCase().indexOf(\"remix_collab_\");\n if (remixToolIndex >= 0) {\n return trimmed.slice(remixToolIndex);\n }\n\n return trimmed;\n}\n\nexport function extractAssistantResponse(payload: Record<string, unknown>): string | null {\n const candidateKeys = [\n \"last_assistant_message\",\n \"lastAssistantMessage\",\n \"assistant_response\",\n \"assistantResponse\",\n \"assistant_message\",\n \"assistantMessage\",\n \"response\",\n \"message\",\n ];\n\n return (\n extractString(payload, candidateKeys) ??\n extractString(extractToolResponse(payload) ?? {}, candidateKeys) ??\n extractString(extractToolStructuredData(payload) ?? {}, candidateKeys) ??\n extractString(extractToolInput(payload), candidateKeys)\n );\n}\n\nexport function extractFinalizeTurnMode(payload: Record<string, unknown>): \"changed_turn\" | \"no_diff_turn\" | null {\n const mode =\n extractString(extractToolStructuredData(payload) ?? {}, [\"mode\"]) ??\n extractString(extractToolResponse(payload) ?? {}, [\"mode\"]) ??\n extractString(payload, [\"mode\"]);\n if (mode === \"changed_turn\" || mode === \"no_diff_turn\") {\n return mode;\n }\n return null;\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 function extractToolCwd(payload: Record<string, unknown>): string | null {\n const toolInput = extractToolInput(payload);\n return extractString(toolInput, [\"cwd\"]) ?? extractString(payload, [\"cwd\"]);\n}\n\nexport function extractBashCommand(payload: Record<string, unknown>): string | null {\n const toolInput = extractToolInput(payload);\n return extractString(toolInput, [\"command\", \"cmd\", \"bash_command\", \"bashCommand\"]);\n}\n\nexport function extractToolErrorMessage(payload: Record<string, unknown>): string | null {\n const toolResponse = extractToolResponse(payload);\n const toolError = getNestedRecord(toolResponse?.error) ?? getNestedRecord(payload.error);\n return (\n extractString(toolError ?? {}, [\"message\"]) ??\n extractString(toolResponse ?? {}, [\"errorMessage\", \"message\"]) ??\n extractString(payload, [\"errorMessage\"])\n );\n}\n\nfunction extractToolStatus(payload: Record<string, unknown>): string | null {\n const toolResponse = extractToolResponse(payload);\n return (\n extractString(toolResponse ?? {}, [\"status\", \"state\"]) ??\n extractString(payload, [\"status\", \"state\"])\n );\n}\n\nexport function didToolSucceed(payload: Record<string, unknown>): boolean {\n const toolResponse = extractToolResponse(payload);\n const explicitSuccess = toolResponse ? extractBoolean(toolResponse, [\"success\", \"ok\"]) : null;\n if (explicitSuccess !== null) {\n return explicitSuccess;\n }\n\n if (extractToolErrorMessage(payload)) {\n return false;\n }\n\n const status = extractToolStatus(payload)?.toLowerCase();\n if (status === \"error\" || status === \"failed\" || status === \"failure\") {\n return false;\n }\n\n const hookEventName = extractString(payload, [\"hook_event_name\", \"hookEventName\"]);\n return hookEventName === \"PostToolUse\";\n}\n\nexport function isRemoteChangeRecordedButLocalSyncFailed(payload: Record<string, unknown>): boolean {\n return extractToolErrorMessage(payload) === \"Change step succeeded remotely, but automatic local sync failed.\";\n}\n\nfunction collectStringPathValue(value: unknown): string[] {\n if (typeof value === \"string\" && value.trim()) return [value.trim()];\n if (Array.isArray(value)) {\n return value.flatMap((entry) => collectStringPathValue(entry));\n }\n return [];\n}\n\nfunction collectPathTargetsFromObject(input: Record<string, unknown>, keys: string[]): string[] {\n return keys.flatMap((key) => collectStringPathValue(input[key]));\n}\n\nfunction resolveCandidatePath(targetPath: string, baseDir: string): string {\n return path.isAbsolute(targetPath) ? path.normalize(targetPath) : path.resolve(baseDir, targetPath);\n}\n\nexport function extractToolPathTargets(payload: Record<string, unknown>, toolName?: string | null): string[] {\n const name = (toolName ?? extractToolName(payload) ?? \"\").trim().toLowerCase();\n const toolInput = extractToolInput(payload);\n const baseDir = extractToolCwd(payload) ?? process.cwd();\n const baseKeys = [\"path\", \"paths\", \"file_path\", \"filePath\", \"target_file\", \"targetFile\", \"filename\"];\n\n const targets =\n name === \"notebookedit\"\n ? collectPathTargetsFromObject(toolInput, [\"target_notebook\", \"notebook_path\", \"notebookPath\", ...baseKeys])\n : collectPathTargetsFromObject(toolInput, baseKeys);\n\n return Array.from(new Set(targets.map((entry) => resolveCandidatePath(entry, baseDir))));\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, \".remix\", \"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\nexport async function resolveBoundRepoSummary(startPath: string | null): Promise<BindingSummary | null> {\n const repoRoot = await findBoundRepo(startPath);\n if (!repoRoot) return null;\n const binding = await readCollabBinding(repoRoot).catch(() => null);\n if (!binding) return null;\n return {\n repoRoot,\n projectId: binding.projectId,\n currentAppId: binding.currentAppId,\n upstreamAppId: binding.upstreamAppId,\n };\n}\n\nexport async function resolveTouchedBoundReposFromPaths(paths: string[]): Promise<BindingSummary[]> {\n const resolved = await Promise.all(paths.map((targetPath) => resolveBoundRepoSummary(targetPath)));\n const unique = new Map<string, BindingSummary>();\n for (const repo of resolved) {\n if (!repo) continue;\n unique.set(repo.repoRoot, repo);\n }\n return Array.from(unique.values()).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));\n}\n\nexport async function resolveBoundRepoFromToolCwd(payload: Record<string, unknown>): Promise<BindingSummary | null> {\n return resolveBoundRepoSummary(extractToolCwd(payload));\n}\n"],"mappings":"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","names":["import_node_crypto","import_promises","import_node_os","import_node_path","path","os","fs","path","os","fs","path","import_promises","import_node_path","path","fs"]}
1
+ {"version":3,"sources":["../src/hook-user-prompt.ts","../src/history-routing.ts","../src/hook-diagnostics.ts","../src/hook-state.ts","../package.json","../src/metadata.ts","../src/hook-utils.ts"],"sourcesContent":["import { buildPromptRoutingAdvisory, classifyTurnIntent } from \"./history-routing.js\";\nimport { appendHookDiagnosticsEvent, summarizeText } from \"./hook-diagnostics.js\";\nimport { createPendingTurnState } from \"./hook-state.js\";\nimport { extractString, findBoundRepo, readJsonStdin } from \"./hook-utils.js\";\n\nfunction buildGlobalFinalRecordingReminder(): string {\n return [\n \"Remix recording rule:\",\n \"If you work in any Remix-bound repo during this turn, you must record exactly one final Remix turn before your final user-facing response.\",\n \"Use `remix_collab_finalize_turn`. Include the exact user prompt and your final assistant response. If no Remix-bound repo is involved in this turn, ignore this reminder.\",\n ].join(\"\\n\");\n}\n\nfunction buildBoundRepoFinalRecordingReminder(): string {\n return [\n \"Remix bound-repo reminder:\",\n \"This turn is in a Remix-bound repo. You must record exactly one final Remix turn before your final user-facing response.\",\n \"In this repo, Remix MCP tools are the required workflow layer for ordinary collaboration work. Use `remix_collab_finalize_turn`. It chooses changed-turn vs no-diff automatically from the live worktree, and it can accept an explicit external diff when needed.\",\n \"Branch reminder: switching git branches can change the valid Remix lane binding for this checkout. Run `remix_collab_status` after branch switches and before mutation commands. If the branch is unbound or mismatched, resolve that first.\",\n \"Do not finish the response before recording.\",\n ].join(\"\\n\");\n}\n\nfunction combineAdvisories(...messages: Array<string | null>): string | null {\n const parts = messages.map((message) => message?.trim()).filter((message): message is string => Boolean(message));\n if (parts.length === 0) return null;\n return parts.join(\"\\n\\n\");\n}\n\nexport async function runHookUserPrompt(payload: Record<string, unknown>): Promise<void> {\n const sessionId = extractString(payload, [\"session_id\"]);\n const prompt = extractString(payload, [\"prompt\"]);\n const cwd = extractString(payload, [\"cwd\"]);\n\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n stage: \"payload_received\",\n result: \"start\",\n fields: {\n hasSessionId: Boolean(sessionId),\n hasPrompt: Boolean(prompt),\n hasCwd: Boolean(cwd),\n },\n });\n\n if (!sessionId) {\n const reminder = buildGlobalFinalRecordingReminder();\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n stage: \"payload_validation\",\n result: \"info\",\n reason: \"missing_session_id\",\n fields: {\n hasPrompt: Boolean(prompt),\n advisoryLength: reminder.length,\n },\n });\n process.stdout.write(reminder);\n return;\n }\n\n if (!prompt) {\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n stage: \"payload_validation\",\n result: \"skip\",\n reason: \"missing_prompt\",\n fields: {\n hasCwd: Boolean(cwd),\n },\n });\n return;\n }\n\n const promptSummary = summarizeText(prompt);\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n stage: \"payload_parsed\",\n result: \"info\",\n fields: {\n hasCwd: Boolean(cwd),\n promptLength: promptSummary.length,\n promptHash: promptSummary.sha256Prefix,\n },\n });\n\n const intent = classifyTurnIntent(prompt);\n const state = await createPendingTurnState({\n sessionId,\n prompt,\n initialCwd: cwd,\n intent,\n });\n\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n turnId: state.turnId,\n stage: \"state_created\",\n result: \"success\",\n fields: {\n intent,\n hasInitialCwd: Boolean(cwd),\n },\n });\n\n const boundRepo = await findBoundRepo(cwd);\n if (!boundRepo) {\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n turnId: state.turnId,\n stage: \"bound_repo_lookup\",\n result: \"skip\",\n reason: \"repo_not_bound\",\n fields: {\n hasCwd: Boolean(cwd),\n },\n });\n return;\n }\n\n const advisory = boundRepo ? buildPromptRoutingAdvisory(intent) : null;\n const combinedAdvisory = combineAdvisories(\n buildGlobalFinalRecordingReminder(),\n advisory,\n boundRepo ? buildBoundRepoFinalRecordingReminder() : null,\n );\n if (combinedAdvisory) {\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n turnId: state.turnId,\n stage: \"advisory_emitted\",\n result: \"info\",\n repoRoot: boundRepo,\n fields: {\n advisoryLength: combinedAdvisory.length,\n intent,\n },\n });\n process.stdout.write(combinedAdvisory);\n return;\n }\n\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n turnId: state.turnId,\n stage: \"completed\",\n result: \"success\",\n repoRoot: boundRepo,\n fields: {\n intent,\n },\n });\n}\n\nasync function main(): Promise<void> {\n const payload = await readJsonStdin();\n await runHookUserPrompt(payload);\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n void appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n stage: \"unhandled_error\",\n result: \"error\",\n reason: \"exception\",\n message,\n });\n process.stderr.write(`${message}\\n`);\n process.exitCode = 0;\n});\n","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 shouldPreferRemixMemory(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 \"Remix advisory:\",\n \"This prompt looks like a historical reasoning request in a repo bound to Remix.\",\n \"Start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline` before raw git history. Only fetch `remix_collab_memory_change_step_diff` after identifying a relevant `changeStepId`.\",\n ].join(\"\\n\");\n }\n\n if (intent === \"collab_state\") {\n return [\n \"Remix advisory:\",\n \"This prompt looks like a repo collaboration-state request in a repo bound to Remix.\",\n \"Start with `remix_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 { createHash } from \"node:crypto\";\nimport fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nimport { getPendingTurnStateRootPath, listPendingTurnStateSummaries, type PendingTurnStateSummary } from \"./hook-state.js\";\nimport { pluginMetadata } from \"./metadata.js\";\n\ntype HookDiagnosticsResult = \"start\" | \"info\" | \"skip\" | \"success\" | \"error\";\ntype HookDiagnosticsFieldValue = string | number | boolean | null;\n\nexport type HookDiagnosticsEvent = {\n ts: string;\n hook: string;\n pluginVersion: string;\n pid: number;\n sessionId: string | null;\n turnId: string | null;\n stage: string;\n result: HookDiagnosticsResult;\n reason: string | null;\n toolName: string | null;\n repoRoot: string | null;\n message: string | null;\n fields: Record<string, HookDiagnosticsFieldValue>;\n};\n\nexport type HookDiagnosticsReport = {\n logPath: string;\n stateRoot: string;\n recentEvents: HookDiagnosticsEvent[];\n pendingStates: PendingTurnStateSummary[];\n};\n\nconst DEFAULT_EVENT_LIMIT = 50;\nconst MAX_EVENT_LIMIT = 200;\nconst MAX_LOG_BYTES = 512 * 1024;\n\nfunction resolveClaudeRoot(): string {\n const configured = process.env.CLAUDE_CONFIG_DIR?.trim();\n return configured || path.join(os.homedir(), \".claude\");\n}\n\nfunction resolvePluginDataDirName(): string {\n return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;\n}\n\nexport function getHookDiagnosticsDirPath(): string {\n const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();\n return configured || path.join(resolveClaudeRoot(), \"plugins\", \"data\", resolvePluginDataDirName());\n}\n\nexport function getHookDiagnosticsLogPath(): string {\n return path.join(getHookDiagnosticsDirPath(), \"hooks.ndjson\");\n}\n\nfunction clampEventLimit(limit?: number): number {\n if (typeof limit !== \"number\" || !Number.isFinite(limit)) return DEFAULT_EVENT_LIMIT;\n return Math.max(1, Math.min(MAX_EVENT_LIMIT, Math.trunc(limit)));\n}\n\nfunction toFieldValue(value: unknown): HookDiagnosticsFieldValue | undefined {\n if (value === null) return null;\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"boolean\") return value;\n return undefined;\n}\n\nfunction normalizeFields(fields?: Record<string, unknown>): Record<string, HookDiagnosticsFieldValue> {\n if (!fields) return {};\n const normalizedEntries = Object.entries(fields)\n .map(([key, value]) => {\n const normalized = toFieldValue(value);\n return normalized === undefined ? null : ([key, normalized] as const);\n })\n .filter((entry): entry is readonly [string, HookDiagnosticsFieldValue] => entry !== null);\n return Object.fromEntries(normalizedEntries);\n}\n\nasync function rotateLogIfNeeded(logPath: string): Promise<void> {\n const stat = await fs.stat(logPath).catch(() => null);\n if (!stat || stat.size < MAX_LOG_BYTES) {\n return;\n }\n\n const rotatedPath = `${logPath}.1`;\n await fs.rm(rotatedPath, { force: true }).catch(() => undefined);\n await fs.rename(logPath, rotatedPath).catch(() => undefined);\n}\n\nexport function summarizeText(value: string | null | undefined): {\n present: boolean;\n length: number;\n sha256Prefix: string | null;\n} {\n if (typeof value !== \"string\" || !value.trim()) {\n return {\n present: false,\n length: 0,\n sha256Prefix: null,\n };\n }\n\n const trimmed = value.trim();\n return {\n present: true,\n length: trimmed.length,\n sha256Prefix: createHash(\"sha256\").update(trimmed).digest(\"hex\").slice(0, 12),\n };\n}\n\nexport async function appendHookDiagnosticsEvent(params: {\n hook: string;\n sessionId?: string | null;\n turnId?: string | null;\n stage: string;\n result: HookDiagnosticsResult;\n reason?: string | null;\n toolName?: string | null;\n repoRoot?: string | null;\n message?: string | null;\n fields?: Record<string, unknown>;\n}): Promise<void> {\n try {\n const logPath = getHookDiagnosticsLogPath();\n await fs.mkdir(path.dirname(logPath), { recursive: true });\n await rotateLogIfNeeded(logPath);\n const event: HookDiagnosticsEvent = {\n ts: new Date().toISOString(),\n hook: params.hook,\n pluginVersion: pluginMetadata.version,\n pid: process.pid,\n sessionId: params.sessionId?.trim() || null,\n turnId: params.turnId?.trim() || null,\n stage: params.stage.trim(),\n result: params.result,\n reason: params.reason?.trim() || null,\n toolName: params.toolName?.trim() || null,\n repoRoot: params.repoRoot?.trim() || null,\n message: params.message?.trim() || null,\n fields: normalizeFields(params.fields),\n };\n await fs.appendFile(logPath, `${JSON.stringify(event)}\\n`, \"utf8\");\n } catch {\n // Diagnostics are best-effort and must never break hook execution.\n }\n}\n\nasync function readEventsFromFile(filePath: string): Promise<HookDiagnosticsEvent[]> {\n const raw = await fs.readFile(filePath, \"utf8\").catch(() => null);\n if (!raw) return [];\n return raw\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter(Boolean)\n .flatMap((line) => {\n try {\n const parsed = JSON.parse(line) as HookDiagnosticsEvent;\n return parsed && typeof parsed === \"object\" ? [parsed] : [];\n } catch {\n return [];\n }\n });\n}\n\nexport async function readRecentHookDiagnosticsEvents(limit?: number): Promise<HookDiagnosticsEvent[]> {\n const eventLimit = clampEventLimit(limit);\n const logPath = getHookDiagnosticsLogPath();\n const [rotated, current] = await Promise.all([readEventsFromFile(`${logPath}.1`), readEventsFromFile(logPath)]);\n return [...rotated, ...current].slice(-eventLimit);\n}\n\nexport async function readHookDiagnosticsReport(limit?: number): Promise<HookDiagnosticsReport> {\n const [recentEvents, pendingStates] = await Promise.all([\n readRecentHookDiagnosticsEvents(limit),\n listPendingTurnStateSummaries(),\n ]);\n\n return {\n logPath: getHookDiagnosticsLogPath(),\n stateRoot: getPendingTurnStateRootPath(),\n recentEvents,\n pendingStates,\n };\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 RepoRecordMode = \"changed_turn\" | \"no_diff_turn\";\nexport type ManualRecordingScope = \"change_step\" | \"full_turn\";\n\nexport type TouchedRepoState = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n firstTouchedAt: string;\n lastTouchedAt: string;\n lastObservedWriteAt: string | null;\n touchedBy: string[];\n hasObservedWrite: boolean;\n manuallyRecorded: boolean;\n manuallyRecordedAt: string | null;\n manuallyRecordedByTool: string | null;\n manualRecordingScope: ManualRecordingScope | null;\n manualRemoteChangeRecordedAt: string | null;\n stopAttempted: boolean;\n stopRecorded: boolean;\n stopRecordedAt: string | null;\n stopRecordedMode: RepoRecordMode | null;\n recordingFailureMessage: string | null;\n recordingFailureHint: string | null;\n recordingFailedAt: string | null;\n};\n\nexport type PendingTurnState = {\n sessionId: string;\n turnId: string;\n prompt: string;\n initialCwd: string | null;\n intent: TurnIntent;\n submittedAt: string;\n consultedMemory: boolean;\n touchedRepos: Record<string, TouchedRepoState>;\n turnFailureMessage: string | null;\n turnFailureHint: string | null;\n turnFailedAt: string | null;\n};\n\nexport type PendingTouchedRepoSummary = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n lastTouchedAt: string;\n lastObservedWriteAt: string | null;\n touchedBy: string[];\n hasObservedWrite: boolean;\n manuallyRecorded: boolean;\n manualRecordingScope: ManualRecordingScope | null;\n stopAttempted: boolean;\n stopRecorded: boolean;\n stopRecordedMode: RepoRecordMode | null;\n recordingFailureMessage: string | null;\n recordingFailureHint: string | null;\n recordingFailedAt: string | null;\n};\n\nexport type PendingTurnStateSummary = {\n sessionId: string;\n turnId: string;\n initialCwd: string | null;\n submittedAt: string;\n consultedMemory: boolean;\n promptLength: number;\n touchedRepoCount: number;\n turnFailureMessage: string | null;\n turnFailureHint: string | null;\n turnFailedAt: string | null;\n touchedRepos: PendingTouchedRepoSummary[];\n};\n\nfunction stateRoot(): string {\n const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();\n return configured || path.join(os.tmpdir(), \"remix-claude-plugin-hooks\");\n}\n\nfunction statePath(sessionId: string): string {\n return path.join(stateRoot(), `${sessionId}.json`);\n}\n\nfunction stateLockPath(sessionId: string): string {\n return path.join(stateRoot(), `${sessionId}.lock`);\n}\n\nfunction stateLockMetaPath(sessionId: string): string {\n return path.join(stateLockPath(sessionId), \"owner.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\nconst STATE_LOCK_WAIT_MS = 2_000;\nconst STATE_LOCK_POLL_MS = 25;\nconst STATE_LOCK_STALE_MS = 30_000;\nconst STATE_LOCK_HEARTBEAT_MS = 5_000;\n\ntype StateLockMetadata = {\n ownerId: string;\n pid: number;\n createdAt: string;\n heartbeatAt: string;\n};\n\nasync function sleep(ms: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function readStateLockMetadata(sessionId: string): Promise<StateLockMetadata | null> {\n const raw = await fs.readFile(stateLockMetaPath(sessionId), \"utf8\").catch(() => null);\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<StateLockMetadata>;\n if (\n typeof parsed.ownerId !== \"string\" ||\n typeof parsed.pid !== \"number\" ||\n typeof parsed.createdAt !== \"string\" ||\n typeof parsed.heartbeatAt !== \"string\"\n ) {\n return null;\n }\n return {\n ownerId: parsed.ownerId,\n pid: parsed.pid,\n createdAt: parsed.createdAt,\n heartbeatAt: parsed.heartbeatAt,\n };\n } catch {\n return null;\n }\n}\n\nasync function writeStateLockMetadata(sessionId: string, metadata: StateLockMetadata): Promise<void> {\n await writeJsonAtomic(stateLockMetaPath(sessionId), metadata);\n}\n\nasync function tryRemoveStaleStateLock(sessionId: string): Promise<boolean> {\n const lockPath = stateLockPath(sessionId);\n const metadata = await readStateLockMetadata(sessionId);\n const staleByHeartbeat =\n metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;\n if (staleByHeartbeat) {\n await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n return true;\n }\n\n if (!metadata) {\n const lockStat = await fs.stat(lockPath).catch(() => null);\n if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {\n await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n return true;\n }\n }\n\n return false;\n}\n\nasync function acquireStateLock(sessionId: string): Promise<() => Promise<void>> {\n const lockPath = stateLockPath(sessionId);\n const deadline = Date.now() + STATE_LOCK_WAIT_MS;\n await fs.mkdir(stateRoot(), { recursive: true });\n\n while (true) {\n try {\n await fs.mkdir(lockPath);\n const ownerId = randomUUID();\n const createdAt = new Date().toISOString();\n const metadata: StateLockMetadata = {\n ownerId,\n pid: process.pid,\n createdAt,\n heartbeatAt: createdAt,\n };\n await writeStateLockMetadata(sessionId, metadata);\n let released = false;\n const heartbeat = setInterval(() => {\n if (released) return;\n void writeStateLockMetadata(sessionId, {\n ...metadata,\n heartbeatAt: new Date().toISOString(),\n }).catch(() => undefined);\n }, STATE_LOCK_HEARTBEAT_MS);\n heartbeat.unref?.();\n\n return async () => {\n if (released) return;\n released = true;\n clearInterval(heartbeat);\n const currentMetadata = await readStateLockMetadata(sessionId);\n if (currentMetadata?.ownerId === ownerId) {\n await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n }\n };\n } catch (error) {\n const code = error && typeof error === \"object\" && \"code\" in error ? (error as { code?: unknown }).code : null;\n if (code !== \"EEXIST\") {\n throw error;\n }\n\n if (await tryRemoveStaleStateLock(sessionId)) {\n continue;\n }\n\n if (Date.now() >= deadline) {\n throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);\n }\n await sleep(STATE_LOCK_POLL_MS);\n }\n }\n}\n\nasync function withStateLock<T>(sessionId: string, fn: () => Promise<T>): Promise<T> {\n const release = await acquireStateLock(sessionId);\n try {\n return await fn();\n } finally {\n await release();\n }\n}\n\nfunction normalizeIntent(value: unknown): TurnIntent {\n return value === \"memory_first\" || value === \"collab_state\" || value === \"git_facts\" ? value : \"neutral\";\n}\n\nfunction normalizeString(value: unknown): string | null {\n return typeof value === \"string\" && value.trim() ? value.trim() : null;\n}\n\nfunction normalizeStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return Array.from(\n new Set(\n value\n .filter((entry): entry is string => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => entry.trim()),\n ),\n );\n}\n\nfunction normalizeTouchedRepo(value: unknown, repoRoot: string): TouchedRepoState | null {\n if (!value || typeof value !== \"object\") return null;\n const parsed = value as Partial<TouchedRepoState>;\n const normalizedRepoRoot = normalizeString(parsed.repoRoot) ?? repoRoot.trim();\n if (!normalizedRepoRoot) return null;\n\n return {\n repoRoot: normalizedRepoRoot,\n projectId: normalizeString(parsed.projectId),\n currentAppId: normalizeString(parsed.currentAppId),\n upstreamAppId: normalizeString(parsed.upstreamAppId),\n firstTouchedAt: normalizeString(parsed.firstTouchedAt) ?? new Date().toISOString(),\n lastTouchedAt: normalizeString(parsed.lastTouchedAt) ?? new Date().toISOString(),\n lastObservedWriteAt: normalizeString(parsed.lastObservedWriteAt),\n touchedBy: normalizeStringArray(parsed.touchedBy),\n hasObservedWrite: Boolean(parsed.hasObservedWrite),\n manuallyRecorded: Boolean(parsed.manuallyRecorded),\n manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),\n manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),\n manualRecordingScope:\n parsed.manualRecordingScope === \"change_step\" || parsed.manualRecordingScope === \"full_turn\"\n ? parsed.manualRecordingScope\n : null,\n manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),\n stopAttempted: Boolean(parsed.stopAttempted),\n stopRecorded: Boolean(parsed.stopRecorded),\n stopRecordedAt: normalizeString(parsed.stopRecordedAt),\n stopRecordedMode: parsed.stopRecordedMode === \"changed_turn\" || parsed.stopRecordedMode === \"no_diff_turn\" ? parsed.stopRecordedMode : null,\n recordingFailureMessage: normalizeString(parsed.recordingFailureMessage),\n recordingFailureHint: normalizeString(parsed.recordingFailureHint),\n recordingFailedAt: normalizeString(parsed.recordingFailedAt),\n };\n}\n\nfunction normalizeTouchedRepos(value: unknown): Record<string, TouchedRepoState> {\n if (!value || typeof value !== \"object\") return {};\n const entries = Object.entries(value as Record<string, unknown>)\n .map(([repoRoot, repo]) => normalizeTouchedRepo(repo, repoRoot))\n .filter((repo): repo is TouchedRepoState => repo !== null)\n .sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));\n return Object.fromEntries(entries.map((repo) => [repo.repoRoot, repo]));\n}\n\nfunction createTouchedRepo(params: {\n repoRoot: string;\n projectId?: string | null;\n currentAppId?: string | null;\n upstreamAppId?: string | null;\n touchedBy?: string | null;\n hasObservedWrite?: boolean;\n}): TouchedRepoState {\n const now = new Date().toISOString();\n const touchedBy = params.touchedBy?.trim() ? [params.touchedBy.trim()] : [];\n return {\n repoRoot: params.repoRoot,\n projectId: normalizeString(params.projectId),\n currentAppId: normalizeString(params.currentAppId),\n upstreamAppId: normalizeString(params.upstreamAppId),\n firstTouchedAt: now,\n lastTouchedAt: now,\n lastObservedWriteAt: params.hasObservedWrite ? now : null,\n touchedBy,\n hasObservedWrite: Boolean(params.hasObservedWrite),\n manuallyRecorded: false,\n manuallyRecordedAt: null,\n manuallyRecordedByTool: null,\n manualRecordingScope: null,\n manualRemoteChangeRecordedAt: null,\n stopAttempted: false,\n stopRecorded: false,\n stopRecordedAt: null,\n stopRecordedMode: null,\n recordingFailureMessage: null,\n recordingFailureHint: null,\n recordingFailedAt: null,\n };\n}\n\nasync function updatePendingTurnState(\n sessionId: string,\n updater: (state: PendingTurnState) => void | boolean,\n): Promise<PendingTurnState | null> {\n return withStateLock(sessionId, async () => {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing) return null;\n const result = updater(existing);\n if (result === false) return existing;\n await savePendingTurnState(existing);\n return existing;\n });\n}\n\nfunction summarizeTouchedRepo(repo: TouchedRepoState): PendingTouchedRepoSummary {\n return {\n repoRoot: repo.repoRoot,\n projectId: repo.projectId,\n currentAppId: repo.currentAppId,\n upstreamAppId: repo.upstreamAppId,\n lastTouchedAt: repo.lastTouchedAt,\n lastObservedWriteAt: repo.lastObservedWriteAt,\n touchedBy: [...repo.touchedBy],\n hasObservedWrite: repo.hasObservedWrite,\n manuallyRecorded: repo.manuallyRecorded,\n manualRecordingScope: repo.manualRecordingScope,\n stopAttempted: repo.stopAttempted,\n stopRecorded: repo.stopRecorded,\n stopRecordedMode: repo.stopRecordedMode,\n recordingFailureMessage: repo.recordingFailureMessage,\n recordingFailureHint: repo.recordingFailureHint,\n recordingFailedAt: repo.recordingFailedAt,\n };\n}\n\nfunction summarizePendingTurnState(state: PendingTurnState): PendingTurnStateSummary {\n const touchedRepos = Object.values(state.touchedRepos)\n .sort((a, b) => a.repoRoot.localeCompare(b.repoRoot))\n .map((repo) => summarizeTouchedRepo(repo));\n return {\n sessionId: state.sessionId,\n turnId: state.turnId,\n initialCwd: state.initialCwd,\n submittedAt: state.submittedAt,\n consultedMemory: state.consultedMemory,\n promptLength: state.prompt.length,\n touchedRepoCount: touchedRepos.length,\n turnFailureMessage: state.turnFailureMessage,\n turnFailureHint: state.turnFailureHint,\n turnFailedAt: state.turnFailedAt,\n touchedRepos,\n };\n}\n\nexport function getPendingTurnStateRootPath(): string {\n return stateRoot();\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 return {\n sessionId: parsed.sessionId,\n turnId: parsed.turnId,\n prompt: parsed.prompt,\n initialCwd: normalizeString(parsed.initialCwd),\n intent: normalizeIntent(parsed.intent),\n submittedAt: typeof parsed.submittedAt === \"string\" ? parsed.submittedAt : new Date().toISOString(),\n consultedMemory: Boolean(parsed.consultedMemory),\n touchedRepos: normalizeTouchedRepos(parsed.touchedRepos),\n turnFailureMessage: normalizeString(parsed.turnFailureMessage),\n turnFailureHint: normalizeString(parsed.turnFailureHint),\n turnFailedAt: normalizeString(parsed.turnFailedAt),\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 initialCwd?: string | null;\n intent: TurnIntent;\n}): Promise<PendingTurnState> {\n return withStateLock(params.sessionId, async () => {\n const state: PendingTurnState = {\n sessionId: params.sessionId,\n turnId: randomUUID(),\n prompt: params.prompt,\n initialCwd: params.initialCwd?.trim() || null,\n intent: params.intent,\n submittedAt: new Date().toISOString(),\n consultedMemory: false,\n touchedRepos: {},\n turnFailureMessage: null,\n turnFailureHint: null,\n turnFailedAt: null,\n };\n await savePendingTurnState(state);\n return state;\n });\n}\n\nexport async function upsertTouchedRepo(\n sessionId: string,\n params: {\n repoRoot: string;\n projectId?: string | null;\n currentAppId?: string | null;\n upstreamAppId?: string | null;\n touchedBy?: string | null;\n hasObservedWrite?: boolean;\n },\n): Promise<TouchedRepoState | null> {\n const normalizedRepoRoot = params.repoRoot.trim();\n if (!normalizedRepoRoot) return null;\n const state = await updatePendingTurnState(sessionId, (existing) => {\n const current =\n existing.touchedRepos[normalizedRepoRoot] ??\n createTouchedRepo({\n repoRoot: normalizedRepoRoot,\n projectId: params.projectId,\n currentAppId: params.currentAppId,\n upstreamAppId: params.upstreamAppId,\n touchedBy: params.touchedBy,\n hasObservedWrite: params.hasObservedWrite,\n });\n\n current.projectId = normalizeString(params.projectId) ?? current.projectId;\n current.currentAppId = normalizeString(params.currentAppId) ?? current.currentAppId;\n current.upstreamAppId = normalizeString(params.upstreamAppId) ?? current.upstreamAppId;\n current.lastTouchedAt = new Date().toISOString();\n if (params.touchedBy?.trim() && !current.touchedBy.includes(params.touchedBy.trim())) {\n current.touchedBy = [...current.touchedBy, params.touchedBy.trim()].sort((a, b) => a.localeCompare(b));\n }\n if (params.hasObservedWrite) {\n current.hasObservedWrite = true;\n current.lastObservedWriteAt = new Date().toISOString();\n }\n existing.touchedRepos[normalizedRepoRoot] = current;\n });\n return state?.touchedRepos[normalizedRepoRoot] ?? null;\n}\n\nexport async function markTouchedRepoObservedWrite(\n sessionId: string,\n repoRoot: string,\n params?: { toolName?: string | null },\n): Promise<void> {\n await upsertTouchedRepo(sessionId, {\n repoRoot,\n touchedBy: params?.toolName ?? null,\n hasObservedWrite: true,\n });\n}\n\nexport async function markTouchedRepoManuallyRecorded(\n sessionId: string,\n repoRoot: string,\n params?: { toolName?: string | null; scope?: ManualRecordingScope | null; remoteChangeRecorded?: boolean },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const normalizedRepoRoot = repoRoot.trim();\n if (!normalizedRepoRoot) return false;\n const current =\n existing.touchedRepos[normalizedRepoRoot] ??\n createTouchedRepo({\n repoRoot: normalizedRepoRoot,\n touchedBy: params?.toolName ?? null,\n hasObservedWrite: false,\n });\n current.lastTouchedAt = new Date().toISOString();\n current.manuallyRecorded = true;\n current.manuallyRecordedAt = new Date().toISOString();\n current.manuallyRecordedByTool = normalizeString(params?.toolName) ?? current.manuallyRecordedByTool;\n current.manualRecordingScope = params?.scope ?? current.manualRecordingScope;\n if (params?.remoteChangeRecorded) {\n current.manualRemoteChangeRecordedAt = current.manuallyRecordedAt;\n }\n current.recordingFailureMessage = null;\n current.recordingFailureHint = null;\n current.recordingFailedAt = null;\n if (params?.toolName?.trim() && !current.touchedBy.includes(params.toolName.trim())) {\n current.touchedBy = [...current.touchedBy, params.toolName.trim()].sort((a, b) => a.localeCompare(b));\n }\n existing.touchedRepos[normalizedRepoRoot] = current;\n });\n}\n\nexport async function markPendingTurnConsultedMemory(sessionId: string): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n if (existing.consultedMemory) return false;\n existing.consultedMemory = true;\n });\n}\n\nexport async function markTouchedRepoStopAttempted(sessionId: string, repoRoot: string): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const current = existing.touchedRepos[repoRoot];\n if (!current) return false;\n current.stopAttempted = true;\n current.lastTouchedAt = new Date().toISOString();\n });\n}\n\nexport async function markTouchedRepoStopRecorded(\n sessionId: string,\n repoRoot: string,\n params: {\n mode: RepoRecordMode;\n },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const current = existing.touchedRepos[repoRoot];\n if (!current) return false;\n current.stopAttempted = true;\n current.stopRecorded = true;\n current.stopRecordedAt = new Date().toISOString();\n current.stopRecordedMode = params.mode;\n current.recordingFailureMessage = null;\n current.recordingFailureHint = null;\n current.recordingFailedAt = null;\n current.lastTouchedAt = new Date().toISOString();\n });\n}\n\nexport async function markTouchedRepoRecordingFailure(\n sessionId: string,\n repoRoot: string,\n params: {\n message: string;\n hint?: string | null;\n },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const current = existing.touchedRepos[repoRoot];\n if (!current) return false;\n current.stopAttempted = true;\n current.recordingFailureMessage = params.message.trim();\n current.recordingFailureHint = params.hint?.trim() || null;\n current.recordingFailedAt = new Date().toISOString();\n current.lastTouchedAt = new Date().toISOString();\n });\n}\n\nexport async function markPendingTurnFailure(\n sessionId: string,\n params: {\n message: string;\n hint?: string | null;\n },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n existing.turnFailureMessage = params.message.trim();\n existing.turnFailureHint = params.hint?.trim() || null;\n existing.turnFailedAt = new Date().toISOString();\n });\n}\n\nexport async function listTouchedRepos(sessionId: string): Promise<TouchedRepoState[]> {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing) return [];\n return Object.values(existing.touchedRepos).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));\n}\n\nexport async function listPendingTurnStateSummaries(): Promise<PendingTurnStateSummary[]> {\n const root = stateRoot();\n const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);\n const sessionIds = entries\n .filter((entry) => entry.isFile() && entry.name.endsWith(\".json\"))\n .map((entry) => entry.name.replace(/\\.json$/, \"\"))\n .sort((a, b) => a.localeCompare(b));\n const states = await Promise.all(sessionIds.map((sessionId) => loadPendingTurnState(sessionId)));\n return states\n .filter((state): state is PendingTurnState => state !== null)\n .sort((a, b) => b.submittedAt.localeCompare(a.submittedAt))\n .map((state) => summarizePendingTurnState(state));\n}\n\nexport async function clearPendingTurnState(sessionId: string): Promise<void> {\n await withStateLock(sessionId, async () => {\n await fs.rm(statePath(sessionId), { force: true }).catch(() => undefined);\n });\n}\n","{\n \"name\": \"@remixhq/claude-plugin\",\n \"version\": \"0.1.16\",\n \"description\": \"Claude Code plugin for Remix collaboration workflows\",\n \"homepage\": \"https://github.com/RemixDotOne/remix-claude-plugin\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/RemixDotOne/remix-claude-plugin.git\"\n },\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"files\": [\n \"dist\",\n \".claude-plugin/plugin.json\",\n \".mcp.json\",\n \"skills\",\n \"hooks\",\n \"agents\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"postbuild\": \"node -e \\\"const fs=require('node:fs'); for (const p of ['dist/mcp-server.cjs','dist/hook-pre-git.cjs','dist/hook-user-prompt.cjs','dist/hook-post-collab.cjs','dist/hook-stop-collab.cjs']) fs.chmodSync(p, 0o755);\\\"\",\n \"dev\": \"tsx src/mcp-server.ts\",\n \"typecheck\": \"tsc -p tsconfig.json --noEmit\",\n \"prepack\": \"npm run build\"\n },\n \"dependencies\": {\n \"@remixhq/core\": \"^0.1.11\",\n \"@remixhq/mcp\": \"^0.1.11\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^25.4.0\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.9.3\"\n }\n}\n","import pkg from \"../package.json\";\n\nexport const pluginMetadata = {\n name: pkg.name,\n version: pkg.version,\n description: pkg.description,\n pluginId: \"remix\",\n agentName: \"remix-collab\",\n};\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { readCollabBinding } from \"@remixhq/core/binding\";\n\ntype BindingSummary = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n};\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 extractToolResponse(payload: Record<string, unknown>): Record<string, unknown> | null {\n return getNestedRecord(payload.tool_response) ?? getNestedRecord(payload.toolResponse);\n}\n\nfunction extractToolStructuredData(payload: Record<string, unknown>): Record<string, unknown> | null {\n const toolResponse = extractToolResponse(payload);\n const structuredContent = getNestedRecord(toolResponse?.structuredContent) ?? getNestedRecord(payload.structuredContent);\n return (\n getNestedRecord(toolResponse?.data) ??\n getNestedRecord(structuredContent?.data) ??\n structuredContent\n );\n}\n\nexport function extractToolName(payload: Record<string, unknown>): string | null {\n return extractString(payload, [\"tool_name\", \"toolName\"]);\n}\n\nexport function normalizeHookToolName(toolName: string | null): string | null {\n if (!toolName) return null;\n const trimmed = toolName.trim();\n if (!trimmed) return null;\n\n const remixToolIndex = trimmed.toLowerCase().indexOf(\"remix_collab_\");\n if (remixToolIndex >= 0) {\n return trimmed.slice(remixToolIndex);\n }\n\n return trimmed;\n}\n\nexport function extractAssistantResponse(payload: Record<string, unknown>): string | null {\n const candidateKeys = [\n \"last_assistant_message\",\n \"lastAssistantMessage\",\n \"assistant_response\",\n \"assistantResponse\",\n \"assistant_message\",\n \"assistantMessage\",\n \"response\",\n \"message\",\n ];\n\n return (\n extractString(payload, candidateKeys) ??\n extractString(extractToolResponse(payload) ?? {}, candidateKeys) ??\n extractString(extractToolStructuredData(payload) ?? {}, candidateKeys) ??\n extractString(extractToolInput(payload), candidateKeys)\n );\n}\n\nexport function extractFinalizeTurnMode(payload: Record<string, unknown>): \"changed_turn\" | \"no_diff_turn\" | null {\n const mode =\n extractString(extractToolStructuredData(payload) ?? {}, [\"mode\"]) ??\n extractString(extractToolResponse(payload) ?? {}, [\"mode\"]) ??\n extractString(payload, [\"mode\"]);\n if (mode === \"changed_turn\" || mode === \"no_diff_turn\") {\n return mode;\n }\n return null;\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 function extractToolCwd(payload: Record<string, unknown>): string | null {\n const toolInput = extractToolInput(payload);\n return extractString(toolInput, [\"cwd\"]) ?? extractString(payload, [\"cwd\"]);\n}\n\nexport function extractBashCommand(payload: Record<string, unknown>): string | null {\n const toolInput = extractToolInput(payload);\n return extractString(toolInput, [\"command\", \"cmd\", \"bash_command\", \"bashCommand\"]);\n}\n\nexport function extractToolErrorMessage(payload: Record<string, unknown>): string | null {\n const toolResponse = extractToolResponse(payload);\n const toolError = getNestedRecord(toolResponse?.error) ?? getNestedRecord(payload.error);\n return (\n extractString(toolError ?? {}, [\"message\"]) ??\n extractString(toolResponse ?? {}, [\"errorMessage\", \"message\"]) ??\n extractString(payload, [\"errorMessage\"])\n );\n}\n\nfunction extractToolStatus(payload: Record<string, unknown>): string | null {\n const toolResponse = extractToolResponse(payload);\n return (\n extractString(toolResponse ?? {}, [\"status\", \"state\"]) ??\n extractString(payload, [\"status\", \"state\"])\n );\n}\n\nexport function didToolSucceed(payload: Record<string, unknown>): boolean {\n const toolResponse = extractToolResponse(payload);\n const explicitSuccess = toolResponse ? extractBoolean(toolResponse, [\"success\", \"ok\"]) : null;\n if (explicitSuccess !== null) {\n return explicitSuccess;\n }\n\n if (extractToolErrorMessage(payload)) {\n return false;\n }\n\n const status = extractToolStatus(payload)?.toLowerCase();\n if (status === \"error\" || status === \"failed\" || status === \"failure\") {\n return false;\n }\n\n const hookEventName = extractString(payload, [\"hook_event_name\", \"hookEventName\"]);\n return hookEventName === \"PostToolUse\";\n}\n\nexport function isRemoteChangeRecordedButLocalSyncFailed(payload: Record<string, unknown>): boolean {\n return extractToolErrorMessage(payload) === \"Change step succeeded remotely, but automatic local sync failed.\";\n}\n\nfunction collectStringPathValue(value: unknown): string[] {\n if (typeof value === \"string\" && value.trim()) return [value.trim()];\n if (Array.isArray(value)) {\n return value.flatMap((entry) => collectStringPathValue(entry));\n }\n return [];\n}\n\nfunction collectPathTargetsFromObject(input: Record<string, unknown>, keys: string[]): string[] {\n return keys.flatMap((key) => collectStringPathValue(input[key]));\n}\n\nfunction resolveCandidatePath(targetPath: string, baseDir: string): string {\n return path.isAbsolute(targetPath) ? path.normalize(targetPath) : path.resolve(baseDir, targetPath);\n}\n\nexport function extractToolPathTargets(payload: Record<string, unknown>, toolName?: string | null): string[] {\n const name = (toolName ?? extractToolName(payload) ?? \"\").trim().toLowerCase();\n const toolInput = extractToolInput(payload);\n const baseDir = extractToolCwd(payload) ?? process.cwd();\n const baseKeys = [\"path\", \"paths\", \"file_path\", \"filePath\", \"target_file\", \"targetFile\", \"filename\"];\n\n const targets =\n name === \"notebookedit\"\n ? collectPathTargetsFromObject(toolInput, [\"target_notebook\", \"notebook_path\", \"notebookPath\", ...baseKeys])\n : collectPathTargetsFromObject(toolInput, baseKeys);\n\n return Array.from(new Set(targets.map((entry) => resolveCandidatePath(entry, baseDir))));\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, \".remix\", \"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\nexport async function resolveBoundRepoSummary(startPath: string | null): Promise<BindingSummary | null> {\n const repoRoot = await findBoundRepo(startPath);\n if (!repoRoot) return null;\n const binding = await readCollabBinding(repoRoot).catch(() => null);\n if (!binding) return null;\n return {\n repoRoot,\n projectId: binding.projectId,\n currentAppId: binding.currentAppId,\n upstreamAppId: binding.upstreamAppId,\n };\n}\n\nexport async function resolveTouchedBoundReposFromPaths(paths: string[]): Promise<BindingSummary[]> {\n const resolved = await Promise.all(paths.map((targetPath) => resolveBoundRepoSummary(targetPath)));\n const unique = new Map<string, BindingSummary>();\n for (const repo of resolved) {\n if (!repo) continue;\n unique.set(repo.repoRoot, repo);\n }\n return Array.from(unique.values()).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));\n}\n\nexport async function resolveBoundRepoFromToolCwd(payload: Record<string, unknown>): Promise<BindingSummary | null> {\n return resolveBoundRepoSummary(extractToolCwd(payload));\n}\n"],"mappings":"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","names":["import_node_crypto","import_promises","import_node_os","import_node_path","path","os","fs","path","os","fs","path","import_promises","import_node_path","path","fs"]}
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@remixhq/claude-plugin",
6
- version: "0.1.15",
6
+ version: "0.1.16",
7
7
  description: "Claude Code plugin for Remix collaboration workflows",
8
8
  homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
9
9
  license: "MIT",
@@ -34,8 +34,8 @@ var package_default = {
34
34
  prepack: "npm run build"
35
35
  },
36
36
  dependencies: {
37
- "@remixhq/core": "^0.1.10",
38
- "@remixhq/mcp": "^0.1.10"
37
+ "@remixhq/core": "^0.1.11",
38
+ "@remixhq/mcp": "^0.1.11"
39
39
  },
40
40
  devDependencies: {
41
41
  "@types/node": "^25.4.0",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../package.json","../src/metadata.ts"],"sourcesContent":["{\n \"name\": \"@remixhq/claude-plugin\",\n \"version\": \"0.1.15\",\n \"description\": \"Claude Code plugin for Remix collaboration workflows\",\n \"homepage\": \"https://github.com/RemixDotOne/remix-claude-plugin\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/RemixDotOne/remix-claude-plugin.git\"\n },\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"files\": [\n \"dist\",\n \".claude-plugin/plugin.json\",\n \".mcp.json\",\n \"skills\",\n \"hooks\",\n \"agents\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"postbuild\": \"node -e \\\"const fs=require('node:fs'); for (const p of ['dist/mcp-server.cjs','dist/hook-pre-git.cjs','dist/hook-user-prompt.cjs','dist/hook-post-collab.cjs','dist/hook-stop-collab.cjs']) fs.chmodSync(p, 0o755);\\\"\",\n \"dev\": \"tsx src/mcp-server.ts\",\n \"typecheck\": \"tsc -p tsconfig.json --noEmit\",\n \"prepack\": \"npm run build\"\n },\n \"dependencies\": {\n \"@remixhq/core\": \"^0.1.10\",\n \"@remixhq/mcp\": \"^0.1.10\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^25.4.0\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.9.3\"\n }\n}\n","import pkg from \"../package.json\";\n\nexport const pluginMetadata = {\n name: pkg.name,\n version: pkg.version,\n description: pkg.description,\n pluginId: \"remix\",\n agentName: \"remix-collab\",\n};\n"],"mappings":";;;AAAA;AAAA,EACE,MAAQ;AAAA,EACR,SAAW;AAAA,EACX,aAAe;AAAA,EACf,UAAY;AAAA,EACZ,SAAW;AAAA,EACX,YAAc;AAAA,IACZ,MAAQ;AAAA,IACR,KAAO;AAAA,EACT;AAAA,EACA,MAAQ;AAAA,EACR,SAAW;AAAA,IACT,MAAQ;AAAA,EACV;AAAA,EACA,eAAiB;AAAA,IACf,QAAU;AAAA,EACZ;AAAA,EACA,OAAS;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,SAAW;AAAA,IACT,OAAS;AAAA,IACT,WAAa;AAAA,IACb,KAAO;AAAA,IACP,WAAa;AAAA,IACb,SAAW;AAAA,EACb;AAAA,EACA,cAAgB;AAAA,IACd,iBAAiB;AAAA,IACjB,gBAAgB;AAAA,EAClB;AAAA,EACA,iBAAmB;AAAA,IACjB,eAAe;AAAA,IACf,MAAQ;AAAA,IACR,KAAO;AAAA,IACP,YAAc;AAAA,EAChB;AACF;;;ACxCO,IAAM,iBAAiB;AAAA,EAC5B,MAAM,gBAAI;AAAA,EACV,SAAS,gBAAI;AAAA,EACb,aAAa,gBAAI;AAAA,EACjB,UAAU;AAAA,EACV,WAAW;AACb;","names":[]}
1
+ {"version":3,"sources":["../package.json","../src/metadata.ts"],"sourcesContent":["{\n \"name\": \"@remixhq/claude-plugin\",\n \"version\": \"0.1.16\",\n \"description\": \"Claude Code plugin for Remix collaboration workflows\",\n \"homepage\": \"https://github.com/RemixDotOne/remix-claude-plugin\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/RemixDotOne/remix-claude-plugin.git\"\n },\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"files\": [\n \"dist\",\n \".claude-plugin/plugin.json\",\n \".mcp.json\",\n \"skills\",\n \"hooks\",\n \"agents\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"postbuild\": \"node -e \\\"const fs=require('node:fs'); for (const p of ['dist/mcp-server.cjs','dist/hook-pre-git.cjs','dist/hook-user-prompt.cjs','dist/hook-post-collab.cjs','dist/hook-stop-collab.cjs']) fs.chmodSync(p, 0o755);\\\"\",\n \"dev\": \"tsx src/mcp-server.ts\",\n \"typecheck\": \"tsc -p tsconfig.json --noEmit\",\n \"prepack\": \"npm run build\"\n },\n \"dependencies\": {\n \"@remixhq/core\": \"^0.1.11\",\n \"@remixhq/mcp\": \"^0.1.11\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^25.4.0\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.9.3\"\n }\n}\n","import pkg from \"../package.json\";\n\nexport const pluginMetadata = {\n name: pkg.name,\n version: pkg.version,\n description: pkg.description,\n pluginId: \"remix\",\n agentName: \"remix-collab\",\n};\n"],"mappings":";;;AAAA;AAAA,EACE,MAAQ;AAAA,EACR,SAAW;AAAA,EACX,aAAe;AAAA,EACf,UAAY;AAAA,EACZ,SAAW;AAAA,EACX,YAAc;AAAA,IACZ,MAAQ;AAAA,IACR,KAAO;AAAA,EACT;AAAA,EACA,MAAQ;AAAA,EACR,SAAW;AAAA,IACT,MAAQ;AAAA,EACV;AAAA,EACA,eAAiB;AAAA,IACf,QAAU;AAAA,EACZ;AAAA,EACA,OAAS;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,SAAW;AAAA,IACT,OAAS;AAAA,IACT,WAAa;AAAA,IACb,KAAO;AAAA,IACP,WAAa;AAAA,IACb,SAAW;AAAA,EACb;AAAA,EACA,cAAgB;AAAA,IACd,iBAAiB;AAAA,IACjB,gBAAgB;AAAA,EAClB;AAAA,EACA,iBAAmB;AAAA,IACjB,eAAe;AAAA,IACf,MAAQ;AAAA,IACR,KAAO;AAAA,IACP,YAAc;AAAA,EAChB;AACF;;;ACxCO,IAAM,iBAAiB;AAAA,EAC5B,MAAM,gBAAI;AAAA,EACV,SAAS,gBAAI;AAAA,EACb,aAAa,gBAAI;AAAA,EACjB,UAAU;AAAA,EACV,WAAW;AACb;","names":[]}