@bahulam/code 2.6.0
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 +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -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 +50 -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/benchmarks/page.tsx +224 -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 +143 -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 +164 -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 +53 -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 +183 -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/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -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 +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -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 +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -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 +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -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/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -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/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -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 +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -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/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -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/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Injection Check — detect dangerous shell patterns.
|
|
3
|
+
*
|
|
4
|
+
* Scans commands for common injection vectors before allowing
|
|
5
|
+
* Bash tool execution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DANGEROUS_PATTERNS = [
|
|
9
|
+
{ pattern: /;\s*rm\s+-rf\s+\//, label: 'rm -rf /' },
|
|
10
|
+
{ pattern: /\|\s*sh\b/, label: 'pipe to sh' },
|
|
11
|
+
{ pattern: /\|\s*bash\b/, label: 'pipe to bash' },
|
|
12
|
+
{ pattern: /`[^`]+`/, label: 'backtick execution' },
|
|
13
|
+
{ pattern: /\$\([^)]+\)/, label: 'command substitution' },
|
|
14
|
+
{ pattern: />\s*\/etc\//, label: 'write to /etc' },
|
|
15
|
+
{ pattern: />\s*\/usr\//, label: 'write to /usr' },
|
|
16
|
+
{ pattern: /curl\s.*\|\s*(bash|sh)/, label: 'curl pipe to shell' },
|
|
17
|
+
{ pattern: /wget\s.*\|\s*(bash|sh)/, label: 'wget pipe to shell' },
|
|
18
|
+
{ pattern: /mkfs\./, label: 'filesystem format' },
|
|
19
|
+
{ pattern: /dd\s+if=.*of=\/dev\//, label: 'dd to device' },
|
|
20
|
+
{ pattern: /:\(\)\s*\{.*\|.*&\s*\}/, label: 'fork bomb' },
|
|
21
|
+
{ pattern: /chmod\s+777\s+\//, label: 'chmod 777 root' },
|
|
22
|
+
{ pattern: />\s*\/dev\/sda/, label: 'write to disk device' },
|
|
23
|
+
{ pattern: /eval\s+"?\$/, label: 'eval variable' },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check a command string for injection patterns.
|
|
28
|
+
* @param {string} command - shell command to check
|
|
29
|
+
* @returns {{ safe: boolean, pattern?: string, label?: string }}
|
|
30
|
+
*/
|
|
31
|
+
export function checkInjection(command) {
|
|
32
|
+
if (typeof command !== 'string') {
|
|
33
|
+
return { safe: false, label: 'non-string command' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const { pattern, label } of DANGEROUS_PATTERNS) {
|
|
37
|
+
if (pattern.test(command)) {
|
|
38
|
+
return { safe: false, pattern: pattern.source, label };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { safe: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the list of dangerous patterns (for display/testing).
|
|
47
|
+
* @returns {Array<{ pattern: RegExp, label: string }>}
|
|
48
|
+
*/
|
|
49
|
+
export function getDangerousPatterns() {
|
|
50
|
+
return DANGEROUS_PATTERNS.map(({ pattern, label }) => ({ pattern, label }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a command uses any elevated privilege patterns.
|
|
55
|
+
* @param {string} command
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
export function usesElevation(command) {
|
|
59
|
+
return /\bsudo\b/.test(command) || /\bsu\s+-?\s/.test(command) || /\bdoas\b/.test(command);
|
|
60
|
+
}
|
|
@@ -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,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable skill installation and lock metadata.
|
|
3
|
+
*
|
|
4
|
+
* Bundles are copied as data. No scripts or hooks from a skill are executed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import { discoverSkillDirectories, parseSkill } from './loader.mjs';
|
|
13
|
+
|
|
14
|
+
function isGitSource(source) {
|
|
15
|
+
return /^(https?:\/\/|ssh:\/\/|git@|github:)/.test(source) || source.endsWith('.git');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeGitSource(source) {
|
|
19
|
+
if (source.startsWith('github:')) {
|
|
20
|
+
return `https://github.com/${source.slice('github:'.length).replace(/\.git$/, '')}.git`;
|
|
21
|
+
}
|
|
22
|
+
return source;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function discoverInstallableSkills(root, maxDepth = 4) {
|
|
26
|
+
const direct = discoverSkillDirectories(root);
|
|
27
|
+
if (direct.length) return direct;
|
|
28
|
+
|
|
29
|
+
const found = [];
|
|
30
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
31
|
+
const ignored = new Set(['.git', 'node_modules', '.venv', 'venv', 'dist', 'build']);
|
|
32
|
+
while (queue.length) {
|
|
33
|
+
const { dir, depth } = queue.shift();
|
|
34
|
+
if (depth >= maxDepth) continue;
|
|
35
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
36
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || ignored.has(entry.name)) continue;
|
|
37
|
+
const child = path.join(dir, entry.name);
|
|
38
|
+
if (fs.existsSync(path.join(child, 'SKILL.md'))) {
|
|
39
|
+
found.push(child);
|
|
40
|
+
} else {
|
|
41
|
+
queue.push({ dir: child, depth: depth + 1 });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return found.sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rejectSymlinks(root) {
|
|
49
|
+
const queue = [root];
|
|
50
|
+
while (queue.length) {
|
|
51
|
+
const current = queue.shift();
|
|
52
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
53
|
+
const fullPath = path.join(current, entry.name);
|
|
54
|
+
if (entry.isSymbolicLink()) throw new Error(`Symlinked skill resource is not allowed: ${fullPath}`);
|
|
55
|
+
if (entry.isDirectory()) queue.push(fullPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function copyDirectory(source, destination) {
|
|
61
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
62
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
63
|
+
const from = path.join(source, entry.name);
|
|
64
|
+
const to = path.join(destination, entry.name);
|
|
65
|
+
if (entry.isDirectory()) copyDirectory(from, to);
|
|
66
|
+
else if (entry.isFile()) fs.copyFileSync(from, to);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readJson(file, fallback) {
|
|
71
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf-8')); } catch { return fallback; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writeJson(file, value) {
|
|
75
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
76
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class SkillInstaller {
|
|
80
|
+
constructor({ cwd = process.cwd(), homeDir = os.homedir() } = {}) {
|
|
81
|
+
this.cwd = path.resolve(cwd);
|
|
82
|
+
this.homeDir = homeDir;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
paths(scope = 'global') {
|
|
86
|
+
const base = scope === 'project'
|
|
87
|
+
? path.join(this.cwd, '.bahulam')
|
|
88
|
+
: path.join(this.homeDir, '.bahulam');
|
|
89
|
+
return {
|
|
90
|
+
skillsDir: path.join(base, 'skills'),
|
|
91
|
+
lockFile: path.join(base, 'skills.lock.json'),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
install(source, { scope = 'global', force = false, onlyNames = null } = {}) {
|
|
96
|
+
if (!source) throw new Error('A local directory or Git repository is required');
|
|
97
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'bahulam-code-skill-'));
|
|
98
|
+
let resolvedSource;
|
|
99
|
+
let commit = null;
|
|
100
|
+
try {
|
|
101
|
+
if (isGitSource(source)) {
|
|
102
|
+
resolvedSource = path.join(tempRoot, 'repository');
|
|
103
|
+
execFileSync(
|
|
104
|
+
'git',
|
|
105
|
+
['clone', '--depth', '1', normalizeGitSource(source), resolvedSource],
|
|
106
|
+
{ stdio: 'pipe' },
|
|
107
|
+
);
|
|
108
|
+
commit = execFileSync('git', ['-C', resolvedSource, 'rev-parse', 'HEAD'], { encoding: 'utf-8' }).trim();
|
|
109
|
+
} else {
|
|
110
|
+
resolvedSource = fs.realpathSync(path.resolve(this.cwd, source));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const skillDirs = discoverInstallableSkills(resolvedSource);
|
|
114
|
+
if (!skillDirs.length) throw new Error(`No SKILL.md bundles found in ${source}`);
|
|
115
|
+
|
|
116
|
+
const { skillsDir, lockFile } = this.paths(scope);
|
|
117
|
+
const lock = readJson(lockFile, { version: 1, skills: {} });
|
|
118
|
+
const installed = [];
|
|
119
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
120
|
+
|
|
121
|
+
const plans = [];
|
|
122
|
+
for (const skillDir of skillDirs) {
|
|
123
|
+
rejectSymlinks(skillDir);
|
|
124
|
+
const content = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf-8');
|
|
125
|
+
const name = parseSkill(content, path.basename(skillDir)).name;
|
|
126
|
+
if (onlyNames && !onlyNames.includes(name)) continue;
|
|
127
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) throw new Error(`Unsafe skill name: ${name}`);
|
|
128
|
+
const destination = path.join(skillsDir, name);
|
|
129
|
+
if (fs.existsSync(destination)) {
|
|
130
|
+
if (!force) throw new Error(`Skill already installed: ${name} (use --force to replace)`);
|
|
131
|
+
}
|
|
132
|
+
plans.push({ skillDir, content, name, destination });
|
|
133
|
+
}
|
|
134
|
+
if (!plans.length) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
onlyNames
|
|
137
|
+
? `Requested skill not found in source: ${onlyNames.join(', ')}`
|
|
138
|
+
: `No installable skills found in ${source}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const { skillDir, content, name, destination } of plans) {
|
|
143
|
+
if (fs.existsSync(destination)) fs.rmSync(destination, { recursive: true, force: true });
|
|
144
|
+
copyDirectory(skillDir, destination);
|
|
145
|
+
|
|
146
|
+
const digest = crypto.createHash('sha256')
|
|
147
|
+
.update(content)
|
|
148
|
+
.digest('hex');
|
|
149
|
+
lock.skills[name] = {
|
|
150
|
+
source,
|
|
151
|
+
scope,
|
|
152
|
+
commit,
|
|
153
|
+
destination,
|
|
154
|
+
installed_at: new Date().toISOString(),
|
|
155
|
+
manifest_hash: `sha256:${digest}`,
|
|
156
|
+
};
|
|
157
|
+
installed.push(name);
|
|
158
|
+
}
|
|
159
|
+
writeJson(lockFile, lock);
|
|
160
|
+
return { installed, scope, lock_file: lockFile };
|
|
161
|
+
} finally {
|
|
162
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
remove(name, { scope = 'global' } = {}) {
|
|
167
|
+
const { skillsDir, lockFile } = this.paths(scope);
|
|
168
|
+
const destination = path.join(skillsDir, name);
|
|
169
|
+
if (!fs.existsSync(destination)) throw new Error(`Skill is not installed: ${name}`);
|
|
170
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
171
|
+
const lock = readJson(lockFile, { version: 1, skills: {} });
|
|
172
|
+
delete lock.skills[name];
|
|
173
|
+
writeJson(lockFile, lock);
|
|
174
|
+
return { removed: name, scope };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
update(name, { scope = 'global' } = {}) {
|
|
178
|
+
const { lockFile } = this.paths(scope);
|
|
179
|
+
const lock = readJson(lockFile, { version: 1, skills: {} });
|
|
180
|
+
const entry = lock.skills[name];
|
|
181
|
+
if (!entry?.source) throw new Error(`No locked source for skill: ${name}`);
|
|
182
|
+
return this.install(entry.source, { scope, force: true, onlyNames: [name] });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
lock(scope = 'global') {
|
|
186
|
+
return readJson(this.paths(scope).lockFile, { version: 1, skills: {} });
|
|
187
|
+
}
|
|
188
|
+
}
|