claude-ws 0.4.9-beta.6 → 0.4.9-beta.8
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/package.json +11 -5
- package/packages/agentic-sdk/README.md +3 -1
- package/packages/agentic-sdk/src/services/attempt/attempt-creation-orchestrator.ts +3 -2
- package/packages/agentic-sdk/src/services/command/slash-command-listing.ts +10 -5
- package/scripts/build-test-pm2.sh +12 -2
- package/scripts/check-dependencies.sh +3 -2
- package/scripts/migrate-all-projects.ts +287 -0
- package/scripts/migrate-existing-projects.ts +23 -9
- package/server.ts +22 -1
- package/src/app/api/attempts/route.ts +3 -2
- package/src/app/api/projects/route.ts +7 -2
- package/src/components/claude/tool-use-block-write-diff-renderer.tsx +24 -0
- package/src/components/claude/tool-use-block.tsx +12 -2
- package/src/components/kanban/create-task-dialog.tsx +17 -2
- package/src/components/sidebar/file-browser/use-file-tab-save-copy-download-operations.ts +4 -0
- package/src/components/sidebar/file-browser/use-file-tab-state.ts +5 -1
- package/src/components/task/file-mention-dropdown.tsx +5 -5
- package/src/components/task/floating-chat-window.tsx +9 -6
- package/src/components/task/prompt-input-form-body.tsx +1 -0
- package/src/components/task/task-detail-panel.tsx +2 -1
- package/src/components/terminal/terminal-local-echo.ts +133 -0
- package/src/components/terminal/use-terminal-lifecycle.ts +11 -3
- package/src/hooks/template/hooks/minio-pull-sync.ts +103 -23
- package/src/hooks/template/hooks/minio-push-sync.ts +86 -26
- package/src/lib/minio-pull-queue.ts +26 -40
- package/src/lib/minio-push-queue.ts +20 -36
- package/src/lib/project-utils.ts +22 -39
- package/src/lib/socket-service.ts +3 -0
- package/src/stores/project-store-crud-api-actions.ts +1 -1
- package/src/stores/terminal-store-socket-session-actions.ts +66 -10
- package/src/stores/terminal-store.ts +21 -4
- package/src/hooks/template/hooks/.env.example +0 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-ws",
|
|
3
|
-
"version": "0.4.9-beta.
|
|
3
|
+
"version": "0.4.9-beta.8",
|
|
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": [
|
|
@@ -67,7 +67,9 @@
|
|
|
67
67
|
},
|
|
68
68
|
"pnpm": {
|
|
69
69
|
"overrides": {
|
|
70
|
-
"axios": "0.30.3"
|
|
70
|
+
"axios": "0.30.3",
|
|
71
|
+
"picomatch@^2.3.1": "2.3.2",
|
|
72
|
+
"picomatch@^4.0.0": "4.0.4"
|
|
71
73
|
},
|
|
72
74
|
"onlyBuiltDependencies": [
|
|
73
75
|
"@parcel/watcher",
|
|
@@ -95,7 +97,10 @@
|
|
|
95
97
|
"db:fix": "tsx scripts/db-fix-columns.ts",
|
|
96
98
|
"db:migrate:health": "tsx scripts/db-migrate-health.ts",
|
|
97
99
|
"projects:migrate:defaults": "tsx scripts/migrate-existing-projects.ts",
|
|
98
|
-
"migrate:sessions": "tsx scripts/migrate-sdk-sessions.ts"
|
|
100
|
+
"migrate:sessions": "tsx scripts/migrate-sdk-sessions.ts",
|
|
101
|
+
"migrate-hook-env": "tsx scripts/migrate-hook-env.ts",
|
|
102
|
+
"migrate-all-projects": "tsx scripts/migrate-all-projects.ts",
|
|
103
|
+
"heal-hooks": "tsx scripts/heal-hook-env.ts"
|
|
99
104
|
},
|
|
100
105
|
"dependencies": {
|
|
101
106
|
"@anthropic-ai/claude-agent-sdk": "^0.2.81",
|
|
@@ -164,7 +169,6 @@
|
|
|
164
169
|
"diff": "^8.0.3",
|
|
165
170
|
"dompurify": "^3.3.3",
|
|
166
171
|
"dotenv": "^17.3.1",
|
|
167
|
-
"drizzle-kit": "^0.31.10",
|
|
168
172
|
"drizzle-orm": "^0.45.1",
|
|
169
173
|
"eslint": "^9.39.4",
|
|
170
174
|
"eslint-config-next": "^16.2.1",
|
|
@@ -205,6 +209,8 @@
|
|
|
205
209
|
"node-pty": "^1.1.0"
|
|
206
210
|
},
|
|
207
211
|
"devDependencies": {
|
|
208
|
-
"@
|
|
212
|
+
"@tailwindcss/oxide-linux-x64-musl": "4.2.2",
|
|
213
|
+
"@types/js-yaml": "^4.0.9",
|
|
214
|
+
"drizzle-kit": "^0.31.10"
|
|
209
215
|
}
|
|
210
216
|
}
|
|
@@ -61,7 +61,7 @@ KEY="my-secret-key"
|
|
|
61
61
|
# Create a project
|
|
62
62
|
curl -X POST http://localhost:3100/api/projects \
|
|
63
63
|
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
|
|
64
|
-
-d '{"name": "my-app", "path": "/home/user/my-app"}'
|
|
64
|
+
-d '{"name": "my-app", "path": "/home/user/my-app", "useHookTemplate": true}'
|
|
65
65
|
|
|
66
66
|
# Create a task
|
|
67
67
|
curl -X POST http://localhost:3100/api/tasks \
|
|
@@ -135,6 +135,7 @@ curl -X POST http://localhost:3100/api/attempts \
|
|
|
135
135
|
"taskId": "task_...",
|
|
136
136
|
"prompt": "Build the auth module",
|
|
137
137
|
"force_create": true,
|
|
138
|
+
"use_hook_template": true,
|
|
138
139
|
"projectName": "my-project",
|
|
139
140
|
"taskTitle": "Auth module",
|
|
140
141
|
"projectRootPath": "/path/to/project",
|
|
@@ -146,6 +147,7 @@ curl -X POST http://localhost:3100/api/attempts \
|
|
|
146
147
|
```
|
|
147
148
|
|
|
148
149
|
- `force_create` — auto-creates project + task if they don't exist
|
|
150
|
+
- `use_hook_template` — only applied when `force_create=true` creates a new project (default: `true`)
|
|
149
151
|
- `request_method` — `queue` (default) or `sync` (waits for completion)
|
|
150
152
|
- `output_format` / `output_schema` — structured output instructions
|
|
151
153
|
|
|
@@ -11,6 +11,7 @@ export interface AttemptCreationInput {
|
|
|
11
11
|
taskId: string;
|
|
12
12
|
prompt: string;
|
|
13
13
|
force_create?: boolean;
|
|
14
|
+
use_hook_template?: boolean;
|
|
14
15
|
projectId?: string;
|
|
15
16
|
projectName?: string;
|
|
16
17
|
taskTitle?: string;
|
|
@@ -67,7 +68,7 @@ interface OrchestratorDeps {
|
|
|
67
68
|
/** Callback to start the agent process (runtime singleton, lives outside SDK) */
|
|
68
69
|
startAgent: (params: AgentStartParams) => void;
|
|
69
70
|
defaultBasePath: string;
|
|
70
|
-
onProjectForceCreated?: (project: any) => Promise<void> | void;
|
|
71
|
+
onProjectForceCreated?: (project: any, input: AttemptCreationInput) => Promise<void> | void;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export function createAttemptOrchestrator(deps: OrchestratorDeps) {
|
|
@@ -98,7 +99,7 @@ export function createAttemptOrchestrator(deps: OrchestratorDeps) {
|
|
|
98
99
|
const project = await projectService.getById(task.projectId);
|
|
99
100
|
if (!project) throw new AttemptValidationError('Project not found', 404);
|
|
100
101
|
if (projectCreatedByForceCreate && onProjectForceCreated) {
|
|
101
|
-
await onProjectForceCreated(project);
|
|
102
|
+
await onProjectForceCreated(project, input);
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
// Create attempt record
|
|
@@ -63,13 +63,15 @@ const BUILTIN_COMMANDS: CommandInfo[] = [
|
|
|
63
63
|
{ name: 'vim', description: 'Enter vim mode for multi-line input', isBuiltIn: true },
|
|
64
64
|
];
|
|
65
65
|
|
|
66
|
-
function parseFrontmatter(content: string): { description?: string; argumentHint?: string } {
|
|
66
|
+
function parseFrontmatter(content: string): { name?: string; description?: string; argumentHint?: string } {
|
|
67
67
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
68
68
|
if (!match) return {};
|
|
69
69
|
const fm = match[1];
|
|
70
|
+
const nameMatch = fm.match(/name:\s*(.+)/);
|
|
70
71
|
const desc = fm.match(/description:\s*(.+)/);
|
|
71
72
|
const arg = fm.match(/argument-hint:\s*(.+)/);
|
|
72
73
|
return {
|
|
74
|
+
name: nameMatch ? nameMatch[1].trim().replace(/^["']|["']$/g, '') : undefined,
|
|
73
75
|
description: desc ? desc[1].trim().replace(/^["']|["']$/g, '') : undefined,
|
|
74
76
|
argumentHint: arg ? arg[1].trim().replace(/^["']|["']$/g, '') : undefined,
|
|
75
77
|
};
|
|
@@ -106,9 +108,7 @@ function scanSkillsDir(dir: string): CommandInfo[] {
|
|
|
106
108
|
if (existsSync(skillFile)) {
|
|
107
109
|
const content = readFileSync(skillFile, 'utf-8');
|
|
108
110
|
const fm = parseFrontmatter(content);
|
|
109
|
-
|
|
110
|
-
const nameMatch = content.match(/^---\n[\s\S]*?name:\s*(.+?)[\s\S]*?\n---/);
|
|
111
|
-
const name = nameMatch ? nameMatch[1].trim().replace(/^["']|["']$/g, '') : item;
|
|
111
|
+
const name = fm.name || item;
|
|
112
112
|
skills.push({ name, description: fm.description || `Run /${item} skill`, argumentHint: fm.argumentHint });
|
|
113
113
|
} else {
|
|
114
114
|
skills.push(...scanSkillsDir(itemPath));
|
|
@@ -182,7 +182,12 @@ export function createCommandService() {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
// Deduplicate: user commands and skills override built-ins with same name
|
|
186
|
+
const byName = new Map<string, CommandInfo>();
|
|
187
|
+
for (const cmd of [...BUILTIN_COMMANDS, ...userCommands, ...skills]) {
|
|
188
|
+
byName.set(cmd.name, cmd);
|
|
189
|
+
}
|
|
190
|
+
const all = Array.from(byName.values());
|
|
186
191
|
all.sort((a, b) => a.name.localeCompare(b.name));
|
|
187
192
|
return all;
|
|
188
193
|
},
|
|
@@ -14,7 +14,17 @@ if [[ -n "${HEALTH_URL:-}" ]]; then
|
|
|
14
14
|
else
|
|
15
15
|
PORT_VALUE="${PORT:-8556}"
|
|
16
16
|
if [[ -f .env ]]; then
|
|
17
|
-
ENV_PORT="$(
|
|
17
|
+
ENV_PORT="$(
|
|
18
|
+
awk -F '=' '
|
|
19
|
+
/^[[:space:]]*(export[[:space:]]+)?PORT[[:space:]]*=/ {
|
|
20
|
+
sub(/^[[:space:]]*(export[[:space:]]+)?PORT[[:space:]]*=[[:space:]]*/, "", $0)
|
|
21
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0)
|
|
22
|
+
gsub(/^["'"'"']|["'"'"']$/, "", $0)
|
|
23
|
+
gsub(/\r/, "", $0)
|
|
24
|
+
print $0
|
|
25
|
+
}
|
|
26
|
+
' .env | tail -n1
|
|
27
|
+
)"
|
|
18
28
|
if [[ -n "$ENV_PORT" ]]; then
|
|
19
29
|
PORT_VALUE="$ENV_PORT"
|
|
20
30
|
fi
|
|
@@ -37,7 +47,7 @@ need_cmd pm2
|
|
|
37
47
|
need_cmd curl
|
|
38
48
|
|
|
39
49
|
echo "📦 Building project..."
|
|
40
|
-
$BUILD_CMD
|
|
50
|
+
bash -lc "$BUILD_CMD"
|
|
41
51
|
|
|
42
52
|
echo "🚀 Starting/Restarting PM2 app: $APP_NAME"
|
|
43
53
|
if pm2 describe "$APP_NAME" >/dev/null 2>&1; then
|
|
@@ -5,9 +5,10 @@ echo "🔍 Checking dependencies for production build..."
|
|
|
5
5
|
echo ""
|
|
6
6
|
|
|
7
7
|
# Find all imports from source files
|
|
8
|
-
echo "Scanning imports in src/, server.ts,
|
|
8
|
+
echo "Scanning imports in src/, server.ts, next.config.ts..."
|
|
9
9
|
IMPORTS=$(find src -name "*.ts" -o -name "*.tsx" | xargs grep -h "^import.*from ['\"]" | sed -E "s/.*from ['\"]([^'\"\/]+).*/\1/" | sort -u)
|
|
10
|
-
|
|
10
|
+
# Exclude dev-only config files (drizzle.config.ts) — they only run during development
|
|
11
|
+
ROOT_IMPORTS=$(grep -h "^import.*from ['\"]" next.config.ts server.ts 2>/dev/null | sed -E "s/.*from ['\"]([^'\"\/]+).*/\1/" | sort -u)
|
|
11
12
|
|
|
12
13
|
ALL_IMPORTS=$(echo -e "$IMPORTS\n$ROOT_IMPORTS" | sort -u | grep -v "^\." | grep -v "^@/" | grep -v "^node:")
|
|
13
14
|
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Migrate all projects to the current hook template contract.
|
|
4
|
+
*
|
|
5
|
+
* What it does:
|
|
6
|
+
* 1. Sync `minio-pull-sync.ts`, `minio-push-sync.ts`, `.claude/settings.json`
|
|
7
|
+
* 2. Inject project id into hook scripts (via setupProjectDefaults)
|
|
8
|
+
* 3. Remove legacy hook env files (`hook.env`, `.env`, `hook.env.example`, `.env.example`)
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* pnpm migrate-all-projects # Dry run
|
|
12
|
+
* pnpm migrate-all-projects --force # Execute migration
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import fs from 'fs/promises';
|
|
17
|
+
import fsSync from 'fs';
|
|
18
|
+
import { db, schema } from '../src/lib/db';
|
|
19
|
+
import { setupProjectDefaults } from '../src/lib/project-utils';
|
|
20
|
+
|
|
21
|
+
interface ProjectTarget {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
path: string;
|
|
25
|
+
source: 'db' | 'scan';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface MigrationResult {
|
|
29
|
+
projectId: string;
|
|
30
|
+
projectName: string;
|
|
31
|
+
projectPath: string;
|
|
32
|
+
actions: string[];
|
|
33
|
+
status: 'success' | 'error' | 'skipped' | 'updated';
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface MigrationReport {
|
|
38
|
+
totalProjects: number;
|
|
39
|
+
processed: number;
|
|
40
|
+
updated: number;
|
|
41
|
+
skipped: number;
|
|
42
|
+
errors: number;
|
|
43
|
+
results: MigrationResult[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function inferProjectIdFromDirName(dirName: string): string {
|
|
47
|
+
const firstDash = dirName.indexOf('-');
|
|
48
|
+
if (firstDash <= 0) return dirName;
|
|
49
|
+
return dirName.slice(0, firstDash);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function getAllProjectTargets(workspaceRoot: string): Promise<ProjectTarget[]> {
|
|
53
|
+
const dataDir = process.env.DATA_DIR || path.join(workspaceRoot, 'data');
|
|
54
|
+
const projectsDir = path.join(dataDir, 'projects');
|
|
55
|
+
const targets = new Map<string, ProjectTarget>();
|
|
56
|
+
|
|
57
|
+
const dbProjects = db.select({
|
|
58
|
+
id: schema.projects.id,
|
|
59
|
+
name: schema.projects.name,
|
|
60
|
+
path: schema.projects.path,
|
|
61
|
+
}).from(schema.projects).all();
|
|
62
|
+
|
|
63
|
+
for (const project of dbProjects) {
|
|
64
|
+
const absPath = path.resolve(project.path);
|
|
65
|
+
targets.set(absPath, {
|
|
66
|
+
id: project.id,
|
|
67
|
+
name: project.name || project.id,
|
|
68
|
+
path: absPath,
|
|
69
|
+
source: 'db',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (fsSync.existsSync(projectsDir)) {
|
|
74
|
+
const entries = fsSync.readdirSync(projectsDir, { withFileTypes: true });
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (!entry.isDirectory()) continue;
|
|
77
|
+
const absPath = path.resolve(path.join(projectsDir, entry.name));
|
|
78
|
+
if (targets.has(absPath)) continue;
|
|
79
|
+
|
|
80
|
+
targets.set(absPath, {
|
|
81
|
+
id: inferProjectIdFromDirName(entry.name),
|
|
82
|
+
name: entry.name,
|
|
83
|
+
path: absPath,
|
|
84
|
+
source: 'scan',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return [...targets.values()];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function fileContent(filePath: string): Promise<string> {
|
|
93
|
+
return fs.readFile(filePath, 'utf-8');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function expectedPullContent(template: string, projectId: string): string {
|
|
97
|
+
return template.replace(/__PROJECT_ID__/g, projectId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function expectedPushContent(template: string, projectId: string): string {
|
|
101
|
+
return template.replace(/__PROJECT_ID__/g, projectId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function getProjectDiffs(projectPath: string, projectId: string, workspaceRoot: string): Promise<string[]> {
|
|
105
|
+
const diffs: string[] = [];
|
|
106
|
+
const hooksDir = path.join(projectPath, '.claude', 'hooks');
|
|
107
|
+
const claudeDir = path.join(projectPath, '.claude');
|
|
108
|
+
const templateDir = path.join(workspaceRoot, 'src', 'hooks', 'template');
|
|
109
|
+
const templateHooksDir = path.join(templateDir, 'hooks');
|
|
110
|
+
|
|
111
|
+
const pullTemplate = await fileContent(path.join(templateHooksDir, 'minio-pull-sync.ts'));
|
|
112
|
+
const pushTemplate = await fileContent(path.join(templateHooksDir, 'minio-push-sync.ts'));
|
|
113
|
+
const settingsTemplate = await fileContent(path.join(templateDir, 'settings.json'));
|
|
114
|
+
|
|
115
|
+
const expectedPull = expectedPullContent(pullTemplate, projectId);
|
|
116
|
+
const expectedPush = expectedPushContent(pushTemplate, projectId);
|
|
117
|
+
|
|
118
|
+
const pullPath = path.join(hooksDir, 'minio-pull-sync.ts');
|
|
119
|
+
const pushPath = path.join(hooksDir, 'minio-push-sync.ts');
|
|
120
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
121
|
+
|
|
122
|
+
if (!fsSync.existsSync(pullPath) || fsSync.readFileSync(pullPath, 'utf-8') !== expectedPull) {
|
|
123
|
+
diffs.push('sync:minio-pull-sync.ts');
|
|
124
|
+
}
|
|
125
|
+
if (!fsSync.existsSync(pushPath) || fsSync.readFileSync(pushPath, 'utf-8') !== expectedPush) {
|
|
126
|
+
diffs.push('sync:minio-push-sync.ts');
|
|
127
|
+
}
|
|
128
|
+
if (!fsSync.existsSync(settingsPath) || fsSync.readFileSync(settingsPath, 'utf-8') !== settingsTemplate) {
|
|
129
|
+
diffs.push('sync:.claude/settings.json');
|
|
130
|
+
}
|
|
131
|
+
for (const legacyName of ['hook.env', '.env', 'hook.env.example', '.env.example']) {
|
|
132
|
+
if (fsSync.existsSync(path.join(hooksDir, legacyName))) {
|
|
133
|
+
diffs.push(`remove:${legacyName}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return diffs;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function removeLegacyHookFiles(projectPath: string): Promise<string[]> {
|
|
141
|
+
const hooksDir = path.join(projectPath, '.claude', 'hooks');
|
|
142
|
+
const removed: string[] = [];
|
|
143
|
+
for (const fileName of ['hook.env', '.env', 'hook.env.example', '.env.example']) {
|
|
144
|
+
const filePath = path.join(hooksDir, fileName);
|
|
145
|
+
if (!fsSync.existsSync(filePath)) continue;
|
|
146
|
+
await fs.rm(filePath, { force: true });
|
|
147
|
+
removed.push(fileName);
|
|
148
|
+
}
|
|
149
|
+
return removed;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function migrateProject(target: ProjectTarget, execute: boolean, workspaceRoot: string): Promise<MigrationResult> {
|
|
153
|
+
const result: MigrationResult = {
|
|
154
|
+
projectId: target.id,
|
|
155
|
+
projectName: target.name,
|
|
156
|
+
projectPath: target.path,
|
|
157
|
+
actions: [],
|
|
158
|
+
status: 'success',
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
if (!fsSync.existsSync(target.path)) {
|
|
163
|
+
result.status = 'skipped';
|
|
164
|
+
result.actions.push('Path does not exist');
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const diffs = await getProjectDiffs(target.path, target.id, workspaceRoot);
|
|
169
|
+
if (diffs.length === 0) {
|
|
170
|
+
result.status = 'skipped';
|
|
171
|
+
result.actions.push('No changes needed');
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!execute) {
|
|
176
|
+
result.status = 'updated';
|
|
177
|
+
for (const diff of diffs) {
|
|
178
|
+
result.actions.push(`[DRY RUN] Would ${diff}`);
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await setupProjectDefaults(target.path, target.id, workspaceRoot, { useHookTemplate: true });
|
|
184
|
+
result.actions.push('Synced hook templates + settings');
|
|
185
|
+
|
|
186
|
+
const removed = await removeLegacyHookFiles(target.path);
|
|
187
|
+
for (const fileName of removed) {
|
|
188
|
+
result.actions.push(`Removed legacy ${fileName}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
result.status = 'updated';
|
|
192
|
+
return result;
|
|
193
|
+
} catch (error: any) {
|
|
194
|
+
result.status = 'error';
|
|
195
|
+
result.error = error?.message || String(error);
|
|
196
|
+
result.actions.push(`Failed: ${result.error}`);
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function printReport(report: MigrationReport, execute: boolean): void {
|
|
202
|
+
console.log('\n' + '='.repeat(80));
|
|
203
|
+
console.log(`📊 MIGRATION REPORT ${execute ? '(EXECUTED)' : '(DRY RUN)'}`);
|
|
204
|
+
console.log('='.repeat(80));
|
|
205
|
+
console.log(`Total projects: ${report.totalProjects}`);
|
|
206
|
+
console.log(`Processed: ${report.processed}`);
|
|
207
|
+
console.log(`Updated: ${report.updated}`);
|
|
208
|
+
console.log(`Skipped: ${report.skipped}`);
|
|
209
|
+
console.log(`Errors: ${report.errors}`);
|
|
210
|
+
console.log('='.repeat(80) + '\n');
|
|
211
|
+
|
|
212
|
+
for (const result of report.results) {
|
|
213
|
+
const statusIcon = result.status === 'error'
|
|
214
|
+
? '❌'
|
|
215
|
+
: result.status === 'updated'
|
|
216
|
+
? '🔄'
|
|
217
|
+
: result.status === 'skipped'
|
|
218
|
+
? '⏭️'
|
|
219
|
+
: '✅';
|
|
220
|
+
|
|
221
|
+
console.log(`${statusIcon} ${result.projectName} (${result.projectId})`);
|
|
222
|
+
console.log(` Path: ${result.projectPath}`);
|
|
223
|
+
for (const action of result.actions) {
|
|
224
|
+
console.log(` - ${action}`);
|
|
225
|
+
}
|
|
226
|
+
if (result.error) {
|
|
227
|
+
console.log(` Error: ${result.error}`);
|
|
228
|
+
}
|
|
229
|
+
console.log('');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function main() {
|
|
234
|
+
const args = process.argv.slice(2);
|
|
235
|
+
const execute = args.includes('--force');
|
|
236
|
+
const workspaceRoot = process.env.CLAUDE_WS_USER_CWD || process.cwd();
|
|
237
|
+
|
|
238
|
+
console.log('🚀 Starting project migration...');
|
|
239
|
+
console.log(`Mode: ${execute ? 'EXECUTE' : 'DRY RUN'}`);
|
|
240
|
+
console.log(`Workspace: ${workspaceRoot}\n`);
|
|
241
|
+
|
|
242
|
+
const targets = await getAllProjectTargets(workspaceRoot);
|
|
243
|
+
if (targets.length === 0) {
|
|
244
|
+
console.log('No projects found.');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(`Found ${targets.length} projects to process.\n`);
|
|
249
|
+
|
|
250
|
+
const report: MigrationReport = {
|
|
251
|
+
totalProjects: targets.length,
|
|
252
|
+
processed: 0,
|
|
253
|
+
updated: 0,
|
|
254
|
+
skipped: 0,
|
|
255
|
+
errors: 0,
|
|
256
|
+
results: [],
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
for (const target of targets) {
|
|
260
|
+
const result = await migrateProject(target, execute, workspaceRoot);
|
|
261
|
+
report.results.push(result);
|
|
262
|
+
report.processed += 1;
|
|
263
|
+
|
|
264
|
+
if (result.status === 'updated') report.updated += 1;
|
|
265
|
+
if (result.status === 'skipped') report.skipped += 1;
|
|
266
|
+
if (result.status === 'error') report.errors += 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
printReport(report, execute);
|
|
270
|
+
|
|
271
|
+
if (!execute) {
|
|
272
|
+
console.log('🔎 Dry run completed. Re-run with --force to apply changes.');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (report.errors > 0) {
|
|
277
|
+
console.error(`\n⚠️ Migration completed with ${report.errors} error(s).`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log('\n✅ Migration completed successfully.');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
main().catch((error) => {
|
|
285
|
+
console.error('\n❌ Migration failed with fatal error:', error);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
});
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
* Migrate existing projects to current default Claude workspace structure.
|
|
4
4
|
*
|
|
5
5
|
* What it does per project:
|
|
6
|
-
* -
|
|
7
|
-
* - Syncs
|
|
8
|
-
* -
|
|
9
|
-
* - Creates `.claude/hooks/.env` when missing
|
|
6
|
+
* - Syncs hook templates (`minio-pull-sync.ts`, `minio-push-sync.ts`) with injected project id
|
|
7
|
+
* - Syncs `.claude/settings.json` and `.claude/CLAUDE.md` from template
|
|
8
|
+
* - Removes legacy hook env files (`hook.env`, `.env`, `hook.env.example`, `.env.example`)
|
|
10
9
|
*
|
|
11
10
|
* Usage:
|
|
12
11
|
* pnpm projects:migrate:defaults
|
|
@@ -25,12 +24,26 @@ type ProjectTarget = {
|
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
function inferProjectIdFromDirName(dirName: string): string {
|
|
28
|
-
// Folders may be: "<projectId>" or "<projectId>-<name>"
|
|
29
27
|
const firstDash = dirName.indexOf('-');
|
|
30
28
|
if (firstDash <= 0) return dirName;
|
|
31
29
|
return dirName.slice(0, firstDash);
|
|
32
30
|
}
|
|
33
31
|
|
|
32
|
+
function removeLegacyHookEnvFiles(projectPath: string): number {
|
|
33
|
+
const hooksDir = path.join(projectPath, '.claude', 'hooks');
|
|
34
|
+
const legacyFiles = ['hook.env', '.env', 'hook.env.example', '.env.example'];
|
|
35
|
+
let removed = 0;
|
|
36
|
+
|
|
37
|
+
for (const fileName of legacyFiles) {
|
|
38
|
+
const filePath = path.join(hooksDir, fileName);
|
|
39
|
+
if (!fs.existsSync(filePath)) continue;
|
|
40
|
+
fs.rmSync(filePath, { force: true });
|
|
41
|
+
removed += 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return removed;
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
async function run(): Promise<void> {
|
|
35
48
|
const workspaceRoot = process.env.CLAUDE_WS_USER_CWD || process.cwd();
|
|
36
49
|
config({ path: path.join(workspaceRoot, '.env') });
|
|
@@ -40,7 +53,6 @@ async function run(): Promise<void> {
|
|
|
40
53
|
|
|
41
54
|
const targets = new Map<string, ProjectTarget>();
|
|
42
55
|
|
|
43
|
-
// 1) Projects tracked in DB
|
|
44
56
|
const dbProjects = db.select({
|
|
45
57
|
id: schema.projects.id,
|
|
46
58
|
path: schema.projects.path,
|
|
@@ -54,7 +66,6 @@ async function run(): Promise<void> {
|
|
|
54
66
|
});
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
// 2) Extra dirs under data/projects not present in DB
|
|
58
69
|
if (fs.existsSync(projectsDir)) {
|
|
59
70
|
const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
|
|
60
71
|
for (const entry of entries) {
|
|
@@ -79,6 +90,7 @@ async function run(): Promise<void> {
|
|
|
79
90
|
let ok = 0;
|
|
80
91
|
let skipped = 0;
|
|
81
92
|
let failed = 0;
|
|
93
|
+
let legacyRemoved = 0;
|
|
82
94
|
|
|
83
95
|
for (const target of targets.values()) {
|
|
84
96
|
try {
|
|
@@ -88,7 +100,9 @@ async function run(): Promise<void> {
|
|
|
88
100
|
continue;
|
|
89
101
|
}
|
|
90
102
|
|
|
91
|
-
await setupProjectDefaults(target.projectPath, target.id, workspaceRoot);
|
|
103
|
+
await setupProjectDefaults(target.projectPath, target.id, workspaceRoot, { useHookTemplate: true });
|
|
104
|
+
legacyRemoved += removeLegacyHookEnvFiles(target.projectPath);
|
|
105
|
+
|
|
92
106
|
console.log(`[projects:migrate:defaults] OK ${target.source} id=${target.id} path=${target.projectPath}`);
|
|
93
107
|
ok++;
|
|
94
108
|
} catch (error) {
|
|
@@ -97,7 +111,7 @@ async function run(): Promise<void> {
|
|
|
97
111
|
}
|
|
98
112
|
}
|
|
99
113
|
|
|
100
|
-
console.log(`[projects:migrate:defaults] Done. ok=${ok} skipped=${skipped} failed=${failed}`);
|
|
114
|
+
console.log(`[projects:migrate:defaults] Done. ok=${ok} skipped=${skipped} failed=${failed} legacyRemoved=${legacyRemoved}`);
|
|
101
115
|
if (failed > 0) process.exit(1);
|
|
102
116
|
}
|
|
103
117
|
|
package/server.ts
CHANGED
|
@@ -155,6 +155,10 @@ app.prepare().then(async () => {
|
|
|
155
155
|
// Keep connections alive through Cloudflare Tunnel (100s idle timeout)
|
|
156
156
|
pingInterval: 10000,
|
|
157
157
|
pingTimeout: 10000,
|
|
158
|
+
// Compress WebSocket messages — terminal output is highly compressible text
|
|
159
|
+
perMessageDeflate: {
|
|
160
|
+
threshold: 256, // only compress messages larger than 256 bytes
|
|
161
|
+
},
|
|
158
162
|
});
|
|
159
163
|
|
|
160
164
|
// Restore autopilot state from DB (needs io for worker callbacks)
|
|
@@ -1040,8 +1044,25 @@ app.prepare().then(async () => {
|
|
|
1040
1044
|
// Terminal Manager Event Handlers
|
|
1041
1045
|
// ========================================
|
|
1042
1046
|
|
|
1047
|
+
// Batch rapid PTY output bursts into single socket messages (~16ms window).
|
|
1048
|
+
// Reduces message count significantly for high-throughput commands (ls, cat, etc.)
|
|
1049
|
+
const terminalOutputBuffers = new Map<string, string>();
|
|
1050
|
+
let terminalOutputFlushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1051
|
+
|
|
1052
|
+
function flushTerminalOutputBuffers() {
|
|
1053
|
+
terminalOutputFlushTimer = null;
|
|
1054
|
+
for (const [terminalId, buffered] of terminalOutputBuffers) {
|
|
1055
|
+
io.to(`terminal:${terminalId}`).emit('terminal:output', { terminalId, data: buffered });
|
|
1056
|
+
}
|
|
1057
|
+
terminalOutputBuffers.clear();
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1043
1060
|
terminalManager.on('output', ({ terminalId, data }) => {
|
|
1044
|
-
|
|
1061
|
+
const existing = terminalOutputBuffers.get(terminalId) || '';
|
|
1062
|
+
terminalOutputBuffers.set(terminalId, existing + data);
|
|
1063
|
+
if (!terminalOutputFlushTimer) {
|
|
1064
|
+
terminalOutputFlushTimer = setTimeout(flushTerminalOutputBuffers, 16);
|
|
1065
|
+
}
|
|
1045
1066
|
});
|
|
1046
1067
|
|
|
1047
1068
|
terminalManager.on('exit', ({ terminalId, exitCode, signal }) => {
|
|
@@ -18,8 +18,9 @@ function getOrchestrator() {
|
|
|
18
18
|
sessionManager,
|
|
19
19
|
startAgent: (params) => agentManager.start(params),
|
|
20
20
|
defaultBasePath: process.env.CLAUDE_WS_USER_CWD || /* turbopackIgnore: true */ process.cwd(),
|
|
21
|
-
onProjectForceCreated: async (project) => {
|
|
22
|
-
|
|
21
|
+
onProjectForceCreated: async (project, input) => {
|
|
22
|
+
const shouldUseHookTemplate = typeof input?.use_hook_template === 'boolean' ? input.use_hook_template : true;
|
|
23
|
+
await setupProjectDefaults(project.path, project.id, process.cwd(), { useHookTemplate: shouldUseHookTemplate });
|
|
23
24
|
},
|
|
24
25
|
});
|
|
25
26
|
}
|
|
@@ -17,9 +17,14 @@ export async function GET() {
|
|
|
17
17
|
// POST /api/projects - Create a new project
|
|
18
18
|
export async function POST(request: NextRequest) {
|
|
19
19
|
try {
|
|
20
|
-
const { id, name, path, projectId } = await request.json();
|
|
20
|
+
const { id, name, path, projectId, useHookTemplate, use_hook_template } = await request.json();
|
|
21
21
|
const project = await projectService.createProject({ id: projectId || id, name, path });
|
|
22
|
-
|
|
22
|
+
const shouldUseHookTemplate = typeof useHookTemplate === 'boolean'
|
|
23
|
+
? useHookTemplate
|
|
24
|
+
: typeof use_hook_template === 'boolean'
|
|
25
|
+
? use_hook_template
|
|
26
|
+
: true;
|
|
27
|
+
await setupProjectDefaults(project.path, project.id, process.cwd(), { useHookTemplate: shouldUseHookTemplate });
|
|
23
28
|
return NextResponse.json(project, { status: 201 });
|
|
24
29
|
} catch (error: any) {
|
|
25
30
|
if (error instanceof ProjectValidationError) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { DiffView } from '@/components/claude/diff-view';
|
|
4
|
+
|
|
5
|
+
interface WriteBlockProps {
|
|
6
|
+
input: any;
|
|
7
|
+
result?: string;
|
|
8
|
+
isError?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Renders a Write tool invocation as a diff view (shows new file content) */
|
|
12
|
+
export function WriteBlock({ input, result: _result, isError: _isError }: WriteBlockProps) {
|
|
13
|
+
if (!input?.content) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<DiffView
|
|
19
|
+
oldText=""
|
|
20
|
+
newText={input.content}
|
|
21
|
+
filePath={input.file_path}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|