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.
- package/README.md +228 -2
- package/bin/kcgcode.ts +13 -0
- package/bun-env.d.ts +19 -0
- package/kcg-code.config.example.json +3 -0
- package/package.json +83 -4
- package/public/logo.svg +1 -0
- package/public/manifest.json +18 -0
- package/public/sw.js +49 -0
- package/src/App.tsx +39 -0
- package/src/__tests__/manifest.test.ts +61 -0
- package/src/build-config.ts +9 -0
- package/src/cli/__tests__/dashboard.test.ts +62 -0
- package/src/cli/args.ts +123 -0
- package/src/cli/commands.ts +174 -0
- package/src/cli/dashboard.tsx +343 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/theme.ts +100 -0
- package/src/components/auth/AuthGate.tsx +182 -0
- package/src/components/auth/AvatarEditor.tsx +236 -0
- package/src/components/auth/LockScreen.tsx +714 -0
- package/src/components/auth/ProfileAvatar.tsx +60 -0
- package/src/components/auth/WelcomeScreen.tsx +672 -0
- package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
- package/src/components/auth/avatar-icons.tsx +114 -0
- package/src/components/layout/AppShell.tsx +142 -0
- package/src/components/layout/AppSidebar.tsx +419 -0
- package/src/components/layout/SearchDialog.tsx +189 -0
- package/src/components/layout/SidebarAccount.tsx +111 -0
- package/src/components/layout/SidebarSessionMenu.tsx +163 -0
- package/src/components/layout/ThemeToggle.tsx +67 -0
- package/src/components/projects/FolderBrowser.tsx +142 -0
- package/src/components/projects/HomeChat.tsx +250 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/PanelSection.tsx +102 -0
- package/src/components/projects/ProjectInstructions.tsx +229 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +448 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +123 -0
- package/src/components/sessions/AssistantTurn.tsx +145 -0
- package/src/components/sessions/AttachmentTile.tsx +161 -0
- package/src/components/sessions/CodeBlock.tsx +107 -0
- package/src/components/sessions/ComposerActionSheet.tsx +96 -0
- package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +72 -0
- package/src/components/sessions/ContextPanel.tsx +561 -0
- package/src/components/sessions/DropPlate.tsx +165 -0
- package/src/components/sessions/FileDropOverlay.tsx +232 -0
- package/src/components/sessions/FileMentionDropdown.tsx +47 -0
- package/src/components/sessions/ImageLightbox.tsx +173 -0
- package/src/components/sessions/MarkdownContent.tsx +187 -0
- package/src/components/sessions/Mascot.tsx +47 -0
- package/src/components/sessions/MessageActions.tsx +60 -0
- package/src/components/sessions/MessageRail.tsx +121 -0
- package/src/components/sessions/ModelPicker.tsx +209 -0
- package/src/components/sessions/ModelSearchList.tsx +151 -0
- package/src/components/sessions/NewSessionDialog.tsx +189 -0
- package/src/components/sessions/PendingAttachments.tsx +127 -0
- package/src/components/sessions/PromptCard.tsx +1085 -0
- package/src/components/sessions/PromptPanel.tsx +112 -0
- package/src/components/sessions/SessionComposer.tsx +640 -0
- package/src/components/sessions/SessionHeader.tsx +175 -0
- package/src/components/sessions/SessionList.tsx +396 -0
- package/src/components/sessions/SessionRow.tsx +137 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +236 -0
- package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
- package/src/components/sessions/UserMessage.tsx +177 -0
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +220 -0
- package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
- package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
- package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
- package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
- package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
- package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
- package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
- package/src/components/settings/AboutGroup.tsx +170 -0
- package/src/components/settings/AccountGroup.tsx +122 -0
- package/src/components/settings/DevicesGroup.tsx +151 -0
- package/src/components/settings/LhrRemoteAccess.tsx +262 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1226 -0
- package/src/components/settings/SecurityGroup.tsx +566 -0
- package/src/components/settings/WorkspaceGroup.tsx +370 -0
- package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
- package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
- package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
- package/src/components/settings/prefs.tsx +265 -0
- package/src/components/skills/InstallNotifications.tsx +325 -0
- package/src/components/skills/InstallSkillDialog.tsx +233 -0
- package/src/components/skills/SkillCards.tsx +210 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/alert-dialog.tsx +179 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/bubble.tsx +125 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +225 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +234 -0
- package/src/components/ui/input-group.tsx +161 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +85 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/select.tsx +172 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +699 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/config.ts +85 -0
- package/src/db.ts +146 -0
- package/src/frontend.tsx +51 -0
- package/src/hooks/__tests__/useMention.test.ts +79 -0
- package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
- package/src/hooks/__tests__/useRouter.test.ts +17 -0
- package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
- package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
- package/src/hooks/__tests__/useTheme.test.ts +118 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/hooks/useAgentPicker.ts +97 -0
- package/src/hooks/useMediaQuery.ts +49 -0
- package/src/hooks/useMention.ts +236 -0
- package/src/hooks/useProjects.ts +88 -0
- package/src/hooks/useResyncOnReconnect.ts +28 -0
- package/src/hooks/useRouter.ts +92 -0
- package/src/hooks/useSessionChat.ts +427 -0
- package/src/hooks/useSessionContext.ts +106 -0
- package/src/hooks/useSessionList.ts +234 -0
- package/src/hooks/useSidebarData.ts +108 -0
- package/src/hooks/useSmoothText.ts +122 -0
- package/src/hooks/useTheme.ts +164 -0
- package/src/hooks/useWebSocket.ts +196 -0
- package/src/index.css +27 -0
- package/src/index.html +14 -0
- package/src/index.ts +49 -0
- package/src/lib/__tests__/attachments.test.ts +58 -0
- package/src/lib/__tests__/avatar-presets.test.ts +25 -0
- package/src/lib/__tests__/collapsible.test.ts +101 -0
- package/src/lib/__tests__/composer.test.ts +118 -0
- package/src/lib/__tests__/highlight.test.ts +42 -0
- package/src/lib/__tests__/home-chat.test.ts +64 -0
- package/src/lib/__tests__/lock-screen.test.ts +64 -0
- package/src/lib/__tests__/model-picker.test.ts +205 -0
- package/src/lib/__tests__/onboarding.test.ts +41 -0
- package/src/lib/__tests__/permission-info.test.ts +61 -0
- package/src/lib/__tests__/project-extensions.test.ts +52 -0
- package/src/lib/__tests__/project-overview.test.ts +173 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-summary.test.ts +115 -0
- package/src/lib/__tests__/session-title.test.ts +58 -0
- package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
- package/src/lib/__tests__/skill-installs.test.ts +134 -0
- package/src/lib/__tests__/skills-catalog.test.ts +96 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/api.ts +306 -0
- package/src/lib/attachments.ts +203 -0
- package/src/lib/auth.ts +92 -0
- package/src/lib/avatar-presets.ts +31 -0
- package/src/lib/collapsible.ts +75 -0
- package/src/lib/composer.ts +89 -0
- package/src/lib/data-events.ts +17 -0
- package/src/lib/headlines.ts +30 -0
- package/src/lib/highlight.ts +186 -0
- package/src/lib/layout.ts +20 -0
- package/src/lib/lock-screen.ts +54 -0
- package/src/lib/model-picker.ts +133 -0
- package/src/lib/onboarding.ts +38 -0
- package/src/lib/passkeys.ts +113 -0
- package/src/lib/pending-prompt.ts +53 -0
- package/src/lib/permission-info.ts +101 -0
- package/src/lib/project-extensions.ts +74 -0
- package/src/lib/project-overview.ts +117 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +115 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-status.ts +45 -0
- package/src/lib/session-summary.ts +68 -0
- package/src/lib/session-title.ts +63 -0
- package/src/lib/sidebar-groups.ts +78 -0
- package/src/lib/skill-installs.ts +360 -0
- package/src/lib/skills-catalog.ts +99 -0
- package/src/lib/time.ts +22 -0
- package/src/lib/tone.ts +80 -0
- package/src/lib/turns.ts +320 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +31 -0
- package/src/pages/projects/ProjectDetailPage.tsx +130 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +299 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +9 -0
- package/src/pages/skills/SkillsPage.tsx +701 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1031 -0
- package/src/server/__tests__/auth.test.ts +370 -0
- package/src/server/__tests__/config.test.ts +101 -0
- package/src/server/__tests__/db.test.ts +485 -0
- package/src/server/__tests__/host.test.ts +14 -0
- package/src/server/__tests__/tunnel-config.test.ts +38 -0
- package/src/server/app.ts +415 -0
- package/src/server/db/auth.ts +311 -0
- package/src/server/db/messages.ts +50 -0
- package/src/server/db/projects.ts +106 -0
- package/src/server/db/prompts.ts +78 -0
- package/src/server/db/rows.ts +150 -0
- package/src/server/db/schema.ts +221 -0
- package/src/server/db/sessions.ts +239 -0
- package/src/server/db/tunnel.ts +124 -0
- package/src/server/host.ts +12 -0
- package/src/server/result.ts +15 -0
- package/src/server/routes/auth-guard.ts +156 -0
- package/src/server/routes/auth.routes.ts +287 -0
- package/src/server/routes/helpers.ts +165 -0
- package/src/server/routes/meta.routes.ts +24 -0
- package/src/server/routes/passkeys.routes.ts +161 -0
- package/src/server/routes/projects.routes.ts +157 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +117 -0
- package/src/server/routes/tunnel.routes.ts +132 -0
- package/src/server/routes/types.ts +35 -0
- package/src/server/routes/uploads.routes.ts +54 -0
- package/src/server/routes/workspace.routes.ts +127 -0
- package/src/server/services/__tests__/app-info.test.ts +18 -0
- package/src/server/services/__tests__/attachments.test.ts +159 -0
- package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
- package/src/server/services/__tests__/auth.test.ts +341 -0
- package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
- package/src/server/services/__tests__/git-info.test.ts +74 -0
- package/src/server/services/__tests__/opencode.test.ts +320 -0
- package/src/server/services/__tests__/project-manager.test.ts +190 -0
- package/src/server/services/__tests__/sandbox.test.ts +141 -0
- package/src/server/services/__tests__/session-manager.test.ts +3469 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +226 -0
- package/src/server/services/__tests__/skills-registry.test.ts +359 -0
- package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
- package/src/server/services/__tests__/tunnel.test.ts +367 -0
- package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
- package/src/server/services/__tests__/workspace.test.ts +249 -0
- package/src/server/services/app-info.ts +29 -0
- package/src/server/services/attachments.ts +193 -0
- package/src/server/services/auth-passkeys.ts +459 -0
- package/src/server/services/auth.ts +633 -0
- package/src/server/services/frpc-installer.ts +183 -0
- package/src/server/services/git-info.ts +87 -0
- package/src/server/services/opencode-client.ts +714 -0
- package/src/server/services/opencode-server.ts +319 -0
- package/src/server/services/opencode-sse.ts +82 -0
- package/src/server/services/project-manager.ts +147 -0
- package/src/server/services/sandbox.ts +83 -0
- package/src/server/services/session-events.ts +256 -0
- package/src/server/services/session-manager.ts +1374 -0
- package/src/server/services/skill-install-jobs.ts +262 -0
- package/src/server/services/skills-registry.ts +468 -0
- package/src/server/services/tunnel-api.ts +207 -0
- package/src/server/services/tunnel-config.ts +45 -0
- package/src/server/services/tunnel-lhr.ts +387 -0
- package/src/server/services/tunnel.ts +633 -0
- package/src/server/services/turn-stream.ts +339 -0
- package/src/server/services/websocket-gateway.ts +374 -0
- package/src/server/services/workspace.ts +223 -0
- package/src/server/services/ws-transport.ts +121 -0
- package/src/types/context.ts +69 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +58 -0
- package/src/types/project.ts +16 -0
- package/src/types/prompt.ts +118 -0
- package/src/types/session.ts +52 -0
- package/src/types/turns.ts +47 -0
- package/src/ws-protocol.ts +93 -0
- package/styles/globals.css +311 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository kunci aplikasi (lock screen): pengaturan tunggal (`auth_settings`,
|
|
3
|
+
* id = 1) dan sesi login per perangkat (`auth_sessions`).
|
|
4
|
+
*
|
|
5
|
+
* Hanya hash yang disimpan: argon2id untuk sandi, SHA-256 untuk token sesi.
|
|
6
|
+
* Passkey (`auth_passkeys`) hanya menyimpan kunci publik.
|
|
7
|
+
*/
|
|
8
|
+
import type { Database } from "bun:sqlite";
|
|
9
|
+
|
|
10
|
+
export type LockKind = "pin" | "password";
|
|
11
|
+
|
|
12
|
+
export interface AuthSettings {
|
|
13
|
+
lockKind: LockKind | null;
|
|
14
|
+
lockHash: string | null;
|
|
15
|
+
nickname: string | null;
|
|
16
|
+
avatarMime: string | null;
|
|
17
|
+
/** Avatar bawaan (id preset) — null bila memakai foto / tanpa avatar. */
|
|
18
|
+
avatarPreset: string | null;
|
|
19
|
+
avatarVersion: number;
|
|
20
|
+
autoLockMinutes: number;
|
|
21
|
+
/** Onboarding (welcome screen) selesai; null = instalasi baru. */
|
|
22
|
+
onboardedAt: number | null;
|
|
23
|
+
updatedAt: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AuthSessionRow {
|
|
27
|
+
id: string;
|
|
28
|
+
tokenHash: string;
|
|
29
|
+
userAgent: string | null;
|
|
30
|
+
createdAt: number;
|
|
31
|
+
lastSeenAt: number;
|
|
32
|
+
expiresAt: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PasskeyRow {
|
|
36
|
+
id: string;
|
|
37
|
+
/** Credential ID WebAuthn (base64url). */
|
|
38
|
+
credentialId: string;
|
|
39
|
+
/** Kunci publik COSE. */
|
|
40
|
+
publicKey: Uint8Array;
|
|
41
|
+
counter: number;
|
|
42
|
+
transports: string[];
|
|
43
|
+
/** Domain tempat passkey didaftarkan. */
|
|
44
|
+
rpId: string;
|
|
45
|
+
name: string;
|
|
46
|
+
/** `singleDevice` | `multiDevice` (tersinkron, mis. iCloud/Google). */
|
|
47
|
+
deviceType: string | null;
|
|
48
|
+
backedUp: boolean;
|
|
49
|
+
createdAt: number;
|
|
50
|
+
lastUsedAt: number | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface PasskeyRowRaw {
|
|
54
|
+
id: string;
|
|
55
|
+
credential_id: string;
|
|
56
|
+
public_key: Uint8Array;
|
|
57
|
+
counter: number;
|
|
58
|
+
transports: string | null;
|
|
59
|
+
rp_id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
device_type: string | null;
|
|
62
|
+
backed_up: number;
|
|
63
|
+
created_at: number;
|
|
64
|
+
last_used_at: number | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function mapPasskey(r: PasskeyRowRaw): PasskeyRow {
|
|
68
|
+
let transports: string[] = [];
|
|
69
|
+
try {
|
|
70
|
+
const parsed = r.transports ? JSON.parse(r.transports) : [];
|
|
71
|
+
if (Array.isArray(parsed)) transports = parsed.filter((t) => typeof t === "string");
|
|
72
|
+
} catch {
|
|
73
|
+
// Kolom rusak -> tanpa hint transport (WebAuthn tetap jalan).
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
id: r.id,
|
|
77
|
+
credentialId: r.credential_id,
|
|
78
|
+
publicKey: new Uint8Array(r.public_key),
|
|
79
|
+
counter: r.counter,
|
|
80
|
+
transports,
|
|
81
|
+
rpId: r.rp_id,
|
|
82
|
+
name: r.name,
|
|
83
|
+
deviceType: r.device_type,
|
|
84
|
+
backedUp: r.backed_up === 1,
|
|
85
|
+
createdAt: r.created_at,
|
|
86
|
+
lastUsedAt: r.last_used_at,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface SettingsRow {
|
|
91
|
+
lock_kind: string | null;
|
|
92
|
+
lock_hash: string | null;
|
|
93
|
+
nickname: string | null;
|
|
94
|
+
avatar_mime: string | null;
|
|
95
|
+
avatar_preset: string | null;
|
|
96
|
+
avatar_version: number;
|
|
97
|
+
auto_lock_minutes: number;
|
|
98
|
+
onboarded_at: number | null;
|
|
99
|
+
updated_at: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface SessionRowRaw {
|
|
103
|
+
id: string;
|
|
104
|
+
token_hash: string;
|
|
105
|
+
user_agent: string | null;
|
|
106
|
+
created_at: number;
|
|
107
|
+
last_seen_at: number;
|
|
108
|
+
expires_at: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const DEFAULT_AUTO_LOCK_MINUTES = 15;
|
|
112
|
+
|
|
113
|
+
function mapSettings(r: SettingsRow): AuthSettings {
|
|
114
|
+
return {
|
|
115
|
+
lockKind: r.lock_kind === "pin" || r.lock_kind === "password" ? r.lock_kind : null,
|
|
116
|
+
lockHash: r.lock_hash,
|
|
117
|
+
nickname: r.nickname,
|
|
118
|
+
avatarMime: r.avatar_mime,
|
|
119
|
+
avatarPreset: r.avatar_preset,
|
|
120
|
+
avatarVersion: r.avatar_version,
|
|
121
|
+
autoLockMinutes: r.auto_lock_minutes,
|
|
122
|
+
onboardedAt: r.onboarded_at,
|
|
123
|
+
updatedAt: r.updated_at,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function mapSession(r: SessionRowRaw): AuthSessionRow {
|
|
128
|
+
return {
|
|
129
|
+
id: r.id,
|
|
130
|
+
tokenHash: r.token_hash,
|
|
131
|
+
userAgent: r.user_agent,
|
|
132
|
+
createdAt: r.created_at,
|
|
133
|
+
lastSeenAt: r.last_seen_at,
|
|
134
|
+
expiresAt: r.expires_at,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createAuthRepo(db: Database) {
|
|
139
|
+
const q = {
|
|
140
|
+
ensure: db.query(
|
|
141
|
+
"INSERT OR IGNORE INTO auth_settings (id, auto_lock_minutes, updated_at) VALUES (1, ?, ?)",
|
|
142
|
+
),
|
|
143
|
+
getSettings: db.query(
|
|
144
|
+
`SELECT lock_kind, lock_hash, nickname, avatar_mime, avatar_preset, avatar_version,
|
|
145
|
+
auto_lock_minutes, onboarded_at, updated_at FROM auth_settings WHERE id = 1`,
|
|
146
|
+
),
|
|
147
|
+
setLock: db.query(
|
|
148
|
+
"UPDATE auth_settings SET lock_kind = ?, lock_hash = ?, updated_at = ? WHERE id = 1",
|
|
149
|
+
),
|
|
150
|
+
setNickname: db.query("UPDATE auth_settings SET nickname = ?, updated_at = ? WHERE id = 1"),
|
|
151
|
+
// Foto & preset saling eksklusif: mengatur satu menghapus yang lain.
|
|
152
|
+
setAvatar: db.query(
|
|
153
|
+
`UPDATE auth_settings SET avatar = ?, avatar_mime = ?, avatar_preset = NULL,
|
|
154
|
+
avatar_version = avatar_version + 1, updated_at = ? WHERE id = 1`,
|
|
155
|
+
),
|
|
156
|
+
setPreset: db.query(
|
|
157
|
+
`UPDATE auth_settings SET avatar_preset = ?, avatar = NULL, avatar_mime = NULL,
|
|
158
|
+
avatar_version = avatar_version + 1, updated_at = ? WHERE id = 1`,
|
|
159
|
+
),
|
|
160
|
+
getAvatar: db.query("SELECT avatar, avatar_mime FROM auth_settings WHERE id = 1"),
|
|
161
|
+
setAutoLock: db.query(
|
|
162
|
+
"UPDATE auth_settings SET auto_lock_minutes = ?, updated_at = ? WHERE id = 1",
|
|
163
|
+
),
|
|
164
|
+
setOnboarded: db.query(
|
|
165
|
+
"UPDATE auth_settings SET onboarded_at = ?, updated_at = ? WHERE id = 1 AND onboarded_at IS NULL",
|
|
166
|
+
),
|
|
167
|
+
insertSession: db.query(
|
|
168
|
+
`INSERT INTO auth_sessions (id, token_hash, user_agent, created_at, last_seen_at, expires_at)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
170
|
+
),
|
|
171
|
+
getSessionByHash: db.query("SELECT * FROM auth_sessions WHERE token_hash = ?"),
|
|
172
|
+
getSessionById: db.query("SELECT * FROM auth_sessions WHERE id = ?"),
|
|
173
|
+
touchSession: db.query("UPDATE auth_sessions SET last_seen_at = ? WHERE id = ?"),
|
|
174
|
+
deleteSession: db.query("DELETE FROM auth_sessions WHERE id = ?"),
|
|
175
|
+
deleteAllSessions: db.query("DELETE FROM auth_sessions"),
|
|
176
|
+
deleteOtherSessions: db.query("DELETE FROM auth_sessions WHERE id <> ?"),
|
|
177
|
+
listSessions: db.query("SELECT * FROM auth_sessions ORDER BY last_seen_at DESC"),
|
|
178
|
+
deleteExpired: db.query("DELETE FROM auth_sessions WHERE expires_at <= ?"),
|
|
179
|
+
insertPasskey: db.query(
|
|
180
|
+
`INSERT INTO auth_passkeys (id, credential_id, public_key, counter, transports, rp_id, name,
|
|
181
|
+
device_type, backed_up, created_at, last_used_at)
|
|
182
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
183
|
+
),
|
|
184
|
+
listPasskeys: db.query("SELECT * FROM auth_passkeys ORDER BY created_at ASC"),
|
|
185
|
+
listPasskeysByRp: db.query(
|
|
186
|
+
"SELECT * FROM auth_passkeys WHERE rp_id = ? ORDER BY created_at ASC",
|
|
187
|
+
),
|
|
188
|
+
getPasskeyByCredential: db.query("SELECT * FROM auth_passkeys WHERE credential_id = ?"),
|
|
189
|
+
getPasskeyById: db.query("SELECT * FROM auth_passkeys WHERE id = ?"),
|
|
190
|
+
usePasskey: db.query("UPDATE auth_passkeys SET counter = ?, last_used_at = ? WHERE id = ?"),
|
|
191
|
+
renamePasskey: db.query("UPDATE auth_passkeys SET name = ? WHERE id = ?"),
|
|
192
|
+
deletePasskey: db.query("DELETE FROM auth_passkeys WHERE id = ?"),
|
|
193
|
+
deleteAllPasskeys: db.query("DELETE FROM auth_passkeys"),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
function ensureSettings(now: number): void {
|
|
197
|
+
q.ensure.run(DEFAULT_AUTO_LOCK_MINUTES, now);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
getAuthSettings(now: number): AuthSettings {
|
|
202
|
+
ensureSettings(now);
|
|
203
|
+
return mapSettings(q.getSettings.get() as SettingsRow);
|
|
204
|
+
},
|
|
205
|
+
setAuthLock(kind: LockKind | null, hash: string | null, now: number): void {
|
|
206
|
+
ensureSettings(now);
|
|
207
|
+
q.setLock.run(kind, hash, now);
|
|
208
|
+
},
|
|
209
|
+
setAuthNickname(nickname: string | null, now: number): void {
|
|
210
|
+
ensureSettings(now);
|
|
211
|
+
q.setNickname.run(nickname, now);
|
|
212
|
+
},
|
|
213
|
+
setAuthAvatar(bytes: Uint8Array | null, mime: string | null, now: number): void {
|
|
214
|
+
ensureSettings(now);
|
|
215
|
+
q.setAvatar.run(bytes, mime, now);
|
|
216
|
+
},
|
|
217
|
+
setAuthAvatarPreset(preset: string, now: number): void {
|
|
218
|
+
ensureSettings(now);
|
|
219
|
+
q.setPreset.run(preset, now);
|
|
220
|
+
},
|
|
221
|
+
getAuthAvatar(): { bytes: Uint8Array; mime: string } | null {
|
|
222
|
+
const row = q.getAvatar.get() as {
|
|
223
|
+
avatar: Uint8Array | null;
|
|
224
|
+
avatar_mime: string | null;
|
|
225
|
+
} | null;
|
|
226
|
+
if (!row?.avatar || !row.avatar_mime) return null;
|
|
227
|
+
return { bytes: row.avatar, mime: row.avatar_mime };
|
|
228
|
+
},
|
|
229
|
+
setAuthAutoLock(minutes: number, now: number): void {
|
|
230
|
+
ensureSettings(now);
|
|
231
|
+
q.setAutoLock.run(minutes, now);
|
|
232
|
+
},
|
|
233
|
+
/** Tandai onboarding selesai (idempoten: waktu pertama yang dipakai). */
|
|
234
|
+
setAuthOnboarded(now: number): void {
|
|
235
|
+
ensureSettings(now);
|
|
236
|
+
q.setOnboarded.run(now, now);
|
|
237
|
+
},
|
|
238
|
+
insertAuthSession(s: AuthSessionRow): void {
|
|
239
|
+
q.insertSession.run(s.id, s.tokenHash, s.userAgent, s.createdAt, s.lastSeenAt, s.expiresAt);
|
|
240
|
+
},
|
|
241
|
+
getAuthSessionByHash(tokenHash: string): AuthSessionRow | null {
|
|
242
|
+
const row = q.getSessionByHash.get(tokenHash) as SessionRowRaw | null;
|
|
243
|
+
return row ? mapSession(row) : null;
|
|
244
|
+
},
|
|
245
|
+
getAuthSessionById(id: string): AuthSessionRow | null {
|
|
246
|
+
const row = q.getSessionById.get(id) as SessionRowRaw | null;
|
|
247
|
+
return row ? mapSession(row) : null;
|
|
248
|
+
},
|
|
249
|
+
touchAuthSession(id: string, now: number): void {
|
|
250
|
+
q.touchSession.run(now, id);
|
|
251
|
+
},
|
|
252
|
+
deleteAuthSession(id: string): void {
|
|
253
|
+
q.deleteSession.run(id);
|
|
254
|
+
},
|
|
255
|
+
deleteAllAuthSessions(): void {
|
|
256
|
+
q.deleteAllSessions.run();
|
|
257
|
+
},
|
|
258
|
+
deleteOtherAuthSessions(keepId: string): void {
|
|
259
|
+
q.deleteOtherSessions.run(keepId);
|
|
260
|
+
},
|
|
261
|
+
listAuthSessions(): AuthSessionRow[] {
|
|
262
|
+
return (q.listSessions.all() as SessionRowRaw[]).map(mapSession);
|
|
263
|
+
},
|
|
264
|
+
deleteExpiredAuthSessions(now: number): void {
|
|
265
|
+
q.deleteExpired.run(now);
|
|
266
|
+
},
|
|
267
|
+
insertPasskey(p: PasskeyRow): void {
|
|
268
|
+
q.insertPasskey.run(
|
|
269
|
+
p.id,
|
|
270
|
+
p.credentialId,
|
|
271
|
+
p.publicKey,
|
|
272
|
+
p.counter,
|
|
273
|
+
JSON.stringify(p.transports),
|
|
274
|
+
p.rpId,
|
|
275
|
+
p.name,
|
|
276
|
+
p.deviceType,
|
|
277
|
+
p.backedUp ? 1 : 0,
|
|
278
|
+
p.createdAt,
|
|
279
|
+
p.lastUsedAt,
|
|
280
|
+
);
|
|
281
|
+
},
|
|
282
|
+
listPasskeys(rpId?: string): PasskeyRow[] {
|
|
283
|
+
const rows = (
|
|
284
|
+
rpId === undefined ? q.listPasskeys.all() : q.listPasskeysByRp.all(rpId)
|
|
285
|
+
) as PasskeyRowRaw[];
|
|
286
|
+
return rows.map(mapPasskey);
|
|
287
|
+
},
|
|
288
|
+
getPasskeyByCredentialId(credentialId: string): PasskeyRow | null {
|
|
289
|
+
const row = q.getPasskeyByCredential.get(credentialId) as PasskeyRowRaw | null;
|
|
290
|
+
return row ? mapPasskey(row) : null;
|
|
291
|
+
},
|
|
292
|
+
getPasskeyById(id: string): PasskeyRow | null {
|
|
293
|
+
const row = q.getPasskeyById.get(id) as PasskeyRowRaw | null;
|
|
294
|
+
return row ? mapPasskey(row) : null;
|
|
295
|
+
},
|
|
296
|
+
markPasskeyUsed(id: string, counter: number, now: number): void {
|
|
297
|
+
q.usePasskey.run(counter, now, id);
|
|
298
|
+
},
|
|
299
|
+
renamePasskey(id: string, name: string): void {
|
|
300
|
+
q.renamePasskey.run(name, id);
|
|
301
|
+
},
|
|
302
|
+
deletePasskey(id: string): void {
|
|
303
|
+
q.deletePasskey.run(id);
|
|
304
|
+
},
|
|
305
|
+
deleteAllPasskeys(): void {
|
|
306
|
+
q.deleteAllPasskeys.run();
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export type AuthRepo = ReturnType<typeof createAuthRepo>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository pesan percakapan terstruktur (append-only). Seluruh tulis baca
|
|
3
|
+
* pesan milik satu Session, jadi penjagaan `SESSION_NOT_FOUND` ada di sini.
|
|
4
|
+
*/
|
|
5
|
+
import type { Database } from "bun:sqlite";
|
|
6
|
+
import type { SessionMessage } from "../../types";
|
|
7
|
+
import type { Result } from "../result";
|
|
8
|
+
import { errResult, type MessageRow, mapMessage } from "./rows";
|
|
9
|
+
import { sessionExists } from "./sessions";
|
|
10
|
+
|
|
11
|
+
export function createMessageRepo(db: Database) {
|
|
12
|
+
const q = {
|
|
13
|
+
insertMessage: db.query(
|
|
14
|
+
"INSERT INTO messages (session_id, message_id, role, parts_json, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
15
|
+
),
|
|
16
|
+
getMessages: db.query(
|
|
17
|
+
"SELECT session_id, message_id, role, parts_json, created_at FROM messages WHERE session_id = ? ORDER BY created_at ASC, id ASC",
|
|
18
|
+
),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
insertMessage(message: SessionMessage): Result<SessionMessage> {
|
|
23
|
+
try {
|
|
24
|
+
if (!sessionExists(db, message.sessionId)) return errResult("SESSION_NOT_FOUND");
|
|
25
|
+
q.insertMessage.run(
|
|
26
|
+
message.sessionId,
|
|
27
|
+
message.id,
|
|
28
|
+
message.role,
|
|
29
|
+
JSON.stringify(message.parts),
|
|
30
|
+
message.createdAt,
|
|
31
|
+
);
|
|
32
|
+
return { ok: true, data: message };
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return errResult(`MESSAGE_WRITE_FAILED: ${(e as Error).message}`);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
getMessages(sessionId: string): Result<SessionMessage[]> {
|
|
39
|
+
try {
|
|
40
|
+
if (!sessionExists(db, sessionId)) return errResult("SESSION_NOT_FOUND");
|
|
41
|
+
const rows = q.getMessages.all(sessionId) as MessageRow[];
|
|
42
|
+
return { ok: true, data: rows.map(mapMessage) };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return errResult(`MESSAGE_READ_FAILED: ${(e as Error).message}`);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type MessageRepo = ReturnType<typeof createMessageRepo>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository Projects. Berinteraksi dengan tabel `sessions` hanya untuk
|
|
3
|
+
* penjagaan referensial `deleteProject` (membaca, tidak menulis).
|
|
4
|
+
*/
|
|
5
|
+
import type { Database } from "bun:sqlite";
|
|
6
|
+
import type { Project, Session } from "../../types";
|
|
7
|
+
import type { Result, SimpleResult } from "../result";
|
|
8
|
+
import { errResult, mapProject, mapSession, type ProjectRow, type SessionRow } from "./rows";
|
|
9
|
+
|
|
10
|
+
export function createProjectRepo(db: Database) {
|
|
11
|
+
const q = {
|
|
12
|
+
insertProject: db.query(
|
|
13
|
+
"INSERT INTO projects (id, name, path, instructions, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
14
|
+
),
|
|
15
|
+
updateInstructions: db.query("UPDATE projects SET instructions = ? WHERE id = ?"),
|
|
16
|
+
getProjectById: db.query("SELECT * FROM projects WHERE id = ?"),
|
|
17
|
+
getProjectByName: db.query("SELECT * FROM projects WHERE name = ?"),
|
|
18
|
+
getProjectByPath: db.query("SELECT * FROM projects WHERE path = ?"),
|
|
19
|
+
listProjects: db.query("SELECT * FROM projects ORDER BY created_at ASC, name ASC"),
|
|
20
|
+
listProjectSessions: db.query(
|
|
21
|
+
"SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at ASC, id ASC",
|
|
22
|
+
),
|
|
23
|
+
deleteProjectRow: db.query("DELETE FROM projects WHERE id = ?"),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
insertProject(project: Project): Result<Project> {
|
|
28
|
+
try {
|
|
29
|
+
q.insertProject.run(
|
|
30
|
+
project.id,
|
|
31
|
+
project.name,
|
|
32
|
+
project.path,
|
|
33
|
+
project.instructions ?? null,
|
|
34
|
+
project.createdAt,
|
|
35
|
+
);
|
|
36
|
+
return { ok: true, data: project };
|
|
37
|
+
} catch (e) {
|
|
38
|
+
const msg = (e as Error).message;
|
|
39
|
+
if (msg.includes("projects.name")) return errResult("NAME_TAKEN");
|
|
40
|
+
if (msg.includes("projects.path")) return errResult("PATH_TAKEN");
|
|
41
|
+
return errResult(`PROJECT_WRITE_FAILED: ${msg}`);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
getProjectById(id: string): Result<Project> {
|
|
46
|
+
const row = q.getProjectById.get(id) as ProjectRow | null;
|
|
47
|
+
if (!row) return errResult("PROJECT_NOT_FOUND");
|
|
48
|
+
return { ok: true, data: mapProject(row) };
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
getProjectByName(name: string): Result<Project> {
|
|
52
|
+
const row = q.getProjectByName.get(name) as ProjectRow | null;
|
|
53
|
+
if (!row) return errResult("PROJECT_NOT_FOUND");
|
|
54
|
+
return { ok: true, data: mapProject(row) };
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
getProjectByPath(filePath: string): Result<Project> {
|
|
58
|
+
const row = q.getProjectByPath.get(filePath) as ProjectRow | null;
|
|
59
|
+
if (!row) return errResult("PROJECT_NOT_FOUND");
|
|
60
|
+
return { ok: true, data: mapProject(row) };
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/** Simpan custom instruction Project; `null` = hapus instruksi. */
|
|
64
|
+
updateProjectInstructions(projectId: string, instructions: string | null): Result<Project> {
|
|
65
|
+
try {
|
|
66
|
+
const res = q.updateInstructions.run(instructions, projectId);
|
|
67
|
+
if (res.changes === 0) return errResult("PROJECT_NOT_FOUND");
|
|
68
|
+
const row = q.getProjectById.get(projectId) as ProjectRow | null;
|
|
69
|
+
if (!row) return errResult("PROJECT_NOT_FOUND");
|
|
70
|
+
return { ok: true, data: mapProject(row) };
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return errResult(`PROJECT_WRITE_FAILED: ${(e as Error).message}`);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
listProjects(): Project[] {
|
|
77
|
+
return (q.listProjects.all() as ProjectRow[]).map(mapProject);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/** Session milik satu Project (dipakai sebelum menghapus Project). */
|
|
81
|
+
listProjectSessions(projectId: string): Session[] {
|
|
82
|
+
return (q.listProjectSessions.all(projectId) as SessionRow[]).map(mapSession);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Hapus baris Project. Session milik Project harus sudah dihapus lebih
|
|
87
|
+
* dulu: `sessions.project_id` punya foreign key ke `projects(id)`, dan
|
|
88
|
+
* menghapus Session lewat jalur ini akan melewatkan pembersihan sesi
|
|
89
|
+
* remote opencode + lampiran gambarnya.
|
|
90
|
+
*/
|
|
91
|
+
deleteProject(projectId: string): SimpleResult {
|
|
92
|
+
try {
|
|
93
|
+
const existing = q.getProjectById.get(projectId) as ProjectRow | null;
|
|
94
|
+
if (!existing) return errResult("PROJECT_NOT_FOUND");
|
|
95
|
+
const sessions = q.listProjectSessions.all(projectId) as SessionRow[];
|
|
96
|
+
if (sessions.length > 0) return errResult("PROJECT_HAS_SESSIONS");
|
|
97
|
+
q.deleteProjectRow.run(projectId);
|
|
98
|
+
return { ok: true };
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return errResult(`PROJECT_DELETE_FAILED: ${(e as Error).message}`);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type ProjectRepo = ReturnType<typeof createProjectRepo>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository prompt interaktif (permission/question opencode) per Session.
|
|
3
|
+
*/
|
|
4
|
+
import type { Database } from "bun:sqlite";
|
|
5
|
+
import type { InteractivePrompt, PromptStatus } from "../../types";
|
|
6
|
+
import type { Result } from "../result";
|
|
7
|
+
import { errResult, mapPrompt, type PromptRow } from "./rows";
|
|
8
|
+
import { sessionExists } from "./sessions";
|
|
9
|
+
|
|
10
|
+
export function createPromptRepo(db: Database) {
|
|
11
|
+
const q = {
|
|
12
|
+
insertPrompt: db.query(
|
|
13
|
+
"INSERT INTO prompts (id, session_id, kind, type, custom, multiple, title, options_json, questions_json, status, created_at, resolved_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
14
|
+
),
|
|
15
|
+
getPrompt: db.query("SELECT * FROM prompts WHERE id = ?"),
|
|
16
|
+
pendingPrompts: db.query(
|
|
17
|
+
"SELECT * FROM prompts WHERE session_id = ? AND status = 'pending' ORDER BY created_at ASC, id ASC",
|
|
18
|
+
),
|
|
19
|
+
updatePrompt: db.query("UPDATE prompts SET status = ?, resolved_at = ? WHERE id = ?"),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
insertPrompt(prompt: InteractivePrompt): Result<InteractivePrompt> {
|
|
24
|
+
try {
|
|
25
|
+
if (!sessionExists(db, prompt.sessionId)) return errResult("SESSION_NOT_FOUND");
|
|
26
|
+
q.insertPrompt.run(
|
|
27
|
+
prompt.id,
|
|
28
|
+
prompt.sessionId,
|
|
29
|
+
prompt.kind,
|
|
30
|
+
prompt.type,
|
|
31
|
+
prompt.custom === true ? 1 : 0,
|
|
32
|
+
// Flag multi-select question (default false) — ikut tersimpan agar
|
|
33
|
+
// kartu yang dirender ulang dari history tetap multi-select.
|
|
34
|
+
prompt.multiple === true ? 1 : 0,
|
|
35
|
+
prompt.title,
|
|
36
|
+
prompt.options ? JSON.stringify(prompt.options) : null,
|
|
37
|
+
// Multi-question: seluruh pertanyaan request (null = pertanyaan
|
|
38
|
+
// tunggal — field legacy sudah cukup).
|
|
39
|
+
prompt.questions && prompt.questions.length > 0 ? JSON.stringify(prompt.questions) : null,
|
|
40
|
+
prompt.status,
|
|
41
|
+
prompt.createdAt,
|
|
42
|
+
prompt.resolvedAt,
|
|
43
|
+
);
|
|
44
|
+
return { ok: true, data: prompt };
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return errResult(`PROMPT_WRITE_FAILED: ${(e as Error).message}`);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
getPrompt(promptId: string): Result<InteractivePrompt> {
|
|
51
|
+
const row = q.getPrompt.get(promptId) as PromptRow | null;
|
|
52
|
+
if (!row) return errResult("PROMPT_NOT_FOUND");
|
|
53
|
+
return { ok: true, data: mapPrompt(row) };
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
listPendingPrompts(sessionId: string): InteractivePrompt[] {
|
|
57
|
+
return (q.pendingPrompts.all(sessionId) as PromptRow[]).map(mapPrompt);
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
updatePromptStatus(
|
|
61
|
+
promptId: string,
|
|
62
|
+
status: PromptStatus,
|
|
63
|
+
resolvedAt = Date.now(),
|
|
64
|
+
): Result<InteractivePrompt> {
|
|
65
|
+
try {
|
|
66
|
+
const existing = q.getPrompt.get(promptId) as PromptRow | null;
|
|
67
|
+
if (!existing) return errResult("PROMPT_NOT_FOUND");
|
|
68
|
+
q.updatePrompt.run(status, resolvedAt, promptId);
|
|
69
|
+
const updated = mapPrompt(q.getPrompt.get(promptId) as PromptRow);
|
|
70
|
+
return { ok: true, data: updated };
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return errResult(`PROMPT_UPDATE_FAILED: ${(e as Error).message}`);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type PromptRepo = ReturnType<typeof createPromptRepo>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bentuk baris SQLite & pemetaan ke tipe domain (shared antar repository).
|
|
3
|
+
* Internal data-layer — tidak untuk dipakai luar `server/db`.
|
|
4
|
+
*/
|
|
5
|
+
import type {
|
|
6
|
+
InteractivePrompt,
|
|
7
|
+
MessagePart,
|
|
8
|
+
Project,
|
|
9
|
+
PromptStatus,
|
|
10
|
+
Session,
|
|
11
|
+
SessionMessage,
|
|
12
|
+
SessionModel,
|
|
13
|
+
SessionStatus,
|
|
14
|
+
} from "../../types";
|
|
15
|
+
import { normalizePromptOptions } from "../../types";
|
|
16
|
+
|
|
17
|
+
/** Hasil gagal seragam `{ ok: false; error }`. */
|
|
18
|
+
export function errResult(msg: string): { ok: false; error: string } {
|
|
19
|
+
return { ok: false, error: msg };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProjectRow {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
instructions: string | null;
|
|
27
|
+
created_at: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SessionRow {
|
|
31
|
+
id: string;
|
|
32
|
+
project_id: string;
|
|
33
|
+
agent_type: string;
|
|
34
|
+
cwd: string;
|
|
35
|
+
status: string;
|
|
36
|
+
oc_session_id: string | null;
|
|
37
|
+
model: string | null;
|
|
38
|
+
agent: string | null;
|
|
39
|
+
title: string | null;
|
|
40
|
+
created_at: number;
|
|
41
|
+
updated_at: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PromptRow {
|
|
45
|
+
id: string;
|
|
46
|
+
session_id: string;
|
|
47
|
+
kind: string;
|
|
48
|
+
type: string;
|
|
49
|
+
custom: number;
|
|
50
|
+
multiple: number;
|
|
51
|
+
title: string | null;
|
|
52
|
+
options_json: string | null;
|
|
53
|
+
questions_json: string | null;
|
|
54
|
+
status: string;
|
|
55
|
+
created_at: number;
|
|
56
|
+
resolved_at: number | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface MessageRow {
|
|
60
|
+
session_id: string;
|
|
61
|
+
message_id: string;
|
|
62
|
+
role: string;
|
|
63
|
+
parts_json: string;
|
|
64
|
+
created_at: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function mapProject(r: ProjectRow): Project {
|
|
68
|
+
return {
|
|
69
|
+
id: r.id,
|
|
70
|
+
name: r.name,
|
|
71
|
+
path: r.path,
|
|
72
|
+
createdAt: r.created_at,
|
|
73
|
+
instructions: r.instructions ?? null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse kolom `sessions.model` (JSON) -> SessionModel.
|
|
79
|
+
* Nilai NULL/korup/tidak lengkap dianggap "pakai model default" (null).
|
|
80
|
+
*/
|
|
81
|
+
export function parseModel(raw: string | null): SessionModel | null {
|
|
82
|
+
if (raw === null || raw === "") return null;
|
|
83
|
+
try {
|
|
84
|
+
const obj = JSON.parse(raw) as { providerID?: unknown; modelID?: unknown } | null;
|
|
85
|
+
if (obj && typeof obj.providerID === "string" && typeof obj.modelID === "string") {
|
|
86
|
+
return { providerID: obj.providerID, modelID: obj.modelID };
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
/* data korup -> default */
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function mapSession(r: SessionRow): Session {
|
|
95
|
+
return {
|
|
96
|
+
id: r.id,
|
|
97
|
+
projectId: r.project_id,
|
|
98
|
+
agentType: r.agent_type as Session["agentType"],
|
|
99
|
+
cwd: r.cwd,
|
|
100
|
+
status: r.status as SessionStatus,
|
|
101
|
+
ocSessionId: r.oc_session_id,
|
|
102
|
+
model: parseModel(r.model),
|
|
103
|
+
agent: r.agent ?? null,
|
|
104
|
+
title: r.title ?? null,
|
|
105
|
+
createdAt: r.created_at,
|
|
106
|
+
updatedAt: r.updated_at,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function mapPrompt(r: PromptRow): InteractivePrompt {
|
|
111
|
+
// `questions_json` korup / bukan array -> dianggap tanpa multi-question
|
|
112
|
+
// (field legacy tetap dipakai) — sama seperti toleransi options_json.
|
|
113
|
+
let questions: InteractivePrompt["questions"];
|
|
114
|
+
if (r.questions_json) {
|
|
115
|
+
try {
|
|
116
|
+
const parsed: unknown = JSON.parse(r.questions_json);
|
|
117
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
118
|
+
questions = parsed as InteractivePrompt["questions"];
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
/* data korup -> tanpa multi-question */
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
id: r.id,
|
|
126
|
+
sessionId: r.session_id,
|
|
127
|
+
kind: (r.kind as InteractivePrompt["kind"]) ?? "permission",
|
|
128
|
+
type: r.type as InteractivePrompt["type"],
|
|
129
|
+
custom: r.custom === 1,
|
|
130
|
+
multiple: r.multiple === 1,
|
|
131
|
+
title: r.title,
|
|
132
|
+
// `options_json` bisa berisi string polos (DB lama) atau objek opsi
|
|
133
|
+
// ({label, description}) — normalisasi toleran terhadap keduanya.
|
|
134
|
+
options: r.options_json ? normalizePromptOptions(JSON.parse(r.options_json)) : null,
|
|
135
|
+
questions,
|
|
136
|
+
status: r.status as PromptStatus,
|
|
137
|
+
createdAt: r.created_at,
|
|
138
|
+
resolvedAt: r.resolved_at,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function mapMessage(r: MessageRow): SessionMessage {
|
|
143
|
+
return {
|
|
144
|
+
id: r.message_id,
|
|
145
|
+
sessionId: r.session_id,
|
|
146
|
+
role: r.role as SessionMessage["role"],
|
|
147
|
+
parts: JSON.parse(r.parts_json) as MessagePart[],
|
|
148
|
+
createdAt: r.created_at,
|
|
149
|
+
};
|
|
150
|
+
}
|