@docscode/core 1.0.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.
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import chalk from 'chalk';
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name('kairo')
11
+ .description('CLI to scaffold Kairo AI plugins and integrations')
12
+ .version('1.0.0');
13
+
14
+ program
15
+ .command('create-plugin')
16
+ .description('Scaffold a new Kairo plugin')
17
+ .argument('<name>', 'Name of the plugin')
18
+ .action((name) => {
19
+ const className = name.charAt(0).toUpperCase() + name.slice(1) + 'Plugin';
20
+ const fileName = `${className}.ts`;
21
+ const targetPath = path.join(process.cwd(), fileName);
22
+
23
+ const template = `import { KairoPlugin, AICollaborator } from 'kairo';
24
+
25
+ export class ${className} extends KairoPlugin {
26
+ constructor(ai: AICollaborator) {
27
+ super(ai);
28
+ }
29
+
30
+ public setup() {
31
+ console.log('[Kairo] ${className} initialized');
32
+ // Hook into Yjs document updates here
33
+ }
34
+
35
+ public destroy() {
36
+ // Cleanup resources
37
+ }
38
+ }
39
+ `;
40
+
41
+ fs.writeFileSync(targetPath, template);
42
+ console.log(chalk.green(`\nāœ” Created ${className} at ${targetPath}`));
43
+ console.log(chalk.cyan(`\nNext steps:`));
44
+ console.log(`1. Implement your logic in setup()`);
45
+ console.log(`2. Register it: ai.registerPlugin(new ${className}(ai));\n`);
46
+ });
47
+
48
+ program.parse();
@@ -0,0 +1,47 @@
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const getDirname = () => {
6
+ if (typeof __dirname !== 'undefined') return __dirname;
7
+ return path.dirname(fileURLToPath((import.meta as any).url));
8
+ };
9
+ const _dirname = getDirname();
10
+ const BRIDGE_PATH = path.join(_dirname, '../scripts/docling_bridge.py');
11
+
12
+ export class DoclingClient {
13
+ async convert(filePathOrBase64: string): Promise<any> {
14
+ return new Promise((resolve, reject) => {
15
+ const pythonProcess = spawn('python', [BRIDGE_PATH, filePathOrBase64]);
16
+
17
+ let output = '';
18
+ let error = '';
19
+
20
+ pythonProcess.stdout.on('data', (data) => {
21
+ output += data.toString();
22
+ });
23
+
24
+ pythonProcess.stderr.on('data', (data) => {
25
+ error += data.toString();
26
+ });
27
+
28
+ pythonProcess.on('close', (code) => {
29
+ if (code !== 0) {
30
+ reject(new Error(`Python process exited with code ${code}: ${error}`));
31
+ return;
32
+ }
33
+
34
+ try {
35
+ const result = JSON.parse(output);
36
+ if (result.error) {
37
+ reject(new Error(result.error));
38
+ } else {
39
+ resolve(result);
40
+ }
41
+ } catch (e) {
42
+ reject(new Error(`Failed to parse Python output: ${output}`));
43
+ }
44
+ });
45
+ });
46
+ }
47
+ }
@@ -0,0 +1,22 @@
1
+ import { AICollaborator } from '../AICollaborator.js';
2
+
3
+ export class MonacoAdapter {
4
+ constructor(
5
+ private editor: any, // monaco.editor.IStandaloneCodeEditor
6
+ private ai: AICollaborator,
7
+ private fieldName: string = 'content'
8
+ ) {
9
+ // Monaco doesn't have a direct "plugin" system for awareness cursors
10
+ // in the same way Tiptap does out-of-the-box, but we can hook into
11
+ // y-monaco if the user is using it.
12
+ // For Kairo, the AICollaborator already handles Yjs awareness.
13
+ // This adapter simply provides a convenient wrapper.
14
+ }
15
+
16
+ public static bind(editor: any, ai: AICollaborator, fieldName: string = 'content'): MonacoAdapter {
17
+ return new MonacoAdapter(editor, ai, fieldName);
18
+ }
19
+
20
+ // Monaco-specific UI bindings can be added here, such as custom decorations
21
+ // for AI-generated text or inline ghost text suggestions.
22
+ }
@@ -0,0 +1,17 @@
1
+ import { AICollaborator } from '../AICollaborator.js';
2
+
3
+ export class ProseMirrorAdapter {
4
+ constructor(
5
+ private view: any, // EditorView
6
+ private ai: AICollaborator,
7
+ private fieldName: string = 'content'
8
+ ) {
9
+ // ProseMirror integration. The AICollaborator syncs via Yjs.
10
+ // This adapter can be extended to add ProseMirror decorations
11
+ // for AI text highlighting or suggestion overlays.
12
+ }
13
+
14
+ public static bind(view: any, ai: AICollaborator, fieldName: string = 'content'): ProseMirrorAdapter {
15
+ return new ProseMirrorAdapter(view, ai, fieldName);
16
+ }
17
+ }
@@ -0,0 +1,27 @@
1
+ import * as Y from 'yjs';
2
+ import { Awareness } from 'y-protocols/awareness';
3
+ import { AICollaborator } from '../AICollaborator.js';
4
+ import { AutocompletePlugin } from '../AutocompletePlugin.js';
5
+ import { SummarizationPlugin } from '../SummarizationPlugin.js';
6
+ import { LLMAdapter } from '../LLMAdapter.js';
7
+
8
+ /**
9
+ * High-level adapter for Quill editors using y-quill.
10
+ */
11
+ export class QuillAdapter {
12
+ /**
13
+ * Bind Kairo to a Quill instance.
14
+ * Since y-quill binding is external, we need the doc and awareness explicitly.
15
+ */
16
+ public static bind(doc: Y.Doc, awareness: Awareness, adapter: LLMAdapter, options: { field?: string } = {}) {
17
+ const { field = 'content' } = options;
18
+
19
+ const kairo = new AICollaborator(doc, awareness);
20
+ kairo.registerPlugin(new AutocompletePlugin(kairo, adapter, field));
21
+ kairo.registerPlugin(new SummarizationPlugin(kairo, adapter, field));
22
+
23
+ console.log('[Kairo] QuillAdapter bound successfully.');
24
+
25
+ return kairo;
26
+ }
27
+ }
@@ -0,0 +1,50 @@
1
+ import { Editor } from '@tiptap/core';
2
+ import { AICollaborator } from '../AICollaborator.js';
3
+ import { AutocompletePlugin } from '../AutocompletePlugin.js';
4
+ import { SummarizationPlugin } from '../SummarizationPlugin.js';
5
+ import { LLMAdapter } from '../LLMAdapter.js';
6
+
7
+ export interface TiptapAdapterOptions {
8
+ field?: string;
9
+ plugins?: ('autocomplete' | 'summarize')[];
10
+ }
11
+
12
+ /**
13
+ * High-level adapter to bind Kairo to a Tiptap editor instance.
14
+ */
15
+ export class TiptapAdapter {
16
+ public static bind(editor: Editor, adapter: LLMAdapter, options: TiptapAdapterOptions = {}) {
17
+ const { field = 'content', plugins = ['autocomplete', 'summarize'] } = options;
18
+
19
+ // 1. Get the underlying Yjs doc from Tiptap's collaboration extension
20
+ // Note: This assumes the user is using the @tiptap/extension-collaboration
21
+ const collaborationExtension = editor.extensionManager.extensions.find((e: any) => e.name === 'collaboration');
22
+
23
+ if (!collaborationExtension) {
24
+ throw new Error('[Kairo] TiptapAdapter requires @tiptap/extension-collaboration to be active.');
25
+ }
26
+
27
+ const doc = collaborationExtension.options.document;
28
+ const awareness = collaborationExtension.options.awareness;
29
+
30
+ if (!doc || !awareness) {
31
+ throw new Error('[Kairo] Could not find Yjs Document or Awareness in Tiptap editor.');
32
+ }
33
+
34
+ // 2. Initialize AICollaborator
35
+ const kairo = new AICollaborator(doc, awareness);
36
+
37
+ // 3. Register requested plugins
38
+ if (plugins.includes('autocomplete')) {
39
+ kairo.registerPlugin(new AutocompletePlugin(kairo, adapter, field));
40
+ }
41
+
42
+ if (plugins.includes('summarize')) {
43
+ kairo.registerPlugin(new SummarizationPlugin(kairo, adapter, field));
44
+ }
45
+
46
+ console.log('[Kairo] TiptapAdapter bound successfully.');
47
+
48
+ return kairo;
49
+ }
50
+ }
@@ -0,0 +1,25 @@
1
+ import * as Y from 'yjs';
2
+
3
+ export interface Suggestion {
4
+ id: string;
5
+ type: 'insert' | 'delete' | 'replace';
6
+ from: number;
7
+ to?: number;
8
+ text?: string;
9
+ author: string;
10
+ timestamp: number;
11
+ }
12
+
13
+ export interface FormatAdapter {
14
+ /** Supported format identifier */
15
+ readonly format: 'docx' | 'pdf' | 'gdoc' | 'markdown' | 'html' | 'txt' | string;
16
+
17
+ /** Parse a file/buffer into a Y.Doc */
18
+ read(source: Buffer | string): Promise<Y.Doc>;
19
+
20
+ /** Serialize a Y.Doc back to the native format */
21
+ write(doc: Y.Doc, original?: Buffer): Promise<Buffer>;
22
+
23
+ /** Map AI suggestions to format-native tracked changes */
24
+ applyTrackedChanges?(doc: Y.Doc, suggestions: Suggestion[]): Promise<Buffer>;
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ // ─── Core DCAL Engine ──────────────────────────────────────────────────────────
2
+ export { Kairo, KairoSession, kairo } from './kairo.js';
3
+ export type { KairoConnectOptions } from './kairo.js';
4
+
5
+ // ─── AI Collaborator Peer ─────────────────────────────────────────────────────
6
+ export { AICollaborator } from './AICollaborator.js';
7
+ export type { KairoStatus, KairoMetadata, AICollaboratorOptions } from './AICollaborator.js';
8
+
9
+ // ─── LLM Adapters (zero SDK, raw fetch) ───────────────────────────────────────
10
+ export type { LLMAdapter, StreamOptions, CompleteOptions } from './LLMAdapter.js';
11
+ export { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
12
+ export { AnthropicAdapter } from './adapters/AnthropicAdapter.js';
13
+ export { OllamaAdapter } from './adapters/OllamaAdapter.js';
14
+ export { GeminiAdapter } from './adapters/GeminiAdapter.js';
15
+ export { MockLLMAdapter } from './MockLLMAdapter.js';
16
+
17
+ // ─── CRDT Model ───────────────────────────────────────────────────────────────
18
+ export { CanonicalDoc } from './canonical-model.js';
19
+ export type { BlockType, CanonicalBlockMeta } from './canonical-model.js';
20
+
21
+ // ─── Format Adapter Interface ─────────────────────────────────────────────────
22
+ export type { FormatAdapter } from './format-adapter.js';
23
+
24
+ // ─── Plugins ──────────────────────────────────────────────────────────────────
25
+ export type { KairoPlugin } from './KairoPlugin.js';
26
+ export { AutocompletePlugin } from './AutocompletePlugin.js';
27
+ export { SummarizationPlugin } from './SummarizationPlugin.js';
28
+
29
+ // ─── Infrastructure ───────────────────────────────────────────────────────────
30
+ export { StreamBuffer } from './StreamBuffer.js';
31
+ export { PromptCache } from './PromptCache.js';
32
+ export { ConflictResolver, MergePolicy } from './ConflictResolver.js';
33
+ export { SuggestionManager } from './suggestion-manager.js';
34
+ export type { Suggestion } from './suggestion-manager.js';
35
+
36
+ // ─── Document Intelligence ────────────────────────────────────────────────────
37
+ export { DoclingClient } from './docling-client.js';
package/src/kairo.ts ADDED
@@ -0,0 +1,151 @@
1
+ import * as Y from 'yjs';
2
+ import { AICollaborator } from './AICollaborator.js';
3
+ import type { FormatAdapter } from './format-adapter.js';
4
+ import type { LLMAdapter } from './LLMAdapter.js';
5
+ import { AutocompletePlugin } from './AutocompletePlugin.js';
6
+ import { SummarizationPlugin } from './SummarizationPlugin.js';
7
+ import { ConflictResolver } from './ConflictResolver.js';
8
+ import { Awareness } from 'y-protocols/awareness';
9
+
10
+ export interface KairoConnectOptions {
11
+ /** Document content */
12
+ content: Buffer | string;
13
+ /** Document format. Auto-detected from fileName if omitted. */
14
+ format?: 'docx' | 'pdf' | 'gdoc' | 'markdown' | 'html' | 'txt' | string;
15
+ /** Optional file name for format auto-detection */
16
+ fileName?: string;
17
+ /** LLM adapter. Required for AI features. */
18
+ llm?: LLMAdapter;
19
+ /** Behaviors to activate. Default: all registered. */
20
+ behaviors?: ('autocomplete' | 'summarize')[];
21
+ /** Format adapter override. If omitted, uses registered adapter for format. */
22
+ adapter?: FormatAdapter;
23
+ /** Custom AI client ID. Auto-generated if omitted. */
24
+ clientId?: number;
25
+ /** Flush interval for streaming to CRDT in ms. Default: 50ms. */
26
+ streamFlushMs?: number;
27
+ /** Enable conflict resolution observer. Default: true. */
28
+ conflictResolution?: boolean;
29
+ }
30
+
31
+ /**
32
+ * KairoSession — A live, AI-collaborative document session.
33
+ *
34
+ * The session holds the Yjs document, the AI collaborator peer,
35
+ * the conflict resolver, and the format adapter for write-back.
36
+ *
37
+ * Treat this like a live Google Docs tab — it's a real-time shared artifact.
38
+ */
39
+ export class KairoSession {
40
+ public readonly ai: AICollaborator;
41
+ public readonly conflictResolver?: ConflictResolver;
42
+ private readonly _adapter?: FormatAdapter;
43
+
44
+ constructor(
45
+ public readonly doc: Y.Doc,
46
+ ai: AICollaborator,
47
+ adapter?: FormatAdapter,
48
+ conflictResolver?: ConflictResolver,
49
+ ) {
50
+ this.ai = ai;
51
+ this._adapter = adapter;
52
+ this.conflictResolver = conflictResolver;
53
+ }
54
+
55
+ /** Export the document back to its original format */
56
+ async export(): Promise<Buffer> {
57
+ if (!this._adapter) throw new Error('[Kairo] No format adapter — cannot export.');
58
+ return this._adapter.write(this.doc);
59
+ }
60
+
61
+ /** Get the plain text content of the document */
62
+ getText(key: string = 'content'): string {
63
+ return this.doc.getText(key).toString();
64
+ }
65
+
66
+ /** Get word count */
67
+ wordCount(): number {
68
+ return this.getText().split(/\s+/).filter(Boolean).length;
69
+ }
70
+
71
+ /** Destroy the session and free resources */
72
+ destroy() {
73
+ this.ai.destroy();
74
+ this.doc.destroy();
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Kairo — The Document CRDT Abstraction Layer (DCAL) entry point.
80
+ *
81
+ * One-line AI collaboration for any document:
82
+ * const session = await kairo.connect({ file: 'report.docx', llm: new OpenAIAdapter() });
83
+ * await session.ai.streamToDoc(yText, 'Improve the executive summary');
84
+ * const output = await session.export();
85
+ */
86
+ export class Kairo {
87
+ private formatAdapters: Map<string, FormatAdapter> = new Map();
88
+
89
+ /** Register a format adapter (DOCX, PDF, MD, HTML, GDoc, etc.) */
90
+ registerFormatAdapter(adapter: FormatAdapter): this {
91
+ this.formatAdapters.set(adapter.format, adapter);
92
+ return this; // chainable
93
+ }
94
+
95
+ /** Connect to a document and start an AI session */
96
+ async connect(options: KairoConnectOptions): Promise<KairoSession> {
97
+ const format = options.format ?? this._detectFormat(options.fileName ?? '');
98
+ const adapter = options.adapter ?? this.formatAdapters.get(format);
99
+
100
+ let doc: Y.Doc;
101
+
102
+ if (adapter) {
103
+ doc = await adapter.read(options.content);
104
+ } else {
105
+ // Fallback: treat as plain text
106
+ doc = new Y.Doc();
107
+ const text = typeof options.content === 'string' ? options.content : options.content.toString('utf-8');
108
+ doc.getText('content').insert(0, text);
109
+ }
110
+
111
+ const awareness = new Awareness(doc);
112
+ const ai = new AICollaborator(doc, awareness, {
113
+ llm: options.llm,
114
+ clientID: options.clientId,
115
+ streamFlushMs: options.streamFlushMs,
116
+ });
117
+
118
+ // Register behaviors
119
+ const behaviors = options.behaviors ?? ['autocomplete', 'summarize'];
120
+ if (options.llm) {
121
+ if (behaviors.includes('autocomplete')) ai.registerPlugin(new AutocompletePlugin(ai, options.llm));
122
+ if (behaviors.includes('summarize')) ai.registerPlugin(new SummarizationPlugin(ai, options.llm));
123
+ }
124
+
125
+ // Conflict resolver
126
+ let conflictResolver: ConflictResolver | undefined;
127
+ if (options.conflictResolution !== false) {
128
+ conflictResolver = new ConflictResolver(doc, [ai.clientID]);
129
+ }
130
+
131
+ return new KairoSession(doc, ai, adapter, conflictResolver);
132
+ }
133
+
134
+ /** Open multiple documents concurrently */
135
+ async connectAll(configs: KairoConnectOptions[]): Promise<KairoSession[]> {
136
+ return Promise.all(configs.map(c => this.connect(c)));
137
+ }
138
+
139
+ private _detectFormat(file: Buffer | string): string {
140
+ if (typeof file !== 'string') return 'txt';
141
+ const ext = file.split('.').pop()?.toLowerCase() ?? '';
142
+ const map: Record<string, string> = {
143
+ docx: 'docx', pdf: 'pdf', md: 'markdown', markdown: 'markdown',
144
+ html: 'html', htm: 'html', txt: 'txt',
145
+ };
146
+ return map[ext] ?? 'txt';
147
+ }
148
+ }
149
+
150
+ /** Singleton Kairo instance for quick use */
151
+ export const kairo = new Kairo();
@@ -0,0 +1,39 @@
1
+ import * as Y from 'yjs';
2
+ import { Suggestion } from './format-adapter.js';
3
+ export type { Suggestion } from './format-adapter.js';
4
+
5
+ export class SuggestionManager {
6
+ private suggestions: Y.Map<any>;
7
+
8
+ constructor(public readonly doc: Y.Doc) {
9
+ this.suggestions = doc.getMap('kairo-suggestions');
10
+ }
11
+
12
+ addSuggestion(suggestion: Omit<Suggestion, 'id' | 'timestamp'>): string {
13
+ const id = Math.random().toString(36).substring(7);
14
+ const fullSuggestion: Suggestion = {
15
+ ...suggestion,
16
+ id,
17
+ timestamp: Date.now()
18
+ };
19
+
20
+ this.suggestions.set(id, fullSuggestion);
21
+ return id;
22
+ }
23
+
24
+ getSuggestions(): Suggestion[] {
25
+ return Array.from(this.suggestions.values()) as Suggestion[];
26
+ }
27
+
28
+ resolveSuggestion(id: string, accept: boolean) {
29
+ const suggestion = this.suggestions.get(id) as Suggestion;
30
+ if (!suggestion) return;
31
+
32
+ if (accept) {
33
+ // Apply the edit to the actual document content
34
+ // This logic depends on the specific format/block
35
+ }
36
+
37
+ this.suggestions.delete(id);
38
+ }
39
+ }