gigaclaw 1.4.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/LICENSE +26 -0
- package/README.md +237 -0
- package/api/CLAUDE.md +19 -0
- package/api/index.js +265 -0
- package/bin/cli.js +823 -0
- package/bin/local.sh +85 -0
- package/bin/postinstall.js +63 -0
- package/config/index.js +26 -0
- package/config/instrumentation.js +62 -0
- package/drizzle/0000_initial.sql +52 -0
- package/drizzle/0001_nostalgic_sersi.sql +11 -0
- package/drizzle/0002_black_daimon_hellstrom.sql +19 -0
- package/drizzle/0003_rename_code_workspaces.sql +5 -0
- package/drizzle/meta/0000_snapshot.json +321 -0
- package/drizzle/meta/0001_snapshot.json +390 -0
- package/drizzle/meta/0002_snapshot.json +411 -0
- package/drizzle/meta/0003_snapshot.json +419 -0
- package/drizzle/meta/_journal.json +34 -0
- package/lib/actions.js +44 -0
- package/lib/ai/agent.js +86 -0
- package/lib/ai/index.js +342 -0
- package/lib/ai/model.js +180 -0
- package/lib/ai/tools.js +269 -0
- package/lib/ai/web-search.js +42 -0
- package/lib/auth/actions.js +28 -0
- package/lib/auth/config.js +27 -0
- package/lib/auth/edge-config.js +27 -0
- package/lib/auth/index.js +27 -0
- package/lib/auth/middleware.js +62 -0
- package/lib/channels/base.js +56 -0
- package/lib/channels/index.js +15 -0
- package/lib/channels/telegram.js +148 -0
- package/lib/chat/actions.js +579 -0
- package/lib/chat/api.js +140 -0
- package/lib/chat/components/app-sidebar.js +213 -0
- package/lib/chat/components/app-sidebar.jsx +279 -0
- package/lib/chat/components/chat-header.js +192 -0
- package/lib/chat/components/chat-header.jsx +223 -0
- package/lib/chat/components/chat-input.js +236 -0
- package/lib/chat/components/chat-input.jsx +249 -0
- package/lib/chat/components/chat-nav-context.js +11 -0
- package/lib/chat/components/chat-nav-context.jsx +11 -0
- package/lib/chat/components/chat-page.js +99 -0
- package/lib/chat/components/chat-page.jsx +121 -0
- package/lib/chat/components/chat.js +153 -0
- package/lib/chat/components/chat.jsx +199 -0
- package/lib/chat/components/chats-page.js +367 -0
- package/lib/chat/components/chats-page.jsx +394 -0
- package/lib/chat/components/code-mode-toggle.js +132 -0
- package/lib/chat/components/code-mode-toggle.jsx +163 -0
- package/lib/chat/components/crons-page.js +172 -0
- package/lib/chat/components/crons-page.jsx +244 -0
- package/lib/chat/components/greeting.js +11 -0
- package/lib/chat/components/greeting.jsx +16 -0
- package/lib/chat/components/icons.js +805 -0
- package/lib/chat/components/icons.jsx +751 -0
- package/lib/chat/components/index.js +20 -0
- package/lib/chat/components/message.js +363 -0
- package/lib/chat/components/message.jsx +422 -0
- package/lib/chat/components/messages.js +65 -0
- package/lib/chat/components/messages.jsx +74 -0
- package/lib/chat/components/notifications-page.js +56 -0
- package/lib/chat/components/notifications-page.jsx +87 -0
- package/lib/chat/components/page-layout.js +21 -0
- package/lib/chat/components/page-layout.jsx +28 -0
- package/lib/chat/components/pull-requests-page.js +103 -0
- package/lib/chat/components/pull-requests-page.jsx +113 -0
- package/lib/chat/components/settings-layout.js +39 -0
- package/lib/chat/components/settings-layout.jsx +53 -0
- package/lib/chat/components/settings-secrets-page.js +216 -0
- package/lib/chat/components/settings-secrets-page.jsx +264 -0
- package/lib/chat/components/sidebar-history-item.js +138 -0
- package/lib/chat/components/sidebar-history-item.jsx +119 -0
- package/lib/chat/components/sidebar-history.js +167 -0
- package/lib/chat/components/sidebar-history.jsx +220 -0
- package/lib/chat/components/sidebar-user-nav.js +61 -0
- package/lib/chat/components/sidebar-user-nav.jsx +77 -0
- package/lib/chat/components/swarm-page.js +157 -0
- package/lib/chat/components/swarm-page.jsx +210 -0
- package/lib/chat/components/tool-call.js +89 -0
- package/lib/chat/components/tool-call.jsx +107 -0
- package/lib/chat/components/triggers-page.js +153 -0
- package/lib/chat/components/triggers-page.jsx +221 -0
- package/lib/chat/components/ui/combobox.js +98 -0
- package/lib/chat/components/ui/combobox.jsx +114 -0
- package/lib/chat/components/ui/confirm-dialog.js +53 -0
- package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
- package/lib/chat/components/ui/dropdown-menu.js +194 -0
- package/lib/chat/components/ui/dropdown-menu.jsx +215 -0
- package/lib/chat/components/ui/rename-dialog.js +78 -0
- package/lib/chat/components/ui/rename-dialog.jsx +74 -0
- package/lib/chat/components/ui/scroll-area.js +13 -0
- package/lib/chat/components/ui/scroll-area.jsx +17 -0
- package/lib/chat/components/ui/separator.js +21 -0
- package/lib/chat/components/ui/separator.jsx +18 -0
- package/lib/chat/components/ui/sheet.js +75 -0
- package/lib/chat/components/ui/sheet.jsx +95 -0
- package/lib/chat/components/ui/sidebar.js +228 -0
- package/lib/chat/components/ui/sidebar.jsx +246 -0
- package/lib/chat/components/ui/tooltip.js +56 -0
- package/lib/chat/components/ui/tooltip.jsx +66 -0
- package/lib/chat/components/upgrade-dialog.js +151 -0
- package/lib/chat/components/upgrade-dialog.jsx +170 -0
- package/lib/chat/utils.js +11 -0
- package/lib/code/actions.js +153 -0
- package/lib/code/code-page.js +22 -0
- package/lib/code/code-page.jsx +25 -0
- package/lib/code/index.js +1 -0
- package/lib/code/terminal-view.js +201 -0
- package/lib/code/terminal-view.jsx +224 -0
- package/lib/code/ws-proxy.js +80 -0
- package/lib/cron.js +246 -0
- package/lib/db/api-keys.js +163 -0
- package/lib/db/chats.js +168 -0
- package/lib/db/code-workspaces.js +110 -0
- package/lib/db/index.js +52 -0
- package/lib/db/notifications.js +99 -0
- package/lib/db/schema.js +66 -0
- package/lib/db/update-check.js +96 -0
- package/lib/db/users.js +89 -0
- package/lib/paths.js +42 -0
- package/lib/tools/create-job.js +97 -0
- package/lib/tools/docker.js +146 -0
- package/lib/tools/github.js +271 -0
- package/lib/tools/openai.js +35 -0
- package/lib/tools/telegram.js +292 -0
- package/lib/triggers.js +104 -0
- package/lib/utils/render-md.js +111 -0
- package/package.json +118 -0
- package/setup/lib/auth.mjs +81 -0
- package/setup/lib/env.mjs +21 -0
- package/setup/lib/fs-utils.mjs +20 -0
- package/setup/lib/github.mjs +149 -0
- package/setup/lib/prerequisites.mjs +155 -0
- package/setup/lib/prompts.mjs +267 -0
- package/setup/lib/providers.mjs +105 -0
- package/setup/lib/sync.mjs +125 -0
- package/setup/lib/targets.mjs +45 -0
- package/setup/lib/telegram-verify.mjs +63 -0
- package/setup/lib/telegram.mjs +76 -0
- package/setup/setup-cloud.mjs +833 -0
- package/setup/setup-local.mjs +377 -0
- package/setup/setup-telegram.mjs +265 -0
- package/setup/setup.mjs +87 -0
- package/templates/.dockerignore +5 -0
- package/templates/.env.example +104 -0
- package/templates/.github/workflows/auto-merge.yml +117 -0
- package/templates/.github/workflows/notify-job-failed.yml +64 -0
- package/templates/.github/workflows/notify-pr-complete.yml +119 -0
- package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
- package/templates/.github/workflows/run-job.yml +89 -0
- package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
- package/templates/.gitignore.template +45 -0
- package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
- package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
- package/templates/CLAUDE.md +29 -0
- package/templates/CLAUDE.md.template +308 -0
- package/templates/app/api/[...gigaclaw]/route.js +1 -0
- package/templates/app/api/auth/[...nextauth]/route.js +1 -0
- package/templates/app/chat/[chatId]/page.js +9 -0
- package/templates/app/chats/page.js +7 -0
- package/templates/app/code/[codeWorkspaceId]/page.js +9 -0
- package/templates/app/components/ascii-logo.jsx +12 -0
- package/templates/app/components/login-form.jsx +92 -0
- package/templates/app/components/setup-form.jsx +82 -0
- package/templates/app/components/theme-provider.jsx +11 -0
- package/templates/app/components/theme-toggle.jsx +38 -0
- package/templates/app/components/ui/button.jsx +21 -0
- package/templates/app/components/ui/card.jsx +23 -0
- package/templates/app/components/ui/input.jsx +10 -0
- package/templates/app/components/ui/label.jsx +10 -0
- package/templates/app/crons/page.js +5 -0
- package/templates/app/globals.css +90 -0
- package/templates/app/layout.js +33 -0
- package/templates/app/login/page.js +15 -0
- package/templates/app/notifications/page.js +7 -0
- package/templates/app/page.js +7 -0
- package/templates/app/pull-requests/page.js +7 -0
- package/templates/app/settings/crons/page.js +5 -0
- package/templates/app/settings/layout.js +7 -0
- package/templates/app/settings/page.js +5 -0
- package/templates/app/settings/secrets/page.js +5 -0
- package/templates/app/settings/triggers/page.js +5 -0
- package/templates/app/stream/chat/route.js +1 -0
- package/templates/app/swarm/page.js +7 -0
- package/templates/app/triggers/page.js +5 -0
- package/templates/config/CODE_PLANNING.md +14 -0
- package/templates/config/CRONS.json +56 -0
- package/templates/config/HEARTBEAT.md +3 -0
- package/templates/config/JOB_AGENT.md +30 -0
- package/templates/config/JOB_PLANNING.md +240 -0
- package/templates/config/JOB_SUMMARY.md +130 -0
- package/templates/config/SKILL_BUILDING_GUIDE.md +96 -0
- package/templates/config/SOUL.md +48 -0
- package/templates/config/TRIGGERS.json +58 -0
- package/templates/config/WEB_SEARCH_AVAILABLE.md +5 -0
- package/templates/config/WEB_SEARCH_UNAVAILABLE.md +3 -0
- package/templates/docker/claude-code-job/Dockerfile +34 -0
- package/templates/docker/claude-code-job/entrypoint.sh +149 -0
- package/templates/docker/claude-code-workspace/.tmux.conf +5 -0
- package/templates/docker/claude-code-workspace/Dockerfile +61 -0
- package/templates/docker/claude-code-workspace/entrypoint.sh +51 -0
- package/templates/docker/event-handler/Dockerfile +20 -0
- package/templates/docker/event-handler/ecosystem.config.cjs +7 -0
- package/templates/docker/pi-coding-agent-job/Dockerfile +51 -0
- package/templates/docker/pi-coding-agent-job/entrypoint.sh +164 -0
- package/templates/docker-compose.local.yml +78 -0
- package/templates/docker-compose.yml +64 -0
- package/templates/instrumentation.js +6 -0
- package/templates/middleware.js +23 -0
- package/templates/next.config.mjs +3 -0
- package/templates/postcss.config.mjs +5 -0
- package/templates/public/favicon.ico +0 -0
- package/templates/server.js +25 -0
- package/templates/skills/LICENSE +21 -0
- package/templates/skills/README.md +119 -0
- package/templates/skills/brave-search/SKILL.md +79 -0
- package/templates/skills/brave-search/content.js +86 -0
- package/templates/skills/brave-search/package-lock.json +621 -0
- package/templates/skills/brave-search/package.json +14 -0
- package/templates/skills/brave-search/search.js +199 -0
- package/templates/skills/browser-tools/SKILL.md +196 -0
- package/templates/skills/browser-tools/browser-content.js +103 -0
- package/templates/skills/browser-tools/browser-cookies.js +35 -0
- package/templates/skills/browser-tools/browser-eval.js +53 -0
- package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
- package/templates/skills/browser-tools/browser-nav.js +44 -0
- package/templates/skills/browser-tools/browser-pick.js +162 -0
- package/templates/skills/browser-tools/browser-screenshot.js +34 -0
- package/templates/skills/browser-tools/browser-start.js +87 -0
- package/templates/skills/browser-tools/package-lock.json +2556 -0
- package/templates/skills/browser-tools/package.json +19 -0
- package/templates/skills/google-docs/SKILL.md +23 -0
- package/templates/skills/google-docs/create.sh +69 -0
- package/templates/skills/google-drive/SKILL.md +47 -0
- package/templates/skills/google-drive/delete.sh +47 -0
- package/templates/skills/google-drive/download.sh +50 -0
- package/templates/skills/google-drive/list.sh +41 -0
- package/templates/skills/google-drive/upload.sh +76 -0
- package/templates/skills/kie-ai/SKILL.md +38 -0
- package/templates/skills/kie-ai/generate-image.sh +77 -0
- package/templates/skills/kie-ai/generate-video.sh +69 -0
- package/templates/skills/llm-secrets/SKILL.md +34 -0
- package/templates/skills/llm-secrets/llm-secrets.js +33 -0
- package/templates/skills/modify-self/SKILL.md +12 -0
- package/templates/skills/youtube-transcript/SKILL.md +41 -0
- package/templates/skills/youtube-transcript/package-lock.json +24 -0
- package/templates/skills/youtube-transcript/package.json +8 -0
- package/templates/skills/youtube-transcript/transcript.js +84 -0
package/lib/cron.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import cron from 'node-cron';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { cronsFile, cronDir } from './paths.js';
|
|
5
|
+
import { executeAction } from './actions.js';
|
|
6
|
+
|
|
7
|
+
function getInstalledVersion() {
|
|
8
|
+
const pkgPath = path.join(process.cwd(), 'node_modules', 'gigaclaw', 'package.json');
|
|
9
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// In-memory flag for available update (read by sidebar, written by cron)
|
|
13
|
+
let _updateAvailable = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the in-memory update-available version (or null).
|
|
17
|
+
* @returns {string|null}
|
|
18
|
+
*/
|
|
19
|
+
function getUpdateAvailable() {
|
|
20
|
+
return _updateAvailable;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Set the in-memory update-available version.
|
|
25
|
+
* @param {string|null} v
|
|
26
|
+
*/
|
|
27
|
+
function setUpdateAvailable(v) {
|
|
28
|
+
_updateAvailable = v;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compare two semver strings numerically.
|
|
33
|
+
* @param {string} candidate - e.g. "1.2.40"
|
|
34
|
+
* @param {string} baseline - e.g. "1.2.39"
|
|
35
|
+
* @returns {boolean} true if candidate > baseline
|
|
36
|
+
*/
|
|
37
|
+
function isVersionNewer(candidate, baseline) {
|
|
38
|
+
// Pre-release candidate is never "newer" for upgrade purposes
|
|
39
|
+
if (candidate.includes('-')) return false;
|
|
40
|
+
|
|
41
|
+
const a = candidate.split('.').map(Number);
|
|
42
|
+
const b = baseline.replace(/-.*$/, '').split('.').map(Number);
|
|
43
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
44
|
+
const av = a[i] || 0;
|
|
45
|
+
const bv = b[i] || 0;
|
|
46
|
+
if (av > bv) return true;
|
|
47
|
+
if (av < bv) return false;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a version string is a pre-release (contains '-').
|
|
54
|
+
* @param {string} v
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
function isPrerelease(v) {
|
|
58
|
+
return v.includes('-');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Compare two semver strings (including pre-release).
|
|
63
|
+
* Returns positive if a > b, negative if a < b, 0 if equal.
|
|
64
|
+
* Ordering: 1.2.71-beta.0 < 1.2.71-beta.1 < 1.2.71 (stable) < 1.2.72-beta.0
|
|
65
|
+
* @param {string} a
|
|
66
|
+
* @param {string} b
|
|
67
|
+
* @returns {number}
|
|
68
|
+
*/
|
|
69
|
+
function compareVersions(a, b) {
|
|
70
|
+
const [aCore, aPre] = a.split('-');
|
|
71
|
+
const [bCore, bPre] = b.split('-');
|
|
72
|
+
|
|
73
|
+
const aParts = aCore.split('.').map(Number);
|
|
74
|
+
const bParts = bCore.split('.').map(Number);
|
|
75
|
+
|
|
76
|
+
// Compare major.minor.patch
|
|
77
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
78
|
+
const av = aParts[i] || 0;
|
|
79
|
+
const bv = bParts[i] || 0;
|
|
80
|
+
if (av !== bv) return av - bv;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Same core version: stable beats pre-release
|
|
84
|
+
if (!aPre && bPre) return 1; // a is stable, b is pre-release
|
|
85
|
+
if (aPre && !bPre) return -1; // a is pre-release, b is stable
|
|
86
|
+
if (!aPre && !bPre) return 0; // both stable, same core
|
|
87
|
+
|
|
88
|
+
// Both pre-release with same core: compare pre-release number
|
|
89
|
+
const aNum = parseInt(aPre.split('.').pop(), 10) || 0;
|
|
90
|
+
const bNum = parseInt(bPre.split('.').pop(), 10) || 0;
|
|
91
|
+
return aNum - bNum;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Fetch release notes from GitHub for the target version.
|
|
96
|
+
* @param {string} target - Target upgrade version
|
|
97
|
+
*/
|
|
98
|
+
async function fetchAndStoreReleaseNotes(target) {
|
|
99
|
+
try {
|
|
100
|
+
const ghRes = await fetch(
|
|
101
|
+
`https://api.github.com/repos/gignaati/gigaclaw/releases/tags/v${target}`
|
|
102
|
+
);
|
|
103
|
+
if (!ghRes.ok) return;
|
|
104
|
+
const release = await ghRes.json();
|
|
105
|
+
if (release.body) {
|
|
106
|
+
const { setReleaseNotes } = await import('./db/update-check.js');
|
|
107
|
+
setReleaseNotes(release.body);
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check npm registry for a newer version of gigaclaw.
|
|
114
|
+
*/
|
|
115
|
+
async function runVersionCheck() {
|
|
116
|
+
try {
|
|
117
|
+
const installed = getInstalledVersion();
|
|
118
|
+
|
|
119
|
+
if (isPrerelease(installed)) {
|
|
120
|
+
// Beta path: check both stable and beta dist-tags
|
|
121
|
+
const results = await Promise.allSettled([
|
|
122
|
+
fetch('https://registry.npmjs.org/gigaclaw/latest'),
|
|
123
|
+
fetch('https://registry.npmjs.org/gigaclaw/beta'),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const candidates = [];
|
|
127
|
+
for (const result of results) {
|
|
128
|
+
if (result.status !== 'fulfilled') continue;
|
|
129
|
+
const res = result.value;
|
|
130
|
+
if (!res.ok) continue;
|
|
131
|
+
const data = await res.json();
|
|
132
|
+
if (data.version && compareVersions(data.version, installed) > 0) {
|
|
133
|
+
candidates.push(data.version);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (candidates.length > 0) {
|
|
138
|
+
// Pick the best candidate (highest version)
|
|
139
|
+
candidates.sort(compareVersions);
|
|
140
|
+
const best = candidates[candidates.length - 1];
|
|
141
|
+
console.log(`[version check] update available: ${installed} → ${best}`);
|
|
142
|
+
setUpdateAvailable(best);
|
|
143
|
+
const { setAvailableVersion } = await import('./db/update-check.js');
|
|
144
|
+
setAvailableVersion(best);
|
|
145
|
+
await fetchAndStoreReleaseNotes(best);
|
|
146
|
+
} else {
|
|
147
|
+
setUpdateAvailable(null);
|
|
148
|
+
const { clearAvailableVersion, clearReleaseNotes } = await import('./db/update-check.js');
|
|
149
|
+
clearAvailableVersion();
|
|
150
|
+
clearReleaseNotes();
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
// Stable path: existing logic, untouched
|
|
154
|
+
const res = await fetch('https://registry.npmjs.org/gigaclaw/latest');
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
console.warn(`[version check] npm registry returned ${res.status}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const data = await res.json();
|
|
160
|
+
const latest = data.version;
|
|
161
|
+
|
|
162
|
+
if (isVersionNewer(latest, installed)) {
|
|
163
|
+
console.log(`[version check] update available: ${installed} → ${latest}`);
|
|
164
|
+
setUpdateAvailable(latest);
|
|
165
|
+
// Persist to DB
|
|
166
|
+
const { setAvailableVersion } = await import('./db/update-check.js');
|
|
167
|
+
setAvailableVersion(latest);
|
|
168
|
+
await fetchAndStoreReleaseNotes(latest);
|
|
169
|
+
} else {
|
|
170
|
+
setUpdateAvailable(null);
|
|
171
|
+
// Clear DB
|
|
172
|
+
const { clearAvailableVersion, clearReleaseNotes } = await import('./db/update-check.js');
|
|
173
|
+
clearAvailableVersion();
|
|
174
|
+
clearReleaseNotes();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.warn(`[version check] failed: ${err.message}`);
|
|
179
|
+
// Leave existing flag untouched on error
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Start built-in crons (version check). Called from instrumentation.
|
|
185
|
+
*/
|
|
186
|
+
function startBuiltinCrons() {
|
|
187
|
+
// Schedule hourly
|
|
188
|
+
cron.schedule('0 * * * *', runVersionCheck);
|
|
189
|
+
// Run once immediately
|
|
190
|
+
runVersionCheck();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Load and schedule crons from CRONS.json
|
|
195
|
+
* @returns {Array} - Array of scheduled cron tasks
|
|
196
|
+
*/
|
|
197
|
+
function loadCrons() {
|
|
198
|
+
const cronFile = cronsFile;
|
|
199
|
+
|
|
200
|
+
console.log('\n--- Cron Jobs ---');
|
|
201
|
+
|
|
202
|
+
if (!fs.existsSync(cronFile)) {
|
|
203
|
+
console.log('No CRONS.json found');
|
|
204
|
+
console.log('-----------------\n');
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const crons = JSON.parse(fs.readFileSync(cronFile, 'utf8'));
|
|
209
|
+
const tasks = [];
|
|
210
|
+
|
|
211
|
+
for (const cronEntry of crons) {
|
|
212
|
+
const { name, schedule, type = 'agent', enabled } = cronEntry;
|
|
213
|
+
if (enabled === false) continue;
|
|
214
|
+
|
|
215
|
+
if (!cron.validate(schedule)) {
|
|
216
|
+
console.error(`Invalid schedule for "${name}": ${schedule}`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const task = cron.schedule(schedule, async () => {
|
|
221
|
+
try {
|
|
222
|
+
const result = await executeAction(cronEntry, { cwd: cronDir });
|
|
223
|
+
console.log(`[CRON] ${name}: ${result || 'ran'}`);
|
|
224
|
+
console.log(`[CRON] ${name}: completed!`);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error(`[CRON] ${name}: error - ${err.message}`);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
tasks.push({ name, schedule, type, task });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (tasks.length === 0) {
|
|
234
|
+
console.log('No active cron jobs');
|
|
235
|
+
} else {
|
|
236
|
+
for (const { name, schedule, type } of tasks) {
|
|
237
|
+
console.log(` ${name}: ${schedule} (${type})`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log('-----------------\n');
|
|
242
|
+
|
|
243
|
+
return tasks;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export { loadCrons, startBuiltinCrons, getUpdateAvailable, setUpdateAvailable, getInstalledVersion, isPrerelease };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { randomUUID, randomBytes, createHash, timingSafeEqual } from 'crypto';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { getDb } from './index.js';
|
|
4
|
+
import { settings } from './schema.js';
|
|
5
|
+
|
|
6
|
+
const KEY_PREFIX = 'tpb_';
|
|
7
|
+
|
|
8
|
+
// In-memory cache: { key_hash, id } or null
|
|
9
|
+
let _cache = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a new API key: tpb_ + 64 hex chars (32 random bytes).
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function generateApiKey() {
|
|
16
|
+
return KEY_PREFIX + randomBytes(32).toString('hex');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hash an API key using SHA-256.
|
|
21
|
+
* @param {string} key - Raw API key
|
|
22
|
+
* @returns {string} Hex digest
|
|
23
|
+
*/
|
|
24
|
+
export function hashApiKey(key) {
|
|
25
|
+
return createHash('sha256').update(key).digest('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Lazy-load the API key hash into the in-memory cache.
|
|
30
|
+
*/
|
|
31
|
+
function _ensureCache() {
|
|
32
|
+
if (_cache !== null) return _cache;
|
|
33
|
+
|
|
34
|
+
const db = getDb();
|
|
35
|
+
const row = db
|
|
36
|
+
.select()
|
|
37
|
+
.from(settings)
|
|
38
|
+
.where(eq(settings.type, 'api_key'))
|
|
39
|
+
.get();
|
|
40
|
+
|
|
41
|
+
if (row) {
|
|
42
|
+
const parsed = JSON.parse(row.value);
|
|
43
|
+
_cache = { keyHash: parsed.key_hash, id: row.id };
|
|
44
|
+
} else {
|
|
45
|
+
_cache = false; // no key exists — distinguish from "not loaded yet"
|
|
46
|
+
}
|
|
47
|
+
return _cache;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clear the in-memory cache (call after create/delete).
|
|
52
|
+
*/
|
|
53
|
+
export function invalidateApiKeyCache() {
|
|
54
|
+
_cache = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create (or replace) the API key. Deletes any existing key first.
|
|
59
|
+
* @param {string} createdBy - User ID
|
|
60
|
+
* @returns {{ key: string, record: object }}
|
|
61
|
+
*/
|
|
62
|
+
export function createApiKeyRecord(createdBy) {
|
|
63
|
+
const db = getDb();
|
|
64
|
+
|
|
65
|
+
// Delete any existing API key
|
|
66
|
+
db.delete(settings).where(eq(settings.type, 'api_key')).run();
|
|
67
|
+
|
|
68
|
+
const key = generateApiKey();
|
|
69
|
+
const keyHash = hashApiKey(key);
|
|
70
|
+
const keyPrefix = key.slice(0, 8); // "tpb_" + first 4 hex chars
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
|
|
73
|
+
const record = {
|
|
74
|
+
id: randomUUID(),
|
|
75
|
+
type: 'api_key',
|
|
76
|
+
key: 'api_key',
|
|
77
|
+
value: JSON.stringify({ key_prefix: keyPrefix, key_hash: keyHash, last_used_at: null }),
|
|
78
|
+
createdBy,
|
|
79
|
+
createdAt: now,
|
|
80
|
+
updatedAt: now,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
db.insert(settings).values(record).run();
|
|
84
|
+
invalidateApiKeyCache();
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
key,
|
|
88
|
+
record: {
|
|
89
|
+
id: record.id,
|
|
90
|
+
keyPrefix,
|
|
91
|
+
createdAt: now,
|
|
92
|
+
lastUsedAt: null,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the current API key metadata (no hash).
|
|
99
|
+
* @returns {object|null}
|
|
100
|
+
*/
|
|
101
|
+
export function getApiKey() {
|
|
102
|
+
const db = getDb();
|
|
103
|
+
const row = db
|
|
104
|
+
.select()
|
|
105
|
+
.from(settings)
|
|
106
|
+
.where(eq(settings.type, 'api_key'))
|
|
107
|
+
.get();
|
|
108
|
+
|
|
109
|
+
if (!row) return null;
|
|
110
|
+
|
|
111
|
+
const parsed = JSON.parse(row.value);
|
|
112
|
+
return {
|
|
113
|
+
id: row.id,
|
|
114
|
+
keyPrefix: parsed.key_prefix,
|
|
115
|
+
createdAt: row.createdAt,
|
|
116
|
+
lastUsedAt: parsed.last_used_at,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Delete the API key.
|
|
122
|
+
*/
|
|
123
|
+
export function deleteApiKey() {
|
|
124
|
+
const db = getDb();
|
|
125
|
+
db.delete(settings).where(eq(settings.type, 'api_key')).run();
|
|
126
|
+
invalidateApiKeyCache();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Verify a raw API key against the cached hash.
|
|
131
|
+
* @param {string} rawKey - Raw API key from request header
|
|
132
|
+
* @returns {object|null} Record if valid, null otherwise
|
|
133
|
+
*/
|
|
134
|
+
export function verifyApiKey(rawKey) {
|
|
135
|
+
if (!rawKey || !rawKey.startsWith(KEY_PREFIX)) return null;
|
|
136
|
+
|
|
137
|
+
const keyHash = hashApiKey(rawKey);
|
|
138
|
+
const cached = _ensureCache();
|
|
139
|
+
|
|
140
|
+
if (!cached) return null;
|
|
141
|
+
const a = Buffer.from(cached.keyHash, 'hex');
|
|
142
|
+
const b = Buffer.from(keyHash, 'hex');
|
|
143
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
|
|
144
|
+
|
|
145
|
+
// Update last_used_at in background (non-blocking)
|
|
146
|
+
try {
|
|
147
|
+
const db = getDb();
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
const row = db.select().from(settings).where(eq(settings.id, cached.id)).get();
|
|
150
|
+
if (row) {
|
|
151
|
+
const parsed = JSON.parse(row.value);
|
|
152
|
+
parsed.last_used_at = now;
|
|
153
|
+
db.update(settings)
|
|
154
|
+
.set({ value: JSON.stringify(parsed), updatedAt: now })
|
|
155
|
+
.where(eq(settings.id, cached.id))
|
|
156
|
+
.run();
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Non-fatal: last_used_at is informational
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return cached;
|
|
163
|
+
}
|
package/lib/db/chats.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { eq, desc, asc } from 'drizzle-orm';
|
|
3
|
+
import { getDb } from './index.js';
|
|
4
|
+
import { chats, messages } from './schema.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a new chat.
|
|
8
|
+
* @param {string} userId
|
|
9
|
+
* @param {string} [title='New Chat']
|
|
10
|
+
* @param {string} [id] - Optional chat ID (UUID). Generated if not provided.
|
|
11
|
+
* @returns {object} The created chat
|
|
12
|
+
*/
|
|
13
|
+
export function createChat(userId, title = 'New Chat', id = null) {
|
|
14
|
+
const db = getDb();
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const chat = {
|
|
17
|
+
id: id || randomUUID(),
|
|
18
|
+
userId,
|
|
19
|
+
title,
|
|
20
|
+
createdAt: now,
|
|
21
|
+
updatedAt: now,
|
|
22
|
+
};
|
|
23
|
+
db.insert(chats).values(chat).run();
|
|
24
|
+
return chat;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get all chats for a user, ordered by most recently updated.
|
|
29
|
+
* @param {string} userId
|
|
30
|
+
* @returns {object[]}
|
|
31
|
+
*/
|
|
32
|
+
export function getChatsByUser(userId) {
|
|
33
|
+
const db = getDb();
|
|
34
|
+
return db
|
|
35
|
+
.select()
|
|
36
|
+
.from(chats)
|
|
37
|
+
.where(eq(chats.userId, userId))
|
|
38
|
+
.orderBy(desc(chats.updatedAt))
|
|
39
|
+
.all();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get a single chat by ID.
|
|
44
|
+
* @param {string} chatId
|
|
45
|
+
* @returns {object|undefined}
|
|
46
|
+
*/
|
|
47
|
+
export function getChatById(chatId) {
|
|
48
|
+
const db = getDb();
|
|
49
|
+
return db.select().from(chats).where(eq(chats.id, chatId)).get();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get a single chat by its code_workspace_id.
|
|
54
|
+
* @param {string} workspaceId
|
|
55
|
+
* @returns {object|undefined}
|
|
56
|
+
*/
|
|
57
|
+
export function getChatByWorkspaceId(workspaceId) {
|
|
58
|
+
const db = getDb();
|
|
59
|
+
return db.select().from(chats).where(eq(chats.codeWorkspaceId, workspaceId)).get();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Update a chat's title.
|
|
64
|
+
* @param {string} chatId
|
|
65
|
+
* @param {string} title
|
|
66
|
+
*/
|
|
67
|
+
export function updateChatTitle(chatId, title) {
|
|
68
|
+
const db = getDb();
|
|
69
|
+
db.update(chats)
|
|
70
|
+
.set({ title, updatedAt: Date.now() })
|
|
71
|
+
.where(eq(chats.id, chatId))
|
|
72
|
+
.run();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Toggle a chat's starred status.
|
|
77
|
+
* @param {string} chatId
|
|
78
|
+
* @returns {number} The new starred value (0 or 1)
|
|
79
|
+
*/
|
|
80
|
+
export function toggleChatStarred(chatId) {
|
|
81
|
+
const db = getDb();
|
|
82
|
+
const chat = db.select({ starred: chats.starred }).from(chats).where(eq(chats.id, chatId)).get();
|
|
83
|
+
const newValue = chat?.starred ? 0 : 1;
|
|
84
|
+
db.update(chats)
|
|
85
|
+
.set({ starred: newValue })
|
|
86
|
+
.where(eq(chats.id, chatId))
|
|
87
|
+
.run();
|
|
88
|
+
return newValue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Delete a chat and all its messages.
|
|
93
|
+
* @param {string} chatId
|
|
94
|
+
*/
|
|
95
|
+
export function deleteChat(chatId) {
|
|
96
|
+
const db = getDb();
|
|
97
|
+
db.delete(messages).where(eq(messages.chatId, chatId)).run();
|
|
98
|
+
db.delete(chats).where(eq(chats.id, chatId)).run();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Delete all chats and messages for a user.
|
|
103
|
+
* @param {string} userId
|
|
104
|
+
*/
|
|
105
|
+
export function deleteAllChatsByUser(userId) {
|
|
106
|
+
const db = getDb();
|
|
107
|
+
const userChats = db
|
|
108
|
+
.select({ id: chats.id })
|
|
109
|
+
.from(chats)
|
|
110
|
+
.where(eq(chats.userId, userId))
|
|
111
|
+
.all();
|
|
112
|
+
|
|
113
|
+
for (const chat of userChats) {
|
|
114
|
+
db.delete(messages).where(eq(messages.chatId, chat.id)).run();
|
|
115
|
+
}
|
|
116
|
+
db.delete(chats).where(eq(chats.userId, userId)).run();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get all messages for a chat, ordered by creation time.
|
|
121
|
+
* @param {string} chatId
|
|
122
|
+
* @returns {object[]}
|
|
123
|
+
*/
|
|
124
|
+
export function getMessagesByChatId(chatId) {
|
|
125
|
+
const db = getDb();
|
|
126
|
+
return db
|
|
127
|
+
.select()
|
|
128
|
+
.from(messages)
|
|
129
|
+
.where(eq(messages.chatId, chatId))
|
|
130
|
+
.orderBy(asc(messages.createdAt))
|
|
131
|
+
.all();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Link a chat to a code workspace.
|
|
136
|
+
* @param {string} chatId
|
|
137
|
+
* @param {string} workspaceId
|
|
138
|
+
*/
|
|
139
|
+
export function linkChatToWorkspace(chatId, workspaceId) {
|
|
140
|
+
const db = getDb();
|
|
141
|
+
db.update(chats)
|
|
142
|
+
.set({ codeWorkspaceId: workspaceId, updatedAt: Date.now() })
|
|
143
|
+
.where(eq(chats.id, chatId))
|
|
144
|
+
.run();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Save a message to a chat. Also updates the chat's updatedAt timestamp.
|
|
149
|
+
* @param {string} chatId
|
|
150
|
+
* @param {string} role - 'user' or 'assistant'
|
|
151
|
+
* @param {string} content
|
|
152
|
+
* @param {string} [id] - Optional message ID
|
|
153
|
+
* @returns {object} The created message
|
|
154
|
+
*/
|
|
155
|
+
export function saveMessage(chatId, role, content, id = null) {
|
|
156
|
+
const db = getDb();
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const message = {
|
|
159
|
+
id: id || randomUUID(),
|
|
160
|
+
chatId,
|
|
161
|
+
role,
|
|
162
|
+
content,
|
|
163
|
+
createdAt: now,
|
|
164
|
+
};
|
|
165
|
+
db.insert(messages).values(message).run();
|
|
166
|
+
db.update(chats).set({ updatedAt: now }).where(eq(chats.id, chatId)).run();
|
|
167
|
+
return message;
|
|
168
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { eq, desc } from 'drizzle-orm';
|
|
3
|
+
import { getDb } from './index.js';
|
|
4
|
+
import { codeWorkspaces } from './schema.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a new code workspace.
|
|
8
|
+
* @param {string} userId
|
|
9
|
+
* @param {object} options
|
|
10
|
+
* @param {string} [options.containerName] - Docker container DNS name (null until launched)
|
|
11
|
+
* @param {string} [options.repo] - GitHub repo full name (e.g. "owner/repo")
|
|
12
|
+
* @param {string} [options.branch] - Git branch name
|
|
13
|
+
* @param {string} [options.title='Code Workspace']
|
|
14
|
+
* @param {string} [options.codingAgent='claude-code'] - Coding agent identifier
|
|
15
|
+
* @param {string} [options.id] - Optional ID (UUID). Generated if not provided.
|
|
16
|
+
* @returns {object} The created workspace
|
|
17
|
+
*/
|
|
18
|
+
export function createCodeWorkspace(userId, { containerName = null, repo = null, branch = null, title = 'Code Workspace', codingAgent = 'claude-code', id = null } = {}) {
|
|
19
|
+
const db = getDb();
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const workspace = {
|
|
22
|
+
id: id || randomUUID(),
|
|
23
|
+
userId,
|
|
24
|
+
containerName,
|
|
25
|
+
repo,
|
|
26
|
+
branch,
|
|
27
|
+
title,
|
|
28
|
+
codingAgent,
|
|
29
|
+
createdAt: now,
|
|
30
|
+
updatedAt: now,
|
|
31
|
+
};
|
|
32
|
+
db.insert(codeWorkspaces).values(workspace).run();
|
|
33
|
+
return workspace;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Update the container name on an existing workspace (when Docker launches).
|
|
38
|
+
* @param {string} id - Workspace ID
|
|
39
|
+
* @param {string} containerName - Docker container name
|
|
40
|
+
*/
|
|
41
|
+
export function updateContainerName(id, containerName) {
|
|
42
|
+
const db = getDb();
|
|
43
|
+
db.update(codeWorkspaces)
|
|
44
|
+
.set({ containerName, updatedAt: Date.now() })
|
|
45
|
+
.where(eq(codeWorkspaces.id, id))
|
|
46
|
+
.run();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get a single code workspace by ID.
|
|
51
|
+
* @param {string} id
|
|
52
|
+
* @returns {object|undefined}
|
|
53
|
+
*/
|
|
54
|
+
export function getCodeWorkspaceById(id) {
|
|
55
|
+
const db = getDb();
|
|
56
|
+
return db.select().from(codeWorkspaces).where(eq(codeWorkspaces.id, id)).get();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get all code workspaces for a user, ordered by most recently updated.
|
|
61
|
+
* @param {string} userId
|
|
62
|
+
* @returns {object[]}
|
|
63
|
+
*/
|
|
64
|
+
export function getCodeWorkspacesByUser(userId) {
|
|
65
|
+
const db = getDb();
|
|
66
|
+
return db
|
|
67
|
+
.select()
|
|
68
|
+
.from(codeWorkspaces)
|
|
69
|
+
.where(eq(codeWorkspaces.userId, userId))
|
|
70
|
+
.orderBy(desc(codeWorkspaces.updatedAt))
|
|
71
|
+
.all();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Update a code workspace's title.
|
|
76
|
+
* @param {string} id
|
|
77
|
+
* @param {string} title
|
|
78
|
+
*/
|
|
79
|
+
export function updateCodeWorkspaceTitle(id, title) {
|
|
80
|
+
const db = getDb();
|
|
81
|
+
db.update(codeWorkspaces)
|
|
82
|
+
.set({ title, updatedAt: Date.now() })
|
|
83
|
+
.where(eq(codeWorkspaces.id, id))
|
|
84
|
+
.run();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Toggle a code workspace's starred status.
|
|
89
|
+
* @param {string} id
|
|
90
|
+
* @returns {number} The new starred value (0 or 1)
|
|
91
|
+
*/
|
|
92
|
+
export function toggleCodeWorkspaceStarred(id) {
|
|
93
|
+
const db = getDb();
|
|
94
|
+
const workspace = db.select({ starred: codeWorkspaces.starred }).from(codeWorkspaces).where(eq(codeWorkspaces.id, id)).get();
|
|
95
|
+
const newValue = workspace?.starred ? 0 : 1;
|
|
96
|
+
db.update(codeWorkspaces)
|
|
97
|
+
.set({ starred: newValue })
|
|
98
|
+
.where(eq(codeWorkspaces.id, id))
|
|
99
|
+
.run();
|
|
100
|
+
return newValue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Delete a code workspace.
|
|
105
|
+
* @param {string} id
|
|
106
|
+
*/
|
|
107
|
+
export function deleteCodeWorkspace(id) {
|
|
108
|
+
const db = getDb();
|
|
109
|
+
db.delete(codeWorkspaces).where(eq(codeWorkspaces.id, id)).run();
|
|
110
|
+
}
|