shiva-code 0.4.3 → 0.5.1

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.
@@ -21,8 +21,8 @@ import {
21
21
  parseGitHubUrl,
22
22
  runGh,
23
23
  runGhRaw
24
- } from "./chunk-ZDLLPNCK.js";
25
- import "./chunk-6RAACMKF.js";
24
+ } from "./chunk-IVFCZLBX.js";
25
+ import "./chunk-HIQO2DBA.js";
26
26
  import "./chunk-3RG5ZIWI.js";
27
27
  export {
28
28
  findMentionedIssueNumbers,
@@ -1,14 +1,90 @@
1
1
  import {
2
2
  cache
3
- } from "./chunk-6RAACMKF.js";
3
+ } from "./chunk-HIQO2DBA.js";
4
4
 
5
5
  // src/services/session/manager.ts
6
6
  import * as fs from "fs";
7
+ import * as fsPromises from "fs/promises";
8
+ import * as path2 from "path";
9
+ import * as os2 from "os";
10
+ import * as readline from "readline";
11
+
12
+ // src/utils/sanitize.ts
7
13
  import * as path from "path";
8
14
  import * as os from "os";
9
- import * as readline from "readline";
15
+ function isPathSafe(inputPath, allowedBase) {
16
+ try {
17
+ const resolved = path.resolve(inputPath);
18
+ if (inputPath.includes("\0")) {
19
+ return false;
20
+ }
21
+ if (allowedBase) {
22
+ const resolvedBase = path.resolve(allowedBase);
23
+ const relative2 = path.relative(resolvedBase, resolved);
24
+ if (relative2.startsWith("..") || path.isAbsolute(relative2)) {
25
+ return false;
26
+ }
27
+ }
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+ function isPathWithinAllowedBoundaries(inputPath) {
34
+ const resolved = path.resolve(inputPath);
35
+ const homeDir = os.homedir();
36
+ if (resolved.startsWith(homeDir)) {
37
+ return true;
38
+ }
39
+ const allowedPrefixes = [
40
+ "/tmp",
41
+ "/var/tmp",
42
+ homeDir
43
+ ];
44
+ return allowedPrefixes.some((prefix) => resolved.startsWith(prefix));
45
+ }
46
+ function isValidSessionId(sessionId) {
47
+ if (!sessionId || typeof sessionId !== "string") {
48
+ return false;
49
+ }
50
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
51
+ return uuidRegex.test(sessionId);
52
+ }
53
+ function isValidProjectPath(projectPath) {
54
+ if (!projectPath || typeof projectPath !== "string") {
55
+ return false;
56
+ }
57
+ if (!path.isAbsolute(projectPath)) {
58
+ return false;
59
+ }
60
+ if (projectPath.includes("\0")) {
61
+ return false;
62
+ }
63
+ const dangerousForPath = /[;&|`$(){}[\]<>\\!#*?"'\n\r\t]/;
64
+ if (dangerousForPath.test(projectPath)) {
65
+ return false;
66
+ }
67
+ return true;
68
+ }
69
+ function maskSecret(secret) {
70
+ if (!secret || secret.length < 8) {
71
+ return "********";
72
+ }
73
+ const visibleStart = secret.substring(0, 2);
74
+ const visibleEnd = secret.substring(secret.length - 2);
75
+ const maskedLength = Math.min(secret.length - 4, 20);
76
+ return `${visibleStart}${"*".repeat(maskedLength)}${visibleEnd}`;
77
+ }
78
+ function sanitizeForLog(input) {
79
+ if (!input || typeof input !== "string") {
80
+ return "[invalid]";
81
+ }
82
+ return input.replace(/[\x00-\x1F\x7F]/g, "").substring(0, 500);
83
+ }
84
+
85
+ // src/services/session/manager.ts
10
86
  function getClaudeProjectsPath() {
11
- return path.join(os.homedir(), ".claude", "projects");
87
+ return path2.join(os2.homedir(), ".claude", "projects");
12
88
  }
13
89
  function encodeProjectPath(projectPath) {
14
90
  return projectPath.replace(/\//g, "-").replace(/^-/, "");
@@ -18,7 +94,7 @@ function decodeProjectPath(encoded) {
18
94
  return "/" + parts.join("/");
19
95
  }
20
96
  function getProjectName(projectPath) {
21
- return path.basename(projectPath);
97
+ return path2.basename(projectPath);
22
98
  }
23
99
  async function getAllClaudeProjects(skipCache = false) {
24
100
  if (!skipCache) {
@@ -31,26 +107,25 @@ async function getAllClaudeProjects(skipCache = false) {
31
107
  if (!fs.existsSync(projectsPath)) {
32
108
  return [];
33
109
  }
34
- const projects = [];
35
- const entries = fs.readdirSync(projectsPath, { withFileTypes: true });
36
- for (const entry of entries) {
37
- if (!entry.isDirectory()) continue;
110
+ const entries = await fsPromises.readdir(projectsPath, { withFileTypes: true });
111
+ const projectPromises = entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
38
112
  const encodedPath = entry.name;
39
113
  const absolutePath = decodeProjectPath(encodedPath);
40
114
  const projectName = getProjectName(absolutePath);
41
- const sessionIndex = getSessionIndex(encodedPath);
115
+ const sessionIndex = await getSessionIndexAsync(encodedPath);
42
116
  const sessions = sessionIndex?.entries || [];
43
117
  sessions.sort(
44
118
  (a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime()
45
119
  );
46
- projects.push({
120
+ return {
47
121
  encodedPath,
48
122
  absolutePath,
49
123
  projectName,
50
124
  sessions,
51
125
  latestSession: sessions[0]
52
- });
53
- }
126
+ };
127
+ });
128
+ const projects = await Promise.all(projectPromises);
54
129
  projects.sort((a, b) => {
55
130
  const aTime = a.latestSession ? new Date(a.latestSession.modified).getTime() : 0;
56
131
  const bTime = b.latestSession ? new Date(b.latestSession.modified).getTime() : 0;
@@ -63,11 +138,12 @@ function invalidateSessionsCache() {
63
138
  cache.invalidateSessions();
64
139
  }
65
140
  function getSessionIndex(encodedPath) {
66
- const indexPath = path.join(
67
- getClaudeProjectsPath(),
68
- encodedPath,
69
- "sessions-index.json"
70
- );
141
+ const projectsPath = getClaudeProjectsPath();
142
+ const indexPath = path2.join(projectsPath, encodedPath, "sessions-index.json");
143
+ if (!isPathSafe(indexPath, projectsPath)) {
144
+ console.error(`[Security] Invalid path detected: ${sanitizeForLog(encodedPath)}`);
145
+ return null;
146
+ }
71
147
  if (!fs.existsSync(indexPath)) {
72
148
  return null;
73
149
  }
@@ -78,9 +154,33 @@ function getSessionIndex(encodedPath) {
78
154
  return null;
79
155
  }
80
156
  }
81
- async function findProject(nameOrPath) {
82
- const projects = await getAllClaudeProjects();
83
- const absolutePath = path.resolve(nameOrPath);
157
+ async function getSessionIndexAsync(encodedPath) {
158
+ const projectsPath = getClaudeProjectsPath();
159
+ const indexPath = path2.join(projectsPath, encodedPath, "sessions-index.json");
160
+ if (!isPathSafe(indexPath, projectsPath)) {
161
+ console.error(`[Security] Invalid path detected: ${sanitizeForLog(encodedPath)}`);
162
+ return null;
163
+ }
164
+ try {
165
+ const content = await fsPromises.readFile(indexPath, "utf-8");
166
+ return JSON.parse(content);
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+ async function findProject(nameOrPath, useCache = true) {
172
+ const projects = await getAllClaudeProjects(!useCache);
173
+ const absolutePath = path2.resolve(nameOrPath);
174
+ const byPath = projects.find((p) => p.absolutePath === absolutePath);
175
+ if (byPath) return byPath;
176
+ const lowerName = nameOrPath.toLowerCase();
177
+ const byName = projects.find(
178
+ (p) => p.projectName.toLowerCase() === lowerName || p.projectName.toLowerCase().includes(lowerName)
179
+ );
180
+ return byName || null;
181
+ }
182
+ function findProjectFromArray(projects, nameOrPath) {
183
+ const absolutePath = path2.resolve(nameOrPath);
84
184
  const byPath = projects.find((p) => p.absolutePath === absolutePath);
85
185
  if (byPath) return byPath;
86
186
  const lowerName = nameOrPath.toLowerCase();
@@ -103,6 +203,15 @@ async function findProjectForCurrentDir() {
103
203
  function getSessionFilePath(session) {
104
204
  return session.fullPath;
105
205
  }
206
+ function isSessionCorruptedQuick(session) {
207
+ const filePath = getSessionFilePath(session);
208
+ try {
209
+ const stats = fs.statSync(filePath);
210
+ return stats.size > 100 * 1024 * 1024;
211
+ } catch {
212
+ return true;
213
+ }
214
+ }
106
215
  function isSessionCorrupted(session) {
107
216
  const filePath = getSessionFilePath(session);
108
217
  if (!fs.existsSync(filePath)) {
@@ -141,10 +250,15 @@ function isSessionActive(session) {
141
250
  }
142
251
  async function parseSessionFile(jsonlPath, level = "standard") {
143
252
  const summaries = [];
253
+ const resolvedPath = path2.resolve(jsonlPath);
254
+ if (!isPathWithinAllowedBoundaries(resolvedPath)) {
255
+ console.error(`[Security] Path outside allowed boundaries: ${sanitizeForLog(jsonlPath)}`);
256
+ return summaries;
257
+ }
144
258
  if (!fs.existsSync(jsonlPath)) {
145
259
  return summaries;
146
260
  }
147
- return new Promise((resolve2) => {
261
+ return new Promise((resolve3) => {
148
262
  const fileStream = fs.createReadStream(jsonlPath, { encoding: "utf-8" });
149
263
  const rl = readline.createInterface({
150
264
  input: fileStream,
@@ -181,10 +295,10 @@ async function parseSessionFile(jsonlPath, level = "standard") {
181
295
  }
182
296
  });
183
297
  rl.on("close", () => {
184
- resolve2(summaries);
298
+ resolve3(summaries);
185
299
  });
186
300
  rl.on("error", () => {
187
- resolve2(summaries);
301
+ resolve3(summaries);
188
302
  });
189
303
  });
190
304
  }
@@ -229,14 +343,21 @@ function formatRecoveredContextAsMarkdown(context) {
229
343
  return lines.join("\n");
230
344
  }
231
345
  function saveRecoveredContext(context, outputDir) {
232
- const dir = outputDir || path.join(os.homedir(), ".shiva", "recovery");
233
- if (!fs.existsSync(dir)) {
234
- fs.mkdirSync(dir, { recursive: true });
346
+ const dir = outputDir || path2.join(os2.homedir(), ".shiva", "recovery");
347
+ const resolvedDir = path2.resolve(dir);
348
+ if (!isPathWithinAllowedBoundaries(resolvedDir)) {
349
+ throw new Error(`Output directory outside allowed boundaries: ${sanitizeForLog(dir)}`);
350
+ }
351
+ if (!fs.existsSync(resolvedDir)) {
352
+ fs.mkdirSync(resolvedDir, { recursive: true });
235
353
  }
236
354
  const projectSlug = getProjectName(context.projectPath).toLowerCase().replace(/[^a-z0-9]+/g, "-");
237
355
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
238
356
  const filename = `${projectSlug}-${date}.md`;
239
- const filepath = path.join(dir, filename);
357
+ const filepath = path2.join(resolvedDir, filename);
358
+ if (!isPathWithinAllowedBoundaries(filepath)) {
359
+ throw new Error(`Output path outside allowed boundaries: ${sanitizeForLog(filepath)}`);
360
+ }
240
361
  const content = formatRecoveredContextAsMarkdown(context);
241
362
  fs.writeFileSync(filepath, content, "utf-8");
242
363
  return filepath;
@@ -281,6 +402,10 @@ function formatDate(date) {
281
402
  }
282
403
 
283
404
  export {
405
+ isValidSessionId,
406
+ isValidProjectPath,
407
+ maskSecret,
408
+ sanitizeForLog,
284
409
  getClaudeProjectsPath,
285
410
  encodeProjectPath,
286
411
  decodeProjectPath,
@@ -288,10 +413,13 @@ export {
288
413
  getAllClaudeProjects,
289
414
  invalidateSessionsCache,
290
415
  getSessionIndex,
416
+ getSessionIndexAsync,
291
417
  findProject,
418
+ findProjectFromArray,
292
419
  findSessionByBranch,
293
420
  findProjectForCurrentDir,
294
421
  getSessionFilePath,
422
+ isSessionCorruptedQuick,
295
423
  isSessionCorrupted,
296
424
  isSessionActive,
297
425
  parseSessionFile,
@@ -3,8 +3,8 @@ var CacheService = class _CacheService {
3
3
  cache;
4
4
  // Default TTLs in milliseconds
5
5
  static TTL = {
6
- SESSIONS: 30 * 1e3,
7
- // 30 seconds for sessions
6
+ SESSIONS: 5 * 60 * 1e3,
7
+ // 5 minutes for sessions (optimized for better cache hit rate)
8
8
  GITHUB_ISSUES: 5 * 60 * 1e3,
9
9
  // 5 minutes for GitHub issues
10
10
  GITHUB_PRS: 5 * 60 * 1e3,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  cache
3
- } from "./chunk-6RAACMKF.js";
3
+ } from "./chunk-HIQO2DBA.js";
4
4
 
5
5
  // src/services/github/api.ts
6
6
  import { execSync, spawnSync } from "child_process";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  findProject,
3
3
  getProjectName
4
- } from "./chunk-TI6Y3VT4.js";
4
+ } from "./chunk-H5OFO4VS.js";
5
5
 
6
6
  // src/services/data/package-manager.ts
7
7
  import Conf from "conf";