@isaacthoman/pulpo 0.57.0 → 0.59.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 +24 -0
- package/dist/index.js +426 -7
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ pulpo auth 2fa setup
|
|
|
11
11
|
pulpo auth 2fa confirm
|
|
12
12
|
pulpo settings export --output pulpo-settings.json
|
|
13
13
|
pulpo icon upload ./acme.svg --name Acme --mode monochrome
|
|
14
|
+
pulpo model test acme-model "Reply with a short greeting" --no-agent --preset reasoning=off
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
Use `pulpo help` or `pulpo <command> --help` for the complete command reference.
|
|
@@ -21,6 +22,29 @@ canonical [Lucide](https://lucide.dev/icons/) name. Discover available names wit
|
|
|
21
22
|
`pulpo model icons camera`. Invalid names are rejected before the model request is
|
|
22
23
|
sent. Add `--json` for stable `{ "name": "..." }` rows in scripts.
|
|
23
24
|
|
|
25
|
+
Test a newly configured model with `pulpo model test <model-id> [prompt...]`.
|
|
26
|
+
The command requires exactly one of `--agent` or `--no-agent`, plus one explicit
|
|
27
|
+
`--preset <preset-id>=<choice-id>` for every preset exposed by the model. It
|
|
28
|
+
does not silently apply preset defaults. For example:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pulpo model test acme-model "Investigate the failing build" \
|
|
32
|
+
--agent \
|
|
33
|
+
--preset reasoning=high \
|
|
34
|
+
--preset web-search=enabled
|
|
35
|
+
|
|
36
|
+
git diff | pulpo model test acme-model \
|
|
37
|
+
--no-agent \
|
|
38
|
+
--preset reasoning=off \
|
|
39
|
+
--preset web-search=disabled
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Model tests stream assistant text to stdout and use temporary chats by default.
|
|
43
|
+
Pass `--keep` to retain the test in normal chat history, `--no-stream` to wait
|
|
44
|
+
for the completed text, `--json` for one final result, or `--jsonl` for the
|
|
45
|
+
replayable response event stream. Model testing requires a user session created
|
|
46
|
+
by `pulpo auth login`; management tokens cannot access user chat endpoints.
|
|
47
|
+
|
|
24
48
|
The major command groups are `context`, `auth`, `token`, `instance`, `settings`,
|
|
25
49
|
`provider`, `lab`, `icon`, `model`, `user`, `usage`, `audit`, `workspace`, `banner`,
|
|
26
50
|
`job`, `export`, and `backup`. There is intentionally no restore command.
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { mkdtemp, readFile as readFile2, rm as rm2, writeFile as writeFile2 } fr
|
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { basename, join as join2 } from "node:path";
|
|
13
13
|
import { pathToFileURL } from "node:url";
|
|
14
|
-
import { Command, CommanderError } from "commander";
|
|
14
|
+
import { Command, CommanderError, Option } from "commander";
|
|
15
15
|
|
|
16
16
|
// ../../node_modules/zod/v4/classic/external.js
|
|
17
17
|
var external_exports = {};
|
|
@@ -1359,8 +1359,8 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
|
1359
1359
|
let i = 0;
|
|
1360
1360
|
while (i < fullpath.length) {
|
|
1361
1361
|
const el = fullpath[i];
|
|
1362
|
-
const
|
|
1363
|
-
if (!
|
|
1362
|
+
const terminal2 = i === fullpath.length - 1;
|
|
1363
|
+
if (!terminal2) {
|
|
1364
1364
|
curr[el] = curr[el] || { _errors: [] };
|
|
1365
1365
|
} else {
|
|
1366
1366
|
curr[el] = curr[el] || { _errors: [] };
|
|
@@ -1397,7 +1397,7 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
|
1397
1397
|
let i = 0;
|
|
1398
1398
|
while (i < fullpath.length) {
|
|
1399
1399
|
const el = fullpath[i];
|
|
1400
|
-
const
|
|
1400
|
+
const terminal2 = i === fullpath.length - 1;
|
|
1401
1401
|
if (typeof el === "string") {
|
|
1402
1402
|
curr.properties ?? (curr.properties = {});
|
|
1403
1403
|
(_a3 = curr.properties)[el] ?? (_a3[el] = { errors: [] });
|
|
@@ -1407,7 +1407,7 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
|
1407
1407
|
(_b = curr.items)[el] ?? (_b[el] = { errors: [] });
|
|
1408
1408
|
curr = curr.items[el];
|
|
1409
1409
|
}
|
|
1410
|
-
if (
|
|
1410
|
+
if (terminal2) {
|
|
1411
1411
|
curr.errors.push(mapper(issue2));
|
|
1412
1412
|
}
|
|
1413
1413
|
i++;
|
|
@@ -16462,6 +16462,167 @@ var responseSnapshotSchema = external_exports.object({
|
|
|
16462
16462
|
updatedAt: isoDateSchema
|
|
16463
16463
|
});
|
|
16464
16464
|
var embeddedResponseSnapshotSchema = responseSnapshotSchema.omit({ output: true });
|
|
16465
|
+
function targetItemIndex(output, payload, type) {
|
|
16466
|
+
const itemId = typeof payload.item_id === "string" ? payload.item_id : typeof payload.itemId === "string" ? payload.itemId : void 0;
|
|
16467
|
+
if (itemId) {
|
|
16468
|
+
const byId = output.findIndex((item) => item.id === itemId);
|
|
16469
|
+
return byId;
|
|
16470
|
+
}
|
|
16471
|
+
const outputIndex = typeof payload.output_index === "number" ? payload.output_index : typeof payload.outputIndex === "number" ? payload.outputIndex : void 0;
|
|
16472
|
+
if (outputIndex !== void 0 && output[outputIndex]?.type === type)
|
|
16473
|
+
return outputIndex;
|
|
16474
|
+
const agentTurn = typeof payload.agent_turn === "number" ? payload.agent_turn : void 0;
|
|
16475
|
+
const contentIndex = typeof payload.content_index === "number" ? payload.content_index : typeof payload.contentIndex === "number" ? payload.contentIndex : void 0;
|
|
16476
|
+
if (agentTurn !== void 0 && contentIndex !== void 0) {
|
|
16477
|
+
const byAgentPart = output.findIndex((item) => item.type === type && item.agent_turn === agentTurn && item.agent_content_index === contentIndex);
|
|
16478
|
+
if (byAgentPart >= 0)
|
|
16479
|
+
return byAgentPart;
|
|
16480
|
+
}
|
|
16481
|
+
for (let index = output.length - 1; index >= 0; index -= 1) {
|
|
16482
|
+
if (output[index]?.type === type && output[index]?.status === "in_progress")
|
|
16483
|
+
return index;
|
|
16484
|
+
}
|
|
16485
|
+
for (let index = output.length - 1; index >= 0; index -= 1) {
|
|
16486
|
+
if (output[index]?.type === type)
|
|
16487
|
+
return index;
|
|
16488
|
+
}
|
|
16489
|
+
return -1;
|
|
16490
|
+
}
|
|
16491
|
+
function appendOutputText(output, delta, payload) {
|
|
16492
|
+
const copy = output.slice();
|
|
16493
|
+
const index = targetItemIndex(copy, payload, "message");
|
|
16494
|
+
let message = index >= 0 ? { ...copy[index] } : void 0;
|
|
16495
|
+
if (!message) {
|
|
16496
|
+
const itemId = typeof payload.item_id === "string" ? payload.item_id : typeof payload.itemId === "string" ? payload.itemId : void 0;
|
|
16497
|
+
message = { ...itemId ? { id: itemId } : {}, type: "message", role: "assistant", status: "in_progress", content: [] };
|
|
16498
|
+
copy.push(message);
|
|
16499
|
+
} else {
|
|
16500
|
+
copy[index] = message;
|
|
16501
|
+
}
|
|
16502
|
+
const content = Array.isArray(message.content) ? message.content.slice() : [];
|
|
16503
|
+
const contentIndex = typeof payload.content_index === "number" ? payload.content_index : typeof payload.contentIndex === "number" ? payload.contentIndex : void 0;
|
|
16504
|
+
const partIndex = contentIndex !== void 0 && content[contentIndex]?.type === "output_text" ? contentIndex : content.findIndex((item) => item.type === "output_text");
|
|
16505
|
+
let part = partIndex >= 0 ? { ...content[partIndex] } : void 0;
|
|
16506
|
+
if (!part) {
|
|
16507
|
+
part = { type: "output_text", text: "" };
|
|
16508
|
+
content.push(part);
|
|
16509
|
+
} else {
|
|
16510
|
+
content[partIndex] = part;
|
|
16511
|
+
}
|
|
16512
|
+
part.text = `${typeof part.text === "string" ? part.text : ""}${delta}`;
|
|
16513
|
+
message.content = content;
|
|
16514
|
+
return copy;
|
|
16515
|
+
}
|
|
16516
|
+
function appendReasoning(output, delta, payload) {
|
|
16517
|
+
const copy = output.slice();
|
|
16518
|
+
const index = targetItemIndex(copy, payload, "reasoning");
|
|
16519
|
+
let reasoning = index >= 0 ? { ...copy[index] } : void 0;
|
|
16520
|
+
if (!reasoning) {
|
|
16521
|
+
const itemId = typeof payload.item_id === "string" ? payload.item_id : typeof payload.itemId === "string" ? payload.itemId : void 0;
|
|
16522
|
+
reasoning = { ...itemId ? { id: itemId } : {}, type: "reasoning", status: "in_progress", summary: [] };
|
|
16523
|
+
copy.push(reasoning);
|
|
16524
|
+
} else {
|
|
16525
|
+
copy[index] = reasoning;
|
|
16526
|
+
}
|
|
16527
|
+
const summary = Array.isArray(reasoning.summary) ? reasoning.summary.slice() : [];
|
|
16528
|
+
const partIndex = summary.findIndex((item) => item.type === "summary_text");
|
|
16529
|
+
let part = partIndex >= 0 ? { ...summary[partIndex] } : void 0;
|
|
16530
|
+
if (!part) {
|
|
16531
|
+
part = { type: "summary_text", text: "" };
|
|
16532
|
+
summary.push(part);
|
|
16533
|
+
} else {
|
|
16534
|
+
summary[partIndex] = part;
|
|
16535
|
+
}
|
|
16536
|
+
part.text = `${typeof part.text === "string" ? part.text : ""}${delta}`;
|
|
16537
|
+
reasoning.summary = summary;
|
|
16538
|
+
return copy;
|
|
16539
|
+
}
|
|
16540
|
+
function upsertOutputItem(output, match, value) {
|
|
16541
|
+
const copy = output.slice();
|
|
16542
|
+
const index = copy.findIndex((item) => Boolean(item) && typeof item === "object" && match(item));
|
|
16543
|
+
if (index < 0)
|
|
16544
|
+
copy.push(value);
|
|
16545
|
+
else
|
|
16546
|
+
copy[index] = { ...copy[index], ...value };
|
|
16547
|
+
return copy;
|
|
16548
|
+
}
|
|
16549
|
+
function applyAgentEventOutput(output, event) {
|
|
16550
|
+
const payload = event.payload;
|
|
16551
|
+
if (event.type.startsWith("pulpo.agent.workspace.")) {
|
|
16552
|
+
return upsertOutputItem(output, (item) => item.type === "pulpo_workspace", payload);
|
|
16553
|
+
}
|
|
16554
|
+
if (event.type === "pulpo.compaction.updated" && typeof payload.id === "string") {
|
|
16555
|
+
return upsertOutputItem(output, (item) => item.id === payload.id, payload);
|
|
16556
|
+
}
|
|
16557
|
+
if (event.type === "pulpo.agent.attachment.created" && typeof payload.attachment_id === "string") {
|
|
16558
|
+
return upsertOutputItem(output, (item) => item.type === "pulpo_attachment" && item.attachment_id === payload.attachment_id, payload);
|
|
16559
|
+
}
|
|
16560
|
+
if (!event.type.startsWith("pulpo.agent.tool.") || typeof payload.id !== "string")
|
|
16561
|
+
return output;
|
|
16562
|
+
if (event.type === "pulpo.agent.tool.delta") {
|
|
16563
|
+
return upsertOutputItem(output, (item) => item.id === payload.id, {
|
|
16564
|
+
id: payload.id,
|
|
16565
|
+
type: "pulpo_tool",
|
|
16566
|
+
output: typeof payload.delta === "string" ? payload.delta : "",
|
|
16567
|
+
status: "running"
|
|
16568
|
+
});
|
|
16569
|
+
}
|
|
16570
|
+
if (event.type === "pulpo.agent.tool.completed") {
|
|
16571
|
+
return upsertOutputItem(output, (item) => item.id === payload.id, {
|
|
16572
|
+
...payload,
|
|
16573
|
+
type: "pulpo_tool",
|
|
16574
|
+
status: payload.isError ? "failed" : "completed"
|
|
16575
|
+
});
|
|
16576
|
+
}
|
|
16577
|
+
return upsertOutputItem(output, (item) => item.id === payload.id, payload);
|
|
16578
|
+
}
|
|
16579
|
+
function applyResponseEventToSnapshot(snapshot, event) {
|
|
16580
|
+
if (event.sequence <= snapshot.sequence)
|
|
16581
|
+
return snapshot;
|
|
16582
|
+
const payload = event.payload;
|
|
16583
|
+
const delta = typeof payload.delta === "string" ? payload.delta : "";
|
|
16584
|
+
let output = snapshot.output;
|
|
16585
|
+
if (delta && event.type === "response.output_text.delta")
|
|
16586
|
+
output = appendOutputText(output, delta, payload);
|
|
16587
|
+
if (delta && event.type === "response.reasoning_summary_text.delta")
|
|
16588
|
+
output = appendReasoning(output, delta, payload);
|
|
16589
|
+
output = applyAgentEventOutput(output, event);
|
|
16590
|
+
return {
|
|
16591
|
+
...snapshot,
|
|
16592
|
+
status: snapshot.status === "queued" ? "in_progress" : snapshot.status,
|
|
16593
|
+
sequence: event.sequence,
|
|
16594
|
+
output,
|
|
16595
|
+
updatedAt: event.emittedAt
|
|
16596
|
+
};
|
|
16597
|
+
}
|
|
16598
|
+
function mergeResponseSnapshots(current, incoming) {
|
|
16599
|
+
if (incoming.sequence < current.sequence)
|
|
16600
|
+
return current;
|
|
16601
|
+
if (incoming.sequence === current.sequence) {
|
|
16602
|
+
const currentTerminal = current.status !== "queued" && current.status !== "in_progress";
|
|
16603
|
+
const incomingTerminal = incoming.status !== "queued" && incoming.status !== "in_progress";
|
|
16604
|
+
if (currentTerminal && !incomingTerminal)
|
|
16605
|
+
return current;
|
|
16606
|
+
if (incomingTerminal && !currentTerminal) {
|
|
16607
|
+
return incoming.output.length === 0 && current.output.length > 0 ? { ...incoming, output: current.output } : incoming;
|
|
16608
|
+
}
|
|
16609
|
+
if (incoming.updatedAt < current.updatedAt)
|
|
16610
|
+
return current;
|
|
16611
|
+
if (incoming.updatedAt === current.updatedAt) {
|
|
16612
|
+
if (current.output.length === 0 && incoming.output.length > 0)
|
|
16613
|
+
return incoming;
|
|
16614
|
+
return current;
|
|
16615
|
+
}
|
|
16616
|
+
if (incoming.output.length === 0 && current.output.length > 0) {
|
|
16617
|
+
return { ...incoming, output: current.output };
|
|
16618
|
+
}
|
|
16619
|
+
}
|
|
16620
|
+
const incomingIsActive = incoming.status === "queued" || incoming.status === "in_progress";
|
|
16621
|
+
if (incomingIsActive && incoming.output.length === 0 && current.output.length > 0) {
|
|
16622
|
+
return { ...incoming, output: current.output };
|
|
16623
|
+
}
|
|
16624
|
+
return incoming;
|
|
16625
|
+
}
|
|
16465
16626
|
var catalogIconModeSchema = external_exports.enum(["original", "monochrome"]);
|
|
16466
16627
|
var catalogIconReferenceSchema = external_exports.object({
|
|
16467
16628
|
id: idSchema,
|
|
@@ -17416,11 +17577,222 @@ async function confirmExact(io, expected, yes, json2) {
|
|
|
17416
17577
|
}
|
|
17417
17578
|
}
|
|
17418
17579
|
|
|
17580
|
+
// src/model-test.ts
|
|
17581
|
+
import { randomUUID } from "node:crypto";
|
|
17582
|
+
import { io as createSocket } from "socket.io-client";
|
|
17583
|
+
function explicitAgentMode(options) {
|
|
17584
|
+
const enabled = options.agentEnabled === true;
|
|
17585
|
+
const disabled = options.agentDisabled === false;
|
|
17586
|
+
if (enabled === disabled) {
|
|
17587
|
+
throw selectionError(["choose --agent or --no-agent"]);
|
|
17588
|
+
}
|
|
17589
|
+
return enabled;
|
|
17590
|
+
}
|
|
17591
|
+
function terminal(snapshot) {
|
|
17592
|
+
return snapshot.status !== "queued" && snapshot.status !== "in_progress";
|
|
17593
|
+
}
|
|
17594
|
+
async function followResponse(input) {
|
|
17595
|
+
if (terminal(input.snapshot)) return input.snapshot;
|
|
17596
|
+
return new Promise((resolve, reject) => {
|
|
17597
|
+
let current = input.snapshot;
|
|
17598
|
+
const pending = /* @__PURE__ */ new Map();
|
|
17599
|
+
const socket = createSocket(input.baseUrl, {
|
|
17600
|
+
path: "/socket.io",
|
|
17601
|
+
auth: { sessionToken: input.token },
|
|
17602
|
+
autoConnect: false,
|
|
17603
|
+
reconnection: true
|
|
17604
|
+
});
|
|
17605
|
+
let settled = false;
|
|
17606
|
+
const cleanup = () => {
|
|
17607
|
+
socket.removeAllListeners();
|
|
17608
|
+
socket.disconnect();
|
|
17609
|
+
};
|
|
17610
|
+
const fail = (error51) => {
|
|
17611
|
+
if (settled) return;
|
|
17612
|
+
settled = true;
|
|
17613
|
+
cleanup();
|
|
17614
|
+
reject(error51);
|
|
17615
|
+
};
|
|
17616
|
+
const finishIfTerminal = () => {
|
|
17617
|
+
if (settled || !terminal(current)) return;
|
|
17618
|
+
settled = true;
|
|
17619
|
+
cleanup();
|
|
17620
|
+
resolve(current);
|
|
17621
|
+
};
|
|
17622
|
+
const flush = () => {
|
|
17623
|
+
for (; ; ) {
|
|
17624
|
+
const event = pending.get(current.sequence + 1);
|
|
17625
|
+
if (!event) break;
|
|
17626
|
+
pending.delete(event.sequence);
|
|
17627
|
+
current = applyResponseEventToSnapshot(current, event);
|
|
17628
|
+
input.onEvent?.(event);
|
|
17629
|
+
}
|
|
17630
|
+
};
|
|
17631
|
+
socket.on("connect", () => {
|
|
17632
|
+
socket.emit("response.subscribe", {
|
|
17633
|
+
responseId: current.responseId,
|
|
17634
|
+
afterSequence: current.sequence
|
|
17635
|
+
});
|
|
17636
|
+
});
|
|
17637
|
+
socket.on("connect_error", (error51) => fail(new Error(`Realtime connection failed: ${error51.message}`)));
|
|
17638
|
+
socket.on("response.event", (event) => {
|
|
17639
|
+
if (event.responseId !== current.responseId || event.sequence <= current.sequence) return;
|
|
17640
|
+
pending.set(event.sequence, event);
|
|
17641
|
+
flush();
|
|
17642
|
+
});
|
|
17643
|
+
socket.on("response.snapshot", (snapshot) => {
|
|
17644
|
+
if (snapshot.responseId !== current.responseId) return;
|
|
17645
|
+
current = mergeResponseSnapshots(current, snapshot);
|
|
17646
|
+
for (const sequence of pending.keys()) if (sequence <= current.sequence) pending.delete(sequence);
|
|
17647
|
+
flush();
|
|
17648
|
+
input.onSnapshot?.(current);
|
|
17649
|
+
finishIfTerminal();
|
|
17650
|
+
});
|
|
17651
|
+
socket.connect();
|
|
17652
|
+
});
|
|
17653
|
+
}
|
|
17654
|
+
function selectionError(lines) {
|
|
17655
|
+
return new Error(`Explicit selections are required:
|
|
17656
|
+
${lines.map((line) => ` ${line}`).join("\n")}`);
|
|
17657
|
+
}
|
|
17658
|
+
function resolvePresetSelections(model, values) {
|
|
17659
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
17660
|
+
for (const value of values) {
|
|
17661
|
+
const separator = value.indexOf("=");
|
|
17662
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
17663
|
+
throw new Error(`Invalid preset selection ${JSON.stringify(value)}; expected <preset-id>=<choice-id>`);
|
|
17664
|
+
}
|
|
17665
|
+
const presetId = value.slice(0, separator);
|
|
17666
|
+
const choiceId = value.slice(separator + 1);
|
|
17667
|
+
if (parsed.has(presetId)) throw new Error(`Preset ${presetId} was selected more than once`);
|
|
17668
|
+
parsed.set(presetId, choiceId);
|
|
17669
|
+
}
|
|
17670
|
+
const exposed = new Map(model.presets.map((preset) => [preset.id, preset]));
|
|
17671
|
+
const unknown2 = [...parsed.keys()].filter((presetId) => !exposed.has(presetId));
|
|
17672
|
+
if (unknown2.length) throw new Error(`Unknown preset${unknown2.length === 1 ? "" : "s"} for ${model.id}: ${unknown2.join(", ")}`);
|
|
17673
|
+
const missing = model.presets.filter((preset) => !parsed.has(preset.id));
|
|
17674
|
+
if (missing.length) {
|
|
17675
|
+
throw selectionError(missing.map((preset) => `--preset ${preset.id}=<${preset.choices.map((choice) => choice.id).join("|")}>`));
|
|
17676
|
+
}
|
|
17677
|
+
for (const preset of model.presets) {
|
|
17678
|
+
const choiceId = parsed.get(preset.id);
|
|
17679
|
+
if (!preset.choices.some((choice) => choice.id === choiceId)) {
|
|
17680
|
+
throw new Error(
|
|
17681
|
+
`Unknown choice ${choiceId} for preset ${preset.id}; choose ${preset.choices.map((choice) => choice.id).join(", ")}`
|
|
17682
|
+
);
|
|
17683
|
+
}
|
|
17684
|
+
}
|
|
17685
|
+
return Object.fromEntries(parsed);
|
|
17686
|
+
}
|
|
17687
|
+
async function readModelTestPrompt(io, promptParts) {
|
|
17688
|
+
const argument = promptParts.join(" ").trim();
|
|
17689
|
+
if (argument) return argument;
|
|
17690
|
+
if (io.stdin.isTTY) throw new Error("Prompt is required as an argument or on stdin");
|
|
17691
|
+
const chunks = [];
|
|
17692
|
+
for await (const chunk of io.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
17693
|
+
const prompt = Buffer.concat(chunks).toString("utf8").trim();
|
|
17694
|
+
if (!prompt) throw new Error("Prompt is required as an argument or on stdin");
|
|
17695
|
+
return prompt;
|
|
17696
|
+
}
|
|
17697
|
+
function responseText(snapshot) {
|
|
17698
|
+
const parts = [];
|
|
17699
|
+
for (const item of snapshot.output) {
|
|
17700
|
+
if (!item || typeof item !== "object") continue;
|
|
17701
|
+
const content = item.content;
|
|
17702
|
+
if (!Array.isArray(content)) continue;
|
|
17703
|
+
for (const part of content) {
|
|
17704
|
+
if (!part || typeof part !== "object") continue;
|
|
17705
|
+
const candidate = part;
|
|
17706
|
+
if (candidate.type === "output_text" && typeof candidate.text === "string") parts.push(candidate.text);
|
|
17707
|
+
}
|
|
17708
|
+
}
|
|
17709
|
+
return parts.join("");
|
|
17710
|
+
}
|
|
17711
|
+
function writeJsonLine(output, value) {
|
|
17712
|
+
output.write(`${JSON.stringify(value)}
|
|
17713
|
+
`);
|
|
17714
|
+
}
|
|
17715
|
+
async function runModelTest(input) {
|
|
17716
|
+
const chatId = randomUUID();
|
|
17717
|
+
const responseId = randomUUID();
|
|
17718
|
+
const temporary = !input.keep;
|
|
17719
|
+
const created = await input.client.request("/api/chats/start", {
|
|
17720
|
+
method: "POST",
|
|
17721
|
+
headers: { "idempotency-key": responseId },
|
|
17722
|
+
body: {
|
|
17723
|
+
chat: {
|
|
17724
|
+
clientId: chatId,
|
|
17725
|
+
modelId: input.model.id,
|
|
17726
|
+
title: `CLI test: ${input.model.name}`.slice(0, 200),
|
|
17727
|
+
temporary
|
|
17728
|
+
},
|
|
17729
|
+
response: {
|
|
17730
|
+
clientId: responseId,
|
|
17731
|
+
parentResponseId: null,
|
|
17732
|
+
input: input.prompt,
|
|
17733
|
+
modelId: input.model.id,
|
|
17734
|
+
presetSelections: input.presetSelections,
|
|
17735
|
+
attachmentIds: [],
|
|
17736
|
+
agentMode: input.agentMode
|
|
17737
|
+
}
|
|
17738
|
+
}
|
|
17739
|
+
});
|
|
17740
|
+
let writtenText = "";
|
|
17741
|
+
const writeProgress = (snapshot) => {
|
|
17742
|
+
if (!input.streamText) return;
|
|
17743
|
+
const text = responseText(snapshot);
|
|
17744
|
+
if (text.startsWith(writtenText)) {
|
|
17745
|
+
input.io.stdout.write(text.slice(writtenText.length));
|
|
17746
|
+
writtenText = text;
|
|
17747
|
+
}
|
|
17748
|
+
};
|
|
17749
|
+
if (input.jsonl) writeJsonLine(input.io.stdout, { type: "response.snapshot", chatId: created.chat.id, snapshot: created.response });
|
|
17750
|
+
const finalSnapshot = await (input.follow ?? followResponse)({
|
|
17751
|
+
baseUrl: input.baseUrl,
|
|
17752
|
+
token: input.token,
|
|
17753
|
+
snapshot: created.response,
|
|
17754
|
+
onEvent: (event) => {
|
|
17755
|
+
if (input.jsonl) writeJsonLine(input.io.stdout, { type: "response.event", chatId: created.chat.id, event });
|
|
17756
|
+
if (input.streamText && event.type === "response.output_text.delta") {
|
|
17757
|
+
const delta = event.payload.delta;
|
|
17758
|
+
if (typeof delta === "string") {
|
|
17759
|
+
input.io.stdout.write(delta);
|
|
17760
|
+
writtenText += delta;
|
|
17761
|
+
}
|
|
17762
|
+
}
|
|
17763
|
+
},
|
|
17764
|
+
onSnapshot: (snapshot) => {
|
|
17765
|
+
if (input.jsonl) writeJsonLine(input.io.stdout, { type: "response.snapshot", chatId: created.chat.id, snapshot });
|
|
17766
|
+
writeProgress(snapshot);
|
|
17767
|
+
}
|
|
17768
|
+
});
|
|
17769
|
+
writeProgress(finalSnapshot);
|
|
17770
|
+
if (input.streamText) input.io.stdout.write("\n");
|
|
17771
|
+
return {
|
|
17772
|
+
chatId: created.chat.id,
|
|
17773
|
+
responseId: finalSnapshot.responseId,
|
|
17774
|
+
modelId: input.model.id,
|
|
17775
|
+
agentMode: input.agentMode,
|
|
17776
|
+
presetSelections: input.presetSelections,
|
|
17777
|
+
temporary,
|
|
17778
|
+
snapshot: finalSnapshot
|
|
17779
|
+
};
|
|
17780
|
+
}
|
|
17781
|
+
|
|
17419
17782
|
// src/index.ts
|
|
17420
|
-
var CLI_VERSION = true ? "0.
|
|
17783
|
+
var CLI_VERSION = true ? "0.59.0" : "0.1.0";
|
|
17421
17784
|
var CLI_BUNDLED = true;
|
|
17422
17785
|
var commandIo = /* @__PURE__ */ new WeakMap();
|
|
17423
17786
|
var commandClientFactory = /* @__PURE__ */ new WeakMap();
|
|
17787
|
+
var NamedOption = class extends Option {
|
|
17788
|
+
constructor(flags, optionAttribute, description) {
|
|
17789
|
+
super(flags, description);
|
|
17790
|
+
this.optionAttribute = optionAttribute;
|
|
17791
|
+
}
|
|
17792
|
+
attributeName() {
|
|
17793
|
+
return this.optionAttribute;
|
|
17794
|
+
}
|
|
17795
|
+
};
|
|
17424
17796
|
function catalogIconContentType(filename) {
|
|
17425
17797
|
const extension = /\.([^.]+)$/.exec(filename)?.[1]?.toLowerCase();
|
|
17426
17798
|
return extension === "png" ? "image/png" : extension === "jpg" || extension === "jpeg" ? "image/jpeg" : extension === "webp" ? "image/webp" : extension === "svg" ? "image/svg+xml" : null;
|
|
@@ -17542,7 +17914,14 @@ async function clientFor(command, authenticated = true) {
|
|
|
17542
17914
|
if (capability && !info.capabilities.includes(capability)) {
|
|
17543
17915
|
throw new Error(`The selected Pulpo instance does not advertise the ${capability} capability`);
|
|
17544
17916
|
}
|
|
17545
|
-
return {
|
|
17917
|
+
return {
|
|
17918
|
+
client,
|
|
17919
|
+
contextName: connection.contextName,
|
|
17920
|
+
context: connection.context,
|
|
17921
|
+
info,
|
|
17922
|
+
token: connection.token ?? "",
|
|
17923
|
+
url: connection.url
|
|
17924
|
+
};
|
|
17546
17925
|
}
|
|
17547
17926
|
async function twoFactorSecret(io, json2, prompt = "Authenticator or recovery code: ") {
|
|
17548
17927
|
const value = process.env.PULPO_2FA_CODE;
|
|
@@ -17902,6 +18281,46 @@ function createProgram(io = processIo, dependencies = {}) {
|
|
|
17902
18281
|
const icons = CHAT_PRESET_ICON_NAMES.filter((name) => !normalized || name.includes(normalized)).map((name) => ({ name }));
|
|
17903
18282
|
emit(io, command, icons);
|
|
17904
18283
|
});
|
|
18284
|
+
model.command("test <id> [prompt...]").description("Send a model smoke-test message with explicit user-facing options").addOption(new NamedOption("--agent", "agentEnabled", "run with agent mode enabled").conflicts("agentDisabled")).addOption(new NamedOption("--no-agent", "agentDisabled", "run with agent mode disabled").default(void 0).conflicts("agentEnabled")).option("--preset <preset=choice>", "explicit preset choice; repeat for every exposed preset", (value, previous) => [...previous, value], []).option("--keep", "keep the test in normal chat history").option("--no-stream", "wait and print only the completed response").option("--jsonl", "stream response events as JSON Lines").action(async (id, promptParts, options, command) => {
|
|
18285
|
+
const globals = globalOptions(command);
|
|
18286
|
+
if (globals.json && options.jsonl) throw new Error("--json and --jsonl cannot be used together");
|
|
18287
|
+
if (options.jsonl && options.stream === false) throw new Error("--jsonl and --no-stream cannot be used together");
|
|
18288
|
+
const agentMode = explicitAgentMode(options);
|
|
18289
|
+
const prompt = await readModelTestPrompt(io, promptParts ?? []);
|
|
18290
|
+
const { client, token: token2, url: url2 } = await clientFor(command);
|
|
18291
|
+
if (!token2) throw new Error("Model tests require a logged-in session. Run `pulpo auth login`.");
|
|
18292
|
+
const catalog = await client.request("/api/models");
|
|
18293
|
+
const selectedModel = catalog.data.find((candidate) => candidate.id === id);
|
|
18294
|
+
if (!selectedModel) throw new Error(`Exposed model not found: ${id}`);
|
|
18295
|
+
if (agentMode && !selectedModel.agentEnabled) throw new Error(`Model ${id} does not expose agent mode`);
|
|
18296
|
+
if (agentMode && !catalog.agentAvailable) throw new Error("Agent mode is not available on the selected Pulpo instance");
|
|
18297
|
+
const presetSelections = resolvePresetSelections(selectedModel, options.preset);
|
|
18298
|
+
const result = await runModelTest({
|
|
18299
|
+
client,
|
|
18300
|
+
baseUrl: url2,
|
|
18301
|
+
token: token2,
|
|
18302
|
+
model: selectedModel,
|
|
18303
|
+
prompt,
|
|
18304
|
+
agentMode,
|
|
18305
|
+
presetSelections,
|
|
18306
|
+
keep: Boolean(options.keep),
|
|
18307
|
+
streamText: !globals.json && !options.jsonl && options.stream !== false,
|
|
18308
|
+
jsonl: Boolean(options.jsonl),
|
|
18309
|
+
io,
|
|
18310
|
+
follow: dependencies.followResponse ?? followResponse
|
|
18311
|
+
});
|
|
18312
|
+
if (globals.json) emit(io, command, result);
|
|
18313
|
+
else if (!options.jsonl && options.stream === false) io.stdout.write(`${responseText(result.snapshot)}
|
|
18314
|
+
`);
|
|
18315
|
+
if (!globals.json && !options.jsonl) {
|
|
18316
|
+
io.stderr.write(`Chat ${result.chatId} \xB7 response ${result.responseId}${result.temporary ? " \xB7 temporary" : ""}
|
|
18317
|
+
`);
|
|
18318
|
+
}
|
|
18319
|
+
if (!["completed", "incomplete"].includes(result.snapshot.status)) {
|
|
18320
|
+
const detail = result.snapshot.error && typeof result.snapshot.error === "object" ? result.snapshot.error.message : result.snapshot.error;
|
|
18321
|
+
throw new Error(typeof detail === "string" ? detail : `Response ended with status ${result.snapshot.status}`);
|
|
18322
|
+
}
|
|
18323
|
+
});
|
|
17905
18324
|
const user = registerFileCrud(program, io, { name: "user", pluralPath: "/api/management/v1/users" });
|
|
17906
18325
|
for (const [action, patch] of [["approve", { role: "user" }], ["block", { blocked: true }], ["unblock", { blocked: false }]]) {
|
|
17907
18326
|
user.command(`${action} <id>`).action(async (id, _options, command) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isaacthoman/pulpo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.0",
|
|
4
4
|
"description": "Operator-first command-line client for Pulpo",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"pack:check": "npm pack --dry-run"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"commander": "^14.0.2"
|
|
34
|
+
"commander": "^14.0.2",
|
|
35
|
+
"socket.io-client": "^4.8.3"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@pulpo/client-core": "0.1.0",
|