@parallel-cli/parallel 0.3.3

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/index.js ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { render } from 'ink';
7
+ import { App } from './ui/App.js';
8
+ import { Controller } from './controller.js';
9
+ import { loadConfig, setConfigHome } from './config.js';
10
+ import { setLang } from './i18n.js';
11
+ const argv = process.argv.slice(2);
12
+ function takeFlagValue(flag) {
13
+ const i = argv.indexOf(flag);
14
+ if (i === -1)
15
+ return undefined;
16
+ const value = argv[i + 1];
17
+ argv.splice(i, value && !value.startsWith('-') ? 2 : 1);
18
+ return value && !value.startsWith('-') ? value : undefined;
19
+ }
20
+ const firstRun = argv.includes('--first-run');
21
+ if (firstRun)
22
+ argv.splice(argv.indexOf('--first-run'), 1);
23
+ const headless = argv.includes('--headless');
24
+ if (headless)
25
+ argv.splice(argv.indexOf('--headless'), 1);
26
+ const jsonOut = argv.includes('--json');
27
+ if (jsonOut)
28
+ argv.splice(argv.indexOf('--json'), 1);
29
+ const configHome = takeFlagValue('--config-home');
30
+ if (argv.includes('--help') || argv.includes('-h')) {
31
+ console.log(`⚡ Parallel — real-time parallel coding agents.
32
+
33
+ Usage:
34
+ parallel [folder] Start the TUI (wizard only if first setup is incomplete)
35
+ prl [folder] Short alias
36
+ parallel attach <agent> [--root <dir>]
37
+ Open a DEDICATED terminal view on one agent of a
38
+ running session (native scrollback + live steering)
39
+ parallel --first-run Test the first-run wizard with a temporary config home
40
+ parallel --config-home <dir> [folder]
41
+ Use <dir>/config.json instead of ~/.parallel/config.json
42
+ parallel --headless "task1" ["task2"…] [--json]
43
+ No TUI: one agent per task in the current folder,
44
+ auto-approved commands, summary (or JSON) on stdout — for CI
45
+
46
+ Environment variables:
47
+ PARALLEL_API_KEY / DEEPSEEK_API_KEY API key
48
+ PARALLEL_MODEL Default model (e.g. deepseek-chat)
49
+ PARALLEL_BASE_URL OpenAI-compatible endpoint
50
+
51
+ Inside the TUI:
52
+ <task> + Enter Launch agent N+1 — even while the others are working
53
+ @a1 <message> Real-time instruction to an agent (@all for everyone)
54
+ /help All commands
55
+ `);
56
+ process.exit(0);
57
+ }
58
+ if (firstRun) {
59
+ setConfigHome(fs.mkdtempSync(path.join(os.tmpdir(), 'parallel-first-run-')));
60
+ }
61
+ else if (configHome) {
62
+ setConfigHome(configHome);
63
+ }
64
+ // ---------- attach mode: dedicated terminal for ONE agent of a running session ----------
65
+ if (argv[0] === 'attach') {
66
+ const root = path.resolve(takeFlagValue('--root') ?? process.cwd());
67
+ const agentRef = argv[1];
68
+ if (!agentRef) {
69
+ console.error('Usage: parallel attach <agent> [--root <dir>]');
70
+ process.exit(1);
71
+ }
72
+ if (!process.stdout.isTTY) {
73
+ console.error('Parallel requires an interactive terminal (TTY).');
74
+ process.exit(1);
75
+ }
76
+ const config = loadConfig();
77
+ if (config.language)
78
+ setLang(config.language);
79
+ const { socketPath } = await import('./server.js');
80
+ const sock = socketPath(root);
81
+ if (!fs.existsSync(sock)) {
82
+ console.error(`No running Parallel session found in ${root} (missing ${sock}).`);
83
+ console.error('Start `parallel` in that folder first, then re-run attach.');
84
+ process.exit(1);
85
+ }
86
+ const { AttachApp } = await import('./ui/AttachApp.js');
87
+ // NO alternate screen here: <Static> writes into the native scrollback,
88
+ // so the user can scroll this agent's history like any terminal output.
89
+ const attachApp = render(_jsx(AttachApp, { agentRef: agentRef, sock: sock }), { exitOnCtrlC: true });
90
+ await attachApp.waitUntilExit();
91
+ process.exit(0);
92
+ }
93
+ // ---------- headless mode (CI / scripts): no TUI, one agent per task ----------
94
+ if (headless) {
95
+ const tasks = argv.filter((a) => !a.startsWith('-'));
96
+ if (tasks.length === 0) {
97
+ console.error('Usage: parallel --headless "task1" ["task2"…] [--json]');
98
+ process.exit(1);
99
+ }
100
+ const config = loadConfig();
101
+ if (config.language)
102
+ setLang(config.language);
103
+ const ctl = new Controller(config, process.cwd());
104
+ // No human in the loop: commands are auto-approved (like /approvals auto).
105
+ ctl.setSessionApprovalMode('auto');
106
+ const provider = ctl.sessionProvider();
107
+ if (!provider || !provider.apiKey) {
108
+ console.error('Headless mode needs a configured provider + API key. Run `parallel` interactively once, or set PARALLEL_API_KEY.');
109
+ process.exit(1);
110
+ }
111
+ // Agent questions cannot be asked: auto-answer with the recommended option.
112
+ ctl.on('update', () => {
113
+ for (const q of [...ctl.questions])
114
+ ctl.answerQuestion(q.id, q.options[q.recommended] ?? '', true);
115
+ });
116
+ for (const task of tasks) {
117
+ if (!ctl.spawnAgent(task)) {
118
+ console.error(`Failed to spawn an agent for: ${task}`);
119
+ process.exit(1);
120
+ }
121
+ }
122
+ const TERMINAL = ['done', 'error', 'stopped'];
123
+ let printed = 0;
124
+ await new Promise((resolve) => {
125
+ const iv = setInterval(() => {
126
+ if (!jsonOut) {
127
+ // Stream the live log to stdout (text mode only).
128
+ const logs = ctl.board.logs;
129
+ for (; printed < logs.length; printed++) {
130
+ const l = logs[printed];
131
+ const who = l.agentId ? (ctl.board.agents.get(l.agentId)?.name ?? l.agentId) : 'system';
132
+ console.log(`[${who}] ${l.text}`);
133
+ }
134
+ }
135
+ const all = [...ctl.board.agents.values()];
136
+ if (all.length > 0 && all.every((a) => TERMINAL.includes(a.state))) {
137
+ clearInterval(iv);
138
+ resolve();
139
+ }
140
+ }, 400);
141
+ });
142
+ ctl.saveSession();
143
+ const agents = [...ctl.board.agents.values()].map((a) => ({
144
+ name: a.name,
145
+ alias: a.alias,
146
+ task: a.task,
147
+ state: a.state,
148
+ steps: a.steps,
149
+ tokensIn: a.tokensIn,
150
+ tokensOut: a.tokensOut,
151
+ cost: a.cost,
152
+ result: a.lastResult ?? null,
153
+ }));
154
+ const changedFiles = [...new Set(ctl.board.changes.map((c) => c.path))];
155
+ if (jsonOut) {
156
+ console.log(JSON.stringify({ agents, changedFiles }, null, 2));
157
+ }
158
+ else {
159
+ console.log('\n— Summary —');
160
+ for (const a of agents) {
161
+ console.log(`${a.state === 'done' ? '✅' : '❌'} ${a.name} [${a.state}] ${a.result ?? ''}`);
162
+ }
163
+ if (changedFiles.length > 0)
164
+ console.log(`Files changed: ${changedFiles.join(', ')}`);
165
+ }
166
+ process.exit(agents.every((a) => a.state === 'done') ? 0 : 1);
167
+ }
168
+ if (!process.stdout.isTTY) {
169
+ console.error('Parallel requires an interactive terminal (TTY).');
170
+ process.exit(1);
171
+ }
172
+ const config = loadConfig();
173
+ if (config.language)
174
+ setLang(config.language);
175
+ const initialFolder = argv.find((a) => !a.startsWith('-'));
176
+ const useAltScreen = process.stdout.isTTY && process.env.PARALLEL_NO_ALT_SCREEN !== '1';
177
+ const restoreTerminal = () => {
178
+ if (!useAltScreen)
179
+ return;
180
+ process.stdout.write('\x1b[?25h\x1b[?1049l');
181
+ };
182
+ if (useAltScreen) {
183
+ process.stdout.write('\x1b[?1049h\x1b[2J\x1b[3J\x1b[H');
184
+ process.once('exit', restoreTerminal);
185
+ process.once('SIGINT', () => {
186
+ restoreTerminal();
187
+ process.exit(130);
188
+ });
189
+ process.once('SIGTERM', () => {
190
+ restoreTerminal();
191
+ process.exit(143);
192
+ });
193
+ }
194
+ const app = render(_jsx(App, { config: config, initialFolder: initialFolder }), { exitOnCtrlC: true });
195
+ await app.waitUntilExit();
196
+ restoreTerminal();
@@ -0,0 +1,46 @@
1
+ import OpenAI from 'openai';
2
+ export class LLMClient {
3
+ client;
4
+ model;
5
+ constructor(apiKey, baseUrl, model) {
6
+ this.client = new OpenAI({ apiKey, baseURL: baseUrl });
7
+ this.model = model;
8
+ }
9
+ /**
10
+ * One chat completion round, with optional function-calling tools.
11
+ * Retries on transient errors (429/5xx) with exponential backoff.
12
+ */
13
+ async chat(messages, tools, signal) {
14
+ let lastErr;
15
+ for (let attempt = 0; attempt < 4; attempt++) {
16
+ try {
17
+ const res = await this.client.chat.completions.create({
18
+ model: this.model,
19
+ messages,
20
+ tools: tools && tools.length > 0 ? tools : undefined,
21
+ temperature: 0.2,
22
+ max_tokens: 4096,
23
+ }, { signal });
24
+ const choice = res.choices[0];
25
+ if (!choice?.message)
26
+ throw new Error('Empty response from model');
27
+ return {
28
+ message: choice.message,
29
+ tokensIn: res.usage?.prompt_tokens ?? 0,
30
+ tokensOut: res.usage?.completion_tokens ?? 0,
31
+ };
32
+ }
33
+ catch (err) {
34
+ lastErr = err;
35
+ if (signal?.aborted)
36
+ throw err;
37
+ const status = err?.status ?? err?.response?.status;
38
+ const retriable = status === 429 || (typeof status === 'number' && status >= 500) || err?.code === 'ECONNRESET';
39
+ if (!retriable || attempt === 3)
40
+ throw err;
41
+ await new Promise((r) => setTimeout(r, 1500 * Math.pow(2, attempt)));
42
+ }
43
+ }
44
+ throw lastErr;
45
+ }
46
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Built-in pricing table (USD per 1M tokens) for well-known models — same
3
+ * approach as Roo Code: known models are priced out of the box, and the user
4
+ * can override or add prices per model in /settings (stored on the provider).
5
+ * Prices drift over time; overrides always win.
6
+ */
7
+ const BUILTIN = {
8
+ // DeepSeek
9
+ 'deepseek-v4-flash': { input: 0.27, output: 1.1 },
10
+ 'deepseek-v4-pro': { input: 0.55, output: 2.19 },
11
+ 'deepseek-chat': { input: 0.27, output: 1.1 },
12
+ 'deepseek-reasoner': { input: 0.55, output: 2.19 },
13
+ // OpenAI
14
+ 'gpt-4o': { input: 2.5, output: 10 },
15
+ 'gpt-4o-mini': { input: 0.15, output: 0.6 },
16
+ 'gpt-4.1': { input: 2, output: 8 },
17
+ 'gpt-4.1-mini': { input: 0.4, output: 1.6 },
18
+ 'gpt-4.1-nano': { input: 0.1, output: 0.4 },
19
+ 'o3-mini': { input: 1.1, output: 4.4 },
20
+ 'o4-mini': { input: 1.1, output: 4.4 },
21
+ // Anthropic
22
+ 'claude-opus-4': { input: 15, output: 75 },
23
+ 'claude-sonnet-4': { input: 3, output: 15 },
24
+ 'claude-haiku-4': { input: 1, output: 5 },
25
+ 'claude-3-5-haiku': { input: 0.8, output: 4 },
26
+ // Mistral
27
+ 'mistral-large': { input: 2, output: 6 },
28
+ 'codestral': { input: 0.3, output: 0.9 },
29
+ 'devstral': { input: 0.1, output: 0.3 },
30
+ // Alibaba
31
+ 'qwen2.5-coder': { input: 0.09, output: 0.09 },
32
+ 'qwen-max': { input: 1.6, output: 6.4 },
33
+ // Local endpoints are free
34
+ 'ollama': { input: 0, output: 0 },
35
+ };
36
+ /**
37
+ * Resolve the price of a model: provider override first, then built-in table
38
+ * (exact match, then prefix/substring for versioned names like
39
+ * "claude-sonnet-4-20250514" or "openai/gpt-4o-mini"). null = unknown.
40
+ */
41
+ export function priceFor(provider, model) {
42
+ const override = provider?.prices?.[model];
43
+ if (override)
44
+ return override;
45
+ const m = model.toLowerCase();
46
+ // strip an optional "vendor/" prefix (OpenRouter-style ids)
47
+ const bare = m.includes('/') ? m.slice(m.lastIndexOf('/') + 1) : m;
48
+ if (BUILTIN[bare])
49
+ return BUILTIN[bare];
50
+ // longest prefix wins so "deepseek-chat" beats nothing else
51
+ let best = null;
52
+ for (const key of Object.keys(BUILTIN)) {
53
+ if ((bare.startsWith(key) || bare.includes(key)) && (!best || key.length > best.length))
54
+ best = key;
55
+ }
56
+ if (best)
57
+ return BUILTIN[best];
58
+ // local endpoints (ollama, llama.cpp, vLLM on localhost) → free
59
+ if (provider && /localhost|127\.0\.0\.1|0\.0\.0\.0/.test(provider.baseUrl))
60
+ return { input: 0, output: 0 };
61
+ return null;
62
+ }
63
+ /** Cost in USD of a token delta at a given price. */
64
+ export function costOf(price, tokensIn, tokensOut) {
65
+ return (tokensIn * price.input + tokensOut * price.output) / 1_000_000;
66
+ }
67
+ /** "$0.0042", "$0.13", "$2.41" — compact, always meaningful. */
68
+ export function fmtCost(usd) {
69
+ if (usd === 0)
70
+ return '$0.00';
71
+ if (usd < 0.01)
72
+ return `$${usd.toFixed(4)}`;
73
+ if (usd < 1)
74
+ return `$${usd.toFixed(3)}`;
75
+ return `$${usd.toFixed(2)}`;
76
+ }
package/dist/server.js ADDED
@@ -0,0 +1,149 @@
1
+ import net from 'node:net';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ export function socketPath(projectRoot) {
5
+ return path.join(projectRoot, '.parallel', 'session.sock');
6
+ }
7
+ /** Start the session server. Returns a stop function (closes socket + clients). */
8
+ export function startSessionServer(ctl) {
9
+ const sock = socketPath(ctl.projectRoot);
10
+ try {
11
+ fs.mkdirSync(path.dirname(sock), { recursive: true });
12
+ // A previous run may have crashed without cleaning up: remove the stale socket.
13
+ if (fs.existsSync(sock))
14
+ fs.unlinkSync(sock);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ const clients = new Set();
20
+ const infoFor = (ref) => ctl.board.getAgentByName(ref);
21
+ const send = (socket, msg) => {
22
+ try {
23
+ socket.write(JSON.stringify(msg) + '\n');
24
+ }
25
+ catch {
26
+ /* client gone — cleaned up on 'close' */
27
+ }
28
+ };
29
+ const pushTo = (c) => {
30
+ const info = infoFor(c.agent);
31
+ if (!info) {
32
+ send(c.socket, { type: 'state', info: null, others: [], logs: [] });
33
+ return;
34
+ }
35
+ const fresh = [];
36
+ for (const l of ctl.board.logs) {
37
+ if ((l.seq ?? 0) > c.lastSeq && (l.agentId === info.id || l.agentId === ''))
38
+ fresh.push(l);
39
+ }
40
+ if (fresh.length > 0)
41
+ c.lastSeq = fresh[fresh.length - 1].seq ?? c.lastSeq;
42
+ // Shared awareness, made VISIBLE: every attached terminal also sees what
43
+ // the other agents are doing right now (same data the agents receive).
44
+ const others = [...ctl.board.agents.values()]
45
+ .filter((a) => a.id !== info.id)
46
+ .map((a) => ({ name: a.name, alias: a.alias, state: a.state, task: a.task, currentAction: a.currentAction }));
47
+ // Pending interaction for THIS agent (if any): the attached terminal can
48
+ // answer it directly — no need to switch back to the hub.
49
+ const appr = ctl.approvals.find((a) => a.agentId === info.id);
50
+ const q = ctl.questions.find((x) => x.agentId === info.id);
51
+ const approval = appr ? { id: appr.id, agentName: appr.agentName, command: appr.command } : undefined;
52
+ const question = q
53
+ ? { id: q.id, agentName: q.agentName, question: q.question, options: q.options, recommended: q.recommended }
54
+ : undefined;
55
+ send(c.socket, { type: 'state', info, others, logs: fresh, approval, question });
56
+ };
57
+ // Throttled broadcast: at most one push per ~120ms, on blackboard updates.
58
+ let pending = false;
59
+ const onUpdate = () => {
60
+ if (pending || clients.size === 0)
61
+ return;
62
+ pending = true;
63
+ setTimeout(() => {
64
+ pending = false;
65
+ for (const c of clients)
66
+ pushTo(c);
67
+ }, 120).unref?.();
68
+ };
69
+ ctl.on('update', onUpdate);
70
+ const server = net.createServer((socket) => {
71
+ const client = { socket, agent: '', lastSeq: 0 };
72
+ let buffer = '';
73
+ socket.setEncoding('utf8');
74
+ socket.on('data', (chunk) => {
75
+ buffer += chunk;
76
+ let nl;
77
+ while ((nl = buffer.indexOf('\n')) !== -1) {
78
+ const line = buffer.slice(0, nl).trim();
79
+ buffer = buffer.slice(nl + 1);
80
+ if (!line)
81
+ continue;
82
+ let msg;
83
+ try {
84
+ msg = JSON.parse(line);
85
+ }
86
+ catch {
87
+ continue;
88
+ }
89
+ if (msg.type === 'hello' && typeof msg.agent === 'string') {
90
+ client.agent = msg.agent;
91
+ clients.add(client);
92
+ pushTo(client); // immediate first snapshot (full backlog: lastSeq = 0)
93
+ }
94
+ else if (msg.type === 'input' && typeof msg.text === 'string' && client.agent) {
95
+ const text = msg.text.trim();
96
+ if (!text)
97
+ continue;
98
+ if (!ctl.sendToAgent(client.agent, text)) {
99
+ send(socket, { type: 'state', info: infoFor(client.agent) ?? null, others: [], logs: [] });
100
+ }
101
+ }
102
+ else if (msg.type === 'approve' && typeof msg.id === 'number') {
103
+ // First answer wins (hub or any attached terminal) — the controller
104
+ // ignores answers for ids that are no longer pending.
105
+ ctl.answerApproval(msg.id, !!msg.approved, !!msg.always);
106
+ }
107
+ else if (msg.type === 'answer' && typeof msg.id === 'number' && typeof msg.text === 'string') {
108
+ ctl.answerQuestion(msg.id, msg.text);
109
+ }
110
+ else if (msg.type === 'spawn' && typeof msg.text === 'string') {
111
+ // Agent N+1 can be launched from ANY terminal of the session —
112
+ // its own dedicated terminal then opens automatically.
113
+ const task = msg.text.trim();
114
+ if (task)
115
+ ctl.spawnAgent(task);
116
+ }
117
+ }
118
+ });
119
+ const drop = () => {
120
+ clients.delete(client);
121
+ };
122
+ socket.on('close', drop);
123
+ socket.on('error', drop);
124
+ });
125
+ try {
126
+ server.listen(sock);
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ server.on('error', () => {
132
+ /* keep the TUI alive even if the server dies */
133
+ });
134
+ return () => {
135
+ ctl.off('update', onUpdate);
136
+ for (const c of clients) {
137
+ send(c.socket, { type: 'bye' });
138
+ c.socket.destroy();
139
+ }
140
+ clients.clear();
141
+ server.close();
142
+ try {
143
+ fs.unlinkSync(sock);
144
+ }
145
+ catch {
146
+ /* already gone */
147
+ }
148
+ };
149
+ }
package/dist/skills.js ADDED
@@ -0,0 +1,132 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ function parseFrontMatter(raw) {
5
+ const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
6
+ if (!m)
7
+ return { fields: {}, body: raw.trim() };
8
+ const fields = {};
9
+ for (const line of m[1].split(/\r?\n/)) {
10
+ const kv = line.match(/^([A-Za-z][\w-]*)\s*:\s*(.*)$/);
11
+ if (kv)
12
+ fields[kv[1].toLowerCase()] = kv[2].trim();
13
+ }
14
+ return { fields, body: m[2].trim() };
15
+ }
16
+ function globalDir(kind) {
17
+ return path.join(os.homedir(), '.parallel', kind);
18
+ }
19
+ function projectDir(projectRoot, kind) {
20
+ return path.join(projectRoot, '.parallel', kind);
21
+ }
22
+ function readMarkdownFiles(dir) {
23
+ let entries;
24
+ try {
25
+ entries = fs.readdirSync(dir);
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ const out = [];
31
+ for (const e of entries) {
32
+ if (!e.endsWith('.md'))
33
+ continue;
34
+ const file = path.join(dir, e);
35
+ try {
36
+ out.push({ file, raw: fs.readFileSync(file, 'utf8') });
37
+ }
38
+ catch {
39
+ /* unreadable file — skip */
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+ /** Load skills: global first, then project (project overrides same-name global). */
45
+ export function loadSkills(projectRoot) {
46
+ const byName = new Map();
47
+ for (const scope of ['global', 'project']) {
48
+ const dir = scope === 'global' ? globalDir('skills') : projectDir(projectRoot, 'skills');
49
+ for (const { file, raw } of readMarkdownFiles(dir)) {
50
+ const { fields, body } = parseFrontMatter(raw);
51
+ const name = (fields.name || path.basename(file, '.md')).toLowerCase();
52
+ if (!body)
53
+ continue;
54
+ byName.set(name, {
55
+ name,
56
+ description: fields.description || '',
57
+ body,
58
+ scope,
59
+ file,
60
+ });
61
+ }
62
+ }
63
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
64
+ }
65
+ /** Load specialists: same layering as skills; body is the role definition. */
66
+ export function loadSpecialists(projectRoot) {
67
+ const byName = new Map();
68
+ for (const scope of ['global', 'project']) {
69
+ const dir = scope === 'global' ? globalDir('specialists') : projectDir(projectRoot, 'specialists');
70
+ for (const { file, raw } of readMarkdownFiles(dir)) {
71
+ const { fields, body } = parseFrontMatter(raw);
72
+ const name = (fields.name || path.basename(file, '.md')).toLowerCase();
73
+ if (!body)
74
+ continue;
75
+ byName.set(name, {
76
+ name,
77
+ description: fields.description || '',
78
+ model: fields.model || undefined,
79
+ role: body,
80
+ scope,
81
+ file,
82
+ });
83
+ }
84
+ }
85
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
86
+ }
87
+ function slug(name) {
88
+ return name.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'unnamed';
89
+ }
90
+ /** Create a skill template file; returns its path. Throws if it already exists. */
91
+ export function createSkillTemplate(name, description, scope, projectRoot) {
92
+ const dir = scope === 'global' ? globalDir('skills') : projectDir(projectRoot, 'skills');
93
+ fs.mkdirSync(dir, { recursive: true });
94
+ const file = path.join(dir, `${slug(name)}.md`);
95
+ if (fs.existsSync(file))
96
+ throw new Error(`already exists: ${file}`);
97
+ const content = `---
98
+ name: ${slug(name)}
99
+ description: ${description || 'What this skill is for (used to decide relevance)'}
100
+ ---
101
+
102
+ Write here the instructions, conventions or checklists the agent must follow
103
+ when this skill is loaded. Plain markdown.
104
+ `;
105
+ fs.writeFileSync(file, content, 'utf8');
106
+ return file;
107
+ }
108
+ /** Create a specialist template file; returns its path. Throws if it already exists. */
109
+ export function createSpecialistTemplate(name, description, scope, projectRoot, model) {
110
+ const dir = scope === 'global' ? globalDir('specialists') : projectDir(projectRoot, 'specialists');
111
+ fs.mkdirSync(dir, { recursive: true });
112
+ const file = path.join(dir, `${slug(name)}.md`);
113
+ if (fs.existsSync(file))
114
+ throw new Error(`already exists: ${file}`);
115
+ const content = `---
116
+ name: ${slug(name)}
117
+ description: ${description || 'What this specialist is good at'}
118
+ ${model ? `model: ${model}\n` : ''}---
119
+
120
+ You are a ${name} specialist. Describe here the role, focus areas, constraints
121
+ and working style of this specialist. This text is appended to the agent's
122
+ system prompt.
123
+ `;
124
+ fs.writeFileSync(file, content, 'utf8');
125
+ return file;
126
+ }
127
+ /** Compact catalog of skills for the agent system prompt (name — description). */
128
+ export function skillsCatalog(skills) {
129
+ if (skills.length === 0)
130
+ return '';
131
+ return skills.map((s) => `- ${s.name}${s.description ? ` — ${s.description}` : ''}`).join('\n');
132
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { Spinner } from './Spinner.js';
4
+ import { fmtCost } from '../pricing.js';
5
+ import { STATE_LABEL, stateLabel, elapsed, truncate } from './theme.js';
6
+ import { Md } from './Md.js';
7
+ import { t } from '../i18n.js';
8
+ export const KIND_COLOR = {
9
+ tool: 'cyanBright',
10
+ llm: 'gray',
11
+ error: 'red',
12
+ note: 'magentaBright',
13
+ system: 'yellow',
14
+ info: 'white',
15
+ };
16
+ /** Thinking/commentary lines (kind 'llm') are dimmed + italic, à la Codex/Claude Code. */
17
+ export const KIND_DIM = { llm: true };
18
+ export function AgentPanel({ agent, logs, width, expanded = false, }) {
19
+ const st = STATE_LABEL[agent.state];
20
+ const busy = agent.state === 'thinking' || agent.state === 'working' || agent.state === 'listening';
21
+ return (_jsx(Box, { width: width, paddingX: 0, children: _jsxs(Box, { borderStyle: agent.state === 'listening' ? 'double' : 'round', borderColor: agent.state === 'error' ? 'red' : agent.state === 'listening' ? 'cyanBright' : agent.color, flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsxs(Text, { color: agent.color, bold: true, children: ["\u25C6 ", agent.name, agent.alias && agent.alias !== agent.name ? _jsxs(Text, { color: "gray", children: [" @", agent.alias] }) : null, ' '] }), _jsxs(Text, { backgroundColor: st.color, color: "black", bold: true, children: [' ', st.icon, " ", stateLabel(agent.state), ' '] }), busy && (_jsxs(Text, { children: [' ', _jsx(Spinner, { color: agent.color })] }))] }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [agent.specialist ? `🎓${agent.specialist} · ` : '', truncate(agent.model, 18), " \u00B7 ", elapsed(agent.startedAt), " \u00B7 ", agent.steps, " st \u00B7", ' ', Math.round((agent.tokensIn + agent.tokensOut) / 1000), "k \u00B7", ' ', agent.ctxPct !== undefined ? (_jsxs(Text, { color: agent.ctxPct >= 90 ? 'redBright' : agent.ctxPct >= 70 ? 'yellowBright' : 'gray', children: ["\u25D4", agent.ctxPct, "% \u00B7", ' '] })) : null, _jsx(Text, { color: "greenBright", children: agent.cost === null ? '$—' : fmtCost(agent.cost) })] })] }), _jsxs(Text, { color: "gray", wrap: expanded ? 'wrap' : 'truncate-end', children: ["\u25E6 ", expanded ? agent.task : truncate(agent.task, 120)] }), agent.claims && agent.claims.length > 0 ? (_jsxs(Text, { color: "yellowBright", wrap: "truncate-end", children: ["\uD83D\uDEA9 ", agent.claims.join(' ')] })) : null, agent.currentAction ? (_jsxs(Text, { color: agent.color, wrap: "truncate-end", children: ["\u25B8 ", truncate(agent.currentAction, 120)] })) : null, agent.lastResult ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "greenBright", bold: true, children: t('agent.summary') }), expanded || agent.state === 'done' ? (
22
+ // A finished agent's summary is the deliverable: show it ENTIRELY,
23
+ // wrapped and lightly formatted — never truncated.
24
+ _jsx(Md, { text: agent.lastResult })) : (_jsx(Text, { color: "white", wrap: "truncate-end", children: truncate(agent.lastResult, 260) }))] })) : null, _jsx(Box, { flexDirection: "column", marginTop: 0, children: logs.map((l, i) => (_jsx(Text, { color: KIND_COLOR[l.kind] ?? 'white', italic: KIND_DIM[l.kind] ?? false, wrap: "truncate-end", children: truncate(l.text, expanded ? 220 : 140) }, i))) })] }) }));
25
+ }