@milpa/comal 0.2.3 → 0.2.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/README.md +1 -1
- package/backend/contracts/remote-device-v1.json +423 -0
- package/package.json +2 -1
- package/src/cli.mjs +51 -10
- package/src/qr-scan.mjs +240 -0
- package/src/relay/broker.mjs +13 -0
- package/src/remote-control.mjs +179 -14
- package/src/remote-device-card.mjs +111 -0
- package/src/remote-device.mjs +92 -0
- package/src/remote-permissions.mjs +159 -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.4` or `npm install --global @milpa/comal@0.2.4`. 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,423 @@
|
|
|
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 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
|
+
"arm": {
|
|
410
|
+
"default_seconds": 1800,
|
|
411
|
+
"max_seconds": 28800
|
|
412
|
+
},
|
|
413
|
+
"refusals": [
|
|
414
|
+
"disarmed",
|
|
415
|
+
"expired",
|
|
416
|
+
"stale",
|
|
417
|
+
"terminal_only",
|
|
418
|
+
"other_device",
|
|
419
|
+
"plan_mode",
|
|
420
|
+
"confirm_always",
|
|
421
|
+
"offline"
|
|
422
|
+
]
|
|
423
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milpa/comal",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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,7 @@ 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 { PermissionDesk, confirmWithDevice } from './remote-permissions.mjs';
|
|
8
11
|
import { remoteCommandHelp, isRemoteCommand } from './remote-commands.mjs';
|
|
9
12
|
import { codeRunDefinition } from './code-run.mjs';
|
|
10
13
|
import {installCodeRunSession} from './code-run-session.mjs';
|
|
@@ -53,6 +56,7 @@ import {GitIntegrationOperations} from './git-integration-operations.mjs';
|
|
|
53
56
|
import {prepareGitPageCliBridge,assertGitPageCliEntry} from './git-page-cli-bridge.mjs';
|
|
54
57
|
import { registerGitSourceOperations } from './git-source.mjs';
|
|
55
58
|
import { projectRemoteWorkspace } from './remote-workspace.mjs';
|
|
59
|
+
import { buildSessionStats, readContext, statsFromRows } from './remote-stats.mjs';
|
|
56
60
|
import { evidenceOperations } from './work-state.mjs';
|
|
57
61
|
import { auditExecution, auditReport } from './audit.mjs';
|
|
58
62
|
import { comalEnvironment, defaultStateDir } from './compat.mjs';
|
|
@@ -371,10 +375,12 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
371
375
|
const interactive = !print;
|
|
372
376
|
const color = (code, text) => stdout.isTTY && !process.env.NO_COLOR ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
373
377
|
let remoteHost;
|
|
374
|
-
|
|
378
|
+
// One desk per process: a permission card answered from the paired M5Stack races the terminal card.
|
|
379
|
+
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() }) });
|
|
380
|
+
let controller, authController, modelController, scanController, runtime, peerBus, listenTimer, listenFinish, swarmFeed, relayFollower, textOpen = false, lastGoalDisplay = '';
|
|
375
381
|
const peerGuard=new PeerTurnGuard({cwd,resolveWrite:file=>tools.resolve(file,{create:true}),effects:name=>house.registry.get(name)?.contract?.effects});
|
|
376
382
|
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(); });
|
|
383
|
+
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
384
|
// Private files named by flags join the default private locations: no /add-dir may cover them.
|
|
379
385
|
const flagPrivate = [v['state-dir'], v['providers-config'], v['plugin-config']].filter(Boolean).map(item => path.resolve(item));
|
|
380
386
|
const policy = new PermissionPolicy({ mode: permissionMode, dangerouslySkipPermissions: skipPermissions, allowed: csv(v['allowed-tools']), denied: csv(v['disallowed-tools']), cwd, readRoots, privateLocations: flagPrivate,
|
|
@@ -382,7 +388,8 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
382
388
|
confirm: interactive ? async (name, input, options = {}) => {
|
|
383
389
|
const signals = [options.signal, controller?.signal || startupSignal].filter(Boolean), signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
|
|
384
390
|
const card = options.summary === undefined ? await house.confirmSummary(name, input, { signal }) : null;
|
|
385
|
-
|
|
391
|
+
const summary = options.summary ?? card?.summary, terminal = shown => ui.confirm(name, input, shown, { summary, edit: card?.edit, remember: options.remember });
|
|
392
|
+
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
393
|
} : undefined });
|
|
387
394
|
// /add-dir rows are the human's decisions in this session's book (under .comal, which the model cannot write);
|
|
388
395
|
// on resume they open again if the directory still qualifies, otherwise one line says why.
|
|
@@ -400,7 +407,10 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
400
407
|
}
|
|
401
408
|
};
|
|
402
409
|
const recordLaunchPermissions = () => store.append(session.id, { type: 'mode', source: 'human_cli', mode: policy.mode, dangerously_skip_permissions: skipPermissions, scope: 'process_launch' });
|
|
410
|
+
// Model calls of the current work and their limit, as model_progress reports them (session.stats).
|
|
411
|
+
let statsCalls = null;
|
|
403
412
|
const onEvent = event => {
|
|
413
|
+
if (event.type === 'model_progress') statsCalls = { calls: event.turn, call_limit: event.maxTurns };
|
|
404
414
|
remoteHost?.observe(event);
|
|
405
415
|
if (ui) { ui.handleEvent(event); return; }
|
|
406
416
|
if (format === 'stream-json') stdout.write(JSON.stringify(event) + '\n');
|
|
@@ -438,7 +448,7 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
438
448
|
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
449
|
}
|
|
440
450
|
};
|
|
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; } });
|
|
451
|
+
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
452
|
if (v.codemode) house.registry.add(codeRunDefinition);
|
|
443
453
|
house.guard('peer-authority',view=>peerGuard.check(view),{revision:'peer-turn-v1',configuration_digest:digest({cwd})});
|
|
444
454
|
const onPermission = decision => store.append(session.id, { type: 'permission', ...decision });
|
|
@@ -610,7 +620,7 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
610
620
|
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
621
|
const openRemoteControl=async()=>{
|
|
612
622
|
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});
|
|
623
|
+
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,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
624
|
const connection = new AbortController(); authController = connection;
|
|
615
625
|
const cancel = () => remoteHost.stop().catch(() => {});
|
|
616
626
|
connection.signal.addEventListener('abort', cancel, { once: true });
|
|
@@ -798,7 +808,7 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
798
808
|
if (ui && error.usage) ui.handleEvent({ type: 'turn_usage', usage: error.usage });
|
|
799
809
|
if (ui) ui.notice(error.message, true); else stderr.write(`\n${color('31', 'Error')}: ${clean(error.message)}\n`);
|
|
800
810
|
return 1;
|
|
801
|
-
} finally { await remoteHost?.endTurn(); controller = null; peerGuard.reset();await peerBus.setState('idle'); }
|
|
811
|
+
} finally { statsCalls = null; await remoteHost?.endTurn(); controller = null; peerGuard.reset();await peerBus.setState('idle'); }
|
|
802
812
|
};
|
|
803
813
|
const peerProbe = memoizedPeerProbe({
|
|
804
814
|
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 +825,7 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
815
825
|
if(format==='json')stdout.write(JSON.stringify(result)+'\n');
|
|
816
826
|
return result.is_error?1:0;
|
|
817
827
|
} 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');}
|
|
828
|
+
finally{statsCalls=null;await remoteHost?.endTurn();controller=null;peerGuard.reset();await peerBus.setState('idle');}
|
|
819
829
|
};
|
|
820
830
|
const directTool = async (name, input, text, userIntent) => {
|
|
821
831
|
if (name === 'Agent' && v.demo) throw new Error(t("Subagents require a real model; --demo is a script."));
|
|
@@ -888,7 +898,17 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
888
898
|
const format = { '.csv': 'csv', '.tsv': 'tsv', '.json': 'json', '.md': 'markdown' }[path.extname(file_path).toLowerCase()];
|
|
889
899
|
if (!format) throw new Error(t("Use a .csv, .tsv, .json, or .md file.")); return JSON.stringify({ text, format });
|
|
890
900
|
} });
|
|
891
|
-
const changeMode = mode => { if (skipPermissions) throw new Error(t(BYPASS_LOCKED_MESSAGE));
|
|
901
|
+
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; };
|
|
902
|
+
// session.stats for /rc: the same sources /status, /context and the status bar read; buildSessionStats keeps only numbers, enums and bounded labels.
|
|
903
|
+
const sessionStats = async () => { try {
|
|
904
|
+
const current = await store.load(session.id), recorded = statsFromRows(current.rows), rc = remoteHost?.status() || {};
|
|
905
|
+
let work = null; try { work = projectRemoteWorkspace(current, house.stateContext()); } catch {}
|
|
906
|
+
const calls = statsCalls || (recorded.usage?.calls !== undefined ? { calls: recorded.usage.calls, call_limit: agent.workMaxCalls } : {});
|
|
907
|
+
return buildSessionStats({ metadata: metadata(), promptCaching: typeof provider?.promptCaching === 'boolean' ? provider.promptCaching : null,
|
|
908
|
+
...readContext({ compactor: agent.compactor, system, tools: agent.tools.definitions(), maxTokens: requestProvider.maxTokens, session: current }),
|
|
909
|
+
lastCompaction: recorded.lastCompaction, usage: recorded.usage ? { ...recorded.usage, ...calls } : null, timing: recorded.timing, work,
|
|
910
|
+
permissions: rc.permission_answers || null, transport: rc.transport, addDirCount: policy.workRoots.length, cwd });
|
|
911
|
+
} catch { return null; } };
|
|
892
912
|
let discoveredModels = [], discoveredModelKey = '', discoveredSessions = [], discoveredPeers = [];
|
|
893
913
|
let authStatus={};
|
|
894
914
|
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 +975,7 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
955
975
|
validateRemote: item => remoteHost?.validate(item) ?? Promise.reject(Error('Remote host detached.')),
|
|
956
976
|
remoteTurnMatches: id => !remoteHost?.stopped && remoteHost?.turnId===id,
|
|
957
977
|
detachRemote: () => remoteHost?.stop(),
|
|
958
|
-
metadata, discovery, execute: run, interrupt: () => {controller?.abort();authController?.abort();modelController?.abort();}, cycleMode: () => changeMode(nextPermissionMode(policy.mode)),
|
|
978
|
+
metadata, discovery, execute: run, interrupt: () => {controller?.abort();authController?.abort();modelController?.abort();scanController?.abort();}, cycleMode: () => changeMode(nextPermissionMode(policy.mode)),
|
|
959
979
|
hasPeerMessages:peerProbe,executePeer:runPeer,
|
|
960
980
|
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
981
|
goalControl: argument => runGoal(argument),
|
|
@@ -1012,7 +1032,28 @@ async function boot(args, createScreen, setInterrupt, startupSignal, host) {
|
|
|
1012
1032
|
await relayFlow.record({action:'remote.arm',until:new Date(until).toISOString(),armed:true});
|
|
1013
1033
|
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
1034
|
}
|
|
1015
|
-
|
|
1035
|
+
// /rc device CODE · /rc device · /rc device forget: empareja el M5Stack con esta sesión de /rc.
|
|
1036
|
+
const deviceCommand=parseRcDevice(action);
|
|
1037
|
+
const bookRecord=row=>store.append(session.id,{...row,source:interactive?'human_tui':'human_cli',at:new Date().toISOString()});
|
|
1038
|
+
// /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.
|
|
1039
|
+
if(deviceCommand?.kind==='scan'){
|
|
1040
|
+
const scan=async({camera})=>{
|
|
1041
|
+
scanController=new AbortController();
|
|
1042
|
+
const signal=controller?.signal?AbortSignal.any([controller.signal,scanController.signal]):scanController.signal;
|
|
1043
|
+
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});
|
|
1044
|
+
ui.callbacks.onSetupCancel=()=>scanController?.abort();
|
|
1045
|
+
try{return await scanPairingQr({camera,signal});}
|
|
1046
|
+
finally{scanController=null;delete ui.callbacks.onSetupCancel;ui.clearSetup();}
|
|
1047
|
+
};
|
|
1048
|
+
const confirm=interactive&&ui?async({code})=>await ui.setupChoice({...deviceCodeChoice(code,t),signal:startupSignal})==='pair':undefined;
|
|
1049
|
+
try{return await runRcDeviceScan(deviceCommand,remoteHost,t,{record:bookRecord,scan,confirm});}
|
|
1050
|
+
catch(error){if(error.name==='AbortError')return error.message;throw error;}
|
|
1051
|
+
}
|
|
1052
|
+
if(deviceCommand)return runRcDevice(deviceCommand,remoteHost,t,{record:bookRecord});
|
|
1053
|
+
// /rc permissions [on [30m|2h]|off]: el aparato emparejado responde tarjetas de permisos, sólo mientras esté armado.
|
|
1054
|
+
const permissionsCommand=parseRcPermissions(action);
|
|
1055
|
+
if(permissionsCommand)return runRcPermissions(permissionsCommand,remoteHost,t,{record:bookRecord});
|
|
1056
|
+
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
1057
|
if(action==='stop')return humanTools.execute('RemoteControlClose',{});
|
|
1017
1058
|
if(action==='status')return JSON.stringify(remoteHost?.status() || {enabled:false},null,2);
|
|
1018
1059
|
if(!await cloudClient.credentials()) {
|