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.
- package/README.md +44 -6
- package/dist/agents/claude.d.ts +14 -0
- package/dist/agents/claude.d.ts.map +1 -1
- package/dist/agents/claude.js +221 -1
- package/dist/agents/claude.js.map +1 -1
- package/dist/agents/cursor-hook-input.d.ts +14 -0
- package/dist/agents/cursor-hook-input.d.ts.map +1 -0
- package/dist/agents/cursor-hook-input.js +80 -0
- package/dist/agents/cursor-hook-input.js.map +1 -0
- package/dist/agents/cursor.d.ts +10 -0
- package/dist/agents/cursor.d.ts.map +1 -1
- package/dist/agents/cursor.js +171 -1
- package/dist/agents/cursor.js.map +1 -1
- package/dist/agents/hook-runtime.d.ts +108 -0
- package/dist/agents/hook-runtime.d.ts.map +1 -0
- package/dist/agents/hook-runtime.js +260 -0
- package/dist/agents/hook-runtime.js.map +1 -0
- package/dist/agents/types.d.ts +4 -0
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/cli.js +12 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +68 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/handoff/list.d.ts +9 -0
- package/dist/commands/handoff/list.d.ts.map +1 -1
- package/dist/commands/handoff/list.js +15 -4
- package/dist/commands/handoff/list.js.map +1 -1
- package/dist/commands/handoff/peek.d.ts +38 -0
- package/dist/commands/handoff/peek.d.ts.map +1 -0
- package/dist/commands/handoff/peek.js +120 -0
- package/dist/commands/handoff/peek.js.map +1 -0
- package/dist/commands/vault/init.d.ts +2 -0
- package/dist/commands/vault/init.d.ts.map +1 -1
- package/dist/commands/vault/init.js +28 -0
- package/dist/commands/vault/init.js.map +1 -1
- package/dist/util/frontmatter.d.ts +11 -0
- package/dist/util/frontmatter.d.ts.map +1 -0
- package/dist/util/frontmatter.js +69 -0
- package/dist/util/frontmatter.js.map +1 -0
- package/dist/util/merge-json.d.ts +15 -0
- package/dist/util/merge-json.d.ts.map +1 -0
- package/dist/util/merge-json.js +40 -0
- package/dist/util/merge-json.js.map +1 -0
- package/dist/vault/find-usable-handoff.d.ts +19 -0
- package/dist/vault/find-usable-handoff.d.ts.map +1 -0
- package/dist/vault/find-usable-handoff.js +28 -0
- package/dist/vault/find-usable-handoff.js.map +1 -0
- package/package.json +1 -1
- package/templates/agents/claude/commands/grounder-task.md +4 -4
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
package/dist/agents/claude.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/agents/claude.js
CHANGED
|
@@ -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":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor-hook-input.js","sourceRoot":"","sources":["../../src/agents/cursor-hook-input.ts"],"names":[],"mappings":"AAEA,qEAAqE;AACrE,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,QAA+B,OAAO,CAAC,KAAK;IAE5C,IAAI,OAAO,IAAI,KAAK,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QACpC,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,oBAAoB,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;IAChE,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC3C,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,KAAK,GAAI,MAAkC,CAAC,eAAe,CAAC;QAClE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChD,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACvB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACrD,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,oBAAoB,CAC3B,KAA4B,EAC5B,SAAiB;IAEjB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,IAAI,OAAO,GAAG,KAAK,CAAC;QAEpB,MAAM,MAAM,GAAG,CAAC,KAAyB,EAAE,EAAE;YAC3C,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO;YACT,CAAC;YACD,OAAO,GAAG,IAAI,CAAC;YACf,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAC1B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACxB,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC5B,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,CAAC;QAEF,MAAM,MAAM,GAAG,CAAC,KAAsB,EAAE,EAAE;YACxC,MAAM,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACtE,CAAC,CAAC;QACF,MAAM,QAAQ,GAAG,GAAuB,EAAE;YACxC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxB,OAAO,SAAS,CAAC;YACnB,CAAC;YACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAChD,CAAC,CAAC;QAEF,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QACvC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAExC,qEAAqE;QACrE,0EAA0E;QAC1E,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC;QAC9D,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;QAEhB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACzB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACvB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAE3B,IAAI,eAAe,IAAI,KAAK,IAAK,KAAkB,CAAC,aAAa,EAAE,CAAC;YAClE,KAAK,EAAE,CAAC;QACV,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/agents/cursor.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|