From bd0cf34aa3f99c1415486a5cd7e7da1fd008bf66 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 9 Jul 2026 04:19:23 +0000 Subject: [PATCH] feat: add phase 1 runnable skeleton --- PROJECT_STRUCTURE.md | 5 +- README.md | 2 +- apps/companion/package.json | 1 + apps/companion/src/app.ts | 20 ++ apps/companion/src/index.ts | 13 + apps/companion/src/routes/health.ts | 21 ++ apps/companion/src/system/hermesHealth.ts | 26 ++ apps/companion/tsconfig.json | 1 + apps/mobile/index.html | 14 + apps/mobile/package.json | 1 + apps/mobile/public/manifest.webmanifest | 1 + apps/mobile/src/app/App.tsx | 26 ++ apps/mobile/src/app/navigation.ts | 11 + .../src/components/app-shell/AppShell.tsx | 28 ++ .../src/components/bottom-nav/BottomNav.tsx | 31 +++ apps/mobile/src/main.tsx | 10 + .../screens/ActivityScreen/ActivityScreen.tsx | 21 ++ .../src/screens/AskScreen/AskScreen.tsx | 28 ++ .../src/screens/CronScreen/CronScreen.tsx | 10 + .../src/screens/FilesScreen/FilesScreen.tsx | 10 + .../screens/SettingsScreen/SettingsScreen.tsx | 12 + apps/mobile/src/styles/globals.css | 256 ++++++++++++++++++ apps/mobile/tsconfig.json | 1 + apps/mobile/vite.config.ts | 9 + docs/ROADMAP.md | 4 +- eslint.config.js | 31 +++ package.json | 1 + packages/shared/package.json | 1 + packages/shared/src/api.ts | 21 ++ packages/shared/src/events.ts | 42 +++ packages/shared/src/index.ts | 3 + packages/shared/src/tasks.ts | 14 + packages/shared/tsconfig.json | 1 + pnpm-workspace.yaml | 3 + tsconfig.base.json | 1 + 35 files changed, 675 insertions(+), 5 deletions(-) create mode 100644 apps/companion/package.json create mode 100644 apps/companion/src/app.ts create mode 100644 apps/companion/src/index.ts create mode 100644 apps/companion/src/routes/health.ts create mode 100644 apps/companion/src/system/hermesHealth.ts create mode 100644 apps/companion/tsconfig.json create mode 100644 apps/mobile/index.html create mode 100644 apps/mobile/package.json create mode 100644 apps/mobile/public/manifest.webmanifest create mode 100644 apps/mobile/src/app/App.tsx create mode 100644 apps/mobile/src/app/navigation.ts create mode 100644 apps/mobile/src/components/app-shell/AppShell.tsx create mode 100644 apps/mobile/src/components/bottom-nav/BottomNav.tsx create mode 100644 apps/mobile/src/main.tsx create mode 100644 apps/mobile/src/screens/ActivityScreen/ActivityScreen.tsx create mode 100644 apps/mobile/src/screens/AskScreen/AskScreen.tsx create mode 100644 apps/mobile/src/screens/CronScreen/CronScreen.tsx create mode 100644 apps/mobile/src/screens/FilesScreen/FilesScreen.tsx create mode 100644 apps/mobile/src/screens/SettingsScreen/SettingsScreen.tsx create mode 100644 apps/mobile/src/styles/globals.css create mode 100644 apps/mobile/tsconfig.json create mode 100644 apps/mobile/vite.config.ts create mode 100644 eslint.config.js create mode 100644 package.json create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/api.ts create mode 100644 packages/shared/src/events.ts create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/src/tasks.ts create mode 100644 packages/shared/tsconfig.json create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.base.json diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 21f541b..6dab949 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -7,7 +7,7 @@ hermes-mobile/ LICENSE .gitignore .editorconfig - package.json # pnpm workspace root, later + package.json # pnpm workspace root pnpm-workspace.yaml tsconfig.base.json install.sh # one-liner installer entrypoint, later @@ -31,8 +31,7 @@ hermes-mobile/ src/ app/ App.tsx - router.tsx - providers.tsx + navigation.ts screens/ AskScreen/ ActivityScreen/ diff --git a/README.md b/README.md index 8ed849e..02e4953 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Repo: https://git.molberg.cloud/bonzi/hermes-mobile ## Status -Planning scaffold only. No production code yet. +Phase 1 skeleton is underway. The repo now contains a pnpm monorepo with a shared TypeScript package, a Fastify companion server exposing `/api/health`, and a Vite React mobile shell with bottom navigation. Hermes prompting, pairing/auth, persistence, realtime streams, Capacitor Android, and installer productionization are still upcoming roadmap items. ## Product Goals diff --git a/apps/companion/package.json b/apps/companion/package.json new file mode 100644 index 0000000..c45402d --- /dev/null +++ b/apps/companion/package.json @@ -0,0 +1 @@ +{"name":"@hermes-mobile/companion","version":"0.1.0","private":true,"type":"module","main":"dist/index.js","scripts":{"dev":"tsx watch src/index.ts","build":"tsc -p tsconfig.json","start":"node dist/index.js","typecheck":"tsc -p tsconfig.json --noEmit","lint":"eslint src"},"dependencies":{"@fastify/cors":"^10.0.1","@hermes-mobile/shared":"workspace:*","fastify":"^5.2.1"},"devDependencies":{"tsx":"^4.19.2"}} diff --git a/apps/companion/src/app.ts b/apps/companion/src/app.ts new file mode 100644 index 0000000..15e6341 --- /dev/null +++ b/apps/companion/src/app.ts @@ -0,0 +1,20 @@ +import cors from '@fastify/cors'; +import Fastify from 'fastify'; +import { registerHealthRoutes } from './routes/health.js'; + +export async function buildApp() { + const app = Fastify({ logger: true }); + + await app.register(cors, { + origin: true, + }); + + app.get('/', async () => ({ + service: 'hermes-mobile-companion', + health: '/api/health', + })); + + await registerHealthRoutes(app); + + return app; +} diff --git a/apps/companion/src/index.ts b/apps/companion/src/index.ts new file mode 100644 index 0000000..899c230 --- /dev/null +++ b/apps/companion/src/index.ts @@ -0,0 +1,13 @@ +import { buildApp } from './app.js'; + +const port = Number.parseInt(process.env.PORT ?? '8787', 10); +const host = process.env.HOST ?? '0.0.0.0'; + +const app = await buildApp(); + +try { + await app.listen({ port, host }); +} catch (error) { + app.log.error(error); + process.exit(1); +} diff --git a/apps/companion/src/routes/health.ts b/apps/companion/src/routes/health.ts new file mode 100644 index 0000000..4660131 --- /dev/null +++ b/apps/companion/src/routes/health.ts @@ -0,0 +1,21 @@ +import type { FastifyInstance } from 'fastify'; +import { healthResponseSchema } from '@hermes-mobile/shared'; +import { getHermesHealth } from '../system/hermesHealth.js'; + +export async function registerHealthRoutes(app: FastifyInstance): Promise { + app.get('/api/health', async () => { + const response = { + ok: true, + service: 'hermes-mobile-companion' as const, + version: process.env.npm_package_version ?? '0.1.0', + mode: process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test' ? process.env.NODE_ENV : 'development', + uptimeSeconds: process.uptime(), + timestamp: new Date().toISOString(), + checks: { + hermes: await getHermesHealth(), + }, + }; + + return healthResponseSchema.parse(response); + }); +} diff --git a/apps/companion/src/system/hermesHealth.ts b/apps/companion/src/system/hermesHealth.ts new file mode 100644 index 0000000..c9c0089 --- /dev/null +++ b/apps/companion/src/system/hermesHealth.ts @@ -0,0 +1,26 @@ +import { access } from 'node:fs/promises'; +import { delimiter } from 'node:path'; +import { env } from 'node:process'; +import type { HermesHealth } from '@hermes-mobile/shared'; + +async function isExecutable(path: string): Promise { + try { + await access(path); + return true; + } catch { + return false; + } +} + +export async function getHermesHealth(): Promise { + const pathEntries = env.PATH?.split(delimiter).filter(Boolean) ?? []; + + for (const pathEntry of pathEntries) { + const cliPath = `${pathEntry}/hermes`; + if (await isExecutable(cliPath)) { + return { cliAvailable: true, cliPath }; + } + } + + return { cliAvailable: false }; +} diff --git a/apps/companion/tsconfig.json b/apps/companion/tsconfig.json new file mode 100644 index 0000000..db6ebe9 --- /dev/null +++ b/apps/companion/tsconfig.json @@ -0,0 +1 @@ +{"extends":"../../tsconfig.base.json","compilerOptions":{"outDir":"dist","rootDir":"src","noEmit":false},"references":[{"path":"../../packages/shared"}],"include":["src"]} diff --git a/apps/mobile/index.html b/apps/mobile/index.html new file mode 100644 index 0000000..8e5f5ad --- /dev/null +++ b/apps/mobile/index.html @@ -0,0 +1,14 @@ + + + + + + + + Hermes Mobile + + +
+ + + diff --git a/apps/mobile/package.json b/apps/mobile/package.json new file mode 100644 index 0000000..6e74721 --- /dev/null +++ b/apps/mobile/package.json @@ -0,0 +1 @@ +{"name":"@hermes-mobile/mobile","version":"0.1.0","private":true,"type":"module","scripts":{"dev":"vite --host 0.0.0.0","build":"tsc -p tsconfig.json && vite build","preview":"vite preview --host 0.0.0.0","typecheck":"tsc -p tsconfig.json --noEmit","lint":"eslint src"},"dependencies":{"@hermes-mobile/shared":"workspace:*","lucide-react":"^0.468.0","react":"^19.0.0","react-dom":"^19.0.0"},"devDependencies":{"@types/react":"^19.0.2","@types/react-dom":"^19.0.2","@vitejs/plugin-react":"^4.3.4","vite":"^6.0.7"}} diff --git a/apps/mobile/public/manifest.webmanifest b/apps/mobile/public/manifest.webmanifest new file mode 100644 index 0000000..9690793 --- /dev/null +++ b/apps/mobile/public/manifest.webmanifest @@ -0,0 +1 @@ +{"name":"Hermes Mobile","short_name":"Hermes","description":"Mobile control surface for Hermes Agent","start_url":"/","display":"standalone","background_color":"#11131f","theme_color":"#11131f","icons":[]} diff --git a/apps/mobile/src/app/App.tsx b/apps/mobile/src/app/App.tsx new file mode 100644 index 0000000..8122aa6 --- /dev/null +++ b/apps/mobile/src/app/App.tsx @@ -0,0 +1,26 @@ +import { type ReactNode, useState } from 'react'; +import { AppShell } from '../components/app-shell/AppShell.js'; +import { ActivityScreen } from '../screens/ActivityScreen/ActivityScreen.js'; +import { AskScreen } from '../screens/AskScreen/AskScreen.js'; +import { CronScreen } from '../screens/CronScreen/CronScreen.js'; +import { FilesScreen } from '../screens/FilesScreen/FilesScreen.js'; +import { SettingsScreen } from '../screens/SettingsScreen/SettingsScreen.js'; +import type { ScreenId } from './navigation.js'; + +const screens: Record = { + ask: , + activity: , + cron: , + files: , + settings: , +}; + +export function App() { + const [activeScreen, setActiveScreen] = useState('ask'); + + return ( + + {screens[activeScreen]} + + ); +} diff --git a/apps/mobile/src/app/navigation.ts b/apps/mobile/src/app/navigation.ts new file mode 100644 index 0000000..19a0fbd --- /dev/null +++ b/apps/mobile/src/app/navigation.ts @@ -0,0 +1,11 @@ +import { Activity, CalendarClock, Files, MessageCircle, Settings } from 'lucide-react'; + +export const navItems = [ + { id: 'ask', label: 'Ask', icon: MessageCircle }, + { id: 'activity', label: 'Activity', icon: Activity }, + { id: 'cron', label: 'Cron', icon: CalendarClock }, + { id: 'files', label: 'Files', icon: Files }, + { id: 'settings', label: 'Settings', icon: Settings }, +] as const; + +export type ScreenId = (typeof navItems)[number]['id']; diff --git a/apps/mobile/src/components/app-shell/AppShell.tsx b/apps/mobile/src/components/app-shell/AppShell.tsx new file mode 100644 index 0000000..f93e27b --- /dev/null +++ b/apps/mobile/src/components/app-shell/AppShell.tsx @@ -0,0 +1,28 @@ +import type { ReactNode } from 'react'; +import type { ScreenId } from '../../app/navigation.js'; +import { BottomNav } from '../bottom-nav/BottomNav.js'; + +type AppShellProps = { + activeScreen: ScreenId; + children: ReactNode; + onNavigate: (screen: ScreenId) => void; +}; + +export function AppShell({ activeScreen, children, onNavigate }: AppShellProps) { + return ( +
+
+
+

Hermes Mobile

+

Agent command center

+
+
+ + Companion pending +
+
+
{children}
+ +
+ ); +} diff --git a/apps/mobile/src/components/bottom-nav/BottomNav.tsx b/apps/mobile/src/components/bottom-nav/BottomNav.tsx new file mode 100644 index 0000000..17134c3 --- /dev/null +++ b/apps/mobile/src/components/bottom-nav/BottomNav.tsx @@ -0,0 +1,31 @@ +import type { ScreenId } from '../../app/navigation.js'; +import { navItems } from '../../app/navigation.js'; + +type BottomNavProps = { + activeScreen: ScreenId; + onNavigate: (screen: ScreenId) => void; +}; + +export function BottomNav({ activeScreen, onNavigate }: BottomNavProps) { + return ( + + ); +} diff --git a/apps/mobile/src/main.tsx b/apps/mobile/src/main.tsx new file mode 100644 index 0000000..57274e8 --- /dev/null +++ b/apps/mobile/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './app/App.js'; +import './styles/globals.css'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/apps/mobile/src/screens/ActivityScreen/ActivityScreen.tsx b/apps/mobile/src/screens/ActivityScreen/ActivityScreen.tsx new file mode 100644 index 0000000..78e7e33 --- /dev/null +++ b/apps/mobile/src/screens/ActivityScreen/ActivityScreen.tsx @@ -0,0 +1,21 @@ +const events = [ + ['task.created', 'Task lifecycle events will appear here.'], + ['tool_call.started', 'Tool cards will expand with raw output.'], + ['assistant.final', 'Final answers stay attached to sessions.'], +] as const; + +export function ActivityScreen() { + return ( +
+

Activity timeline

+
+ {events.map(([type, description]) => ( +
+ {type} +

{description}

+
+ ))} +
+
+ ); +} diff --git a/apps/mobile/src/screens/AskScreen/AskScreen.tsx b/apps/mobile/src/screens/AskScreen/AskScreen.tsx new file mode 100644 index 0000000..3a193e2 --- /dev/null +++ b/apps/mobile/src/screens/AskScreen/AskScreen.tsx @@ -0,0 +1,28 @@ +import { Mic, Paperclip, SendHorizontal } from 'lucide-react'; + +export function AskScreen() { + return ( +
+
+

Quick ask

+

Send Hermes a task from your phone.

+

Text prompting lands first; voice, uploads, streaming activity, and approvals build on this shell.

+
+ +
event.preventDefault()}> +