Delete orphaned ChatGPT product crate

This commit is contained in:
Meta Vens 2026-05-13 16:13:56 +01:00
commit a4310dd4e9
17 changed files with 11 additions and 779 deletions

View file

@ -883,6 +883,12 @@ and no longer have test-client helper senders. The old `Plugin*` data structs
remain only as inert protocol-shape residue for orphaned plugin crates such as
`core-plugins`; they are not live routes.
The orphaned `codex-chatgpt` crate is deleted from the vendored workspace. Do
not confuse that with ChatGPT auth itself: originator strings and login/model
auth surfaces that preserve the user's Codex subscription compatibility remain
part of the keeper spine until the native OpenAI adapter can replace more of
that transport.
The Phase 6 freshness slice is landed. It exposes read-only
`thread/epiphany/freshness` from live retrieval summaries plus graph
frontier/churn state and, for loaded threads, watcher-backed invalidation

Binary file not shown.

View file

@ -313,7 +313,9 @@ current_status:
route surface is no longer live either: `plugin/list`, `plugin/read`,
`plugin/install`, and `plugin/uninstall` were removed from the shared
`ClientRequest` protocol and from `codex_message_processor.rs`; only inert
legacy protocol structs remain for orphaned plugin crates. MCP remains
JSON-RPC at the edge; user-declared MCP servers are the only runtime MCP
authority.
legacy protocol structs remain for orphaned plugin crates. The orphaned
`codex-chatgpt` product crate was deleted from the vendored workspace; only
ChatGPT auth originator strings remain where they serve the subscription
compatibility spine. MCP remains JSON-RPC at the edge; user-declared MCP
servers are the only runtime MCP authority.
next_action: Continue starving coordinator/runtime execution and operator surfaces off app-server JSON-RPC and out of `codex_message_processor.rs`; keep the vestigial Codex organ narrowed to subscription auth/model transport.

View file

@ -125,7 +125,6 @@ codex-apply-patch = { path = "apply-patch" }
codex-arg0 = { path = "arg0" }
codex-async-utils = { path = "async-utils" }
codex-backend-client = { path = "backend-client" }
codex-chatgpt = { path = "chatgpt" }
codex-cli = { path = "cli" }
codex-client = { path = "codex-client" }
codex-collaboration-mode-templates = { path = "collaboration-mode-templates" }

View file

@ -1,6 +0,0 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "chatgpt",
crate_name = "codex_chatgpt",
)

View file

@ -1,27 +0,0 @@
[package]
name = "codex-chatgpt"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
codex-app-server-protocol = { workspace = true }
codex-connectors = { workspace = true }
codex-config = { workspace = true }
codex-core = { workspace = true }
codex-git-utils = { workspace = true }
codex-login = { workspace = true }
codex-utils-cli = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["full"] }
[dev-dependencies]
codex-utils-cargo-bin = { workspace = true }
pretty_assertions = { workspace = true }
serde_json = { workspace = true }
tempfile = { workspace = true }

View file

@ -1,5 +0,0 @@
# ChatGPT
This crate pertains to first party ChatGPT APIs and products such as Codex agent.
This crate should be primarily built and maintained by OpenAI employees. Please reach out to a maintainer before making an external contribution.

View file

@ -1,81 +0,0 @@
use std::path::PathBuf;
use clap::Parser;
use codex_core::config::Config;
use codex_git_utils::ApplyGitRequest;
use codex_git_utils::apply_git_patch;
use codex_utils_cli::CliConfigOverrides;
use crate::chatgpt_token::init_chatgpt_token_from_auth;
use crate::get_task::GetTaskResponse;
use crate::get_task::OutputItem;
use crate::get_task::PrOutputItem;
use crate::get_task::get_task;
/// Applies the latest diff from a Codex agent task.
#[derive(Debug, Parser)]
pub struct ApplyCommand {
pub task_id: String,
#[clap(flatten)]
pub config_overrides: CliConfigOverrides,
}
pub async fn run_apply_command(
apply_cli: ApplyCommand,
cwd: Option<PathBuf>,
) -> anyhow::Result<()> {
let config = Config::load_with_cli_overrides(
apply_cli
.config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?,
)
.await?;
init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode)
.await?;
let task_response = get_task(&config, apply_cli.task_id).await?;
apply_diff_from_task(task_response, cwd).await
}
pub async fn apply_diff_from_task(
task_response: GetTaskResponse,
cwd: Option<PathBuf>,
) -> anyhow::Result<()> {
let diff_turn = match task_response.current_diff_task_turn {
Some(turn) => turn,
None => anyhow::bail!("No diff turn found"),
};
let output_diff = diff_turn.output_items.iter().find_map(|item| match item {
OutputItem::Pr(PrOutputItem { output_diff }) => Some(output_diff),
_ => None,
});
match output_diff {
Some(output_diff) => apply_diff(&output_diff.diff, cwd).await,
None => anyhow::bail!("No PR output item found"),
}
}
async fn apply_diff(diff: &str, cwd: Option<PathBuf>) -> anyhow::Result<()> {
let cwd = cwd.unwrap_or(std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir()));
let req = ApplyGitRequest {
cwd,
diff: diff.to_string(),
revert: false,
preflight: false,
};
let res = apply_git_patch(&req)?;
if res.exit_code != 0 {
anyhow::bail!(
"Git apply failed (applied={}, skipped={}, conflicts={})\nstdout:\n{}\nstderr:\n{}",
res.applied_paths.len(),
res.skipped_paths.len(),
res.conflicted_paths.len(),
res.stdout,
res.stderr
);
}
println!("Successfully applied diff");
Ok(())
}

View file

@ -1,62 +0,0 @@
use codex_core::config::Config;
use codex_login::default_client::create_client;
use crate::chatgpt_token::get_chatgpt_token_data;
use crate::chatgpt_token::init_chatgpt_token_from_auth;
use anyhow::Context;
use serde::de::DeserializeOwned;
use std::time::Duration;
/// Make a GET request to the ChatGPT backend API.
pub(crate) async fn chatgpt_get_request<T: DeserializeOwned>(
config: &Config,
path: String,
) -> anyhow::Result<T> {
chatgpt_get_request_with_timeout(config, path, /*timeout*/ None).await
}
pub(crate) async fn chatgpt_get_request_with_timeout<T: DeserializeOwned>(
config: &Config,
path: String,
timeout: Option<Duration>,
) -> anyhow::Result<T> {
let chatgpt_base_url = &config.chatgpt_base_url;
init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode)
.await?;
// Make direct HTTP request to ChatGPT backend API with the token
let client = create_client();
let url = format!("{chatgpt_base_url}{path}");
let token =
get_chatgpt_token_data().ok_or_else(|| anyhow::anyhow!("ChatGPT token not available"))?;
let account_id = token.account_id.ok_or_else(|| {
anyhow::anyhow!("ChatGPT account ID not available, please re-run `codex login`")
});
let mut request = client
.get(&url)
.bearer_auth(&token.access_token)
.header("chatgpt-account-id", account_id?)
.header("Content-Type", "application/json");
if let Some(timeout) = timeout {
request = request.timeout(timeout);
}
let response = request.send().await.context("Failed to send request")?;
if response.status().is_success() {
let result: T = response
.json()
.await
.context("Failed to parse JSON response")?;
Ok(result)
} else {
let status = response.status();
let body = response.text().await.unwrap_or_default();
anyhow::bail!("Request failed with status {status}: {body}")
}
}

View file

@ -1,36 +0,0 @@
use codex_config::types::AuthCredentialsStoreMode;
use codex_login::AuthManager;
use codex_login::token_data::TokenData;
use std::path::Path;
use std::sync::LazyLock;
use std::sync::RwLock;
static CHATGPT_TOKEN: LazyLock<RwLock<Option<TokenData>>> = LazyLock::new(|| RwLock::new(None));
pub fn get_chatgpt_token_data() -> Option<TokenData> {
CHATGPT_TOKEN.read().ok()?.clone()
}
pub fn set_chatgpt_token_data(value: TokenData) {
if let Ok(mut guard) = CHATGPT_TOKEN.write() {
*guard = Some(value);
}
}
/// Initialize the ChatGPT token from auth.json file
pub async fn init_chatgpt_token_from_auth(
codex_home: &Path,
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> std::io::Result<()> {
let auth_manager = AuthManager::new(
codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
auth_credentials_store_mode,
/*chatgpt_base_url*/ None,
);
if let Some(auth) = auth_manager.auth().await {
let token_data = auth.get_token_data()?;
set_chatgpt_token_data(token_data);
}
Ok(())
}

View file

@ -1,255 +0,0 @@
use std::collections::HashSet;
use std::time::Duration;
use crate::chatgpt_client::chatgpt_get_request_with_timeout;
use crate::chatgpt_token::get_chatgpt_token_data;
use crate::chatgpt_token::init_chatgpt_token_from_auth;
use codex_app_server_protocol::AppInfo;
use codex_connectors::AllConnectorsCacheKey;
use codex_connectors::DirectoryListResponse;
use codex_connectors::filter::filter_disallowed_connectors;
use codex_connectors::merge::merge_connectors;
use codex_connectors::merge::merge_plugin_connectors;
use codex_core::config::Config;
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools;
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager;
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options;
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status;
pub use codex_core::connectors::list_cached_accessible_connectors_from_mcp_tools;
pub use codex_core::connectors::with_app_enabled_state;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::default_client::originator;
use codex_login::token_data::TokenData;
const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60);
async fn apps_enabled(config: &Config) -> bool {
let auth_manager = AuthManager::shared(
config.codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
config.cli_auth_credentials_store_mode,
Some(config.chatgpt_base_url.clone()),
);
let auth = auth_manager.auth().await;
config
.features
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth))
}
pub async fn list_connectors(config: &Config) -> anyhow::Result<Vec<AppInfo>> {
if !apps_enabled(config).await {
return Ok(Vec::new());
}
let (connectors_result, accessible_result) = tokio::join!(
list_all_connectors(config),
list_accessible_connectors_from_mcp_tools(config),
);
let connectors = connectors_result?;
let accessible = accessible_result?;
Ok(with_app_enabled_state(
merge_connectors_with_accessible(
connectors, accessible, /*all_connectors_loaded*/ true,
),
config,
))
}
pub async fn list_all_connectors(config: &Config) -> anyhow::Result<Vec<AppInfo>> {
list_all_connectors_with_options(config, /*force_refetch*/ false).await
}
pub async fn list_cached_all_connectors(config: &Config) -> Option<Vec<AppInfo>> {
if !apps_enabled(config).await {
return Some(Vec::new());
}
if init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode)
.await
.is_err()
{
return None;
}
let token_data = get_chatgpt_token_data()?;
let cache_key = all_connectors_cache_key(config, &token_data);
let connectors = codex_connectors::cached_all_connectors(&cache_key)?;
let connectors = merge_plugin_connectors(connectors, plugin_apps_for_config(config).await);
Some(filter_disallowed_connectors(
connectors,
originator().value.as_str(),
))
}
pub async fn list_all_connectors_with_options(
config: &Config,
force_refetch: bool,
) -> anyhow::Result<Vec<AppInfo>> {
if !apps_enabled(config).await {
return Ok(Vec::new());
}
init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode)
.await?;
let token_data =
get_chatgpt_token_data().ok_or_else(|| anyhow::anyhow!("ChatGPT token not available"))?;
let cache_key = all_connectors_cache_key(config, &token_data);
let connectors = codex_connectors::list_all_connectors_with_options(
cache_key,
token_data.id_token.is_workspace_account(),
force_refetch,
|path| async move {
chatgpt_get_request_with_timeout::<DirectoryListResponse>(
config,
path,
Some(DIRECTORY_CONNECTORS_TIMEOUT),
)
.await
},
)
.await?;
let connectors = merge_plugin_connectors(connectors, plugin_apps_for_config(config).await);
Ok(filter_disallowed_connectors(
connectors,
originator().value.as_str(),
))
}
fn all_connectors_cache_key(config: &Config, token_data: &TokenData) -> AllConnectorsCacheKey {
AllConnectorsCacheKey::new(
config.chatgpt_base_url.clone(),
token_data.account_id.clone(),
token_data.id_token.chatgpt_user_id.clone(),
token_data.id_token.is_workspace_account(),
)
}
async fn plugin_apps_for_config(_config: &Config) -> Vec<String> {
Vec::new()
}
pub fn connectors_for_plugin_apps(
connectors: Vec<AppInfo>,
plugin_apps: &[String],
) -> Vec<AppInfo> {
let plugin_app_ids = plugin_apps
.iter()
.map(String::as_str)
.collect::<HashSet<_>>();
let connectors = merge_plugin_connectors(connectors, plugin_apps.iter().cloned());
filter_disallowed_connectors(connectors, originator().value.as_str())
.into_iter()
.filter(|connector| plugin_app_ids.contains(connector.id.as_str()))
.collect()
}
pub fn merge_connectors_with_accessible(
connectors: Vec<AppInfo>,
accessible_connectors: Vec<AppInfo>,
all_connectors_loaded: bool,
) -> Vec<AppInfo> {
let accessible_connectors = if all_connectors_loaded {
let connector_ids: HashSet<&str> = connectors
.iter()
.map(|connector| connector.id.as_str())
.collect();
accessible_connectors
.into_iter()
.filter(|connector| connector_ids.contains(connector.id.as_str()))
.collect()
} else {
accessible_connectors
};
let merged = merge_connectors(connectors, accessible_connectors);
filter_disallowed_connectors(merged, originator().value.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
use codex_connectors::metadata::connector_install_url;
use pretty_assertions::assert_eq;
fn app(id: &str) -> AppInfo {
AppInfo {
id: id.to_string(),
name: id.to_string(),
description: None,
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
}
}
fn merged_app(id: &str, is_accessible: bool) -> AppInfo {
AppInfo {
id: id.to_string(),
name: id.to_string(),
description: None,
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: Some(connector_install_url(id, id)),
is_accessible,
is_enabled: true,
plugin_display_names: Vec::new(),
}
}
#[test]
fn excludes_accessible_connectors_not_in_all_when_all_loaded() {
let merged = merge_connectors_with_accessible(
vec![app("alpha")],
vec![app("alpha"), app("beta")],
/*all_connectors_loaded*/ true,
);
assert_eq!(merged, vec![merged_app("alpha", /*is_accessible*/ true)]);
}
#[test]
fn keeps_accessible_connectors_not_in_all_while_all_loading() {
let merged = merge_connectors_with_accessible(
vec![app("alpha")],
vec![app("alpha"), app("beta")],
/*all_connectors_loaded*/ false,
);
assert_eq!(
merged,
vec![
merged_app("alpha", /*is_accessible*/ true),
merged_app("beta", /*is_accessible*/ true)
]
);
}
#[test]
fn connectors_for_plugin_apps_returns_only_requested_plugin_apps() {
let connectors = connectors_for_plugin_apps(
vec![app("alpha"), app("beta")],
&["alpha".to_string(), "gmail".to_string()],
);
assert_eq!(
connectors,
vec![app("alpha"), merged_app("gmail", /*is_accessible*/ false)]
);
}
#[test]
fn connectors_for_plugin_apps_filters_disallowed_plugin_apps() {
let connectors = connectors_for_plugin_apps(
Vec::new(),
&["asdk_app_6938a94a61d881918ef32cb999ff937c".to_string()],
);
assert_eq!(connectors, Vec::<AppInfo>::new());
}
}

View file

@ -1,40 +0,0 @@
use codex_core::config::Config;
use serde::Deserialize;
use crate::chatgpt_client::chatgpt_get_request;
#[derive(Debug, Deserialize)]
pub struct GetTaskResponse {
pub current_diff_task_turn: Option<AssistantTurn>,
}
// Only relevant fields for our extraction
#[derive(Debug, Deserialize)]
pub struct AssistantTurn {
pub output_items: Vec<OutputItem>,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
pub enum OutputItem {
#[serde(rename = "pr")]
Pr(PrOutputItem),
#[serde(other)]
Other,
}
#[derive(Debug, Deserialize)]
pub struct PrOutputItem {
pub output_diff: OutputDiff,
}
#[derive(Debug, Deserialize)]
pub struct OutputDiff {
pub diff: String,
}
pub(crate) async fn get_task(config: &Config, task_id: String) -> anyhow::Result<GetTaskResponse> {
let path = format!("/wham/tasks/{task_id}");
chatgpt_get_request(config, path).await
}

View file

@ -1,5 +0,0 @@
pub mod apply_command;
mod chatgpt_client;
mod chatgpt_token;
pub mod connectors;
pub mod get_task;

View file

@ -1,3 +0,0 @@
// Single integration test binary that aggregates all test modules.
// The submodules live in `tests/suite/`.
mod suite;

View file

@ -1,188 +0,0 @@
use codex_chatgpt::apply_command::apply_diff_from_task;
use codex_chatgpt::get_task::GetTaskResponse;
use codex_utils_cargo_bin::find_resource;
use tempfile::TempDir;
use tokio::process::Command;
/// Creates a temporary git repository with initial commit
async fn create_temp_git_repo() -> anyhow::Result<TempDir> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
let envs = vec![
("GIT_CONFIG_GLOBAL", "/dev/null"),
("GIT_CONFIG_NOSYSTEM", "1"),
];
let output = Command::new("git")
.envs(envs.clone())
.args(["init"])
.current_dir(repo_path)
.output()
.await?;
if !output.status.success() {
anyhow::bail!(
"Failed to initialize git repo: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Command::new("git")
.envs(envs.clone())
.args(["config", "user.email", "test@example.com"])
.current_dir(repo_path)
.output()
.await?;
Command::new("git")
.envs(envs.clone())
.args(["config", "user.name", "Test User"])
.current_dir(repo_path)
.output()
.await?;
std::fs::write(repo_path.join("README.md"), "# Test Repo\n")?;
Command::new("git")
.envs(envs.clone())
.args(["add", "README.md"])
.current_dir(repo_path)
.output()
.await?;
let output = Command::new("git")
.envs(envs.clone())
.args(["commit", "-m", "Initial commit"])
.current_dir(repo_path)
.output()
.await?;
if !output.status.success() {
anyhow::bail!(
"Failed to create initial commit: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(temp_dir)
}
async fn mock_get_task_with_fixture() -> anyhow::Result<GetTaskResponse> {
let fixture_path = find_resource!("tests/task_turn_fixture.json")?;
let fixture_content = tokio::fs::read_to_string(fixture_path).await?;
let response: GetTaskResponse = serde_json::from_str(&fixture_content)?;
Ok(response)
}
#[tokio::test]
async fn test_apply_command_creates_fibonacci_file() {
let temp_repo = create_temp_git_repo()
.await
.expect("Failed to create temp git repo");
let repo_path = temp_repo.path();
let task_response = mock_get_task_with_fixture()
.await
.expect("Failed to load fixture");
apply_diff_from_task(task_response, Some(repo_path.to_path_buf()))
.await
.expect("Failed to apply diff from task");
// Assert that fibonacci.js was created in scripts/ directory
let fibonacci_path = repo_path.join("scripts/fibonacci.js");
assert!(fibonacci_path.exists(), "fibonacci.js was not created");
// Verify the file contents match expected
let contents = std::fs::read_to_string(&fibonacci_path).expect("Failed to read fibonacci.js");
assert!(
contents.contains("function fibonacci(n)"),
"fibonacci.js doesn't contain expected function"
);
assert!(
contents.contains("#!/usr/bin/env node"),
"fibonacci.js doesn't have shebang"
);
assert!(
contents.contains("module.exports = fibonacci;"),
"fibonacci.js doesn't export function"
);
// Verify file has correct number of lines (31 as specified in fixture)
let line_count = contents.lines().count();
assert_eq!(
line_count, 31,
"fibonacci.js should have 31 lines, got {line_count}",
);
}
#[tokio::test]
async fn test_apply_command_with_merge_conflicts() {
let temp_repo = create_temp_git_repo()
.await
.expect("Failed to create temp git repo");
let repo_path = temp_repo.path();
// Create conflicting fibonacci.js file first
let scripts_dir = repo_path.join("scripts");
std::fs::create_dir_all(&scripts_dir).expect("Failed to create scripts directory");
let conflicting_content = r#"#!/usr/bin/env node
// This is a different fibonacci implementation
function fib(num) {
if (num <= 1) return num;
return fib(num - 1) + fib(num - 2);
}
console.log("Running fibonacci...");
console.log(fib(10));
"#;
let fibonacci_path = scripts_dir.join("fibonacci.js");
std::fs::write(&fibonacci_path, conflicting_content).expect("Failed to write conflicting file");
Command::new("git")
.args(["add", "scripts/fibonacci.js"])
.current_dir(repo_path)
.output()
.await
.expect("Failed to add fibonacci.js");
Command::new("git")
.args(["commit", "-m", "Add conflicting fibonacci implementation"])
.current_dir(repo_path)
.output()
.await
.expect("Failed to commit conflicting file");
let original_dir = std::env::current_dir().expect("Failed to get current dir");
std::env::set_current_dir(repo_path).expect("Failed to change directory");
struct DirGuard(std::path::PathBuf);
impl Drop for DirGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.0);
}
}
let _guard = DirGuard(original_dir);
let task_response = mock_get_task_with_fixture()
.await
.expect("Failed to load fixture");
let apply_result = apply_diff_from_task(task_response, Some(repo_path.to_path_buf())).await;
assert!(
apply_result.is_err(),
"Expected apply to fail due to merge conflicts"
);
let contents = std::fs::read_to_string(&fibonacci_path).expect("Failed to read fibonacci.js");
assert!(
contents.contains("<<<<<<< HEAD")
|| contents.contains("=======")
|| contents.contains(">>>>>>> "),
"fibonacci.js should contain merge conflict markers, got: {contents}",
);
}

View file

@ -1,2 +0,0 @@
// Aggregates all former standalone integration tests as modules.
mod apply_command_e2e;

View file

@ -1,65 +0,0 @@
{
"current_diff_task_turn": {
"output_items": [
{
"type": "pr",
"pr_title": "Add fibonacci script",
"pr_message": "## Summary\n- add a basic Fibonacci script under `scripts/`\n\n## Testing\n- `node scripts/fibonacci.js 10`\n- `npm run lint` *(fails: next not found)*",
"output_diff": {
"type": "output_diff",
"repo_id": "/workspace/rddit-vercel",
"base_commit_sha": "1a2e9baf2ce2fdd0c126b47b1bcfd512de2a9f7b",
"diff": "diff --git a/scripts/fibonacci.js b/scripts/fibonacci.js\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..6c9fdfdbf8669b7968936411050525b995d0a9a6\n--- /dev/null\n+++ b/scripts/fibonacci.js\n@@ -0,0 +1,31 @@\n+#!/usr/bin/env node\n+\n+function fibonacci(n) {\n+ if (n < 0) {\n+ throw new Error(\"n must be non-negative\");\n+ }\n+ let a = 0;\n+ let b = 1;\n+ for (let i = 0; i < n; i++) {\n+ const next = a + b;\n+ a = b;\n+ b = next;\n+ }\n+ return a;\n+}\n+\n+function printUsage() {\n+ console.log(\"Usage: node scripts/fibonacci.js <n>\");\n+}\n+\n+if (require.main === module) {\n+ const arg = process.argv[2];\n+ if (arg === undefined || isNaN(Number(arg))) {\n+ printUsage();\n+ process.exit(1);\n+ }\n+ const n = Number(arg);\n+ console.log(fibonacci(n));\n+}\n+\n+module.exports = fibonacci;\n",
"external_storage_diff": {
"file_id": "file_00000000114c61f786900f8c2130ace7",
"ttl": null
},
"files_modified": 1,
"lines_added": 31,
"lines_removed": 0,
"commit_message": "Add fibonacci script"
}
},
{
"type": "message",
"role": "assistant",
"content": [
{
"content_type": "text",
"text": "**Summary**\n\n- Created a command-line Fibonacci script that validates input and prints the result when executed with Node"
},
{
"content_type": "repo_file_citation",
"path": "scripts/fibonacci.js",
"line_range_start": 1,
"line_range_end": 31
},
{
"content_type": "text",
"text": "\n\n**Testing**\n\n- ❌ `npm run lint` (failed to run `next lint`)"
},
{
"content_type": "terminal_chunk_citation",
"terminal_chunk_id": "7dd543",
"line_range_start": 1,
"line_range_end": 5
},
{
"content_type": "text",
"text": "\n- ✅ `node scripts/fibonacci.js 10` produced “55”"
},
{
"content_type": "terminal_chunk_citation",
"terminal_chunk_id": "6ee559",
"line_range_start": 1,
"line_range_end": 3
},
{
"content_type": "text",
"text": "\n\nCodex couldn't run certain commands due to environment limitations. Consider configuring a setup script or internet access in your Codex environment to install dependencies."
}
]
}
]
}
}