context-mode 1.0.154 → 1.0.155
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/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/concurrency/runPool.d.ts +36 -0
- package/build/concurrency/runPool.js +51 -0
- package/build/openclaw/mcp-tools.d.ts +54 -0
- package/build/openclaw/mcp-tools.js +198 -0
- package/build/openclaw/workspace-router.d.ts +29 -0
- package/build/openclaw/workspace-router.js +64 -0
- package/build/openclaw-plugin.d.ts +130 -0
- package/build/openclaw-plugin.js +626 -0
- package/build/opencode-plugin.d.ts +122 -0
- package/build/opencode-plugin.js +372 -0
- package/build/pi-extension.d.ts +14 -0
- package/build/pi-extension.js +451 -0
- package/build/util/db-lock.d.ts +65 -0
- package/build/util/db-lock.js +166 -0
- package/cli.bundle.mjs +2 -2
- package/hooks/platform-bridge.mjs +207 -0
- package/hooks/session-loaders.mjs +15 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +2 -2
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// hooks/platform-bridge.mjs — Fire-and-forget event forwarder.
|
|
2
|
+
// Reads ~/.context-mode/platform.json {api_key, platform_url}.
|
|
3
|
+
// POSTs every event to ${platform_url}/events.
|
|
4
|
+
// Privacy: redacts secrets + normalizes $HOME before send.
|
|
5
|
+
// Backward-compat (PRD §5.3 Upgrade Lag): v1 events_url → platform_url; token → api_key.
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
|
|
11
|
+
const CACHE_TTL_MS = 60_000;
|
|
12
|
+
const FETCH_TIMEOUT_MS = 2_000;
|
|
13
|
+
const MAX_FIELD_LEN = 200;
|
|
14
|
+
const MAX_DEPTH = 4;
|
|
15
|
+
|
|
16
|
+
// Negative-cache sentinel — distinguishes "uninitialized" (null) from
|
|
17
|
+
// "we checked recently and there's no config" (NO_CONFIG). Without this,
|
|
18
|
+
// the unconfigured-user path would hit fs.readFileSync on every event.
|
|
19
|
+
const NO_CONFIG = Symbol("no-config");
|
|
20
|
+
|
|
21
|
+
let _cache = null;
|
|
22
|
+
let _cacheLoadedAt = 0;
|
|
23
|
+
let _warned = false; // dedupe stderr — log first failure only, reset on success
|
|
24
|
+
let _fsLoads = 0; // test-only counter: how many times readConfig hit the FS
|
|
25
|
+
|
|
26
|
+
// === Cross-platform config path (bug-free across Win/Linux/Mac/WSL) ===
|
|
27
|
+
export function configPath() {
|
|
28
|
+
if (process.platform === "win32") {
|
|
29
|
+
const appdata = process.env.APPDATA;
|
|
30
|
+
if (appdata) return path.join(appdata, "context-mode", "platform.json");
|
|
31
|
+
// Fallback if APPDATA unset (rare — Git Bash without env)
|
|
32
|
+
return path.join(os.homedir(), "AppData", "Roaming", "context-mode", "platform.json");
|
|
33
|
+
}
|
|
34
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
35
|
+
return path.join(process.env.XDG_CONFIG_HOME, "context-mode", "platform.json");
|
|
36
|
+
}
|
|
37
|
+
return path.join(os.homedir(), ".context-mode", "platform.json");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function warn(msg) {
|
|
41
|
+
if (_warned) return;
|
|
42
|
+
_warned = true;
|
|
43
|
+
process.stderr.write(`[context-mode] ${msg}\n`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// v1 events_url + token alias kept for Upgrade Lag (PRD §5.3).
|
|
47
|
+
// Stray fields (version, hostname, device_id, endpoints.sessions, routes) silently ignored.
|
|
48
|
+
function normalizeConfig(raw) {
|
|
49
|
+
if (!raw || typeof raw !== "object") return null;
|
|
50
|
+
const api_key = raw.api_key ?? raw.token;
|
|
51
|
+
let platform_url = raw.platform_url;
|
|
52
|
+
if (!platform_url && raw.endpoints?.events) platform_url = String(raw.endpoints.events).replace(/\/events$/, "");
|
|
53
|
+
if (!platform_url && raw.events_url) platform_url = String(raw.events_url).replace(/\/events$/, "");
|
|
54
|
+
if (typeof api_key !== "string" || !api_key.startsWith("ctxm_")) return null;
|
|
55
|
+
if (typeof platform_url !== "string" || !platform_url) return null;
|
|
56
|
+
return { api_key, platform_url: platform_url.replace(/\/$/, "") };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readConfig() {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
// Cache hit — covers BOTH positive (config object) and negative (NO_CONFIG) results.
|
|
62
|
+
if (_cache !== null && now - _cacheLoadedAt < CACHE_TTL_MS) {
|
|
63
|
+
return _cache === NO_CONFIG ? null : _cache;
|
|
64
|
+
}
|
|
65
|
+
_cacheLoadedAt = now;
|
|
66
|
+
_fsLoads++;
|
|
67
|
+
const cfgPath = configPath();
|
|
68
|
+
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = fs.readFileSync(cfgPath, "utf8");
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (e.code !== "ENOENT") warn(`cannot read ${cfgPath}: ${e.code || e.message}`);
|
|
74
|
+
_cache = NO_CONFIG;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let parsed;
|
|
79
|
+
try {
|
|
80
|
+
parsed = JSON.parse(raw);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
warn(`${cfgPath} is not valid JSON: ${e.message}`);
|
|
83
|
+
_cache = NO_CONFIG;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const normalized = normalizeConfig(parsed);
|
|
88
|
+
if (!normalized) {
|
|
89
|
+
warn(`${cfgPath} schema invalid — expected {api_key (ctxm_*), platform_url}`);
|
|
90
|
+
_cache = NO_CONFIG;
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_warned = false; // success — re-arm warning for future failures
|
|
95
|
+
_cache = normalized;
|
|
96
|
+
return _cache;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// === Gate — cheap boolean for callers that want to skip allocations entirely ===
|
|
100
|
+
// `session-loaders.mjs` uses this BEFORE the per-event forwarding loop so the
|
|
101
|
+
// loop never executes when ~/.context-mode/platform.json is missing. Honors
|
|
102
|
+
// the same 60s TTL as readConfig() — first call hits the FS, subsequent calls
|
|
103
|
+
// within the TTL return the cached decision (positive or negative).
|
|
104
|
+
export function hasPlatformConfig() {
|
|
105
|
+
return readConfig() !== null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// === URL construction (single endpoint per ADR-0006) ===
|
|
109
|
+
export function buildUrl(cfg, _eventType) {
|
|
110
|
+
return `${cfg.platform_url}/events`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// === Privacy: secret + PII redaction ===
|
|
114
|
+
const SECRETS = [
|
|
115
|
+
/\b(?:ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{20,}\b/g, // GitHub
|
|
116
|
+
/\bAKIA[0-9A-Z]{16}\b/g, // AWS
|
|
117
|
+
/\bsk-(?:ant|proj)?-?[A-Za-z0-9_-]{32,}\b/g, // OpenAI/Anthropic
|
|
118
|
+
/\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, // JWT
|
|
119
|
+
/\bxox[bpoas]-[A-Za-z0-9-]{10,}\b/g, // Slack
|
|
120
|
+
/\bglpat-[A-Za-z0-9_-]{20,}\b/g, // GitLab
|
|
121
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, // emails
|
|
122
|
+
/\b\d{3}-\d{2}-\d{4}\b/g, // SSN-like
|
|
123
|
+
];
|
|
124
|
+
const HOME = os.homedir();
|
|
125
|
+
const USER_DIR_RE = /(\/Users\/|\/home\/|\\+Users\\+)[^/\\]+/g;
|
|
126
|
+
|
|
127
|
+
function privacyTransform(s) {
|
|
128
|
+
if (typeof s !== "string") return s;
|
|
129
|
+
let out = s.split(HOME).join("<HOME>")
|
|
130
|
+
.replace(USER_DIR_RE, (m) => m.replace(/[^/\\]+$/, "<USER>"));
|
|
131
|
+
for (const re of SECRETS) out = out.replace(re, "[REDACTED]");
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function walk(obj, depth) {
|
|
136
|
+
if (depth > MAX_DEPTH) return "[depth-limited]";
|
|
137
|
+
if (obj == null) return obj;
|
|
138
|
+
if (typeof obj === "string") {
|
|
139
|
+
const cleaned = privacyTransform(obj);
|
|
140
|
+
return cleaned.length > MAX_FIELD_LEN ? cleaned.slice(0, MAX_FIELD_LEN) + "…[truncated]" : cleaned;
|
|
141
|
+
}
|
|
142
|
+
if (Array.isArray(obj)) return obj.slice(0, 50).map((x) => walk(x, depth + 1));
|
|
143
|
+
if (typeof obj === "object") {
|
|
144
|
+
const out = {};
|
|
145
|
+
for (const [k, v] of Object.entries(obj)) out[k] = walk(v, depth + 1);
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
return obj;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function sanitizeEvent(event) {
|
|
152
|
+
return event && typeof event === "object" ? walk(event, 0) : event;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// === Public API ===
|
|
156
|
+
export async function maybeForward(event, platform, opts = {}) {
|
|
157
|
+
const cfg = readConfig();
|
|
158
|
+
if (!cfg) return;
|
|
159
|
+
|
|
160
|
+
const ev = sanitizeEvent(event);
|
|
161
|
+
const ctrl = new AbortController();
|
|
162
|
+
const t = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const res = await fetch(buildUrl(cfg, ev.type), {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: {
|
|
168
|
+
"Authorization": `Bearer ${cfg.api_key}`,
|
|
169
|
+
"Content-Type": "application/json",
|
|
170
|
+
"X-Source-Platform": platform,
|
|
171
|
+
"X-Schema-Version": "2",
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
tool: ev.type,
|
|
175
|
+
category: ev.category,
|
|
176
|
+
error: ev.category === "error" ? 1 : 0,
|
|
177
|
+
ts: opts.ts ?? Math.floor(Date.now() / 1000),
|
|
178
|
+
platform,
|
|
179
|
+
project: opts.project,
|
|
180
|
+
session_category: ev.category,
|
|
181
|
+
session_type: ev.type,
|
|
182
|
+
session_data: typeof ev.data === "string" ? ev.data : undefined,
|
|
183
|
+
}),
|
|
184
|
+
signal: ctrl.signal,
|
|
185
|
+
});
|
|
186
|
+
if (res.status === 401) { _cache = null; _cacheLoadedAt = 0; }
|
|
187
|
+
else if (res.status === 429) {
|
|
188
|
+
process.stderr.write(`[context-mode-platform] rate limited (retry after ${res.headers.get("Retry-After")}s)\n`);
|
|
189
|
+
}
|
|
190
|
+
return { ok: res.ok, status: res.status };
|
|
191
|
+
} catch (e) {
|
|
192
|
+
return { ok: false, status: 0, error: e.message };
|
|
193
|
+
} finally {
|
|
194
|
+
clearTimeout(t);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const _internal = {
|
|
199
|
+
readConfig,
|
|
200
|
+
normalizeConfig,
|
|
201
|
+
buildUrl,
|
|
202
|
+
sanitizeEvent,
|
|
203
|
+
privacyTransform,
|
|
204
|
+
configPath,
|
|
205
|
+
resetState: () => { _cache = null; _cacheLoadedAt = 0; _warned = false; _fsLoads = 0; },
|
|
206
|
+
get fsLoads() { return _fsLoads; },
|
|
207
|
+
};
|
|
@@ -10,6 +10,9 @@ import { join } from "node:path";
|
|
|
10
10
|
import { pathToFileURL } from "node:url";
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
12
|
|
|
13
|
+
import { hasPlatformConfig, maybeForward } from "./platform-bridge.mjs";
|
|
14
|
+
import { detectPlatformFromEnv } from "./core/platform-detect.mjs";
|
|
15
|
+
|
|
13
16
|
export function createSessionLoaders(hookDir) {
|
|
14
17
|
// Auto-detect bundle directory: bundles live in hooks/ root, not platform subdirs.
|
|
15
18
|
// If hookDir itself has bundles, use it; otherwise go up one level.
|
|
@@ -96,5 +99,17 @@ export function attributeAndInsertEvents(db, sessionId, events, input, projectDi
|
|
|
96
99
|
db.insertEvent(sessionId, events[i], hookName, attributions[i], bytesList?.[i]);
|
|
97
100
|
}
|
|
98
101
|
}
|
|
102
|
+
|
|
103
|
+
// PRD-context-as-a-service §5.2 — Forwarder injection.
|
|
104
|
+
// Gated: the per-event loop never runs when ~/.context-mode/platform.json
|
|
105
|
+
// is missing. hasPlatformConfig() is a single cached probe (60s TTL), so
|
|
106
|
+
// the unconfigured-user path costs at most one syscall per minute.
|
|
107
|
+
if (hasPlatformConfig()) {
|
|
108
|
+
const platform = detectPlatformFromEnv();
|
|
109
|
+
for (let i = 0; i < events.length; i++) {
|
|
110
|
+
maybeForward({ ...events[i], session_id: sessionId, ...attributions[i] }, platform);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
99
114
|
return attributions;
|
|
100
115
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.155",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.155",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP plugin that saves 98% of your context window. Works with Claude Code, Gemini CLI, VS Code Copilot, OpenCode, and Codex CLI. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
|
|
6
6
|
"author": "Mert Koseoğlu",
|