@remixhq/claude-plugin 0.1.10 → 0.1.12

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.
@@ -1,2 +1,3 @@
1
+ declare function runHookStopCollab(payload: Record<string, unknown>): Promise<void>;
1
2
 
2
- export { }
3
+ export { runHookStopCollab };
@@ -6,6 +6,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
9
13
  var __copyProps = (to, from, except, desc) => {
10
14
  if (from && typeof from === "object" || typeof from === "function") {
11
15
  for (let key of __getOwnPropNames(from))
@@ -22,6 +26,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
26
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
27
  mod
24
28
  ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/hook-user-prompt.ts
32
+ var hook_user_prompt_exports = {};
33
+ __export(hook_user_prompt_exports, {
34
+ runHookUserPrompt: () => runHookUserPrompt
35
+ });
36
+ module.exports = __toCommonJS(hook_user_prompt_exports);
25
37
 
26
38
  // src/history-routing.ts
27
39
  var STRONG_MEMORY_FIRST_PATTERNS = [
@@ -126,13 +138,20 @@ function buildPromptRoutingAdvisory(intent) {
126
138
  return null;
127
139
  }
128
140
 
141
+ // src/hook-diagnostics.ts
142
+ var import_node_crypto2 = require("crypto");
143
+ var import_promises2 = __toESM(require("fs/promises"), 1);
144
+ var import_node_os2 = __toESM(require("os"), 1);
145
+ var import_node_path2 = __toESM(require("path"), 1);
146
+
129
147
  // src/hook-state.ts
130
148
  var import_promises = __toESM(require("fs/promises"), 1);
131
149
  var import_node_os = __toESM(require("os"), 1);
132
150
  var import_node_path = __toESM(require("path"), 1);
133
151
  var import_node_crypto = require("crypto");
134
152
  function stateRoot() {
135
- return import_node_path.default.join(import_node_os.default.tmpdir(), "remix-claude-plugin-hooks");
153
+ const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();
154
+ return configured || import_node_path.default.join(import_node_os.default.tmpdir(), "remix-claude-plugin-hooks");
136
155
  }
137
156
  function statePath(sessionId) {
138
157
  return import_node_path.default.join(stateRoot(), `${sessionId}.json`);
@@ -197,6 +216,7 @@ async function tryRemoveStaleStateLock(sessionId) {
197
216
  async function acquireStateLock(sessionId) {
198
217
  const lockPath = stateLockPath(sessionId);
199
218
  const deadline = Date.now() + STATE_LOCK_WAIT_MS;
219
+ await import_promises.default.mkdir(stateRoot(), { recursive: true });
200
220
  while (true) {
201
221
  try {
202
222
  await import_promises.default.mkdir(lockPath);
@@ -273,9 +293,144 @@ async function createPendingTurnState(params) {
273
293
  });
274
294
  }
275
295
 
296
+ // package.json
297
+ var package_default = {
298
+ name: "@remixhq/claude-plugin",
299
+ version: "0.1.12",
300
+ description: "Claude Code plugin for Remix collaboration workflows",
301
+ homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
302
+ license: "MIT",
303
+ repository: {
304
+ type: "git",
305
+ url: "https://github.com/RemixDotOne/remix-claude-plugin.git"
306
+ },
307
+ type: "module",
308
+ engines: {
309
+ node: ">=20"
310
+ },
311
+ publishConfig: {
312
+ access: "public"
313
+ },
314
+ files: [
315
+ "dist",
316
+ ".claude-plugin/plugin.json",
317
+ ".mcp.json",
318
+ "skills",
319
+ "hooks",
320
+ "agents"
321
+ ],
322
+ scripts: {
323
+ build: "tsup",
324
+ postbuild: `node -e "const fs=require('node:fs'); for (const p of ['dist/mcp-server.cjs','dist/hook-pre-git.cjs','dist/hook-user-prompt.cjs','dist/hook-post-collab.cjs','dist/hook-stop-collab.cjs']) fs.chmodSync(p, 0o755);"`,
325
+ dev: "tsx src/mcp-server.ts",
326
+ typecheck: "tsc -p tsconfig.json --noEmit",
327
+ prepack: "npm run build"
328
+ },
329
+ dependencies: {
330
+ "@remixhq/core": "^0.1.8",
331
+ "@remixhq/mcp": "^0.1.8"
332
+ },
333
+ devDependencies: {
334
+ "@types/node": "^25.4.0",
335
+ tsup: "^8.5.1",
336
+ tsx: "^4.21.0",
337
+ typescript: "^5.9.3"
338
+ }
339
+ };
340
+
341
+ // src/metadata.ts
342
+ var pluginMetadata = {
343
+ name: package_default.name,
344
+ version: package_default.version,
345
+ description: package_default.description,
346
+ pluginId: "remix",
347
+ agentName: "remix-collab"
348
+ };
349
+
350
+ // src/hook-diagnostics.ts
351
+ var MAX_LOG_BYTES = 512 * 1024;
352
+ function resolveClaudeRoot() {
353
+ const configured = process.env.CLAUDE_CONFIG_DIR?.trim();
354
+ return configured || import_node_path2.default.join(import_node_os2.default.homedir(), ".claude");
355
+ }
356
+ function resolvePluginDataDirName() {
357
+ return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;
358
+ }
359
+ function getHookDiagnosticsDirPath() {
360
+ const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();
361
+ return configured || import_node_path2.default.join(resolveClaudeRoot(), "plugins", "data", resolvePluginDataDirName());
362
+ }
363
+ function getHookDiagnosticsLogPath() {
364
+ return import_node_path2.default.join(getHookDiagnosticsDirPath(), "hooks.ndjson");
365
+ }
366
+ function toFieldValue(value) {
367
+ if (value === null) return null;
368
+ if (typeof value === "string") return value;
369
+ if (typeof value === "number" && Number.isFinite(value)) return value;
370
+ if (typeof value === "boolean") return value;
371
+ return void 0;
372
+ }
373
+ function normalizeFields(fields) {
374
+ if (!fields) return {};
375
+ const normalizedEntries = Object.entries(fields).map(([key, value]) => {
376
+ const normalized = toFieldValue(value);
377
+ return normalized === void 0 ? null : [key, normalized];
378
+ }).filter((entry) => entry !== null);
379
+ return Object.fromEntries(normalizedEntries);
380
+ }
381
+ async function rotateLogIfNeeded(logPath) {
382
+ const stat = await import_promises2.default.stat(logPath).catch(() => null);
383
+ if (!stat || stat.size < MAX_LOG_BYTES) {
384
+ return;
385
+ }
386
+ const rotatedPath = `${logPath}.1`;
387
+ await import_promises2.default.rm(rotatedPath, { force: true }).catch(() => void 0);
388
+ await import_promises2.default.rename(logPath, rotatedPath).catch(() => void 0);
389
+ }
390
+ function summarizeText(value) {
391
+ if (typeof value !== "string" || !value.trim()) {
392
+ return {
393
+ present: false,
394
+ length: 0,
395
+ sha256Prefix: null
396
+ };
397
+ }
398
+ const trimmed = value.trim();
399
+ return {
400
+ present: true,
401
+ length: trimmed.length,
402
+ sha256Prefix: (0, import_node_crypto2.createHash)("sha256").update(trimmed).digest("hex").slice(0, 12)
403
+ };
404
+ }
405
+ async function appendHookDiagnosticsEvent(params) {
406
+ try {
407
+ const logPath = getHookDiagnosticsLogPath();
408
+ await import_promises2.default.mkdir(import_node_path2.default.dirname(logPath), { recursive: true });
409
+ await rotateLogIfNeeded(logPath);
410
+ const event = {
411
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
412
+ hook: params.hook,
413
+ pluginVersion: pluginMetadata.version,
414
+ pid: process.pid,
415
+ sessionId: params.sessionId?.trim() || null,
416
+ turnId: params.turnId?.trim() || null,
417
+ stage: params.stage.trim(),
418
+ result: params.result,
419
+ reason: params.reason?.trim() || null,
420
+ toolName: params.toolName?.trim() || null,
421
+ repoRoot: params.repoRoot?.trim() || null,
422
+ message: params.message?.trim() || null,
423
+ fields: normalizeFields(params.fields)
424
+ };
425
+ await import_promises2.default.appendFile(logPath, `${JSON.stringify(event)}
426
+ `, "utf8");
427
+ } catch {
428
+ }
429
+ }
430
+
276
431
  // src/hook-utils.ts
277
- var import_promises2 = __toESM(require("fs/promises"), 1);
278
- var import_node_path2 = __toESM(require("path"), 1);
432
+ var import_promises3 = __toESM(require("fs/promises"), 1);
433
+ var import_node_path3 = __toESM(require("path"), 1);
279
434
  async function readJsonStdin() {
280
435
  const chunks = [];
281
436
  for await (const chunk of process.stdin) {
@@ -301,50 +456,155 @@ function extractString(input, keys) {
301
456
  }
302
457
  async function findBoundRepo(startPath) {
303
458
  if (!startPath) return null;
304
- let current = import_node_path2.default.resolve(startPath);
305
- let stats = await import_promises2.default.stat(current).catch(() => null);
459
+ let current = import_node_path3.default.resolve(startPath);
460
+ let stats = await import_promises3.default.stat(current).catch(() => null);
306
461
  if (stats?.isFile()) {
307
- current = import_node_path2.default.dirname(current);
462
+ current = import_node_path3.default.dirname(current);
308
463
  }
309
464
  while (true) {
310
- const bindingPath = import_node_path2.default.join(current, ".remix", "config.json");
311
- const bindingStats = await import_promises2.default.stat(bindingPath).catch(() => null);
465
+ const bindingPath = import_node_path3.default.join(current, ".remix", "config.json");
466
+ const bindingStats = await import_promises3.default.stat(bindingPath).catch(() => null);
312
467
  if (bindingStats?.isFile()) return current;
313
- const parent = import_node_path2.default.dirname(current);
468
+ const parent = import_node_path3.default.dirname(current);
314
469
  if (parent === current) return null;
315
470
  current = parent;
316
471
  }
317
472
  }
318
473
 
319
474
  // src/hook-user-prompt.ts
320
- async function main() {
321
- const payload = await readJsonStdin();
475
+ async function runHookUserPrompt(payload) {
322
476
  const sessionId = extractString(payload, ["session_id"]);
323
477
  const prompt = extractString(payload, ["prompt"]);
324
- if (!sessionId || !prompt) {
478
+ const cwd = extractString(payload, ["cwd"]);
479
+ await appendHookDiagnosticsEvent({
480
+ hook: "UserPromptSubmit",
481
+ sessionId,
482
+ stage: "payload_received",
483
+ result: "start",
484
+ fields: {
485
+ hasSessionId: Boolean(sessionId),
486
+ hasPrompt: Boolean(prompt),
487
+ hasCwd: Boolean(cwd)
488
+ }
489
+ });
490
+ if (!sessionId) {
491
+ await appendHookDiagnosticsEvent({
492
+ hook: "UserPromptSubmit",
493
+ stage: "payload_validation",
494
+ result: "skip",
495
+ reason: "missing_session_id",
496
+ fields: {
497
+ hasPrompt: Boolean(prompt)
498
+ }
499
+ });
325
500
  return;
326
501
  }
327
- const cwd = extractString(payload, ["cwd"]);
502
+ if (!prompt) {
503
+ await appendHookDiagnosticsEvent({
504
+ hook: "UserPromptSubmit",
505
+ sessionId,
506
+ stage: "payload_validation",
507
+ result: "skip",
508
+ reason: "missing_prompt",
509
+ fields: {
510
+ hasCwd: Boolean(cwd)
511
+ }
512
+ });
513
+ return;
514
+ }
515
+ const promptSummary = summarizeText(prompt);
516
+ await appendHookDiagnosticsEvent({
517
+ hook: "UserPromptSubmit",
518
+ sessionId,
519
+ stage: "payload_parsed",
520
+ result: "info",
521
+ fields: {
522
+ hasCwd: Boolean(cwd),
523
+ promptLength: promptSummary.length,
524
+ promptHash: promptSummary.sha256Prefix
525
+ }
526
+ });
328
527
  const intent = classifyTurnIntent(prompt);
329
- await createPendingTurnState({
528
+ const state = await createPendingTurnState({
330
529
  sessionId,
331
530
  prompt,
332
531
  initialCwd: cwd,
333
532
  intent
334
533
  });
534
+ await appendHookDiagnosticsEvent({
535
+ hook: "UserPromptSubmit",
536
+ sessionId,
537
+ turnId: state.turnId,
538
+ stage: "state_created",
539
+ result: "success",
540
+ fields: {
541
+ intent,
542
+ hasInitialCwd: Boolean(cwd)
543
+ }
544
+ });
335
545
  const boundRepo = await findBoundRepo(cwd);
336
546
  if (!boundRepo) {
547
+ await appendHookDiagnosticsEvent({
548
+ hook: "UserPromptSubmit",
549
+ sessionId,
550
+ turnId: state.turnId,
551
+ stage: "bound_repo_lookup",
552
+ result: "skip",
553
+ reason: "repo_not_bound",
554
+ fields: {
555
+ hasCwd: Boolean(cwd)
556
+ }
557
+ });
337
558
  return;
338
559
  }
339
560
  const advisory = buildPromptRoutingAdvisory(intent);
340
561
  if (advisory) {
562
+ await appendHookDiagnosticsEvent({
563
+ hook: "UserPromptSubmit",
564
+ sessionId,
565
+ turnId: state.turnId,
566
+ stage: "advisory_emitted",
567
+ result: "info",
568
+ repoRoot: boundRepo,
569
+ fields: {
570
+ advisoryLength: advisory.length,
571
+ intent
572
+ }
573
+ });
341
574
  process.stdout.write(advisory);
575
+ return;
342
576
  }
577
+ await appendHookDiagnosticsEvent({
578
+ hook: "UserPromptSubmit",
579
+ sessionId,
580
+ turnId: state.turnId,
581
+ stage: "completed",
582
+ result: "success",
583
+ repoRoot: boundRepo,
584
+ fields: {
585
+ intent
586
+ }
587
+ });
588
+ }
589
+ async function main() {
590
+ const payload = await readJsonStdin();
591
+ await runHookUserPrompt(payload);
343
592
  }
344
593
  main().catch((error) => {
345
594
  const message = error instanceof Error ? error.message : String(error);
595
+ void appendHookDiagnosticsEvent({
596
+ hook: "UserPromptSubmit",
597
+ stage: "unhandled_error",
598
+ result: "error",
599
+ reason: "exception",
600
+ message
601
+ });
346
602
  process.stderr.write(`${message}
347
603
  `);
348
604
  process.exitCode = 0;
349
605
  });
606
+ // Annotate the CommonJS export names for ESM import in node:
607
+ 0 && (module.exports = {
608
+ runHookUserPrompt
609
+ });
350
610
  //# sourceMappingURL=hook-user-prompt.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/history-routing.ts","../src/hook-state.ts","../src/hook-utils.ts","../src/hook-user-prompt.ts"],"sourcesContent":["export type TurnIntent = \"memory_first\" | \"collab_state\" | \"git_facts\" | \"neutral\";\n\nconst STRONG_MEMORY_FIRST_PATTERNS: RegExp[] = [\n /\\bwhy\\b/i,\n /\\breason(?:ing)?\\b/i,\n /\\brationale\\b/i,\n /\\bintent\\b/i,\n /\\bdecision(?: trail)?\\b/i,\n /\\bhidden assumptions?\\b/i,\n /\\bwhat led to\\b/i,\n /\\btrying to solve\\b/i,\n /\\bearlier prompts?\\b/i,\n /\\brequirements?\\b/i,\n /\\btemporary patch\\b/i,\n /\\bworkaround\\b/i,\n /\\blong[-\\s]?term design\\b/i,\n /\\bfailed attempts?\\b/i,\n /\\btried before\\b/i,\n /\\bprevious attempts?\\b/i,\n /\\babandon(?:ed)?\\b/i,\n /\\broll(?:ed)? back\\b/i,\n /\\bregressions?\\b/i,\n /\\berrors?\\b.*\\bkept happening\\b/i,\n /\\bbefore i (?:touch|change|modify|refactor)\\b/i,\n /\\bmerge request discussions?\\b/i,\n /\\brecovery\\b/i,\n /\\bdrift\\b/i,\n /\\bcontext did the agent have\\b/i,\n /\\buser (?:ask|request|approval)\\b/i,\n];\n\nconst MEMORY_FIRST_PATTERNS: RegExp[] = [\n /\\brecent changes?\\b/i,\n /\\bwhat led to\\b/i,\n /\\bproblem\\b/i,\n /\\bchange step\\b/i,\n /\\bhistorical\\b/i,\n /\\bhistory\\b/i,\n ...STRONG_MEMORY_FIRST_PATTERNS,\n];\n\nconst COLLAB_STATE_PATTERNS: RegExp[] = [\n /\\bcollab status\\b/i,\n /\\bsync\\b/i,\n /\\breconcile\\b/i,\n /\\bmerge request\\b/i,\n /\\brequest merge\\b/i,\n /\\breview\\b/i,\n /\\bbind(?:ing)?\\b/i,\n /\\bremix\\b/i,\n /\\bupstream\\b/i,\n];\n\nconst GIT_FACT_PATTERNS: RegExp[] = [\n /\\bgit (?:log|show|diff|blame|rev-list|whatchanged)\\b/i,\n /\\bcommit hash(?:es)?\\b/i,\n /\\bexact commits?\\b/i,\n /\\braw git\\b/i,\n /\\bgit history\\b/i,\n /\\bblame this\\b/i,\n /\\bwho changed (?:this line|this file|that line)\\b/i,\n /\\bbranch ancestr(?:y|ies)\\b/i,\n /\\bpatch[-\\s]?level\\b/i,\n];\n\nfunction hasMatch(prompt: string, patterns: RegExp[]): boolean {\n return patterns.some((pattern) => pattern.test(prompt));\n}\n\nexport function classifyTurnIntent(prompt: string): TurnIntent {\n const normalizedPrompt = prompt.trim();\n if (!normalizedPrompt) {\n return \"neutral\";\n }\n\n const hasStrongMemorySignals = hasMatch(normalizedPrompt, STRONG_MEMORY_FIRST_PATTERNS);\n const hasMemorySignals = hasMatch(normalizedPrompt, MEMORY_FIRST_PATTERNS);\n const hasGitFactSignals = hasMatch(normalizedPrompt, GIT_FACT_PATTERNS);\n\n if (hasGitFactSignals && !hasStrongMemorySignals) {\n return \"git_facts\";\n }\n\n if (hasMemorySignals) {\n return \"memory_first\";\n }\n\n if (hasMatch(normalizedPrompt, COLLAB_STATE_PATTERNS)) {\n return \"collab_state\";\n }\n\n if (hasMatch(normalizedPrompt, GIT_FACT_PATTERNS)) {\n return \"git_facts\";\n }\n\n return \"neutral\";\n}\n\nexport function 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\nfunction stateRoot(): string {\n return 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\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\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 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 { buildPromptRoutingAdvisory, classifyTurnIntent } from \"./history-routing.js\";\nimport { createPendingTurnState } from \"./hook-state.js\";\nimport { extractString, findBoundRepo, readJsonStdin } from \"./hook-utils.js\";\n\nasync function main(): Promise<void> {\n const payload = await readJsonStdin();\n const sessionId = extractString(payload, [\"session_id\"]);\n const prompt = extractString(payload, [\"prompt\"]);\n if (!sessionId || !prompt) {\n return;\n }\n\n const cwd = extractString(payload, [\"cwd\"]);\n const intent = classifyTurnIntent(prompt);\n await createPendingTurnState({\n sessionId,\n prompt,\n initialCwd: cwd,\n intent,\n });\n\n const boundRepo = await findBoundRepo(cwd);\n if (!boundRepo) {\n return;\n }\n\n const advisory = buildPromptRoutingAdvisory(intent);\n if (advisory) {\n process.stdout.write(advisory);\n }\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exitCode = 0;\n});\n"],"mappings":"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","names":["path","os","fs","import_promises","import_node_path","path","fs"]}
1
+ {"version":3,"sources":["../src/hook-user-prompt.ts","../src/history-routing.ts","../src/hook-diagnostics.ts","../src/hook-state.ts","../package.json","../src/metadata.ts","../src/hook-utils.ts"],"sourcesContent":["import { buildPromptRoutingAdvisory, classifyTurnIntent } from \"./history-routing.js\";\nimport { appendHookDiagnosticsEvent, summarizeText } from \"./hook-diagnostics.js\";\nimport { createPendingTurnState } from \"./hook-state.js\";\nimport { extractString, findBoundRepo, readJsonStdin } from \"./hook-utils.js\";\n\nexport async function runHookUserPrompt(payload: Record<string, unknown>): Promise<void> {\n const sessionId = extractString(payload, [\"session_id\"]);\n const prompt = extractString(payload, [\"prompt\"]);\n const cwd = extractString(payload, [\"cwd\"]);\n\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n stage: \"payload_received\",\n result: \"start\",\n fields: {\n hasSessionId: Boolean(sessionId),\n hasPrompt: Boolean(prompt),\n hasCwd: Boolean(cwd),\n },\n });\n\n if (!sessionId) {\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n stage: \"payload_validation\",\n result: \"skip\",\n reason: \"missing_session_id\",\n fields: {\n hasPrompt: Boolean(prompt),\n },\n });\n return;\n }\n\n if (!prompt) {\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n stage: \"payload_validation\",\n result: \"skip\",\n reason: \"missing_prompt\",\n fields: {\n hasCwd: Boolean(cwd),\n },\n });\n return;\n }\n\n const promptSummary = summarizeText(prompt);\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n stage: \"payload_parsed\",\n result: \"info\",\n fields: {\n hasCwd: Boolean(cwd),\n promptLength: promptSummary.length,\n promptHash: promptSummary.sha256Prefix,\n },\n });\n\n const intent = classifyTurnIntent(prompt);\n const state = await createPendingTurnState({\n sessionId,\n prompt,\n initialCwd: cwd,\n intent,\n });\n\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n turnId: state.turnId,\n stage: \"state_created\",\n result: \"success\",\n fields: {\n intent,\n hasInitialCwd: Boolean(cwd),\n },\n });\n\n const boundRepo = await findBoundRepo(cwd);\n if (!boundRepo) {\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n turnId: state.turnId,\n stage: \"bound_repo_lookup\",\n result: \"skip\",\n reason: \"repo_not_bound\",\n fields: {\n hasCwd: Boolean(cwd),\n },\n });\n return;\n }\n\n const advisory = buildPromptRoutingAdvisory(intent);\n if (advisory) {\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n turnId: state.turnId,\n stage: \"advisory_emitted\",\n result: \"info\",\n repoRoot: boundRepo,\n fields: {\n advisoryLength: advisory.length,\n intent,\n },\n });\n process.stdout.write(advisory);\n return;\n }\n\n await appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n sessionId,\n turnId: state.turnId,\n stage: \"completed\",\n result: \"success\",\n repoRoot: boundRepo,\n fields: {\n intent,\n },\n });\n}\n\nasync function main(): Promise<void> {\n const payload = await readJsonStdin();\n await runHookUserPrompt(payload);\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n void appendHookDiagnosticsEvent({\n hook: \"UserPromptSubmit\",\n stage: \"unhandled_error\",\n result: \"error\",\n reason: \"exception\",\n message,\n });\n process.stderr.write(`${message}\\n`);\n process.exitCode = 0;\n});\n","export type TurnIntent = \"memory_first\" | \"collab_state\" | \"git_facts\" | \"neutral\";\n\nconst STRONG_MEMORY_FIRST_PATTERNS: RegExp[] = [\n /\\bwhy\\b/i,\n /\\breason(?:ing)?\\b/i,\n /\\brationale\\b/i,\n /\\bintent\\b/i,\n /\\bdecision(?: trail)?\\b/i,\n /\\bhidden assumptions?\\b/i,\n /\\bwhat led to\\b/i,\n /\\btrying to solve\\b/i,\n /\\bearlier prompts?\\b/i,\n /\\brequirements?\\b/i,\n /\\btemporary patch\\b/i,\n /\\bworkaround\\b/i,\n /\\blong[-\\s]?term design\\b/i,\n /\\bfailed attempts?\\b/i,\n /\\btried before\\b/i,\n /\\bprevious attempts?\\b/i,\n /\\babandon(?:ed)?\\b/i,\n /\\broll(?:ed)? back\\b/i,\n /\\bregressions?\\b/i,\n /\\berrors?\\b.*\\bkept happening\\b/i,\n /\\bbefore i (?:touch|change|modify|refactor)\\b/i,\n /\\bmerge request discussions?\\b/i,\n /\\brecovery\\b/i,\n /\\bdrift\\b/i,\n /\\bcontext did the agent have\\b/i,\n /\\buser (?:ask|request|approval)\\b/i,\n];\n\nconst MEMORY_FIRST_PATTERNS: RegExp[] = [\n /\\brecent changes?\\b/i,\n /\\bwhat led to\\b/i,\n /\\bproblem\\b/i,\n /\\bchange step\\b/i,\n /\\bhistorical\\b/i,\n /\\bhistory\\b/i,\n ...STRONG_MEMORY_FIRST_PATTERNS,\n];\n\nconst COLLAB_STATE_PATTERNS: RegExp[] = [\n /\\bcollab status\\b/i,\n /\\bsync\\b/i,\n /\\breconcile\\b/i,\n /\\bmerge request\\b/i,\n /\\brequest merge\\b/i,\n /\\breview\\b/i,\n /\\bbind(?:ing)?\\b/i,\n /\\bremix\\b/i,\n /\\bupstream\\b/i,\n];\n\nconst GIT_FACT_PATTERNS: RegExp[] = [\n /\\bgit (?:log|show|diff|blame|rev-list|whatchanged)\\b/i,\n /\\bcommit hash(?:es)?\\b/i,\n /\\bexact commits?\\b/i,\n /\\braw git\\b/i,\n /\\bgit history\\b/i,\n /\\bblame this\\b/i,\n /\\bwho changed (?:this line|this file|that line)\\b/i,\n /\\bbranch ancestr(?:y|ies)\\b/i,\n /\\bpatch[-\\s]?level\\b/i,\n];\n\nfunction hasMatch(prompt: string, patterns: RegExp[]): boolean {\n return patterns.some((pattern) => pattern.test(prompt));\n}\n\nexport function classifyTurnIntent(prompt: string): TurnIntent {\n const normalizedPrompt = prompt.trim();\n if (!normalizedPrompt) {\n return \"neutral\";\n }\n\n const hasStrongMemorySignals = hasMatch(normalizedPrompt, STRONG_MEMORY_FIRST_PATTERNS);\n const hasMemorySignals = hasMatch(normalizedPrompt, MEMORY_FIRST_PATTERNS);\n const hasGitFactSignals = hasMatch(normalizedPrompt, GIT_FACT_PATTERNS);\n\n if (hasGitFactSignals && !hasStrongMemorySignals) {\n return \"git_facts\";\n }\n\n if (hasMemorySignals) {\n return \"memory_first\";\n }\n\n if (hasMatch(normalizedPrompt, COLLAB_STATE_PATTERNS)) {\n return \"collab_state\";\n }\n\n if (hasMatch(normalizedPrompt, GIT_FACT_PATTERNS)) {\n return \"git_facts\";\n }\n\n return \"neutral\";\n}\n\nexport function shouldPreferRemixMemory(intent: TurnIntent): boolean {\n return intent === \"memory_first\";\n}\n\nexport function buildPromptRoutingAdvisory(intent: TurnIntent): string | null {\n if (intent === \"memory_first\") {\n return [\n \"Remix advisory:\",\n \"This prompt looks like a historical reasoning request in a repo bound to Remix.\",\n \"Start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline` before raw git history. Only fetch `remix_collab_memory_change_step_diff` after identifying a relevant `changeStepId`.\",\n ].join(\"\\n\");\n }\n\n if (intent === \"collab_state\") {\n return [\n \"Remix advisory:\",\n \"This prompt looks like a repo collaboration-state request in a repo bound to Remix.\",\n \"Start with `remix_collab_status`, then follow the recommended sync, reconcile, merge-request, or memory reads from there.\",\n ].join(\"\\n\");\n }\n\n return null;\n}\n","import { createHash } from \"node:crypto\";\nimport fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nimport { getPendingTurnStateRootPath, listPendingTurnStateSummaries, type PendingTurnStateSummary } from \"./hook-state.js\";\nimport { pluginMetadata } from \"./metadata.js\";\n\ntype HookDiagnosticsResult = \"start\" | \"info\" | \"skip\" | \"success\" | \"error\";\ntype HookDiagnosticsFieldValue = string | number | boolean | null;\n\nexport type HookDiagnosticsEvent = {\n ts: string;\n hook: string;\n pluginVersion: string;\n pid: number;\n sessionId: string | null;\n turnId: string | null;\n stage: string;\n result: HookDiagnosticsResult;\n reason: string | null;\n toolName: string | null;\n repoRoot: string | null;\n message: string | null;\n fields: Record<string, HookDiagnosticsFieldValue>;\n};\n\nexport type HookDiagnosticsReport = {\n logPath: string;\n stateRoot: string;\n recentEvents: HookDiagnosticsEvent[];\n pendingStates: PendingTurnStateSummary[];\n};\n\nconst DEFAULT_EVENT_LIMIT = 50;\nconst MAX_EVENT_LIMIT = 200;\nconst MAX_LOG_BYTES = 512 * 1024;\n\nfunction resolveClaudeRoot(): string {\n const configured = process.env.CLAUDE_CONFIG_DIR?.trim();\n return configured || path.join(os.homedir(), \".claude\");\n}\n\nfunction resolvePluginDataDirName(): string {\n return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;\n}\n\nexport function getHookDiagnosticsDirPath(): string {\n const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();\n return configured || path.join(resolveClaudeRoot(), \"plugins\", \"data\", resolvePluginDataDirName());\n}\n\nexport function getHookDiagnosticsLogPath(): string {\n return path.join(getHookDiagnosticsDirPath(), \"hooks.ndjson\");\n}\n\nfunction clampEventLimit(limit?: number): number {\n if (typeof limit !== \"number\" || !Number.isFinite(limit)) return DEFAULT_EVENT_LIMIT;\n return Math.max(1, Math.min(MAX_EVENT_LIMIT, Math.trunc(limit)));\n}\n\nfunction toFieldValue(value: unknown): HookDiagnosticsFieldValue | undefined {\n if (value === null) return null;\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"boolean\") return value;\n return undefined;\n}\n\nfunction normalizeFields(fields?: Record<string, unknown>): Record<string, HookDiagnosticsFieldValue> {\n if (!fields) return {};\n const normalizedEntries = Object.entries(fields)\n .map(([key, value]) => {\n const normalized = toFieldValue(value);\n return normalized === undefined ? null : ([key, normalized] as const);\n })\n .filter((entry): entry is readonly [string, HookDiagnosticsFieldValue] => entry !== null);\n return Object.fromEntries(normalizedEntries);\n}\n\nasync function rotateLogIfNeeded(logPath: string): Promise<void> {\n const stat = await fs.stat(logPath).catch(() => null);\n if (!stat || stat.size < MAX_LOG_BYTES) {\n return;\n }\n\n const rotatedPath = `${logPath}.1`;\n await fs.rm(rotatedPath, { force: true }).catch(() => undefined);\n await fs.rename(logPath, rotatedPath).catch(() => undefined);\n}\n\nexport function summarizeText(value: string | null | undefined): {\n present: boolean;\n length: number;\n sha256Prefix: string | null;\n} {\n if (typeof value !== \"string\" || !value.trim()) {\n return {\n present: false,\n length: 0,\n sha256Prefix: null,\n };\n }\n\n const trimmed = value.trim();\n return {\n present: true,\n length: trimmed.length,\n sha256Prefix: createHash(\"sha256\").update(trimmed).digest(\"hex\").slice(0, 12),\n };\n}\n\nexport async function appendHookDiagnosticsEvent(params: {\n hook: string;\n sessionId?: string | null;\n turnId?: string | null;\n stage: string;\n result: HookDiagnosticsResult;\n reason?: string | null;\n toolName?: string | null;\n repoRoot?: string | null;\n message?: string | null;\n fields?: Record<string, unknown>;\n}): Promise<void> {\n try {\n const logPath = getHookDiagnosticsLogPath();\n await fs.mkdir(path.dirname(logPath), { recursive: true });\n await rotateLogIfNeeded(logPath);\n const event: HookDiagnosticsEvent = {\n ts: new Date().toISOString(),\n hook: params.hook,\n pluginVersion: pluginMetadata.version,\n pid: process.pid,\n sessionId: params.sessionId?.trim() || null,\n turnId: params.turnId?.trim() || null,\n stage: params.stage.trim(),\n result: params.result,\n reason: params.reason?.trim() || null,\n toolName: params.toolName?.trim() || null,\n repoRoot: params.repoRoot?.trim() || null,\n message: params.message?.trim() || null,\n fields: normalizeFields(params.fields),\n };\n await fs.appendFile(logPath, `${JSON.stringify(event)}\\n`, \"utf8\");\n } catch {\n // Diagnostics are best-effort and must never break hook execution.\n }\n}\n\nasync function readEventsFromFile(filePath: string): Promise<HookDiagnosticsEvent[]> {\n const raw = await fs.readFile(filePath, \"utf8\").catch(() => null);\n if (!raw) return [];\n return raw\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter(Boolean)\n .flatMap((line) => {\n try {\n const parsed = JSON.parse(line) as HookDiagnosticsEvent;\n return parsed && typeof parsed === \"object\" ? [parsed] : [];\n } catch {\n return [];\n }\n });\n}\n\nexport async function readRecentHookDiagnosticsEvents(limit?: number): Promise<HookDiagnosticsEvent[]> {\n const eventLimit = clampEventLimit(limit);\n const logPath = getHookDiagnosticsLogPath();\n const [rotated, current] = await Promise.all([readEventsFromFile(`${logPath}.1`), readEventsFromFile(logPath)]);\n return [...rotated, ...current].slice(-eventLimit);\n}\n\nexport async function readHookDiagnosticsReport(limit?: number): Promise<HookDiagnosticsReport> {\n const [recentEvents, pendingStates] = await Promise.all([\n readRecentHookDiagnosticsEvents(limit),\n listPendingTurnStateSummaries(),\n ]);\n\n return {\n logPath: getHookDiagnosticsLogPath(),\n stateRoot: getPendingTurnStateRootPath(),\n recentEvents,\n pendingStates,\n };\n}\n","import fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nimport type { TurnIntent } from \"./history-routing.js\";\n\nexport type RepoRecordMode = \"changed_turn\" | \"no_diff_turn\";\nexport type ManualRecordingScope = \"change_step\" | \"full_turn\";\n\nexport type TouchedRepoState = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n firstTouchedAt: string;\n lastTouchedAt: string;\n lastObservedWriteAt: string | null;\n touchedBy: string[];\n hasObservedWrite: boolean;\n manuallyRecorded: boolean;\n manuallyRecordedAt: string | null;\n manuallyRecordedByTool: string | null;\n manualRecordingScope: ManualRecordingScope | null;\n manualRemoteChangeRecordedAt: string | null;\n stopAttempted: boolean;\n stopRecorded: boolean;\n stopRecordedAt: string | null;\n stopRecordedMode: RepoRecordMode | null;\n recordingFailureMessage: string | null;\n recordingFailureHint: string | null;\n recordingFailedAt: string | null;\n};\n\nexport type PendingTurnState = {\n sessionId: string;\n turnId: string;\n prompt: string;\n initialCwd: string | null;\n intent: TurnIntent;\n submittedAt: string;\n consultedMemory: boolean;\n touchedRepos: Record<string, TouchedRepoState>;\n turnFailureMessage: string | null;\n turnFailureHint: string | null;\n turnFailedAt: string | null;\n};\n\nexport type PendingTouchedRepoSummary = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n lastTouchedAt: string;\n lastObservedWriteAt: string | null;\n touchedBy: string[];\n hasObservedWrite: boolean;\n manuallyRecorded: boolean;\n manualRecordingScope: ManualRecordingScope | null;\n stopAttempted: boolean;\n stopRecorded: boolean;\n stopRecordedMode: RepoRecordMode | null;\n recordingFailureMessage: string | null;\n recordingFailureHint: string | null;\n recordingFailedAt: string | null;\n};\n\nexport type PendingTurnStateSummary = {\n sessionId: string;\n turnId: string;\n initialCwd: string | null;\n submittedAt: string;\n consultedMemory: boolean;\n promptLength: number;\n touchedRepoCount: number;\n turnFailureMessage: string | null;\n turnFailureHint: string | null;\n turnFailedAt: string | null;\n touchedRepos: PendingTouchedRepoSummary[];\n};\n\nfunction stateRoot(): string {\n const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();\n return configured || path.join(os.tmpdir(), \"remix-claude-plugin-hooks\");\n}\n\nfunction statePath(sessionId: string): string {\n return path.join(stateRoot(), `${sessionId}.json`);\n}\n\nfunction stateLockPath(sessionId: string): string {\n return path.join(stateRoot(), `${sessionId}.lock`);\n}\n\nfunction stateLockMetaPath(sessionId: string): string {\n return path.join(stateLockPath(sessionId), \"owner.json\");\n}\n\nasync function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;\n await fs.writeFile(tmpPath, JSON.stringify(value, null, 2) + \"\\n\", \"utf8\");\n await fs.rename(tmpPath, filePath);\n}\n\nconst STATE_LOCK_WAIT_MS = 2_000;\nconst STATE_LOCK_POLL_MS = 25;\nconst STATE_LOCK_STALE_MS = 30_000;\nconst STATE_LOCK_HEARTBEAT_MS = 5_000;\n\ntype StateLockMetadata = {\n ownerId: string;\n pid: number;\n createdAt: string;\n heartbeatAt: string;\n};\n\nasync function sleep(ms: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function readStateLockMetadata(sessionId: string): Promise<StateLockMetadata | null> {\n const raw = await fs.readFile(stateLockMetaPath(sessionId), \"utf8\").catch(() => null);\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<StateLockMetadata>;\n if (\n typeof parsed.ownerId !== \"string\" ||\n typeof parsed.pid !== \"number\" ||\n typeof parsed.createdAt !== \"string\" ||\n typeof parsed.heartbeatAt !== \"string\"\n ) {\n return null;\n }\n return {\n ownerId: parsed.ownerId,\n pid: parsed.pid,\n createdAt: parsed.createdAt,\n heartbeatAt: parsed.heartbeatAt,\n };\n } catch {\n return null;\n }\n}\n\nasync function writeStateLockMetadata(sessionId: string, metadata: StateLockMetadata): Promise<void> {\n await writeJsonAtomic(stateLockMetaPath(sessionId), metadata);\n}\n\nasync function tryRemoveStaleStateLock(sessionId: string): Promise<boolean> {\n const lockPath = stateLockPath(sessionId);\n const metadata = await readStateLockMetadata(sessionId);\n const staleByHeartbeat =\n metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;\n if (staleByHeartbeat) {\n await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n return true;\n }\n\n if (!metadata) {\n const lockStat = await fs.stat(lockPath).catch(() => null);\n if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {\n await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n return true;\n }\n }\n\n return false;\n}\n\nasync function acquireStateLock(sessionId: string): Promise<() => Promise<void>> {\n const lockPath = stateLockPath(sessionId);\n const deadline = Date.now() + STATE_LOCK_WAIT_MS;\n await fs.mkdir(stateRoot(), { recursive: true });\n\n while (true) {\n try {\n await fs.mkdir(lockPath);\n const ownerId = randomUUID();\n const createdAt = new Date().toISOString();\n const metadata: StateLockMetadata = {\n ownerId,\n pid: process.pid,\n createdAt,\n heartbeatAt: createdAt,\n };\n await writeStateLockMetadata(sessionId, metadata);\n let released = false;\n const heartbeat = setInterval(() => {\n if (released) return;\n void writeStateLockMetadata(sessionId, {\n ...metadata,\n heartbeatAt: new Date().toISOString(),\n }).catch(() => undefined);\n }, STATE_LOCK_HEARTBEAT_MS);\n heartbeat.unref?.();\n\n return async () => {\n if (released) return;\n released = true;\n clearInterval(heartbeat);\n const currentMetadata = await readStateLockMetadata(sessionId);\n if (currentMetadata?.ownerId === ownerId) {\n await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n }\n };\n } catch (error) {\n const code = error && typeof error === \"object\" && \"code\" in error ? (error as { code?: unknown }).code : null;\n if (code !== \"EEXIST\") {\n throw error;\n }\n\n if (await tryRemoveStaleStateLock(sessionId)) {\n continue;\n }\n\n if (Date.now() >= deadline) {\n throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);\n }\n await sleep(STATE_LOCK_POLL_MS);\n }\n }\n}\n\nasync function withStateLock<T>(sessionId: string, fn: () => Promise<T>): Promise<T> {\n const release = await acquireStateLock(sessionId);\n try {\n return await fn();\n } finally {\n await release();\n }\n}\n\nfunction normalizeIntent(value: unknown): TurnIntent {\n return value === \"memory_first\" || value === \"collab_state\" || value === \"git_facts\" ? value : \"neutral\";\n}\n\nfunction normalizeString(value: unknown): string | null {\n return typeof value === \"string\" && value.trim() ? value.trim() : null;\n}\n\nfunction normalizeStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return Array.from(\n new Set(\n value\n .filter((entry): entry is string => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => entry.trim()),\n ),\n );\n}\n\nfunction normalizeTouchedRepo(value: unknown, repoRoot: string): TouchedRepoState | null {\n if (!value || typeof value !== \"object\") return null;\n const parsed = value as Partial<TouchedRepoState>;\n const normalizedRepoRoot = normalizeString(parsed.repoRoot) ?? repoRoot.trim();\n if (!normalizedRepoRoot) return null;\n\n return {\n repoRoot: normalizedRepoRoot,\n projectId: normalizeString(parsed.projectId),\n currentAppId: normalizeString(parsed.currentAppId),\n upstreamAppId: normalizeString(parsed.upstreamAppId),\n firstTouchedAt: normalizeString(parsed.firstTouchedAt) ?? new Date().toISOString(),\n lastTouchedAt: normalizeString(parsed.lastTouchedAt) ?? new Date().toISOString(),\n lastObservedWriteAt: normalizeString(parsed.lastObservedWriteAt),\n touchedBy: normalizeStringArray(parsed.touchedBy),\n hasObservedWrite: Boolean(parsed.hasObservedWrite),\n manuallyRecorded: Boolean(parsed.manuallyRecorded),\n manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),\n manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),\n manualRecordingScope:\n parsed.manualRecordingScope === \"change_step\" || parsed.manualRecordingScope === \"full_turn\"\n ? parsed.manualRecordingScope\n : null,\n manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),\n stopAttempted: Boolean(parsed.stopAttempted),\n stopRecorded: Boolean(parsed.stopRecorded),\n stopRecordedAt: normalizeString(parsed.stopRecordedAt),\n stopRecordedMode: parsed.stopRecordedMode === \"changed_turn\" || parsed.stopRecordedMode === \"no_diff_turn\" ? parsed.stopRecordedMode : null,\n recordingFailureMessage: normalizeString(parsed.recordingFailureMessage),\n recordingFailureHint: normalizeString(parsed.recordingFailureHint),\n recordingFailedAt: normalizeString(parsed.recordingFailedAt),\n };\n}\n\nfunction normalizeTouchedRepos(value: unknown): Record<string, TouchedRepoState> {\n if (!value || typeof value !== \"object\") return {};\n const entries = Object.entries(value as Record<string, unknown>)\n .map(([repoRoot, repo]) => normalizeTouchedRepo(repo, repoRoot))\n .filter((repo): repo is TouchedRepoState => repo !== null)\n .sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));\n return Object.fromEntries(entries.map((repo) => [repo.repoRoot, repo]));\n}\n\nfunction createTouchedRepo(params: {\n repoRoot: string;\n projectId?: string | null;\n currentAppId?: string | null;\n upstreamAppId?: string | null;\n touchedBy?: string | null;\n hasObservedWrite?: boolean;\n}): TouchedRepoState {\n const now = new Date().toISOString();\n const touchedBy = params.touchedBy?.trim() ? [params.touchedBy.trim()] : [];\n return {\n repoRoot: params.repoRoot,\n projectId: normalizeString(params.projectId),\n currentAppId: normalizeString(params.currentAppId),\n upstreamAppId: normalizeString(params.upstreamAppId),\n firstTouchedAt: now,\n lastTouchedAt: now,\n lastObservedWriteAt: params.hasObservedWrite ? now : null,\n touchedBy,\n hasObservedWrite: Boolean(params.hasObservedWrite),\n manuallyRecorded: false,\n manuallyRecordedAt: null,\n manuallyRecordedByTool: null,\n manualRecordingScope: null,\n manualRemoteChangeRecordedAt: null,\n stopAttempted: false,\n stopRecorded: false,\n stopRecordedAt: null,\n stopRecordedMode: null,\n recordingFailureMessage: null,\n recordingFailureHint: null,\n recordingFailedAt: null,\n };\n}\n\nasync function updatePendingTurnState(\n sessionId: string,\n updater: (state: PendingTurnState) => void | boolean,\n): Promise<PendingTurnState | null> {\n return withStateLock(sessionId, async () => {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing) return null;\n const result = updater(existing);\n if (result === false) return existing;\n await savePendingTurnState(existing);\n return existing;\n });\n}\n\nfunction summarizeTouchedRepo(repo: TouchedRepoState): PendingTouchedRepoSummary {\n return {\n repoRoot: repo.repoRoot,\n projectId: repo.projectId,\n currentAppId: repo.currentAppId,\n upstreamAppId: repo.upstreamAppId,\n lastTouchedAt: repo.lastTouchedAt,\n lastObservedWriteAt: repo.lastObservedWriteAt,\n touchedBy: [...repo.touchedBy],\n hasObservedWrite: repo.hasObservedWrite,\n manuallyRecorded: repo.manuallyRecorded,\n manualRecordingScope: repo.manualRecordingScope,\n stopAttempted: repo.stopAttempted,\n stopRecorded: repo.stopRecorded,\n stopRecordedMode: repo.stopRecordedMode,\n recordingFailureMessage: repo.recordingFailureMessage,\n recordingFailureHint: repo.recordingFailureHint,\n recordingFailedAt: repo.recordingFailedAt,\n };\n}\n\nfunction summarizePendingTurnState(state: PendingTurnState): PendingTurnStateSummary {\n const touchedRepos = Object.values(state.touchedRepos)\n .sort((a, b) => a.repoRoot.localeCompare(b.repoRoot))\n .map((repo) => summarizeTouchedRepo(repo));\n return {\n sessionId: state.sessionId,\n turnId: state.turnId,\n initialCwd: state.initialCwd,\n submittedAt: state.submittedAt,\n consultedMemory: state.consultedMemory,\n promptLength: state.prompt.length,\n touchedRepoCount: touchedRepos.length,\n turnFailureMessage: state.turnFailureMessage,\n turnFailureHint: state.turnFailureHint,\n turnFailedAt: state.turnFailedAt,\n touchedRepos,\n };\n}\n\nexport function getPendingTurnStateRootPath(): string {\n return stateRoot();\n}\n\nexport async function loadPendingTurnState(sessionId: string): Promise<PendingTurnState | null> {\n const raw = await fs.readFile(statePath(sessionId), \"utf8\").catch(() => null);\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<PendingTurnState>;\n if (!parsed || typeof parsed !== \"object\") return null;\n if (typeof parsed.sessionId !== \"string\" || typeof parsed.turnId !== \"string\" || typeof parsed.prompt !== \"string\") {\n return null;\n }\n return {\n sessionId: parsed.sessionId,\n turnId: parsed.turnId,\n prompt: parsed.prompt,\n initialCwd: normalizeString(parsed.initialCwd),\n intent: normalizeIntent(parsed.intent),\n submittedAt: typeof parsed.submittedAt === \"string\" ? parsed.submittedAt : new Date().toISOString(),\n consultedMemory: Boolean(parsed.consultedMemory),\n touchedRepos: normalizeTouchedRepos(parsed.touchedRepos),\n turnFailureMessage: normalizeString(parsed.turnFailureMessage),\n turnFailureHint: normalizeString(parsed.turnFailureHint),\n turnFailedAt: normalizeString(parsed.turnFailedAt),\n };\n } catch {\n return null;\n }\n}\n\nexport async function savePendingTurnState(state: PendingTurnState): Promise<void> {\n await writeJsonAtomic(statePath(state.sessionId), state);\n}\n\nexport async function createPendingTurnState(params: {\n sessionId: string;\n prompt: string;\n initialCwd?: string | null;\n intent: TurnIntent;\n}): Promise<PendingTurnState> {\n return withStateLock(params.sessionId, async () => {\n const state: PendingTurnState = {\n sessionId: params.sessionId,\n turnId: randomUUID(),\n prompt: params.prompt,\n initialCwd: params.initialCwd?.trim() || null,\n intent: params.intent,\n submittedAt: new Date().toISOString(),\n consultedMemory: false,\n touchedRepos: {},\n turnFailureMessage: null,\n turnFailureHint: null,\n turnFailedAt: null,\n };\n await savePendingTurnState(state);\n return state;\n });\n}\n\nexport async function upsertTouchedRepo(\n sessionId: string,\n params: {\n repoRoot: string;\n projectId?: string | null;\n currentAppId?: string | null;\n upstreamAppId?: string | null;\n touchedBy?: string | null;\n hasObservedWrite?: boolean;\n },\n): Promise<TouchedRepoState | null> {\n const normalizedRepoRoot = params.repoRoot.trim();\n if (!normalizedRepoRoot) return null;\n const state = await updatePendingTurnState(sessionId, (existing) => {\n const current =\n existing.touchedRepos[normalizedRepoRoot] ??\n createTouchedRepo({\n repoRoot: normalizedRepoRoot,\n projectId: params.projectId,\n currentAppId: params.currentAppId,\n upstreamAppId: params.upstreamAppId,\n touchedBy: params.touchedBy,\n hasObservedWrite: params.hasObservedWrite,\n });\n\n current.projectId = normalizeString(params.projectId) ?? current.projectId;\n current.currentAppId = normalizeString(params.currentAppId) ?? current.currentAppId;\n current.upstreamAppId = normalizeString(params.upstreamAppId) ?? current.upstreamAppId;\n current.lastTouchedAt = new Date().toISOString();\n if (params.touchedBy?.trim() && !current.touchedBy.includes(params.touchedBy.trim())) {\n current.touchedBy = [...current.touchedBy, params.touchedBy.trim()].sort((a, b) => a.localeCompare(b));\n }\n if (params.hasObservedWrite) {\n current.hasObservedWrite = true;\n current.lastObservedWriteAt = new Date().toISOString();\n }\n existing.touchedRepos[normalizedRepoRoot] = current;\n });\n return state?.touchedRepos[normalizedRepoRoot] ?? null;\n}\n\nexport async function markTouchedRepoObservedWrite(\n sessionId: string,\n repoRoot: string,\n params?: { toolName?: string | null },\n): Promise<void> {\n await upsertTouchedRepo(sessionId, {\n repoRoot,\n touchedBy: params?.toolName ?? null,\n hasObservedWrite: true,\n });\n}\n\nexport async function markTouchedRepoManuallyRecorded(\n sessionId: string,\n repoRoot: string,\n params?: { toolName?: string | null; scope?: ManualRecordingScope | null; remoteChangeRecorded?: boolean },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const normalizedRepoRoot = repoRoot.trim();\n if (!normalizedRepoRoot) return false;\n const current =\n existing.touchedRepos[normalizedRepoRoot] ??\n createTouchedRepo({\n repoRoot: normalizedRepoRoot,\n touchedBy: params?.toolName ?? null,\n hasObservedWrite: false,\n });\n current.lastTouchedAt = new Date().toISOString();\n current.manuallyRecorded = true;\n current.manuallyRecordedAt = new Date().toISOString();\n current.manuallyRecordedByTool = normalizeString(params?.toolName) ?? current.manuallyRecordedByTool;\n current.manualRecordingScope = params?.scope ?? current.manualRecordingScope;\n if (params?.remoteChangeRecorded) {\n current.manualRemoteChangeRecordedAt = current.manuallyRecordedAt;\n }\n current.recordingFailureMessage = null;\n current.recordingFailureHint = null;\n current.recordingFailedAt = null;\n if (params?.toolName?.trim() && !current.touchedBy.includes(params.toolName.trim())) {\n current.touchedBy = [...current.touchedBy, params.toolName.trim()].sort((a, b) => a.localeCompare(b));\n }\n existing.touchedRepos[normalizedRepoRoot] = current;\n });\n}\n\nexport async function markPendingTurnConsultedMemory(sessionId: string): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n if (existing.consultedMemory) return false;\n existing.consultedMemory = true;\n });\n}\n\nexport async function markTouchedRepoStopAttempted(sessionId: string, repoRoot: string): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const current = existing.touchedRepos[repoRoot];\n if (!current) return false;\n current.stopAttempted = true;\n current.lastTouchedAt = new Date().toISOString();\n });\n}\n\nexport async function markTouchedRepoStopRecorded(\n sessionId: string,\n repoRoot: string,\n params: {\n mode: RepoRecordMode;\n },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const current = existing.touchedRepos[repoRoot];\n if (!current) return false;\n current.stopAttempted = true;\n current.stopRecorded = true;\n current.stopRecordedAt = new Date().toISOString();\n current.stopRecordedMode = params.mode;\n current.recordingFailureMessage = null;\n current.recordingFailureHint = null;\n current.recordingFailedAt = null;\n current.lastTouchedAt = new Date().toISOString();\n });\n}\n\nexport async function markTouchedRepoRecordingFailure(\n sessionId: string,\n repoRoot: string,\n params: {\n message: string;\n hint?: string | null;\n },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const current = existing.touchedRepos[repoRoot];\n if (!current) return false;\n current.stopAttempted = true;\n current.recordingFailureMessage = params.message.trim();\n current.recordingFailureHint = params.hint?.trim() || null;\n current.recordingFailedAt = new Date().toISOString();\n current.lastTouchedAt = new Date().toISOString();\n });\n}\n\nexport async function markPendingTurnFailure(\n sessionId: string,\n params: {\n message: string;\n hint?: string | null;\n },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n existing.turnFailureMessage = params.message.trim();\n existing.turnFailureHint = params.hint?.trim() || null;\n existing.turnFailedAt = new Date().toISOString();\n });\n}\n\nexport async function listTouchedRepos(sessionId: string): Promise<TouchedRepoState[]> {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing) return [];\n return Object.values(existing.touchedRepos).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));\n}\n\nexport async function listPendingTurnStateSummaries(): Promise<PendingTurnStateSummary[]> {\n const root = stateRoot();\n const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);\n const sessionIds = entries\n .filter((entry) => entry.isFile() && entry.name.endsWith(\".json\"))\n .map((entry) => entry.name.replace(/\\.json$/, \"\"))\n .sort((a, b) => a.localeCompare(b));\n const states = await Promise.all(sessionIds.map((sessionId) => loadPendingTurnState(sessionId)));\n return states\n .filter((state): state is PendingTurnState => state !== null)\n .sort((a, b) => b.submittedAt.localeCompare(a.submittedAt))\n .map((state) => summarizePendingTurnState(state));\n}\n\nexport async function clearPendingTurnState(sessionId: string): Promise<void> {\n await withStateLock(sessionId, async () => {\n await fs.rm(statePath(sessionId), { force: true }).catch(() => undefined);\n });\n}\n","{\n \"name\": \"@remixhq/claude-plugin\",\n \"version\": \"0.1.12\",\n \"description\": \"Claude Code plugin for Remix collaboration workflows\",\n \"homepage\": \"https://github.com/RemixDotOne/remix-claude-plugin\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/RemixDotOne/remix-claude-plugin.git\"\n },\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"files\": [\n \"dist\",\n \".claude-plugin/plugin.json\",\n \".mcp.json\",\n \"skills\",\n \"hooks\",\n \"agents\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"postbuild\": \"node -e \\\"const fs=require('node:fs'); for (const p of ['dist/mcp-server.cjs','dist/hook-pre-git.cjs','dist/hook-user-prompt.cjs','dist/hook-post-collab.cjs','dist/hook-stop-collab.cjs']) fs.chmodSync(p, 0o755);\\\"\",\n \"dev\": \"tsx src/mcp-server.ts\",\n \"typecheck\": \"tsc -p tsconfig.json --noEmit\",\n \"prepack\": \"npm run build\"\n },\n \"dependencies\": {\n \"@remixhq/core\": \"^0.1.8\",\n \"@remixhq/mcp\": \"^0.1.8\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^25.4.0\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.9.3\"\n }\n}\n","import pkg from \"../package.json\";\n\nexport const pluginMetadata = {\n name: pkg.name,\n version: pkg.version,\n description: pkg.description,\n pluginId: \"remix\",\n agentName: \"remix-collab\",\n};\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { readCollabBinding } from \"@remixhq/core/binding\";\n\ntype BindingSummary = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n};\n\nexport async function readJsonStdin(): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));\n }\n const raw = Buffer.concat(chunks).toString(\"utf8\").trim();\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw);\n return parsed && typeof parsed === \"object\" ? (parsed as Record<string, unknown>) : {};\n } catch {\n return {};\n }\n}\n\nfunction getNestedRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === \"object\" ? (value as Record<string, unknown>) : null;\n}\n\nexport function extractToolInput(payload: Record<string, unknown>): Record<string, unknown> {\n return getNestedRecord(payload.tool_input) ?? getNestedRecord(payload.toolInput) ?? payload;\n}\n\nexport function extractToolResponse(payload: Record<string, unknown>): Record<string, unknown> | null {\n return getNestedRecord(payload.tool_response) ?? getNestedRecord(payload.toolResponse);\n}\n\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"],"mappings":"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","names":["import_node_crypto","import_promises","import_node_os","import_node_path","path","os","fs","path","os","fs","path","import_promises","import_node_path","path","fs"]}
@@ -1,2 +1,3 @@
1
+ declare function runHookUserPrompt(payload: Record<string, unknown>): Promise<void>;
1
2
 
2
- export { }
3
+ export { runHookUserPrompt };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@remixhq/claude-plugin",
6
- version: "0.1.10",
6
+ version: "0.1.12",
7
7
  description: "Claude Code plugin for Remix collaboration workflows",
8
8
  homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
9
9
  license: "MIT",
@@ -34,8 +34,8 @@ var package_default = {
34
34
  prepack: "npm run build"
35
35
  },
36
36
  dependencies: {
37
- "@remixhq/core": "^0.1.7",
38
- "@remixhq/mcp": "^0.1.7"
37
+ "@remixhq/core": "^0.1.8",
38
+ "@remixhq/mcp": "^0.1.8"
39
39
  },
40
40
  devDependencies: {
41
41
  "@types/node": "^25.4.0",