claude-ws 0.4.9-beta.4 → 0.4.9-beta.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-ws",
3
- "version": "0.4.9-beta.4",
3
+ "version": "0.4.9-beta.6",
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": [
@@ -141,9 +141,9 @@
141
141
  "@tailwindcss/postcss": "^4.2.2",
142
142
  "@types/adm-zip": "^0.5.8",
143
143
  "@types/better-sqlite3": "^7.6.13",
144
- "@types/js-yaml": "^4.0.9",
145
144
  "@types/compression": "^1.8.1",
146
145
  "@types/dompurify": "^3.2.0",
146
+ "@types/js-yaml": "^4.0.9",
147
147
  "@types/node": "^20.19.37",
148
148
  "@types/react": "^19.2.14",
149
149
  "@types/react-dom": "^19.2.3",
@@ -164,6 +164,7 @@
164
164
  "diff": "^8.0.3",
165
165
  "dompurify": "^3.3.3",
166
166
  "dotenv": "^17.3.1",
167
+ "drizzle-kit": "^0.31.10",
167
168
  "drizzle-orm": "^0.45.1",
168
169
  "eslint": "^9.39.4",
169
170
  "eslint-config-next": "^16.2.1",
@@ -204,7 +205,6 @@
204
205
  "node-pty": "^1.1.0"
205
206
  },
206
207
  "devDependencies": {
207
- "drizzle-kit": "^0.31.10",
208
208
  "@types/js-yaml": "^4.0.9"
209
209
  }
210
210
  }
@@ -98,7 +98,7 @@ export async function POST(request: NextRequest) {
98
98
  return NextResponse.json({ error: 'projectId is required' }, { status: 400 });
99
99
  }
100
100
 
101
- const apiHookUrl = resolveApiHookUrl(undefined, request.nextUrl.hostname);
101
+ const apiHookUrl = resolveApiHookUrl(undefined, request.nextUrl.hostname, projectId);
102
102
  if (!apiHookUrl) {
103
103
  return NextResponse.json({ error: 'API_HOOK_URL is not configured on server' }, { status: 500 });
104
104
  }
@@ -98,7 +98,7 @@ export async function POST(request: NextRequest) {
98
98
  return NextResponse.json({ error: 'projectId is required' }, { status: 400 });
99
99
  }
100
100
 
101
- const apiHookUrl = resolveApiHookUrl(undefined, request.nextUrl.hostname);
101
+ const apiHookUrl = resolveApiHookUrl(undefined, request.nextUrl.hostname, projectId);
102
102
  if (!apiHookUrl) {
103
103
  return NextResponse.json({ error: 'API_HOOK_URL is not configured on server' }, { status: 500 });
104
104
  }
@@ -1,11 +1,12 @@
1
- # Optional hard override (if set, this value is always used)
2
- # API_HOOK_URL="http://localhost:5005/api/sync/"
1
+ # Domain template (required)
2
+ # {room_id} (or room_id) will be replaced by PROJECT_ID at runtime.
3
+ API_HOOK_URL_DOMAIN="https://privos-chat-dev.roxane.one/api/v1/internal/rooms/{room_id}/files/"
3
4
 
4
- # Auto-select by environment/host:
5
- # - localhost/dev -> API_HOOK_URL_LOCAL
6
- # - domain/prod -> API_HOOK_URL_DOMAIN
7
- API_HOOK_URL_LOCAL="http://localhost:5005/api/sync/"
8
- API_HOOK_URL_DOMAIN="https://privos-chat-dev.roxane.one/api/v1/internal/rooms/room_id/files/"
5
+ # Optional explicit override. If this contains room_id/{room_id}, it is also replaced by PROJECT_ID.
6
+ # API_HOOK_URL="https://privos-chat-dev.roxane.one/api/v1/internal/rooms/{room_id}/files/"
7
+
8
+ # Optional local fallback for non-domain environments.
9
+ # API_HOOK_URL_LOCAL="http://localhost:5005/api/sync/"
9
10
  API_HOOK_API_KEY=
10
11
  API_QUEUE_URL="http://localhost:8052"
11
12
  # Optional comma-separated extra queue endpoints/base URLs
@@ -113,10 +113,12 @@ async function ensurePullDb(): Promise<Database.Database> {
113
113
  return sqlite;
114
114
  }
115
115
 
116
- async function fetchManifest(folder: string, label: string, allowNotFound = false): Promise<ManifestEntry[]> {
117
- console.error(`🔍 Calling API to get manifest for '${label}' (${folder})...`);
116
+ async function fetchManifest(label: string, options?: { root?: "markdown"; allowNotFound?: boolean }): Promise<ManifestEntry[]> {
117
+ const root = options?.root;
118
+ const allowNotFound = options?.allowNotFound || false;
119
+ console.error(`🔍 Calling API to get manifest for '${label}'${root ? ` (root=${root})` : ""}...`);
118
120
 
119
- const url = buildApiUrl(`manifest?folder=${encodeURIComponent(folder)}`);
121
+ const url = root ? buildApiUrl(`manifest?root=${encodeURIComponent(root)}`) : buildApiUrl("manifest");
120
122
  const response = await fetch(url, { headers: buildApiHeaders() });
121
123
 
122
124
  if (response.status === 404 && allowNotFound) {
@@ -140,12 +142,9 @@ async function fetchManifest(folder: string, label: string, allowNotFound = fals
140
142
  }
141
143
 
142
144
  async function getQueueCandidates(): Promise<{ candidates: QueueFileCandidate[]; manifestData: ManifestEntry[] }> {
143
- const mainPrefix = config.projectId;
144
- const markdownPrefix = `markdown/${config.projectId}`;
145
-
146
145
  const [mainManifest, markdownManifest] = await Promise.all([
147
- fetchManifest(mainPrefix, "main folder"),
148
- fetchManifest(markdownPrefix, "markdown folder", true),
146
+ fetchManifest("main folder"),
147
+ fetchManifest("markdown folder", { root: "markdown", allowNotFound: true }),
149
148
  ]);
150
149
 
151
150
  const normalize = (entries: ManifestEntry[], folder: FolderType): QueueFileCandidate[] => (
@@ -24,34 +24,44 @@ function readVar(key: string, hookEnvValues?: HookEnvMapLike): string {
24
24
  return '';
25
25
  }
26
26
 
27
- function shouldUseLocalByHost(hostname?: string): boolean {
28
- const fromHost = (hostname || '').toLowerCase();
29
- const fromEnvHost = (process.env.HOST || '').toLowerCase();
30
- const appUrl = (process.env.API_BASE_URL || process.env.CORS_ORIGIN || '').toLowerCase();
31
-
32
- const localMarkers = ['localhost', '127.0.0.1', '0.0.0.0'];
33
- if (fromHost) {
34
- return localMarkers.some((marker) => fromHost.includes(marker));
27
+ function hasRoomPlaceholder(value: string): boolean {
28
+ return /\{room_id\}|room_id/i.test(value);
29
+ }
30
+
31
+ function resolveRoomTemplate(value: string, roomId?: string): string {
32
+ const trimmed = trimQuotes(value);
33
+ if (!hasRoomPlaceholder(trimmed)) {
34
+ return trimmed;
35
35
  }
36
36
 
37
- const hasLocalEnvHost = localMarkers.some((marker) => fromEnvHost.includes(marker) || appUrl.includes(marker));
38
- if (hasLocalEnvHost) return true;
37
+ const normalizedRoomId = (roomId || '').trim();
38
+ if (!normalizedRoomId) {
39
+ return trimmed;
40
+ }
39
41
 
40
- return process.env.NODE_ENV !== 'production';
42
+ return trimmed
43
+ .replace(/\{room_id\}/gi, normalizedRoomId)
44
+ .replace(/room_id/gi, normalizedRoomId);
41
45
  }
42
46
 
43
- export function resolveApiHookUrl(hookEnvValues?: HookEnvMapLike, hostname?: string): string {
47
+ export function resolveApiHookUrl(hookEnvValues?: HookEnvMapLike, _hostname?: string, roomId?: string): string {
48
+ const resolvedRoomId = (roomId || readVar('PROJECT_ID', hookEnvValues)).trim();
49
+ const domainTemplate = readVar('API_HOOK_URL_DOMAIN', hookEnvValues);
50
+ if (domainTemplate) {
51
+ return resolveRoomTemplate(domainTemplate, resolvedRoomId);
52
+ }
53
+
44
54
  const explicit = readVar('API_HOOK_URL', hookEnvValues);
45
- if (explicit) return explicit;
55
+ if (explicit) {
56
+ return resolveRoomTemplate(explicit, resolvedRoomId);
57
+ }
46
58
 
47
59
  const local = readVar('API_HOOK_URL_LOCAL', hookEnvValues);
48
- const domain = readVar('API_HOOK_URL_DOMAIN', hookEnvValues);
49
-
50
- if (shouldUseLocalByHost(hostname)) {
51
- return local || domain;
60
+ if (local) {
61
+ return resolveRoomTemplate(local, resolvedRoomId);
52
62
  }
53
63
 
54
- return domain || local;
64
+ return '';
55
65
  }
56
66
 
57
67
  /**
@@ -257,15 +257,15 @@ async function readHookEnv(projectPath: string, fallbackProjectId: string): Prom
257
257
  map.set(key, value);
258
258
  }
259
259
 
260
- const apiHookUrl = resolveApiHookUrl(map);
260
+ const projectId = map.get('PROJECT_ID')?.trim() || fallbackProjectId;
261
+ const apiHookUrl = resolveApiHookUrl(map, undefined, projectId);
261
262
  if (!apiHookUrl) {
262
- throw new Error('Missing API_HOOK_URL in project hook .env and process env');
263
+ throw new Error('Missing API_HOOK_URL/API_HOOK_URL_DOMAIN in project hook .env and process env');
263
264
  }
264
265
  const apiHookApiKey = map.get('API_HOOK_API_KEY')?.trim()
265
266
  || process.env.API_HOOK_API_KEY?.trim()
266
267
  || '';
267
268
 
268
- const projectId = map.get('PROJECT_ID')?.trim() || fallbackProjectId;
269
269
  return { apiHookUrl, apiHookApiKey, projectId };
270
270
  }
271
271
 
@@ -277,10 +277,11 @@ function buildApiHeaders(apiHookApiKey: string): Record<string, string> {
277
277
  async function fetchManifest(
278
278
  apiHookUrl: string,
279
279
  apiHookApiKey: string,
280
- folder: string,
281
- label: string
280
+ label: string,
281
+ options?: { root?: 'markdown' }
282
282
  ): Promise<ManifestEntry[]> {
283
- const url = buildApiHookEndpoint(apiHookUrl, `manifest?folder=${encodeURIComponent(folder)}`);
283
+ const endpoint = options?.root ? `manifest?root=${options.root}` : 'manifest';
284
+ const url = buildApiHookEndpoint(apiHookUrl, endpoint);
284
285
  const response = await fetch(url, { headers: buildApiHeaders(apiHookApiKey) });
285
286
  if (!response.ok) {
286
287
  throw new Error(`Manifest API failed for ${label}: HTTP ${response.status} ${response.statusText}`);
@@ -362,12 +363,9 @@ async function fetchQueueCandidates(
362
363
  projectId: string,
363
364
  sqlite: Database.Database
364
365
  ): Promise<QueueFileCandidate[]> {
365
- const targetPrefix = projectId;
366
- const markdownPrefix = `markdown/${projectId}`;
367
-
368
366
  const [mainManifest, markdownManifest] = await Promise.all([
369
- fetchManifest(apiHookUrl, apiHookApiKey, targetPrefix, 'main folder'),
370
- fetchManifest(apiHookUrl, apiHookApiKey, markdownPrefix, 'markdown folder'),
367
+ fetchManifest(apiHookUrl, apiHookApiKey, 'main folder'),
368
+ fetchManifest(apiHookUrl, apiHookApiKey, 'markdown folder', { root: 'markdown' }),
371
369
  ]);
372
370
 
373
371
  const normalize = (entries: ManifestEntry[], folder: 'main' | 'markdown'): QueueFileCandidate[] => (
@@ -908,8 +906,8 @@ async function processJob(projectPath: string, projectId: string, job: PendingJo
908
906
  const manifestEntry = await fetchManifest(
909
907
  hookEnv.apiHookUrl,
910
908
  hookEnv.apiHookApiKey,
911
- row.folder === 'main' ? hookEnv.projectId : `markdown/${hookEnv.projectId}`,
912
- row.folder
909
+ row.folder,
910
+ row.folder === 'markdown' ? { root: 'markdown' } : undefined
913
911
  );
914
912
  const latest = manifestEntry.find((entry) => entry.key === row.fileKey);
915
913
  if (!latest) {
@@ -226,15 +226,15 @@ async function readHookEnv(projectPath: string, fallbackProjectId: string): Prom
226
226
  values.set(key, value);
227
227
  }
228
228
 
229
- const apiHookUrl = resolveApiHookUrl(values);
229
+ const projectId = values.get('PROJECT_ID')?.trim() || fallbackProjectId;
230
+ const apiHookUrl = resolveApiHookUrl(values, undefined, projectId);
230
231
  if (!apiHookUrl) {
231
- throw new Error('Missing API_HOOK_URL in project hook .env and process env');
232
+ throw new Error('Missing API_HOOK_URL/API_HOOK_URL_DOMAIN in project hook .env and process env');
232
233
  }
233
234
  const apiHookApiKey = values.get('API_HOOK_API_KEY')?.trim()
234
235
  || process.env.API_HOOK_API_KEY?.trim()
235
236
  || '';
236
237
 
237
- const projectId = values.get('PROJECT_ID')?.trim() || fallbackProjectId;
238
238
  return { apiHookUrl, apiHookApiKey, projectId };
239
239
  }
240
240
 
@@ -339,12 +339,18 @@ async function requestWithoutJsonWithRetry(
339
339
  throw new Error(`${label} failed after ${REQUEST_MAX_RETRIES} attempts: ${lastError?.message || 'Unknown error'}`);
340
340
  }
341
341
 
342
- async function fetchManifest(apiHookUrl: string, apiHookApiKey: string, folder: string): Promise<ManifestEntry[]> {
343
- const url = buildApiHookEndpoint(apiHookUrl, `manifest?folder=${encodeURIComponent(folder)}`);
342
+ async function fetchManifest(
343
+ apiHookUrl: string,
344
+ apiHookApiKey: string,
345
+ label: string,
346
+ options?: { root?: 'markdown' },
347
+ ): Promise<ManifestEntry[]> {
348
+ const endpoint = options?.root ? `manifest?root=${options.root}` : 'manifest';
349
+ const url = buildApiHookEndpoint(apiHookUrl, endpoint);
344
350
  const payload = await requestJsonWithRetry(
345
351
  url,
346
352
  { method: 'GET', headers: buildApiHeaders(apiHookApiKey) },
347
- `Manifest fetch (${folder})`
353
+ `Manifest fetch (${label})`
348
354
  ) as {
349
355
  status?: string;
350
356
  data?: unknown;
@@ -352,7 +358,7 @@ async function fetchManifest(apiHookUrl: string, apiHookApiKey: string, folder:
352
358
  };
353
359
 
354
360
  if (payload.status !== 'success' || !Array.isArray(payload.data)) {
355
- throw new Error(`Invalid manifest response for ${folder}: ${payload.message || 'Invalid payload'}`);
361
+ throw new Error(`Invalid manifest response for ${label}: ${payload.message || 'Invalid payload'}`);
356
362
  }
357
363
 
358
364
  return payload.data as ManifestEntry[];
@@ -581,10 +587,12 @@ function enqueueInDb(
581
587
 
582
588
  export async function enqueueProjectPushSync(projectPath: string, fallbackProjectId: string) {
583
589
  const hookEnv = await readHookEnv(projectPath, fallbackProjectId);
584
- const [remoteManifest, localFiles] = await Promise.all([
585
- fetchManifest(hookEnv.apiHookUrl, hookEnv.apiHookApiKey, hookEnv.projectId),
590
+ const [mainManifest, markdownManifest, localFiles] = await Promise.all([
591
+ fetchManifest(hookEnv.apiHookUrl, hookEnv.apiHookApiKey, 'main'),
592
+ fetchManifest(hookEnv.apiHookUrl, hookEnv.apiHookApiKey, 'markdown', { root: 'markdown' }),
586
593
  scanLocalFiles(projectPath, hookEnv.projectId),
587
594
  ]);
595
+ const remoteManifest = [...mainManifest, ...markdownManifest];
588
596
 
589
597
  const candidates = buildQueueCandidates(hookEnv.projectId, localFiles, remoteManifest);
590
598
  const sqlite = await ensurePushQueueDb(projectPath);
@@ -705,7 +713,7 @@ async function uploadByPresignedUrl(url: string, localPath: string): Promise<voi
705
713
 
706
714
  async function deleteByApiEndpoint(apiHookUrl: string, apiHookApiKey: string, key: string): Promise<void> {
707
715
  const encodedKey = encodeURIComponent(key);
708
- const deleteUrl = buildApiHookEndpoint(apiHookUrl, `delete?key=${encodedKey}`);
716
+ const deleteUrl = buildApiHookEndpoint(apiHookUrl, `by-path?key=${encodedKey}`);
709
717
  await requestWithoutJsonWithRetry(
710
718
  deleteUrl,
711
719
  { method: 'DELETE', headers: buildApiHeaders(apiHookApiKey) },
@@ -106,7 +106,7 @@ export async function setupProjectDefaults(projectPath: string, projectId?: stri
106
106
  try {
107
107
  await access(envPath);
108
108
  } catch {
109
- const apiBase = resolveApiHookUrl();
109
+ const apiBase = resolveApiHookUrl(undefined, undefined, projectId);
110
110
  const apiHookApiKey = process.env.API_HOOK_API_KEY?.trim() || '';
111
111
  const projectIdEnvLine = projectId ? `PROJECT_ID="${projectId}"\n` : '';
112
112
  const apiKeyEnvLine = apiHookApiKey ? `API_HOOK_API_KEY="${apiHookApiKey}"\n` : '';