use anyhow::Result;
use rig::{
    completion::Prompt,
    embeddings::EmbeddingsBuilder,
    prelude::*,
    providers::openai::{self, Client},
    tool::{Tool, ToolEmbedding, ToolSet},
    vector_store::in_memory_store::InMemoryVectorStore,
};
use serde::{Deserialize, Serialize};
use serde_json::json;

#[derive(Deserialize)]
struct OperationArgs {
    x: i32,
    y: i32,
}

#[derive(Debug, thiserror::Error)]
#[error("Math error")]
struct MathError;

#[derive(Debug, thiserror::Error)]
#[error("Math error")]
struct InitError;

#[derive(Deserialize, Serialize)]
struct Add;

impl Tool for Add {
    const NAME: &'static str = "add";
    type Error = MathError;
    type Args = OperationArgs;
    type Output = i32;

    fn description(&self) -> String {
        "Add x and y together".to_string()
    }

    fn parameters(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "x": {
                    "type": "number",
                    "description": "The first number to add"
                },
                "y": {
                    "type": "number",
                    "description": "The second number to add"
                }
            }
        })
    }

    async fn call(
        &self,
        _context: &mut rig::tool::ToolContext,
        args: Self::Args,
    ) -> Result<Self::Output, Self::Error> {
        let result = args.x + args.y;
        Ok(result)
    }
}

impl ToolEmbedding for Add {
    type InitError = InitError;
    type Context = ();
    type State = ();

    fn init(_state: Self::State, _context: Self::Context) -> Result<Self, Self::InitError> {
        Ok(Add)
    }

    fn embedding_docs(&self) -> Vec<String> {
        vec!["Add x and y together".into()]
    }

    fn context(&self) -> Self::Context {}
}

#[derive(Deserialize, Serialize)]
struct Subtract;

impl Tool for Subtract {
    const NAME: &'static str = "subtract";
    type Error = MathError;
    type Args = OperationArgs;
    type Output = i32;

    fn description(&self) -> String {
        "Subtract y from x (i.e.: x - y)".to_string()
    }

    fn parameters(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "x": {
                    "type": "number",
                    "description": "The number to subtract from"
                },
                "y": {
                    "type": "number",
                    "description": "The number to subtract"
                }
            }
        })
    }

    async fn call(
        &self,
        _context: &mut rig::tool::ToolContext,
        args: Self::Args,
    ) -> Result<Self::Output, Self::Error> {
        let result = args.x - args.y;
        Ok(result)
    }
}

impl ToolEmbedding for Subtract {
    type InitError = InitError;
    type Context = ();
    type State = ();

    fn init(_state: Self::State, _context: Self::Context) -> Result<Self, Self::InitError> {
        Ok(Subtract)
    }

    fn context(&self) -> Self::Context {}

    fn embedding_docs(&self) -> Vec<String> {
        vec!["Subtract y from x (i.e.: x - y)".into()]
    }
}

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    // required to enable CloudWatch error logging by the runtime
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        // disable printing the name of the module in every log line.
        .with_target(false)
        .init();

    // Create OpenAI client
    let openai_client = Client::from_env()?;

    let embedding_model = openai_client.embedding_model(openai::TEXT_EMBEDDING_ADA_002);

    let toolset = ToolSet::builder()
        .retrieved_tool(Add)
        .retrieved_tool(Subtract)
        .build();

    let embeddings = EmbeddingsBuilder::new(embedding_model.clone())
        .documents(toolset.schemas()?)?
        .build()
        .await?;

    // Create vector store with the embeddings
    let vector_store =
        InMemoryVectorStore::from_documents_with_id_f(embeddings, |tool| tool.name.clone());

    // Create vector store index
    let index = vector_store.index(embedding_model);

    // Create RAG agent with a single context prompt and a dynamic tool source
    let calculator_rag = openai_client
        .agent(openai::GPT_4)
        .preamble(
            "You are a calculator here to help the user perform arithmetic operations.
            Use the tools provided to answer the user's question and do not do any math on your own.",
        )
        // Add a dynamic tool source with a sample rate of 2 (i.e.: only
        // 2 additional tool will be added to prompts)
        .retrieved_tools(2, index, toolset)
        .build();

    // Prompt the agent and print the response
    let response = calculator_rag
        .prompt("Calculate (3 - 7) + 17")
        .max_turns(10)
        .await?;

    println!("{response}");

    Ok(())
}


use std::path::Path;
use std::str;

use rustc_hash::FxHashSet;
use tokio::io::AsyncReadExt;

use crate::git;

pub(super) async fn file_has_shebang(path: &Path) -> Result<bool, anyhow::Error> {
    let mut file = fs_err::tokio::File::open(path).await?;
    let mut buf = [0u8; 2];
    let n = file.read(&mut buf).await?;
    Ok(n >= 2 && buf[0] == b'#' && buf[1] == b'!')
}

pub(super) async fn git_index_stage_output(file_base: &Path) -> Result<Vec<u8>, anyhow::Error> {
    Ok(git::git_cmd()?
        .arg("ls-files")
        .arg("--stage")
        .arg("-z")
        .arg("--")
        .arg(if file_base.as_os_str().is_empty() {
            Path::new(".")
        } else {
            file_base
        })
        .check(true)
        .output()
        .await?
        .stdout)
}

pub(super) fn matching_git_index_paths_by_executable_bit<'a>(
    stdout: &'a [u8],
    file_base: &'a Path,
    filenames: &'a FxHashSet<&Path>,
    executable: bool,
) -> impl Iterator<Item = &'a Path> + 'a {
    stdout
        .split(|&b| b == b'\0')
        .filter_map(move |entry| parse_stage_entry(entry, file_base, filenames, executable))
}

fn parse_stage_entry<'a>(
    entry: &'a [u8],
    file_base: &Path,
    filenames: &FxHashSet<&Path>,
    executable: bool,
) -> Option<&'a Path> {
    let entry = str::from_utf8(entry).ok()?;
    if entry.is_empty() {
        return None;
    }

    let (metadata, file_name) = entry.split_once('\t')?;
    let file_name = Path::new(file_name);
    let file_name = file_name.strip_prefix(file_base).unwrap_or(file_name);
    if !filenames.contains(file_name) {
        return None;
    }

    let mode_bits = u32::from_str_radix(metadata.split_whitespace().next()?, 8).ok()?;
    (((mode_bits & 0o111) != 0) == executable).then_some(file_name)
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::NamedTempFile;

    #[test]
    fn parse_stage_entry_strips_project_prefix() {
        let filenames = FxHashSet::from_iter([Path::new("script.sh")]);
        let entry = b"100644 abcdef0123456789abcdef0123456789abcdef 0\tsubdir/script.sh";

        assert_eq!(
            parse_stage_entry(entry, Path::new("subdir"), &filenames, false),
            Some(Path::new("script.sh"))
        );
    }

    #[test]
    fn parse_stage_entry_filters_by_executable_bit() {
        let filenames = FxHashSet::from_iter([Path::new("script.sh")]);
        let executable_entry = b"100755 abcdef0123456789abcdef0123456789abcdef 0\tscript.sh";
        let non_executable_entry = b"100644 abcdef0123456789abcdef0123456789abcdef 0\tscript.sh";

        assert_eq!(
            parse_stage_entry(executable_entry, Path::new(""), &filenames, true),
            Some(Path::new("script.sh"))
        );
        assert_eq!(
            parse_stage_entry(executable_entry, Path::new(""), &filenames, false),
            None
        );
        assert_eq!(
            parse_stage_entry(non_executable_entry, Path::new(""), &filenames, false),
            Some(Path::new("script.sh"))
        );
    }

    #[tokio::test]
    async fn file_has_shebang_detects_valid_shebang() -> Result<(), anyhow::Error> {
        let file = NamedTempFile::new()?;
        fs_err::tokio::write(file.path(), b"#!/bin/sh\necho hi\n").await?;

        assert!(file_has_shebang(file.path()).await?);
        Ok(())
    }

    #[tokio::test]
    async fn file_has_shebang_rejects_non_shebang_prefixes() -> Result<(), anyhow::Error> {
        let file = NamedTempFile::new()?;
        fs_err::tokio::write(file.path(), b"##!/bin/sh\n").await?;

        assert!(!file_has_shebang(file.path()).await?);
        Ok(())
    }
}


#!/usr/bin/env node

const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');

const root = path.join(__dirname, '..');

function runUninstall(env) {
  return spawnSync(process.execPath, [path.join(root, 'scripts', 'uninstall.js')], {
    env: { ...process.env, ...env },
    encoding: 'utf8',
  });
}

delete process.env.CLAUDE_CONFIG_DIR;

const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ponytail-uninstall-'));
process.on('exit', () => fs.rmSync(temp, { recursive: true, force: true }));

const home = path.join(temp, 'home');
const claudeDir = path.join(home, '.claude');
fs.mkdirSync(claudeDir, { recursive: true });

const flagPath = path.join(claudeDir, '.ponytail-active');
fs.writeFileSync(flagPath, 'full');

const configDir = path.join(temp, 'config-home', 'ponytail');
fs.mkdirSync(configDir, { recursive: true });
const configPath = path.join(configDir, 'config.json');
fs.writeFileSync(configPath, JSON.stringify({ defaultMode: 'ultra' }));

const settingsPath = path.join(claudeDir, 'settings.json');
fs.writeFileSync(settingsPath, JSON.stringify({
  statusLine: { type: 'command', command: 'bash /some/path/ponytail-statusline.sh' },
}));

const env = {
  HOME: home,
  USERPROFILE: home,
  XDG_CONFIG_HOME: path.join(temp, 'config-home'),
};

let result = runUninstall(env);
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.existsSync(flagPath), false, 'mode flag must be removed');
assert.equal(fs.existsSync(configPath), false, 'config file must be removed');

const settingsAfter = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
assert.equal(
  settingsAfter.statusLine,
  undefined,
  'ponytail statusLine entry must be removed',
);

// A user's own, unrelated statusLine must survive untouched.
fs.writeFileSync(settingsPath, JSON.stringify({
  statusLine: { type: 'command', command: 'bash ~/my-custom-statusline.sh' },
}));

result = runUninstall(env);
assert.equal(result.status, 0, result.stderr);
const settingsAfter2 = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
assert.equal(
  settingsAfter2.statusLine.command,
  'bash ~/my-custom-statusline.sh',
  "a user's own statusLine must not be touched",
);

// #374: a combined statusline (another plugin && ponytail) must keep the other
// plugin's part — uninstall must not nuke the whole command or leave a husk.
fs.writeFileSync(settingsPath, JSON.stringify({
  statusLine: { type: 'command', command: 'bash ~/caveman-statusline.sh && bash /p/ponytail-statusline.sh' },
}));

result = runUninstall(env);
assert.equal(result.status, 0, result.stderr);
const settingsAfter3 = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
assert.equal(
  settingsAfter3.statusLine.command,
  'bash ~/caveman-statusline.sh',
  'a combined statusLine must keep the non-ponytail command',
);

// #434: a malformed settings.json must not crash the script mid-cleanup. It
// can't be safely edited, so uninstall warns and leaves the file byte-for-byte
// intact instead of throwing a SyntaxError after other state was already removed.
const malformedSettings = '{ "statusLine": { "command": "ponytail-statusline.sh", broken';
fs.writeFileSync(settingsPath, malformedSettings);

result = runUninstall(env);
assert.equal(
  result.status,
  0,
  `expected exit 0 on malformed settings.json, got:\n${result.stdout}${result.stderr}`,
);
assert.ok(
  /malformed/i.test(result.stdout + result.stderr),
  'must warn that the statusLine entry could not be removed',
);
assert.equal(
  fs.readFileSync(settingsPath, 'utf8'),
  malformedSettings,
  'malformed settings.json must be left unchanged',
);

// Running on an already-clean machine must not throw.
result = runUninstall({ HOME: path.join(temp, 'home-empty'), USERPROFILE: path.join(temp, 'home-empty') });
assert.equal(result.status, 0, result.stderr);

console.log('uninstall script checks passed');


#!/usr/bin/env node
// Publish the generated OpenClaw skills (.openclaw/skills/) to ClawHub.
//
// ClawHub does not sync from GitHub: each skill is pushed explicitly with the
// clawhub CLI and carries its own version. This publishes every generated skill
// in one pass, versioned from the repo's package.json so ClawHub tracks the repo
// instead of drifting (the same drift that hit the plugin manifests in #260).
//
// Prereqs:
//   - `clawhub login` once (registry auth persists)
//   - skills must be current: run `node scripts/build-openclaw-skills.js` first
//     if you changed a skill (CI fails if the committed copies are stale)
//
// Usage:
//   node scripts/publish-openclaw-skills.js            # publish all as latest
//   node scripts/publish-openclaw-skills.js --dry-run  # preview, upload nothing
//   (any extra args are passed through to `clawhub skill publish`)

const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

const root = path.join(__dirname, '..');
const skillsDir = path.join(root, '.openclaw', 'skills');

const version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;

// Every generated skill dir with a SKILL.md is publishable. Reading the dir
// (instead of a hardcoded list) covers whatever build-openclaw-skills emits,
// with nothing to keep in sync.
const slugs = fs.readdirSync(skillsDir, { withFileTypes: true })
  .filter((e) => e.isDirectory() && fs.existsSync(path.join(skillsDir, e.name, 'SKILL.md')))
  .map((e) => e.name)
  .sort();

if (slugs.length === 0) {
  console.error(`No skills under ${path.relative(root, skillsDir)}; run build-openclaw-skills.js first.`);
  process.exit(1);
}

// "ponytail-review" -> "Ponytail Review"
const displayName = (slug) =>
  slug.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');

// Minimal quoting that satisfies both POSIX sh and cmd.exe: only display names
// (which contain a space) need wrapping; slugs, versions, paths, and flags don't.
const quote = (a) => (/[^\w./-]/.test(a) ? `"${a}"` : a);

const passthrough = process.argv.slice(2);
const extra = passthrough.length ? ` (${passthrough.join(' ')})` : '';
console.log(`Publishing ${slugs.length} skills to ClawHub at version ${version}${extra}:`);

for (const slug of slugs) {
  const args = [
    'clawhub', 'skill', 'publish', `.openclaw/skills/${slug}`,
    '--slug', slug,
    '--name', displayName(slug),
    '--version', version,
    '--tags', 'latest',
    ...passthrough,
  ];
  const cmdline = args.map(quote).join(' ');
  console.log(`\n$ ${cmdline}`);
  const res = spawnSync(cmdline, { stdio: 'inherit', cwd: root, shell: true });
  if (res.status !== 0) {
    console.error(
      `\nPublish failed for "${slug}" (exit ${res.status}). ` +
      `Check that the clawhub CLI is installed and you have run \`clawhub login\`, then re-run. ` +
      `Skills already published in this run are unaffected.`,
    );
    process.exit(res.status || 1);
  }
}

console.log(`\nDone. Published ${slugs.length} skills at ${version}.`);