@zhijiewang/openharness 2.22.1 → 2.24.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 +61 -1
- package/README.zh-CN.md +61 -1
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +19 -11
- package/dist/commands/session.js +5 -2
- package/dist/commands/settings.d.ts +1 -1
- package/dist/commands/settings.js +47 -6
- package/dist/harness/approvals.d.ts +45 -0
- package/dist/harness/approvals.js +100 -0
- package/dist/harness/config.d.ts +34 -0
- package/dist/harness/config.js +24 -0
- package/dist/harness/hooks.js +25 -1
- package/dist/harness/status-line-script.d.ts +52 -0
- package/dist/harness/status-line-script.js +88 -0
- package/dist/harness/trust.d.ts +42 -0
- package/dist/harness/trust.js +99 -0
- package/dist/query/tools.js +36 -0
- package/dist/renderer/cells.d.ts +6 -0
- package/dist/renderer/cells.js +48 -2
- package/dist/renderer/differ.js +18 -1
- package/dist/renderer/index.d.ts +11 -2
- package/dist/renderer/index.js +37 -4
- package/dist/renderer/input.js +7 -0
- package/dist/renderer/layout-sections.js +27 -5
- package/dist/renderer/layout.d.ts +8 -0
- package/dist/renderer/layout.js +5 -1
- package/dist/renderer/markdown.js +4 -1
- package/dist/repl.js +115 -11
- package/dist/tools/ExaSearchTool/index.d.ts +101 -0
- package/dist/tools/ExaSearchTool/index.js +165 -0
- package/dist/tools.js +2 -0
- package/dist/utils/fuzzy.d.ts +39 -0
- package/dist/utils/fuzzy.js +70 -0
- package/package.json +1 -1
package/dist/renderer/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Flushed messages flow to scrollback; live area is rewritten in-place
|
|
4
4
|
* right after the scrollback content each frame (no absolute positioning gap).
|
|
5
5
|
*/
|
|
6
|
+
import { recordApproval } from "../harness/approvals.js";
|
|
6
7
|
import { getTheme } from "../utils/theme-data.js";
|
|
7
8
|
import { summarizeToolArgs } from "../utils/tool-summary.js";
|
|
8
9
|
import { CellGrid } from "./cells.js";
|
|
@@ -62,6 +63,7 @@ export class TerminalRenderer {
|
|
|
62
63
|
questionPrompt: null,
|
|
63
64
|
autocomplete: [],
|
|
64
65
|
autocompleteDescriptions: [],
|
|
66
|
+
autocompleteCategories: [],
|
|
65
67
|
autocompleteIndex: -1,
|
|
66
68
|
manualScroll: 0,
|
|
67
69
|
codeBlocksExpanded: false,
|
|
@@ -195,9 +197,10 @@ export class TerminalRenderer {
|
|
|
195
197
|
this.state.bannerLines = lines;
|
|
196
198
|
this.scheduleRender();
|
|
197
199
|
}
|
|
198
|
-
setAutocomplete(suggestions, index, descriptions) {
|
|
200
|
+
setAutocomplete(suggestions, index, descriptions, categories) {
|
|
199
201
|
this.state.autocomplete = suggestions;
|
|
200
202
|
this.state.autocompleteDescriptions = descriptions ?? [];
|
|
203
|
+
this.state.autocompleteCategories = categories ?? [];
|
|
201
204
|
this.state.autocompleteIndex = index;
|
|
202
205
|
this.scheduleRender();
|
|
203
206
|
}
|
|
@@ -366,20 +369,48 @@ export class TerminalRenderer {
|
|
|
366
369
|
this.animationCallback = handler;
|
|
367
370
|
}
|
|
368
371
|
// ── Input routing ──
|
|
369
|
-
/**
|
|
372
|
+
/**
|
|
373
|
+
* Handle permission prompt keys (Y/N/A/D).
|
|
374
|
+
* - Y / N: approve or deny this single call.
|
|
375
|
+
* - A: approve AND persist a `toolPermissions: { tool, action: "allow" }`
|
|
376
|
+
* rule to `.oh/config.yaml` so future calls to this tool skip the prompt
|
|
377
|
+
* entirely (audit U-A2). Mirrors Claude Code's "yes, don't ask again".
|
|
378
|
+
* - D: toggle inline diff (when available).
|
|
379
|
+
*
|
|
380
|
+
* Returns true if key was consumed.
|
|
381
|
+
*/
|
|
370
382
|
handlePermissionKey(key) {
|
|
371
383
|
if (!this.permissionResolve)
|
|
372
384
|
return false;
|
|
373
385
|
const k = key.char.toLowerCase();
|
|
374
|
-
if (k === "y" || k === "n") {
|
|
386
|
+
if (k === "y" || k === "n" || k === "a") {
|
|
375
387
|
const resolve = this.permissionResolve;
|
|
388
|
+
const toolName = this.state.permissionBox?.toolName;
|
|
376
389
|
this.permissionResolve = null;
|
|
377
390
|
this.permissionPrompt = null;
|
|
378
391
|
this.state.permissionBox = null;
|
|
379
392
|
this.state.permissionDiffVisible = false;
|
|
380
393
|
this.state.permissionDiffInfo = null;
|
|
394
|
+
// Persist before resolving — any error in the write should not block
|
|
395
|
+
// the resolution. The persist call itself is no-op when no .oh/config.yaml
|
|
396
|
+
// exists (we don't auto-create on first interaction).
|
|
397
|
+
if (k === "a" && toolName) {
|
|
398
|
+
try {
|
|
399
|
+
// Lazy import to avoid pulling config into the renderer bundle
|
|
400
|
+
// for callers that don't trip the permission path.
|
|
401
|
+
const { appendToolPermission } = require("../harness/config.js");
|
|
402
|
+
appendToolPermission(toolName);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
/* persistence failure must not block the agent */
|
|
406
|
+
}
|
|
407
|
+
// Audit U-B5: log the "always allow this tool" rule promotion as a
|
|
408
|
+
// supplementary record so /permissions log shows the user upgraded
|
|
409
|
+
// from a one-shot allow to a persistent rule.
|
|
410
|
+
recordApproval({ tool: toolName, decision: "always", source: "user", cwd: process.cwd() });
|
|
411
|
+
}
|
|
381
412
|
this.scheduleRender();
|
|
382
|
-
resolve(k === "y");
|
|
413
|
+
resolve(k === "y" || k === "a");
|
|
383
414
|
}
|
|
384
415
|
else if (k === "d" && this.state.permissionDiffInfo) {
|
|
385
416
|
this.state.permissionDiffVisible = !this.state.permissionDiffVisible;
|
|
@@ -626,6 +657,8 @@ export class TerminalRenderer {
|
|
|
626
657
|
if (this.state.statusLine)
|
|
627
658
|
rows += 1;
|
|
628
659
|
rows += this.state.autocomplete.length;
|
|
660
|
+
// Audit U-A3: one row per distinct category header.
|
|
661
|
+
rows += new Set((this.state.autocompleteCategories ?? []).filter((c) => c && c.length > 0)).size;
|
|
629
662
|
if (this.state.permissionBox) {
|
|
630
663
|
rows += 3;
|
|
631
664
|
if (this.state.permissionDiffVisible && this.state.permissionDiffInfo)
|
package/dist/renderer/input.js
CHANGED
|
@@ -84,6 +84,13 @@ export function parseKey(data, offset) {
|
|
|
84
84
|
return { event: key("", "pageup", seq.slice(0, 4)), consumed: 4 };
|
|
85
85
|
if (seq.startsWith("\x1b[6~"))
|
|
86
86
|
return { event: key("", "pagedown", seq.slice(0, 4)), consumed: 4 };
|
|
87
|
+
// Shift+Tab (xterm "backtab"): ESC [ Z. Used as a quick-toggle for
|
|
88
|
+
// permission mode (mirrors Claude Code's Shift+Tab cycler).
|
|
89
|
+
if (seq.startsWith("\x1b[Z"))
|
|
90
|
+
return {
|
|
91
|
+
event: { char: "", name: "tab", ctrl: false, meta: false, shift: true, sequence: seq.slice(0, 3) },
|
|
92
|
+
consumed: 3,
|
|
93
|
+
};
|
|
87
94
|
// Shift+Arrow: ESC [ 1 ; 2 A/B/C/D
|
|
88
95
|
if (seq.startsWith("\x1b[1;2A"))
|
|
89
96
|
return {
|
|
@@ -187,7 +187,7 @@ export function renderToolCallsSection(state, grid, r, limit, opts) {
|
|
|
187
187
|
for (const line of visible) {
|
|
188
188
|
if (r >= limit)
|
|
189
189
|
break;
|
|
190
|
-
grid.
|
|
190
|
+
grid.writeTextWithLinks(r, 6, line.slice(0, w - 8), S_DIM, w - 2);
|
|
191
191
|
r++;
|
|
192
192
|
}
|
|
193
193
|
}
|
|
@@ -205,7 +205,7 @@ export function renderToolCallsSection(state, grid, r, limit, opts) {
|
|
|
205
205
|
if (r >= limit)
|
|
206
206
|
break;
|
|
207
207
|
const lineStyle = tc.status === "error" ? S_ERROR : S_DIM;
|
|
208
|
-
grid.
|
|
208
|
+
grid.writeTextWithLinks(r, 6, line.slice(0, w - 8), lineStyle, w - 2);
|
|
209
209
|
r++;
|
|
210
210
|
}
|
|
211
211
|
if (outLines.length > maxOut && r < limit) {
|
|
@@ -280,6 +280,13 @@ export function renderPermissionBoxSection(state, grid, nextRow, h, opts) {
|
|
|
280
280
|
kc += 1;
|
|
281
281
|
grid.writeText(nextRow, kc, "o", S_DIM);
|
|
282
282
|
kc += 1;
|
|
283
|
+
grid.writeText(nextRow, kc, " ", S_DIM);
|
|
284
|
+
kc += 2;
|
|
285
|
+
// Audit U-A2: "always allow this tool" — persists toolPermissions rule.
|
|
286
|
+
grid.writeText(nextRow, kc, "A", S_KEY_GREEN);
|
|
287
|
+
kc += 1;
|
|
288
|
+
grid.writeText(nextRow, kc, "lways", S_DIM);
|
|
289
|
+
kc += 5;
|
|
283
290
|
if (state.permissionDiffInfo) {
|
|
284
291
|
grid.writeText(nextRow, kc, " ", S_DIM);
|
|
285
292
|
kc += 2;
|
|
@@ -300,10 +307,12 @@ export function renderPermissionBoxSection(state, grid, nextRow, h, opts) {
|
|
|
300
307
|
grid.writeText(nextRow, 1, "Y", S_KEY_GREEN);
|
|
301
308
|
grid.writeText(nextRow, 2, "es ", S_DIM);
|
|
302
309
|
grid.writeText(nextRow, 6, "N", S_KEY_RED);
|
|
303
|
-
grid.writeText(nextRow, 7, "o", S_DIM);
|
|
310
|
+
grid.writeText(nextRow, 7, "o ", S_DIM);
|
|
311
|
+
grid.writeText(nextRow, 10, "A", S_KEY_GREEN);
|
|
312
|
+
grid.writeText(nextRow, 11, "lways", S_DIM);
|
|
304
313
|
if (state.permissionDiffInfo) {
|
|
305
|
-
grid.writeText(nextRow,
|
|
306
|
-
grid.writeText(nextRow,
|
|
314
|
+
grid.writeText(nextRow, 18, "D", S_KEY_CYAN);
|
|
315
|
+
grid.writeText(nextRow, 19, "iff", S_DIM);
|
|
307
316
|
}
|
|
308
317
|
nextRow++;
|
|
309
318
|
if (state.permissionDiffVisible && state.permissionDiffInfo && nextRow + 3 < h) {
|
|
@@ -375,7 +384,20 @@ export function renderAutocompleteSection(state, grid, nextRow, limit, promptWid
|
|
|
375
384
|
if (state.autocomplete.length === 0)
|
|
376
385
|
return nextRow;
|
|
377
386
|
const w = grid.width;
|
|
387
|
+
let lastCategory = "";
|
|
378
388
|
for (let ai = 0; ai < state.autocomplete.length; ai++) {
|
|
389
|
+
if (nextRow >= limit)
|
|
390
|
+
break;
|
|
391
|
+
// Category header — draw whenever the category changes between entries.
|
|
392
|
+
// First-entry header is drawn when the category is non-empty (audit U-A3).
|
|
393
|
+
const cat = state.autocompleteCategories?.[ai] ?? "";
|
|
394
|
+
if (cat && cat !== lastCategory) {
|
|
395
|
+
if (nextRow >= limit)
|
|
396
|
+
break;
|
|
397
|
+
grid.writeText(nextRow, promptWidth, `── ${cat} ──`, S_DIM);
|
|
398
|
+
nextRow++;
|
|
399
|
+
lastCategory = cat;
|
|
400
|
+
}
|
|
379
401
|
if (nextRow >= limit)
|
|
380
402
|
break;
|
|
381
403
|
const cmd = state.autocomplete[ai];
|
|
@@ -57,6 +57,14 @@ export type LayoutState = {
|
|
|
57
57
|
} | null;
|
|
58
58
|
autocomplete: string[];
|
|
59
59
|
autocompleteDescriptions: string[];
|
|
60
|
+
/**
|
|
61
|
+
* Optional category label per autocomplete entry (audit U-A3). When two
|
|
62
|
+
* adjacent entries differ in category, the renderer draws a header line
|
|
63
|
+
* before the second. Empty / missing category strings render flat (the
|
|
64
|
+
* pre-A3 behavior). Optional so older test fixtures + non-REPL callers
|
|
65
|
+
* don't need to thread an empty array.
|
|
66
|
+
*/
|
|
67
|
+
autocompleteCategories?: string[];
|
|
60
68
|
autocompleteIndex: number;
|
|
61
69
|
manualScroll: number;
|
|
62
70
|
codeBlocksExpanded: boolean;
|
package/dist/renderer/layout.js
CHANGED
|
@@ -28,7 +28,11 @@ export function rasterize(state, grid) {
|
|
|
28
28
|
const questionHeight = state.questionPrompt ? 4 + (state.questionPrompt.options?.length ?? 0) : 0;
|
|
29
29
|
const statusLineHeight = state.statusLine ? 1 : 0;
|
|
30
30
|
const contextWarningHeight = state.contextWarning ? 1 : 0;
|
|
31
|
-
|
|
31
|
+
// Autocomplete height — each entry is one row, plus one extra row per
|
|
32
|
+
// distinct category (audit U-A3 header lines). Distinct-category count
|
|
33
|
+
// is bounded by entry count so this stays cheap.
|
|
34
|
+
const distinctCategories = new Set((state.autocompleteCategories ?? []).filter((c) => c && c.length > 0));
|
|
35
|
+
const autocompleteHeight = state.autocomplete.length + distinctCategories.size;
|
|
32
36
|
const inputLineCount = Math.min(5, (state.inputText.match(/\n/g)?.length ?? 0) + 1);
|
|
33
37
|
const rawFooterHeight = Math.max(2 + inputLineCount + statusLineHeight + autocompleteHeight, companionHeight + 1) +
|
|
34
38
|
permissionHeight +
|
|
@@ -241,7 +241,10 @@ function parseInline(text, baseStyle) {
|
|
|
241
241
|
// Link: [text](url)
|
|
242
242
|
const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
|
|
243
243
|
if (linkMatch) {
|
|
244
|
-
segments.push({
|
|
244
|
+
segments.push({
|
|
245
|
+
text: linkMatch[1],
|
|
246
|
+
style: { ...baseStyle, underline: true, fg: "cyan", hyperlink: linkMatch[2] },
|
|
247
|
+
});
|
|
245
248
|
segments.push({ text: ` (${linkMatch[2]})`, style: { ...baseStyle, dim: true } });
|
|
246
249
|
remaining = remaining.slice(linkMatch[0].length);
|
|
247
250
|
continue;
|
package/dist/repl.js
CHANGED
|
@@ -14,8 +14,10 @@ import { readOhConfig, writeOhConfig } from "./harness/config.js";
|
|
|
14
14
|
import { estimateMessageTokens, getContextWarning } from "./harness/context-warning.js";
|
|
15
15
|
import { CostTracker, estimateCost, getContextWindow } from "./harness/cost.js";
|
|
16
16
|
import { createSession, loadSession, saveSession } from "./harness/session.js";
|
|
17
|
+
import { runStatusLineScript } from "./harness/status-line-script.js";
|
|
17
18
|
import { createStore } from "./harness/store.js";
|
|
18
19
|
import { handleUserInput } from "./harness/submit-handler.js";
|
|
20
|
+
import { isTrusted, trustSystemActive } from "./harness/trust.js";
|
|
19
21
|
import { query } from "./query/index.js";
|
|
20
22
|
import { resetDiffStyleCache } from "./renderer/diff.js";
|
|
21
23
|
import { TerminalRenderer } from "./renderer/index.js";
|
|
@@ -23,6 +25,7 @@ import { resetStyleCache } from "./renderer/layout.js";
|
|
|
23
25
|
import { resetMdStyleCache } from "./renderer/markdown.js";
|
|
24
26
|
import { createAssistantMessage, createInfoMessage, createMessage } from "./types/message.js";
|
|
25
27
|
import { formatTokenCount } from "./utils/format.js";
|
|
28
|
+
import { fuzzyFilter } from "./utils/fuzzy.js";
|
|
26
29
|
import { setActiveTheme } from "./utils/theme-data.js";
|
|
27
30
|
import { formatToolArgs, summarizeToolOutput } from "./utils/tool-summary.js";
|
|
28
31
|
export async function startREPL(config) {
|
|
@@ -105,6 +108,9 @@ export async function startREPL(config) {
|
|
|
105
108
|
let fastMode = s().fastMode;
|
|
106
109
|
let acSuggestions = s().acSuggestions;
|
|
107
110
|
let acDescriptions = s().acDescriptions;
|
|
111
|
+
// Audit U-A3: parallel category array for the picker. Local-only — no
|
|
112
|
+
// need to round-trip through `store` since no other consumer reads it.
|
|
113
|
+
let acCategories = [];
|
|
108
114
|
let acIndex = s().acIndex;
|
|
109
115
|
let acTokenStart = s().acTokenStart;
|
|
110
116
|
let acIsPath = s().acIsPath;
|
|
@@ -128,13 +134,15 @@ export async function startREPL(config) {
|
|
|
128
134
|
function updateAutocomplete() {
|
|
129
135
|
acIsPath = false;
|
|
130
136
|
if (inputText.startsWith("/") && inputText.length > 1 && !inputText.includes(" ")) {
|
|
131
|
-
// Slash command autocomplete
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
// Slash command autocomplete (audit U-B3): subsequence-match scoring,
|
|
138
|
+
// not a startsWith filter. Prefix matches still rank first via the
|
|
139
|
+
// bonus in `fuzzyScore`, but the user can type "gst" to surface
|
|
140
|
+
// "/git-status" or "perm" to surface "/permissions".
|
|
141
|
+
const query = inputText.slice(1);
|
|
142
|
+
const ranked = fuzzyFilter(query, getCommandEntries()).slice(0, 8);
|
|
143
|
+
acSuggestions = ranked.map((r) => r.entry.name);
|
|
144
|
+
acDescriptions = ranked.map((r) => r.entry.description);
|
|
145
|
+
acCategories = ranked.map((r) => r.entry.category);
|
|
138
146
|
acTokenStart = 0;
|
|
139
147
|
acIndex = -1;
|
|
140
148
|
}
|
|
@@ -176,26 +184,30 @@ export async function startREPL(config) {
|
|
|
176
184
|
return "";
|
|
177
185
|
}
|
|
178
186
|
});
|
|
187
|
+
acCategories = [];
|
|
179
188
|
acIsPath = acSuggestions.length > 0;
|
|
180
189
|
}
|
|
181
190
|
catch {
|
|
182
191
|
acSuggestions = [];
|
|
183
192
|
acDescriptions = [];
|
|
193
|
+
acCategories = [];
|
|
184
194
|
}
|
|
185
195
|
acIndex = -1;
|
|
186
196
|
}
|
|
187
197
|
else {
|
|
188
198
|
acSuggestions = [];
|
|
189
199
|
acDescriptions = [];
|
|
200
|
+
acCategories = [];
|
|
190
201
|
acIndex = -1;
|
|
191
202
|
}
|
|
192
203
|
}
|
|
193
204
|
else {
|
|
194
205
|
acSuggestions = [];
|
|
195
206
|
acDescriptions = [];
|
|
207
|
+
acCategories = [];
|
|
196
208
|
acIndex = -1;
|
|
197
209
|
}
|
|
198
|
-
renderer.setAutocomplete(acSuggestions, acIndex, acDescriptions);
|
|
210
|
+
renderer.setAutocomplete(acSuggestions, acIndex, acDescriptions, acCategories);
|
|
199
211
|
}
|
|
200
212
|
// Companion
|
|
201
213
|
let companionVisible = true;
|
|
@@ -263,8 +275,38 @@ export async function startREPL(config) {
|
|
|
263
275
|
const pct = Math.max(1, Math.ceil(usage * 100));
|
|
264
276
|
ctxStr = `ctx [${bar}] ${pct}%`;
|
|
265
277
|
}
|
|
266
|
-
//
|
|
267
|
-
|
|
278
|
+
// Resolution priority: script (audit U-B1) → template → default.
|
|
279
|
+
//
|
|
280
|
+
// Script path: spawn user-configured shell with a JSON envelope on
|
|
281
|
+
// stdin; gated through the workspace-trust system from audit U-A4 so
|
|
282
|
+
// a hostile project can't auto-execute on first launch. Cached for
|
|
283
|
+
// `refreshMs` (default 1s) inside `status-line-script.ts` so the
|
|
284
|
+
// script doesn't run on every keypress. Failure → fall through to
|
|
285
|
+
// the template / default below.
|
|
286
|
+
let scriptLine = null;
|
|
287
|
+
const sl = cachedConfig?.statusLine;
|
|
288
|
+
if (sl?.command) {
|
|
289
|
+
const cwd = process.cwd();
|
|
290
|
+
if (trustSystemActive() && !isTrusted(cwd)) {
|
|
291
|
+
scriptLine = null; // untrusted — silently skip; user can /trust
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
const ctxPct = ctxWindow > 0 && estimatedTokenCount > 0 ? estimatedTokenCount / ctxWindow : 0;
|
|
295
|
+
scriptLine = runStatusLineScript({
|
|
296
|
+
model: currentModel || "",
|
|
297
|
+
tokens: { input: inTok, output: outTok },
|
|
298
|
+
cost: totalCostVal,
|
|
299
|
+
contextPercent: ctxPct,
|
|
300
|
+
sessionId: session.id,
|
|
301
|
+
cwd,
|
|
302
|
+
gitBranch: session.gitBranch,
|
|
303
|
+
}, sl);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (scriptLine !== null) {
|
|
307
|
+
renderer.setStatusLine(scriptLine);
|
|
308
|
+
}
|
|
309
|
+
else if (cachedConfig?.statusLineFormat) {
|
|
268
310
|
const line = cachedConfig.statusLineFormat
|
|
269
311
|
.replace("{model}", currentModel || "")
|
|
270
312
|
.replace("{tokens}", tokensStr)
|
|
@@ -516,6 +558,14 @@ export async function startREPL(config) {
|
|
|
516
558
|
}
|
|
517
559
|
if (key.name === "pageup" || key.name === "pagedown" || key.name === "mouse")
|
|
518
560
|
return;
|
|
561
|
+
// Shift+Tab: cycle permission mode (audit U-A1). Mirrors Claude Code's
|
|
562
|
+
// quick-toggle. Cycles ask → acceptEdits → plan → trust → ask. The
|
|
563
|
+
// session-level mode is mutated on `config` so all downstream callers
|
|
564
|
+
// (`query()`, `cronExecutor`, status line) read the new value.
|
|
565
|
+
if (key.name === "tab" && key.shift) {
|
|
566
|
+
cyclePermissionMode();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
519
569
|
// Tab: autocomplete slash commands or file paths, or cycle tool call expansion
|
|
520
570
|
if (key.name === "tab" && !loading) {
|
|
521
571
|
if (acSuggestions.length > 0) {
|
|
@@ -533,7 +583,7 @@ export async function startREPL(config) {
|
|
|
533
583
|
}
|
|
534
584
|
renderer.setInputText(inputText);
|
|
535
585
|
renderer.setInputCursor(inputCursor);
|
|
536
|
-
renderer.setAutocomplete(acSuggestions, acIndex, acDescriptions);
|
|
586
|
+
renderer.setAutocomplete(acSuggestions, acIndex, acDescriptions, acCategories);
|
|
537
587
|
return;
|
|
538
588
|
}
|
|
539
589
|
renderer.cycleToolCallExpansion();
|
|
@@ -621,6 +671,26 @@ export async function startREPL(config) {
|
|
|
621
671
|
acIsPath,
|
|
622
672
|
});
|
|
623
673
|
});
|
|
674
|
+
/**
|
|
675
|
+
* Cycle the session permission mode (audit U-A1, Shift+Tab). The cycle
|
|
676
|
+
* intentionally covers the four interactive modes a user is likely to
|
|
677
|
+
* toggle between — `ask`, `acceptEdits`, `plan`, `trust`. The other modes
|
|
678
|
+
* (`deny`, `auto`, `bypassPermissions`) stay reachable via `/permissions
|
|
679
|
+
* <mode>` but aren't on the quick-cycle path because they're either
|
|
680
|
+
* destructive (`bypassPermissions`) or seldom-used.
|
|
681
|
+
*
|
|
682
|
+
* Mutates `config.permissionMode` directly so every existing read site
|
|
683
|
+
* (the `query()` call sites, `cronExecutor`, status hints) sees the new
|
|
684
|
+
* value without extra plumbing.
|
|
685
|
+
*/
|
|
686
|
+
function cyclePermissionMode() {
|
|
687
|
+
const cycle = ["ask", "acceptEdits", "plan", "trust"];
|
|
688
|
+
const idx = cycle.indexOf(config.permissionMode);
|
|
689
|
+
const next = cycle[(idx === -1 ? 0 : idx + 1) % cycle.length];
|
|
690
|
+
config.permissionMode = next;
|
|
691
|
+
messages.push(createInfoMessage(`Permission mode → ${next}`));
|
|
692
|
+
syncRenderer();
|
|
693
|
+
}
|
|
624
694
|
function navigateHistory(dir) {
|
|
625
695
|
if (dir < 0 && historyIndex < inputHistory.length - 1) {
|
|
626
696
|
historyIndex++;
|
|
@@ -1063,5 +1133,39 @@ export async function startREPL(config) {
|
|
|
1063
1133
|
renderer.start();
|
|
1064
1134
|
// Banner is already printed to stdout by main.tsx (visible in terminal scrollback)
|
|
1065
1135
|
syncRenderer();
|
|
1136
|
+
// Workspace-trust prompt (audit U-A4). Fires once per session when:
|
|
1137
|
+
// - the cwd isn't already on the trust list, AND
|
|
1138
|
+
// - `.oh/config.yaml` defines at least one shell-executing hook
|
|
1139
|
+
// (command/http) — `prompt` hooks don't trip the gate.
|
|
1140
|
+
// Untrusted cwd silently skips command/http hooks via the gate in
|
|
1141
|
+
// `harness/hooks.ts`. The prompt is non-blocking: we fire-and-forget
|
|
1142
|
+
// the askQuestion so the REPL stays responsive while the question is
|
|
1143
|
+
// displayed.
|
|
1144
|
+
void (async () => {
|
|
1145
|
+
try {
|
|
1146
|
+
const { isTrusted, trust } = await import("./harness/trust.js");
|
|
1147
|
+
if (isTrusted(process.cwd()))
|
|
1148
|
+
return;
|
|
1149
|
+
const cfgWithHooks = readOhConfig();
|
|
1150
|
+
const hooks = cfgWithHooks?.hooks;
|
|
1151
|
+
if (!hooks)
|
|
1152
|
+
return;
|
|
1153
|
+
const hasShellHook = Object.values(hooks).some((defs) => Array.isArray(defs) && defs.some((d) => d.command || d.http));
|
|
1154
|
+
if (!hasShellHook)
|
|
1155
|
+
return;
|
|
1156
|
+
const answer = await renderer.askQuestion(`Trust this workspace? Shell hooks are configured in ${process.cwd()}. (yes/no)`);
|
|
1157
|
+
if (answer.toLowerCase().startsWith("y")) {
|
|
1158
|
+
trust(process.cwd());
|
|
1159
|
+
messages.push(createInfoMessage(`Trusted ${process.cwd()} — shell hooks will now execute.`));
|
|
1160
|
+
}
|
|
1161
|
+
else {
|
|
1162
|
+
messages.push(createInfoMessage(`Workspace not trusted — shell hooks are silently skipped. Run /trust to grant.`));
|
|
1163
|
+
}
|
|
1164
|
+
syncRenderer();
|
|
1165
|
+
}
|
|
1166
|
+
catch {
|
|
1167
|
+
/* trust prompt is best-effort; never block the REPL */
|
|
1168
|
+
}
|
|
1169
|
+
})();
|
|
1066
1170
|
}
|
|
1067
1171
|
//# sourceMappingURL=repl.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Tool } from "../../Tool.js";
|
|
3
|
+
declare const SEARCH_TYPES: readonly ["auto", "neural", "fast", "keyword"];
|
|
4
|
+
declare const CATEGORIES: readonly ["company", "research paper", "news", "personal site", "financial report", "people"];
|
|
5
|
+
declare const inputSchema: z.ZodObject<{
|
|
6
|
+
query: z.ZodString;
|
|
7
|
+
num_results: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
type: z.ZodOptional<z.ZodEnum<["auto", "neural", "fast", "keyword"]>>;
|
|
9
|
+
category: z.ZodOptional<z.ZodEnum<["company", "research paper", "news", "personal site", "financial report", "people"]>>;
|
|
10
|
+
include_domains: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
11
|
+
exclude_domains: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
12
|
+
include_text: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
13
|
+
exclude_text: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
14
|
+
start_published_date: z.ZodOptional<z.ZodString>;
|
|
15
|
+
end_published_date: z.ZodOptional<z.ZodString>;
|
|
16
|
+
user_location: z.ZodOptional<z.ZodString>;
|
|
17
|
+
text: z.ZodOptional<z.ZodBoolean>;
|
|
18
|
+
highlights: z.ZodOptional<z.ZodBoolean>;
|
|
19
|
+
summary: z.ZodOptional<z.ZodBoolean>;
|
|
20
|
+
summary_query: z.ZodOptional<z.ZodString>;
|
|
21
|
+
max_text_chars: z.ZodOptional<z.ZodNumber>;
|
|
22
|
+
}, "strip", z.ZodTypeAny, {
|
|
23
|
+
query: string;
|
|
24
|
+
type?: "auto" | "fast" | "neural" | "keyword" | undefined;
|
|
25
|
+
text?: boolean | undefined;
|
|
26
|
+
summary?: boolean | undefined;
|
|
27
|
+
num_results?: number | undefined;
|
|
28
|
+
category?: "company" | "research paper" | "news" | "personal site" | "financial report" | "people" | undefined;
|
|
29
|
+
include_domains?: string[] | undefined;
|
|
30
|
+
exclude_domains?: string[] | undefined;
|
|
31
|
+
include_text?: string[] | undefined;
|
|
32
|
+
exclude_text?: string[] | undefined;
|
|
33
|
+
start_published_date?: string | undefined;
|
|
34
|
+
end_published_date?: string | undefined;
|
|
35
|
+
user_location?: string | undefined;
|
|
36
|
+
highlights?: boolean | undefined;
|
|
37
|
+
summary_query?: string | undefined;
|
|
38
|
+
max_text_chars?: number | undefined;
|
|
39
|
+
}, {
|
|
40
|
+
query: string;
|
|
41
|
+
type?: "auto" | "fast" | "neural" | "keyword" | undefined;
|
|
42
|
+
text?: boolean | undefined;
|
|
43
|
+
summary?: boolean | undefined;
|
|
44
|
+
num_results?: number | undefined;
|
|
45
|
+
category?: "company" | "research paper" | "news" | "personal site" | "financial report" | "people" | undefined;
|
|
46
|
+
include_domains?: string[] | undefined;
|
|
47
|
+
exclude_domains?: string[] | undefined;
|
|
48
|
+
include_text?: string[] | undefined;
|
|
49
|
+
exclude_text?: string[] | undefined;
|
|
50
|
+
start_published_date?: string | undefined;
|
|
51
|
+
end_published_date?: string | undefined;
|
|
52
|
+
user_location?: string | undefined;
|
|
53
|
+
highlights?: boolean | undefined;
|
|
54
|
+
summary_query?: string | undefined;
|
|
55
|
+
max_text_chars?: number | undefined;
|
|
56
|
+
}>;
|
|
57
|
+
type ExaContents = {
|
|
58
|
+
text?: boolean | {
|
|
59
|
+
maxCharacters?: number;
|
|
60
|
+
};
|
|
61
|
+
highlights?: boolean | {
|
|
62
|
+
maxCharacters?: number;
|
|
63
|
+
};
|
|
64
|
+
summary?: boolean | {
|
|
65
|
+
query?: string;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
type ExaRequest = {
|
|
69
|
+
query: string;
|
|
70
|
+
numResults: number;
|
|
71
|
+
type?: (typeof SEARCH_TYPES)[number];
|
|
72
|
+
category?: (typeof CATEGORIES)[number];
|
|
73
|
+
includeDomains?: string[];
|
|
74
|
+
excludeDomains?: string[];
|
|
75
|
+
includeText?: string[];
|
|
76
|
+
excludeText?: string[];
|
|
77
|
+
startPublishedDate?: string;
|
|
78
|
+
endPublishedDate?: string;
|
|
79
|
+
userLocation?: string;
|
|
80
|
+
contents?: ExaContents;
|
|
81
|
+
};
|
|
82
|
+
type ExaResultItem = {
|
|
83
|
+
id?: string;
|
|
84
|
+
url: string;
|
|
85
|
+
title?: string | null;
|
|
86
|
+
publishedDate?: string;
|
|
87
|
+
author?: string | null;
|
|
88
|
+
text?: string;
|
|
89
|
+
highlights?: string[];
|
|
90
|
+
summary?: string;
|
|
91
|
+
};
|
|
92
|
+
type ExaResponse = {
|
|
93
|
+
results: ExaResultItem[];
|
|
94
|
+
requestId?: string;
|
|
95
|
+
};
|
|
96
|
+
export declare function buildRequestBody(input: z.infer<typeof inputSchema>): ExaRequest;
|
|
97
|
+
export declare function extractSnippet(item: ExaResultItem): string;
|
|
98
|
+
export declare function formatResults(response: ExaResponse): string;
|
|
99
|
+
export declare const ExaSearchTool: Tool<typeof inputSchema>;
|
|
100
|
+
export {};
|
|
101
|
+
//# sourceMappingURL=index.d.ts.map
|