@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.
- package/dist/index.mjs +105368 -274002
- package/package.json +13 -19
- package/src/colors.ts +15 -15
- package/src/commands/chat.tsx +32 -23
- package/src/commands/provider.tsx +11 -238
- package/src/commands/repo.tsx +66 -120
- package/src/commands/timeline.tsx +11 -22
- package/src/components/ChatView.tsx +238 -0
- package/src/components/Message.tsx +46 -0
- package/src/components/ToolCall.tsx +67 -0
- package/src/components/chat/ChatView.tsx +550 -0
- package/src/components/chat/Message.tsx +152 -0
- package/src/components/chat/StatusBar.tsx +214 -0
- package/src/components/chat/TextArea.tsx +173 -176
- package/src/components/provider/ApiKeyStep.tsx +207 -199
- package/src/components/provider/ModelStep.tsx +90 -88
- package/src/components/provider/ProviderSetup.tsx +331 -0
- package/src/components/provider/ProviderTypeStep.tsx +53 -61
- package/src/components/repo/StepRow.tsx +68 -69
- package/src/components/timeline/TimelineView.tsx +840 -0
- package/src/components/toolcall-utils.ts +103 -0
- package/src/components/watch/RunView.tsx +497 -0
- package/src/hooks/useChatInput.ts +49 -0
- package/src/hooks/useCommandHandler.ts +117 -0
- package/src/index.tsx +386 -139
- package/src/utils/git.ts +149 -155
- package/src/utils/repo.ts +62 -69
- package/src/utils/thinking.tsx +64 -0
- package/src/utils/watch.ts +165 -307
- package/tests/message.test.ts +38 -0
- package/tests/toolcall-utils.test.ts +111 -0
- package/tsconfig.json +8 -24
- package/CLAUDE.md +0 -50
- package/LENS.md +0 -48
- package/LICENSE +0 -21
- package/README.md +0 -93
- package/addons/README.md +0 -55
- package/addons/clean-cache.js +0 -48
- package/addons/generate-readme.js +0 -67
- package/addons/git-stats.js +0 -29
- package/addons/run-tests.js +0 -127
- package/src/commands/commit.tsx +0 -668
- package/src/commands/review.tsx +0 -294
- package/src/commands/run.tsx +0 -56
- package/src/commands/task.tsx +0 -36
- package/src/components/chat/ChatMessage.tsx +0 -195
- package/src/components/chat/ChatOverlays.tsx +0 -399
- package/src/components/chat/ChatRunner.tsx +0 -517
- package/src/components/chat/hooks/useChat.ts +0 -631
- package/src/components/chat/hooks/useChatInput.ts +0 -79
- package/src/components/chat/hooks/useCommandHandlers.ts +0 -327
- package/src/components/provider/ProviderPicker.tsx +0 -76
- package/src/components/provider/RemoveProviderStep.tsx +0 -82
- package/src/components/repo/DiffViewer.tsx +0 -175
- package/src/components/repo/FileReviewer.tsx +0 -70
- package/src/components/repo/FileViewer.tsx +0 -60
- package/src/components/repo/IssueFixer.tsx +0 -666
- package/src/components/repo/LensFileMenu.tsx +0 -115
- package/src/components/repo/NoProviderPrompt.tsx +0 -28
- package/src/components/repo/PreviewRunner.tsx +0 -217
- package/src/components/repo/RepoAnalysis.tsx +0 -534
- package/src/components/task/TaskRunner.tsx +0 -396
- package/src/components/timeline/CommitDetail.tsx +0 -272
- package/src/components/timeline/CommitList.tsx +0 -162
- package/src/components/timeline/TimelineChat.tsx +0 -166
- package/src/components/timeline/TimelineRunner.tsx +0 -1285
- package/src/components/watch/RunRunner.tsx +0 -929
- package/src/prompts/fewshot.ts +0 -252
- package/src/prompts/index.ts +0 -2
- package/src/prompts/system.ts +0 -285
- package/src/tools/chart.ts +0 -202
- package/src/tools/convert-image.ts +0 -312
- package/src/tools/files.ts +0 -253
- package/src/tools/git.ts +0 -603
- package/src/tools/index.ts +0 -17
- package/src/tools/pdf.ts +0 -164
- package/src/tools/shell.ts +0 -96
- package/src/tools/view-image.ts +0 -335
- package/src/tools/web.ts +0 -212
- package/src/types/chat.ts +0 -86
- package/src/types/config.ts +0 -20
- package/src/types/repo.ts +0 -54
- package/src/utils/addons/loadAddons.ts +0 -34
- package/src/utils/ai.ts +0 -321
- package/src/utils/chat.ts +0 -326
- package/src/utils/chatHistory.ts +0 -121
- package/src/utils/config.ts +0 -61
- package/src/utils/files.ts +0 -105
- package/src/utils/intentClassifier.ts +0 -58
- package/src/utils/lensfile.ts +0 -142
- package/src/utils/llm.ts +0 -81
- package/src/utils/memory.ts +0 -209
- package/src/utils/preview.ts +0 -119
- package/src/utils/stats.ts +0 -174
- package/src/utils/tools/builtins.ts +0 -377
- 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 "
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
}
|