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,270 @@
1
+ /**
2
+ * DID Manager — Decentralized Identity management for CLI.
3
+ * Ed25519 key generation, DID document creation, signing, and verification.
4
+ * Uses Node.js built-in crypto module (no external dependencies).
5
+ */
6
+
7
+ import crypto from "crypto";
8
+
9
+ /**
10
+ * Ensure DID tables exist.
11
+ */
12
+ export function ensureDIDTables(db) {
13
+ db.exec(`
14
+ CREATE TABLE IF NOT EXISTS did_identities (
15
+ did TEXT PRIMARY KEY,
16
+ display_name TEXT,
17
+ public_key TEXT NOT NULL,
18
+ secret_key TEXT NOT NULL,
19
+ did_document TEXT,
20
+ is_default INTEGER DEFAULT 0,
21
+ created_at TEXT DEFAULT (datetime('now')),
22
+ updated_at TEXT DEFAULT (datetime('now'))
23
+ )
24
+ `);
25
+ }
26
+
27
+ /**
28
+ * Generate an Ed25519 keypair.
29
+ * Returns { publicKey, secretKey } as hex strings.
30
+ */
31
+ export function generateKeyPair() {
32
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519", {
33
+ publicKeyEncoding: { type: "spki", format: "der" },
34
+ privateKeyEncoding: { type: "pkcs8", format: "der" },
35
+ });
36
+ return {
37
+ publicKey: publicKey.toString("hex"),
38
+ secretKey: privateKey.toString("hex"),
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Generate a DID from a public key.
44
+ * Format: did:chainless:<base64url-of-sha256-of-pubkey>
45
+ */
46
+ export function generateDID(publicKeyHex) {
47
+ const hash = crypto
48
+ .createHash("sha256")
49
+ .update(Buffer.from(publicKeyHex, "hex"))
50
+ .digest();
51
+ const id = hash.toString("base64url").slice(0, 32);
52
+ return `did:chainless:${id}`;
53
+ }
54
+
55
+ /**
56
+ * Create a DID Document (W3C DID Core spec subset).
57
+ */
58
+ export function createDIDDocument(did, publicKeyHex, displayName) {
59
+ return {
60
+ "@context": ["https://www.w3.org/ns/did/v1"],
61
+ id: did,
62
+ controller: did,
63
+ verificationMethod: [
64
+ {
65
+ id: `${did}#key-1`,
66
+ type: "Ed25519VerificationKey2020",
67
+ controller: did,
68
+ publicKeyHex: publicKeyHex,
69
+ },
70
+ ],
71
+ authentication: [`${did}#key-1`],
72
+ assertionMethod: [`${did}#key-1`],
73
+ service: displayName
74
+ ? [
75
+ {
76
+ id: `${did}#profile`,
77
+ type: "ProfileService",
78
+ serviceEndpoint: { name: displayName },
79
+ },
80
+ ]
81
+ : [],
82
+ created: new Date().toISOString(),
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Create a new DID identity and store in DB.
88
+ */
89
+ export function createIdentity(db, displayName) {
90
+ ensureDIDTables(db);
91
+
92
+ const keys = generateKeyPair();
93
+ const did = generateDID(keys.publicKey);
94
+ const doc = createDIDDocument(did, keys.publicKey, displayName);
95
+
96
+ // If no identities exist, this becomes the default
97
+ const count = db.prepare("SELECT COUNT(*) as c FROM did_identities").get().c;
98
+ const isDefault = count === 0 ? 1 : 0;
99
+
100
+ db.prepare(
101
+ `INSERT INTO did_identities (did, display_name, public_key, secret_key, did_document, is_default)
102
+ VALUES (?, ?, ?, ?, ?, ?)`,
103
+ ).run(
104
+ did,
105
+ displayName || null,
106
+ keys.publicKey,
107
+ keys.secretKey,
108
+ JSON.stringify(doc),
109
+ isDefault,
110
+ );
111
+
112
+ return {
113
+ did,
114
+ displayName,
115
+ publicKey: keys.publicKey,
116
+ document: doc,
117
+ isDefault: isDefault === 1,
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Get identity by DID or prefix.
123
+ */
124
+ export function getIdentity(db, didOrPrefix) {
125
+ ensureDIDTables(db);
126
+ return db
127
+ .prepare("SELECT * FROM did_identities WHERE did LIKE ?")
128
+ .get(`${didOrPrefix}%`);
129
+ }
130
+
131
+ /**
132
+ * Get all identities.
133
+ */
134
+ export function getAllIdentities(db) {
135
+ ensureDIDTables(db);
136
+ return db
137
+ .prepare(
138
+ "SELECT * FROM did_identities ORDER BY is_default DESC, created_at DESC",
139
+ )
140
+ .all();
141
+ }
142
+
143
+ /**
144
+ * Get the default identity.
145
+ */
146
+ export function getDefaultIdentity(db) {
147
+ ensureDIDTables(db);
148
+ return db.prepare("SELECT * FROM did_identities WHERE is_default = 1").get();
149
+ }
150
+
151
+ /**
152
+ * Set an identity as the default.
153
+ */
154
+ export function setDefaultIdentity(db, did) {
155
+ ensureDIDTables(db);
156
+ const identity = getIdentity(db, did);
157
+ if (!identity) return false;
158
+
159
+ db.prepare("UPDATE did_identities SET is_default = ? WHERE did LIKE ?").run(
160
+ 0,
161
+ "%",
162
+ );
163
+ db.prepare("UPDATE did_identities SET is_default = ? WHERE did = ?").run(
164
+ 1,
165
+ identity.did,
166
+ );
167
+ return true;
168
+ }
169
+
170
+ /**
171
+ * Delete an identity by DID.
172
+ */
173
+ export function deleteIdentity(db, did) {
174
+ ensureDIDTables(db);
175
+ const identity = getIdentity(db, did);
176
+ if (!identity) return false;
177
+
178
+ const result = db
179
+ .prepare("DELETE FROM did_identities WHERE did = ?")
180
+ .run(identity.did);
181
+ if (result.changes > 0 && identity.is_default) {
182
+ // Promote next identity to default
183
+ const next = db
184
+ .prepare("SELECT did FROM did_identities ORDER BY created_at ASC LIMIT 1")
185
+ .get();
186
+ if (next) {
187
+ db.prepare("UPDATE did_identities SET is_default = 1 WHERE did = ?").run(
188
+ next.did,
189
+ );
190
+ }
191
+ }
192
+ return result.changes > 0;
193
+ }
194
+
195
+ /**
196
+ * Sign a message using an identity's secret key.
197
+ * Returns the signature as hex string.
198
+ */
199
+ export function signMessage(db, did, message) {
200
+ ensureDIDTables(db);
201
+ const identity = getIdentity(db, did);
202
+ if (!identity) throw new Error(`Identity not found: ${did}`);
203
+
204
+ const privateKey = crypto.createPrivateKey({
205
+ key: Buffer.from(identity.secret_key, "hex"),
206
+ format: "der",
207
+ type: "pkcs8",
208
+ });
209
+
210
+ const signature = crypto.sign(null, Buffer.from(message, "utf8"), privateKey);
211
+ return signature.toString("hex");
212
+ }
213
+
214
+ /**
215
+ * Verify a signature against a message and public key.
216
+ */
217
+ export function verifySignature(publicKeyHex, message, signatureHex) {
218
+ try {
219
+ const publicKey = crypto.createPublicKey({
220
+ key: Buffer.from(publicKeyHex, "hex"),
221
+ format: "der",
222
+ type: "spki",
223
+ });
224
+
225
+ return crypto.verify(
226
+ null,
227
+ Buffer.from(message, "utf8"),
228
+ publicKey,
229
+ Buffer.from(signatureHex, "hex"),
230
+ );
231
+ } catch (_err) {
232
+ // Invalid key or signature format
233
+ return false;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Verify a signature using a DID from the database.
239
+ */
240
+ export function verifyWithDID(db, did, message, signatureHex) {
241
+ const identity = getIdentity(db, did);
242
+ if (!identity) throw new Error(`Identity not found: ${did}`);
243
+ return verifySignature(identity.public_key, message, signatureHex);
244
+ }
245
+
246
+ /**
247
+ * Export identity (public data only, no secret key).
248
+ */
249
+ export function exportIdentity(db, did) {
250
+ const identity = getIdentity(db, did);
251
+ if (!identity) return null;
252
+
253
+ return {
254
+ did: identity.did,
255
+ displayName: identity.display_name,
256
+ publicKey: identity.public_key,
257
+ document: JSON.parse(identity.did_document || "{}"),
258
+ createdAt: identity.created_at,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Resolve a DID — returns the DID document.
264
+ * Currently local-only resolution.
265
+ */
266
+ export function resolveDID(db, did) {
267
+ const identity = getIdentity(db, did);
268
+ if (!identity) return null;
269
+ return JSON.parse(identity.did_document || "{}");
270
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Ensure UTF-8 encoding on Windows to prevent Chinese character garbling (乱码).
3
+ *
4
+ * Windows console defaults to the system codepage (e.g. CP936/GBK for Chinese),
5
+ * which causes UTF-8 output from Node.js to display as garbled text.
6
+ * This module sets the console codepage to 65001 (UTF-8) and configures
7
+ * Node.js streams to use UTF-8 encoding.
8
+ */
9
+
10
+ import { execSync } from "child_process";
11
+
12
+ /**
13
+ * Call this as early as possible in the process entry point.
14
+ */
15
+ export function ensureUtf8() {
16
+ if (process.platform !== "win32") return;
17
+
18
+ // Set Windows console codepage to UTF-8
19
+ try {
20
+ execSync("chcp 65001", { stdio: "ignore" });
21
+ } catch (_err) {
22
+ // Ignore - may fail in non-interactive environments
23
+ }
24
+
25
+ // Ensure stdout/stderr use UTF-8 encoding
26
+ if (process.stdout.setDefaultEncoding) {
27
+ process.stdout.setDefaultEncoding("utf8");
28
+ }
29
+ if (process.stderr.setDefaultEncoding) {
30
+ process.stderr.setDefaultEncoding("utf8");
31
+ }
32
+
33
+ // Set environment variable so child processes inherit UTF-8
34
+ process.env.PYTHONIOENCODING = "utf-8";
35
+ process.env.LANG = "en_US.UTF-8";
36
+ }
37
+
38
+ /**
39
+ * Get spawn options that ensure UTF-8 output from child processes on Windows.
40
+ * Use this when spawning cmd.exe or other system processes.
41
+ *
42
+ * @param {object} [opts] - Additional spawn options to merge
43
+ * @returns {object} Spawn options with encoding set to utf-8
44
+ */
45
+ export function getUtf8SpawnOptions(opts = {}) {
46
+ if (process.platform !== "win32") return { encoding: "utf-8", ...opts };
47
+
48
+ return {
49
+ encoding: "utf-8",
50
+ env: {
51
+ ...process.env,
52
+ PYTHONIOENCODING: "utf-8",
53
+ // Force cmd.exe to use UTF-8 codepage
54
+ CHCP: "65001",
55
+ ...opts.env,
56
+ },
57
+ ...opts,
58
+ };
59
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Git integration — wraps system git commands for knowledge base versioning.
3
+ */
4
+
5
+ import { execSync } from "child_process";
6
+ import { existsSync } from "fs";
7
+ import { join } from "path";
8
+
9
+ /**
10
+ * Check if a directory is a git repository.
11
+ */
12
+ export function isGitRepo(dir) {
13
+ return existsSync(join(dir, ".git"));
14
+ }
15
+
16
+ /**
17
+ * Run a git command and return stdout.
18
+ */
19
+ export function gitExec(args, cwd) {
20
+ try {
21
+ return execSync(`git ${args}`, {
22
+ cwd,
23
+ encoding: "utf-8",
24
+ stdio: ["pipe", "pipe", "pipe"],
25
+ }).trim();
26
+ } catch (err) {
27
+ const stderr = err.stderr ? err.stderr.toString().trim() : "";
28
+ throw new Error(stderr || err.message);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Initialize a git repo in the given directory.
34
+ */
35
+ export function gitInit(dir) {
36
+ if (isGitRepo(dir)) {
37
+ return { initialized: false, message: "Already a git repository" };
38
+ }
39
+ gitExec("init", dir);
40
+ return { initialized: true, message: "Initialized git repository" };
41
+ }
42
+
43
+ /**
44
+ * Get git status as structured data.
45
+ */
46
+ export function gitStatus(dir) {
47
+ if (!isGitRepo(dir)) {
48
+ return { isRepo: false, files: [] };
49
+ }
50
+
51
+ // Use execSync directly to preserve leading whitespace in porcelain format
52
+ const output = execSync("git status --porcelain", {
53
+ cwd: dir,
54
+ encoding: "utf-8",
55
+ stdio: ["pipe", "pipe", "pipe"],
56
+ });
57
+ const files = output
58
+ .split("\n")
59
+ .filter(Boolean)
60
+ .map((line) => ({
61
+ status: line.substring(0, 2).trim(),
62
+ file: line.substring(3),
63
+ }));
64
+
65
+ const branch = getCurrentBranch(dir);
66
+
67
+ return {
68
+ isRepo: true,
69
+ branch,
70
+ files,
71
+ clean: files.length === 0,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Get current branch name.
77
+ */
78
+ export function getCurrentBranch(dir) {
79
+ try {
80
+ return gitExec("rev-parse --abbrev-ref HEAD", dir);
81
+ } catch {
82
+ return "unknown";
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Auto-commit all changes with a generated message.
88
+ */
89
+ export function gitAutoCommit(dir, message) {
90
+ if (!isGitRepo(dir)) {
91
+ throw new Error("Not a git repository");
92
+ }
93
+
94
+ const status = gitStatus(dir);
95
+ if (status.clean) {
96
+ return { committed: false, message: "No changes to commit" };
97
+ }
98
+
99
+ // Stage all changes
100
+ gitExec("add -A", dir);
101
+
102
+ // Generate commit message if not provided
103
+ const commitMsg =
104
+ message ||
105
+ `auto: ${status.files.length} file(s) changed — ${new Date().toISOString().slice(0, 16)}`;
106
+
107
+ gitExec(`commit -m "${commitMsg.replace(/"/g, '\\"')}"`, dir);
108
+
109
+ const hash = gitExec("rev-parse --short HEAD", dir);
110
+
111
+ return {
112
+ committed: true,
113
+ hash,
114
+ message: commitMsg,
115
+ filesChanged: status.files.length,
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Get recent commit log.
121
+ */
122
+ export function gitLog(dir, limit = 10) {
123
+ if (!isGitRepo(dir)) {
124
+ return [];
125
+ }
126
+
127
+ try {
128
+ const output = gitExec(
129
+ `log --oneline --no-decorate -${limit} --format="%H|%h|%s|%ai|%an"`,
130
+ dir,
131
+ );
132
+ return output
133
+ .split("\n")
134
+ .filter(Boolean)
135
+ .map((line) => {
136
+ const [hash, shortHash, subject, date, author] = line.split("|");
137
+ return { hash, shortHash, subject, date, author };
138
+ });
139
+ } catch {
140
+ return [];
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Analyze repository history statistics.
146
+ */
147
+ export function gitHistoryAnalyze(dir) {
148
+ if (!isGitRepo(dir)) {
149
+ throw new Error("Not a git repository");
150
+ }
151
+
152
+ const totalCommits = parseInt(gitExec("rev-list --count HEAD", dir)) || 0;
153
+ const firstCommit =
154
+ totalCommits > 0
155
+ ? gitExec("log --reverse --format=%ai --max-count=1", dir)
156
+ : null;
157
+ const lastCommit =
158
+ totalCommits > 0 ? gitExec("log --format=%ai --max-count=1", dir) : null;
159
+
160
+ // Contributors
161
+ let contributors = [];
162
+ try {
163
+ const authorOutput = gitExec("shortlog -sn --no-merges HEAD", dir);
164
+ contributors = authorOutput
165
+ .split("\n")
166
+ .filter(Boolean)
167
+ .map((line) => {
168
+ const match = line.trim().match(/(\d+)\s+(.+)/);
169
+ return match ? { commits: parseInt(match[1]), author: match[2] } : null;
170
+ })
171
+ .filter(Boolean);
172
+ } catch {
173
+ // Empty repo or no commits
174
+ }
175
+
176
+ // File statistics
177
+ let fileCount = 0;
178
+ try {
179
+ const files = gitExec("ls-files", dir);
180
+ fileCount = files.split("\n").filter(Boolean).length;
181
+ } catch {
182
+ // Ignore
183
+ }
184
+
185
+ return {
186
+ totalCommits,
187
+ firstCommit,
188
+ lastCommit,
189
+ contributors,
190
+ trackedFiles: fileCount,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Install git hooks for auto-commit on note changes.
196
+ * Creates a post-commit hook that logs the commit.
197
+ */
198
+ export function installHooks(dir) {
199
+ if (!isGitRepo(dir)) {
200
+ throw new Error("Not a git repository");
201
+ }
202
+
203
+ const hooksDir = join(dir, ".git", "hooks");
204
+ const hookPath = join(hooksDir, "pre-commit");
205
+
206
+ const hookContent = `#!/bin/sh
207
+ # ChainlessChain auto-format hook
208
+ echo "ChainlessChain: pre-commit hook running"
209
+ `;
210
+
211
+ const { writeFileSync, chmodSync } = require("fs");
212
+ writeFileSync(hookPath, hookContent, "utf-8");
213
+ try {
214
+ chmodSync(hookPath, 0o755);
215
+ } catch {
216
+ // Windows may not support chmod
217
+ }
218
+
219
+ return { installed: true, hook: "pre-commit", path: hookPath };
220
+ }