Skip to content
Draft
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
109 changes: 109 additions & 0 deletions plugins/meridian/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# webcmd-plugin-meridian

Webcmd commands for [Meridian](https://app.getmeridian.tech) (getmeridian.tech), the founder copilot whose Astra agent takes an idea from ideation to validated market intelligence. The plugin drives the same APIs the Meridian app uses, through a logged-in Webcmd browser profile.

## Install

```bash
webcmd plugin install github:agentrhq/webcmd/meridian
```

## Authorize your Meridian account

Every command runs against your Meridian account, so authorize the browser profile first:

```bash
webcmd meridian login
```

This opens Meridian in the Webcmd browser. Sign in — or sign up if you have no account yet (email/password or Google; new email accounts must verify their address before login works). Then confirm:

```bash
webcmd meridian whoami
```

Commands raise a typed auth error (instead of empty results) whenever the session is missing or expired.

## Commands

| Command | Description |
| --- | --- |
| `webcmd meridian login` | Open Meridian sign-in/sign-up to authorize this browser profile |
| `webcmd meridian whoami` | Show the signed-in Meridian account (email, org, credits) |
| `webcmd meridian projects` | List the projects (initiatives) in your account |
| `webcmd meridian ideate` | One turn of the Astra ideation chat; repeat until stage=ready_to_start |
| `webcmd meridian approve` | Approve the ready draft and start the project |
| `webcmd meridian persona` | Build and save an ideal-user persona from behaviours or interview notes |
| `webcmd meridian personas` | List the personas saved on a project |
| `webcmd meridian competitors` | List a project's competitor board, prioritized by relevance |
| `webcmd meridian competitor-add` | Add a competitor by name or website |
| `webcmd meridian competitor-scan` | Rebuild the prioritized competitor landscape |
| `webcmd meridian agent-status` | Show the Astra background agent and its branch states |
| `webcmd meridian agent-start` | Start/resume the Astra market-intelligence agent (`--scan` for an immediate tick) |
| `webcmd meridian agent-pause` | Pause the Astra background agent |

## Workflows

### Ideation → Approve & Start

Astra interviews you about the idea and web-researches the market. Keep answering until the readiness gate opens (it needs `problem_statement`, `solution`, and `market_opportunity` confidence, and at least three founder turns):

```bash
webcmd meridian ideate "AI agents that QA mobile apps before release" -f json
webcmd meridian ideate "Mobile teams at seed-stage startups; they ship weekly" -f json
webcmd meridian ideate "They find UI regressions only after users complain" --research -f json
```

Each turn returns the running summary — problem statement, solution, market opportunity, differentiation — plus readiness scores, tap-to-answer `suggestions`, and the web-research `research_sources` behind Astra's context. When `stage` is `ready_to_start`, the row also carries the three-stage plan. Then:

```bash
webcmd meridian approve -f json
```

Astra creates the project and sets it up (starter persona, competitor scan, first signals). The conversation state lives in the persistent browser session; `--reset` starts a fresh draft.

### Ideal-user personas

From observed behaviour, or from uploaded user-interview notes:

```bash
webcmd meridian persona <project-id> "Churned users all mention onboarding friction" -f json
webcmd meridian persona <project-id> --file ./interviews/batch-3.md -f json
webcmd meridian personas <project-id> -f json
```

When Astra marks the draft ready, the persona is saved to your Meridian account automatically (OCEAN traits included).

To mine a Reddit community for persona signals, compose with the `reddit` plugin: pull the community and its top discussions (`webcmd reddit subreddit-info r/mobiledev`, `webcmd reddit subreddit r/mobiledev --limit 25`), then feed the recurring behaviours and complaints into `webcmd meridian persona`.

### Competitor research

Meridian's own scan builds and prioritizes the board for the project context:

```bash
webcmd meridian competitor-scan <project-id>
webcmd meridian competitors <project-id> -f json
```

To widen the net, source candidates with the research plugins first — `webcmd ycombinator companies "mobile testing"`, `webcmd hackernews search "mobile app QA"`, `webcmd producthunt search "app testing"` — then add the credible ones so the next scan positions them:

```bash
webcmd meridian competitor-add <project-id> acme.ai
webcmd meridian competitor-scan <project-id>
```

### Market intelligence (Astra background agent)

```bash
webcmd meridian agent-start <project-id> --scan
webcmd meridian agent-status <project-id> -f json
webcmd meridian agent-pause <project-id>
```

The agent runs 24×7 server-side across parallel branches (market research, synthesis, planning, execution); `agent-status` shows each branch's state and any checkpoints awaiting a human decision.

## Notes

- Meridian meters some actions in account credits (new project, competitor scan/add, research turns). Commands surface the API's insufficient-credit errors as actionable messages.
- Astra turns can take a while (LLM + web research); every long-running command takes `--timeout`.
- Chat drafts (ideation, persona) keep their conversation state in the persistent Webcmd browser session; ideation drafts are also autosaved to your account so they stay resumable in the Meridian app.
32 changes: 32 additions & 0 deletions plugins/meridian/agent-pause.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { cli, Strategy } from '@agentrhq/webcmd/registry';
import { MERIDIAN_DOMAIN, apiFetch, normalizeText, requireProjectId } from './utils.js';

export const agentPauseCommand = cli({
site: 'meridian',
name: 'agent-pause',
access: 'write',
description: 'Pause the Astra background agent on a Meridian project',
example: 'webcmd meridian agent-pause <project-id>',
domain: MERIDIAN_DOMAIN,
strategy: Strategy.COOKIE,
browser: true,
navigateBefore: false,
siteSession: 'persistent',
args: [
{ name: 'project', positional: true, required: true, help: 'Meridian project id (see: webcmd meridian projects)' },
],
columns: ['status', 'detail', 'next'],
func: async (page, kwargs) => {
const projectId = requireProjectId(kwargs.project);
const instance = await apiFetch(page, `/astra/initiatives/${encodeURIComponent(projectId)}/pause`, {
method: 'POST',
body: {},
label: 'agent pause',
});
return [{
status: normalizeText(instance?.status) || 'PAUSED',
detail: 'Astra background agent paused; resume any time to continue market intelligence',
next: `webcmd meridian agent-start ${projectId}`,
}];
},
});
45 changes: 45 additions & 0 deletions plugins/meridian/agent-start.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { cli, Strategy } from '@agentrhq/webcmd/registry';
import { MERIDIAN_DOMAIN, apiFetch, normalizeText, parseBoolFlag, requireBoundedInt, requireProjectId } from './utils.js';

export const agentStartCommand = cli({
site: 'meridian',
name: 'agent-start',
access: 'write',
description: 'Start (or resume) the Astra market-intelligence background agent on a Meridian project',
example: 'webcmd meridian agent-start <project-id> --scan',
domain: MERIDIAN_DOMAIN,
strategy: Strategy.COOKIE,
browser: true,
navigateBefore: false,
siteSession: 'persistent',
args: [
{ name: 'project', positional: true, required: true, help: 'Meridian project id (see: webcmd meridian projects)' },
{ name: 'scan', type: 'boolean', default: false, help: 'Also run one blocking market-research scan tick right now' },
{ name: 'timeout', type: 'int', default: 240, help: 'Max seconds to wait for the --scan tick (30-600)' },
],
columns: ['status', 'detail', 'next'],
func: async (page, kwargs) => {
const projectId = requireProjectId(kwargs.project);
const timeoutSeconds = requireBoundedInt(kwargs.timeout, 240, 30, 600, 'meridian agent-start --timeout');
const instance = await apiFetch(page, `/astra/initiatives/${encodeURIComponent(projectId)}/resume`, {
method: 'POST',
body: {},
label: 'agent resume',
});
let detail = `Astra agent is ${normalizeText(instance?.status) || 'ACTIVE'}; it keeps researching, validating, and positioning in the background`;
if (parseBoolFlag(kwargs.scan)) {
const scanned = await apiFetch(page, `/astra/initiatives/${encodeURIComponent(projectId)}/scan-now`, {
method: 'POST',
body: {},
timeoutSeconds,
label: 'agent scan',
});
detail += ` | scan tick: ${normalizeText(scanned?.status) || 'done'}`;
}
return [{
status: normalizeText(instance?.status) || 'ACTIVE',
detail,
next: `webcmd meridian agent-status ${projectId}`,
}];
},
});
50 changes: 50 additions & 0 deletions plugins/meridian/agent-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { cli, Strategy } from '@agentrhq/webcmd/registry';
import { CommandExecutionError } from '@agentrhq/webcmd/errors';
import { MERIDIAN_DOMAIN, apiFetch, normalizeText, requireProjectId } from './utils.js';

export const agentStatusCommand = cli({
site: 'meridian',
name: 'agent-status',
access: 'read',
description: 'Show the Astra background agent status and branch states for a Meridian project',
example: 'webcmd meridian agent-status <project-id> -f json',
domain: MERIDIAN_DOMAIN,
strategy: Strategy.COOKIE,
browser: true,
navigateBefore: false,
siteSession: 'persistent',
args: [
{ name: 'project', positional: true, required: true, help: 'Meridian project id (see: webcmd meridian projects)' },
],
columns: ['branch', 'state', 'stage', 'summary'],
func: async (page, kwargs) => {
const projectId = requireProjectId(kwargs.project);
const overview = await apiFetch(page, `/astra/initiatives/${encodeURIComponent(projectId)}/overview`, {
label: 'agent status',
});
if (!overview || typeof overview !== 'object') {
throw new CommandExecutionError('Meridian agent status returned an unreadable response');
}
const awaiting = Number(overview.awaiting_human);
const rootSummary = [
normalizeText(overview.initiative_name),
Number.isFinite(awaiting) && awaiting > 0 ? `${awaiting} checkpoint(s) awaiting you` : '',
].filter(Boolean).join(' — ');
const rows = [{
branch: 'root',
state: normalizeText(overview.status) || 'UNKNOWN',
stage: normalizeText(overview.root_state) || null,
summary: rootSummary || null,
}];
const branches = overview.branches && typeof overview.branches === 'object' ? overview.branches : {};
for (const [branch, info] of Object.entries(branches)) {
rows.push({
branch,
state: normalizeText(info?.state) || 'UNKNOWN',
stage: normalizeText(info?.stage) || null,
summary: normalizeText(info?.summary) || null,
});
}
return rows;
},
});
108 changes: 108 additions & 0 deletions plugins/meridian/approve.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { cli, Strategy } from '@agentrhq/webcmd/registry';
import { ArgumentError, CommandExecutionError } from '@agentrhq/webcmd/errors';
import {
IDEATION_STATE_KEY, MERIDIAN_APP_ORIGIN, MERIDIAN_DOMAIN,
apiFetch, clearPageState, loadPageState, normalizeText, parseBoolFlag, pollJob, requireBoundedInt,
} from './utils.js';

// Same payload mapping as the app's buildInitiativeDraft — the backend expects
// this exact initiative shape on start-initiative-job.
function buildInitiativePayload(summary) {
return {
name: normalizeText(summary.initiative_name)
|| normalizeText(summary.solution || summary.problem_statement || 'New Project').split(/[.!?]/)[0].slice(0, 60),
problem_statement: summary.problem_statement || '',
value_proposition: summary.solution || '',
product_definition: summary.solution || '',
market_definition: summary.market_opportunity || '',
current_stage: summary.current_stage || 'IDEATION',
goal_stage: summary.goal_stage || 'PMF',
burn_rate_monthly: summary.burn_rate_monthly ?? null,
operating_capital: summary.operating_capital ?? null,
goal_eta: summary.goal_eta || '',
repo_ids: [],
};
}

export const approveCommand = cli({
site: 'meridian',
name: 'approve',
access: 'write',
description: 'Approve the ready ideation draft and start the project in your Meridian account (costs Meridian credits)',
example: 'webcmd meridian approve -f json',
domain: MERIDIAN_DOMAIN,
strategy: Strategy.COOKIE,
browser: true,
navigateBefore: false,
siteSession: 'persistent',
args: [
{ name: 'force', type: 'boolean', default: false, help: 'Start even if Astra has not marked the draft ready' },
{ name: 'timeout', type: 'int', default: 360, help: 'Max seconds to wait for Astra to set the project up (30-600)' },
],
columns: ['status', 'project_id', 'project_name', 'url', 'detail'],
func: async (page, kwargs) => {
const timeoutSeconds = requireBoundedInt(kwargs.timeout, 360, 30, 600, 'meridian approve --timeout');
const state = await loadPageState(page, IDEATION_STATE_KEY, { messages: [] });
if (!Array.isArray(state.messages) || !state.messages.length || !state.assessment) {
throw new ArgumentError(
'no ideation draft found in this browser session',
'Draft one first: webcmd meridian ideate "<your idea>"',
);
}
if (!state.ready && !parseBoolFlag(kwargs.force)) {
throw new ArgumentError(
'the ideation draft has not reached the Approve & Start stage yet',
'Keep answering Astra via `webcmd meridian ideate "..."` until stage=ready_to_start, or pass --force.',
);
}

const summary = state.assessment.summary && typeof state.assessment.summary === 'object'
? state.assessment.summary
: {};
const started = await apiFetch(page, '/astra/onboarding/start-initiative-job', {
method: 'POST',
body: {
initiative: buildInitiativePayload(summary),
messages: state.messages.map((entry) => ({ role: entry.role, content: entry.content })),
links: Array.isArray(state.links) ? state.links : [],
documents: Array.isArray(state.documents) ? state.documents : [],
assessment: state.assessment,
},
timeoutSeconds: 90,
label: 'project start',
});
const job = await pollJob(page, started?.job_id, { timeoutSeconds, label: 'project setup' });

const initiative = job?.result?.initiative;
const projectId = String(initiative?.id ?? '');
if (!projectId) {
throw new CommandExecutionError(
`Meridian project setup finished without a project id: ${JSON.stringify(job?.result ?? {}).slice(0, 300)}`,
);
}
// The draft became a real project — retire the saved draft card. Best
// effort: the project already exists even if this cleanup fails.
if (state.session_id) {
try {
await apiFetch(page, `/astra/onboarding/draft-sessions/${encodeURIComponent(state.session_id)}/discard`, {
method: 'POST',
body: {},
timeoutSeconds: 30,
label: 'draft discard',
});
} catch {
// Non-fatal cleanup.
}
}
await clearPageState(page, IDEATION_STATE_KEY);

const redirect = normalizeText(job?.result?.redirect);
return [{
status: 'started',
project_id: projectId,
project_name: normalizeText(initiative?.name) || null,
url: `${MERIDIAN_APP_ORIGIN}${redirect || `/app/initiatives/${projectId}/dashboard`}`,
detail: 'Astra set the project up (starter persona, competitor scan, first signals). Next: webcmd meridian agent-status ' + projectId,
}];
},
});
Loading