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.
- package/README.md +51 -14
- package/api/index.js +13 -0
- package/bin/cli.js +19 -7
- package/drizzle/0004_trust_ledger_audit_log.sql +15 -0
- package/drizzle/meta/_journal.json +8 -1
- package/lib/ai/agent.js +40 -16
- package/lib/ai/index.js +100 -2
- package/lib/ai/provider-health.js +96 -0
- package/lib/ai/task-router.js +177 -0
- package/lib/chat/api.js +3 -1
- package/lib/chat/components/app-sidebar.js +15 -1
- package/lib/chat/components/app-sidebar.jsx +19 -1
- package/lib/chat/components/icons.js +40 -0
- package/lib/chat/components/icons.jsx +38 -0
- package/lib/chat/components/index.js +1 -0
- package/lib/chat/components/trust-ledger-page.js +408 -0
- package/lib/chat/components/trust-ledger-page.jsx +528 -0
- package/lib/chat/trust-ledger-actions.js +61 -0
- package/lib/code/ws-proxy.js +14 -6
- package/lib/db/api-keys.js +5 -4
- package/lib/db/audit-log.js +346 -0
- package/lib/db/schema.js +12 -0
- package/package.json +64 -20
- package/setup/lib/providers.mjs +2 -2
- package/setup/setup-hybrid.mjs +399 -0
- package/setup/setup-local.mjs +9 -9
- package/setup/setup.mjs +17 -9
- package/templates/.env.example +15 -5
- package/templates/CLAUDE.md.template +12 -12
- package/templates/app/globals.css +1 -1
- package/templates/app/layout.js +5 -5
- package/templates/app/trust-ledger/page.js +6 -0
- package/templates/config/SOUL.md +3 -3
- package/templates/docker/event-handler/Dockerfile +1 -1
- package/templates/middleware.js +1 -1
- package/templates/next.config.mjs +23 -2
- package/templates/skills/README.md +7 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
#
|
|
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
|
|
21
|
+
## What is GigaClaw?
|
|
22
22
|
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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** —
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`);
|
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
|
-
|
|
10
|
+
/** Cache agents by provider:model key so each combo is created once */
|
|
11
|
+
const _agentCache = new Map();
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
56
|
+
return _agentCache.get(key);
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
/**
|
|
41
|
-
* Reset
|
|
60
|
+
* Reset all cached agents (e.g., when config changes).
|
|
42
61
|
*/
|
|
43
62
|
export function resetAgent() {
|
|
44
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|