chainlesschain 0.37.8 → 0.37.10

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.
Files changed (59) hide show
  1. package/README.md +403 -8
  2. package/bin/chainlesschain.js +4 -0
  3. package/package.json +7 -2
  4. package/src/commands/agent.js +30 -0
  5. package/src/commands/ask.js +114 -0
  6. package/src/commands/audit.js +286 -0
  7. package/src/commands/auth.js +387 -0
  8. package/src/commands/browse.js +184 -0
  9. package/src/commands/chat.js +35 -0
  10. package/src/commands/db.js +152 -0
  11. package/src/commands/did.js +376 -0
  12. package/src/commands/encrypt.js +233 -0
  13. package/src/commands/export.js +125 -0
  14. package/src/commands/git.js +215 -0
  15. package/src/commands/import.js +259 -0
  16. package/src/commands/instinct.js +202 -0
  17. package/src/commands/llm.js +288 -0
  18. package/src/commands/mcp.js +302 -0
  19. package/src/commands/memory.js +282 -0
  20. package/src/commands/note.js +489 -0
  21. package/src/commands/org.js +505 -0
  22. package/src/commands/p2p.js +274 -0
  23. package/src/commands/plugin.js +398 -0
  24. package/src/commands/search.js +237 -0
  25. package/src/commands/session.js +238 -0
  26. package/src/commands/skill.js +479 -0
  27. package/src/commands/sync.js +249 -0
  28. package/src/commands/tokens.js +214 -0
  29. package/src/commands/wallet.js +416 -0
  30. package/src/index.js +65 -0
  31. package/src/lib/audit-logger.js +364 -0
  32. package/src/lib/bm25-search.js +322 -0
  33. package/src/lib/browser-automation.js +216 -0
  34. package/src/lib/crypto-manager.js +246 -0
  35. package/src/lib/did-manager.js +270 -0
  36. package/src/lib/ensure-utf8.js +59 -0
  37. package/src/lib/git-integration.js +220 -0
  38. package/src/lib/instinct-manager.js +190 -0
  39. package/src/lib/knowledge-exporter.js +302 -0
  40. package/src/lib/knowledge-importer.js +293 -0
  41. package/src/lib/llm-providers.js +325 -0
  42. package/src/lib/mcp-client.js +413 -0
  43. package/src/lib/memory-manager.js +211 -0
  44. package/src/lib/note-versioning.js +244 -0
  45. package/src/lib/org-manager.js +424 -0
  46. package/src/lib/p2p-manager.js +317 -0
  47. package/src/lib/pdf-parser.js +96 -0
  48. package/src/lib/permission-engine.js +374 -0
  49. package/src/lib/plan-mode.js +333 -0
  50. package/src/lib/platform.js +15 -0
  51. package/src/lib/plugin-manager.js +312 -0
  52. package/src/lib/response-cache.js +156 -0
  53. package/src/lib/session-manager.js +189 -0
  54. package/src/lib/sync-manager.js +347 -0
  55. package/src/lib/token-tracker.js +200 -0
  56. package/src/lib/wallet-manager.js +348 -0
  57. package/src/repl/agent-repl.js +912 -0
  58. package/src/repl/chat-repl.js +262 -0
  59. package/src/runtime/bootstrap.js +159 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Headless browser automation — fetch, scrape, and extract content from web pages.
3
+ * Uses built-in fetch for basic operations, optional playwright for screenshots.
4
+ */
5
+
6
+ /**
7
+ * Fetch a URL and return the raw HTML.
8
+ */
9
+ export async function fetchPage(url, options = {}) {
10
+ const timeout = options.timeout || 30000;
11
+ const controller = new AbortController();
12
+ const timer = setTimeout(() => controller.abort(), timeout);
13
+
14
+ try {
15
+ const headers = {
16
+ "User-Agent": "ChainlessChain-CLI/0.37.9",
17
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
18
+ ...(options.headers || {}),
19
+ };
20
+
21
+ const response = await fetch(url, {
22
+ headers,
23
+ signal: controller.signal,
24
+ redirect: "follow",
25
+ });
26
+
27
+ if (!response.ok) {
28
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
29
+ }
30
+
31
+ const contentType = response.headers.get("content-type") || "";
32
+ const html = await response.text();
33
+
34
+ return {
35
+ url: response.url,
36
+ status: response.status,
37
+ contentType,
38
+ html,
39
+ size: html.length,
40
+ };
41
+ } finally {
42
+ clearTimeout(timer);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Extract text content from HTML, stripping tags.
48
+ */
49
+ export function extractText(html) {
50
+ return html
51
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
52
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
53
+ .replace(/<nav[\s\S]*?<\/nav>/gi, "")
54
+ .replace(/<footer[\s\S]*?<\/footer>/gi, "")
55
+ .replace(/<!--[\s\S]*?-->/g, "")
56
+ .replace(/<br\s*\/?>/gi, "\n")
57
+ .replace(/<\/p>/gi, "\n\n")
58
+ .replace(/<\/div>/gi, "\n")
59
+ .replace(/<\/h[1-6]>/gi, "\n\n")
60
+ .replace(/<\/li>/gi, "\n")
61
+ .replace(/<[^>]+>/g, "")
62
+ .replace(/&nbsp;/g, " ")
63
+ .replace(/&amp;/g, "&")
64
+ .replace(/&lt;/g, "<")
65
+ .replace(/&gt;/g, ">")
66
+ .replace(/&quot;/g, '"')
67
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n)))
68
+ .replace(/\n{3,}/g, "\n\n")
69
+ .replace(/[ \t]+/g, " ")
70
+ .split("\n")
71
+ .map((l) => l.trim())
72
+ .join("\n")
73
+ .trim();
74
+ }
75
+
76
+ /**
77
+ * Extract page title from HTML.
78
+ */
79
+ export function extractTitle(html) {
80
+ const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
81
+ return match ? match[1].trim().replace(/\s+/g, " ") : "";
82
+ }
83
+
84
+ /**
85
+ * Extract meta description from HTML.
86
+ */
87
+ export function extractMeta(html) {
88
+ const descMatch =
89
+ html.match(
90
+ /<meta\s+name=["']description["']\s+content=["']([^"']*?)["']/i,
91
+ ) ||
92
+ html.match(/<meta\s+content=["']([^"']*?)["']\s+name=["']description["']/i);
93
+ return descMatch ? descMatch[1] : "";
94
+ }
95
+
96
+ /**
97
+ * Extract elements matching a simple CSS selector.
98
+ * Supports: tag, .class, #id, tag.class, tag#id
99
+ */
100
+ export function querySelectorAll(html, selector) {
101
+ const results = [];
102
+
103
+ // Parse selector
104
+ const tagMatch = selector.match(/^(\w+)/);
105
+ const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
106
+ const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
107
+
108
+ const tag = tagMatch ? tagMatch[1] : null;
109
+ const className = classMatch ? classMatch[1] : null;
110
+ const id = idMatch ? idMatch[1] : null;
111
+
112
+ // Build regex pattern
113
+ let pattern;
114
+ if (tag) {
115
+ pattern = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "gi");
116
+ } else if (className || id) {
117
+ pattern = new RegExp(`<\\w+[^>]*>([\\s\\S]*?)<\\/\\w+>`, "gi");
118
+ } else {
119
+ return results;
120
+ }
121
+
122
+ let match;
123
+ while ((match = pattern.exec(html)) !== null) {
124
+ const fullTag = match[0];
125
+
126
+ // Check class filter
127
+ if (className) {
128
+ const classAttr = fullTag.match(/class=["']([^"']*?)["']/i);
129
+ if (!classAttr || !classAttr[1].split(/\s+/).includes(className))
130
+ continue;
131
+ }
132
+
133
+ // Check id filter
134
+ if (id) {
135
+ const idAttr = fullTag.match(/id=["']([^"']*?)["']/i);
136
+ if (!idAttr || idAttr[1] !== id) continue;
137
+ }
138
+
139
+ results.push({
140
+ html: fullTag,
141
+ text: extractText(fullTag),
142
+ });
143
+ }
144
+
145
+ return results;
146
+ }
147
+
148
+ /**
149
+ * Extract all links from HTML.
150
+ */
151
+ export function extractLinks(html, baseUrl) {
152
+ const links = [];
153
+ const linkRegex = /<a\s+[^>]*href=["']([^"'#]+)["'][^>]*>([\s\S]*?)<\/a>/gi;
154
+ let match;
155
+
156
+ while ((match = linkRegex.exec(html)) !== null) {
157
+ let href = match[1];
158
+ const text = extractText(match[2]).trim();
159
+
160
+ // Resolve relative URLs
161
+ if (baseUrl && !href.startsWith("http")) {
162
+ try {
163
+ href = new URL(href, baseUrl).href;
164
+ } catch {
165
+ continue;
166
+ }
167
+ }
168
+
169
+ if (text && href.startsWith("http")) {
170
+ links.push({ href, text });
171
+ }
172
+ }
173
+
174
+ return links;
175
+ }
176
+
177
+ /**
178
+ * Take a screenshot using playwright (optional dependency).
179
+ * Returns null if playwright is not installed.
180
+ */
181
+ export async function takeScreenshot(url, outputPath, options = {}) {
182
+ try {
183
+ const { chromium } = await import("playwright");
184
+ const browser = await chromium.launch({ headless: true });
185
+ const page = await browser.newPage({
186
+ viewport: {
187
+ width: options.width || 1280,
188
+ height: options.height || 720,
189
+ },
190
+ });
191
+
192
+ await page.goto(url, {
193
+ waitUntil: options.waitUntil || "networkidle",
194
+ timeout: options.timeout || 30000,
195
+ });
196
+
197
+ await page.screenshot({
198
+ path: outputPath,
199
+ fullPage: options.fullPage || false,
200
+ });
201
+
202
+ await browser.close();
203
+ return { success: true, path: outputPath };
204
+ } catch (err) {
205
+ if (
206
+ err.code === "ERR_MODULE_NOT_FOUND" ||
207
+ err.message?.includes("Cannot find")
208
+ ) {
209
+ return {
210
+ success: false,
211
+ error: "playwright not installed. Run: npm install -g playwright",
212
+ };
213
+ }
214
+ throw err;
215
+ }
216
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Crypto Manager — file encryption/decryption using AES-256-GCM.
3
+ * Uses Node.js built-in crypto module, no external dependencies.
4
+ */
5
+
6
+ import crypto from "crypto";
7
+ import fs from "fs";
8
+ import path from "path";
9
+
10
+ const ALGORITHM = "aes-256-gcm";
11
+ const KEY_LENGTH = 32; // 256 bits
12
+ const IV_LENGTH = 12; // 96 bits for GCM
13
+ const SALT_LENGTH = 32;
14
+ const TAG_LENGTH = 16;
15
+ const PBKDF2_ITERATIONS = 100000;
16
+ const MAGIC_HEADER = Buffer.from("CCLC01"); // ChainLessChain v01
17
+
18
+ /**
19
+ * Derive a key from a password using PBKDF2.
20
+ */
21
+ export function deriveKey(password, salt) {
22
+ return crypto.pbkdf2Sync(
23
+ password,
24
+ salt,
25
+ PBKDF2_ITERATIONS,
26
+ KEY_LENGTH,
27
+ "sha512",
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Encrypt a buffer with AES-256-GCM.
33
+ * Returns: MAGIC(6) + SALT(32) + IV(12) + TAG(16) + CIPHERTEXT
34
+ */
35
+ export function encryptBuffer(plaintext, password) {
36
+ const salt = crypto.randomBytes(SALT_LENGTH);
37
+ const key = deriveKey(password, salt);
38
+ const iv = crypto.randomBytes(IV_LENGTH);
39
+
40
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
41
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
42
+ const tag = cipher.getAuthTag();
43
+
44
+ return Buffer.concat([MAGIC_HEADER, salt, iv, tag, encrypted]);
45
+ }
46
+
47
+ /**
48
+ * Decrypt a buffer encrypted with encryptBuffer.
49
+ */
50
+ export function decryptBuffer(data, password) {
51
+ // Validate magic header
52
+ if (
53
+ data.length <
54
+ MAGIC_HEADER.length + SALT_LENGTH + IV_LENGTH + TAG_LENGTH
55
+ ) {
56
+ throw new Error("Invalid encrypted data: too short");
57
+ }
58
+
59
+ const magic = data.subarray(0, MAGIC_HEADER.length);
60
+ if (!magic.equals(MAGIC_HEADER)) {
61
+ throw new Error(
62
+ "Invalid encrypted data: bad header (not a ChainlessChain encrypted file)",
63
+ );
64
+ }
65
+
66
+ let offset = MAGIC_HEADER.length;
67
+ const salt = data.subarray(offset, offset + SALT_LENGTH);
68
+ offset += SALT_LENGTH;
69
+ const iv = data.subarray(offset, offset + IV_LENGTH);
70
+ offset += IV_LENGTH;
71
+ const tag = data.subarray(offset, offset + TAG_LENGTH);
72
+ offset += TAG_LENGTH;
73
+ const ciphertext = data.subarray(offset);
74
+
75
+ const key = deriveKey(password, salt);
76
+
77
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
78
+ decipher.setAuthTag(tag);
79
+
80
+ try {
81
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
82
+ } catch (_err) {
83
+ throw new Error("Decryption failed: wrong password or corrupted data");
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Encrypt a file to a .enc output file.
89
+ */
90
+ export function encryptFile(inputPath, password, outputPath) {
91
+ if (!fs.existsSync(inputPath)) {
92
+ throw new Error(`File not found: ${inputPath}`);
93
+ }
94
+
95
+ const plaintext = fs.readFileSync(inputPath);
96
+ const encrypted = encryptBuffer(plaintext, password);
97
+
98
+ const outPath = outputPath || `${inputPath}.enc`;
99
+ fs.writeFileSync(outPath, encrypted);
100
+
101
+ return {
102
+ inputPath,
103
+ outputPath: outPath,
104
+ originalSize: plaintext.length,
105
+ encryptedSize: encrypted.length,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Decrypt a .enc file.
111
+ */
112
+ export function decryptFile(inputPath, password, outputPath) {
113
+ if (!fs.existsSync(inputPath)) {
114
+ throw new Error(`File not found: ${inputPath}`);
115
+ }
116
+
117
+ const data = fs.readFileSync(inputPath);
118
+ const plaintext = decryptBuffer(data, password);
119
+
120
+ // Default output: remove .enc extension, or add .dec
121
+ const outPath =
122
+ outputPath ||
123
+ (inputPath.endsWith(".enc") ? inputPath.slice(0, -4) : `${inputPath}.dec`);
124
+
125
+ fs.writeFileSync(outPath, plaintext);
126
+
127
+ return {
128
+ inputPath,
129
+ outputPath: outPath,
130
+ encryptedSize: data.length,
131
+ decryptedSize: plaintext.length,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Check if a file is encrypted with our format.
137
+ */
138
+ export function isEncryptedFile(filePath) {
139
+ try {
140
+ const fd = fs.openSync(filePath, "r");
141
+ const buf = Buffer.alloc(MAGIC_HEADER.length);
142
+ fs.readSync(fd, buf, 0, MAGIC_HEADER.length, 0);
143
+ fs.closeSync(fd);
144
+ return buf.equals(MAGIC_HEADER);
145
+ } catch (_err) {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Generate a random encryption key (hex string).
152
+ */
153
+ export function generateKey() {
154
+ return crypto.randomBytes(KEY_LENGTH).toString("hex");
155
+ }
156
+
157
+ /**
158
+ * Hash a password for storage (SHA-256 + salt).
159
+ * Returns { hash, salt } as hex strings.
160
+ */
161
+ export function hashPassword(password) {
162
+ const salt = crypto.randomBytes(16).toString("hex");
163
+ const hash = crypto
164
+ .createHash("sha256")
165
+ .update(password + salt)
166
+ .digest("hex");
167
+ return { hash, salt };
168
+ }
169
+
170
+ /**
171
+ * Verify a password against a stored hash+salt.
172
+ */
173
+ export function verifyPassword(password, hash, salt) {
174
+ const computed = crypto
175
+ .createHash("sha256")
176
+ .update(password + salt)
177
+ .digest("hex");
178
+ return crypto.timingSafeEqual(
179
+ Buffer.from(computed, "hex"),
180
+ Buffer.from(hash, "hex"),
181
+ );
182
+ }
183
+
184
+ /**
185
+ * Ensure crypto metadata table exists.
186
+ */
187
+ export function ensureCryptoTable(db) {
188
+ db.exec(`
189
+ CREATE TABLE IF NOT EXISTS crypto_metadata (
190
+ key TEXT PRIMARY KEY,
191
+ value TEXT NOT NULL,
192
+ created_at TEXT DEFAULT (datetime('now'))
193
+ )
194
+ `);
195
+ }
196
+
197
+ /**
198
+ * Set database encryption status.
199
+ */
200
+ export function setDbEncryptionStatus(
201
+ db,
202
+ encrypted,
203
+ passwordHash,
204
+ passwordSalt,
205
+ ) {
206
+ ensureCryptoTable(db);
207
+ const stmt = db.prepare(
208
+ "INSERT OR REPLACE INTO crypto_metadata (key, value) VALUES (?, ?)",
209
+ );
210
+ stmt.run("db_encrypted", encrypted ? "true" : "false");
211
+ if (passwordHash) {
212
+ stmt.run("db_password_hash", passwordHash);
213
+ stmt.run("db_password_salt", passwordSalt);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Get database encryption status.
219
+ */
220
+ export function getDbEncryptionStatus(db) {
221
+ ensureCryptoTable(db);
222
+ const row = db
223
+ .prepare("SELECT value FROM crypto_metadata WHERE key = ?")
224
+ .get("db_encrypted");
225
+ return row?.value === "true";
226
+ }
227
+
228
+ /**
229
+ * Get file info for encrypted file.
230
+ */
231
+ export function getEncryptedFileInfo(filePath) {
232
+ if (!fs.existsSync(filePath)) {
233
+ throw new Error(`File not found: ${filePath}`);
234
+ }
235
+
236
+ const stats = fs.statSync(filePath);
237
+ const encrypted = isEncryptedFile(filePath);
238
+
239
+ return {
240
+ path: path.resolve(filePath),
241
+ size: stats.size,
242
+ encrypted,
243
+ extension: path.extname(filePath),
244
+ modified: stats.mtime.toISOString(),
245
+ };
246
+ }