@warmio/mcp 2.2.0 → 3.0.2
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/dist/server.d.ts +3 -0
- package/dist/server.js +43 -70
- package/package.json +2 -3
- package/dist/api-types.d.ts +0 -8
- package/dist/api-types.js +0 -33
- package/dist/sandbox.d.ts +0 -10
- package/dist/sandbox.js +0 -87
package/dist/server.d.ts
CHANGED
|
@@ -3,5 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides financial data from the Warm API as MCP tools.
|
|
5
5
|
* Reads API key from WARM_API_KEY env var or ~/.config/warm/api_key.
|
|
6
|
+
*
|
|
7
|
+
* Four read-only tools: get_accounts, get_transactions, get_snapshots, verify_key.
|
|
8
|
+
* The AI client handles all analysis — no sandbox needed.
|
|
6
9
|
*/
|
|
7
10
|
export {};
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides financial data from the Warm API as MCP tools.
|
|
5
5
|
* Reads API key from WARM_API_KEY env var or ~/.config/warm/api_key.
|
|
6
|
+
*
|
|
7
|
+
* Four read-only tools: get_accounts, get_transactions, get_snapshots, verify_key.
|
|
8
|
+
* The AI client handles all analysis — no sandbox needed.
|
|
6
9
|
*/
|
|
7
10
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
8
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -10,25 +13,23 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
10
13
|
import * as fs from 'fs';
|
|
11
14
|
import * as path from 'path';
|
|
12
15
|
import * as os from 'os';
|
|
13
|
-
import { generateApiTypeString } from './api-types.js';
|
|
14
|
-
import { executeSandboxedCode } from './sandbox.js';
|
|
15
16
|
const API_URL = process.env.WARM_API_URL || 'https://warm.io';
|
|
16
|
-
const MAX_RESPONSE_SIZE = 50_000;
|
|
17
17
|
const MAX_TRANSACTION_PAGES = 10;
|
|
18
18
|
const MAX_TRANSACTION_SCAN = 5_000;
|
|
19
19
|
const TRANSACTION_PAGE_SIZE = 200;
|
|
20
|
+
const TRANSACTION_CACHE_TTL_MS = 600_000; // 10min cache for paginated reads
|
|
20
21
|
const REQUEST_TIMEOUT_MS = (() => {
|
|
21
22
|
const raw = Number(process.env.WARM_API_TIMEOUT_MS || 10_000);
|
|
22
23
|
return Number.isFinite(raw) && raw > 0 ? raw : 10_000;
|
|
23
24
|
})();
|
|
24
25
|
let cachedApiKey;
|
|
26
|
+
// In-memory transaction cache to avoid re-fetching on paginated reads
|
|
27
|
+
let txnCache = null;
|
|
25
28
|
function compactTransaction(t) {
|
|
26
29
|
return {
|
|
27
30
|
d: t.date || '',
|
|
28
|
-
// Positive = expense, negative = income/deposit (Plaid convention)
|
|
29
31
|
a: t.amount ? Math.round(t.amount * 100) / 100 : 0,
|
|
30
32
|
m: t.merchant_name || t.name || 'Unknown',
|
|
31
|
-
// Include category (null if not set)
|
|
32
33
|
c: t.primary_category ?? null,
|
|
33
34
|
};
|
|
34
35
|
}
|
|
@@ -116,7 +117,6 @@ async function apiRequest(endpoint, params = {}) {
|
|
|
116
117
|
if (errorMessages[response.status]) {
|
|
117
118
|
throw new Error(errorMessages[response.status]);
|
|
118
119
|
}
|
|
119
|
-
// Read the actual error message from the API response body
|
|
120
120
|
let detail = `HTTP ${response.status}`;
|
|
121
121
|
try {
|
|
122
122
|
const body = (await response.json());
|
|
@@ -137,9 +137,11 @@ async function handleGetAccounts() {
|
|
|
137
137
|
accounts: response.accounts || [],
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
|
-
async function
|
|
141
|
-
const
|
|
142
|
-
|
|
140
|
+
async function fetchAllTransactions(since, until) {
|
|
141
|
+
const cacheKey = `${since || ''}|${until || ''}`;
|
|
142
|
+
if (txnCache && txnCache.key === cacheKey && Date.now() - txnCache.fetchedAt < TRANSACTION_CACHE_TTL_MS) {
|
|
143
|
+
return { transactions: txnCache.transactions, summary: txnCache.summary };
|
|
144
|
+
}
|
|
143
145
|
let transactions = [];
|
|
144
146
|
let cursor;
|
|
145
147
|
let pagesFetched = 0;
|
|
@@ -148,7 +150,6 @@ async function handleGetTransactions(args) {
|
|
|
148
150
|
const params = {
|
|
149
151
|
limit: String(TRANSACTION_PAGE_SIZE),
|
|
150
152
|
};
|
|
151
|
-
// API rejects last_knowledge + cursor together; only use last_knowledge on first page
|
|
152
153
|
if (since && !cursor)
|
|
153
154
|
params.last_knowledge = since;
|
|
154
155
|
if (cursor)
|
|
@@ -165,26 +166,29 @@ async function handleGetTransactions(args) {
|
|
|
165
166
|
}
|
|
166
167
|
const compactTxns = transactions.map(compactTransaction);
|
|
167
168
|
const summary = calculateSummary(compactTxns);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
169
|
+
txnCache = { key: cacheKey, transactions: compactTxns, summary, fetchedAt: Date.now() };
|
|
170
|
+
return { transactions: compactTxns, summary };
|
|
171
|
+
}
|
|
172
|
+
async function handleGetTransactions(args) {
|
|
173
|
+
const since = args?.since ? String(args.since) : undefined;
|
|
174
|
+
const until = args?.until ? String(args.until) : undefined;
|
|
175
|
+
const limit = Math.min(Math.max(args?.limit ? Number(args.limit) : 500, 1), 1000);
|
|
176
|
+
const offset = Math.max(args?.offset ? Number(args.offset) : 0, 0);
|
|
177
|
+
const { transactions: compactTxns, summary } = await fetchAllTransactions(since, until);
|
|
178
|
+
const total = compactTxns.length;
|
|
179
|
+
const page = compactTxns.slice(offset, offset + limit);
|
|
180
|
+
return {
|
|
181
|
+
summary,
|
|
182
|
+
txns: page,
|
|
183
|
+
total,
|
|
184
|
+
has_more: offset + limit < total,
|
|
185
|
+
};
|
|
181
186
|
}
|
|
182
187
|
async function handleGetSnapshots(args) {
|
|
183
188
|
const response = (await apiRequest('/api/snapshots'));
|
|
184
189
|
const snapshots = response.snapshots || [];
|
|
185
190
|
const limit = args?.limit ? Number(args.limit) : 30;
|
|
186
191
|
const since = args?.since;
|
|
187
|
-
// Normalize snapshot dates (support both snapshot_date and d)
|
|
188
192
|
const normalized = snapshots.map((s) => ({
|
|
189
193
|
date: s.snapshot_date || s.d || '',
|
|
190
194
|
net_worth: s.net_worth ?? s.nw ?? 0,
|
|
@@ -214,7 +218,6 @@ async function handleVerifyKey() {
|
|
|
214
218
|
status: response.status || (response.valid ? 'ok' : 'invalid'),
|
|
215
219
|
};
|
|
216
220
|
}
|
|
217
|
-
// Tool name → handler mapping for sandbox dispatch
|
|
218
221
|
const toolHandlers = {
|
|
219
222
|
get_accounts: handleGetAccounts,
|
|
220
223
|
get_transactions: handleGetTransactions,
|
|
@@ -224,12 +227,12 @@ const toolHandlers = {
|
|
|
224
227
|
// ============================================
|
|
225
228
|
// SERVER SETUP
|
|
226
229
|
// ============================================
|
|
227
|
-
const server = new Server({ name: 'warm', version: '
|
|
230
|
+
const server = new Server({ name: 'warm', version: '3.0.2' }, { capabilities: { tools: {} } });
|
|
228
231
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
229
232
|
tools: [
|
|
230
233
|
{
|
|
231
234
|
name: 'get_accounts',
|
|
232
|
-
description: 'Get all connected bank accounts with balances
|
|
235
|
+
description: 'Get all connected bank accounts with current balances.\n\nReturns: { accounts: Array<{ name: string; type: string; balance: number; institution: string }> }\n\nAccount types: depository (checking/savings), credit, loan, investment, other.',
|
|
233
236
|
inputSchema: {
|
|
234
237
|
type: 'object',
|
|
235
238
|
properties: {},
|
|
@@ -238,7 +241,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
238
241
|
},
|
|
239
242
|
{
|
|
240
243
|
name: 'get_transactions',
|
|
241
|
-
description: 'Get transactions
|
|
244
|
+
description: 'Get transactions with date filtering and pagination. Returns a summary of the FULL date range plus a paginated slice of individual transactions.\n\nAmounts: positive = expense/debit, negative = income/credit (Plaid convention).\nCategories in field `c`: INCOME, TRANSFER_IN = income. FOOD_AND_DRINK, TRANSPORTATION, ENTERTAINMENT, GENERAL_MERCHANDISE, RENT_AND_UTILITIES, LOAN_PAYMENTS, etc. = expenses.\n\nReturns: { summary: { total: number; count: number; avg: number }; txns: Array<{ d: string; a: number; m: string; c: string | null }>; total: number; has_more: boolean }\n\nCall multiple times with increasing offset to paginate. The summary is always computed over ALL matching transactions regardless of limit/offset.',
|
|
242
245
|
inputSchema: {
|
|
243
246
|
type: 'object',
|
|
244
247
|
properties: {
|
|
@@ -250,46 +253,39 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
250
253
|
type: 'string',
|
|
251
254
|
description: 'End date inclusive (YYYY-MM-DD). Omit for no end date filter.',
|
|
252
255
|
},
|
|
256
|
+
limit: {
|
|
257
|
+
type: 'number',
|
|
258
|
+
description: 'Max transactions per page (default 500, max 1000).',
|
|
259
|
+
},
|
|
260
|
+
offset: {
|
|
261
|
+
type: 'number',
|
|
262
|
+
description: 'Skip N transactions for pagination (default 0).',
|
|
263
|
+
},
|
|
253
264
|
},
|
|
254
265
|
},
|
|
255
266
|
annotations: { readOnlyHint: true },
|
|
256
267
|
},
|
|
257
268
|
{
|
|
258
269
|
name: 'get_snapshots',
|
|
259
|
-
description: 'Get daily net worth
|
|
270
|
+
description: 'Get daily net worth snapshots over time.\n\nReturns: { snapshots: Array<{ d: string; nw: number; a: number; l: number }> }\n\nFields: d = date, nw = net worth, a = total assets, l = total liabilities. Sorted newest first.',
|
|
260
271
|
inputSchema: {
|
|
261
272
|
type: 'object',
|
|
262
273
|
properties: {
|
|
263
274
|
limit: {
|
|
264
275
|
type: 'number',
|
|
265
|
-
description: '
|
|
276
|
+
description: 'Max snapshots to return (default 30).',
|
|
266
277
|
},
|
|
267
278
|
since: {
|
|
268
279
|
type: 'string',
|
|
269
|
-
description: 'Start date (YYYY-MM-DD)',
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
annotations: { readOnlyHint: true },
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
name: 'run_analysis',
|
|
277
|
-
description: `Run JavaScript code that calls warm.* functions for complex multi-step analysis. Use when a query requires combining data from multiple tools, custom calculations, or comparisons that would take 3+ tool calls.\n\nAvailable API:\n${generateApiTypeString()}\n\nUse console.log() to output results. Example:\nconst [accounts, txns] = await Promise.all([warm.getAccounts(), warm.getTransactions({ since: "2024-01-01" })]);\nconst total = txns.txns.reduce((s, t) => s + t.a, 0);\nconsole.log(JSON.stringify({ accounts: accounts.accounts.length, totalSpent: total }));`,
|
|
278
|
-
inputSchema: {
|
|
279
|
-
type: 'object',
|
|
280
|
-
properties: {
|
|
281
|
-
code: {
|
|
282
|
-
type: 'string',
|
|
283
|
-
description: 'JavaScript code to execute. Use warm.* functions and console.log() for output.',
|
|
280
|
+
description: 'Start date inclusive (YYYY-MM-DD).',
|
|
284
281
|
},
|
|
285
282
|
},
|
|
286
|
-
required: ['code'],
|
|
287
283
|
},
|
|
288
284
|
annotations: { readOnlyHint: true },
|
|
289
285
|
},
|
|
290
286
|
{
|
|
291
287
|
name: 'verify_key',
|
|
292
|
-
description: 'Check if API key is valid
|
|
288
|
+
description: 'Check if the API key is valid.\n\nReturns: { valid: boolean; status: string }',
|
|
293
289
|
inputSchema: {
|
|
294
290
|
type: 'object',
|
|
295
291
|
properties: {},
|
|
@@ -301,29 +297,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
301
297
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
302
298
|
const { name, arguments: args } = request.params;
|
|
303
299
|
try {
|
|
304
|
-
// Handle run_analysis separately (sandbox execution)
|
|
305
|
-
if (name === 'run_analysis') {
|
|
306
|
-
const code = args?.code ? String(args.code) : '';
|
|
307
|
-
if (!code) {
|
|
308
|
-
throw new Error('Code parameter is required');
|
|
309
|
-
}
|
|
310
|
-
const callApi = async (tool, params) => {
|
|
311
|
-
const handler = toolHandlers[tool];
|
|
312
|
-
if (!handler) {
|
|
313
|
-
throw new Error(`Unknown tool: ${tool}`);
|
|
314
|
-
}
|
|
315
|
-
return handler(params);
|
|
316
|
-
};
|
|
317
|
-
const result = await executeSandboxedCode(code, callApi);
|
|
318
|
-
const text = result.error
|
|
319
|
-
? `Output:\n${result.output}\n\nError: ${result.error}`
|
|
320
|
-
: result.output;
|
|
321
|
-
return {
|
|
322
|
-
content: [{ type: 'text', text }],
|
|
323
|
-
isError: !!result.error,
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
// Standard tool dispatch
|
|
327
300
|
const handler = toolHandlers[name];
|
|
328
301
|
if (!handler) {
|
|
329
302
|
throw new Error(`Unknown tool: ${name}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warmio/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "MCP server for Warm Financial API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,8 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/sdk": "^1.25.0"
|
|
27
|
-
"quickjs-emscripten": "^0.31.0"
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.25.0"
|
|
28
27
|
},
|
|
29
28
|
"devDependencies": {
|
|
30
29
|
"@types/node": "^22.0.0",
|
package/dist/api-types.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TypeScript API type definitions for MCP code mode.
|
|
3
|
-
* Embedded in the run_analysis tool description so the LLM
|
|
4
|
-
* knows the shape of each warm.* function.
|
|
5
|
-
*
|
|
6
|
-
* All monetary amounts are positive (normalized).
|
|
7
|
-
*/
|
|
8
|
-
export declare function generateApiTypeString(): string;
|
package/dist/api-types.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TypeScript API type definitions for MCP code mode.
|
|
3
|
-
* Embedded in the run_analysis tool description so the LLM
|
|
4
|
-
* knows the shape of each warm.* function.
|
|
5
|
-
*
|
|
6
|
-
* All monetary amounts are positive (normalized).
|
|
7
|
-
*/
|
|
8
|
-
export function generateApiTypeString() {
|
|
9
|
-
return `declare const warm: {
|
|
10
|
-
/** Get all connected bank accounts with balances */
|
|
11
|
-
getAccounts(): Promise<{
|
|
12
|
-
accounts: Array<{ name: string; type: string; balance: number; institution: string }>;
|
|
13
|
-
}>;
|
|
14
|
-
|
|
15
|
-
/** Get transactions (up to 1000). Amounts: positive = expense, negative = income. Category c: "INCOME"/"TRANSFER_IN" = income, others = expenses. */
|
|
16
|
-
getTransactions(params?: {
|
|
17
|
-
since?: string; // YYYY-MM-DD inclusive
|
|
18
|
-
until?: string; // YYYY-MM-DD inclusive
|
|
19
|
-
}): Promise<{
|
|
20
|
-
summary: { total: number; count: number; avg: number };
|
|
21
|
-
txns: Array<{ d: string; a: number; m: string; c: string | null }>;
|
|
22
|
-
more?: number;
|
|
23
|
-
}>;
|
|
24
|
-
|
|
25
|
-
/** Get daily net worth history */
|
|
26
|
-
getSnapshots(params?: {
|
|
27
|
-
limit?: number;
|
|
28
|
-
since?: string;
|
|
29
|
-
}): Promise<{
|
|
30
|
-
snapshots: Array<{ d: string; nw: number; a: number; l: number }>;
|
|
31
|
-
}>;
|
|
32
|
-
};`;
|
|
33
|
-
}
|
package/dist/sandbox.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QuickJS Sandbox for MCP Code Mode
|
|
3
|
-
*
|
|
4
|
-
* Executes user-provided JavaScript code in a sandboxed QuickJS WASM runtime.
|
|
5
|
-
* Injects `warm.*` functions that delegate to a `callApi` callback.
|
|
6
|
-
*/
|
|
7
|
-
export declare function executeSandboxedCode(code: string, callApi: (tool: string, params: Record<string, unknown>) => Promise<unknown>): Promise<{
|
|
8
|
-
output: string;
|
|
9
|
-
error?: string;
|
|
10
|
-
}>;
|
package/dist/sandbox.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QuickJS Sandbox for MCP Code Mode
|
|
3
|
-
*
|
|
4
|
-
* Executes user-provided JavaScript code in a sandboxed QuickJS WASM runtime.
|
|
5
|
-
* Injects `warm.*` functions that delegate to a `callApi` callback.
|
|
6
|
-
*/
|
|
7
|
-
import { newAsyncContext } from 'quickjs-emscripten';
|
|
8
|
-
const MEMORY_LIMIT = 16 * 1024 * 1024; // 16MB
|
|
9
|
-
const TIMEOUT_MS = 30_000; // 30s
|
|
10
|
-
export async function executeSandboxedCode(code, callApi) {
|
|
11
|
-
const ctx = await newAsyncContext();
|
|
12
|
-
const outputLines = [];
|
|
13
|
-
try {
|
|
14
|
-
// Set memory limit
|
|
15
|
-
const rt = ctx.runtime;
|
|
16
|
-
rt.setMemoryLimit(MEMORY_LIMIT);
|
|
17
|
-
// Set execution timeout
|
|
18
|
-
let expired = false;
|
|
19
|
-
const deadline = Date.now() + TIMEOUT_MS;
|
|
20
|
-
rt.setInterruptHandler(() => {
|
|
21
|
-
if (Date.now() > deadline) {
|
|
22
|
-
expired = true;
|
|
23
|
-
return true; // interrupt
|
|
24
|
-
}
|
|
25
|
-
return false;
|
|
26
|
-
});
|
|
27
|
-
// Inject console.log
|
|
28
|
-
const consoleObj = ctx.newObject();
|
|
29
|
-
const logFn = ctx.newFunction('log', (...args) => {
|
|
30
|
-
const parts = args.map((arg) => {
|
|
31
|
-
const str = ctx.getString(arg);
|
|
32
|
-
return str;
|
|
33
|
-
});
|
|
34
|
-
outputLines.push(parts.join(' '));
|
|
35
|
-
});
|
|
36
|
-
ctx.setProp(consoleObj, 'log', logFn);
|
|
37
|
-
ctx.setProp(ctx.global, 'console', consoleObj);
|
|
38
|
-
logFn.dispose();
|
|
39
|
-
consoleObj.dispose();
|
|
40
|
-
// Inject __callApi as a native async function
|
|
41
|
-
const callApiFn = ctx.newAsyncifiedFunction('__callApi', async (toolHandle, paramsHandle) => {
|
|
42
|
-
const tool = ctx.getString(toolHandle);
|
|
43
|
-
const paramsJson = ctx.getString(paramsHandle);
|
|
44
|
-
const params = paramsJson ? JSON.parse(paramsJson) : {};
|
|
45
|
-
const result = await callApi(tool, params);
|
|
46
|
-
return ctx.newString(JSON.stringify(result));
|
|
47
|
-
});
|
|
48
|
-
ctx.setProp(ctx.global, '__callApi', callApiFn);
|
|
49
|
-
callApiFn.dispose();
|
|
50
|
-
// Wrapper code that creates the warm.* API using __callApi
|
|
51
|
-
const wrappedCode = `
|
|
52
|
-
async function __run() {
|
|
53
|
-
const warm = {
|
|
54
|
-
getAccounts: () => __callApi('get_accounts', '{}').then(JSON.parse),
|
|
55
|
-
getTransactions: (p) => __callApi('get_transactions', JSON.stringify(p || {})).then(JSON.parse),
|
|
56
|
-
getSnapshots: (p) => __callApi('get_snapshots', JSON.stringify(p || {})).then(JSON.parse),
|
|
57
|
-
};
|
|
58
|
-
${code}
|
|
59
|
-
}
|
|
60
|
-
__run();
|
|
61
|
-
`;
|
|
62
|
-
const result = await ctx.evalCodeAsync(wrappedCode);
|
|
63
|
-
if (expired) {
|
|
64
|
-
result.dispose();
|
|
65
|
-
return { output: outputLines.join('\n'), error: `Execution timed out after ${TIMEOUT_MS / 1000}s` };
|
|
66
|
-
}
|
|
67
|
-
if (result.error) {
|
|
68
|
-
const errorMsg = ctx.getString(result.error);
|
|
69
|
-
result.error.dispose();
|
|
70
|
-
return { output: outputLines.join('\n'), error: errorMsg };
|
|
71
|
-
}
|
|
72
|
-
// If the result is a promise, await it
|
|
73
|
-
const promiseResult = await ctx.resolvePromise(result.value);
|
|
74
|
-
if (promiseResult.error) {
|
|
75
|
-
const errorMsg = ctx.getString(promiseResult.error);
|
|
76
|
-
promiseResult.error.dispose();
|
|
77
|
-
result.value.dispose();
|
|
78
|
-
return { output: outputLines.join('\n'), error: errorMsg };
|
|
79
|
-
}
|
|
80
|
-
promiseResult.value.dispose();
|
|
81
|
-
result.value.dispose();
|
|
82
|
-
return { output: outputLines.join('\n') };
|
|
83
|
-
}
|
|
84
|
-
finally {
|
|
85
|
-
ctx.dispose();
|
|
86
|
-
}
|
|
87
|
-
}
|