@lore-co/cli 0.2.4 → 0.2.7

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.
Files changed (113) hide show
  1. package/README.md +10 -2
  2. package/dist/cli.d.ts +10 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +109 -27
  5. package/dist/cli.js.map +1 -1
  6. package/dist/command-help.d.ts +4 -1
  7. package/dist/command-help.d.ts.map +1 -1
  8. package/dist/command-help.js +43 -0
  9. package/dist/command-help.js.map +1 -1
  10. package/dist/config-command.d.ts +2 -0
  11. package/dist/config-command.d.ts.map +1 -0
  12. package/dist/config-command.js +46 -0
  13. package/dist/config-command.js.map +1 -0
  14. package/dist/connections.d.ts +28 -0
  15. package/dist/connections.d.ts.map +1 -0
  16. package/dist/connections.js +68 -0
  17. package/dist/connections.js.map +1 -0
  18. package/dist/devin-capture.d.ts +3 -1
  19. package/dist/devin-capture.d.ts.map +1 -1
  20. package/dist/devin-capture.js +17 -5
  21. package/dist/devin-capture.js.map +1 -1
  22. package/dist/devin-cli.d.ts.map +1 -1
  23. package/dist/devin-cli.js +12 -9
  24. package/dist/devin-cli.js.map +1 -1
  25. package/dist/directory-policy.d.ts +2 -0
  26. package/dist/directory-policy.d.ts.map +1 -0
  27. package/dist/directory-policy.js +2 -0
  28. package/dist/directory-policy.js.map +1 -0
  29. package/dist/generated-assets.d.ts +4 -4
  30. package/dist/generated-assets.d.ts.map +1 -1
  31. package/dist/generated-assets.js +4 -4
  32. package/dist/generated-assets.js.map +1 -1
  33. package/dist/guard.js +2 -2
  34. package/dist/guard.js.map +1 -1
  35. package/dist/host.d.ts +2 -1
  36. package/dist/host.d.ts.map +1 -1
  37. package/dist/host.js +17 -5
  38. package/dist/host.js.map +1 -1
  39. package/dist/image-capture.d.ts +8 -1
  40. package/dist/image-capture.d.ts.map +1 -1
  41. package/dist/image-capture.js +30 -19
  42. package/dist/image-capture.js.map +1 -1
  43. package/dist/invocation-health-writer.d.ts +1 -0
  44. package/dist/invocation-health-writer.d.ts.map +1 -1
  45. package/dist/invocation-health-writer.js +3 -0
  46. package/dist/invocation-health-writer.js.map +1 -1
  47. package/dist/live.d.ts.map +1 -1
  48. package/dist/live.js +45 -9
  49. package/dist/live.js.map +1 -1
  50. package/dist/recovery-cloud-receipt.d.ts.map +1 -1
  51. package/dist/recovery-cloud-receipt.js +36 -11
  52. package/dist/recovery-cloud-receipt.js.map +1 -1
  53. package/dist/recovery-cloud-workspace.d.ts.map +1 -1
  54. package/dist/recovery-cloud-workspace.js +4 -1
  55. package/dist/recovery-cloud-workspace.js.map +1 -1
  56. package/dist/recovery-cloud.d.ts +1 -0
  57. package/dist/recovery-cloud.d.ts.map +1 -1
  58. package/dist/recovery-cloud.js +45 -17
  59. package/dist/recovery-cloud.js.map +1 -1
  60. package/dist/recovery-codex.d.ts.map +1 -1
  61. package/dist/recovery-codex.js +2 -0
  62. package/dist/recovery-codex.js.map +1 -1
  63. package/dist/recovery-connection.d.ts +11 -0
  64. package/dist/recovery-connection.d.ts.map +1 -0
  65. package/dist/recovery-connection.js +35 -0
  66. package/dist/recovery-connection.js.map +1 -0
  67. package/dist/recovery-copilot.d.ts.map +1 -1
  68. package/dist/recovery-copilot.js +3 -0
  69. package/dist/recovery-copilot.js.map +1 -1
  70. package/dist/recovery-desktop.d.ts.map +1 -1
  71. package/dist/recovery-desktop.js +4 -0
  72. package/dist/recovery-desktop.js.map +1 -1
  73. package/dist/recovery-external.d.ts.map +1 -1
  74. package/dist/recovery-external.js +3 -0
  75. package/dist/recovery-external.js.map +1 -1
  76. package/dist/recovery-hosts.d.ts.map +1 -1
  77. package/dist/recovery-hosts.js +4 -6
  78. package/dist/recovery-hosts.js.map +1 -1
  79. package/dist/recovery-polytoken.d.ts.map +1 -1
  80. package/dist/recovery-polytoken.js +8 -8
  81. package/dist/recovery-polytoken.js.map +1 -1
  82. package/dist/recovery-remote.d.ts.map +1 -1
  83. package/dist/recovery-remote.js +22 -9
  84. package/dist/recovery-remote.js.map +1 -1
  85. package/dist/recovery-state.d.ts +2 -1
  86. package/dist/recovery-state.d.ts.map +1 -1
  87. package/dist/recovery-state.js +18 -3
  88. package/dist/recovery-state.js.map +1 -1
  89. package/dist/recovery-transfer.d.ts.map +1 -1
  90. package/dist/recovery-transfer.js +17 -9
  91. package/dist/recovery-transfer.js.map +1 -1
  92. package/dist/recovery.d.ts.map +1 -1
  93. package/dist/recovery.js +12 -18
  94. package/dist/recovery.js.map +1 -1
  95. package/dist/reliability-store.d.ts +5 -0
  96. package/dist/reliability-store.d.ts.map +1 -1
  97. package/dist/reliability-store.js +31 -3
  98. package/dist/reliability-store.js.map +1 -1
  99. package/dist/repository.d.ts +1 -3
  100. package/dist/repository.d.ts.map +1 -1
  101. package/dist/repository.js +1 -65
  102. package/dist/repository.js.map +1 -1
  103. package/dist/runtime.d.ts.map +1 -1
  104. package/dist/runtime.js +104 -39
  105. package/dist/runtime.js.map +1 -1
  106. package/dist/self-host.d.ts +1 -1
  107. package/dist/self-host.js +2 -2
  108. package/dist/stdin.d.ts +3 -0
  109. package/dist/stdin.d.ts.map +1 -0
  110. package/dist/stdin.js +21 -0
  111. package/dist/stdin.js.map +1 -0
  112. package/dist/update.js +3 -3
  113. package/package.json +7 -3
package/dist/runtime.js CHANGED
@@ -6,12 +6,13 @@ import { dirname, parse, relative, resolve } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  import { promisify } from "node:util";
9
- import { boundedUtf8Text, repositoryScopeFromGitRoot, } from "./repository.js";
9
+ import { boundedUtf8Text, repositoryScopeFromGitRoot, repositoryScopeFromDirectory, } from "./repository.js";
10
10
  import { ReliabilityStore, ReliabilityStoreError, atomicWriteJson as durableAtomicWriteJson, classifyRetryFailure, createIntegrationInvocationAttempt, durableUnlink, recordLocalGuardMetric, recordLocalRetrievalMetric, } from "./reliability-store.js";
11
11
  import { writeInvocationAttemptBounded, writeInvocationCompletionBounded, } from "./invocation-health-writer.js";
12
12
  import { SignedSnapshotError, verifySignedSnapshot, } from "./signed-snapshot.js";
13
13
  import { CONTEXT_CACHE_MAX_STALE_MS, isContextTransportFailure, selectCachedContext, } from "./context-fallback.js";
14
14
  import { RUNTIME_VERSION } from "./runtime-version.js";
15
+ import { checkLoreAccess, checkDirectory, checkWorkspaceRepository, readLocalConfig, resolveLocalConnection, bindLocalSession } from "./directory-policy.js";
15
16
  import { capturePendingImages, enqueueEventImages, flushImageAttachment, hostMessageImages, rememberImageCapture } from "./image-capture.js";
16
17
  import { guardRuleConflict, guardToolTrigger, supportedGuardRule, guardEmergencyCommand, emergencyOverrideApplies } from "@lore-co/core/guard-evaluation";
17
18
  const IS_STANDALONE_RUNTIME = typeof __LORE_STANDALONE__ === "boolean" && __LORE_STANDALONE__;
@@ -367,9 +368,11 @@ async function atomicWriteJson(path, value) {
367
368
  await chmod(dirname(path), 0o700);
368
369
  await durableAtomicWriteJson(path, value);
369
370
  }
370
- async function readRuntimeConfig(home) {
371
+ async function readRuntimeConfig(home, cwd) {
372
+ const saved = await readLocalConfig(home);
373
+ const selected = saved?.savedConnections && cwd !== undefined ? await resolveLocalConnection(cwd, home) : saved;
371
374
  try {
372
- const parsed = JSON.parse(await readFile(resolve(loreDirectory(home), "config.json"), "utf8"));
375
+ const parsed = selected;
373
376
  if (!isObject(parsed) ||
374
377
  (parsed.version !== 1 && parsed.version !== 2) ||
375
378
  typeof parsed.apiUrl !== "string" ||
@@ -394,6 +397,7 @@ async function readRuntimeConfig(home) {
394
397
  : undefined;
395
398
  return {
396
399
  version: parsed.version,
400
+ ...(typeof parsed.connectionId === "string" ? { connectionId: parsed.connectionId } : {}),
397
401
  apiUrl: parsed.apiUrl,
398
402
  ...(dashboardUrl === undefined ? {} : { dashboardUrl }),
399
403
  ...(workspaceId === undefined ? {} : { workspaceId }),
@@ -406,17 +410,17 @@ async function readRuntimeConfig(home) {
406
410
  return null;
407
411
  }
408
412
  }
409
- function pendingPath(agent, sessionId, home) {
410
- return resolve(loreDirectory(home), "state", "pending", `${sha256(`${agent}\0${sessionId}`)}.json`);
413
+ function pendingPath(agent, sessionId, home, connectionId) {
414
+ return resolve(loreDirectory(home), "state", "pending", `${sha256(`${connectionId ? `${connectionId}\0` : ""}${agent}\0${sessionId}`)}.json`);
411
415
  }
412
- async function savePending(input, agent, sessionId, now, home) {
416
+ async function savePending(input, agent, sessionId, now, home, connectionId) {
413
417
  const assistantMessage = stringField(input.last_assistant_message);
414
418
  if (assistantMessage === undefined) {
415
419
  return;
416
420
  }
417
421
  const cwd = stringField(input.cwd);
418
422
  const turnId = stringField(input.turn_id);
419
- await atomicWriteJson(pendingPath(agent, sessionId, home), {
423
+ await atomicWriteJson(pendingPath(agent, sessionId, home, connectionId), {
420
424
  agent,
421
425
  sessionId,
422
426
  assistantMessage: redactSecrets(assistantMessage),
@@ -425,8 +429,8 @@ async function savePending(input, agent, sessionId, now, home) {
425
429
  capturedAt: now.toISOString(),
426
430
  });
427
431
  }
428
- async function readPending(agent, sessionId, home) {
429
- const path = pendingPath(agent, sessionId, home);
432
+ async function readPending(agent, sessionId, home, connectionId) {
433
+ const path = pendingPath(agent, sessionId, home, connectionId);
430
434
  try {
431
435
  const parsed = JSON.parse(await readFile(path, "utf8"));
432
436
  if (!isObject(parsed) ||
@@ -456,8 +460,8 @@ async function readPending(agent, sessionId, home) {
456
460
  return null;
457
461
  }
458
462
  }
459
- async function clearPending(agent, sessionId, home) {
460
- await durableUnlink(pendingPath(agent, sessionId, home), true);
463
+ async function clearPending(agent, sessionId, home, connectionId) {
464
+ await durableUnlink(pendingPath(agent, sessionId, home, connectionId), true);
461
465
  }
462
466
  function normalizedRepositoryPath(value) {
463
467
  const parts = [];
@@ -983,10 +987,12 @@ function runtimeAccept(config) {
983
987
  async function runtimeStore(config, home) {
984
988
  const store = new ReliabilityStore(reliabilityWorkspaceKey(config), {
985
989
  home: homeDirectory(home),
990
+ ...(config.connectionId ? { connectionId: config.connectionId } : {}),
986
991
  });
987
992
  await store.initialize();
988
- await store.migrateLegacyQueue(queueDirectory(home));
989
- if (config.workspaceId !== undefined) {
993
+ if (!config.connectionId)
994
+ await store.migrateLegacyQueue(queueDirectory(home));
995
+ if (config.workspaceId !== undefined && !config.connectionId) {
990
996
  const credentialStore = new ReliabilityStore(credentialReliabilityWorkspaceKey(config), { home: homeDirectory(home) });
991
997
  await credentialStore.transferPendingTo(store).catch(() => undefined);
992
998
  }
@@ -1511,6 +1517,7 @@ async function flushOne(config, fetchImplementation, store) {
1511
1517
  const claimed = await store.claimNext({
1512
1518
  workerId: `native-hook:${process.pid}`,
1513
1519
  kinds: ["turn", "prompt", "guard_override"],
1520
+ eligible: async (entry) => entry.kind === "guard_override" || (await checkWorkspaceRepository(config, queuedRequest(entry.payload)?.request.scope?.repo, fetchImplementation)).allowed,
1514
1521
  });
1515
1522
  if (claimed === null || claimed.claim === undefined) {
1516
1523
  return null;
@@ -1541,6 +1548,9 @@ async function flushOne(config, fetchImplementation, store) {
1541
1548
  state: failed.state === "auth-blocked" ? "auth-blocked" : "dead",
1542
1549
  };
1543
1550
  }
1551
+ const permission = await checkWorkspaceRepository(config, queued.request.scope?.repo, fetchImplementation);
1552
+ if (!permission.allowed)
1553
+ throw Object.assign(new Error(permission.reason), { retryable: true });
1544
1554
  let delivery;
1545
1555
  if (queued.kind === "turn") {
1546
1556
  delivery = await postTurn(config, queued.request, fetchImplementation, store);
@@ -1631,12 +1641,12 @@ const GUARD_SHELL_TOOLS = new Set(["Bash", "bash", "Shell", "shell", "exec_comma
1631
1641
  export function guardTrigger(toolName, toolInput) {
1632
1642
  return guardToolTrigger(toolName, toolInput);
1633
1643
  }
1634
- function guardStatePath(agent, sessionId, home) {
1635
- return resolve(loreDirectory(home), "state", "guard", `${sha256(`${agent}\0${sessionId}`)}.json`);
1644
+ function guardStatePath(agent, sessionId, home, connectionId) {
1645
+ return resolve(loreDirectory(home), "state", "guard", `${sha256(`${connectionId ? `${connectionId}\0` : ""}${agent}\0${sessionId}`)}.json`);
1636
1646
  }
1637
- async function readGuardState(agent, sessionId, home) {
1647
+ async function readGuardState(agent, sessionId, home, connectionId) {
1638
1648
  try {
1639
- const parsed = JSON.parse(await readFile(guardStatePath(agent, sessionId, home), "utf8"));
1649
+ const parsed = JSON.parse(await readFile(guardStatePath(agent, sessionId, home, connectionId), "utf8"));
1640
1650
  if (isObject(parsed)) {
1641
1651
  return {
1642
1652
  ...(typeof parsed.mode === "string" ? { mode: parsed.mode } : {}),
@@ -1654,12 +1664,12 @@ async function readGuardState(agent, sessionId, home) {
1654
1664
  }
1655
1665
  return { keys: {} };
1656
1666
  }
1657
- async function writeGuardState(agent, sessionId, state, home) {
1667
+ async function writeGuardState(agent, sessionId, state, home, connectionId) {
1658
1668
  const entries = Object.entries(state.keys)
1659
1669
  .sort((a, b) => (a[1] < b[1] ? 1 : -1))
1660
1670
  .slice(0, GUARD_MAX_CACHED_KEYS);
1661
1671
  try {
1662
- await atomicWriteJson(guardStatePath(agent, sessionId, home), {
1672
+ await atomicWriteJson(guardStatePath(agent, sessionId, home, connectionId), {
1663
1673
  ...state,
1664
1674
  keys: Object.fromEntries(entries),
1665
1675
  });
@@ -1890,7 +1900,7 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
1890
1900
  return undefined;
1891
1901
  }
1892
1902
  const store = await runtimeStore(config, options.home);
1893
- const state = await readGuardState(agent, sessionId, options.home);
1903
+ const state = await readGuardState(agent, sessionId, options.home, config.connectionId);
1894
1904
  const rememberedPolicy = await store.readState("guard-policy-state");
1895
1905
  const enforcementMayHaveChanged = isObject(rememberedPolicy) && (rememberedPolicy.mode === "enforce" ||
1896
1906
  (Array.isArray(rememberedPolicy.overrides) && rememberedPolicy.overrides.some((entry) => isObject(entry) && entry.mode === "enforce")));
@@ -1971,7 +1981,7 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
1971
1981
  if (!result.requiresConfirmation && !result.policyUnavailable) {
1972
1982
  state.keys[key] = now.toISOString();
1973
1983
  }
1974
- await writeGuardState(agent, sessionId, state, options.home).catch(() => undefined);
1984
+ await writeGuardState(agent, sessionId, state, options.home, config.connectionId).catch(() => undefined);
1975
1985
  if (result.mode === "off") {
1976
1986
  return undefined;
1977
1987
  }
@@ -2040,10 +2050,45 @@ export async function handleHookEvent(value, agent, options = {}) {
2040
2050
  !validHookInput(input, agent, eventName)) {
2041
2051
  throw new InvalidHookInputError();
2042
2052
  }
2043
- const config = await readRuntimeConfig(options.home);
2053
+ let config;
2054
+ try {
2055
+ config = await readRuntimeConfig(options.home, stringField(input.cwd) ?? "");
2056
+ }
2057
+ catch (error) {
2058
+ if (eventName !== "UserPromptSubmit" && eventName !== "SessionStart")
2059
+ return undefined;
2060
+ const notice = `Lore is off: ${error instanceof Error ? error.message : "connection unavailable"}`;
2061
+ return agent === "cursor" ? { continue: true, userMessage: notice } : agent === "polytoken" ? { outcome: "accept", additional_context: notice } : { systemMessage: notice };
2062
+ }
2044
2063
  if (config === null || !config.agents.includes(agent)) {
2045
2064
  return undefined;
2046
2065
  }
2066
+ if (!await bindLocalSession(homeDirectory(options.home), agent, sessionId, config.connectionId)) {
2067
+ const notice = "Lore is paused for this conversation because its account or workspace changed. Start a new agent session.";
2068
+ return eventName === "UserPromptSubmit" ? (agent === "cursor" ? { continue: true, userMessage: notice } : { systemMessage: notice }) : undefined;
2069
+ }
2070
+ const access = await checkLoreAccess(stringField(input.cwd), options.home, config, options.fetch ?? globalThis.fetch);
2071
+ if (!access.allowed) {
2072
+ await clearPending(agent, sessionId, options.home, config.connectionId);
2073
+ // A policy outage must not silently bypass an already enforced Guard.
2074
+ if (eventName === "PreToolUse" && access.reason === "Workspace repository settings are unavailable") {
2075
+ const prior = await readGuardState(agent, sessionId, options.home, config.connectionId).catch(() => null);
2076
+ const saved = await runtimeStore(config, options.home).then(store => store.readState("guard-policy-state")).catch(() => null);
2077
+ const repo = stringField(input.cwd) ? await repositoryScopeFromDirectory(stringField(input.cwd)) : undefined;
2078
+ const override = isObject(saved) && Array.isArray(saved.overrides) ? saved.overrides.find(value => isObject(value) && value.repo === repo) : undefined;
2079
+ if (prior?.mode === "enforce" || (isObject(override) ? override.mode : isObject(saved) ? saved.mode : undefined) === "enforce") {
2080
+ const reason = "Lore cannot verify workspace repository settings. Restore the connection before retrying.";
2081
+ if (agent === "cursor")
2082
+ return { permission: "deny", userMessage: reason, agentMessage: reason };
2083
+ if (agent === "copilot-cli")
2084
+ return { permissionDecision: "deny", permissionDecisionReason: reason };
2085
+ if (agent === "polytoken")
2086
+ return { outcome: "deny", reason };
2087
+ return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } };
2088
+ }
2089
+ }
2090
+ return agent === "cursor" && eventName === "UserPromptSubmit" ? { continue: true } : undefined;
2091
+ }
2047
2092
  const now = (options.now ?? (() => new Date()))();
2048
2093
  if (agent === "cursor" && eventName === "SessionStart") {
2049
2094
  const cwd = stringField(input.cwd);
@@ -2060,7 +2105,7 @@ export async function handleHookEvent(value, agent, options = {}) {
2060
2105
  ].join("\n") };
2061
2106
  }
2062
2107
  if (eventName === "Stop" || eventName === "AssistantResponse") {
2063
- await savePending(input, agent, sessionId, now, options.home);
2108
+ await savePending(input, agent, sessionId, now, options.home, config.connectionId);
2064
2109
  {
2065
2110
  const store = await runtimeStore(config, options.home);
2066
2111
  const codexHome = (options.environment ?? process.env).CODEX_HOME ?? resolve(homeDirectory(options.home), ".codex");
@@ -2069,7 +2114,7 @@ export async function handleHookEvent(value, agent, options = {}) {
2069
2114
  const target = await store.readState(`image-event-${agent}-${sha256(sessionId)}`);
2070
2115
  if (isObject(target) && typeof target.eventId === "string") {
2071
2116
  await hostMessageImages(input.attachments ?? input.parts ?? input.content, "assistant")
2072
- .then(images => enqueueEventImages(store, { connector: "lore-cli", eventId: String(target.eventId), sessionId }, images)).catch(() => undefined);
2117
+ .then(images => enqueueEventImages(store, { connector: "lore-cli", eventId: String(target.eventId), sessionId, cwd: stringField(target.cwd) }, images)).catch(() => undefined);
2073
2118
  }
2074
2119
  await flushImageAttachment(store, config, options.fetch ?? globalThis.fetch).catch(() => undefined);
2075
2120
  }
@@ -2086,7 +2131,7 @@ export async function handleHookEvent(value, agent, options = {}) {
2086
2131
  return await handleGuardEvent(input, agent, config, sessionId, options, now);
2087
2132
  }
2088
2133
  catch {
2089
- const prior = await readGuardState(agent, sessionId, options.home).catch(() => null);
2134
+ const prior = await readGuardState(agent, sessionId, options.home, config.connectionId).catch(() => null);
2090
2135
  const saved = await runtimeStore(config, options.home).then((store) => store.readState("guard-policy-state")).catch(() => null);
2091
2136
  const scope = await repositoryScope(stringField(input.cwd)).catch(() => undefined);
2092
2137
  const override = isObject(saved) && Array.isArray(saved.overrides) ? saved.overrides.find((entry) => isObject(entry) && entry.repo === scope?.repo) : undefined;
@@ -2142,7 +2187,16 @@ function cursorSessionKey(sessionId) {
2142
2187
  }
2143
2188
  /** MCP requests are bound to a session/workspace observed by the native hook. */
2144
2189
  async function cursorSession(sessionId, options) {
2145
- const config = await readRuntimeConfig(options.home);
2190
+ const bindingPath = resolve(loreDirectory(options.home), "state", "connection-sessions", `${sha256(`cursor\0${sessionId}`)}.json`);
2191
+ const binding = await readFile(bindingPath, "utf8").then(value => JSON.parse(value)).catch(() => undefined);
2192
+ let config = await readRuntimeConfig(options.home);
2193
+ if (binding?.connectionId) {
2194
+ const connections = await import("./directory-policy.js").then(module => module.savedLocalConnections(options.home));
2195
+ const saved = connections.find(value => value.connectionId === binding.connectionId);
2196
+ if (!saved)
2197
+ throw new Error("This Cursor connection is no longer available. Start a new conversation.");
2198
+ config = { ...saved, version: 2, agents: Array.isArray(saved.agents) ? saved.agents.filter(isCommandHookAgent) : [] };
2199
+ }
2146
2200
  if (!config?.agents.includes("cursor"))
2147
2201
  throw new Error("Reconnect Cursor to Lore before requesting context.");
2148
2202
  const store = await runtimeStore(config, options.home);
@@ -2150,6 +2204,9 @@ async function cursorSession(sessionId, options) {
2150
2204
  if (!isObject(state) || state.sessionId !== sessionId || !stringField(state.cwd)) {
2151
2205
  throw new Error("Start a new Cursor conversation to register this Lore session.");
2152
2206
  }
2207
+ const access = await checkLoreAccess(stringField(state.cwd), options.home, config, options.fetch ?? globalThis.fetch);
2208
+ if (!access.allowed)
2209
+ throw new Error(`Lore is off: ${access.reason}.`);
2153
2210
  return { config, store, state, cwd: stringField(state.cwd) };
2154
2211
  }
2155
2212
  export async function getCursorContext(sessionId, prompt, options = {}) {
@@ -2162,9 +2219,9 @@ export async function getCursorContext(sessionId, prompt, options = {}) {
2162
2219
  return result?.additional_context || "No relevant shared knowledge was found for this request.";
2163
2220
  }
2164
2221
  export async function completeCursorResponse(sessionId, response, options = {}) {
2165
- const { store, state, cwd } = await cursorSession(sessionId, options);
2222
+ const { config, store, state, cwd } = await cursorSession(sessionId, options);
2166
2223
  const now = (options.now ?? (() => new Date()))();
2167
- await savePending({ cwd, last_assistant_message: response }, "cursor", sessionId, now, options.home);
2224
+ await savePending({ cwd, last_assistant_message: response }, "cursor", sessionId, now, options.home, config.connectionId);
2168
2225
  const { prompt: _prompt, promptId: _promptId, ...rest } = state;
2169
2226
  await store.writeState(cursorSessionKey(sessionId), { ...rest, updatedAt: now.toISOString() });
2170
2227
  }
@@ -2210,18 +2267,23 @@ async function handlePromptEvent(input, agent, sessionId, config, options, now,
2210
2267
  catch {
2211
2268
  notices.push("Lore retained queued captures locally, but replay could not run. Run lore doctor.");
2212
2269
  }
2213
- const pending = capture ? await readPending(agent, sessionId, options.home) : null;
2270
+ let pending = capture ? await readPending(agent, sessionId, options.home, config.connectionId) : null;
2271
+ if (pending && (!(await checkDirectory(pending.cwd, options.home)).allowed ||
2272
+ (pending.cwd !== undefined && pending.cwd !== stringField(input.cwd)))) {
2273
+ await clearPending(agent, sessionId, options.home, config.connectionId);
2274
+ pending = null;
2275
+ }
2214
2276
  const codexHome = (options.environment ?? process.env).CODEX_HOME ?? resolve(homeDirectory(options.home), ".codex");
2215
2277
  const claudeHome = (options.environment ?? process.env).CLAUDE_CONFIG_DIR ?? resolve(homeDirectory(options.home), ".claude");
2216
2278
  await capturePendingImages(store, sessionId, codexHome, claudeHome).catch(() => undefined);
2217
2279
  const rememberImages = async (eventId) => {
2218
2280
  if (!capture)
2219
2281
  return;
2220
- await store.writeState(`image-event-${agent}-${sha256(sessionId)}`, { eventId });
2221
- await enqueueEventImages(store, { connector: "lore-cli", eventId, sessionId }, await hostMessageImages(input.attachments ?? input.parts ?? input.content));
2282
+ await store.writeState(`image-event-${agent}-${sha256(sessionId)}`, { eventId, ...(stringField(input.cwd) ? { cwd: stringField(input.cwd) } : {}) });
2283
+ await enqueueEventImages(store, { connector: "lore-cli", eventId, sessionId, cwd: stringField(input.cwd) }, await hostMessageImages(input.attachments ?? input.parts ?? input.content));
2222
2284
  if (agent !== "codex" && agent !== "claude")
2223
2285
  return;
2224
- await rememberImageCapture(store, { eventId, sessionId, prompt, occurredAt: now.toISOString(), agent,
2286
+ await rememberImageCapture(store, { eventId, sessionId, prompt, occurredAt: now.toISOString(), agent, cwd: stringField(input.cwd),
2225
2287
  ...(typeof input.transcript_path === "string" ? { transcriptPath: input.transcript_path } : {}),
2226
2288
  });
2227
2289
  await capturePendingImages(store, sessionId, codexHome, claudeHome);
@@ -2239,7 +2301,7 @@ async function handlePromptEvent(input, agent, sessionId, config, options, now,
2239
2301
  await enqueueCapture(store, { kind: "turn", request });
2240
2302
  enqueued = true;
2241
2303
  await rememberImages(request.eventId).catch(() => undefined);
2242
- await clearPending(agent, sessionId, options.home);
2304
+ await clearPending(agent, sessionId, options.home, config.connectionId);
2243
2305
  }
2244
2306
  catch (error) {
2245
2307
  notices.push(error instanceof ReliabilityStoreError &&
@@ -2440,22 +2502,22 @@ function nativeIntegrationId(agent) {
2440
2502
  ? "preview/copilot-vscode"
2441
2503
  : `native/${agent}`;
2442
2504
  }
2443
- async function beginHookInvocation(agent) {
2444
- const config = await readRuntimeConfig();
2505
+ async function beginHookInvocation(agent, cwd) {
2506
+ const config = await readRuntimeConfig(undefined, cwd);
2445
2507
  if (config === null || !config.agents.includes(agent)) {
2446
2508
  return null;
2447
2509
  }
2448
2510
  const workspaceKey = reliabilityWorkspaceKey(config);
2449
2511
  const home = homeDirectory();
2450
2512
  const attempt = createIntegrationInvocationAttempt(nativeIntegrationId(agent), { runtimeVersion: RUNTIME_VERSION });
2451
- const recorded = await writeInvocationAttemptBounded(workspaceKey, attempt, { home, standalone: IS_STANDALONE_RUNTIME });
2452
- return recorded ? { workspaceKey, home, attempt } : null;
2513
+ const recorded = await writeInvocationAttemptBounded(workspaceKey, attempt, { home, standalone: IS_STANDALONE_RUNTIME, ...(config.connectionId ? { connectionId: config.connectionId } : {}) });
2514
+ return recorded ? { workspaceKey, home, attempt, ...(config.connectionId ? { connectionId: config.connectionId } : {}) } : null;
2453
2515
  }
2454
2516
  async function finishHookInvocation(tracker, outcome) {
2455
2517
  if (tracker === null) {
2456
2518
  return;
2457
2519
  }
2458
- await writeInvocationCompletionBounded(tracker.workspaceKey, tracker.attempt, outcome, { home: tracker.home, standalone: IS_STANDALONE_RUNTIME }).catch(() => false);
2520
+ await writeInvocationCompletionBounded(tracker.workspaceKey, tracker.attempt, outcome, { home: tracker.home, standalone: IS_STANDALONE_RUNTIME, ...(tracker.connectionId ? { connectionId: tracker.connectionId } : {}) }).catch(() => false);
2459
2521
  }
2460
2522
  export async function runHook(args = process.argv.slice(2)) {
2461
2523
  const agent = parseAgent(args);
@@ -2465,12 +2527,15 @@ export async function runHook(args = process.argv.slice(2)) {
2465
2527
  if (agent === null) {
2466
2528
  return;
2467
2529
  }
2468
- const tracker = await beginHookInvocation(agent).catch(() => null);
2530
+ let tracker = null;
2469
2531
  let input;
2470
2532
  try {
2471
2533
  input = await readStdin();
2534
+ const normalized = isObject(input) ? normalizeHookInput(input, agent, process.env).input : {};
2535
+ tracker = await beginHookInvocation(agent, stringField(normalized.cwd) ?? "").catch(() => null);
2472
2536
  }
2473
2537
  catch {
2538
+ tracker = await beginHookInvocation(agent).catch(() => null);
2474
2539
  await finishHookInvocation(tracker, {
2475
2540
  success: false,
2476
2541
  failureCode: "invalid_input",