claude-ws 0.4.8 → 0.4.9-beta.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.
- package/eslint.config.mjs +14 -0
- package/next.config.ts +8 -1
- package/package.json +11 -4
- package/packages/agentic-sdk/src/routes/projects/route.ts +2 -2
- package/packages/agentic-sdk/src/services/agent-factory/upload-analysis-and-import.ts +20 -7
- package/packages/agentic-sdk/src/services/agent-factory/upload-filesystem-helpers.ts +98 -15
- package/packages/agentic-sdk/src/services/attempt/attempt-creation-orchestrator.ts +10 -6
- package/packages/agentic-sdk/src/services/force-create-project-and-task.ts +4 -1
- package/packages/agentic-sdk/src/services/project/project-crud.ts +1 -1
- package/scripts/build-test-pm2.sh +78 -0
- package/server.ts +24 -6
- package/src/app/[locale]/page.tsx +40 -7
- package/src/app/api/agent-factory/projects/[projectId]/sync/route.ts +81 -1
- package/src/app/api/attempts/route.ts +4 -0
- package/src/app/api/projects/route.ts +4 -2
- package/src/app/api/sync/minio/pull/route.ts +130 -0
- package/src/app/api/sync/minio/push/route.ts +131 -0
- package/src/app/api/tasks/route.ts +3 -2
- package/src/components/header/project-selector.tsx +1 -1
- package/src/components/kanban/create-task-dialog.tsx +7 -1
- package/src/components/ui/popover.tsx +1 -1
- package/src/hooks/template/CLAUDE.template.md +20 -0
- package/src/hooks/template/hooks/.env.example +10 -0
- package/src/hooks/template/hooks/minio-pull-sync.ts +270 -0
- package/src/hooks/template/hooks/minio-push-sync.ts +219 -0
- package/src/hooks/template/settings.json +28 -0
- package/src/lib/agent-manager.ts +1 -1
- package/src/lib/api-hook-url.ts +74 -0
- package/src/lib/minio-pull-queue.ts +1110 -0
- package/src/lib/minio-push-queue.ts +939 -0
- package/src/lib/project-utils.ts +119 -0
- package/src/stores/project-store-crud-api-actions.ts +1 -1
- package/src/stores/project-store.ts +1 -1
package/eslint.config.mjs
CHANGED
|
@@ -5,6 +5,20 @@ import nextTs from "eslint-config-next/typescript";
|
|
|
5
5
|
const eslintConfig = defineConfig([
|
|
6
6
|
...nextVitals,
|
|
7
7
|
...nextTs,
|
|
8
|
+
{
|
|
9
|
+
rules: {
|
|
10
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
11
|
+
"@typescript-eslint/no-require-imports": "off",
|
|
12
|
+
"react-hooks/preserve-manual-memoization": "off",
|
|
13
|
+
"react-hooks/refs": "off",
|
|
14
|
+
"react-hooks/set-state-in-effect": "off",
|
|
15
|
+
"react-hooks/immutability": "off",
|
|
16
|
+
"react-hooks/purity": "off",
|
|
17
|
+
"react-hooks/static-components": "off",
|
|
18
|
+
"@next/next/no-html-link-for-pages": "off",
|
|
19
|
+
"prefer-const": "off",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
8
22
|
// Override default ignores of eslint-config-next.
|
|
9
23
|
globalIgnores([
|
|
10
24
|
// Default ignores of eslint-config-next:
|
package/next.config.ts
CHANGED
|
@@ -13,7 +13,14 @@ const nextConfig: NextConfig = {
|
|
|
13
13
|
// Enable gzip compression for responses
|
|
14
14
|
compress: true,
|
|
15
15
|
// Transpile xterm packages for proper CSS/ESM handling
|
|
16
|
-
transpilePackages: [
|
|
16
|
+
transpilePackages: [
|
|
17
|
+
'@xterm/xterm', '@xterm/addon-fit', '@xterm/addon-web-links', '@pierre/diffs',
|
|
18
|
+
// Force Turbopack to compile these as a single bundle to avoid
|
|
19
|
+
// "module factory not available" HMR errors with shared @lezer/common
|
|
20
|
+
'@codemirror/state', '@codemirror/view', '@codemirror/language',
|
|
21
|
+
'@lezer/common', '@lezer/highlight', '@lezer/lr',
|
|
22
|
+
'@uiw/react-codemirror',
|
|
23
|
+
],
|
|
17
24
|
outputFileTracingRoot: path.join(__dirname),
|
|
18
25
|
outputFileTracingIncludes: {
|
|
19
26
|
'/': ['./src/**/*'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-ws",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9-beta.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "AI-powered workspace for solo CEOs and indie builders — manage your entire business with AI agents, not just code. Kanban board, code editor, Git integration, claw agent hub, local-first SQLite.",
|
|
6
6
|
"keywords": [
|
|
@@ -66,6 +66,9 @@
|
|
|
66
66
|
"pnpm": ">=9.0.0"
|
|
67
67
|
},
|
|
68
68
|
"pnpm": {
|
|
69
|
+
"overrides": {
|
|
70
|
+
"axios": "0.30.3"
|
|
71
|
+
},
|
|
69
72
|
"onlyBuiltDependencies": [
|
|
70
73
|
"@parcel/watcher",
|
|
71
74
|
"@swc/core",
|
|
@@ -81,6 +84,11 @@
|
|
|
81
84
|
"scripts": {
|
|
82
85
|
"postinstall": "node scripts/postinstall-optional-native-deps.js",
|
|
83
86
|
"dev": "cross-env CLAUDECODE= tsx server.ts",
|
|
87
|
+
"pm2": "pm2 start ecosystem.config.js && pm2 save",
|
|
88
|
+
"pm2-restart": "pm2 restart claudews",
|
|
89
|
+
"pm2-stop": "pm2 stop claudews",
|
|
90
|
+
"pm2-logs": "pm2 logs claudews",
|
|
91
|
+
"pm2-monit": "pm2 monit",
|
|
84
92
|
"build": "cross-env NODE_ENV=production next build",
|
|
85
93
|
"start": "cross-env NODE_ENV=production tsx server.ts",
|
|
86
94
|
"db:generate": "drizzle-kit generate",
|
|
@@ -119,6 +127,7 @@
|
|
|
119
127
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
120
128
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
121
129
|
"@radix-ui/react-label": "^2.1.8",
|
|
130
|
+
"@radix-ui/react-popover": "^1.1.15",
|
|
122
131
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
123
132
|
"@radix-ui/react-select": "^2.2.6",
|
|
124
133
|
"@radix-ui/react-separator": "^1.1.8",
|
|
@@ -130,6 +139,7 @@
|
|
|
130
139
|
"@tailwindcss/postcss": "^4.2.2",
|
|
131
140
|
"@types/adm-zip": "^0.5.8",
|
|
132
141
|
"@types/better-sqlite3": "^7.6.13",
|
|
142
|
+
"@types/js-yaml": "^4.0.9",
|
|
133
143
|
"@types/compression": "^1.8.1",
|
|
134
144
|
"@types/dompurify": "^3.2.0",
|
|
135
145
|
"@types/node": "^20.19.37",
|
|
@@ -191,8 +201,5 @@
|
|
|
191
201
|
"optionalDependencies": {
|
|
192
202
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
|
193
203
|
"node-pty": "^1.1.0"
|
|
194
|
-
},
|
|
195
|
-
"devDependencies": {
|
|
196
|
-
"@types/js-yaml": "^4.0.9"
|
|
197
204
|
}
|
|
198
205
|
}
|
|
@@ -12,8 +12,8 @@ export default async function projectsRoute(fastify: FastifyInstance) {
|
|
|
12
12
|
|
|
13
13
|
fastify.post('/api/projects', async (request, reply) => {
|
|
14
14
|
try {
|
|
15
|
-
const { name, path: projectPath } = request.body as any;
|
|
16
|
-
const project = await fastify.services.project.createProject({ name, path: projectPath });
|
|
15
|
+
const { id, projectId, name, path: projectPath } = request.body as any;
|
|
16
|
+
const project = await fastify.services.project.createProject({ id: projectId || id, name, path: projectPath });
|
|
17
17
|
return reply.code(201).send(project);
|
|
18
18
|
} catch (error: any) {
|
|
19
19
|
if (error instanceof ProjectValidationError) {
|
|
@@ -37,7 +37,12 @@ export async function analyzeForPreview(extractDir: string, agentFactoryDir: str
|
|
|
37
37
|
|
|
38
38
|
if (entry.isDirectory()) {
|
|
39
39
|
if (entry.name === 'skills' || entry.name === 'commands' || entry.name === 'agents') {
|
|
40
|
-
|
|
40
|
+
const componentType = entry.name === 'skills'
|
|
41
|
+
? 'skill'
|
|
42
|
+
: entry.name === 'commands'
|
|
43
|
+
? 'command'
|
|
44
|
+
: 'agent';
|
|
45
|
+
await previewDirectoryContents(entryPath, agentFactoryDir, items, componentType);
|
|
41
46
|
} else {
|
|
42
47
|
const subEntries = await readdir(entryPath, { withFileTypes: true });
|
|
43
48
|
const hasSubdirs = subEntries.some(e =>
|
|
@@ -110,8 +115,12 @@ export async function analyzeAndOrganize(extractDir: string, agentFactoryDir: st
|
|
|
110
115
|
|
|
111
116
|
if (entry.isDirectory()) {
|
|
112
117
|
if (entry.name === 'skills' || entry.name === 'commands' || entry.name === 'agents') {
|
|
113
|
-
const
|
|
114
|
-
|
|
118
|
+
const componentType = entry.name === 'skills'
|
|
119
|
+
? 'skill'
|
|
120
|
+
: entry.name === 'commands'
|
|
121
|
+
? 'command'
|
|
122
|
+
: 'agent';
|
|
123
|
+
await moveDirectoryContents(entryPath, agentFactoryDir, items, componentType);
|
|
115
124
|
} else {
|
|
116
125
|
const subEntries = await readdir(entryPath, { withFileTypes: true });
|
|
117
126
|
const hasSubdirs = subEntries.some(e =>
|
|
@@ -165,11 +174,11 @@ export async function importFromSession(
|
|
|
165
174
|
if (item.type === 'agent_set') {
|
|
166
175
|
targetPath = join(targetBaseDir, 'agent-sets', item.name);
|
|
167
176
|
} else if (item.type === 'skill') {
|
|
168
|
-
targetPath = join(targetBaseDir, 'skills', item.name, 'SKILL.md');
|
|
177
|
+
targetPath = join(targetBaseDir, 'skills', item.relativePath || item.name, 'SKILL.md');
|
|
169
178
|
} else if (item.type === 'agent') {
|
|
170
|
-
targetPath = join(targetBaseDir, 'agents', `${item.name}.md`);
|
|
179
|
+
targetPath = join(targetBaseDir, 'agents', item.relativePath || `${item.name}.md`);
|
|
171
180
|
} else {
|
|
172
|
-
targetPath = join(targetBaseDir, 'commands', `${item.name}.md`);
|
|
181
|
+
targetPath = join(targetBaseDir, 'commands', item.relativePath || `${item.name}.md`);
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
if (item.type === 'agent_set') {
|
|
@@ -207,7 +216,11 @@ export async function importFromSession(
|
|
|
207
216
|
description = extractDescriptionFromMarkdown(content);
|
|
208
217
|
} catch { /* ignore */ }
|
|
209
218
|
|
|
210
|
-
const componentType = item.type === '
|
|
219
|
+
const componentType = item.type === 'skill'
|
|
220
|
+
? 'skill'
|
|
221
|
+
: item.type === 'agent'
|
|
222
|
+
? 'agent'
|
|
223
|
+
: 'command';
|
|
211
224
|
await registryService.upsertPlugin(item.name, componentType, {
|
|
212
225
|
description,
|
|
213
226
|
sourcePath: targetPath,
|
|
@@ -11,6 +11,8 @@ export interface ExtractedItem {
|
|
|
11
11
|
sourcePath: string;
|
|
12
12
|
targetPath: string;
|
|
13
13
|
name: string;
|
|
14
|
+
// Relative path under skills/commands/agents root. Used to preserve command namespaces.
|
|
15
|
+
relativePath?: string;
|
|
14
16
|
componentCount?: number;
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -71,9 +73,10 @@ export async function moveDirectory(source: string, target: string): Promise<voi
|
|
|
71
73
|
|
|
72
74
|
export async function moveDirectoryContents(
|
|
73
75
|
sourceDir: string,
|
|
74
|
-
|
|
76
|
+
targetBaseDir: string,
|
|
75
77
|
items: ExtractedItem[],
|
|
76
|
-
type: 'skill' | 'command' | 'agent'
|
|
78
|
+
type: 'skill' | 'command' | 'agent',
|
|
79
|
+
relativePrefix = ''
|
|
77
80
|
): Promise<void> {
|
|
78
81
|
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
79
82
|
|
|
@@ -81,15 +84,57 @@ export async function moveDirectoryContents(
|
|
|
81
84
|
if (entry.name.startsWith('.')) continue;
|
|
82
85
|
|
|
83
86
|
const sourcePath = join(sourceDir, entry.name);
|
|
84
|
-
const
|
|
87
|
+
const relPath = relativePrefix ? join(relativePrefix, entry.name) : entry.name;
|
|
88
|
+
|
|
89
|
+
if (type === 'skill') {
|
|
90
|
+
if (!entry.isDirectory()) continue;
|
|
91
|
+
const skillMdPath = join(sourcePath, 'SKILL.md');
|
|
92
|
+
if (existsSync(skillMdPath)) {
|
|
93
|
+
const targetPath = join(targetBaseDir, 'skills', relPath);
|
|
94
|
+
await moveDirectory(sourcePath, targetPath);
|
|
95
|
+
items.push({
|
|
96
|
+
type: 'skill',
|
|
97
|
+
sourcePath,
|
|
98
|
+
targetPath: join(targetPath, 'SKILL.md'),
|
|
99
|
+
name: relPath,
|
|
100
|
+
relativePath: relPath,
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
await moveDirectoryContents(sourcePath, targetBaseDir, items, type, relPath);
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
85
107
|
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
108
|
+
if (type === 'command') {
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
await moveDirectoryContents(sourcePath, targetBaseDir, items, type, relPath);
|
|
111
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
112
|
+
const targetPath = join(targetBaseDir, 'commands', relPath);
|
|
113
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
114
|
+
await copyFile(sourcePath, targetPath);
|
|
115
|
+
items.push({
|
|
116
|
+
type: 'command',
|
|
117
|
+
sourcePath,
|
|
118
|
+
targetPath,
|
|
119
|
+
name: relPath.replace(/\.md$/i, '').split('/').join(':'),
|
|
120
|
+
relativePath: relPath,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Agents: only allow markdown files directly under agents/ (no subdirectories)
|
|
127
|
+
if (!relativePrefix && entry.isFile() && entry.name.endsWith('.md')) {
|
|
128
|
+
const targetPath = join(targetBaseDir, 'agents', entry.name);
|
|
90
129
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
91
130
|
await copyFile(sourcePath, targetPath);
|
|
92
|
-
items.push({
|
|
131
|
+
items.push({
|
|
132
|
+
type: 'agent',
|
|
133
|
+
sourcePath,
|
|
134
|
+
targetPath,
|
|
135
|
+
name: basename(entry.name, '.md'),
|
|
136
|
+
relativePath: entry.name,
|
|
137
|
+
});
|
|
93
138
|
}
|
|
94
139
|
}
|
|
95
140
|
}
|
|
@@ -173,9 +218,10 @@ export async function previewDirectory(
|
|
|
173
218
|
|
|
174
219
|
export async function previewDirectoryContents(
|
|
175
220
|
sourceDir: string,
|
|
176
|
-
|
|
221
|
+
targetBaseDir: string,
|
|
177
222
|
items: ExtractedItem[],
|
|
178
|
-
type: 'skill' | 'command' | 'agent'
|
|
223
|
+
type: 'skill' | 'command' | 'agent',
|
|
224
|
+
relativePrefix = ''
|
|
179
225
|
): Promise<void> {
|
|
180
226
|
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
181
227
|
|
|
@@ -183,12 +229,49 @@ export async function previewDirectoryContents(
|
|
|
183
229
|
if (entry.name.startsWith('.')) continue;
|
|
184
230
|
|
|
185
231
|
const sourcePath = join(sourceDir, entry.name);
|
|
186
|
-
const
|
|
232
|
+
const relPath = relativePrefix ? join(relativePrefix, entry.name) : entry.name;
|
|
233
|
+
|
|
234
|
+
if (type === 'skill') {
|
|
235
|
+
if (!entry.isDirectory()) continue;
|
|
236
|
+
const skillMdPath = join(sourcePath, 'SKILL.md');
|
|
237
|
+
if (existsSync(skillMdPath)) {
|
|
238
|
+
items.push({
|
|
239
|
+
type: 'skill',
|
|
240
|
+
sourcePath,
|
|
241
|
+
targetPath: join(targetBaseDir, 'skills', relPath, 'SKILL.md'),
|
|
242
|
+
name: relPath,
|
|
243
|
+
relativePath: relPath,
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
await previewDirectoryContents(sourcePath, targetBaseDir, items, type, relPath);
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
187
250
|
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
251
|
+
if (type === 'command') {
|
|
252
|
+
if (entry.isDirectory()) {
|
|
253
|
+
await previewDirectoryContents(sourcePath, targetBaseDir, items, type, relPath);
|
|
254
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
255
|
+
items.push({
|
|
256
|
+
type: 'command',
|
|
257
|
+
sourcePath,
|
|
258
|
+
targetPath: join(targetBaseDir, 'commands', relPath),
|
|
259
|
+
name: relPath.replace(/\.md$/i, '').split('/').join(':'),
|
|
260
|
+
relativePath: relPath,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Agents: only allow markdown files directly under agents/ (no subdirectories)
|
|
267
|
+
if (!relativePrefix && entry.isFile() && entry.name.endsWith('.md')) {
|
|
268
|
+
items.push({
|
|
269
|
+
type: 'agent',
|
|
270
|
+
sourcePath,
|
|
271
|
+
targetPath: join(targetBaseDir, 'agents', entry.name),
|
|
272
|
+
name: basename(entry.name, '.md'),
|
|
273
|
+
relativePath: entry.name,
|
|
274
|
+
});
|
|
192
275
|
}
|
|
193
276
|
}
|
|
194
277
|
}
|
|
@@ -67,10 +67,11 @@ interface OrchestratorDeps {
|
|
|
67
67
|
/** Callback to start the agent process (runtime singleton, lives outside SDK) */
|
|
68
68
|
startAgent: (params: AgentStartParams) => void;
|
|
69
69
|
defaultBasePath: string;
|
|
70
|
+
onProjectForceCreated?: (project: any) => Promise<void> | void;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
export function createAttemptOrchestrator(deps: OrchestratorDeps) {
|
|
73
|
-
const { taskService, projectService, attemptService, forceCreateService, sessionManager, startAgent, defaultBasePath } = deps;
|
|
74
|
+
const { taskService, projectService, attemptService, forceCreateService, sessionManager, startAgent, defaultBasePath, onProjectForceCreated } = deps;
|
|
74
75
|
|
|
75
76
|
return {
|
|
76
77
|
/**
|
|
@@ -91,11 +92,14 @@ export function createAttemptOrchestrator(deps: OrchestratorDeps) {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
// Resolve task (lookup or force-create)
|
|
94
|
-
const task = await this.resolveTask(input);
|
|
95
|
+
const { task, projectCreatedByForceCreate } = await this.resolveTask(input);
|
|
95
96
|
|
|
96
97
|
// Resolve project
|
|
97
98
|
const project = await projectService.getById(task.projectId);
|
|
98
99
|
if (!project) throw new AttemptValidationError('Project not found', 404);
|
|
100
|
+
if (projectCreatedByForceCreate && onProjectForceCreated) {
|
|
101
|
+
await onProjectForceCreated(project);
|
|
102
|
+
}
|
|
99
103
|
|
|
100
104
|
// Create attempt record
|
|
101
105
|
const attempt = await attemptService.create({
|
|
@@ -166,18 +170,18 @@ export function createAttemptOrchestrator(deps: OrchestratorDeps) {
|
|
|
166
170
|
},
|
|
167
171
|
|
|
168
172
|
/** Resolve task: find existing or force-create project+task */
|
|
169
|
-
async resolveTask(input: AttemptCreationInput): Promise<any> {
|
|
173
|
+
async resolveTask(input: AttemptCreationInput): Promise<{ task: any; projectCreatedByForceCreate: boolean }> {
|
|
170
174
|
const { taskId, force_create, projectId, projectName, taskTitle, projectRootPath } = input;
|
|
171
175
|
|
|
172
176
|
if (!force_create) {
|
|
173
177
|
const task = await taskService.getById(taskId);
|
|
174
178
|
if (!task) throw new AttemptValidationError('Task not found', 404);
|
|
175
|
-
return task;
|
|
179
|
+
return { task, projectCreatedByForceCreate: false };
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
// Force-create: check if task already exists
|
|
179
183
|
const existingTask = await taskService.getById(taskId);
|
|
180
|
-
if (existingTask) return existingTask;
|
|
184
|
+
if (existingTask) return { task: existingTask, projectCreatedByForceCreate: false };
|
|
181
185
|
|
|
182
186
|
// Task doesn't exist — force-create project + task
|
|
183
187
|
if (!projectId) throw new AttemptValidationError('projectId required', 400);
|
|
@@ -185,7 +189,7 @@ export function createAttemptOrchestrator(deps: OrchestratorDeps) {
|
|
|
185
189
|
const result = await forceCreateService.ensureProjectAndTask({
|
|
186
190
|
taskId, projectId, projectName, taskTitle, projectRootPath, defaultBasePath,
|
|
187
191
|
});
|
|
188
|
-
return result.task;
|
|
192
|
+
return { task: result.task, projectCreatedByForceCreate: Boolean(result.projectCreated) };
|
|
189
193
|
},
|
|
190
194
|
|
|
191
195
|
/** Poll attempt status until terminal state or timeout */
|
|
@@ -31,6 +31,7 @@ export interface ForceCreateParams {
|
|
|
31
31
|
export interface ForceCreateResult {
|
|
32
32
|
task: typeof schema.tasks.$inferSelect;
|
|
33
33
|
project: typeof schema.projects.$inferSelect;
|
|
34
|
+
projectCreated: boolean;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export function createForceCreateService(db: any) {
|
|
@@ -42,6 +43,7 @@ export function createForceCreateService(db: any) {
|
|
|
42
43
|
*/
|
|
43
44
|
async ensureProjectAndTask(params: ForceCreateParams): Promise<ForceCreateResult> {
|
|
44
45
|
const { taskId, projectId, projectName, taskTitle, projectRootPath, defaultBasePath } = params;
|
|
46
|
+
let projectCreated = false;
|
|
45
47
|
|
|
46
48
|
// Check if project exists
|
|
47
49
|
let project = await db.select().from(schema.projects)
|
|
@@ -79,6 +81,7 @@ export function createForceCreateService(db: any) {
|
|
|
79
81
|
path: projectPath,
|
|
80
82
|
createdAt: Date.now(),
|
|
81
83
|
});
|
|
84
|
+
projectCreated = true;
|
|
82
85
|
|
|
83
86
|
project = await db.select().from(schema.projects)
|
|
84
87
|
.where(eq(schema.projects.id, projectId)).get();
|
|
@@ -114,7 +117,7 @@ export function createForceCreateService(db: any) {
|
|
|
114
117
|
const task = await db.select().from(schema.tasks)
|
|
115
118
|
.where(eq(schema.tasks.id, taskId)).get();
|
|
116
119
|
|
|
117
|
-
return { task, project };
|
|
120
|
+
return { task, project, projectCreated };
|
|
118
121
|
},
|
|
119
122
|
};
|
|
120
123
|
}
|
|
@@ -97,7 +97,7 @@ export function createProjectService(db: any) {
|
|
|
97
97
|
},
|
|
98
98
|
|
|
99
99
|
/** Validate + setup directory + create project. Throws ProjectValidationError on constraint violation. */
|
|
100
|
-
async createProject(data: { name: string; path: string }) {
|
|
100
|
+
async createProject(data: { id?: string; name: string; path: string }) {
|
|
101
101
|
if (!data.name || !data.path) throw new ProjectValidationError('Name and path are required', 400);
|
|
102
102
|
try {
|
|
103
103
|
await this.setupProjectDirectory(data.path);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
cd "$ROOT_DIR"
|
|
6
|
+
|
|
7
|
+
APP_NAME="${PM2_APP_NAME:-claudews}"
|
|
8
|
+
ECOSYSTEM_FILE="${PM2_ECOSYSTEM_FILE:-ecosystem.config.js}"
|
|
9
|
+
BUILD_CMD="${BUILD_CMD:-pnpm build}"
|
|
10
|
+
|
|
11
|
+
# Resolve health URL from env override or fallback to localhost:PORT/api/tunnel/status
|
|
12
|
+
if [[ -n "${HEALTH_URL:-}" ]]; then
|
|
13
|
+
HEALTH_URL="$HEALTH_URL"
|
|
14
|
+
else
|
|
15
|
+
PORT_VALUE="${PORT:-8556}"
|
|
16
|
+
if [[ -f .env ]]; then
|
|
17
|
+
ENV_PORT="$(grep -E '^PORT=' .env | tail -n1 | cut -d'=' -f2- | tr -d '"' | tr -d "'" || true)"
|
|
18
|
+
if [[ -n "$ENV_PORT" ]]; then
|
|
19
|
+
PORT_VALUE="$ENV_PORT"
|
|
20
|
+
fi
|
|
21
|
+
fi
|
|
22
|
+
HEALTH_URL="http://127.0.0.1:${PORT_VALUE}/api/tunnel/status"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
RETRY_COUNT="${HEALTH_RETRY_COUNT:-20}"
|
|
26
|
+
RETRY_DELAY_SECONDS="${HEALTH_RETRY_DELAY_SECONDS:-2}"
|
|
27
|
+
|
|
28
|
+
need_cmd() {
|
|
29
|
+
if ! command -v "$1" >/dev/null 2>&1; then
|
|
30
|
+
echo "❌ Missing required command: $1"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
need_cmd pnpm
|
|
36
|
+
need_cmd pm2
|
|
37
|
+
need_cmd curl
|
|
38
|
+
|
|
39
|
+
echo "📦 Building project..."
|
|
40
|
+
$BUILD_CMD
|
|
41
|
+
|
|
42
|
+
echo "🚀 Starting/Restarting PM2 app: $APP_NAME"
|
|
43
|
+
if pm2 describe "$APP_NAME" >/dev/null 2>&1; then
|
|
44
|
+
pm2 restart "$APP_NAME" --update-env
|
|
45
|
+
else
|
|
46
|
+
if [[ ! -f "$ECOSYSTEM_FILE" ]]; then
|
|
47
|
+
echo "❌ Ecosystem file not found: $ECOSYSTEM_FILE"
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Prefer starting the specific app in ecosystem; fallback to starting entire file.
|
|
52
|
+
if ! pm2 start "$ECOSYSTEM_FILE" --only "$APP_NAME"; then
|
|
53
|
+
pm2 start "$ECOSYSTEM_FILE"
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
pm2 save >/dev/null
|
|
58
|
+
|
|
59
|
+
echo "🩺 Health check: $HEALTH_URL"
|
|
60
|
+
attempt=1
|
|
61
|
+
while (( attempt <= RETRY_COUNT )); do
|
|
62
|
+
if curl -fsS --max-time 8 "$HEALTH_URL" >/dev/null; then
|
|
63
|
+
echo "✅ Health check passed (attempt $attempt/$RETRY_COUNT)"
|
|
64
|
+
pm2 status "$APP_NAME"
|
|
65
|
+
echo "✅ PM2 build + test completed"
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
echo "⏳ Waiting for service... ($attempt/$RETRY_COUNT)"
|
|
70
|
+
sleep "$RETRY_DELAY_SECONDS"
|
|
71
|
+
((attempt++))
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
echo "❌ Health check failed after $RETRY_COUNT attempts"
|
|
75
|
+
pm2 status "$APP_NAME" || true
|
|
76
|
+
echo "--- Last 120 lines of PM2 logs for $APP_NAME ---"
|
|
77
|
+
pm2 logs "$APP_NAME" --lines 120 --nostream || true
|
|
78
|
+
exit 1
|
package/server.ts
CHANGED
|
@@ -52,6 +52,8 @@ import { usageTracker } from './src/lib/usage-tracker';
|
|
|
52
52
|
import { workflowTracker } from './src/lib/workflow-tracker';
|
|
53
53
|
import { gitStatsCache } from './src/lib/git-stats-collector';
|
|
54
54
|
import { tunnelService } from './src/lib/tunnel-service';
|
|
55
|
+
import { getMinioPullQueueWorker } from './src/lib/minio-pull-queue';
|
|
56
|
+
import { getMinioPushQueueWorker } from './src/lib/minio-push-queue';
|
|
55
57
|
import { createAutopilotManager, appendQuestionAnswer, appendSubagentEnded, appendTrackedTaskUpdate } from './src/lib/autopilot';
|
|
56
58
|
import type { AutopilotMode } from './src/lib/autopilot';
|
|
57
59
|
|
|
@@ -63,6 +65,8 @@ const port = getPort();
|
|
|
63
65
|
|
|
64
66
|
const app = next({ dev, hostname, port, turbopack: false });
|
|
65
67
|
const handle = app.getRequestHandler();
|
|
68
|
+
const minioPullQueueWorker = getMinioPullQueueWorker();
|
|
69
|
+
const minioPushQueueWorker = getMinioPushQueueWorker();
|
|
66
70
|
|
|
67
71
|
app.prepare().then(async () => {
|
|
68
72
|
const httpServer = createServer((req, res) => {
|
|
@@ -224,14 +228,18 @@ app.prepare().then(async () => {
|
|
|
224
228
|
const { mkdir } = await import('fs/promises');
|
|
225
229
|
const { join } = await import('path');
|
|
226
230
|
|
|
227
|
-
const projectDirName = projectId
|
|
228
|
-
|
|
231
|
+
const projectDirName = `${projectId}-${projectName}`;
|
|
232
|
+
log.info({ projectRootPath, userCwd, projectDirName }, '[Socket] Debug project path creation');
|
|
233
|
+
const projectPath = (projectRootPath && projectRootPath.trim() !== '')
|
|
229
234
|
? join(projectRootPath, projectDirName)
|
|
230
235
|
: join(userCwd, 'data', 'projects', projectDirName);
|
|
236
|
+
log.info({ projectPath, projectRootPath, userCwd }, '[Socket] Final project path');
|
|
231
237
|
|
|
232
238
|
try {
|
|
239
|
+
const { setupProjectDefaults } = await import('./src/lib/project-utils');
|
|
233
240
|
await mkdir(projectPath, { recursive: true });
|
|
234
|
-
|
|
241
|
+
await setupProjectDefaults(projectPath, projectId);
|
|
242
|
+
log.info({ projectPath, projectId }, '[Socket] Created project directory and defaults');
|
|
235
243
|
} catch (mkdirError: any) {
|
|
236
244
|
if (mkdirError?.code !== 'EEXIST') {
|
|
237
245
|
log.error({ mkdirError }, '[Socket] Failed to create project folder');
|
|
@@ -615,7 +623,7 @@ app.prepare().then(async () => {
|
|
|
615
623
|
|
|
616
624
|
log.warn(`[Server] Auto-retried answer for ${attemptId} as new attempt ${newAttemptId}`);
|
|
617
625
|
} catch (error) {
|
|
618
|
-
log.error({error}
|
|
626
|
+
log.error({ error }, `[Server] Auto-retry failed for ${attemptId}:`);
|
|
619
627
|
socket.emit('error', { message: 'Auto-retry failed: ' + (error instanceof Error ? error.message : 'Unknown error') });
|
|
620
628
|
}
|
|
621
629
|
}
|
|
@@ -1632,9 +1640,9 @@ app.prepare().then(async () => {
|
|
|
1632
1640
|
taskTitle: task?.title || 'Unknown',
|
|
1633
1641
|
summary: expanded.summary,
|
|
1634
1642
|
});
|
|
1635
|
-
}).catch(() => {});
|
|
1643
|
+
}).catch(() => { });
|
|
1636
1644
|
}
|
|
1637
|
-
}).catch(() => {});
|
|
1645
|
+
}).catch(() => { });
|
|
1638
1646
|
}
|
|
1639
1647
|
});
|
|
1640
1648
|
|
|
@@ -1828,6 +1836,9 @@ app.prepare().then(async () => {
|
|
|
1828
1836
|
httpServer.listen(port, () => {
|
|
1829
1837
|
log.info(`> Ready on http://${hostname}:${port}`);
|
|
1830
1838
|
|
|
1839
|
+
// Start MinIO sync queue workers with server lifecycle
|
|
1840
|
+
minioPullQueueWorker.start();
|
|
1841
|
+
minioPushQueueWorker.start();
|
|
1831
1842
|
if (!process.env.NEXT_PUBLIC_URL) {
|
|
1832
1843
|
log.warn('[Server] NEXT_PUBLIC_URL is not set. Access is limited to localhost only. Set NEXT_PUBLIC_URL in .env to enable remote access.');
|
|
1833
1844
|
}
|
|
@@ -1851,6 +1862,13 @@ app.prepare().then(async () => {
|
|
|
1851
1862
|
await tunnelService.stop();
|
|
1852
1863
|
log.info('> Tunnel stopped');
|
|
1853
1864
|
|
|
1865
|
+
// Stop MinIO sync queue workers
|
|
1866
|
+
minioPullQueueWorker.stop();
|
|
1867
|
+
log.info('> MinIO pull queue worker stopped');
|
|
1868
|
+
|
|
1869
|
+
minioPushQueueWorker.stop();
|
|
1870
|
+
log.info('> MinIO push queue worker stopped');
|
|
1871
|
+
|
|
1854
1872
|
// Cancel all Claude agents first
|
|
1855
1873
|
agentManager.cancelAll();
|
|
1856
1874
|
log.info('> Cancelled all Claude agents');
|
|
@@ -3,13 +3,7 @@
|
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import { SearchProvider } from '@/components/search/search-provider';
|
|
5
5
|
import { Header } from '@/components/header';
|
|
6
|
-
import
|
|
7
|
-
import { CreateTaskDialog } from '@/components/kanban/create-task-dialog';
|
|
8
|
-
import { TaskDetailPanel } from '@/components/task/task-detail-panel';
|
|
9
|
-
import { FloatingChatWindowsContainer } from '@/components/task/floating-chat-windows-container';
|
|
10
|
-
import { SettingsPage } from '@/components/settings/settings-page';
|
|
11
|
-
import { SetupDialog } from '@/components/settings/setup-dialog';
|
|
12
|
-
import { SidebarPanel, FileTabsPanel, DiffTabsPanel } from '@/components/sidebar';
|
|
6
|
+
import dynamic from 'next/dynamic';
|
|
13
7
|
import { RightSidebar } from '@/components/right-sidebar';
|
|
14
8
|
import { QuestionsPanel } from '@/components/questions/questions-panel';
|
|
15
9
|
import { TeamView } from '@/components/team-view/team-view';
|
|
@@ -28,6 +22,45 @@ import { useKanbanKeyboardShortcuts } from '@/hooks/use-kanban-keyboard-shortcut
|
|
|
28
22
|
import type { Task } from '@/types';
|
|
29
23
|
import { useTranslations } from 'next-intl';
|
|
30
24
|
|
|
25
|
+
// Lazy-load heavy components to isolate third-party chunks (@dnd-kit, @codemirror, @lezer)
|
|
26
|
+
// and avoid Turbopack HMR "module factory not available" errors from stale browser cache.
|
|
27
|
+
const Board = dynamic(
|
|
28
|
+
() => import('@/components/kanban/board').then(m => ({ default: m.Board })),
|
|
29
|
+
{ ssr: false }
|
|
30
|
+
);
|
|
31
|
+
const CreateTaskDialog = dynamic(
|
|
32
|
+
() => import('@/components/kanban/create-task-dialog').then(m => ({ default: m.CreateTaskDialog })),
|
|
33
|
+
{ ssr: false }
|
|
34
|
+
);
|
|
35
|
+
const TaskDetailPanel = dynamic(
|
|
36
|
+
() => import('@/components/task/task-detail-panel').then(m => ({ default: m.TaskDetailPanel })),
|
|
37
|
+
{ ssr: false }
|
|
38
|
+
);
|
|
39
|
+
const FloatingChatWindowsContainer = dynamic(
|
|
40
|
+
() => import('@/components/task/floating-chat-windows-container').then(m => ({ default: m.FloatingChatWindowsContainer })),
|
|
41
|
+
{ ssr: false }
|
|
42
|
+
);
|
|
43
|
+
const SettingsPage = dynamic(
|
|
44
|
+
() => import('@/components/settings/settings-page').then(m => ({ default: m.SettingsPage })),
|
|
45
|
+
{ ssr: false }
|
|
46
|
+
);
|
|
47
|
+
const SetupDialog = dynamic(
|
|
48
|
+
() => import('@/components/settings/setup-dialog').then(m => ({ default: m.SetupDialog })),
|
|
49
|
+
{ ssr: false }
|
|
50
|
+
);
|
|
51
|
+
const SidebarPanel = dynamic(
|
|
52
|
+
() => import('@/components/sidebar/sidebar-panel').then(m => ({ default: m.SidebarPanel })),
|
|
53
|
+
{ ssr: false }
|
|
54
|
+
);
|
|
55
|
+
const FileTabsPanel = dynamic(
|
|
56
|
+
() => import('@/components/sidebar/file-browser/file-tabs-panel').then(m => ({ default: m.FileTabsPanel })),
|
|
57
|
+
{ ssr: false }
|
|
58
|
+
);
|
|
59
|
+
const DiffTabsPanel = dynamic(
|
|
60
|
+
() => import('@/components/sidebar/git-changes/diff-tabs-panel').then(m => ({ default: m.DiffTabsPanel })),
|
|
61
|
+
{ ssr: false }
|
|
62
|
+
);
|
|
63
|
+
|
|
31
64
|
function KanbanApp() {
|
|
32
65
|
const tCommon = useTranslations('common');
|
|
33
66
|
const [createTaskOpen, setCreateTaskOpen] = useState(false);
|