vigthoria-cli 1.8.15 → 1.9.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.
@@ -155,28 +155,27 @@ class Config {
155
155
  expiresAt: data.expiresAt || null,
156
156
  });
157
157
  }
158
- // Model selection - Vigthoria Models (internal: qwen3-coder, deepseek via OpenRouter)
158
+ // Model selection - Vigthoria Models (internal routing IDs only)
159
159
  getAvailableModels() {
160
160
  const sub = this.store.get('subscription');
161
161
  const plan = (sub.plan || '').toLowerCase();
162
162
  // ═══════════════════════════════════════════════════════════════
163
- // VIGTHORIA LOCAL - Self-hosted models (fast, no API costs)
163
+ // VIGTHORIA LOCAL - Self-hosted operational models
164
164
  // ═══════════════════════════════════════════════════════════════
165
165
  const models = [
166
- { id: 'agent', name: 'Vigthoria Agent GPU', description: 'Blackwell autonomous agent workflow', tier: 'local', backendModel: 'vigthoria-v3-code-30b' },
167
- { id: 'code', name: 'Vigthoria v3 Code 30B', description: 'Native 30B coding model on Blackwell', tier: 'local', backendModel: 'vigthoria-v3-code-30b' },
168
- { id: 'code-30b', name: 'Vigthoria v3 Code 30B', description: 'Same flagship model as code', tier: 'local', backendModel: 'vigthoria-v3-code-30b' },
169
- { id: 'code-8b', name: 'Vigthoria v2 Code 8B', description: 'Native 8B coding specialist', tier: 'local', backendModel: 'vigthoria-v2-code-8b' },
170
- { id: 'balanced', name: 'Vigthoria Balanced 4B', description: 'Balanced general-purpose local model', tier: 'local', backendModel: 'vigthoria-balanced-4b' },
171
- { id: 'fast', name: 'Vigthoria Fast 1.7B', description: 'Low-latency local responses', tier: 'local', backendModel: 'vigthoria-fast-1.7b' },
172
- { id: 'creative', name: 'Vigthoria Creative 9B V4', description: 'Creative and lyric generation model', tier: 'local', backendModel: 'vigthoria-creative-9b-v4' },
166
+ { id: 'agent', name: 'Vigthoria Agent GPU', description: 'Blackwell autonomous agent workflow', tier: 'local', backendModel: 'vigthoria-v3-code-35b' },
167
+ { id: 'code', name: 'Vigthoria v3 Code 35B', description: 'Native 35B coding model on Blackwell', tier: 'local', backendModel: 'vigthoria-v3-code-35b' },
168
+ { id: 'code-35b', name: 'Vigthoria v3 Code 35B', description: 'Same flagship model as code', tier: 'local', backendModel: 'vigthoria-v3-code-35b' },
169
+ { id: 'code-9b', name: 'Vigthoria C1 Code 9B', description: 'Efficient coding specialist for quick tasks', tier: 'local', backendModel: 'vigthoria_c1_m' },
170
+ { id: 'balanced', name: 'Vigthoria Master 7.6B', description: 'Balanced general-purpose local model', tier: 'local', backendModel: 'vigthoria_master' },
171
+ { id: 'balanced-4b', name: 'Vigthoria v3 Balanced 4B', description: 'Efficient 4B general-purpose model (Qwen3.5-4B based)', tier: 'local', backendModel: 'vigthoria-balanced-4b' },
173
172
  ];
174
173
  // ═══════════════════════════════════════════════════════════════
175
174
  // VIGTHORIA CLOUD - Premium cloud models (Pro subscription)
176
175
  // For complex multi-file tasks, large refactoring, architecture
177
176
  // ═══════════════════════════════════════════════════════════════
178
177
  if (this.hasCloudAccess()) {
179
- models.push({ id: 'cloud', name: 'Vigthoria Cloud Pro', description: 'DeepSeek-class cloud model for complex tasks', tier: 'cloud', backendModel: 'vigthoria-cloud-pro' }, { id: 'cloud-reason', name: 'Vigthoria Cloud K2', description: 'Reasoning-focused cloud model', tier: 'cloud', backendModel: 'vigthoria-cloud-k2' }, { id: 'ultra', name: 'Vigthoria Cloud Ultra', description: 'Maximum capability cloud routing', tier: 'cloud', backendModel: 'vigthoria-cloud-ultra' });
178
+ models.push({ id: 'cloud', name: 'Vigthoria Cloud Pro', description: 'High-capability cloud model for complex tasks', tier: 'cloud', backendModel: 'vigthoria-cloud-pro' }, { id: 'cloud-reason', name: 'Vigthoria Cloud K2', description: 'Reasoning-focused cloud model', tier: 'cloud', backendModel: 'vigthoria-cloud-k2' }, { id: 'ultra', name: 'Vigthoria Cloud Ultra', description: 'Maximum capability cloud routing', tier: 'cloud', backendModel: 'vigthoria-cloud-ultra' });
180
179
  }
181
180
  return models;
182
181
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Vigthoria CLI — Semantic Context Ranker
3
+ *
4
+ * Lightweight local-first file relevance scoring. Extracts keywords from the
5
+ * user prompt and scores workspace files by occurrence in filename + content.
6
+ * Used to prioritise which files are sent to V3 as workspace context.
7
+ *
8
+ * No external dependencies — uses only Node.js built-ins.
9
+ */
10
+ export interface RankedFile {
11
+ path: string;
12
+ score: number;
13
+ snippet: string;
14
+ }
15
+ export interface RankedContext {
16
+ topFiles: RankedFile[];
17
+ totalFilesScanned: number;
18
+ keywords: string[];
19
+ }
20
+ /**
21
+ * Score and rank workspace files by relevance to the given prompt.
22
+ * Returns the top N files with path, score, and content snippet.
23
+ */
24
+ export declare function buildSemanticContext(workspacePath: string, prompt: string, topN?: number): RankedContext;
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ /**
3
+ * Vigthoria CLI — Semantic Context Ranker
4
+ *
5
+ * Lightweight local-first file relevance scoring. Extracts keywords from the
6
+ * user prompt and scores workspace files by occurrence in filename + content.
7
+ * Used to prioritise which files are sent to V3 as workspace context.
8
+ *
9
+ * No external dependencies — uses only Node.js built-ins.
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.buildSemanticContext = buildSemanticContext;
16
+ const node_fs_1 = __importDefault(require("node:fs"));
17
+ const node_path_1 = __importDefault(require("node:path"));
18
+ const IGNORED_DIRS = new Set([
19
+ '.git', 'node_modules', 'dist', '.next', '__pycache__',
20
+ '.vigthoria', 'coverage', '.cache', '.turbo', 'build', 'out',
21
+ ]);
22
+ const TEXT_EXTENSIONS = new Set([
23
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
24
+ '.py', '.go', '.rs', '.java', '.c', '.cpp', '.h', '.cs', '.php', '.rb',
25
+ '.json', '.yaml', '.yml', '.toml', '.env', '.sh', '.bash',
26
+ '.html', '.css', '.scss', '.sass', '.less',
27
+ '.md', '.txt', '.xml', '.svg',
28
+ ]);
29
+ /** Extract scoring keywords from the user prompt. */
30
+ function extractKeywords(prompt) {
31
+ // General words (min length 4)
32
+ const words = prompt
33
+ .replace(/[^\w\s./\\-]/g, ' ')
34
+ .split(/\s+/)
35
+ .filter((w) => w.length >= 4)
36
+ .map((w) => w.toLowerCase());
37
+ // CamelCase splits
38
+ const camel = (prompt.match(/[a-z][A-Z][a-zA-Z]+|[A-Z]{2,}[a-z]+[a-zA-Z]*/g) || [])
39
+ .map((w) => w.toLowerCase());
40
+ // Acronyms: uppercase sequences 2-4 chars (SSE, API, HTTP, etc.)
41
+ const acronyms = (prompt.match(/\b[A-Z]{2,4}\b/g) || [])
42
+ .map((w) => w.toLowerCase());
43
+ return [...new Set([...words, ...camel, ...acronyms])].filter((kw) => kw.length >= 2);
44
+ }
45
+ function scoreFile(filePath, keywords) {
46
+ if (keywords.length === 0)
47
+ return 0;
48
+ const filename = node_path_1.default.basename(filePath).toLowerCase();
49
+ const parentDir = node_path_1.default.basename(node_path_1.default.dirname(filePath)).toLowerCase();
50
+ let score = 0;
51
+ for (const kw of keywords) {
52
+ // Filename match (high weight)
53
+ if (filename.includes(kw))
54
+ score += 15;
55
+ // Parent directory name match (medium weight)
56
+ if (parentDir.includes(kw))
57
+ score += 6;
58
+ }
59
+ try {
60
+ const content = node_fs_1.default.readFileSync(filePath, 'utf-8').slice(0, 300000);
61
+ const lower = content.toLowerCase();
62
+ for (const kw of keywords) {
63
+ const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
64
+ // Word boundaries for short keywords prevent false-positives (e.g. 'sse' inside 'assert')
65
+ const pattern = kw.length <= 4 ? '\\b' + escaped + '\\b' : escaped;
66
+ const hits = (lower.match(new RegExp(pattern, 'g')) || []).length;
67
+ score += hits;
68
+ }
69
+ }
70
+ catch { /* binary or unreadable */ }
71
+ return score;
72
+ }
73
+ function walkWorkspace(dir, maxFiles = 800) {
74
+ const results = [];
75
+ const stack = [dir];
76
+ const workspaceRoot = node_path_1.default.resolve(dir);
77
+ while (stack.length > 0 && results.length < maxFiles) {
78
+ const current = stack.pop();
79
+ if (!current)
80
+ continue;
81
+ let entries;
82
+ try {
83
+ entries = node_fs_1.default.readdirSync(current, { withFileTypes: true });
84
+ }
85
+ catch {
86
+ continue;
87
+ }
88
+ for (const entry of entries) {
89
+ // Hard cap enforced inside inner loop (fixes > 800 overshoot)
90
+ if (results.length >= maxFiles)
91
+ break;
92
+ if (IGNORED_DIRS.has(entry.name))
93
+ continue;
94
+ const fullPath = node_path_1.default.join(current, entry.name);
95
+ // Security: skip symlinks that escape workspace root (fixes 8.2)
96
+ if (entry.isSymbolicLink()) {
97
+ try {
98
+ const real = node_fs_1.default.realpathSync(fullPath);
99
+ if (!real.startsWith(workspaceRoot + node_path_1.default.sep) && real !== workspaceRoot)
100
+ continue;
101
+ }
102
+ catch {
103
+ continue;
104
+ }
105
+ }
106
+ if (entry.isDirectory()) {
107
+ stack.push(fullPath);
108
+ }
109
+ else if (entry.isFile() && TEXT_EXTENSIONS.has(node_path_1.default.extname(entry.name).toLowerCase())) {
110
+ results.push(fullPath);
111
+ }
112
+ }
113
+ }
114
+ return results;
115
+ }
116
+ /**
117
+ * Score and rank workspace files by relevance to the given prompt.
118
+ * Returns the top N files with path, score, and content snippet.
119
+ */
120
+ function buildSemanticContext(workspacePath, prompt, topN = 12) {
121
+ if (!workspacePath || !node_fs_1.default.existsSync(workspacePath)) {
122
+ return { topFiles: [], totalFilesScanned: 0, keywords: [] };
123
+ }
124
+ const keywords = extractKeywords(prompt);
125
+ if (keywords.length === 0) {
126
+ return { topFiles: [], totalFilesScanned: 0, keywords: [] };
127
+ }
128
+ const files = walkWorkspace(workspacePath);
129
+ const scored = files
130
+ .map((f) => ({ filePath: f, score: scoreFile(f, keywords) }))
131
+ .filter((f) => f.score > 0)
132
+ .sort((a, b) => b.score - a.score)
133
+ .slice(0, topN);
134
+ const topFiles = scored.map(({ filePath, score }) => {
135
+ let snippet = '';
136
+ try {
137
+ snippet = node_fs_1.default.readFileSync(filePath, 'utf-8').slice(0, 600);
138
+ }
139
+ catch { /* ignore */ }
140
+ return {
141
+ path: node_path_1.default.relative(workspacePath, filePath).replace(/\\/g, '/'),
142
+ score,
143
+ snippet,
144
+ };
145
+ });
146
+ return { topFiles, totalFilesScanned: files.length, keywords };
147
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Vigthoria CLI — Post-Write Validator / Self-Healing Layer
3
+ *
4
+ * After the V3 agent writes files to the workspace, this module:
5
+ * 1. Detects available validators (tsc, npm test, py_compile).
6
+ * 2. Runs them in the project directory.
7
+ * 3. Returns structured pass/fail results for each tool.
8
+ *
9
+ * Used by runSelfHealingCycle in api.ts to send targeted correction prompts.
10
+ */
11
+ export interface ValidationResult {
12
+ ran: boolean;
13
+ tool: string;
14
+ passed: boolean;
15
+ output: string;
16
+ }
17
+ /**
18
+ * Run post-write validation in the project directory.
19
+ * Each check is time-bounded to avoid blocking the CLI.
20
+ */
21
+ export declare function runPostWriteValidation(workspacePath: string): Promise<ValidationResult[]>;
22
+ /**
23
+ * Build a clear error summary string from failed validation results.
24
+ */
25
+ export declare function formatValidationErrors(results: ValidationResult[]): string;
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ /**
3
+ * Vigthoria CLI — Post-Write Validator / Self-Healing Layer
4
+ *
5
+ * After the V3 agent writes files to the workspace, this module:
6
+ * 1. Detects available validators (tsc, npm test, py_compile).
7
+ * 2. Runs them in the project directory.
8
+ * 3. Returns structured pass/fail results for each tool.
9
+ *
10
+ * Used by runSelfHealingCycle in api.ts to send targeted correction prompts.
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.runPostWriteValidation = runPostWriteValidation;
17
+ exports.formatValidationErrors = formatValidationErrors;
18
+ const node_child_process_1 = require("node:child_process");
19
+ const node_fs_1 = __importDefault(require("node:fs"));
20
+ const node_path_1 = __importDefault(require("node:path"));
21
+ function commandExists(cmd, args = ['--version']) {
22
+ try {
23
+ (0, node_child_process_1.execFileSync)(cmd, args, { stdio: 'pipe', timeout: 4000, windowsHide: true });
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ function hasTsConfig(dir) {
31
+ return node_fs_1.default.existsSync(node_path_1.default.join(dir, 'tsconfig.json'));
32
+ }
33
+ /**
34
+ * Resolve the best available tsc binary for the project.
35
+ * Prefers local node_modules/.bin/tsc over npx to avoid network fetches.
36
+ */
37
+ function resolveTscBin(workspacePath) {
38
+ const localTsc = node_path_1.default.join(workspacePath, 'node_modules', '.bin', 'tsc');
39
+ if (node_fs_1.default.existsSync(localTsc))
40
+ return { cmd: localTsc, args: ["--noEmit", "--skipLibCheck"] };
41
+ try {
42
+ (0, node_child_process_1.execFileSync)("npx", ["--version"], { stdio: "pipe", timeout: 4000, windowsHide: true });
43
+ return { cmd: "npx", args: ["tsc", "--noEmit", "--skipLibCheck"] };
44
+ }
45
+ catch { /* npx not available */ }
46
+ try {
47
+ (0, node_child_process_1.execFileSync)("tsc", ["--version"], { stdio: "pipe", timeout: 4000, windowsHide: true });
48
+ return { cmd: "tsc", args: ["--noEmit", "--skipLibCheck"] };
49
+ }
50
+ catch { /* tsc not available */ }
51
+ return null;
52
+ }
53
+ function hasRealTestScript(dir) {
54
+ try {
55
+ const pkgPath = node_path_1.default.join(dir, 'package.json');
56
+ if (!node_fs_1.default.existsSync(pkgPath))
57
+ return false;
58
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf-8'));
59
+ const script = pkg.scripts?.test ?? '';
60
+ return typeof script === 'string' && script.length > 0
61
+ && !script.startsWith('echo "Error: no test specified"');
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ /**
68
+ * Run post-write validation in the project directory.
69
+ * Each check is time-bounded to avoid blocking the CLI.
70
+ */
71
+ async function runPostWriteValidation(workspacePath) {
72
+ if (!workspacePath || !node_fs_1.default.existsSync(workspacePath))
73
+ return [];
74
+ const results = [];
75
+ const opts = { cwd: workspacePath, stdio: 'pipe', windowsHide: true };
76
+ // 1. TypeScript type check
77
+ const tscBin = hasTsConfig(workspacePath) ? resolveTscBin(workspacePath) : null;
78
+ if (tscBin) {
79
+ try {
80
+ (0, node_child_process_1.execFileSync)(tscBin.cmd, tscBin.args, { ...opts, timeout: 45_000 });
81
+ results.push({ ran: true, tool: 'tsc', passed: true, output: '' });
82
+ }
83
+ catch (err) {
84
+ const out = [err.stderr?.toString(), err.stdout?.toString()].filter(Boolean).join('\n').slice(0, 4000);
85
+ results.push({ ran: true, tool: 'tsc', passed: false, output: out });
86
+ }
87
+ }
88
+ // 2. npm test
89
+ if (hasRealTestScript(workspacePath) && commandExists('npm')) {
90
+ try {
91
+ (0, node_child_process_1.execFileSync)('npm', ['test', '--', '--passWithNoTests'], { ...opts, timeout: 90_000 });
92
+ results.push({ ran: true, tool: 'npm test', passed: true, output: '' });
93
+ }
94
+ catch (err) {
95
+ const out = [err.stderr?.toString(), err.stdout?.toString()].filter(Boolean).join('\n').slice(0, 4000);
96
+ results.push({ ran: true, tool: 'npm test', passed: false, output: out });
97
+ }
98
+ }
99
+ // 3. Python syntax check
100
+ const hasPy = node_fs_1.default.existsSync(node_path_1.default.join(workspacePath, 'requirements.txt'))
101
+ || node_fs_1.default.existsSync(node_path_1.default.join(workspacePath, 'setup.py'))
102
+ || node_fs_1.default.existsSync(node_path_1.default.join(workspacePath, 'pyproject.toml'));
103
+ if (hasPy && commandExists('python3', ['-c', 'import ast'])) {
104
+ const pyFiles = [];
105
+ try {
106
+ const entries = node_fs_1.default.readdirSync(workspacePath, { withFileTypes: true });
107
+ for (const e of entries) {
108
+ if (e.isFile() && e.name.endsWith('.py'))
109
+ pyFiles.push(e.name);
110
+ }
111
+ }
112
+ catch { /* ignore */ }
113
+ let pyPassed = true;
114
+ let pyOut = '';
115
+ for (const pyFile of pyFiles.slice(0, 20)) {
116
+ try {
117
+ (0, node_child_process_1.execFileSync)('python3', ['-m', 'py_compile', pyFile], { ...opts, timeout: 10_000 });
118
+ }
119
+ catch (err) {
120
+ pyPassed = false;
121
+ pyOut += `${pyFile}: ${err.stderr?.toString()?.trim() || 'syntax error'}\n`;
122
+ }
123
+ }
124
+ if (pyFiles.length > 0) {
125
+ results.push({ ran: true, tool: 'py_compile', passed: pyPassed, output: pyOut.slice(0, 2000) });
126
+ }
127
+ }
128
+ return results;
129
+ }
130
+ /**
131
+ * Build a clear error summary string from failed validation results.
132
+ */
133
+ function formatValidationErrors(results) {
134
+ return results
135
+ .filter((r) => r.ran && !r.passed)
136
+ .map((r) => `[${r.tool}]\n${r.output.trim()}`)
137
+ .join('\n\n─────────────────────────\n\n');
138
+ }
@@ -3,6 +3,25 @@
3
3
  * Similar to Vigthoria's session persistence
4
4
  */
5
5
  import { ChatMessage } from './api.js';
6
+ export type AuthState = {
7
+ token: string | null;
8
+ expiresAt: number | null;
9
+ isValid: boolean;
10
+ };
11
+ export type CliError = {
12
+ code: string;
13
+ message: string;
14
+ details?: any;
15
+ isCritical: boolean;
16
+ };
17
+ /**
18
+ * Validate persisted authentication state without assuming any field is present.
19
+ */
20
+ export declare function validateSession(session: AuthState): boolean;
21
+ /**
22
+ * Load persisted authentication state and normalize nullable token fields safely.
23
+ */
24
+ export declare function loadSession(): Promise<AuthState>;
6
25
  export interface Session {
7
26
  id: string;
8
27
  name: string;
@@ -38,9 +38,96 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  })();
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  exports.SessionManager = void 0;
41
+ exports.validateSession = validateSession;
42
+ exports.loadSession = loadSession;
41
43
  const fs = __importStar(require("fs"));
42
44
  const path = __importStar(require("path"));
43
45
  const os = __importStar(require("os"));
46
+ function createSessionLoadError(message, details) {
47
+ return {
48
+ code: 'SESSION_LOAD_FAILED',
49
+ message,
50
+ details,
51
+ isCritical: true,
52
+ };
53
+ }
54
+ function normalizeAuthState(raw) {
55
+ if (!raw || typeof raw !== 'object') {
56
+ const session = { token: null, expiresAt: null, isValid: false };
57
+ session.isValid = validateSession(session);
58
+ return session;
59
+ }
60
+ const source = raw.auth && typeof raw.auth === 'object' ? raw.auth : raw;
61
+ const rawToken = source.token ?? source.accessToken ?? source.jwt ?? null;
62
+ const rawExpiresAt = source.expiresAt ?? null;
63
+ const token = typeof rawToken === 'string' && rawToken.trim().length > 0 ? rawToken.trim() : null;
64
+ const expiresAt = typeof rawExpiresAt === 'number' && Number.isFinite(rawExpiresAt) ? rawExpiresAt : null;
65
+ const session = { token, expiresAt, isValid: false };
66
+ session.isValid = validateSession(session);
67
+ return session;
68
+ }
69
+ /**
70
+ * Validate persisted authentication state without assuming any field is present.
71
+ */
72
+ function validateSession(session) {
73
+ if (!session || typeof session !== 'object') {
74
+ console.warn('Invalid session: session state is missing.');
75
+ return false;
76
+ }
77
+ if (typeof session.token !== 'string' || session.token.trim().length === 0) {
78
+ console.warn('Invalid session: authentication token is missing.');
79
+ return false;
80
+ }
81
+ if (typeof session.expiresAt !== 'number' || !Number.isFinite(session.expiresAt)) {
82
+ console.warn('Invalid session: expiration timestamp is missing.');
83
+ return false;
84
+ }
85
+ if (session.expiresAt <= Date.now()) {
86
+ console.warn('Invalid session: authentication token has expired.');
87
+ return false;
88
+ }
89
+ return true;
90
+ }
91
+ /**
92
+ * Load persisted authentication state and normalize nullable token fields safely.
93
+ */
94
+ async function loadSession() {
95
+ const configDir = path.join(os.homedir(), '.vigthoria');
96
+ const candidateFiles = [
97
+ path.join(configDir, 'auth.json'),
98
+ path.join(configDir, 'session.json'),
99
+ path.join(configDir, 'config.json'),
100
+ ];
101
+ try {
102
+ const sessionFile = candidateFiles.find((filePath) => fs.existsSync(filePath));
103
+ if (!sessionFile) {
104
+ const session = { token: null, expiresAt: null, isValid: false };
105
+ validateSession(session);
106
+ throw createSessionLoadError('Failed to load persisted authentication session: no session file found.');
107
+ }
108
+ const content = fs.readFileSync(sessionFile, 'utf-8');
109
+ const parsed = JSON.parse(content);
110
+ const session = normalizeAuthState(parsed);
111
+ if (session.token === null || session.expiresAt === null || !session.isValid) {
112
+ throw createSessionLoadError('Failed to load persisted authentication session: token is missing or expired.', {
113
+ sessionFile,
114
+ hasToken: session.token !== null,
115
+ expiresAt: session.expiresAt,
116
+ });
117
+ }
118
+ return session;
119
+ }
120
+ catch (error) {
121
+ if (error?.code === 'SESSION_LOAD_FAILED') {
122
+ console.error(error.message, error.details ?? '');
123
+ throw error;
124
+ }
125
+ console.error('Failed to load persisted authentication session.', error?.message ?? error);
126
+ throw createSessionLoadError('Failed to load persisted authentication session.', {
127
+ message: error?.message ?? String(error),
128
+ });
129
+ }
130
+ }
44
131
  class SessionManager {
45
132
  sessionsDir;
46
133
  compactThreshold = 40;
@@ -56,15 +143,12 @@ class SessionManager {
56
143
  }
57
144
  }
58
145
  catch (error) {
59
- // On permission errors, try alternative location
60
146
  if (error.code === 'EACCES' || error.code === 'EPERM') {
61
- // Fallback to temp directory
62
147
  this.sessionsDir = path.join(os.tmpdir(), 'vigthoria-sessions');
63
148
  if (!fs.existsSync(this.sessionsDir)) {
64
149
  fs.mkdirSync(this.sessionsDir, { recursive: true });
65
150
  }
66
151
  }
67
- // Silently continue if directory creation fails
68
152
  }
69
153
  }
70
154
  /**
@@ -117,7 +201,8 @@ class SessionManager {
117
201
  const content = fs.readFileSync(filePath, 'utf-8');
118
202
  return JSON.parse(content);
119
203
  }
120
- catch {
204
+ catch (error) {
205
+ console.warn(`Failed to load session ${id}:`, error);
121
206
  return null;
122
207
  }
123
208
  }
@@ -141,11 +226,11 @@ class SessionManager {
141
226
  try {
142
227
  const content = fs.readFileSync(path.join(this.sessionsDir, f), 'utf-8');
143
228
  const session = JSON.parse(content);
144
- // Return without messages for efficiency
145
229
  const { messages, ...metadata } = session;
146
230
  return { ...metadata, messages: [] };
147
231
  }
148
- catch {
232
+ catch (error) {
233
+ console.warn(`Failed to read session metadata from ${f}:`, error);
149
234
  return null;
150
235
  }
151
236
  }).filter(Boolean);
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Vigthoria CLI — Multi-Step Terminal Task Display
3
+ *
4
+ * Renders a live, updating task progress list in the terminal using ANSI
5
+ * escape codes and chalk — no external UI framework required.
6
+ *
7
+ * Only activates when stderr is a real TTY; JSON mode and piped output stay clean.
8
+ */
9
+ export type TaskStatus = 'pending' | 'running' | 'done' | 'error' | 'skipped';
10
+ export interface Task {
11
+ label: string;
12
+ status: TaskStatus;
13
+ detail?: string;
14
+ }
15
+ export declare class TaskDisplay {
16
+ private tasks;
17
+ private linesRendered;
18
+ private readonly enabled;
19
+ constructor(labels: string[], enabled?: boolean);
20
+ private clearLines;
21
+ private renderLines;
22
+ render(): void;
23
+ start(index: number, detail?: string): void;
24
+ complete(index: number, detail?: string): void;
25
+ fail(index: number, detail?: string): void;
26
+ skip(index: number, detail?: string): void;
27
+ setDetail(index: number, detail: string): void;
28
+ clear(): void;
29
+ finalize(): void;
30
+ get isEnabled(): boolean;
31
+ }