@tikoci/centrs 0.1.2 → 0.1.3
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/CHANGELOG.md +59 -0
- package/GLOSSARY.txt +71 -0
- package/README.md +24 -8
- package/SECURITY.md +3 -3
- package/commands/api/README.md +27 -9
- package/commands/api/examples.md +109 -6
- package/commands/check/README.md +371 -53
- package/commands/check/examples.md +390 -0
- package/commands/devices/README.md +177 -37
- package/commands/devices/examples.md +148 -0
- package/commands/execute/README.md +39 -4
- package/commands/execute/examples.md +90 -6
- package/commands/retrieve/README.md +36 -50
- package/commands/retrieve/examples.md +40 -9
- package/commands/settings/README.md +286 -22
- package/commands/settings/examples.md +408 -0
- package/commands/terminal/README.md +6 -0
- package/commands/transfer/README.md +32 -2
- package/commands/transfer/examples.md +66 -0
- package/docs/CONSTITUTION.md +36 -11
- package/docs/MATRIX.md +68 -172
- package/docs/errors/README.md +2 -1
- package/docs/errors/cdb/empty-selection.md +19 -0
- package/docs/errors/input/incomplete-gps.md +9 -0
- package/docs/errors/input/invalid-altitude.md +9 -0
- package/docs/errors/input/invalid-bbox.md +9 -0
- package/docs/errors/input/invalid-coordinate.md +9 -0
- package/docs/errors/input/invalid-radius.md +9 -0
- package/docs/errors/internal/settings-failed.md +9 -0
- package/docs/errors/settings/consequential-value.md +26 -0
- package/docs/errors/settings/invalid-boolean.md +21 -3
- package/docs/errors/settings/invalid-format.md +18 -3
- package/docs/errors/settings/invalid-integer.md +18 -3
- package/docs/errors/settings/invalid-timeout.md +20 -3
- package/docs/errors/settings/invalid-via.md +22 -3
- package/docs/errors/settings/reserved-key.md +29 -0
- package/docs/errors/settings/skip-env-file-active.md +25 -0
- package/docs/errors/settings/unknown-key.md +25 -0
- package/docs/errors/usage/fanout-not-supported.md +16 -0
- package/package.json +6 -6
- package/src/api-fanout.ts +454 -0
- package/src/api.ts +91 -19
- package/src/cli/api.ts +135 -11
- package/src/cli/btest.ts +20 -2
- package/src/cli/common.ts +11 -0
- package/src/cli/devices.ts +235 -6
- package/src/cli/discover.ts +3 -1
- package/src/cli/execute.ts +107 -6
- package/src/cli/mcp.ts +8 -1
- package/src/cli/retrieve.ts +135 -90
- package/src/cli/selection.ts +200 -0
- package/src/cli/settings.ts +286 -0
- package/src/cli/terminal.ts +22 -7
- package/src/cli/transfer.ts +171 -47
- package/src/cli.ts +7 -0
- package/src/core/error-catalog.ts +55 -0
- package/src/core/fanout.ts +143 -2
- package/src/devices.ts +212 -20
- package/src/discover.ts +7 -2
- package/src/execute-fanout.ts +438 -0
- package/src/execute.ts +120 -23
- package/src/index.ts +105 -1
- package/src/mcp/config.ts +12 -2
- package/src/mcp/tools.ts +137 -2
- package/src/protocols/adapter.ts +38 -18
- package/src/protocols/index.ts +11 -5
- package/src/resolver/cdb.ts +10 -2
- package/src/resolver/comment-kv.ts +58 -4
- package/src/resolver/config-file.ts +136 -0
- package/src/resolver/facts.ts +78 -0
- package/src/resolver/geo.ts +453 -0
- package/src/resolver/index.ts +46 -0
- package/src/resolver/mac.ts +12 -3
- package/src/resolver/selection.ts +304 -0
- package/src/resolver/settings.ts +54 -5
- package/src/resolver/target.ts +20 -2
- package/src/retrieve-fanout.ts +225 -133
- package/src/retrieve.ts +39 -7
- package/src/settings.ts +1078 -0
- package/src/terminal.ts +11 -1
- package/src/transfer-fanout.ts +527 -0
- package/src/transfer.ts +61 -11
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
apiListen,
|
|
18
18
|
apiMethods,
|
|
19
19
|
apiOutputFormats,
|
|
20
|
+
apiRequestSummaryFromRequest,
|
|
20
21
|
buildApiBody,
|
|
21
22
|
buildApiErrorEnvelope,
|
|
22
23
|
buildApiErrorEnvelopeFromResolved,
|
|
@@ -26,6 +27,7 @@ export {
|
|
|
26
27
|
mapMethodToVerb,
|
|
27
28
|
type NormalizedApiEndpoint,
|
|
28
29
|
normalizeApiEndpoint,
|
|
30
|
+
parseApiMethod,
|
|
29
31
|
type ResolvedApiRequest,
|
|
30
32
|
renderApiEnvelope,
|
|
31
33
|
renderApiStreamLine,
|
|
@@ -33,6 +35,18 @@ export {
|
|
|
33
35
|
runResolvedApi,
|
|
34
36
|
validateApiRequestShape,
|
|
35
37
|
} from "./api.ts";
|
|
38
|
+
export {
|
|
39
|
+
type ApiFanoutData,
|
|
40
|
+
type ApiFanoutEnvelope,
|
|
41
|
+
type ApiFanoutErrorEnvelope,
|
|
42
|
+
type ApiFanoutInternals,
|
|
43
|
+
type ApiFanoutOperationMeta,
|
|
44
|
+
type ApiFanoutOptions,
|
|
45
|
+
type ApiSelectionSummary,
|
|
46
|
+
apiFanout,
|
|
47
|
+
buildApiFanoutErrorEnvelope,
|
|
48
|
+
renderApiFanoutEnvelope,
|
|
49
|
+
} from "./api-fanout.ts";
|
|
36
50
|
export {
|
|
37
51
|
BTEST_CLIENT_CSV_HEADER,
|
|
38
52
|
BTEST_SERVER_CSV_HEADER,
|
|
@@ -75,13 +89,18 @@ export {
|
|
|
75
89
|
type Warning,
|
|
76
90
|
} from "./core/envelope.ts";
|
|
77
91
|
export {
|
|
92
|
+
buildFanoutEnvelope,
|
|
93
|
+
buildFanoutResolveFailure,
|
|
94
|
+
commonVia,
|
|
78
95
|
FANOUT_BACKOFF_BASE_MS,
|
|
79
96
|
FANOUT_CONCURRENCY_DEFAULTS,
|
|
80
97
|
FANOUT_MAX_RETRIES,
|
|
81
98
|
FANOUT_RETRYABLE_CODES,
|
|
99
|
+
fanoutExitCode,
|
|
82
100
|
isRetryableFanoutError,
|
|
83
101
|
resolveFanoutConcurrency,
|
|
84
102
|
runBoundedPool,
|
|
103
|
+
runFanout,
|
|
85
104
|
runWithRetry,
|
|
86
105
|
summarizeFanout,
|
|
87
106
|
} from "./core/fanout.ts";
|
|
@@ -173,6 +192,7 @@ export {
|
|
|
173
192
|
type DevicesShowEnvelopeData,
|
|
174
193
|
type DevicesShowItem,
|
|
175
194
|
type DevicesWarning,
|
|
195
|
+
type DevicesWhereClause,
|
|
176
196
|
defaultCdbPath,
|
|
177
197
|
devicesOutputFormats,
|
|
178
198
|
editInteractiveOnlyError,
|
|
@@ -228,6 +248,7 @@ export {
|
|
|
228
248
|
canonicalizeExecuteCommand,
|
|
229
249
|
type ExecuteEnvelope,
|
|
230
250
|
type ExecuteErrorEnvelope,
|
|
251
|
+
type ExecuteGlobalContext,
|
|
231
252
|
type ExecuteOperationMeta,
|
|
232
253
|
type ExecuteOutputFormat,
|
|
233
254
|
type ExecuteRequest,
|
|
@@ -238,10 +259,23 @@ export {
|
|
|
238
259
|
executeOutputFormats,
|
|
239
260
|
isWriteShaped,
|
|
240
261
|
renderExecuteEnvelope,
|
|
262
|
+
resolveExecuteGlobalContext,
|
|
241
263
|
resolveExecuteRequest,
|
|
242
264
|
runResolvedExecute,
|
|
243
265
|
validateExecuteRequestShape,
|
|
244
266
|
} from "./execute.ts";
|
|
267
|
+
export {
|
|
268
|
+
buildExecuteFanoutErrorEnvelope,
|
|
269
|
+
type ExecuteFanoutData,
|
|
270
|
+
type ExecuteFanoutEnvelope,
|
|
271
|
+
type ExecuteFanoutErrorEnvelope,
|
|
272
|
+
type ExecuteFanoutInternals,
|
|
273
|
+
type ExecuteFanoutOperationMeta,
|
|
274
|
+
type ExecuteFanoutOptions,
|
|
275
|
+
type ExecuteSelectionSummary,
|
|
276
|
+
executeFanout,
|
|
277
|
+
renderExecuteFanoutEnvelope,
|
|
278
|
+
} from "./execute-fanout.ts";
|
|
245
279
|
export {
|
|
246
280
|
type ApiVerb,
|
|
247
281
|
createProtocolAdapter,
|
|
@@ -262,27 +296,48 @@ export {
|
|
|
262
296
|
type RouterOsProtocol,
|
|
263
297
|
} from "./protocols/index.ts";
|
|
264
298
|
export {
|
|
299
|
+
type AltitudeType,
|
|
265
300
|
applyCommentKv,
|
|
301
|
+
type BboxPredicate,
|
|
266
302
|
type CdbGroupExpansion,
|
|
267
303
|
type CdbGroupResolveInput,
|
|
268
304
|
type CdbGroupTarget,
|
|
269
305
|
type CdbResolution,
|
|
270
306
|
type CdbResolveInput,
|
|
307
|
+
type CdbSelectionExpansion,
|
|
308
|
+
type CdbSelectionMember,
|
|
309
|
+
type CdbSelectionResolveInput,
|
|
310
|
+
type CommentKvGeoKey,
|
|
271
311
|
type CommentKvKey,
|
|
272
312
|
type CommentKvLayer,
|
|
273
313
|
type CommentKvOverrides,
|
|
274
314
|
type CommentKvResult,
|
|
275
315
|
type CommentKvUpdate,
|
|
276
316
|
type CommentKvWarning,
|
|
317
|
+
canonicalizeGeoKey,
|
|
277
318
|
coerceCommentKv,
|
|
278
319
|
commentKvAllowlist,
|
|
320
|
+
commentKvGeoKeys,
|
|
279
321
|
commentKvReservedKeys,
|
|
322
|
+
type DeviceLocation,
|
|
280
323
|
defaultPortForScheme,
|
|
324
|
+
deviceLocation,
|
|
281
325
|
expandCdbGroup,
|
|
326
|
+
expandCdbSelection,
|
|
327
|
+
isDefaultRecordTarget,
|
|
328
|
+
type NearPredicate,
|
|
329
|
+
type ParsedGpsTuple,
|
|
330
|
+
parseAltitude,
|
|
331
|
+
parseAltitudeType,
|
|
332
|
+
parseBbox,
|
|
282
333
|
parseBoolean,
|
|
283
334
|
parseCommentKv,
|
|
284
335
|
parseDuration,
|
|
336
|
+
parseGpsTuple,
|
|
285
337
|
parseHostCandidate,
|
|
338
|
+
parseLatLon,
|
|
339
|
+
parseNear,
|
|
340
|
+
parseRawCommentFacts,
|
|
286
341
|
type ResolvedAuth,
|
|
287
342
|
type ResolvedSetting,
|
|
288
343
|
type ResolvedTarget,
|
|
@@ -296,7 +351,9 @@ export {
|
|
|
296
351
|
resolveOptionalIntegerSetting,
|
|
297
352
|
resolveStringSetting,
|
|
298
353
|
resolveTarget,
|
|
354
|
+
type SelectionWhereClause,
|
|
299
355
|
type TargetResolveInput,
|
|
356
|
+
type TargetSelection,
|
|
300
357
|
toCoreSource,
|
|
301
358
|
} from "./resolver/index.ts";
|
|
302
359
|
export {
|
|
@@ -326,11 +383,44 @@ export {
|
|
|
326
383
|
type RetrieveFanoutData,
|
|
327
384
|
type RetrieveFanoutEnvelope,
|
|
328
385
|
type RetrieveFanoutErrorEnvelope,
|
|
386
|
+
type RetrieveFanoutInternals,
|
|
329
387
|
type RetrieveFanoutOperationMeta,
|
|
330
|
-
type
|
|
388
|
+
type RetrieveFanoutOptions,
|
|
389
|
+
type RetrieveSelectionSummary,
|
|
331
390
|
renderRetrieveFanoutEnvelope,
|
|
391
|
+
retrieveFanout,
|
|
332
392
|
retrieveGroup,
|
|
333
393
|
} from "./retrieve-fanout.ts";
|
|
394
|
+
export {
|
|
395
|
+
buildSettingsErrorEnvelope,
|
|
396
|
+
renderSettingsEnvelope,
|
|
397
|
+
type SettingsCommand,
|
|
398
|
+
type SettingsDefaultDeviceSummary,
|
|
399
|
+
type SettingsEnvelope,
|
|
400
|
+
type SettingsErrorEnvelope,
|
|
401
|
+
type SettingsGetArgs,
|
|
402
|
+
type SettingsGetData,
|
|
403
|
+
type SettingsKeyDef,
|
|
404
|
+
type SettingsKeyEntry,
|
|
405
|
+
type SettingsOperationMeta,
|
|
406
|
+
type SettingsOutputFormat,
|
|
407
|
+
type SettingsPrintArgs,
|
|
408
|
+
type SettingsPrintData,
|
|
409
|
+
type SettingsResetArgs,
|
|
410
|
+
type SettingsResetData,
|
|
411
|
+
type SettingsSetArgs,
|
|
412
|
+
type SettingsSetData,
|
|
413
|
+
type SettingsSuccessEnvelope,
|
|
414
|
+
type SettingsUnrecognizedEntry,
|
|
415
|
+
type SettingsValue,
|
|
416
|
+
settingsGet,
|
|
417
|
+
settingsManagedKeys,
|
|
418
|
+
settingsOutputFormats,
|
|
419
|
+
settingsPrint,
|
|
420
|
+
settingsRefusedKeys,
|
|
421
|
+
settingsReset,
|
|
422
|
+
settingsSet,
|
|
423
|
+
} from "./settings.ts";
|
|
334
424
|
export {
|
|
335
425
|
buildSshTerminalArgv,
|
|
336
426
|
buildTerminalErrorEnvelope,
|
|
@@ -343,6 +433,7 @@ export {
|
|
|
343
433
|
export {
|
|
344
434
|
buildTransferErrorEnvelope,
|
|
345
435
|
FILE_READ_CHUNK_BYTES,
|
|
436
|
+
isTransferWriteVerb,
|
|
346
437
|
normalizeRemotePath,
|
|
347
438
|
REST_FILE_WRITE_CAP_BYTES,
|
|
348
439
|
renderTransferEnvelope,
|
|
@@ -365,6 +456,19 @@ export {
|
|
|
365
456
|
transferVerbs,
|
|
366
457
|
validateTransferRequestShape,
|
|
367
458
|
} from "./transfer.ts";
|
|
459
|
+
export {
|
|
460
|
+
buildTransferFanoutErrorEnvelope,
|
|
461
|
+
renderTransferFanoutEnvelope,
|
|
462
|
+
type TransferFanoutData,
|
|
463
|
+
type TransferFanoutEnvelope,
|
|
464
|
+
type TransferFanoutErrorEnvelope,
|
|
465
|
+
type TransferFanoutInternals,
|
|
466
|
+
type TransferFanoutOperationMeta,
|
|
467
|
+
type TransferFanoutOptions,
|
|
468
|
+
type TransferFanoutRequestSummary,
|
|
469
|
+
type TransferSelectionSummary,
|
|
470
|
+
transferFanout,
|
|
471
|
+
} from "./transfer-fanout.ts";
|
|
368
472
|
|
|
369
473
|
import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
|
|
370
474
|
|
package/src/mcp/config.ts
CHANGED
|
@@ -46,16 +46,26 @@ export interface ResolveMcpConfigArgs {
|
|
|
46
46
|
cdbPassword?: string;
|
|
47
47
|
allowAdhocTargets?: boolean;
|
|
48
48
|
env?: Record<string, string | undefined>;
|
|
49
|
+
/**
|
|
50
|
+
* The `centrs.env` config tier (`src/resolver/config-file.ts`), loaded by
|
|
51
|
+
* the caller — `resolveMcpConfig` itself stays synchronous so it can keep
|
|
52
|
+
* serving as a default-parameter value (`server.ts`). `CENTRS_CDB_PASSWORD`
|
|
53
|
+
* deliberately never reads it, same as `resolveAuth`'s credential fields.
|
|
54
|
+
*/
|
|
55
|
+
config?: Record<string, string | undefined>;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
export function resolveMcpConfig(
|
|
52
59
|
args: ResolveMcpConfigArgs = {},
|
|
53
60
|
): CentrsMcpConfig {
|
|
54
61
|
const env = args.env ?? Bun.env;
|
|
55
|
-
const
|
|
62
|
+
const config = args.config ?? {};
|
|
63
|
+
const explicitPath =
|
|
64
|
+
args.cdbFile ?? env[ENV_CDB_FILE] ?? config[ENV_CDB_FILE] ?? undefined;
|
|
56
65
|
const cdbFile = explicitPath ?? defaultCdbPath(env);
|
|
57
66
|
const allowAdhocTargets =
|
|
58
|
-
args.allowAdhocTargets ??
|
|
67
|
+
args.allowAdhocTargets ??
|
|
68
|
+
(env[ENV_ALLOW_ADHOC] ?? config[ENV_ALLOW_ADHOC]) === "1";
|
|
59
69
|
return {
|
|
60
70
|
cdbFile,
|
|
61
71
|
cdbFileExplicit: explicitPath !== undefined,
|
package/src/mcp/tools.ts
CHANGED
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
isWriteShaped,
|
|
34
34
|
validateExecuteEnvelope,
|
|
35
35
|
} from "../execute.ts";
|
|
36
|
+
import type { CommentKvUpdate } from "../resolver/comment-kv.ts";
|
|
37
|
+
import { parseBbox, parseNear } from "../resolver/geo.ts";
|
|
36
38
|
import { parseDuration } from "../resolver/settings.ts";
|
|
37
39
|
import { buildRetrieveErrorEnvelope, retrieve } from "../retrieve.ts";
|
|
38
40
|
import {
|
|
@@ -416,6 +418,24 @@ export const devicesInputShape = {
|
|
|
416
418
|
.describe(
|
|
417
419
|
"Group filter for op=list; first-class group field for op=add/set.",
|
|
418
420
|
),
|
|
421
|
+
where: z
|
|
422
|
+
.array(z.string())
|
|
423
|
+
.optional()
|
|
424
|
+
.describe(
|
|
425
|
+
"op=list device-class filters `attr=value` (repeatable, AND-combined; e.g. board=RB5009 or lat=37.7749).",
|
|
426
|
+
),
|
|
427
|
+
near: z
|
|
428
|
+
.string()
|
|
429
|
+
.optional()
|
|
430
|
+
.describe(
|
|
431
|
+
"op=list geo filter `<lat>,<lon>,<radius>` (lat-first; radius m/km/mi/ft, bare = km).",
|
|
432
|
+
),
|
|
433
|
+
bbox: z
|
|
434
|
+
.string()
|
|
435
|
+
.optional()
|
|
436
|
+
.describe(
|
|
437
|
+
"op=list geo filter `<south>,<west>,<north>,<east>` (lat-first bounding box).",
|
|
438
|
+
),
|
|
419
439
|
user: z.string().optional().describe("CDB username for op=add/set."),
|
|
420
440
|
password: z
|
|
421
441
|
.string()
|
|
@@ -454,6 +474,30 @@ export const devicesInputShape = {
|
|
|
454
474
|
.boolean()
|
|
455
475
|
.optional()
|
|
456
476
|
.describe("For op=set, reject comment keys outside the allowlist."),
|
|
477
|
+
lat: z
|
|
478
|
+
.number()
|
|
479
|
+
.optional()
|
|
480
|
+
.describe(
|
|
481
|
+
"Latitude in decimal degrees (-90..90) for op=add/set; a comment-kv fact, paired with lon (issue #146).",
|
|
482
|
+
),
|
|
483
|
+
lon: z
|
|
484
|
+
.number()
|
|
485
|
+
.optional()
|
|
486
|
+
.describe(
|
|
487
|
+
"Longitude in decimal degrees (-180..180) for op=add/set; a comment-kv fact, paired with lat.",
|
|
488
|
+
),
|
|
489
|
+
altitude: z
|
|
490
|
+
.number()
|
|
491
|
+
.optional()
|
|
492
|
+
.describe(
|
|
493
|
+
"Altitude in meters for op=add/set (may be negative); a comment-kv fact.",
|
|
494
|
+
),
|
|
495
|
+
altitudeType: z
|
|
496
|
+
.enum(["MSL", "AGL"])
|
|
497
|
+
.optional()
|
|
498
|
+
.describe(
|
|
499
|
+
"Vertical datum for altitude, op=add/set (default MSL when altitude is set).",
|
|
500
|
+
),
|
|
457
501
|
confirm: z
|
|
458
502
|
.boolean()
|
|
459
503
|
.optional()
|
|
@@ -478,6 +522,56 @@ function requireDevicesTarget(
|
|
|
478
522
|
});
|
|
479
523
|
}
|
|
480
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Lower the typed `lat`/`lon`/`altitude`/`altitudeType` MCP args into comment-kv
|
|
527
|
+
* updates — the same core surface the CLI's `--lat`/`--lon`/`--gps` flags lower
|
|
528
|
+
* into (`src/cli/devices.ts`). Light touch: no alias handling here (the MCP
|
|
529
|
+
* schema only exposes the canonical field names), range/enum/pairing
|
|
530
|
+
* validation happens once in `validateCommentKvUpdates` (`src/devices.ts`).
|
|
531
|
+
*/
|
|
532
|
+
function buildGeoUpdates(args: DevicesArgs): CommentKvUpdate[] {
|
|
533
|
+
const updates: CommentKvUpdate[] = [];
|
|
534
|
+
if (args.lat !== undefined) {
|
|
535
|
+
updates.push({ key: "lat", value: String(args.lat) });
|
|
536
|
+
}
|
|
537
|
+
if (args.lon !== undefined) {
|
|
538
|
+
updates.push({ key: "lon", value: String(args.lon) });
|
|
539
|
+
}
|
|
540
|
+
if (args.altitude !== undefined) {
|
|
541
|
+
updates.push({ key: "altitude", value: String(args.altitude) });
|
|
542
|
+
}
|
|
543
|
+
if (args.altitudeType !== undefined) {
|
|
544
|
+
updates.push({ key: "altitude-type", value: args.altitudeType });
|
|
545
|
+
}
|
|
546
|
+
return updates;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Parse the MCP `where` array (`["attr=value", …]`) into device-class clauses
|
|
551
|
+
* for op=list. Geo-alias canonicalization happens centrally in `matchesWhere`
|
|
552
|
+
* (`src/resolver/facts.ts`), so this only splits on the first `=`.
|
|
553
|
+
*/
|
|
554
|
+
function parseMcpWhereClauses(
|
|
555
|
+
where: readonly string[] | undefined,
|
|
556
|
+
): { key: string; value: string }[] | undefined {
|
|
557
|
+
if (where === undefined) {
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
return where.map((clause) => {
|
|
561
|
+
const eq = clause.indexOf("=");
|
|
562
|
+
if (eq <= 0) {
|
|
563
|
+
throw new CentrsError({
|
|
564
|
+
code: "input/invalid-command",
|
|
565
|
+
summary: `where clause must be \`attr=value\`; received: ${clause}`,
|
|
566
|
+
remediation:
|
|
567
|
+
"Pass device-class filters like `board=RB5009` or `lat=37.7749`.",
|
|
568
|
+
context: { where: clause },
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return { key: clause.slice(0, eq), value: clause.slice(eq + 1) };
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
481
575
|
function resolveRecordType(recordType: string | undefined): number | undefined {
|
|
482
576
|
if (recordType === undefined) {
|
|
483
577
|
return undefined;
|
|
@@ -502,9 +596,49 @@ export async function handleDevices(
|
|
|
502
596
|
): Promise<McpEnvelope> {
|
|
503
597
|
try {
|
|
504
598
|
const cdb = await loadAllowlistCdb(config);
|
|
599
|
+
// GPS *setter* fields write via add/set only; the *query* selectors
|
|
600
|
+
// (where/near/bbox) select via list only. Reject the wrong-op combination
|
|
601
|
+
// instead of silently dropping the caller's input (mirrors the CLI's
|
|
602
|
+
// per-subcommand flag guard in src/cli/devices.ts).
|
|
603
|
+
if (
|
|
604
|
+
args.op !== "add" &&
|
|
605
|
+
args.op !== "set" &&
|
|
606
|
+
(args.lat !== undefined ||
|
|
607
|
+
args.lon !== undefined ||
|
|
608
|
+
args.altitude !== undefined ||
|
|
609
|
+
args.altitudeType !== undefined)
|
|
610
|
+
) {
|
|
611
|
+
throw new CentrsError({
|
|
612
|
+
code: "input/invalid-command",
|
|
613
|
+
summary: `GPS fields (lat/lon/altitude/altitudeType) are not valid for op="${args.op}".`,
|
|
614
|
+
remediation:
|
|
615
|
+
"Store GPS with op=add or op=set; drop the GPS fields for list/show/groups/edit/remove.",
|
|
616
|
+
context: { op: args.op },
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
if (
|
|
620
|
+
args.op !== "list" &&
|
|
621
|
+
(args.where !== undefined ||
|
|
622
|
+
args.near !== undefined ||
|
|
623
|
+
args.bbox !== undefined)
|
|
624
|
+
) {
|
|
625
|
+
throw new CentrsError({
|
|
626
|
+
code: "input/invalid-command",
|
|
627
|
+
summary: `Query selectors (where/near/bbox) are only valid for op="list", not op="${args.op}".`,
|
|
628
|
+
remediation:
|
|
629
|
+
"Use op=list with where/near/bbox to query the registry; for op=show name a target directly.",
|
|
630
|
+
context: { op: args.op },
|
|
631
|
+
});
|
|
632
|
+
}
|
|
505
633
|
switch (args.op) {
|
|
506
634
|
case "list":
|
|
507
|
-
return listDevices({
|
|
635
|
+
return listDevices({
|
|
636
|
+
cdb,
|
|
637
|
+
group: args.group,
|
|
638
|
+
where: parseMcpWhereClauses(args.where),
|
|
639
|
+
near: args.near !== undefined ? parseNear(args.near) : undefined,
|
|
640
|
+
bbox: args.bbox !== undefined ? parseBbox(args.bbox) : undefined,
|
|
641
|
+
});
|
|
508
642
|
case "groups":
|
|
509
643
|
return listGroups({ cdb, withMembers: true });
|
|
510
644
|
case "show": {
|
|
@@ -527,6 +661,7 @@ export async function handleDevices(
|
|
|
527
661
|
profile: args.profile,
|
|
528
662
|
session: args.session,
|
|
529
663
|
comment: args.comment,
|
|
664
|
+
commentKvUpdates: buildGeoUpdates(args),
|
|
530
665
|
savedPassword: args.savedPassword,
|
|
531
666
|
force: args.force,
|
|
532
667
|
}),
|
|
@@ -542,7 +677,7 @@ export async function handleDevices(
|
|
|
542
677
|
await setDevice({
|
|
543
678
|
cdb,
|
|
544
679
|
target,
|
|
545
|
-
updates: args.updates,
|
|
680
|
+
updates: [...buildGeoUpdates(args), ...(args.updates ?? [])],
|
|
546
681
|
user: args.user,
|
|
547
682
|
password: args.password,
|
|
548
683
|
group: args.group,
|
package/src/protocols/adapter.ts
CHANGED
|
@@ -610,23 +610,39 @@ class NativeApiAdapter implements ProtocolAdapter {
|
|
|
610
610
|
async execute(
|
|
611
611
|
request: ProtocolExecuteRequest,
|
|
612
612
|
): Promise<ProtocolExecuteResult> {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
) {
|
|
613
|
+
if (request.script !== undefined) {
|
|
614
|
+
return normalizeNativeExecute(await this.executeScript(request.script));
|
|
615
|
+
}
|
|
616
|
+
const command: NativeApiCommand = {
|
|
617
|
+
command: `${request.path.replace(/\/$/, "")}/${request.command}`,
|
|
618
|
+
};
|
|
619
|
+
if (request.attributes && Object.keys(request.attributes).length > 0) {
|
|
621
620
|
command.attributes = request.attributes;
|
|
622
621
|
}
|
|
623
|
-
if (
|
|
622
|
+
if (request.queries && request.queries.length > 0) {
|
|
624
623
|
command.queries = request.queries;
|
|
625
624
|
}
|
|
626
625
|
const replies = await this.talk(command);
|
|
627
626
|
return { records: repliesToRecords(replies) };
|
|
628
627
|
}
|
|
629
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Run a script through native `/execute` with `as-string`, the sole shared
|
|
631
|
+
* path for both the CLI-shaped `execute` command and the `api` run+script
|
|
632
|
+
* verb: without `as-string` the native API schedules a background job and
|
|
633
|
+
* returns only its id rather than the captured output (CHR-grounded —
|
|
634
|
+
* mirrors REST `/rest/execute`).
|
|
635
|
+
*/
|
|
636
|
+
private async executeScript(
|
|
637
|
+
script: string,
|
|
638
|
+
): Promise<Record<string, string>[]> {
|
|
639
|
+
const replies = await this.talk({
|
|
640
|
+
command: "/execute",
|
|
641
|
+
attributes: { script, "as-string": "" },
|
|
642
|
+
});
|
|
643
|
+
return repliesToRecords(replies);
|
|
644
|
+
}
|
|
645
|
+
|
|
630
646
|
async apiRequest(request: ProtocolApiRequest): Promise<ProtocolApiResult> {
|
|
631
647
|
const base = request.path.replace(/\/$/, "");
|
|
632
648
|
switch (request.verb) {
|
|
@@ -682,15 +698,7 @@ class NativeApiAdapter implements ProtocolAdapter {
|
|
|
682
698
|
}
|
|
683
699
|
case "run": {
|
|
684
700
|
if (request.script !== undefined) {
|
|
685
|
-
|
|
686
|
-
// output; without it the native API schedules a job and returns its id
|
|
687
|
-
// (CHR-grounded — mirrors REST `/rest/execute`).
|
|
688
|
-
const records = repliesToRecords(
|
|
689
|
-
await this.talk({
|
|
690
|
-
command: "/execute",
|
|
691
|
-
attributes: { script: request.script, "as-string": "" },
|
|
692
|
-
}),
|
|
693
|
-
);
|
|
701
|
+
const records = await this.executeScript(request.script);
|
|
694
702
|
return { data: restStyleRunData(records) };
|
|
695
703
|
}
|
|
696
704
|
const command: NativeApiCommand = { command: base };
|
|
@@ -1089,6 +1097,18 @@ function exhaustiveApiVerb(verb: never): never {
|
|
|
1089
1097
|
throw new Error(`Unhandled api verb: ${String(verb)}`);
|
|
1090
1098
|
}
|
|
1091
1099
|
|
|
1100
|
+
/** Re-map an `as-string` native `/execute` reply into `ProtocolExecuteResult`,
|
|
1101
|
+
* mirroring `normalizeRestExecute`'s `ret`-vs-records split. */
|
|
1102
|
+
function normalizeNativeExecute(
|
|
1103
|
+
records: readonly Record<string, string>[],
|
|
1104
|
+
): ProtocolExecuteResult {
|
|
1105
|
+
const ret = records.length === 1 ? records[0]?.["ret"] : undefined;
|
|
1106
|
+
if (typeof ret === "string") {
|
|
1107
|
+
return { records: [], ret };
|
|
1108
|
+
}
|
|
1109
|
+
return { records: [...records] };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1092
1112
|
function normalizeRestExecute(data: unknown): ProtocolExecuteResult {
|
|
1093
1113
|
if (Array.isArray(data)) {
|
|
1094
1114
|
return { records: data.filter(isPlainObject) as Record<string, string>[] };
|
package/src/protocols/index.ts
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* Status per protocol is the implementation reality (`implemented` flag).
|
|
8
8
|
* Per-cell status across commands is in `docs/MATRIX.md`. Selection rules
|
|
9
9
|
* are in `docs/CONSTITUTION.md` (protocol selection).
|
|
10
|
+
*
|
|
11
|
+
* Contract with MATRIX (asserted by `test/unit/protocol-registry.test.ts`):
|
|
12
|
+
* every `CHR-passed` cell in the `docs/MATRIX.md` grid implies its protocol
|
|
13
|
+
* here is `implemented: true` and lists the matching capability. MATRIX stays
|
|
14
|
+
* the only status surface; this registry must never under-claim against it.
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
export type ProtocolCapability =
|
|
@@ -35,7 +40,7 @@ export const protocolPlans = [
|
|
|
35
40
|
},
|
|
36
41
|
{
|
|
37
42
|
id: "native-api",
|
|
38
|
-
capabilities: ["retrieve", "execute", "proxy"],
|
|
43
|
+
capabilities: ["retrieve", "execute", "transfer", "proxy"],
|
|
39
44
|
notes:
|
|
40
45
|
"Binary API (TCP 8728 / TLS 8729). Transport base implemented in native-api.ts (word/sentence codec, login, tagged talk); command wiring tracked in docs/MATRIX.md.",
|
|
41
46
|
implemented: true,
|
|
@@ -44,8 +49,8 @@ export const protocolPlans = [
|
|
|
44
49
|
id: "ssh",
|
|
45
50
|
capabilities: ["execute", "terminal", "transfer"],
|
|
46
51
|
notes:
|
|
47
|
-
"Reuses host ssh client.
|
|
48
|
-
implemented:
|
|
52
|
+
"Reuses host ssh client. execute/terminal/transfer all CHR-passed (docs/MATRIX.md); facts in ssh.ts, sftp.ts, terminal.ts.",
|
|
53
|
+
implemented: true,
|
|
49
54
|
},
|
|
50
55
|
{
|
|
51
56
|
id: "snmp",
|
|
@@ -56,8 +61,9 @@ export const protocolPlans = [
|
|
|
56
61
|
{
|
|
57
62
|
id: "mndp",
|
|
58
63
|
capabilities: ["discover"],
|
|
59
|
-
notes:
|
|
60
|
-
|
|
64
|
+
notes:
|
|
65
|
+
"Passive hint source only; not authoritative inventory. discover CHR-passed (docs/MATRIX.md); facts in src/data/mndp.ts.",
|
|
66
|
+
implemented: true,
|
|
61
67
|
},
|
|
62
68
|
{
|
|
63
69
|
id: "mac-telnet",
|
package/src/resolver/cdb.ts
CHANGED
|
@@ -153,6 +153,7 @@ function warningFromDevices(warning: DevicesWarning): ResolverWarning {
|
|
|
153
153
|
export async function resolveCdb(
|
|
154
154
|
input: CdbResolveInput,
|
|
155
155
|
env: Record<string, string | undefined>,
|
|
156
|
+
config: Record<string, string | undefined> = {},
|
|
156
157
|
): Promise<CdbResolution | undefined> {
|
|
157
158
|
if (!input.targetInput) {
|
|
158
159
|
return undefined;
|
|
@@ -162,12 +163,18 @@ export async function resolveCdb(
|
|
|
162
163
|
cdbFile: input.cdbFile,
|
|
163
164
|
cdbPassword: input.cdbPassword,
|
|
164
165
|
env,
|
|
166
|
+
config,
|
|
165
167
|
});
|
|
168
|
+
// A user-configured `centrs.env` default CDB path counts as explicit, same
|
|
169
|
+
// as an env var: it is an intentional choice, not the built-in fallback,
|
|
170
|
+
// so a missing file there should surface as a real error rather than
|
|
171
|
+
// silently degrading to "no CDB in play".
|
|
166
172
|
const explicitCdb =
|
|
167
173
|
input.cdbFile !== undefined ||
|
|
168
174
|
input.cdbPassword !== undefined ||
|
|
169
175
|
env[ENV_CDB_FILE] !== undefined ||
|
|
170
|
-
env[ENV_CDB_PASSWORD] !== undefined
|
|
176
|
+
env[ENV_CDB_PASSWORD] !== undefined ||
|
|
177
|
+
config[ENV_CDB_FILE] !== undefined;
|
|
171
178
|
if (!explicitCdb && !(await Bun.file(settings.cdbFile.value).exists())) {
|
|
172
179
|
return undefined;
|
|
173
180
|
}
|
|
@@ -178,6 +185,7 @@ export async function resolveCdb(
|
|
|
178
185
|
cdbFile: input.cdbFile,
|
|
179
186
|
cdbPassword: input.cdbPassword,
|
|
180
187
|
env,
|
|
188
|
+
config,
|
|
181
189
|
});
|
|
182
190
|
} catch (error) {
|
|
183
191
|
if (
|
|
@@ -312,7 +320,7 @@ export async function expandCdbGroup(
|
|
|
312
320
|
return { targets, warnings, empty: false };
|
|
313
321
|
}
|
|
314
322
|
|
|
315
|
-
function resolutionFromEntry(
|
|
323
|
+
export function resolutionFromEntry(
|
|
316
324
|
entry: WinBoxCdbEntry,
|
|
317
325
|
recordIndex: number,
|
|
318
326
|
): CdbResolution {
|