@axplusb/kepler 0.0.1 → 1.0.1
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 +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +52 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +142 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +159 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +52 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +182 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/claude-reader.ts +594 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +101 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- package/index.js +0 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Path Sanitization — validate file paths before allowing access.
|
|
3
|
+
*
|
|
4
|
+
* Prevents directory traversal, blocks sensitive files, and normalizes paths.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
/** File patterns that should never be read or written. */
|
|
10
|
+
const SENSITIVE_PATTERNS = [
|
|
11
|
+
/\.env$/,
|
|
12
|
+
/\.env\..+$/,
|
|
13
|
+
/credentials\.json$/,
|
|
14
|
+
/credentials\.yaml$/,
|
|
15
|
+
/\.pem$/,
|
|
16
|
+
/\.key$/,
|
|
17
|
+
/id_rsa$/,
|
|
18
|
+
/id_ed25519$/,
|
|
19
|
+
/\.ssh\/config$/,
|
|
20
|
+
/\.netrc$/,
|
|
21
|
+
/\.pgpass$/,
|
|
22
|
+
/\.aws\/credentials$/,
|
|
23
|
+
/\.docker\/config\.json$/,
|
|
24
|
+
/secrets\.yaml$/,
|
|
25
|
+
/secrets\.json$/,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** Directories that should never be written to. */
|
|
29
|
+
const PROTECTED_DIRS = [
|
|
30
|
+
'/etc',
|
|
31
|
+
'/usr',
|
|
32
|
+
'/sbin',
|
|
33
|
+
'/boot',
|
|
34
|
+
'/sys',
|
|
35
|
+
'/proc',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate a file path for safety.
|
|
40
|
+
* @param {string} filePath - the path to validate
|
|
41
|
+
* @param {object} [options]
|
|
42
|
+
* @param {string} [options.cwd] - current working directory (default: process.cwd())
|
|
43
|
+
* @param {boolean} [options.write] - whether this is a write operation
|
|
44
|
+
* @returns {{ safe: boolean, resolved: string, reason?: string, warning?: string }}
|
|
45
|
+
*/
|
|
46
|
+
export function validatePath(filePath, options = {}) {
|
|
47
|
+
if (typeof filePath !== 'string' || filePath.trim().length === 0) {
|
|
48
|
+
return { safe: false, resolved: '', reason: 'Empty or invalid path' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const resolved = path.resolve(filePath);
|
|
52
|
+
const cwd = options.cwd || process.cwd();
|
|
53
|
+
|
|
54
|
+
// Check for null bytes (path injection)
|
|
55
|
+
if (filePath.includes('\0')) {
|
|
56
|
+
return { safe: false, resolved, reason: 'Null byte in path' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check sensitive file patterns
|
|
60
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
61
|
+
if (pattern.test(resolved) || pattern.test(path.basename(resolved))) {
|
|
62
|
+
return { safe: false, resolved, reason: 'Sensitive file' };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check protected directories for writes
|
|
67
|
+
if (options.write) {
|
|
68
|
+
for (const dir of PROTECTED_DIRS) {
|
|
69
|
+
if (resolved.startsWith(dir + '/') || resolved === dir) {
|
|
70
|
+
return { safe: false, resolved, reason: `Protected directory: ${dir}` };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for traversal outside cwd
|
|
76
|
+
let warning;
|
|
77
|
+
if (!resolved.startsWith(cwd) && !resolved.startsWith('/tmp')) {
|
|
78
|
+
warning = 'Path is outside the current working directory';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { safe: true, resolved, warning };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if a filename matches sensitive patterns.
|
|
86
|
+
* @param {string} filename
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
export function isSensitiveFile(filename) {
|
|
90
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
91
|
+
if (pattern.test(filename)) return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get list of sensitive patterns (for display/testing).
|
|
98
|
+
* @returns {RegExp[]}
|
|
99
|
+
*/
|
|
100
|
+
export function getSensitivePatterns() {
|
|
101
|
+
return [...SENSITIVE_PATTERNS];
|
|
102
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission Prompts — interactive yes/no for dangerous operations.
|
|
3
|
+
*
|
|
4
|
+
* Used in "default" permission mode to ask the user before executing
|
|
5
|
+
* potentially dangerous tool calls (Bash, Edit, Write, Agent).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prompt the user for permission to execute a tool.
|
|
10
|
+
* @param {string} toolName - tool being invoked
|
|
11
|
+
* @param {object} input - tool input
|
|
12
|
+
* @param {object} rl - readline interface with .question()
|
|
13
|
+
* @returns {Promise<boolean>} true if allowed
|
|
14
|
+
*/
|
|
15
|
+
export async function promptPermission(toolName, input, rl) {
|
|
16
|
+
if (!rl || typeof rl.question !== 'function') {
|
|
17
|
+
// No readline available — deny by default
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const summary = formatToolSummary(toolName, input);
|
|
22
|
+
return new Promise(resolve => {
|
|
23
|
+
rl.question(`Allow ${summary}? [y/N] `, answer => {
|
|
24
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Format a human-readable summary of a tool call.
|
|
31
|
+
* @param {string} toolName
|
|
32
|
+
* @param {object} input
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function formatToolSummary(toolName, input) {
|
|
36
|
+
switch (toolName) {
|
|
37
|
+
case 'Bash':
|
|
38
|
+
return `Bash: ${truncate(input.command || '', 60)}`;
|
|
39
|
+
case 'Edit':
|
|
40
|
+
return `Edit: ${input.file_path || 'unknown file'}`;
|
|
41
|
+
case 'Write':
|
|
42
|
+
return `Write: ${input.file_path || 'unknown file'} (${(input.content || '').length} chars)`;
|
|
43
|
+
case 'MultiEdit':
|
|
44
|
+
return `MultiEdit: ${input.file_path || 'unknown file'} (${(input.edits || []).length} edits)`;
|
|
45
|
+
case 'Agent':
|
|
46
|
+
return `Agent: ${truncate(input.prompt || '', 40)}`;
|
|
47
|
+
case 'WebFetch':
|
|
48
|
+
return `WebFetch: ${truncate(input.url || '', 50)}`;
|
|
49
|
+
case 'RemoteTrigger':
|
|
50
|
+
return `RemoteTrigger: ${input.url || 'unknown'}`;
|
|
51
|
+
default:
|
|
52
|
+
return `${toolName}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a tool requires interactive permission in default mode.
|
|
58
|
+
* Read-only tools are always allowed.
|
|
59
|
+
* @param {string} toolName
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
export function requiresPermission(toolName) {
|
|
63
|
+
const SAFE_TOOLS = new Set([
|
|
64
|
+
'Read', 'Glob', 'Grep', 'LS', 'ToolSearch',
|
|
65
|
+
'AskUser', 'CronList', 'TodoWrite',
|
|
66
|
+
]);
|
|
67
|
+
return !SAFE_TOOLS.has(toolName);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function truncate(str, maxLen) {
|
|
71
|
+
if (str.length <= maxLen) return str;
|
|
72
|
+
return str.substring(0, maxLen - 3) + '...';
|
|
73
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox — wrap commands in platform-specific sandboxes.
|
|
3
|
+
*
|
|
4
|
+
* Linux: bubblewrap (bwrap)
|
|
5
|
+
* macOS: sandbox-exec (seatbelt)
|
|
6
|
+
* Windows/other: passthrough (no sandbox)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export class Sandbox {
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} [platform] - override process.platform
|
|
12
|
+
*/
|
|
13
|
+
constructor(platform) {
|
|
14
|
+
this.platform = platform || process.platform;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wrap a command to run inside a sandbox.
|
|
19
|
+
* @param {string} command - the command to sandbox
|
|
20
|
+
* @param {object} [options]
|
|
21
|
+
* @param {string[]} [options.allowWrite] - directories to allow writes
|
|
22
|
+
* @param {string[]} [options.allowNet] - allow network access (macOS)
|
|
23
|
+
* @param {boolean} [options.allowDevices] - allow device access
|
|
24
|
+
* @returns {string} sandboxed command
|
|
25
|
+
*/
|
|
26
|
+
wrapCommand(command, options = {}) {
|
|
27
|
+
if (this.platform === 'linux') return this.bubblewrap(command, options);
|
|
28
|
+
if (this.platform === 'darwin') return this.seatbelt(command, options);
|
|
29
|
+
return command; // fallback: no sandbox
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Linux sandbox using bubblewrap.
|
|
34
|
+
* Creates a minimal read-only root with /dev, /proc, /tmp.
|
|
35
|
+
*/
|
|
36
|
+
bubblewrap(command, opts = {}) {
|
|
37
|
+
const args = [
|
|
38
|
+
'--ro-bind', '/', '/',
|
|
39
|
+
'--dev', '/dev',
|
|
40
|
+
'--proc', '/proc',
|
|
41
|
+
'--tmpfs', '/tmp',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Allow specific writable directories
|
|
45
|
+
if (opts.allowWrite) {
|
|
46
|
+
for (const dir of opts.allowWrite) {
|
|
47
|
+
if (typeof dir === 'string' && dir.length > 0) {
|
|
48
|
+
args.push('--bind', dir, dir);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Allow /dev access if requested
|
|
54
|
+
if (opts.allowDevices) {
|
|
55
|
+
args.push('--dev-bind', '/dev', '/dev');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return `bwrap ${args.join(' ')} -- ${command}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* macOS sandbox using sandbox-exec with a seatbelt profile.
|
|
63
|
+
* Returns a sandbox-exec wrapped command with a generated profile.
|
|
64
|
+
*/
|
|
65
|
+
seatbelt(command, opts = {}) {
|
|
66
|
+
const rules = [
|
|
67
|
+
'(version 1)',
|
|
68
|
+
'(deny default)',
|
|
69
|
+
'(allow process-exec)',
|
|
70
|
+
'(allow process-fork)',
|
|
71
|
+
'(allow file-read*)',
|
|
72
|
+
'(allow sysctl-read)',
|
|
73
|
+
'(allow mach-lookup)',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// Allow writes to specific directories
|
|
77
|
+
if (opts.allowWrite) {
|
|
78
|
+
for (const dir of opts.allowWrite) {
|
|
79
|
+
if (typeof dir === 'string' && dir.length > 0) {
|
|
80
|
+
rules.push(`(allow file-write* (subpath "${dir}"))`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Allow /tmp writes by default
|
|
86
|
+
rules.push('(allow file-write* (subpath "/tmp"))');
|
|
87
|
+
|
|
88
|
+
// Allow network if requested
|
|
89
|
+
if (opts.allowNet) {
|
|
90
|
+
rules.push('(allow network*)');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const profile = rules.join('\n');
|
|
94
|
+
// Escape single quotes in profile for shell
|
|
95
|
+
const escaped = profile.replace(/'/g, "'\\''");
|
|
96
|
+
return `sandbox-exec -p '${escaped}' ${command}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if sandbox tooling is available on this platform.
|
|
101
|
+
* @returns {{ available: boolean, tool: string }}
|
|
102
|
+
*/
|
|
103
|
+
check() {
|
|
104
|
+
if (this.platform === 'linux') {
|
|
105
|
+
return { available: true, tool: 'bwrap' };
|
|
106
|
+
}
|
|
107
|
+
if (this.platform === 'darwin') {
|
|
108
|
+
return { available: true, tool: 'sandbox-exec' };
|
|
109
|
+
}
|
|
110
|
+
return { available: false, tool: 'none' };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Loader — load plugins from directory, git, or npm.
|
|
3
|
+
*
|
|
4
|
+
* Plugins can provide: tools, agents, skills, hooks.
|
|
5
|
+
* Plugin format: a directory with a plugin.json manifest.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
|
|
13
|
+
export class PluginLoader {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} [pluginDir] - directory to scan for plugins
|
|
16
|
+
*/
|
|
17
|
+
constructor(pluginDir) {
|
|
18
|
+
this.pluginDir = pluginDir ||
|
|
19
|
+
path.join(os.homedir(), '.claude', 'plugins');
|
|
20
|
+
this.plugins = new Map();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load plugins from the plugin directory.
|
|
25
|
+
* @returns {Array<object>} loaded plugin manifests
|
|
26
|
+
*/
|
|
27
|
+
async loadFromDirectory(dir) {
|
|
28
|
+
const targetDir = dir || this.pluginDir;
|
|
29
|
+
const loaded = [];
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(targetDir)) return loaded;
|
|
33
|
+
|
|
34
|
+
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.isDirectory()) continue;
|
|
37
|
+
|
|
38
|
+
const manifestPath = path.join(targetDir, entry.name, 'plugin.json');
|
|
39
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
43
|
+
manifest._dir = path.join(targetDir, entry.name);
|
|
44
|
+
manifest._name = entry.name;
|
|
45
|
+
this.plugins.set(manifest.name || entry.name, manifest);
|
|
46
|
+
loaded.push(manifest);
|
|
47
|
+
} catch {
|
|
48
|
+
// Skip malformed plugins
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Directory not readable
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return loaded;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Clone a plugin from a git repo and load it.
|
|
60
|
+
* @param {string} repoUrl - git repository URL
|
|
61
|
+
* @param {string} [name] - plugin name (default: repo name)
|
|
62
|
+
* @returns {object|null} loaded manifest
|
|
63
|
+
*/
|
|
64
|
+
async loadFromGit(repoUrl, name) {
|
|
65
|
+
const pluginName = name || repoUrl.split('/').pop()?.replace('.git', '') || 'plugin';
|
|
66
|
+
const targetDir = path.join(this.pluginDir, pluginName);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
fs.mkdirSync(this.pluginDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
if (fs.existsSync(targetDir)) {
|
|
72
|
+
// Update existing
|
|
73
|
+
execSync('git pull', { cwd: targetDir, stdio: 'pipe' });
|
|
74
|
+
} else {
|
|
75
|
+
// Clone new
|
|
76
|
+
execSync(`git clone --depth 1 ${repoUrl} ${targetDir}`, { stdio: 'pipe' });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const manifestPath = path.join(targetDir, 'plugin.json');
|
|
80
|
+
if (fs.existsSync(manifestPath)) {
|
|
81
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
82
|
+
manifest._dir = targetDir;
|
|
83
|
+
manifest._name = pluginName;
|
|
84
|
+
this.plugins.set(manifest.name || pluginName, manifest);
|
|
85
|
+
return manifest;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Git operation failed
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get all installed plugins.
|
|
96
|
+
* @returns {Array<object>}
|
|
97
|
+
*/
|
|
98
|
+
getInstalledPlugins() {
|
|
99
|
+
return [...this.plugins.values()];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get a plugin by name.
|
|
104
|
+
* @param {string} name
|
|
105
|
+
* @returns {object|undefined}
|
|
106
|
+
*/
|
|
107
|
+
getPlugin(name) {
|
|
108
|
+
return this.plugins.get(name);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove a plugin by name.
|
|
113
|
+
* @param {string} name
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
removePlugin(name) {
|
|
117
|
+
const plugin = this.plugins.get(name);
|
|
118
|
+
if (!plugin) return false;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
if (plugin._dir && fs.existsSync(plugin._dir)) {
|
|
122
|
+
fs.rmSync(plugin._dir, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Best effort
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return this.plugins.delete(name);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get plugin count.
|
|
133
|
+
* @returns {number}
|
|
134
|
+
*/
|
|
135
|
+
count() {
|
|
136
|
+
return this.plugins.size;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Loader — loads skills from .claude/skills/{name}/SKILL.md
|
|
3
|
+
*
|
|
4
|
+
* Skills are invoked via /skill-name in REPL or the Skill tool.
|
|
5
|
+
* Each skill has a SKILL.md that defines:
|
|
6
|
+
* - name, description, trigger conditions
|
|
7
|
+
* - The prompt to inject when the skill is invoked
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
export class SkillsLoader {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.skills = new Map();
|
|
16
|
+
this.searchPaths = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load skills from standard directories.
|
|
21
|
+
* @param {string} [cwd] - project working directory
|
|
22
|
+
*/
|
|
23
|
+
load(cwd = process.cwd()) {
|
|
24
|
+
this.searchPaths = [
|
|
25
|
+
path.join(cwd, '.claude', 'skills'),
|
|
26
|
+
path.join(process.env.HOME || '', '.claude', 'skills'),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const dir of this.searchPaths) {
|
|
30
|
+
this._loadFromDir(dir);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_loadFromDir(dir) {
|
|
37
|
+
try {
|
|
38
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (!entry.isDirectory()) continue;
|
|
41
|
+
|
|
42
|
+
const skillFile = path.join(dir, entry.name, 'SKILL.md');
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
45
|
+
const skill = parseSkill(content, entry.name);
|
|
46
|
+
if (skill) {
|
|
47
|
+
skill.source = skillFile;
|
|
48
|
+
this.skills.set(skill.name, skill);
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Skill directory without SKILL.md, skip
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Directory does not exist
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get a skill by name.
|
|
61
|
+
* @param {string} name
|
|
62
|
+
* @returns {object|null}
|
|
63
|
+
*/
|
|
64
|
+
get(name) {
|
|
65
|
+
// Try exact match, then prefix match
|
|
66
|
+
if (this.skills.has(name)) return this.skills.get(name);
|
|
67
|
+
for (const [key, skill] of this.skills) {
|
|
68
|
+
if (key.startsWith(name) || skill.aliases?.includes(name)) {
|
|
69
|
+
return skill;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* List all loaded skills.
|
|
77
|
+
* @returns {Array<object>}
|
|
78
|
+
*/
|
|
79
|
+
list() {
|
|
80
|
+
return [...this.skills.values()];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run a skill, returning its prompt for injection into the conversation.
|
|
85
|
+
* @param {string} name - skill name
|
|
86
|
+
* @param {string} [args] - optional arguments
|
|
87
|
+
* @returns {string} skill prompt
|
|
88
|
+
*/
|
|
89
|
+
async run(name, args) {
|
|
90
|
+
const skill = this.get(name);
|
|
91
|
+
if (!skill) {
|
|
92
|
+
throw new Error(`Unknown skill: ${name}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let prompt = skill.prompt;
|
|
96
|
+
if (args) {
|
|
97
|
+
prompt = prompt.replace('$ARGUMENTS', args);
|
|
98
|
+
prompt += `\n\nArguments: ${args}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return `[Skill: ${skill.name}]\n${prompt}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse a SKILL.md file into a skill definition.
|
|
107
|
+
*/
|
|
108
|
+
function parseSkill(content, dirName) {
|
|
109
|
+
const lines = content.split('\n');
|
|
110
|
+
const skill = {
|
|
111
|
+
name: dirName,
|
|
112
|
+
description: '',
|
|
113
|
+
aliases: [],
|
|
114
|
+
trigger: null,
|
|
115
|
+
prompt: content,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Parse YAML frontmatter if present
|
|
119
|
+
if (lines[0]?.trim() === '---') {
|
|
120
|
+
const endIdx = lines.indexOf('---', 1);
|
|
121
|
+
if (endIdx > 0) {
|
|
122
|
+
const frontmatter = lines.slice(1, endIdx).join('\n');
|
|
123
|
+
for (const line of frontmatter.split('\n')) {
|
|
124
|
+
const colonIdx = line.indexOf(':');
|
|
125
|
+
if (colonIdx === -1) continue;
|
|
126
|
+
const key = line.slice(0, colonIdx).trim();
|
|
127
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
128
|
+
|
|
129
|
+
if (key === 'name') skill.name = value;
|
|
130
|
+
else if (key === 'description') skill.description = value;
|
|
131
|
+
else if (key === 'trigger') skill.trigger = value;
|
|
132
|
+
else if (key === 'aliases') {
|
|
133
|
+
skill.aliases = value.replace(/[\[\]]/g, '').split(',').map(s => s.trim());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
skill.prompt = lines.slice(endIdx + 1).join('\n').trim();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Extract description from first paragraph if not in frontmatter
|
|
141
|
+
if (!skill.description && skill.prompt) {
|
|
142
|
+
const firstLine = skill.prompt.split('\n').find(l => l.trim() && !l.startsWith('#'));
|
|
143
|
+
if (firstLine) skill.description = firstLine.trim().slice(0, 100);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return skill;
|
|
147
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Runner — executes a skill by injecting its prompt.
|
|
3
|
+
*
|
|
4
|
+
* When a skill is invoked, its prompt is injected as a system message
|
|
5
|
+
* into the conversation context, guiding the agent's behavior.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class SkillRunner {
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} loader - SkillsLoader instance
|
|
11
|
+
* @param {object} agentLoop - agent loop instance
|
|
12
|
+
*/
|
|
13
|
+
constructor(loader, agentLoop) {
|
|
14
|
+
this.loader = loader;
|
|
15
|
+
this.loop = agentLoop;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute a skill.
|
|
20
|
+
* @param {string} name - skill name
|
|
21
|
+
* @param {string} [args] - optional arguments
|
|
22
|
+
* @returns {AsyncGenerator} event stream from agent loop
|
|
23
|
+
*/
|
|
24
|
+
async *execute(name, args) {
|
|
25
|
+
const skill = this.loader.get(name);
|
|
26
|
+
if (!skill) {
|
|
27
|
+
yield { type: 'error', message: `Unknown skill: ${name}` };
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Build the skill prompt
|
|
32
|
+
let prompt = skill.prompt;
|
|
33
|
+
if (args) {
|
|
34
|
+
prompt = prompt.replace(/\$ARGUMENTS/g, args);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Inject skill context as a user message
|
|
38
|
+
const message = `[Invoking skill: ${skill.name}]\n\n${prompt}${args ? `\n\nArguments: ${args}` : ''}`;
|
|
39
|
+
|
|
40
|
+
// Run through agent loop
|
|
41
|
+
yield* this.loop.run(message);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* List available skills for display.
|
|
46
|
+
* @returns {Array<{name: string, description: string}>}
|
|
47
|
+
*/
|
|
48
|
+
listAvailable() {
|
|
49
|
+
return this.loader.list().map(s => ({
|
|
50
|
+
name: s.name,
|
|
51
|
+
description: s.description,
|
|
52
|
+
aliases: s.aliases || [],
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
}
|