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,374 @@
1
+ /**
2
+ * Permission Engine — RBAC (Role-Based Access Control) for CLI.
3
+ * Manages roles, permissions, grants, and checks.
4
+ */
5
+
6
+ import crypto from "crypto";
7
+
8
+ function generateId() {
9
+ return crypto.randomUUID();
10
+ }
11
+
12
+ /**
13
+ * Built-in roles.
14
+ */
15
+ export const BUILT_IN_ROLES = {
16
+ admin: {
17
+ name: "admin",
18
+ description: "Full system access",
19
+ permissions: ["*"],
20
+ },
21
+ editor: {
22
+ name: "editor",
23
+ description: "Read and write access to content",
24
+ permissions: [
25
+ "note:read",
26
+ "note:write",
27
+ "note:delete",
28
+ "search:read",
29
+ "memory:read",
30
+ "memory:write",
31
+ "session:read",
32
+ "session:write",
33
+ "export:read",
34
+ "import:write",
35
+ ],
36
+ },
37
+ viewer: {
38
+ name: "viewer",
39
+ description: "Read-only access",
40
+ permissions: [
41
+ "note:read",
42
+ "search:read",
43
+ "memory:read",
44
+ "session:read",
45
+ "export:read",
46
+ ],
47
+ },
48
+ agent: {
49
+ name: "agent",
50
+ description: "AI agent access",
51
+ permissions: [
52
+ "note:read",
53
+ "note:write",
54
+ "search:read",
55
+ "memory:read",
56
+ "memory:write",
57
+ "llm:read",
58
+ "llm:write",
59
+ "skill:execute",
60
+ ],
61
+ },
62
+ };
63
+
64
+ /**
65
+ * All known permission scopes.
66
+ */
67
+ export const PERMISSION_SCOPES = [
68
+ "note:read",
69
+ "note:write",
70
+ "note:delete",
71
+ "search:read",
72
+ "memory:read",
73
+ "memory:write",
74
+ "session:read",
75
+ "session:write",
76
+ "export:read",
77
+ "import:write",
78
+ "llm:read",
79
+ "llm:write",
80
+ "skill:execute",
81
+ "did:read",
82
+ "did:write",
83
+ "did:delete",
84
+ "encrypt:read",
85
+ "encrypt:write",
86
+ "auth:read",
87
+ "auth:write",
88
+ "auth:admin",
89
+ "audit:read",
90
+ "audit:write",
91
+ "config:read",
92
+ "config:write",
93
+ "system:admin",
94
+ ];
95
+
96
+ /**
97
+ * Ensure permission tables exist.
98
+ */
99
+ export function ensurePermissionTables(db) {
100
+ db.exec(`
101
+ CREATE TABLE IF NOT EXISTS rbac_roles (
102
+ name TEXT PRIMARY KEY,
103
+ description TEXT,
104
+ permissions TEXT NOT NULL,
105
+ is_builtin INTEGER DEFAULT 0,
106
+ created_at TEXT DEFAULT (datetime('now'))
107
+ )
108
+ `);
109
+
110
+ db.exec(`
111
+ CREATE TABLE IF NOT EXISTS rbac_grants (
112
+ id TEXT PRIMARY KEY,
113
+ user_did TEXT NOT NULL,
114
+ role_name TEXT NOT NULL,
115
+ granted_by TEXT,
116
+ expires_at TEXT,
117
+ created_at TEXT DEFAULT (datetime('now')),
118
+ UNIQUE(user_did, role_name)
119
+ )
120
+ `);
121
+
122
+ db.exec(`
123
+ CREATE TABLE IF NOT EXISTS rbac_direct_permissions (
124
+ id TEXT PRIMARY KEY,
125
+ user_did TEXT NOT NULL,
126
+ permission TEXT NOT NULL,
127
+ granted_by TEXT,
128
+ expires_at TEXT,
129
+ created_at TEXT DEFAULT (datetime('now')),
130
+ UNIQUE(user_did, permission)
131
+ )
132
+ `);
133
+
134
+ // Seed built-in roles
135
+ const existing = db
136
+ .prepare("SELECT COUNT(*) as c FROM rbac_roles WHERE is_builtin = 1")
137
+ .get();
138
+ if (existing.c === 0) {
139
+ const stmt = db.prepare(
140
+ "INSERT OR IGNORE INTO rbac_roles (name, description, permissions, is_builtin) VALUES (?, ?, ?, ?)",
141
+ );
142
+ for (const role of Object.values(BUILT_IN_ROLES)) {
143
+ stmt.run(
144
+ role.name,
145
+ role.description,
146
+ JSON.stringify(role.permissions),
147
+ 1,
148
+ );
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Get all roles.
155
+ */
156
+ export function getRoles(db) {
157
+ ensurePermissionTables(db);
158
+ const rows = db
159
+ .prepare("SELECT * FROM rbac_roles ORDER BY is_builtin DESC, name ASC")
160
+ .all();
161
+ return rows.map((r) => ({
162
+ ...r,
163
+ permissions: JSON.parse(r.permissions || "[]"),
164
+ isBuiltin: r.is_builtin === 1,
165
+ }));
166
+ }
167
+
168
+ /**
169
+ * Create a custom role.
170
+ */
171
+ export function createRole(db, name, description, permissions) {
172
+ ensurePermissionTables(db);
173
+
174
+ if (BUILT_IN_ROLES[name]) {
175
+ throw new Error(`Cannot override built-in role: ${name}`);
176
+ }
177
+
178
+ const existing = db
179
+ .prepare("SELECT name FROM rbac_roles WHERE name = ?")
180
+ .get(name);
181
+ if (existing) {
182
+ throw new Error(`Role already exists: ${name}`);
183
+ }
184
+
185
+ db.prepare(
186
+ "INSERT INTO rbac_roles (name, description, permissions, is_builtin) VALUES (?, ?, ?, ?)",
187
+ ).run(name, description || "", JSON.stringify(permissions || []), 0);
188
+
189
+ return {
190
+ name,
191
+ description,
192
+ permissions: permissions || [],
193
+ isBuiltin: false,
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Delete a custom role.
199
+ */
200
+ export function deleteRole(db, name) {
201
+ ensurePermissionTables(db);
202
+
203
+ if (BUILT_IN_ROLES[name]) {
204
+ throw new Error(`Cannot delete built-in role: ${name}`);
205
+ }
206
+
207
+ const result = db
208
+ .prepare("DELETE FROM rbac_roles WHERE name = ? AND is_builtin = 0")
209
+ .run(name);
210
+ if (result.changes > 0) {
211
+ // Remove grants referencing this role
212
+ db.prepare("DELETE FROM rbac_grants WHERE role_name = ?").run(name);
213
+ }
214
+ return result.changes > 0;
215
+ }
216
+
217
+ /**
218
+ * Grant a role to a user (by DID).
219
+ */
220
+ export function grantRole(db, userDid, roleName, grantedBy, expiresAt) {
221
+ ensurePermissionTables(db);
222
+
223
+ // Verify role exists
224
+ const role = db
225
+ .prepare("SELECT name FROM rbac_roles WHERE name = ?")
226
+ .get(roleName);
227
+ if (!role) {
228
+ throw new Error(`Role not found: ${roleName}`);
229
+ }
230
+
231
+ const id = generateId();
232
+ db.prepare(
233
+ "INSERT OR REPLACE INTO rbac_grants (id, user_did, role_name, granted_by, expires_at) VALUES (?, ?, ?, ?, ?)",
234
+ ).run(id, userDid, roleName, grantedBy || null, expiresAt || null);
235
+
236
+ return { id, userDid, roleName, grantedBy, expiresAt };
237
+ }
238
+
239
+ /**
240
+ * Revoke a role from a user.
241
+ */
242
+ export function revokeRole(db, userDid, roleName) {
243
+ ensurePermissionTables(db);
244
+ const result = db
245
+ .prepare("DELETE FROM rbac_grants WHERE user_did = ? AND role_name = ?")
246
+ .run(userDid, roleName);
247
+ return result.changes > 0;
248
+ }
249
+
250
+ /**
251
+ * Grant a direct permission to a user.
252
+ */
253
+ export function grantPermission(db, userDid, permission, grantedBy) {
254
+ ensurePermissionTables(db);
255
+ const id = generateId();
256
+ db.prepare(
257
+ "INSERT OR REPLACE INTO rbac_direct_permissions (id, user_did, permission, granted_by) VALUES (?, ?, ?, ?)",
258
+ ).run(id, userDid, permission, grantedBy || null);
259
+ return { id, userDid, permission };
260
+ }
261
+
262
+ /**
263
+ * Revoke a direct permission from a user.
264
+ */
265
+ export function revokePermission(db, userDid, permission) {
266
+ ensurePermissionTables(db);
267
+ const result = db
268
+ .prepare(
269
+ "DELETE FROM rbac_direct_permissions WHERE user_did = ? AND permission = ?",
270
+ )
271
+ .run(userDid, permission);
272
+ return result.changes > 0;
273
+ }
274
+
275
+ /**
276
+ * Get all roles and direct permissions for a user.
277
+ */
278
+ export function getUserPermissions(db, userDid) {
279
+ ensurePermissionTables(db);
280
+
281
+ // Get active role grants
282
+ const allGrants = db
283
+ .prepare("SELECT * FROM rbac_grants WHERE user_did = ?")
284
+ .all(userDid);
285
+
286
+ // Filter out expired grants
287
+ const now = new Date().toISOString();
288
+ const grants = allGrants.filter((g) => !g.expires_at || g.expires_at > now);
289
+
290
+ // Get direct permissions (not expired)
291
+ const allDirectPerms = db
292
+ .prepare("SELECT * FROM rbac_direct_permissions WHERE user_did = ?")
293
+ .all(userDid);
294
+ const directPerms = allDirectPerms.filter(
295
+ (d) => !d.expires_at || d.expires_at > now,
296
+ );
297
+
298
+ // Collect all permissions by looking up each role
299
+ const allPerms = new Set();
300
+ const roles = [];
301
+
302
+ for (const grant of grants) {
303
+ roles.push(grant.role_name);
304
+ const role = db
305
+ .prepare("SELECT permissions FROM rbac_roles WHERE name = ?")
306
+ .get(grant.role_name);
307
+ if (role) {
308
+ const perms = JSON.parse(role.permissions || "[]");
309
+ for (const p of perms) allPerms.add(p);
310
+ }
311
+ }
312
+
313
+ for (const dp of directPerms) {
314
+ allPerms.add(dp.permission);
315
+ }
316
+
317
+ return {
318
+ userDid,
319
+ roles,
320
+ directPermissions: directPerms.map((d) => d.permission),
321
+ effectivePermissions: [...allPerms],
322
+ isAdmin: allPerms.has("*"),
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Check if a user has a specific permission.
328
+ */
329
+ export function checkPermission(db, userDid, permission) {
330
+ const userPerms = getUserPermissions(db, userDid);
331
+
332
+ // Admin wildcard
333
+ if (userPerms.isAdmin) return true;
334
+
335
+ // Exact match
336
+ if (userPerms.effectivePermissions.includes(permission)) return true;
337
+
338
+ // Wildcard match (e.g., "note:*" matches "note:read")
339
+ const [scope] = permission.split(":");
340
+ if (userPerms.effectivePermissions.includes(`${scope}:*`)) return true;
341
+
342
+ return false;
343
+ }
344
+
345
+ /**
346
+ * Get all grants for a specific role.
347
+ */
348
+ export function getRoleGrants(db, roleName) {
349
+ ensurePermissionTables(db);
350
+ return db
351
+ .prepare("SELECT * FROM rbac_grants WHERE role_name = ?")
352
+ .all(roleName);
353
+ }
354
+
355
+ /**
356
+ * List all users with their roles.
357
+ */
358
+ export function listUserRoles(db) {
359
+ ensurePermissionTables(db);
360
+ const grants = db
361
+ .prepare("SELECT * FROM rbac_grants ORDER BY user_did")
362
+ .all();
363
+
364
+ // Group by user_did in JS to avoid GROUP_CONCAT
365
+ const userMap = new Map();
366
+ for (const g of grants) {
367
+ if (!userMap.has(g.user_did)) {
368
+ userMap.set(g.user_did, []);
369
+ }
370
+ userMap.get(g.user_did).push(g.role_name);
371
+ }
372
+
373
+ return [...userMap.entries()].map(([userDid, roles]) => ({ userDid, roles }));
374
+ }
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Plan Mode for CLI Agent REPL
3
+ *
4
+ * During plan mode, the AI can only use read-only tools (read_file, search_files, list_dir, list_skills).
5
+ * Write/execute tools (write_file, edit_file, run_shell, run_skill) are blocked until the plan is approved.
6
+ *
7
+ * Lightweight port of desktop-app-vue/src/main/ai-engine/plan-mode/index.js
8
+ */
9
+
10
+ import { EventEmitter } from "events";
11
+
12
+ /**
13
+ * Plan item status
14
+ */
15
+ export const PlanStatus = {
16
+ PENDING: "pending",
17
+ APPROVED: "approved",
18
+ REJECTED: "rejected",
19
+ EXECUTING: "executing",
20
+ COMPLETED: "completed",
21
+ FAILED: "failed",
22
+ };
23
+
24
+ /**
25
+ * Plan mode states
26
+ */
27
+ export const PlanState = {
28
+ INACTIVE: "inactive",
29
+ ANALYZING: "analyzing",
30
+ PLAN_READY: "plan_ready",
31
+ APPROVED: "approved",
32
+ EXECUTING: "executing",
33
+ COMPLETED: "completed",
34
+ REJECTED: "rejected",
35
+ };
36
+
37
+ /**
38
+ * Tool categories for permission control
39
+ */
40
+ const READ_TOOLS = new Set([
41
+ "read_file",
42
+ "search_files",
43
+ "list_dir",
44
+ "list_skills",
45
+ ]);
46
+
47
+ const WRITE_TOOLS = new Set([
48
+ "write_file",
49
+ "edit_file",
50
+ "run_shell",
51
+ "run_skill",
52
+ ]);
53
+
54
+ /**
55
+ * A single item in an execution plan
56
+ */
57
+ export class PlanItem {
58
+ constructor(data = {}) {
59
+ this.id =
60
+ data.id || `item-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
61
+ this.order = data.order || 0;
62
+ this.title = data.title || "";
63
+ this.description = data.description || "";
64
+ this.tool = data.tool || null;
65
+ this.params = data.params || {};
66
+ this.dependencies = data.dependencies || [];
67
+ this.estimatedImpact = data.estimatedImpact || "low"; // low, medium, high
68
+ this.status = data.status || PlanStatus.PENDING;
69
+ this.result = null;
70
+ this.error = null;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * An execution plan containing multiple items
76
+ */
77
+ export class ExecutionPlan {
78
+ constructor(data = {}) {
79
+ this.id = data.id || `plan-${Date.now()}`;
80
+ this.title = data.title || "Untitled Plan";
81
+ this.description = data.description || "";
82
+ this.goal = data.goal || "";
83
+ this.items = (data.items || []).map((i) => new PlanItem(i));
84
+ this.status = data.status || PlanState.ANALYZING;
85
+ this.createdAt = new Date().toISOString();
86
+ }
87
+
88
+ addItem(item) {
89
+ const planItem = item instanceof PlanItem ? item : new PlanItem(item);
90
+ planItem.order = this.items.length;
91
+ this.items.push(planItem);
92
+ return planItem;
93
+ }
94
+
95
+ removeItem(itemId) {
96
+ this.items = this.items.filter((i) => i.id !== itemId);
97
+ this.items.forEach((item, idx) => {
98
+ item.order = idx;
99
+ });
100
+ }
101
+
102
+ getItem(itemId) {
103
+ return this.items.find((i) => i.id === itemId);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Plan Mode Manager
109
+ *
110
+ * Controls the plan mode lifecycle in the agent REPL.
111
+ */
112
+ export class PlanModeManager extends EventEmitter {
113
+ constructor() {
114
+ super();
115
+ this.state = PlanState.INACTIVE;
116
+ this.currentPlan = null;
117
+ this.history = [];
118
+ this.blockedToolLog = [];
119
+ }
120
+
121
+ /**
122
+ * Check if plan mode is active
123
+ */
124
+ isActive() {
125
+ return this.state !== PlanState.INACTIVE;
126
+ }
127
+
128
+ /**
129
+ * Enter plan mode
130
+ */
131
+ enterPlanMode(options = {}) {
132
+ if (this.isActive()) {
133
+ return { error: "Already in plan mode" };
134
+ }
135
+
136
+ this.currentPlan = new ExecutionPlan({
137
+ title: options.title || "New Plan",
138
+ goal: options.goal || "",
139
+ });
140
+ this.state = PlanState.ANALYZING;
141
+ this.blockedToolLog = [];
142
+
143
+ this.emit("enter", { plan: this.currentPlan, state: this.state });
144
+ return { plan: this.currentPlan };
145
+ }
146
+
147
+ /**
148
+ * Exit plan mode
149
+ */
150
+ exitPlanMode(options = {}) {
151
+ if (!this.isActive()) {
152
+ return { error: "Not in plan mode" };
153
+ }
154
+
155
+ if (options.savePlan && this.currentPlan) {
156
+ this.history.push(this.currentPlan);
157
+ }
158
+
159
+ const plan = this.currentPlan;
160
+ this.state = PlanState.INACTIVE;
161
+ this.currentPlan = null;
162
+ this.blockedToolLog = [];
163
+
164
+ this.emit("exit", { plan, reason: options.reason || "manual" });
165
+ return { plan };
166
+ }
167
+
168
+ /**
169
+ * Add a plan item
170
+ */
171
+ addPlanItem(itemData) {
172
+ if (!this.currentPlan) {
173
+ return { error: "No active plan" };
174
+ }
175
+
176
+ const item = this.currentPlan.addItem(itemData);
177
+ this.emit("item-added", { planId: this.currentPlan.id, item });
178
+ return { item };
179
+ }
180
+
181
+ /**
182
+ * Mark the plan as ready for approval
183
+ */
184
+ markPlanReady() {
185
+ if (this.state !== PlanState.ANALYZING) {
186
+ return { error: "Plan is not in analyzing state" };
187
+ }
188
+
189
+ this.state = PlanState.PLAN_READY;
190
+ this.currentPlan.status = PlanState.PLAN_READY;
191
+ this.emit("plan-ready", { plan: this.currentPlan });
192
+ return { plan: this.currentPlan };
193
+ }
194
+
195
+ /**
196
+ * Approve the plan (or specific items)
197
+ */
198
+ approvePlan(options = {}) {
199
+ if (
200
+ this.state !== PlanState.PLAN_READY &&
201
+ this.state !== PlanState.ANALYZING
202
+ ) {
203
+ return { error: "Plan is not ready for approval" };
204
+ }
205
+
206
+ const approvedItems = options.itemIds
207
+ ? this.currentPlan.items.filter((i) => options.itemIds.includes(i.id))
208
+ : this.currentPlan.items;
209
+
210
+ for (const item of approvedItems) {
211
+ item.status = PlanStatus.APPROVED;
212
+ }
213
+
214
+ this.state = PlanState.APPROVED;
215
+ this.currentPlan.status = PlanState.APPROVED;
216
+ this.emit("plan-approved", {
217
+ plan: this.currentPlan,
218
+ approvedCount: approvedItems.length,
219
+ });
220
+ return { plan: this.currentPlan, approvedCount: approvedItems.length };
221
+ }
222
+
223
+ /**
224
+ * Reject the plan
225
+ */
226
+ rejectPlan(reason = "") {
227
+ if (!this.isActive()) {
228
+ return { error: "No active plan" };
229
+ }
230
+
231
+ for (const item of this.currentPlan.items) {
232
+ item.status = PlanStatus.REJECTED;
233
+ }
234
+
235
+ this.state = PlanState.REJECTED;
236
+ return this.exitPlanMode({ savePlan: true, reason: reason || "rejected" });
237
+ }
238
+
239
+ /**
240
+ * Check if a tool is allowed in current state
241
+ */
242
+ isToolAllowed(toolName) {
243
+ if (!this.isActive()) return true;
244
+ if (
245
+ this.state === PlanState.APPROVED ||
246
+ this.state === PlanState.EXECUTING
247
+ ) {
248
+ return true;
249
+ }
250
+
251
+ // In analyzing/plan_ready state, only read tools are allowed
252
+ if (READ_TOOLS.has(toolName)) return true;
253
+
254
+ // Block write tools and log
255
+ if (WRITE_TOOLS.has(toolName)) {
256
+ this.blockedToolLog.push({
257
+ tool: toolName,
258
+ timestamp: new Date().toISOString(),
259
+ });
260
+ this.emit("tool-blocked", { toolName });
261
+ return false;
262
+ }
263
+
264
+ // Unknown tools are blocked by default in plan mode
265
+ return false;
266
+ }
267
+
268
+ /**
269
+ * Generate a text summary of the current plan
270
+ */
271
+ generatePlanSummary() {
272
+ if (!this.currentPlan) return "No active plan.";
273
+
274
+ const plan = this.currentPlan;
275
+ const lines = [
276
+ `## Plan: ${plan.title}`,
277
+ plan.goal ? `**Goal**: ${plan.goal}` : "",
278
+ `**Status**: ${this.state}`,
279
+ `**Items**: ${plan.items.length}`,
280
+ "",
281
+ ];
282
+
283
+ for (const item of plan.items) {
284
+ const icon =
285
+ item.status === PlanStatus.COMPLETED
286
+ ? "✅"
287
+ : item.status === PlanStatus.FAILED
288
+ ? "❌"
289
+ : item.status === PlanStatus.APPROVED
290
+ ? "✓"
291
+ : "○";
292
+ lines.push(
293
+ `${icon} ${item.order + 1}. ${item.title} [${item.estimatedImpact}]`,
294
+ );
295
+ if (item.description) {
296
+ lines.push(` ${item.description}`);
297
+ }
298
+ }
299
+
300
+ if (this.blockedToolLog.length > 0) {
301
+ lines.push("");
302
+ lines.push(
303
+ `**Blocked tools**: ${this.blockedToolLog.map((b) => b.tool).join(", ")}`,
304
+ );
305
+ }
306
+
307
+ return lines.filter(Boolean).join("\n");
308
+ }
309
+
310
+ /**
311
+ * Get plans history
312
+ */
313
+ getHistory() {
314
+ return this.history;
315
+ }
316
+ }
317
+
318
+ // Singleton
319
+ let _instance = null;
320
+
321
+ export function getPlanModeManager() {
322
+ if (!_instance) {
323
+ _instance = new PlanModeManager();
324
+ }
325
+ return _instance;
326
+ }
327
+
328
+ export function destroyPlanModeManager() {
329
+ if (_instance) {
330
+ _instance.removeAllListeners();
331
+ _instance = null;
332
+ }
333
+ }
@@ -22,3 +22,18 @@ export function isMac() {
22
22
  export function isLinux() {
23
23
  return getPlatform() === "linux";
24
24
  }
25
+
26
+ export function getBinaryName(version) {
27
+ const p = getPlatform();
28
+ const a = getArch();
29
+ const ext = getBinaryExtension();
30
+ return `chainlesschain-${version}-${p}-${a}${ext}`;
31
+ }
32
+
33
+ export function getBinaryExtension() {
34
+ const p = getPlatform();
35
+ if (p === "win32") return ".exe";
36
+ if (p === "darwin") return ".dmg";
37
+ if (p === "linux") return ".deb";
38
+ return "";
39
+ }