notoken-core 1.4.1 → 1.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.
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/utils/sessionBackup.d.ts +40 -0
- package/dist/utils/sessionBackup.js +181 -0
- package/dist/utils/sessionSummary.d.ts +39 -0
- package/dist/utils/sessionSummary.js +185 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -45,7 +45,9 @@ export { formatParsedCommand } from "./utils/output.js";
|
|
|
45
45
|
export { Spinner, withSpinner, progressBar } from "./utils/spinner.js";
|
|
46
46
|
export { createBackup, rollback, listBackups, cleanExpiredBackups, formatBackupList } from "./utils/autoBackup.js";
|
|
47
47
|
export { checkForUpdate, checkForUpdateSync, runUpdate, formatUpdateBanner, type UpdateInfo } from "./utils/updater.js";
|
|
48
|
-
export { detectProviders, formatStatus, goOffline, goOnline, disableLLM, enableLLM, isOfflineMode, isLLMDisabled, recordOfflineCommand, getTokensSaved, formatTokensSaved, formatTokensSavedBrief, saveOnExit, type LLMProvider, type LLMState, } from "./utils/llmManager.js";
|
|
48
|
+
export { detectProviders, formatStatus, goOffline, goOnline, disableLLM, enableLLM, isOfflineMode, isLLMDisabled, recordOfflineCommand, getTokensSaved, formatTokensSaved, formatTokensSavedBrief, saveOnExit, getSessionId, type LLMProvider, type LLMState, } from "./utils/llmManager.js";
|
|
49
|
+
export { getRecentSessions, getSessionsForFolder, formatSessionSummary, formatSessionList, type SessionSummary, } from "./utils/sessionSummary.js";
|
|
50
|
+
export { isSessionOpen, toggleSession, hideSession, unhideSession, getHiddenSessions, getLastViewedSession, createFullBackup, restoreFromBackup, listFullBackups, formatBackupsList, type BackupInfo, } from "./utils/sessionBackup.js";
|
|
49
51
|
export { logFailure, loadFailures, clearFailures } from "./utils/logger.js";
|
|
50
52
|
export { logUncertainty, loadUncertaintyLog, getUncertaintySummary } from "./nlp/uncertainty.js";
|
|
51
53
|
export { recordHistory, loadHistory, getRecentHistory, searchHistory } from "./context/history.js";
|
package/dist/index.js
CHANGED
|
@@ -59,7 +59,11 @@ export { createBackup, rollback, listBackups, cleanExpiredBackups, formatBackupL
|
|
|
59
59
|
// ── Updates ──
|
|
60
60
|
export { checkForUpdate, checkForUpdateSync, runUpdate, formatUpdateBanner } from "./utils/updater.js";
|
|
61
61
|
// ── LLM Manager ──
|
|
62
|
-
export { detectProviders, formatStatus, goOffline, goOnline, disableLLM, enableLLM, isOfflineMode, isLLMDisabled, recordOfflineCommand, getTokensSaved, formatTokensSaved, formatTokensSavedBrief, saveOnExit, } from "./utils/llmManager.js";
|
|
62
|
+
export { detectProviders, formatStatus, goOffline, goOnline, disableLLM, enableLLM, isOfflineMode, isLLMDisabled, recordOfflineCommand, getTokensSaved, formatTokensSaved, formatTokensSavedBrief, saveOnExit, getSessionId, } from "./utils/llmManager.js";
|
|
63
|
+
// ── Session Summaries ──
|
|
64
|
+
export { getRecentSessions, getSessionsForFolder, formatSessionSummary, formatSessionList, } from "./utils/sessionSummary.js";
|
|
65
|
+
// ── Session Backup & Prefs ──
|
|
66
|
+
export { isSessionOpen, toggleSession, hideSession, unhideSession, getHiddenSessions, getLastViewedSession, createFullBackup, restoreFromBackup, listFullBackups, formatBackupsList, } from "./utils/sessionBackup.js";
|
|
63
67
|
// ── Logging ──
|
|
64
68
|
export { logFailure, loadFailures, clearFailures } from "./utils/logger.js";
|
|
65
69
|
export { logUncertainty, loadUncertaintyLog, getUncertaintySummary } from "./nlp/uncertainty.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session backup and restore.
|
|
3
|
+
*
|
|
4
|
+
* Backup: tars ~/.notoken/ into a timestamped archive
|
|
5
|
+
* Restore: extracts an archive back to ~/.notoken/
|
|
6
|
+
* Manages open/closed state per session for the dashboard
|
|
7
|
+
*/
|
|
8
|
+
export declare function isSessionOpen(sessionId: string): boolean;
|
|
9
|
+
export declare function toggleSession(sessionId: string): boolean;
|
|
10
|
+
export declare function hideSession(sessionId: string): void;
|
|
11
|
+
export declare function unhideSession(sessionId: string): void;
|
|
12
|
+
export declare function getHiddenSessions(): string[];
|
|
13
|
+
export declare function getLastViewedSession(): string | undefined;
|
|
14
|
+
export interface BackupInfo {
|
|
15
|
+
path: string;
|
|
16
|
+
filename: string;
|
|
17
|
+
size: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a full backup of ~/.notoken/ as a tar.gz archive.
|
|
22
|
+
* Saves to ~/.notoken/backups/ by default, or a custom path.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createFullBackup(outputDir?: string): BackupInfo;
|
|
25
|
+
/**
|
|
26
|
+
* Restore from a backup archive.
|
|
27
|
+
* Creates a safety backup of current state first.
|
|
28
|
+
*/
|
|
29
|
+
export declare function restoreFromBackup(archivePath: string): {
|
|
30
|
+
success: boolean;
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* List available backups.
|
|
35
|
+
*/
|
|
36
|
+
export declare function listFullBackups(): BackupInfo[];
|
|
37
|
+
/**
|
|
38
|
+
* Format backup list for display.
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatBackupsList(backups: BackupInfo[]): string;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session backup and restore.
|
|
3
|
+
*
|
|
4
|
+
* Backup: tars ~/.notoken/ into a timestamped archive
|
|
5
|
+
* Restore: extracts an archive back to ~/.notoken/
|
|
6
|
+
* Manages open/closed state per session for the dashboard
|
|
7
|
+
*/
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { resolve, basename } from "node:path";
|
|
11
|
+
import { USER_HOME } from "./paths.js";
|
|
12
|
+
const PREFS_FILE = resolve(USER_HOME, "session-prefs.json");
|
|
13
|
+
const BACKUP_DIR = resolve(USER_HOME, "backups");
|
|
14
|
+
function loadPrefs() {
|
|
15
|
+
try {
|
|
16
|
+
if (existsSync(PREFS_FILE))
|
|
17
|
+
return JSON.parse(readFileSync(PREFS_FILE, "utf-8"));
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
return { openSessions: [], hiddenSessions: [] };
|
|
21
|
+
}
|
|
22
|
+
function savePrefs(prefs) {
|
|
23
|
+
try {
|
|
24
|
+
mkdirSync(USER_HOME, { recursive: true });
|
|
25
|
+
writeFileSync(PREFS_FILE, JSON.stringify(prefs, null, 2));
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
}
|
|
29
|
+
export function isSessionOpen(sessionId) {
|
|
30
|
+
return loadPrefs().openSessions.includes(sessionId);
|
|
31
|
+
}
|
|
32
|
+
export function toggleSession(sessionId) {
|
|
33
|
+
const prefs = loadPrefs();
|
|
34
|
+
const idx = prefs.openSessions.indexOf(sessionId);
|
|
35
|
+
if (idx >= 0) {
|
|
36
|
+
prefs.openSessions.splice(idx, 1);
|
|
37
|
+
savePrefs(prefs);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
prefs.openSessions.push(sessionId);
|
|
42
|
+
prefs.lastViewed = sessionId;
|
|
43
|
+
savePrefs(prefs);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function hideSession(sessionId) {
|
|
48
|
+
const prefs = loadPrefs();
|
|
49
|
+
if (!prefs.hiddenSessions.includes(sessionId)) {
|
|
50
|
+
prefs.hiddenSessions.push(sessionId);
|
|
51
|
+
}
|
|
52
|
+
prefs.openSessions = prefs.openSessions.filter(s => s !== sessionId);
|
|
53
|
+
savePrefs(prefs);
|
|
54
|
+
}
|
|
55
|
+
export function unhideSession(sessionId) {
|
|
56
|
+
const prefs = loadPrefs();
|
|
57
|
+
prefs.hiddenSessions = prefs.hiddenSessions.filter(s => s !== sessionId);
|
|
58
|
+
savePrefs(prefs);
|
|
59
|
+
}
|
|
60
|
+
export function getHiddenSessions() {
|
|
61
|
+
return loadPrefs().hiddenSessions;
|
|
62
|
+
}
|
|
63
|
+
export function getLastViewedSession() {
|
|
64
|
+
return loadPrefs().lastViewed;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Create a full backup of ~/.notoken/ as a tar.gz archive.
|
|
68
|
+
* Saves to ~/.notoken/backups/ by default, or a custom path.
|
|
69
|
+
*/
|
|
70
|
+
export function createFullBackup(outputDir) {
|
|
71
|
+
const dir = outputDir ?? BACKUP_DIR;
|
|
72
|
+
mkdirSync(dir, { recursive: true });
|
|
73
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
74
|
+
const filename = `notoken-backup-${ts}.tar.gz`;
|
|
75
|
+
const fullPath = resolve(dir, filename);
|
|
76
|
+
// Tar everything except the backups directory itself
|
|
77
|
+
execSync(`tar -czf "${fullPath}" -C "${resolve(USER_HOME, "..")}" --exclude="backups" "${basename(USER_HOME)}"`, { timeout: 60_000, stdio: "pipe" });
|
|
78
|
+
const size = tryExec(`ls -lh "${fullPath}" | awk '{print $5}'`) ?? "unknown";
|
|
79
|
+
return {
|
|
80
|
+
path: fullPath,
|
|
81
|
+
filename,
|
|
82
|
+
size,
|
|
83
|
+
createdAt: new Date().toISOString(),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Restore from a backup archive.
|
|
88
|
+
* Creates a safety backup of current state first.
|
|
89
|
+
*/
|
|
90
|
+
export function restoreFromBackup(archivePath) {
|
|
91
|
+
if (!existsSync(archivePath)) {
|
|
92
|
+
return { success: false, message: `Backup not found: ${archivePath}` };
|
|
93
|
+
}
|
|
94
|
+
// Safety backup of current state
|
|
95
|
+
try {
|
|
96
|
+
const safety = createFullBackup();
|
|
97
|
+
console.error(`\x1b[2m[backup] Safety backup created: ${safety.path}\x1b[0m`);
|
|
98
|
+
}
|
|
99
|
+
catch { }
|
|
100
|
+
// Extract
|
|
101
|
+
try {
|
|
102
|
+
execSync(`tar -xzf "${archivePath}" -C "${resolve(USER_HOME, "..")}"`, { timeout: 60_000, stdio: "pipe" });
|
|
103
|
+
return { success: true, message: `Restored from ${basename(archivePath)}` };
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
return { success: false, message: `Restore failed: ${err.message}` };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* List available backups.
|
|
111
|
+
*/
|
|
112
|
+
export function listFullBackups() {
|
|
113
|
+
if (!existsSync(BACKUP_DIR))
|
|
114
|
+
return [];
|
|
115
|
+
try {
|
|
116
|
+
const { readdirSync, statSync } = require("node:fs");
|
|
117
|
+
return readdirSync(BACKUP_DIR)
|
|
118
|
+
.filter((f) => f.startsWith("notoken-backup-") && f.endsWith(".tar.gz"))
|
|
119
|
+
.map((f) => {
|
|
120
|
+
const full = resolve(BACKUP_DIR, f);
|
|
121
|
+
const stat = statSync(full);
|
|
122
|
+
return {
|
|
123
|
+
path: full,
|
|
124
|
+
filename: f,
|
|
125
|
+
size: formatSize(stat.size),
|
|
126
|
+
createdAt: stat.mtime.toISOString(),
|
|
127
|
+
};
|
|
128
|
+
})
|
|
129
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Format backup list for display.
|
|
137
|
+
*/
|
|
138
|
+
export function formatBackupsList(backups) {
|
|
139
|
+
const c = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", cyan: "\x1b[36m", green: "\x1b[32m" };
|
|
140
|
+
if (backups.length === 0)
|
|
141
|
+
return `${c.dim}No backups found.${c.reset}`;
|
|
142
|
+
const lines = [`${c.bold}Backups:${c.reset}\n`];
|
|
143
|
+
for (const b of backups) {
|
|
144
|
+
const ago = timeAgo(b.createdAt);
|
|
145
|
+
lines.push(` ${c.cyan}${b.filename}${c.reset} — ${b.size} — ${ago}`);
|
|
146
|
+
}
|
|
147
|
+
lines.push(`\n ${c.dim}Backup dir: ${BACKUP_DIR}${c.reset}`);
|
|
148
|
+
lines.push(` ${c.dim}Restore: notoken restore <filename>${c.reset}`);
|
|
149
|
+
return lines.join("\n");
|
|
150
|
+
}
|
|
151
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
152
|
+
function formatSize(bytes) {
|
|
153
|
+
if (bytes < 1024)
|
|
154
|
+
return `${bytes} B`;
|
|
155
|
+
if (bytes < 1048576)
|
|
156
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
157
|
+
if (bytes < 1073741824)
|
|
158
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
159
|
+
return `${(bytes / 1073741824).toFixed(1)} GB`;
|
|
160
|
+
}
|
|
161
|
+
function timeAgo(dateStr) {
|
|
162
|
+
const ms = Date.now() - new Date(dateStr).getTime();
|
|
163
|
+
const mins = Math.floor(ms / 60000);
|
|
164
|
+
if (mins < 1)
|
|
165
|
+
return "just now";
|
|
166
|
+
if (mins < 60)
|
|
167
|
+
return `${mins}m ago`;
|
|
168
|
+
const hours = Math.floor(mins / 60);
|
|
169
|
+
if (hours < 24)
|
|
170
|
+
return `${hours}h ago`;
|
|
171
|
+
const days = Math.floor(hours / 24);
|
|
172
|
+
return `${days}d ago`;
|
|
173
|
+
}
|
|
174
|
+
function tryExec(cmd) {
|
|
175
|
+
try {
|
|
176
|
+
return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10_000 }).trim() || null;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session summary generator.
|
|
3
|
+
*
|
|
4
|
+
* Reads conversation turns from ~/.notoken/conversations/ and generates
|
|
5
|
+
* summaries of what was done in each session.
|
|
6
|
+
*
|
|
7
|
+
* Used by:
|
|
8
|
+
* - Desktop app dashboard session card
|
|
9
|
+
* - :sessions command in CLI
|
|
10
|
+
* - Exit summary
|
|
11
|
+
*/
|
|
12
|
+
export interface SessionSummary {
|
|
13
|
+
id: string;
|
|
14
|
+
folder: string;
|
|
15
|
+
startedAt: string;
|
|
16
|
+
endedAt: string;
|
|
17
|
+
turns: number;
|
|
18
|
+
commands: string[];
|
|
19
|
+
intents: string[];
|
|
20
|
+
entities: string[];
|
|
21
|
+
errors: number;
|
|
22
|
+
highlights: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get summaries for recent sessions across all folders.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getRecentSessions(limit?: number): SessionSummary[];
|
|
28
|
+
/**
|
|
29
|
+
* Get sessions for a specific folder.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getSessionsForFolder(folder: string, limit?: number): SessionSummary[];
|
|
32
|
+
/**
|
|
33
|
+
* Format a session summary for display.
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatSessionSummary(session: SessionSummary): string;
|
|
36
|
+
/**
|
|
37
|
+
* Format multiple sessions as a list.
|
|
38
|
+
*/
|
|
39
|
+
export declare function formatSessionList(sessions: SessionSummary[]): string;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session summary generator.
|
|
3
|
+
*
|
|
4
|
+
* Reads conversation turns from ~/.notoken/conversations/ and generates
|
|
5
|
+
* summaries of what was done in each session.
|
|
6
|
+
*
|
|
7
|
+
* Used by:
|
|
8
|
+
* - Desktop app dashboard session card
|
|
9
|
+
* - :sessions command in CLI
|
|
10
|
+
* - Exit summary
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
const CONVERSATIONS_ROOT = resolve(homedir(), ".notoken", "conversations");
|
|
16
|
+
/**
|
|
17
|
+
* Get summaries for recent sessions across all folders.
|
|
18
|
+
*/
|
|
19
|
+
export function getRecentSessions(limit = 20) {
|
|
20
|
+
const sessions = [];
|
|
21
|
+
if (!existsSync(CONVERSATIONS_ROOT))
|
|
22
|
+
return sessions;
|
|
23
|
+
// Walk all folder subdirectories
|
|
24
|
+
try {
|
|
25
|
+
const walkDir = (dir) => {
|
|
26
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const full = resolve(dir, entry.name);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
walkDir(full);
|
|
31
|
+
}
|
|
32
|
+
else if (entry.name.endsWith(".json")) {
|
|
33
|
+
try {
|
|
34
|
+
const conv = JSON.parse(readFileSync(full, "utf-8"));
|
|
35
|
+
if (conv.turns && conv.turns.length > 0) {
|
|
36
|
+
sessions.push(summarizeConversation(conv, dir));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
walkDir(CONVERSATIONS_ROOT);
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
// Sort by most recent first
|
|
47
|
+
sessions.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
48
|
+
return sessions.slice(0, limit);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get sessions for a specific folder.
|
|
52
|
+
*/
|
|
53
|
+
export function getSessionsForFolder(folder, limit = 10) {
|
|
54
|
+
const safePath = folder.replace(/[^a-zA-Z0-9_\-\/]/g, "_").replace(/^\/+/, "");
|
|
55
|
+
const dir = resolve(CONVERSATIONS_ROOT, safePath || "default");
|
|
56
|
+
if (!existsSync(dir))
|
|
57
|
+
return [];
|
|
58
|
+
const sessions = [];
|
|
59
|
+
const files = readdirSync(dir).filter(f => f.endsWith(".json")).sort().reverse();
|
|
60
|
+
for (const file of files.slice(0, limit)) {
|
|
61
|
+
try {
|
|
62
|
+
const conv = JSON.parse(readFileSync(resolve(dir, file), "utf-8"));
|
|
63
|
+
if (conv.turns && conv.turns.length > 0) {
|
|
64
|
+
sessions.push(summarizeConversation(conv, folder));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
}
|
|
69
|
+
return sessions;
|
|
70
|
+
}
|
|
71
|
+
function summarizeConversation(conv, folder) {
|
|
72
|
+
const turns = conv.turns ?? [];
|
|
73
|
+
const userTurns = turns.filter(t => t.role === "user");
|
|
74
|
+
const systemTurns = turns.filter(t => t.role === "system");
|
|
75
|
+
// Extract unique intents
|
|
76
|
+
const intents = [...new Set(userTurns.map(t => t.intent).filter(Boolean))];
|
|
77
|
+
// Extract commands (raw text of user turns)
|
|
78
|
+
const commands = userTurns.map(t => t.rawText).filter(Boolean);
|
|
79
|
+
// Extract entities from knowledge tree
|
|
80
|
+
const knowledgeTree = conv.knowledgeTree ?? [];
|
|
81
|
+
const entities = knowledgeTree.map(e => `${e.entity} (${e.type})`);
|
|
82
|
+
// Count errors
|
|
83
|
+
const errors = systemTurns.filter(t => t.error).length;
|
|
84
|
+
// Generate highlights — most interesting things that happened
|
|
85
|
+
const highlights = [];
|
|
86
|
+
if (intents.includes("service.restart"))
|
|
87
|
+
highlights.push("Restarted services");
|
|
88
|
+
if (intents.includes("deploy.run"))
|
|
89
|
+
highlights.push("Deployed");
|
|
90
|
+
if (intents.includes("deploy.rollback"))
|
|
91
|
+
highlights.push("Rolled back deploy");
|
|
92
|
+
if (intents.some(i => i.startsWith("docker.")))
|
|
93
|
+
highlights.push("Docker operations");
|
|
94
|
+
if (intents.some(i => i.startsWith("git.")))
|
|
95
|
+
highlights.push("Git operations");
|
|
96
|
+
if (intents.some(i => i.startsWith("security.")))
|
|
97
|
+
highlights.push("Security checks");
|
|
98
|
+
if (intents.includes("server.check_disk"))
|
|
99
|
+
highlights.push("Disk check");
|
|
100
|
+
if (intents.includes("server.check_memory"))
|
|
101
|
+
highlights.push("Memory check");
|
|
102
|
+
if (intents.some(i => i.startsWith("logs.")))
|
|
103
|
+
highlights.push("Log inspection");
|
|
104
|
+
if (intents.some(i => i.startsWith("file.")))
|
|
105
|
+
highlights.push("File operations");
|
|
106
|
+
if (intents.some(i => i.startsWith("db.")))
|
|
107
|
+
highlights.push("Database operations");
|
|
108
|
+
if (intents.some(i => i.startsWith("backup.")))
|
|
109
|
+
highlights.push("Backup operations");
|
|
110
|
+
if (errors > 0)
|
|
111
|
+
highlights.push(`${errors} error(s)`);
|
|
112
|
+
// If no specific highlights, summarize by count
|
|
113
|
+
if (highlights.length === 0 && intents.length > 0) {
|
|
114
|
+
highlights.push(`${intents.length} different operations`);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
id: conv.id ?? "unknown",
|
|
118
|
+
folder,
|
|
119
|
+
startedAt: conv.createdAt ?? "",
|
|
120
|
+
endedAt: conv.updatedAt ?? "",
|
|
121
|
+
turns: turns.length,
|
|
122
|
+
commands: commands.slice(0, 10),
|
|
123
|
+
intents,
|
|
124
|
+
entities: entities.slice(0, 10),
|
|
125
|
+
errors,
|
|
126
|
+
highlights,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Format a session summary for display.
|
|
131
|
+
*/
|
|
132
|
+
export function formatSessionSummary(session) {
|
|
133
|
+
const c = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", cyan: "\x1b[36m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m" };
|
|
134
|
+
const ago = timeAgo(session.startedAt);
|
|
135
|
+
const duration = timeBetween(session.startedAt, session.endedAt);
|
|
136
|
+
const errorTag = session.errors > 0 ? ` ${c.red}(${session.errors} errors)${c.reset}` : "";
|
|
137
|
+
const lines = [];
|
|
138
|
+
lines.push(`${c.bold}${ago}${c.reset} — ${duration}${errorTag}`);
|
|
139
|
+
lines.push(`${c.dim}${session.folder} | ${session.turns} turns | ${session.id}${c.reset}`);
|
|
140
|
+
if (session.highlights.length > 0) {
|
|
141
|
+
lines.push(`${c.cyan}${session.highlights.join(" · ")}${c.reset}`);
|
|
142
|
+
}
|
|
143
|
+
if (session.commands.length > 0) {
|
|
144
|
+
lines.push(`${c.dim}Commands: ${session.commands.slice(0, 5).join(", ")}${session.commands.length > 5 ? "..." : ""}${c.reset}`);
|
|
145
|
+
}
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Format multiple sessions as a list.
|
|
150
|
+
*/
|
|
151
|
+
export function formatSessionList(sessions) {
|
|
152
|
+
if (sessions.length === 0)
|
|
153
|
+
return "\x1b[2mNo sessions found.\x1b[0m";
|
|
154
|
+
return sessions.map((s, i) => {
|
|
155
|
+
const sep = i < sessions.length - 1 ? "\n" : "";
|
|
156
|
+
return ` ${formatSessionSummary(s)}${sep}`;
|
|
157
|
+
}).join("\n");
|
|
158
|
+
}
|
|
159
|
+
function timeAgo(dateStr) {
|
|
160
|
+
const ms = Date.now() - new Date(dateStr).getTime();
|
|
161
|
+
const mins = Math.floor(ms / 60000);
|
|
162
|
+
if (mins < 1)
|
|
163
|
+
return "Just now";
|
|
164
|
+
if (mins < 60)
|
|
165
|
+
return `${mins}m ago`;
|
|
166
|
+
const hours = Math.floor(mins / 60);
|
|
167
|
+
if (hours < 24)
|
|
168
|
+
return `${hours}h ago`;
|
|
169
|
+
const days = Math.floor(hours / 24);
|
|
170
|
+
if (days < 7)
|
|
171
|
+
return `${days}d ago`;
|
|
172
|
+
return new Date(dateStr).toLocaleDateString();
|
|
173
|
+
}
|
|
174
|
+
function timeBetween(start, end) {
|
|
175
|
+
const ms = new Date(end).getTime() - new Date(start).getTime();
|
|
176
|
+
const mins = Math.floor(ms / 60000);
|
|
177
|
+
if (mins < 1)
|
|
178
|
+
return "<1m";
|
|
179
|
+
if (mins < 60)
|
|
180
|
+
return `${mins}m`;
|
|
181
|
+
const hours = Math.floor(mins / 60);
|
|
182
|
+
if (hours < 24)
|
|
183
|
+
return `${hours}h ${mins % 60}m`;
|
|
184
|
+
return `${Math.floor(hours / 24)}d`;
|
|
185
|
+
}
|