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.
Files changed (249) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +237 -0
  3. package/api/CLAUDE.md +19 -0
  4. package/api/index.js +265 -0
  5. package/bin/cli.js +823 -0
  6. package/bin/local.sh +85 -0
  7. package/bin/postinstall.js +63 -0
  8. package/config/index.js +26 -0
  9. package/config/instrumentation.js +62 -0
  10. package/drizzle/0000_initial.sql +52 -0
  11. package/drizzle/0001_nostalgic_sersi.sql +11 -0
  12. package/drizzle/0002_black_daimon_hellstrom.sql +19 -0
  13. package/drizzle/0003_rename_code_workspaces.sql +5 -0
  14. package/drizzle/meta/0000_snapshot.json +321 -0
  15. package/drizzle/meta/0001_snapshot.json +390 -0
  16. package/drizzle/meta/0002_snapshot.json +411 -0
  17. package/drizzle/meta/0003_snapshot.json +419 -0
  18. package/drizzle/meta/_journal.json +34 -0
  19. package/lib/actions.js +44 -0
  20. package/lib/ai/agent.js +86 -0
  21. package/lib/ai/index.js +342 -0
  22. package/lib/ai/model.js +180 -0
  23. package/lib/ai/tools.js +269 -0
  24. package/lib/ai/web-search.js +42 -0
  25. package/lib/auth/actions.js +28 -0
  26. package/lib/auth/config.js +27 -0
  27. package/lib/auth/edge-config.js +27 -0
  28. package/lib/auth/index.js +27 -0
  29. package/lib/auth/middleware.js +62 -0
  30. package/lib/channels/base.js +56 -0
  31. package/lib/channels/index.js +15 -0
  32. package/lib/channels/telegram.js +148 -0
  33. package/lib/chat/actions.js +579 -0
  34. package/lib/chat/api.js +140 -0
  35. package/lib/chat/components/app-sidebar.js +213 -0
  36. package/lib/chat/components/app-sidebar.jsx +279 -0
  37. package/lib/chat/components/chat-header.js +192 -0
  38. package/lib/chat/components/chat-header.jsx +223 -0
  39. package/lib/chat/components/chat-input.js +236 -0
  40. package/lib/chat/components/chat-input.jsx +249 -0
  41. package/lib/chat/components/chat-nav-context.js +11 -0
  42. package/lib/chat/components/chat-nav-context.jsx +11 -0
  43. package/lib/chat/components/chat-page.js +99 -0
  44. package/lib/chat/components/chat-page.jsx +121 -0
  45. package/lib/chat/components/chat.js +153 -0
  46. package/lib/chat/components/chat.jsx +199 -0
  47. package/lib/chat/components/chats-page.js +367 -0
  48. package/lib/chat/components/chats-page.jsx +394 -0
  49. package/lib/chat/components/code-mode-toggle.js +132 -0
  50. package/lib/chat/components/code-mode-toggle.jsx +163 -0
  51. package/lib/chat/components/crons-page.js +172 -0
  52. package/lib/chat/components/crons-page.jsx +244 -0
  53. package/lib/chat/components/greeting.js +11 -0
  54. package/lib/chat/components/greeting.jsx +16 -0
  55. package/lib/chat/components/icons.js +805 -0
  56. package/lib/chat/components/icons.jsx +751 -0
  57. package/lib/chat/components/index.js +20 -0
  58. package/lib/chat/components/message.js +363 -0
  59. package/lib/chat/components/message.jsx +422 -0
  60. package/lib/chat/components/messages.js +65 -0
  61. package/lib/chat/components/messages.jsx +74 -0
  62. package/lib/chat/components/notifications-page.js +56 -0
  63. package/lib/chat/components/notifications-page.jsx +87 -0
  64. package/lib/chat/components/page-layout.js +21 -0
  65. package/lib/chat/components/page-layout.jsx +28 -0
  66. package/lib/chat/components/pull-requests-page.js +103 -0
  67. package/lib/chat/components/pull-requests-page.jsx +113 -0
  68. package/lib/chat/components/settings-layout.js +39 -0
  69. package/lib/chat/components/settings-layout.jsx +53 -0
  70. package/lib/chat/components/settings-secrets-page.js +216 -0
  71. package/lib/chat/components/settings-secrets-page.jsx +264 -0
  72. package/lib/chat/components/sidebar-history-item.js +138 -0
  73. package/lib/chat/components/sidebar-history-item.jsx +119 -0
  74. package/lib/chat/components/sidebar-history.js +167 -0
  75. package/lib/chat/components/sidebar-history.jsx +220 -0
  76. package/lib/chat/components/sidebar-user-nav.js +61 -0
  77. package/lib/chat/components/sidebar-user-nav.jsx +77 -0
  78. package/lib/chat/components/swarm-page.js +157 -0
  79. package/lib/chat/components/swarm-page.jsx +210 -0
  80. package/lib/chat/components/tool-call.js +89 -0
  81. package/lib/chat/components/tool-call.jsx +107 -0
  82. package/lib/chat/components/triggers-page.js +153 -0
  83. package/lib/chat/components/triggers-page.jsx +221 -0
  84. package/lib/chat/components/ui/combobox.js +98 -0
  85. package/lib/chat/components/ui/combobox.jsx +114 -0
  86. package/lib/chat/components/ui/confirm-dialog.js +53 -0
  87. package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
  88. package/lib/chat/components/ui/dropdown-menu.js +194 -0
  89. package/lib/chat/components/ui/dropdown-menu.jsx +215 -0
  90. package/lib/chat/components/ui/rename-dialog.js +78 -0
  91. package/lib/chat/components/ui/rename-dialog.jsx +74 -0
  92. package/lib/chat/components/ui/scroll-area.js +13 -0
  93. package/lib/chat/components/ui/scroll-area.jsx +17 -0
  94. package/lib/chat/components/ui/separator.js +21 -0
  95. package/lib/chat/components/ui/separator.jsx +18 -0
  96. package/lib/chat/components/ui/sheet.js +75 -0
  97. package/lib/chat/components/ui/sheet.jsx +95 -0
  98. package/lib/chat/components/ui/sidebar.js +228 -0
  99. package/lib/chat/components/ui/sidebar.jsx +246 -0
  100. package/lib/chat/components/ui/tooltip.js +56 -0
  101. package/lib/chat/components/ui/tooltip.jsx +66 -0
  102. package/lib/chat/components/upgrade-dialog.js +151 -0
  103. package/lib/chat/components/upgrade-dialog.jsx +170 -0
  104. package/lib/chat/utils.js +11 -0
  105. package/lib/code/actions.js +153 -0
  106. package/lib/code/code-page.js +22 -0
  107. package/lib/code/code-page.jsx +25 -0
  108. package/lib/code/index.js +1 -0
  109. package/lib/code/terminal-view.js +201 -0
  110. package/lib/code/terminal-view.jsx +224 -0
  111. package/lib/code/ws-proxy.js +80 -0
  112. package/lib/cron.js +246 -0
  113. package/lib/db/api-keys.js +163 -0
  114. package/lib/db/chats.js +168 -0
  115. package/lib/db/code-workspaces.js +110 -0
  116. package/lib/db/index.js +52 -0
  117. package/lib/db/notifications.js +99 -0
  118. package/lib/db/schema.js +66 -0
  119. package/lib/db/update-check.js +96 -0
  120. package/lib/db/users.js +89 -0
  121. package/lib/paths.js +42 -0
  122. package/lib/tools/create-job.js +97 -0
  123. package/lib/tools/docker.js +146 -0
  124. package/lib/tools/github.js +271 -0
  125. package/lib/tools/openai.js +35 -0
  126. package/lib/tools/telegram.js +292 -0
  127. package/lib/triggers.js +104 -0
  128. package/lib/utils/render-md.js +111 -0
  129. package/package.json +118 -0
  130. package/setup/lib/auth.mjs +81 -0
  131. package/setup/lib/env.mjs +21 -0
  132. package/setup/lib/fs-utils.mjs +20 -0
  133. package/setup/lib/github.mjs +149 -0
  134. package/setup/lib/prerequisites.mjs +155 -0
  135. package/setup/lib/prompts.mjs +267 -0
  136. package/setup/lib/providers.mjs +105 -0
  137. package/setup/lib/sync.mjs +125 -0
  138. package/setup/lib/targets.mjs +45 -0
  139. package/setup/lib/telegram-verify.mjs +63 -0
  140. package/setup/lib/telegram.mjs +76 -0
  141. package/setup/setup-cloud.mjs +833 -0
  142. package/setup/setup-local.mjs +377 -0
  143. package/setup/setup-telegram.mjs +265 -0
  144. package/setup/setup.mjs +87 -0
  145. package/templates/.dockerignore +5 -0
  146. package/templates/.env.example +104 -0
  147. package/templates/.github/workflows/auto-merge.yml +117 -0
  148. package/templates/.github/workflows/notify-job-failed.yml +64 -0
  149. package/templates/.github/workflows/notify-pr-complete.yml +119 -0
  150. package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
  151. package/templates/.github/workflows/run-job.yml +89 -0
  152. package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
  153. package/templates/.gitignore.template +45 -0
  154. package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
  155. package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
  156. package/templates/CLAUDE.md +29 -0
  157. package/templates/CLAUDE.md.template +308 -0
  158. package/templates/app/api/[...gigaclaw]/route.js +1 -0
  159. package/templates/app/api/auth/[...nextauth]/route.js +1 -0
  160. package/templates/app/chat/[chatId]/page.js +9 -0
  161. package/templates/app/chats/page.js +7 -0
  162. package/templates/app/code/[codeWorkspaceId]/page.js +9 -0
  163. package/templates/app/components/ascii-logo.jsx +12 -0
  164. package/templates/app/components/login-form.jsx +92 -0
  165. package/templates/app/components/setup-form.jsx +82 -0
  166. package/templates/app/components/theme-provider.jsx +11 -0
  167. package/templates/app/components/theme-toggle.jsx +38 -0
  168. package/templates/app/components/ui/button.jsx +21 -0
  169. package/templates/app/components/ui/card.jsx +23 -0
  170. package/templates/app/components/ui/input.jsx +10 -0
  171. package/templates/app/components/ui/label.jsx +10 -0
  172. package/templates/app/crons/page.js +5 -0
  173. package/templates/app/globals.css +90 -0
  174. package/templates/app/layout.js +33 -0
  175. package/templates/app/login/page.js +15 -0
  176. package/templates/app/notifications/page.js +7 -0
  177. package/templates/app/page.js +7 -0
  178. package/templates/app/pull-requests/page.js +7 -0
  179. package/templates/app/settings/crons/page.js +5 -0
  180. package/templates/app/settings/layout.js +7 -0
  181. package/templates/app/settings/page.js +5 -0
  182. package/templates/app/settings/secrets/page.js +5 -0
  183. package/templates/app/settings/triggers/page.js +5 -0
  184. package/templates/app/stream/chat/route.js +1 -0
  185. package/templates/app/swarm/page.js +7 -0
  186. package/templates/app/triggers/page.js +5 -0
  187. package/templates/config/CODE_PLANNING.md +14 -0
  188. package/templates/config/CRONS.json +56 -0
  189. package/templates/config/HEARTBEAT.md +3 -0
  190. package/templates/config/JOB_AGENT.md +30 -0
  191. package/templates/config/JOB_PLANNING.md +240 -0
  192. package/templates/config/JOB_SUMMARY.md +130 -0
  193. package/templates/config/SKILL_BUILDING_GUIDE.md +96 -0
  194. package/templates/config/SOUL.md +48 -0
  195. package/templates/config/TRIGGERS.json +58 -0
  196. package/templates/config/WEB_SEARCH_AVAILABLE.md +5 -0
  197. package/templates/config/WEB_SEARCH_UNAVAILABLE.md +3 -0
  198. package/templates/docker/claude-code-job/Dockerfile +34 -0
  199. package/templates/docker/claude-code-job/entrypoint.sh +149 -0
  200. package/templates/docker/claude-code-workspace/.tmux.conf +5 -0
  201. package/templates/docker/claude-code-workspace/Dockerfile +61 -0
  202. package/templates/docker/claude-code-workspace/entrypoint.sh +51 -0
  203. package/templates/docker/event-handler/Dockerfile +20 -0
  204. package/templates/docker/event-handler/ecosystem.config.cjs +7 -0
  205. package/templates/docker/pi-coding-agent-job/Dockerfile +51 -0
  206. package/templates/docker/pi-coding-agent-job/entrypoint.sh +164 -0
  207. package/templates/docker-compose.local.yml +78 -0
  208. package/templates/docker-compose.yml +64 -0
  209. package/templates/instrumentation.js +6 -0
  210. package/templates/middleware.js +23 -0
  211. package/templates/next.config.mjs +3 -0
  212. package/templates/postcss.config.mjs +5 -0
  213. package/templates/public/favicon.ico +0 -0
  214. package/templates/server.js +25 -0
  215. package/templates/skills/LICENSE +21 -0
  216. package/templates/skills/README.md +119 -0
  217. package/templates/skills/brave-search/SKILL.md +79 -0
  218. package/templates/skills/brave-search/content.js +86 -0
  219. package/templates/skills/brave-search/package-lock.json +621 -0
  220. package/templates/skills/brave-search/package.json +14 -0
  221. package/templates/skills/brave-search/search.js +199 -0
  222. package/templates/skills/browser-tools/SKILL.md +196 -0
  223. package/templates/skills/browser-tools/browser-content.js +103 -0
  224. package/templates/skills/browser-tools/browser-cookies.js +35 -0
  225. package/templates/skills/browser-tools/browser-eval.js +53 -0
  226. package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
  227. package/templates/skills/browser-tools/browser-nav.js +44 -0
  228. package/templates/skills/browser-tools/browser-pick.js +162 -0
  229. package/templates/skills/browser-tools/browser-screenshot.js +34 -0
  230. package/templates/skills/browser-tools/browser-start.js +87 -0
  231. package/templates/skills/browser-tools/package-lock.json +2556 -0
  232. package/templates/skills/browser-tools/package.json +19 -0
  233. package/templates/skills/google-docs/SKILL.md +23 -0
  234. package/templates/skills/google-docs/create.sh +69 -0
  235. package/templates/skills/google-drive/SKILL.md +47 -0
  236. package/templates/skills/google-drive/delete.sh +47 -0
  237. package/templates/skills/google-drive/download.sh +50 -0
  238. package/templates/skills/google-drive/list.sh +41 -0
  239. package/templates/skills/google-drive/upload.sh +76 -0
  240. package/templates/skills/kie-ai/SKILL.md +38 -0
  241. package/templates/skills/kie-ai/generate-image.sh +77 -0
  242. package/templates/skills/kie-ai/generate-video.sh +69 -0
  243. package/templates/skills/llm-secrets/SKILL.md +34 -0
  244. package/templates/skills/llm-secrets/llm-secrets.js +33 -0
  245. package/templates/skills/modify-self/SKILL.md +12 -0
  246. package/templates/skills/youtube-transcript/SKILL.md +41 -0
  247. package/templates/skills/youtube-transcript/package-lock.json +24 -0
  248. package/templates/skills/youtube-transcript/package.json +8 -0
  249. package/templates/skills/youtube-transcript/transcript.js +84 -0
@@ -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
+ }
@@ -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
+ }
@@ -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 };