@neta-art/cohub-cli 9.1.1 → 9.1.2

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.
@@ -1,16 +1,16 @@
1
1
  import type { BoardDocument, BoardExportRegion, BoardSceneItem } from "@neta-art/cohub/board";
2
- import type { BoardHeadlessExportFormat, BoardHeadlessFont, BoardHeadlessRenderer, BoardHeadlessTexture } from "@neta-art/cohub/board/headless";
3
- export declare function resolveBundledFonts(): BoardHeadlessFont[];
2
+ import type { BoardNodeExportFormat, BoardNodeFont, BoardNodeRenderer, BoardNodeTexture } from "@neta-art/cohub/board/export/node";
3
+ export declare function resolveBundledFonts(): BoardNodeFont[];
4
4
  export type BoardExportSource = {
5
5
  document: BoardDocument;
6
6
  boardId: string;
7
7
  title: string | null;
8
8
  };
9
9
  export declare function loadBoardDocument(spaceId: string, target: string): Promise<BoardExportSource>;
10
- export declare function loadBoardTextures(headless: BoardHeadlessRenderer, spaceId: string, items: readonly BoardSceneItem[], options?: {
10
+ export declare function loadBoardTextures(renderer: BoardNodeRenderer, spaceId: string, items: readonly BoardSceneItem[], options?: {
11
11
  concurrency?: number;
12
12
  }): Promise<{
13
- textures: Map<string, BoardHeadlessTexture>;
13
+ textures: Map<string, BoardNodeTexture>;
14
14
  failed: string[];
15
15
  omitted: string[];
16
16
  }>;
@@ -24,7 +24,7 @@ export type BoardExportRunOptions = {
24
24
  padding?: number;
25
25
  colorScheme: "dark" | "light";
26
26
  background: "paper" | "transparent";
27
- format: BoardHeadlessExportFormat;
27
+ format: BoardNodeExportFormat;
28
28
  quality?: number;
29
29
  withImages: boolean;
30
30
  };
@@ -35,7 +35,7 @@ export type BoardExportFrame = {
35
35
  height: number;
36
36
  scale: number;
37
37
  itemCount: number;
38
- format: BoardHeadlessExportFormat;
38
+ format: BoardNodeExportFormat;
39
39
  };
40
40
  export type BoardExportRunResult = {
41
41
  frames: BoardExportFrame[];
@@ -7,7 +7,7 @@ import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { tmpdir } from "node:os";
9
9
  const execFile = promisify(execFileCallback);
10
- import { boardDocumentAt, boardImageKeySource, buildBoardScene, createBoardHeadlessRenderer, createBoardHeadlessSketchHost, exportBoardImageBytes, imageAssetKey, parseBoardDocument, planBoardExport, selectBoardExportAssets, } from "./board-kernel.js";
10
+ import { boardDocumentAt, boardImageKeySource, buildBoardScene, createNodeBoardRenderer, createNodeBoardSketchHost, exportBoardImageBytes, imageAssetKey, parseBoardDocument, planBoardExport, selectBoardExportAssets, } from "./board-kernel.js";
11
11
  import { resolveBoardId } from "./board-command-support.js";
12
12
  import { BOARD_EXPORT_FONTS } from "./board-fonts.js";
13
13
  import { createClient } from "./client.js";
@@ -45,7 +45,7 @@ export async function loadBoardDocument(spaceId, target) {
45
45
  throw new Error(`Board ${result.id} could not be read: ${parsed.diagnostics[0]?.path}: ${parsed.diagnostics[0]?.message}`);
46
46
  return { document: parsed.document, boardId: result.id, title: result.title };
47
47
  }
48
- export async function loadBoardTextures(headless, spaceId, items, options = {}) {
48
+ export async function loadBoardTextures(renderer, spaceId, items, options = {}) {
49
49
  const selection = selectBoardExportAssets([...items], imageAssetKey);
50
50
  const textures = new Map();
51
51
  const failed = [];
@@ -66,7 +66,7 @@ export async function loadBoardTextures(headless, spaceId, items, options = {})
66
66
  const { bytes, mimeType } = source.kind === "file"
67
67
  ? await readSpaceFileBytes(client, spaceId, source.value)
68
68
  : await downloadPublicImage(source.value);
69
- const texture = await headless.decodeImage(bytes, mimeType);
69
+ const texture = await renderer.decodeImage(bytes, mimeType);
70
70
  textures.set(key, texture);
71
71
  }
72
72
  catch {
@@ -161,7 +161,7 @@ export async function runBoardExport(options) {
161
161
  });
162
162
  if (!plan)
163
163
  return null;
164
- const headless = await createBoardHeadlessRenderer({ fonts: resolveBundledFonts() });
164
+ const renderer = await createNodeBoardRenderer({ fonts: resolveBundledFonts() });
165
165
  let sketchHost = null;
166
166
  try {
167
167
  const warnings = [];
@@ -169,7 +169,7 @@ export async function runBoardExport(options) {
169
169
  let backgroundTexture;
170
170
  let omittedKeys = new Set();
171
171
  if (options.withImages) {
172
- const loaded = await loadBoardTextures(headless, options.spaceId, plan.items);
172
+ const loaded = await loadBoardTextures(renderer, options.spaceId, plan.items);
173
173
  textures = loaded.textures;
174
174
  omittedKeys = new Set(loaded.omitted);
175
175
  if (loaded.failed.length > 0) {
@@ -186,14 +186,14 @@ export async function runBoardExport(options) {
186
186
  declaredBackground.imageUrl) {
187
187
  try {
188
188
  const { bytes, mimeType } = await downloadPublicImage(declaredBackground.imageUrl);
189
- backgroundTexture = await headless.decodeImage(bytes, mimeType);
189
+ backgroundTexture = await renderer.decodeImage(bytes, mimeType);
190
190
  }
191
191
  catch {
192
192
  warnings.push("The board background image could not be loaded; the fallback color was exported.");
193
193
  }
194
194
  }
195
195
  const videoCount = plan.items.filter((item) => item.type === "video").length;
196
- sketchHost = createBoardHeadlessSketchHost(headless, {
196
+ sketchHost = createNodeBoardSketchHost(renderer, {
197
197
  readModule: async (src) => {
198
198
  const { bytes } = await readSpaceFileBytes(createClient(), options.spaceId, src);
199
199
  return new TextDecoder().decode(bytes);
@@ -202,7 +202,7 @@ export async function runBoardExport(options) {
202
202
  const region = times.length > 1 ? { kind: "rect", rect: plan.world } : options.region;
203
203
  const host = sketchHost;
204
204
  if (!host)
205
- throw new Error("Headless sketch host could not be created.");
205
+ throw new Error("Node sketch host could not be created.");
206
206
  const sketchVariants = new Map();
207
207
  for (const time of times) {
208
208
  const sketchPlan = planBoardExport({
@@ -220,7 +220,7 @@ export async function runBoardExport(options) {
220
220
  }
221
221
  await Promise.all([...sketchVariants.values()].map((variant) => host.prepare([variant.item], variant.times, plan.scale)));
222
222
  if (videoCount > 0) {
223
- warnings.push(`${videoCount} video preview${videoCount === 1 ? " was" : "s were"} drawn as placeholders; headless video decoding is unavailable.`);
223
+ warnings.push(`${videoCount} video preview${videoCount === 1 ? " was" : "s were"} drawn as placeholders; video decoding is unavailable in Node.`);
224
224
  }
225
225
  const backgroundImage = backgroundTexture
226
226
  ? { texture: backgroundTexture, fit: declaredBackground.fit ?? "cover", position: "center", opacity: declaredBackground.opacity ?? 1 }
@@ -234,7 +234,7 @@ export async function runBoardExport(options) {
234
234
  const frames = [];
235
235
  const reported = new Set();
236
236
  for (const time of times) {
237
- const result = exportBoardImageBytes(headless, document, {
237
+ const result = exportBoardImageBytes(renderer, document, {
238
238
  ...(time === null ? {} : { at: { ...(options.animation ? { animation: options.animation } : {}), time } }),
239
239
  region,
240
240
  scale: options.scale,
@@ -272,7 +272,7 @@ export async function runBoardExport(options) {
272
272
  }
273
273
  finally {
274
274
  sketchHost?.destroy();
275
- headless.destroy();
275
+ renderer.destroy();
276
276
  }
277
277
  }
278
278
  function describeWarning(warning) {
@@ -1,2 +1,2 @@
1
1
  export { boardDocumentAt, boardImageKeySource, buildBoardScene, imageAssetKey, parseBoardDocument, planBoardExport, selectBoardExportAssets, } from "@neta-art/cohub/board";
2
- export { createBoardHeadlessRenderer, createBoardHeadlessSketchHost, exportBoardImageBytes } from "@neta-art/cohub/board/headless";
2
+ export { createNodeBoardRenderer, createNodeBoardSketchHost, exportBoardImageBytes } from "@neta-art/cohub/board/export/node";
@@ -7119,7 +7119,7 @@ function parseBoardDocument(input) {
7119
7119
  } : result;
7120
7120
  }
7121
7121
  //#endregion
7122
- //#region ../sdk/src/board/animation.ts
7122
+ //#region ../sdk/src/board/model/animation.ts
7123
7123
  const linear = (x) => x;
7124
7124
  function cubicBezier(x1, y1, x2, y2) {
7125
7125
  const cx = 3 * x1;
@@ -7519,7 +7519,7 @@ function boardDocumentAt(document, animationId, time, options = {}) {
7519
7519
  };
7520
7520
  }
7521
7521
  //#endregion
7522
- //#region ../sdk/src/board/geometry.ts
7522
+ //#region ../sdk/src/board/model/geometry.ts
7523
7523
  function worldPoint(x, y) {
7524
7524
  return {
7525
7525
  x,
@@ -7862,7 +7862,7 @@ const { PI: e } = Math;
7862
7862
  e + 1e-4;
7863
7863
  const { min: a } = Math;
7864
7864
  //#endregion
7865
- //#region ../sdk/src/board/core/draw-geometry.ts
7865
+ //#region ../sdk/src/board/model/draw-geometry.ts
7866
7866
  const sampleRadius = drawSampleRadius;
7867
7867
  const computeDrawBounds = drawPointsBounds;
7868
7868
  function trimDrawPoints(points, trim) {
@@ -8038,7 +8038,7 @@ function selectBoardExportAssets(items, assetKey, maxTextures = 64) {
8038
8038
  };
8039
8039
  }
8040
8040
  //#endregion
8041
- //#region ../sdk/src/board/core/export-plan.ts
8041
+ //#region ../sdk/src/board/model/export-plan.ts
8042
8042
  function normalizeBoardDocument(document) {
8043
8043
  const parsed = parseBoardDocument(document);
8044
8044
  if (!parsed.ok) throw new Error(`Invalid Board document: ${parsed.diagnostics.map((entry) => `${entry.path}: ${entry.message}`).join("; ")}`);
@@ -8152,7 +8152,7 @@ function planBoardExport(input) {
8152
8152
  };
8153
8153
  }
8154
8154
  //#endregion
8155
- //#region ../sdk/src/board/core/file-snapshot.ts
8155
+ //#region ../sdk/src/board/model/file-snapshot.ts
8156
8156
  const DOC_EXTENSIONS = /* @__PURE__ */ new Set([
8157
8157
  "md",
8158
8158
  "mdx",
@@ -8280,7 +8280,7 @@ function fileStem(path) {
8280
8280
  return name.slice(0, dot);
8281
8281
  }
8282
8282
  //#endregion
8283
- //#region ../sdk/src/board/core/file-preview.ts
8283
+ //#region ../sdk/src/board/model/file-preview.ts
8284
8284
  /**
8285
8285
  * Which tier a card renders at. Derived rather than stored: presentation follows
8286
8286
  * from the facts present, so there is no second piece of state to fall out of
@@ -8310,7 +8310,7 @@ function fileCategoryAccent(category, palette) {
8310
8310
  }
8311
8311
  }
8312
8312
  //#endregion
8313
- //#region ../sdk/src/board/core/palette.ts
8313
+ //#region ../sdk/src/board/model/palette.ts
8314
8314
  const LABEL_DARK = 16053492;
8315
8315
  const LABEL_LIGHT = 1579035;
8316
8316
  const BOARD_COLORS = [
@@ -8467,7 +8467,7 @@ function resolveItemColor(color, fallback, colors, mode, parseCss, part = "strok
8467
8467
  return parseCss(value) ?? pickBoardColor(colors, fallback, mode)[part];
8468
8468
  }
8469
8469
  //#endregion
8470
- //#region ../sdk/src/board/core/scene.ts
8470
+ //#region ../sdk/src/board/model/scene.ts
8471
8471
  function orderBoardItems(items) {
8472
8472
  const children = /* @__PURE__ */ new Map();
8473
8473
  for (const [id, item] of Object.entries(items)) {
@@ -8575,13 +8575,13 @@ function toSceneItem(id, item, layout) {
8575
8575
  return scene;
8576
8576
  }
8577
8577
  //#endregion
8578
- //#region ../sdk/src/board/core/text-metrics.ts
8578
+ //#region ../sdk/src/board/model/text-metrics.ts
8579
8579
  function boardFontFamily(font, stacks) {
8580
8580
  if (font === "sans" || font === "serif" || font === "mono") return stacks[font];
8581
8581
  return `"${font.replaceAll("\"", "")}", ${stacks.sans}`;
8582
8582
  }
8583
8583
  //#endregion
8584
- //#region ../sdk/src/board/task.ts
8584
+ //#region ../sdk/src/board/model/task.ts
8585
8585
  function artifactScore(artifact) {
8586
8586
  const kind = {
8587
8587
  text: 1,
@@ -8617,7 +8617,7 @@ function taskArtifactPreviewUrl(artifact) {
8617
8617
  if (artifact?.type === "video" || artifact?.type === "audio") return artifact.previewUrl;
8618
8618
  }
8619
8619
  //#endregion
8620
- //#region ../sdk/src/board/image-key.ts
8620
+ //#region ../sdk/src/board/model/image-key.ts
8621
8621
  function imageAssetKey(item) {
8622
8622
  if (item.type === "image") return `file:${item.props.src}`;
8623
8623
  if (item.type === "file") {
@@ -28481,7 +28481,7 @@ function sync(container, item, context) {
28481
28481
  }
28482
28482
  //#endregion
28483
28483
  //#region ../sdk/src/board/render/renderers/board-renderer-registry.ts
28484
- const boardCardRenderers = [
28484
+ const builtinCardRenderers = [
28485
28485
  textCardRenderer,
28486
28486
  imageCardRenderer,
28487
28487
  {
@@ -28546,13 +28546,21 @@ const boardCardRenderers = [
28546
28546
  arrowCardRenderer,
28547
28547
  frameCardRenderer,
28548
28548
  effectCardRenderer,
28549
- sketchCardRenderer,
28550
- unknownCardRenderer
28549
+ sketchCardRenderer
28551
28550
  ];
28552
- function getBoardCardRenderer(item, context) {
28553
- ensureBoardTextMeasurement();
28554
- return boardCardRenderers.find((renderer) => renderer.canRender(item, context)) ?? unknownCardRenderer;
28551
+ /**
28552
+ * Pick a renderer per item: `renderers` first, then the built-ins. Items no
28553
+ * renderer claims draw as a labelled placeholder box.
28554
+ */
28555
+ function createBoardCardRendererResolver(renderers = []) {
28556
+ const candidates = [...renderers, ...builtinCardRenderers];
28557
+ return (item, context) => {
28558
+ ensureBoardTextMeasurement();
28559
+ return candidates.find((renderer) => renderer.canRender(item, context)) ?? unknownCardRenderer;
28560
+ };
28555
28561
  }
28562
+ /** The built-in renderers alone. */
28563
+ const getBoardCardRenderer = createBoardCardRendererResolver();
28556
28564
  //#endregion
28557
28565
  //#region ../sdk/src/board/export/scene.ts
28558
28566
  function buildContext(input) {
@@ -28625,11 +28633,12 @@ function createBoardExportScene(input) {
28625
28633
  });
28626
28634
  world.scale.set(input.scale);
28627
28635
  world.position.set(-input.world.x * input.scale, -input.world.y * input.scale);
28636
+ const resolve = input.renderers ?? getBoardCardRenderer;
28628
28637
  const groups = /* @__PURE__ */ new Map();
28629
28638
  for (const item of input.items) {
28630
28639
  const clip = clippingAncestor(input.scene, item);
28631
28640
  const host = clip && groups.get(clip) || world;
28632
- const container = getBoardCardRenderer(item, context).create(item, context);
28641
+ const container = resolve(item, context).create(item, context);
28633
28642
  container.alpha = input.scene.opacity(item.id);
28634
28643
  host.addChild(container);
28635
28644
  if (isClippingFrame(item)) {
@@ -28686,6 +28695,7 @@ function renderBoardExport(renderer, input, options = {}) {
28686
28695
  textures: options.textures,
28687
28696
  sketches: options.sketches,
28688
28697
  assetKey: options.assetKey,
28698
+ renderers: options.renderers,
28689
28699
  background: resolveBackground(options.background, palette, document, colorScheme),
28690
28700
  backgroundImage: options.backgroundImage
28691
28701
  });
@@ -28721,7 +28731,7 @@ function renderBoardExport(renderer, input, options = {}) {
28721
28731
  }
28722
28732
  }
28723
28733
  //#endregion
28724
- //#region ../sdk/src/board/headless/sketch.ts
28734
+ //#region ../sdk/src/board/export/node/sketch.ts
28725
28735
  const MAX_EDGE = 2048;
28726
28736
  const DEFAULT_TIMEOUT_MS = 2e3;
28727
28737
  const DEFAULT_READ_TIMEOUT_MS = 15e3;
@@ -28755,7 +28765,7 @@ function runModule(code, src, timeoutMs) {
28755
28765
  ...module.exports
28756
28766
  } : module.exports;
28757
28767
  if (typeof value.draw !== "function") throw new Error(`${src} does not export draw(ctx, frame)`);
28758
- if (value.draw.constructor?.name === "AsyncFunction") throw new Error(`${src} must export a synchronous draw(ctx, frame) for headless export.`);
28768
+ if (value.draw.constructor?.name === "AsyncFunction") throw new Error(`${src} must export a synchronous draw(ctx, frame) for Node export.`);
28759
28769
  return {
28760
28770
  module: value,
28761
28771
  context
@@ -28780,7 +28790,7 @@ function textureFromCanvas(canvas) {
28780
28790
  height: canvas.height
28781
28791
  }) });
28782
28792
  }
28783
- function createBoardHeadlessSketchHost(headless, options) {
28793
+ function createNodeBoardSketchHost(renderer, options) {
28784
28794
  const textures = /* @__PURE__ */ new Map();
28785
28795
  const errors = /* @__PURE__ */ new Map();
28786
28796
  const lastErrors = /* @__PURE__ */ new Map();
@@ -28803,7 +28813,7 @@ function createBoardHeadlessSketchHost(headless, options) {
28803
28813
  const runtime = await loadModule(item.props.src);
28804
28814
  const width = Math.max(1, Math.round(item.frame.width * scale));
28805
28815
  const height = Math.max(1, Math.round(item.frame.height * scale));
28806
- const canvas = headless.canvasModule.createCanvas(width, height);
28816
+ const canvas = renderer.canvasModule.createCanvas(width, height);
28807
28817
  const context = canvas.getContext("2d");
28808
28818
  context.scale(scale, scale);
28809
28819
  const frame = {
@@ -28818,7 +28828,7 @@ function createBoardHeadlessSketchHost(headless, options) {
28818
28828
  runtime.context.__boardFrame = frame;
28819
28829
  try {
28820
28830
  const drawResult = new Script("__boardDraw(__boardDrawContext, __boardFrame)", { filename: item.props.src }).runInContext(runtime.context, { timeout: timeoutMs });
28821
- if (drawResult && typeof drawResult.then === "function") throw new Error(`${item.props.src} must export a synchronous draw(ctx, frame) for headless export.`);
28831
+ if (drawResult && typeof drawResult.then === "function") throw new Error(`${item.props.src} must export a synchronous draw(ctx, frame) for Node export.`);
28822
28832
  } finally {
28823
28833
  delete runtime.context.__boardDraw;
28824
28834
  delete runtime.context.__boardDrawContext;
@@ -28864,7 +28874,7 @@ function createBoardHeadlessSketchHost(headless, options) {
28864
28874
  };
28865
28875
  }
28866
28876
  //#endregion
28867
- //#region ../sdk/src/board/headless/index.ts
28877
+ //#region ../sdk/src/board/export/node/index.ts
28868
28878
  const SYSTEM_UI_FAMILIES = [
28869
28879
  "Noto Sans",
28870
28880
  "DejaVu Sans",
@@ -28922,7 +28932,7 @@ function installAnimationFrameShim() {
28922
28932
  globals.cancelAnimationFrame = void 0;
28923
28933
  };
28924
28934
  }
28925
- async function createBoardHeadlessRenderer(options = {}) {
28935
+ async function createNodeBoardRenderer(options = {}) {
28926
28936
  const canvasModule = options.canvasModule ?? await loadCanvasModule();
28927
28937
  extensions.add(CanvasGraphicsPipe, GraphicsPipe, CanvasGraphicsContextSystem, GraphicsContextSystem, CanvasRendererTextSystem, CanvasTextSystem, CanvasTextPipe);
28928
28938
  for (const font of options.fonts ?? []) canvasModule.GlobalFonts.registerFromPath(font.path, font.family ?? "Geist");
@@ -28939,14 +28949,14 @@ async function createBoardHeadlessRenderer(options = {}) {
28939
28949
  getCanvasRenderingContext2D: () => createCanvas(1, 1).getContext("2d").constructor,
28940
28950
  getWebGLRenderingContext: () => WEBGL_PROBE_STUB,
28941
28951
  getNavigator: () => ({
28942
- userAgent: "cohub-board-headless",
28952
+ userAgent: "cohub-board-node",
28943
28953
  gpu: null
28944
28954
  }),
28945
28955
  getBaseUrl: () => "file://",
28946
28956
  getFontFaceSet: () => null,
28947
28957
  fetch: (url, init) => fetch(url, init),
28948
28958
  parseXML: () => {
28949
- throw new Error("XML parsing is not available in headless board export.");
28959
+ throw new Error("XML parsing is not available in Node board export.");
28950
28960
  }
28951
28961
  });
28952
28962
  installBoardTextMeasurement();
@@ -28988,9 +28998,9 @@ const MIME = {
28988
28998
  jpeg: "image/jpeg",
28989
28999
  webp: "image/webp"
28990
29000
  };
28991
- function exportBoardImageBytes(headless, document, options = {}) {
29001
+ function exportBoardImageBytes(renderer, document, options = {}) {
28992
29002
  const { format = "png", quality = .92, textures, sketches, backgroundImage, ...rest } = options;
28993
- const result = renderBoardExport(headless.renderer, document, {
29003
+ const result = renderBoardExport(renderer.renderer, document, {
28994
29004
  ...rest,
28995
29005
  ...sketches ? { sketches } : {},
28996
29006
  ...textures ? { textures } : {},
@@ -29010,4 +29020,4 @@ function exportBoardImageBytes(headless, document, options = {}) {
29010
29020
  };
29011
29021
  }
29012
29022
  //#endregion
29013
- export { boardDocumentAt, boardImageKeySource, buildBoardScene, createBoardHeadlessRenderer, createBoardHeadlessSketchHost, exportBoardImageBytes, imageAssetKey, parseBoardDocument, planBoardExport, selectBoardExportAssets };
29023
+ export { boardDocumentAt, boardImageKeySource, buildBoardScene, createNodeBoardRenderer, createNodeBoardSketchHost, exportBoardImageBytes, imageAssetKey, parseBoardDocument, planBoardExport, selectBoardExportAssets };
@@ -1,5 +1,6 @@
1
1
  import { createClient } from "../client.js";
2
2
  import { formatLocalDateTime, handleHttp, json as outJson, jsonRequested, table } from "../output.js";
3
+ import { SESSION_STATUS_COLUMN } from "../session-status.js";
3
4
  function parseInteger(value, name, min) {
4
5
  const parsed = Number.parseInt(value, 10);
5
6
  if (!Number.isSafeInteger(parsed) || parsed < min) {
@@ -34,6 +35,7 @@ export function registerMe(program) {
34
35
  { key: "id", label: "ID" },
35
36
  { key: "spaceId", label: "Space" },
36
37
  { key: "title", label: "Title" },
38
+ SESSION_STATUS_COLUMN,
37
39
  { key: "totalMessages", label: "Messages" },
38
40
  { key: "createdAt", label: "Created" },
39
41
  ]);
@@ -4,6 +4,7 @@ import { readSentTurns, readSessionTurnOrigin, resolveCohubEnvironment } from "@
4
4
  import { uploadAvatarAsset, uploadChatImageAsset } from "../avatar.js";
5
5
  import { createClient } from "../client.js";
6
6
  import { table, json as outJson, jsonRequested, ok, error, handleHttp, formatEpochMs, truncateText } from "../output.js";
7
+ import { SESSION_STATUS_COLUMN } from "../session-status.js";
7
8
  import { resolveSpace } from "../space.js";
8
9
  import { uploadLocalFiles } from "../space-files/upload.js";
9
10
  import { registerSpaceCommerce } from "./space-commerce.js";
@@ -1329,6 +1330,7 @@ function registerSessions(spacesCmd) {
1329
1330
  table(result.sessions, [
1330
1331
  { key: "id", label: "ID" },
1331
1332
  { key: "title", label: "Title" },
1333
+ SESSION_STATUS_COLUMN,
1332
1334
  { key: "totalMessages", label: "Messages" },
1333
1335
  { key: "createdAt", label: "Created" },
1334
1336
  ]);
@@ -1376,6 +1378,7 @@ function registerSessions(spacesCmd) {
1376
1378
  table([result.session], [
1377
1379
  { key: "id", label: "ID" },
1378
1380
  { key: "title", label: "Title" },
1381
+ SESSION_STATUS_COLUMN,
1379
1382
  { key: "totalMessages", label: "Messages" },
1380
1383
  { key: "totalToolCalls", label: "Tool Calls" },
1381
1384
  { key: "createdAt", label: "Created" },
@@ -2,7 +2,7 @@ import { access, stat } from "node:fs/promises";
2
2
  import { constants } from "node:fs";
3
3
  import { delimiter, join, resolve, win32 } from "node:path";
4
4
  import { JsonRpcProcess, record } from "./json-rpc.js";
5
- import { codexModelCatalog } from "./model-catalog.js";
5
+ import { codexModelCatalog, piModelCatalog } from "./model-catalog.js";
6
6
  import { downloadPublicImage } from "../safe-remote-image.js";
7
7
  export function harnessExecutableCandidates(binary, cwd, path, platform = process.platform, pathExt = process.env.PATHEXT) {
8
8
  const windows = platform === "win32";
@@ -68,11 +68,8 @@ export async function discoverHarnesses(harnesses, options, cwd) {
68
68
  try {
69
69
  if (harness === "pi") {
70
70
  const result = await rpc.request("get_available_models");
71
- for (const value of array(result.models)) {
72
- const model = record(value);
73
- if (model.id && model.provider)
74
- models.push({ harness, id: text(model.id), provider: text(model.provider), name: text(model.name) || text(model.id) });
75
- }
71
+ const state = await rpc.request("get_state");
72
+ models.push(...piModelCatalog(array(result.models), state));
76
73
  }
77
74
  else {
78
75
  await initializeCodex(rpc);
@@ -1,6 +1,14 @@
1
1
  import type { RuntimeCapabilities } from "@neta-art/cohub";
2
2
  import { type JsonRecord } from "./json-rpc.js";
3
3
  type Model = RuntimeCapabilities["models"][number];
4
+ /**
5
+ * Publish only picker metadata, never provider credentials or request configuration.
6
+ * `state` is a fresh Pi's `get_state`: its model is what Pi runs when Cohub requests
7
+ * nothing, and its level is resolved and clamped for that model only. Pi's levels for
8
+ * other models live in its settings, which RPC does not expose; those models fall back
9
+ * to the picker's rule, and Cohub always sends the chosen level explicitly.
10
+ */
11
+ export declare function piModelCatalog(entries: unknown[], state: JsonRecord): Model[];
4
12
  /** Codex's built-in catalog is not authoritative for an arbitrary custom provider. */
5
13
  export declare function codexModelCatalog(configResponse: JsonRecord, entries: unknown[]): Model[];
6
14
  export {};
@@ -1,4 +1,50 @@
1
1
  import { record } from "./json-rpc.js";
2
+ const thinkingLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
3
+ const thinkingLevel = (value) => thinkingLevels.includes(value) ? value : undefined;
4
+ /**
5
+ * Publish only picker metadata, never provider credentials or request configuration.
6
+ * `state` is a fresh Pi's `get_state`: its model is what Pi runs when Cohub requests
7
+ * nothing, and its level is resolved and clamped for that model only. Pi's levels for
8
+ * other models live in its settings, which RPC does not expose; those models fall back
9
+ * to the picker's rule, and Cohub always sends the chosen level explicitly.
10
+ */
11
+ export function piModelCatalog(entries, state) {
12
+ const current = record(state.model);
13
+ const currentLevel = thinkingLevel(state.thinkingLevel);
14
+ return entries.flatMap((value) => {
15
+ const model = record(value);
16
+ if (typeof model.id !== "string" || !model.id || typeof model.provider !== "string" || !model.provider)
17
+ return [];
18
+ const map = record(model.thinkingLevelMap);
19
+ const thinkingLevelMap = Object.fromEntries(thinkingLevels.flatMap((level) => map[level] === null || typeof map[level] === "string" ? [[level, map[level]]] : []));
20
+ const isDefault = current.provider === model.provider && current.id === model.id;
21
+ return [{
22
+ harness: "pi", id: model.id, provider: model.provider, name: typeof model.name === "string" ? model.name : model.id,
23
+ ...(isDefault ? { isDefault } : {}),
24
+ ...(typeof model.reasoning === "boolean" ? { reasoning: model.reasoning } : {}),
25
+ ...(isDefault && currentLevel ? { defaultThinkingLevel: currentLevel } : {}),
26
+ ...(Object.keys(thinkingLevelMap).length ? { thinkingLevelMap } : {}),
27
+ }];
28
+ });
29
+ }
30
+ function codexThinking(model, configuredEffort) {
31
+ if (!Array.isArray(model.supportedReasoningEfforts))
32
+ return {};
33
+ const efforts = new Set(model.supportedReasoningEfforts.map((entry) => record(entry).reasoningEffort));
34
+ const thinkingLevelMap = Object.fromEntries(thinkingLevels.map((level) => {
35
+ const effort = level === "off" ? "none" : level;
36
+ return [level, efforts.has(effort) ? effort : null];
37
+ }));
38
+ const normalize = (value) => thinkingLevel(value === "none" ? "off" : value);
39
+ const configured = normalize(configuredEffort);
40
+ const fallback = normalize(model.defaultReasoningEffort);
41
+ const defaultThinkingLevel = [configured, fallback].find((level) => level && thinkingLevelMap[level] != null);
42
+ return {
43
+ reasoning: thinkingLevels.some((level) => level !== "off" && thinkingLevelMap[level] != null),
44
+ thinkingLevelMap,
45
+ ...(defaultThinkingLevel ? { defaultThinkingLevel } : {}),
46
+ };
47
+ }
2
48
  /** Codex's built-in catalog is not authoritative for an arbitrary custom provider. */
3
49
  export function codexModelCatalog(configResponse, entries) {
4
50
  const config = record(configResponse.config);
@@ -11,9 +57,11 @@ export function codexModelCatalog(configResponse, entries) {
11
57
  const id = typeof model.model === "string" ? model.model : typeof model.id === "string" ? model.id : null;
12
58
  if (!id || model.hidden)
13
59
  return [];
14
- return [{ harness: "codex", provider, id, name: typeof model.displayName === "string" ? model.displayName : id }];
60
+ // A configured model replaces the catalog's own default.
61
+ const isDefault = configuredModel ? id === configuredModel : model.isDefault === true;
62
+ return [{ harness: "codex", provider, id, name: typeof model.displayName === "string" ? model.displayName : id, ...(isDefault ? { isDefault } : {}), ...codexThinking(model, config.model_reasoning_effort) }];
15
63
  });
16
64
  if (configuredModel && !models.some((model) => model.id === configuredModel))
17
- models.unshift({ harness: "codex", provider, id: configuredModel, name: configuredModel });
65
+ models.unshift({ harness: "codex", provider, id: configuredModel, name: configuredModel, isDefault: true });
18
66
  return models;
19
67
  }
@@ -294,7 +294,7 @@ async function runCodex(executor, turn, context) {
294
294
  escalation = setTimeout(() => void host?.close().catch(() => undefined), ABORT_ESCALATION_MS);
295
295
  };
296
296
  signal.addEventListener("abort", abort, { once: true });
297
- codex.request("turn/start", { threadId, clientUserMessageId: input.turnId, input: content, ...(input.thinkingLevel ? { effort: input.thinkingLevel } : {}) })
297
+ codex.request("turn/start", { threadId, clientUserMessageId: input.turnId, input: content, ...(input.thinkingLevel ? { effort: input.thinkingLevel === "off" ? "none" : input.thinkingLevel } : {}) })
298
298
  .then((result) => { nativeTurnId ??= text(record(result.turn).id) || null; if (signal.aborted)
299
299
  interrupt(); })
300
300
  .catch(finish);
@@ -0,0 +1,6 @@
1
+ import type { SessionRecord } from "@neta-art/cohub";
2
+ import type { Column } from "./output.js";
3
+ type SessionStatusFields = Pick<SessionRecord, "activeTurn" | "lastTurnIssue">;
4
+ export declare function sessionStatusText(session: Partial<SessionStatusFields>): string;
5
+ export declare const SESSION_STATUS_COLUMN: Column;
6
+ export {};
@@ -0,0 +1,14 @@
1
+ export function sessionStatusText(session) {
2
+ const turn = session.activeTurn;
3
+ if (turn)
4
+ return turn.status === "abort_requested" ? "stopping" : turn.status;
5
+ const issue = session.lastTurnIssue;
6
+ if (issue?.status === "failed")
7
+ return "failed";
8
+ return issue?.reason === "stale_active_recovered" ? "interrupted" : "";
9
+ }
10
+ export const SESSION_STATUS_COLUMN = {
11
+ key: "activeTurn",
12
+ label: "Status",
13
+ format: (_value, row) => sessionStatusText(row),
14
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "9.1.1",
3
+ "version": "9.1.2",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -18,7 +18,7 @@
18
18
  "NOTICE"
19
19
  ],
20
20
  "dependencies": {
21
- "@neta-art/cohub": "9.0.1",
21
+ "@neta-art/cohub": "10.0.0",
22
22
  "@neta-art/generation": "^0.1.30",
23
23
  "commander": "^15.0.0",
24
24
  "sharp": "^0.35.4",