@docsworth/cli 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.
Files changed (103) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +238 -2
  3. package/dist/cli.d.ts +11 -0
  4. package/dist/cli.js +458 -0
  5. package/dist/core/acl.d.ts +37 -0
  6. package/dist/core/acl.js +136 -0
  7. package/dist/core/adopt.d.ts +68 -0
  8. package/dist/core/adopt.js +665 -0
  9. package/dist/core/audit-log.d.ts +133 -0
  10. package/dist/core/audit-log.js +290 -0
  11. package/dist/core/blocks.d.ts +35 -0
  12. package/dist/core/blocks.js +417 -0
  13. package/dist/core/codeowners.d.ts +62 -0
  14. package/dist/core/codeowners.js +133 -0
  15. package/dist/core/config.d.ts +101 -0
  16. package/dist/core/config.js +175 -0
  17. package/dist/core/coverage.d.ts +25 -0
  18. package/dist/core/coverage.js +41 -0
  19. package/dist/core/duration.d.ts +20 -0
  20. package/dist/core/duration.js +60 -0
  21. package/dist/core/edits.d.ts +13 -0
  22. package/dist/core/edits.js +20 -0
  23. package/dist/core/errors.d.ts +11 -0
  24. package/dist/core/errors.js +13 -0
  25. package/dist/core/frontmatter.d.ts +29 -0
  26. package/dist/core/frontmatter.js +49 -0
  27. package/dist/core/gate.d.ts +17 -0
  28. package/dist/core/gate.js +25 -0
  29. package/dist/core/gfm.d.ts +13 -0
  30. package/dist/core/gfm.js +244 -0
  31. package/dist/core/github.d.ts +152 -0
  32. package/dist/core/github.js +234 -0
  33. package/dist/core/host.d.ts +125 -0
  34. package/dist/core/host.js +22 -0
  35. package/dist/core/identity.d.ts +39 -0
  36. package/dist/core/identity.js +127 -0
  37. package/dist/core/init.d.ts +25 -0
  38. package/dist/core/init.js +86 -0
  39. package/dist/core/links.d.ts +82 -0
  40. package/dist/core/links.js +345 -0
  41. package/dist/core/lint.d.ts +16 -0
  42. package/dist/core/lint.js +149 -0
  43. package/dist/core/mcp/audit-transport.d.ts +16 -0
  44. package/dist/core/mcp/audit-transport.js +238 -0
  45. package/dist/core/mcp/server.d.ts +19 -0
  46. package/dist/core/mcp/server.js +216 -0
  47. package/dist/core/mcp/text.d.ts +14 -0
  48. package/dist/core/mcp/text.js +43 -0
  49. package/dist/core/mirror-view.d.ts +68 -0
  50. package/dist/core/mirror-view.js +367 -0
  51. package/dist/core/new-page.d.ts +52 -0
  52. package/dist/core/new-page.js +49 -0
  53. package/dist/core/policy.d.ts +10 -0
  54. package/dist/core/policy.js +30 -0
  55. package/dist/core/polish.d.ts +42 -0
  56. package/dist/core/polish.js +1000 -0
  57. package/dist/core/printable.d.ts +4 -0
  58. package/dist/core/printable.js +16 -0
  59. package/dist/core/references.d.ts +87 -0
  60. package/dist/core/references.js +1744 -0
  61. package/dist/core/repo.d.ts +46 -0
  62. package/dist/core/repo.js +118 -0
  63. package/dist/core/schema.d.ts +50 -0
  64. package/dist/core/schema.js +51 -0
  65. package/dist/core/search.d.ts +32 -0
  66. package/dist/core/search.js +132 -0
  67. package/dist/core/serve/auth.d.ts +23 -0
  68. package/dist/core/serve/auth.js +38 -0
  69. package/dist/core/serve/server.d.ts +21 -0
  70. package/dist/core/serve/server.js +138 -0
  71. package/dist/core/serve/sessions.d.ts +40 -0
  72. package/dist/core/serve/sessions.js +203 -0
  73. package/dist/core/serve/token.d.ts +10 -0
  74. package/dist/core/serve/token.js +96 -0
  75. package/dist/core/service.d.ts +473 -0
  76. package/dist/core/service.js +973 -0
  77. package/dist/core/sla.d.ts +16 -0
  78. package/dist/core/sla.js +20 -0
  79. package/dist/core/stale.d.ts +16 -0
  80. package/dist/core/stale.js +24 -0
  81. package/dist/core/tool-registry.d.ts +31 -0
  82. package/dist/core/tool-registry.js +80 -0
  83. package/dist/core/url.d.ts +4 -0
  84. package/dist/core/url.js +31 -0
  85. package/dist/core/verify-pages.d.ts +19 -0
  86. package/dist/core/verify-pages.js +110 -0
  87. package/dist/core/version.d.ts +22 -0
  88. package/dist/core/version.js +32 -0
  89. package/dist/core/writes.d.ts +58 -0
  90. package/dist/core/writes.js +113 -0
  91. package/dist/web/assets/index-8J-FUbb0.js +29 -0
  92. package/dist/web/assets/index-BGpAZzeM.css +1 -0
  93. package/dist/web/index.html +14 -0
  94. package/docs/format-spec.md +349 -0
  95. package/package.json +75 -4
  96. package/plugin/.claude-plugin/plugin.json +8 -0
  97. package/plugin/.mcp.json +8 -0
  98. package/plugin/bin/docsworth-mcp.mjs +46 -0
  99. package/plugin/skills/draft-runbook/SKILL.md +75 -0
  100. package/plugin/skills/polish-page/SKILL.md +64 -0
  101. package/plugin/skills/update-after-incident/SKILL.md +59 -0
  102. package/plugin/skills/verify-page/SKILL.md +46 -0
  103. package/templates/github/knowledge.yml +51 -0
@@ -0,0 +1,473 @@
1
+ import { type Identity } from "./acl.js";
2
+ import { type MirrorPage } from "./mirror-view.js";
3
+ import { type NewPageType } from "./new-page.js";
4
+ import { type Policy } from "./policy.js";
5
+ import type { GitHubClient } from "./github.js";
6
+ import { type Page, type Repo } from "./repo.js";
7
+ import { type PageType } from "./schema.js";
8
+ import { SearchIndex } from "./search.js";
9
+ import { type StaleEntry } from "./stale.js";
10
+ import { type Edit } from "./writes.js";
11
+ export type ToolErrorCode = "not_found" | "forbidden" | "invalid" | "unavailable";
12
+ export declare class ToolError extends Error {
13
+ readonly code: ToolErrorCode;
14
+ constructor(code: ToolErrorCode, message: string);
15
+ }
16
+ /** What the service needs from identity resolution (IdentityProvider, or a test fake). */
17
+ export interface IdentitySource {
18
+ get(): Promise<Identity>;
19
+ fresh(): Promise<Identity>;
20
+ readonly github: GitHubClient | null;
21
+ }
22
+ export interface PageSummary {
23
+ id: string;
24
+ title: string;
25
+ path: string;
26
+ type: PageType;
27
+ status: Page["status"];
28
+ owner_team: string;
29
+ last_verified: string;
30
+ due: string | null;
31
+ sla_state: string;
32
+ citable: boolean;
33
+ }
34
+ export interface SearchResult extends PageSummary {
35
+ snippet: string;
36
+ score: number;
37
+ /** Present only with include_stale: why this page is not citable. */
38
+ gate?: string;
39
+ }
40
+ /** A mirrored wiki page that matched: context for the agent, never a citation. */
41
+ export interface MirroredHit {
42
+ id: string;
43
+ title: string;
44
+ source: string;
45
+ source_url: string;
46
+ owner_team: string | null;
47
+ last_edited: string;
48
+ citable: false;
49
+ snippet: string;
50
+ }
51
+ export interface Excluded {
52
+ id: string;
53
+ title: string;
54
+ owner_team: string;
55
+ reason: string;
56
+ }
57
+ /**
58
+ * The six knowledge tools as plain functions. The MCP server is a thin wrapper, and the
59
+ * ACL matrix test exercises this class directly, so there is one code path to audit.
60
+ *
61
+ * Invariant: every page a method touches comes from `visiblePages(repo, identity)`.
62
+ * Unknown ids and ids the caller cannot see raise the same not_found error.
63
+ */
64
+ /** How long after starting a watcher to reload once anyway. A write 500 ms after a restart
65
+ * was always reported (20 of 20); this leaves room on a loaded machine. */
66
+ export declare const WATCH_SETTLE_MS = 2000;
67
+ /** A draft_page body's limit, and how many fresh names it tries before giving up. */
68
+ export declare const DRAFT_MAX_CHARS: number;
69
+ export declare const DRAFT_NAME_ATTEMPTS = 3;
70
+ /** At most this many mirrored pages per search: context, not a second result list. */
71
+ export declare const MIRROR_LIMIT = 3;
72
+ type MirrorView = {
73
+ pages: MirrorPage[];
74
+ origin: MirrorPage[];
75
+ hidden: MirrorPage[];
76
+ index: SearchIndex;
77
+ };
78
+ type Snapshot = {
79
+ commit: string | null;
80
+ dirty: boolean | null;
81
+ content_sha256: string;
82
+ };
83
+ /**
84
+ * One load of the repo: the pages, their search index and the served mirror. Never changed
85
+ * after it is built, except two caches filled on first use: the mirror's views (one per
86
+ * audience) and the snapshot. A reload builds a new one, so a call that took this one keeps
87
+ * a consistent view.
88
+ */
89
+ interface Loaded {
90
+ readonly repo: Repo;
91
+ readonly index: SearchIndex;
92
+ readonly mirror: MirrorPage[];
93
+ /** Keyed by audience (repo access and teams), so services with any identity can share it. */
94
+ readonly mirrorViews: Map<string, MirrorView>;
95
+ /** mtimes of the non-page inputs (config, CODEOWNERS) it was built from. */
96
+ readonly inputs: string;
97
+ snap: Snapshot | null;
98
+ }
99
+ /**
100
+ * Loads the repo and keeps it until something changes, for one service or, in `docsworth
101
+ * serve`, for every session's service: one copy of the pages and their index however many
102
+ * tabs are open (a 3,000-page repo took about 50 MB per copy). It owns the one file watcher.
103
+ * What belongs to a session (its taint, its cached default branch) stays on the service.
104
+ */
105
+ export declare class RepoLoader {
106
+ private readonly root;
107
+ private loaded;
108
+ private dirty;
109
+ /** watch() was asked for, and close() hasn't stopped it. */
110
+ private watching;
111
+ private watcher;
112
+ private settle;
113
+ /** The directory being watched, or to be once it exists (tests read it). */
114
+ watchedDir: string | null;
115
+ /** How many times the repo was loaded (tests). */
116
+ loads: number;
117
+ /** Called after each load: serve moves its idle sessions onto it, so no old load is kept. */
118
+ onLoad: (() => void) | null;
119
+ constructor(root: string);
120
+ /** The current load: the same object until a change is seen, then a new one, built once. */
121
+ current(): Loaded;
122
+ /** The newest load, without looking for changes. */
123
+ latest(): Loaded | null;
124
+ /** Has a change been seen that the next call will load (tests). */
125
+ get pending(): boolean;
126
+ private load;
127
+ /** Config and CODEOWNERS live outside the watched knowledge root; stat them per call. */
128
+ private inputsStamp;
129
+ /** Load again on the next call. */
130
+ invalidate(): void;
131
+ /** Mark the load stale on any change under the knowledge root. */
132
+ watch(): void;
133
+ /** Watch `repo`'s knowledge root, in place of whatever was watched before. */
134
+ private follow;
135
+ close(): void;
136
+ private stop;
137
+ }
138
+ export declare class KnowledgeService {
139
+ private readonly root;
140
+ private readonly identity;
141
+ private readonly now;
142
+ private readonly loader;
143
+ /** A loader this service made is its own to close; a shared one belongs to its owner. */
144
+ private readonly ownsLoader;
145
+ private loaded;
146
+ private repo;
147
+ private index;
148
+ private defaultBranch;
149
+ /** Mirrored pages, when mirror.serve is on; one search index per audience (views). */
150
+ private mirror;
151
+ private mirrorViews;
152
+ /** Team ACLs whose content this session has seen (write-path taint check). */
153
+ private readonly tainted;
154
+ /** The random end of a drafted page's id and file name (replaced in tests). */
155
+ private draftSuffix;
156
+ /** `loader`: a load shared with other services (`docsworth serve`'s sessions); else its own. */
157
+ constructor(root: string, identity: IdentitySource, now?: () => Date, loader?: RepoLoader);
158
+ /** Load the repo again now. */
159
+ reload(): void;
160
+ /**
161
+ * Point at the loader's newest load, letting go of an older one. Only while no call of
162
+ * this service is running: `docsworth serve` calls it for a session with no request in
163
+ * flight, so an idle tab doesn't keep a whole old copy of the repo alive.
164
+ */
165
+ release(): void;
166
+ /** Take a new load, and drop what this service cached from the last one. */
167
+ private adopt;
168
+ /** The watched directory and a pending reload live on the loader (tests read them here). */
169
+ private get watchedDir();
170
+ private get dirty();
171
+ /**
172
+ * The mirrored pages `who` may see, each with links to the ones they can't see
173
+ * redacted (and to repo pages they can't see: a wiki page can link to one by its URL),
174
+ * and a search index over exactly that view: ranking, snippets and counts never draw on
175
+ * a page outside it. One per audience (the caller's teams), rebuilt on reload.
176
+ */
177
+ private mirrorView;
178
+ /** Reload on the next call. */
179
+ invalidate(): void;
180
+ /** Mark the index dirty on any change under the knowledge root. */
181
+ watch(): void;
182
+ /** Stops this service's own watcher; a shared loader's is its owner's to stop. */
183
+ close(): void;
184
+ private current;
185
+ /**
186
+ * The knowledge actually being served: the checkout's HEAD and whether the served files
187
+ * differ from it, plus a hash of the loaded config, ownership and pages. Refreshed after
188
+ * every reload, so an audit event names the snapshot its answer came from.
189
+ */
190
+ snapshot(): Snapshot;
191
+ /** Every page id, team and applies_to tag the repo uses: what an audited argument may record. */
192
+ knownValues(): {
193
+ ids: Set<string>;
194
+ teams: Set<string>;
195
+ tags: Set<string>;
196
+ };
197
+ /** Whether mirrored pages are served (mirror.serve). */
198
+ mirrorServed(): boolean;
199
+ /** The org policy in force now (config is re-read when it changes). */
200
+ policy(): Policy;
201
+ get org(): string;
202
+ /** Policy gate, checked on every call, before any page is resolved. */
203
+ private permit;
204
+ private summary;
205
+ private resolve;
206
+ /** Any restricted page represented in a response (content, snippet or just its title)
207
+ * taints the session for the write-path check. */
208
+ private taint;
209
+ private noteRead;
210
+ search(args: {
211
+ query: string;
212
+ type?: PageType;
213
+ owner_team?: string;
214
+ applies_to?: string;
215
+ include_stale?: boolean;
216
+ limit?: number;
217
+ /** "text": snippets cut from the page as a reader sees it (the web app); else markdown. */
218
+ snippets?: "markdown" | "text";
219
+ }): Promise<{
220
+ results: SearchResult[];
221
+ excluded: Excluded[];
222
+ mirrored?: MirroredHit[];
223
+ note?: string;
224
+ snippets?: "text";
225
+ }>;
226
+ read(args: {
227
+ id: string;
228
+ }): Promise<{
229
+ id: string;
230
+ title: string;
231
+ path: string;
232
+ type: PageType;
233
+ status: Page["status"];
234
+ owner_team: string;
235
+ last_verified: string;
236
+ due: string | null;
237
+ sla_state: string;
238
+ citable: boolean;
239
+ review_every: string;
240
+ acl: string;
241
+ supersedes: string[];
242
+ applies_to: string[];
243
+ flags: string[];
244
+ gate_reason?: string | undefined;
245
+ content: string;
246
+ } | {
247
+ id: string;
248
+ title: string;
249
+ source: "confluence" | "notion";
250
+ space: string;
251
+ source_url: string;
252
+ owner_team: string | null;
253
+ last_edited: string;
254
+ acl: string;
255
+ citable: boolean;
256
+ flags: string[];
257
+ gate_reason: string;
258
+ content: string;
259
+ }>;
260
+ /** read() for `tool`: the same view; only an agent's `read` goes in the usage log. */
261
+ private readAs;
262
+ /**
263
+ * `readAs`, with what it read: the page, the caller and the repo, from the one look at the
264
+ * repo after the only await. A caller that builds more of its answer from these (ui_page's
265
+ * links) answers from that same look, as `read` does: a reload after it can't mix in.
266
+ */
267
+ private readWith;
268
+ /** A repo page's text as `who` may see it: links to pages, or mirrored pages, they can't see redacted. */
269
+ private pageText;
270
+ /** A mirrored page, as context: never citable. Hidden and unknown ids read the same. */
271
+ private readMirror;
272
+ list(args?: {
273
+ path?: string;
274
+ type?: PageType;
275
+ }): Promise<{
276
+ type: "adr" | "guide" | "policy" | "runbook" | "service";
277
+ pages: PageSummary[];
278
+ path?: undefined;
279
+ index?: undefined;
280
+ dirs?: undefined;
281
+ } | {
282
+ type?: undefined;
283
+ path: string;
284
+ index: PageSummary | null;
285
+ pages: PageSummary[];
286
+ dirs: string[];
287
+ }>;
288
+ stale(args?: {
289
+ owner_team?: string;
290
+ }): Promise<{
291
+ pages: StaleEntry[];
292
+ }>;
293
+ /** Who is served and whether writes would work, for the app's header and settings. */
294
+ uiCapabilities(opts?: {
295
+ pro?: string[];
296
+ }): Promise<{
297
+ mode: "github" | "local";
298
+ login: string | null;
299
+ org: string;
300
+ repo: string | null;
301
+ writes: boolean;
302
+ write_tools: string[];
303
+ teams: string[];
304
+ writes_off?: string | undefined;
305
+ pro: string[];
306
+ }>;
307
+ /**
308
+ * Counts over the caller's visible pages, by team, with what is due soon and what is stale.
309
+ * `list_limit` keeps a big repo's overview short: the first of each list, the most pressing
310
+ * first, with `due_soon_total` and `stale_total`. Only the pages returned are named, so only
311
+ * those taint the session; the counts above the lists name no page.
312
+ */
313
+ uiOverview(args?: {
314
+ list_limit?: number;
315
+ }): Promise<{
316
+ as_of: string;
317
+ totals: {
318
+ pages: number;
319
+ citable: number;
320
+ overdue: number;
321
+ due_soon: number;
322
+ draft: number;
323
+ deprecated: number;
324
+ };
325
+ teams: {
326
+ pages: number;
327
+ citable: number;
328
+ overdue: number;
329
+ due_soon: number;
330
+ team: string;
331
+ }[];
332
+ due_soon: PageSummary[];
333
+ stale: StaleEntry[];
334
+ due_soon_total: number;
335
+ stale_total: number;
336
+ skipped_files: number;
337
+ }>;
338
+ /**
339
+ * A page as `read` shows it, with `links`: the repo path and id of each visible page it
340
+ * links to, so the app can route those links. Taints the session with the page and those.
341
+ */
342
+ uiPage(args: {
343
+ id: string;
344
+ }): Promise<{
345
+ id: string;
346
+ title: string;
347
+ source: "confluence" | "notion";
348
+ space: string;
349
+ source_url: string;
350
+ owner_team: string | null;
351
+ last_edited: string;
352
+ acl: string;
353
+ citable: boolean;
354
+ flags: string[];
355
+ gate_reason: string;
356
+ content: string;
357
+ links: {};
358
+ } | {
359
+ id: string;
360
+ title: string;
361
+ path: string;
362
+ type: PageType;
363
+ status: Page["status"];
364
+ owner_team: string;
365
+ last_verified: string;
366
+ due: string | null;
367
+ sla_state: string;
368
+ citable: boolean;
369
+ review_every: string;
370
+ acl: string;
371
+ supersedes: string[];
372
+ applies_to: string[];
373
+ flags: string[];
374
+ gate_reason?: string | undefined;
375
+ content: string;
376
+ links: {
377
+ [k: string]: string;
378
+ };
379
+ }>;
380
+ /**
381
+ * Visible pages and the links between them. A link to a page the caller can't see is no
382
+ * edge; `has_restricted_links` says a page has one, exactly as its `read` text shows a
383
+ * placeholder for it, so the graph reveals nothing `read` doesn't.
384
+ *
385
+ * `max_nodes` keeps a big repo drawable: past it, the most-linked pages (links between
386
+ * visible pages only; ties by id) and the links among them. `total` says how many it
387
+ * chose from, and `teams` how many visible pages each team has, a count that names no
388
+ * page. Only the returned pages are named, so only those taint the session.
389
+ */
390
+ uiGraph(args?: {
391
+ owner_team?: string;
392
+ max_nodes?: number;
393
+ }): Promise<{
394
+ nodes: {
395
+ id: string;
396
+ title: string;
397
+ path: string;
398
+ type: PageType;
399
+ status: Page["status"];
400
+ owner_team: string;
401
+ last_verified: string;
402
+ due: string | null;
403
+ sla_state: string;
404
+ citable: boolean;
405
+ has_restricted_links: boolean;
406
+ }[];
407
+ edges: {
408
+ from: string;
409
+ to: string;
410
+ }[];
411
+ total: number;
412
+ teams: {
413
+ team: string;
414
+ pages: number;
415
+ }[];
416
+ }>;
417
+ /** What every write needs first: the policy, a live identity, a GitHub repo. */
418
+ private writeIdentity;
419
+ private pushContext;
420
+ /** Common preamble for writes to a page: live identity, visibility, a GitHub repo. */
421
+ private writeContext;
422
+ /** The same for a write that creates a page: no id to resolve. */
423
+ private newWriteContext;
424
+ /** The taint check: a session that saw team-only pages writes only into pages with that audience. */
425
+ private refuseLeak;
426
+ proposeEdit(args: {
427
+ id: string;
428
+ edits?: Edit[];
429
+ body?: string;
430
+ content?: string;
431
+ append?: string;
432
+ reason: string;
433
+ draft?: boolean;
434
+ polish?: boolean;
435
+ }): Promise<{
436
+ pr_url: string;
437
+ pr_number: number;
438
+ branch: string;
439
+ }>;
440
+ /**
441
+ * A new page, as a draft PR. The server picks its id and file name, each ending in 12
442
+ * random hex digits: a caller-chosen name could be aimed at a hidden page, and "already
443
+ * exists" would then say it is there. The title still sets the slug, so CODEOWNERS
444
+ * rules that match file names (`runbooks/payments-*`) still apply.
445
+ */
446
+ draftPage(args: {
447
+ type: NewPageType;
448
+ title: string;
449
+ owner_team: string;
450
+ acl?: string;
451
+ dir?: string;
452
+ description?: string;
453
+ body: string;
454
+ source_url?: string;
455
+ reason: string;
456
+ draft?: boolean;
457
+ }): Promise<{
458
+ id: string;
459
+ path: string;
460
+ pr_url: string;
461
+ pr_number: number;
462
+ branch: string;
463
+ }>;
464
+ verify(args: {
465
+ id: string;
466
+ }): Promise<{
467
+ pr_url: string;
468
+ pr_number: number;
469
+ branch: string;
470
+ last_verified: string;
471
+ }>;
472
+ }
473
+ export {};