@ccpocket/bridge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/claude-process.d.ts +108 -0
- package/dist/claude-process.js +471 -0
- package/dist/claude-process.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +42 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-process.d.ts +46 -0
- package/dist/codex-process.js +420 -0
- package/dist/codex-process.js.map +1 -0
- package/dist/debug-trace-store.d.ts +15 -0
- package/dist/debug-trace-store.js +78 -0
- package/dist/debug-trace-store.js.map +1 -0
- package/dist/firebase-auth.d.ts +35 -0
- package/dist/firebase-auth.js +132 -0
- package/dist/firebase-auth.js.map +1 -0
- package/dist/gallery-store.d.ts +66 -0
- package/dist/gallery-store.js +310 -0
- package/dist/gallery-store.js.map +1 -0
- package/dist/image-store.d.ts +22 -0
- package/dist/image-store.js +113 -0
- package/dist/image-store.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/mdns.d.ts +6 -0
- package/dist/mdns.js +42 -0
- package/dist/mdns.js.map +1 -0
- package/dist/parser.d.ts +381 -0
- package/dist/parser.js +218 -0
- package/dist/parser.js.map +1 -0
- package/dist/project-history.d.ts +10 -0
- package/dist/project-history.js +73 -0
- package/dist/project-history.js.map +1 -0
- package/dist/prompt-history-backup.d.ts +15 -0
- package/dist/prompt-history-backup.js +46 -0
- package/dist/prompt-history-backup.js.map +1 -0
- package/dist/push-relay.d.ts +27 -0
- package/dist/push-relay.js +69 -0
- package/dist/push-relay.js.map +1 -0
- package/dist/recording-store.d.ts +51 -0
- package/dist/recording-store.js +158 -0
- package/dist/recording-store.js.map +1 -0
- package/dist/screenshot.d.ts +28 -0
- package/dist/screenshot.js +98 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/sdk-process.d.ts +151 -0
- package/dist/sdk-process.js +740 -0
- package/dist/sdk-process.js.map +1 -0
- package/dist/session.d.ts +126 -0
- package/dist/session.js +550 -0
- package/dist/session.js.map +1 -0
- package/dist/sessions-index.d.ts +86 -0
- package/dist/sessions-index.js +1027 -0
- package/dist/sessions-index.js.map +1 -0
- package/dist/setup-launchd.d.ts +8 -0
- package/dist/setup-launchd.js +109 -0
- package/dist/setup-launchd.js.map +1 -0
- package/dist/startup-info.d.ts +8 -0
- package/dist/startup-info.js +78 -0
- package/dist/startup-info.js.map +1 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +236 -0
- package/dist/usage.js.map +1 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.js +39 -0
- package/dist/version.js.map +1 -0
- package/dist/websocket.d.ts +71 -0
- package/dist/websocket.js +1487 -0
- package/dist/websocket.js.map +1 -0
- package/dist/worktree-store.d.ts +25 -0
- package/dist/worktree-store.js +59 -0
- package/dist/worktree-store.js.map +1 -0
- package/dist/worktree.d.ts +43 -0
- package/dist/worktree.js +295 -0
- package/dist/worktree.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Anonymous Auth client for Bridge Server.
|
|
3
|
+
*
|
|
4
|
+
* Uses the Firebase Auth REST API directly instead of the client SDK
|
|
5
|
+
* to avoid Node.js compatibility issues with the browser-oriented SDK.
|
|
6
|
+
*
|
|
7
|
+
* Each Bridge instance signs in anonymously and obtains:
|
|
8
|
+
* - A unique UID (used as bridgeId)
|
|
9
|
+
* - An ID token (used as Bearer token for Cloud Functions)
|
|
10
|
+
*
|
|
11
|
+
* Credentials are persisted to ~/.ccpocket/firebase-credentials.json
|
|
12
|
+
* so that Bridge restarts reuse the same UID instead of creating
|
|
13
|
+
* a new anonymous account each time.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
const FIREBASE_API_KEY = "AIzaSyAptNnokWPqJIgv2Lr3I8ETN6bqZb5BGvc";
|
|
19
|
+
const SIGN_UP_URL = `https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=${FIREBASE_API_KEY}`;
|
|
20
|
+
const REFRESH_URL = `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`;
|
|
21
|
+
const CREDENTIALS_DIR = join(homedir(), ".ccpocket");
|
|
22
|
+
const CREDENTIALS_FILE = join(CREDENTIALS_DIR, "firebase-credentials.json");
|
|
23
|
+
function loadCredentials() {
|
|
24
|
+
try {
|
|
25
|
+
if (!existsSync(CREDENTIALS_FILE))
|
|
26
|
+
return null;
|
|
27
|
+
const raw = readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
28
|
+
const data = JSON.parse(raw);
|
|
29
|
+
if (typeof data.uid === "string" && typeof data.refreshToken === "string") {
|
|
30
|
+
return { uid: data.uid, refreshToken: data.refreshToken };
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function saveCredentials(creds) {
|
|
39
|
+
try {
|
|
40
|
+
mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
41
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), "utf-8");
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.warn("[firebase-auth] Failed to persist credentials:", err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class FirebaseAuthClient {
|
|
48
|
+
_uid = null;
|
|
49
|
+
_idToken = null;
|
|
50
|
+
_refreshToken = null;
|
|
51
|
+
_expiresAt = 0;
|
|
52
|
+
get uid() {
|
|
53
|
+
if (!this._uid)
|
|
54
|
+
throw new Error("Firebase auth not initialized");
|
|
55
|
+
return this._uid;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Initialize Firebase auth.
|
|
59
|
+
* Tries to restore a previous session from disk first; falls back to
|
|
60
|
+
* creating a new anonymous account if restoration fails.
|
|
61
|
+
*/
|
|
62
|
+
async initialize() {
|
|
63
|
+
const saved = loadCredentials();
|
|
64
|
+
if (saved) {
|
|
65
|
+
try {
|
|
66
|
+
await this.restoreSession(saved);
|
|
67
|
+
console.log(`[firebase-auth] Restored session. UID: ${this._uid}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.warn("[firebase-auth] Failed to restore session, creating new account:", err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
await this.signUpAnonymously();
|
|
75
|
+
console.log(`[firebase-auth] Signed in anonymously. UID: ${this._uid}`);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns a fresh Firebase ID token.
|
|
79
|
+
* Automatically refreshes if the token is expired or about to expire.
|
|
80
|
+
*/
|
|
81
|
+
async getIdToken() {
|
|
82
|
+
if (!this._idToken || !this._refreshToken) {
|
|
83
|
+
throw new Error("Firebase auth not initialized");
|
|
84
|
+
}
|
|
85
|
+
if (Date.now() >= this._expiresAt) {
|
|
86
|
+
await this.refreshIdToken();
|
|
87
|
+
}
|
|
88
|
+
return this._idToken;
|
|
89
|
+
}
|
|
90
|
+
async signUpAnonymously() {
|
|
91
|
+
const res = await fetch(SIGN_UP_URL, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify({ returnSecureToken: true }),
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
const text = await res.text();
|
|
98
|
+
throw new Error(`Firebase anonymous sign-in failed (${res.status}): ${text}`);
|
|
99
|
+
}
|
|
100
|
+
const data = (await res.json());
|
|
101
|
+
this._uid = data.localId;
|
|
102
|
+
this._idToken = data.idToken;
|
|
103
|
+
this._refreshToken = data.refreshToken;
|
|
104
|
+
this._expiresAt = Date.now() + (parseInt(data.expiresIn, 10) || 3600) * 1000 - 60_000;
|
|
105
|
+
saveCredentials({ uid: this._uid, refreshToken: this._refreshToken });
|
|
106
|
+
}
|
|
107
|
+
async restoreSession(saved) {
|
|
108
|
+
this._uid = saved.uid;
|
|
109
|
+
this._refreshToken = saved.refreshToken;
|
|
110
|
+
// Force a token refresh to validate the saved credentials
|
|
111
|
+
await this.refreshIdToken();
|
|
112
|
+
// Persist potentially rotated refresh token
|
|
113
|
+
saveCredentials({ uid: this._uid, refreshToken: this._refreshToken });
|
|
114
|
+
}
|
|
115
|
+
async refreshIdToken() {
|
|
116
|
+
const res = await fetch(REFRESH_URL, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
119
|
+
body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(this._refreshToken)}`,
|
|
120
|
+
});
|
|
121
|
+
if (!res.ok) {
|
|
122
|
+
const text = await res.text();
|
|
123
|
+
throw new Error(`Firebase token refresh failed (${res.status}): ${text}`);
|
|
124
|
+
}
|
|
125
|
+
const data = (await res.json());
|
|
126
|
+
this._idToken = data.id_token;
|
|
127
|
+
this._refreshToken = data.refresh_token;
|
|
128
|
+
this._expiresAt = Date.now() + (parseInt(data.expires_in, 10) || 3600) * 1000 - 60_000;
|
|
129
|
+
console.log("[firebase-auth] ID token refreshed");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=firebase-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firebase-auth.js","sourceRoot":"","sources":["../src/firebase-auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
export interface GalleryImageMeta {
|
|
3
|
+
id: string;
|
|
4
|
+
filename: string;
|
|
5
|
+
mimeType: string;
|
|
6
|
+
projectPath: string;
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
sourcePath: string;
|
|
9
|
+
addedAt: string;
|
|
10
|
+
sizeBytes: number;
|
|
11
|
+
}
|
|
12
|
+
export interface GalleryImageInfo {
|
|
13
|
+
id: string;
|
|
14
|
+
url: string;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
projectPath: string;
|
|
17
|
+
projectName: string;
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
addedAt: string;
|
|
20
|
+
sizeBytes: number;
|
|
21
|
+
}
|
|
22
|
+
export declare class GalleryStore {
|
|
23
|
+
private index;
|
|
24
|
+
init(): Promise<void>;
|
|
25
|
+
private saveIndex;
|
|
26
|
+
addImage(filePath: string, projectPath: string, sessionId?: string): Promise<GalleryImageMeta | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Add an image from base64-encoded data.
|
|
29
|
+
* This allows mobile clients to upload images directly without file paths.
|
|
30
|
+
*/
|
|
31
|
+
addImageFromBase64(base64Data: string, mimeType: string, projectPath: string, sessionId?: string): Promise<GalleryImageMeta | null>;
|
|
32
|
+
list(options?: {
|
|
33
|
+
projectPath?: string;
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
}): GalleryImageInfo[];
|
|
36
|
+
getImagePath(id: string): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Get image as Base64 for SDK message embedding.
|
|
39
|
+
* Returns null if image not found.
|
|
40
|
+
*/
|
|
41
|
+
getImageAsBase64(id: string): Promise<{
|
|
42
|
+
base64: string;
|
|
43
|
+
mimeType: string;
|
|
44
|
+
} | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Get mime type for an image by ID.
|
|
47
|
+
*/
|
|
48
|
+
getMimeType(id: string): string | null;
|
|
49
|
+
delete(id: string): Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* Handle HTTP requests for gallery image serving.
|
|
52
|
+
* Returns true if the request was handled.
|
|
53
|
+
*/
|
|
54
|
+
handleRequest(req: IncomingMessage, res: ServerResponse): boolean;
|
|
55
|
+
private serveImage;
|
|
56
|
+
/**
|
|
57
|
+
* Handle POST /api/gallery/upload.
|
|
58
|
+
* Accepts JSON body with EITHER:
|
|
59
|
+
* - { filePath: string, projectPath: string, sessionId?: string } (file path mode)
|
|
60
|
+
* - { base64: string, mimeType: string, projectPath: string, sessionId?: string } (base64 mode)
|
|
61
|
+
* Returns true if the request was handled.
|
|
62
|
+
*/
|
|
63
|
+
handleUploadRequest(req: IncomingMessage, res: ServerResponse, onNewImage?: (meta: GalleryImageMeta) => void): boolean;
|
|
64
|
+
/** Convert GalleryImageMeta to GalleryImageInfo for WS broadcast. */
|
|
65
|
+
metaToInfo(meta: GalleryImageMeta): GalleryImageInfo;
|
|
66
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, writeFile, mkdir, copyFile, stat, unlink } from "node:fs/promises";
|
|
3
|
+
import { join, extname, basename } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
const GALLERY_DIR = join(homedir(), ".ccpocket", "gallery");
|
|
6
|
+
const IMAGES_DIR = join(GALLERY_DIR, "images");
|
|
7
|
+
const INDEX_FILE = join(GALLERY_DIR, "index.json");
|
|
8
|
+
const MIME_TYPES = {
|
|
9
|
+
".png": "image/png",
|
|
10
|
+
".jpg": "image/jpeg",
|
|
11
|
+
".jpeg": "image/jpeg",
|
|
12
|
+
".gif": "image/gif",
|
|
13
|
+
".webp": "image/webp",
|
|
14
|
+
};
|
|
15
|
+
function projectNameFromPath(projectPath) {
|
|
16
|
+
const parts = projectPath.split("/").filter(Boolean);
|
|
17
|
+
return parts.length > 0 ? parts[parts.length - 1] : projectPath;
|
|
18
|
+
}
|
|
19
|
+
export class GalleryStore {
|
|
20
|
+
index = [];
|
|
21
|
+
async init() {
|
|
22
|
+
await mkdir(IMAGES_DIR, { recursive: true });
|
|
23
|
+
try {
|
|
24
|
+
const data = await readFile(INDEX_FILE, "utf-8");
|
|
25
|
+
this.index = JSON.parse(data);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// File doesn't exist or is corrupt — start fresh
|
|
29
|
+
this.index = [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async saveIndex() {
|
|
33
|
+
await writeFile(INDEX_FILE, JSON.stringify(this.index, null, 2), "utf-8");
|
|
34
|
+
}
|
|
35
|
+
async addImage(filePath, projectPath, sessionId) {
|
|
36
|
+
try {
|
|
37
|
+
const st = await stat(filePath);
|
|
38
|
+
if (!st.isFile())
|
|
39
|
+
return null;
|
|
40
|
+
const ext = extname(filePath).toLowerCase();
|
|
41
|
+
const mimeType = MIME_TYPES[ext];
|
|
42
|
+
if (!mimeType)
|
|
43
|
+
return null;
|
|
44
|
+
const id = randomUUID();
|
|
45
|
+
const filename = `${id}${ext}`;
|
|
46
|
+
const destPath = join(IMAGES_DIR, filename);
|
|
47
|
+
await copyFile(filePath, destPath);
|
|
48
|
+
const meta = {
|
|
49
|
+
id,
|
|
50
|
+
filename,
|
|
51
|
+
mimeType,
|
|
52
|
+
projectPath,
|
|
53
|
+
sessionId,
|
|
54
|
+
sourcePath: filePath,
|
|
55
|
+
addedAt: new Date().toISOString(),
|
|
56
|
+
sizeBytes: st.size,
|
|
57
|
+
};
|
|
58
|
+
this.index.push(meta);
|
|
59
|
+
await this.saveIndex();
|
|
60
|
+
console.log(`[gallery] Added image ${id} from ${basename(filePath)}`);
|
|
61
|
+
return meta;
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.warn(`[gallery] Failed to add image ${filePath}:`, err);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Add an image from base64-encoded data.
|
|
70
|
+
* This allows mobile clients to upload images directly without file paths.
|
|
71
|
+
*/
|
|
72
|
+
async addImageFromBase64(base64Data, mimeType, projectPath, sessionId) {
|
|
73
|
+
try {
|
|
74
|
+
// Validate mime type and get extension
|
|
75
|
+
const ext = Object.entries(MIME_TYPES).find(([, mime]) => mime === mimeType)?.[0];
|
|
76
|
+
if (!ext) {
|
|
77
|
+
console.warn(`[gallery] Unsupported mime type: ${mimeType}`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const id = randomUUID();
|
|
81
|
+
const filename = `${id}${ext}`;
|
|
82
|
+
const destPath = join(IMAGES_DIR, filename);
|
|
83
|
+
// Decode base64 and write to file
|
|
84
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
85
|
+
await writeFile(destPath, buffer);
|
|
86
|
+
const meta = {
|
|
87
|
+
id,
|
|
88
|
+
filename,
|
|
89
|
+
mimeType,
|
|
90
|
+
projectPath,
|
|
91
|
+
sessionId,
|
|
92
|
+
sourcePath: "base64_upload",
|
|
93
|
+
addedAt: new Date().toISOString(),
|
|
94
|
+
sizeBytes: buffer.length,
|
|
95
|
+
};
|
|
96
|
+
this.index.push(meta);
|
|
97
|
+
await this.saveIndex();
|
|
98
|
+
console.log(`[gallery] Added image ${id} from base64 (${Math.round(buffer.length / 1024)}KB)`);
|
|
99
|
+
return meta;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
console.warn(`[gallery] Failed to add image from base64:`, err);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
list(options) {
|
|
107
|
+
let items = this.index;
|
|
108
|
+
if (options?.projectPath) {
|
|
109
|
+
items = items.filter((m) => m.projectPath === options.projectPath);
|
|
110
|
+
}
|
|
111
|
+
if (options?.sessionId) {
|
|
112
|
+
items = items.filter((m) => m.sessionId === options.sessionId);
|
|
113
|
+
}
|
|
114
|
+
// Return newest first
|
|
115
|
+
return [...items]
|
|
116
|
+
.sort((a, b) => new Date(b.addedAt).getTime() - new Date(a.addedAt).getTime())
|
|
117
|
+
.map((m) => ({
|
|
118
|
+
id: m.id,
|
|
119
|
+
url: `/api/gallery/${m.id}`,
|
|
120
|
+
mimeType: m.mimeType,
|
|
121
|
+
projectPath: m.projectPath,
|
|
122
|
+
projectName: projectNameFromPath(m.projectPath),
|
|
123
|
+
sessionId: m.sessionId,
|
|
124
|
+
addedAt: m.addedAt,
|
|
125
|
+
sizeBytes: m.sizeBytes,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
getImagePath(id) {
|
|
129
|
+
const meta = this.index.find((m) => m.id === id);
|
|
130
|
+
if (!meta)
|
|
131
|
+
return null;
|
|
132
|
+
return join(IMAGES_DIR, meta.filename);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get image as Base64 for SDK message embedding.
|
|
136
|
+
* Returns null if image not found.
|
|
137
|
+
*/
|
|
138
|
+
async getImageAsBase64(id) {
|
|
139
|
+
const meta = this.index.find((m) => m.id === id);
|
|
140
|
+
if (!meta)
|
|
141
|
+
return null;
|
|
142
|
+
const filePath = join(IMAGES_DIR, meta.filename);
|
|
143
|
+
try {
|
|
144
|
+
const buffer = await readFile(filePath);
|
|
145
|
+
return {
|
|
146
|
+
base64: buffer.toString("base64"),
|
|
147
|
+
mimeType: meta.mimeType,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get mime type for an image by ID.
|
|
156
|
+
*/
|
|
157
|
+
getMimeType(id) {
|
|
158
|
+
const meta = this.index.find((m) => m.id === id);
|
|
159
|
+
return meta?.mimeType ?? null;
|
|
160
|
+
}
|
|
161
|
+
async delete(id) {
|
|
162
|
+
const idx = this.index.findIndex((m) => m.id === id);
|
|
163
|
+
if (idx === -1)
|
|
164
|
+
return false;
|
|
165
|
+
const meta = this.index[idx];
|
|
166
|
+
const filePath = join(IMAGES_DIR, meta.filename);
|
|
167
|
+
try {
|
|
168
|
+
await unlink(filePath);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// File may already be deleted
|
|
172
|
+
}
|
|
173
|
+
this.index.splice(idx, 1);
|
|
174
|
+
await this.saveIndex();
|
|
175
|
+
console.log(`[gallery] Deleted image ${id}`);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Handle HTTP requests for gallery image serving.
|
|
180
|
+
* Returns true if the request was handled.
|
|
181
|
+
*/
|
|
182
|
+
handleRequest(req, res) {
|
|
183
|
+
const url = req.url ?? "";
|
|
184
|
+
// Match /api/gallery/:id (alphanumeric, hyphens, underscores)
|
|
185
|
+
const imageMatch = url.match(/^\/api\/gallery\/([a-zA-Z0-9_-]+)$/);
|
|
186
|
+
// GET /api/gallery/:id — serve image file
|
|
187
|
+
if (imageMatch && req.method === "GET") {
|
|
188
|
+
const id = imageMatch[1];
|
|
189
|
+
return this.serveImage(id, res);
|
|
190
|
+
}
|
|
191
|
+
// DELETE /api/gallery/:id
|
|
192
|
+
if (imageMatch && req.method === "DELETE") {
|
|
193
|
+
const id = imageMatch[1];
|
|
194
|
+
this.delete(id).then((ok) => {
|
|
195
|
+
if (ok) {
|
|
196
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
197
|
+
res.end(JSON.stringify({ deleted: true }));
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
201
|
+
res.end("Not Found");
|
|
202
|
+
}
|
|
203
|
+
}).catch(() => {
|
|
204
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
205
|
+
res.end("Internal Server Error");
|
|
206
|
+
});
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
// GET /api/gallery — list images (exact path or with query string)
|
|
210
|
+
if ((url === "/api/gallery" || url.startsWith("/api/gallery?")) && req.method === "GET") {
|
|
211
|
+
const parsedUrl = new URL(url, `http://${req.headers.host ?? "localhost"}`);
|
|
212
|
+
const project = parsedUrl.searchParams.get("project") ?? undefined;
|
|
213
|
+
const images = this.list({ projectPath: project });
|
|
214
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
215
|
+
res.end(JSON.stringify({ images }));
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
serveImage(id, res) {
|
|
221
|
+
const meta = this.index.find((m) => m.id === id);
|
|
222
|
+
if (!meta) {
|
|
223
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
224
|
+
res.end("Not Found");
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
const filePath = join(IMAGES_DIR, meta.filename);
|
|
228
|
+
readFile(filePath)
|
|
229
|
+
.then((buffer) => {
|
|
230
|
+
res.writeHead(200, {
|
|
231
|
+
"Content-Type": meta.mimeType,
|
|
232
|
+
"Content-Length": buffer.length,
|
|
233
|
+
"Cache-Control": "public, max-age=3600",
|
|
234
|
+
});
|
|
235
|
+
res.end(buffer);
|
|
236
|
+
})
|
|
237
|
+
.catch(() => {
|
|
238
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
239
|
+
res.end("Not Found");
|
|
240
|
+
});
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Handle POST /api/gallery/upload.
|
|
245
|
+
* Accepts JSON body with EITHER:
|
|
246
|
+
* - { filePath: string, projectPath: string, sessionId?: string } (file path mode)
|
|
247
|
+
* - { base64: string, mimeType: string, projectPath: string, sessionId?: string } (base64 mode)
|
|
248
|
+
* Returns true if the request was handled.
|
|
249
|
+
*/
|
|
250
|
+
handleUploadRequest(req, res, onNewImage) {
|
|
251
|
+
const url = req.url ?? "";
|
|
252
|
+
if (url !== "/api/gallery/upload" || req.method !== "POST")
|
|
253
|
+
return false;
|
|
254
|
+
let body = "";
|
|
255
|
+
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
256
|
+
req.on("end", async () => {
|
|
257
|
+
try {
|
|
258
|
+
const parsed = JSON.parse(body);
|
|
259
|
+
if (!parsed.projectPath) {
|
|
260
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
261
|
+
res.end(JSON.stringify({ error: "projectPath is required" }));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
let meta = null;
|
|
265
|
+
// Base64 mode: save from base64 data
|
|
266
|
+
if (parsed.base64 && parsed.mimeType) {
|
|
267
|
+
meta = await this.addImageFromBase64(parsed.base64, parsed.mimeType, parsed.projectPath, parsed.sessionId);
|
|
268
|
+
}
|
|
269
|
+
// File path mode: copy from file path
|
|
270
|
+
else if (parsed.filePath) {
|
|
271
|
+
meta = await this.addImage(parsed.filePath, parsed.projectPath, parsed.sessionId);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
275
|
+
res.end(JSON.stringify({ error: "Either filePath or (base64 + mimeType) is required" }));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (!meta) {
|
|
279
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
280
|
+
res.end(JSON.stringify({ error: "Failed to add image (unsupported format or invalid data)" }));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const info = this.metaToInfo(meta);
|
|
284
|
+
if (onNewImage)
|
|
285
|
+
onNewImage(meta);
|
|
286
|
+
res.writeHead(201, { "Content-Type": "application/json" });
|
|
287
|
+
res.end(JSON.stringify({ image: info }));
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
291
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
/** Convert GalleryImageMeta to GalleryImageInfo for WS broadcast. */
|
|
297
|
+
metaToInfo(meta) {
|
|
298
|
+
return {
|
|
299
|
+
id: meta.id,
|
|
300
|
+
url: `/api/gallery/${meta.id}`,
|
|
301
|
+
mimeType: meta.mimeType,
|
|
302
|
+
projectPath: meta.projectPath,
|
|
303
|
+
projectName: projectNameFromPath(meta.projectPath),
|
|
304
|
+
sessionId: meta.sessionId,
|
|
305
|
+
addedAt: meta.addedAt,
|
|
306
|
+
sizeBytes: meta.sizeBytes,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=gallery-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gallery-store.js","sourceRoot":"","sources":["../src/gallery-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
export interface ImageRef {
|
|
3
|
+
id: string;
|
|
4
|
+
url: string;
|
|
5
|
+
mimeType: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class ImageStore {
|
|
8
|
+
private store;
|
|
9
|
+
/** Evict least-recently-used entries if store exceeds MAX_ENTRIES. */
|
|
10
|
+
private evictLRU;
|
|
11
|
+
/** Extract local image file paths from text (ignores URLs). */
|
|
12
|
+
extractImagePaths(text: unknown): string[];
|
|
13
|
+
/** Register an image from raw base64 data. Returns an ImageRef with a URL for HTTP access. */
|
|
14
|
+
registerFromBase64(base64Data: string, mimeType: string): ImageRef | null;
|
|
15
|
+
/** Read files from disk, assign UUIDs, store in memory. */
|
|
16
|
+
registerImages(paths: string[]): Promise<ImageRef[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Handle HTTP request for image serving.
|
|
19
|
+
* Returns true if the request was handled, false otherwise.
|
|
20
|
+
*/
|
|
21
|
+
handleRequest(req: IncomingMessage, res: ServerResponse): boolean;
|
|
22
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { extname } from "node:path";
|
|
4
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
5
|
+
const MAX_ENTRIES = 100;
|
|
6
|
+
const MIME_TYPES = {
|
|
7
|
+
".png": "image/png",
|
|
8
|
+
".jpg": "image/jpeg",
|
|
9
|
+
".jpeg": "image/jpeg",
|
|
10
|
+
".gif": "image/gif",
|
|
11
|
+
".webp": "image/webp",
|
|
12
|
+
};
|
|
13
|
+
// Matches absolute paths ending with image extensions
|
|
14
|
+
const IMAGE_PATH_RE = /(\/[\w./_-]+\.(?:png|jpe?g|gif|webp))/gi;
|
|
15
|
+
export class ImageStore {
|
|
16
|
+
store = new Map();
|
|
17
|
+
/** Evict least-recently-used entries if store exceeds MAX_ENTRIES. */
|
|
18
|
+
evictLRU() {
|
|
19
|
+
while (this.store.size > MAX_ENTRIES) {
|
|
20
|
+
let oldestId = null;
|
|
21
|
+
let oldestTime = Infinity;
|
|
22
|
+
for (const [key, val] of this.store) {
|
|
23
|
+
if (val.accessedAt < oldestTime) {
|
|
24
|
+
oldestTime = val.accessedAt;
|
|
25
|
+
oldestId = key;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (oldestId)
|
|
29
|
+
this.store.delete(oldestId);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Extract local image file paths from text (ignores URLs). */
|
|
33
|
+
extractImagePaths(text) {
|
|
34
|
+
const str = typeof text === "string" ? text : JSON.stringify(text ?? "");
|
|
35
|
+
const matches = str.match(IMAGE_PATH_RE);
|
|
36
|
+
if (!matches)
|
|
37
|
+
return [];
|
|
38
|
+
// Filter out URLs (paths starting with //) and deduplicate
|
|
39
|
+
const localPaths = matches.filter((p) => !p.startsWith("//"));
|
|
40
|
+
return [...new Set(localPaths)];
|
|
41
|
+
}
|
|
42
|
+
/** Register an image from raw base64 data. Returns an ImageRef with a URL for HTTP access. */
|
|
43
|
+
registerFromBase64(base64Data, mimeType) {
|
|
44
|
+
try {
|
|
45
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
46
|
+
if (buffer.length > MAX_FILE_SIZE) {
|
|
47
|
+
console.warn(`[image-store] Skipping base64 image (>10MB)`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const id = randomUUID();
|
|
51
|
+
this.store.set(id, { id, mimeType, buffer, accessedAt: Date.now() });
|
|
52
|
+
this.evictLRU();
|
|
53
|
+
return { id, url: `/images/${id}`, mimeType };
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
console.warn(`[image-store] Failed to register base64 image:`, err);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Read files from disk, assign UUIDs, store in memory. */
|
|
61
|
+
async registerImages(paths) {
|
|
62
|
+
const refs = [];
|
|
63
|
+
for (const filePath of paths) {
|
|
64
|
+
try {
|
|
65
|
+
const st = await stat(filePath);
|
|
66
|
+
if (!st.isFile() || st.size > MAX_FILE_SIZE) {
|
|
67
|
+
console.warn(`[image-store] Skipping ${filePath} (not file or >10MB)`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const ext = extname(filePath).toLowerCase();
|
|
71
|
+
const mimeType = MIME_TYPES[ext];
|
|
72
|
+
if (!mimeType)
|
|
73
|
+
continue;
|
|
74
|
+
const buffer = await readFile(filePath);
|
|
75
|
+
const id = randomUUID();
|
|
76
|
+
this.store.set(id, { id, mimeType, buffer, accessedAt: Date.now() });
|
|
77
|
+
const ref = { id, url: `/images/${id}`, mimeType };
|
|
78
|
+
refs.push(ref);
|
|
79
|
+
this.evictLRU();
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
console.warn(`[image-store] Failed to read ${filePath}:`, err);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return refs;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Handle HTTP request for image serving.
|
|
89
|
+
* Returns true if the request was handled, false otherwise.
|
|
90
|
+
*/
|
|
91
|
+
handleRequest(req, res) {
|
|
92
|
+
const url = req.url ?? "";
|
|
93
|
+
const match = url.match(/^\/images\/([a-f0-9-]+)$/);
|
|
94
|
+
if (!match)
|
|
95
|
+
return false;
|
|
96
|
+
const id = match[1];
|
|
97
|
+
const entry = this.store.get(id);
|
|
98
|
+
if (!entry) {
|
|
99
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
100
|
+
res.end("Not Found");
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
entry.accessedAt = Date.now();
|
|
104
|
+
res.writeHead(200, {
|
|
105
|
+
"Content-Type": entry.mimeType,
|
|
106
|
+
"Content-Length": entry.buffer.length,
|
|
107
|
+
"Cache-Control": "public, max-age=3600",
|
|
108
|
+
});
|
|
109
|
+
res.end(entry.buffer);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=image-store.js.map
|