@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.
Files changed (3) hide show
  1. package/README.md +24 -0
  2. package/dist/index.js +432 -7
  3. 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 terminal = i === fullpath.length - 1;
1363
- if (!terminal) {
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 terminal = i === fullpath.length - 1;
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 (terminal) {
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.58.0" : "0.1.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 { client, contextName: connection.contextName, context: connection.context, info };
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.58.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",