@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.
- package/package.json +24 -0
- package/scripts/docling_bridge.py +56 -0
- package/src/AICollaborator.ts +209 -0
- package/src/AutocompletePlugin.ts +108 -0
- package/src/ConflictResolver.ts +113 -0
- package/src/KairoPlugin.ts +27 -0
- package/src/LLMAdapter.ts +27 -0
- package/src/MockLLMAdapter.ts +45 -0
- package/src/PromptCache.ts +76 -0
- package/src/StreamBuffer.ts +60 -0
- package/src/SummarizationPlugin.ts +100 -0
- package/src/TransformersAdapter.ts +48 -0
- package/src/adapters/AnthropicAdapter.ts +72 -0
- package/src/adapters/GeminiAdapter.ts +72 -0
- package/src/adapters/OllamaAdapter.ts +81 -0
- package/src/adapters/OpenAIAdapter.ts +87 -0
- package/src/canonical-model.ts +164 -0
- package/src/cli/index.ts +48 -0
- package/src/docling-client.ts +47 -0
- package/src/editors/MonacoAdapter.ts +22 -0
- package/src/editors/ProseMirrorAdapter.ts +17 -0
- package/src/editors/QuillAdapter.ts +27 -0
- package/src/editors/TiptapAdapter.ts +50 -0
- package/src/format-adapter.ts +25 -0
- package/src/index.ts +37 -0
- package/src/kairo.ts +151 -0
- package/src/suggestion-manager.ts +39 -0
package/src/cli/index.ts
ADDED
|
@@ -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
|
+
}
|