@docscode/core 1.0.0 → 1.0.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.
@@ -0,0 +1,517 @@
1
+ import * as Y from 'yjs';
2
+ import { Awareness } from 'y-protocols/awareness';
3
+
4
+ declare abstract class KairoPlugin {
5
+ protected ai: AICollaborator;
6
+ enabled: boolean;
7
+ constructor(ai: AICollaborator);
8
+ /**
9
+ * Called when the plugin is registered with the AICollaborator
10
+ */
11
+ abstract setup(): void;
12
+ /**
13
+ * Called to cleanup the plugin
14
+ */
15
+ abstract destroy(): void;
16
+ /**
17
+ * Enable/Disable the plugin
18
+ */
19
+ setEnabled(enabled: boolean): void;
20
+ }
21
+
22
+ /**
23
+ * PromptCache — LRU cache for LLM prompts.
24
+ *
25
+ * Prevents re-querying the LLM for identical prompts within a session.
26
+ * Critical for summarization and autocomplete performance.
27
+ *
28
+ * Uses a Map-based LRU: entries at the front are most recent,
29
+ * entries at the back are evicted first.
30
+ */
31
+ declare class PromptCache {
32
+ private cache;
33
+ private readonly maxSize;
34
+ private totalHits;
35
+ private totalMisses;
36
+ constructor(maxSize?: number);
37
+ /** Retrieve cached result for a prompt. Returns null on miss. */
38
+ get(prompt: string): string | null;
39
+ /** Cache a result for a prompt. Evicts LRU entry if at capacity. */
40
+ set(prompt: string, value: string): void;
41
+ /** Check if a prompt is cached without counting as a hit */
42
+ has(prompt: string): boolean;
43
+ /** Remove a specific entry */
44
+ invalidate(prompt: string): void;
45
+ /** Clear all entries */
46
+ clear(): void;
47
+ /** Cache statistics */
48
+ stats(): {
49
+ size: number;
50
+ maxSize: number;
51
+ hitRate: number;
52
+ totalHits: number;
53
+ totalMisses: number;
54
+ };
55
+ }
56
+
57
+ interface StreamOptions {
58
+ systemPrompt?: string;
59
+ maxTokens?: number;
60
+ temperature?: number;
61
+ signal?: AbortSignal;
62
+ }
63
+ interface CompleteOptions extends StreamOptions {
64
+ }
65
+ /**
66
+ * LLMAdapter — Kairo's universal LLM interface.
67
+ * Implement this to plug any model into the CRDT streaming pipeline.
68
+ * Zero SDK dependency by design — uses raw fetch for SSE streaming.
69
+ */
70
+ interface LLMAdapter {
71
+ readonly provider: string;
72
+ readonly model: string;
73
+ /**
74
+ * Stream tokens one-by-one as an async generator.
75
+ * This feeds directly into StreamBuffer → Y.Text CRDT operations.
76
+ */
77
+ stream(prompt: string, options?: StreamOptions): AsyncGenerator<string>;
78
+ /** Non-streaming fallback for summarization / one-shot tasks */
79
+ complete(prompt: string, options?: CompleteOptions): Promise<string>;
80
+ /** Optional: embed text for semantic search / context retrieval */
81
+ embed?(text: string): Promise<number[]>;
82
+ }
83
+
84
+ interface Suggestion {
85
+ id: string;
86
+ type: 'insert' | 'delete' | 'replace';
87
+ from: number;
88
+ to?: number;
89
+ text?: string;
90
+ author: string;
91
+ timestamp: number;
92
+ }
93
+ interface FormatAdapter {
94
+ /** Supported format identifier */
95
+ readonly format: 'docx' | 'pdf' | 'gdoc' | 'markdown' | 'html' | 'txt' | string;
96
+ /** Parse a file/buffer into a Y.Doc */
97
+ read(source: Buffer | string): Promise<Y.Doc>;
98
+ /** Serialize a Y.Doc back to the native format */
99
+ write(doc: Y.Doc, original?: Buffer): Promise<Buffer>;
100
+ /** Map AI suggestions to format-native tracked changes */
101
+ applyTrackedChanges?(doc: Y.Doc, suggestions: Suggestion[]): Promise<Buffer>;
102
+ }
103
+
104
+ declare class SuggestionManager {
105
+ readonly doc: Y.Doc;
106
+ private suggestions;
107
+ constructor(doc: Y.Doc);
108
+ addSuggestion(suggestion: Omit<Suggestion, 'id' | 'timestamp'>): string;
109
+ getSuggestions(): Suggestion[];
110
+ resolveSuggestion(id: string, accept: boolean): void;
111
+ }
112
+
113
+ type KairoStatus = 'idle' | 'thinking' | 'writing' | 'reviewing';
114
+ interface KairoMetadata {
115
+ name: string;
116
+ version: string;
117
+ capabilities: string[];
118
+ model?: string;
119
+ }
120
+ interface AICollaboratorOptions {
121
+ clientID?: number;
122
+ cacheSize?: number;
123
+ llm?: LLMAdapter;
124
+ streamFlushMs?: number;
125
+ }
126
+ /**
127
+ * AICollaborator — The AI peer inside your Yjs document.
128
+ *
129
+ * Appears as a real-time participant in the awareness protocol.
130
+ * Streams LLM tokens directly into Y.Text CRDT operations.
131
+ * Human peers see a live cursor and status indicator — just like a human collaborator.
132
+ */
133
+ declare class AICollaborator {
134
+ readonly doc: Y.Doc;
135
+ readonly awareness: Awareness;
136
+ readonly clientID: number;
137
+ readonly suggestions: SuggestionManager;
138
+ readonly cache: PromptCache;
139
+ private _status;
140
+ private _plugins;
141
+ private _llm?;
142
+ private _streamFlushMs;
143
+ constructor(doc: Y.Doc, awareness: Awareness, options?: AICollaboratorOptions);
144
+ setStatus(status: KairoStatus): void;
145
+ setThinking(): void;
146
+ setWriting(): void;
147
+ setReviewing(): void;
148
+ setIdle(): void;
149
+ private _updateAwareness;
150
+ registerPlugin(plugin: KairoPlugin): void;
151
+ /**
152
+ * Insert text with AI attribution markers.
153
+ * Consuming editors can style/filter AI-generated content using these markers.
154
+ */
155
+ insertWithAttribution(text: Y.Text, index: number, content: string): void;
156
+ /**
157
+ * Stream LLM tokens directly into the document as CRDT operations.
158
+ *
159
+ * Modes:
160
+ * - 'insert': appends after current position
161
+ * - 'continue': appends to end of document
162
+ * - 'rewrite': clears existing text and rewrites
163
+ *
164
+ * Token cadence: tokens are batched with a 50ms flush interval for
165
+ * smooth real-time rendering without CRDT overload.
166
+ */
167
+ streamToDoc(yText: Y.Text, prompt: string, options?: {
168
+ systemPrompt?: string;
169
+ mode?: 'insert' | 'continue' | 'rewrite';
170
+ insertAt?: number;
171
+ signal?: AbortSignal;
172
+ onToken?: (token: string) => void;
173
+ }): Promise<{
174
+ tokensWritten: number;
175
+ durationMs: number;
176
+ }>;
177
+ /**
178
+ * One-shot: summarize a block of text and return the result (no streaming).
179
+ */
180
+ summarize(text: string, options?: {
181
+ maxTokens?: number;
182
+ }): Promise<string>;
183
+ destroy(): void;
184
+ }
185
+
186
+ /**
187
+ * ConflictResolver — Human-precedence CRDT conflict resolution for Kairo.
188
+ *
189
+ * Strategy:
190
+ * 1. Human edits always win over AI edits in the same position.
191
+ * 2. AI edits made within 500ms of a human edit at the same offset are rolled back.
192
+ * 3. Attribution is preserved — every merged character knows who wrote it.
193
+ *
194
+ * This is surfaced as a Y.Doc observer that monitors for concurrent human+AI
195
+ * writes and reorders them deterministically.
196
+ */
197
+ interface ConflictEvent {
198
+ type: 'human_overrides_ai' | 'ai_deferred' | 'concurrent_edit';
199
+ position: number;
200
+ humanClientId: number;
201
+ aiClientId: number;
202
+ timestamp: number;
203
+ }
204
+ type ConflictHandler = (event: ConflictEvent) => void;
205
+ declare class ConflictResolver {
206
+ private doc;
207
+ private conflictLog;
208
+ private handlers;
209
+ private aiClientIds;
210
+ constructor(doc: Y.Doc, aiClientIds?: number[]);
211
+ /** Register a client ID as an AI participant */
212
+ registerAI(clientId: number): void;
213
+ /** Listen for conflict events */
214
+ onConflict(handler: ConflictHandler): void;
215
+ /** Get the conflict log */
216
+ getLog(): ConflictEvent[];
217
+ /** Clear the conflict log */
218
+ clearLog(): void;
219
+ private _observe;
220
+ private _emit;
221
+ }
222
+ /**
223
+ * MergePolicy — Determines how concurrent AI and human edits are resolved.
224
+ *
225
+ * Kairo uses "human-precedence linear merge":
226
+ * - Human text is always displayed first at the same logical position
227
+ * - AI suggestions appear as tracked changes until accepted
228
+ * - This matches the mental model of track-changes in Word/Google Docs
229
+ */
230
+ declare class MergePolicy {
231
+ static readonly HUMAN_WINS = "human_wins";
232
+ static readonly LAST_WRITE_WINS = "last_write_wins";
233
+ static readonly AI_WINS = "ai_wins";
234
+ static isAIGenerated(attrs: Record<string, any> | null | undefined): boolean;
235
+ static getAuthor(attrs: Record<string, any> | null | undefined): 'ai' | 'human';
236
+ }
237
+
238
+ interface KairoConnectOptions {
239
+ /** Document content */
240
+ content: Buffer | string;
241
+ /** Document format. Auto-detected from fileName if omitted. */
242
+ format?: 'docx' | 'pdf' | 'gdoc' | 'markdown' | 'html' | 'txt' | string;
243
+ /** Optional file name for format auto-detection */
244
+ fileName?: string;
245
+ /** LLM adapter. Required for AI features. */
246
+ llm?: LLMAdapter;
247
+ /** Behaviors to activate. Default: all registered. */
248
+ behaviors?: ('autocomplete' | 'summarize')[];
249
+ /** Format adapter override. If omitted, uses registered adapter for format. */
250
+ adapter?: FormatAdapter;
251
+ /** Custom AI client ID. Auto-generated if omitted. */
252
+ clientId?: number;
253
+ /** Flush interval for streaming to CRDT in ms. Default: 50ms. */
254
+ streamFlushMs?: number;
255
+ /** Enable conflict resolution observer. Default: true. */
256
+ conflictResolution?: boolean;
257
+ }
258
+ /**
259
+ * KairoSession — A live, AI-collaborative document session.
260
+ *
261
+ * The session holds the Yjs document, the AI collaborator peer,
262
+ * the conflict resolver, and the format adapter for write-back.
263
+ *
264
+ * Treat this like a live Google Docs tab — it's a real-time shared artifact.
265
+ */
266
+ declare class KairoSession {
267
+ readonly doc: Y.Doc;
268
+ readonly ai: AICollaborator;
269
+ readonly conflictResolver?: ConflictResolver;
270
+ private readonly _adapter?;
271
+ constructor(doc: Y.Doc, ai: AICollaborator, adapter?: FormatAdapter, conflictResolver?: ConflictResolver);
272
+ /** Export the document back to its original format */
273
+ export(): Promise<Buffer>;
274
+ /** Get the plain text content of the document */
275
+ getText(key?: string): string;
276
+ /** Get word count */
277
+ wordCount(): number;
278
+ /** Destroy the session and free resources */
279
+ destroy(): void;
280
+ }
281
+ /**
282
+ * Kairo — The Document CRDT Abstraction Layer (DCAL) entry point.
283
+ *
284
+ * One-line AI collaboration for any document:
285
+ * const session = await kairo.connect({ file: 'report.docx', llm: new OpenAIAdapter() });
286
+ * await session.ai.streamToDoc(yText, 'Improve the executive summary');
287
+ * const output = await session.export();
288
+ */
289
+ declare class Kairo {
290
+ private formatAdapters;
291
+ /** Register a format adapter (DOCX, PDF, MD, HTML, GDoc, etc.) */
292
+ registerFormatAdapter(adapter: FormatAdapter): this;
293
+ /** Connect to a document and start an AI session */
294
+ connect(options: KairoConnectOptions): Promise<KairoSession>;
295
+ /** Open multiple documents concurrently */
296
+ connectAll(configs: KairoConnectOptions[]): Promise<KairoSession[]>;
297
+ private _detectFormat;
298
+ }
299
+ /** Singleton Kairo instance for quick use */
300
+ declare const kairo: Kairo;
301
+
302
+ /**
303
+ * OpenAIAdapter — Production streaming adapter using raw fetch (zero SDK).
304
+ * Works with OpenAI API and any OpenAI-compatible endpoint (Together, Groq, etc.)
305
+ */
306
+ declare class OpenAIAdapter implements LLMAdapter {
307
+ readonly provider = "openai";
308
+ readonly model: string;
309
+ private apiKey;
310
+ private baseUrl;
311
+ constructor(options?: {
312
+ apiKey?: string;
313
+ model?: string;
314
+ baseUrl?: string;
315
+ });
316
+ stream(prompt: string, options?: StreamOptions): AsyncGenerator<string>;
317
+ complete(prompt: string, options?: StreamOptions): Promise<string>;
318
+ embed(text: string): Promise<number[]>;
319
+ }
320
+
321
+ /**
322
+ * AnthropicAdapter — Production Claude streaming via raw fetch SSE.
323
+ * Zero SDK dependency. Uses Messages API with streaming.
324
+ */
325
+ declare class AnthropicAdapter implements LLMAdapter {
326
+ readonly provider = "anthropic";
327
+ readonly model: string;
328
+ private apiKey;
329
+ constructor(options?: {
330
+ apiKey?: string;
331
+ model?: string;
332
+ });
333
+ stream(prompt: string, options?: StreamOptions): AsyncGenerator<string>;
334
+ complete(prompt: string, options?: StreamOptions): Promise<string>;
335
+ }
336
+
337
+ /**
338
+ * OllamaAdapter — Local-first LLM streaming via Ollama REST API.
339
+ * No cloud. No API key. Pure local inference.
340
+ * Works with llama3, mistral, codellama, phi3, gemma2, qwen2.5, etc.
341
+ */
342
+ declare class OllamaAdapter implements LLMAdapter {
343
+ readonly provider = "ollama";
344
+ readonly model: string;
345
+ private baseUrl;
346
+ constructor(options?: {
347
+ model?: string;
348
+ baseUrl?: string;
349
+ });
350
+ stream(prompt: string, options?: StreamOptions): AsyncGenerator<string>;
351
+ complete(prompt: string, options?: StreamOptions): Promise<string>;
352
+ /** Check if Ollama is running and model is available */
353
+ isAvailable(): Promise<boolean>;
354
+ }
355
+
356
+ /**
357
+ * GeminiAdapter — Google Gemini streaming via raw fetch SSE.
358
+ * Supports Gemini 2.0 Flash, Gemini 1.5 Pro/Flash.
359
+ * Zero SDK dependency.
360
+ */
361
+ declare class GeminiAdapter implements LLMAdapter {
362
+ readonly provider = "gemini";
363
+ readonly model: string;
364
+ private apiKey;
365
+ constructor(options?: {
366
+ apiKey?: string;
367
+ model?: string;
368
+ });
369
+ stream(prompt: string, options?: StreamOptions): AsyncGenerator<string>;
370
+ complete(prompt: string, options?: StreamOptions): Promise<string>;
371
+ }
372
+
373
+ /**
374
+ * MockLLMAdapter — Test/offline LLM adapter.
375
+ *
376
+ * Returns deterministic responses without any network call.
377
+ * Perfect for unit tests and offline development.
378
+ */
379
+ declare class MockLLMAdapter implements LLMAdapter {
380
+ readonly provider = "mock";
381
+ readonly model: string;
382
+ private delay;
383
+ constructor(options?: {
384
+ model?: string;
385
+ delay?: number;
386
+ });
387
+ stream(prompt: string, options?: StreamOptions): AsyncGenerator<string>;
388
+ complete(prompt: string, options?: StreamOptions): Promise<string>;
389
+ embed(text: string): Promise<number[]>;
390
+ private _respond;
391
+ }
392
+
393
+ /**
394
+ * Kairo Canonical Document Model — The universal CRDT schema.
395
+ *
396
+ * Every document format (DOCX, PDF, MD, HTML, GDoc) normalizes to this schema.
397
+ * The AI peer reads and writes exclusively via this model.
398
+ *
399
+ * Schema:
400
+ * metadata: Y.Map — title, author, format, source, lastModified, etc.
401
+ * content: Y.Array — ordered array of Block Y.Maps
402
+ *
403
+ * Block structure (Y.Map):
404
+ * type: 'p' | 'h1'–'h6' | 'li' | 'code' | 'table' | 'blockquote' | 'hr' | 'image'
405
+ * id: string (stable UUID for anchor references)
406
+ * text: Y.Text (the actual content, with rich attributes for formatting)
407
+ * meta: Y.Map (optional, e.g. { lang: 'typescript' } for code blocks)
408
+ *
409
+ * Text attributes (Y.Text marks):
410
+ * ai-generated: true — marks AI-inserted ranges
411
+ * ai-client-id: number — which AI peer inserted this
412
+ * ai-timestamp: number — when
413
+ * ai-model: string — which model
414
+ * bold / italic / code: — inline formatting
415
+ */
416
+ type BlockType = 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'li' | 'oli' | 'code' | 'table' | 'blockquote' | 'hr' | 'image';
417
+ interface CanonicalBlockMeta {
418
+ lang?: string;
419
+ level?: number;
420
+ src?: string;
421
+ alt?: string;
422
+ aiConfidence?: number;
423
+ [key: string]: unknown;
424
+ }
425
+ declare class CanonicalDoc {
426
+ readonly yDoc: Y.Doc;
427
+ constructor(yDoc: Y.Doc);
428
+ get metadata(): Y.Map<any>;
429
+ get content(): Y.Array<any>;
430
+ /**
431
+ * Add a block to the document.
432
+ * Returns the Y.Text of the block so callers can further instrument it.
433
+ */
434
+ addBlock(type: BlockType | string, text?: string, meta?: CanonicalBlockMeta): Y.Text;
435
+ /** Convenience: add a paragraph block */
436
+ addParagraph(text?: string): Y.Text;
437
+ /** Convenience: add a heading block */
438
+ addHeading(level: 1 | 2 | 3 | 4 | 5 | 6, text: string): Y.Text;
439
+ /** Set or update metadata on an existing block's Y.Text */
440
+ setBlockMeta(yText: Y.Text, meta: CanonicalBlockMeta): void;
441
+ /** Get block by index */
442
+ getBlock(index: number): Y.Map<any> | undefined;
443
+ /** Find block by id */
444
+ findById(id: string): Y.Map<any> | undefined;
445
+ /** Get all text content as plain string */
446
+ toPlainText(): string;
447
+ /** Get document stats */
448
+ stats(): {
449
+ blockCount: number;
450
+ charCount: number;
451
+ wordCount: number;
452
+ };
453
+ /** Get AI-contributed character count */
454
+ aiContributions(): {
455
+ charCount: number;
456
+ blockCount: number;
457
+ };
458
+ }
459
+
460
+ /**
461
+ * AutocompletePlugin — Trigger AI completions with '...' in the document.
462
+ * When a human types '...' at the end of any text, Kairo's AI peer takes over
463
+ * and streams a continuation directly into the shared document.
464
+ */
465
+ declare class AutocompletePlugin extends KairoPlugin {
466
+ private targetTextName;
467
+ private adapter;
468
+ private _deepObserver;
469
+ constructor(ai: AICollaborator, adapter: LLMAdapter, targetTextName?: string);
470
+ setup(): void;
471
+ destroy(): void;
472
+ private _handleUpdate;
473
+ private _generateSuggestion;
474
+ }
475
+
476
+ /**
477
+ * SummarizationPlugin — Trigger document summarization with '/summarize' in the text.
478
+ * The AI peer generates a summary and inserts it as a blockquote below the trigger.
479
+ */
480
+ declare class SummarizationPlugin extends KairoPlugin {
481
+ private targetTextName;
482
+ private adapter;
483
+ private _deepObserver;
484
+ constructor(ai: AICollaborator, adapter: LLMAdapter, targetTextName?: string);
485
+ setup(): void;
486
+ destroy(): void;
487
+ private _handleUpdate;
488
+ private _performSummarization;
489
+ }
490
+
491
+ declare class StreamBuffer {
492
+ private text;
493
+ private clientID;
494
+ private buffer;
495
+ private flushInterval;
496
+ private timer;
497
+ private currentIndex;
498
+ constructor(text: Y.Text, clientID: number, startIndex: number, flushInterval?: number);
499
+ /**
500
+ * Add a new token to the buffer
501
+ */
502
+ push(token: string): void;
503
+ /**
504
+ * Immediately flush the buffer to the Yjs document
505
+ */
506
+ flush(): void;
507
+ /**
508
+ * Stop any pending flushes
509
+ */
510
+ stop(): void;
511
+ }
512
+
513
+ declare class DoclingClient {
514
+ convert(filePathOrBase64: string): Promise<any>;
515
+ }
516
+
517
+ export { AICollaborator, type AICollaboratorOptions, AnthropicAdapter, AutocompletePlugin, type BlockType, type CanonicalBlockMeta, CanonicalDoc, type CompleteOptions, ConflictResolver, DoclingClient, type FormatAdapter, GeminiAdapter, Kairo, type KairoConnectOptions, type KairoMetadata, KairoPlugin, KairoSession, type KairoStatus, type LLMAdapter, MergePolicy, MockLLMAdapter, OllamaAdapter, OpenAIAdapter, PromptCache, StreamBuffer, type StreamOptions, type Suggestion, SuggestionManager, SummarizationPlugin, kairo };