Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 118 additions & 34 deletions bun.lock

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions mcp-skills/financeagent/finance-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// mcp-skills/financeAgent.ts

interface FinanceAgentArgs {
portalUrl: string;
}

interface InvoiceRow {
invoiceId: string;
vendor: string;
amountDue: string;
status?: string;
}

interface ExecutionResult {
status: 'success' | 'failure';
timestamp: string;
foundInvoices: number;
totalDue: string;
data: InvoiceRow[];
}

interface FinanceBrowser {
goto: (url: string) => Promise<void>;
click: (selector: string) => Promise<void>;
evaluate: <T>(fn: () => T | Promise<T>) => Promise<T>;
}

export const financePlugin = {
name: 'finance-agent',
description: 'Automates checking pending invoices and verifying corporate balances.',

inputs: {
portalUrl: { type: 'string', default: 'https://mock-finance-portal.test' },
},

async execute(browser: FinanceBrowser, args: FinanceAgentArgs): Promise<ExecutionResult> {
const portalUrl = args.portalUrl || 'https://mock-finance-portal.test';
console.log(`🚀 Starting finance invoice scan on: ${portalUrl}`);

await browser.goto(portalUrl);
await browser.click('text=Invoices');

const invoiceData = await browser.evaluate(() => {
const rows = Array.from(document.querySelectorAll('table.invoice-list tr'));
return rows.slice(1).map((row) => {
const columns = row.querySelectorAll('td');
return {
invoiceId: columns[0]?.textContent?.trim() || 'N/A',
vendor: columns[1]?.textContent?.trim() || 'Unknown',
amountDue: columns[2]?.textContent?.trim() || '$0.00',
status: columns[3]?.textContent?.trim() || 'pending',
} satisfies InvoiceRow;
});
});

const totalDue = invoiceData.reduce((sum, item) => {
const numeric = Number.parseFloat(item.amountDue.replace(/[^\d.-]/g, '')) || 0;
return sum + numeric;
}, 0);

return {
status: 'success',
timestamp: new Date().toISOString(),
foundInvoices: invoiceData.length,
totalDue: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(totalDue),
data: invoiceData,
};
},
} as const;

export const financeAgentPlugin = financePlugin;
export default financePlugin;
7 changes: 0 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getCompletionScriptFast, getCompletionsFromManifest, hasAllManifests } from './completion-fast.js';
import { findPackageRoot, getCliManifestPath } from './package-paths.js';
import { configureHostedWorkspaceOption, parseHostedRootCommandSurface, rootCompletionSentinelIndex } from './root-command-surface.js';
import { PKG_VERSION } from './version.js';
import { EXIT_CODES } from './errors.js';
import { isSupportedNodeVersion, MIN_SUPPORTED_NODE_MAJOR } from './runtime-detect.js';
import { CONFIG_DIR_NAME } from './brand.js';
import { configureHostedWorkspaceOption, parseHostedRootCommandSurface, rootCompletionSentinelIndex } from './root-command-surface.js';
import { financePlugin } from '../mcp-skills/financeagent/finance-agent.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand Down
4 changes: 3 additions & 1 deletion src/root-command-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ export function parseHostedRootCommandSurface(argv: readonly string[]): HostedRo
const input = [...argv];

// main.ts checks an exact first-token version before its completion scan.
if (input[0] === '--version' || input[0] === '-V') {
// Commander also treats short version clusters such as -Vx/-Vh as the same
// root-level fast path, so keep those aligned with the actual CLI behavior.
if (input[0] === '--version' || input[0] === '-V' || /^-V(?:[A-Za-z-]|$)/.test(input[0] ?? '')) {
return { kind: 'version', output: `${PKG_VERSION}\n` };
}
// Completion is a Webcmd root sentinel. Once a command or `--` begins the
Expand Down
20 changes: 19 additions & 1 deletion src/site-memory/local-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,12 +324,21 @@ async function walkFiles(root: string, dir = root): Promise<string[]> {

async function readableRelativePath(root: string, path: string): Promise<string> {
const relativePath = safeRelativePath(root, path);
if ((await lstat(join(root, relativePath))).isSymbolicLink()) throw new Error(`Invalid site memory path: ${path}`);
await assertNoSymlinkInPath(root, relativePath, path);
const [realRoot, realTarget] = await Promise.all([realpath(root), realpath(join(root, relativePath))]);
if (realTarget !== realRoot && !realTarget.startsWith(`${realRoot}${sep}`)) throw new Error(`Invalid site memory path: ${path}`);
return relativePath.split(sep).join('/');
}

async function assertNoSymlinkInPath(root: string, relativePath: string, originalPath: string): Promise<void> {
const segments = relativePath.split(/[\\/]+/).filter(Boolean);
let current = root;
for (const segment of segments) {
current = join(current, segment);
if ((await lstat(current)).isSymbolicLink()) throw new Error(`Invalid site memory path: ${originalPath}`);
}
}

function safeRelativePath(root: string, path: string): string {
const target = resolve(root, path);
const resolvedRoot = resolve(root);
Expand Down Expand Up @@ -390,6 +399,15 @@ function validateVerifyFixture(body: string): void {
}

async function assertInsideSiteRoot(root: string, parent: string, path: string): Promise<void> {
const relativeParent = relative(root, parent);
if (relativeParent === '' || relativeParent === '.' || !relativeParent.startsWith('..')) {
const segments = relativeParent.split(/[\\/]+/).filter(Boolean);
let current = root;
for (const segment of segments) {
current = join(current, segment);
if ((await lstat(current)).isSymbolicLink()) throw new Error(`Invalid site memory path: ${path}`);
}
}
const [realRoot, realParent] = await Promise.all([realpath(root), realpath(parent)]);
if (realParent !== realRoot && !realParent.startsWith(`${realRoot}${sep}`)) {
throw new Error(`Invalid site memory path: ${path}`);
Expand Down
Binary file added verify.txt
Binary file not shown.
39 changes: 39 additions & 0 deletions vitest-targeted.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

⎯⎯⎯⎯⎯⎯⎯ Startup Error ⎯⎯⎯⎯⎯⎯⎯⎯
Error: Failed to load custom Reporter from basic
at loadCustomReporterModule (file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:11338:9)
at file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:11354:23
... 3 lines matching cause stack trace ...
at _createServer (file:///C:/Users/srise/webcmd/node_modules/vite/dist/node/chunks/node.js:26145:84)
at createViteServer (file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:8819:17)
at createVitest (file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:14159:18)
at prepareVitest (file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:14526:14)
at startVitest (file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:14469:14) {
[cause]: Error: Failed to load url basic (resolved id: basic). Does the file exist?
at reviveInvokeError (file:///C:/Users/srise/webcmd/node_modules/vite/dist/node/module-runner.js:538:14)
at Object.invoke (file:///C:/Users/srise/webcmd/node_modules/vite/dist/node/module-runner.js:554:33)
at ServerModuleRunner.getModuleInformation (file:///C:/Users/srise/webcmd/node_modules/vite/dist/node/module-runner.js:1183:7)
at ServerModuleRunner.import (file:///C:/Users/srise/webcmd/node_modules/vite/dist/node/module-runner.js:1099:23)
at loadCustomReporterModule (file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:11336:26)
at file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:11354:23
at async Promise.all (index 0)
at Vitest._setServer (file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:13153:138)
at BasicMinimalPluginContext.handler (file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:14125:5)
at _createServer (file:///C:/Users/srise/webcmd/node_modules/vite/dist/node/chunks/node.js:26145:84) {
code: 'ERR_LOAD_URL',
runnerError: Error: RunnerError
at reviveInvokeError (file:///C:/Users/srise/webcmd/node_modules/vite/dist/node/module-runner.js:539:64)
at Object.invoke (file:///C:/Users/srise/webcmd/node_modules/vite/dist/node/module-runner.js:554:33)
at ServerModuleRunner.getModuleInformation (file:///C:/Users/srise/webcmd/node_modules/vite/dist/node/module-runner.js:1183:7)
at ServerModuleRunner.import (file:///C:/Users/srise/webcmd/node_modules/vite/dist/node/module-runner.js:1099:23)
at loadCustomReporterModule (file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:11336:26)
at file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:11354:23
at async Promise.all (index 0)
at Vitest._setServer (file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:13153:138)
at BasicMinimalPluginContext.handler (file:///C:/Users/srise/webcmd/node_modules/vitest/dist/chunks/cli-api.BUXBO6jS.js:14125:5)
at _createServer (file:///C:/Users/srise/webcmd/node_modules/vite/dist/node/chunks/node.js:26145:84)
}
}