kcg-code 0.0.0-stage → 0.1.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 (286) hide show
  1. package/README.md +228 -2
  2. package/bin/kcgcode.ts +13 -0
  3. package/bun-env.d.ts +19 -0
  4. package/kcg-code.config.example.json +3 -0
  5. package/package.json +83 -4
  6. package/public/logo.svg +1 -0
  7. package/public/manifest.json +18 -0
  8. package/public/sw.js +49 -0
  9. package/src/App.tsx +39 -0
  10. package/src/__tests__/manifest.test.ts +61 -0
  11. package/src/build-config.ts +9 -0
  12. package/src/cli/__tests__/dashboard.test.ts +62 -0
  13. package/src/cli/args.ts +123 -0
  14. package/src/cli/commands.ts +174 -0
  15. package/src/cli/dashboard.tsx +343 -0
  16. package/src/cli/index.ts +37 -0
  17. package/src/cli/theme.ts +100 -0
  18. package/src/components/auth/AuthGate.tsx +182 -0
  19. package/src/components/auth/AvatarEditor.tsx +236 -0
  20. package/src/components/auth/LockScreen.tsx +714 -0
  21. package/src/components/auth/ProfileAvatar.tsx +60 -0
  22. package/src/components/auth/WelcomeScreen.tsx +672 -0
  23. package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
  24. package/src/components/auth/avatar-icons.tsx +114 -0
  25. package/src/components/layout/AppShell.tsx +142 -0
  26. package/src/components/layout/AppSidebar.tsx +419 -0
  27. package/src/components/layout/SearchDialog.tsx +189 -0
  28. package/src/components/layout/SidebarAccount.tsx +111 -0
  29. package/src/components/layout/SidebarSessionMenu.tsx +163 -0
  30. package/src/components/layout/ThemeToggle.tsx +67 -0
  31. package/src/components/projects/FolderBrowser.tsx +142 -0
  32. package/src/components/projects/HomeChat.tsx +250 -0
  33. package/src/components/projects/NewProjectDialog.tsx +249 -0
  34. package/src/components/projects/PanelSection.tsx +102 -0
  35. package/src/components/projects/ProjectInstructions.tsx +229 -0
  36. package/src/components/projects/ProjectRow.tsx +58 -0
  37. package/src/components/projects/ProjectSidePanel.tsx +448 -0
  38. package/src/components/sessions/AgentModeList.tsx +265 -0
  39. package/src/components/sessions/AgentPicker.tsx +123 -0
  40. package/src/components/sessions/AssistantTurn.tsx +145 -0
  41. package/src/components/sessions/AttachmentTile.tsx +161 -0
  42. package/src/components/sessions/CodeBlock.tsx +107 -0
  43. package/src/components/sessions/ComposerActionSheet.tsx +96 -0
  44. package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
  45. package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +72 -0
  46. package/src/components/sessions/ContextPanel.tsx +561 -0
  47. package/src/components/sessions/DropPlate.tsx +165 -0
  48. package/src/components/sessions/FileDropOverlay.tsx +232 -0
  49. package/src/components/sessions/FileMentionDropdown.tsx +47 -0
  50. package/src/components/sessions/ImageLightbox.tsx +173 -0
  51. package/src/components/sessions/MarkdownContent.tsx +187 -0
  52. package/src/components/sessions/Mascot.tsx +47 -0
  53. package/src/components/sessions/MessageActions.tsx +60 -0
  54. package/src/components/sessions/MessageRail.tsx +121 -0
  55. package/src/components/sessions/ModelPicker.tsx +209 -0
  56. package/src/components/sessions/ModelSearchList.tsx +151 -0
  57. package/src/components/sessions/NewSessionDialog.tsx +189 -0
  58. package/src/components/sessions/PendingAttachments.tsx +127 -0
  59. package/src/components/sessions/PromptCard.tsx +1085 -0
  60. package/src/components/sessions/PromptPanel.tsx +112 -0
  61. package/src/components/sessions/SessionComposer.tsx +640 -0
  62. package/src/components/sessions/SessionHeader.tsx +175 -0
  63. package/src/components/sessions/SessionList.tsx +396 -0
  64. package/src/components/sessions/SessionRow.tsx +137 -0
  65. package/src/components/sessions/SessionTimeline.tsx +275 -0
  66. package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
  67. package/src/components/sessions/SessionView.tsx +236 -0
  68. package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
  69. package/src/components/sessions/UserMessage.tsx +177 -0
  70. package/src/components/sessions/__tests__/ContextPanel.test.tsx +220 -0
  71. package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
  72. package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
  73. package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
  74. package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
  75. package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
  76. package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
  77. package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
  78. package/src/components/settings/AboutGroup.tsx +170 -0
  79. package/src/components/settings/AccountGroup.tsx +122 -0
  80. package/src/components/settings/DevicesGroup.tsx +151 -0
  81. package/src/components/settings/LhrRemoteAccess.tsx +262 -0
  82. package/src/components/settings/PasskeysGroup.tsx +551 -0
  83. package/src/components/settings/RemoteAccessGroup.tsx +1226 -0
  84. package/src/components/settings/SecurityGroup.tsx +566 -0
  85. package/src/components/settings/WorkspaceGroup.tsx +370 -0
  86. package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
  87. package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
  88. package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
  89. package/src/components/settings/prefs.tsx +265 -0
  90. package/src/components/skills/InstallNotifications.tsx +325 -0
  91. package/src/components/skills/InstallSkillDialog.tsx +233 -0
  92. package/src/components/skills/SkillCards.tsx +210 -0
  93. package/src/components/skills/SkillsProjectPicker.tsx +178 -0
  94. package/src/components/ui/alert-dialog.tsx +179 -0
  95. package/src/components/ui/alert.tsx +60 -0
  96. package/src/components/ui/badge.tsx +46 -0
  97. package/src/components/ui/bubble.tsx +125 -0
  98. package/src/components/ui/button.tsx +62 -0
  99. package/src/components/ui/card.tsx +75 -0
  100. package/src/components/ui/collapsible.tsx +19 -0
  101. package/src/components/ui/dialog.tsx +142 -0
  102. package/src/components/ui/dropdown-menu.tsx +225 -0
  103. package/src/components/ui/empty.tsx +94 -0
  104. package/src/components/ui/field.tsx +234 -0
  105. package/src/components/ui/input-group.tsx +161 -0
  106. package/src/components/ui/input.tsx +21 -0
  107. package/src/components/ui/label.tsx +19 -0
  108. package/src/components/ui/message-scroller.tsx +131 -0
  109. package/src/components/ui/message.tsx +85 -0
  110. package/src/components/ui/popover.tsx +46 -0
  111. package/src/components/ui/select.tsx +172 -0
  112. package/src/components/ui/separator.tsx +28 -0
  113. package/src/components/ui/sheet.tsx +138 -0
  114. package/src/components/ui/sidebar.tsx +699 -0
  115. package/src/components/ui/skeleton.tsx +15 -0
  116. package/src/components/ui/sonner.tsx +45 -0
  117. package/src/components/ui/spinner.tsx +16 -0
  118. package/src/components/ui/switch.tsx +32 -0
  119. package/src/components/ui/textarea.tsx +18 -0
  120. package/src/components/ui/tooltip.tsx +51 -0
  121. package/src/config.ts +85 -0
  122. package/src/db.ts +146 -0
  123. package/src/frontend.tsx +51 -0
  124. package/src/hooks/__tests__/useMention.test.ts +79 -0
  125. package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
  126. package/src/hooks/__tests__/useRouter.test.ts +17 -0
  127. package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
  128. package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
  129. package/src/hooks/__tests__/useTheme.test.ts +118 -0
  130. package/src/hooks/use-mobile.ts +16 -0
  131. package/src/hooks/useAgentPicker.ts +97 -0
  132. package/src/hooks/useMediaQuery.ts +49 -0
  133. package/src/hooks/useMention.ts +236 -0
  134. package/src/hooks/useProjects.ts +88 -0
  135. package/src/hooks/useResyncOnReconnect.ts +28 -0
  136. package/src/hooks/useRouter.ts +92 -0
  137. package/src/hooks/useSessionChat.ts +427 -0
  138. package/src/hooks/useSessionContext.ts +106 -0
  139. package/src/hooks/useSessionList.ts +234 -0
  140. package/src/hooks/useSidebarData.ts +108 -0
  141. package/src/hooks/useSmoothText.ts +122 -0
  142. package/src/hooks/useTheme.ts +164 -0
  143. package/src/hooks/useWebSocket.ts +196 -0
  144. package/src/index.css +27 -0
  145. package/src/index.html +14 -0
  146. package/src/index.ts +49 -0
  147. package/src/lib/__tests__/attachments.test.ts +58 -0
  148. package/src/lib/__tests__/avatar-presets.test.ts +25 -0
  149. package/src/lib/__tests__/collapsible.test.ts +101 -0
  150. package/src/lib/__tests__/composer.test.ts +118 -0
  151. package/src/lib/__tests__/highlight.test.ts +42 -0
  152. package/src/lib/__tests__/home-chat.test.ts +64 -0
  153. package/src/lib/__tests__/lock-screen.test.ts +64 -0
  154. package/src/lib/__tests__/model-picker.test.ts +205 -0
  155. package/src/lib/__tests__/onboarding.test.ts +41 -0
  156. package/src/lib/__tests__/permission-info.test.ts +61 -0
  157. package/src/lib/__tests__/project-extensions.test.ts +52 -0
  158. package/src/lib/__tests__/project-overview.test.ts +173 -0
  159. package/src/lib/__tests__/references.test.ts +121 -0
  160. package/src/lib/__tests__/routes.test.ts +134 -0
  161. package/src/lib/__tests__/session-summary.test.ts +115 -0
  162. package/src/lib/__tests__/session-title.test.ts +58 -0
  163. package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
  164. package/src/lib/__tests__/skill-installs.test.ts +134 -0
  165. package/src/lib/__tests__/skills-catalog.test.ts +96 -0
  166. package/src/lib/__tests__/tone.test.ts +37 -0
  167. package/src/lib/__tests__/turns-delta.test.ts +55 -0
  168. package/src/lib/api.ts +306 -0
  169. package/src/lib/attachments.ts +203 -0
  170. package/src/lib/auth.ts +92 -0
  171. package/src/lib/avatar-presets.ts +31 -0
  172. package/src/lib/collapsible.ts +75 -0
  173. package/src/lib/composer.ts +89 -0
  174. package/src/lib/data-events.ts +17 -0
  175. package/src/lib/headlines.ts +30 -0
  176. package/src/lib/highlight.ts +186 -0
  177. package/src/lib/layout.ts +20 -0
  178. package/src/lib/lock-screen.ts +54 -0
  179. package/src/lib/model-picker.ts +133 -0
  180. package/src/lib/onboarding.ts +38 -0
  181. package/src/lib/passkeys.ts +113 -0
  182. package/src/lib/pending-prompt.ts +53 -0
  183. package/src/lib/permission-info.ts +101 -0
  184. package/src/lib/project-extensions.ts +74 -0
  185. package/src/lib/project-overview.ts +117 -0
  186. package/src/lib/prompts.ts +87 -0
  187. package/src/lib/references.ts +106 -0
  188. package/src/lib/routes.ts +115 -0
  189. package/src/lib/session-handoff.ts +25 -0
  190. package/src/lib/session-status.ts +45 -0
  191. package/src/lib/session-summary.ts +68 -0
  192. package/src/lib/session-title.ts +63 -0
  193. package/src/lib/sidebar-groups.ts +78 -0
  194. package/src/lib/skill-installs.ts +360 -0
  195. package/src/lib/skills-catalog.ts +99 -0
  196. package/src/lib/time.ts +22 -0
  197. package/src/lib/tone.ts +80 -0
  198. package/src/lib/turns.ts +320 -0
  199. package/src/lib/utils.ts +6 -0
  200. package/src/logo.svg +31 -0
  201. package/src/pages/projects/ProjectDetailPage.tsx +130 -0
  202. package/src/pages/projects/ProjectsPage.tsx +97 -0
  203. package/src/pages/sessions/SessionPage.tsx +122 -0
  204. package/src/pages/settings/SettingsPage.tsx +299 -0
  205. package/src/pages/settings/__tests__/SettingsPage.test.ts +9 -0
  206. package/src/pages/skills/SkillsPage.tsx +701 -0
  207. package/src/paths.ts +121 -0
  208. package/src/runtime.ts +24 -0
  209. package/src/server/__tests__/app.test.ts +1031 -0
  210. package/src/server/__tests__/auth.test.ts +370 -0
  211. package/src/server/__tests__/config.test.ts +101 -0
  212. package/src/server/__tests__/db.test.ts +485 -0
  213. package/src/server/__tests__/host.test.ts +14 -0
  214. package/src/server/__tests__/tunnel-config.test.ts +38 -0
  215. package/src/server/app.ts +415 -0
  216. package/src/server/db/auth.ts +311 -0
  217. package/src/server/db/messages.ts +50 -0
  218. package/src/server/db/projects.ts +106 -0
  219. package/src/server/db/prompts.ts +78 -0
  220. package/src/server/db/rows.ts +150 -0
  221. package/src/server/db/schema.ts +221 -0
  222. package/src/server/db/sessions.ts +239 -0
  223. package/src/server/db/tunnel.ts +124 -0
  224. package/src/server/host.ts +12 -0
  225. package/src/server/result.ts +15 -0
  226. package/src/server/routes/auth-guard.ts +156 -0
  227. package/src/server/routes/auth.routes.ts +287 -0
  228. package/src/server/routes/helpers.ts +165 -0
  229. package/src/server/routes/meta.routes.ts +24 -0
  230. package/src/server/routes/passkeys.routes.ts +161 -0
  231. package/src/server/routes/projects.routes.ts +157 -0
  232. package/src/server/routes/sessions.routes.ts +197 -0
  233. package/src/server/routes/skills.routes.ts +117 -0
  234. package/src/server/routes/tunnel.routes.ts +132 -0
  235. package/src/server/routes/types.ts +35 -0
  236. package/src/server/routes/uploads.routes.ts +54 -0
  237. package/src/server/routes/workspace.routes.ts +127 -0
  238. package/src/server/services/__tests__/app-info.test.ts +18 -0
  239. package/src/server/services/__tests__/attachments.test.ts +159 -0
  240. package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
  241. package/src/server/services/__tests__/auth.test.ts +341 -0
  242. package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
  243. package/src/server/services/__tests__/git-info.test.ts +74 -0
  244. package/src/server/services/__tests__/opencode.test.ts +320 -0
  245. package/src/server/services/__tests__/project-manager.test.ts +190 -0
  246. package/src/server/services/__tests__/sandbox.test.ts +141 -0
  247. package/src/server/services/__tests__/session-manager.test.ts +3469 -0
  248. package/src/server/services/__tests__/skill-install-jobs.test.ts +226 -0
  249. package/src/server/services/__tests__/skills-registry.test.ts +359 -0
  250. package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
  251. package/src/server/services/__tests__/tunnel.test.ts +367 -0
  252. package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
  253. package/src/server/services/__tests__/workspace.test.ts +249 -0
  254. package/src/server/services/app-info.ts +29 -0
  255. package/src/server/services/attachments.ts +193 -0
  256. package/src/server/services/auth-passkeys.ts +459 -0
  257. package/src/server/services/auth.ts +633 -0
  258. package/src/server/services/frpc-installer.ts +183 -0
  259. package/src/server/services/git-info.ts +87 -0
  260. package/src/server/services/opencode-client.ts +714 -0
  261. package/src/server/services/opencode-server.ts +319 -0
  262. package/src/server/services/opencode-sse.ts +82 -0
  263. package/src/server/services/project-manager.ts +147 -0
  264. package/src/server/services/sandbox.ts +83 -0
  265. package/src/server/services/session-events.ts +256 -0
  266. package/src/server/services/session-manager.ts +1374 -0
  267. package/src/server/services/skill-install-jobs.ts +262 -0
  268. package/src/server/services/skills-registry.ts +468 -0
  269. package/src/server/services/tunnel-api.ts +207 -0
  270. package/src/server/services/tunnel-config.ts +45 -0
  271. package/src/server/services/tunnel-lhr.ts +387 -0
  272. package/src/server/services/tunnel.ts +633 -0
  273. package/src/server/services/turn-stream.ts +339 -0
  274. package/src/server/services/websocket-gateway.ts +374 -0
  275. package/src/server/services/workspace.ts +223 -0
  276. package/src/server/services/ws-transport.ts +121 -0
  277. package/src/types/context.ts +69 -0
  278. package/src/types/index.ts +13 -0
  279. package/src/types/message.ts +58 -0
  280. package/src/types/project.ts +16 -0
  281. package/src/types/prompt.ts +118 -0
  282. package/src/types/session.ts +52 -0
  283. package/src/types/turns.ts +47 -0
  284. package/src/ws-protocol.ts +93 -0
  285. package/styles/globals.css +311 -0
  286. package/tsconfig.json +37 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Skema SQLite & migrasi idempoten untuk `Session_Store` (headless).
3
+ * Dipisah dari `db.ts` agar komposisi root tetap ramping dan evolusi skema
4
+ * (menambah tabel/kolom) punya satu tempat.
5
+ */
6
+ import type { Database } from "bun:sqlite";
7
+
8
+ export const SCHEMA = `
9
+ CREATE TABLE IF NOT EXISTS projects (
10
+ id TEXT PRIMARY KEY,
11
+ name TEXT NOT NULL UNIQUE,
12
+ path TEXT NOT NULL UNIQUE,
13
+ instructions TEXT,
14
+ created_at INTEGER NOT NULL
15
+ );
16
+
17
+ CREATE TABLE IF NOT EXISTS sessions (
18
+ id TEXT PRIMARY KEY,
19
+ project_id TEXT NOT NULL REFERENCES projects(id),
20
+ agent_type TEXT NOT NULL,
21
+ cwd TEXT NOT NULL,
22
+ status TEXT NOT NULL CHECK (status IN ('running','stopped','crashed')),
23
+ oc_session_id TEXT,
24
+ model TEXT,
25
+ agent TEXT,
26
+ title TEXT,
27
+ created_at INTEGER NOT NULL,
28
+ updated_at INTEGER NOT NULL
29
+ );
30
+
31
+ -- Append-only: tidak pernah UPDATE/DELETE, hanya INSERT (Requirement 3.3)
32
+ CREATE TABLE IF NOT EXISTS session_status_history (
33
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
34
+ session_id TEXT NOT NULL REFERENCES sessions(id),
35
+ status TEXT NOT NULL,
36
+ changed_at INTEGER NOT NULL
37
+ );
38
+
39
+ -- Append-only: pesan percakapan terstruktur (Requirement 3.1, 3.2)
40
+ CREATE TABLE IF NOT EXISTS messages (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ session_id TEXT NOT NULL REFERENCES sessions(id),
43
+ message_id TEXT NOT NULL,
44
+ role TEXT NOT NULL CHECK (role IN ('user','assistant')),
45
+ parts_json TEXT NOT NULL,
46
+ created_at INTEGER NOT NULL,
47
+ UNIQUE(session_id, message_id)
48
+ );
49
+ CREATE INDEX IF NOT EXISTS idx_messages_session_created ON messages(session_id, created_at);
50
+
51
+ CREATE TABLE IF NOT EXISTS prompts (
52
+ id TEXT PRIMARY KEY,
53
+ session_id TEXT NOT NULL REFERENCES sessions(id),
54
+ kind TEXT NOT NULL DEFAULT 'permission',
55
+ type TEXT NOT NULL CHECK (type IN ('confirmation','menu')),
56
+ custom INTEGER NOT NULL DEFAULT 0,
57
+ title TEXT,
58
+ options_json TEXT,
59
+ status TEXT NOT NULL CHECK (status IN ('pending','resolved')),
60
+ created_at INTEGER NOT NULL,
61
+ resolved_at INTEGER
62
+ );
63
+ CREATE INDEX IF NOT EXISTS idx_prompts_session_status ON prompts(session_id, status);
64
+
65
+ -- Kunci aplikasi (lock screen). Satu baris (id = 1). Hash argon2id — sandi
66
+ -- asli tidak pernah disimpan. lock_kind NULL = kunci belum diatur.
67
+ CREATE TABLE IF NOT EXISTS auth_settings (
68
+ id INTEGER PRIMARY KEY CHECK (id = 1),
69
+ lock_kind TEXT CHECK (lock_kind IN ('pin','password')),
70
+ lock_hash TEXT,
71
+ nickname TEXT,
72
+ avatar_mime TEXT,
73
+ avatar BLOB,
74
+ avatar_preset TEXT,
75
+ avatar_version INTEGER NOT NULL DEFAULT 0,
76
+ auto_lock_minutes INTEGER NOT NULL DEFAULT 15,
77
+ updated_at INTEGER NOT NULL
78
+ );
79
+
80
+ -- Sesi login per perangkat. Yang disimpan hanya SHA-256 dari token cookie,
81
+ -- sehingga bocornya DB tidak memberi sesi yang bisa dipakai.
82
+ CREATE TABLE IF NOT EXISTS auth_sessions (
83
+ id TEXT PRIMARY KEY,
84
+ token_hash TEXT NOT NULL UNIQUE,
85
+ user_agent TEXT,
86
+ created_at INTEGER NOT NULL,
87
+ last_seen_at INTEGER NOT NULL,
88
+ expires_at INTEGER NOT NULL
89
+ );
90
+
91
+ -- Passkey (WebAuthn) — cara buka kunci TAMBAHAN di samping PIN/password.
92
+ -- Hanya kunci publik yang disimpan (bukan rahasia). rp_id = domain tempat
93
+ -- passkey didaftarkan; passkey hanya berlaku di domain yang sama.
94
+ CREATE TABLE IF NOT EXISTS auth_passkeys (
95
+ id TEXT PRIMARY KEY,
96
+ credential_id TEXT NOT NULL UNIQUE,
97
+ public_key BLOB NOT NULL,
98
+ counter INTEGER NOT NULL DEFAULT 0,
99
+ transports TEXT,
100
+ rp_id TEXT NOT NULL,
101
+ name TEXT NOT NULL,
102
+ device_type TEXT,
103
+ backed_up INTEGER NOT NULL DEFAULT 0,
104
+ created_at INTEGER NOT NULL,
105
+ last_used_at INTEGER
106
+ );
107
+ CREATE INDEX IF NOT EXISTS idx_auth_passkeys_rp ON auth_passkeys(rp_id);
108
+
109
+ -- Tunnel publik (<username>.<domain>) lewat API kcgcode-rp + frpc.
110
+ -- Satu baris (id = 1). device_token (dari device flow, bisa dicabut di VPS)
111
+ -- & tunnel_secret tidak pernah dikirim ke browser; enabled = tunnel
112
+ -- dinyalakan ulang saat start.
113
+ CREATE TABLE IF NOT EXISTS tunnel_account (
114
+ id INTEGER PRIMARY KEY CHECK (id = 1),
115
+ email TEXT,
116
+ username TEXT,
117
+ subdomain TEXT,
118
+ device_id TEXT,
119
+ device_token TEXT,
120
+ tunnel_secret TEXT,
121
+ enabled INTEGER NOT NULL DEFAULT 0,
122
+ updated_at INTEGER NOT NULL
123
+ );
124
+ `;
125
+
126
+ /** Menambah kolom bila belum ada (migrasi DB lama yang idempoten). */
127
+ function ensureColumn(db: Database, table: string, column: string, ddl: string): void {
128
+ const cols = db.query(`PRAGMA table_info(${table})`).all() as { name: string }[];
129
+ if (!cols.some((c) => c.name === column)) db.exec(ddl);
130
+ }
131
+
132
+ /**
133
+ * Menjalankan seluruh migrasi idempoten: membuat tabel yang belum ada lalu
134
+ * menambah kolom dari skema lama (versi PTY) yang masih dipakai.
135
+ */
136
+ export function runMigrations(db: Database): void {
137
+ db.exec(SCHEMA);
138
+ ensureColumn(
139
+ db,
140
+ "sessions",
141
+ "oc_session_id",
142
+ "ALTER TABLE sessions ADD COLUMN oc_session_id TEXT",
143
+ );
144
+ ensureColumn(
145
+ db,
146
+ "prompts",
147
+ "kind",
148
+ "ALTER TABLE prompts ADD COLUMN kind TEXT NOT NULL DEFAULT 'permission'",
149
+ );
150
+ ensureColumn(db, "prompts", "title", "ALTER TABLE prompts ADD COLUMN title TEXT");
151
+ // Question saja: boleh jawab bebas (flag `custom` skema question opencode).
152
+ ensureColumn(
153
+ db,
154
+ "prompts",
155
+ "custom",
156
+ "ALTER TABLE prompts ADD COLUMN custom INTEGER NOT NULL DEFAULT 0",
157
+ );
158
+ // Question saja: boleh pilih lebih dari satu opsi (flag `multiple` skema
159
+ // question opencode) — kartu history tetap tampil sebagai multi-select.
160
+ ensureColumn(
161
+ db,
162
+ "prompts",
163
+ "multiple",
164
+ "ALTER TABLE prompts ADD COLUMN multiple INTEGER NOT NULL DEFAULT 0",
165
+ );
166
+ // Multi-question: JSON seluruh pertanyaan request (null = pertanyaan
167
+ // tunggal, field legacy title/options cukup).
168
+ ensureColumn(
169
+ db,
170
+ "prompts",
171
+ "questions_json",
172
+ "ALTER TABLE prompts ADD COLUMN questions_json TEXT",
173
+ );
174
+ // Agent (mode) opencode pilihan Session — null = default opencode (build).
175
+ ensureColumn(db, "sessions", "agent", "ALTER TABLE sessions ADD COLUMN agent TEXT");
176
+ // Model pilihan per Session (JSON `{providerID, modelID}`), NULL = default.
177
+ ensureColumn(db, "sessions", "model", "ALTER TABLE sessions ADD COLUMN model TEXT");
178
+ // Judul Session hasil generate opencode (SSE `session.updated`), NULL = belum ada.
179
+ ensureColumn(db, "sessions", "title", "ALTER TABLE sessions ADD COLUMN title TEXT");
180
+ // Custom instruction per Project (dikirim sebagai `system`), NULL = tidak ada.
181
+ ensureColumn(db, "projects", "instructions", "ALTER TABLE projects ADD COLUMN instructions TEXT");
182
+ // Remote access: provider terpilih (kcg = layanan bawaan, lhr = localhost.run)
183
+ // + status nyala localhost.run (dinyalakan ulang otomatis saat start).
184
+ ensureColumn(
185
+ db,
186
+ "tunnel_account",
187
+ "provider",
188
+ "ALTER TABLE tunnel_account ADD COLUMN provider TEXT NOT NULL DEFAULT 'kcg'",
189
+ );
190
+ ensureColumn(
191
+ db,
192
+ "tunnel_account",
193
+ "lhr_enabled",
194
+ "ALTER TABLE tunnel_account ADD COLUMN lhr_enabled INTEGER NOT NULL DEFAULT 0",
195
+ );
196
+ // Avatar bawaan (id preset ikon). Eksklusif dengan foto upload `avatar`.
197
+ ensureColumn(
198
+ db,
199
+ "auth_settings",
200
+ "avatar_preset",
201
+ "ALTER TABLE auth_settings ADD COLUMN avatar_preset TEXT",
202
+ );
203
+ // Onboarding (welcome screen) selesai — NULL = instalasi baru.
204
+ const hadOnboarded = (
205
+ db.query("PRAGMA table_info(auth_settings)").all() as { name: string }[]
206
+ ).some((c) => c.name === "onboarded_at");
207
+ ensureColumn(
208
+ db,
209
+ "auth_settings",
210
+ "onboarded_at",
211
+ "ALTER TABLE auth_settings ADD COLUMN onboarded_at INTEGER",
212
+ );
213
+ if (!hadOnboarded) {
214
+ // Instalasi LAMA (sudah punya kunci / profil / project sebelum fitur ini)
215
+ // tidak perlu melihat welcome screen — tandai selesai saat migrasi.
216
+ db.exec(`UPDATE auth_settings SET onboarded_at = updated_at
217
+ WHERE onboarded_at IS NULL
218
+ AND (lock_kind IS NOT NULL OR nickname IS NOT NULL
219
+ OR EXISTS (SELECT 1 FROM projects))`);
220
+ }
221
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Repository Sessions + riwayat statusnya. Status history bukan domain
3
+ * terpisah melainkan sub-entitas agregat Session (tabel-nya pun
4
+ * `session_status_history`), sehingga transaksi yang menyentuh keduanya
5
+ * (`updateSessionStatus`) dan cascade delete tinggal di sini.
6
+ */
7
+ import type { Database } from "bun:sqlite";
8
+ import type { Session, SessionModel, SessionStatus } from "../../types";
9
+ import type { Result, SimpleResult } from "../result";
10
+ import { errResult, mapSession, type SessionRow } from "./rows";
11
+
12
+ /** Satu entri riwayat status (append-only). */
13
+ export interface StatusHistoryEntry {
14
+ sessionId: string;
15
+ status: SessionStatus;
16
+ changedAt: number;
17
+ }
18
+
19
+ export function sessionExists(db: Database, sessionId: string): unknown {
20
+ return db.query("SELECT id FROM sessions WHERE id = ?").get(sessionId);
21
+ }
22
+
23
+ export function createSessionRepo(db: Database) {
24
+ const q = {
25
+ projectById: db.query("SELECT id FROM projects WHERE id = ?"),
26
+ insertSession: db.query(
27
+ "INSERT INTO sessions (id, project_id, agent_type, cwd, status, oc_session_id, model, agent, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
28
+ ),
29
+ getSession: db.query("SELECT * FROM sessions WHERE id = ?"),
30
+ getSessionByOcId: db.query("SELECT * FROM sessions WHERE oc_session_id = ?"),
31
+ listSessions: db.query("SELECT * FROM sessions ORDER BY created_at ASC, id ASC"),
32
+ updateSessionStatus: db.query("UPDATE sessions SET status = ?, updated_at = ? WHERE id = ?"),
33
+ updateSessionOcId: db.query(
34
+ "UPDATE sessions SET oc_session_id = ?, updated_at = ? WHERE id = ?",
35
+ ),
36
+ updateSessionModel: db.query("UPDATE sessions SET model = ?, updated_at = ? WHERE id = ?"),
37
+ updateSessionAgent: db.query("UPDATE sessions SET agent = ?, updated_at = ? WHERE id = ?"),
38
+ updateSessionTitle: db.query("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?"),
39
+ deleteSessionMessages: db.query("DELETE FROM messages WHERE session_id = ?"),
40
+ deleteSessionPrompts: db.query("DELETE FROM prompts WHERE session_id = ?"),
41
+ deleteSessionHistory: db.query("DELETE FROM session_status_history WHERE session_id = ?"),
42
+ deleteSessionRow: db.query("DELETE FROM sessions WHERE id = ?"),
43
+ insertHistory: db.query(
44
+ "INSERT INTO session_status_history (session_id, status, changed_at) VALUES (?, ?, ?)",
45
+ ),
46
+ getHistory: db.query(
47
+ "SELECT session_id, status, changed_at FROM session_status_history WHERE session_id = ? ORDER BY id ASC",
48
+ ),
49
+ };
50
+
51
+ function findRow(sessionId: string): SessionRow | null {
52
+ return q.getSession.get(sessionId) as SessionRow | null;
53
+ }
54
+
55
+ return {
56
+ insertSession(session: Session): Result<Session> {
57
+ try {
58
+ if (!q.projectById.get(session.projectId)) return errResult("PROJECT_NOT_FOUND");
59
+ q.insertSession.run(
60
+ session.id,
61
+ session.projectId,
62
+ session.agentType,
63
+ session.cwd,
64
+ session.status,
65
+ session.ocSessionId,
66
+ session.model ? JSON.stringify(session.model) : null,
67
+ session.agent ?? null,
68
+ session.title ?? null,
69
+ session.createdAt,
70
+ session.updatedAt,
71
+ );
72
+ return { ok: true, data: session };
73
+ } catch (e) {
74
+ return errResult(`SESSION_WRITE_FAILED: ${(e as Error).message}`);
75
+ }
76
+ },
77
+
78
+ updateSessionStatus(
79
+ sessionId: string,
80
+ status: SessionStatus,
81
+ changedAt = Date.now(),
82
+ ): Result<Session> {
83
+ try {
84
+ if (!findRow(sessionId)) return errResult("SESSION_NOT_FOUND");
85
+ const tx = db.transaction(() => {
86
+ q.updateSessionStatus.run(status, changedAt, sessionId);
87
+ q.insertHistory.run(sessionId, status, changedAt);
88
+ });
89
+ tx();
90
+ const updated = mapSession(findRow(sessionId) as SessionRow);
91
+ return { ok: true, data: updated };
92
+ } catch (e) {
93
+ return errResult(`SESSION_UPDATE_FAILED: ${(e as Error).message}`);
94
+ }
95
+ },
96
+
97
+ /**
98
+ * Perbarui `oc_session_id` tanpa mengubah status — dipakai `resumeSession`
99
+ * saat Session lama menunjuk oc session yang sudah tidak dikenal server
100
+ * (mis. storage opencode dibersihkan) dan perlu memakai sesi remote baru.
101
+ */
102
+ updateSessionOcId(sessionId: string, ocSessionId: string | null): Result<Session> {
103
+ try {
104
+ if (!findRow(sessionId)) return errResult("SESSION_NOT_FOUND");
105
+ q.updateSessionOcId.run(ocSessionId, Date.now(), sessionId);
106
+ return { ok: true, data: mapSession(findRow(sessionId) as SessionRow) };
107
+ } catch (e) {
108
+ return errResult(`SESSION_UPDATE_FAILED: ${(e as Error).message}`);
109
+ }
110
+ },
111
+
112
+ /**
113
+ * Perbarui model pilihan Session tanpa mengubah status — dipakai rute
114
+ * `PUT /api/sessions/:id/model`. `null` berarti kembali ke default opencode.
115
+ */
116
+ updateSessionModel(sessionId: string, model: SessionModel | null): Result<Session> {
117
+ try {
118
+ if (!findRow(sessionId)) return errResult("SESSION_NOT_FOUND");
119
+ q.updateSessionModel.run(model ? JSON.stringify(model) : null, Date.now(), sessionId);
120
+ return { ok: true, data: mapSession(findRow(sessionId) as SessionRow) };
121
+ } catch (e) {
122
+ return errResult(`SESSION_UPDATE_FAILED: ${(e as Error).message}`);
123
+ }
124
+ },
125
+
126
+ /**
127
+ * Perbarui agent (mode) pilihan Session tanpa mengubah status — dipakai
128
+ * rute `PUT /api/sessions/:id`. `null` berarti kembali ke agent default
129
+ * opencode (biasanya `build`).
130
+ */
131
+ updateSessionAgent(sessionId: string, agent: string | null): Result<Session> {
132
+ try {
133
+ if (!findRow(sessionId)) return errResult("SESSION_NOT_FOUND");
134
+ q.updateSessionAgent.run(agent ?? null, Date.now(), sessionId);
135
+ return { ok: true, data: mapSession(findRow(sessionId) as SessionRow) };
136
+ } catch (e) {
137
+ return errResult(`SESSION_UPDATE_FAILED: ${(e as Error).message}`);
138
+ }
139
+ },
140
+
141
+ /**
142
+ * Simpan judul Session hasil generate opencode (SSE `session.updated`).
143
+ * Tanpa mengubah status; dipanggil berulang aman (judul sama dioverwrite).
144
+ */
145
+ updateSessionTitle(sessionId: string, title: string): Result<Session> {
146
+ try {
147
+ if (!findRow(sessionId)) return errResult("SESSION_NOT_FOUND");
148
+ q.updateSessionTitle.run(title, Date.now(), sessionId);
149
+ return { ok: true, data: mapSession(findRow(sessionId) as SessionRow) };
150
+ } catch (e) {
151
+ return errResult(`SESSION_UPDATE_FAILED: ${(e as Error).message}`);
152
+ }
153
+ },
154
+
155
+ getSession(sessionId: string): Result<Session> {
156
+ try {
157
+ const row = findRow(sessionId);
158
+ if (!row) return errResult("SESSION_NOT_FOUND");
159
+ return { ok: true, data: mapSession(row) };
160
+ } catch (e) {
161
+ return errResult(`SESSION_READ_FAILED: ${(e as Error).message}`);
162
+ }
163
+ },
164
+
165
+ getSessionByOcId(ocSessionId: string): Result<Session> {
166
+ try {
167
+ const row = q.getSessionByOcId.get(ocSessionId) as SessionRow | null;
168
+ if (!row) return errResult("SESSION_NOT_FOUND");
169
+ return { ok: true, data: mapSession(row) };
170
+ } catch (e) {
171
+ return errResult(`SESSION_READ_FAILED: ${(e as Error).message}`);
172
+ }
173
+ },
174
+
175
+ listSessions(): Session[] {
176
+ return (q.listSessions.all() as SessionRow[]).map(mapSession);
177
+ },
178
+
179
+ /**
180
+ * Hapus Session beserta seluruh baris anaknya (messages, prompts,
181
+ * riwayat status) dalam satu transaksi. Gagal di tengah jalan di-rollback
182
+ * agar data tidak setengah terhapus.
183
+ */
184
+ deleteSession(sessionId: string): SimpleResult {
185
+ try {
186
+ if (!findRow(sessionId)) return errResult("SESSION_NOT_FOUND");
187
+ const tx = db.transaction(() => {
188
+ q.deleteSessionMessages.run(sessionId);
189
+ q.deleteSessionPrompts.run(sessionId);
190
+ q.deleteSessionHistory.run(sessionId);
191
+ q.deleteSessionRow.run(sessionId);
192
+ });
193
+ tx();
194
+ return { ok: true };
195
+ } catch (e) {
196
+ return errResult(`SESSION_DELETE_FAILED: ${(e as Error).message}`);
197
+ }
198
+ },
199
+
200
+ // ---------------- Riwayat status (append-only) ----------------
201
+
202
+ insertStatusHistory(
203
+ sessionId: string,
204
+ status: SessionStatus,
205
+ changedAt = Date.now(),
206
+ ): Result<StatusHistoryEntry> {
207
+ try {
208
+ if (!sessionExists(db, sessionId)) return errResult("SESSION_NOT_FOUND");
209
+ q.insertHistory.run(sessionId, status, changedAt);
210
+ return { ok: true, data: { sessionId, status, changedAt } };
211
+ } catch (e) {
212
+ return errResult(`HISTORY_WRITE_FAILED: ${(e as Error).message}`);
213
+ }
214
+ },
215
+
216
+ getStatusHistory(sessionId: string): Result<StatusHistoryEntry[]> {
217
+ try {
218
+ if (!sessionExists(db, sessionId)) return errResult("SESSION_NOT_FOUND");
219
+ const rows = q.getHistory.all(sessionId) as {
220
+ session_id: string;
221
+ status: string;
222
+ changed_at: number;
223
+ }[];
224
+ return {
225
+ ok: true,
226
+ data: rows.map((r) => ({
227
+ sessionId: r.session_id,
228
+ status: r.status as SessionStatus,
229
+ changedAt: r.changed_at,
230
+ })),
231
+ };
232
+ } catch (e) {
233
+ return errResult(`HISTORY_READ_FAILED: ${(e as Error).message}`);
234
+ }
235
+ },
236
+ };
237
+ }
238
+
239
+ export type SessionRepo = ReturnType<typeof createSessionRepo>;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Repository akun tunnel (`tunnel_account`, id = 1): hasil device flow
3
+ * kcgcode-rp — device token, subdomain, dan tunnel_secret frpc.
4
+ */
5
+ import type { Database } from "bun:sqlite";
6
+
7
+ export interface TunnelAccount {
8
+ email: string;
9
+ username: string;
10
+ subdomain: string;
11
+ deviceId: string;
12
+ deviceToken: string;
13
+ tunnelSecret: string;
14
+ }
15
+
16
+ /** Penyedia remote access: layanan bawaan KCG Code atau localhost.run. */
17
+ export type TunnelProvider = "kcg" | "lhr";
18
+
19
+ export interface TunnelSettings {
20
+ account: TunnelAccount | null;
21
+ /** Tunnel dinyalakan -> dijalankan ulang otomatis saat kcgcode start. */
22
+ enabled: boolean;
23
+ /** Penyedia yang dipilih pengguna (hanya satu yang aktif). */
24
+ provider: TunnelProvider;
25
+ /** localhost.run dinyalakan -> dijalankan ulang otomatis saat start. */
26
+ lhrEnabled: boolean;
27
+ updatedAt: number;
28
+ }
29
+
30
+ interface Row {
31
+ email: string | null;
32
+ username: string | null;
33
+ subdomain: string | null;
34
+ device_id: string | null;
35
+ device_token: string | null;
36
+ tunnel_secret: string | null;
37
+ enabled: number;
38
+ provider: string | null;
39
+ lhr_enabled: number | null;
40
+ updated_at: number;
41
+ }
42
+
43
+ function mapRow(r: Row): TunnelSettings {
44
+ const { email, username, subdomain, device_id, device_token, tunnel_secret } = r;
45
+ return {
46
+ account:
47
+ email && username && subdomain && device_id && device_token && tunnel_secret
48
+ ? {
49
+ email,
50
+ username,
51
+ subdomain,
52
+ deviceId: device_id,
53
+ deviceToken: device_token,
54
+ tunnelSecret: tunnel_secret,
55
+ }
56
+ : null,
57
+ enabled: r.enabled === 1,
58
+ provider: r.provider === "lhr" ? "lhr" : "kcg",
59
+ lhrEnabled: r.lhr_enabled === 1,
60
+ updatedAt: r.updated_at,
61
+ };
62
+ }
63
+
64
+ export function createTunnelRepo(db: Database) {
65
+ const q = {
66
+ ensure: db.query("INSERT OR IGNORE INTO tunnel_account (id, updated_at) VALUES (1, ?)"),
67
+ get: db.query(
68
+ `SELECT email, username, subdomain, device_id, device_token, tunnel_secret, enabled,
69
+ provider, lhr_enabled, updated_at FROM tunnel_account WHERE id = 1`,
70
+ ),
71
+ setAccount: db.query(
72
+ `UPDATE tunnel_account SET email = ?, username = ?, subdomain = ?, device_id = ?,
73
+ device_token = ?, tunnel_secret = ?, updated_at = ? WHERE id = 1`,
74
+ ),
75
+ setSecret: db.query("UPDATE tunnel_account SET tunnel_secret = ?, updated_at = ? WHERE id = 1"),
76
+ setEnabled: db.query("UPDATE tunnel_account SET enabled = ?, updated_at = ? WHERE id = 1"),
77
+ setProvider: db.query("UPDATE tunnel_account SET provider = ?, updated_at = ? WHERE id = 1"),
78
+ setLhrEnabled: db.query(
79
+ "UPDATE tunnel_account SET lhr_enabled = ?, updated_at = ? WHERE id = 1",
80
+ ),
81
+ };
82
+
83
+ function ensure(now: number): void {
84
+ q.ensure.run(now);
85
+ }
86
+
87
+ return {
88
+ getTunnelSettings(now: number): TunnelSettings {
89
+ ensure(now);
90
+ return mapRow(q.get.get() as Row);
91
+ },
92
+ /** Simpan akun (null = sign out). */
93
+ setTunnelAccount(account: TunnelAccount | null, now: number): void {
94
+ ensure(now);
95
+ q.setAccount.run(
96
+ account?.email ?? null,
97
+ account?.username ?? null,
98
+ account?.subdomain ?? null,
99
+ account?.deviceId ?? null,
100
+ account?.deviceToken ?? null,
101
+ account?.tunnelSecret ?? null,
102
+ now,
103
+ );
104
+ },
105
+ setTunnelSecret(secret: string, now: number): void {
106
+ ensure(now);
107
+ q.setSecret.run(secret, now);
108
+ },
109
+ setTunnelEnabled(enabled: boolean, now: number): void {
110
+ ensure(now);
111
+ q.setEnabled.run(enabled ? 1 : 0, now);
112
+ },
113
+ setTunnelProvider(provider: TunnelProvider, now: number): void {
114
+ ensure(now);
115
+ q.setProvider.run(provider, now);
116
+ },
117
+ setLhrEnabled(enabled: boolean, now: number): void {
118
+ ensure(now);
119
+ q.setLhrEnabled.run(enabled ? 1 : 0, now);
120
+ },
121
+ };
122
+ }
123
+
124
+ export type TunnelRepo = ReturnType<typeof createTunnelRepo>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Alamat bind server. Default loopback `127.0.0.1` sehingga hanya dapat
3
+ * diakses dari mesin ini; ubah lewat `KCG_HOST` (atau `--host` di CLI).
4
+ *
5
+ * Catatan: server tidak memakai otentikasi. Bind ke `0.0.0.0` membuat
6
+ * siapa pun di jaringan yang sama dapat mengontrol agent di mesin ini.
7
+ */
8
+ export const DEFAULT_HOSTNAME = "127.0.0.1";
9
+
10
+ export function resolveHostname(env: Record<string, string | undefined> = process.env): string {
11
+ return env.KCG_HOST ?? DEFAULT_HOSTNAME;
12
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Primitif tipe server-only (pola kcgcode: tipe internal server dipisah
3
+ * dari tipe domain FE yang hidup di `src/types/`). Tidak untuk dipakai
4
+ * frontend.
5
+ */
6
+
7
+ /** Hasil operasi tanpa payload (ok / gagal dengan kode error). */
8
+ export type SimpleResult = { ok: boolean; error?: string };
9
+
10
+ /**
11
+ * Tipe hasil diskriminasi yang dipakai seluruh layer domain:
12
+ * `{ ok: true; data } | { ok: false; error }` — bukan `throw`,
13
+ * sesuai `design.md` — Error Handling.
14
+ */
15
+ export type Result<T> = { ok: true; data: T } | { ok: false; error: string };