junecoder 1.0.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/README.md +2 -0
- package/agent.mjs +650 -0
- package/checkpoint.mjs +43 -0
- package/cli.js +199 -0
- package/config.mjs +118 -0
- package/context.mjs +160 -0
- package/distill.mjs +178 -0
- package/executor.mjs +207 -0
- package/mcp.mjs +209 -0
- package/memory.mjs +218 -0
- package/metaTools.mjs +523 -0
- package/package.json +26 -0
- package/provider.mjs +145 -0
- package/session.mjs +114 -0
- package/skills.mjs +147 -0
- package/tools/bash.mjs +41 -0
- package/tools/delete.mjs +57 -0
- package/tools/edit.mjs +71 -0
- package/tools/fetch.mjs +55 -0
- package/tools/glob.mjs +83 -0
- package/tools/grep.mjs +58 -0
- package/tools/index.mjs +41 -0
- package/tools/ls.mjs +62 -0
- package/tools/read.mjs +50 -0
- package/tools/websearch.mjs +58 -0
- package/tools/write.mjs +54 -0
- package/tools.mjs +15 -0
- package/tui.mjs +437 -0
package/metaTools.mjs
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta tools — built-in agent tools for task management, planning, skills,
|
|
3
|
+
* goal tracking, verification, and sub-agent spawning.
|
|
4
|
+
*
|
|
5
|
+
* Each tool exports: { name, description, parameters, readonly, parallel, execute }
|
|
6
|
+
*
|
|
7
|
+
* subagentTool uses dynamic import('./agent.mjs') to break circular dependency.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { MIN_REPORT_CHARS } from './agent.mjs';
|
|
11
|
+
import { loadSkills, formatSkillListing, readSkill } from './skills.mjs';
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
// ─── Overlays (system prompt additions for sub-agent roles) ───────────────────
|
|
15
|
+
|
|
16
|
+
/** Overlay for 'explore' role: read-only research sub-agent. */
|
|
17
|
+
export const EXPLORE_OVERLAY = [
|
|
18
|
+
'You are an explore sub-agent. You perform read-only research and analysis.',
|
|
19
|
+
'You CANNOT write files, run commands, or modify anything.',
|
|
20
|
+
'Report your findings concisely. Do not ask for clarification — infer and proceed.',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/** Overlay for 'plan' role: read-only design/planning sub-agent. */
|
|
24
|
+
export const PLAN_OVERLAY = [
|
|
25
|
+
'You are a planning sub-agent. Design solutions and write step-by-step plans.',
|
|
26
|
+
'You CANNOT write files, run commands, or modify anything.',
|
|
27
|
+
'Output a clear, actionable plan. Use task list format when appropriate.',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** Overlay for 'coder' role: full implementation sub-agent (write-capable). */
|
|
31
|
+
export const CODER_OVERLAY = [
|
|
32
|
+
'You are a coder sub-agent. Implement self-contained tasks in isolated context.',
|
|
33
|
+
'You have full tool access within your scope. Return only your final report.',
|
|
34
|
+
'Be thorough — verify your work before reporting.',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// ─── taskTool ─────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export const taskTool = {
|
|
40
|
+
name: 'task',
|
|
41
|
+
description:
|
|
42
|
+
'Plan and track a task list for complex multi-step work. ' +
|
|
43
|
+
'Replaces the entire list on each call. ' +
|
|
44
|
+
'Each item: { title, status } where status is pending|in_progress|done. ' +
|
|
45
|
+
'Keep exactly ONE item in_progress.',
|
|
46
|
+
parameters: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
items: {
|
|
50
|
+
type: 'array',
|
|
51
|
+
description: 'Full task list with status',
|
|
52
|
+
items: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
title: { type: 'string' },
|
|
56
|
+
status: { type: 'string', enum: ['pending', 'in_progress', 'done'] },
|
|
57
|
+
},
|
|
58
|
+
required: ['title', 'status'],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ['items'],
|
|
63
|
+
},
|
|
64
|
+
readonly: true,
|
|
65
|
+
parallel: false,
|
|
66
|
+
|
|
67
|
+
async execute(args, agent) {
|
|
68
|
+
const items = args.items || [];
|
|
69
|
+
agent.tasks = items;
|
|
70
|
+
|
|
71
|
+
const counts = { pending: 0, in_progress: 0, done: 0 };
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
if (counts[item.status] !== undefined) counts[item.status]++;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lines = [`Task list updated: ${counts.done}/${items.length} done.`];
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
const icon = item.status === 'done' ? '✓' : item.status === 'in_progress' ? '▶' : '○';
|
|
79
|
+
lines.push(` ${icon} [${item.status}] ${item.title}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Reset turn counter
|
|
83
|
+
agent._turnsSinceTaskUpdate = 0;
|
|
84
|
+
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// ─── planTool ─────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export const planTool = {
|
|
92
|
+
name: 'plan',
|
|
93
|
+
description:
|
|
94
|
+
'Enter or exit plan mode. In plan mode, only read-only tools are allowed. ' +
|
|
95
|
+
'Use for exploring the codebase and designing solutions before implementing.',
|
|
96
|
+
parameters: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
action: { type: 'string', enum: ['enter', 'exit'], description: 'Enter or exit plan mode' },
|
|
100
|
+
},
|
|
101
|
+
required: ['action'],
|
|
102
|
+
},
|
|
103
|
+
readonly: true,
|
|
104
|
+
parallel: false,
|
|
105
|
+
|
|
106
|
+
async execute(args, agent) {
|
|
107
|
+
if (args.action === 'enter') {
|
|
108
|
+
if (agent.planMode) return 'Already in plan mode.';
|
|
109
|
+
agent.planMode = true;
|
|
110
|
+
agent._turnsInPlanMode = 0;
|
|
111
|
+
return 'Entered plan mode. Only read-only tools are available. ' +
|
|
112
|
+
'Explore the codebase, design your approach, then exit plan mode to implement.';
|
|
113
|
+
} else {
|
|
114
|
+
if (!agent.planMode) return 'Not in plan mode.';
|
|
115
|
+
agent.planMode = false;
|
|
116
|
+
return `Exited plan mode after ${agent._turnsInPlanMode || 0} turns. Full tool access restored.`;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// ─── skillTool ────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
export const skillTool = {
|
|
124
|
+
name: 'skill',
|
|
125
|
+
description:
|
|
126
|
+
'Manage project skills. Skills are reusable workflows stored in .junecode/skills/. ' +
|
|
127
|
+
'Use skill=list to see available skills, skill=load to activate one.',
|
|
128
|
+
parameters: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
action: { type: 'string', enum: ['list', 'load'], description: "'list' to see available skills, 'load' to activate one" },
|
|
132
|
+
name: { type: 'string', description: 'Skill name (required for load action)' },
|
|
133
|
+
},
|
|
134
|
+
required: ['action'],
|
|
135
|
+
},
|
|
136
|
+
readonly: true,
|
|
137
|
+
parallel: false,
|
|
138
|
+
|
|
139
|
+
async execute(args, agent) {
|
|
140
|
+
const cwd = agent.cwd || process.cwd();
|
|
141
|
+
|
|
142
|
+
if (args.action === 'list') {
|
|
143
|
+
const skills = loadSkills(cwd);
|
|
144
|
+
return 'Available skills:\n' + formatSkillListing(skills);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (args.action === 'load') {
|
|
148
|
+
if (!args.name) return 'Error: skill name is required for load action.';
|
|
149
|
+
const content = readSkill(cwd, args.name);
|
|
150
|
+
if (!content) return `Skill "${args.name}" not found.`;
|
|
151
|
+
return `Loaded skill "${args.name}":\n\n${content}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return `Unknown action: ${args.action}`;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// ─── goalTool ─────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
export const goalTool = {
|
|
161
|
+
name: 'goal',
|
|
162
|
+
description:
|
|
163
|
+
'Manage a long-running autonomous goal. ' +
|
|
164
|
+
'Set a goal with verifiable criteria; mark complete when criteria are met; ' +
|
|
165
|
+
'mark blocked when stuck (only after 3 genuine attempts). ' +
|
|
166
|
+
'Actions: set | complete | blocked | cancel.',
|
|
167
|
+
parameters: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
action: { type: 'string', enum: ['set', 'complete', 'blocked', 'cancel'] },
|
|
171
|
+
objective: { type: 'string', description: 'What to accomplish (for set)' },
|
|
172
|
+
criteria: { type: 'string', description: 'How completion is PROVEN (required for set)' },
|
|
173
|
+
reason: { type: 'string', description: 'Blocking condition (required for blocked)' },
|
|
174
|
+
},
|
|
175
|
+
required: ['action'],
|
|
176
|
+
},
|
|
177
|
+
readonly: true,
|
|
178
|
+
parallel: false,
|
|
179
|
+
|
|
180
|
+
async execute(args, agent) {
|
|
181
|
+
switch (args.action) {
|
|
182
|
+
case 'set': {
|
|
183
|
+
if (!args.objective) return 'Error: objective is required for set.';
|
|
184
|
+
agent.goal = {
|
|
185
|
+
objective: args.objective,
|
|
186
|
+
criteria: args.criteria || '',
|
|
187
|
+
startedAt: Date.now(),
|
|
188
|
+
attempts: 0,
|
|
189
|
+
};
|
|
190
|
+
agent._blockTally = {};
|
|
191
|
+
return `Goal set: "${args.objective}"\nCriteria: ${args.criteria || '(none)'}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case 'complete': {
|
|
195
|
+
if (!agent.goal) return 'No active goal.';
|
|
196
|
+
const g = agent.goal;
|
|
197
|
+
agent.goal = null;
|
|
198
|
+
agent._blockTally = {};
|
|
199
|
+
return `Goal completed: "${g.objective}"`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case 'blocked': {
|
|
203
|
+
if (!agent.goal) return 'No active goal.';
|
|
204
|
+
if (!args.reason) return 'Error: reason is required for blocked action.';
|
|
205
|
+
agent.goal.attempts = (agent.goal.attempts || 0) + 1;
|
|
206
|
+
return `Goal blocked (attempt ${agent.goal.attempts}): ${args.reason}\n` +
|
|
207
|
+
'Try a different approach. After 3 genuine attempts, the goal will be audited.';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case 'cancel': {
|
|
211
|
+
if (!agent.goal) return 'No active goal.';
|
|
212
|
+
const obj = agent.goal.objective;
|
|
213
|
+
agent.goal = null;
|
|
214
|
+
agent._blockTally = {};
|
|
215
|
+
return `Goal cancelled: "${obj}"`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
default:
|
|
219
|
+
return `Unknown action: ${args.action}`;
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// ─── verifyTool ───────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
export const verifyTool = {
|
|
227
|
+
name: 'verify',
|
|
228
|
+
description:
|
|
229
|
+
'Run a pre-completion self-check. Shows what files changed (git diff --stat), ' +
|
|
230
|
+
'the current task list, and a verification checklist. ' +
|
|
231
|
+
'Call BEFORE declaring any coding task complete.',
|
|
232
|
+
parameters: {
|
|
233
|
+
type: 'object',
|
|
234
|
+
properties: {},
|
|
235
|
+
required: [],
|
|
236
|
+
},
|
|
237
|
+
readonly: true,
|
|
238
|
+
parallel: false,
|
|
239
|
+
|
|
240
|
+
async execute(args, agent) {
|
|
241
|
+
const cwd = agent.cwd || process.cwd();
|
|
242
|
+
const lines = ['=== VERIFICATION REPORT ===', ''];
|
|
243
|
+
|
|
244
|
+
// Git diff
|
|
245
|
+
try {
|
|
246
|
+
const diffStat = execSync('git diff --stat', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
247
|
+
lines.push(`Changed files (git diff --stat):\n${diffStat || '(no changes)'}`);
|
|
248
|
+
} catch {
|
|
249
|
+
lines.push('Changed files: (not a git repo or git unavailable)');
|
|
250
|
+
}
|
|
251
|
+
lines.push('');
|
|
252
|
+
|
|
253
|
+
// Task list
|
|
254
|
+
if (agent.tasks.length > 0) {
|
|
255
|
+
const done = agent.tasks.filter((t) => t.status === 'done').length;
|
|
256
|
+
lines.push(`Task list: ${done}/${agent.tasks.length} done`);
|
|
257
|
+
for (const item of agent.tasks) {
|
|
258
|
+
const icon = item.status === 'done' ? '✓' : item.status === 'in_progress' ? '▶' : '○';
|
|
259
|
+
lines.push(` ${icon} [${item.status}] ${item.title}`);
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
lines.push('No active tasks.');
|
|
263
|
+
}
|
|
264
|
+
lines.push('');
|
|
265
|
+
|
|
266
|
+
// Checklist
|
|
267
|
+
lines.push('Self-review checklist:');
|
|
268
|
+
lines.push('- [ ] Did I run the project\'s tests and do they pass?');
|
|
269
|
+
lines.push('- [ ] Did I read every file I changed to catch leftover debug code or stale comments?');
|
|
270
|
+
lines.push('- [ ] Do comments and docstrings match what the code actually does?');
|
|
271
|
+
lines.push('- [ ] Did I remove placeholder code, TODO stubs, or commented-out experiment blocks?');
|
|
272
|
+
lines.push('- [ ] If I used a subagent, did I verify its report against the actual files it touched?');
|
|
273
|
+
lines.push('- [ ] Are all task items genuinely done (not just marked done to finish early)?');
|
|
274
|
+
|
|
275
|
+
agent._verifiedThisRun = true;
|
|
276
|
+
return lines.join('\n');
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// ─── memorySearchTool ──────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
export const memorySearchTool = {
|
|
283
|
+
name: 'memory_search',
|
|
284
|
+
description:
|
|
285
|
+
'Search long-term memory. Use to recall past debugging insights, ' +
|
|
286
|
+
'project conventions, or architecture decisions. Returns top matches.',
|
|
287
|
+
parameters: {
|
|
288
|
+
type: 'object',
|
|
289
|
+
properties: {
|
|
290
|
+
query: { type: 'string', description: 'Natural language search query' },
|
|
291
|
+
limit: { type: 'number', description: 'Max results (default 5)' },
|
|
292
|
+
},
|
|
293
|
+
required: ['query'],
|
|
294
|
+
},
|
|
295
|
+
readonly: true,
|
|
296
|
+
parallel: true,
|
|
297
|
+
|
|
298
|
+
async execute(args, agent) {
|
|
299
|
+
const { search } = await import('./memory.mjs');
|
|
300
|
+
const result = await search(agent.memory, args.query, { limit: args.limit || 5 });
|
|
301
|
+
return result || 'No matches found.';
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// ─── memoryPutTool ─────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
export const memoryPutTool = {
|
|
308
|
+
name: 'memory_put',
|
|
309
|
+
description:
|
|
310
|
+
'Save knowledge to long-term memory. Types: rule (coding standards), ' +
|
|
311
|
+
'knowledge (facts), decision (architecture decisions), pattern (debugging/workflows). ' +
|
|
312
|
+
'Scopes: personal (default), project, team.',
|
|
313
|
+
parameters: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
properties: {
|
|
316
|
+
type: { type: 'string', enum: ['rule', 'knowledge', 'decision', 'pattern'] },
|
|
317
|
+
title: { type: 'string', description: 'Short title' },
|
|
318
|
+
content: { type: 'string', description: 'Full content to remember' },
|
|
319
|
+
tags: { type: 'string', description: 'Space-separated tags' },
|
|
320
|
+
scope: { type: 'string', enum: ['personal', 'project', 'team'], description: 'Storage scope (default personal)' },
|
|
321
|
+
},
|
|
322
|
+
required: ['type', 'title', 'content'],
|
|
323
|
+
},
|
|
324
|
+
readonly: false,
|
|
325
|
+
parallel: true,
|
|
326
|
+
|
|
327
|
+
async execute(args, agent) {
|
|
328
|
+
const { put } = await import('./memory.mjs');
|
|
329
|
+
const entry = {
|
|
330
|
+
type: args.type || 'knowledge',
|
|
331
|
+
title: args.title,
|
|
332
|
+
content: args.content,
|
|
333
|
+
tags: args.tags ? args.tags.split(/\s+/) : [],
|
|
334
|
+
scope: args.scope || 'personal',
|
|
335
|
+
};
|
|
336
|
+
return await put(agent.memory, entry);
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// ─── mcpConnectTool ───────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
export const mcpConnectTool = {
|
|
343
|
+
name: 'mcp_connect',
|
|
344
|
+
description:
|
|
345
|
+
'Connect to an MCP server. Returns its tools, registered with an mcp_<name>_ prefix. ' +
|
|
346
|
+
'Tools become available immediately in subsequent turns.',
|
|
347
|
+
parameters: {
|
|
348
|
+
type: 'object',
|
|
349
|
+
properties: {
|
|
350
|
+
command: { type: 'string', description: 'Executable command to spawn the server' },
|
|
351
|
+
args: { type: 'string', description: 'Command arguments (space-separated)' },
|
|
352
|
+
name: { type: 'string', description: 'Server name for disambiguation' },
|
|
353
|
+
},
|
|
354
|
+
required: ['command'],
|
|
355
|
+
},
|
|
356
|
+
readonly: false,
|
|
357
|
+
parallel: false,
|
|
358
|
+
|
|
359
|
+
async execute(args, agent) {
|
|
360
|
+
const { connectMcpServer } = await import('./mcp.mjs');
|
|
361
|
+
const srv = {
|
|
362
|
+
name: args.name || 'mcp',
|
|
363
|
+
command: args.command,
|
|
364
|
+
args: args.args ? args.args.split(/\s+/) : [],
|
|
365
|
+
};
|
|
366
|
+
try {
|
|
367
|
+
const mcpTools = await connectMcpServer(srv);
|
|
368
|
+
|
|
369
|
+
// Track the child process
|
|
370
|
+
if (!agent._mcpProcesses) agent._mcpProcesses = [];
|
|
371
|
+
if (mcpTools._mcpProc) {
|
|
372
|
+
mcpTools._mcpProc._mcpName = srv.name;
|
|
373
|
+
agent._mcpProcesses.push(mcpTools._mcpProc);
|
|
374
|
+
|
|
375
|
+
// Register only the actual tool objects (strip meta properties)
|
|
376
|
+
const cleanTools = mcpTools.filter(t => typeof t.execute === 'function');
|
|
377
|
+
agent.tools.push(...cleanTools);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const toolNames = mcpTools
|
|
381
|
+
.filter(t => t.name)
|
|
382
|
+
.map(t => t.name)
|
|
383
|
+
.join('\n ');
|
|
384
|
+
return `Connected to "${srv.name}". Tools available:\n ${toolNames || '(none)'}`;
|
|
385
|
+
} catch (err) {
|
|
386
|
+
return `MCP connection failed: ${err.message}`;
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// ─── mcpDisconnectTool ────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
export const mcpDisconnectTool = {
|
|
394
|
+
name: 'mcp_disconnect',
|
|
395
|
+
description:
|
|
396
|
+
'Disconnect from an MCP server by name. Removes its tools and kills the process.',
|
|
397
|
+
parameters: {
|
|
398
|
+
type: 'object',
|
|
399
|
+
properties: {
|
|
400
|
+
name: { type: 'string', description: 'Server name to disconnect' },
|
|
401
|
+
},
|
|
402
|
+
required: ['name'],
|
|
403
|
+
},
|
|
404
|
+
readonly: false,
|
|
405
|
+
parallel: false,
|
|
406
|
+
|
|
407
|
+
async execute(args, agent) {
|
|
408
|
+
const { removeMcpTools } = await import('./mcp.mjs');
|
|
409
|
+
try {
|
|
410
|
+
removeMcpTools(agent, args.name);
|
|
411
|
+
return `Disconnected from "${args.name}".`;
|
|
412
|
+
} catch (err) {
|
|
413
|
+
return `Disconnect failed: ${err.message}`;
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// ─── mcpListTool ──────────────────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
export const mcpListTool = {
|
|
421
|
+
name: 'mcp_list',
|
|
422
|
+
description: 'List currently connected MCP servers.',
|
|
423
|
+
parameters: {
|
|
424
|
+
type: 'object',
|
|
425
|
+
properties: {},
|
|
426
|
+
required: [],
|
|
427
|
+
},
|
|
428
|
+
readonly: true,
|
|
429
|
+
parallel: true,
|
|
430
|
+
|
|
431
|
+
async execute(args, agent) {
|
|
432
|
+
if (!agent._mcpProcesses || agent._mcpProcesses.length === 0) {
|
|
433
|
+
return 'No MCP servers connected.';
|
|
434
|
+
}
|
|
435
|
+
const names = agent._mcpProcesses
|
|
436
|
+
.map(p => `- ${p._mcpName || '(unnamed)'}`)
|
|
437
|
+
.join('\n');
|
|
438
|
+
return `Connected MCP servers:\n${names}`;
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// ─── subagentTool ─────────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
export const subagentTool = {
|
|
445
|
+
name: 'subagent',
|
|
446
|
+
description:
|
|
447
|
+
'Spawn a sub-agent to handle an independent subtask in an isolated context. ' +
|
|
448
|
+
'Use role=explore for read-only research, role=plan for design, role=coder for implementation. ' +
|
|
449
|
+
'Spawn MULTIPLE sub-agents in parallel for concurrent work.',
|
|
450
|
+
parameters: {
|
|
451
|
+
type: 'object',
|
|
452
|
+
properties: {
|
|
453
|
+
task: { type: 'string', description: 'Self-contained task description for the sub-agent' },
|
|
454
|
+
context: { type: 'string', description: 'Optional background the sub-agent needs' },
|
|
455
|
+
role: {
|
|
456
|
+
type: 'string',
|
|
457
|
+
enum: ['explore', 'plan', 'coder'],
|
|
458
|
+
description: "explore (read-only research), plan (read-only design), or coder (full implementation)",
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
required: ['task'],
|
|
462
|
+
},
|
|
463
|
+
readonly: true,
|
|
464
|
+
parallel: true,
|
|
465
|
+
|
|
466
|
+
async execute(args, agent) {
|
|
467
|
+
const role = args.role || 'coder';
|
|
468
|
+
|
|
469
|
+
// Dynamic import to break circular dependency with agent.mjs
|
|
470
|
+
const { createAgent } = await import('./agent.mjs');
|
|
471
|
+
|
|
472
|
+
// Map role to overlay
|
|
473
|
+
const overlays = {
|
|
474
|
+
explore: EXPLORE_OVERLAY,
|
|
475
|
+
plan: PLAN_OVERLAY,
|
|
476
|
+
coder: CODER_OVERLAY,
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const subAgent = createAgent({
|
|
480
|
+
provider: agent.provider,
|
|
481
|
+
tools: agent.tools,
|
|
482
|
+
config: agent.config,
|
|
483
|
+
cwd: agent.cwd,
|
|
484
|
+
memory: agent.memory,
|
|
485
|
+
overlay: overlays[role] || overlays.coder,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Build input
|
|
489
|
+
let input = args.task;
|
|
490
|
+
if (args.context) {
|
|
491
|
+
input = `Context: ${args.context}\n\nTask: ${args.task}`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// We need runAgent from agent.mjs too
|
|
495
|
+
const { runAgent } = await import('./agent.mjs');
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const report = await runAgent(subAgent, input, {}, { depth: (agent._depth || 0) + 1 });
|
|
499
|
+
if (!report || (typeof report === 'string' && report.length < MIN_REPORT_CHARS)) {
|
|
500
|
+
return `Sub-agent report too short (${report ? report.length : 0} chars). Full report:\n${report || '(empty)'}`;
|
|
501
|
+
}
|
|
502
|
+
return `Sub-agent report:\n\n${report}`;
|
|
503
|
+
} catch (err) {
|
|
504
|
+
return `Sub-agent error: ${err.message}`;
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// ─── All meta tools ───────────────────────────────────────────────────────────
|
|
510
|
+
|
|
511
|
+
export const metaTools = [
|
|
512
|
+
taskTool,
|
|
513
|
+
planTool,
|
|
514
|
+
skillTool,
|
|
515
|
+
goalTool,
|
|
516
|
+
verifyTool,
|
|
517
|
+
memorySearchTool,
|
|
518
|
+
memoryPutTool,
|
|
519
|
+
mcpConnectTool,
|
|
520
|
+
mcpDisconnectTool,
|
|
521
|
+
mcpListTool,
|
|
522
|
+
subagentTool,
|
|
523
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "junecoder",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero Npm Dependencies Agent Framework",
|
|
5
|
+
"main": "agent.mjs",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"junecode": "cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"*.mjs",
|
|
12
|
+
"cli.js",
|
|
13
|
+
"tools/*.mjs",
|
|
14
|
+
"package.json",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test tests/test-*.test.mjs"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=21.7"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["agent", "react", "llm", "coding", "ai"],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "ISC"
|
|
26
|
+
}
|
package/provider.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek LLM provider — calls api.deepseek.com/chat/completions.
|
|
3
|
+
*
|
|
4
|
+
* @param {object} provider
|
|
5
|
+
* @param {string} provider.apiKey - DeepSeek API key
|
|
6
|
+
* @param {string} [provider.model='deepseek-v4-pro']
|
|
7
|
+
* @param {string} [provider.baseURL='https://api.deepseek.com']
|
|
8
|
+
* @param {object} [provider.thinking] - thinking/reasoning config
|
|
9
|
+
* @param {string} [provider.thinking.type='enabled'] - "enabled" | "disabled"
|
|
10
|
+
* @param {string} [provider.thinking.reasoning_effort] - "low"|"medium"|"high"
|
|
11
|
+
* @param {object} opts
|
|
12
|
+
* @param {object[]} opts.messages
|
|
13
|
+
* @param {object[]} [opts.tools]
|
|
14
|
+
* @param {(token: string) => void} [opts.onToken]
|
|
15
|
+
* @param {(reasoning: string) => void} [opts.onReasoning]
|
|
16
|
+
* @param {AbortSignal} [opts.signal]
|
|
17
|
+
* @returns {Promise<{ content: string|null, toolCalls: object[], usage: object|null, reasoning: string|null }>}
|
|
18
|
+
*/
|
|
19
|
+
export async function chat(provider, { messages, tools, onToken, onReasoning, signal } = {}) {
|
|
20
|
+
const {
|
|
21
|
+
apiKey,
|
|
22
|
+
model = 'deepseek-v4-pro',
|
|
23
|
+
baseURL = 'https://api.deepseek.com',
|
|
24
|
+
thinking = { type: 'enabled' },
|
|
25
|
+
} = provider;
|
|
26
|
+
|
|
27
|
+
const body = {
|
|
28
|
+
model,
|
|
29
|
+
messages,
|
|
30
|
+
stream: true,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// DeepSeek thinking/reasoning config
|
|
34
|
+
if (thinking.type === 'enabled') {
|
|
35
|
+
body.thinking = { type: 'enabled' };
|
|
36
|
+
}
|
|
37
|
+
if (thinking.reasoning_effort) {
|
|
38
|
+
body.reasoning_effort = thinking.reasoning_effort;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (tools && tools.length > 0) {
|
|
42
|
+
body.tools = tools;
|
|
43
|
+
body.tool_choice = 'auto';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const response = await fetch(`${baseURL}/chat/completions`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
Authorization: `Bearer ${apiKey}`,
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
signal,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const errText = await response.text().catch(() => '');
|
|
58
|
+
throw new Error(`DeepSeek request failed (${response.status}): ${errText}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return parseStream(response, { onToken, onReasoning });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse SSE stream from DeepSeek chat/completions endpoint.
|
|
66
|
+
* Accumulates content, tool_calls, reasoning_content, and usage.
|
|
67
|
+
*/
|
|
68
|
+
async function parseStream(response, { onToken, onReasoning }) {
|
|
69
|
+
const reader = response.body.getReader();
|
|
70
|
+
const decoder = new TextDecoder();
|
|
71
|
+
|
|
72
|
+
let content = '';
|
|
73
|
+
let reasoning = '';
|
|
74
|
+
let toolCalls = [];
|
|
75
|
+
let usage = null;
|
|
76
|
+
|
|
77
|
+
let buffer = '';
|
|
78
|
+
while (true) {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
if (done) break;
|
|
81
|
+
|
|
82
|
+
buffer += decoder.decode(value, { stream: true });
|
|
83
|
+
const lines = buffer.split('\n');
|
|
84
|
+
buffer = lines.pop() || '';
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
const trimmed = line.trim();
|
|
88
|
+
if (!trimmed || !trimmed.startsWith('data:')) continue;
|
|
89
|
+
|
|
90
|
+
const dataStr = trimmed.slice(5).trim();
|
|
91
|
+
if (dataStr === '[DONE]') continue;
|
|
92
|
+
|
|
93
|
+
let chunk;
|
|
94
|
+
try {
|
|
95
|
+
chunk = JSON.parse(dataStr);
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
101
|
+
if (delta) {
|
|
102
|
+
// Text content
|
|
103
|
+
if (delta.content) {
|
|
104
|
+
content += delta.content;
|
|
105
|
+
if (onToken) onToken(delta.content);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Reasoning/thinking content (DeepSeek thinking mode)
|
|
109
|
+
if (delta.reasoning_content) {
|
|
110
|
+
reasoning += delta.reasoning_content;
|
|
111
|
+
if (onReasoning) onReasoning(delta.reasoning_content);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Tool calls (function calling)
|
|
115
|
+
if (delta.tool_calls) {
|
|
116
|
+
for (const tc of delta.tool_calls) {
|
|
117
|
+
const idx = tc.index ?? toolCalls.length;
|
|
118
|
+
if (!toolCalls[idx]) {
|
|
119
|
+
toolCalls[idx] = {
|
|
120
|
+
id: tc.id || '',
|
|
121
|
+
type: 'function',
|
|
122
|
+
function: { name: '', arguments: '' },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (tc.id) toolCalls[idx].id = tc.id;
|
|
126
|
+
if (tc.function?.name) toolCalls[idx].function.name += tc.function.name;
|
|
127
|
+
if (tc.function?.arguments) toolCalls[idx].function.arguments += tc.function.arguments;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Usage (may appear in last chunk)
|
|
133
|
+
if (chunk.usage) {
|
|
134
|
+
usage = chunk.usage;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
content: content || null,
|
|
141
|
+
toolCalls: toolCalls.filter(Boolean),
|
|
142
|
+
usage,
|
|
143
|
+
reasoning: reasoning || null,
|
|
144
|
+
};
|
|
145
|
+
}
|