@groundtrack/hooks 0.1.7 → 0.1.8
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 +104 -43
- package/dist/agent-instructions.d.ts +2 -1
- package/dist/agents.d.ts +36 -0
- package/dist/agents.js +27 -0
- package/dist/doctor.d.ts +6 -1
- package/dist/doctor.js +9 -6
- package/dist/enrich-store-event.js +2 -1
- package/dist/groundtrack-hook.js +0 -0
- package/dist/groundtrack.js +6 -195
- package/dist/setup/agents.d.ts +59 -0
- package/dist/setup/agents.js +160 -0
- package/dist/setup/cli.d.ts +16 -0
- package/dist/setup/cli.js +331 -0
- package/dist/setup/config.d.ts +9 -0
- package/dist/setup/config.js +143 -0
- package/dist/setup/connections.d.ts +26 -0
- package/dist/setup/connections.js +210 -0
- package/dist/setup/files.d.ts +18 -0
- package/dist/setup/files.js +104 -0
- package/dist/setup/plan.d.ts +30 -0
- package/dist/setup/plan.js +324 -0
- package/package.json +15 -2
- package/src/agents.ts +37 -0
- package/templates/AGENTS.md +2 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { AGENTS } from "../agents.js";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { AGENT_INSTRUCTION_BODY } from "../agent-instructions.js";
|
|
4
|
+
import { CLAUDE_INSTRUCTION_BODY, claudeInstructionsAreCurrent, claudeUsesInlineGroundtrackInstructions, upsertManagedContent, } from "../doctor.js";
|
|
5
|
+
import { configPaths, isGroundtrack, readConfig, serverMap, serverPath, stateDir, } from "./agents.js";
|
|
6
|
+
import { at, object, readJson, readToml, setJson, setToml } from "./config.js";
|
|
7
|
+
import { FilePlan } from "./files.js";
|
|
8
|
+
function record(value) {
|
|
9
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
10
|
+
? value
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
function array(value, label) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return [];
|
|
16
|
+
if (!Array.isArray(value))
|
|
17
|
+
throw new Error(`${label} must be an array. Fix its configuration and retry.`);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function ownedHook(value) {
|
|
21
|
+
const command = record(value)?.command;
|
|
22
|
+
return typeof command === "string" && /(?:^|[/\\\s])groundtrack-hook(?:\s|$)/.test(command);
|
|
23
|
+
}
|
|
24
|
+
function matcherMatches(value, aliases) {
|
|
25
|
+
if (typeof value !== "string")
|
|
26
|
+
return false;
|
|
27
|
+
try {
|
|
28
|
+
return aliases.every((alias) => new RegExp(value).test(`mcp__${alias}__store_event`) &&
|
|
29
|
+
new RegExp(value).test(`mcp__${alias}__search_experiences`));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function matcher(aliases) {
|
|
36
|
+
return `^mcp__(?:${aliases.map((alias) => alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})__(store_event|search_experiences)$`;
|
|
37
|
+
}
|
|
38
|
+
function commandHook(command) {
|
|
39
|
+
return {
|
|
40
|
+
type: "command",
|
|
41
|
+
command,
|
|
42
|
+
timeout: 5,
|
|
43
|
+
statusMessage: "Adding Groundtrack session metadata",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function hasHook(config, aliases, command) {
|
|
47
|
+
return array(at(config, ["hooks", "PreToolUse"]), "PreToolUse hooks").some((entry) => {
|
|
48
|
+
const group = record(entry);
|
|
49
|
+
return (matcherMatches(group?.matcher, aliases) &&
|
|
50
|
+
array(group?.hooks, "Hook commands").some((hook) => record(hook)?.command === command && record(hook)?.type === "command"));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Remove only our commands from shared groups, leaving other commands and their matchers intact. */
|
|
54
|
+
function upsertCommandHook(text, aliases, command) {
|
|
55
|
+
const config = readJson(text);
|
|
56
|
+
if (hasHook(config, aliases, command))
|
|
57
|
+
return text;
|
|
58
|
+
const groups = array(at(config, ["hooks", "PreToolUse"]), "PreToolUse hooks");
|
|
59
|
+
let updated = text;
|
|
60
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
61
|
+
const hooks = array(record(groups[i])?.hooks, "Hook commands");
|
|
62
|
+
if (!hooks.some(ownedHook))
|
|
63
|
+
continue;
|
|
64
|
+
if (hooks.every(ownedHook))
|
|
65
|
+
updated = setJson(updated, ["hooks", "PreToolUse", i], undefined);
|
|
66
|
+
else
|
|
67
|
+
for (let j = hooks.length - 1; j >= 0; j--)
|
|
68
|
+
if (ownedHook(hooks[j]))
|
|
69
|
+
updated = setJson(updated, ["hooks", "PreToolUse", i, "hooks", j], undefined);
|
|
70
|
+
}
|
|
71
|
+
if (at(readJson(updated), ["hooks", "PreToolUse"]) === undefined)
|
|
72
|
+
updated = setJson(updated, ["hooks", "PreToolUse"], []);
|
|
73
|
+
return setJson(updated, ["hooks", "PreToolUse", -1], {
|
|
74
|
+
matcher: matcher(aliases),
|
|
75
|
+
hooks: [commandHook(command)],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function jsonFile(files, path) {
|
|
79
|
+
return readJson(files.read(path) ?? "{}");
|
|
80
|
+
}
|
|
81
|
+
function managedInstructions(source, desired, name) {
|
|
82
|
+
const starts = source?.match(/<!-- groundtrack:start\b/g)?.length ?? 0;
|
|
83
|
+
const ends = source?.match(/<!-- groundtrack:end -->/g)?.length ?? 0;
|
|
84
|
+
if (starts !== ends || starts > 1)
|
|
85
|
+
throw new Error(`${name} has incomplete or duplicate Groundtrack markers. Resolve them before setup.`);
|
|
86
|
+
return upsertManagedContent(source, desired, true);
|
|
87
|
+
}
|
|
88
|
+
function planClaude(files, context, aliases) {
|
|
89
|
+
const userPath = join(context.env.CLAUDE_CONFIG_DIR || join(context.homeDir, ".claude"), "settings.json");
|
|
90
|
+
const user = jsonFile(files, userPath);
|
|
91
|
+
const projectPath = join(context.repoDir, ".claude/settings.json");
|
|
92
|
+
const project = jsonFile(files, projectPath);
|
|
93
|
+
const local = jsonFile(files, join(context.repoDir, ".claude/settings.local.json"));
|
|
94
|
+
const connectionSettings = jsonFile(files, configPaths("claude", context).user);
|
|
95
|
+
const localConnection = record(at(connectionSettings, ["projects", context.repoDir])) ?? {};
|
|
96
|
+
if ([user, project, local, connectionSettings, localConnection].some((config) => {
|
|
97
|
+
const disabled = config.disabledMcpServers;
|
|
98
|
+
return Array.isArray(disabled) && aliases.some((alias) => disabled.includes(alias));
|
|
99
|
+
})) {
|
|
100
|
+
throw new Error("Groundtrack is disabled in Claude Code. Re-enable the server in /mcp, then run setup again.");
|
|
101
|
+
}
|
|
102
|
+
if ([user, project, local].some((config) => config.disableAllHooks === true || config.allowManagedHooksOnly === true)) {
|
|
103
|
+
throw new Error("Claude Code's settings disable project hooks. Enable them in Claude Code, then run setup again.");
|
|
104
|
+
}
|
|
105
|
+
if (!hasHook(user, aliases, "groundtrack-hook claude") &&
|
|
106
|
+
!hasHook(local, aliases, "groundtrack-hook claude")) {
|
|
107
|
+
files.set(projectPath, upsertCommandHook(files.read(projectPath) ?? "", aliases, "groundtrack-hook claude"));
|
|
108
|
+
}
|
|
109
|
+
const bridgePath = join(context.repoDir, "CLAUDE.md");
|
|
110
|
+
const bridge = files.read(bridgePath);
|
|
111
|
+
if (!claudeInstructionsAreCurrent(bridge)) {
|
|
112
|
+
files.set(bridgePath, managedInstructions(bridge, claudeUsesInlineGroundtrackInstructions(bridge)
|
|
113
|
+
? AGENT_INSTRUCTION_BODY
|
|
114
|
+
: CLAUDE_INSTRUCTION_BODY, "CLAUDE.md"));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function planCodex(files, context, aliases) {
|
|
118
|
+
const overridePath = join(context.repoDir, "AGENTS.override.md");
|
|
119
|
+
const override = files.read(overridePath);
|
|
120
|
+
if (override !== null)
|
|
121
|
+
files.set(overridePath, managedInstructions(override, AGENT_INSTRUCTION_BODY, "AGENTS.override.md"));
|
|
122
|
+
const paths = configPaths("codex", context);
|
|
123
|
+
const project = readToml(files.read(paths.project) ?? "");
|
|
124
|
+
const user = readToml(files.read(paths.user) ?? "");
|
|
125
|
+
const hookEnabled = at(project, ["features", "hooks"]) ??
|
|
126
|
+
at(project, ["features", "codex_hooks"]) ??
|
|
127
|
+
at(user, ["features", "hooks"]) ??
|
|
128
|
+
at(user, ["features", "codex_hooks"]);
|
|
129
|
+
if (hookEnabled === false || user.allow_managed_hooks_only === true)
|
|
130
|
+
throw new Error("Codex's settings disable project hooks. Enable them in Codex, then run setup again.");
|
|
131
|
+
const userHooksPath = join(context.env.CODEX_HOME || join(context.homeDir, ".codex"), "hooks.json");
|
|
132
|
+
const hookPath = join(context.repoDir, ".codex/hooks.json");
|
|
133
|
+
if ([project, user, jsonFile(files, userHooksPath), jsonFile(files, hookPath)].some((config) => hasHook(config, aliases, "groundtrack-hook")))
|
|
134
|
+
return;
|
|
135
|
+
// Repair legacy inline Groundtrack hooks in place without serializing the TOML document.
|
|
136
|
+
const groups = array(at(project, ["hooks", "PreToolUse"]), "Codex PreToolUse hooks");
|
|
137
|
+
for (let index = 0; index < groups.length; index++) {
|
|
138
|
+
const hooks = array(record(groups[index])?.hooks, "Codex hook commands");
|
|
139
|
+
if (!hooks.some(ownedHook))
|
|
140
|
+
continue;
|
|
141
|
+
if (!hooks.every(ownedHook))
|
|
142
|
+
throw new Error("A Codex hook group mixes Groundtrack and other commands. Separate that group before repairing it.");
|
|
143
|
+
let updated = setToml(files.read(paths.project) ?? "", ["hooks", "PreToolUse", index, "matcher"], matcher(aliases));
|
|
144
|
+
for (let j = 0; j < hooks.length; j++)
|
|
145
|
+
for (const [key, value] of Object.entries(commandHook("groundtrack-hook"))) {
|
|
146
|
+
updated = setToml(updated, ["hooks", "PreToolUse", index, "hooks", j, key], value);
|
|
147
|
+
}
|
|
148
|
+
files.set(paths.project, updated);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
files.set(hookPath, upsertCommandHook(files.read(hookPath) ?? "", aliases, "groundtrack-hook"));
|
|
152
|
+
}
|
|
153
|
+
function planCursor(files, context) {
|
|
154
|
+
const path = join(context.repoDir, ".cursor/hooks.json");
|
|
155
|
+
let text = files.read(path) ?? "";
|
|
156
|
+
const config = readJson(text);
|
|
157
|
+
const hooks = array(at(config, ["hooks", "preToolUse"]), "Cursor preToolUse hooks");
|
|
158
|
+
const desired = {
|
|
159
|
+
command: "groundtrack-hook",
|
|
160
|
+
matcher: "MCP:.*(store_event|search_experiences)|MCP:(store_event|search_experiences)",
|
|
161
|
+
};
|
|
162
|
+
if (hooks.some((hook) => record(hook)?.command === desired.command && record(hook)?.matcher === desired.matcher))
|
|
163
|
+
return;
|
|
164
|
+
if (config.version !== undefined && config.version !== 1)
|
|
165
|
+
throw new Error("Unsupported Cursor hooks version. Update Groundtrack before changing this file.");
|
|
166
|
+
if (config.version === undefined)
|
|
167
|
+
text = setJson(text, ["version"], 1);
|
|
168
|
+
for (let i = hooks.length - 1; i >= 0; i--)
|
|
169
|
+
if (ownedHook(hooks[i]))
|
|
170
|
+
text = setJson(text, ["hooks", "preToolUse", i], undefined);
|
|
171
|
+
if (at(readJson(text), ["hooks", "preToolUse"]) === undefined)
|
|
172
|
+
text = setJson(text, ["hooks", "preToolUse"], []);
|
|
173
|
+
files.set(path, setJson(text, ["hooks", "preToolUse", -1], desired));
|
|
174
|
+
}
|
|
175
|
+
function planOpenCode(files, context) {
|
|
176
|
+
const path = configPaths("opencode", context).project;
|
|
177
|
+
const text = files.read(path) ?? "";
|
|
178
|
+
const config = readJson(text);
|
|
179
|
+
const plugins = array(config.plugin, "OpenCode plugins");
|
|
180
|
+
if (plugins.some((plugin) => typeof plugin === "string" && /^@groundtrack\/opencode-plugin(?:@|$)/.test(plugin)))
|
|
181
|
+
return;
|
|
182
|
+
files.set(path, setJson(text, ["plugin"], [...plugins, "@groundtrack/opencode-plugin"]));
|
|
183
|
+
}
|
|
184
|
+
export function activationSteps(agent, alias, existing, authenticated = false) {
|
|
185
|
+
const verify = "In a new session, ask: Verify Groundtrack's connection and hook context with search_experiences.";
|
|
186
|
+
const signIn = existing ? "If asked to sign in, " : "Sign in: ";
|
|
187
|
+
switch (agent) {
|
|
188
|
+
case "claude":
|
|
189
|
+
return [
|
|
190
|
+
...(!authenticated
|
|
191
|
+
? [
|
|
192
|
+
"Open this project in Claude Code and approve Groundtrack from .mcp.json if prompted.",
|
|
193
|
+
`${signIn}use /mcp in Claude Code, or run claude mcp login ${alias} after approval.`,
|
|
194
|
+
]
|
|
195
|
+
: []),
|
|
196
|
+
verify,
|
|
197
|
+
];
|
|
198
|
+
case "codex":
|
|
199
|
+
return [
|
|
200
|
+
"Open and trust this project in Codex so it can load the project's MCP configuration.",
|
|
201
|
+
...(!authenticated
|
|
202
|
+
? [`${signIn}run codex mcp login ${alias}, or connect in Codex Settings → MCP servers.`]
|
|
203
|
+
: []),
|
|
204
|
+
"Review and trust the Groundtrack hook in /hooks, or accept Codex's hook approval prompt.",
|
|
205
|
+
verify,
|
|
206
|
+
];
|
|
207
|
+
case "cursor":
|
|
208
|
+
return ["Open Cursor Settings → Tools & MCP → Groundtrack and connect if prompted.", verify];
|
|
209
|
+
case "opencode":
|
|
210
|
+
return [
|
|
211
|
+
...(!authenticated ? [`${signIn}run opencode mcp auth ${alias}.`] : []),
|
|
212
|
+
"Restart OpenCode to load the plugin.",
|
|
213
|
+
verify,
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function buildAgentPlan(agent, context, url) {
|
|
218
|
+
const name = AGENTS.find((item) => item.id === agent).name;
|
|
219
|
+
const files = new FilePlan();
|
|
220
|
+
let alias = "groundtrack";
|
|
221
|
+
let existingConnection = false;
|
|
222
|
+
try {
|
|
223
|
+
const paths = configPaths(agent, context);
|
|
224
|
+
const project = readConfig(agent, files.read(paths.project) ?? "");
|
|
225
|
+
const user = readConfig(agent, files.read(paths.user) ?? "");
|
|
226
|
+
const maps = [serverMap(agent, project), serverMap(agent, user)];
|
|
227
|
+
if (agent === "claude")
|
|
228
|
+
maps.unshift(object(at(user, ["projects", context.repoDir, "mcpServers"]) ?? {}, "Claude local MCP servers"));
|
|
229
|
+
const candidates = maps.flatMap((map) => Object.entries(map).filter(([key, value]) => isGroundtrack(key, value, url)));
|
|
230
|
+
for (const [key, value] of candidates) {
|
|
231
|
+
const server = record(value);
|
|
232
|
+
if (server?.url !== url || server.command !== undefined)
|
|
233
|
+
throw new Error(`Existing ${name} server "${key}" has a different endpoint or transport. Use the matching --url or resolve it in the agent's settings.`);
|
|
234
|
+
if (server.enabled === false || server.disabled === true)
|
|
235
|
+
throw new Error(`Groundtrack is disabled in ${name}. Enable it in the agent's settings, then retry.`);
|
|
236
|
+
// The current runtime identifies Groundtrack tools by their server name.
|
|
237
|
+
if (!key.toLowerCase().includes("groundtrack") || !/^[a-zA-Z0-9_.-]+$/.test(key))
|
|
238
|
+
throw new Error(`Server "${key}" needs a Groundtrack-compatible alias containing "groundtrack" (letters, numbers, dots, hyphens, underscores). Rename it in ${name} before setup.`);
|
|
239
|
+
}
|
|
240
|
+
if (candidates.length) {
|
|
241
|
+
alias = candidates[0][0];
|
|
242
|
+
existingConnection = true;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const config = agent === "opencode"
|
|
246
|
+
? { type: "remote", url, enabled: true }
|
|
247
|
+
: agent === "claude"
|
|
248
|
+
? { type: "http", url }
|
|
249
|
+
: { url };
|
|
250
|
+
const path = [...serverPath(agent, project), alias];
|
|
251
|
+
files.set(paths.project, agent === "codex"
|
|
252
|
+
? setToml(files.read(paths.project) ?? "", path, config)
|
|
253
|
+
: setJson(files.read(paths.project) ?? "", path, config));
|
|
254
|
+
}
|
|
255
|
+
const aliases = candidates.length ? [...new Set(candidates.map(([key]) => key))] : [alias];
|
|
256
|
+
if (agent === "claude")
|
|
257
|
+
planClaude(files, context, aliases);
|
|
258
|
+
if (agent === "codex")
|
|
259
|
+
planCodex(files, context, aliases);
|
|
260
|
+
if (agent === "cursor")
|
|
261
|
+
planCursor(files, context);
|
|
262
|
+
if (agent === "opencode")
|
|
263
|
+
planOpenCode(files, context);
|
|
264
|
+
return {
|
|
265
|
+
agent,
|
|
266
|
+
name,
|
|
267
|
+
alias,
|
|
268
|
+
existingConnection,
|
|
269
|
+
changes: files.changes,
|
|
270
|
+
issue: null,
|
|
271
|
+
activation: "unverified",
|
|
272
|
+
nextSteps: activationSteps(agent, alias, existingConnection),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
return {
|
|
277
|
+
agent,
|
|
278
|
+
name,
|
|
279
|
+
alias,
|
|
280
|
+
existingConnection,
|
|
281
|
+
changes: [],
|
|
282
|
+
issue: error instanceof Error ? error.message : "Configuration could not be read.",
|
|
283
|
+
activation: "unverified",
|
|
284
|
+
nextSteps: [],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
export function buildSetupPlan(context, agents, url) {
|
|
289
|
+
const files = new FilePlan();
|
|
290
|
+
let sharedIssue = null;
|
|
291
|
+
try {
|
|
292
|
+
const path = join(context.repoDir, "AGENTS.md");
|
|
293
|
+
const source = files.read(path);
|
|
294
|
+
files.set(path, managedInstructions(source, AGENT_INSTRUCTION_BODY, "AGENTS.md"));
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
sharedIssue = error instanceof Error ? error.message : "Cannot update AGENTS.md.";
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
agents: [...new Set(agents)].map((agent) => buildAgentPlan(agent, context, url)),
|
|
301
|
+
shared: files.changes,
|
|
302
|
+
sharedIssue,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
export function planSummary(plan, context) {
|
|
306
|
+
return plan.agents.map((agent) => ({
|
|
307
|
+
agent: agent.agent,
|
|
308
|
+
name: agent.name,
|
|
309
|
+
status: plan.sharedIssue || agent.issue
|
|
310
|
+
? "blocked"
|
|
311
|
+
: agent.changes.length || plan.shared.length
|
|
312
|
+
? "needs_setup"
|
|
313
|
+
: "configured",
|
|
314
|
+
issue: plan.sharedIssue ?? agent.issue,
|
|
315
|
+
activation: agent.activation,
|
|
316
|
+
files: agent.changes.map((change) => relative(context.repoDir, change.path)),
|
|
317
|
+
nextSteps: agent.nextSteps,
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
export function selectionChange(context, agents) {
|
|
321
|
+
const files = new FilePlan();
|
|
322
|
+
files.set(join(stateDir(context), "setup.json"), `${JSON.stringify({ version: 1, agents }, null, 2)}\n`);
|
|
323
|
+
return files.changes.map((change) => ({ ...change, mode: 0o600 }));
|
|
324
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groundtrack/hooks",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Lifecycle hook utilities for Groundtrack MCP clients.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"dist/",
|
|
21
21
|
"templates/",
|
|
22
|
+
"src/agents.ts",
|
|
22
23
|
"README.md",
|
|
23
24
|
"LICENSE"
|
|
24
25
|
],
|
|
@@ -33,7 +34,14 @@
|
|
|
33
34
|
"types": "./dist/agent-instructions.d.ts",
|
|
34
35
|
"default": "./dist/agent-instructions.js"
|
|
35
36
|
},
|
|
36
|
-
"./package.json": "./package.json"
|
|
37
|
+
"./package.json": "./package.json",
|
|
38
|
+
"./agents": {
|
|
39
|
+
"types": "./src/agents.ts",
|
|
40
|
+
"browser": "./src/agents.ts",
|
|
41
|
+
"workerd": "./src/agents.ts",
|
|
42
|
+
"development": "./src/agents.ts",
|
|
43
|
+
"default": "./dist/agents.js"
|
|
44
|
+
}
|
|
37
45
|
},
|
|
38
46
|
"publishConfig": {
|
|
39
47
|
"access": "public"
|
|
@@ -47,6 +55,11 @@
|
|
|
47
55
|
"test": "pnpm run build && node --import tsx --test test/*.test.ts",
|
|
48
56
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json"
|
|
49
57
|
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@clack/prompts": "0.11.0",
|
|
60
|
+
"jsonc-parser": "3.3.1",
|
|
61
|
+
"toml-eslint-parser": "0.10.0"
|
|
62
|
+
},
|
|
50
63
|
"devDependencies": {
|
|
51
64
|
"@groundtrack/typescript": "workspace:*",
|
|
52
65
|
"@types/node": "^25.9.1",
|
package/src/agents.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Shared by the CLI, browser UI, hooks, and server. Keep this module dependency-free. */
|
|
2
|
+
export const AGENTS = [
|
|
3
|
+
{
|
|
4
|
+
id: "claude",
|
|
5
|
+
name: "Claude Code",
|
|
6
|
+
aliases: ["claude-code"],
|
|
7
|
+
source: "claude_code",
|
|
8
|
+
enrichment: "command",
|
|
9
|
+
},
|
|
10
|
+
{ id: "codex", name: "Codex", aliases: [], source: "codex", enrichment: "command" },
|
|
11
|
+
{ id: "cursor", name: "Cursor", aliases: [], source: "cursor", enrichment: "command" },
|
|
12
|
+
{ id: "opencode", name: "OpenCode", aliases: [], source: "opencode", enrichment: "plugin" },
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export type AgentDefinition = (typeof AGENTS)[number];
|
|
16
|
+
export type Agent = AgentDefinition["id"];
|
|
17
|
+
export type CommandHookAgent = Extract<AgentDefinition, { enrichment: "command" }>["id"];
|
|
18
|
+
export const AGENT_IDS = AGENTS.map((agent) => agent.id);
|
|
19
|
+
|
|
20
|
+
export function isAgent(value: unknown): value is Agent {
|
|
21
|
+
return AGENTS.some((agent) => agent.id === value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveAgent(value: string): AgentDefinition | undefined {
|
|
25
|
+
return AGENTS.find(
|
|
26
|
+
(agent) => agent.id === value || agent.aliases.some((alias) => alias === value),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function agentFromClientName(value: string | null | undefined): AgentDefinition | undefined {
|
|
31
|
+
const normalized = value?.trim().toLowerCase() ?? "";
|
|
32
|
+
return AGENTS.find((agent) => normalized.includes(agent.id));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function requiresHookRuntime(id: Agent): boolean {
|
|
36
|
+
return AGENTS.some((agent) => agent.id === id && agent.enrichment === "command");
|
|
37
|
+
}
|
package/templates/AGENTS.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
<!-- groundtrack:start version=
|
|
1
|
+
<!-- groundtrack:start version=4 -->
|
|
2
2
|
|
|
3
3
|
## Groundtrack
|
|
4
4
|
|
|
5
5
|
Use Groundtrack as the shared engineering memory for this repository.
|
|
6
6
|
|
|
7
|
+
- At session start, check whether Groundtrack tools are available, using tool discovery if the client supports it. If `search_experiences` is unavailable, or a Groundtrack call reports connection, sign-in, or hook-setup problems, tell the user promptly, once per session unless the problem changes. Give the setup command for the current agent from this repository's root, for example `npx --yes --package @groundtrack/hooks@latest groundtrack setup --agent codex` when running in Codex. If the current agent is unclear, omit `--agent` to use the picker. Continue the user's task; when Groundtrack cannot be used, say so rather than claiming a successful lookup or inventing retrieved experiences.
|
|
7
8
|
- Before non-trivial investigation or implementation where prior decisions, failures, or constraints could matter, call `search_experiences`. Start with a focused description of the work, then inspect the results and search one or two materially different adjacent concepts when related components, constraints, failure modes, or prior decisions could change the approach. Do not stop merely because the first search has no exact match; try a meaningfully broader or neighboring query. Keep this expansion bounded, and also search before repeating an approach that may already have been tried.
|
|
8
9
|
- When a relevant experience needs more context or verification, call `get_experience_events` with its id to inspect the underlying raw evidence and provenance.
|
|
9
10
|
- Apply relevant current experiences and heed any caveats that an experience was contradicted, superseded, or expired.
|