context-mode 1.0.151 → 1.0.152
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/.codex-plugin/mcp.json +5 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +16 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +89 -3
- package/build/adapters/claude-code/hooks.js +2 -2
- package/build/adapters/claude-code/index.js +14 -13
- package/build/adapters/client-map.js +3 -0
- package/build/adapters/detect.js +13 -1
- package/build/adapters/gemini-cli/hooks.d.ts +10 -0
- package/build/adapters/gemini-cli/hooks.js +12 -2
- package/build/adapters/gemini-cli/index.d.ts +21 -1
- package/build/adapters/gemini-cli/index.js +37 -1
- package/build/adapters/kimi/config.d.ts +8 -0
- package/build/adapters/kimi/config.js +8 -0
- package/build/adapters/kimi/hooks.d.ts +28 -0
- package/build/adapters/kimi/hooks.js +34 -0
- package/build/adapters/kimi/index.d.ts +66 -0
- package/build/adapters/kimi/index.js +537 -0
- package/build/adapters/kimi/paths.d.ts +1 -0
- package/build/adapters/kimi/paths.js +12 -0
- package/build/adapters/kiro/hooks.js +2 -2
- package/build/adapters/openclaw/plugin.d.ts +14 -13
- package/build/adapters/openclaw/plugin.js +140 -40
- package/build/adapters/opencode/plugin.js +4 -3
- package/build/adapters/opencode/zod3tov4.js +8 -8
- package/build/adapters/pi/extension.js +9 -24
- package/build/adapters/pi/mcp-bridge.js +37 -0
- package/build/adapters/qwen-code/index.js +7 -7
- package/build/adapters/types.d.ts +39 -2
- package/build/adapters/types.js +55 -2
- package/build/cli.js +433 -25
- package/build/executor.js +6 -3
- package/build/runtime.d.ts +81 -1
- package/build/runtime.js +195 -9
- package/build/search/ctx-search-schema.d.ts +90 -0
- package/build/search/ctx-search-schema.js +135 -0
- package/build/search/unified.d.ts +12 -0
- package/build/search/unified.js +17 -2
- package/build/server.d.ts +2 -1
- package/build/server.js +378 -97
- package/build/session/analytics.d.ts +36 -13
- package/build/session/analytics.js +123 -26
- package/build/session/db.d.ts +24 -0
- package/build/session/db.js +41 -0
- package/build/session/extract.js +30 -0
- package/build/session/snapshot.js +24 -0
- package/build/store.d.ts +12 -1
- package/build/store.js +72 -20
- package/build/types.d.ts +7 -0
- package/build/util/project-dir.d.ts +19 -16
- package/build/util/project-dir.js +80 -45
- package/cli.bundle.mjs +371 -320
- package/configs/kimi/hooks.json +54 -0
- package/configs/pi/AGENTS.md +3 -85
- package/hooks/cache-heal-utils.mjs +148 -0
- package/hooks/core/formatters.mjs +26 -0
- package/hooks/core/routing.mjs +9 -1
- package/hooks/core/stdin.mjs +74 -3
- package/hooks/core/tool-naming.mjs +1 -0
- package/hooks/heal-partial-install.mjs +712 -0
- package/hooks/kimi/platform.mjs +1 -0
- package/hooks/kimi/posttooluse.mjs +72 -0
- package/hooks/kimi/precompact.mjs +80 -0
- package/hooks/kimi/pretooluse.mjs +42 -0
- package/hooks/kimi/sessionend.mjs +61 -0
- package/hooks/kimi/sessionstart.mjs +113 -0
- package/hooks/kimi/stop.mjs +61 -0
- package/hooks/kimi/userpromptsubmit.mjs +90 -0
- package/hooks/normalize-hooks.mjs +66 -12
- package/hooks/routing-block.mjs +8 -2
- package/hooks/security.bundle.mjs +1 -1
- package/hooks/session-db.bundle.mjs +6 -4
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +93 -3
- package/hooks/session-snapshot.bundle.mjs +20 -19
- package/hooks/sessionstart.mjs +64 -0
- package/insight/server.mjs +15 -3
- package/openclaw.plugin.json +16 -1
- package/package.json +1 -1
- package/scripts/heal-installed-plugins.mjs +31 -10
- package/scripts/postinstall.mjs +10 -0
- package/server.bundle.mjs +206 -157
- package/skills/ctx-index/SKILL.md +46 -0
- package/skills/ctx-search/SKILL.md +35 -0
- package/start.mjs +84 -11
- package/build/cache-heal.d.ts +0 -48
- package/build/cache-heal.js +0 -150
- package/build/concurrency/runPool.d.ts +0 -36
- package/build/concurrency/runPool.js +0 -51
- package/build/openclaw/mcp-tools.d.ts +0 -54
- package/build/openclaw/mcp-tools.js +0 -198
- package/build/openclaw/workspace-router.d.ts +0 -29
- package/build/openclaw/workspace-router.js +0 -64
- package/build/openclaw-plugin.d.ts +0 -130
- package/build/openclaw-plugin.js +0 -626
- package/build/opencode-plugin.d.ts +0 -122
- package/build/opencode-plugin.js +0 -375
- package/build/pi-extension.d.ts +0 -14
- package/build/pi-extension.js +0 -451
- package/build/routing-block.d.ts +0 -8
- package/build/routing-block.js +0 -86
- package/build/tool-naming.d.ts +0 -4
- package/build/tool-naming.js +0 -24
package/build/store.js
CHANGED
|
@@ -262,7 +262,7 @@ function findMinSpan(positionLists) {
|
|
|
262
262
|
return Infinity;
|
|
263
263
|
if (positionLists.length === 1)
|
|
264
264
|
return 0;
|
|
265
|
-
const sorted = positionLists
|
|
265
|
+
const sorted = positionLists;
|
|
266
266
|
const ptrs = new Array(sorted.length).fill(0);
|
|
267
267
|
let minSpan = Infinity;
|
|
268
268
|
while (true) {
|
|
@@ -507,7 +507,8 @@ export class ContentStore {
|
|
|
507
507
|
chunks.timestamp,
|
|
508
508
|
sources.label,
|
|
509
509
|
bm25(chunks, 5.0, 1.0) AS rank,
|
|
510
|
-
highlight(chunks, 1, char(2), char(3)) AS highlighted
|
|
510
|
+
highlight(chunks, 1, char(2), char(3)) AS highlighted,
|
|
511
|
+
chunks.session_id
|
|
511
512
|
FROM chunks
|
|
512
513
|
JOIN sources ON sources.id = chunks.source_id
|
|
513
514
|
WHERE chunks MATCH ?
|
|
@@ -522,7 +523,8 @@ export class ContentStore {
|
|
|
522
523
|
chunks.timestamp,
|
|
523
524
|
sources.label,
|
|
524
525
|
bm25(chunks, 5.0, 1.0) AS rank,
|
|
525
|
-
highlight(chunks, 1, char(2), char(3)) AS highlighted
|
|
526
|
+
highlight(chunks, 1, char(2), char(3)) AS highlighted,
|
|
527
|
+
chunks.session_id
|
|
526
528
|
FROM chunks
|
|
527
529
|
JOIN sources ON sources.id = chunks.source_id
|
|
528
530
|
WHERE chunks MATCH ? AND sources.label LIKE ? ESCAPE '\\'
|
|
@@ -537,7 +539,8 @@ export class ContentStore {
|
|
|
537
539
|
chunks.timestamp,
|
|
538
540
|
sources.label,
|
|
539
541
|
bm25(chunks, 5.0, 1.0) AS rank,
|
|
540
|
-
highlight(chunks, 1, char(2), char(3)) AS highlighted
|
|
542
|
+
highlight(chunks, 1, char(2), char(3)) AS highlighted,
|
|
543
|
+
chunks.session_id
|
|
541
544
|
FROM chunks
|
|
542
545
|
JOIN sources ON sources.id = chunks.source_id
|
|
543
546
|
WHERE chunks MATCH ? AND sources.label = ?
|
|
@@ -552,7 +555,8 @@ export class ContentStore {
|
|
|
552
555
|
chunks_trigram.timestamp,
|
|
553
556
|
sources.label,
|
|
554
557
|
bm25(chunks_trigram, 5.0, 1.0) AS rank,
|
|
555
|
-
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
|
|
558
|
+
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted,
|
|
559
|
+
chunks_trigram.session_id
|
|
556
560
|
FROM chunks_trigram
|
|
557
561
|
JOIN sources ON sources.id = chunks_trigram.source_id
|
|
558
562
|
WHERE chunks_trigram MATCH ?
|
|
@@ -567,7 +571,8 @@ export class ContentStore {
|
|
|
567
571
|
chunks_trigram.timestamp,
|
|
568
572
|
sources.label,
|
|
569
573
|
bm25(chunks_trigram, 5.0, 1.0) AS rank,
|
|
570
|
-
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
|
|
574
|
+
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted,
|
|
575
|
+
chunks_trigram.session_id
|
|
571
576
|
FROM chunks_trigram
|
|
572
577
|
JOIN sources ON sources.id = chunks_trigram.source_id
|
|
573
578
|
WHERE chunks_trigram MATCH ? AND sources.label LIKE ? ESCAPE '\\'
|
|
@@ -582,7 +587,8 @@ export class ContentStore {
|
|
|
582
587
|
chunks_trigram.timestamp,
|
|
583
588
|
sources.label,
|
|
584
589
|
bm25(chunks_trigram, 5.0, 1.0) AS rank,
|
|
585
|
-
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
|
|
590
|
+
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted,
|
|
591
|
+
chunks_trigram.session_id
|
|
586
592
|
FROM chunks_trigram
|
|
587
593
|
JOIN sources ON sources.id = chunks_trigram.source_id
|
|
588
594
|
WHERE chunks_trigram MATCH ? AND sources.label = ?
|
|
@@ -598,7 +604,8 @@ export class ContentStore {
|
|
|
598
604
|
chunks.timestamp,
|
|
599
605
|
sources.label,
|
|
600
606
|
bm25(chunks, 5.0, 1.0) AS rank,
|
|
601
|
-
highlight(chunks, 1, char(2), char(3)) AS highlighted
|
|
607
|
+
highlight(chunks, 1, char(2), char(3)) AS highlighted,
|
|
608
|
+
chunks.session_id
|
|
602
609
|
FROM chunks
|
|
603
610
|
JOIN sources ON sources.id = chunks.source_id
|
|
604
611
|
WHERE chunks MATCH ? AND chunks.content_type = ?
|
|
@@ -613,7 +620,8 @@ export class ContentStore {
|
|
|
613
620
|
chunks.timestamp,
|
|
614
621
|
sources.label,
|
|
615
622
|
bm25(chunks, 5.0, 1.0) AS rank,
|
|
616
|
-
highlight(chunks, 1, char(2), char(3)) AS highlighted
|
|
623
|
+
highlight(chunks, 1, char(2), char(3)) AS highlighted,
|
|
624
|
+
chunks.session_id
|
|
617
625
|
FROM chunks
|
|
618
626
|
JOIN sources ON sources.id = chunks.source_id
|
|
619
627
|
WHERE chunks MATCH ? AND sources.label LIKE ? ESCAPE '\\' AND chunks.content_type = ?
|
|
@@ -628,7 +636,8 @@ export class ContentStore {
|
|
|
628
636
|
chunks.timestamp,
|
|
629
637
|
sources.label,
|
|
630
638
|
bm25(chunks, 5.0, 1.0) AS rank,
|
|
631
|
-
highlight(chunks, 1, char(2), char(3)) AS highlighted
|
|
639
|
+
highlight(chunks, 1, char(2), char(3)) AS highlighted,
|
|
640
|
+
chunks.session_id
|
|
632
641
|
FROM chunks
|
|
633
642
|
JOIN sources ON sources.id = chunks.source_id
|
|
634
643
|
WHERE chunks MATCH ? AND sources.label = ? AND chunks.content_type = ?
|
|
@@ -643,7 +652,8 @@ export class ContentStore {
|
|
|
643
652
|
chunks_trigram.timestamp,
|
|
644
653
|
sources.label,
|
|
645
654
|
bm25(chunks_trigram, 5.0, 1.0) AS rank,
|
|
646
|
-
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
|
|
655
|
+
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted,
|
|
656
|
+
chunks_trigram.session_id
|
|
647
657
|
FROM chunks_trigram
|
|
648
658
|
JOIN sources ON sources.id = chunks_trigram.source_id
|
|
649
659
|
WHERE chunks_trigram MATCH ? AND chunks_trigram.content_type = ?
|
|
@@ -658,7 +668,8 @@ export class ContentStore {
|
|
|
658
668
|
chunks_trigram.timestamp,
|
|
659
669
|
sources.label,
|
|
660
670
|
bm25(chunks_trigram, 5.0, 1.0) AS rank,
|
|
661
|
-
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
|
|
671
|
+
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted,
|
|
672
|
+
chunks_trigram.session_id
|
|
662
673
|
FROM chunks_trigram
|
|
663
674
|
JOIN sources ON sources.id = chunks_trigram.source_id
|
|
664
675
|
WHERE chunks_trigram MATCH ? AND sources.label LIKE ? ESCAPE '\\' AND chunks_trigram.content_type = ?
|
|
@@ -673,7 +684,8 @@ export class ContentStore {
|
|
|
673
684
|
chunks_trigram.timestamp,
|
|
674
685
|
sources.label,
|
|
675
686
|
bm25(chunks_trigram, 5.0, 1.0) AS rank,
|
|
676
|
-
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
|
|
687
|
+
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted,
|
|
688
|
+
chunks_trigram.session_id
|
|
677
689
|
FROM chunks_trigram
|
|
678
690
|
JOIN sources ON sources.id = chunks_trigram.source_id
|
|
679
691
|
WHERE chunks_trigram MATCH ? AND sources.label = ? AND chunks_trigram.content_type = ?
|
|
@@ -902,6 +914,7 @@ export class ContentStore {
|
|
|
902
914
|
contentType: r.content_type,
|
|
903
915
|
highlighted: r.highlighted,
|
|
904
916
|
timestamp: r.timestamp ?? undefined,
|
|
917
|
+
sessionId: r.session_id ?? "",
|
|
905
918
|
}));
|
|
906
919
|
}
|
|
907
920
|
#sourceFilterParam(source, sourceMatchMode) {
|
|
@@ -1088,13 +1101,21 @@ export class ContentStore {
|
|
|
1088
1101
|
.map(({ result }) => result);
|
|
1089
1102
|
}
|
|
1090
1103
|
// ── Unified Fallback Search ──
|
|
1091
|
-
searchWithFallback(query, limit = 3, source, contentType, sourceMatchMode = "like") {
|
|
1104
|
+
searchWithFallback(query, limit = 3, source, contentType, sourceMatchMode = "like", sessionIdAllowSet) {
|
|
1092
1105
|
// Step 0: Auto-refresh stale file-backed sources before searching
|
|
1093
1106
|
this.#refreshStaleSources();
|
|
1107
|
+
// When a session-id allow-set is in play (issue #737 project filter),
|
|
1108
|
+
// fetch a larger candidate pool from the FTS5 layers so the post-filter
|
|
1109
|
+
// can still deliver `limit` matches even if many candidates are excluded.
|
|
1110
|
+
// The cap is bounded — even at the largest installs the chunk count
|
|
1111
|
+
// dwarfs `limit * 8`, and the surplus is dropped on the post-filter.
|
|
1112
|
+
const fetchLimit = sessionIdAllowSet ? Math.max(limit * 8, 40) : limit;
|
|
1113
|
+
const sessionFilter = this.#makeSessionFilter(sessionIdAllowSet);
|
|
1094
1114
|
// Step 1: RRF fusion (porter OR + trigram OR → merge)
|
|
1095
|
-
const rrfResults = this.#rrfSearch(query,
|
|
1096
|
-
|
|
1097
|
-
|
|
1115
|
+
const rrfResults = this.#rrfSearch(query, fetchLimit, source, contentType, sourceMatchMode);
|
|
1116
|
+
const rrfFiltered = sessionFilter ? rrfResults.filter(sessionFilter) : rrfResults;
|
|
1117
|
+
if (rrfFiltered.length > 0) {
|
|
1118
|
+
const reranked = this.#applyProximityReranking(rrfFiltered.slice(0, limit), query);
|
|
1098
1119
|
return reranked.map((r) => ({ ...r, matchLayer: "rrf" }));
|
|
1099
1120
|
}
|
|
1100
1121
|
// Step 2: Fuzzy correction → RRF re-run
|
|
@@ -1109,14 +1130,29 @@ export class ContentStore {
|
|
|
1109
1130
|
const correctedWords = words.map((w) => this.fuzzyCorrect(w) ?? w);
|
|
1110
1131
|
const correctedQuery = correctedWords.join(" ");
|
|
1111
1132
|
if (correctedQuery !== original) {
|
|
1112
|
-
const fuzzyResults = this.#rrfSearch(correctedQuery,
|
|
1113
|
-
|
|
1114
|
-
|
|
1133
|
+
const fuzzyResults = this.#rrfSearch(correctedQuery, fetchLimit, source, contentType, sourceMatchMode);
|
|
1134
|
+
const fuzzyFiltered = sessionFilter ? fuzzyResults.filter(sessionFilter) : fuzzyResults;
|
|
1135
|
+
if (fuzzyFiltered.length > 0) {
|
|
1136
|
+
const reranked = this.#applyProximityReranking(fuzzyFiltered.slice(0, limit), correctedQuery);
|
|
1115
1137
|
return reranked.map((r) => ({ ...r, matchLayer: "rrf-fuzzy" }));
|
|
1116
1138
|
}
|
|
1117
1139
|
}
|
|
1118
1140
|
return [];
|
|
1119
1141
|
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Build the session-id post-filter for the FTS5 candidate pool. Legacy
|
|
1144
|
+
* chunks indexed before per-session attribution carry `session_id=''` and
|
|
1145
|
+
* stay visible across projects so user-indexed content remains reachable
|
|
1146
|
+
* after opting into the shared-DB mode (#737).
|
|
1147
|
+
*/
|
|
1148
|
+
#makeSessionFilter(allowSet) {
|
|
1149
|
+
if (!allowSet)
|
|
1150
|
+
return null;
|
|
1151
|
+
return (r) => {
|
|
1152
|
+
const sid = r.sessionId ?? "";
|
|
1153
|
+
return sid === "" || allowSet.has(sid);
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1120
1156
|
/** Number of sources auto-refreshed in the last searchWithFallback call. */
|
|
1121
1157
|
lastRefreshCount = 0;
|
|
1122
1158
|
/**
|
|
@@ -1181,6 +1217,22 @@ export class ContentStore {
|
|
|
1181
1217
|
listSources() {
|
|
1182
1218
|
return this.#stmtListSources.all();
|
|
1183
1219
|
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Aggregate snapshot of the persistent content store. Returns total
|
|
1222
|
+
* chunk count, source count, and the most recent indexed_at timestamp.
|
|
1223
|
+
* Used by ctx_stats so callers can see observability state in the same
|
|
1224
|
+
* round trip instead of inferring it from snapshot diffs.
|
|
1225
|
+
*/
|
|
1226
|
+
getIndexState() {
|
|
1227
|
+
const row = this.#db
|
|
1228
|
+
.prepare("SELECT COALESCE(SUM(chunk_count), 0) AS total_chunks, COUNT(*) AS total_sources, MAX(indexed_at) AS last_indexed_at FROM sources")
|
|
1229
|
+
.get();
|
|
1230
|
+
return {
|
|
1231
|
+
totalChunks: row.total_chunks ?? 0,
|
|
1232
|
+
totalSources: row.total_sources ?? 0,
|
|
1233
|
+
lastIndexedAt: row.last_indexed_at ?? undefined,
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1184
1236
|
/**
|
|
1185
1237
|
* Get all chunks for a given source by ID — bypasses FTS5 MATCH entirely.
|
|
1186
1238
|
* Use this for inventory/listing where you need all sections, not search.
|
package/build/types.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ export interface SearchResult {
|
|
|
75
75
|
matchLayer?: "porter" | "trigram" | "fuzzy" | "rrf" | "rrf-fuzzy";
|
|
76
76
|
highlighted?: string;
|
|
77
77
|
timestamp?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Session-id attribution copied from the `chunks.session_id` column
|
|
80
|
+
* (legacy unattributed chunks carry an empty string). Used by the
|
|
81
|
+
* `ctx_search` per-project filter (#737) to scope shared-DB results
|
|
82
|
+
* to the current project via the 2-step IN-clause strategy.
|
|
83
|
+
*/
|
|
84
|
+
sessionId?: string;
|
|
78
85
|
}
|
|
79
86
|
/**
|
|
80
87
|
* Aggregate statistics for a ContentStore instance.
|
|
@@ -18,12 +18,13 @@ import type { PlatformId } from "../adapters/types.js";
|
|
|
18
18
|
* (shell-set, survives `process.chdir`) before falling back.
|
|
19
19
|
*/
|
|
20
20
|
/**
|
|
21
|
-
* Detect whether a path lives inside
|
|
22
|
-
* specifically `<home>/.claude/plugins/cache/<plugin>/<plugin>/<version
|
|
23
|
-
*
|
|
21
|
+
* Detect whether a path lives inside an agent plugin install tree —
|
|
22
|
+
* specifically `<home>/.claude/plugins/cache/<plugin>/<plugin>/<version>/`,
|
|
23
|
+
* `<home>/.codex/plugins/cache/<plugin>/<plugin>/<version>/`, or the
|
|
24
|
+
* marketplace mirror under `<home>/.{claude,codex}/plugins/marketplaces/...`.
|
|
24
25
|
*
|
|
25
26
|
* Cross-OS: matches both POSIX (`/`) and Windows (`\`) path separators.
|
|
26
|
-
* Independent of `home` location — we only care about the
|
|
27
|
+
* Independent of `home` location — we only care about the agent plugin
|
|
27
28
|
* suffix pattern.
|
|
28
29
|
*/
|
|
29
30
|
export declare function isPluginInstallPath(p: string): boolean;
|
|
@@ -65,24 +66,26 @@ export declare function resolveProjectDirFromTranscript(opts: {
|
|
|
65
66
|
* session log when the spawned MCP child inherits a non-project cwd
|
|
66
67
|
* (e.g. $HOME when Codex was launched from anywhere outside the project).
|
|
67
68
|
*
|
|
68
|
-
* Codex writes its session transcripts to
|
|
69
|
-
* `${CODEX_HOME ?? ~/.codex}/sessions/<uuid>.jsonl
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
69
|
+
* Codex writes its session transcripts to either
|
|
70
|
+
* `${CODEX_HOME ?? ~/.codex}/sessions/<uuid>.jsonl` (CLI) or a dated desktop
|
|
71
|
+
* layout such as
|
|
72
|
+
* `${CODEX_HOME ?? ~/.codex}/sessions/YYYY/MM/DD/rollout-*.jsonl`.
|
|
73
|
+
* The cwd appears on `meta.cwd` for the CLI shape and on
|
|
74
|
+
* `payload.cwd` in `type: "session_meta"` records for Codex Desktop. Codex
|
|
75
|
+
* publishes NO workspace env var to its child MCP processes — so unlike
|
|
76
|
+
* Claude/Pi/Cursor, we have no env signal at all. The session log is the
|
|
77
|
+
* strongest available signal.
|
|
75
78
|
*
|
|
76
79
|
* Mirror of `resolveProjectDirFromTranscript` for Claude Code; differences:
|
|
77
|
-
* • Sessions live flat in
|
|
78
|
-
*
|
|
79
|
-
* • The cwd is on `meta.cwd`
|
|
80
|
+
* • Sessions may live flat or in a dated hierarchy (no per-project encoded
|
|
81
|
+
* subdir like Claude's `~/.claude/projects/<encoded>/`).
|
|
82
|
+
* • The cwd is nested on `meta.cwd` or `payload.cwd`, not top-level `cwd`.
|
|
80
83
|
*
|
|
81
84
|
* Returns `null` when:
|
|
82
85
|
* • `codexHome` or its `sessions/` subdir does not exist.
|
|
83
|
-
* • No `.jsonl` files exist or none has a parseable
|
|
86
|
+
* • No `.jsonl` files exist or none has a parseable cwd string.
|
|
84
87
|
* • The newest log is older than `transcriptMaxAgeMs` (multi-window guard).
|
|
85
|
-
* • The resolved
|
|
88
|
+
* • The resolved cwd points at a plugin install path (poisoned).
|
|
86
89
|
*/
|
|
87
90
|
export declare function resolveCodexSessionCwd(opts?: {
|
|
88
91
|
/** Defaults to `process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex")`. */
|
|
@@ -47,18 +47,19 @@ const LEGACY_NON_STRICT_CANDIDATES = [
|
|
|
47
47
|
* (shell-set, survives `process.chdir`) before falling back.
|
|
48
48
|
*/
|
|
49
49
|
/**
|
|
50
|
-
* Detect whether a path lives inside
|
|
51
|
-
* specifically `<home>/.claude/plugins/cache/<plugin>/<plugin>/<version
|
|
52
|
-
*
|
|
50
|
+
* Detect whether a path lives inside an agent plugin install tree —
|
|
51
|
+
* specifically `<home>/.claude/plugins/cache/<plugin>/<plugin>/<version>/`,
|
|
52
|
+
* `<home>/.codex/plugins/cache/<plugin>/<plugin>/<version>/`, or the
|
|
53
|
+
* marketplace mirror under `<home>/.{claude,codex}/plugins/marketplaces/...`.
|
|
53
54
|
*
|
|
54
55
|
* Cross-OS: matches both POSIX (`/`) and Windows (`\`) path separators.
|
|
55
|
-
* Independent of `home` location — we only care about the
|
|
56
|
+
* Independent of `home` location — we only care about the agent plugin
|
|
56
57
|
* suffix pattern.
|
|
57
58
|
*/
|
|
58
59
|
export function isPluginInstallPath(p) {
|
|
59
60
|
if (!p)
|
|
60
61
|
return false;
|
|
61
|
-
return /[/\\]\.claude[/\\]plugins[/\\](cache|marketplaces)[/\\]/.test(p);
|
|
62
|
+
return /[/\\]\.(claude|codex)[/\\]plugins[/\\](cache|marketplaces)[/\\]/.test(p);
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
65
|
* Read the per-session project dir from Claude Code's transcript files.
|
|
@@ -162,46 +163,76 @@ export function resolveProjectDirFromTranscript(opts) {
|
|
|
162
163
|
* session log when the spawned MCP child inherits a non-project cwd
|
|
163
164
|
* (e.g. $HOME when Codex was launched from anywhere outside the project).
|
|
164
165
|
*
|
|
165
|
-
* Codex writes its session transcripts to
|
|
166
|
-
* `${CODEX_HOME ?? ~/.codex}/sessions/<uuid>.jsonl
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
166
|
+
* Codex writes its session transcripts to either
|
|
167
|
+
* `${CODEX_HOME ?? ~/.codex}/sessions/<uuid>.jsonl` (CLI) or a dated desktop
|
|
168
|
+
* layout such as
|
|
169
|
+
* `${CODEX_HOME ?? ~/.codex}/sessions/YYYY/MM/DD/rollout-*.jsonl`.
|
|
170
|
+
* The cwd appears on `meta.cwd` for the CLI shape and on
|
|
171
|
+
* `payload.cwd` in `type: "session_meta"` records for Codex Desktop. Codex
|
|
172
|
+
* publishes NO workspace env var to its child MCP processes — so unlike
|
|
173
|
+
* Claude/Pi/Cursor, we have no env signal at all. The session log is the
|
|
174
|
+
* strongest available signal.
|
|
172
175
|
*
|
|
173
176
|
* Mirror of `resolveProjectDirFromTranscript` for Claude Code; differences:
|
|
174
|
-
* • Sessions live flat in
|
|
175
|
-
*
|
|
176
|
-
* • The cwd is on `meta.cwd`
|
|
177
|
+
* • Sessions may live flat or in a dated hierarchy (no per-project encoded
|
|
178
|
+
* subdir like Claude's `~/.claude/projects/<encoded>/`).
|
|
179
|
+
* • The cwd is nested on `meta.cwd` or `payload.cwd`, not top-level `cwd`.
|
|
177
180
|
*
|
|
178
181
|
* Returns `null` when:
|
|
179
182
|
* • `codexHome` or its `sessions/` subdir does not exist.
|
|
180
|
-
* • No `.jsonl` files exist or none has a parseable
|
|
183
|
+
* • No `.jsonl` files exist or none has a parseable cwd string.
|
|
181
184
|
* • The newest log is older than `transcriptMaxAgeMs` (multi-window guard).
|
|
182
|
-
* • The resolved
|
|
185
|
+
* • The resolved cwd points at a plugin install path (poisoned).
|
|
183
186
|
*/
|
|
184
187
|
export function resolveCodexSessionCwd(opts) {
|
|
185
188
|
const codexHome = opts?.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
|
186
189
|
const sessionsDir = path.join(codexHome, "sessions");
|
|
187
190
|
if (!fs.existsSync(sessionsDir))
|
|
188
191
|
return null;
|
|
192
|
+
const MAX_SCAN_DEPTH = 4; // sessions/YYYY/MM/DD/<file>.jsonl plus one spare.
|
|
193
|
+
const MAX_SCAN_ENTRIES = 10_000;
|
|
194
|
+
let visitedEntries = 0;
|
|
189
195
|
let bestPath;
|
|
190
196
|
let bestMtime = 0;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
const visit = (dir, depth) => {
|
|
198
|
+
if (visitedEntries >= MAX_SCAN_ENTRIES)
|
|
199
|
+
return;
|
|
200
|
+
let entries;
|
|
201
|
+
try {
|
|
202
|
+
entries = fs.readdirSync(dir);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
entries.sort().reverse();
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
if (visitedEntries >= MAX_SCAN_ENTRIES)
|
|
210
|
+
return;
|
|
211
|
+
visitedEntries++;
|
|
212
|
+
const fp = path.join(dir, entry);
|
|
213
|
+
let stat;
|
|
196
214
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
215
|
+
stat = fs.statSync(fp);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (stat.isDirectory()) {
|
|
221
|
+
if (depth < MAX_SCAN_DEPTH)
|
|
222
|
+
visit(fp, depth + 1);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!stat.isFile() || !entry.endsWith(".jsonl"))
|
|
226
|
+
continue;
|
|
227
|
+
const m = stat.mtimeMs;
|
|
228
|
+
if (m > bestMtime) {
|
|
229
|
+
bestMtime = m;
|
|
230
|
+
bestPath = fp;
|
|
202
231
|
}
|
|
203
|
-
catch { /* skip */ }
|
|
204
232
|
}
|
|
233
|
+
};
|
|
234
|
+
try {
|
|
235
|
+
visit(sessionsDir, 0);
|
|
205
236
|
}
|
|
206
237
|
catch {
|
|
207
238
|
return null;
|
|
@@ -213,28 +244,31 @@ export function resolveCodexSessionCwd(opts) {
|
|
|
213
244
|
if (nowMs - bestMtime > opts.transcriptMaxAgeMs)
|
|
214
245
|
return null;
|
|
215
246
|
}
|
|
216
|
-
// Read
|
|
217
|
-
//
|
|
247
|
+
// Read a bounded head chunk. Codex Desktop's first session_meta line can be
|
|
248
|
+
// larger than Claude/Codex CLI metadata because it includes dynamic tool and
|
|
249
|
+
// instruction fields, but the full transcript can still be tens of MB.
|
|
218
250
|
try {
|
|
219
251
|
const fd = fs.openSync(bestPath, "r");
|
|
220
252
|
try {
|
|
221
|
-
const buf = Buffer.alloc(
|
|
253
|
+
const buf = Buffer.alloc(1024 * 1024);
|
|
222
254
|
const bytes = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
223
255
|
const text = buf.subarray(0, bytes).toString("utf-8");
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
256
|
+
for (const line of text.split("\n").slice(0, 10)) {
|
|
257
|
+
if (!line.trim())
|
|
258
|
+
continue;
|
|
259
|
+
try {
|
|
260
|
+
const obj = JSON.parse(line);
|
|
261
|
+
const cwd = obj?.meta?.cwd ??
|
|
262
|
+
(obj?.type === "session_meta" ? obj?.payload?.cwd : undefined);
|
|
263
|
+
if (typeof cwd !== "string" || cwd.length === 0)
|
|
264
|
+
continue;
|
|
265
|
+
if (isPluginInstallPath(cwd))
|
|
266
|
+
return null;
|
|
267
|
+
return cwd;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return null; /* malformed session metadata line */
|
|
271
|
+
}
|
|
238
272
|
}
|
|
239
273
|
}
|
|
240
274
|
finally {
|
|
@@ -244,6 +278,7 @@ export function resolveCodexSessionCwd(opts) {
|
|
|
244
278
|
catch {
|
|
245
279
|
return null; /* file vanished mid-read */
|
|
246
280
|
}
|
|
281
|
+
return null;
|
|
247
282
|
}
|
|
248
283
|
/**
|
|
249
284
|
* Pure project-dir resolver. Mirror of the env-var chain inside
|