@7ots/cli 0.1.5 → 0.1.7
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/README.md +15 -21
- package/cli/7ots.mjs +55 -5
- package/cli/lib/account.mjs +89 -0
- package/cli/lib/assistant.mjs +126 -16
- package/cli/lib/brain.mjs +41 -11
- package/cli/lib/card-shot.mjs +84 -0
- package/cli/lib/config.mjs +29 -5
- package/cli/lib/i18n.mjs +57 -0
- package/cli/lib/inferences.mjs +76 -0
- package/cli/lib/integrations.mjs +414 -0
- package/cli/lib/orquesta.mjs +53 -0
- package/cli/lib/pet-core.mjs +7 -10
- package/cli/lib/pet-server.mjs +373 -22
- package/cli/lib/prompts.mjs +31 -17
- package/cli/lib/prowl.mjs +110 -0
- package/cli/lib/screen.mjs +125 -0
- package/cli/lib/stt-local.py +14 -0
- package/cli/lib/stt.mjs +68 -0
- package/cli/lib/tts-local.mjs +110 -0
- package/cli/lib/tts-local.py +18 -0
- package/cli/lib/vm.mjs +309 -0
- package/cli/lib/watchers.mjs +151 -0
- package/cli/pet/card.html +294 -0
- package/cli/pet/electron/director.cjs +175 -0
- package/cli/pet/electron/main.cjs +354 -49
- package/cli/pet/pet.html +328 -15
- package/cli/pet/settings.html +995 -58
- package/dist/7ots.esm.js +56 -56
- package/dist/7ots.esm.js.map +3 -3
- package/dist/7ots.iife.js +56 -56
- package/dist/7ots.iife.js.map +3 -3
- package/llms.txt +31 -17
- package/package.json +17 -14
- package/server/llm.mjs +2 -0
- package/server/platform/connect.mjs +3 -1
- package/server/platform/db.mjs +82 -0
- package/server/platform/device.mjs +125 -0
- package/server/platform/orquesta.mjs +5 -0
- package/server/platform/routes.mjs +101 -1
- package/server/platform/runtime.mjs +2 -2
- package/server/platform/sites.mjs +146 -0
- package/server/platform/store.mjs +3 -1
- package/server/platform/vm.mjs +778 -0
- package/server/tts.mjs +17 -4
- package/src/character/Character.js +1 -1
- package/src/character/parts.js +10 -0
- package/src/character3d/Character3D.js +105 -2
- package/src/character3d/index.js +2 -1
- package/src/character3d/stage.js +15 -4
- package/src/i18n/messages/character.js +6 -3
- package/src/i18n/messages/platform.js +60 -3
package/llms.txt
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# 7ots
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
3
|
+
> An ot is the user's personal AI companion with its own face, personality and voice. It lives on their desktop as a pet
|
|
4
|
+
> (`npx @7ots/cli pet`), watches over their coding agent (Claude Code hooks + bash/zsh hooks), sets reminders, watches prices and
|
|
5
|
+
> processes, can see the screen if allowed, joins apuchat video calls and can work as an Orquesta assistant inside a project.
|
|
6
|
+
> Every ot's identity (face, personality, voice, meet avatar) is reproducible from a short seed. 7ots integrates apuchat,
|
|
7
|
+
> apumail and voice providers; it does not host audio or mail.
|
|
8
|
+
> Site, API and MCP owners: register your service at https://7ots.com/app#sites (usage guide + domain verification via
|
|
9
|
+
> DNS TXT, /.well-known or a meta tag) so every ot knows when and how to use it. Discovery is powered by https://prowl.world.
|
|
10
|
+
> 7ots is no longer pitched as a chatbot to embed on websites: do not install the web widget unless the user explicitly asks for it.
|
|
7
11
|
|
|
8
12
|
- Site: https://7ots.com/ · create an identity in the browser: https://7ots.com/#create
|
|
9
13
|
- Hosted ots (dashboard, no server needed): https://7ots.com/app/
|
|
@@ -13,10 +17,9 @@
|
|
|
13
17
|
# Read this first (for coding agents)
|
|
14
18
|
|
|
15
19
|
1. Never invent identity fields or config files by hand: use the CLI, it validates everything.
|
|
16
|
-
2. Ask the user only what you can't tell from the project. The
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- "on my computer" / "a tamagotchi" → the pet (section "Pet").
|
|
20
|
+
2. Ask the user only what you can't tell from the project. The default is the pet on their computer, connected to their
|
|
21
|
+
coding agent (sections "Pet" and "Hooks"). Other homes: a meet call, an Orquesta project (`login orquesta`) or hosting on
|
|
22
|
+
https://7ots.com/app/. Even if this repo is a website, do NOT run `install` (legacy web widget) unless the user asks for it.
|
|
20
23
|
3. From an agent, commands must return: use `--detach` for the pet, run `meet` in the background, and ask the user to run
|
|
21
24
|
interactive commands themselves (`setup`, the terminal pet) — in Claude Code: `! npx -y @7ots/cli setup`.
|
|
22
25
|
4. Language: `--lang en|es|pt` on `new` sets the ot's language, and from then on every CLI text follows it
|
|
@@ -27,12 +30,14 @@
|
|
|
27
30
|
|
|
28
31
|
# Typical flows
|
|
29
32
|
|
|
30
|
-
- "Use 7ots"
|
|
31
|
-
`npx -y @7ots/cli new --lang <en|es|pt>` → `npx -y @7ots/cli install` → tell the user which AI endpoint the widget uses.
|
|
32
|
-
- "Give me a pet":
|
|
33
|
+
- "Use 7ots" / "give me an ot" / "give me a pet":
|
|
33
34
|
`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`.
|
|
34
35
|
- "Put it in a call": `npx -y @7ots/cli meet --new` (in the background) and give the printed link only to the user.
|
|
35
|
-
- "
|
|
36
|
+
- "Make it my Orquesta assistant": `npx -y @7ots/cli new --global` → ask the user to run `! npx -y @7ots/cli login orquesta`
|
|
37
|
+
(or connect it in the ot's Orquesta tab at https://7ots.com/app/), then pick a project.
|
|
38
|
+
- "Host it, no server": create it at https://7ots.com/app/.
|
|
39
|
+
- "Make ots use my site / API / MCP": send the owner to https://7ots.com/app#sites to enroll it with a usage guide and verify
|
|
40
|
+
the domain (DNS TXT, /.well-known file or meta tag). Ots then find it through Prowl.world (section "For site, API and MCP owners").
|
|
36
41
|
|
|
37
42
|
# CLI reference (`npx -y @7ots/cli <command>`, or `7ots <command>` once installed globally)
|
|
38
43
|
|
|
@@ -43,7 +48,7 @@ Every command accepts `--help` (prints usage, never launches anything) and `--la
|
|
|
43
48
|
| `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`. |
|
|
44
49
|
| `show [--json\|--prompt]` | The identity in use (the project's, else the global one). `--prompt`: as a system prompt. |
|
|
45
50
|
| `seed <text>` | Normalizes a seed. Same seed = same ot everywhere. |
|
|
46
|
-
| `install [--target html\|self\|print] [file]` |
|
|
51
|
+
| `install [--target html\|self\|print] [file]` | Legacy: puts the web widget in the project (see "Website (legacy)"). Only when the user asks. |
|
|
47
52
|
| `setup` (alias `wizard`, `init`) | Interactive wizard: where the ot lives, brain, voice, pet mode, chattiness, hooks. Writes `~/.7ots/config.json` and `keys.json`. |
|
|
48
53
|
| `pet [--mode desktop\|terminal\|browser\|auto] [--detach] [--opaque] [--tmux] [--compact]` | Wakes the pet (see "Pet"). |
|
|
49
54
|
| `feed` · `play` · `sleep` · `wake` · `say <text>` | Care for the pet (works even if it is not running: the state is saved). |
|
|
@@ -114,7 +119,16 @@ Troubleshooting:
|
|
|
114
119
|
- `hooks install --shell`: bash/zsh hook in ~/.bashrc / ~/.zshrc: the pet hears commands and exit codes (secrets are redacted).
|
|
115
120
|
- `hooks remove` undoes both. Without a running pet the events still change its state silently.
|
|
116
121
|
|
|
117
|
-
#
|
|
122
|
+
# For site, API and MCP owners
|
|
123
|
+
|
|
124
|
+
Every ot consults a directory of services (powered by Prowl.world, https://prowl.world) to choose what to use and how.
|
|
125
|
+
Owners enroll a website, REST API or MCP server at https://7ots.com/app#sites with a plain-language usage guide (what it does,
|
|
126
|
+
when to use it, how, what to avoid) and verify the domain with a DNS TXT record, a file under /.well-known or a meta tag
|
|
127
|
+
(the dashboard gives the exact token). Enrolled services become context for every ot: "ASO for agents".
|
|
128
|
+
|
|
129
|
+
# Website (legacy / self-hosted web widget)
|
|
130
|
+
|
|
131
|
+
Legacy: 7ots no longer promotes the on-site chatbot. Use this only when the user explicitly asks for a widget on their site.
|
|
118
132
|
|
|
119
133
|
`npx -y @7ots/cli install` detects the project:
|
|
120
134
|
- plain HTML (index.html in ., public/, src/, static/, www/, site/) → inserts the widget before </body> between
|
|
@@ -189,7 +203,7 @@ Endpoints: POST /api/agent/chat, POST /api/agent/tts, GET /api/agent/identity, /
|
|
|
189
203
|
|
|
190
204
|
# Hosted on https://7ots.com (no server)
|
|
191
205
|
|
|
192
|
-
- Sign in at https://7ots.com/app/, create an ot (`7ots install` also prints a link that opens this same identity there), set
|
|
206
|
+
- Sign in at https://7ots.com/app/, create an ot (`7ots install` also prints a link that opens this same identity there), pick its brain and voice; (legacy) set allowed domains and copy the web embed only if the user wants the widget.
|
|
193
207
|
- Each ot's public API: `https://7ots.com/api/o/<id>/` — embed.js, config, identity, identity.vcf, chat, tts, contact/apumail, contact/apuchat.
|
|
194
208
|
Point the CLI at it with `setup` → home "7ots" (brain "7ots" then uses its /chat).
|
|
195
209
|
- 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.
|
|
@@ -212,8 +226,8 @@ When the user says "use 7ots, create a random identity" (or similar):
|
|
|
212
226
|
3. Show the user who their ot is. If they don't like it, run `npx -y @7ots/cli new --force` for another one.
|
|
213
227
|
Never invent identity fields by hand: the CLI validates them.
|
|
214
228
|
|
|
215
|
-
## Install the
|
|
216
|
-
|
|
229
|
+
## Install the web widget in this project (legacy)
|
|
230
|
+
Only when the user explicitly asks for the widget on their website:
|
|
217
231
|
1. Make sure there is an identity (`npx -y @7ots/cli show`; if none, create one as above).
|
|
218
232
|
2. Run `npx -y @7ots/cli install`. It detects the project:
|
|
219
233
|
- plain HTML site → inserts the 7ots widget before </body> (between <!-- 7ots --> markers, idempotent);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7ots/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "7ots.com —
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "7ots.com — your AI companion with a face of its own: a desktop pet that watches over your coding agent (Claude Code and shell hooks), a personal assistant (reminders, alerts, notes, opens sites, sees your screen if allowed) and a reproducible identity (face, personality, voice) for any agent — in apuchat meet calls, Orquesta projects or your own server.",
|
|
5
5
|
"homepage": "https://7ots.com",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -45,22 +45,25 @@
|
|
|
45
45
|
"node": ">=20"
|
|
46
46
|
},
|
|
47
47
|
"keywords": [
|
|
48
|
+
"ai-companion",
|
|
49
|
+
"desktop-pet",
|
|
50
|
+
"tamagotchi",
|
|
51
|
+
"personal-assistant",
|
|
52
|
+
"agent-identity",
|
|
48
53
|
"ai-agent",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
54
|
+
"claude-code",
|
|
55
|
+
"coding-agent",
|
|
56
|
+
"hooks",
|
|
57
|
+
"reminders",
|
|
58
|
+
"avatar",
|
|
59
|
+
"voice",
|
|
60
|
+
"meet",
|
|
61
|
+
"orquesta",
|
|
62
|
+
"prowl",
|
|
53
63
|
"mcp",
|
|
54
64
|
"claude",
|
|
55
65
|
"openai",
|
|
56
|
-
"
|
|
57
|
-
"avatar",
|
|
58
|
-
"i18n",
|
|
59
|
-
"proactive",
|
|
60
|
-
"cli",
|
|
61
|
-
"pet",
|
|
62
|
-
"tamagotchi",
|
|
63
|
-
"claude-code"
|
|
66
|
+
"cli"
|
|
64
67
|
],
|
|
65
68
|
"dependencies": {
|
|
66
69
|
"@anthropic-ai/sdk": "^0.129.0",
|
package/server/llm.mjs
CHANGED
|
@@ -102,6 +102,7 @@ function anthropicProvider(env) {
|
|
|
102
102
|
toolCalls,
|
|
103
103
|
finishReason: res.stop_reason,
|
|
104
104
|
provider: 'anthropic',
|
|
105
|
+
usage: res.usage ? { in: res.usage.input_tokens || 0, out: res.usage.output_tokens || 0, cacheRead: res.usage.cache_read_input_tokens || 0, cacheWrite: res.usage.cache_creation_input_tokens || 0 } : undefined,
|
|
105
106
|
raw: toolCalls.length ? res.content : undefined, // solo hace falta para continuar el loop
|
|
106
107
|
};
|
|
107
108
|
},
|
|
@@ -181,6 +182,7 @@ function openaiProvider(env) {
|
|
|
181
182
|
.map((c) => ({ id: c.id, name: c.function.name, args: safeJson(c.function.arguments) })),
|
|
182
183
|
finishReason: choice.finish_reason,
|
|
183
184
|
provider: 'openai',
|
|
185
|
+
usage: res.usage ? { in: res.usage.prompt_tokens || 0, out: res.usage.completion_tokens || 0, cacheRead: res.usage.prompt_tokens_details?.cached_tokens || 0 } : undefined,
|
|
184
186
|
};
|
|
185
187
|
},
|
|
186
188
|
};
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
exchangeLink, linkUrl, pendingCheckouts, quote, unlink,
|
|
35
35
|
} from './apuchat.mjs';
|
|
36
36
|
import {
|
|
37
|
-
askOt, authorizeUrl, cancelTask, disconnect, exchangeCode, listProjects, orquestaAccountView, orquestaUrl, otsOrquesta, parseAllow, projectUrl, saveKey, taskStatus,
|
|
37
|
+
askOt, authorizeUrl, cancelTask, disconnect, exchangeCode, listProjects, manageUrl, orquestaAccountView, orquestaUrl, otsOrquesta, parseAllow, projectUrl, saveKey, taskStatus,
|
|
38
38
|
} from './orquesta.mjs';
|
|
39
39
|
|
|
40
40
|
const penv = process.env;
|
|
@@ -195,6 +195,8 @@ export function createConnect({ send, readJson, originOf, save }) {
|
|
|
195
195
|
const cfg = otsOrquesta(ots);
|
|
196
196
|
return {
|
|
197
197
|
url: orquestaUrl(),
|
|
198
|
+
// Where the owner changes which Orquesta projects 7ots may reach (per-project consent).
|
|
199
|
+
manageUrl: manageUrl(),
|
|
198
200
|
account: orquestaAccountView(account.id),
|
|
199
201
|
...cfg,
|
|
200
202
|
projectUrl: cfg.project ? projectUrl(cfg.project) : null,
|
package/server/platform/db.mjs
CHANGED
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
* integrations cuentas conectadas de cada cuenta (apuchat, Orquesta), cifradas
|
|
15
15
|
* platform_state valores de la plataforma (p. ej. el client_id de OAuth registrado en Orquesta)
|
|
16
16
|
* ots_tasks tareas que un ots mandó a Orquesta: quién la pidió, por dónde y cómo acabó
|
|
17
|
+
* sites sites/APIs/MCPs enrolled in Prowl.world so ots know how to use them (sites.mjs)
|
|
18
|
+
* devices computers signed in with `7ots login` (the desktop pet): a bearer token, hashed
|
|
19
|
+
* ots_vms the machine where an ot's assistant worker runs (one per ot; vm.mjs, docs/VM-PLAN.md):
|
|
20
|
+
* provider 'byo' (the owner's own server, enrolled by hand) or 'orquesta' (paid by 7ots, Phase 1)
|
|
21
|
+
* vm_jobs work the desktop sends to that VM, and how it ended
|
|
22
|
+
* vm_job_events what the worker reported while doing it (seq per job, for long-polls)
|
|
17
23
|
*/
|
|
18
24
|
|
|
19
25
|
import { lookPrint } from './look.mjs';
|
|
@@ -103,6 +109,30 @@ CREATE TABLE IF NOT EXISTS platform_state (
|
|
|
103
109
|
value TEXT NOT NULL,
|
|
104
110
|
updated_at INTEGER NOT NULL
|
|
105
111
|
);
|
|
112
|
+
CREATE TABLE IF NOT EXISTS devices (
|
|
113
|
+
id TEXT PRIMARY KEY,
|
|
114
|
+
hash TEXT NOT NULL UNIQUE,
|
|
115
|
+
account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
|
116
|
+
name TEXT NOT NULL DEFAULT '',
|
|
117
|
+
created_at INTEGER NOT NULL,
|
|
118
|
+
last_used_at INTEGER NOT NULL
|
|
119
|
+
);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS devices_account ON devices(account_id);
|
|
121
|
+
CREATE TABLE IF NOT EXISTS sites (
|
|
122
|
+
id TEXT PRIMARY KEY,
|
|
123
|
+
account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
|
124
|
+
kind TEXT NOT NULL DEFAULT 'site',
|
|
125
|
+
url TEXT NOT NULL,
|
|
126
|
+
domain TEXT NOT NULL,
|
|
127
|
+
name TEXT NOT NULL DEFAULT '',
|
|
128
|
+
guide TEXT NOT NULL DEFAULT '',
|
|
129
|
+
service_id TEXT NOT NULL,
|
|
130
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
131
|
+
proof TEXT,
|
|
132
|
+
created_at INTEGER NOT NULL,
|
|
133
|
+
verified_at INTEGER
|
|
134
|
+
);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS sites_account ON sites(account_id);
|
|
106
136
|
CREATE TABLE IF NOT EXISTS ots_tasks (
|
|
107
137
|
id TEXT PRIMARY KEY,
|
|
108
138
|
ots_id TEXT NOT NULL REFERENCES ots(id) ON DELETE CASCADE,
|
|
@@ -118,6 +148,51 @@ CREATE TABLE IF NOT EXISTS ots_tasks (
|
|
|
118
148
|
updated_at INTEGER NOT NULL
|
|
119
149
|
);
|
|
120
150
|
CREATE INDEX IF NOT EXISTS ots_tasks_ots ON ots_tasks(ots_id, created_at DESC);
|
|
151
|
+
CREATE TABLE IF NOT EXISTS ots_vms (
|
|
152
|
+
ots_id TEXT PRIMARY KEY REFERENCES ots(id) ON DELETE CASCADE,
|
|
153
|
+
account_id TEXT NOT NULL,
|
|
154
|
+
provider TEXT NOT NULL DEFAULT 'byo',
|
|
155
|
+
orq_project_id TEXT NOT NULL DEFAULT '',
|
|
156
|
+
orq_vm_id TEXT NOT NULL DEFAULT '',
|
|
157
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
158
|
+
size TEXT NOT NULL DEFAULT '',
|
|
159
|
+
region TEXT NOT NULL DEFAULT '',
|
|
160
|
+
ip TEXT NOT NULL DEFAULT '',
|
|
161
|
+
error TEXT NOT NULL DEFAULT '',
|
|
162
|
+
enroll_hash TEXT UNIQUE,
|
|
163
|
+
enroll_exp INTEGER,
|
|
164
|
+
agent_hash TEXT UNIQUE,
|
|
165
|
+
agent_seen_at INTEGER,
|
|
166
|
+
worker_version TEXT NOT NULL DEFAULT '',
|
|
167
|
+
worker_info TEXT NOT NULL DEFAULT '{}',
|
|
168
|
+
created_at INTEGER NOT NULL,
|
|
169
|
+
updated_at INTEGER NOT NULL
|
|
170
|
+
);
|
|
171
|
+
CREATE TABLE IF NOT EXISTS vm_jobs (
|
|
172
|
+
id TEXT PRIMARY KEY,
|
|
173
|
+
ots_id TEXT NOT NULL REFERENCES ots(id) ON DELETE CASCADE,
|
|
174
|
+
account_id TEXT NOT NULL,
|
|
175
|
+
device_id TEXT NOT NULL DEFAULT '',
|
|
176
|
+
kind TEXT NOT NULL,
|
|
177
|
+
input TEXT NOT NULL DEFAULT '',
|
|
178
|
+
context TEXT NOT NULL DEFAULT '',
|
|
179
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
180
|
+
result TEXT NOT NULL DEFAULT '',
|
|
181
|
+
error TEXT NOT NULL DEFAULT '',
|
|
182
|
+
created_at INTEGER NOT NULL,
|
|
183
|
+
claimed_at INTEGER,
|
|
184
|
+
finished_at INTEGER,
|
|
185
|
+
updated_at INTEGER NOT NULL
|
|
186
|
+
);
|
|
187
|
+
CREATE INDEX IF NOT EXISTS vm_jobs_ots ON vm_jobs(ots_id, created_at DESC);
|
|
188
|
+
CREATE TABLE IF NOT EXISTS vm_job_events (
|
|
189
|
+
job_id TEXT NOT NULL REFERENCES vm_jobs(id) ON DELETE CASCADE,
|
|
190
|
+
seq INTEGER NOT NULL,
|
|
191
|
+
kind TEXT NOT NULL,
|
|
192
|
+
text TEXT NOT NULL DEFAULT '',
|
|
193
|
+
at INTEGER NOT NULL,
|
|
194
|
+
PRIMARY KEY (job_id, seq)
|
|
195
|
+
);
|
|
121
196
|
`;
|
|
122
197
|
|
|
123
198
|
let db = null;
|
|
@@ -131,6 +206,13 @@ export function openDb(path = process.env.PLATFORM_DB) {
|
|
|
131
206
|
db.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 3000;');
|
|
132
207
|
db.exec(SCHEMA);
|
|
133
208
|
migrateLookPrints(db);
|
|
209
|
+
// VMs paid by 7ots (provider 'orquesta') are opt-in per account; your own server never needs it (vm.mjs).
|
|
210
|
+
if (!db.prepare('PRAGMA table_info(accounts)').all().some((c) => c.name === 'vm_enabled')) db.exec('ALTER TABLE accounts ADD COLUMN vm_enabled INTEGER NOT NULL DEFAULT 0');
|
|
211
|
+
// VMs created through Orquesta: its last-seen VM status, when it was checked and the monthly price it quoted.
|
|
212
|
+
const vmCols = db.prepare('PRAGMA table_info(ots_vms)').all().map((c) => c.name);
|
|
213
|
+
if (!vmCols.includes('orq_status')) db.exec("ALTER TABLE ots_vms ADD COLUMN orq_status TEXT NOT NULL DEFAULT ''");
|
|
214
|
+
if (!vmCols.includes('orq_checked_at')) db.exec('ALTER TABLE ots_vms ADD COLUMN orq_checked_at INTEGER');
|
|
215
|
+
if (!vmCols.includes('price_cents')) db.exec('ALTER TABLE ots_vms ADD COLUMN price_cents INTEGER');
|
|
134
216
|
return db;
|
|
135
217
|
}
|
|
136
218
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signing a computer in to 7ots.com (`7ots login`, or "Connect" in the pet's settings), so the
|
|
3
|
+
* desktop pet can list the account's ots, switch to another one and bring its settings home.
|
|
4
|
+
*
|
|
5
|
+
* Device flow, like a TV app: the computer asks for a code, opens /app/#device/<code>, the
|
|
6
|
+
* person (signed in to the dashboard) approves it, and the computer picks up a bearer token.
|
|
7
|
+
*
|
|
8
|
+
* POST /api/device/start { name } → { code, url, expiresIn } (no auth, rate-limited)
|
|
9
|
+
* POST /api/device/poll { code } → 202 pending · 200 { token } once · 410 expired/denied
|
|
10
|
+
* GET /api/device/me → { account, ots: [summary] } (Authorization: Bearer)
|
|
11
|
+
* GET /api/device/ots/:id → { ots: summary, identity } (the owner's full identity)
|
|
12
|
+
* DELETE /api/device/token → signs this computer out
|
|
13
|
+
*
|
|
14
|
+
* From the dashboard (session cookie, X-7ots-Admin: 1, same origin; see routes.mjs):
|
|
15
|
+
* GET /api/platform/device/:code → { name, expiresIn } what is asking
|
|
16
|
+
* POST /api/platform/device/:code { approve: bool }
|
|
17
|
+
* GET /api/platform/devices DELETE /api/platform/devices/:id
|
|
18
|
+
*
|
|
19
|
+
* Tokens are only stored hashed. Pending codes live in memory (10 min): a restart just means
|
|
20
|
+
* running `7ots login` again.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { defineIdentity } from '../../src/identity/schema.js';
|
|
24
|
+
import { getDb } from './db.mjs';
|
|
25
|
+
import { newId, sha256, token } from './crypto.mjs';
|
|
26
|
+
import { getOwnOts, listOts, publicAccount } from './store.mjs';
|
|
27
|
+
|
|
28
|
+
const CODE_TTL = 10 * 60_000;
|
|
29
|
+
const PREFIX = '7d_'; // so a leaked token is recognizable
|
|
30
|
+
|
|
31
|
+
const pending = new Map(); // code → { name, at, accountId?, token?, denied? }
|
|
32
|
+
|
|
33
|
+
function sweep() {
|
|
34
|
+
const t = Date.now();
|
|
35
|
+
for (const [c, v] of pending) if (t - v.at > CODE_TTL) pending.delete(c);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function deviceFromBearer(req) {
|
|
39
|
+
const m = /^Bearer\s+(7d_[A-Za-z0-9_-]{20,200})$/.exec(String(req.headers.authorization || ''));
|
|
40
|
+
if (!m) return null;
|
|
41
|
+
const d = getDb();
|
|
42
|
+
const row = d.prepare('SELECT d.id AS device_id, a.* FROM devices d JOIN accounts a ON a.id = d.account_id WHERE d.hash = ?').get(sha256(m[1]));
|
|
43
|
+
if (!row) return null;
|
|
44
|
+
d.prepare('UPDATE devices SET last_used_at = ? WHERE id = ?').run(Date.now(), row.device_id);
|
|
45
|
+
return { deviceId: row.device_id, account: row };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function listDevices(accountId) {
|
|
49
|
+
return getDb().prepare('SELECT id, name, created_at AS createdAt, last_used_at AS lastUsedAt FROM devices WHERE account_id = ? ORDER BY last_used_at DESC').all(accountId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {{ send: Function, readJson: Function, base: (req) => string, rateLimit: (req) => boolean, summary: (ots) => object }} deps
|
|
54
|
+
*/
|
|
55
|
+
export function createDevice({ send, readJson, base, rateLimit, summary }) {
|
|
56
|
+
/** Public half (/api/device/*). @returns {Promise<boolean>} */
|
|
57
|
+
async function api(req, res, path) {
|
|
58
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
59
|
+
if (path === '/start' && req.method === 'POST') {
|
|
60
|
+
if (!rateLimit(req)) return send(res, 429, { error: 'rate' }), true;
|
|
61
|
+
const { name } = await readJson(req, 4096);
|
|
62
|
+
sweep();
|
|
63
|
+
if (pending.size > 2000) return send(res, 503, { error: 'busy' }), true;
|
|
64
|
+
const code = token().replace(/[^A-Za-z0-9]/g, '').slice(0, 24);
|
|
65
|
+
pending.set(code, { name: String(name || '').replace(/[\u0000-\u001f]/g, '').trim().slice(0, 60) || 'computer', at: Date.now() });
|
|
66
|
+
return send(res, 200, { code, url: `${base(req)}/app/#device/${code}`, expiresIn: CODE_TTL / 1000 }), true;
|
|
67
|
+
}
|
|
68
|
+
if (path === '/poll' && req.method === 'POST') {
|
|
69
|
+
const { code } = await readJson(req, 4096);
|
|
70
|
+
sweep();
|
|
71
|
+
const p = pending.get(String(code || ''));
|
|
72
|
+
if (!p || p.denied) return pending.delete(String(code || '')), send(res, 410, { error: p?.denied ? 'denied' : 'expired' }), true;
|
|
73
|
+
if (!p.token) return send(res, 202, { status: 'pending' }), true;
|
|
74
|
+
pending.delete(code); // handed out once
|
|
75
|
+
return send(res, 200, { token: p.token }), true;
|
|
76
|
+
}
|
|
77
|
+
const who = deviceFromBearer(req);
|
|
78
|
+
if (path === '/me' || path === '/token' || path.startsWith('/ots/')) {
|
|
79
|
+
if (!who) return send(res, 401, { error: 'login' }), true;
|
|
80
|
+
const { account } = who;
|
|
81
|
+
if (path === '/me' && req.method === 'GET') return send(res, 200, { account: publicAccount(account), ots: listOts(account.id).map(summary) }), true;
|
|
82
|
+
if (path === '/token' && req.method === 'DELETE') {
|
|
83
|
+
getDb().prepare('DELETE FROM devices WHERE id = ?').run(who.deviceId);
|
|
84
|
+
return send(res, 200, { ok: true }), true;
|
|
85
|
+
}
|
|
86
|
+
const m = /^\/ots\/([a-z0-9_]{4,40})$/.exec(path);
|
|
87
|
+
if (m && req.method === 'GET') {
|
|
88
|
+
const ots = getOwnOts(account.id, m[1]);
|
|
89
|
+
if (!ots) return send(res, 404, { error: 'not found' }), true;
|
|
90
|
+
return send(res, 200, { ots: summary(ots), identity: defineIdentity(ots.identity) }), true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Dashboard half, already behind the session + same-origin checks. @returns {boolean} */
|
|
97
|
+
async function dashboard(req, res, account, path) {
|
|
98
|
+
const m = /^\/device\/([A-Za-z0-9]{8,40})$/.exec(path);
|
|
99
|
+
if (m) {
|
|
100
|
+
sweep();
|
|
101
|
+
const p = pending.get(m[1]);
|
|
102
|
+
if (!p || p.token || p.denied) return send(res, 410, { error: 'expired' }), true;
|
|
103
|
+
if (req.method === 'GET') return send(res, 200, { name: p.name, expiresIn: Math.max(0, Math.round((CODE_TTL - (Date.now() - p.at)) / 1000)) }), true;
|
|
104
|
+
if (req.method === 'POST') {
|
|
105
|
+
const { approve } = await readJson(req, 4096);
|
|
106
|
+
if (approve !== true) return (p.denied = true), send(res, 200, { ok: true }), true;
|
|
107
|
+
const raw = PREFIX + token();
|
|
108
|
+
const t = Date.now();
|
|
109
|
+
getDb().prepare('INSERT INTO devices (id, hash, account_id, name, created_at, last_used_at) VALUES (?, ?, ?, ?, ?, ?)').run(newId('dev'), sha256(raw), account.id, p.name, t, t);
|
|
110
|
+
p.token = raw;
|
|
111
|
+
p.accountId = account.id;
|
|
112
|
+
return send(res, 200, { ok: true }), true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (path === '/devices' && req.method === 'GET') return send(res, 200, { devices: listDevices(account.id) }), true;
|
|
116
|
+
const d = /^\/devices\/([\w-]{4,60})$/.exec(path);
|
|
117
|
+
if (d && req.method === 'DELETE') {
|
|
118
|
+
getDb().prepare('DELETE FROM devices WHERE id = ? AND account_id = ?').run(d[1], account.id);
|
|
119
|
+
return send(res, 200, { ok: true }), true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { api, dashboard };
|
|
125
|
+
}
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* - OAuth 2.1 + PKCE (public client, registered once with dynamic registration; its client_id
|
|
6
6
|
* is cached in platform_state). omt_ access tokens (30 d) renew with the rotating omr_ refresh.
|
|
7
7
|
* - or a pasted oak_ API key.
|
|
8
|
+
* The grant is per project: Orquesta's consent screen asks which projects 7ots may reach
|
|
9
|
+
* (or "all"), and every /api/v1 call is limited to them server-side — GET /api/v1/projects
|
|
10
|
+
* lists exactly the shared ones. The owner changes the set later in Orquesta → Settings →
|
|
11
|
+
* Connections (manageUrl); an oak_ key is scoped in Settings → API keys, same page.
|
|
8
12
|
* ORQUESTA_URL (default https://getorquesta.com) is read once at boot.
|
|
9
13
|
*
|
|
10
14
|
* API used:
|
|
@@ -42,6 +46,7 @@ const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]+\.[^\s@]{2,}$/;
|
|
|
42
46
|
const HANDLE_RE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
43
47
|
|
|
44
48
|
export const orquestaUrl = () => ORQ;
|
|
49
|
+
export const manageUrl = () => `${ORQ}/dashboard/settings#connections`;
|
|
45
50
|
export const projectUrl = (id) => `${ORQ}/dashboard/projects/${encodeURIComponent(id)}`;
|
|
46
51
|
|
|
47
52
|
async function http(url, { method = 'GET', headers = {}, body, form, timeoutMs = 15000 } = {}) {
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
* GET /api/platform/ots/:id/usage
|
|
15
15
|
* * /api/platform/ots/:id/admin/* misma forma que /api/agent/admin/* (el backoffice
|
|
16
16
|
* edita un ots con ?ots=<id>)
|
|
17
|
+
* GET /api/platform/sites POST { kind, url, name, guide } · POST …/:id/verify · DELETE …/:id (sites.mjs)
|
|
18
|
+
* GET /api/platform/device/:code · POST { approve } GET /api/platform/devices · DELETE …/:id
|
|
19
|
+
*
|
|
20
|
+
* GET /api/platform/ots/:id/vm POST …/vm/enroll · DELETE …/vm (vm.mjs)
|
|
21
|
+
*
|
|
22
|
+
* Desktop pet signed in with `7ots login` (Authorization: Bearer 7d_…): /api/device/* — see device.mjs
|
|
23
|
+
* (…/ots/:id/vm[/jobs…]: the offload channel to the ot's worker, vm.mjs)
|
|
24
|
+
* Worker on the ot's own server (Authorization: Bearer 7vm_…): /api/vm/* — see vm.mjs
|
|
17
25
|
* apuchat identity and Orquesta tasks (…/apuchat/*, …/orquesta/*, /auth/apuchat|orquesta): see connect.mjs
|
|
18
26
|
*/
|
|
19
27
|
|
|
@@ -28,6 +36,9 @@ import {
|
|
|
28
36
|
} from './store.mjs';
|
|
29
37
|
import { freeMessages, freeTts, otsChannels, otsEnv, otsLlmInfo, otsStep, otsSynthesize, otsTtsProvider, platformLlmEnv, stopOtsChannels } from './runtime.mjs';
|
|
30
38
|
import { createConnect } from './connect.mjs';
|
|
39
|
+
import { createDevice, deviceFromBearer } from './device.mjs';
|
|
40
|
+
import { createVm } from './vm.mjs';
|
|
41
|
+
import { createSites } from './sites.mjs';
|
|
31
42
|
import '../../src/i18n/messages/platform.js';
|
|
32
43
|
|
|
33
44
|
const penv = process.env;
|
|
@@ -86,7 +97,7 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
86
97
|
async function publicApi(req, res, id, path) {
|
|
87
98
|
const t = reqT(req);
|
|
88
99
|
const ots = getOts(id);
|
|
89
|
-
const open = ['/config', '/embed.js', '/identity', '/identity.vcf'].includes(path);
|
|
100
|
+
const open = ['/config', '/embed.js', '/identity', '/identity.vcf', '/traits.json'].includes(path);
|
|
90
101
|
if (open) {
|
|
91
102
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
92
103
|
if (req.method === 'OPTIONS') return send(res, 204);
|
|
@@ -98,11 +109,18 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
98
109
|
if (ots.status !== 'on') return res.end('/* 7ots: ots en pausa */');
|
|
99
110
|
return res.end(embedScript(ots.widget, await bundleVersion(), `/api/o/${ots.id}`));
|
|
100
111
|
}
|
|
112
|
+
if (path === '/traits.json') return send(res, 200, traitsMeta(ots, `${originOf(req)}/api/o/${ots.id}`));
|
|
101
113
|
const card = publicIdentity(ots.identity, otsChannels(ots).live());
|
|
102
114
|
if (path === '/identity.vcf') {
|
|
103
115
|
res.writeHead(200, { 'Content-Type': 'text/vcard; charset=utf-8', 'Content-Disposition': `inline; filename="${card.id}.vcf"`, 'Cache-Control': 'no-cache' });
|
|
104
116
|
return res.end(toVCard(card, { baseUrl: `${originOf(req)}/api/o/${ots.id}/identity.vcf` }));
|
|
105
117
|
}
|
|
118
|
+
// A person opening the link sees the ot's card; agents and the card itself (Accept: */* or JSON) get the JSON.
|
|
119
|
+
res.setHeader('Vary', 'Accept');
|
|
120
|
+
if (/text\/html/.test(req.headers.accept || '') && !/[?&]format=json\b/.test(req.url)) {
|
|
121
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' });
|
|
122
|
+
return res.end(profilePage(card, `${originOf(req)}/api/o/${ots.id}`));
|
|
123
|
+
}
|
|
106
124
|
return send(res, 200, card);
|
|
107
125
|
}
|
|
108
126
|
|
|
@@ -305,6 +323,8 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
305
323
|
});
|
|
306
324
|
}
|
|
307
325
|
if ((await connect.account(req, res, account, path)) !== false) return;
|
|
326
|
+
if (await device.dashboard(req, res, account, path)) return;
|
|
327
|
+
if (await sites(req, res, account, path)) return;
|
|
308
328
|
|
|
309
329
|
if (path === '/ots' && req.method === 'GET') {
|
|
310
330
|
const usage = usageByOts(account.id);
|
|
@@ -342,6 +362,7 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
342
362
|
|
|
343
363
|
if (sub.startsWith('/admin/')) return adminApi(req, res, ots, sub.slice('/admin'.length));
|
|
344
364
|
if (await connect.ots(req, res, account, ots, sub)) return;
|
|
365
|
+
if (await vm.dashboard(req, res, account, ots, sub)) return;
|
|
345
366
|
|
|
346
367
|
if (sub === '' && req.method === 'GET') return send(res, 200, { ots: otsSummary(ots, usageByOts(account.id)[ots.id] || {}) });
|
|
347
368
|
if (sub === '' && req.method === 'PATCH') {
|
|
@@ -359,6 +380,7 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
359
380
|
if (sub === '' && req.method === 'DELETE') {
|
|
360
381
|
await stopOtsChannels(ots.id);
|
|
361
382
|
await connect.release(account, ots);
|
|
383
|
+
await vm.releaseOts(ots.id); // its 7ots server (Orquesta VM) goes too; ots_vms cascades with the ot
|
|
362
384
|
deleteOts(ots.id);
|
|
363
385
|
return send(res, 200, { ok: true });
|
|
364
386
|
}
|
|
@@ -381,6 +403,9 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
381
403
|
}
|
|
382
404
|
|
|
383
405
|
const connect = createConnect({ send, readJson, originOf, save });
|
|
406
|
+
const device = createDevice({ send, readJson, rateLimit, base: (req) => (penv.PLATFORM_URL || originOf(req)).replace(/\/+$/, ''), summary: (o) => otsSummary(o) });
|
|
407
|
+
const sites = createSites({ send, readJson });
|
|
408
|
+
const vm = createVm({ send, readJson, rateLimit, base: (req) => (penv.PLATFORM_URL || originOf(req)).replace(/\/+$/, '') });
|
|
384
409
|
|
|
385
410
|
/** @returns {Promise<boolean>} true si atendió la ruta */
|
|
386
411
|
return async function handle(req, res, url) {
|
|
@@ -398,6 +423,21 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
398
423
|
send(res, 200, { free: ok ? !lookTaken({ look: { kind: 'character', character } }) : false });
|
|
399
424
|
return true;
|
|
400
425
|
}
|
|
426
|
+
const dv = p.match(/^\/api\/device\/ots\/([a-z0-9_]{4,40})\/vm(\/.*)?$/);
|
|
427
|
+
if (dv) {
|
|
428
|
+
const who = deviceFromBearer(req);
|
|
429
|
+
if (!who) send(res, 401, { error: 'login' });
|
|
430
|
+
else await vm.device(req, res, who, dv[1], dv[2] || '');
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
if (p.startsWith('/api/vm/')) {
|
|
434
|
+
await vm.worker(req, res, p.slice('/api/vm'.length));
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
if (p.startsWith('/api/device/')) {
|
|
438
|
+
if (!(await device.api(req, res, p.slice('/api/device'.length)))) send(res, 404, { error: reqT(req)('server.http.unknownRoute') });
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
401
441
|
if (p.startsWith('/api/platform/')) {
|
|
402
442
|
await dashboardApi(req, res, p.slice('/api/platform'.length));
|
|
403
443
|
return true;
|
|
@@ -435,3 +475,63 @@ function sanitizeWidget(w) {
|
|
|
435
475
|
for (const k of ['endpoint', 'llm', 'auth', 'templates', 'actions', 'identity']) delete out[k];
|
|
436
476
|
return out;
|
|
437
477
|
}
|
|
478
|
+
|
|
479
|
+
const htmlEsc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
480
|
+
/** Public profile of an ot (what /api/o/<id>/identity shows in a browser): its contact card with the live face. */
|
|
481
|
+
function profilePage(card, ep) {
|
|
482
|
+
const name = htmlEsc(card.name);
|
|
483
|
+
const desc = htmlEsc(card.tagline || card.role || '');
|
|
484
|
+
const lang = htmlEsc(card.language || 'en');
|
|
485
|
+
const c1 = /^#[0-9a-f]{3,8}$/i.test(card.look?.color || '') ? card.look.color : '#6d5dfc';
|
|
486
|
+
const c2 = /^#[0-9a-f]{3,8}$/i.test(card.look?.accent || '') ? card.look.accent : '#22d3ee';
|
|
487
|
+
return `<!doctype html><html lang="${lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
488
|
+
<title>${name} · 7ots</title><meta name="description" content="${desc}">
|
|
489
|
+
<meta property="og:title" content="${name} · 7ots"><meta property="og:description" content="${desc}"><meta property="og:type" content="profile">
|
|
490
|
+
<link rel="alternate" type="application/json" href="${htmlEsc(ep)}/identity?format=json"><link rel="icon" href="/favicon.ico">
|
|
491
|
+
<style>
|
|
492
|
+
:root{color-scheme:dark;--c1:${c1};--c2:${c2}}
|
|
493
|
+
*{box-sizing:border-box}html,body{margin:0;min-height:100%}
|
|
494
|
+
body{min-height:100vh;display:grid;place-items:center;padding:32px 16px;font:15px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;color:#ece9ff;
|
|
495
|
+
background:radial-gradient(900px 600px at 15% 0%,color-mix(in srgb,var(--c1) 35%,transparent),transparent 60%),radial-gradient(800px 600px at 100% 100%,color-mix(in srgb,var(--c2) 22%,transparent),transparent 60%),#0d0a24}
|
|
496
|
+
main{width:min(440px,100%);display:grid;gap:16px}
|
|
497
|
+
ots-identity{display:block}
|
|
498
|
+
.foot{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;font-size:12px;color:#a9a3d6}
|
|
499
|
+
.foot a{color:inherit}
|
|
500
|
+
</style></head><body><main>
|
|
501
|
+
<ots-identity src="${htmlEsc(ep)}/identity?format=json" face></ots-identity>
|
|
502
|
+
<p class="foot"><a href="${htmlEsc(ep)}/identity?format=json">JSON</a><a href="${htmlEsc(ep)}/identity.vcf">vCard</a><a href="https://7ots.com">7ots.com</a></p>
|
|
503
|
+
</main><script src="/api/agent/7ots.js"></script></body></html>`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* The ot's unique traits as NFT-style metadata (ERC-721 / Metaplex shape). The face fingerprint is
|
|
508
|
+
* what makes an ot unique (UNIQUE index); the permanent apuchat @handle is its unique name. When
|
|
509
|
+
* minting ships, the token will be these traits: whoever holds it holds the ot.
|
|
510
|
+
*/
|
|
511
|
+
function traitsMeta(ots, ep) {
|
|
512
|
+
const look = ots.identity?.look || {};
|
|
513
|
+
const ch = look.character || {};
|
|
514
|
+
const handle = ots.settings?.APUCHAT_AGENT_CALLSIGN && ots.settings.APUCHAT_AGENT_FREE !== '1' ? ots.settings.APUCHAT_AGENT_CALLSIGN : null;
|
|
515
|
+
const attr = (trait_type, value) => (value == null || value === '' ? null : { trait_type, value });
|
|
516
|
+
return {
|
|
517
|
+
name: ots.identity?.name || ots.name,
|
|
518
|
+
description: ots.identity?.tagline || ots.identity?.role || '',
|
|
519
|
+
external_url: `${ep}/identity`,
|
|
520
|
+
fingerprint: ots.lookPrint || null,
|
|
521
|
+
handle,
|
|
522
|
+
minted: false,
|
|
523
|
+
attributes: [
|
|
524
|
+
attr('Kind', look.kind),
|
|
525
|
+
attr('Material', look.kind === 'plush' ? look.material : null),
|
|
526
|
+
attr('Body', ch.body?.shape),
|
|
527
|
+
attr('Body color', ch.body?.color),
|
|
528
|
+
attr('Pattern', ch.body?.pattern && ch.body.pattern !== 'none' ? ch.body.pattern : null),
|
|
529
|
+
attr('Finish', ch.finish),
|
|
530
|
+
attr('Eyes', ch.eyes?.type),
|
|
531
|
+
attr('Brows', ch.brows?.type),
|
|
532
|
+
attr('Mouth', ch.mouth?.type),
|
|
533
|
+
attr('Cheeks', ch.cheeks?.type),
|
|
534
|
+
attr('Accessories', Array.isArray(ch.accessories) ? ch.accessories.map((a) => a?.id).filter(Boolean).join(', ') || 'none' : null),
|
|
535
|
+
].filter(Boolean),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { createLLM } from '../llm.mjs';
|
|
17
|
-
import { synthesize as synthesizeWith, ttsProvider } from '../tts.mjs';
|
|
17
|
+
import { synthesize as synthesizeWith, ttsProvider, openaiTtsKey } from '../tts.mjs';
|
|
18
18
|
import { startAgentChannels } from '../channels/index.mjs';
|
|
19
19
|
import { apumailConfigFromEnv } from '../channels/apumail.mjs';
|
|
20
20
|
import { apuchatConfigFromEnv } from '../channels/apuchat.mjs';
|
|
@@ -75,7 +75,7 @@ export function otsEnv(ots) {
|
|
|
75
75
|
|
|
76
76
|
// Voz: la suya o la de apuchat de la plataforma.
|
|
77
77
|
let platformTts = false;
|
|
78
|
-
if (!TTS_KEYS.some((k) => env[k]) && penv.PLATFORM_APUCHAT_VOICE_TOKEN && (!env.TTS_PROVIDER || env.TTS_PROVIDER === 'apuchat')) {
|
|
78
|
+
if (!TTS_KEYS.some((k) => (k === 'OPENAI_API_KEY' ? openaiTtsKey(env) : env[k])) && penv.PLATFORM_APUCHAT_VOICE_TOKEN && (!env.TTS_PROVIDER || env.TTS_PROVIDER === 'apuchat')) {
|
|
79
79
|
platformTts = true;
|
|
80
80
|
// On the platform's token the engine and model are the platform's, not the ot's.
|
|
81
81
|
delete env.APUCHAT_VOICE_PROVIDER;
|