Skip to content
Merged
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
47 changes: 10 additions & 37 deletions README.md

Large diffs are not rendered by default.

57 changes: 2 additions & 55 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Tool responses include different data based on the tool's category:
| Category | Examples | Observations in response? |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| **Mutating** | click, type, navigate, launch, cleanup, build, clipboard, cdp, mock_network, scroll_to_element, device_swipe, long_press, tap_coordinates, dismiss_keyboard, dismiss_alert, open_app, close_app, press_button, device_context, device_clipboard | Yes — `state` + `a11y` (compacted) + `testIds` |
| **Read-only** | get_state, get_text, knowledge\_\*, get_context, set_context, get_alert_text, screen_recording, device_logs, generate_locators, hermes_targets | No — faster response |
| **Read-only** | get_state, get_text, get_context, set_context, get_alert_text, screen_recording, device_logs, generate_locators, hermes_targets | No — faster response |
| **Discovery** | describe_screen, list_testids, accessibility_snapshot, screenshot | Data is already in `result` |
| **Batch** | run_steps | Controlled by `includeObservations` param |

Expand Down Expand Up @@ -116,7 +116,7 @@ The `run_steps` tool collects observations once after all steps complete. Contro
Starts the daemon (if not running) and launches a headed Chrome session with the extension.

```
mm launch [--context e2e|prod] [--state default|onboarding|custom] [--extension-path <path>] [--goal <text>] [--force] [--flow-tags <tags>]
mm launch [--context e2e|prod] [--state default|onboarding|custom] [--extension-path <path>] [--force]
```

| Flag | Description |
Expand All @@ -126,9 +126,7 @@ mm launch [--context e2e|prod] [--state default|onboarding|custom] [--extension-
| `--state onboarding` | Fresh wallet requiring manual onboarding setup |
| `--state custom` | Use a custom fixture for wallet state |
| `--extension-path <path>` | Override the extension build directory |
| `--goal <text>` | Tag the session with a goal for knowledge store |
| `--force` | Replace an existing active session |
| `--flow-tags <tags>` | Comma-separated flow tags for cross-session knowledge |

Returns: `sessionId`, `extensionId`, `state` (current extension state).

Expand Down Expand Up @@ -399,42 +397,6 @@ Switches the session environment between `e2e` and `prod` modes. Blocked while a
mm set-context <e2e|prod>
```

### Knowledge Store

The knowledge store records every tool invocation and uses past sessions to suggest actions.

#### `mm knowledge-search <query>`

Searches past sessions for steps matching the query. Matches against tool names, screen names, test IDs, and a11y node names.

```
mm knowledge-search "confirm transaction"
```

#### `mm knowledge-last`

Gets the most recent step records from the current session.

```
mm knowledge-last
```

#### `mm knowledge-sessions`

Lists recent sessions with metadata (goal, flow tags, timestamps).

```
mm knowledge-sessions
```

#### `mm knowledge-summarize`

Generates a recipe-style summary of a session's tool invocations, showing the step sequence with targets and outcomes.

```
mm knowledge-summarize [--session <id>]
```

### Contracts (E2E only)

#### `mm seed-contract <name>`
Expand Down Expand Up @@ -710,7 +672,6 @@ When a command fails, the response includes `error.code`. Use this to decide wha
| `MM_DISCOVERY_FAILED` | Discovery tool failure | Page may be loading; wait and retry |
| `MM_SCREENSHOT_FAILED` | Screenshot capture failure | Page may be in unstable state; retry after describe-screen |
| `MM_STATE_FAILED` | State retrieval failed | Session may be unstable; run `mm describe-screen` |
| `MM_KNOWLEDGE_ERROR` | Knowledge store operation failed | Retry; check that session exists |
| `MM_CONTRACT_NOT_FOUND` | Unknown contract name for seeding | See available contracts below |
| `MM_SEED_FAILED` | Contract deployment failure | Check Anvil chain is running; verify contract name |
| `MM_CAPABILITY_NOT_AVAILABLE` | Feature requires a capability not configured | Check environment mode (e2e vs prod) |
Expand Down Expand Up @@ -746,20 +707,6 @@ These contracts can be deployed to the local Anvil chain via `seed_contract` / `
| `simpleAccountFactory` | ERC-4337 account factory |
| `verifyingPaymaster` | ERC-4337 paymaster |

## Flow Tags

When launching, tag your session with flow tags for cross-session knowledge:

| Tag | Use for |
| ----------------- | ------------------------------ |
| `send` | Token send flows |
| `swap` | Token swap flows |
| `connect` | dApp connection flows |
| `sign` | Message/transaction signing |
| `onboarding` | Wallet setup/onboarding |
| `settings` | Settings configuration |
| `tx-confirmation` | Transaction confirmation flows |

## Daemon Model

- Daemon runs per project, state tracked in `.mm-server` at the project root
Expand Down
111 changes: 35 additions & 76 deletions src/cli/mm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,25 +570,19 @@ describe('parseLaunchArgs', () => {
});
});

it('parses --goal value', () => {
expect(parseLaunchArgs(['--goal', 'test swap'])).toStrictEqual({
goal: 'test swap',
});
});

it('parses --flow-tags as comma-separated array', () => {
expect(parseLaunchArgs(['--flow-tags', 'send, swap'])).toStrictEqual({
flowTags: ['send', 'swap'],
});
});

it('parses multiple flags together', () => {
expect(
parseLaunchArgs(['--state', 'default', '--force', '--goal', 'test it']),
parseLaunchArgs([
'--state',
'default',
'--force',
'--extension-path',
'/ext',
]),
).toStrictEqual({
stateMode: 'default',
force: true,
goal: 'test it',
extensionPath: '/ext',
});
});

Expand All @@ -614,18 +608,6 @@ describe('parseLaunchArgs', () => {
);
});

it('exits for --goal without value', () => {
expect(() => parseLaunchArgs(['--goal'])).toThrowError('process.exit');
expect(stderrSpy).toHaveBeenCalledWith('Error: --goal requires a value\n');
});

it('exits for --flow-tags without value', () => {
expect(() => parseLaunchArgs(['--flow-tags'])).toThrowError('process.exit');
expect(stderrSpy).toHaveBeenCalledWith(
'Error: --flow-tags requires a comma-separated value\n',
);
});

it('parses --context value', () => {
expect(parseLaunchArgs(['--context', 'prod'])).toStrictEqual({
context: 'prod',
Expand Down Expand Up @@ -781,6 +763,18 @@ describe('parseLaunchArgs', () => {
"Warning: unknown launch flag '--unknown'\n",
);
});

it.each(['--goal', '--flow-tags'])(
'treats removed %s flag as unknown',
(flag) => {
const result = parseLaunchArgs([flag, 'x']);
expect(stderrSpy).toHaveBeenCalledWith(
`Warning: unknown launch flag '${flag}'\n`,
);
expect(result).not.toHaveProperty('goal');
expect(result).not.toHaveProperty('flowTags');
},
);
});

describe('printHelp', () => {
Expand Down Expand Up @@ -2045,56 +2039,6 @@ describe('routeCommand', () => {
);
});

it('routes knowledge-search', async () => {
await routeCommand('knowledge-search', ['send flow'], 3000);
expect(globalThis.fetch).toHaveBeenCalledWith(
'http://127.0.0.1:3000/tool/knowledge_search',
expect.objectContaining({
body: JSON.stringify({ query: 'send flow' }),
}),
);
});

it('exits when knowledge-search has no query', async () => {
await expect(
routeCommand('knowledge-search', [], 3000),
).rejects.toThrowError('process.exit');
});

it('routes knowledge-last', async () => {
await routeCommand('knowledge-last', [], 3000);
expect(globalThis.fetch).toHaveBeenCalledWith(
'http://127.0.0.1:3000/tool/knowledge_last',
expect.anything(),
);
});

it('routes knowledge-sessions', async () => {
await routeCommand('knowledge-sessions', [], 3000);
expect(globalThis.fetch).toHaveBeenCalledWith(
'http://127.0.0.1:3000/tool/knowledge_sessions',
expect.anything(),
);
});

it('routes knowledge-summarize', async () => {
await routeCommand('knowledge-summarize', [], 3000);
expect(globalThis.fetch).toHaveBeenCalledWith(
'http://127.0.0.1:3000/tool/knowledge_summarize',
expect.objectContaining({ body: JSON.stringify({}) }),
);
});

it('routes knowledge-summarize with --session', async () => {
await routeCommand('knowledge-summarize', ['--session', 'sid'], 3000);
expect(globalThis.fetch).toHaveBeenCalledWith(
'http://127.0.0.1:3000/tool/knowledge_summarize',
expect.objectContaining({
body: JSON.stringify({ scope: { sessionId: 'sid' } }),
}),
);
});

it('routes run-steps with JSON input', async () => {
const input = JSON.stringify({
steps: [{ tool: 'click', args: { a11yRef: 'e1' } }],
Expand Down Expand Up @@ -2716,6 +2660,21 @@ describe('routeCommand', () => {
expect.stringContaining("unknown command 'unknown-cmd'"),
);
});

it.each([
'knowledge-last',
'knowledge-search',
'knowledge-sessions',
'knowledge-summarize',
])('exits for removed %s command', async (command) => {
await expect(routeCommand(command, [], 3000)).rejects.toThrowError(
'process.exit',
);
expect(stderrSpy).toHaveBeenCalledWith(
expect.stringContaining(`unknown command '${command}'`),
);
expect(globalThis.fetch).not.toHaveBeenCalled();
});
});

describe('discoverDaemon', () => {
Expand Down
64 changes: 2 additions & 62 deletions src/cli/mm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,28 +618,6 @@ export async function routeCommand(
});
break;
}
case 'knowledge-search':
if (!args[0]) {
process.stderr.write('Usage: mm knowledge-search <query>\n');
process.exit(1);
}
await sendRequest(port, 'POST', '/tool/knowledge_search', {
query: args[0],
});
break;
case 'knowledge-last':
await sendRequest(port, 'POST', '/tool/knowledge_last', {});
break;
case 'knowledge-sessions':
await sendRequest(port, 'POST', '/tool/knowledge_sessions', {});
break;
case 'knowledge-summarize': {
const summarizeSession = parseStringFlag(args, '--session');
await sendRequest(port, 'POST', '/tool/knowledge_summarize', {
...(summarizeSession ? { scope: { sessionId: summarizeSession } } : {}),
});
break;
}
case 'run-steps':
if (!args[0]) {
process.stderr.write(
Expand Down Expand Up @@ -1637,22 +1615,6 @@ export async function readSelfOnboardingInput(
*/
export function parseLaunchArgs(args: string[]): Record<string, unknown> {
const result: Record<string, unknown> = {};
const knownFlags = new Set([
'--context',
'--state',
'--extension-path',
'--goal',
'--force',
'--flow-tags',
'--platform',
'--device-id',
'--app-bundle',
'--metro-port',
'--reinstall',
'--reset-app-data',
'--allow-fox-code-mismatch',
'--self-onboarding',
]);

for (let i = 0; i < args.length; i++) {
const arg = args[i];
Expand Down Expand Up @@ -1681,22 +1643,6 @@ export function parseLaunchArgs(args: string[]): Record<string, unknown> {
process.exit(1);
}
result.extensionPath = args[i];
} else if (arg === '--goal') {
i += 1;
if (!args[i] || args[i].startsWith('--')) {
process.stderr.write('Error: --goal requires a value\n');
process.exit(1);
}
result.goal = args[i];
} else if (arg === '--flow-tags') {
i += 1;
if (!args[i] || args[i].startsWith('--')) {
process.stderr.write(
'Error: --flow-tags requires a comma-separated value\n',
);
process.exit(1);
}
result.flowTags = args[i].split(',').map((tag) => tag.trim());
} else if (arg === '--platform') {
i += 1;
if (!args[i] || args[i].startsWith('--')) {
Expand Down Expand Up @@ -1745,7 +1691,7 @@ export function parseLaunchArgs(args: string[]): Record<string, unknown> {
result.allowFoxCodeMismatch = true;
} else if (arg === '--self-onboarding') {
result.selfOnboarding = true;
} else if (arg.startsWith('--') && !knownFlags.has(arg)) {
} else if (arg.startsWith('--')) {
process.stderr.write(`Warning: unknown launch flag '${arg}'\n`);
}
}
Expand All @@ -1769,7 +1715,7 @@ Environment Variables:
Falls back to the current git worktree root.

Lifecycle:
mm launch [--context e2e|prod] [--state default|onboarding|custom] [--self-onboarding] [--extension-path <path>] [--goal <text>] [--force] [--flow-tags <tags>] [--platform browser|ios|android] [--device-id <id>]
mm launch [--context e2e|prod] [--state default|onboarding|custom] [--self-onboarding] [--extension-path <path>] [--force] [--platform browser|ios|android] [--device-id <id>]
mm launch --platform ios|android [--device-id <id>] [--app-bundle <path>] [--metro-port <port>] [--reinstall] [--reset-app-data] [--allow-fox-code-mismatch]
mm launch --self-onboarding reads the secret recovery phrase from stdin,
and an optional vault password from the next line.
Expand Down Expand Up @@ -1805,12 +1751,6 @@ State & Context:
mm get-context
mm set-context <e2e|prod>

Knowledge:
mm knowledge-search <query>
mm knowledge-last
mm knowledge-sessions
mm knowledge-summarize [--session <id>]

Contracts (E2E only):
mm seed-contract <name> [--hardfork <fork>]
mm seed-contracts <name1> <name2> ... [--hardfork <fork>]
Expand Down
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ export type {
} from './server/session-manager.js';

// Core Components
export * from './knowledge-store/knowledge-store.js';
export * from './tools/utils/discovery.js';
export * from './validation/schemas.js';
export * from './knowledge-store/tokenization.js';

// Types
export * from './tools/types';
Expand All @@ -41,6 +39,7 @@ export * from './server/request-queue.js';
export * from './server/port-allocator.js';
export * from './server/daemon-state.js';
export * from './server/create-server.js';
export * from './server/session-metadata.js';

// Utils
export * from './utils';
Expand Down
Loading
Loading