summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/cli/src/gemini.ts66
-rw-r--r--packages/cli/src/ui/App.tsx15
-rw-r--r--packages/cli/src/ui/hooks/useStdin.ts84
-rw-r--r--packages/cli/src/utils/readStdin.ts27
4 files changed, 100 insertions, 92 deletions
diff --git a/packages/cli/src/gemini.ts b/packages/cli/src/gemini.ts
index 8df10aba..0d8b1ac7 100644
--- a/packages/cli/src/gemini.ts
+++ b/packages/cli/src/gemini.ts
@@ -8,16 +8,70 @@ import React from 'react';
import { render } from 'ink';
import { App } from './ui/App.js';
import { loadCliConfig } from './config/config.js';
+import { readStdin } from './utils/readStdin.js';
+import { GeminiClient, ServerTool } from '@gemini-code/server';
+
+import { PartListUnion } from '@google/genai';
async function main() {
+ let initialInput: string | undefined = undefined;
+
+ // Check if input is being piped
+ if (!process.stdin.isTTY) {
+ try {
+ initialInput = await readStdin();
+ } catch (error) {
+ console.error('Error reading from stdin:', error);
+ process.exit(1);
+ }
+ }
+
// Load configuration
const config = loadCliConfig();
- // Render UI, passing necessary config values
- render(
- React.createElement(App, {
- config,
- }),
- );
+
+ // Render UI, passing necessary config values and initial input
+ if (process.stdin.isTTY) {
+ render(
+ React.createElement(App, {
+ config,
+ initialInput,
+ }),
+ );
+ } else if (initialInput) {
+ // If not a TTY and we have initial input, process it directly
+ const geminiClient = new GeminiClient(
+ config.getApiKey(),
+ config.getModel(),
+ );
+ const toolRegistry = config.getToolRegistry();
+ const availableTools: ServerTool[] = toolRegistry.getAllTools();
+ const toolDeclarations = toolRegistry.getFunctionDeclarations();
+ const chat = await geminiClient.startChat(toolDeclarations);
+
+ const request: PartListUnion = [{ text: initialInput }];
+
+ try {
+ for await (const event of geminiClient.sendMessageStream(
+ chat,
+ request,
+ availableTools,
+ )) {
+ if (event.type === 'content') {
+ process.stdout.write(event.value);
+ }
+ // We might need to handle other event types later, but for now, just content.
+ }
+ process.stdout.write('\n'); // Add a newline at the end
+ process.exit(0);
+ } catch (error) {
+ console.error('Error processing piped input:', error);
+ process.exit(1);
+ }
+ } else {
+ // If not a TTY and no initial input, exit with an error
+ console.error('No input provided via stdin.');
+ process.exit(1);
+ }
}
// --- Global Unhandled Rejection Handler ---
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index f3e8b742..3bfb73db 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { useState, useMemo } from 'react';
+import React, { useState, useMemo, useEffect } from 'react'; // Added useEffect
import { Box, Text } from 'ink';
import { StreamingState, type HistoryItem } from './types.js';
import { useGeminiStream } from './hooks/useGeminiStream.js';
@@ -25,9 +25,11 @@ import { Colors } from './colors.js';
interface AppProps {
config: Config;
+ initialInput?: string; // Added optional prop
}
-export const App = ({ config }: AppProps) => {
+export const App = ({ config, initialInput }: AppProps) => {
+ // Destructured prop
const [history, setHistory] = useState<HistoryItem[]>([]);
const [startupWarnings, setStartupWarnings] = useState<string[]>([]);
const { streamingState, submitQuery, initError, debugMessage } =
@@ -38,6 +40,15 @@ export const App = ({ config }: AppProps) => {
useStartupWarnings(setStartupWarnings);
useInitializationErrorEffect(initError, history, setHistory);
+ // Effect to handle initial piped input
+ useEffect(() => {
+ if (initialInput && initialInput.trim() !== '') {
+ submitQuery(initialInput);
+ }
+ // Run only once on mount
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
const userMessages = useMemo(
() =>
history
diff --git a/packages/cli/src/ui/hooks/useStdin.ts b/packages/cli/src/ui/hooks/useStdin.ts
deleted file mode 100644
index dc245254..00000000
--- a/packages/cli/src/ui/hooks/useStdin.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * @license
- * Copyright 2025 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { useState, useEffect } from 'react';
-import { useStdin } from 'ink';
-
-export interface PipedInputState {
- data: string | null; // Use null initially to distinguish from empty string
- isLoading: boolean;
- error: string | null;
- isPiped: boolean; // Flag to indicate if input was piped
-}
-
-export function usePipedInput(): PipedInputState {
- const { stdin, setRawMode, isRawModeSupported } = useStdin();
- // Keep exit available if needed, e.g., for error handling, but maybe let consumer handle it
- // const { exit } = useApp();
-
- const [pipedData, setPipedData] = useState<string | null>(null);
- const [isLoading, setIsLoading] = useState<boolean>(true); // Assume loading until checked
- const [error, setError] = useState<string | null>(null);
- const [isPiped, setIsPiped] = useState<boolean>(false);
-
- useEffect(() => {
- // Determine if input is piped ONLY ONCE
- const checkIsPiped = !stdin || !stdin.isTTY;
- setIsPiped(checkIsPiped);
-
- if (checkIsPiped) {
- // Piped input detected
- if (isRawModeSupported) {
- setRawMode(false); // Ensure raw mode is off for stream reading
- }
-
- // Ensure stdin is available (it should be if !isTTY)
- if (!stdin) {
- setError('Stdin stream is unavailable.');
- setIsLoading(false);
- return; // Cannot proceed
- }
-
- let data = '';
- const handleData = (chunk: Buffer) => {
- data += chunk.toString();
- };
-
- const handleError = (err: Error) => {
- setError('Error reading from stdin: ' + err.message);
- setIsLoading(false);
- // Decide if the hook should trigger exit or just report the error
- // exit();
- };
-
- const handleEnd = () => {
- setPipedData(data);
- setIsLoading(false);
- // Don't exit here, let the component using the hook decide
- };
-
- stdin.on('data', handleData);
- stdin.on('error', handleError);
- stdin.on('end', handleEnd);
-
- // Cleanup listeners
- return () => {
- stdin.removeListener('data', handleData);
- stdin.removeListener('error', handleError);
- stdin.removeListener('end', handleEnd);
- };
- }
-
- // No piped input (running interactively)
- setIsLoading(false);
- // Optionally set an 'info' state or just let isLoading=false & isPiped=false suffice
- // setError('No piped input detected.'); // Maybe don't treat this as an 'error'
-
- // Intentionally run only once on mount or when stdin theoretically changes
- }, [stdin, isRawModeSupported, setRawMode /*, exit */]);
-
- return { data: pipedData, isLoading, error, isPiped };
-}
diff --git a/packages/cli/src/utils/readStdin.ts b/packages/cli/src/utils/readStdin.ts
new file mode 100644
index 00000000..d890aa2c
--- /dev/null
+++ b/packages/cli/src/utils/readStdin.ts
@@ -0,0 +1,27 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export async function readStdin(): Promise<string> {
+ return new Promise((resolve, reject) => {
+ let data = '';
+ process.stdin.setEncoding('utf8');
+
+ process.stdin.on('readable', () => {
+ let chunk;
+ while ((chunk = process.stdin.read()) !== null) {
+ data += chunk;
+ }
+ });
+
+ process.stdin.on('end', () => {
+ resolve(data);
+ });
+
+ process.stdin.on('error', (err) => {
+ reject(err);
+ });
+ });
+}