ispbills-icli 8.5.5 → 8.5.7

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": "ispbills-icli",
3
- "version": "8.5.5",
3
+ "version": "8.5.7",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/session.js CHANGED
@@ -1,9 +1,50 @@
1
- import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from 'fs';
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
4
 
5
5
  const SESSION_DIR = join(homedir(), '.config', 'ispbills-icli', 'sessions');
6
6
 
7
+ function safeJson(raw, fallback = null) {
8
+ try {
9
+ return JSON.parse(raw);
10
+ } catch {
11
+ return fallback;
12
+ }
13
+ }
14
+
15
+ function normalizeMessages(messages = []) {
16
+ return messages
17
+ .filter((m) => m && typeof m.role === 'string')
18
+ .map((m) => ({ role: m.role, content: String(m.content ?? '') }));
19
+ }
20
+
21
+ function autoNameFromMessages(messages = []) {
22
+ const firstUser = messages.find((m) => m?.role === 'user' && String(m.content || '').trim());
23
+ const text = String(firstUser?.content || '').replace(/\s+/g, ' ').trim();
24
+ if (!text) return 'Untitled session';
25
+ return text.slice(0, 40).trim();
26
+ }
27
+
28
+ function normalizeSession(session = {}) {
29
+ const now = new Date().toISOString();
30
+ const messages = normalizeMessages(session.messages);
31
+ const id = String(session.id || Date.now().toString(36));
32
+ return {
33
+ id,
34
+ name: String(session.name || autoNameFromMessages(messages) || 'Untitled session'),
35
+ cwd: String(session.cwd || process.cwd()),
36
+ branch: String(session.branch || ''),
37
+ model: String(session.model || ''),
38
+ createdAt: session.createdAt || now,
39
+ updatedAt: session.updatedAt || now,
40
+ messages,
41
+ };
42
+ }
43
+
44
+ function sessionPathFor(id) {
45
+ return join(SESSION_DIR, `${id}.json`);
46
+ }
47
+
7
48
  export function sessionDir() {
8
49
  return SESSION_DIR;
9
50
  }
@@ -12,30 +53,105 @@ export function initSessionDir() {
12
53
  if (!existsSync(SESSION_DIR)) mkdirSync(SESSION_DIR, { recursive: true });
13
54
  }
14
55
 
15
- export function newSessionPath() {
16
- const id = new Date().toISOString().replace(/[:.]/g, '-');
17
- return join(SESSION_DIR, `${id}.jsonl`);
56
+ export function saveSession(session) {
57
+ try {
58
+ initSessionDir();
59
+ const existing = loadSessionById(session?.id);
60
+ const normalized = normalizeSession({
61
+ ...existing,
62
+ ...session,
63
+ id: session?.id || existing?.id,
64
+ createdAt: session?.createdAt || existing?.createdAt,
65
+ updatedAt: new Date().toISOString(),
66
+ name: session?.name || existing?.name,
67
+ });
68
+ if (!normalized.name || normalized.name === 'Untitled session') {
69
+ normalized.name = autoNameFromMessages(normalized.messages);
70
+ }
71
+ writeFileSync(sessionPathFor(normalized.id), JSON.stringify(normalized, null, 2) + '\n');
72
+ return normalized;
73
+ } catch {
74
+ return null;
75
+ }
18
76
  }
19
77
 
20
- /** Append a single message to the JSONL session log (best-effort). */
21
- export function saveMessage(sessionPath, message) {
78
+ export function loadSessionById(id) {
79
+ if (!id) return null;
80
+ const path = sessionPathFor(id);
81
+ if (!existsSync(path)) return null;
82
+ const loaded = safeJson(readFileSync(path, 'utf8'), null);
83
+ return loaded ? normalizeSession(loaded) : null;
84
+ }
85
+
86
+ export function deleteSession(id) {
22
87
  try {
23
- appendFileSync(sessionPath, JSON.stringify({ timestamp: new Date().toISOString(), message }) + '\n');
88
+ const path = sessionPathFor(id);
89
+ if (!existsSync(path)) return false;
90
+ unlinkSync(path);
91
+ return true;
24
92
  } catch {
25
- /* non-fatal — session logging is a convenience only */
93
+ return false;
26
94
  }
27
95
  }
28
96
 
29
- export function listSessions() {
97
+ export function listSessionsDetailed() {
30
98
  if (!existsSync(SESSION_DIR)) return [];
31
- return readdirSync(SESSION_DIR).filter((f) => f.endsWith('.jsonl')).sort();
99
+ return readdirSync(SESSION_DIR)
100
+ .filter((f) => f.endsWith('.json'))
101
+ .map((file) => {
102
+ const full = join(SESSION_DIR, file);
103
+ const session = safeJson(readFileSync(full, 'utf8'), null);
104
+ if (!session) return null;
105
+ const normalized = normalizeSession(session);
106
+ const stats = statSync(full);
107
+ return {
108
+ id: normalized.id,
109
+ name: normalized.name || autoNameFromMessages(normalized.messages),
110
+ cwd: normalized.cwd,
111
+ branch: normalized.branch,
112
+ model: normalized.model,
113
+ createdAt: normalized.createdAt,
114
+ updatedAt: normalized.updatedAt,
115
+ messageCount: normalized.messages.length,
116
+ messages: normalized.messages,
117
+ mtimeMs: stats.mtimeMs,
118
+ };
119
+ })
120
+ .filter(Boolean)
121
+ .sort((a, b) => new Date(b.updatedAt || 0).getTime() - new Date(a.updatedAt || 0).getTime());
32
122
  }
33
123
 
34
- export function loadSession(sessionPath) {
35
- if (!existsSync(sessionPath)) return [];
36
- return readFileSync(sessionPath, 'utf-8')
37
- .split('\n')
38
- .filter(Boolean)
39
- .map((line) => { try { return JSON.parse(line).message; } catch { return null; } })
40
- .filter(Boolean);
124
+ export function loadMostRecent() {
125
+ const [session] = listSessionsDetailed();
126
+ if (!session?.id) return null;
127
+ return loadSessionById(session.id);
128
+ }
129
+
130
+ export function listSessions() {
131
+ return listSessionsDetailed().map((s) => s.id);
132
+ }
133
+
134
+ export function loadSession(sessionPathOrId) {
135
+ if (!sessionPathOrId) return [];
136
+ if (sessionPathOrId.endsWith?.('.json')) {
137
+ const session = safeJson(readFileSync(sessionPathOrId, 'utf8'), null);
138
+ return normalizeSession(session || {}).messages;
139
+ }
140
+ return loadSessionById(sessionPathOrId)?.messages || [];
141
+ }
142
+
143
+ export function newSessionPath() {
144
+ initSessionDir();
145
+ return sessionPathFor(Date.now().toString(36));
146
+ }
147
+
148
+ export function saveMessage(sessionPathOrId, message) {
149
+ if (!message) return null;
150
+ const id = String(sessionPathOrId || Date.now().toString(36)).replace(/\.json$/, '');
151
+ const existing = loadSessionById(id);
152
+ return saveSession({
153
+ ...(existing || {}),
154
+ id,
155
+ messages: [...(existing?.messages || []), message],
156
+ });
41
157
  }