plasalid 0.7.9 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -6
- 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/cli/commands/scan.js +78 -10
- package/dist/cli/commands/status.js +15 -2
- package/dist/cli/ink/ScanDashboard.d.ts +7 -6
- package/dist/cli/ink/ScanDashboard.js +14 -6
- package/dist/cli/setup.js +175 -119
- package/dist/config.d.ts +10 -4
- package/dist/config.js +40 -11
- package/dist/scanner/clarifier.d.ts +2 -0
- package/dist/scanner/clarifier.js +1 -0
- package/dist/scanner/concurrency.d.ts +9 -2
- package/dist/scanner/concurrency.js +3 -1
- package/dist/scanner/engine.d.ts +2 -1
- package/dist/scanner/engine.js +21 -3
- package/dist/scanner/hooks.d.ts +6 -0
- package/dist/scanner/parse.js +28 -16
- 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.d.ts +6 -0
- package/dist/scanner/worker.js +16 -3
- package/package.json +2 -1
|
@@ -30,16 +30,18 @@ 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 }), phase !== "done" && _jsx(Footnote, {})] }));
|
|
38
|
+
}
|
|
39
|
+
function Footnote() {
|
|
40
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "output accuracy depends on the model's VL capability." }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "we also provide " }), _jsx(Text, { color: "cyan", children: "clarify" }), _jsx(Text, { dimColor: true, children: ", " }), _jsx(Text, { color: "cyan", children: "record" }), _jsx(Text, { dimColor: true, children: ", and " }), _jsx(Text, { color: "cyan", children: "chat" }), _jsx(Text, { dimColor: true, children: " to rectify the data later." })] })] }));
|
|
41
|
+
}
|
|
42
|
+
function AttachmentLine({ info }) {
|
|
43
|
+
const detail = info.format === "pdf" ? "pdf (native)" : "png (rasterized)";
|
|
44
|
+
return (_jsxs(Text, { dimColor: true, children: ["sending: ", detail, " (", info.providerName, "/", info.modelName, ")"] }));
|
|
43
45
|
}
|
|
44
46
|
function usePhase(controller) {
|
|
45
47
|
const [phase, setPhase] = useState("parse");
|
|
@@ -79,6 +81,12 @@ function phaseStateOf(label, current) {
|
|
|
79
81
|
return "pending";
|
|
80
82
|
}
|
|
81
83
|
function Header({ phase }) {
|
|
84
|
+
// Cancellation collapses the parse/clarify segments — neither is still
|
|
85
|
+
// running once the user hits Ctrl+C, and showing them as "pending" would
|
|
86
|
+
// be misleading. The single "cancelling…" label communicates the wind-down.
|
|
87
|
+
if (phase === "cancelling") {
|
|
88
|
+
return (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Scanner" }), _jsx(Text, { dimColor: true, children: " · " }), _jsx(Text, { color: "green", children: "\u2713 decrypt" }), _jsx(Text, { dimColor: true, children: " -> " }), _jsx(Text, { color: "green", children: "\u2713 chunk" }), _jsx(Text, { dimColor: true, children: " -> " }), _jsxs(Text, { color: "red", children: [_jsx(Spinner, { type: "dots" }), " cancelling\u2026"] })] }));
|
|
89
|
+
}
|
|
82
90
|
return (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Scanner" }), _jsx(Text, { dimColor: true, children: " · " }), _jsx(Text, { color: "green", children: "\u2713 decrypt" }), _jsx(Text, { dimColor: true, children: " -> " }), _jsx(Text, { color: "green", children: "\u2713 chunk" }), _jsx(Text, { dimColor: true, children: " -> " }), PHASE_RENDER[phaseStateOf("parse", phase)]("parse"), _jsx(Text, { dimColor: true, children: " -> " }), PHASE_RENDER[phaseStateOf("clarify", phase)]("clarify")] }));
|
|
83
91
|
}
|
|
84
92
|
function ColumnHeader() {
|
package/dist/cli/setup.js
CHANGED
|
@@ -6,26 +6,24 @@ import { config, saveConfig, getConfigPath, getPlasalidDir, getDataDir, } from "
|
|
|
6
6
|
import { generateKey } from "../db/encryption.js";
|
|
7
7
|
import { createContextTemplate } from "../ai/context.js";
|
|
8
8
|
import { printLogo } from "./logo.js";
|
|
9
|
-
|
|
10
|
-
const
|
|
9
|
+
import { statusSpinner } from "./ux.js";
|
|
10
|
+
const DEFAULT_LOCAL_OPENAI_BASE_URL = "http://localhost:11434/v1";
|
|
11
|
+
const RECOMMENDED_MODEL = {
|
|
12
|
+
anthropic: "claude-sonnet-4-6",
|
|
13
|
+
openai: "gpt-5.4-mini",
|
|
14
|
+
gemini: "gemini-2.5-pro",
|
|
15
|
+
};
|
|
11
16
|
function ensureDir(p) {
|
|
12
17
|
if (!existsSync(p))
|
|
13
18
|
mkdirSync(p, { recursive: true });
|
|
14
19
|
}
|
|
15
|
-
function readEnvDefaults() {
|
|
16
|
-
return {
|
|
17
|
-
anthropicKey: process.env.ANTHROPIC_API_KEY?.trim() || "",
|
|
18
|
-
userName: process.env.PLASALID_USER_NAME?.trim() || "",
|
|
19
|
-
openaiBaseURL: process.env.OPENAI_COMPATIBLE_BASE_URL?.trim() || "",
|
|
20
|
-
openaiKey: process.env.OPENAI_COMPATIBLE_API_KEY?.trim() || "",
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
20
|
function printBanner() {
|
|
24
21
|
console.log("");
|
|
25
22
|
printLogo();
|
|
26
23
|
console.log("");
|
|
27
24
|
console.log("Welcome to Plasalid. Let's get you set up — a few quick questions.");
|
|
28
25
|
console.log("");
|
|
26
|
+
console.log(chalk.dim("Time to power up your engine — wire in an AI, pick a model, seal your vault."));
|
|
29
27
|
}
|
|
30
28
|
function printSummary(dataDir) {
|
|
31
29
|
console.log("");
|
|
@@ -42,9 +40,10 @@ function printSummary(dataDir) {
|
|
|
42
40
|
console.log(chalk.dim(` Optional: ${chalk.cyan(`plasalid record "..."`)}${chalk.dim(" to record manual/undocumented transaction, balance, or account at any time.")}`));
|
|
43
41
|
}
|
|
44
42
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
43
|
+
* Each helper prints one leading blank line. Inquirer collapses the resolved
|
|
44
|
+
* prompt to a single line, so each new helper produces exactly one blank row
|
|
45
|
+
* between adjacent questions. passwordPrompt has no `default` because the
|
|
46
|
+
* masked-but-pre-filled state confuses "press Enter to keep".
|
|
48
47
|
*/
|
|
49
48
|
async function listPrompt(opts) {
|
|
50
49
|
console.log("");
|
|
@@ -53,134 +52,193 @@ async function listPrompt(opts) {
|
|
|
53
52
|
type: "list",
|
|
54
53
|
name: opts.name,
|
|
55
54
|
message: opts.message,
|
|
56
|
-
choices:
|
|
55
|
+
choices: opts.choices,
|
|
57
56
|
default: opts.default,
|
|
58
57
|
},
|
|
59
58
|
]);
|
|
60
|
-
console.log("");
|
|
61
59
|
return answer[opts.name];
|
|
62
60
|
}
|
|
63
|
-
async function
|
|
64
|
-
|
|
61
|
+
async function inputPrompt(opts) {
|
|
62
|
+
console.log("");
|
|
63
|
+
const answer = await inquirer.prompt([
|
|
65
64
|
{
|
|
66
65
|
type: "input",
|
|
67
|
-
name:
|
|
68
|
-
message:
|
|
69
|
-
default:
|
|
66
|
+
name: opts.name,
|
|
67
|
+
message: opts.message,
|
|
68
|
+
default: opts.default,
|
|
69
|
+
validate: opts.validate,
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
return String(answer[opts.name] ?? "").trim();
|
|
73
|
+
}
|
|
74
|
+
async function passwordPrompt(opts) {
|
|
75
|
+
console.log("");
|
|
76
|
+
const answer = await inquirer.prompt([
|
|
77
|
+
{
|
|
78
|
+
type: "password",
|
|
79
|
+
name: opts.name,
|
|
80
|
+
message: opts.message,
|
|
81
|
+
mask: "*",
|
|
82
|
+
validate: opts.validate,
|
|
70
83
|
},
|
|
71
84
|
]);
|
|
72
|
-
return String(
|
|
85
|
+
return String(answer[opts.name] ?? "");
|
|
86
|
+
}
|
|
87
|
+
function savedModelFor(vendor) {
|
|
88
|
+
switch (vendor) {
|
|
89
|
+
case "anthropic":
|
|
90
|
+
return config.anthropicModel;
|
|
91
|
+
case "openai":
|
|
92
|
+
return config.openaiModel;
|
|
93
|
+
case "gemini":
|
|
94
|
+
return config.geminiModel;
|
|
95
|
+
case "openai-compat":
|
|
96
|
+
return config.openaiCompatModel;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function promptUserName() {
|
|
100
|
+
return inputPrompt({
|
|
101
|
+
name: "userName",
|
|
102
|
+
message: "What should I call you? (Your name)",
|
|
103
|
+
});
|
|
73
104
|
}
|
|
74
105
|
async function promptProviderChoice() {
|
|
75
106
|
return listPrompt({
|
|
76
|
-
name: "
|
|
107
|
+
name: "vendor",
|
|
77
108
|
message: "Which AI provider would you like to use?",
|
|
78
109
|
choices: [
|
|
79
|
-
{ name: "Anthropic
|
|
110
|
+
{ name: "Anthropic", value: "anthropic" },
|
|
111
|
+
{ name: "OpenAI", value: "openai" },
|
|
112
|
+
{ name: "Google Gemini", value: "gemini" },
|
|
80
113
|
{
|
|
81
|
-
name: "OpenAI
|
|
82
|
-
value: "openai-
|
|
114
|
+
name: "OpenAI Compatible (LM Studio, vLLM, Ollama, other)",
|
|
115
|
+
value: "openai-compat",
|
|
83
116
|
},
|
|
84
117
|
],
|
|
85
118
|
default: "anthropic",
|
|
86
119
|
});
|
|
87
120
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
model: model || DEFAULT_ANTHROPIC_MODEL,
|
|
110
|
-
};
|
|
121
|
+
/**
|
|
122
|
+
* Model default: the value previously saved for this vendor, else its
|
|
123
|
+
* recommended flagship. openai-compat has none, so it starts blank and the
|
|
124
|
+
* required-non-empty validator catches blank submissions.
|
|
125
|
+
*/
|
|
126
|
+
async function promptModelInput(vendor) {
|
|
127
|
+
const carriedOver = savedModelFor(vendor);
|
|
128
|
+
const recommended = vendor === "openai-compat" ? "" : RECOMMENDED_MODEL[vendor];
|
|
129
|
+
const defaultValue = carriedOver || recommended;
|
|
130
|
+
// openai-compat has no single recommended model — the scanner rasterizes
|
|
131
|
+
// PDFs to PNG on this path, so any non-vision model will fail on scan. Steer
|
|
132
|
+
// the user toward a vision-language model in the prompt.
|
|
133
|
+
const message = vendor === "openai-compat"
|
|
134
|
+
? "Which AI model? (use a vision-language model)"
|
|
135
|
+
: `Which AI model? (recommended: ${RECOMMENDED_MODEL[vendor]})`;
|
|
136
|
+
return inputPrompt({
|
|
137
|
+
name: "model",
|
|
138
|
+
message,
|
|
139
|
+
default: defaultValue || undefined,
|
|
140
|
+
validate: (v) => v.trim().length > 0 || "Required",
|
|
141
|
+
});
|
|
111
142
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
type: "input",
|
|
132
|
-
name: "model",
|
|
133
|
-
message: "Which model? (e.g. gpt-5, qwen3-coder:480b, deepseek-v3.1:671b)",
|
|
134
|
-
default: config.providerType === "openai-compatible" && config.model
|
|
135
|
-
? config.model
|
|
136
|
-
: "",
|
|
137
|
-
validate: (v) => v.trim().length > 0 || "Required",
|
|
143
|
+
/**
|
|
144
|
+
* Empty submit silently keeps the existing key when one is on file. Otherwise
|
|
145
|
+
* the validator rejects empty (or accepts empty when `optional`, e.g. local
|
|
146
|
+
* servers that need no auth).
|
|
147
|
+
*/
|
|
148
|
+
async function promptApiKey(opts) {
|
|
149
|
+
const hasExisting = opts.existing.length > 0;
|
|
150
|
+
const fresh = await passwordPrompt({
|
|
151
|
+
name: "key",
|
|
152
|
+
message: `${opts.label}:`,
|
|
153
|
+
validate: (v) => {
|
|
154
|
+
if (v === "" && (hasExisting || opts.optional))
|
|
155
|
+
return true;
|
|
156
|
+
if (opts.prefix && !v.startsWith(opts.prefix)) {
|
|
157
|
+
return `Enter a key starting with ${opts.prefix}...`;
|
|
158
|
+
}
|
|
159
|
+
if (v.length === 0)
|
|
160
|
+
return "Required";
|
|
161
|
+
return true;
|
|
138
162
|
},
|
|
139
|
-
|
|
163
|
+
});
|
|
164
|
+
return fresh === "" && hasExisting ? opts.existing : fresh;
|
|
165
|
+
}
|
|
166
|
+
async function promptAnthropicCredentials() {
|
|
167
|
+
const anthropicKey = await promptApiKey({
|
|
168
|
+
label: "Paste your Anthropic API key (https://console.anthropic.com)",
|
|
169
|
+
existing: config.anthropicKey,
|
|
170
|
+
prefix: "sk-",
|
|
171
|
+
});
|
|
172
|
+
const anthropicModel = await promptModelInput("anthropic");
|
|
173
|
+
return { providerType: "anthropic", anthropicKey, anthropicModel };
|
|
174
|
+
}
|
|
175
|
+
async function promptOpenAICredentials() {
|
|
176
|
+
const openaiKey = await promptApiKey({
|
|
177
|
+
label: "Paste your OpenAI API key (https://platform.openai.com/api-keys)",
|
|
178
|
+
existing: config.openaiKey,
|
|
179
|
+
prefix: "sk-",
|
|
180
|
+
});
|
|
181
|
+
const openaiModel = await promptModelInput("openai");
|
|
182
|
+
return { providerType: "openai", openaiKey, openaiModel };
|
|
183
|
+
}
|
|
184
|
+
async function promptGeminiCredentials() {
|
|
185
|
+
const geminiKey = await promptApiKey({
|
|
186
|
+
label: "Paste your Google AI Studio API key (https://aistudio.google.com/apikey)",
|
|
187
|
+
existing: config.geminiKey,
|
|
188
|
+
});
|
|
189
|
+
const geminiModel = await promptModelInput("gemini");
|
|
190
|
+
return { providerType: "gemini", geminiKey, geminiModel };
|
|
191
|
+
}
|
|
192
|
+
async function promptOpenAICompatCredentials() {
|
|
193
|
+
const baseURLDefault = config.providerType === "openai-compat" && config.openaiCompatBaseURL
|
|
194
|
+
? config.openaiCompatBaseURL
|
|
195
|
+
: DEFAULT_LOCAL_OPENAI_BASE_URL;
|
|
196
|
+
const openaiCompatBaseURL = await inputPrompt({
|
|
197
|
+
name: "baseURL",
|
|
198
|
+
message: "What's the base URL of your LLM server?",
|
|
199
|
+
default: baseURLDefault,
|
|
200
|
+
validate: (v) => /^https?:\/\//.test(v) || "Must start with http:// or https://",
|
|
201
|
+
});
|
|
202
|
+
const openaiCompatKey = await promptApiKey({
|
|
203
|
+
label: "Paste your LLM server API key",
|
|
204
|
+
existing: config.openaiCompatKey,
|
|
205
|
+
optional: true,
|
|
206
|
+
});
|
|
207
|
+
const openaiCompatModel = await promptModelInput("openai-compat");
|
|
140
208
|
return {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
209
|
+
providerType: "openai-compat",
|
|
210
|
+
openaiCompatBaseURL,
|
|
211
|
+
openaiCompatKey,
|
|
212
|
+
openaiCompatModel,
|
|
144
213
|
};
|
|
145
214
|
}
|
|
146
|
-
async function promptCredentials(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
215
|
+
async function promptCredentials(vendor) {
|
|
216
|
+
switch (vendor) {
|
|
217
|
+
case "anthropic":
|
|
218
|
+
return promptAnthropicCredentials();
|
|
219
|
+
case "openai":
|
|
220
|
+
return promptOpenAICredentials();
|
|
221
|
+
case "gemini":
|
|
222
|
+
return promptGeminiCredentials();
|
|
223
|
+
case "openai-compat":
|
|
224
|
+
return promptOpenAICompatCredentials();
|
|
156
225
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (mode === "auto") {
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Encryption key is auto-generated. The work is microseconds,
|
|
229
|
+
* but the banner just told the user to "seal your vault" — hold the spinner
|
|
230
|
+
* so the step is visible.
|
|
231
|
+
*/
|
|
232
|
+
async function sealVault() {
|
|
233
|
+
const spinner = statusSpinner("Sealing your vault…");
|
|
234
|
+
const start = Date.now();
|
|
235
|
+
if (!config.dbEncryptionKey) {
|
|
168
236
|
saveConfig({ dbEncryptionKey: generateKey() });
|
|
169
|
-
console.log(chalk.dim(`Generated a new DB encryption key and saved it to ${getConfigPath()}.`));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (mode === "manual") {
|
|
173
|
-
const { key: passphrase } = await inquirer.prompt([
|
|
174
|
-
{
|
|
175
|
-
type: "password",
|
|
176
|
-
name: "key",
|
|
177
|
-
message: "Choose a passphrase (at least 8 characters):",
|
|
178
|
-
mask: "*",
|
|
179
|
-
validate: (v) => v.length >= 8 || "Use at least 8 characters.",
|
|
180
|
-
},
|
|
181
|
-
]);
|
|
182
|
-
saveConfig({ dbEncryptionKey: passphrase });
|
|
183
237
|
}
|
|
238
|
+
const remaining = 600 - (Date.now() - start);
|
|
239
|
+
if (remaining > 0)
|
|
240
|
+
await new Promise((r) => setTimeout(r, remaining));
|
|
241
|
+
spinner.succeed("Vault sealed.");
|
|
184
242
|
}
|
|
185
243
|
function finalizeDataDir(userName) {
|
|
186
244
|
const dataDir = config.dataDir || resolve(getPlasalidDir(), "data");
|
|
@@ -192,16 +250,14 @@ function finalizeDataDir(userName) {
|
|
|
192
250
|
export async function runSetup() {
|
|
193
251
|
printBanner();
|
|
194
252
|
ensureDir(getPlasalidDir());
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const credentials = await promptCredentials(provider, env);
|
|
253
|
+
const userName = await promptUserName();
|
|
254
|
+
const vendor = await promptProviderChoice();
|
|
255
|
+
const credentials = await promptCredentials(vendor);
|
|
199
256
|
saveConfig({
|
|
200
|
-
providerType: provider,
|
|
201
257
|
userName: userName || "User",
|
|
202
258
|
...credentials,
|
|
203
259
|
});
|
|
204
|
-
await
|
|
260
|
+
await sealVault();
|
|
205
261
|
const dataDir = finalizeDataDir(userName || "User");
|
|
206
262
|
printSummary(dataDir);
|
|
207
263
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
2
|
export interface PlasalidConfig {
|
|
3
|
+
providerType: "anthropic" | "openai" | "gemini" | "openai-compat";
|
|
3
4
|
anthropicKey: string;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
anthropicModel: string;
|
|
6
|
+
openaiKey: string;
|
|
7
|
+
openaiModel: string;
|
|
8
|
+
geminiKey: string;
|
|
9
|
+
geminiModel: string;
|
|
10
|
+
openaiCompatKey: string;
|
|
11
|
+
openaiCompatBaseURL: string;
|
|
12
|
+
openaiCompatModel: string;
|
|
8
13
|
displayLocale: string;
|
|
9
14
|
displayCurrency: string;
|
|
10
15
|
dbPath: string;
|
|
@@ -18,4 +23,5 @@ export declare function getConfigPath(): string;
|
|
|
18
23
|
export declare function getDataDir(): string;
|
|
19
24
|
export declare const config: PlasalidConfig;
|
|
20
25
|
export declare function isConfigured(): boolean;
|
|
26
|
+
export declare function getActiveModel(): string;
|
|
21
27
|
export declare function saveConfig(partial: Partial<PlasalidConfig>): void;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, } from "fs";
|
|
3
3
|
import { resolve } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
const PLASALID_DIR = resolve(homedir(), ".plasalid");
|
|
@@ -28,28 +28,55 @@ function buildConfig() {
|
|
|
28
28
|
// Precedence: env > file > default. Env is checked first so a shell-exported
|
|
29
29
|
// override always wins over whatever is in ~/.plasalid/config.json.
|
|
30
30
|
return {
|
|
31
|
-
anthropicKey: process.env.ANTHROPIC_API_KEY || file.anthropicKey || "",
|
|
32
|
-
model: process.env.PLASALID_MODEL || file.model || "claude-sonnet-4-6",
|
|
33
31
|
providerType: process.env.PLASALID_PROVIDER ||
|
|
34
32
|
file.providerType ||
|
|
35
33
|
"anthropic",
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
anthropicKey: process.env.ANTHROPIC_API_KEY || file.anthropicKey || "",
|
|
35
|
+
anthropicModel: process.env.ANTHROPIC_MODEL || file.anthropicModel || "claude-sonnet-4-6",
|
|
36
|
+
openaiKey: process.env.OPENAI_API_KEY || file.openaiKey || "",
|
|
37
|
+
openaiModel: process.env.OPENAI_MODEL || file.openaiModel || "gpt-5.4-mini",
|
|
38
|
+
openaiCompatKey: process.env.OPENAI_COMPAT_API_KEY || file.openaiCompatKey || "",
|
|
39
|
+
openaiCompatBaseURL: process.env.OPENAI_COMPAT_BASE_URL || file.openaiCompatBaseURL || "",
|
|
40
|
+
openaiCompatModel: process.env.OPENAI_COMPAT_MODEL || file.openaiCompatModel || "",
|
|
41
|
+
geminiKey: process.env.GEMINI_API_KEY || file.geminiKey || "",
|
|
42
|
+
geminiModel: process.env.GEMINI_MODEL || file.geminiModel || "gemini-2.5-pro",
|
|
38
43
|
displayLocale: file.displayLocale || "th-TH",
|
|
39
44
|
displayCurrency: file.displayCurrency || "THB",
|
|
40
|
-
dbPath: process.env.PLASALID_DB_PATH ||
|
|
45
|
+
dbPath: process.env.PLASALID_DB_PATH ||
|
|
46
|
+
file.dbPath ||
|
|
47
|
+
resolve(PLASALID_DIR, "db.sqlite"),
|
|
41
48
|
dbEncryptionKey: process.env.PLASALID_DB_ENCRYPTION_KEY || file.dbEncryptionKey || "",
|
|
42
|
-
dataDir: process.env.PLASALID_DATA_DIR ||
|
|
49
|
+
dataDir: process.env.PLASALID_DATA_DIR ||
|
|
50
|
+
file.dataDir ||
|
|
51
|
+
resolve(PLASALID_DIR, "data"),
|
|
43
52
|
userName: file.userName || "User",
|
|
44
53
|
thinkingBudget: file.thinkingBudget ?? 8000,
|
|
45
54
|
};
|
|
46
55
|
}
|
|
47
56
|
export const config = buildConfig();
|
|
48
57
|
export function isConfigured() {
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
switch (config.providerType) {
|
|
59
|
+
case "anthropic":
|
|
60
|
+
return !!config.anthropicKey;
|
|
61
|
+
case "openai":
|
|
62
|
+
return !!config.openaiKey;
|
|
63
|
+
case "gemini":
|
|
64
|
+
return !!config.geminiKey;
|
|
65
|
+
case "openai-compat":
|
|
66
|
+
return !!config.openaiCompatBaseURL;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function getActiveModel() {
|
|
70
|
+
switch (config.providerType) {
|
|
71
|
+
case "anthropic":
|
|
72
|
+
return config.anthropicModel;
|
|
73
|
+
case "openai":
|
|
74
|
+
return config.openaiModel;
|
|
75
|
+
case "gemini":
|
|
76
|
+
return config.geminiModel;
|
|
77
|
+
case "openai-compat":
|
|
78
|
+
return config.openaiCompatModel;
|
|
51
79
|
}
|
|
52
|
-
return !!config.anthropicKey;
|
|
53
80
|
}
|
|
54
81
|
export function saveConfig(partial) {
|
|
55
82
|
const configPath = getConfigPath();
|
|
@@ -57,7 +84,9 @@ export function saveConfig(partial) {
|
|
|
57
84
|
mkdirSync(PLASALID_DIR, { recursive: true });
|
|
58
85
|
const existing = loadFileConfig();
|
|
59
86
|
const merged = { ...existing, ...partial };
|
|
60
|
-
writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", {
|
|
87
|
+
writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", {
|
|
88
|
+
mode: 0o600,
|
|
89
|
+
});
|
|
61
90
|
try {
|
|
62
91
|
chmodSync(configPath, 0o600);
|
|
63
92
|
}
|
|
@@ -28,6 +28,8 @@ export interface RunClarifyOpts {
|
|
|
28
28
|
toolCount: number;
|
|
29
29
|
elapsedMs: number;
|
|
30
30
|
}) => void;
|
|
31
|
+
/** When set and aborted, runClarify stops between passes/questions. */
|
|
32
|
+
signal?: AbortSignal;
|
|
31
33
|
}
|
|
32
34
|
export declare const CLARIFIER_PASSES: readonly ClarifierPass[];
|
|
33
35
|
/**
|
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
* never aborts the rest — its slot settles as `{ ok: false, error }` and the
|
|
5
5
|
* caller decides what to do.
|
|
6
6
|
*
|
|
7
|
+
* Pass `signal` to make the pool cancellation-aware: when it aborts, no new
|
|
8
|
+
* task is claimed (tasks already running aren't interrupted — their own
|
|
9
|
+
* signal-aware work is expected to react). Unclaimed slots stay `undefined`
|
|
10
|
+
* in the returned array; the caller can spot them by checking length vs the
|
|
11
|
+
* filled entries.
|
|
12
|
+
*
|
|
7
13
|
* No new dependency. Simple worker-pool: kicks off up to `n` tasks, then each
|
|
8
|
-
* worker pulls the next index from a shared cursor until the queue is drained
|
|
14
|
+
* worker pulls the next index from a shared cursor until the queue is drained
|
|
15
|
+
* or the signal aborts.
|
|
9
16
|
*/
|
|
10
17
|
export type Settled<T> = {
|
|
11
18
|
ok: true;
|
|
@@ -14,4 +21,4 @@ export type Settled<T> = {
|
|
|
14
21
|
ok: false;
|
|
15
22
|
error: unknown;
|
|
16
23
|
};
|
|
17
|
-
export declare function runWithConcurrency<T>(tasks: Array<() => Promise<T>>, n: number): Promise<Settled<T>[]>;
|
|
24
|
+
export declare function runWithConcurrency<T>(tasks: Array<() => Promise<T>>, n: number, signal?: AbortSignal): Promise<Settled<T>[]>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
export async function runWithConcurrency(tasks, n) {
|
|
1
|
+
export async function runWithConcurrency(tasks, n, signal) {
|
|
2
2
|
const results = new Array(tasks.length);
|
|
3
3
|
const workerCount = Math.max(1, Math.min(n, tasks.length));
|
|
4
4
|
let cursor = 0;
|
|
5
5
|
async function worker() {
|
|
6
6
|
while (cursor < tasks.length) {
|
|
7
|
+
if (signal?.aborted)
|
|
8
|
+
return;
|
|
7
9
|
const index = cursor++;
|
|
8
10
|
try {
|
|
9
11
|
results[index] = { ok: true, value: await tasks[index]() };
|
package/dist/scanner/engine.d.ts
CHANGED
|
@@ -65,6 +65,7 @@ export interface ScanState {
|
|
|
65
65
|
readonly startedAt: number;
|
|
66
66
|
readonly options: RunScanOptions;
|
|
67
67
|
readonly progress: ScanProgress;
|
|
68
|
+
readonly signal: AbortSignal;
|
|
68
69
|
files: ScannedFile[];
|
|
69
70
|
decrypted: DecryptedFile[];
|
|
70
71
|
skipped: SkippedFile[];
|
|
@@ -87,4 +88,4 @@ export declare const DEFAULT_PHASES: readonly {
|
|
|
87
88
|
* through ScanState, then runs the phase chain. Nothing survives between
|
|
88
89
|
* scans.
|
|
89
90
|
*/
|
|
90
|
-
export declare function runScan(db: Database.Database, opts?: RunScanOptions, hooks?: ScanHooks): Promise<ScanResult>;
|
|
91
|
+
export declare function runScan(db: Database.Database, opts?: RunScanOptions, hooks?: ScanHooks, signal?: AbortSignal): Promise<ScanResult>;
|
package/dist/scanner/engine.js
CHANGED
|
@@ -5,6 +5,9 @@ import { parsePhase } from "./parse.js";
|
|
|
5
5
|
import { chunkPdf } from "./pdf/chunker.js";
|
|
6
6
|
import { runClarify } from "./clarifier.js";
|
|
7
7
|
import { errorMessage } from "./result.js";
|
|
8
|
+
import { AbortedError } from "../ai/errors.js";
|
|
9
|
+
/** A signal that never aborts. Used when callers don't pass one. */
|
|
10
|
+
const NEVER_ABORTS = new AbortController().signal;
|
|
8
11
|
const chunkPhase = async (_db, state, hooks) => {
|
|
9
12
|
await hooks.beforeChunk?.(state);
|
|
10
13
|
for (const file of state.decrypted)
|
|
@@ -17,6 +20,7 @@ const clarifyPhase = async (db, state, hooks) => {
|
|
|
17
20
|
db,
|
|
18
21
|
scanId: state.scanId,
|
|
19
22
|
interactive: state.options.interactive ?? true,
|
|
23
|
+
signal: state.signal,
|
|
20
24
|
});
|
|
21
25
|
state.clarifySummary = summary;
|
|
22
26
|
await hooks.afterClarify?.(state, summary);
|
|
@@ -32,7 +36,7 @@ export const DEFAULT_PHASES = [
|
|
|
32
36
|
* through ScanState, then runs the phase chain. Nothing survives between
|
|
33
37
|
* scans.
|
|
34
38
|
*/
|
|
35
|
-
export async function runScan(db, opts = {}, hooks = {}) {
|
|
39
|
+
export async function runScan(db, opts = {}, hooks = {}, signal = NEVER_ABORTS) {
|
|
36
40
|
const scanId = `sc:${randomUUID()}`;
|
|
37
41
|
const progress = createProgress();
|
|
38
42
|
const state = {
|
|
@@ -40,6 +44,7 @@ export async function runScan(db, opts = {}, hooks = {}) {
|
|
|
40
44
|
startedAt: Date.now(),
|
|
41
45
|
options: opts,
|
|
42
46
|
progress,
|
|
47
|
+
signal,
|
|
43
48
|
files: [],
|
|
44
49
|
decrypted: [],
|
|
45
50
|
skipped: [],
|
|
@@ -50,8 +55,19 @@ export async function runScan(db, opts = {}, hooks = {}) {
|
|
|
50
55
|
};
|
|
51
56
|
await fire(hooks.onStart, state);
|
|
52
57
|
const phases = opts.phases ?? DEFAULT_PHASES;
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
try {
|
|
59
|
+
await runPhaseChain(db, state, hooks, phases);
|
|
60
|
+
if (state.signal.aborted)
|
|
61
|
+
throw new AbortedError();
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err instanceof AbortedError)
|
|
65
|
+
await fire(hooks.onAbort, state);
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await fire(hooks.onFinish, state);
|
|
70
|
+
}
|
|
55
71
|
return { scanId, state };
|
|
56
72
|
}
|
|
57
73
|
async function runPhaseChain(db, state, hooks, phases) {
|
|
@@ -67,6 +83,8 @@ async function tryPhase(db, state, hooks, name, phase) {
|
|
|
67
83
|
return false;
|
|
68
84
|
}
|
|
69
85
|
catch (err) {
|
|
86
|
+
if (err instanceof AbortedError)
|
|
87
|
+
throw err;
|
|
70
88
|
state.errors.push({ phase: name, error: err });
|
|
71
89
|
await fire(hooks.onError, err, name, state);
|
|
72
90
|
return true;
|