Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v7
uses: actions/setup-python@v4
with:
python-version: '3.10'

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/graphify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v7
uses: actions/setup-python@v5
with:
python-version: "3.12"

Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

Expand Down
4 changes: 1 addition & 3 deletions cli/src/commands/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,7 @@ fn replace_call(source: &str, prefix: &str, keyword: &str) -> String {
remaining = &remaining[pos + full.len()..];
if let Some(close) = remaining.find(')') {
let args = &remaining[..close];
result.push_str(keyword);
result.push(' ');
result.push_str(args);
result.push_str(&format!("{keyword} {args}"));
remaining = &remaining[close + 1..];
} else {
result.push_str(remaining);
Expand Down
50 changes: 23 additions & 27 deletions cli/src/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,29 @@ impl FileWatcher {
fn scan_files(&self, states: &mut HashMap<PathBuf, SystemTime>) -> anyhow::Result<()> {
let mut dirs = vec![self.root.clone()];
while let Some(dir) = dirs.pop() {
if !dir.is_dir() {
if dir.is_dir() {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
// Don't watch build or cache directories to prevent infinite loops
let name = path.file_name().unwrap_or_default().to_string_lossy();
if name != "build" && name != ".git" && name != "target" {
dirs.push(path);
}
} else {
let ext = path.extension().unwrap_or_default().to_string_lossy();
if ext == "txs" || ext == "ts" {
if let Ok(metadata) = entry.metadata() {
if let Ok(modified) = metadata.modified() {
states.insert(path, modified);
}
}
}
}
}
}
} else {
// Root is a single file
let ext = self.root.extension().unwrap_or_default().to_string_lossy();
if ext == "txs" || ext == "ts" {
Expand All @@ -81,32 +103,6 @@ impl FileWatcher {
}
}
}
continue;
}

if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
// Don't watch build or cache directories to prevent infinite loops
let name = path.file_name().unwrap_or_default().to_string_lossy();
if name != "build" && name != ".git" && name != "target" {
dirs.push(path);
}
continue;
}

let ext = path.extension().unwrap_or_default().to_string_lossy();
if ext != "txs" && ext != "ts" {
continue;
}

if let Ok(metadata) = entry.metadata() {
if let Ok(modified) = metadata.modified() {
states.insert(path, modified);
}
}
}
}
}
Ok(())
Expand Down
1 change: 1 addition & 0 deletions compiler/lexer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use techscript_syntax::{lookup_keyword, Token, TokenKind};
/// Private token enumeration used internally by Logos for scanning.
#[derive(Logos, Debug, Clone, Copy, PartialEq, Eq)]
#[logos(skip r"[ \t\r]+")] // Skip spaces, tabs, and carriage returns
#[allow(dead_code)]
enum LogosToken {
#[token("\n")]
#[token("\r\n")]
Expand Down
2 changes: 1 addition & 1 deletion scripts/migrate_syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
(r'std\.io\.println\((.+?)\)', r'say \1', 0),
(r'std\.io\.print\((.+?)\)', r'say \1', 0),

# ── std.xxx β†’ module ─────────────────────────────────────────
# ── std.xxx.yyy() calls β†’ module.yyy() ─────────────────────────────────
(r'std\.math\.', r'math.', 0),
(r'std\.strings\.', r'string.', 0),
(r'std\.fs\.', r'file.', 0),
Expand Down
228 changes: 79 additions & 149 deletions stdlib/src/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ impl StdlibRegistry {
// Check environment capability for retrieving API keys
if !ctx.config.capabilities.contains(&Capability::Environment) {
return Err(RuntimeError::new(
RuntimeErrorKind::InvalidOperation(
"Security policy violation: Environment capability is denied"
.to_string(),
),
RuntimeErrorKind::InvalidOperation("Security policy violation: Environment capability is denied".to_string()),
None,
None,
));
Expand All @@ -38,27 +35,93 @@ impl StdlibRegistry {
// Also requires Network capability to make HTTP requests
if !ctx.config.capabilities.contains(&Capability::Network) {
return Err(RuntimeError::new(
RuntimeErrorKind::InvalidOperation(
"Security policy violation: Network capability is denied"
.to_string(),
),
RuntimeErrorKind::InvalidOperation("Security policy violation: Network capability is denied".to_string()),
None,
None,
));
}

match provider.as_str() {
"openai" => generate_openai(&prompt),
"gemini" => generate_gemini(&prompt),
"local" => generate_local(&prompt),
"openai" => {
let key = std::env::var("OPENAI_API_KEY").unwrap_or_default();
if key.is_empty() {
return Ok(RuntimeValue::Str(format!("[Mock OpenAI Response] Prompt: {}", prompt)));
}

// Real HTTP call to OpenAI Chat Completion
let body = serde_json::json!({
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": prompt}]
});

let resp = ureq::post("https://api.openai.com/v1/chat/completions")
.set("Authorization", &format!("Bearer {}", key))
.set("Content-Type", "application/json")
.send_json(body)
.map_err(|e| RuntimeError::new(RuntimeErrorKind::InvalidOperation(format!("OpenAI request failed: {}", e)), None, None))?;

let json: serde_json::Value = resp.into_json()
.map_err(|e| RuntimeError::new(RuntimeErrorKind::InvalidOperation(format!("Failed to parse OpenAI JSON response: {}", e)), None, None))?;

let content = json["choices"][0]["message"]["content"].as_str()
.ok_or_else(|| RuntimeError::new(RuntimeErrorKind::InvalidOperation("OpenAI response content empty".to_string()), None, None))?;

Ok(RuntimeValue::Str(content.to_string()))
}
"gemini" => {
let key = std::env::var("GEMINI_API_KEY").unwrap_or_default();
if key.is_empty() {
return Ok(RuntimeValue::Str(format!("[Mock Gemini Response] Prompt: {}", prompt)));
}

// Real HTTP call to Gemini API
let url = format!("https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={}", key);
let body = serde_json::json!({
"contents": [{
"parts": [{"text": prompt}]
}]
});

let resp = ureq::post(&url)
.set("Content-Type", "application/json")
.send_json(body)
.map_err(|e| RuntimeError::new(RuntimeErrorKind::InvalidOperation(format!("Gemini request failed: {}", e)), None, None))?;

let json: serde_json::Value = resp.into_json()
.map_err(|e| RuntimeError::new(RuntimeErrorKind::InvalidOperation(format!("Failed to parse Gemini JSON response: {}", e)), None, None))?;

let content = json["candidates"][0]["content"]["parts"][0]["text"].as_str()
.ok_or_else(|| RuntimeError::new(RuntimeErrorKind::InvalidOperation("Gemini response content empty".to_string()), None, None))?;

Ok(RuntimeValue::Str(content.to_string()))
}
"local" => {
// Mock local Llama.cpp inference endpoint check (e.g. running on localhost:8080)
let local_url = "http://127.0.0.1:8080/completion";
let body = serde_json::json!({
"prompt": prompt,
"n_predict": 128
});

match ureq::post(local_url).set("Content-Type", "application/json").send_json(body) {
Ok(resp) => {
if let Ok(json) = resp.into_json::<serde_json::Value>() {
if let Some(content) = json["content"].as_str() {
return Ok(RuntimeValue::Str(content.to_string()));
}
}
Ok(RuntimeValue::Str("[Mock Local LLM Response] (local server responded with invalid content)".to_string()))
}
Err(_) => {
Ok(RuntimeValue::Str(format!("[Mock Local LLM Response] Prompt: {}", prompt)))
}
}
}
_ => Err(RuntimeError::new(
RuntimeErrorKind::InvalidOperation(format!(
"Unknown AI provider: {}",
provider
)),
RuntimeErrorKind::InvalidOperation(format!("Unknown AI provider: {}", provider)),
None,
None,
)),
))
}
},
}),
Expand All @@ -75,136 +138,3 @@ impl StdlibRegistry {
);
}
}

fn generate_openai(prompt: &str) -> Result<RuntimeValue, RuntimeError> {
let key = std::env::var("OPENAI_API_KEY").unwrap_or_default();
if key.is_empty() {
return Ok(RuntimeValue::Str(format!(
"[Mock OpenAI Response] Prompt: {}",
prompt
)));
}

// Real HTTP call to OpenAI Chat Completion
let body = serde_json::json!({
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": prompt}]
});

let resp = ureq::post("https://api.openai.com/v1/chat/completions")
.set("Authorization", &format!("Bearer {}", key))
.set("Content-Type", "application/json")
.send_json(body)
.map_err(|e| {
RuntimeError::new(
RuntimeErrorKind::InvalidOperation(format!("OpenAI request failed: {}", e)),
None,
None,
)
})?;

let json: serde_json::Value = resp.into_json().map_err(|e| {
RuntimeError::new(
RuntimeErrorKind::InvalidOperation(format!(
"Failed to parse OpenAI JSON response: {}",
e
)),
None,
None,
)
})?;

let content = json["choices"][0]["message"]["content"]
.as_str()
.ok_or_else(|| {
RuntimeError::new(
RuntimeErrorKind::InvalidOperation("OpenAI response content empty".to_string()),
None,
None,
)
})?;

Ok(RuntimeValue::Str(content.to_string()))
}

fn generate_gemini(prompt: &str) -> Result<RuntimeValue, RuntimeError> {
let key = std::env::var("GEMINI_API_KEY").unwrap_or_default();
if key.is_empty() {
return Ok(RuntimeValue::Str(format!(
"[Mock Gemini Response] Prompt: {}",
prompt
)));
}

// Real HTTP call to Gemini API
let url = format!("https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={}", key);
let body = serde_json::json!({
"contents": [{
"parts": [{"text": prompt}]
}]
});

let resp = ureq::post(&url)
.set("Content-Type", "application/json")
.send_json(body)
.map_err(|e| {
RuntimeError::new(
RuntimeErrorKind::InvalidOperation(format!("Gemini request failed: {}", e)),
None,
None,
)
})?;

let json: serde_json::Value = resp.into_json().map_err(|e| {
RuntimeError::new(
RuntimeErrorKind::InvalidOperation(format!(
"Failed to parse Gemini JSON response: {}",
e
)),
None,
None,
)
})?;

let content = json["candidates"][0]["content"]["parts"][0]["text"]
.as_str()
.ok_or_else(|| {
RuntimeError::new(
RuntimeErrorKind::InvalidOperation("Gemini response content empty".to_string()),
None,
None,
)
})?;

Ok(RuntimeValue::Str(content.to_string()))
}

fn generate_local(prompt: &str) -> Result<RuntimeValue, RuntimeError> {
// Mock local Llama.cpp inference endpoint check (e.g. running on localhost:8080)
let local_url = "http://127.0.0.1:8080/completion";
let body = serde_json::json!({
"prompt": prompt,
"n_predict": 128
});

match ureq::post(local_url)
.set("Content-Type", "application/json")
.send_json(body)
{
Ok(resp) => {
if let Ok(json) = resp.into_json::<serde_json::Value>() {
if let Some(content) = json["content"].as_str() {
return Ok(RuntimeValue::Str(content.to_string()));
}
}
Ok(RuntimeValue::Str(
"[Mock Local LLM Response] (local server responded with invalid content)"
.to_string(),
))
}
Err(_) => Ok(RuntimeValue::Str(format!(
"[Mock Local LLM Response] Prompt: {}",
prompt
))),
}
}
Loading
Loading