@agent-spaces/server 0.3.67 → 0.4.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.
- package/dist/adapters/agent-runtime.js +4 -0
- package/dist/adapters/codex-function-tool-bridge.js +9 -1
- package/dist/adapters/git.js +13 -2
- package/dist/adapters/hermes-runtime.js +711 -108
- package/dist/adapters/langchain-runtime.js +384 -21
- package/dist/adapters/oh-my-pi-runtime.js +858 -0
- package/dist/adapters/open-agent-sdk-runtime.js +202 -5
- package/dist/adapters/open-agent-sdk-runtime.test.js +35 -0
- package/dist/agents/agent-message-parts.js +52 -2
- package/dist/agents/issue-task-controller.js +4 -2
- package/dist/agents/title-generator-agent.js +120 -0
- package/dist/app.js +59 -6
- package/dist/routes/agent-sse.js +70 -7
- package/dist/routes/channel.js +23 -2
- package/dist/routes/chat-run.js +191 -0
- package/dist/routes/chat.js +142 -0
- package/dist/routes/command.js +16 -0
- package/dist/routes/data.js +189 -0
- package/dist/routes/git.js +10 -1
- package/dist/routes/import.js +199 -0
- package/dist/routes/issue.js +16 -4
- package/dist/routes/plugin.js +69 -0
- package/dist/routes/version.js +2 -1
- package/dist/routes/workflow-hook.js +71 -0
- package/dist/routes/workflow.js +282 -7
- package/dist/routes/workspace.js +13 -4
- package/dist/services/agent.js +123 -36
- package/dist/services/ai-text.js +185 -0
- package/dist/services/builtin-tools/index.js +1 -0
- package/dist/services/builtin-tools/workflow-editor-tools.js +509 -0
- package/dist/services/builtin-tools/workflow-exec-tools.js +320 -0
- package/dist/services/chat.js +134 -0
- package/dist/services/command-process-manager.js +16 -0
- package/dist/services/execution-manager.js +1346 -0
- package/dist/services/generated-title.js +59 -0
- package/dist/services/gitignore.js +22 -18
- package/dist/services/interaction-manager.js +114 -0
- package/dist/services/issue-retry.js +25 -0
- package/dist/services/output-style.js +8 -1
- package/dist/services/plugin.js +257 -0
- package/dist/services/prompt-template.js +8 -1
- package/dist/services/pty.js +20 -0
- package/dist/services/search.js +16 -6
- package/dist/services/skill.js +2 -0
- package/dist/services/version.js +17 -7
- package/dist/services/workflow-command-runner.js +5 -4
- package/dist/services/workflow-trigger-service.js +137 -0
- package/dist/services/workflow.js +199 -36
- package/dist/storage/agent-store.js +8 -0
- package/dist/storage/chat-store.js +151 -0
- package/dist/storage/database-store.js +6 -0
- package/dist/storage/json-store.js +2 -1
- package/dist/storage/kanban-store.js +6 -0
- package/dist/storage/workflow-store.js +386 -22
- package/dist/ws/agent-prompt.js +6 -2
- package/dist/ws/agent-runner.js +29 -18
- package/dist/ws/chat-handler.js +89 -0
- package/dist/ws/connection-manager.js +49 -2
- package/dist/ws/execution-channels.js +88 -0
- package/dist/ws/handler.js +32 -5
- package/dist/ws/message-parts.js +130 -12
- package/dist/ws/terminal-handler.js +26 -0
- package/package.json +12 -1
- package/public/avatars/user.jpg +0 -0
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, delimiter, extname, join } from 'node:path';
|
|
4
|
+
import { appendOutputStyleToPrompt, summarizeResult } from './agent-runtime-types.js';
|
|
5
|
+
import { startCodexFunctionToolBridge } from './codex-function-tool-bridge.js';
|
|
6
|
+
/**
|
|
7
|
+
* Runtime backed by the external `omp` CLI.
|
|
8
|
+
*
|
|
9
|
+
* The published @oh-my-pi/pi-coding-agent SDK is Bun-native, so the Node server
|
|
10
|
+
* keeps OMP behind a process boundary and talks to the CLI instead of importing
|
|
11
|
+
* the SDK into this process.
|
|
12
|
+
*/
|
|
13
|
+
export class OhMyPiRuntime {
|
|
14
|
+
config;
|
|
15
|
+
child = null;
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
}
|
|
19
|
+
async execute(prompt, workingDir, options) {
|
|
20
|
+
const output = [];
|
|
21
|
+
const cwd = workingDir || process.cwd();
|
|
22
|
+
const startTime = Date.now();
|
|
23
|
+
const d = (message) => console.log(`[oh-my-pi] ${message}`);
|
|
24
|
+
const finalPrompt = appendOutputStyleToPrompt(prompt, options?.outputStyle);
|
|
25
|
+
let functionToolBridge;
|
|
26
|
+
let usage;
|
|
27
|
+
let costUsd;
|
|
28
|
+
const emittedToolUseKeys = new Set();
|
|
29
|
+
const emittedToolResultKeys = new Set();
|
|
30
|
+
d(`starting | cwd=${cwd} provider=${this.config.provider ?? 'default'} model=${this.config.model ?? 'default'} baseURL=${this.config.baseURL ?? 'default'} maxTurns=${options?.maxTurns ?? '∞'} allowedTools=${options?.tools?.join(',') ?? 'all'} mcpServers=${Object.keys(options?.mcpServers ?? {}).join(',') || '-'} functionTools=${options?.functionTools?.map((tool) => tool.name).join(',') || '-'} skills=${options?.skills?.join(',') || '-'} sandboxDirs=${options?.sandboxDirs?.join(',') ?? '-'}`);
|
|
31
|
+
d(`prompt: ${prompt.slice(0, 300)}${prompt.length > 300 ? '...' : ''}`);
|
|
32
|
+
try {
|
|
33
|
+
functionToolBridge = await startCodexFunctionToolBridge(options?.functionTools, d);
|
|
34
|
+
const mcpServers = withFunctionToolBridge(options?.mcpServers, functionToolBridge);
|
|
35
|
+
const ompHome = prepareOmpConfigHome(this.config, options, mcpServers, d);
|
|
36
|
+
const args = buildOmpArgs(finalPrompt, this.config, options, ompHome);
|
|
37
|
+
d(`resolved tools | mcpServers=${Object.keys(mcpServers ?? {}).join(',') || '-'} functionToolBridge=${functionToolBridge?.url ?? '-'}`);
|
|
38
|
+
return await new Promise((resolve) => {
|
|
39
|
+
let settled = false;
|
|
40
|
+
let stdoutBuffer = '';
|
|
41
|
+
let stderrBuffer = '';
|
|
42
|
+
let sessionId = options?.resumeSessionId;
|
|
43
|
+
let emittedSessionId = sessionId;
|
|
44
|
+
const reasoningSnapshots = { blocks: [] };
|
|
45
|
+
const finish = (result) => {
|
|
46
|
+
if (settled)
|
|
47
|
+
return;
|
|
48
|
+
settled = true;
|
|
49
|
+
this.child = null;
|
|
50
|
+
resolve(result);
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
this.child = spawn(resolveOmpCommand(), args, {
|
|
54
|
+
cwd,
|
|
55
|
+
env: buildEnv(this.config, options, ompHome),
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
61
|
+
d(`failed to start | ${message}`);
|
|
62
|
+
finish({
|
|
63
|
+
success: false,
|
|
64
|
+
summary: 'Oh My Pi execution failed',
|
|
65
|
+
artifacts: [],
|
|
66
|
+
error: message,
|
|
67
|
+
output,
|
|
68
|
+
sessionId,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.child.stdout.setEncoding('utf-8');
|
|
73
|
+
this.child.stderr.setEncoding('utf-8');
|
|
74
|
+
this.child.stdout.on('data', (chunk) => {
|
|
75
|
+
stdoutBuffer = consumeLines(stdoutBuffer + chunk, (line) => {
|
|
76
|
+
if (!line)
|
|
77
|
+
return;
|
|
78
|
+
const parsed = parseJsonLine(line);
|
|
79
|
+
if (parsed) {
|
|
80
|
+
const result = handleJsonEvent(parsed, {
|
|
81
|
+
output,
|
|
82
|
+
options,
|
|
83
|
+
log: d,
|
|
84
|
+
emittedToolUseKeys,
|
|
85
|
+
emittedToolResultKeys,
|
|
86
|
+
reasoningSnapshots,
|
|
87
|
+
});
|
|
88
|
+
if (result.sessionId && result.sessionId !== emittedSessionId) {
|
|
89
|
+
sessionId = result.sessionId;
|
|
90
|
+
emittedSessionId = result.sessionId;
|
|
91
|
+
options?.onEvent?.({ type: 'session', sessionId });
|
|
92
|
+
}
|
|
93
|
+
if (result.usage)
|
|
94
|
+
usage = result.usage;
|
|
95
|
+
if (typeof result.costUsd === 'number')
|
|
96
|
+
costUsd = result.costUsd;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
output.push(line);
|
|
100
|
+
d(`stdout text | ${truncateForLog(line)}`);
|
|
101
|
+
const parsedSessionId = extractSessionId(line);
|
|
102
|
+
if (parsedSessionId && parsedSessionId !== emittedSessionId) {
|
|
103
|
+
sessionId = parsedSessionId;
|
|
104
|
+
emittedSessionId = parsedSessionId;
|
|
105
|
+
options?.onEvent?.({ type: 'session', sessionId });
|
|
106
|
+
}
|
|
107
|
+
options?.onEvent?.({ type: 'output', line });
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
this.child.stderr.on('data', (chunk) => {
|
|
111
|
+
stderrBuffer = consumeLines(stderrBuffer + chunk, (line) => {
|
|
112
|
+
if (!line)
|
|
113
|
+
return;
|
|
114
|
+
const formatted = `[stderr] ${line}`;
|
|
115
|
+
output.push(formatted);
|
|
116
|
+
options?.onEvent?.({ type: 'output', line: formatted });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
this.child.on('error', (err) => {
|
|
120
|
+
const message = err.message.includes('ENOENT')
|
|
121
|
+
? 'Oh My Pi CLI was not found. Install OMP and ensure the `omp` command is available on PATH.'
|
|
122
|
+
: err.message;
|
|
123
|
+
d(`failed ${Date.now() - startTime}ms | ${message}`);
|
|
124
|
+
finish({
|
|
125
|
+
success: false,
|
|
126
|
+
summary: 'Oh My Pi execution failed',
|
|
127
|
+
artifacts: [],
|
|
128
|
+
error: message,
|
|
129
|
+
output,
|
|
130
|
+
sessionId,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
this.child.on('close', (code, signal) => {
|
|
134
|
+
stdoutBuffer = flushLine(stdoutBuffer, output, options);
|
|
135
|
+
stderrBuffer = flushLine(stderrBuffer, output, options, '[stderr] ');
|
|
136
|
+
void stdoutBuffer;
|
|
137
|
+
void stderrBuffer;
|
|
138
|
+
const elapsed = Date.now() - startTime;
|
|
139
|
+
if (code === 0) {
|
|
140
|
+
const text = lastMeaningfulLine(output);
|
|
141
|
+
d(`done ${elapsed}ms`);
|
|
142
|
+
finish({
|
|
143
|
+
success: true,
|
|
144
|
+
summary: summarizeResult(text),
|
|
145
|
+
artifacts: [],
|
|
146
|
+
output,
|
|
147
|
+
sessionId,
|
|
148
|
+
usage,
|
|
149
|
+
costUsd,
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const error = signal
|
|
154
|
+
? `Oh My Pi execution stopped by signal ${signal}`
|
|
155
|
+
: `Oh My Pi execution failed with exit code ${code ?? 'unknown'}`;
|
|
156
|
+
d(`failed ${elapsed}ms | ${error}`);
|
|
157
|
+
finish({
|
|
158
|
+
success: false,
|
|
159
|
+
summary: 'Oh My Pi execution failed',
|
|
160
|
+
artifacts: [],
|
|
161
|
+
error,
|
|
162
|
+
output,
|
|
163
|
+
sessionId,
|
|
164
|
+
usage,
|
|
165
|
+
costUsd,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
const elapsed = Date.now() - startTime;
|
|
172
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
173
|
+
d(`failed ${elapsed}ms | ${message}`);
|
|
174
|
+
if (err instanceof Error && err.stack)
|
|
175
|
+
console.error(err.stack);
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
summary: 'Oh My Pi execution failed',
|
|
179
|
+
artifacts: [],
|
|
180
|
+
error: message,
|
|
181
|
+
output,
|
|
182
|
+
sessionId: options?.resumeSessionId,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
try {
|
|
187
|
+
await functionToolBridge?.close();
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
191
|
+
d(`function tool bridge close failed | ${message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
stop() {
|
|
196
|
+
this.child?.kill();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function buildOmpArgs(prompt, config, options, ompHome) {
|
|
200
|
+
const args = ['--mode', 'json'];
|
|
201
|
+
if (options?.resumeSessionId)
|
|
202
|
+
args.push('--resume', options.resumeSessionId);
|
|
203
|
+
if (config.model)
|
|
204
|
+
args.push('--model', config.model);
|
|
205
|
+
if (config.provider)
|
|
206
|
+
args.push('--provider', String(config.provider));
|
|
207
|
+
if (config.apiKey)
|
|
208
|
+
args.push('--api-key', config.apiKey);
|
|
209
|
+
const thinking = normalizeThinkingLevel(config);
|
|
210
|
+
if (thinking)
|
|
211
|
+
args.push('--thinking', thinking);
|
|
212
|
+
if (options?.tools?.length)
|
|
213
|
+
args.push('--tools', options.tools.join(','));
|
|
214
|
+
if (options?.systemPrompt?.trim())
|
|
215
|
+
args.push('--system-prompt', options.systemPrompt.trim());
|
|
216
|
+
const skills = normalizeSkillNames(options?.skills);
|
|
217
|
+
if (skills.length)
|
|
218
|
+
args.push('--skills', skills.join(','));
|
|
219
|
+
const agentDir = ompAgentDir(ompHome, options);
|
|
220
|
+
if (agentDir)
|
|
221
|
+
args.push('--session-dir', join(agentDir, 'sessions'));
|
|
222
|
+
args.push('-p', prompt);
|
|
223
|
+
return args;
|
|
224
|
+
}
|
|
225
|
+
function buildEnv(config, options, ompHome) {
|
|
226
|
+
const apiKey = config.apiKey || process.env.PI_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
227
|
+
const agentDir = ompAgentDir(ompHome, options);
|
|
228
|
+
return removeUndefined({
|
|
229
|
+
...process.env,
|
|
230
|
+
HOME: ompHome || process.env.HOME,
|
|
231
|
+
USERPROFILE: ompHome || process.env.USERPROFILE,
|
|
232
|
+
HOMEDRIVE: ompHome ? '' : process.env.HOMEDRIVE,
|
|
233
|
+
HOMEPATH: ompHome ? '' : process.env.HOMEPATH,
|
|
234
|
+
OMP_LOG_DIR: agentDir ? join(agentDir, 'logs') : process.env.OMP_LOG_DIR,
|
|
235
|
+
AGENT_SPACES_OMP_API_KEY: apiKey,
|
|
236
|
+
PI_API_KEY: apiKey,
|
|
237
|
+
OMP_API_KEY: apiKey,
|
|
238
|
+
PI_CODING_AGENT_DIR: agentDir || process.env.PI_CODING_AGENT_DIR,
|
|
239
|
+
OPENAI_API_KEY: config.provider === 'anthropic-messages' ? process.env.OPENAI_API_KEY : apiKey,
|
|
240
|
+
ANTHROPIC_API_KEY: config.provider === 'anthropic-messages' ? apiKey : process.env.ANTHROPIC_API_KEY,
|
|
241
|
+
OPENAI_BASE_URL: config.baseURL || process.env.OPENAI_BASE_URL,
|
|
242
|
+
ANTHROPIC_BASE_URL: config.baseURL || process.env.ANTHROPIC_BASE_URL,
|
|
243
|
+
PI_AGENT_DIR: agentDir || process.env.PI_AGENT_DIR,
|
|
244
|
+
OMP_AGENT_DIR: agentDir || process.env.OMP_AGENT_DIR,
|
|
245
|
+
NO_COLOR: process.env.NO_COLOR || '1',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function resolveOmpCommand() {
|
|
249
|
+
const configured = process.env.OMP_CLI_PATH?.trim();
|
|
250
|
+
if (configured)
|
|
251
|
+
return configured;
|
|
252
|
+
if (process.platform !== 'win32')
|
|
253
|
+
return 'omp';
|
|
254
|
+
const pathCommand = resolveWindowsPathCommand('omp.exe');
|
|
255
|
+
if (pathCommand)
|
|
256
|
+
return pathCommand;
|
|
257
|
+
const candidates = [
|
|
258
|
+
process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, 'omp', 'omp.exe') : undefined,
|
|
259
|
+
process.env.USERPROFILE ? join(process.env.USERPROFILE, 'AppData', 'Local', 'omp', 'omp.exe') : undefined,
|
|
260
|
+
].filter((candidate) => Boolean(candidate));
|
|
261
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? 'omp';
|
|
262
|
+
}
|
|
263
|
+
function resolveWindowsPathCommand(command) {
|
|
264
|
+
const pathValue = getPathEnvValue();
|
|
265
|
+
if (!pathValue)
|
|
266
|
+
return undefined;
|
|
267
|
+
for (const dir of pathValue.split(delimiter)) {
|
|
268
|
+
if (!dir.trim())
|
|
269
|
+
continue;
|
|
270
|
+
const candidate = join(dir.trim(), command);
|
|
271
|
+
if (existsSync(candidate))
|
|
272
|
+
return candidate;
|
|
273
|
+
}
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
function getPathEnvValue() {
|
|
277
|
+
return Object.entries(process.env).find(([key]) => key.toLowerCase() === 'path')?.[1];
|
|
278
|
+
}
|
|
279
|
+
function prepareOmpConfigHome(config, options, mcpServers, log) {
|
|
280
|
+
if (!options?.configDir)
|
|
281
|
+
return undefined;
|
|
282
|
+
const homeDir = join(options.configDir, 'omp-home');
|
|
283
|
+
const agentDir = join(homeDir, '.omp', 'agent');
|
|
284
|
+
mkdirSync(agentDir, { recursive: true });
|
|
285
|
+
copySkillsToOmpAgentDir(options.configDir, agentDir, options.skills);
|
|
286
|
+
writeFileSync(join(agentDir, 'config.yml'), buildOmpConfigYaml(config), 'utf-8');
|
|
287
|
+
const modelsYaml = buildOmpModelsYaml(config);
|
|
288
|
+
if (modelsYaml)
|
|
289
|
+
writeFileSync(join(agentDir, 'models.yml'), modelsYaml, 'utf-8');
|
|
290
|
+
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
291
|
+
const mcpConfigPath = join(agentDir, 'mcp.json');
|
|
292
|
+
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), 'utf-8');
|
|
293
|
+
log?.(`wrote MCP config | path=${mcpConfigPath} servers=${Object.keys(mcpServers).join(',')}`);
|
|
294
|
+
}
|
|
295
|
+
return homeDir;
|
|
296
|
+
}
|
|
297
|
+
function copySkillsToOmpAgentDir(sourceAgentDir, targetAgentDir, requestedSkills) {
|
|
298
|
+
const sourceSkillsDir = join(sourceAgentDir, 'skills');
|
|
299
|
+
const targetSkillsDir = join(targetAgentDir, 'skills');
|
|
300
|
+
const copied = new Set();
|
|
301
|
+
rmSync(targetSkillsDir, { recursive: true, force: true });
|
|
302
|
+
mkdirSync(targetSkillsDir, { recursive: true });
|
|
303
|
+
if (existsSync(sourceSkillsDir)) {
|
|
304
|
+
for (const entry of readdirSync(sourceSkillsDir)) {
|
|
305
|
+
const source = join(sourceSkillsDir, entry);
|
|
306
|
+
const skillName = copySkillSource(source, entry, targetSkillsDir);
|
|
307
|
+
if (skillName)
|
|
308
|
+
copied.add(skillName);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (const skill of normalizeSkillNames(requestedSkills)) {
|
|
312
|
+
if (copied.has(skill))
|
|
313
|
+
continue;
|
|
314
|
+
const fallback = findFallbackSkillSource(sourceAgentDir, skill);
|
|
315
|
+
if (!fallback)
|
|
316
|
+
continue;
|
|
317
|
+
const skillName = copySkillSource(fallback, skill, targetSkillsDir);
|
|
318
|
+
if (skillName)
|
|
319
|
+
copied.add(skillName);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function copySkillSource(source, entry, targetSkillsDir) {
|
|
323
|
+
if (!existsSync(source))
|
|
324
|
+
return undefined;
|
|
325
|
+
const sourceStat = statSync(source);
|
|
326
|
+
if (sourceStat.isDirectory()) {
|
|
327
|
+
const skillName = sanitizeSkillName(entry);
|
|
328
|
+
if (!skillName)
|
|
329
|
+
return undefined;
|
|
330
|
+
const sourceSkillFile = join(source, 'SKILL.md');
|
|
331
|
+
if (!existsSync(sourceSkillFile) || statSync(sourceSkillFile).size === 0)
|
|
332
|
+
return undefined;
|
|
333
|
+
cpSync(source, join(targetSkillsDir, skillName), { recursive: true, force: true });
|
|
334
|
+
writeOmpSkillFile(join(targetSkillsDir, skillName, 'SKILL.md'), readFileSync(sourceSkillFile, 'utf-8'), skillName);
|
|
335
|
+
return skillName;
|
|
336
|
+
}
|
|
337
|
+
if (!sourceStat.isFile() || extname(entry).toLowerCase() !== '.md' || sourceStat.size === 0)
|
|
338
|
+
return undefined;
|
|
339
|
+
const skillName = sanitizeSkillName(entry);
|
|
340
|
+
if (!skillName)
|
|
341
|
+
return undefined;
|
|
342
|
+
const targetSkillDir = join(targetSkillsDir, skillName);
|
|
343
|
+
mkdirSync(targetSkillDir, { recursive: true });
|
|
344
|
+
writeOmpSkillFile(join(targetSkillDir, 'SKILL.md'), readFileSync(source, 'utf-8'), skillName);
|
|
345
|
+
return skillName;
|
|
346
|
+
}
|
|
347
|
+
function writeOmpSkillFile(targetFile, content, skillName) {
|
|
348
|
+
writeFileSync(targetFile, ensureOmpSkillFrontmatter(content, skillName), 'utf-8');
|
|
349
|
+
}
|
|
350
|
+
function ensureOmpSkillFrontmatter(content, skillName) {
|
|
351
|
+
const parsed = parseSkillMarkdown(content);
|
|
352
|
+
if (parsed.meta.name && parsed.meta.description)
|
|
353
|
+
return content;
|
|
354
|
+
const body = parsed.body.trim();
|
|
355
|
+
const meta = {
|
|
356
|
+
...parsed.meta,
|
|
357
|
+
name: parsed.meta.name || skillName,
|
|
358
|
+
description: parsed.meta.description || summarizeSkillDescription(body, skillName),
|
|
359
|
+
};
|
|
360
|
+
const frontmatter = Object.entries(meta)
|
|
361
|
+
.filter(([, value]) => value.trim())
|
|
362
|
+
.map(([key, value]) => `${key}: ${yamlScalar(value)}`)
|
|
363
|
+
.join('\n');
|
|
364
|
+
return `---\n${frontmatter}\n---\n\n${body}\n`;
|
|
365
|
+
}
|
|
366
|
+
function parseSkillMarkdown(source) {
|
|
367
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
368
|
+
if (!match)
|
|
369
|
+
return { meta: {}, body: source.trim() };
|
|
370
|
+
const meta = {};
|
|
371
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
372
|
+
const parsed = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
373
|
+
if (!parsed)
|
|
374
|
+
continue;
|
|
375
|
+
meta[parsed[1]] = parsed[2].trim().replace(/^['"]|['"]$/g, '');
|
|
376
|
+
}
|
|
377
|
+
return { meta, body: source.slice(match[0].length).trim() };
|
|
378
|
+
}
|
|
379
|
+
function summarizeSkillDescription(body, skillName) {
|
|
380
|
+
const firstLine = body
|
|
381
|
+
.split(/\r?\n/)
|
|
382
|
+
.map((line) => line.replace(/^#+\s*/, '').trim())
|
|
383
|
+
.find(Boolean);
|
|
384
|
+
return firstLine || `Configured skill ${skillName}`;
|
|
385
|
+
}
|
|
386
|
+
function findFallbackSkillSource(sourceAgentDir, skill) {
|
|
387
|
+
const workspaceAgentspaceDir = join(sourceAgentDir, '..', '..');
|
|
388
|
+
const candidates = [
|
|
389
|
+
join(workspaceAgentspaceDir, 'skills', skill),
|
|
390
|
+
join(workspaceAgentspaceDir, 'skills', `${skill}.md`),
|
|
391
|
+
join(process.cwd(), 'skills', skill),
|
|
392
|
+
join(process.cwd(), 'skills', `${skill}.md`),
|
|
393
|
+
...builtInSkillCandidates(skill),
|
|
394
|
+
];
|
|
395
|
+
return candidates.find(isReadableSkillSource);
|
|
396
|
+
}
|
|
397
|
+
function builtInSkillCandidates(skill) {
|
|
398
|
+
const roots = [
|
|
399
|
+
join(process.cwd(), 'packages', 'agents', 'skills'),
|
|
400
|
+
join(import.meta.dirname ?? '', '..', '..', '..', 'agents', 'skills'),
|
|
401
|
+
];
|
|
402
|
+
const candidates = [];
|
|
403
|
+
for (const root of roots) {
|
|
404
|
+
candidates.push(join(root, skill), join(root, `${skill}.md`), join(root, 'superpowers', skill));
|
|
405
|
+
if (!existsSync(root))
|
|
406
|
+
continue;
|
|
407
|
+
for (const group of readdirSync(root)) {
|
|
408
|
+
const groupDir = join(root, group);
|
|
409
|
+
if (existsSync(groupDir) && statSync(groupDir).isDirectory()) {
|
|
410
|
+
candidates.push(join(groupDir, skill), join(groupDir, `${skill}.md`));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return candidates;
|
|
415
|
+
}
|
|
416
|
+
function isReadableSkillSource(source) {
|
|
417
|
+
if (!existsSync(source))
|
|
418
|
+
return false;
|
|
419
|
+
const sourceStat = statSync(source);
|
|
420
|
+
if (sourceStat.isDirectory()) {
|
|
421
|
+
const skillFile = join(source, 'SKILL.md');
|
|
422
|
+
return existsSync(skillFile) && statSync(skillFile).size > 0;
|
|
423
|
+
}
|
|
424
|
+
return sourceStat.isFile() && extname(source).toLowerCase() === '.md' && sourceStat.size > 0;
|
|
425
|
+
}
|
|
426
|
+
function withFunctionToolBridge(mcpServers, bridge) {
|
|
427
|
+
if (!bridge)
|
|
428
|
+
return mcpServers;
|
|
429
|
+
return {
|
|
430
|
+
...(mcpServers ?? {}),
|
|
431
|
+
[bridge.name]: { url: bridge.url, type: 'http' },
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function ompAgentDir(ompHome, options) {
|
|
435
|
+
if (ompHome)
|
|
436
|
+
return join(ompHome, '.omp', 'agent');
|
|
437
|
+
return options?.configDir;
|
|
438
|
+
}
|
|
439
|
+
function buildOmpConfigYaml(config) {
|
|
440
|
+
const model = config.provider && config.model ? `${sanitizeProviderName(config.provider)}/${config.model}` : config.model;
|
|
441
|
+
const modelRoles = model ? `modelRoles:\n default: ${yamlScalar(model)}\n` : '';
|
|
442
|
+
const thinking = normalizeThinkingLevel(config);
|
|
443
|
+
const thinkingLine = thinking ? `defaultThinkingLevel: ${yamlScalar(thinking)}\n` : '';
|
|
444
|
+
return `${modelRoles}${thinkingLine}`;
|
|
445
|
+
}
|
|
446
|
+
function buildOmpModelsYaml(config) {
|
|
447
|
+
if (!config.model || !config.baseURL)
|
|
448
|
+
return undefined;
|
|
449
|
+
const providerName = sanitizeProviderName(config.provider);
|
|
450
|
+
const api = normalizeOmpApi(config.provider);
|
|
451
|
+
const apiKeyRef = config.apiKey ? 'AGENT_SPACES_OMP_API_KEY' : undefined;
|
|
452
|
+
const lines = [
|
|
453
|
+
'providers:',
|
|
454
|
+
` ${yamlKey(providerName)}:`,
|
|
455
|
+
` baseUrl: ${yamlScalar(config.baseURL)}`,
|
|
456
|
+
` api: ${yamlScalar(api)}`,
|
|
457
|
+
];
|
|
458
|
+
if (apiKeyRef)
|
|
459
|
+
lines.push(` apiKey: ${yamlScalar(apiKeyRef)}`);
|
|
460
|
+
else
|
|
461
|
+
lines.push(' auth: none');
|
|
462
|
+
lines.push(' models:', ` - id: ${yamlScalar(config.model)}`, ` name: ${yamlScalar(config.model)}`, ` api: ${yamlScalar(api)}`, ` reasoning: ${config.thinkingEnabled === false ? 'false' : 'true'}`, ' input: [text]', ' cost:', ' input: 0', ' output: 0', ' cacheRead: 0', ' cacheWrite: 0', ' contextWindow: 128000', ' maxTokens: 16384', '');
|
|
463
|
+
return lines.join('\n');
|
|
464
|
+
}
|
|
465
|
+
function normalizeOmpApi(provider) {
|
|
466
|
+
switch (provider) {
|
|
467
|
+
case 'anthropic-messages':
|
|
468
|
+
return 'anthropic-messages';
|
|
469
|
+
case 'openai-responses':
|
|
470
|
+
return 'openai-responses';
|
|
471
|
+
case 'gemini-generate-content':
|
|
472
|
+
return 'google-generative-ai';
|
|
473
|
+
case 'openai-chat-completions':
|
|
474
|
+
default:
|
|
475
|
+
return 'openai-completions';
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function sanitizeProviderName(provider) {
|
|
479
|
+
const raw = String(provider || 'agent-spaces').trim().toLowerCase();
|
|
480
|
+
return raw.replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'agent-spaces';
|
|
481
|
+
}
|
|
482
|
+
function sanitizeSkillName(name) {
|
|
483
|
+
const raw = basename(name).replace(/\.md$/i, '').trim();
|
|
484
|
+
return raw.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
485
|
+
}
|
|
486
|
+
function yamlKey(value) {
|
|
487
|
+
return /^[A-Za-z0-9_-]+$/.test(value) ? value : yamlScalar(value);
|
|
488
|
+
}
|
|
489
|
+
function yamlScalar(value) {
|
|
490
|
+
return JSON.stringify(value);
|
|
491
|
+
}
|
|
492
|
+
function normalizeThinkingLevel(config) {
|
|
493
|
+
if (config.thinkingEnabled === false)
|
|
494
|
+
return 'off';
|
|
495
|
+
return config.thinkingEffort ?? 'medium';
|
|
496
|
+
}
|
|
497
|
+
function normalizeSkillNames(skills) {
|
|
498
|
+
if (!Array.isArray(skills))
|
|
499
|
+
return [];
|
|
500
|
+
return skills
|
|
501
|
+
.map((skill) => skill.trim())
|
|
502
|
+
.filter(Boolean);
|
|
503
|
+
}
|
|
504
|
+
function consumeLines(buffer, onLine) {
|
|
505
|
+
const lines = buffer.split(/\r?\n/);
|
|
506
|
+
const remainder = lines.pop() ?? '';
|
|
507
|
+
for (const line of lines)
|
|
508
|
+
onLine(stripAnsi(line).trimEnd());
|
|
509
|
+
return remainder;
|
|
510
|
+
}
|
|
511
|
+
function parseJsonLine(line) {
|
|
512
|
+
const trimmed = stripAnsi(line).trim();
|
|
513
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('['))
|
|
514
|
+
return undefined;
|
|
515
|
+
try {
|
|
516
|
+
return JSON.parse(trimmed);
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function handleJsonEvent(event, ctx) {
|
|
523
|
+
const record = isRecord(event) ? event : {};
|
|
524
|
+
const eventType = stringValue(record.type) ?? stringValue(record.event) ?? stringValue(record.kind) ?? 'unknown';
|
|
525
|
+
const keys = Object.keys(record).slice(0, 12).join(',') || '-';
|
|
526
|
+
const contentBlocks = collectContentBlocks(event);
|
|
527
|
+
// ctx.log(`json event | type=${eventType} keys=${keys} contentBlocks=${summarizeBlockTypes(contentBlocks)}`);
|
|
528
|
+
const sessionId = readSessionId(event);
|
|
529
|
+
const usage = readUsage(event);
|
|
530
|
+
const costUsd = readCostUsd(event);
|
|
531
|
+
const reasoningTexts = collectReasoningTexts(event);
|
|
532
|
+
const completedReasoningTexts = isOmpStreamingMessageEvent(eventType)
|
|
533
|
+
? collectCompletedStreamingReasoning(eventType, reasoningTexts, ctx.reasoningSnapshots)
|
|
534
|
+
: reasoningTexts;
|
|
535
|
+
for (const text of completedReasoningTexts) {
|
|
536
|
+
ctx.log(`reasoning | ${truncateForLog(text)}`);
|
|
537
|
+
ctx.options?.onEvent?.({ type: 'reasoning', text, status: 'completed' });
|
|
538
|
+
}
|
|
539
|
+
for (const toolUse of collectToolUses(event, eventType)) {
|
|
540
|
+
const key = `${toolUse.id}:${toolUse.name}`;
|
|
541
|
+
if (ctx.emittedToolUseKeys.has(key))
|
|
542
|
+
continue;
|
|
543
|
+
ctx.emittedToolUseKeys.add(key);
|
|
544
|
+
const line = formatToolUseLine(toolUse);
|
|
545
|
+
ctx.log(`tool use | id=${toolUse.id} name=${toolUse.name} input=${summarizeUnknown(toolUse.input)}`);
|
|
546
|
+
ctx.options?.onEvent?.({
|
|
547
|
+
type: 'tool_use',
|
|
548
|
+
id: toolUse.id,
|
|
549
|
+
name: toolUse.name,
|
|
550
|
+
input: toolUse.input,
|
|
551
|
+
line,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
for (const toolResult of collectToolResults(event)) {
|
|
555
|
+
const key = `${toolResult.toolUseId ?? '-'}:${summarizeUnknown(toolResult.result)}`;
|
|
556
|
+
if (ctx.emittedToolResultKeys.has(key))
|
|
557
|
+
continue;
|
|
558
|
+
ctx.emittedToolResultKeys.add(key);
|
|
559
|
+
ctx.log(`tool result | id=${toolResult.toolUseId ?? '-'} result=${summarizeUnknown(toolResult.result)}`);
|
|
560
|
+
ctx.options?.onEvent?.({
|
|
561
|
+
type: 'tool_result',
|
|
562
|
+
toolUseId: toolResult.toolUseId,
|
|
563
|
+
result: toolResult.result,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
for (const text of collectOutputTexts(event, eventType)) {
|
|
567
|
+
ctx.output.push(text);
|
|
568
|
+
ctx.log(`output | ${truncateForLog(text)}`);
|
|
569
|
+
ctx.options?.onEvent?.({ type: 'output', line: text });
|
|
570
|
+
}
|
|
571
|
+
return { sessionId, usage, costUsd };
|
|
572
|
+
}
|
|
573
|
+
function collectOutputTexts(value, eventType) {
|
|
574
|
+
if (eventType !== 'turn_end')
|
|
575
|
+
return [];
|
|
576
|
+
if (hasToolCallBlock(value)) {
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
const texts = [];
|
|
580
|
+
for (const block of collectContentBlocks(value)) {
|
|
581
|
+
if (!isRecord(block))
|
|
582
|
+
continue;
|
|
583
|
+
const type = stringValue(block.type);
|
|
584
|
+
if (type && type !== 'text' && type !== 'output_text')
|
|
585
|
+
continue;
|
|
586
|
+
const text = stringValue(block.text) ?? stringValue(block.content) ?? stringValue(block.output);
|
|
587
|
+
const visible = normalizeVisibleOutput(text);
|
|
588
|
+
if (visible)
|
|
589
|
+
texts.push(visible);
|
|
590
|
+
}
|
|
591
|
+
if (texts.length)
|
|
592
|
+
return texts;
|
|
593
|
+
if (!isRecord(value))
|
|
594
|
+
return [];
|
|
595
|
+
const type = stringValue(value.type) ?? stringValue(value.event);
|
|
596
|
+
if (type && /tool|usage|session|reasoning/i.test(type))
|
|
597
|
+
return [];
|
|
598
|
+
const text = stringValue(value.text) ?? stringValue(value.output) ?? stringValue(value.content);
|
|
599
|
+
const visible = normalizeVisibleOutput(text);
|
|
600
|
+
return visible ? [visible] : [];
|
|
601
|
+
}
|
|
602
|
+
function collectReasoningTexts(value) {
|
|
603
|
+
const texts = [];
|
|
604
|
+
for (const block of collectContentBlocks(value)) {
|
|
605
|
+
if (!isRecord(block))
|
|
606
|
+
continue;
|
|
607
|
+
const type = stringValue(block.type);
|
|
608
|
+
if (type !== 'reasoning' && type !== 'thinking')
|
|
609
|
+
continue;
|
|
610
|
+
const text = stringValue(block.text) ?? stringValue(block.content) ?? stringValue(block.thinking);
|
|
611
|
+
if (text)
|
|
612
|
+
texts.push(text);
|
|
613
|
+
}
|
|
614
|
+
if (!isRecord(value))
|
|
615
|
+
return texts;
|
|
616
|
+
const type = stringValue(value.type) ?? stringValue(value.event);
|
|
617
|
+
if (type === 'reasoning' || type === 'thinking') {
|
|
618
|
+
const text = stringValue(value.text) ?? stringValue(value.content) ?? stringValue(value.thinking);
|
|
619
|
+
if (text)
|
|
620
|
+
texts.push(text);
|
|
621
|
+
}
|
|
622
|
+
return texts;
|
|
623
|
+
}
|
|
624
|
+
function collectCompletedStreamingReasoning(eventType, texts, state) {
|
|
625
|
+
if (eventType === 'message_start') {
|
|
626
|
+
state.blocks = [];
|
|
627
|
+
}
|
|
628
|
+
texts.forEach((text, index) => {
|
|
629
|
+
state.blocks[index] = text;
|
|
630
|
+
});
|
|
631
|
+
if (eventType !== 'turn_end')
|
|
632
|
+
return [];
|
|
633
|
+
const completed = state.blocks.filter(Boolean);
|
|
634
|
+
state.blocks = [];
|
|
635
|
+
return completed;
|
|
636
|
+
}
|
|
637
|
+
function collectToolUses(value, eventType) {
|
|
638
|
+
const uses = [];
|
|
639
|
+
const ompToolExecution = parseOmpToolExecutionStart(value);
|
|
640
|
+
if (ompToolExecution)
|
|
641
|
+
uses.push(ompToolExecution);
|
|
642
|
+
if (!isOmpStreamingMessageEvent(eventType)) {
|
|
643
|
+
for (const block of collectContentBlocks(value)) {
|
|
644
|
+
const parsed = parseToolUse(block);
|
|
645
|
+
if (parsed)
|
|
646
|
+
uses.push(parsed);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const direct = parseToolUse(value);
|
|
650
|
+
if (direct)
|
|
651
|
+
uses.push(direct);
|
|
652
|
+
return uniqueBy(uses, (item) => `${item.id}:${item.name}`);
|
|
653
|
+
}
|
|
654
|
+
function isOmpStreamingMessageEvent(eventType) {
|
|
655
|
+
return eventType === 'message_start'
|
|
656
|
+
|| eventType === 'message_update'
|
|
657
|
+
|| eventType === 'message_end'
|
|
658
|
+
|| eventType === 'turn_end';
|
|
659
|
+
}
|
|
660
|
+
function collectToolResults(value) {
|
|
661
|
+
const results = [];
|
|
662
|
+
for (const block of collectContentBlocks(value)) {
|
|
663
|
+
const parsed = parseToolResult(block);
|
|
664
|
+
if (parsed)
|
|
665
|
+
results.push(parsed);
|
|
666
|
+
}
|
|
667
|
+
const direct = parseToolResult(value);
|
|
668
|
+
if (direct)
|
|
669
|
+
results.push(direct);
|
|
670
|
+
return uniqueBy(results, (item) => `${item.toolUseId ?? '-'}:${summarizeUnknown(item.result)}`);
|
|
671
|
+
}
|
|
672
|
+
function parseToolUse(value) {
|
|
673
|
+
if (!isRecord(value))
|
|
674
|
+
return undefined;
|
|
675
|
+
const type = stringValue(value.type) ?? stringValue(value.event) ?? stringValue(value.kind);
|
|
676
|
+
if (!type || !/^(tool_use|tool_call|toolCall|tool_start|tool_execution_start|tool)$/i.test(type))
|
|
677
|
+
return undefined;
|
|
678
|
+
const id = stringValue(value.id)
|
|
679
|
+
?? stringValue(value.tool_use_id)
|
|
680
|
+
?? stringValue(value.toolCallId)
|
|
681
|
+
?? stringValue(value.call_id)
|
|
682
|
+
?? `omp-tool-${Date.now()}`;
|
|
683
|
+
const name = stringValue(value.name)
|
|
684
|
+
?? stringValue(value.tool_name)
|
|
685
|
+
?? stringValue(value.toolName)
|
|
686
|
+
?? stringValue(value.function_name)
|
|
687
|
+
?? 'unknown_tool';
|
|
688
|
+
const input = value.input ?? value.arguments ?? value.args ?? value.parameters ?? value.intent;
|
|
689
|
+
return { id, name, input };
|
|
690
|
+
}
|
|
691
|
+
function parseOmpToolExecutionStart(value) {
|
|
692
|
+
if (!isRecord(value))
|
|
693
|
+
return undefined;
|
|
694
|
+
if (stringValue(value.type) !== 'tool_execution_start')
|
|
695
|
+
return undefined;
|
|
696
|
+
const id = stringValue(value.toolCallId) ?? stringValue(value.id) ?? `omp-tool-${Date.now()}`;
|
|
697
|
+
const name = stringValue(value.toolName) ?? stringValue(value.name) ?? 'unknown_tool';
|
|
698
|
+
const input = value.args ?? value.input ?? value.intent;
|
|
699
|
+
return { id, name, input };
|
|
700
|
+
}
|
|
701
|
+
function parseToolResult(value) {
|
|
702
|
+
if (!isRecord(value))
|
|
703
|
+
return undefined;
|
|
704
|
+
const type = stringValue(value.type) ?? stringValue(value.event) ?? stringValue(value.kind);
|
|
705
|
+
if (!type || !/^(tool_result|tool_output|tool_end|tool_error|tool_execution_end)$/i.test(type))
|
|
706
|
+
return undefined;
|
|
707
|
+
const toolUseId = stringValue(value.tool_use_id)
|
|
708
|
+
?? stringValue(value.toolUseId)
|
|
709
|
+
?? stringValue(value.toolCallId)
|
|
710
|
+
?? stringValue(value.parent_tool_use_id)
|
|
711
|
+
?? stringValue(value.id)
|
|
712
|
+
?? stringValue(value.call_id);
|
|
713
|
+
const result = value.result ?? value.output ?? value.content ?? value.error ?? value;
|
|
714
|
+
return { toolUseId, result };
|
|
715
|
+
}
|
|
716
|
+
function collectContentBlocks(value) {
|
|
717
|
+
const blocks = [];
|
|
718
|
+
const visit = (candidate) => {
|
|
719
|
+
if (Array.isArray(candidate)) {
|
|
720
|
+
blocks.push(...candidate);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (!isRecord(candidate))
|
|
724
|
+
return;
|
|
725
|
+
visit(candidate.content);
|
|
726
|
+
visit(candidate.message);
|
|
727
|
+
visit(candidate.delta);
|
|
728
|
+
};
|
|
729
|
+
visit(value);
|
|
730
|
+
return blocks.filter((block) => block !== value);
|
|
731
|
+
}
|
|
732
|
+
function hasToolCallBlock(value) {
|
|
733
|
+
return collectContentBlocks(value).some((block) => {
|
|
734
|
+
if (!isRecord(block))
|
|
735
|
+
return false;
|
|
736
|
+
const type = stringValue(block.type);
|
|
737
|
+
return Boolean(type && /^(toolCall|tool_call|tool_use)$/i.test(type));
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
function normalizeVisibleOutput(text) {
|
|
741
|
+
if (!text)
|
|
742
|
+
return undefined;
|
|
743
|
+
const withoutThinking = text
|
|
744
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
|
745
|
+
.replace(/<think>[\s\S]*$/gi, '')
|
|
746
|
+
.trim();
|
|
747
|
+
return withoutThinking || undefined;
|
|
748
|
+
}
|
|
749
|
+
function readSessionId(value) {
|
|
750
|
+
if (!isRecord(value))
|
|
751
|
+
return undefined;
|
|
752
|
+
return stringValue(value.sessionId)
|
|
753
|
+
?? stringValue(value.session_id)
|
|
754
|
+
?? stringValue(value.conversation_id)
|
|
755
|
+
?? stringValue(value.thread_id);
|
|
756
|
+
}
|
|
757
|
+
function readUsage(value) {
|
|
758
|
+
if (!isRecord(value))
|
|
759
|
+
return undefined;
|
|
760
|
+
const usage = isRecord(value.usage) ? value.usage : value;
|
|
761
|
+
const inputTokens = numberValue(usage.inputTokens) ?? numberValue(usage.input_tokens) ?? numberValue(usage.prompt_tokens);
|
|
762
|
+
const outputTokens = numberValue(usage.outputTokens) ?? numberValue(usage.output_tokens) ?? numberValue(usage.completion_tokens);
|
|
763
|
+
const totalTokens = numberValue(usage.totalTokens) ?? numberValue(usage.total_tokens);
|
|
764
|
+
const cachedInputTokens = numberValue(usage.cachedInputTokens)
|
|
765
|
+
?? numberValue(usage.cache_read_input_tokens)
|
|
766
|
+
?? numberValue(usage.cached_tokens);
|
|
767
|
+
const reasoningTokens = numberValue(usage.reasoningTokens)
|
|
768
|
+
?? numberValue(usage.reasoning_tokens)
|
|
769
|
+
?? numberValue(usage.reasoning_output_tokens);
|
|
770
|
+
if (inputTokens === undefined
|
|
771
|
+
&& outputTokens === undefined
|
|
772
|
+
&& totalTokens === undefined
|
|
773
|
+
&& cachedInputTokens === undefined
|
|
774
|
+
&& reasoningTokens === undefined)
|
|
775
|
+
return undefined;
|
|
776
|
+
return {
|
|
777
|
+
inputTokens,
|
|
778
|
+
outputTokens,
|
|
779
|
+
totalTokens,
|
|
780
|
+
cachedInputTokens,
|
|
781
|
+
reasoningTokens,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
function readCostUsd(value) {
|
|
785
|
+
if (!isRecord(value))
|
|
786
|
+
return undefined;
|
|
787
|
+
return numberValue(value.costUsd) ?? numberValue(value.cost_usd) ?? numberValue(value.total_cost_usd);
|
|
788
|
+
}
|
|
789
|
+
function formatToolUseLine(toolUse) {
|
|
790
|
+
const input = toolUse.input === undefined ? '' : ` ${truncateForLog(stableStringify(toolUse.input), 800)}`;
|
|
791
|
+
return `Tool: ${toolUse.name}${input}`;
|
|
792
|
+
}
|
|
793
|
+
function summarizeBlockTypes(blocks) {
|
|
794
|
+
if (!blocks.length)
|
|
795
|
+
return '-';
|
|
796
|
+
return blocks
|
|
797
|
+
.map((block) => isRecord(block) ? stringValue(block.type) ?? typeof block : typeof block)
|
|
798
|
+
.slice(0, 12)
|
|
799
|
+
.join(',');
|
|
800
|
+
}
|
|
801
|
+
function summarizeUnknown(value) {
|
|
802
|
+
return truncateForLog(stableStringify(value), 240);
|
|
803
|
+
}
|
|
804
|
+
function stableStringify(value) {
|
|
805
|
+
if (typeof value === 'string')
|
|
806
|
+
return value;
|
|
807
|
+
try {
|
|
808
|
+
return JSON.stringify(value);
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
return String(value);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
function uniqueBy(items, key) {
|
|
815
|
+
const seen = new Set();
|
|
816
|
+
return items.filter((item) => {
|
|
817
|
+
const value = key(item);
|
|
818
|
+
if (seen.has(value))
|
|
819
|
+
return false;
|
|
820
|
+
seen.add(value);
|
|
821
|
+
return true;
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
function isRecord(value) {
|
|
825
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
826
|
+
}
|
|
827
|
+
function stringValue(value) {
|
|
828
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
829
|
+
}
|
|
830
|
+
function numberValue(value) {
|
|
831
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
832
|
+
}
|
|
833
|
+
function truncateForLog(value, maxLength = 300) {
|
|
834
|
+
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
|
|
835
|
+
}
|
|
836
|
+
function flushLine(buffer, output, options, prefix = '') {
|
|
837
|
+
const line = stripAnsi(buffer).trimEnd();
|
|
838
|
+
if (line) {
|
|
839
|
+
const formatted = `${prefix}${line}`;
|
|
840
|
+
output.push(formatted);
|
|
841
|
+
options?.onEvent?.({ type: 'output', line: formatted });
|
|
842
|
+
}
|
|
843
|
+
return '';
|
|
844
|
+
}
|
|
845
|
+
function extractSessionId(line) {
|
|
846
|
+
const match = line.match(/\bsession(?:\s+id)?\s*[:=]\s*([a-zA-Z0-9._:/-]+)/i);
|
|
847
|
+
return match?.[1];
|
|
848
|
+
}
|
|
849
|
+
function lastMeaningfulLine(output) {
|
|
850
|
+
return [...output].reverse().find((line) => line.trim() && !line.startsWith('[stderr]')) ?? '';
|
|
851
|
+
}
|
|
852
|
+
function stripAnsi(value) {
|
|
853
|
+
return value.replace(/\u001b\[[0-9;]*m/g, '');
|
|
854
|
+
}
|
|
855
|
+
function removeUndefined(env) {
|
|
856
|
+
return Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== undefined));
|
|
857
|
+
}
|
|
858
|
+
//# sourceMappingURL=oh-my-pi-runtime.js.map
|