Skip to content

Commit 7c6232e

Browse files
feat: 2.6.0 - custom instructions, prompt enhancements, migration fix
- Custom instructions: import instructions.md or paste; appended to system prompt - Protected directives prevent user instructions from overriding core rules - System prompt: step budget, search scaling, fiscal/quarter context polish - Tool orchestration: resolution model, MCP rules, completion conditions - Migration 0006: inceptionApiKey + customInstructions (fixes 2.4.0/2.5.0) - Settings: shrink-0 on icon containers, removed customInstructions toggle Made-with: Cursor
1 parent 33a96af commit 7c6232e

11 files changed

Lines changed: 912 additions & 246 deletions

File tree

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,42 @@ All notable changes to OpenSuiteMCP will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.6.0] - 2026-03-04
9+
10+
### ✨ Added
11+
12+
- **Custom Instructions (instructions.md)**
13+
- Settings → Custom Instructions: import an `instructions.md` file or paste content to add user-specific directives
14+
- Custom instructions are appended to the system prompt as "Additional User Instructions"
15+
- Protected core directives (tool completion, no fabrication, orchestration rules, Ava identity) cannot be overridden by user instructions
16+
17+
- **System prompt enhancements**
18+
- Refactored prompts for clearer identity, response rules, search triage, and tool orchestration
19+
- Intent-based search triage (by user need) instead of fixed priority
20+
- Step budget clarified: "Do not stop early unless the objective is satisfied" to reduce artificial tool usage
21+
- Search scaling: prefer 1–2 targeted searches; additional searches only when they address clearly distinct sub-topics and stay within the step budget
22+
- Fiscal/quarter-based queries: explicit guidance to derive period from provided date/time
23+
24+
- **Robust tool orchestration**
25+
- Resolution model (Fully Resolved, Partially Resolved, Blocked) with clearer decision sequencing
26+
- MCP rules: max 3 consecutive calls before alternating with search; alternating resets the count
27+
- Completion condition: stop only when objective satisfied, NetSuite operation completes, or system ends turn
28+
- Protected directives block user instructions from overriding safety and orchestration rules
29+
30+
### 🐛 Fixed
31+
32+
- **Migrations (inceptionApiKey)**
33+
- 2.4.0 added `inceptionApiKey` to the schema and journal but the migration file was never committed
34+
- 2.5.0 did not address this; users on 2.4.0/2.5.0 could encounter "column already exists" or missing-column errors
35+
- Migration `0006_illegal_sunfire` adds `inceptionApiKey` and `customInstructions` with `IF NOT EXISTS` for reliable fresh installs and upgrades
36+
37+
### 🧰 Technical
38+
39+
- New `customInstructions` column in UserSettings for user-provided prompt additions
40+
- `PROTECTED_DIRECTIVES` in prompts.ts enforces non-overridable core rules when custom instructions are present
41+
42+
---
43+
844
## [2.5.0] - 2026-03-03
945

1046
### ✨ Added
@@ -375,6 +411,7 @@ First stable release of OpenSuiteMCP - an open source, production-ready NetSuite
375411

376412
---
377413

414+
[2.6.0]: https://github.com/unstackedapps/opensuitemcp/releases/tag/v2.6.0
378415
[2.5.0]: https://github.com/unstackedapps/opensuitemcp/releases/tag/v2.5.0
379416
[2.4.0]: https://github.com/unstackedapps/opensuitemcp/releases/tag/v2.4.0
380417
[2.3.0]: https://github.com/unstackedapps/opensuitemcp/releases/tag/v2.3.0

app/api/chat/route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ export async function POST(request: Request) {
238238
let userTimezone = "UTC";
239239
let userMaxIterations = 10; // Default to 10
240240
let selectedSearchDomainIds: string[] = [];
241+
let customInstructions: string | null = null;
241242
if (session.user?.id) {
242243
try {
243244
const settings = await getUserSettings({
@@ -305,6 +306,9 @@ export async function POST(request: Request) {
305306
}
306307
userTimezone = settings.timezone ?? "UTC";
307308
selectedSearchDomainIds = settings.searchDomainIds ?? [];
309+
if (settings.customInstructions?.trim()) {
310+
customInstructions = settings.customInstructions;
311+
}
308312
} else {
309313
console.log(
310314
"[Settings] No settings found for user:",
@@ -420,6 +424,8 @@ export async function POST(request: Request) {
420424
netsuiteTools: netsuiteToolNames,
421425
timezone: userTimezone,
422426
enabledSearchToolNames: Object.keys(searchTools),
427+
maxSteps: userMaxIterations,
428+
additionalInstructions: customInstructions,
423429
});
424430
console.log(
425431
`[NetSuite] System prompt includes ${netsuiteToolNames.length} NetSuite tools`,

app/api/settings/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const settingsSchema = z.object({
2727
})
2828
.optional()
2929
.nullable(),
30+
customInstructions: z.string().max(32_000).optional().nullable(),
3031
});
3132

3233
export async function GET() {
@@ -59,6 +60,7 @@ export async function GET() {
5960
timezone: "UTC",
6061
searchDomainIds: [],
6162
maxIterations: "10",
63+
customInstructions: null,
6264
});
6365
}
6466

@@ -147,6 +149,7 @@ export async function GET() {
147149
timezone: settings.timezone ?? "UTC",
148150
searchDomainIds: settings.searchDomainIds ?? [],
149151
maxIterations: settings.maxIterations ?? "10",
152+
customInstructions: settings.customInstructions ?? null,
150153
};
151154

152155
console.log("[Settings API] Sending response:", {
@@ -302,6 +305,7 @@ export async function POST(request: Request) {
302305
? (validated.searchDomainIds ?? [])
303306
: undefined,
304307
maxIterations: validated.maxIterations,
308+
customInstructions: validated.customInstructions,
305309
});
306310

307311
return NextResponse.json({

components/icons.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ export const RouteIcon = ({ size = 16 }: { size?: number }) => {
274274
export const FileIcon = ({ size = 16 }: { size?: number }) => {
275275
return (
276276
<svg
277+
data-testid="geist-icon"
277278
height={size}
278279
strokeLinejoin="round"
279280
style={{ color: "currentcolor" }}

components/settings-modal.tsx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
CloudIcon,
1010
EyeIcon,
1111
EyeOffIcon,
12+
FileIcon,
1213
GlobeIcon,
1314
LoaderIcon,
1415
SparklesIcon,
@@ -41,6 +42,7 @@ import {
4142
} from "@/components/ui/sheet";
4243
import { Skeleton } from "@/components/ui/skeleton";
4344
import { Switch } from "@/components/ui/switch";
45+
import { Textarea } from "@/components/ui/textarea";
4446
// Tabs removed - consolidated into single pane
4547
import { getSearchDomainUrl, searchDomains } from "@/lib/ai/search-domains";
4648
import { guestRegex } from "@/lib/constants";
@@ -88,6 +90,7 @@ async function fetchSettings() {
8890
timezone: string;
8991
searchDomainIds: string[];
9092
maxIterations: string;
93+
customInstructions: string | null;
9194
};
9295
} catch (error) {
9396
console.error("[Settings] Error in fetchSettings:", error);
@@ -260,6 +263,9 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
260263
const [searchDomainIds, setSearchDomainIds] = useState<string[]>([]);
261264
const [maxIterations, setMaxIterations] = useState("10");
262265
const maxIterationsId = useId();
266+
const [customInstructions, setCustomInstructions] = useState("");
267+
const customInstructionsFileInputId = useId();
268+
const customInstructionsTextareaId = useId();
263269
const [isConnectingNetSuite, setIsConnectingNetSuite] = useState(false);
264270
const searchInputRef = useRef<HTMLInputElement>(null);
265271
const initializedForThisOpenRef = useRef(false);
@@ -330,6 +336,10 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
330336
setTimezone(settings.timezone ?? "UTC");
331337
setSearchDomainIds(settings.searchDomainIds ?? []);
332338
setMaxIterations(settings.maxIterations ?? "10");
339+
setCustomInstructions(
340+
(settings as { customInstructions?: string | null })
341+
?.customInstructions ?? "",
342+
);
333343
initializedForThisOpenRef.current = true;
334344
} else {
335345
console.warn("[Settings] Settings object invalid:", settings);
@@ -363,6 +373,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
363373
timezone: timezone?.trim() || "UTC",
364374
searchDomainIds: effectiveSearchDomainIds,
365375
maxIterations: maxIterations?.trim() || "10",
376+
customInstructions: customInstructions?.trim() || null,
366377
};
367378

368379
const response = await fetch("/api/settings", {
@@ -1068,6 +1079,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
10681079
<div
10691080
className={cn(
10701081
"flex",
1082+
"shrink-0",
10711083
"items-center",
10721084
"justify-center",
10731085
"rounded-md",
@@ -1143,6 +1155,111 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
11431155
)}
11441156
</CardContent>
11451157
</Card>
1158+
1159+
{/* Custom Instructions */}
1160+
<Card className="bg-background shadow-none">
1161+
<CardHeader className="py-6">
1162+
<div className="flex items-start justify-between gap-3">
1163+
<div className="flex items-start gap-3">
1164+
<div
1165+
className={cn(
1166+
"flex",
1167+
"shrink-0",
1168+
"items-center",
1169+
"justify-center",
1170+
"rounded-md",
1171+
"bg-muted",
1172+
"text-primary",
1173+
"size-10",
1174+
"mt-0.5",
1175+
)}
1176+
>
1177+
<FileIcon size={20} />
1178+
</div>
1179+
<div>
1180+
<CardTitle className="text-base">
1181+
Custom Instructions
1182+
</CardTitle>
1183+
<p className="text-muted-foreground text-sm">
1184+
Add your own instructions (e.g. from{" "}
1185+
<code className="rounded bg-muted px-1 py-0.5 text-xs">
1186+
instructions.md
1187+
</code>
1188+
) to tailor how Ava responds. These are appended to
1189+
the system prompt as additional user instructions.
1190+
</p>
1191+
</div>
1192+
</div>
1193+
</div>
1194+
</CardHeader>
1195+
<CardContent className="pt-0">
1196+
{showSkeletons ? (
1197+
<Skeleton className="h-32 w-full" />
1198+
) : (
1199+
<div className="space-y-2">
1200+
<div className="flex items-center gap-2">
1201+
<Label
1202+
className="sr-only"
1203+
htmlFor={customInstructionsFileInputId}
1204+
>
1205+
Import instructions.md file
1206+
</Label>
1207+
<Input
1208+
accept=".md"
1209+
aria-label="Import instructions.md file"
1210+
className="hidden"
1211+
id={customInstructionsFileInputId}
1212+
onChange={(e) => {
1213+
const file = e.target.files?.[0];
1214+
if (!file) return;
1215+
const reader = new FileReader();
1216+
reader.onload = () => {
1217+
const text = reader.result;
1218+
if (typeof text === "string") {
1219+
setCustomInstructions(text);
1220+
toast({
1221+
type: "success",
1222+
description: `Imported ${file.name}`,
1223+
});
1224+
}
1225+
};
1226+
reader.readAsText(file, "UTF-8");
1227+
e.target.value = "";
1228+
}}
1229+
type="file"
1230+
/>
1231+
<Button
1232+
onClick={() =>
1233+
document
1234+
.getElementById(customInstructionsFileInputId)
1235+
?.click()
1236+
}
1237+
type="button"
1238+
variant="outline"
1239+
>
1240+
Import instructions.md
1241+
</Button>
1242+
</div>
1243+
<Label
1244+
className="sr-only"
1245+
htmlFor={customInstructionsTextareaId}
1246+
>
1247+
Custom instructions content
1248+
</Label>
1249+
<Textarea
1250+
aria-label="Custom instructions content"
1251+
className="min-h-[120px] font-mono text-sm"
1252+
id={customInstructionsTextareaId}
1253+
onChange={(e) =>
1254+
setCustomInstructions(e.target.value)
1255+
}
1256+
placeholder="Paste or import your custom instructions (Markdown supported)..."
1257+
value={customInstructions}
1258+
/>
1259+
</div>
1260+
)}
1261+
</CardContent>
1262+
</Card>
11461263
</>
11471264
)}
11481265

0 commit comments

Comments
 (0)