@isaacthoman/pulpo 0.58.0 → 0.60.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 +432 -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++;
|
|
@@ -16290,6 +16290,8 @@ var CHAT_PRESET_ICON_NAMES = [
|
|
|
16290
16290
|
// ../../packages/contracts/dist/index.js
|
|
16291
16291
|
var idSchema = external_exports.uuid();
|
|
16292
16292
|
var isoDateSchema = external_exports.iso.datetime();
|
|
16293
|
+
var DEFAULT_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
16294
|
+
var MAX_CONFIGURABLE_ATTACHMENT_BYTES = 1e3 * 1024 * 1024;
|
|
16293
16295
|
var roleSchema = external_exports.enum(["pending", "user", "admin"]);
|
|
16294
16296
|
var userSchema = external_exports.object({
|
|
16295
16297
|
id: idSchema,
|
|
@@ -16339,6 +16341,9 @@ var mobileConfigSchema = external_exports.object({
|
|
|
16339
16341
|
adminEmail: external_exports.string(),
|
|
16340
16342
|
pendingMessage: external_exports.string()
|
|
16341
16343
|
}),
|
|
16344
|
+
limits: external_exports.object({
|
|
16345
|
+
maxAttachmentBytes: external_exports.number().int().nonnegative().max(MAX_CONFIGURABLE_ATTACHMENT_BYTES)
|
|
16346
|
+
}).default({ maxAttachmentBytes: DEFAULT_MAX_ATTACHMENT_BYTES }),
|
|
16342
16347
|
capabilities: external_exports.object({
|
|
16343
16348
|
bearerSessions: external_exports.literal(true),
|
|
16344
16349
|
realtime: external_exports.literal(true),
|
|
@@ -16462,6 +16467,167 @@ var responseSnapshotSchema = external_exports.object({
|
|
|
16462
16467
|
updatedAt: isoDateSchema
|
|
16463
16468
|
});
|
|
16464
16469
|
var embeddedResponseSnapshotSchema = responseSnapshotSchema.omit({ output: true });
|
|
16470
|
+
function targetItemIndex(output, payload, type) {
|
|
16471
|
+
const itemId = typeof payload.item_id === "string" ? payload.item_id : typeof payload.itemId === "string" ? payload.itemId : void 0;
|
|
16472
|
+
if (itemId) {
|
|
16473
|
+
const byId = output.findIndex((item) => item.id === itemId);
|
|
16474
|
+
return byId;
|
|
16475
|
+
}
|
|
16476
|
+
const outputIndex = typeof payload.output_index === "number" ? payload.output_index : typeof payload.outputIndex === "number" ? payload.outputIndex : void 0;
|
|
16477
|
+
if (outputIndex !== void 0 && output[outputIndex]?.type === type)
|
|
16478
|
+
return outputIndex;
|
|
16479
|
+
const agentTurn = typeof payload.agent_turn === "number" ? payload.agent_turn : void 0;
|
|
16480
|
+
const contentIndex = typeof payload.content_index === "number" ? payload.content_index : typeof payload.contentIndex === "number" ? payload.contentIndex : void 0;
|
|
16481
|
+
if (agentTurn !== void 0 && contentIndex !== void 0) {
|
|
16482
|
+
const byAgentPart = output.findIndex((item) => item.type === type && item.agent_turn === agentTurn && item.agent_content_index === contentIndex);
|
|
16483
|
+
if (byAgentPart >= 0)
|
|
16484
|
+
return byAgentPart;
|
|
16485
|
+
}
|
|
16486
|
+
for (let index = output.length - 1; index >= 0; index -= 1) {
|
|
16487
|
+
if (output[index]?.type === type && output[index]?.status === "in_progress")
|
|
16488
|
+
return index;
|
|
16489
|
+
}
|
|
16490
|
+
for (let index = output.length - 1; index >= 0; index -= 1) {
|
|
16491
|
+
if (output[index]?.type === type)
|
|
16492
|
+
return index;
|
|
16493
|
+
}
|
|
16494
|
+
return -1;
|
|
16495
|
+
}
|
|
16496
|
+
function appendOutputText(output, delta, payload) {
|
|
16497
|
+
const copy = output.slice();
|
|
16498
|
+
const index = targetItemIndex(copy, payload, "message");
|
|
16499
|
+
let message = index >= 0 ? { ...copy[index] } : void 0;
|
|
16500
|
+
if (!message) {
|
|
16501
|
+
const itemId = typeof payload.item_id === "string" ? payload.item_id : typeof payload.itemId === "string" ? payload.itemId : void 0;
|
|
16502
|
+
message = { ...itemId ? { id: itemId } : {}, type: "message", role: "assistant", status: "in_progress", content: [] };
|
|
16503
|
+
copy.push(message);
|
|
16504
|
+
} else {
|
|
16505
|
+
copy[index] = message;
|
|
16506
|
+
}
|
|
16507
|
+
const content = Array.isArray(message.content) ? message.content.slice() : [];
|
|
16508
|
+
const contentIndex = typeof payload.content_index === "number" ? payload.content_index : typeof payload.contentIndex === "number" ? payload.contentIndex : void 0;
|
|
16509
|
+
const partIndex = contentIndex !== void 0 && content[contentIndex]?.type === "output_text" ? contentIndex : content.findIndex((item) => item.type === "output_text");
|
|
16510
|
+
let part = partIndex >= 0 ? { ...content[partIndex] } : void 0;
|
|
16511
|
+
if (!part) {
|
|
16512
|
+
part = { type: "output_text", text: "" };
|
|
16513
|
+
content.push(part);
|
|
16514
|
+
} else {
|
|
16515
|
+
content[partIndex] = part;
|
|
16516
|
+
}
|
|
16517
|
+
part.text = `${typeof part.text === "string" ? part.text : ""}${delta}`;
|
|
16518
|
+
message.content = content;
|
|
16519
|
+
return copy;
|
|
16520
|
+
}
|
|
16521
|
+
function appendReasoning(output, delta, payload) {
|
|
16522
|
+
const copy = output.slice();
|
|
16523
|
+
const index = targetItemIndex(copy, payload, "reasoning");
|
|
16524
|
+
let reasoning = index >= 0 ? { ...copy[index] } : void 0;
|
|
16525
|
+
if (!reasoning) {
|
|
16526
|
+
const itemId = typeof payload.item_id === "string" ? payload.item_id : typeof payload.itemId === "string" ? payload.itemId : void 0;
|
|
16527
|
+
reasoning = { ...itemId ? { id: itemId } : {}, type: "reasoning", status: "in_progress", summary: [] };
|
|
16528
|
+
copy.push(reasoning);
|
|
16529
|
+
} else {
|
|
16530
|
+
copy[index] = reasoning;
|
|
16531
|
+
}
|
|
16532
|
+
const summary = Array.isArray(reasoning.summary) ? reasoning.summary.slice() : [];
|
|
16533
|
+
const partIndex = summary.findIndex((item) => item.type === "summary_text");
|
|
16534
|
+
let part = partIndex >= 0 ? { ...summary[partIndex] } : void 0;
|
|
16535
|
+
if (!part) {
|
|
16536
|
+
part = { type: "summary_text", text: "" };
|
|
16537
|
+
summary.push(part);
|
|
16538
|
+
} else {
|
|
16539
|
+
summary[partIndex] = part;
|
|
16540
|
+
}
|
|
16541
|
+
part.text = `${typeof part.text === "string" ? part.text : ""}${delta}`;
|
|
16542
|
+
reasoning.summary = summary;
|
|
16543
|
+
return copy;
|
|
16544
|
+
}
|
|
16545
|
+
function upsertOutputItem(output, match, value) {
|
|
16546
|
+
const copy = output.slice();
|
|
16547
|
+
const index = copy.findIndex((item) => Boolean(item) && typeof item === "object" && match(item));
|
|
16548
|
+
if (index < 0)
|
|
16549
|
+
copy.push(value);
|
|
16550
|
+
else
|
|
16551
|
+
copy[index] = { ...copy[index], ...value };
|
|
16552
|
+
return copy;
|
|
16553
|
+
}
|
|
16554
|
+
function applyAgentEventOutput(output, event) {
|
|
16555
|
+
const payload = event.payload;
|
|
16556
|
+
if (event.type.startsWith("pulpo.agent.workspace.")) {
|
|
16557
|
+
return upsertOutputItem(output, (item) => item.type === "pulpo_workspace", payload);
|
|
16558
|
+
}
|
|
16559
|
+
if (event.type === "pulpo.compaction.updated" && typeof payload.id === "string") {
|
|
16560
|
+
return upsertOutputItem(output, (item) => item.id === payload.id, payload);
|
|
16561
|
+
}
|
|
16562
|
+
if (event.type === "pulpo.agent.attachment.created" && typeof payload.attachment_id === "string") {
|
|
16563
|
+
return upsertOutputItem(output, (item) => item.type === "pulpo_attachment" && item.attachment_id === payload.attachment_id, payload);
|
|
16564
|
+
}
|
|
16565
|
+
if (!event.type.startsWith("pulpo.agent.tool.") || typeof payload.id !== "string")
|
|
16566
|
+
return output;
|
|
16567
|
+
if (event.type === "pulpo.agent.tool.delta") {
|
|
16568
|
+
return upsertOutputItem(output, (item) => item.id === payload.id, {
|
|
16569
|
+
id: payload.id,
|
|
16570
|
+
type: "pulpo_tool",
|
|
16571
|
+
output: typeof payload.delta === "string" ? payload.delta : "",
|
|
16572
|
+
status: "running"
|
|
16573
|
+
});
|
|
16574
|
+
}
|
|
16575
|
+
if (event.type === "pulpo.agent.tool.completed") {
|
|
16576
|
+
return upsertOutputItem(output, (item) => item.id === payload.id, {
|
|
16577
|
+
...payload,
|
|
16578
|
+
type: "pulpo_tool",
|
|
16579
|
+
status: payload.isError ? "failed" : "completed"
|
|
16580
|
+
});
|
|
16581
|
+
}
|
|
16582
|
+
return upsertOutputItem(output, (item) => item.id === payload.id, payload);
|
|
16583
|
+
}
|
|
16584
|
+
function applyResponseEventToSnapshot(snapshot, event) {
|
|
16585
|
+
if (event.sequence <= snapshot.sequence)
|
|
16586
|
+
return snapshot;
|
|
16587
|
+
const payload = event.payload;
|
|
16588
|
+
const delta = typeof payload.delta === "string" ? payload.delta : "";
|
|
16589
|
+
let output = snapshot.output;
|
|
16590
|
+
if (delta && event.type === "response.output_text.delta")
|
|
16591
|
+
output = appendOutputText(output, delta, payload);
|
|
16592
|
+
if (delta && event.type === "response.reasoning_summary_text.delta")
|
|
16593
|
+
output = appendReasoning(output, delta, payload);
|
|
16594
|
+
output = applyAgentEventOutput(output, event);
|
|
16595
|
+
return {
|
|
16596
|
+
...snapshot,
|
|
16597
|
+
status: snapshot.status === "queued" ? "in_progress" : snapshot.status,
|
|
16598
|
+
sequence: event.sequence,
|
|
16599
|
+
output,
|
|
16600
|
+
updatedAt: event.emittedAt
|
|
16601
|
+
};
|
|
16602
|
+
}
|
|
16603
|
+
function mergeResponseSnapshots(current, incoming) {
|
|
16604
|
+
if (incoming.sequence < current.sequence)
|
|
16605
|
+
return current;
|
|
16606
|
+
if (incoming.sequence === current.sequence) {
|
|
16607
|
+
const currentTerminal = current.status !== "queued" && current.status !== "in_progress";
|
|
16608
|
+
const incomingTerminal = incoming.status !== "queued" && incoming.status !== "in_progress";
|
|
16609
|
+
if (currentTerminal && !incomingTerminal)
|
|
16610
|
+
return current;
|
|
16611
|
+
if (incomingTerminal && !currentTerminal) {
|
|
16612
|
+
return incoming.output.length === 0 && current.output.length > 0 ? { ...incoming, output: current.output } : incoming;
|
|
16613
|
+
}
|
|
16614
|
+
if (incoming.updatedAt < current.updatedAt)
|
|
16615
|
+
return current;
|
|
16616
|
+
if (incoming.updatedAt === current.updatedAt) {
|
|
16617
|
+
if (current.output.length === 0 && incoming.output.length > 0)
|
|
16618
|
+
return incoming;
|
|
16619
|
+
return current;
|
|
16620
|
+
}
|
|
16621
|
+
if (incoming.output.length === 0 && current.output.length > 0) {
|
|
16622
|
+
return { ...incoming, output: current.output };
|
|
16623
|
+
}
|
|
16624
|
+
}
|
|
16625
|
+
const incomingIsActive = incoming.status === "queued" || incoming.status === "in_progress";
|
|
16626
|
+
if (incomingIsActive && incoming.output.length === 0 && current.output.length > 0) {
|
|
16627
|
+
return { ...incoming, output: current.output };
|
|
16628
|
+
}
|
|
16629
|
+
return incoming;
|
|
16630
|
+
}
|
|
16465
16631
|
var catalogIconModeSchema = external_exports.enum(["original", "monochrome"]);
|
|
16466
16632
|
var catalogIconReferenceSchema = external_exports.object({
|
|
16467
16633
|
id: idSchema,
|
|
@@ -16720,6 +16886,7 @@ var authSettingsSchema = external_exports.object({
|
|
|
16720
16886
|
signupEnabled: external_exports.boolean().default(true),
|
|
16721
16887
|
defaultBalanceMicros: external_exports.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).default(5e6),
|
|
16722
16888
|
defaultStorageLimitBytes: external_exports.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).default(5e3 * 1024 * 1024),
|
|
16889
|
+
maxAttachmentBytes: external_exports.number().int().nonnegative().max(MAX_CONFIGURABLE_ATTACHMENT_BYTES).default(DEFAULT_MAX_ATTACHMENT_BYTES),
|
|
16723
16890
|
pendingDetails: external_exports.boolean().default(true),
|
|
16724
16891
|
adminEmail: external_exports.union([external_exports.literal(""), external_exports.email()]).default(""),
|
|
16725
16892
|
pendingMessage: external_exports.string().max(2e3).default("Your account is pending approval. An admin will review it shortly."),
|
|
@@ -17416,11 +17583,222 @@ async function confirmExact(io, expected, yes, json2) {
|
|
|
17416
17583
|
}
|
|
17417
17584
|
}
|
|
17418
17585
|
|
|
17586
|
+
// src/model-test.ts
|
|
17587
|
+
import { randomUUID } from "node:crypto";
|
|
17588
|
+
import { io as createSocket } from "socket.io-client";
|
|
17589
|
+
function explicitAgentMode(options) {
|
|
17590
|
+
const enabled = options.agentEnabled === true;
|
|
17591
|
+
const disabled = options.agentDisabled === false;
|
|
17592
|
+
if (enabled === disabled) {
|
|
17593
|
+
throw selectionError(["choose --agent or --no-agent"]);
|
|
17594
|
+
}
|
|
17595
|
+
return enabled;
|
|
17596
|
+
}
|
|
17597
|
+
function terminal(snapshot) {
|
|
17598
|
+
return snapshot.status !== "queued" && snapshot.status !== "in_progress";
|
|
17599
|
+
}
|
|
17600
|
+
async function followResponse(input) {
|
|
17601
|
+
if (terminal(input.snapshot)) return input.snapshot;
|
|
17602
|
+
return new Promise((resolve, reject) => {
|
|
17603
|
+
let current = input.snapshot;
|
|
17604
|
+
const pending = /* @__PURE__ */ new Map();
|
|
17605
|
+
const socket = createSocket(input.baseUrl, {
|
|
17606
|
+
path: "/socket.io",
|
|
17607
|
+
auth: { sessionToken: input.token },
|
|
17608
|
+
autoConnect: false,
|
|
17609
|
+
reconnection: true
|
|
17610
|
+
});
|
|
17611
|
+
let settled = false;
|
|
17612
|
+
const cleanup = () => {
|
|
17613
|
+
socket.removeAllListeners();
|
|
17614
|
+
socket.disconnect();
|
|
17615
|
+
};
|
|
17616
|
+
const fail = (error51) => {
|
|
17617
|
+
if (settled) return;
|
|
17618
|
+
settled = true;
|
|
17619
|
+
cleanup();
|
|
17620
|
+
reject(error51);
|
|
17621
|
+
};
|
|
17622
|
+
const finishIfTerminal = () => {
|
|
17623
|
+
if (settled || !terminal(current)) return;
|
|
17624
|
+
settled = true;
|
|
17625
|
+
cleanup();
|
|
17626
|
+
resolve(current);
|
|
17627
|
+
};
|
|
17628
|
+
const flush = () => {
|
|
17629
|
+
for (; ; ) {
|
|
17630
|
+
const event = pending.get(current.sequence + 1);
|
|
17631
|
+
if (!event) break;
|
|
17632
|
+
pending.delete(event.sequence);
|
|
17633
|
+
current = applyResponseEventToSnapshot(current, event);
|
|
17634
|
+
input.onEvent?.(event);
|
|
17635
|
+
}
|
|
17636
|
+
};
|
|
17637
|
+
socket.on("connect", () => {
|
|
17638
|
+
socket.emit("response.subscribe", {
|
|
17639
|
+
responseId: current.responseId,
|
|
17640
|
+
afterSequence: current.sequence
|
|
17641
|
+
});
|
|
17642
|
+
});
|
|
17643
|
+
socket.on("connect_error", (error51) => fail(new Error(`Realtime connection failed: ${error51.message}`)));
|
|
17644
|
+
socket.on("response.event", (event) => {
|
|
17645
|
+
if (event.responseId !== current.responseId || event.sequence <= current.sequence) return;
|
|
17646
|
+
pending.set(event.sequence, event);
|
|
17647
|
+
flush();
|
|
17648
|
+
});
|
|
17649
|
+
socket.on("response.snapshot", (snapshot) => {
|
|
17650
|
+
if (snapshot.responseId !== current.responseId) return;
|
|
17651
|
+
current = mergeResponseSnapshots(current, snapshot);
|
|
17652
|
+
for (const sequence of pending.keys()) if (sequence <= current.sequence) pending.delete(sequence);
|
|
17653
|
+
flush();
|
|
17654
|
+
input.onSnapshot?.(current);
|
|
17655
|
+
finishIfTerminal();
|
|
17656
|
+
});
|
|
17657
|
+
socket.connect();
|
|
17658
|
+
});
|
|
17659
|
+
}
|
|
17660
|
+
function selectionError(lines) {
|
|
17661
|
+
return new Error(`Explicit selections are required:
|
|
17662
|
+
${lines.map((line) => ` ${line}`).join("\n")}`);
|
|
17663
|
+
}
|
|
17664
|
+
function resolvePresetSelections(model, values) {
|
|
17665
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
17666
|
+
for (const value of values) {
|
|
17667
|
+
const separator = value.indexOf("=");
|
|
17668
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
17669
|
+
throw new Error(`Invalid preset selection ${JSON.stringify(value)}; expected <preset-id>=<choice-id>`);
|
|
17670
|
+
}
|
|
17671
|
+
const presetId = value.slice(0, separator);
|
|
17672
|
+
const choiceId = value.slice(separator + 1);
|
|
17673
|
+
if (parsed.has(presetId)) throw new Error(`Preset ${presetId} was selected more than once`);
|
|
17674
|
+
parsed.set(presetId, choiceId);
|
|
17675
|
+
}
|
|
17676
|
+
const exposed = new Map(model.presets.map((preset) => [preset.id, preset]));
|
|
17677
|
+
const unknown2 = [...parsed.keys()].filter((presetId) => !exposed.has(presetId));
|
|
17678
|
+
if (unknown2.length) throw new Error(`Unknown preset${unknown2.length === 1 ? "" : "s"} for ${model.id}: ${unknown2.join(", ")}`);
|
|
17679
|
+
const missing = model.presets.filter((preset) => !parsed.has(preset.id));
|
|
17680
|
+
if (missing.length) {
|
|
17681
|
+
throw selectionError(missing.map((preset) => `--preset ${preset.id}=<${preset.choices.map((choice) => choice.id).join("|")}>`));
|
|
17682
|
+
}
|
|
17683
|
+
for (const preset of model.presets) {
|
|
17684
|
+
const choiceId = parsed.get(preset.id);
|
|
17685
|
+
if (!preset.choices.some((choice) => choice.id === choiceId)) {
|
|
17686
|
+
throw new Error(
|
|
17687
|
+
`Unknown choice ${choiceId} for preset ${preset.id}; choose ${preset.choices.map((choice) => choice.id).join(", ")}`
|
|
17688
|
+
);
|
|
17689
|
+
}
|
|
17690
|
+
}
|
|
17691
|
+
return Object.fromEntries(parsed);
|
|
17692
|
+
}
|
|
17693
|
+
async function readModelTestPrompt(io, promptParts) {
|
|
17694
|
+
const argument = promptParts.join(" ").trim();
|
|
17695
|
+
if (argument) return argument;
|
|
17696
|
+
if (io.stdin.isTTY) throw new Error("Prompt is required as an argument or on stdin");
|
|
17697
|
+
const chunks = [];
|
|
17698
|
+
for await (const chunk of io.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
17699
|
+
const prompt = Buffer.concat(chunks).toString("utf8").trim();
|
|
17700
|
+
if (!prompt) throw new Error("Prompt is required as an argument or on stdin");
|
|
17701
|
+
return prompt;
|
|
17702
|
+
}
|
|
17703
|
+
function responseText(snapshot) {
|
|
17704
|
+
const parts = [];
|
|
17705
|
+
for (const item of snapshot.output) {
|
|
17706
|
+
if (!item || typeof item !== "object") continue;
|
|
17707
|
+
const content = item.content;
|
|
17708
|
+
if (!Array.isArray(content)) continue;
|
|
17709
|
+
for (const part of content) {
|
|
17710
|
+
if (!part || typeof part !== "object") continue;
|
|
17711
|
+
const candidate = part;
|
|
17712
|
+
if (candidate.type === "output_text" && typeof candidate.text === "string") parts.push(candidate.text);
|
|
17713
|
+
}
|
|
17714
|
+
}
|
|
17715
|
+
return parts.join("");
|
|
17716
|
+
}
|
|
17717
|
+
function writeJsonLine(output, value) {
|
|
17718
|
+
output.write(`${JSON.stringify(value)}
|
|
17719
|
+
`);
|
|
17720
|
+
}
|
|
17721
|
+
async function runModelTest(input) {
|
|
17722
|
+
const chatId = randomUUID();
|
|
17723
|
+
const responseId = randomUUID();
|
|
17724
|
+
const temporary = !input.keep;
|
|
17725
|
+
const created = await input.client.request("/api/chats/start", {
|
|
17726
|
+
method: "POST",
|
|
17727
|
+
headers: { "idempotency-key": responseId },
|
|
17728
|
+
body: {
|
|
17729
|
+
chat: {
|
|
17730
|
+
clientId: chatId,
|
|
17731
|
+
modelId: input.model.id,
|
|
17732
|
+
title: `CLI test: ${input.model.name}`.slice(0, 200),
|
|
17733
|
+
temporary
|
|
17734
|
+
},
|
|
17735
|
+
response: {
|
|
17736
|
+
clientId: responseId,
|
|
17737
|
+
parentResponseId: null,
|
|
17738
|
+
input: input.prompt,
|
|
17739
|
+
modelId: input.model.id,
|
|
17740
|
+
presetSelections: input.presetSelections,
|
|
17741
|
+
attachmentIds: [],
|
|
17742
|
+
agentMode: input.agentMode
|
|
17743
|
+
}
|
|
17744
|
+
}
|
|
17745
|
+
});
|
|
17746
|
+
let writtenText = "";
|
|
17747
|
+
const writeProgress = (snapshot) => {
|
|
17748
|
+
if (!input.streamText) return;
|
|
17749
|
+
const text = responseText(snapshot);
|
|
17750
|
+
if (text.startsWith(writtenText)) {
|
|
17751
|
+
input.io.stdout.write(text.slice(writtenText.length));
|
|
17752
|
+
writtenText = text;
|
|
17753
|
+
}
|
|
17754
|
+
};
|
|
17755
|
+
if (input.jsonl) writeJsonLine(input.io.stdout, { type: "response.snapshot", chatId: created.chat.id, snapshot: created.response });
|
|
17756
|
+
const finalSnapshot = await (input.follow ?? followResponse)({
|
|
17757
|
+
baseUrl: input.baseUrl,
|
|
17758
|
+
token: input.token,
|
|
17759
|
+
snapshot: created.response,
|
|
17760
|
+
onEvent: (event) => {
|
|
17761
|
+
if (input.jsonl) writeJsonLine(input.io.stdout, { type: "response.event", chatId: created.chat.id, event });
|
|
17762
|
+
if (input.streamText && event.type === "response.output_text.delta") {
|
|
17763
|
+
const delta = event.payload.delta;
|
|
17764
|
+
if (typeof delta === "string") {
|
|
17765
|
+
input.io.stdout.write(delta);
|
|
17766
|
+
writtenText += delta;
|
|
17767
|
+
}
|
|
17768
|
+
}
|
|
17769
|
+
},
|
|
17770
|
+
onSnapshot: (snapshot) => {
|
|
17771
|
+
if (input.jsonl) writeJsonLine(input.io.stdout, { type: "response.snapshot", chatId: created.chat.id, snapshot });
|
|
17772
|
+
writeProgress(snapshot);
|
|
17773
|
+
}
|
|
17774
|
+
});
|
|
17775
|
+
writeProgress(finalSnapshot);
|
|
17776
|
+
if (input.streamText) input.io.stdout.write("\n");
|
|
17777
|
+
return {
|
|
17778
|
+
chatId: created.chat.id,
|
|
17779
|
+
responseId: finalSnapshot.responseId,
|
|
17780
|
+
modelId: input.model.id,
|
|
17781
|
+
agentMode: input.agentMode,
|
|
17782
|
+
presetSelections: input.presetSelections,
|
|
17783
|
+
temporary,
|
|
17784
|
+
snapshot: finalSnapshot
|
|
17785
|
+
};
|
|
17786
|
+
}
|
|
17787
|
+
|
|
17419
17788
|
// src/index.ts
|
|
17420
|
-
var CLI_VERSION = true ? "0.
|
|
17789
|
+
var CLI_VERSION = true ? "0.60.0" : "0.1.0";
|
|
17421
17790
|
var CLI_BUNDLED = true;
|
|
17422
17791
|
var commandIo = /* @__PURE__ */ new WeakMap();
|
|
17423
17792
|
var commandClientFactory = /* @__PURE__ */ new WeakMap();
|
|
17793
|
+
var NamedOption = class extends Option {
|
|
17794
|
+
constructor(flags, optionAttribute, description) {
|
|
17795
|
+
super(flags, description);
|
|
17796
|
+
this.optionAttribute = optionAttribute;
|
|
17797
|
+
}
|
|
17798
|
+
attributeName() {
|
|
17799
|
+
return this.optionAttribute;
|
|
17800
|
+
}
|
|
17801
|
+
};
|
|
17424
17802
|
function catalogIconContentType(filename) {
|
|
17425
17803
|
const extension = /\.([^.]+)$/.exec(filename)?.[1]?.toLowerCase();
|
|
17426
17804
|
return extension === "png" ? "image/png" : extension === "jpg" || extension === "jpeg" ? "image/jpeg" : extension === "webp" ? "image/webp" : extension === "svg" ? "image/svg+xml" : null;
|
|
@@ -17542,7 +17920,14 @@ async function clientFor(command, authenticated = true) {
|
|
|
17542
17920
|
if (capability && !info.capabilities.includes(capability)) {
|
|
17543
17921
|
throw new Error(`The selected Pulpo instance does not advertise the ${capability} capability`);
|
|
17544
17922
|
}
|
|
17545
|
-
return {
|
|
17923
|
+
return {
|
|
17924
|
+
client,
|
|
17925
|
+
contextName: connection.contextName,
|
|
17926
|
+
context: connection.context,
|
|
17927
|
+
info,
|
|
17928
|
+
token: connection.token ?? "",
|
|
17929
|
+
url: connection.url
|
|
17930
|
+
};
|
|
17546
17931
|
}
|
|
17547
17932
|
async function twoFactorSecret(io, json2, prompt = "Authenticator or recovery code: ") {
|
|
17548
17933
|
const value = process.env.PULPO_2FA_CODE;
|
|
@@ -17902,6 +18287,46 @@ function createProgram(io = processIo, dependencies = {}) {
|
|
|
17902
18287
|
const icons = CHAT_PRESET_ICON_NAMES.filter((name) => !normalized || name.includes(normalized)).map((name) => ({ name }));
|
|
17903
18288
|
emit(io, command, icons);
|
|
17904
18289
|
});
|
|
18290
|
+
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) => {
|
|
18291
|
+
const globals = globalOptions(command);
|
|
18292
|
+
if (globals.json && options.jsonl) throw new Error("--json and --jsonl cannot be used together");
|
|
18293
|
+
if (options.jsonl && options.stream === false) throw new Error("--jsonl and --no-stream cannot be used together");
|
|
18294
|
+
const agentMode = explicitAgentMode(options);
|
|
18295
|
+
const prompt = await readModelTestPrompt(io, promptParts ?? []);
|
|
18296
|
+
const { client, token: token2, url: url2 } = await clientFor(command);
|
|
18297
|
+
if (!token2) throw new Error("Model tests require a logged-in session. Run `pulpo auth login`.");
|
|
18298
|
+
const catalog = await client.request("/api/models");
|
|
18299
|
+
const selectedModel = catalog.data.find((candidate) => candidate.id === id);
|
|
18300
|
+
if (!selectedModel) throw new Error(`Exposed model not found: ${id}`);
|
|
18301
|
+
if (agentMode && !selectedModel.agentEnabled) throw new Error(`Model ${id} does not expose agent mode`);
|
|
18302
|
+
if (agentMode && !catalog.agentAvailable) throw new Error("Agent mode is not available on the selected Pulpo instance");
|
|
18303
|
+
const presetSelections = resolvePresetSelections(selectedModel, options.preset);
|
|
18304
|
+
const result = await runModelTest({
|
|
18305
|
+
client,
|
|
18306
|
+
baseUrl: url2,
|
|
18307
|
+
token: token2,
|
|
18308
|
+
model: selectedModel,
|
|
18309
|
+
prompt,
|
|
18310
|
+
agentMode,
|
|
18311
|
+
presetSelections,
|
|
18312
|
+
keep: Boolean(options.keep),
|
|
18313
|
+
streamText: !globals.json && !options.jsonl && options.stream !== false,
|
|
18314
|
+
jsonl: Boolean(options.jsonl),
|
|
18315
|
+
io,
|
|
18316
|
+
follow: dependencies.followResponse ?? followResponse
|
|
18317
|
+
});
|
|
18318
|
+
if (globals.json) emit(io, command, result);
|
|
18319
|
+
else if (!options.jsonl && options.stream === false) io.stdout.write(`${responseText(result.snapshot)}
|
|
18320
|
+
`);
|
|
18321
|
+
if (!globals.json && !options.jsonl) {
|
|
18322
|
+
io.stderr.write(`Chat ${result.chatId} \xB7 response ${result.responseId}${result.temporary ? " \xB7 temporary" : ""}
|
|
18323
|
+
`);
|
|
18324
|
+
}
|
|
18325
|
+
if (!["completed", "incomplete"].includes(result.snapshot.status)) {
|
|
18326
|
+
const detail = result.snapshot.error && typeof result.snapshot.error === "object" ? result.snapshot.error.message : result.snapshot.error;
|
|
18327
|
+
throw new Error(typeof detail === "string" ? detail : `Response ended with status ${result.snapshot.status}`);
|
|
18328
|
+
}
|
|
18329
|
+
});
|
|
17905
18330
|
const user = registerFileCrud(program, io, { name: "user", pluralPath: "/api/management/v1/users" });
|
|
17906
18331
|
for (const [action, patch] of [["approve", { role: "user" }], ["block", { blocked: true }], ["unblock", { blocked: false }]]) {
|
|
17907
18332
|
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.60.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",
|