Skip to content

Commit 513d395

Browse files
committed
feat(adapter): per-agent language instruction
Add optional 'language' config field on all local adapters (Claude, Codex, Cursor, Gemini, OpenCode, Pi). When set, a final instruction is appended to the rendered prompt telling the agent to produce all outputs in that language (responses, commit messages, explanations, etc.). - New buildLanguageInstruction() helper in adapter-utils/server-utils - Each adapter's execute.ts imports the helper and appends it to its joinPromptSections call - Each adapter's ui/build-config.ts passes v.language to adapterConfig - Add language field to CreateConfigValues (optional string) + default
1 parent 9a8d219 commit 513d395

15 files changed

Lines changed: 34 additions & 2 deletions

File tree

packages/adapter-utils/src/server-utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,12 @@ export function joinPromptSections(
263263
.join(separator);
264264
}
265265

266+
export function buildLanguageInstruction(config: Record<string, unknown>): string {
267+
const language = asString(config.language, "").trim();
268+
if (!language) return "";
269+
return `IMPORTANT: You MUST write all your responses, comments, commit messages, and any other output in ${language}. This includes explanations, summaries, and any text you produce.`;
270+
}
271+
266272
type PaperclipWakeIssue = {
267273
id: string | null;
268274
identifier: string | null;

packages/adapter-utils/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ export interface CreateConfigValues {
440440
worktreeParentDir?: string;
441441
runtimeServicesJson?: string;
442442
defaultEnvironmentId?: string;
443+
language?: string;
443444
maxTurnsPerRun: number;
444445
heartbeatEnabled: boolean;
445446
intervalSec: number;

packages/adapters/claude-local/src/server/execute.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
buildPaperclipEnv,
2828
readPaperclipRuntimeSkillEntries,
2929
joinPromptSections,
30+
buildLanguageInstruction,
3031
buildInvocationEnvForLogs,
3132
ensureAbsoluteDirectory,
3233
ensurePathInEnv,
@@ -489,12 +490,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
489490
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
490491
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
491492
const taskContextNote = asString(context.paperclipTaskMarkdown, "").trim();
493+
const languageInstruction = buildLanguageInstruction(config);
492494
const prompt = joinPromptSections([
493495
renderedBootstrapPrompt,
494496
wakePrompt,
495497
sessionHandoffNote,
496498
taskContextNote,
497499
renderedPrompt,
500+
languageInstruction,
498501
]);
499502
const promptMetrics = {
500503
promptChars: prompt.length,

packages/adapters/claude-local/src/ui/build-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export function buildClaudeLocalConfig(v: CreateConfigValues): Record<string, un
6767
if (v.cwd) ac.cwd = v.cwd;
6868
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
6969
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
70+
if (v.language) ac.language = v.language;
7071
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
7172
if (v.model) ac.model = v.model;
7273
if (v.thinkingEffort) ac.effort = v.thinkingEffort;

packages/adapters/codex-local/src/server/execute.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
stringifyPaperclipWakePayload,
3232
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
3333
joinPromptSections,
34+
buildLanguageInstruction,
3435
} from "@paperclipai/adapter-utils/server-utils";
3536
import {
3637
parseCodexJsonl,
@@ -243,7 +244,7 @@ export async function ensureCodexSkillsInjected(
243244
if (linkSkill) {
244245
await linkSkill(entry.source, target);
245246
} else {
246-
await fs.symlink(entry.source, target);
247+
await fs.symlink(entry.source, target, process.platform === "win32" ? "junction" : undefined);
247248
}
248249
await onLog(
249250
"stdout",
@@ -615,13 +616,15 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
615616
})();
616617
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
617618
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
619+
const languageInstruction = buildLanguageInstruction(config);
618620
const prompt = joinPromptSections([
619621
promptInstructionsPrefix,
620622
renderedBootstrapPrompt,
621623
wakePrompt,
622624
codexFallbackHandoffNote,
623625
sessionHandoffNote,
624626
renderedPrompt,
627+
languageInstruction,
625628
]);
626629
const promptMetrics = {
627630
promptChars: prompt.length,

packages/adapters/codex-local/src/ui/build-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export function buildCodexLocalConfig(v: CreateConfigValues): Record<string, unk
7171
if (v.cwd) ac.cwd = v.cwd;
7272
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
7373
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
74+
if (v.language) ac.language = v.language;
7475
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
7576
ac.model = v.model || DEFAULT_CODEX_LOCAL_MODEL;
7677
if (v.thinkingEffort) ac.modelReasoningEffort = v.thinkingEffort;

packages/adapters/cursor-local/src/server/execute.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
stringifyPaperclipWakePayload,
3838
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
3939
joinPromptSections,
40+
buildLanguageInstruction,
4041
} from "@paperclipai/adapter-utils/server-utils";
4142
import { DEFAULT_CURSOR_LOCAL_MODEL } from "../index.js";
4243
import { parseCursorJsonl, isCursorUnknownSessionError } from "./parse.js";
@@ -166,7 +167,7 @@ export async function ensureCursorSkillsInjected(
166167
`[paperclip] Removed maintainer-only Cursor skill "${skillName}" from ${skillsHome}\n`,
167168
);
168169
}
169-
const linkSkill = options.linkSkill ?? ((source: string, target: string) => fs.symlink(source, target));
170+
const linkSkill = options.linkSkill ?? ((source: string, target: string) => fs.symlink(source, target, process.platform === "win32" ? "junction" : undefined));
170171
for (const entry of skillsEntries) {
171172
const target = path.join(skillsHome, entry.runtimeName);
172173
try {
@@ -464,13 +465,15 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
464465
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
465466
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
466467
const paperclipEnvNote = renderPaperclipEnvNote(env);
468+
const languageInstruction = buildLanguageInstruction(config);
467469
const prompt = joinPromptSections([
468470
instructionsPrefix,
469471
renderedBootstrapPrompt,
470472
wakePrompt,
471473
sessionHandoffNote,
472474
paperclipEnvNote,
473475
renderedPrompt,
476+
languageInstruction,
474477
]);
475478
const promptMetrics = {
476479
promptChars: prompt.length,

packages/adapters/cursor-local/src/ui/build-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export function buildCursorLocalConfig(v: CreateConfigValues): Record<string, un
6262
if (v.cwd) ac.cwd = v.cwd;
6363
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
6464
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
65+
if (v.language) ac.language = v.language;
6566
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
6667
ac.model = v.model || DEFAULT_CURSOR_LOCAL_MODEL;
6768
const mode = normalizeMode(v.thinkingEffort);

packages/adapters/gemini-local/src/server/execute.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
ensureAbsoluteDirectory,
3131
ensurePaperclipSkillSymlink,
3232
joinPromptSections,
33+
buildLanguageInstruction,
3334
ensurePathInEnv,
3435
readPaperclipRuntimeSkillEntries,
3536
resolvePaperclipDesiredSkillNames,
@@ -408,6 +409,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
408409
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
409410
const paperclipEnvNote = renderPaperclipEnvNote(env);
410411
const apiAccessNote = renderApiAccessNote(env);
412+
const languageInstruction = buildLanguageInstruction(config);
411413
const prompt = joinPromptSections([
412414
instructionsPrefix,
413415
renderedBootstrapPrompt,
@@ -416,6 +418,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
416418
paperclipEnvNote,
417419
apiAccessNote,
418420
renderedPrompt,
421+
languageInstruction,
419422
]);
420423
const promptMetrics = {
421424
promptChars: prompt.length,

packages/adapters/gemini-local/src/ui/build-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export function buildGeminiLocalConfig(v: CreateConfigValues): Record<string, un
5656
if (v.cwd) ac.cwd = v.cwd;
5757
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
5858
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
59+
if (v.language) ac.language = v.language;
5960
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
6061
ac.model = v.model || DEFAULT_GEMINI_LOCAL_MODEL;
6162
ac.timeoutSec = 0;

0 commit comments

Comments
 (0)