context-mode 1.0.146 → 1.0.147
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 +2 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +26 -23
- package/bin/statusline.mjs +22 -9
- package/build/adapters/base.d.ts +9 -4
- package/build/adapters/base.js +16 -7
- package/build/adapters/codex/index.d.ts +1 -1
- package/build/adapters/codex/index.js +10 -4
- package/build/adapters/openclaw/index.d.ts +11 -2
- package/build/adapters/openclaw/index.js +12 -3
- package/build/adapters/pi/mcp-bridge.d.ts +8 -0
- package/build/adapters/pi/mcp-bridge.js +118 -15
- package/build/adapters/types.d.ts +7 -1
- package/build/cli.d.ts +2 -0
- package/build/cli.js +82 -19
- package/build/search/auto-memory.d.ts +6 -1
- package/build/search/auto-memory.js +11 -2
- package/build/server.js +305 -105
- package/build/session/db.d.ts +37 -0
- package/build/session/db.js +197 -2
- package/build/session/extract.js +16 -0
- package/build/truncate.d.ts +15 -0
- package/build/truncate.js +28 -0
- package/cli.bundle.mjs +424 -350
- package/hooks/core/routing.mjs +4 -4
- package/hooks/routing-block.mjs +18 -23
- package/hooks/session-db.bundle.mjs +21 -19
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +13 -2
- package/hooks/session-snapshot.bundle.mjs +7 -7
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -2
- package/server.bundle.mjs +372 -300
package/build/session/db.d.ts
CHANGED
|
@@ -8,6 +8,42 @@
|
|
|
8
8
|
import { SQLiteBase } from "../db-base.js";
|
|
9
9
|
import type { SessionEvent } from "../types.js";
|
|
10
10
|
import type { ProjectAttribution } from "./project-attribution.js";
|
|
11
|
+
declare const STORAGE_ROOT_ENV: "CONTEXT_MODE_DIR";
|
|
12
|
+
export type StorageDirectoryKind = "session" | "content" | "stats";
|
|
13
|
+
export type StorageOverrideEnvVar = typeof STORAGE_ROOT_ENV;
|
|
14
|
+
export type StorageDirectorySource = "default" | "override";
|
|
15
|
+
export type IgnoredStorageOverrideReason = "empty";
|
|
16
|
+
export interface ResolvedStorageDir {
|
|
17
|
+
kind: StorageDirectoryKind;
|
|
18
|
+
path: string;
|
|
19
|
+
envVar: StorageOverrideEnvVar | null;
|
|
20
|
+
source: StorageDirectorySource;
|
|
21
|
+
ignoredEnvVar?: StorageOverrideEnvVar;
|
|
22
|
+
ignoredReason?: IgnoredStorageOverrideReason;
|
|
23
|
+
}
|
|
24
|
+
export declare class StorageDirectoryError extends Error {
|
|
25
|
+
readonly kind: StorageDirectoryKind;
|
|
26
|
+
readonly path: string;
|
|
27
|
+
readonly overrideEnvVar: StorageOverrideEnvVar;
|
|
28
|
+
readonly ignoredEnvVar?: StorageOverrideEnvVar;
|
|
29
|
+
readonly ignoredReason?: IgnoredStorageOverrideReason;
|
|
30
|
+
constructor(kind: StorageDirectoryKind, path: string, overrideEnvVar?: StorageOverrideEnvVar, cause?: unknown, message?: string, metadata?: Pick<ResolvedStorageDir, "ignoredEnvVar" | "ignoredReason">);
|
|
31
|
+
}
|
|
32
|
+
export interface DefaultSessionDirOptions {
|
|
33
|
+
configDir: string;
|
|
34
|
+
configDirEnv?: string;
|
|
35
|
+
legacySessionDirEnv?: string;
|
|
36
|
+
onLegacySessionDir?: (envVar: string, dir: string) => void;
|
|
37
|
+
env?: NodeJS.ProcessEnv;
|
|
38
|
+
}
|
|
39
|
+
export declare function resolveDefaultSessionDir(opts: DefaultSessionDirOptions): string;
|
|
40
|
+
export declare function resolveSessionStorageDir(getDefaultDir: () => string): ResolvedStorageDir;
|
|
41
|
+
export declare function resolveContentStorageDir(getSessionDir: () => string): ResolvedStorageDir;
|
|
42
|
+
export declare function resolveStatsStorageDir(getDefaultSessionDir: () => string): ResolvedStorageDir;
|
|
43
|
+
export declare function formatStorageDirectoryError(err: StorageDirectoryError): string;
|
|
44
|
+
export declare function describeStorageDirectorySource(dir: ResolvedStorageDir): string;
|
|
45
|
+
export declare function clearStorageDirectoryCheckCacheForTests(): void;
|
|
46
|
+
export declare function ensureWritableStorageDir(dir: ResolvedStorageDir): string;
|
|
11
47
|
export declare function normalizeWorktreePath(path: string): string;
|
|
12
48
|
export declare function getWorktreeSuffix(projectDir?: string): string;
|
|
13
49
|
export declare function _resetWorktreeSuffixCacheForTests(): void;
|
|
@@ -314,3 +350,4 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
314
350
|
*/
|
|
315
351
|
cleanupOldSessions(maxAgeDays?: number): number;
|
|
316
352
|
}
|
|
353
|
+
export {};
|
package/build/session/db.js
CHANGED
|
@@ -8,8 +8,203 @@
|
|
|
8
8
|
import { SQLiteBase, defaultDBPath } from "../db-base.js";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
|
-
import { existsSync, realpathSync, renameSync } from "node:fs";
|
|
12
|
-
import {
|
|
11
|
+
import { accessSync, constants, existsSync, mkdirSync, realpathSync, renameSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
14
|
+
// ─────────────────────────────────────────────────────────
|
|
15
|
+
// Storage root resolution
|
|
16
|
+
// ─────────────────────────────────────────────────────────
|
|
17
|
+
//
|
|
18
|
+
// This lives beside the session DB path helpers because packaged hooks and the
|
|
19
|
+
// statusline already consume `hooks/session-db.bundle.mjs` as their no-build
|
|
20
|
+
// runtime bridge. Keeping the storage resolver here avoids adding a second
|
|
21
|
+
// generated hook bundle just to share CONTEXT_MODE_DIR behavior.
|
|
22
|
+
const STORAGE_ROOT_ENV = "CONTEXT_MODE_DIR";
|
|
23
|
+
const STORAGE_SESSIONS_SUBDIR = "sessions";
|
|
24
|
+
const STORAGE_CONTENT_SUBDIR = "content";
|
|
25
|
+
export class StorageDirectoryError extends Error {
|
|
26
|
+
kind;
|
|
27
|
+
path;
|
|
28
|
+
overrideEnvVar;
|
|
29
|
+
ignoredEnvVar;
|
|
30
|
+
ignoredReason;
|
|
31
|
+
constructor(kind, path, overrideEnvVar = STORAGE_ROOT_ENV, cause, message, metadata = {}) {
|
|
32
|
+
super(message ?? storageDirectoryErrorMessage(kind, path, metadata), { cause });
|
|
33
|
+
this.name = "StorageDirectoryError";
|
|
34
|
+
this.kind = kind;
|
|
35
|
+
this.path = path;
|
|
36
|
+
this.overrideEnvVar = overrideEnvVar;
|
|
37
|
+
this.ignoredEnvVar = metadata.ignoredEnvVar;
|
|
38
|
+
this.ignoredReason = metadata.ignoredReason;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const writableStorageCache = new Map();
|
|
42
|
+
export function resolveDefaultSessionDir(opts) {
|
|
43
|
+
const env = opts.env ?? process.env;
|
|
44
|
+
const legacyEnvVar = opts.legacySessionDirEnv;
|
|
45
|
+
const legacy = legacyEnvVar ? env[legacyEnvVar]?.trim() : undefined;
|
|
46
|
+
if (legacy && legacyEnvVar) {
|
|
47
|
+
opts.onLegacySessionDir?.(legacyEnvVar, legacy);
|
|
48
|
+
return legacy;
|
|
49
|
+
}
|
|
50
|
+
return join(resolveConfigDirForDefaultSession(opts.configDir, opts.configDirEnv, env), "context-mode", "sessions");
|
|
51
|
+
}
|
|
52
|
+
function resolveConfigDirForDefaultSession(configDir, configDirEnv, env) {
|
|
53
|
+
const envValue = configDirEnv ? env[configDirEnv] : undefined;
|
|
54
|
+
if (envValue && envValue.trim() !== "") {
|
|
55
|
+
return resolveConfigDirValue(envValue.trim());
|
|
56
|
+
}
|
|
57
|
+
return resolveConfigDirValue(configDir, homedir());
|
|
58
|
+
}
|
|
59
|
+
function resolveConfigDirValue(value, baseDir) {
|
|
60
|
+
if (value.startsWith("~"))
|
|
61
|
+
return resolve(homedir(), value.replace(/^~[/\\]?/, ""));
|
|
62
|
+
if (isAbsolute(value))
|
|
63
|
+
return resolve(value);
|
|
64
|
+
return baseDir ? resolve(baseDir, value) : resolve(value);
|
|
65
|
+
}
|
|
66
|
+
function invalidStorageOverride(kind, path, detail) {
|
|
67
|
+
return new StorageDirectoryError(kind, path, STORAGE_ROOT_ENV, undefined, [`Invalid ${STORAGE_ROOT_ENV} for context-mode ${kind} directory: ${detail}`, storageDirectoryHint()].join("\n"));
|
|
68
|
+
}
|
|
69
|
+
function storageOverrideRoot(kind) {
|
|
70
|
+
const raw = process.env[STORAGE_ROOT_ENV];
|
|
71
|
+
if (raw === undefined)
|
|
72
|
+
return { kind: "unset" };
|
|
73
|
+
const trimmed = raw.trim();
|
|
74
|
+
if (!trimmed) {
|
|
75
|
+
return { kind: "ignored-empty", ignoredEnvVar: STORAGE_ROOT_ENV, ignoredReason: "empty" };
|
|
76
|
+
}
|
|
77
|
+
if (!isAbsolute(trimmed)) {
|
|
78
|
+
throw invalidStorageOverride(kind, trimmed, `${STORAGE_ROOT_ENV} must be an absolute path.`);
|
|
79
|
+
}
|
|
80
|
+
return { kind: "override", root: resolve(trimmed) };
|
|
81
|
+
}
|
|
82
|
+
function ignoredStorageMetadata(root) {
|
|
83
|
+
return root.kind === "ignored-empty"
|
|
84
|
+
? { ignoredEnvVar: root.ignoredEnvVar, ignoredReason: root.ignoredReason }
|
|
85
|
+
: {};
|
|
86
|
+
}
|
|
87
|
+
function overrideStorageDir(kind, subdir) {
|
|
88
|
+
const root = storageOverrideRoot(kind);
|
|
89
|
+
if (root.kind !== "override")
|
|
90
|
+
return null;
|
|
91
|
+
return {
|
|
92
|
+
kind,
|
|
93
|
+
path: join(root.root, subdir),
|
|
94
|
+
envVar: STORAGE_ROOT_ENV,
|
|
95
|
+
source: "override",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function defaultStorageDir(kind, getDefaultDir, metadata) {
|
|
99
|
+
return {
|
|
100
|
+
kind,
|
|
101
|
+
path: resolve(getDefaultDir()),
|
|
102
|
+
envVar: null,
|
|
103
|
+
source: "default",
|
|
104
|
+
...metadata,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function resolveSessionStorageDir(getDefaultDir) {
|
|
108
|
+
const root = storageOverrideRoot("session");
|
|
109
|
+
if (root.kind === "override") {
|
|
110
|
+
return {
|
|
111
|
+
kind: "session",
|
|
112
|
+
path: join(root.root, STORAGE_SESSIONS_SUBDIR),
|
|
113
|
+
envVar: STORAGE_ROOT_ENV,
|
|
114
|
+
source: "override",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return defaultStorageDir("session", getDefaultDir, ignoredStorageMetadata(root));
|
|
118
|
+
}
|
|
119
|
+
export function resolveContentStorageDir(getSessionDir) {
|
|
120
|
+
const override = overrideStorageDir("content", STORAGE_CONTENT_SUBDIR);
|
|
121
|
+
if (override)
|
|
122
|
+
return override;
|
|
123
|
+
const session = resolveSessionStorageDir(getSessionDir);
|
|
124
|
+
return {
|
|
125
|
+
kind: "content",
|
|
126
|
+
path: join(dirname(session.path), STORAGE_CONTENT_SUBDIR),
|
|
127
|
+
envVar: session.envVar,
|
|
128
|
+
source: session.source,
|
|
129
|
+
ignoredEnvVar: session.ignoredEnvVar,
|
|
130
|
+
ignoredReason: session.ignoredReason,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function resolveStatsStorageDir(getDefaultSessionDir) {
|
|
134
|
+
const override = overrideStorageDir("stats", STORAGE_SESSIONS_SUBDIR);
|
|
135
|
+
if (override)
|
|
136
|
+
return override;
|
|
137
|
+
const session = resolveSessionStorageDir(getDefaultSessionDir);
|
|
138
|
+
return {
|
|
139
|
+
kind: "stats",
|
|
140
|
+
path: session.path,
|
|
141
|
+
envVar: session.envVar,
|
|
142
|
+
source: session.source,
|
|
143
|
+
ignoredEnvVar: session.ignoredEnvVar,
|
|
144
|
+
ignoredReason: session.ignoredReason,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function formatStorageDirectoryError(err) {
|
|
148
|
+
return err.message;
|
|
149
|
+
}
|
|
150
|
+
export function describeStorageDirectorySource(dir) {
|
|
151
|
+
if (dir.source === "override" && dir.envVar)
|
|
152
|
+
return `via ${dir.envVar}`;
|
|
153
|
+
if (dir.ignoredEnvVar && dir.ignoredReason === "empty")
|
|
154
|
+
return `default; ignored empty ${dir.ignoredEnvVar}`;
|
|
155
|
+
return "default";
|
|
156
|
+
}
|
|
157
|
+
export function clearStorageDirectoryCheckCacheForTests() {
|
|
158
|
+
writableStorageCache.clear();
|
|
159
|
+
}
|
|
160
|
+
export function ensureWritableStorageDir(dir) {
|
|
161
|
+
const key = [
|
|
162
|
+
dir.kind,
|
|
163
|
+
dir.path,
|
|
164
|
+
dir.source,
|
|
165
|
+
dir.envVar ?? "",
|
|
166
|
+
dir.ignoredEnvVar ?? "",
|
|
167
|
+
dir.ignoredReason ?? "",
|
|
168
|
+
].join("\0");
|
|
169
|
+
const cached = writableStorageCache.get(key);
|
|
170
|
+
if (cached instanceof StorageDirectoryError)
|
|
171
|
+
throw cached;
|
|
172
|
+
if (cached === dir.path)
|
|
173
|
+
return cached;
|
|
174
|
+
try {
|
|
175
|
+
mkdirSync(dir.path, { recursive: true });
|
|
176
|
+
accessSync(dir.path, constants.W_OK);
|
|
177
|
+
writableStorageCache.set(key, dir.path);
|
|
178
|
+
return dir.path;
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
const storageErr = new StorageDirectoryError(dir.kind, pathFromStorageError(err) ?? dir.path, STORAGE_ROOT_ENV, err, undefined, { ignoredEnvVar: dir.ignoredEnvVar, ignoredReason: dir.ignoredReason });
|
|
182
|
+
writableStorageCache.set(key, storageErr);
|
|
183
|
+
throw storageErr;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function storageDirectoryErrorMessage(kind, path, metadata = {}) {
|
|
187
|
+
return [
|
|
188
|
+
`context-mode ${kind} directory is not writable: ${path}`,
|
|
189
|
+
ignoredStorageOverrideHint(metadata),
|
|
190
|
+
storageDirectoryHint(),
|
|
191
|
+
].filter(Boolean).join("\n");
|
|
192
|
+
}
|
|
193
|
+
function ignoredStorageOverrideHint(metadata) {
|
|
194
|
+
if (metadata.ignoredEnvVar && metadata.ignoredReason === "empty") {
|
|
195
|
+
return `Ignored empty ${metadata.ignoredEnvVar}; using adapter default.`;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
function storageDirectoryHint() {
|
|
200
|
+
return `Set ${STORAGE_ROOT_ENV} to a writable absolute path.`;
|
|
201
|
+
}
|
|
202
|
+
function pathFromStorageError(err) {
|
|
203
|
+
if (!err || typeof err !== "object")
|
|
204
|
+
return null;
|
|
205
|
+
const path = err.path;
|
|
206
|
+
return typeof path === "string" && path.length > 0 ? path : null;
|
|
207
|
+
}
|
|
13
208
|
// ─────────────────────────────────────────────────────────
|
|
14
209
|
// Worktree isolation
|
|
15
210
|
// ─────────────────────────────────────────────────────────
|
package/build/session/extract.js
CHANGED
|
@@ -19,6 +19,22 @@ function safeStringAny(value) {
|
|
|
19
19
|
}
|
|
20
20
|
function isToolError(input) {
|
|
21
21
|
const response = String(input.tool_response ?? "");
|
|
22
|
+
// PreToolUse rewrites curl/wget/inline-HTTP/WebFetch commands into
|
|
23
|
+
// echo "context-mode: <guidance text including 'retry', 'fails', 'error'>"
|
|
24
|
+
// The user-facing copy legitimately mentions failure modes ("retry if it
|
|
25
|
+
// fails with a transient DNS error"), but those words must NOT classify
|
|
26
|
+
// our OWN guidance message as a tool error or it gets captured into
|
|
27
|
+
// session_resume and surfaces as a fake error in the next chat.
|
|
28
|
+
// We check BOTH sides because:
|
|
29
|
+
// - real shell run → response starts with `context-mode:` (echo stdout)
|
|
30
|
+
// - test/captured-output path → response is the raw command itself
|
|
31
|
+
// (`echo "context-mode: …"`), so we also match the command shape
|
|
32
|
+
const command = String(input.tool_input?.command ?? "");
|
|
33
|
+
if (response.startsWith("context-mode:") ||
|
|
34
|
+
command.startsWith('echo "context-mode:') ||
|
|
35
|
+
command.startsWith("echo 'context-mode:")) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
22
38
|
const isErrorFlag = input.tool_output?.isError === true || input.tool_output?.is_error === true;
|
|
23
39
|
const isBashError = input.tool_name === "Bash" &&
|
|
24
40
|
/exit code [1-9]|error:|Error:|FAIL|failed/i.test(response);
|
package/build/truncate.d.ts
CHANGED
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
* SessionDB (snapshot building). They are extracted here so any
|
|
6
6
|
* consumer can import them without pulling in the full store or executor.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Return a prefix of `str` no longer than `maxChars` UTF-16 code units, with
|
|
10
|
+
* the cut backed off by one unit if it would split a surrogate pair. Mirrors
|
|
11
|
+
* the surrogate-safety semantics of the internal `byteSafePrefix`, but uses
|
|
12
|
+
* a character budget rather than a byte budget.
|
|
13
|
+
*
|
|
14
|
+
* Use this instead of bare `str.slice(0, n)` whenever the result is later
|
|
15
|
+
* JSON-encoded for an RFC 8259-strict consumer (e.g. tool_result payloads
|
|
16
|
+
* sent back to the host LLM API). `JSON.stringify` will emit a lone high
|
|
17
|
+
* surrogate as a literal `\uD8xx` escape, which is invalid JSON.
|
|
18
|
+
*
|
|
19
|
+
* @param str - Input string.
|
|
20
|
+
* @param maxChars - Hard cap in UTF-16 code units.
|
|
21
|
+
*/
|
|
22
|
+
export declare function charSafePrefix(str: string, maxChars: number): string;
|
|
8
23
|
/**
|
|
9
24
|
* Serialize a value to JSON, then truncate the result to `maxBytes` bytes.
|
|
10
25
|
* If truncation occurs, the string is cut at a UTF-8-safe boundary and
|
package/build/truncate.js
CHANGED
|
@@ -43,6 +43,34 @@ function byteSafePrefix(str, maxBytes) {
|
|
|
43
43
|
return str.slice(0, lo);
|
|
44
44
|
}
|
|
45
45
|
// ─────────────────────────────────────────────────────────
|
|
46
|
+
// Char-count prefix (UTF-16 surrogate-safe)
|
|
47
|
+
// ─────────────────────────────────────────────────────────
|
|
48
|
+
/**
|
|
49
|
+
* Return a prefix of `str` no longer than `maxChars` UTF-16 code units, with
|
|
50
|
+
* the cut backed off by one unit if it would split a surrogate pair. Mirrors
|
|
51
|
+
* the surrogate-safety semantics of the internal `byteSafePrefix`, but uses
|
|
52
|
+
* a character budget rather than a byte budget.
|
|
53
|
+
*
|
|
54
|
+
* Use this instead of bare `str.slice(0, n)` whenever the result is later
|
|
55
|
+
* JSON-encoded for an RFC 8259-strict consumer (e.g. tool_result payloads
|
|
56
|
+
* sent back to the host LLM API). `JSON.stringify` will emit a lone high
|
|
57
|
+
* surrogate as a literal `\uD8xx` escape, which is invalid JSON.
|
|
58
|
+
*
|
|
59
|
+
* @param str - Input string.
|
|
60
|
+
* @param maxChars - Hard cap in UTF-16 code units.
|
|
61
|
+
*/
|
|
62
|
+
export function charSafePrefix(str, maxChars) {
|
|
63
|
+
if (maxChars <= 0)
|
|
64
|
+
return "";
|
|
65
|
+
if (str.length <= maxChars)
|
|
66
|
+
return str;
|
|
67
|
+
let end = maxChars;
|
|
68
|
+
const code = str.charCodeAt(end - 1);
|
|
69
|
+
if (code >= 0xd800 && code <= 0xdbff)
|
|
70
|
+
end -= 1;
|
|
71
|
+
return str.slice(0, end);
|
|
72
|
+
}
|
|
73
|
+
// ─────────────────────────────────────────────────────────
|
|
46
74
|
// JSON truncation
|
|
47
75
|
// ─────────────────────────────────────────────────────────
|
|
48
76
|
/**
|