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,18 @@
|
|
|
1
|
+
import { runMcpServer } from "../mcp";
|
|
2
|
+
|
|
3
|
+
export async function runMcpCommand(args: string[]): Promise<void> {
|
|
4
|
+
const sub = args[0];
|
|
5
|
+
if (sub === "serve" || !sub) {
|
|
6
|
+
await runMcpServer();
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (sub === "tools") {
|
|
10
|
+
const { TOOLS } = await import("../mcp");
|
|
11
|
+
console.log(`Available joc-mcp tools (${TOOLS.length}):`);
|
|
12
|
+
for (const t of TOOLS) console.log(` ${t.name.padEnd(28)} ${t.description}`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
console.log(`unknown 'joc mcp' subcommand: ${sub}`);
|
|
16
|
+
console.log("Usage: joc mcp [serve|tools]");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { readGlobalConfig } from "../agent/state";
|
|
2
|
+
import { listAliases, resolveModelId } from "../ai/model-registry";
|
|
3
|
+
import { resolveProvider, resolveRoleModel } from "../ai/model-manager";
|
|
4
|
+
import { describeAllProviders } from "../ai/provider-status";
|
|
5
|
+
import { discoverModels } from "../ai/model-discovery";
|
|
6
|
+
import { formatLiveModels, formatCatalogTable, formatEnrichedModels } from "../tui/components/config-panel";
|
|
7
|
+
import { MODEL_CATALOG, fuzzyMatchCatalog, type ThinkLevel } from "../ai/model-catalog";
|
|
8
|
+
import { enrichAll, filterCapable, sortByCapability, knownCount } from "../ai/model-enrich";
|
|
9
|
+
|
|
10
|
+
async function probeOllama(baseUrl: string): Promise<string[]> {
|
|
11
|
+
try {
|
|
12
|
+
const r = await fetch(`${baseUrl.replace(/\/$/, "")}/api/tags`, { signal: AbortSignal.timeout(2500) });
|
|
13
|
+
if (!r.ok) return [];
|
|
14
|
+
const data = (await r.json()) as { models?: { name: string }[] };
|
|
15
|
+
return (data.models ?? []).map(m => `ollama/${m.name}`);
|
|
16
|
+
} catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function probeOpenAiCompat(baseUrl: string): Promise<string[]> {
|
|
22
|
+
try {
|
|
23
|
+
const r = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, { signal: AbortSignal.timeout(2500) });
|
|
24
|
+
if (!r.ok) return [];
|
|
25
|
+
const data = (await r.json()) as { data?: { id: string }[] };
|
|
26
|
+
return (data.data ?? []).map(m => `openai/${m.id}`);
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runModelsCommand(args: string[] = []): Promise<void> {
|
|
33
|
+
const checkMode = args.includes("--check");
|
|
34
|
+
const providerFilter = args.find(a => ["anthropic", "openai", "gemini", "ollama"].includes(a.toLowerCase()))?.toLowerCase();
|
|
35
|
+
if (args.includes("--catalog")) {
|
|
36
|
+
const query = args.find(a => !a.startsWith("--") && !["anthropic", "openai", "gemini", "ollama"].includes(a.toLowerCase()));
|
|
37
|
+
const rows = query ? fuzzyMatchCatalog(query) : [...MODEL_CATALOG];
|
|
38
|
+
console.log("\n=== joc models --catalog ===");
|
|
39
|
+
console.log(`Known model capabilities${query ? ` matching '${query}'` : ""}:`);
|
|
40
|
+
for (const line of formatCatalogTable(rows)) console.log(line);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (args.includes("--caps")) {
|
|
44
|
+
const cfg = await readGlobalConfig();
|
|
45
|
+
const def = await resolveModelId(cfg.defaultModel);
|
|
46
|
+
const thinkArg = args.find(a => a.startsWith("--thinking="))?.split("=")[1] as ThinkLevel | undefined;
|
|
47
|
+
const filter = {
|
|
48
|
+
thinking: thinkArg,
|
|
49
|
+
images: args.includes("--images") ? true : undefined,
|
|
50
|
+
minContext: args.includes("--long") ? 200_000 : undefined,
|
|
51
|
+
};
|
|
52
|
+
console.log("\n=== joc models --caps (live + capabilities) ===");
|
|
53
|
+
const live = await discoverModels({ config: cfg, timeoutMs: 4000 });
|
|
54
|
+
const enriched = sortByCapability(filterCapable(enrichAll(live), filter));
|
|
55
|
+
const { known, unknown } = knownCount(enriched);
|
|
56
|
+
for (const line of formatEnrichedModels(enriched, { current: def })) console.log(line);
|
|
57
|
+
console.log(`\n${known} with known capabilities, ${unknown} unknown.`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const config = await readGlobalConfig();
|
|
61
|
+
console.log("\n=== joc models ===");
|
|
62
|
+
const resolved = await resolveModelId(config.defaultModel);
|
|
63
|
+
console.log(`Default model: ${config.defaultModel}${resolved !== config.defaultModel ? ` → ${resolved}` : ""} → ${resolveProvider(resolved)}`);
|
|
64
|
+
console.log(`Role tiers: smol=${resolveRoleModel("smol", config)} · slow=${resolveRoleModel("slow", config)} · plan=${resolveRoleModel("plan", config)}`);
|
|
65
|
+
|
|
66
|
+
const aliases = await listAliases();
|
|
67
|
+
console.log("\nAliases (use as the model id; config overrides built-ins):");
|
|
68
|
+
for (const [alias, target] of Object.entries(aliases)) {
|
|
69
|
+
console.log(` ${alias.padEnd(10)} → ${target.padEnd(22)} (${resolveProvider(target)})`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ollamaBase = config.ollamaBaseUrl ?? "http://localhost:11434";
|
|
73
|
+
const ollama = await probeOllama(ollamaBase);
|
|
74
|
+
console.log(`\nLocal Ollama (${ollamaBase}):`);
|
|
75
|
+
if (ollama.length) for (const m of ollama.slice(0, 30)) console.log(` ${m}`);
|
|
76
|
+
else console.log(" (none reachable)");
|
|
77
|
+
|
|
78
|
+
if (config.openaiBaseUrl) {
|
|
79
|
+
const compat = await probeOpenAiCompat(config.openaiBaseUrl);
|
|
80
|
+
console.log(`\nOpenAI-compatible (${config.openaiBaseUrl}):`);
|
|
81
|
+
if (compat.length) for (const m of compat.slice(0, 30)) console.log(` ${m}`);
|
|
82
|
+
else console.log(" (none reachable)");
|
|
83
|
+
}
|
|
84
|
+
console.log("\nProvider credentials:");
|
|
85
|
+
for (const status of await describeAllProviders(config)) {
|
|
86
|
+
const base = status.baseUrl ? ` [${status.baseUrl}]` : "";
|
|
87
|
+
console.log(` ${status.name.padEnd(10)} ${status.ready ? "✓" : "·"} ${status.label}${base}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log("\nLive models (logged-in providers):");
|
|
91
|
+
let live = await discoverModels({ config, timeoutMs: 4000 });
|
|
92
|
+
if (providerFilter) live = live.filter(r => r.provider === providerFilter);
|
|
93
|
+
if (checkMode) {
|
|
94
|
+
for (const r of live) {
|
|
95
|
+
const mark = r.ok ? "✓" : "✗";
|
|
96
|
+
const detail = r.ok ? `${r.models.length} models (${r.source})` : `${r.error} (${r.source})`;
|
|
97
|
+
console.log(` ${mark} ${r.provider.padEnd(10)} ${detail}`);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
for (const line of formatLiveModels(live, { current: resolved, perProvider: 20 })) console.log(line);
|
|
102
|
+
|
|
103
|
+
console.log("\nSet a default with 'joc setup' or JOC_DEFAULT_MODEL=<id>.");
|
|
104
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { callLlm } from "../agent/loop";
|
|
4
|
+
import {
|
|
5
|
+
readWorkflowState,
|
|
6
|
+
writeWorkflowState,
|
|
7
|
+
getLocalJocDir,
|
|
8
|
+
type WorkflowState,
|
|
9
|
+
} from "../agent/state";
|
|
10
|
+
|
|
11
|
+
export async function runRalplanCommand(): Promise<void> {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
|
|
14
|
+
// Read deep-interview state
|
|
15
|
+
const interviewState = await readWorkflowState("deep-interview", cwd);
|
|
16
|
+
if (!interviewState || interviewState.current_phase !== "complete" || !interviewState.seed_path) {
|
|
17
|
+
console.log(
|
|
18
|
+
`[ERROR] No crystallized requirements found. Please run 'joc deep-interview' to crystallize requirements first.`
|
|
19
|
+
);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const seedPath = interviewState.seed_path;
|
|
24
|
+
console.log(`\n=== Starting Ralplan Planning Stage ===`);
|
|
25
|
+
console.log(`Reading requirements seed from: ${seedPath}`);
|
|
26
|
+
|
|
27
|
+
let seedContent = "";
|
|
28
|
+
try {
|
|
29
|
+
seedContent = await fs.readFile(seedPath, "utf-8");
|
|
30
|
+
} catch (err: any) {
|
|
31
|
+
console.log(`[ERROR] Failed to read seed file: ${err.message}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Initialize ralplan state
|
|
36
|
+
const ralplanState: WorkflowState = {
|
|
37
|
+
active: true,
|
|
38
|
+
current_phase: "planning",
|
|
39
|
+
skill: "ralplan" as const,
|
|
40
|
+
slug: interviewState.slug,
|
|
41
|
+
seed_path: seedPath,
|
|
42
|
+
};
|
|
43
|
+
await writeWorkflowState("ralplan", ralplanState, cwd);
|
|
44
|
+
|
|
45
|
+
console.log("Generating resilient task sequence plan (Critiqued by Planner/Architect/Critic)...");
|
|
46
|
+
|
|
47
|
+
// Standard multi-role system prompt
|
|
48
|
+
const systemPrompt =
|
|
49
|
+
`You are the Ralplan Orchestrator, combining three expert roles:\n` +
|
|
50
|
+
`1. Planner: Focuses on sequencing tasks into a highly logical, outcome-based progression.\n` +
|
|
51
|
+
`2. Architect: Reviews technical feasibility, structural directories, and patterns.\n` +
|
|
52
|
+
`3. Critic: Critiques the plan for vagueness, redundant copies, and missing steps.\n\n` +
|
|
53
|
+
`Analyze the given crystallized spec (seed.yaml) and generate a step-by-step implementation plan.\n` +
|
|
54
|
+
`Output the final plan in YAML format. Ensure it contains a clear sequence of tasks with descriptive names and target files.\n` +
|
|
55
|
+
`Output ONLY the YAML. Do not include markdown wraps or code blocks.`;
|
|
56
|
+
|
|
57
|
+
const messages = [
|
|
58
|
+
{ role: "user" as const, content: `Here is the crystallized spec (seed.yaml):\n\n${seedContent}` }
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const rawPlan = await callLlm(messages, { systemPrompt });
|
|
63
|
+
const cleanPlan = rawPlan.replace(/```yaml|```/g, "").trim();
|
|
64
|
+
|
|
65
|
+
const planDir = path.join(getLocalJocDir(cwd), "plans");
|
|
66
|
+
await fs.mkdir(planDir, { recursive: true });
|
|
67
|
+
const planPath = path.join(planDir, `plan-${interviewState.slug}.yaml`);
|
|
68
|
+
|
|
69
|
+
await fs.writeFile(planPath, cleanPlan, "utf-8");
|
|
70
|
+
console.log(`\n[SUCCESS] Plan successfully created and saved to: ${planPath}`);
|
|
71
|
+
|
|
72
|
+
ralplanState.current_phase = "complete";
|
|
73
|
+
ralplanState.plan_path = planPath;
|
|
74
|
+
ralplanState.approved = false;
|
|
75
|
+
await writeWorkflowState("ralplan", ralplanState, cwd);
|
|
76
|
+
|
|
77
|
+
console.log("\nPlan preview:");
|
|
78
|
+
console.log("-----------------------------------------");
|
|
79
|
+
console.log(cleanPlan);
|
|
80
|
+
console.log("-----------------------------------------");
|
|
81
|
+
console.log("\n[Handoff Ready] The blueprint is prepared. Run 'joc team' to execute the plan.");
|
|
82
|
+
|
|
83
|
+
} catch (error: any) {
|
|
84
|
+
console.log(`[ERROR calling LLM during Planning]: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the model/provider setting flow (`joc setup`). Extracted from
|
|
3
|
+
* the readline-driven command so the validation, normalization, recommendation,
|
|
4
|
+
* and summary logic is unit-testable without a TTY.
|
|
5
|
+
*/
|
|
6
|
+
import type { ProviderName } from "../ai/types";
|
|
7
|
+
import { recommendedModel, validateModelId, suggestModels, findCatalogEntry, catalogForProvider } from "../ai/model-catalog-compat";
|
|
8
|
+
import { resolveProvider } from "../ai/model-manager";
|
|
9
|
+
import type { Config } from "../agent/state";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a base URL: trim, default when blank, add `http://` when no scheme,
|
|
13
|
+
* strip trailing slashes. Returns the fallback unchanged when input is empty.
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeBaseUrl(input: string | undefined, fallback: string): string {
|
|
16
|
+
let u = (input ?? "").trim();
|
|
17
|
+
if (!u) u = fallback;
|
|
18
|
+
if (!u) return u;
|
|
19
|
+
if (!/^https?:\/\//i.test(u)) u = `http://${u}`;
|
|
20
|
+
return u.replace(/\/+$/, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ModelChoiceResult {
|
|
24
|
+
model: string;
|
|
25
|
+
known: boolean;
|
|
26
|
+
/** Non-fatal advisory (provider mismatch or uncatalogued id). */
|
|
27
|
+
warning?: string;
|
|
28
|
+
/** "Did you mean" candidates when the typed id is unrecognized. */
|
|
29
|
+
suggestions: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the default model from a (possibly blank) typed value for a provider:
|
|
34
|
+
* - blank → the provider's recommended model
|
|
35
|
+
* - known id → accepted (warns when it routes to a different provider)
|
|
36
|
+
* - unknown id → accepted but flagged with "did you mean" suggestions
|
|
37
|
+
*/
|
|
38
|
+
export function chooseDefaultModel(typed: string | undefined, provider: ProviderName): ModelChoiceResult {
|
|
39
|
+
const t = (typed ?? "").trim();
|
|
40
|
+
if (!t) {
|
|
41
|
+
const rec = recommendedModel(provider) ?? "";
|
|
42
|
+
return { model: rec, known: !!findCatalogEntry(rec), suggestions: [] };
|
|
43
|
+
}
|
|
44
|
+
const v = validateModelId(t, provider);
|
|
45
|
+
if (v.known) {
|
|
46
|
+
const warning =
|
|
47
|
+
v.providerMatch === false ? `Note: '${t}' routes to ${v.entry!.provider}, not ${provider}.` : undefined;
|
|
48
|
+
return { model: t, known: true, warning, suggestions: [] };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
model: t,
|
|
52
|
+
known: false,
|
|
53
|
+
warning: `'${t}' is not in the model catalog (it may still work).`,
|
|
54
|
+
suggestions: suggestModels(t),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Top-N recommended catalog rows for a provider, as `id — note` display lines. */
|
|
59
|
+
export function recommendedModelsFor(provider: ProviderName, n = 5): string[] {
|
|
60
|
+
return catalogForProvider(provider)
|
|
61
|
+
.slice(0, n)
|
|
62
|
+
.map(e => `${e.id}${e.note ? ` — ${e.note}` : ""}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Human list of providers that are configured in a config object. */
|
|
66
|
+
export function buildEnabledProviders(config: Config): string[] {
|
|
67
|
+
const enabled: string[] = [];
|
|
68
|
+
const cfg = config as Config & { openaiBaseUrl?: string };
|
|
69
|
+
if (cfg.providers?.anthropic || cfg.oauth?.anthropic) enabled.push("anthropic");
|
|
70
|
+
if (cfg.providers?.openai || cfg.oauth?.openai) enabled.push("openai");
|
|
71
|
+
if (cfg.providers?.gemini || cfg.oauth?.gemini) enabled.push("gemini");
|
|
72
|
+
if (cfg.ollamaBaseUrl) enabled.push(`ollama(${cfg.ollamaBaseUrl})`);
|
|
73
|
+
if (cfg.openaiBaseUrl) enabled.push(`openai-compatible(${cfg.openaiBaseUrl})`);
|
|
74
|
+
return enabled;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build the post-save summary lines: default model + the provider it routes to,
|
|
79
|
+
* catalog metadata when known, and the enabled-provider list.
|
|
80
|
+
*/
|
|
81
|
+
export function buildSetupSummary(config: Config): string[] {
|
|
82
|
+
const lines: string[] = [];
|
|
83
|
+
const model = config.defaultModel;
|
|
84
|
+
const provider = resolveProvider(model);
|
|
85
|
+
const entry = findCatalogEntry(model);
|
|
86
|
+
const meta = entry
|
|
87
|
+
? ` (${entry.contextWindow ? `${Math.round(entry.contextWindow / 1000)}k ctx` : "ctx ?"}${entry.reasoning ? ", reasoning" : ""})`
|
|
88
|
+
: "";
|
|
89
|
+
lines.push(`Default model: ${model} → ${provider}${meta}`);
|
|
90
|
+
const enabled = buildEnabledProviders(config);
|
|
91
|
+
lines.push(`Enabled providers: ${enabled.join(", ") || "None"}`);
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { saveGlobalConfig, readGlobalConfig, type Config } from "../agent/state";
|
|
3
|
+
import {
|
|
4
|
+
interactiveLogin,
|
|
5
|
+
getStoredOAuth,
|
|
6
|
+
OAUTH_FLOW_REGISTRY,
|
|
7
|
+
openInBrowser,
|
|
8
|
+
type AuthProvider,
|
|
9
|
+
type OAuthController,
|
|
10
|
+
} from "../auth";
|
|
11
|
+
import {
|
|
12
|
+
normalizeBaseUrl,
|
|
13
|
+
chooseDefaultModel,
|
|
14
|
+
recommendedModelsFor,
|
|
15
|
+
buildSetupSummary,
|
|
16
|
+
} from "./setup-helpers";
|
|
17
|
+
|
|
18
|
+
/** Print a model choice's advisory warning + "did you mean" suggestions, if any. */
|
|
19
|
+
function reportModelChoice(r: { warning?: string; suggestions: string[] }): void {
|
|
20
|
+
if (r.warning) console.log(` ${r.warning}`);
|
|
21
|
+
if (r.suggestions.length) console.log(` Did you mean: ${r.suggestions.join(", ")}?`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ProviderChoice = "anthropic" | "openai" | "gemini" | "ollama" | "lmstudio" | "openai-compatible";
|
|
25
|
+
|
|
26
|
+
const DEFAULT_MODELS: Record<ProviderChoice, string> = {
|
|
27
|
+
anthropic: "claude-3-5-sonnet-20241022",
|
|
28
|
+
openai: "gpt-4o",
|
|
29
|
+
gemini: "gemini-2.0-flash",
|
|
30
|
+
ollama: "ollama/llama3.1:8b",
|
|
31
|
+
lmstudio: "openai/local-model",
|
|
32
|
+
"openai-compatible": "openai/local-model",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const DEFAULT_BASE_URLS: Partial<Record<ProviderChoice, string>> = {
|
|
36
|
+
ollama: "http://localhost:11434",
|
|
37
|
+
lmstudio: "http://localhost:1234/v1",
|
|
38
|
+
"openai-compatible": "http://localhost:8000/v1",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
async function listOllamaModels(baseUrl: string): Promise<string[]> {
|
|
42
|
+
try {
|
|
43
|
+
const r = await fetch(`${baseUrl}/api/tags`, { signal: AbortSignal.timeout(2000) });
|
|
44
|
+
if (!r.ok) return [];
|
|
45
|
+
const data = (await r.json()) as { models?: { name: string }[] };
|
|
46
|
+
return (data.models ?? []).map(m => m.name);
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function listOpenAiCompatibleModels(baseUrl: string, apiKey?: string): Promise<string[]> {
|
|
53
|
+
try {
|
|
54
|
+
const r = await fetch(`${baseUrl}/models`, {
|
|
55
|
+
headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {},
|
|
56
|
+
signal: AbortSignal.timeout(2000),
|
|
57
|
+
});
|
|
58
|
+
if (!r.ok) return [];
|
|
59
|
+
const data = (await r.json()) as { data?: { id: string }[] };
|
|
60
|
+
return (data.data ?? []).map(m => m.id);
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function runSetupCommand(): Promise<void> {
|
|
67
|
+
const current = await readGlobalConfig();
|
|
68
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
69
|
+
|
|
70
|
+
console.log("\n=== @jeo-code CLI Configuration (joc setup) ===");
|
|
71
|
+
console.log("Configure providers, API keys / OAuth tokens, and default model.\n");
|
|
72
|
+
|
|
73
|
+
console.log("Available provider types:");
|
|
74
|
+
console.log(" 1) anthropic — Claude (API key or OAuth bearer)");
|
|
75
|
+
console.log(" 2) openai — GPT (API key or OAuth bearer)");
|
|
76
|
+
console.log(" 3) gemini — Google AI Studio (API key or OAuth bearer)");
|
|
77
|
+
console.log(" 4) ollama — local Ollama (no auth)");
|
|
78
|
+
console.log(" 5) lmstudio — local LM Studio (OpenAI-compatible)");
|
|
79
|
+
console.log(" 6) openai-compatible — custom OpenAI-compatible (vLLM, llama-cpp-server, ...)");
|
|
80
|
+
console.log(" 7) skip — keep existing values, just change defaults\n");
|
|
81
|
+
|
|
82
|
+
const sel = (await rl.question("Configure which provider? [1-7]: ")).trim();
|
|
83
|
+
const map: Record<string, ProviderChoice | "skip"> = {
|
|
84
|
+
"1": "anthropic", "2": "openai", "3": "gemini",
|
|
85
|
+
"4": "ollama", "5": "lmstudio", "6": "openai-compatible",
|
|
86
|
+
"7": "skip",
|
|
87
|
+
};
|
|
88
|
+
const choice = map[sel] ?? "skip";
|
|
89
|
+
|
|
90
|
+
const next: Config = JSON.parse(JSON.stringify(current)) as Config;
|
|
91
|
+
next.providers = next.providers || {};
|
|
92
|
+
next.oauth = next.oauth || {};
|
|
93
|
+
|
|
94
|
+
if (choice === "anthropic" || choice === "openai" || choice === "gemini") {
|
|
95
|
+
const authMode = (
|
|
96
|
+
await rl.question("Auth mode: (b)rowser OAuth login, (t)oken paste, or api (k)ey? [b]: ")
|
|
97
|
+
).trim().toLowerCase();
|
|
98
|
+
if (authMode === "t") {
|
|
99
|
+
const tok = await rl.question(`${choice} OAuth bearer token: `);
|
|
100
|
+
if (tok.trim()) next.oauth[choice] = tok.trim();
|
|
101
|
+
} else if (authMode === "k") {
|
|
102
|
+
const key = await rl.question(`${choice} API key [${current.providers[choice] ? "********" : "None"}]: `);
|
|
103
|
+
if (key.trim()) next.providers[choice] = key.trim();
|
|
104
|
+
} else {
|
|
105
|
+
const flow = OAUTH_FLOW_REGISTRY[choice as AuthProvider];
|
|
106
|
+
if (!flow.verifiedEndToEnd && flow.note) console.log(`Note: ${flow.note}`);
|
|
107
|
+
const ctrl: OAuthController = {
|
|
108
|
+
onAuth: ({ url, instructions }) => {
|
|
109
|
+
console.log(`Opening browser:\n ${url}\n`);
|
|
110
|
+
if (instructions) console.log(instructions + "\n");
|
|
111
|
+
void openInBrowser(url);
|
|
112
|
+
},
|
|
113
|
+
onProgress: msg => console.log(` … ${msg}`),
|
|
114
|
+
onManualCodeInput: async () =>
|
|
115
|
+
(await rl.question("Paste redirect URL or code (or wait for the browser callback): ")).trim(),
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
const { email } = await interactiveLogin(choice as AuthProvider, ctrl);
|
|
119
|
+
const stored = await getStoredOAuth(choice as AuthProvider);
|
|
120
|
+
if (stored) next.oauth[choice] = stored;
|
|
121
|
+
console.log(`[SUCCESS] OAuth login complete for ${choice}${email ? ` (${email})` : ""}.`);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.log(`[FAILED] OAuth login: ${(err as Error).message}`);
|
|
124
|
+
console.log("Falling back — you can paste an API key instead.");
|
|
125
|
+
const key = await rl.question(`${choice} API key [skip]: `);
|
|
126
|
+
if (key.trim()) next.providers[choice] = key.trim();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
console.log(`\nRecommended ${choice} models:`);
|
|
130
|
+
for (const m of recommendedModelsFor(choice)) console.log(` - ${m}`);
|
|
131
|
+
const dm = await rl.question(`Default model for ${choice} [${recommendedModelsFor(choice)[0]?.split(" ")[0] ?? DEFAULT_MODELS[choice]}]: `);
|
|
132
|
+
const picked = chooseDefaultModel(dm, choice);
|
|
133
|
+
reportModelChoice(picked);
|
|
134
|
+
next.defaultModel = picked.model || DEFAULT_MODELS[choice];
|
|
135
|
+
} else if (choice === "ollama") {
|
|
136
|
+
const url = await rl.question(`Ollama base URL [${current.ollamaBaseUrl || DEFAULT_BASE_URLS.ollama}]: `);
|
|
137
|
+
next.ollamaBaseUrl = normalizeBaseUrl(url, current.ollamaBaseUrl || DEFAULT_BASE_URLS.ollama!);
|
|
138
|
+
console.log(`Probing models at ${next.ollamaBaseUrl} …`);
|
|
139
|
+
const models = await listOllamaModels(next.ollamaBaseUrl!);
|
|
140
|
+
if (models.length) {
|
|
141
|
+
console.log("Detected local Ollama models:");
|
|
142
|
+
models.slice(0, 20).forEach((m, i) => console.log(` - ${m}`));
|
|
143
|
+
const def = await rl.question(`Default model (ollama/<name>) [${"ollama/" + (models[0] ?? "llama3.1:8b")}]: `);
|
|
144
|
+
next.defaultModel = def.trim() || `ollama/${models[0] ?? "llama3.1:8b"}`;
|
|
145
|
+
reportModelChoice(chooseDefaultModel(next.defaultModel, "ollama"));
|
|
146
|
+
} else {
|
|
147
|
+
console.log(" (no models detected — Ollama not reachable, defaulting to llama3.1:8b)");
|
|
148
|
+
const picked = chooseDefaultModel(await rl.question(`Default model [${DEFAULT_MODELS.ollama}]: `), "ollama");
|
|
149
|
+
reportModelChoice(picked);
|
|
150
|
+
next.defaultModel = picked.model || DEFAULT_MODELS.ollama;
|
|
151
|
+
}
|
|
152
|
+
} else if (choice === "lmstudio" || choice === "openai-compatible") {
|
|
153
|
+
const dflt = DEFAULT_BASE_URLS[choice]!;
|
|
154
|
+
const url = normalizeBaseUrl(await rl.question(`Base URL [${dflt}]: `), dflt);
|
|
155
|
+
const key = (await rl.question(`API key (optional, blank for none): `)).trim();
|
|
156
|
+
// Reuse the openai slot for compat (loop.ts treats OpenAI URL when openai key is set).
|
|
157
|
+
// To not collide, keep an explicit override field via env-style.
|
|
158
|
+
next.providers.openai = key || next.providers.openai;
|
|
159
|
+
process.env.OPENAI_BASE_URL = url; // session hint
|
|
160
|
+
console.log(`Probing models at ${url} …`);
|
|
161
|
+
const models = await listOpenAiCompatibleModels(url, key);
|
|
162
|
+
if (models.length) {
|
|
163
|
+
console.log("Detected models:");
|
|
164
|
+
models.slice(0, 20).forEach(m => console.log(` - ${m}`));
|
|
165
|
+
const def = await rl.question(`Default model (openai/<name>) [openai/${models[0]}]: `);
|
|
166
|
+
next.defaultModel = def.trim() || `openai/${models[0]}`;
|
|
167
|
+
reportModelChoice(chooseDefaultModel(next.defaultModel, "openai"));
|
|
168
|
+
} else {
|
|
169
|
+
console.log(" (no models detected — endpoint not reachable yet)");
|
|
170
|
+
const picked = chooseDefaultModel(await rl.question(`Default model [${DEFAULT_MODELS[choice]}]: `), "openai");
|
|
171
|
+
reportModelChoice(picked);
|
|
172
|
+
next.defaultModel = picked.model || DEFAULT_MODELS[choice];
|
|
173
|
+
}
|
|
174
|
+
// Persist base URL by writing it to the config via a non-typed field — adopt a small extension.
|
|
175
|
+
(next as Config & { openaiBaseUrl?: string }).openaiBaseUrl = url;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const level = (await rl.question(`Thinking level (low/medium/high) [${current.thinkingLevel || "medium"}]: `)).trim();
|
|
179
|
+
next.thinkingLevel = (level || current.thinkingLevel || "medium") as "low" | "medium" | "high";
|
|
180
|
+
|
|
181
|
+
rl.close();
|
|
182
|
+
|
|
183
|
+
// Drop empty oauth/providers to keep config tidy.
|
|
184
|
+
if (next.oauth && !next.oauth.anthropic && !next.oauth.openai && !next.oauth.gemini) delete next.oauth;
|
|
185
|
+
|
|
186
|
+
await saveGlobalConfig(next);
|
|
187
|
+
console.log("\n[SUCCESS] Configuration saved to ~/.joc/config.json");
|
|
188
|
+
for (const line of buildSetupSummary(next)) console.log(line);
|
|
189
|
+
console.log("");
|
|
190
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { SKILLS, getSkill, formatSkill, skillNames } from "../skills/catalog";
|
|
4
|
+
import { getLocalJocDir } from "../agent/state";
|
|
5
|
+
|
|
6
|
+
export async function runSkillsCommand(args: string[] = []): Promise<void> {
|
|
7
|
+
// `joc skills --write [dir]` materializes bundled skill docs to disk (gjc-style SKILL.md files).
|
|
8
|
+
if (args[0] === "--write") {
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const dir = args[1] ? path.resolve(cwd, args[1]) : path.join(getLocalJocDir(cwd), "skills");
|
|
11
|
+
await fs.mkdir(dir, { recursive: true });
|
|
12
|
+
for (const s of SKILLS) {
|
|
13
|
+
const file = path.join(dir, `${s.name}.md`);
|
|
14
|
+
await fs.writeFile(file, `# ${s.name}\n\n${formatSkill(s)}\n`, "utf-8");
|
|
15
|
+
}
|
|
16
|
+
console.log(`Wrote ${SKILLS.length} skill docs to ${dir}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const name = args[0];
|
|
21
|
+
if (name) {
|
|
22
|
+
const skill = getSkill(name);
|
|
23
|
+
if (!skill) {
|
|
24
|
+
console.log(`Unknown skill: ${name}\nAvailable: ${skillNames().join(", ")}`);
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(formatSkill(skill));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log("\n=== joc skills ===");
|
|
33
|
+
console.log("Bundled workflow skills (run 'joc skills <name>' for details, --write to export):\n");
|
|
34
|
+
for (const s of SKILLS) {
|
|
35
|
+
console.log(` ${s.name.padEnd(16)} ${s.summary}`);
|
|
36
|
+
}
|
|
37
|
+
console.log("");
|
|
38
|
+
}
|