@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.
Files changed (218) hide show
  1. package/README.md +82 -0
  2. package/package.json +36 -4
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/costs/route.ts +88 -0
  6. package/pulse/app/api/export/route.ts +77 -0
  7. package/pulse/app/api/history/route.ts +11 -0
  8. package/pulse/app/api/import/route.ts +31 -0
  9. package/pulse/app/api/memory/route.ts +52 -0
  10. package/pulse/app/api/plans/route.ts +9 -0
  11. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  12. package/pulse/app/api/projects/route.ts +121 -0
  13. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  14. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  15. package/pulse/app/api/sessions/route.ts +112 -0
  16. package/pulse/app/api/settings/route.ts +14 -0
  17. package/pulse/app/api/stats/route.ts +143 -0
  18. package/pulse/app/api/todos/route.ts +9 -0
  19. package/pulse/app/api/tools/route.ts +160 -0
  20. package/pulse/app/costs/page.tsx +179 -0
  21. package/pulse/app/export/page.tsx +465 -0
  22. package/pulse/app/favicon.ico +0 -0
  23. package/pulse/app/globals.css +263 -0
  24. package/pulse/app/help/page.tsx +142 -0
  25. package/pulse/app/history/page.tsx +157 -0
  26. package/pulse/app/layout.tsx +46 -0
  27. package/pulse/app/memory/page.tsx +365 -0
  28. package/pulse/app/overview-client.tsx +393 -0
  29. package/pulse/app/page.tsx +14 -0
  30. package/pulse/app/plans/page.tsx +308 -0
  31. package/pulse/app/projects/[slug]/page.tsx +390 -0
  32. package/pulse/app/projects/page.tsx +110 -0
  33. package/pulse/app/sessions/[id]/page.tsx +243 -0
  34. package/pulse/app/sessions/page.tsx +39 -0
  35. package/pulse/app/settings/page.tsx +188 -0
  36. package/pulse/app/todos/page.tsx +211 -0
  37. package/pulse/app/tools/page.tsx +249 -0
  38. package/pulse/cli.js +159 -0
  39. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  40. package/pulse/components/activity/streak-card.tsx +36 -0
  41. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  42. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  43. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  44. package/pulse/components/costs/model-token-table.tsx +60 -0
  45. package/pulse/components/global-search.tsx +193 -0
  46. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  47. package/pulse/components/layout/bottom-nav.tsx +52 -0
  48. package/pulse/components/layout/client-layout.tsx +31 -0
  49. package/pulse/components/layout/sidebar-context.tsx +50 -0
  50. package/pulse/components/layout/sidebar.tsx +182 -0
  51. package/pulse/components/layout/top-bar.tsx +121 -0
  52. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  53. package/pulse/components/overview/conversation-table.tsx +148 -0
  54. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  55. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  56. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  57. package/pulse/components/overview/stat-card.tsx +102 -0
  58. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  59. package/pulse/components/projects/project-card.tsx +175 -0
  60. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  61. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  62. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  63. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  64. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  65. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  66. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  67. package/pulse/components/sessions/session-badges.tsx +49 -0
  68. package/pulse/components/sessions/session-table.tsx +299 -0
  69. package/pulse/components/theme-provider.tsx +44 -0
  70. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  71. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  72. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  73. package/pulse/components/tools/version-history-table.tsx +32 -0
  74. package/pulse/components/ui/alert.tsx +66 -0
  75. package/pulse/components/ui/badge.tsx +48 -0
  76. package/pulse/components/ui/breadcrumb.tsx +109 -0
  77. package/pulse/components/ui/button.tsx +64 -0
  78. package/pulse/components/ui/calendar.tsx +220 -0
  79. package/pulse/components/ui/card.tsx +92 -0
  80. package/pulse/components/ui/command.tsx +158 -0
  81. package/pulse/components/ui/dialog.tsx +158 -0
  82. package/pulse/components/ui/input.tsx +21 -0
  83. package/pulse/components/ui/popover.tsx +89 -0
  84. package/pulse/components/ui/progress.tsx +31 -0
  85. package/pulse/components/ui/select.tsx +190 -0
  86. package/pulse/components/ui/separator.tsx +28 -0
  87. package/pulse/components/ui/sheet.tsx +143 -0
  88. package/pulse/components/ui/skeleton.tsx +13 -0
  89. package/pulse/components/ui/table.tsx +116 -0
  90. package/pulse/components/ui/tabs.tsx +91 -0
  91. package/pulse/components/ui/tooltip.tsx +57 -0
  92. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  93. package/pulse/components.json +23 -0
  94. package/pulse/eslint.config.mjs +18 -0
  95. package/pulse/lib/claude-reader.ts +594 -0
  96. package/pulse/lib/decode.ts +129 -0
  97. package/pulse/lib/pricing.ts +102 -0
  98. package/pulse/lib/replay-parser.ts +165 -0
  99. package/pulse/lib/tool-categories.ts +127 -0
  100. package/pulse/lib/utils.ts +6 -0
  101. package/pulse/next-env.d.ts +6 -0
  102. package/pulse/next.config.ts +16 -0
  103. package/pulse/package.json +45 -0
  104. package/pulse/postcss.config.mjs +7 -0
  105. package/pulse/public/activity.png +0 -0
  106. package/pulse/public/cc-lens.png +0 -0
  107. package/pulse/public/command-k.png +0 -0
  108. package/pulse/public/costs.png +0 -0
  109. package/pulse/public/dashboard-dark.png +0 -0
  110. package/pulse/public/dashboard-white.png +0 -0
  111. package/pulse/public/export.png +0 -0
  112. package/pulse/public/file.svg +1 -0
  113. package/pulse/public/globe.svg +1 -0
  114. package/pulse/public/next.svg +1 -0
  115. package/pulse/public/projects.png +0 -0
  116. package/pulse/public/session-chat.png +0 -0
  117. package/pulse/public/todos.png +0 -0
  118. package/pulse/public/tools.png +0 -0
  119. package/pulse/public/vercel.svg +1 -0
  120. package/pulse/public/window.svg +1 -0
  121. package/pulse/tsconfig.json +34 -0
  122. package/pulse/types/claude.ts +294 -0
  123. package/src/agents/loader.mjs +89 -0
  124. package/src/agents/parser.mjs +98 -0
  125. package/src/agents/teams.mjs +123 -0
  126. package/src/auth/oauth.mjs +220 -0
  127. package/src/auth/tarang-auth.mjs +277 -0
  128. package/src/config/cli-args.mjs +173 -0
  129. package/src/config/env.mjs +263 -0
  130. package/src/config/settings.mjs +132 -0
  131. package/src/context/ast-parser.mjs +298 -0
  132. package/src/context/bm25.mjs +85 -0
  133. package/src/context/retriever.mjs +270 -0
  134. package/src/context/skeleton.mjs +134 -0
  135. package/src/core/agent-loop.mjs +480 -0
  136. package/src/core/approval.mjs +273 -0
  137. package/src/core/backend-url.mjs +57 -0
  138. package/src/core/cache.mjs +105 -0
  139. package/src/core/callback-client.mjs +149 -0
  140. package/src/core/checkpoints.mjs +142 -0
  141. package/src/core/context-manager.mjs +198 -0
  142. package/src/core/headless.mjs +168 -0
  143. package/src/core/hooks-manager.mjs +87 -0
  144. package/src/core/jsonl-writer.mjs +351 -0
  145. package/src/core/local-agent.mjs +429 -0
  146. package/src/core/local-store.mjs +325 -0
  147. package/src/core/mode-selector.mjs +51 -0
  148. package/src/core/output-filter.mjs +177 -0
  149. package/src/core/paths.mjs +101 -0
  150. package/src/core/pricing.mjs +314 -0
  151. package/src/core/providers.mjs +219 -0
  152. package/src/core/rate-limiter.mjs +119 -0
  153. package/src/core/safety.mjs +200 -0
  154. package/src/core/scheduler.mjs +173 -0
  155. package/src/core/session-manager.mjs +317 -0
  156. package/src/core/session.mjs +143 -0
  157. package/src/core/settings-sync.mjs +85 -0
  158. package/src/core/stagnation.mjs +57 -0
  159. package/src/core/stream-client.mjs +367 -0
  160. package/src/core/streaming.mjs +182 -0
  161. package/src/core/system-prompt.mjs +135 -0
  162. package/src/core/tool-executor.mjs +725 -0
  163. package/src/hooks/engine.mjs +162 -0
  164. package/src/index.mjs +370 -0
  165. package/src/mcp/client.mjs +253 -0
  166. package/src/mcp/transport-shttp.mjs +130 -0
  167. package/src/mcp/transport-sse.mjs +131 -0
  168. package/src/mcp/transport-ws.mjs +134 -0
  169. package/src/permissions/checker.mjs +57 -0
  170. package/src/permissions/command-classifier.mjs +573 -0
  171. package/src/permissions/injection-check.mjs +60 -0
  172. package/src/permissions/path-check.mjs +102 -0
  173. package/src/permissions/prompt.mjs +73 -0
  174. package/src/permissions/sandbox.mjs +112 -0
  175. package/src/plugins/loader.mjs +138 -0
  176. package/src/skills/loader.mjs +147 -0
  177. package/src/skills/runner.mjs +55 -0
  178. package/src/telemetry/index.mjs +96 -0
  179. package/src/terminal/agents.mjs +177 -0
  180. package/src/terminal/analytics.mjs +292 -0
  181. package/src/terminal/ansi.mjs +421 -0
  182. package/src/terminal/main.mjs +150 -0
  183. package/src/terminal/repl.mjs +1484 -0
  184. package/src/terminal/tool-display.mjs +58 -0
  185. package/src/tools/agent.mjs +137 -0
  186. package/src/tools/ask-user.mjs +61 -0
  187. package/src/tools/bash.mjs +148 -0
  188. package/src/tools/cron-create.mjs +120 -0
  189. package/src/tools/cron-delete.mjs +49 -0
  190. package/src/tools/cron-list.mjs +37 -0
  191. package/src/tools/edit.mjs +82 -0
  192. package/src/tools/enter-worktree.mjs +69 -0
  193. package/src/tools/exit-worktree.mjs +57 -0
  194. package/src/tools/glob.mjs +117 -0
  195. package/src/tools/grep.mjs +129 -0
  196. package/src/tools/lint.mjs +71 -0
  197. package/src/tools/ls.mjs +58 -0
  198. package/src/tools/lsp.mjs +115 -0
  199. package/src/tools/multi-edit.mjs +94 -0
  200. package/src/tools/notebook-edit.mjs +96 -0
  201. package/src/tools/read-mcp-resource.mjs +57 -0
  202. package/src/tools/read.mjs +138 -0
  203. package/src/tools/registry.mjs +132 -0
  204. package/src/tools/remote-trigger.mjs +84 -0
  205. package/src/tools/send-message.mjs +64 -0
  206. package/src/tools/skill.mjs +52 -0
  207. package/src/tools/test-runner.mjs +49 -0
  208. package/src/tools/todo-write.mjs +68 -0
  209. package/src/tools/tool-search.mjs +77 -0
  210. package/src/tools/web-fetch.mjs +65 -0
  211. package/src/tools/web-search.mjs +89 -0
  212. package/src/tools/write.mjs +55 -0
  213. package/src/ui/banner.mjs +237 -0
  214. package/src/ui/commands.mjs +499 -0
  215. package/src/ui/formatter.mjs +379 -0
  216. package/src/ui/markdown.mjs +278 -0
  217. package/src/ui/slash-commands.mjs +258 -0
  218. 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
+ }