@7ots/cli 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -1
- package/cli/7ots.mjs +144 -36
- package/cli/lib/assistant.mjs +347 -0
- package/cli/lib/brain.mjs +10 -7
- package/cli/lib/config.mjs +8 -1
- package/cli/lib/i18n.mjs +84 -11
- package/cli/lib/install.mjs +29 -1
- package/cli/lib/pet-core.mjs +3 -2
- package/cli/lib/pet-server.mjs +133 -13
- package/cli/lib/prompts.mjs +199 -7
- package/cli/lib/terminal.mjs +27 -6
- package/cli/pet/electron/main.cjs +96 -32
- package/cli/pet/pet.html +59 -6
- package/cli/pet/settings.html +430 -0
- package/dist/7ots.esm.js +370 -142
- package/dist/7ots.esm.js.map +4 -4
- package/dist/7ots.iife.js +370 -142
- package/dist/7ots.iife.js.map +4 -4
- package/llms.txt +198 -6
- package/package.json +1 -1
- package/server/channels/apuchat.mjs +8 -5
- package/server/channels/apumail.mjs +13 -3
- package/server/channels/index.mjs +5 -3
- package/server/platform/apuchat.mjs +266 -0
- package/server/platform/auth.mjs +39 -10
- package/server/platform/connect.mjs +347 -0
- package/server/platform/db.mjs +55 -0
- package/server/platform/look.mjs +20 -0
- package/server/platform/orquesta.mjs +499 -0
- package/server/platform/routes.mjs +57 -8
- package/server/platform/runtime.mjs +75 -15
- package/server/platform/store.mjs +130 -9
- package/server/server.mjs +14 -3
- package/src/avatar/AvatarStage.js +54 -9
- package/src/avatar/three.js +62 -0
- package/src/character/AvatarEditor.js +174 -50
- package/src/character/Character.js +164 -26
- package/src/character/motion.js +95 -34
- package/src/character/parts.js +43 -12
- package/src/character/skin.js +412 -0
- package/src/character/styles.js +1429 -0
- package/src/character3d/Character3D.js +1693 -0
- package/src/character3d/body.js +154 -0
- package/src/character3d/fx.js +323 -0
- package/src/character3d/index.js +281 -0
- package/src/character3d/materials.js +429 -0
- package/src/character3d/stage.js +187 -0
- package/src/i18n/messages/character.js +18 -42
- package/src/i18n/messages/platform.js +135 -0
- package/src/identity/schema.js +16 -8
- package/src/index.js +1 -0
package/cli/lib/prompts.mjs
CHANGED
|
@@ -55,18 +55,210 @@ export function promptText(topic = 'all') {
|
|
|
55
55
|
return PROMPTS[topic];
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/** https://7ots.com/llms.txt */
|
|
58
|
+
/** https://7ots.com/llms.txt — everything a coding agent needs to set up 7ots without guessing. */
|
|
59
59
|
export function llmsTxt(base = 'https://7ots.com') {
|
|
60
60
|
return `# 7ots
|
|
61
61
|
|
|
62
62
|
> Open-source embeddable web agent: it sees the page, speaks and acts for your visitor. One HTML tag, your server, your brand.
|
|
63
|
-
> Every ot has an identity (face, personality, voice, meet avatar) reproducible from a short seed
|
|
64
|
-
>
|
|
63
|
+
> Every ot has an identity (face, personality, voice, meet avatar) reproducible from a short seed. The same ot can live
|
|
64
|
+
> on a website (widget), on the user's computer (a virtual pet: desktop window, terminal or tmux), in apuchat meet
|
|
65
|
+
> video calls, and answer email (apumail) and chat (apuchat). 7ots integrates those services; it does not host audio or mail.
|
|
65
66
|
|
|
66
|
-
- Site: ${base}/
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
67
|
+
- Site: ${base}/ · create an identity in the browser: ${base}/#create
|
|
68
|
+
- Hosted ots (dashboard, no server needed): ${base}/app/
|
|
69
|
+
- CLI: npm \`@7ots/cli\` (https://www.npmjs.com/package/@7ots/cli) → \`npx -y @7ots/cli help\`, \`npx -y @7ots/cli <cmd> --help\`
|
|
70
|
+
- This file: ${base}/llms.txt (also \`npx -y @7ots/cli docs\`)
|
|
71
|
+
|
|
72
|
+
# Read this first (for coding agents)
|
|
73
|
+
|
|
74
|
+
1. Never invent identity fields or config files by hand: use the CLI, it validates everything.
|
|
75
|
+
2. Ask the user only what you can't tell from the project. The usual question is WHERE the ot should live:
|
|
76
|
+
- a website in this repo → widget (section "Website");
|
|
77
|
+
- not a website (library, backend, CLI, data…) → the pet on their computer, a meet call, or hosting on ${base}/app/;
|
|
78
|
+
- "on my computer" / "a tamagotchi" → the pet (section "Pet").
|
|
79
|
+
3. From an agent, commands must return: use \`--detach\` for the pet, run \`meet\` in the background, and ask the user to run
|
|
80
|
+
interactive commands themselves (\`setup\`, the terminal pet) — in Claude Code: \`! npx -y @7ots/cli setup\`.
|
|
81
|
+
4. Language: \`--lang en|es|pt\` on \`new\` sets the ot's language, and from then on every CLI text follows it
|
|
82
|
+
(order: SEVENOTS_LANG > ~/.7ots/config.json "lang" > the ot's language > LANG > English).
|
|
83
|
+
5. Secrets (API keys, tokens) never go in HTML, in identity.json or in your reply: they go in env vars / .env of a server,
|
|
84
|
+
in ~/.7ots/keys.json (written by \`setup\`, 0600) or in the ${base}/app/ dashboard.
|
|
85
|
+
6. When done, tell the user: the ot's name and seed, where it lives, and how to see it (URL, \`7ots pet\`, …).
|
|
86
|
+
|
|
87
|
+
# Typical flows
|
|
88
|
+
|
|
89
|
+
- "Use 7ots" in a website repo:
|
|
90
|
+
\`npx -y @7ots/cli new --lang <en|es|pt>\` → \`npx -y @7ots/cli install\` → tell the user which AI endpoint the widget uses.
|
|
91
|
+
- "Give me a pet":
|
|
92
|
+
\`npx -y @7ots/cli new\` (if there is no identity) → \`npx -y @7ots/cli pet --mode desktop --detach\` → \`npx -y @7ots/cli hooks install --claude\`.
|
|
93
|
+
- "Put it in a call": \`npx -y @7ots/cli meet --new\` (in the background) and give the printed link only to the user.
|
|
94
|
+
- "Host it, no server": create it at ${base}/app/ (or open the link \`install\` prints) and paste its one-line embed.
|
|
95
|
+
|
|
96
|
+
# CLI reference (\`npx -y @7ots/cli <command>\`, or \`7ots <command>\` once installed globally)
|
|
97
|
+
|
|
98
|
+
Every command accepts \`--help\` (prints usage, never launches anything) and \`--lang en|es|pt\`.
|
|
99
|
+
|
|
100
|
+
| Command | What it does |
|
|
101
|
+
|---|---|
|
|
102
|
+
| \`new [--seed s] [--name n] [--lang en\\|es\\|pt] [--global] [--force] [--json]\` | Random identity → \`.7ots/identity.json\` (\`--global\`: \`~/.7ots/identity.json\`, shared by every project). Keeps an existing one unless \`--force\`. |
|
|
103
|
+
| \`show [--json\\|--prompt]\` | The identity in use (the project's, else the global one). \`--prompt\`: as a system prompt. |
|
|
104
|
+
| \`seed <text>\` | Normalizes a seed. Same seed = same ot everywhere. |
|
|
105
|
+
| \`install [--target html\\|self\\|print] [file]\` | Puts the widget in the project (see "Website"). |
|
|
106
|
+
| \`setup\` (alias \`wizard\`, \`init\`) | Interactive wizard: where the ot lives, brain, voice, pet mode, chattiness, hooks. Writes \`~/.7ots/config.json\` and \`keys.json\`. |
|
|
107
|
+
| \`pet [--mode desktop\\|terminal\\|browser\\|auto] [--detach] [--opaque] [--tmux] [--compact]\` | Wakes the pet (see "Pet"). |
|
|
108
|
+
| \`feed\` · \`play\` · \`sleep\` · \`wake\` · \`say <text>\` | Care for the pet (works even if it is not running: the state is saved). |
|
|
109
|
+
| \`status [--line\\|--json]\` | Level, food, energy, fun. \`--line\`: one short line for status bars. |
|
|
110
|
+
| \`stop\` | Stops the pet daemon (and its windows). |
|
|
111
|
+
| \`hooks install\\|remove [--claude] [--shell] [--global]\` | Lets the pet react to your work (see "Hooks"). |
|
|
112
|
+
| \`meet --new [--minutes n]\` · \`meet "<invite or link>"\` | The ot joins an apuchat meet video call. |
|
|
113
|
+
| \`prompt [identity\\|install\\|pet\\|meet\\|all]\` | Short recipes for coding agents. |
|
|
114
|
+
| \`docs\` | Prints this file. |
|
|
115
|
+
| \`login orquesta\` | Connects a getorquesta.com account (brain "batuta", home "orquesta"). |
|
|
116
|
+
| \`server\` | Runs the 7ots proxy (\`/api/agent\`) with the env vars below. |
|
|
117
|
+
| \`version\` · \`help\` | |
|
|
118
|
+
|
|
119
|
+
# Files
|
|
120
|
+
|
|
121
|
+
| Path | Content |
|
|
122
|
+
|---|---|
|
|
123
|
+
| \`.7ots/identity.json\` (project) / \`~/.7ots/identity.json\` (global) | The identity. Public data only (name, role, look, voice, personality, contact). Commit it if you want. |
|
|
124
|
+
| \`~/.7ots/config.json\` | Written by \`setup\`: \`lang\`, \`home\`, \`brain\`, \`voice\`, \`pet\` (below). |
|
|
125
|
+
| \`~/.7ots/keys.json\` (0600) | Keys typed in \`setup\`, under their env-var names. Env vars always win. Never print it. |
|
|
126
|
+
| \`~/.7ots/pet.json\` · \`pet.log\` · \`pet.token\` | Pet state, daemon log, local auth token. |
|
|
127
|
+
| \`SEVENOTS_HOME\` | Moves \`~/.7ots\` elsewhere. \`SEVENOTS_PET_PORT\` (default 7717) moves the pet daemon. \`SEVENOTS_LANG\` forces the CLI language. |
|
|
128
|
+
|
|
129
|
+
Identity fields: \`id, name, role, tagline, seed, bio, language, languages, personality{tone, traits, instructions}, look{color, …},
|
|
130
|
+
voice{provider, id, lang}, contact{…}\`. Change them with \`new --force\`, the web editor (${base}/#create) or the backoffice, not by hand.
|
|
131
|
+
|
|
132
|
+
## config.json
|
|
133
|
+
|
|
134
|
+
\`\`\`
|
|
135
|
+
home { kind: 'local' } only on this computer
|
|
136
|
+
{ kind: '7ots', url: '${base}/api/o/<id>' } hosted on 7ots.com
|
|
137
|
+
{ kind: 'server', url: 'https://site/api/agent' } the user's own 7ots server
|
|
138
|
+
{ kind: 'orquesta', projectId, projectName } a getorquesta.com project
|
|
139
|
+
brain { kind: 'lines' } built-in phrases, no AI (default)
|
|
140
|
+
{ kind: 'cli', cli: 'claude'|'codex'|'gemini'|'ollama'|'custom', model?, command? } the user's own AI CLI
|
|
141
|
+
{ kind: 'api', provider: 'anthropic'|'openai', model?, baseUrl? } a key in keys.json/env
|
|
142
|
+
{ kind: '7ots' } the home's /chat (7ots.com or own server)
|
|
143
|
+
{ kind: 'batuta', model? } Orquesta's Batuta
|
|
144
|
+
voice { kind: 'none'|'browser'|'apuchat'|'elevenlabs'|'grok'|'fish'|'openai', voiceId? }
|
|
145
|
+
pet { mode: 'ask'|'desktop'|'terminal'|'browser', annoy: 0..3, opaque?: true }
|
|
146
|
+
annoy: 0 silent · 1 rarely · 2 normal · 3 chatty (+ terminal bell)
|
|
147
|
+
\`\`\`
|
|
148
|
+
Keys by name: ANTHROPIC_API_KEY, OPENAI_API_KEY, ORQUESTA_TOKEN, APUCHAT_VOICE_TOKEN, ELEVENLABS_API_KEY, XAI_API_KEY, FISH_API_KEY.
|
|
149
|
+
|
|
150
|
+
# Pet
|
|
151
|
+
|
|
152
|
+
- \`desktop\`: a small always-on-top window that walks along the bottom of the screen (Electron via npx, ~100 MB the first time).
|
|
153
|
+
Find it bottom-right; menu on right click or the tray icon (Show / Hide / Feed / Play / Sleep / Quit). Hide keeps it alive.
|
|
154
|
+
- \`terminal\`: drawn in the current terminal, keys f feed · p play · s sleep/wake · q quit. Needs its own terminal: ask the user.
|
|
155
|
+
- \`browser\`: a tab at http://127.0.0.1:7717/pet.
|
|
156
|
+
- \`auto\`: desktop if there is a screen, else terminal.
|
|
157
|
+
- \`--detach\`: return immediately (always from agents). The daemon keeps running until \`7ots stop\`.
|
|
158
|
+
- tmux: \`7ots pet --tmux\` opens the terminal pet in a side pane (\`--compact\` for a narrow one); for the status bar add
|
|
159
|
+
\`set -g status-right "#(7ots status --line)"\` to ~/.tmux.conf.
|
|
160
|
+
- Needs: food, energy and fun decay with time (time away counts at most 8 h); it gains XP and levels from passing
|
|
161
|
+
commands and finished agent turns.
|
|
162
|
+
|
|
163
|
+
Troubleshooting:
|
|
164
|
+
- A grey/black box behind the pet (Linux without a compositor, some GPUs): \`7ots pet --opaque\` (or \`"pet": {"opaque": true}\` in config.json).
|
|
165
|
+
- Can't see it: look at the tray icon, or \`7ots stop && 7ots pet --mode desktop\` (it comes back bottom-right).
|
|
166
|
+
- Electron can't start (no display, headless, WSL): it falls back to the browser tab; or use \`--mode terminal\`.
|
|
167
|
+
- Port busy: \`SEVENOTS_PET_PORT=7720\`.
|
|
168
|
+
|
|
169
|
+
# Hooks
|
|
170
|
+
|
|
171
|
+
- \`hooks install --claude\`: Claude Code hooks in \`.claude/settings.local.json\` of this project (\`--global\`: ~/.claude/settings.json).
|
|
172
|
+
The pet hears tool calls, errors and finished turns (\`7ots event --from claude\`, reads the hook JSON on stdin; never fails).
|
|
173
|
+
- \`hooks install --shell\`: bash/zsh hook in ~/.bashrc / ~/.zshrc: the pet hears commands and exit codes (secrets are redacted).
|
|
174
|
+
- \`hooks remove\` undoes both. Without a running pet the events still change its state silently.
|
|
175
|
+
|
|
176
|
+
# Website (widget)
|
|
177
|
+
|
|
178
|
+
\`npx -y @7ots/cli install\` detects the project:
|
|
179
|
+
- plain HTML (index.html in ., public/, src/, static/, www/, site/) → inserts the widget before </body> between
|
|
180
|
+
\`<!-- 7ots -->\` markers (idempotent: re-running replaces the block);
|
|
181
|
+
- a framework (Next, Nuxt, Astro, SvelteKit, Remix, Gatsby, Vite, Django, Rails, Laravel, Hugo, Jekyll…) → prints the snippet and
|
|
182
|
+
which file to paste it in (root layout/template, before </body>);
|
|
183
|
+
- not a website → says so and suggests the pet, meet or hosting;
|
|
184
|
+
- \`--target print\` only prints the snippet; \`--target self\` prepares the identity for the user's own server.
|
|
185
|
+
|
|
186
|
+
Snippet for an ot hosted on 7ots.com (simplest; allowed domains are set in the dashboard):
|
|
187
|
+
\`\`\`html
|
|
188
|
+
<script src="${base}/api/o/<id>/embed.js" defer></script>
|
|
189
|
+
\`\`\`
|
|
190
|
+
Snippet with your own proxy:
|
|
191
|
+
\`\`\`html
|
|
192
|
+
<script src="https://cdn.jsdelivr.net/npm/@7ots/cli/dist/7ots.iife.js" defer></script>
|
|
193
|
+
<script>
|
|
194
|
+
addEventListener('DOMContentLoaded', () => SevenOts.init({ endpoint: '/api/agent', identity: true }));
|
|
195
|
+
</script>
|
|
196
|
+
\`\`\`
|
|
197
|
+
Declarative: \`<ots-agent endpoint="/api/agent" site-key="pk_x" name="Ana"></ots-agent>\`. ES module: \`import { init } from '@7ots/cli'\`.
|
|
198
|
+
|
|
199
|
+
\`SevenOts.init(options)\`:
|
|
200
|
+
| Option | Meaning |
|
|
201
|
+
|---|---|
|
|
202
|
+
| \`endpoint\` | Proxy URL (\`/api/agent\`, \`${base}/api/o/<id>\`, …). Required. |
|
|
203
|
+
| \`siteKey\` | Public site id the proxy may require (\`SITE_KEYS\`). |
|
|
204
|
+
| \`identity\` | \`true\` (ask the endpoint), a URL, or an inline identity object (public fields only). |
|
|
205
|
+
| \`agent\` | \`{ name, role, siteName, language, instructions, expressive }\`. |
|
|
206
|
+
| \`avatar\` | \`{ url, body: 'F'\\|'M', mood, cameraView }\` (3D, needs an importmap for three) or \`false\`; default is the 2D character. |
|
|
207
|
+
| \`voice\` | \`{ tts: 'proxy'\\|'browser'\\|false, stt: true, lang }\` or \`false\`. |
|
|
208
|
+
| \`auth\` | \`{ token }\`, \`{ getToken, getUser }\` or \`{ credentials: 'include' }\`; \`exposeClaims\`. |
|
|
209
|
+
| \`mcp\` | \`[{ url, name, requiresAuth, filter, confirm }]\` MCP servers (Streamable HTTP). |
|
|
210
|
+
| \`actions\` | Custom tools \`[{ name, description, parameters, handler, confirm }]\`. |
|
|
211
|
+
| \`templates\` | \`{ name: (data, { escape }) => html }\` for show_modal / open_sidebar. |
|
|
212
|
+
| \`navigation\` | \`{ allowedOrigins, router }\` (\`router\` for SPAs). |
|
|
213
|
+
| \`proactive\` | \`{ level: 'quiet'\\|'normal'\\|'bold', greetDelayMs, dwellMs, idleMs, cooldownMs, maxPerSession, … }\` or \`false\`. |
|
|
214
|
+
| \`context\` | \`{ privateSelectors, ignoreSelectors, extra: () => ({...}) }\` — what it may read on the page. |
|
|
215
|
+
| \`contact\` | \`{ apuchat: true, apumail: true \\| { categories } }\` or \`false\` — hand off to a human. |
|
|
216
|
+
| \`builtins\` | \`{ exclude: ['click', …], confirmClicks: 'submit'\\|'all'\\|'none' }\`. |
|
|
217
|
+
| \`pointer\` | \`{ speed, visible }\` or \`false\` — the virtual mouse/keyboard. |
|
|
218
|
+
| \`pageTools\` | \`true\` (default): forms/buttons with \`data-ots-tool\` become tools. |
|
|
219
|
+
| \`mode\` / \`companion\` | \`'panel'\` (default) or \`'companion'\` (the avatar walks the page); \`{ size, idleHomeMs, follow, wanderMs, watchCursor }\`. |
|
|
220
|
+
| \`theme\` | \`{ primary, radius, position: 'right'\\|'left', font }\`. |
|
|
221
|
+
|
|
222
|
+
HTML tools without JS: \`<form data-ots-tool="invite_member" data-ots-description="…" data-ots-confirm="Invite {email}?" data-ots-auth>\`
|
|
223
|
+
(named fields become parameters). Runtime API: \`agent.ask(text)\`, \`say\`, \`notify\`, \`registerAction\`, \`registerMcp\`,
|
|
224
|
+
\`setAuthToken\`, \`setIdentity\`, \`setProactivity\`, \`open/close/reset/destroy\`, \`on(event, fn)\`.
|
|
225
|
+
|
|
226
|
+
# Own server (\`npx -y @7ots/cli server\`, or \`node server/server.mjs\` from the repo)
|
|
227
|
+
|
|
228
|
+
Reads env vars / \`.env\`. Keys live only here. Admin UI at /backoffice/ (localhost only unless ADMIN_PASSWORD).
|
|
229
|
+
- Core: PORT (8787), ALLOWED_ORIGINS (comma list), SITE_KEYS, RATE_LIMIT_PER_MIN (40), TRUST_PROXY, SERVER_INSTRUCTIONS, LOCALE (es|en|pt), LOG_LEVEL.
|
|
230
|
+
- LLM: LLM_PROVIDER (anthropic | openai | mock), ANTHROPIC_API_KEY, OPENAI_API_KEY, LLM_MODEL, LLM_BASE_URL (OpenAI-compatible:
|
|
231
|
+
DeepSeek, Groq, Ollama, OpenRouter…), LLM_EFFORT (low…max), LLM_MAX_TOKENS, LLM_FALLBACKS (on|off).
|
|
232
|
+
- Voice (TTS): TTS_PROVIDER (apuchat | elevenlabs | grok | fish | openai | none = browser voice; empty = first with a key);
|
|
233
|
+
APUCHAT_VOICE_URL, APUCHAT_VOICE_TOKEN, APUCHAT_VOICE_ID, APUCHAT_VOICE_PROVIDER; ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID,
|
|
234
|
+
ELEVENLABS_MODEL; XAI_API_KEY, XAI_TTS_VOICE; FISH_API_KEY, FISH_VOICE_ID, FISH_MODEL; OPENAI_TTS_MODEL, OPENAI_TTS_VOICE.
|
|
235
|
+
- Email tickets (apumail): APUMAIL_API, APUMAIL_INBOX, APUMAIL_INBOX_TOKEN, APUMAIL_TO.
|
|
236
|
+
- Human handoff (apuchat): APUCHAT_HUB, APUCHAT_NOTIFIER_IDENTITY_KEY, APUCHAT_OPERATOR_HANDLE, APUCHAT_TRANSCRIBE.
|
|
237
|
+
- Backoffice: ADMIN_PASSWORD, CONFIG_FILE, SECRETS_FILE, IDENTITY_FILE (default data/identity.json).
|
|
238
|
+
- The agent outside the web: AGENT_NAME, AGENT_ROLE, AGENT_SITE_URL, AGENT_INSTRUCTIONS, AGENT_LANG; own mailbox APUMAIL_AGENT_INBOX,
|
|
239
|
+
APUMAIL_AGENT_TOKEN, APUMAIL_AGENT_WEBHOOK_SECRET, APUMAIL_AGENT_DAILY; own apuchat/meet identity APUCHAT_AGENT_IDENTITY_KEY,
|
|
240
|
+
APUCHAT_AGENT_AVATAR, APUCHAT_AGENT_SCENE, APUCHAT_AGENT_ALLOW, AGENT_MAX_CALLS, AGENT_CALL_MAX_MINUTES, AGENT_CALL_MAX_TURNS.
|
|
241
|
+
- Demo: DEMO, SERVE_STATIC, DEMO_JWT_SECRET.
|
|
242
|
+
- Platform (multi-tenant, what ${base} runs): PLATFORM=true, PLATFORM_URL, PLATFORM_DB, PLATFORM_SECRET, PLATFORM_MAX_OTS,
|
|
243
|
+
PLATFORM_OTS_RATE_PER_MIN, PLATFORM_OTS_DAILY_LIMIT, PLATFORM_NEW_ACCOUNTS_PER_IP_DAY, PLATFORM_MAX_INFLIGHT,
|
|
244
|
+
PLATFORM_LLM_PROVIDER/API_KEY/BASE_URL/MODEL, PLATFORM_FREE_MESSAGES, PLATFORM_APUCHAT_VOICE_TOKEN, PLATFORM_FREE_TTS,
|
|
245
|
+
PLATFORM_MAIL_INBOX/TOKEN, NOTLOGIN_URL/CLIENT_ID/CLIENT_SECRET, PLATFORM_APUCHAT_IDENTITIES_PER_DAY (free apuchat
|
|
246
|
+
identities per account and day, default 5), ORQUESTA_URL (default https://getorquesta.com).
|
|
247
|
+
Endpoints: POST /api/agent/chat, POST /api/agent/tts, GET /api/agent/identity, /api/agent/contact/*, /llms.txt.
|
|
248
|
+
|
|
249
|
+
# Hosted on ${base} (no server)
|
|
250
|
+
|
|
251
|
+
- Sign in at ${base}/app/, create an ot (\`7ots install\` also prints a link that opens this same identity there), set the allowed domains, copy the embed.
|
|
252
|
+
- Each ot's public API: \`${base}/api/o/<id>/\` — embed.js, config, identity, identity.vcf, chat, tts, contact/apumail, contact/apuchat.
|
|
253
|
+
Point the CLI at it with \`setup\` → home "7ots" (brain "7ots" then uses its /chat).
|
|
254
|
+
- Brain: the account's free monthly quota on the platform's AI, or the user's own key (BYOK) set in the ot's settings.
|
|
255
|
+
Same for voice. Keys are encrypted and never shown again. Per-ot daily and per-minute limits apply.
|
|
256
|
+
- Settings per ot in /backoffice/?ots=<id>: instructions, LLM/voice keys, apumail/apuchat channels.
|
|
257
|
+
- apuchat tab of an ot: a free @handle (expires after 24h without activity, e.g. while the ot is paused), made permanent
|
|
258
|
+
by paying once by card on apuchat; connect your apuchat account to keep paid identities under it, or paste an identity key you own.
|
|
259
|
+
- Orquesta tab of an ot: connect Orquesta (OAuth or an oak_ key), pick a project, enable tasks. Then the ot can start Orquesta
|
|
260
|
+
tasks (run_task, task_status) only for its owner in the dashboard ("Ask your ot to do something") and for apuchat DMs /
|
|
261
|
+
apumail emails from senders on the ot's allowlist — never from the public chat (/api/o/<id>/chat). Daily cap per ot (default 20).
|
|
70
262
|
|
|
71
263
|
# Recipes for coding agents
|
|
72
264
|
|
package/cli/lib/terminal.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal mode: the pet drawn with ANSI in the current terminal, fed by the daemon's SSE.
|
|
3
3
|
* Keys: f feed · p play · s sleep/wake · q quit (the daemon keeps running if --detach started it).
|
|
4
|
+
* Only redraws when the frame changes, line by line (no full clears: no flicker, fine inside tmux).
|
|
5
|
+
* compact: a few narrow lines for a small tmux pane.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import { PET_PORT } from './paths.mjs';
|
|
@@ -46,7 +48,7 @@ function bar(v, width = 16) {
|
|
|
46
48
|
return `${color}${'█'.repeat(n)}${DIM}${'░'.repeat(width - n)}${RESET} ${String(v).padStart(3)}`;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
export async function runTerminal({ identity, lang, port = PET_PORT, bell = false }) {
|
|
51
|
+
export async function runTerminal({ identity, lang, port = PET_PORT, bell = false, compact = false }) {
|
|
50
52
|
const out = process.stdout;
|
|
51
53
|
const color = identity.look.color;
|
|
52
54
|
let state = null;
|
|
@@ -55,15 +57,26 @@ export async function runTerminal({ identity, lang, port = PET_PORT, bell = fals
|
|
|
55
57
|
let thinking = false;
|
|
56
58
|
let blink = false;
|
|
57
59
|
let hop = 0;
|
|
60
|
+
let last = '';
|
|
58
61
|
|
|
59
|
-
const
|
|
60
|
-
if (!state) return;
|
|
62
|
+
const frame = () => {
|
|
61
63
|
const [l, m, r] = FACES[state.mood] || FACES.neutral;
|
|
62
64
|
const eyesL = blink && !state.asleep ? '-' : l;
|
|
63
65
|
const eyesR = blink && !state.asleep ? '-' : r;
|
|
64
66
|
const c = fg(color);
|
|
65
67
|
const lines = [];
|
|
66
68
|
const now = Date.now();
|
|
69
|
+
if (compact) {
|
|
70
|
+
const say = bubble && now < bubbleUntil ? bubble : thinking ? '…' : '';
|
|
71
|
+
const w = Math.max(16, (out.columns || 28) - 2);
|
|
72
|
+
lines.push(`${c}(${RESET}${BOLD}${eyesL}${RESET}${m}${BOLD}${eyesR}${RESET}${c})${RESET} ${BOLD}${identity.name}${RESET}${state.asleep ? `${DIM} zz${RESET}` : ''}`);
|
|
73
|
+
lines.push(`${t('pet.food', lang).slice(0, 3)} ${bar(state.food, 6)}`);
|
|
74
|
+
lines.push(`${t('pet.energy', lang).slice(0, 3)} ${bar(state.energy, 6)}`);
|
|
75
|
+
lines.push(`${t('pet.fun', lang).slice(0, 3)} ${bar(state.fun, 6)}`);
|
|
76
|
+
for (const x of wrap(say, w).slice(0, 4)) lines.push(`${DIM}${x}${RESET}`);
|
|
77
|
+
lines.push(`${DIM}f p s q${RESET}`);
|
|
78
|
+
return lines;
|
|
79
|
+
}
|
|
67
80
|
if (bubble && now < bubbleUntil) {
|
|
68
81
|
const w = wrap(bubble, 38);
|
|
69
82
|
const width = Math.max(...w.map((x) => x.length));
|
|
@@ -85,7 +98,15 @@ export async function runTerminal({ identity, lang, port = PET_PORT, bell = fals
|
|
|
85
98
|
lines.push(` ${DIM}${t('pet.level', lang, { level: state.level, xp: state.xp, next: state.next })}${RESET}`);
|
|
86
99
|
lines.push('');
|
|
87
100
|
lines.push(` ${DIM}${t('pet.keys', lang)}${RESET}`);
|
|
88
|
-
|
|
101
|
+
return lines;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const draw = (force = false) => {
|
|
105
|
+
if (!state) return;
|
|
106
|
+
const f = frame().map((x) => x + '\x1b[K').join('\r\n');
|
|
107
|
+
if (f === last && !force) return;
|
|
108
|
+
last = f;
|
|
109
|
+
out.write('\x1b[H' + f + '\x1b[J');
|
|
89
110
|
};
|
|
90
111
|
|
|
91
112
|
out.write('\x1b[?1049h\x1b[?25l');
|
|
@@ -103,11 +124,11 @@ export async function runTerminal({ identity, lang, port = PET_PORT, bell = fals
|
|
|
103
124
|
if (k === 's') postPet('/event', { type: state?.asleep ? 'wake' : 'sleep' }, port);
|
|
104
125
|
});
|
|
105
126
|
}
|
|
106
|
-
out.on('resize', draw);
|
|
127
|
+
out.on('resize', () => draw(true));
|
|
107
128
|
setInterval(() => {
|
|
108
129
|
blink = Math.random() < 0.08;
|
|
109
130
|
if (hop) hop--;
|
|
110
|
-
draw();
|
|
131
|
+
draw(); // no-op unless something visible changed (blink, hop, bubble expiring)
|
|
111
132
|
}, 500);
|
|
112
133
|
|
|
113
134
|
// SSE over fetch (Node has no EventSource without flags).
|
|
@@ -1,76 +1,135 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Desktop mode: the pet page in a transparent, frameless, always-on-top window that strolls
|
|
3
3
|
* along the bottom of the screen. Launched by `7ots pet --mode desktop` with
|
|
4
|
-
*
|
|
5
|
-
* and SEVENOTS_PET_URL / SEVENOTS_PET_TOKEN in the environment.
|
|
4
|
+
* ~/.7ots/electron/…/electron cli/pet/electron/main.cjs (installed once by the CLI)
|
|
5
|
+
* and SEVENOTS_PET_URL / SEVENOTS_PET_TOKEN in the environment.
|
|
6
|
+
* Menu: right click on the pet, or the tray icon (Show / Hide / Talk / care / Settings / Quit).
|
|
7
|
+
* SEVENOTS_PET_OPAQUE=1 paints a solid card instead of transparency (Linux setups without a compositor).
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
const
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const { app, BrowserWindow, Menu, Tray, nativeImage, screen, shell } = require('electron');
|
|
9
12
|
|
|
10
13
|
const URL_ = process.env.SEVENOTS_PET_URL;
|
|
11
14
|
const TOKEN = process.env.SEVENOTS_PET_TOKEN;
|
|
15
|
+
const OPAQUE = process.env.SEVENOTS_PET_OPAQUE === '1';
|
|
16
|
+
const VIEW3D = process.env.SEVENOTS_PET_3D === '1';
|
|
17
|
+
const LANG = String(process.env.SEVENOTS_PET_LANG || 'en').slice(0, 2);
|
|
18
|
+
const ICON = path.join(__dirname, '..', '..', '..', 'brand', 'icon-512.png');
|
|
12
19
|
const W = 240;
|
|
13
20
|
const H = 270;
|
|
21
|
+
const linux = process.platform === 'linux';
|
|
22
|
+
|
|
23
|
+
const L = {
|
|
24
|
+
en: { settings: '⚙️ Settings', talk: '💬 Talk', show: 'Show pet', hide: 'Hide (keeps living)', feed: '🍎 Feed', play: '🎾 Play', sleep: '💤 Sleep', wake: '☀️ Wake up', still: 'Stay still', quit: 'Quit pet' },
|
|
25
|
+
es: { settings: '⚙️ Configuración', talk: '💬 Hablar', show: 'Mostrar mascota', hide: 'Ocultar (sigue viva)', feed: '🍎 Dar comida', play: '🎾 Jugar', sleep: '💤 Dormir', wake: '☀️ Despertar', still: 'Quedarse quieta', quit: 'Cerrar mascota' },
|
|
26
|
+
pt: { settings: '⚙️ Configurações', talk: '💬 Falar', show: 'Mostrar bichinho', hide: 'Esconder (segue vivo)', feed: '🍎 Dar comida', play: '🎾 Brincar', sleep: '💤 Dormir', wake: '☀️ Acordar', still: 'Ficar parado', quit: 'Fechar bichinho' },
|
|
27
|
+
}[LANG] || {};
|
|
28
|
+
const tr = (k) => L[k] || { show: 'Show pet', hide: 'Hide', talk: 'Talk', settings: 'Settings', feed: 'Feed', play: 'Play', sleep: 'Sleep', wake: 'Wake up', still: 'Stay still', quit: 'Quit pet' }[k];
|
|
14
29
|
|
|
15
|
-
app.commandLine.appendSwitch('enable-transparent-visuals'); // Linux compositors
|
|
16
30
|
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required'); // it talks without a click
|
|
31
|
+
if (linux && !OPAQUE) {
|
|
32
|
+
// Transparent windows on Linux: these avoid the black/grey box on most X11 setups.
|
|
33
|
+
app.commandLine.appendSwitch('enable-transparent-visuals');
|
|
34
|
+
if (!VIEW3D) app.disableHardwareAcceleration(); // the 3D ot needs the GPU (WebGL)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const post = (p, body) =>
|
|
38
|
+
fetch(new URL(p, URL_), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-7ots-Token': TOKEN }, body: JSON.stringify(body || {}) }).catch(() => {});
|
|
17
39
|
|
|
18
|
-
|
|
19
|
-
fetch(new URL(path, URL_), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-7ots-Token': TOKEN }, body: JSON.stringify(body || {}) }).catch(() => {});
|
|
40
|
+
app.whenReady().then(() => setTimeout(start, linux ? 400 : 0)); // Linux: the compositor needs a moment
|
|
20
41
|
|
|
21
|
-
|
|
42
|
+
function start() {
|
|
22
43
|
const area = screen.getPrimaryDisplay().workArea;
|
|
23
|
-
|
|
24
|
-
|
|
44
|
+
const home = () => [area.x + area.width - W - 24, area.y + area.height - H];
|
|
45
|
+
let [x, y] = home();
|
|
46
|
+
|
|
47
|
+
// Tray first: if there is one, the window can stay out of the taskbar; if not, it shows there so it can be found.
|
|
48
|
+
let tray = null;
|
|
49
|
+
try {
|
|
50
|
+
const img = nativeImage.createFromPath(ICON);
|
|
51
|
+
if (!img.isEmpty()) {
|
|
52
|
+
tray = new Tray(img.resize({ width: 22, height: 22 }));
|
|
53
|
+
tray.setToolTip('7ots pet');
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
|
|
25
57
|
const win = new BrowserWindow({
|
|
26
58
|
width: W,
|
|
27
59
|
height: H,
|
|
28
60
|
x,
|
|
29
61
|
y,
|
|
30
|
-
transparent:
|
|
62
|
+
transparent: !OPAQUE,
|
|
31
63
|
frame: false,
|
|
32
64
|
resizable: false,
|
|
33
|
-
hasShadow:
|
|
65
|
+
hasShadow: OPAQUE,
|
|
34
66
|
alwaysOnTop: true,
|
|
35
|
-
skipTaskbar:
|
|
36
|
-
|
|
67
|
+
skipTaskbar: !!tray,
|
|
68
|
+
title: '7ots pet',
|
|
69
|
+
icon: ICON,
|
|
70
|
+
backgroundColor: OPAQUE ? '#ffffff' : '#00000000',
|
|
37
71
|
webPreferences: { contextIsolation: true, sandbox: true },
|
|
38
72
|
});
|
|
39
73
|
win.setAlwaysOnTop(true, 'floating');
|
|
40
74
|
win.setVisibleOnAllWorkspaces?.(true);
|
|
41
|
-
win.loadURL(`${URL_}/pet?desktop=1&token=${encodeURIComponent(TOKEN)}`);
|
|
42
|
-
|
|
75
|
+
win.loadURL(`${URL_}/pet?desktop=1${OPAQUE ? '&opaque=1' : ''}${VIEW3D ? '&3d=1' : ''}&token=${encodeURIComponent(TOKEN)}`);
|
|
76
|
+
const settingsUrl = `${URL_}/settings?token=${encodeURIComponent(TOKEN)}`;
|
|
77
|
+
const openSettings = () => shell.openExternal(settingsUrl).catch(() => {});
|
|
78
|
+
// The page's ⚙️ button opens the settings in the real browser; anything else is denied.
|
|
79
|
+
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
80
|
+
if (url.startsWith(`${URL_}/settings`)) openSettings();
|
|
81
|
+
return { action: 'deny' };
|
|
82
|
+
});
|
|
43
83
|
win.webContents.on('will-navigate', (e) => e.preventDefault());
|
|
44
84
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
85
|
+
let still = false;
|
|
86
|
+
const show = () => {
|
|
87
|
+
[x, y] = home();
|
|
88
|
+
win.setPosition(x, y);
|
|
89
|
+
win.showInactive();
|
|
90
|
+
win.moveTop();
|
|
91
|
+
};
|
|
92
|
+
const menu = () =>
|
|
93
|
+
Menu.buildFromTemplate([
|
|
94
|
+
win.isVisible() ? { label: tr('hide'), click: () => win.hide() } : { label: tr('show'), click: show },
|
|
95
|
+
{ type: 'separator' },
|
|
96
|
+
{ label: tr('talk'), click: () => { show(); win.focus(); win.webContents.executeJavaScript('postMessage({ talk: true })').catch(() => {}); } },
|
|
97
|
+
{ label: tr('feed'), click: () => post('/event', { type: 'feed' }) },
|
|
98
|
+
{ label: tr('play'), click: () => post('/event', { type: 'play' }) },
|
|
99
|
+
{ label: tr('sleep'), click: () => post('/event', { type: 'sleep' }) },
|
|
100
|
+
{ label: tr('wake'), click: () => post('/event', { type: 'wake' }) },
|
|
101
|
+
{ type: 'separator' },
|
|
102
|
+
{ label: tr('settings'), click: openSettings },
|
|
103
|
+
{ label: tr('still'), type: 'checkbox', checked: still, click: (i) => (still = i.checked) },
|
|
104
|
+
{ label: tr('quit'), click: () => post('/quit').finally(() => app.quit()) },
|
|
105
|
+
]);
|
|
106
|
+
win.webContents.on('context-menu', () => menu().popup({ window: win }));
|
|
107
|
+
if (tray) {
|
|
108
|
+
const refresh = () => tray.setContextMenu(menu()); // Linux trays only show the context menu
|
|
109
|
+
refresh();
|
|
110
|
+
win.on('show', refresh);
|
|
111
|
+
win.on('hide', refresh);
|
|
112
|
+
tray.on('click', () => (win.isVisible() ? win.hide() : show()));
|
|
113
|
+
}
|
|
56
114
|
|
|
57
115
|
// Walk: a few seconds walking, a few standing, turn at the edges.
|
|
58
116
|
let dir = -1;
|
|
59
117
|
let walking = false;
|
|
60
|
-
let still = false;
|
|
61
118
|
let dragged = 0;
|
|
62
119
|
let lastSet = x;
|
|
63
120
|
win.on('moved', () => {
|
|
64
121
|
if (Math.abs(win.getPosition()[0] - lastSet) > 4) dragged = Date.now(); // the user dragged it
|
|
65
122
|
});
|
|
66
|
-
const
|
|
67
|
-
|
|
123
|
+
const alive = () => !win.isDestroyed();
|
|
124
|
+
const tell = () => alive() && win.webContents.executeJavaScript(`postMessage({ walk: ${walking ? dir : 0} })`).catch(() => {});
|
|
125
|
+
const walkTimer = setInterval(() => {
|
|
68
126
|
walking = !still && Math.random() < 0.45;
|
|
69
127
|
if (Math.random() < 0.3) dir = -dir;
|
|
70
128
|
tell();
|
|
71
129
|
}, 4000);
|
|
72
|
-
setInterval(() => {
|
|
73
|
-
|
|
130
|
+
const stepTimer = setInterval(() => {
|
|
131
|
+
// isDestroyed first: once the window is gone, isVisible() throws ("Object has been destroyed").
|
|
132
|
+
if (!alive() || !walking || still || !win.isVisible() || Date.now() - dragged < 8000) return;
|
|
74
133
|
const [cx, cy] = win.getPosition();
|
|
75
134
|
x = cx + dir * 2;
|
|
76
135
|
if (x < area.x || x > area.x + area.width - W) {
|
|
@@ -81,6 +140,11 @@ app.whenReady().then(() => {
|
|
|
81
140
|
lastSet = x;
|
|
82
141
|
win.setPosition(x, cy);
|
|
83
142
|
}, 40);
|
|
84
|
-
|
|
143
|
+
win.on('closed', () => {
|
|
144
|
+
clearInterval(walkTimer);
|
|
145
|
+
clearInterval(stepTimer);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
85
148
|
|
|
149
|
+
// Hidden is not closed: the pet lives on in the tray until Quit.
|
|
86
150
|
app.on('window-all-closed', () => app.quit());
|
package/cli/pet/pet.html
CHANGED
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
/* actions */
|
|
81
81
|
.actions { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 12px 18px 16px; }
|
|
82
82
|
.act { display: flex; align-items: center; justify-content: center; gap: 7px; height: 38px; border-radius: 12px; border: 1px solid var(--line); background: var(--panel); font-weight: 650; font-size: 13px; cursor: pointer; transition: background .15s, border-color .15s, transform .1s; }
|
|
83
|
+
a.act { color: inherit; text-decoration: none; -webkit-app-region: no-drag; }
|
|
83
84
|
.act svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
84
85
|
.act:hover { border-color: color-mix(in srgb, var(--accent) 50%, var(--line)); background: color-mix(in srgb, var(--accent) 7%, var(--panel)); }
|
|
85
86
|
.act:active { transform: scale(.97); }
|
|
@@ -108,7 +109,11 @@
|
|
|
108
109
|
.foot { display: flex; gap: 6px 12px; flex-wrap: wrap; justify-content: space-between; padding: 10px 18px 14px; color: var(--muted); font-size: 11.5px; border-top: 1px solid var(--line); background: var(--panel-2); }
|
|
109
110
|
.foot b { color: var(--ink-2); font-weight: 600; }
|
|
110
111
|
|
|
112
|
+
.ask { display: flex; width: 100%; margin: 4px 0 2px; }
|
|
113
|
+
.ask input { flex: 1; min-width: 0; height: 36px; padding: 0 12px; border-radius: 12px; border: 1px solid var(--line); background: var(--panel-2); color: var(--ink); font: inherit; font-size: 13px; outline: none; }
|
|
114
|
+
.ask input:focus { border-color: var(--accent); }
|
|
111
115
|
/* ── desktop: transparent frameless window walking along the bottom of the screen ── */
|
|
116
|
+
html:has(body.desktop:not(.opaque)) { background: transparent; } /* html also paints --bg: without this the window is a white box */
|
|
112
117
|
body.desktop { background: transparent; padding: 0; place-items: end center; height: 100vh; overflow: hidden; -webkit-app-region: drag; user-select: none; }
|
|
113
118
|
body.desktop .app { width: 100%; background: transparent; border: 0; box-shadow: none; border-radius: 0; overflow: visible; }
|
|
114
119
|
body.desktop .top, body.desktop .who, body.desktop .level, body.desktop .stats, body.desktop .feed, body.desktop .foot { display: none; }
|
|
@@ -124,9 +129,13 @@
|
|
|
124
129
|
body.desktop .act span { display: none; }
|
|
125
130
|
body.desktop .act.primary { background: var(--accent); }
|
|
126
131
|
body.desktop .badge { display: flex; }
|
|
132
|
+
body.desktop .ask { display: none; position: absolute; left: 6px; right: 6px; top: 6px; margin: 0; z-index: 4; -webkit-app-region: no-drag; }
|
|
133
|
+
body.desktop.talking .ask { display: flex; }
|
|
127
134
|
.badge { display: none; position: absolute; z-index: 2; left: -6px; bottom: 18px; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 99px; align-items: center; justify-content: center;
|
|
128
135
|
background: var(--panel); border: 1px solid var(--line); box-shadow: var(--shadow); font: 750 11px/1 var(--font); color: var(--ink); }
|
|
129
136
|
body.desktop .floor { bottom: 0; }
|
|
137
|
+
/* --opaque: a solid card instead of transparency (for Linux setups that paint a box behind the window) */
|
|
138
|
+
body.desktop.opaque { background: var(--panel); }
|
|
130
139
|
|
|
131
140
|
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }
|
|
132
141
|
</style>
|
|
@@ -147,6 +156,7 @@
|
|
|
147
156
|
<div class="floor" aria-hidden="true"></div>
|
|
148
157
|
<span class="badge" id="badge"></span>
|
|
149
158
|
</div>
|
|
159
|
+
<form class="ask" id="ask" autocomplete="off"><input id="askIn" maxlength="800" enterkeyhint="send"></form>
|
|
150
160
|
<div class="who">
|
|
151
161
|
<h1 id="name"></h1>
|
|
152
162
|
<div class="sub"><span id="role"></span><span class="mood" id="mood"></span></div>
|
|
@@ -162,8 +172,10 @@
|
|
|
162
172
|
|
|
163
173
|
<nav class="actions">
|
|
164
174
|
<button class="act primary" data-act="feed"><svg viewBox="0 0 24 24"><path d="M12 7c-1.5-2.5-6-2.5-7 1.5S7 20 9.5 20c1.2 0 1.6-.6 2.5-.6s1.3.6 2.5.6C17 20 20 12.5 19 8.5S13.5 4.5 12 7z"/><path d="M12 7c0-2 1-3.5 3-4"/></svg><span data-t="feed"></span></button>
|
|
175
|
+
<button class="act" id="talkBtn" type="button"><svg viewBox="0 0 24 24"><path d="M5 6h14v9H9l-4 3z"/></svg><span data-t="talk"></span></button>
|
|
165
176
|
<button class="act" data-act="play"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M5 8.5c3 1.5 11 1.5 14 0M5 15.5c3-1.5 11-1.5 14 0"/></svg><span data-t="play"></span></button>
|
|
166
177
|
<button class="act" data-act="sleep" id="sleepBtn"><svg viewBox="0 0 24 24"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z"/></svg><span data-t="sleep"></span></button>
|
|
178
|
+
<a class="act" id="setBtn" target="_blank" rel="noopener"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5.3 5.3l2.1 2.1M16.6 16.6l2.1 2.1M5.3 18.7l2.1-2.1M16.6 7.4l2.1-2.1"/></svg><span data-t="settings"></span></a>
|
|
167
179
|
</nav>
|
|
168
180
|
|
|
169
181
|
<section class="feed" aria-labelledby="feed-h">
|
|
@@ -177,17 +189,17 @@
|
|
|
177
189
|
<script type="module">
|
|
178
190
|
const BOOT = __BOOT__;
|
|
179
191
|
const T = {
|
|
180
|
-
en: { feed: 'Feed', play: 'Play', sleep: 'Sleep', wake: 'Wake', food: 'Food', energy: 'Energy', fun: 'Fun', level: 'Level {l}', xp: '{x} / {n} xp', live: 'Live', off: 'Reconnecting…',
|
|
192
|
+
en: { settings: 'Settings', talk: 'Talk', askPh: 'Remind me to check email in 10 min…', feed: 'Feed', play: 'Play', sleep: 'Sleep', wake: 'Wake', food: 'Food', energy: 'Energy', fun: 'Fun', level: 'Level {l}', xp: '{x} / {n} xp', live: 'Live', off: 'Reconnecting…',
|
|
181
193
|
activity: 'Activity', empty: 'Nothing yet. Run a command or ask your coding agent something.', tally: '{ok} ok · {fail} failed · turns {turns}',
|
|
182
194
|
brain: 'Brain: {b}', age: 'Born {d}', lines: 'built-in phrases', now: 'now', m: '{n}m', h: '{n}h',
|
|
183
195
|
moods: { happy: 'Happy', neutral: 'Calm', sad: 'Sad', disgust: 'Bored', sleep: 'Asleep', thinking: 'Thinking' },
|
|
184
196
|
kind: { tool: 'Tool', shell: 'Shell', prompt: 'You asked', stop: 'Turn finished', notify: 'Needs you', start: 'Session started' } },
|
|
185
|
-
es: { feed: 'Comida', play: 'Jugar', sleep: 'Dormir', wake: 'Despertar', food: 'Comida', energy: 'Energía', fun: 'Diversión', level: 'Nivel {l}', xp: '{x} / {n} xp', live: 'En vivo', off: 'Reconectando…',
|
|
197
|
+
es: { settings: 'Ajustes', talk: 'Hablar', askPh: 'Recuérdame ver el correo en 10 min…', feed: 'Comida', play: 'Jugar', sleep: 'Dormir', wake: 'Despertar', food: 'Comida', energy: 'Energía', fun: 'Diversión', level: 'Nivel {l}', xp: '{x} / {n} xp', live: 'En vivo', off: 'Reconectando…',
|
|
186
198
|
activity: 'Actividad', empty: 'Nada todavía. Ejecuta un comando o pregúntale algo a tu agente.', tally: '{ok} ok · {fail} fallos · turnos {turns}',
|
|
187
199
|
brain: 'Cerebro: {b}', age: 'Nació {d}', lines: 'frases propias', now: 'ahora', m: '{n} min', h: '{n} h',
|
|
188
200
|
moods: { happy: 'Feliz', neutral: 'Tranquilo', sad: 'Triste', disgust: 'Aburrido', sleep: 'Dormido', thinking: 'Pensando' },
|
|
189
201
|
kind: { tool: 'Herramienta', shell: 'Terminal', prompt: 'Preguntaste', stop: 'Turno terminado', notify: 'Te necesita', start: 'Sesión iniciada' } },
|
|
190
|
-
pt: { feed: 'Comida', play: 'Brincar', sleep: 'Dormir', wake: 'Acordar', food: 'Comida', energy: 'Energia', fun: 'Diversão', level: 'Nível {l}', xp: '{x} / {n} xp', live: 'Ao vivo', off: 'Reconectando…',
|
|
202
|
+
pt: { settings: 'Ajustes', talk: 'Falar', askPh: 'Me lembre de ver o e-mail em 10 min…', feed: 'Comida', play: 'Brincar', sleep: 'Dormir', wake: 'Acordar', food: 'Comida', energy: 'Energia', fun: 'Diversão', level: 'Nível {l}', xp: '{x} / {n} xp', live: 'Ao vivo', off: 'Reconectando…',
|
|
191
203
|
activity: 'Atividade', empty: 'Nada ainda. Rode um comando ou pergunte algo ao seu agente.', tally: '{ok} ok · {fail} falhas · turnos {turns}',
|
|
192
204
|
brain: 'Cérebro: {b}', age: 'Nasceu {d}', lines: 'frases próprias', now: 'agora', m: '{n} min', h: '{n} h',
|
|
193
205
|
moods: { happy: 'Feliz', neutral: 'Tranquilo', sad: 'Triste', disgust: 'Entediado', sleep: 'Dormindo', thinking: 'Pensando' },
|
|
@@ -202,6 +214,7 @@
|
|
|
202
214
|
|
|
203
215
|
document.documentElement.lang = lang;
|
|
204
216
|
document.body.classList.toggle('desktop', desktop);
|
|
217
|
+
document.body.classList.toggle('opaque', desktop && new URLSearchParams(location.search).has('opaque'));
|
|
205
218
|
document.title = `${id.name} · 7ots`;
|
|
206
219
|
$('#name').textContent = id.name;
|
|
207
220
|
$('#role').textContent = id.role || '';
|
|
@@ -217,8 +230,22 @@
|
|
|
217
230
|
$('#brain').innerHTML = '';
|
|
218
231
|
$('#brain').append(t('brain', { b: '' }), Object.assign(document.createElement('b'), { textContent: BOOT.brain === 'lines' ? t('lines') : BOOT.brain }));
|
|
219
232
|
|
|
220
|
-
const { createCharacter } = await import('/dist/7ots.esm.js');
|
|
221
|
-
const
|
|
233
|
+
const { createCharacter, loadCharacter3D } = await import('/dist/7ots.esm.js');
|
|
234
|
+
const spec = id.look?.character || {};
|
|
235
|
+
let ch = null;
|
|
236
|
+
if (new URLSearchParams(location.search).has('3d')) {
|
|
237
|
+
// three comes from the CDN; without WebGL or network the 2D ot takes over
|
|
238
|
+
try {
|
|
239
|
+
const m = await loadCharacter3D();
|
|
240
|
+
ch = m.createCharacter3D($('#face'), spec, { material: 'plush', backdrop: !desktop, orbit: !desktop, stage: { key: 'pet', zIndex: 3, maxDpr: 2 } });
|
|
241
|
+
if (ch.fallback) console.warn('[7ots pet] no WebGL: 2D ot');
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.warn('[7ots pet] 3D ot unavailable, using 2D:', err?.message || err);
|
|
244
|
+
ch = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
ch ||= createCharacter($('#face'), spec, { locale: lang, title: id.name });
|
|
248
|
+
const is3d = !!ch.character;
|
|
222
249
|
addEventListener('pointermove', (e) => ch.lookAtPoint(e.clientX, e.clientY));
|
|
223
250
|
$('#face').addEventListener('click', () => ch.gesture('bounce'));
|
|
224
251
|
|
|
@@ -395,9 +422,35 @@
|
|
|
395
422
|
live.textContent = t('off');
|
|
396
423
|
connect();
|
|
397
424
|
|
|
425
|
+
// ── talk: reminders, notes, open a site (POST /ask) ──
|
|
426
|
+
const ask = $('#ask');
|
|
427
|
+
const askIn = $('#askIn');
|
|
428
|
+
askIn.placeholder = t('askPh');
|
|
429
|
+
$('#setBtn').href = `/settings?token=${encodeURIComponent(BOOT.token)}`;
|
|
430
|
+
$('#setBtn').title = t('settings');
|
|
431
|
+
const talking = (on) => {
|
|
432
|
+
document.body.classList.toggle('talking', on);
|
|
433
|
+
if (on) askIn.focus();
|
|
434
|
+
};
|
|
435
|
+
$('#talkBtn').addEventListener('click', () => talking(!document.body.classList.contains('talking') || !desktop));
|
|
436
|
+
$('#face').addEventListener('dblclick', () => talking(true));
|
|
437
|
+
askIn.addEventListener('keydown', (e) => e.key === 'Escape' && talking(false));
|
|
438
|
+
ask.addEventListener('submit', async (e) => {
|
|
439
|
+
e.preventDefault();
|
|
440
|
+
const text = askIn.value.trim();
|
|
441
|
+
if (!text) return;
|
|
442
|
+
askIn.value = '';
|
|
443
|
+
if (desktop) talking(false);
|
|
444
|
+
const r = await post('/ask', { text }).then((x) => x.json()).catch(() => null);
|
|
445
|
+
if (!r) say('…');
|
|
446
|
+
});
|
|
447
|
+
addEventListener('message', (e) => e.data?.talk && talking(true));
|
|
448
|
+
|
|
398
449
|
// The desktop shell tells us which way we walk.
|
|
399
450
|
addEventListener('message', (e) => {
|
|
400
|
-
if (e.data?.walk
|
|
451
|
+
if (e.data?.walk == null) return;
|
|
452
|
+
if (is3d) ch.character.yaw = e.data.walk ? -e.data.walk * 0.9 : 0; // turns toward where it walks
|
|
453
|
+
else $('#stage').classList.toggle('flip', e.data.walk < 0);
|
|
401
454
|
});
|
|
402
455
|
</script>
|
|
403
456
|
</body>
|