@kimuson/claude-code-viewer 0.6.0-beta.1 → 0.6.0-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/README.md CHANGED
@@ -168,6 +168,15 @@ Claude Code Viewer can be configured using command-line options or environment v
168
168
  - `CLAUDE_CODE_VIEWER_CC_EXECUTABLE_PATH` → `CCV_CC_EXECUTABLE_PATH`
169
169
  - New environment variable added: `CCV_GLOBAL_CLAUDE_DIR` (previously the Claude directory path was hardcoded to `~/.claude`)
170
170
 
171
+ ### API Authentication
172
+
173
+ When `--password` / `CCV_PASSWORD` is set, all `/api` routes (except auth-related endpoints) require authentication. You can authenticate in two ways:
174
+
175
+ 1. **Session cookie**: Use the `/api/auth/login` endpoint to set the `ccv-session` cookie, then include it in subsequent requests.
176
+ 2. **Authorization header**: Send `Authorization: Bearer <password>` on each request.
177
+
178
+ If no password is configured, API authentication is disabled.
179
+
171
180
  ### User Settings
172
181
 
173
182
  Settings can be configured from the sidebar in Claude Code Viewer.
package/dist/main.js CHANGED
@@ -7,7 +7,7 @@ import { Effect as Effect65 } from "effect";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@kimuson/claude-code-viewer",
10
- version: "0.6.0-beta.1",
10
+ version: "0.6.0-beta.2",
11
11
  description: "A full-featured web-based Claude Code client that provides complete interactive functionality for managing Claude Code projects.",
12
12
  type: "module",
13
13
  license: "MIT",
@@ -39,7 +39,7 @@ var package_default = {
39
39
  fix: "run-s 'fix:*'",
40
40
  "fix:biome-format": "biome format --write .",
41
41
  "fix:biome-lint": "biome check --write --unsafe .",
42
- typecheck: "tsc --noEmit",
42
+ typecheck: "tsgo --noEmit",
43
43
  test: "vitest --run",
44
44
  "test:watch": "vitest",
45
45
  e2e: "./scripts/e2e/exec_e2e.sh",
@@ -76,6 +76,7 @@ var package_default = {
76
76
  "@radix-ui/react-slot": "1.2.4",
77
77
  "@radix-ui/react-tabs": "1.1.13",
78
78
  "@radix-ui/react-tooltip": "1.2.8",
79
+ "@replit/ruspty": "^3.6.0",
79
80
  "@tailwindcss/vite": "4.1.18",
80
81
  "@tanstack/react-devtools": "0.9.2",
81
82
  "@tanstack/react-query": "5.90.20",
@@ -107,7 +108,6 @@ var package_default = {
107
108
  sonner: "2.0.7",
108
109
  "tailwind-merge": "3.4.0",
109
110
  ulid: "3.0.2",
110
- "node-pty": "1.1.0-beta34",
111
111
  ws: "8.18.3",
112
112
  zod: "4.3.6"
113
113
  },
@@ -127,6 +127,7 @@ var package_default = {
127
127
  "@types/react-dom": "19.2.3",
128
128
  "@types/react-syntax-highlighter": "15.5.13",
129
129
  "@types/ws": "8.18.0",
130
+ "@typescript/native-preview": "7.0.0-dev.20260207.1",
130
131
  "@vitejs/plugin-react-swc": "4.2.2",
131
132
  dotenv: "17.2.3",
132
133
  esbuild: "0.27.2",
@@ -8758,6 +8759,41 @@ var TasksController = class extends Context37.Tag("TasksController")() {
8758
8759
  // src/server/core/terminal/TerminalService.ts
8759
8760
  import { Context as Context38, Effect as Effect48, Layer as Layer40 } from "effect";
8760
8761
  import { ulid as ulid5 } from "ulid";
8762
+
8763
+ // src/server/core/terminal/normalizePtyChunk.ts
8764
+ var normalizePtyChunk = (chunk) => {
8765
+ if (typeof chunk === "string") {
8766
+ return chunk;
8767
+ }
8768
+ if (chunk instanceof Uint8Array) {
8769
+ return Buffer.from(chunk).toString("utf8");
8770
+ }
8771
+ if (chunk instanceof ArrayBuffer) {
8772
+ return Buffer.from(chunk).toString("utf8");
8773
+ }
8774
+ return null;
8775
+ };
8776
+
8777
+ // src/server/core/terminal/rusptyAdapter.ts
8778
+ var createRusptySession = (ruspty, options) => {
8779
+ const pty = new ruspty.Pty(options);
8780
+ return {
8781
+ process: {
8782
+ write: (data) => {
8783
+ pty.write.write(data);
8784
+ },
8785
+ resize: (cols, rows) => {
8786
+ pty.resize({ cols, rows });
8787
+ },
8788
+ kill: () => {
8789
+ pty.close();
8790
+ }
8791
+ },
8792
+ read: pty.read
8793
+ };
8794
+ };
8795
+
8796
+ // src/server/core/terminal/TerminalService.ts
8761
8797
  var DEFAULT_COLS = 80;
8762
8798
  var DEFAULT_ROWS = 24;
8763
8799
  var MAX_BUFFER_BYTES = 1024 * 1024 * 2;
@@ -8794,9 +8830,9 @@ var LayerImpl30 = Effect48.gen(function* () {
8794
8830
  snapshotSince: () => Effect48.succeed(void 0)
8795
8831
  };
8796
8832
  };
8797
- const nodePty = yield* Effect48.tryPromise({
8798
- try: () => import("node-pty"),
8799
- catch: (error) => new Error(`Failed to load node-pty: ${String(error)}`)
8833
+ const ruspty = yield* Effect48.tryPromise({
8834
+ try: () => import("@replit/ruspty"),
8835
+ catch: (error) => new Error(`Failed to load @replit/ruspty: ${String(error)}`)
8800
8836
  }).pipe(
8801
8837
  Effect48.catchAll(
8802
8838
  (error) => Effect48.sync(() => {
@@ -8805,10 +8841,9 @@ var LayerImpl30 = Effect48.gen(function* () {
8805
8841
  })
8806
8842
  )
8807
8843
  );
8808
- if (!nodePty) {
8809
- return disabledService("node-pty failed to load");
8844
+ if (!ruspty) {
8845
+ return disabledService("@replit/ruspty failed to load");
8810
8846
  }
8811
- const { spawn } = nodePty;
8812
8847
  const trimBuffer = (session) => {
8813
8848
  while (session.bufferBytes > MAX_BUFFER_BYTES && session.buffer.length > 0) {
8814
8849
  const removed = session.buffer.shift();
@@ -8864,16 +8899,26 @@ var LayerImpl30 = Effect48.gen(function* () {
8864
8899
  options.fallbackShell,
8865
8900
  options.unrestrictedFlag
8866
8901
  );
8867
- const pty = spawn(shell.command, shell.args, {
8868
- name: "xterm-color",
8869
- cols: DEFAULT_COLS,
8870
- rows: DEFAULT_ROWS,
8871
- cwd: options.cwd,
8872
- env: options.env
8902
+ const { process: ptyProcess, read } = createRusptySession(ruspty, {
8903
+ command: shell.command,
8904
+ args: shell.args,
8905
+ envs: options.env,
8906
+ dir: options.cwd,
8907
+ size: { cols: DEFAULT_COLS, rows: DEFAULT_ROWS },
8908
+ onExit: (_error, exitCode) => {
8909
+ const current = sessions.get(id);
8910
+ if (!current) return;
8911
+ current.exited = true;
8912
+ current.lastActivity = Date.now();
8913
+ broadcast(current, { type: "exit", code: exitCode });
8914
+ if (current.clients.size === 0) {
8915
+ destroySession(current.id);
8916
+ }
8917
+ }
8873
8918
  });
8874
8919
  const session = {
8875
8920
  id,
8876
- pty,
8921
+ pty: ptyProcess,
8877
8922
  seq: 0,
8878
8923
  buffer: [],
8879
8924
  bufferBytes: 0,
@@ -8882,27 +8927,22 @@ var LayerImpl30 = Effect48.gen(function* () {
8882
8927
  exited: false,
8883
8928
  inputBuffer: ""
8884
8929
  };
8885
- pty.onData((data) => {
8886
- if (session.exited) return;
8887
- session.lastActivity = Date.now();
8888
- session.seq += 1;
8889
- session.buffer.push({ seq: session.seq, data });
8890
- session.bufferBytes += Buffer.byteLength(data, "utf8");
8891
- trimBuffer(session);
8892
- broadcast(session, {
8930
+ read.on("data", (chunk) => {
8931
+ const data = normalizePtyChunk(chunk);
8932
+ if (data === null) return;
8933
+ const current = sessions.get(session.id);
8934
+ if (!current || current.exited) return;
8935
+ current.lastActivity = Date.now();
8936
+ current.seq += 1;
8937
+ current.buffer.push({ seq: current.seq, data });
8938
+ current.bufferBytes += Buffer.byteLength(data, "utf8");
8939
+ trimBuffer(current);
8940
+ broadcast(current, {
8893
8941
  type: "output",
8894
- seq: session.seq,
8942
+ seq: current.seq,
8895
8943
  data
8896
8944
  });
8897
8945
  });
8898
- pty.onExit((event) => {
8899
- session.exited = true;
8900
- session.lastActivity = Date.now();
8901
- broadcast(session, { type: "exit", code: event.exitCode });
8902
- if (session.clients.size === 0) {
8903
- destroySession(session.id);
8904
- }
8905
- });
8906
8946
  sessions.set(id, session);
8907
8947
  return session;
8908
8948
  };
@@ -9139,7 +9179,15 @@ var generateSessionToken = (password) => {
9139
9179
  if (!password) return "";
9140
9180
  return Buffer.from(`ccv-session:${password}`).toString("base64");
9141
9181
  };
9142
- var createAuthRequiredMiddleware = (authEnabled, validSessionToken) => {
9182
+ var getBearerToken = (authorization) => {
9183
+ if (!authorization) return void 0;
9184
+ const [scheme, token] = authorization.split(" ");
9185
+ if (!scheme || !token) return void 0;
9186
+ if (scheme.toLowerCase() !== "bearer") return void 0;
9187
+ const trimmedToken = token.trim();
9188
+ return trimmedToken.length > 0 ? trimmedToken : void 0;
9189
+ };
9190
+ var createAuthRequiredMiddleware = (authEnabled, validSessionToken, authPassword) => {
9143
9191
  return createMiddleware(async (c, next) => {
9144
9192
  if (!c.req.path.startsWith("/api")) {
9145
9193
  return next();
@@ -9148,7 +9196,10 @@ var createAuthRequiredMiddleware = (authEnabled, validSessionToken) => {
9148
9196
  return next();
9149
9197
  }
9150
9198
  const sessionToken = getCookie(c, "ccv-session");
9151
- if (!sessionToken || sessionToken !== validSessionToken) {
9199
+ const bearerToken = getBearerToken(c.req.header("Authorization"));
9200
+ const cookieAuthorized = sessionToken === validSessionToken;
9201
+ const bearerAuthorized = authPassword !== void 0 && bearerToken === authPassword;
9202
+ if (!cookieAuthorized && !bearerAuthorized) {
9152
9203
  return c.json({ error: "Unauthorized" }, 401);
9153
9204
  }
9154
9205
  await next();
@@ -9170,11 +9221,12 @@ var LayerImpl31 = Effect50.gen(function* () {
9170
9221
  if (!c.req.path.startsWith("/api")) {
9171
9222
  return next();
9172
9223
  }
9173
- const { authEnabled, validSessionToken } = await runPromise(getAuthState);
9174
- return createAuthRequiredMiddleware(authEnabled, validSessionToken)(
9175
- c,
9176
- next
9177
- );
9224
+ const { authEnabled, validSessionToken, authPassword } = await runPromise(getAuthState);
9225
+ return createAuthRequiredMiddleware(
9226
+ authEnabled,
9227
+ validSessionToken,
9228
+ authPassword
9229
+ )(c, next);
9178
9230
  }
9179
9231
  );
9180
9232
  return {