context-mode 1.0.89 → 1.0.91
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +184 -60
- package/build/adapters/antigravity/index.d.ts +3 -5
- package/build/adapters/antigravity/index.js +7 -35
- package/build/adapters/base.d.ts +27 -0
- package/build/adapters/base.js +59 -0
- package/build/adapters/claude-code/index.d.ts +9 -25
- package/build/adapters/claude-code/index.js +12 -140
- package/build/adapters/claude-code-base.d.ts +49 -0
- package/build/adapters/claude-code-base.js +113 -0
- package/build/adapters/client-map.js +5 -0
- package/build/adapters/codex/hooks.d.ts +21 -14
- package/build/adapters/codex/hooks.js +22 -15
- package/build/adapters/codex/index.d.ts +6 -10
- package/build/adapters/codex/index.js +13 -43
- package/build/adapters/copilot-base.d.ts +78 -0
- package/build/adapters/copilot-base.js +281 -0
- package/build/adapters/cursor/index.d.ts +3 -5
- package/build/adapters/cursor/index.js +6 -34
- package/build/adapters/detect.d.ts +7 -0
- package/build/adapters/detect.js +57 -56
- package/build/adapters/gemini-cli/index.d.ts +3 -5
- package/build/adapters/gemini-cli/index.js +7 -35
- package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
- package/build/adapters/jetbrains-copilot/config.js +8 -0
- package/build/adapters/jetbrains-copilot/hooks.d.ts +51 -0
- package/build/adapters/jetbrains-copilot/hooks.js +82 -0
- package/build/adapters/jetbrains-copilot/index.d.ts +24 -0
- package/build/adapters/jetbrains-copilot/index.js +119 -0
- package/build/adapters/kiro/hooks.d.ts +14 -0
- package/build/adapters/kiro/hooks.js +23 -0
- package/build/adapters/kiro/index.d.ts +3 -5
- package/build/adapters/kiro/index.js +10 -38
- package/build/adapters/openclaw/index.d.ts +3 -4
- package/build/adapters/openclaw/index.js +6 -22
- package/build/adapters/opencode/index.d.ts +2 -3
- package/build/adapters/opencode/index.js +5 -16
- package/build/adapters/qwen-code/index.d.ts +39 -0
- package/build/adapters/qwen-code/index.js +199 -0
- package/build/adapters/types.d.ts +1 -1
- package/build/adapters/vscode-copilot/index.d.ts +16 -46
- package/build/adapters/vscode-copilot/index.js +29 -320
- package/build/adapters/zed/index.d.ts +3 -5
- package/build/adapters/zed/index.js +7 -35
- package/build/cli.js +13 -0
- package/build/lifecycle.d.ts +23 -0
- package/build/lifecycle.js +54 -13
- package/build/opencode-plugin.d.ts +19 -7
- package/build/opencode-plugin.js +19 -7
- package/build/runtime.js +24 -9
- package/build/security.d.ts +17 -1
- package/build/security.js +40 -6
- package/build/server.js +53 -10
- package/build/session/analytics.d.ts +8 -7
- package/build/session/analytics.js +107 -76
- package/build/session/db.d.ts +10 -1
- package/build/session/db.js +67 -8
- package/build/session/extract.js +10 -2
- package/build/session/project-attribution.d.ts +73 -0
- package/build/session/project-attribution.js +231 -0
- package/build/store.d.ts +4 -0
- package/build/store.js +58 -9
- package/build/types.d.ts +8 -0
- package/cli.bundle.mjs +135 -121
- package/configs/antigravity/GEMINI.md +31 -36
- package/configs/claude-code/CLAUDE.md +31 -37
- package/configs/codex/AGENTS.md +35 -49
- package/configs/cursor/context-mode.mdc +24 -25
- package/configs/gemini-cli/GEMINI.md +30 -36
- package/configs/jetbrains-copilot/copilot-instructions.md +59 -0
- package/configs/jetbrains-copilot/hooks.json +16 -0
- package/configs/jetbrains-copilot/mcp.json +8 -0
- package/configs/kilo/AGENTS.md +30 -36
- package/configs/kiro/KIRO.md +30 -36
- package/configs/kiro/agent.json +1 -1
- package/configs/openclaw/AGENTS.md +30 -36
- package/configs/opencode/AGENTS.md +30 -36
- package/configs/pi/AGENTS.md +31 -36
- package/configs/qwen-code/QWEN.md +63 -0
- package/configs/vscode-copilot/copilot-instructions.md +30 -36
- package/configs/zed/AGENTS.md +31 -36
- package/hooks/codex/posttooluse.mjs +7 -7
- package/hooks/codex/pretooluse.mjs +3 -3
- package/hooks/codex/sessionstart.mjs +2 -1
- package/hooks/core/formatters.mjs +24 -0
- package/hooks/core/routing.mjs +40 -15
- package/hooks/core/tool-naming.mjs +2 -0
- package/hooks/cursor/posttooluse.mjs +7 -7
- package/hooks/cursor/pretooluse.mjs +3 -3
- package/hooks/cursor/sessionstart.mjs +2 -1
- package/hooks/cursor/stop.mjs +2 -2
- package/hooks/ensure-deps.mjs +22 -10
- package/hooks/gemini-cli/aftertool.mjs +8 -8
- package/hooks/gemini-cli/beforetool.mjs +3 -2
- package/hooks/gemini-cli/precompress.mjs +2 -2
- package/hooks/gemini-cli/sessionstart.mjs +12 -4
- package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
- package/hooks/jetbrains-copilot/precompact.mjs +54 -0
- package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
- package/hooks/jetbrains-copilot/sessionstart.mjs +119 -0
- package/hooks/kiro/posttooluse.mjs +6 -7
- package/hooks/kiro/pretooluse.mjs +3 -2
- package/hooks/posttooluse.mjs +8 -8
- package/hooks/precompact.mjs +3 -4
- package/hooks/pretooluse.mjs +5 -4
- package/hooks/routing-block.mjs +35 -33
- package/hooks/session-attribution.bundle.mjs +1 -0
- package/hooks/session-db.bundle.mjs +27 -8
- package/hooks/session-extract.bundle.mjs +2 -1
- package/hooks/session-helpers.mjs +44 -3
- package/hooks/session-loaders.mjs +37 -0
- package/hooks/sessionstart.mjs +5 -5
- package/hooks/userpromptsubmit.mjs +26 -9
- package/hooks/vscode-copilot/posttooluse.mjs +8 -8
- package/hooks/vscode-copilot/precompact.mjs +2 -2
- package/hooks/vscode-copilot/pretooluse.mjs +3 -2
- package/hooks/vscode-copilot/sessionstart.mjs +2 -2
- package/insight/server.mjs +237 -25
- package/insight/src/lib/api.ts +2 -1
- package/insight/src/routes/index.tsx +16 -3
- package/insight/src/routes/search.tsx +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +11 -2
- package/server.bundle.mjs +94 -80
- package/skills/ctx-insight/SKILL.md +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CopilotBaseAdapter — shared implementation for VS Code Copilot and JetBrains Copilot.
|
|
3
|
+
*
|
|
4
|
+
* Both platforms share the SAME Copilot agent runtime:
|
|
5
|
+
* - hookSpecificOutput wrapper with hookEventName
|
|
6
|
+
* - Same hook events (PreToolUse, PostToolUse, PreCompact, SessionStart)
|
|
7
|
+
* - Same .github/hooks/ config location
|
|
8
|
+
* - Same configureHooks logic
|
|
9
|
+
* - Same generateHookConfig format
|
|
10
|
+
* - Same parse/format methods
|
|
11
|
+
*
|
|
12
|
+
* Platform-specific differences handled by subclasses:
|
|
13
|
+
* - extractSessionId() — different env var fallbacks
|
|
14
|
+
* - getProjectDir() — different env vars for project root
|
|
15
|
+
* - getSessionDir() — different default session directories
|
|
16
|
+
* - checkPluginRegistration() — VS Code reads .vscode/mcp.json, JetBrains uses IDE UI
|
|
17
|
+
* - getInstalledVersion() — VS Code checks extensions dir, JetBrains checks hook config
|
|
18
|
+
* - validateHooks() — different warning messages
|
|
19
|
+
*/
|
|
20
|
+
import { BaseAdapter } from "./base.js";
|
|
21
|
+
import type { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, PreToolUseEvent, PostToolUseEvent, PreCompactEvent, SessionStartEvent, PreToolUseResponse, PostToolUseResponse, PreCompactResponse, SessionStartResponse, HookRegistration } from "./types.js";
|
|
22
|
+
export interface CopilotHookInput {
|
|
23
|
+
tool_name?: string;
|
|
24
|
+
tool_input?: Record<string, unknown>;
|
|
25
|
+
tool_output?: string;
|
|
26
|
+
is_error?: boolean;
|
|
27
|
+
/** Copilot uses camelCase sessionId (NOT session_id). */
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
source?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface CopilotHookModule {
|
|
32
|
+
HOOK_TYPES: {
|
|
33
|
+
readonly PRE_TOOL_USE: string;
|
|
34
|
+
readonly POST_TOOL_USE: string;
|
|
35
|
+
readonly PRE_COMPACT: string;
|
|
36
|
+
readonly SESSION_START: string;
|
|
37
|
+
readonly STOP: string;
|
|
38
|
+
readonly SUBAGENT_START: string;
|
|
39
|
+
readonly SUBAGENT_STOP: string;
|
|
40
|
+
};
|
|
41
|
+
HOOK_SCRIPTS: Record<string, string>;
|
|
42
|
+
buildHookCommand: (hookType: any, pluginRoot?: string) => string;
|
|
43
|
+
}
|
|
44
|
+
export declare abstract class CopilotBaseAdapter extends BaseAdapter implements HookAdapter {
|
|
45
|
+
readonly paradigm: HookParadigm;
|
|
46
|
+
readonly capabilities: PlatformCapabilities;
|
|
47
|
+
/** Subclasses must provide their platform name. */
|
|
48
|
+
abstract readonly name: string;
|
|
49
|
+
/** Subclasses must provide their hook module (HOOK_TYPES, HOOK_SCRIPTS, buildHookCommand). */
|
|
50
|
+
protected abstract readonly hookModule: CopilotHookModule;
|
|
51
|
+
/** Subclasses must provide the hook scripts subdirectory name (e.g., "vscode-copilot"). */
|
|
52
|
+
protected abstract readonly hookSubdir: string;
|
|
53
|
+
/** Extract session ID from Copilot hook input — env var fallbacks differ per platform. */
|
|
54
|
+
protected abstract extractSessionId(input: CopilotHookInput): string;
|
|
55
|
+
/** Get the project directory — env vars differ per platform. */
|
|
56
|
+
protected abstract getProjectDir(): string;
|
|
57
|
+
/** Validate that hooks are properly configured for this platform. */
|
|
58
|
+
abstract validateHooks(pluginRoot: string): DiagnosticResult[];
|
|
59
|
+
/** Check if the plugin is registered/enabled on this platform. */
|
|
60
|
+
abstract checkPluginRegistration(): DiagnosticResult;
|
|
61
|
+
/** Get the installed version from this platform's registry/marketplace. */
|
|
62
|
+
abstract getInstalledVersion(): string;
|
|
63
|
+
parsePreToolUseInput(raw: unknown): PreToolUseEvent;
|
|
64
|
+
parsePostToolUseInput(raw: unknown): PostToolUseEvent;
|
|
65
|
+
parsePreCompactInput(raw: unknown): PreCompactEvent;
|
|
66
|
+
parseSessionStartInput(raw: unknown): SessionStartEvent;
|
|
67
|
+
formatPreToolUseResponse(response: PreToolUseResponse): unknown;
|
|
68
|
+
formatPostToolUseResponse(response: PostToolUseResponse): unknown;
|
|
69
|
+
formatPreCompactResponse(response: PreCompactResponse): unknown;
|
|
70
|
+
formatSessionStartResponse(response: SessionStartResponse): unknown;
|
|
71
|
+
getSettingsPath(): string;
|
|
72
|
+
generateHookConfig(pluginRoot: string): HookRegistration;
|
|
73
|
+
readSettings(): Record<string, unknown> | null;
|
|
74
|
+
writeSettings(settings: Record<string, unknown>): void;
|
|
75
|
+
configureAllHooks(pluginRoot: string): string[];
|
|
76
|
+
setHookPermissions(pluginRoot: string): string[];
|
|
77
|
+
updatePluginRegistry(_pluginRoot: string, _version: string): void;
|
|
78
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CopilotBaseAdapter — shared implementation for VS Code Copilot and JetBrains Copilot.
|
|
3
|
+
*
|
|
4
|
+
* Both platforms share the SAME Copilot agent runtime:
|
|
5
|
+
* - hookSpecificOutput wrapper with hookEventName
|
|
6
|
+
* - Same hook events (PreToolUse, PostToolUse, PreCompact, SessionStart)
|
|
7
|
+
* - Same .github/hooks/ config location
|
|
8
|
+
* - Same configureHooks logic
|
|
9
|
+
* - Same generateHookConfig format
|
|
10
|
+
* - Same parse/format methods
|
|
11
|
+
*
|
|
12
|
+
* Platform-specific differences handled by subclasses:
|
|
13
|
+
* - extractSessionId() — different env var fallbacks
|
|
14
|
+
* - getProjectDir() — different env vars for project root
|
|
15
|
+
* - getSessionDir() — different default session directories
|
|
16
|
+
* - checkPluginRegistration() — VS Code reads .vscode/mcp.json, JetBrains uses IDE UI
|
|
17
|
+
* - getInstalledVersion() — VS Code checks extensions dir, JetBrains checks hook config
|
|
18
|
+
* - validateHooks() — different warning messages
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync, writeFileSync, mkdirSync, accessSync, chmodSync, constants, } from "node:fs";
|
|
21
|
+
import { resolve, join } from "node:path";
|
|
22
|
+
import { BaseAdapter } from "./base.js";
|
|
23
|
+
// ─────────────────────────────────────────────────────────
|
|
24
|
+
// Abstract base adapter for Copilot platforms
|
|
25
|
+
// ─────────────────────────────────────────────────────────
|
|
26
|
+
export class CopilotBaseAdapter extends BaseAdapter {
|
|
27
|
+
paradigm = "json-stdio";
|
|
28
|
+
capabilities = {
|
|
29
|
+
preToolUse: true,
|
|
30
|
+
postToolUse: true,
|
|
31
|
+
preCompact: true,
|
|
32
|
+
sessionStart: true,
|
|
33
|
+
canModifyArgs: true,
|
|
34
|
+
canModifyOutput: true,
|
|
35
|
+
canInjectSessionContext: true,
|
|
36
|
+
};
|
|
37
|
+
// ── Input parsing (shared) ─────────────────────────────
|
|
38
|
+
parsePreToolUseInput(raw) {
|
|
39
|
+
const input = raw;
|
|
40
|
+
return {
|
|
41
|
+
toolName: input.tool_name ?? "",
|
|
42
|
+
toolInput: input.tool_input ?? {},
|
|
43
|
+
sessionId: this.extractSessionId(input),
|
|
44
|
+
projectDir: this.getProjectDir(),
|
|
45
|
+
raw,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
parsePostToolUseInput(raw) {
|
|
49
|
+
const input = raw;
|
|
50
|
+
return {
|
|
51
|
+
toolName: input.tool_name ?? "",
|
|
52
|
+
toolInput: input.tool_input ?? {},
|
|
53
|
+
toolOutput: input.tool_output,
|
|
54
|
+
isError: input.is_error,
|
|
55
|
+
sessionId: this.extractSessionId(input),
|
|
56
|
+
projectDir: this.getProjectDir(),
|
|
57
|
+
raw,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
parsePreCompactInput(raw) {
|
|
61
|
+
const input = raw;
|
|
62
|
+
return {
|
|
63
|
+
sessionId: this.extractSessionId(input),
|
|
64
|
+
projectDir: this.getProjectDir(),
|
|
65
|
+
raw,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
parseSessionStartInput(raw) {
|
|
69
|
+
const input = raw;
|
|
70
|
+
const rawSource = input.source ?? "startup";
|
|
71
|
+
let source;
|
|
72
|
+
switch (rawSource) {
|
|
73
|
+
case "compact":
|
|
74
|
+
source = "compact";
|
|
75
|
+
break;
|
|
76
|
+
case "resume":
|
|
77
|
+
source = "resume";
|
|
78
|
+
break;
|
|
79
|
+
case "clear":
|
|
80
|
+
source = "clear";
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
source = "startup";
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
sessionId: this.extractSessionId(input),
|
|
87
|
+
source,
|
|
88
|
+
projectDir: this.getProjectDir(),
|
|
89
|
+
raw,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// ── Response formatting (shared) ───────────────────────
|
|
93
|
+
formatPreToolUseResponse(response) {
|
|
94
|
+
if (response.decision === "deny") {
|
|
95
|
+
return {
|
|
96
|
+
permissionDecision: "deny",
|
|
97
|
+
reason: response.reason ?? "Blocked by context-mode hook",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (response.decision === "modify" && response.updatedInput) {
|
|
101
|
+
return {
|
|
102
|
+
hookSpecificOutput: {
|
|
103
|
+
hookEventName: this.hookModule.HOOK_TYPES.PRE_TOOL_USE,
|
|
104
|
+
updatedInput: response.updatedInput,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (response.decision === "context" && response.additionalContext) {
|
|
109
|
+
return {
|
|
110
|
+
hookSpecificOutput: {
|
|
111
|
+
hookEventName: this.hookModule.HOOK_TYPES.PRE_TOOL_USE,
|
|
112
|
+
additionalContext: response.additionalContext,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (response.decision === "ask") {
|
|
117
|
+
return {
|
|
118
|
+
permissionDecision: "deny",
|
|
119
|
+
reason: response.reason ?? "Action requires user confirmation (security policy)",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// "allow" — return undefined for passthrough
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
formatPostToolUseResponse(response) {
|
|
126
|
+
if (response.updatedOutput) {
|
|
127
|
+
return {
|
|
128
|
+
hookSpecificOutput: {
|
|
129
|
+
hookEventName: this.hookModule.HOOK_TYPES.POST_TOOL_USE,
|
|
130
|
+
decision: "block",
|
|
131
|
+
reason: response.updatedOutput,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (response.additionalContext) {
|
|
136
|
+
return {
|
|
137
|
+
hookSpecificOutput: {
|
|
138
|
+
hookEventName: this.hookModule.HOOK_TYPES.POST_TOOL_USE,
|
|
139
|
+
additionalContext: response.additionalContext,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
formatPreCompactResponse(response) {
|
|
146
|
+
return response.context ?? "";
|
|
147
|
+
}
|
|
148
|
+
formatSessionStartResponse(response) {
|
|
149
|
+
return response.context ?? "";
|
|
150
|
+
}
|
|
151
|
+
// ── Configuration (shared) ─────────────────────────────
|
|
152
|
+
getSettingsPath() {
|
|
153
|
+
return resolve(".github", "hooks", "context-mode.json");
|
|
154
|
+
}
|
|
155
|
+
generateHookConfig(pluginRoot) {
|
|
156
|
+
const { HOOK_TYPES, buildHookCommand } = this.hookModule;
|
|
157
|
+
return {
|
|
158
|
+
[HOOK_TYPES.PRE_TOOL_USE]: [
|
|
159
|
+
{
|
|
160
|
+
matcher: "",
|
|
161
|
+
hooks: [
|
|
162
|
+
{
|
|
163
|
+
type: "command",
|
|
164
|
+
command: buildHookCommand(HOOK_TYPES.PRE_TOOL_USE, pluginRoot),
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
[HOOK_TYPES.POST_TOOL_USE]: [
|
|
170
|
+
{
|
|
171
|
+
matcher: "",
|
|
172
|
+
hooks: [
|
|
173
|
+
{
|
|
174
|
+
type: "command",
|
|
175
|
+
command: buildHookCommand(HOOK_TYPES.POST_TOOL_USE, pluginRoot),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
[HOOK_TYPES.PRE_COMPACT]: [
|
|
181
|
+
{
|
|
182
|
+
matcher: "",
|
|
183
|
+
hooks: [
|
|
184
|
+
{
|
|
185
|
+
type: "command",
|
|
186
|
+
command: buildHookCommand(HOOK_TYPES.PRE_COMPACT, pluginRoot),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
[HOOK_TYPES.SESSION_START]: [
|
|
192
|
+
{
|
|
193
|
+
matcher: "",
|
|
194
|
+
hooks: [
|
|
195
|
+
{
|
|
196
|
+
type: "command",
|
|
197
|
+
command: buildHookCommand(HOOK_TYPES.SESSION_START, pluginRoot),
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
readSettings() {
|
|
205
|
+
// Primary: .github/hooks/context-mode.json
|
|
206
|
+
try {
|
|
207
|
+
const raw = readFileSync(this.getSettingsPath(), "utf-8");
|
|
208
|
+
return JSON.parse(raw);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
/* fall through */
|
|
212
|
+
}
|
|
213
|
+
// Fallback: .claude/settings.json
|
|
214
|
+
try {
|
|
215
|
+
const raw = readFileSync(resolve(".claude", "settings.json"), "utf-8");
|
|
216
|
+
return JSON.parse(raw);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
writeSettings(settings) {
|
|
223
|
+
const configPath = this.getSettingsPath();
|
|
224
|
+
mkdirSync(resolve(".github", "hooks"), { recursive: true });
|
|
225
|
+
writeFileSync(configPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
226
|
+
}
|
|
227
|
+
// ── Upgrade (shared) ──────────────────────────────────
|
|
228
|
+
configureAllHooks(pluginRoot) {
|
|
229
|
+
const changes = [];
|
|
230
|
+
const settings = this.readSettings() ?? {};
|
|
231
|
+
const hooks = settings.hooks ?? {};
|
|
232
|
+
const { HOOK_TYPES, HOOK_SCRIPTS, buildHookCommand } = this.hookModule;
|
|
233
|
+
const hookTypes = [
|
|
234
|
+
HOOK_TYPES.PRE_TOOL_USE,
|
|
235
|
+
HOOK_TYPES.POST_TOOL_USE,
|
|
236
|
+
HOOK_TYPES.PRE_COMPACT,
|
|
237
|
+
HOOK_TYPES.SESSION_START,
|
|
238
|
+
];
|
|
239
|
+
for (const hookType of hookTypes) {
|
|
240
|
+
const script = HOOK_SCRIPTS[hookType];
|
|
241
|
+
if (!script)
|
|
242
|
+
continue;
|
|
243
|
+
hooks[hookType] = [
|
|
244
|
+
{
|
|
245
|
+
matcher: "",
|
|
246
|
+
hooks: [
|
|
247
|
+
{
|
|
248
|
+
type: "command",
|
|
249
|
+
command: buildHookCommand(hookType, pluginRoot),
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
changes.push(`Configured ${hookType} hook`);
|
|
255
|
+
}
|
|
256
|
+
settings.hooks = hooks;
|
|
257
|
+
this.writeSettings(settings);
|
|
258
|
+
changes.push(`Wrote hook config to ${this.getSettingsPath()}`);
|
|
259
|
+
return changes;
|
|
260
|
+
}
|
|
261
|
+
setHookPermissions(pluginRoot) {
|
|
262
|
+
const set = [];
|
|
263
|
+
const hooksDir = join(pluginRoot, "hooks", this.hookSubdir);
|
|
264
|
+
for (const scriptName of Object.values(this.hookModule.HOOK_SCRIPTS)) {
|
|
265
|
+
const scriptPath = resolve(hooksDir, scriptName);
|
|
266
|
+
try {
|
|
267
|
+
accessSync(scriptPath, constants.R_OK);
|
|
268
|
+
chmodSync(scriptPath, 0o755);
|
|
269
|
+
set.push(scriptPath);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
/* skip missing scripts */
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return set;
|
|
276
|
+
}
|
|
277
|
+
updatePluginRegistry(_pluginRoot, _version) {
|
|
278
|
+
// Copilot platforms manage plugins through their own marketplaces.
|
|
279
|
+
// No manual registry update needed.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Native Cursor hooks use lower-camel hook names and flat command entries in
|
|
5
5
|
* `.cursor/hooks.json` / `~/.cursor/hooks.json`.
|
|
6
6
|
*/
|
|
7
|
+
import { BaseAdapter } from "../base.js";
|
|
7
8
|
import type { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, PreToolUseEvent, PostToolUseEvent, SessionStartEvent, PreToolUseResponse, PostToolUseResponse, SessionStartResponse, HookRegistration } from "../types.js";
|
|
8
9
|
interface StopEvent {
|
|
9
10
|
sessionId: string;
|
|
@@ -12,7 +13,8 @@ interface StopEvent {
|
|
|
12
13
|
generationId?: string;
|
|
13
14
|
transcriptPath?: string;
|
|
14
15
|
}
|
|
15
|
-
export declare class CursorAdapter implements HookAdapter {
|
|
16
|
+
export declare class CursorAdapter extends BaseAdapter implements HookAdapter {
|
|
17
|
+
constructor();
|
|
16
18
|
readonly name = "Cursor";
|
|
17
19
|
readonly paradigm: HookParadigm;
|
|
18
20
|
readonly capabilities: PlatformCapabilities;
|
|
@@ -30,9 +32,6 @@ export declare class CursorAdapter implements HookAdapter {
|
|
|
30
32
|
text: string;
|
|
31
33
|
};
|
|
32
34
|
getSettingsPath(): string;
|
|
33
|
-
getSessionDir(): string;
|
|
34
|
-
getSessionDBPath(projectDir: string): string;
|
|
35
|
-
getSessionEventsPath(projectDir: string): string;
|
|
36
35
|
generateHookConfig(_pluginRoot: string): HookRegistration;
|
|
37
36
|
readSettings(): Record<string, unknown> | null;
|
|
38
37
|
writeSettings(settings: Record<string, unknown>): void;
|
|
@@ -40,7 +39,6 @@ export declare class CursorAdapter implements HookAdapter {
|
|
|
40
39
|
checkPluginRegistration(): DiagnosticResult;
|
|
41
40
|
getInstalledVersion(): string;
|
|
42
41
|
configureAllHooks(_pluginRoot: string): string[];
|
|
43
|
-
backupSettings(): string | null;
|
|
44
42
|
setHookPermissions(pluginRoot: string): string[];
|
|
45
43
|
updatePluginRegistry(_pluginRoot: string, _version: string): void;
|
|
46
44
|
private getCandidateHookConfigPaths;
|
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
* Native Cursor hooks use lower-camel hook names and flat command entries in
|
|
5
5
|
* `.cursor/hooks.json` / `~/.cursor/hooks.json`.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, accessSync, chmodSync, constants, existsSync, } from "node:fs";
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, accessSync, chmodSync, constants, existsSync, } from "node:fs";
|
|
9
8
|
import { execSync } from "node:child_process";
|
|
10
9
|
import { resolve, join } from "node:path";
|
|
11
10
|
import { homedir } from "node:os";
|
|
11
|
+
import { BaseAdapter } from "../base.js";
|
|
12
12
|
import { HOOK_TYPES as CURSOR_HOOK_NAMES, HOOK_SCRIPTS as CURSOR_HOOK_SCRIPTS, PRE_TOOL_USE_MATCHER_PATTERN, REQUIRED_HOOKS, OPTIONAL_HOOKS, isContextModeHook, buildHookCommand, } from "./hooks.js";
|
|
13
13
|
const CURSOR_ENTERPRISE_HOOKS_PATH = "/Library/Application Support/Cursor/hooks.json";
|
|
14
|
-
export class CursorAdapter {
|
|
14
|
+
export class CursorAdapter extends BaseAdapter {
|
|
15
|
+
constructor() {
|
|
16
|
+
super([".cursor"]);
|
|
17
|
+
}
|
|
15
18
|
name = "Cursor";
|
|
16
19
|
paradigm = "json-stdio";
|
|
17
20
|
capabilities = {
|
|
@@ -125,25 +128,6 @@ export class CursorAdapter {
|
|
|
125
128
|
getSettingsPath() {
|
|
126
129
|
return resolve(".cursor", "hooks.json");
|
|
127
130
|
}
|
|
128
|
-
getSessionDir() {
|
|
129
|
-
const dir = join(homedir(), ".cursor", "context-mode", "sessions");
|
|
130
|
-
mkdirSync(dir, { recursive: true });
|
|
131
|
-
return dir;
|
|
132
|
-
}
|
|
133
|
-
getSessionDBPath(projectDir) {
|
|
134
|
-
const hash = createHash("sha256")
|
|
135
|
-
.update(projectDir)
|
|
136
|
-
.digest("hex")
|
|
137
|
-
.slice(0, 16);
|
|
138
|
-
return join(this.getSessionDir(), `${hash}.db`);
|
|
139
|
-
}
|
|
140
|
-
getSessionEventsPath(projectDir) {
|
|
141
|
-
const hash = createHash("sha256")
|
|
142
|
-
.update(projectDir)
|
|
143
|
-
.digest("hex")
|
|
144
|
-
.slice(0, 16);
|
|
145
|
-
return join(this.getSessionDir(), `${hash}-events.md`);
|
|
146
|
-
}
|
|
147
131
|
generateHookConfig(_pluginRoot) {
|
|
148
132
|
const hooks = {
|
|
149
133
|
[CURSOR_HOOK_NAMES.PRE_TOOL_USE]: [
|
|
@@ -355,18 +339,6 @@ export class CursorAdapter {
|
|
|
355
339
|
changes.push(`Wrote native Cursor hooks to ${this.getSettingsPath()}`);
|
|
356
340
|
return changes;
|
|
357
341
|
}
|
|
358
|
-
backupSettings() {
|
|
359
|
-
const settingsPath = this.getSettingsPath();
|
|
360
|
-
try {
|
|
361
|
-
accessSync(settingsPath, constants.R_OK);
|
|
362
|
-
const backupPath = settingsPath + ".bak";
|
|
363
|
-
copyFileSync(settingsPath, backupPath);
|
|
364
|
-
return backupPath;
|
|
365
|
-
}
|
|
366
|
-
catch {
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
342
|
setHookPermissions(pluginRoot) {
|
|
371
343
|
const set = [];
|
|
372
344
|
const hooksDir = join(pluginRoot, "hooks", "cursor");
|
|
@@ -15,8 +15,15 @@
|
|
|
15
15
|
* - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
|
|
16
16
|
* - Cursor: CURSOR_TRACE_ID (MCP), CURSOR_CLI (terminal) | ~/.cursor/
|
|
17
17
|
* - VS Code Copilot: VSCODE_PID, VSCODE_CWD | ~/.vscode/
|
|
18
|
+
* - JetBrains Copilot: IDEA_INITIAL_DIRECTORY, IDEA_HOME, JETBRAINS_CLIENT_ID | ~/.config/JetBrains/
|
|
18
19
|
*/
|
|
19
20
|
import type { PlatformId, DetectionSignal, HookAdapter } from "./types.js";
|
|
21
|
+
/**
|
|
22
|
+
* High-confidence env vars per platform, checked in priority order.
|
|
23
|
+
* Single source of truth — consumed by detectPlatform() below and by
|
|
24
|
+
* tests that need to clear platform-related env vars deterministically.
|
|
25
|
+
*/
|
|
26
|
+
export declare const PLATFORM_ENV_VARS: readonly [readonly ["claude-code", readonly ["CLAUDE_PROJECT_DIR", "CLAUDE_SESSION_ID"]], readonly ["gemini-cli", readonly ["GEMINI_PROJECT_DIR", "GEMINI_CLI"]], readonly ["openclaw", readonly ["OPENCLAW_HOME", "OPENCLAW_CLI"]], readonly ["kilo", readonly ["KILO", "KILO_PID"]], readonly ["opencode", readonly ["OPENCODE", "OPENCODE_PID"]], readonly ["codex", readonly ["CODEX_CI", "CODEX_THREAD_ID"]], readonly ["cursor", readonly ["CURSOR_TRACE_ID", "CURSOR_CLI"]], readonly ["vscode-copilot", readonly ["VSCODE_PID", "VSCODE_CWD"]], readonly ["jetbrains-copilot", readonly ["IDEA_INITIAL_DIRECTORY", "IDEA_HOME", "JETBRAINS_CLIENT_ID"]], readonly ["qwen-code", readonly ["QWEN_PROJECT_DIR", "QWEN_SESSION_ID"]]];
|
|
20
27
|
/**
|
|
21
28
|
* Detect the current platform by checking env vars and config dirs.
|
|
22
29
|
*
|
package/build/adapters/detect.js
CHANGED
|
@@ -15,11 +15,29 @@
|
|
|
15
15
|
* - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
|
|
16
16
|
* - Cursor: CURSOR_TRACE_ID (MCP), CURSOR_CLI (terminal) | ~/.cursor/
|
|
17
17
|
* - VS Code Copilot: VSCODE_PID, VSCODE_CWD | ~/.vscode/
|
|
18
|
+
* - JetBrains Copilot: IDEA_INITIAL_DIRECTORY, IDEA_HOME, JETBRAINS_CLIENT_ID | ~/.config/JetBrains/
|
|
18
19
|
*/
|
|
19
20
|
import { existsSync } from "node:fs";
|
|
20
21
|
import { resolve } from "node:path";
|
|
21
22
|
import { homedir } from "node:os";
|
|
22
23
|
import { CLIENT_NAME_TO_PLATFORM } from "./client-map.js";
|
|
24
|
+
/**
|
|
25
|
+
* High-confidence env vars per platform, checked in priority order.
|
|
26
|
+
* Single source of truth — consumed by detectPlatform() below and by
|
|
27
|
+
* tests that need to clear platform-related env vars deterministically.
|
|
28
|
+
*/
|
|
29
|
+
export const PLATFORM_ENV_VARS = [
|
|
30
|
+
["claude-code", ["CLAUDE_PROJECT_DIR", "CLAUDE_SESSION_ID"]],
|
|
31
|
+
["gemini-cli", ["GEMINI_PROJECT_DIR", "GEMINI_CLI"]],
|
|
32
|
+
["openclaw", ["OPENCLAW_HOME", "OPENCLAW_CLI"]],
|
|
33
|
+
["kilo", ["KILO", "KILO_PID"]],
|
|
34
|
+
["opencode", ["OPENCODE", "OPENCODE_PID"]],
|
|
35
|
+
["codex", ["CODEX_CI", "CODEX_THREAD_ID"]],
|
|
36
|
+
["cursor", ["CURSOR_TRACE_ID", "CURSOR_CLI"]],
|
|
37
|
+
["vscode-copilot", ["VSCODE_PID", "VSCODE_CWD"]],
|
|
38
|
+
["jetbrains-copilot", ["IDEA_INITIAL_DIRECTORY", "IDEA_HOME", "JETBRAINS_CLIENT_ID"]],
|
|
39
|
+
["qwen-code", ["QWEN_PROJECT_DIR", "QWEN_SESSION_ID"]],
|
|
40
|
+
];
|
|
23
41
|
/**
|
|
24
42
|
* Detect the current platform by checking env vars and config dirs.
|
|
25
43
|
*
|
|
@@ -37,13 +55,21 @@ export function detectPlatform(clientInfo) {
|
|
|
37
55
|
reason: `MCP clientInfo.name="${clientInfo.name}"`,
|
|
38
56
|
};
|
|
39
57
|
}
|
|
58
|
+
// Qwen Code uses dynamic client names: qwen-cli-mcp-client-<serverName>
|
|
59
|
+
if (clientInfo.name.startsWith("qwen-cli-mcp-client")) {
|
|
60
|
+
return {
|
|
61
|
+
platform: "qwen-code",
|
|
62
|
+
confidence: "high",
|
|
63
|
+
reason: `MCP clientInfo.name="${clientInfo.name}" (qwen-cli pattern)`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
40
66
|
}
|
|
41
67
|
// ── Explicit platform override ────────────────────────
|
|
42
68
|
const platformOverride = process.env.CONTEXT_MODE_PLATFORM;
|
|
43
69
|
if (platformOverride) {
|
|
44
70
|
const validPlatforms = [
|
|
45
71
|
"claude-code", "gemini-cli", "kilo", "opencode", "codex",
|
|
46
|
-
"vscode-copilot", "cursor", "antigravity", "kiro", "pi", "zed",
|
|
72
|
+
"vscode-copilot", "jetbrains-copilot", "cursor", "antigravity", "kiro", "pi", "zed", "qwen-code",
|
|
47
73
|
];
|
|
48
74
|
if (validPlatforms.includes(platformOverride)) {
|
|
49
75
|
return {
|
|
@@ -54,61 +80,14 @@ export function detectPlatform(clientInfo) {
|
|
|
54
80
|
}
|
|
55
81
|
}
|
|
56
82
|
// ── High confidence: environment variables ─────────────
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
platform: "gemini-cli",
|
|
67
|
-
confidence: "high",
|
|
68
|
-
reason: "GEMINI_PROJECT_DIR or GEMINI_CLI env var set",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
if (process.env.OPENCLAW_HOME || process.env.OPENCLAW_CLI) {
|
|
72
|
-
return {
|
|
73
|
-
platform: "openclaw",
|
|
74
|
-
confidence: "high",
|
|
75
|
-
reason: "OPENCLAW_HOME or OPENCLAW_CLI env var set",
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
if (process.env.KILO || process.env.KILO_PID) {
|
|
79
|
-
return {
|
|
80
|
-
platform: "kilo",
|
|
81
|
-
confidence: "high",
|
|
82
|
-
reason: "KILO or KILO_PID env var set",
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
if (process.env.OPENCODE || process.env.OPENCODE_PID) {
|
|
86
|
-
return {
|
|
87
|
-
platform: "opencode",
|
|
88
|
-
confidence: "high",
|
|
89
|
-
reason: "OPENCODE or OPENCODE_PID env var set",
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
if (process.env.CODEX_CI || process.env.CODEX_THREAD_ID) {
|
|
93
|
-
return {
|
|
94
|
-
platform: "codex",
|
|
95
|
-
confidence: "high",
|
|
96
|
-
reason: "CODEX_CI or CODEX_THREAD_ID env var set",
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
if (process.env.CURSOR_TRACE_ID || process.env.CURSOR_CLI) {
|
|
100
|
-
return {
|
|
101
|
-
platform: "cursor",
|
|
102
|
-
confidence: "high",
|
|
103
|
-
reason: "CURSOR_TRACE_ID or CURSOR_CLI env var set",
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
if (process.env.VSCODE_PID || process.env.VSCODE_CWD) {
|
|
107
|
-
return {
|
|
108
|
-
platform: "vscode-copilot",
|
|
109
|
-
confidence: "high",
|
|
110
|
-
reason: "VSCODE_PID or VSCODE_CWD env var set",
|
|
111
|
-
};
|
|
83
|
+
for (const [platform, vars] of PLATFORM_ENV_VARS) {
|
|
84
|
+
if (vars.some((v) => process.env[v])) {
|
|
85
|
+
return {
|
|
86
|
+
platform,
|
|
87
|
+
confidence: "high",
|
|
88
|
+
reason: `${vars.join(" or ")} env var set`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
112
91
|
}
|
|
113
92
|
// ── Medium confidence: config directory existence ──────
|
|
114
93
|
const home = homedir();
|
|
@@ -154,6 +133,13 @@ export function detectPlatform(clientInfo) {
|
|
|
154
133
|
reason: "~/.pi/ directory exists",
|
|
155
134
|
};
|
|
156
135
|
}
|
|
136
|
+
if (existsSync(resolve(home, ".qwen"))) {
|
|
137
|
+
return {
|
|
138
|
+
platform: "qwen-code",
|
|
139
|
+
confidence: "medium",
|
|
140
|
+
reason: "~/.qwen/ directory exists",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
157
143
|
if (existsSync(resolve(home, ".openclaw"))) {
|
|
158
144
|
return {
|
|
159
145
|
platform: "openclaw",
|
|
@@ -168,6 +154,13 @@ export function detectPlatform(clientInfo) {
|
|
|
168
154
|
reason: "~/.config/kilo/ directory exists",
|
|
169
155
|
};
|
|
170
156
|
}
|
|
157
|
+
if (existsSync(resolve(home, ".config", "JetBrains"))) {
|
|
158
|
+
return {
|
|
159
|
+
platform: "jetbrains-copilot",
|
|
160
|
+
confidence: "medium",
|
|
161
|
+
reason: "~/.config/JetBrains/ directory exists",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
171
164
|
if (existsSync(resolve(home, ".config", "opencode"))) {
|
|
172
165
|
return {
|
|
173
166
|
platform: "opencode",
|
|
@@ -221,6 +214,10 @@ export async function getAdapter(platform) {
|
|
|
221
214
|
const { VSCodeCopilotAdapter } = await import("./vscode-copilot/index.js");
|
|
222
215
|
return new VSCodeCopilotAdapter();
|
|
223
216
|
}
|
|
217
|
+
case "jetbrains-copilot": {
|
|
218
|
+
const { JetBrainsCopilotAdapter } = await import("./jetbrains-copilot/index.js");
|
|
219
|
+
return new JetBrainsCopilotAdapter();
|
|
220
|
+
}
|
|
224
221
|
case "cursor": {
|
|
225
222
|
const { CursorAdapter } = await import("./cursor/index.js");
|
|
226
223
|
return new CursorAdapter();
|
|
@@ -237,6 +234,10 @@ export async function getAdapter(platform) {
|
|
|
237
234
|
const { ZedAdapter } = await import("./zed/index.js");
|
|
238
235
|
return new ZedAdapter();
|
|
239
236
|
}
|
|
237
|
+
case "qwen-code": {
|
|
238
|
+
const { QwenCodeAdapter } = await import("./qwen-code/index.js");
|
|
239
|
+
return new QwenCodeAdapter();
|
|
240
|
+
}
|
|
240
241
|
default: {
|
|
241
242
|
// Unsupported platform — fall back to Claude Code adapter
|
|
242
243
|
// (MCP server works everywhere, hooks may not)
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
* - Project dir env: GEMINI_PROJECT_DIR (also CLAUDE_PROJECT_DIR alias)
|
|
19
19
|
* - Session dir: ~/.gemini/context-mode/sessions/
|
|
20
20
|
*/
|
|
21
|
+
import { BaseAdapter } from "../base.js";
|
|
21
22
|
import type { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, PreToolUseEvent, PostToolUseEvent, PreCompactEvent, SessionStartEvent, PreToolUseResponse, PostToolUseResponse, PreCompactResponse, SessionStartResponse, HookRegistration } from "../types.js";
|
|
22
|
-
export declare class GeminiCLIAdapter implements HookAdapter {
|
|
23
|
+
export declare class GeminiCLIAdapter extends BaseAdapter implements HookAdapter {
|
|
24
|
+
constructor();
|
|
23
25
|
readonly name = "Gemini CLI";
|
|
24
26
|
readonly paradigm: HookParadigm;
|
|
25
27
|
readonly capabilities: PlatformCapabilities;
|
|
@@ -32,9 +34,6 @@ export declare class GeminiCLIAdapter implements HookAdapter {
|
|
|
32
34
|
formatPreCompactResponse(response: PreCompactResponse): unknown;
|
|
33
35
|
formatSessionStartResponse(response: SessionStartResponse): unknown;
|
|
34
36
|
getSettingsPath(): string;
|
|
35
|
-
getSessionDir(): string;
|
|
36
|
-
getSessionDBPath(projectDir: string): string;
|
|
37
|
-
getSessionEventsPath(projectDir: string): string;
|
|
38
37
|
generateHookConfig(pluginRoot: string): HookRegistration;
|
|
39
38
|
readSettings(): Record<string, unknown> | null;
|
|
40
39
|
writeSettings(settings: Record<string, unknown>): void;
|
|
@@ -42,7 +41,6 @@ export declare class GeminiCLIAdapter implements HookAdapter {
|
|
|
42
41
|
checkPluginRegistration(): DiagnosticResult;
|
|
43
42
|
getInstalledVersion(): string;
|
|
44
43
|
configureAllHooks(pluginRoot: string): string[];
|
|
45
|
-
backupSettings(): string | null;
|
|
46
44
|
setHookPermissions(pluginRoot: string): string[];
|
|
47
45
|
updatePluginRegistry(pluginRoot: string, version: string): void;
|
|
48
46
|
/** Get the project directory from environment variables. */
|