Skill: t-sinclair2500/lm-studio-subagents (version 1.0.3)

--- name: lmstudio-subagents description: "Reduces token usage from paid providers by offloading work to local LM Studio models. Use when: (1) Cutting costs—use local models for summarization, extraction, classification, rewriting, first-pass review, brainstorming when quality suffices, (2) Avoiding paid API calls for high-volume or repetitive tasks, (3) No extra model configuration—JIT loading and REST API work with existing LM Studio setup, (4) Local-only or privacy-sensitive work. Requires LM Studio 0.4+ with server (default :1234). No CLI required." metadata: {"openclaw":{"requires":{},"tags":["local-model","local-llm","lm-studio","token-management","privacy","subagents"]}} license: MIT --- # LM Studio Models Offload tasks to local models when quality suffices. Base URL: http://127.0.0.1:1234. Auth: `Authorization: Bearer lmstudio`. instance_id = `loaded_instances[].id` (same model can have multiple, e.g. `key` and `key:2`). ## Key Terms - **model**: From GET models key; use in chat and optional load. - **lm_studio_api_url**: Default http://127.0.0.1:1234 (paths /api/v1/...). - **response_id** / **previous_response_id**: Chat returns response_id; pass as previous_response_id for stateful. - **instance_id**: For unload, use only the value from GET /api/v1/models for that model: each `loaded_instances[].id`. Do not assume it equals the model key; with multiple instances ids can be like key:2. LM Studio docs: List (loaded_instances[].id), Unload (instance_id). Trigger in frontmatter; below = implementation. ## Prerequisites LM Studio 0.4+, server :1234, models on disk; load/unload via API (JIT optional); Node for script (curl ok). ## Quick start Minimal path: list models, then one chat. Replace `<model>` with a key from GET /api/v1/models and `<task>` with the task text. ```bash curl -s -H 'Authorization: Bearer lmstudio' http://127.0.0.1:1234/api/v1/models node scripts/lmstudio-api.mjs <model> '<task>' --temperature=0.5 --max-output-tokens=200 ``` Stateful multi-turn: pass `--previous-response-id=<id>` from the prior script output. Or use `--stateful` to persist response_id automatically. Optional `--log <path>` for request/response. ```bash node scripts/lmstudio-api.mjs <model> 'First turn...' --previous-response-id=$ID1 node scripts/lmstudio-api.mjs <model> 'Second turn...' --previous-response-id=$ID2 ``` ## Complete Workflow ### Step 0: Preflight GET <base>/api/v1/models; non-200 or connection error = server not ready. ```bash exec command:"curl -s -o /dev/null -w '%{http_code}' -H 'Authorization: Bearer lmstudio' http://127.0.0.1:1234/api/v1/models" ``` ### Step 1: List Models and Select GET /api/v1/models to list models. Parse each entry: key, type, loaded_instances, max_context_length, capabilities. If a model already has loaded_instances.length > 0 and fits the task, skip to Step 5; otherwise pick a key for chat (and optional load in Step 3). Choose by task: vision -> capabilities.vision; embedding -> type=embedding; context -> max_context_length. Prefer already-loaded; prefer smaller for speed, larger for reasoning. Note loaded_instances[].id for optional unload later. **Example — list models:** ```bash exec command:"curl -s -H 'Authorization: Bearer lmstudio' http://127.0.0.1:1234/api/v1/models" ``` Parse models[] (key, type, loaded_instances, max_context_length, capabilities, params_string). If a model has loaded_instances.length > 0 and fits task, skip to Step 5; else pick key for chat (and optional load). Note loaded_instances[].id for optional unload. ### Step 2: Model Selection Pick key from GET response; use as model in chat (optional load). Constraints: vision -> capabilities.vision; embedding -> type=embedding; context -> max_context_length. Prefer loaded (loaded_instances non-empty), smaller for speed/larger for reasoning; fallback primary. If unsure, use the first loaded instance for that key or the smallest loaded model that fits the task. Optional POST load; else JIT on first chat. ### Step 3: Load Model (optional) Optional: POST /api/v1/models/load { model, context_length?, ... }. Or run scripts/load.mjs &lt;model&gt;. JIT: first chat loads; explicit load only for specific options. ### Step 4: Verify Loaded (optional) If explicit load: GET models, confirm loaded_instances. If JIT: no verify; first chat returns model_instance_id, stats.model_load_time_seconds. ### Step 5: Call API From the skill folder: node scripts/lmstudio-api.mjs &lt;model&gt; '&lt;task&gt;' [options]. ```bash exec command:"node scripts/lmstudio-api.mjs <model> '<task>' --temperature=0.7 --max-output-tokens=2000" ``` Stateful: add --previous-response-id=<response_id>. Curl: POST <base>/api/v1/chat, body model, input, store, temperature, max_output_tokens; optional previous_response_id. Parse: output (type message) -> content; response_id, model_instance_id, stats. Script outputs content, model_instance_id, response_id, usage. ### Step 6: Unload (optional) For the model key you used: GET /api/v1/models, then for **each** `loaded_instances[].id` for that model, POST /api/v1/models/unload with body `{"instance_id": "<that id>"}`. Use the id from the response only (do not send the model key unless it exactly equals that id). Or run scripts/unload.mjs &lt;model_key&gt; (script does GET then unloads each instance id). Optional --unload-after (default off); use --keep to leave loaded. Unload only that model's instances. JIT+TTL auto-unload; explicit when needed. ```bash # One unload per instance_id; repeat for each id in that model's loaded_instances exec command:"curl -s -X POST http://127.0.0.1:1234/api/v1/models/unload -H 'Content-Type: application/json' -H 'Authorization: Bearer lmstudio' -d '{\"instance_id\": \"<instance_id>\"}'" ``` ### Step 7: Verify unload (optional) After unloading, confirm no instances remain for that model key. Run the jq check below; result must be `0`. If non-zero, unload the remaining instance_id(s) from that model and re-run the check. Do not infer from "model object exists"; the object still exists with an empty `loaded_instances` array. ```bash exec command:"curl -s -H 'Authorization: Bearer lmstudio' http://127.0.0.1:1234/api/v1/models | jq '.models[]|select(.key==\"<model_key>\")|.loaded_instances|length'" ``` Expect output `0`. If not, unload remaining instance_ids and re-run. ## Error Handling - Script retries on transient failure (2-3 attempts with backoff). - Model not found -> pick another model from GET response. - API/server errors -> GET models, check URL. - Invalid output -> retry. - Memory -> unload or smaller model. - Unload fails -> instance_id must be exactly from GET /api/v1/models for that model's loaded_instances[].id (not the model key unless it matches). ## Copy-paste Replace `<model>` with a key from GET /api/v1/models and `<task>` with the task text. Optional unload per Step 6 (instance_id from GET models for that key). ```bash exec command:"curl -s -H 'Authorization: Bearer lmstudio' http://127.0.0.1:1234/api/v1/models" exec command:"node scripts/lmstudio-api.mjs <model> '<task>' --temperature=0.7 --max-output-tokens=2000" ``` ## LM Studio API Details Helper/API: see Step 5. Output: content, model_instance_id, response_id, usage. Auth: Bearer lmstudio. List GET /api/v1/models. Load POST /api/v1/models/load (optional). Unload POST /api/v1/models/unload { instance_id }. ## Scripts lmstudio-api.mjs: chat; options --stateful, --unload-after, --keep, --log &lt;path&gt;, --previous-response-id, --temperature, --max-output-tokens. load.mjs: load model by key. unload.mjs: unload by model key (all instances). test.mjs: smoke test (load, chat, unload one model). ## Notes - LM Studio 0.4+. - JIT (first chat loads; model_load_time_seconds in stats); stateful (response_id / previous_response_id).

path: README.md
content: # LM Studio Subagents Skill

An OpenClaw skill that equips agents to search for and offload tasks to local models running in LM Studio. This skill enables agents to discover available models, select appropriate ones based on task requirements, and use them for cost-effective local processing.

## Features

- **REST-only (no CLI)** - Uses LM Studio v1 REST API only; no `lms` on PATH required
- **Model discovery** - Lists and selects from models via GET /api/v1/models
- **Task offloading** - Routes appropriate tasks to local models to save paid API tokens
- **Stateful multi-turn** - Optional response_id / previous_response_id for conversation context
- **JIT loading** - No explicit load required; first chat request loads the model (stats.model_load_time_seconds)
- **No configuration required** - Works with models in LM Studio without OpenClaw config setup
- **Local processing** - All processing happens locally for privacy
- **Model selection** - Supports LLMs, VLMs, and embedding models based on task needs
- **Helper scripts** - load.mjs, unload.mjs; lmstudio-api.mjs supports --stateful, --unload-after, --log; smoke test: test.mjs

## Installation

### Via ClawdHub (Recommended)

```bash
npm i -g clawdhub
clawdhub install lmstudio-subagents
```

### Manual Installation

1. Clone this repository or download the skill folder
2. Place the `lmstudio-subagents` folder in your OpenClaw skills directory:
   - Workspace: `<workspace>/skills/lmstudio-subagents/`
   - Global: `~/.openclaw/skills/lmstudio-subagents/`

## Prerequisites

- LM Studio 0.4+ with server running (default: http://127.0.0.1:1234)
- No CLI required
- Models downloaded in LM Studio

## Usage

The skill is automatically triggered when the agent needs to:
- Offload simple tasks to free local models (summarization, extraction, classification, rewriting, first-pass code review, brainstorming)
- Use specialized model capabilities (vision models for images, smaller models for quick tasks, larger models for complex reasoning)
- Save paid API tokens by using local models when quality is sufficient
- Process tasks locally for privacy

Example: "Use lmstudio-subagents to summarize this document"

## How It Works

1. Lists available models via GET /api/v1/models
2. Optionally checks loaded_instances or runs scripts/load.mjs for explicit load
3. Selects model by key and capabilities (vision, embedding, context)
4. Calls POST /api/v1/chat via lmstudio-api.mjs (JIT loads model if needed)
5. Parses output and optional response_id for stateful follow-up (--stateful persists id)
6. Optionally runs scripts/unload.mjs or uses --unload-after on chat

## Performance

Tested with LM Studio 0.4.x. JIT first-request load time in response stats.model_load_time_seconds. API call latency varies with generation length. Run scripts/test.mjs to verify setup.

## License

MIT License - See [LICENSE](LICENSE) file for details.

## Contributing

Contributions welcome! Please open an issue or pull request.

## Links

- [GitHub Repository](https://github.com/t-sinclair2500/clawdbot_skills) - Source code and issues
- [ClawdHub](https://clawdhub.com) - Browse and install skills
- [OpenClaw Documentation](https://docs.openclaw.ai) - Learn more about OpenClaw
- [LM Studio](https://lmstudio.ai) - Download LM Studio


path: scripts/test.mjs
content: #!/usr/bin/env node

/**
 * Smoke test: load one model, run one chat, unload. Exit 0 if all succeed.
 * Usage: node scripts/test.mjs [model_key] [--api-url=...]
 * If model_key omitted, uses first model from GET /api/v1/models.
 */

const BASE_URL = process.env.LM_STUDIO_API_URL || 'http://127.0.0.1:1234';

async function getModels(apiUrl) {
  const url = `${apiUrl.replace(/\/$/, '')}/api/v1/models`;
  const res = await fetch(url, { headers: { 'Authorization': 'Bearer lmstudio' } });
  if (!res.ok) throw new Error(`GET models failed: ${res.status}`);
  return res.json();
}

async function loadModel(model, apiUrl) {
  const url = `${apiUrl.replace(/\/$/, '')}/api/v1/models/load`;
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer lmstudio' },
    body: JSON.stringify({ model })
  });
  if (!res.ok) {
    const d = await res.json().catch(() => ({}));
    throw new Error(d.error?.message || d.message || `Load failed: ${res.status}`);
  }
}

async function chat(model, apiUrl) {
  const url = `${apiUrl.replace(/\/$/, '')}/api/v1/chat`;
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer lmstudio' },
    body: JSON.stringify({
      model,
      input: 'Reply with exactly: OK',
      store: true,
      temperature: 0,
      max_output_tokens: 10
    })
  });
  if (!res.ok) {
    const d = await res.json().catch(() => ({}));
    throw new Error(d.error?.message || d.message || `Chat failed: ${res.status}`);
  }
  const data = await res.json();
  if (!data.output || !Array.isArray(data.output)) throw new Error('Invalid chat response');
  return data.model_instance_id;
}

async function unloadInstance(instanceId, apiUrl) {
  const url = `${apiUrl.replace(/\/$/, '')}/api/v1/models/unload`;
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer lmstudio' },
    body: JSON.stringify({ instance_id: instanceId })
  });
  if (!res.ok) {
    const d = await res.json().catch(() => ({}));
    throw new Error(d.error?.message || d.message || `Unload failed: ${res.status}`);
  }
}

async function run() {
  const args = process.argv.slice(2);
  let apiUrl = BASE_URL;
  let modelKey = process.env.MODEL || null;
  args.forEach(arg => {
    if (arg.startsWith('--api-url=')) apiUrl = arg.split('=')[1];
    else if (!arg.startsWith('--')) modelKey = arg;
  });

  const data = await getModels(apiUrl);
  const models = data.models || [];
  if (models.length === 0) {
    console.error('No models found');
    process.exit(1);
  }
  const model = modelKey || models[0].key;
  const found = models.find(m => m.key === model);
  if (!found) {
    console.error(`Model not found: ${model}`);
    process.exit(1);
  }

  await loadModel(model, apiUrl);
  const instanceId = await chat(model, apiUrl);
  if (instanceId) await unloadInstance(instanceId, apiUrl);
  console.log('OK');
}

run().catch(err => {
  console.error(err.message);
  process.exit(1);
});


path: scripts/unload.mjs
content: #!/usr/bin/env node

/**
 * Unload all instances for a model key via LM Studio GET models + POST unload.
 * instance_id for unload must be from loaded_instances[].id (List response), not the model key.
 * Usage: node scripts/unload.mjs <model_key> [--api-url=http://127.0.0.1:1234]
 */

const BASE_URL = process.env.LM_STUDIO_API_URL || 'http://127.0.0.1:1234';

async function getModels(apiUrl) {
  const url = `${apiUrl.replace(/\/$/, '')}/api/v1/models`;
  const res = await fetch(url, {
    headers: { 'Authorization': 'Bearer lmstudio' }
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`GET models failed: ${res.status} ${text}`);
  }
  return res.json();
}

async function unloadInstance(instanceId, apiUrl) {
  const url = `${apiUrl.replace(/\/$/, '')}/api/v1/models/unload`;
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer lmstudio'
    },
    body: JSON.stringify({ instance_id: instanceId })
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    const msg = data.error?.message || data.message || `HTTP ${res.status}`;
    throw new Error(msg);
  }
}

async function unloadModel(modelKey, apiUrl = BASE_URL) {
  const data = await getModels(apiUrl);
  const model = data.models?.find(m => m.key === modelKey);
  if (!model) {
    console.error(JSON.stringify({ error: `Model not found: ${modelKey}`, type: 'model_not_found' }));
    process.exit(1);
  }
  const instances = model.loaded_instances || [];
  if (instances.length === 0) {
    return;
  }
  for (const inst of instances) {
    const id = inst.id ?? inst;
    await unloadInstance(id, apiUrl);
  }
}

const [modelKey, ...args] = process.argv.slice(2);
let apiUrl = BASE_URL;
args.forEach(arg => {
  if (arg.startsWith('--api-url=')) apiUrl = arg.split('=')[1];
});

if (!modelKey) {
  console.error('Usage: node scripts/unload.mjs <model_key> [--api-url=...]');
  process.exit(1);
}

unloadModel(modelKey, apiUrl).then(() => process.exit(0)).catch(err => {
  console.error(JSON.stringify({ error: err.message, type: 'unload_error' }));
  process.exit(1);
});


path: scripts/load.mjs
content: #!/usr/bin/env node

/**
 * Load a model by key via LM Studio POST /api/v1/models/load
 * Usage: node scripts/load.mjs <model> [--api-url=http://127.0.0.1:1234]
 */

const BASE_URL = process.env.LM_STUDIO_API_URL || 'http://127.0.0.1:1234';

async function loadModel(model, apiUrl = BASE_URL) {
  const url = `${apiUrl.replace(/\/$/, '')}/api/v1/models/load`;
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer lmstudio'
    },
    body: JSON.stringify({ model })
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    const msg = data.error?.message || data.message || `HTTP ${res.status}`;
    console.error(JSON.stringify({ error: msg, type: 'load_error' }));
    process.exit(1);
  }
  return data;
}

const [model, ...args] = process.argv.slice(2);
let apiUrl = BASE_URL;
args.forEach(arg => {
  if (arg.startsWith('--api-url=')) apiUrl = arg.split('=')[1];
});

if (!model) {
  console.error('Usage: node scripts/load.mjs <model> [--api-url=...]');
  process.exit(1);
}

loadModel(model, apiUrl).then(() => process.exit(0)).catch(err => {
  console.error(JSON.stringify({ error: err.message, type: 'load_error' }));
  process.exit(1);
});


path: scripts/lmstudio-api.mjs
content: #!/usr/bin/env node

/**
 * LM Studio v1 REST API (POST /api/v1/chat)
 * Usage: node scripts/lmstudio-api.mjs <model> '<task>' [--temperature=0.7] [--max-output-tokens=2000] [--previous-response-id=...] [--stateful] [--log=path] [--unload-after] [--keep] [--api-url=...]
 */

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const BASE_URL = process.env.LM_STUDIO_API_URL || 'http://127.0.0.1:1234';
const STATE_FILE = path.join(process.cwd(), '.lmstudio-state');
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 1000;

function sleep(ms) {
  return new Promise(r => setTimeout(r, ms));
}

async function unloadInstance(instanceId, apiUrl) {
  const url = `${apiUrl.replace(/\/$/, '')}/api/v1/models/unload`;
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer lmstudio'
    },
    body: JSON.stringify({ instance_id: instanceId })
  });
  if (!res.ok) {
    const data = await res.json().catch(() => ({}));
    const msg = data.error?.message || data.message || `HTTP ${res.status}`;
    console.error(JSON.stringify({ error: `Unload failed: ${msg}`, type: 'unload_error' }));
  }
}

async function callLMStudioAPI(model, taskContent, options = {}) {
  const {
    temperature = 0.7,
    maxOutputTokens = 2000,
    previousResponseId = null,
    apiUrl = BASE_URL,
    logPath = null,
    unloadAfter = false,
    keep = false
  } = options;

  const url = `${apiUrl.replace(/\/$/, '')}/api/v1/chat`;
  const payload = {
    model,
    input: taskContent,
    store: true,
    temperature: parseFloat(temperature),
    max_output_tokens: parseInt(maxOutputTokens)
  };
  if (previousResponseId) payload.previous_response_id = previousResponseId;

  let lastError = null;
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
    try {
      if (logPath) {
        fs.writeFileSync(logPath, JSON.stringify({ request: payload, attempt }, null, 2) + '\n', { flag: 'a' });
      }
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer lmstudio'
        },
        body: JSON.stringify(payload)
      });

      const data = await response.json();

      if (logPath) {
        fs.writeFileSync(logPath, JSON.stringify({ response: data, status: response.status }, null, 2) + '\n', { flag: 'a' });
      }

      if (!response.ok) {
        const errorMsg = data.error?.message || data.message || `HTTP ${response.status}`;
        const code = response.status;
        if (code === 404 || (errorMsg && /model|not found/i.test(errorMsg))) {
          console.error(JSON.stringify({ error: 'Model not found', detail: errorMsg, type: 'model_not_found' }));
          process.exit(1);
        }
        if (attempt < MAX_RETRIES && code >= 500) {
          lastError = errorMsg;
          await sleep(RETRY_DELAY_MS * attempt);
          continue;
        }
        console.error(JSON.stringify({ error: errorMsg, type: data.error?.type || 'api_error' }));
        process.exit(1);
      }

      if (!data.output || !Array.isArray(data.output)) {
        console.error(JSON.stringify({ error: 'Invalid API response structure', data }));
        process.exit(1);
      }

      const messages = data.output
        .filter(item => item.type === 'message')
        .map(item => item.content)
        .join('')
        .trim();
      const reasoning = data.output
        .filter(item => item.type === 'reasoning')
        .map(item => item.content)
        .join('')
        .trim();
      const content = (messages && messages.length > 0) ? messages : (reasoning || '');

      const result = {
        content,
        content_source: (messages && messages.length > 0) ? 'message' : 'reasoning',
        model_instance_id: data.model_instance_id || null,
        response_id: data.response_id || null,
        usage: data.stats ? {
          input_tokens: data.stats.input_tokens,
          total_output_tokens: data.stats.total_output_tokens,
          model_load_time_seconds: data.stats.model_load_time_seconds
        } : null
      };

      if (unloadAfter && !keep && result.model_instance_id) {
        await unloadInstance(result.model_instance_id, apiUrl);
      }

      return result;
    } catch (err) {
      lastError = err.message;
      const isNetwork = /fetch|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network/i.test(err.message);
      if (isNetwork && attempt < MAX_RETRIES) {
        await sleep(RETRY_DELAY_MS * attempt);
        continue;
      }
      if (isNetwork) {
        console.error(JSON.stringify({ error: 'Server unreachable (is LM Studio running?)', detail: err.message, type: 'network_error' }));
      } else {
        console.error(JSON.stringify({ error: err.message, type: 'unexpected_error' }));
      }
      process.exit(1);
    }
  }
  console.error(JSON.stringify({ error: lastError || 'Request failed after retries', type: 'api_error' }));
  process.exit(1);
}

function readState() {
  try {
    const raw = fs.readFileSync(STATE_FILE, 'utf8');
    return JSON.parse(raw);
  } catch {
    return null;
  }
}

function writeState(model, responseId) {
  try {
    fs.writeFileSync(STATE_FILE, JSON.stringify({ model, response_id: responseId }));
  } catch (err) {
    console.error(JSON.stringify({ error: `Could not write state: ${err.message}`, type: 'state_error' }));
  }
}

const __filename = fileURLToPath(import.meta.url);
if (process.argv[1] && (path.resolve(process.cwd(), process.argv[1]) === __filename || process.argv[1] === __filename)) {
  const [model, taskContent, ...args] = process.argv.slice(2);

  if (!model || !taskContent) {
    console.error('Usage: node scripts/lmstudio-api.mjs <model> \'<task>\' [--temperature=0.7] [--max-output-tokens=2000] [--previous-response-id=...] [--stateful] [--log=path] [--unload-after] [--keep] [--api-url=...]');
    process.exit(1);
  }

  const options = { apiUrl: BASE_URL };
  let stateful = false;
  let unloadAfter = false;
  let keep = false;

  args.forEach(arg => {
    if (arg.startsWith('--temperature=')) options.temperature = arg.split('=')[1];
    else if (arg.startsWith('--max-output-tokens=')) options.maxOutputTokens = arg.split('=')[1];
    else if (arg.startsWith('--previous-response-id=')) options.previousResponseId = arg.split('=')[1];
    else if (arg === '--stateful') stateful = true;
    else if (arg.startsWith('--log=')) options.logPath = arg.slice(6);
    else if (arg === '--unload-after') unloadAfter = true;
    else if (arg === '--keep') keep = true;
    else if (arg.startsWith('--api-url=')) options.apiUrl = arg.split('=')[1];
  });

  options.unloadAfter = unloadAfter;
  options.keep = keep;

  if (stateful && !options.previousResponseId) {
    const state = readState();
    if (state?.response_id && (state.model == null || state.model === model)) {
      options.previousResponseId = state.response_id;
    }
  }

  callLMStudioAPI(model, taskContent, options).then(result => {
    if (stateful && result.response_id) {
      writeState(model, result.response_id);
    }
    console.log(JSON.stringify(result));
  }).catch(err => {
    console.error(JSON.stringify({ error: err.message, type: 'unexpected_error' }));
    process.exit(1);
  });
}

export { callLMStudioAPI };


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)