gigaclaw 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +51 -14
  2. package/api/index.js +13 -0
  3. package/bin/cli.js +19 -7
  4. package/drizzle/0004_trust_ledger_audit_log.sql +15 -0
  5. package/drizzle/meta/_journal.json +8 -1
  6. package/lib/ai/agent.js +40 -16
  7. package/lib/ai/index.js +100 -2
  8. package/lib/ai/provider-health.js +96 -0
  9. package/lib/ai/task-router.js +177 -0
  10. package/lib/chat/api.js +3 -1
  11. package/lib/chat/components/app-sidebar.js +15 -1
  12. package/lib/chat/components/app-sidebar.jsx +19 -1
  13. package/lib/chat/components/icons.js +40 -0
  14. package/lib/chat/components/icons.jsx +38 -0
  15. package/lib/chat/components/index.js +1 -0
  16. package/lib/chat/components/trust-ledger-page.js +408 -0
  17. package/lib/chat/components/trust-ledger-page.jsx +528 -0
  18. package/lib/chat/trust-ledger-actions.js +61 -0
  19. package/lib/code/ws-proxy.js +14 -6
  20. package/lib/db/api-keys.js +5 -4
  21. package/lib/db/audit-log.js +346 -0
  22. package/lib/db/schema.js +12 -0
  23. package/package.json +64 -20
  24. package/setup/lib/providers.mjs +2 -2
  25. package/setup/setup-hybrid.mjs +399 -0
  26. package/setup/setup-local.mjs +9 -9
  27. package/setup/setup.mjs +17 -9
  28. package/templates/.env.example +15 -5
  29. package/templates/CLAUDE.md.template +12 -12
  30. package/templates/app/globals.css +1 -1
  31. package/templates/app/layout.js +5 -5
  32. package/templates/app/trust-ledger/page.js +6 -0
  33. package/templates/config/SOUL.md +3 -3
  34. package/templates/docker/event-handler/Dockerfile +1 -1
  35. package/templates/middleware.js +1 -1
  36. package/templates/next.config.mjs +23 -2
  37. package/templates/skills/README.md +7 -7
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- # Giga Bot
3
+ # GigaClaw
4
4
 
5
5
  ### Autonomous AI Agent Platform — Powered by Gignaati
6
6
 
@@ -18,16 +18,16 @@ India-first. Edge-native. Zero vendor lock-in.
18
18
 
19
19
  ---
20
20
 
21
- ## What is Giga Bot?
21
+ ## What is GigaClaw?
22
22
 
23
- Giga Bot is a self-hosted, autonomous AI agent platform. You deploy it to your own server or VPS, and it runs 24/7 — responding to messages, executing scheduled jobs, handling webhooks, writing code, managing files, and completing complex multi-step tasks.
23
+ GigaClaw is a self-hosted, autonomous AI agent platform. You deploy it to your own server or VPS, and it runs 24/7 — responding to messages, executing scheduled jobs, handling webhooks, writing code, managing files, and completing complex multi-step tasks.
24
24
 
25
25
  It is built on a two-layer architecture:
26
26
 
27
27
  - **Event Handler** — A Next.js server that handles real-time chat (web UI + Telegram), manages your agent's configuration, and creates jobs for the agent to execute.
28
28
  - **Agent Engine** — A Docker container that runs your agent jobs using GitHub Actions or a local Docker daemon. The agent can write code, run shell commands, browse the web, and interact with GitHub.
29
29
 
30
- Giga Bot is the only autonomous agent platform with **native PragatiGPT support** — India's indigenous Small Language Model for edge deployment, delivering 100% data privacy and zero foreign cloud dependency.
30
+ GigaClaw is the only autonomous agent platform with **native PragatiGPT support** — India's indigenous Small Language Model for edge deployment, delivering 100% data privacy and zero foreign cloud dependency.
31
31
 
32
32
  ---
33
33
 
@@ -45,7 +45,7 @@ irm https://raw.githubusercontent.com/gignaati/gigaclaw/main/install.ps1 | iex
45
45
 
46
46
  ### All Platforms (npm / npx)
47
47
  ```bash
48
- # Create a new Giga Bot project
48
+ # Create a new GigaClaw project
49
49
  mkdir my-gigaclaw && cd my-gigaclaw
50
50
  npx gigaclaw@latest init
51
51
 
@@ -61,7 +61,7 @@ npm run setup
61
61
 
62
62
  **Step 1 — Create a new GitHub repository** for your agent (e.g., `my-gigaclaw`).
63
63
 
64
- **Step 2 — Install Giga Bot** into a local folder with the same name:
64
+ **Step 2 — Install GigaClaw** into a local folder with the same name:
65
65
  ```bash
66
66
  mkdir my-gigaclaw && cd my-gigaclaw
67
67
  npx gigaclaw@latest init
@@ -72,10 +72,10 @@ npm install
72
72
  ```bash
73
73
  npm run setup
74
74
  ```
75
- The wizard will ask for:
76
- - Your GitHub Personal Access Token
77
- - Your public URL (domain or ngrok URL)
78
- - Your LLM provider and API key (Claude, GPT, Gemini, PragatiGPT, or Ollama)
75
+ The wizard will ask for your setup mode:
76
+ - **Hybrid** (recommended) Cloud + Local AI with smart routing
77
+ - **Cloud** GitHub + ngrok + Telegram, full features
78
+ - **Local** Ollama only, 100% offline
79
79
 
80
80
  **Step 4 — Start your agent:**
81
81
  ```bash
@@ -88,7 +88,7 @@ docker compose up -d
88
88
 
89
89
  ## Supported LLM Providers
90
90
 
91
- Giga Bot supports **6 LLM providers** — more than any other self-hosted agent platform:
91
+ GigaClaw supports **6 LLM providers** — more than any other self-hosted agent platform:
92
92
 
93
93
  | Provider | Description | Data Privacy |
94
94
  |---|---|---|
@@ -135,7 +135,8 @@ LLM_PROVIDER=custom # Any OpenAI-compatible API
135
135
  - **Auto-merge** — Agent can merge its own PRs after review
136
136
  - **Hot reload** — Push to `main` triggers automatic rebuild and restart
137
137
 
138
- ### Giga Bot Exclusive Features
138
+ ### GigaClaw Exclusive Features
139
+ - **Hybrid Mode** — Cloud + Local AI with smart per-task routing (v1.6.0)
139
140
  - **PragatiGPT** — India's indigenous SLM for edge deployment
140
141
  - **Ollama** — Run any open-source model with zero cloud dependency
141
142
  - **Multi-LLM routing** — Different LLMs for chat vs. agent jobs
@@ -143,6 +144,42 @@ LLM_PROVIDER=custom # Any OpenAI-compatible API
143
144
 
144
145
  ---
145
146
 
147
+ ## Hybrid Mode (New in v1.6.0)
148
+
149
+ Run both cloud and local LLMs simultaneously. GigaClaw automatically routes each task to the best provider.
150
+
151
+ ```bash
152
+ npm run setup # Choose "Hybrid Mode" (recommended)
153
+ ```
154
+
155
+ ### Routing Strategies
156
+
157
+ | Strategy | Best for |
158
+ |----------|----------|
159
+ | **Auto** | Smart routing — complex tasks go to cloud, simple ones stay local |
160
+ | **Cost-Optimized** | Minimize API costs — local by default, cloud only when needed |
161
+ | **Quality-First** | Best output quality — cloud by default, local for drafts |
162
+ | **Privacy-First** | Maximum data privacy — local by default, cloud only for complex tasks |
163
+
164
+ ### How it works
165
+
166
+ 1. Setup configures a **cloud provider** (Claude, GPT, Gemini, PragatiGPT) and a **local provider** (Ollama)
167
+ 2. Each message is scored for complexity and privacy sensitivity
168
+ 3. The task router picks the optimal provider based on your chosen strategy
169
+ 4. Ollama availability is auto-detected at runtime — no reconfiguration needed
170
+
171
+ ```bash
172
+ # Example .env for hybrid mode
173
+ GIGACLAW_MODE=hybrid
174
+ LLM_PROVIDER=anthropic # Cloud (primary)
175
+ LLM_MODEL=claude-sonnet-4-6
176
+ LOCAL_LLM_PROVIDER=ollama # Local (secondary)
177
+ LOCAL_LLM_MODEL=llama3.2
178
+ HYBRID_ROUTING=auto # auto | cost-optimized | quality-first | privacy-first
179
+ ```
180
+
181
+ ---
182
+
146
183
  ## CLI Commands
147
184
 
148
185
  ```bash
@@ -162,7 +199,7 @@ npx gigaclaw set-var <KEY> [VALUE] # Set GitHub repository variable
162
199
 
163
200
  ## Configuration Files
164
201
 
165
- These files in `config/` define your agent's personality and behavior. They are **yours to customize** — Giga Bot will never overwrite them:
202
+ These files in `config/` define your agent's personality and behavior. They are **yours to customize** — GigaClaw will never overwrite them:
166
203
 
167
204
  | File | Purpose |
168
205
  |---|---|
@@ -187,7 +224,7 @@ npx gigaclaw upgrade 1.2.72 # Specific version
187
224
 
188
225
  ## Deployment
189
226
 
190
- Giga Bot runs on any Linux server with Docker. Recommended:
227
+ GigaClaw runs on any Linux server with Docker. Recommended:
191
228
 
192
229
  | Provider | Spec | Monthly Cost |
193
230
  |---|---|---|
package/api/index.js CHANGED
@@ -7,6 +7,7 @@ import { chat, summarizeJob } from '../lib/ai/index.js';
7
7
  import { createNotification } from '../lib/db/notifications.js';
8
8
  import { loadTriggers } from '../lib/triggers.js';
9
9
  import { verifyApiKey } from '../lib/db/api-keys.js';
10
+ import { logAction } from '../lib/db/audit-log.js';
10
11
 
11
12
  // Bot token from env, can be overridden by /telegram/register
12
13
  let telegramBotToken = null;
@@ -88,6 +89,18 @@ async function handleWebhook(request) {
88
89
 
89
90
  try {
90
91
  const result = await createJob(job);
92
+ // Audit log: record job creation via webhook
93
+ logAction({
94
+ actionType: 'job_create',
95
+ actor: 'api:webhook',
96
+ target: `job:${result.job_id || 'unknown'}`,
97
+ summary: `Job created via webhook — ${String(job).slice(0, 80)}`,
98
+ metadata: {
99
+ job_id: result.job_id,
100
+ title: result.title,
101
+ source: 'webhook',
102
+ },
103
+ });
91
104
  return Response.json(result);
92
105
  } catch (err) {
93
106
  console.error(err);
package/bin/cli.js CHANGED
@@ -25,6 +25,9 @@ if (command === '--version' || command === '-v') {
25
25
  const MANAGED_PATHS = [
26
26
  '.github/workflows/',
27
27
  'docker/event-handler/',
28
+ 'docker/claude-code-job/',
29
+ 'docker/claude-code-workspace/',
30
+ 'docker/pi-coding-agent-job/',
28
31
  'docker-compose.yml',
29
32
  'docker-compose.local.yml',
30
33
  '.dockerignore',
@@ -136,7 +139,7 @@ async function init() {
136
139
  if (deps.gigaclaw || devDeps.gigaclaw) {
137
140
  isExistingProject = true;
138
141
  }
139
- } catch {}
142
+ } catch (e) { console.warn(' Warning: could not parse existing package.json:', e.message); }
140
143
  }
141
144
 
142
145
  if (!isExistingProject) {
@@ -339,7 +342,7 @@ GIGACLAW_VERSION=${version}
339
342
  }
340
343
  fs.writeFileSync(envPath, envContent);
341
344
  console.log(` Updated GIGACLAW_VERSION to ${version}`);
342
- } catch {}
345
+ } catch (e) { console.warn(' Warning: could not update GIGACLAW_VERSION in .env:', e.message); }
343
346
  }
344
347
 
345
348
  console.log('\nDone! Run: npm run setup\n');
@@ -438,7 +441,7 @@ function diff(filePath) {
438
441
 
439
442
  try {
440
443
  // Use git diff for nice colored output, fall back to plain diff
441
- execSync(`git diff --no-index -- "${dest}" "${src}"`, { stdio: 'inherit', shell: true });
444
+ execFileSync('git', ['diff', '--no-index', '--', dest, src], { stdio: 'inherit' });
442
445
  console.log('\nFiles are identical.\n');
443
446
  } catch (e) {
444
447
  // git diff exits with 1 when files differ (output already printed)
@@ -470,7 +473,8 @@ function setup() {
470
473
  const setupScript = path.join(__dirname, '..', 'setup', 'setup.mjs');
471
474
  try {
472
475
  execFileSync(process.execPath, [setupScript], { stdio: 'inherit', cwd: process.cwd() });
473
- } catch {
476
+ } catch (e) {
477
+ console.error(e.message);
474
478
  process.exit(1);
475
479
  }
476
480
  }
@@ -479,7 +483,8 @@ function setupTelegram() {
479
483
  const setupScript = path.join(__dirname, '..', 'setup', 'setup-telegram.mjs');
480
484
  try {
481
485
  execFileSync(process.execPath, [setupScript], { stdio: 'inherit', cwd: process.cwd() });
482
- } catch {
486
+ } catch (e) {
487
+ console.error(e.message);
483
488
  process.exit(1);
484
489
  }
485
490
  }
@@ -505,6 +510,13 @@ async function resetAuth() {
505
510
  async function upgrade() {
506
511
  const cwd = process.cwd();
507
512
  const tag = parseUpgradeTarget(args[0]);
513
+
514
+ // Validate tag to prevent shell injection
515
+ if (!/^[a-zA-Z0-9._-]+$/.test(tag)) {
516
+ console.error(`\n Invalid version or tag: ${args[0]}\n`);
517
+ process.exit(1);
518
+ }
519
+
508
520
  const { confirm, isCancel } = await import('@clack/prompts');
509
521
 
510
522
  // --- Pre-flight: verify this is a gigaclaw project ---
@@ -553,7 +565,7 @@ async function upgrade() {
553
565
  execSync('git add -A && git commit -m "save local changes before gigaclaw upgrade"', { stdio: 'inherit', cwd, shell: true });
554
566
  } catch {
555
567
  console.error('\n Could not save your local changes. Please try again.\n');
556
- return;
568
+ process.exit(1);
557
569
  }
558
570
  }
559
571
 
@@ -569,7 +581,7 @@ async function upgrade() {
569
581
  console.error(' 2. Edit each file to keep the version you want');
570
582
  console.error(' 3. Run: git add -A && git rebase --continue');
571
583
  console.error(' 4. Then run the upgrade again\n');
572
- return;
584
+ process.exit(1);
573
585
  }
574
586
 
575
587
  // --- Install ---
@@ -0,0 +1,15 @@
1
+ CREATE TABLE `audit_log` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `timestamp` integer NOT NULL,
4
+ `action_type` text NOT NULL,
5
+ `actor` text NOT NULL,
6
+ `target` text NOT NULL,
7
+ `summary` text NOT NULL,
8
+ `metadata` text NOT NULL DEFAULT '{}',
9
+ `prev_hash` text NOT NULL,
10
+ `entry_hash` text NOT NULL
11
+ );
12
+ --> statement-breakpoint
13
+ CREATE INDEX `audit_log_timestamp_idx` ON `audit_log` (`timestamp`);
14
+ --> statement-breakpoint
15
+ CREATE INDEX `audit_log_action_type_idx` ON `audit_log` (`action_type`);
@@ -29,6 +29,13 @@
29
29
  "when": 1772486536207,
30
30
  "tag": "0003_rename_code_workspaces",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "6",
36
+ "when": 1742500800000,
37
+ "tag": "0004_trust_ledger_audit_log",
38
+ "breakpoints": true
32
39
  }
33
40
  ]
34
- }
41
+ }
package/lib/ai/agent.js CHANGED
@@ -7,16 +7,33 @@ import { jobPlanningMd, codePlanningMd, gigaclawDb } from '../paths.js';
7
7
  import { render_md } from '../utils/render-md.js';
8
8
  import { createWebSearchTool, getProvider } from './web-search.js';
9
9
 
10
- let _agent = null;
10
+ /** Cache agents by provider:model key so each combo is created once */
11
+ const _agentCache = new Map();
11
12
 
12
13
  /**
13
- * Get or create the LangGraph job agent singleton.
14
- * Uses createReactAgent which handles the tool loop automatically.
15
- * Prompt is a function so {{datetime}} resolves fresh each invocation.
14
+ * Build a cache key from provider overrides (falls back to env defaults).
16
15
  */
17
- export async function getJobAgent() {
18
- if (!_agent) {
19
- const model = await createModel();
16
+ function agentCacheKey(options = {}) {
17
+ const p = options.providerOverride || process.env.LLM_PROVIDER || 'anthropic';
18
+ const m = options.modelOverride || process.env.LLM_MODEL || 'default';
19
+ return `${p}:${m}`;
20
+ }
21
+
22
+ /**
23
+ * Get or create a LangGraph job agent.
24
+ * Supports per-request provider/model overrides for hybrid mode.
25
+ * Agents are cached by provider:model key.
26
+ *
27
+ * @param {object} [options]
28
+ * @param {string} [options.providerOverride] - LLM provider override
29
+ * @param {string} [options.modelOverride] - LLM model override
30
+ * @returns {Promise<object>} LangGraph agent
31
+ */
32
+ export async function getJobAgent(options = {}) {
33
+ const key = agentCacheKey(options);
34
+
35
+ if (!_agentCache.has(key)) {
36
+ const model = await createModel(options);
20
37
  const tools = [createJobTool, getJobStatusTool, getSystemTechnicalSpecsTool, getSkillBuildingGuideTool, getSkillDetailsTool];
21
38
 
22
39
  const webSearchTool = await createWebSearchTool();
@@ -27,21 +44,23 @@ export async function getJobAgent() {
27
44
 
28
45
  const checkpointer = SqliteSaver.fromConnString(gigaclawDb);
29
46
 
30
- _agent = createReactAgent({
47
+ const agent = createReactAgent({
31
48
  llm: model,
32
49
  tools,
33
50
  checkpointSaver: checkpointer,
34
51
  prompt: (state) => [new SystemMessage(render_md(jobPlanningMd)), ...state.messages],
35
52
  });
53
+
54
+ _agentCache.set(key, agent);
36
55
  }
37
- return _agent;
56
+ return _agentCache.get(key);
38
57
  }
39
58
 
40
59
  /**
41
- * Reset the agent singleton (e.g., when config changes).
60
+ * Reset all cached agents (e.g., when config changes).
42
61
  */
43
62
  export function resetAgent() {
44
- _agent = null;
63
+ _agentCache.clear();
45
64
  }
46
65
 
47
66
  const _codeAgents = new Map();
@@ -49,19 +68,24 @@ const _codeAgents = new Map();
49
68
  /**
50
69
  * Get or create a code agent for a specific chat/workspace.
51
70
  * Each code chat gets its own agent with unique start_coding tool bindings.
71
+ * Supports per-request provider/model overrides for hybrid mode.
72
+ *
52
73
  * @param {object} context
53
74
  * @param {string} context.repo - GitHub repo
54
75
  * @param {string} context.branch - Git branch
55
76
  * @param {string} context.workspaceId - Pre-created workspace row ID
56
77
  * @param {string} context.chatId - Chat thread ID
78
+ * @param {string} [context.providerOverride] - LLM provider override
79
+ * @param {string} [context.modelOverride] - LLM model override
57
80
  * @returns {Promise<object>} LangGraph agent
58
81
  */
59
- export async function getCodeAgent({ repo, branch, workspaceId, chatId }) {
60
- if (_codeAgents.has(chatId)) {
61
- return _codeAgents.get(chatId);
82
+ export async function getCodeAgent({ repo, branch, workspaceId, chatId, providerOverride, modelOverride }) {
83
+ const cacheKey = `${chatId}:${providerOverride || 'default'}:${modelOverride || 'default'}`;
84
+ if (_codeAgents.has(cacheKey)) {
85
+ return _codeAgents.get(cacheKey);
62
86
  }
63
87
 
64
- const model = await createModel();
88
+ const model = await createModel({ providerOverride, modelOverride });
65
89
  const startCodingTool = createStartCodingTool({ repo, branch, workspaceId });
66
90
  const getRepoDetailsTool = createGetRepositoryDetailsTool({ repo, branch });
67
91
  const tools = [startCodingTool, getRepoDetailsTool];
@@ -81,6 +105,6 @@ export async function getCodeAgent({ repo, branch, workspaceId, chatId }) {
81
105
  prompt: (state) => [new SystemMessage(render_md(codePlanningMd)), ...state.messages],
82
106
  });
83
107
 
84
- _codeAgents.set(chatId, agent);
108
+ _codeAgents.set(cacheKey, agent);
85
109
  return agent;
86
110
  }
package/lib/ai/index.js CHANGED
@@ -2,9 +2,23 @@ import { HumanMessage, AIMessage } from '@langchain/core/messages';
2
2
  import { z } from 'zod';
3
3
  import { getJobAgent, getCodeAgent } from './agent.js';
4
4
  import { createModel } from './model.js';
5
+ import { routeTask } from './task-router.js';
5
6
  import { jobSummaryMd } from '../paths.js';
6
7
  import { render_md } from '../utils/render-md.js';
7
8
  import { getChatById, createChat, saveMessage, updateChatTitle, linkChatToWorkspace } from '../db/chats.js';
9
+ import { logAction } from '../db/audit-log.js';
10
+
11
+ /** Providers that run 100% locally — no data leaves the machine */
12
+ const LOCAL_PROVIDERS = new Set(['ollama', 'pragatigpt']);
13
+
14
+ /**
15
+ * Estimate token count from text length (rough approximation: 1 token ≈ 4 chars).
16
+ * Used when the LLM response does not include usage metadata.
17
+ */
18
+ function estimateTokens(text) {
19
+ if (!text) return 0;
20
+ return Math.ceil(text.length / 4);
21
+ }
8
22
 
9
23
  /**
10
24
  * Ensure a chat exists in the DB and save a message.
@@ -38,7 +52,17 @@ function persistMessage(threadId, role, text, options = {}) {
38
52
  * @returns {Promise<string>} AI response text
39
53
  */
40
54
  async function chat(threadId, message, attachments = [], options = {}) {
41
- const agent = await getJobAgent();
55
+ // Hybrid routing: resolve provider/model if not explicitly set
56
+ if (!options.llmProvider && process.env.GIGACLAW_MODE === 'hybrid') {
57
+ const route = await routeTask(message, { explicitProvider: options.llmProvider });
58
+ options.llmProvider = route.provider;
59
+ options.llmModel = route.model;
60
+ console.log(`[hybrid] chat routed to ${route.provider}:${route.model} — ${route.reason}`);
61
+ }
62
+ const agentOptions = {};
63
+ if (options.llmProvider) agentOptions.providerOverride = options.llmProvider;
64
+ if (options.llmModel) agentOptions.modelOverride = options.llmModel;
65
+ const agent = await getJobAgent(agentOptions);
42
66
 
43
67
  // Save user message to DB
44
68
  persistMessage(threadId, 'user', message || '[attachment]', options);
@@ -88,6 +112,26 @@ async function chat(threadId, message, attachments = [], options = {}) {
88
112
  // Save assistant response to DB
89
113
  persistMessage(threadId, 'assistant', response, options);
90
114
 
115
+ // Audit log: record this LLM call
116
+ const provider = options.llmProvider || process.env.LLM_PROVIDER || 'anthropic';
117
+ const tokensIn = estimateTokens(message);
118
+ const tokensOut = estimateTokens(response);
119
+ logAction({
120
+ actionType: 'llm_call',
121
+ actor: options.userId ? `user:${options.userId}` : 'user:unknown',
122
+ target: `provider:${provider}`,
123
+ summary: `Chat message — ${tokensIn} tokens in, ${tokensOut} tokens out via ${provider}`,
124
+ metadata: {
125
+ provider,
126
+ model: options.llmModel || process.env.LLM_MODEL || 'default',
127
+ tokens_in: tokensIn,
128
+ tokens_out: tokensOut,
129
+ is_local: LOCAL_PROVIDERS.has(provider),
130
+ thread_id: threadId,
131
+ call_type: 'chat',
132
+ },
133
+ });
134
+
91
135
  // Auto-generate title for new chats
92
136
  if (options.userId && message) {
93
137
  autoTitle(threadId, message).catch(() => {});
@@ -107,6 +151,14 @@ async function chat(threadId, message, attachments = [], options = {}) {
107
151
  * @returns {AsyncIterableIterator<string>} Stream of text chunks
108
152
  */
109
153
  async function* chatStream(threadId, message, attachments = [], options = {}) {
154
+ // Hybrid routing: resolve provider/model if not explicitly set
155
+ if (!options.llmProvider && process.env.GIGACLAW_MODE === 'hybrid') {
156
+ const route = await routeTask(message, { explicitProvider: options.llmProvider });
157
+ options.llmProvider = route.provider;
158
+ options.llmModel = route.model;
159
+ console.log(`[hybrid] chatStream routed to ${route.provider}:${route.model} — ${route.reason}`);
160
+ }
161
+
110
162
  let agent;
111
163
 
112
164
  // Code mode: set up workspace + code agent
@@ -133,9 +185,14 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
133
185
  branch: options.branch,
134
186
  workspaceId,
135
187
  chatId: threadId,
188
+ providerOverride: options.llmProvider,
189
+ modelOverride: options.llmModel,
136
190
  });
137
191
  } else {
138
- agent = await getJobAgent();
192
+ const agentOpts = {};
193
+ if (options.llmProvider) agentOpts.providerOverride = options.llmProvider;
194
+ if (options.llmModel) agentOpts.modelOverride = options.llmModel;
195
+ agent = await getJobAgent(agentOpts);
139
196
  }
140
197
 
141
198
  // Save user message to DB (skip on regeneration — message already exists)
@@ -235,6 +292,28 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
235
292
  persistMessage(threadId, 'assistant', fullText, options);
236
293
  }
237
294
 
295
+ // Audit log: record this streaming LLM call
296
+ if (fullText) {
297
+ const streamProvider = options.llmProvider || process.env.LLM_PROVIDER || 'anthropic';
298
+ const streamTokensIn = estimateTokens(message);
299
+ const streamTokensOut = estimateTokens(fullText);
300
+ logAction({
301
+ actionType: 'llm_call',
302
+ actor: options.userId ? `user:${options.userId}` : 'user:unknown',
303
+ target: `provider:${streamProvider}`,
304
+ summary: `Chat stream — ${streamTokensIn} tokens in, ${streamTokensOut} tokens out via ${streamProvider}`,
305
+ metadata: {
306
+ provider: streamProvider,
307
+ model: options.llmModel || process.env.LLM_MODEL || 'default',
308
+ tokens_in: streamTokensIn,
309
+ tokens_out: streamTokensOut,
310
+ is_local: LOCAL_PROVIDERS.has(streamProvider),
311
+ thread_id: threadId,
312
+ call_type: 'chat_stream',
313
+ },
314
+ });
315
+ }
316
+
238
317
  // Auto-generate title for new chats
239
318
  if (options.userId && message) {
240
319
  autoTitle(threadId, message).catch(() => {});
@@ -276,6 +355,7 @@ async function autoTitle(threadId, firstMessage) {
276
355
  */
277
356
  async function summarizeJob(results) {
278
357
  try {
358
+ const provider = process.env.LLM_PROVIDER || 'anthropic';
279
359
  const model = await createModel({ maxTokens: 1024 });
280
360
  const systemPrompt = render_md(jobSummaryMd);
281
361
 
@@ -313,6 +393,24 @@ async function summarizeJob(results) {
313
393
 
314
394
  console.log(`[summarizeJob] Result: ${text.length} chars — ${text.slice(0, 200)}`);
315
395
 
396
+ // Audit log: record this LLM call
397
+ const tokensIn = estimateTokens(systemPrompt) + estimateTokens(userMessage);
398
+ const tokensOut = estimateTokens(text);
399
+ logAction({
400
+ actionType: 'llm_call',
401
+ actor: 'agent:job-summary',
402
+ target: `provider:${provider}`,
403
+ summary: `Job summary — ${tokensIn} tokens in, ${tokensOut} tokens out via ${provider}`,
404
+ metadata: {
405
+ provider,
406
+ model: process.env.LLM_MODEL || 'default',
407
+ tokens_in: tokensIn,
408
+ tokens_out: tokensOut,
409
+ is_local: LOCAL_PROVIDERS.has(provider),
410
+ call_type: 'job_summary',
411
+ },
412
+ });
413
+
316
414
  return text.trim() || 'Job finished.';
317
415
  } catch (err) {
318
416
  console.error('[summarizeJob] Failed to summarize job:', err);
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Provider health checking for hybrid mode.
3
+ * Detects whether local (Ollama) and cloud providers are available at runtime.
4
+ */
5
+
6
+ const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
7
+
8
+ /** Cache health check results for a short TTL to avoid hammering endpoints */
9
+ const _cache = new Map();
10
+ const CACHE_TTL_MS = 30_000; // 30 seconds
11
+
12
+ /**
13
+ * Check if Ollama is running and reachable.
14
+ * @returns {Promise<{ available: boolean, models?: string[], error?: string }>}
15
+ */
16
+ export async function checkOllamaHealth() {
17
+ const cached = _cache.get('ollama');
18
+ if (cached && Date.now() - cached.ts < CACHE_TTL_MS) return cached.result;
19
+
20
+ try {
21
+ const res = await fetch(`${OLLAMA_BASE_URL}/api/tags`, {
22
+ signal: AbortSignal.timeout(3000),
23
+ });
24
+ if (!res.ok) {
25
+ const result = { available: false, error: `Ollama returned ${res.status}` };
26
+ _cache.set('ollama', { ts: Date.now(), result });
27
+ return result;
28
+ }
29
+ const data = await res.json();
30
+ const models = (data.models || []).map((m) => m.name);
31
+ const result = { available: true, models };
32
+ _cache.set('ollama', { ts: Date.now(), result });
33
+ return result;
34
+ } catch (err) {
35
+ const result = { available: false, error: err.message };
36
+ _cache.set('ollama', { ts: Date.now(), result });
37
+ return result;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Check if a cloud provider's API key is configured.
43
+ * Does NOT make a network call — just checks env vars.
44
+ * @param {string} provider - Provider name (anthropic, openai, google, pragatigpt, custom)
45
+ * @returns {{ available: boolean, error?: string }}
46
+ */
47
+ export function checkCloudProviderConfig(provider) {
48
+ const keyMap = {
49
+ anthropic: 'ANTHROPIC_API_KEY',
50
+ openai: 'OPENAI_API_KEY',
51
+ google: 'GOOGLE_API_KEY',
52
+ pragatigpt: 'PRAGATIGPT_API_KEY',
53
+ custom: 'CUSTOM_API_KEY',
54
+ };
55
+
56
+ if (provider === 'ollama') {
57
+ return { available: true }; // Ollama doesn't need an API key
58
+ }
59
+
60
+ const envKey = keyMap[provider];
61
+ if (!envKey) return { available: false, error: `Unknown provider: ${provider}` };
62
+
63
+ return process.env[envKey]
64
+ ? { available: true }
65
+ : { available: false, error: `${envKey} is not set` };
66
+ }
67
+
68
+ /**
69
+ * Get the availability status of all configured providers.
70
+ * @returns {Promise<Record<string, { available: boolean, type: 'local'|'cloud', error?: string }>>}
71
+ */
72
+ export async function getAllProviderStatus() {
73
+ const LOCAL_PROVIDERS = new Set(['ollama', 'pragatigpt']);
74
+ const providers = ['anthropic', 'openai', 'google', 'pragatigpt', 'ollama', 'custom'];
75
+ const status = {};
76
+
77
+ for (const p of providers) {
78
+ const type = LOCAL_PROVIDERS.has(p) ? 'local' : 'cloud';
79
+ if (p === 'ollama') {
80
+ const health = await checkOllamaHealth();
81
+ status[p] = { ...health, type };
82
+ } else {
83
+ const config = checkCloudProviderConfig(p);
84
+ status[p] = { ...config, type };
85
+ }
86
+ }
87
+
88
+ return status;
89
+ }
90
+
91
+ /**
92
+ * Clear the health check cache (e.g., after config changes).
93
+ */
94
+ export function clearHealthCache() {
95
+ _cache.clear();
96
+ }