mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-12 18:11:51 +08:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4243575fa0 | |||
| 6b27a0d11d | |||
| 5743a48f9b | |||
| 69348510e9 | |||
| 17a72938be | |||
| 4511644d0d | |||
| 86063e0ea0 | |||
| d1ce15a4de | |||
| 5b24dd4d2e | |||
| 0d8f7f8668 | |||
| fff32f3028 | |||
| 1bb065e655 | |||
| ea7561a978 | |||
| cfc4b6c28e | |||
| dad98b0a8f | |||
| 3e41e54e10 | |||
| 4d9f0288ee | |||
| 972edd14f6 | |||
| fd59ff0ec9 | |||
| e2e32219c9 | |||
| c601aaa947 | |||
| d43d53244f | |||
| e8326bae62 | |||
| d71ffaf7ef | |||
| 5b5ee91aa7 | |||
| 2007471f4f | |||
| 9e90c0712e | |||
| 2317302745 | |||
| b247357e0d | |||
| 4dd27adb84 | |||
| cc4f03a203 | |||
| 4bc232e513 | |||
| c9d1569702 | |||
| 5b20e2908a | |||
| 089fcea474 | |||
| bd64fd667d | |||
| f0ac7fbb6d | |||
| 207addfa19 | |||
| 4afb5bd9f1 | |||
| dfcdfcac11 | |||
| d33e514d04 | |||
| 4cb13b2b60 |
@@ -0,0 +1 @@
|
|||||||
|
OLLAMA_HOST=
|
||||||
|
|||||||
@@ -409,7 +409,8 @@ async function runQuery(
|
|||||||
'TeamCreate', 'TeamDelete', 'SendMessage',
|
'TeamCreate', 'TeamDelete', 'SendMessage',
|
||||||
'TodoWrite', 'ToolSearch', 'Skill',
|
'TodoWrite', 'ToolSearch', 'Skill',
|
||||||
'NotebookEdit',
|
'NotebookEdit',
|
||||||
'mcp__nanoclaw__*'
|
'mcp__nanoclaw__*',
|
||||||
|
'mcp__ollama__*'
|
||||||
],
|
],
|
||||||
env: sdkEnv,
|
env: sdkEnv,
|
||||||
permissionMode: 'bypassPermissions',
|
permissionMode: 'bypassPermissions',
|
||||||
@@ -425,6 +426,10 @@ async function runQuery(
|
|||||||
NANOCLAW_IS_MAIN: containerInput.isMain ? '1' : '0',
|
NANOCLAW_IS_MAIN: containerInput.isMain ? '1' : '0',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ollama: {
|
||||||
|
command: 'node',
|
||||||
|
args: [path.join(path.dirname(mcpServerPath), 'ollama-mcp-stdio.js')],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
PreCompact: [{ hooks: [createPreCompactHook(containerInput.assistantName)] }],
|
PreCompact: [{ hooks: [createPreCompactHook(containerInput.assistantName)] }],
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
/**
|
||||||
|
* Ollama MCP Server for NanoClaw
|
||||||
|
* Exposes local Ollama models as tools for the container agent.
|
||||||
|
* Uses host.docker.internal to reach the host's Ollama instance from Docker.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const OLLAMA_HOST = process.env.OLLAMA_HOST || 'http://host.docker.internal:11434';
|
||||||
|
const OLLAMA_ADMIN_TOOLS = process.env.OLLAMA_ADMIN_TOOLS === 'true';
|
||||||
|
const OLLAMA_STATUS_FILE = '/workspace/ipc/ollama_status.json';
|
||||||
|
|
||||||
|
function log(msg: string): void {
|
||||||
|
console.error(`[OLLAMA] ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStatus(status: string, detail?: string): void {
|
||||||
|
try {
|
||||||
|
const data = { status, detail, timestamp: new Date().toISOString() };
|
||||||
|
const tmpPath = `${OLLAMA_STATUS_FILE}.tmp`;
|
||||||
|
fs.mkdirSync(path.dirname(OLLAMA_STATUS_FILE), { recursive: true });
|
||||||
|
fs.writeFileSync(tmpPath, JSON.stringify(data));
|
||||||
|
fs.renameSync(tmpPath, OLLAMA_STATUS_FILE);
|
||||||
|
} catch { /* best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ollamaFetch(path: string, options?: RequestInit): Promise<Response> {
|
||||||
|
const url = `${OLLAMA_HOST}${path}`;
|
||||||
|
try {
|
||||||
|
return await fetch(url, options);
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback to localhost if host.docker.internal fails
|
||||||
|
if (OLLAMA_HOST.includes('host.docker.internal')) {
|
||||||
|
const fallbackUrl = url.replace('host.docker.internal', 'localhost');
|
||||||
|
return await fetch(fallbackUrl, options);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new McpServer({
|
||||||
|
name: 'ollama',
|
||||||
|
version: '1.0.0',
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
'ollama_list_models',
|
||||||
|
'List all locally installed Ollama models. Use this to see which models are available before calling ollama_generate.',
|
||||||
|
{},
|
||||||
|
async () => {
|
||||||
|
log('Listing models...');
|
||||||
|
writeStatus('listing', 'Listing available models');
|
||||||
|
try {
|
||||||
|
const res = await ollamaFetch('/api/tags');
|
||||||
|
if (!res.ok) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Ollama API error: ${res.status} ${res.statusText}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json() as { models?: Array<{ name: string; size: number; modified_at: string }> };
|
||||||
|
const models = data.models || [];
|
||||||
|
|
||||||
|
if (models.length === 0) {
|
||||||
|
return { content: [{ type: 'text' as const, text: 'No models installed. Run `ollama pull <model>` on the host to install one.' }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = models
|
||||||
|
.map(m => `- ${m.name} (${(m.size / 1e9).toFixed(1)}GB)`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
log(`Found ${models.length} models`);
|
||||||
|
return { content: [{ type: 'text' as const, text: `Installed models:\n${list}` }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Failed to connect to Ollama at ${OLLAMA_HOST}: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
'ollama_generate',
|
||||||
|
'Send a prompt to a local Ollama model and get a response. Good for cheaper/faster tasks like summarization, translation, or general queries. Use ollama_list_models first to see available models.',
|
||||||
|
{
|
||||||
|
model: z.string().describe('The model name (e.g., "llama3.2", "mistral", "gemma2")'),
|
||||||
|
prompt: z.string().describe('The prompt to send to the model'),
|
||||||
|
system: z.string().optional().describe('Optional system prompt to set model behavior'),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
log(`>>> Generating with ${args.model} (${args.prompt.length} chars)...`);
|
||||||
|
writeStatus('generating', `Generating with ${args.model}`);
|
||||||
|
try {
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
model: args.model,
|
||||||
|
prompt: args.prompt,
|
||||||
|
stream: false,
|
||||||
|
};
|
||||||
|
if (args.system) {
|
||||||
|
body.system = args.system;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ollamaFetch('/api/generate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Ollama error (${res.status}): ${errorText}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json() as { response: string; total_duration?: number; eval_count?: number };
|
||||||
|
|
||||||
|
let meta = '';
|
||||||
|
if (data.total_duration) {
|
||||||
|
const secs = (data.total_duration / 1e9).toFixed(1);
|
||||||
|
meta = `\n\n[${args.model} | ${secs}s${data.eval_count ? ` | ${data.eval_count} tokens` : ''}]`;
|
||||||
|
log(`<<< Done: ${args.model} | ${secs}s | ${data.eval_count || '?'} tokens | ${data.response.length} chars`);
|
||||||
|
writeStatus('done', `${args.model} | ${secs}s | ${data.eval_count || '?'} tokens`);
|
||||||
|
} else {
|
||||||
|
log(`<<< Done: ${args.model} | ${data.response.length} chars`);
|
||||||
|
writeStatus('done', `${args.model} | ${data.response.length} chars`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content: [{ type: 'text' as const, text: data.response + meta }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Failed to call Ollama: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Management tools — only registered when OLLAMA_ADMIN_TOOLS=true
|
||||||
|
if (OLLAMA_ADMIN_TOOLS) {
|
||||||
|
server.tool(
|
||||||
|
'ollama_pull_model',
|
||||||
|
'Pull (download) a model from the Ollama registry by name. Returns the final status once the pull is complete. Use model names like "llama3.2", "mistral", "gemma2:9b".',
|
||||||
|
{
|
||||||
|
model: z.string().describe('Model name to pull, e.g. "llama3.2", "mistral", "gemma2:9b"'),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
log(`Pulling model: ${args.model}...`);
|
||||||
|
writeStatus('pulling', `Pulling ${args.model}`);
|
||||||
|
try {
|
||||||
|
const res = await ollamaFetch('/api/pull', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ model: args.model, stream: false }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Ollama error (${res.status}): ${errorText}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const data = await res.json() as { status: string };
|
||||||
|
log(`Pull complete: ${args.model} — ${data.status}`);
|
||||||
|
writeStatus('done', `Pulled ${args.model}`);
|
||||||
|
return { content: [{ type: 'text' as const, text: `Pull complete: ${args.model} — ${data.status}` }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Failed to pull model: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
'ollama_delete_model',
|
||||||
|
'Delete a locally installed Ollama model to free up disk space.',
|
||||||
|
{
|
||||||
|
model: z.string().describe('Model name to delete, e.g. "llama3.2", "mistral:latest"'),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
log(`Deleting model: ${args.model}...`);
|
||||||
|
writeStatus('deleting', `Deleting ${args.model}`);
|
||||||
|
try {
|
||||||
|
const res = await ollamaFetch('/api/delete', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ model: args.model }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Ollama error (${res.status}): ${errorText}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
log(`Deleted: ${args.model}`);
|
||||||
|
writeStatus('done', `Deleted ${args.model}`);
|
||||||
|
return { content: [{ type: 'text' as const, text: `Deleted model: ${args.model}` }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Failed to delete model: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
'ollama_show_model',
|
||||||
|
'Show details for a locally installed Ollama model: modelfile, parameters, template, system prompt, and architecture info.',
|
||||||
|
{
|
||||||
|
model: z.string().describe('Model name to inspect, e.g. "llama3.2", "mistral:latest"'),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
log(`Showing model info: ${args.model}...`);
|
||||||
|
try {
|
||||||
|
const res = await ollamaFetch('/api/show', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ model: args.model }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Ollama error (${res.status}): ${errorText}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Failed to show model info: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
'ollama_list_running',
|
||||||
|
'List Ollama models currently loaded in memory with their memory usage, processor type (CPU/GPU), and time until they are unloaded.',
|
||||||
|
{},
|
||||||
|
async () => {
|
||||||
|
log('Listing running models...');
|
||||||
|
try {
|
||||||
|
const res = await ollamaFetch('/api/ps');
|
||||||
|
if (!res.ok) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Ollama API error: ${res.status} ${res.statusText}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const data = await res.json() as { models?: Array<{ name: string; size: number; size_vram: number; processor: string; expires_at: string }> };
|
||||||
|
const models = data.models || [];
|
||||||
|
if (models.length === 0) {
|
||||||
|
return { content: [{ type: 'text' as const, text: 'No models currently loaded in memory.' }] };
|
||||||
|
}
|
||||||
|
const list = models
|
||||||
|
.map(m => {
|
||||||
|
const size = m.size_vram > 0 ? m.size_vram : m.size;
|
||||||
|
return `- ${m.name} (${(size / 1e9).toFixed(1)}GB ${m.processor}, unloads at ${m.expires_at})`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
log(`${models.length} model(s) running`);
|
||||||
|
return { content: [{ type: 'text' as const, text: `Models loaded in memory:\n${list}` }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Failed to list running models: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
log('Admin tools enabled (pull, delete, show, list-running)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Watch NanoClaw IPC for Ollama activity and show macOS notifications
|
||||||
|
# Usage: ./scripts/ollama-watch.sh
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
|
||||||
|
echo "Watching for Ollama activity..."
|
||||||
|
echo "Press Ctrl+C to stop"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
LAST_TIMESTAMP=""
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
# Check all group IPC dirs for ollama_status.json
|
||||||
|
for status_file in data/ipc/*/ollama_status.json; do
|
||||||
|
[ -f "$status_file" ] || continue
|
||||||
|
|
||||||
|
TIMESTAMP=$(python3 -c "import json; print(json.load(open('$status_file'))['timestamp'])" 2>/dev/null)
|
||||||
|
[ -z "$TIMESTAMP" ] && continue
|
||||||
|
[ "$TIMESTAMP" = "$LAST_TIMESTAMP" ] && continue
|
||||||
|
|
||||||
|
LAST_TIMESTAMP="$TIMESTAMP"
|
||||||
|
STATUS=$(python3 -c "import json; d=json.load(open('$status_file')); print(d['status'])" 2>/dev/null)
|
||||||
|
DETAIL=$(python3 -c "import json; d=json.load(open('$status_file')); print(d.get('detail',''))" 2>/dev/null)
|
||||||
|
|
||||||
|
case "$STATUS" in
|
||||||
|
generating)
|
||||||
|
osascript -e "display notification \"$DETAIL\" with title \"NanoClaw → Ollama\" sound name \"Submarine\"" 2>/dev/null
|
||||||
|
echo "$(date +%H:%M:%S) 🔄 $DETAIL"
|
||||||
|
;;
|
||||||
|
done)
|
||||||
|
osascript -e "display notification \"$DETAIL\" with title \"NanoClaw ← Ollama ✓\" sound name \"Glass\"" 2>/dev/null
|
||||||
|
echo "$(date +%H:%M:%S) ✅ $DETAIL"
|
||||||
|
;;
|
||||||
|
listing)
|
||||||
|
echo "$(date +%H:%M:%S) 📋 Listing models..."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
@@ -8,6 +8,7 @@ import { isValidTimezone } from './timezone.js';
|
|||||||
const envConfig = readEnvFile([
|
const envConfig = readEnvFile([
|
||||||
'ASSISTANT_NAME',
|
'ASSISTANT_NAME',
|
||||||
'ASSISTANT_HAS_OWN_NUMBER',
|
'ASSISTANT_HAS_OWN_NUMBER',
|
||||||
|
'OLLAMA_ADMIN_TOOLS',
|
||||||
'ONECLI_URL',
|
'ONECLI_URL',
|
||||||
'TZ',
|
'TZ',
|
||||||
]);
|
]);
|
||||||
@@ -17,6 +18,8 @@ export const ASSISTANT_NAME =
|
|||||||
export const ASSISTANT_HAS_OWN_NUMBER =
|
export const ASSISTANT_HAS_OWN_NUMBER =
|
||||||
(process.env.ASSISTANT_HAS_OWN_NUMBER ||
|
(process.env.ASSISTANT_HAS_OWN_NUMBER ||
|
||||||
envConfig.ASSISTANT_HAS_OWN_NUMBER) === 'true';
|
envConfig.ASSISTANT_HAS_OWN_NUMBER) === 'true';
|
||||||
|
export const OLLAMA_ADMIN_TOOLS =
|
||||||
|
(process.env.OLLAMA_ADMIN_TOOLS || envConfig.OLLAMA_ADMIN_TOOLS) === 'true';
|
||||||
export const POLL_INTERVAL = 2000;
|
export const POLL_INTERVAL = 2000;
|
||||||
export const SCHEDULER_POLL_INTERVAL = 60000;
|
export const SCHEDULER_POLL_INTERVAL = 60000;
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -13,6 +13,7 @@ import {
|
|||||||
DATA_DIR,
|
DATA_DIR,
|
||||||
GROUPS_DIR,
|
GROUPS_DIR,
|
||||||
IDLE_TIMEOUT,
|
IDLE_TIMEOUT,
|
||||||
|
OLLAMA_ADMIN_TOOLS,
|
||||||
ONECLI_URL,
|
ONECLI_URL,
|
||||||
TIMEZONE,
|
TIMEZONE,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
@@ -233,6 +234,11 @@ async function buildContainerArgs(
|
|||||||
// Pass host timezone so container's local time matches the user's
|
// Pass host timezone so container's local time matches the user's
|
||||||
args.push('-e', `TZ=${TIMEZONE}`);
|
args.push('-e', `TZ=${TIMEZONE}`);
|
||||||
|
|
||||||
|
// Forward Ollama admin tools flag if enabled
|
||||||
|
if (OLLAMA_ADMIN_TOOLS) {
|
||||||
|
args.push('-e', 'OLLAMA_ADMIN_TOOLS=true');
|
||||||
|
}
|
||||||
|
|
||||||
// OneCLI gateway handles credential injection — containers never see real secrets.
|
// OneCLI gateway handles credential injection — containers never see real secrets.
|
||||||
// The gateway intercepts HTTPS traffic and injects API keys or OAuth tokens.
|
// The gateway intercepts HTTPS traffic and injects API keys or OAuth tokens.
|
||||||
const onecliApplied = await onecli.applyContainerConfig(args, {
|
const onecliApplied = await onecli.applyContainerConfig(args, {
|
||||||
@@ -400,7 +406,12 @@ export async function runContainerAgent(
|
|||||||
const chunk = data.toString();
|
const chunk = data.toString();
|
||||||
const lines = chunk.trim().split('\n');
|
const lines = chunk.trim().split('\n');
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line) logger.debug({ container: group.folder }, line);
|
if (!line) continue;
|
||||||
|
if (line.includes('[OLLAMA]')) {
|
||||||
|
logger.info({ container: group.folder }, line);
|
||||||
|
} else {
|
||||||
|
logger.debug({ container: group.folder }, line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Don't reset timeout on stderr — SDK writes debug logs continuously.
|
// Don't reset timeout on stderr — SDK writes debug logs continuously.
|
||||||
// Timeout only resets on actual output (OUTPUT_MARKER in stdout).
|
// Timeout only resets on actual output (OUTPUT_MARKER in stdout).
|
||||||
|
|||||||
Reference in New Issue
Block a user