Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,22 @@ Add this to your `services.yaml`:

To get the icon to work, you have to add the icon to `/app/public/icons`. See detailed [instructions](https://gethomepage.dev/configs/services/#icons).


### API Documentation

For developers who are not satisfied in just looking a number on the screen but are craving to add new features, a comprehensive development API documentation is available in [`devdocs/API.md`](devdocs/API.md).

**Available Endpoints:**
- `GET /api/stats` - Current node statistics
- `POST /api/chat` - Send P2P chat messages
- `GET /api/github/latest-release` - Latest release information
- `GET /events` - Server-Sent Events stream for real-time updates
- `GET /js/lists.js` - Dynamic adjectives/nouns for screenname generation
- `GET /js/screenname.js` - Screenname generation logic
- `GET /` - Main HTML page

The documentation includes security best practices, rate limiting guidelines, modular route structure, and examples for creating new endpoints.

</details>

<details>
Expand Down
209 changes: 209 additions & 0 deletions devdocs/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# API Documentation

HTTP endpoints for Hypermind integration and development.

## Endpoints

You can test all the available endpoints running: `nude test-api.js`.

<details>
<summary><code>GET /api/stats</code></summary>

Returns node statistics and swarm information.

```json
{
"count": 42,
"totalUnique": 1337,
"direct": 8,
"id": "abc123...",
"screenname": "BraveElephant",
"diagnostics": {...},
"chatEnabled": true,
"mapEnabled": true,
"peers": [...]
}
```

</details>

<details>
<summary><code>POST /api/chat</code></summary>

Send P2P chat message. Rate limited: 5 messages per 5 seconds.

```json
{
"content": "Hello, world!",
"scope": "GLOBAL",
"target": null
}
```

</details>

<details>
<summary><code>GET /api/github/latest-release</code></summary>

Latest GitHub release information.

```json
{
"tag_name": "v1.2.3",
"html_url": "https://github.com/lklynet/hypermind/releases/tag/v1.2.3",
"published_at": "2026-01-08T12:00:00Z",
"body": "Release notes..."
}
```

</details>

<details>
<summary><code>GET /events</code></summary>

Server-Sent Events stream for real-time updates. Returns same data as `/api/stats`.

</details>

<details>
<summary><code>GET /js/lists.js</code></summary>

Dynamic JavaScript file containing adjectives and nouns for screenname generation.

</details>

<details>
<summary><code>GET /js/screenname.js</code></summary>

Dynamic JavaScript file with screenname generation logic for browser.

</details>

<details>
<summary><code>GET /</code></summary>

Main HTML page with server-side template rendering.

</details>

## Development

### Route Structure

Routes are modular in `src/web/routes/`:

```
src/web/routes/
├── your-new-route.js
```

### Creating Routes

<details>
<summary>1. Create module</summary>

`src/web/routes/your-new-route.js`:

```javascript
const setupYourNewRouteRoutes = (router, dependencies) => {
const { identity, peerManager } = dependencies;

router.get("/api/your-new-route", (req, res) => {
res.json({ success: true });
});
};

module.exports = { setupYourNewRouteRoutes };
```

</details>

<details>
<summary>2. Register route</summary>

In `src/web/routes.js`:

```javascript
const { setupYourNewRouteRoutes } = require("./routes/your-new-route");

const yourNewRouteDeps = { identity, peerManager };
setupYourNewRouteRoutes(app, yourNewRouteDeps);
```

</details>

<details>
<summary>3. Add constants</summary>

In `src/config/constants.js`:

```javascript
const YOUR_CONFIG = {
enabled: process.env.ENABLE_YOUR_FEATURE === "true",
rateLimit: parseInt(process.env.YOUR_RATE_LIMIT) || 1000,
};

module.exports = { YOUR_CONFIG };
```

</details>

### Security

<details>
<summary>Rate limiting</summary>

```javascript
let requestHistory = [];

router.get("/api/your-new-route", (req, res) => {
const now = Date.now();
requestHistory = requestHistory.filter((time) => now - time < 10000);

if (requestHistory.length >= 5) {
return res.status(429).json({ error: "Rate limit exceeded" });
}

requestHistory.push(now);
});
```

</details>

<details>
<summary>Input validation</summary>

```javascript
router.post("/api/your-new-route", (req, res) => {
const { data } = req.body;

if (!data || typeof data !== "string" || data.length > 1000) {
return res.status(400).json({ error: "Invalid data" });
}
});
```

</details>

<details>
<summary>Error handling</summary>

```javascript
router.get("/api/your-new-route", async (req, res) => {
try {
const result = await someOperation();
res.json({ success: true, data: result });
} catch (error) {
console.error("Error:", error);
res.status(500).json({ error: "Operation failed" });
}
});
```

</details>

### Testing

```bash
curl http://localhost:3000/api/your-new-route
```
25 changes: 25 additions & 0 deletions devdocs/CHAT-COMMANDS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Chat Commands

Chat commands are modular and located in `public/js/chat-commands/`.

## Adding a Command

1. Create `chat-commands/mycommand.js`:
```javascript
export const myCommand = {
description: "What it does",
execute: () => {
const output = document.getElementById("terminal-output");
// implementation
},
};
```

2. Import and register in `commands.js`:
```javascript
import { myCommand } from "./chat-commands/mycommand.js";

const actions = {
// ... existing commands
"/mycommand": myCommand,
};
26 changes: 20 additions & 6 deletions public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ const appendMessage = (msg) => {
senderName = msg.sender.slice(-8);
}

// Update name map
// Update name map (before changing senderName to "You")
nameToId.set(senderName, msg.sender);

if (msg.sender === myId) senderName = "You";
Expand All @@ -491,7 +491,7 @@ const appendMessage = (msg) => {

const contentSpan = document.createElement("span");
contentSpan.className = "msg-content";
// Use formatMessage for rich text rendering

const rawContent = ` > ${msg.content}`;
if (window.ChatCommands) {
contentSpan.innerHTML = window.ChatCommands.formatMessage(rawContent);
Expand Down Expand Up @@ -523,13 +523,24 @@ terminalInput.addEventListener("keypress", async (e) => {
if (window.ChatCommands) {
const result = window.ChatCommands.processInput(content);
if (result.type === "action") {
window.ChatCommands.actions[result.command].execute();
const action = window.ChatCommands.actions[result.command];
if (action && typeof action.execute === "function") {
try {
action.execute();
} catch (err) {
console.error("Command execution error:", err);
systemStatusBar.innerText = `[SYSTEM] Command failed: ${result.command}`;
}
} else {
systemStatusBar.innerText = `[SYSTEM] Unknown command: ${result.command}`;
}
return;
} else if (result.type === "text") {
content = result.content;
}
}


let scope = "GLOBAL";
let target = null;

Expand Down Expand Up @@ -594,10 +605,11 @@ terminalInput.addEventListener("keypress", async (e) => {
target = nameToId.get(potentialName);
content = msg;
scope = "GLOBAL";
} else if (!msg) {
systemStatusBar.innerText = `[SYSTEM] Usage: /${potentialName} <message>`;
return;
} else {
// If it looks like a command but we don't recognize the user or command
// Prevent sending it as raw text to chat
systemStatusBar.innerText = `[SYSTEM] Unknown command or user: ${potentialName}`;
systemStatusBar.innerText = `[SYSTEM] Unknown user: ${potentialName}`;
return;
}
}
Expand Down Expand Up @@ -805,4 +817,6 @@ function cycleTheme() {
}
}

window.cycleTheme = cycleTheme;

document.getElementById("theme-switcher").addEventListener("click", cycleTheme);
Loading