@jxtools/promptline 1.3.7 → 1.3.9

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": "@jxtools/promptline",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "type": "module",
5
5
  "license": "ISC",
6
6
  "bin": {
@@ -32,10 +32,11 @@ else
32
32
  exit 0
33
33
  fi
34
34
 
35
- export QUEUE_FILE
35
+ QUEUE_DIR="$(dirname "$QUEUE_FILE")"
36
+ export QUEUE_FILE QUEUE_DIR SESSION_ID
36
37
 
37
38
  python3 << 'PYEOF'
38
- import json, os, tempfile
39
+ import json, os, glob, tempfile
39
40
  from datetime import datetime, timezone
40
41
 
41
42
  def atomic_write(path, obj):
@@ -50,18 +51,39 @@ def atomic_write(path, obj):
50
51
  except OSError: pass
51
52
  raise
52
53
 
54
+ def close_session(path, now):
55
+ with open(path, "r") as f:
56
+ data = json.load(f)
57
+ data["closedAt"] = now
58
+ data["lastActivity"] = now
59
+ atomic_write(path, data)
60
+
53
61
  queue_file = os.environ["QUEUE_FILE"]
62
+ queue_dir = os.environ["QUEUE_DIR"]
63
+ session_id = os.environ["SESSION_ID"]
54
64
  now = datetime.now(timezone.utc).isoformat()
55
65
 
56
66
  try:
57
- with open(queue_file, "r") as f:
58
- data = json.load(f)
59
- data["closedAt"] = now
60
- data["lastActivity"] = now
61
- atomic_write(queue_file, data)
67
+ close_session(queue_file, now)
62
68
  except (json.JSONDecodeError, IOError, OSError):
63
69
  pass
64
70
 
71
+ # Close orphaned sessions in the same project
72
+ for path in glob.glob(os.path.join(queue_dir, "*.json")):
73
+ if os.path.basename(path) == f"{session_id}.json":
74
+ continue
75
+ try:
76
+ with open(path, "r") as f:
77
+ data = json.load(f)
78
+ if data.get("closedAt") is not None:
79
+ continue
80
+ has_pending = any(p.get("status") in ("pending", "running") for p in data.get("prompts", []))
81
+ if has_pending:
82
+ continue
83
+ close_session(path, now)
84
+ except (json.JSONDecodeError, IOError, OSError):
85
+ continue
86
+
65
87
  PYEOF
66
88
 
67
89
  exit 0
@@ -37,6 +37,7 @@ export function SessionSection({ session, project, onMutate, defaultExpanded = t
37
37
 
38
38
  async function handleClearPrompts(e: React.MouseEvent) {
39
39
  e.stopPropagation();
40
+ if (!window.confirm('Clear all prompts?')) return;
40
41
  try {
41
42
  await api.clearPrompts(project, session.sessionId);
42
43
  onMutate();
@@ -23,6 +23,10 @@ import {
23
23
 
24
24
  const QUEUES_DIR = join(homedir(), '.promptline', 'queues');
25
25
 
26
+ export function isSafeSegment(s: string): boolean {
27
+ return s.length > 0 && !s.includes('/') && !s.includes('\\') && !s.includes('..');
28
+ }
29
+
26
30
  function parseBody(req: IncomingMessage): Promise<Record<string, unknown>> {
27
31
  return new Promise((resolve, reject) => {
28
32
  let body = '';
@@ -160,6 +164,10 @@ async function handleApi(
160
164
  // Parse URL segments: /api/projects/:project/sessions/:sessionId/prompts/:promptId
161
165
  const segments = url.replace(/^\/api\//, '').split('/').map(decodeURIComponent);
162
166
 
167
+ if (!segments.every(isSafeSegment)) {
168
+ return jsonError(res, 400, 'Invalid path segment');
169
+ }
170
+
163
171
  // GET /api/projects
164
172
  if (segments[0] === 'projects' && segments.length === 1 && method === 'GET') {
165
173
  return json(res, 200, listProjects(QUEUES_DIR));