grounder 0.0.2 → 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 (60) hide show
  1. package/README.md +113 -40
  2. package/dist/agents/claude.d.ts +15 -0
  3. package/dist/agents/claude.d.ts.map +1 -1
  4. package/dist/agents/claude.js +225 -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 +11 -0
  11. package/dist/agents/cursor.d.ts.map +1 -1
  12. package/dist/agents/cursor.js +175 -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 +6 -0
  19. package/dist/agents/types.d.ts.map +1 -1
  20. package/dist/cli.js +25 -1
  21. package/dist/cli.js.map +1 -1
  22. package/dist/commands/check.d.ts +13 -0
  23. package/dist/commands/check.d.ts.map +1 -0
  24. package/dist/commands/check.js +21 -0
  25. package/dist/commands/check.js.map +1 -0
  26. package/dist/commands/doctor.d.ts +9 -0
  27. package/dist/commands/doctor.d.ts.map +1 -0
  28. package/dist/commands/doctor.js +270 -0
  29. package/dist/commands/doctor.js.map +1 -0
  30. package/dist/commands/handoff/list.d.ts +9 -0
  31. package/dist/commands/handoff/list.d.ts.map +1 -1
  32. package/dist/commands/handoff/list.js +15 -4
  33. package/dist/commands/handoff/list.js.map +1 -1
  34. package/dist/commands/handoff/peek.d.ts +38 -0
  35. package/dist/commands/handoff/peek.d.ts.map +1 -0
  36. package/dist/commands/handoff/peek.js +120 -0
  37. package/dist/commands/handoff/peek.js.map +1 -0
  38. package/dist/commands/status.d.ts +7 -0
  39. package/dist/commands/status.d.ts.map +1 -0
  40. package/dist/commands/status.js +101 -0
  41. package/dist/commands/status.js.map +1 -0
  42. package/dist/commands/vault/init.d.ts +2 -0
  43. package/dist/commands/vault/init.d.ts.map +1 -1
  44. package/dist/commands/vault/init.js +28 -0
  45. package/dist/commands/vault/init.js.map +1 -1
  46. package/dist/util/frontmatter.d.ts +11 -0
  47. package/dist/util/frontmatter.d.ts.map +1 -0
  48. package/dist/util/frontmatter.js +69 -0
  49. package/dist/util/frontmatter.js.map +1 -0
  50. package/dist/util/merge-json.d.ts +15 -0
  51. package/dist/util/merge-json.d.ts.map +1 -0
  52. package/dist/util/merge-json.js +40 -0
  53. package/dist/util/merge-json.js.map +1 -0
  54. package/dist/vault/find-usable-handoff.d.ts +19 -0
  55. package/dist/vault/find-usable-handoff.d.ts.map +1 -0
  56. package/dist/vault/find-usable-handoff.js +28 -0
  57. package/dist/vault/find-usable-handoff.js.map +1 -0
  58. package/package.json +1 -1
  59. package/templates/agents/claude/commands/grounder-task.md +4 -4
  60. package/templates/agents/cursor/commands/grounder-task.md +4 -4
package/README.md CHANGED
@@ -1,9 +1,19 @@
1
1
  # Grounder
2
2
 
3
- Connect project folders to a personal Obsidian vault so AI agents (Cursor, Claude Code, etc.) get persistent memory without committing personal docs to the repo.
3
+ [![npm version](https://img.shields.io/npm/v/grounder.svg)](https://www.npmjs.com/package/grounder)
4
+ [![license](https://img.shields.io/npm/l/grounder.svg)](../../LICENSE)
5
+
6
+ AI coding agents forget everything between sessions. Grounder gives them persistent memory in a personal Obsidian vault — notes and handoffs live outside the repo, under your control, and never get committed.
7
+
8
+ - **Private by default** — vault notes live outside the project tree; only a small `projectId` marker is safe to commit
9
+ - **Built for agents** — installs `/grounder-note`, `/grounder-task`, `/grounder-task-handoff` slash commands in Cursor and Claude Code
10
+ - **Structured handoffs** — end a session with a Done/Next/Blockers/Decisions checkpoint; resume next time by hydrating from it
11
+ - **Zero per-project install** — slash commands shell out via `npx`; nothing to add to the repo besides the marker file
4
12
 
5
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).
6
14
 
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
+
7
17
  ## Install
8
18
 
9
19
  ```bash
@@ -16,75 +26,75 @@ Or run without installing:
16
26
  npx grounder --help
17
27
  ```
18
28
 
29
+ `grounder -h` prints the full reference; `grounder -v` prints the installed version.
30
+
19
31
  ## Quickstart
20
32
 
33
+ **1. One-time setup:**
34
+
21
35
  ```bash
22
36
  # Once per machine — set vault location + install agent slash commands
23
- grounder vault init ~/Documents/obsidian/dev
37
+ # --hooks adds an optional one-line session-start reminder (see Session-start hooks)
38
+ grounder vault init <path-to-your-vault> --hooks
24
39
 
25
- # Once per folder — link project id to vault notes/ + logs/
40
+ # Once per project folder — link project id to vault notes/ + logs/
26
41
  cd your-project
27
42
  grounder init
43
+ ```
28
44
 
29
- # Write a note (or use /grounder-note in Cursor / Claude Code)
30
- grounder note "Investigate auth middleware"
45
+ Both commands preview what they'll write and ask to confirm; add `--yes` to skip the prompt (e.g. in scripts).
31
46
 
32
- # End a session with a structured handoff (or use /grounder-task-handoff)
33
- grounder handoff "$(cat <<'EOF'
34
- # Handoff: auth middleware
47
+ **2. Daily use from your agent's chat:**
35
48
 
36
- ## Done
37
- - Mapped middleware order
49
+ ```text
50
+ > /grounder-task
51
+
52
+ Reading latest handoff… (logs/2026-07-21-091500-auth-middleware.md)
53
+ Done: mapped middleware order.
54
+ Next: 1. Add tests for 401 path
55
+ Starting on tests for the 401 path now.
38
56
 
39
- ## Next
40
- 1. Add tests for 401 path
57
+ > ...you work with the agent...
41
58
 
42
- ## Blockers
43
- - None
59
+ > /grounder-task-handoff
44
60
 
45
- ## Decisions
46
- - Keep cookie session for now
61
+ Wrote handoff → <vault>/10-Projects/your-project/logs/2026-07-28-143200-auth-middleware.md
62
+ ```
47
63
 
48
- ## Files
49
- - src/middleware/auth.ts
50
- EOF
51
- )"
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).
52
65
 
53
- # Next session hydrate from newest handoff (or use /grounder-task)
54
- grounder handoff list
66
+ No agent, or want to write by hand? The same actions are plain CLI commands:
67
+
68
+ ```bash
69
+ grounder note "Investigate auth middleware" # ad-hoc note
70
+ grounder handoff "# Handoff: ...\n\n## Next\n1. ..." # session checkpoint
71
+ grounder handoff list # newest handoffs, for manual hydrate
55
72
  ```
56
73
 
57
74
  Notes land in `<vault>/10-Projects/{projectId}/notes/`.
58
- 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).
76
+
77
+ Inspect or debug setup any time with `grounder status` / `grounder doctor` — see [Troubleshooting](#troubleshooting).
59
78
 
60
79
  ### Session loop
61
80
 
62
81
  ```text
63
- /grounder-task → work → /grounder-task-handoff → next /grounder-task
82
+ (optional teaser) → /grounder-task → work → /grounder-task-handoff → next session
64
83
  ```
65
84
 
66
85
  | Slash command | CLI | Role |
67
86
  | --- | --- | --- |
68
87
  | `/grounder-note` | `grounder note` | Ad-hoc vault note |
69
88
  | `/grounder-task-handoff` | `grounder handoff` | Write session checkpoint to `logs/` |
70
- | `/grounder-task` | `grounder handoff list` + read newest | Read-only hydrate from newest handoff + `AGENTS.md` |
71
-
72
- ## Setup overview
89
+ | `/grounder-task` | `grounder handoff list --head` + read it | Read-only hydrate from newest usable handoff + `AGENTS.md` |
73
90
 
74
- Three steps vault once per machine, then link each project folder:
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).
75
92
 
76
- 1. **`grounder vault init <path>`** (once per machine)
77
- - Writes `~/.grounder/config.json` with the vault root
78
- - Creates `<vault>/10-Projects/` if missing
79
- - Installs agent slash commands for detected agents (or `--agent=<id>`):
80
- - Cursor → `~/.cursor/commands/grounder-{note,task,task-handoff}.md`
81
- - Claude Code → `~/.claude/commands/grounder-{note,task,task-handoff}.md`
82
-
83
- 2. **`grounder init`** (once per project folder)
84
- - Writes `.grounder.json` in the current directory (`projectId` — safe to commit)
85
- - Creates `<vault>/10-Projects/{projectId}/notes/` and `logs/`
93
+ ## Setup overview
86
94
 
87
- 3. **Daily use** notes, handoffs, and recall via CLI or slash commands; no further install.
95
+ - **`grounder vault init <path>`** (once per machine) writes `~/.grounder/config.json`, creates `<vault>/10-Projects/`, and installs slash commands for detected agents (Cursor → `~/.cursor/commands/`, Claude Code → `~/.claude/commands/`; override with `--agent=<id>`).
96
+ - **`grounder init`** (once per project folder) writes `.grounder.json` (`projectId` — safe to commit) and creates `<vault>/10-Projects/{projectId}/notes/` and `logs/`.
97
+ - **Daily use** — notes, handoffs, and recall via CLI or slash commands; no further install.
88
98
 
89
99
  Nothing is written into the repo except the small `.grounder.json` marker. Agent artifacts stay under the user’s home directory; vault notes stay outside the project tree.
90
100
 
@@ -96,8 +106,12 @@ grounder init Connect the current folder to your vault
96
106
  grounder note <text> Write a note to the vault
97
107
  grounder handoff <text> Write a session handoff to vault logs/
98
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)
99
111
  grounder path notes Print resolved notes directory
100
112
  grounder path logs Print resolved logs directory
113
+ grounder status Snapshot of machine + project link + resolved paths
114
+ grounder doctor Health checks with fix hints
101
115
  ```
102
116
 
103
117
  ### Init flags
@@ -109,6 +123,7 @@ grounder path logs Print resolved logs directory
109
123
  | `--id <id>` | `init` | Override detected project id |
110
124
  | `--vault <path>` | `init` | Override home vault root for this run |
111
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)) |
112
127
 
113
128
  ### Note / handoff flags
114
129
 
@@ -116,9 +131,25 @@ grounder path logs Print resolved logs directory
116
131
  | --- | --- | --- |
117
132
  | `--title <slug>` | `note`, `handoff` | Filename slug (default: slugified text / first line) |
118
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` |
135
+
136
+ ### Doctor flags
137
+
138
+ | Flag | Description |
139
+ | --- | --- |
140
+ | `--global` | Machine-only checks (skip project/link checks) |
119
141
 
120
142
  Run `grounder --help` for the full reference.
121
143
 
144
+ ### Status vs doctor
145
+
146
+ | Command | Job | When to use |
147
+ | --- | --- | --- |
148
+ | `grounder status` | Snapshot of Machine (home config + vault path) and Project (link, id, notes/logs, git) | “Am I wired?” — see paths and link state |
149
+ | `grounder doctor` | Health checklist (`ok` / `fail` / `warn`) with fix hints; exit `1` on any fail | “Why isn’t memory working?” — verify setup |
150
+
151
+ Both are read-only. `status` exits `0` even when unlinked; `doctor` fails when checks fail. Use `doctor --global` to check the machine without a project link.
152
+
122
153
  ## Configuration
123
154
 
124
155
  **Machine config** — `~/.grounder/config.json`:
@@ -158,13 +189,55 @@ The vault layout is agent-agnostic. `grounder vault init` installs thin glue art
158
189
  No `--agent` flag: auto-detect installed agents. Explicit install:
159
190
 
160
191
  ```bash
161
- grounder vault init ~/Documents/obsidian/dev --agent=cursor --agent=claude
192
+ grounder vault init <path-to-your-vault> --agent=cursor --agent=claude
162
193
  ```
163
194
 
164
195
  Slash commands tell the agent to run `npx grounder …` from the linked project folder (no global install required). Re-run with `--force` to refresh existing installs.
165
196
 
166
197
  Templates live under `templates/agents/{id}/`. Adding another agent means one adapter file + one template directory — `vault init` stays agent-blind.
167
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
+
228
+ ## Troubleshooting
229
+
230
+ | Symptom | Try |
231
+ | --- | --- |
232
+ | Not sure if this folder is linked | `grounder status` — check Project `Linked:` and paths |
233
+ | Notes / handoffs fail or slash commands missing | `grounder doctor` — follow fix hints |
234
+ | Machine setup only (no project yet) | `grounder doctor --global` |
235
+ | Home config / vault missing | `grounder vault init <path>` |
236
+ | No `.grounder.json` / notes dirs | `grounder init` |
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 |
240
+
168
241
  ## Development
169
242
 
170
243
  Source, tests, and contribution workflow live in the [Grounder monorepo](https://github.com/andrej-kolic/grounder).
@@ -1,7 +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;
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[];
6
21
  export declare const claude: AgentAdapter;
7
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;AAkBD,eAAO,MAAM,MAAM,EAAE,YAgBpB,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
  }
@@ -18,6 +36,13 @@ export function grounderTaskHandoffCommandPath(homeDir) {
18
36
  export function grounderTaskCommandPath(homeDir) {
19
37
  return path.join(claudeCommandsDir(homeDir), "grounder-task.md");
20
38
  }
39
+ export function expectedArtifacts(homeDir) {
40
+ return COMMANDS.map((filename) => path.join(claudeCommandsDir(homeDir), filename));
41
+ }
42
+ /** Paths of hook config this adapter touches — currently just `settings.json`. */
43
+ export function expectedHookArtifacts(homeDir) {
44
+ return [claudeSettingsJsonPath(homeDir)];
45
+ }
21
46
  async function installCommand(filename, opts) {
22
47
  const dest = path.join(claudeCommandsDir(opts.homeDir), filename);
23
48
  const existed = await fileExists(dest);
@@ -28,12 +53,210 @@ async function installCommand(filename, opts) {
28
53
  await copyFile(path.join(templateDir, filename), dest);
29
54
  return { dest, status: existed ? "overwritten" : "created" };
30
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
+ }
31
252
  export const claude = {
32
253
  id: "claude",
33
254
  name: "Claude Code",
34
255
  async isInstalled() {
35
256
  return fileExists(path.join(resolveHomeDir(), ".claude"));
36
257
  },
258
+ expectedArtifacts,
259
+ expectedHookArtifacts,
37
260
  async install(opts) {
38
261
  const artifacts = {};
39
262
  for (const filename of COMMANDS) {
@@ -42,5 +265,6 @@ export const claude = {
42
265
  }
43
266
  return { artifacts };
44
267
  },
268
+ installHooks,
45
269
  };
46
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":"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"}
@@ -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