@milpa/comal 0.2.3 → 0.2.5
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 +1 -1
- package/backend/contracts/remote-device-v1.json +470 -0
- package/package.json +2 -1
- package/src/cli.mjs +55 -10
- package/src/qr-scan.mjs +240 -0
- package/src/relay/broker.mjs +13 -0
- package/src/remote-control.mjs +227 -17
- package/src/remote-device-card.mjs +111 -0
- package/src/remote-device.mjs +97 -0
- package/src/remote-permissions.mjs +159 -0
- package/src/remote-speech.mjs +275 -0
- package/src/remote-stats.mjs +205 -0
- package/src/tui/discovery.mjs +9 -3
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ comal --help
|
|
|
28
28
|
|
|
29
29
|
Keep the PATH line in your shell startup file if you use that prefix. If your Node manager already provides a user prefix, you can omit `--prefix "$HOME/.local"`. The package registers the `comal` executable; you do not need to create a shell alias.
|
|
30
30
|
|
|
31
|
-
You can pin a version with `npx @milpa/comal@0.2.
|
|
31
|
+
You can pin a version with `npx @milpa/comal@0.2.5` or `npm install --global @milpa/comal@0.2.5`. The local release manifest preserves SHA-256 and SHA-512 integrity is checked against npm at publish time. No independent publisher signature is claimed.
|
|
32
32
|
|
|
33
33
|
## Test without account or model
|
|
34
34
|
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
{
|
|
2
|
+
"protocol": "comal.remote.device.v1",
|
|
3
|
+
"description": "Contract between a Comal Remote Control (/rc) session and an M5Stack device: pairing, the two inputs a device may send, the ephemeral permission card, the session.stats projection, the ephemeral speech chunks (8-bit unsigned mono PCM the host sends the paired device through the relay; never stored) and the reasons a device is refused. The device is a pre-check surface only: the local Comal host revalidates every answer (arm, expiry, card digest, session, device) before mutating. Owned by the Comal Node repository; the backend keeps a byte-identical copy.",
|
|
4
|
+
"pairing": {
|
|
5
|
+
"code_alphabet": "0123456789ABCDEFGHJKMNPQRSTVWXYZ",
|
|
6
|
+
"code_length": 8,
|
|
7
|
+
"code_ttl_seconds": 600,
|
|
8
|
+
"poll_interval_seconds": 5
|
|
9
|
+
},
|
|
10
|
+
"inputs": {
|
|
11
|
+
"permission_answer": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"properties": {
|
|
15
|
+
"question_id": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
|
18
|
+
},
|
|
19
|
+
"card_digest": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
22
|
+
},
|
|
23
|
+
"decision": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"enum": [
|
|
26
|
+
"allow_once",
|
|
27
|
+
"deny"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"required": [
|
|
32
|
+
"question_id",
|
|
33
|
+
"card_digest",
|
|
34
|
+
"decision"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"quick_reply": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"additionalProperties": false,
|
|
40
|
+
"properties": {
|
|
41
|
+
"text": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": [
|
|
44
|
+
"continúa",
|
|
45
|
+
"sí",
|
|
46
|
+
"no"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"required": [
|
|
51
|
+
"text"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"stats": {
|
|
56
|
+
"maxBytes": 2048,
|
|
57
|
+
"schema": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"additionalProperties": false,
|
|
60
|
+
"properties": {
|
|
61
|
+
"model": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"additionalProperties": false,
|
|
64
|
+
"properties": {
|
|
65
|
+
"provider": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"maxLength": 64
|
|
68
|
+
},
|
|
69
|
+
"profile": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"maxLength": 64
|
|
72
|
+
},
|
|
73
|
+
"model": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"maxLength": 64
|
|
76
|
+
},
|
|
77
|
+
"effort": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"maxLength": 64
|
|
80
|
+
},
|
|
81
|
+
"effort_levels": {
|
|
82
|
+
"type": "array",
|
|
83
|
+
"maxItems": 8,
|
|
84
|
+
"items": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"maxLength": 64
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"capabilities": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"additionalProperties": false,
|
|
94
|
+
"properties": {
|
|
95
|
+
"tools": {
|
|
96
|
+
"type": [
|
|
97
|
+
"boolean",
|
|
98
|
+
"null"
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
"vision": {
|
|
102
|
+
"type": [
|
|
103
|
+
"boolean",
|
|
104
|
+
"null"
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
"thinking": {
|
|
108
|
+
"type": [
|
|
109
|
+
"boolean",
|
|
110
|
+
"null"
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
"cache": {
|
|
114
|
+
"type": [
|
|
115
|
+
"boolean",
|
|
116
|
+
"null"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"sources": {
|
|
120
|
+
"type": "object",
|
|
121
|
+
"maxProperties": 8,
|
|
122
|
+
"additionalProperties": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"maxLength": 64
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"permissions": {
|
|
130
|
+
"type": "object",
|
|
131
|
+
"additionalProperties": false,
|
|
132
|
+
"properties": {
|
|
133
|
+
"mode": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"enum": [
|
|
136
|
+
"default",
|
|
137
|
+
"acceptEdits",
|
|
138
|
+
"plan",
|
|
139
|
+
"auto",
|
|
140
|
+
"bypassPermissions"
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
"device_armed": {
|
|
144
|
+
"type": "boolean"
|
|
145
|
+
},
|
|
146
|
+
"device_until": {
|
|
147
|
+
"type": [
|
|
148
|
+
"integer",
|
|
149
|
+
"null"
|
|
150
|
+
],
|
|
151
|
+
"minimum": 0
|
|
152
|
+
},
|
|
153
|
+
"add_dirs": {
|
|
154
|
+
"type": "integer",
|
|
155
|
+
"minimum": 0
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"context": {
|
|
160
|
+
"type": "object",
|
|
161
|
+
"additionalProperties": false,
|
|
162
|
+
"properties": {
|
|
163
|
+
"input_tokens": {
|
|
164
|
+
"type": "integer",
|
|
165
|
+
"minimum": 0
|
|
166
|
+
},
|
|
167
|
+
"window": {
|
|
168
|
+
"type": "integer",
|
|
169
|
+
"minimum": 0
|
|
170
|
+
},
|
|
171
|
+
"output_reserve": {
|
|
172
|
+
"type": "integer",
|
|
173
|
+
"minimum": 0
|
|
174
|
+
},
|
|
175
|
+
"percent": {
|
|
176
|
+
"type": "number",
|
|
177
|
+
"minimum": 0,
|
|
178
|
+
"maximum": 100
|
|
179
|
+
},
|
|
180
|
+
"compaction_window": {
|
|
181
|
+
"type": "integer",
|
|
182
|
+
"minimum": 0
|
|
183
|
+
},
|
|
184
|
+
"soft_threshold": {
|
|
185
|
+
"type": "integer",
|
|
186
|
+
"minimum": 0
|
|
187
|
+
},
|
|
188
|
+
"hard_threshold": {
|
|
189
|
+
"type": "integer",
|
|
190
|
+
"minimum": 0
|
|
191
|
+
},
|
|
192
|
+
"last_compaction": {
|
|
193
|
+
"type": [
|
|
194
|
+
"object",
|
|
195
|
+
"null"
|
|
196
|
+
],
|
|
197
|
+
"additionalProperties": false,
|
|
198
|
+
"properties": {
|
|
199
|
+
"at": {
|
|
200
|
+
"type": "integer",
|
|
201
|
+
"minimum": 0
|
|
202
|
+
},
|
|
203
|
+
"trigger": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"maxLength": 64
|
|
206
|
+
},
|
|
207
|
+
"before": {
|
|
208
|
+
"type": "integer",
|
|
209
|
+
"minimum": 0
|
|
210
|
+
},
|
|
211
|
+
"after": {
|
|
212
|
+
"type": "integer",
|
|
213
|
+
"minimum": 0
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
"messages_active": {
|
|
218
|
+
"type": "integer",
|
|
219
|
+
"minimum": 0
|
|
220
|
+
},
|
|
221
|
+
"messages_original": {
|
|
222
|
+
"type": "integer",
|
|
223
|
+
"minimum": 0
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
"usage": {
|
|
228
|
+
"type": "object",
|
|
229
|
+
"additionalProperties": false,
|
|
230
|
+
"properties": {
|
|
231
|
+
"last": {
|
|
232
|
+
"type": "object",
|
|
233
|
+
"additionalProperties": false,
|
|
234
|
+
"properties": {
|
|
235
|
+
"input": {
|
|
236
|
+
"type": "integer",
|
|
237
|
+
"minimum": 0
|
|
238
|
+
},
|
|
239
|
+
"output": {
|
|
240
|
+
"type": "integer",
|
|
241
|
+
"minimum": 0
|
|
242
|
+
},
|
|
243
|
+
"cached": {
|
|
244
|
+
"type": "integer",
|
|
245
|
+
"minimum": 0
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
"session": {
|
|
250
|
+
"type": "object",
|
|
251
|
+
"additionalProperties": false,
|
|
252
|
+
"properties": {
|
|
253
|
+
"input": {
|
|
254
|
+
"type": "integer",
|
|
255
|
+
"minimum": 0
|
|
256
|
+
},
|
|
257
|
+
"output": {
|
|
258
|
+
"type": "integer",
|
|
259
|
+
"minimum": 0
|
|
260
|
+
},
|
|
261
|
+
"cached": {
|
|
262
|
+
"type": "integer",
|
|
263
|
+
"minimum": 0
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"calls": {
|
|
268
|
+
"type": "integer",
|
|
269
|
+
"minimum": 0
|
|
270
|
+
},
|
|
271
|
+
"call_limit": {
|
|
272
|
+
"type": "integer",
|
|
273
|
+
"minimum": 0
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
"timing": {
|
|
278
|
+
"type": "object",
|
|
279
|
+
"additionalProperties": false,
|
|
280
|
+
"properties": {
|
|
281
|
+
"comal_ms": {
|
|
282
|
+
"type": "integer",
|
|
283
|
+
"minimum": 0
|
|
284
|
+
},
|
|
285
|
+
"reasoning_ms": {
|
|
286
|
+
"type": "integer",
|
|
287
|
+
"minimum": 0
|
|
288
|
+
},
|
|
289
|
+
"first_response_ms": {
|
|
290
|
+
"type": "integer",
|
|
291
|
+
"minimum": 0
|
|
292
|
+
},
|
|
293
|
+
"tools_ms": {
|
|
294
|
+
"type": "integer",
|
|
295
|
+
"minimum": 0
|
|
296
|
+
},
|
|
297
|
+
"human_wait_ms": {
|
|
298
|
+
"type": "integer",
|
|
299
|
+
"minimum": 0
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
"work": {
|
|
304
|
+
"type": "object",
|
|
305
|
+
"additionalProperties": false,
|
|
306
|
+
"properties": {
|
|
307
|
+
"objective": {
|
|
308
|
+
"type": "string",
|
|
309
|
+
"maxLength": 120
|
|
310
|
+
},
|
|
311
|
+
"done": {
|
|
312
|
+
"type": "integer",
|
|
313
|
+
"minimum": 0
|
|
314
|
+
},
|
|
315
|
+
"total": {
|
|
316
|
+
"type": "integer",
|
|
317
|
+
"minimum": 0
|
|
318
|
+
},
|
|
319
|
+
"current_task": {
|
|
320
|
+
"type": [
|
|
321
|
+
"string",
|
|
322
|
+
"null"
|
|
323
|
+
],
|
|
324
|
+
"maxLength": 120
|
|
325
|
+
},
|
|
326
|
+
"checks_passing": {
|
|
327
|
+
"type": "integer",
|
|
328
|
+
"minimum": 0
|
|
329
|
+
},
|
|
330
|
+
"checks_failing": {
|
|
331
|
+
"type": "integer",
|
|
332
|
+
"minimum": 0
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
"connection": {
|
|
337
|
+
"type": "object",
|
|
338
|
+
"additionalProperties": false,
|
|
339
|
+
"properties": {
|
|
340
|
+
"transport": {
|
|
341
|
+
"type": "string",
|
|
342
|
+
"enum": [
|
|
343
|
+
"websocket",
|
|
344
|
+
"https-polling",
|
|
345
|
+
"disconnected"
|
|
346
|
+
]
|
|
347
|
+
},
|
|
348
|
+
"host_online": {
|
|
349
|
+
"type": "boolean"
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
"card": {
|
|
357
|
+
"maxBytes": 4096,
|
|
358
|
+
"schema": {
|
|
359
|
+
"type": "object",
|
|
360
|
+
"additionalProperties": false,
|
|
361
|
+
"properties": {
|
|
362
|
+
"question_id": {
|
|
363
|
+
"type": "string",
|
|
364
|
+
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
|
365
|
+
},
|
|
366
|
+
"card_digest": {
|
|
367
|
+
"type": "string",
|
|
368
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
369
|
+
},
|
|
370
|
+
"tool": {
|
|
371
|
+
"type": "string",
|
|
372
|
+
"maxLength": 64
|
|
373
|
+
},
|
|
374
|
+
"kind": {
|
|
375
|
+
"type": "string",
|
|
376
|
+
"enum": [
|
|
377
|
+
"bash",
|
|
378
|
+
"write",
|
|
379
|
+
"edit",
|
|
380
|
+
"summary",
|
|
381
|
+
"terminal"
|
|
382
|
+
]
|
|
383
|
+
},
|
|
384
|
+
"lines": {
|
|
385
|
+
"type": "array",
|
|
386
|
+
"maxItems": 16,
|
|
387
|
+
"items": {
|
|
388
|
+
"type": "string",
|
|
389
|
+
"maxLength": 120
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
"truncated": {
|
|
393
|
+
"type": "boolean"
|
|
394
|
+
},
|
|
395
|
+
"overwrite": {
|
|
396
|
+
"type": "boolean"
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
"required": [
|
|
400
|
+
"question_id",
|
|
401
|
+
"card_digest",
|
|
402
|
+
"tool",
|
|
403
|
+
"kind",
|
|
404
|
+
"lines",
|
|
405
|
+
"truncated"
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
"speech": {
|
|
410
|
+
"maxBytes": 6144,
|
|
411
|
+
"format": {
|
|
412
|
+
"encoding": "pcm_u8",
|
|
413
|
+
"channels": 1,
|
|
414
|
+
"sample_rate": 11025,
|
|
415
|
+
"chunk_max_bytes": 4096,
|
|
416
|
+
"phrase_max_seconds": 6
|
|
417
|
+
},
|
|
418
|
+
"schema": {
|
|
419
|
+
"type": "object",
|
|
420
|
+
"additionalProperties": false,
|
|
421
|
+
"properties": {
|
|
422
|
+
"id": {
|
|
423
|
+
"type": "string",
|
|
424
|
+
"pattern": "^[0-9a-f]{16}$"
|
|
425
|
+
},
|
|
426
|
+
"seq": {
|
|
427
|
+
"type": "string",
|
|
428
|
+
"pattern": "^(0|[1-9][0-9]?)$"
|
|
429
|
+
},
|
|
430
|
+
"last": {
|
|
431
|
+
"type": "boolean"
|
|
432
|
+
},
|
|
433
|
+
"about": {
|
|
434
|
+
"type": "string",
|
|
435
|
+
"enum": [
|
|
436
|
+
"permission",
|
|
437
|
+
"failure",
|
|
438
|
+
"turn"
|
|
439
|
+
]
|
|
440
|
+
},
|
|
441
|
+
"audio": {
|
|
442
|
+
"type": "string",
|
|
443
|
+
"maxLength": 5464,
|
|
444
|
+
"pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$"
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
"required": [
|
|
448
|
+
"id",
|
|
449
|
+
"seq",
|
|
450
|
+
"last",
|
|
451
|
+
"about",
|
|
452
|
+
"audio"
|
|
453
|
+
]
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
"arm": {
|
|
457
|
+
"default_seconds": 1800,
|
|
458
|
+
"max_seconds": 28800
|
|
459
|
+
},
|
|
460
|
+
"refusals": [
|
|
461
|
+
"disarmed",
|
|
462
|
+
"expired",
|
|
463
|
+
"stale",
|
|
464
|
+
"terminal_only",
|
|
465
|
+
"other_device",
|
|
466
|
+
"plan_mode",
|
|
467
|
+
"confirm_always",
|
|
468
|
+
"offline"
|
|
469
|
+
]
|
|
470
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milpa/comal",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Comal: governed operations, execution receipts, and programming assistance in any language",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"backend/contracts/remote-commands-v1.json",
|
|
26
26
|
"backend/contracts/remote-relay-answers-v1.json",
|
|
27
27
|
"backend/contracts/remote-workspace-v1.schema.json",
|
|
28
|
+
"backend/contracts/remote-device-v1.json",
|
|
28
29
|
"governance/source-task-v1.json",
|
|
29
30
|
"governance/source-task-v2.json",
|
|
30
31
|
"governance/chrome-task-v1.json",
|
package/src/cli.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { forwardHouseRow, parseRcDevice, runRcDevice, runRcDeviceScan, deviceCodeChoice, parseRcPermissions, runRcPermissions } from './remote-device.mjs';
|
|
2
|
+
import { scanPairingQr } from './qr-scan.mjs';
|
|
1
3
|
import { registerSecretEnv } from './child-env.mjs';
|
|
2
4
|
import {prepareSourceTask,readSourceTask} from './workflows/source-task.mjs';
|
|
3
5
|
import {savedSourceTask} from './source-task-proof.mjs';
|
|
@@ -5,6 +7,8 @@ import { t, registerMessages, configureLanguage, languageArguments } from './i18
|
|
|
5
7
|
import cliSpanish from './i18n/cli-es.mjs';
|
|
6
8
|
registerMessages(cliSpanish);
|
|
7
9
|
import { RemoteHost } from './remote-control.mjs';
|
|
10
|
+
import { speechFromEnv, permissionSpeech } from './remote-speech.mjs';
|
|
11
|
+
import { PermissionDesk, confirmWithDevice } from './remote-permissions.mjs';
|
|
8
12
|
import { remoteCommandHelp, isRemoteCommand } from './remote-commands.mjs';
|
|
9
13
|
import { codeRunDefinition } from './code-run.mjs';
|
|
10
14
|
import {installCodeRunSession} from './code-run-session.mjs';
|
|
@@ -53,6 +57,7 @@ import {GitIntegrationOperations} from './git-integration-operations.mjs';
|
|
|
53
57
|
import {prepareGitPageCliBridge,assertGitPageCliEntry} from './git-page-cli-bridge.mjs';
|
|
54
58
|
import { registerGitSourceOperations } from './git-source.mjs';
|
|
55
59
|
import { projectRemoteWorkspace } from './remote-workspace.mjs';
|
|
60
|
+
import { buildSessionStats, readContext, statsFromRows } from './remote-stats.mjs';
|
|
56
61
|
import { evidenceOperations } from './work-state.mjs';
|
|
57
62
|
import { auditExecution, auditReport } from './audit.mjs';
|
|
58
63
|
import { comalEnvironment, defaultStateDir } from './compat.mjs';
|
|
@@ -371,10 +376,13 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
371
376
|
const interactive = !print;
|
|
372
377
|
const color = (code, text) => stdout.isTTY && !process.env.NO_COLOR ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
373
378
|
let remoteHost;
|
|
374
|
-
|
|
379
|
+
const remoteSpeech = speechFromEnv(process.env);
|
|
380
|
+
// One desk per process: a permission card answered from the paired M5Stack races the terminal card.
|
|
381
|
+
const permissionDesk = new PermissionDesk({ sendCard: card => remoteHost?.sendCard(card), closeCard: id => remoteHost?.closeCard(id), record: row => store.append(session.id, { ...row, source: 'remote_device', at: new Date().toISOString() }) });
|
|
382
|
+
let controller, authController, modelController, scanController, runtime, peerBus, listenTimer, listenFinish, swarmFeed, relayFollower, textOpen = false, lastGoalDisplay = '';
|
|
375
383
|
const peerGuard=new PeerTurnGuard({cwd,resolveWrite:file=>tools.resolve(file,{create:true}),effects:name=>house.registry.get(name)?.contract?.effects});
|
|
376
384
|
let lastModelProgress = 0;
|
|
377
|
-
setInterrupt(() => { if(modelController)modelController.abort();else if(authController)authController.abort();else if (controller) controller.abort(); else if (runtime) runtime.exit().catch(error => ui.notice(error.message, true)); else if(listenFinish)listenFinish();else ui?.close(); });
|
|
385
|
+
setInterrupt(() => { if(scanController)scanController.abort();else if(modelController)modelController.abort();else if(authController)authController.abort();else if (controller) controller.abort(); else if (runtime) runtime.exit().catch(error => ui.notice(error.message, true)); else if(listenFinish)listenFinish();else ui?.close(); });
|
|
378
386
|
// Private files named by flags join the default private locations: no /add-dir may cover them.
|
|
379
387
|
const flagPrivate = [v['state-dir'], v['providers-config'], v['plugin-config']].filter(Boolean).map(item => path.resolve(item));
|
|
380
388
|
const policy = new PermissionPolicy({ mode: permissionMode, dangerouslySkipPermissions: skipPermissions, allowed: csv(v['allowed-tools']), denied: csv(v['disallowed-tools']), cwd, readRoots, privateLocations: flagPrivate,
|
|
@@ -382,7 +390,10 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
382
390
|
confirm: interactive ? async (name, input, options = {}) => {
|
|
383
391
|
const signals = [options.signal, controller?.signal || startupSignal].filter(Boolean), signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
|
|
384
392
|
const card = options.summary === undefined ? await house.confirmSummary(name, input, { signal }) : null;
|
|
385
|
-
|
|
393
|
+
const summary = options.summary ?? card?.summary, terminal = shown => ui.confirm(name, input, shown, { summary, edit: card?.edit, remember: options.remember });
|
|
394
|
+
// Granito speaks the question whether or not the device is armed to answer it; synchronous, never awaited.
|
|
395
|
+
try { remoteHost?.sendSpeech(permissionSpeech({ name, input, armed: remoteHost?.permissionsArmed() === true, plan: policy.mode === 'plan', confirmAlways: policy.confirmAlways(name) === true }), 'permission'); } catch {}
|
|
396
|
+
return confirmWithDevice({ armed: remoteHost?.permissionsArmed() === true, desk: permissionDesk, questionId: options.questionId, name, input, summary, signal, terminal, resolveWrite: (file, how) => tools.resolve(file, how), confirmAlways: () => policy.confirmAlways(name), mode: () => policy.mode, notice: text => ui.notice(text) });
|
|
386
397
|
} : undefined });
|
|
387
398
|
// /add-dir rows are the human's decisions in this session's book (under .comal, which the model cannot write);
|
|
388
399
|
// on resume they open again if the directory still qualifies, otherwise one line says why.
|
|
@@ -400,7 +411,10 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
400
411
|
}
|
|
401
412
|
};
|
|
402
413
|
const recordLaunchPermissions = () => store.append(session.id, { type: 'mode', source: 'human_cli', mode: policy.mode, dangerously_skip_permissions: skipPermissions, scope: 'process_launch' });
|
|
414
|
+
// Model calls of the current work and their limit, as model_progress reports them (session.stats).
|
|
415
|
+
let statsCalls = null;
|
|
403
416
|
const onEvent = event => {
|
|
417
|
+
if (event.type === 'model_progress') statsCalls = { calls: event.turn, call_limit: event.maxTurns };
|
|
404
418
|
remoteHost?.observe(event);
|
|
405
419
|
if (ui) { ui.handleEvent(event); return; }
|
|
406
420
|
if (format === 'stream-json') stdout.write(JSON.stringify(event) + '\n');
|
|
@@ -438,7 +452,7 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
438
452
|
stderr.write(color('90', t("Session {v0} · {v1} calls · {v2} tokens", {v0: event.session_id, v1: event.turns, v2: event.usage.input_tokens + event.usage.output_tokens})) + '\n');
|
|
439
453
|
}
|
|
440
454
|
};
|
|
441
|
-
const house = new House({ cwd, record: async event => { const row=await store.append(event.session_id,event); if(event.session_id===session.id)remoteHost?.invalidateWorkspace(); return row; } });
|
|
455
|
+
const house = new House({ cwd, record: async event => { const row=await store.append(event.session_id,event); if(event.session_id===session.id)remoteHost?.invalidateWorkspace(); forwardHouseRow(event,{sessionId:session.id,remoteHost}); return row; } });
|
|
442
456
|
if (v.codemode) house.registry.add(codeRunDefinition);
|
|
443
457
|
house.guard('peer-authority',view=>peerGuard.check(view),{revision:'peer-turn-v1',configuration_digest:digest({cwd})});
|
|
444
458
|
const onPermission = decision => store.append(session.id, { type: 'permission', ...decision });
|
|
@@ -610,7 +624,7 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
610
624
|
house.registry.add({name:'CloudAccountLogout',description:'Human detaches Remote Control and removes the local Comal account credential.',input_schema:emptyInput,audience:'human',surfaces:['tui'],permission:null,effects:{workspace:'none',external:'write',process:'none',authority:'credential_control'},run:async()=>{await remoteHost?.stop();return cloudClient.logout();}});
|
|
611
625
|
const openRemoteControl=async()=>{
|
|
612
626
|
if(!runtime)throw Error('Remote Control requires an interactive local session.');
|
|
613
|
-
if(!remoteHost || remoteHost.stopped)remoteHost=new RemoteHost({client:new CloudClient({origin:v['cloud-url']}),onInput:(input,binding)=>runtime.admitRemote(input,binding),onStop:id=>runtime.stopRemote(id),onNotice:text=>ui.notice(text),getWorkspace:async snapshot=>projectRemoteWorkspace(snapshot || await store.load(session.id),house.stateContext()),relay:relayRemote,onRelay:interactive?(input,binding)=>relayAnswers.handle(input,binding):null});
|
|
627
|
+
if(!remoteHost || remoteHost.stopped)remoteHost=new RemoteHost({client:new CloudClient({origin:v['cloud-url']}),speech:remoteSpeech,onInput:(input,binding)=>runtime.admitRemote(input,binding),onStop:id=>runtime.stopRemote(id),onNotice:text=>ui.notice(text),getWorkspace:async snapshot=>projectRemoteWorkspace(snapshot || await store.load(session.id),house.stateContext()),relay:relayRemote,onRelay:interactive?(input,binding)=>relayAnswers.handle(input,binding):null,onPermission:interactive?(input,binding)=>permissionDesk.handle(input,binding):null,onPermissionsDisarmed:reason=>{store.append(session.id,{type:'remote_control',action:'permissions.arm',armed:false,until:null,reason,source:'host',at:new Date().toISOString()}).catch(()=>{});},getStats:sessionStats});
|
|
614
628
|
const connection = new AbortController(); authController = connection;
|
|
615
629
|
const cancel = () => remoteHost.stop().catch(() => {});
|
|
616
630
|
connection.signal.addEventListener('abort', cancel, { once: true });
|
|
@@ -798,7 +812,7 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
798
812
|
if (ui && error.usage) ui.handleEvent({ type: 'turn_usage', usage: error.usage });
|
|
799
813
|
if (ui) ui.notice(error.message, true); else stderr.write(`\n${color('31', 'Error')}: ${clean(error.message)}\n`);
|
|
800
814
|
return 1;
|
|
801
|
-
} finally { await remoteHost?.endTurn(); controller = null; peerGuard.reset();await peerBus.setState('idle'); }
|
|
815
|
+
} finally { statsCalls = null; await remoteHost?.endTurn(); controller = null; peerGuard.reset();await peerBus.setState('idle'); }
|
|
802
816
|
};
|
|
803
817
|
const peerProbe = memoizedPeerProbe({
|
|
804
818
|
stamp: async () => { const info = await stat(store.file(session.id)); return [session.id, info.size, info.mtimeMs, peerBus.inbound, peerBus.availability?.available].join(':'); },
|
|
@@ -815,7 +829,7 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
815
829
|
if(format==='json')stdout.write(JSON.stringify(result)+'\n');
|
|
816
830
|
return result.is_error?1:0;
|
|
817
831
|
} catch(error){if(ui)ui.notice(error.message,true);else stderr.write(t("Pending message: {v0}\n", {v0: clean(error.message)}));return 1;}
|
|
818
|
-
finally{await remoteHost?.endTurn();controller=null;peerGuard.reset();await peerBus.setState('idle');}
|
|
832
|
+
finally{statsCalls=null;await remoteHost?.endTurn();controller=null;peerGuard.reset();await peerBus.setState('idle');}
|
|
819
833
|
};
|
|
820
834
|
const directTool = async (name, input, text, userIntent) => {
|
|
821
835
|
if (name === 'Agent' && v.demo) throw new Error(t("Subagents require a real model; --demo is a script."));
|
|
@@ -888,7 +902,17 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
888
902
|
const format = { '.csv': 'csv', '.tsv': 'tsv', '.json': 'json', '.md': 'markdown' }[path.extname(file_path).toLowerCase()];
|
|
889
903
|
if (!format) throw new Error(t("Use a .csv, .tsv, .json, or .md file.")); return JSON.stringify({ text, format });
|
|
890
904
|
} });
|
|
891
|
-
const changeMode = mode => { if (skipPermissions) throw new Error(t(BYPASS_LOCKED_MESSAGE));
|
|
905
|
+
const changeMode = async mode => { if (skipPermissions) throw new Error(t(BYPASS_LOCKED_MESSAGE)); const result = await humanTools.execute('SessionMode', { mode }); remoteHost?.publishStats().catch(() => {}); return result; };
|
|
906
|
+
// session.stats for /rc: the same sources /status, /context and the status bar read; buildSessionStats keeps only numbers, enums and bounded labels.
|
|
907
|
+
const sessionStats = async () => { try {
|
|
908
|
+
const current = await store.load(session.id), recorded = statsFromRows(current.rows), rc = remoteHost?.status() || {};
|
|
909
|
+
let work = null; try { work = projectRemoteWorkspace(current, house.stateContext()); } catch {}
|
|
910
|
+
const calls = statsCalls || (recorded.usage?.calls !== undefined ? { calls: recorded.usage.calls, call_limit: agent.workMaxCalls } : {});
|
|
911
|
+
return buildSessionStats({ metadata: metadata(), promptCaching: typeof provider?.promptCaching === 'boolean' ? provider.promptCaching : null,
|
|
912
|
+
...readContext({ compactor: agent.compactor, system, tools: agent.tools.definitions(), maxTokens: requestProvider.maxTokens, session: current }),
|
|
913
|
+
lastCompaction: recorded.lastCompaction, usage: recorded.usage ? { ...recorded.usage, ...calls } : null, timing: recorded.timing, work,
|
|
914
|
+
permissions: rc.permission_answers || null, transport: rc.transport, addDirCount: policy.workRoots.length, cwd });
|
|
915
|
+
} catch { return null; } };
|
|
892
916
|
let discoveredModels = [], discoveredModelKey = '', discoveredSessions = [], discoveredPeers = [];
|
|
893
917
|
let authStatus={};
|
|
894
918
|
const refreshConnections=async()=>{const statuses=await Promise.allSettled(AUTH_PROVIDERS.map(item=>authManager.status(item.id)));authStatus=Object.fromEntries(AUTH_PROVIDERS.map((item,index)=>[item.id,statuses[index].status==='fulfilled'?statuses[index].value.status:'unknown']));};
|
|
@@ -955,7 +979,7 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
955
979
|
validateRemote: item => remoteHost?.validate(item) ?? Promise.reject(Error('Remote host detached.')),
|
|
956
980
|
remoteTurnMatches: id => !remoteHost?.stopped && remoteHost?.turnId===id,
|
|
957
981
|
detachRemote: () => remoteHost?.stop(),
|
|
958
|
-
metadata, discovery, execute: run, interrupt: () => {controller?.abort();authController?.abort();modelController?.abort();}, cycleMode: () => changeMode(nextPermissionMode(policy.mode)),
|
|
982
|
+
metadata, discovery, execute: run, interrupt: () => {controller?.abort();authController?.abort();modelController?.abort();scanController?.abort();}, cycleMode: () => changeMode(nextPermissionMode(policy.mode)),
|
|
959
983
|
hasPeerMessages:peerProbe,executePeer:runPeer,
|
|
960
984
|
listSessions: async () => { discoveredSessions = await store.list(); return discoveredSessions; }, saveQueue: (id, snapshot) => store.append(id, { type: 'queue', snapshot }), saveDraft: (id, text, attachments = []) => store.append(id, { type: 'draft', text, attachments }),
|
|
961
985
|
goalControl: argument => runGoal(argument),
|
|
@@ -1012,7 +1036,28 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
1012
1036
|
await relayFlow.record({action:'remote.arm',until:new Date(until).toISOString(),armed:true});
|
|
1013
1037
|
return relayStatus()+' '+t('Plans and success criteria also need a sign-in from the last 10 minutes in that browser; retrying blocked tasks stays here. /rc relay off disarms.');
|
|
1014
1038
|
}
|
|
1015
|
-
|
|
1039
|
+
// /rc device CODE · /rc device · /rc device forget: empareja el M5Stack con esta sesión de /rc.
|
|
1040
|
+
const deviceCommand=parseRcDevice(action);
|
|
1041
|
+
const bookRecord=row=>store.append(session.id,{...row,source:interactive?'human_tui':'human_cli',at:new Date().toISOString()});
|
|
1042
|
+
// /rc device scan [/dev/videoN]: la cámara lee el QR del M5Stack sólo durante este escaneo (≤30 s) y la persona confirma el código.
|
|
1043
|
+
if(deviceCommand?.kind==='scan'){
|
|
1044
|
+
const scan=async({camera})=>{
|
|
1045
|
+
scanController=new AbortController();
|
|
1046
|
+
const signal=controller?.signal?AbortSignal.any([controller.signal,scanController.signal]):scanController.signal;
|
|
1047
|
+
ui.setupStatus({title:t('Scanning the M5Stack pairing QR'),description:t('Bring the M5Stack 10-20 cm from the camera, so the QR fills about a third of the picture. Up to 30 s; Esc cancels. Frames stay on this machine and are deleted at once.'),signal});
|
|
1048
|
+
ui.callbacks.onSetupCancel=()=>scanController?.abort();
|
|
1049
|
+
try{return await scanPairingQr({camera,signal});}
|
|
1050
|
+
finally{scanController=null;delete ui.callbacks.onSetupCancel;ui.clearSetup();}
|
|
1051
|
+
};
|
|
1052
|
+
const confirm=interactive&&ui?async({code})=>await ui.setupChoice({...deviceCodeChoice(code,t),signal:startupSignal})==='pair':undefined;
|
|
1053
|
+
try{return await runRcDeviceScan(deviceCommand,remoteHost,t,{record:bookRecord,scan,confirm});}
|
|
1054
|
+
catch(error){if(error.name==='AbortError')return error.message;throw error;}
|
|
1055
|
+
}
|
|
1056
|
+
if(deviceCommand)return runRcDevice(deviceCommand,remoteHost,t,{record:bookRecord});
|
|
1057
|
+
// /rc permissions [on [30m|2h]|off]: el aparato emparejado responde tarjetas de permisos, sólo mientras esté armado.
|
|
1058
|
+
const permissionsCommand=parseRcPermissions(action);
|
|
1059
|
+
if(permissionsCommand)return runRcPermissions(permissionsCommand,remoteHost,t,{record:bookRecord});
|
|
1060
|
+
if(!['','start','status','stop'].includes(action))throw Error('Use /rc [start|status|stop|relay on [30m|2h|8h]|relay off|device [CODE|scan [/dev/videoN]|forget]|permissions [on [30m|2h]|off]].');
|
|
1016
1061
|
if(action==='stop')return humanTools.execute('RemoteControlClose',{});
|
|
1017
1062
|
if(action==='status')return JSON.stringify(remoteHost?.status() || {enabled:false},null,2);
|
|
1018
1063
|
if(!await cloudClient.credentials()) {
|