Skip to content

Commit cc7e342

Browse files
committed
fix: platform-aware session hooks and auto-memory for Qwen Code
# Conflicts: # cli.bundle.mjs # server.bundle.mjs
1 parent 0501887 commit cc7e342

8 files changed

Lines changed: 272 additions & 227 deletions

File tree

cli.bundle.mjs

Lines changed: 107 additions & 107 deletions
Large diffs are not rendered by default.

hooks/posttooluse.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,29 @@ import "./ensure-deps.mjs";
1010
* Must be fast (<20ms). No network, no LLM, just SQLite writes.
1111
*/
1212

13-
import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir } from "./session-helpers.mjs";
13+
import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, detectPlatform } from "./session-helpers.mjs";
1414
import { createSessionLoaders, attributeAndInsertEvents } from "./session-loaders.mjs";
1515
import { dirname, resolve } from "node:path";
1616
import { fileURLToPath } from "node:url";
1717
import { readFileSync, unlinkSync } from "node:fs";
1818
import { tmpdir } from "node:os";
1919

2020
// Resolve absolute path for imports — relative dynamic imports can fail
21-
// when Claude Code invokes hooks from a different working directory.
21+
// when the host IDE invokes hooks from a different working directory.
2222
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
2323
const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
24+
const platformOpts = detectPlatform();
2425

2526
try {
2627
const raw = await readStdin();
2728
const input = parseStdin(raw);
28-
const projectDir = getInputProjectDir(input);
29+
const projectDir = getInputProjectDir(input, platformOpts);
2930

3031
const { extractEvents } = await loadExtract();
3132
const { resolveProjectAttributions } = await loadProjectAttribution();
3233
const { SessionDB } = await loadSessionDB();
3334

34-
const dbPath = getSessionDBPath();
35+
const dbPath = getSessionDBPath(platformOpts);
3536
const db = new SessionDB({ dbPath });
3637
const sessionId = getSessionId(input);
3738

hooks/precompact.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import "./ensure-deps.mjs";
99
* snapshot (<2KB XML), and stores it for injection after compact.
1010
*/
1111

12-
import { readStdin, parseStdin, getSessionId, getSessionDBPath, resolveConfigDir } from "./session-helpers.mjs";
12+
import { readStdin, parseStdin, getSessionId, getSessionDBPath, resolveConfigDir, detectPlatform } from "./session-helpers.mjs";
1313
import { createSessionLoaders } from "./session-loaders.mjs";
1414
import { appendFileSync } from "node:fs";
1515
import { join, dirname } from "node:path";
@@ -18,7 +18,8 @@ import { fileURLToPath } from "node:url";
1818
// Resolve absolute path for imports
1919
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
2020
const { loadSessionDB, loadSnapshot } = createSessionLoaders(HOOK_DIR);
21-
const DEBUG_LOG = join(resolveConfigDir(), "context-mode", "precompact-debug.log");
21+
const platformOpts = detectPlatform();
22+
const DEBUG_LOG = join(resolveConfigDir(platformOpts), "context-mode", "precompact-debug.log");
2223

2324
try {
2425
const raw = await readStdin();
@@ -27,7 +28,7 @@ try {
2728
const { buildResumeSnapshot } = await loadSnapshot();
2829
const { SessionDB } = await loadSessionDB();
2930

30-
const dbPath = getSessionDBPath();
31+
const dbPath = getSessionDBPath(platformOpts);
3132
const db = new SessionDB({ dbPath });
3233
const sessionId = getSessionId(input);
3334

hooks/session-helpers.mjs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ const CLAUDE_OPTS = {
5151
sessionIdEnv: "CLAUDE_SESSION_ID",
5252
};
5353

54+
/** Qwen Code platform options. */
55+
export const QWEN_OPTS = {
56+
configDir: ".qwen",
57+
configDirEnv: undefined, // Qwen Code has no config dir env var
58+
projectDirEnv: "QWEN_PROJECT_DIR",
59+
sessionIdEnv: "QWEN_SESSION_ID",
60+
};
61+
5462
/** Gemini CLI platform options. */
5563
export const GEMINI_OPTS = {
5664
configDir: ".gemini",
@@ -99,6 +107,24 @@ export const JETBRAINS_OPTS = {
99107
sessionIdEnv: undefined,
100108
};
101109

110+
/**
111+
* Auto-detect the running platform from environment variables.
112+
* Checks platform-specific env vars in priority order.
113+
* Falls back to CLAUDE_OPTS when no platform is detected.
114+
*
115+
* @returns {object} Platform options { configDir, configDirEnv, projectDirEnv, sessionIdEnv }
116+
*/
117+
export function detectPlatform() {
118+
if (process.env.QWEN_PROJECT_DIR || process.env.QWEN_SESSION_ID) return QWEN_OPTS;
119+
if (process.env.GEMINI_PROJECT_DIR) return GEMINI_OPTS;
120+
if (process.env.VSCODE_CWD) return VSCODE_OPTS;
121+
if (process.env.CURSOR_CWD) return CURSOR_OPTS;
122+
if (process.env.CODEX_HOME) return CODEX_OPTS;
123+
if (process.env.IDEA_INITIAL_DIRECTORY) return JETBRAINS_OPTS;
124+
// Kiro has no unique env var — detected via tool_name prefix in hook stdin
125+
return CLAUDE_OPTS;
126+
}
127+
102128
/**
103129
* Resolve the platform config directory, respecting env var overrides.
104130
* Platforms like Claude Code (CLAUDE_CONFIG_DIR), Gemini CLI (GEMINI_CLI_HOME),

hooks/sessionstart.mjs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ import { createRoutingBlock } from "./routing-block.mjs";
1919
import { createToolNamer } from "./core/tool-naming.mjs";
2020
import { buildAutoInjection } from "./auto-injection.mjs";
2121

22-
const toolNamer = createToolNamer("claude-code");
22+
// Auto-detect platform for correct env vars and config paths
23+
const platformOpts = detectPlatform();
24+
const platformId = platformOpts.configDir === ".qwen" ? "qwen-code" : "claude-code";
25+
const toolNamer = createToolNamer(platformId);
2326
const ROUTING_BLOCK = createRoutingBlock(toolNamer);
24-
import { readStdin, parseStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath, resolveConfigDir } from "./session-helpers.mjs";
27+
import { readStdin, parseStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath, resolveConfigDir, detectPlatform } from "./session-helpers.mjs";
2528
import { writeSessionEventsFile, buildSessionDirective, getSessionEvents, getLatestSessionEvents } from "./session-directive.mjs";
2629
import { createSessionLoaders } from "./session-loaders.mjs";
2730
import { join, dirname } from "node:path";
@@ -102,18 +105,21 @@ try {
102105
db.cleanupOldSessions(7);
103106
db.db.exec(`DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)`);
104107

105-
// Proactively capture CLAUDE.md files — Claude Code loads them as system
108+
// Proactively capture CLAUDE.md / QWEN.md files — the host IDE loads them as system
106109
// context at startup, invisible to PostToolUse hooks. We read them from
107110
// disk so they survive compact/resume via the session events pipeline.
108111
const sessionId = getSessionId(input);
109-
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
112+
const projectDir = process.env[platformOpts.projectDirEnv] || process.cwd();
110113
db.ensureSession(sessionId, projectDir);
111-
const claudeMdPaths = [
112-
join(resolveConfigDir(), "CLAUDE.md"),
113-
join(projectDir, "CLAUDE.md"),
114-
join(projectDir, ".claude", "CLAUDE.md"),
114+
const memoryFileNames = platformOpts.configDir === ".qwen"
115+
? ["QWEN.md"]
116+
: ["CLAUDE.md"];
117+
const memoryMdPaths = [
118+
join(resolveConfigDir(platformOpts), memoryFileNames[0]),
119+
join(projectDir, memoryFileNames[0]),
120+
join(projectDir, platformOpts.configDir, memoryFileNames[0]),
115121
];
116-
for (const p of claudeMdPaths) {
122+
for (const p of memoryMdPaths) {
117123
try {
118124
const content = readFileSync(p, "utf-8");
119125
if (content.trim()) {
@@ -128,7 +134,10 @@ try {
128134
// Age-gated lazy cleanup of old plugin cache version dirs (#181).
129135
// Only delete dirs older than 1 hour to avoid breaking active sessions.
130136
try {
131-
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
137+
const pluginRootEnv = platformOpts.configDir === ".qwen"
138+
? "QWEN_PLUGIN_ROOT"
139+
: "CLAUDE_PLUGIN_ROOT";
140+
const pluginRoot = process.env[pluginRootEnv];
132141
if (pluginRoot) {
133142
const cacheParentMatch = pluginRoot.match(/^(.*[\\/]plugins[\\/]cache[\\/][^\\/]+[\\/][^\\/]+[\\/])/);
134143
if (cacheParentMatch) {

server.bundle.mjs

Lines changed: 90 additions & 90 deletions
Large diffs are not rendered by default.

src/search/auto-memory.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,44 +20,50 @@ export interface AutoMemoryResult {
2020
}
2121

2222
/**
23-
* Search auto-memory files (CLAUDE.md, MEMORY.md, user identity files)
23+
* Search auto-memory files (CLAUDE.md, QWEN.md, MEMORY.md, user identity files)
2424
* for content matching any of the given queries.
2525
*
2626
* Scans:
27-
* 1. Project-level: <projectDir>/CLAUDE.md
28-
* 2. User-level: <configDir>/CLAUDE.md
27+
* 1. Project-level: <projectDir>/CLAUDE.md, <projectDir>/QWEN.md
28+
* 2. User-level: <configDir>/CLAUDE.md, <configDir>/QWEN.md
2929
* 3. User memory: <configDir>/memory/*.md
3030
*
3131
* @param queries Array of search terms
3232
* @param limit Max results to return
3333
* @param projectDir Project directory path
34-
* @param configDir Config directory (e.g. ~/.claude)
34+
* @param configDir Config directory (e.g. ~/.claude, ~/.qwen)
35+
* @param memoryFileNames Memory file names to scan (default: ["CLAUDE.md", "QWEN.md"])
3536
* @returns Matching auto-memory results
3637
*/
3738
export function searchAutoMemory(
3839
queries: string[],
3940
limit: number = 5,
4041
projectDir?: string,
4142
configDir?: string,
43+
memoryFileNames: string[] = ["CLAUDE.md", "QWEN.md"],
4244
): AutoMemoryResult[] {
4345
const results: AutoMemoryResult[] = [];
4446
const effectiveConfigDir = configDir || join(homedir(), ".claude");
4547

4648
// Collect candidate files
4749
const candidates: Array<{ path: string; label: string }> = [];
4850

49-
// 1. Project-level CLAUDE.md
51+
// 1. Project-level memory files
5052
if (projectDir) {
51-
const projectClaude = join(projectDir, "CLAUDE.md");
52-
if (existsSync(projectClaude)) {
53-
candidates.push({ path: projectClaude, label: "project/CLAUDE.md" });
53+
for (const name of memoryFileNames) {
54+
const projectMemory = join(projectDir, name);
55+
if (existsSync(projectMemory)) {
56+
candidates.push({ path: projectMemory, label: `project/${name}` });
57+
}
5458
}
5559
}
5660

57-
// 2. User-level CLAUDE.md
58-
const userClaude = join(effectiveConfigDir, "CLAUDE.md");
59-
if (existsSync(userClaude)) {
60-
candidates.push({ path: userClaude, label: "user/CLAUDE.md" });
61+
// 2. User-level memory files
62+
for (const name of memoryFileNames) {
63+
const userMemory = join(effectiveConfigDir, name);
64+
if (existsSync(userMemory)) {
65+
candidates.push({ path: userMemory, label: `user/${name}` });
66+
}
6167
}
6268

6369
// 3. User memory directory

src/server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1337,7 +1337,9 @@ server.registerTool(
13371337
} catch { /* SessionDB unavailable — search ContentStore + auto-memory only */ }
13381338
}
13391339

1340-
const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
1340+
const configDir = _detectedAdapter
1341+
? dirname(_detectedAdapter.getSettingsPath())
1342+
: (process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"));
13411343

13421344
try {
13431345
for (const q of queryList) {

0 commit comments

Comments
 (0)