@aitherium/shell-cli 1.1.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/auth.d.ts +68 -0
- package/dist/auth.js +288 -0
- package/dist/client.d.ts +116 -0
- package/dist/client.js +528 -0
- package/dist/command-registry.d.ts +71 -0
- package/dist/command-registry.js +223 -0
- package/dist/commands.d.ts +14 -0
- package/dist/commands.js +6785 -0
- package/dist/completions.d.ts +27 -0
- package/dist/completions.js +351 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +48 -0
- package/dist/gargbot.d.ts +11 -0
- package/dist/gargbot.js +230 -0
- package/dist/jobs.d.ts +65 -0
- package/dist/jobs.js +386 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +389 -0
- package/dist/notebooks.d.ts +19 -0
- package/dist/notebooks.js +685 -0
- package/dist/products.d.ts +12 -0
- package/dist/products.js +159 -0
- package/dist/renderer.d.ts +104 -0
- package/dist/renderer.js +1812 -0
- package/dist/repl.d.ts +16 -0
- package/dist/repl.js +1190 -0
- package/dist/session-store.d.ts +35 -0
- package/dist/session-store.js +153 -0
- package/dist/tunnel.d.ts +13 -0
- package/dist/tunnel.js +169 -0
- package/package.json +36 -0
package/dist/gargbot.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GargBot CLI commands — chat, ingest, search, status, and Obsidian sync.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* aither gargbot chat "What projects did we do in Texas?"
|
|
6
|
+
* aither gargbot ingest ./resumes/john_doe.pdf
|
|
7
|
+
* aither gargbot search "bridge engineers with PE"
|
|
8
|
+
* aither gargbot status
|
|
9
|
+
* aither gargbot sync-obsidian /path/to/vault
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
function getGargbotUrl() {
|
|
15
|
+
// 1. Environment variable
|
|
16
|
+
if (process.env.GARGBOT_URL)
|
|
17
|
+
return process.env.GARGBOT_URL;
|
|
18
|
+
// 2. Config file ~/.aither/gargbot.json
|
|
19
|
+
const configPath = path.join(os.homedir(), '.aither', 'gargbot.json');
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(configPath)) {
|
|
22
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
23
|
+
if (cfg.url)
|
|
24
|
+
return cfg.url;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch { /* fallthrough */ }
|
|
28
|
+
// 3. Default
|
|
29
|
+
return 'http://localhost:8900';
|
|
30
|
+
}
|
|
31
|
+
async function gargGet(endpoint) {
|
|
32
|
+
const url = `${getGargbotUrl()}${endpoint}`;
|
|
33
|
+
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
36
|
+
return res.json();
|
|
37
|
+
}
|
|
38
|
+
async function gargPost(endpoint, body) {
|
|
39
|
+
const url = `${getGargbotUrl()}${endpoint}`;
|
|
40
|
+
const res = await fetch(url, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
43
|
+
body: JSON.stringify(body),
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok)
|
|
46
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
47
|
+
return res.json();
|
|
48
|
+
}
|
|
49
|
+
async function gargUpload(endpoint, filePath, fields = {}) {
|
|
50
|
+
const url = `${getGargbotUrl()}${endpoint}`;
|
|
51
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
52
|
+
const fileName = path.basename(filePath);
|
|
53
|
+
// Build multipart form data manually for Node fetch
|
|
54
|
+
const boundary = '----AitherShellBoundary' + Date.now().toString(36);
|
|
55
|
+
let body = '';
|
|
56
|
+
for (const [key, val] of Object.entries(fields)) {
|
|
57
|
+
body += `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${val}\r\n`;
|
|
58
|
+
}
|
|
59
|
+
body += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`;
|
|
60
|
+
const ending = `\r\n--${boundary}--\r\n`;
|
|
61
|
+
const bodyBuffer = Buffer.concat([
|
|
62
|
+
Buffer.from(body, 'utf-8'),
|
|
63
|
+
fileBuffer,
|
|
64
|
+
Buffer.from(ending, 'utf-8'),
|
|
65
|
+
]);
|
|
66
|
+
const res = await fetch(url, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` },
|
|
69
|
+
body: bodyBuffer,
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok)
|
|
72
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
73
|
+
return res.json();
|
|
74
|
+
}
|
|
75
|
+
// ── Subcommand handlers ──────────────────────────────────────────────────
|
|
76
|
+
async function handleChat(args) {
|
|
77
|
+
const message = args.join(' ');
|
|
78
|
+
if (!message) {
|
|
79
|
+
console.log('\x1b[33mUsage:\x1b[0m aither gargbot chat <message>');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
console.log('\x1b[2mThinking...\x1b[0m');
|
|
83
|
+
try {
|
|
84
|
+
const data = await gargPost('/api/chat', { message });
|
|
85
|
+
console.log(`\n\x1b[36mGargBot:\x1b[0m ${data.response}`);
|
|
86
|
+
if (data.sources?.length > 0) {
|
|
87
|
+
console.log(`\n\x1b[2mSources: ${data.sources.map((s) => s.filename).join(', ')}\x1b[0m`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function handleIngest(args) {
|
|
95
|
+
const target = args[0];
|
|
96
|
+
// If no args and stdin is piped, read from stdin
|
|
97
|
+
if (!target && !process.stdin.isTTY) {
|
|
98
|
+
const chunks = [];
|
|
99
|
+
for await (const chunk of process.stdin) {
|
|
100
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
101
|
+
}
|
|
102
|
+
const text = Buffer.concat(chunks).toString('utf-8').trim();
|
|
103
|
+
if (!text) {
|
|
104
|
+
console.error('\x1b[31mNo input received from stdin\x1b[0m');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.log('\x1b[2mIngesting from stdin...\x1b[0m');
|
|
108
|
+
try {
|
|
109
|
+
const data = await gargPost('/api/documents/ingest', {
|
|
110
|
+
text,
|
|
111
|
+
filename: `stdin-${Date.now()}`,
|
|
112
|
+
doc_type: 'other',
|
|
113
|
+
});
|
|
114
|
+
console.log(`\x1b[32m✓\x1b[0m Ingested stdin (${data.chunks_created || '?'} chunks)`);
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!target) {
|
|
122
|
+
console.log('\x1b[33mUsage:\x1b[0m aither gargbot ingest <file-or-url>');
|
|
123
|
+
console.log(' echo "text" | aither gargbot ingest');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// URL detection
|
|
127
|
+
if (target.startsWith('http://') || target.startsWith('https://')) {
|
|
128
|
+
console.log(`\x1b[2mIngesting URL: ${target}...\x1b[0m`);
|
|
129
|
+
try {
|
|
130
|
+
const data = await gargPost('/api/documents/ingest-url', { url: target, auto_extract: true });
|
|
131
|
+
console.log(`\x1b[32m✓\x1b[0m Ingested: ${data.title || target} (${data.chunk_count || '?'} chunks)`);
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// File path
|
|
139
|
+
const resolved = path.resolve(target);
|
|
140
|
+
if (!fs.existsSync(resolved)) {
|
|
141
|
+
console.error(`\x1b[31mFile not found:\x1b[0m ${resolved}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
console.log(`\x1b[2mIngesting ${path.basename(resolved)}...\x1b[0m`);
|
|
145
|
+
try {
|
|
146
|
+
const data = await gargUpload('/api/documents/upload', resolved, { auto_extract: 'true' });
|
|
147
|
+
console.log(`\x1b[32m✓\x1b[0m Ingested: ${data.filename || path.basename(resolved)} (${data.chunk_count || data.chunks_created || '?'} chunks)`);
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function handleSearch(args) {
|
|
154
|
+
const query = args.join(' ');
|
|
155
|
+
if (!query) {
|
|
156
|
+
console.log('\x1b[33mUsage:\x1b[0m aither gargbot search <query>');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const data = await gargPost('/api/chat', { message: `Search: ${query}` });
|
|
161
|
+
console.log(`\n\x1b[36mResults:\x1b[0m ${data.response}`);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function handleStatus(_args) {
|
|
168
|
+
try {
|
|
169
|
+
const health = await gargGet('/api/health');
|
|
170
|
+
const stats = await gargGet('/api/stats');
|
|
171
|
+
console.log(`\x1b[36mGargBot Status\x1b[0m`);
|
|
172
|
+
console.log(` Service: ${health.service}`);
|
|
173
|
+
console.log(` Provider: ${health.llm_provider}`);
|
|
174
|
+
console.log(` Mode: ${health.deployment_mode}`);
|
|
175
|
+
console.log(` Documents: ${stats.documents}`);
|
|
176
|
+
console.log(` Staff: ${stats.staff}`);
|
|
177
|
+
console.log(` Projects: ${stats.projects}`);
|
|
178
|
+
console.log(` Feedback: ${stats.feedback_entries}`);
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
console.error(`\x1b[31mCannot reach GargBot:\x1b[0m ${e.message}`);
|
|
182
|
+
console.log(` URL: ${getGargbotUrl()}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function handleSyncObsidian(args) {
|
|
186
|
+
const vaultPath = args[0];
|
|
187
|
+
if (!vaultPath) {
|
|
188
|
+
console.log('\x1b[33mUsage:\x1b[0m aither gargbot sync-obsidian <vault-path>');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.log(`\x1b[2mSyncing vault: ${vaultPath}...\x1b[0m`);
|
|
192
|
+
try {
|
|
193
|
+
const data = await gargPost('/api/obsidian/sync', { vault_path: vaultPath });
|
|
194
|
+
console.log(`\x1b[32m✓\x1b[0m Synced: ${data.total_files} files (${data.ingested} new, ${data.skipped} unchanged)`);
|
|
195
|
+
if (data.error_count > 0) {
|
|
196
|
+
console.log(`\x1b[33m ⚠ ${data.error_count} errors\x1b[0m`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
console.error(`\x1b[31mError:\x1b[0m ${e.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// ── Main dispatcher ──────────────────────────────────────────────────────
|
|
204
|
+
export async function handleGargbotCommand(args) {
|
|
205
|
+
const subcommand = args[0] || 'help';
|
|
206
|
+
const subArgs = args.slice(1);
|
|
207
|
+
switch (subcommand) {
|
|
208
|
+
case 'chat':
|
|
209
|
+
return handleChat(subArgs);
|
|
210
|
+
case 'ingest':
|
|
211
|
+
return handleIngest(subArgs);
|
|
212
|
+
case 'search':
|
|
213
|
+
return handleSearch(subArgs);
|
|
214
|
+
case 'status':
|
|
215
|
+
return handleStatus(subArgs);
|
|
216
|
+
case 'sync-obsidian':
|
|
217
|
+
case 'sync':
|
|
218
|
+
return handleSyncObsidian(subArgs);
|
|
219
|
+
case 'help':
|
|
220
|
+
default:
|
|
221
|
+
console.log(`\x1b[36mGargBot CLI\x1b[0m — Knowledge management commands\n`);
|
|
222
|
+
console.log(' aither gargbot chat <message> Chat with GargBot');
|
|
223
|
+
console.log(' aither gargbot ingest <file-or-url> Ingest a document or URL');
|
|
224
|
+
console.log(' echo "text" | aither gargbot ingest Ingest from stdin');
|
|
225
|
+
console.log(' aither gargbot search <query> Search knowledge base');
|
|
226
|
+
console.log(' aither gargbot status Show health & stats');
|
|
227
|
+
console.log(' aither gargbot sync-obsidian <path> Sync Obsidian vault');
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
package/dist/jobs.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Job Manager for AitherShell.
|
|
3
|
+
*
|
|
4
|
+
* Allows long-running tasks (forge, swarm, chat with agents) to execute
|
|
5
|
+
* in the background while the REPL remains interactive. Jobs collect their
|
|
6
|
+
* output silently and notify the shell on completion.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: Background jobs NEVER write to stdout/stderr directly.
|
|
9
|
+
* All output is collected into job.output[] and read via /jobs <id>.
|
|
10
|
+
* This prevents background tasks from corrupting the interactive prompt.
|
|
11
|
+
*/
|
|
12
|
+
import type { GenesisClient, StreamChatOpts } from './client.js';
|
|
13
|
+
export type JobStatus = 'running' | 'completed' | 'failed' | 'cancelled';
|
|
14
|
+
export type JobKind = 'chat' | 'forge' | 'swarm' | 'command';
|
|
15
|
+
export interface Job {
|
|
16
|
+
id: number;
|
|
17
|
+
kind: JobKind;
|
|
18
|
+
label: string;
|
|
19
|
+
status: JobStatus;
|
|
20
|
+
startedAt: Date;
|
|
21
|
+
finishedAt: Date | null;
|
|
22
|
+
output: string[];
|
|
23
|
+
error: string | null;
|
|
24
|
+
abortController: AbortController | null;
|
|
25
|
+
}
|
|
26
|
+
type NotifyFn = (job: Job) => void;
|
|
27
|
+
/** Register a callback that fires when any background job completes. */
|
|
28
|
+
export declare function setJobNotifier(fn: NotifyFn): void;
|
|
29
|
+
/** Get all jobs (most recent first). */
|
|
30
|
+
export declare function listJobs(): Job[];
|
|
31
|
+
/** Get a single job by ID. */
|
|
32
|
+
export declare function getJob(id: number): Job | undefined;
|
|
33
|
+
/** Get count of currently running jobs. */
|
|
34
|
+
export declare function runningCount(): number;
|
|
35
|
+
/** Cancel a running job. */
|
|
36
|
+
export declare function cancelJob(id: number): boolean;
|
|
37
|
+
/** Prune completed/failed/cancelled jobs older than `maxAge` ms (default 30 min). */
|
|
38
|
+
export declare function pruneJobs(maxAge?: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* Launch a streaming chat in the background.
|
|
41
|
+
* Returns the job immediately; the stream runs asynchronously.
|
|
42
|
+
*
|
|
43
|
+
* Consumes the SSE stream silently — no stdout writes, no spinners.
|
|
44
|
+
* Content and trace info are collected into job.output[].
|
|
45
|
+
*/
|
|
46
|
+
export declare function launchChatJob(client: GenesisClient, message: string, opts?: StreamChatOpts & {
|
|
47
|
+
label?: string;
|
|
48
|
+
}): Job;
|
|
49
|
+
export declare function launchForgeJob(client: GenesisClient, task: string, opts?: {
|
|
50
|
+
agent?: string;
|
|
51
|
+
effort?: number;
|
|
52
|
+
}): Job;
|
|
53
|
+
export declare function launchSwarmJob(client: GenesisClient, task: string, mode?: string): Job;
|
|
54
|
+
/**
|
|
55
|
+
* Run an arbitrary async function as a background job.
|
|
56
|
+
* Replaces console.log within the function scope to capture output.
|
|
57
|
+
*
|
|
58
|
+
* WARNING: This uses global console.log replacement, so only ONE
|
|
59
|
+
* command job should run at a time. For concurrent background work,
|
|
60
|
+
* use the specific launchers (chat, forge, swarm) instead.
|
|
61
|
+
*/
|
|
62
|
+
export declare function launchCommandJob(label: string, fn: () => Promise<void>): Job;
|
|
63
|
+
export declare function formatJobLine(job: Job): string;
|
|
64
|
+
export declare function formatJobOutput(job: Job): string;
|
|
65
|
+
export {};
|
package/dist/jobs.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Job Manager for AitherShell.
|
|
3
|
+
*
|
|
4
|
+
* Allows long-running tasks (forge, swarm, chat with agents) to execute
|
|
5
|
+
* in the background while the REPL remains interactive. Jobs collect their
|
|
6
|
+
* output silently and notify the shell on completion.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: Background jobs NEVER write to stdout/stderr directly.
|
|
9
|
+
* All output is collected into job.output[] and read via /jobs <id>.
|
|
10
|
+
* This prevents background tasks from corrupting the interactive prompt.
|
|
11
|
+
*/
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
let nextJobId = 1;
|
|
14
|
+
const jobs = new Map();
|
|
15
|
+
let onJobDone = null;
|
|
16
|
+
/** Register a callback that fires when any background job completes. */
|
|
17
|
+
export function setJobNotifier(fn) {
|
|
18
|
+
onJobDone = fn;
|
|
19
|
+
}
|
|
20
|
+
/** Get all jobs (most recent first). */
|
|
21
|
+
export function listJobs() {
|
|
22
|
+
return [...jobs.values()].reverse();
|
|
23
|
+
}
|
|
24
|
+
/** Get a single job by ID. */
|
|
25
|
+
export function getJob(id) {
|
|
26
|
+
return jobs.get(id);
|
|
27
|
+
}
|
|
28
|
+
/** Get count of currently running jobs. */
|
|
29
|
+
export function runningCount() {
|
|
30
|
+
let count = 0;
|
|
31
|
+
for (const j of jobs.values()) {
|
|
32
|
+
if (j.status === 'running')
|
|
33
|
+
count++;
|
|
34
|
+
}
|
|
35
|
+
return count;
|
|
36
|
+
}
|
|
37
|
+
/** Cancel a running job. */
|
|
38
|
+
export function cancelJob(id) {
|
|
39
|
+
const job = jobs.get(id);
|
|
40
|
+
if (!job || job.status !== 'running')
|
|
41
|
+
return false;
|
|
42
|
+
if (job.abortController) {
|
|
43
|
+
job.abortController.abort();
|
|
44
|
+
}
|
|
45
|
+
job.status = 'cancelled';
|
|
46
|
+
job.finishedAt = new Date();
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/** Prune completed/failed/cancelled jobs older than `maxAge` ms (default 30 min). */
|
|
50
|
+
export function pruneJobs(maxAge = 30 * 60 * 1000) {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
let pruned = 0;
|
|
53
|
+
for (const [id, job] of jobs) {
|
|
54
|
+
if (job.status !== 'running' && job.finishedAt && now - job.finishedAt.getTime() > maxAge) {
|
|
55
|
+
jobs.delete(id);
|
|
56
|
+
pruned++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return pruned;
|
|
60
|
+
}
|
|
61
|
+
/* ── Job creation helpers ──────────────────────────────────── */
|
|
62
|
+
function createJob(kind, label) {
|
|
63
|
+
const job = {
|
|
64
|
+
id: nextJobId++,
|
|
65
|
+
kind,
|
|
66
|
+
label,
|
|
67
|
+
status: 'running',
|
|
68
|
+
startedAt: new Date(),
|
|
69
|
+
finishedAt: null,
|
|
70
|
+
output: [],
|
|
71
|
+
error: null,
|
|
72
|
+
abortController: new AbortController(),
|
|
73
|
+
};
|
|
74
|
+
jobs.set(job.id, job);
|
|
75
|
+
return job;
|
|
76
|
+
}
|
|
77
|
+
function finishJob(job, status, error) {
|
|
78
|
+
job.status = status;
|
|
79
|
+
job.finishedAt = new Date();
|
|
80
|
+
job.error = error || null;
|
|
81
|
+
job.abortController = null;
|
|
82
|
+
onJobDone?.(job);
|
|
83
|
+
}
|
|
84
|
+
/* ── Silent SSE content extractor ─────────────────────────── */
|
|
85
|
+
/**
|
|
86
|
+
* Extract the final text content from an SSE event stream without
|
|
87
|
+
* writing ANYTHING to stdout. No spinners, no progress — just data.
|
|
88
|
+
*/
|
|
89
|
+
function extractContentFromEvent(event) {
|
|
90
|
+
switch (event.type) {
|
|
91
|
+
case 'token':
|
|
92
|
+
return event.data.t || null;
|
|
93
|
+
case 'message':
|
|
94
|
+
case 'answer':
|
|
95
|
+
case 'final_answer':
|
|
96
|
+
return event.data.response || event.data.answer || event.data.content || null;
|
|
97
|
+
case 'partial':
|
|
98
|
+
return event.data.content || event.data.text || null;
|
|
99
|
+
case 'done':
|
|
100
|
+
case 'complete':
|
|
101
|
+
return event.data.content || null;
|
|
102
|
+
default:
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Extract metadata/trace info from SSE events for the job log.
|
|
108
|
+
*/
|
|
109
|
+
function extractTraceFromEvent(event) {
|
|
110
|
+
switch (event.type) {
|
|
111
|
+
case 'thinking': {
|
|
112
|
+
const thought = event.data.thought || event.data.content || event.data.phase || '';
|
|
113
|
+
const clean = thought.replace(/<\/?think(?:ing)?>/g, '').trim();
|
|
114
|
+
if (clean.length > 10)
|
|
115
|
+
return `[think] ${clean.slice(0, 200)}`;
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
case 'tool_call': {
|
|
119
|
+
const tools = event.data.tools || event.data.tool_calls || [];
|
|
120
|
+
const names = tools.map((t) => t.name || t.function?.name || 'tool');
|
|
121
|
+
return names.length ? `[tools] ${names.join(', ')}` : null;
|
|
122
|
+
}
|
|
123
|
+
case 'tool_result': {
|
|
124
|
+
const results = event.data.results || [];
|
|
125
|
+
const summary = results.map((r) => {
|
|
126
|
+
const icon = r.success !== false ? '+' : 'x';
|
|
127
|
+
return `${icon}${r.tool || 'tool'}`;
|
|
128
|
+
});
|
|
129
|
+
return summary.length ? `[result] ${summary.join(', ')}` : null;
|
|
130
|
+
}
|
|
131
|
+
case 'plan_ready':
|
|
132
|
+
return event.data.summary ? `[plan] ${event.data.summary}` : null;
|
|
133
|
+
case 'error':
|
|
134
|
+
return `[error] ${event.data.error || 'unknown'}`;
|
|
135
|
+
case 'classify': {
|
|
136
|
+
const intent = event.data.intent?.type || '?';
|
|
137
|
+
const effort = event.data.effort?.level || '?';
|
|
138
|
+
return `[classify] intent=${intent} effort=${effort}`;
|
|
139
|
+
}
|
|
140
|
+
case 'classify_update': {
|
|
141
|
+
const uIntent = event.data.intent?.type || '?';
|
|
142
|
+
const uEffort = event.data.effort?.level || '?';
|
|
143
|
+
const uReason = event.data.reason || 'context';
|
|
144
|
+
return `[classify_update] intent=${uIntent} effort=${uEffort} reason=${uReason}`;
|
|
145
|
+
}
|
|
146
|
+
default:
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/* ── Background chat ───────────────────────────────────────── */
|
|
151
|
+
/**
|
|
152
|
+
* Launch a streaming chat in the background.
|
|
153
|
+
* Returns the job immediately; the stream runs asynchronously.
|
|
154
|
+
*
|
|
155
|
+
* Consumes the SSE stream silently — no stdout writes, no spinners.
|
|
156
|
+
* Content and trace info are collected into job.output[].
|
|
157
|
+
*/
|
|
158
|
+
export function launchChatJob(client, message, opts = {}) {
|
|
159
|
+
const label = opts.label || `Chat: ${message.slice(0, 50)}${message.length > 50 ? '...' : ''}`;
|
|
160
|
+
const job = createJob('chat', label);
|
|
161
|
+
const run = async () => {
|
|
162
|
+
const contentParts = [];
|
|
163
|
+
let fullAnswer = '';
|
|
164
|
+
// Each background chat gets its own session ID so concurrent jobs
|
|
165
|
+
// don't clobber each other's context on Genesis.
|
|
166
|
+
const bgSessionId = `${opts.sessionId || 'shell'}-bg-${job.id}-${Date.now().toString(36)}`;
|
|
167
|
+
try {
|
|
168
|
+
const stream = client.streamChat(message, {
|
|
169
|
+
...opts,
|
|
170
|
+
sessionId: bgSessionId,
|
|
171
|
+
priority: 'background',
|
|
172
|
+
signal: job.abortController.signal,
|
|
173
|
+
});
|
|
174
|
+
for await (const event of stream) {
|
|
175
|
+
// Collect content tokens
|
|
176
|
+
const content = extractContentFromEvent(event);
|
|
177
|
+
if (content) {
|
|
178
|
+
// Token events are incremental, answer/message events are full text
|
|
179
|
+
if (event.type === 'token') {
|
|
180
|
+
contentParts.push(content);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
fullAnswer = content;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Collect trace info for the job log
|
|
187
|
+
const trace = extractTraceFromEvent(event);
|
|
188
|
+
if (trace) {
|
|
189
|
+
job.output.push(trace);
|
|
190
|
+
}
|
|
191
|
+
// Extract metadata from done/complete
|
|
192
|
+
if (event.type === 'done' || event.type === 'complete') {
|
|
193
|
+
const meta = [];
|
|
194
|
+
if (event.data.model || event.data.model_used)
|
|
195
|
+
meta.push(event.data.model || event.data.model_used);
|
|
196
|
+
if (event.data.duration_ms || event.data.elapsed_ms) {
|
|
197
|
+
meta.push(`${Math.round(event.data.duration_ms || event.data.elapsed_ms)}ms`);
|
|
198
|
+
}
|
|
199
|
+
if (event.data.turns_completed)
|
|
200
|
+
meta.push(`${event.data.turns_completed} turns`);
|
|
201
|
+
if (meta.length) {
|
|
202
|
+
job.output.push(`[meta] ${meta.join(' | ')}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Use full answer if available, otherwise join tokens
|
|
207
|
+
const finalContent = fullAnswer || contentParts.join('');
|
|
208
|
+
if (finalContent) {
|
|
209
|
+
job.output.push('---'); // separator between trace and content
|
|
210
|
+
job.output.push(finalContent);
|
|
211
|
+
}
|
|
212
|
+
finishJob(job, 'completed');
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
if (err.name === 'AbortError') {
|
|
216
|
+
const partial = fullAnswer || contentParts.join('');
|
|
217
|
+
if (partial) {
|
|
218
|
+
job.output.push('---');
|
|
219
|
+
job.output.push(partial);
|
|
220
|
+
job.output.push('(cancelled — partial output above)');
|
|
221
|
+
}
|
|
222
|
+
finishJob(job, 'cancelled');
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
finishJob(job, 'failed', err.message);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
run().catch((err) => {
|
|
230
|
+
finishJob(job, 'failed', err.message);
|
|
231
|
+
});
|
|
232
|
+
return job;
|
|
233
|
+
}
|
|
234
|
+
/* ── Background forge ──────────────────────────────────────── */
|
|
235
|
+
export function launchForgeJob(client, task, opts = {}) {
|
|
236
|
+
const label = opts.agent
|
|
237
|
+
? `Forge @${opts.agent}: ${task.slice(0, 40)}...`
|
|
238
|
+
: `Forge: ${task.slice(0, 50)}...`;
|
|
239
|
+
const job = createJob('forge', label);
|
|
240
|
+
const run = async () => {
|
|
241
|
+
try {
|
|
242
|
+
const result = await client.forgeDispatch(task, opts);
|
|
243
|
+
if (result?.error) {
|
|
244
|
+
job.output.push(result.error);
|
|
245
|
+
finishJob(job, 'failed', result.error);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const output = result?.response || result?.result || result?.output;
|
|
249
|
+
job.output.push(output || JSON.stringify(result, null, 2));
|
|
250
|
+
finishJob(job, 'completed');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
finishJob(job, 'failed', err.message);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
run().catch((err) => finishJob(job, 'failed', err.message));
|
|
258
|
+
return job;
|
|
259
|
+
}
|
|
260
|
+
/* ── Background swarm ──────────────────────────────────────── */
|
|
261
|
+
export function launchSwarmJob(client, task, mode = 'llm') {
|
|
262
|
+
const label = `Swarm (${mode}): ${task.slice(0, 40)}...`;
|
|
263
|
+
const job = createJob('swarm', label);
|
|
264
|
+
const run = async () => {
|
|
265
|
+
try {
|
|
266
|
+
const result = await client.post('/swarm/code/sync', { problem: task, mode });
|
|
267
|
+
if (result?.error) {
|
|
268
|
+
job.output.push(result.error);
|
|
269
|
+
finishJob(job, 'failed', result.error);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
const output = result?.result || result?.plan || result?.response;
|
|
273
|
+
job.output.push(typeof output === 'string' ? output : JSON.stringify(output, null, 2));
|
|
274
|
+
finishJob(job, 'completed');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
finishJob(job, 'failed', err.message);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
run().catch((err) => finishJob(job, 'failed', err.message));
|
|
282
|
+
return job;
|
|
283
|
+
}
|
|
284
|
+
/* ── Background command ────────────────────────────────────── */
|
|
285
|
+
/**
|
|
286
|
+
* Run an arbitrary async function as a background job.
|
|
287
|
+
* Replaces console.log within the function scope to capture output.
|
|
288
|
+
*
|
|
289
|
+
* WARNING: This uses global console.log replacement, so only ONE
|
|
290
|
+
* command job should run at a time. For concurrent background work,
|
|
291
|
+
* use the specific launchers (chat, forge, swarm) instead.
|
|
292
|
+
*/
|
|
293
|
+
export function launchCommandJob(label, fn) {
|
|
294
|
+
const job = createJob('command', label);
|
|
295
|
+
const run = async () => {
|
|
296
|
+
const origLog = console.log;
|
|
297
|
+
const origError = console.error;
|
|
298
|
+
const origWarn = console.warn;
|
|
299
|
+
const origWrite = process.stdout.write;
|
|
300
|
+
const capture = (...args) => {
|
|
301
|
+
const line = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
|
|
302
|
+
job.output.push(line);
|
|
303
|
+
};
|
|
304
|
+
const captureWrite = (chunk) => {
|
|
305
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
306
|
+
if (text.trim())
|
|
307
|
+
job.output.push(text);
|
|
308
|
+
return true;
|
|
309
|
+
};
|
|
310
|
+
// Replace all output channels
|
|
311
|
+
console.log = capture;
|
|
312
|
+
console.error = capture;
|
|
313
|
+
console.warn = capture;
|
|
314
|
+
process.stdout.write = captureWrite;
|
|
315
|
+
try {
|
|
316
|
+
await fn();
|
|
317
|
+
finishJob(job, 'completed');
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
finishJob(job, 'failed', err.message);
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
// Always restore — even if the fn throws
|
|
324
|
+
console.log = origLog;
|
|
325
|
+
console.error = origError;
|
|
326
|
+
console.warn = origWarn;
|
|
327
|
+
process.stdout.write = origWrite;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
run().catch((err) => {
|
|
331
|
+
finishJob(job, 'failed', err.message);
|
|
332
|
+
});
|
|
333
|
+
return job;
|
|
334
|
+
}
|
|
335
|
+
/* ── Formatting helpers ────────────────────────────────────── */
|
|
336
|
+
export function formatJobLine(job) {
|
|
337
|
+
const icons = {
|
|
338
|
+
running: chalk.blue('\u25B6'),
|
|
339
|
+
completed: chalk.green('\u2713'),
|
|
340
|
+
failed: chalk.red('\u2717'),
|
|
341
|
+
cancelled: chalk.yellow('\u2015'),
|
|
342
|
+
};
|
|
343
|
+
const icon = icons[job.status];
|
|
344
|
+
const id = chalk.bold(`#${job.id}`);
|
|
345
|
+
const elapsed = formatElapsed(job);
|
|
346
|
+
const status = job.status === 'running'
|
|
347
|
+
? chalk.blue(job.status)
|
|
348
|
+
: job.status === 'completed'
|
|
349
|
+
? chalk.green(job.status)
|
|
350
|
+
: job.status === 'failed'
|
|
351
|
+
? chalk.red(job.status)
|
|
352
|
+
: chalk.yellow(job.status);
|
|
353
|
+
return ` ${icon} ${id} ${status} ${chalk.dim(elapsed)} ${job.label}`;
|
|
354
|
+
}
|
|
355
|
+
function formatElapsed(job) {
|
|
356
|
+
const end = job.finishedAt || new Date();
|
|
357
|
+
const ms = end.getTime() - job.startedAt.getTime();
|
|
358
|
+
if (ms < 1000)
|
|
359
|
+
return `${ms}ms`;
|
|
360
|
+
if (ms < 60_000)
|
|
361
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
362
|
+
return `${Math.floor(ms / 60_000)}m${Math.round((ms % 60_000) / 1000)}s`;
|
|
363
|
+
}
|
|
364
|
+
export function formatJobOutput(job) {
|
|
365
|
+
const lines = [];
|
|
366
|
+
lines.push(chalk.bold(`\n Job #${job.id}: ${job.label}`));
|
|
367
|
+
lines.push(chalk.dim(` Status: ${job.status} | Started: ${job.startedAt.toLocaleTimeString()}`));
|
|
368
|
+
if (job.finishedAt) {
|
|
369
|
+
lines.push(chalk.dim(` Finished: ${job.finishedAt.toLocaleTimeString()} (${formatElapsed(job)})`));
|
|
370
|
+
}
|
|
371
|
+
if (job.error) {
|
|
372
|
+
lines.push(chalk.red(` Error: ${job.error}`));
|
|
373
|
+
}
|
|
374
|
+
if (job.output.length > 0) {
|
|
375
|
+
lines.push('');
|
|
376
|
+
const outputLines = job.output.join('\n').split('\n');
|
|
377
|
+
for (const line of outputLines) {
|
|
378
|
+
lines.push(` ${line}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else if (job.status === 'running') {
|
|
382
|
+
lines.push(chalk.dim(' (no output yet)'));
|
|
383
|
+
}
|
|
384
|
+
lines.push('');
|
|
385
|
+
return lines.join('\n');
|
|
386
|
+
}
|
package/dist/main.d.ts
ADDED