Skip to content

Commit 3bc8463

Browse files
authored
Merge pull request #8 from framerslab/feat/onboarding-pages
Port the guides, tutorials and examples pages and the Manic Agency README section from main
2 parents 954847f + 900d87c commit 3bc8463

22 files changed

Lines changed: 1050 additions & 261 deletions

‎.github/workflows/app-checks.yml‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: App checks
2+
3+
# Quick checks for pull requests that touch the Next.js app: a type check of
4+
# app/, the site page tests, and the tests of the CLI pieces the pages
5+
# describe (where `wunderland start` listens, what PresetLoader returns).
6+
# The CLI's own dependencies live in the parent monorepo, so those tests run
7+
# here with the app's vitest.
8+
on:
9+
pull_request:
10+
branches: [master]
11+
paths:
12+
- 'app/**'
13+
- 'src/cli/**'
14+
- 'src/core/PresetLoader.ts'
15+
- 'presets/agents/**'
16+
- '.github/workflows/app-checks.yml'
17+
18+
concurrency:
19+
group: app-checks-${{ github.event.pull_request.number }}
20+
cancel-in-progress: true
21+
22+
permissions:
23+
contents: read
24+
25+
jobs:
26+
app:
27+
name: App type check + site and CLI tests
28+
runs-on: ubuntu-latest
29+
timeout-minutes: 20
30+
defaults:
31+
run:
32+
working-directory: app
33+
steps:
34+
# Actions are pinned to the commit of a release; the comment names it.
35+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
36+
with:
37+
# No step pushes, so the token stays out of .git/config.
38+
persist-credentials: false
39+
40+
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
41+
with:
42+
node-version: 22
43+
44+
# app/package-lock.json trails app/package.json, so `npm ci` refuses it;
45+
# `npm install` resolves the ranges package.json declares.
46+
- name: Install app dependencies
47+
id: install
48+
run: npm install --no-audit --no-fund
49+
50+
# Both checks run once the install succeeds, so one push reports both.
51+
- name: Type check
52+
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
53+
run: npx tsc --noEmit
54+
55+
- name: Site page and CLI tests
56+
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
57+
run: >-
58+
npx vitest run
59+
src/__tests__/site-pages.test.tsx
60+
src/__tests__/cli-start-listen.test.ts
61+
src/__tests__/preset-loader.test.ts

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
## [Unreleased]
44

5+
### Added
6+
- app: `/guides`, `/tutorials` and `/examples` pages built from `app/src/app/site-content.ts`, a Docs menu in the nav that links them, links in the mobile menu and footer, and sitemap entries. Ported from the `main` line (6c5a306f).
7+
- docs: a "Built by Manic Agency" README section, ported from the `main` line (e64b11cc).
8+
- ci: an App checks workflow that type-checks `app/` and runs the site page tests, and the tests of the CLI pieces those pages describe, on pull requests that touch them. Its actions are pinned to release commits and its checkout keeps the job token out of `.git/config`.
9+
10+
### Fixed
11+
- cli: PresetLoader dropped a preset's `suggestedExtensions`, `extensionOverrides` and `toolAccessProfile`, so `wunderland init --preset devops-assistant` (or `data-analyst`) wrote no `extensions` into `agent.config.json` and `wunderland chat` loaded its default set instead of the shell executor and browser the preset names. The loader returns all three, and the Ops Assistant example says what init writes and chat loads.
12+
- cli: `wunderland start` listened on 0.0.0.0, so any machine that could reach port 3777 could send `POST /chat`, which has no authentication, and spend the agent's model key. It listens on 127.0.0.1 unless `--host` names another address (a bracketed IPv6 address such as `[::1]` works too), prints the address it listens on, and warns when other machines can connect. `wunderland --help` lists `--host`, and the local-server tutorial says how the server listens.
13+
- app: `site-content.ts` showed CLI commands that `src/cli` does not handle (`extensions add`, `voice test`, `workflows create`, `cron add`) and voice, provider and library claims the CLI does not back. The copy now follows `src/cli`, and a test checks every command it shows.
14+
- app: the JSON-LD license links pointed at `blob/main/LICENSE`, a branch with no LICENSE file. They point at master.
15+
- app: `tweetnacl` is declared in `app/package.json`. `SolanaWalletProvider` imports the test wallet adapter that uses it, so the app built only when a workspace install hoisted it from another package.
16+
- docs: the README license badge and section said MIT. Versions 0.1.1 and later are Apache-2.0, as LICENSE and NOTICE state.
17+
518
### Updated
619
- docs: align Wunderland ON SOL docs with AgentOS/Wunderland adaptive execution and schema-on-demand capability loading flow.
720
- docs: clarify integration points with `@framers/sql-storage-adapter` and discovery-driven runtime behavior.

‎README.md‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
<p align="center">
2828
<a href="https://www.npmjs.com/package/wunderland"><img src="https://img.shields.io/npm/v/wunderland.svg" alt="npm version" /></a>
29-
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
29+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="License: Apache 2.0" /></a>
3030
</p>
3131

3232
---
@@ -91,7 +91,7 @@ cd docs-site && npm start
9191

9292
## What's Inside
9393

94-
- **`app/`** — Next.js 15 frontend. Product landing page, agent browser, posts feed, leaderboard, mint wizard, tipping, jobs board, rewards. Solana wallet adapters (Phantom, Solflare). Tailwind CSS 4, Vitest, Playwright.
94+
- **`app/`** — Next.js 15 frontend. Product landing page, agent browser, posts feed, leaderboard, mint wizard, tipping, jobs board, rewards, and the `/guides`, `/tutorials` and `/examples` pages that link into the docs. Solana wallet adapters (Phantom, Solflare). Tailwind CSS 4, Vitest, Playwright.
9595

9696
- **`docs-site/`** — Docusaurus 3.9 documentation portal. 48 hand-written guides plus an auto-generated TypeDoc API reference. Covers all 12 wunderland modules: personality, security, inference, authorization, social, channels, tools, skills, scheduling, browser automation, and more.
9797

@@ -124,6 +124,8 @@ The full developer reference lives at [docs.wunderland.sh](https://docs.wunderla
124124
- **29 Guides** — Security pipeline, step-up auth, inference routing, channels, tools, CLI, deployment
125125
- **API Reference** — Auto-generated from TypeDoc
126126

127+
The app's `/guides`, `/tutorials` and `/examples` pages index these docs. Their copy lives in [`app/src/app/site-content.ts`](app/src/app/site-content.ts), and a test checks every `wunderland` command it shows against the CLI in [`src/cli`](src/cli).
128+
127129
To build docs locally: `cd docs-site && npm run build`
128130

129131
---
@@ -209,6 +211,15 @@ See the `.env.example` files for the full list with descriptions.
209211

210212
---
211213

214+
## Built by Manic Agency
215+
216+
Wunderland on Sol is built by [Manic Agency LLC](https://manic.agency). Framers Lab, Inc. holds the copyright; see [`NOTICE`](NOTICE).
217+
218+
- **Website**: [manic.agency](https://manic.agency)
219+
- **GitHub**: [github.com/manicinc](https://github.com/manicinc)
220+
221+
---
222+
212223
## License
213224

214-
MIT
225+
Versions 0.1.1 and later are licensed under the [Apache License 2.0](LICENSE). Versions 0.1.0 and earlier were released under the MIT License, and those grants still hold. See [`NOTICE`](NOTICE).

‎app/e2e/navigation.spec.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,33 @@ test.describe('Navigation', () => {
3939
await expect(page).toHaveURL('/agents', { timeout: 60_000 });
4040
});
4141

42+
test('docs menu navigates to the guides, tutorials and examples pages', async ({ page }) => {
43+
for (const { href, heading } of [
44+
{ href: '/guides', heading: /^Guides$/ },
45+
{ href: '/tutorials', heading: /^Tutorials$/ },
46+
{ href: '/examples', heading: /^Examples$/ },
47+
]) {
48+
// Use a full load for nav interactions; the navbar hydrates asynchronously in production builds.
49+
await page.goto('/', { waitUntil: 'load' });
50+
const docsBtn = page.getByRole('navigation').getByRole('button', { name: /^Docs$/i });
51+
const menu = page.locator('#docs-menu');
52+
await expect
53+
.poll(
54+
async () => {
55+
if (await menu.isVisible()) return true;
56+
await docsBtn.click();
57+
await page.waitForTimeout(50);
58+
return menu.isVisible();
59+
},
60+
{ timeout: 60_000 },
61+
)
62+
.toBe(true);
63+
await menu.locator(`a[href="${href}"]`).click({ force: true });
64+
await expect(page).toHaveURL(href, { timeout: 60_000 });
65+
await expect(page.getByRole('heading', { level: 1, name: heading })).toBeVisible();
66+
}
67+
});
68+
4269
test('feed page has sort tabs', async ({ page }) => {
4370
await page.goto('/feed');
4471
await expect(page.getByRole('button', { name: /new/i })).toBeVisible();

‎app/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"next": "^15.1.0",
2525
"react": "^19.0.0",
2626
"react-dom": "^19.0.0",
27-
"styled-jsx": "5.1.6"
27+
"styled-jsx": "5.1.6",
28+
"tweetnacl": "^1.0.3"
2829
},
2930
"overrides": {
3031
"@types/react": "$@types/react",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* Where `wunderland start` listens.
3+
*
4+
* The CLI's own dependencies come from the parent monorepo, so this job
5+
* cannot import src/cli/commands/start.ts. It binds real servers through the
6+
* helper start.ts uses and reads start.ts to check that it uses it.
7+
*/
8+
import { readFileSync } from 'node:fs';
9+
import { createServer, type Server } from 'node:http';
10+
import path from 'node:path';
11+
import { afterEach, describe, expect, it } from 'vitest';
12+
13+
import {
14+
DEFAULT_LISTEN_HOST,
15+
displayHost,
16+
isLoopbackHost,
17+
listenOn,
18+
resolveListenHost,
19+
} from '../../../src/cli/config/listen-host';
20+
21+
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
22+
const START_SOURCE = path.join(REPO_ROOT, 'src', 'cli', 'commands', 'start.ts');
23+
24+
const servers: Server[] = [];
25+
26+
function newServer(): Server {
27+
const server = createServer((_req, res) => res.end('ok'));
28+
servers.push(server);
29+
return server;
30+
}
31+
32+
afterEach(async () => {
33+
await Promise.all(
34+
servers.splice(0).map((server) => new Promise<void>((resolve) => server.close(() => resolve()))),
35+
);
36+
});
37+
38+
describe('wunderland start listen address', () => {
39+
it('listens on 127.0.0.1 when --host is not given', async () => {
40+
expect(DEFAULT_LISTEN_HOST).toBe('127.0.0.1');
41+
const address = await listenOn(newServer(), 0, resolveListenHost(undefined));
42+
expect(address.address).toBe('127.0.0.1');
43+
});
44+
45+
it('listens on the address --host names', async () => {
46+
const address = await listenOn(newServer(), 0, resolveListenHost('0.0.0.0'));
47+
expect(address.address).toBe('0.0.0.0');
48+
});
49+
50+
it('drops the brackets from an IPv6 --host, which server.listen cannot resolve', () => {
51+
expect(resolveListenHost('[::1]')).toBe('::1');
52+
expect(resolveListenHost('::1')).toBe('::1');
53+
expect(resolveListenHost('[not-an-address]')).toBe('[not-an-address]');
54+
});
55+
56+
it('refuses --host without an address instead of guessing one', () => {
57+
expect(() => resolveListenHost(true)).toThrow(/--host needs an address/);
58+
expect(() => resolveListenHost(' ')).toThrow(/--host needs an address/);
59+
});
60+
61+
it('rejects when the port is taken', async () => {
62+
const first = await listenOn(newServer(), 0, DEFAULT_LISTEN_HOST);
63+
await expect(listenOn(newServer(), first.port, DEFAULT_LISTEN_HOST)).rejects.toMatchObject({
64+
code: 'EADDRINUSE',
65+
});
66+
});
67+
68+
it('tells loopback addresses from addresses other machines can reach', () => {
69+
for (const host of ['127.0.0.1', '127.0.0.2', 'localhost', '::1', '[::1]']) {
70+
expect(isLoopbackHost(host), host).toBe(true);
71+
}
72+
for (const host of ['0.0.0.0', '::', '192.168.1.20', 'example.com']) {
73+
expect(isLoopbackHost(host), host).toBe(false);
74+
}
75+
});
76+
77+
it('prints a wildcard address as localhost and brackets IPv6', () => {
78+
expect(displayHost('0.0.0.0')).toBe('localhost');
79+
expect(displayHost('::')).toBe('localhost');
80+
expect(displayHost('127.0.0.1')).toBe('127.0.0.1');
81+
expect(displayHost('::1')).toBe('[::1]');
82+
});
83+
84+
it('start.ts listens through the helper on the --host address, never a fixed 0.0.0.0', () => {
85+
const source = readFileSync(START_SOURCE, 'utf8');
86+
expect(source).not.toContain("'0.0.0.0'");
87+
expect(source).toContain("resolveListenHost(flags['host'])");
88+
expect(source).toContain('listenOn(server, port, host)');
89+
});
90+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* PresetLoader against the presets shipped in presets/agents.
3+
*
4+
* `wunderland init --preset <id>` writes the loaded preset's
5+
* suggestedExtensions into agent.config.json as `extensions`, and
6+
* `wunderland chat` loads exactly that list when it is present.
7+
*/
8+
import { readFileSync } from 'node:fs';
9+
import path from 'node:path';
10+
import { describe, expect, it } from 'vitest';
11+
12+
import { PresetLoader } from '../../../src/core/PresetLoader';
13+
14+
const PRESETS_DIR = path.resolve(__dirname, '..', '..', '..', 'presets');
15+
16+
function presetConfig(id: string): Record<string, unknown> {
17+
return JSON.parse(readFileSync(path.join(PRESETS_DIR, 'agents', id, 'agent.config.json'), 'utf8'));
18+
}
19+
20+
describe('PresetLoader with the shipped presets', () => {
21+
const loader = new PresetLoader(PRESETS_DIR);
22+
23+
it.each(['devops-assistant', 'data-analyst'])(
24+
'%s keeps the extensions and tool access profile its config names',
25+
(id) => {
26+
const preset = loader.loadPreset(id);
27+
const config = presetConfig(id);
28+
expect(config.suggestedExtensions).toBeDefined();
29+
expect(preset.suggestedExtensions).toEqual(config.suggestedExtensions);
30+
expect(preset.toolAccessProfile).toBe(config.toolAccessProfile);
31+
},
32+
);
33+
34+
it('gives devops-assistant the shell executor and the browser', () => {
35+
expect(loader.loadPreset('devops-assistant').suggestedExtensions?.tools).toEqual([
36+
'cli-executor',
37+
'web-browser',
38+
]);
39+
});
40+
41+
it('leaves extensions unset for a preset that names none, so chat keeps its default set', () => {
42+
expect(presetConfig('research-assistant').suggestedExtensions).toBeUndefined();
43+
expect(loader.loadPreset('research-assistant').suggestedExtensions).toBeUndefined();
44+
});
45+
46+
it('returns the same fields from listPresets', () => {
47+
const devops = loader.listPresets().find((preset) => preset.id === 'devops-assistant');
48+
expect(devops?.suggestedExtensions).toEqual(presetConfig('devops-assistant').suggestedExtensions);
49+
});
50+
});

0 commit comments

Comments
 (0)