plasalid 0.7.8 → 0.8.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/README.md +23 -7
- package/dist/ai/agent.d.ts +1 -0
- package/dist/ai/agent.js +25 -10
- package/dist/ai/provider.d.ts +21 -1
- package/dist/ai/providers/anthropic.d.ts +0 -1
- package/dist/ai/providers/anthropic.js +2 -3
- package/dist/ai/providers/gemini.d.ts +14 -0
- package/dist/ai/providers/gemini.js +188 -0
- package/dist/ai/providers/index.d.ts +2 -1
- package/dist/ai/providers/index.js +23 -8
- package/dist/ai/providers/openai-compat.d.ts +6 -1
- package/dist/ai/providers/openai-compat.js +48 -104
- package/dist/ai/providers/openai-shared.d.ts +26 -0
- package/dist/ai/providers/openai-shared.js +118 -0
- package/dist/ai/providers/openai.d.ts +27 -3
- package/dist/ai/providers/openai.js +142 -91
- package/dist/ai/thinking.js +1 -1
- package/dist/cli/commands/clarify.js +1 -6
- package/dist/cli/commands/scan.js +9 -0
- package/dist/cli/ink/ScanDashboard.d.ts +6 -5
- package/dist/cli/ink/ScanDashboard.js +5 -6
- package/dist/cli/setup.js +169 -119
- package/dist/config.d.ts +10 -4
- package/dist/config.js +40 -11
- package/dist/scanner/pdf/pdf.d.ts +3 -2
- package/dist/scanner/pdf/pdf.js +11 -1
- package/dist/scanner/pdf/rasterize.d.ts +6 -0
- package/dist/scanner/pdf/rasterize.js +36 -0
- package/dist/scanner/worker.js +4 -2
- package/package.json +2 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import type { NormalizedResponse, NormalizedContentBlock, NormalizedMessage, NormalizedToolResult, ToolDefinition } from "../provider.js";
|
|
3
|
+
export type ChatCompletion = OpenAI.Chat.Completions.ChatCompletion;
|
|
4
|
+
type RequestOptions = Parameters<OpenAI["chat"]["completions"]["create"]>[1];
|
|
5
|
+
export interface CompletionBody {
|
|
6
|
+
model: string;
|
|
7
|
+
maxTokens: number;
|
|
8
|
+
messages: OpenAI.ChatCompletionMessageParam[];
|
|
9
|
+
tools: OpenAI.ChatCompletionTool[] | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Older models accept `max_tokens`; o-series / gpt-5+ require
|
|
13
|
+
* `max_completion_tokens`. Try the former, fall back on the 400.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createCompletionWithTokenFallback(client: OpenAI, body: CompletionBody, options: RequestOptions): Promise<ChatCompletion>;
|
|
16
|
+
export declare function normalizeResponse(response: ChatCompletion): NormalizedResponse;
|
|
17
|
+
export declare function convertTools(tools: ToolDefinition[]): OpenAI.ChatCompletionTool[];
|
|
18
|
+
/**
|
|
19
|
+
* Throw on malformed JSON so truncated tool args surface as an error instead
|
|
20
|
+
* of being silently coerced to `{}` (which would record zero transactions).
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseArguments(toolName: string, args: string): unknown;
|
|
23
|
+
export declare function convertToolResults(toolResults: NormalizedToolResult[]): OpenAI.ChatCompletionToolMessageParam[];
|
|
24
|
+
export declare function convertAssistantMessage(blocks: NormalizedContentBlock[]): OpenAI.ChatCompletionAssistantMessageParam;
|
|
25
|
+
export declare function isToolResultEnvelope(content: NormalizedMessage["content"]): content is NormalizedToolResult[];
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
function isMaxTokensRejection(e) {
|
|
2
|
+
const err = e;
|
|
3
|
+
return err.status === 400 && (err.message?.includes("max_tokens") ?? false);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Older models accept `max_tokens`; o-series / gpt-5+ require
|
|
7
|
+
* `max_completion_tokens`. Try the former, fall back on the 400.
|
|
8
|
+
*/
|
|
9
|
+
export async function createCompletionWithTokenFallback(client, body, options) {
|
|
10
|
+
const base = {
|
|
11
|
+
model: body.model,
|
|
12
|
+
messages: body.messages,
|
|
13
|
+
tools: body.tools,
|
|
14
|
+
};
|
|
15
|
+
try {
|
|
16
|
+
return await client.chat.completions.create({ ...base, max_tokens: body.maxTokens }, options);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
if (isMaxTokensRejection(e)) {
|
|
20
|
+
return await client.chat.completions.create({ ...base, max_completion_tokens: body.maxTokens }, options);
|
|
21
|
+
}
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function normalizeResponse(response) {
|
|
26
|
+
const choice = response.choices[0];
|
|
27
|
+
if (!choice) {
|
|
28
|
+
return { content: [], stopReason: "end_turn" };
|
|
29
|
+
}
|
|
30
|
+
const content = [];
|
|
31
|
+
if (choice.message.content) {
|
|
32
|
+
content.push({ type: "text", text: choice.message.content });
|
|
33
|
+
}
|
|
34
|
+
if (choice.message.tool_calls) {
|
|
35
|
+
for (const tc of choice.message.tool_calls) {
|
|
36
|
+
if (tc.type !== "function")
|
|
37
|
+
continue;
|
|
38
|
+
content.push({
|
|
39
|
+
type: "tool_use",
|
|
40
|
+
id: tc.id,
|
|
41
|
+
name: tc.function.name,
|
|
42
|
+
input: parseArguments(tc.function.name, tc.function.arguments),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const hasToolCalls = content.some((b) => b.type === "tool_use");
|
|
47
|
+
/**
|
|
48
|
+
* finish_reason "length" → "max_tokens" so the agent loop records a
|
|
49
|
+
* scan_truncated question instead of silently accepting a partial batch.
|
|
50
|
+
*/
|
|
51
|
+
const stopReason = choice.finish_reason === "length"
|
|
52
|
+
? "max_tokens"
|
|
53
|
+
: hasToolCalls
|
|
54
|
+
? "tool_use"
|
|
55
|
+
: "end_turn";
|
|
56
|
+
return {
|
|
57
|
+
content,
|
|
58
|
+
stopReason,
|
|
59
|
+
usage: response.usage
|
|
60
|
+
? { input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens }
|
|
61
|
+
: undefined,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function convertTools(tools) {
|
|
65
|
+
return tools.map((t) => ({
|
|
66
|
+
type: "function",
|
|
67
|
+
function: {
|
|
68
|
+
name: t.name,
|
|
69
|
+
description: t.description,
|
|
70
|
+
parameters: t.input_schema,
|
|
71
|
+
},
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Throw on malformed JSON so truncated tool args surface as an error instead
|
|
76
|
+
* of being silently coerced to `{}` (which would record zero transactions).
|
|
77
|
+
*/
|
|
78
|
+
export function parseArguments(toolName, args) {
|
|
79
|
+
if (typeof args !== "string")
|
|
80
|
+
return args;
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(args);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
const preview = args.length > 120 ? `${args.slice(0, 120)}…` : args;
|
|
86
|
+
throw new Error(`Tool call arguments for "${toolName}" were not valid JSON (likely truncated by max_tokens): ${preview}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function convertToolResults(toolResults) {
|
|
90
|
+
return toolResults.map((tr) => ({
|
|
91
|
+
role: "tool",
|
|
92
|
+
tool_call_id: tr.tool_use_id,
|
|
93
|
+
content: tr.content,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
export function convertAssistantMessage(blocks) {
|
|
97
|
+
const textParts = blocks
|
|
98
|
+
.filter((b) => b.type === "text")
|
|
99
|
+
.map((b) => b.text)
|
|
100
|
+
.join("\n");
|
|
101
|
+
const toolCalls = blocks
|
|
102
|
+
.filter((b) => b.type === "tool_use")
|
|
103
|
+
.map((tu) => ({
|
|
104
|
+
id: tu.id,
|
|
105
|
+
type: "function",
|
|
106
|
+
function: { name: tu.name, arguments: JSON.stringify(tu.input) },
|
|
107
|
+
}));
|
|
108
|
+
return {
|
|
109
|
+
role: "assistant",
|
|
110
|
+
content: textParts || null,
|
|
111
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function isToolResultEnvelope(content) {
|
|
115
|
+
return (Array.isArray(content) &&
|
|
116
|
+
content.length > 0 &&
|
|
117
|
+
content[0].type === "tool_result");
|
|
118
|
+
}
|
|
@@ -1,5 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import type { Provider, NormalizedResponse, NormalizedContentBlock, NormalizedMessage, NormalizedToolResult, ToolDefinition } from "../provider.js";
|
|
3
|
+
export type ChatCompletion = OpenAI.Chat.Completions.ChatCompletion;
|
|
4
|
+
type RequestOptions = Parameters<OpenAI["chat"]["completions"]["create"]>[1];
|
|
5
|
+
export interface CompletionBody {
|
|
6
|
+
model: string;
|
|
7
|
+
maxTokens: number;
|
|
8
|
+
messages: OpenAI.ChatCompletionMessageParam[];
|
|
9
|
+
tools: OpenAI.ChatCompletionTool[] | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare function createOpenAIProvider(opts: {
|
|
3
12
|
apiKey: string;
|
|
4
|
-
baseURL: string;
|
|
5
13
|
}): Provider;
|
|
14
|
+
/**
|
|
15
|
+
* Older models accept `max_tokens`; o-series / gpt-5+ require
|
|
16
|
+
* `max_completion_tokens`. Try the former, fall back on the 400.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createCompletionWithTokenFallback(client: OpenAI, body: CompletionBody, options: RequestOptions): Promise<ChatCompletion>;
|
|
19
|
+
export declare function normalizeResponse(response: ChatCompletion): NormalizedResponse;
|
|
20
|
+
export declare function convertTools(tools: ToolDefinition[]): OpenAI.ChatCompletionTool[];
|
|
21
|
+
/**
|
|
22
|
+
* Throw on malformed JSON so truncated tool args surface as an error instead
|
|
23
|
+
* of being silently coerced to `{}` (which would record zero transactions).
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseArguments(toolName: string, args: string): unknown;
|
|
26
|
+
export declare function convertToolResults(toolResults: NormalizedToolResult[]): OpenAI.ChatCompletionToolMessageParam[];
|
|
27
|
+
export declare function convertAssistantMessage(blocks: NormalizedContentBlock[]): OpenAI.ChatCompletionAssistantMessageParam;
|
|
28
|
+
export declare function isToolResultEnvelope(content: NormalizedMessage["content"]): content is NormalizedToolResult[];
|
|
29
|
+
export {};
|
|
@@ -1,38 +1,15 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
2
|
import { classifyProviderError } from "../errors.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return err.status === 400 && (err.message?.includes("max_tokens") ?? false);
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Some OpenAI-compatible endpoints (older models, Ollama, vLLM) accept `max_tokens`;
|
|
9
|
-
* newer OpenAI models require `max_completion_tokens`. Try the former, fall back on a
|
|
10
|
-
* 400 that explicitly names the parameter.
|
|
11
|
-
*/
|
|
12
|
-
async function createCompletionWithTokenFallback(client, body, options) {
|
|
13
|
-
const base = {
|
|
14
|
-
model: body.model,
|
|
15
|
-
messages: body.messages,
|
|
16
|
-
tools: body.tools,
|
|
17
|
-
};
|
|
18
|
-
try {
|
|
19
|
-
return await client.chat.completions.create({ ...base, max_tokens: body.maxTokens }, options);
|
|
20
|
-
}
|
|
21
|
-
catch (e) {
|
|
22
|
-
if (isMaxTokensRejection(e)) {
|
|
23
|
-
return await client.chat.completions.create({ ...base, max_completion_tokens: body.maxTokens }, options);
|
|
24
|
-
}
|
|
25
|
-
throw e;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
export function createOpenAICompatibleProvider(opts) {
|
|
3
|
+
const OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
4
|
+
export function createOpenAIProvider(opts) {
|
|
29
5
|
const client = new OpenAI({
|
|
30
6
|
apiKey: opts.apiKey,
|
|
31
|
-
baseURL:
|
|
7
|
+
baseURL: OPENAI_BASE_URL,
|
|
32
8
|
});
|
|
33
9
|
return {
|
|
34
|
-
name: "openai
|
|
10
|
+
name: "openai",
|
|
35
11
|
supportsThinking: false,
|
|
12
|
+
acceptsDocuments: true,
|
|
36
13
|
async sendMessage(params) {
|
|
37
14
|
const tools = convertTools(params.tools);
|
|
38
15
|
const body = {
|
|
@@ -52,7 +29,94 @@ export function createOpenAICompatibleProvider(opts) {
|
|
|
52
29
|
},
|
|
53
30
|
};
|
|
54
31
|
}
|
|
55
|
-
function
|
|
32
|
+
function convertMessages(system, messages) {
|
|
33
|
+
const result = [
|
|
34
|
+
{ role: "system", content: system },
|
|
35
|
+
];
|
|
36
|
+
for (const msg of messages) {
|
|
37
|
+
if (msg.role === "user") {
|
|
38
|
+
if (isToolResultEnvelope(msg.content)) {
|
|
39
|
+
result.push(...convertToolResults(msg.content));
|
|
40
|
+
}
|
|
41
|
+
else if (Array.isArray(msg.content)) {
|
|
42
|
+
result.push(buildUserMessage(msg.content));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
result.push({ role: "user", content: msg.content });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
if (Array.isArray(msg.content)) {
|
|
50
|
+
result.push(convertAssistantMessage(msg.content));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
result.push({ role: "assistant", content: msg.content });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
/** Real OpenAI accepts `file` parts for PDFs and `image_url` for images. */
|
|
60
|
+
function buildUserMessage(blocks) {
|
|
61
|
+
const hasAttachment = blocks.some((b) => b.type === "document" || b.type === "image");
|
|
62
|
+
if (!hasAttachment) {
|
|
63
|
+
const text = blocks
|
|
64
|
+
.filter((b) => b.type === "text")
|
|
65
|
+
.map((b) => b.text)
|
|
66
|
+
.join("\n");
|
|
67
|
+
return { role: "user", content: text };
|
|
68
|
+
}
|
|
69
|
+
const parts = [];
|
|
70
|
+
for (const block of blocks) {
|
|
71
|
+
if (block.type === "text") {
|
|
72
|
+
parts.push({ type: "text", text: block.text });
|
|
73
|
+
}
|
|
74
|
+
else if (block.type === "document") {
|
|
75
|
+
parts.push({
|
|
76
|
+
type: "file",
|
|
77
|
+
file: {
|
|
78
|
+
filename: block.title ?? "document.pdf",
|
|
79
|
+
file_data: `data:${block.source.media_type};base64,${block.source.data}`,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else if (block.type === "image") {
|
|
84
|
+
parts.push({
|
|
85
|
+
type: "image_url",
|
|
86
|
+
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { role: "user", content: parts };
|
|
91
|
+
}
|
|
92
|
+
/* ------------------------------------------------------------------ */
|
|
93
|
+
/* Shared helpers — also imported by openai-compat.ts. */
|
|
94
|
+
/* ------------------------------------------------------------------ */
|
|
95
|
+
function isMaxTokensRejection(e) {
|
|
96
|
+
const err = e;
|
|
97
|
+
return err.status === 400 && (err.message?.includes("max_tokens") ?? false);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Older models accept `max_tokens`; o-series / gpt-5+ require
|
|
101
|
+
* `max_completion_tokens`. Try the former, fall back on the 400.
|
|
102
|
+
*/
|
|
103
|
+
export async function createCompletionWithTokenFallback(client, body, options) {
|
|
104
|
+
const base = {
|
|
105
|
+
model: body.model,
|
|
106
|
+
messages: body.messages,
|
|
107
|
+
tools: body.tools,
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
return await client.chat.completions.create({ ...base, max_tokens: body.maxTokens }, options);
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
if (isMaxTokensRejection(e)) {
|
|
114
|
+
return await client.chat.completions.create({ ...base, max_completion_tokens: body.maxTokens }, options);
|
|
115
|
+
}
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function normalizeResponse(response) {
|
|
56
120
|
const choice = response.choices[0];
|
|
57
121
|
if (!choice) {
|
|
58
122
|
return { content: [], stopReason: "end_turn" };
|
|
@@ -69,77 +133,29 @@ function normalizeResponse(response) {
|
|
|
69
133
|
type: "tool_use",
|
|
70
134
|
id: tc.id,
|
|
71
135
|
name: tc.function.name,
|
|
72
|
-
input: parseArguments(tc.function.arguments),
|
|
136
|
+
input: parseArguments(tc.function.name, tc.function.arguments),
|
|
73
137
|
});
|
|
74
138
|
}
|
|
75
139
|
}
|
|
76
140
|
const hasToolCalls = content.some((b) => b.type === "tool_use");
|
|
141
|
+
/**
|
|
142
|
+
* finish_reason "length" → "max_tokens" so the agent loop records a
|
|
143
|
+
* scan_truncated question instead of silently accepting a partial batch.
|
|
144
|
+
*/
|
|
145
|
+
const stopReason = choice.finish_reason === "length"
|
|
146
|
+
? "max_tokens"
|
|
147
|
+
: hasToolCalls
|
|
148
|
+
? "tool_use"
|
|
149
|
+
: "end_turn";
|
|
77
150
|
return {
|
|
78
151
|
content,
|
|
79
|
-
stopReason
|
|
152
|
+
stopReason,
|
|
80
153
|
usage: response.usage
|
|
81
154
|
? { input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens }
|
|
82
155
|
: undefined,
|
|
83
156
|
};
|
|
84
157
|
}
|
|
85
|
-
function
|
|
86
|
-
const result = [
|
|
87
|
-
{ role: "system", content: system },
|
|
88
|
-
];
|
|
89
|
-
for (const msg of messages) {
|
|
90
|
-
if (msg.role === "user") {
|
|
91
|
-
if (Array.isArray(msg.content) &&
|
|
92
|
-
msg.content.length > 0 &&
|
|
93
|
-
msg.content[0].type === "tool_result") {
|
|
94
|
-
const toolResults = msg.content;
|
|
95
|
-
for (const tr of toolResults) {
|
|
96
|
-
result.push({
|
|
97
|
-
role: "tool",
|
|
98
|
-
tool_call_id: tr.tool_use_id,
|
|
99
|
-
content: tr.content,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
else if (Array.isArray(msg.content)) {
|
|
104
|
-
// Strip document blocks (OpenAI-compat doesn't accept them); keep text.
|
|
105
|
-
const text = msg.content
|
|
106
|
-
.filter((b) => b.type === "text")
|
|
107
|
-
.map((b) => b.text)
|
|
108
|
-
.join("\n");
|
|
109
|
-
result.push({ role: "user", content: text });
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
result.push({ role: "user", content: msg.content });
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
if (Array.isArray(msg.content)) {
|
|
117
|
-
const blocks = msg.content;
|
|
118
|
-
const textParts = blocks
|
|
119
|
-
.filter((b) => b.type === "text")
|
|
120
|
-
.map((b) => b.text)
|
|
121
|
-
.join("\n");
|
|
122
|
-
const toolCalls = blocks
|
|
123
|
-
.filter((b) => b.type === "tool_use")
|
|
124
|
-
.map((tu) => ({
|
|
125
|
-
id: tu.id,
|
|
126
|
-
type: "function",
|
|
127
|
-
function: { name: tu.name, arguments: JSON.stringify(tu.input) },
|
|
128
|
-
}));
|
|
129
|
-
result.push({
|
|
130
|
-
role: "assistant",
|
|
131
|
-
content: textParts || null,
|
|
132
|
-
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
result.push({ role: "assistant", content: msg.content });
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return result;
|
|
141
|
-
}
|
|
142
|
-
function convertTools(tools) {
|
|
158
|
+
export function convertTools(tools) {
|
|
143
159
|
return tools.map((t) => ({
|
|
144
160
|
type: "function",
|
|
145
161
|
function: {
|
|
@@ -149,13 +165,48 @@ function convertTools(tools) {
|
|
|
149
165
|
},
|
|
150
166
|
}));
|
|
151
167
|
}
|
|
152
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Throw on malformed JSON so truncated tool args surface as an error instead
|
|
170
|
+
* of being silently coerced to `{}` (which would record zero transactions).
|
|
171
|
+
*/
|
|
172
|
+
export function parseArguments(toolName, args) {
|
|
153
173
|
if (typeof args !== "string")
|
|
154
174
|
return args;
|
|
155
175
|
try {
|
|
156
176
|
return JSON.parse(args);
|
|
157
177
|
}
|
|
158
178
|
catch {
|
|
159
|
-
|
|
179
|
+
const preview = args.length > 120 ? `${args.slice(0, 120)}…` : args;
|
|
180
|
+
throw new Error(`Tool call arguments for "${toolName}" were not valid JSON (likely truncated by max_tokens): ${preview}`);
|
|
160
181
|
}
|
|
161
182
|
}
|
|
183
|
+
export function convertToolResults(toolResults) {
|
|
184
|
+
return toolResults.map((tr) => ({
|
|
185
|
+
role: "tool",
|
|
186
|
+
tool_call_id: tr.tool_use_id,
|
|
187
|
+
content: tr.content,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
export function convertAssistantMessage(blocks) {
|
|
191
|
+
const textParts = blocks
|
|
192
|
+
.filter((b) => b.type === "text")
|
|
193
|
+
.map((b) => b.text)
|
|
194
|
+
.join("\n");
|
|
195
|
+
const toolCalls = blocks
|
|
196
|
+
.filter((b) => b.type === "tool_use")
|
|
197
|
+
.map((tu) => ({
|
|
198
|
+
id: tu.id,
|
|
199
|
+
type: "function",
|
|
200
|
+
function: { name: tu.name, arguments: JSON.stringify(tu.input) },
|
|
201
|
+
}));
|
|
202
|
+
return {
|
|
203
|
+
role: "assistant",
|
|
204
|
+
content: textParts || null,
|
|
205
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
export function isToolResultEnvelope(content) {
|
|
209
|
+
return (Array.isArray(content) &&
|
|
210
|
+
content.length > 0 &&
|
|
211
|
+
content[0].type === "tool_result");
|
|
212
|
+
}
|
package/dist/ai/thinking.js
CHANGED
|
@@ -28,7 +28,7 @@ const THINKING_PHRASES = [
|
|
|
28
28
|
"Doing math, the slow kind...",
|
|
29
29
|
"Computing... probably correctly...",
|
|
30
30
|
"Pondering quietly...",
|
|
31
|
-
"
|
|
31
|
+
"Connecting the dots...",
|
|
32
32
|
"Making sense of it...",
|
|
33
33
|
"Sharpening the pencil...",
|
|
34
34
|
"Catching up on the details...",
|
|
@@ -31,12 +31,7 @@ function formatSummary(summary) {
|
|
|
31
31
|
if (summary.total === 0) {
|
|
32
32
|
return chalk.dim("No questions.");
|
|
33
33
|
}
|
|
34
|
-
const
|
|
35
|
-
.map(([k, v]) => `${k}×${v}`)
|
|
36
|
-
.join(", ");
|
|
37
|
-
const lines = [
|
|
38
|
-
chalk.bold(`Clarified ${summary.clarified}/${summary.total} questions${tally ? ` (${tally})` : ""}.`),
|
|
39
|
-
];
|
|
34
|
+
const lines = [chalk.bold(`Clarified ${summary.clarified}/${summary.total} questions.`)];
|
|
40
35
|
if (summary.remaining > 0) {
|
|
41
36
|
lines.push(chalk.yellow(`${summary.remaining} question(s) remain.`));
|
|
42
37
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { getDb } from "../../db/connection.js";
|
|
3
3
|
import { runScan } from "../../scanner/engine.js";
|
|
4
|
+
import { getActiveModel } from "../../config.js";
|
|
5
|
+
import { getProvider } from "../../ai/providers/index.js";
|
|
4
6
|
export async function runScanCommand(opts) {
|
|
5
7
|
if (opts.regex) {
|
|
6
8
|
try {
|
|
@@ -60,9 +62,16 @@ async function buildTtyHooks() {
|
|
|
60
62
|
fileName: d.fileName,
|
|
61
63
|
totalPages: s.chunks.filter((c) => c.fileId === d.path).length,
|
|
62
64
|
}));
|
|
65
|
+
const provider = getProvider();
|
|
66
|
+
const attachment = {
|
|
67
|
+
format: provider.acceptsDocuments ? "pdf" : "png",
|
|
68
|
+
providerName: provider.name,
|
|
69
|
+
modelName: getActiveModel(),
|
|
70
|
+
};
|
|
63
71
|
inkInstance = render(createElement(ScanDashboard, {
|
|
64
72
|
controller,
|
|
65
73
|
files,
|
|
74
|
+
attachment,
|
|
66
75
|
}), {
|
|
67
76
|
exitOnCtrlC: false,
|
|
68
77
|
patchConsole: false,
|
|
@@ -36,14 +36,15 @@ export interface FileSeed {
|
|
|
36
36
|
readonly fileName: string;
|
|
37
37
|
readonly totalPages: number;
|
|
38
38
|
}
|
|
39
|
+
export interface AttachmentInfo {
|
|
40
|
+
format: "pdf" | "png";
|
|
41
|
+
providerName: string;
|
|
42
|
+
modelName: string;
|
|
43
|
+
}
|
|
39
44
|
interface Props {
|
|
40
45
|
controller: ScanDashboardController;
|
|
41
46
|
files: ReadonlyArray<FileSeed>;
|
|
47
|
+
attachment: AttachmentInfo;
|
|
42
48
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Tree-layout scan dashboard. Header carries the only animated element (one
|
|
45
|
-
* `<Spinner>`). All other status indicators are static glyphs that only
|
|
46
|
-
* redraw when their data changes.
|
|
47
|
-
*/
|
|
48
49
|
export declare function ScanDashboard(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
49
50
|
export {};
|
|
@@ -30,16 +30,15 @@ const COL = {
|
|
|
30
30
|
transactions: 13,
|
|
31
31
|
questions: 10,
|
|
32
32
|
};
|
|
33
|
-
/**
|
|
34
|
-
* Tree-layout scan dashboard. Header carries the only animated element (one
|
|
35
|
-
* `<Spinner>`). All other status indicators are static glyphs that only
|
|
36
|
-
* redraw when their data changes.
|
|
37
|
-
*/
|
|
38
33
|
export function ScanDashboard(props) {
|
|
39
34
|
const rows = useFileGroups(props.controller, props.files);
|
|
40
35
|
const phase = usePhase(props.controller);
|
|
41
36
|
const ruleWidth = useRuleWidth();
|
|
42
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { phase: phase }), _jsx(Box, { marginTop: 1, children: _jsx(ColumnHeader, {}) }), _jsx(Divider, { width: ruleWidth }), Array.from(rows.entries()).map(([fileId, group]) => (_jsx(FileGroupView, { group: group }, fileId))), _jsx(Divider, { width: ruleWidth })] }));
|
|
37
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { phase: phase }), _jsx(AttachmentLine, { info: props.attachment }), _jsx(Box, { marginTop: 1, children: _jsx(ColumnHeader, {}) }), _jsx(Divider, { width: ruleWidth }), Array.from(rows.entries()).map(([fileId, group]) => (_jsx(FileGroupView, { group: group }, fileId))), _jsx(Divider, { width: ruleWidth })] }));
|
|
38
|
+
}
|
|
39
|
+
function AttachmentLine({ info }) {
|
|
40
|
+
const detail = info.format === "pdf" ? "pdf (native)" : "png (rasterized)";
|
|
41
|
+
return (_jsxs(Text, { dimColor: true, children: ["sending: ", detail, " \u00B7 ", info.providerName, " \u00B7 ", info.modelName] }));
|
|
43
42
|
}
|
|
44
43
|
function usePhase(controller) {
|
|
45
44
|
const [phase, setPhase] = useState("parse");
|