@neta-art/cohub-cli 10.1.0 → 10.2.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.
@@ -0,0 +1,4 @@
1
+ export declare function collectOption(value: string, previous?: string[]): string[];
2
+ export declare function readPromptContent(words: string[], options?: {
3
+ allowEmpty?: boolean;
4
+ }): Promise<string>;
@@ -0,0 +1,16 @@
1
+ import { error } from "./output.js";
2
+ export function collectOption(value, previous = []) {
3
+ return [...previous, value];
4
+ }
5
+ export async function readPromptContent(words, options = {}) {
6
+ let content = words.join(" ");
7
+ if (!content && !process.stdin.isTTY) {
8
+ const chunks = [];
9
+ for await (const chunk of process.stdin)
10
+ chunks.push(chunk);
11
+ content = Buffer.concat(chunks).toString().trim();
12
+ }
13
+ if (!content && !options.allowEmpty)
14
+ return error("No content", "Pass as argument or pipe via stdin");
15
+ return content;
16
+ }
@@ -1,9 +1,7 @@
1
1
  import { resolveExecutionAppId, } from "@neta-art/cohub";
2
2
  import { createClient } from "../client.js";
3
+ import { collectOption } from "../command-input.js";
3
4
  import { error, formatLocalDateTime, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
4
- function collectOption(value, previous = []) {
5
- return [...previous, value];
6
- }
7
5
  function requireText(value, label, flag) {
8
6
  const text = value?.trim();
9
7
  if (text)
@@ -0,0 +1,4 @@
1
+ import type { DecisionStateImage } from "@neta-art/cohub";
2
+ import type { Command } from "commander";
3
+ export declare function mergeDecisionState(state: unknown, text: string, images: DecisionStateImage[]): unknown;
4
+ export declare function registerDecisionCommand(parent: Command): void;
@@ -0,0 +1,113 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { uploadChatImageAsset } from "../avatar.js";
3
+ import { createClient } from "../client.js";
4
+ import { collectOption, readPromptContent } from "../command-input.js";
5
+ import { error, handleHttp, json as outJson, jsonRequested, table } from "../output.js";
6
+ import { resolveSpace } from "../space.js";
7
+ const REMOTE_URL = /^https?:\/\//i;
8
+ async function readJsonObject(source, label) {
9
+ const text = source.trim().startsWith("{")
10
+ ? source
11
+ : await readFile(source, "utf-8").catch(() => error(`Cannot read ${label}`, source));
12
+ let value;
13
+ try {
14
+ value = JSON.parse(text);
15
+ }
16
+ catch {
17
+ return error(`Invalid ${label}`, "Expected a JSON object or a path to a JSON file");
18
+ }
19
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
20
+ return error(`Invalid ${label}`, "Expected a JSON object");
21
+ }
22
+ return value;
23
+ }
24
+ export function mergeDecisionState(state, text, images) {
25
+ if (!text && images.length === 0)
26
+ return state;
27
+ if (state !== undefined && typeof state !== "string" && !Array.isArray(state)) {
28
+ throw new Error("Cannot combine a JSON object state with text or --image; use a text/image array in --input instead.");
29
+ }
30
+ const parts = [
31
+ ...(Array.isArray(state) ? state : typeof state === "string" ? [state] : []),
32
+ ...(text ? [text] : []),
33
+ ...images,
34
+ ];
35
+ return parts.length === 1 && typeof parts[0] === "string" ? parts[0] : parts;
36
+ }
37
+ function formatAnswer(answer) {
38
+ if (answer.type === "noul")
39
+ return (answer.noul ?? 0).toFixed(2);
40
+ if (answer.type === "choice")
41
+ return answer.choice ?? "";
42
+ const top = Object.entries(answer.probabilities ?? {}).sort(([, a], [, b]) => b - a)[0]?.[0];
43
+ const label = top === undefined ? undefined : answer.legend?.[top];
44
+ return `${(answer.score ?? 0).toFixed(2)}${label ? ` ${label}` : ""}`;
45
+ }
46
+ function printResult(result) {
47
+ table(Object.entries(result.answers).map(([key, answer]) => ({
48
+ question: key,
49
+ type: answer.type,
50
+ answer: formatAnswer(answer),
51
+ confidence: answer.confidence,
52
+ })), [
53
+ { key: "question", label: "QUESTION" },
54
+ { key: "type", label: "TYPE" },
55
+ { key: "answer", label: "ANSWER" },
56
+ { key: "confidence", label: "CONFIDENCE", format: (value) => (typeof value === "number" ? value.toFixed(2) : "") },
57
+ ]);
58
+ const cost = result.usage.cost?.total;
59
+ process.stderr.write(`\n ${result.provider}/${result.model}${typeof cost === "number" ? ` cost: $${cost}` : ""}\n`);
60
+ }
61
+ async function runDecisionCommand(command, words, opts) {
62
+ const base = opts.input ? await readJsonObject(opts.input, "input") : {};
63
+ const questions = opts.questions ? await readJsonObject(opts.questions, "questions") : base.questions;
64
+ if (!questions)
65
+ return error("Questions required", "Pass --questions or --input");
66
+ const spaceId = await resolveSpace(command, { home: true });
67
+ const client = createClient();
68
+ try {
69
+ const images = opts.image ?? [];
70
+ const text = words.length > 0 || base.state === undefined
71
+ ? await readPromptContent(words, { allowEmpty: images.length > 0 })
72
+ : "";
73
+ const imageParts = images.map((url) => ({ type: "image_url", image_url: { url } }));
74
+ const state = mergeDecisionState(base.state, text, imageParts);
75
+ await Promise.all(imageParts.map(async (part) => {
76
+ if (!REMOTE_URL.test(part.image_url.url)) {
77
+ part.image_url.url = (await uploadChatImageAsset({ client, spaceId, path: part.image_url.url })).publicUrl;
78
+ }
79
+ }));
80
+ const input = {
81
+ ...base,
82
+ provider: opts.provider ?? base.provider,
83
+ model: opts.model ?? base.model,
84
+ state,
85
+ questions,
86
+ };
87
+ const result = await client.space(spaceId).decision(input);
88
+ if (jsonRequested(opts))
89
+ return outJson(result);
90
+ printResult(result);
91
+ }
92
+ catch (e) {
93
+ handleHttp(e);
94
+ }
95
+ }
96
+ export function registerDecisionCommand(parent) {
97
+ parent
98
+ .command("decision [state...]")
99
+ .description("Answer yes/no, choice and score questions about text or images")
100
+ .option("-q, --questions <json|path>", "Questions as inline JSON or a JSON file")
101
+ .option("--input <path>", "Full request JSON file (state, questions, model)")
102
+ .option("--image <path-or-url>", "Attach an image to the state", collectOption, [])
103
+ .option("-m, --model <model>", "Decision model ID")
104
+ .option("-p, --provider <provider>", "Provider name")
105
+ .option("--json", "Output as JSON")
106
+ .addHelpText("after", `
107
+ Examples:
108
+ cohub decision "I was charged twice" -q '{"refund":{"type":"noul","instructions":"Refund?"}}'
109
+ cat ticket.txt | cohub decision -q questions.json --json
110
+ cohub decision "Duplicate charge?" --image receipt.png -q questions.json
111
+ cohub decision --input request.json`)
112
+ .action((words, opts) => runDecisionCommand(parent, words, opts));
113
+ }
@@ -3,10 +3,12 @@ import { basename, dirname, relative, resolve, sep } from "node:path";
3
3
  import { readSentTurns, readSessionTurnOrigin, resolveCohubEnvironment } from "@neta-art/cohub";
4
4
  import { uploadAvatarAsset, uploadChatImageAsset } from "../avatar.js";
5
5
  import { createClient } from "../client.js";
6
+ import { collectOption, readPromptContent } from "../command-input.js";
6
7
  import { table, json as outJson, jsonRequested, ok, error, handleHttp, formatEpochMs, truncateText } from "../output.js";
7
8
  import { SESSION_STATUS_COLUMN } from "../session-status.js";
8
9
  import { resolveSpace } from "../space.js";
9
10
  import { uploadLocalFiles } from "../space-files/upload.js";
11
+ import { registerDecisionCommand } from "./decision.js";
10
12
  import { registerSpaceCommerce } from "./space-commerce.js";
11
13
  import { registerSpaceFileTransfer } from "./space-files.js";
12
14
  import { registerSpaceActivity } from "./space-activity.js";
@@ -33,9 +35,6 @@ function parseInteger(value, name, options = {}) {
33
35
  return error(`Invalid ${name}`, `${name} must be at most ${options.max}`);
34
36
  return parsed;
35
37
  }
36
- function collectOption(value, previous = []) {
37
- return [...previous, value];
38
- }
39
38
  function parseEnvOptions(values) {
40
39
  if (!values?.length)
41
40
  return undefined;
@@ -181,18 +180,6 @@ async function confirmRestart(opts) {
181
180
  if (answer !== "y" && answer !== "yes")
182
181
  return error("Cancelled");
183
182
  }
184
- async function readPromptContent(words, options = {}) {
185
- let content = words.join(" ");
186
- if (!content && !process.stdin.isTTY) {
187
- const chunks = [];
188
- for await (const chunk of process.stdin)
189
- chunks.push(chunk);
190
- content = Buffer.concat(chunks).toString().trim();
191
- }
192
- if (!content && !options.allowEmpty)
193
- return error("No content", "Pass as argument or pipe via stdin");
194
- return content;
195
- }
196
183
  async function sendPrompt(command, words, opts) {
197
184
  const content = await readPromptContent(words, { allowEmpty: Boolean(opts.image?.length) });
198
185
  const scheduleFlags = [opts.delayMs, opts.at, opts.cron].filter((value) => value !== undefined);
@@ -379,6 +366,7 @@ export function registerPrompt(program) {
379
366
  .option("--thinking-level <level>", "Thinking level: off|minimal|low|medium|high|xhigh")
380
367
  .option("--json", "Output as JSON")
381
368
  .action((words, opts) => runCompletionCommand(program, words, opts));
369
+ registerDecisionCommand(program);
382
370
  }
383
371
  export function registerSpaceCreate(spacesCmd, dependencies = {}) {
384
372
  const getClient = dependencies.createClient ?? createClient;
@@ -699,6 +687,7 @@ export function registerSpaces(program) {
699
687
  .option("--thinking-level <level>", "Thinking level: off|minimal|low|medium|high|xhigh")
700
688
  .option("--json", "Output as JSON")
701
689
  .action((words, opts) => runCompletionCommand(spacesCmd, words, opts));
690
+ registerDecisionCommand(spacesCmd);
702
691
  // ── spaces files ──
703
692
  registerFiles(spacesCmd);
704
693
  registerSpaceDisplays(spacesCmd);
package/dist/index.js CHANGED
@@ -54,6 +54,7 @@ Common commands:
54
54
  cohub spaces ls
55
55
  cohub prompt "Fix the failing tests"
56
56
  cohub completion "Summarize AGENTS.md" --system-prompt AGENTS.md --stream
57
+ cohub decision "I was charged twice" -q questions.json
57
58
  cohub run -- git status
58
59
  cohub runtime up ./my-project
59
60
  cohub runtime logs --follow
@@ -74,7 +75,7 @@ Common commands:
74
75
  Target space:
75
76
  -s <space>, then COHUB_SPACE_ID, then the current directory Runtime binding.
76
77
  <space> is an ID, a slug you own such as home, or username/slug.
77
- prompt, completion, generate, apps, and public fall back to your Home Space;
78
+ prompt, completion, decision, generate, apps, and public fall back to your Home Space;
78
79
  other Space commands need a target.
79
80
 
80
81
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "10.1.0",
3
+ "version": "10.2.0",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -18,8 +18,8 @@
18
18
  "NOTICE"
19
19
  ],
20
20
  "dependencies": {
21
- "@neta-art/cohub": "11.1.0",
22
- "@neta-art/generation": "^0.1.33",
21
+ "@neta-art/cohub": "11.2.0",
22
+ "@neta-art/generation": "^0.1.35",
23
23
  "commander": "^15.0.0",
24
24
  "sharp": "^0.35.4",
25
25
  "ws": "^8.21.3"