@juspay/neurolink 12.20.2 → 12.21.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/CHANGELOG.md +3 -3
- package/dist/browser/neurolink.min.js +343 -343
- package/dist/cli/commands/decide.d.ts +22 -0
- package/dist/cli/commands/decide.js +287 -0
- package/dist/cli/parser.js +6 -0
- package/dist/neurolink.js +8 -0
- package/dist/routing/classifierStrategies.js +7 -0
- package/dist/types/classifierRouter.d.ts +4 -0
- package/dist/types/cli.d.ts +12 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.js +27 -2
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* NeuroLink CLI Decide Command
|
|
4
|
+
*
|
|
5
|
+
* Calls `NeuroLink.decide()` — the third inference type, alongside `generate`
|
|
6
|
+
* and `stream`. Takes a `state` plus a map of named typed questions and
|
|
7
|
+
* prints one calibrated answer per question. See src/lib/types/decision.ts
|
|
8
|
+
* for the full vocabulary.
|
|
9
|
+
*
|
|
10
|
+
* Credentials are env-only here, exactly as every other CLI command: the
|
|
11
|
+
* decision provider (TypeSafe) reads `TYPESAFE_API_KEY` itself.
|
|
12
|
+
*/
|
|
13
|
+
import type { CommandModule } from "yargs";
|
|
14
|
+
import type { CliDecideArgs } from "../../types/index.js";
|
|
15
|
+
export declare const decideCommand: CommandModule<object, CliDecideArgs>;
|
|
16
|
+
/**
|
|
17
|
+
* Create decide command factory for CLICommandFactory-style registration,
|
|
18
|
+
* matching the EvaluateCommandFactory pattern.
|
|
19
|
+
*/
|
|
20
|
+
export declare class DecideCommandFactory {
|
|
21
|
+
static createDecideCommand(): CommandModule;
|
|
22
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* NeuroLink CLI Decide Command
|
|
4
|
+
*
|
|
5
|
+
* Calls `NeuroLink.decide()` — the third inference type, alongside `generate`
|
|
6
|
+
* and `stream`. Takes a `state` plus a map of named typed questions and
|
|
7
|
+
* prints one calibrated answer per question. See src/lib/types/decision.ts
|
|
8
|
+
* for the full vocabulary.
|
|
9
|
+
*
|
|
10
|
+
* Credentials are env-only here, exactly as every other CLI command: the
|
|
11
|
+
* decision provider (TypeSafe) reads `TYPESAFE_API_KEY` itself.
|
|
12
|
+
*/
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
import ora from "ora";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import { NeuroLink } from "../../neurolink.js";
|
|
17
|
+
import { readDecisionChoice } from "../../utils/decisionAnswers.js";
|
|
18
|
+
import { calculateCost, hasPricing } from "../../utils/pricing.js";
|
|
19
|
+
import { logger } from "../../utils/logger.js";
|
|
20
|
+
const VALID_QUESTION_TYPES = new Set(["boolean", "choice", "score"]);
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Validates that parsed JSON is a non-empty map of `{ type, ... }` question
|
|
26
|
+
* objects. Runs before any provider work — a malformed `--questions` payload
|
|
27
|
+
* must never reach the network.
|
|
28
|
+
*/
|
|
29
|
+
function isDecisionQuestionMap(value) {
|
|
30
|
+
if (!isRecord(value)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const ids = Object.keys(value);
|
|
34
|
+
return (ids.length > 0 &&
|
|
35
|
+
ids.every((id) => {
|
|
36
|
+
const question = value[id];
|
|
37
|
+
return (isRecord(question) &&
|
|
38
|
+
typeof question.type === "string" &&
|
|
39
|
+
VALID_QUESTION_TYPES.has(question.type));
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
/** `state` is the raw text, or its parsed JSON when that JSON is an object or array. */
|
|
43
|
+
function parseState(raw) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
if (isRecord(parsed) || Array.isArray(parsed)) {
|
|
47
|
+
// Single assertion (CLAUDE.md rule 14 allows this) — `isRecord`/
|
|
48
|
+
// `Array.isArray` narrow `unknown` down to a structural shape TS can't
|
|
49
|
+
// itself equate with the recursive `DecisionInput` union.
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Not JSON — treat as plain text.
|
|
55
|
+
}
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
function isDecisionErrorCause(value) {
|
|
59
|
+
return (isRecord(value) &&
|
|
60
|
+
typeof value.kind === "string" &&
|
|
61
|
+
typeof value.message === "string");
|
|
62
|
+
}
|
|
63
|
+
const ERROR_KIND_MESSAGES = {
|
|
64
|
+
authentication: "Authentication failed — check the decision provider's API key.",
|
|
65
|
+
invalid_request: "The decision provider rejected the request as invalid.",
|
|
66
|
+
max_tokens_exceeded: "The decision request exceeded the provider's token limit.",
|
|
67
|
+
rate_limit: "The decision provider is rate-limiting requests — try again shortly.",
|
|
68
|
+
overloaded: "The decision provider is overloaded — try again shortly.",
|
|
69
|
+
server: "The decision provider returned a server error.",
|
|
70
|
+
timeout: "The decision request timed out.",
|
|
71
|
+
network: "A network error occurred while calling the decision provider.",
|
|
72
|
+
};
|
|
73
|
+
/** One clean line describing a `decide()` failure — never a stack trace. */
|
|
74
|
+
function describeDecideError(error) {
|
|
75
|
+
const cause = error instanceof Error
|
|
76
|
+
? error.cause
|
|
77
|
+
: undefined;
|
|
78
|
+
if (isDecisionErrorCause(cause)) {
|
|
79
|
+
const generic = ERROR_KIND_MESSAGES[cause.kind];
|
|
80
|
+
if (!generic) {
|
|
81
|
+
return cause.message;
|
|
82
|
+
}
|
|
83
|
+
// The provider's own text is what says which field or question was
|
|
84
|
+
// rejected, so it is kept alongside the generic line.
|
|
85
|
+
const detail = cause.message.trim();
|
|
86
|
+
return detail && detail !== generic ? `${generic} (${detail})` : generic;
|
|
87
|
+
}
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
89
|
+
if (message.includes("No decision provider is configured")) {
|
|
90
|
+
return "No decision provider is configured. Set TYPESAFE_API_KEY, or AI_GATEWAY_API_KEY for the Vercel AI Gateway route.";
|
|
91
|
+
}
|
|
92
|
+
return message;
|
|
93
|
+
}
|
|
94
|
+
function formatNumber(value) {
|
|
95
|
+
return value.toFixed(2);
|
|
96
|
+
}
|
|
97
|
+
function formatAnswerLine(id, answers) {
|
|
98
|
+
const answer = answers[id];
|
|
99
|
+
if (!answer) {
|
|
100
|
+
return `${id}: (no answer)`;
|
|
101
|
+
}
|
|
102
|
+
switch (answer.type) {
|
|
103
|
+
case "boolean":
|
|
104
|
+
return `${id}: probability ${formatNumber(answer.probability)}`;
|
|
105
|
+
case "choice": {
|
|
106
|
+
const reading = readDecisionChoice(answers, id);
|
|
107
|
+
if (!reading) {
|
|
108
|
+
return `${id}: (unreadable choice answer)`;
|
|
109
|
+
}
|
|
110
|
+
const alternatives = reading.ranked
|
|
111
|
+
.filter((entry) => entry.name !== reading.choice)
|
|
112
|
+
.slice(0, 2)
|
|
113
|
+
.map((entry) => `${entry.name} (${formatNumber(entry.probability)})`)
|
|
114
|
+
.join(", ");
|
|
115
|
+
return (`${id}: choice ${reading.choice}, confidence ${formatNumber(reading.confidence)}` +
|
|
116
|
+
(alternatives ? `, alternatives: ${alternatives}` : ""));
|
|
117
|
+
}
|
|
118
|
+
case "score":
|
|
119
|
+
return `${id}: score ${formatNumber(answer.score)}, confidence ${formatNumber(answer.confidence)}`;
|
|
120
|
+
default:
|
|
121
|
+
return `${id}: (unrecognised answer type)`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function printTextResult(result) {
|
|
125
|
+
for (const id of Object.keys(result.answers)) {
|
|
126
|
+
logger.always(formatAnswerLine(id, result.answers));
|
|
127
|
+
}
|
|
128
|
+
logger.always("");
|
|
129
|
+
logger.always(chalk.gray(`Model: ${result.model}`));
|
|
130
|
+
logger.always(chalk.gray(`Latency: ${result.latencyMs}ms`));
|
|
131
|
+
if (hasPricing(result.provider, result.model)) {
|
|
132
|
+
const cost = calculateCost(result.provider, result.model, {
|
|
133
|
+
input: result.usage.inputTokens,
|
|
134
|
+
output: result.usage.outputTokens,
|
|
135
|
+
total: result.usage.inputTokens + result.usage.outputTokens,
|
|
136
|
+
});
|
|
137
|
+
logger.always(chalk.gray(`Cost: $${cost.toFixed(6)}`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function readQuestionsSource(argv) {
|
|
141
|
+
if (argv.questionsFile) {
|
|
142
|
+
return fs.readFileSync(argv.questionsFile, "utf-8");
|
|
143
|
+
}
|
|
144
|
+
if (!argv.questions) {
|
|
145
|
+
// Unreachable given the exactly-one-of check in the handler; keeps this
|
|
146
|
+
// function sound on its own rather than asserting the type away.
|
|
147
|
+
throw new Error("No questions source provided.");
|
|
148
|
+
}
|
|
149
|
+
return argv.questions;
|
|
150
|
+
}
|
|
151
|
+
function resolveState(argv) {
|
|
152
|
+
if (argv.stateFile) {
|
|
153
|
+
return parseState(fs.readFileSync(argv.stateFile, "utf-8"));
|
|
154
|
+
}
|
|
155
|
+
if (argv.state) {
|
|
156
|
+
return parseState(argv.state);
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
export const decideCommand = {
|
|
161
|
+
command: "decide [state]",
|
|
162
|
+
describe: "Get typed, calibrated judgements from a decision model (no free text)",
|
|
163
|
+
builder: (yargs) => yargs
|
|
164
|
+
.positional("state", {
|
|
165
|
+
type: "string",
|
|
166
|
+
describe: "The content to judge (or use --state-file)",
|
|
167
|
+
})
|
|
168
|
+
.option("state-file", {
|
|
169
|
+
type: "string",
|
|
170
|
+
describe: "Path to a file holding the state (text or JSON)",
|
|
171
|
+
})
|
|
172
|
+
.option("questions", {
|
|
173
|
+
type: "string",
|
|
174
|
+
describe: "Inline JSON map of questions (exactly one of this or --questions-file)",
|
|
175
|
+
})
|
|
176
|
+
.option("questions-file", {
|
|
177
|
+
type: "string",
|
|
178
|
+
describe: "Path to a JSON file holding the questions map",
|
|
179
|
+
})
|
|
180
|
+
.option("provider", {
|
|
181
|
+
type: "string",
|
|
182
|
+
describe: "Decision provider to use",
|
|
183
|
+
})
|
|
184
|
+
.option("model", {
|
|
185
|
+
type: "string",
|
|
186
|
+
describe: "Overrides the provider's configured model for this call",
|
|
187
|
+
})
|
|
188
|
+
.option("timeout", {
|
|
189
|
+
type: "number",
|
|
190
|
+
describe: "Timeout in milliseconds",
|
|
191
|
+
})
|
|
192
|
+
.option("format", {
|
|
193
|
+
type: "string",
|
|
194
|
+
alias: ["f", "output-format"],
|
|
195
|
+
describe: "Output format",
|
|
196
|
+
choices: ["text", "json"],
|
|
197
|
+
default: "text",
|
|
198
|
+
})
|
|
199
|
+
.option("debug", {
|
|
200
|
+
type: "boolean",
|
|
201
|
+
alias: ["v", "verbose"],
|
|
202
|
+
default: false,
|
|
203
|
+
describe: "Enable debug logging (written to stderr when --format json is used)",
|
|
204
|
+
})
|
|
205
|
+
.example('$0 decide "Refund request for a damaged item" --questions \'{"urgent":{"type":"boolean","instructions":"Is this urgent?"}}\'', "Ask a single yes/no question")
|
|
206
|
+
.example("$0 decide --state-file ticket.json --questions-file questions.json --format json", "Read state and questions from files, emit raw JSON"),
|
|
207
|
+
handler: async (argv) => {
|
|
208
|
+
const outputFormat = argv.format ?? "text";
|
|
209
|
+
// --- Validate before any provider work ---------------------------------
|
|
210
|
+
if (!!argv.questions === !!argv.questionsFile) {
|
|
211
|
+
logger.error(chalk.red("Error: pass exactly one of --questions or --questions-file."));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
let questionsRaw;
|
|
215
|
+
try {
|
|
216
|
+
questionsRaw = readQuestionsSource(argv);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
logger.error(chalk.red(`Error: could not read questions — ${error instanceof Error ? error.message : String(error)}`));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
let parsedQuestions;
|
|
223
|
+
try {
|
|
224
|
+
parsedQuestions = JSON.parse(questionsRaw);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
const flag = argv.questionsFile ? "--questions-file" : "--questions";
|
|
228
|
+
logger.error(chalk.red(`Error: ${flag} is not valid JSON.`));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
if (argv.timeout !== undefined &&
|
|
232
|
+
!(Number.isFinite(argv.timeout) && argv.timeout > 0)) {
|
|
233
|
+
logger.error(chalk.red("Error: --timeout must be a positive number of milliseconds."));
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
if (!isDecisionQuestionMap(parsedQuestions)) {
|
|
237
|
+
logger.error(chalk.red('Error: questions must be a non-empty object of { type: "boolean" | "choice" | "score", ... }.'));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
let state;
|
|
241
|
+
try {
|
|
242
|
+
state = resolveState(argv);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
logger.error(chalk.red(`Error: could not read state file — ${error instanceof Error ? error.message : String(error)}`));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
if (state === undefined) {
|
|
249
|
+
logger.error(chalk.red("Error: provide state as a positional argument or via --state-file."));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
// --- Provider work -------------------------------------------------
|
|
253
|
+
const spinner = outputFormat === "json" ? null : ora("Deciding...").start();
|
|
254
|
+
try {
|
|
255
|
+
const neurolink = new NeuroLink();
|
|
256
|
+
const result = await neurolink.decide({
|
|
257
|
+
state,
|
|
258
|
+
questions: parsedQuestions,
|
|
259
|
+
provider: argv.provider,
|
|
260
|
+
model: argv.model,
|
|
261
|
+
timeoutMs: argv.timeout,
|
|
262
|
+
});
|
|
263
|
+
spinner?.succeed("Decision complete");
|
|
264
|
+
if (outputFormat === "json") {
|
|
265
|
+
logger.always(JSON.stringify(result, null, 2));
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
printTextResult(result);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
spinner?.fail("Decision failed");
|
|
273
|
+
logger.error(chalk.red(`Error: ${describeDecideError(error)}`));
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Create decide command factory for CLICommandFactory-style registration,
|
|
280
|
+
* matching the EvaluateCommandFactory pattern.
|
|
281
|
+
*/
|
|
282
|
+
export class DecideCommandFactory {
|
|
283
|
+
static createDecideCommand() {
|
|
284
|
+
return decideCommand;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=decide.js.map
|
package/dist/cli/parser.js
CHANGED
|
@@ -29,6 +29,7 @@ import { proxyPeerCommand } from "./commands/proxyPeer.js";
|
|
|
29
29
|
import { proxyExposeCommand } from "./commands/proxyExpose.js";
|
|
30
30
|
import { proxyReplayCommand } from "./commands/proxyReplay.js";
|
|
31
31
|
import { EvaluateCommandFactory } from "./commands/evaluate.js";
|
|
32
|
+
import { DecideCommandFactory } from "./commands/decide.js";
|
|
32
33
|
import { TaskCommandFactory } from "./commands/task.js";
|
|
33
34
|
import { AutoresearchCommandFactory } from "./commands/autoresearch.js";
|
|
34
35
|
import { voiceServerCommand } from "./commands/voiceServer.js";
|
|
@@ -72,6 +73,9 @@ export function initializeCliParser() {
|
|
|
72
73
|
// Always set to false when debug is not enabled (including when not provided)
|
|
73
74
|
process.env.NEUROLINK_DEBUG = "false";
|
|
74
75
|
}
|
|
76
|
+
// With --format json, stdout carries the payload; a debug or info
|
|
77
|
+
// line there would make it unparseable, so diagnostics go to stderr.
|
|
78
|
+
logger.setDiagnosticsToStderr(argv.format === "json");
|
|
75
79
|
// Keep existing quiet middleware
|
|
76
80
|
if (process.env.NEUROLINK_QUIET === "true" &&
|
|
77
81
|
typeof argv.quiet === "undefined") {
|
|
@@ -164,6 +168,8 @@ export function initializeCliParser() {
|
|
|
164
168
|
.command(UsageCommandFactory.createUsageCommands())
|
|
165
169
|
// Stream Text Command - Using CLICommandFactory
|
|
166
170
|
.command(CLICommandFactory.createStreamCommand())
|
|
171
|
+
// Decide Command - typed, calibrated judgements (no free text)
|
|
172
|
+
.command(DecideCommandFactory.createDecideCommand())
|
|
167
173
|
// Batch Processing Command - Using CLICommandFactory
|
|
168
174
|
.command(CLICommandFactory.createBatchCommand())
|
|
169
175
|
// Provider Command Group - Using CLICommandFactory
|
package/dist/neurolink.js
CHANGED
|
@@ -4019,6 +4019,12 @@ Current user's request: ${currentInput}`;
|
|
|
4019
4019
|
const sessionId = typeof opt.context?.sessionId === "string"
|
|
4020
4020
|
? opt.context.sessionId
|
|
4021
4021
|
: undefined;
|
|
4022
|
+
// Routing runs before conversation memory is loaded, so only the history
|
|
4023
|
+
// the caller passed in is countable here.
|
|
4024
|
+
const sessionBound = sessionId !== undefined;
|
|
4025
|
+
const priorMessageCount = Array.isArray(opt.conversationMessages)
|
|
4026
|
+
? opt.conversationMessages.length
|
|
4027
|
+
: undefined;
|
|
4022
4028
|
try {
|
|
4023
4029
|
const decision = await this.classifierRouter.route({
|
|
4024
4030
|
prompt: promptText,
|
|
@@ -4027,6 +4033,8 @@ Current user's request: ${currentInput}`;
|
|
|
4027
4033
|
requiresVision,
|
|
4028
4034
|
thinkingLevel,
|
|
4029
4035
|
sessionId,
|
|
4036
|
+
sessionBound,
|
|
4037
|
+
priorMessageCount,
|
|
4030
4038
|
});
|
|
4031
4039
|
if (!decision) {
|
|
4032
4040
|
return;
|
|
@@ -295,6 +295,9 @@ export async function classifyJev(input, decide, timeoutMs, candidates, threshol
|
|
|
295
295
|
// are already assembled by the caller. Sending them costs almost nothing
|
|
296
296
|
// and they are exactly what distinguishes a long tool-using request from a
|
|
297
297
|
// short conversational one.
|
|
298
|
+
// Continuity is sent as `session_bound` / `prior_messages`, never as the
|
|
299
|
+
// raw sessionId: the id is an opaque identifier that tells a third-party
|
|
300
|
+
// model nothing, so it stays in-process.
|
|
298
301
|
const result = await decide({
|
|
299
302
|
state: {
|
|
300
303
|
request: (input.prompt ?? "").slice(0, 8000),
|
|
@@ -302,6 +305,10 @@ export async function classifyJev(input, decide, timeoutMs, candidates, threshol
|
|
|
302
305
|
has_tools_available: input.hasTools ?? false,
|
|
303
306
|
request_includes_images: input.requiresVision ?? false,
|
|
304
307
|
caller_requested_thinking: input.thinkingLevel ?? "none",
|
|
308
|
+
session_bound: input.sessionBound ?? false,
|
|
309
|
+
...(typeof input.priorMessageCount === "number"
|
|
310
|
+
? { prior_messages: input.priorMessageCount }
|
|
311
|
+
: {}),
|
|
305
312
|
},
|
|
306
313
|
questions,
|
|
307
314
|
timeoutMs,
|
|
@@ -296,6 +296,10 @@ export type ClassifierRouterInput = {
|
|
|
296
296
|
requiresVision?: boolean;
|
|
297
297
|
thinkingLevel?: string;
|
|
298
298
|
sessionId?: string;
|
|
299
|
+
/** Whether this request is tied to a session, independent of the (withheld) session id itself. */
|
|
300
|
+
sessionBound?: boolean;
|
|
301
|
+
/** Number of prior conversation messages the caller supplied, when known. */
|
|
302
|
+
priorMessageCount?: number;
|
|
299
303
|
};
|
|
300
304
|
/** Enriched per-model metadata used while ranking pool members. */
|
|
301
305
|
export type ClassifierModelMeta = {
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -1296,6 +1296,18 @@ export type AutoresearchInitArgs = {
|
|
|
1296
1296
|
provider?: string;
|
|
1297
1297
|
model?: string;
|
|
1298
1298
|
};
|
|
1299
|
+
/** Arguments for `neurolink decide` — the third inference type's CLI surface. */
|
|
1300
|
+
export type CliDecideArgs = {
|
|
1301
|
+
state?: string;
|
|
1302
|
+
stateFile?: string;
|
|
1303
|
+
questions?: string;
|
|
1304
|
+
questionsFile?: string;
|
|
1305
|
+
provider?: string;
|
|
1306
|
+
model?: string;
|
|
1307
|
+
timeout?: number;
|
|
1308
|
+
format?: "text" | "json";
|
|
1309
|
+
debug?: boolean;
|
|
1310
|
+
};
|
|
1299
1311
|
/** Base options shared across all `neurolink evaluate` subcommands. */
|
|
1300
1312
|
export type BaseEvaluateArgs = {
|
|
1301
1313
|
json?: boolean;
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ declare class NeuroLinkLogger {
|
|
|
19
19
|
private logs;
|
|
20
20
|
private maxLogs;
|
|
21
21
|
private isDebugMode;
|
|
22
|
+
private diagnosticsToStderr;
|
|
22
23
|
private eventEmitter?;
|
|
23
24
|
constructor();
|
|
24
25
|
/**
|
|
@@ -49,6 +50,14 @@ declare class NeuroLinkLogger {
|
|
|
49
50
|
* @param level - The minimum log level to process ("debug", "info", "warn", or "error")
|
|
50
51
|
*/
|
|
51
52
|
setLogLevel(level: LogLevel): void;
|
|
53
|
+
/**
|
|
54
|
+
* Routes debug and info output to stderr instead of stdout. The CLI turns
|
|
55
|
+
* this on for `--format json`, where stdout carries the payload and any
|
|
56
|
+
* diagnostic line in it would make the output unparseable.
|
|
57
|
+
*
|
|
58
|
+
* @param enabled - True to send debug/info to stderr; false restores stdout
|
|
59
|
+
*/
|
|
60
|
+
setDiagnosticsToStderr(enabled: boolean): void;
|
|
52
61
|
/**
|
|
53
62
|
* Determines whether a message with the given log level should be processed.
|
|
54
63
|
* This method considers both the configured log level and the current debug mode.
|
|
@@ -222,6 +231,7 @@ export declare const logger: {
|
|
|
222
231
|
table: (data: unknown) => void;
|
|
223
232
|
shouldLog: (level: LogLevel) => boolean;
|
|
224
233
|
setLogLevel: (level: LogLevel) => void;
|
|
234
|
+
setDiagnosticsToStderr: (enabled: boolean) => void;
|
|
225
235
|
getLogs: (level?: LogLevel) => LogEntry[];
|
|
226
236
|
clearLogs: () => void;
|
|
227
237
|
setEventEmitter: (emitter: {
|
package/dist/utils/logger.js
CHANGED
|
@@ -36,11 +36,24 @@ const UPPERCASE_LOG_LEVELS = {
|
|
|
36
36
|
warn: "WARN",
|
|
37
37
|
error: "ERROR",
|
|
38
38
|
};
|
|
39
|
+
const FORMAT_FLAGS = ["--format", "-f", "--output-format"];
|
|
40
|
+
/**
|
|
41
|
+
* True when the process was started with JSON output requested. Read from
|
|
42
|
+
* argv on every write, like the `--debug` check in `shouldLog`: modules log
|
|
43
|
+
* while they are still being imported, before the CLI middleware that calls
|
|
44
|
+
* `setDiagnosticsToStderr` has run.
|
|
45
|
+
*/
|
|
46
|
+
function argvRequestsJsonOutput() {
|
|
47
|
+
const argv = process.argv;
|
|
48
|
+
return argv.some((arg, index) => (FORMAT_FLAGS.includes(arg) && argv[index + 1] === "json") ||
|
|
49
|
+
FORMAT_FLAGS.some((flag) => arg === `${flag}=json`));
|
|
50
|
+
}
|
|
39
51
|
class NeuroLinkLogger {
|
|
40
52
|
logLevel = "info";
|
|
41
53
|
logs = [];
|
|
42
54
|
maxLogs = 1000;
|
|
43
55
|
isDebugMode;
|
|
56
|
+
diagnosticsToStderr = false;
|
|
44
57
|
eventEmitter;
|
|
45
58
|
constructor() {
|
|
46
59
|
// Cache debug mode check to avoid repeated array searches
|
|
@@ -86,6 +99,16 @@ class NeuroLinkLogger {
|
|
|
86
99
|
setLogLevel(level) {
|
|
87
100
|
this.logLevel = level;
|
|
88
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Routes debug and info output to stderr instead of stdout. The CLI turns
|
|
104
|
+
* this on for `--format json`, where stdout carries the payload and any
|
|
105
|
+
* diagnostic line in it would make the output unparseable.
|
|
106
|
+
*
|
|
107
|
+
* @param enabled - True to send debug/info to stderr; false restores stdout
|
|
108
|
+
*/
|
|
109
|
+
setDiagnosticsToStderr(enabled) {
|
|
110
|
+
this.diagnosticsToStderr = enabled;
|
|
111
|
+
}
|
|
89
112
|
/**
|
|
90
113
|
* Determines whether a message with the given log level should be processed.
|
|
91
114
|
* This method considers both the configured log level and the current debug mode.
|
|
@@ -209,9 +232,10 @@ class NeuroLinkLogger {
|
|
|
209
232
|
* @param data - Optional additional data to log.
|
|
210
233
|
*/
|
|
211
234
|
outputToConsole(level, prefix, message, data) {
|
|
235
|
+
const toStderr = this.diagnosticsToStderr || argvRequestsJsonOutput();
|
|
212
236
|
const logMethod = {
|
|
213
|
-
debug: console.debug,
|
|
214
|
-
info: console.info,
|
|
237
|
+
debug: toStderr ? console.error : console.debug,
|
|
238
|
+
info: toStderr ? console.error : console.info,
|
|
215
239
|
warn: console.warn,
|
|
216
240
|
error: console.error,
|
|
217
241
|
}[level];
|
|
@@ -467,6 +491,7 @@ export const logger = {
|
|
|
467
491
|
shouldLog: (level) => neuroLinkLogger.shouldLog(level),
|
|
468
492
|
// Expose structured logging methods
|
|
469
493
|
setLogLevel: (level) => neuroLinkLogger.setLogLevel(level),
|
|
494
|
+
setDiagnosticsToStderr: (enabled) => neuroLinkLogger.setDiagnosticsToStderr(enabled),
|
|
470
495
|
getLogs: (level) => neuroLinkLogger.getLogs(level),
|
|
471
496
|
clearLogs: () => neuroLinkLogger.clearLogs(),
|
|
472
497
|
setEventEmitter: (emitter) => neuroLinkLogger.setEventEmitter(emitter),
|