@ridit/lens 0.4.5 → 0.4.7
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 +27 -1
- package/dist/index.mjs +106 -23
- package/package.json +1 -1
- package/src/commands/chat.tsx +3 -0
- package/src/components/chat/ChatView.tsx +4 -0
- package/src/index.tsx +72 -17
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ lens chat --dev output structured JSON (for SDK/tooling
|
|
|
41
41
|
lens chat --single run one message then exit, resumes latest session
|
|
42
42
|
lens chat --force-all auto-approve all tools including writes and shell
|
|
43
43
|
lens chat --dev --prompt <text> headless mode: JSON output, no UI
|
|
44
|
+
lens chat --runtime-tools <path> load extra tools from a JSON file at runtime
|
|
44
45
|
|
|
45
46
|
lens provider configure AI providers (interactive)
|
|
46
47
|
lens provider --list list configured providers
|
|
@@ -90,7 +91,32 @@ Once inside a `lens chat` session, use slash commands:
|
|
|
90
91
|
|
|
91
92
|
## Extending Lens
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
### Runtime Tools
|
|
95
|
+
|
|
96
|
+
Pass a JSON file to `--runtime-tools` to inject custom tools into any chat session without modifying Lens itself. Each tool declares a name, description, optional parameters, and an HTTP endpoint that Lens will POST to when the AI calls it.
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
[
|
|
100
|
+
{
|
|
101
|
+
"name": "get_weather",
|
|
102
|
+
"description": "Returns current weather for a city",
|
|
103
|
+
"parameters": {
|
|
104
|
+
"city": { "type": "string", "description": "City name" }
|
|
105
|
+
},
|
|
106
|
+
"endpoint": "http://localhost:4242/get_weather"
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
lens chat --runtime-tools ./my-tools.json --prompt "What's the weather in London?"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Lens POSTs the tool arguments as JSON to the endpoint and returns the response body to the model. Runtime tools are always auto-approved in headless mode.
|
|
116
|
+
|
|
117
|
+
### SDK
|
|
118
|
+
|
|
119
|
+
Custom tools can also be built and registered using [`@ridit/lens-sdk`](https://www.npmjs.com/package/@ridit/lens-sdk).
|
|
94
120
|
|
|
95
121
|
## License
|
|
96
122
|
|
package/dist/index.mjs
CHANGED
|
@@ -68823,6 +68823,9 @@ function trimStartOfStream() {
|
|
|
68823
68823
|
}
|
|
68824
68824
|
var HANGING_STREAM_WARNING_TIME_MS = 15 * 1000;
|
|
68825
68825
|
|
|
68826
|
+
// ../core/src/agent/index.ts
|
|
68827
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
68828
|
+
|
|
68826
68829
|
// ../core/src/config/index.ts
|
|
68827
68830
|
import { join } from "path";
|
|
68828
68831
|
import { homedir as homedir2 } from "os";
|
|
@@ -74705,8 +74708,43 @@ var scrape = tool({
|
|
|
74705
74708
|
var tools = { read, write, bash, grep, ls, remember, del, search, scrape };
|
|
74706
74709
|
|
|
74707
74710
|
// ../core/src/agent/index.ts
|
|
74711
|
+
function buildZodSchema(parameters) {
|
|
74712
|
+
const shape = {};
|
|
74713
|
+
for (const [key, val] of Object.entries(parameters)) {
|
|
74714
|
+
let field;
|
|
74715
|
+
if (val.type === "number")
|
|
74716
|
+
field = exports_external.number();
|
|
74717
|
+
else if (val.type === "boolean")
|
|
74718
|
+
field = exports_external.boolean();
|
|
74719
|
+
else
|
|
74720
|
+
field = exports_external.string();
|
|
74721
|
+
if (val.description)
|
|
74722
|
+
field = field.describe(val.description);
|
|
74723
|
+
shape[key] = field;
|
|
74724
|
+
}
|
|
74725
|
+
return exports_external.object(shape);
|
|
74726
|
+
}
|
|
74708
74727
|
async function chat(options2) {
|
|
74709
|
-
|
|
74728
|
+
let extraTools = {};
|
|
74729
|
+
if (options2.runtimeTools && existsSync5(options2.runtimeTools)) {
|
|
74730
|
+
const raw = JSON.parse(readFileSync5(options2.runtimeTools, "utf-8"));
|
|
74731
|
+
for (const t of raw) {
|
|
74732
|
+
extraTools[t.name] = tool({
|
|
74733
|
+
description: t.description,
|
|
74734
|
+
parameters: buildZodSchema(t.parameters ?? {}),
|
|
74735
|
+
execute: async (args) => {
|
|
74736
|
+
const res = await fetch(t.endpoint, {
|
|
74737
|
+
method: "POST",
|
|
74738
|
+
headers: { "Content-Type": "application/json" },
|
|
74739
|
+
body: JSON.stringify(args)
|
|
74740
|
+
});
|
|
74741
|
+
return await res.json();
|
|
74742
|
+
}
|
|
74743
|
+
});
|
|
74744
|
+
}
|
|
74745
|
+
}
|
|
74746
|
+
const allTools = { ...tools, ...extraTools };
|
|
74747
|
+
const activeTools = options2.onBeforeToolCall ? Object.fromEntries(Object.entries(allTools).map(([name17, t]) => [
|
|
74710
74748
|
name17,
|
|
74711
74749
|
{
|
|
74712
74750
|
...t,
|
|
@@ -74717,7 +74755,7 @@ async function chat(options2) {
|
|
|
74717
74755
|
return t.execute(args, opts);
|
|
74718
74756
|
}
|
|
74719
74757
|
}
|
|
74720
|
-
])) :
|
|
74758
|
+
])) : allTools;
|
|
74721
74759
|
const responseMessages = [];
|
|
74722
74760
|
const providerSettings = (() => {
|
|
74723
74761
|
try {
|
|
@@ -74732,8 +74770,12 @@ async function chat(options2) {
|
|
|
74732
74770
|
messages: options2.messages,
|
|
74733
74771
|
system: options2.system,
|
|
74734
74772
|
maxSteps: options2.maxSteps ?? 50,
|
|
74735
|
-
...providerSettings?.maxTokens !== undefined && {
|
|
74736
|
-
|
|
74773
|
+
...providerSettings?.maxTokens !== undefined && {
|
|
74774
|
+
maxTokens: providerSettings.maxTokens
|
|
74775
|
+
},
|
|
74776
|
+
...providerSettings?.temperature !== undefined && {
|
|
74777
|
+
temperature: providerSettings.temperature
|
|
74778
|
+
},
|
|
74737
74779
|
onStepFinish: (step) => {
|
|
74738
74780
|
responseMessages.push(...step.response.messages);
|
|
74739
74781
|
for (const toolResult of step.toolResults) {
|
|
@@ -76188,7 +76230,8 @@ function ChatRunner({
|
|
|
76188
76230
|
initialMessage,
|
|
76189
76231
|
dev = false,
|
|
76190
76232
|
single = false,
|
|
76191
|
-
sessionId
|
|
76233
|
+
sessionId,
|
|
76234
|
+
runtimeTools
|
|
76192
76235
|
}) {
|
|
76193
76236
|
const [stage, setStage] = useState12("idle");
|
|
76194
76237
|
const [showProvider, setShowProvider] = useState12(false);
|
|
@@ -76310,6 +76353,7 @@ function ChatRunner({
|
|
|
76310
76353
|
await chat({
|
|
76311
76354
|
messages: getMessages(sessionRef.current),
|
|
76312
76355
|
system: getSystemPrompt(repoPath),
|
|
76356
|
+
runtimeTools,
|
|
76313
76357
|
onChunk: () => {},
|
|
76314
76358
|
onToolCall: (tool2, args) => {
|
|
76315
76359
|
devTools.push({ tool: tool2, args, result: null });
|
|
@@ -76359,6 +76403,7 @@ function ChatRunner({
|
|
|
76359
76403
|
await chat({
|
|
76360
76404
|
messages: getMessages(sessionRef.current),
|
|
76361
76405
|
system: getSystemPrompt(repoPath),
|
|
76406
|
+
runtimeTools,
|
|
76362
76407
|
onBeforeToolCall: (tool2, args) => {
|
|
76363
76408
|
if (forceApproveRef.current || SAFE_TOOLS.has(tool2))
|
|
76364
76409
|
return Promise.resolve(true);
|
|
@@ -76622,7 +76667,8 @@ function ChatCommand({
|
|
|
76622
76667
|
initialMessage,
|
|
76623
76668
|
dev = false,
|
|
76624
76669
|
single = false,
|
|
76625
|
-
sessionId
|
|
76670
|
+
sessionId,
|
|
76671
|
+
runtimeTools
|
|
76626
76672
|
}) {
|
|
76627
76673
|
return /* @__PURE__ */ jsxDEV15(Box12, {
|
|
76628
76674
|
flexDirection: "column",
|
|
@@ -76632,7 +76678,8 @@ function ChatCommand({
|
|
|
76632
76678
|
initialMessage,
|
|
76633
76679
|
dev,
|
|
76634
76680
|
single,
|
|
76635
|
-
sessionId
|
|
76681
|
+
sessionId,
|
|
76682
|
+
runtimeTools
|
|
76636
76683
|
}, undefined, false, undefined, this)
|
|
76637
76684
|
}, undefined, false, undefined, this);
|
|
76638
76685
|
}
|
|
@@ -78158,24 +78205,59 @@ function getLastDeniedAction(messages) {
|
|
|
78158
78205
|
async function runHeadless(opts) {
|
|
78159
78206
|
const repoPath = opts.path;
|
|
78160
78207
|
let session = opts.sessionId ? loadSession(opts.sessionId) ?? createSessionWithId(opts.sessionId, repoPath) : opts.single ? getLatestSession(repoPath) ?? createSession(repoPath) : createSession(repoPath);
|
|
78161
|
-
|
|
78162
|
-
|
|
78163
|
-
|
|
78164
|
-
|
|
78165
|
-
|
|
78208
|
+
if (opts.resume) {
|
|
78209
|
+
const msgs = getMessages(session);
|
|
78210
|
+
let trimAt = -1;
|
|
78211
|
+
for (let i = msgs.length - 1;i >= 0; i--) {
|
|
78212
|
+
const msg = msgs[i];
|
|
78213
|
+
if (!msg || msg.role !== "tool")
|
|
78214
|
+
continue;
|
|
78215
|
+
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
78216
|
+
const isDenied = content.some((p) => typeof p === "object" && p !== null && ("type" in p) && p.type === "tool-result" && ("result" in p) && typeof p.result === "string" && p.result.includes("Permission denied"));
|
|
78217
|
+
if (isDenied) {
|
|
78218
|
+
for (let j = i - 1;j >= 0; j--) {
|
|
78219
|
+
if (msgs[j]?.role === "assistant") {
|
|
78220
|
+
trimAt = j;
|
|
78221
|
+
break;
|
|
78222
|
+
}
|
|
78223
|
+
}
|
|
78224
|
+
break;
|
|
78225
|
+
}
|
|
78226
|
+
}
|
|
78227
|
+
if (trimAt >= 0) {
|
|
78228
|
+
session = { ...session, messages: msgs.slice(0, trimAt) };
|
|
78166
78229
|
}
|
|
78230
|
+
if (!opts.single || opts.sessionId)
|
|
78231
|
+
saveSession(session);
|
|
78232
|
+
} else {
|
|
78233
|
+
let prompt = opts.prompt;
|
|
78234
|
+
if (opts.forceAll && APPROVAL_WORDS.has(prompt.trim().toLowerCase())) {
|
|
78235
|
+
const pending = getLastDeniedAction(getMessages(session));
|
|
78236
|
+
if (pending) {
|
|
78237
|
+
prompt = `Proceed with the previously denied operation: use the ${pending.tool} tool on "${pending.description}".`;
|
|
78238
|
+
}
|
|
78239
|
+
}
|
|
78240
|
+
session = addMessage(session, "user", prompt);
|
|
78241
|
+
if (!opts.single || opts.sessionId)
|
|
78242
|
+
saveSession(session);
|
|
78167
78243
|
}
|
|
78168
|
-
session = addMessage(session, "user", prompt);
|
|
78169
|
-
if (!opts.single || opts.sessionId)
|
|
78170
|
-
saveSession(session);
|
|
78171
78244
|
const toolLog = [];
|
|
78172
78245
|
const denied = [];
|
|
78246
|
+
const runtimeToolNames = new Set;
|
|
78247
|
+
if (opts.runtimeTools) {
|
|
78248
|
+
try {
|
|
78249
|
+
const raw = JSON.parse(__require("fs").readFileSync(opts.runtimeTools, "utf-8"));
|
|
78250
|
+
if (Array.isArray(raw))
|
|
78251
|
+
raw.forEach((t) => runtimeToolNames.add(t.name));
|
|
78252
|
+
} catch {}
|
|
78253
|
+
}
|
|
78173
78254
|
await chat({
|
|
78174
78255
|
messages: getMessages(session),
|
|
78175
78256
|
system: getSystemPrompt(repoPath),
|
|
78257
|
+
runtimeTools: opts.runtimeTools,
|
|
78176
78258
|
maxSteps: opts.forceAll ? 50 : 2,
|
|
78177
78259
|
onBeforeToolCall: (tool2, args) => {
|
|
78178
|
-
if (opts.forceAll || HEADLESS_SAFE_TOOLS.has(tool2))
|
|
78260
|
+
if (opts.forceAll || HEADLESS_SAFE_TOOLS.has(tool2) || runtimeToolNames.has(tool2))
|
|
78179
78261
|
return Promise.resolve(true);
|
|
78180
78262
|
const a = args;
|
|
78181
78263
|
const description = tool2 === "bash" ? String(a.command ?? a.cmd ?? "") : tool2 === "write" ? String(a.path ?? a.file_path ?? "") : String(a.path ?? a.file_path ?? "");
|
|
@@ -78208,10 +78290,10 @@ async function runHeadless(opts) {
|
|
|
78208
78290
|
});
|
|
78209
78291
|
}
|
|
78210
78292
|
var program = new Command().enablePositionalOptions();
|
|
78211
|
-
program.command("chat").description("Chat with your codebase — ask questions or make changes").option("-p, --path <path>", "Path to the repo", ".").option("-d, --dev", "Output structured JSON (no UI)").option("--single", "Single-shot: run one message then exit").option("--session <id>", "Resume session by ID, or create one with that ID").option("--id <id>", "Alias for --session").option("--force-all", "Auto-approve all tools").option("--prompt <text>", "Run a prompt non-interactively").action((opts) => {
|
|
78293
|
+
program.command("chat").description("Chat with your codebase — ask questions or make changes").option("-p, --path <path>", "Path to the repo", ".").option("-d, --dev", "Output structured JSON (no UI)").option("--single", "Single-shot: run one message then exit").option("--session <id>", "Resume session by ID, or create one with that ID").option("--id <id>", "Alias for --session").option("--force-all", "Auto-approve all tools").option("--prompt <text>", "Run a prompt non-interactively").option("--resume", "Resume from last permission-denied tool call (no new prompt needed)").option("--runtime-tools <path>", "path to runtime tools JSON file").action((opts) => {
|
|
78212
78294
|
const sessionId = opts.session ?? opts.id;
|
|
78213
|
-
if (opts.prompt && (opts.dev || opts.single)) {
|
|
78214
|
-
runHeadless({ path: opts.path, prompt: opts.prompt, sessionId, single: opts.single, forceAll: opts.forceAll });
|
|
78295
|
+
if ((opts.prompt || opts.resume) && (opts.dev || opts.single)) {
|
|
78296
|
+
runHeadless({ path: opts.path, prompt: opts.prompt, sessionId, single: opts.single, forceAll: opts.forceAll ?? opts.resume, runtimeTools: opts.runtimeTools, resume: opts.resume });
|
|
78215
78297
|
return;
|
|
78216
78298
|
}
|
|
78217
78299
|
render(/* @__PURE__ */ jsxDEV21(ChatCommand, {
|
|
@@ -78220,7 +78302,8 @@ program.command("chat").description("Chat with your codebase — ask questions o
|
|
|
78220
78302
|
dev: opts.dev ?? false,
|
|
78221
78303
|
single: opts.single ?? false,
|
|
78222
78304
|
sessionId,
|
|
78223
|
-
initialMessage: opts.prompt
|
|
78305
|
+
initialMessage: opts.prompt,
|
|
78306
|
+
runtimeTools: opts.runtimeTools
|
|
78224
78307
|
}, undefined, false, undefined, this));
|
|
78225
78308
|
});
|
|
78226
78309
|
program.command("commit [files...]").description("Generate a smart conventional commit message from staged changes").option("-p, --path <path>", "Path to the repo", ".").option("--auto", "Stage all changes and commit without confirmation").option("--push", "Push to remote after committing").action((files, opts) => {
|
|
@@ -78308,13 +78391,13 @@ program.command("run <cmd>").description("Run your dev server. Lens watches and
|
|
|
78308
78391
|
});
|
|
78309
78392
|
var firstArg = process.argv[2];
|
|
78310
78393
|
if (!firstArg || firstArg.startsWith("-")) {
|
|
78311
|
-
const defaultFlags = new Command().option("-p, --path <path>", "Path to the repo", ".").option("--session <id>", "Resume session by ID").option("--single", "Single-shot mode").option("--prompt <text>", "Run a prompt").option("-d, --dev", "Output JSON (no UI)").option("--force-all", "Auto-approve all tools").allowUnknownOption().exitOverride();
|
|
78394
|
+
const defaultFlags = new Command().option("-p, --path <path>", "Path to the repo", ".").option("--session <id>", "Resume session by ID").option("--single", "Single-shot mode").option("--prompt <text>", "Run a prompt").option("-d, --dev", "Output JSON (no UI)").option("--force-all", "Auto-approve all tools").option("--resume", "Resume from last permission-denied tool call").allowUnknownOption().exitOverride();
|
|
78312
78395
|
try {
|
|
78313
78396
|
defaultFlags.parse(process.argv);
|
|
78314
78397
|
} catch {}
|
|
78315
78398
|
const opts = defaultFlags.opts();
|
|
78316
|
-
if (opts.prompt && (opts.dev || opts.single)) {
|
|
78317
|
-
runHeadless({ path: opts.path ?? ".", prompt: opts.prompt, sessionId: opts.session, single: opts.single, forceAll: opts.forceAll });
|
|
78399
|
+
if ((opts.prompt || opts.resume) && (opts.dev || opts.single)) {
|
|
78400
|
+
runHeadless({ path: opts.path ?? ".", prompt: opts.prompt, sessionId: opts.session, single: opts.single, forceAll: opts.forceAll ?? opts.resume, resume: opts.resume });
|
|
78318
78401
|
} else {
|
|
78319
78402
|
render(/* @__PURE__ */ jsxDEV21(ChatCommand, {
|
|
78320
78403
|
path: opts.path ?? ".",
|
package/package.json
CHANGED
package/src/commands/chat.tsx
CHANGED
|
@@ -9,6 +9,7 @@ export function ChatCommand({
|
|
|
9
9
|
dev = false,
|
|
10
10
|
single = false,
|
|
11
11
|
sessionId,
|
|
12
|
+
runtimeTools,
|
|
12
13
|
}: {
|
|
13
14
|
path: string;
|
|
14
15
|
autoForce?: boolean;
|
|
@@ -16,6 +17,7 @@ export function ChatCommand({
|
|
|
16
17
|
dev?: boolean;
|
|
17
18
|
single?: boolean;
|
|
18
19
|
sessionId?: string;
|
|
20
|
+
runtimeTools?: string;
|
|
19
21
|
}) {
|
|
20
22
|
return (
|
|
21
23
|
<Box flexDirection="column">
|
|
@@ -26,6 +28,7 @@ export function ChatCommand({
|
|
|
26
28
|
dev={dev}
|
|
27
29
|
single={single}
|
|
28
30
|
sessionId={sessionId}
|
|
31
|
+
runtimeTools={runtimeTools}
|
|
29
32
|
/>
|
|
30
33
|
</Box>
|
|
31
34
|
);
|
|
@@ -128,6 +128,7 @@ export function ChatRunner({
|
|
|
128
128
|
dev = false,
|
|
129
129
|
single = false,
|
|
130
130
|
sessionId,
|
|
131
|
+
runtimeTools,
|
|
131
132
|
}: {
|
|
132
133
|
repoPath: string;
|
|
133
134
|
autoForce?: boolean;
|
|
@@ -135,6 +136,7 @@ export function ChatRunner({
|
|
|
135
136
|
dev?: boolean;
|
|
136
137
|
single?: boolean;
|
|
137
138
|
sessionId?: string;
|
|
139
|
+
runtimeTools?: string;
|
|
138
140
|
}) {
|
|
139
141
|
const [stage, setStage] = useState<"idle" | "thinking">("idle");
|
|
140
142
|
const [showProvider, setShowProvider] = useState(false);
|
|
@@ -292,6 +294,7 @@ export function ChatRunner({
|
|
|
292
294
|
await chat({
|
|
293
295
|
messages: getMessages(sessionRef.current),
|
|
294
296
|
system: getSystemPrompt(repoPath),
|
|
297
|
+
runtimeTools,
|
|
295
298
|
onChunk: () => {},
|
|
296
299
|
onToolCall: (tool, args) => {
|
|
297
300
|
devTools.push({ tool, args, result: null });
|
|
@@ -350,6 +353,7 @@ export function ChatRunner({
|
|
|
350
353
|
await chat({
|
|
351
354
|
messages: getMessages(sessionRef.current),
|
|
352
355
|
system: getSystemPrompt(repoPath),
|
|
356
|
+
runtimeTools,
|
|
353
357
|
onBeforeToolCall: (tool, args) => {
|
|
354
358
|
if (forceApproveRef.current || SAFE_TOOLS.has(tool))
|
|
355
359
|
return Promise.resolve(true);
|
package/src/index.tsx
CHANGED
|
@@ -72,10 +72,12 @@ function getLastDeniedAction(messages: ReturnType<typeof getMessages>): { tool:
|
|
|
72
72
|
|
|
73
73
|
async function runHeadless(opts: {
|
|
74
74
|
path: string;
|
|
75
|
-
prompt
|
|
75
|
+
prompt?: string;
|
|
76
76
|
sessionId?: string;
|
|
77
77
|
single?: boolean;
|
|
78
78
|
forceAll?: boolean;
|
|
79
|
+
runtimeTools?: string;
|
|
80
|
+
resume?: boolean;
|
|
79
81
|
}) {
|
|
80
82
|
const repoPath = opts.path;
|
|
81
83
|
|
|
@@ -85,29 +87,75 @@ async function runHeadless(opts: {
|
|
|
85
87
|
? (getLatestSession(repoPath) ?? createSession(repoPath))
|
|
86
88
|
: createSession(repoPath);
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
if (opts.resume) {
|
|
91
|
+
// Rewind the session: strip the last denied tool-call (assistant message),
|
|
92
|
+
// its "Permission denied" tool result, and the assistant's text response after it.
|
|
93
|
+
// This leaves the session ending at the last successful state so the agent
|
|
94
|
+
// can re-attempt the denied tool with forceAll: true.
|
|
95
|
+
const msgs = getMessages(session);
|
|
96
|
+
let trimAt = -1;
|
|
97
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
98
|
+
const msg = msgs[i];
|
|
99
|
+
if (!msg || msg.role !== "tool") continue;
|
|
100
|
+
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
101
|
+
const isDenied = content.some(
|
|
102
|
+
(p: unknown) =>
|
|
103
|
+
typeof p === "object" && p !== null &&
|
|
104
|
+
"type" in p && (p as { type: string }).type === "tool-result" &&
|
|
105
|
+
"result" in p && typeof (p as { result: unknown }).result === "string" &&
|
|
106
|
+
((p as { result: string }).result).includes("Permission denied"),
|
|
107
|
+
);
|
|
108
|
+
if (isDenied) {
|
|
109
|
+
// Find the assistant message immediately before this tool result (the tool-call message)
|
|
110
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
111
|
+
if (msgs[j]?.role === "assistant") {
|
|
112
|
+
trimAt = j;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (trimAt >= 0) {
|
|
120
|
+
session = { ...session, messages: msgs.slice(0, trimAt) };
|
|
121
|
+
}
|
|
122
|
+
// Save trimmed session so context is clean for this run
|
|
123
|
+
if (!opts.single || opts.sessionId) saveSession(session);
|
|
124
|
+
} else {
|
|
125
|
+
// if user is approving a prior denial, make the intent unambiguous
|
|
126
|
+
let prompt = opts.prompt!;
|
|
127
|
+
if (opts.forceAll && APPROVAL_WORDS.has(prompt.trim().toLowerCase())) {
|
|
128
|
+
const pending = getLastDeniedAction(getMessages(session));
|
|
129
|
+
if (pending) {
|
|
130
|
+
prompt = `Proceed with the previously denied operation: use the ${pending.tool} tool on "${pending.description}".`;
|
|
131
|
+
}
|
|
94
132
|
}
|
|
95
|
-
}
|
|
96
133
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
134
|
+
session = addMessage(session, "user", prompt);
|
|
135
|
+
// save now so context is available on follow-up messages even if we exit early
|
|
136
|
+
if (!opts.single || opts.sessionId) saveSession(session);
|
|
137
|
+
}
|
|
100
138
|
|
|
101
139
|
const toolLog: { tool: string; args: unknown; result: unknown }[] = [];
|
|
102
140
|
const denied: { tool: string; description: string }[] = [];
|
|
103
141
|
|
|
142
|
+
// runtime tools are explicitly user-provided — always approve them
|
|
143
|
+
const runtimeToolNames = new Set<string>();
|
|
144
|
+
if (opts.runtimeTools) {
|
|
145
|
+
try {
|
|
146
|
+
const raw = JSON.parse(require("fs").readFileSync(opts.runtimeTools, "utf-8"));
|
|
147
|
+
if (Array.isArray(raw)) raw.forEach((t: { name: string }) => runtimeToolNames.add(t.name));
|
|
148
|
+
} catch { /* ignore parse errors */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
104
151
|
await chat({
|
|
105
152
|
messages: getMessages(session),
|
|
106
153
|
system: getSystemPrompt(repoPath),
|
|
154
|
+
runtimeTools: opts.runtimeTools,
|
|
107
155
|
// 2 steps: 1 tool attempt (or denial) + 1 text response
|
|
108
156
|
maxSteps: opts.forceAll ? 50 : 2,
|
|
109
157
|
onBeforeToolCall: (tool, args) => {
|
|
110
|
-
if (opts.forceAll || HEADLESS_SAFE_TOOLS.has(tool)) return Promise.resolve(true);
|
|
158
|
+
if (opts.forceAll || HEADLESS_SAFE_TOOLS.has(tool) || runtimeToolNames.has(tool)) return Promise.resolve(true);
|
|
111
159
|
// record denial — model will respond naturally explaining what it needs
|
|
112
160
|
const a = args as Record<string, unknown>;
|
|
113
161
|
const description =
|
|
@@ -159,6 +207,8 @@ program
|
|
|
159
207
|
.option("--id <id>", "Alias for --session")
|
|
160
208
|
.option("--force-all", "Auto-approve all tools")
|
|
161
209
|
.option("--prompt <text>", "Run a prompt non-interactively")
|
|
210
|
+
.option("--resume", "Resume from last permission-denied tool call (no new prompt needed)")
|
|
211
|
+
.option("--runtime-tools <path>", "path to runtime tools JSON file")
|
|
162
212
|
.action(
|
|
163
213
|
(opts: {
|
|
164
214
|
path: string;
|
|
@@ -168,11 +218,13 @@ program
|
|
|
168
218
|
id?: string;
|
|
169
219
|
forceAll?: boolean;
|
|
170
220
|
prompt?: string;
|
|
221
|
+
resume?: boolean;
|
|
222
|
+
runtimeTools?: string;
|
|
171
223
|
}) => {
|
|
172
224
|
const sessionId = opts.session ?? opts.id;
|
|
173
|
-
// headless: dev+prompt
|
|
174
|
-
if (opts.prompt && (opts.dev || opts.single)) {
|
|
175
|
-
runHeadless({ path: opts.path, prompt: opts.prompt, sessionId, single: opts.single, forceAll: opts.forceAll });
|
|
225
|
+
// headless: dev+prompt, single+prompt, or --resume → no UI, output JSON and exit
|
|
226
|
+
if ((opts.prompt || opts.resume) && (opts.dev || opts.single)) {
|
|
227
|
+
runHeadless({ path: opts.path, prompt: opts.prompt, sessionId, single: opts.single, forceAll: opts.forceAll ?? opts.resume, runtimeTools: opts.runtimeTools, resume: opts.resume });
|
|
176
228
|
return;
|
|
177
229
|
}
|
|
178
230
|
render(
|
|
@@ -183,6 +235,7 @@ program
|
|
|
183
235
|
single={opts.single ?? false}
|
|
184
236
|
sessionId={sessionId}
|
|
185
237
|
initialMessage={opts.prompt}
|
|
238
|
+
runtimeTools={opts.runtimeTools}
|
|
186
239
|
/>,
|
|
187
240
|
);
|
|
188
241
|
},
|
|
@@ -353,6 +406,7 @@ if (!firstArg || firstArg.startsWith("-")) {
|
|
|
353
406
|
.option("--prompt <text>", "Run a prompt")
|
|
354
407
|
.option("-d, --dev", "Output JSON (no UI)")
|
|
355
408
|
.option("--force-all", "Auto-approve all tools")
|
|
409
|
+
.option("--resume", "Resume from last permission-denied tool call")
|
|
356
410
|
.allowUnknownOption()
|
|
357
411
|
.exitOverride();
|
|
358
412
|
|
|
@@ -365,10 +419,11 @@ if (!firstArg || firstArg.startsWith("-")) {
|
|
|
365
419
|
prompt?: string;
|
|
366
420
|
dev?: boolean;
|
|
367
421
|
forceAll?: boolean;
|
|
422
|
+
resume?: boolean;
|
|
368
423
|
}>();
|
|
369
424
|
|
|
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 });
|
|
425
|
+
if ((opts.prompt || opts.resume) && (opts.dev || opts.single)) {
|
|
426
|
+
runHeadless({ path: opts.path ?? ".", prompt: opts.prompt, sessionId: opts.session, single: opts.single, forceAll: opts.forceAll ?? opts.resume, resume: opts.resume });
|
|
372
427
|
} else {
|
|
373
428
|
render(
|
|
374
429
|
<ChatCommand
|