@axplusb/kepler 0.0.1 → 1.0.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.
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 +98 -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,52 @@
1
+ /**
2
+ * Skill Tool — invoke a loaded skill by name.
3
+ *
4
+ * Skills are loaded from .claude/skills/{name}/SKILL.md and provide
5
+ * specialized prompts injected into the conversation.
6
+ */
7
+
8
+ export const SkillTool = {
9
+ name: 'Skill',
10
+ description: 'Invoke a skill within the conversation. Skills provide specialized capabilities.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ skill: {
15
+ type: 'string',
16
+ description: 'The skill name to invoke (e.g., "commit", "review-pr")',
17
+ },
18
+ args: {
19
+ type: 'string',
20
+ description: 'Optional arguments for the skill',
21
+ },
22
+ },
23
+ required: ['skill'],
24
+ },
25
+
26
+ // Set by skills loader
27
+ _skillsLoader: null,
28
+
29
+ validateInput(input) {
30
+ return input.skill ? [] : ['skill name is required'];
31
+ },
32
+
33
+ async call(input) {
34
+ if (!this._skillsLoader) {
35
+ return 'Skills system not initialized. No skills available.';
36
+ }
37
+
38
+ try {
39
+ const skill = this._skillsLoader.get(input.skill);
40
+ if (!skill) {
41
+ const available = this._skillsLoader.list();
42
+ const names = available.map(s => s.name).join(', ');
43
+ return `Unknown skill: "${input.skill}". Available: ${names || 'none'}`;
44
+ }
45
+
46
+ const result = await this._skillsLoader.run(input.skill, input.args);
47
+ return result;
48
+ } catch (err) {
49
+ return `Skill error: ${err.message}`;
50
+ }
51
+ },
52
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Test Runner Tool — run test suites in contained environment.
3
+ *
4
+ * Executes in .venv/sandbox when available.
5
+ * Agent should use this instead of shell("pytest tests/").
6
+ *
7
+ * Execution: contained (.venv + OS sandbox if available).
8
+ */
9
+
10
+ import { BashTool } from './bash.mjs';
11
+
12
+ export const TestRunnerTool = {
13
+ name: 'TestRunner',
14
+ description: 'Run tests. Executes in project environment. Use this instead of Bash for pytest, jest, vitest, cargo test, go test.',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ command: {
19
+ type: 'string',
20
+ description: 'Test command (e.g., "pytest tests/test_auth.py -v", "npm test", "cargo test")',
21
+ },
22
+ timeout: {
23
+ type: 'number',
24
+ description: 'Timeout in ms (default: 120000, max: 300000)',
25
+ default: 120000,
26
+ },
27
+ },
28
+ required: ['command'],
29
+ },
30
+ validateInput(input) {
31
+ const errors = [];
32
+ if (!input.command) errors.push('command is required');
33
+ return errors;
34
+ },
35
+ async call(input) {
36
+ const timeout = Math.min(input.timeout || 120000, 300000);
37
+
38
+ // TODO: Wire sandbox.mjs here when sandbox execution is ready
39
+ // For now, execute directly (same as Bash but with metadata)
40
+ const result = await BashTool.call({
41
+ command: input.command,
42
+ timeout,
43
+ description: `Test: ${input.command.slice(0, 60)}`,
44
+ });
45
+
46
+ return result;
47
+ },
48
+ _executionPolicy: 'contained',
49
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * TodoWrite Tool — in-memory task management.
3
+ *
4
+ * Maintains a task list that the agent can use to track work items.
5
+ * State persists for the duration of the session.
6
+ */
7
+
8
+ const todos = [];
9
+ let nextId = 1;
10
+
11
+ export const TodoWriteTool = {
12
+ name: 'TodoWrite',
13
+ description: 'Manage a task list. Supports add, update, complete, and list operations.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ todos: {
18
+ type: 'array',
19
+ items: {
20
+ type: 'object',
21
+ properties: {
22
+ id: { type: 'string', description: 'Task ID (auto-assigned if adding)' },
23
+ content: { type: 'string', description: 'Task description' },
24
+ status: {
25
+ type: 'string',
26
+ enum: ['pending', 'in_progress', 'completed'],
27
+ description: 'Task status',
28
+ },
29
+ priority: {
30
+ type: 'string',
31
+ enum: ['high', 'medium', 'low'],
32
+ description: 'Task priority',
33
+ },
34
+ },
35
+ required: ['content', 'status'],
36
+ },
37
+ description: 'Array of todo items to write (replaces all todos)',
38
+ },
39
+ },
40
+ required: ['todos'],
41
+ },
42
+
43
+ validateInput(input) {
44
+ if (!input.todos || !Array.isArray(input.todos)) return ['todos must be an array'];
45
+ return [];
46
+ },
47
+
48
+ async call(input) {
49
+ // Replace entire todo list (matches Claude Code behavior)
50
+ todos.length = 0;
51
+ nextId = 1;
52
+
53
+ for (const item of input.todos) {
54
+ todos.push({
55
+ id: item.id || String(nextId++),
56
+ content: item.content,
57
+ status: item.status || 'pending',
58
+ priority: item.priority || 'medium',
59
+ });
60
+ }
61
+
62
+ const summary = todos.map(t =>
63
+ `[${t.status === 'completed' ? 'x' : t.status === 'in_progress' ? '~' : ' '}] ${t.id}. ${t.content} (${t.priority})`
64
+ ).join('\n');
65
+
66
+ return `Updated ${todos.length} todos:\n${summary}`;
67
+ },
68
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * ToolSearch Tool — deferred tool loading / search.
3
+ *
4
+ * Allows the agent to discover and load tools that are not
5
+ * in the default set. Supports searching MCP servers and
6
+ * deferred tool definitions.
7
+ */
8
+
9
+ export const ToolSearchTool = {
10
+ name: 'ToolSearch',
11
+ description: 'Search for and load deferred tools by name or keyword.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ query: {
16
+ type: 'string',
17
+ description: 'Tool name or search keywords',
18
+ },
19
+ max_results: {
20
+ type: 'number',
21
+ description: 'Maximum results to return (default: 5)',
22
+ },
23
+ },
24
+ required: ['query'],
25
+ },
26
+
27
+ // Will be set by the registry when MCP tools are available
28
+ _mcpTools: [],
29
+ _registry: null,
30
+
31
+ validateInput(input) {
32
+ return input.query ? [] : ['query is required'];
33
+ },
34
+
35
+ async call(input) {
36
+ const query = input.query.toLowerCase();
37
+ const maxResults = input.max_results || 5;
38
+ const matches = [];
39
+
40
+ // Search MCP tools
41
+ for (const tool of this._mcpTools) {
42
+ const name = (tool.name || '').toLowerCase();
43
+ const desc = (tool.description || '').toLowerCase();
44
+ if (name.includes(query) || desc.includes(query)) {
45
+ matches.push({
46
+ name: tool.name,
47
+ description: tool.description,
48
+ source: 'mcp',
49
+ });
50
+ }
51
+ }
52
+
53
+ // Search registry tools
54
+ if (this._registry) {
55
+ for (const tool of this._registry.list()) {
56
+ const name = (tool.name || '').toLowerCase();
57
+ const desc = (tool.description || '').toLowerCase();
58
+ if (name.includes(query) || desc.includes(query)) {
59
+ matches.push({
60
+ name: tool.name,
61
+ description: tool.description,
62
+ source: 'builtin',
63
+ });
64
+ }
65
+ }
66
+ }
67
+
68
+ if (matches.length === 0) {
69
+ return `No tools found matching "${input.query}"`;
70
+ }
71
+
72
+ const limited = matches.slice(0, maxResults);
73
+ return limited.map(m =>
74
+ `[${m.source}] ${m.name}: ${m.description}`
75
+ ).join('\n');
76
+ },
77
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Web Fetch Tool — fetch URL content using built-in Node.js fetch.
3
+ */
4
+
5
+ export const WebFetchTool = {
6
+ name: 'WebFetch',
7
+ description: 'Fetch content from a URL. Returns the response body as text.',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ url: { type: 'string', description: 'The URL to fetch' },
12
+ headers: {
13
+ type: 'object',
14
+ description: 'Optional HTTP headers',
15
+ },
16
+ max_length: {
17
+ type: 'number',
18
+ description: 'Max response length in characters (default: 50000)',
19
+ },
20
+ },
21
+ required: ['url'],
22
+ },
23
+
24
+ validateInput(input) {
25
+ const errors = [];
26
+ if (!input.url) errors.push('url is required');
27
+ try {
28
+ new URL(input.url);
29
+ } catch {
30
+ errors.push('url must be a valid URL');
31
+ }
32
+ return errors;
33
+ },
34
+
35
+ async call(input) {
36
+ const maxLength = input.max_length || 50000;
37
+
38
+ try {
39
+ const controller = new AbortController();
40
+ const timeout = setTimeout(() => controller.abort(), 30000);
41
+
42
+ const res = await fetch(input.url, {
43
+ headers: input.headers || {},
44
+ signal: controller.signal,
45
+ redirect: 'follow',
46
+ });
47
+
48
+ clearTimeout(timeout);
49
+
50
+ if (!res.ok) {
51
+ return `HTTP ${res.status}: ${res.statusText}`;
52
+ }
53
+
54
+ const contentType = res.headers.get('content-type') || '';
55
+ const text = await res.text();
56
+ const truncated = text.length > maxLength
57
+ ? text.slice(0, maxLength) + `\n...[truncated at ${maxLength} chars]`
58
+ : text;
59
+
60
+ return `Content-Type: ${contentType}\nLength: ${text.length}\n\n${truncated}`;
61
+ } catch (err) {
62
+ return `Fetch error: ${err.message}`;
63
+ }
64
+ },
65
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Web Search Tool — search the web via configurable API backends.
3
+ *
4
+ * Supports:
5
+ * - Brave Search API (BRAVE_API_KEY)
6
+ * - SearXNG (self-hosted, SEARXNG_URL)
7
+ * - Fallback: returns instructions to use web-fetch instead
8
+ */
9
+
10
+ export const WebSearchTool = {
11
+ name: 'WebSearch',
12
+ description: 'Search the web for information. Requires BRAVE_API_KEY or SEARXNG_URL.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ query: { type: 'string', description: 'Search query' },
17
+ limit: { type: 'number', description: 'Max results (default: 5)' },
18
+ },
19
+ required: ['query'],
20
+ },
21
+
22
+ validateInput(input) {
23
+ return input.query ? [] : ['query is required'];
24
+ },
25
+
26
+ async call(input) {
27
+ const limit = input.limit || 5;
28
+
29
+ // Try Brave Search API
30
+ const braveKey = process.env.BRAVE_API_KEY;
31
+ if (braveKey) {
32
+ return searchBrave(input.query, limit, braveKey);
33
+ }
34
+
35
+ // Try SearXNG
36
+ const searxUrl = process.env.SEARXNG_URL;
37
+ if (searxUrl) {
38
+ return searchSearxng(input.query, limit, searxUrl);
39
+ }
40
+
41
+ return 'No search API configured. Set BRAVE_API_KEY or SEARXNG_URL environment variable. ' +
42
+ 'Alternatively, use the WebFetch tool to fetch specific URLs directly.';
43
+ },
44
+ };
45
+
46
+ async function searchBrave(query, limit, apiKey) {
47
+ try {
48
+ const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${limit}`;
49
+ const res = await fetch(url, {
50
+ headers: {
51
+ 'Accept': 'application/json',
52
+ 'X-Subscription-Token': apiKey,
53
+ },
54
+ });
55
+
56
+ if (!res.ok) return `Brave API error: ${res.status}`;
57
+
58
+ const data = await res.json();
59
+ const results = (data.web?.results || []).slice(0, limit);
60
+
61
+ if (results.length === 0) return 'No results found.';
62
+
63
+ return results.map((r, i) =>
64
+ `${i + 1}. ${r.title}\n ${r.url}\n ${r.description || ''}`
65
+ ).join('\n\n');
66
+ } catch (err) {
67
+ return `Search error: ${err.message}`;
68
+ }
69
+ }
70
+
71
+ async function searchSearxng(query, limit, baseUrl) {
72
+ try {
73
+ const url = `${baseUrl}/search?q=${encodeURIComponent(query)}&format=json&categories=general`;
74
+ const res = await fetch(url);
75
+
76
+ if (!res.ok) return `SearXNG error: ${res.status}`;
77
+
78
+ const data = await res.json();
79
+ const results = (data.results || []).slice(0, limit);
80
+
81
+ if (results.length === 0) return 'No results found.';
82
+
83
+ return results.map((r, i) =>
84
+ `${i + 1}. ${r.title}\n ${r.url}\n ${r.content || ''}`
85
+ ).join('\n\n');
86
+ } catch (err) {
87
+ return `Search error: ${err.message}`;
88
+ }
89
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Write Tool — matches Claude Code's exact behavior.
3
+ *
4
+ * Features:
5
+ * - Creates parent directories if needed
6
+ * - Requires Read first for existing file overwrites
7
+ * - No README creation unless explicitly asked
8
+ */
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { hasBeenRead, markRead } from './read.mjs';
12
+
13
+ export const WriteTool = {
14
+ name: 'Write',
15
+ description: 'Write content to a file. Creates parent dirs if needed.',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ file_path: { type: 'string', description: 'Absolute path to the file' },
20
+ content: { type: 'string', description: 'The content to write' },
21
+ },
22
+ required: ['file_path', 'content'],
23
+ },
24
+ validateInput(input) {
25
+ const errors = [];
26
+ if (!input.file_path) errors.push('file_path required');
27
+ return errors;
28
+ },
29
+ async call(input) {
30
+ const filePath = path.resolve(input.file_path);
31
+
32
+ // Check if file already exists — require Read first for overwrites
33
+ if (fs.existsSync(filePath)) {
34
+ if (!hasBeenRead(filePath)) {
35
+ return `Error: File ${filePath} already exists. You must Read it first before overwriting.`;
36
+ }
37
+ }
38
+
39
+ // Create parent directory if it doesn't exist
40
+ const dir = path.dirname(filePath);
41
+ try {
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ } catch (e) {
44
+ return `Error creating directory ${dir}: ${e.message}`;
45
+ }
46
+
47
+ try {
48
+ fs.writeFileSync(filePath, input.content);
49
+ markRead(filePath); // Mark as read after writing
50
+ return `File written: ${filePath}`;
51
+ } catch (e) {
52
+ return `Error writing file: ${e.message}`;
53
+ }
54
+ },
55
+ };
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Banner & Branding — Tarang CLI startup display.
3
+ */
4
+
5
+ import { execSync } from 'node:child_process';
6
+ import * as path from 'node:path';
7
+
8
+ // ANSI color helpers
9
+ const BOLD = '\x1b[1m';
10
+ const DIM = '\x1b[2m';
11
+ const RESET = '\x1b[0m';
12
+ const GREEN = '\x1b[32m';
13
+ const CYAN = '\x1b[36m';
14
+ const YELLOW = '\x1b[33m';
15
+ const RED = '\x1b[31m';
16
+ const BLUE = '\x1b[34m';
17
+ const BOLD_GREEN = '\x1b[1;32m';
18
+ const BOLD_CYAN = '\x1b[1;36m';
19
+
20
+ const BANNER_ORCA = [
21
+ ' ██████╗ ██████╗ ██████╗ █████╗ ',
22
+ '██╔═══██╗ ██╔══██╗ ██╔════╝ ██╔══██╗',
23
+ '██║ ██║ ██████╔╝ ██║ ███████║',
24
+ '██║ ██║ ██╔══██╗ ██║ ██╔══██║',
25
+ '╚██████╔╝ ██║ ██║ ╚██████╗ ██║ ██║',
26
+ ' ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝',
27
+ ];
28
+
29
+ /**
30
+ * Print the branded ASCII art banner.
31
+ */
32
+ export function printBanner() {
33
+ process.stderr.write('\n');
34
+ for (const line of BANNER_ORCA) {
35
+ process.stderr.write(` ${BOLD_CYAN}${line}${RESET}\n`);
36
+ }
37
+ process.stderr.write(` ${DIM}Orchestration of Composable Agents${RESET}\n`);
38
+ process.stderr.write('\n');
39
+ }
40
+
41
+ /**
42
+ * Print project info bar with version, project name, and git info.
43
+ * @param {string} version
44
+ */
45
+ export function printProjectInfo(version) {
46
+ const cwd = process.cwd();
47
+ const projectName = path.basename(cwd);
48
+ const gitInfo = getGitInfo(cwd);
49
+
50
+ const sep = `${DIM} │ ${RESET}`;
51
+ let info = ` ${DIM}v${version}${RESET}${sep}${BOLD}${projectName}${RESET}`;
52
+ if (gitInfo) {
53
+ info += `${sep}${YELLOW}${gitInfo}${RESET}`;
54
+ }
55
+
56
+ // Draw a boxed info bar
57
+ const barWidth = 60;
58
+ const topBorder = `${BLUE}┌${'─'.repeat(barWidth)}┐${RESET}`;
59
+ const bottomBorder = `${BLUE}└${'─'.repeat(barWidth)}┘${RESET}`;
60
+
61
+ process.stderr.write(`${topBorder}\n`);
62
+ process.stderr.write(`${BLUE}│${RESET} ${info}${' '.repeat(Math.max(0, barWidth - stripAnsi(info).length - 1))}${BLUE}│${RESET}\n`);
63
+ process.stderr.write(`${bottomBorder}\n`);
64
+ }
65
+
66
+ /**
67
+ * Print keyboard hints and usage tips.
68
+ */
69
+ export function printHints() {
70
+ const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
71
+
72
+ process.stderr.write(`${GREEN}Type your instructions${RESET}, or ${CYAN}/help${RESET} for commands\n`);
73
+ process.stderr.write(`${DIM}Ctrl+C${RESET}${DIM}=exit ${RESET}${DIM}/clear${RESET}${DIM}=reset ${RESET}${DIM}/config${RESET}${DIM}=settings ${RESET}${DIM}/login${RESET}${DIM}=auth${RESET}\n`);
74
+ process.stderr.write(`${DIM}env:${env} models:configured via browser (/config)${RESET}\n`);
75
+ process.stderr.write('\n');
76
+ }
77
+
78
+ /**
79
+ * Print auth status indicators.
80
+ * @param {object} creds - { token, openRouterKey, anthropicKey, backendUrl, mode }
81
+ */
82
+ export function printAuthStatus(creds) {
83
+ const check = `${GREEN}✓${RESET}`;
84
+ const cross = `${RED}✗${RESET}`;
85
+
86
+ const tokenOk = !!creds.token;
87
+ const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
88
+
89
+ process.stderr.write(` Auth: ${tokenOk ? `${check} logged in ${DIM}(/whoami for details)${RESET}` : `${cross} not logged in ${DIM}(/login)${RESET}`}\n`);
90
+ process.stderr.write(` Env: ${DIM}${env}${RESET}\n`);
91
+ process.stderr.write(` Mode: ${DIM}${creds.mode || 'auto'}${RESET}\n`);
92
+ process.stderr.write('\n');
93
+ }
94
+
95
+ /**
96
+ * Print config in a styled format.
97
+ * @param {object} creds
98
+ */
99
+ export function printStyledConfig(creds) {
100
+ const check = `${GREEN}✓${RESET}`;
101
+ const cross = `${RED}✗${RESET}`;
102
+ const mask = (val) => {
103
+ if (!val) return `${cross} not set`;
104
+ if (val.length <= 8) return `${check} ****`;
105
+ return `${check} ${val.slice(0, 6)}...${val.slice(-4)}`;
106
+ };
107
+
108
+ process.stderr.write(`\n${BOLD}Orca Configuration${RESET} ${DIM}(~/.orca/config.json)${RESET}\n`);
109
+ process.stderr.write(`${'─'.repeat(50)}\n`);
110
+ process.stderr.write(` Token: ${mask(creds.token)}\n`);
111
+ process.stderr.write(` OpenRouter: ${mask(creds.openRouterKey)}\n`);
112
+ process.stderr.write(` Anthropic: ${mask(creds.anthropicKey)}\n`);
113
+ const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
114
+ process.stderr.write(` Environment: ${DIM}${env}${RESET}\n`);
115
+ process.stderr.write(` Backend URL: ${DIM}${creds.backendUrl}${RESET}\n`);
116
+ process.stderr.write(` Mode: ${DIM}${creds.mode || 'auto'}${RESET}\n`);
117
+ process.stderr.write('\n');
118
+ }
119
+
120
+ /**
121
+ * Print a goodbye message.
122
+ */
123
+ export function printGoodbye() {
124
+ process.stderr.write(`\n${BOLD_CYAN}Goodbye!${RESET}\n\n`);
125
+ }
126
+
127
+ /**
128
+ * Get git branch and change count for current directory.
129
+ * @param {string} cwd
130
+ * @returns {string|null}
131
+ */
132
+ function getGitInfo(cwd) {
133
+ try {
134
+ const branch = execSync('git branch --show-current', {
135
+ cwd, encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'],
136
+ }).trim();
137
+
138
+ if (!branch) return null;
139
+
140
+ const status = execSync('git status --porcelain', {
141
+ cwd, encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'],
142
+ }).trim();
143
+
144
+ const changes = status ? status.split('\n').filter(Boolean).length : 0;
145
+
146
+ if (changes > 0) {
147
+ return `\u2387 ${branch} (${changes} changed)`;
148
+ }
149
+ return `\u2387 ${branch}`;
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Strip ANSI escape codes for length calculation.
157
+ */
158
+ function stripAnsi(str) {
159
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
160
+ }
161
+
162
+ /**
163
+ * Branded HTML for OAuth success page.
164
+ */
165
+ export function getLoginSuccessHTML() {
166
+ return `<!DOCTYPE html>
167
+ <html>
168
+ <head>
169
+ <meta charset="utf-8">
170
+ <title>Orca - Login Successful</title>
171
+ <style>
172
+ body {
173
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
174
+ background: #0d1117;
175
+ color: #c9d1d9;
176
+ display: flex;
177
+ justify-content: center;
178
+ align-items: center;
179
+ min-height: 100vh;
180
+ margin: 0;
181
+ }
182
+ .container {
183
+ text-align: center;
184
+ padding: 40px;
185
+ }
186
+ .logo {
187
+ font-size: 48px;
188
+ font-weight: bold;
189
+ margin-bottom: 8px;
190
+ }
191
+ .dev { color: #3fb950; }
192
+ .orca { color: #58a6ff; }
193
+ .check {
194
+ font-size: 64px;
195
+ color: #3fb950;
196
+ margin: 24px 0;
197
+ }
198
+ h1 {
199
+ color: #f0f6fc;
200
+ font-size: 24px;
201
+ margin: 16px 0 8px;
202
+ }
203
+ p {
204
+ color: #8b949e;
205
+ font-size: 16px;
206
+ }
207
+ .hint {
208
+ margin-top: 32px;
209
+ padding: 16px;
210
+ background: #161b22;
211
+ border-radius: 8px;
212
+ border: 1px solid #30363d;
213
+ }
214
+ code {
215
+ background: #1f2937;
216
+ padding: 2px 6px;
217
+ border-radius: 4px;
218
+ color: #58a6ff;
219
+ }
220
+ </style>
221
+ </head>
222
+ <body>
223
+ <div class="container">
224
+ <div class="logo">
225
+ <span class="orca">ORCA</span>
226
+ </div>
227
+ <div class="check">&#10003;</div>
228
+ <h1>Login Successful!</h1>
229
+ <p>You can close this tab and return to your terminal.</p>
230
+ <div class="hint">
231
+ <p>Next step: set your API key</p>
232
+ <code>orca config --openrouter-key YOUR_KEY</code>
233
+ </div>
234
+ </div>
235
+ </body>
236
+ </html>`;
237
+ }