grounder 0.1.0 → 0.2.0

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 (51) hide show
  1. package/README.md +44 -6
  2. package/dist/agents/claude.d.ts +14 -0
  3. package/dist/agents/claude.d.ts.map +1 -1
  4. package/dist/agents/claude.js +221 -1
  5. package/dist/agents/claude.js.map +1 -1
  6. package/dist/agents/cursor-hook-input.d.ts +14 -0
  7. package/dist/agents/cursor-hook-input.d.ts.map +1 -0
  8. package/dist/agents/cursor-hook-input.js +80 -0
  9. package/dist/agents/cursor-hook-input.js.map +1 -0
  10. package/dist/agents/cursor.d.ts +10 -0
  11. package/dist/agents/cursor.d.ts.map +1 -1
  12. package/dist/agents/cursor.js +171 -1
  13. package/dist/agents/cursor.js.map +1 -1
  14. package/dist/agents/hook-runtime.d.ts +108 -0
  15. package/dist/agents/hook-runtime.d.ts.map +1 -0
  16. package/dist/agents/hook-runtime.js +260 -0
  17. package/dist/agents/hook-runtime.js.map +1 -0
  18. package/dist/agents/types.d.ts +4 -0
  19. package/dist/agents/types.d.ts.map +1 -1
  20. package/dist/cli.js +12 -1
  21. package/dist/cli.js.map +1 -1
  22. package/dist/commands/doctor.d.ts.map +1 -1
  23. package/dist/commands/doctor.js +68 -2
  24. package/dist/commands/doctor.js.map +1 -1
  25. package/dist/commands/handoff/list.d.ts +9 -0
  26. package/dist/commands/handoff/list.d.ts.map +1 -1
  27. package/dist/commands/handoff/list.js +15 -4
  28. package/dist/commands/handoff/list.js.map +1 -1
  29. package/dist/commands/handoff/peek.d.ts +38 -0
  30. package/dist/commands/handoff/peek.d.ts.map +1 -0
  31. package/dist/commands/handoff/peek.js +120 -0
  32. package/dist/commands/handoff/peek.js.map +1 -0
  33. package/dist/commands/vault/init.d.ts +2 -0
  34. package/dist/commands/vault/init.d.ts.map +1 -1
  35. package/dist/commands/vault/init.js +28 -0
  36. package/dist/commands/vault/init.js.map +1 -1
  37. package/dist/util/frontmatter.d.ts +11 -0
  38. package/dist/util/frontmatter.d.ts.map +1 -0
  39. package/dist/util/frontmatter.js +69 -0
  40. package/dist/util/frontmatter.js.map +1 -0
  41. package/dist/util/merge-json.d.ts +15 -0
  42. package/dist/util/merge-json.d.ts.map +1 -0
  43. package/dist/util/merge-json.js +40 -0
  44. package/dist/util/merge-json.js.map +1 -0
  45. package/dist/vault/find-usable-handoff.d.ts +19 -0
  46. package/dist/vault/find-usable-handoff.d.ts.map +1 -0
  47. package/dist/vault/find-usable-handoff.js +28 -0
  48. package/dist/vault/find-usable-handoff.js.map +1 -0
  49. package/package.json +1 -1
  50. package/templates/agents/claude/commands/grounder-task.md +4 -4
  51. package/templates/agents/cursor/commands/grounder-task.md +4 -4
package/README.md CHANGED
@@ -12,7 +12,7 @@ AI coding agents forget everything between sessions. Grounder gives them persist
12
12
 
13
13
  **Requirements:** Node.js 18+ and an Obsidian vault on disk. Git is optional but used when present (project id detection and link lookup bounds).
14
14
 
15
- **Contents:** [Install](#install) · [Quickstart](#quickstart) · [Setup overview](#setup-overview) · [Commands](#commands) · [Configuration](#configuration) · [Agents](#agents) · [Troubleshooting](#troubleshooting)
15
+ **Contents:** [Install](#install) · [Quickstart](#quickstart) · [Setup overview](#setup-overview) · [Commands](#commands) · [Configuration](#configuration) · [Agents](#agents) · [Session-start hooks](#session-start-hooks) · [Troubleshooting](#troubleshooting)
16
16
 
17
17
  ## Install
18
18
 
@@ -34,7 +34,8 @@ npx grounder --help
34
34
 
35
35
  ```bash
36
36
  # Once per machine — set vault location + install agent slash commands
37
- grounder vault init <path-to-your-vault>
37
+ # --hooks adds an optional one-line session-start reminder (see Session-start hooks)
38
+ grounder vault init <path-to-your-vault> --hooks
38
39
 
39
40
  # Once per project folder — link project id to vault notes/ + logs/
40
41
  cd your-project
@@ -60,7 +61,7 @@ Both commands preview what they'll write and ask to confirm; add `--yes` to skip
60
61
  Wrote handoff → <vault>/10-Projects/your-project/logs/2026-07-28-143200-auth-middleware.md
61
62
  ```
62
63
 
63
- `/grounder-task` hydrates the agent from the newest handoff plus `AGENTS.md`; `/grounder-task-handoff` writes the next checkpoint when you close the session. Behind the scenes these run `grounder handoff list` and `grounder handoff <text>` for you — see [Session loop](#session-loop).
64
+ `/grounder-task` hydrates the agent from the newest *usable* handoff plus `AGENTS.md`; `/grounder-task-handoff` writes the next checkpoint when you close the session. Behind the scenes these run `grounder handoff list --head` and `grounder handoff <text>` for you — see [Session loop](#session-loop).
64
65
 
65
66
  No agent, or want to write by hand? The same actions are plain CLI commands:
66
67
 
@@ -71,21 +72,23 @@ grounder handoff list # newest handoffs, for manual h
71
72
  ```
72
73
 
73
74
  Notes land in `<vault>/10-Projects/{projectId}/notes/`.
74
- Handoffs land in `<vault>/10-Projects/{projectId}/logs/` (one file per close; newest wins).
75
+ Handoffs land in `<vault>/10-Projects/{projectId}/logs/` (one file per close; newest *usable* file wins — an empty or unreadable newest file falls back to the next one).
75
76
 
76
77
  Inspect or debug setup any time with `grounder status` / `grounder doctor` — see [Troubleshooting](#troubleshooting).
77
78
 
78
79
  ### Session loop
79
80
 
80
81
  ```text
81
- /grounder-task → work → /grounder-task-handoff → next /grounder-task
82
+ (optional teaser) → /grounder-task → work → /grounder-task-handoff → next session
82
83
  ```
83
84
 
84
85
  | Slash command | CLI | Role |
85
86
  | --- | --- | --- |
86
87
  | `/grounder-note` | `grounder note` | Ad-hoc vault note |
87
88
  | `/grounder-task-handoff` | `grounder handoff` | Write session checkpoint to `logs/` |
88
- | `/grounder-task` | `grounder handoff list` + read newest | Read-only hydrate from newest handoff + `AGENTS.md` |
89
+ | `/grounder-task` | `grounder handoff list --head` + read it | Read-only hydrate from newest usable handoff + `AGENTS.md` |
90
+
91
+ With `--hooks` on `vault init`, a new Cursor/Claude session may also print a one-line teaser when a handoff exists — never the full body. See [Session-start hooks](#session-start-hooks).
89
92
 
90
93
  ## Setup overview
91
94
 
@@ -103,6 +106,8 @@ grounder init Connect the current folder to your vault
103
106
  grounder note <text> Write a note to the vault
104
107
  grounder handoff <text> Write a session handoff to vault logs/
105
108
  grounder handoff list Print recent handoff paths (newest first)
109
+ grounder handoff list --head Print only the newest usable handoff path
110
+ grounder handoff peek One-line latest-handoff teaser (used by session hooks)
106
111
  grounder path notes Print resolved notes directory
107
112
  grounder path logs Print resolved logs directory
108
113
  grounder status Snapshot of machine + project link + resolved paths
@@ -118,6 +123,7 @@ grounder doctor Health checks with fix hints
118
123
  | `--id <id>` | `init` | Override detected project id |
119
124
  | `--vault <path>` | `init` | Override home vault root for this run |
120
125
  | `--agent <id>` | `vault init` | Install for a specific agent (repeatable; default: auto-detect). Supported: `cursor`, `claude` |
126
+ | `--hooks` | `vault init` | Also install session-start teaser hooks (opt-in; see [Session-start hooks](#session-start-hooks)) |
121
127
 
122
128
  ### Note / handoff flags
123
129
 
@@ -125,6 +131,7 @@ grounder doctor Health checks with fix hints
125
131
  | --- | --- | --- |
126
132
  | `--title <slug>` | `note`, `handoff` | Filename slug (default: slugified text / first line) |
127
133
  | `--limit <n>` | `handoff list` | Max paths to print (default: 5) |
134
+ | `--head` | `handoff list` | Print only the newest *usable* handoff path — skips empty/unreadable files, same pick as `handoff peek` |
128
135
 
129
136
  ### Doctor flags
130
137
 
@@ -189,6 +196,35 @@ Slash commands tell the agent to run `npx grounder …` from the linked project
189
196
 
190
197
  Templates live under `templates/agents/{id}/`. Adding another agent means one adapter file + one template directory — `vault init` stays agent-blind.
191
198
 
199
+ ## Session-start hooks
200
+
201
+ Opt-in safety net for the session loop: when a Cursor or Claude Code session starts in a linked project that already has a handoff, Grounder prints **one line** reminding you it exists. You (or the agent) still decide whether to run `/grounder-task`.
202
+
203
+ ```bash
204
+ grounder vault init <path-to-your-vault> --hooks
205
+ ```
206
+
207
+ Example teaser:
208
+
209
+ ```text
210
+ [grounder] Latest handoff: "auth middleware" (2026-07-28). Run /grounder-task to load it, or ignore if unrelated.
211
+ ```
212
+
213
+ What hooks do **not** do:
214
+
215
+ - They never auto-load the full handoff body into context
216
+ - They never block or delay a session from starting
217
+ - Unlinked folders and projects with no handoffs print nothing (exit 0, silent)
218
+
219
+ `doctor` reports a `warn` (never a `fail`) when a detected agent has no Grounder hook installed, and when hooks are installed but `~/.grounder/runtime` is stale or missing.
220
+
221
+ Hooks run `~/.grounder/runtime/dist/cli.js` directly (not `npx`), materialized on install:
222
+
223
+ - **Real install** (`npm i -g grounder`, `pnpm add -g grounder`, or a monorepo checkout) → symlinked. Upgrading overwrites the same path in place, so hooks stay current with **no re-run needed**.
224
+ - **Bare `npx grounder vault init --hooks`** (nothing installed) → copied, since each `npx` invocation resolves to a disposable, version-pinned cache dir that can't be symlinked durably. Re-run the same command after upgrading grounder to refresh (no `--force` needed).
225
+
226
+ If you want hooks that stay current with zero maintenance, install grounder rather than using bare `npx` for this step.
227
+
192
228
  ## Troubleshooting
193
229
 
194
230
  | Symptom | Try |
@@ -199,6 +235,8 @@ Templates live under `templates/agents/{id}/`. Adding another agent means one ad
199
235
  | Home config / vault missing | `grounder vault init <path>` |
200
236
  | No `.grounder.json` / notes dirs | `grounder init` |
201
237
  | Agent slash commands stale or partial | `grounder vault init <path> --force` (or `--agent=<id>`) |
238
+ | Session-start teaser missing (optional) | `grounder vault init <path> --hooks` — `doctor` warns when absent |
239
+ | Session-start teaser stale after upgrade (bare npx) | `grounder vault init <path> --hooks` — `doctor` warns when `hook-runtime` is stale |
202
240
 
203
241
  ## Development
204
242
 
@@ -1,8 +1,22 @@
1
1
  import type { AgentAdapter } from "./types.js";
2
+ /**
3
+ * Canonical SessionStart command for Claude Code (home-local runtime, not `npx`).
4
+ * @see {@link peekHookCommand} — REVERT: restore `"npx grounder handoff peek"` and drop runtime.
5
+ */
6
+ export declare function claudePeekHookCommand(homeDir?: string): string;
7
+ /**
8
+ * SessionStart matcher Grounder owns.
9
+ * Excludes `resume` and `fork` — those sessions already carry prior context.
10
+ */
11
+ export declare const CLAUDE_SESSION_START_MATCHER = "startup|clear|compact";
2
12
  export declare function claudeCommandsDir(homeDir?: string): string;
13
+ /** Absolute path to Claude Code's shared settings file (`~/.claude/settings.json`). */
14
+ export declare function claudeSettingsJsonPath(homeDir?: string): string;
3
15
  export declare function grounderNoteCommandPath(homeDir?: string): string;
4
16
  export declare function grounderTaskHandoffCommandPath(homeDir?: string): string;
5
17
  export declare function grounderTaskCommandPath(homeDir?: string): string;
6
18
  export declare function expectedArtifacts(homeDir?: string): string[];
19
+ /** Paths of hook config this adapter touches — currently just `settings.json`. */
20
+ export declare function expectedHookArtifacts(homeDir?: string): string[];
7
21
  export declare const claude: AgentAdapter;
8
22
  //# sourceMappingURL=claude.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,YAAY,EAIb,MAAM,YAAY,CAAC;AAOpB,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,8BAA8B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAE5D;AAkBD,eAAO,MAAM,MAAM,EAAE,YAkBpB,CAAC"}
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,YAAY,EAIb,MAAM,YAAY,CAAC;AAOpB;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,0BAA0B,CAAC;AAEpE,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,uFAAuF;AACvF,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,8BAA8B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAE5D;AAED,kFAAkF;AAClF,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAEhE;AAwOD,eAAO,MAAM,MAAM,EAAE,YAqBpB,CAAC"}
@@ -1,14 +1,32 @@
1
- import { copyFile, mkdir } from "node:fs/promises";
1
+ import { copyFile, mkdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { resolveHomeDir } from "../connector/home.js";
5
5
  import { fileExists } from "../util/fs.js";
6
+ import { mergeJsonFile } from "../util/merge-json.js";
7
+ import { installHookRuntime, isGrounderPeekHookCommand, isHookRuntimeStale, peekHookCommand, } from "./hook-runtime.js";
6
8
  const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
7
9
  const templateDir = path.join(pkgRoot, "templates", "agents", "claude", "commands");
8
10
  const COMMANDS = ["grounder-note.md", "grounder-task-handoff.md", "grounder-task.md"];
11
+ /**
12
+ * Canonical SessionStart command for Claude Code (home-local runtime, not `npx`).
13
+ * @see {@link peekHookCommand} — REVERT: restore `"npx grounder handoff peek"` and drop runtime.
14
+ */
15
+ export function claudePeekHookCommand(homeDir) {
16
+ return peekHookCommand(homeDir);
17
+ }
18
+ /**
19
+ * SessionStart matcher Grounder owns.
20
+ * Excludes `resume` and `fork` — those sessions already carry prior context.
21
+ */
22
+ export const CLAUDE_SESSION_START_MATCHER = "startup|clear|compact";
9
23
  export function claudeCommandsDir(homeDir) {
10
24
  return path.join(resolveHomeDir(homeDir), ".claude", "commands");
11
25
  }
26
+ /** Absolute path to Claude Code's shared settings file (`~/.claude/settings.json`). */
27
+ export function claudeSettingsJsonPath(homeDir) {
28
+ return path.join(resolveHomeDir(homeDir), ".claude", "settings.json");
29
+ }
12
30
  export function grounderNoteCommandPath(homeDir) {
13
31
  return path.join(claudeCommandsDir(homeDir), "grounder-note.md");
14
32
  }
@@ -21,6 +39,10 @@ export function grounderTaskCommandPath(homeDir) {
21
39
  export function expectedArtifacts(homeDir) {
22
40
  return COMMANDS.map((filename) => path.join(claudeCommandsDir(homeDir), filename));
23
41
  }
42
+ /** Paths of hook config this adapter touches — currently just `settings.json`. */
43
+ export function expectedHookArtifacts(homeDir) {
44
+ return [claudeSettingsJsonPath(homeDir)];
45
+ }
24
46
  async function installCommand(filename, opts) {
25
47
  const dest = path.join(claudeCommandsDir(opts.homeDir), filename);
26
48
  const existed = await fileExists(dest);
@@ -31,6 +53,202 @@ async function installCommand(filename, opts) {
31
53
  await copyFile(path.join(templateDir, filename), dest);
32
54
  return { dest, status: existed ? "overwritten" : "created" };
33
55
  }
56
+ // ---------------------------------------------------------------------------
57
+ // Session-start hook install (~/.claude/settings.json)
58
+ //
59
+ // Claude Code settings are a shared JSON object. Grounder only owns one nested
60
+ // command entry; unrelated keys (permissions, other hook events, etc.) must
61
+ // survive merge. Relevant shape after install (path varies by home / Node):
62
+ //
63
+ // {
64
+ // "hooks": {
65
+ // "SessionStart": [
66
+ // {
67
+ // "matcher": "startup|clear|compact",
68
+ // "hooks": [
69
+ // {
70
+ // "type": "command",
71
+ // "command": "'/path/to/node' '/path/to/.grounder/runtime/dist/cli.js' handoff peek"
72
+ // }
73
+ // ]
74
+ // }
75
+ // ]
76
+ // }
77
+ // }
78
+ //
79
+ // Terminology used below:
80
+ // - settings root → the whole settings.json object
81
+ // - hooks → settings.hooks (map of event name → matcher groups)
82
+ // - SessionStart → hooks.SessionStart (array of matcher groups)
83
+ // - matcher group → { matcher, hooks: Hook[] }
84
+ // - hook entry → { type: "command", command: string }
85
+ //
86
+ // Idempotency: {@link isGrounderPeekHookCommand} (runtime path or legacy npx).
87
+ // ---------------------------------------------------------------------------
88
+ function peekHookEntry(homeDir) {
89
+ return { type: "command", command: claudePeekHookCommand(homeDir) };
90
+ }
91
+ /**
92
+ * Locate Grounder's peek command inside a `hooks.SessionStart` array.
93
+ *
94
+ * @param sessionStart - `settings.hooks.SessionStart` — array of matcher groups
95
+ * @returns Indices into that array / the group's `hooks` array, or `null` if absent
96
+ */
97
+ function findPeekHook(sessionStart) {
98
+ for (let groupIdx = 0; groupIdx < sessionStart.length; groupIdx++) {
99
+ const group = sessionStart[groupIdx];
100
+ if (!group || typeof group !== "object" || Array.isArray(group)) {
101
+ continue;
102
+ }
103
+ const hooks = group.hooks;
104
+ if (!Array.isArray(hooks)) {
105
+ continue;
106
+ }
107
+ for (let hookIdx = 0; hookIdx < hooks.length; hookIdx++) {
108
+ const hook = hooks[hookIdx];
109
+ if (hook &&
110
+ typeof hook === "object" &&
111
+ !Array.isArray(hook) &&
112
+ isGrounderPeekHookCommand(hook.command)) {
113
+ return { groupIdx, hookIdx };
114
+ }
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+ /** Whether `settings.json` already lists any Grounder peek command (for status labeling). */
120
+ async function peekHookHadGrounderEntry(filePath) {
121
+ try {
122
+ const parsed = JSON.parse(await readFile(filePath, "utf8"));
123
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
124
+ return false;
125
+ }
126
+ const hooks = parsed.hooks;
127
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
128
+ return false;
129
+ }
130
+ const sessionStart = hooks.SessionStart;
131
+ return Array.isArray(sessionStart) && findPeekHook(sessionStart) !== null;
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ }
137
+ /**
138
+ * Skip only when the canonical command is already present *and* the runtime is
139
+ * current for the running grounder version/source. Legacy `npx` entries or a
140
+ * stale runtime (missing, or symlinked/copied from a different source) always
141
+ * refresh — no `--force` required to migrate or to pick up an upgrade.
142
+ */
143
+ async function peekHookUpToDate(filePath, homeDir) {
144
+ if (!(await fileExists(filePath))) {
145
+ return false;
146
+ }
147
+ if (await isHookRuntimeStale(homeDir)) {
148
+ return false;
149
+ }
150
+ try {
151
+ const parsed = JSON.parse(await readFile(filePath, "utf8"));
152
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
153
+ return false;
154
+ }
155
+ const hooks = parsed.hooks;
156
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
157
+ return false;
158
+ }
159
+ const sessionStart = hooks.SessionStart;
160
+ if (!Array.isArray(sessionStart)) {
161
+ return false;
162
+ }
163
+ const found = findPeekHook(sessionStart);
164
+ if (!found) {
165
+ return false;
166
+ }
167
+ const group = sessionStart[found.groupIdx];
168
+ return group.hooks[found.hookIdx]?.command === claudePeekHookCommand(homeDir);
169
+ }
170
+ catch {
171
+ return false;
172
+ }
173
+ }
174
+ /**
175
+ * Deep-merge Grounder's SessionStart hook into an existing settings object.
176
+ *
177
+ * Preserves every key except the nested path it owns. Strategy:
178
+ * 1. If a Grounder peek hook already exists (runtime or legacy npx) → replace in place.
179
+ * 2. Else if a matcher group with `matcher === CLAUDE_SESSION_START_MATCHER` exists →
180
+ * append Grounder's hook to that group's `hooks` array.
181
+ * 3. Else → push a new matcher group with Grounder's hook.
182
+ *
183
+ * @param current - Parsed settings.json root (object). Other top-level keys untouched.
184
+ * @param homeDir - Home override for the canonical command path
185
+ * @returns New settings object with `hooks.SessionStart` updated
186
+ */
187
+ function mergeClaudeHooks(current, homeDir) {
188
+ const hooks = current.hooks && typeof current.hooks === "object" && !Array.isArray(current.hooks)
189
+ ? { ...current.hooks }
190
+ : {};
191
+ const sessionStart = Array.isArray(hooks.SessionStart) ? [...hooks.SessionStart] : [];
192
+ const found = findPeekHook(sessionStart);
193
+ const entry = peekHookEntry(homeDir);
194
+ if (found) {
195
+ // Path 1: refresh existing Grounder hook entry in place
196
+ const group = { ...sessionStart[found.groupIdx] };
197
+ const groupHooks = Array.isArray(group.hooks) ? [...group.hooks] : [];
198
+ groupHooks[found.hookIdx] = entry;
199
+ group.hooks = groupHooks;
200
+ sessionStart[found.groupIdx] = group;
201
+ }
202
+ else {
203
+ const matcherIdx = sessionStart.findIndex((group) => group &&
204
+ typeof group === "object" &&
205
+ !Array.isArray(group) &&
206
+ group.matcher === CLAUDE_SESSION_START_MATCHER);
207
+ if (matcherIdx >= 0) {
208
+ // Path 2: same matcher group already exists (e.g. user hooks) — append ours
209
+ const group = { ...sessionStart[matcherIdx] };
210
+ const groupHooks = Array.isArray(group.hooks) ? [...group.hooks] : [];
211
+ groupHooks.push(entry);
212
+ group.hooks = groupHooks;
213
+ sessionStart[matcherIdx] = group;
214
+ }
215
+ else {
216
+ // Path 3: no matching group — create the canonical SessionStart entry
217
+ sessionStart.push({
218
+ matcher: CLAUDE_SESSION_START_MATCHER,
219
+ hooks: [entry],
220
+ });
221
+ }
222
+ }
223
+ return { ...current, hooks: { ...hooks, SessionStart: sessionStart } };
224
+ }
225
+ /**
226
+ * Install (or refresh) Grounder's SessionStart teaser hook into `~/.claude/settings.json`.
227
+ *
228
+ * Also materializes `~/.grounder/runtime` (see {@link installHookRuntime}).
229
+ *
230
+ * Force semantics:
231
+ * - Up-to-date canonical entry + fresh runtime and `force` false → skip
232
+ * - Otherwise → refresh runtime + merge host config
233
+ *
234
+ * Unparseable settings.json: {@link mergeJsonFile} backs off and this throws (never clobbers).
235
+ */
236
+ async function installHooks(opts) {
237
+ const dest = claudeSettingsJsonPath(opts.homeDir);
238
+ const upToDate = await peekHookUpToDate(dest, opts.homeDir);
239
+ if (upToDate && !opts.force) {
240
+ return { artifacts: { [dest]: "skipped" } };
241
+ }
242
+ await installHookRuntime({ homeDir: opts.homeDir });
243
+ const fileExisted = await fileExists(dest);
244
+ const hadGrounderEntry = fileExisted && (await peekHookHadGrounderEntry(dest));
245
+ const result = await mergeJsonFile(dest, (current) => mergeClaudeHooks(current, opts.homeDir));
246
+ if (!result.ok) {
247
+ throw new Error(result.message);
248
+ }
249
+ const status = hadGrounderEntry ? "overwritten" : "created";
250
+ return { artifacts: { [dest]: status } };
251
+ }
34
252
  export const claude = {
35
253
  id: "claude",
36
254
  name: "Claude Code",
@@ -38,6 +256,7 @@ export const claude = {
38
256
  return fileExists(path.join(resolveHomeDir(), ".claude"));
39
257
  },
40
258
  expectedArtifacts,
259
+ expectedHookArtifacts,
41
260
  async install(opts) {
42
261
  const artifacts = {};
43
262
  for (const filename of COMMANDS) {
@@ -46,5 +265,6 @@ export const claude = {
46
265
  }
47
266
  return { artifacts };
48
267
  },
268
+ installHooks,
49
269
  };
50
270
  //# sourceMappingURL=claude.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,kBAAkB,EAClB,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAQ3B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AACpF,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;AAEpF,MAAM,QAAQ,GAAG,CAAC,kBAAkB,EAAE,0BAA0B,EAAE,kBAAkB,CAAU,CAAC;AAE/F;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,OAAO,eAAe,CAAC,OAAO,CAAC,CAAC;AAClC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,uBAAuB,CAAC;AAEpE,MAAM,UAAU,iBAAiB,CAAC,OAAgB;IAChD,OAAO,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;AACnE,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,sBAAsB,CAAC,OAAgB;IACrD,OAAO,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,OAAgB;IACtD,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,OAAgB;IAC7D,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,0BAA0B,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,OAAgB;IACtD,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAgB;IAChD,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;AACrF,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,OAAO,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,KAAK,UAAU,cAAc,CAC3B,QAAmC,EACnC,IAAyB;IAEzB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;IAClE,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;IAEvC,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAC3B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACrC,CAAC;IAED,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC;IACvD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;AAC/D,CAAC;AAED,8EAA8E;AAC9E,uDAAuD;AACvD,EAAE;AACF,+EAA+E;AAC/E,4EAA4E;AAC5E,4EAA4E;AAC5E,EAAE;AACF,MAAM;AACN,iBAAiB;AACjB,0BAA0B;AAC1B,YAAY;AACZ,gDAAgD;AAChD,uBAAuB;AACvB,gBAAgB;AAChB,mCAAmC;AACnC,mGAAmG;AACnG,gBAAgB;AAChB,cAAc;AACd,YAAY;AACZ,UAAU;AACV,QAAQ;AACR,MAAM;AACN,EAAE;AACF,0BAA0B;AAC1B,0DAA0D;AAC1D,+EAA+E;AAC/E,wEAAwE;AACxE,sDAAsD;AACtD,gEAAgE;AAChE,EAAE;AACF,+EAA+E;AAC/E,8EAA8E;AAE9E,SAAS,aAAa,CAAC,OAAgB;IACrC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,qBAAqB,CAAC,OAAO,CAAC,EAAE,CAAC;AACtE,CAAC;AAED;;;;;GAKG;AACH,SAAS,YAAY,CAAC,YAAuB;IAC3C,KAAK,IAAI,QAAQ,GAAG,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,CAAC;QAClE,MAAM,KAAK,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAChE,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAI,KAA6B,CAAC,KAAK,CAAC;QACnD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,SAAS;QACX,CAAC;QACD,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC;YACxD,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5B,IACE,IAAI;gBACJ,OAAO,IAAI,KAAK,QAAQ;gBACxB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;gBACpB,yBAAyB,CAAE,IAA8B,CAAC,OAAO,CAAC,EAClE,CAAC;gBACD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;YAC/B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,6FAA6F;AAC7F,KAAK,UAAU,wBAAwB,CAAC,QAAgB;IACtD,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;QACrE,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,KAAK,GAAI,MAAkC,CAAC,KAAK,CAAC;QACxD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAChE,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,YAAY,GAAI,KAAiC,CAAC,YAAY,CAAC;QACrE,OAAO,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,YAAY,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,gBAAgB,CAAC,QAAgB,EAAE,OAAgB;IAChE,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;QAClC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,MAAM,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;QACrE,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,KAAK,GAAI,MAAkC,CAAC,KAAK,CAAC;QACxD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAChE,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,YAAY,GAAI,KAAiC,CAAC,YAAY,CAAC;QACrE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;YACjC,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,KAAK,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,QAAQ,CAA4C,CAAC;QACtF,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,gBAAgB,CACvB,OAAgC,EAChC,OAAgB;IAEhB,MAAM,KAAK,GACT,OAAO,CAAC,KAAK,IAAI,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC;QACjF,CAAC,CAAC,EAAE,GAAI,OAAO,CAAC,KAAiC,EAAE;QACnD,CAAC,CAAC,EAAE,CAAC;IACT,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACtF,MAAM,KAAK,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IAErC,IAAI,KAAK,EAAE,CAAC;QACV,wDAAwD;QACxD,MAAM,KAAK,GAAG,EAAE,GAAI,YAAY,CAAC,KAAK,CAAC,QAAQ,CAA6B,EAAE,CAAC;QAC/E,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC;QAClC,KAAK,CAAC,KAAK,GAAG,UAAU,CAAC;QACzB,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC;IACvC,CAAC;SAAM,CAAC;QACN,MAAM,UAAU,GAAG,YAAY,CAAC,SAAS,CACvC,CAAC,KAAK,EAAE,EAAE,CACR,KAAK;YACL,OAAO,KAAK,KAAK,QAAQ;YACzB,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YACpB,KAA+B,CAAC,OAAO,KAAK,4BAA4B,CAC5E,CAAC;QACF,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;YACpB,4EAA4E;YAC5E,MAAM,KAAK,GAAG,EAAE,GAAI,YAAY,CAAC,UAAU,CAA6B,EAAE,CAAC;YAC3E,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACtE,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,KAAK,CAAC,KAAK,GAAG,UAAU,CAAC;YACzB,YAAY,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC;QACnC,CAAC;aAAM,CAAC;YACN,sEAAsE;YACtE,YAAY,CAAC,IAAI,CAAC;gBAChB,OAAO,EAAE,4BAA4B;gBACrC,KAAK,EAAE,CAAC,KAAK,CAAC;aACf,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,EAAE,GAAG,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,EAAE,CAAC;AACzE,CAAC;AAED;;;;;;;;;;GAUG;AACH,KAAK,UAAU,YAAY,CAAC,IAAyB;IACnD,MAAM,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAE5D,IAAI,QAAQ,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAC5B,OAAO,EAAE,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC;IAC9C,CAAC;IAED,MAAM,kBAAkB,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IACpD,MAAM,WAAW,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,gBAAgB,GAAG,WAAW,IAAI,CAAC,MAAM,wBAAwB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/E,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAE/F,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,MAAM,GAAmB,gBAAgB,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5E,OAAO,EAAE,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;AAC3C,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAiB;IAClC,EAAE,EAAE,QAAQ;IACZ,IAAI,EAAE,aAAa;IAEnB,KAAK,CAAC,WAAW;QACf,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,iBAAiB;IACjB,qBAAqB;IAErB,KAAK,CAAC,OAAO,CAAC,IAAyB;QACrC,MAAM,SAAS,GAAmC,EAAE,CAAC;QACrD,KAAK,MAAM,QAAQ,IAAI,QAAQ,EAAE,CAAC;YAChC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAC9D,SAAS,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC;QAC3B,CAAC;QACD,OAAO,EAAE,SAAS,EAAE,CAAC;IACvB,CAAC;IAED,YAAY;CACb,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Read Cursor's sessionStart hook payload from stdin and return the first
3
+ * workspace root when present.
4
+ *
5
+ * Cursor pipes JSON like `{ "workspace_roots": ["/path/to/repo"] }` into
6
+ * user-level hooks, whose `cwd` is `~/.cursor` — not the open project.
7
+ *
8
+ * Never throws. Returns `undefined` for TTY stdin, empty/malformed input,
9
+ * missing `workspace_roots`, or when no data arrives within the timeout
10
+ * (so interactive/`grounder handoff peek` runs never hang). If data arrived
11
+ * but stdin has not ended yet, the buffered payload is still parsed.
12
+ */
13
+ export declare function readCursorHookWorkspaceRoot(stdin?: NodeJS.ReadableStream): Promise<string | undefined>;
14
+ //# sourceMappingURL=cursor-hook-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor-hook-input.d.ts","sourceRoot":"","sources":["../../src/agents/cursor-hook-input.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;GAWG;AACH,wBAAsB,2BAA2B,CAC/C,KAAK,GAAE,MAAM,CAAC,cAA8B,GAC3C,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA2B7B"}
@@ -0,0 +1,80 @@
1
+ /** Max wait for piped Cursor hook JSON on stdin before giving up. */
2
+ const STDIN_TIMEOUT_MS = 200;
3
+ /**
4
+ * Read Cursor's sessionStart hook payload from stdin and return the first
5
+ * workspace root when present.
6
+ *
7
+ * Cursor pipes JSON like `{ "workspace_roots": ["/path/to/repo"] }` into
8
+ * user-level hooks, whose `cwd` is `~/.cursor` — not the open project.
9
+ *
10
+ * Never throws. Returns `undefined` for TTY stdin, empty/malformed input,
11
+ * missing `workspace_roots`, or when no data arrives within the timeout
12
+ * (so interactive/`grounder handoff peek` runs never hang). If data arrived
13
+ * but stdin has not ended yet, the buffered payload is still parsed.
14
+ */
15
+ export async function readCursorHookWorkspaceRoot(stdin = process.stdin) {
16
+ if ("isTTY" in stdin && stdin.isTTY) {
17
+ return undefined;
18
+ }
19
+ const raw = await readStdinWithTimeout(stdin, STDIN_TIMEOUT_MS);
20
+ if (raw === undefined || raw.trim() === "") {
21
+ return undefined;
22
+ }
23
+ try {
24
+ const parsed = JSON.parse(raw);
25
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
26
+ return undefined;
27
+ }
28
+ const roots = parsed.workspace_roots;
29
+ if (!Array.isArray(roots) || roots.length === 0) {
30
+ return undefined;
31
+ }
32
+ const first = roots[0];
33
+ if (typeof first !== "string" || first.trim() === "") {
34
+ return undefined;
35
+ }
36
+ return first;
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ function readStdinWithTimeout(stdin, timeoutMs) {
43
+ return new Promise((resolve) => {
44
+ const chunks = [];
45
+ let settled = false;
46
+ const settle = (value) => {
47
+ if (settled) {
48
+ return;
49
+ }
50
+ settled = true;
51
+ clearTimeout(timer);
52
+ stdin.off("data", onData);
53
+ stdin.off("end", onEnd);
54
+ stdin.off("error", onError);
55
+ resolve(value);
56
+ };
57
+ const onData = (chunk) => {
58
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
59
+ };
60
+ const buffered = () => {
61
+ if (chunks.length === 0) {
62
+ return undefined;
63
+ }
64
+ return Buffer.concat(chunks).toString("utf8");
65
+ };
66
+ const onEnd = () => settle(buffered());
67
+ const onError = () => settle(undefined);
68
+ // On timeout, still use whatever arrived — Cursor may flush the JSON
69
+ // payload before closing stdin; discarding it would lose workspace_roots.
70
+ const timer = setTimeout(() => settle(buffered()), timeoutMs);
71
+ timer.unref?.();
72
+ stdin.on("data", onData);
73
+ stdin.on("end", onEnd);
74
+ stdin.on("error", onError);
75
+ if ("readableEnded" in stdin && stdin.readableEnded) {
76
+ onEnd();
77
+ }
78
+ });
79
+ }
80
+ //# sourceMappingURL=cursor-hook-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor-hook-input.js","sourceRoot":"","sources":["../../src/agents/cursor-hook-input.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,18 @@
1
1
  import type { AgentAdapter } from "./types.js";
2
+ /**
3
+ * Canonical sessionStart command for Cursor (home-local runtime, not `npx`).
4
+ * Always includes `--json` so stdout matches Cursor's `additional_context` contract.
5
+ * @see {@link peekHookCommand}
6
+ */
7
+ export declare function cursorPeekHookCommand(homeDir?: string, extraArgs?: readonly string[]): string;
2
8
  export declare function cursorCommandsDir(homeDir?: string): string;
9
+ /** Absolute path to Cursor's shared hooks config (`~/.cursor/hooks.json`). */
10
+ export declare function cursorHooksJsonPath(homeDir?: string): string;
3
11
  export declare function grounderNoteCommandPath(homeDir?: string): string;
4
12
  export declare function grounderTaskHandoffCommandPath(homeDir?: string): string;
5
13
  export declare function grounderTaskCommandPath(homeDir?: string): string;
6
14
  export declare function expectedArtifacts(homeDir?: string): string[];
15
+ /** Paths of hook config this adapter touches — currently just `hooks.json`. */
16
+ export declare function expectedHookArtifacts(homeDir?: string): string[];
7
17
  export declare const cursor: AgentAdapter;
8
18
  //# sourceMappingURL=cursor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/agents/cursor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,YAAY,EAIb,MAAM,YAAY,CAAC;AAOpB,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,8BAA8B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAE5D;AAkBD,eAAO,MAAM,MAAM,EAAE,YAkBpB,CAAC"}
1
+ {"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/agents/cursor.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,YAAY,EAIb,MAAM,YAAY,CAAC;AAOpB;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,GAAE,SAAS,MAAM,EAAe,GACxC,MAAM,CAER;AAED,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,8BAA8B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAE5D;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAEhE;AA2LD,eAAO,MAAM,MAAM,EAAE,YAqBpB,CAAC"}