@crosery/dsh-viewer 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/README.md +182 -2
- package/README.zh.md +255 -0
- package/assets/logo.png +0 -0
- package/assets/screenshot-1-media.png +0 -0
- package/assets/screenshot-2-office.png +0 -0
- package/assets/screenshot-3-pdf-html.png +0 -0
- package/cordis.patch.yml +29 -0
- package/lib/client.js +1136 -0
- package/lib/index.js +975 -0
- package/lib/types/asset-route.d.ts +67 -0
- package/lib/types/asset-token.d.ts +53 -0
- package/lib/types/cache.d.ts +96 -0
- package/lib/types/client/ViewerCard.d.ts +77 -0
- package/lib/types/client/ViewerTail.d.ts +40 -0
- package/lib/types/client/card-model.d.ts +117 -0
- package/lib/types/client/host.d.ts +54 -0
- package/lib/types/client/index.d.ts +59 -0
- package/lib/types/client/locales.d.ts +11 -0
- package/lib/types/client/registration.d.ts +55 -0
- package/lib/types/client/sources.d.ts +32 -0
- package/lib/types/client/styles.d.ts +14 -0
- package/lib/types/client/turn-tail.d.ts +373 -0
- package/lib/types/contract.d.ts +209 -0
- package/lib/types/convert.d.ts +181 -0
- package/lib/types/display-file.d.ts +77 -0
- package/lib/types/index.d.ts +77 -0
- package/lib/types/read-redirect.d.ts +109 -0
- package/lib/types/read-target.d.ts +37 -0
- package/lib/types/settings.d.ts +23 -0
- package/lib/types/supersede-read-image.d.ts +34 -0
- package/package.json +126 -4
- package/screenshots.json +5 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Office and OpenDocument conversion: turn a format no browser renders into
|
|
3
|
+
* one every browser renders.
|
|
4
|
+
*
|
|
5
|
+
* The target is always PDF. A pure front-end path exists (`docx-preview` for
|
|
6
|
+
* Word, `exceljs` + a grid for Excel) but does not survive this plugin's
|
|
7
|
+
* constraints: the browser half is a lazy-CJS bundle whose module table answers
|
|
8
|
+
* only the shell baseline, so every dependency would have to be inlined, and
|
|
9
|
+
* there is no free PPTX renderer to inline in the first place. One converter
|
|
10
|
+
* producing one format also means the card has exactly one document code path.
|
|
11
|
+
*
|
|
12
|
+
* Conversion costs seconds, so the cache is the real feature. The key covers
|
|
13
|
+
* the converter version as well as the file identity, because the same bytes
|
|
14
|
+
* through a newer LibreOffice are a different artifact and a stale hit would be
|
|
15
|
+
* invisible. The key must also be stable across restarts, or nothing is ever
|
|
16
|
+
* hit twice and the directory only grows; `cache.ts` bounds it either way.
|
|
17
|
+
*
|
|
18
|
+
* Two converters. From 0.1.6-alpha.2 the harness composes its own
|
|
19
|
+
* `officeToPdf` service with a bundled LibreOffice kit, so a desktop user with
|
|
20
|
+
* no LibreOffice installed still gets a preview of the six Office formats it
|
|
21
|
+
* accepts; {@link convertWithOfficeToPdf} drives it. Every other document
|
|
22
|
+
* format, and every older train, uses a locally installed LibreOffice through
|
|
23
|
+
* {@link convertDocument}.
|
|
24
|
+
* @module @crosery/dsh-viewer/convert
|
|
25
|
+
*/
|
|
26
|
+
/** Wall-clock budget for one conversion. A cold LibreOffice start is seconds. */
|
|
27
|
+
export declare const CONVERT_TIMEOUT_MS = 120000;
|
|
28
|
+
/** What a converted document is served as. */
|
|
29
|
+
export declare const CONVERTED_MEDIA_TYPE = "application/pdf";
|
|
30
|
+
/** A resolved converter: its binary and the version string that keys the cache. */
|
|
31
|
+
export interface Converter {
|
|
32
|
+
binary: string;
|
|
33
|
+
version: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Locate LibreOffice and read its version, once per process.
|
|
37
|
+
*
|
|
38
|
+
* The version is part of the cache key, so it has to come from the binary
|
|
39
|
+
* rather than be assumed; a machine that upgrades LibreOffice mid-session
|
|
40
|
+
* simply starts writing artifacts under a new key.
|
|
41
|
+
* @returns the converter, or `undefined` when no LibreOffice is installed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveConverter(): Promise<Converter | undefined>;
|
|
44
|
+
/** Reset the memoized probe. Test seam; production resolves once per process. */
|
|
45
|
+
export declare function resetConverterProbe(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Content-addressed artifact name for one source file.
|
|
48
|
+
*
|
|
49
|
+
* Keyed on path plus mtime plus size rather than on a digest of the bytes: a
|
|
50
|
+
* multi-hundred-megabyte presentation should not be read twice just to decide
|
|
51
|
+
* whether it was already converted, and the triple changes on every edit that
|
|
52
|
+
* matters.
|
|
53
|
+
* @param converter - the resolved converter, whose version joins the key.
|
|
54
|
+
* @param sourcePath - absolute path of the source document.
|
|
55
|
+
* @param mtimeMs - source modification time.
|
|
56
|
+
* @param size - source byte length.
|
|
57
|
+
* @returns the artifact's basename, extension included.
|
|
58
|
+
*/
|
|
59
|
+
export declare function artifactName(converter: Converter, sourcePath: string, mtimeMs: number, size: number): string;
|
|
60
|
+
/**
|
|
61
|
+
* Convert one document to PDF, or return the cached artifact.
|
|
62
|
+
* @param sourcePath - absolute path of the source, in the Host's own filesystem.
|
|
63
|
+
* @param cacheDir - directory owning converted artifacts.
|
|
64
|
+
* @param signal - cancellation for the whole operation.
|
|
65
|
+
* @returns the artifact's absolute path.
|
|
66
|
+
* @throws when no converter is installed, or when LibreOffice produced nothing.
|
|
67
|
+
*/
|
|
68
|
+
export declare function convertDocument(sourcePath: string, cacheDir: string, signal?: AbortSignal): Promise<string>;
|
|
69
|
+
/**
|
|
70
|
+
* Extensions the harness's own Office converter accepts (`dsh-office-to-pdf`,
|
|
71
|
+
* shipped with 0.1.6-alpha.2 and later). Everything else a `document` card
|
|
72
|
+
* shows — RTF and the OpenDocument trio — still goes through LibreOffice.
|
|
73
|
+
*/
|
|
74
|
+
export declare const OFFICE_EXTENSIONS: readonly ["doc", "docx", "xls", "xlsx", "ppt", "pptx"];
|
|
75
|
+
/** One extension {@link OfficeToPdfLike.convert} accepts. */
|
|
76
|
+
export type OfficeExtension = typeof OFFICE_EXTENSIONS[number];
|
|
77
|
+
/**
|
|
78
|
+
* The slice of the harness `officeToPdf` service this plugin calls, declared
|
|
79
|
+
* structurally.
|
|
80
|
+
*
|
|
81
|
+
* Not imported, not even as a type: `@deepseek-ai/dsh-office-to-pdf` does not
|
|
82
|
+
* exist on the trains before 0.1.6, so naming it would fail the typecheck of
|
|
83
|
+
* every older train this plugin still supports, and a value import would fail
|
|
84
|
+
* the whole entry at ESM link time there. The service is read by name through
|
|
85
|
+
* `ctx.get` and narrowed with {@link officeToPdfOf}.
|
|
86
|
+
*/
|
|
87
|
+
export interface OfficeToPdfLike {
|
|
88
|
+
/**
|
|
89
|
+
* The provider's resolved configuration — fonts, fallbacks, image
|
|
90
|
+
* resolution, limits. Read only to key the cache: see {@link officeConverterIdentity}.
|
|
91
|
+
*/
|
|
92
|
+
readonly config?: unknown;
|
|
93
|
+
/**
|
|
94
|
+
* Convert Office bytes. The provider owns queueing, the bundled engine and
|
|
95
|
+
* its own content cache; the caller owns authorization and the source read.
|
|
96
|
+
*/
|
|
97
|
+
convert(request: {
|
|
98
|
+
readonly extension: OfficeExtension;
|
|
99
|
+
readonly priority: 'foreground' | 'background';
|
|
100
|
+
readonly source: {
|
|
101
|
+
readonly key: string;
|
|
102
|
+
readonly version: string;
|
|
103
|
+
readonly bytes?: number;
|
|
104
|
+
read(signal: AbortSignal, maxBytes: number): Promise<{
|
|
105
|
+
readonly bytes: Uint8Array;
|
|
106
|
+
readonly version: string;
|
|
107
|
+
}>;
|
|
108
|
+
};
|
|
109
|
+
}, signal?: AbortSignal): Promise<{
|
|
110
|
+
readonly pdf: Uint8Array;
|
|
111
|
+
}>;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Narrow an optional service value to {@link OfficeToPdfLike}.
|
|
115
|
+
* @param service - whatever `ctx.get('officeToPdf')` returned.
|
|
116
|
+
* @returns the converter, or `undefined` when this train composes none.
|
|
117
|
+
*/
|
|
118
|
+
export declare function officeToPdfOf(service: unknown): OfficeToPdfLike | undefined;
|
|
119
|
+
/**
|
|
120
|
+
* The extension `officeToPdf` would accept for this path, if any.
|
|
121
|
+
* @param sourcePath - the document's path.
|
|
122
|
+
* @returns the bare lowercased extension, or `undefined` for a LibreOffice-only format.
|
|
123
|
+
*/
|
|
124
|
+
export declare function officeExtensionOf(sourcePath: string): OfficeExtension | undefined;
|
|
125
|
+
/** The source one {@link convertWithOfficeToPdf} call converts. */
|
|
126
|
+
export interface OfficeSource {
|
|
127
|
+
/** The Host's own path of the document; keys the artifact and the provider's dedup. */
|
|
128
|
+
path: string;
|
|
129
|
+
/** The filesystem freshness token observed when the call resolved the file. */
|
|
130
|
+
version: string;
|
|
131
|
+
/** Byte size, when the backend reported one. */
|
|
132
|
+
bytes?: number;
|
|
133
|
+
/**
|
|
134
|
+
* Read the document's bytes, bounded by the provider's reservation.
|
|
135
|
+
* @param signal - the provider's conversion lifetime.
|
|
136
|
+
* @param maxBytes - the capacity the provider reserved for this source.
|
|
137
|
+
*/
|
|
138
|
+
read(signal: AbortSignal, maxBytes: number): Promise<Uint8Array>;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A stable identity for the bundled converter, for the artifact key.
|
|
142
|
+
*
|
|
143
|
+
* Not the provider's `generation`: that is `randomUUID()` per provider
|
|
144
|
+
* instance, so keying on it made every restart a cold cache — the cache was
|
|
145
|
+
* never hit across restarts and only grew. The provider replaces its
|
|
146
|
+
* generation when its configuration is replaced, so the configuration itself
|
|
147
|
+
* is the stable half of the same idea: different fonts or rendering settings
|
|
148
|
+
* are a different artifact, a restart is not. What it cannot see is a harness
|
|
149
|
+
* upgrade that ships a new engine under an unchanged configuration; such an
|
|
150
|
+
* artifact is still a faithful render of unchanged bytes, and it ages out of
|
|
151
|
+
* the cache like any other.
|
|
152
|
+
* @param converter - the harness `officeToPdf` service.
|
|
153
|
+
* @returns a string that changes exactly when the provider's configuration does.
|
|
154
|
+
*/
|
|
155
|
+
export declare function officeConverterIdentity(converter: OfficeToPdfLike): string;
|
|
156
|
+
/**
|
|
157
|
+
* Artifact name for one conversion through the bundled converter.
|
|
158
|
+
*
|
|
159
|
+
* Keyed on the converter's identity as well as on the source identity, for the
|
|
160
|
+
* same reason the LibreOffice key carries the LibreOffice version: the same
|
|
161
|
+
* bytes through different fonts or rendering settings are a different PDF.
|
|
162
|
+
* @param identity - {@link officeConverterIdentity} of the provider.
|
|
163
|
+
* @param source - the converted document.
|
|
164
|
+
* @returns the artifact's basename, extension included.
|
|
165
|
+
*/
|
|
166
|
+
export declare function officeArtifactName(identity: string, source: Pick<OfficeSource, 'path' | 'version' | 'bytes'>): string;
|
|
167
|
+
/**
|
|
168
|
+
* Convert one Office document through the harness's bundled converter, or
|
|
169
|
+
* return the cached artifact.
|
|
170
|
+
*
|
|
171
|
+
* The provider hands the PDF back as bytes, but the asset route serves files:
|
|
172
|
+
* the bytes are written into this plugin's own cache and signed there, like a
|
|
173
|
+
* LibreOffice artifact, through {@link placeArtifact}.
|
|
174
|
+
* @param converter - the harness `officeToPdf` service.
|
|
175
|
+
* @param source - the document and its bounded reader.
|
|
176
|
+
* @param cacheDir - directory owning converted artifacts.
|
|
177
|
+
* @param signal - cancellation for the whole operation.
|
|
178
|
+
* @returns the artifact's absolute path.
|
|
179
|
+
* @throws when the format is not one the provider accepts, or the provider refuses or fails.
|
|
180
|
+
*/
|
|
181
|
+
export declare function convertWithOfficeToPdf(converter: OfficeToPdfLike, source: OfficeSource, cacheDir: string, signal?: AbortSignal): Promise<string>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `display_file` tool: put a file on the user's screen.
|
|
3
|
+
*
|
|
4
|
+
* The difference from the shipped `read_image` is one deliberate inversion.
|
|
5
|
+
* `read_image` exists to put an image into MODEL context, so it refuses outright
|
|
6
|
+
* on a route that declares no image input, and it handles images only. This tool
|
|
7
|
+
* exists to put a file on the SCREEN, so a text-only route is a normal outcome
|
|
8
|
+
* rather than a refusal, and every medium a browser can play is in scope —
|
|
9
|
+
* video, audio, PDF and HTML have no representation in model context at all and
|
|
10
|
+
* would otherwise be undisplayable.
|
|
11
|
+
*
|
|
12
|
+
* That is why the durable facts ride `output.presentationMeta`. An image block
|
|
13
|
+
* is model-facing and exists only for four raster types on one kind of route;
|
|
14
|
+
* the meta is UI-facing, persists with `tool/result`, and therefore rebuilds
|
|
15
|
+
* the card when the session is reopened.
|
|
16
|
+
* @module @crosery/dsh-viewer/display-file
|
|
17
|
+
*/
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
19
|
+
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
|
|
20
|
+
import type { ToolExecution } from '@deepseek-ai/dsh-tools';
|
|
21
|
+
import { type DisplayValue, type ModelImage } from './contract.ts';
|
|
22
|
+
/** Live settings and key material {@link applyDisplayTool} reads per call. */
|
|
23
|
+
export interface DisplayToolOptions {
|
|
24
|
+
/** Whether an image-capable route may also receive the image itself. */
|
|
25
|
+
feedModel: () => boolean;
|
|
26
|
+
/** The harness asset MAC key, or `undefined` while it is still loading. */
|
|
27
|
+
secret: () => Buffer | undefined;
|
|
28
|
+
/** Directory owning converted document artifacts. */
|
|
29
|
+
cacheDir: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether the call's routed model accepts image input.
|
|
33
|
+
*
|
|
34
|
+
* Resolution mirrors the shipped `read_image` gate — request-header config
|
|
35
|
+
* first, then the agent's own options — but the outcome is a boolean, not a
|
|
36
|
+
* refusal: an unresolvable route simply means "not image-capable", which
|
|
37
|
+
* degrades to a screen-only display instead of failing a call that would
|
|
38
|
+
* otherwise have worked.
|
|
39
|
+
* @param ctx - the plugin context used to reach the optional `llm` service.
|
|
40
|
+
* @param exec - the execution whose calling agent names the route.
|
|
41
|
+
* @returns whether an image block may be emitted for this call.
|
|
42
|
+
*/
|
|
43
|
+
export declare function routeAcceptsImages(ctx: Context, exec: ToolExecution): Promise<boolean>;
|
|
44
|
+
/**
|
|
45
|
+
* Project a durable attachment reference into the plugin's wire shape.
|
|
46
|
+
* @param ref - the reference the attachment service published.
|
|
47
|
+
* @returns the card-facing image facts.
|
|
48
|
+
*/
|
|
49
|
+
export declare function modelImageOf(ref: ImageAttachmentRef): ModelImage;
|
|
50
|
+
/**
|
|
51
|
+
* Re-brand the plugin's wire shape back into an attachment reference.
|
|
52
|
+
* @param image - the canonical image facts.
|
|
53
|
+
* @returns the branded reference an `ImageBlock` carries.
|
|
54
|
+
*/
|
|
55
|
+
export declare function attachmentRefOf(image: ModelImage): ImageAttachmentRef;
|
|
56
|
+
/**
|
|
57
|
+
* Format the model-facing envelope. Pure, and — for every medium except an
|
|
58
|
+
* in-context raster — the only thing the model receives, so it states plainly
|
|
59
|
+
* what is on the user's screen and that the model itself cannot see it.
|
|
60
|
+
* @param value - the canonical outcome.
|
|
61
|
+
* @returns the envelope body.
|
|
62
|
+
*/
|
|
63
|
+
export declare function formatDisplayOutput(value: DisplayValue): string;
|
|
64
|
+
/**
|
|
65
|
+
* Register `display_file` into the given context.
|
|
66
|
+
*
|
|
67
|
+
* The composing plugin owns the service gates: `src/index.ts` calls this inside
|
|
68
|
+
* the `fs` injection, and execution treats `attachments` as optional so a
|
|
69
|
+
* deployment with no durable store still displays every medium — it just cannot
|
|
70
|
+
* put a raster into model context.
|
|
71
|
+
* @param ctx - the registration scope; execution uses its `fs` service plus the
|
|
72
|
+
* optional `attachments`/`llm` services.
|
|
73
|
+
* @param options - live settings reads and the asset MAC key.
|
|
74
|
+
* @returns the disposer that unregisters the tool, so a settings change can
|
|
75
|
+
* remove it from the model's schema list rather than leave it there refusing.
|
|
76
|
+
*/
|
|
77
|
+
export declare function applyDisplayTool(ctx: Context, options: DisplayToolOptions): () => void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of the viewer plugin: a `display_file` tool the model calls to put
|
|
3
|
+
* a file on the user's screen, and the signed HTTP route that carries the bytes.
|
|
4
|
+
*
|
|
5
|
+
* Why a route at all, when the harness already has a durable attachment service:
|
|
6
|
+
* that service is images-only, by design (`saveImage`, `readImage`,
|
|
7
|
+
* `ImageAttachmentLimits.mediaTypes`). Video, audio, PDF and HTML have no
|
|
8
|
+
* representation in it and no representation in model context either, so the
|
|
9
|
+
* only way to get them in front of the user is to serve their bytes to the page.
|
|
10
|
+
* Serving them by signed reference — rather than by a path the browser supplies
|
|
11
|
+
* — is what keeps that route from being an arbitrary-file-read endpoint.
|
|
12
|
+
*
|
|
13
|
+
* The two paths are complementary, not redundant: an admissible raster on a
|
|
14
|
+
* vision route still goes through the attachment store, because that is the only
|
|
15
|
+
* way it also reaches the model.
|
|
16
|
+
*/
|
|
17
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
18
|
+
import { type ViewerSettings } from './contract.ts';
|
|
19
|
+
export { ASSET_ROUTE, DISPLAY_TOOL, VIEWER_SETTINGS_NAMESPACE, classifyPath, extensionOf, formatBytes, isOpaqueMediaPath, modelImageMediaTypeForPath, } from './contract.ts';
|
|
20
|
+
export type { DisplayValue, ModelImage, ViewerKind, ViewerSettings } from './contract.ts';
|
|
21
|
+
export { ViewerSettingsSchema } from './settings.ts';
|
|
22
|
+
export { assetUrlFor, verifyAssetRequest } from './asset-token.ts';
|
|
23
|
+
export { guardHeaders, parseRange } from './asset-route.ts';
|
|
24
|
+
export { artifactName, convertDocument, resolveConverter } from './convert.ts';
|
|
25
|
+
export { isMisdirectedRead, mediaReadValue, readPathOf } from './read-redirect.ts';
|
|
26
|
+
export { applySupersedeReadImage } from './supersede-read-image.ts';
|
|
27
|
+
/**
|
|
28
|
+
* Settings namespace this plugin owns, as the settings service keys it.
|
|
29
|
+
*
|
|
30
|
+
* Plain string rather than a branded one since 0.1.2 removed the `settingsNamespace`
|
|
31
|
+
* constructor that produced the brand. Consumers hand it to `ctx.settings`, which
|
|
32
|
+
* validates it at registration either way.
|
|
33
|
+
*/
|
|
34
|
+
export declare const VIEWER_NAMESPACE = "crosery-viewer";
|
|
35
|
+
export declare const name = "@crosery/dsh-viewer";
|
|
36
|
+
/**
|
|
37
|
+
* Required services — deliberately the smallest set that lets the tool exist.
|
|
38
|
+
*
|
|
39
|
+
* `webServer` is NOT here even though the asset route is the transport for
|
|
40
|
+
* every non-raster medium. An entry that never activates is a hard boot failure
|
|
41
|
+
* (`dsh: 1 entry did not activate`), not a graceful skip, so requiring it would
|
|
42
|
+
* make this bundle un-composable with `dsh-headless` or `acp` rather than
|
|
43
|
+
* merely inert there. It is injected as a nested scope below instead.
|
|
44
|
+
*
|
|
45
|
+
* `attachments` and `llm` are likewise optional and read through `ctx.get`: a
|
|
46
|
+
* deployment without a durable store still displays everything it can reach —
|
|
47
|
+
* it just cannot put an image into model context.
|
|
48
|
+
*/
|
|
49
|
+
export declare const inject: string[];
|
|
50
|
+
export type Config = ViewerSettings;
|
|
51
|
+
export declare const Config: import("@deepseek-ai/schemastery").default<ViewerSettings>;
|
|
52
|
+
/** Callbacks a mount invokes. Identical on every harness train that has a mount. */
|
|
53
|
+
export interface SettingsHooks {
|
|
54
|
+
/** Receive the authoritative value: the resolved section while one is attached. */
|
|
55
|
+
setSource(current: () => ViewerSettings): void;
|
|
56
|
+
/** Re-judge derived state after an attach, a detach, or a committed change. */
|
|
57
|
+
onChange(): void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Mount the `crosery-viewer` namespace over whichever settings API exists.
|
|
61
|
+
*
|
|
62
|
+
* Only called while a settings service is present: a composition without one
|
|
63
|
+
* keeps the composition entry as the sole source, which `reconcile()` in
|
|
64
|
+
* {@link apply} establishes on its own.
|
|
65
|
+
*
|
|
66
|
+
* @param ctx - plugin context: the mount's owner, and the injection parent.
|
|
67
|
+
* @param config - composition entry config; both the `base` layer and the
|
|
68
|
+
* fallback value once the settings service detaches.
|
|
69
|
+
* @param hooks - source and change callbacks.
|
|
70
|
+
*/
|
|
71
|
+
export declare function mountSettingsSection(ctx: Context, config: ViewerSettings, hooks: SettingsHooks): void;
|
|
72
|
+
/**
|
|
73
|
+
* Mount the settings section, the asset route, the tool, and the read redirect.
|
|
74
|
+
* @param ctx - plugin context.
|
|
75
|
+
* @param config - composition entry config, used as the settings `base` layer.
|
|
76
|
+
*/
|
|
77
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping `read` off opaque media — without turning it into a failure.
|
|
3
|
+
*
|
|
4
|
+
* The shipped `read` is a UTF-8 decode. Pointed at a PNG or an MP4 it does not
|
|
5
|
+
* fail: it returns thousands of replacement characters, which the model then
|
|
6
|
+
* tries to interpret. That is worse than an error, because the model has no
|
|
7
|
+
* signal that it read nothing.
|
|
8
|
+
*
|
|
9
|
+
* Worse still, the shipped filesystem provider does not even get that far: it
|
|
10
|
+
* samples the head of the file and throws `FS_NOT_TEXT` (`cannot read "…":
|
|
11
|
+
* binary file`) on a NUL byte. So a `read` aimed at a PNG paints a RED FAILURE
|
|
12
|
+
* ROW into the transcript with or without this plugin — for a file that exists,
|
|
13
|
+
* is perfectly readable, and is one tool call away from being on screen.
|
|
14
|
+
*
|
|
15
|
+
* Neither obvious correction removes that row. A `tools/pre-execute` denial
|
|
16
|
+
* materializes its own `isError`. A `tools/post-execute` decision cannot help
|
|
17
|
+
* either: replacing a value is explicitly refused on a failed result, and
|
|
18
|
+
* replacing content leaves `isError` set.
|
|
19
|
+
*
|
|
20
|
+
* The correction therefore runs in `tools/execute`, the around-dispatch
|
|
21
|
+
* waterfall, and does NOT delegate — `next()` is never called, so the doomed
|
|
22
|
+
* read performs no filesystem I/O at all. The authored result is what makes
|
|
23
|
+
* this work: the registry feeds a wrapper's returned result through
|
|
24
|
+
* `normalizeDispatchResult`, which for a success re-runs the owning tool's
|
|
25
|
+
* `output.render` AND its `output.presentationMeta` over the authored value.
|
|
26
|
+
* Replacing the value therefore replaces the persisted read metadata too, so
|
|
27
|
+
* the shipped read card — which rebuilds itself from that metadata — renders
|
|
28
|
+
* this plugin's one-line pointer as an ordinary SUCCESSFUL read.
|
|
29
|
+
* @module @crosery/dsh-viewer/read-redirect
|
|
30
|
+
*/
|
|
31
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
32
|
+
/**
|
|
33
|
+
* The replacement `read` value for one misdirected call.
|
|
34
|
+
*
|
|
35
|
+
* Shaped to the shipped `read` tool's own output schema — `path`, `offset`,
|
|
36
|
+
* `lines`, `totalLines` — because the registry validates the replacement
|
|
37
|
+
* against that schema before projecting it. One line of explanation reads
|
|
38
|
+
* correctly in every surface the value reaches: as the model's tool output, as
|
|
39
|
+
* the read card's single content line, and in a replayed session.
|
|
40
|
+
* @param filePath - the path the model asked to read.
|
|
41
|
+
* @returns the canonical value to substitute.
|
|
42
|
+
*/
|
|
43
|
+
export declare function mediaReadValue(filePath: string): {
|
|
44
|
+
path: string;
|
|
45
|
+
offset: number;
|
|
46
|
+
lines: {
|
|
47
|
+
number: number;
|
|
48
|
+
text: string;
|
|
49
|
+
}[];
|
|
50
|
+
totalLines: number;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Read the `file_path` argument out of one `read` call.
|
|
54
|
+
*
|
|
55
|
+
* The arguments arrive as losslessly parsed JSON of whatever the model emitted,
|
|
56
|
+
* so nothing about their shape is guaranteed; anything that is not a non-empty
|
|
57
|
+
* string path is left for the tool's own validation to reject.
|
|
58
|
+
* @param args - the parsed call arguments.
|
|
59
|
+
* @returns the path, or `undefined` when the call names none.
|
|
60
|
+
*/
|
|
61
|
+
export declare function readPathOf(args: unknown): string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Whether one pending call is a text read aimed at opaque media.
|
|
64
|
+
* @param name - the wire tool name.
|
|
65
|
+
* @param args - the parsed call arguments.
|
|
66
|
+
* @returns true when the dispatch should be replaced with the display pointer.
|
|
67
|
+
*/
|
|
68
|
+
export declare function isMisdirectedRead(name: string, args: unknown): boolean;
|
|
69
|
+
/** Wire name of the harness's own deliverable tool (`dsh-tool-present`, 0.1.7). */
|
|
70
|
+
export declare const PRESENT_TOOL = "present";
|
|
71
|
+
/**
|
|
72
|
+
* Where the display guidance goes: directly after the shipped read guidance.
|
|
73
|
+
*
|
|
74
|
+
* Up to 0.1.1 tool guidance sat at 100–199 with `tool:read` at 100. From 0.1.2
|
|
75
|
+
* orders are allocated centrally (`TOOL_READ` is 1100) behind
|
|
76
|
+
* `getSectionOrder`; a hard-coded 101 there would put this section ahead of
|
|
77
|
+
* every tool's guidance instead of beside the read tool's.
|
|
78
|
+
* @param registry - the `systemPrompt` service.
|
|
79
|
+
* @returns the order to register at.
|
|
80
|
+
*/
|
|
81
|
+
export declare function displaySectionOrder(registry: unknown): number;
|
|
82
|
+
/**
|
|
83
|
+
* The guidance one prompt assembly receives.
|
|
84
|
+
*
|
|
85
|
+
* Empty whenever the scope cannot call `display_file` — the tool switched off
|
|
86
|
+
* in settings, or restricted away for this agent — so the model is never told
|
|
87
|
+
* about a tool it does not have. Where the harness also offers `present`, the
|
|
88
|
+
* three ways of putting a file in front of the user are divided explicitly:
|
|
89
|
+
* without that, a model reading both this section and the harness's own
|
|
90
|
+
* deliverable guidance tends to display, embed and present the same file.
|
|
91
|
+
*
|
|
92
|
+
* The redirect is described only while it is on: with `redirectRead` off, a
|
|
93
|
+
* `read` of a binary file fails or returns undecodable text, and a prompt that
|
|
94
|
+
* promised a pointer back would be the model's only, wrong, expectation.
|
|
95
|
+
* @param visible - whether a tool is visible in the assembling scope.
|
|
96
|
+
* @param redirecting - whether `read` on binary media is currently redirected.
|
|
97
|
+
* @returns the section text, or `''`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function displaySectionText(visible: (tool: string) => boolean, redirecting: boolean): string;
|
|
100
|
+
/**
|
|
101
|
+
* Install the correction and the prompt section that makes it unnecessary.
|
|
102
|
+
*
|
|
103
|
+
* The section is the primary mechanism — a model told about `display_file` up
|
|
104
|
+
* front rarely reaches the correction — and the correction is the backstop for
|
|
105
|
+
* the calls that get there anyway.
|
|
106
|
+
* @param ctx - the plugin context; both registrations are effects on it.
|
|
107
|
+
* @param enabled - live read of the `redirectRead` setting.
|
|
108
|
+
*/
|
|
109
|
+
export declare function applyReadRedirect(ctx: Context, enabled: () => boolean): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path resolution shared by the display tool and the read-redirect policy.
|
|
3
|
+
*
|
|
4
|
+
* `dsh-tool-fs` keeps its own `read-target`/`session-cwd` modules private (its
|
|
5
|
+
* package entry exports only `apply`/`Config`), so the session-cwd rule is
|
|
6
|
+
* restated here rather than imported. Keeping it byte-for-byte compatible
|
|
7
|
+
* matters: a path this plugin resolved differently from `read` would display a
|
|
8
|
+
* different file than the one the model believes it named.
|
|
9
|
+
* @module @crosery/dsh-viewer/read-target
|
|
10
|
+
*/
|
|
11
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
12
|
+
import type { FsInfo, FsTarget } from '@deepseek-ai/dsh-fs';
|
|
13
|
+
import type { ToolExecution } from '@deepseek-ai/dsh-tools';
|
|
14
|
+
/**
|
|
15
|
+
* The session workspace cwd for this call, or `undefined` when none applies.
|
|
16
|
+
*
|
|
17
|
+
* Non-agent callers get `undefined` on purpose: the fallback belongs to the
|
|
18
|
+
* filesystem provider, not to a tool reading `process.cwd()` at its boundary.
|
|
19
|
+
* @param exec - the tool execution; only its optional `agent` is read.
|
|
20
|
+
* @param requestedPath - the path about to be resolved; parent traversal makes a
|
|
21
|
+
* symlinked cwd's filesystem identity observable, so the cwd is canonicalized
|
|
22
|
+
* in exactly that case.
|
|
23
|
+
* @returns the calling agent's session cwd, or `undefined`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sessionCwd(exec: ToolExecution, requestedPath: string): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a model-supplied path, observe absence, and require a regular file.
|
|
28
|
+
* @param ctx - the plugin context supplying `fs` and the observation event.
|
|
29
|
+
* @param exec - the current execution, for session cwd and cancellation.
|
|
30
|
+
* @param requestedPath - the raw path the model supplied.
|
|
31
|
+
* @returns the resolved target and its single stat result.
|
|
32
|
+
* @throws {FsError} when the path is absent or is not a regular file.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveDisplayTarget(ctx: Context, exec: ToolExecution, requestedPath: string): Promise<{
|
|
35
|
+
target: FsTarget;
|
|
36
|
+
info: FsInfo;
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side settings schema over the shared contract. Only the Node half
|
|
3
|
+
* imports this module, so the validator never reaches the browser bundle.
|
|
4
|
+
*/
|
|
5
|
+
import z from '@deepseek-ai/schemastery';
|
|
6
|
+
import { type ViewerSettings } from './contract.ts';
|
|
7
|
+
/**
|
|
8
|
+
* Durable schema for the viewer's settings: the `crosery-viewer` section up to
|
|
9
|
+
* 0.1.6, and the plugin entry's own Config (entry id `viewer`) from 0.1.7.
|
|
10
|
+
*
|
|
11
|
+
* Every field defaults to the behavior the plugin exists to provide, so an
|
|
12
|
+
* empty composition row is the intended configuration and each flag is a way to
|
|
13
|
+
* give one piece back.
|
|
14
|
+
*
|
|
15
|
+
* The descriptions document each field wherever the schema is projected — on
|
|
16
|
+
* 0.1.7, the JSON Schema `dsh --dump-config-schema` prints for profile entries.
|
|
17
|
+
* They do not make a settings form: 0.1.7's Settings page builds forms only
|
|
18
|
+
* from volatile fields, and none of these is. They are English only:
|
|
19
|
+
* schemastery can carry a locale dictionary, but no first-party Config uses one
|
|
20
|
+
* and nothing guarantees every consumer resolves it, so a dictionary risks
|
|
21
|
+
* rendering as an unreadable object where a string always reads.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ViewerSettingsSchema: z<ViewerSettings>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Making `display_file` the single image entry point.
|
|
3
|
+
*
|
|
4
|
+
* `read_image` and `display_file` overlap on exactly one thing — putting a
|
|
5
|
+
* raster into model context — and a model presented with both will reasonably
|
|
6
|
+
* use both: `read_image` "to look at it", then `display_file` "to show it to
|
|
7
|
+
* the user". Observed in a real session on a 1672x941 PNG: the same 2.2 MB
|
|
8
|
+
* image was ingested TWICE in one turn, and the transcript showed two
|
|
9
|
+
* identical-looking cards. No prompt fixes this reliably, because both tools
|
|
10
|
+
* genuinely do what the model thinks they do.
|
|
11
|
+
*
|
|
12
|
+
* So one of them stops being visible. `display_file` is the strict superset: it
|
|
13
|
+
* handles every medium, works on text-only routes (where `read_image` refuses
|
|
14
|
+
* outright), and still feeds an admissible raster into context on a vision
|
|
15
|
+
* route. Hiding it is therefore a pure removal of duplication, not of ability.
|
|
16
|
+
*
|
|
17
|
+
* The mechanism is forced by the registry: `tools.restrict()` refuses a
|
|
18
|
+
* context-global call outright ("a context-global restriction would mask every
|
|
19
|
+
* agent"), so the restriction is applied per agent, on `agent.ctx`.
|
|
20
|
+
* @module @crosery/dsh-viewer/supersede-read-image
|
|
21
|
+
*/
|
|
22
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
23
|
+
/**
|
|
24
|
+
* Hide `read_image` from every agent while this plugin owns image display.
|
|
25
|
+
*
|
|
26
|
+
* Two triggers, because one is not enough. `agent/created` covers agents made
|
|
27
|
+
* after the tool exists; `tools/change` covers the race in the other order —
|
|
28
|
+
* `read_image` is registered behind an async service injection, so an agent
|
|
29
|
+
* created first would otherwise keep seeing it for its whole life. Agents that
|
|
30
|
+
* already carry the restriction are skipped, so the retry is idempotent.
|
|
31
|
+
* @param ctx - the plugin context; both listeners are effects on it.
|
|
32
|
+
* @param enabled - live read of the `supersedeReadImage` setting.
|
|
33
|
+
*/
|
|
34
|
+
export declare function applySupersedeReadImage(ctx: Context, enabled: () => boolean): void;
|