vibepulse 0.1.0 → 0.1.2

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 (68) hide show
  1. package/README.md +7 -13
  2. package/bin/vibepulse.js +1 -0
  3. package/dist/index.js +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/docs/session-status-detection.md +258 -0
  6. package/next.config.ts +11 -0
  7. package/package.json +17 -11
  8. package/postcss.config.mjs +7 -0
  9. package/public/file.svg +1 -0
  10. package/public/globe.svg +1 -0
  11. package/public/next.svg +1 -0
  12. package/public/readme-cover.png +0 -0
  13. package/public/vercel.svg +1 -0
  14. package/public/window.svg +1 -0
  15. package/src/app/api/opencode-config/route.ts +304 -0
  16. package/src/app/api/opencode-config/status/route.ts +31 -0
  17. package/src/app/api/opencode-events/route.ts +86 -0
  18. package/src/app/api/opencode-models/route.test.ts +135 -0
  19. package/src/app/api/opencode-models/route.ts +58 -0
  20. package/src/app/api/profiles/[id]/apply/route.ts +49 -0
  21. package/src/app/api/profiles/[id]/route.ts +160 -0
  22. package/src/app/api/profiles/route.ts +107 -0
  23. package/src/app/api/sessions/[id]/archive/route.ts +35 -0
  24. package/src/app/api/sessions/[id]/delete/route.ts +26 -0
  25. package/src/app/api/sessions/[id]/route.ts +45 -0
  26. package/src/app/api/sessions/route.ts +596 -0
  27. package/src/app/favicon.ico +0 -0
  28. package/src/app/globals.css +66 -0
  29. package/src/app/layout.tsx +37 -0
  30. package/src/app/page.tsx +239 -0
  31. package/src/components/ErrorBoundary.tsx +72 -0
  32. package/src/components/KanbanBoard.tsx +442 -0
  33. package/src/components/LoadingState.tsx +37 -0
  34. package/src/components/ProjectCard.tsx +382 -0
  35. package/src/components/QueryProvider.tsx +25 -0
  36. package/src/components/SessionCard.tsx +291 -0
  37. package/src/components/SessionList.tsx +60 -0
  38. package/src/components/opencode-config/AgentConfigForm.test.tsx +66 -0
  39. package/src/components/opencode-config/AgentConfigForm.tsx +445 -0
  40. package/src/components/opencode-config/AgentModelSelector.tsx +284 -0
  41. package/src/components/opencode-config/AgentsConfigPanel.tsx +162 -0
  42. package/src/components/opencode-config/ConfigButton.tsx +43 -0
  43. package/src/components/opencode-config/ConfigPanel.tsx +91 -0
  44. package/src/components/opencode-config/FullscreenConfigPanel.tsx +360 -0
  45. package/src/components/opencode-config/categories/CategoriesList.tsx +328 -0
  46. package/src/components/opencode-config/categories/CategoriesManager.test.tsx +97 -0
  47. package/src/components/opencode-config/categories/CategoriesManager.tsx +174 -0
  48. package/src/components/opencode-config/categories/CategoryConfigForm.tsx +384 -0
  49. package/src/components/opencode-config/profiles/ProfileCard.tsx +140 -0
  50. package/src/components/opencode-config/profiles/ProfileEditor.tsx +446 -0
  51. package/src/components/opencode-config/profiles/ProfileList.tsx +398 -0
  52. package/src/components/opencode-config/profiles/ProfileManager.test.tsx +122 -0
  53. package/src/components/opencode-config/profiles/ProfileManager.tsx +293 -0
  54. package/src/components/ui/Tabs.tsx +59 -0
  55. package/src/hooks/useOpencodeSync.ts +378 -0
  56. package/src/index.ts +2 -0
  57. package/src/lib/notificationSound.ts +266 -0
  58. package/src/lib/opencodeConfig.test.ts +81 -0
  59. package/src/lib/opencodeConfig.ts +48 -0
  60. package/src/lib/opencodeDiscovery.ts +154 -0
  61. package/src/lib/profiles/storage.ts +264 -0
  62. package/src/lib/transform.ts +84 -0
  63. package/src/test/setup.ts +8 -0
  64. package/src/types/index.ts +89 -0
  65. package/src/types/opencodeConfig.ts +133 -0
  66. package/src/types/testing-library-vitest.d.ts +17 -0
  67. package/tsconfig.json +34 -0
  68. package/tsconfig.lib.json +17 -0
@@ -0,0 +1,160 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import {
3
+ readProfileIndex,
4
+ writeProfileIndex,
5
+ getProfileById,
6
+ readProfileConfig,
7
+ writeProfileConfig,
8
+ deleteProfileConfig,
9
+ } from '@/lib/profiles/storage';
10
+
11
+
12
+ interface RouteParams {
13
+ params: Promise<{ id: string }>;
14
+ }
15
+
16
+ export async function GET(_request: NextRequest, { params }: RouteParams) {
17
+ try {
18
+ const { id } = await params;
19
+ const profile = await getProfileById(id);
20
+
21
+ if (!profile) {
22
+ return NextResponse.json(
23
+ { error: 'Profile not found' },
24
+ { status: 404 }
25
+ );
26
+ }
27
+
28
+ const config = await readProfileConfig(id);
29
+
30
+ return NextResponse.json({
31
+ profile,
32
+ config,
33
+ });
34
+ } catch (error) {
35
+ console.error('Error reading profile:', error);
36
+ return NextResponse.json(
37
+ { error: 'Internal server error' },
38
+ { status: 500 }
39
+ );
40
+ }
41
+ }
42
+
43
+ export async function PUT(request: NextRequest, { params }: RouteParams) {
44
+ try {
45
+ const { id } = await params;
46
+ const profile = await getProfileById(id);
47
+
48
+ if (!profile) {
49
+ return NextResponse.json(
50
+ { error: 'Profile not found' },
51
+ { status: 404 }
52
+ );
53
+ }
54
+
55
+ const body = await request.json();
56
+
57
+ if (!body || typeof body !== 'object') {
58
+ return NextResponse.json(
59
+ { error: 'Invalid request body' },
60
+ { status: 400 }
61
+ );
62
+ }
63
+
64
+ // Support both { name, ... } and { profile: { name, ... }, config } formats
65
+ const profileData = body.profile || body;
66
+ const { name, description, emoji } = profileData;
67
+ const config = body.config || profileData.config;
68
+
69
+ if (name !== undefined) {
70
+ if (typeof name !== 'string' || name.trim() === '') {
71
+ return NextResponse.json(
72
+ { error: 'name must be a non-empty string' },
73
+ { status: 400 }
74
+ );
75
+ }
76
+ profile.name = name.trim();
77
+ }
78
+
79
+ if (description !== undefined) {
80
+ profile.description = description?.trim() || undefined;
81
+ }
82
+
83
+ if (emoji !== undefined) {
84
+ profile.emoji = emoji || '⚙️';
85
+ }
86
+
87
+ profile.updatedAt = new Date().toISOString();
88
+
89
+ const index = await readProfileIndex();
90
+ const profileIndex = index.profiles.findIndex(p => p.id === id);
91
+
92
+ if (profileIndex === -1) {
93
+ return NextResponse.json(
94
+ { error: 'Profile not found in index' },
95
+ { status: 404 }
96
+ );
97
+ }
98
+
99
+ index.profiles[profileIndex] = profile;
100
+ await writeProfileIndex(index);
101
+
102
+ if (config && typeof config === 'object') {
103
+ await writeProfileConfig(id, {
104
+ agents: config.agents || {},
105
+ categories: config.categories,
106
+ });
107
+ }
108
+
109
+ return NextResponse.json({ profile });
110
+ } catch (error) {
111
+ console.error('Error updating profile:', error);
112
+ return NextResponse.json(
113
+ { error: 'Internal server error' },
114
+ { status: 500 }
115
+ );
116
+ }
117
+ }
118
+
119
+ export async function DELETE(_request: NextRequest, { params }: RouteParams) {
120
+ try {
121
+ const { id } = await params;
122
+ const profile = await getProfileById(id);
123
+
124
+ if (!profile) {
125
+ return NextResponse.json(
126
+ { error: 'Profile not found' },
127
+ { status: 404 }
128
+ );
129
+ }
130
+
131
+ if (profile.isBuiltIn) {
132
+ await deleteProfileConfig(id);
133
+
134
+ return NextResponse.json({
135
+ message: 'Built-in profile reset to defaults',
136
+ profile,
137
+ });
138
+ }
139
+
140
+ const index = await readProfileIndex();
141
+ index.profiles = index.profiles.filter(p => p.id !== id);
142
+
143
+ if (index.activeProfileId === id) {
144
+ index.activeProfileId = null;
145
+ }
146
+
147
+ await writeProfileIndex(index);
148
+ await deleteProfileConfig(id);
149
+
150
+ return NextResponse.json({
151
+ message: 'Profile deleted successfully',
152
+ });
153
+ } catch (error) {
154
+ console.error('Error deleting profile:', error);
155
+ return NextResponse.json(
156
+ { error: 'Internal server error' },
157
+ { status: 500 }
158
+ );
159
+ }
160
+ }
@@ -0,0 +1,107 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import {
3
+ readProfileIndex,
4
+ writeProfileIndex,
5
+ getProfileById,
6
+ writeProfileConfig,
7
+ } from '@/lib/profiles/storage';
8
+ import type { Profile, ProfileConfig } from '@/lib/profiles/storage';
9
+
10
+ export async function GET() {
11
+ try {
12
+ const index = await readProfileIndex();
13
+
14
+ return NextResponse.json({
15
+ profiles: index.profiles,
16
+ activeProfileId: index.activeProfileId,
17
+ });
18
+ } catch (error) {
19
+ console.error('Error reading profiles:', error);
20
+ return NextResponse.json(
21
+ { error: 'Internal server error' },
22
+ { status: 500 }
23
+ );
24
+ }
25
+ }
26
+
27
+ export async function POST(request: NextRequest) {
28
+ try {
29
+ const body = await request.json();
30
+
31
+ if (!body || typeof body !== 'object') {
32
+ return NextResponse.json(
33
+ { error: 'Invalid request body' },
34
+ { status: 400 }
35
+ );
36
+ }
37
+
38
+ // Support both { id, name, ... } and { profile: { id, name, ... }, config } formats
39
+ const profileData = body.profile || body;
40
+ const { id, name, emoji, description } = profileData;
41
+ const config = body.config || profileData.config;
42
+
43
+ if (!id || typeof id !== 'string' || id.trim() === '') {
44
+ return NextResponse.json(
45
+ { error: 'id is required and must be a non-empty string' },
46
+ { status: 400 }
47
+ );
48
+ }
49
+
50
+ if (!name || typeof name !== 'string' || name.trim() === '') {
51
+ return NextResponse.json(
52
+ { error: 'name is required and must be a non-empty string' },
53
+ { status: 400 }
54
+ );
55
+ }
56
+
57
+ const existingProfile = await getProfileById(id);
58
+ if (existingProfile) {
59
+ return NextResponse.json(
60
+ { error: `Profile with id '${id}' already exists` },
61
+ { status: 400 }
62
+ );
63
+ }
64
+
65
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
66
+ return NextResponse.json(
67
+ { error: 'id must contain only letters, numbers, hyphens, and underscores' },
68
+ { status: 400 }
69
+ );
70
+ }
71
+
72
+ const now = new Date().toISOString();
73
+ const newProfile: Profile = {
74
+ id: id.trim(),
75
+ name: name.trim(),
76
+ emoji: emoji || '⚙️',
77
+ description: description?.trim() || undefined,
78
+ createdAt: now,
79
+ updatedAt: now,
80
+ };
81
+
82
+ const index = await readProfileIndex();
83
+ index.profiles.push(newProfile);
84
+ await writeProfileIndex(index);
85
+
86
+ if (config && typeof config === 'object') {
87
+ const profileConfig: ProfileConfig = {
88
+ agents: config.agents || {},
89
+ categories: config.categories,
90
+ };
91
+ await writeProfileConfig(id, profileConfig);
92
+ } else {
93
+ await writeProfileConfig(id, { agents: {} });
94
+ }
95
+
96
+ return NextResponse.json(
97
+ { profile: newProfile },
98
+ { status: 201 }
99
+ );
100
+ } catch (error) {
101
+ console.error('Error creating profile:', error);
102
+ return NextResponse.json(
103
+ { error: 'Internal server error' },
104
+ { status: 500 }
105
+ );
106
+ }
107
+ }
@@ -0,0 +1,35 @@
1
+ import { discoverOpencodePorts } from '@/lib/opencodeDiscovery';
2
+
3
+ export async function POST(_: Request, { params }: { params: Promise<{ id: string }> }) {
4
+ const { id: sessionId } = await params;
5
+ const ports = discoverOpencodePorts();
6
+ if (!ports.length) {
7
+ return Response.json(
8
+ { error: 'OpenCode server not found' },
9
+ { status: 503 }
10
+ );
11
+ }
12
+ for (const port of ports) {
13
+ try {
14
+ const baseUrl = `http://localhost:${port}`;
15
+ const response = await fetch(`${baseUrl}/session/${sessionId}`, {
16
+ method: 'PATCH',
17
+ headers: {
18
+ 'Content-Type': 'application/json'
19
+ },
20
+ body: JSON.stringify({ time: { archived: Date.now() } })
21
+ });
22
+ if (response.ok) {
23
+ return Response.json({ success: true });
24
+ }
25
+ console.error(`Failed to archive session on port ${port}:`, await response.text());
26
+ } catch (error) {
27
+ console.error(`Failed to archive session on port ${port}:`, error);
28
+ }
29
+ }
30
+
31
+ return Response.json(
32
+ { error: 'Session not found' },
33
+ { status: 404 }
34
+ );
35
+ }
@@ -0,0 +1,26 @@
1
+ import { createOpencodeClient } from '@opencode-ai/sdk';
2
+ import { discoverOpencodePorts } from '@/lib/opencodeDiscovery';
3
+
4
+ export async function POST(_: Request, { params }: { params: Promise<{ id: string }> }) {
5
+ const { id: sessionId } = await params;
6
+ const ports = discoverOpencodePorts();
7
+ if (!ports.length) {
8
+ return Response.json(
9
+ { error: 'OpenCode server not found' },
10
+ { status: 503 }
11
+ );
12
+ }
13
+ for (const port of ports) {
14
+ try {
15
+ const client = createOpencodeClient({ baseUrl: `http://localhost:${port}` });
16
+ await client.session.delete({ path: { id: sessionId } });
17
+ return Response.json({ success: true });
18
+ } catch {
19
+ }
20
+ }
21
+
22
+ return Response.json(
23
+ { error: 'Session not found' },
24
+ { status: 404 }
25
+ );
26
+ }
@@ -0,0 +1,45 @@
1
+ import { createOpencodeClient } from '@opencode-ai/sdk';
2
+ import { discoverOpencodePorts } from '@/lib/opencodeDiscovery';
3
+
4
+ export async function GET(
5
+ _: Request,
6
+ { params }: { params: Promise<{ id: string }> }
7
+ ) {
8
+ const ports = discoverOpencodePorts();
9
+
10
+ if (!ports.length) {
11
+ return Response.json(
12
+ {
13
+ error: 'OpenCode server not found',
14
+ hint: 'Make sure OpenCode is running with an exposed API port. Example: opencode --port <PORT> (VibePulse auto-detects active ports).'
15
+ },
16
+ { status: 503 }
17
+ );
18
+ }
19
+
20
+ try {
21
+ const { id } = await params;
22
+ for (const port of ports) {
23
+ try {
24
+ const client = createOpencodeClient({ baseUrl: `http://localhost:${port}` });
25
+ const result = await client.session.get({ path: { id } });
26
+ if (result.data) {
27
+ return Response.json({ session: result.data });
28
+ }
29
+ } catch {
30
+ // Try next port
31
+ }
32
+ }
33
+ return Response.json({ error: 'Session not found' }, { status: 404 });
34
+ } catch (error) {
35
+ console.error('Error fetching session:', error);
36
+ return Response.json(
37
+ {
38
+ error: 'Failed to fetch session',
39
+ details: error instanceof Error ? error.message : String(error),
40
+ hint: 'Make sure OpenCode is running with an exposed API port. Example: opencode --port <PORT> (VibePulse auto-detects active ports).'
41
+ },
42
+ { status: 500 }
43
+ );
44
+ }
45
+ }