@poolzin/pool-bot 2026.3.7 → 2026.3.9

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/agents/error-classifier.js +302 -0
  4. package/dist/agents/skills/security.js +217 -0
  5. package/dist/build-info.json +3 -3
  6. package/dist/cli/lazy-commands.example.js +113 -0
  7. package/dist/cli/lazy-commands.js +329 -0
  8. package/dist/cli/program/command-registry.js +13 -0
  9. package/dist/cli/program/register.skills.js +4 -0
  10. package/dist/config/config.js +1 -0
  11. package/dist/config/secrets-integration.js +88 -0
  12. package/dist/context-engine/index.js +33 -0
  13. package/dist/context-engine/legacy.js +181 -0
  14. package/dist/context-engine/registry.js +86 -0
  15. package/dist/context-engine/summarizing.js +293 -0
  16. package/dist/context-engine/types.js +7 -0
  17. package/dist/infra/abort-pattern.js +106 -0
  18. package/dist/infra/retry.js +94 -0
  19. package/dist/secrets/index.js +28 -0
  20. package/dist/secrets/resolver.js +185 -0
  21. package/dist/secrets/runtime.js +142 -0
  22. package/dist/secrets/types.js +11 -0
  23. package/dist/security/dangerous-tools.js +80 -0
  24. package/dist/security/types.js +12 -0
  25. package/dist/skills/commands.js +351 -0
  26. package/dist/skills/index.js +167 -0
  27. package/dist/skills/loader.js +282 -0
  28. package/dist/skills/parser.js +461 -0
  29. package/dist/skills/registry.js +397 -0
  30. package/dist/skills/security.js +318 -0
  31. package/dist/skills/types.js +21 -0
  32. package/dist/test-utils/index.js +219 -0
  33. package/dist/tui/index.js +595 -0
  34. package/docs/INTEGRATION_PLAN.md +475 -0
  35. package/docs/INTEGRATION_SUMMARY.md +215 -0
  36. package/docs/integrations/HEXSTRIKE_PLAN.md +796 -0
  37. package/docs/integrations/INTEGRATION_PLAN.md +424 -0
  38. package/docs/integrations/PAGE_AGENT_PLAN.md +370 -0
  39. package/docs/integrations/XYOPS_PLAN.md +978 -0
  40. package/docs/skills/IMPLEMENTATION_SUMMARY.md +145 -0
  41. package/docs/skills/SKILL.md +524 -0
  42. package/docs/skills.md +405 -0
  43. package/package.json +1 -1
  44. package/skills/example-skill/SKILL.md +195 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Lazy Command Loading System
3
+ *
4
+ * Optimizes CLI startup time by loading commands on-demand
5
+ * rather than loading all commands at startup.
6
+ *
7
+ * This significantly improves startup performance when only
8
+ * a subset of commands is used.
9
+ *
10
+ * ## Usage
11
+ *
12
+ * ```typescript
13
+ * import { Command } from "commander";
14
+ * import { lazyCommand } from "./lazy-commands.js";
15
+ *
16
+ * const program = new Command();
17
+ *
18
+ * // Register lazy-loaded command
19
+ * lazyCommand(program, "agent", "Manage AI agents", async () => {
20
+ * const { buildAgentCommand } = await import("./commands/agent.js");
21
+ * return buildAgentCommand();
22
+ * });
23
+ * ```
24
+ */
25
+ /**
26
+ * Global registry for lazy commands
27
+ */
28
+ class LazyCommandRegistry {
29
+ commands = new Map();
30
+ loadTimes = new Map();
31
+ /**
32
+ * Register a lazy command
33
+ */
34
+ register(options) {
35
+ if (this.commands.has(options.name)) {
36
+ throw new Error(`Command "${options.name}" is already registered`);
37
+ }
38
+ const entry = {
39
+ name: options.name,
40
+ description: options.description,
41
+ loader: options.loader,
42
+ aliases: options.aliases ?? [],
43
+ loaded: false,
44
+ };
45
+ this.commands.set(options.name, entry);
46
+ // Register aliases pointing to main entry
47
+ for (const alias of options.aliases ?? []) {
48
+ if (this.commands.has(alias)) {
49
+ throw new Error(`Alias "${alias}" is already registered`);
50
+ }
51
+ this.commands.set(alias, entry);
52
+ }
53
+ }
54
+ /**
55
+ * Check if a command is registered
56
+ */
57
+ has(name) {
58
+ return this.commands.has(name);
59
+ }
60
+ /**
61
+ * Get a registered command entry
62
+ */
63
+ get(name) {
64
+ return this.commands.get(name);
65
+ }
66
+ /**
67
+ * Get command loading statistics
68
+ */
69
+ getStats() {
70
+ const uniqueCommands = new Set(Array.from(this.commands.values()).map((c) => c.name));
71
+ const total = uniqueCommands.size;
72
+ const loadedEntries = Array.from(this.commands.values()).filter((c) => c.loaded && !c.aliases.includes(c.name) // Count main entries only
73
+ );
74
+ const loaded = loadedEntries.length;
75
+ const loadTimes = {};
76
+ for (const [name, time] of this.loadTimes) {
77
+ loadTimes[name] = time;
78
+ }
79
+ const times = Object.values(loadTimes);
80
+ const averageLoadTime = times.length > 0 ? times.reduce((a, b) => a + b, 0) / times.length : undefined;
81
+ return {
82
+ total,
83
+ loaded,
84
+ lazy: total - loaded,
85
+ averageLoadTime,
86
+ loadTimes,
87
+ };
88
+ }
89
+ /**
90
+ * Mark a command as loaded with its load time
91
+ */
92
+ markLoaded(name, loadTime) {
93
+ const entry = this.commands.get(name);
94
+ if (entry) {
95
+ entry.loaded = true;
96
+ entry.loadTime = loadTime;
97
+ this.loadTimes.set(name, loadTime);
98
+ }
99
+ }
100
+ /**
101
+ * Mark a command as failed to load
102
+ */
103
+ markFailed(name, error) {
104
+ const entry = this.commands.get(name);
105
+ if (entry) {
106
+ entry.error = error;
107
+ }
108
+ }
109
+ /**
110
+ * Get list of registered command names (main commands only, not aliases)
111
+ */
112
+ getCommandNames() {
113
+ const seen = new Set();
114
+ const names = [];
115
+ for (const entry of this.commands.values()) {
116
+ if (!seen.has(entry.name)) {
117
+ seen.add(entry.name);
118
+ names.push(entry.name);
119
+ }
120
+ }
121
+ return names;
122
+ }
123
+ /**
124
+ * Get all entries (for debugging)
125
+ */
126
+ getAllEntries() {
127
+ return Array.from(this.commands.values());
128
+ }
129
+ /**
130
+ * Clear all registrations
131
+ */
132
+ clear() {
133
+ this.commands.clear();
134
+ this.loadTimes.clear();
135
+ }
136
+ }
137
+ /**
138
+ * Global registry instance
139
+ */
140
+ export const globalLazyCommandRegistry = new LazyCommandRegistry();
141
+ /**
142
+ * Register a lazy-loaded command with a Commander program
143
+ *
144
+ * This creates a placeholder command that will load the actual
145
+ * implementation only when the command is invoked.
146
+ *
147
+ * @param program - The Commander program instance
148
+ * @param options - Lazy command configuration
149
+ * @returns The placeholder command (for chaining)
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * lazyCommand(program, {
154
+ * name: "agent",
155
+ * description: "Manage AI agents",
156
+ * loader: async () => {
157
+ * const { buildAgentCommand } = await import("./commands/agent.js");
158
+ * return buildAgentCommand();
159
+ * },
160
+ * aliases: ["a"]
161
+ * });
162
+ * ```
163
+ */
164
+ export function lazyCommand(program, options) {
165
+ // Register in global registry
166
+ globalLazyCommandRegistry.register(options);
167
+ // Create placeholder command
168
+ const placeholder = program
169
+ .command(options.name)
170
+ .description(options.description);
171
+ // Add arguments if specified
172
+ if (options.arguments) {
173
+ placeholder.arguments(options.arguments);
174
+ }
175
+ // Add aliases
176
+ for (const alias of options.aliases ?? []) {
177
+ placeholder.alias(alias);
178
+ }
179
+ // Set up lazy loading action
180
+ placeholder.action(async (...actionArgs) => {
181
+ const startTime = performance.now();
182
+ try {
183
+ // Load the actual command module
184
+ const realCommand = await options.loader();
185
+ // Calculate load time
186
+ const loadTime = performance.now() - startTime;
187
+ globalLazyCommandRegistry.markLoaded(options.name, loadTime);
188
+ // Execute the loaded command's action with the same arguments
189
+ // The loaded command should have its own action handler
190
+ if (realCommand && typeof realCommand.parseAsync === "function") {
191
+ // If it's a full Command instance, we need to execute its action
192
+ // Get the action handler from the loaded command
193
+ const actionHandler = realCommand._actionHandler;
194
+ if (actionHandler) {
195
+ await actionHandler(...actionArgs);
196
+ }
197
+ else {
198
+ // Fallback: parse with the same arguments
199
+ // Remove command object from args and re-parse
200
+ const argv = process.argv.slice(2);
201
+ await realCommand.parseAsync(argv);
202
+ }
203
+ }
204
+ else {
205
+ throw new Error(`Loader for "${options.name}" did not return a valid Command instance`);
206
+ }
207
+ }
208
+ catch (error) {
209
+ const err = error instanceof Error ? error : new Error(String(error));
210
+ globalLazyCommandRegistry.markFailed(options.name, err);
211
+ throw new Error(`Failed to load command "${options.name}": ${err.message}`);
212
+ }
213
+ });
214
+ return placeholder;
215
+ }
216
+ /**
217
+ * Simple shorthand for creating a lazy command with minimal config
218
+ *
219
+ * @param program - The Commander program
220
+ * @param name - Command name
221
+ * @param description - Command description
222
+ * @param loader - Async function that loads and returns the command
223
+ * @returns The placeholder command
224
+ *
225
+ * @example
226
+ * ```typescript
227
+ * lazy(program, "agent", "Manage agents", async () => {
228
+ * const mod = await import("./commands/agent.js");
229
+ * return mod.buildAgentCommand();
230
+ * });
231
+ * ```
232
+ */
233
+ export function lazy(program, name, description, loader) {
234
+ return lazyCommand(program, { name, description, loader });
235
+ }
236
+ /**
237
+ * Preload commands that are likely to be used
238
+ *
239
+ * This can be called after initial setup to preload commands
240
+ * in the background, improving responsiveness for common commands.
241
+ *
242
+ * @param commandNames - Names of commands to preload
243
+ * @returns Promise that resolves when all preloads are complete
244
+ */
245
+ export async function preloadCommands(commandNames) {
246
+ await Promise.all(commandNames.map(async (name) => {
247
+ const entry = globalLazyCommandRegistry.get(name);
248
+ if (entry && !entry.loaded) {
249
+ try {
250
+ const startTime = performance.now();
251
+ await entry.loader();
252
+ globalLazyCommandRegistry.markLoaded(name, performance.now() - startTime);
253
+ }
254
+ catch (error) {
255
+ // Preload errors are non-fatal
256
+ console.warn(`Failed to preload command "${name}": ${error instanceof Error ? error.message : String(error)}`);
257
+ }
258
+ }
259
+ }));
260
+ }
261
+ /**
262
+ * Eagerly load all registered commands
263
+ *
264
+ * Use this for testing or when startup time doesn't matter.
265
+ *
266
+ * @returns Promise that resolves when all commands are loaded
267
+ */
268
+ export async function loadAllCommands() {
269
+ const names = globalLazyCommandRegistry.getCommandNames();
270
+ await preloadCommands(names);
271
+ }
272
+ /**
273
+ * Get current loading statistics
274
+ *
275
+ * @returns Statistics about lazy-loaded commands
276
+ */
277
+ export function getLazyCommandStats() {
278
+ return globalLazyCommandRegistry.getStats();
279
+ }
280
+ /**
281
+ * Performance measurement utilities for command loading
282
+ */
283
+ export class LoadTimeTracker {
284
+ measurements = new Map();
285
+ /**
286
+ * Measure execution time of a function
287
+ */
288
+ async measure(name, fn) {
289
+ const start = performance.now();
290
+ try {
291
+ return await fn();
292
+ }
293
+ finally {
294
+ const duration = performance.now() - start;
295
+ this.measurements.set(name, duration);
296
+ }
297
+ }
298
+ /**
299
+ * Get all measurements
300
+ */
301
+ getMeasurements() {
302
+ return Object.fromEntries(this.measurements);
303
+ }
304
+ /**
305
+ * Get total loading time
306
+ */
307
+ getTotalTime() {
308
+ return Array.from(this.measurements.values()).reduce((a, b) => a + b, 0);
309
+ }
310
+ /**
311
+ * Get slowest operations
312
+ */
313
+ getSlowest(count = 5) {
314
+ return Array.from(this.measurements.entries())
315
+ .sort((a, b) => b[1] - a[1])
316
+ .slice(0, count)
317
+ .map(([name, time]) => ({ name, time }));
318
+ }
319
+ /**
320
+ * Clear all measurements
321
+ */
322
+ clear() {
323
+ this.measurements.clear();
324
+ }
325
+ }
326
+ /**
327
+ * Global load time tracker
328
+ */
329
+ export const globalLoadTimeTracker = new LoadTimeTracker();
@@ -174,6 +174,19 @@ const coreEntries = [
174
174
  mod.registerBrowserCli(program);
175
175
  },
176
176
  },
177
+ {
178
+ commands: [
179
+ {
180
+ name: "mods",
181
+ description: "Manage capability modules (SKILL.md discovery, enable, configure)",
182
+ hasSubcommands: true,
183
+ },
184
+ ],
185
+ register: async ({ program }) => {
186
+ const mod = await import("./register.skills.js");
187
+ mod.registerSkillsCli(program);
188
+ },
189
+ },
177
190
  ];
178
191
  function collectCoreCliCommandNames(predicate) {
179
192
  const seen = new Set();
@@ -0,0 +1,4 @@
1
+ import { registerSkillsCommands } from "../../skills/commands.js";
2
+ export function registerSkillsCli(program) {
3
+ registerSkillsCommands(program);
4
+ }
@@ -2,6 +2,7 @@ export { createConfigIO, loadConfig, parseConfigJson5, readConfigFileSnapshot, r
2
2
  export { migrateLegacyConfig } from "./legacy-migrate.js";
3
3
  export * from "./paths.js";
4
4
  export * from "./runtime-overrides.js";
5
+ export * from "./secrets-integration.js";
5
6
  export * from "./types.js";
6
7
  export { validateConfigObject, validateConfigObjectWithPlugins } from "./validation.js";
7
8
  export { PoolBotSchema } from "./zod-schema.js";
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Config Secrets Integration
3
+ *
4
+ * Integrates the secrets management system with config loading.
5
+ * Resolves SecretRefs in config values at runtime.
6
+ */
7
+ import { SecretsRuntime } from "../secrets/index.js";
8
+ const SECRET_PATHS = [
9
+ "agent.model",
10
+ "agent.apiKey",
11
+ "channels.telegram.botToken",
12
+ "channels.discord.token",
13
+ "channels.slack.botToken",
14
+ "channels.slack.appToken",
15
+ "gateway.auth.token",
16
+ "gateway.auth.password",
17
+ "memory.apiKey",
18
+ "tools.browser.apiKey",
19
+ ];
20
+ /**
21
+ * Deeply traverse config and resolve secret references
22
+ */
23
+ export function resolveConfigSecrets(config, secretsRuntime) {
24
+ const resolved = { ...config };
25
+ // Resolve specific known secret paths
26
+ for (const path of SECRET_PATHS) {
27
+ resolvePath(resolved, path.split("."), secretsRuntime);
28
+ }
29
+ // Also scan for any string values that look like secret refs
30
+ scanAndResolveSecrets(resolved, "", secretsRuntime);
31
+ return resolved;
32
+ }
33
+ function resolvePath(obj, path, runtime) {
34
+ if (path.length === 0)
35
+ return;
36
+ const [head, ...tail] = path;
37
+ if (!(head in obj))
38
+ return;
39
+ if (tail.length === 0) {
40
+ // Leaf node - resolve if it's a string
41
+ const value = obj[head];
42
+ if (typeof value === "string") {
43
+ const result = runtime.resolve(head, value);
44
+ if (result.value !== undefined) {
45
+ obj[head] = result.value;
46
+ }
47
+ }
48
+ }
49
+ else {
50
+ // Traverse deeper
51
+ const child = obj[head];
52
+ if (typeof child === "object" && child !== null && !Array.isArray(child)) {
53
+ resolvePath(child, tail, runtime);
54
+ }
55
+ }
56
+ }
57
+ function scanAndResolveSecrets(obj, path, runtime) {
58
+ if (typeof obj === "string") {
59
+ // Check if it looks like a secret ref
60
+ if (obj.startsWith("env:") || obj.startsWith("file:")) {
61
+ runtime.resolve(path || "unknown", obj);
62
+ }
63
+ return;
64
+ }
65
+ if (Array.isArray(obj)) {
66
+ obj.forEach((item, index) => {
67
+ scanAndResolveSecrets(item, `${path}[${index}]`, runtime);
68
+ });
69
+ return;
70
+ }
71
+ if (typeof obj === "object" && obj !== null) {
72
+ for (const [key, value] of Object.entries(obj)) {
73
+ const newPath = path ? `${path}.${key}` : key;
74
+ scanAndResolveSecrets(value, newPath, runtime);
75
+ }
76
+ }
77
+ }
78
+ /**
79
+ * Create a secrets runtime from config
80
+ */
81
+ export function createSecretsRuntimeFromConfig(_config, secretsConfig) {
82
+ return new SecretsRuntime({
83
+ allowDirectValues: true,
84
+ warnOnDirectValues: true,
85
+ failOnUnresolved: false,
86
+ ...secretsConfig,
87
+ });
88
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Context Engine - Pluggable conversation context management
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { LegacyContextEngine, globalContextEngineRegistry } from './context-engine/index.js';
7
+ *
8
+ * // Register the legacy engine
9
+ * globalContextEngineRegistry.register('legacy', LegacyContextEngine);
10
+ *
11
+ * // Get engine instance
12
+ * const engine = globalContextEngineRegistry.getEngine('legacy');
13
+ *
14
+ * // Bootstrap for a session
15
+ * await engine.bootstrap({ sessionId: 'session-123' });
16
+ *
17
+ * // Ingest a message
18
+ * await engine.ingest({
19
+ * sessionId: 'session-123',
20
+ * message: { role: 'user', content: 'Hello!' }
21
+ * });
22
+ *
23
+ * // Assemble context
24
+ * const result = await engine.assemble({
25
+ * sessionId: 'session-123',
26
+ * messages: []
27
+ * });
28
+ * ```
29
+ */
30
+ // Registry
31
+ export { ContextEngineRegistry, globalContextEngineRegistry, resolveContextEngine, } from "./registry.js";
32
+ // Legacy Engine
33
+ export { LegacyContextEngine, createLegacyContextEngine, } from "./legacy.js";
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Legacy Context Engine
3
+ *
4
+ * Maintains PoolBot's original context management behavior.
5
+ * This is the default engine that preserves backward compatibility.
6
+ *
7
+ * Strategy:
8
+ * - Simple message accumulation
9
+ * - Truncation-based overflow handling
10
+ * - Basic compaction via summarization
11
+ */
12
+ export class LegacyContextEngine {
13
+ info = {
14
+ id: "legacy",
15
+ name: "Legacy Context Engine",
16
+ description: "PoolBot's original context management with simple accumulation and truncation",
17
+ supportsVectorSearch: false,
18
+ supportsFullTextSearch: false,
19
+ tokenLimit: undefined, // No explicit limit
20
+ };
21
+ sessions = new Map();
22
+ async bootstrap(params) {
23
+ try {
24
+ // Initialize session storage
25
+ this.sessions.set(params.sessionId, {
26
+ messages: [],
27
+ lastAccessed: new Date(),
28
+ });
29
+ return {
30
+ success: true,
31
+ };
32
+ }
33
+ catch (error) {
34
+ return {
35
+ success: false,
36
+ error: error instanceof Error ? error.message : String(error),
37
+ };
38
+ }
39
+ }
40
+ async ingest(params) {
41
+ try {
42
+ const session = this.sessions.get(params.sessionId);
43
+ if (!session) {
44
+ return {
45
+ success: false,
46
+ error: `Session ${params.sessionId} not found`,
47
+ };
48
+ }
49
+ session.messages.push(params.message);
50
+ session.lastAccessed = new Date();
51
+ // Estimate tokens (rough approximation)
52
+ const content = typeof params.message.content === "string"
53
+ ? params.message.content
54
+ : JSON.stringify(params.message.content);
55
+ const estimatedTokens = Math.ceil(content.length / 4);
56
+ return {
57
+ success: true,
58
+ tokensConsumed: estimatedTokens,
59
+ };
60
+ }
61
+ catch (error) {
62
+ return {
63
+ success: false,
64
+ error: error instanceof Error ? error.message : String(error),
65
+ };
66
+ }
67
+ }
68
+ async assemble(params) {
69
+ const session = this.sessions.get(params.sessionId);
70
+ const messages = session?.messages ?? params.messages;
71
+ // Apply token budget if specified
72
+ let resultMessages = messages;
73
+ let wasTruncated = false;
74
+ let excludedMessages = [];
75
+ if (params.tokenBudget && params.tokenBudget > 0) {
76
+ const estimate = this.estimateTokenCount(messages);
77
+ if (estimate > params.tokenBudget) {
78
+ // Simple truncation: keep newest messages that fit
79
+ const reversed = [...messages].reverse();
80
+ const fitting = [];
81
+ let currentTokens = 0;
82
+ for (const msg of reversed) {
83
+ const msgTokens = this.estimateTokenCount([msg]);
84
+ if (currentTokens + msgTokens <= params.tokenBudget) {
85
+ fitting.unshift(msg);
86
+ currentTokens += msgTokens;
87
+ }
88
+ else {
89
+ excludedMessages.unshift(msg);
90
+ }
91
+ }
92
+ resultMessages = fitting;
93
+ wasTruncated = true;
94
+ }
95
+ }
96
+ return {
97
+ messages: resultMessages,
98
+ totalTokens: this.estimateTokenCount(resultMessages),
99
+ wasTruncated,
100
+ excludedMessages: wasTruncated ? excludedMessages : undefined,
101
+ };
102
+ }
103
+ async compact(params) {
104
+ try {
105
+ const session = this.sessions.get(params.sessionId);
106
+ if (!session || session.messages.length === 0) {
107
+ return {
108
+ success: true,
109
+ tokensSaved: 0,
110
+ };
111
+ }
112
+ const originalTokens = this.estimateTokenCount(session.messages);
113
+ // Legacy compaction: summarize older messages
114
+ // Keep recent messages, summarize older ones
115
+ const keepRecent = 4; // Keep last 2 exchanges
116
+ const toSummarize = session.messages.slice(0, -keepRecent);
117
+ const toKeep = session.messages.slice(-keepRecent);
118
+ if (toSummarize.length === 0) {
119
+ return {
120
+ success: true,
121
+ tokensSaved: 0,
122
+ };
123
+ }
124
+ // Create summary message
125
+ const summary = {
126
+ role: "system",
127
+ content: `[${toSummarize.length} earlier messages summarized]`,
128
+ metadata: { compacted: true, originalCount: toSummarize.length },
129
+ };
130
+ session.messages = [summary, ...toKeep];
131
+ const newTokens = this.estimateTokenCount(session.messages);
132
+ return {
133
+ success: true,
134
+ summary: summary.content,
135
+ tokensSaved: originalTokens - newTokens,
136
+ };
137
+ }
138
+ catch (error) {
139
+ return {
140
+ success: false,
141
+ error: error instanceof Error ? error.message : String(error),
142
+ };
143
+ }
144
+ }
145
+ async cleanup(params) {
146
+ this.sessions.delete(params.sessionId);
147
+ }
148
+ async getStats(params) {
149
+ const session = this.sessions.get(params.sessionId);
150
+ if (!session) {
151
+ return {
152
+ messageCount: 0,
153
+ };
154
+ }
155
+ return {
156
+ messageCount: session.messages.length,
157
+ totalTokens: this.estimateTokenCount(session.messages),
158
+ lastAccessed: session.lastAccessed,
159
+ };
160
+ }
161
+ /**
162
+ * Estimate token count for messages
163
+ * Rough approximation: ~4 characters per token
164
+ */
165
+ estimateTokenCount(messages) {
166
+ let totalChars = 0;
167
+ for (const msg of messages) {
168
+ const content = typeof msg.content === "string"
169
+ ? msg.content
170
+ : JSON.stringify(msg.content);
171
+ totalChars += content.length;
172
+ }
173
+ return Math.ceil(totalChars / 4);
174
+ }
175
+ }
176
+ /**
177
+ * Create the default legacy context engine
178
+ */
179
+ export function createLegacyContextEngine() {
180
+ return new LegacyContextEngine();
181
+ }