@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.
- package/KEPLER-README.md +128 -2
- package/package.json +4 -4
- package/pulse/app/activity/page.tsx +1 -1
- package/pulse/app/api/import/route.ts +1 -1
- package/pulse/app/api/memory/route.ts +2 -2
- package/pulse/app/costs/page.tsx +1 -1
- package/pulse/app/export/page.tsx +3 -3
- package/pulse/app/globals.css +3 -3
- package/pulse/app/help/page.tsx +11 -11
- package/pulse/app/history/page.tsx +2 -2
- package/pulse/app/layout.tsx +2 -2
- package/pulse/app/memory/page.tsx +2 -2
- package/pulse/app/overview-client.tsx +1 -1
- package/pulse/app/page.tsx +2 -2
- package/pulse/app/plans/page.tsx +2 -2
- package/pulse/app/projects/page.tsx +1 -1
- package/pulse/app/sessions/page.tsx +1 -1
- package/pulse/app/settings/page.tsx +4 -4
- package/pulse/app/todos/page.tsx +2 -2
- package/pulse/app/tools/page.tsx +1 -1
- package/pulse/cli.js +15 -25
- package/pulse/components/layout/sidebar.tsx +2 -2
- package/pulse/components/sessions/replay/user-tool-result.tsx +1 -1
- package/pulse/lib/claude-reader.ts +1 -1
- package/pulse/lib/decode.ts +1 -1
- package/pulse/package.json +3 -3
- package/src/auth/tarang-auth.mjs +1 -1
- package/src/config/cli-args.mjs +5 -0
- package/src/context/retriever.mjs +1 -1
- package/src/context/skeleton.mjs +1 -1
- package/src/core/approval.mjs +22 -53
- package/src/core/headless.mjs +68 -24
- package/src/core/paths.mjs +1 -1
- package/src/core/project-artifacts.mjs +37 -0
- package/src/core/stream-client.mjs +6 -1
- package/src/core/tool-executor.mjs +163 -55
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +217 -112
- package/src/terminal/main.mjs +19 -1
- package/src/terminal/repl.mjs +40 -105
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +82 -0
- package/src/tools/bash.mjs +5 -2
- package/src/tools/project-overview.mjs +418 -0
- package/src/tools/registry.mjs +0 -16
- package/src/ui/banner.mjs +7 -14
- package/src/ui/formatter.mjs +6 -40
- package/README.md.orca +0 -82
package/src/skills/loader.mjs
CHANGED
|
@@ -1,147 +1,252 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Portable Agent Skills loader.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - The prompt to inject when the skill is invoked
|
|
4
|
+
* Discovers standard <root>/<skill>/SKILL.md bundles from Kepler and Claude
|
|
5
|
+
* locations. Only metadata is exposed eagerly; instructions and resources are
|
|
6
|
+
* loaded on demand through view().
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
import
|
|
11
|
-
import
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MAX_CHARS = 12_000;
|
|
15
|
+
|
|
16
|
+
function isWithin(root, candidate) {
|
|
17
|
+
const relative = path.relative(root, candidate);
|
|
18
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseScalar(value) {
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
if (
|
|
24
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"'))
|
|
25
|
+
|| (trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
26
|
+
) {
|
|
27
|
+
return trimmed.slice(1, -1);
|
|
28
|
+
}
|
|
29
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
30
|
+
return trimmed.slice(1, -1).split(',').map(item => parseScalar(item)).filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
if (trimmed === 'true') return true;
|
|
33
|
+
if (trimmed === 'false') return false;
|
|
34
|
+
return trimmed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseSkill(content, fallbackName) {
|
|
38
|
+
const match = content.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?/);
|
|
39
|
+
if (!match) throw new Error('SKILL.md requires YAML frontmatter');
|
|
40
|
+
|
|
41
|
+
const metadata = {};
|
|
42
|
+
let activeList = null;
|
|
43
|
+
for (const rawLine of match[1].split(/\r?\n/)) {
|
|
44
|
+
if (!rawLine.trim() || rawLine.trimStart().startsWith('#')) continue;
|
|
45
|
+
const listMatch = rawLine.match(/^\s*-\s+(.+)$/);
|
|
46
|
+
if (listMatch && activeList) {
|
|
47
|
+
metadata[activeList].push(parseScalar(listMatch[1]));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const fieldMatch = rawLine.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
51
|
+
if (!fieldMatch) continue;
|
|
52
|
+
const [, key, rawValue] = fieldMatch;
|
|
53
|
+
if (!rawValue.trim()) {
|
|
54
|
+
metadata[key] = [];
|
|
55
|
+
activeList = key;
|
|
56
|
+
} else {
|
|
57
|
+
metadata[key] = parseScalar(rawValue);
|
|
58
|
+
activeList = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const name = String(metadata.name || fallbackName || '').trim();
|
|
63
|
+
const description = String(metadata.description || '').trim();
|
|
64
|
+
const instructions = content.slice(match[0].length).trim();
|
|
65
|
+
if (!name) throw new Error("SKILL.md requires non-empty 'name'");
|
|
66
|
+
if (!description) throw new Error("SKILL.md requires non-empty 'description'");
|
|
67
|
+
if (!instructions) throw new Error('SKILL.md requires instruction content');
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
name,
|
|
71
|
+
description,
|
|
72
|
+
aliases: Array.isArray(metadata.aliases) ? metadata.aliases : [],
|
|
73
|
+
compatibility: Array.isArray(metadata.compatibility) ? metadata.compatibility : [],
|
|
74
|
+
metadata,
|
|
75
|
+
instructions,
|
|
76
|
+
prompt: instructions,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function discoverSkillDirectories(root) {
|
|
81
|
+
if (!root || !fs.existsSync(root)) return [];
|
|
82
|
+
const resolved = fs.realpathSync(root);
|
|
83
|
+
if (fs.existsSync(path.join(resolved, 'SKILL.md'))) return [resolved];
|
|
84
|
+
return fs.readdirSync(resolved, { withFileTypes: true })
|
|
85
|
+
.filter(entry => entry.isDirectory() && !entry.isSymbolicLink())
|
|
86
|
+
.map(entry => path.join(resolved, entry.name))
|
|
87
|
+
.filter(dir => fs.existsSync(path.join(dir, 'SKILL.md')))
|
|
88
|
+
.sort();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function walkFiles(root) {
|
|
92
|
+
const files = [];
|
|
93
|
+
const queue = [root];
|
|
94
|
+
while (queue.length) {
|
|
95
|
+
const current = queue.shift();
|
|
96
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
97
|
+
const fullPath = path.join(current, entry.name);
|
|
98
|
+
if (entry.isSymbolicLink()) continue;
|
|
99
|
+
if (entry.isDirectory()) queue.push(fullPath);
|
|
100
|
+
else if (entry.isFile()) files.push(fullPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function contentHash(root) {
|
|
107
|
+
const digest = crypto.createHash('sha256');
|
|
108
|
+
for (const filePath of walkFiles(root)) {
|
|
109
|
+
digest.update(path.relative(root, filePath).split(path.sep).join('/'));
|
|
110
|
+
digest.update('\0');
|
|
111
|
+
digest.update(fs.readFileSync(filePath));
|
|
112
|
+
digest.update('\0');
|
|
113
|
+
}
|
|
114
|
+
return `sha256:${digest.digest('hex')}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function bounded(content, maxChars) {
|
|
118
|
+
if (content.length <= maxChars) return content;
|
|
119
|
+
const head = Math.floor(maxChars * 0.7);
|
|
120
|
+
const tail = maxChars - head;
|
|
121
|
+
return `${content.slice(0, head)}\n\n[... skill content truncated ...]\n\n${content.slice(-tail)}`;
|
|
122
|
+
}
|
|
12
123
|
|
|
13
124
|
export class SkillsLoader {
|
|
14
|
-
constructor() {
|
|
125
|
+
constructor({ homeDir = os.homedir() } = {}) {
|
|
126
|
+
this.homeDir = homeDir;
|
|
15
127
|
this.skills = new Map();
|
|
128
|
+
this.versions = new Map();
|
|
16
129
|
this.searchPaths = [];
|
|
17
130
|
}
|
|
18
131
|
|
|
19
|
-
/**
|
|
20
|
-
* Load skills from standard directories.
|
|
21
|
-
* @param {string} [cwd] - project working directory
|
|
22
|
-
*/
|
|
23
132
|
load(cwd = process.cwd()) {
|
|
133
|
+
this.skills.clear();
|
|
134
|
+
this.versions.clear();
|
|
135
|
+
const project = path.resolve(cwd);
|
|
24
136
|
this.searchPaths = [
|
|
25
|
-
path.join(
|
|
26
|
-
path.join(
|
|
137
|
+
{ dir: path.join(project, '.kepler', 'skills'), source: 'kepler-project', scope: 'project', priority: 500 },
|
|
138
|
+
{ dir: path.join(project, '.claude', 'skills'), source: 'claude-project', scope: 'project', priority: 400 },
|
|
139
|
+
{ dir: path.join(this.homeDir, '.kepler', 'skills'), source: 'kepler-global', scope: 'global', priority: 300 },
|
|
140
|
+
{ dir: path.join(this.homeDir, '.claude', 'skills'), source: 'claude-global', scope: 'global', priority: 200 },
|
|
27
141
|
];
|
|
28
142
|
|
|
29
|
-
for (const
|
|
30
|
-
this._loadFromDir(dir);
|
|
31
|
-
}
|
|
32
|
-
|
|
143
|
+
for (const config of this.searchPaths) this._loadFromDir(config);
|
|
33
144
|
return this;
|
|
34
145
|
}
|
|
35
146
|
|
|
36
|
-
_loadFromDir(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
147
|
+
_loadFromDir(config) {
|
|
148
|
+
for (const skillDir of discoverSkillDirectories(config.dir)) {
|
|
149
|
+
try {
|
|
150
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
151
|
+
if (fs.lstatSync(skillFile).isSymbolicLink()) continue;
|
|
152
|
+
const parsed = parseSkill(fs.readFileSync(skillFile, 'utf-8'), path.basename(skillDir));
|
|
153
|
+
const skill = {
|
|
154
|
+
...parsed,
|
|
155
|
+
root: skillDir,
|
|
156
|
+
source: config.source,
|
|
157
|
+
source_id: `${config.source}:${parsed.name}`,
|
|
158
|
+
scope: config.scope,
|
|
159
|
+
priority: config.priority,
|
|
160
|
+
content_hash: contentHash(skillDir),
|
|
161
|
+
};
|
|
162
|
+
const versions = this.versions.get(skill.name) || [];
|
|
163
|
+
versions.push(skill);
|
|
164
|
+
versions.sort((a, b) => b.priority - a.priority || a.source.localeCompare(b.source));
|
|
165
|
+
this.versions.set(skill.name, versions);
|
|
166
|
+
this.skills.set(skill.name, versions[0]);
|
|
167
|
+
} catch {
|
|
168
|
+
// Invalid third-party bundles are ignored during discovery.
|
|
53
169
|
}
|
|
54
|
-
} catch {
|
|
55
|
-
// Directory does not exist
|
|
56
170
|
}
|
|
57
171
|
}
|
|
58
172
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*/
|
|
64
|
-
get(name) {
|
|
65
|
-
// Try exact match, then prefix match
|
|
173
|
+
get(name, sourceId = null) {
|
|
174
|
+
if (sourceId) {
|
|
175
|
+
return (this.versions.get(name) || []).find(skill => skill.source_id === sourceId) || null;
|
|
176
|
+
}
|
|
66
177
|
if (this.skills.has(name)) return this.skills.get(name);
|
|
67
|
-
for (const
|
|
68
|
-
if (
|
|
69
|
-
return skill;
|
|
70
|
-
}
|
|
178
|
+
for (const skill of this.skills.values()) {
|
|
179
|
+
if (skill.name.startsWith(name) || skill.aliases.includes(name)) return skill;
|
|
71
180
|
}
|
|
72
181
|
return null;
|
|
73
182
|
}
|
|
74
183
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
184
|
+
list({ query = '', source = '', scope = '' } = {}) {
|
|
185
|
+
const needle = query.toLowerCase();
|
|
186
|
+
return [...this.skills.values()]
|
|
187
|
+
.filter(skill => !source || skill.source === source)
|
|
188
|
+
.filter(skill => !scope || skill.scope === scope)
|
|
189
|
+
.filter(skill => !needle || `${skill.name} ${skill.description}`.toLowerCase().includes(needle))
|
|
190
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
191
|
+
.map(skill => ({
|
|
192
|
+
name: skill.name,
|
|
193
|
+
description: skill.description,
|
|
194
|
+
source: skill.source,
|
|
195
|
+
source_id: skill.source_id,
|
|
196
|
+
scope: skill.scope,
|
|
197
|
+
content_hash: skill.content_hash,
|
|
198
|
+
compatibility: skill.compatibility,
|
|
199
|
+
shadowed_sources: (this.versions.get(skill.name) || []).slice(1).map(item => item.source_id),
|
|
200
|
+
}));
|
|
81
201
|
}
|
|
82
202
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
203
|
+
view(name, resourcePath = null, { sourceId = null, maxChars = DEFAULT_MAX_CHARS } = {}) {
|
|
204
|
+
const skill = this.get(name, sourceId);
|
|
205
|
+
if (!skill) throw new Error(`Unknown skill: ${name}`);
|
|
206
|
+
if (!resourcePath) {
|
|
207
|
+
return {
|
|
208
|
+
name: skill.name,
|
|
209
|
+
description: skill.description,
|
|
210
|
+
source: skill.source,
|
|
211
|
+
source_id: skill.source_id,
|
|
212
|
+
scope: skill.scope,
|
|
213
|
+
content_hash: skill.content_hash,
|
|
214
|
+
compatibility: skill.compatibility,
|
|
215
|
+
shadowed_sources: (this.versions.get(skill.name) || [])
|
|
216
|
+
.filter(item => item.source_id !== skill.source_id)
|
|
217
|
+
.map(item => item.source_id),
|
|
218
|
+
instructions: bounded(skill.instructions, maxChars),
|
|
219
|
+
resources: walkFiles(skill.root)
|
|
220
|
+
.map(file => path.relative(skill.root, file).split(path.sep).join('/'))
|
|
221
|
+
.filter(file => file !== 'SKILL.md'),
|
|
222
|
+
metadata: skill.metadata,
|
|
223
|
+
};
|
|
99
224
|
}
|
|
100
225
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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();
|
|
226
|
+
if (path.isAbsolute(resourcePath)) throw new Error('Skill resource path must be relative');
|
|
227
|
+
const candidate = path.resolve(skill.root, resourcePath);
|
|
228
|
+
if (!isWithin(skill.root, candidate)) throw new Error('Skill resource escapes its bundle');
|
|
229
|
+
const relativeParts = path.relative(skill.root, candidate).split(path.sep);
|
|
230
|
+
let cursor = skill.root;
|
|
231
|
+
for (const part of relativeParts) {
|
|
232
|
+
cursor = path.join(cursor, part);
|
|
233
|
+
if (!fs.existsSync(cursor)) throw new Error(`Skill resource not found: ${resourcePath}`);
|
|
234
|
+
if (fs.lstatSync(cursor).isSymbolicLink()) throw new Error('Symlinked skill resources are not allowed');
|
|
137
235
|
}
|
|
236
|
+
if (!fs.statSync(candidate).isFile()) throw new Error(`Skill resource not found: ${resourcePath}`);
|
|
237
|
+
return {
|
|
238
|
+
name: skill.name,
|
|
239
|
+
source_id: skill.source_id,
|
|
240
|
+
path: resourcePath,
|
|
241
|
+
content: bounded(fs.readFileSync(candidate, 'utf-8'), maxChars),
|
|
242
|
+
};
|
|
138
243
|
}
|
|
139
244
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
245
|
+
async run(name, args) {
|
|
246
|
+
const skill = this.get(name);
|
|
247
|
+
if (!skill) throw new Error(`Unknown skill: ${name}`);
|
|
248
|
+
let prompt = skill.instructions;
|
|
249
|
+
if (args) prompt = prompt.replace(/\$ARGUMENTS/g, args);
|
|
250
|
+
return `[Skill: ${skill.name}]\n${prompt}${args ? `\n\nArguments: ${args}` : ''}`;
|
|
144
251
|
}
|
|
145
|
-
|
|
146
|
-
return skill;
|
|
147
252
|
}
|
package/src/terminal/main.mjs
CHANGED
|
@@ -47,6 +47,17 @@ async function main() {
|
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
if (subcommand === 'skills' || subcommand === 'skill') {
|
|
51
|
+
const { runSkillsCommand } = await import('./skills.mjs');
|
|
52
|
+
try {
|
|
53
|
+
await runSkillsCommand(subcommandArgs);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
process.stderr.write(`\x1b[31m✗ Skills command failed: ${err.message}\x1b[0m\n`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
if (subcommand === 'login') {
|
|
51
62
|
const { TarangAuth } = await import('../auth/tarang-auth.mjs');
|
|
52
63
|
const auth = new TarangAuth();
|
|
@@ -99,6 +110,13 @@ async function main() {
|
|
|
99
110
|
kepler stats Show aggregate local session stats
|
|
100
111
|
kepler history Show recent prompt history
|
|
101
112
|
|
|
113
|
+
\x1b[1mSkills:\x1b[0m
|
|
114
|
+
kepler skills list [--all|--project]
|
|
115
|
+
kepler skills view <name> [resource]
|
|
116
|
+
kepler skills install <path-or-git-url> [--project] [--force]
|
|
117
|
+
kepler skills update <name> [--project]
|
|
118
|
+
kepler skills remove <name> [--project]
|
|
119
|
+
|
|
102
120
|
\x1b[1mREPL Commands:\x1b[0m
|
|
103
121
|
/help Show available commands
|
|
104
122
|
/stats Session metrics (tokens, cost, tools)
|
|
@@ -135,7 +153,7 @@ async function main() {
|
|
|
135
153
|
await runHeadless({
|
|
136
154
|
instruction: args.prompt,
|
|
137
155
|
model: args.model,
|
|
138
|
-
timeout: args.maxTurns ? args.maxTurns * 60 :
|
|
156
|
+
timeout: args.timeout || (args.maxTurns ? args.maxTurns * 60 : 600),
|
|
139
157
|
verbose: args.verbose,
|
|
140
158
|
});
|
|
141
159
|
return;
|
package/src/terminal/repl.mjs
CHANGED
|
@@ -17,20 +17,20 @@
|
|
|
17
17
|
|
|
18
18
|
import * as readline from 'node:readline';
|
|
19
19
|
import * as path from 'node:path';
|
|
20
|
+
import * as fs from 'node:fs';
|
|
20
21
|
import { c, progressBar, spinner, inPlace, renderMarkdown, renderDiff, formatElapsed, formatCost, stripAnsi } from './ansi.mjs';
|
|
21
22
|
import { calculateCost, formatCostValue, formatTokens } from '../core/pricing.mjs';
|
|
22
23
|
import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
|
|
23
24
|
import { JsonlWriter } from '../core/jsonl-writer.mjs';
|
|
24
25
|
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
26
|
+
import { persistProjectArtifacts } from '../core/project-artifacts.mjs';
|
|
25
27
|
import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
26
28
|
import { ApprovalManager } from '../core/approval.mjs';
|
|
27
29
|
import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
28
30
|
import { BUILTIN_AGENTS, runAgent } from './agents.mjs';
|
|
29
|
-
import { ContextRetriever } from '../context/retriever.mjs';
|
|
30
|
-
import { buildProjectSkeleton } from '../context/skeleton.mjs';
|
|
31
31
|
import { SessionManager } from '../core/session-manager.mjs';
|
|
32
32
|
import { parseArgs } from '../config/cli-args.mjs';
|
|
33
|
-
import { formatShellCommand, toolDisplayLabel } from './tool-display.mjs';
|
|
33
|
+
import { formatShellCommand, toolDisplayLabel, toolDisplaySummary } from './tool-display.mjs';
|
|
34
34
|
|
|
35
35
|
import { createRequire } from 'node:module';
|
|
36
36
|
const __require = createRequire(import.meta.url);
|
|
@@ -120,18 +120,19 @@ function printBanner(auth) {
|
|
|
120
120
|
const env = process.env.TARANG_ENV || 'production';
|
|
121
121
|
const authStatus = creds.token ? c.green('authenticated') : c.red('/login to start');
|
|
122
122
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
' ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝',
|
|
130
|
-
];
|
|
123
|
+
const CYAN = '\x1b[36m';
|
|
124
|
+
const DIM = '\x1b[2m';
|
|
125
|
+
const BOLD = '\x1b[1m';
|
|
126
|
+
const YELLOW = '\x1b[33m';
|
|
127
|
+
const RST = '\x1b[0m';
|
|
128
|
+
|
|
131
129
|
process.stderr.write('\n');
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
130
|
+
process.stderr.write(`${DIM} ✦${RST}\n`);
|
|
131
|
+
process.stderr.write(`${DIM} ╭──────────────────────────╮${RST}\n`);
|
|
132
|
+
process.stderr.write(`${DIM} │${RST} ${BOLD}${CYAN}K · E · P · L · E · R${RST} ${DIM}│${RST}\n`);
|
|
133
|
+
process.stderr.write(`${DIM} ╰──────── ${YELLOW}◯${RST}${DIM} ───────────────╯${RST}\n`);
|
|
134
|
+
process.stderr.write(`${DIM} ╱ ╲${RST}\n`);
|
|
135
|
+
process.stderr.write(`${DIM} the agentic os${RST}\n`);
|
|
135
136
|
process.stderr.write('\n');
|
|
136
137
|
process.stderr.write(` ${c.gray('v' + VERSION)} ${c.dim(env)} ${authStatus}\n`);
|
|
137
138
|
process.stderr.write('\n');
|
|
@@ -221,52 +222,7 @@ function renderToolCall(data) {
|
|
|
221
222
|
const args = data?.args || {};
|
|
222
223
|
const indent = session.inSubAgent ? ' ' : ' ';
|
|
223
224
|
|
|
224
|
-
|
|
225
|
-
let summary;
|
|
226
|
-
switch (tool) {
|
|
227
|
-
case 'read_file': {
|
|
228
|
-
const fp = shortPath(args.file_path || args.path || '');
|
|
229
|
-
const range = args.start_line && args.end_line
|
|
230
|
-
? ` lines ${args.start_line}-${args.end_line}`
|
|
231
|
-
: args.start_line ? ` from line ${args.start_line}` : '';
|
|
232
|
-
summary = `${fp}${range}`;
|
|
233
|
-
break;
|
|
234
|
-
}
|
|
235
|
-
case 'write_file': {
|
|
236
|
-
const fp = shortPath(args.file_path || args.path || '');
|
|
237
|
-
const lines = args.content ? `, ${args.content.split('\n').length} lines` : '';
|
|
238
|
-
summary = `${fp}${lines}`;
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
case 'edit_file': {
|
|
242
|
-
const fp = shortPath(args.file_path || args.path || '');
|
|
243
|
-
const search = args.search ? `, "${(args.search || '').slice(0, 30)}..."` : '';
|
|
244
|
-
summary = `${fp}${search}`;
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
case 'shell':
|
|
248
|
-
summary = args.command || '';
|
|
249
|
-
break;
|
|
250
|
-
case 'search_code':
|
|
251
|
-
summary = `"${args.query || args.pattern || ''}"`;
|
|
252
|
-
break;
|
|
253
|
-
case 'list_files':
|
|
254
|
-
summary = `${args.pattern || '*'}${args.path ? ` in ${shortPath(args.path)}` : ''}`;
|
|
255
|
-
break;
|
|
256
|
-
case 'delete_file':
|
|
257
|
-
summary = shortPath(args.file_path || args.path || '');
|
|
258
|
-
break;
|
|
259
|
-
case 'read_files':
|
|
260
|
-
summary = (args.file_paths || args.paths || []).map(shortPath).join(', ');
|
|
261
|
-
break;
|
|
262
|
-
case 'write_project': {
|
|
263
|
-
const files = (args.files || []).map(f => shortPath(f.path || f.file_path || ''));
|
|
264
|
-
summary = files.length > 0 ? files.join(', ') : '';
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
default:
|
|
268
|
-
summary = Object.values(args || {}).filter(v => typeof v === 'string').join(', ').slice(0, 60);
|
|
269
|
-
}
|
|
225
|
+
const summary = toolDisplaySummary(tool, args, { cwd: safeCwd() });
|
|
270
226
|
|
|
271
227
|
// Render: ⏺ Human-readable action(summary)
|
|
272
228
|
// Use terminal width minus label and padding, minimum 60
|
|
@@ -665,6 +621,16 @@ function renderEvent(event) {
|
|
|
665
621
|
break;
|
|
666
622
|
}
|
|
667
623
|
|
|
624
|
+
case 'plan_created': {
|
|
625
|
+
process.stderr.write(` ${c.dim('project plan prepared')}\n`);
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
case 'goal_created': {
|
|
630
|
+
process.stderr.write(` ${c.dim('project goal prepared')}\n`);
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
|
|
668
634
|
case 'session_info': {
|
|
669
635
|
if (data?.session_id) {
|
|
670
636
|
session.id = data.session_id;
|
|
@@ -1166,9 +1132,8 @@ export async function startTerminalRepl() {
|
|
|
1166
1132
|
const cliArgs = parseArgs(process.argv.slice(2));
|
|
1167
1133
|
const auth = new TarangAuth();
|
|
1168
1134
|
|
|
1169
|
-
//
|
|
1170
|
-
const
|
|
1171
|
-
const toolExecutor = createToolExecutor({ retriever });
|
|
1135
|
+
// Projects are registered and indexed on demand through get_project_overview.
|
|
1136
|
+
const toolExecutor = createToolExecutor();
|
|
1172
1137
|
const skipPerms = cliArgs.freeswim;
|
|
1173
1138
|
const approval = new ApprovalManager({ autoApprove: skipPerms });
|
|
1174
1139
|
|
|
@@ -1186,51 +1151,13 @@ export async function startTerminalRepl() {
|
|
|
1186
1151
|
|
|
1187
1152
|
printBanner(auth);
|
|
1188
1153
|
|
|
1189
|
-
// ── Initialization
|
|
1190
|
-
// BM25 indexing is CPU-bound and blocks the event loop, so setInterval
|
|
1191
|
-
// spinners won't tick during it. Instead, show a static "Initializing..."
|
|
1192
|
-
// message, then yield to the event loop between phases so the spinner runs.
|
|
1193
|
-
let projectSkeleton = '';
|
|
1194
|
-
|
|
1195
|
-
// Phase 1: Show immediate feedback
|
|
1154
|
+
// ── Initialization ──
|
|
1196
1155
|
process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
|
|
1197
|
-
|
|
1198
|
-
// Fetch user in parallel (network I/O, won't block event loop)
|
|
1199
|
-
const userPromise = fetchUser(ctx);
|
|
1200
|
-
|
|
1201
|
-
// Phase 2: BM25 index — CPU-bound, blocks event loop.
|
|
1202
|
-
// Wrap in a microtask break so the initial message renders first.
|
|
1203
|
-
const indexResult = await new Promise((resolve) => {
|
|
1204
|
-
// Let the event loop flush stderr before blocking
|
|
1205
|
-
setImmediate(async () => {
|
|
1206
|
-
try {
|
|
1207
|
-
process.stderr.write(`\r ${c.brand('⠹')} ${c.dim('Indexing project files...')}${' '.repeat(20)}\r`);
|
|
1208
|
-
const result = await retriever.buildIndex();
|
|
1209
|
-
resolve(result);
|
|
1210
|
-
} catch {
|
|
1211
|
-
resolve({ fileCount: 0, chunkCount: 0 });
|
|
1212
|
-
}
|
|
1213
|
-
});
|
|
1214
|
-
});
|
|
1215
|
-
|
|
1216
|
-
// Phase 3: Build skeleton (fast, synchronous)
|
|
1217
|
-
process.stderr.write(`\r ${c.brand('⠼')} ${c.dim('Building project skeleton...')}${' '.repeat(20)}\r`);
|
|
1218
|
-
await new Promise(r => setImmediate(r)); // yield so message renders
|
|
1219
|
-
projectSkeleton = buildProjectSkeleton(safeCwd());
|
|
1220
|
-
|
|
1221
|
-
// Wait for user fetch
|
|
1222
|
-
await userPromise;
|
|
1156
|
+
await fetchUser(ctx);
|
|
1223
1157
|
|
|
1224
1158
|
// Clear the spinner line
|
|
1225
1159
|
process.stderr.write(`\r${' '.repeat(60)}\r`);
|
|
1226
|
-
|
|
1227
|
-
// Show init summary
|
|
1228
|
-
if (indexResult.fileCount > 0) {
|
|
1229
|
-
process.stderr.write(` ${c.green('✓')} ${c.dim(`Indexed ${indexResult.fileCount} files (${indexResult.chunkCount} chunks)`)}\n`);
|
|
1230
|
-
}
|
|
1231
|
-
if (projectSkeleton) {
|
|
1232
|
-
process.stderr.write(` ${c.green('✓')} ${c.dim('Project skeleton ready')}\n`);
|
|
1233
|
-
}
|
|
1160
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Ready; projects will be indexed on demand')}\n`);
|
|
1234
1161
|
if (session.user) {
|
|
1235
1162
|
process.stderr.write(` ${c.green('✓')} ${c.dim(`Logged in as ${session.user.github_username || session.user.email || 'user'}`)}\n`);
|
|
1236
1163
|
}
|
|
@@ -1411,9 +1338,17 @@ export async function startTerminalRepl() {
|
|
|
1411
1338
|
|
|
1412
1339
|
const execContext = { cwd: safeCwd() };
|
|
1413
1340
|
if (skipPerms) execContext.freeswim = true;
|
|
1414
|
-
|
|
1341
|
+
execContext.project_resources = toolExecutor.getProjectResources();
|
|
1342
|
+
execContext.agent_context = toolExecutor.getAgentContext();
|
|
1415
1343
|
|
|
1416
1344
|
for await (const event of client.execute(input, execContext, session.history)) {
|
|
1345
|
+
if (event.type === 'plan_created' || event.type === 'goal_created') {
|
|
1346
|
+
persistProjectArtifacts(
|
|
1347
|
+
event.data,
|
|
1348
|
+
toolExecutor.getProjectResources(),
|
|
1349
|
+
message => process.stderr.write(` ${c.dim(message)}\n`),
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1417
1352
|
renderEvent(event);
|
|
1418
1353
|
|
|
1419
1354
|
if (event.type === 'content_partial') {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SkillInstaller } from '../skills/installer.mjs';
|
|
2
|
+
import { SkillsLoader } from '../skills/loader.mjs';
|
|
3
|
+
|
|
4
|
+
function has(args, flag) {
|
|
5
|
+
return args.includes(flag);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function scopeFrom(args) {
|
|
9
|
+
return has(args, '--project') ? 'project' : 'global';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function print(value) {
|
|
13
|
+
process.stdout.write(typeof value === 'string' ? `${value}\n` : `${JSON.stringify(value, null, 2)}\n`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function runSkillsCommand(args, { cwd = process.cwd() } = {}) {
|
|
17
|
+
const action = args[0] || 'list';
|
|
18
|
+
const rest = args.slice(1);
|
|
19
|
+
const scope = scopeFrom(rest);
|
|
20
|
+
const installer = new SkillInstaller({ cwd });
|
|
21
|
+
const loader = new SkillsLoader().load(cwd);
|
|
22
|
+
|
|
23
|
+
if (action === 'list') {
|
|
24
|
+
const rows = loader.list({ scope: has(rest, '--all') ? '' : scope });
|
|
25
|
+
if (has(rest, '--json')) print(rows);
|
|
26
|
+
else if (!rows.length) print('No skills found.');
|
|
27
|
+
else for (const row of rows) print(`${row.name}\t${row.scope}\t${row.source}\t${row.description}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (action === 'view') {
|
|
31
|
+
const name = rest.find(arg => !arg.startsWith('--'));
|
|
32
|
+
if (!name) throw new Error('Usage: kepler skills view <name> [resource-path]');
|
|
33
|
+
const nameIndex = rest.indexOf(name);
|
|
34
|
+
const resource = rest.slice(nameIndex + 1).find(arg => !arg.startsWith('--')) || null;
|
|
35
|
+
print(loader.view(name, resource));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (action === 'install') {
|
|
39
|
+
const source = rest.find(arg => !arg.startsWith('--'));
|
|
40
|
+
print(installer.install(source, { scope, force: has(rest, '--force') }));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (action === 'remove') {
|
|
44
|
+
const name = rest.find(arg => !arg.startsWith('--'));
|
|
45
|
+
print(installer.remove(name, { scope }));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (action === 'update') {
|
|
49
|
+
const name = rest.find(arg => !arg.startsWith('--'));
|
|
50
|
+
print(installer.update(name, { scope }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Unknown skills command: ${action}`);
|
|
54
|
+
}
|