Make research skill self-contained without external scripts
- Rewrite SKILL.md with inline instructions to spawn g3 --agent scout directly - Extend read_file to handle embedded skill paths (<embedded:name>/SKILL.md) - Remove scripts field from EmbeddedSkill struct (no longer needed) - Delete extraction.rs module (was only for script extraction) - Delete g3-research bash script - Remove obsolete Async Research Tool section from workspace memory Skills are now fully portable - they work when g3 is installed as a binary without access to source files. Agents can read embedded skill content via read_file with the special <embedded:...> path syntax.
This commit is contained in:
@@ -211,37 +211,6 @@ max_tokens = 4096
|
|||||||
gpu_layers = 99
|
gpu_layers = 99
|
||||||
```
|
```
|
||||||
|
|
||||||
### Async Research Tool
|
|
||||||
Research tool is asynchronous - spawns scout agent in background, returns immediately with research_id.
|
|
||||||
|
|
||||||
- `crates/g3-core/src/pending_research.rs`
|
|
||||||
- `PendingResearchManager` [80..100] - thread-safe task storage (Arc<Mutex<HashMap>>)
|
|
||||||
- `ResearchTask` [40..75] - id, query, status, result, started_at, injected
|
|
||||||
- `ResearchStatus` [20..35] - Pending, Complete, Failed enum
|
|
||||||
- `register()` [110..125] - creates task, returns research_id
|
|
||||||
- `complete()` / `fail()` [130..150] - update task status
|
|
||||||
- `take_completed()` [180..200] - returns completed tasks, marks as injected
|
|
||||||
- `list_all()` [165..170] - returns all tasks for /research command
|
|
||||||
|
|
||||||
- `crates/g3-core/src/tools/research.rs`
|
|
||||||
- `execute_research()` [150..210] - spawns scout in tokio::spawn, returns placeholder
|
|
||||||
- `run_scout_agent()` [215..300] - async fn that runs in background task
|
|
||||||
- `execute_research_status()` [305..380] - check status of pending research
|
|
||||||
|
|
||||||
- `crates/g3-core/src/lib.rs`
|
|
||||||
- `inject_completed_research()` [1080..1120] - injects completed research into context
|
|
||||||
- Called at start of each tool iteration and before user prompt in interactive mode
|
|
||||||
|
|
||||||
- `crates/g3-cli/src/commands.rs`
|
|
||||||
- `/research` command [125..160] - lists all research tasks with status
|
|
||||||
|
|
||||||
**Flow:**
|
|
||||||
1. Agent calls `research(query)` → returns immediately with research_id
|
|
||||||
2. Scout agent runs in background tokio task
|
|
||||||
3. On completion, `PendingResearchManager.complete()` stores result
|
|
||||||
4. At next iteration start or user prompt, `inject_completed_research()` adds to context
|
|
||||||
5. Agent can check status with `research_status` tool or user with `/research` command
|
|
||||||
|
|
||||||
### Plan Mode (replaces TODO system)
|
### Plan Mode (replaces TODO system)
|
||||||
Structured task planning with cognitive forcing - requires happy/negative/boundary checks.
|
Structured task planning with cognitive forcing - requires happy/negative/boundary checks.
|
||||||
|
|
||||||
|
|||||||
@@ -12,15 +12,13 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// An embedded skill with its SKILL.md content and optional scripts.
|
/// An embedded skill with its SKILL.md content.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EmbeddedSkill {
|
pub struct EmbeddedSkill {
|
||||||
/// Skill name (must match the name in SKILL.md frontmatter)
|
/// Skill name (must match the name in SKILL.md frontmatter)
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
/// Content of SKILL.md
|
/// Content of SKILL.md
|
||||||
pub skill_md: &'static str,
|
pub skill_md: &'static str,
|
||||||
/// Scripts bundled with the skill: (filename, content)
|
|
||||||
pub scripts: &'static [(&'static str, &'static str)],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All embedded skills, compiled into the binary.
|
/// All embedded skills, compiled into the binary.
|
||||||
@@ -32,9 +30,6 @@ static EMBEDDED_SKILLS: &[EmbeddedSkill] = &[
|
|||||||
EmbeddedSkill {
|
EmbeddedSkill {
|
||||||
name: "research",
|
name: "research",
|
||||||
skill_md: include_str!("../../../../skills/research/SKILL.md"),
|
skill_md: include_str!("../../../../skills/research/SKILL.md"),
|
||||||
scripts: &[
|
|
||||||
("g3-research", include_str!("../../../../skills/research/g3-research")),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -70,7 +65,6 @@ mod tests {
|
|||||||
|
|
||||||
let skill = skill.unwrap();
|
let skill = skill.unwrap();
|
||||||
assert!(skill.skill_md.contains("name: research"), "SKILL.md should have name field");
|
assert!(skill.skill_md.contains("name: research"), "SKILL.md should have name field");
|
||||||
assert!(!skill.scripts.is_empty(), "Research skill should have scripts");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
//! Script extraction for embedded skills.
|
|
||||||
//!
|
|
||||||
//! Extracts embedded scripts to `.g3/bin/` on first use.
|
|
||||||
//! Scripts are re-extracted if the embedded version changes.
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use std::fs;
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use tracing::{debug, info};
|
|
||||||
|
|
||||||
use super::embedded::get_embedded_skill;
|
|
||||||
|
|
||||||
/// Directory where extracted scripts are placed (relative to workspace)
|
|
||||||
const BIN_DIR: &str = ".g3/bin";
|
|
||||||
|
|
||||||
/// Version file to track when scripts need re-extraction
|
|
||||||
const VERSION_FILE: &str = ".version";
|
|
||||||
|
|
||||||
/// Extract a script from an embedded skill to the bin directory.
|
|
||||||
///
|
|
||||||
/// Returns the path to the extracted script.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `skill_name` - Name of the skill containing the script
|
|
||||||
/// * `script_name` - Name of the script file to extract
|
|
||||||
/// * `workspace_dir` - Workspace root directory
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// Path to the extracted script, ready to execute.
|
|
||||||
pub fn extract_script(
|
|
||||||
skill_name: &str,
|
|
||||||
script_name: &str,
|
|
||||||
workspace_dir: &Path,
|
|
||||||
) -> Result<PathBuf> {
|
|
||||||
let skill = get_embedded_skill(skill_name)
|
|
||||||
.with_context(|| format!("Embedded skill '{}' not found", skill_name))?;
|
|
||||||
|
|
||||||
let script_content = skill
|
|
||||||
.scripts
|
|
||||||
.iter()
|
|
||||||
.find(|(name, _)| *name == script_name)
|
|
||||||
.map(|(_, content)| *content)
|
|
||||||
.with_context(|| format!("Script '{}' not found in skill '{}'", script_name, skill_name))?;
|
|
||||||
|
|
||||||
let bin_dir = workspace_dir.join(BIN_DIR);
|
|
||||||
fs::create_dir_all(&bin_dir)
|
|
||||||
.with_context(|| format!("Failed to create bin directory: {}", bin_dir.display()))?;
|
|
||||||
|
|
||||||
let script_path = bin_dir.join(script_name);
|
|
||||||
let version_path = bin_dir.join(format!("{}{}", script_name, VERSION_FILE));
|
|
||||||
|
|
||||||
// Check if we need to extract (script missing or version changed)
|
|
||||||
let needs_extraction = if !script_path.exists() {
|
|
||||||
debug!("Script {} does not exist, extracting", script_path.display());
|
|
||||||
true
|
|
||||||
} else if needs_update(&version_path, script_content)? {
|
|
||||||
debug!("Script {} is outdated, re-extracting", script_path.display());
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
debug!("Script {} is up to date", script_path.display());
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if needs_extraction {
|
|
||||||
// Write the script
|
|
||||||
fs::write(&script_path, script_content)
|
|
||||||
.with_context(|| format!("Failed to write script: {}", script_path.display()))?;
|
|
||||||
|
|
||||||
// Make it executable (Unix only)
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
let mut perms = fs::metadata(&script_path)?.permissions();
|
|
||||||
perms.set_mode(0o755);
|
|
||||||
fs::set_permissions(&script_path, perms)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write version file (content hash)
|
|
||||||
let hash = compute_hash(script_content);
|
|
||||||
fs::write(&version_path, hash)
|
|
||||||
.with_context(|| format!("Failed to write version file: {}", version_path.display()))?;
|
|
||||||
|
|
||||||
info!("Extracted {} to {}", script_name, script_path.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(script_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract all scripts from an embedded skill.
|
|
||||||
///
|
|
||||||
/// Returns a vector of (script_name, script_path) pairs.
|
|
||||||
pub fn extract_all_scripts(
|
|
||||||
skill_name: &str,
|
|
||||||
workspace_dir: &Path,
|
|
||||||
) -> Result<Vec<(String, PathBuf)>> {
|
|
||||||
let skill = get_embedded_skill(skill_name)
|
|
||||||
.with_context(|| format!("Embedded skill '{}' not found", skill_name))?;
|
|
||||||
|
|
||||||
let mut extracted = Vec::new();
|
|
||||||
|
|
||||||
for (script_name, _) in skill.scripts {
|
|
||||||
let path = extract_script(skill_name, script_name, workspace_dir)?;
|
|
||||||
extracted.push((script_name.to_string(), path));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(extracted)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a script needs to be updated based on version file.
|
|
||||||
fn needs_update(version_path: &Path, current_content: &str) -> Result<bool> {
|
|
||||||
if !version_path.exists() {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
let stored_hash = fs::read_to_string(version_path)
|
|
||||||
.with_context(|| format!("Failed to read version file: {}", version_path.display()))?;
|
|
||||||
|
|
||||||
let current_hash = compute_hash(current_content);
|
|
||||||
|
|
||||||
Ok(stored_hash.trim() != current_hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute a simple hash of content for version tracking.
|
|
||||||
/// Uses a fast non-cryptographic hash.
|
|
||||||
fn compute_hash(content: &str) -> String {
|
|
||||||
use std::collections::hash_map::DefaultHasher;
|
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
content.hash(&mut hasher);
|
|
||||||
format!("{:016x}", hasher.finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the path where a script would be extracted.
|
|
||||||
/// Does not actually extract the script.
|
|
||||||
pub fn get_script_path(script_name: &str, workspace_dir: &Path) -> PathBuf {
|
|
||||||
workspace_dir.join(BIN_DIR).join(script_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a script has been extracted.
|
|
||||||
pub fn is_script_extracted(script_name: &str, workspace_dir: &Path) -> bool {
|
|
||||||
get_script_path(script_name, workspace_dir).exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_research_script() {
|
|
||||||
let temp = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
let result = extract_script("research", "g3-research", temp.path());
|
|
||||||
assert!(result.is_ok(), "Should extract research script: {:?}", result.err());
|
|
||||||
|
|
||||||
let script_path = result.unwrap();
|
|
||||||
assert!(script_path.exists(), "Script should exist after extraction");
|
|
||||||
|
|
||||||
// Check it's executable
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
let metadata = fs::metadata(&script_path).unwrap();
|
|
||||||
let mode = metadata.permissions().mode();
|
|
||||||
assert!(mode & 0o111 != 0, "Script should be executable");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check content
|
|
||||||
let content = fs::read_to_string(&script_path).unwrap();
|
|
||||||
assert!(content.starts_with("#!/bin/bash"), "Should be a bash script");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_idempotent() {
|
|
||||||
let temp = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
// Extract twice
|
|
||||||
let path1 = extract_script("research", "g3-research", temp.path()).unwrap();
|
|
||||||
let path2 = extract_script("research", "g3-research", temp.path()).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(path1, path2, "Should return same path");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_version_tracking() {
|
|
||||||
let temp = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
// Extract
|
|
||||||
extract_script("research", "g3-research", temp.path()).unwrap();
|
|
||||||
|
|
||||||
// Version file should exist
|
|
||||||
let version_path = temp.path().join(".g3/bin/g3-research.version");
|
|
||||||
assert!(version_path.exists(), "Version file should exist");
|
|
||||||
|
|
||||||
let hash = fs::read_to_string(&version_path).unwrap();
|
|
||||||
assert!(!hash.is_empty(), "Version file should contain hash");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_nonexistent_skill() {
|
|
||||||
let temp = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
let result = extract_script("nonexistent", "script", temp.path());
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().to_string().contains("not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_nonexistent_script() {
|
|
||||||
let temp = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
let result = extract_script("research", "nonexistent", temp.path());
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().to_string().contains("not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_script_path() {
|
|
||||||
let workspace = Path::new("/workspace");
|
|
||||||
let path = get_script_path("g3-research", workspace);
|
|
||||||
assert_eq!(path, PathBuf::from("/workspace/.g3/bin/g3-research"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_compute_hash() {
|
|
||||||
let hash1 = compute_hash("hello world");
|
|
||||||
let hash2 = compute_hash("hello world");
|
|
||||||
let hash3 = compute_hash("different content");
|
|
||||||
|
|
||||||
assert_eq!(hash1, hash2, "Same content should produce same hash");
|
|
||||||
assert_ne!(hash1, hash3, "Different content should produce different hash");
|
|
||||||
assert_eq!(hash1.len(), 16, "Hash should be 16 hex chars");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,6 @@ mod parser;
|
|||||||
mod discovery;
|
mod discovery;
|
||||||
mod prompt;
|
mod prompt;
|
||||||
mod embedded;
|
mod embedded;
|
||||||
pub mod extraction;
|
|
||||||
|
|
||||||
pub use parser::Skill;
|
pub use parser::Skill;
|
||||||
pub use discovery::discover_skills;
|
pub use discovery::discover_skills;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use tracing::debug;
|
|||||||
use crate::ui_writer::UiWriter;
|
use crate::ui_writer::UiWriter;
|
||||||
use crate::utils::resolve_path_with_unicode_fallback;
|
use crate::utils::resolve_path_with_unicode_fallback;
|
||||||
use crate::utils::apply_unified_diff_to_string;
|
use crate::utils::apply_unified_diff_to_string;
|
||||||
|
use crate::skills::get_embedded_skill;
|
||||||
use crate::ToolCall;
|
use crate::ToolCall;
|
||||||
|
|
||||||
use super::executor::ToolContext;
|
use super::executor::ToolContext;
|
||||||
@@ -68,6 +69,28 @@ fn calculate_read_limit(file_bytes: usize, total_tokens: u32, used_tokens: u32)
|
|||||||
Some(max_bytes)
|
Some(max_bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Try to read an embedded skill by path.
|
||||||
|
///
|
||||||
|
/// Recognizes paths like:
|
||||||
|
/// - `<embedded:research>/SKILL.md`
|
||||||
|
/// - `<embedded:skill-name>/SKILL.md`
|
||||||
|
///
|
||||||
|
/// Returns the skill content if found, None otherwise.
|
||||||
|
fn try_read_embedded_skill(path: &str) -> Option<&'static str> {
|
||||||
|
// Check for embedded skill path pattern: <embedded:name>/SKILL.md
|
||||||
|
if !path.starts_with("<embedded:") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract skill name from path like "<embedded:research>/SKILL.md"
|
||||||
|
let after_prefix = path.strip_prefix("<embedded:")?;
|
||||||
|
let skill_name = after_prefix.split('>').next()?;
|
||||||
|
|
||||||
|
// Look up the embedded skill
|
||||||
|
let skill = get_embedded_skill(skill_name)?;
|
||||||
|
Some(skill.skill_md)
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute the `read_file` tool.
|
/// Execute the `read_file` tool.
|
||||||
pub async fn execute_read_file<W: UiWriter>(
|
pub async fn execute_read_file<W: UiWriter>(
|
||||||
tool_call: &ToolCall,
|
tool_call: &ToolCall,
|
||||||
@@ -80,13 +103,7 @@ pub async fn execute_read_file<W: UiWriter>(
|
|||||||
None => return Ok("❌ Missing file_path argument".to_string()),
|
None => return Ok("❌ Missing file_path argument".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expand tilde (~) to home directory
|
// Extract optional start and end positions (needed for both embedded and file reads)
|
||||||
let expanded_path = shellexpand::tilde(file_path);
|
|
||||||
// Try to resolve with Unicode space fallback (macOS uses U+202F in screenshot names)
|
|
||||||
let resolved_path = resolve_path_with_unicode_fallback(expanded_path.as_ref());
|
|
||||||
let path_str = resolved_path.as_ref();
|
|
||||||
|
|
||||||
// Extract optional start and end positions
|
|
||||||
let start_char = tool_call
|
let start_char = tool_call
|
||||||
.args
|
.args
|
||||||
.get("start")
|
.get("start")
|
||||||
@@ -98,6 +115,25 @@ pub async fn execute_read_file<W: UiWriter>(
|
|||||||
.and_then(|v| v.as_u64())
|
.and_then(|v| v.as_u64())
|
||||||
.map(|n| n as usize);
|
.map(|n| n as usize);
|
||||||
|
|
||||||
|
// Check for embedded skill paths (e.g., "<embedded:research>/SKILL.md")
|
||||||
|
if let Some(content) = try_read_embedded_skill(file_path) {
|
||||||
|
let total_len = content.len();
|
||||||
|
let start = start_char.unwrap_or(0);
|
||||||
|
let end = end_char.unwrap_or(total_len).min(total_len);
|
||||||
|
if start >= total_len {
|
||||||
|
return Ok(format!("❌ Start position {} exceeds embedded skill length {}", start, total_len));
|
||||||
|
}
|
||||||
|
let slice = &content[start..end];
|
||||||
|
let line_count = slice.lines().count();
|
||||||
|
return Ok(format!("{}\n🔍 {} lines read (embedded skill)", slice, line_count));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand tilde (~) to home directory
|
||||||
|
let expanded_path = shellexpand::tilde(file_path);
|
||||||
|
// Try to resolve with Unicode space fallback (macOS uses U+202F in screenshot names)
|
||||||
|
let resolved_path = resolve_path_with_unicode_fallback(expanded_path.as_ref());
|
||||||
|
let path_str = resolved_path.as_ref();
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Reading file: {}, start={:?}, end={:?}",
|
"Reading file: {}, start={:?}, end={:?}",
|
||||||
path_str, start_char, end_char
|
path_str, start_char, end_char
|
||||||
|
|||||||
@@ -5,115 +5,114 @@ license: Apache-2.0
|
|||||||
compatibility: Requires g3 binary in PATH. WebDriver (Safari or Chrome) recommended for best results.
|
compatibility: Requires g3 binary in PATH. WebDriver (Safari or Chrome) recommended for best results.
|
||||||
metadata:
|
metadata:
|
||||||
author: g3
|
author: g3
|
||||||
version: "1.0"
|
version: "2.0"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Research Skill
|
# Research Skill
|
||||||
|
|
||||||
Perform asynchronous web research without blocking your current work. Research runs in the background and saves results to disk for you to read when ready.
|
Perform asynchronous web research without blocking your current work. Research runs in the background and results are saved to disk.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start research (ALWAYS use background_process, never blocking shell)
|
# 1. Create research directory and status file
|
||||||
background_process("research-<topic>", ".g3/bin/g3-research 'Your research question here'")
|
RESEARCH_ID="research_$(date +%s)_$(head -c 3 /dev/urandom | xxd -p)"
|
||||||
|
mkdir -p ".g3/research/$RESEARCH_ID"
|
||||||
|
echo '{"id":"'$RESEARCH_ID'","status":"running","query":"YOUR QUERY"}' > ".g3/research/$RESEARCH_ID/status.json"
|
||||||
|
|
||||||
# Check status
|
# 2. Start research in background
|
||||||
shell(".g3/bin/g3-research --status <research-id>")
|
background_process("research-topic", "g3 --agent scout --new-session --quiet 'Your research question' > .g3/research/$RESEARCH_ID/report.md 2>&1 && sed -i '' 's/running/complete/' .g3/research/$RESEARCH_ID/status.json || sed -i '' 's/running/failed/' .g3/research/$RESEARCH_ID/status.json")
|
||||||
# Or list all:
|
|
||||||
shell(".g3/bin/g3-research --list")
|
|
||||||
|
|
||||||
# Read the report when complete
|
# 3. Check status
|
||||||
read_file(".g3/research/<research-id>/report.md")
|
cat .g3/research/$RESEARCH_ID/status.json
|
||||||
|
|
||||||
|
# 4. Read report when complete
|
||||||
|
read_file(".g3/research/$RESEARCH_ID/report.md")
|
||||||
```
|
```
|
||||||
|
|
||||||
## How It Works
|
## Step-by-Step Instructions
|
||||||
|
|
||||||
1. **Start research** - The `g3-research` script spawns a scout agent that performs web research
|
### 1. Generate a Unique Research ID
|
||||||
2. **Background execution** - Research runs asynchronously; you can continue other work
|
|
||||||
3. **Filesystem handoff** - Results are written to `.g3/research/<id>/` with machine-readable status
|
Use shell to create a unique ID and directory:
|
||||||
4. **Read when ready** - Use `read_file` to load the report into context only when needed
|
|
||||||
|
```bash
|
||||||
|
shell("RESEARCH_ID=\"research_$(date +%s)_$(head -c 3 /dev/urandom | xxd -p)\" && mkdir -p \".g3/research/$RESEARCH_ID\" && echo $RESEARCH_ID")
|
||||||
|
```
|
||||||
|
|
||||||
|
Save the returned ID for later use.
|
||||||
|
|
||||||
|
### 2. Write Initial Status File
|
||||||
|
|
||||||
|
```bash
|
||||||
|
shell("echo '{\"id\":\"<RESEARCH_ID>\",\"status\":\"running\",\"query\":\"<YOUR_QUERY>\",\"started_at\":\"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'\"}' > .g3/research/<RESEARCH_ID>/status.json")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Start the Scout Agent
|
||||||
|
|
||||||
|
Use `background_process` to run the scout agent (NEVER use blocking `shell`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
background_process("research-<topic>", "g3 --agent scout --new-session --quiet '<Your detailed research question>' > .g3/research/<RESEARCH_ID>/report.md 2>&1; if [ $? -eq 0 ]; then sed -i '' 's/running/complete/' .g3/research/<RESEARCH_ID>/status.json; else sed -i '' 's/running/failed/' .g3/research/<RESEARCH_ID>/status.json; fi")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important flags:**
|
||||||
|
- `--agent scout` - Uses the scout agent optimized for web research
|
||||||
|
- `--new-session` - Starts a fresh session
|
||||||
|
- `--quiet` - Reduces UI noise in output
|
||||||
|
|
||||||
|
### 4. Check Research Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
shell("cat .g3/research/<RESEARCH_ID>/status.json")
|
||||||
|
```
|
||||||
|
|
||||||
|
Status values:
|
||||||
|
- `running` - Research in progress
|
||||||
|
- `complete` - Report ready to read
|
||||||
|
- `failed` - Error occurred
|
||||||
|
|
||||||
|
### 5. Read the Report
|
||||||
|
|
||||||
|
Once status is `complete`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
read_file(".g3/research/<RESEARCH_ID>/report.md")
|
||||||
|
```
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
.g3/research/
|
.g3/research/
|
||||||
├── research_1738700000_a1b2c3/
|
└── research_1738700000_a1b2c3/
|
||||||
│ ├── status.json # Machine-readable status
|
├── status.json # Machine-readable status
|
||||||
│ └── report.md # The research brief (when complete)
|
└── report.md # The research brief (when complete)
|
||||||
└── research_1738700100_d4e5f6/
|
|
||||||
├── status.json
|
|
||||||
└── report.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## status.json Schema
|
## Example: Complete Workflow
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "research_1738700000_a1b2c3",
|
|
||||||
"query": "What are the best Rust async runtimes?",
|
|
||||||
"status": "complete",
|
|
||||||
"started_at": "2026-02-04T12:00:00Z",
|
|
||||||
"completed_at": "2026-02-04T12:01:30Z",
|
|
||||||
"report_path": ".g3/research/research_1738700000_a1b2c3/report.md",
|
|
||||||
"error": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Status values:**
|
|
||||||
- `running` - Research in progress
|
|
||||||
- `complete` - Report ready to read
|
|
||||||
- `failed` - Error occurred (check `error` field)
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### Start Research
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.g3/bin/g3-research "<query>"
|
# Step 1: Create research task
|
||||||
```
|
shell("RESEARCH_ID=\"research_$(date +%s)_$(head -c 3 /dev/urandom | xxd -p)\" && mkdir -p \".g3/research/$RESEARCH_ID\" && echo '{\"id\":\"'$RESEARCH_ID'\",\"status\":\"running\",\"query\":\"Rust async runtimes comparison\"}' > \".g3/research/$RESEARCH_ID/status.json\" && echo $RESEARCH_ID")
|
||||||
|
# Returns: research_1738700000_a1b2c3
|
||||||
|
|
||||||
Outputs the research ID and path on success. **Always run via `background_process`**, not `shell`.
|
# Step 2: Start scout in background
|
||||||
|
background_process("research-rust-async", "g3 --agent scout --new-session --quiet 'Compare Tokio vs async-std vs smol for Rust async runtimes. Include performance, ecosystem, and ease of use.' > .g3/research/research_1738700000_a1b2c3/report.md 2>&1; [ $? -eq 0 ] && sed -i '' 's/running/complete/' .g3/research/research_1738700000_a1b2c3/status.json || sed -i '' 's/running/failed/' .g3/research/research_1738700000_a1b2c3/status.json")
|
||||||
|
|
||||||
### Check Status
|
# Step 3: Continue other work...
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check specific research
|
|
||||||
.g3/bin/g3-research --status <research-id>
|
|
||||||
|
|
||||||
# List all research tasks
|
|
||||||
.g3/bin/g3-research --list
|
|
||||||
```
|
|
||||||
|
|
||||||
Outputs JSON for machine parsing.
|
|
||||||
|
|
||||||
### Read Report
|
|
||||||
|
|
||||||
Once status is `complete`, read the report:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
read_file(".g3/research/<research-id>/report.md")
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tip:** If the report is large, use partial reads:
|
|
||||||
```bash
|
|
||||||
read_file(".g3/research/<id>/report.md", start=0, end=2000)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example Workflow
|
|
||||||
|
|
||||||
```
|
|
||||||
# 1. Start research on async runtimes
|
|
||||||
background_process("research-async", ".g3/bin/g3-research 'Compare Tokio vs async-std vs smol for Rust async runtimes'")
|
|
||||||
|
|
||||||
# 2. Continue with other work while research runs...
|
|
||||||
shell("cargo check")
|
shell("cargo check")
|
||||||
|
|
||||||
# 3. Check if research is done
|
# Step 4: Check if done
|
||||||
shell(".g3/bin/g3-research --list")
|
shell("cat .g3/research/research_1738700000_a1b2c3/status.json")
|
||||||
|
|
||||||
# 4. Read the report
|
# Step 5: Read report
|
||||||
read_file(".g3/research/research_1738700000_abc123/report.md")
|
read_file(".g3/research/research_1738700000_a1b2c3/report.md")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Listing All Research Tasks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
shell("for f in .g3/research/*/status.json; do cat \"$f\" 2>/dev/null; echo; done")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
@@ -121,7 +120,7 @@ read_file(".g3/research/research_1738700000_abc123/report.md")
|
|||||||
1. **Always use `background_process`** - Never run research with blocking `shell`
|
1. **Always use `background_process`** - Never run research with blocking `shell`
|
||||||
2. **Be specific** - Narrow queries get better results faster
|
2. **Be specific** - Narrow queries get better results faster
|
||||||
3. **Read selectively** - Only load reports into context when you need them
|
3. **Read selectively** - Only load reports into context when you need them
|
||||||
4. **Check status first** - Don't try to read reports that aren't complete
|
4. **Check status first** - Don't try to read reports that aren't complete yet
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
@@ -131,14 +130,9 @@ read_file(".g3/research/research_1738700000_abc123/report.md")
|
|||||||
|
|
||||||
### WebDriver not available
|
### WebDriver not available
|
||||||
- Research will still work but may have limited web access
|
- Research will still work but may have limited web access
|
||||||
- Install Safari WebDriver or Chrome for best results
|
- The scout agent will fall back to shell-based methods
|
||||||
|
|
||||||
### Report is empty or failed
|
### Report is empty or failed
|
||||||
- Check `status.json` for error details
|
- Check status.json for the status
|
||||||
|
- Look at the report.md file for any error output
|
||||||
- The query may be too broad or the topic too obscure
|
- The query may be too broad or the topic too obscure
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Research results accumulate in `.g3/research/` - they are not auto-cleaned
|
|
||||||
- Each research task gets a unique ID based on timestamp
|
|
||||||
- Multiple concurrent research tasks are supported
|
|
||||||
|
|||||||
@@ -1,338 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# g3-research - Perform web research via scout agent with filesystem handoff
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# g3-research "<query>" Start new research
|
|
||||||
# g3-research --status <id> Check status of specific research
|
|
||||||
# g3-research --list List all research tasks
|
|
||||||
# g3-research --help Show this help
|
|
||||||
#
|
|
||||||
# Research results are stored in .g3/research/<id>/
|
|
||||||
# - status.json: Machine-readable status
|
|
||||||
# - report.md: The research brief (when complete)
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
RESEARCH_DIR=".g3/research"
|
|
||||||
SCOUT_AGENT="scout"
|
|
||||||
|
|
||||||
# Report markers (must match scout agent output)
|
|
||||||
REPORT_START_MARKER="---SCOUT_REPORT_START---"
|
|
||||||
REPORT_END_MARKER="---SCOUT_REPORT_END---"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Generate a unique research ID
|
|
||||||
#######################################
|
|
||||||
generate_id() {
|
|
||||||
local timestamp
|
|
||||||
local random_suffix
|
|
||||||
timestamp=$(date +%s)
|
|
||||||
random_suffix=$(head -c 6 /dev/urandom | xxd -p | head -c 6)
|
|
||||||
echo "research_${timestamp}_${random_suffix}"
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Get current ISO 8601 timestamp
|
|
||||||
#######################################
|
|
||||||
get_timestamp() {
|
|
||||||
date -u +"%Y-%m-%dT%H:%M:%SZ"
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Write status.json file
|
|
||||||
# Arguments:
|
|
||||||
# $1 - research directory
|
|
||||||
# $2 - id
|
|
||||||
# $3 - query
|
|
||||||
# $4 - status (running|complete|failed)
|
|
||||||
# $5 - started_at
|
|
||||||
# $6 - completed_at (optional, use "null" for running)
|
|
||||||
# $7 - error (optional, use "null" for success)
|
|
||||||
#######################################
|
|
||||||
write_status() {
|
|
||||||
local dir="$1"
|
|
||||||
local id="$2"
|
|
||||||
local query="$3"
|
|
||||||
local status="$4"
|
|
||||||
local started_at="$5"
|
|
||||||
local completed_at="$6"
|
|
||||||
local error="$7"
|
|
||||||
|
|
||||||
# Escape query for JSON (handle quotes and newlines)
|
|
||||||
local escaped_query
|
|
||||||
escaped_query=$(echo -n "$query" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\n/\\n/g')
|
|
||||||
|
|
||||||
# Format completed_at and error as JSON values
|
|
||||||
local completed_json
|
|
||||||
local error_json
|
|
||||||
if [[ "$completed_at" == "null" ]]; then
|
|
||||||
completed_json="null"
|
|
||||||
else
|
|
||||||
completed_json="\"$completed_at\""
|
|
||||||
fi
|
|
||||||
if [[ "$error" == "null" ]]; then
|
|
||||||
error_json="null"
|
|
||||||
else
|
|
||||||
# Escape error message for JSON
|
|
||||||
local escaped_error
|
|
||||||
escaped_error=$(echo -n "$error" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\n/\\n/g' | head -c 1000)
|
|
||||||
error_json="\"$escaped_error\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > "${dir}/status.json" << EOF
|
|
||||||
{
|
|
||||||
"id": "${id}",
|
|
||||||
"query": "${escaped_query}",
|
|
||||||
"status": "${status}",
|
|
||||||
"started_at": "${started_at}",
|
|
||||||
"completed_at": ${completed_json},
|
|
||||||
"report_path": "${dir}/report.md",
|
|
||||||
"error": ${error_json}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Extract report from scout output
|
|
||||||
# Arguments:
|
|
||||||
# $1 - scout output file
|
|
||||||
# Returns:
|
|
||||||
# Report content between markers, or empty if not found
|
|
||||||
#######################################
|
|
||||||
strip_ansi() {
|
|
||||||
# Comprehensive ANSI escape sequence stripping
|
|
||||||
perl -pe 's/\e\[[0-9;]*[a-zA-Z]//g; s/\e\][^\a]*\a//g; s/\e[()][AB012]//g'
|
|
||||||
}
|
|
||||||
|
|
||||||
extract_report() {
|
|
||||||
local output_file="$1"
|
|
||||||
local report
|
|
||||||
|
|
||||||
# Use sed to extract content between markers
|
|
||||||
report=$(sed -n "/${REPORT_START_MARKER}/,/${REPORT_END_MARKER}/p" "$output_file" | \
|
|
||||||
sed "1d;\$d" | \
|
|
||||||
strip_ansi) # Remove markers and strip ANSI codes
|
|
||||||
|
|
||||||
if [[ -n "$report" ]]; then
|
|
||||||
echo "$report"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Fallback: if no markers found, try to extract useful content from raw output
|
|
||||||
# Strip ANSI escape codes and g3 UI elements
|
|
||||||
report=$(cat "$output_file" | \
|
|
||||||
strip_ansi | \
|
|
||||||
grep -v '^🆕 Starting new session' | \
|
|
||||||
grep -v '^>> agent mode' | \
|
|
||||||
grep -v '^\[38;' | \
|
|
||||||
grep -v '^-> ~' | \
|
|
||||||
grep -v '^ *✓' | \
|
|
||||||
grep -v '^📝 Auto-memory:' | \
|
|
||||||
grep -v 'Auto-memory:' | \
|
|
||||||
grep -v '^$' | \
|
|
||||||
sed '/^[[:space:]]*$/d' | \
|
|
||||||
head -500)
|
|
||||||
|
|
||||||
if [[ -n "$report" ]]; then
|
|
||||||
echo "$report"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Run research
|
|
||||||
# Arguments:
|
|
||||||
# $1 - query
|
|
||||||
#######################################
|
|
||||||
run_research() {
|
|
||||||
local query="$1"
|
|
||||||
local id
|
|
||||||
local research_dir
|
|
||||||
local started_at
|
|
||||||
local output_file
|
|
||||||
local exit_code
|
|
||||||
|
|
||||||
# Generate unique ID and create directory
|
|
||||||
id=$(generate_id)
|
|
||||||
research_dir="${RESEARCH_DIR}/${id}"
|
|
||||||
mkdir -p "$research_dir"
|
|
||||||
|
|
||||||
started_at=$(get_timestamp)
|
|
||||||
output_file="${research_dir}/scout_output.txt"
|
|
||||||
|
|
||||||
# Write initial status
|
|
||||||
write_status "$research_dir" "$id" "$query" "running" "$started_at" "null" "null"
|
|
||||||
|
|
||||||
# Output the research ID immediately so caller knows where to look
|
|
||||||
echo "{\"id\": \"${id}\", \"status\": \"running\", \"path\": \"${research_dir}\"}"
|
|
||||||
|
|
||||||
# Find g3 binary
|
|
||||||
local g3_bin
|
|
||||||
if command -v g3 &> /dev/null; then
|
|
||||||
g3_bin="g3"
|
|
||||||
elif [[ -x "./target/release/g3" ]]; then
|
|
||||||
g3_bin="./target/release/g3"
|
|
||||||
elif [[ -x "./target/debug/g3" ]]; then
|
|
||||||
g3_bin="./target/debug/g3"
|
|
||||||
else
|
|
||||||
write_status "$research_dir" "$id" "$query" "failed" "$started_at" "$(get_timestamp)" "g3 binary not found in PATH or target/"
|
|
||||||
echo "{\"id\": \"${id}\", \"status\": \"failed\", \"error\": \"g3 binary not found\"}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run scout agent and capture output
|
|
||||||
set +e
|
|
||||||
"$g3_bin" --agent "$SCOUT_AGENT" --new-session --quiet "$query" > "$output_file" 2>&1
|
|
||||||
exit_code=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
local completed_at
|
|
||||||
completed_at=$(get_timestamp)
|
|
||||||
|
|
||||||
if [[ $exit_code -ne 0 ]]; then
|
|
||||||
# Scout failed
|
|
||||||
local error_msg
|
|
||||||
error_msg=$(tail -20 "$output_file" 2>/dev/null || echo "Unknown error")
|
|
||||||
write_status "$research_dir" "$id" "$query" "failed" "$started_at" "$completed_at" "$error_msg"
|
|
||||||
echo "{\"id\": \"${id}\", \"status\": \"failed\", \"error\": \"Scout agent exited with code ${exit_code}\"}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract report from output
|
|
||||||
local report
|
|
||||||
report=$(extract_report "$output_file")
|
|
||||||
|
|
||||||
if [[ -z "$report" ]]; then
|
|
||||||
write_status "$research_dir" "$id" "$query" "failed" "$started_at" "$completed_at" "Scout did not produce a valid report (missing markers)"
|
|
||||||
echo "{\"id\": \"${id}\", \"status\": \"failed\", \"error\": \"No report markers found in output\"}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Write report to file
|
|
||||||
echo "$report" > "${research_dir}/report.md"
|
|
||||||
|
|
||||||
# Update status to complete
|
|
||||||
write_status "$research_dir" "$id" "$query" "complete" "$started_at" "$completed_at" "null"
|
|
||||||
|
|
||||||
# Clean up scout output (optional - keep for debugging)
|
|
||||||
# rm -f "$output_file"
|
|
||||||
|
|
||||||
echo "{\"id\": \"${id}\", \"status\": \"complete\", \"report_path\": \"${research_dir}/report.md\"}"
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Check status of a specific research task
|
|
||||||
# Arguments:
|
|
||||||
# $1 - research ID
|
|
||||||
#######################################
|
|
||||||
check_status() {
|
|
||||||
local id="$1"
|
|
||||||
local status_file="${RESEARCH_DIR}/${id}/status.json"
|
|
||||||
|
|
||||||
if [[ ! -f "$status_file" ]]; then
|
|
||||||
echo "{\"error\": \"Research task not found: ${id}\"}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat "$status_file"
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# List all research tasks
|
|
||||||
#######################################
|
|
||||||
list_research() {
|
|
||||||
if [[ ! -d "$RESEARCH_DIR" ]]; then
|
|
||||||
echo "[]"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
local first=true
|
|
||||||
echo "["
|
|
||||||
|
|
||||||
for status_file in "${RESEARCH_DIR}"/*/status.json; do
|
|
||||||
if [[ ! -f "$status_file" ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$first" == true ]]; then
|
|
||||||
first=false
|
|
||||||
else
|
|
||||||
echo ","
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat "$status_file"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "]"
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Show help
|
|
||||||
#######################################
|
|
||||||
show_help() {
|
|
||||||
cat << 'EOF'
|
|
||||||
g3-research - Perform web research via scout agent
|
|
||||||
|
|
||||||
USAGE:
|
|
||||||
g3-research "<query>" Start new research
|
|
||||||
g3-research --status <id> Check status of specific research
|
|
||||||
g3-research --list List all research tasks
|
|
||||||
g3-research --help Show this help
|
|
||||||
|
|
||||||
EXAMPLES:
|
|
||||||
# Start research (run via background_process)
|
|
||||||
g3-research "What are the best Rust HTTP client libraries?"
|
|
||||||
|
|
||||||
# Check status
|
|
||||||
g3-research --status research_1738700000_a1b2c3
|
|
||||||
|
|
||||||
# List all research
|
|
||||||
g3-research --list
|
|
||||||
|
|
||||||
OUTPUT:
|
|
||||||
All commands output JSON for machine parsing.
|
|
||||||
Research results are stored in .g3/research/<id>/
|
|
||||||
|
|
||||||
FILES:
|
|
||||||
.g3/research/<id>/status.json Machine-readable status
|
|
||||||
.g3/research/<id>/report.md Research brief (when complete)
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Main
|
|
||||||
#######################################
|
|
||||||
main() {
|
|
||||||
if [[ $# -eq 0 ]]; then
|
|
||||||
show_help
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
--help|-h)
|
|
||||||
show_help
|
|
||||||
;;
|
|
||||||
--status)
|
|
||||||
if [[ $# -lt 2 ]]; then
|
|
||||||
echo "{\"error\": \"Missing research ID\"}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
check_status "$2"
|
|
||||||
;;
|
|
||||||
--list)
|
|
||||||
list_research
|
|
||||||
;;
|
|
||||||
-*)
|
|
||||||
echo "{\"error\": \"Unknown option: $1\"}" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
# Treat as query
|
|
||||||
run_research "$1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
Reference in New Issue
Block a user