chainlesschain 0.37.9 → 0.37.11
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/README.md +309 -19
- package/bin/chainlesschain.js +4 -0
- package/package.json +1 -1
- package/src/commands/a2a.js +374 -0
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/bi.js +240 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/cowork.js +317 -0
- package/src/commands/did.js +376 -0
- package/src/commands/economy.js +375 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/evolution.js +398 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/hmemory.js +273 -0
- package/src/commands/hook.js +260 -0
- package/src/commands/import.js +259 -0
- package/src/commands/init.js +184 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +155 -4
- package/src/commands/lowcode.js +320 -0
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +187 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +451 -0
- package/src/commands/sandbox.js +366 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/skill.js +254 -201
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/commands/workflow.js +359 -0
- package/src/commands/zkp.js +277 -0
- package/src/index.js +93 -1
- package/src/lib/a2a-protocol.js +371 -0
- package/src/lib/agent-coordinator.js +273 -0
- package/src/lib/agent-economy.js +369 -0
- package/src/lib/app-builder.js +377 -0
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bi-engine.js +299 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -0
- package/src/lib/cowork/ab-comparator-cli.js +180 -0
- package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
- package/src/lib/cowork/debate-review-cli.js +144 -0
- package/src/lib/cowork/decision-kb-cli.js +153 -0
- package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
- package/src/lib/cowork-adapter.js +106 -0
- package/src/lib/crypto-manager.js +246 -0
- package/src/lib/did-manager.js +270 -0
- package/src/lib/ensure-utf8.js +59 -0
- package/src/lib/evolution-system.js +508 -0
- package/src/lib/git-integration.js +220 -0
- package/src/lib/hierarchical-memory.js +471 -0
- package/src/lib/hook-manager.js +387 -0
- package/src/lib/instinct-manager.js +190 -0
- package/src/lib/knowledge-exporter.js +302 -0
- package/src/lib/knowledge-importer.js +293 -0
- package/src/lib/llm-providers.js +325 -0
- package/src/lib/mcp-client.js +413 -0
- package/src/lib/memory-manager.js +211 -0
- package/src/lib/note-versioning.js +244 -0
- package/src/lib/org-manager.js +424 -0
- package/src/lib/p2p-manager.js +317 -0
- package/src/lib/pdf-parser.js +96 -0
- package/src/lib/permission-engine.js +374 -0
- package/src/lib/plan-mode.js +333 -0
- package/src/lib/plugin-manager.js +430 -0
- package/src/lib/project-detector.js +53 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/sandbox-v2.js +503 -0
- package/src/lib/service-container.js +183 -0
- package/src/lib/session-manager.js +189 -0
- package/src/lib/skill-loader.js +274 -0
- package/src/lib/sync-manager.js +347 -0
- package/src/lib/token-tracker.js +200 -0
- package/src/lib/wallet-manager.js +348 -0
- package/src/lib/workflow-engine.js +503 -0
- package/src/lib/zkp-engine.js +241 -0
- package/src/repl/agent-repl.js +259 -124
|
@@ -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
|
+
}
|