@lasso-ai/cli 1.3.2 → 1.3.4

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.
@@ -209,16 +209,13 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
209
209
  const isConversational = await isConversationalPrompt(prompt, config, apiKey);
210
210
  console.log('[Lasso] Is conversational:', isConversational);
211
211
  // Browser inspection & snapshot steps - skip for simple conversational queries
212
+ // FAST CONTEXT: Skip detailed DOM analysis if pageContext is already provided
212
213
  if (pageContext && !isConversational) {
213
- onProgress("thinking", "Inspecting browser page...", `Page: "${pageContext.title || "Landing page"}" (${pageContext.route || "/"})`);
214
+ onProgress("thinking", "Analyzing page context...", `Page: "${pageContext.title || "Landing page"}" (${pageContext.route || "/"})`);
215
+ // Skip the slow "Scrolling & analyzing" steps - the pageContext already has the data
216
+ // Only show viewport info if available
214
217
  if (pageContext.viewport) {
215
- onProgress("thinking", "Capturing visual snapshot...", `Viewport: ${pageContext.viewport.width}x${pageContext.viewport.height} (Document height: ${pageContext.viewport.scrollHeight}px)`);
216
- }
217
- if (pageContext.domSummary) {
218
- const sectionsCount = pageContext.domSummary.sections?.length || 0;
219
- const headingsCount = pageContext.domSummary.headings?.length || 0;
220
- const buttonsCount = pageContext.domSummary.buttons?.length || 0;
221
- onProgress("thinking", "Scrolling & analyzing page sections...", `${sectionsCount} sections, ${headingsCount} headings, ${buttonsCount} CTAs detected`);
218
+ onProgress("thinking", "Ready to proceed...", `Viewport: ${pageContext.viewport.width}x${pageContext.viewport.height}`);
222
219
  }
223
220
  }
224
221
  else {
@@ -599,47 +596,69 @@ async function analyzeProject(cwd) {
599
596
  const hasReact = dependencies.includes("react") || devDependencies.includes("react");
600
597
  const hasNext = dependencies.includes("next") || devDependencies.includes("next");
601
598
  const hasVite = dependencies.includes("vite") || devDependencies.includes("vite");
602
- // Scan directory structure
599
+ // FAST CONTEXT: Only scan top-level directories, not full recursive scan
600
+ // This is the key optimization - let the server-side agent handle file discovery
603
601
  const files = [];
604
602
  const directories = [];
605
603
  const appDirs = [];
606
604
  const pagesDirs = [];
607
- const roots = ["src", "app", "pages", ".", "web", "lib", "components"];
605
+ const roots = ["src", "app", "pages", "components", "lib"];
608
606
  for (const root of roots) {
609
607
  const rootPath = node_path_1.default.join(cwd, root);
610
608
  if (!node_fs_1.default.existsSync(rootPath))
611
609
  continue;
612
- const scanDir = (dir, base = "", depth = 0) => {
613
- if (depth > 5)
614
- return;
615
- try {
616
- const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
617
- for (const entry of entries) {
618
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === ".next") {
619
- continue;
620
- }
621
- const rel = base ? node_path_1.default.join(base, entry.name) : entry.name;
622
- if (entry.isDirectory()) {
623
- directories.push(rel);
624
- // Track app and pages directories
625
- if (entry.name === "app")
626
- appDirs.push(rel);
627
- if (entry.name === "pages")
628
- pagesDirs.push(rel);
629
- scanDir(node_path_1.default.join(dir, entry.name), rel, depth + 1);
630
- }
631
- else if (/\.(tsx?|jsx?|vue|svelte|css|html|json)$/i.test(entry.name)) {
632
- files.push(rel);
610
+ // Only scan top-level (depth 0-1), not recursive
611
+ try {
612
+ const entries = node_fs_1.default.readdirSync(rootPath, { withFileTypes: true });
613
+ for (const entry of entries) {
614
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
615
+ continue;
616
+ }
617
+ const rel = entry.name;
618
+ if (entry.isDirectory()) {
619
+ directories.push(rel);
620
+ // Track app and pages directories at top level
621
+ if (entry.name === "app")
622
+ appDirs.push(rel);
623
+ if (entry.name === "pages")
624
+ pagesDirs.push(rel);
625
+ // Only scan one level deep for key directories
626
+ if (["components", "lib", "hooks", "utils", "styles"].includes(entry.name)) {
627
+ try {
628
+ const subEntries = node_fs_1.default.readdirSync(node_path_1.default.join(rootPath, entry.name), { withFileTypes: true });
629
+ for (const subEntry of subEntries) {
630
+ if (!subEntry.name.startsWith(".") && !subEntry.isDirectory() && /\.(tsx?|jsx?|vue|svelte|css)$/i.test(subEntry.name)) {
631
+ files.push(node_path_1.default.join(rel, subEntry.name));
632
+ }
633
+ }
634
+ }
635
+ catch {
636
+ // ignore scan errors
637
+ }
633
638
  }
634
639
  }
640
+ else if (/\.(tsx?|jsx?|vue|svelte|css|html|json)$/i.test(entry.name)) {
641
+ files.push(rel);
642
+ }
635
643
  }
636
- catch {
637
- // ignore scan errors
644
+ }
645
+ catch {
646
+ // ignore scan errors
647
+ }
648
+ if (files.length > 20)
649
+ break; // Limit to 20 files for fast context
650
+ }
651
+ // Also check root directory for key files
652
+ try {
653
+ const rootEntries = node_fs_1.default.readdirSync(cwd, { withFileTypes: true });
654
+ for (const entry of rootEntries) {
655
+ if (!entry.isDirectory() && /\.(tsx?|jsx?|vue|svelte|css|html|json)$/i.test(entry.name)) {
656
+ files.push(entry.name);
638
657
  }
639
- };
640
- scanDir(rootPath);
641
- if (files.length > 100)
642
- break;
658
+ }
659
+ }
660
+ catch {
661
+ // ignore scan errors
643
662
  }
644
663
  // Determine source root
645
664
  let sourceRoot = ".";
@@ -649,12 +668,9 @@ async function analyzeProject(cwd) {
649
668
  sourceRoot = ".";
650
669
  else if (directories.includes("src"))
651
670
  sourceRoot = "src";
652
- // Detect App Router vs Pages Router
653
- const appRouter = appDirs.length > 0 && appDirs.some(dir => {
654
- const appPath = node_path_1.default.join(cwd, dir);
655
- return node_fs_1.default.existsSync(node_path_1.default.join(appPath, "layout.tsx")) || node_fs_1.default.existsSync(node_path_1.default.join(appPath, "page.tsx"));
656
- });
657
- const pagesRouter = pagesDirs.length > 0 && node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pages", "_app.tsx")) || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pages", "_document.tsx"));
671
+ // Detect App Router vs Pages Router (lightweight check)
672
+ const appRouter = appDirs.length > 0 && node_fs_1.default.existsSync(node_path_1.default.join(cwd, "app", "layout.tsx")) || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "app", "page.tsx"));
673
+ const pagesRouter = pagesDirs.length > 0 && (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pages", "_app.tsx")) || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pages", "_document.tsx")));
658
674
  return {
659
675
  framework: (0, framework_1.detectFramework)(cwd),
660
676
  hasTypeScript,
@@ -0,0 +1,33 @@
1
+ export type FileSnapshot = {
2
+ filePath: string;
3
+ content: string | null;
4
+ hash: string;
5
+ timestamp: string;
6
+ };
7
+ export type TimelineSnapshot = {
8
+ id: string;
9
+ taskId: string;
10
+ summary: string;
11
+ files: FileSnapshot[];
12
+ createdAt: string;
13
+ parentSnapshotId?: string;
14
+ };
15
+ export type ConflictInfo = {
16
+ filePath: string;
17
+ expectedHash: string;
18
+ actualHash: string;
19
+ originalContent: string;
20
+ currentContent: string;
21
+ };
22
+ export declare function createSnapshot(cwd: string, taskId: string, summary: string, filePaths: string[], parentSnapshotId?: string): TimelineSnapshot;
23
+ export declare function restoreSnapshot(cwd: string, snapshotId: string): boolean;
24
+ export declare function detectConflicts(cwd: string, expectedFiles: Array<{
25
+ filePath: string;
26
+ content: string | null;
27
+ hash?: string;
28
+ }>): ConflictInfo[];
29
+ export declare function getTimeline(cwd: string, limit?: number): TimelineSnapshot[];
30
+ export declare function deleteSnapshot(cwd: string, snapshotId: string): boolean;
31
+ export declare function clearSnapshots(cwd: string): void;
32
+ export declare function getSnapshotByTaskId(cwd: string, taskId: string): TimelineSnapshot | undefined;
33
+ export declare function getLatestSnapshot(cwd: string): TimelineSnapshot | undefined;
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createSnapshot = createSnapshot;
7
+ exports.restoreSnapshot = restoreSnapshot;
8
+ exports.detectConflicts = detectConflicts;
9
+ exports.getTimeline = getTimeline;
10
+ exports.deleteSnapshot = deleteSnapshot;
11
+ exports.clearSnapshots = clearSnapshots;
12
+ exports.getSnapshotByTaskId = getSnapshotByTaskId;
13
+ exports.getLatestSnapshot = getLatestSnapshot;
14
+ // src/cli/snapshot.ts
15
+ // Local history snapshot system for safe concurrent edits
16
+ const node_fs_1 = __importDefault(require("node:fs"));
17
+ const node_path_1 = __importDefault(require("node:path"));
18
+ const node_crypto_1 = __importDefault(require("node:crypto"));
19
+ const SNAPSHOTS_DIR = ".lasso/snapshots";
20
+ const TIMELINE_FILE = ".lasso/timeline.json";
21
+ const MAX_SNAPSHOTS = 50;
22
+ // Ensure snapshots directory exists
23
+ function ensureSnapshotsDir(cwd) {
24
+ const dir = node_path_1.default.join(cwd, SNAPSHOTS_DIR);
25
+ if (!node_fs_1.default.existsSync(dir)) {
26
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
27
+ }
28
+ }
29
+ // Generate content hash
30
+ function hashContent(content) {
31
+ return node_crypto_1.default.createHash("sha256").update(content).digest("hex");
32
+ }
33
+ // Generate unique snapshot ID
34
+ function generateSnapshotId() {
35
+ return `snap-${Date.now()}-${node_crypto_1.default.randomBytes(4).toString("hex")}`;
36
+ }
37
+ // Load timeline from disk
38
+ function loadTimeline(cwd) {
39
+ const timelinePath = node_path_1.default.join(cwd, TIMELINE_FILE);
40
+ try {
41
+ if (node_fs_1.default.existsSync(timelinePath)) {
42
+ const data = node_fs_1.default.readFileSync(timelinePath, "utf8");
43
+ return JSON.parse(data);
44
+ }
45
+ }
46
+ catch {
47
+ // File might be corrupted, start fresh
48
+ }
49
+ return [];
50
+ }
51
+ // Save timeline to disk
52
+ function saveTimeline(cwd, timeline) {
53
+ const timelinePath = node_path_1.default.join(cwd, TIMELINE_FILE);
54
+ try {
55
+ node_fs_1.default.writeFileSync(timelinePath, JSON.stringify(timeline, null, 2));
56
+ }
57
+ catch (error) {
58
+ console.warn("[lasso] Failed to save timeline:", error);
59
+ }
60
+ }
61
+ // Create a snapshot of current file states
62
+ function createSnapshot(cwd, taskId, summary, filePaths, parentSnapshotId) {
63
+ ensureSnapshotsDir(cwd);
64
+ const snapshotId = generateSnapshotId();
65
+ const files = [];
66
+ const timestamp = new Date().toISOString();
67
+ for (const filePath of filePaths) {
68
+ const fullPath = node_path_1.default.resolve(cwd, filePath);
69
+ let content = null;
70
+ let hash = "";
71
+ try {
72
+ if (node_fs_1.default.existsSync(fullPath)) {
73
+ content = node_fs_1.default.readFileSync(fullPath, "utf8");
74
+ hash = hashContent(content);
75
+ }
76
+ }
77
+ catch (error) {
78
+ console.warn(`[lasso] Failed to snapshot ${filePath}:`, error);
79
+ }
80
+ files.push({
81
+ filePath,
82
+ content,
83
+ hash,
84
+ timestamp,
85
+ });
86
+ }
87
+ const snapshot = {
88
+ id: snapshotId,
89
+ taskId,
90
+ summary,
91
+ files,
92
+ createdAt: timestamp,
93
+ parentSnapshotId,
94
+ };
95
+ // Update timeline
96
+ const timeline = loadTimeline(cwd);
97
+ timeline.unshift(snapshot);
98
+ // Trim old snapshots
99
+ if (timeline.length > MAX_SNAPSHOTS) {
100
+ const removed = timeline.splice(MAX_SNAPSHOTS);
101
+ // Cleanup old snapshot files
102
+ for (const old of removed) {
103
+ cleanupSnapshotFiles(cwd, old.id);
104
+ }
105
+ }
106
+ saveTimeline(cwd, timeline);
107
+ return snapshot;
108
+ }
109
+ // Restore a snapshot
110
+ function restoreSnapshot(cwd, snapshotId) {
111
+ const timeline = loadTimeline(cwd);
112
+ const snapshot = timeline.find((s) => s.id === snapshotId);
113
+ if (!snapshot) {
114
+ console.warn(`[lasso] Snapshot ${snapshotId} not found`);
115
+ return false;
116
+ }
117
+ try {
118
+ for (const file of snapshot.files) {
119
+ const fullPath = node_path_1.default.resolve(cwd, file.filePath);
120
+ if (file.content === null) {
121
+ // File didn't exist, delete if it exists now
122
+ if (node_fs_1.default.existsSync(fullPath)) {
123
+ node_fs_1.default.unlinkSync(fullPath);
124
+ }
125
+ }
126
+ else {
127
+ // Restore file content
128
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(fullPath), { recursive: true });
129
+ node_fs_1.default.writeFileSync(fullPath, file.content);
130
+ }
131
+ }
132
+ console.log(`[lasso] Restored snapshot ${snapshotId}: ${snapshot.summary}`);
133
+ return true;
134
+ }
135
+ catch (error) {
136
+ console.error(`[lasso] Failed to restore snapshot ${snapshotId}:`, error);
137
+ return false;
138
+ }
139
+ }
140
+ // Check for conflicts between expected and current file states
141
+ function detectConflicts(cwd, expectedFiles) {
142
+ const conflicts = [];
143
+ for (const expected of expectedFiles) {
144
+ const fullPath = node_path_1.default.resolve(cwd, expected.filePath);
145
+ const expectedHash = expected.hash || (expected.content ? hashContent(expected.content) : "");
146
+ try {
147
+ if (!node_fs_1.default.existsSync(fullPath)) {
148
+ // File doesn't exist but was expected
149
+ if (expected.content !== null) {
150
+ conflicts.push({
151
+ filePath: expected.filePath,
152
+ expectedHash,
153
+ actualHash: "file-missing",
154
+ originalContent: expected.content || "",
155
+ currentContent: "",
156
+ });
157
+ }
158
+ continue;
159
+ }
160
+ const currentContent = node_fs_1.default.readFileSync(fullPath, "utf8");
161
+ const actualHash = hashContent(currentContent);
162
+ if (expectedHash !== actualHash) {
163
+ conflicts.push({
164
+ filePath: expected.filePath,
165
+ expectedHash,
166
+ actualHash,
167
+ originalContent: expected.content || "",
168
+ currentContent,
169
+ });
170
+ }
171
+ }
172
+ catch (error) {
173
+ console.warn(`[lasso] Failed to check conflict for ${expected.filePath}:`, error);
174
+ }
175
+ }
176
+ return conflicts;
177
+ }
178
+ // Get timeline history
179
+ function getTimeline(cwd, limit = 20) {
180
+ const timeline = loadTimeline(cwd);
181
+ return timeline.slice(0, limit);
182
+ }
183
+ // Delete a snapshot
184
+ function deleteSnapshot(cwd, snapshotId) {
185
+ const timeline = loadTimeline(cwd);
186
+ const index = timeline.findIndex((s) => s.id === snapshotId);
187
+ if (index === -1) {
188
+ return false;
189
+ }
190
+ timeline.splice(index, 1);
191
+ saveTimeline(cwd, timeline);
192
+ cleanupSnapshotFiles(cwd, snapshotId);
193
+ return true;
194
+ }
195
+ // Cleanup snapshot-specific files (if we implement per-snapshot storage)
196
+ function cleanupSnapshotFiles(cwd, snapshotId) {
197
+ // Currently all data is in timeline.json, but if we add per-snapshot files,
198
+ // this is where we'd clean them up
199
+ }
200
+ // Clear all snapshots
201
+ function clearSnapshots(cwd) {
202
+ const timelinePath = node_path_1.default.join(cwd, TIMELINE_FILE);
203
+ try {
204
+ if (node_fs_1.default.existsSync(timelinePath)) {
205
+ node_fs_1.default.unlinkSync(timelinePath);
206
+ }
207
+ const snapshotsDir = node_path_1.default.join(cwd, SNAPSHOTS_DIR);
208
+ if (node_fs_1.default.existsSync(snapshotsDir)) {
209
+ node_fs_1.default.rmSync(snapshotsDir, { recursive: true, force: true });
210
+ }
211
+ }
212
+ catch (error) {
213
+ console.warn("[lasso] Failed to clear snapshots:", error);
214
+ }
215
+ }
216
+ // Get snapshot by task ID
217
+ function getSnapshotByTaskId(cwd, taskId) {
218
+ const timeline = loadTimeline(cwd);
219
+ return timeline.find((s) => s.taskId === taskId);
220
+ }
221
+ // Get latest snapshot
222
+ function getLatestSnapshot(cwd) {
223
+ const timeline = loadTimeline(cwd);
224
+ return timeline[0];
225
+ }