@ridit/lens 0.3.7 → 0.3.9

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.
Files changed (96) hide show
  1. package/dist/index.mjs +105368 -274002
  2. package/package.json +13 -19
  3. package/src/colors.ts +15 -15
  4. package/src/commands/chat.tsx +32 -23
  5. package/src/commands/provider.tsx +11 -238
  6. package/src/commands/repo.tsx +66 -120
  7. package/src/commands/timeline.tsx +11 -22
  8. package/src/components/ChatView.tsx +238 -0
  9. package/src/components/Message.tsx +46 -0
  10. package/src/components/ToolCall.tsx +67 -0
  11. package/src/components/chat/ChatView.tsx +550 -0
  12. package/src/components/chat/Message.tsx +152 -0
  13. package/src/components/chat/StatusBar.tsx +214 -0
  14. package/src/components/chat/TextArea.tsx +173 -176
  15. package/src/components/provider/ApiKeyStep.tsx +207 -199
  16. package/src/components/provider/ModelStep.tsx +90 -88
  17. package/src/components/provider/ProviderSetup.tsx +331 -0
  18. package/src/components/provider/ProviderTypeStep.tsx +53 -61
  19. package/src/components/repo/StepRow.tsx +68 -69
  20. package/src/components/timeline/TimelineView.tsx +840 -0
  21. package/src/components/toolcall-utils.ts +103 -0
  22. package/src/components/watch/RunView.tsx +497 -0
  23. package/src/hooks/useChatInput.ts +49 -0
  24. package/src/hooks/useCommandHandler.ts +117 -0
  25. package/src/index.tsx +386 -139
  26. package/src/utils/git.ts +149 -155
  27. package/src/utils/repo.ts +62 -69
  28. package/src/utils/thinking.tsx +64 -0
  29. package/src/utils/watch.ts +165 -307
  30. package/tests/message.test.ts +38 -0
  31. package/tests/toolcall-utils.test.ts +111 -0
  32. package/tsconfig.json +8 -24
  33. package/CLAUDE.md +0 -50
  34. package/LENS.md +0 -48
  35. package/LICENSE +0 -21
  36. package/README.md +0 -93
  37. package/addons/README.md +0 -55
  38. package/addons/clean-cache.js +0 -48
  39. package/addons/generate-readme.js +0 -67
  40. package/addons/git-stats.js +0 -29
  41. package/addons/run-tests.js +0 -127
  42. package/src/commands/commit.tsx +0 -668
  43. package/src/commands/review.tsx +0 -294
  44. package/src/commands/run.tsx +0 -56
  45. package/src/commands/task.tsx +0 -36
  46. package/src/components/chat/ChatMessage.tsx +0 -195
  47. package/src/components/chat/ChatOverlays.tsx +0 -399
  48. package/src/components/chat/ChatRunner.tsx +0 -517
  49. package/src/components/chat/hooks/useChat.ts +0 -631
  50. package/src/components/chat/hooks/useChatInput.ts +0 -79
  51. package/src/components/chat/hooks/useCommandHandlers.ts +0 -327
  52. package/src/components/provider/ProviderPicker.tsx +0 -76
  53. package/src/components/provider/RemoveProviderStep.tsx +0 -82
  54. package/src/components/repo/DiffViewer.tsx +0 -175
  55. package/src/components/repo/FileReviewer.tsx +0 -70
  56. package/src/components/repo/FileViewer.tsx +0 -60
  57. package/src/components/repo/IssueFixer.tsx +0 -666
  58. package/src/components/repo/LensFileMenu.tsx +0 -115
  59. package/src/components/repo/NoProviderPrompt.tsx +0 -28
  60. package/src/components/repo/PreviewRunner.tsx +0 -217
  61. package/src/components/repo/RepoAnalysis.tsx +0 -534
  62. package/src/components/task/TaskRunner.tsx +0 -396
  63. package/src/components/timeline/CommitDetail.tsx +0 -272
  64. package/src/components/timeline/CommitList.tsx +0 -162
  65. package/src/components/timeline/TimelineChat.tsx +0 -166
  66. package/src/components/timeline/TimelineRunner.tsx +0 -1285
  67. package/src/components/watch/RunRunner.tsx +0 -929
  68. package/src/prompts/fewshot.ts +0 -252
  69. package/src/prompts/index.ts +0 -2
  70. package/src/prompts/system.ts +0 -285
  71. package/src/tools/chart.ts +0 -202
  72. package/src/tools/convert-image.ts +0 -312
  73. package/src/tools/files.ts +0 -253
  74. package/src/tools/git.ts +0 -603
  75. package/src/tools/index.ts +0 -17
  76. package/src/tools/pdf.ts +0 -164
  77. package/src/tools/shell.ts +0 -96
  78. package/src/tools/view-image.ts +0 -335
  79. package/src/tools/web.ts +0 -212
  80. package/src/types/chat.ts +0 -86
  81. package/src/types/config.ts +0 -20
  82. package/src/types/repo.ts +0 -54
  83. package/src/utils/addons/loadAddons.ts +0 -34
  84. package/src/utils/ai.ts +0 -321
  85. package/src/utils/chat.ts +0 -326
  86. package/src/utils/chatHistory.ts +0 -121
  87. package/src/utils/config.ts +0 -61
  88. package/src/utils/files.ts +0 -105
  89. package/src/utils/intentClassifier.ts +0 -58
  90. package/src/utils/lensfile.ts +0 -142
  91. package/src/utils/llm.ts +0 -81
  92. package/src/utils/memory.ts +0 -209
  93. package/src/utils/preview.ts +0 -119
  94. package/src/utils/stats.ts +0 -174
  95. package/src/utils/tools/builtins.ts +0 -377
  96. package/src/utils/tools/registry.ts +0 -105
@@ -0,0 +1,117 @@
1
+ import { createSession } from "@ridit/lens-core";
2
+ import type { UIMessage } from "../components/chat/Message";
3
+
4
+ interface CommandContext {
5
+ repoPath: string;
6
+ autoApprove: boolean;
7
+ forceApprove: boolean;
8
+ setAutoApprove: (v: boolean) => void;
9
+ setForceApprove: (v: boolean) => void;
10
+ pushMsg: (msg: UIMessage) => void;
11
+ resetSession: () => void;
12
+ openProvider?: () => void;
13
+ }
14
+
15
+ export function handleCommand(text: string, ctx: CommandContext): boolean {
16
+ const t = text.trim().toLowerCase();
17
+
18
+ if (t === "/auto --force-all") {
19
+ if (ctx.forceApprove) {
20
+ ctx.setForceApprove(false);
21
+ ctx.setAutoApprove(false);
22
+ ctx.pushMsg({
23
+ role: "assistant",
24
+ type: "text",
25
+ content: "Force-all mode OFF — tools will ask for permission again.",
26
+ });
27
+ } else {
28
+ ctx.setForceApprove(true);
29
+ ctx.setAutoApprove(true);
30
+ ctx.pushMsg({
31
+ role: "assistant",
32
+ type: "text",
33
+ content:
34
+ "⚡⚡ Force-all mode ON (dangerous) — ALL tools auto-approved including shell and writes. Type /auto --force-all again to disable.",
35
+ });
36
+ }
37
+ return true;
38
+ }
39
+
40
+ if (t === "/auto") {
41
+ if (ctx.forceApprove) {
42
+ ctx.setForceApprove(false);
43
+ ctx.setAutoApprove(true);
44
+ ctx.pushMsg({
45
+ role: "assistant",
46
+ type: "text",
47
+ content:
48
+ "Force-all mode OFF — switched to normal auto-approve (safe tools only).",
49
+ });
50
+ return true;
51
+ }
52
+ const next = !ctx.autoApprove;
53
+ ctx.setAutoApprove(next);
54
+ ctx.pushMsg({
55
+ role: "assistant",
56
+ type: "text",
57
+ content: next
58
+ ? "Auto-approve ON — safe tools (read, search, grep) will run without asking."
59
+ : "Auto-approve OFF — all tools will ask for permission.",
60
+ });
61
+ return true;
62
+ }
63
+
64
+ if (t === "/clear history") {
65
+ ctx.resetSession();
66
+ ctx.pushMsg({
67
+ role: "assistant",
68
+ type: "text",
69
+ content: "History cleared for this repo.",
70
+ });
71
+ return true;
72
+ }
73
+
74
+ if (t === "/memory" || t === "/memory list") {
75
+ ctx.pushMsg({
76
+ role: "assistant",
77
+ type: "text",
78
+ content:
79
+ "Memory is managed automatically. Use `/memory add <text>` to save context.",
80
+ });
81
+ return true;
82
+ }
83
+
84
+ if (t.startsWith("/memory add")) {
85
+ const content = text.trim().slice("/memory add".length).trim();
86
+ if (!content) {
87
+ ctx.pushMsg({
88
+ role: "assistant",
89
+ type: "text",
90
+ content: "Usage: `/memory add <content>`",
91
+ });
92
+ return true;
93
+ }
94
+ ctx.pushMsg({
95
+ role: "assistant",
96
+ type: "text",
97
+ content: `Memory saved: ${content}`,
98
+ });
99
+ return true;
100
+ }
101
+
102
+ if (t === "/memory clear") {
103
+ ctx.pushMsg({
104
+ role: "assistant",
105
+ type: "text",
106
+ content: "Memories cleared.",
107
+ });
108
+ return true;
109
+ }
110
+
111
+ if (t === "/provider") {
112
+ ctx.openProvider?.();
113
+ return true;
114
+ }
115
+
116
+ return false;
117
+ }
package/src/index.tsx CHANGED
@@ -1,139 +1,386 @@
1
- import React from "react";
2
- import "./utils/tools/registry";
3
- import { render } from "ink";
4
- import { Command } from "commander";
5
- import { RepoCommand } from "./commands/repo";
6
- import { InitCommand } from "./commands/provider";
7
- import { ReviewCommand } from "./commands/review";
8
- import { TaskCommand } from "./commands/task";
9
- import { ChatCommand } from "./commands/chat";
10
- import { RunCommand } from "./commands/run";
11
- import { TimelineCommand } from "./commands/timeline";
12
- import { CommitCommand } from "./commands/commit";
13
- import { registerBuiltins } from "./utils/tools/builtins";
14
- import { loadAddons } from "./utils/addons/loadAddons";
15
-
16
- registerBuiltins();
17
- await loadAddons();
18
-
19
- const program = new Command();
20
-
21
- program
22
- .command("repo <url>")
23
- .description("Analyze a remote repository")
24
- .action((url) => {
25
- render(<RepoCommand url={url} />);
26
- });
27
-
28
- program
29
- .command("provider")
30
- .description("Configure AI providers")
31
- .action(() => {
32
- render(<InitCommand />);
33
- });
34
-
35
- program
36
- .command("review [path]")
37
- .description("Review a local codebase")
38
- .action((inputPath) => {
39
- render(<ReviewCommand path={inputPath ?? "."} />);
40
- });
41
-
42
- program
43
- .command("task <text>")
44
- .description("Apply a natural language change to the codebase")
45
- .option("-p, --path <path>", "Path to the repo", ".")
46
- .action((text: string, opts: { path: string }) => {
47
- render(<TaskCommand prompt={text} path={opts.path} />);
48
- });
49
-
50
- program
51
- .command("chat")
52
- .description("Chat with your codebase — ask questions or make changes")
53
- .option("-p, --path <path>", "Path to the repo", ".")
54
- .action((opts: { path: string }) => {
55
- render(<ChatCommand path={opts.path} />);
56
- });
57
-
58
- program
59
- .command("timeline")
60
- .description(
61
- "Explore your code history see commits, changes, and evolution",
62
- )
63
- .option("-p, --path <path>", "Path to the repo", ".")
64
- .action((opts: { path: string }) => {
65
- render(<TimelineCommand path={opts.path} />);
66
- });
67
-
68
- program
69
- .command("commit [files...]")
70
- .description(
71
- "Generate a smart conventional commit message from staged changes or specific files",
72
- )
73
- .option("-p, --path <path>", "Path to the repo", ".")
74
- .option(
75
- "--auto",
76
- "Stage all changes (or the given files) and commit without confirmation",
77
- )
78
- .option("--confirm", "Show preview before committing even when using --auto")
79
- .option("--preview", "Show the generated message without committing")
80
- .option("--push", "Push to remote after committing")
81
- .action(
82
- (
83
- files: string[],
84
- opts: {
85
- path: string;
86
- auto: boolean;
87
- confirm: boolean;
88
- preview: boolean;
89
- push: boolean;
90
- },
91
- ) => {
92
- render(
93
- <CommitCommand
94
- path={opts.path}
95
- files={files ?? []}
96
- auto={opts.auto ?? false}
97
- confirm={opts.confirm ?? false}
98
- preview={opts.preview ?? false}
99
- push={opts.push ?? false}
100
- />,
101
- );
102
- },
103
- );
104
-
105
- program
106
- .command("run <cmd>")
107
- .description(
108
- "Run your dev server. Lens detects and fixes errors automatically",
109
- )
110
- .option("-p, --path <path>", "Path to the repo", ".")
111
- .option("--clean", "Only show AI suggestions, hide raw logs")
112
- .option("--fix-all", "Auto-apply fixes as errors are detected")
113
- .option("--auto-restart", "Automatically re-run the command after a crash")
114
- .option("--prompt <text>", "Extra context for the AI about your project")
115
- .action(
116
- (
117
- cmd: string,
118
- opts: {
119
- path: string;
120
- clean: boolean;
121
- fixAll: boolean;
122
- autoRestart: boolean;
123
- prompt?: string;
124
- },
125
- ) => {
126
- render(
127
- <RunCommand
128
- cmd={cmd}
129
- path={opts.path}
130
- clean={opts.clean ?? false}
131
- fixAll={opts.fixAll ?? false}
132
- autoRestart={opts.autoRestart ?? false}
133
- prompt={opts.prompt}
134
- />,
135
- );
136
- },
137
- );
138
-
139
- program.parse(process.argv);
1
+ import React from "react";
2
+ import { render } from "ink";
3
+ import { Command } from "commander";
4
+ import { ChatCommand } from "./commands/chat";
5
+ import { TimelineCommand } from "./commands/timeline";
6
+ import { RepoCommand } from "./commands/repo";
7
+ import { ProviderCommand } from "./commands/provider";
8
+ import {
9
+ chat,
10
+ createSession,
11
+ createSessionWithId,
12
+ addMessage,
13
+ appendMessages,
14
+ getMessages,
15
+ getSystemPrompt,
16
+ saveSession,
17
+ loadSession,
18
+ getLatestSession,
19
+ addProvider,
20
+ setActiveProvider,
21
+ removeProvider,
22
+ getConfiguredProviders,
23
+ getActiveModelName,
24
+ type Provider,
25
+ } from "@ridit/lens-core";
26
+
27
+ // ── Headless chat (--dev or --single + --prompt, no Ink UI) ──────────────────
28
+
29
+ // Safe (read-only) tools that never need approval
30
+ const HEADLESS_SAFE_TOOLS = new Set(["read", "grep", "ls", "remember", "search", "scrape"]);
31
+
32
+ // Words that mean "approve the last denied operation"
33
+ const APPROVAL_WORDS = new Set(["execute", "yes", "proceed", "do it", "confirm", "allow", "ok", "approve"]);
34
+
35
+ // Scan session messages for the last denied tool call (tool result containing "Permission denied")
36
+ function getLastDeniedAction(messages: ReturnType<typeof getMessages>): { tool: string; description: string } | null {
37
+ for (let i = messages.length - 1; i >= 0; i--) {
38
+ const msg = messages[i];
39
+ if (!msg || msg.role !== "tool") continue;
40
+ const content = Array.isArray(msg.content) ? msg.content : [];
41
+ for (const part of content) {
42
+ if (
43
+ typeof part === "object" && part !== null &&
44
+ "type" in part && (part as { type: string }).type === "tool-result"
45
+ ) {
46
+ const r = part as unknown as { type: string; toolCallId: string; result: unknown };
47
+ const result = r.result;
48
+ const text = typeof result === "string" ? result : (result !== undefined ? JSON.stringify(result) : "");
49
+ if (text.includes("Permission denied")) {
50
+ // find the matching tool call in the assistant message before this
51
+ for (let j = i - 1; j >= 0; j--) {
52
+ const prev = messages[j];
53
+ if (!prev || prev.role !== "assistant") continue;
54
+ const calls = Array.isArray(prev.content) ? prev.content : [];
55
+ for (const c of calls) {
56
+ if (typeof c === "object" && c !== null && "type" in c && (c as { type: string }).type === "tool-call") {
57
+ const call = c as { type: string; toolName: string; args: Record<string, unknown> };
58
+ const desc =
59
+ call.toolName === "bash" ? String(call.args.command ?? call.args.cmd ?? "") :
60
+ String(call.args.path ?? call.args.file_path ?? "");
61
+ return { tool: call.toolName, description: desc || call.toolName };
62
+ }
63
+ }
64
+ break;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ async function runHeadless(opts: {
74
+ path: string;
75
+ prompt: string;
76
+ sessionId?: string;
77
+ single?: boolean;
78
+ forceAll?: boolean;
79
+ }) {
80
+ const repoPath = opts.path;
81
+
82
+ let session = opts.sessionId
83
+ ? (loadSession(opts.sessionId) ?? createSessionWithId(opts.sessionId, repoPath))
84
+ : opts.single
85
+ ? (getLatestSession(repoPath) ?? createSession(repoPath))
86
+ : createSession(repoPath);
87
+
88
+ // if user is approving a prior denial, make the intent unambiguous
89
+ let prompt = opts.prompt;
90
+ if (opts.forceAll && APPROVAL_WORDS.has(prompt.trim().toLowerCase())) {
91
+ const pending = getLastDeniedAction(getMessages(session));
92
+ if (pending) {
93
+ prompt = `Proceed with the previously denied operation: use the ${pending.tool} tool on "${pending.description}".`;
94
+ }
95
+ }
96
+
97
+ session = addMessage(session, "user", prompt);
98
+ // save now so context is available on follow-up messages even if we exit early
99
+ if (!opts.single) saveSession(session);
100
+
101
+ const toolLog: { tool: string; args: unknown; result: unknown }[] = [];
102
+ const denied: { tool: string; description: string }[] = [];
103
+
104
+ await chat({
105
+ messages: getMessages(session),
106
+ system: getSystemPrompt(repoPath),
107
+ // 2 steps: 1 tool attempt (or denial) + 1 text response
108
+ maxSteps: opts.forceAll ? 50 : 2,
109
+ onBeforeToolCall: (tool, args) => {
110
+ if (opts.forceAll || HEADLESS_SAFE_TOOLS.has(tool)) return Promise.resolve(true);
111
+ // record denial model will respond naturally explaining what it needs
112
+ const a = args as Record<string, unknown>;
113
+ const description =
114
+ tool === "bash" ? String(a.command ?? a.cmd ?? "") :
115
+ tool === "write" ? String(a.path ?? a.file_path ?? "") :
116
+ String(a.path ?? a.file_path ?? "");
117
+ denied.push({ tool, description: description || tool });
118
+ return Promise.resolve(false);
119
+ },
120
+ onChunk: () => {},
121
+ onToolCall: (tool, args) => toolLog.push({ tool, args, result: null }),
122
+ onToolResult: (tool, result) => {
123
+ const entry = [...toolLog].reverse().find((t) => t.tool === tool && t.result === null);
124
+ if (entry) entry.result = result;
125
+ },
126
+ onFinish: (message, responseMessages, model) => {
127
+ session = appendMessages(session, responseMessages);
128
+ if (!opts.single) saveSession(session);
129
+
130
+ const output: Record<string, unknown> = {
131
+ message,
132
+ model,
133
+ sessionId: session.id,
134
+ tools: toolLog,
135
+ };
136
+ if (denied.length > 0) output.permissionRequired = denied;
137
+
138
+ process.stdout.write(JSON.stringify(output) + "\n");
139
+ process.exit(denied.length > 0 ? 2 : 0);
140
+ },
141
+ });
142
+ }
143
+
144
+ // ── Commander setup ───────────────────────────────────────────────────────────
145
+
146
+ // enablePositionalOptions ensures options after a subcommand name are parsed by
147
+ // the subcommand, not the root — prevents root --dev from shadowing sub --dev.
148
+ const program = new Command().enablePositionalOptions();
149
+
150
+ // ── chat ──────────────────────────────────────────────────────────────────────
151
+
152
+ program
153
+ .command("chat")
154
+ .description("Chat with your codebase — ask questions or make changes")
155
+ .option("-p, --path <path>", "Path to the repo", ".")
156
+ .option("-d, --dev", "Output structured JSON (no UI)")
157
+ .option("--single", "Single-shot: run one message then exit")
158
+ .option("--session <id>", "Resume session by ID, or create one with that ID")
159
+ .option("--id <id>", "Alias for --session")
160
+ .option("--force-all", "Auto-approve all tools")
161
+ .option("--prompt <text>", "Run a prompt non-interactively")
162
+ .action(
163
+ (opts: {
164
+ path: string;
165
+ dev?: boolean;
166
+ single?: boolean;
167
+ session?: string;
168
+ id?: string;
169
+ forceAll?: boolean;
170
+ prompt?: string;
171
+ }) => {
172
+ const sessionId = opts.session ?? opts.id;
173
+ // headless: dev+prompt or single+prompt → no UI, output JSON and exit
174
+ if (opts.prompt && (opts.dev || opts.single)) {
175
+ runHeadless({ path: opts.path, prompt: opts.prompt, sessionId, single: opts.single, forceAll: opts.forceAll });
176
+ return;
177
+ }
178
+ render(
179
+ <ChatCommand
180
+ path={opts.path}
181
+ autoForce={opts.forceAll ?? false}
182
+ dev={opts.dev ?? false}
183
+ single={opts.single ?? false}
184
+ sessionId={sessionId}
185
+ initialMessage={opts.prompt}
186
+ />,
187
+ );
188
+ },
189
+ );
190
+
191
+ // ── commit ────────────────────────────────────────────────────────────────────
192
+
193
+ program
194
+ .command("commit [files...]")
195
+ .description("Generate a smart conventional commit message from staged changes")
196
+ .option("-p, --path <path>", "Path to the repo", ".")
197
+ .option("--auto", "Stage all changes and commit without confirmation")
198
+ .option("--push", "Push to remote after committing")
199
+ .action(
200
+ (files: string[], opts: { path: string; auto: boolean; push: boolean }) => {
201
+ const fileList =
202
+ (files ?? []).length > 0 ? ` for files: ${files.join(", ")}` : "";
203
+ const extra = opts.auto ? " Commit automatically without confirmation." : "";
204
+ const push = opts.push ? " Then push to remote." : "";
205
+ render(
206
+ <ChatCommand
207
+ path={opts.path}
208
+ autoForce={opts.auto ?? false}
209
+ initialMessage={`Generate a smart conventional commit message from the staged changes${fileList}.${extra}${push}`}
210
+ />,
211
+ );
212
+ },
213
+ );
214
+
215
+ // ── review ────────────────────────────────────────────────────────────────────
216
+
217
+ program
218
+ .command("review [path]")
219
+ .description("Review a local codebase")
220
+ .action((inputPath: string) => {
221
+ render(
222
+ <ChatCommand
223
+ path={inputPath ?? "."}
224
+ initialMessage="Review this codebase thoroughly. Identify strengths, weaknesses, potential bugs, and improvement opportunities."
225
+ />,
226
+ );
227
+ });
228
+
229
+ // ── task ──────────────────────────────────────────────────────────────────────
230
+
231
+ program
232
+ .command("task <text>")
233
+ .description("Apply a natural language change to the codebase")
234
+ .option("-p, --path <path>", "Path to the repo", ".")
235
+ .action((text: string, opts: { path: string }) => {
236
+ render(<ChatCommand path={opts.path} autoForce initialMessage={text} />);
237
+ });
238
+
239
+ // ── repo ──────────────────────────────────────────────────────────────────────
240
+
241
+ program
242
+ .command("repo <url>")
243
+ .description("Analyze a remote repository")
244
+ .action((url: string) => {
245
+ render(<RepoCommand url={url} />);
246
+ });
247
+
248
+ // ── timeline ──────────────────────────────────────────────────────────────────
249
+
250
+ program
251
+ .command("timeline")
252
+ .description("Explore your code history — see commits, changes, and evolution")
253
+ .option("-p, --path <path>", "Path to the repo", ".")
254
+ .action((opts: { path: string }) => {
255
+ render(<TimelineCommand path={opts.path} />);
256
+ });
257
+
258
+ // ── provider ──────────────────────────────────────────────────────────────────
259
+
260
+ program
261
+ .command("provider")
262
+ .description("Configure an AI provider")
263
+ .option("--provider <name>", "Provider to add/update (anthropic, openai, google, groq, openrouter, ollama, custom)")
264
+ .option("--api-key <key>", "API key")
265
+ .option("--base-url <url>", "Base URL (ollama/custom)")
266
+ .option("--model <model>", "Model to use")
267
+ .option("--remove <name>", "Remove a configured provider")
268
+ .option("--switch <name>", "Switch the active provider")
269
+ .option("--list", "List all configured providers")
270
+ .option("-d, --dev", "Output result as JSON")
271
+ .action((opts: {
272
+ provider?: string;
273
+ apiKey?: string;
274
+ baseUrl?: string;
275
+ model?: string;
276
+ remove?: string;
277
+ switch?: string;
278
+ list?: boolean;
279
+ dev?: boolean;
280
+ }) => {
281
+ const out = (data: object) => {
282
+ if (opts.dev) {
283
+ process.stdout.write(JSON.stringify(data) + "\n");
284
+ } else {
285
+ Object.entries(data).forEach(([k, v]) => v !== undefined && console.log(`✓ ${k}: ${v}`));
286
+ }
287
+ };
288
+
289
+ if (opts.list) {
290
+ const configured = getConfiguredProviders();
291
+ if (opts.dev) {
292
+ process.stdout.write(JSON.stringify({ providers: configured }) + "\n");
293
+ } else if (configured.length === 0) {
294
+ console.log("No providers configured.");
295
+ } else {
296
+ configured.forEach((p) => console.log(` ${p}`));
297
+ }
298
+ process.exit(0);
299
+ }
300
+
301
+ if (opts.remove) {
302
+ removeProvider(opts.remove as Provider);
303
+ out({ removed: opts.remove });
304
+ process.exit(0);
305
+ }
306
+
307
+ if (opts.switch) {
308
+ setActiveProvider(opts.switch as Provider);
309
+ out({ active: opts.switch });
310
+ process.exit(0);
311
+ }
312
+
313
+ if (opts.provider && opts.model) {
314
+ addProvider(opts.provider as Provider, {
315
+ apiKey: opts.apiKey ?? "ollama",
316
+ model: opts.model,
317
+ baseURL: opts.baseUrl,
318
+ });
319
+ setActiveProvider(opts.provider as Provider);
320
+ out({ provider: opts.provider, model: opts.model, baseUrl: opts.baseUrl });
321
+ process.exit(0);
322
+ }
323
+
324
+ render(<ProviderCommand />);
325
+ });
326
+
327
+ // ── run ───────────────────────────────────────────────────────────────────────
328
+
329
+ program
330
+ .command("run <cmd>")
331
+ .description("Run your dev server. Lens watches and helps fix errors")
332
+ .option("-p, --path <path>", "Path to the repo", ".")
333
+ .option("--fix-all", "Auto-apply fixes as errors are detected")
334
+ .action((cmd: string, opts: { path: string; fixAll: boolean }) => {
335
+ render(
336
+ <ChatCommand
337
+ path={opts.path}
338
+ autoForce={opts.fixAll ?? false}
339
+ initialMessage={`Run this command and help me fix any errors that appear: \`${cmd}\``}
340
+ />,
341
+ );
342
+ });
343
+
344
+ // ── Default: no subcommand → parse flags with a fresh Command, open chat ──────
345
+
346
+ const firstArg = process.argv[2];
347
+ if (!firstArg || firstArg.startsWith("-")) {
348
+ // Use a separate Command so root flags don't interfere with subcommands above
349
+ const defaultFlags = new Command()
350
+ .option("-p, --path <path>", "Path to the repo", ".")
351
+ .option("--session <id>", "Resume session by ID")
352
+ .option("--single", "Single-shot mode")
353
+ .option("--prompt <text>", "Run a prompt")
354
+ .option("-d, --dev", "Output JSON (no UI)")
355
+ .option("--force-all", "Auto-approve all tools")
356
+ .allowUnknownOption()
357
+ .exitOverride();
358
+
359
+ try { defaultFlags.parse(process.argv); } catch { /* ignore unknown options */ }
360
+
361
+ const opts = defaultFlags.opts<{
362
+ path: string;
363
+ session?: string;
364
+ single?: boolean;
365
+ prompt?: string;
366
+ dev?: boolean;
367
+ forceAll?: boolean;
368
+ }>();
369
+
370
+ if (opts.prompt && (opts.dev || opts.single)) {
371
+ runHeadless({ path: opts.path ?? ".", prompt: opts.prompt, sessionId: opts.session, single: opts.single, forceAll: opts.forceAll });
372
+ } else {
373
+ render(
374
+ <ChatCommand
375
+ path={opts.path ?? "."}
376
+ autoForce={opts.forceAll ?? false}
377
+ dev={opts.dev ?? false}
378
+ single={opts.single ?? false}
379
+ sessionId={opts.session}
380
+ initialMessage={opts.prompt}
381
+ />,
382
+ );
383
+ }
384
+ } else {
385
+ program.parse(process.argv);
386
+ }