@axplusb/kepler 1.0.4 → 1.0.9

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 (48) hide show
  1. package/KEPLER-README.md +128 -2
  2. package/package.json +4 -4
  3. package/pulse/app/activity/page.tsx +1 -1
  4. package/pulse/app/api/import/route.ts +1 -1
  5. package/pulse/app/api/memory/route.ts +2 -2
  6. package/pulse/app/costs/page.tsx +1 -1
  7. package/pulse/app/export/page.tsx +3 -3
  8. package/pulse/app/globals.css +3 -3
  9. package/pulse/app/help/page.tsx +11 -11
  10. package/pulse/app/history/page.tsx +2 -2
  11. package/pulse/app/layout.tsx +2 -2
  12. package/pulse/app/memory/page.tsx +2 -2
  13. package/pulse/app/overview-client.tsx +1 -1
  14. package/pulse/app/page.tsx +2 -2
  15. package/pulse/app/plans/page.tsx +2 -2
  16. package/pulse/app/projects/page.tsx +1 -1
  17. package/pulse/app/sessions/page.tsx +1 -1
  18. package/pulse/app/settings/page.tsx +4 -4
  19. package/pulse/app/todos/page.tsx +2 -2
  20. package/pulse/app/tools/page.tsx +1 -1
  21. package/pulse/cli.js +15 -25
  22. package/pulse/components/layout/sidebar.tsx +2 -2
  23. package/pulse/components/sessions/replay/user-tool-result.tsx +1 -1
  24. package/pulse/lib/claude-reader.ts +1 -1
  25. package/pulse/lib/decode.ts +1 -1
  26. package/pulse/package.json +3 -3
  27. package/src/auth/tarang-auth.mjs +1 -1
  28. package/src/config/cli-args.mjs +5 -0
  29. package/src/context/retriever.mjs +1 -1
  30. package/src/context/skeleton.mjs +1 -1
  31. package/src/core/approval.mjs +22 -53
  32. package/src/core/headless.mjs +68 -24
  33. package/src/core/paths.mjs +1 -1
  34. package/src/core/project-artifacts.mjs +37 -0
  35. package/src/core/stream-client.mjs +6 -1
  36. package/src/core/tool-executor.mjs +163 -55
  37. package/src/skills/installer.mjs +188 -0
  38. package/src/skills/loader.mjs +217 -112
  39. package/src/terminal/main.mjs +19 -1
  40. package/src/terminal/repl.mjs +40 -105
  41. package/src/terminal/skills.mjs +54 -0
  42. package/src/terminal/tool-display.mjs +82 -0
  43. package/src/tools/bash.mjs +5 -2
  44. package/src/tools/project-overview.mjs +418 -0
  45. package/src/tools/registry.mjs +0 -16
  46. package/src/ui/banner.mjs +7 -14
  47. package/src/ui/formatter.mjs +6 -40
  48. package/README.md.orca +0 -82
@@ -1,8 +1,8 @@
1
1
  {
2
- "name": "orca-pulse",
3
- "version": "1.0.0",
2
+ "name": "kepler-pulse",
3
+ "version": "1.0.1",
4
4
  "private": true,
5
- "description": "Orca Pulse — real-time analytics dashboard for Orca AI agent sessions",
5
+ "description": "Kepler Pulse — real-time analytics dashboard for Kepler AI agent sessions",
6
6
  "scripts": {
7
7
  "dev": "next dev",
8
8
  "build": "next build",
@@ -10,7 +10,7 @@ import * as http from 'node:http';
10
10
  import { getLoginSuccessHTML } from '../ui/banner.mjs';
11
11
  import { resolveBackendUrl } from '../core/backend-url.mjs';
12
12
 
13
- const CONFIG_DIR = process.env.KEPLER_HOME || process.env.ORCA_HOME || path.join(os.homedir(), '.kepler');
13
+ const CONFIG_DIR = process.env.KEPLER_HOME || path.join(os.homedir(), '.kepler');
14
14
  const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
15
15
 
16
16
  export class TarangAuth {
@@ -26,6 +26,7 @@ export function parseArgs(args) {
26
26
  systemPrompt: null,
27
27
  addDirs: [],
28
28
  maxTurns: null,
29
+ timeout: null,
29
30
  allowedTools: null,
30
31
  disallowedTools: null,
31
32
  resume: false,
@@ -72,6 +73,10 @@ export function parseArgs(args) {
72
73
  result.maxTurns = parseInt(args[++i], 10);
73
74
  break;
74
75
 
76
+ case '--timeout':
77
+ result.timeout = parseInt(args[++i], 10);
78
+ break;
79
+
75
80
  case '--allowedTools':
76
81
  result.allowedTools = args[++i]?.split(',').map(s => s.trim());
77
82
  break;
@@ -8,7 +8,7 @@ import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import { indexDir as getIndexDir } from '../core/paths.mjs';
10
10
 
11
- const IGNORED_DIRS = new Set(['.git', 'node_modules', '.orca', '__pycache__', '.venv', 'venv', 'dist', 'build', '.next']);
11
+ const IGNORED_DIRS = new Set(['.git', 'node_modules', '.kepler', '__pycache__', '.venv', 'venv', 'dist', 'build', '.next']);
12
12
  const CODE_EXTS = new Set(['.js', '.mjs', '.ts', '.tsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.c', '.cpp', '.h', '.css', '.html', '.json', '.yaml', '.yml', '.toml', '.md', '.sh']);
13
13
  const MAX_FILE_SIZE = 100_000; // 100KB
14
14
  const CHUNK_LINES = 50;
@@ -12,7 +12,7 @@
12
12
  import * as fs from 'node:fs';
13
13
  import * as path from 'node:path';
14
14
 
15
- const IGNORED_DIRS = new Set(['.git', 'node_modules', '.orca', '__pycache__', '.venv', 'venv', 'dist', 'build', '.next', '.cache', 'coverage', '.tox']);
15
+ const IGNORED_DIRS = new Set(['.git', 'node_modules', '.kepler', '__pycache__', '.venv', 'venv', 'dist', 'build', '.next', '.cache', 'coverage', '.tox']);
16
16
  const CODE_EXTS = new Set(['.js', '.mjs', '.ts', '.tsx', '.py', '.go', '.rs', '.java', '.rb', '.c', '.cpp', '.h']);
17
17
  const MAX_FILE_SIZE = 200_000;
18
18
 
@@ -10,7 +10,7 @@
10
10
  * Shell commands are risk-assessed (safe/medium/high).
11
11
  */
12
12
 
13
- import * as path from 'node:path';
13
+ import { toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
14
14
 
15
15
  // ── Tool Classification ──
16
16
 
@@ -66,44 +66,6 @@ const WHITE = '\x1b[37m';
66
66
 
67
67
  const write = (s) => process.stderr.write(s);
68
68
 
69
- // ── Tool Description ──
70
-
71
- function shortPath(p) {
72
- if (!p) return '';
73
- const cwd = process.cwd();
74
- return p.startsWith(cwd) ? p.slice(cwd.length + 1) : p;
75
- }
76
-
77
- function toolSummary(toolName, args) {
78
- switch (toolName) {
79
- case 'shell':
80
- return args.command || '(empty)';
81
- case 'write_file':
82
- return shortPath(args.path || args.file_path || '');
83
- case 'write_project': {
84
- const files = args.files || [];
85
- if (files.length === 0) return 'batch write';
86
- return files.map(f => shortPath(f.path || f.file_path || '')).join(', ');
87
- }
88
- case 'edit_file': {
89
- const fp = shortPath(args.path || args.file_path || '');
90
- const search = (args.search || '').slice(0, 30);
91
- return search ? `${fp} "${search}..."` : fp;
92
- }
93
- case 'delete_file':
94
- return shortPath(args.path || args.file_path || '');
95
- case 'read_file':
96
- case 'read_files':
97
- return shortPath(args.file_path || args.path || (args.file_paths || [])[0] || '');
98
- case 'search_code':
99
- return `"${args.query || args.pattern || ''}"`;
100
- case 'list_files':
101
- return args.pattern || args.path || '';
102
- default:
103
- return Object.values(args || {}).filter(v => typeof v === 'string').join(', ').slice(0, 50) || '';
104
- }
105
- }
106
-
107
69
  // ── Approval Manager ──
108
70
 
109
71
  export class ApprovalManager {
@@ -138,7 +100,7 @@ export class ApprovalManager {
138
100
  return `${DIM}ask${RST}`;
139
101
  }
140
102
 
141
- async check(toolName, args, requireApproval = false) {
103
+ async check(toolName, args, requireApproval = false, context = {}) {
142
104
  if (this.planMode && WRITE_TOOLS.has(toolName)) {
143
105
  return { approved: false, reason: `Blocked by plan mode: ${toolName}` };
144
106
  }
@@ -157,11 +119,11 @@ export class ApprovalManager {
157
119
  return { approved: true };
158
120
  }
159
121
  if (FORCE_APPROVAL_SHELL.some(p => p.test(args.command || ''))) {
160
- return this._prompt(toolName, args);
122
+ return this._prompt(toolName, args, context);
161
123
  }
162
124
  }
163
125
  if (NEVER_AUTO_APPROVE.has(toolName)) {
164
- return this._prompt(toolName, args);
126
+ return this._prompt(toolName, args, context);
165
127
  }
166
128
  if (this.approveAll) {
167
129
  this.history.push({ tool: toolName, decision: 'auto', time: Date.now() });
@@ -171,20 +133,27 @@ export class ApprovalManager {
171
133
  this.history.push({ tool: toolName, decision: 'type-auto', time: Date.now() });
172
134
  return { approved: true };
173
135
  }
174
- return this._prompt(toolName, args);
136
+ return this._prompt(toolName, args, context);
175
137
  }
176
138
 
177
- async _prompt(toolName, args) {
139
+ async _prompt(toolName, args, context = {}) {
178
140
  const baseRisk = RISK_LEVELS[toolName] || 'medium';
179
- const risk = toolName === 'shell' ? assessShellRisk(args.command) : baseRisk;
180
- const summary = toolSummary(toolName, args);
141
+ const assessedRisk = toolName === 'shell' ? assessShellRisk(args.command) : baseRisk;
142
+ const risk = context.risk || assessedRisk;
143
+ const label = toolDisplayLabel(toolName);
144
+ const summary = toolDisplaySummary(toolName, args);
181
145
  const isDestructive = risk === 'high';
182
146
 
183
- // ── Prompt line ──
147
+ write(`\n ${isDestructive ? `${YELLOW}⚠${RST}` : `${CYAN}?${RST}`} ${BOLD}Approval required${RST}\n`);
148
+ write(` ${GRAY}Action${RST} ${WHITE}${label}${RST}\n`);
149
+ if (summary) write(` ${GRAY}Target${RST} ${WHITE}${summary.slice(0, 160)}${RST}\n`);
150
+ write(` ${GRAY}Risk${RST} ${isDestructive ? YELLOW : CYAN}${risk}${RST}\n`);
151
+ if (context.reason) write(` ${GRAY}Reason${RST} ${DIM}${String(context.reason).slice(0, 160)}${RST}\n`);
152
+
184
153
  if (isDestructive) {
185
- write(` ${YELLOW}⚠${RST} ${DIM}Allow?${RST} ${WHITE}[y]${RST} yes ${WHITE}[n]${RST} no ${WHITE}[d]${RST} details\n`);
154
+ write(` ${DIM}Choose${RST} ${WHITE}[y]${RST} allow once ${WHITE}[n]${RST} deny ${WHITE}[d]${RST} details\n`);
186
155
  } else {
187
- write(` ${DIM}?${RST} ${DIM}Allow?${RST} ${WHITE}[y]${RST} yes ${WHITE}[n]${RST} no ${WHITE}[a]${RST} always ${WHITE}[t]${RST} type ${WHITE}[d]${RST} details\n`);
156
+ write(` ${DIM}Choose${RST} ${WHITE}[y]${RST} once ${WHITE}[n]${RST} deny ${WHITE}[t]${RST} this action ${WHITE}[a]${RST} all ${WHITE}[d]${RST} details\n`);
188
157
  }
189
158
 
190
159
  const key = await this._readKey();
@@ -206,7 +175,7 @@ export class ApprovalManager {
206
175
 
207
176
  case 'a':
208
177
  case 'A':
209
- if (isDestructive) return this._prompt(toolName, args);
178
+ if (isDestructive) return this._prompt(toolName, args, context);
210
179
  this.approveAll = true;
211
180
  write(` ${GREEN}✓✓${RST} ${DIM}allow-all activated${RST}\n\n`);
212
181
  this.history.push({ tool: toolName, decision: 'approve-all', time: Date.now() });
@@ -214,7 +183,7 @@ export class ApprovalManager {
214
183
 
215
184
  case 't':
216
185
  case 'T':
217
- if (isDestructive) return this._prompt(toolName, args);
186
+ if (isDestructive) return this._prompt(toolName, args, context);
218
187
  this.approvedToolTypes.add(toolName);
219
188
  write(` ${GREEN}✓${RST} ${DIM}always allow ${toolName}${RST}\n\n`);
220
189
  this.history.push({ tool: toolName, decision: 'type-approve', time: Date.now() });
@@ -223,10 +192,10 @@ export class ApprovalManager {
223
192
  case 'd':
224
193
  case 'D':
225
194
  write(`\n${DIM}${JSON.stringify(args, null, 2)}${RST}\n\n`);
226
- return this._prompt(toolName, args);
195
+ return this._prompt(toolName, args, context);
227
196
 
228
197
  default:
229
- return this._prompt(toolName, args);
198
+ return this._prompt(toolName, args, context);
230
199
  }
231
200
  }
232
201
 
@@ -13,10 +13,9 @@
13
13
 
14
14
  import { TarangStreamClient } from './stream-client.mjs';
15
15
  import { createToolExecutor } from './tool-executor.mjs';
16
+ import { persistProjectArtifacts } from './project-artifacts.mjs';
16
17
  import { TarangAuth } from '../auth/tarang-auth.mjs';
17
18
  import { ApprovalManager } from './approval.mjs';
18
- import { ContextRetriever } from '../context/retriever.mjs';
19
- import { buildProjectSkeleton } from '../context/skeleton.mjs';
20
19
 
21
20
  /**
22
21
  * Run a single instruction in headless mode.
@@ -46,22 +45,8 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
46
45
  process.exit(1);
47
46
  }
48
47
 
49
- // ── Index project (with timeout large repos can take minutes) ──
50
- log('Indexing project...');
51
- const retriever = new ContextRetriever(process.cwd());
52
- try {
53
- const indexPromise = retriever.buildIndex();
54
- const indexTimeout = new Promise((_, reject) =>
55
- setTimeout(() => reject(new Error('Index timeout')), 15000)
56
- );
57
- await Promise.race([indexPromise, indexTimeout]);
58
- log('Index ready');
59
- } catch (e) {
60
- log(`Index skipped: ${e.message || 'failed'}`);
61
- }
62
-
63
- const skeleton = buildProjectSkeleton(process.cwd());
64
- const toolExecutor = createToolExecutor({ retriever });
48
+ // Projects are registered and indexed only when the agent requests an overview.
49
+ const toolExecutor = createToolExecutor();
65
50
 
66
51
  // Auto-approve everything — no prompts
67
52
  const approval = new ApprovalManager({ autoApprove: true });
@@ -85,14 +70,24 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
85
70
  // ── Execute ──
86
71
  emit({ type: 'start', timestamp: Date.now(), instruction, model: model || 'default', cwd: process.cwd() });
87
72
 
88
- const execContext = { cwd: process.cwd(), freeswim: true };
89
- if (skeleton) execContext.project_skeleton = skeleton;
73
+ const execContext = {
74
+ cwd: process.cwd(),
75
+ freeswim: true,
76
+ project_resources: toolExecutor.getProjectResources(),
77
+ agent_context: toolExecutor.getAgentContext(),
78
+ };
90
79
  if (model) execContext.model_override = model;
91
80
 
92
81
  let toolCount = 0;
93
82
  let finalContent = '';
94
83
  let totalCost = 0;
95
84
 
85
+ // ── Telemetry collectors ──
86
+ const toolCalls = []; // { tool, duration_ms, success }
87
+ const subAgents = []; // { type, model, duration_s, tool_calls, success }
88
+ let stagnationCount = 0;
89
+ let usage = {}; // { input_tokens, output_tokens, cache_read, cache_write }
90
+
96
91
  try {
97
92
  for await (const event of client.execute(instruction, execContext)) {
98
93
  const { type, data } = event;
@@ -101,6 +96,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
101
96
  toolCount++;
102
97
  const toolName = data?.tool || 'unknown';
103
98
  const args = data?.args || {};
99
+ toolCalls.push({ tool: toolName, success: null, duration_ms: 0 });
104
100
  emit({ type: 'tool_call', tool: toolName, args, approved: true });
105
101
  log(`Tool: ${toolName}`);
106
102
  }
@@ -108,9 +104,42 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
108
104
  if (type === 'tool_result' || type === 'tool_done') {
109
105
  const success = data?.success !== false;
110
106
  const duration = data?.duration_s || 0;
107
+ // Update last tool call with result
108
+ const last = toolCalls.findLast(t => t.tool === (data?.tool || ''));
109
+ if (last) { last.success = success; last.duration_ms = Math.round(duration * 1000); }
111
110
  emit({ type: 'tool_result', tool: data?.tool || '', success, duration_ms: Math.round(duration * 1000) });
112
111
  }
113
112
 
113
+ if (type === 'sub_agent_start') {
114
+ log(`SubAgent: ${data?.type} (${data?.model})`);
115
+ }
116
+
117
+ if (type === 'sub_agent_complete') {
118
+ subAgents.push({
119
+ type: data?.type || '',
120
+ model: data?.model || '',
121
+ duration_s: data?.duration_s || 0,
122
+ tool_calls: data?.tool_calls || 0,
123
+ success: data?.success !== false,
124
+ });
125
+ emit({ type: 'sub_agent', ...data });
126
+ log(`SubAgent done: ${data?.type} (${data?.tool_calls} tools, ${data?.duration_s}s)`);
127
+ }
128
+
129
+ if (type === 'plan_created' || type === 'goal_created') {
130
+ persistProjectArtifacts(
131
+ data,
132
+ toolExecutor.getProjectResources(),
133
+ log,
134
+ );
135
+ }
136
+
137
+ if (type === 'stagnation' || type === 'stagnation_detected') {
138
+ stagnationCount++;
139
+ emit({ type: 'stagnation', reason: data?.reason || '', strategy: data?.recovery_strategy || '' });
140
+ log(`Stagnation: ${data?.reason || ''}`);
141
+ }
142
+
114
143
  if (type === 'content') {
115
144
  finalContent = data?.text || '';
116
145
  }
@@ -122,10 +151,15 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
122
151
 
123
152
  if (type === 'complete') {
124
153
  totalCost = data?.cost || data?.total_cost || 0;
125
-
126
- // Extract cost from usage breakdown if available
127
- if (data?.usage?.total_cost) {
128
- totalCost = data.usage.total_cost;
154
+ if (data?.usage?.total_cost) totalCost = data.usage.total_cost;
155
+ // Capture token usage
156
+ if (data?.usage) {
157
+ usage = {
158
+ input_tokens: data.usage.total_input_tokens || data.usage.input_tokens || 0,
159
+ output_tokens: data.usage.total_output_tokens || data.usage.output_tokens || 0,
160
+ cache_read: data.usage.cache_read_input_tokens || data.usage.cache_read || 0,
161
+ cache_write: data.usage.cache_creation_input_tokens || data.usage.cache_write || 0,
162
+ };
129
163
  }
130
164
  }
131
165
 
@@ -148,9 +182,19 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
148
182
 
149
183
  const durationS = (Date.now() - startTime) / 1000;
150
184
 
185
+ // ── Tool breakdown ──
186
+ const toolBreakdown = {};
187
+ for (const t of toolCalls) {
188
+ toolBreakdown[t.tool] = (toolBreakdown[t.tool] || 0) + 1;
189
+ }
190
+
151
191
  emit({
152
192
  type: 'complete',
153
193
  tools: toolCount,
194
+ tool_breakdown: toolBreakdown,
195
+ sub_agents: subAgents,
196
+ stagnation_triggers: stagnationCount,
197
+ usage,
154
198
  duration_s: Math.round(durationS * 10) / 10,
155
199
  cost_usd: totalCost,
156
200
  model: model || 'default',
@@ -22,7 +22,7 @@ import * as path from 'node:path';
22
22
  import * as os from 'node:os';
23
23
  import * as crypto from 'node:crypto';
24
24
 
25
- const KEPLER_HOME = process.env.KEPLER_HOME || process.env.ORCA_HOME || path.join(os.homedir(), '.kepler');
25
+ const KEPLER_HOME = process.env.KEPLER_HOME || path.join(os.homedir(), '.kepler');
26
26
 
27
27
  /**
28
28
  * Hash a project path to a short directory name.
@@ -0,0 +1,37 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ /**
5
+ * Persist generated session artifacts only beneath roots registered by the CLI.
6
+ */
7
+ export function persistProjectArtifacts(data, resources, log = () => {}) {
8
+ const projectIds = new Set(
9
+ Array.isArray(data?.project_ids) ? data.project_ids.filter(Boolean) : []
10
+ );
11
+ const registered = Array.isArray(resources) ? resources : [];
12
+ const targets = projectIds.size > 0
13
+ ? registered.filter(resource => projectIds.has(resource.project_id))
14
+ : registered.length === 1 ? registered : [];
15
+ const artifacts = [
16
+ ['goal', 'goal.md'],
17
+ ['plan', 'plan.md'],
18
+ ].filter(([field]) => typeof data?.[field] === 'string' && data[field].trim());
19
+ const written = [];
20
+
21
+ for (const resource of targets) {
22
+ for (const [field, filename] of artifacts) {
23
+ try {
24
+ const keplerDir = path.join(resource.root, '.kepler');
25
+ fs.mkdirSync(keplerDir, { recursive: true });
26
+ const artifactPath = path.join(keplerDir, filename);
27
+ fs.writeFileSync(artifactPath, data[field], 'utf-8');
28
+ resource[field] = data[field];
29
+ written.push(artifactPath);
30
+ log(`${field} saved -> ${artifactPath}`);
31
+ } catch {
32
+ // Persistence is helpful but must not stop the agent.
33
+ }
34
+ }
35
+ }
36
+ return written;
37
+ }
@@ -290,7 +290,12 @@ export class TarangStreamClient {
290
290
  }
291
291
 
292
292
  // Use the same ApprovalManager for consistent UX
293
- const { approved, reason: denyReason } = await this.approval.check(tool, args || {}, true);
293
+ const { approved, reason: denyReason } = await this.approval.check(
294
+ tool,
295
+ args || {},
296
+ true,
297
+ { risk, reason },
298
+ );
294
299
 
295
300
  // Map ApprovalManager decision to framework scope
296
301
  let decision, scope;