grounder 0.4.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 +150 -271
- package/dist/agents/claude.d.ts +4 -4
- package/dist/agents/claude.d.ts.map +1 -1
- package/dist/agents/claude.js +158 -178
- 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 -143
- 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 +80 -13
- package/dist/agents/hook-runtime.d.ts.map +1 -1
- package/dist/agents/hook-runtime.js +278 -74
- 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 +15 -10
- 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 +197 -101
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/handoff/list.d.ts +23 -13
- package/dist/commands/handoff/list.d.ts.map +1 -1
- package/dist/commands/handoff/list.js +28 -16
- package/dist/commands/handoff/list.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/handoff.d.ts +2 -0
- package/dist/commands/handoff.d.ts.map +1 -1
- package/dist/commands/handoff.js +10 -1
- package/dist/commands/handoff.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/link.d.ts +12 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/{repo/init.js → link.js} +35 -27
- package/dist/commands/link.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 +66 -34
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/note/list.d.ts +17 -9
- package/dist/commands/note/list.d.ts.map +1 -1
- package/dist/commands/note/list.js +19 -14
- package/dist/commands/note/list.js.map +1 -1
- package/dist/commands/note.d.ts +2 -0
- package/dist/commands/note.d.ts.map +1 -1
- package/dist/commands/note.js +10 -1
- package/dist/commands/note.js.map +1 -1
- package/dist/commands/output.d.ts +22 -2
- package/dist/commands/output.d.ts.map +1 -1
- package/dist/commands/output.js +18 -6
- package/dist/commands/output.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/plan/list.d.ts +20 -12
- package/dist/commands/plan/list.d.ts.map +1 -1
- package/dist/commands/plan/list.js +22 -17
- package/dist/commands/plan/list.js.map +1 -1
- package/dist/commands/plan.d.ts +2 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +12 -2
- package/dist/commands/plan.js.map +1 -1
- 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/require-linked.js +2 -2
- package/dist/commands/require-linked.js.map +1 -1
- package/dist/commands/search.d.ts +23 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +288 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/{vault/init.d.ts → setup.d.ts} +5 -4
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +210 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +273 -83
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/upgrade-banner.d.ts +2 -2
- package/dist/commands/upgrade-banner.js +2 -2
- package/dist/connector/home.d.ts +12 -0
- package/dist/connector/home.d.ts.map +1 -1
- package/dist/connector/home.js +26 -2
- package/dist/connector/home.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 +97 -60
- 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/connector/vault.d.ts +2 -0
- package/dist/connector/vault.d.ts.map +1 -1
- package/dist/connector/vault.js +6 -1
- package/dist/connector/vault.js.map +1 -1
- package/dist/help.d.ts +3 -3
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +97 -56
- 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/frontmatter.d.ts +9 -7
- package/dist/util/frontmatter.d.ts.map +1 -1
- package/dist/util/frontmatter.js +29 -4
- package/dist/util/frontmatter.js.map +1 -1
- 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/util/path.d.ts +23 -0
- package/dist/util/path.d.ts.map +1 -1
- package/dist/util/path.js +39 -0
- package/dist/util/path.js.map +1 -1
- package/dist/vault/list-handoffs.d.ts +14 -2
- package/dist/vault/list-handoffs.d.ts.map +1 -1
- package/dist/vault/list-handoffs.js +40 -23
- package/dist/vault/list-handoffs.js.map +1 -1
- package/dist/vault/list-markdown.d.ts +8 -0
- package/dist/vault/list-markdown.d.ts.map +1 -0
- package/dist/vault/list-markdown.js +35 -0
- package/dist/vault/list-markdown.js.map +1 -0
- package/dist/vault/list-notes.d.ts +15 -6
- package/dist/vault/list-notes.d.ts.map +1 -1
- package/dist/vault/list-notes.js +23 -27
- 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 +20 -24
- package/dist/vault/list-plans.js.map +1 -1
- package/dist/vault/search.d.ts +55 -0
- package/dist/vault/search.d.ts.map +1 -0
- package/dist/vault/search.js +432 -0
- package/dist/vault/search.js.map +1 -0
- package/dist/vault/write-handoff.d.ts +2 -0
- package/dist/vault/write-handoff.d.ts.map +1 -1
- package/dist/vault/write-handoff.js +5 -0
- package/dist/vault/write-handoff.js.map +1 -1
- package/dist/vault/write-note.d.ts +2 -0
- package/dist/vault/write-note.d.ts.map +1 -1
- package/dist/vault/write-note.js +9 -1
- package/dist/vault/write-note.js.map +1 -1
- package/dist/vault/write-plan.d.ts +4 -0
- package/dist/vault/write-plan.d.ts.map +1 -1
- package/dist/vault/write-plan.js +14 -1
- package/dist/vault/write-plan.js.map +1 -1
- package/package.json +11 -4
- package/templates/agents/claude/skills/grounder-handoff/SKILL.md +73 -0
- package/templates/agents/claude/{commands/grounder-note.md → skills/grounder-note/SKILL.md} +12 -2
- package/templates/agents/claude/skills/grounder-overview/SKILL.md +21 -0
- package/templates/agents/claude/{commands/grounder-plan.md → skills/grounder-plan/SKILL.md} +13 -5
- package/templates/agents/claude/skills/grounder-recall/SKILL.md +34 -0
- package/templates/agents/claude/skills/grounder-search/SKILL.md +152 -0
- package/templates/agents/cursor/skills/grounder-handoff/SKILL.md +73 -0
- package/templates/agents/cursor/{commands/grounder-note.md → skills/grounder-note/SKILL.md} +12 -2
- package/templates/agents/cursor/skills/grounder-overview/SKILL.md +21 -0
- package/templates/agents/cursor/{commands/grounder-plan.md → skills/grounder-plan/SKILL.md} +13 -5
- package/templates/agents/cursor/skills/grounder-recall/SKILL.md +34 -0
- package/templates/agents/cursor/skills/grounder-search/SKILL.md +152 -0
- 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/dist/commands/repo/init.d.ts +0 -11
- package/dist/commands/repo/init.d.ts.map +0 -1
- package/dist/commands/repo/init.js.map +0 -1
- package/dist/commands/vault/init.d.ts.map +0 -1
- package/dist/commands/vault/init.js +0 -90
- package/dist/commands/vault/init.js.map +0 -1
- package/templates/agents/claude/commands/grounder-task-handoff.md +0 -54
- package/templates/agents/claude/commands/grounder-task.md +0 -22
- package/templates/agents/cursor/commands/grounder-task-handoff.md +0 -54
- package/templates/agents/cursor/commands/grounder-task.md +0 -22
package/dist/agents/cursor.js
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
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-task-handoff.md",
|
|
15
|
-
"grounder-task.md",
|
|
16
|
-
];
|
|
5
|
+
import { homeSkillsLayout } from "./home-skills.js";
|
|
6
|
+
import { isAlreadyConverged, 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: `~/.cursor/skills/` plus the retired `~/.cursor/commands/`. */
|
|
10
|
+
const layout = homeSkillsLayout({ id: "cursor", agentDir: ".cursor" });
|
|
11
|
+
/** Hook event Grounder owns in `hooks.json`. */
|
|
12
|
+
const SESSION_START_EVENT = "sessionStart";
|
|
17
13
|
/**
|
|
18
14
|
* Canonical sessionStart command for Cursor (home-local runtime, not `npx`).
|
|
19
15
|
* Always includes `--json` so stdout matches Cursor's `additional_context` contract.
|
|
@@ -22,27 +18,27 @@ const COMMANDS = [
|
|
|
22
18
|
export function cursorPeekHookCommand(homeDir, extraArgs = ["--json"]) {
|
|
23
19
|
return peekHookCommand(homeDir, extraArgs);
|
|
24
20
|
}
|
|
25
|
-
export function
|
|
26
|
-
return
|
|
21
|
+
export function cursorSkillsDir(homeDir) {
|
|
22
|
+
return layout.skillsDir(homeDir);
|
|
27
23
|
}
|
|
28
24
|
/** Absolute path to Cursor's shared hooks config (`~/.cursor/hooks.json`). */
|
|
29
25
|
export function cursorHooksJsonPath(homeDir) {
|
|
30
26
|
return path.join(resolveHomeDir(homeDir), ".cursor", "hooks.json");
|
|
31
27
|
}
|
|
32
28
|
export function grounderNoteCommandPath(homeDir) {
|
|
33
|
-
return
|
|
29
|
+
return layout.skillPath("grounder-note", homeDir);
|
|
34
30
|
}
|
|
35
31
|
export function grounderPlanCommandPath(homeDir) {
|
|
36
|
-
return
|
|
32
|
+
return layout.skillPath("grounder-plan", homeDir);
|
|
37
33
|
}
|
|
38
|
-
export function
|
|
39
|
-
return
|
|
34
|
+
export function grounderHandoffCommandPath(homeDir) {
|
|
35
|
+
return layout.skillPath("grounder-handoff", homeDir);
|
|
40
36
|
}
|
|
41
|
-
export function
|
|
42
|
-
return
|
|
37
|
+
export function grounderRecallCommandPath(homeDir) {
|
|
38
|
+
return layout.skillPath("grounder-recall", homeDir);
|
|
43
39
|
}
|
|
44
40
|
export function expectedArtifacts(homeDir) {
|
|
45
|
-
return
|
|
41
|
+
return layout.expectedArtifacts(homeDir);
|
|
46
42
|
}
|
|
47
43
|
/** Paths of hook config this adapter touches — currently just `hooks.json`. */
|
|
48
44
|
export function expectedHookArtifacts(homeDir) {
|
|
@@ -71,37 +67,35 @@ export function expectedHookArtifacts(homeDir) {
|
|
|
71
67
|
// - hook entry → { command: string }
|
|
72
68
|
//
|
|
73
69
|
// Unlike Claude Code, Cursor has no matcher groups — sessionStart is a flat
|
|
74
|
-
// array. Idempotency: {@link isGrounderPeekHookCommand} (runtime
|
|
70
|
+
// array. Idempotency / recognizer: {@link isGrounderPeekHookCommand} (runtime
|
|
71
|
+
// path or legacy npx). Always-converge (Ansible `blockinfile` / Kubernetes
|
|
72
|
+
// Server-Side Apply sole-owner model): every recognizer match is removed and
|
|
73
|
+
// replaced with exactly one canonical entry — no conflict / `--force` gate.
|
|
75
74
|
// ---------------------------------------------------------------------------
|
|
76
|
-
function
|
|
77
|
-
return
|
|
75
|
+
function isCursorPeekEntry(item) {
|
|
76
|
+
return (item !== null &&
|
|
78
77
|
typeof item === "object" &&
|
|
79
78
|
!Array.isArray(item) &&
|
|
80
79
|
isGrounderPeekHookCommand(item.command));
|
|
81
80
|
}
|
|
82
|
-
/**
|
|
83
|
-
async function
|
|
81
|
+
/** `hooks.sessionStart` from a hooks.json on disk; `null` if absent or unreadable. */
|
|
82
|
+
async function readSessionStart(filePath) {
|
|
84
83
|
try {
|
|
85
|
-
|
|
86
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
const hooks = parsed.hooks;
|
|
90
|
-
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
const sessionStart = hooks.sessionStart;
|
|
94
|
-
return Array.isArray(sessionStart) && findPeekHookIndex(sessionStart) >= 0;
|
|
84
|
+
return readEventEntries(JSON.parse(await readFile(filePath, "utf8")), SESSION_START_EVENT);
|
|
95
85
|
}
|
|
96
86
|
catch {
|
|
97
|
-
return
|
|
87
|
+
return null;
|
|
98
88
|
}
|
|
99
89
|
}
|
|
90
|
+
/** Whether `hooks.json` already lists any Grounder peek command (for status labeling). */
|
|
91
|
+
async function peekHookHadGrounderEntry(filePath) {
|
|
92
|
+
return ((await readSessionStart(filePath)) ?? []).some(isCursorPeekEntry);
|
|
93
|
+
}
|
|
100
94
|
/**
|
|
101
|
-
* Skip only when
|
|
102
|
-
* current for the running grounder version/source.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
95
|
+
* Skip only when exactly one canonical entry is already present *and* the
|
|
96
|
+
* runtime is current for the running grounder version/source. Anything else
|
|
97
|
+
* — no entry, a legacy `npx` form, a drifted command, more than one match —
|
|
98
|
+
* always converges. No `--force` required to migrate or pick up an upgrade.
|
|
105
99
|
*/
|
|
106
100
|
async function peekHookUpToDate(filePath, homeDir) {
|
|
107
101
|
if (!(await fileExists(filePath))) {
|
|
@@ -110,39 +104,22 @@ async function peekHookUpToDate(filePath, homeDir) {
|
|
|
110
104
|
if (await isHookRuntimeStale(homeDir)) {
|
|
111
105
|
return false;
|
|
112
106
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
const hooks = parsed.hooks;
|
|
119
|
-
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
const sessionStart = hooks.sessionStart;
|
|
123
|
-
if (!Array.isArray(sessionStart)) {
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
const idx = findPeekHookIndex(sessionStart);
|
|
127
|
-
if (idx < 0) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
const command = sessionStart[idx].command;
|
|
131
|
-
return command === cursorPeekHookCommand(homeDir);
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
107
|
+
const sessionStart = await readSessionStart(filePath);
|
|
108
|
+
if (!sessionStart) {
|
|
134
109
|
return false;
|
|
135
110
|
}
|
|
111
|
+
return isAlreadyConverged(sessionStart, isCursorPeekEntry, {
|
|
112
|
+
command: cursorPeekHookCommand(homeDir),
|
|
113
|
+
});
|
|
136
114
|
}
|
|
137
115
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* 1. If a Grounder peek hook already exists (runtime or legacy npx) → replace in place.
|
|
142
|
-
* 2. Else → append Grounder's entry to `hooks.sessionStart`.
|
|
116
|
+
* Converge Grounder's sessionStart hook into an existing hooks.json object:
|
|
117
|
+
* remove every recognizer match (runtime-form or legacy npx, however many),
|
|
118
|
+
* append exactly one canonical entry.
|
|
143
119
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
120
|
+
* Preserves every key except the nested path it owns. On a brand-new file
|
|
121
|
+
* (`isFreshFile`), also sets `version: 1` when absent. Existing files keep
|
|
122
|
+
* whatever `version` (or lack of it) they already have.
|
|
146
123
|
*
|
|
147
124
|
* @param current - Parsed hooks.json root (object). Other top-level keys untouched.
|
|
148
125
|
* @param isFreshFile - True when the file did not exist before this write
|
|
@@ -154,91 +131,68 @@ function mergeCursorHooks(current, isFreshFile, homeDir) {
|
|
|
154
131
|
if (isFreshFile && next.version === undefined) {
|
|
155
132
|
next.version = 1;
|
|
156
133
|
}
|
|
157
|
-
const hooks = next
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (idx >= 0) {
|
|
164
|
-
// Path 1: refresh existing Grounder hook entry in place
|
|
165
|
-
existing[idx] = entry;
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
// Path 2: append — first install, or file had other sessionStart hooks only
|
|
169
|
-
existing.push(entry);
|
|
170
|
-
}
|
|
171
|
-
next.hooks = { ...hooks, sessionStart: existing };
|
|
134
|
+
const hooks = readHooksObject(next, cursorHooksJsonPath(homeDir));
|
|
135
|
+
const raw = hooks[SESSION_START_EVENT];
|
|
136
|
+
const existing = Array.isArray(raw) ? raw : [];
|
|
137
|
+
const cleaned = removeMatchingEntries(existing, isCursorPeekEntry);
|
|
138
|
+
cleaned.push({ command: cursorPeekHookCommand(homeDir) });
|
|
139
|
+
next.hooks = { ...hooks, [SESSION_START_EVENT]: cleaned };
|
|
172
140
|
return next;
|
|
173
141
|
}
|
|
174
142
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* Force semantics:
|
|
180
|
-
* - Up-to-date canonical entry + fresh runtime and `force` false → skip
|
|
181
|
-
* - Otherwise → refresh runtime + merge host config
|
|
182
|
-
*
|
|
183
|
-
* Unparseable hooks.json: {@link mergeJsonFile} backs off and this throws (never clobbers).
|
|
143
|
+
* Remove every Grounder peek entry from `hooks.sessionStart`, touching
|
|
144
|
+
* nothing else. Returns `current` verbatim (no restructuring at all) when
|
|
145
|
+
* there's no Grounder entry to remove, so `mergeJsonFile` sees no change and
|
|
146
|
+
* leaves an unrelated `hooks.json` untouched instead of reformatting it.
|
|
184
147
|
*/
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
148
|
+
function removeCursorHooks(current) {
|
|
149
|
+
const hooks = current.hooks && typeof current.hooks === "object" && !Array.isArray(current.hooks)
|
|
150
|
+
? current.hooks
|
|
151
|
+
: undefined;
|
|
152
|
+
const raw = hooks?.[SESSION_START_EVENT];
|
|
153
|
+
const existing = Array.isArray(raw) ? raw : [];
|
|
154
|
+
const cleaned = removeMatchingEntries(existing, isCursorPeekEntry);
|
|
155
|
+
if (cleaned.length === existing.length) {
|
|
156
|
+
return current;
|
|
190
157
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
158
|
+
return {
|
|
159
|
+
...current,
|
|
160
|
+
hooks: { ...hooks, [SESSION_START_EVENT]: cleaned },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Install (or converge) Grounder's sessionStart teaser hook into `~/.cursor/hooks.json`.
|
|
165
|
+
* @see {@link installHookFragment} for the shared install/report scaffolding.
|
|
166
|
+
*/
|
|
167
|
+
async function installHooks(opts) {
|
|
168
|
+
return installHookFragment({
|
|
169
|
+
dest: cursorHooksJsonPath(opts.homeDir),
|
|
170
|
+
isUpToDate: (filePath) => peekHookUpToDate(filePath, opts.homeDir),
|
|
171
|
+
hasGrounderEntry: peekHookHadGrounderEntry,
|
|
172
|
+
merge: (current, fileExisted) => mergeCursorHooks(current, !fileExisted, opts.homeDir),
|
|
173
|
+
}, opts);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Remove Grounder's sessionStart hook entry entirely (`--no-hooks`) — the
|
|
177
|
+
* opt-out must also remove the fragment, not just flip `hooksEnabled` false,
|
|
178
|
+
* or the session hook keeps firing and the next plain `migrate` would find
|
|
179
|
+
* nothing to converge against (an absent entry looks like "never installed",
|
|
180
|
+
* which is exactly what makes the tri-state's `false` sticky in the first
|
|
181
|
+
* place).
|
|
182
|
+
*/
|
|
183
|
+
async function removeHooks(opts) {
|
|
184
|
+
return removeHookFragment(cursorHooksJsonPath(opts.homeDir), removeCursorHooks, opts);
|
|
206
185
|
}
|
|
207
186
|
export const cursor = {
|
|
208
187
|
id: "cursor",
|
|
209
188
|
name: "Cursor",
|
|
210
|
-
|
|
211
|
-
hooksSchema: 1,
|
|
212
|
-
async isInstalled() {
|
|
213
|
-
return fileExists(path.join(resolveHomeDir(), ".cursor"));
|
|
214
|
-
},
|
|
189
|
+
isInstalled: layout.isInstalled,
|
|
215
190
|
expectedArtifacts,
|
|
216
191
|
expectedHookArtifacts,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
for (const filename of COMMANDS) {
|
|
221
|
-
const { dest, status, hash } = await installCommandFile({
|
|
222
|
-
...opts,
|
|
223
|
-
agentId: cursor.id,
|
|
224
|
-
templateDir,
|
|
225
|
-
commandsDir: cursorCommandsDir(opts.homeDir),
|
|
226
|
-
filename,
|
|
227
|
-
});
|
|
228
|
-
artifacts[dest] = status;
|
|
229
|
-
if (hash) {
|
|
230
|
-
files[dest] = { hash };
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
await recordCommandFileHashes({
|
|
234
|
-
agentId: cursor.id,
|
|
235
|
-
commandsSchema: cursor.commandsSchema,
|
|
236
|
-
files,
|
|
237
|
-
homeDir: opts.homeDir,
|
|
238
|
-
dryRun: opts.dryRun,
|
|
239
|
-
});
|
|
240
|
-
return { artifacts };
|
|
241
|
-
},
|
|
192
|
+
desiredArtifacts: layout.desiredArtifacts,
|
|
193
|
+
tombstones: layout.tombstones,
|
|
194
|
+
ownedPrefixes: layout.ownedPrefixes,
|
|
242
195
|
installHooks,
|
|
196
|
+
removeHooks,
|
|
243
197
|
};
|
|
244
198
|
//# sourceMappingURL=cursor.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/agents/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/agents/cursor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole-file install layout Cursor and Claude Code share: Grounder's
|
|
3
|
+
* skills live at `~/<agentDir>/skills/<skill>/SKILL.md`, and the
|
|
4
|
+
* pre-Agent-Skills command files they replaced lived at
|
|
5
|
+
* `~/<agentDir>/commands/<skill>.md`. Give {@link homeSkillsLayout} the two
|
|
6
|
+
* names that differ between the two agents and it derives every path, template
|
|
7
|
+
* render, tombstone, and owned prefix an adapter needs for those artifacts.
|
|
8
|
+
*
|
|
9
|
+
* Hook config is deliberately not here. `~/.cursor/hooks.json` and
|
|
10
|
+
* `~/.claude/settings.json` differ in filename, in JSON shape (a flat array
|
|
11
|
+
* vs. matcher groups), and in what "already converged" even means, so each
|
|
12
|
+
* adapter keeps its own fragment reconciler; `hook-fragment.ts` and
|
|
13
|
+
* `hook-install.ts` hold the parts of *that* job which genuinely are shared.
|
|
14
|
+
*/
|
|
15
|
+
/** Skill directory names. Both adapters ship the same set. */
|
|
16
|
+
declare const SKILL_NAMES: readonly ["grounder-note", "grounder-search", "grounder-overview", "grounder-plan", "grounder-handoff", "grounder-recall"];
|
|
17
|
+
export type SkillName = (typeof SKILL_NAMES)[number];
|
|
18
|
+
export interface HomeSkillsLayout {
|
|
19
|
+
/** `~/<agentDir>/skills` — where today's `SKILL.md` files live. */
|
|
20
|
+
skillsDir(homeDir?: string): string;
|
|
21
|
+
/** `~/<agentDir>/commands` — where the retired pre-skill command files lived. */
|
|
22
|
+
legacyCommandsDir(homeDir?: string): string;
|
|
23
|
+
/** Absolute path to one skill's `SKILL.md`. */
|
|
24
|
+
skillPath(skill: SkillName, homeDir?: string): string;
|
|
25
|
+
/** @see AgentAdapter.expectedArtifacts */
|
|
26
|
+
expectedArtifacts(homeDir?: string): string[];
|
|
27
|
+
/** @see AgentAdapter.desiredArtifacts */
|
|
28
|
+
desiredArtifacts(homeDir?: string, options?: {
|
|
29
|
+
invocation?: string;
|
|
30
|
+
}): Promise<Record<string, string>>;
|
|
31
|
+
/** @see AgentAdapter.tombstones */
|
|
32
|
+
tombstones(homeDir?: string): string[];
|
|
33
|
+
/** @see AgentAdapter.ownedPrefixes */
|
|
34
|
+
ownedPrefixes(homeDir?: string): string[];
|
|
35
|
+
/** @see AgentAdapter.isInstalled */
|
|
36
|
+
isInstalled(): Promise<boolean>;
|
|
37
|
+
}
|
|
38
|
+
export interface HomeSkillsLayoutOptions {
|
|
39
|
+
/** Adapter id — names the template directory (`templates/agents/<id>/skills`). */
|
|
40
|
+
id: string;
|
|
41
|
+
/** This agent's config directory, relative to home (e.g. `.cursor`). */
|
|
42
|
+
agentDir: string;
|
|
43
|
+
}
|
|
44
|
+
export declare function homeSkillsLayout(options: HomeSkillsLayoutOptions): HomeSkillsLayout;
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=home-skills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"home-skills.d.ts","sourceRoot":"","sources":["../../src/agents/home-skills.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH,8DAA8D;AAC9D,QAAA,MAAM,WAAW,4HAOP,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAsBrD,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,iFAAiF;IACjF,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5C,+CAA+C;IAC/C,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtD,0CAA0C;IAC1C,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9C,yCAAyC;IACzC,gBAAgB,CACd,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAChC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnC,mCAAmC;IACnC,UAAU,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACvC,sCAAsC;IACtC,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1C,oCAAoC;IACpC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,uBAAuB;IACtC,kFAAkF;IAClF,EAAE,EAAE,MAAM,CAAC;IACX,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CA4CnF"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole-file install layout Cursor and Claude Code share: Grounder's
|
|
3
|
+
* skills live at `~/<agentDir>/skills/<skill>/SKILL.md`, and the
|
|
4
|
+
* pre-Agent-Skills command files they replaced lived at
|
|
5
|
+
* `~/<agentDir>/commands/<skill>.md`. Give {@link homeSkillsLayout} the two
|
|
6
|
+
* names that differ between the two agents and it derives every path, template
|
|
7
|
+
* render, tombstone, and owned prefix an adapter needs for those artifacts.
|
|
8
|
+
*
|
|
9
|
+
* Hook config is deliberately not here. `~/.cursor/hooks.json` and
|
|
10
|
+
* `~/.claude/settings.json` differ in filename, in JSON shape (a flat array
|
|
11
|
+
* vs. matcher groups), and in what "already converged" even means, so each
|
|
12
|
+
* adapter keeps its own fragment reconciler; `hook-fragment.ts` and
|
|
13
|
+
* `hook-install.ts` hold the parts of *that* job which genuinely are shared.
|
|
14
|
+
*/
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { resolveHomeDir } from "../connector/home.js";
|
|
19
|
+
import { fileExists } from "../util/fs.js";
|
|
20
|
+
import { runtimeInvocation } from "./hook-runtime.js";
|
|
21
|
+
/** Package root (`packages/grounder`) when running from `dist/agents/`. */
|
|
22
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
23
|
+
/** Skill directory names. Both adapters ship the same set. */
|
|
24
|
+
const SKILL_NAMES = [
|
|
25
|
+
"grounder-note",
|
|
26
|
+
"grounder-search",
|
|
27
|
+
"grounder-overview",
|
|
28
|
+
"grounder-plan",
|
|
29
|
+
"grounder-handoff",
|
|
30
|
+
"grounder-recall",
|
|
31
|
+
];
|
|
32
|
+
/** Relative `<skill>/SKILL.md` paths, resolved under whichever agent's skills dir. */
|
|
33
|
+
const SKILL_FILES = SKILL_NAMES.map((name) => path.join(name, "SKILL.md"));
|
|
34
|
+
/**
|
|
35
|
+
* Frozen historical fact about the schema-3 (pre-skill) install layout —
|
|
36
|
+
* deliberately spelled out rather than derived from {@link SKILL_NAMES}, which
|
|
37
|
+
* describes today's Agent Skills layout. Deriving it would rewrite history the
|
|
38
|
+
* moment a skill is added or renamed, retiring paths that never existed while
|
|
39
|
+
* forgetting one that does. Safe to delete once schema-3 installs are assumed
|
|
40
|
+
* extinct in the wild — a maintainer call, not something to automate via a
|
|
41
|
+
* version check.
|
|
42
|
+
*/
|
|
43
|
+
const LEGACY_COMMAND_FILENAMES = [
|
|
44
|
+
"grounder-note.md",
|
|
45
|
+
"grounder-search.md",
|
|
46
|
+
"grounder-plan.md",
|
|
47
|
+
"grounder-task-handoff.md",
|
|
48
|
+
"grounder-task.md",
|
|
49
|
+
];
|
|
50
|
+
export function homeSkillsLayout(options) {
|
|
51
|
+
const templateDir = path.join(pkgRoot, "templates", "agents", options.id, "skills");
|
|
52
|
+
const agentRoot = (homeDir) => path.join(resolveHomeDir(homeDir), options.agentDir);
|
|
53
|
+
const skillsDir = (homeDir) => path.join(agentRoot(homeDir), "skills");
|
|
54
|
+
const legacyCommandsDir = (homeDir) => path.join(agentRoot(homeDir), "commands");
|
|
55
|
+
return {
|
|
56
|
+
skillsDir,
|
|
57
|
+
legacyCommandsDir,
|
|
58
|
+
skillPath: (skill, homeDir) => path.join(skillsDir(homeDir), skill, "SKILL.md"),
|
|
59
|
+
expectedArtifacts: (homeDir) => SKILL_FILES.map((filename) => path.join(skillsDir(homeDir), filename)),
|
|
60
|
+
// Arrow function, not a shorthand method — every member of this object
|
|
61
|
+
// must be reachable with no bound `this` (see the file-level doc comment
|
|
62
|
+
// on why `cursor.ts`/`claude.ts` detach these off the closure), and a
|
|
63
|
+
// shorthand method is the one member shape that can silently start using
|
|
64
|
+
// `this` and type-check fine while breaking only at those detached call
|
|
65
|
+
// sites.
|
|
66
|
+
desiredArtifacts: async (homeDir, renderOptions) => {
|
|
67
|
+
const cli = renderOptions?.invocation ?? runtimeInvocation(homeDir);
|
|
68
|
+
const dir = skillsDir(homeDir);
|
|
69
|
+
const templates = await Promise.all(SKILL_FILES.map((filename) => readFile(path.join(templateDir, filename), "utf8")));
|
|
70
|
+
const desired = {};
|
|
71
|
+
SKILL_FILES.forEach((filename, i) => {
|
|
72
|
+
desired[path.join(dir, filename)] = templates[i].replaceAll("{{GROUNDER_CLI}}", cli);
|
|
73
|
+
});
|
|
74
|
+
return desired;
|
|
75
|
+
},
|
|
76
|
+
tombstones: (homeDir) => LEGACY_COMMAND_FILENAMES.map((filename) => path.join(legacyCommandsDir(homeDir), filename)),
|
|
77
|
+
ownedPrefixes: (homeDir) => [skillsDir(homeDir), legacyCommandsDir(homeDir)],
|
|
78
|
+
// No `homeDir` argument on purpose: detection runs before any per-call
|
|
79
|
+
// home override exists, off `GROUNDER_HOME` / `withHomeDir` alone.
|
|
80
|
+
isInstalled: async () => fileExists(agentRoot()),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=home-skills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"home-skills.js","sourceRoot":"","sources":["../../src/agents/home-skills.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic pieces of the session-hook fragment reconciler shared by
|
|
3
|
+
* `cursor.ts` (flat `hooks.sessionStart` array) and `claude.ts` (nested
|
|
4
|
+
* `hooks.SessionStart` matcher groups, each with its own `hooks` array).
|
|
5
|
+
*
|
|
6
|
+
* Model: Ansible `blockinfile` / Kubernetes Server-Side Apply's sole-owner
|
|
7
|
+
* case — locate every entry a recognizer predicate matches (there may be
|
|
8
|
+
* more than one: a legacy `npx grounder handoff peek` entry alongside a
|
|
9
|
+
* runtime-form one, say), remove all of them, and always converge to exactly
|
|
10
|
+
* one canonical entry. No conflict / `--force` gate — Grounder solely owns
|
|
11
|
+
* this one nested entry, there is nothing a user could have "locally edited"
|
|
12
|
+
* the way a whole skill file can be.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The entries registered for one hook event (`hooks.<event>`) in an
|
|
16
|
+
* already-parsed config root, or `null` when the file has no such array —
|
|
17
|
+
* a non-object root, no `hooks` key, a `hooks` that isn't an object, or an
|
|
18
|
+
* event key that is absent or not an array.
|
|
19
|
+
*
|
|
20
|
+
* `null` rather than `[]` because the two callers want opposite things from
|
|
21
|
+
* "nothing there": counting existing entries treats it as empty, while
|
|
22
|
+
* "already converged?" must answer `false` and go on to converge.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readEventEntries(parsed: unknown, event: string): unknown[] | null;
|
|
25
|
+
/**
|
|
26
|
+
* Grounder's fragment lives at `hooks.<event>`, so a merge has to read `hooks`
|
|
27
|
+
* as a JSON object. A present-but-non-object `hooks` (an array, a string, a
|
|
28
|
+
* number) is refused rather than replaced: these are shared config files whose
|
|
29
|
+
* unrelated keys Grounder promises to preserve, and silently dropping a value
|
|
30
|
+
* it can't interpret is the one way a merge could destroy user data. Refusing
|
|
31
|
+
* matches how {@link mergeJsonFile} already backs off an unparseable root or a
|
|
32
|
+
* non-object top level — same contract, one level deeper.
|
|
33
|
+
*
|
|
34
|
+
* `null` and absent both mean "nothing there yet" and yield a fresh `{}` —
|
|
35
|
+
* neither carries content a user could lose.
|
|
36
|
+
*
|
|
37
|
+
* Returns a shallow copy, so callers can mutate the result without touching
|
|
38
|
+
* the parsed original (`mergeJsonFile` compares by reference to decide whether
|
|
39
|
+
* anything changed).
|
|
40
|
+
*/
|
|
41
|
+
export declare function readHooksObject(current: Record<string, unknown>, filePath: string): Record<string, unknown>;
|
|
42
|
+
/** Remove every entry `isMatch` accepts, preserving order of the rest. */
|
|
43
|
+
export declare function removeMatchingEntries<T>(entries: readonly T[], isMatch: (entry: T) => boolean): T[];
|
|
44
|
+
/**
|
|
45
|
+
* True when `entries` already holds exactly one match and it is byte-for-byte
|
|
46
|
+
* the canonical entry (same keys, same order) — the only case a fragment
|
|
47
|
+
* reconciler should report as already-converged (no write needed). Two
|
|
48
|
+
* matches, zero matches, or a single match with drifted content all count as
|
|
49
|
+
* "not yet converged."
|
|
50
|
+
*/
|
|
51
|
+
export declare function isAlreadyConverged<T>(entries: readonly T[], isMatch: (entry: T) => boolean, canonical: T): boolean;
|
|
52
|
+
//# sourceMappingURL=hook-fragment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-fragment.d.ts","sourceRoot":"","sources":["../../src/agents/hook-fragment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE,GAAG,IAAI,CAUjF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED,0EAA0E;AAC1E,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,GAC7B,CAAC,EAAE,CAEL;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,EAC9B,SAAS,EAAE,CAAC,GACX,OAAO,CAGT"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic pieces of the session-hook fragment reconciler shared by
|
|
3
|
+
* `cursor.ts` (flat `hooks.sessionStart` array) and `claude.ts` (nested
|
|
4
|
+
* `hooks.SessionStart` matcher groups, each with its own `hooks` array).
|
|
5
|
+
*
|
|
6
|
+
* Model: Ansible `blockinfile` / Kubernetes Server-Side Apply's sole-owner
|
|
7
|
+
* case — locate every entry a recognizer predicate matches (there may be
|
|
8
|
+
* more than one: a legacy `npx grounder handoff peek` entry alongside a
|
|
9
|
+
* runtime-form one, say), remove all of them, and always converge to exactly
|
|
10
|
+
* one canonical entry. No conflict / `--force` gate — Grounder solely owns
|
|
11
|
+
* this one nested entry, there is nothing a user could have "locally edited"
|
|
12
|
+
* the way a whole skill file can be.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The entries registered for one hook event (`hooks.<event>`) in an
|
|
16
|
+
* already-parsed config root, or `null` when the file has no such array —
|
|
17
|
+
* a non-object root, no `hooks` key, a `hooks` that isn't an object, or an
|
|
18
|
+
* event key that is absent or not an array.
|
|
19
|
+
*
|
|
20
|
+
* `null` rather than `[]` because the two callers want opposite things from
|
|
21
|
+
* "nothing there": counting existing entries treats it as empty, while
|
|
22
|
+
* "already converged?" must answer `false` and go on to converge.
|
|
23
|
+
*/
|
|
24
|
+
export function readEventEntries(parsed, event) {
|
|
25
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const hooks = parsed.hooks;
|
|
29
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const entries = hooks[event];
|
|
33
|
+
return Array.isArray(entries) ? entries : null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Grounder's fragment lives at `hooks.<event>`, so a merge has to read `hooks`
|
|
37
|
+
* as a JSON object. A present-but-non-object `hooks` (an array, a string, a
|
|
38
|
+
* number) is refused rather than replaced: these are shared config files whose
|
|
39
|
+
* unrelated keys Grounder promises to preserve, and silently dropping a value
|
|
40
|
+
* it can't interpret is the one way a merge could destroy user data. Refusing
|
|
41
|
+
* matches how {@link mergeJsonFile} already backs off an unparseable root or a
|
|
42
|
+
* non-object top level — same contract, one level deeper.
|
|
43
|
+
*
|
|
44
|
+
* `null` and absent both mean "nothing there yet" and yield a fresh `{}` —
|
|
45
|
+
* neither carries content a user could lose.
|
|
46
|
+
*
|
|
47
|
+
* Returns a shallow copy, so callers can mutate the result without touching
|
|
48
|
+
* the parsed original (`mergeJsonFile` compares by reference to decide whether
|
|
49
|
+
* anything changed).
|
|
50
|
+
*/
|
|
51
|
+
export function readHooksObject(current, filePath) {
|
|
52
|
+
const hooks = current.hooks;
|
|
53
|
+
if (hooks === undefined || hooks === null) {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
if (typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
57
|
+
throw new Error(`Refusing to modify ${filePath}: "hooks" must be a JSON object`);
|
|
58
|
+
}
|
|
59
|
+
return { ...hooks };
|
|
60
|
+
}
|
|
61
|
+
/** Remove every entry `isMatch` accepts, preserving order of the rest. */
|
|
62
|
+
export function removeMatchingEntries(entries, isMatch) {
|
|
63
|
+
return entries.filter((entry) => !isMatch(entry));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* True when `entries` already holds exactly one match and it is byte-for-byte
|
|
67
|
+
* the canonical entry (same keys, same order) — the only case a fragment
|
|
68
|
+
* reconciler should report as already-converged (no write needed). Two
|
|
69
|
+
* matches, zero matches, or a single match with drifted content all count as
|
|
70
|
+
* "not yet converged."
|
|
71
|
+
*/
|
|
72
|
+
export function isAlreadyConverged(entries, isMatch, canonical) {
|
|
73
|
+
const matches = entries.filter(isMatch);
|
|
74
|
+
return matches.length === 1 && JSON.stringify(matches[0]) === JSON.stringify(canonical);
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=hook-fragment.js.map
|