Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/config/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,11 @@ export function isConfigured(): boolean {
process.env.NIM_API_KEY ||
process.env.OPENAI_API_KEY ||
process.env.ANTHROPIC_API_KEY ||
process.env.ARROWCODE_API_KEY;
process.env.ARROWCODE_API_KEY ||
process.env.DEMO_API_KEY;
if (key) return true;

if (process.env.ARROWCODE_PROVIDER === "ollama") return true;
if (process.env.ARROWCODE_PROVIDER === "ollama" || process.env.ARROWCODE_PROVIDER === "demo" || process.env.ARROWCODE_PROVIDER === "mock") return true;

for (const p of ["ORCH", "FE", "BE", "QA"]) {
if (process.env[`ARROW_${p}_API_KEY`]) return true;
Expand All @@ -108,7 +109,7 @@ export function isConfigured(): boolean {
if (existsSync(CONFIG_PATH)) {
try {
const c = parseYaml(readFileSync(CONFIG_PATH, "utf8")) as Partial<ArrowConfig>;
if (c.provider === "ollama" || c.apiKey) return true;
if (c.provider === "ollama" || c.provider === "demo" || c.provider === "mock" || c.apiKey) return true;
} catch {
/* */
}
Expand All @@ -122,6 +123,13 @@ function providerDefaults(provider: ProviderId): {
envKey: string;
} {
switch (provider) {
case "demo":
case "mock":
return {
baseUrl: "demo",
model: "demo-v1",
envKey: "DEMO_API_KEY",
};
case "nim":
return {
baseUrl: DEFAULT_NIM_BASE,
Expand Down Expand Up @@ -314,7 +322,7 @@ export function loadConfig(overrides: Partial<ArrowConfig> = {}): ArrowConfig {
process.env.OPENAI_API_KEY ||
process.env.ANTHROPIC_API_KEY ||
file.apiKey ||
(provider === "ollama" ? "ollama" : "");
(provider === "ollama" ? "ollama" : (provider === "demo" || provider === "mock" ? "demo" : ""));

const baseUrl =
overrides.baseUrl ||
Expand Down
2 changes: 1 addition & 1 deletion src/config/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Shared types for ArrowCode — swarm multi-agent coding harness. */

export type ProviderId = "nim" | "openai" | "anthropic" | "ollama" | "groq" | "deepseek" | "gemini" | "openrouter" | "custom";
export type ProviderId = "nim" | "openai" | "anthropic" | "ollama" | "groq" | "deepseek" | "gemini" | "openrouter" | "custom" | "demo" | "mock";

export type AgentId = "orchestrator" | "frontend" | "backend" | "tester";

Expand Down
46 changes: 39 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,46 @@ async function main() {
if (!isConfigured()) {
printBanner({ compact: true });
console.log("ArrowCode is not configured yet.\n");
console.log(
"This will create ~/.arrowcode from packaged defaults/ and ask for an API key.\n",
);
console.log("Welcome to ArrowCode! Since you don't have an API key configured yet,");
console.log("you can choose one of the following options to get started immediately:");
console.log(" 1) Use Mock / Demo mode (free, runs locally, no keys needed)");
console.log(" 2) Configure a real LLM provider (NVIDIA NIM, OpenAI, Anthropic, etc.)");
console.log("");
const readline = await import("node:readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const choice = await new Promise<string>((res) => {
rl.question("Choose [1-2] (default 1): ", (ans) => res((ans || "").trim()));
});
rl.close();

bootstrapUserHome();
const ok = await runSetupWizard();
if (!ok) process.exit(1);
if (!isConfigured()) {
process.exit(0);

if (choice === "2") {
const ok = await runSetupWizard();
if (!ok) process.exit(1);
if (!isConfigured()) {
process.exit(0);
}
} else {
// Setup demo/mock mode
const { saveEnv, saveConfig } = await import("./config/load");
saveEnv({
ARROWCODE_PROVIDER: "demo",
ARROWCODE_MODEL: "demo-v1",
ARROWCODE_BASE_URL: "demo",
DEMO_API_KEY: "demo",
});
saveConfig({
provider: "demo",
model: "demo-v1",
baseUrl: "demo",
apiKey: "demo",
});
console.log("\nDemo Mode configured! Starting ArrowCode...\n");
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}

Expand Down
56 changes: 54 additions & 2 deletions src/llm/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ export class LLMClient {
this.endpoint = endpoint;
this.model = endpoint.model;
this.provider = endpoint.provider;
if (!endpoint.apiKey && endpoint.provider !== "ollama") {
if (!endpoint.apiKey && endpoint.provider !== "ollama" && endpoint.provider !== "demo" && endpoint.provider !== "mock") {
throw new LLMError(
"No API key for this agent. Set global key or ARROW_<AGENT>_API_KEY / agents.yaml",
);
}
this.client = new OpenAI({
apiKey: endpoint.apiKey || "ollama",
baseURL: endpoint.baseUrl,
baseURL: endpoint.provider === "demo" || endpoint.provider === "mock" ? "http://demo-url" : endpoint.baseUrl,
timeout: 120_000,
maxRetries: 0,
});
Expand All @@ -54,6 +54,58 @@ export class LLMClient {
tools?: OpenAITool[],
opts?: { onToken?: (t: string) => void; stream?: boolean },
): Promise<LLMResponse> {
if (this.provider === "demo" || this.provider === "mock") {
// Mock LLM response generator for instant works/tryout!
const userMessage = messages[messages.length - 1]?.content || "";
let replyContent = "Let's explore the workspace and carry out your request.";
const toolCalls: ToolCall[] = [];

// Determine the kind of reply based on the message content
const lower = typeof userMessage === "string" ? userMessage.toLowerCase() : "";
if (lower.includes("[mode=plan]") || lower.includes("plan")) {
replyContent = `\`\`\`arrow-plan
title: Demo Implementation Plan
summary: A mockup flow to demonstrate clean and fast execution.
steps:
1. Explore local directories and files.
2. Setup frontend core elements.
3. Validate using offline tests.
risks:
- Mock mode doesn't execute actual remote files.
acceptance:
- UI works fine.
- Tests pass with green indicators.
agents:
frontend: Core UI setup
backend: API endpoints mocking
tester: Verify everything
\`\`\``;
} else if (lower.includes("[execute]") || lower.includes("execute") || lower.includes("confirm")) {
replyContent = `\`\`\`arrow-ready
Verification: Verified that all mock procedures ran fine and the application is highly optimized.
All tests are passing.
\`\`\``;
} else if (lower.includes("health") || lower.includes("status")) {
replyContent = "Everything is functioning correctly under the demo environment.";
}

if (opts?.onToken) {
// simulate streaming
const parts = replyContent.split(" ");
for (const p of parts) {
opts.onToken(p + " ");
await Bun.sleep(5);
}
}

return {
content: replyContent,
toolCalls,
finishReason: "stop",
usage: { prompt_tokens: messages.length * 10, completion_tokens: replyContent.length / 4 }
};
}

const stream = Boolean(opts?.stream && opts.onToken);
const body: OpenAI.Chat.ChatCompletionCreateParams = {
model: this.model,
Expand Down
Loading
Loading