@axplusb/kepler 1.0.10 → 2.0.2
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/package.json +5 -2
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/components/layout/bottom-nav.tsx +2 -1
- package/pulse/components/layout/sidebar.tsx +2 -1
- package/src/context/retriever.mjs +42 -4
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/approval.mjs +154 -95
- package/src/core/backend-url.mjs +2 -2
- package/src/core/headless.mjs +5 -0
- package/src/core/risk-tier.mjs +245 -0
- package/src/core/stream-client.mjs +24 -1
- package/src/core/tool-executor.mjs +58 -5
- package/src/onboarding/preflight.mjs +292 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/terminal/ansi.mjs +44 -22
- package/src/terminal/repl.mjs +487 -133
- package/src/tools/project-overview.mjs +109 -16
- package/src/ui/approval.mjs +167 -0
- package/src/ui/banner.mjs +133 -122
- package/src/ui/dock.mjs +88 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/mission-report.mjs +264 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/status-bar.mjs +275 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/tool-card.mjs +322 -0
- package/src/ui/tool-details.mjs +277 -0
|
@@ -8,6 +8,35 @@ import { buildProjectSkeleton } from '../context/skeleton.mjs';
|
|
|
8
8
|
import { indexDir as getIndexDir } from '../core/paths.mjs';
|
|
9
9
|
|
|
10
10
|
const RESOURCE_FILE = 'project-resource.json';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Expand "~" and trim surrounding quotes/whitespace. Does NOT unescape shell
|
|
14
|
+
* meta characters — that is a separate, last-resort step done only if the
|
|
15
|
+
* literal path does not resolve.
|
|
16
|
+
*/
|
|
17
|
+
function normalizePathInput(p) {
|
|
18
|
+
let s = String(p || '').trim();
|
|
19
|
+
// Trim balanced surrounding quotes.
|
|
20
|
+
if ((s.startsWith('"') && s.endsWith('"')) ||
|
|
21
|
+
(s.startsWith("'") && s.endsWith("'"))) {
|
|
22
|
+
s = s.slice(1, -1);
|
|
23
|
+
}
|
|
24
|
+
// Tilde expansion (~ or ~/...).
|
|
25
|
+
if (s === '~' || s.startsWith('~/')) {
|
|
26
|
+
s = path.join(os.homedir(), s.slice(1));
|
|
27
|
+
}
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Replace common shell escape sequences with their literal characters. Used
|
|
33
|
+
* as a fallback when the literal path does not resolve — the agent may have
|
|
34
|
+
* pasted a copy of what they would type at a shell prompt.
|
|
35
|
+
*/
|
|
36
|
+
function unescapeShellPath(p) {
|
|
37
|
+
return String(p || '').replace(/\\([ \t()&$;'"])/g, '$1');
|
|
38
|
+
}
|
|
39
|
+
|
|
11
40
|
const LANGUAGE_EXTENSIONS = new Map([
|
|
12
41
|
['.py', 'Python'],
|
|
13
42
|
['.js', 'JavaScript'],
|
|
@@ -280,6 +309,12 @@ export class ProjectRegistry {
|
|
|
280
309
|
if (!rawPath) {
|
|
281
310
|
throw new Error('get_project_overview requires a project path');
|
|
282
311
|
}
|
|
312
|
+
|
|
313
|
+
// LLM sometimes passes shell-escaped paths ("Tarang\ Orca") or paths
|
|
314
|
+
// beginning with "~". Normalize defensively so the tool does not bounce
|
|
315
|
+
// back a "not found" error on a path that's correct apart from quoting.
|
|
316
|
+
rawPath = normalizePathInput(rawPath);
|
|
317
|
+
|
|
283
318
|
if (!path.isAbsolute(rawPath)) {
|
|
284
319
|
rawPath = path.resolve(process.cwd(), rawPath);
|
|
285
320
|
}
|
|
@@ -288,7 +323,15 @@ export class ProjectRegistry {
|
|
|
288
323
|
try {
|
|
289
324
|
root = fs.realpathSync(rawPath);
|
|
290
325
|
} catch {
|
|
291
|
-
|
|
326
|
+
// Try the unescaped variant explicitly so the error message can
|
|
327
|
+
// tell the agent what it actually attempted.
|
|
328
|
+
const unescaped = unescapeShellPath(rawPath);
|
|
329
|
+
if (unescaped !== rawPath) {
|
|
330
|
+
try { root = fs.realpathSync(unescaped); }
|
|
331
|
+
catch { throw new Error(`Project path not found: ${rawPath} (also tried ${unescaped})`); }
|
|
332
|
+
} else {
|
|
333
|
+
throw new Error(`Project path not found: ${rawPath}`);
|
|
334
|
+
}
|
|
292
335
|
}
|
|
293
336
|
if (!fs.statSync(root).isDirectory()) {
|
|
294
337
|
throw new Error(`Project path is not a directory: ${root}`);
|
|
@@ -377,25 +420,64 @@ export class ProjectRegistry {
|
|
|
377
420
|
if (!rawPath) {
|
|
378
421
|
if (root) return root;
|
|
379
422
|
if (this.projects.size === 1) return this.resources()[0].root;
|
|
380
|
-
|
|
423
|
+
// Fall back to the first registered project when the model omits
|
|
424
|
+
// both path and project_id. Beats throwing on an inferable case.
|
|
425
|
+
const first = this.resources()[0];
|
|
426
|
+
if (first) return first.root;
|
|
427
|
+
throw new Error('No projects registered. Call get_project_overview first.');
|
|
381
428
|
}
|
|
382
429
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
430
|
+
// LLM frequently passes shell-quoted paths copied from a terminal,
|
|
431
|
+
// e.g. "Tarang\ Orca/src/app/\(kepler\)/page.tsx". Normalize here so
|
|
432
|
+
// every tool benefits, not just get_project_overview.
|
|
433
|
+
rawPath = normalizePathInput(rawPath);
|
|
434
|
+
|
|
435
|
+
const buildCandidate = (input) => {
|
|
436
|
+
if (path.isAbsolute(input)) {
|
|
437
|
+
return canonicalizeCandidate(path.resolve(input));
|
|
438
|
+
}
|
|
387
439
|
if (!root) {
|
|
388
|
-
if (this.projects.size
|
|
389
|
-
|
|
440
|
+
if (this.projects.size === 1) {
|
|
441
|
+
return canonicalizeCandidate(path.resolve(this.resources()[0].root, input));
|
|
390
442
|
}
|
|
391
|
-
|
|
443
|
+
if (this.projects.size > 1) {
|
|
444
|
+
throw new Error('Relative path requires project_id when multiple projects are registered. Pass project_id or use an absolute path.');
|
|
445
|
+
}
|
|
446
|
+
throw new Error('No projects registered. Call get_project_overview first.');
|
|
392
447
|
}
|
|
393
|
-
|
|
394
|
-
}
|
|
448
|
+
return canonicalizeCandidate(path.resolve(root, input));
|
|
449
|
+
};
|
|
395
450
|
|
|
396
|
-
|
|
397
|
-
|
|
451
|
+
let candidate = buildCandidate(rawPath);
|
|
452
|
+
|
|
453
|
+
const findContaining = (cand) => [...this.projects.values()].find(({ resource }) =>
|
|
454
|
+
isWithin(resource.root, cand)
|
|
398
455
|
);
|
|
456
|
+
|
|
457
|
+
let containingProject = findContaining(candidate);
|
|
458
|
+
|
|
459
|
+
// Two reasons to try the unescaped variant:
|
|
460
|
+
// (1) candidate is outside every project root (literal "Tarang\ Orca"
|
|
461
|
+
// does not contain a real project), or
|
|
462
|
+
// (2) candidate is inside a root but does not exist on disk because
|
|
463
|
+
// a path segment like "\(kepler\)" only resolves once unescaped.
|
|
464
|
+
// We retry once on the unescaped form before raising.
|
|
465
|
+
const needsRetry = !containingProject ||
|
|
466
|
+
(!allowMissing && !fs.existsSync(candidate));
|
|
467
|
+
if (needsRetry) {
|
|
468
|
+
const unescaped = unescapeShellPath(rawPath);
|
|
469
|
+
if (unescaped !== rawPath) {
|
|
470
|
+
try {
|
|
471
|
+
const altCandidate = buildCandidate(unescaped);
|
|
472
|
+
const altProject = findContaining(altCandidate);
|
|
473
|
+
if (altProject && (allowMissing || fs.existsSync(altCandidate))) {
|
|
474
|
+
candidate = altCandidate;
|
|
475
|
+
containingProject = altProject;
|
|
476
|
+
}
|
|
477
|
+
} catch { /* fall through to the original error */ }
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
399
481
|
if (!containingProject) {
|
|
400
482
|
throw new Error(`Path is outside registered project roots: ${rawPath}`);
|
|
401
483
|
}
|
|
@@ -406,10 +488,21 @@ export class ProjectRegistry {
|
|
|
406
488
|
}
|
|
407
489
|
|
|
408
490
|
projectForPath(filePath) {
|
|
409
|
-
const
|
|
410
|
-
|
|
491
|
+
const normalized = normalizePathInput(filePath);
|
|
492
|
+
const candidate = canonicalizeCandidate(path.resolve(normalized));
|
|
493
|
+
const direct = [...this.projects.values()].find(({ resource }) =>
|
|
411
494
|
isWithin(resource.root, candidate)
|
|
412
|
-
)
|
|
495
|
+
);
|
|
496
|
+
if (direct) return direct;
|
|
497
|
+
// Same unescape fallback used in resolvePath.
|
|
498
|
+
const unescaped = unescapeShellPath(normalized);
|
|
499
|
+
if (unescaped !== normalized) {
|
|
500
|
+
const altCandidate = canonicalizeCandidate(path.resolve(unescaped));
|
|
501
|
+
return [...this.projects.values()].find(({ resource }) =>
|
|
502
|
+
isWithin(resource.root, altCandidate)
|
|
503
|
+
) || null;
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
413
506
|
}
|
|
414
507
|
|
|
415
508
|
reset() {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval prompt UI — Mission Control (PRD-055 §8.2).
|
|
3
|
+
*
|
|
4
|
+
* ┃ AWAITING APPROVAL ┃
|
|
5
|
+
* ┃ ┃
|
|
6
|
+
* ┃ ⚙️ shell "rm -rf node_modules" ┃
|
|
7
|
+
* ┃ ┃
|
|
8
|
+
* ┃ Tier: SHELL-DANGEROUS ┃
|
|
9
|
+
* ┃ Why: Recursive delete in project directory ┃
|
|
10
|
+
* ┃ ┃
|
|
11
|
+
* ┃ [Enter] approve [e] edit [r] re-plan [n] reject [?] why
|
|
12
|
+
*
|
|
13
|
+
* The border colour is `brand.accent` (magenta) for explicit-approval
|
|
14
|
+
* tiers; safe-default prompts use `brand.data` so they read as advisory.
|
|
15
|
+
*
|
|
16
|
+
* Pure — caller writes the returned string to stderr.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { paint, width as visibleWidth } from './palette.mjs';
|
|
20
|
+
import { icon } from './icons.mjs';
|
|
21
|
+
import { toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
22
|
+
import { label as tierLabel, requiresExplicitApproval, TIERS } from '../core/risk-tier.mjs';
|
|
23
|
+
|
|
24
|
+
const VBAR = '┃';
|
|
25
|
+
const PAD_X = 3;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default option set per tier. Caller can override via `opts.options`.
|
|
29
|
+
*
|
|
30
|
+
* Each option is `{ key, label, value, hint? }`:
|
|
31
|
+
* key — single-letter shortcut (case-insensitive)
|
|
32
|
+
* label — text shown in the menu
|
|
33
|
+
* value — return value from the menu loop
|
|
34
|
+
* hint — secondary description shown to the right of the label
|
|
35
|
+
*/
|
|
36
|
+
export function defaultOptions(tier) {
|
|
37
|
+
if (requiresExplicitApproval(tier)) {
|
|
38
|
+
return [
|
|
39
|
+
{ key: 'y', label: 'approve', value: 'approve', hint: 'run this once' },
|
|
40
|
+
{ key: 'e', label: 'edit', value: 'edit', hint: 'tweak the args before running' },
|
|
41
|
+
{ key: 'r', label: 're-plan', value: 'replan', hint: 'send back to the agent' },
|
|
42
|
+
{ key: 'n', label: 'reject', value: 'reject', hint: 'do not run' },
|
|
43
|
+
{ key: '?', label: 'why', value: 'why', hint: 'show reasoning' },
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
return [
|
|
47
|
+
{ key: 'y', label: 'approve', value: 'approve', hint: 'run this once' },
|
|
48
|
+
{ key: 't', label: 'always allow', value: 'allow-type', hint: 'auto-approve future calls to this tool' },
|
|
49
|
+
{ key: 'n', label: 'reject', value: 'reject', hint: 'do not run' },
|
|
50
|
+
{ key: '?', label: 'why', value: 'why', hint: 'show reasoning' },
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Render the bordered prompt with vertical, arrow-navigable options.
|
|
56
|
+
*
|
|
57
|
+
* ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
|
|
58
|
+
* ┃ AWAITING APPROVAL ┃
|
|
59
|
+
* ┃ ┃
|
|
60
|
+
* ┃ ⚙️ shell "rm -rf …" ┃
|
|
61
|
+
* ┃ ┃
|
|
62
|
+
* ┃ Tier: SHELL-DANGEROUS ┃
|
|
63
|
+
* ┃ Why: Recursive delete… ┃
|
|
64
|
+
* ┃ ┃
|
|
65
|
+
* ┃ ▸ [y] approve ┃ ← selected (accent)
|
|
66
|
+
* ┃ [e] edit ┃
|
|
67
|
+
* ┃ [r] re-plan ┃
|
|
68
|
+
* ┃ [n] reject ┃
|
|
69
|
+
* ┃ [?] why ┃
|
|
70
|
+
* ┃ ┃
|
|
71
|
+
* ┃ ↑↓ move · Enter pick ┃
|
|
72
|
+
* ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
|
|
73
|
+
*
|
|
74
|
+
* @param {object} opts
|
|
75
|
+
* @param {string} opts.tool
|
|
76
|
+
* @param {object} opts.args
|
|
77
|
+
* @param {string} opts.tier
|
|
78
|
+
* @param {string} [opts.why]
|
|
79
|
+
* @param {number} [opts.width]
|
|
80
|
+
* @param {Array} [opts.options] — override default option set
|
|
81
|
+
* @param {number} [opts.selected] — index of the highlighted option
|
|
82
|
+
*/
|
|
83
|
+
export function renderApprovalPrompt({
|
|
84
|
+
tool, args = {}, tier, why = '', width,
|
|
85
|
+
options, selected = 0,
|
|
86
|
+
} = {}) {
|
|
87
|
+
const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
|
|
88
|
+
const explicit = requiresExplicitApproval(tier);
|
|
89
|
+
const accent = explicit ? paint.brand.accent : paint.brand.data;
|
|
90
|
+
const opts = options || defaultOptions(tier);
|
|
91
|
+
|
|
92
|
+
const summary = toolDisplaySummary(tool, args, {});
|
|
93
|
+
const toolLine = `${icon(tool)} ${paint.text.primary(toolDisplayLabel(tool))} ${paint.text.muted(summary ? `"${truncate(summary, cols - 20)}"` : '')}`;
|
|
94
|
+
|
|
95
|
+
const lines = [
|
|
96
|
+
bar(cols, accent),
|
|
97
|
+
fill(' AWAITING APPROVAL', cols, accent, paint.bold(accent('AWAITING APPROVAL'))),
|
|
98
|
+
fill('', cols, accent),
|
|
99
|
+
fill(' ' + ' '.repeat(PAD_X - 1) + toolLine, cols, accent),
|
|
100
|
+
fill('', cols, accent),
|
|
101
|
+
fill(' ' + ' '.repeat(PAD_X - 1) + paint.text.dim('Tier: ') + accent(tierLabel(tier)), cols, accent),
|
|
102
|
+
];
|
|
103
|
+
if (why) {
|
|
104
|
+
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) + paint.text.dim('Why: ') + paint.text.primary(truncate(why, cols - PAD_X - 12)), cols, accent));
|
|
105
|
+
}
|
|
106
|
+
lines.push(fill('', cols, accent));
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < opts.length; i++) {
|
|
109
|
+
const o = opts[i];
|
|
110
|
+
const isSel = i === selected;
|
|
111
|
+
const cursor = isSel ? accent('▸ ') : paint.text.dim(' ');
|
|
112
|
+
const keyTag = paint.text.dim('[') + (isSel ? accent(o.key) : paint.brand.data(o.key)) + paint.text.dim('] ');
|
|
113
|
+
const label = isSel ? paint.bold(accent(o.label)) : paint.text.primary(o.label);
|
|
114
|
+
const hint = o.hint ? ' ' + paint.text.muted(o.hint) : '';
|
|
115
|
+
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) + cursor + keyTag + label + hint, cols, accent));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
lines.push(fill('', cols, accent));
|
|
119
|
+
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) +
|
|
120
|
+
paint.text.dim('↑↓ ') + paint.brand.data('move') +
|
|
121
|
+
paint.text.dim(' · Enter ') + paint.brand.data('pick') +
|
|
122
|
+
paint.text.dim(' · or press a letter'), cols, accent));
|
|
123
|
+
lines.push(bar(cols, accent));
|
|
124
|
+
|
|
125
|
+
return '\n' + lines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function bar(width, painter) {
|
|
129
|
+
// Top / bottom rule: a magenta vertical-stack line spanning the full width.
|
|
130
|
+
return painter('▔'.repeat(width));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function fill(text, width, painter, override) {
|
|
134
|
+
const visible = override ?? text;
|
|
135
|
+
const pad = Math.max(0, width - 2 - visibleWidth(visible));
|
|
136
|
+
return painter(VBAR) + visible + ' '.repeat(pad) + painter(VBAR);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function truncate(text, n) {
|
|
140
|
+
const s = String(text || '');
|
|
141
|
+
if (s.length <= n) return s;
|
|
142
|
+
return s.slice(0, Math.max(0, n - 1)) + '…';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Inline (safe-default) prompt for shell-medium / network ────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Render the compact one-line prompt used for safe-default tiers.
|
|
149
|
+
* The bordered block is reserved for explicit-approval tiers.
|
|
150
|
+
*
|
|
151
|
+
* ? Run `npm install lodash`? Tier: SHELL-MEDIUM [Enter=yes n=no ?=why]
|
|
152
|
+
*/
|
|
153
|
+
export function renderInlinePrompt({ tool, args = {}, tier, why = '' } = {}) {
|
|
154
|
+
const summary = toolDisplaySummary(tool, args, {});
|
|
155
|
+
const label = toolDisplayLabel(tool);
|
|
156
|
+
const lines = [];
|
|
157
|
+
const head = `${paint.brand.data('?')} ${paint.text.primary(label)} ${paint.text.muted(summary ? `"${truncate(summary, 80)}"` : '')}`;
|
|
158
|
+
lines.push(' ' + head);
|
|
159
|
+
lines.push(' ' + paint.text.dim('Tier ') + paint.brand.data(tierLabel(tier)) +
|
|
160
|
+
(why ? ' ' + paint.text.dim('Why ') + paint.text.muted(truncate(why, 60)) : ''));
|
|
161
|
+
lines.push(' ' + paint.text.dim('[') + paint.brand.data('Enter') + paint.text.dim('=yes ') +
|
|
162
|
+
paint.brand.data('n') + paint.text.dim('=no ') +
|
|
163
|
+
paint.brand.data('?') + paint.text.dim('=why]'));
|
|
164
|
+
return '\n' + lines.join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { TIERS };
|
package/src/ui/banner.mjs
CHANGED
|
@@ -1,162 +1,173 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Banner & Branding — Kepler CLI startup display.
|
|
3
|
+
*
|
|
4
|
+
* Refreshed for Mission Control (PRD-055 §4.3). Uses the semantic palette
|
|
5
|
+
* (`paint.brand.*`) so the same banner renders correctly across truecolor,
|
|
6
|
+
* 256-color, ansi16, and monochrome terminals.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
9
|
import { execSync } from 'node:child_process';
|
|
6
10
|
import * as path from 'node:path';
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
12
|
+
import { icons } from './icons.mjs';
|
|
13
|
+
import { paint, strip } from './palette.mjs';
|
|
14
|
+
import { term } from './term.mjs';
|
|
15
|
+
|
|
16
|
+
const out = process.stderr;
|
|
17
|
+
const write = (s) => { try { out.write(s); } catch {} };
|
|
18
|
+
|
|
19
|
+
// ── Brand banner ─────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const KEPLER_LETTERS = ['K', 'E', 'P', 'L', 'E', 'R'];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render `KEPLER` letter-by-letter as a purple→magenta→cyan gradient.
|
|
25
|
+
* Each letter picks the appropriate brand token; the palette handles tier
|
|
26
|
+
* fallbacks transparently.
|
|
27
|
+
*
|
|
28
|
+
* Falls back to a single solid color in monochrome / ascii mode so we
|
|
29
|
+
* still see something distinctive on hostile terminals.
|
|
30
|
+
*/
|
|
31
|
+
function gradientKepler() {
|
|
32
|
+
if (!term().color) return KEPLER_LETTERS.join(' · ');
|
|
33
|
+
|
|
34
|
+
// Three-stop gradient mapped onto six letters. Stop selection:
|
|
35
|
+
// 0,1 → primary 2,3 → accent 4,5 → data
|
|
36
|
+
const painters = [
|
|
37
|
+
paint.brand.primary, paint.brand.primary,
|
|
38
|
+
paint.brand.accent, paint.brand.accent,
|
|
39
|
+
paint.brand.data, paint.brand.data,
|
|
40
|
+
];
|
|
41
|
+
return KEPLER_LETTERS.map((ch, i) => painters[i](ch)).join(paint.text.dim(' · '));
|
|
42
|
+
}
|
|
19
43
|
|
|
20
44
|
/**
|
|
21
45
|
* Print the branded orbital banner.
|
|
22
46
|
*/
|
|
23
47
|
export function printBanner() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
const dim = paint.text.dim;
|
|
49
|
+
const brandMark = paint.brand.primary(icons.brand);
|
|
50
|
+
const orbit = paint.brand.accent(icons.orbit);
|
|
51
|
+
|
|
52
|
+
write('\n');
|
|
53
|
+
write(` ${brandMark}\n`);
|
|
54
|
+
write(` ${dim('╭───────────────────────────╮')}\n`);
|
|
55
|
+
write(` ${dim('│')} ${gradientKepler()} ${dim('│')}\n`);
|
|
56
|
+
write(` ${dim('╰────── ')}${orbit}${dim(' ─────────────────╯')}\n`);
|
|
57
|
+
write(` ${dim('╱ ╲')}\n`);
|
|
58
|
+
write(` ${dim('the agentic os')}\n`);
|
|
59
|
+
write('\n');
|
|
32
60
|
}
|
|
33
61
|
|
|
62
|
+
// ── Project info bar ─────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const BAR_WIDTH = 60;
|
|
65
|
+
|
|
34
66
|
/**
|
|
35
67
|
* Print project info bar with version, project name, and git info.
|
|
36
|
-
* @param {string} version
|
|
37
68
|
*/
|
|
38
69
|
export function printProjectInfo(version) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
process.stderr.write(`${BLUE}│${RESET} ${info}${' '.repeat(Math.max(0, barWidth - stripAnsi(info).length - 1))}${BLUE}│${RESET}\n`);
|
|
56
|
-
process.stderr.write(`${bottomBorder}\n`);
|
|
70
|
+
const cwd = process.cwd();
|
|
71
|
+
const projectName = path.basename(cwd);
|
|
72
|
+
const gitInfo = getGitInfo(cwd);
|
|
73
|
+
|
|
74
|
+
const sep = paint.text.dim(' │ ');
|
|
75
|
+
let info = ` ${paint.text.dim('v' + version)}${sep}${paint.bold(projectName)}`;
|
|
76
|
+
if (gitInfo) {
|
|
77
|
+
info += `${sep}${paint.state.warn(gitInfo)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const padCount = Math.max(0, BAR_WIDTH - strip(info).length - 1);
|
|
81
|
+
const border = paint.brand.primary;
|
|
82
|
+
|
|
83
|
+
write(`${border('┌' + '─'.repeat(BAR_WIDTH) + '┐')}\n`);
|
|
84
|
+
write(`${border('│')} ${info}${' '.repeat(padCount)}${border('│')}\n`);
|
|
85
|
+
write(`${border('└' + '─'.repeat(BAR_WIDTH) + '┘')}\n`);
|
|
57
86
|
}
|
|
58
87
|
|
|
59
|
-
|
|
60
|
-
* Print keyboard hints and usage tips.
|
|
61
|
-
*/
|
|
62
|
-
export function printHints() {
|
|
63
|
-
const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
|
|
88
|
+
// ── Hints ────────────────────────────────────────────────────────────────
|
|
64
89
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
90
|
+
export function printHints() {
|
|
91
|
+
const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
|
|
92
|
+
const dim = paint.text.dim;
|
|
93
|
+
const accent = paint.brand.data;
|
|
94
|
+
|
|
95
|
+
write(`${paint.state.success('Type your instructions')}, or ${accent('/help')} for commands\n`);
|
|
96
|
+
write(`${dim('Ctrl+C')}${dim('=exit ')}${dim('/clear')}${dim('=reset ')}${dim('/config')}${dim('=settings ')}${dim('/login')}${dim('=auth')}\n`);
|
|
97
|
+
write(`${dim('env:' + env + ' models:configured via browser (/config)')}\n`);
|
|
98
|
+
write('\n');
|
|
69
99
|
}
|
|
70
100
|
|
|
71
|
-
|
|
72
|
-
* Print auth status indicators.
|
|
73
|
-
* @param {object} creds - { token, openRouterKey, anthropicKey, backendUrl, mode }
|
|
74
|
-
*/
|
|
75
|
-
export function printAuthStatus(creds) {
|
|
76
|
-
const check = `${GREEN}✓${RESET}`;
|
|
77
|
-
const cross = `${RED}✗${RESET}`;
|
|
101
|
+
// ── Auth + config ────────────────────────────────────────────────────────
|
|
78
102
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
export function printAuthStatus(creds) {
|
|
104
|
+
const check = paint.state.success(icons.pass);
|
|
105
|
+
const cross = paint.state.danger(icons.fail);
|
|
106
|
+
const dim = paint.text.dim;
|
|
107
|
+
|
|
108
|
+
const tokenOk = !!creds.token;
|
|
109
|
+
const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
|
|
110
|
+
|
|
111
|
+
write(` Auth: ${tokenOk
|
|
112
|
+
? `${check} logged in ${dim('(/whoami for details)')}`
|
|
113
|
+
: `${cross} not logged in ${dim('(/login)')}`}\n`);
|
|
114
|
+
write(` Env: ${dim(env)}\n`);
|
|
115
|
+
write(` Mode: ${dim(creds.mode || 'auto')}\n`);
|
|
116
|
+
write('\n');
|
|
86
117
|
}
|
|
87
118
|
|
|
88
|
-
/**
|
|
89
|
-
* Print config in a styled format.
|
|
90
|
-
* @param {object} creds
|
|
91
|
-
*/
|
|
92
119
|
export function printStyledConfig(creds) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
const check = paint.state.success(icons.pass);
|
|
121
|
+
const cross = paint.state.danger(icons.fail);
|
|
122
|
+
const dim = paint.text.dim;
|
|
123
|
+
|
|
124
|
+
const mask = (val) => {
|
|
125
|
+
if (!val) return `${cross} not set`;
|
|
126
|
+
if (val.length <= 8) return `${check} ****`;
|
|
127
|
+
return `${check} ${val.slice(0, 6)}...${val.slice(-4)}`;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
|
|
131
|
+
|
|
132
|
+
write(`\n${paint.bold('Kepler Configuration')} ${dim('(~/.kepler/config.json)')}\n`);
|
|
133
|
+
write(`${dim('─'.repeat(50))}\n`);
|
|
134
|
+
write(` Token: ${mask(creds.token)}\n`);
|
|
135
|
+
write(` OpenRouter: ${mask(creds.openRouterKey)}\n`);
|
|
136
|
+
write(` Anthropic: ${mask(creds.anthropicKey)}\n`);
|
|
137
|
+
write(` Environment: ${dim(env)}\n`);
|
|
138
|
+
write(` Backend URL: ${dim(creds.backendUrl)}\n`);
|
|
139
|
+
write(` Mode: ${dim(creds.mode || 'auto')}\n`);
|
|
140
|
+
write('\n');
|
|
111
141
|
}
|
|
112
142
|
|
|
113
|
-
/**
|
|
114
|
-
* Print a goodbye message.
|
|
115
|
-
*/
|
|
116
143
|
export function printGoodbye() {
|
|
117
|
-
|
|
144
|
+
write(`\n${paint.bold(paint.brand.primary('Goodbye!'))}\n\n`);
|
|
118
145
|
}
|
|
119
146
|
|
|
120
|
-
|
|
121
|
-
* Get git branch and change count for current directory.
|
|
122
|
-
* @param {string} cwd
|
|
123
|
-
* @returns {string|null}
|
|
124
|
-
*/
|
|
125
|
-
function getGitInfo(cwd) {
|
|
126
|
-
try {
|
|
127
|
-
const branch = execSync('git branch --show-current', {
|
|
128
|
-
cwd, encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
129
|
-
}).trim();
|
|
130
|
-
|
|
131
|
-
if (!branch) return null;
|
|
132
|
-
|
|
133
|
-
const status = execSync('git status --porcelain', {
|
|
134
|
-
cwd, encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
135
|
-
}).trim();
|
|
147
|
+
// ── Git probe ────────────────────────────────────────────────────────────
|
|
136
148
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
function getGitInfo(cwd) {
|
|
150
|
+
try {
|
|
151
|
+
const branch = execSync('git branch --show-current', {
|
|
152
|
+
cwd, encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
153
|
+
}).trim();
|
|
154
|
+
if (!branch) return null;
|
|
155
|
+
|
|
156
|
+
const status = execSync('git status --porcelain', {
|
|
157
|
+
cwd, encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
158
|
+
}).trim();
|
|
159
|
+
const changes = status ? status.split('\n').filter(Boolean).length : 0;
|
|
160
|
+
|
|
161
|
+
return changes > 0 ? `⎇ ${branch} (${changes} changed)` : `⎇ ${branch}`;
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
146
165
|
}
|
|
147
166
|
|
|
148
|
-
|
|
149
|
-
* Strip ANSI escape codes for length calculation.
|
|
150
|
-
*/
|
|
151
|
-
function stripAnsi(str) {
|
|
152
|
-
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
153
|
-
}
|
|
167
|
+
// ── OAuth success page (unchanged from prior version) ────────────────────
|
|
154
168
|
|
|
155
|
-
/**
|
|
156
|
-
* Branded HTML for OAuth success page.
|
|
157
|
-
*/
|
|
158
169
|
export function getLoginSuccessHTML() {
|
|
159
|
-
|
|
170
|
+
return `<!DOCTYPE html>
|
|
160
171
|
<html>
|
|
161
172
|
<head>
|
|
162
173
|
<meta charset="utf-8">
|