@json-to-office/mcp-server 1.5.0 → 1.8.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.
- package/README.md +29 -14
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +687 -96
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +268 -2
- package/dist/index.js +656 -94
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -338,8 +338,18 @@ declare function createOutputRoot(options?: OutputRootOptions): OutputRoot;
|
|
|
338
338
|
* failures (see `errors.ts`).
|
|
339
339
|
*/
|
|
340
340
|
|
|
341
|
+
/**
|
|
342
|
+
* A success may still have something to say.
|
|
343
|
+
*
|
|
344
|
+
* `warnings` is how a store reports what did not go wrong enough to fail the
|
|
345
|
+
* call — a revision that did not reach disk (#290) being the case that
|
|
346
|
+
* matters: the edit landed, the handle works, and the agent still needs to
|
|
347
|
+
* know it has stopped being recoverable. Tools fold these into the
|
|
348
|
+
* `diagnostics` of the result they were already returning.
|
|
349
|
+
*/
|
|
341
350
|
type WorkspaceResult<T> = ({
|
|
342
351
|
ok: true;
|
|
352
|
+
warnings?: Diagnostic[];
|
|
343
353
|
} & T) | Failure;
|
|
344
354
|
/** RFC 6902 operation. Paths are RFC 6901 pointers — no private dialect (#271). */
|
|
345
355
|
interface JsonPatchOperation {
|
|
@@ -363,6 +373,11 @@ interface WorkspaceRecord {
|
|
|
363
373
|
title?: string;
|
|
364
374
|
/** Revisions pinned by `snapshot`, still retrievable through `get`. */
|
|
365
375
|
pinnedRevisions: number[];
|
|
376
|
+
/**
|
|
377
|
+
* Whether this revision is on disk and so survives the connection (#290).
|
|
378
|
+
* Absent when the store has no durable backing at all, which is the default.
|
|
379
|
+
*/
|
|
380
|
+
persisted?: boolean;
|
|
366
381
|
}
|
|
367
382
|
interface WorkspaceStore {
|
|
368
383
|
/**
|
|
@@ -436,7 +451,9 @@ interface WorkspaceStore {
|
|
|
436
451
|
*
|
|
437
452
|
* A connection's store is reachable only through its `ToolDeps`, so the
|
|
438
453
|
* documents go when the connection's deps do; this is for a host that wants
|
|
439
|
-
* the memory back at a moment of its own choosing.
|
|
454
|
+
* the memory back at a moment of its own choosing. It releases memory only —
|
|
455
|
+
* a durable store keeps its copies, because an event that ends a connection
|
|
456
|
+
* is exactly what #290 stops being fatal.
|
|
440
457
|
*/
|
|
441
458
|
closeAll(): Promise<void>;
|
|
442
459
|
}
|
|
@@ -452,6 +469,140 @@ declare const unavailableWorkspaceStore: WorkspaceStore;
|
|
|
452
469
|
declare function setWorkspaceStore(store: WorkspaceStore | undefined): void;
|
|
453
470
|
declare function getWorkspaceStore(): WorkspaceStore;
|
|
454
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Disk backing for workspaces (#290).
|
|
474
|
+
*
|
|
475
|
+
* #271 made the document JSON authoritative and then kept it in exactly one
|
|
476
|
+
* place: server memory. Anything that ends a connection — a host session
|
|
477
|
+
* reset, a client restart, a crash — took every revision with it, and
|
|
478
|
+
* `jto_workspace_snapshot` did not help because the pins lived in the same
|
|
479
|
+
* `Map`. This module is the durable half: memory stays the fast path and the
|
|
480
|
+
* only thing tools read from, and every committed revision is mirrored here so
|
|
481
|
+
* a reconnecting client can resume instead of re-authoring.
|
|
482
|
+
*
|
|
483
|
+
* Off unless a root is configured (`--workspace-dir`, `JTO_MCP_WORKSPACE_DIR`),
|
|
484
|
+
* because turning handles durable turns them cross-connection, and a server
|
|
485
|
+
* that started leaving documents on a user's disk without being asked would be
|
|
486
|
+
* a surprise rather than a feature.
|
|
487
|
+
*
|
|
488
|
+
* Three properties the layout is chosen for:
|
|
489
|
+
*
|
|
490
|
+
* - Crash safety. A revision file is written and renamed into place BEFORE the
|
|
491
|
+
* metadata that names it, so `meta.json` never points at bytes that are not
|
|
492
|
+
* there. Interrupt this at any point and what is on disk is either the old
|
|
493
|
+
* revision or the new one, never half of either.
|
|
494
|
+
* - Cheap listing. Recovering handles after a reset reads one small
|
|
495
|
+
* `meta.json` per workspace; the documents are only paid for when a handle
|
|
496
|
+
* is actually used.
|
|
497
|
+
* - No caller-named paths. A handle is the only thing that reaches the
|
|
498
|
+
* filesystem, and it is matched against a whitelist first, so nothing a
|
|
499
|
+
* client sends can address a directory of its choosing.
|
|
500
|
+
*
|
|
501
|
+
* Nothing here is a tool result, so these functions throw rather than return
|
|
502
|
+
* diagnostics; the store catches and downgrades a write failure to a warning
|
|
503
|
+
* (`W_WORKSPACE_NOT_PERSISTED`), because losing durability is worth saying out
|
|
504
|
+
* loud and is never worth failing an edit over.
|
|
505
|
+
*
|
|
506
|
+
* Not a concurrency layer. Individual writes are atomic, so a root two
|
|
507
|
+
* connections share never tears — but each holds its own memory copy of an
|
|
508
|
+
* entry, and the second to commit a revision is the one on disk. The intended
|
|
509
|
+
* arrangement is one root per client, which is what a reconnect is.
|
|
510
|
+
*/
|
|
511
|
+
|
|
512
|
+
/** Env var that names the workspace root, below the `--workspace-dir` flag. */
|
|
513
|
+
declare const WORKSPACE_DIR_ENV = "JTO_MCP_WORKSPACE_DIR";
|
|
514
|
+
interface PersistenceLimits {
|
|
515
|
+
/** Workspaces kept on disk; the least recently updated go first. */
|
|
516
|
+
maxWorkspaces: number;
|
|
517
|
+
/** Revision files per workspace — the head plus its pinned revisions. */
|
|
518
|
+
maxRevisionsPerWorkspace: number;
|
|
519
|
+
/** Ceiling for a single revision file. */
|
|
520
|
+
maxEntryBytes: number;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Roomier than the memory limits on purpose.
|
|
524
|
+
*
|
|
525
|
+
* Memory bounds protect a process; these bound a directory that outlives it,
|
|
526
|
+
* where the cost of keeping a document is a few hundred kilobytes of disk and
|
|
527
|
+
* the cost of dropping one is authoring an agent has to redo. `maxWorkspaces`
|
|
528
|
+
* is twice the memory ceiling so a reconnecting client still finds what the
|
|
529
|
+
* previous connection had open, and the revision cap is the head plus the
|
|
530
|
+
* default eight pins, so a default setup never has to discard a pin it took.
|
|
531
|
+
*/
|
|
532
|
+
declare const DEFAULT_PERSISTENCE_LIMITS: PersistenceLimits;
|
|
533
|
+
/** One retained revision: the serialized document and its size. */
|
|
534
|
+
interface PersistedDocument {
|
|
535
|
+
revision: number;
|
|
536
|
+
text: string;
|
|
537
|
+
bytes: number;
|
|
538
|
+
}
|
|
539
|
+
/** What the store hands over to be made durable. */
|
|
540
|
+
interface PersistedSnapshot {
|
|
541
|
+
handle: string;
|
|
542
|
+
format: FormatName;
|
|
543
|
+
createdAt: number;
|
|
544
|
+
updatedAt: number;
|
|
545
|
+
title?: string;
|
|
546
|
+
/** The current revision. */
|
|
547
|
+
head: PersistedDocument;
|
|
548
|
+
/** Revisions pinned by `snapshot`, oldest first. */
|
|
549
|
+
pins: readonly PersistedDocument[];
|
|
550
|
+
}
|
|
551
|
+
/** A workspace as `list` reports it, without paying for the documents. */
|
|
552
|
+
interface PersistedMeta {
|
|
553
|
+
handle: string;
|
|
554
|
+
format: FormatName;
|
|
555
|
+
revision: number;
|
|
556
|
+
bytes: number;
|
|
557
|
+
createdAt: number;
|
|
558
|
+
updatedAt: number;
|
|
559
|
+
title?: string;
|
|
560
|
+
pinnedRevisions: number[];
|
|
561
|
+
}
|
|
562
|
+
/** A workspace read back whole, ready to become a live entry again. */
|
|
563
|
+
interface RestoredWorkspace extends PersistedMeta {
|
|
564
|
+
head: PersistedDocument;
|
|
565
|
+
pins: PersistedDocument[];
|
|
566
|
+
}
|
|
567
|
+
interface WorkspacePersistence {
|
|
568
|
+
/** Absolute path of the root. May not exist on disk until first write. */
|
|
569
|
+
readonly root: string;
|
|
570
|
+
readonly limits: PersistenceLimits;
|
|
571
|
+
/** Mirror a workspace's current state. Overwrites whatever was there. */
|
|
572
|
+
save(snapshot: PersistedSnapshot): Promise<void>;
|
|
573
|
+
/** Everything on disk, newest update first. Corrupt entries are skipped. */
|
|
574
|
+
list(): Promise<PersistedMeta[]>;
|
|
575
|
+
/** One workspace, documents included, or undefined when it is not there. */
|
|
576
|
+
load(handle: string): Promise<RestoredWorkspace | undefined>;
|
|
577
|
+
/**
|
|
578
|
+
* Forget a workspace durably. Idempotent; answers whether anything was
|
|
579
|
+
* there, which is how `close` reports a handle it only ever saw on disk.
|
|
580
|
+
*
|
|
581
|
+
* Throws when the directory is there and could not be deleted. That has to
|
|
582
|
+
* reach the caller: `jto_workspace_close` promises the document is gone, and
|
|
583
|
+
* a swallowed failure would report it closed while the JSON stayed readable
|
|
584
|
+
* by the next connection to use the handle.
|
|
585
|
+
*/
|
|
586
|
+
remove(handle: string): Promise<boolean>;
|
|
587
|
+
}
|
|
588
|
+
interface WorkspacePersistenceOptions extends Partial<PersistenceLimits> {
|
|
589
|
+
/** `--workspace-dir` value, highest precedence. */
|
|
590
|
+
flagDir?: string;
|
|
591
|
+
/** Defaults to `process.env`; injectable so the precedence is testable. */
|
|
592
|
+
env?: NodeJS.ProcessEnv;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Build the persistence layer, or answer `undefined` when none was configured.
|
|
596
|
+
*
|
|
597
|
+
* `undefined` rather than a no-op implementation: the store branches on
|
|
598
|
+
* whether it has durability at all — to decide what `jto_info` reports and
|
|
599
|
+
* whether an evicted handle is recoverable — and a stand-in that quietly
|
|
600
|
+
* discarded writes would make that question unanswerable.
|
|
601
|
+
*/
|
|
602
|
+
declare function createWorkspacePersistence(options?: WorkspacePersistenceOptions): WorkspacePersistence | undefined;
|
|
603
|
+
/** The same layer on an explicit directory, for hosts and tests. */
|
|
604
|
+
declare function createWorkspacePersistenceAt(root: string, limits?: Partial<PersistenceLimits>): WorkspacePersistence;
|
|
605
|
+
|
|
455
606
|
/**
|
|
456
607
|
* What every tool module is handed at registration.
|
|
457
608
|
*
|
|
@@ -474,6 +625,15 @@ interface ToolDeps {
|
|
|
474
625
|
* registered, and a snapshot taken at registration would pin the stand-in.
|
|
475
626
|
*/
|
|
476
627
|
workspaces(): WorkspaceStore;
|
|
628
|
+
/**
|
|
629
|
+
* Disk backing for that store (#290), when a root was configured.
|
|
630
|
+
*
|
|
631
|
+
* Undefined is the default and means memory-only handles. It lives here
|
|
632
|
+
* rather than inside the store because `tools/workspace.ts` is what builds
|
|
633
|
+
* the connection's store, and `jto_info` reports the root without touching
|
|
634
|
+
* one.
|
|
635
|
+
*/
|
|
636
|
+
workspacePersistence?: WorkspacePersistence;
|
|
477
637
|
/** Ceiling for `outputMode: 'base64'`, in bytes. */
|
|
478
638
|
maxInlineArtifactBytes: number;
|
|
479
639
|
}
|
|
@@ -481,6 +641,10 @@ interface CreateToolDepsOptions {
|
|
|
481
641
|
/** `--output-dir`, or an already-built root (tests hand one in). */
|
|
482
642
|
outputDir?: string;
|
|
483
643
|
outputRoot?: OutputRoot;
|
|
644
|
+
/** `--workspace-dir`. Absent, `JTO_MCP_WORKSPACE_DIR` decides. */
|
|
645
|
+
workspaceDir?: string;
|
|
646
|
+
/** An already-built persistence layer, for hosts and tests. */
|
|
647
|
+
workspacePersistence?: WorkspacePersistence;
|
|
484
648
|
env?: NodeJS.ProcessEnv;
|
|
485
649
|
serverVersion?: string;
|
|
486
650
|
workspaces?: () => WorkspaceStore;
|
|
@@ -724,10 +888,112 @@ declare function parseDocumentJson(text: string, path?: string): {
|
|
|
724
888
|
document: unknown;
|
|
725
889
|
} | Failure;
|
|
726
890
|
|
|
891
|
+
/**
|
|
892
|
+
* The in-memory workspace store (#271).
|
|
893
|
+
*
|
|
894
|
+
* Documents are held as **serialized JSON text**, not as live trees. That one
|
|
895
|
+
* decision buys three of the issue's requirements outright:
|
|
896
|
+
*
|
|
897
|
+
* - Isolation. A read is a fresh `JSON.parse`, so a caller can mutate what it
|
|
898
|
+
* got and two workspaces can never share a subtree — the structural-sharing
|
|
899
|
+
* bug this feature invites cannot be written.
|
|
900
|
+
* - Atomicity. A commit is a single string assignment after a successful
|
|
901
|
+
* apply; a patch that fails leaves the stored bytes untouched, literally.
|
|
902
|
+
* - Accounting. `bytes` is the real size, already computed, so the count and
|
|
903
|
+
* byte budgets are exact rather than estimated.
|
|
904
|
+
*
|
|
905
|
+
* The cost is a parse and a stringify per operation, which for documents in
|
|
906
|
+
* the tens to hundreds of kilobytes this format produces is far below the
|
|
907
|
+
* round trip that made the agent call us.
|
|
908
|
+
*
|
|
909
|
+
* Nothing here throws: an unknown handle, a stale revision and a full store
|
|
910
|
+
* are answers an agent repairs, so they come back as structured failures
|
|
911
|
+
* (`lib/errors.ts`) exactly like a bad document does.
|
|
912
|
+
*/
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Workspace-lifecycle codes.
|
|
916
|
+
*
|
|
917
|
+
* Kept here rather than in `lib/errors.ts` (owned by #203, edited
|
|
918
|
+
* concurrently); they read as ordinary diagnostic codes to a client. TTL
|
|
919
|
+
* eviction gets its own code rather than reusing `E_UNKNOWN_HANDLE` because
|
|
920
|
+
* an agent can reopen and carry on after an idle handle expires.
|
|
921
|
+
*/
|
|
922
|
+
declare const WORKSPACE_ERROR_CODES: {
|
|
923
|
+
readonly EVICTED: "E_WORKSPACE_EVICTED";
|
|
924
|
+
readonly LIMIT: "E_WORKSPACE_LIMIT";
|
|
925
|
+
readonly DOCUMENT_TOO_LARGE: "E_DOCUMENT_TOO_LARGE";
|
|
926
|
+
readonly INVALID_ROOT: "E_INVALID_DOCUMENT_ROOT";
|
|
927
|
+
/**
|
|
928
|
+
* A revision could not be mirrored to disk (#290). A warning, never a
|
|
929
|
+
* failure: the edit landed and the workspace is usable, but this connection
|
|
930
|
+
* has stopped being survivable and saying so is the whole point.
|
|
931
|
+
*/
|
|
932
|
+
readonly NOT_PERSISTED: "W_WORKSPACE_NOT_PERSISTED";
|
|
933
|
+
/**
|
|
934
|
+
* A close could not delete the durable copy, so nothing was released (#290).
|
|
935
|
+
* An error rather than a warning: the agent asked for the document to be
|
|
936
|
+
* destroyed, and it is still there to be read by the next connection.
|
|
937
|
+
*/
|
|
938
|
+
readonly NOT_CLOSED: "E_WORKSPACE_NOT_CLOSED";
|
|
939
|
+
};
|
|
940
|
+
interface WorkspaceLimits {
|
|
941
|
+
/** Open documents at once. */
|
|
942
|
+
maxWorkspaces: number;
|
|
943
|
+
/** Ceiling for a single serialized document. */
|
|
944
|
+
maxDocumentBytes: number;
|
|
945
|
+
/** Ceiling for every document and pinned snapshot together. */
|
|
946
|
+
maxTotalBytes: number;
|
|
947
|
+
/** Idle time after which a handle is dropped. Any read or write resets it. */
|
|
948
|
+
idleTtlMs: number;
|
|
949
|
+
/** Snapshots kept retrievable per workspace; new pins are refused at the cap. */
|
|
950
|
+
maxPinnedRevisions: number;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Deliberately modest.
|
|
954
|
+
*
|
|
955
|
+
* A workspace is a live authoring buffer for one agent on one stdio
|
|
956
|
+
* connection, not a document store: sixteen open documents is already more
|
|
957
|
+
* than an agent can hold in context, and the byte ceilings exist to stop a
|
|
958
|
+
* runaway loop from turning a helper process into the machine's memory
|
|
959
|
+
* problem. The idle TTL is half an hour because an agent's turn can stall on a
|
|
960
|
+
* human for a long while, and losing a document mid-conversation is worse than
|
|
961
|
+
* holding a few megabytes.
|
|
962
|
+
*/
|
|
963
|
+
declare const DEFAULT_WORKSPACE_LIMITS: WorkspaceLimits;
|
|
964
|
+
interface MemoryWorkspaceStoreOptions extends Partial<WorkspaceLimits> {
|
|
965
|
+
/** Injectable clock; the TTL suite drives it instead of waiting. */
|
|
966
|
+
now?: () => number;
|
|
967
|
+
/** Injectable handle source, for tests that need predictable handles. */
|
|
968
|
+
newHandle?: () => string;
|
|
969
|
+
/**
|
|
970
|
+
* Disk backing (#290). Absent is the historical behaviour: memory only, and
|
|
971
|
+
* everything goes when the connection does.
|
|
972
|
+
*/
|
|
973
|
+
persistence?: WorkspacePersistence;
|
|
974
|
+
}
|
|
975
|
+
interface MemoryWorkspaceStore extends WorkspaceStore {
|
|
976
|
+
readonly limits: WorkspaceLimits;
|
|
977
|
+
/** Live totals, for `jto_workspace_list` to show the agent its budget. */
|
|
978
|
+
usage(): {
|
|
979
|
+
workspaces: number;
|
|
980
|
+
bytes: number;
|
|
981
|
+
};
|
|
982
|
+
/**
|
|
983
|
+
* Where revisions survive the connection, when a root was configured.
|
|
984
|
+
* Absent means handles are memory-only and end with this process (#290).
|
|
985
|
+
*/
|
|
986
|
+
readonly persistence?: {
|
|
987
|
+
root: string;
|
|
988
|
+
limits: PersistenceLimits;
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
declare function createMemoryWorkspaceStore(options?: MemoryWorkspaceStoreOptions): MemoryWorkspaceStore;
|
|
992
|
+
|
|
727
993
|
declare const SERVER_VERSION: string;
|
|
728
994
|
/** MCP `serverInfo.name`. Stable: clients key configuration off it. */
|
|
729
995
|
declare const SERVER_NAME = "json-to-office";
|
|
730
996
|
/** npm identity, reported by `jto_info` next to the workspace packages. */
|
|
731
997
|
declare const PACKAGE_NAME = "@json-to-office/mcp-server";
|
|
732
998
|
|
|
733
|
-
export { type Artifact, type ArtifactMode, type ArtifactOutputInput, type CreateToolDepsOptions, DOCUMENT_SOURCE_RULE, type DeliverArtifactOptions, type DeliverArtifactResult, type Diagnostic, type DiagnosticCounts, type DiagnosticSeverity, type DocumentSourceInput, ERROR_CODES, type ErrorCode, FORMAT_NAMES, type Failure, type JsonPatchOperation, MAX_INLINE_ARTIFACT_BYTES, MIME_TYPES, OPTION_ERROR_CODES, OUTPUT_DIR_ENV, type OutputRoot, type OutputRootOptions, PACKAGE_NAME, type RenderOptionsInput, type ResolvedDocument, type ResolvedOutputPath, S, SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, type SourceSummary, type ToolDeps, type ToolEnvelope, type WorkspaceRecord, type WorkspaceResult, type WorkspaceStore, artifactOutputProperties, artifactSchema, checkOutputName, checkRenderer, countDiagnostics, createOutputRoot, createServer, createServerFactory, createToolDeps, deliverArtifact, diagnostic, diagnosticSchema, diagnosticsFromThrown, diagnosticsSchema, documentSourceProperties, documentSourceSchema, envelopeProperties, failure, failureFrom, formatSchema, fromValidationError, fromValidationErrors, getAdapter, getWorkspaceStore, guarded, outputSchema, parseDocumentJson, renderOptionProperties, resetAdapters, resolveDocumentSource, setWorkspaceStore, sourceSummary, sourceSummarySchema, success, toJsonPointer, toolResult, unavailableWorkspaceStore, validationDiagnostics, withRenderer };
|
|
999
|
+
export { type Artifact, type ArtifactMode, type ArtifactOutputInput, type CreateToolDepsOptions, DEFAULT_PERSISTENCE_LIMITS, DEFAULT_WORKSPACE_LIMITS, DOCUMENT_SOURCE_RULE, type DeliverArtifactOptions, type DeliverArtifactResult, type Diagnostic, type DiagnosticCounts, type DiagnosticSeverity, type DocumentSourceInput, ERROR_CODES, type ErrorCode, FORMAT_NAMES, type Failure, type JsonPatchOperation, MAX_INLINE_ARTIFACT_BYTES, MIME_TYPES, type MemoryWorkspaceStore, type MemoryWorkspaceStoreOptions, OPTION_ERROR_CODES, OUTPUT_DIR_ENV, type OutputRoot, type OutputRootOptions, PACKAGE_NAME, type PersistenceLimits, type RenderOptionsInput, type ResolvedDocument, type ResolvedOutputPath, S, SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, type SourceSummary, type ToolDeps, type ToolEnvelope, WORKSPACE_DIR_ENV, WORKSPACE_ERROR_CODES, type WorkspaceLimits, type WorkspacePersistence, type WorkspacePersistenceOptions, type WorkspaceRecord, type WorkspaceResult, type WorkspaceStore, artifactOutputProperties, artifactSchema, checkOutputName, checkRenderer, countDiagnostics, createMemoryWorkspaceStore, createOutputRoot, createServer, createServerFactory, createToolDeps, createWorkspacePersistence, createWorkspacePersistenceAt, deliverArtifact, diagnostic, diagnosticSchema, diagnosticsFromThrown, diagnosticsSchema, documentSourceProperties, documentSourceSchema, envelopeProperties, failure, failureFrom, formatSchema, fromValidationError, fromValidationErrors, getAdapter, getWorkspaceStore, guarded, outputSchema, parseDocumentJson, renderOptionProperties, resetAdapters, resolveDocumentSource, setWorkspaceStore, sourceSummary, sourceSummarySchema, success, toJsonPointer, toolResult, unavailableWorkspaceStore, validationDiagnostics, withRenderer };
|