@7ots/cli 0.1.0

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.
Files changed (97) hide show
  1. package/.env.example +140 -0
  2. package/LICENSE +21 -0
  3. package/README.md +655 -0
  4. package/brand/apple-touch-icon.png +0 -0
  5. package/brand/favicon.svg +4 -0
  6. package/brand/icon-512.png +0 -0
  7. package/brand/mark-mono.svg +3 -0
  8. package/brand/mark.svg +4 -0
  9. package/brand/og-es.png +0 -0
  10. package/brand/og-pt.png +0 -0
  11. package/brand/og.png +0 -0
  12. package/brand/ots.json +11 -0
  13. package/brand/tokens.css +59 -0
  14. package/brand/wordmark.svg +5 -0
  15. package/cli/7ots.mjs +400 -0
  16. package/cli/lib/brain.mjs +143 -0
  17. package/cli/lib/config.mjs +70 -0
  18. package/cli/lib/hooks.mjs +151 -0
  19. package/cli/lib/i18n.mjs +480 -0
  20. package/cli/lib/identity.mjs +51 -0
  21. package/cli/lib/install.mjs +79 -0
  22. package/cli/lib/lines.mjs +73 -0
  23. package/cli/lib/meet.mjs +166 -0
  24. package/cli/lib/open.mjs +19 -0
  25. package/cli/lib/orquesta.mjs +84 -0
  26. package/cli/lib/paths.mjs +49 -0
  27. package/cli/lib/pet-core.mjs +184 -0
  28. package/cli/lib/pet-server.mjs +296 -0
  29. package/cli/lib/prompts.mjs +75 -0
  30. package/cli/lib/terminal.mjs +145 -0
  31. package/cli/lib/ui.mjs +234 -0
  32. package/cli/lib/wizard.mjs +279 -0
  33. package/cli/pet/electron/main.cjs +86 -0
  34. package/cli/pet/pet.html +404 -0
  35. package/dist/7ots.esm.js +627 -0
  36. package/dist/7ots.esm.js.map +7 -0
  37. package/dist/7ots.iife.js +627 -0
  38. package/dist/7ots.iife.js.map +7 -0
  39. package/llms.txt +57 -0
  40. package/package.json +73 -0
  41. package/server/admin.mjs +213 -0
  42. package/server/apuchat-relay.mjs +170 -0
  43. package/server/channels/agent.mjs +136 -0
  44. package/server/channels/apuchat.mjs +364 -0
  45. package/server/channels/apumail.mjs +211 -0
  46. package/server/channels/index.mjs +69 -0
  47. package/server/contact.mjs +151 -0
  48. package/server/demo.mjs +154 -0
  49. package/server/i18n.mjs +54 -0
  50. package/server/identity.mjs +108 -0
  51. package/server/llm.mjs +413 -0
  52. package/server/platform/auth.mjs +290 -0
  53. package/server/platform/crypto.mjs +80 -0
  54. package/server/platform/db.mjs +130 -0
  55. package/server/platform/routes.mjs +388 -0
  56. package/server/platform/runtime.mjs +180 -0
  57. package/server/platform/store.mjs +331 -0
  58. package/server/sdk.mjs +125 -0
  59. package/server/server.mjs +456 -0
  60. package/server/settings.mjs +177 -0
  61. package/server/tts.mjs +186 -0
  62. package/skills/7ots/SKILL.md +59 -0
  63. package/src/actions/ActionRegistry.js +254 -0
  64. package/src/actions/PageTools.js +270 -0
  65. package/src/actions/builtins.js +565 -0
  66. package/src/auth/AuthManager.js +128 -0
  67. package/src/avatar/AvatarStage.js +473 -0
  68. package/src/character/AvatarEditor.js +829 -0
  69. package/src/character/Character.js +1191 -0
  70. package/src/character/motion.js +794 -0
  71. package/src/character/parts.js +559 -0
  72. package/src/context/ContextManager.js +472 -0
  73. package/src/core/AgentBrain.js +341 -0
  74. package/src/core/AgentWidget.js +1059 -0
  75. package/src/core/EventBus.js +61 -0
  76. package/src/core/Proactivity.js +363 -0
  77. package/src/core/storage.js +35 -0
  78. package/src/i18n/index.js +193 -0
  79. package/src/i18n/messages/character.js +543 -0
  80. package/src/i18n/messages/identity.js +141 -0
  81. package/src/i18n/messages/platform.js +114 -0
  82. package/src/i18n/messages/server.js +630 -0
  83. package/src/i18n/messages/widget.js +362 -0
  84. package/src/identity/ContactCard.js +351 -0
  85. package/src/identity/Face.js +148 -0
  86. package/src/identity/random.js +112 -0
  87. package/src/identity/schema.js +262 -0
  88. package/src/index.js +117 -0
  89. package/src/integrations/apuchat.js +212 -0
  90. package/src/integrations/apumail.js +88 -0
  91. package/src/llm/ProxyLLM.js +87 -0
  92. package/src/mcp/McpClient.js +153 -0
  93. package/src/ui/Companion.js +498 -0
  94. package/src/ui/UIManager.js +425 -0
  95. package/src/ui/VirtualPointer.js +746 -0
  96. package/src/ui/markdown.js +94 -0
  97. package/src/voice/VoiceEngine.js +227 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * PageTools — herramientas que el sitio declara en su propio HTML.
3
+ *
4
+ * El dashboard ya tiene formularios y botones que hacen cosas (invitar a alguien, crear una
5
+ * carpeta, exportar). Con un atributo se convierten en herramientas del agente, sin escribir JS:
6
+ *
7
+ * <form data-ots-tool="invitar_miembro"
8
+ * data-ots-description="Invita a una persona al equipo"
9
+ * data-ots-confirm="¿Invitar a {email} como {rol}?"> ← "false" = sin confirmación
10
+ * <label>Email <input name="email" type="email" required></label>
11
+ * <select name="rol"><option>lector</option><option>editor</option></select>
12
+ * </form>
13
+ *
14
+ * <button data-ots-tool="exportar_csv" data-ots-description="Descarga la tabla en CSV">…</button>
15
+ *
16
+ * Formularios → los campos con `name` son los parámetros (tipo, enum, required y descripción
17
+ * salen del propio HTML). El agente rellena (con el ratón visible si está activo) y envía.
18
+ * Botones / enlaces → herramienta sin parámetros que hace click.
19
+ *
20
+ * Resultado: el sitio puede responder al agente disparando en el formulario/botón
21
+ * el.dispatchEvent(new CustomEvent('ots-result', { detail: {...} }))
22
+ * y si no, se devuelve el texto de [role=status] / [role=alert] que aparezca.
23
+ *
24
+ * Opcionales: data-ots-auth (solo con sesión), data-ots-ignore (ocultar al agente).
25
+ * Los campos privados (contraseña, tarjeta, [data-ots-private]) nunca son parámetros.
26
+ * La lista se actualiza sola cuando la página cambia (SPA, pestañas, modales).
27
+ */
28
+
29
+ import { setFieldValue } from './builtins.js';
30
+ import { translator } from '../i18n/index.js';
31
+ import '../i18n/messages/widget.js';
32
+
33
+ const PRIVATE = 'input[type=password], [autocomplete^=cc-], [autocomplete=one-time-code], [data-ots-private], [data-ots-private] *';
34
+ const RESULT_WAIT_MS = 1500;
35
+
36
+ export class PageTools {
37
+ /**
38
+ * @param {object} o { actions, context, bus, pointer?, t? } t = traductor de las confirmaciones
39
+ */
40
+ constructor({ actions, context, bus, pointer = null, t = translator() }) {
41
+ this.t = t;
42
+ this.actions = actions;
43
+ this.context = context;
44
+ this.bus = bus;
45
+ this.pointer = pointer;
46
+ /** name → Element */
47
+ this._tools = new Map();
48
+ this._offs = [];
49
+ this._timer = null;
50
+ }
51
+
52
+ start() {
53
+ const later = () => {
54
+ clearTimeout(this._timer);
55
+ this._timer = setTimeout(() => this.scan(), 250);
56
+ };
57
+ this._offs.push(this.bus.on('context:mutation', later), this.bus.on('context:route', later));
58
+ this.scan();
59
+ }
60
+
61
+ stop() {
62
+ clearTimeout(this._timer);
63
+ this._offs.forEach((off) => off?.());
64
+ this._offs = [];
65
+ for (const name of this._tools.keys()) this.actions.unregister(name);
66
+ this._tools.clear();
67
+ }
68
+
69
+ /** Sincroniza el registro con los [data-ots-tool] presentes ahora en el DOM. */
70
+ scan() {
71
+ const seen = new Map();
72
+ for (const el of document.querySelectorAll('[data-ots-tool]')) {
73
+ if (el.closest('[data-ots-ignore], [data-ots-root]')) continue;
74
+ const name = String(el.dataset.otsTool).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '_').slice(0, 64);
75
+ if (!name || seen.has(name)) continue;
76
+ if (this.actions.has(name) && !this._tools.has(name)) {
77
+ console.warn(`[7ots] data-ots-tool="${name}" choca con una acción existente; se ignora.`);
78
+ continue;
79
+ }
80
+ seen.set(name, el);
81
+ }
82
+ for (const [name, el] of this._tools) {
83
+ if (seen.get(name) !== el) {
84
+ this.actions.unregister(name);
85
+ this._tools.delete(name);
86
+ }
87
+ }
88
+ for (const [name, el] of seen) {
89
+ if (this._tools.get(name) === el) continue;
90
+ this.actions.register(el.tagName === 'FORM' ? this._formTool(name, el) : this._buttonTool(name, el));
91
+ this._tools.set(name, el);
92
+ }
93
+ }
94
+
95
+ list() {
96
+ return [...this._tools.keys()];
97
+ }
98
+
99
+ // ───────────────────────────── formularios ─────────────────────────────
100
+
101
+ _formTool(name, form) {
102
+ const fields = formFields(form);
103
+ const properties = {};
104
+ const required = [];
105
+ for (const f of fields) {
106
+ properties[f.name] = f.schema;
107
+ if (f.required) required.push(f.name);
108
+ }
109
+ const privateRequired = [...form.querySelectorAll(PRIVATE)].some((el) => el.required);
110
+ return {
111
+ name,
112
+ category: 'page',
113
+ title: form.dataset.otsTitle || name,
114
+ description:
115
+ `[página] ${form.dataset.otsDescription || form.getAttribute('aria-label') || form.querySelector('legend, h2, h3')?.textContent?.trim() || name}` +
116
+ (privateRequired ? ' (tiene campos privados que el usuario debe escribir antes)' : ''),
117
+ parameters: { type: 'object', properties, required },
118
+ requiresAuth: form.hasAttribute('data-ots-auth'),
119
+ confirm: confirmFor(form, this.t),
120
+ timeoutMs: 60000,
121
+ handler: async (args, { ui }) => {
122
+ if (!form.isConnected) return { ok: false, error: 'El formulario ya no está en la página.' };
123
+ const visible = isVisible(form);
124
+ for (const f of fields) {
125
+ if (!(f.name in args)) continue;
126
+ const value = args[f.name];
127
+ const el = f.pick(value);
128
+ if (!el) return { ok: false, error: `Valor no válido para ${f.name}: ${value}` };
129
+ if (this.pointer && visible && isVisible(el)) {
130
+ if (f.kind === 'text') {
131
+ await this.pointer.type(el, String(value), { clear: true });
132
+ el.dispatchEvent(new Event('change', { bubbles: true }));
133
+ } else if (f.kind === 'check' || f.kind === 'radio') {
134
+ const want = f.kind === 'radio' ? true : value === true || value === 'true';
135
+ if (el.checked !== want) await this.pointer.click(el);
136
+ } else {
137
+ await this.pointer.moveTo(el);
138
+ setFieldValue(el, f.kind === 'number' ? String(value) : value);
139
+ }
140
+ } else {
141
+ setFieldValue(el, typeof value === 'boolean' ? String(value) : String(value));
142
+ }
143
+ }
144
+ if (!form.checkValidity()) {
145
+ const bad = [...form.elements].filter((el) => el.willValidate && !el.validity.valid);
146
+ if (visible) form.reportValidity();
147
+ return { ok: false, error: `El formulario no es válido: ${bad.map((el) => `${el.name || el.id}: ${el.validationMessage}`).join('; ')}` };
148
+ }
149
+ const submitter = form.querySelector('[type=submit], button:not([type])');
150
+ if (this.pointer && visible && submitter && isVisible(submitter)) await this.pointer.moveTo(submitter);
151
+ const result = waitResult(form);
152
+ form.requestSubmit ? form.requestSubmit(submitter || undefined) : form.submit();
153
+ if (!visible) ui?.toast?.({ message: this.t('widget.pageTools.done', { title: form.dataset.otsTitle || name.replace(/_/g, ' ') }), type: 'success' });
154
+ return { ok: true, data: (await result) ?? 'Enviado.' };
155
+ },
156
+ };
157
+ }
158
+
159
+ // ───────────────────────────── botones ─────────────────────────────
160
+
161
+ _buttonTool(name, el) {
162
+ return {
163
+ name,
164
+ category: 'page',
165
+ title: el.dataset.otsTitle || name,
166
+ description: `[página] ${el.dataset.otsDescription || el.getAttribute('aria-label') || el.textContent.trim().slice(0, 80) || name}`,
167
+ parameters: { type: 'object', properties: {} },
168
+ requiresAuth: el.hasAttribute('data-ots-auth'),
169
+ confirm: confirmFor(el, this.t),
170
+ handler: async () => {
171
+ if (!el.isConnected) return { ok: false, error: 'El botón ya no está en la página.' };
172
+ const result = waitResult(el);
173
+ if (this.pointer && isVisible(el)) await this.pointer.click(el);
174
+ else el.click();
175
+ return { ok: true, data: (await result) ?? 'Hecho.' };
176
+ },
177
+ };
178
+ }
179
+ }
180
+
181
+ // ───────────────────────────── helpers ─────────────────────────────
182
+
183
+ /** Describe los campos con name de un formulario como parámetros JSON Schema. */
184
+ function formFields(form) {
185
+ const groups = new Map();
186
+ for (const el of form.elements) {
187
+ if (!el.name || el.disabled || el.matches(PRIVATE) || el.closest('[data-ots-ignore]')) continue;
188
+ if (/^(hidden|submit|button|reset|file|image)$/.test(el.type)) continue;
189
+ if (!groups.has(el.name)) groups.set(el.name, []);
190
+ groups.get(el.name).push(el);
191
+ }
192
+ const out = [];
193
+ for (const [name, els] of groups) {
194
+ const el = els[0];
195
+ const desc = el.dataset.otsDescription || labelText(el) || el.placeholder || name;
196
+ let schema;
197
+ let kind = 'text';
198
+ let pick = () => el;
199
+ if (el.type === 'radio') {
200
+ kind = 'radio';
201
+ const values = els.map((r) => r.value);
202
+ schema = { type: 'string', enum: values, description: `${labelText(el.closest('fieldset')?.querySelector('legend')) || desc}` };
203
+ pick = (v) => els.find((r) => r.value === v) || null;
204
+ } else if (el.type === 'checkbox') {
205
+ kind = 'check';
206
+ schema = { type: 'boolean', description: desc };
207
+ } else if (el.tagName === 'SELECT') {
208
+ kind = 'select';
209
+ schema = { type: 'string', enum: [...el.options].filter((o) => !o.disabled && o.value !== '').map((o) => o.value), description: desc };
210
+ } else if (el.type === 'number' || el.type === 'range') {
211
+ kind = 'number';
212
+ schema = { type: 'number', description: [desc, el.min && `mín. ${el.min}`, el.max && `máx. ${el.max}`].filter(Boolean).join(', ') };
213
+ } else {
214
+ schema = { type: 'string', description: desc + (el.type && el.type !== 'text' && el.tagName === 'INPUT' ? ` (${el.type})` : '') };
215
+ }
216
+ out.push({ name, schema, kind, pick, required: els.some((x) => x.required) });
217
+ }
218
+ return out;
219
+ }
220
+
221
+ function confirmFor(el, t = translator()) {
222
+ const c = el.dataset.otsConfirm;
223
+ if (c === 'false') return false;
224
+ const title = el.dataset.otsTitle || el.dataset.otsTool.replace(/_/g, ' ');
225
+ return (args) => {
226
+ if (c && c !== 'true') return c.replace(/\{(\w+)\}/g, (_, k) => String(args?.[k] ?? ''));
227
+ const detail = Object.entries(args || {}).map(([k, v]) => `${k}: **${v}**`).join(', ');
228
+ return detail ? t('widget.confirm.pageToolWith', { title, detail }) : t('widget.confirm.pageTool', { title });
229
+ };
230
+ }
231
+
232
+ /** Espera la respuesta del sitio (evento ots-result) o un mensaje de estado. */
233
+ function waitResult(el) {
234
+ const scope = el.closest('form, [role=dialog], section, main') || document.body;
235
+ const before = statusText(scope);
236
+ return new Promise((resolve) => {
237
+ const done = (v) => {
238
+ clearTimeout(timer);
239
+ el.removeEventListener('ots-result', onResult);
240
+ resolve(v);
241
+ };
242
+ const onResult = (e) => done(e.detail ?? 'Hecho.');
243
+ const timer = setTimeout(() => {
244
+ const after = statusText(scope.isConnected ? scope : document.body);
245
+ done(after && after !== before ? after : null);
246
+ }, RESULT_WAIT_MS);
247
+ el.addEventListener('ots-result', onResult);
248
+ });
249
+ }
250
+
251
+ function statusText(scope) {
252
+ return [...scope.querySelectorAll('[role=status], [role=alert], [aria-live]')]
253
+ .map((n) => n.innerText?.trim())
254
+ .filter(Boolean)
255
+ .join(' · ')
256
+ .slice(0, 500);
257
+ }
258
+
259
+ function labelText(el) {
260
+ if (!el) return '';
261
+ const l = el.labels?.[0] || (el.tagName === 'LEGEND' ? el : null);
262
+ const t = l ? [...l.childNodes].filter((n) => n.nodeType === 3).map((n) => n.textContent).join(' ') : el.getAttribute?.('aria-label') || '';
263
+ return t.replace(/\s+/g, ' ').trim().slice(0, 80);
264
+ }
265
+
266
+ function isVisible(el) {
267
+ if (!el.isConnected || el.closest('[hidden], [inert]')) return false;
268
+ const r = el.getBoundingClientRect();
269
+ return r.width > 0 && r.height > 0;
270
+ }