grounder 0.5.0 → 0.6.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 +21 -20
- package/dist/agents/claude.d.ts +4 -4
- package/dist/agents/claude.d.ts.map +1 -1
- package/dist/agents/claude.js +158 -179
- package/dist/agents/claude.js.map +1 -1
- package/dist/agents/cursor.d.ts +3 -3
- package/dist/agents/cursor.d.ts.map +1 -1
- package/dist/agents/cursor.js +97 -144
- package/dist/agents/cursor.js.map +1 -1
- package/dist/agents/home-skills.d.ts +46 -0
- package/dist/agents/home-skills.d.ts.map +1 -0
- package/dist/agents/home-skills.js +83 -0
- package/dist/agents/home-skills.js.map +1 -0
- package/dist/agents/hook-fragment.d.ts +52 -0
- package/dist/agents/hook-fragment.d.ts.map +1 -0
- package/dist/agents/hook-fragment.js +76 -0
- package/dist/agents/hook-fragment.js.map +1 -0
- package/dist/agents/hook-install.d.ts +62 -0
- package/dist/agents/hook-install.d.ts.map +1 -0
- package/dist/agents/hook-install.js +74 -0
- package/dist/agents/hook-install.js.map +1 -0
- package/dist/agents/hook-runtime.d.ts +77 -10
- package/dist/agents/hook-runtime.d.ts.map +1 -1
- package/dist/agents/hook-runtime.js +274 -70
- package/dist/agents/hook-runtime.js.map +1 -1
- package/dist/agents/index.d.ts +8 -14
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +13 -19
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/types.d.ts +56 -16
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/apply.d.ts +69 -0
- package/dist/commands/apply.d.ts.map +1 -0
- package/dist/commands/apply.js +238 -0
- package/dist/commands/apply.js.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +190 -97
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/handoff/peek.d.ts +1 -1
- package/dist/commands/handoff/peek.d.ts.map +1 -1
- package/dist/commands/handoff/peek.js +28 -12
- package/dist/commands/handoff/peek.js.map +1 -1
- package/dist/commands/install-drift.d.ts +34 -0
- package/dist/commands/install-drift.d.ts.map +1 -0
- package/dist/commands/install-drift.js +72 -0
- package/dist/commands/install-drift.js.map +1 -0
- package/dist/commands/migrate.d.ts +11 -3
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +67 -33
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/overview.d.ts +33 -0
- package/dist/commands/overview.d.ts.map +1 -0
- package/dist/commands/overview.js +150 -0
- package/dist/commands/overview.js.map +1 -0
- package/dist/commands/render-artifact-table.d.ts +85 -0
- package/dist/commands/render-artifact-table.d.ts.map +1 -0
- package/dist/commands/render-artifact-table.js +232 -0
- package/dist/commands/render-artifact-table.js.map +1 -0
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +15 -4
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +86 -13
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +269 -88
- package/dist/commands/status.js.map +1 -1
- package/dist/connector/ledger-migrations.d.ts +55 -0
- package/dist/connector/ledger-migrations.d.ts.map +1 -0
- package/dist/connector/ledger-migrations.js +94 -0
- package/dist/connector/ledger-migrations.js.map +1 -0
- package/dist/connector/state.d.ts +96 -59
- package/dist/connector/state.d.ts.map +1 -1
- package/dist/connector/state.js +165 -107
- package/dist/connector/state.js.map +1 -1
- package/dist/help.d.ts +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +27 -5
- package/dist/help.js.map +1 -1
- package/dist/reconcile/apply.d.ts +31 -0
- package/dist/reconcile/apply.d.ts.map +1 -0
- package/dist/reconcile/apply.js +97 -0
- package/dist/reconcile/apply.js.map +1 -0
- package/dist/reconcile/core.d.ts +82 -0
- package/dist/reconcile/core.d.ts.map +1 -0
- package/dist/reconcile/core.js +163 -0
- package/dist/reconcile/core.js.map +1 -0
- package/dist/reconcile/disk.d.ts +13 -0
- package/dist/reconcile/disk.d.ts.map +1 -0
- package/dist/reconcile/disk.js +28 -0
- package/dist/reconcile/disk.js.map +1 -0
- package/dist/util/fs.d.ts +14 -0
- package/dist/util/fs.d.ts.map +1 -1
- package/dist/util/fs.js +27 -1
- package/dist/util/fs.js.map +1 -1
- package/dist/util/merge-json.d.ts +13 -2
- package/dist/util/merge-json.d.ts.map +1 -1
- package/dist/util/merge-json.js +24 -10
- package/dist/util/merge-json.js.map +1 -1
- package/dist/vault/list-handoffs.d.ts +13 -3
- package/dist/vault/list-handoffs.d.ts.map +1 -1
- package/dist/vault/list-handoffs.js +28 -13
- package/dist/vault/list-handoffs.js.map +1 -1
- package/dist/vault/list-notes.d.ts +12 -3
- package/dist/vault/list-notes.d.ts.map +1 -1
- package/dist/vault/list-notes.js +13 -7
- package/dist/vault/list-notes.js.map +1 -1
- package/dist/vault/list-plans.d.ts +12 -3
- package/dist/vault/list-plans.d.ts.map +1 -1
- package/dist/vault/list-plans.js +13 -7
- package/dist/vault/list-plans.js.map +1 -1
- package/dist/vault/search.d.ts.map +1 -1
- package/dist/vault/search.js +128 -24
- package/dist/vault/search.js.map +1 -1
- package/package.json +2 -2
- package/templates/agents/claude/skills/grounder-handoff/SKILL.md +73 -0
- package/templates/agents/claude/{commands/grounder-note.md → skills/grounder-note/SKILL.md} +6 -0
- package/templates/agents/claude/skills/grounder-overview/SKILL.md +21 -0
- package/templates/agents/claude/{commands/grounder-plan.md → skills/grounder-plan/SKILL.md} +6 -0
- package/templates/agents/claude/skills/grounder-recall/SKILL.md +34 -0
- package/templates/agents/claude/{commands/grounder-search.md → skills/grounder-search/SKILL.md} +16 -8
- package/templates/agents/cursor/skills/grounder-handoff/SKILL.md +73 -0
- package/templates/agents/cursor/{commands/grounder-note.md → skills/grounder-note/SKILL.md} +6 -0
- package/templates/agents/cursor/skills/grounder-overview/SKILL.md +21 -0
- package/templates/agents/cursor/{commands/grounder-plan.md → skills/grounder-plan/SKILL.md} +6 -0
- package/templates/agents/cursor/skills/grounder-recall/SKILL.md +34 -0
- package/templates/agents/cursor/{commands/grounder-search.md → skills/grounder-search/SKILL.md} +16 -8
- package/templates/vault/plan.md +1 -1
- package/templates/vault/session-handoff.md +2 -1
- package/dist/agents/install-command.d.ts +0 -35
- package/dist/agents/install-command.d.ts.map +0 -1
- package/dist/agents/install-command.js +0 -65
- package/dist/agents/install-command.js.map +0 -1
- package/dist/commands/apply-agent-installs.d.ts +0 -34
- package/dist/commands/apply-agent-installs.d.ts.map +0 -1
- package/dist/commands/apply-agent-installs.js +0 -137
- package/dist/commands/apply-agent-installs.js.map +0 -1
- package/templates/agents/claude/commands/grounder-task-handoff.md +0 -58
- package/templates/agents/claude/commands/grounder-task.md +0 -22
- package/templates/agents/cursor/commands/grounder-task-handoff.md +0 -58
- package/templates/agents/cursor/commands/grounder-task.md +0 -22
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Session summaries, plans, and notes in files you own.
|
|
|
7
7
|
|
|
8
8
|
<picture>
|
|
9
9
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/andrej-kolic/grounder/main/docs/assets/what-dark.svg">
|
|
10
|
-
<img alt="Grounder connects Cursor, Claude Code, and more agents to one markdown vault through
|
|
10
|
+
<img alt="Grounder connects Cursor, Claude Code, and more agents to one markdown vault through skills and the CLI. The vault is a folder tree: 10-Projects/your-project/ containing plans (updated across sessions), notes (always a new file), and logs (saved session summaries). Caption: Any agent. One vault. Only when you ask." src="https://raw.githubusercontent.com/andrej-kolic/grounder/main/docs/assets/what.svg">
|
|
11
11
|
</picture>
|
|
12
12
|
|
|
13
13
|
<div> </div>
|
|
@@ -21,7 +21,7 @@ Session summaries, plans, and notes in files you own.
|
|
|
21
21
|
directory. Because it's files instead of chat history, work started in Cursor can be
|
|
22
22
|
picked up in Claude Code, weeks later, on a different machine.
|
|
23
23
|
|
|
24
|
-
The daily loop is five
|
|
24
|
+
The daily loop is five skills, typed as `/name` — [examples](#examples) show what you type.
|
|
25
25
|
|
|
26
26
|
## The problem
|
|
27
27
|
|
|
@@ -36,9 +36,9 @@ The daily loop is five slash commands — [examples](#examples) show what you ty
|
|
|
36
36
|
- **Plans live, sessions accumulate** — a plan updates in place across sessions; notes and handoffs are dated files, kept to go back to. `search` ranks across all of them.
|
|
37
37
|
- **Deliberate** — nothing is read or written until you run a command. No auto-capture, no RAG, no tokens spent otherwise.
|
|
38
38
|
- **Follows you across machines** — the vault is just a folder, so git (or Syncthing, or Dropbox) is all the sync you need.
|
|
39
|
-
- **Cursor and Claude Code today** —
|
|
39
|
+
- **Cursor and Claude Code today** — skills for both; more agents on the [roadmap](#roadmap).
|
|
40
40
|
|
|
41
|
-
**Requirements:** Node.js
|
|
41
|
+
**Requirements:** Node.js 22+ and a folder to keep the files in — an existing Obsidian
|
|
42
42
|
vault, or an empty directory Grounder fills over time. Git is optional.
|
|
43
43
|
|
|
44
44
|
## Install
|
|
@@ -63,7 +63,7 @@ run it (e.g. "set up grounder"). Skip the global install with
|
|
|
63
63
|
## Quickstart
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
# Once per machine — connect to a vault + install agent
|
|
66
|
+
# Once per machine — connect to a vault + install agent skills
|
|
67
67
|
grounder setup <path-to-your-vault> --hooks
|
|
68
68
|
|
|
69
69
|
# Once per project folder
|
|
@@ -78,12 +78,12 @@ one-line reminder at session start when a saved session exists — see
|
|
|
78
78
|
|
|
79
79
|
## Daily use
|
|
80
80
|
|
|
81
|
-
Work from the agent's chat. A typical loop starts by
|
|
81
|
+
Work from the agent's chat. A typical loop starts by recalling the last saved session and
|
|
82
82
|
ends by saving a short summary.
|
|
83
83
|
|
|
84
|
-
Anything after
|
|
85
|
-
it — writes a plan, saves a note, or searches the vault. `/grounder-
|
|
86
|
-
`/grounder-
|
|
84
|
+
Anything after the skill name is an instruction, not file content. The agent follows
|
|
85
|
+
it — writes a plan, saves a note, or searches the vault. `/grounder-recall` and
|
|
86
|
+
`/grounder-handoff` don't need text at all.
|
|
87
87
|
|
|
88
88
|
### Examples
|
|
89
89
|
|
|
@@ -91,22 +91,22 @@ A typical session:
|
|
|
91
91
|
|
|
92
92
|
| You type | What it does |
|
|
93
93
|
| -------- | ------------ |
|
|
94
|
-
| `/grounder-
|
|
94
|
+
| `/grounder-recall` | Recall the latest saved session |
|
|
95
95
|
| `/grounder-plan save insights from this session as an implementation plan with steps` | Write a named plan |
|
|
96
96
|
| `/grounder-search decisions and discussions on token refresh` | Search the vault |
|
|
97
97
|
| `/grounder-note explain why we rejected cookie sessions` | Save a new note |
|
|
98
|
-
| `/grounder-
|
|
98
|
+
| `/grounder-handoff` | Save a short session summary |
|
|
99
99
|
|
|
100
|
-
Later, update the living plan or
|
|
100
|
+
Later, update the living plan or recall a named session:
|
|
101
101
|
|
|
102
102
|
| You type | What it does |
|
|
103
103
|
| -------- | ------------ |
|
|
104
104
|
| `/grounder-plan update the auth rewrite plan — jwt validator is done` | Update an existing plan |
|
|
105
|
-
| `/grounder-
|
|
105
|
+
| `/grounder-recall the auth-middleware session` | Recall a specific saved session, not the latest |
|
|
106
106
|
|
|
107
107
|
### Demo
|
|
108
108
|
|
|
109
|
-

|
|
110
110
|
|
|
111
111
|
## How it works
|
|
112
112
|
|
|
@@ -133,7 +133,7 @@ Three things, and that's the whole model:
|
|
|
133
133
|
`10-Projects/` is a common Obsidian vault convention, so Grounder slots into an existing vault instead
|
|
134
134
|
of fighting it. A **handoff** is a saved session summary — what got done, what's next,
|
|
135
135
|
what's blocked — and they live under `logs/` because they accumulate one per session.
|
|
136
|
-
`/grounder-
|
|
136
|
+
`/grounder-recall` recalls the latest saved session by default, or an earlier one by name.
|
|
137
137
|
|
|
138
138
|
Here's the living plan, `plans/auth-rewrite.md`:
|
|
139
139
|
|
|
@@ -163,7 +163,7 @@ Machine config, the link marker, and how commands find the project:
|
|
|
163
163
|
|
|
164
164
|
## Commands
|
|
165
165
|
|
|
166
|
-
No agent, or want to write by hand? Each
|
|
166
|
+
No agent, or want to write by hand? Each skill has a matching CLI command.
|
|
167
167
|
Pass the file text (or search query) — not an instruction for the agent.
|
|
168
168
|
|
|
169
169
|
```bash
|
|
@@ -172,6 +172,7 @@ grounder note "Investigate auth middleware" # always a new note
|
|
|
172
172
|
grounder handoff $'# Handoff: ...\n\n## Next\n1. ...' # saved session summary
|
|
173
173
|
grounder search "token refresh" # find earlier documents on token refresh
|
|
174
174
|
grounder plan list # also: note list, handoff list
|
|
175
|
+
grounder overview # counts + recent titles, all three buckets
|
|
175
176
|
grounder status # am I wired up?
|
|
176
177
|
grounder doctor # why isn't this working?
|
|
177
178
|
```
|
|
@@ -184,11 +185,11 @@ Full flags and behavior: **[CLI reference](https://github.com/andrej-kolic/groun
|
|
|
184
185
|
|
|
185
186
|
Those are instructions written once: stable rules about the project. Grounder stores
|
|
186
187
|
what accumulates: what happened last session, what's next, the plan currently in flight.
|
|
187
|
-
They're complements — `/grounder-
|
|
188
|
+
They're complements — `/grounder-recall` reads the latest saved session *and* `AGENTS.md`.
|
|
188
189
|
|
|
189
190
|
### Is this an MCP server?
|
|
190
191
|
|
|
191
|
-
No. It's a CLI plus
|
|
192
|
+
No. It's a CLI plus skill files. Nothing is registered with the agent, nothing
|
|
192
193
|
runs in the background, and no tokens are spent until you type a command.
|
|
193
194
|
|
|
194
195
|
### Do I need Obsidian?
|
|
@@ -204,7 +205,7 @@ with the project.
|
|
|
204
205
|
|
|
205
206
|
### What does it put in my repo?
|
|
206
207
|
|
|
207
|
-
Only `.grounder.json` — two lines, safe to commit.
|
|
208
|
+
Only `.grounder.json` — two lines, safe to commit. Skill files go under `~/.cursor` and
|
|
208
209
|
`~/.claude`; vault content stays outside the project tree entirely.
|
|
209
210
|
|
|
210
211
|
### Does it capture my conversations automatically?
|
|
@@ -222,7 +223,7 @@ No. Nothing is written or loaded unless you ask. That's the point.
|
|
|
222
223
|
## Roadmap
|
|
223
224
|
|
|
224
225
|
- **Support for Copilot, Codex, and other popular agents** — expand beyond Cursor and Claude Code so more agent tools can use the same vault memory.
|
|
225
|
-
- **Auto-draft handoff on session end** (under consideration) — a hook that has the agent write the same structured Done/Next/Blockers checkpoint automatically, instead of requiring `/grounder-
|
|
226
|
+
- **Auto-draft handoff on session end** (under consideration) — a hook that has the agent write the same structured Done/Next/Blockers checkpoint automatically, instead of requiring `/grounder-handoff`.
|
|
226
227
|
|
|
227
228
|
## Development
|
|
228
229
|
|
package/dist/agents/claude.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentAdapter } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Canonical SessionStart command for Claude Code (home-local runtime, not `npx`).
|
|
4
|
-
* @see {@link peekHookCommand}
|
|
4
|
+
* @see {@link peekHookCommand}
|
|
5
5
|
*/
|
|
6
6
|
export declare function claudePeekHookCommand(homeDir?: string): string;
|
|
7
7
|
/**
|
|
@@ -9,13 +9,13 @@ export declare function claudePeekHookCommand(homeDir?: string): string;
|
|
|
9
9
|
* Excludes `resume` and `fork` — those sessions already carry prior context.
|
|
10
10
|
*/
|
|
11
11
|
export declare const CLAUDE_SESSION_START_MATCHER = "startup|clear|compact";
|
|
12
|
-
export declare function
|
|
12
|
+
export declare function claudeSkillsDir(homeDir?: string): string;
|
|
13
13
|
/** Absolute path to Claude Code's shared settings file (`~/.claude/settings.json`). */
|
|
14
14
|
export declare function claudeSettingsJsonPath(homeDir?: string): string;
|
|
15
15
|
export declare function grounderNoteCommandPath(homeDir?: string): string;
|
|
16
16
|
export declare function grounderPlanCommandPath(homeDir?: string): string;
|
|
17
|
-
export declare function
|
|
18
|
-
export declare function
|
|
17
|
+
export declare function grounderHandoffCommandPath(homeDir?: string): string;
|
|
18
|
+
export declare function grounderRecallCommandPath(homeDir?: string): string;
|
|
19
19
|
export declare function expectedArtifacts(homeDir?: string): string[];
|
|
20
20
|
/** Paths of hook config this adapter touches — currently just `settings.json`. */
|
|
21
21
|
export declare function expectedHookArtifacts(homeDir?: string): string[];
|
|
@@ -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":"AAQA,OAAO,KAAK,EAAE,YAAY,EAA2C,MAAM,YAAY,CAAC;AAQxF;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,0BAA0B,CAAC;AAEpE,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAExD;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,uBAAuB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,0BAA0B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAElE;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;AAyPD,eAAO,MAAM,MAAM,EAAE,YAapB,CAAC"}
|
package/dist/agents/claude.js
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
import { resolveHomeDir } from "../connector/home.js";
|
|
5
4
|
import { fileExists } from "../util/fs.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"grounder-plan.md",
|
|
15
|
-
"grounder-task-handoff.md",
|
|
16
|
-
"grounder-task.md",
|
|
17
|
-
];
|
|
5
|
+
import { homeSkillsLayout } from "./home-skills.js";
|
|
6
|
+
import { readEventEntries, readHooksObject, removeMatchingEntries } from "./hook-fragment.js";
|
|
7
|
+
import { installHookFragment, removeHookFragment } from "./hook-install.js";
|
|
8
|
+
import { isGrounderPeekHookCommand, isHookRuntimeStale, peekHookCommand } from "./hook-runtime.js";
|
|
9
|
+
/** Whole-file artifacts: `~/.claude/skills/` plus the retired `~/.claude/commands/`. */
|
|
10
|
+
const layout = homeSkillsLayout({ id: "claude", agentDir: ".claude" });
|
|
11
|
+
/** Hook event Grounder owns in `settings.json`. */
|
|
12
|
+
const SESSION_START_EVENT = "SessionStart";
|
|
18
13
|
/**
|
|
19
14
|
* Canonical SessionStart command for Claude Code (home-local runtime, not `npx`).
|
|
20
|
-
* @see {@link peekHookCommand}
|
|
15
|
+
* @see {@link peekHookCommand}
|
|
21
16
|
*/
|
|
22
17
|
export function claudePeekHookCommand(homeDir) {
|
|
23
18
|
return peekHookCommand(homeDir);
|
|
@@ -27,27 +22,27 @@ export function claudePeekHookCommand(homeDir) {
|
|
|
27
22
|
* Excludes `resume` and `fork` — those sessions already carry prior context.
|
|
28
23
|
*/
|
|
29
24
|
export const CLAUDE_SESSION_START_MATCHER = "startup|clear|compact";
|
|
30
|
-
export function
|
|
31
|
-
return
|
|
25
|
+
export function claudeSkillsDir(homeDir) {
|
|
26
|
+
return layout.skillsDir(homeDir);
|
|
32
27
|
}
|
|
33
28
|
/** Absolute path to Claude Code's shared settings file (`~/.claude/settings.json`). */
|
|
34
29
|
export function claudeSettingsJsonPath(homeDir) {
|
|
35
30
|
return path.join(resolveHomeDir(homeDir), ".claude", "settings.json");
|
|
36
31
|
}
|
|
37
32
|
export function grounderNoteCommandPath(homeDir) {
|
|
38
|
-
return
|
|
33
|
+
return layout.skillPath("grounder-note", homeDir);
|
|
39
34
|
}
|
|
40
35
|
export function grounderPlanCommandPath(homeDir) {
|
|
41
|
-
return
|
|
36
|
+
return layout.skillPath("grounder-plan", homeDir);
|
|
42
37
|
}
|
|
43
|
-
export function
|
|
44
|
-
return
|
|
38
|
+
export function grounderHandoffCommandPath(homeDir) {
|
|
39
|
+
return layout.skillPath("grounder-handoff", homeDir);
|
|
45
40
|
}
|
|
46
|
-
export function
|
|
47
|
-
return
|
|
41
|
+
export function grounderRecallCommandPath(homeDir) {
|
|
42
|
+
return layout.skillPath("grounder-recall", homeDir);
|
|
48
43
|
}
|
|
49
44
|
export function expectedArtifacts(homeDir) {
|
|
50
|
-
return
|
|
45
|
+
return layout.expectedArtifacts(homeDir);
|
|
51
46
|
}
|
|
52
47
|
/** Paths of hook config this adapter touches — currently just `settings.json`. */
|
|
53
48
|
export function expectedHookArtifacts(homeDir) {
|
|
@@ -83,62 +78,86 @@ export function expectedHookArtifacts(homeDir) {
|
|
|
83
78
|
// - matcher group → { matcher, hooks: Hook[] }
|
|
84
79
|
// - hook entry → { type: "command", command: string }
|
|
85
80
|
//
|
|
86
|
-
// Idempotency: {@link isGrounderPeekHookCommand} (runtime path
|
|
81
|
+
// Idempotency / recognizer: {@link isGrounderPeekHookCommand} (runtime path
|
|
82
|
+
// or legacy npx). Always-converge (Ansible `blockinfile` / Kubernetes
|
|
83
|
+
// Server-Side Apply sole-owner model): every recognizer match — however many,
|
|
84
|
+
// wherever they live across matcher groups — is removed and replaced with
|
|
85
|
+
// exactly one canonical entry. No conflict / `--force` gate.
|
|
87
86
|
// ---------------------------------------------------------------------------
|
|
88
87
|
function peekHookEntry(homeDir) {
|
|
89
88
|
return { type: "command", command: claudePeekHookCommand(homeDir) };
|
|
90
89
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
90
|
+
function isClaudeHookEntry(hook) {
|
|
91
|
+
return (hook !== null &&
|
|
92
|
+
typeof hook === "object" &&
|
|
93
|
+
!Array.isArray(hook) &&
|
|
94
|
+
hook.type === "command" &&
|
|
95
|
+
isGrounderPeekHookCommand(hook.command));
|
|
96
|
+
}
|
|
97
|
+
function isMatcherGroup(group) {
|
|
98
|
+
return group !== null && typeof group === "object" && !Array.isArray(group);
|
|
99
|
+
}
|
|
100
|
+
/** Every Grounder hook entry across every matcher group, tagged with its group's matcher. */
|
|
101
|
+
function findAllPeekHooksPlaced(sessionStart) {
|
|
102
|
+
const found = [];
|
|
103
|
+
for (const group of sessionStart) {
|
|
104
|
+
if (!isMatcherGroup(group) || !Array.isArray(group.hooks)) {
|
|
105
105
|
continue;
|
|
106
106
|
}
|
|
107
|
-
for (
|
|
108
|
-
|
|
109
|
-
if (hook &&
|
|
110
|
-
typeof hook === "object" &&
|
|
111
|
-
!Array.isArray(hook) &&
|
|
112
|
-
isGrounderPeekHookCommand(hook.command)) {
|
|
113
|
-
return { groupIdx, hookIdx };
|
|
114
|
-
}
|
|
107
|
+
for (const hook of group.hooks.filter(isClaudeHookEntry)) {
|
|
108
|
+
found.push({ matcher: group.matcher, hook });
|
|
115
109
|
}
|
|
116
110
|
}
|
|
117
|
-
return
|
|
111
|
+
return found;
|
|
118
112
|
}
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return false;
|
|
113
|
+
/** Every Grounder hook entry across every matcher group, flattened. */
|
|
114
|
+
function findAllPeekHooks(sessionStart) {
|
|
115
|
+
return findAllPeekHooksPlaced(sessionStart).map((placed) => placed.hook);
|
|
116
|
+
}
|
|
117
|
+
/** Remove every Grounder hook entry from every matcher group's `hooks` array. */
|
|
118
|
+
function removeAllPeekHooks(sessionStart) {
|
|
119
|
+
return sessionStart.map((group) => {
|
|
120
|
+
if (!isMatcherGroup(group) || !Array.isArray(group.hooks)) {
|
|
121
|
+
return group;
|
|
129
122
|
}
|
|
130
|
-
|
|
131
|
-
|
|
123
|
+
return { ...group, hooks: removeMatchingEntries(group.hooks, isClaudeHookEntry) };
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function hasHooks(group) {
|
|
127
|
+
return isMatcherGroup(group) && Array.isArray(group.hooks) && group.hooks.length > 0;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Drop matcher groups that removal itself emptied — i.e. `next[i]` has no
|
|
131
|
+
* hooks left but `before[i]` (same index, pre-removal) did. A group that was
|
|
132
|
+
* already empty for reasons of its own (not something Grounder touched) is
|
|
133
|
+
* left alone: it isn't clutter Grounder created room for, so it isn't
|
|
134
|
+
* Grounder's call to delete it. `next` may be longer than `before` (a newly
|
|
135
|
+
* appended canonical group) — those extra entries always have hooks, so the
|
|
136
|
+
* length mismatch never reaches the `before[i]` lookup.
|
|
137
|
+
*/
|
|
138
|
+
function dropGroupsEmptiedByRemoval(before, next) {
|
|
139
|
+
return next.filter((group, i) => hasHooks(group) || !hasHooks(before[i]));
|
|
140
|
+
}
|
|
141
|
+
/** `hooks.SessionStart` from a settings.json on disk; `null` if absent or unreadable. */
|
|
142
|
+
async function readSessionStart(filePath) {
|
|
143
|
+
try {
|
|
144
|
+
return readEventEntries(JSON.parse(await readFile(filePath, "utf8")), SESSION_START_EVENT);
|
|
132
145
|
}
|
|
133
146
|
catch {
|
|
134
|
-
return
|
|
147
|
+
return null;
|
|
135
148
|
}
|
|
136
149
|
}
|
|
150
|
+
/** Whether `settings.json` already lists any Grounder peek command (for status labeling). */
|
|
151
|
+
async function peekHookHadGrounderEntry(filePath) {
|
|
152
|
+
return findAllPeekHooks((await readSessionStart(filePath)) ?? []).length > 0;
|
|
153
|
+
}
|
|
137
154
|
/**
|
|
138
|
-
* Skip only when
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
155
|
+
* Skip only when exactly one canonical entry is already present *under the
|
|
156
|
+
* canonical matcher group* ({@link CLAUDE_SESSION_START_MATCHER}) *and* the
|
|
157
|
+
* runtime is current for the running grounder version/source. Anything else
|
|
158
|
+
* — no entry, a legacy `npx` form, a drifted command, more than one match
|
|
159
|
+
* (however scattered across matcher groups), or the one match sitting under
|
|
160
|
+
* the wrong matcher — always converges.
|
|
142
161
|
*/
|
|
143
162
|
async function peekHookUpToDate(filePath, homeDir) {
|
|
144
163
|
if (!(await fileExists(filePath))) {
|
|
@@ -147,149 +166,109 @@ async function peekHookUpToDate(filePath, homeDir) {
|
|
|
147
166
|
if (await isHookRuntimeStale(homeDir)) {
|
|
148
167
|
return false;
|
|
149
168
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
const sessionStart = await readSessionStart(filePath);
|
|
170
|
+
if (!sessionStart) {
|
|
171
|
+
return false;
|
|
169
172
|
}
|
|
170
|
-
|
|
173
|
+
const placed = findAllPeekHooksPlaced(sessionStart);
|
|
174
|
+
if (placed.length !== 1 || placed[0].matcher !== CLAUDE_SESSION_START_MATCHER) {
|
|
171
175
|
return false;
|
|
172
176
|
}
|
|
177
|
+
return JSON.stringify(placed[0].hook) === JSON.stringify(peekHookEntry(homeDir));
|
|
173
178
|
}
|
|
174
179
|
/**
|
|
175
|
-
*
|
|
180
|
+
* Converge Grounder's SessionStart hook into an existing settings object:
|
|
181
|
+
* remove every recognizer match from every matcher group, then insert
|
|
182
|
+
* exactly one canonical entry.
|
|
183
|
+
*
|
|
184
|
+
* Preserves every key except the nested path it owns. Insertion strategy
|
|
185
|
+
* (after removal, so this always runs against a Grounder-free tree):
|
|
186
|
+
* 1. A matcher group with `matcher === CLAUDE_SESSION_START_MATCHER` exists
|
|
187
|
+
* (possibly the very one a match was just removed from) → append there.
|
|
188
|
+
* 2. Else → push a new matcher group with Grounder's hook.
|
|
176
189
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* append Grounder's hook to that group's `hooks` array.
|
|
181
|
-
* 3. Else → push a new matcher group with Grounder's hook.
|
|
190
|
+
* Any matcher group the removal step emptied — e.g. a Grounder-only hook
|
|
191
|
+
* that lived under a non-canonical matcher — is dropped rather than left
|
|
192
|
+
* behind as clutter, matching {@link removeClaudeHooks}'s uninstall path.
|
|
182
193
|
*
|
|
183
194
|
* @param current - Parsed settings.json root (object). Other top-level keys untouched.
|
|
184
195
|
* @param homeDir - Home override for the canonical command path
|
|
185
196
|
* @returns New settings object with `hooks.SessionStart` updated
|
|
186
197
|
*/
|
|
187
198
|
function mergeClaudeHooks(current, homeDir) {
|
|
188
|
-
const hooks = current
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
const found = findPeekHook(sessionStart);
|
|
199
|
+
const hooks = readHooksObject(current, claudeSettingsJsonPath(homeDir));
|
|
200
|
+
const raw = hooks[SESSION_START_EVENT];
|
|
201
|
+
const sessionStart = Array.isArray(raw) ? raw : [];
|
|
202
|
+
const cleaned = removeAllPeekHooks(sessionStart);
|
|
193
203
|
const entry = peekHookEntry(homeDir);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
204
|
+
const matcherIdx = cleaned.findIndex((group) => isMatcherGroup(group) && group.matcher === CLAUDE_SESSION_START_MATCHER);
|
|
205
|
+
let nextSessionStart;
|
|
206
|
+
if (matcherIdx >= 0) {
|
|
207
|
+
const group = { ...cleaned[matcherIdx] };
|
|
197
208
|
const groupHooks = Array.isArray(group.hooks) ? [...group.hooks] : [];
|
|
198
|
-
groupHooks
|
|
209
|
+
groupHooks.push(entry);
|
|
199
210
|
group.hooks = groupHooks;
|
|
200
|
-
|
|
211
|
+
nextSessionStart = [...cleaned];
|
|
212
|
+
nextSessionStart[matcherIdx] = group;
|
|
201
213
|
}
|
|
202
214
|
else {
|
|
203
|
-
|
|
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
|
-
}
|
|
215
|
+
nextSessionStart = [...cleaned, { matcher: CLAUDE_SESSION_START_MATCHER, hooks: [entry] }];
|
|
222
216
|
}
|
|
223
|
-
|
|
217
|
+
nextSessionStart = dropGroupsEmptiedByRemoval(sessionStart, nextSessionStart);
|
|
218
|
+
return { ...current, hooks: { ...hooks, [SESSION_START_EVENT]: nextSessionStart } };
|
|
224
219
|
}
|
|
225
220
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* Unparseable settings.json: {@link mergeJsonFile} backs off and this throws (never clobbers).
|
|
221
|
+
* Remove every Grounder hook entry from every matcher group, touching
|
|
222
|
+
* nothing else, and drop a matcher group only when this removal is what left
|
|
223
|
+
* its `hooks` array empty (see {@link dropGroupsEmptiedByRemoval}) — clutter
|
|
224
|
+
* Grounder itself created room for, not a group that started empty for
|
|
225
|
+
* reasons of its own. Returns `current` verbatim when there's no Grounder
|
|
226
|
+
* entry to remove, so `mergeJsonFile` sees no change and leaves an unrelated
|
|
227
|
+
* `settings.json` untouched instead of reformatting it.
|
|
235
228
|
*/
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
const hadGrounderEntry = fileExisted && (await peekHookHadGrounderEntry(dest));
|
|
245
|
-
const status = hadGrounderEntry ? "overwritten" : "created";
|
|
246
|
-
return { artifacts: { [dest]: status } };
|
|
247
|
-
}
|
|
248
|
-
await installHookRuntime({ homeDir: opts.homeDir });
|
|
249
|
-
const fileExisted = await fileExists(dest);
|
|
250
|
-
const hadGrounderEntry = fileExisted && (await peekHookHadGrounderEntry(dest));
|
|
251
|
-
const result = await mergeJsonFile(dest, (current) => mergeClaudeHooks(current, opts.homeDir));
|
|
252
|
-
if (!result.ok) {
|
|
253
|
-
throw new Error(result.message);
|
|
229
|
+
function removeClaudeHooks(current) {
|
|
230
|
+
const hooks = current.hooks && typeof current.hooks === "object" && !Array.isArray(current.hooks)
|
|
231
|
+
? current.hooks
|
|
232
|
+
: undefined;
|
|
233
|
+
const raw = hooks?.[SESSION_START_EVENT];
|
|
234
|
+
const sessionStart = Array.isArray(raw) ? raw : [];
|
|
235
|
+
if (findAllPeekHooks(sessionStart).length === 0) {
|
|
236
|
+
return current;
|
|
254
237
|
}
|
|
255
|
-
const
|
|
256
|
-
return {
|
|
238
|
+
const cleaned = dropGroupsEmptiedByRemoval(sessionStart, removeAllPeekHooks(sessionStart));
|
|
239
|
+
return { ...current, hooks: { ...hooks, [SESSION_START_EVENT]: cleaned } };
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Install (or converge) Grounder's SessionStart teaser hook into `~/.claude/settings.json`.
|
|
243
|
+
* @see {@link installHookFragment} for the shared install/report scaffolding.
|
|
244
|
+
*/
|
|
245
|
+
async function installHooks(opts) {
|
|
246
|
+
return installHookFragment({
|
|
247
|
+
dest: claudeSettingsJsonPath(opts.homeDir),
|
|
248
|
+
isUpToDate: (filePath) => peekHookUpToDate(filePath, opts.homeDir),
|
|
249
|
+
hasGrounderEntry: peekHookHadGrounderEntry,
|
|
250
|
+
merge: (current) => mergeClaudeHooks(current, opts.homeDir),
|
|
251
|
+
}, opts);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Remove Grounder's SessionStart hook entry entirely (`--no-hooks`) — the
|
|
255
|
+
* opt-out must also remove the fragment, not just flip `hooksEnabled` false,
|
|
256
|
+
* or the session hook keeps firing and the tri-state's stickiness against
|
|
257
|
+
* the next plain `migrate` would have nothing to rest on.
|
|
258
|
+
*/
|
|
259
|
+
async function removeHooks(opts) {
|
|
260
|
+
return removeHookFragment(claudeSettingsJsonPath(opts.homeDir), removeClaudeHooks, opts);
|
|
257
261
|
}
|
|
258
262
|
export const claude = {
|
|
259
263
|
id: "claude",
|
|
260
264
|
name: "Claude Code",
|
|
261
|
-
|
|
262
|
-
hooksSchema: 1,
|
|
263
|
-
async isInstalled() {
|
|
264
|
-
return fileExists(path.join(resolveHomeDir(), ".claude"));
|
|
265
|
-
},
|
|
265
|
+
isInstalled: layout.isInstalled,
|
|
266
266
|
expectedArtifacts,
|
|
267
267
|
expectedHookArtifacts,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
for (const filename of COMMANDS) {
|
|
272
|
-
const { dest, status, hash } = await installCommandFile({
|
|
273
|
-
...opts,
|
|
274
|
-
agentId: claude.id,
|
|
275
|
-
templateDir,
|
|
276
|
-
commandsDir: claudeCommandsDir(opts.homeDir),
|
|
277
|
-
filename,
|
|
278
|
-
});
|
|
279
|
-
artifacts[dest] = status;
|
|
280
|
-
if (hash) {
|
|
281
|
-
files[dest] = { hash };
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
await recordCommandFileHashes({
|
|
285
|
-
agentId: claude.id,
|
|
286
|
-
commandsSchema: claude.commandsSchema,
|
|
287
|
-
files,
|
|
288
|
-
homeDir: opts.homeDir,
|
|
289
|
-
dryRun: opts.dryRun,
|
|
290
|
-
});
|
|
291
|
-
return { artifacts };
|
|
292
|
-
},
|
|
268
|
+
desiredArtifacts: layout.desiredArtifacts,
|
|
269
|
+
tombstones: layout.tombstones,
|
|
270
|
+
ownedPrefixes: layout.ownedPrefixes,
|
|
293
271
|
installHooks,
|
|
272
|
+
removeHooks,
|
|
294
273
|
};
|
|
295
274
|
//# sourceMappingURL=claude.js.map
|