@remixhq/claude-plugin 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/remix-collab.md +5 -6
- package/dist/hook-post-collab.cjs +49 -11
- package/dist/hook-post-collab.cjs.map +1 -1
- package/dist/hook-pre-git.cjs +1 -1
- package/dist/hook-pre-git.cjs.map +1 -1
- package/dist/hook-stop-collab.cjs +136 -29
- package/dist/hook-stop-collab.cjs.map +1 -1
- package/dist/hook-user-prompt.cjs +37 -9
- package/dist/hook-user-prompt.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +2087 -238
- package/dist/mcp-server.cjs.map +1 -1
- package/package.json +3 -3
- package/skills/access-troubleshooting/SKILL.md +35 -0
- package/skills/app-ops-triage/SKILL.md +32 -0
- package/skills/historical-memory-routing/SKILL.md +2 -0
- package/skills/identity-and-scope-routing/SKILL.md +31 -0
- package/skills/init-or-remix/SKILL.md +1 -1
- package/skills/review-merge-request/SKILL.md +1 -1
- package/skills/safe-collab-workflow/SKILL.md +15 -21
- package/skills/submit-change-step/SKILL.md +11 -19
- package/skills/sync-and-reconcile/SKILL.md +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/history-routing.ts","../src/hook-state.ts","../src/hook-utils.ts","../src/hook-pre-git.ts"],"sourcesContent":["export type TurnIntent = \"memory_first\" | \"collab_state\" | \"git_facts\" | \"neutral\";\n\nconst STRONG_MEMORY_FIRST_PATTERNS: RegExp[] = [\n /\\bwhy\\b/i,\n /\\breason(?:ing)?\\b/i,\n /\\brationale\\b/i,\n /\\bintent\\b/i,\n /\\bdecision(?: trail)?\\b/i,\n /\\bhidden assumptions?\\b/i,\n /\\bwhat led to\\b/i,\n /\\btrying to solve\\b/i,\n /\\bearlier prompts?\\b/i,\n /\\brequirements?\\b/i,\n /\\btemporary patch\\b/i,\n /\\bworkaround\\b/i,\n /\\blong[-\\s]?term design\\b/i,\n /\\bfailed attempts?\\b/i,\n /\\btried before\\b/i,\n /\\bprevious attempts?\\b/i,\n /\\babandon(?:ed)?\\b/i,\n /\\broll(?:ed)? back\\b/i,\n /\\bregressions?\\b/i,\n /\\berrors?\\b.*\\bkept happening\\b/i,\n /\\bbefore i (?:touch|change|modify|refactor)\\b/i,\n /\\bmerge request discussions?\\b/i,\n /\\brecovery\\b/i,\n /\\bdrift\\b/i,\n /\\bcontext did the agent have\\b/i,\n /\\buser (?:ask|request|approval)\\b/i,\n];\n\nconst MEMORY_FIRST_PATTERNS: RegExp[] = [\n /\\brecent changes?\\b/i,\n /\\bwhat led to\\b/i,\n /\\bproblem\\b/i,\n /\\bchange step\\b/i,\n /\\bhistorical\\b/i,\n /\\bhistory\\b/i,\n ...STRONG_MEMORY_FIRST_PATTERNS,\n];\n\nconst COLLAB_STATE_PATTERNS: RegExp[] = [\n /\\bcollab status\\b/i,\n /\\bsync\\b/i,\n /\\breconcile\\b/i,\n /\\bmerge request\\b/i,\n /\\brequest merge\\b/i,\n /\\breview\\b/i,\n /\\bbind(?:ing)?\\b/i,\n /\\bremix\\b/i,\n /\\bupstream\\b/i,\n];\n\nconst GIT_FACT_PATTERNS: RegExp[] = [\n /\\bgit (?:log|show|diff|blame|rev-list|whatchanged)\\b/i,\n /\\bcommit hash(?:es)?\\b/i,\n /\\bexact commits?\\b/i,\n /\\braw git\\b/i,\n /\\bgit history\\b/i,\n /\\bblame this\\b/i,\n /\\bwho changed (?:this line|this file|that line)\\b/i,\n /\\bbranch ancestr(?:y|ies)\\b/i,\n /\\bpatch[-\\s]?level\\b/i,\n];\n\nfunction hasMatch(prompt: string, patterns: RegExp[]): boolean {\n return patterns.some((pattern) => pattern.test(prompt));\n}\n\nexport function classifyTurnIntent(prompt: string): TurnIntent {\n const normalizedPrompt = prompt.trim();\n if (!normalizedPrompt) {\n return \"neutral\";\n }\n\n const hasStrongMemorySignals = hasMatch(normalizedPrompt, STRONG_MEMORY_FIRST_PATTERNS);\n const hasMemorySignals = hasMatch(normalizedPrompt, MEMORY_FIRST_PATTERNS);\n const hasGitFactSignals = hasMatch(normalizedPrompt, GIT_FACT_PATTERNS);\n\n if (hasGitFactSignals && !hasStrongMemorySignals) {\n return \"git_facts\";\n }\n\n if (hasMemorySignals) {\n return \"memory_first\";\n }\n\n if (hasMatch(normalizedPrompt, COLLAB_STATE_PATTERNS)) {\n return \"collab_state\";\n }\n\n if (hasMatch(normalizedPrompt, GIT_FACT_PATTERNS)) {\n return \"git_facts\";\n }\n\n return \"neutral\";\n}\n\nexport function 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 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","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\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(extractToolInput(payload), candidateKeys)\n );\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","import { shouldPreferRemixMemory } from \"./history-routing.js\";\nimport { loadPendingTurnState } from \"./hook-state.js\";\nimport { extractString, extractToolCwd, extractToolInput, findBoundRepo, readJsonStdin } from \"./hook-utils.js\";\n\ntype GitAdvisory = {\n subcommand: string;\n category: \"history_read\" | \"mutation\";\n message: string;\n blocked: boolean;\n};\n\nconst GIT_ADVISORIES: Array<{ subcommand: string; re: RegExp; category: \"history_read\" | \"mutation\"; message: string; blocked: boolean }> = [\n {\n subcommand: \"commit\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+commit\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Remix. Raw `git commit` is blocked for ordinary collaboration work because it bypasses Remix change-step recording. Let the stop hook record changed turns automatically, or use `remix_collab_add` only for explicit recovery, backfill, or manual-recording workflows.\",\n blocked: true,\n },\n {\n subcommand: \"push\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+push\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Remix. Raw `git push` is blocked for ordinary collaboration work because publishing Remix collaboration state must happen through Remix recording, sync, and merge-request flows rather than direct branch pushes.\",\n blocked: true,\n },\n {\n subcommand: \"pull\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+pull\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Remix. Raw `git pull` is blocked because Remix-managed repo alignment must go through `remix_collab_status`, `remix_collab_sync_preview` / `remix_collab_sync_apply`, or reconcile and upstream-sync flows when history diverges.\",\n blocked: true,\n },\n {\n subcommand: \"merge\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+merge\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Remix. Raw `git merge` is blocked because Remix work must be merged through Remix merge requests rather than local branch merges.\",\n blocked: true,\n },\n {\n subcommand: \"rebase\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+rebase\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Remix. Raw `git rebase` is blocked because it rewrites local history outside Remix reconcile and sync controls. Use `remix_collab_status` first, then follow the sync or reconcile path.\",\n blocked: true,\n },\n {\n subcommand: \"reset\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+reset\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Remix. Raw `git reset` is blocked because it can discard or rewrite local state outside Remix recovery flows. Use `remix_collab_status` first, then use Remix sync or reconcile flows to realign state safely.\",\n blocked: true,\n },\n {\n subcommand: \"log\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+log\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Remix. Raw `git log` is a fallback for exact commit history, not the default starting point for historical reasoning. Use Remix memory first when the goal is to understand intent, prior prompts, failed attempts, or decision trail context.\",\n blocked: false,\n },\n {\n subcommand: \"show\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+show\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Remix. Raw `git show` is useful for exact patch inspection, but historical reasoning should start from Remix memory so you can identify the right change step or merge context before expanding into raw diffs.\",\n blocked: false,\n },\n {\n subcommand: \"blame\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+blame\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Remix. Raw `git blame` can identify line ownership, but it does not explain the user ask, reasoning, or failed attempts behind a change. Use Remix memory first when you need historical intent.\",\n blocked: false,\n },\n {\n subcommand: \"diff\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+diff\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Remix. Raw `git diff` is useful for exact patch detail, but Remix memory should be the default first read for understanding why a change happened or which historical step matters before inspecting raw diffs.\",\n blocked: false,\n },\n {\n subcommand: \"rev-list\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+rev-list\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Remix. Raw `git rev-list` is commit-level history detail; use Remix memory first when the question is about reasoning, chronology, or related historical attempts rather than exact ancestry mechanics.\",\n blocked: false,\n },\n];\n\nfunction getGitAdvisory(command: string): GitAdvisory | null {\n for (const advisory of GIT_ADVISORIES) {\n if (advisory.re.test(command)) {\n return {\n subcommand: advisory.subcommand,\n category: advisory.category,\n message: advisory.message,\n blocked: advisory.blocked,\n };\n }\n }\n return null;\n}\n\nfunction buildMemoryFirstMessage(state: Awaited<ReturnType<typeof loadPendingTurnState>>): string | null {\n if (!state) {\n return \"Use `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline` first, then fall back to raw git only if you still need exact commit or patch details.\";\n }\n\n if (state.intent === \"git_facts\") {\n return null;\n }\n\n if (state.intent === \"collab_state\") {\n return \"This turn looks like Remix collaboration-state work. Start with `remix_collab_status`, then use memory reads if you need related historical context before raw git history.\";\n }\n\n if (shouldPreferRemixMemory(state.intent) && !state.consultedMemory) {\n return \"This turn is classified as a Remix memory-first history question, and no memory tool has been used yet. Start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline`. Only use `remix_collab_memory_change_step_diff` after identifying the relevant `changeStepId`, and use raw git after that only if exact repository facts are still needed.\";\n }\n\n if (!state.consultedMemory) {\n return \"Prefer Remix memory before raw git history in a bound repo: start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline`, then use raw git only if you still need exact repository facts.\";\n }\n\n return null;\n}\n\nasync function main(): Promise<void> {\n const payload = await readJsonStdin();\n const toolInput = extractToolInput(payload);\n\n const command = extractString(toolInput, [\"command\", \"cmd\", \"bash_command\"]);\n if (!command) {\n return;\n }\n\n const advisory = getGitAdvisory(command);\n if (!advisory) {\n return;\n }\n\n const cwd = extractToolCwd(payload) ?? process.cwd();\n const boundRepo = await findBoundRepo(cwd);\n if (!boundRepo) {\n return;\n }\n\n const sessionId = extractString(payload, [\"session_id\"]);\n const turnState = sessionId ? await loadPendingTurnState(sessionId) : null;\n if (advisory.category === \"history_read\" && turnState?.intent === \"git_facts\") {\n return;\n }\n\n const memoryFirstMessage = advisory.category === \"history_read\" ? buildMemoryFirstMessage(turnState) : null;\n if (advisory.category === \"history_read\" && !memoryFirstMessage) {\n return;\n }\n\n process.stdout.write(\n [\n advisory.blocked ? \"Remix guardrail:\" : \"Remix advisory:\",\n `Detected raw git ${advisory.subcommand} usage in a repo bound to Remix: ${command}`,\n advisory.message,\n ...(memoryFirstMessage ? [memoryFirstMessage] : []),\n ].join(\"\\n\"),\n );\n if (advisory.blocked) {\n process.exitCode = 2;\n }\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exitCode = 0;\n});\n"],"mappings":"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","names":["path","os","fs","import_promises","import_node_path","path","fs"]}
|
|
1
|
+
{"version":3,"sources":["../src/history-routing.ts","../src/hook-state.ts","../src/hook-utils.ts","../src/hook-pre-git.ts"],"sourcesContent":["export type TurnIntent = \"memory_first\" | \"collab_state\" | \"git_facts\" | \"neutral\";\n\nconst STRONG_MEMORY_FIRST_PATTERNS: RegExp[] = [\n /\\bwhy\\b/i,\n /\\breason(?:ing)?\\b/i,\n /\\brationale\\b/i,\n /\\bintent\\b/i,\n /\\bdecision(?: trail)?\\b/i,\n /\\bhidden assumptions?\\b/i,\n /\\bwhat led to\\b/i,\n /\\btrying to solve\\b/i,\n /\\bearlier prompts?\\b/i,\n /\\brequirements?\\b/i,\n /\\btemporary patch\\b/i,\n /\\bworkaround\\b/i,\n /\\blong[-\\s]?term design\\b/i,\n /\\bfailed attempts?\\b/i,\n /\\btried before\\b/i,\n /\\bprevious attempts?\\b/i,\n /\\babandon(?:ed)?\\b/i,\n /\\broll(?:ed)? back\\b/i,\n /\\bregressions?\\b/i,\n /\\berrors?\\b.*\\bkept happening\\b/i,\n /\\bbefore i (?:touch|change|modify|refactor)\\b/i,\n /\\bmerge request discussions?\\b/i,\n /\\brecovery\\b/i,\n /\\bdrift\\b/i,\n /\\bcontext did the agent have\\b/i,\n /\\buser (?:ask|request|approval)\\b/i,\n];\n\nconst MEMORY_FIRST_PATTERNS: RegExp[] = [\n /\\brecent changes?\\b/i,\n /\\bwhat led to\\b/i,\n /\\bproblem\\b/i,\n /\\bchange step\\b/i,\n /\\bhistorical\\b/i,\n /\\bhistory\\b/i,\n ...STRONG_MEMORY_FIRST_PATTERNS,\n];\n\nconst COLLAB_STATE_PATTERNS: RegExp[] = [\n /\\bcollab status\\b/i,\n /\\bsync\\b/i,\n /\\breconcile\\b/i,\n /\\bmerge request\\b/i,\n /\\brequest merge\\b/i,\n /\\breview\\b/i,\n /\\bbind(?:ing)?\\b/i,\n /\\bremix\\b/i,\n /\\bupstream\\b/i,\n];\n\nconst GIT_FACT_PATTERNS: RegExp[] = [\n /\\bgit (?:log|show|diff|blame|rev-list|whatchanged)\\b/i,\n /\\bcommit hash(?:es)?\\b/i,\n /\\bexact commits?\\b/i,\n /\\braw git\\b/i,\n /\\bgit history\\b/i,\n /\\bblame this\\b/i,\n /\\bwho changed (?:this line|this file|that line)\\b/i,\n /\\bbranch ancestr(?:y|ies)\\b/i,\n /\\bpatch[-\\s]?level\\b/i,\n];\n\nfunction hasMatch(prompt: string, patterns: RegExp[]): boolean {\n return patterns.some((pattern) => pattern.test(prompt));\n}\n\nexport function classifyTurnIntent(prompt: string): TurnIntent {\n const normalizedPrompt = prompt.trim();\n if (!normalizedPrompt) {\n return \"neutral\";\n }\n\n const hasStrongMemorySignals = hasMatch(normalizedPrompt, STRONG_MEMORY_FIRST_PATTERNS);\n const hasMemorySignals = hasMatch(normalizedPrompt, MEMORY_FIRST_PATTERNS);\n const hasGitFactSignals = hasMatch(normalizedPrompt, GIT_FACT_PATTERNS);\n\n if (hasGitFactSignals && !hasStrongMemorySignals) {\n return \"git_facts\";\n }\n\n if (hasMemorySignals) {\n return \"memory_first\";\n }\n\n if (hasMatch(normalizedPrompt, COLLAB_STATE_PATTERNS)) {\n return \"collab_state\";\n }\n\n if (hasMatch(normalizedPrompt, GIT_FACT_PATTERNS)) {\n return \"git_facts\";\n }\n\n return \"neutral\";\n}\n\nexport function 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 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","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","import { shouldPreferRemixMemory } from \"./history-routing.js\";\nimport { loadPendingTurnState } from \"./hook-state.js\";\nimport { extractString, extractToolCwd, extractToolInput, findBoundRepo, readJsonStdin } from \"./hook-utils.js\";\n\ntype GitAdvisory = {\n subcommand: string;\n category: \"history_read\" | \"mutation\";\n message: string;\n blocked: boolean;\n};\n\nconst GIT_ADVISORIES: Array<{ subcommand: string; re: RegExp; category: \"history_read\" | \"mutation\"; message: string; blocked: boolean }> = [\n {\n subcommand: \"commit\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+commit\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Remix. Raw `git commit` is blocked for ordinary collaboration work because it bypasses Remix turn recording. You must record the completed turn with `remix_collab_finalize_turn` before ending the response.\",\n blocked: true,\n },\n {\n subcommand: \"push\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+push\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Remix. Raw `git push` is blocked for ordinary collaboration work because publishing Remix collaboration state must happen through Remix recording, sync, and merge-request flows rather than direct branch pushes.\",\n blocked: true,\n },\n {\n subcommand: \"pull\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+pull\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Remix. Raw `git pull` is blocked because Remix-managed repo alignment must go through `remix_collab_status`, `remix_collab_sync_preview` / `remix_collab_sync_apply`, or reconcile and upstream-sync flows when history diverges.\",\n blocked: true,\n },\n {\n subcommand: \"merge\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+merge\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Remix. Raw `git merge` is blocked because Remix work must be merged through Remix merge requests rather than local branch merges.\",\n blocked: true,\n },\n {\n subcommand: \"rebase\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+rebase\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Remix. Raw `git rebase` is blocked because it rewrites local history outside Remix reconcile and sync controls. Use `remix_collab_status` first, then follow the sync or reconcile path.\",\n blocked: true,\n },\n {\n subcommand: \"reset\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+reset\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Remix. Raw `git reset` is blocked because it can discard or rewrite local state outside Remix recovery flows. Use `remix_collab_status` first, then use Remix sync or reconcile flows to realign state safely.\",\n blocked: true,\n },\n {\n subcommand: \"log\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+log\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Remix. Raw `git log` is a fallback for exact commit history, not the default starting point for historical reasoning. Use Remix memory first when the goal is to understand intent, prior prompts, failed attempts, or decision trail context.\",\n blocked: false,\n },\n {\n subcommand: \"show\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+show\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Remix. Raw `git show` is useful for exact patch inspection, but historical reasoning should start from Remix memory so you can identify the right change step or merge context before expanding into raw diffs.\",\n blocked: false,\n },\n {\n subcommand: \"blame\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+blame\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Remix. Raw `git blame` can identify line ownership, but it does not explain the user ask, reasoning, or failed attempts behind a change. Use Remix memory first when you need historical intent.\",\n blocked: false,\n },\n {\n subcommand: \"diff\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+diff\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Remix. Raw `git diff` is useful for exact patch detail, but Remix memory should be the default first read for understanding why a change happened or which historical step matters before inspecting raw diffs.\",\n blocked: false,\n },\n {\n subcommand: \"rev-list\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+rev-list\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Remix. Raw `git rev-list` is commit-level history detail; use Remix memory first when the question is about reasoning, chronology, or related historical attempts rather than exact ancestry mechanics.\",\n blocked: false,\n },\n];\n\nfunction getGitAdvisory(command: string): GitAdvisory | null {\n for (const advisory of GIT_ADVISORIES) {\n if (advisory.re.test(command)) {\n return {\n subcommand: advisory.subcommand,\n category: advisory.category,\n message: advisory.message,\n blocked: advisory.blocked,\n };\n }\n }\n return null;\n}\n\nfunction buildMemoryFirstMessage(state: Awaited<ReturnType<typeof loadPendingTurnState>>): string | null {\n if (!state) {\n return \"Use `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline` first, then fall back to raw git only if you still need exact commit or patch details.\";\n }\n\n if (state.intent === \"git_facts\") {\n return null;\n }\n\n if (state.intent === \"collab_state\") {\n return \"This turn looks like Remix collaboration-state work. Start with `remix_collab_status`, then use memory reads if you need related historical context before raw git history.\";\n }\n\n if (shouldPreferRemixMemory(state.intent) && !state.consultedMemory) {\n return \"This turn is classified as a Remix memory-first history question, and no memory tool has been used yet. Start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline`. Only use `remix_collab_memory_change_step_diff` after identifying the relevant `changeStepId`, and use raw git after that only if exact repository facts are still needed.\";\n }\n\n if (!state.consultedMemory) {\n return \"Prefer Remix memory before raw git history in a bound repo: start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline`, then use raw git only if you still need exact repository facts.\";\n }\n\n return null;\n}\n\nasync function main(): Promise<void> {\n const payload = await readJsonStdin();\n const toolInput = extractToolInput(payload);\n\n const command = extractString(toolInput, [\"command\", \"cmd\", \"bash_command\"]);\n if (!command) {\n return;\n }\n\n const advisory = getGitAdvisory(command);\n if (!advisory) {\n return;\n }\n\n const cwd = extractToolCwd(payload) ?? process.cwd();\n const boundRepo = await findBoundRepo(cwd);\n if (!boundRepo) {\n return;\n }\n\n const sessionId = extractString(payload, [\"session_id\"]);\n const turnState = sessionId ? await loadPendingTurnState(sessionId) : null;\n if (advisory.category === \"history_read\" && turnState?.intent === \"git_facts\") {\n return;\n }\n\n const memoryFirstMessage = advisory.category === \"history_read\" ? buildMemoryFirstMessage(turnState) : null;\n if (advisory.category === \"history_read\" && !memoryFirstMessage) {\n return;\n }\n\n process.stdout.write(\n [\n advisory.blocked ? \"Remix guardrail:\" : \"Remix advisory:\",\n `Detected raw git ${advisory.subcommand} usage in a repo bound to Remix: ${command}`,\n advisory.message,\n ...(memoryFirstMessage ? [memoryFirstMessage] : []),\n ].join(\"\\n\"),\n );\n if (advisory.blocked) {\n process.exitCode = 2;\n }\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exitCode = 0;\n});\n"],"mappings":"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","names":["path","os","fs","import_promises","import_node_path","path","fs"]}
|
|
@@ -553,7 +553,7 @@ var RemixError = class extends Error {
|
|
|
553
553
|
}
|
|
554
554
|
};
|
|
555
555
|
|
|
556
|
-
// node_modules/@remixhq/core/dist/chunk-
|
|
556
|
+
// node_modules/@remixhq/core/dist/chunk-GEHSFPCD.js
|
|
557
557
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
558
558
|
var import_path = __toESM(require("path"), 1);
|
|
559
559
|
function getCollabBindingPath(repoRoot) {
|
|
@@ -590,7 +590,7 @@ var REMIX_ERROR_CODES = {
|
|
|
590
590
|
PREFERRED_BRANCH_MISMATCH: "PREFERRED_BRANCH_MISMATCH"
|
|
591
591
|
};
|
|
592
592
|
|
|
593
|
-
// node_modules/@remixhq/core/dist/chunk-
|
|
593
|
+
// node_modules/@remixhq/core/dist/chunk-J3J4PBQ7.js
|
|
594
594
|
var import_promises13 = __toESM(require("fs/promises"), 1);
|
|
595
595
|
var import_crypto = require("crypto");
|
|
596
596
|
var import_os = __toESM(require("os"), 1);
|
|
@@ -7371,7 +7371,7 @@ var {
|
|
|
7371
7371
|
getCancelSignal: getCancelSignal2
|
|
7372
7372
|
} = getIpcExport();
|
|
7373
7373
|
|
|
7374
|
-
// node_modules/@remixhq/core/dist/chunk-
|
|
7374
|
+
// node_modules/@remixhq/core/dist/chunk-J3J4PBQ7.js
|
|
7375
7375
|
async function runGit(args, cwd) {
|
|
7376
7376
|
const res = await execa("git", args, { cwd, stderr: "ignore" });
|
|
7377
7377
|
return String(res.stdout || "").trim();
|
|
@@ -7887,6 +7887,25 @@ function formatCliErrorDetail(err) {
|
|
|
7887
7887
|
}
|
|
7888
7888
|
return typeof err === "string" && err.trim() ? err.trim() : null;
|
|
7889
7889
|
}
|
|
7890
|
+
async function pollAppReady(api, appId) {
|
|
7891
|
+
const started = Date.now();
|
|
7892
|
+
let delay = 2e3;
|
|
7893
|
+
while (Date.now() - started < 20 * 60 * 1e3) {
|
|
7894
|
+
const appResp = await api.getApp(appId);
|
|
7895
|
+
const app = unwrapResponseObject(appResp, "app");
|
|
7896
|
+
const status = typeof app.status === "string" ? app.status : "";
|
|
7897
|
+
if (status === "ready") return app;
|
|
7898
|
+
if (status === "error") {
|
|
7899
|
+
throw new RemixError("App is in error state.", {
|
|
7900
|
+
exitCode: 1,
|
|
7901
|
+
hint: typeof app.statusError === "string" ? app.statusError : null
|
|
7902
|
+
});
|
|
7903
|
+
}
|
|
7904
|
+
await sleep(delay);
|
|
7905
|
+
delay = Math.min(1e4, Math.floor(delay * 1.4));
|
|
7906
|
+
}
|
|
7907
|
+
throw new RemixError("Timed out waiting for app to become ready.", { exitCode: 1 });
|
|
7908
|
+
}
|
|
7890
7909
|
async function pollChangeStep(api, appId, changeStepId) {
|
|
7891
7910
|
const started = Date.now();
|
|
7892
7911
|
let delay = 1500;
|
|
@@ -8708,6 +8727,7 @@ async function collabAdd(params) {
|
|
|
8708
8727
|
}
|
|
8709
8728
|
const { backupPath } = await writeTempUnifiedDiffBackup(diff, "remix-add");
|
|
8710
8729
|
try {
|
|
8730
|
+
await pollAppReady(params.api, binding.currentAppId);
|
|
8711
8731
|
if (submissionSnapshot) {
|
|
8712
8732
|
await assertRepoSnapshotUnchanged(repoRoot, submissionSnapshot, {
|
|
8713
8733
|
operation: "`remix collab add` auto-sync",
|
|
@@ -8853,7 +8873,7 @@ async function collabRecordTurn(params) {
|
|
|
8853
8873
|
return unwrapResponseObject(resp, "collab turn");
|
|
8854
8874
|
}
|
|
8855
8875
|
|
|
8856
|
-
// node_modules/@remixhq/core/dist/chunk-
|
|
8876
|
+
// node_modules/@remixhq/core/dist/chunk-XC2FV57P.js
|
|
8857
8877
|
async function readJsonSafe(res) {
|
|
8858
8878
|
const ct = res.headers.get("content-type") ?? "";
|
|
8859
8879
|
if (!ct.toLowerCase().includes("application/json")) return null;
|
|
@@ -8968,10 +8988,29 @@ function createApiClient(config, opts) {
|
|
|
8968
8988
|
if (params?.projectId) qs.set("projectId", params.projectId);
|
|
8969
8989
|
if (params?.organizationId) qs.set("organizationId", params.organizationId);
|
|
8970
8990
|
if (params?.forked) qs.set("forked", params.forked);
|
|
8991
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
8992
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
8971
8993
|
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
8972
8994
|
return request(`/v1/apps${suffix}`, { method: "GET" });
|
|
8973
8995
|
},
|
|
8974
8996
|
getApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}`, { method: "GET" }),
|
|
8997
|
+
getAppContext: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/context`, { method: "GET" }),
|
|
8998
|
+
getAppOverview: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/overview`, { method: "GET" }),
|
|
8999
|
+
listAppTimeline: (appId, params) => {
|
|
9000
|
+
const qs = new URLSearchParams();
|
|
9001
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
9002
|
+
if (params?.cursor) qs.set("cursor", params.cursor);
|
|
9003
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9004
|
+
return request(`/v1/apps/${encodeURIComponent(appId)}/timeline${suffix}`, { method: "GET" });
|
|
9005
|
+
},
|
|
9006
|
+
getAppTimelineEvent: (appId, eventId) => request(`/v1/apps/${encodeURIComponent(appId)}/timeline/${encodeURIComponent(eventId)}`, { method: "GET" }),
|
|
9007
|
+
listAppEditQueue: (appId, params) => {
|
|
9008
|
+
const qs = new URLSearchParams();
|
|
9009
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
9010
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
9011
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9012
|
+
return request(`/v1/apps/${encodeURIComponent(appId)}/edit-queue${suffix}`, { method: "GET" });
|
|
9013
|
+
},
|
|
8975
9014
|
getMergeRequest: (mrId) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}`, { method: "GET" }),
|
|
8976
9015
|
presignImportUpload: (payload) => request("/v1/apps/import/upload/presign", { method: "POST", body: JSON.stringify(payload) }),
|
|
8977
9016
|
importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) }),
|
|
@@ -9053,6 +9092,8 @@ function createApiClient(config, opts) {
|
|
|
9053
9092
|
qs.set("status", params.status);
|
|
9054
9093
|
}
|
|
9055
9094
|
if (params?.kind) qs.set("kind", params.kind);
|
|
9095
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
9096
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
9056
9097
|
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9057
9098
|
return request(`/v1/merge-requests${suffix}`, { method: "GET" });
|
|
9058
9099
|
},
|
|
@@ -9071,24 +9112,60 @@ function createApiClient(config, opts) {
|
|
|
9071
9112
|
method: "POST",
|
|
9072
9113
|
body: JSON.stringify(payload)
|
|
9073
9114
|
}),
|
|
9074
|
-
listOrganizationMembers: (orgId) =>
|
|
9115
|
+
listOrganizationMembers: (orgId, params) => {
|
|
9116
|
+
const qs = new URLSearchParams();
|
|
9117
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
9118
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
9119
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9120
|
+
return request(`/v1/organizations/${encodeURIComponent(orgId)}/members${suffix}`, { method: "GET" });
|
|
9121
|
+
},
|
|
9075
9122
|
updateOrganizationMember: (orgId, userId, payload) => request(`/v1/organizations/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}`, {
|
|
9076
9123
|
method: "PATCH",
|
|
9077
9124
|
body: JSON.stringify(payload)
|
|
9078
9125
|
}),
|
|
9079
|
-
listProjectMembers: (projectId) =>
|
|
9126
|
+
listProjectMembers: (projectId, params) => {
|
|
9127
|
+
const qs = new URLSearchParams();
|
|
9128
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
9129
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
9130
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9131
|
+
return request(`/v1/projects/${encodeURIComponent(projectId)}/members${suffix}`, { method: "GET" });
|
|
9132
|
+
},
|
|
9080
9133
|
updateProjectMember: (projectId, userId, payload) => request(`/v1/projects/${encodeURIComponent(projectId)}/members/${encodeURIComponent(userId)}`, {
|
|
9081
9134
|
method: "PATCH",
|
|
9082
9135
|
body: JSON.stringify(payload)
|
|
9083
9136
|
}),
|
|
9084
|
-
listAppMembers: (appId) =>
|
|
9137
|
+
listAppMembers: (appId, params) => {
|
|
9138
|
+
const qs = new URLSearchParams();
|
|
9139
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
9140
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
9141
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9142
|
+
return request(`/v1/apps/${encodeURIComponent(appId)}/members${suffix}`, { method: "GET" });
|
|
9143
|
+
},
|
|
9085
9144
|
updateAppMember: (appId, userId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/members/${encodeURIComponent(userId)}`, {
|
|
9086
9145
|
method: "PATCH",
|
|
9087
9146
|
body: JSON.stringify(payload)
|
|
9088
9147
|
}),
|
|
9089
|
-
listOrganizationInvites: (orgId) =>
|
|
9090
|
-
|
|
9091
|
-
|
|
9148
|
+
listOrganizationInvites: (orgId, params) => {
|
|
9149
|
+
const qs = new URLSearchParams();
|
|
9150
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
9151
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
9152
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9153
|
+
return request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations${suffix}`, { method: "GET" });
|
|
9154
|
+
},
|
|
9155
|
+
listProjectInvites: (projectId, params) => {
|
|
9156
|
+
const qs = new URLSearchParams();
|
|
9157
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
9158
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
9159
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9160
|
+
return request(`/v1/projects/${encodeURIComponent(projectId)}/invitations${suffix}`, { method: "GET" });
|
|
9161
|
+
},
|
|
9162
|
+
listAppInvites: (appId, params) => {
|
|
9163
|
+
const qs = new URLSearchParams();
|
|
9164
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
9165
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
9166
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9167
|
+
return request(`/v1/apps/${encodeURIComponent(appId)}/invitations${suffix}`, { method: "GET" });
|
|
9168
|
+
},
|
|
9092
9169
|
resendOrganizationInvite: (orgId, inviteId, payload) => request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations/${encodeURIComponent(inviteId)}/resend`, {
|
|
9093
9170
|
method: "POST",
|
|
9094
9171
|
body: JSON.stringify(payload ?? {})
|
|
@@ -9110,6 +9187,7 @@ function createApiClient(config, opts) {
|
|
|
9110
9187
|
revokeAppInvite: (appId, inviteId) => request(`/v1/apps/${encodeURIComponent(appId)}/invitations/${encodeURIComponent(inviteId)}`, {
|
|
9111
9188
|
method: "DELETE"
|
|
9112
9189
|
}),
|
|
9190
|
+
acceptInvitation: (payload) => request("/v1/invitations/accept", { method: "POST", body: JSON.stringify(payload) }),
|
|
9113
9191
|
syncUpstreamApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/sync-upstream`, {
|
|
9114
9192
|
method: "POST",
|
|
9115
9193
|
body: JSON.stringify({})
|
|
@@ -9145,7 +9223,31 @@ function createApiClient(config, opts) {
|
|
|
9145
9223
|
`/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/assets/download?${qs.toString()}`,
|
|
9146
9224
|
{ method: "GET" }
|
|
9147
9225
|
);
|
|
9148
|
-
}
|
|
9226
|
+
},
|
|
9227
|
+
listAgentRuns: (appId, params) => {
|
|
9228
|
+
const qs = new URLSearchParams();
|
|
9229
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
9230
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
9231
|
+
if (params?.status) qs.set("status", params.status);
|
|
9232
|
+
if (params?.currentPhase) qs.set("currentPhase", params.currentPhase);
|
|
9233
|
+
if (params?.createdAfter) qs.set("createdAfter", params.createdAfter);
|
|
9234
|
+
if (params?.createdBefore) qs.set("createdBefore", params.createdBefore);
|
|
9235
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9236
|
+
return request(`/v1/apps/${encodeURIComponent(appId)}/agent-runs${suffix}`, { method: "GET" });
|
|
9237
|
+
},
|
|
9238
|
+
getAgentRun: (appId, runId) => request(`/v1/apps/${encodeURIComponent(appId)}/agent-runs/${encodeURIComponent(runId)}`, { method: "GET" }),
|
|
9239
|
+
listAgentRunEvents: (appId, runId, params) => {
|
|
9240
|
+
const qs = new URLSearchParams();
|
|
9241
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
9242
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
9243
|
+
if (params?.createdAfter) qs.set("createdAfter", params.createdAfter);
|
|
9244
|
+
if (params?.createdBefore) qs.set("createdBefore", params.createdBefore);
|
|
9245
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9246
|
+
return request(`/v1/apps/${encodeURIComponent(appId)}/agent-runs/${encodeURIComponent(runId)}/events${suffix}`, {
|
|
9247
|
+
method: "GET"
|
|
9248
|
+
});
|
|
9249
|
+
},
|
|
9250
|
+
getSandboxStatus: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/sandbox/status`, { method: "GET" })
|
|
9149
9251
|
};
|
|
9150
9252
|
}
|
|
9151
9253
|
|
|
@@ -33420,7 +33522,7 @@ async function clearPendingTurnState(sessionId) {
|
|
|
33420
33522
|
// package.json
|
|
33421
33523
|
var package_default = {
|
|
33422
33524
|
name: "@remixhq/claude-plugin",
|
|
33423
|
-
version: "0.1.
|
|
33525
|
+
version: "0.1.14",
|
|
33424
33526
|
description: "Claude Code plugin for Remix collaboration workflows",
|
|
33425
33527
|
homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
|
|
33426
33528
|
license: "MIT",
|
|
@@ -33451,8 +33553,8 @@ var package_default = {
|
|
|
33451
33553
|
prepack: "npm run build"
|
|
33452
33554
|
},
|
|
33453
33555
|
dependencies: {
|
|
33454
|
-
"@remixhq/core": "^0.1.
|
|
33455
|
-
"@remixhq/mcp": "^0.1.
|
|
33556
|
+
"@remixhq/core": "^0.1.9",
|
|
33557
|
+
"@remixhq/mcp": "^0.1.9"
|
|
33456
33558
|
},
|
|
33457
33559
|
devDependencies: {
|
|
33458
33560
|
"@types/node": "^25.4.0",
|
|
@@ -33578,6 +33680,11 @@ function extractToolInput(payload) {
|
|
|
33578
33680
|
function extractToolResponse(payload) {
|
|
33579
33681
|
return getNestedRecord(payload.tool_response) ?? getNestedRecord(payload.toolResponse);
|
|
33580
33682
|
}
|
|
33683
|
+
function extractToolStructuredData(payload) {
|
|
33684
|
+
const toolResponse = extractToolResponse(payload);
|
|
33685
|
+
const structuredContent = getNestedRecord(toolResponse?.structuredContent) ?? getNestedRecord(payload.structuredContent);
|
|
33686
|
+
return getNestedRecord(toolResponse?.data) ?? getNestedRecord(structuredContent?.data) ?? structuredContent;
|
|
33687
|
+
}
|
|
33581
33688
|
function extractAssistantResponse(payload) {
|
|
33582
33689
|
const candidateKeys = [
|
|
33583
33690
|
"last_assistant_message",
|
|
@@ -33589,7 +33696,7 @@ function extractAssistantResponse(payload) {
|
|
|
33589
33696
|
"response",
|
|
33590
33697
|
"message"
|
|
33591
33698
|
];
|
|
33592
|
-
return extractString(payload, candidateKeys) ?? extractString(extractToolResponse(payload) ?? {}, candidateKeys) ?? extractString(extractToolInput(payload), candidateKeys);
|
|
33699
|
+
return extractString(payload, candidateKeys) ?? extractString(extractToolResponse(payload) ?? {}, candidateKeys) ?? extractString(extractToolStructuredData(payload) ?? {}, candidateKeys) ?? extractString(extractToolInput(payload), candidateKeys);
|
|
33593
33700
|
}
|
|
33594
33701
|
function extractString(input, keys) {
|
|
33595
33702
|
for (const key of keys) {
|
|
@@ -33649,52 +33756,52 @@ function getErrorDetails(error) {
|
|
|
33649
33756
|
if (error instanceof Error) {
|
|
33650
33757
|
const hint = typeof error.hint === "string" ? String(error.hint) : null;
|
|
33651
33758
|
return {
|
|
33652
|
-
message: error.message || "
|
|
33759
|
+
message: error.message || "Fallback Remix turn recording failed.",
|
|
33653
33760
|
hint
|
|
33654
33761
|
};
|
|
33655
33762
|
}
|
|
33656
|
-
const message = typeof error === "string" && error.trim() ? error.trim() : "
|
|
33763
|
+
const message = typeof error === "string" && error.trim() ? error.trim() : "Fallback Remix turn recording failed.";
|
|
33657
33764
|
return { message, hint: null };
|
|
33658
33765
|
}
|
|
33659
33766
|
function getRecordingBlockedMessage(status, repoRoot) {
|
|
33660
33767
|
switch (status.status) {
|
|
33661
33768
|
case "not_git_repo":
|
|
33662
33769
|
return {
|
|
33663
|
-
message: "
|
|
33770
|
+
message: "Fallback Remix turn recording failed because the repository is no longer inside a git repository.",
|
|
33664
33771
|
hint: status.hint || `Repo root: ${repoRoot}`
|
|
33665
33772
|
};
|
|
33666
33773
|
case "not_bound":
|
|
33667
33774
|
return {
|
|
33668
|
-
message: "
|
|
33775
|
+
message: "Fallback Remix turn recording failed because the repository is no longer bound to Remix.",
|
|
33669
33776
|
hint: status.hint || `Repo root: ${repoRoot}`
|
|
33670
33777
|
};
|
|
33671
33778
|
case "missing_head":
|
|
33672
33779
|
return {
|
|
33673
|
-
message: "
|
|
33780
|
+
message: "Fallback Remix turn recording failed because the repository HEAD could not be resolved.",
|
|
33674
33781
|
hint: status.hint || `Repo root: ${repoRoot}`
|
|
33675
33782
|
};
|
|
33676
33783
|
case "branch_mismatch":
|
|
33677
33784
|
return {
|
|
33678
|
-
message: "
|
|
33785
|
+
message: "Fallback Remix turn recording was blocked by the checkout's preferred-branch policy.",
|
|
33679
33786
|
hint: status.hint || `Repo root: ${repoRoot}`
|
|
33680
33787
|
};
|
|
33681
33788
|
case "metadata_conflict":
|
|
33682
33789
|
return {
|
|
33683
|
-
message: "
|
|
33790
|
+
message: "Fallback Remix turn recording was blocked because local repository metadata conflicts with the bound Remix app.",
|
|
33684
33791
|
hint: status.hint || `Repo root: ${repoRoot}`
|
|
33685
33792
|
};
|
|
33686
33793
|
case "reconcile_required":
|
|
33687
33794
|
return {
|
|
33688
|
-
message: "
|
|
33795
|
+
message: "Fallback Remix turn recording was blocked because the repository must be reconciled before recording can continue safely.",
|
|
33689
33796
|
hint: status.hint || `Repo root: ${repoRoot}`
|
|
33690
33797
|
};
|
|
33691
33798
|
default:
|
|
33692
33799
|
return null;
|
|
33693
33800
|
}
|
|
33694
33801
|
}
|
|
33695
|
-
function buildRepoIdempotencyKey(turnId, repo
|
|
33802
|
+
function buildRepoIdempotencyKey(turnId, repo) {
|
|
33696
33803
|
const repoToken = repo.currentAppId?.trim() || repo.repoRoot;
|
|
33697
|
-
return `${turnId}:${repoToken}
|
|
33804
|
+
return `${turnId}:${repoToken}:finalize_turn`;
|
|
33698
33805
|
}
|
|
33699
33806
|
function shouldSkipStopRecording(repo) {
|
|
33700
33807
|
if (repo.stopRecorded) {
|
|
@@ -33766,7 +33873,7 @@ async function recordTouchedRepo(params) {
|
|
|
33766
33873
|
const binding = await readCollabBinding(repo.repoRoot).catch(() => null);
|
|
33767
33874
|
if (!binding) {
|
|
33768
33875
|
await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, {
|
|
33769
|
-
message: "
|
|
33876
|
+
message: "Fallback Remix turn recording failed because the repository is no longer bound to Remix.",
|
|
33770
33877
|
hint: `Repo root: ${repo.repoRoot}`
|
|
33771
33878
|
});
|
|
33772
33879
|
await appendHookDiagnosticsEvent({
|
|
@@ -33835,7 +33942,7 @@ async function recordTouchedRepo(params) {
|
|
|
33835
33942
|
prompt,
|
|
33836
33943
|
assistantResponse,
|
|
33837
33944
|
diffSource: "worktree",
|
|
33838
|
-
idempotencyKey: buildRepoIdempotencyKey(turnId, repo
|
|
33945
|
+
idempotencyKey: buildRepoIdempotencyKey(turnId, repo),
|
|
33839
33946
|
actor: HOOK_ACTOR
|
|
33840
33947
|
});
|
|
33841
33948
|
await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: "changed_turn" });
|
|
@@ -33889,7 +33996,7 @@ async function recordTouchedRepo(params) {
|
|
|
33889
33996
|
cwd: repo.repoRoot,
|
|
33890
33997
|
prompt,
|
|
33891
33998
|
assistantResponse,
|
|
33892
|
-
idempotencyKey: buildRepoIdempotencyKey(turnId, repo
|
|
33999
|
+
idempotencyKey: buildRepoIdempotencyKey(turnId, repo),
|
|
33893
34000
|
actor: HOOK_ACTOR
|
|
33894
34001
|
});
|
|
33895
34002
|
await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: "no_diff_turn" });
|
|
@@ -34044,7 +34151,7 @@ async function runHookStopCollab(payload) {
|
|
|
34044
34151
|
});
|
|
34045
34152
|
if (!prompt || !assistantResponse) {
|
|
34046
34153
|
await markPendingTurnFailure(sessionId, {
|
|
34047
|
-
message: "
|
|
34154
|
+
message: "Fallback Remix turn recording failed because the prompt or assistant response was missing."
|
|
34048
34155
|
});
|
|
34049
34156
|
await appendHookDiagnosticsEvent({
|
|
34050
34157
|
hook,
|