Skip to content

Commit 32ecf80

Browse files
committed
fix: improve Windows plugin sync handling
Jieli-Thread: http://127.0.0.1:8080/threads/T-019ec058-99fd-75e0-b0fa-104e33f49a5c
1 parent 7bbf19e commit 32ecf80

6 files changed

Lines changed: 131 additions & 29 deletions

File tree

plugins/claude-code/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "jieli",
33
"displayName": "Jieli",
4-
"version": "0.1.15",
4+
"version": "0.1.16",
55
"description": "Sync Claude Code sessions to Jieli threads and attach Jieli thread trailers to Claude-created commits.",
66
"author": {
77
"name": "Jieli"

plugins/claude-code/scripts/jieli_node.mjs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,13 @@ const MODEL_ALIAS_ENV_NAMES = [
3333
"ANTHROPIC_DEFAULT_SONNET_MODEL",
3434
"ANTHROPIC_DEFAULT_OPUS_MODEL",
3535
];
36-
const COMPACTION_PLACEHOLDER =
37-
"[Context compacted - earlier conversation summarized to continue past the context window]";
36+
const COMPACTION_PLACEHOLDER = "[Context automatically compacted]";
37+
const CLAUDE_CONTINUED_SESSION_RE =
38+
/^This session is being continued from a previous conversation that ran out of context\./i;
39+
const CODEX_AUTO_COMPACTION_PREAMBLE_RE =
40+
/^Another language model started to solve this problem and produced a summary of its thinking process\./i;
41+
const AUTO_COMPACTION_CURRENT_PROGRESS_RE = /^Current progress:\s*(?:\n|$)/i;
42+
const AUTO_COMPACTION_SECTION_RE = /^(?:Important context(?: and constraints)?|What remains to do|Relevant files|Critical examples):\s*$/im;
3843
const BASH_NO_OUTPUT_MARKERS = new Set(["", "(Bash completed with no output)"]);
3944
const SUPPORTED_IMAGE_MEDIA_TYPES = new Map([
4045
[".png", "image/png"],
@@ -420,6 +425,9 @@ async function parseTranscript(path, fallbackSessionId = "", imageUploader = nul
420425
role = normalizedRole(message.role || entry.type, content);
421426
content = normalizeLocalCommandMessage(role, content);
422427
if (content == null || isLoadedSkillBodyMessage(role, content)) continue;
428+
if (role === "user" && isAutoCompactionSummaryText(textFromNormalizedContent(content))) {
429+
content = COMPACTION_PLACEHOLDER;
430+
}
423431
}
424432

425433
const bash = role === "user" && typeof content === "string" ? parseBashBlock(content) : null;
@@ -645,6 +653,13 @@ function isLoadedSkillBodyMessage(role, content) {
645653
return role === "user" && textFromNormalizedContent(content).trimStart().startsWith("Base directory for this skill:");
646654
}
647655

656+
function isAutoCompactionSummaryText(text) {
657+
const trimmed = text.trimStart();
658+
if (CLAUDE_CONTINUED_SESSION_RE.test(trimmed)) return true;
659+
if (CODEX_AUTO_COMPACTION_PREAMBLE_RE.test(trimmed)) return true;
660+
return AUTO_COMPACTION_CURRENT_PROGRESS_RE.test(trimmed) && AUTO_COMPACTION_SECTION_RE.test(trimmed);
661+
}
662+
648663
function textFromNormalizedContent(content) {
649664
if (typeof content === "string") return content;
650665
if (Array.isArray(content)) return content.map((block) => (block && typeof block.text === "string" ? block.text : "")).filter(Boolean).join("\n\n");
@@ -932,19 +947,27 @@ function updatedHandoffCommand(command, hookData) {
932947
cwd: String(hookData.cwd || ""),
933948
};
934949
const encoded = Buffer.from(JSON.stringify(context), "utf8").toString("base64");
935-
return `${HANDOFF_CONTEXT_ENV}=${quoteShell(encoded)} ${helper}`;
950+
return `${helper} --context-b64 ${quoteShell(encoded)}`;
936951
}
937952

938953
function resolvedHandoffHelperCommand(command) {
939954
if (command.includes(HANDOFF_CONTEXT_ENV) || AMBIGUOUS_TOKENS.some((token) => command.includes(token))) return "";
940955
const parts = shellSplit(command);
941956
if (!parts) return "";
942-
if (parts.length === 1 && basename(parts[0]) === HANDOFF_HELPER_COMMAND) {
957+
if ((parts.length === 1 && isHandoffHelper(parts[0])) || (parts.length === 2 && parts[0] === "&" && isHandoffHelper(parts[1]))) {
943958
return `node ${quoteShell(join(pluginRoot, "scripts", "jieli_node.mjs"))} handoff-info`;
944959
}
945960
return "";
946961
}
947962

963+
function isHandoffHelper(part) {
964+
const normalized = String(part || "").replace(/\\/g, "/").toLowerCase();
965+
const name = normalized.split("/").pop();
966+
return [HANDOFF_HELPER_COMMAND, `${HANDOFF_HELPER_COMMAND}.cmd`, `${HANDOFF_HELPER_COMMAND}.exe`].some(
967+
(helper) => name === helper || normalized.endsWith(`/bin/${helper}`) || normalized.endsWith(`bin${helper}`),
968+
);
969+
}
970+
948971
function updatedCommitCommand(command, sessionId) {
949972
if (!command || !sessionId || AMBIGUOUS_TOKENS.some((token) => command.includes(token))) return "";
950973
const mapping = readJson(join(homeDir(), ".jieli", "claude-sessions.json"), {});
@@ -1236,15 +1259,17 @@ function formatThreadsMarkdown(payload, baseUrl) {
12361259

12371260
function handoffInfoMain(args = []) {
12381261
if (args.includes("--help") || args.includes("-h")) {
1239-
console.log("usage: jieli-handoff-info");
1262+
console.log("usage: jieli-handoff-info [--context-b64 CONTEXT]");
12401263
return 0;
12411264
}
1242-
console.log(JSON.stringify(buildHandoffInfo(), Object.keys(buildHandoffInfo()).sort()));
1265+
const opts = parseArgs(args);
1266+
const info = buildHandoffInfo(process.env, opts.contextB64 || "");
1267+
console.log(JSON.stringify(info, Object.keys(info).sort()));
12431268
return 0;
12441269
}
12451270

1246-
function buildHandoffInfo(env = process.env) {
1247-
const context = decodeContext(env[HANDOFF_CONTEXT_ENV] || "");
1271+
function buildHandoffInfo(env = process.env, contextB64 = "") {
1272+
const context = decodeContext(contextB64 || env[HANDOFF_CONTEXT_ENV] || "");
12481273
if (!context) return missingInfo("missing hook context");
12491274
const sessionId = String(context.session_id || "").trim();
12501275
if (!sessionId) return missingInfo("missing session_id in hook context");

plugins/claude-code/tests/runtime-node.test.mjs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -362,10 +362,17 @@ test("replaces compaction summaries and renders Claude bash transcript tags", as
362362
const tmp = makeTempDir();
363363
const fakeKey = `jieli_${"a".repeat(30)}`;
364364
const prose = "我用 `!rm -rf x` 执行了:<bash-input>rm -rf x</bash-input><bash-stdout>(Bash completed with no output)</bash-stdout> 你看下";
365+
const unflaggedCompactSummary =
366+
"This session is being continued from a previous conversation that ran out of context. " +
367+
"The conversation is summarized below:\n\n" +
368+
"Current progress:\n\n" +
369+
"- Repo: `/Users/alice/work/jieli`.\n" +
370+
"- This Claude compacted implementation detail should not be uploaded.\n".repeat(20);
365371
const transcript = join(tmp, "session.jsonl");
366372
writeJsonl(transcript, [
367373
{ type: "user", uuid: "u-real", sessionId: "cc-bash", cwd: "/Users/alice/work/jieli", message: { role: "user", content: "原始第一条消息" } },
368374
{ type: "user", uuid: "u-compact", sessionId: "cc-bash", isCompactSummary: true, isVisibleInTranscriptOnly: true, message: { role: "user", content: "This session is being continued. " + "x".repeat(5000) } },
375+
{ type: "user", uuid: "u-compact-text", sessionId: "cc-bash", message: { role: "user", content: unflaggedCompactSummary } },
369376
{ type: "user", uuid: "u-bash-in", sessionId: "cc-bash", cwd: "/Users/alice/work/jieli", message: { role: "user", content: "<bash-input>echo $JIELI_API_KEY</bash-input>" } },
370377
{ type: "user", uuid: "u-bash-out", sessionId: "cc-bash", message: { role: "user", content: `<bash-stdout>${fakeKey}</bash-stdout><bash-stderr></bash-stderr>` } },
371378
{ type: "user", uuid: "u-rm", sessionId: "cc-bash", message: { role: "user", content: "<bash-input>rm -rf /tmp/x</bash-input><bash-stdout>(Bash completed with no output)</bash-stdout><bash-stderr></bash-stderr>" } },
@@ -375,12 +382,15 @@ test("replaces compaction summaries and renders Claude bash transcript tags", as
375382
const payload = await runtime.buildPayloadFromHook({ session_id: "cc-bash", transcript_path: transcript }, "https://jieli.example.test");
376383
assert.equal(payload.thread.title, "原始第一条消息");
377384
assert.equal(payload.thread.messages[1].content, runtime.COMPACTION_PLACEHOLDER);
378-
assert.equal(payload.thread.messages[2].content, "```console\n$ echo $JIELI_API_KEY\n[REDACTED:jieli-api-key]\n```");
379-
assert.equal(payload.thread.messages[3].content, "```console\n$ rm -rf /tmp/x\n# (no output)\n```");
380-
assert.equal(payload.thread.messages[4].content, prose);
385+
assert.equal(payload.thread.messages[2].content, runtime.COMPACTION_PLACEHOLDER);
386+
assert.equal(runtime.COMPACTION_PLACEHOLDER, "[Context automatically compacted]");
387+
assert.equal(payload.thread.messages[3].content, "```console\n$ echo $JIELI_API_KEY\n[REDACTED:jieli-api-key]\n```");
388+
assert.equal(payload.thread.messages[4].content, "```console\n$ rm -rf /tmp/x\n# (no output)\n```");
389+
assert.equal(payload.thread.messages[5].content, prose);
381390
const raw = JSON.stringify(payload);
382391
assert.doesNotMatch(raw, new RegExp(fakeKey));
383392
assert.doesNotMatch(raw, /x{5000}/);
393+
assert.doesNotMatch(raw, /Claude compacted implementation detail/);
384394
});
385395

386396
test("configuration, upload, lock, and transcript flush helpers match hook behavior", async () => {
@@ -554,9 +564,22 @@ test("handoff info and commit trailer helpers inject Node-based context and trai
554564

555565
const handoff = runtime.buildHookResponse({ session_id: "cc-handoff", transcript_path: "/tmp/claude-session.jsonl", cwd: "/repo", tool_name: "Bash", tool_input: { command: "jieli-handoff-info" } });
556566
const handoffCommand = handoff.hookSpecificOutput.updatedInput.command;
557-
assert.match(handoffCommand, /JIELI_HANDOFF_CONTEXT_B64=/);
558-
assert.match(handoffCommand, /node .*jieli_node\.mjs handoff-info/);
567+
assert.doesNotMatch(handoffCommand, /JIELI_HANDOFF_CONTEXT_B64=/);
568+
assert.match(handoffCommand, /node .*jieli_node\.mjs handoff-info --context-b64 /);
559569
assert.deepEqual(decodeHandoffContext(handoffCommand), { session_id: "cc-handoff", transcript_path: "/tmp/claude-session.jsonl", cwd: "/repo" });
570+
for (const command of [
571+
"jieli-handoff-info.cmd",
572+
"jieli-handoff-info.exe",
573+
"C:\\Users\\Administrator\\.claude\\plugins\\cache\\jieliapp\\claude-code\\bin\\jieli-handoff-info.cmd",
574+
"& 'C:\\Users\\Administrator\\.claude\\plugins\\cache\\jieliapp\\claude-code\\bin\\jieli-handoff-info.cmd'",
575+
]) {
576+
const response = runtime.buildHookResponse({ session_id: "cc-win", transcript_path: "C:\\Users\\Administrator\\.claude\\projects\\session.jsonl", cwd: "C:\\repo", tool_name: "Bash", tool_input: { command } });
577+
const updated = response.hookSpecificOutput.updatedInput.command;
578+
assert.match(updated, /handoff-info --context-b64 /);
579+
assert.deepEqual(decodeHandoffContext(updated), { session_id: "cc-win", transcript_path: "C:\\Users\\Administrator\\.claude\\projects\\session.jsonl", cwd: "C:\\repo" });
580+
}
581+
const cliInfo = await withEnv({ JIELI_HANDOFF_CONTEXT_B64: undefined, JIELI_BASE_URL: "https://jieli.example.test" }, () => runtime.buildHandoffInfo(process.env, encoded));
582+
assert.equal(cliInfo.thread_id, "T-cc-1");
560583
assert.deepEqual(runtime.buildHookResponse({ session_id: "cc-handoff", tool_name: "Bash", tool_input: { command: "jieli-handoff-info | cat" } }), {});
561584

562585
await withEnv({ HOME: tmp }, async () => {

plugins/codex/scripts/jieli_node.mjs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,12 @@ const MODEL_ALIAS_ENV_NAMES = [
3636
"ANTHROPIC_DEFAULT_SONNET_MODEL",
3737
"ANTHROPIC_DEFAULT_OPUS_MODEL",
3838
];
39-
const COMPACTION_PLACEHOLDER =
40-
"[Context compacted - earlier conversation summarized to continue past the context window]";
39+
const COMPACTION_PLACEHOLDER = "[Context automatically compacted]";
4140
const HANDOFF_SUMMARY_RE = /^(?:#{1,6}\s*)?\*{0,2}Handoff Summary\*{0,2}\s*(?:\n|$)/i;
41+
const CODEX_AUTO_COMPACTION_PREAMBLE_RE =
42+
/^Another language model started to solve this problem and produced a summary of its thinking process\./i;
43+
const CODEX_AUTO_COMPACTION_CURRENT_PROGRESS_RE = /^Current progress:\s*(?:\n|$)/i;
44+
const CODEX_AUTO_COMPACTION_SECTION_RE = /^(?:Important context(?: and constraints)?|What remains to do|Relevant files|Critical examples):\s*$/im;
4245
const CODEX_GIT_DIRECTIVE_RE = /^[ \t]*::git-[A-Za-z0-9_-]+\{[^\n]*\}[ \t]*\n?/gm;
4346
const CODEX_USER_FILE_MENTIONS_RE =
4447
/^\s*#{0,6}\s*Files mentioned by the user:\s*\n[\s\S]*?^\s*#{0,6}\s*My request for Codex:\s*\n?/m;
@@ -447,6 +450,7 @@ async function parseTranscript(path, fallbackSessionId = "", imageUploader = nul
447450
if (entry.type !== "response_item") continue;
448451
const item = await messageFromResponseItem(payload, lineNumber, imageUploader, dataImageUploader);
449452
if (!item) continue;
453+
if (item.role === "user" && isEnvironmentContextText(textFromContent(item.content))) continue;
450454
if (item.role === "user" && !title) title = textFromContent(item.content).trim().slice(0, 80);
451455
messages.push(item);
452456
}
@@ -674,8 +678,15 @@ function shouldSkipUserMessage(content) {
674678
return ["<codex_internal_context", "<skill>", "Base directory for this skill:", "# AGENTS.md instructions for "].some((prefix) => text.startsWith(prefix));
675679
}
676680

681+
function isEnvironmentContextText(text) {
682+
return String(text || "").trimStart().startsWith("<environment_context");
683+
}
684+
677685
function isHandoffSummaryText(text) {
678-
return HANDOFF_SUMMARY_RE.test(text.trimStart());
686+
const trimmed = text.trimStart();
687+
if (HANDOFF_SUMMARY_RE.test(trimmed)) return true;
688+
if (CODEX_AUTO_COMPACTION_PREAMBLE_RE.test(trimmed)) return true;
689+
return CODEX_AUTO_COMPACTION_CURRENT_PROGRESS_RE.test(trimmed) && CODEX_AUTO_COMPACTION_SECTION_RE.test(trimmed);
679690
}
680691

681692
function cleanCodexText(text) {
@@ -1208,19 +1219,27 @@ function updatedHandoffCommand(command, hookData) {
12081219
cwd: String(hookData.cwd || ""),
12091220
};
12101221
const encoded = Buffer.from(JSON.stringify(context), "utf8").toString("base64");
1211-
return `${HANDOFF_CONTEXT_ENV}=${quoteShell(encoded)} ${helper}`;
1222+
return `${helper} --context-b64 ${quoteShell(encoded)}`;
12121223
}
12131224

12141225
function resolvedHandoffHelperCommand(command) {
12151226
if (command.includes(HANDOFF_CONTEXT_ENV) || AMBIGUOUS_TOKENS.some((token) => command.includes(token))) return "";
12161227
const parts = shellSplit(command);
12171228
if (!parts) return "";
1218-
if (parts.length === 1 && basename(parts[0]) === HANDOFF_HELPER_COMMAND) {
1229+
if ((parts.length === 1 && isHandoffHelper(parts[0])) || (parts.length === 2 && parts[0] === "&" && isHandoffHelper(parts[1]))) {
12191230
return `node ${quoteShell(join(pluginRoot, "scripts", "jieli_node.mjs"))} handoff-info`;
12201231
}
12211232
return "";
12221233
}
12231234

1235+
function isHandoffHelper(part) {
1236+
const normalized = String(part || "").replace(/\\/g, "/").toLowerCase();
1237+
const name = normalized.split("/").pop();
1238+
return [HANDOFF_HELPER_COMMAND, `${HANDOFF_HELPER_COMMAND}.cmd`, `${HANDOFF_HELPER_COMMAND}.exe`].some(
1239+
(helper) => name === helper || normalized.endsWith(`/bin/${helper}`) || normalized.endsWith(`bin${helper}`),
1240+
);
1241+
}
1242+
12241243
function updatedCommitCommand(command, sessionId) {
12251244
if (!command || !sessionId || AMBIGUOUS_TOKENS.some((token) => command.includes(token))) return "";
12261245
const mapping = readJson(join(homeDir(), ".jieli", SESSION_MAPPING_FILE), {});
@@ -1512,15 +1531,17 @@ function formatThreadsMarkdown(payload, baseUrl) {
15121531

15131532
function handoffInfoMain(args = []) {
15141533
if (args.includes("--help") || args.includes("-h")) {
1515-
console.log("usage: jieli-handoff-info");
1534+
console.log("usage: jieli-handoff-info [--context-b64 CONTEXT]");
15161535
return 0;
15171536
}
1518-
console.log(JSON.stringify(buildHandoffInfo(), Object.keys(buildHandoffInfo()).sort()));
1537+
const opts = parseArgs(args);
1538+
const info = buildHandoffInfo(process.env, opts.contextB64 || "");
1539+
console.log(JSON.stringify(info, Object.keys(info).sort()));
15191540
return 0;
15201541
}
15211542

1522-
function buildHandoffInfo(env = process.env) {
1523-
const context = decodeContext(env[HANDOFF_CONTEXT_ENV] || "");
1543+
function buildHandoffInfo(env = process.env, contextB64 = "") {
1544+
const context = decodeContext(contextB64 || env[HANDOFF_CONTEXT_ENV] || "");
15241545
if (!context) return missingInfo("missing hook context");
15251546
const transcriptPath = String(context.transcript_path || context.session_path || "").trim();
15261547
const meta = readSessionMeta(transcriptPath);

0 commit comments

Comments
 (0)