jeo-code 0.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/README.md +342 -0
- package/package.json +57 -0
- package/scripts/install.sh +322 -0
- package/scripts/uninstall.sh +30 -0
- package/src/agent/compaction.ts +75 -0
- package/src/agent/config-schema.ts +87 -0
- package/src/agent/context-files.ts +51 -0
- package/src/agent/engine.ts +208 -0
- package/src/agent/json.ts +87 -0
- package/src/agent/loop.ts +22 -0
- package/src/agent/session.ts +198 -0
- package/src/agent/state.ts +199 -0
- package/src/agent/subagents.ts +149 -0
- package/src/agent/tools.ts +355 -0
- package/src/ai/index.ts +11 -0
- package/src/ai/model-catalog-compat.ts +119 -0
- package/src/ai/model-catalog.ts +97 -0
- package/src/ai/model-discovery.ts +148 -0
- package/src/ai/model-enrich.ts +75 -0
- package/src/ai/model-manager.ts +178 -0
- package/src/ai/model-picker.ts +73 -0
- package/src/ai/model-registry.ts +83 -0
- package/src/ai/provider-status.ts +77 -0
- package/src/ai/providers/anthropic.ts +87 -0
- package/src/ai/providers/errors.ts +47 -0
- package/src/ai/providers/gemini.ts +77 -0
- package/src/ai/providers/ollama.ts +54 -0
- package/src/ai/providers/openai.ts +67 -0
- package/src/ai/sse.ts +46 -0
- package/src/ai/types.ts +37 -0
- package/src/auth/callback-server.ts +195 -0
- package/src/auth/flows/anthropic.ts +114 -0
- package/src/auth/flows/google.ts +120 -0
- package/src/auth/flows/index.ts +50 -0
- package/src/auth/flows/openai.ts +130 -0
- package/src/auth/index.ts +23 -0
- package/src/auth/oauth.ts +80 -0
- package/src/auth/pkce.ts +24 -0
- package/src/auth/refresh.ts +60 -0
- package/src/auth/storage.ts +113 -0
- package/src/auth/types.ts +26 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/runner.ts +245 -0
- package/src/cli.ts +17 -0
- package/src/commands/approve.ts +63 -0
- package/src/commands/auth.ts +144 -0
- package/src/commands/chat.ts +37 -0
- package/src/commands/deep-interview.ts +239 -0
- package/src/commands/doctor.ts +250 -0
- package/src/commands/evolve.ts +191 -0
- package/src/commands/launch.ts +745 -0
- package/src/commands/mcp.ts +18 -0
- package/src/commands/models.ts +104 -0
- package/src/commands/ralplan.ts +86 -0
- package/src/commands/resume.ts +6 -0
- package/src/commands/setup-helpers.ts +93 -0
- package/src/commands/setup.ts +190 -0
- package/src/commands/skills.ts +38 -0
- package/src/commands/team.ts +337 -0
- package/src/commands/ultragoal.ts +102 -0
- package/src/index.ts +31 -0
- package/src/mcp/index.ts +3 -0
- package/src/mcp/protocol.ts +45 -0
- package/src/mcp/server.ts +97 -0
- package/src/mcp/tools.ts +156 -0
- package/src/skills/catalog.ts +61 -0
- package/src/tui/app.ts +297 -0
- package/src/tui/components/ascii-art.ts +340 -0
- package/src/tui/components/autocomplete.ts +165 -0
- package/src/tui/components/capability.ts +29 -0
- package/src/tui/components/code-view.ts +146 -0
- package/src/tui/components/color.ts +172 -0
- package/src/tui/components/config-panel.ts +193 -0
- package/src/tui/components/evolution.ts +305 -0
- package/src/tui/components/footer.ts +95 -0
- package/src/tui/components/forge.ts +167 -0
- package/src/tui/components/index.ts +7 -0
- package/src/tui/components/layout.ts +105 -0
- package/src/tui/components/meter.ts +61 -0
- package/src/tui/components/model-picker.ts +82 -0
- package/src/tui/components/provider-picker.ts +42 -0
- package/src/tui/components/select-list.ts +199 -0
- package/src/tui/components/slash.ts +34 -0
- package/src/tui/components/spinner.ts +49 -0
- package/src/tui/components/status.ts +45 -0
- package/src/tui/components/stream.ts +36 -0
- package/src/tui/components/themes.ts +86 -0
- package/src/tui/components/tool-list.ts +67 -0
- package/src/tui/index.ts +2 -0
- package/src/tui/renderer.ts +70 -0
- package/src/tui/terminal.ts +78 -0
- package/src/util/retry.ts +108 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
readWorkflowState,
|
|
5
|
+
writeWorkflowState,
|
|
6
|
+
} from "../agent/state";
|
|
7
|
+
import { runAgentLoop } from "../agent/engine";
|
|
8
|
+
import { readGlobalConfig } from "../agent/state";
|
|
9
|
+
import {
|
|
10
|
+
defaultSubagentRole,
|
|
11
|
+
resolveSubagentModel,
|
|
12
|
+
resolveSubagentMaxSteps,
|
|
13
|
+
subagentSystemPrompt,
|
|
14
|
+
subagentToolset,
|
|
15
|
+
} from "../agent/subagents";
|
|
16
|
+
import type { Message } from "../agent/loop";
|
|
17
|
+
|
|
18
|
+
export type RalphStreamKind = "step" | "complete" | "error";
|
|
19
|
+
|
|
20
|
+
export function formatRalphTodoGuide(tasks: string[], activeIndex = 0, completed: readonly string[] = []): string[] {
|
|
21
|
+
const done = new Set(completed);
|
|
22
|
+
const lines = [
|
|
23
|
+
"[RALPH] Subagent guidance: follow todos in order; stream every step, complete, and error event.",
|
|
24
|
+
];
|
|
25
|
+
tasks.forEach((task, index) => {
|
|
26
|
+
const mark = done.has(task) ? "x" : index === activeIndex ? ">" : " ";
|
|
27
|
+
lines.push(`[TODO] ${index + 1}/${tasks.length} [${mark}] ${task}`);
|
|
28
|
+
});
|
|
29
|
+
return lines;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatRalphStreamEvent(kind: RalphStreamKind, message: string): string {
|
|
33
|
+
const label = kind === "complete" ? "complete" : kind === "error" ? "error" : "step";
|
|
34
|
+
return ` └─ stream:${label} ${message}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RalphSubagentPromptContext {
|
|
38
|
+
task: string;
|
|
39
|
+
tasks: string[];
|
|
40
|
+
activeIndex: number;
|
|
41
|
+
completed?: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildRalphSubagentPrompt(ctx: RalphSubagentPromptContext): string {
|
|
45
|
+
const guide = formatRalphTodoGuide(ctx.tasks, ctx.activeIndex, ctx.completed ?? []).join("\n");
|
|
46
|
+
return [
|
|
47
|
+
"You are an ooo ralph subagent executing one todo from an immutable plan.",
|
|
48
|
+
"",
|
|
49
|
+
guide,
|
|
50
|
+
"",
|
|
51
|
+
`Current todo: ${ctx.activeIndex + 1}/${ctx.tasks.length} "${ctx.task}"`,
|
|
52
|
+
"",
|
|
53
|
+
"Rules:",
|
|
54
|
+
"- Execute ONLY the current [>] todo; do not skip ahead or rewrite the todo list.",
|
|
55
|
+
"- Treat completed [x] todos as context only; do not redo them unless required to verify this todo.",
|
|
56
|
+
"- Use tools in small steps and verify the current todo before calling done.",
|
|
57
|
+
"- The caller streams your lifecycle as stream:step, stream:complete, and stream:error; keep done.reason concise.",
|
|
58
|
+
].join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
export async function runTeamCommand(): Promise<void> {
|
|
63
|
+
const cwd = process.cwd();
|
|
64
|
+
|
|
65
|
+
// Read ralplan state
|
|
66
|
+
const planState = await readWorkflowState("ralplan", cwd);
|
|
67
|
+
if (!planState || planState.current_phase !== "complete" || !planState.plan_path) {
|
|
68
|
+
console.log(
|
|
69
|
+
`[ERROR] No completed plan found. Please run 'joc ralplan' to generate a plan first.`
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!planState.approved) {
|
|
75
|
+
console.log(
|
|
76
|
+
`[ERROR] Plan is not approved. Please approve the plan before executing.`
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const planPath = planState.plan_path;
|
|
82
|
+
console.log(`\n=== Starting Team Execution Stage ===`);
|
|
83
|
+
console.log(`Reading plan from: ${planPath}`);
|
|
84
|
+
|
|
85
|
+
let planContent = "";
|
|
86
|
+
try {
|
|
87
|
+
planContent = await fs.readFile(planPath, "utf-8");
|
|
88
|
+
} catch (err: any) {
|
|
89
|
+
console.log(`[ERROR] Failed to read plan file: ${err.message}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let rawPlan: any;
|
|
94
|
+
try {
|
|
95
|
+
rawPlan = parseYaml(planContent);
|
|
96
|
+
} catch (err: any) {
|
|
97
|
+
console.log(`[ERROR] Failed to parse plan YAML: ${err.message}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parsed = PlanSchema.safeParse(rawPlan);
|
|
102
|
+
if (!parsed.success) {
|
|
103
|
+
console.log(`[ERROR] Plan validation failed: ${parsed.error.message}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const tasks = parsed.data.steps.map(step => step.name);
|
|
108
|
+
|
|
109
|
+
console.log(`Loaded ${tasks.length} tasks for execution.`);
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
// Initialize team state
|
|
113
|
+
let teamState = await readWorkflowState("team", cwd) || {
|
|
114
|
+
active: true,
|
|
115
|
+
current_phase: "executing",
|
|
116
|
+
skill: "team" as const,
|
|
117
|
+
slug: planState.slug,
|
|
118
|
+
plan_path: planPath,
|
|
119
|
+
completed_tasks: [],
|
|
120
|
+
pending_tasks: [...tasks],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
await writeWorkflowState("team", teamState, cwd);
|
|
124
|
+
for (const line of formatRalphTodoGuide(tasks, Math.max(0, tasks.indexOf(teamState.pending_tasks?.[0] ?? "")), teamState.completed_tasks ?? [])) console.log(line);
|
|
125
|
+
|
|
126
|
+
while (teamState.pending_tasks && teamState.pending_tasks.length > 0) {
|
|
127
|
+
const currentTask = teamState.pending_tasks[0];
|
|
128
|
+
console.log(`\n[TASK] Current: "${currentTask}"`);
|
|
129
|
+
const activeIndex = tasks.indexOf(currentTask);
|
|
130
|
+
for (const line of formatRalphTodoGuide(tasks, activeIndex, teamState.completed_tasks ?? [])) console.log(line);
|
|
131
|
+
|
|
132
|
+
// Run the Executor loop
|
|
133
|
+
const success = await executeTaskWithAgent({
|
|
134
|
+
task: currentTask,
|
|
135
|
+
tasks,
|
|
136
|
+
activeIndex,
|
|
137
|
+
completed: teamState.completed_tasks ?? [],
|
|
138
|
+
cwd,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (success) {
|
|
142
|
+
teamState.completed_tasks = [...(teamState.completed_tasks ?? []), currentTask];
|
|
143
|
+
teamState.pending_tasks = teamState.pending_tasks.slice(1);
|
|
144
|
+
await writeWorkflowState("team", teamState, cwd);
|
|
145
|
+
console.log(`[TASK SUCCESS] Completed: "${currentTask}"`);
|
|
146
|
+
} else {
|
|
147
|
+
console.log(`[TASK FAILED] Failed on task: "${currentTask}". Halting execution.`);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (teamState.pending_tasks && teamState.pending_tasks.length === 0) {
|
|
153
|
+
teamState.current_phase = "complete";
|
|
154
|
+
await writeWorkflowState("team", teamState, cwd);
|
|
155
|
+
console.log("\n[SUCCESS] All tasks in the plan executed successfully!");
|
|
156
|
+
console.log("Run 'joc ultragoal' to run verify tests and evaluate metrics.");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: string }): Promise<boolean> {
|
|
161
|
+
const config = await readGlobalConfig();
|
|
162
|
+
const role = defaultSubagentRole();
|
|
163
|
+
const model = resolveSubagentModel(role.id, config);
|
|
164
|
+
const maxSteps = resolveSubagentMaxSteps(role.id, config);
|
|
165
|
+
console.log(` └─ Subagent: ${role.title} · model ${model} · ≤${maxSteps} steps`);
|
|
166
|
+
|
|
167
|
+
const history: Message[] = [
|
|
168
|
+
{ role: "system", content: subagentSystemPrompt(role) },
|
|
169
|
+
{ role: "user", content: buildRalphSubagentPrompt(ctx) },
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const result = await runAgentLoop(history, {
|
|
173
|
+
cwd: ctx.cwd,
|
|
174
|
+
model,
|
|
175
|
+
maxSteps,
|
|
176
|
+
tools: subagentToolset(role),
|
|
177
|
+
events: {
|
|
178
|
+
onAssistant: (_raw, invocation) => {
|
|
179
|
+
if (!invocation) {
|
|
180
|
+
console.log(formatRalphStreamEvent("error", "invalid tool-call json; retrying"));
|
|
181
|
+
} else if (invocation.tool !== "done") {
|
|
182
|
+
console.log(formatRalphStreamEvent("step", `tool ${invocation.tool} requested`));
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
onStep: step => console.log(formatRalphStreamEvent("step", `${role.title} thinking ${step}/${maxSteps}`)),
|
|
186
|
+
onToolResult: (tool, ok) => console.log(formatRalphStreamEvent(ok ? "complete" : "error", `tool ${tool}`)),
|
|
187
|
+
onError: msg => console.log(formatRalphStreamEvent("error", msg)),
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (result.done) {
|
|
192
|
+
console.log(formatRalphStreamEvent("complete", `${role.title} finished task`));
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
console.log(formatRalphStreamEvent("error", `${role.title} did not converge within ${result.steps} steps`));
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
export const StepSchema = z.object({
|
|
199
|
+
name: z.string(),
|
|
200
|
+
}).passthrough();
|
|
201
|
+
|
|
202
|
+
export const PlanSchema = z.object({
|
|
203
|
+
name: z.string(),
|
|
204
|
+
steps: z.array(StepSchema),
|
|
205
|
+
}).passthrough();
|
|
206
|
+
|
|
207
|
+
function parseValue(v: string): any {
|
|
208
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
209
|
+
return v.slice(1, -1);
|
|
210
|
+
}
|
|
211
|
+
if (v === "true") return true;
|
|
212
|
+
if (v === "false") return false;
|
|
213
|
+
if (v === "null") return null;
|
|
214
|
+
if (v === "") return "";
|
|
215
|
+
if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
|
|
216
|
+
return v;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function parseYaml(yamlStr: string): any {
|
|
220
|
+
const lines = yamlStr.split(/\r?\n/).map(line => {
|
|
221
|
+
const commentIdx = line.indexOf('#');
|
|
222
|
+
const cleanLine = commentIdx !== -1 ? line.slice(0, commentIdx) : line;
|
|
223
|
+
return {
|
|
224
|
+
raw: cleanLine,
|
|
225
|
+
trimmed: cleanLine.trim(),
|
|
226
|
+
indent: cleanLine.length - cleanLine.trimStart().length
|
|
227
|
+
};
|
|
228
|
+
}).filter(l => l.trimmed !== '');
|
|
229
|
+
|
|
230
|
+
let idx = 0;
|
|
231
|
+
|
|
232
|
+
function parseBlock(baseIndent: number): any {
|
|
233
|
+
let result: any = null;
|
|
234
|
+
let isArray = false;
|
|
235
|
+
|
|
236
|
+
if (idx < lines.length) {
|
|
237
|
+
if (lines[idx].trimmed.startsWith('-')) {
|
|
238
|
+
isArray = true;
|
|
239
|
+
result = [];
|
|
240
|
+
} else {
|
|
241
|
+
result = {};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
while (idx < lines.length) {
|
|
246
|
+
const line = lines[idx];
|
|
247
|
+
if (line.indent < baseIndent) {
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isArray) {
|
|
252
|
+
if (!line.trimmed.startsWith('-')) {
|
|
253
|
+
if (result.length > 0 && typeof result[result.length - 1] === 'object') {
|
|
254
|
+
const colonIdx = line.trimmed.indexOf(':');
|
|
255
|
+
if (colonIdx !== -1) {
|
|
256
|
+
const k = line.trimmed.slice(0, colonIdx).trim();
|
|
257
|
+
const rawVal = line.trimmed.slice(colonIdx + 1).trim();
|
|
258
|
+
if (rawVal === '') {
|
|
259
|
+
idx++;
|
|
260
|
+
result[result.length - 1][k] = parseBlock(line.indent + 1);
|
|
261
|
+
continue;
|
|
262
|
+
} else {
|
|
263
|
+
result[result.length - 1][k] = parseValue(rawVal);
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
throw new Error(`Invalid line inside array block: "${line.trimmed}"`);
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
throw new Error(`Invalid line in array: "${line.trimmed}"`);
|
|
270
|
+
}
|
|
271
|
+
idx++;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const rest = line.trimmed.slice(1).trim();
|
|
276
|
+
if (rest === '') {
|
|
277
|
+
idx++;
|
|
278
|
+
const nested = parseBlock(line.indent + 1);
|
|
279
|
+
result.push(nested);
|
|
280
|
+
} else if (rest.includes(':')) {
|
|
281
|
+
const colonIdx = rest.indexOf(':');
|
|
282
|
+
const k = rest.slice(0, colonIdx).trim();
|
|
283
|
+
const rawVal = rest.slice(colonIdx + 1).trim();
|
|
284
|
+
if (rawVal === '') {
|
|
285
|
+
idx++;
|
|
286
|
+
const nestedObj = { [k]: parseBlock(line.indent + 2) };
|
|
287
|
+
result.push(nestedObj);
|
|
288
|
+
} else {
|
|
289
|
+
const item: any = { [k]: parseValue(rawVal) };
|
|
290
|
+
result.push(item);
|
|
291
|
+
idx++;
|
|
292
|
+
while (idx < lines.length && !lines[idx].trimmed.startsWith('-') && lines[idx].indent >= line.indent + 2) {
|
|
293
|
+
const subLine = lines[idx];
|
|
294
|
+
const subColonIdx = subLine.trimmed.indexOf(':');
|
|
295
|
+
if (subColonIdx !== -1) {
|
|
296
|
+
const subK = subLine.trimmed.slice(0, subColonIdx).trim();
|
|
297
|
+
const rawSubVal = subLine.trimmed.slice(subColonIdx + 1).trim();
|
|
298
|
+
if (rawSubVal === '') {
|
|
299
|
+
idx++;
|
|
300
|
+
item[subK] = parseBlock(subLine.indent + 1);
|
|
301
|
+
} else {
|
|
302
|
+
item[subK] = parseValue(rawSubVal);
|
|
303
|
+
idx++;
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
throw new Error(`Invalid sub-line in block mapping: "${subLine.trimmed}"`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
result.push(parseValue(rest));
|
|
312
|
+
idx++;
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
const colonIdx = line.trimmed.indexOf(':');
|
|
316
|
+
if (colonIdx === -1) {
|
|
317
|
+
throw new Error(`Invalid line: "${line.trimmed}"`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const k = line.trimmed.slice(0, colonIdx).trim();
|
|
321
|
+
const rawVal = line.trimmed.slice(colonIdx + 1).trim();
|
|
322
|
+
|
|
323
|
+
if (rawVal === '') {
|
|
324
|
+
idx++;
|
|
325
|
+
result[k] = parseBlock(line.indent + 1);
|
|
326
|
+
} else {
|
|
327
|
+
result[k] = parseValue(rawVal);
|
|
328
|
+
idx++;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return parseBlock(0);
|
|
337
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { readWorkflowState, getLocalJocDir } from "../agent/state";
|
|
4
|
+
import { bashTool } from "../agent/tools";
|
|
5
|
+
|
|
6
|
+
export async function runUltragoalCommand(): Promise<void> {
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
|
|
9
|
+
// Read state to find acceptance criteria
|
|
10
|
+
const interviewState = await readWorkflowState("deep-interview", cwd);
|
|
11
|
+
if (!interviewState || !interviewState.seed_path) {
|
|
12
|
+
console.log(
|
|
13
|
+
`[ERROR] No crystallized requirements found. Please run 'joc deep-interview' first.`
|
|
14
|
+
);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const seedPath = interviewState.seed_path;
|
|
19
|
+
console.log(`\n=== Starting Ultragoal Verification Stage ===`);
|
|
20
|
+
console.log(`Reading requirements and acceptance criteria from: ${seedPath}`);
|
|
21
|
+
|
|
22
|
+
let seedContent = "";
|
|
23
|
+
try {
|
|
24
|
+
seedContent = await fs.readFile(seedPath, "utf-8");
|
|
25
|
+
} catch (err: any) {
|
|
26
|
+
console.log(`[ERROR] Failed to read seed file: ${err.message}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Parse acceptance criteria from seed YAML
|
|
31
|
+
const criteria: string[] = [];
|
|
32
|
+
const lines = seedContent.split("\n");
|
|
33
|
+
let parsingCriteria = false;
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (trimmed.startsWith("acceptance_criteria:")) {
|
|
37
|
+
parsingCriteria = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (parsingCriteria) {
|
|
41
|
+
if (trimmed.startsWith("-")) {
|
|
42
|
+
criteria.push(trimmed.replace(/^-\s*/, "").replace(/"/g, "").trim());
|
|
43
|
+
} else if (trimmed === "" || trimmed.includes(":")) {
|
|
44
|
+
// End of list or next section
|
|
45
|
+
parsingCriteria = false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (criteria.length === 0) {
|
|
51
|
+
criteria.push("Runs successfully in the terminal");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`Loaded ${criteria.length} acceptance criteria for verification.\n`);
|
|
55
|
+
|
|
56
|
+
const results: { criterion: string; passed: boolean; output: string }[] = [];
|
|
57
|
+
|
|
58
|
+
for (const criterion of criteria) {
|
|
59
|
+
console.log(`[CHECK] Verifying: "${criterion}"`);
|
|
60
|
+
|
|
61
|
+
// We can execute a automatic verification pass.
|
|
62
|
+
// E.g., if there are tests, we run bun test. If not, we do a smoke check by running bun src/cli.ts setup or compile.
|
|
63
|
+
let cmd = "bun test";
|
|
64
|
+
if (criterion.toLowerCase().includes("run") || criterion.toLowerCase().includes("cli")) {
|
|
65
|
+
cmd = "bun run src/cli.ts --help";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(` └─ Running validation command: '${cmd}'`);
|
|
69
|
+
const res = await bashTool(cmd, cwd);
|
|
70
|
+
|
|
71
|
+
results.push({
|
|
72
|
+
criterion,
|
|
73
|
+
passed: res.success,
|
|
74
|
+
output: res.output.slice(0, 300) + (res.output.length > 300 ? "..." : "")
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
console.log(` └─ Result: ${res.success ? "PASSED" : "FAILED"}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Write verification report
|
|
81
|
+
const reportDir = path.join(getLocalJocDir(cwd), "state");
|
|
82
|
+
await fs.mkdir(reportDir, { recursive: true });
|
|
83
|
+
const reportPath = path.join(reportDir, "ultragoal-report.md");
|
|
84
|
+
|
|
85
|
+
const passedCount = results.filter(r => r.passed).length;
|
|
86
|
+
const totalCount = results.length;
|
|
87
|
+
const status = passedCount === totalCount ? "SUCCESS" : "DEGRADED";
|
|
88
|
+
|
|
89
|
+
const reportContent =
|
|
90
|
+
`# Ultragoal Verification Report: ${interviewState.slug}\n` +
|
|
91
|
+
`Date: ${new Date().toISOString()}\n` +
|
|
92
|
+
`Status: ${status} (${passedCount}/${totalCount} criteria passed)\n\n` +
|
|
93
|
+
`## Criteria Verification Matrix\n` +
|
|
94
|
+
`| Criterion | Status | Verification Output |\n` +
|
|
95
|
+
`|---|---|---|\n` +
|
|
96
|
+
results.map(r => `| ${r.criterion} | ${r.passed ? "✅ PASSED" : "❌ FAILED"} | \`${r.output.replace(/\n/g, " ")}\` |`).join("\n") +
|
|
97
|
+
`\n`;
|
|
98
|
+
|
|
99
|
+
await fs.writeFile(reportPath, reportContent, "utf-8");
|
|
100
|
+
console.log(`\n[VERIFICATION COMPLETE] Report saved to: ${reportPath}`);
|
|
101
|
+
console.log(`Overall status: ${status} (${passedCount}/${totalCount} passed)`);
|
|
102
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export * from "./agent/state";
|
|
2
|
+
export { callLlm } from "./agent/loop";
|
|
3
|
+
export type { ChatOptions } from "./agent/loop";
|
|
4
|
+
export * from "./agent/tools";
|
|
5
|
+
export * from "./auth";
|
|
6
|
+
export * from "./ai";
|
|
7
|
+
export * from "./cli";
|
|
8
|
+
export {
|
|
9
|
+
runMcpServer,
|
|
10
|
+
TOOLS,
|
|
11
|
+
PARSE_ERROR,
|
|
12
|
+
INVALID_REQUEST,
|
|
13
|
+
METHOD_NOT_FOUND,
|
|
14
|
+
INVALID_PARAMS,
|
|
15
|
+
INTERNAL_ERROR,
|
|
16
|
+
ok,
|
|
17
|
+
fail,
|
|
18
|
+
} from "./mcp";
|
|
19
|
+
export type {
|
|
20
|
+
JsonRpcRequest,
|
|
21
|
+
JsonRpcResponse,
|
|
22
|
+
JsonRpcError,
|
|
23
|
+
ToolDefinition,
|
|
24
|
+
ToolResult as McpToolResult,
|
|
25
|
+
} from "./mcp";
|
|
26
|
+
export * from "./commands/setup";
|
|
27
|
+
export * from "./commands/auth";
|
|
28
|
+
export * from "./commands/deep-interview";
|
|
29
|
+
export * from "./commands/ralplan";
|
|
30
|
+
export * from "./commands/team";
|
|
31
|
+
export * from "./commands/ultragoal";
|
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface JsonRpcRequest {
|
|
2
|
+
jsonrpc: "2.0";
|
|
3
|
+
id?: number | string | null;
|
|
4
|
+
method: string;
|
|
5
|
+
params?: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface JsonRpcResponse {
|
|
9
|
+
jsonrpc: "2.0";
|
|
10
|
+
id: number | string | null;
|
|
11
|
+
result?: unknown;
|
|
12
|
+
error?: JsonRpcError;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface JsonRpcError {
|
|
16
|
+
code: number;
|
|
17
|
+
message: string;
|
|
18
|
+
data?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const PARSE_ERROR = -32700;
|
|
22
|
+
export const INVALID_REQUEST = -32600;
|
|
23
|
+
export const METHOD_NOT_FOUND = -32601;
|
|
24
|
+
export const INVALID_PARAMS = -32602;
|
|
25
|
+
export const INTERNAL_ERROR = -32603;
|
|
26
|
+
|
|
27
|
+
export interface ToolDefinition {
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
inputSchema: Record<string, unknown>;
|
|
31
|
+
handler: (args: Record<string, unknown>) => Promise<ToolResult>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ToolResult {
|
|
35
|
+
content: { type: "text"; text: string }[];
|
|
36
|
+
isError?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ok(id: number | string | null, result: unknown): JsonRpcResponse {
|
|
40
|
+
return { jsonrpc: "2.0", id, result };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function fail(id: number | string | null, code: number, message: string, data?: unknown): JsonRpcResponse {
|
|
44
|
+
return { jsonrpc: "2.0", id, error: { code, message, data } };
|
|
45
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fail,
|
|
3
|
+
INTERNAL_ERROR,
|
|
4
|
+
INVALID_PARAMS,
|
|
5
|
+
INVALID_REQUEST,
|
|
6
|
+
METHOD_NOT_FOUND,
|
|
7
|
+
ok,
|
|
8
|
+
PARSE_ERROR,
|
|
9
|
+
type JsonRpcRequest,
|
|
10
|
+
type JsonRpcResponse,
|
|
11
|
+
type ToolDefinition,
|
|
12
|
+
} from "./protocol";
|
|
13
|
+
import { TOOLS } from "./tools";
|
|
14
|
+
|
|
15
|
+
const SERVER_INFO = { name: "joc-mcp", version: "0.1.0" };
|
|
16
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
17
|
+
|
|
18
|
+
interface ServerOptions {
|
|
19
|
+
tools?: ToolDefinition[];
|
|
20
|
+
log?: (line: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runMcpServer(options: ServerOptions = {}): Promise<void> {
|
|
24
|
+
const tools = options.tools ?? TOOLS;
|
|
25
|
+
const log = options.log ?? (line => process.stderr.write(`${line}\n`));
|
|
26
|
+
log(`joc-mcp v${SERVER_INFO.version} listening on stdio (${tools.length} tools)`);
|
|
27
|
+
|
|
28
|
+
let buffer = "";
|
|
29
|
+
const decoder = new TextDecoder();
|
|
30
|
+
|
|
31
|
+
for await (const chunk of (process.stdin as unknown as AsyncIterable<Uint8Array>)) {
|
|
32
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
33
|
+
let newlineIndex: number;
|
|
34
|
+
while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
|
|
35
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
36
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
37
|
+
if (!line) continue;
|
|
38
|
+
const response = await handleLine(line, tools);
|
|
39
|
+
if (response) writeResponse(response);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function handleLine(line: string, tools: ToolDefinition[]): Promise<JsonRpcResponse | null> {
|
|
45
|
+
let req: JsonRpcRequest;
|
|
46
|
+
try {
|
|
47
|
+
req = JSON.parse(line) as JsonRpcRequest;
|
|
48
|
+
} catch {
|
|
49
|
+
return fail(null, PARSE_ERROR, "invalid JSON");
|
|
50
|
+
}
|
|
51
|
+
if (req.jsonrpc !== "2.0" || typeof req.method !== "string") {
|
|
52
|
+
return fail(req.id ?? null, INVALID_REQUEST, "malformed JSON-RPC request");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
switch (req.method) {
|
|
57
|
+
case "initialize":
|
|
58
|
+
return ok(req.id ?? null, {
|
|
59
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
60
|
+
capabilities: { tools: {} },
|
|
61
|
+
serverInfo: SERVER_INFO,
|
|
62
|
+
});
|
|
63
|
+
case "initialized":
|
|
64
|
+
case "notifications/initialized":
|
|
65
|
+
return null;
|
|
66
|
+
case "ping":
|
|
67
|
+
return ok(req.id ?? null, {});
|
|
68
|
+
case "tools/list":
|
|
69
|
+
return ok(req.id ?? null, {
|
|
70
|
+
tools: tools.map(t => ({
|
|
71
|
+
name: t.name,
|
|
72
|
+
description: t.description,
|
|
73
|
+
inputSchema: t.inputSchema,
|
|
74
|
+
})),
|
|
75
|
+
});
|
|
76
|
+
case "tools/call":
|
|
77
|
+
return await handleToolCall(req, tools);
|
|
78
|
+
default:
|
|
79
|
+
return fail(req.id ?? null, METHOD_NOT_FOUND, `unknown method: ${req.method}`);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return fail(req.id ?? null, INTERNAL_ERROR, (err as Error).message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function handleToolCall(req: JsonRpcRequest, tools: ToolDefinition[]): Promise<JsonRpcResponse> {
|
|
87
|
+
const params = (req.params ?? {}) as { name?: string; arguments?: Record<string, unknown> };
|
|
88
|
+
if (!params.name) return fail(req.id ?? null, INVALID_PARAMS, "'name' is required");
|
|
89
|
+
const tool = tools.find(t => t.name === params.name);
|
|
90
|
+
if (!tool) return fail(req.id ?? null, METHOD_NOT_FOUND, `unknown tool: ${params.name}`);
|
|
91
|
+
const result = await tool.handler(params.arguments ?? {});
|
|
92
|
+
return ok(req.id ?? null, result);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function writeResponse(response: JsonRpcResponse): void {
|
|
96
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
97
|
+
}
|