@creatoria/miniapp-mcp 0.1.0
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/README.md +469 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +144 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/defaults.d.ts +73 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +118 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/loader.d.ts +50 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +189 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/core/element-ref.d.ts +44 -0
- package/dist/core/element-ref.d.ts.map +1 -0
- package/dist/core/element-ref.js +213 -0
- package/dist/core/element-ref.js.map +1 -0
- package/dist/core/logger.d.ts +55 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +378 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/output.d.ts +21 -0
- package/dist/core/output.d.ts.map +1 -0
- package/dist/core/output.js +56 -0
- package/dist/core/output.js.map +1 -0
- package/dist/core/report-generator.d.ts +24 -0
- package/dist/core/report-generator.d.ts.map +1 -0
- package/dist/core/report-generator.js +212 -0
- package/dist/core/report-generator.js.map +1 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +306 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/timeout.d.ts +49 -0
- package/dist/core/timeout.d.ts.map +1 -0
- package/dist/core/timeout.js +67 -0
- package/dist/core/timeout.js.map +1 -0
- package/dist/core/tool-logger.d.ts +83 -0
- package/dist/core/tool-logger.d.ts.map +1 -0
- package/dist/core/tool-logger.js +453 -0
- package/dist/core/tool-logger.js.map +1 -0
- package/dist/core/validation.d.ts +39 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +93 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +85 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/assert.d.ts +108 -0
- package/dist/tools/assert.d.ts.map +1 -0
- package/dist/tools/assert.js +291 -0
- package/dist/tools/assert.js.map +1 -0
- package/dist/tools/automator.d.ts +45 -0
- package/dist/tools/automator.d.ts.map +1 -0
- package/dist/tools/automator.js +186 -0
- package/dist/tools/automator.js.map +1 -0
- package/dist/tools/element.d.ts +253 -0
- package/dist/tools/element.d.ts.map +1 -0
- package/dist/tools/element.js +615 -0
- package/dist/tools/element.js.map +1 -0
- package/dist/tools/index.d.ts +97 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +1565 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/miniprogram.d.ts +79 -0
- package/dist/tools/miniprogram.d.ts.map +1 -0
- package/dist/tools/miniprogram.js +245 -0
- package/dist/tools/miniprogram.js.map +1 -0
- package/dist/tools/network.d.ts +65 -0
- package/dist/tools/network.d.ts.map +1 -0
- package/dist/tools/network.js +205 -0
- package/dist/tools/network.js.map +1 -0
- package/dist/tools/page.d.ts +108 -0
- package/dist/tools/page.d.ts.map +1 -0
- package/dist/tools/page.js +307 -0
- package/dist/tools/page.js.map +1 -0
- package/dist/tools/record.d.ts +86 -0
- package/dist/tools/record.d.ts.map +1 -0
- package/dist/tools/record.js +316 -0
- package/dist/tools/record.js.map +1 -0
- package/dist/tools/snapshot.d.ts +82 -0
- package/dist/tools/snapshot.d.ts.map +1 -0
- package/dist/tools/snapshot.js +258 -0
- package/dist/tools/snapshot.js.map +1 -0
- package/dist/types.d.ts +240 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/docs/SIMPLE_USAGE.md +210 -0
- package/docs/api/README.md +244 -0
- package/docs/api/assert.md +1015 -0
- package/docs/api/automator.md +345 -0
- package/docs/api/element.md +1454 -0
- package/docs/api/miniprogram.md +558 -0
- package/docs/api/network.md +883 -0
- package/docs/api/page.md +909 -0
- package/docs/api/record.md +963 -0
- package/docs/api/snapshot.md +792 -0
- package/docs/architecture.E-Docs.md +1359 -0
- package/docs/architecture.F1.md +720 -0
- package/docs/architecture.F2.md +871 -0
- package/docs/architecture.F3.md +905 -0
- package/docs/architecture.md +90 -0
- package/docs/charter.A1.align.yaml +170 -0
- package/docs/charter.A2.align.yaml +199 -0
- package/docs/charter.A3.align.yaml +242 -0
- package/docs/charter.A4.align.yaml +227 -0
- package/docs/charter.B1.align.yaml +179 -0
- package/docs/charter.B2.align.yaml +200 -0
- package/docs/charter.B3.align.yaml +200 -0
- package/docs/charter.B4.align.yaml +188 -0
- package/docs/charter.C1.align.yaml +190 -0
- package/docs/charter.C2.align.yaml +202 -0
- package/docs/charter.C3.align.yaml +211 -0
- package/docs/charter.C4.align.yaml +263 -0
- package/docs/charter.C5.align.yaml +220 -0
- package/docs/charter.D1.align.yaml +190 -0
- package/docs/charter.D2.align.yaml +234 -0
- package/docs/charter.D3.align.yaml +206 -0
- package/docs/charter.E-Docs.align.yaml +294 -0
- package/docs/charter.F1.align.yaml +193 -0
- package/docs/charter.F2.align.yaml +248 -0
- package/docs/charter.F3.align.yaml +287 -0
- package/docs/charter.G.align.yaml +174 -0
- package/docs/charter.align.yaml +111 -0
- package/docs/examples/session-report-usage.md +449 -0
- package/docs/maintenance.md +682 -0
- package/docs/playwright-mcp/350/260/203/347/240/224.md +53 -0
- package/docs/setup-guide.md +775 -0
- package/docs/tasks.A1.atomize.md +296 -0
- package/docs/tasks.A2.atomize.md +408 -0
- package/docs/tasks.A3.atomize.md +564 -0
- package/docs/tasks.A4.atomize.md +496 -0
- package/docs/tasks.B1.atomize.md +352 -0
- package/docs/tasks.B2.atomize.md +561 -0
- package/docs/tasks.B3.atomize.md +508 -0
- package/docs/tasks.B4.atomize.md +504 -0
- package/docs/tasks.C1.atomize.md +540 -0
- package/docs/tasks.C2.atomize.md +665 -0
- package/docs/tasks.C3.atomize.md +745 -0
- package/docs/tasks.C4.atomize.md +908 -0
- package/docs/tasks.C5.atomize.md +755 -0
- package/docs/tasks.D1.atomize.md +547 -0
- package/docs/tasks.D2.atomize.md +619 -0
- package/docs/tasks.D3.atomize.md +790 -0
- package/docs/tasks.E-Docs.atomize.md +1204 -0
- package/docs/tasks.atomize.md +189 -0
- package/docs/troubleshooting.md +855 -0
- package/docs//345/256/214/346/225/264/345/256/236/347/216/260/346/226/271/346/241/210.md +155 -0
- package/docs//345/274/200/345/217/221/344/273/273/345/212/241/350/256/241/345/210/222.md +110 -0
- package/docs//345/276/256/344/277/241/345/260/217/347/250/213/345/272/217/350/207/252/345/212/250/345/214/226API/345/256/214/346/225/264/346/226/207/346/241/243.md +894 -0
- package/docs//345/276/256/344/277/241/345/260/217/347/250/213/345/272/217/350/207/252/345/212/250/345/214/226/345/256/214/346/225/264/346/223/215/344/275/234/346/211/213/345/206/214.md +1885 -0
- package/docs//346/216/245/345/217/243/346/226/271/346/241/210.md +565 -0
- package/docs//347/254/254/344/270/200/347/211/210/346/234/254/346/226/271/346/241/210.md +380 -0
- package/package.json +87 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ElementRef resolver for locating elements in miniProgram pages
|
|
3
|
+
* Supports multiple location strategies: refId, selector, xpath, index
|
|
4
|
+
*
|
|
5
|
+
* Cache Invalidation:
|
|
6
|
+
* - Element cache is automatically cleared when page navigation is detected
|
|
7
|
+
* - Cached elements are validated against current page before use
|
|
8
|
+
* - Stale elements (from different pages) are removed automatically
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Generate a unique reference ID for caching elements
|
|
12
|
+
*/
|
|
13
|
+
export function generateRefId() {
|
|
14
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check if page has changed and clear stale element cache
|
|
18
|
+
* Called before each element resolution to ensure cache validity
|
|
19
|
+
*
|
|
20
|
+
* @param state - Session state with element cache
|
|
21
|
+
* @param currentPagePath - Current page path
|
|
22
|
+
*/
|
|
23
|
+
export function invalidateStaleCacheEntries(state, currentPagePath) {
|
|
24
|
+
// If page has changed, clear entire cache (safe approach)
|
|
25
|
+
if (state.currentPagePath && state.currentPagePath !== currentPagePath) {
|
|
26
|
+
const previousPath = state.currentPagePath;
|
|
27
|
+
const clearedCount = state.elements.size;
|
|
28
|
+
state.elements.clear();
|
|
29
|
+
state.logger?.info('Element cache cleared due to page navigation', {
|
|
30
|
+
previousPage: previousPath,
|
|
31
|
+
currentPage: currentPagePath,
|
|
32
|
+
clearedElements: clearedCount,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Update current page path
|
|
36
|
+
state.currentPagePath = currentPagePath;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve page object from sessionState
|
|
40
|
+
* @param state - Session state containing miniProgram instance
|
|
41
|
+
* @param pagePath - Optional page path. If not provided, uses currentPage
|
|
42
|
+
* @returns Page object
|
|
43
|
+
* @throws Error if miniProgram not connected or page not found
|
|
44
|
+
*/
|
|
45
|
+
export async function resolvePage(state, pagePath) {
|
|
46
|
+
if (!state.miniProgram) {
|
|
47
|
+
throw new Error('MiniProgram not launched or connected. Call miniprogram_launch or miniprogram_connect first.');
|
|
48
|
+
}
|
|
49
|
+
// If no pagePath specified, use current page
|
|
50
|
+
if (!pagePath) {
|
|
51
|
+
const currentPage = await state.miniProgram.currentPage();
|
|
52
|
+
if (!currentPage) {
|
|
53
|
+
throw new Error('No current page found. Ensure the miniProgram has navigated to a page.');
|
|
54
|
+
}
|
|
55
|
+
return currentPage;
|
|
56
|
+
}
|
|
57
|
+
// Find page in pageStack by path
|
|
58
|
+
const pageStack = await state.miniProgram.pageStack();
|
|
59
|
+
if (!Array.isArray(pageStack) || pageStack.length === 0) {
|
|
60
|
+
throw new Error('Page stack is empty. Ensure miniProgram has navigated to pages.');
|
|
61
|
+
}
|
|
62
|
+
// Normalize pagePath (support both '/path' and 'path')
|
|
63
|
+
const normalizedPath = pagePath.startsWith('/') ? pagePath.slice(1) : pagePath;
|
|
64
|
+
const found = pageStack.find((p) => p.path === normalizedPath || p.path === pagePath);
|
|
65
|
+
if (!found) {
|
|
66
|
+
const availablePaths = pageStack.map((p) => p.path).join(', ');
|
|
67
|
+
throw new Error(`Page not found in stack: ${pagePath}. Available pages: ${availablePaths}`);
|
|
68
|
+
}
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolve element using various location strategies
|
|
73
|
+
* @param state - Session state containing element cache
|
|
74
|
+
* @param ref - Element reference input with location strategy
|
|
75
|
+
* @returns Resolved element with page, element object, and optional refId
|
|
76
|
+
* @throws Error if element not found or invalid reference
|
|
77
|
+
*/
|
|
78
|
+
export async function resolveElement(state, ref) {
|
|
79
|
+
// Get the page first
|
|
80
|
+
const page = await resolvePage(state, ref.pagePath);
|
|
81
|
+
// Get current page path for cache validation
|
|
82
|
+
const currentPagePath = page.path || (await state.miniProgram?.currentPage())?.path;
|
|
83
|
+
// Invalidate stale cache entries if page has changed
|
|
84
|
+
if (currentPagePath) {
|
|
85
|
+
invalidateStaleCacheEntries(state, currentPagePath);
|
|
86
|
+
}
|
|
87
|
+
let element = null;
|
|
88
|
+
// Strategy 1: Use cached refId
|
|
89
|
+
if (ref.refId) {
|
|
90
|
+
const cached = state.elements.get(ref.refId);
|
|
91
|
+
if (!cached) {
|
|
92
|
+
const availableRefs = Array.from(state.elements.keys()).join(', ');
|
|
93
|
+
throw new Error(`Invalid refId: ${ref.refId}. ` +
|
|
94
|
+
`Available refIds: ${availableRefs || '(none)'}. ` +
|
|
95
|
+
`The element may have been removed or the page has changed.`);
|
|
96
|
+
}
|
|
97
|
+
// Validate that cached element is from current page
|
|
98
|
+
if (currentPagePath && cached.pagePath !== currentPagePath) {
|
|
99
|
+
// Remove stale entry
|
|
100
|
+
state.elements.delete(ref.refId);
|
|
101
|
+
throw new Error(`Cached element refId ${ref.refId} is stale. ` +
|
|
102
|
+
`Element was cached on page '${cached.pagePath}' but current page is '${currentPagePath}'. ` +
|
|
103
|
+
`Please re-query the element on the current page.`);
|
|
104
|
+
}
|
|
105
|
+
element = cached.element;
|
|
106
|
+
}
|
|
107
|
+
// Strategy 2: Use XPath (requires SDK 0.11.0+)
|
|
108
|
+
else if (ref.xpath) {
|
|
109
|
+
const anyPage = page;
|
|
110
|
+
// Check if xpath is supported
|
|
111
|
+
if (typeof anyPage.xpath !== 'function' && typeof anyPage.getElementByXpath !== 'function') {
|
|
112
|
+
throw new Error('XPath is not supported by current miniprogram-automator SDK version. ' +
|
|
113
|
+
'Please upgrade to SDK 0.11.0 or later, or use selector instead.');
|
|
114
|
+
}
|
|
115
|
+
// If index is specified, use getElementsByXpath
|
|
116
|
+
if (typeof ref.index === 'number') {
|
|
117
|
+
if (typeof anyPage.getElementsByXpath === 'function') {
|
|
118
|
+
const elements = await anyPage.getElementsByXpath(ref.xpath);
|
|
119
|
+
if (!Array.isArray(elements) || elements.length === 0) {
|
|
120
|
+
throw new Error(`No elements found with XPath: ${ref.xpath}`);
|
|
121
|
+
}
|
|
122
|
+
if (ref.index < 0 || ref.index >= elements.length) {
|
|
123
|
+
throw new Error(`Index ${ref.index} out of range. Found ${elements.length} elements with XPath: ${ref.xpath}`);
|
|
124
|
+
}
|
|
125
|
+
element = elements[ref.index];
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
throw new Error('getElementsByXpath is not supported. Cannot use index with xpath in this SDK version.');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Use getElementByXpath or xpath (fallback)
|
|
133
|
+
if (typeof anyPage.getElementByXpath === 'function') {
|
|
134
|
+
element = await anyPage.getElementByXpath(ref.xpath);
|
|
135
|
+
}
|
|
136
|
+
else if (typeof anyPage.xpath === 'function') {
|
|
137
|
+
element = await anyPage.xpath(ref.xpath);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!element) {
|
|
141
|
+
throw new Error(`Element not found with XPath: ${ref.xpath}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Strategy 3: Use CSS selector
|
|
145
|
+
else if (ref.selector) {
|
|
146
|
+
// If index is specified, use $$
|
|
147
|
+
if (typeof ref.index === 'number') {
|
|
148
|
+
const elements = await page.$$(ref.selector);
|
|
149
|
+
if (!Array.isArray(elements) || elements.length === 0) {
|
|
150
|
+
throw new Error(`No elements found with selector: ${ref.selector}`);
|
|
151
|
+
}
|
|
152
|
+
if (ref.index < 0 || ref.index >= elements.length) {
|
|
153
|
+
throw new Error(`Index ${ref.index} out of range. Found ${elements.length} elements with selector: ${ref.selector}`);
|
|
154
|
+
}
|
|
155
|
+
element = elements[ref.index];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Use $ for single element
|
|
159
|
+
element = await page.$(ref.selector);
|
|
160
|
+
if (!element) {
|
|
161
|
+
throw new Error(`Element not found with selector: ${ref.selector}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// No valid location strategy provided
|
|
166
|
+
else {
|
|
167
|
+
throw new Error('Invalid ElementRef: must provide one of: refId, selector, or xpath');
|
|
168
|
+
}
|
|
169
|
+
// Cache element if save=true
|
|
170
|
+
let newRefId;
|
|
171
|
+
if (ref.save && element) {
|
|
172
|
+
newRefId = generateRefId();
|
|
173
|
+
// Store element with page metadata for cache invalidation
|
|
174
|
+
state.elements.set(newRefId, {
|
|
175
|
+
element,
|
|
176
|
+
pagePath: currentPagePath || page.path || 'unknown',
|
|
177
|
+
cachedAt: new Date(),
|
|
178
|
+
});
|
|
179
|
+
state.logger?.info('Element cached with refId', {
|
|
180
|
+
refId: newRefId,
|
|
181
|
+
pagePath: currentPagePath || page.path,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
page,
|
|
186
|
+
element,
|
|
187
|
+
refId: newRefId,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Validate ElementRefInput and provide helpful error messages
|
|
192
|
+
*/
|
|
193
|
+
export function validateElementRef(ref) {
|
|
194
|
+
if (!ref || typeof ref !== 'object') {
|
|
195
|
+
throw new Error('ElementRef must be an object');
|
|
196
|
+
}
|
|
197
|
+
const hasRefId = typeof ref.refId === 'string' && ref.refId.length > 0;
|
|
198
|
+
const hasSelector = typeof ref.selector === 'string' && ref.selector.length > 0;
|
|
199
|
+
const hasXpath = typeof ref.xpath === 'string' && ref.xpath.length > 0;
|
|
200
|
+
if (!hasRefId && !hasSelector && !hasXpath) {
|
|
201
|
+
throw new Error('ElementRef must provide one of: refId, selector, or xpath');
|
|
202
|
+
}
|
|
203
|
+
if (ref.index !== undefined && typeof ref.index !== 'number') {
|
|
204
|
+
throw new Error('ElementRef.index must be a number');
|
|
205
|
+
}
|
|
206
|
+
if (ref.pagePath !== undefined && typeof ref.pagePath !== 'string') {
|
|
207
|
+
throw new Error('ElementRef.pagePath must be a string');
|
|
208
|
+
}
|
|
209
|
+
if (ref.save !== undefined && typeof ref.save !== 'boolean') {
|
|
210
|
+
throw new Error('ElementRef.save must be a boolean');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=element-ref.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element-ref.js","sourceRoot":"","sources":["../../src/core/element-ref.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger implementation for structured logging with file support
|
|
3
|
+
*/
|
|
4
|
+
import type { Logger, LoggerConfig } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Console-based logger implementation
|
|
7
|
+
*/
|
|
8
|
+
export declare class ConsoleLogger implements Logger {
|
|
9
|
+
private sessionId?;
|
|
10
|
+
private toolName?;
|
|
11
|
+
private config;
|
|
12
|
+
private fileWriter?;
|
|
13
|
+
constructor(sessionId?: string, toolName?: string, config?: LoggerConfig);
|
|
14
|
+
/**
|
|
15
|
+
* Merge user config with defaults and validate values (Issue #7)
|
|
16
|
+
*/
|
|
17
|
+
private mergeConfig;
|
|
18
|
+
/**
|
|
19
|
+
* Check if log level should be output
|
|
20
|
+
*/
|
|
21
|
+
private shouldLog;
|
|
22
|
+
/**
|
|
23
|
+
* Create log entry with metadata
|
|
24
|
+
*/
|
|
25
|
+
private createEntry;
|
|
26
|
+
/**
|
|
27
|
+
* Format log entry for console output
|
|
28
|
+
*/
|
|
29
|
+
private format;
|
|
30
|
+
info(message: string, context?: Record<string, any>): void;
|
|
31
|
+
warn(message: string, context?: Record<string, any>): void;
|
|
32
|
+
error(message: string, context?: Record<string, any>): void;
|
|
33
|
+
debug(message: string, context?: Record<string, any>): void;
|
|
34
|
+
/**
|
|
35
|
+
* Log entry to console and optionally to file
|
|
36
|
+
*/
|
|
37
|
+
private log;
|
|
38
|
+
/**
|
|
39
|
+
* Flush log buffer to file
|
|
40
|
+
*/
|
|
41
|
+
flush(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Dispose logger and flush buffer
|
|
44
|
+
*/
|
|
45
|
+
dispose(): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Create a child logger with specific tool name
|
|
48
|
+
*/
|
|
49
|
+
child(toolName: string): Logger;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create a logger for a session
|
|
53
|
+
*/
|
|
54
|
+
export declare function createLogger(sessionId: string, config?: LoggerConfig): Logger;
|
|
55
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/core/logger.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAsB,YAAY,EAAE,MAAM,aAAa,CAAA;AA8P3E;;GAEG;AACH,qBAAa,aAAc,YAAW,MAAM;IAC1C,OAAO,CAAC,SAAS,CAAC,CAAQ;IAC1B,OAAO,CAAC,QAAQ,CAAC,CAAQ;IACzB,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,UAAU,CAAC,CAAY;gBAEnB,SAAS,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,YAAY;IAgBxE;;OAEG;IACH,OAAO,CAAC,WAAW;IA2CnB;;OAEG;IACH,OAAO,CAAC,SAAS;IAIjB;;OAEG;IACH,OAAO,CAAC,WAAW;IAWnB;;OAEG;IACH,OAAO,CAAC,MAAM;IAUd,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAM1D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAM1D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAM3D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAM3D;;OAEG;IACH,OAAO,CAAC,GAAG;IAQX;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B;;OAEG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;CAGhC;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAE7E"}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger implementation for structured logging with file support
|
|
3
|
+
*/
|
|
4
|
+
import { open, mkdir, rename } from 'fs/promises';
|
|
5
|
+
import { statfs } from 'fs/promises'; // Issue #6: Disk space check (Node 18+)
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
/**
|
|
8
|
+
* Default logger configuration
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_LOGGER_CONFIG = {
|
|
11
|
+
level: 'info',
|
|
12
|
+
enableFileLog: false,
|
|
13
|
+
outputDir: '.mcp-artifacts',
|
|
14
|
+
bufferSize: 100,
|
|
15
|
+
flushInterval: 5000,
|
|
16
|
+
enableFailureSnapshot: false,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Log level priorities for filtering
|
|
20
|
+
*/
|
|
21
|
+
const LOG_LEVELS = {
|
|
22
|
+
debug: 0,
|
|
23
|
+
info: 1,
|
|
24
|
+
warn: 2,
|
|
25
|
+
error: 3,
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Internal file writer with buffering for async log writing
|
|
29
|
+
*/
|
|
30
|
+
class FileWriter {
|
|
31
|
+
sessionId;
|
|
32
|
+
outputDir;
|
|
33
|
+
bufferSize;
|
|
34
|
+
flushInterval;
|
|
35
|
+
buffer = [];
|
|
36
|
+
flushTimer;
|
|
37
|
+
fileHandle;
|
|
38
|
+
disposed = false;
|
|
39
|
+
filePath;
|
|
40
|
+
flushInProgress = false; // Issue #2: Prevent concurrent flushes
|
|
41
|
+
failureCount = 0; // Issue #1: Circuit breaker for persistent failures
|
|
42
|
+
MAX_FAILURES = 3; // Disable after 3 consecutive failures
|
|
43
|
+
constructor(sessionId, outputDir, bufferSize, flushInterval) {
|
|
44
|
+
this.sessionId = sessionId;
|
|
45
|
+
this.outputDir = outputDir;
|
|
46
|
+
this.bufferSize = bufferSize;
|
|
47
|
+
this.flushInterval = flushInterval;
|
|
48
|
+
this.startFlushTimer();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Add log entry to buffer (non-blocking)
|
|
52
|
+
*/
|
|
53
|
+
write(entry) {
|
|
54
|
+
if (this.disposed)
|
|
55
|
+
return;
|
|
56
|
+
this.buffer.push(entry);
|
|
57
|
+
// Trigger flush if buffer is full
|
|
58
|
+
if (this.buffer.length >= this.bufferSize) {
|
|
59
|
+
// Fire-and-forget flush (non-blocking)
|
|
60
|
+
void this.flush().catch((err) => {
|
|
61
|
+
console.error('Failed to flush log buffer:', err);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Flush buffer to file
|
|
67
|
+
*/
|
|
68
|
+
async flush() {
|
|
69
|
+
// Issue #2: Prevent concurrent flushes
|
|
70
|
+
if (this.buffer.length === 0 || this.disposed || this.flushInProgress)
|
|
71
|
+
return;
|
|
72
|
+
this.flushInProgress = true;
|
|
73
|
+
try {
|
|
74
|
+
// Extract buffer entries
|
|
75
|
+
const entries = this.buffer.splice(0, this.buffer.length);
|
|
76
|
+
const lines = entries
|
|
77
|
+
.map((e) => {
|
|
78
|
+
try {
|
|
79
|
+
// Issue #13: Handle circular references
|
|
80
|
+
return JSON.stringify(e);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return JSON.stringify({
|
|
84
|
+
...e,
|
|
85
|
+
context: '<Serialization failed: circular reference>',
|
|
86
|
+
_serializationError: error instanceof Error ? error.message : String(error),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.join('\n') + '\n';
|
|
91
|
+
try {
|
|
92
|
+
// Ensure file handle is open
|
|
93
|
+
if (!this.fileHandle) {
|
|
94
|
+
await this.ensureLogDirectory();
|
|
95
|
+
this.filePath = join(this.outputDir, 'logs', `session-${this.sessionId}.log`);
|
|
96
|
+
this.fileHandle = await open(this.filePath, 'a');
|
|
97
|
+
}
|
|
98
|
+
// Issue #6: Check disk space before writing
|
|
99
|
+
await this.checkDiskSpace();
|
|
100
|
+
if (this.disposed)
|
|
101
|
+
return; // Disk space check may have disabled logging
|
|
102
|
+
// Issue #8: Rotate file if too large
|
|
103
|
+
await this.rotateIfNeeded();
|
|
104
|
+
// Write to file
|
|
105
|
+
await this.fileHandle.write(lines);
|
|
106
|
+
// Issue #1: Reset failure count on success
|
|
107
|
+
this.failureCount = 0;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const err = error instanceof Error ? error.message : String(error);
|
|
111
|
+
// Handle disk full error
|
|
112
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOSPC') {
|
|
113
|
+
console.error('Disk full, disabling file logging');
|
|
114
|
+
// Issue #4: Close file handle before disposing to prevent leak
|
|
115
|
+
if (this.fileHandle) {
|
|
116
|
+
await this.fileHandle.close().catch(() => { });
|
|
117
|
+
this.fileHandle = undefined;
|
|
118
|
+
}
|
|
119
|
+
this.disposed = true;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
console.error('Failed to write logs to file:', err);
|
|
123
|
+
// Issue #1: Circuit breaker - disable after too many failures
|
|
124
|
+
this.failureCount++;
|
|
125
|
+
if (this.failureCount >= this.MAX_FAILURES) {
|
|
126
|
+
console.error(`Too many flush failures (${this.failureCount}), disabling file logging`);
|
|
127
|
+
// Issue #4: Close file handle before disposing
|
|
128
|
+
if (this.fileHandle) {
|
|
129
|
+
await this.fileHandle.close().catch(() => { });
|
|
130
|
+
this.fileHandle = undefined;
|
|
131
|
+
}
|
|
132
|
+
this.disposed = true;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Issue #1: Smart retry with buffer size limit
|
|
136
|
+
// Only keep the most recent entries to prevent unbounded growth
|
|
137
|
+
const entriesToKeep = Math.min(entries.length, this.bufferSize - this.buffer.length);
|
|
138
|
+
if (entriesToKeep > 0) {
|
|
139
|
+
// Keep newest entries, drop oldest
|
|
140
|
+
this.buffer.unshift(...entries.slice(-entriesToKeep));
|
|
141
|
+
if (entriesToKeep < entries.length) {
|
|
142
|
+
console.warn(`Dropped ${entries.length - entriesToKeep} old log entries due to buffer limit`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.warn(`Buffer full, dropped ${entries.length} failed log entries`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
this.flushInProgress = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Dispose writer and flush remaining buffer
|
|
156
|
+
*/
|
|
157
|
+
async dispose() {
|
|
158
|
+
this.disposed = true;
|
|
159
|
+
if (this.flushTimer) {
|
|
160
|
+
clearInterval(this.flushTimer);
|
|
161
|
+
}
|
|
162
|
+
await this.flush();
|
|
163
|
+
if (this.fileHandle) {
|
|
164
|
+
await this.fileHandle.close();
|
|
165
|
+
this.fileHandle = undefined;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Start periodic flush timer
|
|
170
|
+
*/
|
|
171
|
+
startFlushTimer() {
|
|
172
|
+
this.flushTimer = setInterval(() => {
|
|
173
|
+
void this.flush().catch((err) => {
|
|
174
|
+
console.error('Scheduled flush failed:', err);
|
|
175
|
+
});
|
|
176
|
+
}, this.flushInterval);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Ensure log directory exists
|
|
180
|
+
*/
|
|
181
|
+
async ensureLogDirectory() {
|
|
182
|
+
const logDir = join(this.outputDir, 'logs');
|
|
183
|
+
await mkdir(logDir, { recursive: true });
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Check disk space and warn/disable if low (Issue #6)
|
|
187
|
+
*/
|
|
188
|
+
async checkDiskSpace() {
|
|
189
|
+
if (!this.fileHandle || !this.filePath)
|
|
190
|
+
return;
|
|
191
|
+
try {
|
|
192
|
+
const stats = await statfs(this.outputDir);
|
|
193
|
+
const freeBytes = BigInt(stats.bsize) * BigInt(stats.bavail);
|
|
194
|
+
const freeMB = Number(freeBytes / BigInt(1024 * 1024));
|
|
195
|
+
if (freeMB < 100) {
|
|
196
|
+
console.warn(`[Logger] Low disk space: ${freeMB.toFixed(2)}MB remaining`);
|
|
197
|
+
}
|
|
198
|
+
if (freeMB < 10) {
|
|
199
|
+
console.error('[Logger] Critical disk space (<10MB), disabling file logging');
|
|
200
|
+
// Close file handle and dispose
|
|
201
|
+
await this.fileHandle.close().catch(() => { });
|
|
202
|
+
this.fileHandle = undefined;
|
|
203
|
+
this.disposed = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// statfs may not be available on all platforms/Node versions
|
|
208
|
+
// Silently ignore to avoid breaking on older Node versions
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Rotate log file if it exceeds 10MB (Issue #8)
|
|
213
|
+
*/
|
|
214
|
+
async rotateIfNeeded() {
|
|
215
|
+
if (!this.fileHandle || !this.filePath)
|
|
216
|
+
return;
|
|
217
|
+
try {
|
|
218
|
+
const stats = await this.fileHandle.stat();
|
|
219
|
+
const sizeMB = stats.size / (1024 * 1024);
|
|
220
|
+
if (sizeMB > 10) {
|
|
221
|
+
console.warn(`[Logger] Log file exceeds 10MB (${sizeMB.toFixed(2)}MB), rotating...`);
|
|
222
|
+
// Close current file
|
|
223
|
+
await this.fileHandle.close();
|
|
224
|
+
// Rename to timestamped backup
|
|
225
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); // Remove .xxxZ
|
|
226
|
+
const rotatedPath = `${this.filePath}.${timestamp}`;
|
|
227
|
+
await rename(this.filePath, rotatedPath);
|
|
228
|
+
console.error(`[Logger] Rotated log file to: ${rotatedPath}`);
|
|
229
|
+
// Reopen new file
|
|
230
|
+
this.fileHandle = await open(this.filePath, 'a');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
console.error('[Logger] Failed to rotate log file:', error);
|
|
235
|
+
// Continue with existing file handle
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Console-based logger implementation
|
|
241
|
+
*/
|
|
242
|
+
export class ConsoleLogger {
|
|
243
|
+
sessionId;
|
|
244
|
+
toolName;
|
|
245
|
+
config;
|
|
246
|
+
fileWriter;
|
|
247
|
+
constructor(sessionId, toolName, config) {
|
|
248
|
+
this.sessionId = sessionId;
|
|
249
|
+
this.toolName = toolName;
|
|
250
|
+
this.config = this.mergeConfig(config);
|
|
251
|
+
// Initialize file writer if enabled
|
|
252
|
+
if (this.config.enableFileLog && sessionId) {
|
|
253
|
+
this.fileWriter = new FileWriter(sessionId, this.config.outputDir, this.config.bufferSize, this.config.flushInterval);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Merge user config with defaults and validate values (Issue #7)
|
|
258
|
+
*/
|
|
259
|
+
mergeConfig(userConfig) {
|
|
260
|
+
// Start with defaults
|
|
261
|
+
const merged = {
|
|
262
|
+
...DEFAULT_LOGGER_CONFIG,
|
|
263
|
+
...userConfig,
|
|
264
|
+
};
|
|
265
|
+
// Issue #7: Validate and clamp buffer size (10 - 10000)
|
|
266
|
+
const rawBufferSize = merged.bufferSize;
|
|
267
|
+
merged.bufferSize = Math.max(10, Math.min(merged.bufferSize, 10000));
|
|
268
|
+
if (rawBufferSize !== merged.bufferSize) {
|
|
269
|
+
console.warn(`Logger bufferSize clamped to ${merged.bufferSize} (was ${rawBufferSize}). Valid range: 10-10000`);
|
|
270
|
+
}
|
|
271
|
+
// Issue #7: Validate and clamp flush interval (100ms - 60s)
|
|
272
|
+
const rawFlushInterval = merged.flushInterval;
|
|
273
|
+
merged.flushInterval = Math.max(100, Math.min(merged.flushInterval, 60000));
|
|
274
|
+
if (rawFlushInterval !== merged.flushInterval) {
|
|
275
|
+
console.warn(`Logger flushInterval clamped to ${merged.flushInterval}ms (was ${rawFlushInterval}ms). Valid range: 100-60000ms`);
|
|
276
|
+
}
|
|
277
|
+
// Issue #7: Validate log level
|
|
278
|
+
const validLevels = ['debug', 'info', 'warn', 'error'];
|
|
279
|
+
if (!validLevels.includes(merged.level)) {
|
|
280
|
+
console.warn(`Invalid log level "${merged.level}", using default "info". Valid levels: ${validLevels.join(', ')}`);
|
|
281
|
+
merged.level = 'info';
|
|
282
|
+
}
|
|
283
|
+
// Issue #7: Validate outputDir is non-empty string
|
|
284
|
+
if (typeof merged.outputDir !== 'string' || merged.outputDir.trim() === '') {
|
|
285
|
+
console.warn(`Invalid outputDir "${merged.outputDir}", using default ".mcp-artifacts"`);
|
|
286
|
+
merged.outputDir = '.mcp-artifacts';
|
|
287
|
+
}
|
|
288
|
+
return merged;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Check if log level should be output
|
|
292
|
+
*/
|
|
293
|
+
shouldLog(level) {
|
|
294
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[this.config.level];
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Create log entry with metadata
|
|
298
|
+
*/
|
|
299
|
+
createEntry(level, message, context) {
|
|
300
|
+
return {
|
|
301
|
+
timestamp: new Date(),
|
|
302
|
+
level,
|
|
303
|
+
message,
|
|
304
|
+
sessionId: this.sessionId,
|
|
305
|
+
toolName: this.toolName,
|
|
306
|
+
context,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Format log entry for console output
|
|
311
|
+
*/
|
|
312
|
+
format(entry) {
|
|
313
|
+
const timestamp = entry.timestamp.toISOString();
|
|
314
|
+
const level = entry.level.toUpperCase().padEnd(5);
|
|
315
|
+
const sessionInfo = entry.sessionId ? ` [${entry.sessionId}]` : '';
|
|
316
|
+
const toolInfo = entry.toolName ? ` [${entry.toolName}]` : '';
|
|
317
|
+
const contextInfo = entry.context ? ` ${JSON.stringify(entry.context)}` : '';
|
|
318
|
+
return `${timestamp} ${level}${sessionInfo}${toolInfo}: ${entry.message}${contextInfo}`;
|
|
319
|
+
}
|
|
320
|
+
info(message, context) {
|
|
321
|
+
if (!this.shouldLog('info'))
|
|
322
|
+
return;
|
|
323
|
+
const entry = this.createEntry('info', message, context);
|
|
324
|
+
this.log(entry);
|
|
325
|
+
}
|
|
326
|
+
warn(message, context) {
|
|
327
|
+
if (!this.shouldLog('warn'))
|
|
328
|
+
return;
|
|
329
|
+
const entry = this.createEntry('warn', message, context);
|
|
330
|
+
this.log(entry);
|
|
331
|
+
}
|
|
332
|
+
error(message, context) {
|
|
333
|
+
if (!this.shouldLog('error'))
|
|
334
|
+
return;
|
|
335
|
+
const entry = this.createEntry('error', message, context);
|
|
336
|
+
this.log(entry);
|
|
337
|
+
}
|
|
338
|
+
debug(message, context) {
|
|
339
|
+
if (!this.shouldLog('debug'))
|
|
340
|
+
return;
|
|
341
|
+
const entry = this.createEntry('debug', message, context);
|
|
342
|
+
this.log(entry);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Log entry to console and optionally to file
|
|
346
|
+
*/
|
|
347
|
+
log(entry) {
|
|
348
|
+
// Console output (always)
|
|
349
|
+
console.error(this.format(entry));
|
|
350
|
+
// File output (if enabled)
|
|
351
|
+
this.fileWriter?.write(entry);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Flush log buffer to file
|
|
355
|
+
*/
|
|
356
|
+
async flush() {
|
|
357
|
+
await this.fileWriter?.flush();
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Dispose logger and flush buffer
|
|
361
|
+
*/
|
|
362
|
+
async dispose() {
|
|
363
|
+
await this.fileWriter?.dispose();
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Create a child logger with specific tool name
|
|
367
|
+
*/
|
|
368
|
+
child(toolName) {
|
|
369
|
+
return new ConsoleLogger(this.sessionId, toolName, this.config);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Create a logger for a session
|
|
374
|
+
*/
|
|
375
|
+
export function createLogger(sessionId, config) {
|
|
376
|
+
return new ConsoleLogger(sessionId, undefined, config);
|
|
377
|
+
}
|
|
378
|
+
//# sourceMappingURL=logger.js.map
|