plasalid 0.2.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 (153) hide show
  1. package/LICENSE +213 -0
  2. package/README.md +176 -0
  3. package/dist/accounts/taxonomy.d.ts +31 -0
  4. package/dist/accounts/taxonomy.js +189 -0
  5. package/dist/ai/agent.d.ts +43 -0
  6. package/dist/ai/agent.js +155 -0
  7. package/dist/ai/context.d.ts +4 -0
  8. package/dist/ai/context.js +33 -0
  9. package/dist/ai/memory.d.ts +14 -0
  10. package/dist/ai/memory.js +12 -0
  11. package/dist/ai/provider.d.ts +67 -0
  12. package/dist/ai/provider.js +5 -0
  13. package/dist/ai/providers/anthropic.d.ts +5 -0
  14. package/dist/ai/providers/anthropic.js +49 -0
  15. package/dist/ai/providers/index.d.ts +2 -0
  16. package/dist/ai/providers/index.js +12 -0
  17. package/dist/ai/providers/openai-compat.d.ts +5 -0
  18. package/dist/ai/providers/openai-compat.js +147 -0
  19. package/dist/ai/providers/openai.d.ts +5 -0
  20. package/dist/ai/providers/openai.js +147 -0
  21. package/dist/ai/redactor.d.ts +2 -0
  22. package/dist/ai/redactor.js +91 -0
  23. package/dist/ai/sanitize.d.ts +14 -0
  24. package/dist/ai/sanitize.js +25 -0
  25. package/dist/ai/system-prompt.d.ts +13 -0
  26. package/dist/ai/system-prompt.js +174 -0
  27. package/dist/ai/thai-taxonomy-hint.d.ts +8 -0
  28. package/dist/ai/thai-taxonomy-hint.js +22 -0
  29. package/dist/ai/thinking-phrases.d.ts +7 -0
  30. package/dist/ai/thinking-phrases.js +15 -0
  31. package/dist/ai/thinking.d.ts +7 -0
  32. package/dist/ai/thinking.js +15 -0
  33. package/dist/ai/tools/common.d.ts +2 -0
  34. package/dist/ai/tools/common.js +83 -0
  35. package/dist/ai/tools/index.d.ts +8 -0
  36. package/dist/ai/tools/index.js +34 -0
  37. package/dist/ai/tools/ingest.d.ts +2 -0
  38. package/dist/ai/tools/ingest.js +202 -0
  39. package/dist/ai/tools/read.d.ts +2 -0
  40. package/dist/ai/tools/read.js +123 -0
  41. package/dist/ai/tools/reconcile.d.ts +2 -0
  42. package/dist/ai/tools/reconcile.js +227 -0
  43. package/dist/ai/tools/scan.d.ts +2 -0
  44. package/dist/ai/tools/scan.js +24 -0
  45. package/dist/ai/tools/types.d.ts +26 -0
  46. package/dist/ai/tools/types.js +1 -0
  47. package/dist/ai/tools.d.ts +18 -0
  48. package/dist/ai/tools.js +402 -0
  49. package/dist/cli/chat.d.ts +1 -0
  50. package/dist/cli/chat.js +28 -0
  51. package/dist/cli/commands/accounts.d.ts +1 -0
  52. package/dist/cli/commands/accounts.js +86 -0
  53. package/dist/cli/commands/data.d.ts +1 -0
  54. package/dist/cli/commands/data.js +28 -0
  55. package/dist/cli/commands/reconcile.d.ts +2 -0
  56. package/dist/cli/commands/reconcile.js +15 -0
  57. package/dist/cli/commands/revert.d.ts +1 -0
  58. package/dist/cli/commands/revert.js +68 -0
  59. package/dist/cli/commands/scan.d.ts +4 -0
  60. package/dist/cli/commands/scan.js +45 -0
  61. package/dist/cli/commands/status.d.ts +1 -0
  62. package/dist/cli/commands/status.js +22 -0
  63. package/dist/cli/commands/transactions.d.ts +8 -0
  64. package/dist/cli/commands/transactions.js +92 -0
  65. package/dist/cli/commands/undo.d.ts +1 -0
  66. package/dist/cli/commands/undo.js +38 -0
  67. package/dist/cli/commands.d.ts +14 -0
  68. package/dist/cli/commands.js +196 -0
  69. package/dist/cli/format.d.ts +8 -0
  70. package/dist/cli/format.js +109 -0
  71. package/dist/cli/index.d.ts +2 -0
  72. package/dist/cli/index.js +126 -0
  73. package/dist/cli/ink/ChatApp.d.ts +8 -0
  74. package/dist/cli/ink/ChatApp.js +94 -0
  75. package/dist/cli/ink/PromptFrame.d.ts +10 -0
  76. package/dist/cli/ink/PromptFrame.js +11 -0
  77. package/dist/cli/ink/TextInput.d.ts +13 -0
  78. package/dist/cli/ink/TextInput.js +24 -0
  79. package/dist/cli/ink/hooks/useAgent.d.ts +27 -0
  80. package/dist/cli/ink/hooks/useAgent.js +65 -0
  81. package/dist/cli/ink/hooks/useCtrlCExit.d.ts +16 -0
  82. package/dist/cli/ink/hooks/useCtrlCExit.js +43 -0
  83. package/dist/cli/ink/hooks/useFooterText.d.ts +2 -0
  84. package/dist/cli/ink/hooks/useFooterText.js +43 -0
  85. package/dist/cli/ink/hooks/useTextInput.d.ts +32 -0
  86. package/dist/cli/ink/hooks/useTextInput.js +356 -0
  87. package/dist/cli/ink/messages/AssistantMessage.d.ts +3 -0
  88. package/dist/cli/ink/messages/AssistantMessage.js +6 -0
  89. package/dist/cli/ink/messages/ErrorMessage.d.ts +4 -0
  90. package/dist/cli/ink/messages/ErrorMessage.js +6 -0
  91. package/dist/cli/ink/messages/InterruptedMessage.d.ts +1 -0
  92. package/dist/cli/ink/messages/InterruptedMessage.js +6 -0
  93. package/dist/cli/ink/messages/ThinkingLine.d.ts +12 -0
  94. package/dist/cli/ink/messages/ThinkingLine.js +23 -0
  95. package/dist/cli/ink/messages/UserMessage.d.ts +4 -0
  96. package/dist/cli/ink/messages/UserMessage.js +15 -0
  97. package/dist/cli/ink/mount.d.ts +6 -0
  98. package/dist/cli/ink/mount.js +12 -0
  99. package/dist/cli/logo.d.ts +1 -0
  100. package/dist/cli/logo.js +20 -0
  101. package/dist/cli/setup.d.ts +2 -0
  102. package/dist/cli/setup.js +210 -0
  103. package/dist/cli/ux.d.ts +38 -0
  104. package/dist/cli/ux.js +104 -0
  105. package/dist/config.d.ts +21 -0
  106. package/dist/config.js +66 -0
  107. package/dist/currency.d.ts +6 -0
  108. package/dist/currency.js +19 -0
  109. package/dist/db/connection.d.ts +5 -0
  110. package/dist/db/connection.js +45 -0
  111. package/dist/db/encryption.d.ts +11 -0
  112. package/dist/db/encryption.js +45 -0
  113. package/dist/db/helpers.d.ts +16 -0
  114. package/dist/db/helpers.js +45 -0
  115. package/dist/db/queries/account_balance.d.ts +61 -0
  116. package/dist/db/queries/account_balance.js +146 -0
  117. package/dist/db/queries/journal.d.ts +95 -0
  118. package/dist/db/queries/journal.js +204 -0
  119. package/dist/db/queries/search.d.ts +7 -0
  120. package/dist/db/queries/search.js +19 -0
  121. package/dist/db/schema.d.ts +2 -0
  122. package/dist/db/schema.js +95 -0
  123. package/dist/index.d.ts +1 -0
  124. package/dist/index.js +1 -0
  125. package/dist/parser/pdf.d.ts +14 -0
  126. package/dist/parser/pdf.js +40 -0
  127. package/dist/parser/pipeline.d.ts +44 -0
  128. package/dist/parser/pipeline.js +160 -0
  129. package/dist/parser/prompts.d.ts +8 -0
  130. package/dist/parser/prompts.js +20 -0
  131. package/dist/parser/walker.d.ts +8 -0
  132. package/dist/parser/walker.js +42 -0
  133. package/dist/reconciler/pipeline.d.ts +17 -0
  134. package/dist/reconciler/pipeline.js +45 -0
  135. package/dist/reconciler/prompts.d.ts +12 -0
  136. package/dist/reconciler/prompts.js +22 -0
  137. package/dist/scanner/password-store.d.ts +34 -0
  138. package/dist/scanner/password-store.js +83 -0
  139. package/dist/scanner/pdf-unlock.d.ts +17 -0
  140. package/dist/scanner/pdf-unlock.js +48 -0
  141. package/dist/scanner/pdf.d.ts +17 -0
  142. package/dist/scanner/pdf.js +36 -0
  143. package/dist/scanner/pipeline.d.ts +32 -0
  144. package/dist/scanner/pipeline.js +137 -0
  145. package/dist/scanner/prompts.d.ts +8 -0
  146. package/dist/scanner/prompts.js +20 -0
  147. package/dist/scanner/state-machine.d.ts +60 -0
  148. package/dist/scanner/state-machine.js +64 -0
  149. package/dist/scanner/unlock.d.ts +24 -0
  150. package/dist/scanner/unlock.js +122 -0
  151. package/dist/scanner/walker.d.ts +8 -0
  152. package/dist/scanner/walker.js +42 -0
  153. package/package.json +65 -0
@@ -0,0 +1,155 @@
1
+ import { config } from "../config.js";
2
+ import { buildChatSystemPrompt, buildScanSystemPrompt, buildReconcileSystemPrompt, } from "./system-prompt.js";
3
+ import { getToolDefinitions, executeTool } from "./tools/index.js";
4
+ import { getConversationHistory, saveMessage } from "./memory.js";
5
+ import { redact, unredact } from "./redactor.js";
6
+ import { createProvider } from "./providers/index.js";
7
+ const provider = createProvider();
8
+ const MAX_TOOL_STEPS = 20;
9
+ export class AbortedError extends Error {
10
+ constructor() {
11
+ super("aborted");
12
+ this.name = "AbortedError";
13
+ }
14
+ }
15
+ async function runAgent({ db, systemPrompt, tools, initialMessages, agentCtx, onProgress, signal, maxToolSteps, }) {
16
+ const messages = [...initialMessages];
17
+ const useThinking = config.thinkingBudget > 0 && provider.supportsThinking;
18
+ const throwIfAborted = () => {
19
+ if (signal?.aborted)
20
+ throw new AbortedError();
21
+ };
22
+ const stepLimit = maxToolSteps ?? MAX_TOOL_STEPS;
23
+ throwIfAborted();
24
+ let response = await provider.sendMessage({
25
+ model: config.model,
26
+ maxTokens: useThinking ? 16000 : 4096,
27
+ system: systemPrompt,
28
+ tools,
29
+ messages,
30
+ thinking: useThinking ? { type: "enabled", budget_tokens: config.thinkingBudget } : undefined,
31
+ signal,
32
+ });
33
+ const startTime = Date.now();
34
+ let toolCount = 0;
35
+ while (response.stopReason === "tool_use" && toolCount < stepLimit) {
36
+ throwIfAborted();
37
+ messages.push({ role: "assistant", content: response.content });
38
+ const toolResults = [];
39
+ for (const block of response.content) {
40
+ if (block.type === "tool_use") {
41
+ toolCount++;
42
+ onProgress?.({ phase: "tool", toolName: block.name, toolCount, elapsedMs: Date.now() - startTime });
43
+ const result = await executeTool(db, block.name, block.input, agentCtx);
44
+ toolResults.push({
45
+ type: "tool_result",
46
+ tool_use_id: block.id,
47
+ content: redact(result),
48
+ });
49
+ }
50
+ }
51
+ messages.push({ role: "user", content: toolResults });
52
+ onProgress?.({ phase: "responding", toolCount, elapsedMs: Date.now() - startTime });
53
+ throwIfAborted();
54
+ response = await provider.sendMessage({
55
+ model: config.model,
56
+ maxTokens: useThinking ? 16000 : 4096,
57
+ system: systemPrompt,
58
+ tools,
59
+ messages,
60
+ thinking: useThinking ? { type: "enabled", budget_tokens: config.thinkingBudget } : undefined,
61
+ signal,
62
+ });
63
+ }
64
+ const textBlocks = response.content.filter((b) => b.type === "text");
65
+ const text = unredact(textBlocks.map(b => b.text).join("\n"));
66
+ return { text, messages };
67
+ }
68
+ /**
69
+ * Conversational chat used by the Ink TUI. Reuses conversation_history for context
70
+ * continuity, redacts PII on the way out, restores it on the way in for display.
71
+ */
72
+ export async function handleChatMessage(db, userMessage, onProgress, signal) {
73
+ saveMessage(db, "user", userMessage);
74
+ const rawHistory = getConversationHistory(db, 30);
75
+ const MAX_HISTORY_CHARS = 24_000;
76
+ let historyChars = 0;
77
+ const history = [];
78
+ for (let i = rawHistory.length - 1; i >= 0; i--) {
79
+ historyChars += rawHistory[i].content.length;
80
+ if (historyChars > MAX_HISTORY_CHARS)
81
+ break;
82
+ history.unshift(rawHistory[i]);
83
+ }
84
+ const systemPrompt = redact(buildChatSystemPrompt(db));
85
+ const messages = history.map(h => ({
86
+ role: h.role,
87
+ content: redact(h.content),
88
+ }));
89
+ if (messages.length === 0 || messages[messages.length - 1].content !== redact(userMessage)) {
90
+ messages.push({ role: "user", content: redact(userMessage) });
91
+ }
92
+ try {
93
+ const { text } = await runAgent({
94
+ db,
95
+ systemPrompt,
96
+ tools: getToolDefinitions("chat"),
97
+ initialMessages: messages,
98
+ onProgress,
99
+ signal,
100
+ });
101
+ saveMessage(db, "assistant", text);
102
+ return text || "I couldn't formulate a response. Could you rephrase?";
103
+ }
104
+ catch (error) {
105
+ if (error instanceof AbortedError || error?.name === "AbortError" || signal?.aborted) {
106
+ throw new AbortedError();
107
+ }
108
+ if (error.status === 401 || error.status === 403) {
109
+ return "API key was rejected. Run `plasalid setup` to reconfigure your credentials.";
110
+ }
111
+ if (error.status === 429) {
112
+ return "Rate limited. Wait a moment and try again.";
113
+ }
114
+ const safeMessage = error.status ? `API error (${error.status}): ${error.message || ""}` : error.message || "internal error";
115
+ console.error("AI error:", safeMessage);
116
+ return "Sorry, I had trouble processing that. Could you try again?";
117
+ }
118
+ }
119
+ /**
120
+ * Scan-time agent loop. Caller supplies the initial user message (which carries
121
+ * the PDF as a content block) and a AgentExecutionContext that scopes the file
122
+ * id, scanner version, and interactivity for ask_user.
123
+ */
124
+ export async function runScanAgent(opts) {
125
+ const systemPrompt = redact(buildScanSystemPrompt(opts.db, opts.prompt));
126
+ const { text } = await runAgent({
127
+ db: opts.db,
128
+ systemPrompt,
129
+ tools: getToolDefinitions("scan"),
130
+ initialMessages: opts.initialMessages,
131
+ agentCtx: opts.agentCtx,
132
+ onProgress: opts.onProgress,
133
+ signal: opts.signal,
134
+ maxToolSteps: 40,
135
+ });
136
+ return text;
137
+ }
138
+ /**
139
+ * Reconcile-time agent loop. Walks the existing journal with the reconcile
140
+ * tool profile (read tools + write/merge/delete primitives).
141
+ */
142
+ export async function runReconcileAgent(opts) {
143
+ const systemPrompt = redact(buildReconcileSystemPrompt(opts.db, opts.prompt));
144
+ const { text } = await runAgent({
145
+ db: opts.db,
146
+ systemPrompt,
147
+ tools: getToolDefinitions("reconcile"),
148
+ initialMessages: opts.initialMessages,
149
+ agentCtx: opts.agentCtx,
150
+ onProgress: opts.onProgress,
151
+ signal: opts.signal,
152
+ maxToolSteps: 60,
153
+ });
154
+ return text;
155
+ }
@@ -0,0 +1,4 @@
1
+ export declare function getContextPath(): string;
2
+ export declare function readContext(): string;
3
+ export declare function writeContext(content: string): void;
4
+ export declare function createContextTemplate(userName: string): void;
@@ -0,0 +1,33 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
2
+ import { dirname, resolve } from "path";
3
+ import { getPlasalidDir } from "../config.js";
4
+ export function getContextPath() {
5
+ return resolve(getPlasalidDir(), "context.md");
6
+ }
7
+ export function readContext() {
8
+ const p = getContextPath();
9
+ if (!existsSync(p))
10
+ return "";
11
+ try {
12
+ return readFileSync(p, "utf-8");
13
+ }
14
+ catch {
15
+ return "";
16
+ }
17
+ }
18
+ export function writeContext(content) {
19
+ const p = getContextPath();
20
+ const dir = dirname(p);
21
+ if (!existsSync(dir))
22
+ mkdirSync(dir, { recursive: true });
23
+ writeFileSync(p, content, { encoding: "utf-8", mode: 0o600 });
24
+ try {
25
+ chmodSync(p, 0o600);
26
+ }
27
+ catch { }
28
+ }
29
+ export function createContextTemplate(userName) {
30
+ if (existsSync(getContextPath()))
31
+ return;
32
+ writeContext(`# Plasalid context for ${userName}\n\n## Family\n- ${userName}\n\n## Income\n- (Optional: add your primary income source so Plasalid can mark it as PII when sending data to the model.)\n\n## Notes\n- (Free-form notes about your accounts, bank preferences, or anything Plasalid should keep in mind when scanning.)\n`);
33
+ }
@@ -0,0 +1,14 @@
1
+ import type Database from "libsql";
2
+ export declare function getConversationHistory(db: Database.Database, limit?: number): {
3
+ role: string;
4
+ content: string;
5
+ created_at: string;
6
+ }[];
7
+ export declare function saveMessage(db: Database.Database, role: "user" | "assistant", content: string): void;
8
+ export declare function getMemories(db: Database.Database): {
9
+ id: number;
10
+ content: string;
11
+ category: string;
12
+ created_at: string;
13
+ }[];
14
+ export declare function saveMemory(db: Database.Database, content: string, category?: string): void;
@@ -0,0 +1,12 @@
1
+ export function getConversationHistory(db, limit = 20) {
2
+ return db.prepare(`SELECT role, content, created_at FROM conversation_history ORDER BY id DESC LIMIT ?`).all(limit).reverse();
3
+ }
4
+ export function saveMessage(db, role, content) {
5
+ db.prepare(`INSERT INTO conversation_history (role, content) VALUES (?, ?)`).run(role, content);
6
+ }
7
+ export function getMemories(db) {
8
+ return db.prepare(`SELECT id, content, category, created_at FROM memories ORDER BY created_at DESC`).all();
9
+ }
10
+ export function saveMemory(db, content, category = "general") {
11
+ db.prepare(`INSERT INTO memories (content, category) VALUES (?, ?)`).run(content, category);
12
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Normalized types for provider abstraction.
3
+ * Mirrors Anthropic's content-block shape but decoupled from the SDK types.
4
+ */
5
+ export interface TextBlock {
6
+ type: "text";
7
+ text: string;
8
+ }
9
+ export interface ToolUseBlock {
10
+ type: "tool_use";
11
+ id: string;
12
+ name: string;
13
+ input: any;
14
+ }
15
+ export interface DocumentBlock {
16
+ type: "document";
17
+ source: {
18
+ type: "base64";
19
+ media_type: string;
20
+ data: string;
21
+ };
22
+ title?: string;
23
+ }
24
+ export type NormalizedContentBlock = TextBlock | ToolUseBlock | DocumentBlock;
25
+ export interface NormalizedResponse {
26
+ content: NormalizedContentBlock[];
27
+ stopReason: string;
28
+ usage?: {
29
+ input_tokens: number;
30
+ output_tokens: number;
31
+ };
32
+ }
33
+ export interface NormalizedMessage {
34
+ role: "user" | "assistant";
35
+ content: string | NormalizedContentBlock[] | NormalizedToolResult[];
36
+ }
37
+ export interface NormalizedToolResult {
38
+ type: "tool_result";
39
+ tool_use_id: string;
40
+ content: string;
41
+ }
42
+ export interface ToolDefinition {
43
+ name: string;
44
+ description: string;
45
+ input_schema: {
46
+ type: "object";
47
+ properties: Record<string, any>;
48
+ required: string[];
49
+ };
50
+ }
51
+ export interface SendMessageParams {
52
+ model: string;
53
+ system: string;
54
+ messages: NormalizedMessage[];
55
+ tools: ToolDefinition[];
56
+ maxTokens: number;
57
+ thinking?: {
58
+ type: "enabled";
59
+ budget_tokens: number;
60
+ };
61
+ signal?: AbortSignal;
62
+ }
63
+ export interface Provider {
64
+ name: string;
65
+ supportsThinking: boolean;
66
+ sendMessage(params: SendMessageParams): Promise<NormalizedResponse>;
67
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Normalized types for provider abstraction.
3
+ * Mirrors Anthropic's content-block shape but decoupled from the SDK types.
4
+ */
5
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { Provider } from "../provider.js";
2
+ export declare function createAnthropicProvider(opts: {
3
+ apiKey: string;
4
+ baseURL?: string;
5
+ }): Provider;
@@ -0,0 +1,49 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ export function createAnthropicProvider(opts) {
3
+ const client = new Anthropic(opts.baseURL
4
+ ? { apiKey: opts.apiKey, baseURL: opts.baseURL }
5
+ : { apiKey: opts.apiKey });
6
+ return {
7
+ name: "anthropic",
8
+ supportsThinking: true,
9
+ async sendMessage(params) {
10
+ const apiParams = {
11
+ model: params.model,
12
+ max_tokens: params.maxTokens,
13
+ system: params.system,
14
+ tools: params.tools,
15
+ messages: params.messages,
16
+ };
17
+ if (params.thinking) {
18
+ apiParams.thinking = params.thinking;
19
+ }
20
+ const response = await client.messages.create(apiParams, {
21
+ signal: params.signal,
22
+ });
23
+ // Filter thinking blocks and normalize content
24
+ const content = [];
25
+ for (const block of response.content) {
26
+ if (block.type === "thinking")
27
+ continue;
28
+ if (block.type === "text") {
29
+ content.push({ type: "text", text: block.text });
30
+ }
31
+ else if (block.type === "tool_use") {
32
+ content.push({
33
+ type: "tool_use",
34
+ id: block.id,
35
+ name: block.name,
36
+ input: block.input,
37
+ });
38
+ }
39
+ }
40
+ return {
41
+ content,
42
+ stopReason: response.stop_reason || "end_turn",
43
+ usage: response.usage
44
+ ? { input_tokens: response.usage.input_tokens, output_tokens: response.usage.output_tokens }
45
+ : undefined,
46
+ };
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,2 @@
1
+ import type { Provider } from "../provider.js";
2
+ export declare function createProvider(): Provider;
@@ -0,0 +1,12 @@
1
+ import { config } from "../../config.js";
2
+ import { createAnthropicProvider } from "./anthropic.js";
3
+ import { createOpenAICompatibleProvider } from "./openai.js";
4
+ export function createProvider() {
5
+ if (config.providerType === "openai-compatible") {
6
+ return createOpenAICompatibleProvider({
7
+ apiKey: config.openaiCompatibleKey || "openai-compatible",
8
+ baseURL: config.openaiCompatibleBaseURL,
9
+ });
10
+ }
11
+ return createAnthropicProvider({ apiKey: config.anthropicKey });
12
+ }
@@ -0,0 +1,5 @@
1
+ import type { Provider } from "../provider.js";
2
+ export declare function createOpenAICompatibleProvider(opts: {
3
+ apiKey: string;
4
+ baseURL: string;
5
+ }): Provider;
@@ -0,0 +1,147 @@
1
+ import OpenAI from "openai";
2
+ export function createOpenAICompatibleProvider(opts) {
3
+ const client = new OpenAI({
4
+ apiKey: opts.apiKey,
5
+ baseURL: opts.baseURL,
6
+ });
7
+ return {
8
+ name: "openai-compatible",
9
+ supportsThinking: false,
10
+ async sendMessage(params) {
11
+ const messages = convertMessages(params.system, params.messages);
12
+ const tools = convertTools(params.tools);
13
+ // Try max_tokens first (broadest compat: Ollama, vLLM, older OpenAI models),
14
+ // fall back to max_completion_tokens if rejected (newer OpenAI models require it)
15
+ let response;
16
+ try {
17
+ response = await client.chat.completions.create({
18
+ model: params.model,
19
+ max_tokens: params.maxTokens,
20
+ messages,
21
+ tools: tools.length > 0 ? tools : undefined,
22
+ }, { signal: params.signal });
23
+ }
24
+ catch (e) {
25
+ if (e.status === 400 && e.message?.includes("max_tokens")) {
26
+ response = await client.chat.completions.create({
27
+ model: params.model,
28
+ max_completion_tokens: params.maxTokens,
29
+ messages,
30
+ tools: tools.length > 0 ? tools : undefined,
31
+ }, { signal: params.signal });
32
+ }
33
+ else {
34
+ throw e;
35
+ }
36
+ }
37
+ const choice = response.choices[0];
38
+ if (!choice) {
39
+ return { content: [], stopReason: "end_turn" };
40
+ }
41
+ const content = [];
42
+ if (choice.message.content) {
43
+ content.push({ type: "text", text: choice.message.content });
44
+ }
45
+ if (choice.message.tool_calls) {
46
+ for (const tc of choice.message.tool_calls) {
47
+ if (tc.type !== "function")
48
+ continue;
49
+ content.push({
50
+ type: "tool_use",
51
+ id: tc.id,
52
+ name: tc.function.name,
53
+ input: parseArguments(tc.function.arguments),
54
+ });
55
+ }
56
+ }
57
+ const hasToolCalls = content.some((b) => b.type === "tool_use");
58
+ return {
59
+ content,
60
+ stopReason: hasToolCalls ? "tool_use" : "end_turn",
61
+ usage: response.usage
62
+ ? { input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens }
63
+ : undefined,
64
+ };
65
+ },
66
+ };
67
+ }
68
+ function convertMessages(system, messages) {
69
+ const result = [
70
+ { role: "system", content: system },
71
+ ];
72
+ for (const msg of messages) {
73
+ if (msg.role === "user") {
74
+ if (Array.isArray(msg.content) &&
75
+ msg.content.length > 0 &&
76
+ msg.content[0].type === "tool_result") {
77
+ const toolResults = msg.content;
78
+ for (const tr of toolResults) {
79
+ result.push({
80
+ role: "tool",
81
+ tool_call_id: tr.tool_use_id,
82
+ content: tr.content,
83
+ });
84
+ }
85
+ }
86
+ else if (Array.isArray(msg.content)) {
87
+ // Strip document blocks (OpenAI-compat doesn't accept them); keep text.
88
+ const text = msg.content
89
+ .filter((b) => b.type === "text")
90
+ .map((b) => b.text)
91
+ .join("\n");
92
+ result.push({ role: "user", content: text });
93
+ }
94
+ else {
95
+ result.push({ role: "user", content: msg.content });
96
+ }
97
+ }
98
+ else {
99
+ if (Array.isArray(msg.content)) {
100
+ const blocks = msg.content;
101
+ const textParts = blocks
102
+ .filter((b) => b.type === "text")
103
+ .map((b) => b.text)
104
+ .join("\n");
105
+ const toolCalls = blocks
106
+ .filter((b) => b.type === "tool_use")
107
+ .map((b) => {
108
+ const tu = b;
109
+ return {
110
+ id: tu.id,
111
+ type: "function",
112
+ function: { name: tu.name, arguments: JSON.stringify(tu.input) },
113
+ };
114
+ });
115
+ result.push({
116
+ role: "assistant",
117
+ content: textParts || null,
118
+ ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
119
+ });
120
+ }
121
+ else {
122
+ result.push({ role: "assistant", content: msg.content });
123
+ }
124
+ }
125
+ }
126
+ return result;
127
+ }
128
+ function convertTools(tools) {
129
+ return tools.map((t) => ({
130
+ type: "function",
131
+ function: {
132
+ name: t.name,
133
+ description: t.description,
134
+ parameters: t.input_schema,
135
+ },
136
+ }));
137
+ }
138
+ function parseArguments(args) {
139
+ if (typeof args !== "string")
140
+ return args;
141
+ try {
142
+ return JSON.parse(args);
143
+ }
144
+ catch {
145
+ return {};
146
+ }
147
+ }
@@ -0,0 +1,5 @@
1
+ import type { Provider } from "../provider.js";
2
+ export declare function createOpenAICompatibleProvider(opts: {
3
+ apiKey: string;
4
+ baseURL: string;
5
+ }): Provider;
@@ -0,0 +1,147 @@
1
+ import OpenAI from "openai";
2
+ export function createOpenAICompatibleProvider(opts) {
3
+ const client = new OpenAI({
4
+ apiKey: opts.apiKey,
5
+ baseURL: opts.baseURL,
6
+ });
7
+ return {
8
+ name: "openai-compatible",
9
+ supportsThinking: false,
10
+ async sendMessage(params) {
11
+ const messages = convertMessages(params.system, params.messages);
12
+ const tools = convertTools(params.tools);
13
+ // Try max_tokens first (broadest compat: Ollama, vLLM, older OpenAI models),
14
+ // fall back to max_completion_tokens if rejected (newer OpenAI models require it)
15
+ let response;
16
+ try {
17
+ response = await client.chat.completions.create({
18
+ model: params.model,
19
+ max_tokens: params.maxTokens,
20
+ messages,
21
+ tools: tools.length > 0 ? tools : undefined,
22
+ }, { signal: params.signal });
23
+ }
24
+ catch (e) {
25
+ if (e.status === 400 && e.message?.includes("max_tokens")) {
26
+ response = await client.chat.completions.create({
27
+ model: params.model,
28
+ max_completion_tokens: params.maxTokens,
29
+ messages,
30
+ tools: tools.length > 0 ? tools : undefined,
31
+ }, { signal: params.signal });
32
+ }
33
+ else {
34
+ throw e;
35
+ }
36
+ }
37
+ const choice = response.choices[0];
38
+ if (!choice) {
39
+ return { content: [], stopReason: "end_turn" };
40
+ }
41
+ const content = [];
42
+ if (choice.message.content) {
43
+ content.push({ type: "text", text: choice.message.content });
44
+ }
45
+ if (choice.message.tool_calls) {
46
+ for (const tc of choice.message.tool_calls) {
47
+ if (tc.type !== "function")
48
+ continue;
49
+ content.push({
50
+ type: "tool_use",
51
+ id: tc.id,
52
+ name: tc.function.name,
53
+ input: parseArguments(tc.function.arguments),
54
+ });
55
+ }
56
+ }
57
+ const hasToolCalls = content.some((b) => b.type === "tool_use");
58
+ return {
59
+ content,
60
+ stopReason: hasToolCalls ? "tool_use" : "end_turn",
61
+ usage: response.usage
62
+ ? { input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens }
63
+ : undefined,
64
+ };
65
+ },
66
+ };
67
+ }
68
+ function convertMessages(system, messages) {
69
+ const result = [
70
+ { role: "system", content: system },
71
+ ];
72
+ for (const msg of messages) {
73
+ if (msg.role === "user") {
74
+ if (Array.isArray(msg.content) &&
75
+ msg.content.length > 0 &&
76
+ msg.content[0].type === "tool_result") {
77
+ const toolResults = msg.content;
78
+ for (const tr of toolResults) {
79
+ result.push({
80
+ role: "tool",
81
+ tool_call_id: tr.tool_use_id,
82
+ content: tr.content,
83
+ });
84
+ }
85
+ }
86
+ else if (Array.isArray(msg.content)) {
87
+ // Strip document blocks (OpenAI-compat doesn't accept them); keep text.
88
+ const text = msg.content
89
+ .filter((b) => b.type === "text")
90
+ .map((b) => b.text)
91
+ .join("\n");
92
+ result.push({ role: "user", content: text });
93
+ }
94
+ else {
95
+ result.push({ role: "user", content: msg.content });
96
+ }
97
+ }
98
+ else {
99
+ if (Array.isArray(msg.content)) {
100
+ const blocks = msg.content;
101
+ const textParts = blocks
102
+ .filter((b) => b.type === "text")
103
+ .map((b) => b.text)
104
+ .join("\n");
105
+ const toolCalls = blocks
106
+ .filter((b) => b.type === "tool_use")
107
+ .map((b) => {
108
+ const tu = b;
109
+ return {
110
+ id: tu.id,
111
+ type: "function",
112
+ function: { name: tu.name, arguments: JSON.stringify(tu.input) },
113
+ };
114
+ });
115
+ result.push({
116
+ role: "assistant",
117
+ content: textParts || null,
118
+ ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
119
+ });
120
+ }
121
+ else {
122
+ result.push({ role: "assistant", content: msg.content });
123
+ }
124
+ }
125
+ }
126
+ return result;
127
+ }
128
+ function convertTools(tools) {
129
+ return tools.map((t) => ({
130
+ type: "function",
131
+ function: {
132
+ name: t.name,
133
+ description: t.description,
134
+ parameters: t.input_schema,
135
+ },
136
+ }));
137
+ }
138
+ function parseArguments(args) {
139
+ if (typeof args !== "string")
140
+ return args;
141
+ try {
142
+ return JSON.parse(args);
143
+ }
144
+ catch {
145
+ return {};
146
+ }
147
+ }
@@ -0,0 +1,2 @@
1
+ export declare function redact(text: string): string;
2
+ export declare function unredact(text: string): string;