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/db/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
5
|
+
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
|
|
6
|
+
import { gigaclawDb, dataDir, PROJECT_ROOT } from '../paths.js';
|
|
7
|
+
import * as schema from './schema.js';
|
|
8
|
+
|
|
9
|
+
let _db = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get or create the Drizzle database instance (lazy singleton).
|
|
13
|
+
* @returns {import('drizzle-orm/better-sqlite3').BetterSQLite3Database}
|
|
14
|
+
*/
|
|
15
|
+
export function getDb() {
|
|
16
|
+
if (!_db) {
|
|
17
|
+
// Ensure data directory exists
|
|
18
|
+
if (!fs.existsSync(dataDir)) {
|
|
19
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
const sqlite = new Database(gigaclawDb);
|
|
22
|
+
sqlite.pragma('journal_mode = WAL');
|
|
23
|
+
_db = drizzle(sqlite, { schema });
|
|
24
|
+
}
|
|
25
|
+
return _db;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the database — apply pending migrations.
|
|
30
|
+
* Called from instrumentation.js at server startup.
|
|
31
|
+
* Uses Drizzle Kit migrations from the package's drizzle/ folder.
|
|
32
|
+
*/
|
|
33
|
+
export function initDatabase() {
|
|
34
|
+
if (!fs.existsSync(dataDir)) {
|
|
35
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const sqlite = new Database(gigaclawDb);
|
|
39
|
+
sqlite.pragma('journal_mode = WAL');
|
|
40
|
+
const db = drizzle(sqlite, { schema });
|
|
41
|
+
|
|
42
|
+
// Resolve migrations folder from the installed package.
|
|
43
|
+
// import.meta.url doesn't survive webpack bundling, so resolve from PROJECT_ROOT.
|
|
44
|
+
const migrationsFolder = path.join(PROJECT_ROOT, 'node_modules', 'gigaclaw', 'drizzle');
|
|
45
|
+
|
|
46
|
+
migrate(db, { migrationsFolder });
|
|
47
|
+
|
|
48
|
+
sqlite.close();
|
|
49
|
+
|
|
50
|
+
// Force re-creation of drizzle instance on next getDb() call
|
|
51
|
+
_db = null;
|
|
52
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { eq, desc, sql } from 'drizzle-orm';
|
|
3
|
+
import { getDb } from './index.js';
|
|
4
|
+
import { notifications, subscriptions } from './schema.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a notification, then distribute to all subscribers.
|
|
8
|
+
* @param {string} notificationText - Human-readable notification text
|
|
9
|
+
* @param {object} payload - Raw webhook payload
|
|
10
|
+
* @returns {object} The created notification
|
|
11
|
+
*/
|
|
12
|
+
export async function createNotification(notificationText, payload) {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
const row = {
|
|
16
|
+
id: randomUUID(),
|
|
17
|
+
notification: notificationText,
|
|
18
|
+
payload: JSON.stringify(payload),
|
|
19
|
+
read: 0,
|
|
20
|
+
createdAt: now,
|
|
21
|
+
};
|
|
22
|
+
db.insert(notifications).values(row).run();
|
|
23
|
+
|
|
24
|
+
// Distribute to subscribers (fire-and-forget)
|
|
25
|
+
distributeNotification(notificationText).catch((err) => {
|
|
26
|
+
console.error('Failed to distribute notification:', err);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return row;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get all notifications, newest first.
|
|
34
|
+
* @returns {object[]}
|
|
35
|
+
*/
|
|
36
|
+
export function getNotifications() {
|
|
37
|
+
const db = getDb();
|
|
38
|
+
return db
|
|
39
|
+
.select()
|
|
40
|
+
.from(notifications)
|
|
41
|
+
.orderBy(desc(notifications.createdAt))
|
|
42
|
+
.all();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get count of unread notifications.
|
|
47
|
+
* @returns {number}
|
|
48
|
+
*/
|
|
49
|
+
export function getUnreadCount() {
|
|
50
|
+
const db = getDb();
|
|
51
|
+
const result = db
|
|
52
|
+
.select({ count: sql`count(*)` })
|
|
53
|
+
.from(notifications)
|
|
54
|
+
.where(eq(notifications.read, 0))
|
|
55
|
+
.get();
|
|
56
|
+
return result?.count ?? 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Mark all notifications as read.
|
|
61
|
+
*/
|
|
62
|
+
export function markAllRead() {
|
|
63
|
+
const db = getDb();
|
|
64
|
+
db.update(notifications)
|
|
65
|
+
.set({ read: 1 })
|
|
66
|
+
.where(eq(notifications.read, 0))
|
|
67
|
+
.run();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get all subscriptions.
|
|
72
|
+
* @returns {object[]}
|
|
73
|
+
*/
|
|
74
|
+
export function getSubscriptions() {
|
|
75
|
+
const db = getDb();
|
|
76
|
+
return db.select().from(subscriptions).all();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Distribute a notification to all subscribers.
|
|
81
|
+
* @param {string} notificationText - The notification message
|
|
82
|
+
*/
|
|
83
|
+
async function distributeNotification(notificationText) {
|
|
84
|
+
const subs = getSubscriptions();
|
|
85
|
+
if (!subs.length) return;
|
|
86
|
+
|
|
87
|
+
for (const sub of subs) {
|
|
88
|
+
try {
|
|
89
|
+
if (sub.platform === 'telegram') {
|
|
90
|
+
const botToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
91
|
+
if (!botToken) continue;
|
|
92
|
+
const { sendMessage } = await import('../tools/telegram.js');
|
|
93
|
+
await sendMessage(botToken, sub.channelId, notificationText);
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error(`Failed to send to ${sub.platform}/${sub.channelId}:`, err);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
package/lib/db/schema.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
|
|
3
|
+
export const users = sqliteTable('users', {
|
|
4
|
+
id: text('id').primaryKey(),
|
|
5
|
+
email: text('email').notNull().unique(),
|
|
6
|
+
passwordHash: text('password_hash').notNull(),
|
|
7
|
+
role: text('role').notNull().default('admin'),
|
|
8
|
+
createdAt: integer('created_at').notNull(),
|
|
9
|
+
updatedAt: integer('updated_at').notNull(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const chats = sqliteTable('chats', {
|
|
13
|
+
id: text('id').primaryKey(),
|
|
14
|
+
userId: text('user_id').notNull(),
|
|
15
|
+
title: text('title').notNull().default('New Chat'),
|
|
16
|
+
starred: integer('starred').notNull().default(0),
|
|
17
|
+
codeWorkspaceId: text('code_workspace_id'),
|
|
18
|
+
createdAt: integer('created_at').notNull(),
|
|
19
|
+
updatedAt: integer('updated_at').notNull(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const messages = sqliteTable('messages', {
|
|
23
|
+
id: text('id').primaryKey(),
|
|
24
|
+
chatId: text('chat_id').notNull(),
|
|
25
|
+
role: text('role').notNull(),
|
|
26
|
+
content: text('content').notNull(),
|
|
27
|
+
createdAt: integer('created_at').notNull(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const notifications = sqliteTable('notifications', {
|
|
31
|
+
id: text('id').primaryKey(),
|
|
32
|
+
notification: text('notification').notNull(),
|
|
33
|
+
payload: text('payload').notNull(),
|
|
34
|
+
read: integer('read').notNull().default(0),
|
|
35
|
+
createdAt: integer('created_at').notNull(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const subscriptions = sqliteTable('subscriptions', {
|
|
39
|
+
id: text('id').primaryKey(),
|
|
40
|
+
platform: text('platform').notNull(),
|
|
41
|
+
channelId: text('channel_id').notNull(),
|
|
42
|
+
createdAt: integer('created_at').notNull(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const codeWorkspaces = sqliteTable('code_workspaces', {
|
|
46
|
+
id: text('id').primaryKey(),
|
|
47
|
+
userId: text('user_id').notNull(),
|
|
48
|
+
containerName: text('container_name').unique(),
|
|
49
|
+
repo: text('repo'),
|
|
50
|
+
branch: text('branch'),
|
|
51
|
+
title: text('title').notNull().default('Code Workspace'),
|
|
52
|
+
codingAgent: text('coding_agent').notNull().default('claude-code'),
|
|
53
|
+
starred: integer('starred').notNull().default(0),
|
|
54
|
+
createdAt: integer('created_at').notNull(),
|
|
55
|
+
updatedAt: integer('updated_at').notNull(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const settings = sqliteTable('settings', {
|
|
59
|
+
id: text('id').primaryKey(),
|
|
60
|
+
type: text('type').notNull(),
|
|
61
|
+
key: text('key').notNull(),
|
|
62
|
+
value: text('value').notNull(),
|
|
63
|
+
createdBy: text('created_by'),
|
|
64
|
+
createdAt: integer('created_at').notNull(),
|
|
65
|
+
updatedAt: integer('updated_at').notNull(),
|
|
66
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { eq, and } from 'drizzle-orm';
|
|
3
|
+
import { getDb } from './index.js';
|
|
4
|
+
import { settings } from './schema.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the stored available version from the DB.
|
|
8
|
+
* @returns {string|null}
|
|
9
|
+
*/
|
|
10
|
+
export function getAvailableVersion() {
|
|
11
|
+
const db = getDb();
|
|
12
|
+
const row = db
|
|
13
|
+
.select()
|
|
14
|
+
.from(settings)
|
|
15
|
+
.where(and(eq(settings.type, 'update'), eq(settings.key, 'available_version')))
|
|
16
|
+
.get();
|
|
17
|
+
|
|
18
|
+
return row ? row.value : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Set the available version in the DB (delete + insert upsert).
|
|
23
|
+
* @param {string} version
|
|
24
|
+
*/
|
|
25
|
+
export function setAvailableVersion(version) {
|
|
26
|
+
const db = getDb();
|
|
27
|
+
db.delete(settings)
|
|
28
|
+
.where(and(eq(settings.type, 'update'), eq(settings.key, 'available_version')))
|
|
29
|
+
.run();
|
|
30
|
+
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
db.insert(settings).values({
|
|
33
|
+
id: randomUUID(),
|
|
34
|
+
type: 'update',
|
|
35
|
+
key: 'available_version',
|
|
36
|
+
value: version,
|
|
37
|
+
createdAt: now,
|
|
38
|
+
updatedAt: now,
|
|
39
|
+
}).run();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Clear the available version from the DB.
|
|
44
|
+
*/
|
|
45
|
+
export function clearAvailableVersion() {
|
|
46
|
+
const db = getDb();
|
|
47
|
+
db.delete(settings)
|
|
48
|
+
.where(and(eq(settings.type, 'update'), eq(settings.key, 'available_version')))
|
|
49
|
+
.run();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the stored release notes from the DB.
|
|
54
|
+
* @returns {string|null}
|
|
55
|
+
*/
|
|
56
|
+
export function getReleaseNotes() {
|
|
57
|
+
const db = getDb();
|
|
58
|
+
const row = db
|
|
59
|
+
.select()
|
|
60
|
+
.from(settings)
|
|
61
|
+
.where(and(eq(settings.type, 'update'), eq(settings.key, 'release_notes')))
|
|
62
|
+
.get();
|
|
63
|
+
|
|
64
|
+
return row ? row.value : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Set the release notes in the DB (delete + insert upsert).
|
|
69
|
+
* @param {string} notes
|
|
70
|
+
*/
|
|
71
|
+
export function setReleaseNotes(notes) {
|
|
72
|
+
const db = getDb();
|
|
73
|
+
db.delete(settings)
|
|
74
|
+
.where(and(eq(settings.type, 'update'), eq(settings.key, 'release_notes')))
|
|
75
|
+
.run();
|
|
76
|
+
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
db.insert(settings).values({
|
|
79
|
+
id: randomUUID(),
|
|
80
|
+
type: 'update',
|
|
81
|
+
key: 'release_notes',
|
|
82
|
+
value: notes,
|
|
83
|
+
createdAt: now,
|
|
84
|
+
updatedAt: now,
|
|
85
|
+
}).run();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Clear the release notes from the DB.
|
|
90
|
+
*/
|
|
91
|
+
export function clearReleaseNotes() {
|
|
92
|
+
const db = getDb();
|
|
93
|
+
db.delete(settings)
|
|
94
|
+
.where(and(eq(settings.type, 'update'), eq(settings.key, 'release_notes')))
|
|
95
|
+
.run();
|
|
96
|
+
}
|
package/lib/db/users.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { hashSync, genSaltSync, compare } from 'bcrypt-ts';
|
|
3
|
+
import { eq, sql } from 'drizzle-orm';
|
|
4
|
+
import { getDb } from './index.js';
|
|
5
|
+
import { users } from './schema.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get the total number of users.
|
|
9
|
+
* Used to detect first-time setup (no users = needs setup).
|
|
10
|
+
* @returns {number}
|
|
11
|
+
*/
|
|
12
|
+
export function getUserCount() {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
const result = db.select({ count: sql`count(*)` }).from(users).get();
|
|
15
|
+
return result?.count ?? 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Find a user by email address.
|
|
20
|
+
* @param {string} email
|
|
21
|
+
* @returns {object|undefined}
|
|
22
|
+
*/
|
|
23
|
+
export function getUserByEmail(email) {
|
|
24
|
+
const db = getDb();
|
|
25
|
+
return db.select().from(users).where(eq(users.email, email.toLowerCase())).get();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a new user with a hashed password.
|
|
30
|
+
* @param {string} email
|
|
31
|
+
* @param {string} password - Plain text password (will be hashed)
|
|
32
|
+
* @returns {object} The created user (without password_hash)
|
|
33
|
+
*/
|
|
34
|
+
export async function createUser(email, password) {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const passwordHash = hashSync(password, genSaltSync(10));
|
|
38
|
+
|
|
39
|
+
const user = {
|
|
40
|
+
id: randomUUID(),
|
|
41
|
+
email: email.toLowerCase(),
|
|
42
|
+
passwordHash: passwordHash,
|
|
43
|
+
role: 'admin',
|
|
44
|
+
createdAt: now,
|
|
45
|
+
updatedAt: now,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
db.insert(users).values(user).run();
|
|
49
|
+
|
|
50
|
+
return { id: user.id, email: user.email, role: user.role };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Atomically create the first user (admin) if no users exist.
|
|
55
|
+
* Uses a transaction to prevent race conditions — only one caller wins.
|
|
56
|
+
* @param {string} email
|
|
57
|
+
* @param {string} password - Plain text password (will be hashed)
|
|
58
|
+
* @returns {object|null} The created user, or null if users already exist
|
|
59
|
+
*/
|
|
60
|
+
export function createFirstUser(email, password) {
|
|
61
|
+
const db = getDb();
|
|
62
|
+
return db.transaction((tx) => {
|
|
63
|
+
const count = tx.select({ count: sql`count(*)` }).from(users).get();
|
|
64
|
+
if (count?.count > 0) return null;
|
|
65
|
+
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const passwordHash = hashSync(password, genSaltSync(10));
|
|
68
|
+
const user = {
|
|
69
|
+
id: randomUUID(),
|
|
70
|
+
email: email.toLowerCase(),
|
|
71
|
+
passwordHash: passwordHash,
|
|
72
|
+
role: 'admin',
|
|
73
|
+
createdAt: now,
|
|
74
|
+
updatedAt: now,
|
|
75
|
+
};
|
|
76
|
+
tx.insert(users).values(user).run();
|
|
77
|
+
return { id: user.id, email: user.email, role: user.role };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Verify a password against a user's stored hash.
|
|
83
|
+
* @param {object} user - User object with password_hash field
|
|
84
|
+
* @param {string} password - Plain text password to verify
|
|
85
|
+
* @returns {Promise<boolean>}
|
|
86
|
+
*/
|
|
87
|
+
export async function verifyPassword(user, password) {
|
|
88
|
+
return compare(password, user.passwordHash);
|
|
89
|
+
}
|
package/lib/paths.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Central path resolver for gigaclaw.
|
|
5
|
+
* All paths resolve from process.cwd() (the user's project root).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PROJECT_ROOT = process.cwd();
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
PROJECT_ROOT,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// config/ files
|
|
15
|
+
export const configDir = path.join(PROJECT_ROOT, 'config');
|
|
16
|
+
export const cronsFile = path.join(PROJECT_ROOT, 'config', 'CRONS.json');
|
|
17
|
+
export const triggersFile = path.join(PROJECT_ROOT, 'config', 'TRIGGERS.json');
|
|
18
|
+
export const jobPlanningMd = path.join(PROJECT_ROOT, 'config', 'JOB_PLANNING.md');
|
|
19
|
+
export const codePlanningMd = path.join(PROJECT_ROOT, 'config', 'CODE_PLANNING.md');
|
|
20
|
+
export const jobSummaryMd = path.join(PROJECT_ROOT, 'config', 'JOB_SUMMARY.md');
|
|
21
|
+
export const soulMd = path.join(PROJECT_ROOT, 'config', 'SOUL.md');
|
|
22
|
+
export const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
|
|
23
|
+
export const skillGuidePath = path.join(PROJECT_ROOT, 'config', 'SKILL_BUILDING_GUIDE.md');
|
|
24
|
+
|
|
25
|
+
// Skills directory
|
|
26
|
+
export const skillsDir = path.join(PROJECT_ROOT, 'skills');
|
|
27
|
+
|
|
28
|
+
// Working directories for command-type actions
|
|
29
|
+
export const cronDir = path.join(PROJECT_ROOT, 'cron');
|
|
30
|
+
export const triggersDir = path.join(PROJECT_ROOT, 'triggers');
|
|
31
|
+
|
|
32
|
+
// Logs
|
|
33
|
+
export const logsDir = path.join(PROJECT_ROOT, 'logs');
|
|
34
|
+
|
|
35
|
+
// Data (SQLite memory, etc.)
|
|
36
|
+
export const dataDir = path.join(PROJECT_ROOT, 'data');
|
|
37
|
+
|
|
38
|
+
// Database
|
|
39
|
+
export const gigaclawDb = process.env.DATABASE_PATH || path.join(PROJECT_ROOT, 'data', 'gigaclaw.sqlite');
|
|
40
|
+
|
|
41
|
+
// .env
|
|
42
|
+
export const envFile = path.join(PROJECT_ROOT, '.env');
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { githubApi } from './github.js';
|
|
4
|
+
import { createModel } from '../ai/model.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate a short descriptive title for a job using the LLM.
|
|
8
|
+
* Uses structured output to avoid thinking-token leaks with extended-thinking models.
|
|
9
|
+
* @param {string} jobDescription - The full job description
|
|
10
|
+
* @returns {Promise<string>} ~10 word title
|
|
11
|
+
*/
|
|
12
|
+
async function generateJobTitle(jobDescription) {
|
|
13
|
+
try {
|
|
14
|
+
const model = await createModel({ maxTokens: 100 });
|
|
15
|
+
const response = await model.withStructuredOutput(z.object({ title: z.string() })).invoke([
|
|
16
|
+
['system', 'Generate a descriptive ~10 word title for this agent job. The title should clearly describe what the job will do.'],
|
|
17
|
+
['human', jobDescription],
|
|
18
|
+
]);
|
|
19
|
+
return response.title.trim() || jobDescription.slice(0, 80);
|
|
20
|
+
} catch {
|
|
21
|
+
// Fallback: first line, truncated
|
|
22
|
+
const firstLine = jobDescription.split('\n').find(l => l.trim()) || jobDescription;
|
|
23
|
+
return firstLine.replace(/^#+\s*/, '').trim().split(/\s+/).slice(0, 10).join(' ');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a new job branch with job.config.json
|
|
29
|
+
* @param {string} jobDescription - The job description
|
|
30
|
+
* @param {Object} [options] - Optional overrides
|
|
31
|
+
* @param {string} [options.llmProvider] - LLM provider override (e.g. 'openai', 'anthropic')
|
|
32
|
+
* @param {string} [options.llmModel] - LLM model override (e.g. 'gpt-4o', 'claude-sonnet-4-5-20250929')
|
|
33
|
+
* @param {string} [options.agentBackend] - Agent backend override ('pi' or 'claude-code')
|
|
34
|
+
* @returns {Promise<{job_id: string, branch: string, title: string}>} - Job ID, branch name, and title
|
|
35
|
+
*/
|
|
36
|
+
async function createJob(jobDescription, options = {}) {
|
|
37
|
+
const { GH_OWNER, GH_REPO } = process.env;
|
|
38
|
+
const jobId = uuidv4();
|
|
39
|
+
const branch = `job/${jobId}`;
|
|
40
|
+
const repo = `/repos/${GH_OWNER}/${GH_REPO}`;
|
|
41
|
+
|
|
42
|
+
// Generate a short descriptive title
|
|
43
|
+
const title = await generateJobTitle(jobDescription);
|
|
44
|
+
|
|
45
|
+
// 1. Get main branch SHA and its tree SHA
|
|
46
|
+
const mainRef = await githubApi(`${repo}/git/ref/heads/main`);
|
|
47
|
+
const mainSha = mainRef.object.sha;
|
|
48
|
+
const mainCommit = await githubApi(`${repo}/git/commits/${mainSha}`);
|
|
49
|
+
const baseTreeSha = mainCommit.tree.sha;
|
|
50
|
+
|
|
51
|
+
// 2. Build job.config.json — single source of truth for job metadata
|
|
52
|
+
const config = { title, job: jobDescription };
|
|
53
|
+
if (options.llmProvider) config.llm_provider = options.llmProvider;
|
|
54
|
+
if (options.llmModel) config.llm_model = options.llmModel;
|
|
55
|
+
if (options.agentBackend) config.agent_backend = options.agentBackend;
|
|
56
|
+
|
|
57
|
+
const treeEntries = [
|
|
58
|
+
{
|
|
59
|
+
path: `logs/${jobId}/job.config.json`,
|
|
60
|
+
mode: '100644',
|
|
61
|
+
type: 'blob',
|
|
62
|
+
content: JSON.stringify(config, null, 2),
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// 3. Create tree (base_tree preserves all existing files)
|
|
67
|
+
const tree = await githubApi(`${repo}/git/trees`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
base_tree: baseTreeSha,
|
|
71
|
+
tree: treeEntries,
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 4. Create a single commit with job config
|
|
76
|
+
const commit = await githubApi(`${repo}/git/commits`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
message: `🤖 Agent Job: ${title}`,
|
|
80
|
+
tree: tree.sha,
|
|
81
|
+
parents: [mainSha],
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 5. Create branch pointing to the commit (triggers run-job.yml)
|
|
86
|
+
await githubApi(`${repo}/git/refs`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
ref: `refs/heads/${branch}`,
|
|
90
|
+
sha: commit.sha,
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return { job_id: jobId, branch, title };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { createJob };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Make a request to the Docker Engine API via Unix socket.
|
|
5
|
+
* @param {string} method - HTTP method
|
|
6
|
+
* @param {string} path - API path
|
|
7
|
+
* @param {object} [body] - Request body
|
|
8
|
+
* @returns {Promise<object>} Parsed JSON response
|
|
9
|
+
*/
|
|
10
|
+
function dockerApi(method, path, body = null) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const options = {
|
|
13
|
+
socketPath: '/var/run/docker.sock',
|
|
14
|
+
path,
|
|
15
|
+
method,
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const req = http.request(options, (res) => {
|
|
20
|
+
let data = '';
|
|
21
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
22
|
+
res.on('end', () => {
|
|
23
|
+
try {
|
|
24
|
+
resolve({ status: res.statusCode, data: data ? JSON.parse(data) : {} });
|
|
25
|
+
} catch {
|
|
26
|
+
resolve({ status: res.statusCode, data: { message: data } });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
req.on('error', reject);
|
|
32
|
+
if (body) req.write(JSON.stringify(body));
|
|
33
|
+
req.end();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Auto-detect the Docker network by inspecting the event-handler container.
|
|
39
|
+
* @returns {Promise<string>} Network name
|
|
40
|
+
*/
|
|
41
|
+
async function detectNetwork() {
|
|
42
|
+
try {
|
|
43
|
+
const { status, data } = await dockerApi('GET', '/containers/gigaclaw-event-handler/json');
|
|
44
|
+
if (status === 200 && data.NetworkSettings?.Networks) {
|
|
45
|
+
const networks = Object.keys(data.NetworkSettings.Networks);
|
|
46
|
+
if (networks.length > 0) return networks[0];
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
return 'bridge';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create and start a code workspace Docker container.
|
|
54
|
+
* @param {object} options
|
|
55
|
+
* @param {string} options.containerName - Docker container name
|
|
56
|
+
* @param {string} options.repo - GitHub repo full name (e.g. "owner/repo")
|
|
57
|
+
* @param {string} options.branch - Git branch name
|
|
58
|
+
* @param {string} [options.codingAgent='claude-code'] - Coding agent identifier
|
|
59
|
+
* @returns {Promise<{containerId: string}>}
|
|
60
|
+
*/
|
|
61
|
+
async function createCodeWorkspaceContainer({ containerName, repo, branch, codingAgent = 'claude-code' }) {
|
|
62
|
+
if (codingAgent !== 'claude-code') {
|
|
63
|
+
throw new Error(`Unsupported coding agent: ${codingAgent}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const version = process.env.GIGACLAW_VERSION || 'latest';
|
|
67
|
+
const image = `gignaati/gigaclaw:claude-code-workspace-${version}`;
|
|
68
|
+
const network = await detectNetwork();
|
|
69
|
+
|
|
70
|
+
const env = [
|
|
71
|
+
`REPO=${repo}`,
|
|
72
|
+
`BRANCH=${branch}`,
|
|
73
|
+
];
|
|
74
|
+
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
75
|
+
env.push(`CLAUDE_CODE_OAUTH_TOKEN=${process.env.CLAUDE_CODE_OAUTH_TOKEN}`);
|
|
76
|
+
}
|
|
77
|
+
if (process.env.GH_TOKEN) {
|
|
78
|
+
env.push(`GH_TOKEN=${process.env.GH_TOKEN}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Pull image only if not already present locally
|
|
82
|
+
const inspectRes = await dockerApi('GET', `/images/${encodeURIComponent(image)}/json`);
|
|
83
|
+
if (inspectRes.status !== 200) {
|
|
84
|
+
const pullRes = await dockerApi('POST', `/images/create?fromImage=${encodeURIComponent('gignaati/gigaclaw')}&tag=${encodeURIComponent(`claude-code-workspace-${version}`)}`);
|
|
85
|
+
if (pullRes.status !== 200) {
|
|
86
|
+
throw new Error(`Docker pull failed (${pullRes.status}): ${pullRes.data?.message || JSON.stringify(pullRes.data)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Create container
|
|
91
|
+
const createRes = await dockerApi('POST', `/containers/create?name=${encodeURIComponent(containerName)}`, {
|
|
92
|
+
Image: image,
|
|
93
|
+
Env: env,
|
|
94
|
+
HostConfig: {
|
|
95
|
+
NetworkMode: network,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (createRes.status !== 201) {
|
|
100
|
+
throw new Error(`Docker create failed (${createRes.status}): ${createRes.data?.message || JSON.stringify(createRes.data)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const containerId = createRes.data.Id;
|
|
104
|
+
|
|
105
|
+
// Start container
|
|
106
|
+
const startRes = await dockerApi('POST', `/containers/${containerId}/start`);
|
|
107
|
+
if (startRes.status !== 204 && startRes.status !== 304) {
|
|
108
|
+
throw new Error(`Docker start failed (${startRes.status}): ${startRes.data?.message || JSON.stringify(startRes.data)}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { containerId };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Inspect a Docker container by name.
|
|
116
|
+
* @param {string} containerName
|
|
117
|
+
* @returns {Promise<object|null>} Container info or null if not found
|
|
118
|
+
*/
|
|
119
|
+
async function inspectContainer(containerName) {
|
|
120
|
+
const { status, data } = await dockerApi('GET', `/containers/${encodeURIComponent(containerName)}/json`);
|
|
121
|
+
if (status === 404) return null;
|
|
122
|
+
if (status === 200) return data;
|
|
123
|
+
throw new Error(`Docker inspect failed (${status}): ${data?.message || JSON.stringify(data)}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Start a stopped Docker container.
|
|
128
|
+
* @param {string} containerName
|
|
129
|
+
*/
|
|
130
|
+
async function startContainer(containerName) {
|
|
131
|
+
const { status, data } = await dockerApi('POST', `/containers/${encodeURIComponent(containerName)}/start`);
|
|
132
|
+
if (status === 204 || status === 304) return;
|
|
133
|
+
throw new Error(`Docker start failed (${status}): ${data?.message || JSON.stringify(data)}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Force-remove a Docker container.
|
|
138
|
+
* @param {string} containerName
|
|
139
|
+
*/
|
|
140
|
+
async function removeContainer(containerName) {
|
|
141
|
+
const { status, data } = await dockerApi('DELETE', `/containers/${encodeURIComponent(containerName)}?force=true`);
|
|
142
|
+
if (status === 204 || status === 404) return;
|
|
143
|
+
throw new Error(`Docker remove failed (${status}): ${data?.message || JSON.stringify(data)}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { createCodeWorkspaceContainer, inspectContainer, startContainer, removeContainer };
|