@sparkelf/dsh-knowledge-base 0.0.0-stage → 0.1.1

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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/SKILL.md +147 -0
  3. package/cordis.patch.yml +5 -0
  4. package/i18n/zh.json +10 -0
  5. package/lib/client.js +18308 -0
  6. package/lib/client.js.map +1 -0
  7. package/lib/index.js +6005 -0
  8. package/lib/types/api.d.ts +620 -0
  9. package/lib/types/client/KnowledgeAudit.d.ts +39 -0
  10. package/lib/types/client/KnowledgeContextMenu.d.ts +121 -0
  11. package/lib/types/client/KnowledgeCreateDialog.d.ts +134 -0
  12. package/lib/types/client/KnowledgeFileTree.d.ts +117 -0
  13. package/lib/types/client/KnowledgeHome.d.ts +102 -0
  14. package/lib/types/client/KnowledgeIndexing.d.ts +60 -0
  15. package/lib/types/client/KnowledgeListing.d.ts +174 -0
  16. package/lib/types/client/KnowledgeLoading.d.ts +41 -0
  17. package/lib/types/client/KnowledgeMenuIcon.d.ts +27 -0
  18. package/lib/types/client/KnowledgePanel.d.ts +58 -0
  19. package/lib/types/client/KnowledgePermissionOverview.d.ts +147 -0
  20. package/lib/types/client/KnowledgePermissions.d.ts +47 -0
  21. package/lib/types/client/KnowledgeProgressPanel.d.ts +53 -0
  22. package/lib/types/client/KnowledgeReferencePicker.d.ts +81 -0
  23. package/lib/types/client/KnowledgeSearch.d.ts +83 -0
  24. package/lib/types/client/KnowledgeSectionHeader.d.ts +71 -0
  25. package/lib/types/client/KnowledgeSelectionBar.d.ts +45 -0
  26. package/lib/types/client/KnowledgeSettings.d.ts +96 -0
  27. package/lib/types/client/KnowledgeSettingsSection.d.ts +21 -0
  28. package/lib/types/client/KnowledgeShareDialog.d.ts +70 -0
  29. package/lib/types/client/KnowledgeTemplates.d.ts +43 -0
  30. package/lib/types/client/KnowledgeTrash.d.ts +62 -0
  31. package/lib/types/client/MenuGlyphs.d.ts +37 -0
  32. package/lib/types/client/auditPaging.d.ts +40 -0
  33. package/lib/types/client/departmentFolders.d.ts +57 -0
  34. package/lib/types/client/deployTarget.d.ts +34 -0
  35. package/lib/types/client/index.d.ts +40 -0
  36. package/lib/types/client/insertReadiness.d.ts +60 -0
  37. package/lib/types/client/knowledgeMenuCopy.d.ts +23 -0
  38. package/lib/types/client/locales.d.ts +505 -0
  39. package/lib/types/client/openInSidebar.d.ts +62 -0
  40. package/lib/types/client/ownerLabel.d.ts +39 -0
  41. package/lib/types/client/paging.d.ts +69 -0
  42. package/lib/types/client/permissionRows.d.ts +265 -0
  43. package/lib/types/client/progressRules.d.ts +54 -0
  44. package/lib/types/client/referenceTargets.d.ts +60 -0
  45. package/lib/types/client/roleSummary.d.ts +47 -0
  46. package/lib/types/client/saveAudit.d.ts +87 -0
  47. package/lib/types/client/shareRelations.d.ts +81 -0
  48. package/lib/types/client/useFavorites.d.ts +49 -0
  49. package/lib/types/client/useFileDrop.d.ts +171 -0
  50. package/lib/types/client/useFileDropHook.d.ts +64 -0
  51. package/lib/types/client/usePinnedPaths.d.ts +39 -0
  52. package/lib/types/client/useTreeState.d.ts +46 -0
  53. package/lib/types/client/useViewMode.d.ts +39 -0
  54. package/lib/types/deploy.d.ts +209 -0
  55. package/lib/types/extract.d.ts +144 -0
  56. package/lib/types/extractionQueue.d.ts +154 -0
  57. package/lib/types/index.d.ts +899 -0
  58. package/lib/types/permissions.d.ts +237 -0
  59. package/lib/types/protocol.d.ts +229 -0
  60. package/lib/types/remote.d.ts +105 -0
  61. package/lib/types/trash.d.ts +142 -0
  62. package/lib/types/viewMetadata.d.ts +20 -0
  63. package/lib/types/views.d.ts +923 -0
  64. package/package.json +109 -4
  65. package/skills/kb-organize/SKILL.md +365 -0
  66. package/skills/kb-organize/i18n/zh.json +10 -0
  67. package/README.md +0 -3
@@ -0,0 +1,899 @@
1
+ /**
2
+ * Knowledge-base Host plugin.
3
+ *
4
+ * This package owns three things and nothing else:
5
+ * 1. The protocol contract toward the DataOps knowledge API (`protocol.ts`).
6
+ * 2. Idempotent deployment recipes for the remote pieces, run over the
7
+ * ssh-manager seam (`deploy.ts`, `remote.ts`).
8
+ * 3. The same-origin HTTP surface the Settings panel reads, plus the two
9
+ * model-facing tools.
10
+ *
11
+ * It owns no authorization logic. DataOps AuthGuard and PermissionGuard resolve
12
+ * the caller's roles and grants from the session credential; this plugin only
13
+ * forwards requests (AGENTS.md "Managed DataOps Authorization").
14
+ *
15
+ * Content retrieval is deliberately absent. Once the view tree is mounted the
16
+ * official `grep`, `read`, `write`, and `edit` tools operate on it as an
17
+ * ordinary local path, so this capability does not duplicate them.
18
+ *
19
+ * @module @sparkelf/dsh-knowledge-base
20
+ */
21
+ import type { Context } from '@deepseek-ai/cordis';
22
+ import z from '@deepseek-ai/schemastery';
23
+ import { type KnowledgeConfig, type KnowledgeGrant, type KnowledgeGrantRequest, type KnowledgeRevokeRequest, type KnowledgeSshView, type KnowledgeStatus } from './protocol.ts';
24
+ import { type KnowledgeStorageSettings, type ViewDelta } from './views.ts';
25
+ import { type DeployPlanContext, type MineruBackend, type MineruTier } from './deploy.ts';
26
+ import { type RemoteHost, type RemoteRunner } from './remote.ts';
27
+ import { type ExtractionOutcome } from './extractionQueue.ts';
28
+ /** Cordis plugin name. */
29
+ export declare const name = "dsh-knowledge-base";
30
+ /**
31
+ * Host services this plugin needs.
32
+ *
33
+ * `credentials` resolves the session token per request so a rotated token
34
+ * reaches the next request without a restart. `webServer` serves the
35
+ * same-origin routes the Settings panel reads. The SSH transport is reached
36
+ * over HTTP at the ssh-manager's own route, so it is not injected here.
37
+ */
38
+ export declare const inject: string[];
39
+ /** Plugin configuration. */
40
+ export interface Config {
41
+ /**
42
+ * Base URL of the DataOps knowledge API. Empty disables the DataOps-backed
43
+ * status/index routes, leaving only the local deployment controls.
44
+ */
45
+ baseUrl: string;
46
+ /** Credential reference holding the DataOps session token. */
47
+ credentialRef: string;
48
+ /** Request timeout in milliseconds for DataOps API calls. */
49
+ timeoutMs: number;
50
+ /**
51
+ * Where documents are stored.
52
+ *
53
+ * `local` keeps them in the workspace's own directory (`mountPoint`): no mount, no
54
+ * sync, no remote round trip per read. `remote` keeps the synchronized shape, where
55
+ * the browsable pointer is a per-user view inside an NFS share refreshed from
56
+ * `actualRoot`. The two modes share listing, preview, edit, permission and audit and
57
+ * differ only in where the bytes live.
58
+ */
59
+ storageMode: 'local' | 'remote';
60
+ /**
61
+ * How a `remote` deployment reaches its share.
62
+ *
63
+ * Orthogonal to {@link storageMode}: a local deployment stores nothing remotely, so it
64
+ * ignores this. `nfs` is the original shape -- an export mounted on the Host at
65
+ * {@link mountPoint}. `sshfs` reaches the same kind of directory over SSH instead, which
66
+ * is what a host behind NAT needs: NFSv4 callbacks cannot traverse NAT, while an
67
+ * outbound SSH connection can. The rest of the plugin does not branch on this: both
68
+ * protocols end as a directory bound into the container at `mountPoint`.
69
+ */
70
+ storageProtocol: 'nfs' | 'sshfs';
71
+ /** Path inside the container where the user's view tree is mounted. */
72
+ mountPoint: string;
73
+ /** Absolute path of the authoritative tree on the storage host. */
74
+ actualRoot: string;
75
+ /** Absolute path of the per-user view tree on the storage host. */
76
+ viewRoot: string;
77
+ /** Comma-separated export spec of clients allowed to mount the NFS export. */
78
+ permitted: string;
79
+ /** Host serving the export, as an address or `address:/path` pseudo root. */
80
+ nfsServer: string;
81
+ /** SSHFS: remote host, as an address or hostname. */
82
+ sshHost: string;
83
+ /** SSHFS: login user on that host. */
84
+ sshUser: string;
85
+ /** SSHFS: TCP port the SSH daemon listens on. */
86
+ sshPort: number;
87
+ /**
88
+ * SSHFS: credential reference holding the private key.
89
+ *
90
+ * A reference, never the key itself: the panel reports only this name and whether it
91
+ * resolves, so the secret stays in the credential store and out of the settings file.
92
+ */
93
+ sshKeyRef: string;
94
+ /** SSHFS: absolute path of the shared directory on the remote host. */
95
+ sshRemotePath: string;
96
+ /**
97
+ * SSHFS: extra `-o` options for the mount.
98
+ *
99
+ * Kept as one string because that is how `sshfs -o` takes them, and the operator is
100
+ * the one who knows whether their host needs `allow_other` or a different identity
101
+ * file. The mount step always adds the reconnect options this deployment needs.
102
+ */
103
+ sshMountOptions: string;
104
+ /** MinerU quality tier to deploy. */
105
+ mineruTier: MineruTier;
106
+ /** MinerU small-model backend to deploy. */
107
+ mineruBackend: MineruBackend;
108
+ /** TCP port the MinerU API listens on where it is deployed. */
109
+ mineruPort: number;
110
+ /**
111
+ * MinerU's full base address, or `''` to derive it from `mineruPort`.
112
+ *
113
+ * Deployment-level fallback for the setting a caller can change on the settings page.
114
+ * Empty keeps the derivation, so a deployment that never sets this keeps working.
115
+ */
116
+ mineruBaseUrl: string;
117
+ /** Memory ceiling in MiB for the MinerU service, clamped to half the host's RAM. */
118
+ mineruMemoryMaxMiB: number;
119
+ }
120
+ /** Config schema; an invalid value fails the profile load loudly. */
121
+ export declare const Config: z<Config>;
122
+ /** Why a knowledge-base request failed, with a stable machine-routable code. */
123
+ export type KnowledgeErrorCode = 'not-configured' | 'unauthorized' | 'unreachable' | 'bad-response';
124
+ /** Typed knowledge-base failure. */
125
+ export declare class KnowledgeError extends Error {
126
+ readonly code: KnowledgeErrorCode;
127
+ constructor(message: string, code: KnowledgeErrorCode, options?: ErrorOptions);
128
+ }
129
+ /**
130
+ * The resolved layout, derived on the Host.
131
+ *
132
+ * There is no backend counterpart to ask: DataOps serves no knowledge config
133
+ * route, so the mounted layout the container was started with is the answer. A
134
+ * remote call here would only ever answer 404.
135
+ * @param config - resolved plugin config.
136
+ * @returns The layout the page renders.
137
+ */
138
+ export declare function localConfig(config: Config,
139
+ /**
140
+ * Whether the SSHFS credential currently resolves.
141
+ *
142
+ * Passed in rather than resolved here because this function is synchronous and called from
143
+ * a dozen places that never touch the SSH transport. A caller that shows the panel resolves
144
+ * it first; every other caller leaves it false, which only means "this answer does not
145
+ * claim a key".
146
+ */
147
+ sshKeyConfigured?: boolean): KnowledgeConfig & {
148
+ backendConfigured: boolean;
149
+ storageProtocol: 'nfs' | 'sshfs';
150
+ ssh: KnowledgeSshView;
151
+ };
152
+ /**
153
+ * Read the mounted tree's shape without consulting a backend.
154
+ *
155
+ * The count comes from the mounted directory itself, which is the same tree the
156
+ * model's grep and read tools see, so the number the page reports cannot drift
157
+ * from what retrieval actually reaches.
158
+ * @param config - resolved plugin config.
159
+ * @returns The visible file count, or null when the mount is absent.
160
+ */
161
+ export declare function localStatus(config: Config): Promise<KnowledgeStatus>;
162
+ /**
163
+ * Why the visible tree is not readable, when it is not.
164
+ *
165
+ * A null count used to be one condition and is really two, and the page told the
166
+ * reader the wrong one. It means either the mounted tree is absent, or the pointer
167
+ * at `mountPoint` has not been built yet -- a per-user symlink that only a view
168
+ * sync creates. The second is the ordinary state for a caller who has been granted
169
+ * nothing yet, and reporting it as "not mounted" sends the reader to inspect a
170
+ * mount that is working.
171
+ *
172
+ * The distinction is a filesystem fact, not a policy: whether the configured path
173
+ * exists at all, versus whether it is a pointer this plugin can build on demand.
174
+ */
175
+ export type KnowledgeMountState = 'mounted' | 'pointer-missing' | 'absent';
176
+ /** The fields a sync reports beyond the filesystem delta. */
177
+ export interface SyncReportFields {
178
+ /** The browsable path that was retargeted. */
179
+ mountPoint: string;
180
+ /** Whether the pointer now points at this caller's view. */
181
+ mountPointed: boolean;
182
+ /** The identity the view is keyed by. */
183
+ userId: string;
184
+ /** Human-readable name of the caller, when the backend reported one. */
185
+ label?: string;
186
+ /** Candidate paths considered. */
187
+ considered: number;
188
+ /** Paths DataOps resolved as readable. */
189
+ resolved: number;
190
+ /** The view directory this caller's files are linked into. */
191
+ viewPath: string;
192
+ }
193
+ /**
194
+ * Classify why the browsable path does or does not yield a file count.
195
+ *
196
+ * Reads the configured path rather than inferring from the count, because a count
197
+ * of null is compatible with both failure modes and cannot tell them apart.
198
+ * @param config - resolved plugin config.
199
+ * @returns The state, in order of increasing severity.
200
+ */
201
+ export declare function classifyMount(config: Config): Promise<KnowledgeMountState>;
202
+ /**
203
+ * Rebuild the caller's visible tree and point the browsable path at it.
204
+ *
205
+ * Shared by the explicit `views.sync` route and the status route's first-load
206
+ * repair, so both take exactly the same path: one implementation means the
207
+ * automatic case cannot drift from the button the reader can press.
208
+ *
209
+ * It is idempotent and writes nothing when the resolved set is unchanged, which is
210
+ * what makes it safe to run without being asked -- `syncView` returns
211
+ * `unchanged > 0, linked: []` in that case.
212
+ *
213
+ * The decision stays with DataOps: the resolve route is self-scoped, and this
214
+ * function only links what that answer allows.
215
+ * @param host - the plugin context, for the backend calls.
216
+ * @param config - resolved plugin config.
217
+ * @param paths - candidate relative paths; omitted means walk the authoritative root.
218
+ * @returns The sync report, or a refusal the caller should relay.
219
+ */
220
+ export declare function syncCallerView(host: Context, config: Config, paths?: string[]): Promise<(ViewDelta & SyncReportFields & {
221
+ status: number;
222
+ error?: never;
223
+ }) | {
224
+ status: number;
225
+ error: string;
226
+ }>;
227
+ /**
228
+ * Enumerate every file under the authoritative root, as relative paths.
229
+ *
230
+ * The view builder needs candidate paths to ask the permission store about. When a
231
+ * caller does not supply them, this walks the tree and produces exactly the set a
232
+ * view could ever hold: hidden names are skipped, matching what the explorer shows
233
+ * and keeping editor scratch files out of every user's view.
234
+ * @param root - the authoritative directory.
235
+ * @returns Relative paths of the files under it.
236
+ */
237
+ export declare function listRelativeFiles(root: string): Promise<string[]>;
238
+ /**
239
+ * One directory's measured shape, for the indexing table.
240
+ *
241
+ * Every field is a fact read from the filesystem: the count, the newest
242
+ * modification instant, and whether the caller could read it. Nothing here is an
243
+ * estimate of a job state, because this plugin has no index job to report.
244
+ */
245
+ export interface DirectoryShape {
246
+ /** Path relative to the mount point; an empty string is the mount itself. */
247
+ relPath: string;
248
+ /** Files below this directory, counted recursively. */
249
+ fileCount: number;
250
+ /** Newest modification instant in Unix ms, absent when nothing could be read. */
251
+ mtimeMs?: number;
252
+ /** Whether this directory's own entries could be listed. */
253
+ readable: boolean;
254
+ /**
255
+ * Whether some part of this subtree could not be read, making `fileCount` a
256
+ * lower bound rather than a total.
257
+ *
258
+ * Separate from {@link readable} because the two describe different things: a
259
+ * directory can be listable itself while containing a subdirectory that is not.
260
+ * Rolling both into one flag would either call a readable directory unreadable
261
+ * or present a partial count as a whole one.
262
+ */
263
+ partial: boolean;
264
+ }
265
+ /**
266
+ * Report the mount's per-directory shape.
267
+ *
268
+ * The indexing page needs rows, and the only true rows available are directories
269
+ * of the mounted tree: this plugin has no index of its own, so there is no
270
+ * per-directory index status to report and none is invented. What is measurable
271
+ * is how many files sit below each directory, when the newest one changed, and
272
+ * whether the caller can read it.
273
+ *
274
+ * Only the first level is listed. A row per every nested directory would be a
275
+ * tree dump rather than a table, and the children of an expanded directory are
276
+ * already reachable from the file tree beside this page.
277
+ *
278
+ * Hidden entries are skipped, matching every other walk in this module: they are
279
+ * editor scratch files rather than knowledge documents.
280
+ * @param root - the directory to report on, normally the mount point.
281
+ * @returns One row per visible child directory, plus the root's own row.
282
+ */
283
+ export declare function listDirectoryShapes(root: string): Promise<DirectoryShape[]>;
284
+ /**
285
+ * Fetch the grant table from the backend.
286
+ *
287
+ * The route answers `{ grants: [...] }`, not a bare array. Reading it as an array
288
+ * would hand a `.map` call an object -- which is what the empty panel was
289
+ * reporting as "map is not a function" -- so both shapes are accepted here: a
290
+ * backend that answers the bare array keeps working, and the shape this
291
+ * deployment returns is the one the panel sees.
292
+ *
293
+ * @param ctx - Host context.
294
+ /**
295
+ * Fetch the grant table from the backend.
296
+ *
297
+ * Two translations happen here, and both are the reason this is not a one-line
298
+ * relay:
299
+ *
300
+ * 1. The route answers `{ grants: [...] }`, not a bare array. Reading it as an
301
+ * array handed a `.map` an object, which is what the empty panel reported as
302
+ * "map is not a function".
303
+ * 2. A backend grant names its document `resourceId` and does not carry a
304
+ * `kind`, `grantedBy` or `grantedAt`. Those are this plugin's own vocabulary,
305
+ * so they are derived here: the path is the resource id, a path with an
306
+ * extension is a file, and the grantor and timestamp fall back to neutral
307
+ * answers rather than being invented.
308
+ *
309
+ * @param ctx - Host context.
310
+ * @param config - resolved plugin config.
311
+ * @returns the grants, empty when the body carries none.
312
+ */
313
+ export declare function fetchGrants(ctx: Context, config: Config): Promise<KnowledgeGrant[]>;
314
+ /** One match the search surface can render. */
315
+ export interface KnowledgeHit {
316
+ /** Path relative to the mount point, which is also the grant's resource id. */
317
+ relPath: string;
318
+ /** File name alone, for a compact row. */
319
+ name: string;
320
+ /** Whether the term matched the file's text rather than only its name. */
321
+ inContent: boolean;
322
+ /** Matching line, when the term matched inside the file. */
323
+ line?: string;
324
+ /** 1-based line number of {@link line}. */
325
+ lineNumber?: number;
326
+ /** Last-modified instant in Unix ms, when the stat succeeded. */
327
+ mtimeMs?: number;
328
+ }
329
+ /**
330
+ * Search the mounted tree by file name and text.
331
+ *
332
+ * This is a direct walk rather than an index, matching how retrieval works here:
333
+ * the tools read the mount with grep and read, so there is no separate index whose
334
+ * contents could disagree with the files. Only text files are opened, and only up
335
+ * to a byte budget, so a search over a large mount stays bounded.
336
+ * @param root - the mount point to walk.
337
+ * @param term - case-insensitive term to look for.
338
+ * @param limit - maximum hits to return.
339
+ * @returns The hits, name matches first.
340
+ */
341
+ export declare function searchKnowledge(root: string, term: string, limit?: number): Promise<KnowledgeHit[]>;
342
+ /** One directory entry, as the page's own listing renders it. */
343
+ export interface KnowledgeEntry {
344
+ /** Path relative to the mount point. */
345
+ relPath: string;
346
+ /** Name alone. */
347
+ name: string;
348
+ /** Whether this entry is a directory. */
349
+ isDir: boolean;
350
+ /** Last-modified instant in Unix ms, when it could be read. */
351
+ mtimeMs?: number;
352
+ /** Size in bytes for files. */
353
+ size?: number;
354
+ /**
355
+ * Whether the calling user may read this path; `true` on every row a permission
356
+ * check produced.
357
+ *
358
+ * A row carries the file's name, size and modification time, so a denied path
359
+ * cannot be published with a "not yours" marker on it: the marker is not what
360
+ * discloses the file. Denied and undecided paths are removed from the result
361
+ * instead (see {@link listDirectoryWithPermissions}), which is also why the flag
362
+ * is redundant here and only kept for a row that was explicitly resolved.
363
+ */
364
+ readable?: boolean;
365
+ /** Number of direct children, for a directory row. */
366
+ childCount?: number;
367
+ /**
368
+ * Whether a reader exists for this file's format.
369
+ *
370
+ * Set on file rows only. The page uses it to decide whether "extract markdown" is a
371
+ * meaningful action for this row, so the menu does not have to know the extractor's
372
+ * extension list.
373
+ */
374
+ canExtract?: boolean;
375
+ /**
376
+ * Whether this file's `.md` sibling already exists.
377
+ *
378
+ * Set on file rows only, and only for formats that have an extractor. Lets the page
379
+ * say "already extracted" rather than offering a retry that would be refused.
380
+ */
381
+ hasMarkdown?: boolean;
382
+ }
383
+ /**
384
+ * List one directory level with each child's modification time.
385
+ * @param root - the mount point, used to make paths relative.
386
+ * @param dir - absolute directory to read.
387
+ * @returns The entries, directories first then files, each in name order.
388
+ */
389
+ export declare function listDirectory(root: string, dir: string): Promise<KnowledgeEntry[]>;
390
+ /**
391
+ * List one directory level, keeping only the entries DataOps resolved as readable.
392
+ *
393
+ * The listing and the permission check are separate concerns, so this composes
394
+ * them rather than growing {@link listDirectory}: a caller that only needs the
395
+ * filesystem facts (the preview, the search) keeps using that one.
396
+ *
397
+ * Removing a row rather than tagging it is the whole point. A row carries the
398
+ * file's name, size and modification time, so returning a denied path with a "not
399
+ * yours" marker on it discloses precisely what the check was meant to withhold:
400
+ * the marker is not what reveals the file, and a reader can still fetch its bytes.
401
+ * Proving that a name is off limits is not a reason to show the name.
402
+ *
403
+ * Every unresolved case fails closed. An empty base URL, a missing caller id, and a
404
+ * resolve call that throws all drop the row, because "the answer could not be
405
+ * obtained" is not a licence to publish. Answering the unknown case with the full
406
+ * listing is what made an unreachable backend publish every file.
407
+ * @param ctx - Host context.
408
+ * @param config - resolved plugin config.
409
+ * @param root - the mount point.
410
+ * @param dir - absolute directory to read.
411
+ * @param userId - the caller whose view the answer is about.
412
+ * @returns The rows DataOps resolved as readable, in listing order.
413
+ */
414
+ export declare function listDirectoryWithPermissions(ctx: Context, config: Config, root: string, dir: string, userId: string | undefined): Promise<KnowledgeEntry[]>;
415
+ /**
416
+ * Read one knowledge file for the preview.
417
+ *
418
+ * The preview must not depend on a conversation session. The listing is served by
419
+ * this plugin's own route and needs none, so a session-backed read would leave the
420
+ * page able to list files and unable to open them, which reads as a broken preview
421
+ * rather than as a missing session. Containment is checked here because the path
422
+ * arrives from the browser.
423
+ * @param ctx - Host context.
424
+ * @param config - resolved plugin config.
425
+ * @param relPath - path relative to the mount point.
426
+ * @returns The file's text, truncation flag and modification instant.
427
+ */
428
+ export declare function readKnowledgeFile(ctx: Context, config: Config, relPath: string): Promise<{
429
+ kind: 'text';
430
+ content: string;
431
+ truncated: boolean;
432
+ mtimeMs?: number;
433
+ } | {
434
+ kind: 'binary';
435
+ size: number;
436
+ }>;
437
+ /**
438
+ * Read one file as raw bytes for a save-to-disk.
439
+ *
440
+ * Separate from {@link readKnowledgeFile}, which is shaped for the preview pane: that one
441
+ * truncates a large text file and refuses a binary one, because the pane cannot show
442
+ * more. A download has the opposite requirement -- the reader asked for THIS file, so it
443
+ * is returned whole or refused with a reason, never silently shortened.
444
+ *
445
+ * Base64 rather than a byte array: the answer travels as JSON, and a `number[]` for a
446
+ * megabyte file is several times the payload and slower to parse on both ends.
447
+ * @param ctx - the plugin context.
448
+ * @param config - resolved plugin config.
449
+ * @param relPath - the file to read, relative to the mount point.
450
+ * @returns The file's bytes and a suggested name, or a refusal reason.
451
+ */
452
+ export declare function readKnowledgeFileForDownload(ctx: Context, config: Config, relPath: string): Promise<{
453
+ name: string;
454
+ base64: string;
455
+ size: number;
456
+ mime: string;
457
+ }>;
458
+ /** One row the permission surface offers as a principal. */
459
+ export interface PrincipalOption {
460
+ /** Value the grant must carry: a user's UUID, or a code for every other kind. */
461
+ id: string;
462
+ /** Human-facing name, falling back to the id when the backend sends none. */
463
+ label: string;
464
+ /** Login name, for users only: two accounts can share a display name. */
465
+ username?: string;
466
+ /**
467
+ * Role codes this principal belongs to.
468
+ *
469
+ * Users only, and only when the directory reported it. The permission overview needs
470
+ * this to answer "what does this PERSON have": a role grant confers access on every
471
+ * member, so without the membership a reader whose access is entirely role-derived
472
+ * would look like they hold nothing. Absent means the directory did not say -- which
473
+ * the page reports as unavailable inheritance rather than as no inheritance.
474
+ */
475
+ roleCodes?: string[];
476
+ }
477
+ /**
478
+ * Read the principals of one kind from the backend.
479
+ *
480
+ * The permission store matches a user by primary key and every other kind by
481
+ * `code`, and there is nowhere else a valid value can come from. Reading the
482
+ * directory here is what keeps the surface from asking an operator to transcribe
483
+ * a UUID.
484
+ *
485
+ * Failures are reported as an empty list rather than thrown: the directories are
486
+ * a convenience over a field that also accepts a typed value, so a caller whose
487
+ * account may not read them can still grant by hand.
488
+ * @param ctx - Host context.
489
+ * @param config - resolved plugin config.
490
+ * @param kind - principal kind to list.
491
+ * @returns The options, in the order the backend returned them.
492
+ */
493
+ export declare function fetchPrincipals(ctx: Context, config: Config, kind: KnowledgeGrantRequest['principalType']): Promise<PrincipalOption[]>;
494
+ /** The calling user, as DataOps describes them. */
495
+ export interface KnowledgeCaller {
496
+ /** Stable user id. Keys the caller's view directory, so it must be a safe name. */
497
+ userId: string;
498
+ /** Human-facing name, for reporting. */
499
+ label: string;
500
+ /** Role codes the backend reported. Kept for reporting, not for authorization. */
501
+ roleCodes: string[];
502
+ /**
503
+ * DataOps department code, when the caller belongs to one.
504
+ *
505
+ * Read for LAYOUT only: it selects which `department/<code>/` subtree the view
506
+ * links, and never decides whether a path may be read — {@link canReadPath}
507
+ * stays the single authority for that. A caller with no department simply gets
508
+ * no department bucket.
509
+ */
510
+ departmentCode?: string;
511
+ /** Department display name, for reporting beside the bucket. */
512
+ departmentName?: string;
513
+ /**
514
+ * The caller's organization path, root first, as DataOps reported it.
515
+ *
516
+ * Layout only, exactly like {@link departmentCode}: it decides how deep the
517
+ * department bucket nests and never whether a path may be read. Empty when the
518
+ * backend does not report it (an older DataOps), in which case the bucket keeps its
519
+ * single-level shape rather than disappearing.
520
+ */
521
+ departmentPathCodes?: string[];
522
+ /** The organization path's display names, index-aligned with the codes. */
523
+ departmentPathNames?: string[];
524
+ }
525
+ /**
526
+ * Read a `{ code, name }` pair DataOps nests under the profile.
527
+ *
528
+ * The organization path arrays are read here too, but each is independent: a backend
529
+ * that reports `code`/`name` without the arrays (every deployment today) still yields a
530
+ * usable department, and the caller simply gets a single-level bucket. That is why the
531
+ * two are separate fields rather than one required structure — the old shape must keep
532
+ * working without a version check.
533
+ *
534
+ * A path is only used when the two arrays agree in length and the leaf matches `code`.
535
+ * A mismatch means the profile contradicts itself, and half of a contradicting pair
536
+ * would place documents under a folder the reader cannot recognize.
537
+ *
538
+ * @param value - the raw field, which is absent on older backends.
539
+ * @returns The pair with a non-empty code plus any usable path, or undefined.
540
+ */
541
+ export declare function readDepartmentOf(value: unknown): {
542
+ code: string;
543
+ name: string;
544
+ pathCodes?: string[];
545
+ pathNames?: string[];
546
+ } | undefined;
547
+ export declare function fetchCaller(ctx: Context, config: Config): Promise<KnowledgeCaller>;
548
+ /**
549
+ * Ask DataOps whether the CALLER may read one path.
550
+ *
551
+ * The decision is the backend's, not this plugin's: the route is self-scoped, so
552
+ * the answer is about the credential's own user and this function cannot be used
553
+ * to ask about anyone else. That is what keeps a per-user view from becoming a
554
+ * second permission implementation.
555
+ *
556
+ * A failed request answers `false` rather than throwing, so one unreadable path
557
+ * cannot leave a view half-built. An unreachable backend therefore produces an
558
+ * empty view instead of a wrong one.
559
+ * @param ctx - Host context.
560
+ * @param config - resolved plugin config.
561
+ * @param relPath - path relative to the authoritative root.
562
+ * @returns Whether the caller may read it.
563
+ */
564
+ /**
565
+ * The root resource id one knowledge-base-wide grant is stored under.
566
+ *
567
+ * A grant needs a non-empty `resourceId` (DataOps rejects an empty one with
568
+ * "资源ID不能为空"), so "the whole knowledge base" is named explicitly rather than by an
569
+ * empty string. A document can never have this id, because document ids come from titles
570
+ * and never start with `@`.
571
+ */
572
+ export declare const KB_ROOT_RESOURCE_ID = "@kb-root";
573
+ /**
574
+ * The ancestor chain for one knowledge path, nearest parent first.
575
+ *
576
+ * DataOps resolves a permission by matching the resource's own id OR any ancestor named
577
+ * here, so passing this chain is what turns "grant the knowledge base once" into "every
578
+ * document inside it inherits that grant". Without it each file needs its own row, which
579
+ * is why a freshly created document used to look unreadable to an administrator.
580
+ *
581
+ * The chain walks the path's parents (`a/b/c.md` → `a/b` → `a`) and ends at the root id,
582
+ * so a grant on any level applies to everything beneath it and the most specific level
583
+ * still wins for a deny.
584
+ * @param relPath - the path being authorised; empty or the root id stands for the root.
585
+ * @returns Ancestor references, nearest first, root last.
586
+ */
587
+ export declare function knowledgeAncestorsFor(relPath: string): Array<{
588
+ resourceType: string;
589
+ resourceId: string;
590
+ }>;
591
+ export declare function canReadPath(ctx: Context, config: Config, relPath: string): Promise<boolean>;
592
+ /**
593
+ * Ask DataOps whether the CALLER may write one path.
594
+ *
595
+ * The same self-scoped resolve route {@link canReadPath} uses, read for a different
596
+ * field. A move needs this and not merely read: it destroys the source path, so a
597
+ * caller who can only read must not be able to relocate a document out from under
598
+ * the people who own it. The backend distinguishes the two (`canRead` and
599
+ * `canWrite` are separate conclusions drawn from the same grants), and this reads
600
+ * its answer rather than re-deriving one from the other.
601
+ *
602
+ * A failed request answers `false` rather than throwing, so an unreachable backend
603
+ * refuses the move instead of authorizing it.
604
+ * @param ctx - Host context.
605
+ * @param config - resolved plugin config.
606
+ * @param relPath - path relative to the authoritative root.
607
+ * @returns Whether the caller may write it.
608
+ */
609
+ export declare function canWritePath(ctx: Context, config: Config, relPath: string): Promise<boolean>;
610
+ /**
611
+ * Largest document the create and upload routes will write.
612
+ *
613
+ * A knowledge document is text a person reads, not a data dump, and the route
614
+ * buffers the body in memory before writing it. The ceiling is what keeps one
615
+ * request from taking the Host's heap: without it a caller with write access could
616
+ * post an arbitrarily large body, because nothing downstream limits the size of a
617
+ * file on the share.
618
+ */
619
+ export declare const MAX_KNOWLEDGE_WRITE_BYTES = 1048576;
620
+ /**
621
+ * How long the template list may take before the surface gives up.
622
+ *
623
+ * Shorter than the plugin's general timeout on purpose: a template list decorates a
624
+ * page, and the shared backend can be busy with unrelated work for far longer than
625
+ * a reader should wait to see some cards.
626
+ */
627
+ export declare const TEMPLATE_READ_TIMEOUT_MS = 5000;
628
+ /**
629
+ * A refusal from the write path, with a code the route maps to a status.
630
+ *
631
+ * Kept apart from {@link KnowledgeError}, whose codes describe how a TRANSPORT
632
+ * call ended. These say why a write was not performed, which is a different
633
+ * question with different statuses: a name already taken is a conflict, a path
634
+ * outside the root is a bad request, and a document over the ceiling is too large.
635
+ */
636
+ export declare class KnowledgeWriteError extends Error {
637
+ readonly code: 'bad-path' | 'exists' | 'too-large' | 'write-failed';
638
+ constructor(message: string, code: KnowledgeWriteError['code'], options?: ErrorOptions);
639
+ }
640
+ /**
641
+ * Create one document or directory in the authoritative tree.
642
+ *
643
+ * The path is written under `actualRoot` rather than under the caller's own view
644
+ * directory, and that choice is the whole design:
645
+ *
646
+ * - The view is NOT storage. `syncView` mirrors the resolved grant set onto it, so
647
+ * a file that appears there is removed by the next sync unless DataOps resolves
648
+ * it as readable. Measured: a document written into a view was gone after the
649
+ * next `syncView` (`delta.unlinked` named it). Writing there would hand the
650
+ * reader a document that any later permission change silently deletes.
651
+ * - `actual/` is the authoritative tree, so a document written there survives, and
652
+ * the grant system then decides who sees it. Creating is therefore SHARED by
653
+ * construction: the creator needs write access, and everyone DataOps resolves as
654
+ * a reader will find the document in their view at their next sync. That is what
655
+ * a knowledge base is for, and it is the semantic the product already has.
656
+ *
657
+ * There is deliberately no `userId` component in the path. `actual/` is shared and
658
+ * has no per-user dimension; inventing `actual/<uid>/...` would create a shape the
659
+ * rest of the plugin does not understand, because a view is built from grants and
660
+ * not from a directory per user.
661
+ *
662
+ * The write is exclusive (`wx`), so the kernel refuses to replace an existing file.
663
+ * A stat-then-write would leave a window in which another caller's document could
664
+ * be overwritten, and the check would still be the thing that was raced.
665
+ * @param config - resolved plugin config.
666
+ * @param relPath - destination, relative to the authoritative root.
667
+ * @param options - `directory` creates a directory; `content` is the file body.
668
+ * @returns The path written.
669
+ * @throws KnowledgeError when the path escapes the root, exists, or cannot be written.
670
+ */
671
+ export declare function createKnowledgeEntry(config: Config, relPath: string, options: {
672
+ directory: boolean;
673
+ content?: string;
674
+ bytes?: Uint8Array;
675
+ }): Promise<{
676
+ relPath: string;
677
+ kind: 'file' | 'directory';
678
+ }>;
679
+ /**
680
+ * Extract an uploaded document's text into a `.md` sibling.
681
+ *
682
+ * Called after the original is written, never before: the bytes the user chose
683
+ * are the deliverable, and an extraction that fails must not take them down with
684
+ * it. The sibling keeps the original extension in its name (`报告.docx.md`), so it
685
+ * cannot collide with a handwritten `报告.md` and a reader can still tell which
686
+ * document it came from.
687
+ *
688
+ * @param config - resolved plugin config, for the knowledge root and MinerU port.
689
+ * @param relPath - the uploaded document's path, relative to the knowledge root.
690
+ * @returns What happened, or undefined when the format needs no extraction.
691
+ */
692
+ export declare function extractBeside(config: Config, relPath: string, mineruUrl?: string): Promise<{
693
+ markdown: string;
694
+ via?: string;
695
+ reason?: string;
696
+ } | undefined>;
697
+ /**
698
+ * Translate {@link extractBeside}'s answer into a queue terminal state.
699
+ *
700
+ * The extractor reports three different outcomes through two fields, and they mean
701
+ * different things to a reader deciding what to do next:
702
+ *
703
+ * - `markdown` set, no `reason` — the sibling was written (`succeeded`).
704
+ * - `markdown` set WITH a `reason` — the text was produced but writing the sibling was
705
+ * refused, typically because it already exists. The reason is kept so a retry can
706
+ * report why nothing changed (`failed`, with the refusal verbatim).
707
+ * - `markdown` empty, `reason` set — no authority could read the document; this is the
708
+ * `no-extractor-succeeded: …` case a reader retries after an upgrade.
709
+ *
710
+ * An `undefined` answer means the format has no extractor at all, which the queue
711
+ * normally prevents by asking `isExtractable` first; it is mapped for the caller that
712
+ * bypassed that gate rather than left to throw.
713
+ *
714
+ * @param outcome - what {@link extractBeside} returned.
715
+ * @returns the state the queue records.
716
+ */
717
+ export declare function extractionOutcomeOf(outcome: {
718
+ markdown: string;
719
+ via?: string;
720
+ reason?: string;
721
+ } | undefined): ExtractionOutcome;
722
+ /**
723
+ * MinerU's base URL for this deployment, or undefined when it is not configured.
724
+ *
725
+ * @param config - resolved plugin config.
726
+ * @returns `http://127.0.0.1:<port>` when the service is expected, else undefined.
727
+ */
728
+ export declare function mineruUrlFor(config: Config): string | undefined;
729
+ /**
730
+ * Ask DataOps whether the CALLER may manage grants.
731
+ *
732
+ * The question is put to the authority instead of being inferred here. The route
733
+ * read below is guarded by exactly `data:access-grant:manage` -- the permission the
734
+ * grant and revoke calls require -- so the backend's own answer to the read is the
735
+ * answer to "may this caller manage grants": a `200` means the permission is held,
736
+ * a `403` means it is not. Nothing about the body is used, and the filter keeps the
737
+ * response to this resource type rather than downloading the whole grant table.
738
+ *
739
+ * Every other outcome denies. An unreachable backend, a timeout, a rejected token
740
+ * and a malformed body all leave the permission unproven, and an unproven permission
741
+ * must not publish a management surface: an administrator who is wrongly denied
742
+ * retries after the backend recovers, while a non-administrator who is wrongly
743
+ * allowed keeps an entry point the backend would refuse anyway.
744
+ *
745
+ * Role names were considered as the judge and rejected on measured evidence, so that
746
+ * the reason survives the next reader. `/auth/me` returns `roles[].code` as the
747
+ * role's DISPLAY name -- measured 2026-10-16 against the running backend, which
748
+ * answers `管理员` and `超级管理员` -- while `SUPER_ADMIN` / `ADMIN` are the separate
749
+ * `system_key` values that route never exposes. Matching the constant names would
750
+ * therefore deny every real administrator, and matching display names would let a
751
+ * role rename silently change who may manage grants. The enforced permission point
752
+ * has neither problem, which is why it is the only judge used here.
753
+ * @param ctx - Host context.
754
+ * @param config - resolved plugin config.
755
+ * @returns Whether the caller holds the grant-management permission.
756
+ */
757
+ export declare function canManageGrants(ctx: Context, config: Config): Promise<boolean>;
758
+ /**
759
+ * Create one grant in the backend permission store.
760
+ *
761
+ * The plugin names the principal and the path; DataOps decides whether the
762
+ * principal exists and whether the caller may write the grant at all. That split
763
+ * is why this is a thin relay rather than a policy of its own.
764
+ * @param ctx - Host context.
765
+ * @param config - resolved plugin config.
766
+ * @param body - the grant to create.
767
+ * @returns The backend's acknowledgement.
768
+ */
769
+ export declare function createGrant(ctx: Context, config: Config, body: KnowledgeGrantRequest): Promise<unknown>;
770
+ /** Remove one grant from the backend permission store. */
771
+ export declare function revokeGrant(ctx: Context, config: Config, body: KnowledgeRevokeRequest): Promise<unknown>;
772
+ /**
773
+ * Overlay the operator's stored choices onto the deployment configuration.
774
+ *
775
+ * Two sources answer "which storage shape": the profile, which is fixed when the container is
776
+ * created, and `.kb-settings.json`, which the panel writes. The file wins, because it is the
777
+ * one the operator can still change -- a saved choice that the running process ignored would
778
+ * be the same dead control this route exists to replace.
779
+ *
780
+ * Only the fields the panel owns are overlaid. `mountPoint`, `actualRoot`, `viewRoot` and
781
+ * `permitted` stay as the deployment declares them: those are bind mounts and export specs,
782
+ * decided when the container starts, so a file that changed them would point the plugin at
783
+ * paths nothing serves.
784
+ * @param config - the deployment configuration.
785
+ * @param stored - what the settings file holds.
786
+ * @returns The configuration the request should use.
787
+ */
788
+ export declare function withStoredStorage(config: Config, stored: KnowledgeStorageSettings): Config;
789
+ /** Build the substitution context for the deployment recipes. */
790
+ export declare function planContext(config: Config): DeployPlanContext;
791
+ /** One recorded step execution. */
792
+ export interface StepRun {
793
+ stepId: string;
794
+ hostId: string;
795
+ state: 'running' | 'succeeded' | 'failed';
796
+ startedAt: string;
797
+ finishedAt: string | null;
798
+ /** Human-readable transcript assembled from per-command results. */
799
+ log: string[];
800
+ }
801
+ /**
802
+ * Register the knowledge-base routes and tools.
803
+ * @param ctx - Host context carrying credentials, tools, and the Web server.
804
+ * @param config - resolved plugin configuration.
805
+ */
806
+ /** Provider name this plugin registers its bundle skill under. */
807
+ export declare const KNOWLEDGE_SKILL_PROVIDER = "dsh-knowledge-base";
808
+ /** Name the model sees in the skill catalog. */
809
+ export declare const KNOWLEDGE_SKILL_NAME = "knowledge-base";
810
+ /**
811
+ * Short description shown in the skill list.
812
+ *
813
+ * Kept beside the SKILL.md body rather than parsed out of its frontmatter: the
814
+ * catalog answers `list()` without reading any skill content, so the text has to
815
+ * be available before the file is opened.
816
+ */
817
+ export declare const KNOWLEDGE_SKILL_DESCRIPTION = "Read and change the shared knowledge base: browse, search, create, upload, rename, move and delete documents in the \u5168\u5C40 / \u90E8\u95E8 / \u4E2A\u4EBA trees.";
818
+ /**
819
+ * Where this package's SKILL.md lives, from either build layout.
820
+ *
821
+ * The build emits `lib/index.js`, so the file sits one level up; a source run
822
+ * resolves from `src/`, which is also one level up. Both answer the same path,
823
+ * which is why this does not need to believe in a single layout.
824
+ *
825
+ * @returns absolute path of the SKILL.md this plugin ships.
826
+ */
827
+ export declare function knowledgeSkillPath(): string;
828
+ /**
829
+ * The skill provider that teaches the model how to use this knowledge base.
830
+ *
831
+ * One bundled skill, read from disk on `get()` rather than kept in memory: the
832
+ * body is documentation, and a host that never asks for it should not pay for
833
+ * holding it.
834
+ *
835
+ * @returns a provider the skill registry can list and read.
836
+ */
837
+ export declare const KNOWLEDGE_SKILL_RANK = 640;
838
+ /** Both a human and the model may invoke this skill. */
839
+ export declare const KNOWLEDGE_SKILL_INVOCATION: {
840
+ readonly modelInvocable: true;
841
+ readonly userInvocable: true;
842
+ };
843
+ /**
844
+ * The skill provider that teaches the model how to use this knowledge base.
845
+ *
846
+ * One bundled skill, read from disk on `get()` rather than kept in memory: the
847
+ * body is documentation, and a host that never asks for it should not pay for
848
+ * holding it.
849
+ *
850
+ * `rank` places it after the bundled GenUI skill (600): a task that needs both
851
+ * wants the fence reference first, because the knowledge skill's own answers are
852
+ * mostly prose and shell commands.
853
+ *
854
+ * @returns a provider the skill registry can list and read.
855
+ */
856
+ export declare function knowledgeBaseSkillProvider(): {
857
+ readonly name: string;
858
+ readonly list: (options: {
859
+ readonly cwd?: string;
860
+ readonly signal?: AbortSignal;
861
+ }) => Promise<readonly SkillCandidateLike[]>;
862
+ readonly get: (candidate: SkillCandidateLike, options: {
863
+ readonly cwd?: string;
864
+ readonly signal?: AbortSignal;
865
+ }) => Promise<SkillDefinitionLike | undefined>;
866
+ };
867
+ /**
868
+ * One catalog entry, shaped like the host's `SkillCandidate`.
869
+ *
870
+ * Declared here instead of imported: this plugin does not depend on the skill
871
+ * package (the registry is an optional injection), so the structural contract is
872
+ * restated rather than turning an optional neighbour into a build requirement. The
873
+ * registry merges these by the fields it reads, so a shape that satisfies them is
874
+ * the whole contract.
875
+ */
876
+ export interface SkillCandidateLike {
877
+ readonly name: string;
878
+ readonly description: string;
879
+ readonly invocation: {
880
+ readonly modelInvocable: boolean;
881
+ readonly userInvocable: boolean;
882
+ };
883
+ readonly source: 'bundled';
884
+ readonly provider: string;
885
+ readonly path: string;
886
+ readonly resourceBase: {
887
+ readonly kind: 'directory';
888
+ readonly path: string;
889
+ };
890
+ readonly rank: number;
891
+ readonly locator: string;
892
+ }
893
+ /** One loaded skill, shaped like the host's `SkillDefinition`. */
894
+ export interface SkillDefinitionLike extends SkillCandidateLike {
895
+ readonly content: string;
896
+ }
897
+ export declare function apply(ctx: Context, deploymentConfig: Config): void;
898
+ /** Re-exported for tests and for callers that need the host shape. */
899
+ export type { RemoteHost, RemoteRunner };