@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.
- package/dist/cli/agent.js +28 -1
- package/dist/cli/bridge.d.ts +22 -0
- package/dist/cli/bridge.js +268 -74
- package/dist/cli/oneshot.js +59 -43
- package/dist/cli/snapshot.d.ts +33 -0
- package/dist/cli/snapshot.js +225 -0
- package/dist/overlay.js +443 -2
- package/package.json +1 -2
package/dist/cli/oneshot.js
CHANGED
|
@@ -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", "
|
|
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", "
|
|
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
|
-
//
|
|
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", "
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
637
|
-
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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 &&
|
|
654
|
-
|
|
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
|
+
}
|