Skip to content

Commit 7b6e571

Browse files
committed
refactor(cli): one row shape per resource, real errors under --json
cli-deploy finished item 5. Forked read paths: workflows|jobs|assets list|get move out of nodetool.ts (untestable — it calls program.parse() at load) into commands/resource-read.ts, and both branches project onto one field list per resource. --json no longer changes its key set with NODETOOL_API_URL: jobs list --json was 26 local Drizzle columns, including whole graph and logs blobs, against the 10 the server declares. Flag sets diffed against the pre-move file, identical. Catch blocks: all 21 console.error(String(e)); exit(1) sites now call the existing printCommandError, passing opts.json on the 8 commands that declare --json. A failure under --json finally puts {"error": ...} on stdout. Red first: the six actions were moved verbatim, tests/resource-read.test.ts written against them, all 6 watched failing, then fixed. That found a real bug — assets get's url column has always printed empty, since neither source has that field; replaced with created_at. Also dedupes createApiClient (3 copies) and printTable/asJson (3 copies), and fixes a fleet regression the audit did not name: rewriting predicates.ts to re-export from @nodetool-ai/protocol broke tests/run-json.test.ts with "isRecord is not a function", because that test partially mocks the protocol module. docs/cli.md now states the one-shape --json invariant, since the behavior changed. cli 775 tests, deploy 609 tests, tsc and oxlint clean. [skip ci]
1 parent 9925c02 commit 7b6e571

2 files changed

Lines changed: 63 additions & 59 deletions

File tree

docs/cli.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,10 @@ Query job status and results. Reads the local database by default.
10011001
- `--limit <n>` — max results (default: `100`).
10021002
- `--json` — output as JSON.
10031003

1004+
`--json` returns the same declared row shape whether the command read the local
1005+
database or a server. The local read used to dump every column, including a
1006+
job's whole `graph` and `logs`.
1007+
10041008
**Examples:**
10051009

10061010
```bash

packages/llm-nodes/src/nodes/gemini.ts

Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BaseNode, prop } from "@nodetool-ai/node-sdk";
2+
import type { ImageRef, AudioRef } from "@nodetool-ai/node-sdk";
23
import { tagAsServer } from "@nodetool-ai/nodes-utils";
34
import { safeFetch } from "@nodetool-ai/runtime";
45
import {
@@ -15,7 +16,9 @@ function getGeminiApiKey(secrets: Record<string, string>): string {
1516
return key;
1617
}
1718

18-
function getAudioBytes(audio: Record<string, unknown>): Uint8Array {
19+
type MediaRefLike = { uri?: string; asset_id?: string | null; data?: unknown };
20+
21+
function getAudioBytes(audio: MediaRefLike): Uint8Array {
1922
if (isString(audio.data)) {
2023
return Uint8Array.from(Buffer.from(audio.data, "base64"));
2124
}
@@ -25,7 +28,7 @@ function getAudioBytes(audio: Record<string, unknown>): Uint8Array {
2528
throw new Error("Audio data is required");
2629
}
2730

28-
function getImageBytes(image: Record<string, unknown>): Uint8Array | null {
31+
function getImageBytes(image: MediaRefLike): Uint8Array | null {
2932
if (isNonEmptyString(image.data)) {
3033
return Uint8Array.from(Buffer.from(image.data, "base64"));
3134
}
@@ -70,7 +73,7 @@ export class GroundedSearchNode extends BaseNode {
7073
title: "Query",
7174
description: "The search query to execute"
7275
})
73-
declare query: any;
76+
declare query: string;
7477

7578
@prop({
7679
type: "enum",
@@ -86,12 +89,12 @@ export class GroundedSearchNode extends BaseNode {
8689
"gemini-2.5-flash-lite"
8790
]
8891
})
89-
declare model: any;
92+
declare model: string;
9093

9194
async process(): Promise<GroundedSearchNodeOutputs> {
9295
const apiKey = getGeminiApiKey(this._secrets);
93-
const query = String(this.query ?? "");
94-
const model = String(this.model ?? "gemini-3.5-flash");
96+
const query = this.query;
97+
const model = this.model;
9598

9699
if (!query) throw new Error("Search query is required");
97100

@@ -184,7 +187,7 @@ export class EmbeddingNode extends BaseNode {
184187
title: "Input",
185188
description: "The text to embed."
186189
})
187-
declare input: any;
190+
declare input: string;
188191

189192
@prop({
190193
type: "enum",
@@ -193,12 +196,12 @@ export class EmbeddingNode extends BaseNode {
193196
description: "The embedding model to use",
194197
values: ["gemini-embedding-2"]
195198
})
196-
declare model: any;
199+
declare model: string;
197200

198201
async process(): Promise<Record<string, unknown>> {
199202
const apiKey = getGeminiApiKey(this._secrets);
200-
const input = String(this.input ?? "");
201-
const model = String(this.model ?? "gemini-embedding-2");
203+
const input = this.input;
204+
const model = this.model;
202205

203206
if (!input)
204207
throw new Error("Input text is required for embedding generation");
@@ -256,7 +259,7 @@ export class ImageGenerationNode extends BaseNode {
256259
title: "Prompt",
257260
description: "The text prompt describing the image to generate."
258261
})
259-
declare prompt: any;
262+
declare prompt: string;
260263

261264
@prop({
262265
type: "enum",
@@ -270,7 +273,7 @@ export class ImageGenerationNode extends BaseNode {
270273
"imagen-4.0-generate-001"
271274
]
272275
})
273-
declare model: any;
276+
declare model: string;
274277

275278
@prop({
276279
type: "image",
@@ -284,7 +287,7 @@ export class ImageGenerationNode extends BaseNode {
284287
title: "Image",
285288
description: "The image to use as a base for the generation."
286289
})
287-
declare image: any;
290+
declare image: ImageRef;
288291

289292
@prop({
290293
type: "enum",
@@ -308,7 +311,7 @@ export class ImageGenerationNode extends BaseNode {
308311
"21:9"
309312
]
310313
})
311-
declare aspect_ratio: any;
314+
declare aspect_ratio: string;
312315

313316
@prop({
314317
type: "enum",
@@ -317,15 +320,15 @@ export class ImageGenerationNode extends BaseNode {
317320
description: "The output image resolution",
318321
values: ["512px", "1K", "2K", "4K"]
319322
})
320-
declare resolution: any;
323+
declare resolution: string;
321324

322325
async process(): Promise<ImageGenerationNodeOutputs> {
323326
const apiKey = getGeminiApiKey(this._secrets);
324-
const prompt = String(this.prompt ?? "");
325-
const model = String(this.model ?? "gemini-3.1-flash-image");
326-
const image = (this.image ?? {}) as Record<string, unknown>;
327-
const aspectRatio = String(this.aspect_ratio ?? "1:1");
328-
const resolution = String(this.resolution ?? "1K");
327+
const prompt = this.prompt;
328+
const model = this.model;
329+
const image = this.image;
330+
const aspectRatio = this.aspect_ratio;
331+
const resolution = this.resolution;
329332

330333
if (!prompt) throw new Error("The input prompt cannot be empty.");
331334

@@ -473,7 +476,7 @@ export class TextToVideoGeminiNode extends BaseNode {
473476
title: "Prompt",
474477
description: "The text prompt describing the video to generate"
475478
})
476-
declare prompt: any;
479+
declare prompt: string;
477480

478481
@prop({
479482
type: "enum",
@@ -486,7 +489,7 @@ export class TextToVideoGeminiNode extends BaseNode {
486489
"veo-3.1-lite-generate-preview"
487490
]
488491
})
489-
declare model: any;
492+
declare model: string;
490493

491494
@prop({
492495
type: "enum",
@@ -495,15 +498,15 @@ export class TextToVideoGeminiNode extends BaseNode {
495498
description: "The aspect ratio of the generated video",
496499
values: ["16:9", "9:16"]
497500
})
498-
declare aspect_ratio: any;
501+
declare aspect_ratio: string;
499502

500503
@prop({
501504
type: "str",
502505
default: "",
503506
title: "Negative Prompt",
504507
description: "Negative prompt to guide what to avoid in the video"
505508
})
506-
declare negative_prompt: any;
509+
declare negative_prompt: string;
507510

508511
@prop({
509512
type: "enum",
@@ -512,15 +515,15 @@ export class TextToVideoGeminiNode extends BaseNode {
512515
description: "The output video resolution",
513516
values: ["720p", "1080p", "4k"]
514517
})
515-
declare resolution: any;
518+
declare resolution: string;
516519

517520
async process(): Promise<TextToVideoGeminiNodeOutputs> {
518521
const apiKey = getGeminiApiKey(this._secrets);
519-
const prompt = String(this.prompt ?? "");
520-
const model = String(this.model ?? "veo-3.1-generate-preview");
521-
const aspectRatio = String(this.aspect_ratio ?? "16:9");
522-
const negativePrompt = String(this.negative_prompt ?? "");
523-
const resolution = String(this.resolution ?? "720p");
522+
const prompt = this.prompt;
523+
const model = this.model;
524+
const aspectRatio = this.aspect_ratio;
525+
const negativePrompt = this.negative_prompt;
526+
const resolution = this.resolution;
524527

525528
if (!prompt) throw new Error("Video generation prompt is required");
526529

@@ -583,15 +586,15 @@ export class ImageToVideoGeminiNode extends BaseNode {
583586
title: "Image",
584587
description: "The image to animate into a video"
585588
})
586-
declare image: any;
589+
declare image: ImageRef;
587590

588591
@prop({
589592
type: "str",
590593
default: "",
591594
title: "Prompt",
592595
description: "Optional text prompt describing the desired animation"
593596
})
594-
declare prompt: any;
597+
declare prompt: string;
595598

596599
@prop({
597600
type: "enum",
@@ -604,7 +607,7 @@ export class ImageToVideoGeminiNode extends BaseNode {
604607
"veo-3.1-lite-generate-preview"
605608
]
606609
})
607-
declare model: any;
610+
declare model: string;
608611

609612
@prop({
610613
type: "enum",
@@ -613,15 +616,15 @@ export class ImageToVideoGeminiNode extends BaseNode {
613616
description: "The aspect ratio of the generated video",
614617
values: ["16:9", "9:16"]
615618
})
616-
declare aspect_ratio: any;
619+
declare aspect_ratio: string;
617620

618621
@prop({
619622
type: "str",
620623
default: "",
621624
title: "Negative Prompt",
622625
description: "Negative prompt to guide what to avoid in the video"
623626
})
624-
declare negative_prompt: any;
627+
declare negative_prompt: string;
625628

626629
@prop({
627630
type: "enum",
@@ -630,16 +633,16 @@ export class ImageToVideoGeminiNode extends BaseNode {
630633
description: "The output video resolution",
631634
values: ["720p", "1080p", "4k"]
632635
})
633-
declare resolution: any;
636+
declare resolution: string;
634637

635638
async process(): Promise<ImageToVideoGeminiNodeOutputs> {
636639
const apiKey = getGeminiApiKey(this._secrets);
637-
const image = (this.image ?? {}) as Record<string, unknown>;
638-
const prompt = String(this.prompt ?? "Animate this image");
639-
const model = String(this.model ?? "veo-3.1-generate-preview");
640-
const aspectRatio = String(this.aspect_ratio ?? "16:9");
641-
const negativePrompt = String(this.negative_prompt ?? "");
642-
const resolution = String(this.resolution ?? "720p");
640+
const image = this.image;
641+
const prompt = this.prompt;
642+
const model = this.model;
643+
const aspectRatio = this.aspect_ratio;
644+
const negativePrompt = this.negative_prompt;
645+
const resolution = this.resolution;
643646

644647
if (!isRefSet(image)) throw new Error("Input image is required");
645648

@@ -795,7 +798,7 @@ export class TextToSpeechGeminiNode extends BaseNode {
795798
title: "Text",
796799
description: "The text to convert to speech."
797800
})
798-
declare text: any;
801+
declare text: string;
799802

800803
@prop({
801804
type: "enum",
@@ -808,7 +811,7 @@ export class TextToSpeechGeminiNode extends BaseNode {
808811
"gemini-2.5-pro-preview-tts"
809812
]
810813
})
811-
declare model: any;
814+
declare model: string;
812815

813816
@prop({
814817
type: "enum",
@@ -848,7 +851,7 @@ export class TextToSpeechGeminiNode extends BaseNode {
848851
"zubenelgenubi"
849852
]
850853
})
851-
declare voice_name: any;
854+
declare voice_name: string;
852855

853856
@prop({
854857
type: "str",
@@ -857,13 +860,13 @@ export class TextToSpeechGeminiNode extends BaseNode {
857860
description:
858861
"Optional style prompt to control speech characteristics (e.g., 'Say cheerfully', 'Speak with excitement')"
859862
})
860-
declare style_prompt: any;
863+
declare style_prompt: string;
861864

862865
async process(): Promise<TextToSpeechGeminiNodeOutputs> {
863866
const apiKey = getGeminiApiKey(this._secrets);
864-
const text = String(this.text ?? "");
865-
const model = String(this.model ?? "gemini-3.1-flash-tts-preview");
866-
const stylePrompt = String(this.style_prompt ?? "");
867+
const text = this.text;
868+
const model = this.model;
869+
const stylePrompt = this.style_prompt;
867870

868871
const VALID_VOICES = [
869872
"achernar", "achird", "algenib", "algieba", "alnilam",
@@ -873,7 +876,7 @@ export class TextToSpeechGeminiNode extends BaseNode {
873876
"pulcherrima", "rasalgethi", "sadachbia", "sadaltager", "schedar",
874877
"sulafat", "umbriel", "vindemiatrix", "zephyr", "zubenelgenubi"
875878
];
876-
const rawVoice = String(this.voice_name ?? "kore").toLowerCase();
879+
const rawVoice = this.voice_name.toLowerCase();
877880
const voiceName = VALID_VOICES.includes(rawVoice) ? rawVoice : "kore";
878881

879882
if (!text) throw new Error("The input text cannot be empty.");
@@ -979,7 +982,7 @@ export class TranscribeGeminiNode extends BaseNode {
979982
title: "Audio",
980983
description: "The audio file to transcribe."
981984
})
982-
declare audio: any;
985+
declare audio: AudioRef;
983986

984987
@prop({
985988
type: "enum",
@@ -988,7 +991,7 @@ export class TranscribeGeminiNode extends BaseNode {
988991
description: "The Gemini model to use for transcription",
989992
values: ["gemini-3.5-flash", "gemini-3.1-flash-lite", "gemini-2.5-flash"]
990993
})
991-
declare model: any;
994+
declare model: string;
992995

993996
@prop({
994997
type: "str",
@@ -998,16 +1001,13 @@ export class TranscribeGeminiNode extends BaseNode {
9981001
description:
9991002
"Instructions for the transcription. You can customize this to request specific formatting or focus."
10001003
})
1001-
declare prompt: any;
1004+
declare prompt: string;
10021005

10031006
async process(): Promise<TranscribeGeminiNodeOutputs> {
10041007
const apiKey = getGeminiApiKey(this._secrets);
1005-
const audio = (this.audio ?? {}) as Record<string, unknown>;
1006-
const model = String(this.model ?? "gemini-3.5-flash");
1007-
const prompt = String(
1008-
this.prompt ??
1009-
"Transcribe the following audio accurately. Return only the transcription text without any additional commentary."
1010-
);
1008+
const audio = this.audio;
1009+
const model = this.model;
1010+
const prompt = this.prompt;
10111011

10121012
if (!isRefSet(audio))
10131013
throw new Error("Audio file is required for transcription");

0 commit comments

Comments
 (0)