@groundtrack/hooks 0.1.6 → 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 +8 -0
- package/dist/agents.d.ts +36 -0
- package/dist/agents.js +27 -0
- package/dist/doctor.d.ts +34 -0
- package/dist/doctor.js +9 -6
- package/dist/enrich-store-event.d.ts +18 -0
- package/dist/enrich-store-event.js +3 -2
- package/dist/groundtrack-hook.d.ts +2 -0
- package/dist/groundtrack.d.ts +2 -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/dist/version.d.ts +1 -0
- package/package.json +29 -3
- package/src/agents.ts +37 -0
- package/templates/AGENTS.md +2 -1
package/README.md
CHANGED
|
@@ -1,56 +1,117 @@
|
|
|
1
1
|
# @groundtrack/hooks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Set up Groundtrack across Claude Code, Codex, Cursor, and OpenCode in one flow.
|
|
4
|
+
Node.js 18 or newer is required.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
```sh
|
|
7
|
+
npx --yes --package @groundtrack/hooks@latest groundtrack setup
|
|
8
|
+
```
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
Choose the agents you use, review the changes, and finish any sign-in or hook
|
|
11
|
+
approval shown for each agent. The picker detects both machine installations
|
|
12
|
+
and project configuration. Your choices are remembered; adding Codex later
|
|
13
|
+
preserves your working Claude and Cursor connections.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
groundtrack setup --agent codex
|
|
17
|
+
groundtrack setup --agent claude --agent codex --agent cursor --yes
|
|
18
|
+
groundtrack setup --all-detected --yes
|
|
19
|
+
groundtrack setup --agent codex --dry-run
|
|
20
|
+
groundtrack status
|
|
21
|
+
groundtrack status --json
|
|
10
22
|
```
|
|
11
23
|
|
|
12
|
-
|
|
24
|
+
`--agent` is repeatable. `--client` and `claude-code` are accepted aliases.
|
|
25
|
+
Setup configures the current project; it does not activate Groundtrack in every
|
|
26
|
+
repository. Use `--repo PATH` to choose a project and `--url URL` for another
|
|
27
|
+
Groundtrack endpoint. With no explicit targets, an interactive terminal shows
|
|
28
|
+
the picker. An unattended `--yes` run needs a previously saved selection or
|
|
29
|
+
explicit `--agent` / `--all-detected`; it never falls back to all known agents.
|
|
30
|
+
An empty selection or cancellation makes no changes.
|
|
31
|
+
|
|
32
|
+
The same package provides `groundtrack-hook`. Setup installs this shared runtime
|
|
33
|
+
once if needed for Claude, Codex, or Cursor. OpenCode uses
|
|
34
|
+
`@groundtrack/opencode-plugin` instead. Repeat runs use the installed runtime
|
|
35
|
+
without querying npm for updates on every invocation.
|
|
36
|
+
|
|
37
|
+
The CLI validates JSON/JSONC and TOML, preserves unrelated settings and comments,
|
|
38
|
+
reuses existing Groundtrack aliases, and backs up changed files before applying
|
|
39
|
+
atomic writes. Backups and selections live under
|
|
40
|
+
`$XDG_CONFIG_HOME/groundtrack` (default `~/.config/groundtrack`). Backups may
|
|
41
|
+
contain existing private configuration, so their directory and files are private.
|
|
42
|
+
A failure rolls back completed writes where possible and reports the backup
|
|
43
|
+
location. Malformed configuration, symbolic links, disabled connections/hooks,
|
|
44
|
+
and endpoint conflicts are reported for the affected agent. Other independent
|
|
45
|
+
selected agents can still be configured. Adding agents does not uninstall others.
|
|
46
|
+
|
|
47
|
+
## Connection and verification
|
|
48
|
+
|
|
49
|
+
A local check verifies configuration, not a live authenticated host. Status
|
|
50
|
+
reports `configured` and `activation: unverified`; it never infers a connection
|
|
51
|
+
from a generated file. Each agent must load the connection and its hooks.
|
|
52
|
+
|
|
53
|
+
Interactive setup checks native sign-in requirements first. Claude Code must
|
|
54
|
+
approve a new project MCP server before OAuth; setup shows that action instead
|
|
55
|
+
of attempting a login that will fail. Codex can authenticate the selected endpoint
|
|
56
|
+
with a one-command configuration override while project and hook trust remain
|
|
57
|
+
explicit user actions. Existing Claude/OpenCode authentication is reused when
|
|
58
|
+
the client reports it, and successful logins are reflected in the final summary.
|
|
59
|
+
Use `--connect` to retry native sign-in for selected clients. Cursor and
|
|
60
|
+
desktop-only installations show their UI steps. In unattended runs, setup prints instructions and never blocks waiting
|
|
61
|
+
for OAuth. `--json` returns structured results without configuration contents or
|
|
62
|
+
credentials; use `--yes` to apply an explicitly selected setup.
|
|
63
|
+
|
|
64
|
+
Codex requires review and trust of the Groundtrack hook through `/hooks` or its
|
|
65
|
+
hook approval UI. New Codex setups use `.codex/hooks.json`; existing inline
|
|
66
|
+
hooks in `.codex/config.toml` are recognized and repaired without duplicating
|
|
67
|
+
them. Claude's `CLAUDE.md` imports the shared `AGENTS.md` policy. The plugin is
|
|
68
|
+
added to the existing OpenCode JSON/JSONC configuration.
|
|
69
|
+
|
|
70
|
+
In each selected agent, ask:
|
|
71
|
+
|
|
72
|
+
> Verify Groundtrack's connection and hook context with search_experiences.
|
|
73
|
+
|
|
74
|
+
The returned hook-context and instruction-version warnings determine whether
|
|
75
|
+
live activation needs attention. No synthetic engineering event is required.
|
|
76
|
+
|
|
77
|
+
## Doctor compatibility
|
|
78
|
+
|
|
79
|
+
`groundtrack doctor` is an alias for `status`; `groundtrack doctor --fix` uses the
|
|
80
|
+
same setup and repair flow. Existing single-client commands continue to work:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
groundtrack doctor --fix --client codex
|
|
84
|
+
```
|
|
13
85
|
|
|
14
|
-
The
|
|
15
|
-
|
|
86
|
+
The lower-level `runDoctor` API remains for existing consumers. New setup uses
|
|
87
|
+
client-specific plans for MCP, hooks/plugins, and instructions, so it can preview
|
|
88
|
+
and repair several selected agents safely.
|
|
16
89
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
90
|
+
`templates/AGENTS.md` is the source of truth for the managed repository policy.
|
|
91
|
+
After changing it, run `pnpm sync:agent-instructions`; tests reject drift between
|
|
92
|
+
the setup guide, this repository's managed `AGENTS.md` block, and the packaged template.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
Agent IDs, names, aliases, event sources, and enrichment kinds are defined once in
|
|
97
|
+
`src/agents.ts`, exported as `@groundtrack/hooks/agents`. The CLI, web UI, hooks,
|
|
98
|
+
and server consume that catalog and its derived types. Keep it dependency-free
|
|
99
|
+
so browser bundles can load it without the Node-only hook runtime.
|
|
100
|
+
|
|
101
|
+
Build and test the CLI from the repository:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
pnpm --filter @groundtrack/hooks build
|
|
105
|
+
node packages/hooks/dist/groundtrack.js setup --repo /path/to/test-project
|
|
106
|
+
pnpm --filter @groundtrack/hooks test
|
|
20
107
|
```
|
|
21
108
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
repository. This limits MCP alias discovery and configuration changes to that
|
|
29
|
-
client. If more than one client is detected, `doctor --fix` refuses to guess
|
|
30
|
-
which one to modify.
|
|
31
|
-
|
|
32
|
-
The Doctor verifies local configuration and enrichment behavior, but it cannot
|
|
33
|
-
observe whether a coding-agent host loaded or trusted a project hook. A
|
|
34
|
-
`search_experiences` MCP response is the authoritative live check: Groundtrack
|
|
35
|
-
returns a `hook_context_missing` warning when hook-injected session context is
|
|
36
|
-
absent. In Codex, use `/hooks` to review and trust the Groundtrack hook when that
|
|
37
|
-
warning appears.
|
|
38
|
-
|
|
39
|
-
`doctor --fix` previews the diagnosis, asks before changing anything, updates
|
|
40
|
-
only Groundtrack-owned hook entries and versioned managed instruction blocks,
|
|
41
|
-
then repeats the verification. Use `--yes` for an explicitly authorized
|
|
42
|
-
unattended repair. Existing surrounding instructions and unrelated hooks are
|
|
43
|
-
preserved; malformed JSON and symbolic links are reported instead of
|
|
44
|
-
overwritten.
|
|
45
|
-
|
|
46
|
-
The CLI compares its package version with npm. Canonical hook configuration and
|
|
47
|
-
instruction templates ship inside that versioned package, so repair does not
|
|
48
|
-
depend on an LLM or download arbitrary configuration text.
|
|
49
|
-
|
|
50
|
-
`templates/AGENTS.md` is the single source of truth for the managed repository
|
|
51
|
-
policy. After changing it, run `pnpm sync:agent-instructions` to regenerate the
|
|
52
|
-
setup-guide example; tests reject drift between the generated copy and the
|
|
53
|
-
packaged template.
|
|
109
|
+
With Claude Code and Codex installed, also run the isolated native-client
|
|
110
|
+
preflight checks (they do not start OAuth or touch your normal agent profiles):
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
GROUNDTRACK_TEST_NATIVE_CLIENTS=1 pnpm --filter @groundtrack/hooks test
|
|
114
|
+
```
|
|
54
115
|
|
|
55
116
|
The `groundtrack-hook` command reads a pre-tool-use hook payload from standard input and enriches
|
|
56
117
|
Groundtrack `store_event` and `search_experiences` calls with:
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Agent } from "./agents.js";
|
|
2
|
+
/** Canonical managed Groundtrack policy, packaged with the CLI. */
|
|
3
|
+
export declare const AGENT_INSTRUCTION_BODY: string;
|
|
4
|
+
/** Version is derived from the canonical template marker; never define it separately. */
|
|
5
|
+
export declare const AGENT_INSTRUCTION_TEMPLATE_VERSION: number;
|
|
6
|
+
export type AgentInstructionsClient = Agent;
|
|
7
|
+
/** Detect the managed policy version available to a client in one repository. */
|
|
8
|
+
export declare function detectAgentInstructionsVersion(repoDir: string, client: AgentInstructionsClient): number | null;
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Shared by the CLI, browser UI, hooks, and server. Keep this module dependency-free. */
|
|
2
|
+
export declare const AGENTS: readonly [{
|
|
3
|
+
readonly id: "claude";
|
|
4
|
+
readonly name: "Claude Code";
|
|
5
|
+
readonly aliases: readonly ["claude-code"];
|
|
6
|
+
readonly source: "claude_code";
|
|
7
|
+
readonly enrichment: "command";
|
|
8
|
+
}, {
|
|
9
|
+
readonly id: "codex";
|
|
10
|
+
readonly name: "Codex";
|
|
11
|
+
readonly aliases: readonly [];
|
|
12
|
+
readonly source: "codex";
|
|
13
|
+
readonly enrichment: "command";
|
|
14
|
+
}, {
|
|
15
|
+
readonly id: "cursor";
|
|
16
|
+
readonly name: "Cursor";
|
|
17
|
+
readonly aliases: readonly [];
|
|
18
|
+
readonly source: "cursor";
|
|
19
|
+
readonly enrichment: "command";
|
|
20
|
+
}, {
|
|
21
|
+
readonly id: "opencode";
|
|
22
|
+
readonly name: "OpenCode";
|
|
23
|
+
readonly aliases: readonly [];
|
|
24
|
+
readonly source: "opencode";
|
|
25
|
+
readonly enrichment: "plugin";
|
|
26
|
+
}];
|
|
27
|
+
export type AgentDefinition = (typeof AGENTS)[number];
|
|
28
|
+
export type Agent = AgentDefinition["id"];
|
|
29
|
+
export type CommandHookAgent = Extract<AgentDefinition, {
|
|
30
|
+
enrichment: "command";
|
|
31
|
+
}>["id"];
|
|
32
|
+
export declare const AGENT_IDS: ("claude" | "codex" | "cursor" | "opencode")[];
|
|
33
|
+
export declare function isAgent(value: unknown): value is Agent;
|
|
34
|
+
export declare function resolveAgent(value: string): AgentDefinition | undefined;
|
|
35
|
+
export declare function agentFromClientName(value: string | null | undefined): AgentDefinition | undefined;
|
|
36
|
+
export declare function requiresHookRuntime(id: Agent): boolean;
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
];
|
|
14
|
+
export const AGENT_IDS = AGENTS.map((agent) => agent.id);
|
|
15
|
+
export function isAgent(value) {
|
|
16
|
+
return AGENTS.some((agent) => agent.id === value);
|
|
17
|
+
}
|
|
18
|
+
export function resolveAgent(value) {
|
|
19
|
+
return AGENTS.find((agent) => agent.id === value || agent.aliases.some((alias) => alias === value));
|
|
20
|
+
}
|
|
21
|
+
export function agentFromClientName(value) {
|
|
22
|
+
const normalized = value?.trim().toLowerCase() ?? "";
|
|
23
|
+
return AGENTS.find((agent) => normalized.includes(agent.id));
|
|
24
|
+
}
|
|
25
|
+
export function requiresHookRuntime(id) {
|
|
26
|
+
return AGENTS.some((agent) => agent.id === id && agent.enrichment === "command");
|
|
27
|
+
}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CommandHookAgent } from "./agents.js";
|
|
2
|
+
export declare const CLAUDE_INSTRUCTION_BODY: string;
|
|
3
|
+
export type Client = CommandHookAgent;
|
|
4
|
+
type CheckStatus = "ok" | "advisory" | "issue" | "fixed" | "blocked";
|
|
5
|
+
export type DoctorCheck = {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
status: CheckStatus;
|
|
9
|
+
detail: string;
|
|
10
|
+
file?: string;
|
|
11
|
+
};
|
|
12
|
+
export type DoctorOptions = {
|
|
13
|
+
repoDir: string;
|
|
14
|
+
homeDir: string;
|
|
15
|
+
fix?: boolean;
|
|
16
|
+
clients?: Client[];
|
|
17
|
+
installedHookVersion?: string | null;
|
|
18
|
+
latestVersion?: string | null;
|
|
19
|
+
skipPackageInstall?: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type DoctorResult = {
|
|
22
|
+
version: string;
|
|
23
|
+
clients: Client[];
|
|
24
|
+
aliases: string[];
|
|
25
|
+
checks: DoctorCheck[];
|
|
26
|
+
changedFiles: string[];
|
|
27
|
+
healthy: boolean;
|
|
28
|
+
};
|
|
29
|
+
export declare function claudeInstructionsAreCurrent(content: string | null): boolean;
|
|
30
|
+
export declare function claudeUsesInlineGroundtrackInstructions(content: string | null): boolean;
|
|
31
|
+
export declare function upsertManagedContent(content: string | null, desired: string, migrateLegacyAgentInstructions?: boolean): string;
|
|
32
|
+
export declare function compareVersions(left: string, right: string): number;
|
|
33
|
+
export declare function runDoctor(options: DoctorOptions): DoctorResult;
|
|
34
|
+
export {};
|
package/dist/doctor.js
CHANGED
|
@@ -7,7 +7,7 @@ import { packageVersion } from "./version.js";
|
|
|
7
7
|
const HOOK_TEMPLATE_VERSION = 1;
|
|
8
8
|
const MANAGED_START = `<!-- groundtrack:start version=${AGENT_INSTRUCTION_TEMPLATE_VERSION} -->`;
|
|
9
9
|
const MANAGED_END = "<!-- groundtrack:end -->";
|
|
10
|
-
const CLAUDE_INSTRUCTION_BODY = `${MANAGED_START}
|
|
10
|
+
export const CLAUDE_INSTRUCTION_BODY = `${MANAGED_START}
|
|
11
11
|
@AGENTS.md
|
|
12
12
|
${MANAGED_END}`;
|
|
13
13
|
const UNMARKED_AGENT_INSTRUCTION_BODY = AGENT_INSTRUCTION_BODY.replace(MANAGED_START, "")
|
|
@@ -232,14 +232,17 @@ function managedContentIsCurrent(content, desired) {
|
|
|
232
232
|
const end = content.indexOf(MANAGED_END, start);
|
|
233
233
|
return start >= 0 && end >= 0 && content.slice(start, end + MANAGED_END.length) === desired;
|
|
234
234
|
}
|
|
235
|
-
function claudeInstructionsAreCurrent(content) {
|
|
235
|
+
export function claudeInstructionsAreCurrent(content) {
|
|
236
236
|
if (!content)
|
|
237
237
|
return false;
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
if (/^@AGENTS\.md\s*$/m.test(content)) {
|
|
239
|
+
const bridgeVersion = /<!-- groundtrack:start version=(\d+) -->/.exec(content)?.[1];
|
|
240
|
+
return (bridgeVersion === undefined || Number(bridgeVersion) >= AGENT_INSTRUCTION_TEMPLATE_VERSION);
|
|
241
|
+
}
|
|
242
|
+
return (managedContentIsCurrent(content, AGENT_INSTRUCTION_BODY) ||
|
|
240
243
|
content.includes(UNMARKED_AGENT_INSTRUCTION_BODY));
|
|
241
244
|
}
|
|
242
|
-
function claudeUsesInlineGroundtrackInstructions(content) {
|
|
245
|
+
export function claudeUsesInlineGroundtrackInstructions(content) {
|
|
243
246
|
return content?.includes(LEGACY_AGENT_SIGNATURE) ?? false;
|
|
244
247
|
}
|
|
245
248
|
function removeLegacyAgentInstructions(content) {
|
|
@@ -266,7 +269,7 @@ function removeLegacyAgentInstructions(content) {
|
|
|
266
269
|
}
|
|
267
270
|
return lines.join("\n");
|
|
268
271
|
}
|
|
269
|
-
function upsertManagedContent(content, desired, migrateLegacyAgentInstructions = false) {
|
|
272
|
+
export function upsertManagedContent(content, desired, migrateLegacyAgentInstructions = false) {
|
|
270
273
|
const existing = content ?? "";
|
|
271
274
|
const managed = /<!-- groundtrack:start version=\d+ -->[\s\S]*?<!-- groundtrack:end -->/;
|
|
272
275
|
if (managed.test(existing)) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type HookInput = {
|
|
2
|
+
conversation_id?: unknown;
|
|
3
|
+
cwd?: unknown;
|
|
4
|
+
generation_id?: unknown;
|
|
5
|
+
model?: unknown;
|
|
6
|
+
model_id?: unknown;
|
|
7
|
+
session_id?: unknown;
|
|
8
|
+
transcript_path?: unknown;
|
|
9
|
+
tool_input?: unknown;
|
|
10
|
+
tool_name?: unknown;
|
|
11
|
+
tool_use_id?: unknown;
|
|
12
|
+
workspace_roots?: unknown;
|
|
13
|
+
};
|
|
14
|
+
export type GroundtrackContextTool = "store_event" | "search_experiences";
|
|
15
|
+
export declare function groundtrackContextTool(toolName: unknown): GroundtrackContextTool | null;
|
|
16
|
+
export declare function enrichInput(hookInput: HookInput, clientOverride?: string, tool?: GroundtrackContextTool): Record<string, unknown>;
|
|
17
|
+
export declare function hookOutput(hookInput: HookInput, clientOverride?: string): Record<string, unknown>;
|
|
18
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAgent } from "./agents.js";
|
|
1
2
|
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
3
4
|
import { detectAgentInstructionsVersion, } from "./agent-instructions.js";
|
|
@@ -73,7 +74,7 @@ function transcriptModel(transcriptPath) {
|
|
|
73
74
|
}
|
|
74
75
|
return null;
|
|
75
76
|
}
|
|
76
|
-
function groundtrackContextTool(toolName) {
|
|
77
|
+
export function groundtrackContextTool(toolName) {
|
|
77
78
|
const normalized = nonEmptyString(toolName)?.toLowerCase();
|
|
78
79
|
if (!normalized)
|
|
79
80
|
return null;
|
|
@@ -149,7 +150,7 @@ export function enrichInput(hookInput, clientOverride, tool = "store_event") {
|
|
|
149
150
|
toolInput.clientEventId = [host, hostSessionId, invocationId].filter(Boolean).join(":");
|
|
150
151
|
}
|
|
151
152
|
if (tool === "search_experiences") {
|
|
152
|
-
const client = host
|
|
153
|
+
const client = isAgent(host) ? host : "codex";
|
|
153
154
|
const root = repositoryRoot(cwd);
|
|
154
155
|
toolInput.agentInstructionsVersion = root ? detectAgentInstructionsVersion(root, client) : null;
|
|
155
156
|
}
|
package/dist/groundtrack.js
CHANGED
|
@@ -1,199 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { compareVersions, runDoctor } from "./doctor.js";
|
|
8
|
-
import { packageVersion } from "./version.js";
|
|
9
|
-
function usage() {
|
|
10
|
-
return `Groundtrack CLI
|
|
11
|
-
|
|
12
|
-
Usage:
|
|
13
|
-
groundtrack doctor [--fix] [--yes] [--json] [--client CLIENT] [--repo PATH]
|
|
14
|
-
groundtrack version [--json]
|
|
15
|
-
|
|
16
|
-
Commands:
|
|
17
|
-
doctor Diagnose Groundtrack hooks and managed agent instructions
|
|
18
|
-
doctor --fix Repair deterministic configuration drift
|
|
19
|
-
version Print the installed CLI/runtime version
|
|
20
|
-
|
|
21
|
-
Options:
|
|
22
|
-
--client Limit diagnosis and repair to claude, codex, or cursor
|
|
23
|
-
`;
|
|
24
|
-
}
|
|
25
|
-
function parseArguments(argv) {
|
|
26
|
-
if (argv[0] &&
|
|
27
|
-
!["doctor", "version", "--version"].includes(argv[0]) &&
|
|
28
|
-
!argv[0].startsWith("-")) {
|
|
29
|
-
throw new Error(`Unknown command: ${argv[0]}\n\n${usage()}`);
|
|
30
|
-
}
|
|
31
|
-
const command = argv[0] === "version" || argv[0] === "--version" ? "version" : "doctor";
|
|
32
|
-
let repoDir = process.cwd();
|
|
33
|
-
let homeDir = homedir();
|
|
34
|
-
let client;
|
|
35
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
36
|
-
if (argv[index] === "--repo" && argv[index + 1])
|
|
37
|
-
repoDir = resolve(argv[++index]);
|
|
38
|
-
if (argv[index] === "--home" && argv[index + 1])
|
|
39
|
-
homeDir = resolve(argv[++index]);
|
|
40
|
-
if (argv[index] === "--client") {
|
|
41
|
-
const value = argv[++index];
|
|
42
|
-
if (value !== "claude" && value !== "codex" && value !== "cursor") {
|
|
43
|
-
throw new Error("--client must be one of: claude, codex, cursor");
|
|
44
|
-
}
|
|
45
|
-
client = value;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
command,
|
|
50
|
-
fix: argv.includes("--fix"),
|
|
51
|
-
yes: argv.includes("--yes") || argv.includes("-y"),
|
|
52
|
-
json: argv.includes("--json"),
|
|
53
|
-
client,
|
|
54
|
-
repoDir,
|
|
55
|
-
homeDir,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
function printResult(result, options = {}) {
|
|
59
|
-
const selectedClient = options.client ?? (result.clients.length === 1 ? result.clients[0] : null);
|
|
60
|
-
const fixHint = !options.showFixHint
|
|
61
|
-
? ""
|
|
62
|
-
: selectedClient
|
|
63
|
-
? ` Run: groundtrack doctor --fix --client ${selectedClient}`
|
|
64
|
-
: " Re-run with --client claude, --client codex, or --client cursor.";
|
|
65
|
-
stdout.write(`Groundtrack doctor ${result.version}\n`);
|
|
66
|
-
stdout.write(`Clients: ${result.clients.join(", ")}\n`);
|
|
67
|
-
stdout.write(`MCP aliases: ${result.aliases.join(", ")}\n\n`);
|
|
68
|
-
for (const check of result.checks) {
|
|
69
|
-
const icon = check.status === "ok"
|
|
70
|
-
? "✓"
|
|
71
|
-
: check.status === "advisory"
|
|
72
|
-
? "i"
|
|
73
|
-
: check.status === "fixed"
|
|
74
|
-
? "◆"
|
|
75
|
-
: check.status === "blocked"
|
|
76
|
-
? "×"
|
|
77
|
-
: "!";
|
|
78
|
-
stdout.write(`${icon} ${check.label}: ${check.detail}${check.file ? ` (${check.file})` : ""}\n`);
|
|
79
|
-
}
|
|
80
|
-
if (result.changedFiles.length > 0) {
|
|
81
|
-
stdout.write(`\nUpdated ${result.changedFiles.length} file(s).\n`);
|
|
82
|
-
}
|
|
83
|
-
stdout.write(result.healthy
|
|
84
|
-
? "\nGroundtrack local configuration is healthy. Live hook activation is verified by Groundtrack MCP calls.\n"
|
|
85
|
-
: `\nGroundtrack local configuration needs attention.${fixHint}\n`);
|
|
86
|
-
}
|
|
87
|
-
async function confirmFix() {
|
|
88
|
-
const prompt = createInterface({ input: stdin, output: stdout });
|
|
89
|
-
try {
|
|
90
|
-
const answer = await prompt.question("Apply deterministic Groundtrack repairs? [y/N] ");
|
|
91
|
-
return /^y(?:es)?$/i.test(answer.trim());
|
|
92
|
-
}
|
|
93
|
-
finally {
|
|
94
|
-
prompt.close();
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
async function latestPackageVersion() {
|
|
98
|
-
const override = process.env.GROUNDTRACK_DOCTOR_LATEST_VERSION;
|
|
99
|
-
if (override)
|
|
100
|
-
return override;
|
|
101
|
-
if (process.env.GROUNDTRACK_DOCTOR_OFFLINE === "1")
|
|
102
|
-
return null;
|
|
103
|
-
const controller = new AbortController();
|
|
104
|
-
const timeout = setTimeout(() => controller.abort(), 2_000);
|
|
105
|
-
try {
|
|
106
|
-
const response = await fetch("https://registry.npmjs.org/@groundtrack%2Fhooks/latest", {
|
|
107
|
-
signal: controller.signal,
|
|
108
|
-
headers: { accept: "application/json" },
|
|
109
|
-
});
|
|
110
|
-
if (!response.ok)
|
|
111
|
-
return null;
|
|
112
|
-
const payload = (await response.json());
|
|
113
|
-
return typeof payload.version === "string" ? payload.version : null;
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
finally {
|
|
119
|
-
clearTimeout(timeout);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
function updatePackageAndRerun(version) {
|
|
123
|
-
const install = spawnSync("npm", ["install", "-g", `@groundtrack/hooks@${version}`], {
|
|
124
|
-
stdio: "inherit",
|
|
125
|
-
timeout: 120_000,
|
|
126
|
-
});
|
|
127
|
-
if (install.status !== 0)
|
|
128
|
-
return install.status ?? 1;
|
|
129
|
-
const rerun = spawnSync("groundtrack", process.argv.slice(2), {
|
|
130
|
-
stdio: "inherit",
|
|
131
|
-
env: {
|
|
132
|
-
...process.env,
|
|
133
|
-
GROUNDTRACK_DOCTOR_REEXEC: "1",
|
|
134
|
-
GROUNDTRACK_DOCTOR_LATEST_VERSION: version,
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
return rerun.status ?? 1;
|
|
138
|
-
}
|
|
139
|
-
async function main() {
|
|
140
|
-
const args = parseArguments(process.argv.slice(2));
|
|
141
|
-
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
142
|
-
stdout.write(usage());
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (args.command === "version") {
|
|
146
|
-
const version = packageVersion();
|
|
147
|
-
stdout.write(args.json ? `${JSON.stringify({ version })}\n` : `${version}\n`);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const base = {
|
|
151
|
-
repoDir: args.repoDir,
|
|
152
|
-
homeDir: args.homeDir,
|
|
153
|
-
clients: args.client ? [args.client] : undefined,
|
|
154
|
-
skipPackageInstall: process.env.GROUNDTRACK_DOCTOR_SKIP_INSTALL === "1",
|
|
155
|
-
latestVersion: await latestPackageVersion(),
|
|
156
|
-
};
|
|
157
|
-
const diagnosis = runDoctor(base);
|
|
158
|
-
if (!args.fix) {
|
|
159
|
-
if (args.json)
|
|
160
|
-
stdout.write(`${JSON.stringify(diagnosis, null, 2)}\n`);
|
|
161
|
-
else
|
|
162
|
-
printResult(diagnosis, { showFixHint: true, client: args.client });
|
|
163
|
-
process.exitCode = diagnosis.healthy ? 0 : 1;
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
if (!args.client && diagnosis.clients.length > 1) {
|
|
167
|
-
throw new Error(`Multiple coding-agent clients detected: ${diagnosis.clients.join(", ")}. Re-run with --client claude, --client codex, or --client cursor so Doctor does not modify unrelated client configuration.`);
|
|
168
|
-
}
|
|
169
|
-
if (diagnosis.healthy) {
|
|
170
|
-
if (args.json)
|
|
171
|
-
stdout.write(`${JSON.stringify(diagnosis, null, 2)}\n`);
|
|
172
|
-
else
|
|
173
|
-
printResult(diagnosis);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (!args.json)
|
|
177
|
-
printResult(diagnosis);
|
|
178
|
-
if (!args.yes && !(await confirmFix())) {
|
|
179
|
-
stdout.write("No changes made.\n");
|
|
180
|
-
process.exitCode = 1;
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (base.latestVersion &&
|
|
184
|
-
compareVersions(base.latestVersion, diagnosis.version) > 0 &&
|
|
185
|
-
process.env.GROUNDTRACK_DOCTOR_REEXEC !== "1") {
|
|
186
|
-
process.exitCode = updatePackageAndRerun(base.latestVersion);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
const repaired = runDoctor({ ...base, fix: true });
|
|
190
|
-
if (args.json)
|
|
191
|
-
stdout.write(`${JSON.stringify(repaired, null, 2)}\n`);
|
|
2
|
+
import { runCli } from "./setup/cli.js";
|
|
3
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
4
|
+
const message = error instanceof Error ? error.message : "Groundtrack setup failed.";
|
|
5
|
+
if (process.argv.includes("--json"))
|
|
6
|
+
process.stdout.write(`${JSON.stringify({ error: message, applied: false })}\n`);
|
|
192
7
|
else
|
|
193
|
-
|
|
194
|
-
process.exitCode = repaired.healthy ? 0 : 1;
|
|
195
|
-
}
|
|
196
|
-
main().catch((error) => {
|
|
197
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
8
|
+
process.stderr.write(`${message}\n`);
|
|
198
9
|
process.exitCode = 1;
|
|
199
10
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Agent } from "../agents.js";
|
|
2
|
+
export declare const MCP_URL = "https://app.groundtrack.dev/api/mcp";
|
|
3
|
+
export type SetupEnvironment = {
|
|
4
|
+
repoDir: string;
|
|
5
|
+
homeDir: string;
|
|
6
|
+
env: NodeJS.ProcessEnv;
|
|
7
|
+
platform: NodeJS.Platform;
|
|
8
|
+
};
|
|
9
|
+
export declare function environment(options?: Partial<SetupEnvironment>): SetupEnvironment;
|
|
10
|
+
export declare function agentId(value: string): Agent;
|
|
11
|
+
export declare function executable(name: string, context: SetupEnvironment): string | null;
|
|
12
|
+
export declare function stateDir(context: SetupEnvironment): string;
|
|
13
|
+
export declare function configPaths(agent: Agent, context: SetupEnvironment): {
|
|
14
|
+
project: string;
|
|
15
|
+
user: string;
|
|
16
|
+
presence: string[];
|
|
17
|
+
};
|
|
18
|
+
export declare function serverPath(agent: Agent, config: Record<string, unknown>): string[];
|
|
19
|
+
export declare function readConfig(agent: Agent, text: string): Record<string, unknown>;
|
|
20
|
+
export declare function serverMap(agent: Agent, config: Record<string, unknown>): Record<string, unknown>;
|
|
21
|
+
export declare function isGroundtrack(alias: string, value: unknown, url: string): boolean;
|
|
22
|
+
export declare function discoverAgents(context: SetupEnvironment, url?: string): ({
|
|
23
|
+
id: "claude";
|
|
24
|
+
name: "Claude Code";
|
|
25
|
+
aliases: readonly ["claude-code"];
|
|
26
|
+
source: "claude_code";
|
|
27
|
+
enrichment: "command";
|
|
28
|
+
detected: boolean;
|
|
29
|
+
configured: boolean;
|
|
30
|
+
issue: string | null;
|
|
31
|
+
} | {
|
|
32
|
+
id: "codex";
|
|
33
|
+
name: "Codex";
|
|
34
|
+
aliases: readonly [];
|
|
35
|
+
source: "codex";
|
|
36
|
+
enrichment: "command";
|
|
37
|
+
detected: boolean;
|
|
38
|
+
configured: boolean;
|
|
39
|
+
issue: string | null;
|
|
40
|
+
} | {
|
|
41
|
+
id: "cursor";
|
|
42
|
+
name: "Cursor";
|
|
43
|
+
aliases: readonly [];
|
|
44
|
+
source: "cursor";
|
|
45
|
+
enrichment: "command";
|
|
46
|
+
detected: boolean;
|
|
47
|
+
configured: boolean;
|
|
48
|
+
issue: string | null;
|
|
49
|
+
} | {
|
|
50
|
+
id: "opencode";
|
|
51
|
+
name: "OpenCode";
|
|
52
|
+
aliases: readonly [];
|
|
53
|
+
source: "opencode";
|
|
54
|
+
enrichment: "plugin";
|
|
55
|
+
detected: boolean;
|
|
56
|
+
configured: boolean;
|
|
57
|
+
issue: string | null;
|
|
58
|
+
})[];
|
|
59
|
+
export declare function savedAgents(context: SetupEnvironment): Agent[] | null;
|